<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Gke on Tarragon</title><link>https://tarrragon.github.io/blog/tags/gke/</link><description>Recent content in Gke on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Sun, 25 Oct 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/gke/index.xml" rel="self" type="application/rss+xml"/><item><title>GCP 上 MIG 與 GKE 管理存取的實測紀錄：instance template 與 canary 滾動更新、重建與主機金鑰、kubectl debug 的 namespace、node pool 設定與控制平面端點的切換</title><link>https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/hands-on/mig-gke-access-walkthrough/</link><pubDate>Sun, 25 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/hands-on/mig-gke-access-walkthrough/</guid><description>&lt;p>實測日期是 2026-10-24，在 GCP 測試專案 &lt;code>blog-verify-20261024&lt;/code>（zone &lt;code>asia-east1-b&lt;/code>）上：MIG 用 Debian 12 的 e2-micro，GKE 是 zonal 的 Standard 叢集（版本 1.36.4-gke、Container-Optimized OS 節點）；控制平面的 IAM 檢查另外用第二個帳號的 access token 驗證。&lt;code>kubectl debug node&lt;/code> 與 Pod Security Admission 另外在本機的 kind（Kubernetes v1.37.0、kubectl v1.37.1）上實跑。&lt;/p>
&lt;p>這篇記錄把 &lt;a href="https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/hands-on/iap-ssh-migration-walkthrough/" data-link-title="GCP VM 的 SSH 與 RDP 改走 IAP 的實測紀錄：盤點、救援入口、IAP 規則與 IAM、停用舊規則驗證、OS Login 啟用與移除外部 IP" data-link-desc="要把 GCP 專案裡對 0.0.0.0/0 開放 22 與 3389 port 的預設規則換成只放行 IAP，想照一次實跑過的順序做、看每一步沒做或換一種做法的結果時讀；也用在停用舊規則之後連不上、OS Login 啟用後有人登不進去，或拿掉外部 IP 之後要確認出站走了 Cloud NAT 的時候">GCP VM 的 SSH 與 RDP 改走 IAP 的實測紀錄&lt;/a>（下文稱 IAP 實作文章）的三項設定（IAP 用的 network tag、OS Login、移除外部 IP）套到 MIG 與 GKE 上的實際操作。內容依操作的順序是：建一份帶這三項設定的新 instance template，先讓一台 VM 換成新版（canary）再讓整組更新；用 &lt;code>recreate-instances&lt;/code> 重建一台壞掉的 VM、比對重建之後換掉的主機金鑰；template 本身寫錯時退回上一版；在 GKE 上建一個專門給節點除錯用的 namespace，用 &lt;code>kubectl debug node&lt;/code> 查看節點；node pool 的 private nodes、network tag 與 OS Login；控制平面的 authorized networks，以及改連 DNS-based endpoint、關掉 IP 端點的兩步切換。每一步寫出這次的取值與實測結果，以及沒做或換一種做法時的結果；替代做法與沒做時的結果沒有另外標註的是這次實測看到的，照官方文件推出的寫出文件，推論的寫明沒有實跑。逐台設定為什麼在這兩種環境裡會消失、MIG 救援的取捨、GKE 的兩個存取面、&lt;code>kubectl debug node&lt;/code> 的權限邊界，以及控制平面的 authorized networks、DNS-based endpoint 與 private endpoint 三種存取方式的差別，寫在 &lt;a href="https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/gke-mig-management-access/" data-link-title="GCP 上由 MIG 與 GKE 管理的 VM 的管理存取：instance template、node pool、kubectl debug 與控制平面存取" data-link-desc="VM 由 managed instance group 或 GKE node pool 管理時，管理通道的設定為什麼要寫在 instance template 與 node pool 而不是逐台修改、MIG 救援用重建、退回 template 與 Serial Console 各自適用的情形、GKE 的控制平面與節點兩個存取面、kubectl debug 取代節點 SSH 的權限邊界（RBAC 與 Pod Security Admission），以及 GKE 控制平面三種端點（authorized networks、DNS-based endpoint 與 private endpoint）的差別">GCP 上由 MIG 與 GKE 管理的 VM 的管理存取&lt;/a>（下文稱概念篇），這裡不重寫。&lt;/p>
&lt;p>MIG 與 GKE 由 Terraform 這類 IaC 管理時，下面的 &lt;code>gcloud&lt;/code> 修改要改寫在程式碼裡，CLI 上只執行 &lt;code>describe&lt;/code> 這類讀取用的指令；這篇的指令在沒有 IaC 的環境照著執行，有 IaC 的環境把它們當成欄位對照。instance template 不能修改、又被 MIG 引用著，所以 IaC 改 template 時，取代的順序要另外設成先建新的再刪舊的（Terraform 是 &lt;code>name_prefix&lt;/code> 搭配 &lt;code>lifecycle { create_before_destroy = true }&lt;/code>），否則先刪舊 template 的那一步會因為它還在使用中而失敗；canary 對應 &lt;code>google_compute_instance_group_manager&lt;/code> 的兩個 &lt;code>version&lt;/code> 區塊。這一段是推論，這次沒有用 Terraform 實跑。&lt;/p></description><content:encoded><![CDATA[<p>實測日期是 2026-10-24，在 GCP 測試專案 <code>blog-verify-20261024</code>（zone <code>asia-east1-b</code>）上：MIG 用 Debian 12 的 e2-micro，GKE 是 zonal 的 Standard 叢集（版本 1.36.4-gke、Container-Optimized OS 節點）；控制平面的 IAM 檢查另外用第二個帳號的 access token 驗證。<code>kubectl debug node</code> 與 Pod Security Admission 另外在本機的 kind（Kubernetes v1.37.0、kubectl v1.37.1）上實跑。</p>
<p>這篇記錄把 <a href="/blog/devops/infra/cloud-providers/gcp/hands-on/iap-ssh-migration-walkthrough/" data-link-title="GCP VM 的 SSH 與 RDP 改走 IAP 的實測紀錄：盤點、救援入口、IAP 規則與 IAM、停用舊規則驗證、OS Login 啟用與移除外部 IP" data-link-desc="要把 GCP 專案裡對 0.0.0.0/0 開放 22 與 3389 port 的預設規則換成只放行 IAP，想照一次實跑過的順序做、看每一步沒做或換一種做法的結果時讀；也用在停用舊規則之後連不上、OS Login 啟用後有人登不進去，或拿掉外部 IP 之後要確認出站走了 Cloud NAT 的時候">GCP VM 的 SSH 與 RDP 改走 IAP 的實測紀錄</a>（下文稱 IAP 實作文章）的三項設定（IAP 用的 network tag、OS Login、移除外部 IP）套到 MIG 與 GKE 上的實際操作。內容依操作的順序是：建一份帶這三項設定的新 instance template，先讓一台 VM 換成新版（canary）再讓整組更新；用 <code>recreate-instances</code> 重建一台壞掉的 VM、比對重建之後換掉的主機金鑰；template 本身寫錯時退回上一版；在 GKE 上建一個專門給節點除錯用的 namespace，用 <code>kubectl debug node</code> 查看節點；node pool 的 private nodes、network tag 與 OS Login；控制平面的 authorized networks，以及改連 DNS-based endpoint、關掉 IP 端點的兩步切換。每一步寫出這次的取值與實測結果，以及沒做或換一種做法時的結果；替代做法與沒做時的結果沒有另外標註的是這次實測看到的，照官方文件推出的寫出文件，推論的寫明沒有實跑。逐台設定為什麼在這兩種環境裡會消失、MIG 救援的取捨、GKE 的兩個存取面、<code>kubectl debug node</code> 的權限邊界，以及控制平面的 authorized networks、DNS-based endpoint 與 private endpoint 三種存取方式的差別，寫在 <a href="/blog/devops/infra/cloud-providers/gcp/gke-mig-management-access/" data-link-title="GCP 上由 MIG 與 GKE 管理的 VM 的管理存取：instance template、node pool、kubectl debug 與控制平面存取" data-link-desc="VM 由 managed instance group 或 GKE node pool 管理時，管理通道的設定為什麼要寫在 instance template 與 node pool 而不是逐台修改、MIG 救援用重建、退回 template 與 Serial Console 各自適用的情形、GKE 的控制平面與節點兩個存取面、kubectl debug 取代節點 SSH 的權限邊界（RBAC 與 Pod Security Admission），以及 GKE 控制平面三種端點（authorized networks、DNS-based endpoint 與 private endpoint）的差別">GCP 上由 MIG 與 GKE 管理的 VM 的管理存取</a>（下文稱概念篇），這裡不重寫。</p>
<p>MIG 與 GKE 由 Terraform 這類 IaC 管理時，下面的 <code>gcloud</code> 修改要改寫在程式碼裡，CLI 上只執行 <code>describe</code> 這類讀取用的指令；這篇的指令在沒有 IaC 的環境照著執行，有 IaC 的環境把它們當成欄位對照。instance template 不能修改、又被 MIG 引用著，所以 IaC 改 template 時，取代的順序要另外設成先建新的再刪舊的（Terraform 是 <code>name_prefix</code> 搭配 <code>lifecycle { create_before_destroy = true }</code>），否則先刪舊 template 的那一步會因為它還在使用中而失敗；canary 對應 <code>google_compute_instance_group_manager</code> 的兩個 <code>version</code> 區塊。這一段是推論，這次沒有用 Terraform 實跑。</p>
<h2 id="建新的-instance-template現行設定照抄再加-tagos-login-與不配外部-ip">建新的 instance template：現行設定照抄，再加 tag、OS Login 與不配外部 IP</h2>
<p>instance template 建立之後不能修改，<code>instance-templates create</code> 從預設值開始建、不沿用舊 template 的任何設定，所以先把現行 template 讀出來，逐項帶進新的那一份。這三項設定為什麼要寫進 template、不能在 VM 上逐台改，見概念篇〈autohealing、滾動更新與擴增後消失的逐台設定：network tag、OS Login metadata 與外部 IP〉。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 讀出現行 template 的完整設定，新 template 要逐項帶上</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">gcloud compute instance-templates describe &lt;current-template-name&gt; --format<span class="o">=</span>yaml
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 建一份新的 instance template：現行設定照抄，再加上 IAP 用的 tag、啟用 OS Login、不配外部 IP</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">gcloud compute instance-templates create &lt;new-template-name&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>  --machine-type<span class="o">=</span>&lt;machine-type&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>  --image-family<span class="o">=</span>&lt;image-family&gt; --image-project<span class="o">=</span>&lt;image-project&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="se"></span>  --boot-disk-size<span class="o">=</span>&lt;boot-disk-size&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>  --network<span class="o">=</span>&lt;network-name&gt; --subnet<span class="o">=</span>&lt;subnet-name&gt; --region<span class="o">=</span>&lt;region&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  --service-account<span class="o">=</span>&lt;service-account-email&gt; --scopes<span class="o">=</span>&lt;scopes&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="se"></span>  --labels<span class="o">=</span>&lt;labels&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="se"></span>  --tags<span class="o">=</span>&lt;ssh-via-iap-tag&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="se"></span>  --metadata<span class="o">=</span>enable-oslogin<span class="o">=</span>TRUE <span class="se">\
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="se"></span>  --metadata-from-file<span class="o">=</span>startup-script<span class="o">=</span>&lt;startup-script-file&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="se"></span>  --no-address
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1"># 比對新舊兩份的設定：差異只該是刻意改的 tag、enable-oslogin 與 accessConfigs</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">diff &lt;<span class="o">(</span>gcloud compute instance-templates describe &lt;current-template-name&gt; --format<span class="o">=</span><span class="s2">&#34;yaml(properties)&#34;</span><span class="o">)</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="se"></span>     &lt;<span class="o">(</span>gcloud compute instance-templates describe &lt;new-template-name&gt; --format<span class="o">=</span><span class="s2">&#34;yaml(properties)&#34;</span><span class="o">)</span></span></span></code></pre></div><ul>
<li><code>&lt;current-template-name&gt;</code>：MIG 目前用的 template，<code>gcloud compute instance-groups managed describe &lt;mig-name&gt; --zone=&lt;zone&gt;</code> 的 <code>instanceTemplate</code> 欄。</li>
<li><code>&lt;new-template-name&gt;</code>：新 template 的名稱，自己取；template 不能改名也不能改內容，常見的做法是名稱帶版本或日期。</li>
<li><code>--machine-type</code>、<code>--image-family</code>、<code>--image-project</code>、<code>--boot-disk-size</code>、<code>--network</code>、<code>--subnet</code>、<code>--region</code>、<code>--service-account</code>、<code>--scopes</code>、<code>--labels</code>、<code>--metadata-from-file=startup-script=</code>：照 <code>describe</code> 讀出來的值填，<code>describe</code> 裡還有其他欄位（額外的磁碟、GPU、其他 metadata 鍵）也要對應的旗標一起帶上。<code>--region</code> 是 <code>--subnet</code> 所在的 region。<code>describe</code> 的欄位與旗標不是一對一：映像在 <code>disks[].initializeParams.sourceImage</code>，值可能是一個特定映像的完整路徑，也可能是 <code>.../images/family/&lt;family&gt;</code> 這種 family 的路徑（實測：用 <code>--image-family</code> 建的 template 讀出來是 <code>.../images/family/debian-12</code>），兩種都可以原樣交給 <code>--image=&lt;sourceImage 的值&gt;</code>，取代 <code>--image-family</code> 與 <code>--image-project</code>；服務帳號在 <code>serviceAccounts[].email</code>，template 用的是 Compute Engine 預設服務帳號時這一欄顯示 <code>default</code> 而不是 email（實測），原樣寫成 <code>--service-account=default</code>，建出的 template 用的仍是同一個預設服務帳號；startup-script 在 <code>metadata.items</code> 裡 <code>key: startup-script</code> 的 <code>value</code>，先把它存成檔案再交給 <code>--metadata-from-file</code>；regional 的 template 讀取時 <code>describe</code> 要加 <code>--region=&lt;region&gt;</code>。</li>
<li><code>--tags=&lt;ssh-via-iap-tag&gt;</code>：與 IAP 規則的 <code>--target-tags</code> 相同的 tag。新 template 只帶這裡列出的 tag，所以現行 template 已經有的其他 tag（<code>describe</code> 輸出的 <code>tags.items</code>，例如負載平衡器健康檢查用的）要一起列進來，逗號分隔。</li>
<li><code>--metadata=enable-oslogin=TRUE</code>：讓這個 template 建出的 VM 都啟用 OS Login；專案層級已經啟用時可以省略。</li>
<li><code>--no-address</code>：不配外部 IP。先確認流量入口已經在負載平衡器上，<code>gcloud compute backend-services list --format=&quot;table(name, backends[].group)&quot;</code> 的 <code>backends</code> 欄要有這個 MIG。這一點只能事先查：服務若是靠 VM 的外部 IP 對外，〈先讓一台 VM 換成新 template（canary），再讓整組更新〉換上新版的那一台只是收不到流量、不容易察覺，canary 這一步驗不出來，整組更新之後服務就斷了。出站改由 Cloud NAT 承接，所以 Cloud NAT 要在滾動更新之前建好。已經存在的 VM 在滾動更新把它換成新版時才失去外部 IP。</li>
</ul>
<p>這次用 <code>--service-account=default</code> 建的新 template，與現行 template 的 <code>properties</code> 比對，只差刻意改的 tag、<code>enable-oslogin</code> 與 <code>accessConfigs</code>。</p>
<ul>
<li><strong>沒做時</strong>（漏帶 <code>describe</code> 裡的欄位）：漏掉的欄位回到預設值，常見的後果是 VM 換成預設的服務帳號而失去存取權限，或啟動腳本沒有執行（從 <code>create</code> 由預設值開始建這一點推論，這次沒有實跑）；上面的 <code>diff</code> 會把漏掉的欄位列出來。</li>
<li><strong>替代做法</strong>：
<ul>
<li>用 <code>gcloud compute instance-templates create &lt;new-template-name&gt; --source-instance=&lt;instance-name&gt; --source-instance-zone=&lt;zone&gt;</code> 從一台現有的 VM 產生 template，換到的是不必逐項抄旗標；付出的是這條指令只能另外覆寫磁碟的定義，tag、OS Login 與 <code>--no-address</code> 要再以它為基礎建第二份，而來源 VM 有靜態外部 IP 時，那個位址會被抄進 template（Compute Engine〈Create instance templates〉，這次沒有實跑）。</li>
<li>在專案層級設 <code>enable-oslogin=TRUE</code>、不寫進 template，換到的是 template 少一個鍵，付出的是 template 的行為依賴專案的設定，搬到別的專案就變了（推論，這次沒有實跑）。</li>
</ul>
</li>
</ul>
<h2 id="先讓一台-vm-換成新-templatecanary再讓整組更新">先讓一台 VM 換成新 template（canary），再讓整組更新</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 先讓一台 VM 用新 template（canary），其餘維持現行 template</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">gcloud compute instance-groups managed rolling-action start-update &lt;mig-name&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --version<span class="o">=</span><span class="nv">template</span><span class="o">=</span>&lt;current-template-name&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --canary-version<span class="o">=</span><span class="nv">template</span><span class="o">=</span>&lt;new-template-name&gt;,target-size<span class="o">=</span><span class="m">1</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --zone<span class="o">=</span>&lt;zone&gt;
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># 那一台驗證通過之後，讓整組換成新 template</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">gcloud compute instance-groups managed rolling-action start-update &lt;mig-name&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="se"></span>  --version<span class="o">=</span><span class="nv">template</span><span class="o">=</span>&lt;new-template-name&gt; --zone<span class="o">=</span>&lt;zone&gt;</span></span></code></pre></div><ul>
<li><code>--version=template=</code>：canary 那一行放<strong>現行</strong>的 template，代表其餘 VM 維持原狀；整組更新那一行放新的 template。兩行的 <code>--version</code> 放的 template 相反。</li>
<li><code>--canary-version=template=&lt;new-template-name&gt;,target-size=1</code>：讓一台 VM 換成新 template；<code>target-size</code> 也可以寫百分比（<code>10%</code>），條件是 MIG 有 10 台以上（Compute Engine〈Rolling out updates〉）。</li>
<li><code>&lt;mig-name&gt;</code>、<code>--zone</code>：MIG 的名稱與所在 zone，<code>gcloud compute instance-groups managed list</code> 查得到；regional MIG 把 <code>--zone</code> 換成 <code>--region</code>。</li>
</ul>
<p><code>start-update</code> 送出之後 MIG 在背景替換 VM，指令本身不等它完成。用下面兩條確認替換做完、並找出 canary 那一台：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 等到每台 VM 都換成指定的版本才返回</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">gcloud compute instance-groups managed wait-until &lt;mig-name&gt; --version-target-reached --zone<span class="o">=</span>&lt;zone&gt;
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 列出 MIG 裡的 VM 與各自用的 template，INSTANCE_TEMPLATE 欄是新 template 的那一台就是 canary</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">gcloud compute instance-groups managed list-instances &lt;mig-name&gt; --zone<span class="o">=</span>&lt;zone&gt;</span></span></code></pre></div><p>實測 <code>wait-until</code> 在替換做完之後才返回，<code>list-instances</code> 的 <code>INSTANCE_TEMPLATE</code> 欄只有一台是新 template。找到 canary 那一台之後，經由 IAP 連進去，確認 OS Login 登得進去、出站經過 Cloud NAT（驗證方式見 <a href="/blog/devops/infra/cloud-providers/gcp/hands-on/iap-ssh-migration-walkthrough/" data-link-title="GCP VM 的 SSH 與 RDP 改走 IAP 的實測紀錄：盤點、救援入口、IAP 規則與 IAM、停用舊規則驗證、OS Login 啟用與移除外部 IP" data-link-desc="要把 GCP 專案裡對 0.0.0.0/0 開放 22 與 3389 port 的預設規則換成只放行 IAP，想照一次實跑過的順序做、看每一步沒做或換一種做法的結果時讀；也用在停用舊規則之後連不上、OS Login 啟用後有人登不進去，或拿掉外部 IP 之後要確認出站走了 Cloud NAT 的時候">GCP VM 的 SSH 與 RDP 改走 IAP 的實測紀錄</a> 的〈建 Cloud NAT 並移除外部 IP〉）。整組更新之後同樣用 <code>wait-until</code> 等它完成。</p>
<ul>
<li><strong>沒做時</strong>（不經過 canary、直接整組更新）：template 的 tag 或 OS Login 寫錯時，整組 VM 一起連不上（照滾動更新的作用推論，這次沒有實跑）。</li>
<li><strong>替代做法</strong>：
<ul>
<li>只改 tag 與 metadata 時加 <code>--minimal-action=refresh</code>：Compute Engine〈Rolling out updates〉寫明 refresh 不停止 VM，可以套用 metadata、labels、tags 這類項目，換到的是 VM 不重建、主機金鑰不變；付出的是 <code>--no-address</code> 移除外部 IP 不在那份清單上，很可能仍要重建（照官方文件推出，這次沒有實跑）。</li>
<li><code>--type=opportunistic</code> 只在 VM 重建或擴增時才套用新 template，換到的是不主動中斷任何一台，付出的是組內新舊混雜，舊的 VM 一直沒有新的 tag（照官方文件推出，這次沒有實跑）。</li>
<li>整組更新時加 <code>--max-surge=1 --max-unavailable=0</code>，每一批先多建一台新的、再刪一台舊的（實測先建後刪），換到的是更新期間可用台數不減少，付出的是多一台 VM 的費用，而且這兩個旗標只控制速度，更新仍會把整組換完。regional MIG 的 <code>--max-surge</code> 用固定數字時，GCP 只接受 0 或不小於 zone 數的值，錯誤訊息是 <code>has to be either 0 or at least equal to the number of zones</code>，所以分布在兩個以上 zone 的 regional MIG，<code>--max-surge=1</code> 會被拒絕。</li>
</ul>
</li>
</ul>
<h2 id="重建一台壞掉的-vmrecreate-instances-與重建之後的主機金鑰">重建一台壞掉的 VM：recreate-instances 與重建之後的主機金鑰</h2>
<p>一台 VM 連不上、而原因是那台 VM 自己壞了（sshd 設定改壞、磁碟滿），讓 MIG 依 template 重建它：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 依 template 重建一台 VM：開機磁碟換成依 MIG 目前的版本設定新建的一顆，VM 重新開機</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">gcloud compute instance-groups managed recreate-instances &lt;mig-name&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --instances<span class="o">=</span>&lt;instance-name&gt; --zone<span class="o">=</span>&lt;zone&gt;</span></span></code></pre></div><p><code>&lt;instance-name&gt;</code> 是要重建的那台 VM，<code>gcloud compute instance-groups managed list-instances &lt;mig-name&gt; --zone=&lt;zone&gt;</code> 列出 MIG 裡的 VM。實測重建在稽核紀錄裡是一次 <code>instances.update</code>（沒有 delete 與第二次 insert），VM 沿用原本的名稱與執行個體 ID，<code>gcloud compute instances describe</code> 的建立時間也不變，所以確認重建做完要看開機磁碟：MIG 建的 VM 開機磁碟預設與 VM 同名，<code>gcloud compute disks describe &lt;instance-name&gt; --zone=&lt;zone&gt; --format=&quot;value(creationTimestamp)&quot;</code> 印出的時間落在重建的那一刻，就代表磁碟已經換新。canary 期間重建的 VM 用哪一版，由 MIG 依兩個版本各自的 <code>target-size</code> 分配，實測重建的那一台沿用了原版本。</p>
<p>重建之後第一次 <code>gcloud compute ssh</code> 出現 <code>WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!</code> 並拒絕連線（實測），原因與這個警告為什麼不能直接略過，見 <a href="/blog/devops/infra/cloud-providers/gcp/gke-mig-management-access/" data-link-title="GCP 上由 MIG 與 GKE 管理的 VM 的管理存取：instance template、node pool、kubectl debug 與控制平面存取" data-link-desc="VM 由 managed instance group 或 GKE node pool 管理時，管理通道的設定為什麼要寫在 instance template 與 node pool 而不是逐台修改、MIG 救援用重建、退回 template 與 Serial Console 各自適用的情形、GKE 的控制平面與節點兩個存取面、kubectl debug 取代節點 SSH 的權限邊界（RBAC 與 Pod Security Admission），以及 GKE 控制平面三種端點（authorized networks、DNS-based endpoint 與 private endpoint）的差別">GCP 上由 MIG 與 GKE 管理的 VM 的管理存取</a> 的〈MIG 的救援：重建、退回舊版 template 與 Serial Console 各自適用的情形〉。比對的來源是 VM 回報到 guest attributes 的主機金鑰，專案或 template 的 metadata 要事先設了 <code>enable-guest-attributes=TRUE</code>（專案層用 <code>gcloud compute project-info add-metadata --metadata enable-guest-attributes=TRUE</code>）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 讀出 VM 回報的 ED25519 主機金鑰指紋，與警告訊息裡 &#34;The fingerprint for the ED25519 key sent by the remote host is&#34; 下一行的 SHA256 值比對</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">gcloud compute instances get-guest-attributes &lt;instance-name&gt; --zone<span class="o">=</span>&lt;zone&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --query-path<span class="o">=</span>hostkeys/ssh-ed25519 --format<span class="o">=</span><span class="s2">&#34;value(value)&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  <span class="p">|</span> awk <span class="s1">&#39;{print &#34;ssh-ed25519 &#34; $1}&#39;</span> <span class="p">|</span> ssh-keygen -lf -
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># 兩個指紋相同之後，移除這台 VM 的舊紀錄，再用 gcloud compute ssh 連線</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">ssh-keygen -R compute.&lt;instance-id&gt; -f ~/.ssh/google_compute_known_hosts</span></span></code></pre></div><p><code>&lt;instance-id&gt;</code> 是 <code>gcloud compute instances describe &lt;instance-name&gt; --zone=&lt;zone&gt; --format=&quot;value(id)&quot;</code> 印出的數字。<code>ssh-keygen -R</code> 會把原本的檔案備份成同目錄的 <code>google_compute_known_hosts.old</code>。實測啟用 guest attributes 之後，gcloud 不會自動更新已經存在的 known_hosts 紀錄，讀出的 ED25519 指紋與警告裡的一致，移除舊紀錄之後即可連線。兩個指紋不同時不要移除，先確認連到的 IP 仍是這台 VM 的位址，以及連線是否經過了 IAP 以外的代理；兩項都正常而指紋仍不同時，改從 <a href="/blog/devops/infra/knowledge-cards/serial-console/" data-link-title="Serial Console（序列主控台）" data-link-desc="SSH 因為防火牆、sshd 設定或磁碟滿而連不進主機，或要規劃管理通道斷掉時的備用路徑時，查序列主控台與頻外管理介面的用法與它本身的暴露面">Serial Console</a> 登入 VM、讀 <code>/etc/ssh/ssh_host_ed25519_key.pub</code> 的指紋再比一次。警告寫的是 ECDSA 時，指令裡的 <code>ED25519</code> 與 <code>ssh-ed25519</code> 分別換成 <code>ECDSA</code> 與 <code>ecdsa-sha2-nistp256</code>。</p>
<ul>
<li><strong>沒做時</strong>（不設 <code>enable-guest-attributes</code>）：重建之後沒有不經過這條 SSH 連線的地方可以比對主機金鑰，只剩從 Serial Console 登入讀指紋，或不比對就移除舊紀錄；不比對就移除，等於關掉主機金鑰檢查（推論，這次沒有實跑）。</li>
<li><strong>替代做法</strong>：
<ul>
<li>從 Serial Console 登入修那台 VM，換到的是保留現場，付出的是要事先在映像檔裡放救援帳號（救援帳號的密碼雜湊寫在映像檔裡，不寫進 template 的 startup-script，理由與共用密碼雜湊的代價見概念篇〈MIG 的救援：重建、退回舊版 template 與 Serial Console 各自適用的情形〉）；登入與 sudo 的做法見 IAP 實作文章的〈準備救援入口：Serial Console 與本機救援帳號〉。</li>
<li>VM 上有不能丟的狀態時，MIG 的 stateful 設定（stateful policy 或 per-instance config）把指定的磁碟、IP 與 metadata 標為要保留，<code>recreate-instances</code> 重建時會沿用它們（Compute Engine〈Stateful MIGs〉，這次沒有實跑）。</li>
</ul>
</li>
</ul>
<h2 id="template-本身寫錯時退回上一版">template 本身寫錯時退回上一版</h2>
<p>重建修不好的是 template 本身的錯：新版 template 的 tag 寫錯、啟動腳本有錯，重建出來的每一台都一樣壞。這種錯要退回上一版 template，所以舊版 template 留到新版驗證完之後再刪：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 退回上一版 template：整組換回舊版；canary 期間執行時，只有 canary 那幾台被換回</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">gcloud compute instance-groups managed rolling-action start-update &lt;mig-name&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --version<span class="o">=</span><span class="nv">template</span><span class="o">=</span>&lt;previous-template-name&gt; --zone<span class="o">=</span>&lt;zone&gt;</span></span></code></pre></div><p><code>&lt;previous-template-name&gt;</code> 是新版之前那一份 template 的名稱，<code>gcloud compute instance-templates list</code> 列得出來。這次實測退回成功。Compute Engine〈Rolling out updates〉寫明沒有專門的退回指令，退回就是送一次指向舊 template 的更新。</p>
<ul>
<li><strong>沒做時</strong>（新版一驗證就刪掉舊 template）：退回時要用 <code>describe</code> 讀過的舊設定重建一份 template，而舊 template 已經刪掉、讀不到了（從 template 不能修改、刪了就沒有這兩點推論，這次沒有實跑）。</li>
</ul>
<h2 id="gke-節點除錯建專用的-namespace用-kubectl-debug-node-查看節點">GKE 節點除錯：建專用的 namespace，用 kubectl debug node 查看節點</h2>
<p>節點除錯集中在一個刻意保留為 <code>privileged</code> 等級、只有維運群組能建 Pod 的 namespace，由叢集管理員事先建好：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 建一個專門給節點除錯用的 namespace，並允許特權 Pod</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">kubectl create namespace &lt;namespace&gt;
</span></span><span class="line"><span class="ln">3</span><span class="cl">kubectl label namespace &lt;namespace&gt; pod-security.kubernetes.io/enforce<span class="o">=</span>privileged</span></span></code></pre></div><p><code>&lt;namespace&gt;</code> 自己取，例如 <code>node-debug</code>；建好之後只把 <a href="/blog/devops/infra/cloud-providers/gcp/gke-mig-management-access/" data-link-title="GCP 上由 MIG 與 GKE 管理的 VM 的管理存取：instance template、node pool、kubectl debug 與控制平面存取" data-link-desc="VM 由 managed instance group 或 GKE node pool 管理時，管理通道的設定為什麼要寫在 instance template 與 node pool 而不是逐台修改、MIG 救援用重建、退回 template 與 Serial Console 各自適用的情形、GKE 的控制平面與節點兩個存取面、kubectl debug 取代節點 SSH 的權限邊界（RBAC 與 Pod Security Admission），以及 GKE 控制平面三種端點（authorized networks、DNS-based endpoint 與 private endpoint）的差別">GCP 上由 MIG 與 GKE 管理的 VM 的管理存取</a> 的〈kubectl debug node 的權限邊界：RBAC 與 Pod Security Admission〉列的那組 <code>pods</code> 與 <code>pods/attach</code> 權限綁給維運群組。接著在這個 namespace 裡對節點開除錯 Pod：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 在指定節點上開一個除錯用的 Pod，節點的根目錄掛在 /host</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">kubectl debug node/&lt;node-name&gt; -it --image<span class="o">=</span>ubuntu --profile<span class="o">=</span>sysadmin --namespace<span class="o">=</span>&lt;namespace&gt;
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 進到 Pod 之後，切換到節點的根目錄，像登入節點一樣查看</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">chroot /host</span></span></code></pre></div><ul>
<li><code>&lt;node-name&gt;</code>：節點名稱，<code>kubectl get nodes</code> 的第一欄。</li>
<li><code>--image=ubuntu</code>：除錯 Pod 用的映像檔，換成組織允許、帶著除錯工具的映像檔。</li>
<li><code>--profile=sysadmin</code>：讓除錯 Pod 以<a href="/blog/backend/knowledge-cards/privileged-container/" data-link-title="Privileged Container（特權容器）" data-link-desc="在 Pod 設定裡看到 securityContext.privileged=true、要判斷某個除錯或系統元件為什麼需要它，或評估誰能建這種 Pod 等於持有節點 root 權限時，查特權容器能做什麼與它的權限邊界">特權容器（privileged container）</a>執行，多給的是需要額外 Linux capability 的操作，例如用 <code>nsenter</code> 進入節點 PID 1 的 namespace、掛載檔案系統；只查看檔案與 log 時，預設的 <code>general</code> profile 就夠了（<code>kubectl debug --help</code> 的 <code>--profile</code> 說明寫著預設值）。</li>
<li><code>--namespace=&lt;namespace&gt;</code>：除錯 Pod 建在上面建好的那個 namespace；不給時落在 kubeconfig 目前設定的 namespace（通常是 <code>default</code>）。</li>
</ul>
<p>這個 Pod 用完不會自動刪除，結束後用 <code>kubectl delete pod &lt;debug-pod-name&gt; --namespace=&lt;namespace&gt;</code> 刪掉（<code>kubectl get pods --namespace=&lt;namespace&gt;</code> 找名稱以 <code>node-debugger-</code> 開頭的那一個）。實測 GKE 上未設 label 的 <code>default</code> 接受除錯 Pod、設了 <code>baseline</code> 的 namespace 拒絕；kind 上 <code>privileged</code> 的 namespace 接受。</p>
<ul>
<li><strong>沒做時</strong>（不建專用 namespace）：沒有設 Pod Security label 的 namespace（包括 <code>default</code>）在 GKE Standard 上接受特權 Pod，任何能在 <code>default</code> 建 Pod 的帳號都能用這個方法以 root 操作節點。少了 <code>list</code> 或 <code>watch</code> 權限時，Pod 已經建好而 kubectl 停在等待、反覆印出 <code>cannot list resource &quot;pods&quot;</code>；少了 <code>pods/attach</code> 時 Pod 建得出來卻進不去；兩種都會留下一個在跑的除錯 Pod，要用 <code>kubectl delete pod</code> 刪掉（kind 上實跑）。</li>
<li><strong>替代做法</strong>：
<ul>
<li>用 <code>--profile=baseline</code> 或 <code>restricted</code>，換到的是建得進 <code>baseline</code> 以上的 namespace，付出的是看不到節點的檔案。</li>
<li>查節點層的狀況先看 Cloud Logging 裡的節點紀錄，換到的是不必在節點上開任何 Pod，付出的是只看得到已經送進 Logging 的紀錄（推論，這次沒有實跑）。GKE Autopilot 預設拒絕特權 Pod，也不提供節點 SSH，只剩這一條（GKE〈Autopilot overview〉，這次沒有實跑）。</li>
</ul>
</li>
</ul>
<h2 id="node-poolprivate-nodesnetwork-tag-與-os-login">node pool：private nodes、network tag 與 OS Login</h2>
<p>節點仍要 SSH 時（GKE Standard），把 <a href="/blog/devops/infra/cloud-providers/gcp/hands-on/iap-ssh-migration-walkthrough/" data-link-title="GCP VM 的 SSH 與 RDP 改走 IAP 的實測紀錄：盤點、救援入口、IAP 規則與 IAM、停用舊規則驗證、OS Login 啟用與移除外部 IP" data-link-desc="要把 GCP 專案裡對 0.0.0.0/0 開放 22 與 3389 port 的預設規則換成只放行 IAP，想照一次實跑過的順序做、看每一步沒做或換一種做法的結果時讀；也用在停用舊規則之後連不上、OS Login 啟用後有人登不進去，或拿掉外部 IP 之後要確認出站走了 Cloud NAT 的時候">GCP VM 的 SSH 與 RDP 改走 IAP 的實測紀錄</a> 的三項設定搬到 node pool 上：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 既有 node pool 改成節點只有內部 IP（GKE 依升級策略逐批重建節點）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">gcloud container node-pools update &lt;pool-name&gt; --cluster<span class="o">=</span>&lt;cluster-name&gt; --location<span class="o">=</span>&lt;location&gt; --enable-private-nodes
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 讀出現有的 tag，再用整份清單設定（這個指令整組取代使用者設定的 tag）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">gcloud container node-pools describe &lt;pool-name&gt; --cluster<span class="o">=</span>&lt;cluster-name&gt; --location<span class="o">=</span>&lt;location&gt; --format<span class="o">=</span><span class="s2">&#34;value(config.tags)&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">gcloud container node-pools update &lt;pool-name&gt; --cluster<span class="o">=</span>&lt;cluster-name&gt; --location<span class="o">=</span>&lt;location&gt; --tags<span class="o">=</span>&lt;tag1&gt;,&lt;tag2&gt;</span></span></code></pre></div><ul>
<li><code>&lt;pool-name&gt;</code>、<code>&lt;cluster-name&gt;</code>：<code>gcloud container node-pools list --cluster=&lt;cluster-name&gt; --location=&lt;location&gt;</code> 與 <code>gcloud container clusters list</code> 查得到。<code>&lt;location&gt;</code> 是叢集所在的 zone 或 region。</li>
<li><code>--enable-private-nodes</code>：先確認節點所在 subnet 的 region 已經有 Cloud NAT，換成內部 IP 之後節點的出站都經過它；沒有它時節點拉不到外部 registry 的映像，工作負載起不來。GKE 依 node pool 的升級策略逐批重建節點，所以排在可以承受節點輪替的時段做。新叢集在 <code>gcloud container clusters create</code> 加 <code>--enable-private-nodes</code> 作為整個叢集的預設。實測既有 node pool 的這項變更重建了節點，出站走 Cloud NAT、tag 保留。</li>
<li><code>--tags=&lt;tag1&gt;,&lt;tag2&gt;</code>：實測整組取代（gcloud 的確認提示也這樣寫），GKE 自動建立的防火牆規則用的 tag 保留，節點沒有重建。</li>
</ul>
<p>OS Login 的授權方式同單台 VM，見 <a href="/blog/devops/infra/cloud-providers/gcp/hands-on/iap-ssh-migration-walkthrough/" data-link-title="GCP VM 的 SSH 與 RDP 改走 IAP 的實測紀錄：盤點、救援入口、IAP 規則與 IAM、停用舊規則驗證、OS Login 啟用與移除外部 IP" data-link-desc="要把 GCP 專案裡對 0.0.0.0/0 開放 22 與 3389 port 的預設規則換成只放行 IAP，想照一次實跑過的順序做、看每一步沒做或換一種做法的結果時讀；也用在停用舊規則之後連不上、OS Login 啟用後有人登不進去，或拿掉外部 IP 之後要確認出站走了 Cloud NAT 的時候">GCP VM 的 SSH 與 RDP 改走 IAP 的實測紀錄</a> 的〈啟用 OS Login：先授角色、單台先行、再做專案層級〉。專案層級的 <code>enable-oslogin=TRUE</code> 是否套用到 GKE Standard 的節點，GKE 自己的兩份文件寫法相反，所以啟用之後經由 IAP 實際登入一個節點確認：登入後執行 <code>whoami</code>，看到的是由 Google 帳號 email 轉換而來的 OS Login 帳號，才表示這個 node pool 的節點套用了 OS Login；看到的是 metadata 金鑰對應的本機帳號，代表節點沒有套用，SSH 仍由 metadata 金鑰控制。這次在 1.36.4-gke 的節點上看到的是 OS Login 帳號，這是單一版本的觀察。</p>
<ul>
<li><strong>沒做時</strong>（只在單一節點上 <code>add-tags</code>）：節點升級或自動修復重建之後 tag 消失，IAP 規則套不到它（照概念篇〈autohealing、滾動更新與擴增後消失的逐台設定：network tag、OS Login metadata 與外部 IP〉的機制推論，這次沒有實跑）。</li>
<li><strong>替代做法</strong>：不開節點 SSH、只用 <code>kubectl debug node</code>，換到的是不必維護節點的 tag、OS Login 與 IAP 規則，付出的是能建除錯 Pod 的帳號就等於節點的 root，權限邊界移到 RBAC 與 Pod Security Admission。</li>
</ul>
<h2 id="控制平面authorized-networks或改連-dns-based-endpoint-再關掉-ip-端點">控制平面：authorized networks，或改連 DNS-based endpoint 再關掉 IP 端點</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># authorized networks：只放行列出的來源 IP 範圍連到控制平面的 IP 端點</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">gcloud container clusters update &lt;cluster-name&gt; --location<span class="o">=</span>&lt;location&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  --enable-master-authorized-networks --master-authorized-networks<span class="o">=</span>&lt;cidr1&gt;,&lt;cidr2&gt;
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># DNS-based endpoint 的啟用：IP 端點仍然開著</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">gcloud container clusters update &lt;cluster-name&gt; --location<span class="o">=</span>&lt;location&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>  --enable-dns-access
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 關掉 IP 端點：每個人與每條 CI 都改連網域名稱端點之後才執行</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">gcloud container clusters update &lt;cluster-name&gt; --location<span class="o">=</span>&lt;location&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="se"></span>  --no-enable-ip-access</span></span></code></pre></div><ul>
<li><code>&lt;cidr1&gt;,&lt;cidr2&gt;</code>：允許的來源 IP 範圍，<a href="/blog/devops/infra/knowledge-cards/cidr/" data-link-title="CIDR（Classless Inter-Domain Routing）" data-link-desc="用前綴長度表示 IP 地址範圍的表示法，決定 VPC 與 subnet 的地址空間大小">CIDR</a> 格式，單一個 IP 寫成 <code>/32</code>。實測以 Private Service Connect 建立的叢集拒絕文件範例用的保留網段 <code>203.0.113.0/24</code>，錯誤訊息寫明只接受在網際網路上可路由的範圍。<code>--master-authorized-networks</code> 每次都整組取代舊清單，執行這條指令的人與 CI 的出口 IP 要列在裡面：實測漏列時指令一生效 <code>kubectl</code> 就逾時，<code>gcloud</code> 不經過這個端點，用同一條指令改回清單就恢復。</li>
<li><code>--enable-dns-access</code> 與 <code>--no-enable-ip-access</code> 分成兩次執行：寫在同一條指令裡會在啟用的同時關掉 IP 端點，還在用 IP 端點的人與 CI 當場連不上。<code>--no-enable-ip-access</code> 關掉的是公開與內部兩個 IP 端點，VPC 內用內部 IP 連 API server 的跳板機、自架 runner 與 Terraform 也在要改的範圍內。</li>
</ul>
<p>改連 DNS-based endpoint 時，每個人在本機執行 <code>gcloud container clusters get-credentials &lt;cluster-name&gt; --location=&lt;location&gt; --dns-endpoint</code>，讓 kubeconfig 指向那個網域名稱。確認有沒有改連：每個人與每條 CI 用的 kubeconfig，<code>kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}'</code> 輸出的是網域名稱而不是 IP。DNS-based endpoint 預設只接受 Google 的 IAM 憑證，CI 或工具若用 kubeconfig 裡靜態的 Kubernetes ServiceAccount token 或 client certificate，要另外在 <code>clusters update</code> 加 <code>--enable-k8s-tokens-via-dns</code> 或 <code>--enable-k8s-certs-via-dns</code>。</p>
<p>實測：DNS endpoint 的兩步切換不受 IP 清單限制；<code>--no-enable-ip-access</code> 之後 IP 端點逾時（對照組：開啟且來源在清單內時 HTTP 401），DNS 照常；<code>clusters update --enable-ip-access</code> 恢復。用第二個帳號以 <code>--token</code> 帶入它的 access token：沒有 IAM 角色時 DNS endpoint 回 Google 前端的 403 HTML；只有 <code>roles/container.clusterViewer</code> 時進到 API server，列節點回 <code>cannot list resource &quot;nodes&quot;</code>，訊息裡寫著它要的 IAM 權限 <code>container.nodes.list</code>。</p>
<ul>
<li><strong>沒做時</strong>（維持公開端點、不設任何限制）：任何地方都連得到 API server，只靠 Kubernetes 的認證與 RBAC 擋下沒有權限的請求，等於網路可達性全開（照 GKE〈Access the control plane〉推出，這次沒有在公開端點上量）。</li>
<li><strong>替代做法</strong>：
<ul>
<li>authorized networks 換到的是設定一條指令，付出的是 IP 會變動時要一直改、分辨不出個人；設了之後，GCP 上其他客戶的外部 IP 是否放行由 <code>--enable-google-cloud-access</code>／<code>--no-enable-google-cloud-access</code> 決定。實測沒碰過這兩個旗標時，<code>describe</code> 沒有 <code>gcpPublicCidrsAccessEnabled</code> 欄位，這時實際是否放行沒有驗證；變更可能要幾個小時才生效（GKE 文件，這次沒有實跑）。</li>
<li>private endpoint（<code>--enable-private-endpoint</code>）換到的是控制平面只剩內部 IP，付出的是從 VPC 外管理時要經過 VPN 或跳板機（GKE 文件，這次沒有實跑）。三種端點的比較見概念篇〈GKE 控制平面的存取：authorized networks、DNS-based endpoint 與 private endpoint〉。</li>
</ul>
</li>
</ul>
]]></content:encoded></item><item><title>GCP 上由 MIG 與 GKE 管理的 VM 的管理存取：instance template、node pool、kubectl debug 與控制平面存取</title><link>https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/gke-mig-management-access/</link><pubDate>Fri, 23 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/gke-mig-management-access/</guid><description>&lt;p>這篇處理 GCP 上兩種由 template 自動建立與重建 VM 的環境，其中 GKE 是 Google Kubernetes Engine，GCP 託管的 Kubernetes 服務：managed instance group（&lt;a href="https://tarrragon.github.io/blog/devops/infra/knowledge-cards/managed-instance-group/" data-link-title="Managed Instance Group（MIG，代管執行個體群組）" data-link-desc="VM 被自動重建之後設定不見、要決定一組 VM 的設定該寫在哪裡，或要判斷一台 VM 能不能直接刪掉重建時，查 GCP 的 MIG 與 instance template 怎麼建立、修復與更新 VM">MIG&lt;/a>，依 instance template 自動建立、修復與縮放一組 VM）與 GKE 的 node pool（一組共用同一份設定的叢集節點，GKE 在底下用 MIG 管理它們）。範圍是在這兩種環境裡套用 &lt;a href="https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/gcp-iap-ssh-access/" data-link-title="GCP VM 的 SSH 與 RDP 管理通道：從預設的全開規則改走 IAP TCP forwarding" data-link-desc="GCP 預設網路自帶的防火牆規則與它們留下的暴露面、IAP TCP forwarding 的連線路徑與日常連線指令、切換到 IAP 的順序為什麼這樣排、IAP 通道連不上時的依層判讀，以及 IAM 授權範圍、OS Login 的啟用條件與工作階段的結束、拿掉外部 IP 之後的出站路徑、Organization Policy、階層式防火牆、稽核紀錄與 Security Command Center 的 OPEN_SSH_PORT finding 在 GCP 上的處理">GCP VM 的 SSH 與 RDP 管理通道&lt;/a>的三項設定：&lt;a href="https://tarrragon.github.io/blog/devops/infra/knowledge-cards/identity-aware-proxy/" data-link-title="Identity-Aware Proxy（身分感知代理）" data-link-desc="要讓管理員或使用者不經公開入口連到內部主機與服務、評估把存取控制從來源 IP 改成帳號身分時，查身分感知代理的連線路徑、前提與代價">IAP&lt;/a>（Identity-Aware Proxy，GCP 依 IAM 權限轉送 SSH 連線的代理）、&lt;a href="https://tarrragon.github.io/blog/devops/infra/knowledge-cards/os-login/" data-link-title="OS Login" data-link-desc="GCP 專案要讓 SSH 登入權限跟著 Google 帳號與 IAM 走、啟用 OS Login 之後管理員或 CI 連不進 VM、或盤點專案 metadata 裡殘留的 SSH 公鑰時，查 OS Login 的運作方式、IAM role 與它和 metadata 金鑰的差別">OS Login&lt;/a>（讓 VM 的登入帳號與公鑰由 Google 帳號與 IAM 決定）與移除外部 IP：這些設定為什麼要寫在 template 與 node pool、怎麼判斷一台 VM 屬不屬於 MIG、MIG 的幾種救援各自適用的情形、GKE 的兩個存取面、&lt;code>kubectl debug node&lt;/code> 的權限邊界，以及 GKE 另外要管的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/control-plane/" data-link-title="Control Plane" data-link-desc="負責下發策略、配置與路由決策的控制層">控制平面（control plane）&lt;/a>存取。建新 template 與 canary 滾動更新、重建與主機金鑰的比對、除錯 namespace、node pool 設定與控制平面端點切換的逐步指令與實跑結果，在 &lt;a href="https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/hands-on/mig-gke-access-walkthrough/" data-link-title="GCP 上 MIG 與 GKE 管理存取的實測紀錄：instance template 與 canary 滾動更新、重建與主機金鑰、kubectl debug 的 namespace、node pool 設定與控制平面端點的切換" data-link-desc="要把 IAP 的 tag、OS Login 與移除外部 IP 套到 MIG 或 GKE 上，想照一次實跑過的順序做、看每一步換一種做法的結果時讀；也用在 MIG 重建之後 SSH 出現 REMOTE HOST IDENTIFICATION HAS CHANGED、kubectl debug 停在等待，或關掉 GKE 控制平面的 IP 端點之前要確認每個人都改連的時候">GCP 上 MIG 與 GKE 管理存取的實測紀錄&lt;/a>。管理通道拆成網路可達性、通道授權與主機登入驗證三層的模型見&lt;a href="https://tarrragon.github.io/blog/devops/infra/03-network-foundation/management-access-channel/" data-link-title="伺服器管理通道的設計：網路可達性、通道授權、主機登入驗證三層的做法與選擇，以及救援入口" data-link-desc="與雲端廠商無關、適用於雲端 VM、地端主機與 VPS 的管理員登入通道設計：拆成網路可達性、通道授權與主機登入驗證的模型，各層的做法（來源白名單、跳板機、VPN、身分感知代理、代理程式反向連線、SSH 憑證）、更換 port 與 fail2ban 改不了的部分，依存取者身分、連線送達方式、收回方式選擇組合的依據，以及移除公開 IP 之後的出站路徑、切換順序、救援入口、組織護欄與稽核">伺服器管理通道的設計&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>這篇處理 GCP 上兩種由 template 自動建立與重建 VM 的環境，其中 GKE 是 Google Kubernetes Engine，GCP 託管的 Kubernetes 服務：managed instance group（<a href="/blog/devops/infra/knowledge-cards/managed-instance-group/" data-link-title="Managed Instance Group（MIG，代管執行個體群組）" data-link-desc="VM 被自動重建之後設定不見、要決定一組 VM 的設定該寫在哪裡，或要判斷一台 VM 能不能直接刪掉重建時，查 GCP 的 MIG 與 instance template 怎麼建立、修復與更新 VM">MIG</a>，依 instance template 自動建立、修復與縮放一組 VM）與 GKE 的 node pool（一組共用同一份設定的叢集節點，GKE 在底下用 MIG 管理它們）。範圍是在這兩種環境裡套用 <a href="/blog/devops/infra/cloud-providers/gcp/gcp-iap-ssh-access/" data-link-title="GCP VM 的 SSH 與 RDP 管理通道：從預設的全開規則改走 IAP TCP forwarding" data-link-desc="GCP 預設網路自帶的防火牆規則與它們留下的暴露面、IAP TCP forwarding 的連線路徑與日常連線指令、切換到 IAP 的順序為什麼這樣排、IAP 通道連不上時的依層判讀，以及 IAM 授權範圍、OS Login 的啟用條件與工作階段的結束、拿掉外部 IP 之後的出站路徑、Organization Policy、階層式防火牆、稽核紀錄與 Security Command Center 的 OPEN_SSH_PORT finding 在 GCP 上的處理">GCP VM 的 SSH 與 RDP 管理通道</a>的三項設定：<a href="/blog/devops/infra/knowledge-cards/identity-aware-proxy/" data-link-title="Identity-Aware Proxy（身分感知代理）" data-link-desc="要讓管理員或使用者不經公開入口連到內部主機與服務、評估把存取控制從來源 IP 改成帳號身分時，查身分感知代理的連線路徑、前提與代價">IAP</a>（Identity-Aware Proxy，GCP 依 IAM 權限轉送 SSH 連線的代理）、<a href="/blog/devops/infra/knowledge-cards/os-login/" data-link-title="OS Login" data-link-desc="GCP 專案要讓 SSH 登入權限跟著 Google 帳號與 IAM 走、啟用 OS Login 之後管理員或 CI 連不進 VM、或盤點專案 metadata 裡殘留的 SSH 公鑰時，查 OS Login 的運作方式、IAM role 與它和 metadata 金鑰的差別">OS Login</a>（讓 VM 的登入帳號與公鑰由 Google 帳號與 IAM 決定）與移除外部 IP：這些設定為什麼要寫在 template 與 node pool、怎麼判斷一台 VM 屬不屬於 MIG、MIG 的幾種救援各自適用的情形、GKE 的兩個存取面、<code>kubectl debug node</code> 的權限邊界，以及 GKE 另外要管的 <a href="/blog/backend/knowledge-cards/control-plane/" data-link-title="Control Plane" data-link-desc="負責下發策略、配置與路由決策的控制層">控制平面（control plane）</a>存取。建新 template 與 canary 滾動更新、重建與主機金鑰的比對、除錯 namespace、node pool 設定與控制平面端點切換的逐步指令與實跑結果，在 <a href="/blog/devops/infra/cloud-providers/gcp/hands-on/mig-gke-access-walkthrough/" data-link-title="GCP 上 MIG 與 GKE 管理存取的實測紀錄：instance template 與 canary 滾動更新、重建與主機金鑰、kubectl debug 的 namespace、node pool 設定與控制平面端點的切換" data-link-desc="要把 IAP 的 tag、OS Login 與移除外部 IP 套到 MIG 或 GKE 上，想照一次實跑過的順序做、看每一步換一種做法的結果時讀；也用在 MIG 重建之後 SSH 出現 REMOTE HOST IDENTIFICATION HAS CHANGED、kubectl debug 停在等待，或關掉 GKE 控制平面的 IP 端點之前要確認每個人都改連的時候">GCP 上 MIG 與 GKE 管理存取的實測紀錄</a>。管理通道拆成網路可達性、通道授權與主機登入驗證三層的模型見<a href="/blog/devops/infra/03-network-foundation/management-access-channel/" data-link-title="伺服器管理通道的設計：網路可達性、通道授權、主機登入驗證三層的做法與選擇，以及救援入口" data-link-desc="與雲端廠商無關、適用於雲端 VM、地端主機與 VPS 的管理員登入通道設計：拆成網路可達性、通道授權與主機登入驗證的模型，各層的做法（來源白名單、跳板機、VPN、身分感知代理、代理程式反向連線、SSH 憑證）、更換 port 與 fail2ban 改不了的部分，依存取者身分、連線送達方式、收回方式選擇組合的依據，以及移除公開 IP 之後的出站路徑、切換順序、救援入口、組織護欄與稽核">伺服器管理通道的設計</a>。</p>
<h2 id="autohealing滾動更新與擴增後消失的逐台設定network-tagos-login-metadata-與外部-ip">autohealing、滾動更新與擴增後消失的逐台設定：network tag、OS Login metadata 與外部 IP</h2>
<p>MIG 裡的每一台 VM 都由 instance template 建出來。有幾種時機 MIG 會依 template 重建或替換一台 VM，原本開機磁碟上的修改都不會留下：自動修復（autohealing，<a href="/blog/backend/knowledge-cards/health-check/" data-link-title="Health Check" data-link-desc="說明服務如何對外提供可供平台判斷狀態的健康回應">健康檢查</a>失敗時重建）、<a href="/blog/backend/knowledge-cards/rolling-update/" data-link-title="Rolling Update" data-link-desc="說明逐批替換服務版本的發版策略與風險控制">滾動更新</a>（rolling update，換成新版 template），以及 GKE 的節點升級與節點自動修復；<a href="/blog/backend/knowledge-cards/autoscaling/" data-link-title="Autoscaling" data-link-desc="說明系統如何依負載自動調整服務實例數量">autoscaler</a> 擴增台數時則直接依 template 新建。新建的 VM 只帶著 template 裡的設定，所以單台 VM 上做的修改——<code>gcloud compute instances add-tags</code> 加的 <a href="/blog/devops/infra/knowledge-cards/network-tag/" data-link-title="Network Tag（GCP 網路標籤）" data-link-desc="GCP 防火牆規則套不到某台 VM、新建的 VM 連不上，或要決定防火牆規則用 tag 還是服務帳號選 VM 時，查 network tag 怎麼讓防火牆規則選出要套用的 VM">network tag</a>（貼在 VM 上的標籤，防火牆規則用它選出要套用的 VM）、<code>add-metadata</code> 設的 <code>enable-oslogin</code>、登入之後 <code>useradd</code> 建的救援帳號、<code>delete-access-config</code> 移除的外部 IP——都只活到那台 VM 被重建為止，新加進來的 VM 則從一開始就沒有。</p>
<p>這種消失沒有任何通知。重建通常發生在半夜的自動修復或下一次擴增，而只放行 IAP 來源的防火牆規則靠 network tag 選 VM：新建的 VM 沒有那個 tag，這條規則就套不到它。收斂管理通道時通常也已經刪掉 <code>default-allow-ssh</code>（GCP 預設網路裡放行任何來源連 22 port 的規則），兩者加在一起，新 VM 上沒有任何一條規則放行 SSH，它就從管理通道上消失了。所以這兩種環境的做法是：<strong>管理通道的設定寫在建立 VM 的那一份設定裡</strong>（MIG 的 instance template、GKE 的 node pool 設定、或映像檔），逐台的 <code>gcloud compute instances</code> 指令只用在臨時查看。</p>
<p>instance template 建立之後不能修改，改設定的方式是建一份新的 template，再讓 MIG 滾動更新到新版；<code>instance-templates create</code> 從預設值開始建、不沿用舊 template 的任何設定，所以新 template 要把現行設定逐項帶上，漏掉的欄位會回到預設值。新版先讓一台 VM 換上（<a href="/blog/backend/knowledge-cards/canary-release/" data-link-title="Canary Release" data-link-desc="分批把流量導向新版本、用 stop condition 控制 blast radius 的部署策略">canary</a>），驗證通過再讓整組更新，template 寫錯時才只壞一台；建新 template 與 canary 的指令在 <a href="/blog/devops/infra/cloud-providers/gcp/hands-on/mig-gke-access-walkthrough/" data-link-title="GCP 上 MIG 與 GKE 管理存取的實測紀錄：instance template 與 canary 滾動更新、重建與主機金鑰、kubectl debug 的 namespace、node pool 設定與控制平面端點的切換" data-link-desc="要把 IAP 的 tag、OS Login 與移除外部 IP 套到 MIG 或 GKE 上，想照一次實跑過的順序做、看每一步換一種做法的結果時讀；也用在 MIG 重建之後 SSH 出現 REMOTE HOST IDENTIFICATION HAS CHANGED、kubectl debug 停在等待，或關掉 GKE 控制平面的 IP 端點之前要確認每個人都改連的時候">GCP 上 MIG 與 GKE 管理存取的實測紀錄</a> 的〈建新的 instance template：現行設定照抄，再加 tag、OS Login 與不配外部 IP〉與〈先讓一台 VM 換成新 template（canary），再讓整組更新〉。MIG 與 GKE 多半由 Terraform 這類 <a href="/blog/devops/infra/knowledge-cards/iac/" data-link-title="Infrastructure as Code (IaC)" data-link-desc="用程式碼描述基礎設施的最終狀態，由工具負責收斂現實與描述的差異">IaC</a> 工具管理，這時 template 與 node pool 的修改都改在程式碼裡：在 CLI 上建的 template、改的 node pool 設定，下一次 apply 會依程式碼改回去，這種程式碼與實際狀態的落差叫 <a href="/blog/devops/infra/knowledge-cards/drift/" data-link-title="Drift（設定漂移）" data-link-desc="IaC 的 state 與雲端實際狀態之間的不一致，通常因為有人繞過 IaC 直接在 Console 改設定">drift</a>，過渡期的做法見<a href="/blog/devops/infra/takeover/partial-iac-dual-truth-operation/" data-link-title="兩套真相並存的過渡期操作" data-link-desc="部分資源在 IaC、部分在手動時，怎麼安全操作避免比全手動更危險，以及怎麼縮短這個過渡期">兩套真相並存的過渡期操作</a>。</p>
<p>要判斷一台 VM 屬不屬於 MIG，看它的 metadata（掛在 VM 上的鍵值設定，VM 內的程式與有讀取權限的帳號都讀得到）有沒有 <code>created-by</code> 這個鍵（值是那個 MIG 的資源路徑）。這個鍵由 MIG 在建立 VM 時寫入，VM 被移出 MIG（abandon）之後仍然留著，所以它標出的是建立這台 VM 的 MIG；要確認 VM 現在是否仍是那個 MIG 的成員，用 <code>gcloud compute instance-groups managed list-instances &lt;mig-name&gt; --zone=&lt;zone&gt;</code> 列出 MIG 目前的 VM 來對照。GKE 節點帶著 <code>goog-gke-node</code> 標籤。下面的指令用 <code>jq</code>（命令列的 JSON 處理工具）從 <code>gcloud</code> 的 JSON 輸出取出這個鍵，要先在本機裝好；<code>gcloud</code> 讀的是目前設定的專案，要查別的專案時加 <code>--project=&lt;project-id&gt;</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 列出 VM 與它所屬的 MIG（第二欄是 - 的就是單獨建立的 VM）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">gcloud compute instances list --format<span class="o">=</span>json <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  <span class="p">|</span> jq -r <span class="s1">&#39;.[] | [.name, (first((.metadata.items // [])[] | select(.key==&#34;created-by&#34;) | .value) // &#34;-&#34;)] | @tsv&#39;</span></span></span></code></pre></div><p>GKE 的節點同樣是 MIG 的成員，這條指令會把它們列成屬於某個名稱是 <code>gke-...-grp</code> 的 MIG。這類 MIG 由 GKE 管理：對它們建 template、滾動更新或 <code>recreate-instances</code>，改動會被 GKE 覆蓋或讓叢集狀態不一致，<code>recreate-instances</code> 也不會先把節點上的 Pod 移走。帶著 <code>goog-gke-node</code> 標籤的 VM 改走本篇 GKE 的幾節：設定改在 node pool，單一節點壞掉時用 <code>kubectl drain</code> 移走 Pod，再交給 GKE 的節點自動修復。</p>
<h2 id="mig-的救援重建退回舊版-template-與-serial-console-各自適用的情形">MIG 的救援：重建、退回舊版 template 與 Serial Console 各自適用的情形</h2>
<p>單台 VM 的救援入口是 Serial Console 加一個事先建好的本機救援帳號（<a href="/blog/devops/infra/knowledge-cards/serial-console/" data-link-title="Serial Console（序列主控台）" data-link-desc="SSH 因為防火牆、sshd 設定或磁碟滿而連不進主機，或要規劃管理通道斷掉時的備用路徑時，查序列主控台與頻外管理介面的用法與它本身的暴露面">Serial Console（序列主控台）</a>）。MIG 裡的 VM 多了兩條路，各自適用的情形不同：</p>
<ul>
<li><strong>依 template 重建那一台</strong>（<code>recreate-instances</code>）：一台 VM 連不上、而原因是那台 VM 自己壞了（sshd 設定改壞、磁碟滿）時，讓 MIG 依 template 重建它，比從 Serial Console 進去修快，也不必替每台 VM 維護救援密碼。重建的前提是 VM 上沒有只存在那一台的狀態：資料寫在外接的資料庫、物件儲存或另外掛載的磁碟上，VM 本身隨時可以丟。VM 上有不能丟的狀態時，MIG 的 stateful 設定把指定的磁碟、IP 與 metadata 標為要保留；狀態多到每台都不一樣時，它更適合當單獨的 VM 管理。</li>
<li><strong>退回上一版 template</strong>：重建修不好的是 template 本身的錯（tag 寫錯、啟動腳本有錯），重建出來的每一台都一樣壞，這種錯要整組退回上一版，所以舊版 template 留到新版驗證完之後再刪。</li>
<li><strong>Serial Console</strong>：需要登入一台壞掉的 VM 查原因、保留現場做事後分析時仍然可用。救援帳號的帳號與密碼雜湊要在建立<a href="/blog/backend/knowledge-cards/immutable-image/" data-link-title="Immutable Image（不可變映像）" data-link-desc="想「修改 / 升級一個已經 build 好的 image」、或搞不懂為什麼同一個 tag 隔天內容變了時回來讀 — image 是不可變的 build 產物、改了就是新 image">映像檔</a>時寫進去（用 Packer 這類建映像檔的工具，或在一台 VM 上建好帳號再把磁碟做成自訂映像檔），而不是寫進 template 的 startup-script，因為 startup-script 存在 metadata 裡，任何有讀取 metadata 權限的人都看得到，把密碼寫在那裡等於公開。寫進映像檔的代價是整組 VM 共用同一組密碼雜湊，要輪替就要重做映像檔、再滾動更新一次。</li>
</ul>
<p>重建之後要知道的一件事是主機金鑰換了：重建在原地換掉開機磁碟，VM 沿用原本的名稱與執行個體 ID，而 <code>gcloud compute ssh</code> 在本機用 <code>compute.&lt;instance-id&gt;</code> 記錄主機金鑰，所以第一次連線會出現 <code>WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!</code> 並拒絕連線。這個警告也是中間人攻擊時會看到的訊息，處理方式是先用不經過這條 SSH 連線的來源（guest attributes 回報的<a href="/blog/devops/infra/knowledge-cards/ssh-host-key/" data-link-title="SSH Host Key 與 known_hosts（主機金鑰）" data-link-desc="ssh 第一次連線問要不要信任指紋、出現 REMOTE HOST IDENTIFICATION HAS CHANGED 警告，或主機重建之後連不上時，查 host key 怎麼證明主機身分、known_hosts 記了什麼，以及首次信任的風險">主機金鑰</a>；guest attributes 是 VM 寫回 metadata server、可以經由 GCP API 讀取的鍵值，要事先設 <code>enable-guest-attributes=TRUE</code>）確認新的金鑰確實來自這台 VM，再移除舊紀錄，不關掉主機金鑰檢查。重建與退回的指令、重建做完的確認方式與主機金鑰的比對，在 <a href="/blog/devops/infra/cloud-providers/gcp/hands-on/mig-gke-access-walkthrough/" data-link-title="GCP 上 MIG 與 GKE 管理存取的實測紀錄：instance template 與 canary 滾動更新、重建與主機金鑰、kubectl debug 的 namespace、node pool 設定與控制平面端點的切換" data-link-desc="要把 IAP 的 tag、OS Login 與移除外部 IP 套到 MIG 或 GKE 上，想照一次實跑過的順序做、看每一步換一種做法的結果時讀；也用在 MIG 重建之後 SSH 出現 REMOTE HOST IDENTIFICATION HAS CHANGED、kubectl debug 停在等待，或關掉 GKE 控制平面的 IP 端點之前要確認每個人都改連的時候">GCP 上 MIG 與 GKE 管理存取的實測紀錄</a> 的〈重建一台壞掉的 VM：recreate-instances 與重建之後的主機金鑰〉與〈template 本身寫錯時退回上一版〉；Serial Console 的開啟與救援帳號的登入，在 <a href="/blog/devops/infra/cloud-providers/gcp/hands-on/iap-ssh-migration-walkthrough/" data-link-title="GCP VM 的 SSH 與 RDP 改走 IAP 的實測紀錄：盤點、救援入口、IAP 規則與 IAM、停用舊規則驗證、OS Login 啟用與移除外部 IP" data-link-desc="要把 GCP 專案裡對 0.0.0.0/0 開放 22 與 3389 port 的預設規則換成只放行 IAP，想照一次實跑過的順序做、看每一步沒做或換一種做法的結果時讀；也用在停用舊規則之後連不上、OS Login 啟用後有人登不進去，或拿掉外部 IP 之後要確認出站走了 Cloud NAT 的時候">GCP VM 的 SSH 與 RDP 改走 IAP 的實測紀錄</a> 的〈準備救援入口：Serial Console 與本機救援帳號〉。</p>
<h2 id="gke-的存取面控制平面與節點">GKE 的存取面：控制平面與節點</h2>
<p>GKE 叢集有兩個不同的存取面，設計時要分開處理：</p>
<ul>
<li><strong>控制平面</strong>：Kubernetes 的 API server，<code>kubectl</code> 連的就是它。日常的部署、查 log、進 <a href="/blog/backend/knowledge-cards/pod/" data-link-title="Pod" data-link-desc="在 Kubernetes 的輸出或設定裡看到 Pod，要知道它和容器的關係、為什麼它的 IP 與名稱會變、以及誰負責替補它時">Pod</a> 執行指令都經過它，這是 GKE 上的主要管理通道。</li>
<li><strong>節點（node）</strong>：跑工作負載的 VM。節點的作業系統要登入的情形很少：GKE 管理節點的映像檔、升級與修復，節點本身也是隨時可以丟的。</li>
</ul>
<p>多數團隊在 GKE 上不需要節點的 SSH：查 Pod 的狀況用 <code>kubectl logs</code>、<code>kubectl exec</code>，查節點的狀況先看 Cloud Logging 裡的節點紀錄，需要看到節點作業系統時用 <code>kubectl debug node</code>。所以在 GKE 上收斂管理通道時，工作集中在控制平面的存取，節點的 SSH 盡量關掉。</p>
<p>GKE 有兩種叢集模式：Standard 由使用者管理 node pool 與節點設定；Autopilot 由 GKE 管理節點，使用者只管工作負載。〈kubectl debug node 的權限邊界：RBAC 與 Pod Security Admission〉裡建專用 namespace 與節點 SSH 的做法只適用於 Standard。</p>
<h2 id="kubectl-debug-node-的權限邊界rbac-與-pod-security-admission">kubectl debug node 的權限邊界：RBAC 與 Pod Security Admission</h2>
<p><code>kubectl debug node/&lt;node-name&gt;</code> 在指定的節點上建一個 Pod，把節點的根目錄掛在 Pod 的 <code>/host</code>，讓管理員經由 Kubernetes API 看到節點的作業系統，不需要節點開 SSH；進到 Pod 之後 <code>chroot /host</code>（把行程的根目錄換成 <code>/host</code>），之後執行的指令看到的是節點的檔案系統。不加 <code>--profile</code> 時，較新的 kubectl 用預設的 <code>general</code> profile（舊版的預設是 <code>legacy</code>），除錯 Pod 已經以 root 執行、共用節點的行程、網路與 IPC 的 Linux namespace，讀寫節點的檔案、用 <code>crictl</code> 列出節點上的容器都做得到；<code>--profile=sysadmin</code> 以<a href="/blog/backend/knowledge-cards/privileged-container/" data-link-title="Privileged Container（特權容器）" data-link-desc="在 Pod 設定裡看到 securityContext.privileged=true、要判斷某個除錯或系統元件為什麼需要它，或評估誰能建這種 Pod 等於持有節點 root 權限時，查特權容器能做什麼與它的權限邊界">特權容器（privileged container）</a>執行，多給的是需要額外 Linux capability（核心把 root 權限拆成的個別權限項）的操作。</p>
<p>這個做法的權限邊界有兩道。<a href="/blog/backend/knowledge-cards/kubernetes-rbac/" data-link-title="Kubernetes RBAC（角色型存取控制）" data-link-desc="要決定誰能對叢集裡的哪些物件做什麼、或查某個帳號為什麼被拒絕或為什麼權限過大時，查 Kubernetes RBAC 的 Role、ClusterRole、binding 與動詞資源的組成">Kubernetes RBAC</a> 決定誰能用它：在除錯 Pod 所在的 <a href="/blog/backend/knowledge-cards/namespace/" data-link-title="Namespace（命名空間）" data-link-desc="看到 kubectl 的 -n、kube-system、default，或跨 namespace 的 Service 名稱連不到時：namespace 劃分物件名稱的範圍、預設用哪一個、對 DNS 名稱與權限的影響">namespace</a> 要有 <code>pods</code> 的 <code>create</code>（建 Pod）、<code>list</code> 與 <code>watch</code>（kubectl 用 watch 等 Pod 進入執行狀態）、<code>delete</code>（用完刪掉），以及 <code>pods/attach</code> 的 <code>create</code>（<code>-it</code> 在 Pod 建好之後附加到它的終端機），這幾項綁在那個 namespace 的 RoleBinding；另外要有 <code>nodes</code> 的 <code>get</code>，節點不屬於任何 namespace，這一項要用 ClusterRoleBinding 授予。少了 <code>list</code> 或 <code>watch</code> 時 Pod 已經建好，kubectl 卻停在等待；少了 <code>pods/attach</code> 時 Pod 建得出來卻進不去；兩種情形都會留下一個在跑的除錯 Pod。</p>
<p>能在節點上建除錯 Pod 的帳號，用預設的 <code>general</code> profile 就能以 root 讀寫節點的檔案，所以這組權限只給負責維運的群組。<a href="/blog/backend/knowledge-cards/pod-security-admission/" data-link-title="Pod Security Admission（Pod 安全准入）" data-link-desc="Pod 因為安全設定被拒絕建立、要限制某個 namespace 不能跑 privileged 或掛載主機目錄的 Pod，或評估 kubectl debug 這類需要高權限的工具在叢集裡能不能用時，查 Pod Security Admission 的三個等級與 namespace label">Pod Security Admission</a> 決定那個 namespace 是否接受這類 Pod：<code>kubectl debug node</code> 用 <code>general</code> 或 <code>sysadmin</code> profile 建的 Pod 帶著主機的網路、行程與 IPC 的 Linux namespace（hostNetwork、hostPID、hostIPC）與主機目錄掛載（hostPath），namespace 設了 <code>pod-security.kubernetes.io/enforce=baseline</code> 或 <code>restricted</code> 時，這幾項都會讓除錯 Pod 被拒絕；用 <code>--profile=baseline</code> 或 <code>restricted</code> 建的除錯 Pod 不帶這些設定，建得進這類 namespace，但看不到節點的檔案。反過來，沒有設這個 label 的 namespace（包括 <code>default</code>）在 GKE Standard 上預設接受特權 Pod，所以任何能在 <code>default</code> 建 Pod 的帳號，現在就能用這個方法以 root 操作節點。做法是把一般工作負載的 namespace 都設成 <code>baseline</code> 以上，節點除錯集中到一個刻意保留為 <code>privileged</code> 等級、而且只有維運群組能建 Pod 的 namespace，由叢集管理員事先建好；建法與使用的指令在 <a href="/blog/devops/infra/cloud-providers/gcp/hands-on/mig-gke-access-walkthrough/" data-link-title="GCP 上 MIG 與 GKE 管理存取的實測紀錄：instance template 與 canary 滾動更新、重建與主機金鑰、kubectl debug 的 namespace、node pool 設定與控制平面端點的切換" data-link-desc="要把 IAP 的 tag、OS Login 與移除外部 IP 套到 MIG 或 GKE 上，想照一次實跑過的順序做、看每一步換一種做法的結果時讀；也用在 MIG 重建之後 SSH 出現 REMOTE HOST IDENTIFICATION HAS CHANGED、kubectl debug 停在等待，或關掉 GKE 控制平面的 IP 端點之前要確認每個人都改連的時候">GCP 上 MIG 與 GKE 管理存取的實測紀錄</a> 的〈GKE 節點除錯：建專用的 namespace，用 kubectl debug node 查看節點〉。GKE Autopilot 預設拒絕特權 Pod（只有列入允許清單的合作夥伴工作負載例外），也不提供節點 SSH，查節點層的狀況靠 Cloud Logging 裡的節點紀錄與 GKE 主控台的節點狀態。</p>
<p>節點仍要 SSH 時（GKE Standard），單台 VM 的三項設定搬到 node pool 上：節點不配外部 IP 的 private nodes（先要有 Cloud NAT，換成內部 IP 之後節點的出站都經過它）、node pool 的 network tag（<code>node-pools update --tags</code> 整組取代使用者設定的 tag，GKE 自建的 tag 不受影響），以及 OS Login。Autopilot 叢集不能用 OS Login，因為節點由 GKE 管理。專案層級的 <code>enable-oslogin=TRUE</code> 是否套用到 GKE Standard 的節點，GKE 自己的兩份文件寫法相反：OS Login 的設定頁說專案層的設定套用到專案裡所有既有與新建的 VM，node pool 的管理頁則寫 OS Login 在 GKE 節點上被明確停用，所以啟用之後經由 IAP 實際登入一個節點，以登入後的帳號判斷。三項設定的指令在 <a href="/blog/devops/infra/cloud-providers/gcp/hands-on/mig-gke-access-walkthrough/" data-link-title="GCP 上 MIG 與 GKE 管理存取的實測紀錄：instance template 與 canary 滾動更新、重建與主機金鑰、kubectl debug 的 namespace、node pool 設定與控制平面端點的切換" data-link-desc="要把 IAP 的 tag、OS Login 與移除外部 IP 套到 MIG 或 GKE 上，想照一次實跑過的順序做、看每一步換一種做法的結果時讀；也用在 MIG 重建之後 SSH 出現 REMOTE HOST IDENTIFICATION HAS CHANGED、kubectl debug 停在等待，或關掉 GKE 控制平面的 IP 端點之前要確認每個人都改連的時候">GCP 上 MIG 與 GKE 管理存取的實測紀錄</a> 的〈node pool：private nodes、network tag 與 OS Login〉。</p>
<h2 id="gke-控制平面的存取authorized-networksdns-based-endpoint-與-private-endpoint">GKE 控制平面的存取：authorized networks、DNS-based endpoint 與 private endpoint</h2>
<p>控制平面的 API server 預設有一個公開的 IP 端點，任何地方都連得到，靠 Kubernetes 的認證（通道授權）與 <a href="/blog/backend/knowledge-cards/kubernetes-rbac/" data-link-title="Kubernetes RBAC（角色型存取控制）" data-link-desc="要決定誰能對叢集裡的哪些物件做什麼、或查某個帳號為什麼被拒絕或為什麼權限過大時，查 Kubernetes RBAC 的 Role、ClusterRole、binding 與動詞資源的組成">Kubernetes RBAC</a>（主機登入驗證那一層的對應）擋下沒有權限的請求——在管理通道的模型裡，這等於網路可達性全開，與 <code>0.0.0.0/0</code> 開 22 port 是同一種狀態。收窄網路可達性的做法有三種：</p>
<ul>
<li><strong>authorized networks</strong>：只允許列出的來源 IP 範圍連到 API server 的 IP 端點，相當於來源 IP 白名單，代價與白名單相同（遠端工作、IP 會變動時要一直改，而且分辨不出個人）。清單每次都整組取代，執行指令的人與 CI 的出口 IP 漏列時 <code>kubectl</code> 當場連不上，而 <code>gcloud</code> 不經過這個端點，連不上時仍可以改回。設了 authorized networks 之後，GCP 上其他客戶的 VM 的外部 IP 是否放行，由叢集的 Google Cloud 外部 IP 存取設定決定（<code>gcpPublicCidrsAccessEnabled</code>，要明確設定過一次才會出現在 <code>describe</code> 的輸出裡）。</li>
<li><strong>DNS-based endpoint</strong>：叢集多一個專屬的網域名稱端點，請求先經過 Google Cloud、以 IAM 判斷這個帳號能不能連到這個叢集（<code>container.clusters.connect</code> 權限，<code>roles/container.developer</code> 這類角色裡有），通過之後才轉到 API server。它相當於控制平面前面的身分感知代理：網路可達性與通道授權改由 IAM 承擔，不必維護 IP 清單，GitHub Actions 這類外部 CI 也能用 <a href="/blog/backend/knowledge-cards/workload-identity/" data-link-title="Workload Identity" data-link-desc="用於機器工作負載的身份語意與授權邊界">workload identity federation</a> 換到的身分連進來，不必另架<a href="/blog/devops/infra/knowledge-cards/bastion-host/" data-link-title="Bastion Host（跳板機）" data-link-desc="內部主機不想各自對外開管理 port、要設定 ProxyJump 經由跳板機連線、判斷 agent forwarding 的風險，或要評估自建跳板機與改用身分感知代理的取捨時，查跳板機的連線方式、暴露面與維運責任">跳板機</a>。啟用之後 IP 端點仍然開著，每個人與每條 CI 都改連網域名稱端點之後才關掉 IP 端點；兩步分開是因為關掉 IP 端點的那一刻，還在用它的人與 CI 當場連不上。</li>
<li><strong>private endpoint</strong>：控制平面只留內部 IP 端點（<code>--enable-private-endpoint</code>），只有 VPC 內與經 VPN 或 Interconnect 接進 VPC 的來源連得到，可以再加 <code>--enable-authorized-networks-on-private-endpoint</code> 收窄到 VPC 內的特定範圍。既有的 private cluster 多半是這種設定，從 VPC 外管理時要經過 VPN 或跳板機；改用 DNS-based endpoint 之後這一層可以拿掉。</li>
</ul>
<p>三種做法的指令、兩步切換的確認方式與實跑結果，在 <a href="/blog/devops/infra/cloud-providers/gcp/hands-on/mig-gke-access-walkthrough/" data-link-title="GCP 上 MIG 與 GKE 管理存取的實測紀錄：instance template 與 canary 滾動更新、重建與主機金鑰、kubectl debug 的 namespace、node pool 設定與控制平面端點的切換" data-link-desc="要把 IAP 的 tag、OS Login 與移除外部 IP 套到 MIG 或 GKE 上，想照一次實跑過的順序做、看每一步換一種做法的結果時讀；也用在 MIG 重建之後 SSH 出現 REMOTE HOST IDENTIFICATION HAS CHANGED、kubectl debug 停在等待，或關掉 GKE 控制平面的 IP 端點之前要確認每個人都改連的時候">GCP 上 MIG 與 GKE 管理存取的實測紀錄</a> 的〈控制平面：authorized networks，或改連 DNS-based endpoint 再關掉 IP 端點〉。</p>
<p>一個 <code>kubectl</code> 請求要通過兩次檢查。連進叢集的檢查是 IAM 的 <code>container.clusters.connect</code>：經 DNS-based endpoint 連線時缺了它，請求在進到 API server 之前就被 Google 的前端以 <code>403 Forbidden</code> 擋下。請求進到 API server 之後，API server 再判斷這個帳號是否有權限執行這個請求（例如列出節點），這項授權檢查在 GKE 上由 IAM 與 Kubernetes RBAC 共同決定，兩者任一個允許就放行：IAM 的 Kubernetes 物件權限（例如 <code>container.nodes.list</code>，<code>roles/container.developer</code> 這類角色裡有）或 RBAC 的 Role 與 RoleBinding。只拿到 <code>container.clusters.connect</code>（例如 <code>roles/container.clusterViewer</code>）的帳號連得進叢集，列出節點時會收到 <code>nodes is forbidden: User &quot;&lt;email&gt;&quot; cannot list resource &quot;nodes&quot;</code>，訊息後面寫著它要的 IAM 權限名稱。IAM 與 RBAC 的授權要一起設計，原則與 <a href="/blog/devops/infra/cloud-providers/gcp/gcp-iap-ssh-access/" data-link-title="GCP VM 的 SSH 與 RDP 管理通道：從預設的全開規則改走 IAP TCP forwarding" data-link-desc="GCP 預設網路自帶的防火牆規則與它們留下的暴露面、IAP TCP forwarding 的連線路徑與日常連線指令、切換到 IAP 的順序為什麼這樣排、IAP 通道連不上時的依層判讀，以及 IAM 授權範圍、OS Login 的啟用條件與工作階段的結束、拿掉外部 IP 之後的出站路徑、Organization Policy、階層式防火牆、稽核紀錄與 Security Command Center 的 OPEN_SSH_PORT finding 在 GCP 上的處理">GCP VM 的 SSH 與 RDP 管理通道</a>〈IAP 的 IAM 授權範圍：誰拿得到通道〉相同：綁給負責維運的群組，不綁給整個網域。</p>
<h2 id="延伸閱讀逐步的實作單台-vm-的做法管理通道模型與-iac-的落差">延伸閱讀：逐步的實作、單台 VM 的做法、管理通道模型與 IaC 的落差</h2>
<ul>
<li>→ <a href="/blog/devops/infra/cloud-providers/gcp/hands-on/mig-gke-access-walkthrough/" data-link-title="GCP 上 MIG 與 GKE 管理存取的實測紀錄：instance template 與 canary 滾動更新、重建與主機金鑰、kubectl debug 的 namespace、node pool 設定與控制平面端點的切換" data-link-desc="要把 IAP 的 tag、OS Login 與移除外部 IP 套到 MIG 或 GKE 上，想照一次實跑過的順序做、看每一步換一種做法的結果時讀；也用在 MIG 重建之後 SSH 出現 REMOTE HOST IDENTIFICATION HAS CHANGED、kubectl debug 停在等待，或關掉 GKE 控制平面的 IP 端點之前要確認每個人都改連的時候">GCP 上 MIG 與 GKE 管理存取的實測紀錄</a>：新 template 與 canary、重建與主機金鑰、退回 template、除錯 namespace、node pool 設定與控制平面端點切換的逐步指令、實跑結果，以及每一步換一種做法時的結果</li>
<li>→ <a href="/blog/devops/infra/cloud-providers/gcp/gcp-iap-ssh-access/" data-link-title="GCP VM 的 SSH 與 RDP 管理通道：從預設的全開規則改走 IAP TCP forwarding" data-link-desc="GCP 預設網路自帶的防火牆規則與它們留下的暴露面、IAP TCP forwarding 的連線路徑與日常連線指令、切換到 IAP 的順序為什麼這樣排、IAP 通道連不上時的依層判讀，以及 IAM 授權範圍、OS Login 的啟用條件與工作階段的結束、拿掉外部 IP 之後的出站路徑、Organization Policy、階層式防火牆、稽核紀錄與 Security Command Center 的 OPEN_SSH_PORT finding 在 GCP 上的處理">GCP VM 的 SSH 與 RDP 管理通道</a>：本篇沿用的 IAP 規則、IAM 權限、OS Login 與 Cloud NAT 在單台 VM 上的機制與切換順序</li>
<li>→ <a href="/blog/devops/infra/03-network-foundation/management-access-channel/" data-link-title="伺服器管理通道的設計：網路可達性、通道授權、主機登入驗證三層的做法與選擇，以及救援入口" data-link-desc="與雲端廠商無關、適用於雲端 VM、地端主機與 VPS 的管理員登入通道設計：拆成網路可達性、通道授權與主機登入驗證的模型，各層的做法（來源白名單、跳板機、VPN、身分感知代理、代理程式反向連線、SSH 憑證）、更換 port 與 fail2ban 改不了的部分，依存取者身分、連線送達方式、收回方式選擇組合的依據，以及移除公開 IP 之後的出站路徑、切換順序、救援入口、組織護欄與稽核">伺服器管理通道的設計</a>：網路可達性、通道授權、主機登入驗證三層的模型，以及不可變基礎設施下平時關閉通道的做法</li>
<li>→ <a href="/blog/devops/infra/takeover/partial-iac-dual-truth-operation/" data-link-title="兩套真相並存的過渡期操作" data-link-desc="部分資源在 IaC、部分在手動時，怎麼安全操作避免比全手動更危險，以及怎麼縮短這個過渡期">兩套真相並存的過渡期操作</a>：MIG 與 GKE 多半由 IaC 管理，在 CLI 上建的 template 與改的 node pool 設定要寫回程式碼</li>
</ul>
]]></content:encoded></item></channel></rss>