

<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Max的程式語言筆記</title>
	<atom:link href="https://stackoverflow.max-everyday.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://stackoverflow.max-everyday.com</link>
	<description>我要當一個豬頭，快樂過每一天</description>
	<lastBuildDate>Wed, 20 May 2026 07:19:31 +0000</lastBuildDate>
	<language>zh-TW</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>

<image>
	<url>https://stackoverflow.max-everyday.com/wp-content/uploads/2017/02/max-stackoverflow-256.png</url>
	<title>Max的程式語言筆記</title>
	<link>https://stackoverflow.max-everyday.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Azure SQL 資料庫權限管理：不搶 Admin，照樣能下 SQL 指令</title>
		<link>https://stackoverflow.max-everyday.com/2026/05/azure-sql-admin-sql/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/05/azure-sql-admin-sql/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Wed, 20 May 2026 07:18:17 +0000</pubDate>
				<category><![CDATA[Azure 筆記]]></category>
		<category><![CDATA[azure]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8450</guid>

					<description><![CDATA[適合對象：剛接觸 Azure SQL Datab...]]></description>
										<content:encoded><![CDATA[
<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><strong>適合對象</strong>：剛接觸 Azure SQL Database 的開發者、需要協作管理資料庫權限的團隊</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">前言：「我只是要查資料，為什麼要給我 Admin？」</h2>



<p>在 Azure SQL Database 的世界裡，常常會遇到這樣的情境：</p>



<ul class="wp-block-list">
<li>同事 Alice 是團隊的 <strong>SQL Entra Admin（系統管理員）</strong></li>



<li>你只是要對某個測試資料庫跑幾個 SQL 腳本</li>



<li>有人建議「直接把你也設成 Entra Admin 就好」</li>
</ul>



<p>等等！這個做法雖然快，卻是個<strong>大地雷</strong>。本篇文章將說明正確做法，讓你在不動到管理員權限的前提下，順利取得資料庫存取能力。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">問題核心：SQL Entra Admin 的影響範圍</h2>



<p>Azure SQL Server 的 <strong>Entra Admin（Azure AD 管理員）</strong> 是 <strong>整個 SQL Server 實例層級</strong> 的最高管理員，每個 SQL Server <strong>只能設定一個</strong>（可以是使用者或群組）。</p>



<p>如果把你也加入成為 Entra Admin，會發生什麼事？</p>



<ol class="wp-block-list">
<li><strong>原本的 Admin（Alice）的權限會被覆蓋</strong>，她可能因此失去管理員身份。</li>



<li>你拿到的是遠超出需求的<strong>全站最高權限</strong>，違反資安最小授權原則（Principle of Least Privilege）。</li>



<li>未來權限管理會更混亂。</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">正確解法：請 Entra Admin 幫你在資料庫內建立使用者</h2>



<p><strong>最標準、最安全的做法</strong>是：請擁有 Admin 的 Alice，在目標資料庫內幫你建立一個 <strong>資料庫層級的使用者（Database User）</strong>，並只賦予你實際需要的權限。</p>



<h3 class="wp-block-heading">步驟一：Alice 執行 SQL 腳本建立你的帳號</h3>



<p>請 Alice 登入 <strong>Azure Portal → SQL Server → 目標資料庫（例如：<code>myapp-staging</code>）→ 查詢編輯器（Query Editor）</strong>，執行以下指令：</p>



<pre class="wp-block-code"><code>-- 步驟 1：為開發者的 Entra ID 帳號建立資料庫使用者
CREATE USER &#91;developer@example.onmicrosoft.com] FROM EXTERNAL PROVIDER;
GO

-- 步驟 2：依需求選擇一種方案賦予權限

-- &#x2705; 方案 A：唯讀（只查資料，不修改）
ALTER ROLE db_datareader ADD MEMBER &#91;developer@example.onmicrosoft.com];
GO

-- &#x2705; 方案 B：讀寫（查詢、新增、修改、刪除資料）
ALTER ROLE db_datareader ADD MEMBER &#91;developer@example.onmicrosoft.com];
ALTER ROLE db_datawriter ADD MEMBER &#91;developer@example.onmicrosoft.com];
GO

-- &#x26a0; 方案 C：資料庫擁有者（可改 Schema、建刪資料表，謹慎使用）
-- ALTER ROLE db_owner ADD MEMBER &#91;developer@example.onmicrosoft.com];
-- GO</code></pre>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><strong>提醒</strong>：<code>developer@example.onmicrosoft.com</code> 請替換為實際的 Azure Entra ID（Azure AD）帳號 Email。</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">步驟二：你自己登入資料庫下指令</h3>



<p>Alice 執行完畢後，你就可以用<strong>自己的帳號</strong>操作資料庫，完全不需要 Admin 權限：</p>



<ol class="wp-block-list">
<li>前往 <strong>Azure Portal → SQL Server → 目標資料庫</strong></li>



<li>點選左側選單的「<strong>查詢編輯器 (預覽)</strong>」</li>



<li>驗證方式選擇「<strong>以您的目前使用者身份繼續</strong>」（Entra ID 整合驗證）</li>



<li>登入後，即可在瀏覽器內直接執行 SQL 指令 <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f389.png" alt="🎉" class="wp-smiley" style="height: 1em; max-height: 1em;" /></li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">資料庫角色權限對照表</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>角色名稱</th><th>可執行操作</th><th>適合情境</th></tr></thead><tbody><tr><td><code>db_datareader</code></td><td><code>SELECT</code>（唯讀）</td><td>只需拉報表、查資料，不更動任何內容</td></tr><tr><td><code>db_datawriter</code></td><td><code>INSERT</code> / <code>UPDATE</code> / <code>DELETE</code></td><td>維護資料內容、執行資料修正腳本</td></tr><tr><td><code>db_owner</code></td><td>資料庫內所有操作（含 Schema 異動）</td><td>需要建立資料表、修改欄位等結構性工作</td></tr></tbody></table></figure>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><strong>建議</strong>：日常開發大多使用 <code>db_datareader + db_datawriter</code> 的組合，已足夠執行絕大多數的 SQL 操作。<code>db_owner</code> 應僅在確實需要異動資料庫結構時才賦予。</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">整體架構示意</h2>



<pre class="wp-block-code"><code>Azure SQL Server（myapp-sqlsvr）
│
├── Entra Admin：alice@example.com（整個 SQL Server 的管理員）
│
└── myapp-staging 資料庫
    ├── Database User：alice@example.com（繼承 Admin 權限）
    └── Database User：developer@example.com（db_datareader + db_datawriter）
                                              ↑
                                    這才是你應該有的層級！</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">小結</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>做法</th><th>安全性</th><th>對現有 Admin 的影響</th></tr></thead><tbody><tr><td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 把開發者也設為 SQL Entra Admin</td><td>危險，權限過大</td><td>可能覆蓋現有 Admin</td></tr><tr><td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 在資料庫內建立使用者並賦予角色</td><td>安全，符合最小授權</td><td>完全不影響</td></tr></tbody></table></figure>



<p>只要讓 <strong>擁有 Admin 的人執行一次 <code>CREATE USER</code> 腳本</strong>，你就能以最小、最安全的權限獨立作業，也不會干擾到其他管理員的設定。</p>



<p>這才是 Azure SQL 資料庫多人協作的標準做法。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p><em>Tags: <code>Azure</code> <code>SQL Database</code> <code>Entra ID</code> <code>Azure AD</code> <code>資料庫權限</code> <code>db_owner</code> <code>db_datareader</code></em></p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>如果有需要搶走 Entra Admin 可以使用這個 power shell script:</p>



<pre class="wp-block-code"><code># set-sql-entra-admin.ps1
# Usage: .\set-sql-entra-admin.ps1 -UserEmail lisa@example.onmicrosoft.com

param(
    &#91;Parameter(Mandatory=$true)]
    &#91;string]$UserEmail
)

$ResourceGroup = "rg-data-stg-jpe-001"
$ServerName    = "your-sqlsvr-name"

Write-Host "Looking up Object ID for: $UserEmail ..."
$ObjectId = az ad user show --id $UserEmail --query id -o tsv 2>&amp;1

if ($LASTEXITCODE -ne 0) {
    Write-Error "User not found: $UserEmail"
    exit 1
}

Write-Host "Setting Entra admin to: $UserEmail (OID: $ObjectId)"
az sql server ad-admin create `
    --resource-group $ResourceGroup `
    --server $ServerName `
    --display-name $UserEmail `
    --object-id $ObjectId

if ($LASTEXITCODE -eq 0) {
    Write-Host "Done! Entra admin is now: $UserEmail"
} else {
    Write-Error "Failed to set Entra admin."
    exit 1
}
</code></pre>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/05/azure-sql-admin-sql/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>如何確認 Azure Container Apps 上跑的是最新程式碼？</title>
		<link>https://stackoverflow.max-everyday.com/2026/05/azure-container-apps-ver-check/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/05/azure-container-apps-ver-check/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Tue, 19 May 2026 08:49:48 +0000</pubDate>
				<category><![CDATA[Azure 筆記]]></category>
		<category><![CDATA[azure]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8448</guid>

					<description><![CDATA[標籤：Azure、Container Apps、...]]></description>
										<content:encoded><![CDATA[
<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>標籤：Azure、Container Apps、Go、React、DevOps、部署驗證</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">前言</h2>



<p>你有沒有遇過這種狀況：</p>



<ol class="wp-block-list">
<li>修了一個 bug，<code>git push</code>，手動觸發部署。</li>



<li>等了三分鐘，部署顯示「成功」。</li>



<li>開瀏覽器一看…… <strong>bug 還在</strong>。</li>



<li>狐疑地 <code>docker logs</code> 看一下，發現 container 跑的根本是上個版本的 image。</li>
</ol>



<p>這就是我們踩過的坑。本文記錄我們如何系統性地解決這個問題——<strong>在 build 時把 git commit SHA 嵌入程式，透過 HTTP endpoint 暴露出來，讓部署後的驗證變成一行 curl 指令</strong>。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">問題根源：三個隱形的快取陷阱</h2>



<h3 class="wp-block-heading">陷阱 1：Go binary 沒有重新編譯</h3>



<p>我們的 Dockerfile 長這樣：</p>



<pre class="wp-block-code"><code># backend/Dockerfile
FROM alpine:3.19
COPY bin/backend /app/backend
ENTRYPOINT &#91;"/app/backend"]</code></pre>



<p>Dockerfile 直接把 <strong>預先編譯好的 binary</strong> 複製進 image。<br>如果 <code>go build</code> 沒有在 <code>docker build</code> 之前執行，那麼：</p>



<pre class="wp-block-code"><code>修改 Go 原始碼 → docker build → 推送 image → 部署
                ↑ bin/backend 是舊的！</code></pre>



<p><strong>image 裡永遠是舊的 binary</strong>，source code 的修改完全無效。</p>



<h3 class="wp-block-heading">陷阱 2：React <code>dist/</code> 沒有重新 build</h3>



<p>前端的 Dockerfile 同理：</p>



<pre class="wp-block-code"><code># frontend/Dockerfile
FROM nginx:alpine
COPY dist/ /usr/share/nginx/html</code></pre>



<p>如果 <code>npm run build</code> 沒有在 <code>docker build</code> 之前執行，<code>dist/</code> 是舊的，React 改動不生效。</p>



<h3 class="wp-block-heading">陷阱 3：Docker layer cache</h3>



<p>即使你真的重新執行了 <code>go build</code> 和 <code>npm run build</code>，Docker build cache 也可能讓你以為 image 更新了，實際上卻是沿用舊 layer。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">解法：嵌入 Git Commit SHA</h2>



<h3 class="wp-block-heading">概念</h3>



<pre class="wp-block-code"><code>build 時 ──► git rev-parse --short HEAD ──► 嵌入 binary
                                              └─► /api/version 回傳
                                                    │
部署後 ─────────────────────────────────────────────► curl /api/version
                                                       └─► 比對 commit SHA &#x2705; 或 &#x26a0;</code></pre>



<p>只要 <code>/api/version</code> 回傳的 commit hash 和 <code>git rev-parse --short HEAD</code> 一致，就能 100% 確認 Azure 上跑的是最新程式碼。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">實作步驟</h2>



<h3 class="wp-block-heading">Step 1：在 Go 主程式宣告版本變數</h3>



<p><code>cmd/main.go</code>：</p>



<pre class="wp-block-code"><code>package main

// 這些變數在 go build 時由 -ldflags 注入
// 預設值為 "dev"，方便本地開發
var (
    AppName    = "my-backend"
    Version    = "no-version"
    BuildTime  = "no-build-time"
    CommitHash = "no-commit-hash"
)</code></pre>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><strong>為什麼用 <code>var</code> 不用 <code>const</code>？</strong><br><code>-ldflags "-X package.VarName=value"</code> 只能注入 <code>var</code>，無法注入 <code>const</code>。</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">Step 2：註冊 <code>/api/version</code> endpoint</h3>



<p>在 <code>cmd/main.go</code> 的路由設定區塊加入：</p>



<pre class="wp-block-code"><code>import (
    "encoding/json"
    "net/http"
    "time"
)

// 放在其他路由旁邊，不需要 auth middleware
mux.HandleFunc("GET /api/version", func(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map&#91;string]string{
        "app":      AppName,
        "version":  Version,
        "commit":   CommitHash,
        "built_at": BuildTime,
    })
})</code></pre>



<p>本地測試：</p>



<pre class="wp-block-code"><code>go run ./cmd/main.go &amp;
curl http://localhost:8080/api/version
# {"app":"my-backend","built_at":"no-build-time","commit":"no-commit-hash","version":"no-version"}</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">Step 3：在 deploy 腳本加入 <code>go build -ldflags</code></h3>



<p>deploy 腳本（PowerShell 範例）裡，<code>Build-Backend</code> 函式改成：</p>



<pre class="wp-block-code"><code>function Build-Backend {
    # 1. 取得目前 commit SHA（若不在 git repo 則 fallback）
    $GIT_SHA = git rev-parse --short HEAD 2&gt;$null
    if (-not $GIT_SHA) { $GIT_SHA = "unknown" }

    $BUILD_TIME = Get-Date -Format "yyyy-MM-ddTHH:mm:ssZ"

    Write-Host "  &#91;Backend] 編譯 Go binary (commit=$GIT_SHA)..."

    # 2. 交叉編譯 for Linux/amd64（Azure Container Apps 執行環境）
    $env:GOOS       = "linux"
    $env:GOARCH     = "amd64"
    $env:CGO_ENABLED = "0"

    Push-Location $BACKEND_DIR
    go build `
        -ldflags "-X main.CommitHash=$GIT_SHA -X main.BuildTime=$BUILD_TIME -X main.Version=$IMAGE_TAG" `
        -o "bin\backend" `
        "./cmd/main.go"
    Pop-Location

    Remove-Item Env:\GOOS, Env:\GOARCH, Env:\CGO_ENABLED -ErrorAction SilentlyContinue

    # 3. 接著才 docker build（此時 bin/backend 已是最新）
    Write-Host "  &#91;Backend] 建立 Docker Image ($IMAGE_TAG)..."
    docker build -t "$ACR_LOGIN_SERVER/backend:$IMAGE_TAG" $BACKEND_DIR
    docker push "$ACR_LOGIN_SERVER/backend:$IMAGE_TAG"
}</code></pre>



<p><strong>關鍵順序</strong>：<code>go build</code> → <code>docker build</code> → <code>docker push</code> → ACA update，缺一不可。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">Step 4：部署後自動驗證版本</h3>



<p>同樣在 deploy 腳本，更新 ACA 之後加入：</p>



<pre class="wp-block-code"><code># 更新 Azure Container Apps
az containerapp update `
    --name $BACKEND_APP_NAME `
    --resource-group $RESOURCE_GROUP `
    --image "$ACR_LOGIN_SERVER/backend:$IMAGE_TAG"

# 等候新 revision 啟動
Write-Host "  &#x23f3; 等待後端啟動後驗證版本..."
Start-Sleep -Seconds 30

# 驗證
try {
    $deployed = (Invoke-RestMethod "https://your-app.example.com/api/version").commit
    if ($deployed -eq $GIT_SHA) {
        Write-Host "  &#x2705; 版本驗證通過: commit=$deployed" -ForegroundColor Green
    } else {
        Write-Host "  &#x26a0;  版本不符: deployed=$deployed, local=$GIT_SHA" -ForegroundColor Yellow
        Write-Host "     可能原因：ACA revision 還沒切換完成，稍後再手動確認" -ForegroundColor Yellow
    }
} catch {
    Write-Host "  &#x26a0;  無法連線驗證 /api/version: $_" -ForegroundColor Yellow
}</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">Step 5：在前端 footer 顯示 commit SHA（選用）</h3>



<p>前端也可以在 build 時嵌入 SHA，讓使用者（或開發者）能目視確認前端版本。</p>



<p><strong><code>deploy.ps1</code> Build-Frontend 函式</strong>：</p>



<pre class="wp-block-code"><code>function Build-Frontend {
    $GIT_SHA = git rev-parse --short HEAD 2&gt;$null
    if (-not $GIT_SHA) { $GIT_SHA = "unknown" }

    # 寫入 .env.production（只在 build 時使用，不會 commit 進 git）
    Set-Content -Path "$FRONTEND_DIR\.env.production" -Value @"
VITE_API_BASE_URL=https://your-app.example.com
VITE_APP_GIT_SHA=$GIT_SHA
VITE_APP_BUILD_TIME=$(Get-Date -Format "yyyy-MM-ddTHH:mm:ssZ")
"@

    Push-Location $FRONTEND_DIR
    npm run build
    Pop-Location

    docker build -t "$ACR_LOGIN_SERVER/frontend:$IMAGE_TAG" $FRONTEND_DIR
    docker push "$ACR_LOGIN_SERVER/frontend:$IMAGE_TAG"
}</code></pre>



<p><strong><code>vite-env.d.ts</code>（型別宣告）</strong>：</p>



<pre class="wp-block-code"><code>interface ImportMeta {
    readonly env: ImportMetaEnv
}

interface ImportMetaEnv {
    readonly VITE_API_BASE_URL: string
    readonly VITE_APP_GIT_SHA?: string
    readonly VITE_APP_BUILD_TIME?: string
}</code></pre>



<p><strong>React footer 元件</strong>：</p>



<pre class="wp-block-code"><code>const gitSha = import.meta.env.VITE_APP_GIT_SHA

// 在 footer 顯示
{gitSha &amp;&amp; (
    &lt;Text size="xs" c="dimmed"&gt;
        v{gitSha}
    &lt;/Text&gt;
)}</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">驗證流程總結</h2>



<p>部署完成後，只需一行：</p>



<pre class="wp-block-code"><code>curl https://your-app.example.com/api/version</code></pre>



<p>回傳範例：</p>



<pre class="wp-block-code"><code>{
  "app": "my-backend",
  "version": "dev-20250519153000",
  "commit": "a1b2c3d",
  "built_at": "2025-05-19T15:30:00Z"
}</code></pre>



<p>再對照本地：</p>



<pre class="wp-block-code"><code>git rev-parse --short HEAD
# a1b2c3d  ← 一致 &#x2705;</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">常見問題</h2>



<h3 class="wp-block-heading">Q：<code>commit=unknown</code> 是什麼意思？</h3>



<p>A：<code>git rev-parse --short HEAD</code> 找不到 git repo（例如 CI/CD 環境沒有 clone，或是在 zip 解壓的目錄執行），fallback 值為 <code>"unknown"</code>。<br>解法：確保 deploy 腳本執行時，工作目錄是 git repository 根目錄，或在 CI pipeline 裡先執行 <code>git fetch --unshallow</code>。</p>



<h3 class="wp-block-heading">Q：<code>-ldflags</code> 可以注入任何值嗎？</h3>



<p>A：只能注入 <code>string</code> 型別的套件層級 <code>var</code>。格式為 <code>-X 完整套件路徑.變數名稱=值</code>。<br>例如 <code>main.go</code> 裡的 <code>var CommitHash</code> → <code>-X main.CommitHash=abc1234</code>。<br>如果變數在子套件（如 <code>internal/version</code>），則為 <code>-X github.com/your-org/your-app/internal/version.GitCommit=abc1234</code>。</p>



<h3 class="wp-block-heading">Q：Docker layer cache 怎麼辦？</h3>



<p>A：每次 <code>go build</code>/<code>npm run build</code> 都會改變 <code>bin/backend</code> 或 <code>dist/</code> 的內容，讓 <code>COPY</code> 指令的 layer hash 改變，Docker 就不會沿用舊 cache。所以只要確保每次 <code>docker build</code> 前都有重新 build，cache 問題自然消除。</p>



<h3 class="wp-block-heading">Q：前後端版本不一致怎麼辦？</h3>



<p>A：目前前後端各自有獨立的 SHA，若同時部署則 SHA 相同；若分開部署則可能不同。可以考慮：</p>



<ol class="wp-block-list">
<li>永遠同時部署前後端（最簡單）</li>



<li>在 <code>/api/version</code> 加入 <code>frontend_commit</code> 欄位（由前端在打 API 時傳入）</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">小結</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>問題</th><th>解法</th></tr></thead><tbody><tr><td>不知道 Azure 上跑的是哪個版本</td><td><code>/api/version</code> endpoint 回傳 commit SHA</td></tr><tr><td>Go binary 沒有重新編譯</td><td><code>go build</code> 在 <code>docker build</code> 之前執行</td></tr><tr><td>React <code>dist/</code> 沒有重新 build</td><td><code>npm run build</code> 在 <code>docker build</code> 之前執行</td></tr><tr><td>部署後不知道有沒有成功</td><td>腳本自動 curl <code>/api/version</code> 比對 SHA</td></tr><tr><td>前端版本無從確認</td><td><code>VITE_APP_GIT_SHA</code> 嵌入 build，footer 顯示</td></tr></tbody></table></figure>



<p>整套機制只需要三個地方的修改：Go 主程式加幾行、deploy 腳本加 <code>go build -ldflags</code> 和驗證邏輯、前端加一個環境變數。成本很低，但讓部署的可信度大幅提升。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">附錄：完整 deploy.ps1 骨架</h2>



<pre class="wp-block-code"><code>param(
    &#91;switch]$UpdateOnly,
    &#91;string]$Target = "All"   # Backend | Frontend | All
)

# ── 設定區 ────────────────────────────────────────────────────────────
$RESOURCE_GROUP      = "your-resource-group"
$BACKEND_APP_NAME    = "your-backend-container-app"
$FRONTEND_APP_NAME   = "your-frontend-container-app"
$ACR_LOGIN_SERVER    = "your-acr.azurecr.io"
$PUBLIC_URL          = "https://your-app.example.com"
$BACKEND_DIR         = ".\your-backend"
$FRONTEND_DIR        = ".\your-frontend"

$IMAGE_TAG = "dev-$(Get-Date -Format 'yyyyMMddHHmmss')"
$GIT_SHA   = git rev-parse --short HEAD 2&gt;$null; if (-not $GIT_SHA) { $GIT_SHA = "unknown" }

# ── 函式區 ────────────────────────────────────────────────────────────
function Build-Backend {
    Write-Host "--- 重建並部署 Backend ---"
    Write-Host "  &#91;Backend] 編譯 Go binary (commit=$GIT_SHA)..."

    $env:GOOS = "linux"; $env:GOARCH = "amd64"; $env:CGO_ENABLED = "0"
    Push-Location $BACKEND_DIR
    go build -ldflags "-X main.CommitHash=$GIT_SHA -X main.Version=$IMAGE_TAG" `
             -o "bin\backend" "./cmd/main.go"
    Pop-Location
    Remove-Item Env:\GOOS, Env:\GOARCH, Env:\CGO_ENABLED -ErrorAction SilentlyContinue

    Write-Host "  &#91;Backend] 建立 Docker Image..."
    docker build -t "$ACR_LOGIN_SERVER/backend:$IMAGE_TAG" $BACKEND_DIR
    docker push  "$ACR_LOGIN_SERVER/backend:$IMAGE_TAG"

    Write-Host "  &#91;Backend] 更新 Container App..."
    az containerapp update --name $BACKEND_APP_NAME `
                           --resource-group $RESOURCE_GROUP `
                           --image "$ACR_LOGIN_SERVER/backend:$IMAGE_TAG"

    # 驗證
    Start-Sleep -Seconds 30
    try {
        $deployed = (Invoke-RestMethod "$PUBLIC_URL/api/version").commit
        if ($deployed -eq $GIT_SHA) {
            Write-Host "  &#x2705; 版本驗證通過: commit=$deployed" -ForegroundColor Green
        } else {
            Write-Host "  &#x26a0;  版本不符: deployed=$deployed, local=$GIT_SHA" -ForegroundColor Yellow
        }
    } catch {
        Write-Host "  &#x26a0;  無法連線驗證: $_" -ForegroundColor Yellow
    }
}

function Build-Frontend {
    Write-Host "--- 重建並部署 Frontend ---"

    Set-Content "$FRONTEND_DIR\.env.production" @"
VITE_API_BASE_URL=$PUBLIC_URL
VITE_APP_GIT_SHA=$GIT_SHA
"@

    Push-Location $FRONTEND_DIR
    npm run build
    Pop-Location

    docker build -t "$ACR_LOGIN_SERVER/frontend:$IMAGE_TAG" $FRONTEND_DIR
    docker push  "$ACR_LOGIN_SERVER/frontend:$IMAGE_TAG"

    az containerapp update --name $FRONTEND_APP_NAME `
                           --resource-group $RESOURCE_GROUP `
                           --image "$ACR_LOGIN_SERVER/frontend:$IMAGE_TAG"

    Write-Host "  &#x2705; Frontend 部署完成" -ForegroundColor Green
}

# ── 執行 ─────────────────────────────────────────────────────────────
az acr login --name ($ACR_LOGIN_SERVER -replace "\.azurecr\.io","")

if ($Target -in @("Backend","All")) { Build-Backend }
if ($Target -in @("Frontend","All")) { Build-Frontend }</code></pre>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/05/azure-container-apps-ver-check/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Azure 邀請外部使用者加入團隊</title>
		<link>https://stackoverflow.max-everyday.com/2026/05/azure-%e9%82%80%e8%ab%8b%e5%a4%96%e9%83%a8%e4%bd%bf%e7%94%a8%e8%80%85%e5%8a%a0%e5%85%a5%e5%9c%98%e9%9a%8a/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/05/azure-%e9%82%80%e8%ab%8b%e5%a4%96%e9%83%a8%e4%bd%bf%e7%94%a8%e8%80%85%e5%8a%a0%e5%85%a5%e5%9c%98%e9%9a%8a/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Tue, 19 May 2026 08:35:33 +0000</pubDate>
				<category><![CDATA[電腦相關應用]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8445</guid>

					<description><![CDATA[在 Azure (Microsoft Entra...]]></description>
										<content:encoded><![CDATA[
<p>在 Azure (Microsoft Entra ID) 中邀請外部使用者（例如其他公司的 E-mail 或 Gmail/Outlook 等）加入你的小組或團隊，通常有兩種最常見的情境：<strong>直接邀請到 Teams 團隊</strong>，或是<strong>透過 Azure 後台邀請為 B2B 外部來賓</strong>。</p>



<p>以下為您整理這兩種最常用的操作方法：</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">方法一：直接從 Microsoft Teams 邀請（最直覺、最推薦）</h2>



<p>如果你只是想讓對方加入某個特定的小組聊天、存取檔案或開會，直接在 Teams 軟體中操作最快。</p>



<ol start="1" class="wp-block-list">
<li><strong>開啟 Teams：</strong> 找到你想邀請對方的團隊（Team），點選團隊名稱旁邊的 <strong>「&#8230;」（更多選項）</strong>。</li>



<li><strong>新增成員：</strong> 選擇 <strong>「新增成員」 (Add member)</strong>。</li>



<li><strong>輸入 Email：</strong> 輸入該外部人員的完整電子郵件地址（例如 <code>example@gmail.com</code>）。</li>



<li><strong>新增為來賓：</strong> 系統會顯示「將 [Email] 新增為來賓」，點擊它，然後按一下 <strong>「新增」 (Add)</strong>。</li>



<li><strong>完成：</strong> 對方的信箱會收到一封邀請函，點擊信中的連結並按照提示驗證，就能加入你的 Teams 團隊了。</li>
</ol>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>注意：</strong> 如果系統提示「找不到相符項目」或無法新增，代表你們公司的 IT 管理員把「允許外部來賓」的功能關閉了，這時需要請管理員參考<strong>方法二</strong>的設定。</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">方法二：從 Azure 入口網站邀請（IT 管理員適用）</h2>



<p>如果你是 Azure 管理員，希望將外部人員正式加入組織的目錄（Microsoft Entra ID）中，方便後續指派多個群組或雲端資源：</p>



<ol start="1" class="wp-block-list">
<li><strong>登入後台：</strong> 登入 <a href="https://www.google.com/search?q=https://portal.azure.com/" target="_blank" rel="noreferrer noopener">Azure Portal</a> 或 <a href="https://entra.microsoft.com/" target="_blank" rel="noreferrer noopener">Microsoft Entra admin center</a>。</li>



<li><strong>前往使用者：</strong> 導覽至 <strong>Microsoft Entra ID</strong> -> <strong>使用者 (Users)</strong> -> <strong>所有使用者 (All users)</strong>。</li>



<li><strong>邀請外部使用者：</strong> 點選上方的 <strong>「新使用者」 (New user)</strong>，然後選擇 <strong>「邀請外部使用者」 (Invite external user)</strong>。</li>



<li><strong>填寫資料：</strong>
<ul class="wp-block-list">
<li><strong>身分識別：</strong> 輸入對方的電子郵件地址（必填）和姓名。</li>



<li><strong>群組和角色（選填）：</strong> 你可以直接在這裡將他加入對應的 Teams 背後的 M365 群組。</li>
</ul>
</li>



<li><strong>發送邀請：</strong> 填寫完畢後點選 <strong>「邀請」 (Invite)</strong>。</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 常見問題與排查</h2>



<ul class="wp-block-list">
<li><strong>為什麼對方收不到信？</strong>請請對方檢查「垃圾信件匣（Spam）」，邀請函通常是由 <code>invites@microsoft.com</code> 發出。</li>



<li><strong>外部人員需要付費授權嗎？</strong>不用。微軟的外部來賓（B2B）通常享有非常寬鬆的免費額度（通常是前 50,000 名每月活躍使用者免費），不需要為他們購買額外的授權。</li>



<li><strong>無法新增外部人員？</strong>這通常是組織原則限制。管理員需要到 <code>Microsoft Entra ID</code> -> <code>外部身分識別 (External Identities)</code> -> <code>外部協同作業設定 (External collaboration settings)</code> 中，將來賓權限調整為 <strong>「任何使用者都可以邀請來賓」</strong> 或 <strong>「成員和指派特定管理員角色的使用者可以邀請來賓」</strong>。</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>看來您已經在 Azure 中成功建立了外部使用者（杜甫），目前停留在該使用者的設定畫面。</p>



<p>如果您已經為這個使用者建立了一個 <strong>資源群組 (Resource Group, 簡稱 RG)</strong>，並希望讓他能夠存取、管理該 RG，接下來的關鍵步驟是<strong>賦予他該資源群組的存取權限（也就是設定 IAM 角色指派）</strong>。</p>



<p>請依照以下步驟操作：</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">下一步：將資源群組 (RG) 的權限指派給該外部使用者</h3>



<p>不要在目前這個「指派的角色」畫面操作（這裡是設定全域管理員等目錄角色的）。請改到您建立的 <strong>資源群組</strong> 頁面進行設定：</p>



<ol start="1" class="wp-block-list">
<li><strong>尋找您的資源群組：</strong> 在 Azure 頂端的搜尋列輸入您建立的資源群組名稱，或是從左側選單點選 <strong>「資源群組」 (Resource groups)</strong>，並點進該 RG。</li>



<li><strong>進入權限設定：</strong> 在該資源群組的左側選單中，點選 <strong>「存取控制 (IAM)」 (Access control (IAM))</strong>。</li>



<li><strong>新增角色指派：</strong> 點選上方的 <strong>「+ 新增」 (Add)</strong> -> 選擇 <strong>「新增角色指派」 (Add role assignment)</strong>。</li>



<li><strong>選擇角色 (Role)：</strong> 根據您想給對方的權限大小，選擇一個角色（常用的三個基礎角色）：<ul><li><strong>擁有者 (Owner)：</strong> 權限最大，可管理資源並能把權限再分給別人。</li><li><strong>參與者 (Contributor)：</strong> 可以建立、修改、刪除 RG 內的所有資源（例如建立虛擬機、資料庫），但<strong>不能</strong>更改權限。<em>(最常指派給外部開發者的角色)</em></li><li><strong>讀取者 (Reader)：</strong> 只能看，不能做任何變更。</li></ul>選好後點選 <strong>「下一步」</strong>。</li>



<li><strong>選取成員 (Members)：</strong> * 在「將存取權指派給」維持選擇 <strong>「使用者、群組或服務主體」</strong>。
<ul class="wp-block-list">
<li>點選 <strong>「+ 選取成員」 (Select members)</strong>。</li>



<li>在右側搜尋欄輸入 <strong>杜晟甫</strong> 或他的 Email，點擊他讓他出現在下方清單中，然後點選 <strong>「選取」</strong>。</li>
</ul>
</li>



<li><strong>審查並提交：</strong> 點選 <strong>「檢閱 + 指派」 (Review + assign)</strong>，再次確認後點擊按鈕送出。</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">這樣就完成了嗎？外部使用者該如何登入？</h3>



<p>當您完成上述指派後，外部使用者（杜甫）需要透過特定的方式登入才能看到您分給他的資源：</p>



<ul class="wp-block-list">
<li><strong>接受邀請：</strong> 他的 Email 會收到一封來自 Microsoft 的邀請函（如果之前沒收過），他必須點擊信中的 <strong>「Accept invitation」</strong> 接受加入您的租戶 (Tenant)。</li>



<li><strong>專屬登入連結：</strong> 外部來賓登入 Azure 時，有時直接進 <code>portal.azure.com</code> 會切換不到您的公司目錄。最保險的做法是提供他帶有您<strong>租戶網域名稱或租戶 ID</strong> 的專屬連結：<code>https://portal.azure.com/您的公司網域名稱.onmicrosoft.com</code><em>(例如：<code>https://portal.azure.com/mycompany.onmicrosoft.com</code>)</em></li>
</ul>



<p>當他透過該連結登入後，就能在您的 Azure 環境中，看到並操作您指定給他的那個資源群組（RG）了！</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" width="1024" height="915" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-19-15-53-83-1024x915.jpg?v=1779177471" alt="" class="wp-image-8446" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-19-15-53-83-1024x915.jpg?v=1779177471 1024w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-19-15-53-83-600x536.jpg?v=1779177471 600w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-19-15-53-83-768x686.jpg?v=1779177471 768w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-19-15-53-83-1536x1372.jpg?v=1779177471 1536w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-19-15-53-83.jpg?v=1779177471 1749w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>沒錯，您的直覺非常準確！這確實是因為您的<strong>帳號權限不夠</strong>，所以清單中才沒有出現最核心的「擁有者 (Owner)」或「參與者 (Contributor)」選項。</p>



<p>您目前在搜尋列輸入了「擁有者」，系統篩選出來的都是一些特定服務的次級角色（例如：儲存體 Blob 資料擁有者、圖表擁有者等），而不是整個資源群組的基礎管理角色。</p>



<p>要解決這個問題，您有以下兩種解決方案：</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">方案一：聯絡公司的 Azure 系統管理員（最快）</h3>



<p>您需要請你們公司內部權限最高的人（通常是 IT 部門、雲端架構師或當初建立這個 Azure 訂閱的人），幫您進行以下調整：</p>



<ol start="1" class="wp-block-list">
<li><strong>請管理員幫忙指派：</strong> 直接把這張截圖發給管理員，跟他說：<em>「我想把外部人員加入 <code>rg-pr-stg-jpe-001</code> 這個資源群組，但我沒有這個 RG 的 Owner 權限，可以請你幫我把【杜晟甫】加進去，並給他【參與者 (Contributor)】角色嗎？」</em></li>



<li><strong>請管理員提升您的權限：</strong> 或者，請管理員將您在這個資源群組（或整個訂閱）的角色提升為 <strong>「使用者存取系統管理員 (User Access Administrator)」</strong> 或 <strong>「擁有者 (Owner)」</strong>，這樣您以後就能自己指派權限給別人了。</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">方案二：選擇目前選單中「退而求其次」的角色（視對方的需求而定）</h3>



<p>如果您現在急著先讓「杜晟甫」能進來看資源，且管理員暫時聯絡不上，您可以先清除搜尋欄的「擁有者」，改搜尋並指派 <strong>「讀取者 (Reader)」</strong>。</p>



<ul class="wp-block-list">
<li><strong>讀取者 (Reader)</strong> 角色通常不需要太高的管理權限就能指派。</li>



<li><strong>缺點：</strong> 杜晟甫登入後<strong>只能看、不能動</strong>（無法建立任何東西）。但至少能先確認他進得來這個資源群組，等您權限拿到後，再幫他升級成參與者即可。</li>
</ul>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/05/azure-%e9%82%80%e8%ab%8b%e5%a4%96%e9%83%a8%e4%bd%bf%e7%94%a8%e8%80%85%e5%8a%a0%e5%85%a5%e5%9c%98%e9%9a%8a/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Azure Container Apps 多網域部署指南</title>
		<link>https://stackoverflow.max-everyday.com/2026/05/azure-container-apps-multi-domain/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/05/azure-container-apps-multi-domain/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Tue, 19 May 2026 03:27:21 +0000</pubDate>
				<category><![CDATA[Azure 筆記]]></category>
		<category><![CDATA[azure]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8443</guid>

					<description><![CDATA[如何在同一個 Azure Container A...]]></description>
										<content:encoded><![CDATA[
<p>如何在同一個 Azure Container Apps Environment（CAE）中，<br>透過 Application Gateway（AGW）將<strong>不同網域</strong>路由到<strong>不同的 Container App</strong>，<br>達到單一基礎設施服務多個應用程式的效果。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">架構圖</h2>



<pre class="wp-block-code"><code>                        ┌─────────────────────────────────────────────┐
外部使用者                │  Virtual Network (vnet-ag-example-001)        │
  │                     │  ┌──────────────────────────────────────┐   │
  │ HTTPS               │  │  Application Gateway (agw-example)   │   │
  ▼                     │  │                                      │   │
&#91;app-a.example.com]─────┼──►  Listener: app-a.example.com        │   │
&#91;app-b.example.com]─────┼──►  Listener: app-b.example.com        │   │
                        │  └───────────┬──────────────┬───────────┘   │
                        └─────────────┼──────────────┼───────────────┘
                                      │ VNet Peering │
                        ┌─────────────┼──────────────┼───────────────┐
                        │  Virtual Network (vnet-example-001)         │
                        │             │              │                │
                        │  ┌──────────▼──────────────▼───────────┐   │
                        │  │  Container Apps Environment (CAE)   │   │
                        │  │  (Internal, Consumption)            │   │
                        │  │                                      │   │
                        │  │  ca-app-a  (port 80,  nginx)        │   │
                        │  │  ca-app-b  (port 8080, api)         │   │
                        │  └─────────────────────────────────────┘   │
                        │                                             │
                        └─────────────────────────────────────────────┘</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">前置條件</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>資源</th><th>說明</th></tr></thead><tbody><tr><td>Container Apps Environment</td><td>建立為 <strong>Internal</strong>（Workload Profile: Consumption）</td></tr><tr><td>VNet for CAE</td><td>例如 <code>vnet-example-001</code>，CAE 使用其中一個子網路</td></tr><tr><td>VNet for AGW</td><td>例如 <code>vnet-ag-example-001</code>，AGW 使用其中一個子網路</td></tr><tr><td>VNet Peering</td><td>上述兩個 VNet 互相 Peered</td></tr><tr><td>Private DNS Zone</td><td>由 CAE 自動建立，需手動連結至 AGW 所在的 VNet</td></tr><tr><td>SSL 憑證</td><td>每個自訂網域需有對應的憑證（PFX 格式上傳至 AGW）</td></tr></tbody></table></figure>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">一、Private DNS Zone 設定</h2>



<p>CAE 建立後，Azure 會自動在同一資源群組建立一個 Private DNS Zone，<br>格式為：<code>&lt;隨機字串&gt;.&lt;region&gt;.azurecontainerapps.io</code><br>例如：<code>abcd1234efgh.japaneast.azurecontainerapps.io</code></p>



<p><strong>此 DNS Zone 需與 AGW 所在的 VNet 連結，AGW 才能解析 CA 的 FQDN。</strong></p>



<pre class="wp-block-code"><code>az network private-dns link vnet create \
  --resource-group &lt;cae-resource-group&gt; \
  --zone-name "abcd1234efgh.japaneast.azurecontainerapps.io" \
  --name "link-to-agw-vnet" \
  --virtual-network &lt;agw-vnet-id&gt; \
  --registration-enabled false</code></pre>



<h3 class="wp-block-heading">Wildcard DNS 記錄</h3>



<p>Private DNS Zone 中有一筆 Wildcard A 記錄：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>名稱</th><th>類型</th><th>值</th></tr></thead><tbody><tr><td><code>*</code></td><td>A</td><td><code>&lt;CAE 內部 Load Balancer IP&gt;</code></td></tr></tbody></table></figure>



<p>此 Wildcard 只能匹配<strong>單層</strong>子網域（如 <code>ca-app-a.abcd1234efgh...</code>），<br><strong>無法</strong>匹配 <code>ca-app-a.internal.abcd1234efgh...</code>（雙層）。</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>注意</strong>：若使用 <code>--ingress internal</code> 建立 CA，其 FQDN 會包含 <code>.internal.</code>，<br>不會被 Wildcard 記錄匹配，AGW 將無法解析。<br>請一律使用 <code>--ingress external</code>（在 Internal CAE 內，external 仍為 VNet 私有，並非真正對外）。</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">二、建立 Container App</h2>



<p>每個應用程式建立時指定 <code>--ingress external</code>，對應的 FQDN 格式為：<br><code>&lt;app-name&gt;.&lt;env-domain&gt;</code></p>



<pre class="wp-block-code"><code># 建立 App A（前端，nginx on port 80）
az containerapp create \
  --name ca-app-a \
  --resource-group &lt;resource-group&gt; \
  --environment &lt;cae-name&gt; \
  --image &lt;registry&gt;.azurecr.io/app-a:latest \
  --target-port 80 \
  --ingress external \
  --registry-server &lt;registry&gt;.azurecr.io

# 建立 App B（後端 API，on port 8080）
az containerapp create \
  --name ca-app-b \
  --resource-group &lt;resource-group&gt; \
  --environment &lt;cae-name&gt; \
  --image &lt;registry&gt;.azurecr.io/app-b:latest \
  --target-port 8080 \
  --ingress external \
  --registry-server &lt;registry&gt;.azurecr.io</code></pre>



<p>建立後，各 CA 的 FQDN 例如：</p>



<ul class="wp-block-list">
<li><code>ca-app-a.abcd1234efgh.japaneast.azurecontainerapps.io</code></li>



<li><code>ca-app-b.abcd1234efgh.japaneast.azurecontainerapps.io</code></li>
</ul>



<h3 class="wp-block-heading">允許 HTTP（不強制重新導向 HTTPS）</h3>



<p>ACA 預設會將 HTTP 請求 301 重新導向至 HTTPS。<br>若 AGW 以 HTTP 協定連至 CA（通常如此，因為 TLS 已在 AGW 終止），<br>需對每個 CA 啟用 <code>--allow-insecure</code>：</p>



<pre class="wp-block-code"><code>az containerapp ingress update \
  --name ca-app-a \
  --resource-group &lt;resource-group&gt; \
  --allow-insecure</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">三、Application Gateway 設定</h2>



<p>同一個 AGW 可以透過<strong>多組 Listener + Rule + Backend Pool</strong> 路由不同網域。</p>



<h3 class="wp-block-heading">3-1 Backend Pool</h3>



<p>每個應用程式建立一個 Backend Pool，目標為 CA 的 FQDN：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Pool 名稱</th><th>FQDN</th></tr></thead><tbody><tr><td><code>pool-app-a</code></td><td><code>ca-app-a.abcd1234efgh.japaneast.azurecontainerapps.io</code></td></tr><tr><td><code>pool-app-b</code></td><td><code>ca-app-b.abcd1234efgh.japaneast.azurecontainerapps.io</code></td></tr></tbody></table></figure>



<pre class="wp-block-code"><code>az network application-gateway address-pool create \
  --gateway-name &lt;agw-name&gt; \
  --resource-group &lt;agw-resource-group&gt; \
  --name pool-app-a \
  --servers ca-app-a.abcd1234efgh.japaneast.azurecontainerapps.io</code></pre>



<h3 class="wp-block-heading">3-2 Backend HTTP Settings</h3>



<p>指定協定為 HTTP（因為 TLS 在 AGW 終止）、Port 80，<br>並<strong>固定 Host Header 為 CA 的 FQDN</strong>，使 ACA Ingress 能正確路由：</p>



<pre class="wp-block-code"><code>az network application-gateway http-settings create \
  --gateway-name &lt;agw-name&gt; \
  --resource-group &lt;agw-resource-group&gt; \
  --name set-app-a \
  --protocol Http \
  --port 80 \
  --host-name "ca-app-a.abcd1234efgh.japaneast.azurecontainerapps.io"</code></pre>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><code>--host-name</code> 設定會讓 AGW 傳送正確的 <code>Host</code> Header 給 ACA，<br>若不設定，ACA Ingress 收到的 Host 可能是原始網域（如 <code>app-a.example.com</code>），<br>導致路由失敗（HTTP 404）。</p>
</blockquote>



<h3 class="wp-block-heading">3-3 HTTPS Listener</h3>



<p>每個自訂網域需要一個 Listener：</p>



<pre class="wp-block-code"><code># 先上傳 SSL 憑證
az network application-gateway ssl-cert create \
  --gateway-name &lt;agw-name&gt; \
  --resource-group &lt;agw-resource-group&gt; \
  --name cert-app-a \
  --cert-file app-a.pfx \
  --cert-password &lt;password&gt;

# 建立 Listener
az network application-gateway http-listener create \
  --gateway-name &lt;agw-name&gt; \
  --resource-group &lt;agw-resource-group&gt; \
  --name lsn-app-a-https \
  --frontend-port &lt;https-port-name&gt; \
  --frontend-ip &lt;frontend-ip-config-name&gt; \
  --host-name "app-a.example.com" \
  --ssl-cert cert-app-a</code></pre>



<h3 class="wp-block-heading">3-4 Routing Rule</h3>



<p>將 Listener 連接至 Backend Pool + HTTP Settings：</p>



<pre class="wp-block-code"><code>az network application-gateway rule create \
  --gateway-name &lt;agw-name&gt; \
  --resource-group &lt;agw-resource-group&gt; \
  --name rule-app-a \
  --http-listener lsn-app-a-https \
  --address-pool pool-app-a \
  --http-settings set-app-a \
  --rule-type Basic \
  --priority 200</code></pre>



<p>重複 3-1 至 3-4 步驟，為 <code>app-b.example.com</code> 建立對應的資源。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">四、新增應用程式到現有環境</h2>



<p>只需重複以下步驟，<strong>無需修改 DNS Zone 或 CAE 設定</strong>：</p>



<ol class="wp-block-list">
<li>建立 Container App（<code>--ingress external</code>）</li>



<li>啟用 <code>--allow-insecure</code></li>



<li>AGW 新增 Backend Pool（目標為新 CA 的 FQDN）</li>



<li>AGW 新增 HTTP Settings（Host Header 設為新 CA 的 FQDN）</li>



<li>AGW 新增 Listener（綁定新網域的 SSL 憑證）</li>



<li>AGW 新增 Routing Rule（連接上述三者）</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">五、CA 間互相呼叫</h2>



<p>在同一個 Internal CAE 中，Container App 可透過 FQDN 直接呼叫彼此，<br>不需經過 AGW：</p>



<pre class="wp-block-code"><code>http:&#47;&#47;ca-app-b.abcd1234efgh.japaneast.azurecontainerapps.io/api/endpoint</code></pre>



<p>例如前端 nginx 的 <code>proxy_pass</code> 設定：</p>



<pre class="wp-block-code"><code>location /api/ {
    proxy_pass http://ca-app-b.abcd1234efgh.japaneast.azurecontainerapps.io/api/;
}</code></pre>



<p>建議將後端 FQDN 透過環境變數注入（例如 <code>BACKEND_URL</code>），<br>搭配 nginx 的 <code>envsubst</code> 機制於容器啟動時替換：</p>



<pre class="wp-block-code"><code># nginx.conf.template
location /api/ {
    proxy_pass ${BACKEND_URL}/api/;
}</code></pre>



<pre class="wp-block-code"><code># Dockerfile：放在 /etc/nginx/templates/ 會在啟動時自動替換
COPY .deploy/nginx.conf.template /etc/nginx/templates/default.conf.template</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">六、常見問題</h2>



<h3 class="wp-block-heading">AGW Backend Health 顯示 Unknown 或 502</h3>



<ol class="wp-block-list">
<li><strong>DNS 無法解析 CA FQDN</strong>：確認 Private DNS Zone 已連結至 AGW 所在的 VNet。</li>



<li><strong>使用了 <code>.internal.</code> FQDN</strong>：CA 建立時使用了 <code>--ingress internal</code>，改為 <code>--ingress external</code>。</li>



<li><strong>HTTP 重新導向</strong>：CA 收到 HTTP 請求回傳 301，對該 CA 執行 <code>az containerapp ingress update --allow-insecure</code>。</li>



<li><strong>Host Header 不符</strong>：AGW 的 HTTP Settings 未設定 <code>--host-name</code>，導致 ACA Ingress 無法識別目標應用程式。</li>
</ol>



<h3 class="wp-block-heading">CA 啟動失敗：<code>open .: no such file or directory</code></h3>



<p>應用程式嘗試讀取相對路徑的檔案（如資料庫 Migration 檔），<br>但工作目錄與預期不符。<br>解法：透過環境變數指定絕對路徑，例如：</p>



<pre class="wp-block-code"><code>az containerapp update \
  --name ca-app-b \
  --resource-group &lt;resource-group&gt; \
  --set-env-vars "MIGRATION_SOURCE=file:///app/migrations"</code></pre>



<h3 class="wp-block-heading">CA 中的環境變數包含 <code>&amp;</code> 字元</h3>



<p>在 Windows 環境下使用 <code>az</code> CLI（<code>.cmd</code> 包裝器）時，<code>&amp;</code> 會被 cmd.exe 解析為命令分隔符，<br>導致參數被截斷。解法：改用 <code>az rest --method PATCH</code> 搭配 JSON 檔案傳遞：</p>



<pre class="wp-block-code"><code>{
  "properties": {
    "template": {
      "containers": &#91;{
        "name": "ca-app-b",
        "env": &#91;
          { "name": "DATABASE_URL", "value": "sqlserver://host?db=mydb&amp;param=value" }
        ]
      }]
    }
  }
}</code></pre>



<pre class="wp-block-code"><code>az rest --method PATCH \
  --url "https://management.azure.com/subscriptions/&lt;sub&gt;/resourceGroups/&lt;rg&gt;/providers/Microsoft.App/containerApps/&lt;name&gt;?api-version=2024-03-01" \
  --body @patch.json</code></pre>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/05/azure-container-apps-multi-domain/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>解決 Azure App Service 跨區域連線與 VNet 整合挑戰</title>
		<link>https://stackoverflow.max-everyday.com/2026/05/azure-app-service-crose-location/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/05/azure-app-service-crose-location/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Tue, 19 May 2026 01:52:17 +0000</pubDate>
				<category><![CDATA[Azure 筆記]]></category>
		<category><![CDATA[azure]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8440</guid>

					<description><![CDATA[在雲端架構中，我們常會遇到服務分散在不同區域的情...]]></description>
										<content:encoded><![CDATA[
<p>在雲端架構中，我們常會遇到服務分散在不同區域的情況。例如，你的 App Service 部署在 <strong>Taiwan North</strong> ，但主要的測試環境與虛擬網路（ VNet ）卻位在 <strong>Japan East</strong> 。當你發現 App Service 無法連線至合作廠商 <strong>宏達科技（ GrandTech ）</strong> 的內部網路時，通常不只是程式碼的問題，而是底層網路架構需要調整。</p>



<h3 class="wp-block-heading">問題現象：連線超時與網路阻斷</h3>



<p>當 App Service 發出請求卻一直收到 <strong>TLS timeout</strong> 或 <strong>http_status=0</strong> 時，這代表請求根本沒有成功建立連線。這通常有兩個主要原因：</p>



<ol start="1" class="wp-block-list">
<li><strong>IP 白名單限制</strong>：宏達科技的伺服器可能設有防火牆，只允許特定的 IP 區段進入，而 Azure App Service 的預設公用輸出 IP 並不在允許清單內。</li>



<li><strong>網路區域限制</strong>：位在 Japan East 的測試用虛擬機器（ VM ）可以成功連通，是因為該區域的網路已經與宏達科技建立了信任通道（ 或者是固定的白名單 IP ），但 Taiwan North 的流量則是走公用網路出去，因此被對方防火牆攔截。</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">關鍵解決策略：虛擬網路整合</h3>



<p>要讓 App Service 像虛擬機器一樣擁有受控的網路路徑，必須執行 <strong>VNet Integration</strong> （ 虛擬網路整合 ）。但當 App Service 與目標 VNet 跨區域時，我們會遇到一些硬性限制。</p>



<h3 class="wp-block-heading">實戰步驟一：檢查現有網路狀態</h3>



<p>首先，我們需要確認 Taiwan North 當地的網路環境。你可以使用 Azure CLI 檢查該區域的 VNet 清單：</p>



<p>az network vnet list &#8211;query &#8220;[?location==&#8217;northtaiwan&#8217;].{name:name,rg:resourceGroup}&#8221; -o table</p>



<p>在我們的案例中，發現 Taiwan North 只有一個名為 <strong>vnet-proxy-agent</strong> 的虛擬網路。</p>



<h3 class="wp-block-heading">實戰步驟二：處理子網域委派問題</h3>



<p>若要讓 App Service 加入 VNet ，該子網域（ Subnet ）必須委派給 <strong>Microsoft.Web/serverFarms</strong> 。</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>注意：如果該子網域已經有其他虛擬機器在使用，你無法直接更新委派設定。</p>
</blockquote>



<p>這時正確的做法是在 <strong>vnet-proxy-agent</strong> 中，為 App Service 建立一個專屬且乾淨的子網域。</p>



<h3 class="wp-block-heading">實戰步驟三：跨區域網路串接（ Peering ）</h3>



<p>由於宏達科技只認可 Japan East 網路出口的 IP ，我們必須確認 Taiwan North 的 VNet 是否已經與 Japan East 的 Hub VNet 建立 <strong>Peering</strong> （ 虛擬網路對等互連 ）。</p>



<p>當 Peering 建立後， Taiwan North 的流量就能透過 Azure 骨幹網路導向 Japan East ，再經由那邊的白名單管道存取宏達科技的伺服器。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">最終檢查清單</h3>



<ol start="1" class="wp-block-list">
<li><strong>新增專屬子網域</strong>：在 Taiwan North 的 VNet 中切分出一塊新的 IP 區段。</li>



<li><strong>設定委派</strong>：將該子網域委派給 Web App 使用。</li>



<li><strong>啟用路由設定</strong>：在 App Service 的環境變數中，務必設定 <strong>WEBSITE_VNET_ROUTE_ALL=1</strong> ，以確保所有對外流量都強制經過 VNet ，而不是走預設的 Azure 公用出口。</li>



<li><strong>重新部署</strong>：更改環境變數與網路整合設定後，務必重新啟動或部署應用程式，確保新的網路路徑生效。</li>
</ol>



<h3 class="wp-block-heading">結語</h3>



<p>解決雲端連線問題時， <strong>視覺化你的網路路徑</strong> 非常重要。當公用網路不通時，善用 Azure 的區域性 VNet 與跨區域對等互連技術，能有效解決 IP 阻擋與連線超時的困擾。如果你也遇到類似的跨區域連線問題，不妨檢查一下你的 VNet 委派與路由路徑！</p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/05/azure-app-service-crose-location/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>後端容器啟動失敗：四個連環 Bug 的診斷與修復</title>
		<link>https://stackoverflow.max-everyday.com/2026/05/backend-container-bug-login-failed-for-user-msi/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/05/backend-container-bug-login-failed-for-user-msi/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Mon, 18 May 2026 08:27:04 +0000</pubDate>
				<category><![CDATA[Azure 筆記]]></category>
		<category><![CDATA[azure]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8433</guid>

					<description><![CDATA[分類： Bug Fix、Go、Azure App...]]></description>
										<content:encoded><![CDATA[
<p><strong>分類：</strong> Bug Fix、Go、Azure App Service、golang-migrate<br><strong>對象：</strong> 任何將 Go 後端部署至 Azure Web App for Containers 的開發者</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">背景</h2>



<p>將 Go 後端容器成功推送至 Container Registry 並部署至 Azure Web App 後，雖然映像已可正常拉取，容器卻在啟動階段反覆 crash。從 Log stream 可看到四個連環錯誤，每修一個就出現下一個。本文逐一記錄症狀、根本原因與修復方式。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Bug 1 — 資料庫連線失敗：<code>Login failed for user ''</code></h2>



<h3 class="wp-block-heading">症狀</h3>



<pre class="wp-block-code"><code>{"level":"fatal","msg":"Failed to run database migration",
 "error":"failed to open database: mssql: login error: Login failed for user ''."}</code></pre>



<p>容器一啟動就立即退出，完全沒有執行任何 migration。</p>



<h3 class="wp-block-heading">根本原因</h3>



<p><code>DATABASE_URL</code> 中設定了 <code>fedauth=ActiveDirectoryMSI</code>，預期使用 Azure AD Managed Identity 認證，但 <code>main.go</code> 中匯入的是<strong>基礎 driver</strong>：</p>



<pre class="wp-block-code"><code>import _ "github.com/microsoft/go-mssqldb"</code></pre>



<p>這個 driver 只支援 SQL 帳號密碼認證，<strong>完全忽略</strong> <code>fedauth=</code> 參數，導致它以空白使用者名稱嘗試登入，SQL Server 因而拒絕連線。</p>



<p>Azure AD 認證有兩種正確做法：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>做法</th><th>說明</th></tr></thead><tbody><tr><td>改用 <code>go-mssqldb/azuread</code></td><td>匯入 <code>_ "github.com/microsoft/go-mssqldb/azuread"</code> 並使用 <code>"azuresql"</code> driver</td></tr><tr><td>改用 <code>golang-migrate</code> 的 <code>useMsi=true</code></td><td>讓 migrate 函式庫自行透過 ADAL 取得 token</td></tr></tbody></table></figure>



<p>本次採用第二種做法，同時也修改 <code>initDatabasePool</code> 以 ADAL token provider 建立 <code>database/sql</code> 連線：</p>



<h3 class="wp-block-heading">修復方式</h3>



<p><strong>1. 修改連線字串（環境變數）</strong></p>



<pre class="wp-block-code"><code># 錯誤（會被 base driver 靜默忽略）
DATABASE_URL=sqlserver://db.example.com?database=mydb&amp;fedauth=ActiveDirectoryMSI&amp;...

# 正確
DATABASE_URL=sqlserver://db.example.com?database=mydb&amp;useMsi=true&amp;encrypt=true&amp;TrustServerCertificate=false</code></pre>



<p><strong>2. 修改 Go 程式碼，用 ADAL 取得 MSI token</strong></p>



<pre class="wp-block-code"><code>import (
    "database/sql"
    "net/url"
    mssql "github.com/microsoft/go-mssqldb"
    adal  "github.com/Azure/go-autorest/autorest/adal"
)

func getMSITokenProvider(resource string) func() (string, error) {
    return func() (string, error) {
        spt, err := adal.NewServicePrincipalTokenFromManagedIdentity(resource, nil)
        if err != nil {
            return "", err
        }
        if err = spt.Refresh(); err != nil {
            return "", err
        }
        return spt.Token().AccessToken, nil
    }
}

func initDatabasePool(databaseURL string) (*sqlx.DB, error) {
    u, err := url.Parse(databaseURL)
    if err != nil {
        return nil, err
    }

    useMsi := u.Query().Get("useMsi") == "true"
    if useMsi {
        tokenProvider := getMSITokenProvider("https://database.windows.net/")
        connector, err := mssql.NewAccessTokenConnector(databaseURL, tokenProvider)
        if err != nil {
            return nil, err
        }
        return sqlx.NewDb(sql.OpenDB(connector), "sqlserver"), nil
    }

    return sqlx.Connect("sqlserver", databaseURL)
}</code></pre>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><strong>重點：</strong> Azure SQL 的 MSI resource URL 為 <code>https://database.windows.net/</code>，結尾斜線不可省略。</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Bug 2 — Migration 反覆失敗：<code>Invalid column name 'xxx'</code></h2>



<h3 class="wp-block-heading">症狀</h3>



<pre class="wp-block-code"><code>{"level":"fatal","msg":"Failed to run database migration",
 "error":"migration failed: Invalid column name 'old_column_name'. in line 15: MERGE INTO some_table ..."}</code></pre>



<p>Migration 從第 1 版開始跑，跑到中途某個 seed SQL 時因欄位不存在而失敗，<code>schema_migrations</code> 被標記為 <code>dirty=true</code>，下次啟動仍繼續失敗。</p>



<h3 class="wp-block-heading">根本原因</h3>



<p>部署流程分兩個階段：</p>



<ol class="wp-block-list">
<li>在 Azure SQL Query Editor 執行 <code>init.sql</code>（一次性初始化腳本）</li>



<li>後端啟動時由 <code>golang-migrate</code> 執行 migration</li>
</ol>



<p><code>init.sql</code> 為了方便，直接包含了所有 migration 的最終狀態（含後期的欄位更名，例如 <code>old_column_name</code> → <code>new_column_name</code>）。執行完 <code>init.sql</code> 後，資料庫已是<strong>最新 schema</strong>，但 <code>schema_migrations</code> 追蹤表<strong>完全是空的</strong>。</p>



<p>後端啟動時，<code>golang-migrate</code> 看不到任何已完成的 migration，從第 1 版開始執行。早期的 migration（如 seed 測試資料）引用了<strong>舊欄位名稱</strong>，而 <code>init.sql</code> 已把那個欄位改名，導致 SQL 執行失敗。</p>



<pre class="wp-block-code"><code>init.sql 執行後：  schema 已是最終狀態，但 schema_migrations = 空
golang-migrate：   以為從未 migrate 過，從頭執行 → 欄位衝突 → dirty</code></pre>



<h3 class="wp-block-heading">修復方式</h3>



<p><strong>短期（已壞掉的環境）：</strong> 直接以 SQL 強制將版本設為最新</p>



<pre class="wp-block-code"><code>-- 將版本號改為你的最新 migration 編號
DELETE FROM schema_migrations;
INSERT INTO schema_migrations (version, dirty) VALUES (最新版本號, 0);</code></pre>



<p><strong>長期（防止再次發生）：</strong> 在 <code>init.sql</code> 結尾加入 <code>schema_migrations</code> 初始化</p>



<pre class="wp-block-code"><code>-- ============================================================
--  schema_migrations 版本追蹤（與 golang-migrate 同步）
--  init.sql 已套用所有 migration，在此設定版本號，
--  避免後端啟動時重跑 migration 造成欄位衝突。
-- ============================================================
IF OBJECT_ID(N'dbo.schema_migrations', N'U') IS NULL
BEGIN
    CREATE TABLE schema_migrations (
        version BIGINT NOT NULL PRIMARY KEY,
        dirty   BIT    NOT NULL
    );
END

DELETE FROM schema_migrations;
INSERT INTO schema_migrations (version, dirty) VALUES (最新版本號, 0);</code></pre>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><strong>版本號維護：</strong> 每新增一個 migration 檔案時，記得同步更新 <code>init.sql</code> 裡的版本號。</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Bug 3 — 環境變數設定錯誤：<code>invalid mode "xxx"</code></h2>



<h3 class="wp-block-heading">症狀</h3>



<pre class="wp-block-code"><code>{"level":"fatal","msg":"Failed to create External API client",
 "error":"some-client: invalid mode \"prod\", must be 'mock' or 'http'"}</code></pre>



<h3 class="wp-block-heading">根本原因</h3>



<p>部署腳本的預設值沿用了自訂的命名慣例（<code>"prod"</code>），但程式碼實際接受的值只有：</p>



<ul class="wp-block-list">
<li><code>"http"</code> — 呼叫真實的外部 API</li>



<li><code>"mock"</code> — 使用 mock 資料（本地開發用）</li>
</ul>



<p>部署腳本撰寫時沒有對照程式碼確認合法值，造成一個完全不必要的啟動錯誤。</p>



<h3 class="wp-block-heading">修復方式</h3>



<pre class="wp-block-code"><code># deploy.ps1 — 修改前
$SOME_CLIENT_MODE = "prod"   # &#x274c; 程式碼不接受這個值

# deploy.ps1 — 修改後
$SOME_CLIENT_MODE = "http"   # &#x2705; 正確：正式環境使用真實 API</code></pre>



<p>若已部署，可直接更新 App Settings，不需重新部署映像：</p>



<pre class="wp-block-code"><code>az webapp config appsettings set \
  --name my-backend-app \
  --resource-group my-resource-group \
  --settings SOME_CLIENT_MODE=http</code></pre>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><strong>小提醒：</strong> <code>az webapp config appsettings set</code> 只更新指定的 key，不會覆蓋其他設定，比 <code>az rest PUT /config/appsettings</code> 安全。</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Bug 4 — 冷啟動時資料庫連線瞬斷：<code>Read: EOF</code></h2>



<h3 class="wp-block-heading">症狀</h3>



<pre class="wp-block-code"><code>{"level":"fatal","msg":"Failed to run database migration",
 "error":"failed to open database: Read: EOF"}</code></pre>



<p>容器啟動後約 5–7 秒閃退，exit code 1。每次重啟都重現，沒有任何 retry 訊息。Log 顯示 migration 剛要開始就立刻失敗。</p>



<h3 class="wp-block-heading">根本原因</h3>



<p>Azure Web App for Containers 採用<strong>邊車（sidecar）架構</strong>：主容器與 MSI sidecar（負責核發 Managed Identity token）幾乎同時啟動。MSI sidecar 需要幾秒鐘才能就緒。</p>



<p>在這個短暫的空窗期（約 1–7 秒），<code>golang-migrate</code> 試圖開啟 TCP 連線，經過 TLS handshake 時遭遇 TCP RST 或連線中斷，因而回傳 <code>Read: EOF</code>。</p>



<p><code>golang-migrate</code> 的 <code>MigrationUp</code> 不具備內建的 retry 機制——第一次連線失敗就直接回傳錯誤，由呼叫端負責處理。Go 程式在沒有 retry 保護的情況下直接呼叫 <code>logger.Fatal</code>，容器立即退出。</p>



<pre class="wp-block-code"><code>容器啟動
  ├─ MSI sidecar：正在初始化（尚未就緒）
  └─ Go 程式：立刻呼叫 MigrationUp
       └─ golang-migrate 開啟 TCP 連線 → Read: EOF
            └─ main.go 呼叫 logger.Fatal → 容器退出（exit 1）</code></pre>



<h3 class="wp-block-heading">修復方式</h3>



<p>在呼叫 <code>MigrationUp</code> 前，加一層帶有指數退避（exponential backoff）的 retry 包裝：</p>



<pre class="wp-block-code"><code>// retryDB 以指數退避重試資料庫操作，最多重試 maxAttempts 次。
// 等待時間：2s → 4s → 8s → 16s（每次左移一位）
func retryDB(logger *zap.Logger, maxAttempts int, fn func() error) error {
    var err error
    for attempt := 0; attempt &lt; maxAttempts; attempt++ {
        err = fn()
        if err == nil {
            return nil
        }
        if attempt &lt; maxAttempts-1 {
            wait := time.Duration(1&lt;&lt;(attempt+1)) * time.Second
            logger.Warn("Database operation failed, retrying",
                zap.Int("attempt", attempt+1),
                zap.Duration("wait", wait),
                zap.Error(err),
            )
            time.Sleep(wait)
        }
    }
    return err
}

// 使用範例
err = retryDB(logger, 5, func() error {
    return databaseutil.MigrationUp(cfg.MigrationSource, cfg.DatabaseURL, logger)
})
if err != nil {
    logger.Fatal("Failed to run database migration", zap.Error(err))
}</code></pre>



<p>這樣即使第一次連線遇到 EOF，程式會等待 2 秒後重試，最多嘗試 5 次（共可等待 30 秒），足以涵蓋 MSI sidecar 的啟動時間。</p>



<h3 class="wp-block-heading">驗證</h3>



<p>修復後重新建置映像並強制 Azure 拉取新版：</p>



<pre class="wp-block-code"><code># 強制 Azure 拉取新映像（az webapp restart 只重啟，不重拉映像）
az webapp config container set \
  --name my-backend-app \
  --resource-group my-resource-group \
  --container-image-name "my-registry.azurecr.io/my-backend:latest"</code></pre>



<p>若 retry 有發生，Log 會出現以下 warn 訊息，並最終成功啟動：</p>



<pre class="wp-block-code"><code>{"level":"warn","msg":"Database operation failed, retrying","attempt":1,"wait":"2s","error":"failed to open database: Read: EOF"}
{"level":"info","msg":"Current migration version","version":9,"dirty":false}
{"level":"info","msg":"Database schema is up to date, no migration required"}</code></pre>



<p>若 MSI sidecar 已就緒，第一次連線即可成功，不會有 warn 訊息，容器直接順利啟動。</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><strong>注意：</strong> <code>az webapp restart</code> 會使用本地快取的映像，不會拉取最新版本。若需強制更新，請使用 <code>az webapp config container set</code> 重新設定映像名稱。</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">偵錯流程回顧</h2>



<p>四個 bug 必須依序排除——前一個 bug 掩蓋了後一個：</p>



<pre class="wp-block-code"><code>啟動失敗
  └─ Bug 1: Login failed for user ''
       └─ 修復後 → 繼續啟動
            └─ Bug 2: Invalid column name 'xxx' (migration dirty)
                 └─ 修復後 → 繼續啟動
                      └─ Bug 3: invalid mode "prod"
                           └─ 修復後 → 繼續啟動
                                └─ Bug 4: Read: EOF (MSI sidecar not ready)
                                     └─ 修復後 → 啟動成功 &#x2705;</code></pre>



<p>每次改一個問題就要重新部署或重啟容器，並等待 30–60 秒的 cold start 時間，再從 Log stream 確認下一個錯誤。</p>



<p>取得最新 container log 的方式（Azure CLI）：</p>



<pre class="wp-block-code"><code># 快速方式（Kudu VFS API，不需解壓縮）
TOKEN=$(az account get-access-token --query accessToken -o tsv)
curl -H "Authorization: Bearer $TOKEN" \
     -H "Accept: text/plain" \
  "https://my-backend-app.scm.azurewebsites.net/api/vfs/LogFiles/YYYY_MM_DD_&lt;instanceId&gt;_default_docker.log" \
  | tail -30

# 完整下載方式
az webapp log download \
  --name my-backend-app \
  --resource-group my-resource-group \
  --log-file backend-logs.zip</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">最終確認</h2>



<p>後端正常啟動後，Log 應呈現以下模式：</p>



<pre class="wp-block-code"><code>{"msg":"Database schema is up to date, no migration required"}
{"msg":"External API HTTP client initialized","base_url":"https://api.example.com"}
{"msg":"Application initialization","host":"0.0.0.0","port":"8080"}
{"msg":"Starting listening request","host":"0.0.0.0","port":"8080"}</code></pre>



<p>以 HTTP 呼叫受保護的 API 端點，應回傳 <code>401 Unauthorized</code>（JSON 格式），而非 Azure 的 HTML 錯誤頁面：</p>



<pre class="wp-block-code"><code>curl https://my-backend-app.azurewebsites.net/api/some-protected-endpoint
# 預期回應：{"title":"Unauthorized","status":401,...}</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">重點整理</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Bug</th><th>症狀關鍵字</th><th>根本原因</th><th>修復</th></tr></thead><tbody><tr><td>1</td><td><code>Login failed for user ''</code></td><td>base go-mssqldb driver 忽略 <code>fedauth=</code></td><td>改用 <code>useMsi=true</code> + ADAL token provider</td></tr><tr><td>2</td><td><code>Invalid column name</code> + migration dirty</td><td><code>init.sql</code> 與 <code>golang-migrate</code> 狀態不同步</td><td><code>init.sql</code> 末尾補 <code>schema_migrations</code> 初始化</td></tr><tr><td>3</td><td><code>invalid mode "xxx"</code></td><td>部署腳本使用無效的環境變數值</td><td>對照程式碼確認合法值，改為 <code>"http"</code></td></tr><tr><td>4</td><td><code>Read: EOF</code> (migration)</td><td>MSI sidecar 啟動期間 TCP 連線瞬斷，無 retry</td><td>加 <code>retryDB</code> 指數退避包裝（最多 5 次，最長 30s）</td></tr></tbody></table></figure>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/05/backend-container-bug-login-failed-for-user-msi/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Azure Web App 容器映像從 Docker Hub 拉取而非 ACR：診斷與修復指南</title>
		<link>https://stackoverflow.max-everyday.com/2026/05/azure-web-app-docker-linuxfxversion/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/05/azure-web-app-docker-linuxfxversion/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Mon, 18 May 2026 06:35:12 +0000</pubDate>
				<category><![CDATA[Azure 筆記]]></category>
		<category><![CDATA[azure]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8431</guid>

					<description><![CDATA[分類： Azure、DevOps、Docker標...]]></description>
										<content:encoded><![CDATA[
<p><strong>分類：</strong> Azure、DevOps、Docker<br><strong>標籤：</strong> Azure App Service、Azure Container Registry、Docker、CI/CD</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">問題描述</h2>



<p>將 Docker 容器部署至 Azure Web App 時，Log stream 出現以下錯誤：</p>



<pre class="wp-block-code"><code>Pulling image: my-frontend:latest
DockerApiException: Docker API responded with status code=InternalServerError,
  response={"message":"Head \"https://registry-1.docker.io/v2/library/my-frontend/manifests/latest\":
  unauthorized: incorrect username or password"}
Pulling docker image my-frontend:latest failed.
DockerApiException: Docker API responded with status code=NotFound,
  response={"message":"pull access denied for my-frontend, repository does not exist
  or may require 'docker login': denied: requested access to the resource is denied"}
Image pull failed. Defaulting to local copy if present.
Stopping site my-frontend-app because it failed during startup.</code></pre>



<p>明明映像已成功推送至 Azure Container Registry（ACR），Azure 卻跑去 Docker Hub 拉取，導致 Web App 無法啟動。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">根本原因</h2>



<p>Azure Web App for Containers 透過 <code>linuxFxVersion</code> 這個設定決定要拉取哪個映像。可用以下指令查看：</p>



<pre class="wp-block-code"><code>az webapp show \
  --name my-frontend-app \
  --resource-group my-resource-group \
  --query "siteConfig.linuxFxVersion" -o tsv</code></pre>



<p>問題環境的輸出：</p>



<pre class="wp-block-code"><code>DOCKER|my-frontend:latest</code></pre>



<p>這個值<strong>缺少 Registry 前綴</strong>，Azure 因此回退至預設的 Docker Hub（<code>registry-1.docker.io</code>）。</p>



<p>正確值應為：</p>



<pre class="wp-block-code"><code>DOCKER|myacr.azurecr.io/my-frontend:latest</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">為何會產生這個問題？</h2>



<p>使用 <code>az webapp create</code> 建立容器 Web App 時，若 <code>--container-image-name</code> 只傳入 <code>IMAGE:TAG</code>（不含 Registry 網址），Azure 會將 <code>linuxFxVersion</code> 設為不含 Registry 前綴的值：</p>



<pre class="wp-block-code"><code># &#x274c; 這樣建立後，linuxFxVersion 只有 DOCKER|my-frontend:latest
az webapp create \
  --name my-frontend-app \
  --resource-group my-resource-group \
  --plan my-app-plan \
  --container-image-name "my-frontend:latest" \          # 沒有 registry 前綴
  --container-registry-url "https://myacr.azurecr.io" \
  --container-registry-user myacr \
  --container-registry-password "&lt;password&gt;"</code></pre>



<p>此時，<code>--container-registry-url</code> 只用來設定驗證憑證，<strong>不會</strong>自動補入映像名稱的 Registry 前綴。</p>



<p>反之，若在 <code>--container-image-name</code> 傳入完整 URL，搭配 <code>--container-registry-url</code> 時又會導致另一個問題：Registry 網址被<strong>重複拼接</strong>：</p>



<pre class="wp-block-code"><code>DOCKER|myacr.azurecr.io/myacr.azurecr.io/my-frontend:latest  # &#x274c; 雙重前綴</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">診斷步驟</h2>



<h3 class="wp-block-heading">1. 確認 <code>linuxFxVersion</code></h3>



<pre class="wp-block-code"><code>az webapp show \
  --name my-frontend-app \
  --resource-group my-resource-group \
  --query "{linuxFxVersion:siteConfig.linuxFxVersion, state:state}" \
  -o table</code></pre>



<h3 class="wp-block-heading">2. 確認容器設定</h3>



<pre class="wp-block-code"><code>az webapp config container show \
  --name my-frontend-app \
  --resource-group my-resource-group \
  --query "&#91;?name=='DOCKER_CUSTOM_IMAGE_NAME']" \
  -o table</code></pre>



<h3 class="wp-block-heading">3. 確認 App Settings 中的 Docker 憑證</h3>



<pre class="wp-block-code"><code>az webapp config appsettings list \
  --name my-frontend-app \
  --resource-group my-resource-group \
  --query "&#91;?name=='DOCKER_REGISTRY_SERVER_URL' || name=='DOCKER_REGISTRY_SERVER_USERNAME' || name=='DOCKER_REGISTRY_SERVER_PASSWORD']" \
  -o table</code></pre>



<p>若 <code>DOCKER_REGISTRY_SERVER_PASSWORD</code> 顯示為 <code>null</code>，即是憑證遺失的警訊。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">修復方法</h2>



<h3 class="wp-block-heading">方法一：立即修復（手動執行）</h3>



<p><strong>Step 1：用完整 ACR URL 更新容器設定</strong></p>



<pre class="wp-block-code"><code>ACR_PASSWORD=$(az acr credential show --name myacr --query "passwords&#91;0].value" -o tsv)

az webapp config container set \
  --name my-frontend-app \
  --resource-group my-resource-group \
  --container-image-name "myacr.azurecr.io/my-frontend:latest" \
  --container-registry-url "https://myacr.azurecr.io" \
  --container-registry-user myacr \
  --container-registry-password "$ACR_PASSWORD"</code></pre>



<p>執行後確認 <code>linuxFxVersion</code> 已變為 <code>DOCKER|myacr.azurecr.io/my-frontend:latest</code>。</p>



<p><strong>Step 2：確保 App Settings 中的密碼不為 null</strong></p>



<p><code>az webapp config container set</code> 在某些情況下 CLI 顯示密碼為 <code>null</code>（實際上可能已儲存，但保險起見建議明確寫入）。使用 <code>az rest</code> 繞過 Windows 的 <code>cmd.exe</code> <code>&amp;</code> 字元解析問題：</p>



<pre class="wp-block-code"><code># PowerShell
$sub = "&lt;your-subscription-id&gt;"
$rg  = "my-resource-group"
$app = "my-frontend-app"
$ACR_PASSWORD = (az acr credential show --name myacr --query "passwords&#91;0].value" -o tsv)

# 取出現有 App Settings 再合併
$existing = az webapp config appsettings list --name $app --resource-group $rg | ConvertFrom-Json
$props = @{}
foreach ($s in $existing) {
    if ($s.value -ne $null) { $props&#91;$s.name] = $s.value }
}

# 覆寫 Docker 相關設定
$props&#91;"DOCKER_REGISTRY_SERVER_URL"]      = "https://myacr.azurecr.io"
$props&#91;"DOCKER_REGISTRY_SERVER_USERNAME"] = "myacr"
$props&#91;"DOCKER_REGISTRY_SERVER_PASSWORD"] = $ACR_PASSWORD
$props&#91;"DOCKER_CUSTOM_IMAGE_NAME"]        = "myacr.azurecr.io/my-frontend:latest"

$tmpFile = "$env:TEMP\appsettings_fix.json"
@{ properties = $props } | ConvertTo-Json -Depth 3 | Set-Content -Path $tmpFile -Encoding UTF8

az rest --method PUT `
  --url "https://management.azure.com/subscriptions/$sub/resourceGroups/$rg/providers/Microsoft.Web/sites/$app/config/appsettings?api-version=2022-03-01" `
  --body "@$tmpFile"

Remove-Item $tmpFile</code></pre>



<p><strong>Step 3：重啟 Web App</strong></p>



<pre class="wp-block-code"><code>az webapp restart --name my-frontend-app --resource-group my-resource-group</code></pre>



<p>等待 30–60 秒（Docker 容器冷啟動需要時間），再測試 HTTP 回應：</p>



<pre class="wp-block-code"><code>curl -o /dev/null -s -w "%{http_code}" https://my-frontend-app.azurewebsites.net
# 預期：200</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">方法二：從根本修正 CI/CD 部署腳本</h3>



<p>在 <code>az webapp create</code> 之後，<strong>立即執行</strong> <code>az webapp config container set</code> 以確保 <code>linuxFxVersion</code> 包含完整 Registry 路徑：</p>



<pre class="wp-block-code"><code># PowerShell deploy script 範例

$ACR          = "myacr"
$ACR_SERVER   = "myacr.azurecr.io"
$ACR_PASSWORD = (az acr credential show --name $ACR --query "passwords&#91;0].value" -o tsv)
$IMAGE        = "my-frontend"
$TAG          = "latest"
$APP          = "my-frontend-app"
$RG           = "my-resource-group"
$PLAN         = "my-app-plan"

# Step 1: 建立 Web App（用短名稱避免雙重前綴問題）
az webapp create `
  --name $APP `
  --resource-group $RG `
  --plan $PLAN `
  --container-image-name "${IMAGE}:${TAG}" `
  --container-registry-url "https://$ACR_SERVER" `
  --container-registry-user $ACR `
  --container-registry-password $ACR_PASSWORD

# Step 2: 立即修正 linuxFxVersion，補入完整 Registry 前綴
az webapp config container set `
  --name $APP `
  --resource-group $RG `
  --container-image-name "$ACR_SERVER/${IMAGE}:${TAG}" `
  --container-registry-url "https://$ACR_SERVER" `
  --container-registry-user $ACR `
  --container-registry-password $ACR_PASSWORD | Out-Null

# Step 3: 透過 az rest 確保 App Settings 中密碼明確寫入
$props = @{
    DOCKER_REGISTRY_SERVER_URL      = "https://$ACR_SERVER"
    DOCKER_REGISTRY_SERVER_USERNAME = $ACR
    DOCKER_REGISTRY_SERVER_PASSWORD = $ACR_PASSWORD
    DOCKER_CUSTOM_IMAGE_NAME        = "$ACR_SERVER/${IMAGE}:${TAG}"
    WEBSITES_PORT                   = "80"
}
$tmpFile = "$env:TEMP\appsettings.json"
@{ properties = $props } | ConvertTo-Json -Depth 3 | Set-Content $tmpFile -Encoding UTF8
az rest --method PUT `
  --url "https://management.azure.com/subscriptions/&lt;sub-id&gt;/resourceGroups/$RG/providers/Microsoft.Web/sites/$APP/config/appsettings?api-version=2022-03-01" `
  --body "@$tmpFile"
Remove-Item $tmpFile</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">重點整理</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>設定項目</th><th>說明</th><th>正確值範例</th></tr></thead><tbody><tr><td><code>linuxFxVersion</code></td><td>實際拉取的映像（最高優先）</td><td><code>DOCKER|myacr.azurecr.io/my-app:latest</code></td></tr><tr><td><code>DOCKER_CUSTOM_IMAGE_NAME</code></td><td>App Settings 中的映像設定</td><td><code>myacr.azurecr.io/my-app:latest</code></td></tr><tr><td><code>DOCKER_REGISTRY_SERVER_URL</code></td><td>Registry 驗證端點</td><td><code>https://myacr.azurecr.io</code></td></tr><tr><td><code>DOCKER_REGISTRY_SERVER_PASSWORD</code></td><td>ACR 密碼（可能顯示為 null）</td><td>需透過 <code>az rest</code> 明確寫入</td></tr></tbody></table></figure>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><strong>關鍵原則：</strong><br><code>linuxFxVersion</code>（即容器設定 <code>DOCKER_CUSTOM_IMAGE_NAME</code>）是 Azure Web App 決定拉取哪個映像的<strong>最終依據</strong>，<br>其優先級高於 App Settings 中的 <code>DOCKER_CUSTOM_IMAGE_NAME</code>。<br>因此，務必使用 <code>az webapp config container set</code> 確保此值包含完整的 Registry URL。</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">常見陷阱</h2>



<ol class="wp-block-list">
<li><strong><code>az webapp create</code> + 短映像名稱</strong> → <code>linuxFxVersion</code> 無 Registry 前綴 → 拉 Docker Hub</li>



<li><strong><code>az webapp create</code> + 完整 URL + <code>--container-registry-url</code></strong> → <code>linuxFxVersion</code> Registry 前綴重複</li>



<li><strong><code>az webapp config container set</code> 後 CLI 顯示密碼為 null</strong> → 實際可能空值；建議以 <code>az rest PUT /config/appsettings</code> 補強</li>



<li><strong>Windows 上 <code>DATABASE_URL</code> 含 <code>&amp;</code></strong> → <code>az</code> 呼叫走 <code>cmd.exe</code>，<code>&amp;</code> 被解析為命令分隔符 → 改用 <code>az rest</code> 搭配 JSON 檔案</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">參考資料</h2>



<ul class="wp-block-list">
<li><a href="https://learn.microsoft.com/azure/app-service/configure-custom-container">Azure App Service — Configure a custom container</a></li>



<li><a href="https://learn.microsoft.com/cli/azure/webapp/config/container">az webapp config container set — Azure CLI</a></li>



<li><a href="https://learn.microsoft.com/cli/azure/reference-index#az-rest">az rest — Azure CLI</a></li>



<li><a href="https://learn.microsoft.com/azure/app-service/troubleshoot-docker-deployment-failures">Troubleshoot Docker container pull failures in Azure App Service</a></li>
</ul>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/05/azure-web-app-docker-linuxfxversion/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>如何變更 Azure SQL Server 的 Microsoft Entra 系統管理員</title>
		<link>https://stackoverflow.max-everyday.com/2026/05/change-azure-sql-server-entra-admin/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/05/change-azure-sql-server-entra-admin/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Fri, 15 May 2026 01:32:58 +0000</pubDate>
				<category><![CDATA[Azure 筆記]]></category>
		<category><![CDATA[azure]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8417</guid>

					<description><![CDATA[適用情境：需要將 Azure SQL Serve...]]></description>
										<content:encoded><![CDATA[
<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><strong>適用情境</strong>：需要將 Azure SQL Server 的 Microsoft Entra 系統管理員（前稱 Azure AD 管理員）移交給其他人員時。</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">前置條件</h2>



<ul class="wp-block-list">
<li>已安裝 <a href="https://learn.microsoft.com/zh-tw/cli/azure/install-azure-cli">Azure CLI</a></li>



<li>具備目標訂用帳戶的 <strong>Owner</strong> 或 <strong>Contributor</strong> 權限</li>



<li>已執行 <code>az login</code> 並切換至正確的訂用帳戶</li>
</ul>



<pre class="wp-block-code"><code>az login
az account set --subscription "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">環境資訊</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>項目</th><th>值</th></tr></thead><tbody><tr><td>資源群組</td><td><code>rg-example-prod-001</code></td></tr><tr><td>位置</td><td><code>japaneast</code></td></tr><tr><td>訂用帳戶</td><td><code>MyCompany-PoC</code></td></tr><tr><td>伺服器名稱</td><td><code>demo-sqlsvr.database.windows.net</code></td></tr></tbody></table></figure>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">步驟一：查詢新管理員的 Object ID</h2>



<p>在設定 Entra 管理員之前，必須先取得目標使用者在 Entra ID 中的 Object ID。</p>



<pre class="wp-block-code"><code>az ad user show --id newadmin@example.onmicrosoft.com --query id -o tsv</code></pre>



<p><strong>範例輸出：</strong></p>



<p>aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee：設定新的 Entra 系統管理員</p>



<p>使用 <code>az sql server ad-admin create</code> 指令覆蓋現有的 Entra 管理員設定。</p>



<pre class="wp-block-preformatted">az sql server ad-admin create `<br>  --resource-group rg-example-prod-001 `<br>  --server demo-sqlsvr `<br>  --display-name newadmin@example.onmicrosoft.com `<br>  --object-id aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee</pre>



<pre class="wp-block-code"><code><strong>成功後回傳範例：</strong></code></pre>



<p>json</p>



<pre class="wp-block-preformatted">{
  "administratorType": "ActiveDirectory",
  "azureAdOnlyAuthentication": true,
  "login": "newadmin@example.onmicrosoft.com",
  "sid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
  "tenantId": "ffffffff-gggg-hhhh-iiii-jjjjjjjjjjjj"
}
</pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">使用批次腳本（推薦）</h2>



<p>為方便日後移交，可使用以下 PowerShell 腳本快速切換管理員：</p>



<p><strong><code>set-sql-entra-admin.ps1</code></strong></p>



<pre class="wp-block-code"><code>param(
    &#91;Parameter(Mandatory=$true)]
    &#91;string]$UserEmail
)

$ResourceGroup = "rg-example-prod-001"
$ServerName    = "demo-sqlsvr"

Write-Host "Looking up Object ID for: $UserEmail ..."
$ObjectId = az ad user show --id $UserEmail --query id -o tsv

if ($LASTEXITCODE -ne 0) {
    Write-Error "找不到使用者：$UserEmail"
    exit 1
}

Write-Host "設定 Entra 管理員為：$UserEmail (OID: $ObjectId)"
az sql server ad-admin create `
    --resource-group $ResourceGroup `
    --server $ServerName `
    --display-name $UserEmail `
    --object-id $ObjectId

if ($LASTEXITCODE -eq 0) {
    Write-Host "完成！Entra 管理員現為：$UserEmail"
} else {
    Write-Error "設定失敗，請確認帳號與權限。"
    exit 1
}</code></pre>



<p><strong>執行方式：</strong></p>



<pre class="wp-block-code"><code># 指派給舊管理員
.\set-sql-entra-admin.ps1 -UserEmail oldadmin@example.onmicrosoft.com

# 指派給新管理員
.\set-sql-entra-admin.ps1 -UserEmail newadmin@example.onmicrosoft.com</code></pre>



<p></p>



<p><strong>若遇到執行原則限制，請先執行：</strong></p>



<p>Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">驗證結果</h2>



<p>執行以下指令確認管理員已成功更新：</p>



<pre class="wp-block-code"><code>az sql server ad-admin list `
  --resource-group rg-example-prod-001 `
  --server demo-sqlsvr `
  -o table</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">常見問題</h2>



<h3 class="wp-block-heading">Q：執行後提示權限不足？</h3>



<p>確認執行帳號在該訂用帳戶具備 <code>Contributor</code> 以上角色，且在 Entra ID 中具備讀取使用者資訊的權限。</p>



<h3 class="wp-block-heading">Q：找不到使用者（User not found）？</h3>



<p>請確認該帳號確實存在於同一個 Entra 租用戶（Tenant）中，可用以下指令確認：</p>



<pre class="wp-block-code"><code>az ad user list --filter "mail eq 'oldadmin@example.onmicrosoft.com'" -o table</code></pre>



<h3 class="wp-block-heading">Q：變更後原管理員立即失去存取權嗎？</h3>



<p>是的，Entra 管理員每個伺服器只能設定一位，覆蓋後舊管理員將立即失去 Entra 管理員身份。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">參考資料</h2>



<ul class="wp-block-list">
<li><a href="https://learn.microsoft.com/zh-tw/azure/azure-sql/database/authentication-aad-configure">Microsoft 文件：設定 Azure SQL 的 Azure AD 管理員</a></li>



<li><a href="https://learn.microsoft.com/zh-tw/cli/azure/sql/server/ad-admin">az sql server ad-admin 指令參考</a></li>
</ul>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/05/change-azure-sql-server-entra-admin/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Azure MSSQL 無密碼 Python 連線</title>
		<link>https://stackoverflow.max-everyday.com/2026/05/azure-mssql-no-password-connect/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/05/azure-mssql-no-password-connect/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Thu, 14 May 2026 12:04:32 +0000</pubDate>
				<category><![CDATA[Azure 筆記]]></category>
		<category><![CDATA[azure]]></category>
		<category><![CDATA[mssql]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8412</guid>

					<description><![CDATA[在 Azure 上使用 系統指派的受控識別 (S...]]></description>
										<content:encoded><![CDATA[
<p>在 Azure 上使用 <strong>系統指派的受控識別 (System-Assigned Managed Identity)</strong> 是最推薦的做法，因為它完全不需要在程式碼裡寫死密碼，安全性最高，且生命週期與你的資源（如 VM 或 App Service）同步。</p>



<p>要實現這個功能，你需要完成三個步驟：<strong>啟用身份識別</strong>、<strong>資料庫授權</strong>、以及<strong>修改 Python 連線字串</strong>。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">第一步：在 Azure 資源上啟用受控識別</h3>



<p>在你運行 Python 程式碼的資源（例如 Azure VM, App Service 或 Function）的頁面：</p>



<ol start="1" class="wp-block-list">
<li>點選左側選單的 <strong>「身分識別」(Identity)</strong>。</li>



<li>在 <strong>「系統指派」</strong> 頁籤下，將狀態改為 <strong>「開啟」</strong> 並儲存。</li>



<li>系統會產生一個 <strong>物件識別碼 (Object ID)</strong>，請記住這個資源的名稱。</li>
</ol>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="852" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-14-19-41-7p-1024x852.jpg?v=1778758921" alt="" class="wp-image-8414" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-14-19-41-7p-1024x852.jpg?v=1778758921 1024w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-14-19-41-7p-600x499.jpg?v=1778758921 600w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-14-19-41-7p-768x639.jpg?v=1778758921 768w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-14-19-41-7p.jpg?v=1778758921 1477w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>上面的截圖來看，你目前是在 <strong>「Azure SQL Database」</strong> 的介面裡。</p>



<p><strong>受控識別（Managed Identity）不是在資料庫（Database）上開啟的，而是在「跑 Python 程式碼的那個東西」上面開啟的。</strong></p>



<p>請想像：</p>



<ul class="wp-block-list">
<li><strong>SQL Database</strong> 是「銀行」。</li>



<li><strong>你的 Python 程式碼</strong>（跑在 VM、App Service 或 Function 上）是「進去辦事的人」。</li>
</ul>



<p>你現在需要在「辦事的人」身上發一張身分證（啟用受控識別），然後再去「銀行」告訴櫃檯（資料庫）說：「看到拿這張身分證的人就讓他進來」。</p>



<h3 class="wp-block-heading">接下來你該怎麼做：</h3>



<h4 class="wp-block-heading">1. 找到「發身分證」的地方</h4>



<p>請在 Azure Portal 最上方的搜尋框，搜尋你<strong>跑 Python 程式碼的資源名稱</strong>。</p>



<ul class="wp-block-list">
<li>如果你是在 <strong>Azure VM</strong> 跑：去該 VM 的頁面，找左側選單的「身分識別」。</li>



<li>如果你是在 <strong>App Service</strong> 跑：去該 App Service 的頁面，找左側選單的「身分識別」。</li>



<li>如果你是在 <strong>Azure Function</strong> 跑：去該 Function 的頁面，找左側選單的「身分識別」。</li>
</ul>



<p>進入後，你就會看到我上一個回覆說的「系統指派」狀態，將它改為 <strong>「開啟」</strong>。</p>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="351" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-14-20-03-7t-1024x351.jpg?v=1778760219" alt="" class="wp-image-8415" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-14-20-03-7t-1024x351.jpg?v=1778760219 1024w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-14-20-03-7t-600x205.jpg?v=1778760219 600w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-14-20-03-7t-768x263.jpg?v=1778760219 768w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-14-20-03-7t-1536x526.jpg?v=1778760219 1536w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-14-20-03-7t-2048x701.jpg?v=1778760219 2048w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<h4 class="wp-block-heading">2. 在資料庫（也就是你截圖的這個地方）授權</h4>



<p>當你在上面的資源開啟受控識別後，回到你截圖的這個 SQL Database 介面：</p>



<ol start="1" class="wp-block-list">
<li>點擊左側選單的 <strong>「查詢編輯器 (預覽)」(Query editor)</strong>。</li>



<li>用你的管理員帳號登入。</li>



<li>執行以下指令（假設你的 VM 或 App Service 名字叫 <code>my-python-app</code>）：</li>
</ol>



<p>SQL</p>



<pre class="wp-block-code"><code>CREATE USER &#91;my-python-app] FROM EXTERNAL PROVIDER;
ALTER ROLE db_datareader ADD MEMBER &#91;my-python-app];
ALTER ROLE db_datawriter ADD MEMBER &#91;my-python-app];
</code></pre>



<h4 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 補充：截圖中的發現</h4>



<p>從你的截圖中可以看到，在 <strong>「安全性」</strong> 區塊下，有一行：</p>



<ul class="wp-block-list">
<li><strong>系統指派的身分識別：已啟用</strong></li>
</ul>



<p>這指的是 <strong>「SQL Server 自己」</strong> 的身分，通常是用來讓 SQL 去讀取 Key Vault 等其他資源用的。<strong>這不是給你的 Python 程式連線用的。</strong> <strong>總結：</strong> 請去尋找你運行 Python 的那個 Azure 資源（VM / App Service），在那裡開啟身分識別。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>了解更多: 利用客戶自控金鑰支援透明資料加密的受控識別<br><a href="https://learn.microsoft.com/zh-tw/azure/azure-sql/database/transparent-data-encryption-byok-identity?view=azuresql&amp;WT.mc_id=Portal-SqlAzureExtension">https://learn.microsoft.com/zh-tw/azure/azure-sql/database/transparent-data-encryption-byok-identity?view=azuresql&amp;WT.mc_id=Portal-SqlAzureExtension</a></p>



<p>▋ 數據安全的大門與那把遺失的鑰匙</p>



<p>在數位世界裡，我們使用「透明資料加密」技術，這就像是在資料庫門口裝了一道自動感應門，資料進出時會自動上鎖或解鎖。傳統做法是讓雲端平台幫你管理這把鑰匙，這對大多數人來說很方便。然而，對於需要極高掌控權的管理者來說，這就像是把身家財產的進出權限完全交給外人，一旦發生資安爭議，你可能無法第一時間主動切換鎖頭。</p>



<p>▋ 拿回屬於你的主權鑰匙</p>



<p>為了解決這個信任缺口，現在有一種「客戶管理金鑰」的機制。簡單來說，這就是讓你自備鎖頭與鑰匙。當你的資料庫要啟動加密功能時，系統不再使用預設的鑰匙，而是去你的私密保險庫中讀取你指定的「身份證明」。這確保了只有經過你授權的特定身份，才能啟動那把開啟資料的鑰匙。</p>



<p>▋ 建立你的專屬數位防線</p>



<p>如果你希望全面掌控數據的安全命運，可以嘗試以下步驟：</p>



<ul class="wp-block-list">
<li>在雲端環境中建立一個專屬的「身份識別」，這就像是為你的資料庫申請一張專屬的身分證。</li>



<li>設定嚴格的存取權限，確保只有這個「身份」能接觸到你的加密鑰匙。</li>



<li>定期檢查這把鑰匙的使用紀錄，確保沒有任何未經授權的嘗試。</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">第二步：在 SQL Database 中授權</h3>



<p>SQL Server 預設不認識這個受控識別，你必須進到資料庫裡幫它「開門」：</p>



<ol start="1" class="wp-block-list">
<li>使用 <strong>Active Directory 管理員</strong> 帳號登入 SQL Database（可用 Azure Portal 的 Query Editor）。</li>



<li>執行以下 SQL 指令（將 <code>&lt;identity-name&gt;</code> 替換為你的 Azure 資源名稱）：</li>
</ol>



<p>SQL</p>



<pre class="wp-block-code"><code>-- 建立資料庫使用者
CREATE USER &#91;&lt;identity-name&gt;] FROM EXTERNAL PROVIDER;

-- 授予讀取/寫入權限 (視需求調整角色)
ALTER ROLE db_datareader ADD MEMBER &#91;&lt;identity-name&gt;];
ALTER ROLE db_datawriter ADD MEMBER &#91;&lt;identity-name&gt;];
</code></pre>



<p></p>



<p>如果你是在 <strong>Azure App Service</strong> 或 <strong>Azure VM</strong> 上啟用的受控識別，該「顯示名稱」通常就是你的 <strong>資源名稱</strong>。</p>



<ul class="wp-block-list">
<li><strong>錯誤做法</strong>：<code>CREATE USER [1234-1234-1234-1234-1234] ...</code> (不可使用 ID)</li>



<li><strong>正確做法</strong>：<code>CREATE USER [你的AppService名稱] FROM EXTERNAL PROVIDER;</code></li>
</ul>



<p></p>



<p>如果你不確定名稱，或者名稱有特殊字元，你可以直接使用 <code>SID</code> (這對應到你看到的物件識別碼) 來建立，這樣最準確：</p>



<p></p>



<pre class="wp-block-code"><code>-- 請將 '1234-1234-1234-1234-1234' 替換為你在身分識別頁面看到的 Object ID
CREATE USER &#91;MyManagedIdentityName] 
WITH OBJECT_ID = '1234-1234-1234-1234-1234';

-- 接著再授權
ALTER ROLE db_datareader ADD MEMBER &#91;MyManagedIdentityName];
</code></pre>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="707" height="285" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-15-12-54-7w.jpg?v=1778820896" alt="" class="wp-image-8425" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-15-12-54-7w.jpg?v=1778820896 707w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-15-12-54-7w-600x242.jpg?v=1778820896 600w" sizes="auto, (max-width: 707px) 100vw, 707px" /></figure>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">第三步：修改 Python 連線字串</h3>



<p>使用受控識別時，連線字串不需要 <code>UID</code> 和 <code>PWD</code>，而是改用 <code>Authentication=ActiveDirectoryMsi</code>。</p>



<p>Python</p>



<pre class="wp-block-code"><code>import pyodbc

# 設定基礎參數
server = 'your-server.database.windows.net'
database = 'your-database'
driver = '{ODBC Driver 18 for SQL Server}'

# 系統指派受控識別的連線字串
connection_string = (
    f"Driver={driver};"
    f"Server={server};"
    f"Database={database};"
    "Authentication=ActiveDirectoryMsi;"  # 關鍵設定
    "Encrypt=yes;"
    "TrustServerCertificate=no;"
    "Connection Timeout=30;"
)

def connect_and_query():
    try:
        print(f"Connecting to {server} using Managed Identity...")
        conn = pyodbc.connect(connection_string)
        print("連線成功！")
        return conn
    except Exception as e:
        print(f"連線失敗: {e}")

# 執行連線
conn = connect_and_query()
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 關鍵筆記：</h3>



<ul class="wp-block-list">
<li><strong>環境限制</strong>：<code>Authentication=ActiveDirectoryMsi</code> 只能在 <strong>Azure 環境內</strong> 運行（例如在 Azure VM 上跑）。如果你在自己的電腦（在地端）跑這段程式碼會報錯，因為地端環境沒有 Managed Identity 憑證。</li>



<li><strong>驅動程式</strong>：請確保你的環境中安裝了 <strong>ODBC Driver 17 或 18</strong>，舊版驅動程式可能不支援 <code>ActiveDirectoryMsi</code> 參數。</li>



<li><strong>優點</strong>：即使你更換了密碼，或者這台機器被刪除了，都不需要去改程式碼，這就是「無密碼連線」的強大之處。</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>執行以下 SQL 指令時遇到 Error:</p>



<p>A connection was successfully established with the server, but then an error occurred during the pre-login handshake. (provider: TCP Provider, error: 0 &#8211; An existing connection was forcibly closed by the remote host.)<br><br>Failed to execute the query</p>



<p>這個錯誤「An existing connection was forcibly closed by the remote host」發生在 <strong>Pre-login handshake (預先登錄握手)</strong> 階段，通常不是程式碼邏輯問題，而是 <strong>網路層</strong> 或 <strong>防火牆</strong> 的攔截。</p>



<p>這代表你的 Python 程式已經碰到了 SQL Server 的大門，但 SQL Server 因為某些安全規則直接把連線「踢掉」了。</p>



<p>請檢查以下三個最常見的原因：</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">1. Azure SQL 防火牆未允許你的 IP</h3>



<p>這是最常見的原因。即便你使用受控識別，網路還是要通。</p>



<ul class="wp-block-list">
<li><strong>如果你在 Azure 資源內執行</strong>：請到 SQL Server 的「網路 (Networking)」設定，確保 <strong>「允許 Azure 服務和資源存取此伺服器 (Allow Azure services and resources to access this server)」</strong> 有被勾選。</li>



<li><strong>如果你在公司內網執行</strong>：請確認你的公用 IP 已加入 SQL Server 的防火牆白名單。</li>
</ul>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="378" height="1024" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-15-12-53-7v-378x1024.jpg?v=1778820822" alt="" class="wp-image-8423" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-15-12-53-7v-378x1024.jpg?v=1778820822 378w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-15-12-53-7v-221x600.jpg?v=1778820822 221w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-15-12-53-7v.jpg?v=1778820822 434w" sizes="auto, (max-width: 378px) 100vw, 378px" /></figure>
</div>


<h3 class="wp-block-heading">2. 強制使用 TLS 1.2</h3>



<p>Azure SQL 預設要求 TLS 1.2。如果你的 Python 環境或 ODBC 驅動程式版本太舊，握手時會因為加密協定不匹配而被中斷。</p>



<ul class="wp-block-list">
<li><strong>解決方法</strong>：確保你的連線字串中有 <code>Encrypt=yes;</code>（你已經有了）。</li>



<li><strong>檢查驅動</strong>：確認你安裝的是 <strong>ODBC Driver 18 for SQL Server</strong>。注意：Driver 18 預設會強制驗證證書，如果你沒有設定憑證，請試著在連線字串加上 <code>TrustServerCertificate=yes;</code> 測試看看。</li>
</ul>



<h3 class="wp-block-heading">3. ODBC Driver 18 的「預設加密」改變</h3>



<p>如果你是從 Driver 17 升級到 18，Driver 18 的預設行為變更了。請嘗試將連線字串稍微修改如下：</p>



<p>Python</p>



<pre class="wp-block-code"><code>connection_string = (
    f"DRIVER={driver};"
    f"SERVER={server};"
    f"DATABASE={database};"
    "Authentication=ActiveDirectoryMsi;"
    "Encrypt=yes;"
    "TrustServerCertificate=yes;"  # 先暫時改成 yes 測試是否為證書握手失敗
    "Connection Timeout=30;"
)
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">關鍵排除步驟（Debug Check）</h3>



<ol start="1" class="wp-block-list">
<li><strong>測試工具連線</strong>：你在 Azure Portal 上的 <strong>「查詢編輯器 (Query Editor)」</strong> 現在能正常進去嗎？
<ul class="wp-block-list">
<li>如果連 Query Editor 都進不去，那是資料庫本身的網路/防火牆設定問題。</li>
</ul>
</li>



<li><strong>確認受控識別是否已成功建立 User</strong>：
<ul class="wp-block-list">
<li>你剛才在嘗試 <code>CREATE USER</code> 時失敗了，這代表受控識別的身分還沒進到資料庫。</li>



<li><strong>重要</strong>：如果 <code>CREATE USER</code> 沒成功，<code>Authentication=ActiveDirectoryMsi</code> 就絕對連不上。</li>
</ul>
</li>
</ol>



<p><strong>目前的卡點建議：</strong></p>



<p>先解決 <code>CREATE USER</code> 的問題。請確認你是用 <strong>「Microsoft Entra 管理員」</strong> 的帳號登入 Query Editor。只有管理員有權限執行 <code>FROM EXTERNAL PROVIDER</code> 的指令。</p>



<p></p>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/05/azure-mssql-no-password-connect/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>停用 Zed GPU 警告設定</title>
		<link>https://stackoverflow.max-everyday.com/2026/05/%e5%81%9c%e7%94%a8-zed-gpu-%e8%ad%a6%e5%91%8a%e8%a8%ad%e5%ae%9a/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/05/%e5%81%9c%e7%94%a8-zed-gpu-%e8%ad%a6%e5%91%8a%e8%a8%ad%e5%ae%9a/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Thu, 14 May 2026 07:30:57 +0000</pubDate>
				<category><![CDATA[電腦相關應用]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8405</guid>

					<description><![CDATA[在 Windows 虛擬機器（VM）環境中使用 ...]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="566" height="459" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/image.png" alt="" class="wp-image-8406"/></figure>



<p>在 Windows 虛擬機器（VM）環境中使用 Zed 編輯器時，由於 Zed 依賴 GPU 進行介面渲染，而 VM 通常使用模擬顯卡，會導致編輯器跳出警告或效能不佳。</p>



<p>要關閉這個警告並強制編輯器執行，你可以透過設定 <strong>環境變數</strong> 來達成。</p>



<h3 class="wp-block-heading">解決方法：設定環境變數</h3>



<p>請在 Windows 系統中新增以下環境變數，這會告訴 Zed 忽略 GPU 限制並允許使用模擬驅動：</p>



<ol start="1" class="wp-block-list">
<li>按下鍵盤的 <strong>Win + R</strong>，輸入 <code>sysdm.cpl</code> 並按回車。</li>



<li>切換到 <strong>進階</strong> 頁籤，點擊下方的 <strong>環境變數</strong>。</li>



<li>在 <strong>使用者變數</strong> 區塊中，點擊 <strong>新增</strong>：
<ul class="wp-block-list">
<li>變數名稱：<code>ZED_ALLOW_EMULATED_GPU</code></li>



<li>變數值：<code>1</code></li>
</ul>
</li>



<li>點擊 <strong>確定</strong> 儲存。</li>



<li><strong>重新啟動 Zed</strong>（或重新登入 Windows）使設定生效。</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">進階優化建議</h3>



<p>如果在 VM 中運行依然感覺非常延遲，可以嘗試以下操作來提升流暢度：</p>



<ul class="wp-block-list">
<li><strong>啟用硬體加速：</strong> 如果你使用的是 VMware，請進入虛擬機設定，在 <strong>顯示器（Display）</strong> 項目勾選 <strong>加速 3D 圖形（Accelerate 3D graphics）</strong>。</li>



<li><strong>調整電源計畫：</strong> 在 VM 內的 Windows 設定中，將電源模式調整為 <strong>高效能</strong>。</li>



<li><strong>使用 CLI 啟動：</strong> 雖然環境變數已設定，但在某些情況下透過終端機啟動可以強制套用變數：PowerShell<code>$env:ZED_ALLOW_EMULATED_GPU=1 zed .</code></li>
</ul>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>[!TIP]</p>



<p>由於 Zed 使用 DirectX 11 進行渲染，若 VM 的虛擬顯卡完全不支援 DirectX 11，即便忽略警告，畫面仍可能出現閃爍或空白。在這種極端情況下，建議檢查 VM 軟體是否已更新到最新版本，並安裝對應的 Guest Additions 或 VMware Tools。其介面渲染直接使用 GPU 完成。在 VM 環境中，由於缺乏實體顯卡支援，系統會退而求其次使用 CPU 來模擬顯卡運算，這會消耗大量處理器資源並導致畫面撕裂。設定 <code>ZED_ALLOW_EMULATED_GPU=1</code> 的本質是告訴 Zed：「我知曉這會造成效能損耗，但請繼續執行」。</p>
</blockquote>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/05/%e5%81%9c%e7%94%a8-zed-gpu-%e8%ad%a6%e5%91%8a%e8%a8%ad%e5%ae%9a/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
