<?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>Ubuntu on Tarragon</title><link>https://tarrragon.github.io/blog/tags/ubuntu/</link><description>Recent content in Ubuntu on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 26 Oct 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/ubuntu/index.xml" rel="self" type="application/rss+xml"/><item><title>Oracle Cloud 私有子網路 Ubuntu 主機的實測紀錄：套件來源的出站（NAT gateway、路由與 NSG）、序列主控台的連線，以及被回收之後的重建</title><link>https://tarrragon.github.io/blog/devops/infra/cloud-providers/oracle-cloud/hands-on/private-ubuntu-egress-console-rebuild/</link><pubDate>Mon, 26 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/devops/infra/cloud-providers/oracle-cloud/hands-on/private-ubuntu-egress-console-rebuild/</guid><description>&lt;blockquote>
&lt;p>&lt;strong>事件說明&lt;/strong>：2026-10-26（本篇沒有標 UTC 的日期與時刻都是台北時間），在 Oracle Cloud 的 &lt;code>ap-batam-1&lt;/code> 區域，從一台 macOS 開發機用 OCI CLI 操作。tenancy 以 Free Tier 註冊，2026-10-27 確認它已經是付費帳戶（升級的日期這次沒有記錄）。主機是 A1 機型（&lt;code>VM.Standard.A1.Flex&lt;/code>，1 OCPU、6 GB），映像檔是 &lt;code>Canonical-Ubuntu-24.04-aarch64-2026.09.18-0&lt;/code>，放在既有的私有子網路裡、沒有公開 IP，經由 Bastion 的 port forwarding session 登入。當時在做的操作是：在雲端主機上確認 Ubuntu 的自動更新預設與核心更新前後的重開機判斷，並實際連一次序列主控台。2026-10-27 凌晨另外跑了重建腳本，並終止了重建腳本建出的 &lt;code>verify-ubuntu-rebuild&lt;/code>；接著把 &lt;code>verify-ubuntu&lt;/code> 停止、啟動，在三種狀態下各跑一次重建腳本，用 &lt;code>oci limits&lt;/code> 與 usage API 查區塊磁碟額度與帳務，凌晨最後照刪除順序收掉 &lt;code>verify-ubuntu&lt;/code> 的 NAT gateway 與 NSG。2026-10-27 上午又做了三件事：對 &lt;code>verify-ubuntu&lt;/code> 重跑重建腳本，讓它重建 NAT gateway 與 NSG；用重建腳本新建一台開機磁碟 60 GB 的 &lt;code>verify-ubuntu-60g&lt;/code>，建立期間對同一個名稱再跑一份腳本；以及在網頁主控台的 Limits, quotas and usage 頁讀區塊磁碟的用量；中午前用 usage API 查到那一台開機磁碟的計費項目之後，終止了 &lt;code>verify-ubuntu-60g&lt;/code>。2026-10-27 下午（13 時到 15 時）又做了五件事：不給 &lt;code>BOOT_GB&lt;/code> 用重建腳本新建 &lt;code>verify-ubuntu-nb&lt;/code>、設 compartment quota 試超額的建立、線上加大 &lt;code>verify-ubuntu&lt;/code> 的開機磁碟並另建一顆資料磁碟、終止 &lt;code>verify-ubuntu-nb&lt;/code> 之後觀察付費磁碟的額度用量與帳務明細怎麼變，最後照刪除順序把 &lt;code>verify-ubuntu&lt;/code> 與它的出站設定整組收掉。&lt;/p>&lt;/blockquote>
&lt;p>這篇記錄在 Oracle Cloud 的私有子網路裡建一台 Ubuntu 24.04 主機之後，讓它連得到套件來源、連上序列主控台，以及主機不見之後怎麼照原樣重建。各節依序是：&lt;/p>
&lt;ul>
&lt;li>前置條件：沿用 &lt;a href="https://tarrragon.github.io/blog/devops/infra/cloud-providers/oracle-cloud/hands-on/launch-instance-walkthrough/" data-link-title="Oracle Cloud 建立主機的實測紀錄：建立前的檢查、前置網路、主控台各分頁的 Always Free 設定值、等效的 CLI 指令、容量不足的重送、Bastion 工作階段逾時、Run Command 補公鑰的嘗試與重建後經由 port forwarding 登入" data-link-desc="要在 Oracle Cloud 建一台沒有公開 IP、經由 Bastion 登入的 Always Free A1 主機，想照一次實測重做、看建立頁每一欄的取值與換一種做法的結果時讀；也用在建立請求一直回容量不足、managed SSH session 建立逾時，或沒放公鑰的主機想用 Run Command 救援、Oracle Linux 10 的主機上找不到 Bastion plugin、想改用 port forwarding session 登入的時候">Oracle Cloud 建立主機的實測紀錄：建立前的檢查、前置網路、主控台各分頁的 Always Free 設定值、等效的 CLI 指令、容量不足的重送、Bastion 工作階段逾時、Run Command 補公鑰的嘗試與重建後經由 port forwarding 登入&lt;/a>（下文稱「建主機那篇」）建好的 compartment、VCN、私有子網路、service gateway、Bastion 與金鑰。&lt;/li>
&lt;li>建 Ubuntu 24.04 主機，經由 port forwarding session 登入。&lt;/li>
&lt;li>只接了 service gateway 的私有子網路裡，apt 連不到套件來源的錯誤原文與成因。&lt;/li>
&lt;li>加上 NAT gateway、路由規則與只掛在這台主機上的出站 NSG（network security group：套在個別網卡上的一組防火牆規則）之後，&lt;code>apt-get update&lt;/code> 成功。&lt;/li>
&lt;li>序列主控台：產生 RSA 金鑰、建立序列主控台連線（instance console connection）、把官方的連線字串改寫成 ssh 的 ProxyJump 設定，以及看到的開機輸出。&lt;/li>
&lt;li>主機被閒置回收、停止或刪除之後：怎麼發現、哪些資源會留下、主機與序列主控台連線的重建腳本（含用 &lt;code>BOOT_GB&lt;/code> 指定開機磁碟大小）、每一步的讀法、同一個名稱同時跑兩份腳本的結果與會建出第二台主機的空窗、重建之後要重做的事與耗時。&lt;/li>
&lt;li>費用與額度（A1 的免費量、開機磁碟從區塊磁碟額度扣掉多少與用量的查法、合計超過 200 GB 時放不進剩餘額度的那一顆磁碟整顆不算免費額度、用 compartment quota 擋下超額的建立與被擋下時的回應、線上加大開機磁碟與另建資料磁碟超過額度時的結果、空出額度之後付費磁碟自動轉成 Always Free 要等多久與帳務明細怎麼記、網頁主控台 Limits, quotas and usage 頁與 CLI 的對照、在價目表與帳務明細裡找 NAT gateway 收費項目的結果），以及刪除這組資源的順序與實際刪除時的錯誤訊息（NSG 還掛在網卡上時的 412，NAT gateway 還被路由表指著時外層 400、內含 409）與整組收掉的實跑。&lt;/li>
&lt;/ul>
&lt;p>主機上量到的自動更新預設、核心更新前後的 &lt;code>needrestart -b&lt;/code> 與 &lt;code>reboot-required&lt;/code>，寫在 &lt;a href="https://tarrragon.github.io/blog/devops/operations/09-host-lifecycle/long-running-host-maintenance/" data-link-title="長期上線主機的維護項目：會累積的資料、定期讀的 log、對外入口的盤點、安全更新、會到期的設定與檢查間隔的推算" data-link-desc="單台 Linux 主機（VPS、雲端主機）長期上線時，沒有人操作也會改變的狀態與維護對它們做的事：log 與資料的寫入者和清理機制（logrotate、journald、容器 log）、磁碟寫滿時各元件的反應、每份 log 要讀的欄位、入口在主機與雲端各層的盤點（ss）、各條更新管道的安全更新與重開機（dnf-automatic、unattended-upgrades、needs-restarting）、會到期與會改變的設定、檢查間隔的推算，以及接手一台已上線的主機時先處理哪幾項">長期上線主機的維護項目：會累積的資料、定期讀的 log、對外入口的盤點、安全更新、會到期的設定與檢查間隔的推算&lt;/a>（下文稱「維護篇」）〈會老化的軟體：各條更新管道與更新後的重開機〉，這裡只寫在 Oracle Cloud 上建立與連線的操作。本篇自己的 OCID 與 IP 一律是佔位值：OCID 用 &lt;code>oci ... list&lt;/code> 查，私有 IP 用 &lt;code>oci compute instance list-vnics&lt;/code> 查；錯誤原文裡的位址是 Canonical 鏡像站的。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p><strong>事件說明</strong>：2026-10-26（本篇沒有標 UTC 的日期與時刻都是台北時間），在 Oracle Cloud 的 <code>ap-batam-1</code> 區域，從一台 macOS 開發機用 OCI CLI 操作。tenancy 以 Free Tier 註冊，2026-10-27 確認它已經是付費帳戶（升級的日期這次沒有記錄）。主機是 A1 機型（<code>VM.Standard.A1.Flex</code>，1 OCPU、6 GB），映像檔是 <code>Canonical-Ubuntu-24.04-aarch64-2026.09.18-0</code>，放在既有的私有子網路裡、沒有公開 IP，經由 Bastion 的 port forwarding session 登入。當時在做的操作是：在雲端主機上確認 Ubuntu 的自動更新預設與核心更新前後的重開機判斷，並實際連一次序列主控台。2026-10-27 凌晨另外跑了重建腳本，並終止了重建腳本建出的 <code>verify-ubuntu-rebuild</code>；接著把 <code>verify-ubuntu</code> 停止、啟動，在三種狀態下各跑一次重建腳本，用 <code>oci limits</code> 與 usage API 查區塊磁碟額度與帳務，凌晨最後照刪除順序收掉 <code>verify-ubuntu</code> 的 NAT gateway 與 NSG。2026-10-27 上午又做了三件事：對 <code>verify-ubuntu</code> 重跑重建腳本，讓它重建 NAT gateway 與 NSG；用重建腳本新建一台開機磁碟 60 GB 的 <code>verify-ubuntu-60g</code>，建立期間對同一個名稱再跑一份腳本；以及在網頁主控台的 Limits, quotas and usage 頁讀區塊磁碟的用量；中午前用 usage API 查到那一台開機磁碟的計費項目之後，終止了 <code>verify-ubuntu-60g</code>。2026-10-27 下午（13 時到 15 時）又做了五件事：不給 <code>BOOT_GB</code> 用重建腳本新建 <code>verify-ubuntu-nb</code>、設 compartment quota 試超額的建立、線上加大 <code>verify-ubuntu</code> 的開機磁碟並另建一顆資料磁碟、終止 <code>verify-ubuntu-nb</code> 之後觀察付費磁碟的額度用量與帳務明細怎麼變，最後照刪除順序把 <code>verify-ubuntu</code> 與它的出站設定整組收掉。</p></blockquote>
<p>這篇記錄在 Oracle Cloud 的私有子網路裡建一台 Ubuntu 24.04 主機之後，讓它連得到套件來源、連上序列主控台，以及主機不見之後怎麼照原樣重建。各節依序是：</p>
<ul>
<li>前置條件：沿用 <a href="/blog/devops/infra/cloud-providers/oracle-cloud/hands-on/launch-instance-walkthrough/" data-link-title="Oracle Cloud 建立主機的實測紀錄：建立前的檢查、前置網路、主控台各分頁的 Always Free 設定值、等效的 CLI 指令、容量不足的重送、Bastion 工作階段逾時、Run Command 補公鑰的嘗試與重建後經由 port forwarding 登入" data-link-desc="要在 Oracle Cloud 建一台沒有公開 IP、經由 Bastion 登入的 Always Free A1 主機，想照一次實測重做、看建立頁每一欄的取值與換一種做法的結果時讀；也用在建立請求一直回容量不足、managed SSH session 建立逾時，或沒放公鑰的主機想用 Run Command 救援、Oracle Linux 10 的主機上找不到 Bastion plugin、想改用 port forwarding session 登入的時候">Oracle Cloud 建立主機的實測紀錄：建立前的檢查、前置網路、主控台各分頁的 Always Free 設定值、等效的 CLI 指令、容量不足的重送、Bastion 工作階段逾時、Run Command 補公鑰的嘗試與重建後經由 port forwarding 登入</a>（下文稱「建主機那篇」）建好的 compartment、VCN、私有子網路、service gateway、Bastion 與金鑰。</li>
<li>建 Ubuntu 24.04 主機，經由 port forwarding session 登入。</li>
<li>只接了 service gateway 的私有子網路裡，apt 連不到套件來源的錯誤原文與成因。</li>
<li>加上 NAT gateway、路由規則與只掛在這台主機上的出站 NSG（network security group：套在個別網卡上的一組防火牆規則）之後，<code>apt-get update</code> 成功。</li>
<li>序列主控台：產生 RSA 金鑰、建立序列主控台連線（instance console connection）、把官方的連線字串改寫成 ssh 的 ProxyJump 設定，以及看到的開機輸出。</li>
<li>主機被閒置回收、停止或刪除之後：怎麼發現、哪些資源會留下、主機與序列主控台連線的重建腳本（含用 <code>BOOT_GB</code> 指定開機磁碟大小）、每一步的讀法、同一個名稱同時跑兩份腳本的結果與會建出第二台主機的空窗、重建之後要重做的事與耗時。</li>
<li>費用與額度（A1 的免費量、開機磁碟從區塊磁碟額度扣掉多少與用量的查法、合計超過 200 GB 時放不進剩餘額度的那一顆磁碟整顆不算免費額度、用 compartment quota 擋下超額的建立與被擋下時的回應、線上加大開機磁碟與另建資料磁碟超過額度時的結果、空出額度之後付費磁碟自動轉成 Always Free 要等多久與帳務明細怎麼記、網頁主控台 Limits, quotas and usage 頁與 CLI 的對照、在價目表與帳務明細裡找 NAT gateway 收費項目的結果），以及刪除這組資源的順序與實際刪除時的錯誤訊息（NSG 還掛在網卡上時的 412，NAT gateway 還被路由表指著時外層 400、內含 409）與整組收掉的實跑。</li>
</ul>
<p>主機上量到的自動更新預設、核心更新前後的 <code>needrestart -b</code> 與 <code>reboot-required</code>，寫在 <a href="/blog/devops/operations/09-host-lifecycle/long-running-host-maintenance/" data-link-title="長期上線主機的維護項目：會累積的資料、定期讀的 log、對外入口的盤點、安全更新、會到期的設定與檢查間隔的推算" data-link-desc="單台 Linux 主機（VPS、雲端主機）長期上線時，沒有人操作也會改變的狀態與維護對它們做的事：log 與資料的寫入者和清理機制（logrotate、journald、容器 log）、磁碟寫滿時各元件的反應、每份 log 要讀的欄位、入口在主機與雲端各層的盤點（ss）、各條更新管道的安全更新與重開機（dnf-automatic、unattended-upgrades、needs-restarting）、會到期與會改變的設定、檢查間隔的推算，以及接手一台已上線的主機時先處理哪幾項">長期上線主機的維護項目：會累積的資料、定期讀的 log、對外入口的盤點、安全更新、會到期的設定與檢查間隔的推算</a>（下文稱「維護篇」）〈會老化的軟體：各條更新管道與更新後的重開機〉，這裡只寫在 Oracle Cloud 上建立與連線的操作。本篇自己的 OCID 與 IP 一律是佔位值：OCID 用 <code>oci ... list</code> 查，私有 IP 用 <code>oci compute instance list-vnics</code> 查；錯誤原文裡的位址是 Canonical 鏡像站的。</p>
<h2 id="前置條件沿用-oracle-cloud-建立主機實測紀錄的網路bastion-與金鑰">前置條件：沿用 Oracle Cloud 建立主機實測紀錄的網路、Bastion 與金鑰</h2>
<p>這次沿用建主機那篇〈進建立頁之前：compartment、VCN、閘道與 private subnet〉建好的資源，沒有另建網路：</p>
<ul>
<li><strong>compartment 與 VCN</strong>：放實驗資源的 compartment，以及裡面的 VCN。</li>
<li><strong>私有子網路</strong>：這台主機要放的子網路。它的路由表一開始只有一條規則，目的地是 Oracle 服務網路，出口是 service gateway；security list 的出站規則也只放行 Oracle 服務網路與子網路內部。路由規則裡的目的地是一個名稱而不是位址範圍，這個名稱依區域而不同（這次的值是 <code>all-hsg-services-in-oracle-services-network</code>），用 <code>oci network service list</code> 查，名稱帶「All」的那一筆的 <code>cidr-block</code> 欄就是它。</li>
<li><strong>Bastion</strong>：目標子網路是上面那個私有子網路，client CIDR 是開發機對外的 IP（/32）。開發機的對外 IP 換了之後，來源不在 client CIDR 裡，經由 Bastion 的 ssh 連線建立不起來，要先改 client CIDR（從 client CIDR 的作用推論；這次開發機的 IP 沒有變過，沒有看到實際的錯誤訊息）。</li>
<li><strong>兩把金鑰</strong>：登入主機用的金鑰（建立主機時放進去，重建腳本裡的檔名是 <code>verify-a1-key</code>），以及建 Bastion session 用的金鑰（檔名 <code>session_key</code>）。兩把都是建主機那篇建的：session 用的那一把照〈建立 Bastion 與 managed SSH session〉用 <code>ssh-keygen</code> 另產，登入主機的那一把是〈建主機：Oracle Linux 映像加 &ndash;ssh-authorized-keys-file〉另產、跟 session 那一把分開的一組，兩把分開的理由寫在那一節。</li>
<li><strong>OCI CLI 的登入</strong>：session token 登入，有效期約 1 小時，到期時重跑 <code>oci session authenticate --region &lt;區域&gt; --profile-name DEFAULT</code>，做法見 <a href="/blog/devops/infra/cloud-providers/oracle-cloud/cli-session-login/" data-link-title="從本機用 OCI CLI 操作 Oracle Cloud：session token 登入、時效與 API 簽章金鑰" data-link-desc="OCI CLI 的兩種身分驗證方式——瀏覽器登入換來的 session token 與上傳公鑰的 API 簽章金鑰——各自把什麼存在本機、有效多久、外洩時的影響範圍，以及登入時的互動提示與錯誤訊息怎麼讀">從本機用 OCI CLI 操作 Oracle Cloud：session token 登入、時效與 API 簽章金鑰</a>。<code>&lt;區域&gt;</code> 填區域的識別碼（這次是 <code>ap-batam-1</code>），<code>oci iam region-subscription list</code> 列得出 tenancy 訂閱的區域（這一條沒有實跑）。</li>
</ul>
<p>下文的 CLI 指令裡，<code>&lt;compartment 的 OCID&gt;</code> 這類佔位值換成自己的。這次把重建腳本要用的值存在一個 <code>ids.env</code> 裡，腳本開頭 <code>source</code> 一次；每個鍵與查法如下（值是建立資源時從回傳記下的，查法的子命令只確認過 <code>--help</code> 存在，沒有逐一實跑）：</p>
<table>
  <thead>
      <tr>
          <th>鍵</th>
          <th>內容</th>
          <th>查法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>C</code></td>
          <td>compartment 的 OCID</td>
          <td><code>oci iam compartment list</code></td>
      </tr>
      <tr>
          <td><code>V</code></td>
          <td>VCN 的 OCID</td>
          <td><code>oci network vcn list -c &lt;compartment 的 OCID&gt;</code></td>
      </tr>
      <tr>
          <td><code>SG</code></td>
          <td>service gateway 的 OCID</td>
          <td><code>oci network service-gateway list -c &lt;compartment 的 OCID&gt;</code></td>
      </tr>
      <tr>
          <td><code>SGCIDR</code></td>
          <td>service gateway 路由規則的目的地名稱</td>
          <td><code>oci network service list</code>，名稱帶「All」那一筆的 <code>cidr-block</code></td>
      </tr>
      <tr>
          <td><code>RT</code></td>
          <td>私有子網路的路由表的 OCID</td>
          <td><code>oci network route-table list -c &lt;compartment 的 OCID&gt; --vcn-id &lt;VCN&gt;</code></td>
      </tr>
      <tr>
          <td><code>SN</code></td>
          <td>私有子網路的 OCID</td>
          <td><code>oci network subnet list -c &lt;compartment 的 OCID&gt; --vcn-id &lt;VCN&gt;</code></td>
      </tr>
      <tr>
          <td><code>B</code></td>
          <td>Bastion 的 OCID</td>
          <td><code>oci bastion bastion list -c &lt;compartment 的 OCID&gt;</code></td>
      </tr>
  </tbody>
</table>
<h2 id="建-ubuntu-2404-主機與經由-port-forwarding-登入">建 Ubuntu 24.04 主機與經由 port forwarding 登入</h2>
<p>建立指令跟建主機那篇〈等效的 CLI 指令〉相同，換成 Ubuntu 24.04 的映像檔，建立時放登入用的公鑰：</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"># 查 Ubuntu 24.04、相容 A1 的映像檔，取建立時間最新的那一個（實跑）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">oci compute image list -c &lt;compartment 的 OCID&gt; --operating-system <span class="s2">&#34;Canonical Ubuntu&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  --operating-system-version <span class="s2">&#34;24.04&#34;</span> --shape VM.Standard.A1.Flex <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  --sort-by TIMECREATED --sort-order DESC --query <span class="s1">&#39;data[0].id&#39;</span> --raw-output
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># 建主機（實跑；這次第一次送出就成功，容量不足時用建主機那篇的重送腳本）</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">oci compute instance launch -c &lt;compartment 的 OCID&gt; --availability-domain &lt;可用網域&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>  --display-name verify-ubuntu --shape VM.Standard.A1.Flex --shape-config <span class="s1">&#39;{&#34;ocpus&#34;:1,&#34;memoryInGBs&#34;:6}&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="se"></span>  --image-id &lt;映像檔的 OCID&gt; --subnet-id &lt;私有子網路的 OCID&gt; --assign-public-ip <span class="nb">false</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>  --ssh-authorized-keys-file &lt;登入用公鑰的路徑&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  --agent-config <span class="s1">&#39;{&#34;pluginsConfig&#34;:[{&#34;name&#34;:&#34;Bastion&#34;,&#34;desiredState&#34;:&#34;ENABLED&#34;}]}&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="se"></span>  --is-pv-encryption-in-transit-enabled <span class="nb">true</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="se"></span>  --instance-options <span class="s1">&#39;{&#34;areLegacyImdsEndpointsDisabled&#34;:true}&#39;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># 查主機的狀態（重建腳本裡同一條查詢實跑過）：印 PROVISIONING 時隔一陣子再查，印 RUNNING 才往下做</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">oci compute instance get --instance-id &lt;launch 印出的主機 OCID&gt; --query <span class="s1">&#39;data.&#34;lifecycle-state&#34;&#39;</span> --raw-output</span></span></code></pre></div><p>這次的映像檔是 <code>Canonical-Ubuntu-24.04-aarch64-2026.09.18-0</code>。建立請求在送出之後約 4 秒回傳主機的 OCID，下一次查狀態時（約 20 秒後）已經是 <code>RUNNING</code>。建立指令裡各參數這次的取值與換掉之後的結果：</p>
<ul>
<li><code>--availability-domain</code>：可用網域的完整名稱帶有 tenancy 專屬的前綴，用 <code>oci iam availability-domain list</code> 查；<code>ap-batam-1</code> 只有一個可用網域，沒有別的值可選。</li>
<li><code>--shape-config</code>：1 OCPU、6 GB。改大會多用 A1 的運算額度，合計超出免費量之後的結果依帳號狀態而定（〈費用與額度：A1、開機磁碟與區塊儲存額度、出站流量與 NAT gateway〉）；這次沒有比較其他規格。</li>
<li><code>--assign-public-ip false</code>：主機只有私有 IP，所以登入一定要經過 Bastion；換成公開 IP 的做法在下面的〈建主機與登入的其他做法：映像檔、公開 IP、VPN 與 managed SSH session〉。</li>
<li><code>--agent-config</code> 打開 Bastion plugin：plugin 是 managed SSH session 的前提，而這台只用 port forwarding session 登入，port forwarding 不經過 plugin，所以關掉它時登入照樣走得通（從兩種 session 的機制推論，這次沒有關掉 plugin 實跑）；打開它留著改用 managed SSH session 的可能，這台的組合為什麼用不了 managed SSH session 見下面的其他做法。</li>
<li><code>--is-pv-encryption-in-transit-enabled true</code>：開機磁碟在主機與儲存之間的傳輸加密。用這個值，沒有比較其他值；CLI 不給時預設是 false，跟網頁主控台的預設不同（建主機那篇〈等效的 CLI 指令〉）。</li>
<li><code>--instance-options</code> 的 <code>areLegacyImdsEndpointsDisabled</code>：主機只能用第二版的中繼資料服務（IMDSv2；中繼資料服務是什麼見 <a href="/blog/devops/infra/knowledge-cards/instance-metadata-service/" data-link-title="Instance Metadata Service（實例中繼資料服務）" data-link-desc="看到建立主機頁上的 IMDSv2、authorization header、metadata 選項，主機上的程式要取得自己的 ID、區域或身分憑證，或評估網頁應用程式的 SSRF 漏洞會不會讓攻擊者讀到主機的憑證時，查中繼資料服務提供什麼、第二版的存取驗證防的是什麼">Instance Metadata Service（實例中繼資料服務）</a>，Oracle Cloud 的這個選項與第二版的差別見 <a href="/blog/devops/infra/cloud-providers/vm-options/management/" data-link-title="代理程式、中繼資料服務、初始化腳本、維護行為與標籤" data-link-desc="雲端主機建立頁管理區塊的選項：業者裝在主機裡的代理程式與它的 plugin 怎麼決定開關、中繼資料服務的第二版存取驗證、開機時執行的初始化腳本、底層硬體維護時主機會被遷移還是重開，以及標籤">代理程式、中繼資料服務、初始化腳本、維護行為與標籤</a>〈中繼資料服務與 Authorization header〉）。用這個值，沒有比較其他值；這台主機的 cloud-init 在這個設定下照樣讀到了公鑰（下面登入那一段的 <code>status: done</code>）。</li>
</ul>
<p>主機的私有 IP 由子網路配發，建立之後才知道。私有 IP 掛在主機的 <a href="/blog/devops/infra/knowledge-cards/vnic/" data-link-title="VNIC（虛擬網路卡）" data-link-desc="雲端主機的私有 IP、公開 IP 或網卡層的防火牆規則要查、要改，或主機重建之後私有 IP 與防火牆規則跟著不見時，查 VNIC 掛著哪些設定、它跟主機一起建立與刪除的關係，以及各家的名稱">VNIC（虛擬網路卡）</a>（virtual network interface card：主機的虛擬網路卡，私有 IP 與後面〈加出站：NAT gateway、路由規則與只掛在這台主機上的 NSG〉的 NSG 都掛在它上面）上，所以用列出 VNIC 的子命令查：</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">oci compute instance list-vnics --instance-id &lt;主機的 OCID&gt; --query <span class="s1">&#39;data[0].&#34;private-ip&#34;&#39;</span> --raw-output</span></span></code></pre></div><p>接著建一個指向這個 IP、22 port 的 port forwarding session（實跑；建立 session 的 work request 約 1 分鐘後是 <code>SUCCEEDED</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">oci bastion session create-port-forwarding --bastion-id &lt;Bastion 的 OCID&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --target-private-ip &lt;主機的私有 IP&gt; --target-port <span class="m">22</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --ssh-public-key-file &lt;session 用公鑰的路徑&gt; --session-ttl <span class="m">3600</span> --display-name verify-ubuntu-pf <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --wait-for-state SUCCEEDED --wait-for-state FAILED --max-wait-seconds <span class="m">600</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;data.resources[0].identifier&#39;</span> --raw-output</span></span></code></pre></div><p>這個 Bastion 建立時把 session 的最長時效（<code>max-session-ttl</code>）設成 3600 秒，<code>--session-ttl</code> 填更大的值時以 <code>InvalidParameter</code> 失敗，所以 session 最長 1 小時，過了要重建。自己的 Bastion 設了多少，用 <code>oci bastion bastion get --bastion-id &lt;Bastion 的 OCID&gt; --query 'data.&quot;max-session-ttl-in-seconds&quot;'</code> 查（這一條沒有實跑）。上限可以用 <code>oci bastion bastion update --bastion-id &lt;Bastion 的 OCID&gt; --max-session-ttl &lt;秒數&gt;</code> 調高（<code>--help</code> 列得出這個選項，沒有實跑；可以調到多少這次沒有查證）：換到的是 session 少重建幾次，付出的是 session 用的私鑰外洩時，拿到的人能用比較久。登入用一份 ssh 設定檔，把 Bastion 當跳板（<code>ProxyJump</code>）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl"># ssh-ubuntu.conf：&lt;session 的 OCID&gt; 是上一條印出的值，&lt;…路徑&gt; 換成自己的
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">Host verify-bastion-u
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  HostName host.bastion.&lt;區域&gt;.oci.oraclecloud.com
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  User &lt;session 的 OCID&gt;
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  IdentityFile &lt;session 用私鑰的路徑&gt;
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  IdentitiesOnly yes
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">Host verify-ubuntu
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  HostName &lt;主機的私有 IP&gt;
</span></span><span class="line"><span class="ln">10</span><span class="cl">  User ubuntu
</span></span><span class="line"><span class="ln">11</span><span class="cl">  IdentityFile &lt;登入用私鑰的路徑&gt;
</span></span><span class="line"><span class="ln">12</span><span class="cl">  IdentitiesOnly yes
</span></span><span class="line"><span class="ln">13</span><span class="cl">  ProxyJump verify-bastion-u
</span></span><span class="line"><span class="ln">14</span><span class="cl">  UserKnownHostsFile &lt;這台專用的 known_hosts 路徑&gt;
</span></span><span class="line"><span class="ln">15</span><span class="cl">  StrictHostKeyChecking accept-new</span></span></code></pre></div>




<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">ssh -F ssh-ubuntu.conf verify-ubuntu <span class="s1">&#39;cloud-init status --wait; cloud-init status; uptime&#39;</span></span></span></code></pre></div><p>這次第一次連線印出 <code>status: done</code> 與 <code>up 1 min</code>：主機開機約 1 分鐘之後，cloud-init 的首次開機設定已經跑完，預設帳號 <code>ubuntu</code> 也有了公鑰。Ubuntu 映像檔的預設帳號是 <code>ubuntu</code>，Oracle Linux 是 <code>opc</code>，<code>User</code> 那一行要跟著映像檔改。<code>UserKnownHostsFile</code> 指向這台專用的檔案，主機重建之後 host key 會換，清掉這個檔就好，不必動 <code>~/.ssh/known_hosts</code>。</p>
<p><code>StrictHostKeyChecking accept-new</code> 讓 ssh 第一次連線時自動記下主機的 host key，之後 key 變了就拒絕連線。換到的是腳本裡的 ssh 不會停在「要不要信任這把 key」的提問；代價是第一次連線沒有比對指紋，那一刻連到的若不是這台主機，它的 key 也會被記下。要比對指紋時，照建主機那篇〈結果：停在 CREATING 約 15 分鐘後轉成 DELETED〉的做法用 console history 讀開機輸出，cloud-init 產生 host key 時把指紋印在裡面。預設值 <code>ask</code> 每台新主機第一次連線都要人回答，無人值守的腳本會停住（依 <code>ssh_config</code> 的說明，沒有實跑）。</p>
<p>設定檔的 <code>ProxyJump</code> 寫法以外，也可以在本機開一個轉送的 port 再連 <code>localhost</code>，兩種寫法的指令都在建主機那篇〈建 port forwarding session 並經由本機的轉送登入〉。</p>
<h3 id="建主機與登入的其他做法映像檔公開-ipvpn-與-managed-ssh-session">建主機與登入的其他做法：映像檔、公開 IP、VPN 與 managed SSH session</h3>
<ul>
<li><strong>改用 Oracle Linux 映像檔</strong>：Oracle Linux 的套件庫在 Oracle 的服務網路裡，只接 service gateway 的私有子網路就更新得到（從映像檔的套件庫設定與這個子網路的路由推論，這次沒有在這個子網路的 Oracle Linux 主機上跑 dnf 對照），〈加出站：NAT gateway、路由規則與只掛在這台主機上的 NSG〉那些設定就不需要。代價是 Oracle Linux 10 的代理程式沒有 Bastion plugin，managed SSH session 用不了，只能用 port forwarding session（建主機那篇〈重建：Oracle Linux 映像加建立時放公鑰，經由 port forwarding session 登入〉）。</li>
<li><strong>給主機公開 IP</strong>：<code>--assign-public-ip true</code> 並把主機放進有 internet gateway 的公開子網路，主機自己就連得到套件來源，也可以直接 SSH，不需要 Bastion 與 NAT。代價是 22 port 對外暴露，要靠 security list 或 NSG 收窄來源（概念見 <a href="/blog/devops/infra/knowledge-cards/security-group/" data-link-title="Security Group" data-link-desc="掛在資源網卡層級的有狀態防火牆，逐埠決定哪些來源能連進這個資源">Security Group</a>）；這次沒有實跑這種做法。只開給開發機的公開 IP（/32）也能把來源收窄到一個位址，Bastion 比它多給了三件事：誰能開 session 由 IAM 政策決定，不只看來源 IP；主機在網際網路上沒有位址可以連；session 到時效就自己失效。公開 IP 這條路另外有一個重建時的代價：主機被刪除重建之後，臨時配發的公開 IP 會換一個，ssh 設定與允許清單都要跟著改（依公開 IP 的配發方式推論，這次沒有實跑）。三層各自的做法與取捨見 <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>〈網路可達性的做法：管理 port 對誰開放〉。</li>
<li><strong>改用 VPN 或 Tailscale 這類 overlay 網路登入</strong>：主機上裝 VPN 的用戶端，從開發機經由 VPN 連進來，不需要 Bastion session 的 1 小時時效。這台主機的 NSG 只開出站 TCP 80 與 443，Tailscale 這類需要 UDP 直連的工具，推論會退到經由 443 的中繼伺服器轉送，延遲比直連高；這一點沒有查證，也沒有實跑。網路可達性那一層的比較在同一篇〈網路可達性的做法：管理 port 對誰開放〉。</li>
<li><strong>managed SSH session 代替 port forwarding</strong>：Ubuntu 24.04 的代理程式有 Bastion plugin，但官方〈Known Issues for Bastion〉寫明，Arm 架構的 A1 機型上、作業系統不是 Oracle Linux 的主機不支援 managed SSH session（理由是這類主機上的 Bastion plugin 沒有正確啟用）。建主機那篇〈建立之後：Bastion managed SSH session 逾時的排查〉在 A1 加 Ubuntu 上實測三次都逾時，所以這次只有 port forwarding 行得通。</li>
</ul>
<h2 id="只接-service-gateway-的私有子網路apt-連到套件來源時的錯誤原文與成因">只接 service gateway 的私有子網路：apt 連到套件來源時的錯誤原文與成因</h2>
<p>主機建好之後，apt 在這個子網路裡連不到套件來源。這次 <code>apt-get update</code> 失敗時的輸出沒有留下，下面附的是同一台主機上 <code>apt-get install</code> 下載套件時的錯誤原文（實跑；IPv6 位址那幾行省略）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">E: Failed to fetch http://ap-batam-1-ad-1.clouds.ports.ubuntu.com/ubuntu-ports/pool/main/... Cannot initiate the connection to ap-batam-1-ad-1.clouds.ports.ubuntu.com:80 (2620:2d:4002:1::10a). - connect (101: Network is unreachable) ... Could not connect to ap-batam-1-ad-1.clouds.ports.ubuntu.com:80 (91.189.91.103), connection timed out
</span></span><span class="line"><span class="ln">2</span><span class="cl">E: Unable to fetch some archives, maybe run apt-get update or try with --fix-missing?</span></span></code></pre></div><p>錯誤原文裡的兩種錯誤分別出自 IPv6 與 IPv4 兩條連線：</p>
<ul>
<li><strong><code>Network is unreachable</code>（IPv6）</strong>：主機的核心沒有任何通往網際網路的 IPv6 路由，<code>connect()</code> 立刻回錯誤碼 101（<code>ENETUNREACH</code>），封包沒有離開主機。序列主控台記下的開機輸出裡，cloud-init 印出的 IPv6 路由表只有 <code>fe80::/64</code>、<code>local</code> 與 <code>multicast</code> 三條，都只到這張網卡本身。</li>
<li><strong><code>connection timed out</code>（IPv4）</strong>：封包送得出主機，回應沒有回來。這個子網路的路由表沒有任何一條規則把網際網路的位址送到出口，security list 的出站規則也沒有放行網際網路；兩項都沒有放行，是哪一項先擋下封包，這次沒有分開實測（推論）。</li>
</ul>
<p>錯誤原文裡的主機名 <code>ap-batam-1-ad-1.clouds.ports.ubuntu.com</code> 是 Canonical 替雲端區域設的鏡像站；加上出站之後成功的輸出裡，<code>-security</code> 那幾行的來源是 <code>ports.ubuntu.com</code>。所以這個映像檔把一般的套件來源設成區域鏡像、把安全更新設成 <code>ports.ubuntu.com</code>（從兩份輸出與 Ubuntu 雲端映像檔的預設來源設定推論，這次沒有讀主機上的來源設定檔）。兩個主機都在 Oracle 的服務網路之外，而 service gateway 只轉送目的地是 Oracle 服務網路的封包，所以這台主機兩個都連不到。</p>
<p>這個失敗本身沒有訊號：<code>apt-get update</code> 的錯誤在背景排程裡只寫進 log，自動更新的 timer 照常觸發，什麼都裝不到。把 <code>apt-get update</code> 的輸出丟掉、只看 <code>apt list --upgradable</code> 時，連不到套件來源的主機會列出零個待裝的套件，看起來跟已經是最新的主機一樣；這次第一次探測就是這樣，加上出站之後同一台主機有 60 個安全更新待裝。所以私有子網路的主機上，讀自動更新的結果要讀它的 log（<code>/var/log/unattended-upgrades/</code>），不能只看有沒有待裝的套件。</p>
<h2 id="加出站nat-gateway路由規則與只掛在這台主機上的-nsg">加出站：NAT gateway、路由規則與只掛在這台主機上的 NSG</h2>
<p>要讓這台主機連得到網際網路上的套件來源而不給它公開 IP，出站要經過 <a href="/blog/devops/infra/knowledge-cards/nat/" data-link-title="NAT Gateway" data-link-desc="private subnet 或沒有外部 IP 的 VM 要主動對外連線、評估 NAT 的數量與出站成本、或在 AWS 與 GCP 之間對照出站做法時，查 NAT Gateway 的職責、部署模式與取捨">NAT Gateway</a>：主機送出的封包由 NAT gateway 換成它自己的公開 IP，回應的封包再換回來，網際網路上的主機連不進來。拿掉公開 IP 之後的出站需求怎麼處理（NAT，以及存取同一家業者的服務時不經過 NAT 的私有端點）、各自怎麼計費，寫在 <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>〈移除公開 IP 之後的出站路徑〉；這一節是其中 NAT 那一條在 Oracle Cloud 上的實作。加出站要動三項設定：NAT gateway、路由表與 NSG。路由規則要填 NAT gateway 的 OCID，所以 NAT gateway 先建、路由表後改；NSG 跟前兩項沒有依賴，放在哪個位置做都可以。</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"># 1. 建 NAT gateway（實跑）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">oci network nat-gateway create -c &lt;compartment 的 OCID&gt; --vcn-id &lt;VCN 的 OCID&gt; --display-name verify-nat <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  --wait-for-state AVAILABLE --query data.id --raw-output
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 2. 路由表加 0.0.0.0/0 → NAT（實跑）；update 是整份替換，原本那條 service gateway 的規則要一起寫</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">oci network route-table update --rt-id &lt;私有子網路路由表的 OCID&gt; --force --wait-for-state AVAILABLE --route-rules <span class="s1">&#39;[
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s1"> {&#34;destination&#34;:&#34;&lt;Oracle 服務網路的目的地名稱&gt;&#34;,&#34;destinationType&#34;:&#34;SERVICE_CIDR_BLOCK&#34;,&#34;networkEntityId&#34;:&#34;&lt;service gateway 的 OCID&gt;&#34;},
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s1"> {&#34;destination&#34;:&#34;0.0.0.0/0&#34;,&#34;destinationType&#34;:&#34;CIDR_BLOCK&#34;,&#34;networkEntityId&#34;:&#34;&lt;NAT gateway 的 OCID&gt;&#34;}]&#39;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># 確認路由（沒有實跑）：該看到兩條，0.0.0.0/0 指向 NAT gateway 的 OCID，service gateway 那一條也還在</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">oci network route-table get --rt-id &lt;私有子網路路由表的 OCID&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;data.&#34;route-rules&#34;[].{destination:destination,target:&#34;network-entity-id&#34;}&#39;</span> --output table
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># 3. 建只有出站 TCP 80、443 的 NSG，掛到這台主機的 VNIC（實跑）</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">oci network nsg create -c &lt;compartment 的 OCID&gt; --vcn-id &lt;VCN 的 OCID&gt; --display-name verify-ubuntu-egress <span class="se">\
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="se"></span>  --wait-for-state AVAILABLE --query data.id --raw-output
</span></span><span class="line"><span class="ln">14</span><span class="cl">oci network nsg rules add --nsg-id &lt;NSG 的 OCID&gt; --security-rules <span class="s1">&#39;[
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="s1"> {&#34;direction&#34;:&#34;EGRESS&#34;,&#34;protocol&#34;:&#34;6&#34;,&#34;destination&#34;:&#34;0.0.0.0/0&#34;,&#34;destinationType&#34;:&#34;CIDR_BLOCK&#34;,&#34;isStateless&#34;:false,&#34;tcpOptions&#34;:{&#34;destinationPortRange&#34;:{&#34;min&#34;:80,&#34;max&#34;:80}}},
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="s1"> {&#34;direction&#34;:&#34;EGRESS&#34;,&#34;protocol&#34;:&#34;6&#34;,&#34;destination&#34;:&#34;0.0.0.0/0&#34;,&#34;destinationType&#34;:&#34;CIDR_BLOCK&#34;,&#34;isStateless&#34;:false,&#34;tcpOptions&#34;:{&#34;destinationPortRange&#34;:{&#34;min&#34;:443,&#34;max&#34;:443}}}]&#39;</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1"># 確認規則（沒有實跑）：該看到兩列 EGRESS，port 分別是 80 與 443</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">oci network nsg rules list --nsg-id &lt;NSG 的 OCID&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;data[].{direction:direction,port:&#34;tcp-options&#34;.&#34;destination-port-range&#34;.min}&#39;</span> --output table
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="c1"># 查這台主機的 VNIC 的 OCID（實跑），再把 NSG 掛上去</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">oci compute instance list-vnics --instance-id &lt;主機的 OCID&gt; --query <span class="s1">&#39;data[0].id&#39;</span> --raw-output
</span></span><span class="line"><span class="ln">22</span><span class="cl">oci network vnic update --vnic-id &lt;主機的 VNIC 的 OCID&gt; --nsg-ids <span class="s1">&#39;[&#34;&lt;NSG 的 OCID&gt;&#34;]&#39;</span> --force
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="c1"># 確認掛上（沒有實跑）：印出的 OCID 清單裡有 &lt;NSG 的 OCID&gt;</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">oci network vnic get --vnic-id &lt;主機的 VNIC 的 OCID&gt; --query <span class="s1">&#39;data.&#34;nsg-ids&#34;&#39;</span></span></span></code></pre></div><ul>
<li><strong>路由表</strong>：決定封包往哪個出口送。規則依目的地比對，Oracle 服務網路的位址仍走 service gateway，其餘的（<code>0.0.0.0/0</code>）走 NAT gateway。<code>&lt;Oracle 服務網路的目的地名稱&gt;</code> 是原本那條規則的目的地，查法在〈前置條件：沿用 Oracle Cloud 建立主機實測紀錄的網路、Bastion 與金鑰〉的私有子網路那一項。<code>route-table update --route-rules</code> 會用給的清單取代整份規則，<code>--force</code> 略過取代之前的確認提示。只寫 NAT 那一條的話，service gateway 那一條就被刪掉，主機連 Oracle 服務網路的流量（Oracle Cloud Agent 的回報這類）改走 NAT gateway 到公開端點（從路由的比對方式推論，沒有實跑只寫一條的結果）。</li>
<li><strong>NSG</strong>：決定這張網卡（VNIC）放行哪些封包，概念見 <a href="/blog/devops/infra/knowledge-cards/security-group/" data-link-title="Security Group" data-link-desc="掛在資源網卡層級的有狀態防火牆，逐埠決定哪些來源能連進這個資源">Security Group</a>。這個子網路的 security list 只放行 Oracle 服務網路與子網路內部的出站，有路由也送不出去，所以要再放行一條。這一條放在 NSG：security list 套在整個子網路上，同一個子網路裡的其他主機（這次有一台 Oracle Linux 主機）會一起多出網際網路的出站。NSG 只套在掛上它的 VNIC，影響範圍只有這一台。Oracle Cloud 對一張網卡取 security list 與 NSG 兩邊規則的聯集，任何一邊放行就放行（<a href="/blog/devops/infra/cloud-providers/vm-options/networking/" data-link-title="網路：subnet、私有 IP、公開 IP 與防火牆規則" data-link-desc="雲端主機建立頁網路區塊的選項：主機放進哪個虛擬網路與 subnet、私有 IP 與公開 IP 各自的用途與生命週期、防火牆規則掛在 subnet、網卡與作業系統哪一個位置以及各家怎麼合併，以及主控台新建的網路預設對外開放了什麼">網路：subnet、私有 IP、公開 IP 與防火牆規則</a>〈防火牆規則的位置與合併方式〉），所以「只開 80 與 443」成立的前提是這個子網路的 security list 一直不放行網際網路的出站；有人之後在 security list 加了全開的出站規則，這台主機的出站也跟著全開。規則裡的 <code>&quot;protocol&quot;:&quot;6&quot;</code> 是 TCP 在 IANA 協定號碼表上的號碼；規則是有狀態的（<code>isStateless: false</code>），回應的封包自動放行，不必另寫入站規則。</li>
<li><strong>只開 80 與 443</strong>：<code>apt-get update</code> 的輸出顯示 Ubuntu 的套件來源是 <code>http://</code>（80），套件的完整性由 apt 另外驗簽章；443 留給用 HTTPS 的來源（snap 與第三方套件庫這類）。只開這兩個 port 時 <code>apt-get update</code> 成功，名稱解析沒有另外放行；名稱解析走 VCN 內建的 DNS，在 VCN 裡面，不經過這兩條出站規則（從 VCN 的 DNS 設計推論，這次沒有讀主機上的解析設定）。</li>
</ul>
<p><code>vnic update --nsg-ids</code> 同樣是整份取代，<code>--force</code> 同樣略過確認提示：VNIC 原本掛著別的 NSG 時，要把原本的那幾個一起寫進清單。三樣都做完之後，同一條指令成功（實跑）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Get:55 http://ports.ubuntu.com/ubuntu-ports noble-security/multiverse arm64 c-n-f Metadata [456 B]
</span></span><span class="line"><span class="ln">2</span><span class="cl">Fetched 39.9 MB in 9s (4371 kB/s)</span></span></code></pre></div><p>這一次下載 39.9 MB 花了 9 秒，速率 4371 kB/s。<code>apt list --upgradable</code> 列出 79 行，其中 60 個來自 <code>-security</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">sudo unattended-upgrade   <span class="c1"># 裝 -security 的更新（實跑，402 秒）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo systemctl reboot     <span class="c1"># 換上新核心（實跑）</span></span></span></code></pre></div><p><code>unattended-upgrade</code> 跑完升級了 64 個套件，比 <code>-security</code> 的 60 個多 4 個。這 4 個的來源這次沒有查；一個可能是新核心以新的套件名稱安裝，新安裝的套件不會列在 <code>apt list --upgradable</code> 裡（推論）。待裝的數量依映像檔建立的日期到建主機那一天之間公開了多少更新而變，換一個映像檔或換一天建，數字就不同。核心更新前後的重開機判斷在維護篇〈會老化的軟體：各條更新管道與更新後的重開機〉。</p>
<h3 id="出站的其他做法security-list建主機時就掛-nsg限縮出站的目的地與不加出站">出站的其他做法：security list、建主機時就掛 NSG、限縮出站的目的地與不加出站</h3>
<ul>
<li><strong>把出站規則加在 security list</strong>：效果是整個子網路的主機都能出站。同一個子網路只有這一台主機、或每一台本來就要更新時，這樣設定比較少；代價是之後放進這個子網路的主機也自動有網際網路出站（從 security list 套用的範圍推論，沒有實跑）。</li>
<li><strong>建主機時就掛上 NSG</strong>：<code>oci compute instance launch</code> 有 <code>--nsg-ids</code>（<code>--help</code> 確認過，沒有實跑），建立時就把出站 NSG 掛在主機的 VNIC 上。換到的是主機首次開機時就有出站，重建腳本裡掛 NSG 那一步（第 6 步）也不必另外做；付出的是 NAT gateway、路由與 NSG 都要在建主機之前做好，順序從「先建主機、再加出站」變成「先加出站、再建主機」。</li>
<li><strong>限縮出站的目的地</strong>：三種做法都把出站從「任何位址的 80 與 443」收窄。NSG 的規則只收 CIDR，可以只放行 Canonical 鏡像站的網段，代價是鏡像站的位址會變，位址換了之後更新又會逾時（推論，沒有查證）；在有出站的子網路自架一台 forward proxy，依網域篩選，私有子網路的主機只連它；或在 VCN 裡自建一台 apt 的快取代理（例如 apt-cacher-ng），私有子網路的主機只連那一台，另外還能控制更新的內容。後兩種適合主機多的環境，代價是多一台要維護的主機（推論，沒有實跑）。</li>
<li><strong>不加出站</strong>：主機永遠裝不到套件更新，自動更新的 timer 照常觸發、什麼都裝不到；只適合用完即刪、不需要更新的實驗。</li>
</ul>
<h2 id="序列主控台rsa-金鑰建立連線proxyjump-設定與看到的開機輸出">序列主控台：RSA 金鑰、建立連線、ProxyJump 設定與看到的開機輸出</h2>
<p>序列主控台（<a href="/blog/devops/infra/knowledge-cards/serial-console/" data-link-title="Serial Console（序列主控台）" data-link-desc="SSH 因為防火牆、sshd 設定或磁碟滿而連不進主機，或要規劃管理通道斷掉時的備用路徑時，查序列主控台與頻外管理介面的用法與它本身的暴露面">Serial Console（序列主控台）</a>）讀的是主機的虛擬序列埠，不經過主機的網路與 sshd，sshd 設定改壞、防火牆擋住、新核心開不了機時，仍看得到開機輸出。Oracle Cloud 的做法是替主機建一個序列主控台連線（instance console connection），它只收 RSA 公鑰：</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"># 產生 RSA 金鑰；序列主控台連線不收 ed25519（依 Oracle Cloud 文件〈Instance Console Connections〉，沒有拿 ed25519 實試）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">ssh-keygen -q -t rsa -b <span class="m">3072</span> -N <span class="s1">&#39;&#39;</span> -C verify-console -f &lt;序列主控台私鑰的路徑&gt;
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 建序列主控台連線（實跑，回傳時已是 ACTIVE）</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">oci compute instance-console-connection create --instance-id &lt;主機的 OCID&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --ssh-public-key-file &lt;序列主控台私鑰的路徑&gt;.pub --wait-for-state ACTIVE
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># 取得官方的連線字串</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">oci compute instance-console-connection list -c &lt;compartment 的 OCID&gt; --instance-id &lt;主機的 OCID&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;data[0].&#34;connection-string&#34;&#39;</span> --raw-output</span></span></code></pre></div><p>這次的序列主控台連線用完就刪，平時不留。序列主控台是救援入口，繞過平時登入主機的控制點，所以照 <a href="/blog/devops/infra/knowledge-cards/serial-console/" data-link-title="Serial Console（序列主控台）" data-link-desc="SSH 因為防火牆、sshd 設定或磁碟滿而連不進主機，或要規劃管理通道斷掉時的備用路徑時，查序列主控台與頻外管理介面的用法與它本身的暴露面">Serial Console（序列主控台）</a>〈設計責任〉的「平時保持關閉、救援時才開啟、用完關回去」：要排查時跑〈被回收之後的重建：怎麼發現、留下與消失的資源、重建腳本〉裡序列主控台連線的重建腳本建一條（這次實跑約 15 秒），排查完用下面這條刪掉（腳本結尾會印出代入了連線 OCID 的同一條）：</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"># 刪掉序列主控台連線（實跑）：--wait-for-state 等的是連線本身的狀態</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">oci compute instance-console-connection delete --instance-console-connection-id &lt;連線的 OCID&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --force --wait-for-state DELETED</span></span></code></pre></div><p>2026-10-27 對 <code>verify-ubuntu</code> 那條連線實跑這一條：CLI 印出 <code>Action completed. Waiting until the resource has entered state: ('DELETED',)</code>，exit 0，10 秒；之後 <code>instance-console-connection list</code> 只列出這一條，狀態是 <code>DELETED</code>，主機本身仍是 <code>RUNNING</code>。</p>
<p><code>ssh-keygen</code> 的 <code>-N ''</code> 讓私鑰不設保護密碼（passphrase），重建腳本可以不經互動使用它；代價是拿到這個私鑰檔的人就連得上這台主機的序列主控台。這個代價跟上面「連線只在排查時存在」一起看：連線平時不存在，私鑰外洩時，拿到的人還要等下一次有人建連線、而且要在刪掉之前用它，能用的時間窗只剩排查那一段；連線一直留著的話，同一把沒有保護密碼的私鑰隨時都連得上。檔案仍然只給自己讀（<code>ssh-keygen</code> 產生的私鑰權限是 600）。設了保護密碼的話，每次連線要輸入它，或先把私鑰交給 <code>ssh-agent</code>，這次沒有比較這種做法。</p>
<p>官方的連線字串是一條 ssh 指令，外面再包一層 ssh 當跳板，形狀是 <code>ssh -o ProxyCommand='ssh -W %h:%p -p 443 &lt;連線的 OCID&gt;@instance-console.&lt;區域&gt;.oci.oraclecloud.com' &lt;主機的 OCID&gt;</code>。<code>ProxyCommand</code> 讓 ssh 在本機執行一條任意的指令，把它的輸入輸出當成連線；<code>ProxyJump</code> 由 ssh 自己經由跳板轉送連線，不在本機執行別的指令，效果相同。<code>ProxyJump</code> 這一欄只接受跳板的使用者、主機與 port，有人改寫這一欄時，改得到的只有跳板的位址；<code>ProxyCommand</code> 那一欄被改寫時，換上的是會在本機執行的指令。改寫成 <code>ProxyJump</code> 時：跳板主機是 <code>instance-console.&lt;區域&gt;.oci.oraclecloud.com</code> 的 443 port，使用者名稱是連線的 OCID；目的地的「主機名稱」是主機的 OCID，使用者名稱同樣是連線的 OCID。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl"># ssh-console.conf
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">Host verify-console-gw
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  HostName instance-console.&lt;區域&gt;.oci.oraclecloud.com
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  Port 443
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  User &lt;連線的 OCID&gt;
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  IdentityFile &lt;序列主控台私鑰的路徑&gt;
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  IdentitiesOnly yes
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  UserKnownHostsFile &lt;序列主控台專用的 known_hosts 路徑&gt;
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  StrictHostKeyChecking accept-new
</span></span><span class="line"><span class="ln">10</span><span class="cl">  HostKeyAlgorithms +ssh-rsa
</span></span><span class="line"><span class="ln">11</span><span class="cl">  PubkeyAcceptedAlgorithms +ssh-rsa
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">Host verify-console
</span></span><span class="line"><span class="ln">14</span><span class="cl">  HostName &lt;主機的 OCID&gt;
</span></span><span class="line"><span class="ln">15</span><span class="cl">  User &lt;連線的 OCID&gt;
</span></span><span class="line"><span class="ln">16</span><span class="cl">  ProxyJump verify-console-gw
</span></span><span class="line"><span class="ln">17</span><span class="cl">  IdentityFile &lt;序列主控台私鑰的路徑&gt;
</span></span><span class="line"><span class="ln">18</span><span class="cl">  IdentitiesOnly yes
</span></span><span class="line"><span class="ln">19</span><span class="cl">  UserKnownHostsFile &lt;序列主控台專用的 known_hosts 路徑&gt;
</span></span><span class="line"><span class="ln">20</span><span class="cl">  StrictHostKeyChecking accept-new
</span></span><span class="line"><span class="ln">21</span><span class="cl">  HostKeyAlgorithms +ssh-rsa
</span></span><span class="line"><span class="ln">22</span><span class="cl">  PubkeyAcceptedAlgorithms +ssh-rsa</span></span></code></pre></div>




<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"># -tt 讓 ssh 配一個終端機，序列埠的輸出才會照原樣顯示；離開時先按 Enter，再輸入 ~.</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">ssh -tt -F ssh-console.conf verify-console</span></span></code></pre></div><p><code>HostKeyAlgorithms +ssh-rsa</code> 與 <code>PubkeyAcceptedAlgorithms +ssh-rsa</code> 兩行是預防性的：連線用 RSA 金鑰，新版 OpenSSH 預設不接受 <code>ssh-rsa</code> 簽章，這次沒有比較拿掉這兩行的結果。</p>
<p>連上之後先印出一段 Oracle Cloud 的說明，接著是主機目前的序列埠輸出。這次連著序列主控台讓主機重開機（在另一個終端機經由 Bastion 執行 <code>sudo systemctl reboot</code>），依序看到關機時 systemd 停止各個 unit、<code>reboot: Restarting system</code>、韌體的 <code>Oracle AAVMF Version: 20260902</code> 與載入的 UEFI 模組、核心的 <code>Linux version 6.17.0-…-oracle</code>，核心開始開機之後約 27 秒在 <code>ttyAMA0</code> 印出 <code>Ubuntu 24.04.5 LTS verify-ubuntu ttyAMA0</code> 與登入提示。只看開機卡在哪一步（核心、檔案系統、cloud-init）時，不必登入就看得到。</p>
<p>登入作業系統要一個設了密碼的使用者。這台主機的 <code>ubuntu</code> 帳號，<code>passwd -S ubuntu</code> 印出的狀態是 <code>L</code>（密碼鎖定），這次也沒有替任何使用者設密碼，所以登入那一步沒有做。</p>
<h3 id="序列主控台的其他做法在序列主控台登入作業系統console-historycloud-shell-與-vnc">序列主控台的其他做法：在序列主控台登入作業系統、console history、Cloud Shell 與 VNC</h3>
<ul>
<li><strong>事先設一個救援帳號的密碼</strong>：序列主控台出現登入提示時就能登入，sshd 壞掉時從這裡修。代價是主機多了一個密碼登入的帳號，要同時確認 sshd 不接受密碼登入（<code>sshd -T</code> 印 <code>passwordauthentication no</code>），密碼只在序列主控台上有效。GCP 上照這個做法實際登入過，步驟在 <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 的實測紀錄：盤點、救援入口、IAP 規則與 IAM、停用舊規則驗證、OS Login 啟用與移除外部 IP</a>〈準備救援入口：Serial Console 與本機救援帳號〉；三家業者的條件比較在維護篇〈會老化的軟體：各條更新管道與更新後的重開機〉。</li>
<li><strong>只讀開機輸出的快照（console history）</strong>：<code>oci compute console-history capture</code> 擷取一份開機輸出的唯讀快照，不必建連線、不必產生 RSA 金鑰，也不能輸入；只要知道開機停在哪一步、或讀 host key 的指紋時夠用。建主機那篇〈結果：停在 CREATING 約 15 分鐘後轉成 DELETED〉用過這兩條指令。</li>
<li><strong>從網頁主控台的 Cloud Shell 連</strong>：網頁主控台的主機頁面有「Launch Cloud Shell connection」，在瀏覽器裡開連線，不必在本機產生金鑰與設定檔（依 Oracle Cloud 文件〈Instance Console Connections〉，沒有實跑）。</li>
<li><strong>VNC 連線</strong>：同一個序列主控台連線另有 VNC 的連線字串，看的是圖形畫面，Linux 伺服器映像檔沒有圖形介面時跟序列埠看到的相同（依同一份文件，沒有實跑）。</li>
</ul>
<h2 id="被回收之後的重建怎麼發現留下與消失的資源重建腳本">被回收之後的重建：怎麼發現、留下與消失的資源、重建腳本</h2>
<p>Always Free 的主機在連續 7 天內 CPU 使用率（取第 95 百分位）、網路使用率、記憶體使用率（只算 A1）三項都低於 20% 時，Oracle 可能回收它，規則與官方沒寫的部分在 <a href="/blog/devops/infra/cloud-providers/oracle-cloud/account-and-home-region/" data-link-title="Oracle Cloud 的帳號結構與 home region：tenancy、Always Free 的範圍與主控台登入欄位" data-link-desc="OCI 的 tenancy、home region、compartment 與 availability domain 各自管什麼，home region 為什麼在申請當下就要決定，Always Free 額度的計算與回收規則，以及主控台登入時租用戶與使用者名稱兩個欄位的分工">Oracle Cloud 的帳號結構與 home region：tenancy、Always Free 的範圍與主控台登入欄位</a>（下文稱「帳號篇」）〈Always Free 的額度與它綁在 home region 上的範圍〉。這台主機平常沒有負載，三項都落在條件裡；官方沒有寫這條規則對付費帳戶適不適用，這個 tenancy 已經是付費帳戶，所以無法確定這台是否會被回收。官方也沒有寫回收之後主機是停止還是刪除、會不會通知，所以重建的做法要兩種情形都涵蓋。</p>
<p>這次沒有做回收發生之前的對策，只準備了回收之後的重建。事前的對策有三種，這次都沒有實作：</p>
<ul>
<li><strong>維持負載</strong>：讓三項使用率之一一直高於 20%，主機就不落在回收條件裡。官方條款怎麼看待專門為了避開回收而製造的負載，這次沒有查證。</li>
<li><strong>用 Events 或 Monitoring 告警</strong>：主機狀態改變時通知自己，回收發生的當下就知道。回收是否發出可以訂閱的事件，這次沒有查證。</li>
<li><strong>定期備份開機磁碟</strong>：回收之後可以從備份還原，裝過的更新與設定都在。暫停、停止、終止主機各自會留下哪些資源，比較見 <a href="/blog/devops/operations/09-host-lifecycle/suspend-and-resume-service/" data-link-title="服務暫停與恢復上線：只停服務、停止主機、終止主機到全部刪除各自保留的資源，暫停期間仍計費與仍被指向的資源，恢復前要查的漂移與恢復的順序" data-link-desc="一台主機上的服務要暫停一段時間再恢復：只停服務、停止主機、終止主機保留設定、全部刪除靠 repo 重建各自保留什麼與付出什麼，怎麼依保留的月費、重建的成本與漂移量選擇，暫停期間仍在計費的資源（含在作業系統裡 shutdown 不停止計費），仍被 DNS 與 CDN 指向的結果（Cloudflare 521、522，釋放 IP 之後的懸空 DNS），恢復前要重新確認的漂移，以及依元件依賴排出的恢復順序與驗證">服務暫停與恢復上線：只停服務、停止主機、終止主機到全部刪除各自保留的資源，暫停期間仍計費與仍被指向的資源，恢復前要查的漂移與恢復的順序</a>〈暫停時保留的範圍：只停服務、停止主機、終止主機、全部刪除〉；那一篇的〈恢復上線前要重新確認的漂移〉另外寫了主機不是照計畫暫停時（被閒置回收這類）要拿什麼當比對的基準。</li>
</ul>
<p><strong>發現主機不見了</strong>：經由 Bastion 登入失敗時（port forwarding session 建不出來、或 ssh 逾時），先查主機的狀態：</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">oci compute instance get --instance-id &lt;主機的 OCID&gt; --query <span class="s1">&#39;data.&#34;lifecycle-state&#34;&#39;</span> --raw-output
</span></span><span class="line"><span class="ln">2</span><span class="cl">oci compute instance list -c &lt;compartment 的 OCID&gt; --display-name verify-ubuntu <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;data[].{id:id,state:&#34;lifecycle-state&#34;}&#39;</span></span></span></code></pre></div><ul>
<li>印 <code>RUNNING</code>：主機還在，問題在 Bastion、session 過期或開發機的 IP 換了（Bastion 的 client CIDR）。</li>
<li>印 <code>STOPPED</code>：主機被停止，開機磁碟、VNIC 與私有 IP 都還在，啟動之後回到 <code>RUNNING</code>；啟動也可能失敗，例如 A1 容量不足時（推論，這次沒有遇到）。</li>
<li>印 <code>TERMINATED</code>、或 <code>get</code> 回 404、<code>list</code> 找不到：主機被刪除，要重建。開機磁碟是否一起刪掉，官方沒有寫，到 Block Storage → Boot Volumes 查。舊磁碟還在時有兩種處理：拿它重建主機（<code>oci compute instance launch</code> 以 <code>--source-boot-volume-id &lt;開機磁碟的 OCID&gt;</code> 代替 <code>--image-id</code>，沒有實跑），裝過的更新與設定都還在；或刪掉它，釋放它佔的區塊磁碟額度。主機終止時開機磁碟的預設去留見 <a href="/blog/devops/infra/cloud-providers/vm-options/storage/" data-link-title="磁碟：開機磁碟與區塊磁碟的大小、效能、加密與去留" data-link-desc="雲端主機的磁碟選項：開機磁碟的大小與只能加大的限制、效能等級與費用的關係、靜態加密與傳輸中加密、另外掛載的區塊磁碟，以及刪除主機時開機磁碟是否預設一起刪除">磁碟：開機磁碟與區塊磁碟的大小、效能、加密與去留</a>〈刪除（終止）主機時開機磁碟的預設去留〉。</li>
</ul>
<p><strong>兩種情形各自留下與消失的資源</strong>。格內寫「實跑」的是主動操作時看到的，不是回收：停止那一欄的是 2026-10-27 主動停止 <code>verify-ubuntu</code> 再啟動時看到的，刪除那一欄的是主動終止 <code>verify-ubuntu-rebuild</code> 時看到的。格內沒寫「實跑」的，停止那一欄依停止主機的機制推論，刪除那一欄依終止主機的機制推論。回收實際做的是停止還是刪除，這次沒有遇到：</p>
<table>
  <thead>
      <tr>
          <th>資源</th>
          <th>主機被停止</th>
          <th>主機被刪除</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>compartment、VCN、私有子網路、service gateway、Bastion</td>
          <td>留下</td>
          <td>留下</td>
      </tr>
      <tr>
          <td>NAT gateway、路由規則、NSG 與它的規則</td>
          <td>留下（主動停止時實跑）</td>
          <td>留下</td>
      </tr>
      <tr>
          <td>主機、它的 VNIC 與私有 IP</td>
          <td>留下，啟動之後私有 IP 不變（主動停止時實跑）</td>
          <td>消失；重建的主機拿到新的私有 IP</td>
      </tr>
      <tr>
          <td>NSG 掛在 VNIC 上的關係</td>
          <td>留下（主動停止時實跑）</td>
          <td>跟著 VNIC 消失，要重新掛</td>
      </tr>
      <tr>
          <td>開機磁碟與上面裝過的更新</td>
          <td>留下（主動停止時實跑）</td>
          <td>指定刪除時一起刪掉（主動終止時實跑，不是回收）；回收時的去留官方沒寫</td>
      </tr>
      <tr>
          <td>序列主控台連線</td>
          <td>平時沒有；排查時建的那一條用完就刪</td>
          <td>變成 <code>DELETED</code>（主動終止時實跑，不是回收）；平時本來就沒有</td>
      </tr>
      <tr>
          <td>port forwarding session</td>
          <td>1 小時後本來就會失效</td>
          <td>同左</td>
      </tr>
      <tr>
          <td>本機的 ssh 設定與 known_hosts</td>
          <td>不用改；例外是 <code>host-&lt;名稱&gt;.env</code> 還沒有記下這台主機時（主機不是重建腳本建的，例如照〈建 Ubuntu 24.04 主機與經由 port forwarding 登入〉手動建的），重建腳本第一次跑會清空 known_hosts 一次，下一次登入自動重新記下（見下面重建腳本的實跑結果）</td>
          <td>IP 與 host key 都換了，要重寫</td>
      </tr>
  </tbody>
</table>
<p><strong>重建腳本</strong>：下面這支腳本把「停止就啟動、不見就重建」與之後要重新接上的每一項資源與設定寫在一起。每一步先查後建，重跑不會重複建立；例外是 port forwarding session，它最長 1 小時就失效，所以每次都新建一個。每一步至少印一行「已存在」「新建」「啟動」「重送」或「改寫」；任何一條 <code>oci</code> 指令失敗，腳本就停下來，印出那條指令寫在 stderr 的內容，不會把失敗當成「查無結果」往下走。它的每一個建立指令都是〈建 Ubuntu 24.04 主機與經由 port forwarding 登入〉與〈加出站：NAT gateway、路由規則與只掛在這台主機上的 NSG〉兩節實跑過的那一條，先查後建與錯誤處理的部分是新加的。</p>
<p>這支腳本改過三個版本，雲端實跑與測試各自用的是其中一版：</p>
<ul>
<li><strong>沒有等待迴圈的版本</strong>：沒有 <code>wait_state</code>，也沒有等待 VNIC 的迴圈。</li>
<li><strong>加上等待迴圈的版本</strong>：加上 <code>wait_state</code> 與等待 VNIC 的迴圈。</li>
<li><strong>現在這一版</strong>：下面這一版。在加上等待迴圈的版本之外，加了依主機狀態分支、重送迴圈的改寫（只重送 500 與 429）、依主機是否為同一台決定 known_hosts 的去留、路由目的地改讀 <code>ids.env</code> 的 <code>SGCIDR</code>，以及每一條 <code>oci</code> 失敗就停下來的錯誤處理（含路由表裡有其他規則時停下來、NAT gateway 與 NSG 已存在時也記進 <code>ids.env</code>）。2026-10-27 上午另外加了指定開機磁碟大小的 <code>BOOT_GB</code>，只動到建立請求那一行（加上 <code>BOOT_ARGS</code> 的展開）、開頭變數區組 <code>BOOT_ARGS</code> 的一行，以及檔頭的用法註解（改寫一行、加兩行；單位的說明是實跑之後才補上的，執行的程式碼沒有變）；當天凌晨 <code>STOPPED</code>、<code>STOPPING</code>、<code>STARTING</code> 那幾次跑的是加 <code>BOOT_GB</code> 之前的版本，那幾條路徑不經過建立請求，下表一律算在現在這一版。</li>
</ul>
<p>雲端實跑是 2026-10-27 在同一個 tenancy 上跑的；假的 <code>oci</code> 是一支依劇本回傳主機狀態與建立請求回應、也能讓單一種呼叫失敗的指令，搭配一支不等待、只記下秒數的假 <code>sleep</code>。兩者的結果都在腳本之後。序列主控台連線的重建腳本也列在同一張表；它在雲端跑的是訊息還寫「主控台連線」、還沒有錯誤處理與換金鑰處理的前一版：</p>
<table>
  <thead>
      <tr>
          <th>路徑</th>
          <th>在雲端實跑過</th>
          <th>用假的 <code>oci</code> 測過</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>主機已經是 <code>RUNNING</code></td>
          <td>現在這一版（重建 NAT gateway 與 NSG 那一次）；較早還跑過沒有等待迴圈的版本</td>
          <td>現在這一版</td>
      </tr>
      <tr>
          <td>找不到同名主機，新建</td>
          <td>現在這一版從送出建立請求跑到結束（<code>BOOT_GB=60</code> 與不給 <code>BOOT_GB</code> 各一次）</td>
          <td>現在這一版</td>
      </tr>
      <tr>
          <td>主機 <code>STOPPED</code>、<code>STOPPING</code>、<code>STARTING</code></td>
          <td>現在這一版</td>
          <td>現在這一版</td>
      </tr>
      <tr>
          <td>主機 <code>PROVISIONING</code></td>
          <td>現在這一版（另一份腳本正在建同名主機時）</td>
          <td>現在這一版</td>
      </tr>
      <tr>
          <td>主機停在其他狀態（<code>TERMINATING</code>）、或狀態讀不出</td>
          <td>沒有</td>
          <td>現在這一版</td>
      </tr>
      <tr>
          <td>建立請求回 500 或 429 後重送、回 400 或 401、讀不出回應碼、OCID 前有警告、查不到映像檔</td>
          <td>沒有（雲端那一次第一次送出就成功）</td>
          <td>現在這一版</td>
      </tr>
      <tr>
          <td>NAT gateway 與 NSG 新建、路由表少了 0.0.0.0/0 那一條</td>
          <td>現在這一版</td>
          <td>現在這一版</td>
      </tr>
      <tr>
          <td>VNIC 已經掛著其他 NSG</td>
          <td>沒有</td>
          <td>現在這一版</td>
      </tr>
      <tr>
          <td>路由表裡有其他規則</td>
          <td>沒有</td>
          <td>現在這一版</td>
      </tr>
      <tr>
          <td>主機跟上次記下的不是同一台</td>
          <td>現在這一版，<code>host-&lt;名稱&gt;.env</code> 還沒有記下主機的情形（<code>STOPPED</code> 那一次）</td>
          <td>現在這一版</td>
      </tr>
      <tr>
          <td>查詢或寫入的 <code>oci</code> 失敗（查主機、啟動、查 NAT、讀與改路由表、讀與加 NSG 規則、讀與改 VNIC）</td>
          <td>沒有</td>
          <td>現在這一版</td>
      </tr>
      <tr>
          <td><code>instance action</code>、<code>wait_state</code> 實際等待的時間</td>
          <td><code>instance action</code> 等到 <code>RUNNING</code>：現在這一版，<code>STOPPED</code>、<code>STOPPING</code> 兩次，從腳本開始到印出「啟動」各約 9 秒；<code>wait_state</code> 的 10 秒輪詢：新建主機時跑到（路徑跑到是實跑；輪詢的次數從時間戳推論），印出「新建」主機（送出建立請求之前）到印出「已 RUNNING」約 50 秒，同一段時間碰到 <code>PROVISIONING</code> 的另一份腳本也在輪詢；900 秒逾時：沒有跑到</td>
          <td>不適用（假的 <code>sleep</code> 不等待）</td>
      </tr>
      <tr>
          <td>序列主控台連線：沒有 <code>ACTIVE</code> 的連線就建</td>
          <td>前一版</td>
          <td>現在這一版</td>
      </tr>
      <tr>
          <td>序列主控台連線：已有連線就沿用、新產生金鑰時換掉舊連線、找不到主機、<code>oci</code> 失敗</td>
          <td>沒有</td>
          <td>現在這一版</td>
      </tr>
  </tbody>
</table>





<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="cp">#!/bin/bash
</span></span></span><span class="line"><span class="ln">  2</span><span class="cl"><span class="cp"></span><span class="c1"># 主機被回收、停止或刪除之後，把 verify-ubuntu 這一組恢復成可以登入、連得到套件來源的狀態：</span>
</span></span><span class="line"><span class="ln">  3</span><span class="cl"><span class="c1">#   主機（不存在就建、停止就啟動）→ 私有 IP 與 VNIC → NAT gateway → 私有子網路路由的 0.0.0.0/0 規則</span>
</span></span><span class="line"><span class="ln">  4</span><span class="cl"><span class="c1">#   → 出站 NSG 與它的兩條規則 → NSG 掛到這台的 VNIC → port forwarding session → ssh 設定與 known_hosts。</span>
</span></span><span class="line"><span class="ln">  5</span><span class="cl"><span class="c1"># 每一步先查後建，重跑不會重複建立（例外是 port forwarding session，每次都新建一個）；</span>
</span></span><span class="line"><span class="ln">  6</span><span class="cl"><span class="c1">#   每一步至少印一行「已存在」「新建」「啟動」「重送」或「改寫」。任何一條 oci 失敗就停下來，印出它的 stderr。</span>
</span></span><span class="line"><span class="ln">  7</span><span class="cl"><span class="c1"># 用法：./rebuild-ubuntu.sh [主機名稱]（預設 verify-ubuntu）。新建主機時，環境變數 BOOT_GB 指定開機磁碟大小，</span>
</span></span><span class="line"><span class="ln">  8</span><span class="cl"><span class="c1">#   單位跟 boot-volume list 的 size-in-gbs 相同（60 建出 60 GiB）；不給就用映像檔的預設大小</span>
</span></span><span class="line"><span class="ln">  9</span><span class="cl"><span class="c1">#   （Ubuntu 24.04 的 size-in-gbs 是 47）。需要 ids.env 的 C（compartment）、V（VCN）、</span>
</span></span><span class="line"><span class="ln"> 10</span><span class="cl"><span class="c1">#   SG（service gateway）、SGCIDR（service gateway 路由規則的目的地名稱）、RT（私有子網路的路由表）、</span>
</span></span><span class="line"><span class="ln"> 11</span><span class="cl"><span class="c1">#   SN（私有子網路）、B（Bastion）。</span>
</span></span><span class="line"><span class="ln"> 12</span><span class="cl"><span class="c1"># 整個 VCN 共用的 NAT gateway 與 NSG 記在 ids.env；只屬於這一台的值（主機、session）記在 host-&lt;名稱&gt;.env，</span>
</span></span><span class="line"><span class="ln"> 13</span><span class="cl"><span class="c1">#   ssh 設定與 known_hosts 也依名稱各一份，所以給不同名稱重跑不會蓋掉另一台的紀錄。</span>
</span></span><span class="line"><span class="ln"> 14</span><span class="cl"><span class="c1"># 結束碼：0 完成；2 登入過期、查不到映像檔、主機停在無法處理的狀態或其他無法繼續的錯誤；</span>
</span></span><span class="line"><span class="ln"> 15</span><span class="cl"><span class="c1">#   3 建立主機的回應讀不出回應碼（請求可能已經成功），重跑之前先用 instance list 確認沒有同名主機；</span>
</span></span><span class="line"><span class="ln"> 16</span><span class="cl"><span class="c1">#   4 建立主機的回應碼不是 500、429、401（400 參數錯誤這類），照印出的訊息改好再跑。</span>
</span></span><span class="line"><span class="ln"> 17</span><span class="cl"><span class="nb">set</span> -uo pipefail
</span></span><span class="line"><span class="ln"> 18</span><span class="cl"><span class="c1"># OCI CLI 是 Python 程式；PYTHONWARNINGS=ignore 關掉 Python 直譯器印的警告，不讓警告行混進要解析的輸出</span>
</span></span><span class="line"><span class="ln"> 19</span><span class="cl"><span class="nb">export</span> <span class="nv">OCI_CLI_AUTH</span><span class="o">=</span>security_token <span class="nv">PYTHONWARNINGS</span><span class="o">=</span>ignore
</span></span><span class="line"><span class="ln"> 20</span><span class="cl"><span class="nv">D</span><span class="o">=</span><span class="k">$(</span><span class="nb">cd</span> <span class="s2">&#34;</span><span class="k">$(</span>dirname <span class="s2">&#34;</span><span class="nv">$0</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span> <span class="o">&amp;&amp;</span> <span class="nb">pwd</span><span class="k">)</span>
</span></span><span class="line"><span class="ln"> 21</span><span class="cl"><span class="nb">source</span> <span class="s2">&#34;</span><span class="nv">$D</span><span class="s2">/ids.env&#34;</span>
</span></span><span class="line"><span class="ln"> 22</span><span class="cl"><span class="nv">NAME</span><span class="o">=</span><span class="si">${</span><span class="nv">1</span><span class="k">:-</span><span class="nv">verify</span><span class="p">-ubuntu</span><span class="si">}</span>
</span></span><span class="line"><span class="ln"> 23</span><span class="cl"><span class="nv">AD</span><span class="o">=</span><span class="s2">&#34;&lt;可用網域&gt;&#34;</span>
</span></span><span class="line"><span class="ln"> 24</span><span class="cl"><span class="nv">KEY</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$D</span><span class="s2">/verify-a1-key.pub&#34;</span>
</span></span><span class="line"><span class="ln"> 25</span><span class="cl"><span class="nv">NAT_NAME</span><span class="o">=</span>verify-nat
</span></span><span class="line"><span class="ln"> 26</span><span class="cl"><span class="nv">NSG_NAME</span><span class="o">=</span>verify-ubuntu-egress
</span></span><span class="line"><span class="ln"> 27</span><span class="cl"><span class="nv">HOSTENV</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$D</span><span class="s2">/host-</span><span class="si">${</span><span class="nv">NAME</span><span class="si">}</span><span class="s2">.env&#34;</span>
</span></span><span class="line"><span class="ln"> 28</span><span class="cl"><span class="nv">CONF</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$D</span><span class="s2">/ssh-</span><span class="si">${</span><span class="nv">NAME</span><span class="si">}</span><span class="s2">.conf&#34;</span>
</span></span><span class="line"><span class="ln"> 29</span><span class="cl"><span class="nv">KNOWN</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$D</span><span class="s2">/known_hosts_</span><span class="si">${</span><span class="nv">NAME</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln"> 30</span><span class="cl"><span class="nv">BOOT_ARGS</span><span class="o">=()</span><span class="p">;</span> <span class="o">[[</span> -n <span class="s2">&#34;</span><span class="si">${</span><span class="nv">BOOT_GB</span><span class="k">:-</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nv">BOOT_ARGS</span><span class="o">=(</span>--boot-volume-size-in-gbs <span class="s2">&#34;</span><span class="nv">$BOOT_GB</span><span class="s2">&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="ln"> 31</span><span class="cl"><span class="nv">NEW</span><span class="o">=</span><span class="m">0</span>
</span></span><span class="line"><span class="ln"> 32</span><span class="cl"><span class="nv">ERR</span><span class="o">=</span><span class="k">$(</span>mktemp <span class="s2">&#34;</span><span class="si">${</span><span class="nv">TMPDIR</span><span class="k">:-</span><span class="p">/tmp</span><span class="si">}</span><span class="s2">/rebuild-ubuntu.XXXXXX&#34;</span><span class="k">)</span><span class="p">;</span> <span class="nb">trap</span> <span class="s1">&#39;rm -f &#34;$ERR&#34;&#39;</span> EXIT
</span></span><span class="line"><span class="ln"> 33</span><span class="cl">ts<span class="o">()</span> <span class="o">{</span> date +%m-%d<span class="se">\ </span>%H:%M:%S<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="ln"> 34</span><span class="cl">say<span class="o">()</span> <span class="o">{</span> <span class="nb">echo</span> <span class="s2">&#34;</span><span class="k">$(</span>ts<span class="k">)</span><span class="s2"> [</span><span class="nv">$1</span><span class="s2">] </span><span class="nv">$2</span><span class="s2">&#34;</span><span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="ln"> 35</span><span class="cl">die<span class="o">()</span> <span class="o">{</span> <span class="nb">echo</span> <span class="s2">&#34;</span><span class="k">$(</span>ts<span class="k">)</span><span class="s2"> [錯誤] </span><span class="nv">$1</span><span class="s2">&#34;</span> &gt;<span class="p">&amp;</span>2<span class="p">;</span> <span class="nb">exit</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">2</span><span class="k">:-</span><span class="nv">2</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="ln"> 36</span><span class="cl"><span class="c1"># 查詢與寫入用的包裝：stderr 存進 $ERR 那個暫存檔，濾掉兩種不是結果的提示行（Python 的 SyntaxWarning，</span>
</span></span><span class="line"><span class="ln"> 37</span><span class="cl"><span class="c1">#   與 CLI 建議改用 `oci -i` 的 Interactive mode 那一行），結束碼照 oci 自己的回傳。</span>
</span></span><span class="line"><span class="ln"> 38</span><span class="cl"><span class="c1">#   CLI 的 --query 結果是空的時候 stdout 什麼都不印，所以空字串要搭配結束碼 0 才代表查無結果</span>
</span></span><span class="line"><span class="ln"> 39</span><span class="cl">q<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="ln"> 40</span><span class="cl">  <span class="nb">local</span> out rc
</span></span><span class="line"><span class="ln"> 41</span><span class="cl">  <span class="nv">out</span><span class="o">=</span><span class="k">$(</span>oci <span class="s2">&#34;</span><span class="nv">$@</span><span class="s2">&#34;</span> 2&gt;<span class="s2">&#34;</span><span class="nv">$ERR</span><span class="s2">&#34;</span><span class="k">)</span><span class="p">;</span> <span class="nv">rc</span><span class="o">=</span><span class="nv">$?</span>
</span></span><span class="line"><span class="ln"> 42</span><span class="cl">  <span class="o">[[</span> -n <span class="s2">&#34;</span><span class="nv">$out</span><span class="s2">&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> grep -v -e SyntaxWarning -e <span class="s1">&#39;Interactive mode&#39;</span> <span class="o">&lt;&lt;&lt;</span><span class="s2">&#34;</span><span class="nv">$out</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln"> 43</span><span class="cl">  <span class="k">return</span> <span class="nv">$rc</span>
</span></span><span class="line"><span class="ln"> 44</span><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="ln"> 45</span><span class="cl"><span class="c1"># q 失敗時停下來，印出 oci 寫在 stderr 的內容</span>
</span></span><span class="line"><span class="ln"> 46</span><span class="cl">fail<span class="o">()</span> <span class="o">{</span> die <span class="s2">&#34;</span><span class="si">${</span><span class="nv">1</span><span class="si">}</span><span class="s2">失敗：</span><span class="k">$(</span>tr <span class="s1">&#39;\n&#39;</span> <span class="s1">&#39; &#39;</span> &lt; <span class="s2">&#34;</span><span class="nv">$ERR</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span><span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="ln"> 47</span><span class="cl"><span class="c1"># 把「鍵=值」記進 ids.env；已經有同樣的一行就不重複寫</span>
</span></span><span class="line"><span class="ln"> 48</span><span class="cl">record<span class="o">()</span> <span class="o">{</span> grep -qx <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">=</span><span class="nv">$2</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$D</span><span class="s2">/ids.env&#34;</span> <span class="o">||</span> <span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">=</span><span class="nv">$2</span><span class="s2">&#34;</span> &gt;&gt; <span class="s2">&#34;</span><span class="nv">$D</span><span class="s2">/ids.env&#34;</span><span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="ln"> 49</span><span class="cl"><span class="c1"># instance get 沒有 --wait-for-state，所以自己每 10 秒查一次狀態，最多等 900 秒</span>
</span></span><span class="line"><span class="ln"> 50</span><span class="cl">wait_state<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="ln"> 51</span><span class="cl">  <span class="nb">local</span> <span class="nv">want</span><span class="o">=</span><span class="nv">$1</span> i cur
</span></span><span class="line"><span class="ln"> 52</span><span class="cl">  <span class="k">for</span> <span class="o">((</span><span class="nv">i</span> <span class="o">=</span> 0<span class="p">;</span> i &lt; 90<span class="p">;</span> i++<span class="o">))</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln"> 53</span><span class="cl">    <span class="nv">cur</span><span class="o">=</span><span class="k">$(</span>q compute instance get --instance-id <span class="s2">&#34;</span><span class="nv">$IID</span><span class="s2">&#34;</span> --query <span class="s1">&#39;data.&#34;lifecycle-state&#34;&#39;</span> --raw-output<span class="k">)</span> <span class="o">||</span> fail <span class="s2">&#34;查主機狀態&#34;</span>
</span></span><span class="line"><span class="ln"> 54</span><span class="cl">    <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$cur</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;</span><span class="nv">$want</span><span class="s2">&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="k">return</span> <span class="m">0</span>
</span></span><span class="line"><span class="ln"> 55</span><span class="cl">    sleep <span class="m">10</span>
</span></span><span class="line"><span class="ln"> 56</span><span class="cl">  <span class="k">done</span>
</span></span><span class="line"><span class="ln"> 57</span><span class="cl">  die <span class="s2">&#34;主機等了 900 秒仍是 </span><span class="si">${</span><span class="nv">cur</span><span class="k">:-</span><span class="nv">查不到</span><span class="si">}</span><span class="s2">，沒有變成 </span><span class="nv">$want</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln"> 58</span><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="ln"> 59</span><span class="cl">
</span></span><span class="line"><span class="ln"> 60</span><span class="cl"><span class="c1"># 0. 登入：session token 過期時後面每一條都會失敗，先擋下來</span>
</span></span><span class="line"><span class="ln"> 61</span><span class="cl">oci session validate --profile DEFAULT &gt;/dev/null 2&gt;<span class="p">&amp;</span><span class="m">1</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 62</span><span class="cl"><span class="se"></span>  <span class="o">||</span> die <span class="s2">&#34;CLI session 過期，先跑 oci session authenticate --region &lt;區域&gt; --profile-name DEFAULT&#34;</span>
</span></span><span class="line"><span class="ln"> 63</span><span class="cl">
</span></span><span class="line"><span class="ln"> 64</span><span class="cl"><span class="c1"># 1. 主機：依名稱找還沒終止的那一台，依狀態決定跳過、啟動或建立</span>
</span></span><span class="line"><span class="ln"> 65</span><span class="cl"><span class="nv">LAST_IID</span><span class="o">=</span><span class="k">$(</span>grep <span class="s1">&#39;^IID=&#39;</span> <span class="s2">&#34;</span><span class="nv">$HOSTENV</span><span class="s2">&#34;</span> 2&gt;/dev/null <span class="p">|</span> tail -1 <span class="p">|</span> cut -d<span class="o">=</span> -f2<span class="k">)</span>
</span></span><span class="line"><span class="ln"> 66</span><span class="cl"><span class="nv">IID</span><span class="o">=</span><span class="k">$(</span>q compute instance list -c <span class="s2">&#34;</span><span class="nv">$C</span><span class="s2">&#34;</span> --display-name <span class="s2">&#34;</span><span class="nv">$NAME</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 67</span><span class="cl"><span class="se"></span>  --query <span class="s2">&#34;data[?\&#34;lifecycle-state\&#34;!=&#39;TERMINATED&#39;] | [0].id&#34;</span> --raw-output<span class="k">)</span> <span class="o">||</span> fail <span class="s2">&#34;查同名主機&#34;</span>
</span></span><span class="line"><span class="ln"> 68</span><span class="cl"><span class="nv">STATE</span><span class="o">=</span><span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 69</span><span class="cl"><span class="c1"># 查詢成功而且結果是空的，才代表沒有同名主機；查得到主機而狀態讀不出來時，不當成「不存在」去建第二台</span>
</span></span><span class="line"><span class="ln"> 70</span><span class="cl"><span class="k">if</span> <span class="o">[[</span> -n <span class="s2">&#34;</span><span class="nv">$IID</span><span class="s2">&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln"> 71</span><span class="cl">  <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$IID</span><span class="s2">&#34;</span> <span class="o">==</span> ocid1.instance* <span class="o">]]</span> <span class="o">||</span> die <span class="s2">&#34;查同名主機回了不是主機 OCID 的值：</span><span class="nv">$IID</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln"> 72</span><span class="cl">  <span class="nv">STATE</span><span class="o">=</span><span class="k">$(</span>q compute instance get --instance-id <span class="s2">&#34;</span><span class="nv">$IID</span><span class="s2">&#34;</span> --query <span class="s1">&#39;data.&#34;lifecycle-state&#34;&#39;</span> --raw-output<span class="k">)</span> <span class="o">||</span> fail <span class="s2">&#34;查主機狀態&#34;</span>
</span></span><span class="line"><span class="ln"> 73</span><span class="cl">  <span class="o">[[</span> -z <span class="s2">&#34;</span><span class="nv">$STATE</span><span class="s2">&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> die <span class="s2">&#34;查得到主機 </span><span class="si">${</span><span class="nv">IID</span><span class="si">}</span><span class="s2">，卻讀不出它的狀態&#34;</span>
</span></span><span class="line"><span class="ln"> 74</span><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="ln"> 75</span><span class="cl"><span class="k">case</span> <span class="s2">&#34;</span><span class="nv">$STATE</span><span class="s2">&#34;</span> in
</span></span><span class="line"><span class="ln"> 76</span><span class="cl">  RUNNING<span class="o">)</span> say 已存在 <span class="s2">&#34;主機 </span><span class="si">${</span><span class="nv">NAME</span><span class="si">}</span><span class="s2">（RUNNING）&#34;</span> <span class="p">;;</span>
</span></span><span class="line"><span class="ln"> 77</span><span class="cl">  STOPPED<span class="o">)</span>
</span></span><span class="line"><span class="ln"> 78</span><span class="cl">    q compute instance action --instance-id <span class="s2">&#34;</span><span class="nv">$IID</span><span class="s2">&#34;</span> --action START --wait-for-state RUNNING --max-wait-seconds <span class="m">900</span> &gt;/dev/null <span class="se">\
</span></span></span><span class="line"><span class="ln"> 79</span><span class="cl"><span class="se"></span>      <span class="o">||</span> fail <span class="s2">&#34;啟動主機（STOPPED → RUNNING）&#34;</span>
</span></span><span class="line"><span class="ln"> 80</span><span class="cl">    say 啟動 <span class="s2">&#34;主機 </span><span class="si">${</span><span class="nv">NAME</span><span class="si">}</span><span class="s2">（STOPPED → RUNNING）&#34;</span> <span class="p">;;</span>
</span></span><span class="line"><span class="ln"> 81</span><span class="cl">  STARTING<span class="p">|</span>PROVISIONING<span class="o">)</span>
</span></span><span class="line"><span class="ln"> 82</span><span class="cl">    wait_state RUNNING
</span></span><span class="line"><span class="ln"> 83</span><span class="cl">    say 已存在 <span class="s2">&#34;主機 </span><span class="si">${</span><span class="nv">NAME</span><span class="si">}</span><span class="s2">（等到 RUNNING）&#34;</span> <span class="p">;;</span>
</span></span><span class="line"><span class="ln"> 84</span><span class="cl">  STOPPING<span class="o">)</span>
</span></span><span class="line"><span class="ln"> 85</span><span class="cl">    wait_state STOPPED
</span></span><span class="line"><span class="ln"> 86</span><span class="cl">    q compute instance action --instance-id <span class="s2">&#34;</span><span class="nv">$IID</span><span class="s2">&#34;</span> --action START --wait-for-state RUNNING --max-wait-seconds <span class="m">900</span> &gt;/dev/null <span class="se">\
</span></span></span><span class="line"><span class="ln"> 87</span><span class="cl"><span class="se"></span>      <span class="o">||</span> fail <span class="s2">&#34;啟動主機（STOPPING → RUNNING）&#34;</span>
</span></span><span class="line"><span class="ln"> 88</span><span class="cl">    say 啟動 <span class="s2">&#34;主機 </span><span class="si">${</span><span class="nv">NAME</span><span class="si">}</span><span class="s2">（STOPPING → RUNNING）&#34;</span> <span class="p">;;</span>
</span></span><span class="line"><span class="ln"> 89</span><span class="cl">  <span class="s2">&#34;&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="ln"> 90</span><span class="cl">    <span class="c1"># 找不到還沒終止的同名主機：只有這種情形才建立</span>
</span></span><span class="line"><span class="ln"> 91</span><span class="cl">    <span class="nv">IMG</span><span class="o">=</span><span class="k">$(</span>q compute image list -c <span class="s2">&#34;</span><span class="nv">$C</span><span class="s2">&#34;</span> --operating-system <span class="s2">&#34;Canonical Ubuntu&#34;</span> --operating-system-version <span class="s2">&#34;24.04&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 92</span><span class="cl"><span class="se"></span>      --shape VM.Standard.A1.Flex --sort-by TIMECREATED --sort-order DESC --query <span class="s1">&#39;data[0].id&#39;</span> --raw-output<span class="k">)</span> <span class="o">||</span> fail <span class="s2">&#34;查映像檔&#34;</span>
</span></span><span class="line"><span class="ln"> 93</span><span class="cl">    <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$IMG</span><span class="s2">&#34;</span> <span class="o">==</span> ocid1.image* <span class="o">]]</span> <span class="o">||</span> die <span class="s2">&#34;查不到 Ubuntu 24.04 的 A1 映像檔：</span><span class="si">${</span><span class="nv">IMG</span><span class="k">:-</span><span class="nv">查詢結果是空的</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln"> 94</span><span class="cl">    <span class="nv">IMGNAME</span><span class="o">=</span><span class="k">$(</span>q compute image get --image-id <span class="s2">&#34;</span><span class="nv">$IMG</span><span class="s2">&#34;</span> --query <span class="s1">&#39;data.&#34;display-name&#34;&#39;</span> --raw-output<span class="k">)</span> <span class="o">||</span> fail <span class="s2">&#34;查映像檔名稱&#34;</span>
</span></span><span class="line"><span class="ln"> 95</span><span class="cl">    say 新建 <span class="s2">&#34;主機 </span><span class="si">${</span><span class="nv">NAME</span><span class="si">}</span><span class="s2">，映像檔 </span><span class="nv">$IMGNAME</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln"> 96</span><span class="cl">    <span class="c1"># 容量不足時重送：間隔從 120 秒起，429 加倍（上限 600），其他回應每次縮 10 秒、最低 60 秒，±15 秒抖動</span>
</span></span><span class="line"><span class="ln"> 97</span><span class="cl">    <span class="nv">back</span><span class="o">=</span><span class="m">120</span> <span class="nv">n</span><span class="o">=</span><span class="m">0</span> <span class="nv">last_refresh</span><span class="o">=</span><span class="m">0</span>                     <span class="c1"># 0：開跑時先延長一次 token</span>
</span></span><span class="line"><span class="ln"> 98</span><span class="cl">    <span class="k">while</span> :<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln"> 99</span><span class="cl">      <span class="nv">n</span><span class="o">=</span><span class="k">$((</span>n+1<span class="k">))</span>
</span></span><span class="line"><span class="ln">100</span><span class="cl">      <span class="c1"># session token 一小時過期，每 30 分鐘（1800 秒）延長一次，重送跑得再久也不會在中途過期</span>
</span></span><span class="line"><span class="ln">101</span><span class="cl">      <span class="k">if</span> <span class="o">((</span> <span class="k">$(</span>date +%s<span class="k">)</span> - last_refresh &gt;<span class="o">=</span> <span class="m">1800</span> <span class="o">))</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">102</span><span class="cl">        oci session refresh --profile DEFAULT &gt;/dev/null 2&gt;<span class="p">&amp;</span><span class="m">1</span> <span class="o">||</span> die <span class="s2">&#34;token refresh 失敗，重跑 oci session authenticate&#34;</span>
</span></span><span class="line"><span class="ln">103</span><span class="cl">        <span class="nv">last_refresh</span><span class="o">=</span><span class="k">$(</span>date +%s<span class="k">)</span>
</span></span><span class="line"><span class="ln">104</span><span class="cl">      <span class="k">fi</span>
</span></span><span class="line"><span class="ln">105</span><span class="cl">      <span class="nv">out</span><span class="o">=</span><span class="k">$(</span>oci --no-retry compute instance launch -c <span class="s2">&#34;</span><span class="nv">$C</span><span class="s2">&#34;</span> --availability-domain <span class="s2">&#34;</span><span class="nv">$AD</span><span class="s2">&#34;</span> --display-name <span class="s2">&#34;</span><span class="nv">$NAME</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">106</span><span class="cl"><span class="se"></span>        --shape VM.Standard.A1.Flex --shape-config <span class="s1">&#39;{&#34;ocpus&#34;:1,&#34;memoryInGBs&#34;:6}&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">107</span><span class="cl"><span class="se"></span>        --image-id <span class="s2">&#34;</span><span class="nv">$IMG</span><span class="s2">&#34;</span> --subnet-id <span class="s2">&#34;</span><span class="nv">$SN</span><span class="s2">&#34;</span> --assign-public-ip <span class="nb">false</span> <span class="si">${</span><span class="nv">BOOT_ARGS</span><span class="p">[@]+</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">BOOT_ARGS</span><span class="p">[@]</span><span class="si">}</span><span class="s2">&#34;</span><span class="si">}</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">108</span><span class="cl"><span class="se"></span>        --ssh-authorized-keys-file <span class="s2">&#34;</span><span class="nv">$KEY</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">109</span><span class="cl"><span class="se"></span>        --agent-config <span class="s1">&#39;{&#34;pluginsConfig&#34;:[{&#34;name&#34;:&#34;Bastion&#34;,&#34;desiredState&#34;:&#34;ENABLED&#34;}]}&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">110</span><span class="cl"><span class="se"></span>        --is-pv-encryption-in-transit-enabled <span class="nb">true</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">111</span><span class="cl"><span class="se"></span>        --instance-options <span class="s1">&#39;{&#34;areLegacyImdsEndpointsDisabled&#34;:true}&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">112</span><span class="cl"><span class="se"></span>        --query data.id --raw-output 2&gt;<span class="p">&amp;</span>1<span class="k">)</span>
</span></span><span class="line"><span class="ln">113</span><span class="cl">      <span class="c1"># 輸出前面可能夾著 CLI 的警告，所以在整段輸出裡找 OCID，不只比對開頭</span>
</span></span><span class="line"><span class="ln">114</span><span class="cl">      <span class="nv">id</span><span class="o">=</span><span class="k">$(</span>grep -o <span class="s1">&#39;ocid1\.instance\.[^&#34; ]*&#39;</span> <span class="o">&lt;&lt;&lt;</span><span class="s2">&#34;</span><span class="nv">$out</span><span class="s2">&#34;</span> <span class="p">|</span> head -1<span class="k">)</span>
</span></span><span class="line"><span class="ln">115</span><span class="cl">      <span class="k">if</span> <span class="o">[[</span> -n <span class="s2">&#34;</span><span class="nv">$id</span><span class="s2">&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span> <span class="nv">IID</span><span class="o">=</span><span class="nv">$id</span><span class="p">;</span> break<span class="p">;</span> <span class="k">fi</span>
</span></span><span class="line"><span class="ln">116</span><span class="cl">      <span class="nv">status</span><span class="o">=</span><span class="k">$(</span>grep -o <span class="s1">&#39;&#34;status&#34;: [0-9]*&#39;</span> <span class="o">&lt;&lt;&lt;</span><span class="s2">&#34;</span><span class="nv">$out</span><span class="s2">&#34;</span> <span class="p">|</span> head -1 <span class="p">|</span> grep -o <span class="s1">&#39;[0-9]*$&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="ln">117</span><span class="cl">      <span class="c1"># 設定檔錯誤、網路中斷這類失敗沒有回應碼；請求也可能已經在 Oracle 那一端成功，所以不重送</span>
</span></span><span class="line"><span class="ln">118</span><span class="cl">      <span class="o">[[</span> -z <span class="s2">&#34;</span><span class="nv">$status</span><span class="s2">&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> die <span class="s2">&#34;讀不出回應碼，先用 oci compute instance list -c &lt;compartment&gt; --display-name </span><span class="nv">$NAME</span><span class="s2"> 確認沒有同名主機再重跑：</span><span class="nv">$out</span><span class="s2">&#34;</span> <span class="m">3</span>
</span></span><span class="line"><span class="ln">119</span><span class="cl">      <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$status</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="m">401</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> die <span class="s2">&#34;401，重跑 oci session authenticate&#34;</span>
</span></span><span class="line"><span class="ln">120</span><span class="cl">      <span class="c1"># 只有 500（容量不足）與 429（限流）等一段時間可能變好；400 這類代表請求本身有誤，重送也是同一個結果</span>
</span></span><span class="line"><span class="ln">121</span><span class="cl">      <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$status</span><span class="s2">&#34;</span> !<span class="o">=</span> <span class="m">500</span> <span class="o">&amp;&amp;</span> <span class="s2">&#34;</span><span class="nv">$status</span><span class="s2">&#34;</span> !<span class="o">=</span> <span class="m">429</span> <span class="o">]]</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">122</span><span class="cl"><span class="se"></span>        <span class="o">&amp;&amp;</span> die <span class="s2">&#34;建立請求回 </span><span class="si">${</span><span class="nv">status</span><span class="si">}</span><span class="s2">，不重送：</span><span class="k">$(</span>grep -o <span class="s1">&#39;&#34;message&#34;: &#34;[^&#34;]*&#34;&#39;</span> <span class="o">&lt;&lt;&lt;</span><span class="s2">&#34;</span><span class="nv">$out</span><span class="s2">&#34;</span> <span class="p">|</span> head -1<span class="k">)</span><span class="s2">&#34;</span> <span class="m">4</span>
</span></span><span class="line"><span class="ln">123</span><span class="cl">      <span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$status</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="m">429</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span> <span class="nv">back</span><span class="o">=</span><span class="k">$((</span> back <span class="o">*</span> <span class="m">2</span> &gt; <span class="m">600</span> ? <span class="m">600</span> : back <span class="o">*</span> <span class="m">2</span> <span class="k">))</span><span class="p">;</span> <span class="k">else</span> <span class="nv">back</span><span class="o">=</span><span class="k">$((</span> back <span class="o">-</span> <span class="m">10</span> &lt; <span class="m">60</span> ? <span class="m">60</span> : back <span class="o">-</span> <span class="m">10</span> <span class="k">))</span><span class="p">;</span> <span class="k">fi</span>
</span></span><span class="line"><span class="ln">124</span><span class="cl">      <span class="nv">wait</span><span class="o">=</span><span class="k">$((</span> back <span class="o">-</span> <span class="m">15</span> <span class="o">+</span> RANDOM <span class="o">%</span> <span class="m">31</span> <span class="k">))</span>
</span></span><span class="line"><span class="ln">125</span><span class="cl">      say 重送 <span class="s2">&#34;第 </span><span class="nv">$n</span><span class="s2"> 次 </span><span class="si">${</span><span class="nv">status</span><span class="si">}</span><span class="s2">：</span><span class="k">$(</span>grep -o <span class="s1">&#39;&#34;message&#34;: &#34;[^&#34;]*&#34;&#39;</span> <span class="o">&lt;&lt;&lt;</span><span class="s2">&#34;</span><span class="nv">$out</span><span class="s2">&#34;</span> <span class="p">|</span> head -1<span class="k">)</span><span class="s2">，</span><span class="si">${</span><span class="nv">wait</span><span class="si">}</span><span class="s2"> 秒後再送&#34;</span>
</span></span><span class="line"><span class="ln">126</span><span class="cl">      sleep <span class="s2">&#34;</span><span class="nv">$wait</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">127</span><span class="cl">    <span class="k">done</span>
</span></span><span class="line"><span class="ln">128</span><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;IID=</span><span class="nv">$IID</span><span class="s2">&#34;</span> &gt;&gt; <span class="s2">&#34;</span><span class="nv">$HOSTENV</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">129</span><span class="cl">    <span class="c1"># 新主機的 host key 是新的，舊紀錄留著的話 ssh 會因為 host key 不符而拒絕連線</span>
</span></span><span class="line"><span class="ln">130</span><span class="cl">    : &gt; <span class="s2">&#34;</span><span class="nv">$KNOWN</span><span class="s2">&#34;</span><span class="p">;</span> <span class="nv">NEW</span><span class="o">=</span><span class="m">1</span>
</span></span><span class="line"><span class="ln">131</span><span class="cl">    wait_state RUNNING
</span></span><span class="line"><span class="ln">132</span><span class="cl">    say 新建 <span class="s2">&#34;主機 </span><span class="nv">$NAME</span><span class="s2"> 已 RUNNING（第 </span><span class="nv">$n</span><span class="s2"> 次送出成功）&#34;</span> <span class="p">;;</span>
</span></span><span class="line"><span class="ln">133</span><span class="cl">  *<span class="o">)</span>
</span></span><span class="line"><span class="ln">134</span><span class="cl">    <span class="c1"># TERMINATING、MOVING、CREATING_IMAGE 這類狀態，或上面沒有列出的狀態：不猜下一步，停下來</span>
</span></span><span class="line"><span class="ln">135</span><span class="cl">    die <span class="s2">&#34;主機 </span><span class="si">${</span><span class="nv">NAME</span><span class="si">}</span><span class="s2"> 的狀態是 </span><span class="si">${</span><span class="nv">STATE</span><span class="si">}</span><span class="s2">，這支腳本不處理這個狀態；等它變成 RUNNING、STOPPED 或 TERMINATED 再重跑&#34;</span> <span class="p">;;</span>
</span></span><span class="line"><span class="ln">136</span><span class="cl"><span class="k">esac</span>
</span></span><span class="line"><span class="ln">137</span><span class="cl"><span class="c1"># 查到的主機跟 host-&lt;名稱&gt;.env 最後記下的不是同一台時（例如在別處重建過），舊的 host key 紀錄對不上，也清掉</span>
</span></span><span class="line"><span class="ln">138</span><span class="cl"><span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$NEW</span><span class="s2">&#34;</span> !<span class="o">=</span> <span class="m">1</span> <span class="o">&amp;&amp;</span> <span class="s2">&#34;</span><span class="nv">$IID</span><span class="s2">&#34;</span> !<span class="o">=</span> <span class="s2">&#34;</span><span class="nv">$LAST_IID</span><span class="s2">&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">139</span><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;IID=</span><span class="nv">$IID</span><span class="s2">&#34;</span> &gt;&gt; <span class="s2">&#34;</span><span class="nv">$HOSTENV</span><span class="s2">&#34;</span><span class="p">;</span> : &gt; <span class="s2">&#34;</span><span class="nv">$KNOWN</span><span class="s2">&#34;</span><span class="p">;</span> <span class="nv">NEW</span><span class="o">=</span><span class="m">1</span>
</span></span><span class="line"><span class="ln">140</span><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="ln">141</span><span class="cl">
</span></span><span class="line"><span class="ln">142</span><span class="cl"><span class="c1"># 2. 私有 IP 與 VNIC：重建之後兩者都會換</span>
</span></span><span class="line"><span class="ln">143</span><span class="cl"><span class="c1"># 剛 RUNNING 的主機，VNIC 的掛載紀錄可能還沒出現，最多等 120 秒</span>
</span></span><span class="line"><span class="ln">144</span><span class="cl"><span class="k">for</span> <span class="o">((</span><span class="nv">i</span> <span class="o">=</span> 0<span class="p">;</span> i &lt; 12<span class="p">;</span> i++<span class="o">))</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">145</span><span class="cl">  <span class="nv">VNIC</span><span class="o">=</span><span class="k">$(</span>q compute instance list-vnics --instance-id <span class="s2">&#34;</span><span class="nv">$IID</span><span class="s2">&#34;</span> --query <span class="s1">&#39;data[0].id&#39;</span> --raw-output<span class="k">)</span> <span class="o">||</span> fail <span class="s2">&#34;查 VNIC&#34;</span>
</span></span><span class="line"><span class="ln">146</span><span class="cl">  <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$VNIC</span><span class="s2">&#34;</span> <span class="o">==</span> ocid1.vnic* <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">break</span>
</span></span><span class="line"><span class="ln">147</span><span class="cl">  sleep <span class="m">10</span>
</span></span><span class="line"><span class="ln">148</span><span class="cl"><span class="k">done</span>
</span></span><span class="line"><span class="ln">149</span><span class="cl"><span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$VNIC</span><span class="s2">&#34;</span> <span class="o">==</span> ocid1.vnic* <span class="o">]]</span> <span class="o">||</span> die <span class="s2">&#34;等了 120 秒仍查不到主機的 VNIC：</span><span class="nv">$VNIC</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">150</span><span class="cl"><span class="nv">IP</span><span class="o">=</span><span class="k">$(</span>q compute instance list-vnics --instance-id <span class="s2">&#34;</span><span class="nv">$IID</span><span class="s2">&#34;</span> --query <span class="s1">&#39;data[0].&#34;private-ip&#34;&#39;</span> --raw-output<span class="k">)</span> <span class="o">||</span> fail <span class="s2">&#34;查私有 IP&#34;</span>
</span></span><span class="line"><span class="ln">151</span><span class="cl">say 已存在 <span class="s2">&#34;VNIC 與私有 IP </span><span class="nv">$IP</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">152</span><span class="cl">
</span></span><span class="line"><span class="ln">153</span><span class="cl"><span class="c1"># 3. NAT gateway：VCN 裡同名而 AVAILABLE 的那一個；已存在或新建都記進 ids.env</span>
</span></span><span class="line"><span class="ln">154</span><span class="cl"><span class="nv">NAT</span><span class="o">=</span><span class="k">$(</span>q network nat-gateway list -c <span class="s2">&#34;</span><span class="nv">$C</span><span class="s2">&#34;</span> --vcn-id <span class="s2">&#34;</span><span class="nv">$V</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">155</span><span class="cl"><span class="se"></span>  --query <span class="s2">&#34;data[?\&#34;display-name\&#34;==&#39;</span><span class="nv">$NAT_NAME</span><span class="s2">&#39; &amp;&amp; \&#34;lifecycle-state\&#34;==&#39;AVAILABLE&#39;] | [0].id&#34;</span> --raw-output<span class="k">)</span> <span class="o">||</span> fail <span class="s2">&#34;查 NAT gateway&#34;</span>
</span></span><span class="line"><span class="ln">156</span><span class="cl"><span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$NAT</span><span class="s2">&#34;</span> <span class="o">==</span> ocid1.natgateway* <span class="o">]]</span><span class="p">;</span> <span class="k">then</span> say 已存在 <span class="s2">&#34;NAT gateway </span><span class="nv">$NAT_NAME</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">157</span><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="ln">158</span><span class="cl">  <span class="nv">NAT</span><span class="o">=</span><span class="k">$(</span>q network nat-gateway create -c <span class="s2">&#34;</span><span class="nv">$C</span><span class="s2">&#34;</span> --vcn-id <span class="s2">&#34;</span><span class="nv">$V</span><span class="s2">&#34;</span> --display-name <span class="s2">&#34;</span><span class="nv">$NAT_NAME</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">159</span><span class="cl"><span class="se"></span>    --wait-for-state AVAILABLE --query data.id --raw-output<span class="k">)</span> <span class="o">||</span> fail <span class="s2">&#34;建 NAT gateway&#34;</span>
</span></span><span class="line"><span class="ln">160</span><span class="cl">  <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$NAT</span><span class="s2">&#34;</span> <span class="o">==</span> ocid1.natgateway* <span class="o">]]</span> <span class="o">||</span> die <span class="s2">&#34;建不出 NAT gateway：</span><span class="nv">$NAT</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">161</span><span class="cl">  say 新建 <span class="s2">&#34;NAT gateway </span><span class="nv">$NAT_NAME</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">162</span><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="ln">163</span><span class="cl">record NAT <span class="s2">&#34;</span><span class="nv">$NAT</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">164</span><span class="cl">
</span></span><span class="line"><span class="ln">165</span><span class="cl"><span class="c1"># 4. 路由：私有子網路的路由表要有 service gateway 那一條與 0.0.0.0/0 → NAT；update 是整份替換，所以兩條一起寫。</span>
</span></span><span class="line"><span class="ln">166</span><span class="cl"><span class="c1">#   路由表裡還有這兩條以外的規則時，改寫會把它們刪掉，所以先數，有就停下來</span>
</span></span><span class="line"><span class="ln">167</span><span class="cl"><span class="nv">OTHER</span><span class="o">=</span><span class="k">$(</span>q network route-table get --rt-id <span class="s2">&#34;</span><span class="nv">$RT</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">168</span><span class="cl"><span class="se"></span>  --query <span class="s2">&#34;data.\&#34;route-rules\&#34;[?destination!=&#39;0.0.0.0/0&#39; &amp;&amp; destination!=&#39;</span><span class="nv">$SGCIDR</span><span class="s2">&#39;].destination&#34;</span> --raw-output<span class="k">)</span> <span class="o">||</span> fail <span class="s2">&#34;讀路由表&#34;</span>
</span></span><span class="line"><span class="ln">169</span><span class="cl"><span class="nv">OTHER</span><span class="o">=</span><span class="k">$(</span>tr -d <span class="s1">&#39;[]&#34; \n&#39;</span> <span class="o">&lt;&lt;&lt;</span><span class="s2">&#34;</span><span class="nv">$OTHER</span><span class="s2">&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="ln">170</span><span class="cl"><span class="o">[[</span> -n <span class="s2">&#34;</span><span class="nv">$OTHER</span><span class="s2">&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> die <span class="s2">&#34;路由表裡有 0.0.0.0/0 與 service gateway 以外的規則（目的地 </span><span class="si">${</span><span class="nv">OTHER</span><span class="si">}</span><span class="s2">），先把它們加進這一步的 --route-rules 再跑&#34;</span>
</span></span><span class="line"><span class="ln">171</span><span class="cl"><span class="nv">HAS</span><span class="o">=</span><span class="k">$(</span>q network route-table get --rt-id <span class="s2">&#34;</span><span class="nv">$RT</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">172</span><span class="cl"><span class="se"></span>  --query <span class="s2">&#34;data.\&#34;route-rules\&#34;[?destination==&#39;0.0.0.0/0&#39; &amp;&amp; \&#34;network-entity-id\&#34;==&#39;</span><span class="nv">$NAT</span><span class="s2">&#39;] | length(@)&#34;</span> --raw-output<span class="k">)</span> <span class="o">||</span> fail <span class="s2">&#34;讀路由表&#34;</span>
</span></span><span class="line"><span class="ln">173</span><span class="cl"><span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$HAS</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="m">1</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span> say 已存在 <span class="s2">&#34;路由 0.0.0.0/0 → </span><span class="nv">$NAT_NAME</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">174</span><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="ln">175</span><span class="cl">  q network route-table update --rt-id <span class="s2">&#34;</span><span class="nv">$RT</span><span class="s2">&#34;</span> --force --wait-for-state AVAILABLE --route-rules <span class="s2">&#34;[
</span></span></span><span class="line"><span class="ln">176</span><span class="cl"><span class="s2"> {\&#34;destination\&#34;:\&#34;</span><span class="nv">$SGCIDR</span><span class="s2">\&#34;,\&#34;destinationType\&#34;:\&#34;SERVICE_CIDR_BLOCK\&#34;,\&#34;networkEntityId\&#34;:\&#34;</span><span class="nv">$SG</span><span class="s2">\&#34;},
</span></span></span><span class="line"><span class="ln">177</span><span class="cl"><span class="s2"> {\&#34;destination\&#34;:\&#34;0.0.0.0/0\&#34;,\&#34;destinationType\&#34;:\&#34;CIDR_BLOCK\&#34;,\&#34;networkEntityId\&#34;:\&#34;</span><span class="nv">$NAT</span><span class="s2">\&#34;}]&#34;</span> &gt;/dev/null <span class="se">\
</span></span></span><span class="line"><span class="ln">178</span><span class="cl"><span class="se"></span>    <span class="o">||</span> fail <span class="s2">&#34;改寫路由表&#34;</span>
</span></span><span class="line"><span class="ln">179</span><span class="cl">  say 改寫 <span class="s2">&#34;路由表加上 0.0.0.0/0 → </span><span class="si">${</span><span class="nv">NAT_NAME</span><span class="si">}</span><span class="s2">（service gateway 那一條保留）&#34;</span>
</span></span><span class="line"><span class="ln">180</span><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="ln">181</span><span class="cl">
</span></span><span class="line"><span class="ln">182</span><span class="cl"><span class="c1"># 5. 出站 NSG：只有出站 TCP 80、443 兩條規則；已存在或新建都記進 ids.env</span>
</span></span><span class="line"><span class="ln">183</span><span class="cl"><span class="nv">NSGU</span><span class="o">=</span><span class="k">$(</span>q network nsg list -c <span class="s2">&#34;</span><span class="nv">$C</span><span class="s2">&#34;</span> --vcn-id <span class="s2">&#34;</span><span class="nv">$V</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">184</span><span class="cl"><span class="se"></span>  --query <span class="s2">&#34;data[?\&#34;display-name\&#34;==&#39;</span><span class="nv">$NSG_NAME</span><span class="s2">&#39; &amp;&amp; \&#34;lifecycle-state\&#34;==&#39;AVAILABLE&#39;] | [0].id&#34;</span> --raw-output<span class="k">)</span> <span class="o">||</span> fail <span class="s2">&#34;查 NSG&#34;</span>
</span></span><span class="line"><span class="ln">185</span><span class="cl"><span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$NSGU</span><span class="s2">&#34;</span> <span class="o">==</span> ocid1.networksecuritygroup* <span class="o">]]</span><span class="p">;</span> <span class="k">then</span> say 已存在 <span class="s2">&#34;NSG </span><span class="nv">$NSG_NAME</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">186</span><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="ln">187</span><span class="cl">  <span class="nv">NSGU</span><span class="o">=</span><span class="k">$(</span>q network nsg create -c <span class="s2">&#34;</span><span class="nv">$C</span><span class="s2">&#34;</span> --vcn-id <span class="s2">&#34;</span><span class="nv">$V</span><span class="s2">&#34;</span> --display-name <span class="s2">&#34;</span><span class="nv">$NSG_NAME</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">188</span><span class="cl"><span class="se"></span>    --wait-for-state AVAILABLE --query data.id --raw-output<span class="k">)</span> <span class="o">||</span> fail <span class="s2">&#34;建 NSG&#34;</span>
</span></span><span class="line"><span class="ln">189</span><span class="cl">  <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$NSGU</span><span class="s2">&#34;</span> <span class="o">==</span> ocid1.networksecuritygroup* <span class="o">]]</span> <span class="o">||</span> die <span class="s2">&#34;建不出 NSG：</span><span class="nv">$NSGU</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">190</span><span class="cl">  say 新建 <span class="s2">&#34;NSG </span><span class="nv">$NSG_NAME</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">191</span><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="ln">192</span><span class="cl">record NSGU <span class="s2">&#34;</span><span class="nv">$NSGU</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">193</span><span class="cl"><span class="k">for</span> port in <span class="m">80</span> 443<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">194</span><span class="cl">  <span class="nv">N</span><span class="o">=</span><span class="k">$(</span>q network nsg rules list --nsg-id <span class="s2">&#34;</span><span class="nv">$NSGU</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">195</span><span class="cl"><span class="se"></span>    --query <span class="s2">&#34;data[?direction==&#39;EGRESS&#39; &amp;&amp; \&#34;tcp-options\&#34;.\&#34;destination-port-range\&#34;.min==\`</span><span class="nv">$port</span><span class="s2">\`] | length(@)&#34;</span> --raw-output<span class="k">)</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">196</span><span class="cl"><span class="se"></span>    <span class="o">||</span> fail <span class="s2">&#34;讀 NSG 的規則&#34;</span>
</span></span><span class="line"><span class="ln">197</span><span class="cl">  <span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$N</span><span class="s2">&#34;</span> -ge <span class="m">1</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span> say 已存在 <span class="s2">&#34;NSG 出站 TCP </span><span class="nv">$port</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">198</span><span class="cl">  <span class="k">else</span>
</span></span><span class="line"><span class="ln">199</span><span class="cl">    q network nsg rules add --nsg-id <span class="s2">&#34;</span><span class="nv">$NSGU</span><span class="s2">&#34;</span> --security-rules <span class="s2">&#34;[{\&#34;direction\&#34;:\&#34;EGRESS\&#34;,\&#34;protocol\&#34;:\&#34;6\&#34;,
</span></span></span><span class="line"><span class="ln">200</span><span class="cl"><span class="s2">\&#34;destination\&#34;:\&#34;0.0.0.0/0\&#34;,\&#34;destinationType\&#34;:\&#34;CIDR_BLOCK\&#34;,\&#34;isStateless\&#34;:false,
</span></span></span><span class="line"><span class="ln">201</span><span class="cl"><span class="s2">\&#34;tcpOptions\&#34;:{\&#34;destinationPortRange\&#34;:{\&#34;min\&#34;:</span><span class="nv">$port</span><span class="s2">,\&#34;max\&#34;:</span><span class="nv">$port</span><span class="s2">}}}]&#34;</span> &gt;/dev/null <span class="se">\
</span></span></span><span class="line"><span class="ln">202</span><span class="cl"><span class="se"></span>      <span class="o">||</span> fail <span class="s2">&#34;加 NSG 出站 TCP </span><span class="nv">$port</span><span class="s2"> 的規則&#34;</span>
</span></span><span class="line"><span class="ln">203</span><span class="cl">    say 新建 <span class="s2">&#34;NSG 出站 TCP </span><span class="nv">$port</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">204</span><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="ln">205</span><span class="cl"><span class="k">done</span>
</span></span><span class="line"><span class="ln">206</span><span class="cl">
</span></span><span class="line"><span class="ln">207</span><span class="cl"><span class="c1"># 6. 把 NSG 掛到這台主機目前的 VNIC；--nsg-ids 是整份替換，所以保留 VNIC 原本掛著的其他 NSG</span>
</span></span><span class="line"><span class="ln">208</span><span class="cl"><span class="nv">CUR</span><span class="o">=</span><span class="k">$(</span>q network vnic get --vnic-id <span class="s2">&#34;</span><span class="nv">$VNIC</span><span class="s2">&#34;</span> --query <span class="s1">&#39;data.&#34;nsg-ids&#34;&#39;</span> --raw-output<span class="k">)</span> <span class="o">||</span> fail <span class="s2">&#34;讀 VNIC 的 NSG 清單&#34;</span>
</span></span><span class="line"><span class="ln">209</span><span class="cl"><span class="nv">CUR</span><span class="o">=</span><span class="k">$(</span>tr -d <span class="s1">&#39;[]&#34; \n&#39;</span> <span class="o">&lt;&lt;&lt;</span><span class="s2">&#34;</span><span class="nv">$CUR</span><span class="s2">&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="ln">210</span><span class="cl"><span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;,</span><span class="nv">$CUR</span><span class="s2">,&#34;</span> <span class="o">==</span> *<span class="s2">&#34;,</span><span class="nv">$NSGU</span><span class="s2">,&#34;</span>* <span class="o">]]</span><span class="p">;</span> <span class="k">then</span> say 已存在 <span class="s2">&#34;NSG 掛在 VNIC 上&#34;</span>
</span></span><span class="line"><span class="ln">211</span><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="ln">212</span><span class="cl">  <span class="nv">LIST</span><span class="o">=</span><span class="k">$(</span><span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$CUR</span><span class="s2">,</span><span class="nv">$NSGU</span><span class="s2">&#34;</span> <span class="p">|</span> sed <span class="s1">&#39;s/^,//&#39;</span> <span class="p">|</span> awk -F, <span class="s1">&#39;{for(i=1;i&lt;=NF;i++) printf &#34;%s\&#34;%s\&#34;&#34;, (i&gt;1?&#34;,&#34;:&#34;&#34;), $i}&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="ln">213</span><span class="cl">  q network vnic update --vnic-id <span class="s2">&#34;</span><span class="nv">$VNIC</span><span class="s2">&#34;</span> --nsg-ids <span class="s2">&#34;[</span><span class="nv">$LIST</span><span class="s2">]&#34;</span> --force &gt;/dev/null <span class="o">||</span> fail <span class="s2">&#34;改寫 VNIC 的 NSG 清單&#34;</span>
</span></span><span class="line"><span class="ln">214</span><span class="cl">  say 改寫 <span class="s2">&#34;VNIC 的 NSG 清單加上 </span><span class="nv">$NSG_NAME</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">215</span><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="ln">216</span><span class="cl">
</span></span><span class="line"><span class="ln">217</span><span class="cl"><span class="c1"># 7. port forwarding session：指向這台現在的私有 IP 與 22 port；session 最長 1 小時，每次重跑都開新的</span>
</span></span><span class="line"><span class="ln">218</span><span class="cl"><span class="nv">SPFU</span><span class="o">=</span><span class="k">$(</span>q bastion session create-port-forwarding --bastion-id <span class="s2">&#34;</span><span class="nv">$B</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">219</span><span class="cl"><span class="se"></span>  --target-private-ip <span class="s2">&#34;</span><span class="nv">$IP</span><span class="s2">&#34;</span> --target-port <span class="m">22</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">220</span><span class="cl"><span class="se"></span>  --ssh-public-key-file <span class="s2">&#34;</span><span class="nv">$D</span><span class="s2">/session_key.pub&#34;</span> --session-ttl <span class="m">3600</span> --display-name <span class="s2">&#34;</span><span class="nv">$NAME</span><span class="s2">-pf&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">221</span><span class="cl"><span class="se"></span>  --wait-for-state SUCCEEDED --wait-for-state FAILED --max-wait-seconds <span class="m">600</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">222</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;data.resources[0].identifier&#39;</span> --raw-output <span class="p">|</span> tail -1<span class="k">)</span> <span class="o">||</span> fail <span class="s2">&#34;建 port forwarding session&#34;</span>
</span></span><span class="line"><span class="ln">223</span><span class="cl"><span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$SPFU</span><span class="s2">&#34;</span> <span class="o">==</span> ocid1.bastionsession* <span class="o">]]</span> <span class="o">||</span> die <span class="s2">&#34;建不出 port forwarding session：</span><span class="nv">$SPFU</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">224</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;SPF=</span><span class="nv">$SPFU</span><span class="s2">&#34;</span> &gt;&gt; <span class="s2">&#34;</span><span class="nv">$HOSTENV</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">225</span><span class="cl">say 新建 <span class="s2">&#34;port forwarding session（1 小時後失效）&#34;</span>
</span></span><span class="line"><span class="ln">226</span><span class="cl">
</span></span><span class="line"><span class="ln">227</span><span class="cl"><span class="c1"># 8. ssh 設定：整份由範本產生；known_hosts 只在主機那一步（第 1 步）換了一台主機時清空（新建，</span>
</span></span><span class="line"><span class="ln">228</span><span class="cl"><span class="c1">#   或查到的主機跟 host-&lt;名稱&gt;.env 最後記下的不同），同一台主機還在時保留</span>
</span></span><span class="line"><span class="ln">229</span><span class="cl">cat &gt; <span class="s2">&#34;</span><span class="nv">$CONF</span><span class="s2">&#34;</span> <span class="s">&lt;&lt;EOF
</span></span></span><span class="line"><span class="ln">230</span><span class="cl"><span class="s"># 由 rebuild-ubuntu.sh 產生：經 Bastion 的 port forwarding session 登入 $NAME
</span></span></span><span class="line"><span class="ln">231</span><span class="cl"><span class="s">Host ${NAME}-bastion
</span></span></span><span class="line"><span class="ln">232</span><span class="cl"><span class="s">  HostName host.bastion.&lt;區域&gt;.oci.oraclecloud.com
</span></span></span><span class="line"><span class="ln">233</span><span class="cl"><span class="s">  User $SPFU
</span></span></span><span class="line"><span class="ln">234</span><span class="cl"><span class="s">  IdentityFile $D/session_key
</span></span></span><span class="line"><span class="ln">235</span><span class="cl"><span class="s">  IdentitiesOnly yes
</span></span></span><span class="line"><span class="ln">236</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">237</span><span class="cl"><span class="s">Host $NAME
</span></span></span><span class="line"><span class="ln">238</span><span class="cl"><span class="s">  HostName $IP
</span></span></span><span class="line"><span class="ln">239</span><span class="cl"><span class="s">  User ubuntu
</span></span></span><span class="line"><span class="ln">240</span><span class="cl"><span class="s">  IdentityFile $D/verify-a1-key
</span></span></span><span class="line"><span class="ln">241</span><span class="cl"><span class="s">  IdentitiesOnly yes
</span></span></span><span class="line"><span class="ln">242</span><span class="cl"><span class="s">  ProxyJump ${NAME}-bastion
</span></span></span><span class="line"><span class="ln">243</span><span class="cl"><span class="s">  UserKnownHostsFile $KNOWN
</span></span></span><span class="line"><span class="ln">244</span><span class="cl"><span class="s">  StrictHostKeyChecking accept-new
</span></span></span><span class="line"><span class="ln">245</span><span class="cl"><span class="s">EOF</span>
</span></span><span class="line"><span class="ln">246</span><span class="cl"><span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$NEW</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="m">1</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span> say 改寫 <span class="s2">&#34;ssh 設定 </span><span class="si">${</span><span class="nv">CONF</span><span class="si">}</span><span class="s2">；known_hosts 已清空（換了一台主機）&#34;</span>
</span></span><span class="line"><span class="ln">247</span><span class="cl"><span class="k">else</span> say 改寫 <span class="s2">&#34;ssh 設定 </span><span class="si">${</span><span class="nv">CONF</span><span class="si">}</span><span class="s2">；known_hosts 保留&#34;</span><span class="p">;</span> <span class="k">fi</span>
</span></span><span class="line"><span class="ln">248</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;下一步：ssh -F </span><span class="nv">$CONF</span><span class="s2"> </span><span class="nv">$NAME</span><span class="s2"> &#39;cloud-init status --wait; uptime&#39;&#34;</span></span></span></code></pre></div><p>腳本放在跟 <code>ids.env</code>、金鑰同一個目錄執行。要依環境換或確認的位置：</p>
<ul>
<li><strong><code>&lt;可用網域&gt;</code></strong>（<code>oci iam availability-domain list</code> 查）與兩處 <strong><code>&lt;區域&gt;</code></strong>（查法在〈前置條件：沿用 Oracle Cloud 建立主機實測紀錄的網路、Bastion 與金鑰〉的 CLI 登入那一項）。</li>
<li><strong><code>--profile DEFAULT</code></strong>：<code>oci session authenticate</code> 時取的 profile 名稱，另外取了名字就兩處都換。</li>
<li><strong><code>NAT_NAME</code> 與 <code>NSG_NAME</code></strong>：要跟〈加出站：NAT gateway、路由規則與只掛在這台主機上的 NSG〉建 NAT gateway 與 NSG 時用的名稱相同。名稱不同的話，腳本依名稱找不到既有的那一份，會再建一份 NAT gateway 與 NSG。</li>
<li><strong>規格與映像檔</strong>：<code>--shape-config</code> 的 1 OCPU、6 GB，以及查映像檔的 <code>&quot;Canonical Ubuntu&quot;</code>、<code>&quot;24.04&quot;</code>、<code>VM.Standard.A1.Flex</code> 三個篩選字串；換成別的作業系統時，產生 ssh 設定那一步的 <code>User ubuntu</code> 也要跟著換（Oracle Linux 是 <code>opc</code>）。</li>
<li><strong><code>BOOT_GB</code></strong>：只在新建主機時有作用，寫在指令前面（<code>BOOT_GB=60 ./rebuild-ubuntu.sh &lt;主機名稱&gt;</code>）。不給時開機磁碟是映像檔的預設大小，Ubuntu 24.04 扣區塊磁碟額度 47 GB；給了之後新主機的開機磁碟就是這個大小，<code>60</code> 實際建出 60 GiB（<code>lsblk -b</code> 讀到 64424509440 位元組，實跑）。<code>--boot-volume-size-in-gbs</code>、<code>size-in-gbs</code> 與額度用量寫的 GB，數值上都是 GiB（帳務明細與價目表的「GB」是哪一種沒有查）；映像檔預設的「50 GB」是十進位的 50.0 GB，也就是 46.6 GiB、<code>size-in-gbs</code> 47，所以 <code>BOOT_GB=50</code> 已經比預設大、扣 50（從 60 的實跑推論，50 沒有試）。主控台建立頁自訂大小的下限是 50 GB，出處見 <a href="/blog/devops/infra/cloud-providers/vm-options/storage/" data-link-title="磁碟：開機磁碟與區塊磁碟的大小、效能、加密與去留" data-link-desc="雲端主機的磁碟選項：開機磁碟的大小與只能加大的限制、效能等級與費用的關係、靜態加密與傳輸中加密、另外掛載的區塊磁碟，以及刪除主機時開機磁碟是否預設一起刪除">磁碟：開機磁碟與區塊磁碟的大小、效能、加密與去留</a>〈開機磁碟的大小〉；給小於 50 的值時 CLI 回什麼，這次沒有試。加大之後，Ubuntu 的根目錄在開機時自動延伸到整顆磁碟（<code>df -h /</code> 讀到 58G）；換成別的映像檔時，檔案系統可能要另外延伸（Oracle Linux 的 <code>oci-growfs</code> 這類工具），否則多出的容量用不到（沒有實跑）。加大之前先讀免費額度還剩多少（<code>total-free-storage-gb-regional</code> 的 <code>available</code>，額度算的是開機磁碟與區塊磁碟的合計，指令在〈費用與額度：A1、開機磁碟與區塊儲存額度、出站流量與 NAT gateway〉）：新磁碟比 <code>available</code> 大時，這一顆放不進剩餘額度，整顆不算免費額度。</li>
<li><strong><code>--session-ttl 3600</code></strong>：不能大於 Bastion 的 session 上限，查法在〈建 Ubuntu 24.04 主機與經由 port forwarding 登入〉。</li>
<li><strong>金鑰的檔名</strong>：<code>verify-a1-key</code>（登入主機）與 <code>session_key</code>（Bastion session）；序列主控台連線的重建腳本另外用 <code>console_rsa</code>，不存在時以 RSA 3072 位元（<code>-b 3072</code>）產生。</li>
<li><strong><code>ids.env</code> 裡七個鍵的值</strong>。</li>
</ul>
<p><code>ids.env</code> 的鍵與查法在〈前置條件：沿用 Oracle Cloud 建立主機實測紀錄的網路、Bastion 與金鑰〉的表，格式是一行一個 <code>鍵=值</code>（例如 <code>C=&lt;compartment 的 OCID&gt;</code>），腳本開頭 <code>source</code> 它。NAT gateway 與 NSG 不論是已存在還是腳本新建的，腳本都把它們的 OCID 以 <code>NAT=</code> 與 <code>NSGU=</code> 記進檔尾，已經有同樣的一行就不重複寫，所以跑過一次之後，〈刪除這組資源的順序〉要填的值都在這個檔裡。<code>host-&lt;名稱&gt;.env</code> 是這一台主機的紀錄：腳本每次新建主機或 session 就在檔尾多加一行 <code>IID=</code> 或 <code>SPF=</code>，同一個鍵出現好幾次時，最後一行才是現在有效的值。腳本只讀它最後一行的 <code>IID=</code>，拿來跟這次查到的主機比對：不是同一台時（新建，或主機在別處重建過），舊的 host key 紀錄對不上，腳本就清空 known_hosts 並補記這次的 <code>IID=</code>。檔案裡還沒有任何一行 <code>IID=</code> 時（主機不是重建腳本建的，例如照〈建 Ubuntu 24.04 主機與經由 port forwarding 登入〉手動建的，或是腳本較早的版本建的），腳本拿空字串比對，同樣判成不是同一台、清空一次。</p>
<p>兩支腳本都假設 CLI 用 session token 登入（〈前置條件：沿用 Oracle Cloud 建立主機實測紀錄的網路、Bastion 與金鑰〉的 CLI 登入那一項）。改用 API 簽章金鑰的話，拿掉三處：<code>export</code> 那一行的 <code>OCI_CLI_AUTH=security_token</code>、開頭的 <code>oci session validate</code> 檢查，以及重建腳本重送迴圈裡每 1800 秒跑一次的 <code>oci session refresh</code>；拿掉之後 CLI 改用設定檔裡的 API 簽章金鑰（沒有實跑）。</p>
<p>重建腳本改寫路由表時，<code>--route-rules</code> 只寫 service gateway 與 NAT gateway 兩條，前提是這張路由表原本只有 service gateway 那一條（加上之前跑腳本時加進去的 0.0.0.0/0 那一條）。改寫之前，腳本先列出這兩條以外的規則，列得出任何一條就停下來，印出那幾條的目的地：<code>route-table update</code> 是整份替換，沒寫進 <code>--route-rules</code> 的規則會被刪掉。這時把那幾條規則加進腳本裡的 <code>--route-rules</code>，再重跑。</p>
<p><strong>輸出的讀法</strong>：主機那一步（第 1 步）依查到的狀態印不同的行：</p>
<ul>
<li><strong>主機已經是 <code>RUNNING</code></strong>：印「已存在」。</li>
<li><strong>主機被停止（<code>STOPPED</code>）</strong>：印「啟動」（<code>STOPPED → RUNNING</code>）。<code>STOPPING</code> 時先等到 <code>STOPPED</code> 再啟動，<code>STARTING</code>、<code>PROVISIONING</code> 時等到 <code>RUNNING</code> 再印「已存在」。啟動失敗時（例如容量不足，推論）印出 <code>instance action</code> 的錯誤並以 2 結束。<code>STOPPED</code>、<code>STOPPING</code>、<code>STARTING</code>、<code>PROVISIONING</code> 四條在雲端跑過（結果在下面「在雲端實跑的結果」的「名稱不存在、同時另一份腳本碰到 <code>PROVISIONING</code>」與「主機 <code>STOPPED</code>、<code>STOPPING</code>、<code>STARTING</code>」兩項）；啟動失敗只用假的 <code>oci</code> 測過。</li>
<li><strong>找不到還沒終止的同名主機</strong>：印「新建」與映像檔名稱，容量不足（500）或被限流（429）時中間會有幾行「重送」（例：<code>[重送] 第 1 次 500：&quot;message&quot;: &quot;Out of host capacity.&quot;，112 秒後再送</code>，這一行出自假的 <code>oci</code>）。建立請求回 500、429、401 以外的回應碼時（例如 <code>&lt;可用網域&gt;</code> 沒換掉而回 400），腳本印出回應碼與 <code>message</code> 就以 4 結束，不重送：這類回應代表請求本身有誤，重送幾次都是同一個結果，照訊息改好參數再跑。</li>
<li><strong>主機停在其他狀態</strong>（<code>TERMINATING</code>、<code>MOVING</code>、<code>CREATING_IMAGE</code> 這類）：腳本不猜下一步，印出狀態並以 2 結束，等它變成 <code>RUNNING</code>、<code>STOPPED</code> 或 <code>TERMINATED</code> 再重跑。</li>
</ul>
<p>之後的各步，「已存在」代表那一項不必動，「新建」與「改寫」代表腳本這一次在雲端建了或改了資源：</p>
<ul>
<li><strong>VNIC 與私有 IP</strong>（第 2 步）：一律印「已存在」與私有 IP；主機重建過時這個 IP 是新的。</li>
<li><strong>NAT gateway</strong>（第 3 步）與 <strong>NSG 和它的兩條規則</strong>（第 5 步）：VCN 裡已經有同名的就印「已存在」；只有第一次跑、或有人刪掉了它們時才印「新建」。</li>
<li><strong>路由</strong>（第 4 步）：已經有 0.0.0.0/0 指向 NAT gateway 那一條就印「已存在」；印「改寫」代表路由表被整份替換成 service gateway 與 NAT gateway 兩條，也就是先前少了 0.0.0.0/0 那一條，或它指向別的出口。</li>
<li><strong>NSG 掛到 VNIC</strong>（第 6 步）：印「改寫」代表 VNIC 原本沒有掛這個 NSG，主機重建之後的新 VNIC 一定走這一條。</li>
<li><strong>port forwarding session</strong>（第 7 步）：每次都印「新建」。</li>
<li><strong>ssh 設定</strong>（第 8 步）：每次都印「改寫」，並註明 known_hosts 是保留還是清空；清空代表這次的主機跟 <code>host-&lt;名稱&gt;.env</code> 最後記下的不是同一台，或那個檔案還沒有記下任何一台（主機不是重建腳本建的；實跑結果在下面的 <code>STOPPED</code> 那一項）。</li>
</ul>
<p>任何一步印「錯誤」時腳本以 2 結束（建立請求被拒的 4 在上一項），錯誤那一行寫出是哪一步；<code>oci</code> 失敗時，冒號後面接的是那條指令寫在 stderr 的內容。修好之後重跑整支腳本即可，前面已經完成的步驟會印「已存在」。例外是建立請求的回應讀不出回應碼（exit 3）：設定檔錯誤、網路中斷都會走到這裡，而網路中斷時請求可能已經在 Oracle 那一端成功，所以重跑之前先用 <code>oci compute instance list -c &lt;compartment 的 OCID&gt; --display-name &lt;主機名稱&gt;</code> 確認沒有還在的同名主機。</p>
<p><strong>在雲端實跑的結果</strong>（「主機已經是 <code>RUNNING</code>，沒有等待迴圈的版本」與「名稱不存在，較早的兩個版本」兩項跑的是上面三個版本裡較早的兩個，其餘都是現在這一版；各項用的版本寫在項目裡）：</p>
<ul>
<li>
<p><strong>NAT gateway 與 NSG 都不存在</strong>（現在這一版，2026-10-27 09:32，跑的是 <code>./rebuild-ubuntu.sh</code>，不給名稱就是 <code>verify-ubuntu</code>；它的 NAT gateway 與 NSG 已在凌晨照〈刪除這組資源的順序〉刪掉）。exit 0，全程 32 秒。主機那一步印「已存在」（<code>RUNNING</code>），NAT gateway 印「新建」，路由印「改寫」（加上 0.0.0.0/0 → NAT gateway <code>verify-nat</code>，名稱是腳本的 <code>NAT_NAME</code>；保留 service gateway 那一條），NSG 與它的出站 TCP 80、443 兩條規則都印「新建」，掛 NSG 那一步印「改寫」；新的 NAT gateway 與 NSG 的 OCID 以新的一行接在 <code>ids.env</code> 檔尾。登入之後 <code>apt-get update</code> 下載 3982 kB，<code>curl https://archive.ubuntu.com/ubuntu/</code> 回 200，出站恢復。從 NAT gateway 那一行到掛 NSG 那一行共 14 秒，NAT gateway 與 NSG 的 <code>create</code> 帶的 <code>--wait-for-state AVAILABLE</code> 各約 3 秒（從 log 的時間戳推論）。</p>
</li>
<li>
<p><strong>名稱不存在、同時另一份腳本碰到 <code>PROVISIONING</code></strong>（現在這一版，2026-10-27 09:33，接在上一項之後，所以 NAT gateway 與 NSG 已經存在）。先在背景跑一份新建主機的腳本（下面稱「新建的那一份」），本機另開一個 shell 迴圈查同名主機的狀態，查到 <code>PROVISIONING</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"># 迴圈裡的 oci 跟重建腳本一樣用 session token 登入</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nb">export</span> <span class="nv">OCI_CLI_AUTH</span><span class="o">=</span>security_token
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># 新建的那一份：放到背景跑，開機磁碟 60 GB；APID 記下它的程序編號</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nv">BOOT_GB</span><span class="o">=</span><span class="m">60</span> ./rebuild-ubuntu.sh verify-ubuntu-60g &gt; new.log 2&gt;<span class="p">&amp;</span><span class="m">1</span> <span class="p">&amp;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="nv">APID</span><span class="o">=</span><span class="nv">$!</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># 新建的那一份還在跑時，每次查完等 3 秒再查（這次實際約 4 秒一次）；</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># 查到 PROVISIONING 就跑建立中才開跑的那一份。新建的那一份提早結束（例如建立請求回 400）時迴圈跟著結束</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="k">while</span> <span class="nb">kill</span> -0 <span class="s2">&#34;</span><span class="nv">$APID</span><span class="s2">&#34;</span> 2&gt;/dev/null<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="nv">st</span><span class="o">=</span><span class="k">$(</span>oci compute instance list -c &lt;compartment 的 OCID&gt; --display-name verify-ubuntu-60g <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>    --query <span class="s1">&#39;data[?&#34;lifecycle-state&#34;!=`TERMINATED`] | [0].&#34;lifecycle-state&#34;&#39;</span> --raw-output 2&gt;/dev/null<span class="k">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$st</span><span class="s2">&#34;</span> <span class="o">==</span> PROVISIONING <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="o">{</span> ./rebuild-ubuntu.sh verify-ubuntu-60g &gt; provisioning.log 2&gt;<span class="p">&amp;</span>1<span class="p">;</span> break<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  sleep <span class="m">3</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="k">done</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="nb">wait</span> <span class="s2">&#34;</span><span class="nv">$APID</span><span class="s2">&#34;</span></span></span></code></pre></div><p>新建的那一份開跑後第 6 秒，迴圈還查不到主機；第 10 秒查到 <code>PROVISIONING</code>，建立中才開跑的那一份隨即開跑。這一台讓區塊磁碟的合計變成 201，60 GB 那一顆整顆不算免費額度（〈費用與額度：A1、開機磁碟與區塊儲存額度、出站流量與 NAT gateway〉的「合計超過 200 GB 時」），所以照做這個測試會多出一顆不在免費額度裡的磁碟；這台在帳務明細出現它的計費項目之後，於 2026-10-27 11:58 終止，開機磁碟一起刪除（work request 65 秒 <code>SUCCEEDED</code>），區塊磁碟的兩條用量都回到 141；帳務的結果在〈費用與額度：A1、開機磁碟與區塊儲存額度、出站流量與 NAT gateway〉的「合計超過 200 GB 時」。</p>
<ul>
<li><strong>新建的那一份</strong>：建立請求第一次送出就成功，exit 0，全程 87 秒。從印出「新建」主機（送出建立請求之前）到印出「已 RUNNING」約 50 秒。建立請求不帶 <code>--wait-for-state</code>，這段時間由 <code>wait_state</code> 每 10 秒查一次狀態，所以它輪詢了好幾次，建立中才開跑的那一份的 <code>wait_state</code> 在同一段時間也一樣（從時間戳與腳本推論，腳本不記每一次查詢）。這兩份腳本的 <code>wait_state</code> 是它的輪詢第一次在雲端實際等待，先前幾次都在第一次查詢就拿到目標狀態。網路那幾步都印「已存在」，掛 NSG 那一步印「改寫」（新主機的 VNIC），port forwarding session 從上一行到印出「新建」17 秒，ssh 設定印「known_hosts 已清空（換了一台主機）」。</li>
<li><strong>建立中才開跑的那一份</strong>：查到主機是 <code>PROVISIONING</code>，走進 <code>STARTING|PROVISIONING</code> 那一條等到 <code>RUNNING</code>；它的 log 第一行（主機那一步）在新建的那一份印出「已 RUNNING」之後 4 秒印出「已存在」（等到 <code>RUNNING</code>），沒有再送建立請求，tenancy 裡只有一台 <code>verify-ubuntu-60g</code>。之後各步都印「已存在」，連掛 NSG 那一步也是（新建的那一份在 3 秒前掛上了），exit 0，全程 80 秒。建立中才開跑的那一份開跑時，新建的那一份已經把新主機的 <code>IID=</code> 寫進 <code>host-verify-ubuntu-60g.env</code>，所以建立中才開跑的那一份判成同一台，印「known_hosts 保留」（從它印「保留」、這個檔只有一行 <code>IID=</code>，以及腳本在開頭讀上一次的 <code>IID</code> 推得）。兩份腳本各建了一個 port forwarding session，<code>host-verify-ubuntu-60g.env</code> 因此有兩行 <code>SPF=</code>，ssh 設定是後寫的這一份。</li>
<li><strong>登入之後</strong>：照產生的 ssh 設定登入，<code>cloud-init status --wait</code> 正常結束（輸出沒有留下），<code>apt-get update</code> 照常連到套件來源；<code>df -h /</code> 印出根目錄 58G。</li>
<li><strong>兩份同時跑的邊界</strong>：這次只驗了後一份在 <code>PROVISIONING</code> 出現之後才開跑的情形。新建的那一份查到沒有同名主機之後、新主機在 <code>instance list</code> 查得到之前有一段空窗（這次迴圈在新建的那一份開跑後第 6 秒還查不到，空窗本身多長沒有量），後一份的查詢落在這段空窗時，結果是空的，會再送一次建立請求，建出第二台同名主機，A1 用量與開機磁碟額度都多扣一台主機的量。NAT gateway 與 NSG 是整個 VCN 共用、同樣依名稱先查後建：兩份腳本（名稱不同也一樣）在它們還不存在時同時跑，會各建一份，<code>ids.env</code> 記下兩行、<code>source</code> 時取後一行，路由表的「改寫」互相覆蓋，路由可能指向沒有記下的那一份。這兩種都是依腳本邏輯推論，沒有實跑；這次實跑時 NAT gateway 與 NSG 已經存在。所以同一個 VCN 一次只跑一份重建腳本。</li>
<li><strong>建立中的主機沒有變成 <code>RUNNING</code></strong>：<code>wait_state</code> 只比對 <code>RUNNING</code>，建立失敗、主機從 <code>PROVISIONING</code> 變成 <code>TERMINATED</code> 時，兩份腳本都會等滿 900 秒，再以「沒有變成 RUNNING」結束，訊息裡的狀態是最後一次查到的值（依腳本邏輯推論，沒有實跑）。</li>
<li><strong>不給 <code>BOOT_GB</code> 的新建</strong>（現在這一版，2026-10-27 13:04 實跑）：不給 <code>BOOT_GB</code> 時 <code>BOOT_ARGS</code> 是空陣列，建立請求那一行展開空陣列的寫法先前只用假的 <code>oci</code> 與本機的 bash 3.2 測過（寫法的理由在下面「腳本裡幾處寫法的理由」的 <code>BOOT_ARGS</code> 那一項）。在 tenancy 裡已有三台 A1、免費區塊儲存扣了 141 的狀態下，用 <code>env -u BOOT_GB /bin/bash ./rebuild-ubuntu.sh verify-ubuntu-nb</code> 跑（<code>env -u</code> 確保 shell 裡沒有殘留的 <code>BOOT_GB</code>）：建立請求第一次送出就成功，從印出「新建」到「已 RUNNING」37 秒，exit 0，log 第一行到最後一行 68 秒；網路那幾步都印「已存在」，掛 NSG 那一步印「改寫」。登入之後 <code>cloud-init status --wait</code> 回 <code>done</code>，<code>df -h /</code> 的根目錄是 45G；<code>oci bv boot-volume list</code> 列出這顆開機磁碟的 <code>size-in-gbs</code> 是 47，免費區塊儲存的用量從 141 變成 188，跟手動照預設大小建的主機相同。這台主機之後拿來測付費磁碟的自動轉換（〈費用與額度：A1、開機磁碟與區塊儲存額度、出站流量與 NAT gateway〉），測完已終止。</li>
</ul>
</li>
<li>
<p><strong>主機 <code>STOPPED</code>、<code>STOPPING</code>、<code>STARTING</code></strong>（現在這一版，2026-10-27 對 <code>verify-ubuntu</code> 連跑四次，<code>STARTING</code> 試了兩次；每次之前用 <code>oci compute instance action --instance-id &lt;主機的 OCID&gt; --action STOP</code> 或 <code>--action START</code> 讓主機落在那個狀態）。四次都是 exit 0，網路設定那幾步都印「已存在」，port forwarding session 每次都印「新建」。各次全程的秒數不同，差在 API 回應的快慢：port forwarding session 那一行以外，log 各行的時間戳之間都不到 10 秒，沒有任何一步走進 <code>sleep</code>，而 port forwarding session 那一行就從 8 秒到 22 秒不等。下面的「從腳本開始到這一行」是用 log 最後一行的時間減去全程秒數推算的，時間戳只到秒，誤差約 1 秒：</p>
<ul>
<li><strong><code>STOPPED</code></strong>：先帶 <code>--wait-for-state STOPPED</code> 停機，6 秒回 <code>STOPPED</code>。腳本全程 28 秒，第一行印「啟動」（<code>STOPPED → RUNNING</code>），從腳本開始到這一行約 9 秒，<code>instance action</code> 的 START 等到 <code>RUNNING</code> 包含在內。登入時 <code>uptime</code> 是 <code>up 0 min</code>，<code>cloud-init status --wait</code> 回 <code>done</code>，<code>apt-get update</code> 照常連到套件來源。</li>
<li><strong><code>STOPPING</code></strong>：送出 STOP 不帶 <code>--wait-for-state</code>，CLI 回傳時是 <code>STOPPING</code>，再查一次仍是 <code>STOPPING</code>，接著跑腳本。腳本查到 <code>STOPPING</code>，全程 35 秒，第一行印「啟動」（<code>STOPPING → RUNNING</code>），從腳本開始到這一行約 9 秒。這 9 秒裡有登入檢查、查主機、查狀態、<code>wait_state</code> 的查詢與 START 共五次 API 呼叫，<code>wait_state</code> 要是睡過一次 10 秒就不可能少於 10 秒，所以它第一次查時主機已經是 <code>STOPPED</code>，沒有進 <code>sleep</code>（從時間戳推論）。</li>
<li><strong><code>STARTING</code></strong>：試了兩次，第二次才走到這條分支。兩次都是主機停好之後送出 START 不帶 <code>--wait-for-state</code>。第一次在送出 START 與跑腳本之間多查了一次狀態（回 <code>STARTING</code>），腳本開始時主機已經是 <code>RUNNING</code>，第一行印「已存在」（<code>RUNNING</code>），全程 21 秒，走的是主機已經是 <code>RUNNING</code> 的那一條；從腳本開始到第一行只有約 2 秒，所以 <code>STARTING</code> 在多查的那一次之後幾秒內就變成 <code>RUNNING</code>（推論，沒有量 <code>STARTING</code> 維持多久）。第二次送出 START 之後立刻跑腳本：腳本查到 <code>STARTING</code>，進 <code>wait_state</code> 之後第一次查就已經是 <code>RUNNING</code>，沒有進到兩次查詢之間的 10 秒 <code>sleep</code>（從腳本開始到第一行只有約 4 秒推論）；全程 55 秒，第一行印「已存在」（等到 <code>RUNNING</code>）。</li>
<li><strong>VNIC 的等待迴圈</strong>：四次的 VNIC 那一行都在主機那一行之後 3 到 9 秒內印出，主機沒有換過、VNIC 一直掛著，迴圈第一次查就拿到，沒有進 <code>sleep</code>（從時間戳推論）。</li>
<li><strong>known_hosts</strong>：<code>STOPPED</code> 那一次印「known_hosts 已清空（換了一台主機）」，而主機是同一台。<code>verify-ubuntu</code> 是手動建的，之後只跑過腳本較早的版本（那一版只在新建主機時記 <code>IID</code>），<code>host-verify-ubuntu.env</code> 裡只有 session 的紀錄、沒有 <code>IID</code> 那一行，腳本拿空字串跟查到的 OCID 比，判成不同，同時寫進這一行；之後三次都印「保留」。下面假的 <code>oci</code> 測試表裡「主機 <code>STOPPED</code>」那一列印「保留」，是因為那一組測試的 <code>host-&lt;名稱&gt;.env</code> 已經記著同一台的 <code>IID</code>。清空之後，ssh 設定的 <code>StrictHostKeyChecking accept-new</code> 讓下一次登入重新記下這台的 host key（<code>Permanently added '&lt;私有 IP&gt;' (ED25519)</code>），登入沒有受影響；主機沒有重裝，host key 應該沒換，但清空之前的紀錄已經是空的，沒有舊的 key 可以比對。</li>
</ul>
</li>
<li>
<p><strong>主機已經是 <code>RUNNING</code>，沒有等待迴圈的版本</strong>：exit 0，全程 34 秒；主機那一步到掛 NSG 那一步都印「已存在」，port forwarding session 的建立佔了其中約 17 秒，最後兩行是 session 的「新建」與 ssh 設定的「改寫」。照產生的 <code>ssh-verify-ubuntu.conf</code> 登入之後，<code>apt-get update</code> 照常連到 Ubuntu 的套件來源。</p>
</li>
<li>
<p><strong>名稱不存在，較早的兩個版本</strong>（2026-10-27 凌晨建 <code>verify-ubuntu-rebuild</code>）：建立請求第一次送出就成功。沒有等待迴圈的版本在印出「已 RUNNING」之後，接著以「查不到主機的 VNIC」結束（exit 2）；加上等待迴圈的版本從主機已經是 <code>RUNNING</code> 的狀態接著跑，exit 0，80 秒：掛 NSG 那一步印「改寫」，替新主機的 VNIC 掛上出站 NSG；port forwarding session 這一次要 64 秒才變成 <code>SUCCEEDED</code>，比既有主機的那一次長。登入時主機已開機 3 分鐘，<code>cloud-init status --wait</code> 立刻回 <code>done</code>，<code>apt-get update</code> 9 秒下載 39.9 MB，速率 4468 kB/s。</p>
</li>
</ul>
<p><strong>用假的 <code>oci</code> 測的結果</strong>（現在這一版；假的 <code>sleep</code> 只記秒數，所以秒數是腳本算出來要等的值。重送前的等待依回應碼調整，再加減 15 秒的隨機抖動，每次跑的值不同）。主機那一步與建立請求的情形：</p>
<table>
  <thead>
      <tr>
          <th>情形</th>
          <th>結束碼</th>
          <th>建立請求送出次數</th>
          <th>記下的等待</th>
          <th>known_hosts</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>名稱不存在，新主機先回 <code>PROVISIONING</code></td>
          <td>0</td>
          <td>1</td>
          <td><code>wait_state</code> 等 10 秒一次</td>
          <td>清空</td>
      </tr>
      <tr>
          <td>名稱不存在，加 <code>BOOT_GB=60</code></td>
          <td>0</td>
          <td>1</td>
          <td><code>wait_state</code> 等 10 秒一次</td>
          <td>清空</td>
      </tr>
      <tr>
          <td>主機已經是 <code>RUNNING</code></td>
          <td>0</td>
          <td>0</td>
          <td>無</td>
          <td>保留</td>
      </tr>
      <tr>
          <td>主機 <code>STOPPED</code></td>
          <td>0</td>
          <td>0</td>
          <td>無（<code>instance action</code> 啟動）</td>
          <td>保留</td>
      </tr>
      <tr>
          <td>主機 <code>TERMINATING</code></td>
          <td>2</td>
          <td>0</td>
          <td>無</td>
          <td>保留</td>
      </tr>
      <tr>
          <td>建立請求先回 500、第二次成功</td>
          <td>0</td>
          <td>2</td>
          <td>重送前 95–125 秒，之後 10 秒</td>
          <td>清空</td>
      </tr>
      <tr>
          <td>建立請求的輸出在 OCID 前面多一行警告</td>
          <td>0</td>
          <td>1</td>
          <td><code>wait_state</code> 等 10 秒一次</td>
          <td>清空</td>
      </tr>
      <tr>
          <td>建立請求的輸出讀不出回應碼</td>
          <td>3</td>
          <td>1</td>
          <td>無</td>
          <td>保留</td>
      </tr>
      <tr>
          <td>主機 <code>PROVISIONING</code>、再查一次變 <code>RUNNING</code></td>
          <td>0</td>
          <td>0</td>
          <td><code>wait_state</code> 等 10 秒一次</td>
          <td>保留</td>
      </tr>
      <tr>
          <td>主機 <code>STOPPING</code>、再查一次變 <code>STOPPED</code></td>
          <td>0</td>
          <td>0</td>
          <td>無（<code>instance action</code> 啟動）</td>
          <td>保留</td>
      </tr>
      <tr>
          <td>建立請求回 401</td>
          <td>2</td>
          <td>1</td>
          <td>無</td>
          <td>保留</td>
      </tr>
      <tr>
          <td>建立請求回 400</td>
          <td>4</td>
          <td>1</td>
          <td>無</td>
          <td>保留</td>
      </tr>
      <tr>
          <td>建立請求先回 500、第二次回 400</td>
          <td>4</td>
          <td>2</td>
          <td>重送前 95–125 秒</td>
          <td>保留</td>
      </tr>
      <tr>
          <td>主機 <code>STARTING</code>、再查一次變 <code>RUNNING</code></td>
          <td>0</td>
          <td>0</td>
          <td>無</td>
          <td>保留</td>
      </tr>
      <tr>
          <td>主機查得到、狀態讀不出</td>
          <td>2</td>
          <td>0</td>
          <td>無</td>
          <td>保留</td>
      </tr>
      <tr>
          <td>建立請求連回兩次 429、第三次成功</td>
          <td>0</td>
          <td>3</td>
          <td>重送前約 240 與 480 秒</td>
          <td>清空</td>
      </tr>
      <tr>
          <td>查不到映像檔</td>
          <td>2</td>
          <td>0</td>
          <td>無</td>
          <td>保留</td>
      </tr>
      <tr>
          <td>主機 <code>RUNNING</code>，但跟上次記下的不是同一台</td>
          <td>0</td>
          <td>0</td>
          <td>無</td>
          <td>清空</td>
      </tr>
  </tbody>
</table>
<p>加 <code>BOOT_GB=60</code> 的那一列，假的 <code>oci</code> 記下的建立請求帶著 <code>--boot-volume-size-in-gbs 60</code>；沒有給 <code>BOOT_GB</code> 的那幾列，建立請求裡沒有這個選項。每一種會送出建立請求的情形，第一次送出之前都先延長了一次 token（<code>last_refresh=0</code>）；回 400 的兩種情形印出的 <code>message</code> 是假的 <code>oci</code> 自己編的，Oracle 實際回的訊息這次沒有看過。查不到映像檔時，錯誤那一行是「查不到 Ubuntu 24.04 的 A1 映像檔：查詢結果是空的」。</p>
<p>網路設定那幾步的情形，以及讓單一種 <code>oci</code> 呼叫失敗的情形（主機都已經是 <code>RUNNING</code>）：</p>
<table>
  <thead>
      <tr>
          <th>情形</th>
          <th>結束碼</th>
          <th>結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>NAT gateway 與 NSG 都不存在、路由表少一條</td>
          <td>0</td>
          <td>新建 NAT gateway、NSG 與兩條規則，改寫路由表與 VNIC；兩者的 OCID 記進 <code>ids.env</code></td>
      </tr>
      <tr>
          <td>路由表少了 0.0.0.0/0 那一條</td>
          <td>0</td>
          <td>改寫路由表，寫進去的 service gateway 目的地是 <code>ids.env</code> 裡 <code>SGCIDR</code> 的值</td>
      </tr>
      <tr>
          <td>路由表裡有 0.0.0.0/0 與 service gateway 以外的規則</td>
          <td>2</td>
          <td>不改寫，印出那條規則的目的地</td>
      </tr>
      <tr>
          <td>VNIC 已經掛著另外兩個 NSG</td>
          <td>0</td>
          <td><code>vnic update</code> 的 <code>--nsg-ids</code> 是原本那兩個加上出站 NSG</td>
      </tr>
      <tr>
          <td>查同名主機失敗</td>
          <td>2</td>
          <td>不建第二台；印出假的 <code>oci</code> 寫在 stderr 的錯誤</td>
      </tr>
      <tr>
          <td>啟動主機（<code>STOPPED</code> 時）失敗</td>
          <td>2</td>
          <td>不印「啟動」，印出錯誤</td>
      </tr>
      <tr>
          <td>查 NAT gateway 失敗</td>
          <td>2</td>
          <td>不建第二個 NAT gateway</td>
      </tr>
      <tr>
          <td>讀路由表失敗、改寫路由表失敗</td>
          <td>2</td>
          <td>讀取失敗時不改寫；改寫失敗時不印「改寫」</td>
      </tr>
      <tr>
          <td>讀 NSG 的規則失敗、加 NSG 規則失敗</td>
          <td>2</td>
          <td>讀取失敗時不加規則；加規則失敗時不印「新建」</td>
      </tr>
      <tr>
          <td>讀 VNIC 的 NSG 清單失敗、改寫 VNIC 失敗</td>
          <td>2</td>
          <td>讀取失敗時不改寫，原本掛著的 NSG 不會被替換掉；改寫失敗時不印「改寫」</td>
      </tr>
  </tbody>
</table>
<p>這幾種失敗，腳本在加上錯誤處理之前都照樣往下走：查同名主機失敗時當成找不到主機，再建一台；啟動或寫入失敗時照樣印「啟動」「改寫」「新建」；讀路由表或 VNIC 失敗時照樣整份替換，刪掉原本的規則或 NSG（審查時用同一支假的 <code>oci</code> 重現過）。假的 <code>oci</code> 測的是腳本的分支與結束碼，測不到雲端的回應格式與等待時間。</p>
<p>序列主控台連線的重建腳本（下面那一支）用同一支假的 <code>oci</code> 測了這幾種情形：主機已有 <code>ACTIVE</code> 的連線時印「已存在」、不呼叫 <code>create</code>，設定檔填的是那條既有連線的 OCID（exit 0）；沒有時呼叫一次 <code>create</code>、印「新建」（exit 0）；金鑰不存在、而主機已有 <code>ACTIVE</code> 的連線時，產生新金鑰、刪掉舊連線、再建一條（exit 0）；以上三種結束時印出的「用完就刪」那條 <code>delete</code>，代入的都是這次設定檔裡用的那條連線的 OCID；找不到 <code>RUNNING</code> 的同名主機時印「找不到 RUNNING 的 verify-ubuntu，先跑 rebuild-ubuntu.sh」並以 2 結束；查主機、查連線、建連線任一條失敗時印出錯誤並以 2 結束。</p>
<p>腳本裡幾處寫法的理由：</p>
<ul>
<li><strong><code>q</code> 保留結束碼與 stderr</strong>：OCI CLI 3.95.1 在 <code>--query</code> 的結果是空的時候，stdout 沒有輸出、結束碼是 0，只在 stderr 印 <code>Query returned empty result, no output to show.</code>（讀 CLI 套件的 <code>cli_util.py</code> 確認的）；查詢失敗時 stdout 同樣是空的。所以空字串本身分不出「查無結果」與「查詢失敗」，要看結束碼：<code>q</code> 把 stderr 存進暫存檔、照原樣回傳 <code>oci</code> 的結束碼，呼叫的地方失敗就以 <code>fail</code> 停下來並印出 stderr。主機那一步只有在查詢成功、結果又是空的時候，才進建立分支。</li>
<li><strong>主機的狀態由 <code>wait_state</code> 自己查</strong>：<code>oci compute instance get</code> 沒有 <code>--wait-for-state</code>，帶了這個選項時 CLI 印出 <code>Error: No such option '--wait-for-state'.</code> 就結束；沒有等待迴圈的版本把 stderr 丟掉，這個錯誤沒有印出來，腳本在主機還沒 <code>RUNNING</code> 時就往下走。哪些子命令有這個選項用 <code>oci &lt;子命令&gt; --help</code> 查，這次查到有的是 <code>compute instance launch</code>、<code>compute instance action</code>、<code>compute instance terminate</code>、<code>network nat-gateway create</code>、<code>network route-table update</code>、<code>network nsg create</code>、<code>network vnic update</code>、<code>bastion session create-port-forwarding</code> 與 <code>compute instance-console-connection create</code>；沒有的是 <code>compute instance get</code>、<code>compute instance list-vnics</code>、<code>network nsg rules add</code> 與 <code>network nsg rules list</code>。VNIC 另外等最多 120 秒：剛送出建立請求約 5 秒的主機，<code>list-vnics</code> 在雲端實跑時回過空值（那一版的等待沒有作用，當時主機可能還沒到 <code>RUNNING</code>）。</li>
<li><strong>變數後面直接接中文的位置寫成 <code>${NAME}</code></strong>：macOS 的 <code>/bin/bash</code>（3.2.57）把變數名後面緊接的全形字元讀成變數名的一部分，<code>&quot;主機 $NAME（RUNNING）&quot;</code> 在 <code>set -u</code> 之下以 <code>NAME�: unbound variable</code> 結束。訊息裡的 <code>�</code> 不是文章的亂碼：bash 把全形括號「（」的 UTF-8 編碼（三個位元組）的第一個位元組當成變數名的一部分，照原樣印出那一個位元組，終端機顯示不出單獨一個位元組，就印成 <code>�</code>。用 <code>LC_ALL=C grep -n '\$[A-Za-z_][A-Za-z0-9_]*[^ -~]' &lt;腳本&gt;</code> 找得出這類位置，這一版跑這條是 0 行。</li>
<li><strong><code>BOOT_ARGS</code> 展開成 <code>${BOOT_ARGS[@]+&quot;${BOOT_ARGS[@]}&quot;}</code></strong>：同一個 bash 3.2.57 在 <code>set -u</code> 之下展開空陣列 <code>&quot;${BOOT_ARGS[@]}&quot;</code> 時，以 <code>BOOT_ARGS[@]: unbound variable</code> 結束（實跑）。沒有給 <code>BOOT_GB</code> 時陣列是空的。展開寫在 <code>out=$(oci …)</code> 的命令替換裡，照一般寫法，命令替換印出 <code>BOOT_ARGS[@]: unbound variable</code> 之後回傳空字串，腳本照樣往下走、讀不出回應碼，以 3 結束，訊息要讀者去查同名主機，看起來像網路中斷（依腳本結構與本機 bash 3.2 的最小重現推得，整支腳本沒有用一般寫法跑過）。重建既有主機的路徑走不到那一行，只測既有主機時看不出來。<code>${A[@]+…}</code> 在陣列有元素時才展開後面那一段，空陣列就什麼都不印。</li>
</ul>
<p><strong>序列主控台連線的重建腳本</strong>：主機沒有 <code>ACTIVE</code> 的序列主控台連線才建，並產生跟〈序列主控台：RSA 金鑰、建立連線、ProxyJump 設定與看到的開機輸出〉相同寫法的 <code>ssh-console.conf</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="cp">#!/bin/bash
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="cp"></span><span class="c1"># 替主機建序列主控台連線（instance console connection），並產生 ssh-console.conf。</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># 主機已經有 ACTIVE 的連線就沿用；沒有就用 console_rsa.pub 建一條（金鑰不存在時先產生 RSA 3072，</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1">#   這時既有的連線登記的是舊的公鑰，先刪掉再建）。</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># 官方給的連線字串在 ssh 外面再包一層 ssh 當跳板；這裡寫成等價的 ProxyJump 設定，不在本機執行任意命令。</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># 連線只在排查時存在：要排查時跑這支建一條，用完以結尾印出的 delete 指令刪掉。</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># 用法：./rebuild-console.sh [主機名稱]（預設 verify-ubuntu）。連線：ssh -tt -F ssh-console.conf verify-console</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># 結束碼：0 完成；2 登入過期、找不到主機、oci 失敗（印出它的 stderr）或建不出連線。</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="nb">set</span> -uo pipefail
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># OCI CLI 是 Python 程式；PYTHONWARNINGS=ignore 關掉 Python 直譯器印的警告，不讓警告行混進要解析的輸出</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="nb">export</span> <span class="nv">OCI_CLI_AUTH</span><span class="o">=</span>security_token <span class="nv">PYTHONWARNINGS</span><span class="o">=</span>ignore
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="nv">D</span><span class="o">=</span><span class="k">$(</span><span class="nb">cd</span> <span class="s2">&#34;</span><span class="k">$(</span>dirname <span class="s2">&#34;</span><span class="nv">$0</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span> <span class="o">&amp;&amp;</span> <span class="nb">pwd</span><span class="k">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="nb">source</span> <span class="s2">&#34;</span><span class="nv">$D</span><span class="s2">/ids.env&#34;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="nv">NAME</span><span class="o">=</span><span class="si">${</span><span class="nv">1</span><span class="k">:-</span><span class="nv">verify</span><span class="p">-ubuntu</span><span class="si">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="nv">REGION</span><span class="o">=</span>&lt;區域&gt;
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="nv">CONF</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$D</span><span class="s2">/ssh-console.conf&#34;</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="nv">KNOWN</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$D</span><span class="s2">/known_hosts_console&#34;</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="nv">ERR</span><span class="o">=</span><span class="k">$(</span>mktemp <span class="s2">&#34;</span><span class="si">${</span><span class="nv">TMPDIR</span><span class="k">:-</span><span class="p">/tmp</span><span class="si">}</span><span class="s2">/rebuild-console.XXXXXX&#34;</span><span class="k">)</span><span class="p">;</span> <span class="nb">trap</span> <span class="s1">&#39;rm -f &#34;$ERR&#34;&#39;</span> EXIT
</span></span><span class="line"><span class="ln">19</span><span class="cl">ts<span class="o">()</span> <span class="o">{</span> date +%m-%d<span class="se">\ </span>%H:%M:%S<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">say<span class="o">()</span> <span class="o">{</span> <span class="nb">echo</span> <span class="s2">&#34;</span><span class="k">$(</span>ts<span class="k">)</span><span class="s2"> [</span><span class="nv">$1</span><span class="s2">] </span><span class="nv">$2</span><span class="s2">&#34;</span><span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">die<span class="o">()</span> <span class="o">{</span> <span class="nb">echo</span> <span class="s2">&#34;</span><span class="k">$(</span>ts<span class="k">)</span><span class="s2"> [錯誤] </span><span class="nv">$1</span><span class="s2">&#34;</span> &gt;<span class="p">&amp;</span>2<span class="p">;</span> <span class="nb">exit</span> 2<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="c1"># 查詢與寫入用的包裝：stderr 存進 $ERR 那個暫存檔，濾掉兩種不是結果的提示行（Python 的 SyntaxWarning，</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="c1">#   與 CLI 建議改用 `oci -i` 的 Interactive mode 那一行），結束碼照 oci 自己的回傳。</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="c1">#   CLI 的 --query 結果是空的時候 stdout 什麼都不印，所以空字串要搭配結束碼 0 才代表查無結果</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">q<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">  <span class="nb">local</span> out rc
</span></span><span class="line"><span class="ln">27</span><span class="cl">  <span class="nv">out</span><span class="o">=</span><span class="k">$(</span>oci <span class="s2">&#34;</span><span class="nv">$@</span><span class="s2">&#34;</span> 2&gt;<span class="s2">&#34;</span><span class="nv">$ERR</span><span class="s2">&#34;</span><span class="k">)</span><span class="p">;</span> <span class="nv">rc</span><span class="o">=</span><span class="nv">$?</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">  <span class="o">[[</span> -n <span class="s2">&#34;</span><span class="nv">$out</span><span class="s2">&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> grep -v -e SyntaxWarning -e <span class="s1">&#39;Interactive mode&#39;</span> <span class="o">&lt;&lt;&lt;</span><span class="s2">&#34;</span><span class="nv">$out</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">  <span class="k">return</span> <span class="nv">$rc</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="c1"># q 失敗時停下來，印出 oci 寫在 stderr 的內容</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">fail<span class="o">()</span> <span class="o">{</span> die <span class="s2">&#34;</span><span class="si">${</span><span class="nv">1</span><span class="si">}</span><span class="s2">失敗：</span><span class="k">$(</span>tr <span class="s1">&#39;\n&#39;</span> <span class="s1">&#39; &#39;</span> &lt; <span class="s2">&#34;</span><span class="nv">$ERR</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span><span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">
</span></span><span class="line"><span class="ln">34</span><span class="cl">oci session validate --profile DEFAULT &gt;/dev/null 2&gt;<span class="p">&amp;</span><span class="m">1</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="se"></span>  <span class="o">||</span> die <span class="s2">&#34;CLI session 過期，先跑 oci session authenticate --region </span><span class="nv">$REGION</span><span class="s2"> --profile-name DEFAULT&#34;</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">
</span></span><span class="line"><span class="ln">37</span><span class="cl"><span class="nv">IID</span><span class="o">=</span><span class="k">$(</span>q compute instance list -c <span class="s2">&#34;</span><span class="nv">$C</span><span class="s2">&#34;</span> --display-name <span class="s2">&#34;</span><span class="nv">$NAME</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">38</span><span class="cl"><span class="se"></span>  --query <span class="s2">&#34;data[?\&#34;lifecycle-state\&#34;==&#39;RUNNING&#39;] | [0].id&#34;</span> --raw-output<span class="k">)</span> <span class="o">||</span> fail <span class="s2">&#34;查 RUNNING 的主機&#34;</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl"><span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$IID</span><span class="s2">&#34;</span> <span class="o">==</span> ocid1.instance* <span class="o">]]</span> <span class="o">||</span> die <span class="s2">&#34;找不到 RUNNING 的 </span><span class="si">${</span><span class="nv">NAME</span><span class="si">}</span><span class="s2">，先跑 rebuild-ubuntu.sh&#34;</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl">
</span></span><span class="line"><span class="ln">41</span><span class="cl"><span class="c1"># 序列主控台連線只收 RSA 公鑰</span>
</span></span><span class="line"><span class="ln">42</span><span class="cl"><span class="nv">NEWKEY</span><span class="o">=</span><span class="m">0</span>
</span></span><span class="line"><span class="ln">43</span><span class="cl"><span class="k">if</span> <span class="o">[[</span> ! -f <span class="s2">&#34;</span><span class="nv">$D</span><span class="s2">/console_rsa&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl">  ssh-keygen -q -t rsa -b <span class="m">3072</span> -N <span class="s1">&#39;&#39;</span> -C verify-console -f <span class="s2">&#34;</span><span class="nv">$D</span><span class="s2">/console_rsa&#34;</span> <span class="o">||</span> die <span class="s2">&#34;產生 RSA 金鑰失敗&#34;</span>
</span></span><span class="line"><span class="ln">45</span><span class="cl">  <span class="nv">NEWKEY</span><span class="o">=</span>1<span class="p">;</span> say 新建 <span class="s2">&#34;RSA 金鑰 console_rsa&#34;</span>
</span></span><span class="line"><span class="ln">46</span><span class="cl"><span class="k">else</span> say 已存在 <span class="s2">&#34;RSA 金鑰 console_rsa&#34;</span><span class="p">;</span> <span class="k">fi</span>
</span></span><span class="line"><span class="ln">47</span><span class="cl">
</span></span><span class="line"><span class="ln">48</span><span class="cl"><span class="nv">CID</span><span class="o">=</span><span class="k">$(</span>q compute instance-console-connection list -c <span class="s2">&#34;</span><span class="nv">$C</span><span class="s2">&#34;</span> --instance-id <span class="s2">&#34;</span><span class="nv">$IID</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">49</span><span class="cl"><span class="se"></span>  --query <span class="s2">&#34;data[?\&#34;lifecycle-state\&#34;==&#39;ACTIVE&#39;] | [0].id&#34;</span> --raw-output<span class="k">)</span> <span class="o">||</span> fail <span class="s2">&#34;查序列主控台連線&#34;</span>
</span></span><span class="line"><span class="ln">50</span><span class="cl"><span class="c1"># 剛產生新金鑰時，既有的連線登記的是舊的公鑰，用新私鑰連不上：先刪掉它，再用新的公鑰建一條</span>
</span></span><span class="line"><span class="ln">51</span><span class="cl"><span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$NEWKEY</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="m">1</span> <span class="o">&amp;&amp;</span> <span class="s2">&#34;</span><span class="nv">$CID</span><span class="s2">&#34;</span> <span class="o">==</span> ocid1.instanceconsoleconnection* <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">52</span><span class="cl">  q compute instance-console-connection delete --instance-console-connection-id <span class="s2">&#34;</span><span class="nv">$CID</span><span class="s2">&#34;</span> --force <span class="se">\
</span></span></span><span class="line"><span class="ln">53</span><span class="cl"><span class="se"></span>    --wait-for-state DELETED &gt;/dev/null <span class="o">||</span> fail <span class="s2">&#34;刪掉用舊公鑰建的序列主控台連線&#34;</span>
</span></span><span class="line"><span class="ln">54</span><span class="cl">  say 改寫 <span class="s2">&#34;刪掉用舊公鑰建的序列主控台連線&#34;</span>
</span></span><span class="line"><span class="ln">55</span><span class="cl">  <span class="nv">CID</span><span class="o">=</span><span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="ln">56</span><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="ln">57</span><span class="cl"><span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$CID</span><span class="s2">&#34;</span> <span class="o">==</span> ocid1.instanceconsoleconnection* <span class="o">]]</span><span class="p">;</span> <span class="k">then</span> say 已存在 <span class="s2">&#34;序列主控台連線&#34;</span>
</span></span><span class="line"><span class="ln">58</span><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="ln">59</span><span class="cl">  <span class="nv">CID</span><span class="o">=</span><span class="k">$(</span>q compute instance-console-connection create --instance-id <span class="s2">&#34;</span><span class="nv">$IID</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">60</span><span class="cl"><span class="se"></span>    --ssh-public-key-file <span class="s2">&#34;</span><span class="nv">$D</span><span class="s2">/console_rsa.pub&#34;</span> --wait-for-state ACTIVE --query data.id --raw-output <span class="p">|</span> tail -1<span class="k">)</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">61</span><span class="cl"><span class="se"></span>    <span class="o">||</span> fail <span class="s2">&#34;建序列主控台連線&#34;</span>
</span></span><span class="line"><span class="ln">62</span><span class="cl">  <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$CID</span><span class="s2">&#34;</span> <span class="o">==</span> ocid1.instanceconsoleconnection* <span class="o">]]</span> <span class="o">||</span> die <span class="s2">&#34;建不出序列主控台連線：</span><span class="nv">$CID</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">63</span><span class="cl">  say 新建 <span class="s2">&#34;序列主控台連線&#34;</span>
</span></span><span class="line"><span class="ln">64</span><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="ln">65</span><span class="cl">
</span></span><span class="line"><span class="ln">66</span><span class="cl">cat &gt; <span class="s2">&#34;</span><span class="nv">$CONF</span><span class="s2">&#34;</span> <span class="s">&lt;&lt;EOF
</span></span></span><span class="line"><span class="ln">67</span><span class="cl"><span class="s"># 由 rebuild-console.sh 產生：$NAME 的序列主控台。官方連線字串的跳板改寫成 ProxyJump。
</span></span></span><span class="line"><span class="ln">68</span><span class="cl"><span class="s">Host verify-console-gw
</span></span></span><span class="line"><span class="ln">69</span><span class="cl"><span class="s">  HostName instance-console.$REGION.oci.oraclecloud.com
</span></span></span><span class="line"><span class="ln">70</span><span class="cl"><span class="s">  Port 443
</span></span></span><span class="line"><span class="ln">71</span><span class="cl"><span class="s">  User $CID
</span></span></span><span class="line"><span class="ln">72</span><span class="cl"><span class="s">  IdentityFile $D/console_rsa
</span></span></span><span class="line"><span class="ln">73</span><span class="cl"><span class="s">  IdentitiesOnly yes
</span></span></span><span class="line"><span class="ln">74</span><span class="cl"><span class="s">  UserKnownHostsFile $KNOWN
</span></span></span><span class="line"><span class="ln">75</span><span class="cl"><span class="s">  StrictHostKeyChecking accept-new
</span></span></span><span class="line"><span class="ln">76</span><span class="cl"><span class="s">  HostKeyAlgorithms +ssh-rsa
</span></span></span><span class="line"><span class="ln">77</span><span class="cl"><span class="s">  PubkeyAcceptedAlgorithms +ssh-rsa
</span></span></span><span class="line"><span class="ln">78</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">79</span><span class="cl"><span class="s">Host verify-console
</span></span></span><span class="line"><span class="ln">80</span><span class="cl"><span class="s">  HostName $IID
</span></span></span><span class="line"><span class="ln">81</span><span class="cl"><span class="s">  User $CID
</span></span></span><span class="line"><span class="ln">82</span><span class="cl"><span class="s">  ProxyJump verify-console-gw
</span></span></span><span class="line"><span class="ln">83</span><span class="cl"><span class="s">  IdentityFile $D/console_rsa
</span></span></span><span class="line"><span class="ln">84</span><span class="cl"><span class="s">  IdentitiesOnly yes
</span></span></span><span class="line"><span class="ln">85</span><span class="cl"><span class="s">  UserKnownHostsFile $KNOWN
</span></span></span><span class="line"><span class="ln">86</span><span class="cl"><span class="s">  StrictHostKeyChecking accept-new
</span></span></span><span class="line"><span class="ln">87</span><span class="cl"><span class="s">  HostKeyAlgorithms +ssh-rsa
</span></span></span><span class="line"><span class="ln">88</span><span class="cl"><span class="s">  PubkeyAcceptedAlgorithms +ssh-rsa
</span></span></span><span class="line"><span class="ln">89</span><span class="cl"><span class="s">EOF</span>
</span></span><span class="line"><span class="ln">90</span><span class="cl">: &gt; <span class="s2">&#34;</span><span class="nv">$KNOWN</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">91</span><span class="cl">say 改寫 <span class="s2">&#34;</span><span class="nv">$CONF</span><span class="s2"> 與 known_hosts_console&#34;</span>
</span></span><span class="line"><span class="ln">92</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;連線：ssh -tt -F </span><span class="nv">$CONF</span><span class="s2"> verify-console（離開：先按 Enter，再輸入 ~.）&#34;</span>
</span></span><span class="line"><span class="ln">93</span><span class="cl"><span class="c1"># 序列主控台連線是救援入口，平時不留：排查完就刪</span>
</span></span><span class="line"><span class="ln">94</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;用完就刪：oci compute instance-console-connection delete --instance-console-connection-id </span><span class="nv">$CID</span><span class="s2"> --force --wait-for-state DELETED&#34;</span></span></span></code></pre></div><p><code>&lt;區域&gt;</code> 換成自己的區域識別碼；<code>ids.env</code> 只用到 <code>C</code>。這支腳本每次都清空 <code>known_hosts_console</code>，下一次連線時由 <code>StrictHostKeyChecking accept-new</code> 重新記下跳板與目的地的 host key；代價跟〈建 Ubuntu 24.04 主機與經由 port forwarding 登入〉那一段寫的相同，第一次連線沒有比對指紋。這份檔案不依主機名稱分檔，給不同主機跑時共用同一份。<code>console_rsa</code> 不存在而這次新產生時，主機上既有的 <code>ACTIVE</code> 連線登記的是舊的公鑰，新私鑰連不上，所以腳本先刪掉那條連線再用新公鑰建一條。對新建的 <code>verify-ubuntu-rebuild</code> 實跑的是前一版：訊息還寫「主控台連線」、還沒有錯誤處理與換金鑰處理，建連線的步驟跟現在相同。結果是 exit 0，15 秒，印「已存在」（RSA 金鑰）、「新建」（序列主控台連線）與「改寫」（設定檔）。照設定檔連上之後印出 Oracle 對序列主控台連線的說明文字；本機用 <code>alarm</code> 讓 ssh 在 25 秒後結束，這段時間裡沒有按 Enter，所以登入提示沒有出現。</p>
<p><strong>重建之後要重做的事</strong>，依順序：</p>
<ol>
<li><code>ssh -F ssh-verify-ubuntu.conf verify-ubuntu 'cloud-init status --wait; uptime'</code>（設定檔名依主機名稱）：等首次開機設定跑完（這次約 1 分鐘）。</li>
<li>主機被刪除重建時，映像檔又是建立那一天的版本：照〈加出站：NAT gateway、路由規則與只掛在這台主機上的 NSG〉最後那兩條，<code>sudo unattended-upgrade</code> 之後 <code>sudo systemctl reboot</code>（這次 402 秒、升級 64 個套件），才把映像檔之後的安全更新補上。只是被停止再啟動時，更新還在，不必重做。</li>
<li>序列主控台連線平時不留，重建之後不必另外建。要排查時才跑序列主控台連線的重建腳本，再 <code>ssh -tt -F ssh-console.conf verify-console</code>；排查完用腳本結尾印出的那條 <code>delete</code> 刪掉。</li>
</ol>
<p><strong>耗時</strong>：這次量到的是單步的時間，建立請求送出到回傳約 4 秒，約 20 秒後的下一次查詢時主機已經是 <code>RUNNING</code>，cloud-init 約 1 分鐘，port forwarding session 約 1 分鐘，<code>unattended-upgrade</code> 402 秒，核心開始開機到登入提示約 27 秒。重建腳本在主機還在時 34 秒跑完，主機停止、停止中、啟動中三種狀態各 28、35、55 秒（差在各步 API 的回應與 port forwarding session 的快慢）；名稱不存在的那一次，那一版沒有等待主機的狀態，建立請求到 <code>RUNNING</code> 的確切時間沒有量到（2 分 30 秒後重跑時已經是 <code>RUNNING</code>），之後接上網路與登入的部分 80 秒，其中 port forwarding session 佔 64 秒；這一版從建主機那一步一次跑到底的時間沒有量。A1 容量不足時，建立那一步要重送，時間取決於區域當時的容量，重送的間隔與限流門檻在建主機那篇〈重送建立請求：CLI 自己的重送、限流門檻與重送腳本〉。</p>
<h3 id="重建的其他做法用-iac-描述這組資源建立時交給-cloud-init-升級只靠手動">重建的其他做法：用 IaC 描述這組資源、建立時交給 cloud-init 升級、只靠手動</h3>
<ul>
<li><strong>用 Terraform 描述主機與網路</strong>：主機被刪除之後 <code>terraform apply</code> 就重建，<code>plan</code> 也會列出被刪掉的資源；回收這種狀況剛好是 <a href="/blog/devops/infra/knowledge-cards/iac/" data-link-title="Infrastructure as Code (IaC)" data-link-desc="用程式碼描述基礎設施的最終狀態，由工具負責收斂現實與描述的差異">Infrastructure as Code (IaC)</a> 擅長處理的 <a href="/blog/devops/infra/knowledge-cards/drift/" data-link-title="Drift（設定漂移）" data-link-desc="IaC 的 state 與雲端實際狀態之間的不一致，通常因為有人繞過 IaC 直接在 Console 改設定">Drift（設定漂移）</a>：宣告的狀態與實際不一致。代價是要先把已經用 CLI 建好的資源匯入 state（做法見 <a href="/blog/devops/infra/takeover/partial-iac-bulk-import/" data-link-title="Unmanaged Resource 批次 Import 工作流" data-link-desc="把 Terraform state 外的雲端資源有系統地納入 IaC 管理：優先序判斷、import block 語法、generated HCL 的 review 要點、批次策略與常見失敗處理">Unmanaged Resource 批次 Import 工作流</a>）；這次沒有做。</li>
<li><strong>建立時交給 cloud-init 升級套件</strong>：<code>oci compute instance launch</code> 的 <code>--user-data-file</code> 放一份 cloud-config，讓 cloud-init 在首次開機時就跑套件升級（cloud-config 的 <code>package_upgrade: true</code>），重建之後不必再手動跑 <code>unattended-upgrade</code>。前提是主機首次開機時已經有出站，所以要搭配上面〈出站的其他做法：security list、建主機時就掛 NSG、限縮出站的目的地與不加出站〉的「建主機時就掛上 NSG」。官方提醒初始化腳本裡不要放會觸發重開機的指令，否則可能讓主機建立失敗（<a href="/blog/devops/infra/cloud-providers/vm-options/management/" data-link-title="代理程式、中繼資料服務、初始化腳本、維護行為與標籤" data-link-desc="雲端主機建立頁管理區塊的選項：業者裝在主機裡的代理程式與它的 plugin 怎麼決定開關、中繼資料服務的第二版存取驗證、開機時執行的初始化腳本、底層硬體維護時主機會被遷移還是重開，以及標籤">代理程式、中繼資料服務、初始化腳本、維護行為與標籤</a>〈初始化腳本〉），所以 cloud-config 只升級、不重開機，換上新核心的那一次重開機留到登入之後手動做，或由重建腳本在主機變成 <code>RUNNING</code>、cloud-init 跑完之後再觸發。這一項沒有實跑。</li>
<li><strong>只靠手動照〈建 Ubuntu 24.04 主機與經由 port forwarding 登入〉與〈加出站：NAT gateway、路由規則與只掛在這台主機上的 NSG〉兩節重做</strong>：步驟都在本篇，不需要腳本；代價是每一步的 OCID 要重查，漏掉 NSG 掛到新 VNIC 這一步時，主機看起來建好了，<code>apt-get update</code> 卻又回到逾時，而且沒有任何錯誤訊息指出原因。</li>
</ul>
<h2 id="費用與額度a1開機磁碟與區塊儲存額度出站流量與-nat-gateway">費用與額度：A1、開機磁碟與區塊儲存額度、出站流量與 NAT gateway</h2>
<ul>
<li>
<p><strong>A1</strong>：帳號篇記錄的 Always Free 額度是每月 1,500 OCPU 小時與 9,000 GB 小時（相當於 2 OCPU、12 GB 整月開著）。2026-10-26 讀到的 Oracle 的 Compute 價目頁（頁名這次沒有記下）寫的是「Each paid tenancy gets the first 3,000 OCPU hours and 18,000 GB hours per month for free」，對象是付費 tenancy，相當於 4 OCPU、24 GB 整月開著。這個 tenancy 是付費帳戶：重建腳本實跑之前，同一個 tenancy 已經有三台各 1 OCPU、6 GB 的 A1 主機在跑（2026-10-27 用 <code>oci compute instance list</code> 查到 <code>verify-a1-ol</code>、<code>verify-origin</code>、<code>verify-ubuntu</code> 三台是 <code>RUNNING</code>），腳本建出的第四台讓合計變成 4 OCPU、24 GB，剛好等於付費 tenancy 每月的免費量，建立沒有被擋下。試用結束那一刻、A1 合計超過 2 OCPU、12 GB 就全部停用的規則，只管沒有升級的帳號（帳號篇〈Always Free 的額度與它綁在 home region 上的範圍〉）；付費帳戶超出免費量的部分照一般價格計費（帳號篇〈超出 Always Free 額度時：試用中、未升級與付費帳戶各自的結果〉）。免費量是帳務的規則，能建出幾個 OCPU 由 compute 的 service limit 決定，兩者是分開的：查法跟下面開機磁碟那一項同一組，A1 的 OCPU 與記憶體的限制名稱是 <code>standard-a1-core-count</code> 與 <code>standard-a1-memory-count</code>，用 <code>oci limits resource-availability get -c &lt;tenancy 的 OCID&gt; --service-name compute --limit-name &lt;名稱&gt; --availability-domain &lt;可用網域&gt;</code> 查（2026-10-27 實跑，寫法見 <a href="/blog/devops/infra/cloud-providers/oracle-cloud/hands-on/suspend-resume-drill/" data-link-title="Oracle Cloud 主機的暫停與恢復上線實測紀錄：暫停前的基準、收窄入口、停止主機、停止期間的計費與 Cloudflare 的回應、恢復上線的每一步與耗時，以及收尾時 DNS 紀錄與主機的刪除順序" data-link-desc="要讓 Oracle Cloud 上一台放在 Cloudflare 後面的主機停機一段時間再恢復上線、想知道停止之後公開 IP 留不留、停止期間 A1 與開機磁碟怎麼計費、Cloudflare 回什麼錯誤、恢復時每一步要多久，或服務要收掉時 DNS 紀錄與主機的刪除順序時，查實際跑過的指令、輸出與量到的時間">Oracle Cloud 主機的暫停與恢復上線實測紀錄：暫停前的基準、收窄入口、停止主機、停止期間的計費與 Cloudflare 的回應、恢復上線的每一步與耗時，以及收尾時 DNS 紀錄與主機的刪除順序</a>〈暫停前記下的基準：接聽的程式、服務、待裝的更新、憑證效期與公開 IP〉）；其他限制的名稱用 <code>oci limits definition list -c &lt;tenancy 的 OCID&gt; --service-name compute</code> 列（沒有實跑）。超出免費量時，停止主機對 OCPU 與記憶體費用的影響見 <a href="/blog/devops/operations/09-host-lifecycle/suspend-and-resume-service/" data-link-title="服務暫停與恢復上線：只停服務、停止主機、終止主機到全部刪除各自保留的資源，暫停期間仍計費與仍被指向的資源，恢復前要查的漂移與恢復的順序" data-link-desc="一台主機上的服務要暫停一段時間再恢復：只停服務、停止主機、終止主機保留設定、全部刪除靠 repo 重建各自保留什麼與付出什麼，怎麼依保留的月費、重建的成本與漂移量選擇，暫停期間仍在計費的資源（含在作業系統裡 shutdown 不停止計費），仍被 DNS 與 CDN 指向的結果（Cloudflare 521、522，釋放 IP 之後的懸空 DNS），恢復前要重新確認的漂移，以及依元件依賴排出的恢復順序與驗證">服務暫停與恢復上線：只停服務、停止主機、終止主機到全部刪除各自保留的資源，暫停期間仍計費與仍被指向的資源，恢復前要查的漂移與恢復的順序</a>〈暫停期間仍在計費的資源〉；停止的主機的開機磁碟照樣佔著區塊磁碟額度。</p>
</li>
<li>
<p><strong>開機磁碟</strong>：每台的開機磁碟從 Always Free 的 200 GB 區塊磁碟額度裡扣；放不進剩餘額度的那一顆整顆在額度外（下面「合計超過 200 GB 時」）。這顆磁碟有三種寫法：序列主控台記下的開機輸出寫 <code>[sda] 97677312 512-byte logical blocks: (50.0 GB/46.6 GiB)</code>；<code>oci bv boot-volume list</code> 對 <code>verify-ubuntu</code> 回 <code>size-in-mbs</code> 47694，正好是 97677312 × 512 位元組換成 MiB；同一條回的 <code>size-in-gbs</code> 是 47，是 46.6 GiB 進位。另外兩台 Oracle Linux 主機的 <code>size-in-mbs</code> 是 47719，換成 GiB 同樣是 46.6，<code>size-in-gbs</code> 同樣是 47。每顆磁碟的大小與額度的用量用下面三條查（2026-10-27 實跑）。<code>&lt;tenancy 的 OCID&gt;</code> 是最上層 compartment（root）的 OCID，<code>~/.oci/config</code> 裡 <code>tenancy=</code> 那一行就是它。這次的 compartment 直接建在 root 底下，所以 <code>oci iam compartment get --compartment-id &lt;compartment 的 OCID&gt; --query 'data.&quot;compartment-id&quot;' --raw-output</code> 印出的上一層也是 tenancy 的 OCID（實跑，跟 <code>tenancy=</code> 的值相同）；compartment 是巢狀的時候，這條印出的是另一個 compartment，要讀 <code>~/.oci/config</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"># 列出每顆還沒終止的開機磁碟與它的大小；&lt;可用網域&gt; 是建主機時用的那一個</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">oci bv boot-volume list -c &lt;compartment 的 OCID&gt; --availability-domain &lt;可用網域&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;data[?&#34;lifecycle-state&#34;!=`TERMINATED`].{name:&#34;display-name&#34;,gb:&#34;size-in-gbs&#34;,mb:&#34;size-in-mbs&#34;}&#39;</span> --output table
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 列出區塊儲存的限制名稱；區域層的免費額度叫 total-free-storage-gb-regional，</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># 可用網域層另有一個 total-free-storage-gb，這個 tenancy 兩者的上限都是 200。</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># 付費 tenancy 合計超過 200 時兩者都沒有擋下建立，放不進的那顆磁碟不算進兩者的 used（下面「合計超過 200 GB 時」）</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">oci limits definition list -c &lt;tenancy 的 OCID&gt; --service-name block-storage --all <span class="se">\
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;data[].{name:name,desc:description,scope:&#34;scope-type&#34;}&#39;</span> --output table
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 查用量：used 是已經扣掉的量，available 是還剩的量。</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># 查可用網域層那一個時，--limit-name 換成 total-free-storage-gb，並加 --availability-domain &lt;可用網域&gt;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">oci limits resource-availability get -c &lt;tenancy 的 OCID&gt; --service-name block-storage <span class="se">\
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="se"></span>  --limit-name total-free-storage-gb-regional</span></span></code></pre></div><p>2026-10-27 終止 <code>verify-ubuntu-rebuild</code> 之後查，剩三台主機時回 <code>&quot;used&quot;: 141</code>、<code>&quot;available&quot;: 59</code>，可用網域層那一個查出來的數字相同。這個 tenancy 裡只有這一個 compartment（<code>oci iam compartment list -c &lt;tenancy 的 OCID&gt; --compartment-id-in-subtree true --all</code> 只列出它），它與 root 底下都沒有開機磁碟以外的區塊磁碟（兩處的 <code>oci bv volume list -c &lt;…的 OCID&gt;</code> 都列不出任何一顆，root 底下也沒有開機磁碟；都是實跑），而 141 正好是三顆 <code>size-in-gbs</code> 47 的合計；三顆的 MiB 加起來是 139.8 GiB，總和進位會是 140，所以進位是每一顆各自做的。由此推論額度扣的數字就是每顆磁碟的 <code>size-in-gbs</code>：照預設大小建的主機，每台扣 47（額度以 GB 標示，數值是 46.6 GiB 進位），不是 50。照這個推算，終止 <code>verify-ubuntu-rebuild</code> 之前四台同時存在時合計扣 188，額度還剩 12（四台同時存在時沒有查用量，那台照同一個映像檔與預設大小算 47）。</p>
<p><strong>合計超過 200 GB 時</strong>（2026-10-27 09:33 實跑）：在扣了 141 的狀態下，用重建腳本以 <code>BOOT_GB=60</code> 建 <code>verify-ubuntu-60g</code>（建出 60 GiB，<code>size-in-gbs</code> 60；這裡的 GB 數值上都是 GiB，見〈被回收之後的重建：怎麼發現、留下與消失的資源、重建腳本〉的 <code>BOOT_GB</code> 那一項），三顆 47 加一顆 60 合計 201。建立請求沒有被擋下，主機照常建好（當時這個 tenancy 沒有設 compartment quota；設了 quota 之後超額的建立被擋下，見下面「用 compartment quota 擋下超額的建立」）。之後免費額度的用量與全部區塊磁碟的合計（<code>total-storage-gb</code>，上面的指令沒有查的第三條限制）不再相同：</p>
<table>
  <thead>
      <tr>
          <th>限制</th>
          <th>範圍</th>
          <th>上限</th>
          <th>used</th>
          <th>代表的量</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>total-free-storage-gb-regional</code></td>
          <td>區域</td>
          <td>200</td>
          <td>141</td>
          <td>算進 Always Free 額度的磁碟合計</td>
      </tr>
      <tr>
          <td><code>total-free-storage-gb</code></td>
          <td>可用網域</td>
          <td>200</td>
          <td>141</td>
          <td>同上，以可用網域計</td>
      </tr>
      <tr>
          <td><code>total-storage-gb</code></td>
          <td>可用網域</td>
          <td>61,440</td>
          <td>201</td>
          <td>全部區塊磁碟的合計，含付費的磁碟</td>
      </tr>
  </tbody>
</table>
<p>表裡的可用網域是 availability domain，區域裡的一個資料中心（跨業者的概念見 <a href="/blog/devops/infra/knowledge-cards/availability-zone/" data-link-title="Availability Zone（可用區）" data-link-desc="建立雲端主機或磁碟時要選可用區、遇到某個可用區容量不足、規劃兩台主機要不要放在不同可用區，或對照各家把這一層叫 zone、AZ 還是 availability domain 時，查可用區的範圍、它跟區域與故障域的關係與它限制了哪些資源的搭配">Availability Zone（可用區）</a> 卡）；主控台的 Scope 選單在這個區域只列出一個可用網域，兩條免費額度的數字因此相同（推論）。免費額度的 used 停在 141：60 GB 那一顆整顆算在免費額度之外。假如只有超出的 1 GB 在額度外，used 會是 200。主機 <code>RUNNING</code> 之後 4 分鐘起，每 2 分鐘查一次、共 6 次（約 10 分鐘），免費額度每次都是 141，而同一批查詢裡的 <code>total-storage-gb</code> 每次都是 201、已經算進這一顆，所以 141 不是還沒更新的值（推論）。Oracle 的 <a href="https://docs.oracle.com/en-us/iaas/Content/FreeTier/freetier_topic-Always_Free_Resources.htm">Always Free Resources</a> 頁（2026-10-27 讀）對區塊磁碟的寫法跟這個結果一致：它舉的例子是四顆 50 GB 的 Always Free 磁碟加一顆 50 GB 的付費磁碟，刪掉其中一顆 Always Free 磁碟、空出額度之後，服務會自動把那顆付費磁碟轉成 Always Free（原文「When you have available Always Free capacity, Block Volume automatically tries to transition an existing paid resource to an Always Free resource」）。原文描述的轉換對象是整顆資源（paid resource），所以免費與否以整顆判定（從這一句推得，頁面沒有逐字寫；例子裡的剩餘額度是 0，單看例子分不出整顆與超出量兩種算法）。照這一句，這次的狀態下刪掉一顆 47 的免費磁碟，剩餘額度從 59 變成 106，放得下 60，這顆就可能轉成免費；同一天下午實測了這個轉換：轉成免費的是線上加大到 60 GB 而移出額度的那一顆開機磁碟，轉換發生在空出額度之後 26 到 31 分鐘；同時另建、一開始就在額度外的 50 GB 資料磁碟放不進剩下的額度，沒有轉換，所以建立時就在額度外的磁碟要等多久沒有量到（下面「付費磁碟自動轉成 Always Free」）。這次被判在額度外的是建立時放不進的那一顆；之後刪掉磁碟、重新分配時會選哪一顆轉成免費，頁面沒有寫。同一頁也寫了「Although it is possible to mix paid and Always Free resources, Oracle does not recommend this」，所以合計快到 200 時，先刪掉不用的舊磁碟再建，比讓一顆磁碟落到額度外單純。帳務明細也是整顆計費（2026-10-27 實跑）。用 usage API 依 <code>skuName</code>、<code>skuPartNumber</code>、<code>resourceId</code> 分組、逐小時（<code>--granularity HOURLY</code>）讀，這顆磁碟在 UTC 02 時那一小時記成兩個付費的項目：<code>Block Volume - Storage</code>（數量 0.0806）與 <code>Block Volume - Performance Units</code>（數量 0.806）。0.0806 正好是 60 GB 乘上 1 小時、除以一個月 744 小時，代表整顆 60 GB 都照價計費，不是只有超出的 1 GB；0.806 是同一顆磁碟的 60 GB 乘上 Balanced 等級的每 GB 10 個效能單位（VPU）再除以 744。同一小時其他三顆免費的開機磁碟記在 <code>Block Volume - Free</code>，數量各 0.0632（47 ÷ 744），金額 0，而且沒有效能單位的項目。兩個付費項目一小時合計約 0.0047，幣別是這個帳戶的 SGD（新加坡幣）。所以合計快到 200 時，下一顆放不進剩餘額度的磁碟是整顆照價計費：剩 59 時建一顆 60 GB，付費的是 60 GB 不是 1 GB。</p>
<p>這筆項目出現的時間也量到了：磁碟在 UTC 01:33 建立，01 時那一小時的明細裡沒有它（那一小時只存在 27 分鐘；之後有沒有補上，這次沒有再查）；02 時那一小時的明細，UTC 03:27 查還沒有，03:57 查已經有了，也就是那一小時結束後 1 到 2 小時之間出現。每 30 分鐘查一次時，查到的最新一筆依序是 00、01、01、02 時那一小時，逐小時明細大約落後 2 小時。</p>
<p><strong>用 compartment quota 擋下超額的建立</strong>（2026-10-27 13:06 實跑）：付費帳戶合計超過 200 GB 時建立請求照常通過，要讓超額的建立被擋下，可以在 compartment 上設 quota。quota 是 policy，建在 tenancy（root）上、指名要限制的 compartment；這次限制的是區塊儲存的合計與 A1 的核心數，兩者都以可用網域為單位，所以用 <code>where request.ad</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"># &lt;compartment 的名稱&gt; 是名稱不是 OCID；&lt;可用網域&gt; 是建主機時用的那一個，這個區域只有一個</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">oci limits quota create --compartment-id &lt;tenancy 的 OCID&gt; --name verify-quota <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --description <span class="s2">&#34;cap block storage and A1 cores&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --statements <span class="s2">&#34;[\&#34;set block-storage quota total-storage-gb to 200 in compartment &lt;compartment 的名稱&gt; where request.ad = &#39;&lt;可用網域&gt;&#39;\&#34;, \&#34;set compute-core quota standard-a1-core-count to 4 in compartment &lt;compartment 的名稱&gt; where request.ad = &#39;&lt;可用網域&gt;&#39;\&#34;]&#34;</span></span></span></code></pre></div><p>回應的 <code>lifecycle-state</code> 是 <code>ACTIVE</code>。當時免費區塊儲存扣了 188（四顆 47）、A1 用了 4 OCPU（四台各 1），A1 已經到 4 的上限，區塊儲存剩 12、放不進 50 GB：</p>
<ul>
<li><strong>區塊儲存</strong>：建立後約 35 秒，<code>oci bv volume create -c &lt;compartment 的 OCID&gt; --availability-domain &lt;可用網域&gt; --display-name verify-data-50 --size-in-gbs 50</code> 建一顆 50 GB 的資料磁碟（合計會變成 238），回 <code>400</code>，<code>code</code> 是 <code>LimitExceeded</code>，訊息是 <code>You have reached your service limit in this Availability Domain for volumes. …</code>。Oracle 的 <a href="https://docs.oracle.com/en-us/iaas/Content/API/References/apierrors.htm">API Errors</a> 頁（2026-10-27 讀）把兩者分開：超過管理員設定的 compartment quota 是 <code>400 QuotaExceeded</code>，超過 Oracle 設定的 tenancy 上限是 <code>400 LimitExceeded</code>。這次區塊儲存那一條實際回的是 <code>LimitExceeded</code>，訊息也寫成 service limit，沒有提到 quota；<code>total-storage-gb</code> 的 service limit 是 61,440，用量只有 188，所以擋下它的是 quota。拿掉 quota 之後同一條指令建立成功（下一項），也對得上這個判讀。只看回應碼與訊息的排查會往 service limit 去查，查到的是用量遠低於上限。</li>
<li><strong>A1 核心數</strong>：重建腳本建第五台（<code>verify-ubuntu-q</code>），建立請求回 <code>400</code>，訊息直接寫出被超過的 quota：<code>The following compartment quotas were exceeded: standard-a1-core-count in policy '&lt;quota 的 OCID&gt;' by 1.</code>。重建腳本只重送 500 與 429，回 400 就不重送、以 4 結束，<code>instance list</code> 查不到這台主機。</li>
</ul>
<p>quota 用 <code>oci limits quota delete --quota-id &lt;quota 的 OCID&gt; --force</code> 刪掉（<code>&lt;quota 的 OCID&gt;</code> 是 <code>quota create</code> 回應裡的 <code>data.id</code>，或用 <code>oci limits quota list -c &lt;tenancy 的 OCID&gt;</code> 查），<code>quota get</code> 回 <code>NotAuthorizedOrNotFound</code>。這兩條 quota 擋得住的範圍比免費量窄，有四個缺口：</p>
<ul>
<li><strong>記憶體</strong>：A1 的免費量是 4 OCPU 與 24 GB 兩項，這次只限了核心數，沒有限 <code>standard-a1-memory-count</code>；1 OCPU 配 32 GB 的 A1 會通過 quota 而超出記憶體的免費量。要擋，再加一條 <code>set compute-memory quota standard-a1-memory-count to 24 …</code>（家族名稱與寫法照 Quota Policy Syntax 推，沒有實跑）。</li>
<li><strong>其他 compartment</strong>：quota 寫的是 <code>in compartment &lt;名稱&gt;</code>，只管那一個 compartment；免費量是整個 tenancy 一份，root 或別的 compartment 裡建的磁碟與主機不受這兩條限制。<a href="https://docs.oracle.com/en-us/iaas/Content/Quotas/Concepts/quota_policy_syntax.htm">Quota Policy Syntax</a> 的範例有 <code>in tenancy</code> 的寫法（2026-10-27 讀，沒有實跑）。</li>
<li><strong>多個可用網域</strong>：同一頁寫明可用網域層的 quota「is allocated to each AD」，每個可用網域各有一份 200；這個區域只有一個可用網域，所以這次的 200 就是全區的 200。可用網域有好幾個的區域，每一個都要用 <code>where request.ad</code> 各設一條，加起來不超過 200。</li>
<li><strong>加大磁碟</strong>：下一段線上加大是在拿掉 quota 之後做的，quota 擋不擋 <code>boot-volume update</code> 這次沒有測。</li>
</ul>
<p>在這幾個範圍裡，建立請求在超出免費量的那一刻就被拒，不會像上面那顆 60 GB 一樣整顆照價計費；代價是要建更大的磁碟時得先改 quota。quota 剛建好之後多久生效沒有量，這次建立後約 35 秒送出的請求已經被擋下。quota 的上限怎麼依最大花費速度訂，在 <a href="/blog/devops/operations/08-cost-management/spend-planning-before-renting/" data-link-title="租用雲端主機之前的費用規劃：費用訊號的延遲、停止之前的最大超支、扣款的信用卡額度、配額與預算門檻的計算，以及停損停到哪一層" data-link-desc="租用雲端主機或把試用帳號升級成付費帳戶之前，費用失控時從觸發到停止之間的金額怎麼從配額與訊號延遲推出來，再用它推扣款的信用卡額度、配額、預算警示與觸發處置的門檻；Oracle Cloud、Google Cloud 與 AWS 的扣款時點、預先授權、扣款失敗的後果、擋下建立與停止用量的機制各自涵蓋什麼，以及停損停到哪一層的判斷">租用雲端主機之前的費用規劃：費用訊號的延遲、停止之前的最大超支、扣款的信用卡額度、配額與預算門檻的計算，以及停損停到哪一層</a>〈平台上的上限：擋下建立、停止用量與只發通知〉。</p>
<p><strong>線上加大開機磁碟與另建資料磁碟，合計超過 200 GB 時</strong>（2026-10-27 13:08 實跑）：拿掉 quota 之後，在免費區塊儲存扣了 188 的狀態下做了兩件事。磁碟只能加大、不能縮小（Oracle 的 <a href="https://docs.oracle.com/en-us/iaas/Content/Block/Tasks/resizingavolume.htm">Resizing a Volume</a> 頁：「You can only increase the size of the volume. You cannot decrease the size.」，2026-10-27 讀），所以合計接近 200 時加大的那一顆移出免費額度之後，要回到免費只能等別處空出放得下它的額度，或另建一顆小的磁碟搬資料：</p>
<table>
  <thead>
      <tr>
          <th>動作</th>
          <th>免費額度的 used（兩條相同）</th>
          <th><code>total-storage-gb</code> 的 used</th>
          <th>讀法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>建 50 GB 的資料磁碟（<code>oci bv volume create … --size-in-gbs 50</code>）</td>
          <td>188</td>
          <td>238</td>
          <td>50 GB 整顆在免費額度外</td>
      </tr>
      <tr>
          <td><code>verify-ubuntu</code> 的開機磁碟從 47 線上加大到 60</td>
          <td>141</td>
          <td>251</td>
          <td>加大的那一顆整顆移出免費額度，不是只有超出的 13 GB</td>
      </tr>
  </tbody>
</table>
<p>開機磁碟的 OCID 用 <code>oci compute boot-volume-attachment list -c &lt;compartment 的 OCID&gt; --availability-domain &lt;可用網域&gt; --instance-id &lt;主機的 OCID&gt; --query 'data[0].&quot;boot-volume-id&quot;' --raw-output</code> 查。加大用 <code>oci bv boot-volume update --boot-volume-id &lt;開機磁碟的 OCID&gt; --size-in-gbs 60 --wait-for-state AVAILABLE</code>，主機開著時 34 秒回 <code>AVAILABLE</code>。另一種做法是先停止主機再加大（離線加大）：不必 rescan，啟動時作業系統直接讀到新的大小，代價是停機（依 Oracle 的磁碟加大文件推論，沒有實跑，也沒有比較兩種做法對免費額度的判定）。加大之前那顆 47 是免費的，加大到 60 之後，另外三顆 47（141）加上這顆 60，合計 201、超過 200，結果是那一顆整顆從免費額度裡拿出來（188 減 47），跟建立時放不進剩餘額度的情形相同。主機裡的作業系統要 rescan 才看到新的大小：加大之後 <code>lsblk -b</code> 讀到的仍是 50010783744 位元組，<code>echo 1 | sudo tee /sys/class/block/sda/device/rescan</code>（<code>sda</code> 是這台主機的開機磁碟名稱，自己的主機用 <code>lsblk</code> 確認）之後變成 64424509440（60 GiB），<code>dmesg</code> 印 <code>sda: detected capacity change from 97677312 to 125829120</code>；分割區與檔案系統的擴大這次沒有做（根目錄仍是 45G）。</p>
<p><strong>付費磁碟自動轉成 Always Free</strong>（2026-10-27 13:11 實跑）：上表之後，終止 <code>verify-ubuntu-nb</code>、刪掉它的 47 GB 開機磁碟（work request 65 秒 <code>SUCCEEDED</code>），免費額度從 141 降到 94，<code>total-storage-gb</code> 從 251 降到 204。之後每 5 分鐘讀一次兩個用量，共 90 分鐘：</p>
<table>
  <thead>
      <tr>
          <th>時刻（UTC）</th>
          <th>免費額度的 used</th>
          <th><code>total-storage-gb</code> 的 used</th>
          <th>讀法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>05:12:55</td>
          <td>94</td>
          <td>204</td>
          <td>終止之後立刻讀；60 GB 與 50 GB 兩顆都還在免費額度外</td>
      </tr>
      <tr>
          <td>05:38:45</td>
          <td>94</td>
          <td>204</td>
          <td>終止之後 26 分鐘，還沒轉換</td>
      </tr>
      <tr>
          <td>05:43:50</td>
          <td>154</td>
          <td>204</td>
          <td>終止之後 31 分鐘，多出的 60 是加大的那顆開機磁碟</td>
      </tr>
      <tr>
          <td>06:39:40</td>
          <td>154</td>
          <td>204</td>
          <td>之後每次都相同；50 GB 那顆放不進（154 加 50 是 204）</td>
      </tr>
  </tbody>
</table>
<p>帳務明細對得上這個轉換。查法是下面 NAT gateway 那一項的 <code>usage-api</code> 指令改成逐小時、看用量，並依資源分組（2026-10-27 實跑）：</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"># 起訖填要看的 UTC 整點；res 欄對回磁碟的 OCID</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">oci usage-api usage-summary request-summarized-usages --tenant-id &lt;tenancy 的 OCID&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --time-usage-started 2026-10-27T04:00:00Z --time-usage-ended 2026-10-27T09:00:00Z <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --granularity HOURLY --query-type USAGE --group-by <span class="s1">&#39;[&#34;skuName&#34;,&#34;skuPartNumber&#34;,&#34;resourceId&#34;]&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;data.items[].{hour:&#34;time-usage-started&#34;,sku:&#34;sku-name&#34;,res:&#34;resource-id&#34;,qty:&#34;computed-quantity&#34;}&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  --output table</span></span></code></pre></div><p>加大的那顆開機磁碟在 UTC 05 時那一小時分成兩段記：<code>Block Volume - Storage</code> 0.0394 與 <code>Block Volume - Performance Units</code> 0.3944（60 GB 付費約 29 分鐘），加上 <code>Block Volume - Free</code> 0.0384（加大之前的 47 GB 約 10 分鐘，加上轉換之後的 60 GB 約 21 分鐘）；換算回去，轉換大約在 05:39，跟用量在 05:38 到 05:43 之間跳動一致。06 時整小時記成 <code>Block Volume - Free</code> 0.0806（60 ÷ 744），沒有付費項目。50 GB 的資料磁碟 05 時記 <code>Block Volume - Storage</code> 0.0572 與 <code>Performance Units</code> 0.5725（建立之後約 51 分鐘）、06 時 0.0672 與 0.672，一直是付費。</p>
<p>所以空出額度之後，付費磁碟會自動轉成免費，但不是當下：這次等了 26 到 31 分鐘，這段時間照付費計。剩餘額度放不下的那一顆不轉換。轉換沒有任何通知，要自己讀免費額度的 used 確認；哪一顆還在額度外，用上面那條逐小時的查詢看那顆磁碟記在 <code>Block Volume - Storage</code> 還是 <code>Block Volume - Free</code>。兩顆都放得下時選哪一顆先轉、轉換的延遲是否固定，這次沒有量。</p>
<p><code>total-storage-gb</code> 要帶 <code>--availability-domain</code> 查，寫法跟上面查可用網域層免費額度那一條相同，只換 <code>--limit-name</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"># 全部區塊磁碟的合計（含付費的磁碟）；跟免費額度的 used 相減，就是不在免費額度裡的 GB 數</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">oci limits resource-availability get -c &lt;tenancy 的 OCID&gt; --service-name block-storage <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --limit-name total-storage-gb --availability-domain &lt;可用網域&gt;</span></span></code></pre></div><p><strong>網頁主控台的同一組數字</strong>：Governance &amp; Administration → Tenancy Management → Limits, quotas and usage 頁（路徑在 <a href="/blog/devops/infra/cloud-providers/oracle-cloud/console-navigation/" data-link-title="Oracle Cloud 主控台導覽：頂端列、導覽選單的分類、清單頁的 compartment 篩選器與常用頁面的路徑" data-link-desc="Oracle Cloud 主控台頂端列各個按鈕（導覽選單、搜尋、區域、Cloud Shell、人像選單）做什麼，導覽選單各分類底下有哪些項目，清單頁的 compartment 篩選器，以及建立主機、網路、Bastion、預算、稽核紀錄、配額這些常用頁面在選單上的實際路徑">Oracle Cloud 主控台導覽：頂端列、導覽選單的分類、清單頁的 compartment 篩選器與常用頁面的路徑</a>），篩選條件的 Service 選 Block Volume 之後，2026-10-27 10 時前後讀到的數字跟 CLI 相同。這次開啟時篩選條件的 Scope 是區域（<code>ap-batam-1</code>，沒有試過換瀏覽器或換使用者時是否相同），這時只列出 <code>total-free-storage-gb-regional</code>（頁上的描述是 Free Tier Volume Quota (GB) in the Region）與兩項備份數量的限制（<code>backup-count</code>、<code>free-backup-count</code>），前者的 Service Limit、Usage、Available 三欄分別顯示 200、141、59；全部區塊磁碟的合計要把 Scope 換成可用網域才列出來，<code>total-free-storage-gb</code>（Free Volume Size (GB)）顯示 200、141、59，<code>total-storage-gb</code>（Volume Size (GB)）顯示 61,440、201、61,239。只看區域範圍時，看不到不在免費額度裡的那 60 GB。</p>
</li>
<li>
<p><strong>出站流量</strong>：每月前 10 TB 免費（Oracle〈Virtual Cloud Network Pricing〉），這台只拿來下載套件。</p>
</li>
<li>
<p><strong>NAT gateway</strong>：Oracle 的 <a href="https://www.oracle.com/cloud/price-list/">Cloud Price List</a>（2026-10-27 讀）在 Networking 那一節只列入站流量與出站流量，沒有 NAT gateway 這一項；<a href="https://www.oracle.com/cloud/networking/virtual-cloud-network/pricing/">Virtual Cloud Network Pricing</a> 頁同樣只寫區域內的流量、對外的出站流量、Dedicated connectivity 與 site-to-site VPN。實際的帳務用 usage API 依服務與 SKU 分組讀（2026-10-27 實跑）。usage API 讀的是網頁主控台 Cost Analysis 頁（Billing &amp; Cost Management → Cost Management → Cost Analysis）背後同一份用量資料，CLI 的服務名是 <code>usage-api</code>；SKU 是帳務明細上的計費品項，例如 <code>Standard - A1</code>。<code>--query</code> 收的是 JMESPath 運算式，上一項 <code>boot-volume list</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"># COST 換成 USAGE 時印的是用量。要看每一項記在哪個資源上，--group-by 加 &#34;resourceId&#34;，</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"># --query 的投影也要加 res:&#34;resource-id&#34;，否則表格裡不會多出資源那一欄（兩處都改過的版本實跑過）</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># 起訖填要看的那幾天（UTC），這次只用過日界的整點</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># tenancy 的 OCID 跟上一項 oci limits 用的是同一個。這次 CLI 設定的區域就是 home region，</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># 沒有試從其他區域呼叫 usage API 的結果</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">oci usage-api usage-summary request-summarized-usages --tenant-id &lt;tenancy 的 OCID&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>  --time-usage-started 2026-10-24T00:00:00Z --time-usage-ended 2026-10-27T00:00:00Z <span class="se">\
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="se"></span>  --granularity DAILY --query-type COST --group-by <span class="s1">&#39;[&#34;service&#34;,&#34;skuName&#34;]&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;data.items[].{day:&#34;time-usage-started&#34;,service:service,sku:&#34;sku-name&#34;,cost:&#34;computed-amount&#34;,qty:&#34;computed-quantity&#34;}&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  --output table</span></span></code></pre></div><p><code>verify-nat</code> 在 2026-10-26 13:18（UTC）建立，同一天 19:11（UTC，台北時間 10-27 03:11）前後刪除，存在約 6 小時，整段都落在明細的 2026-10-26 那一天裡（區間延長到 10-28 重查，明細也只到 10-26）。那一天的明細只有四項：Compute 的 <code>Standard - A1</code> 與 <code>Standard - A1 - Memory</code>、Block Storage 的 <code>Block Volume - Free</code>、Virtual Cloud Network 的 <code>Outbound Data Transfer Zone 2</code>，金額都是 0，沒有 NAT gateway 的項目。出站流量那一項以 <code>resourceId</code> 分組時有四筆，都記在 VNIC 上；用 <code>oci network vnic get</code> 對回去，其中一筆是 <code>verify-ubuntu</code> 的 VNIC（它只經 NAT gateway 出站），一筆是有公開 IP 的 <code>verify-origin</code>，另外兩筆的 VNIC 已經隨主機刪掉、查不到。所以經 NAT gateway 出去的流量記在送出流量的那台主機的 VNIC 上、照出站流量計，算進每月 10 TB 的免費量（從這一天的明細推論）。<code>verify-ubuntu</code> 那一筆是一整天的合計，NAT gateway 建立之前它也經 service gateway 送封包到 Oracle 服務網路，這次沒有把兩段分開，所以沒有確認這一筆裡經 NAT 的流量佔多少，也沒有確認 service gateway 那段是否計入出站流量。這只涵蓋 NAT gateway 存在的那約 6 小時；查詢是在 2026-10-26 19:3x（UTC）做的，那一天還沒結束；逐小時的明細大約落後 2 小時（上面開機磁碟那一項 2026-10-27 量到的），所以那一天最後幾小時的明細當時還沒出來，可能還不完整。<code>verify-nat</code> 已經刪除（〈刪除這組資源的順序〉），沒有讀完一個帳單週期的 Cost Analysis；隔幾天用同一條查詢、起訖涵蓋 10-26 重查一次，就能確認那一天的明細補齊之後有沒有多出 NAT gateway 的項目。<code>Outbound Data Transfer Zone 2</code> 的 Zone 是帳務替出站流量分的區；價目表依流量的出發地分成北美歐洲英國、亞太日本南美、中東非洲三組，三組的前 10 TB 都免費，Zone 2 對應哪一組這次沒有查。</p>
<p>所以就兩份價目頁與建立當天的明細來看，NAT gateway 本身沒有收費項目，開一個 NAT gateway 多出的費用只有經它出去的流量，而流量照一般的出站流量計、先扣每月 10 TB 的免費量。這是推論：證據只涵蓋約 6 小時、而且那一天的明細可能還不完整，也沒有讀完一個帳單週期；這次也沒有查 Oracle 的 NAT gateway 文件是否明文寫了收費規則。</p>
</li>
</ul>
<h2 id="刪除這組資源的順序">刪除這組資源的順序</h2>
<p>順序跟建立相反，依賴有兩組，兩組之間互不依賴：NSG 要先從 VNIC 拿掉才刪得掉，NAT gateway 要先不再被路由表指著才刪得掉；終止主機時 VNIC 與序列主控台連線跟著消失。指令裡要填的值的來源：<code>&lt;NAT gateway 的 OCID&gt;</code> 與 <code>&lt;NSG 的 OCID&gt;</code> 是 <code>ids.env</code> 的 <code>NAT</code> 與 <code>NSGU</code>，同一個鍵有好幾行時取最後一行，<code>source ids.env</code> 之後用 <code>$NAT</code>、<code>$NSGU</code> 拿到的就是最後一行的值（重建腳本跑過一次就會記進去；沒跑過的話，用 <code>oci network nat-gateway list -c &lt;compartment 的 OCID&gt; --vcn-id &lt;VCN 的 OCID&gt; --display-name verify-nat --query 'data[0].id' --raw-output</code> 與同樣寫法的 <code>oci network nsg list … --display-name verify-ubuntu-egress</code> 查，兩條沒有實跑），<code>&lt;Oracle 服務網路的目的地名稱&gt;</code> 是 <code>SGCIDR</code>，<code>&lt;路由表的 OCID&gt;</code> 與 <code>&lt;service gateway 的 OCID&gt;</code> 是 <code>RT</code> 與 <code>SG</code>；<code>&lt;VNIC 的 OCID&gt;</code> 用 <code>oci compute instance list-vnics --instance-id &lt;主機的 OCID&gt; --query 'data[0].id' --raw-output</code> 查，<code>&lt;主機的 OCID&gt;</code> 是 <code>host-&lt;名稱&gt;.env</code> 最後一行的 <code>IID</code>（沒有這個檔時，用〈被回收之後的重建：怎麼發現、留下與消失的資源、重建腳本〉的 <code>instance list</code> 查），<code>&lt;連線的 OCID&gt;</code> 用〈序列主控台：RSA 金鑰、建立連線、ProxyJump 設定與看到的開機輸出〉的 <code>instance-console-connection list</code> 查（把 <code>--query</code> 換成 <code>'data[0].id'</code>）。</p>
<p>主機也要一起刪時，先做終止主機那一步（第 6 步），再刪 NSG、改路由表、刪 NAT gateway：VNIC 隨主機一起消失，序列主控台連線變成 <code>DELETED</code>，所以從 VNIC 拿掉 NSG 那一步（第 1 步）與刪序列主控台連線那一步（第 5 步）可以省略。留著主機、只收掉出站設定時，做從 VNIC 拿掉 NSG 到刪 NAT gateway 那四步（第 1 到第 4 步）；排查到一半、序列主控台連線還在時，再加刪連線那一步（第 5 步）。第 1 到第 4 步帶的 <code>--wait-for-state</code> 讓 CLI 等到這一步的資源回到 <code>AVAILABLE</code> 或變成 <code>TERMINATED</code> 才回傳，同一組依賴裡，後一步的刪除依賴前一步已經生效（NSG 要先從 VNIC 拿掉才刪得掉，路由表要先不再指著 NAT gateway 它才刪得掉）；2026-10-26 那一次刪除沒有帶，這次帶了，兩種寫法在同一組資源上的差別沒有比較：</p>
<ol>
<li>把 NSG 從主機的 VNIC 拿掉：<code>oci network vnic update --vnic-id &lt;VNIC 的 OCID&gt; --nsg-ids '[]' --force --wait-for-state AVAILABLE</code>。<code>--nsg-ids</code> 是整份替換，先用 <code>oci network vnic get --vnic-id &lt;VNIC 的 OCID&gt; --query 'data.&quot;nsg-ids&quot;'</code> 列出 VNIC 現在掛著的 NSG（2026-10-27 實跑，只列出 <code>verify-ubuntu-egress</code> 一個）；清單裡還有別的 NSG 時，<code>'[]'</code> 換成去掉 <code>&lt;NSG 的 OCID&gt;</code> 之後的清單，否則別的 NSG 會一起被拿掉。NSG 還掛在 VNIC 上時刪不掉：2026-10-27 先試刪 <code>verify-ubuntu-egress</code>，<code>nsg delete</code> 回 <code>412 PreconditionFailed</code>，訊息是 <code>cannot be deleted since it still has vnics attached to it</code>；2026-10-26 刪另一組實驗資源的 NSG 時也是同一個回應。</li>
<li>刪 NSG：<code>oci network nsg delete --nsg-id &lt;NSG 的 OCID&gt; --force --wait-for-state TERMINATED</code>。</li>
<li>路由表改回只有 service gateway 那一條：<code>oci network route-table update --rt-id &lt;路由表的 OCID&gt; --force --wait-for-state AVAILABLE --route-rules '[{&quot;destination&quot;:&quot;&lt;Oracle 服務網路的目的地名稱&gt;&quot;,&quot;destinationType&quot;:&quot;SERVICE_CIDR_BLOCK&quot;,&quot;networkEntityId&quot;:&quot;&lt;service gateway 的 OCID&gt;&quot;}]'</code>。路由表還指著 NAT gateway 時，NAT gateway 刪不掉：2026-10-27 在改路由表之前先試刪 <code>verify-nat</code>，<code>nat-gateway delete</code> 回 <code>400 InvalidParameter</code>，訊息裡包著內層的 <code>409, Conflict</code>：<code>The Route Table references the NAT_GATEWAY &lt;NAT gateway 的 OCID&gt;. You must remove the reference to proceed with this operation.</code>。外層的回應碼是 400 而不是 409，寫腳本或排查的人只看回應碼時，會把它讀成參數錯誤，要讀到 <code>message</code> 裡的那一句才知道是路由表的引用。<code>--route-rules</code> 是整份替換，路由表裡還有 service gateway 與 0.0.0.0/0 以外的規則時，要把它們一起寫進清單，否則會跟著被刪掉；先用〈加出站：NAT gateway、路由規則與只掛在這台主機上的 NSG〉的 <code>route-table get</code> 列出現有的規則。</li>
<li>刪 NAT gateway：<code>oci network nat-gateway delete --nat-gateway-id &lt;NAT gateway 的 OCID&gt; --force --wait-for-state TERMINATED</code>。</li>
<li>刪序列主控台連線：<code>oci compute instance-console-connection delete --instance-console-connection-id &lt;連線的 OCID&gt; --force --wait-for-state DELETED</code>。連線平時不留（〈序列主控台：RSA 金鑰、建立連線、ProxyJump 設定與看到的開機輸出〉），所以只有排查到一半、連線還在的時候才需要這一步。這一條的 <code>--wait-for-state</code> 收的是連線本身的狀態（2026-10-27 實跑，10 秒後 <code>DELETED</code>），跟終止主機那一步收 work request 狀態的 <code>terminate</code> 不同。主機終止時連線也會跟著變成 <code>DELETED</code>（終止主機那一步實跑時看到的）。</li>
<li>終止主機並刪掉開機磁碟：<code>oci compute instance terminate --instance-id &lt;主機的 OCID&gt; --preserve-boot-volume false --force --wait-for-state SUCCEEDED --wait-for-state FAILED</code>。<code>terminate --help</code> 列出 <code>--wait-for-state</code> 只接受 work request 的四種狀態（<code>ACCEPTED</code>、<code>IN_PROGRESS</code>、<code>FAILED</code>、<code>SUCCEEDED</code>），所以寫成主機的 <code>TERMINATED</code> 時，CLI 會以參數錯誤結束、請求不會送出（2026-10-27 實跑：CLI 印 <code>Invalid value for '--wait-for-state': invalid choice: TERMINATED.</code>、以 2 結束）。</li>
</ol>
<p>2026-10-27 對重建腳本建出的 <code>verify-ubuntu-rebuild</code> 實跑了終止主機那一步：work request 66 秒後 <code>SUCCEEDED</code>，主機變成 <code>TERMINATED</code>，那台的序列主控台連線變成 <code>DELETED</code>；用 <code>oci bv boot-volume list</code> 加上 <code>lifecycle-state != TERMINATED</code> 的篩選列出開機磁碟時，清單裡已經沒有那顆磁碟。<a href="/blog/devops/infra/cloud-providers/oracle-cloud/hands-on/launch-instance-walkthrough/" data-link-title="Oracle Cloud 建立主機的實測紀錄：建立前的檢查、前置網路、主控台各分頁的 Always Free 設定值、等效的 CLI 指令、容量不足的重送、Bastion 工作階段逾時、Run Command 補公鑰的嘗試與重建後經由 port forwarding 登入" data-link-desc="要在 Oracle Cloud 建一台沒有公開 IP、經由 Bastion 登入的 Always Free A1 主機，想照一次實測重做、看建立頁每一欄的取值與換一種做法的結果時讀；也用在建立請求一直回容量不足、managed SSH session 建立逾時，或沒放公鑰的主機想用 Run Command 救援、Oracle Linux 10 的主機上找不到 Bastion plugin、想改用 port forwarding session 登入的時候">Oracle Cloud 建立主機的實測紀錄：建立前的檢查、前置網路、主控台各分頁的 Always Free 設定值、等效的 CLI 指令、容量不足的重送、Bastion 工作階段逾時、Run Command 補公鑰的嘗試與重建後經由 port forwarding 登入</a>（建主機那篇）〈刪除這組資源的順序〉寫的是開機磁碟的狀態變成 <code>TERMINATED</code>，篩掉 <code>TERMINATED</code> 之後就不在清單裡，兩者不衝突。</p>
<p>同一天留著 <code>verify-ubuntu</code>、照從 VNIC 拿掉 NSG 到刪 NAT gateway 那四步（第 1 到第 4 步）收掉它的出站設定（實跑）：四條各在 1 到 2 秒內結束，結束碼都是 0；之後 <code>nat-gateway get</code> 與 <code>nsg get</code> 都回 <code>404 NotAuthorizedOrNotFound</code>，兩者的 <code>list</code> 也已經列不出來，路由表剩 service gateway 那一條。主機上的 <code>apt-get update</code> 回到〈只接 service gateway 的私有子網路：apt 連到套件來源時的錯誤原文與成因〉那一節的錯誤：IPv6 位址印 <code>Network is unreachable</code>，IPv4 位址印 <code>connection timed out</code>。刪掉之後，<code>ids.env</code> 的 <code>NAT</code> 與 <code>NSGU</code> 指向已經不存在的資源；重建腳本依名稱查 NAT gateway 與 NSG，查不到 <code>AVAILABLE</code> 的同名資源就重建並記下新的 OCID，所以要恢復出站時重跑重建腳本：同一天上午重跑過一次，32 秒重建了 NAT gateway、NSG 與路由，出站恢復（〈被回收之後的重建：怎麼發現、留下與消失的資源、重建腳本〉「NAT gateway 與 NSG 都不存在」那一項）。新的 OCID 以新的一行接在 <code>ids.env</code> 檔尾，舊的那一行還在；<code>source</code> 時後面那一行蓋過前面的，手動抄值時取最後一行。</p>
<p><strong>只暫停出站、不刪資源的做法</strong>（三種的暫停動作都沒有實跑；前兩種恢復時重建腳本走的「改寫」分支，在 NAT gateway 與 NSG 都不存在的那一次實跑過，第三種依 <code>--help</code> 與重建腳本的邏輯推論）：NAT gateway 本身看來沒有收費項目（〈費用與額度：A1、開機磁碟與區塊儲存額度、出站流量與 NAT gateway〉），刪掉它換到的主要是帳號裡不留下沒在用的資源，代價是重建之後 NAT gateway 與 NSG 的 OCID 都換了，NAT gateway 的出站 IP 也會換：在第三方服務用這個 IP 做白名單的話，白名單要跟著改（<code>nat-gateway create --public-ip-id &lt;保留公開 IP 的 OCID&gt;</code> 可以讓重建的 NAT gateway 沿用一個保留的公開 IP，沒有實跑，保留 IP 是否收費沒有查）。只想暫停時有三種做法：</p>
<ul>
<li><strong>只拿掉路由表的 0.0.0.0/0 那一條</strong>：做第 3 步就好。要恢復時重跑重建腳本，路由那一步查到少了 0.0.0.0/0，會印「改寫」補回來。</li>
<li><strong>只把 NSG 從 VNIC 拿掉</strong>：做第 1 步就好。security list 不放行網際網路出站（〈加出站：NAT gateway、路由規則與只掛在這台主機上的 NSG〉），所以出站跟著斷掉。要恢復時重跑重建腳本，掛 NSG 那一步查到 VNIC 沒有掛它，會印「改寫」重新掛上。</li>
<li><strong><code>oci network nat-gateway update --nat-gateway-id &lt;NAT gateway 的 OCID&gt; --block-traffic true</code></strong>：NAT gateway 停止轉送流量，路由與 NSG 都不動。要恢復時要另外跑同一條、帶 <code>--block-traffic false</code>：重建腳本只依名稱與 <code>AVAILABLE</code> 找 NAT gateway，被設成擋流量的 NAT gateway 仍然是 <code>AVAILABLE</code>，腳本每一步都印「已存在」、以 0 結束，出站卻還是不通，<code>apt-get update</code> 回到逾時，沒有任何訊息指出原因。</li>
</ul>
<p><strong>整組收掉</strong>（2026-10-27 15:05 實跑）：照「主機也要一起刪時」的順序，先終止 <code>verify-ubuntu</code>（work request 65 秒 <code>SUCCEEDED</code>，開機磁碟一起刪），再刪 NSG（3 秒）、路由表改回只有 service gateway 那一條（2 秒）、刪 NAT gateway（2 秒）：終止回 <code>SUCCEEDED</code>，刪 NSG 與 NAT gateway 的結束碼是 0，路由表更新之後剩 1 條規則；之後 <code>nat-gateway get</code> 與 <code>nsg get</code> 都回 <code>404 NotAuthorizedOrNotFound</code>。同一個私有子網路裡還有另一台主機（建主機那篇的 Oracle Linux 主機），它的網卡沒有掛這個 NSG（<code>vnic get</code> 的 <code>nsg-ids</code> 是 <code>[]</code>，實跑），套件來源是 Oracle 服務網路裡的鏡像站、經 service gateway 連得到，所以拿掉 NAT gateway 不影響它（套件來源經 service gateway 連得到這一點是推論，收掉之後沒有在它上面跑 <code>dnf</code>）；VCN、私有子網路、service gateway 與 Bastion 留給它用。費用一節建的 50 GB 資料磁碟用 <code>oci bv volume delete --volume-id &lt;磁碟的 OCID&gt; --force --wait-for-state TERMINATED</code> 刪掉（5 秒）。</p>
<p>各項刪不掉時，看錯誤訊息指出的那一項依賴、先刪它。網路、Bastion 與 compartment 的刪除順序在建主機那篇〈刪除這組資源的順序〉。</p>
<h2 id="延伸閱讀建主機的前置網路閒置回收的規則序列主控台的比較主機上的更新與-nat-閘道的計費">延伸閱讀：建主機的前置網路、閒置回收的規則、序列主控台的比較、主機上的更新與 NAT 閘道的計費</h2>
<ul>
<li><a href="/blog/devops/infra/cloud-providers/oracle-cloud/hands-on/launch-instance-walkthrough/" data-link-title="Oracle Cloud 建立主機的實測紀錄：建立前的檢查、前置網路、主控台各分頁的 Always Free 設定值、等效的 CLI 指令、容量不足的重送、Bastion 工作階段逾時、Run Command 補公鑰的嘗試與重建後經由 port forwarding 登入" data-link-desc="要在 Oracle Cloud 建一台沒有公開 IP、經由 Bastion 登入的 Always Free A1 主機，想照一次實測重做、看建立頁每一欄的取值與換一種做法的結果時讀；也用在建立請求一直回容量不足、managed SSH session 建立逾時，或沒放公鑰的主機想用 Run Command 救援、Oracle Linux 10 的主機上找不到 Bastion plugin、想改用 port forwarding session 登入的時候">Oracle Cloud 建立主機的實測紀錄：建立前的檢查、前置網路、主控台各分頁的 Always Free 設定值、等效的 CLI 指令、容量不足的重送、Bastion 工作階段逾時、Run Command 補公鑰的嘗試與重建後經由 port forwarding 登入</a>：本篇沿用的 compartment、VCN、私有子網路、service gateway、Bastion 與重送腳本</li>
<li><a href="/blog/devops/infra/cloud-providers/oracle-cloud/account-and-home-region/" data-link-title="Oracle Cloud 的帳號結構與 home region：tenancy、Always Free 的範圍與主控台登入欄位" data-link-desc="OCI 的 tenancy、home region、compartment 與 availability domain 各自管什麼，home region 為什麼在申請當下就要決定，Always Free 額度的計算與回收規則，以及主控台登入時租用戶與使用者名稱兩個欄位的分工">Oracle Cloud 的帳號結構與 home region：tenancy、Always Free 的範圍與主控台登入欄位</a>：Always Free 的額度、閒置回收與試用結束時超額的規則</li>
<li><a href="/blog/devops/operations/09-host-lifecycle/long-running-host-maintenance/" data-link-title="長期上線主機的維護項目：會累積的資料、定期讀的 log、對外入口的盤點、安全更新、會到期的設定與檢查間隔的推算" data-link-desc="單台 Linux 主機（VPS、雲端主機）長期上線時，沒有人操作也會改變的狀態與維護對它們做的事：log 與資料的寫入者和清理機制（logrotate、journald、容器 log）、磁碟寫滿時各元件的反應、每份 log 要讀的欄位、入口在主機與雲端各層的盤點（ss）、各條更新管道的安全更新與重開機（dnf-automatic、unattended-upgrades、needs-restarting）、會到期與會改變的設定、檢查間隔的推算，以及接手一台已上線的主機時先處理哪幾項">長期上線主機的維護項目：會累積的資料、定期讀的 log、對外入口的盤點、安全更新、會到期的設定與檢查間隔的推算</a>：這台主機上量到的自動更新預設、核心更新前後的重開機判斷，以及三家業者序列主控台的條件</li>
<li><a href="/blog/devops/infra/cloud-providers/pricing-comparison/" data-link-title="雲端免費額度與小型主機的收費比較：試用金與固定額度、計費單位、區域限制、試用結束後的處置、出站流量與查價項目" data-link-desc="Oracle Cloud、Google Cloud 與 AWS 的免費方案分別由哪幾種額度組成，比較時要換算的計費單位，免費主機能放在哪些區域，試用期結束後帳號與資源會怎樣，以及付費主機的價格該去哪裡查">雲端免費額度與小型主機的收費比較：試用金與固定額度、計費單位、區域限制、試用結束後的處置、出站流量與查價項目</a>：AWS 與 Google Cloud 的 NAT 閘道計費、三家的出站流量額度</li>
</ul>
]]></content:encoded></item></channel></rss>