<?xml version="1.0" encoding="utf-8"?><rss version="2.0"><channel><title>Edi Wang</title><description>Microsoft MVP for Azure</description><link>https://edi.wang/</link><pubDate>2026-11-06T15:27:52Z</pubDate><copyright>© 2009 - 2026 edi.wang</copyright><generator>Moonglade v16.9.0-rc.1</generator><language>en-US</language><item><title>Update Files in Azure File Share on Schedule with Azure Container App Jobs</title><link>https://edi.wang/post/2026/2/23/update-files-in-azure-file-share-on-schedule-with-azure-container-app-jobs</link><description>&lt;h2&gt;Problem&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;I have a containerized app that relies on a data file mounted at runtime, an IP lookup API that reads &lt;code&gt;qqwry.dat&lt;/code&gt; file. I deploy the App on Azure Container Apps, and use Azure File Share to mount the data directory for the app to use. One problem remains: How to keep the file updated automatically? e.g., to check for updates daily without manual replacing the file on Azure Storage Account.&lt;/p&gt;
&lt;p&gt;In this post, I will introduce how to use Azure Container Apps Jobs to run a small "updater container" on a schedule. The updater downloads the latest file and writes it into the mounted file share, while the main API container reads the file from the same mount path.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;hr&gt;
&lt;h3&gt;1. Build an updater container image&lt;/h3&gt;
&lt;p&gt;The updater's only job is to download the latest database and atomically replace the file in &lt;code&gt;/data&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For this example, the source is a fixed URL with no auth: &lt;a href="https://github.com/metowolf/qqwry.dat/releases/latest/download/qqwry.dat" target="_blank" rel="noopener"&gt;https://github.com/metowolf/qqwry.dat/releases/latest/download/qqwry.dat&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;update.sh&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="language-shell"&gt;&lt;code&gt;#!/usr/bin/env bash
set -euo pipefail

# Fixed download URL (can also be overridden via environment variable)
QQWRY_URL="${QQWRY_URL:-https://github.com/metowolf/qqwry.dat/releases/latest/download/qqwry.dat}"

DATA_DIR="${DATA_DIR:-/data}"
TARGET_NAME="${TARGET_NAME:-qqwry.dat}"
USER_AGENT="${USER_AGENT:-qqwry-updater/1.0}"
LOCK_FILE="${LOCK_FILE:-$DATA_DIR/.update.lock}"

mkdir -p "$DATA_DIR"

TARGET_PATH="$DATA_DIR/$TARGET_NAME"
TMP_DIR="$(mktemp -d)"
cleanup() { rm -rf "$TMP_DIR"; }
trap cleanup EXIT

log() { echo "[$(date -u +'%Y-%m-%dT%H:%M:%SZ')] $*"; }

sha256_file() {
  sha256sum "$1" | awk '{print $1}'
}

# Use a lock to prevent concurrent writes from corrupting the file
exec 200&amp;gt;"$LOCK_FILE"
flock -n 200 || { log "Another update is running, exit."; exit 0; }

log "Downloading: $QQWRY_URL"
RAW="$TMP_DIR/qqwry.dat"
curl -fsSL --retry 5 --retry-delay 2 -A "$USER_AGENT" "$QQWRY_URL" -o "$RAW"

# Basic validation: non-empty and reasonable size (qqwry database is generally much larger than 100KB)
SIZE=$(stat -c%s "$RAW" 2&amp;gt;/dev/null || stat -f%z "$RAW")
if [[ "$SIZE" -lt 102400 ]]; then
  log "ERROR: downloaded file too small ($SIZE bytes), abort."
  exit 3
fi

NEW_SHA="$(sha256_file "$RAW")"
log "New sha256=$NEW_SHA size=$SIZE"

# If the existing file exists and has the same hash, skip replacement
if [[ -f "$TARGET_PATH" ]]; then
  OLD_SHA="$(sha256_file "$TARGET_PATH" || true)"
  if [[ "$OLD_SHA" == "$NEW_SHA" ]]; then
    log "No change (sha256 same)."
    date -u +'%Y-%m-%dT%H:%M:%SZ' &amp;gt; "$DATA_DIR/$TARGET_NAME.updated_at"
    echo "$NEW_SHA" &amp;gt; "$DATA_DIR/$TARGET_NAME.sha256"
    echo "${NEW_SHA:0:12}" &amp;gt; "$DATA_DIR/$TARGET_NAME.version"
    exit 0
  fi
fi

# Atomic replacement (write to a temp file first, then mv)
TMP_TARGET="$DATA_DIR/.${TARGET_NAME}.tmp"
cp -f "$RAW" "$TMP_TARGET"
sync || true
mv -f "$TMP_TARGET" "$TARGET_PATH"

date -u +'%Y-%m-%dT%H:%M:%SZ' &amp;gt; "$DATA_DIR/$TARGET_NAME.updated_at"
echo "$NEW_SHA" &amp;gt; "$DATA_DIR/$TARGET_NAME.sha256"
echo "${NEW_SHA:0:12}" &amp;gt; "$DATA_DIR/$TARGET_NAME.version"

log "Update done. replaced $TARGET_PATH"&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Dockerfile&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="language-dockerfile"&gt;&lt;code&gt;FROM alpine:3.20

RUN apk add --no-cache bash curl ca-certificates coreutils util-linux

WORKDIR /app
COPY update.sh /app/update.sh
RUN chmod +x /app/update.sh

ENTRYPOINT ["/app/update.sh"]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When running locally:&lt;/p&gt;
&lt;pre class="language-powershell"&gt;&lt;code&gt;docker run --rm -v ./data:/data ediwang/ipinfo-updater&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can see the output like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/20260223-710da357.png" border="0" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Now, you can push it to any container registry you like, in my case, Docker Hub.&lt;/p&gt;
&lt;h3&gt;2. Setup Azure Container App Jobs&lt;/h3&gt;
&lt;p&gt;Next, create a new Azure Container App Job.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Set trigger to &lt;strong&gt;Scheduled&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Set &lt;strong&gt;CRON &lt;/strong&gt;expression as you need. In my case, at 00:00 UTC every day, which is &lt;code&gt;0 0 * * *&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="/image/20260223-9822d088.png" border="0" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;In the container configuration, set the server and image to the location of your registry.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/20260223-86086326.png" border="0" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Next, go to the Volumes blade, and add the volume you configured with Azure File Share. This post won't explain the details as I assume you already done that.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;If you don't know what it is, please read my previous blog post: &lt;a href="/post/2025/7/17/deploy-open-webui-with-azure-openai-on-azure-container-apps" target="_blank" rel="noopener"&gt;Deploy Open WebUI with Azure OpenAI on Azure Container Apps&lt;/a&gt;, that post explains in detail how to setup a File Share as volume mount.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/20260223-5f736029.png" border="0" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Next, go to &lt;strong&gt;Containers &lt;/strong&gt;tab, and click into the container.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/20260223-24f90740.png" border="0" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;In &lt;strong&gt;Volume mounts&lt;/strong&gt; tab, mount the volume as "/data", in my case.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/20260223-0fe43741.png" border="0" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Finally, you can test the Job in &lt;strong&gt;Overview &lt;/strong&gt;blade by clicking "&lt;strong&gt;Run now&lt;/strong&gt;" button, and observe the history.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/20260223-ba6d0fb6.png" border="0" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;This log indicates our Job has successfully completed.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/20260223-c1a729e3.png" border="0" loading="lazy"&gt;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;We can update files in Azure File Share with Azure Container App Jobs. The Azure Container App Job is just a simple container that can be setup to run on schedule using CRON expression.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;This can be helpful if you already has a main workload running in Azure Container App and will use the file.&amp;nbsp;&lt;/p&gt;</description><category>DevOps</category><category>Microsoft Azure</category><guid isPermaLink="false">2bd9d033-62cf-4b4c-8ae2-59d73fc1fa19</guid><pubDate>2026-02-23T10:54:25Z</pubDate><updated xmlns="http://www.w3.org/2005/Atom">2026-02-23T10:54:25Z</updated></item><item><title>Modernizing My Azure Deployment Script with PowerShell and Bicep</title><link>https://edi.wang/post/2026/2/8/modernizing-my-azure-deployment-script-with-powershell-and-bicep</link><description>&lt;h2&gt;Background&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;For a long time, my &lt;a href="https://github.com/EdiWang/Moonglade" target="_blank" rel="noopener"&gt;Moonglade blog&lt;/a&gt; had a single, fairly long PowerShell script that did everything from prompting for subscription and region, to generate random names and passwords, then create App Service Plan, Web App, SQL Server, SQL Database, Storage Account, and finally configure connection strings and app settings.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;It worked. But it was also doing two very different jobs in one place.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;In this post, I&amp;rsquo;ll walk through how I refactored that into a PowerShell script focused on orchestration and UX, and a Bicep template that declaratively defines the Azure resources.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/20260208-d2beadd2.png" border="0" loading="lazy"&gt;&lt;/p&gt;
&lt;h2&gt;What Changed&lt;/h2&gt;
&lt;hr&gt;
&lt;h3&gt;Before: One big PowerShell script&lt;/h3&gt;
&lt;p&gt;The original script did everything through Azure CLI calls:&lt;/p&gt;
&lt;pre class="language-powershell"&gt;&lt;code&gt;az group create
az appservice plan create
az sql server create
az sql db create
az storage account create
az webapp create
az webapp config ...
az sql server firewall-rule create&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All resource definitions were embedded in PowerShell functions like &lt;code&gt;Create-SqlServer&lt;/code&gt;, &lt;code&gt;Create-SqlDatabase&lt;/code&gt;, &lt;code&gt;Create-StorageAccount&lt;/code&gt;, &lt;code&gt;Create-WebApp&lt;/code&gt;, etc.&lt;/p&gt;
&lt;p&gt;It also did the interactive part. Like checking if &lt;code&gt;az &lt;/code&gt;is installed, logging in / listing subscriptions, asking for region and web app name, etc.&lt;/p&gt;
&lt;p&gt;Everything was imperative and driven by CLI operations.&lt;/p&gt;
&lt;h3&gt;After: PowerShell orchestration + Bicep deployment&lt;/h3&gt;
&lt;p&gt;The new approach splits responsibilities. PowerShell script still handles these tasks:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Azure CLI check + login&lt;/li&gt;
&lt;li&gt;Subscription selection&lt;/li&gt;
&lt;li&gt;Region selection (now with a nice arrow-key menu)&lt;/li&gt;
&lt;li&gt;Web app name validation&lt;/li&gt;
&lt;li&gt;Random name/password generation&lt;/li&gt;
&lt;li&gt;Confirmation&lt;/li&gt;
&lt;li&gt;Creates only the resource group with &lt;code&gt;az group create&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Then calls &lt;code&gt;az deployment group&lt;/code&gt; create with &lt;code&gt;main.bicep&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;After Bicep finishes, PowerShell:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Reads Bicep outputs (webAppUrl, sqlServerFqdn)&lt;/li&gt;
&lt;li&gt;Builds the SQL connection string&lt;/li&gt;
&lt;li&gt;Retrieves the storage connection string&lt;/li&gt;
&lt;li&gt;Configures web app connection strings and app settings&lt;/li&gt;
&lt;li&gt;Restarts the Web App&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Bicep template (&lt;code&gt;main.bicep&lt;/code&gt;) defines:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;App Service Plan&lt;/li&gt;
&lt;li&gt;SQL Server&lt;/li&gt;
&lt;li&gt;SQL Database&lt;/li&gt;
&lt;li&gt;SQL firewall rule&lt;/li&gt;
&lt;li&gt;Storage Account&lt;/li&gt;
&lt;li&gt;Web App (with Docker image and basic app settings)&lt;/li&gt;
&lt;li&gt;Returns &lt;code&gt;webAppUrl &lt;/code&gt;and &lt;code&gt;sqlServerFqdn&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Why I Did This&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;There are three main reasons.&lt;/p&gt;
&lt;h3&gt;1. Declarative infrastructure is easier to reason about&lt;/h3&gt;
&lt;p&gt;In the original script, if you wanted to know what was being deployed, you had to read through multiple PowerShell functions and CLI calls like this:&lt;/p&gt;
&lt;pre class="language-powershell"&gt;&lt;code&gt;function Create-SqlServer($sqlServerName, $rsgName, $regionName, $sqlServerUsername, $sqlServerPassword) {
    $sqlServerCheck = az sql server list --query "[?name=='$sqlServerName']" | ConvertFrom-Json
    $sqlServerExists = $sqlServerCheck.Length -gt 0
    if (!$sqlServerExists) {
        az sql server create --name $sqlServerName --resource-group $rsgName --location $regionName --admin-user $sqlServerUsername --admin-password $sqlServerPassword | Out-Null
        az sql server firewall-rule create --resource-group $rsgName --server $sqlServerName --name AllowAllIps --start-ip-address 0.0.0.0 --end-ip-address 0.0.0.0 | Out-Null
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The actual desired state (one SQL Server + default firewall rule) was buried in logic and conditionals. But with Bicep, the same thing becomes declarative:&lt;/p&gt;
&lt;pre class="language-bicep"&gt;&lt;code&gt;resource sqlServer 'Microsoft.Sql/servers@2022-05-01-preview' = {
  name: sqlServerName
  location: location
  properties: {
    administratorLogin: sqlAdminUsername
    administratorLoginPassword: sqlAdminPassword
    version: '12.0'
  }
}

resource sqlFirewallRule 'Microsoft.Sql/servers/firewallRules@2022-05-01-preview' = {
  parent: sqlServer
  name: 'AllowAllAzureIps'
  properties: {
    startIpAddress: '0.0.0.0'
    endIpAddress: '0.0.0.0'
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No "check if exists", no imperative flow. I simply declare what I want, and Bicep/ARM handles how to get there. This makes it easier to review, diff, and maintain over time.&lt;/p&gt;
&lt;h3&gt;2. Separation of concerns and better tooling&lt;/h3&gt;
&lt;p&gt;Splitting responsibilities gives some nice benefits.&lt;/p&gt;
&lt;p&gt;UX logic stays in PowerShell, like arrow-key menu for regions, input validation for web app name, and confirmation prompts.&lt;/p&gt;
&lt;p&gt;While infrastructure is in Bicep, it has strongly-typed resources, IntelliSense in VS Code, parameter validation (@minLength, @maxLength) and ARM deployment history in the portal.&lt;/p&gt;
&lt;p&gt;For example, Bicep now validates name constraints right in the template:&lt;/p&gt;
&lt;pre class="language-bicep"&gt;&lt;code&gt;@description('The name of the Storage Account')
@minLength(3)
@maxLength(24)
param storageAccountName string&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And I still get to keep a nice interactive installer in PowerShell, now improved with a small arrow-key menu:&lt;/p&gt;
&lt;pre class="language-powershell"&gt;&lt;code&gt;$azureRegions = @(
    "Australia Central",
    "Australia East",
    ...
    "West US 3"
)

$defaultIndex = [array]::IndexOf($azureRegions, $defaultRegion)
if ($defaultIndex -eq -1) { $defaultIndex = 0 }

$regionName = Show-Menu -Title "Select Azure Region:" -Options $azureRegions -DefaultIndex $defaultIndex
Write-Host "Selected region: $regionName" -ForegroundColor Cyan&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="/image/20260208-337fa53d.png" border="0" loading="lazy"&gt;&lt;/p&gt;
&lt;h3&gt;3. Reusability and portability&lt;/h3&gt;
&lt;p&gt;With Bicep, I can deploy the same infrastructure using just a simple command:&lt;/p&gt;
&lt;pre class="language-bash"&gt;&lt;code&gt;az deployment group create \
  --resource-group my-rg \
  --template-file main.bicep \
  --parameters webAppName=... ...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I can plug this into&amp;nbsp;GitHub Actions, Azure DevOps and Other CI/CD pipelines. The PowerShell script is now just one client of the Bicep template.&lt;/p&gt;
&lt;h2&gt;The Complete Code&lt;/h2&gt;
&lt;hr&gt;
&lt;h3&gt;Old PowerShell Script&lt;/h3&gt;
&lt;pre class="language-powershell"&gt;&lt;code&gt;param(
    [string] $defaultRegion = "West US",
    [switch] $preRelease = $false
)
function Get-UrlStatusCode([string] $Url) {
    try {
        [System.Net.WebRequest]::Create($Url).GetResponse().StatusCode
    }
    catch [Net.WebException] {
        [int]$_.Exception.Response.StatusCode
    }
}
function Check-Command($cmdname) {
    return [bool](Get-Command -Name $cmdname -ErrorAction SilentlyContinue)
}
function Get-RandomCharacters($length, $characters) {
    $random = 1..$length | ForEach-Object { Get-Random -Maximum $characters.length }
    $private:ofs = ""
    return [String]$characters[$random]
}
function Scramble-String([string]$inputString) {     
    $characterArray = $inputString.ToCharArray()   
    $scrambledStringArray = $characterArray | Get-Random -Count $characterArray.Length     
    $outputString = -join $scrambledStringArray
    return $outputString 
}
function Create-ResourceGroup($rsgName, $regionName) {
    $rsgExists = az group exists -n $rsgName
    if ($rsgExists -eq 'false') {
        Write-Host "Creating Resource Group"
        az group create -l $regionName -n $rsgName | Out-Null
    }
}
function Create-AppServicePlan($aspName, $rsgName, $regionName) {
    $planCheck = az appservice plan list --query "[?name=='$aspName']" | ConvertFrom-Json
    $planExists = $planCheck.Length -gt 0
    if (!$planExists) {
        az appservice plan create -n $aspName -g $rsgName --is-linux --sku P0V3 --location $regionName | Out-Null
    }
}
function Create-SqlServer($sqlServerName, $rsgName, $regionName, $sqlServerUsername, $sqlServerPassword) {
    $sqlServerCheck = az sql server list --query "[?name=='$sqlServerName']" | ConvertFrom-Json
    $sqlServerExists = $sqlServerCheck.Length -gt 0
    if (!$sqlServerExists) {
        Write-Host "Creating SQL Server"
        az sql server create --name $sqlServerName --resource-group $rsgName --location $regionName --admin-user $sqlServerUsername --admin-password $sqlServerPassword | Out-Null
        Write-Host "SQL Server Password: $sqlServerPassword" -ForegroundColor Yellow
        Write-Host "Setting Firewall to Allow Azure Connection"
        az sql server firewall-rule create --resource-group $rsgName --server $sqlServerName --name AllowAllIps --start-ip-address 0.0.0.0 --end-ip-address 0.0.0.0 | Out-Null
    }
}
function Create-SqlDatabase($sqlDatabaseName, $rsgName, $sqlServerName) {
    $sqlDbCheck = az sql db list --resource-group $rsgName --server $sqlServerName --query "[?name=='$sqlDatabaseName']" | ConvertFrom-Json
    $sqlDbExists = $sqlDbCheck.Length -gt 0
    if (!$sqlDbExists) {
        Write-Host "Creating SQL Database"
        az sql db create --resource-group $rsgName --server $sqlServerName --name $sqlDatabaseName --service-objective S0 --backup-storage-redundancy Local | Out-Null
    }
}
function Create-StorageAccount($storageAccountName, $rsgName, $regionName) {
    $storageAccountCheck = az storage account list --query "[?name=='$storageAccountName']" | ConvertFrom-Json
    $storageAccountExists = $storageAccountCheck.Length -gt 0
    if (!$storageAccountExists) {
        Write-Host "Creating Storage Account"
        az storage account create --name $storageAccountName --resource-group $rsgName --location $regionName --sku Standard_LRS --kind StorageV2 --allow-blob-public-access true | Out-Null
    }
}
function Create-WebApp($webAppName, $rsgName, $aspName, $dockerImageName) {
    $appCheck = az webapp list --query "[?name=='$webAppName']" | ConvertFrom-Json
    $appExists = $appCheck.Length -gt 0
    if (!$appExists) {
        Write-Host "Creating Web App"
        Write-Host "Using Linux Plan with Docker image from '$dockerImageName'."
        az webapp create -g $rsgName -p $aspName -n $webAppName --container-image-name $dockerImageName | Out-Null
        az webapp config set -g $rsgName -n $webAppName --always-on true --use-32bit-worker-process false --http20-enabled true | Out-Null
    }
}
function Update-WebAppConfig($webAppName, $rsgName, $sqlConnStr, $storageConn) {
    Write-Host "Updating Configuration" -ForegroundColor Green
    Write-Host "Setting SQL Database Connection String"
    az webapp config connection-string set -g $rsgName -n $webAppName -t SQLAzure --settings MoongladeDatabase=$sqlConnStr
    Write-Host "Adding Blob Storage Connection String"
    $scon = $storageConn.connectionString
    az webapp config appsettings set -g $rsgName -n $webAppName --settings ImageStorage__Provider=azurestorage  | Out-Null
    az webapp config appsettings set -g $rsgName -n $webAppName --settings ImageStorage__AzureStorageSettings__ConnectionString=$scon | Out-Null
    az webapp config appsettings set -g $rsgName -n $webAppName --settings ASPNETCORE_FORWARDEDHEADERS_ENABLED=true | Out-Null
}
# Main script starts here
[Console]::ResetColor()
if (-not (Check-Command -cmdname 'az')) {
    Invoke-WebRequest -Uri https://aka.ms/installazurecliwindows -OutFile .\AzureCLI.msi
    Start-Process msiexec.exe -Wait -ArgumentList '/I AzureCLI.msi /quiet'
    Write-Host "Please run 'az-login' and re-execute this script"
    return
}
# Login and select subscription
$output = az account show -o json | ConvertFrom-Json
$subscriptionList = az account list -o json | ConvertFrom-Json 
$subscriptionList | Format-Table name, id, tenantId -AutoSize
$subscriptionName = $output.name
Write-Host "Currently logged in to subscription """$output.name.Trim()""" in tenant " $output.tenantId
$subscriptionName = Read-Host "Enter subscription Id ("$output.id")"
$subscriptionName = $subscriptionName.Trim()
if ([string]::IsNullOrWhiteSpace($subscriptionName)) {
    $subscriptionName = $output.id
}
else {
    Write-Host "Changed to subscription ("$subscriptionName")"
}
# Select region
$regionName = Read-Host "Enter region name (default: $defaultRegion)"
if ([string]::IsNullOrWhiteSpace($regionName)) {
    $regionName = $defaultRegion
}
else {
    $regionName = $regionName.Trim()
}
# Select web app name
while ($true) {
    $webAppName = Read-Host -Prompt "Enter webapp name"
    $webAppName = $webAppName.Trim()
    $keywords = @("xbox", "windows", "login", "microsoft")
    if ($keywords -contains $webAppName.ToLower()) {
        Write-Host "Webapp name cannot have keywords xbox, windows, login, microsoft"
        continue
    }
    $HTTP_Status = Get-UrlStatusCode('https://' + $webAppName + '.azurewebsites.net')
    if ($HTTP_Status -eq 0) {
        break
    }
    else {
        Write-Host "Webapp name taken"
    }
}
# Generate random names and passwords
$rndNumber = Get-Random -Minimum 1000 -Maximum 9999
$rsgName = "moongladersg$rndNumber"
$aspName = "moongladeplan$rndNumber"
$storageAccountName = "moongladestorage$rndNumber"
$sqlServerUsername = "moonglade"
$sqlServerName = "moongladesql$rndNumber"
$sqlDatabaseName = "moongladedb$rndNumber"
$password = Get-RandomCharacters -length 5 -characters 'abcdefghiklmnoprstuvwxyz'
$password += Get-RandomCharacters -length 1 -characters 'ABCDEFGHKLMNOPRSTUVWXYZ'
$password += Get-RandomCharacters -length 1 -characters '1234567890'
$password += Get-RandomCharacters -length 1 -characters '!$%@#'
$password = Scramble-String $password
$sqlServerPassword = "m$password"
# Set docker image name based on pre-release flag
if ($preRelease) {
    $dockerImageName = "ediwang/moonglade:preview"
}
else {
    $dockerImageName = "ediwang/moonglade"
}
# Confirmation
Clear-Host
Write-Host "Your Moonglade will be deployed to [$rsgName] in [$regionName] under Azure subscription [$subscriptionName]. Please confirm before continuing." -ForegroundColor Green
Write-Host "+ Linux App Service Plan with Docker" -ForegroundColor Cyan
Read-Host -Prompt "Press [ENTER] to continue, [CTRL + C] to cancel"
# Set subscription
az account set --subscription $subscriptionName
Write-Host "Selected Azure Subscription: " $subscriptionName -ForegroundColor Cyan
# Create resources
Create-ResourceGroup $rsgName $regionName
Create-AppServicePlan $aspName $rsgName $regionName
Create-SqlServer $sqlServerName $rsgName $regionName $sqlServerUsername $sqlServerPassword
Create-SqlDatabase $sqlDatabaseName $rsgName $sqlServerName
Create-StorageAccount $storageAccountName $rsgName $regionName
$storageConn = az storage account show-connection-string -g $rsgName -n $storageAccountName | ConvertFrom-Json
Create-WebApp $webAppName $rsgName $aspName $dockerImageName
$createdApp = az webapp list --query "[?name=='$webAppName']" | ConvertFrom-Json
$createdExists = $createdApp.Length -gt 0
if ($createdExists) {
    $webAppUrl = "https://" + $createdApp.defaultHostName
    Write-Host "Web App URL: $webAppUrl"
}
# Update configuration
$sqlConnStrTemplate = az sql db show-connection-string -s $sqlServerName -n $sqlDatabaseName -c ado.net --auth-type SqlPassword
$sqlConnStr = $sqlConnStrTemplate.Replace("&amp;lt;username&amp;gt;", $sqlServerUsername).Replace("&amp;lt;password&amp;gt;", $sqlServerPassword)
Update-WebAppConfig $webAppName $rsgName $sqlConnStr $storageConn
az webapp restart --name $webAppName --resource-group $rsgName | Out-Null
Write-Host "Warming up the container..."
Start-Sleep -Seconds 20
Read-Host -Prompt "Setup is done, you should be able to run Moonglade on '$webAppUrl' now, if you get a 502 error, please try restarting Moonglade from Azure App Service blade. Press [ENTER] to exit."&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;New PowerShell Script&lt;/h3&gt;
&lt;pre class="language-powershell"&gt;&lt;code&gt;param(
    [string] $defaultRegion = "West US",
    [switch] $preRelease = $false
)
function Get-UrlStatusCode([string] $Url) {
    try {
        [System.Net.WebRequest]::Create($Url).GetResponse().StatusCode
    }
    catch [Net.WebException] {
        [int]$_.Exception.Response.StatusCode
    }
}
function Check-Command($cmdname) {
    return [bool](Get-Command -Name $cmdname -ErrorAction SilentlyContinue)
}
function Get-RandomCharacters($length, $characters) {
    $random = 1..$length | ForEach-Object { Get-Random -Maximum $characters.length }
    $private:ofs = ""
    return [String]$characters[$random]
}
function Scramble-String([string]$inputString) {     
    $characterArray = $inputString.ToCharArray()   
    $scrambledStringArray = $characterArray | Get-Random -Count $characterArray.Length     
    $outputString = -join $scrambledStringArray
    return $outputString 
}
function Show-Menu {
    param (
        [string]$Title,
        [string[]]$Options,
        [int]$DefaultIndex = 0
    )
    
    $selectedIndex = $DefaultIndex
    $cursorVisible = [Console]::CursorVisible
    [Console]::CursorVisible = $false
    
    try {
        while ($true) {
            Clear-Host
            Write-Host $Title -ForegroundColor Green
            Write-Host ""
            
            for ($i = 0; $i -lt $Options.Length; $i++) {
                if ($i -eq $selectedIndex) {
                    Write-Host "  &amp;gt; $($Options[$i])" -ForegroundColor Cyan
                } else {
                    Write-Host "    $($Options[$i])"
                }
            }
            
            Write-Host ""
            Write-Host "Use arrow keys to select, press Enter to confirm" -ForegroundColor Gray
            
            $key = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
            
            switch ($key.VirtualKeyCode) {
                38 { # Up arrow
                    $selectedIndex = [Math]::Max(0, $selectedIndex - 1)
                }
                40 { # Down arrow
                    $selectedIndex = [Math]::Min($Options.Length - 1, $selectedIndex + 1)
                }
                13 { # Enter
                    return $Options[$selectedIndex]
                }
            }
        }
    }
    finally {
        [Console]::CursorVisible = $cursorVisible
    }
}
# Main script starts here
[Console]::ResetColor()
if (-not (Check-Command -cmdname 'az')) {
    Invoke-WebRequest -Uri https://aka.ms/installazurecliwindows -OutFile .\AzureCLI.msi
    Start-Process msiexec.exe -Wait -ArgumentList '/I AzureCLI.msi /quiet'
    Write-Host "Please run 'az-login' and re-execute this script"
    return
}
# Login and select subscription
$output = az account show -o json | ConvertFrom-Json
$subscriptionList = az account list -o json | ConvertFrom-Json 
$subscriptionList | Format-Table name, id, tenantId -AutoSize
$subscriptionName = $output.name
Write-Host "Currently logged in to subscription """$output.name.Trim()""" in tenant " $output.tenantId
$subscriptionName = Read-Host "Enter subscription Id ("$output.id")"
$subscriptionName = $subscriptionName.Trim()
if ([string]::IsNullOrWhiteSpace($subscriptionName)) {
    $subscriptionName = $output.id
}
else {
    Write-Host "Changed to subscription ("$subscriptionName")"
}
# Select region
$azureRegions = @(
    "Australia Central",
    "Australia East",
    "Australia Southeast",
    "Brazil South",
    "Canada Central",
    "Canada East",
    "Central India",
    "Central US",
    "East Asia",
    "East US",
    "East US 2",
    "France Central",
    "Germany West Central",
    "Japan East",
    "Japan West",
    "Korea Central",
    "Korea South",
    "North Central US",
    "North Europe",
    "Norway East",
    "South Central US",
    "South India",
    "Southeast Asia",
    "Switzerland North",
    "UK South",
    "UK West",
    "West Central US",
    "West Europe",
    "West India",
    "West US",
    "West US 2",
    "West US 3"
)
$defaultIndex = [array]::IndexOf($azureRegions, $defaultRegion)
if ($defaultIndex -eq -1) { $defaultIndex = 0 }
$regionName = Show-Menu -Title "Select Azure Region:" -Options $azureRegions -DefaultIndex $defaultIndex
Write-Host "Selected region: $regionName" -ForegroundColor Cyan
Start-Sleep -Seconds 1
# Select web app name
while ($true) {
    $webAppName = Read-Host -Prompt "Enter webapp name"
    $webAppName = $webAppName.Trim()
    $keywords = @("xbox", "windows", "login", "microsoft")
    if ($keywords -contains $webAppName.ToLower()) {
        Write-Host "Webapp name cannot have keywords xbox, windows, login, microsoft"
        continue
    }
    $HTTP_Status = Get-UrlStatusCode('https://' + $webAppName + '.azurewebsites.net')
    if ($HTTP_Status -eq 0) {
        break
    }
    else {
        Write-Host "Webapp name taken"
    }
}
# Generate random names and passwords
$rndNumber = Get-Random -Minimum 1000 -Maximum 9999
$rsgName = "moongladersg$rndNumber"
$aspName = "moongladeplan$rndNumber"
$storageAccountName = "moongladestorage$rndNumber"
$sqlServerUsername = "moonglade"
$sqlServerName = "moongladesql$rndNumber"
$sqlDatabaseName = "moongladedb$rndNumber"
$password = Get-RandomCharacters -length 5 -characters 'abcdefghiklmnoprstuvwxyz'
$password += Get-RandomCharacters -length 1 -characters 'ABCDEFGHKLMNOPRSTUVWXYZ'
$password += Get-RandomCharacters -length 1 -characters '1234567890'
$password += Get-RandomCharacters -length 1 -characters '!$%@#'
$password = Scramble-String $password
$sqlServerPassword = "m$password"
# Set docker image name based on pre-release flag
if ($preRelease) {
    $dockerImageName = "ediwang/moonglade:preview"
}
else {
    $dockerImageName = "ediwang/moonglade"
}
# Confirmation
Clear-Host
Write-Host "Your Moonglade will be deployed to [$rsgName] in [$regionName] under Azure subscription [$subscriptionName]. Please confirm before continuing." -ForegroundColor Green
Write-Host "+ Linux App Service Plan with Docker" -ForegroundColor Cyan
Read-Host -Prompt "Press [ENTER] to continue, [CTRL + C] to cancel"
# Set subscription
az account set --subscription $subscriptionName
Write-Host "Selected Azure Subscription: " $subscriptionName -ForegroundColor Cyan
# Create Resource Group
Write-Host "Creating Resource Group: $rsgName" -ForegroundColor Green
$rsgExists = az group exists -n $rsgName
if ($rsgExists -eq 'false') {
    az group create -l $regionName -n $rsgName | Out-Null
}
# Get Bicep file path
$bicepFilePath = Join-Path $PSScriptRoot "main.bicep"
# Deploy using Bicep
Write-Host "Deploying Azure resources using Bicep..." -ForegroundColor Green
Write-Host "SQL Server Password: $sqlServerPassword" -ForegroundColor Yellow
$deploymentName = "moonglade-deployment-$(Get-Date -Format 'yyyyMMddHHmmss')"
$deploymentOutput = az deployment group create `
    --resource-group $rsgName `
    --name $deploymentName `
    --template-file $bicepFilePath `
    --parameters `
        webAppName=$webAppName `
        appServicePlanName=$aspName `
        sqlServerName=$sqlServerName `
        sqlDatabaseName=$sqlDatabaseName `
        storageAccountName=$storageAccountName `
        sqlAdminUsername=$sqlServerUsername `
        sqlAdminPassword=$sqlServerPassword `
        dockerImageName=$dockerImageName `
        location=$regionName `
    --output json | ConvertFrom-Json
if ($null -eq $deploymentOutput) {
    Write-Host "Deployment failed. Please check the error messages above." -ForegroundColor Red
    return
}
Write-Host "Deployment completed successfully!" -ForegroundColor Green
# Get outputs from Bicep deployment
$webAppUrl = $deploymentOutput.properties.outputs.webAppUrl.value
$sqlServerFqdn = $deploymentOutput.properties.outputs.sqlServerFqdn.value
# Build SQL connection string from known variables
$sqlConnStr = "Server=tcp:${sqlServerFqdn},1433;Initial Catalog=${sqlDatabaseName};Persist Security Info=False;User ID=${sqlServerUsername};Password=${sqlServerPassword};MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"
# Retrieve storage connection string securely
$storageConnStr = az storage account show-connection-string -g $rsgName -n $storageAccountName --query connectionString -o tsv
Write-Host "Web App URL: $webAppUrl" -ForegroundColor Cyan
# Update Web App Configuration
Write-Host "Updating Web App Configuration..." -ForegroundColor Green
Write-Host "Setting SQL Database Connection String"
az webapp config connection-string set -g $rsgName -n $webAppName -t SQLAzure --settings MoongladeDatabase=$sqlConnStr | Out-Null
Write-Host "Adding Blob Storage Connection String and other settings"
az webapp config appsettings set -g $rsgName -n $webAppName --settings `
    ImageStorage__Provider=azurestorage `
    ImageStorage__AzureStorageSettings__ConnectionString=$storageConnStr `
    ASPNETCORE_FORWARDEDHEADERS_ENABLED=true | Out-Null
# Restart Web App
Write-Host "Restarting Web App..." -ForegroundColor Green
az webapp restart --name $webAppName --resource-group $rsgName | Out-Null
Write-Host "Warming up the container..."
Start-Sleep -Seconds 20
Read-Host -Prompt "Setup is done, you should be able to run Moonglade on '$webAppUrl' now, if you get a 502 error, please try restarting Moonglade from Azure App Service blade. Press [ENTER] to exit."&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;New Bicep File&lt;/h3&gt;
&lt;pre class="language-bicep"&gt;&lt;code&gt;@description('The name of the web app')
param webAppName string
@description('The name of the App Service Plan')
param appServicePlanName string
@description('The name of the SQL Server')
@minLength(1)
@maxLength(63)
param sqlServerName string
@description('The name of the SQL Database')
param sqlDatabaseName string
@description('The name of the Storage Account')
@minLength(3)
@maxLength(24)
param storageAccountName string
@description('SQL Server admin username')
param sqlAdminUsername string
@description('SQL Server admin password')
@secure()
param sqlAdminPassword string
@description('Docker image name')
param dockerImageName string
@description('Location for all resources')
param location string = resourceGroup().location
// App Service Plan
resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = {
  name: appServicePlanName
  location: location
  sku: {
    name: 'P0v3'
    tier: 'Premium0V3'
  }
  kind: 'linux'
  properties: {
    reserved: true
  }
}
// SQL Server
resource sqlServer 'Microsoft.Sql/servers@2022-05-01-preview' = {
  name: sqlServerName
  location: location
  properties: {
    administratorLogin: sqlAdminUsername
    administratorLoginPassword: sqlAdminPassword
    version: '12.0'
  }
}
// SQL Database
resource sqlDatabase 'Microsoft.Sql/servers/databases@2022-05-01-preview' = {
  parent: sqlServer
  name: sqlDatabaseName
  location: location
  sku: {
    name: 'S0'
    tier: 'Standard'
  }
  properties: {
    collation: 'SQL_Latin1_General_CP1_CI_AS'
    requestedBackupStorageRedundancy: 'Local'
  }
}
// SQL Firewall Rule - Allow Azure Services
resource sqlFirewallRule 'Microsoft.Sql/servers/firewallRules@2022-05-01-preview' = {
  parent: sqlServer
  name: 'AllowAllAzureIps'
  properties: {
    startIpAddress: '0.0.0.0'
    endIpAddress: '0.0.0.0'
  }
}
// Storage Account
resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = {
  name: storageAccountName
  location: location
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
  properties: {
    allowBlobPublicAccess: true
    supportsHttpsTrafficOnly: true
  }
}
// Web App
resource webApp 'Microsoft.Web/sites@2022-03-01' = {
  name: webAppName
  location: location
  properties: {
    serverFarmId: appServicePlan.id
    siteConfig: {
      linuxFxVersion: 'DOCKER|${dockerImageName}'
      alwaysOn: true
      use32BitWorkerProcess: false
      http20Enabled: true
      appSettings: [
        {
          name: 'WEBSITES_ENABLE_APP_SERVICE_STORAGE'
          value: 'false'
        }
        {
          name: 'DOCKER_REGISTRY_SERVER_URL'
          value: 'https://index.docker.io'
        }
      ]
    }
    httpsOnly: true
  }
}
// Outputs
output webAppUrl string = 'https://${webApp.properties.defaultHostName}'
output sqlServerFqdn string = sqlServer.properties.fullyQualifiedDomainName&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;I refactored my Azure deployment script with Bicep. The changes are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Moved resource creation from imperative Azure CLI calls in PowerShell into a Bicep template.&lt;/li&gt;
&lt;li&gt;Kept user experience, validation, and orchestration in PowerShell.&lt;/li&gt;
&lt;li&gt;Used Bicep outputs to wire up connection strings and URLs in PowerShell.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This pattern (PowerShell/Bash for orchestration + Bicep for infrastructure) scales much better than a single giant script, especially as you add more resources, environments, or CI/CD!&lt;/p&gt;</description><category>DevOps</category><category>Microsoft Azure</category><guid isPermaLink="false">fd664ccb-339f-4db8-8a67-656f6d591aab</guid><pubDate>2026-02-08T07:34:39Z</pubDate><updated xmlns="http://www.w3.org/2005/Atom">2026-02-08T07:34:39Z</updated></item><item><title>PowerShell Script to Check Azure App Service SSL Certificate Expiration</title><link>https://edi.wang/post/2025/8/18/powershell-script-to-check-azure-app-service-ssl-certificate-expiration</link><description>&lt;h2&gt;Problem&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;When you have a lot of App Service instances running in your Azure subscription, managing their SSL certificate status could be a problem. Azure portal itself does have certain monitoring and advising feature to visualize the certificate status and warn you before a certificate expires. However, in some cases, we prefer this to be done in command line environment. That's why I put together this handy PowerShell script that uses Azure CLI to keep tabs on all your App Service SSL certificates.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;On Windows machines, just run this command in your PowerShell 5.1/7.x:&lt;/p&gt;
&lt;pre class="language-powershell"&gt;&lt;code&gt;irm https://scripts.edi.wang/Check-AppServiceCertificates.ps1 | iex&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The script is pretty straightforward, it crawls through all your Azure resource groups, finds SSL certificates, and tells you which ones are about to expire (or already have).&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/20250818-6df80a79.png" border="0" loading="lazy"&gt;&lt;/p&gt;
&lt;h3&gt;Key Steps&lt;/h3&gt;
&lt;h4&gt;1. Get All Your Resource Groups&lt;/h4&gt;
&lt;pre class="language-powershell"&gt;&lt;code&gt;az group list&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First, we need to get every resource group in your subscription. Because SSL certificates live at the resource group level, so we need to check them one by one. By iterating through resource groups, we catch certificates bound to multiple apps, certificates that exist but aren't currently bound and certificates in resource groups we might have forgotten about.&lt;/p&gt;
&lt;h4&gt;2. Check SSL Certificates in Each Resource Group&lt;/h4&gt;
&lt;pre class="language-powershell"&gt;&lt;code&gt;az webapp config ssl list --resource-group &amp;lt;resource-group-name&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the core command! For each resource group, we ask Azure CLI to show us all the SSL certificates. You will get JSON like this:&lt;/p&gt;
&lt;pre class="language-json"&gt;&lt;code&gt;{
  "expirationDate": "2025-12-31T23:59:59+00:00",
  "issueDate": "2024-01-01T00:00:00+00:00",
  "subjectName": "CN=yourdomain.com",
  "thumbprint": "ABC123...",
  "selfSigned": false,
  "issuer": "Let's Encrypt Authority X3"
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. Parse the Certificate Data&lt;/h4&gt;
&lt;p&gt;The script takes the JSON response and then extract the information:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Subject name (your domain)&lt;/li&gt;
&lt;li&gt;Expiration date&lt;/li&gt;
&lt;li&gt;Thumbprint&lt;/li&gt;
&lt;li&gt;Whether it's self-signed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Azure CLI response doesn't include which resource group the certificate came from. Since we're aggregating certificates from multiple resource groups, we need to track the source for proper reporting.&lt;/p&gt;
&lt;pre class="language-powershell"&gt;&lt;code&gt;$certWithRG = $cert | Add-Member -NotePropertyName "resourceGroupName" -NotePropertyValue $rg.name -PassThru&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. Calculate Days Until Expiration&lt;/h4&gt;
&lt;pre class="language-powershell"&gt;&lt;code&gt;$daysToExpiry = [Math]::Round(($expiryDate - $currentDate).TotalDays, 0)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Compare the certificate's expiration date with today's date to see how much time it has left. PowerShell's&amp;nbsp;&lt;code&gt;TimeSpan&lt;/code&gt; object gives us precise calculations including fractional days. We round to whole days.&lt;/p&gt;
&lt;h2&gt;Complete Code&lt;/h2&gt;
&lt;hr&gt;
&lt;pre class="language-powershell"&gt;&lt;code&gt;#!/usr/bin/env pwsh

&amp;lt;#
.SYNOPSIS
    Checks the SSL certificate status of Azure App Services using Azure CLI
.DESCRIPTION
    This script retrieves all App Service SSL certificates and groups them by expiration status.
    It highlights certificates that are expired or about to expire within a specified threshold.
.PARAMETER DaysThreshold
    Number of days to consider as "about to expire" (default: 30)
.PARAMETER SubscriptionId
    Azure subscription ID (optional - uses current subscription if not specified)
.PARAMETER Debug
    Enable detailed debugging output
#&amp;gt;

param(
    [int]$DaysThreshold = 30,
    [string]$SubscriptionId = $null,
    [switch]$Debug
)

# Function to write colored output
function Write-ColorOutput {
    param(
        [string]$Message,
        [string]$Color = "White"
    )
    
    switch ($Color) {
        "Red" { Write-Host $Message -ForegroundColor Red }
        "Yellow" { Write-Host $Message -ForegroundColor Yellow }
        "Green" { Write-Host $Message -ForegroundColor Green }
        "Cyan" { Write-Host $Message -ForegroundColor Cyan }
        "Magenta" { Write-Host $Message -ForegroundColor Magenta }
        "Gray" { Write-Host $Message -ForegroundColor Gray }
        default { Write-Host $Message }
    }
}

# Function to write debug output
function Write-DebugOutput {
    param([string]$Message)
    if ($Debug) {
        Write-ColorOutput "DEBUG: $Message" "Gray"
    }
}

# Function to execute Azure CLI command with error capture
function Invoke-AzCommand {
    param(
        [string]$Command,
        [string]$Description
    )
    
    Write-DebugOutput "Executing: $Command"
    
    # Capture both stdout and stderr
    $tempFile = [System.IO.Path]::GetTempFileName()
    $errorFile = [System.IO.Path]::GetTempFileName()
    
    try {
        # Execute command and capture output
        $process = Start-Process -FilePath "az" -ArgumentList $Command.Split(' ', [StringSplitOptions]::RemoveEmptyEntries)[1..($Command.Split(' ').Length - 1)] -RedirectStandardOutput $tempFile -RedirectStandardError $errorFile -Wait -PassThru -NoNewWindow
        
        $output = Get-Content $tempFile -Raw -ErrorAction SilentlyContinue
        $errorOutput = Get-Content $errorFile -Raw -ErrorAction SilentlyContinue
        
        Write-DebugOutput "Exit code: $($process.ExitCode)"
        
        if ($process.ExitCode -ne 0) {
            Write-ColorOutput "FAILED to execute: $Description" "Red"
            Write-ColorOutput "Command: $Command" "Red"
            Write-ColorOutput "Exit Code: $($process.ExitCode)" "Red"
            
            if ($errorOutput) {
                Write-ColorOutput "Error Output:" "Red"
                Write-ColorOutput $errorOutput "Red"
            }
            
            if ($output) {
                Write-ColorOutput "Standard Output:" "Yellow"
                Write-ColorOutput $output "Yellow"
            }
            
            return $null
        }
        
        Write-DebugOutput "Command succeeded"
        if ($Debug -and $output) {
            Write-DebugOutput "Output length: $($output.Length) characters"
        }
        
        return $output
    }
    finally {
        # Cleanup temp files
        if (Test-Path $tempFile) { Remove-Item $tempFile -Force -ErrorAction SilentlyContinue }
        if (Test-Path $errorFile) { Remove-Item $errorFile -Force -ErrorAction SilentlyContinue }
    }
}

# Function to format certificate info
function Format-CertificateInfo {
    param($cert, $webAppName, $resourceGroup, $status, $daysToExpiry)
    
    $expiryDate = if ($cert.expirationDate) { 
        [DateTime]::Parse($cert.expirationDate).ToString("yyyy-MM-dd HH:mm:ss UTC") 
    }
    else { 
        "Unknown" 
    }
    
    $issueDate = if ($cert.issueDate) { 
        [DateTime]::Parse($cert.issueDate).ToString("yyyy-MM-dd") 
    }
    else { 
        "Unknown" 
    }
    
    return [PSCustomObject]@{
        WebApp         = $webAppName
        ResourceGroup  = $resourceGroup
        SubjectName    = $cert.subjectName
        Thumbprint     = $cert.thumbprint
        IssueDate      = $issueDate
        ExpirationDate = $expiryDate
        DaysToExpiry   = $daysToExpiry
        Status         = $status
        Issuer         = $cert.issuer
        SelfSigned     = $cert.selfSigned
    }
}

try {
    Write-ColorOutput "Azure App Service SSL Certificate Status Checker" "Cyan"
    Write-ColorOutput "===================================================" "Cyan"
    Write-Host ""

    # Check PowerShell version
    Write-DebugOutput "PowerShell Version: $($PSVersionTable.PSVersion)"
    Write-DebugOutput "OS: $($PSVersionTable.OS)"

    # Check if Azure CLI is installed and get version
    Write-ColorOutput "Checking Azure CLI..." "Yellow"
    $azVersionOutput = Invoke-AzCommand "az version" "Getting Azure CLI version"
    
    if (-not $azVersionOutput) {
        Write-ColorOutput "Azure CLI is not installed or not accessible" "Red"
        Write-ColorOutput "Please install Azure CLI: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli" "Red"
        exit 1
    }

    try {
        $azVersionJson = $azVersionOutput | ConvertFrom-Json
        Write-ColorOutput "Azure CLI Version: $($azVersionJson.'azure-cli')" "Green"
        Write-DebugOutput "Full version info: $azVersionOutput"
    }
    catch {
        Write-ColorOutput "Azure CLI is installed (version parsing failed)" "Green"
        Write-DebugOutput "Raw version output: $azVersionOutput"
    }

    # Check if logged in
    Write-ColorOutput "Checking authentication..." "Yellow"
    $accountOutput = Invoke-AzCommand "az account show" "Getting current account"
    
    if (-not $accountOutput) {
        Write-ColorOutput "Not logged in to Azure CLI" "Red"
        Write-ColorOutput "Please run 'az login' first" "Red"
        exit 1
    }

    try {
        $account = $accountOutput | ConvertFrom-Json
        Write-ColorOutput "Logged in as: $($account.user.name)" "Green"
        Write-DebugOutput "Account type: $($account.user.type)"
        Write-DebugOutput "Tenant ID: $($account.tenantId)"
    }
    catch {
        Write-ColorOutput "Failed to parse account information" "Red"
        Write-ColorOutput "Raw output: $accountOutput" "Red"
        exit 1
    }
    
    # Set subscription if provided
    if ($SubscriptionId) {
        Write-ColorOutput "Setting subscription to: $SubscriptionId" "Yellow"
        $setSubOutput = Invoke-AzCommand "az account set --subscription $SubscriptionId" "Setting subscription"
        if (-not $setSubOutput -and $setSubOutput -ne "") {
            Write-ColorOutput "Failed to set subscription" "Red"
            exit 1
        }
    }

    # Get current subscription details
    $currentSubscriptionOutput = Invoke-AzCommand "az account show" "Getting current subscription"
    if (-not $currentSubscriptionOutput) {
        Write-ColorOutput "Failed to get current subscription" "Red"
        exit 1
    }

    try {
        $currentSubscription = $currentSubscriptionOutput | ConvertFrom-Json
        Write-ColorOutput "Current subscription: $($currentSubscription.name) ($($currentSubscription.id))" "Green"
        Write-DebugOutput "Subscription state: $($currentSubscription.state)"
    }
    catch {
        Write-ColorOutput "Failed to parse subscription information" "Red"
        exit 1
    }

    Write-Host ""

    # First, let's try to get all SSL certificates at once
    Write-ColorOutput "Retrieving all SSL certificates..." "Yellow"
    
    # Method 1: Try to get all certificates from all resource groups
    $allCertificates = @()
    
    # Get all resource groups
    $resourceGroupsOutput = Invoke-AzCommand "az group list" "Getting resource groups"
    
    if ($resourceGroupsOutput) {
        try {
            $resourceGroups = $resourceGroupsOutput | ConvertFrom-Json
            Write-ColorOutput "Found $($resourceGroups.Count) resource group(s)" "Green"
            Write-DebugOutput "Resource groups: $($resourceGroups.name -join ', ')"
            
            # Check SSL certificates in each resource group
            foreach ($rg in $resourceGroups) {
                Write-ColorOutput "Checking SSL certificates in resource group: $($rg.name)" "Yellow"
                Write-DebugOutput "Resource group: $($rg.name), Location: $($rg.location)"
                
                $sslOutput = Invoke-AzCommand "az webapp config ssl list --resource-group $($rg.name)" "Getting SSL certificates for resource group $($rg.name)"
                
                if ($sslOutput) {
                    try {
                        $sslCerts = $sslOutput | ConvertFrom-Json
                        
                        if ($sslCerts -and $sslCerts.Count -gt 0) {
                            Write-ColorOutput "Found $($sslCerts.Count) SSL certificate(s) in $($rg.name)" "Green"
                            Write-DebugOutput "Certificates in $($rg.name): $($sslCerts.subjectName -join ', ')"
                            
                            foreach ($cert in $sslCerts) {
                                # Add resource group info to certificate
                                $certWithRG = $cert | Add-Member -NotePropertyName "resourceGroupName" -NotePropertyValue $rg.name -PassThru
                                $allCertificates += $certWithRG
                            }
                        }
                        else {
                            Write-DebugOutput "No SSL certificates found in resource group: $($rg.name)"
                        }
                    }
                    catch {
                        Write-DebugOutput "Failed to parse SSL certificates for resource group $($rg.name): $($_.Exception.Message)"
                        if ($Debug) {
                            Write-DebugOutput "Raw SSL output for $($rg.name): $($sslOutput.Substring(0, [Math]::Min(200, $sslOutput.Length)))"
                        }
                    }
                }
                else {
                    Write-DebugOutput "No SSL certificates found in resource group: $($rg.name) or command failed"
                }
            }
        }
        catch {
            Write-ColorOutput "Failed to parse resource groups" "Red"
            exit 1
        }
    }
    else {
        Write-ColorOutput "Failed to get resource groups" "Red"
        exit 1
    }

    Write-Host ""
    
    if (-not $allCertificates -or $allCertificates.Count -eq 0) {
        Write-ColorOutput "No SSL certificates found across all resource groups" "Yellow"
        Write-ColorOutput "This could mean:" "White"
        Write-ColorOutput "&amp;bull; No custom SSL certificates are configured" "White"
        Write-ColorOutput "&amp;bull; All web apps are using default *.azurewebsites.net certificates" "White"
        Write-ColorOutput "&amp;bull; You don't have permissions to view SSL configurations" "White"
        Write-ColorOutput "&amp;bull; SSL certificates are managed through Azure Key Vault or other services" "White"
        Write-Host ""
        
        # Let's also try to check if there are any web apps
        Write-ColorOutput "Checking for web apps in subscription..." "Yellow"
        $webAppsOutput = Invoke-AzCommand "az webapp list" "Getting web apps"
        
        if ($webAppsOutput) {
            try {
                $webApps = $webAppsOutput | ConvertFrom-Json
                if ($webApps -and $webApps.Count -gt 0) {
                    Write-ColorOutput "Found $($webApps.Count) web app(s) in subscription:" "Green"
                    foreach ($app in $webApps) {
                        Write-ColorOutput "   &amp;bull; $($app.name) (Resource Group: $($app.resourceGroup))" "White"
                    }
                    Write-Host ""
                    Write-ColorOutput "These web apps might be using:" "Cyan"
                    Write-ColorOutput "   &amp;bull; Default *.azurewebsites.net certificates (free)" "White"
                    Write-ColorOutput "   &amp;bull; App Service Managed Certificates" "White"
                    Write-ColorOutput "   &amp;bull; Certificates from Azure Key Vault" "White"
                }
                else {
                    Write-ColorOutput "No web apps found in subscription" "Yellow"
                }
            }
            catch {
                Write-DebugOutput "Failed to parse web apps output"
            }
        }
        
        exit 0
    }

    Write-ColorOutput "Found $($allCertificates.Count) SSL certificate(s) across all resource groups" "Green"
    
    if ($Debug) {
        Write-DebugOutput "Certificate summary:"
        foreach ($cert in $allCertificates) {
            Write-DebugOutput "  - $($cert.subjectName) in RG: $($cert.resourceGroupName)"
        }
    }

    # Current date for comparison
    $currentDate = Get-Date
    Write-DebugOutput "Current date: $currentDate"
    
    # Arrays to store categorized certificates
    $expiredCerts = @()
    $aboutToExpireCerts = @()
    $validCerts = @()

    # Process each certificate
    foreach ($cert in $allCertificates) {
        Write-DebugOutput "Processing certificate: $($cert.subjectName) in RG: $($cert.resourceGroupName)"
        
        if ($cert.expirationDate) {
            try {
                $expiryDate = [DateTime]::Parse($cert.expirationDate)
                $daysToExpiry = [Math]::Round(($expiryDate - $currentDate).TotalDays, 0)
                
                Write-DebugOutput "Certificate $($cert.subjectName) expires on $expiryDate (in $daysToExpiry days)"
                
                if ($daysToExpiry -lt 0) {
                    $expiredCerts += Format-CertificateInfo $cert "Multiple/Unknown" $cert.resourceGroupName "EXPIRED" $daysToExpiry
                }
                elseif ($daysToExpiry -le $DaysThreshold) {
                    $aboutToExpireCerts += Format-CertificateInfo $cert "Multiple/Unknown" $cert.resourceGroupName "EXPIRING SOON" $daysToExpiry
                }
                else {
                    $validCerts += Format-CertificateInfo $cert "Multiple/Unknown" $cert.resourceGroupName "VALID" $daysToExpiry
                }
            }
            catch {
                Write-DebugOutput "Failed to parse expiration date for $($cert.subjectName): $($cert.expirationDate)"
                $validCerts += Format-CertificateInfo $cert "Multiple/Unknown" $cert.resourceGroupName "UNKNOWN" "N/A"
            }
        }
        else {
            Write-DebugOutput "Certificate $($cert.subjectName) has no expiration date"
            $validCerts += Format-CertificateInfo $cert "Multiple/Unknown" $cert.resourceGroupName "UNKNOWN" "N/A"
        }
    }

    # Display results
    Write-ColorOutput "SSL CERTIFICATE STATUS SUMMARY" "Cyan"
    Write-ColorOutput "==================================" "Cyan"
    Write-ColorOutput "&amp;bull; Expired: $($expiredCerts.Count)" "Red"
    Write-ColorOutput "&amp;bull; Expiring Soon (&amp;le;$DaysThreshold days): $($aboutToExpireCerts.Count)" "Yellow"
    Write-ColorOutput "&amp;bull; Valid: $($validCerts.Count)" "Green"
    Write-Host ""

    # Display expired certificates
    if ($expiredCerts.Count -gt 0) {
        Write-ColorOutput "EXPIRED CERTIFICATES" "Red"
        Write-ColorOutput "========================" "Red"
        $expiredCerts | Sort-Object DaysToExpiry | Format-Table -Property ResourceGroup, SubjectName, ExpirationDate, DaysToExpiry, SelfSigned, Thumbprint -AutoSize
        Write-Host ""
    }

    # Display certificates about to expire
    if ($aboutToExpireCerts.Count -gt 0) {
        Write-ColorOutput "CERTIFICATES EXPIRING SOON (&amp;le;$DaysThreshold days)" "Yellow"
        Write-ColorOutput "================================================" "Yellow"
        $aboutToExpireCerts | Sort-Object DaysToExpiry | Format-Table -Property ResourceGroup, SubjectName, ExpirationDate, DaysToExpiry, SelfSigned, Thumbprint -AutoSize
        Write-Host ""
    }

    # Display valid certificates
    if ($validCerts.Count -gt 0) {
        Write-ColorOutput "VALID CERTIFICATES" "Green"
        Write-ColorOutput "=====================" "Green"
        $validCerts | Sort-Object DaysToExpiry | Format-Table -Property ResourceGroup, SubjectName, ExpirationDate, DaysToExpiry, SelfSigned, Thumbprint -AutoSize
        Write-Host ""
    }

    # Summary recommendations
    Write-ColorOutput "RECOMMENDATIONS" "Cyan"
    Write-ColorOutput "==================" "Cyan"
    
    if ($expiredCerts.Count -gt 0) {
        Write-ColorOutput "URGENT: $($expiredCerts.Count) certificate(s) have already expired and need immediate attention!" "Red"
    }
    
    if ($aboutToExpireCerts.Count -gt 0) {
        Write-ColorOutput "WARNING: $($aboutToExpireCerts.Count) certificate(s) will expire within $DaysThreshold days. Plan renewal soon." "Yellow"
    }
    
    if ($expiredCerts.Count -eq 0 -and $aboutToExpireCerts.Count -eq 0) {
        Write-ColorOutput "All certificates are valid and not expiring soon. Great job!" "Green"
    }

    Write-Host ""
    Write-ColorOutput "To manage SSL certificates:" "Cyan"
    Write-ColorOutput " &amp;bull; Azure Portal: App Services &amp;gt; [Your App] &amp;gt; TLS/SSL settings" "White"
    Write-ColorOutput " &amp;bull; Azure CLI: az webapp config ssl --help" "White"
    Write-ColorOutput " &amp;bull; List certificates by resource group: az webapp config ssl list --resource-group &amp;lt;rg-name&amp;gt;" "White"

}
catch {
    Write-ColorOutput "An unexpected error occurred: $($_.Exception.Message)" "Red"
    Write-ColorOutput "Stack trace: $($_.ScriptStackTrace)" "Red"
    exit 1
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Tips&amp;nbsp;&lt;/h2&gt;
&lt;hr&gt;
&lt;ul&gt;
&lt;li&gt;You can use the &lt;code&gt;-Debug&lt;/code&gt; flag if something blows up&lt;/li&gt;
&lt;li&gt;If you're getting errors about commands not being recognized, double-check you're using the right Azure CLI syntax. The Azure CLI team loves to keep us on our toes with command changes!&lt;/li&gt;
&lt;/ul&gt;</description><category>DevOps</category><category>Microsoft Azure</category><guid isPermaLink="false">fe8579ac-7bb8-4b4c-8d4e-04e0d557ffd9</guid><pubDate>2025-08-18T04:48:11Z</pubDate><updated xmlns="http://www.w3.org/2005/Atom">2025-08-18T04:48:11Z</updated></item><item><title>Automating Deployment of Open WebUI on Azure Container Apps with Bicep</title><link>https://edi.wang/post/2025/7/24/automating-deployment-of-open-webui-on-azure-container-apps-with-bicep</link><description>&lt;p&gt;In my previous blog post, I introduced how to &lt;a href="/post/2025/7/17/deploy-open-webui-with-azure-openai-on-azure-container-apps" target="_blank" rel="noopener"&gt;deploy Open WebUI with Azure OpenAI on Azure Container Apps&lt;/a&gt; in Azure Portal manually. However, manual deployment usually makes people &lt;a href="https://996.icu/" target="_blank" rel="noopener"&gt;work 996&lt;/a&gt;. In this post, I will show you how to automate everything using &lt;a href="https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/overview?tabs=bicep&amp;amp;WT.mc_id=AZ-MVP-5002809" target="_blank" rel="noopener"&gt;Bicep&lt;/a&gt;. If you&amp;rsquo;re new to Bicep, think of it as a friendlier way to define Azure resources as code compared to raw ARM templates. I&amp;rsquo;ll walk you through my Bicep file, explaining how each block helps set up a scalable, persistent, and easy-to-manage Open Web UI deployment.&amp;nbsp;&lt;/p&gt;
&lt;h2 dir="auto"&gt;1. Define Parameters&lt;/h2&gt;
&lt;hr&gt;
&lt;p dir="auto"&gt;At the very top, we&amp;rsquo;ve got a few parameters:&lt;/p&gt;
&lt;pre class="language-bicep"&gt;&lt;code&gt;@description('Name of the storage account (must be globally unique)')
param storageAccountName string = 'openwebuistorage1069'

@description('Location for all resources')
param location string = resourceGroup().location

@description('Name of the file share')
param fileShareName string = 'owuifileshare'

@description('Name for the Container Apps Environment')
param envName string = 'ca-env'

@description('Name for the Container App')
param containerAppName string = 'openwebui-app'&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These are self-explanatory, just names of your resources.&lt;/p&gt;
&lt;h2&gt;2. Data Storage&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;Open Web UI needs somewhere to save data so you don&amp;rsquo;t lose info across restarts. We use &lt;a href="https://learn.microsoft.com/en-us/azure/storage/common/storage-account-overview?WT.mc_id=AZ-MVP-5002809" target="_blank" rel="noopener"&gt;Azure Storage Account&lt;/a&gt; with &lt;a href="https://learn.microsoft.com/en-us/azure/storage/files/storage-files-introduction?WT.mc_id=AZ-MVP-5002809" target="_blank" rel="noopener"&gt;file share&lt;/a&gt; to help that.&lt;/p&gt;
&lt;pre class="language-bicep"&gt;&lt;code&gt;// Storage Account
resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = {
  name: storageAccountName
  location: location
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
  properties: {
    accessTier: 'Hot'
    supportsHttpsTrafficOnly: true
  }
}

// Storage Share
resource fileShare 'Microsoft.Storage/storageAccounts/fileServices/shares@2022-09-01' = {
  name: '${storageAccount.name}/default/${fileShareName}'
  properties: {
    shareQuota: 100 // in GB
    accessTier: 'TransactionOptimized'
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p dir="auto"&gt;We:&lt;/p&gt;
&lt;ul class="" dir="auto"&gt;
&lt;li class="text-start"&gt;Create a Storage Account (needs a globally unique name!).&lt;/li&gt;
&lt;li class="text-start"&gt;Set up a File Share (100GB quota, TransactionOptimized tier&amp;mdash;a good fit for lots of small file activity).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. Create Log Analytics&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;This creates a workspace with a 30-day log retention (also adjustable). All logs from Container Apps Environment will show up here.&lt;/p&gt;
&lt;pre class="language-bicep"&gt;&lt;code&gt;// Log Analytics for Container Apps Environment
resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = {
  name: '${envName}-log'
  location: location
  properties: {
    sku: {
      name: 'PerGB2018'
    }
    retentionInDays: 30
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. Create&amp;nbsp;Container Apps Environment&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;A "&lt;a href="https://learn.microsoft.com/en-us/azure/container-apps/environment?WT.mc_id=AZ-MVP-5002809" target="_blank" rel="noopener"&gt;Managed Environment&lt;/a&gt;" is required for any Container App in Azure. We link it to the Log Analytics workspace for log aggregation.&lt;/p&gt;
&lt;pre class="language-bicep"&gt;&lt;code&gt;// Container Apps Environment
resource containerAppEnv 'Microsoft.App/managedEnvironments@2023-05-01' = {
  name: envName
  location: location
  properties: {
    appLogsConfiguration: {
      destination: 'log-analytics'
      logAnalyticsConfiguration: {
        customerId: logAnalytics.properties.customerId
        sharedKey: logAnalytics.listKeys().primarySharedKey
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 dir="auto"&gt;5. Hooking Up Azure File as a Persistent Volume&lt;/h2&gt;
&lt;hr&gt;
&lt;p dir="auto"&gt;We fetch the storage account key, then create an Azure File storage for the Container Apps Environment. This is what enables volume mounting from your container.&lt;/p&gt;
&lt;div class="my-2"&gt;
&lt;pre class="language-bicep"&gt;&lt;code&gt;// Get Storage Key for mounting File Share
var storageAccountKey = storageAccount.listKeys().keys[0].value

resource meStorage 'Microsoft.App/managedEnvironments/storages@2023-05-01' = {
  parent: containerAppEnv
  name: '${storageAccountName}fileshare'
  properties: {
    azureFile: {
      accountName: storageAccountName
      shareName: fileShareName
      accountKey: storageAccountKey
      accessMode: 'ReadWrite'
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 dir="auto"&gt;6. Deploying the Open WebUI Container App&lt;/h2&gt;
&lt;hr&gt;
&lt;p dir="auto"&gt;Finally, we can deploy the main web application since all the dependencies are ready.&lt;/p&gt;
&lt;pre class="language-bicep"&gt;&lt;code&gt;resource containerApp 'Microsoft.App/containerApps@2023-05-01' = {
  name: containerAppName
  location: location
  properties: {
    managedEnvironmentId: containerAppEnv.id
    configuration: {
      ingress: {
        external: true
        targetPort: 8080
        transport: 'auto'
        allowInsecure: false
      }
      secrets: [
        {
          name: 'storage-account-key'
          value: storageAccountKey
        }
      ]
    }
    template: {
      containers: [
        {
          name: 'open-webui'
          image: 'ghcr.io/open-webui/open-webui:main'
          resources: {
            cpu: 2
            memory: '4Gi'
          }
          volumeMounts: [
            {
              volumeName: 'open-webui'
              mountPath: '/app/backend/data'
            }
          ]
        }
      ]
      volumes: [
        {
          name: 'open-webui'
          storageType: 'AzureFile'
          storageName: meStorage.name
          mountOptions: 'nobrl'
        }
      ]
      scale: {
        minReplicas: 1
        maxReplicas: 1
        rules: [
          {
            name: 'http-rule'
            http: {
              metadata: {
                concurrentRequests: '10'
              }
            }
          }
        ]
      }
    }
  }
}

output storageAccountName string = storageAccount.name
output fileShareName string = fileShare.name
output containerAppFqdn string = containerApp.properties.configuration.ingress.fqdn
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 dir="auto"&gt;Usage&lt;/h2&gt;
&lt;hr&gt;
&lt;p dir="auto"&gt;Install Azure CLI and Bicep CLI on your computer, and combine the scripts above to &lt;code&gt;main.bicep&lt;/code&gt;.&lt;/p&gt;
&lt;p dir="auto"&gt;Create a resource group first:&lt;/p&gt;
&lt;pre class="language-powershell"&gt;&lt;code&gt;az group create --name openwebui-test996-rg --location japaneast&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then one key deployment!!!&lt;/p&gt;
&lt;pre class="language-powershell"&gt;&lt;code&gt;az deployment group create --resource-group openwebui-test996-rg --template-file main.bicep --parameters storageAccountName=openwebuistorage2996&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice the storage account name must be globally unique. If your deployment blow up, it is very possible that the name is already taken, just change another name.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-c23fd8fe-000d-4a1e-836c-7150dd11a2ab.png" border="0" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;The final resource group:&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-e9161af9-4479-4d3b-83e7-a133e45ea66b.png" border="0" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Container log:&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-734f3a1f-93d5-40af-8ba2-aeafb3564f14.png" border="0" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Finally, your Open WebUI application is now running!&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-54e713a9-1aa1-43f1-a018-58d1dc0c6991.png" border="0" loading="lazy"&gt;&lt;/p&gt;
&lt;h2 dir="auto"&gt;Wrapping Up&lt;/h2&gt;
&lt;hr&gt;
&lt;p dir="auto"&gt;With this one Bicep file, you get:&lt;/p&gt;
&lt;ul class="" dir="auto"&gt;
&lt;li class="text-start"&gt;Persistent storage for app data (survives upgrades/restarts!)&lt;/li&gt;
&lt;li class="text-start"&gt;A managed container app, easily upgradable by swapping container images.&lt;/li&gt;
&lt;li class="text-start"&gt;Centralized, queryable logs.&lt;/li&gt;
&lt;li class="text-start"&gt;All-in-one deployment&amp;mdash;no clicking around the portal!&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description><category>AI</category><category>DevOps</category><category>Microsoft Azure</category><guid isPermaLink="false">b6cce0d3-ef73-4428-85e3-fe89f607b311</guid><pubDate>2025-07-24T05:20:05Z</pubDate><updated xmlns="http://www.w3.org/2005/Atom">2025-07-24T05:20:05Z</updated></item><item><title>Auto Generating Incremental Resource Names in Azure Bicep</title><link>https://edi.wang/post/2025/5/19/auto-generating-incremental-resource-names-in-azure-bicep</link><description>&lt;h2&gt;Problem&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;Bicep's syntax is already pretty clean, but sometimes you run into situations like needing to concatenate strings or format names repeatedly, doing small calculations based on input parameters, or having long, messy expressions inside resource definitions. Let&amp;rsquo;s say you want to automatically generate a series of VM names like&amp;nbsp;&lt;code&gt;web-01&lt;/code&gt;, &lt;code&gt;web-02&lt;/code&gt;, and so on. Without functions, you&amp;rsquo;d have to write '&lt;code&gt;web-${padLeft(string(i+1), 2, '0')}&lt;/code&gt;' every time, makes you &lt;a href="https://996.icu/" target="_blank" rel="noopener"&gt;work 996&lt;/a&gt;. Let&amp;rsquo;s see how to make this easier.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;hr&gt;
&lt;h3&gt;User-Defined Function&lt;/h3&gt;
&lt;p&gt;A &lt;a href="https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/user-defined-functions?WT.mc_id=AZ-MVP-5002809" target="_blank" rel="noopener"&gt;Bicep function&lt;/a&gt; typically looks like this:&lt;/p&gt;
&lt;pre class="language-bicep"&gt;&lt;code&gt;@&amp;lt;decorator&amp;gt;(&amp;lt;argument&amp;gt;)
func &amp;lt;user-defined-function-name&amp;gt; (&amp;lt;argument-name&amp;gt; &amp;lt;data-type&amp;gt;, &amp;lt;argument-name&amp;gt; &amp;lt;data-type&amp;gt;, ...) &amp;lt;function-data-type&amp;gt; =&amp;gt; &amp;lt;expression&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Functions can take parameters and specify a return type.&lt;/p&gt;
&lt;p&gt;In this case, just create a function like below:&lt;/p&gt;
&lt;pre class="language-bicep"&gt;&lt;code&gt;func generateVmName(prefix string, index int) string =&amp;gt; '${prefix}-${padLeft(string(index + 1), 2, '0')}'

// Usage
resource vms 'Microsoft.Compute/virtualMachines@2022-08-01' = [for i in range(0, vmCount): {
  name: generateVmName('web', i)
  ...
}]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Full Bicep File&lt;/h3&gt;
&lt;p&gt;This example will deploy a set of Azure Virtual Machines with automatically generated names like &lt;code&gt;web-01&lt;/code&gt;, &lt;code&gt;web-02&lt;/code&gt;, etc.&lt;/p&gt;
&lt;pre class="language-bicep"&gt;&lt;code&gt;func generateVmName(prefix string, index int) string =&amp;gt; '${prefix}-${padLeft(string(index + 1), 2, '0')}'

@minLength(1)
param vmPrefix string = 'web'

@minValue(1)
@maxValue(10)
param vmCount int = 2

param location string = resourceGroup().location

@description('The subnet resource ID where all VMs will be deployed')
param subnetId string

@secure()
param adminPassword string

param adminUsername string = 'azureuser'

var vmSize = 'Standard_B1s'
var vmImage = {
  publisher: 'Canonical'
  offer: '0001-com-ubuntu-server-jammy'
  sku: '22_04-lts-gen2'
  version: 'latest'
}

resource vms 'Microsoft.Compute/virtualMachines@2022-08-01' = [
  for i in range(0, vmCount): {
    name: generateVmName(vmPrefix, i)
    location: location
    properties: {
      hardwareProfile: {
        vmSize: vmSize
      }
      storageProfile: {
        imageReference: vmImage
        osDisk: {
          createOption: 'FromImage'
          managedDisk: {
            storageAccountType: 'Standard_LRS'
          }
        }
      }
      osProfile: {
        computerName: generateVmName(vmPrefix, i)
        adminUsername: adminUsername
        adminPassword: adminPassword
        linuxConfiguration: {
          disablePasswordAuthentication: false
        }
      }
      networkProfile: {
        networkInterfaces: [
          {
            id: nicResources[i].id
          }
        ]
      }
    }
  }
]

resource nicResources 'Microsoft.Network/networkInterfaces@2022-09-01' = [
  for i in range(0, vmCount): {
    name: '${generateVmName(vmPrefix, i)}-nic'
    location: location
    properties: {
      ipConfigurations: [
        {
          name: 'ipconfig1'
          properties: {
            subnet: {
              id: subnetId
            }
            privateIPAllocationMethod: 'Dynamic'
          }
        }
      ]
    }
  }
]

output vmNames array = [for i in range(0, vmCount): generateVmName(vmPrefix, i)]
output nicNames array = [for i in range(0, vmCount): '${generateVmName(vmPrefix, i)}-nic']
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run this script:&lt;/p&gt;
&lt;pre class="language-powershell"&gt;&lt;code&gt;az deployment group create --resource-group bicep-demo-group --template-file main.bicep --parameters adminPassword=W0rk996S!ickICU&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This script will create the network, subnet, NICs, and VMs automatically.&lt;/p&gt;
&lt;p&gt;You will get:&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-de453ab5-f10f-47cb-be2c-c41450348289.png" border="0" loading="lazy"&gt;&lt;/p&gt;</description><category>DevOps</category><category>Microsoft Azure</category><guid isPermaLink="false">6ec95f94-efac-4f44-b28c-7d6b46f93b36</guid><pubDate>2025-05-19T04:41:45Z</pubDate><updated xmlns="http://www.w3.org/2005/Atom">2025-05-19T04:41:45Z</updated></item><item><title>Deploying Azure App Service and SQL Server with Bicep</title><link>https://edi.wang/post/2025/5/18/deploying-azure-app-service-and-sql-server-with-bicep</link><description>&lt;h2&gt;Background&lt;/h2&gt;
&lt;hr&gt;
&lt;p dir="auto"&gt;Last week at Microsoft Reactor Shanghai, I addressed a speech "Automating Infrastructure as Code (IaC) Deployment with Azure Bicep". One of the demo was how to use &lt;a href="https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/?WT.mc_id=AZ-MVP-5002809" target="_blank" rel="noopener"&gt;Bicep&lt;/a&gt; to deploy a complete Azure web app environment, including an &lt;a href="https://learn.microsoft.com/en-us/azure/app-service/?WT.mc_id=AZ-MVP-5002809" target="_blank" rel="noopener"&gt;App Service&lt;/a&gt; and an &lt;a href="https://learn.microsoft.com/en-us/azure/azure-sql/?view=azuresql&amp;amp;WT.mc_id=AZ-MVP-5002809" target="_blank" rel="noopener"&gt;Azure SQL Database&lt;/a&gt;. I know we didn&amp;rsquo;t have time to walk through the whole script, so I wanted to break it down here in a blog post.&lt;/p&gt;
&lt;p dir="auto"&gt;Let&amp;rsquo;s walk through this Bicep file together, and see what each part does!&lt;/p&gt;
&lt;h2 dir="auto"&gt;The Bicep File&lt;/h2&gt;
&lt;hr&gt;
&lt;p dir="auto"&gt;First, the complete Bicep file is:&lt;/p&gt;
&lt;pre class="language-bicep"&gt;&lt;code&gt;@description('Web App Name')
param webAppName string = 'demowebapp${uniqueString(resourceGroup().id)}'

@description('SQL Server administrator username')
param sqlAdminUsername string = 'demoadmin'

@secure()
@description('SQL Server administrator password')
param sqlAdminPassword string

@description('SQL Server Name')
param sqlServerName string = 'sqlserver${uniqueString(resourceGroup().id)}'

@description('SQL Database Name')
param sqlDbName string = 'demodb'

@description('App Service region')
param location string = resourceGroup().location

// Create App Service Plan
resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = {
  name: 'demoplan'
  location: location
  sku: {
    name: 'S1'
    tier: 'Standard'
    capacity: 1
  }
}

// Create Web App
resource webApp 'Microsoft.Web/sites@2022-03-01' = {
  name: webAppName
  location: location
  properties: {
    serverFarmId: appServicePlan.id
    httpsOnly: true
    siteConfig: {
      appSettings: [
        // Connection string to be added later
      ]
    }
  }
}

// Create SQL Server
resource sqlServer 'Microsoft.Sql/servers@2022-02-01-preview' = {
  name: sqlServerName
  location: location
  properties: {
    administratorLogin: sqlAdminUsername
    administratorLoginPassword: sqlAdminPassword
    version: '12.0'
  }
}

// Create SQL Database
resource sqlDb 'Microsoft.Sql/servers/databases@2022-02-01-preview' = {
  parent: sqlServer
  location: location
  name: sqlDbName
  properties: {
    collation: 'SQL_Latin1_General_CP1_CI_AS'
    maxSizeBytes: 2147483648
    sampleName: 'AdventureWorksLT'
  }
  sku: {
    name: 'S0'
    tier: 'Standard'
    capacity: 10
  }
}

// Allow Azure Services to access SQL Server (firewall rule)
resource allowAzureServices 'Microsoft.Sql/servers/firewallRules@2022-02-01-preview' = {
  name: 'AllowAzureServices'
  parent: sqlServer
  properties: {
    startIpAddress: '0.0.0.0'
    endIpAddress: '0.0.0.0'
  }
}

// Build database connection string
var connectionString = 'Server=tcp:${sqlServer.name}.database.windows.net,1433;Initial Catalog=${sqlDb.name};Persist Security Info=False;User ID=${sqlAdminUsername};Password=${sqlAdminPassword};MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;'

// Add the connection string to the Web App's application settings
resource webAppConnectionString 'Microsoft.Web/sites/config@2022-03-01' = {
  parent: webApp
  name: 'connectionstrings'
  properties: {
    DefaultConnection: {
      value: connectionString
      type: 'SQLAzure'
    }
  }
}

output webAppUrl string = webApp.properties.defaultHostName
output sqlServerName string = sqlServer.name
output sqlDbName string = sqlDb.name
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To deploy it, run this command in PowerShell:&lt;/p&gt;
&lt;pre class="language-powershell"&gt;&lt;code&gt;az deployment group create --resource-group bicep-demo-group --template-file .\webappsql.bicep --parameters sqlAdminPassword=DemoSQLAdmin123!&lt;/code&gt;&lt;/pre&gt;
&lt;p dir="auto"&gt;The result will be:&lt;/p&gt;
&lt;p dir="auto"&gt;&lt;img src="/image/img-4077ef43-5d67-4cac-8fb1-00039988b429.png" border="0" loading="lazy"&gt;&lt;/p&gt;
&lt;p dir="auto"&gt;&lt;img src="/image/img-a274fda3-ed60-4a68-a735-6aebbbb29b59.png" border="0" loading="lazy"&gt;&lt;/p&gt;
&lt;h2 dir="auto"&gt;Step by Step Explanation&lt;/h2&gt;
&lt;hr&gt;
&lt;h3&gt;Parameters&lt;/h3&gt;
&lt;p&gt;At the top, we define a few parameters. These are like variables that let you customize your deployment without editing the code every time.&lt;/p&gt;
&lt;pre class="language-bicep"&gt;&lt;code&gt;param webAppName string = 'demowebapp${uniqueString(resourceGroup().id)}'
param sqlAdminUsername string = 'demoadmin'
@secure()
param sqlAdminPassword string
param sqlServerName string = 'sqlserver${uniqueString(resourceGroup().id)}'
param sqlDbName string = 'demodb'
param location string = resourceGroup().location&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;webAppName&lt;/strong&gt;: The name of your web app. It default to a unique name so you don&amp;rsquo;t get name conflicts.&lt;br&gt;&lt;strong&gt;sqlAdminUsername&amp;nbsp;&lt;/strong&gt;and &lt;strong&gt;sqlAdminPassword&lt;/strong&gt;: The admin login for SQL Server. Note the password is marked as &lt;code&gt;@secure()&lt;/code&gt;, so it won&amp;rsquo;t show up in logs or outputs.&lt;br&gt;&lt;strong&gt;sqlServerName&lt;/strong&gt;: The name for your SQL Server, also unique.&lt;br&gt;&lt;strong&gt;sqlDbName&lt;/strong&gt;: The name of your SQL Database.&lt;br&gt;&lt;strong&gt;location&lt;/strong&gt;: Where all your resources will be (like &amp;ldquo;&lt;code&gt;westus&lt;/code&gt;&amp;rdquo; or &amp;ldquo;&lt;code&gt;eastasia&lt;/code&gt;&amp;rdquo;).&lt;/p&gt;
&lt;h3&gt;App Service Plan&lt;/h3&gt;
&lt;p&gt;Before you can run a web app, you need an &lt;a href="https://docs.azure.cn/en-us/app-service/overview-hosting-plans?WT.mc_id=AZ-MVP-5002809" target="_blank" rel="noopener"&gt;App Service Plan&lt;/a&gt;, it's the host of your app. Here, we&amp;rsquo;re creating a plan called &lt;code&gt;demoplan&lt;/code&gt;, using the Standard S1 tier.&lt;/p&gt;
&lt;pre class="language-bicep"&gt;&lt;code&gt;resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = {
  name: 'demoplan'
  location: location
  sku: {
    name: 'S1'
    tier: 'Standard'
    capacity: 1
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Web App&lt;/h3&gt;
&lt;p&gt;Next, we create the Web App itself, using the plan above. &amp;nbsp;It&amp;rsquo;s set to use HTTPS only. For now, the app settings are empty, we&amp;rsquo;ll add the database connection string later.&lt;/p&gt;
&lt;pre class="language-bicep"&gt;&lt;code&gt;resource webApp 'Microsoft.Web/sites@2022-03-01' = {
  name: webAppName
  location: location
  properties: {
    serverFarmId: appServicePlan.id
    httpsOnly: true
    siteConfig: {
      appSettings: [
        // Connection string to be added later
      ]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;SQL Server and SQL Database&lt;/h3&gt;
&lt;p&gt;Create a SQL Server (not a VM, but a logical server in Azure). You set the admin username and password here.&lt;/p&gt;
&lt;pre class="language-bicep"&gt;&lt;code&gt;resource sqlServer 'Microsoft.Sql/servers@2022-02-01-preview' = {
  name: sqlServerName
  location: location
  properties: {
    administratorLogin: sqlAdminUsername
    administratorLoginPassword: sqlAdminPassword
    version: '12.0'
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, create a SQL Database inside the server. It uses the&amp;nbsp;&lt;strong&gt;AdventureWorksLT &lt;/strong&gt;sample data (so you get some tables and data to play with right away). The database is set to the Standard S0 tier.&lt;/p&gt;
&lt;pre class="language-bicep"&gt;&lt;code&gt;resource sqlDb 'Microsoft.Sql/servers/databases@2022-02-01-preview' = {
  parent: sqlServer
  location: location
  name: sqlDbName
  properties: {
    collation: 'SQL_Latin1_General_CP1_CI_AS'
    maxSizeBytes: 2147483648
    sampleName: 'AdventureWorksLT'
  }
  sku: {
    name: 'S0'
    tier: 'Standard'
    capacity: 10
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, we need to let Azure services access the database, in this case, we are letting the App Service instance access our database.&lt;/p&gt;
&lt;pre class="language-bicep"&gt;&lt;code&gt;resource allowAzureServices 'Microsoft.Sql/servers/firewallRules@2022-02-01-preview' = {
  name: 'AllowAzureServices'
  parent: sqlServer
  properties: {
    startIpAddress: '0.0.0.0'
    endIpAddress: '0.0.0.0'
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Connection String&lt;/h3&gt;
&lt;p&gt;First, we build the connection string. For demo purpose, I hard code &lt;code&gt;database.windows.net&lt;/code&gt;, please use &lt;code&gt;environment()&lt;/code&gt; function in real world apps. Then, we set the connection string into the web app&amp;rsquo;s settings with name &lt;code&gt;DefaultConnection&lt;/code&gt;.&lt;/p&gt;
&lt;pre class="language-bicep"&gt;&lt;code&gt;// Build database connection string
var connectionString = 'Server=tcp:${sqlServer.name}.database.windows.net,1433;Initial Catalog=${sqlDb.name};Persist Security Info=False;User ID=${sqlAdminUsername};Password=${sqlAdminPassword};MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;'

// Add the connection string to the Web App's application settings
resource webAppConnectionString 'Microsoft.Web/sites/config@2022-03-01' = {
  parent: webApp
  name: 'connectionstrings'
  properties: {
    DefaultConnection: {
      value: connectionString
      type: 'SQLAzure'
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Outputs&lt;/h3&gt;
&lt;p&gt;After deployment, you&amp;rsquo;ll get the URL for your web app, plus the names of your SQL Server and Database.&lt;/p&gt;
&lt;pre class="language-bicep"&gt;&lt;code&gt;output webAppUrl string = webApp.properties.defaultHostName
output sqlServerName string = sqlServer.name
output sqlDbName string = sqlDb.name&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;So, in a nutshell, this Bicep file lets you deploy a ready-to-go Web App and SQL database in Azure, all connected and secured, with just a single deployment.&amp;nbsp;&lt;/p&gt;</description><category>DevOps</category><category>Microsoft Azure</category><guid isPermaLink="false">0c38df90-40ef-40cb-9ca0-7abc97c2cc40</guid><pubDate>2025-05-18T01:18:46Z</pubDate><updated xmlns="http://www.w3.org/2005/Atom">2025-05-18T01:18:46Z</updated></item><item><title>Solving "npm install" ECONNRESET Error on Azure DevOps</title><link>https://edi.wang/post/2023/8/16/npm-econnreset-azure-devops</link><description>&lt;h2&gt;Problem&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;A few months ago, my Angular project on Azure DevOps started to have build failure very often. Error message is "&lt;span style="color: rgb(224, 62, 45);"&gt;npm ERR! network read ECONNRESET&lt;/span&gt;", which indicates npm can't connect to internet. This problem does not happen every time, usually re-run failed jobs several times can produce a good build. But still, this is very annoying. So, I took some time to investigate and solve it.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-5c2f53b9-9306-495b-beb4-2dd4998edae5.png" border="0" loading="lazy"&gt;&lt;/p&gt;
&lt;h2&gt;Root Cause&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;First, I thought it was a IP rate limit by npm that reject Azure pipeline's agent. So I set up a VM on Azure as a self-host build agent. However, the problem continues.&lt;/p&gt;
&lt;p&gt;Then, I setup the same build agent on my laptop, which is outside of Azure environment, the build succeed every time. It seems to be npm rejecting the entire Azure IP address. But I can open &lt;a href="https://registry.npmjs.org/"&gt;https://registry.npmjs.org/&lt;/a&gt; without any problem on my Azure VM, how come the IP limit?&lt;/p&gt;
&lt;p&gt;Finally, I added &lt;strong&gt;verbose &lt;/strong&gt;log to the npm install task in my pipeline yaml file like this:&lt;/p&gt;
&lt;pre class="language-yaml"&gt;&lt;code&gt;jobs:
- job: Angular
  pool:
    vmImage: 'ubuntu-latest'
  steps:

  - task: Npm@1
    displayName: 'npm install'
    inputs:
      workingDir: '$(System.DefaultWorkingDirectory)/src'
      command: 'install'
      verbose: true&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run the build again, check it's output&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-29179f16-8207-4c66-b91e-af755c7246df.png" border="0" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;The log indicates that some packages are being fetched from Tencent registry instead of the official &lt;a href="https://registry.npmjs.org/"&gt;registry.npmjs.org&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Azure has problems communicating with Tencent server, this is why the build will fail very often.&lt;/p&gt;
&lt;p&gt;The reason why these packages are going to use Tencent server is because the URLs are written in &lt;code&gt;package-lock.json&lt;/code&gt; file. This file is automatically modified when developers run &lt;code&gt;npm install&lt;/code&gt; on their machines. A typical developer in China would have setup thier npm to use Tencent or Taobao source, due to the national firewall usually block official npm registry.&amp;nbsp;&lt;/p&gt;
&lt;pre class="language-bash"&gt;&lt;code&gt;npm config set registry https://mirrors.cloud.tencent.com/npm/&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And when the developer submit the code, &lt;code&gt;package-lock.json&lt;/code&gt; will have this URL inside it.&lt;/p&gt;
&lt;h2&gt;Fix&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;First, let all developers in the team use official npm registry.&lt;/p&gt;
&lt;pre class="language-bash"&gt;&lt;code&gt;npm config set registry https://registry.npmjs.org/&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, delete &lt;code&gt;node_modules &lt;/code&gt;folder and &lt;code&gt;package-lock.json&lt;/code&gt; under the project directory&lt;/p&gt;
&lt;p&gt;Then, run &lt;code&gt;npm install &lt;/code&gt;on the dev machine again.&lt;/p&gt;
&lt;p&gt;Now, all packages will be updated to use the official npm registry.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-70a6638c-7c62-425e-9537-5b2a35a72450.png" border="0" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Now, submit the code, Azure DevOps now runs &lt;code&gt;npm install&lt;/code&gt; without network error!&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-8584a7d9-9e75-4cba-b59d-43af932cd5a8.png" border="0" loading="lazy"&gt;&lt;/p&gt;</description><category>DevOps</category><category>Web Development</category><guid isPermaLink="false">0b86092d-313b-4ac6-a64e-635fe38196e4</guid><pubDate>2023-08-16T13:34:49Z</pubDate><updated xmlns="http://www.w3.org/2005/Atom">2023-08-16T13:34:49Z</updated></item><item><title>How I Built This Blog with Azure Services</title><link>https://edi.wang/post/2019/7/19/how-i-built-this-blog-with-azure-services</link><description>&lt;p&gt;My website (https://edi.wang) is an open-source blog system written in .NET Core and runs on Microsoft Azure. This article explains why and how the blog benefits from Azure's services.&lt;/p&gt;
&lt;h2&gt;History of this blog&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;The history of my blog dates to 2003, and the .NET version of the blog was originally built 10 years ago by the ASP.NET 2.0 WebForm VB and Access database and has been gradually maintained and upgraded to today, using ASP.NET Core MVC and Azure SQL Database.&lt;/p&gt;
&lt;p&gt;The current system, codename "Moonglade", is a complete rewrite of the previous .NET Framework version of the blog, which was "Nordrassil". For single-user blogs, this version extremely streamlined, eliminating the previous version of over-designed multi-user rights management, multi-level categories, MetaWeblog interface, article review workflow, and other useless components, and targets cloud-native environment.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-9f29c0af-0614-438c-9c99-8b6e2ec9b20a.png" border="0" alt="" width="1920" height="1080" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;This blog is currently deployed in the East Asia region of Microsoft Azure. The average server response time is 6.5ms, while page load time is around 4s, due to the impact of China&amp;rsquo;s network, I still have plenty of visitors from mainland China, besides these users, the page load time is actually 1.5-2s in a good network connection. In fact, it's not just .NET and SQL code optimization that made this performance, Azure's services are also a key role in success.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s check out how Azure empowered every piece of this blog system to achieve more.&lt;/p&gt;
&lt;h2&gt;App Service + Azure SQL Database&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;These two services are at the core services used by the blog and were the only two that the blog system adopted when it first went to the cloud. They can reduce a day or days of work to deploy a VM or on-premises server to ten minutes. And the price is much lower than using VM to deploy a website.&lt;/p&gt;
&lt;h6&gt;App Service&lt;/h6&gt;
&lt;p&gt;&lt;a href="https://docs.microsoft.com/en-us/azure/app-service/?WT.mc_id=AZ-MVP-5002809" target="_blank" rel="noopener"&gt;App Service&lt;/a&gt; is a PaaS service on Azure. Compared to traditional virtual machine deployment, App Service provides a fully managed platform where users don't have to worry about installing and configure virtual machines. As for the underlying system patches, website environment, web server configuration, have all been managed by Microsoft. As a result, we no longer need to &lt;a href="https://996.icu" target="_blank" rel="noopener"&gt;work 996 and get into ICU&lt;/a&gt; just to prepare a web environment. We can now do it in minutes, where traditional methods can take hours or even days.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Disadvantages of VM deployments&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Manual patch every month&lt;/li&gt;
&lt;li&gt;Manual install runtime and web server (IIS, .NET, Python, etc..)&lt;/li&gt;
&lt;li&gt;Manual configure website runtime (environment variables, path, etc..)&lt;/li&gt;
&lt;li&gt;Manual connect CI/CD (setup WebDeploy, FTP, etc..)&lt;/li&gt;
&lt;li&gt;Manual configure IP addresses, network, staging/prod environments, load balancer&lt;/li&gt;
&lt;li&gt;If website went boom, manually RDP to the machine to check logs and crash dump&lt;/li&gt;
&lt;li&gt;Difficult to scale&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Cloud-Native App Service&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fully managed platform&lt;/li&gt;
&lt;li&gt;Rich global regions&lt;/li&gt;
&lt;li&gt;Quickly build and scale Apps&lt;/li&gt;
&lt;li&gt;Smart monitoring&lt;/li&gt;
&lt;li&gt;High performance and security&lt;/li&gt;
&lt;li&gt;Low cost&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Azure portal provides a complete management interface on the web that can be used for dozens of operations, including deployments (different environments), diagnostics, settings, backups, scaling instances, binding domain names/SSL, and more.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-0de3bff2-5d61-40b2-b44a-5a4414fee7ed.png" border="0" alt="" width="700" height="577" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;While .NET should be the right choice, Microsoft doesn't just support .NET. You can choose Node.js, PHP, Python, or other languages, and you can use almost all the App Service experience, or even the Linux/Docker platform.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-908ec663-5613-459c-b4c3-c72209560a93.png" border="0" alt="" width="1074" height="704" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;In addition to the basic features like domain name, SSL, that each site will configure, my blog uses some of the additional features of App Service.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Deployment Slots&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The purpose of this feature is to create and switch between different environments. For example, my blog has only staging and production environments. The code is automatically published from the continuous integration of Azure DevOps to a staging environment that very close to production, and after testing, the deployment of the production environment is manually triggered.&lt;/p&gt;
&lt;p&gt;Azure can seamlessly switch between two environments so that your application is almost zero downtime. Traditionally, if a deployment went wrong, it usually takes a rollback operation, during which time users will surely take screenshots of your website and tweets to celebrate your mistake. So, the reason why my blog has always seemed very stable, it&amp;rsquo;s not I write correct code every time, but thanks to the staging slot, users won&amp;rsquo;t know if something has blown up before going to production.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-75843a40-3628-4733-a190-fb1ea7b6a1ca.png" border="0" alt="" width="641" height="309" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Backup&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Doing backup on traditional VM either requires manual operation, or write a complex script, or with the system scheduled task, or purchase third-party products. Anyway, it&amp;rsquo;s complex. Programmers can easily &lt;a href="https://996.icu/"&gt;996 into ICU&lt;/a&gt; for setting up this kind of environment.&lt;/p&gt;
&lt;p&gt;Azure App Service, on the other hand, can be configured with scheduled automatic backups just with a few mouse click on the web page, even includes a database that will be packaged together. When the website blows up, you can select the backup file and restore it instantly with one click. You can also download the backup package at any time to restore to your local environment.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-2d86cf2b-3b5a-4c20-87c8-085b0cbd1aec.png" border="0" alt="" width="819" height="715" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Scale-out&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Azure App Service makes it easy to configure scaling rules in just a few minutes. For example, when CPU usage reaches an average of more than 70% in 1 minute and lasts 10 minutes, an instance is automatically added.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-c0228237-9ced-402d-86bd-e826793f02f0.png" border="0" alt="" width="552" height="819" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Advanced Tools&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Kudu is a Microsoft open-source tool written in ASP. NET (unfortunately not .NET Core), it's the hero behind App Service. Websites on IIS can be published, managed, and diagnosed using Kudu. Microsoft not only opened the tool for free, but it also integrated it into Azure Portal, where I was able to use advanced features to manage the blog server.&lt;/p&gt;
&lt;p&gt;Kudu can view app settings, server environment variables, browse or edit website directory files, view live log streams, check IIS, node, dotnet, and other processes, and download dump files for local debugging. It's a Swiss Army knife for a website application.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-6191af33-f3ac-4aa6-b24a-d988b5d45c94.png" border="0" alt="" width="700" height="544" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-acc5d998-8420-4014-814d-7f9ea1d76167.png" border="0" alt="" width="759" height="667" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-6152dbe6-6199-4980-88d0-cf1f36104619.png" border="0" alt="" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-9ee1514c-6cd9-4668-ab73-0ab3ed56f3e4.png" border="0" alt="" width="832" height="679" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Other features of App Service are also useful. For example, you can find Let's Encrypt in "Extensions", which can automatically configure and renew free SSL certificates. You can also run scheduled tasks in Web Jobs. Learn more at &lt;a href="https://azure.microsoft.com/en-us/services/app-service/" target="_blank" rel="noopener"&gt;https://azure.microsoft.com/en-us/services/app-service/&lt;/a&gt;&lt;/p&gt;
&lt;h6&gt;Azure SQL Database&lt;/h6&gt;
&lt;p&gt;Like App Service, &lt;a href="https://docs.microsoft.com/en-us/azure/sql-database/?WT.mc_id=AZ-MVP-5002809" target="_blank" rel="noopener"&gt;Azure SQL Database&lt;/a&gt; is a fully-managed database service that includes almost all of SQL Server's capabilities and supports My SQL. Using Azure SQL Database means you don't have to worry about installing and configuring your database server environment, like patching your machines or struggle with firewall configurations. All common operations can be done in minutes in the Azure portal.&lt;/p&gt;
&lt;p&gt;Azure SQL Database can be managed with tools you're familiar with, such as SSMS and Azure Data Studio.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-0df530d4-03f3-4428-b03c-746029383504.png" border="0" alt="" width="388" height="555" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Azure allows internal services to access the database, while you can disable/enable this setting at any time. But excludes other public IPs. You can add IP whitelists on the web portal, or by SSMS and Azure Data Studio.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-0b2c1b6a-b298-43d2-bfaa-3766d35b3001.png" border="0" alt="" width="638" height="617" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Azure also provides data security (including compliance, such as masking sensitive data), performance optimization services, like which SQL Query is the slowest, and give accurate tuning recommendations like where to add indexes. Unfortunately, I am not so familiar with database technology, so I can't give you an in-depth introduction.&lt;/p&gt;
&lt;p&gt;Learn more about Azure SQL Database here: &lt;a href="https://azure.microsoft.com/en-us/services/sql-database/" target="_blank" rel="noopener"&gt;https://azure.microsoft.com/en-us/services/sql-database/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;DNS&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;&lt;a href="https://docs.microsoft.com/en-us/azure/dns/?WT.mc_id=AZ-MVP-5002809" target="_blank" rel="noopener"&gt;Azure DNS&lt;/a&gt; provides domain name resolving services with global ultra-high availability. It is hosted alongside your apps, supports all common DNS record types, and can quickly update DNS records. My blog switched to Azure DNS years ago.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-4a9fa56e-594f-40c4-8d59-def549e07326.png" border="0" alt="" width="1108" height="341" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Azure can now provide one-stop service from domain name registration to DNS resolution. However, you don&amp;rsquo;t have to register a domain name in Azure to use Azure DNS. Just change your NS records to match Azure DNS servers and you are ready to go.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-9ceedd59-40de-4129-ab08-95c6b435bd2c.png" border="0" alt="" width="1076" height="856" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Learn more about Azure DNS here: &lt;a href="https://azure.microsoft.com/en-us/services/dns/" target="_blank" rel="noopener"&gt;https://azure.microsoft.com/en-us/services/dns/&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;CDN&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;CDN is also one of the services usually used with websites. Thanks to Azure's global footprint, using &lt;a href="https://docs.microsoft.com/en-us/azure/cdn/?WT.mc_id=AZ-MVP-5002809" target="_blank" rel="noopener"&gt;Azure CDN&lt;/a&gt; is a better way to ensure global access to resources. In my blog system, the images of the articles are using CDN to speed up Azure Blob Storage, which helped a lot for users in mainland China.&lt;/p&gt;
&lt;p&gt;Azure CDN supports custom domain names and gives away SSL certificates for free! What a conscience does Microsoft have!&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-88631e8f-8f4e-46fa-a140-69554302584e.png" border="0" alt="" width="1109" height="372" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;In my blog system, the first time the user requests an image, the CDN would go to the corresponding Blob Storage to get the picture and cache it. Subsequent requests no longer need to go through Blob Storage, and you can return the results directly from the cache to increase responsiveness. With CDN, the response speed is significantly improved compared to the turtle speed in mainland China to direct access Blob Storage.&lt;/p&gt;
&lt;p&gt;Learn more about Azure CDN: &lt;a href="https://azure.microsoft.com/en-us/services/cdn/" target="_blank" rel="noopener"&gt;https://azure.microsoft.com/en-us/services/cdn/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Blob Storage&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;My blog system contains unstructured data such as pictures and is not suitable for saving with SQL Server databases. &lt;a href="https://docs.microsoft.com/en-us/azure/storage/blobs/storage-quickstart-blobs-dotnet?WT.mc_id=AZ-MVP-5002809" target="_blank" rel="noopener"&gt;Azure Blob Storage&lt;/a&gt;, on the other hand, is ideal for storing resources such as pictures, files, and more. Its benefits are geo-redundancy, multiple blob types, object mutability, and strong consistency.&lt;/p&gt;
&lt;p&gt;Microsoft offers SDK support, including .NET Core, to make it easy for programs to use blobs. For information on how my blog system uploads an image to Azure Blob, see the source code:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/EdiWang/Moonglade/tree/master/src/Moonglade.ImageStorage.AzureBlob" target="_blank" rel="noopener"&gt;https://github.com/EdiWang/Moonglade/tree/master/src/Moonglade.ImageStorage.AzureBlob&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-5664ebb4-10a0-4622-a7ab-1e8bc7b2014c.png" border="0" alt="" width="282" height="113" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Files stored on blobs can be managed on the Azure portal, or by Azure Storage Explorer, Microsoft's official client application.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-21c29329-a8bd-4ebe-91f7-31226872afd2.png" border="0" alt="" width="515" height="451" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-f953cdb5-93c8-41a8-8acc-1901ec2255bf.png" border="0" alt="" width="698" height="584" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Learn more about Blob Storage: &lt;a href="https://azure.microsoft.com/en-us/services/storage/blobs/" target="_blank" rel="noopener"&gt;https://azure.microsoft.com/en-us/services/storage/blobs/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Azure Active Directory&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;People familiar with Microsoft products may know about Active Directory, which helps companies manage employee accounts. &lt;a href="https://docs.microsoft.com/en-us/azure/active-directory/?WT.mc_id=AZ-MVP-5002809" target="_blank" rel="noopener"&gt;Azure Active Directory&lt;/a&gt; is a cloud-based domain service that not only manages accounts within your organization but also invites partners or temporary visitors.&lt;/p&gt;
&lt;p&gt;Microsoft's Office 365 also adopts the service. The most useful feature of Azure AD is that it can be easily integrated with your own applications for an SSO (single sign-on) experience. My blog also supports Azure AD single sign-on.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-03cdf894-ed54-4937-ab24-215978e3b054.png" border="0" alt="" width="562" height="497" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;For information on how my blog code integrates Azure AD, see:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/EdiWang/Moonglade/tree/master/src/Moonglade.Web.Authentication" target="_blank" rel="noopener"&gt;https://github.com/EdiWang/Moonglade/tree/master/src/Moonglade.Web.Authentication&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Learn more about Azure Active Directory: &lt;a href="https://azure.microsoft.com/en-us/services/active-directory/" target="_blank" rel="noopener"&gt;https://azure.microsoft.com/en-us/services/active-directory/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Azure DevOps&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;&lt;a href="https://docs.microsoft.com/en-us/azure/devops/?WT.mc_id=AZ-MVP-5002809" target="_blank" rel="noopener"&gt;Azure DevOps&lt;/a&gt; is not a new thing, but a masterpiece of Microsoft's renaming division. Formerly known as VSTS, or a more familiar name: TFS.&lt;/p&gt;
&lt;p&gt;It is a complete set of DevOps tools that cover all functions such as requirements management, code hosting, compilation and deployment, testing, and more. Using Azure DevOps means you no longer need to buy individual products for individual features and then integrate them at the cost of &lt;a href="https://996.icu/" target="_blank" rel="noopener"&gt;working 996 and get into ICU&lt;/a&gt;. And Azure DevOps is free for teams of up to 5 people and open source projects!&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-14dd9c83-b7c1-4ad0-acea-6eb5e95bf9fe.png" border="0" alt="" width="1249" height="653" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;The staging and production environments mentioned earlier in this article are configured by Azure DevOps.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-c49a5874-611b-4f72-a800-3fe77759ae27.png" border="0" alt="" width="674" height="686" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Because of the open-source nature, my blog doesn't use all the features of Azure DevOps, but only Pipelines. It provides CI/CD generation, testing, and deployment. You can connect to GitHub or any other Git provider (including, of course, Microsoft's own TFVC).&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-cf114f40-af72-41af-8552-9b806f4aa190.png" border="0" alt="" width="1075" height="568" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-06682e39-89ef-4611-85fe-9718cec1442e.png" border="0" alt="" width="692" height="446" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-25970a2c-d4cf-453c-a3a6-69f1a4bdef12.png" border="0" alt="" width="661" height="589" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Learn more about Azure DevOps: &lt;a href="https://azure.microsoft.com/en-us/services/devops/" target="_blank" rel="noopener"&gt;https://azure.microsoft.com/en-us/services/devops/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Azure Application Insights&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;After a website is developed and deployed. Monitoring is important to find problems, optimize performance, and so on. There are no systems without any bugs. Those can quickly find and solve the problem can dominate the market. &lt;a href="https://docs.microsoft.com/en-us/azure/azure-monitor/?WT.mc_id=AZ-MVP-5002809" target="_blank" rel="noopener"&gt;Azure Application Insights&lt;/a&gt; is a powerful APM tool that integrates from front end to backend of your entire web application to provide real-time data.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-c8ebf59a-5330-4c4e-a383-9ea753679cad.png" border="0" alt="" width="538" height="449" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;I can pin the metrics that I care most to the dashboard. Such as server response time, failed requests, page views, page load time. And I can view more information by clicking into each metric.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-e1ad47b3-fdc0-4db3-80d4-719357547dba.png" border="0" alt="" width="985" height="508" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;For failed requests, I can clearly see what environments and services have gone through all way till the database. What&amp;rsquo;s the exception, and what SQL query was run, and how much time they took.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-48960d04-af96-493a-8b09-25ff33f42d52.png" border="0" alt="" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Built-in performance analysis tools that support both client and server can help you quickly locate performance bottlenecks, such as which is the slowest requests, where the slowest SQL queries are, and so on.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-d942361d-da43-44f8-aee1-00e2c5bf3741.png" border="0" alt="" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;By the way, Azure Application Insights also comes with an Application Map that can see very intuitively the relationship of the web application.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-50a2f93f-f7e6-4c23-aaef-1fb60a436302.png" border="0" alt="" width="585" height="763" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Azure Application Insights can also be to integrate deeper into your app by using custom events to help get more useful data that more specific to your own application.&lt;/p&gt;
&lt;h2&gt;Closing&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;Building a cloud-based blog system with Azure has achieved great performance and developer efficiency compared to traditional approaches. I only used 8 out of hundreds of Azure products so far. My blog may take advantage of other Azure services in the future. For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Content moderating with Azure AI&lt;/li&gt;
&lt;li&gt;Integrate &lt;a href="https://docs.microsoft.com/en-us/azure/cognitive-srvices/bing-web-search/?WT.mc_id=AZ-MVP-5002809" target="_blank" rel="noopener"&gt;Bing Search&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Use &lt;a href="https://docs.microsoft.com/en-us/azure/key-vault/?WT.mc_id=AZ-MVP-5002809" target="_blank" rel="noopener"&gt;Azure Key Vault&lt;/a&gt; to manage secret keys&lt;/li&gt;
&lt;li&gt;Use &lt;a href="https://docs.microsoft.com/en-us/azure/cosmos-db/?WT.mc_id=AZ-MVP-5002809" target="_blank" rel="noopener"&gt;Cosmos DB&lt;/a&gt; to store data&lt;/li&gt;
&lt;li&gt;Use &lt;a href="https://docs.microsoft.com/en-us/azure/app-service/webjobs-create?WT.mc_id=AZ-MVP-5002809" target="_blank" rel="noopener"&gt;Web Jobs&lt;/a&gt; to run scheduled tasks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I sill have a lot to learn, and I hope you can also achieve more with Microsoft Azure!&lt;/p&gt;</description><category>DevOps</category><category>Microsoft Azure</category><guid isPermaLink="false">ba564764-b34d-4dfc-8860-c35b79b4f0b5</guid><pubDate>2019-07-19T10:00:22Z</pubDate><updated xmlns="http://www.w3.org/2005/Atom">2019-07-19T10:00:22Z</updated></item><item><title>How to Pack a .NET Core Class Library and Upload to NuGet</title><link>https://edi.wang/post/2019/4/24/how-to-pack-a-net-core-class-library-and-upload-to-nuget</link><description>&lt;p&gt;NuGet is the package manager of the. NET world, we can both use official website &lt;a href="https://www.nuget.org/" target="_blank" rel="noopener"&gt;nuget.org&lt;/a&gt;, or build our own corporate and private servers. Now, with . NET Core, it's easier than ever to pack and upload a NuGet package, let's take a look!&lt;/p&gt;
&lt;h2&gt;NuGet Account&lt;/h2&gt;
&lt;hr /&gt;
&lt;p&gt;If you want your package to be used by every .NET developers in the world, then you need to have a nuget.org account to upload your own package.&amp;nbsp;Nuget.org can log in directly with Microsoft account.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-331c9f87-7fc7-47f9-a7d2-0b75feb16abf.png" alt="" width="370" height="217" /&gt;&lt;/p&gt;
&lt;p&gt;After logging in, it is recommended that you get a set of&lt;strong&gt; API Key&lt;/strong&gt;. Although this Key is not required in the scope of this article, but if you later want to publish the NuGet package through the NuGet CLI or automation tools, such as Azure DevOps, you will need it.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-24801d6e-8a86-4367-b61c-8f17c888d050.png" alt="" width="297" height="433" /&gt;&lt;/p&gt;
&lt;h6&gt;Create NuGet Package in Visual Studio 2019&lt;/h6&gt;
&lt;hr /&gt;
&lt;p&gt;Take one of my class library&lt;strong&gt; Edi.Blog.Pingback&lt;/strong&gt; as an example, after the development is complete, right-click on the project file and open the &lt;strong&gt;Properties&lt;/strong&gt; dialog.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-cc680358-4950-40fa-98c0-aa4c2898063c.png" alt="" width="295" height="329" /&gt;&lt;/p&gt;
&lt;p&gt;On the &lt;strong&gt;Package&lt;/strong&gt; tab, select &lt;strong&gt;Generate NuGet package on builds&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-c16b54ff-82bd-40c7-b5f8-32c2e596f68d.png" alt="" width="677" height="779" /&gt;&lt;/p&gt;
&lt;p&gt;And fill in some necessary information. The&lt;strong&gt; Package ID&lt;/strong&gt; is the most important, that is, the ID used when the end user installs your NuGet package. It's not the same as &lt;strong&gt;Product&lt;/strong&gt;. An example is Json.NET, whose Package ID is Newtonsoft.Json, not Json.NET. In general, in order to avoid confusion, I recommend that you keep the Package ID and Product name consistent.&lt;/p&gt;
&lt;p&gt;The second is the version number, and &lt;strong&gt;Package version&lt;/strong&gt; is the version that will eventually appear on the Nuget.org page, as well as the version used to specify the installation version through the NuGet CLI. In order to avoid confusion, it is also recommended to be consistent with &lt;strong&gt;Assembly/File version&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Additional information can be filled in as needed, the more detailed the better. For example, my package was open source with the &lt;strong&gt;MIT&lt;/strong&gt; protocol in GitHub, and I can fill out the repository&amp;nbsp; address (&lt;strong&gt;Repository URL&lt;/strong&gt;), as well as the &lt;strong&gt;Licensing&lt;/strong&gt;. Even if it is not an open source package, we are advised to fill out the licensing. Otherwise, there will bea a warning after the package is uploaded to nuget.org.&lt;/p&gt;
&lt;p&gt;In fact, these operations on the VS interface are ultimately reflected in the project file (Edi.Blog.Pingback.csproj), that is, if you are a VSCode guy, you can also manually edit the project files to complete this series of settings.&lt;/p&gt;
&lt;pre class="language-markup"&gt;&lt;code&gt;&amp;lt;PropertyGroup&amp;gt;
  &amp;lt;TargetFramework&amp;gt;netcoreapp2.2&amp;lt;/TargetFramework&amp;gt;
  &amp;lt;GeneratePackageOnBuild&amp;gt;true&amp;lt;/GeneratePackageOnBuild&amp;gt;
  &amp;lt;Authors&amp;gt;Edi Wang&amp;lt;/Authors&amp;gt;
  &amp;lt;Company&amp;gt;edi.wang&amp;lt;/Company&amp;gt;
  &amp;lt;Description&amp;gt;Pingback sender and receiver used in my blog system.&amp;lt;/Description&amp;gt;
  &amp;lt;RepositoryUrl&amp;gt;https://github.com/EdiWang/Edi.Blog.Pingback&amp;lt;/RepositoryUrl&amp;gt;
  &amp;lt;PackageProjectUrl&amp;gt;https://edi.wang&amp;lt;/PackageProjectUrl&amp;gt;
  &amp;lt;AssemblyVersion&amp;gt;10.1.0.0&amp;lt;/AssemblyVersion&amp;gt;
  &amp;lt;FileVersion&amp;gt;10.1.0.0&amp;lt;/FileVersion&amp;gt;
  &amp;lt;Version&amp;gt;10.1.0&amp;lt;/Version&amp;gt;
  &amp;lt;PackageTags&amp;gt;Blog, Pingback&amp;lt;/PackageTags&amp;gt;
  &amp;lt;PackageLicenseExpression&amp;gt;MIT&amp;lt;/PackageLicenseExpression&amp;gt;
&amp;lt;/PropertyGroup&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, switch to &lt;strong&gt;Release&lt;/strong&gt; mode to compile your code. You will get a generated &lt;strong&gt;nupkg&lt;/strong&gt; package under the&lt;strong&gt; bin\release&lt;/strong&gt; folder. Release mode is used because the production environment should be used and the end user is expected to use the compiler-optimized DLL.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-e40766c7-ec89-4627-afc8-7c9e25e1a474.png" alt="" width="320" height="127" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-8957b2e6-df7f-4c2b-9ec7-a97f137199e7.png" alt="" width="271" height="115" /&gt;&lt;/p&gt;
&lt;h2&gt;Upload to nuget.org&lt;/h2&gt;
&lt;hr /&gt;
&lt;p&gt;The easiest way is to upload it through a Web page. Under buget.org's user menu, select &lt;strong&gt;Upload Package&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-255ad7b1-852a-4267-abd6-045db3a9abd6.png" alt="" width="330" height="439" /&gt;&lt;/p&gt;
&lt;p&gt;Then, drag and drop the nupkg file we just created to the upload box to finish upload.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-e3bc46fd-c97b-471f-b70c-85f1868ccafb.png" alt="" width="605" height="344" /&gt;&lt;/p&gt;
&lt;p&gt;After confirming that all the information is correct, click the &lt;strong&gt;Submit&lt;/strong&gt; button at the bottom of the page.&lt;/p&gt;
&lt;p&gt;Next, you'll see the status of the validation. It will be in &lt;strong&gt;Validating&lt;/strong&gt; status, then it will become &lt;strong&gt;Listed&lt;/strong&gt; after it is passed. It's usually 5 minutes.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-9e444fdf-a392-4ea6-a167-537091f83ad8.png" alt="" width="868" height="596" /&gt;&lt;/p&gt;
&lt;p&gt;However, after &lt;strong&gt;Listed&lt;/strong&gt;, there will still be a warning, meaning that the server's index has not been completed, the user can not search the version of the package. But you can specify a specific version number to install through the NuGet CLI or edit the project file to install the package as well.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-377d36fc-3ebc-451a-a230-c61b825fb05b.png" alt="" width="452" height="173" /&gt;&lt;/p&gt;
&lt;p&gt;When the warning disappears, your package will all ready!&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-5abdaf3c-d0ac-4ba9-8231-32dac34b37c1.png" alt="" width="861" height="615" /&gt;&lt;/p&gt;
&lt;h6&gt;Other Recommended Tools&lt;/h6&gt;
&lt;hr /&gt;
&lt;p&gt;NuGet CLI: &lt;a href="https://www.nuget.org/downloads"&gt;https://www.nuget.org/downloads&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;NuGet Package Explorer: &lt;a href="https://www.microsoft.com/store/productId/9WZDNCRDMDM3"&gt;https://www.microsoft.com/store/productId/9WZDNCRDMDM3&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-d1dcf32c-4726-4684-9244-565abbb341ce.png" alt="" /&gt;&lt;/p&gt;</description><category>DevOps</category><category>.NET</category><guid isPermaLink="false">0a866ce8-31dc-40c0-81ec-30508059df4f</guid><pubDate>2019-04-24T05:46:32Z</pubDate><updated xmlns="http://www.w3.org/2005/Atom">2019-04-24T05:46:32Z</updated></item><item><title>Incremental Build Number for .NET Core via Azure DevOps</title><link>https://edi.wang/post/2019/3/1/incremental-build-number-for-net-core-via-azure-devops</link><description>&lt;p&gt;People familiar with the. NET framework knows that we can let the compiler self-increase the version number by specifying AssemblyVersion as 10.0.*. But. NET core and. NET Standard are not. Even with open-source projects like MSBump, there are certain flaws. Typically, such a requirement happens on a CI/CD server. Let's take a look at how to easily handle it with Azure DevOps.&lt;/p&gt;
&lt;h2&gt;About Versioning in .NET Core Applications&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;I have wrote a post on the. NET Core Application Version number: &lt;a href="/post/2018/9/27/get-app-version-net-core"&gt;https://edi.wang/post/2018/9/27/get-app-version-net-core&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;We're going to control &lt;strong&gt;Version&lt;/strong&gt; field this time.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-699f382b-e833-45a6-9bca-c4e0ece7790d.png" border="0" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Use &lt;code&gt;&lt;strong&gt;dotnet build&lt;/strong&gt; &lt;/code&gt;to compile your application into dll files, and this version will be shown in the property window.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-261cadbf-daf2-4d90-a6c2-9a82a63e6091.png" border="0" alt="" width="418" height="364" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;But actually, the &lt;code&gt;&lt;strong&gt;build&lt;/strong&gt; &lt;/code&gt;command can have a parameter that sets a specific version number, like this:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;dotnet build /p:Version=10.0.8888.1234&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="/image/img-905b38e4-409f-4323-b165-12b9e96a4fc1.png" border="0" alt="" width="397" height="388" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Based on this, we can control the build number for a .NET Core application on build servers.&lt;/p&gt;
&lt;h2&gt;Why Not Use MSBump&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;You may heard of a project: Msbump, which can also be used to change the version number at compile time. However, it modifies the &lt;strong&gt;csproj&lt;/strong&gt; file at compile time and is a code change. The traditional. NET FX compilation system does not change the code. This is unacceptable to me because it is a kind of uncontrollable factor. It's not a good practice to check in the csproj file that just changed the version number. And instead of generating the version number based on the timestamp, MSBump by default, generates the version by the current version string in the project file. So in a development team, everyone could have inconsistent version of the same csproj file. So I decided to give up MSBump and try to use Microsoft's own technology to solve it's own problem.&lt;/p&gt;
&lt;h2&gt;Automatically Generate Version Numbers&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;In a fully automated CI environment, it is not possible for us to manually specify a version number each time. I need a rule and a method to generate it automatically.&lt;/p&gt;
&lt;p&gt;The rules I personally use is: &lt;strong&gt;[Major].[Minor].[Number of days from January 1, 2000].[Lucky Number]&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I use PowerShell to calculate the number of days from 1/1/2000.&lt;/p&gt;
&lt;pre class="language-markup"&gt;&lt;code&gt;$baseDate = [datetime]"01/01/2000"
$currentDate = $(Get-Date)
$interval = NEW-TIMESPAN &amp;ndash;Start $baseDate &amp;ndash;End $currentDate
$days = $interval.Days&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="/image/img-a1736e25-d957-4a1e-9ee0-35dfd7c6611b.png" border="0" alt="" width="693" height="186" loading="lazy"&gt;&lt;/p&gt;
&lt;h2&gt;Configure Azure DevOps&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;We basically need to tell Azure DevOps to calculate the version number, and use &lt;code&gt;&lt;strong&gt;/p:Version&lt;/strong&gt; &lt;/code&gt;parameter to build our project.&lt;/p&gt;
&lt;h3&gt;Environment variables&lt;/h3&gt;
&lt;p&gt;Add a variable named &lt;strong&gt;buildNumber&amp;nbsp;&lt;/strong&gt;in the build definition.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-9e8dde71-63bb-48bb-87c0-60c523f09e6a.png" border="0" alt="" width="922" height="495" loading="lazy"&gt;&lt;/p&gt;
&lt;h3&gt;PowerShell Task&lt;/h3&gt;
&lt;p&gt;Add a &lt;strong&gt;PowerShell Task&lt;/strong&gt; and put it as the &lt;strong&gt;first&lt;/strong&gt; step in the build pipeline. This PowerShell Task need to calculate the version number and assign it to &lt;strong&gt;buildNumber&amp;nbsp;&lt;/strong&gt;variable&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-ad40588a-aebc-4852-81e5-f4b6f2333b95.png" border="0" alt="" width="651" height="475" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;To set value to a variable in Azure DevOps pipeline, use &lt;code&gt;&lt;strong&gt;Write-Host&lt;/strong&gt;&lt;/code&gt;&amp;nbsp;as:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;##vso[task.setvariable variable=&lt;span style="color: #ff0000;"&gt;variableName&lt;/span&gt;]&lt;span style="color: #0000ff;"&gt;variableValue&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Then, our script will be&lt;/p&gt;
&lt;pre class="language-markup"&gt;&lt;code&gt;Write-Host "Generating Build Number"
$baseDate = [datetime]"01/01/2000"
$currentDate = $(Get-Date)
$interval = NEW-TIMESPAN &amp;ndash;Start $baseDate &amp;ndash;End $currentDate
$days = $interval.Days

Write-Host "##vso[task.setvariable variable=buildNumber]10.0.$days.1024"&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Choose &lt;strong&gt;Inline&lt;/strong&gt; and copy this script to the textarea.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-0108c2ba-a912-4946-b9e4-a16f1406a6dc.png" border="0" alt="" width="537" height="323" loading="lazy"&gt;&lt;/p&gt;
&lt;h3&gt;Modify .NET Core Task Parameter&lt;/h3&gt;
&lt;p&gt;Append this to the &lt;strong&gt;Arguments &lt;/strong&gt;filed of both &lt;strong&gt;Build&lt;/strong&gt; and &lt;strong&gt;Publish&lt;/strong&gt; steps.&lt;/p&gt;
&lt;pre class="language-markup"&gt;&lt;code&gt;/p:Version=$(buildNumber)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice: there should be a space [ ] before &lt;code&gt;&lt;strong&gt;/p&lt;/strong&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-0a40149e-053f-479d-abb4-072c0607e489.png" border="0" alt="" width="824" height="549" loading="lazy"&gt;&lt;/p&gt;
&lt;h2&gt;Compile and Done&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;Trigger a new build, you can see the version number is being set in the build log&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-d72c7459-185b-4960-bfb6-e71abbd39d57.png" border="0" alt="" width="481" height="247" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;and the application will have an updated version after deployment&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-2df88c51-6546-4d65-a4bc-f11b2436cfd6.png" border="0" alt="" width="364" height="258" loading="lazy"&gt;&lt;/p&gt;</description><category>DevOps</category><category>.NET</category><guid isPermaLink="false">0f61eaea-02d0-48a1-b3d1-fb5927bc17b1</guid><pubDate>2019-03-01T08:16:09Z</pubDate><updated xmlns="http://www.w3.org/2005/Atom">2019-03-01T08:16:09Z</updated></item><item><title>Send Email After Release Deployment in Azure DevOps</title><link>https://edi.wang/post/2019/2/21/send-email-after-release-deployment-in-azure-devops</link><description>&lt;p&gt;By default, Azure DevOps sends an email notification to team members after the compilation succeeds or fails. But in a CI/CD environment, we are more concerned about when the deployment is complete and then conducting manual or automated testing. Let's take a look at how to get Azure DevOps to automatically send mail notifications to team members after a successful deployment.&lt;/p&gt;
&lt;p&gt;Enter &lt;strong&gt;Project settings&lt;/strong&gt; in the bottom of the left sidebar.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-2bf1a4cb-b071-46cc-9a6b-0e9f26f6dd76.png" alt="" width="278" height="151" /&gt;&lt;/p&gt;
&lt;p&gt;Choose &lt;strong&gt;Notifications &amp;gt; New subscription&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-653c4eae-e918-4552-a28c-24b1b119caa9.png" alt="" width="608" height="345" /&gt;&lt;/p&gt;
&lt;p&gt;Click &lt;strong&gt;Release &amp;gt; A deployment is completed&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-b1cf6380-9934-4e07-9e0e-9a4cc8113e95.png" alt="" width="570" height="365" /&gt;&lt;/p&gt;
&lt;p&gt;The default settings are to send mail to all members of the current project when the deployment is complete. Specific settings can be changed according to your project needs, such as adding filtering criteria, selecting the Role of Notification member, and so on. Then click Finish to complete the configuration.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-616fd709-23e5-4868-b9e8-e6ca1b1b39f9.png" alt="" width="1011" height="687" /&gt;&lt;/p&gt;
&lt;p&gt;When you're done, you should be able to see this custom notification rule at the bottom of the list.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-b9338741-3686-44bf-9855-06a9524c6b04.png" alt="" width="368" height="391" /&gt;&lt;/p&gt;
&lt;p&gt;Now, trigger a deployment. If all goes well, you should be able to receive a similar email notification. I currently have two environments, and according to the configuration just now, any deployment on both environment will be notified. This is the staging environment:&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-428e8786-1895-4201-a4d1-ff5133ba6627.png" alt="" width="1167" height="1307" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-2872a8e9-9e7a-4e5d-b497-f34d23d1ea15.png" alt="" width="700" height="463" /&gt;&lt;/p&gt;</description><category>DevOps</category><guid isPermaLink="false">60783464-1d05-4754-b11e-e7bf8e008b26</guid><pubDate>2019-02-21T07:55:23Z</pubDate><updated xmlns="http://www.w3.org/2005/Atom">2019-02-21T07:55:23Z</updated></item><item><title>Fix .NET Core Website Failure After Deployment from Azure DevOps</title><link>https://edi.wang/post/2019/2/15/fix-net-core-website-failure-after-deployment-from-azure-devops</link><description>&lt;p&gt;I met a strange problem recently. After configuring the CI/CD pipeline with Azure DevOps and automatically deploy to Azure App Services, the. NET Core Website failed to start. Let's see how we can solve this problem.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-4687d7db-29b4-445b-965a-a584be387877.png" border="0" alt="" width="877" height="237" loading="lazy"&gt;&lt;/p&gt;
&lt;h2&gt;Find the issue&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;First of all, fortunately, this is a staging environment. I found that the site unexpectedly did not log anything, even log directory cannot be found, which is very strange. So I decided to enable development mode in Azure so that the site can output detailed error messages. If you are familiar with the classic ASP.NET, you may know that changing customError in web.config can show detailed error. But .NET Core doesn't have this thing, how can we do this? There's a small trick:&lt;/p&gt;
&lt;p&gt;The settings of .NET Core can be overwritten with environment variables. So we can take advantage of this by adding an environment variable to the settings of Azure App Services:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ASPNETCORE_ENVIRONMENT: Development&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-fe1df43f-67bd-4357-b4c3-8cc4a04d3a75.png" border="0" alt="" width="296" height="240" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;It is located in the &lt;strong&gt;Application settings&lt;/strong&gt; page.&lt;/p&gt;
&lt;p&gt;How could I know this? Actually, it is from Visual Studio itself:&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-0bc9167c-49ad-4b16-8d35-62d59d961b72.png" border="0" alt="" width="765" height="353" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Now, we can see a detailed error:&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-f693b437-6e3e-44fe-a798-ec4fd70d4a2c.png" border="0" alt="" width="1067" height="298" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;I can see that the code that gets executed is not in the root directory of the site.&amp;nbsp; So my code couldn't find the dependency, and it blew up.&lt;/p&gt;
&lt;h2&gt;What is going on?&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;I tried to deploy manually from VS, and it blew up, too. Redeploy in Azure DevOps, blew up. Even if I deactivate the CD pipeline, use kudu to build git code directly, also blew up!&lt;/p&gt;
&lt;p&gt;Eventually, I deleted the site and created a new App Service instance, published website from VS, and it was good. But once again when I deployed it with the CI/CD pipeline, it blew up again! I compared the settings from the previous good website, and found there's a new setting showing up:&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-7cb96a16-e6da-4f8b-a33e-d1cf26d497b6.png" border="0" alt="" width="328" height="128" loading="lazy"&gt;&lt;/p&gt;
&lt;h2&gt;What the heck is this?&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;I traced it back to a Microsoft Azure announcement: &lt;a href="https://github.com/azure/app-service-announcements/issues/84"&gt;https://github.com/azure/app-service-announcements/issues/84&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;It says:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Today, App Service supports a number of different techniques to deploy your files: msdeploy (aka WebDeploy), git, FTP, VSTS, ARM and more. But all these techniques have something in common: when all is said and done, your files are deployed under your wwwroot folder (specifically `d:\home\site\wwwroot`).&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;At that point, the runtime takes over, and it is no longer relevant how the files got there. The runtime can read all those files, and run whatever logic it needs to run based on them.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;But with Run From Package, things are very different.&lt;strong&gt; There is no longer a deployment step which copies the individual files to wwwroot. Instead, you just point it to a zip file, and the zip gets mounted on wwwroot as a read-only file system&lt;/strong&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;So this is why my website runtime root directory is changed.&lt;/p&gt;
&lt;h2&gt;Recover the website&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;To temporarily recover the website to run, very simple, just &lt;strong&gt;delete WEBSITE_RUN_FROM_PACKAGE from setting&lt;/strong&gt;, restart the site, you can revert to a good pre-deployment version. However, future deployments of Azure DevOps will not work. Because the real wwwroot directory will not be updated.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-2f905d60-6575-415c-9200-5c259e7cab94.png" border="0" alt="" width="336" height="210" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;(website recovered to pre-deployment version 10.0.6980.1000)&lt;/p&gt;
&lt;h2&gt;But we still need automated deployments&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;We need to modify the default values for Azure DevOps deployment tasks.&lt;/p&gt;
&lt;p&gt;Edit your Release definition, go to &lt;strong&gt;Tasks &amp;gt; Deploy Azure App Service&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-4aee5534-70d3-429c-a354-cb85f0e658ea.png" border="0" alt="" width="525" height="293" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Expand &lt;strong&gt;Additional Deployment Options&amp;nbsp;&lt;/strong&gt;and select &lt;strong&gt;Select deployment method&amp;nbsp;&lt;/strong&gt;and use &lt;strong&gt;Web Deploy&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-8730c604-31fe-4d50-a850-8675bf6186cd.png" border="0" alt="" width="351" height="224" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Now, issue a new release, wait for it to finish, you will have a good new version deployed at wwwroot directory correctly!&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-2f317fb3-9e93-448b-9922-cabd2b0802f6.png" border="0" alt="" width="356" height="223" loading="lazy"&gt;&lt;/p&gt;</description><category>DevOps</category><category>.NET</category><category>Microsoft Azure</category><guid isPermaLink="false">ea9333b0-6ad2-42aa-acc5-b70b8a829238</guid><pubDate>2019-02-15T13:05:47Z</pubDate><updated xmlns="http://www.w3.org/2005/Atom">2019-02-15T13:05:47Z</updated></item><item><title>Fix Azure DevOps TF14045 Error When Deploy to Azure</title><link>https://edi.wang/post/2019/2/13/fix-azure-devops-tf14045-error-when-deploy-to-azure</link><description>&lt;h2&gt;Problem&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;I encountered an error when I tried to setup CI/CD in Azure DevOps, I tried to deploy a .NET Core application to Azure App Service, but Azure DevOps won't connect to Azure. This is a known issue for Microsoft. Let's see how we can manually set up a connection between Azure DevOps and Azure to work around this issue.&lt;/p&gt;
&lt;p&gt;First, the error message I met was:&lt;/p&gt;
&lt;p&gt;Failed to get resource ID for resource type 'Microsoft.Web/Sites' and resourcename 'moonglade'.&amp;nbsp;&lt;strong&gt;Error: Could not fetch access token for Managed ServicePrincipal&lt;/strong&gt;. Please configure Managed Service Identity (MSI) for virtual machine'https://aka.ms/azure-msi-docs'.Status code: 400, status message: Bad Request&lt;/p&gt;
&lt;p&gt;This is caused by the authentication token is invalid or expired. However, when I try to Authorize a new connection, Azure DevOps blows up:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TF14045: The identity with type 'Microsoft.IdentityModel.Claims.ClaimsIdentity' could not be found.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-2bbf3053-394a-4e04-b1db-534f3f5b6f1e.png" border="0" alt="" width="701" height="456" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Microsoft has a known issue for this, but seems not fixed so far: &lt;a href="https://developercommunity.visualstudio.com/content/problem/412380/tf14045-the-identity-with-type-microsoftidentitymo-1.html"&gt;https://developercommunity.visualstudio.com/content/problem/412380/tf14045-the-identity-with-type-microsoftidentitymo-1.html&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;Go to &lt;strong&gt;Azure Active Directory &amp;gt; App registrations (Preview)&lt;/strong&gt;, Click "&lt;strong&gt;+ New registration&lt;/strong&gt;"&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-05ee2c53-0440-4374-b5e4-8bd62c6ddcdd.png" border="0" alt="" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Set a Name, for example: &lt;strong&gt;ediwang-AzureDevOps&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Choose "&lt;strong&gt;Accounts in this organizational directory only&lt;/strong&gt;"&lt;/p&gt;
&lt;p&gt;Set &lt;strong&gt;https://VisualStudio/SPN &lt;/strong&gt;as &lt;strong&gt;Redirection URL&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-ef7872e2-9029-4ce7-8e4c-dde8c7427333.png" border="0" alt="" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Set a &lt;strong&gt;Description&lt;/strong&gt; and choose "&lt;strong&gt;Never&lt;/strong&gt;" Expires.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-2bcf8623-6132-4761-a2a5-ee9481a79a17.png" border="0" alt="" width="406" height="468" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Copy the &lt;strong&gt;VALUE&lt;/strong&gt; in &lt;strong&gt;Client Secrets&lt;br&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-fd40059a-ed54-422a-9120-4b8ee56142d3.png" border="0" alt="" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;And also copy &lt;strong&gt;Application (client) ID&amp;nbsp;&lt;/strong&gt;and &lt;strong&gt;Directory (tenant) ID &lt;/strong&gt;in &lt;strong&gt;Overview&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-00dc64ef-2220-4e23-9e8e-a6a10c53853c.png" border="0" alt="" width="1068" height="322" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Go to &lt;strong&gt;Subscription &amp;gt; Access control (IAM)&amp;nbsp;&lt;/strong&gt;Search for the Name of the App, and &lt;strong&gt;Add a role assignment&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-ed40813f-cc8d-4b14-8ec9-6f61b4ae8e20.png" border="0" alt="" width="1111" height="570" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Set as &lt;strong&gt;Owner&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-0a3dfcb1-c623-43f8-a4b7-8faeaccd3ddc.png" border="0" alt="" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Go back to &lt;strong&gt;Azure DevOps&lt;/strong&gt;, click &lt;strong&gt;Manage&lt;/strong&gt; in &lt;strong&gt;Azure App Service Deploy&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-aa241afb-6b7d-4847-805e-410c2a2c4174.png" border="0" alt="" width="348" height="384" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Add an &lt;strong&gt;Azure Resource Manager&amp;nbsp;&lt;/strong&gt;in &lt;strong&gt;Service Connections&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;img src="/image/img-280ac92e-0d57-4c77-beb1-71a4da365de8.png" border="0" alt="" loading="lazy"&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Choose &lt;strong&gt;use the full version of the service connection dialog.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-99abb22c-c1ba-4dd0-b231-63f6b829d0ed.png" border="0" alt="" width="637" height="661" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Service pricipal client ID&amp;nbsp;&lt;/strong&gt;is &lt;strong&gt;Application (client) ID&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Service pricipal key&amp;nbsp;&lt;/strong&gt;is the &lt;strong&gt;VALUE&lt;/strong&gt; copied in &lt;strong&gt;Client Secrets&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;img src="/image/img-e2b78fdc-3b34-4e0e-b463-a6a63beb38ef.png" border="0" alt="" width="624" height="595" loading="lazy"&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Click &lt;strong&gt;Verify connection&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-b2a16b1d-435e-4074-b41c-cc0ae37193a6.png" border="0" alt="" width="1012" height="296" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;If nothing goes wrong, you will have a successful connection and deployment&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-31502be7-7496-40e6-9977-abf568d4341f.png" border="0" alt="" loading="lazy"&gt;&lt;/p&gt;</description><category>DevOps</category><category>Microsoft Azure</category><guid isPermaLink="false">37b490ce-8cf1-40fa-a3ac-22e79f706f43</guid><pubDate>2019-02-13T05:14:53Z</pubDate><updated xmlns="http://www.w3.org/2005/Atom">2019-02-13T05:14:53Z</updated></item><item><title>Build .NET Core 2.2 Projects on Azure DevOps</title><link>https://edi.wang/post/2018/12/13/build-dotnet-core-22-projects-on-azure-devops</link><description>&lt;p&gt;.NET Core 2.2 has been release for couple of weeks. However, the build pipeline is still failing these days. Microsoft has not installed .NET Core 2.2 SDK to the hosted VS2017 agents yet. Let's see how to work around this issue and have a success build for our .NET Core 2.2 apps.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-e82512e3-0f74-495c-b876-4e9ba54a0091.png" alt="" width="583" height="205" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Add Task&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Edit your build definition, and click the "+" button to add a new task.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-e8f68c5f-4f22-483c-bf95-0d3445735dbc.png" alt="" width="648" height="354" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Add .NET Core SDK Installer&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Find the ".NET Core SDK Installer" task in the gallery and click "Add" button.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-3a3fe588-5669-41a6-88f2-8b1967e8948d.png" alt="" width="970" height="350" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Task Order&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Move the ".NET Core SDK Installer" task to the first step of your build pipeline.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-60c396e3-e864-4e7c-a652-0472a656587b.png" alt="" width="646" height="637" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Edit Task&amp;nbsp;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Set the Version field to "2.2.100", and I recommend to change the "Display name" field to match the version also.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-152b794b-cbb1-4965-b6e7-95da7d1976d5.png" alt="" width="385" height="432" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. Save &amp;amp; queue&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-ebd9102d-8147-480a-a31a-50639ee00034.png" alt="" width="230" height="170" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6. Result&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Now you will have a sucessful build of your .NET Core 2.2 project.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-40df6f15-ccda-4b4d-90f3-dd86d3cac26c.png" alt="" /&gt;&lt;/p&gt;
&lt;p&gt;If you want to check the logs for what magic has happend, just click on the task, you can see the install log for the .NET Core 2.2 SDK.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-e0dec565-8e12-45c8-8b31-dee392ba0511.png" alt="" width="710" height="552" /&gt;&lt;/p&gt;
&lt;p&gt;Hope this post can help you out, and have a nice build!&lt;/p&gt;</description><category>DevOps</category><guid isPermaLink="false">07e7dce9-bcb8-43b6-9a93-7dae60099f88</guid><pubDate>2018-12-13T08:17:41Z</pubDate><updated xmlns="http://www.w3.org/2005/Atom">2018-12-13T08:17:41Z</updated></item><item><title>Configure VSTS Build for UWP Apps</title><link>https://edi.wang/post/2017/10/6/configure-vsts-build-for-uwp-apps</link><description>&lt;blockquote&gt;
&lt;p&gt;UWP Apps usually require a lot of time to compile a "release" package for submitting to Windows Store, and if you are working in a team, TFS/VSTS can help your team ensure that each check-in has a success build, and make it easier for deployment and testing.&amp;nbsp;Let's see a basic example of how to setup a CI build for a UWP App on VSTS.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Important Pre-condition&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;Before you do that, please ensure that your App is already build and running on your local machine.&lt;/p&gt;
&lt;p&gt;If you want the package can be used to upload to the Windows Store, you need to manually associate the App to the store and create App packages and validate the release on your machine for the first time.&lt;/p&gt;
&lt;p&gt;VSTS build on host agents can not automatically increase the build number in your App's manifest, you will need to do it manully in each check-in, or you can create a custom agent to do that (not discussed in this article)&lt;/p&gt;
&lt;h2&gt;Example App&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;My example App "&lt;strong&gt;QRCodeCreator&lt;/strong&gt;" is under project "&lt;strong&gt;Universal-Windows&lt;/strong&gt;", and there are many other Apps under the same parent directory. Usually in an enterprise, you won't put every App under a same Team Project. But if you do, there will be some different from the default build teamplate, which in this case, you can follow my steps.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-a91d3b3d-9361-4e39-825f-2c055f503dc9.png" border="0" alt="" width="686" height="779" loading="lazy"&gt;&lt;/p&gt;
&lt;h3&gt;Step 1&lt;/h3&gt;
&lt;p&gt;Go to "&lt;strong&gt;Build and Release&lt;/strong&gt;" under "&lt;strong&gt;Universal-Windows&lt;/strong&gt;", click "&lt;strong&gt;+New&lt;/strong&gt;" button.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-e82f5a99-1db1-4162-af30-cabc3a167a95.png" border="0" alt="" width="811" height="207" loading="lazy"&gt;&lt;/p&gt;
&lt;h3&gt;Step 2&lt;/h3&gt;
&lt;p&gt;Choose "&lt;strong&gt;Universal Windows Platform&lt;/strong&gt;" and click "&lt;strong&gt;Apply&lt;/strong&gt;"&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-5a2346d4-50ba-4511-a7ec-7da3a8011a78.png" border="0" alt="" width="811" height="585" loading="lazy"&gt;&lt;/p&gt;
&lt;h3&gt;Step 3&lt;/h3&gt;
&lt;p&gt;Set a Name, change Agent queue to "&lt;strong&gt;Hosted VS2017&lt;/strong&gt;", &lt;span style="color: #ff0000;"&gt;and select the "sln" path for this particular App. This is very important, otherwise VSTS will build every "sln" under "Universal-Windows".&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-2f4c48b2-0d42-424a-b800-4a352191222a.png" border="0" alt="" width="811" height="460" loading="lazy"&gt;&lt;/p&gt;
&lt;h3&gt;Step 4&lt;/h3&gt;
&lt;p&gt;In "&lt;strong&gt;Get sources&lt;/strong&gt;", change Server path to this particular App source folder (&lt;strong&gt;QRCodeCreator&lt;/strong&gt;), &lt;span style="color: #ff0000;"&gt;this is also very important, otherwise VSTS will get all content under "Universal-Windows" even it is only building "QRCodeCreator" solution.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-80821b5f-7b58-4c8a-a7db-7d0d8d9efb9d.png" border="0" alt="" width="811" height="540" loading="lazy"&gt;&lt;/p&gt;
&lt;h3&gt;Step 5&lt;/h3&gt;
&lt;p&gt;Click "&lt;strong&gt;Save &amp;amp; queue&lt;/strong&gt;" button, and test the build definition.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-37d83990-7656-4910-9a38-fac3b0bcc3e9.png" border="0" alt="" width="432" height="190" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Double check if the environment configuration is correct. In this case, I want the build artifacts can be used to upload to the Windows Store, so I need to use "&lt;strong&gt;release&lt;/strong&gt;" mode on&lt;strong&gt; x86, x64 and ARM&lt;/strong&gt;. You can change this in the build definition settings according to your demands.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-ca85d4bc-e850-4612-9f43-a855897021cd.png" border="0" alt="" width="730" height="689" loading="lazy"&gt;&lt;/p&gt;
&lt;h3&gt;Step 6&lt;/h3&gt;
&lt;p&gt;After the build is queued, click the build number on the message will open the build watcher window.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-be56e9cb-5409-4264-9b05-72bf68293001.png" border="0" alt="" width="457" height="256" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;If everything is correct, you should see the build success after 10-20 minutes depending on your project size. I ususally takes more time to build on VSTS comparing to your local machine.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-e43a99d8-7361-458a-8145-612960650fa9.png" border="0" alt="" width="811" height="665" loading="lazy"&gt;&lt;/p&gt;
&lt;h3&gt;Step 7&lt;/h3&gt;
&lt;p&gt;Now, we have validated the build works. Let's configure the CI trigger.&lt;/p&gt;
&lt;p&gt;Back to the build definition, click "&lt;strong&gt;Triggers&lt;/strong&gt;", and enable "&lt;strong&gt;Continuous Integration&lt;/strong&gt;" trigger. &lt;span style="color: #ff0000;"&gt;We also need to change the Path specification to this particular App folder, or every check-in under "Universal Windows" will trigger this build, which is not our intention.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;img src="/image/img-d100bff1-36c0-4126-8010-1bec87ab1c39.png" border="0" alt="" width="581" height="610" loading="lazy"&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;Step 8&lt;/h3&gt;
&lt;p&gt;The build number by VSTS will not automatically increase like you do it with Visual Studio manully. If you want to upload to the store, the build number must be higher than the old one already in the store. Now we need to go back to Visual Studio, open your project's manifest file, and manully increase the build number.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-abec19e8-3e9c-45c9-8e34-927e04042e69.png" border="0" alt="" width="811" height="421" loading="lazy"&gt;&lt;/p&gt;
&lt;h3&gt;Step 9&lt;/h3&gt;
&lt;p&gt;Now when you check-in the App source, a build will be triggered.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-d4fbba56-0d9d-46b8-8461-b2593ba31049.png" border="0" alt="" width="811" height="241" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;After it is done, go to "&lt;strong&gt;Artifacts&lt;/strong&gt;", now you can download or browse the App packages.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-29c1d2fb-b068-4e84-bc1d-641a61734ed9.png" border="0" alt="" width="794" height="419" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-69310c17-2167-4a19-bbfc-91505ad471cc.png" border="0" alt="" width="542" height="299" loading="lazy"&gt;&lt;/p&gt;
&lt;h3&gt;Step 10&lt;/h3&gt;
&lt;p&gt;Upload your wonderful App to Windows Store and change the world!!! You can see the "appxupload" file is successfully recognized by Windows Dev Center.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-6c931d07-f6bd-40fa-865c-5f566ae40abd.png" border="0" alt="" width="823" height="565" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="background-color: #ff0000; color: #ffff00;"&gt;&lt;strong&gt;NOTE FOR CHINESE DEVELOPERS&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="color: #ff0000;"&gt;Downloading artifacts from VSTS is very highly result in broken packages sometimes&amp;nbsp;&lt;strong&gt;even with a VPN connection&lt;/strong&gt;, for example, a 27 MB package will stop at 790 KB / 11 MB or others. Advise to RDP into a oversea VM to download the artifacts. Or you can setup a FTP and configure add build task to upload the artifacts to the FTP server and manually download it.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Appendix&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you have problem when generating App packages for Windows Store, please check the build configuration according to this table:&lt;/p&gt;
&lt;table class="table table-bordered"&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=""&gt;&lt;strong&gt;MSBuild Argument&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Value&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Description&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AppxPackageDir&lt;/td&gt;
&lt;td&gt;$(Build.ArtifactStagingDirectory)\AppxPackages&lt;/td&gt;
&lt;td&gt;Defines the folder to store the generated artifacts.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AppxBundlePlatforms&lt;/td&gt;
&lt;td&gt;$(Build.BuildPlatform)&lt;/td&gt;
&lt;td&gt;Allows you to define the platforms to include in the bundle.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AppxBundle&lt;/td&gt;
&lt;td&gt;Always&lt;/td&gt;
&lt;td&gt;Creates an appxbundle with the appx files for the platform specified.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style="background-color: #ffff00;"&gt;&lt;strong class=""&gt;UapAppxPackageBuildMode&lt;/strong&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td class=""&gt;&lt;span style="background-color: #ffff00;"&gt;StoreUpload&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style="background-color: #ffff00;"&gt;Defines the kind of appx package to generate. (Not included by default)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Reference:&amp;nbsp;&lt;a href="https://docs.microsoft.com/en-us/windows/uwp/packaging/auto-build-package-uwp-apps"&gt;https://docs.microsoft.com/en-us/windows/uwp/packaging/auto-build-package-uwp-apps&lt;/a&gt;&amp;nbsp;&lt;/p&gt;</description><category>Windows Development</category><category>DevOps</category><guid isPermaLink="false">a25f50c1-0de1-40fa-b323-bd6f1c918778</guid><pubDate>2017-10-06T02:41:23Z</pubDate><updated xmlns="http://www.w3.org/2005/Atom">2017-10-06T02:41:23Z</updated></item><item><title>Windows 10 UWP: How to Debug OnFileActivated Launch Failure</title><link>https://edi.wang/post/2017/8/25/uwp-debug-onfileactivated-launch-issue</link><description>&lt;p&gt;Today I am working on my Image Portray App, and I get my code blown up. The problem is, when the App is already started, it can open .ink file with double click from a file. But when the App is not launched, opening a .ink file to start the App will make it crash. As other UWP that supports file type association, I also use &lt;strong&gt;OnFileActivated&amp;nbsp;&lt;/strong&gt;event to launch my App.&lt;/p&gt;
&lt;p&gt;This kind of problem could not be debugged in a triditional way, because you can not find your App's process because it is not running, and you can't make Visual Studio attach to it. If you try to start your App from Visual Studio, you can not reproduce the problem.&lt;/p&gt;
&lt;p&gt;Well, the truth is, &lt;strong&gt;Visual Studio acutally can run debug against an App that is not currently started&lt;/strong&gt;, and VS debugger will hide in the darkness to observe if the App is started, when the App starts, VS will attach to it's process right away.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;First, you need to copy the &lt;strong&gt;pacakge name &lt;/strong&gt;of you App. It can be found in your App's manifest file:&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-66338ceb-f337-458f-9928-365aa27f9918.png" alt="" width="823" height="346" /&gt;&lt;/p&gt;
&lt;p&gt;Then, choose&amp;nbsp;&lt;strong&gt;DEBUG -&amp;gt; Other Debug Targes -&amp;gt; Debug Installed App Package...&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;img src="/image/img-98b7c721-e951-4ce0-a9bd-eeb7ab5df56e.png" alt="" width="823" height="640" /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In the filter textbox, paste in the &lt;strong&gt;package name&amp;nbsp;&lt;/strong&gt;you just copied, and then you can select your App.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Now, make sure you select "Do not launch, but debug my code when it starts"&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;img src="/image/img-b1e4df4c-4bfd-4d90-8c62-3c8bf4debba5.png" alt="" width="823" height="784" /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Then click "&lt;strong&gt;Start&lt;/strong&gt;" button, Visual Studio will now hide himself in the darkness, you can launch your App now.&lt;/p&gt;
&lt;p&gt;For my case, by double clicking a .ink file, Visual Studio will attach to the App process right away, and directly hit the exception point.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-9da4bf3d-6543-4720-a153-08899248615d.png" alt="" width="823" height="543" /&gt;&lt;/p&gt;
&lt;p&gt;That's how it's done!&lt;/p&gt;</description><category>Windows Development</category><category>DevOps</category><guid isPermaLink="false">63f0a51b-0298-4daf-8427-0f42aa56048e</guid><pubDate>2017-08-25T02:21:41Z</pubDate><updated xmlns="http://www.w3.org/2005/Atom">2017-08-25T02:21:41Z</updated></item><item><title>Remote Deploy and Debug UWP Apps</title><link>https://edi.wang/post/2017/7/15/uwp-remote-debug</link><description>&lt;p&gt;These days I am developing an App that need to be tested on multiple languages environment. I can set my local dev box to different language, and restart or sign out / sign in to Windows again to test different languages. But this is very&amp;nbsp;inconvenient. I want to setup a Windows 10 virtual machine with Chinese language and let Visual Studio 2017 deploy and debug the App right into the VM.&lt;/p&gt;
&lt;p&gt;Back the days in 2013, I've wrote &lt;a href="/post/2013/4/15/remote-debugging-on-surface-rt" target="_blank" rel="noopener"&gt;an article about remote debugging Surface RT&lt;/a&gt; (Windows 8 RT), it still works today with a few modification.&lt;/p&gt;
&lt;p&gt;First, you will need to download "Remote Tools for Visual Studio 2017", the latest version today is 15.2. You can download any language, it doesn't matter.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-43150c49-ca07-4380-b659-08d1f6cc1599.png" alt="" width="695" height="317" /&gt;&lt;/p&gt;
&lt;p&gt;Then, go to your Windows 10 VM. Open Windows Settings -&amp;gt; Update &amp;amp; Settings -&amp;gt; For developers, enable Developer mode and Network / USB discovery.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-b22997df-617a-4836-ad0f-25aeec12e7e3.png" /&gt;&lt;/p&gt;
&lt;p&gt;Now, install the remote debugging tools on the VM&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-42e28c85-770b-4e1d-a20d-acb841dbd9f1.png" alt="" width="602" height="395" /&gt;&lt;/p&gt;
&lt;p&gt;After installation, run the "Remote Debugger" as administrator&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-aa8aab55-534e-49c6-93fe-bf70f628a963.png" alt="" width="695" height="390" /&gt;&lt;/p&gt;
&lt;p&gt;Make sure you select all 3 checkboxes, and click "Configure remote debugging" button.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-69a03734-a5e0-4196-85aa-9ccc02a845f6.png" alt="" width="425" height="449" /&gt;&lt;/p&gt;
&lt;p&gt;After the tool launches, you can disable authentication in order to make life easier. Go to Tools -&amp;gt; Options.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-add945ad-ab4f-47bf-8e45-2b037634a0cb.png" alt="" width="353" height="117" /&gt;&lt;/p&gt;
&lt;p&gt;Choose "No Authentication" and check "Allow any user to debug".&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-32abfc62-59b1-4506-b7bd-5bbb03ba4e92.png" alt="" width="695" height="399" /&gt;&lt;/p&gt;
&lt;p&gt;Now, go back to your dev machine. Open your UWP project, in the debug tool bar, choose "Remote Machine"&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-df368ae2-42b7-44e7-9c42-2fcd8e7dd6b7.png" alt="" width="510" height="242" /&gt;&lt;/p&gt;
&lt;p&gt;If everything goes well, your VM will appear in the list, click "Select" to debug on this VM&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-a75d7819-d659-44d1-b034-6bc1708e3310.png" alt="" width="402" height="572" /&gt;&lt;/p&gt;
&lt;p&gt;Now, your App will be deployed and debugged on the VM!&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-f6392417-5289-4959-bf88-c252bc64ef21.png" alt="" width="695" height="344" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-f2af6a7a-62f4-4a6d-bae6-0d9d84b81697.png" alt="" width="695" height="513" /&gt;&lt;/p&gt;</description><category>Windows Development</category><category>DevOps</category><guid isPermaLink="false">60f7a3c4-cf88-4b5e-9fbc-51069f733cfd</guid><pubDate>2017-07-15T02:30:53Z</pubDate><updated xmlns="http://www.w3.org/2005/Atom">2017-07-15T02:30:53Z</updated></item><item><title>Configure Continuous Delivery for ASP.NET Core Website on Azure</title><link>https://edi.wang/post/2017/4/6/configure-continuous-delivery-for-aspnet-core-website-on-azure</link><description>&lt;p&gt;Today I am trying new DevOps tools in VS2017 and Azure, my goal is to automate the process from development to production for an exsiting ASP.NET Core project. I have encountered some issues, and with the help of Microsoft Support, I was able to solve them and share with you guys.&lt;/p&gt;
&lt;p&gt;First, there are two ways to configure CD for Azure Web Apps. I prefer create a website first, then configure it from Azure portal. The pros is that it can configure build and release automatically in your VSTS. But you can also do it from VSTS to Azure, however it will require you to set the build and release configuration by yourself.&lt;/p&gt;
&lt;h2&gt;1. Convert Your Solution Project to Branch&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;Azure can only choose branches from a VSTS project. So, make sure you convert or branch your existing folder. Just right click your project folder form VS2017 Team Explorer, and choose "Branching and Merging" "Convert to Branch"&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-3cad80ad-5973-4ec6-865a-c7c0a4db2f18.png" border="0" alt="" width="691" height="591" loading="lazy"&gt;&lt;/p&gt;
&lt;h2&gt;2. Link Your VSTS Account to Azure&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;In your Azure portal, find "Team Services accounts", and finish linking your VSTS&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-c2c7479e-55aa-4821-bfba-82c15ab433f6.png" border="0" alt="" width="793" height="357" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Azure will find the VSTS account that you have access to. Just link the one you want.&lt;/p&gt;
&lt;p&gt;But be aware, your VSTS account shoud only be linked to the same Azure subscription where&amp;nbsp;your target ASP.NET Core website is.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-23b2edfa-8dfe-469f-8669-294a79336d95.png" border="0" alt="" width="957" height="467" loading="lazy"&gt;&lt;/p&gt;
&lt;h2&gt;3. Create CD Configuration in Azure Web Apps&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;Enter your target ASP.NET Core app service.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Click "Continuous Delivery (Preview)" under "DEPLOYMENT" section, anc click "Configure" button.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-eaa7fa84-b996-4e05-b38e-a372a90164ae.png" border="0" alt="" width="675" height="632" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;In the configuration blade, find your VSTS project and the branch that you created in step 1.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-b366dbc1-17cf-44f2-9517-1cabb5582233.png" border="0" alt="" width="702" height="472" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Under the "Build" section, &lt;span style="color: #ff0000;"&gt;&lt;strong&gt;make sure you select "ASP.NET Core" or your build will blow up sky high.&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-ba34185e-6cba-47e8-9c94-6b79900227d4.png" border="0" alt="" width="693" height="343" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;For test and deploy, just leave default. Then click "OK" to wait for Azure to create the build and release definations in your VSTS.&lt;/p&gt;
&lt;h2&gt;4. Fix VSTS Build Defination&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;Your first CI / CD build will certainlly blow up sky high. This is where I contacted Microsoft Support. Looks like Azure is truly in preview for the CD functionality.&lt;/p&gt;
&lt;p&gt;For now, we will need to fix the build ourselves.&lt;/p&gt;
&lt;p&gt;The error is that, csproj file for the latest ASP.NET Core tooling will not be recognized. Thus, your build will fail, and there will be no deployment to Azure.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-053e505f-8947-4469-9fd3-d65758d51c9e.png" border="0" alt="" width="1214" height="152" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;To fix this, you need to edit your build defination. Click "Edit" button from the top right corner of the build tab.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-a7a52c1d-2ca1-4684-8a52-cc3e7de83dd7.png" border="0" alt="" width="645" height="290" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;In the "Options" tab, change "Default agent queue" to "Hosted VS2017" and save the build.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-071d133f-f371-4b86-b1c5-06a5710316ff.png" border="0" alt="" width="1378" height="464" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;Now, make some code changes or try trigger the build. It shoud work. And then it will try deploy to Azure.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-1bd9db0b-9280-4aac-99e5-5088d60a1289.png" border="0" alt="" width="884" height="622" loading="lazy"&gt;&lt;/p&gt;
&lt;h2&gt;5. Fix "Rejected in Production" Error (Optional)&lt;/h2&gt;
&lt;p&gt;If you find an error in your log looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-233c1917-9fd0-4e4d-a452-8030cdde9046.png" border="0" alt="" width="701" height="561" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;It is most likely that the target DLL file is in use. To fix this, stop your website, do the build and it will work!&lt;/p&gt;
&lt;p&gt;You can then start the website after the deployment.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;It looks like a web deploy bug for ASP.NET Core. If you do the same thing from VS2017, you will sometimes encounter this problem too. The only workround for this is to stop the website, deploy, start the website.&lt;/p&gt;
&lt;p&gt;Thank you for reading! Happy coding!&lt;/p&gt;</description><category>DevOps</category><category>Microsoft Azure</category><guid isPermaLink="false">2d060449-b902-4b98-a57a-f8525a077fa8</guid><pubDate>2017-04-06T08:17:37Z</pubDate><updated xmlns="http://www.w3.org/2005/Atom">2017-04-06T08:17:37Z</updated></item><item><title>Fix Visual Studio 2015 Razor Editor Blow Up</title><link>https://edi.wang/post/2015/8/9/razor-editor-blow-up-vs2015</link><description>&lt;p&gt;Visual Studio 2015 has been RTM for one month. However, it blows up very often, such as Razor editor. When I try to open any .cshtml file, it blows up with this error, then the syntax highlight is gone. I tried to uninstall all add-on, even repair install VS2015 can't get it working.&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-6de57dd1-3bf7-4d7c-bad3-69bf47389baf.jpg" alt="" width="1591" height="1012" /&gt;&lt;/p&gt;
&lt;p&gt;The detailed error message is:&lt;/p&gt;
&lt;pre&gt;System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---&amp;gt; System.ArgumentException: Item has already been added. Key in dictionary: 'RazorSupportedRuntimeVersion' &lt;/pre&gt;
&lt;p&gt;Someone has already filed a bug on "connect", and it is active:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://connect.microsoft.com/VisualStudio/feedback/details/1572978/no-intellisense-in-razor-views-argumentexception-razorsupportedruntimeversion-already-added-to-dictionary" target="_blank" rel="noopener noreferrer"&gt;https://connect.microsoft.com/VisualStudio/feedback/details/1572978/no-intellisense-in-razor-views-argumentexception-razorsupportedruntimeversion-already-added-to-dictionary&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The solution is：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;1. Close all instance of VS2015&lt;/p&gt;
&lt;p&gt;2. Run&amp;nbsp;cmd as administrator, go to VS2015 setup directory, and run:&lt;/p&gt;
&lt;pre&gt;devenv /resetuserdata&lt;/pre&gt;
&lt;p&gt;&lt;img src="/image/img-09e1b90f-f50d-49ac-9a2a-ad664325b22f.png" alt="" width="679" height="448" /&gt;&lt;/p&gt;
&lt;p&gt;3. Go to your project folder, delete a hidden folder named ".VS"&lt;/p&gt;
&lt;p&gt;&lt;img src="/image/img-dc036e4f-41cc-49ac-9ce5-413ea4e44405.png" alt="" width="595" height="741" /&gt;&lt;/p&gt;
&lt;p&gt;4. Restart VS2015, it will work fine!&lt;/p&gt;</description><category>DevOps</category><guid isPermaLink="false">0175ec26-2fb4-4e0c-9850-86ae3bb6fe61</guid><pubDate>2015-08-09T14:07:08Z</pubDate><updated xmlns="http://www.w3.org/2005/Atom">2015-08-09T14:07:08Z</updated></item><item><title>VS2013 Plug-in Development: How Do I Obtain the Path of a Selected File in Solution Explorer</title><link>https://edi.wang/post/2014/10/13/vs2013-vsix-how-to-get-selected-item-path-in-solution-explorer</link><description>&lt;p&gt;最近在爆个插件，有个需求就是能够在Solution Explorer中右键点击某个文件然后做一些下流的操作，那么首先就要想办法得到用户选择的文件或者文件们。肿么搞呢。研究了一下WebEssential的代码，总结了一下：&lt;/p&gt;
&lt;p&gt;首先，你需要获得DTE2对象，貌似指的是你当前的VS实例。为了方便使用定义成一个静态属性，放到package类里面：&lt;/p&gt;
&lt;p&gt;也就是继承Package类的那个类，比如public sealed class ForeverAlonePackage : Package&lt;/p&gt;
&lt;pre&gt;private static DTE2 _dte;

internal static DTE2 DTE
{
    get
    {
        if (_dte == null)
            _dte = ServiceProvider.GlobalProvider.GetService(typeof(DTE)) as DTE2;

        return _dte;
    }
}
&lt;/pre&gt;
&lt;p&gt;然后就可以写个助手类，用来获得当前选中的文件（们）了：&lt;/p&gt;
&lt;pre&gt;///&amp;lt;summary&amp;gt;Gets the full paths to the currently selected item(s) in the Solution Explorer.&amp;lt;/summary&amp;gt;
public static IEnumerable&amp;lt;string&amp;gt; GetSelectedItemPaths(DTE2 dte = null)
{
    var items = (Array)(dte ?? 你的PACKAGE类.DTE).ToolWindows.SolutionExplorer.SelectedItems;
    foreach (UIHierarchyItem selItem in items)
    {
        var item = selItem.Object as ProjectItem;

        if (item != null &amp;amp;&amp;amp; item.Properties != null)
            yield return item.Properties.Item("FullPath").Value.ToString();
    }
}
&lt;/pre&gt;
&lt;p&gt;至于路径，稍微处理一下就行：&lt;/p&gt;
&lt;pre&gt;///&amp;lt;summary&amp;gt;Gets the paths to all files included in the selection, including files within selected folders.&amp;lt;/summary&amp;gt;
public static IEnumerable&amp;lt;string&amp;gt; GetSelectedFilePaths()
{
    return GetSelectedItemPaths()
        .SelectMany(p =&amp;gt; Directory.Exists(p)
                         ? Directory.EnumerateFiles(p, "*", SearchOption.AllDirectories)
                         : new[] { p }
                   );
}
&lt;/pre&gt;
&lt;p&gt;另外，如果要获取选中的项目文件，可以这样：&lt;/p&gt;
&lt;pre&gt;///&amp;lt;summary&amp;gt;Gets the the currently selected project(s) in the Solution Explorer.&amp;lt;/summary&amp;gt;
public static IEnumerable&amp;lt;Project&amp;gt; GetSelectedProjects()
{
    var items = (Array)你的PACKAGE类.DTE.ToolWindows.SolutionExplorer.SelectedItems;
    foreach (UIHierarchyItem selItem in items)
    {
        var item = selItem.Object as Project;

        if (item != null)
            yield return item;
    }
}
&lt;/pre&gt;
&lt;p&gt;获取解决方案（sln）的根目录：&lt;/p&gt;
&lt;pre&gt;///&amp;lt;summary&amp;gt;Gets the directory containing the active solution file.&amp;lt;/summary&amp;gt;
public static string GetSolutionFolderPath()
{
    EnvDTE.Solution solution = 你的PACKAGE类.DTE.Solution;

    if (solution == null)
        return null;

    if (string.IsNullOrEmpty(solution.FullName))
        return GetRootFolder();

    return Path.GetDirectoryName(solution.FullName);
}
&lt;/pre&gt;
&lt;p&gt;这些代码在VS2010 SDK里也能用。&lt;/p&gt;</description><category>DevOps</category><guid isPermaLink="false">3d806416-f4f3-41c2-a7d7-907727749c15</guid><pubDate>2014-10-13T05:18:23Z</pubDate><updated xmlns="http://www.w3.org/2005/Atom">2014-10-13T05:18:23Z</updated></item></channel></rss>