這一年來我常在 Azure 開小 VM 架網站做實驗,中小型 ASP.NET Core 架在 Linux Docker 用便宜的 B1s VM綽綽有餘,若有 Visual Studio Enterprise 訂閱或 Azure 學生方案,可以開一堆小 VM 玩到開心。

時至今日,網站走 HTTPS 已是基本要求,我習慣的做法是安裝 Nginx + Certbot 整合 Docker 容器,自動申請 Let's Encrypt TLS 憑證,憑證到期還會自動更新,Fire ad Forget 就是爽。

每次安裝新 VM 都要重覆一長串相同安裝程序讓人心煩,後來我想出 HTTPS Nginx Docker 之懶人安裝法,寫好自動安裝腳本放在 Github,裝好 VM 用 SSH 登入,下指令從 Github 下載腳本執行,喝口茶等上兩三分鐘,一台可以跑 HTTPS 網站的 Docker 主機就準備好了。而這套安裝程序還遺留幾處手動操作:在 Azure Portal 建立 VM、開 80/443 Port 防火牆、SSH 登入貼指令執行。幸好 Azure 所有管理操作都能改用指令完成,並有 Azure CLI 與 Azure PowerShell 兩種選擇,我個人偏好 Azure CLI (延伸閱讀:敲指令管 Azure,該用 Azure CLI 還是 Azure PowerShell?),今天就來舖完這最後一哩路,將整套安裝程序寫成批次指令,實現下一行指令建好 Azure VM 跑 HTTPS 網站!

開始前推薦幾篇文章:

建 VM、設防火牆等動作很單純,已有現成 Azure CLI 指令。比較複雜的部分是用程式在 VM 上跑 Bash 腳本安裝 Docker Engine 設定 Nginx/Certbot 並建立容器。

Azure CLI 提供兩種在 VM 執行指令的方式:Action RunCommand 與 Managed RunCommand,前者偏向一次性以系統管理者權限執行,後者可包進 ARM 範本、指定執行身分、平行及反覆執行。二者比較如下:


來源

評估後,我選擇較單純的 Action RunCommand,用一行 az vm run-command invoke 解決。以下是個簡單示範,用 az vm run-command invoke --command-id RunShellScript -n B1sVM -g B1sVM_group --scripts "cat /etc/os-release",在回傳 JSON 資料可看到執行結果,也可串接 PowerShell ConvertFrom-Json 擷取內容:

原本的構想是沿用上次做好的 install.sh輕鬆秒殺,但遇到 (TERM is not set, so the dialog frontend is not usable.) 錯誤,研究後發現這個問題也好發於 Docker 製作 Image 過程,apt-get 安裝過程預設會顯示下載進度,在背景非互動環境執行時會出錯,解決方法是設定 DEBIAN_FRONTEND 環境變數或加上 -qq 參數(除非發生錯誤,停用所有訊息輸出),後者感覺較乾淨俐落。由於 install.sh 有多處需要配合背景執行進行修改,我將程式碼改搬進 PowerShell 中,再用點技巧以 && 串成一行指令,最後成品如下:

param(
    [Parameter(Mandatory = $true)]
    [string]$ResourceGroupName,
    [Parameter(Mandatory = $true)]
    [string]$VMName,
    [Parameter(Mandatory = $true)]
    [string]$DnsName,
    [string]$UserName = 'jeffrey',
    [string]$Location = 'eastasia',
    [string]$Email = '<Email address for Let's Encrypt>'
)

# 檢查資源群組是否存在,不存在則建立之
$exist = az group exists -n $ResourceGroupName
if ($exist -eq 'false') {
    az group create -n $ResourceGroupName -l $Location
}

# 輸入密碼
function InputPwd([string]$prompt) {
    $passwd = Read-Host -Prompt $prompt -AsSecureString
    return [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($passwd))
}
$pass = $false
while (-not $pass) {
    $passwd = InputPwd "輸入密碼"
    $pwdConfirm = InputPwd "密碼確認"
    if ($passwd -eq $pwdConfirm) {
        $pass = $true
    } else {
        Write-Host "密碼不一致,請重新輸入"
    }
} 

# 建立 Debian 11 VM
az vm create -g $ResourceGroupName -n $VMName --public-ip-sku Standard --image Debian11 --admin-username $UserName --admin-password $passwd --size Standard_B1s

# 指定對外 DNS 名稱
if (-not $DnsName) {
    $DnsName = $VMName
}
az network public-ip update -g $ResourceGroupName -n "${VMName}PublicIP" --dns-name $DnsName
$fqdn = "$DnsName.$Location.cloudapp.azure.com"

# 啟用 80 及 443 port
az vm open-port -g $ResourceGroupName -n $VMName --port 80 --priority 300
az vm open-port -g $ResourceGroupName -n $VMName --port 443 --priority 310

$script = @"
# install docker
apt-get -y -qq install ca-certificates curl wget gnupg lsb-release
mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=`$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian `$(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
chmod a+r /etc/apt/keyrings/docker.gpg
apt-get -qq update
apt-get -y -qq install docker-ce docker-ce-cli containerd.io docker-compose-plugin
curl -s https://api.github.com/repos/docker/compose/releases/latest | grep browser_download_url  | grep docker-compose-linux-x86_64 | cut -d '"' -f 4 | wget -qi -
chmod +x docker-compose-linux-x86_64
mv docker-compose-linux-x86_64 /usr/local/bin/docker-compose
usermod -aG docker "$UserName"
systemctl enable docker
# download docker images
docker pull -q staticfloat/nginx-certbot
docker pull -q mcr.microsoft.com/dotnet/samples:aspnetapp
# download /etc/nginx conf files
mkdir /etc/nginx
mkdir /etc/nginx/conf.d
curl -s -o /etc/nginx/nginx.conf https://raw.githubusercontent.com/darkthread/nginx-certbot-docker-nstaller/master/etc/nginx/nginx.conf
curl -s -o /etc/nginx/conf.d/00.default.conf https://raw.githubusercontent.com/darkthread/nginx-certbot-docker-nstaller/master/etc/nginx/conf.d/00.default.conf
curl -s -o /etc/nginx/conf.d/01.aspnetcore.conf https://raw.githubusercontent.com/darkthread/nginx-certbot-docker-nstaller/master/etc/nginx/conf.d/01.aspnetcore.conf
sed -i "s/@fqdn/$fqdn/g" /etc/nginx/conf.d/01.aspnetcore.conf
# copy docker-compose.yml to /home/dockers/nginx-certbot
mkdir -p "/home/dockers/nginx-certbot"
cd "/home/dockers/nginx-certbot"
curl -s -O https://raw.githubusercontent.com/darkthread/nginx-certbot-docker-nstaller/master/dockers/nginx-certbot/docker-compose.yml
sed -i "s/@email/$email/g" docker-compose.yml
# copy docker-compose.yml to /home/dockers/aspnetcore
mkdir -p "/home/dockers/aspnetcore"
cd "/home/dockers/aspnetcore"
curl -s -O https://raw.githubusercontent.com/darkthread/nginx-certbot-docker-nstaller/master/dockers/aspnetcore/docker-compose.yml
# start docker containers
cd "/home/dockers/aspnetcore"
docker-compose up -d
cd "/home/dockers/nginx-certbot"
docker-compose up -d
"@
$lines =  $script.Replace("`r", '').Split("`n") | Where-Object { !$_.StartsWith('#') }
$cmd = ($lines -join ' && ').Replace('"', '""')

# 安裝 Nginx + Certbot Docker
& az vm run-command invoke -g $ResourceGroupName -n $VMName --command-id RunShellScript --scripts "`"$cmd`""

將檔案存成 Az-CreateDebianVM.ps1,未來隨時想建台 VM 玩玩 Web,只需 Az-CreateDebianVM.ps1 TestGroup TestVM my-test-web,輸入管理者密碼,等待...

差不多泡一碗泡麵的時間(約五分鐘),便能連上 http:// my-test-web.eastasia.cloundapp.azure.com 驗收成果囉~ 成功。

提醒:安裝程序未包含只開放特定 IP 存取 22 Port,要提升安全性可參考這篇 批次更新 Azure VM 之 RDP/SSH 開放來源 IP

Example of PowerShell script to create a Azure Debian VM with Nginx+cerbot docker installed and configured in one command line.


Comments

Be the first to post a comment

Post a comment