<?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>Bootstrap on Tarragon</title><link>https://tarrragon.github.io/blog/tags/bootstrap/</link><description>Recent content in Bootstrap on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Thu, 02 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/bootstrap/index.xml" rel="self" type="application/rss+xml"/><item><title>模組負一：還沒有 infra 的環境怎麼盡量做好</title><link>https://tarrragon.github.io/blog/infra/before-infra/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/before-infra/</guid><description>&lt;p>理想的 infra 治理是每一個資源都由版本控制描述、每一次變更都走 review、環境之間靠程式碼複製。多數正在運行的服務離這個畫面很遠：資源是有人在 Console 一個一個點出來的，security group 規則靠記憶維護，誰改了什麼只存在當事人腦裡。這一章承接的就是這個落差 — 你現在就在手動環境、還沒有能力或資源導入 IaC，目標是把這個階段做成「可控的手動」、而不是假裝已經納管，把代價最高的傷害先擋住，並為日後納管鋪好輸入。&lt;/p>
&lt;h2 id="把手動環境做成可控的手動">把手動環境做成「可控的手動」&lt;/h2>
&lt;p>可控的手動指的是一種中間狀態：資源還是手點的，但關鍵變更有痕跡、高風險操作有護欄、現實長什麼樣有紀錄。它的責任是降低兩種成本 — 當下出事的成本，以及未來把資源 import 進 IaC 的成本。手動起家是絕大多數服務的常態起點，從一個人驗證想法到小團隊接手都會經過這一階，把它當成需要管理的階段、而不是需要修正的錯誤。&lt;/p>
&lt;p>判讀自己是否「可控」的訊號很具體：能不能在五分鐘內說出 production 有哪些對外開放的 port、上週誰動過資料庫參數、刪掉某台機器會不會連帶弄壞別的東西。任何一題答不出來，代表這個手動環境的不可見區域正在擴大，下面幾節就是把這些區域逐一收斂。&lt;/p>
&lt;h2 id="先守住代價最高的底線">先守住代價最高的底線&lt;/h2>
&lt;p>護欄要先上在「一次失誤就難以挽回」的操作上，因為手動環境沒有 IaC 的 plan / diff 當預檢，人為操作直接生效。優先級看的是失誤的回退代價、不是操作頻率。&lt;/p>
&lt;p>長期憑證外洩是回退代價最高的一類。手動環境常見的反模式是把長期 access key 寫進腳本、CI 變數或開發者筆電，一旦外流，攻擊者拿到的是不會過期的權限。在還沒有完整 IAM 設計之前，最低成本的護欄是：對人改用會過期的登入工作階段（如 AWS IAM Identity Center 的臨時憑證），對自動化盡量改用平台原生的角色綁定，把還在用的長期 key 列一張清單、設定定期輪替。身分與憑證的完整地基在「模組二：身分與憑證地基」展開，這裡先擋住最容易致命的那一個。&lt;/p>
&lt;p>刪除 production 資源是第二類。手動操作沒有「先看會影響什麼」的步驟，刪一個 security group 或 volume 可能瞬間讓服務失聯。對承載狀態的資源（資料庫、儲存桶、有持久資料的磁碟）開啟平台的刪除保護（如 termination protection、deletion protection），讓誤點多一道阻力。網路規則的大改是第三類 — 調整 VPC 路由、subnet 或對外規則時，先確認回退方式存在再動手，網路地基的系統性設計在「模組三：網路地基」。&lt;/p>
&lt;p>這三類的共同點是：護欄成本低、失誤代價高，所以即使還沒有 IaC，CP 值也足以先做。&lt;/p>
&lt;h2 id="讓變更留下痕跡">讓變更留下痕跡&lt;/h2>
&lt;p>變更留痕的責任是讓「誰、在什麼時候、改了什麼、為什麼」事後可追溯，這是手動階段最接近版本控制的替代品。IaC 的 git history 天然提供這件事，手動環境得靠人為紀律補上。&lt;/p>
&lt;p>最低限度是一份變更日誌，可以只是 repo 裡的一個 &lt;code>CHANGELOG&lt;/code> 或團隊共用文件，每次動 production 就追加一行：時間、操作者、改了哪個資源、原因。它不需要漂亮，需要的是每次都寫。和它互補的是平台的稽核日誌（如 AWS CloudTrail），稽核日誌記錄 API 層級「發生了什麼」，人寫的日誌補上「為什麼」— 前者你查得到某個 security group 在幾點被改，後者你才知道那次改動是為了什麼需求。兩者一起，事故排查時才能從「哪裡變了」一路追到「能不能安全回退」。&lt;/p>
&lt;p>常見陷阱是只在「大改動」時才記錄，結果真正出事的往往是某次以為無關緊要的小調整。判準簡化成一句：只要這個操作別人事後可能需要知道，就記。&lt;/p>
&lt;h2 id="命名與-tagging-從手動階段就開始">命名與 tagging 從手動階段就開始&lt;/h2>
&lt;p>命名規範與資源標籤是降低未來 import 成本的最低成本投資，它的責任是讓每個資源自帶「我是誰、屬於哪個服務、誰負責、哪個環境」的身分資訊。手動點出來的資源若名稱是 &lt;code>test-2&lt;/code>、&lt;code>new-db-final&lt;/code>，日後納管時得靠人逐一辨認哪個還在用、屬於哪條業務線，這個考古成本遠高於當初多打幾個字。&lt;/p>
&lt;p>從手動階段就固定一套規則：資源名稱帶上服務與環境（如 &lt;code>payments-api-prod&lt;/code>），標籤至少包含 &lt;code>service&lt;/code>、&lt;code>env&lt;/code>、&lt;code>owner&lt;/code> 三個維度。這套規則在還沒 IaC 時靠人手動填，等到導入 IaC，這些標籤直接成為 Terraform 把現有資源對應到程式碼的依據，也是模組八治理習慣裡成本歸因與批次操作的基礎（見「模組八：治理好習慣」的 tagging 段）。先建立規範的價值在於：早一天統一，需要回頭重命名的資源就少一批。&lt;/p>
&lt;h2 id="盤點現有資源作為納管輸入">盤點現有資源作為納管輸入&lt;/h2>
&lt;p>資源盤點的責任是把「現實長什麼樣」寫成一份清單，它是日後納管的直接輸入 — 不知道有什麼，就無法決定先 import 什麼。手動環境最危險的是沒人記得還開著的資源。&lt;/p>
&lt;p>盤點不必一次到位，先用平台工具把現況拉出來，存成可比對的形式：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 列出某區域所有 EC2 instance 與其關鍵標籤&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">aws ec2 describe-instances &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --query &lt;span class="s1">&amp;#39;Reservations[].Instances[].[InstanceId,Tags,State.Name]&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --output table
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1"># 列出所有 security group 與開放規則，找出對外開放的 port&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">aws ec2 describe-security-groups &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --query &lt;span class="s1">&amp;#39;SecurityGroups[].[GroupId,GroupName,IpPermissions]&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --output json&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>把輸出存進 repo，定期重跑比對差異，就能看出環境在背景悄悄長出了什麼。這份清單同時服務三件事：當下的安全盤查（有沒有不該開的對外 port）、未來 IaC import 的範圍界定、以及成熟度評估時「全手動到底有多少資源」的事實基礎（成熟度階梯的定位見「模組零：infra 是什麼」）。&lt;/p>
&lt;h2 id="資源與信任不足下的高槓桿取捨">資源與信任不足下的高槓桿取捨&lt;/h2>
&lt;p>當時間、人力或上層信任都不足，無法一次把上面每件事做齊時，取捨原則是先做「失誤代價高且護欄成本低」的少數幾件。在這個情境下，最划算的通常是兩件：先擋長期憑證外洩，因為一次外洩可能拖垮整個帳號；再開啟有狀態資源的刪除保護，因為資料一旦刪除多半無法復原。&lt;/p>
&lt;p>變更日誌與資源盤點屬於累積型投資 — 越早開始，未來省的考古成本越多，但晚一週開始不會立刻出事，所以在資源極度受限時可以排在護欄之後。命名與 tagging 的取捨點在於：新建資源時順手套規則幾乎零成本，回頭重整存量資源才貴，所以策略是「新的一律照規範、舊的等有餘力再補」，而不是停下來先整理全部存量。資源不足時怎麼跟上層談這些工作的優先級，在「模組九：怎麼把 infra 推動起來」展開。&lt;/p>
&lt;h2 id="該開始導入-iac-的訊號">該開始導入 IaC 的訊號&lt;/h2>
&lt;p>手動環境到了某些訊號出現時，繼續手動的邊際成本會超過導入 IaC 的一次性成本，這就是該往模組一跨進去的時機。訊號是規模與協作的函數，不是時間的函數。&lt;/p>
&lt;p>第一個訊號是環境數量變多：當你需要 dev、staging、production 三套幾乎一樣的環境，手動複製會在環境之間留下難以察覺的差異，而 IaC 的價值正是用同一份程式碼複製環境。第二個是多人同時動資源：一個人手動操作還能靠記憶維護，兩三個人並行時，沒有 plan / review 的手動變更會互相覆蓋、互相破壞。第三個是環境爆炸頻率上升：如果「改一個設定結果弄壞別的東西」這類事故開始每月發生，代表手動環境的隱性依賴已經超過人腦能追蹤的上限。&lt;/p>
&lt;p>任一訊號穩定出現，就是把第一個資源納入 IaC 的起點 — 前面做的命名、tagging、資源盤點此時直接成為 import 的輸入，第一步怎麼跨進去在「模組一：最小可行 IaC」。在訊號出現前過早導入 IaC 也有代價：單人、單環境、低變更頻率時，IaC 的學習與維護成本可能高於它省下的手動工，所以這裡的判準是等訊號、不是趕進度。&lt;/p>
&lt;h2 id="章節文章">章節文章&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>文章&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/infra/before-infra/manual-environment-baseline/" data-link-title="手動環境的可控底線與納管準備" data-link-desc="還沒有 IaC 的環境怎麼守住底線、讓變更可追溯、降低未來納管成本，以及辨識何時該開始導入 IaC">手動環境的可控底線與納管準備&lt;/a>&lt;/td>
 &lt;td>還沒有 IaC 的環境怎麼守住底線、讓變更可追溯、降低未來納管成本，以及辨識何時該開始導入 IaC&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="跨分類引用">跨分類引用&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/infra/00-infra-mindset/" data-link-title="模組零：infra 是什麼，為什麼 day 1 就要鋪地基" data-link-desc="基礎設施的責任邊界、成熟度階梯，以及地基為什麼總在環境爆炸時才被看見">模組零：infra 是什麼&lt;/a>：成熟度階梯上「全手動」這一階的定位&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/infra/01-minimal-iac/" data-link-title="模組一：最小可行 IaC — state 地基與 Console 唯讀鐵律" data-link-desc="Terraform / OpenTofu 選型、remote state 與 lock，以及「Console 只能看不能改」鐵律">模組一：最小可行 IaC&lt;/a>：訊號出現後，第一步怎麼跨進 IaC&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/infra/02-identity-credentials/" data-link-title="模組二：身分與憑證地基 — IAM 與 OIDC" data-link-desc="IAM role / policy 設計、最小權限，以及用 OIDC 短期憑證取代長期 access key">模組二：身分與憑證地基&lt;/a>：長期憑證護欄的系統性設計&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/infra/03-network-foundation/" data-link-title="模組三：網路地基 — VPC 與分層" data-link-desc="VPC、public / private subnet 切分、route table、NAT、security group 設計">模組三：網路地基&lt;/a>：手動階段網路大改的回退考量、之後的系統性設計&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/infra/08-governance-habits/" data-link-title="模組八：治理好習慣 — 規模長大後不失控的最小節奏" data-link-desc="tagging 規範、secrets 不進 code、成本可見性、最小可行節奏，規模長大後不失控">模組八：治理好習慣&lt;/a>：tagging 在成本歸因與批次操作的後續價值&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/infra/09-driving-adoption/" data-link-title="模組九：怎麼把 infra 推動起來" data-link-desc="技術正確不等於推得動 — 信任赤字、期望值對齊、知識共享，infra 落地的組織課題">模組九：怎麼把 infra 推動起來&lt;/a>：資源不足時怎麼跟上層談優先級&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/infra/takeover/" data-link-title="接手維運：別人建的環境怎麼接管" data-link-desc="接手前人的專案時，怎麼在不搞壞東西的前提下盤點現況、建立維運能力、逐步正規化 — 從無 SSH 的 FTP 環境到有半套 IaC 的雲端環境都適用">接手維運：別人建的環境怎麼接管&lt;/a>：接手前人的專案時的盤點與接管流程&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>理想的 infra 治理是每一個資源都由版本控制描述、每一次變更都走 review、環境之間靠程式碼複製。多數正在運行的服務離這個畫面很遠：資源是有人在 Console 一個一個點出來的，security group 規則靠記憶維護，誰改了什麼只存在當事人腦裡。這一章承接的就是這個落差 — 你現在就在手動環境、還沒有能力或資源導入 IaC，目標是把這個階段做成「可控的手動」、而不是假裝已經納管，把代價最高的傷害先擋住，並為日後納管鋪好輸入。</p>
<h2 id="把手動環境做成可控的手動">把手動環境做成「可控的手動」</h2>
<p>可控的手動指的是一種中間狀態：資源還是手點的，但關鍵變更有痕跡、高風險操作有護欄、現實長什麼樣有紀錄。它的責任是降低兩種成本 — 當下出事的成本，以及未來把資源 import 進 IaC 的成本。手動起家是絕大多數服務的常態起點，從一個人驗證想法到小團隊接手都會經過這一階，把它當成需要管理的階段、而不是需要修正的錯誤。</p>
<p>判讀自己是否「可控」的訊號很具體：能不能在五分鐘內說出 production 有哪些對外開放的 port、上週誰動過資料庫參數、刪掉某台機器會不會連帶弄壞別的東西。任何一題答不出來，代表這個手動環境的不可見區域正在擴大，下面幾節就是把這些區域逐一收斂。</p>
<h2 id="先守住代價最高的底線">先守住代價最高的底線</h2>
<p>護欄要先上在「一次失誤就難以挽回」的操作上，因為手動環境沒有 IaC 的 plan / diff 當預檢，人為操作直接生效。優先級看的是失誤的回退代價、不是操作頻率。</p>
<p>長期憑證外洩是回退代價最高的一類。手動環境常見的反模式是把長期 access key 寫進腳本、CI 變數或開發者筆電，一旦外流，攻擊者拿到的是不會過期的權限。在還沒有完整 IAM 設計之前，最低成本的護欄是：對人改用會過期的登入工作階段（如 AWS IAM Identity Center 的臨時憑證），對自動化盡量改用平台原生的角色綁定，把還在用的長期 key 列一張清單、設定定期輪替。身分與憑證的完整地基在「模組二：身分與憑證地基」展開，這裡先擋住最容易致命的那一個。</p>
<p>刪除 production 資源是第二類。手動操作沒有「先看會影響什麼」的步驟，刪一個 security group 或 volume 可能瞬間讓服務失聯。對承載狀態的資源（資料庫、儲存桶、有持久資料的磁碟）開啟平台的刪除保護（如 termination protection、deletion protection），讓誤點多一道阻力。網路規則的大改是第三類 — 調整 VPC 路由、subnet 或對外規則時，先確認回退方式存在再動手，網路地基的系統性設計在「模組三：網路地基」。</p>
<p>這三類的共同點是：護欄成本低、失誤代價高，所以即使還沒有 IaC，CP 值也足以先做。</p>
<h2 id="讓變更留下痕跡">讓變更留下痕跡</h2>
<p>變更留痕的責任是讓「誰、在什麼時候、改了什麼、為什麼」事後可追溯，這是手動階段最接近版本控制的替代品。IaC 的 git history 天然提供這件事，手動環境得靠人為紀律補上。</p>
<p>最低限度是一份變更日誌，可以只是 repo 裡的一個 <code>CHANGELOG</code> 或團隊共用文件，每次動 production 就追加一行：時間、操作者、改了哪個資源、原因。它不需要漂亮，需要的是每次都寫。和它互補的是平台的稽核日誌（如 AWS CloudTrail），稽核日誌記錄 API 層級「發生了什麼」，人寫的日誌補上「為什麼」— 前者你查得到某個 security group 在幾點被改，後者你才知道那次改動是為了什麼需求。兩者一起，事故排查時才能從「哪裡變了」一路追到「能不能安全回退」。</p>
<p>常見陷阱是只在「大改動」時才記錄，結果真正出事的往往是某次以為無關緊要的小調整。判準簡化成一句：只要這個操作別人事後可能需要知道，就記。</p>
<h2 id="命名與-tagging-從手動階段就開始">命名與 tagging 從手動階段就開始</h2>
<p>命名規範與資源標籤是降低未來 import 成本的最低成本投資，它的責任是讓每個資源自帶「我是誰、屬於哪個服務、誰負責、哪個環境」的身分資訊。手動點出來的資源若名稱是 <code>test-2</code>、<code>new-db-final</code>，日後納管時得靠人逐一辨認哪個還在用、屬於哪條業務線，這個考古成本遠高於當初多打幾個字。</p>
<p>從手動階段就固定一套規則：資源名稱帶上服務與環境（如 <code>payments-api-prod</code>），標籤至少包含 <code>service</code>、<code>env</code>、<code>owner</code> 三個維度。這套規則在還沒 IaC 時靠人手動填，等到導入 IaC，這些標籤直接成為 Terraform 把現有資源對應到程式碼的依據，也是模組八治理習慣裡成本歸因與批次操作的基礎（見「模組八：治理好習慣」的 tagging 段）。先建立規範的價值在於：早一天統一，需要回頭重命名的資源就少一批。</p>
<h2 id="盤點現有資源作為納管輸入">盤點現有資源作為納管輸入</h2>
<p>資源盤點的責任是把「現實長什麼樣」寫成一份清單，它是日後納管的直接輸入 — 不知道有什麼，就無法決定先 import 什麼。手動環境最危險的是沒人記得還開著的資源。</p>
<p>盤點不必一次到位，先用平台工具把現況拉出來，存成可比對的形式：</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"># 列出某區域所有 EC2 instance 與其關鍵標籤</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">aws ec2 describe-instances <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;Reservations[].Instances[].[InstanceId,Tags,State.Name]&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --output table
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># 列出所有 security group 與開放規則，找出對外開放的 port</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">aws ec2 describe-security-groups <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;SecurityGroups[].[GroupId,GroupName,IpPermissions]&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="se"></span>  --output json</span></span></code></pre></div><p>把輸出存進 repo，定期重跑比對差異，就能看出環境在背景悄悄長出了什麼。這份清單同時服務三件事：當下的安全盤查（有沒有不該開的對外 port）、未來 IaC import 的範圍界定、以及成熟度評估時「全手動到底有多少資源」的事實基礎（成熟度階梯的定位見「模組零：infra 是什麼」）。</p>
<h2 id="資源與信任不足下的高槓桿取捨">資源與信任不足下的高槓桿取捨</h2>
<p>當時間、人力或上層信任都不足，無法一次把上面每件事做齊時，取捨原則是先做「失誤代價高且護欄成本低」的少數幾件。在這個情境下，最划算的通常是兩件：先擋長期憑證外洩，因為一次外洩可能拖垮整個帳號；再開啟有狀態資源的刪除保護，因為資料一旦刪除多半無法復原。</p>
<p>變更日誌與資源盤點屬於累積型投資 — 越早開始，未來省的考古成本越多，但晚一週開始不會立刻出事，所以在資源極度受限時可以排在護欄之後。命名與 tagging 的取捨點在於：新建資源時順手套規則幾乎零成本，回頭重整存量資源才貴，所以策略是「新的一律照規範、舊的等有餘力再補」，而不是停下來先整理全部存量。資源不足時怎麼跟上層談這些工作的優先級，在「模組九：怎麼把 infra 推動起來」展開。</p>
<h2 id="該開始導入-iac-的訊號">該開始導入 IaC 的訊號</h2>
<p>手動環境到了某些訊號出現時，繼續手動的邊際成本會超過導入 IaC 的一次性成本，這就是該往模組一跨進去的時機。訊號是規模與協作的函數，不是時間的函數。</p>
<p>第一個訊號是環境數量變多：當你需要 dev、staging、production 三套幾乎一樣的環境，手動複製會在環境之間留下難以察覺的差異，而 IaC 的價值正是用同一份程式碼複製環境。第二個是多人同時動資源：一個人手動操作還能靠記憶維護，兩三個人並行時，沒有 plan / review 的手動變更會互相覆蓋、互相破壞。第三個是環境爆炸頻率上升：如果「改一個設定結果弄壞別的東西」這類事故開始每月發生，代表手動環境的隱性依賴已經超過人腦能追蹤的上限。</p>
<p>任一訊號穩定出現，就是把第一個資源納入 IaC 的起點 — 前面做的命名、tagging、資源盤點此時直接成為 import 的輸入，第一步怎麼跨進去在「模組一：最小可行 IaC」。在訊號出現前過早導入 IaC 也有代價：單人、單環境、低變更頻率時，IaC 的學習與維護成本可能高於它省下的手動工，所以這裡的判準是等訊號、不是趕進度。</p>
<h2 id="章節文章">章節文章</h2>
<table>
  <thead>
      <tr>
          <th>文章</th>
          <th>主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/infra/before-infra/manual-environment-baseline/" data-link-title="手動環境的可控底線與納管準備" data-link-desc="還沒有 IaC 的環境怎麼守住底線、讓變更可追溯、降低未來納管成本，以及辨識何時該開始導入 IaC">手動環境的可控底線與納管準備</a></td>
          <td>還沒有 IaC 的環境怎麼守住底線、讓變更可追溯、降低未來納管成本，以及辨識何時該開始導入 IaC</td>
      </tr>
  </tbody>
</table>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/00-infra-mindset/" data-link-title="模組零：infra 是什麼，為什麼 day 1 就要鋪地基" data-link-desc="基礎設施的責任邊界、成熟度階梯，以及地基為什麼總在環境爆炸時才被看見">模組零：infra 是什麼</a>：成熟度階梯上「全手動」這一階的定位</li>
<li>→ <a href="/blog/infra/01-minimal-iac/" data-link-title="模組一：最小可行 IaC — state 地基與 Console 唯讀鐵律" data-link-desc="Terraform / OpenTofu 選型、remote state 與 lock，以及「Console 只能看不能改」鐵律">模組一：最小可行 IaC</a>：訊號出現後，第一步怎麼跨進 IaC</li>
<li>→ <a href="/blog/infra/02-identity-credentials/" data-link-title="模組二：身分與憑證地基 — IAM 與 OIDC" data-link-desc="IAM role / policy 設計、最小權限，以及用 OIDC 短期憑證取代長期 access key">模組二：身分與憑證地基</a>：長期憑證護欄的系統性設計</li>
<li>→ <a href="/blog/infra/03-network-foundation/" data-link-title="模組三：網路地基 — VPC 與分層" data-link-desc="VPC、public / private subnet 切分、route table、NAT、security group 設計">模組三：網路地基</a>：手動階段網路大改的回退考量、之後的系統性設計</li>
<li>→ <a href="/blog/infra/08-governance-habits/" data-link-title="模組八：治理好習慣 — 規模長大後不失控的最小節奏" data-link-desc="tagging 規範、secrets 不進 code、成本可見性、最小可行節奏，規模長大後不失控">模組八：治理好習慣</a>：tagging 在成本歸因與批次操作的後續價值</li>
<li>→ <a href="/blog/infra/09-driving-adoption/" data-link-title="模組九：怎麼把 infra 推動起來" data-link-desc="技術正確不等於推得動 — 信任赤字、期望值對齊、知識共享，infra 落地的組織課題">模組九：怎麼把 infra 推動起來</a>：資源不足時怎麼跟上層談優先級</li>
<li>→ <a href="/blog/infra/takeover/" data-link-title="接手維運：別人建的環境怎麼接管" data-link-desc="接手前人的專案時，怎麼在不搞壞東西的前提下盤點現況、建立維運能力、逐步正規化 — 從無 SSH 的 FTP 環境到有半套 IaC 的雲端環境都適用">接手維運：別人建的環境怎麼接管</a>：接手前人的專案時的盤點與接管流程</li>
</ul>
]]></content:encoded></item><item><title>Linux 安裝與機器初始化</title><link>https://tarrragon.github.io/blog/linux/install/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/install/</guid><description>&lt;p>這個系列處理一件編號模組預設你已經完成的事：把一台機器從「空的」變成「能接收 dotfile 的」。模組零到九教你怎麼用 code 管理工作環境，但它們都假設你手上已經有一台裝好 Linux、裝了基本工具、能從外部連入的機器。這個系列補的就是那段地基——OS 怎麼裝、裝完缺什麼、怎麼連進去跑 bootstrap。&lt;/p>
&lt;p>這段地基平常被跳過，是因為多數人是在一台早就裝好的機器上開始管理 dotfile。但只要你換到全新環境——開一台 VM、租一台雲端主機、拿到一台空機器——就會直接撞上這層：安裝程式問你十幾個選項該怎麼選、裝完發現連 &lt;code>sudo&lt;/code> 都沒有、想從本機連進去卻還沒有 SSH key。這些都不在編號模組的範圍，卻是跑 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/bootstrap-script-packages/" data-link-title="Bootstrap Script 與套件清單管理" data-link-desc="寫 dotfile 的 install script、或整理「這台機器裝了什麼」的套件清單時回來讀">模組八的 bootstrap script&lt;/a> 之前必須先過的關。&lt;/p>
&lt;p>本系列的內容來自一次完整的 VM 實測：在 Apple Silicon 的 UTM 上從 archboot（Arch 的獨立網路安裝環境 ISO）裝 Arch Linux ARM、跑 dotfile 的 &lt;code>install.sh&lt;/code>、一路把 Hyprland 桌面拉起來。實測中每個卡關點都被記錄下來，這裡把它們蒸餾成可重用的判讀與決策，不綁特定發行版或虛擬化軟體。&lt;/p>
&lt;h2 id="在學習路徑中的位置">在學習路徑中的位置&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>階段&lt;/th>
 &lt;th>對應內容&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>地基（本系列）&lt;/td>
 &lt;td>OS 安裝決策、工具驗證、外部連入與 bootstrap 前置&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>為什麼管理 dotfile&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/00-dotfile-mindset/" data-link-title="模組零：Dotfile 心智模型" data-link-desc="換機器、開 VM、重灌系統時需要快速還原開發環境，或想釐清哪些配置該版控、哪些該排除時回來讀">模組零：心智模型&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>怎麼管理&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/01-dotfile-management/" data-link-title="模組一：管理工具與目錄結構" data-link-desc="要把散落在家目錄的配置檔集中版控時，選 bare repo、stow 還是 chezmoi、目錄該怎麼組織">模組一到七&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>怎麼一鍵還原&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/" data-link-title="模組八：同步、Bootstrap 與環境重建" data-link-desc="換機器或重灌時怎麼還原工作環境 — bootstrap script 設計、套件清單管理、跨機器同步策略、secret 排除，以及 VM 快照和 dotfile 重建兩種思路的場景判讀">模組八：同步與 Bootstrap&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/00-dotfile-mindset/setup-order-guide/" data-link-title="環境建置的操作順序" data-link-desc="第一次從零建立 Linux 或 macOS 開發環境、不確定先做什麼後做什麼時讀 — 依賴順序路線圖，每一步附對應模組連結">模組零的操作順序指引&lt;/a> 列出從 OS 安裝到桌面就緒的完整依賴鏈，但只把「安裝作業系統」標成一步。本系列是那一步的展開：安裝程式每個選項背後的取捨、裝完之後的驗證、以及連入機器的幾種路徑。&lt;/p>
&lt;h2 id="文章">文章&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>文章&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;th>回答什麼問題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="basic-operations/">安裝過程用到的基礎操作&lt;/a>&lt;/td>
 &lt;td>系列用到的基礎操作：&lt;code>su -&lt;/code>、nano 編輯、檔名/指令大小寫、shell 符號（已熟可跳過）&lt;/td>
 &lt;td>照做時撞到沒見過的基礎指令怎麼辦&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="install-option-decisions/">Linux 安裝選項判讀&lt;/a>&lt;/td>
 &lt;td>安裝程式各選項的決策方針：locale、網路、鏡像、磁碟分割、檔案系統、bootloader&lt;/td>
 &lt;td>安裝程式問我這個選項，我該根據什麼選&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="minimal-install-verify/">最小安裝後的工具驗證與補足&lt;/a>&lt;/td>
 &lt;td>最小系統缺哪些必要工具、怎麼驗證、怎麼補&lt;/td>
 &lt;td>為什麼裝完連 sudo 都沒有、bootstrap 跑不起來&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="package-and-network-troubleshooting/">安裝期套件與網路故障排除&lt;/a>&lt;/td>
 &lt;td>第一次抓套件就失敗：分「連不到（DNS/mirror）」vs「連得到但被拒（db lock/簽章/partial/404）」&lt;/td>
 &lt;td>剛裝好跑 pacman 就報錯，是網路還是套件管理器的問題&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="ssh-keyless-bootstrap/">外部連入、SSH key 與無 key 的 bootstrap 路徑&lt;/a>&lt;/td>
 &lt;td>啟用 sshd、從本機連入、設 SSH key，以及還沒有 key 時怎麼把 dotfile 弄進去&lt;/td>
 &lt;td>怎麼從舒適的本機終端機操作新機器、沒有 key 時怎麼辦&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="observable-bootstrap/">可除錯的 bootstrap：把可觀測性內建進安裝腳本&lt;/a>&lt;/td>
 &lt;td>bootstrap 失敗時怎麼留下可診斷的痕跡：log 落地、錯誤定位、狀態 dump&lt;/td>
 &lt;td>安裝腳本失敗時，為什麼我只能瞎找&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="unattended-remote-work/">讓機器跑無人值守的長任務&lt;/a>&lt;/td>
 &lt;td>無人值守執行的三個障礙與解：NOPASSWD sudo、終端機多工器、推送認證，以及 agent 權限放行的取捨&lt;/td>
 &lt;td>怎麼讓機器在我離開後自己跑完任務、把成果送回來&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="platform-divergence-map/">平台與發行版差異的判讀地圖&lt;/a>&lt;/td>
 &lt;td>差異的四層（套件管理器 / 套件名 / 存在性 / 版本節奏）、除錯前先定平台、bootstrap 分歧判準&lt;/td>
 &lt;td>跨平台的清單與腳本該怎麼切、除錯時先確認什麼&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="gui-apps-install-verify/">GUI 應用的安裝驗證&lt;/a>&lt;/td>
 &lt;td>拆包生態（本體與功能模組分離）、首跑同意對話框、播放驗證鏈、VM 硬體解碼回退&lt;/td>
 &lt;td>GUI 應用裝了打不開 / 無聲 / 不能播該查哪一層&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>機器裝好、能連入之後若出問題（連不上、終端機亂、程式行為怪），除錯與診斷自成一組，見同層的 &lt;a href="../debug/">Linux 除錯與診斷&lt;/a>。&lt;/p>
&lt;h2 id="依情境的讀法">依情境的讀法&lt;/h2>
&lt;p>主線那幾篇照「安裝 → 驗證 → 連入 → 可除錯 → 無人值守」的順序，是「從零開一台新機器、到讓它自己跑活」的完整路線，但不是每個讀者都從零開始：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>接手一台別人已裝好的機器&lt;/strong>：OS 已經在，從 &lt;a href="minimal-install-verify/">最小安裝後的工具驗證與補足&lt;/a> 切入，確認它缺不缺你流程要的工具。&lt;/li>
&lt;li>&lt;strong>雲端主機初始化&lt;/strong>：雲端主機多半已附 OS image、已有 sudo 與注入的 key，適用的是 &lt;a href="ssh-keyless-bootstrap/">外部連入、SSH key 與無 key 的 bootstrap 路徑&lt;/a> 跟 &lt;a href="observable-bootstrap/">可除錯的 bootstrap&lt;/a>，前兩篇的 ISO 安裝可略過。&lt;/li>
&lt;li>&lt;strong>bootstrap 失敗來 debug&lt;/strong>：直接讀 &lt;a href="observable-bootstrap/">可除錯的 bootstrap&lt;/a>，它也涵蓋「腳本不是你寫的、只想 debug 一次失敗」的情況。&lt;/li>
&lt;li>&lt;strong>讓機器無人值守跑活&lt;/strong>：機器已能連入操作，只想設好讓它在你離開後自己跑長任務或 agent，直接讀 &lt;a href="unattended-remote-work/">讓機器跑無人值守的長任務&lt;/a>。&lt;/li>
&lt;li>&lt;strong>遇到問題要除錯&lt;/strong>：機器已在跑但出狀況（連不上、終端機亂、程式行為怪），進 &lt;a href="../debug/">Linux 除錯與診斷&lt;/a>，從 &lt;a href="../debug/diagnosis-read-authoritative-state/">診斷心法&lt;/a> 建立判讀紀律，再依症狀分流。&lt;/li>
&lt;li>&lt;strong>裝好後想讓它自己顧&lt;/strong>：服務跑起來後，主動確認這台有沒有在監控自己的服務死活（&lt;code>systemctl show sshd -p OnFailure&lt;/code>），沒有就從最簡單的 OnFailure + ntfy 建起——遠端機器至少把 sshd 掛上，掛了就失聯。見 &lt;a href="../debug/service-failure-monitoring/">服務掛了怎麼自動知道&lt;/a>。&lt;/li>
&lt;/ul>
&lt;h2 id="跟其他模組的交叉引用">跟其他模組的交叉引用&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/bootstrap-script-packages/" data-link-title="Bootstrap Script 與套件清單管理" data-link-desc="寫 dotfile 的 install script、或整理「這台機器裝了什麼」的套件清單時回來讀">模組八：Bootstrap script 設計&lt;/a>——本系列是它的前置；bootstrap 假設套件清單完整、機器可連入，本系列補「在那之前」。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/05-hyprland-config/hyprland-vm-setup/" data-link-title="Hyprland VM 環境設定與測試矩陣" data-link-desc="要在 VM 裡測試 Hyprland 配置、或判斷某個設定該在 VM 還是實機驗證時回來讀">模組五：Hyprland VM 測試&lt;/a>——本系列的 VM 安裝是它的下游前置；裝好機器才能測 Hyprland。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/log-reading-diagnostic-tools/" data-link-title="日誌判讀與診斷工具" data-link-desc="知道桌面出了問題但不確定原因時回來讀 — journalctl、dmesg、hyprctl、systemctl 的使用方式和常見 log pattern">模組七：日誌判讀與診斷工具&lt;/a>——「可除錯的 bootstrap」與它呼應：前者談怎麼產生可診斷的 log，後者談怎麼讀。&lt;/li>
&lt;li>&lt;a href="../debug/">Linux 除錯與診斷&lt;/a>——本系列裝好、連入之後的下游；出問題時的判讀紀律與情境分流。&lt;/li>
&lt;li>&lt;a href="../tools/">Linux 工具選單&lt;/a>——安裝與除錯要用的工具（CLI / 圖形桌面 / 遠端）有哪些選擇、推薦用哪些。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/infra/00-infra-mindset/first-day-with-cloud-account/" data-link-title="拿到雲端帳號的第一天" data-link-desc="被指派 infra 工作、拿到 AWS 或 GCP 帳號、不確定該先做什麼時讀 — 第一小時安全底線、帳號現況判讀、後續學習路線分流">Infra 心智模型：拿到雲端帳號的第一天&lt;/a>——雲端主機的機器初始化是本系列的上游情境；被指派 infra 工作、拿到一台雲端主機後，先過本系列的 OS 連入與 bootstrap 前置，再進 infra 的資源管理。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個系列處理一件編號模組預設你已經完成的事：把一台機器從「空的」變成「能接收 dotfile 的」。模組零到九教你怎麼用 code 管理工作環境，但它們都假設你手上已經有一台裝好 Linux、裝了基本工具、能從外部連入的機器。這個系列補的就是那段地基——OS 怎麼裝、裝完缺什麼、怎麼連進去跑 bootstrap。</p>
<p>這段地基平常被跳過，是因為多數人是在一台早就裝好的機器上開始管理 dotfile。但只要你換到全新環境——開一台 VM、租一台雲端主機、拿到一台空機器——就會直接撞上這層：安裝程式問你十幾個選項該怎麼選、裝完發現連 <code>sudo</code> 都沒有、想從本機連進去卻還沒有 SSH key。這些都不在編號模組的範圍，卻是跑 <a href="/blog/linux/dotfile/08-sync-bootstrap/bootstrap-script-packages/" data-link-title="Bootstrap Script 與套件清單管理" data-link-desc="寫 dotfile 的 install script、或整理「這台機器裝了什麼」的套件清單時回來讀">模組八的 bootstrap script</a> 之前必須先過的關。</p>
<p>本系列的內容來自一次完整的 VM 實測：在 Apple Silicon 的 UTM 上從 archboot（Arch 的獨立網路安裝環境 ISO）裝 Arch Linux ARM、跑 dotfile 的 <code>install.sh</code>、一路把 Hyprland 桌面拉起來。實測中每個卡關點都被記錄下來，這裡把它們蒸餾成可重用的判讀與決策，不綁特定發行版或虛擬化軟體。</p>
<h2 id="在學習路徑中的位置">在學習路徑中的位置</h2>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>對應內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>地基（本系列）</td>
          <td>OS 安裝決策、工具驗證、外部連入與 bootstrap 前置</td>
      </tr>
      <tr>
          <td>為什麼管理 dotfile</td>
          <td><a href="/blog/linux/dotfile/00-dotfile-mindset/" data-link-title="模組零：Dotfile 心智模型" data-link-desc="換機器、開 VM、重灌系統時需要快速還原開發環境，或想釐清哪些配置該版控、哪些該排除時回來讀">模組零：心智模型</a></td>
      </tr>
      <tr>
          <td>怎麼管理</td>
          <td><a href="/blog/linux/dotfile/01-dotfile-management/" data-link-title="模組一：管理工具與目錄結構" data-link-desc="要把散落在家目錄的配置檔集中版控時，選 bare repo、stow 還是 chezmoi、目錄該怎麼組織">模組一到七</a></td>
      </tr>
      <tr>
          <td>怎麼一鍵還原</td>
          <td><a href="/blog/linux/dotfile/08-sync-bootstrap/" data-link-title="模組八：同步、Bootstrap 與環境重建" data-link-desc="換機器或重灌時怎麼還原工作環境 — bootstrap script 設計、套件清單管理、跨機器同步策略、secret 排除，以及 VM 快照和 dotfile 重建兩種思路的場景判讀">模組八：同步與 Bootstrap</a></td>
      </tr>
  </tbody>
</table>
<p><a href="/blog/linux/dotfile/00-dotfile-mindset/setup-order-guide/" data-link-title="環境建置的操作順序" data-link-desc="第一次從零建立 Linux 或 macOS 開發環境、不確定先做什麼後做什麼時讀 — 依賴順序路線圖，每一步附對應模組連結">模組零的操作順序指引</a> 列出從 OS 安裝到桌面就緒的完整依賴鏈，但只把「安裝作業系統」標成一步。本系列是那一步的展開：安裝程式每個選項背後的取捨、裝完之後的驗證、以及連入機器的幾種路徑。</p>
<h2 id="文章">文章</h2>
<table>
  <thead>
      <tr>
          <th>文章</th>
          <th>主題</th>
          <th>回答什麼問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="basic-operations/">安裝過程用到的基礎操作</a></td>
          <td>系列用到的基礎操作：<code>su -</code>、nano 編輯、檔名/指令大小寫、shell 符號（已熟可跳過）</td>
          <td>照做時撞到沒見過的基礎指令怎麼辦</td>
      </tr>
      <tr>
          <td><a href="install-option-decisions/">Linux 安裝選項判讀</a></td>
          <td>安裝程式各選項的決策方針：locale、網路、鏡像、磁碟分割、檔案系統、bootloader</td>
          <td>安裝程式問我這個選項，我該根據什麼選</td>
      </tr>
      <tr>
          <td><a href="minimal-install-verify/">最小安裝後的工具驗證與補足</a></td>
          <td>最小系統缺哪些必要工具、怎麼驗證、怎麼補</td>
          <td>為什麼裝完連 sudo 都沒有、bootstrap 跑不起來</td>
      </tr>
      <tr>
          <td><a href="package-and-network-troubleshooting/">安裝期套件與網路故障排除</a></td>
          <td>第一次抓套件就失敗：分「連不到（DNS/mirror）」vs「連得到但被拒（db lock/簽章/partial/404）」</td>
          <td>剛裝好跑 pacman 就報錯，是網路還是套件管理器的問題</td>
      </tr>
      <tr>
          <td><a href="ssh-keyless-bootstrap/">外部連入、SSH key 與無 key 的 bootstrap 路徑</a></td>
          <td>啟用 sshd、從本機連入、設 SSH key，以及還沒有 key 時怎麼把 dotfile 弄進去</td>
          <td>怎麼從舒適的本機終端機操作新機器、沒有 key 時怎麼辦</td>
      </tr>
      <tr>
          <td><a href="observable-bootstrap/">可除錯的 bootstrap：把可觀測性內建進安裝腳本</a></td>
          <td>bootstrap 失敗時怎麼留下可診斷的痕跡：log 落地、錯誤定位、狀態 dump</td>
          <td>安裝腳本失敗時，為什麼我只能瞎找</td>
      </tr>
      <tr>
          <td><a href="unattended-remote-work/">讓機器跑無人值守的長任務</a></td>
          <td>無人值守執行的三個障礙與解：NOPASSWD sudo、終端機多工器、推送認證，以及 agent 權限放行的取捨</td>
          <td>怎麼讓機器在我離開後自己跑完任務、把成果送回來</td>
      </tr>
      <tr>
          <td><a href="platform-divergence-map/">平台與發行版差異的判讀地圖</a></td>
          <td>差異的四層（套件管理器 / 套件名 / 存在性 / 版本節奏）、除錯前先定平台、bootstrap 分歧判準</td>
          <td>跨平台的清單與腳本該怎麼切、除錯時先確認什麼</td>
      </tr>
      <tr>
          <td><a href="gui-apps-install-verify/">GUI 應用的安裝驗證</a></td>
          <td>拆包生態（本體與功能模組分離）、首跑同意對話框、播放驗證鏈、VM 硬體解碼回退</td>
          <td>GUI 應用裝了打不開 / 無聲 / 不能播該查哪一層</td>
      </tr>
  </tbody>
</table>
<p>機器裝好、能連入之後若出問題（連不上、終端機亂、程式行為怪），除錯與診斷自成一組，見同層的 <a href="../debug/">Linux 除錯與診斷</a>。</p>
<h2 id="依情境的讀法">依情境的讀法</h2>
<p>主線那幾篇照「安裝 → 驗證 → 連入 → 可除錯 → 無人值守」的順序，是「從零開一台新機器、到讓它自己跑活」的完整路線，但不是每個讀者都從零開始：</p>
<ul>
<li><strong>接手一台別人已裝好的機器</strong>：OS 已經在，從 <a href="minimal-install-verify/">最小安裝後的工具驗證與補足</a> 切入，確認它缺不缺你流程要的工具。</li>
<li><strong>雲端主機初始化</strong>：雲端主機多半已附 OS image、已有 sudo 與注入的 key，適用的是 <a href="ssh-keyless-bootstrap/">外部連入、SSH key 與無 key 的 bootstrap 路徑</a> 跟 <a href="observable-bootstrap/">可除錯的 bootstrap</a>，前兩篇的 ISO 安裝可略過。</li>
<li><strong>bootstrap 失敗來 debug</strong>：直接讀 <a href="observable-bootstrap/">可除錯的 bootstrap</a>，它也涵蓋「腳本不是你寫的、只想 debug 一次失敗」的情況。</li>
<li><strong>讓機器無人值守跑活</strong>：機器已能連入操作，只想設好讓它在你離開後自己跑長任務或 agent，直接讀 <a href="unattended-remote-work/">讓機器跑無人值守的長任務</a>。</li>
<li><strong>遇到問題要除錯</strong>：機器已在跑但出狀況（連不上、終端機亂、程式行為怪），進 <a href="../debug/">Linux 除錯與診斷</a>，從 <a href="../debug/diagnosis-read-authoritative-state/">診斷心法</a> 建立判讀紀律，再依症狀分流。</li>
<li><strong>裝好後想讓它自己顧</strong>：服務跑起來後，主動確認這台有沒有在監控自己的服務死活（<code>systemctl show sshd -p OnFailure</code>），沒有就從最簡單的 OnFailure + ntfy 建起——遠端機器至少把 sshd 掛上，掛了就失聯。見 <a href="../debug/service-failure-monitoring/">服務掛了怎麼自動知道</a>。</li>
</ul>
<h2 id="跟其他模組的交叉引用">跟其他模組的交叉引用</h2>
<ul>
<li><a href="/blog/linux/dotfile/08-sync-bootstrap/bootstrap-script-packages/" data-link-title="Bootstrap Script 與套件清單管理" data-link-desc="寫 dotfile 的 install script、或整理「這台機器裝了什麼」的套件清單時回來讀">模組八：Bootstrap script 設計</a>——本系列是它的前置；bootstrap 假設套件清單完整、機器可連入，本系列補「在那之前」。</li>
<li><a href="/blog/linux/dotfile/05-hyprland-config/hyprland-vm-setup/" data-link-title="Hyprland VM 環境設定與測試矩陣" data-link-desc="要在 VM 裡測試 Hyprland 配置、或判斷某個設定該在 VM 還是實機驗證時回來讀">模組五：Hyprland VM 測試</a>——本系列的 VM 安裝是它的下游前置；裝好機器才能測 Hyprland。</li>
<li><a href="/blog/linux/dotfile/07-desktop-maintenance/log-reading-diagnostic-tools/" data-link-title="日誌判讀與診斷工具" data-link-desc="知道桌面出了問題但不確定原因時回來讀 — journalctl、dmesg、hyprctl、systemctl 的使用方式和常見 log pattern">模組七：日誌判讀與診斷工具</a>——「可除錯的 bootstrap」與它呼應：前者談怎麼產生可診斷的 log，後者談怎麼讀。</li>
<li><a href="../debug/">Linux 除錯與診斷</a>——本系列裝好、連入之後的下游；出問題時的判讀紀律與情境分流。</li>
<li><a href="../tools/">Linux 工具選單</a>——安裝與除錯要用的工具（CLI / 圖形桌面 / 遠端）有哪些選擇、推薦用哪些。</li>
<li><a href="/blog/infra/00-infra-mindset/first-day-with-cloud-account/" data-link-title="拿到雲端帳號的第一天" data-link-desc="被指派 infra 工作、拿到 AWS 或 GCP 帳號、不確定該先做什麼時讀 — 第一小時安全底線、帳號現況判讀、後續學習路線分流">Infra 心智模型：拿到雲端帳號的第一天</a>——雲端主機的機器初始化是本系列的上游情境；被指派 infra 工作、拿到一台雲端主機後，先過本系列的 OS 連入與 bootstrap 前置，再進 infra 的資源管理。</li>
</ul>
]]></content:encoded></item><item><title>拍照 vs 重建指令：環境重建的兩種思路</title><link>https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/snapshot-vs-rebuild/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/snapshot-vs-rebuild/</guid><description>&lt;p>環境重建是 dotfile 管理的最終目的：拿到一台空白機器，能在可預期的時間內還原成你熟悉的工作環境。這件事有兩條根本不同的路線，選哪條決定了你之後所有的管理策略。&lt;/p>
&lt;h2 id="拍照vm-快照與磁碟映像">拍照：VM 快照與磁碟映像&lt;/h2>
&lt;p>第一種是&lt;strong>拍照&lt;/strong>。VM 快照和磁碟映像（Clonezilla、&lt;code>dd&lt;/code>）做的事是把整台機器某一刻的完整狀態凍結存檔——整個虛擬硬碟的 block-level 複製，有時連記憶體狀態都包含。還原就是把映像寫回去，系統回到那一刻，像時光倒流。Docker 的 &lt;code>docker commit&lt;/code> 也屬於這個方向：把正在跑的 container 的檔案系統快照成一個 image。&lt;/p>
&lt;p>拍照產出的是&lt;strong>黑盒子&lt;/strong>。一個磁碟映像是二進制檔案，沒人能看出裡面到底做了什麼設定、裝了什麼、改過什麼。它大（動輒 GB 級）、跟硬體耦合（換不同架構或不同顯卡可能開不起來）、無法做 diff 或 code review。&lt;/p>
&lt;h2 id="重建指令dotfile-repo--install-script">重建指令：Dotfile repo + install script&lt;/h2>
&lt;p>第二種是&lt;strong>重建指令&lt;/strong>。Dotfile repo + install script 描述的是「怎麼從一台空白機器組出這個環境」，每次都從零開始執行。Dockerfile 也是重建指令——一份「照著做就能重現」的食譜，描述每一步該安裝什麼、複製什麼、怎麼啟動。&lt;/p>
&lt;p>重建指令產出的是&lt;strong>白盒子&lt;/strong>。每一步都是可讀的文字——這行裝 zsh、那行設定 Hyprland 的 keybind——可以被 review、被 diff、被另一個人讀懂。它小（通常幾十 KB）、跨硬體（同一份 script 加 OS 判斷就能跑在不同機器）、可以進版控走 PR 流程。&lt;/p>
&lt;p>dotfile 管理選的是重建指令這條路。代價是你必須把環境建構的過程記錄清楚——每裝一個新工具、每改一個配置都要同步更新 repo。回報是任何一台機器、任何時間點，都能用一份 Git repo 重現你的工作環境。&lt;/p>
&lt;h2 id="場景判讀">場景判讀&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>需求&lt;/th>
 &lt;th>VM 快照&lt;/th>
 &lt;th>Dotfile 重建&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>保留某一刻的完整系統狀態&lt;/td>
 &lt;td>適合（block-level 完整備份）&lt;/td>
 &lt;td>不適合（只管配置層）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>在新機器還原工作環境&lt;/td>
 &lt;td>不適合（硬體耦合、映像大）&lt;/td>
 &lt;td>適合（跨硬體、輕量）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>讓環境可被他人重現（onboarding）&lt;/td>
 &lt;td>勉強（黑盒子、難維護）&lt;/td>
 &lt;td>適合（白盒子、可 review）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>在多台機器維持一致&lt;/td>
 &lt;td>不適合（每台都要拍照）&lt;/td>
 &lt;td>適合（一份 repo、多台 apply）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>做實驗後回滾（改壞了想恢復）&lt;/td>
 &lt;td>適合（秒級回滾）&lt;/td>
 &lt;td>要靠 git revert + 重新 apply&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>讓桌面配置進 review 流程&lt;/td>
 &lt;td>不適合（二進制映像無法 diff）&lt;/td>
 &lt;td>適合（純文字、可 diff、可 PR）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>兩者不互斥——常見的組合是：用 dotfile 管理配置（長期可維護的基線），VM 快照用於短期實驗保護（改爛了可以秒回）。&lt;/p></description><content:encoded><![CDATA[<p>環境重建是 dotfile 管理的最終目的：拿到一台空白機器，能在可預期的時間內還原成你熟悉的工作環境。這件事有兩條根本不同的路線，選哪條決定了你之後所有的管理策略。</p>
<h2 id="拍照vm-快照與磁碟映像">拍照：VM 快照與磁碟映像</h2>
<p>第一種是<strong>拍照</strong>。VM 快照和磁碟映像（Clonezilla、<code>dd</code>）做的事是把整台機器某一刻的完整狀態凍結存檔——整個虛擬硬碟的 block-level 複製，有時連記憶體狀態都包含。還原就是把映像寫回去，系統回到那一刻，像時光倒流。Docker 的 <code>docker commit</code> 也屬於這個方向：把正在跑的 container 的檔案系統快照成一個 image。</p>
<p>拍照產出的是<strong>黑盒子</strong>。一個磁碟映像是二進制檔案，沒人能看出裡面到底做了什麼設定、裝了什麼、改過什麼。它大（動輒 GB 級）、跟硬體耦合（換不同架構或不同顯卡可能開不起來）、無法做 diff 或 code review。</p>
<h2 id="重建指令dotfile-repo--install-script">重建指令：Dotfile repo + install script</h2>
<p>第二種是<strong>重建指令</strong>。Dotfile repo + install script 描述的是「怎麼從一台空白機器組出這個環境」，每次都從零開始執行。Dockerfile 也是重建指令——一份「照著做就能重現」的食譜，描述每一步該安裝什麼、複製什麼、怎麼啟動。</p>
<p>重建指令產出的是<strong>白盒子</strong>。每一步都是可讀的文字——這行裝 zsh、那行設定 Hyprland 的 keybind——可以被 review、被 diff、被另一個人讀懂。它小（通常幾十 KB）、跨硬體（同一份 script 加 OS 判斷就能跑在不同機器）、可以進版控走 PR 流程。</p>
<p>dotfile 管理選的是重建指令這條路。代價是你必須把環境建構的過程記錄清楚——每裝一個新工具、每改一個配置都要同步更新 repo。回報是任何一台機器、任何時間點，都能用一份 Git repo 重現你的工作環境。</p>
<h2 id="場景判讀">場景判讀</h2>
<table>
  <thead>
      <tr>
          <th>需求</th>
          <th>VM 快照</th>
          <th>Dotfile 重建</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>保留某一刻的完整系統狀態</td>
          <td>適合（block-level 完整備份）</td>
          <td>不適合（只管配置層）</td>
      </tr>
      <tr>
          <td>在新機器還原工作環境</td>
          <td>不適合（硬體耦合、映像大）</td>
          <td>適合（跨硬體、輕量）</td>
      </tr>
      <tr>
          <td>讓環境可被他人重現（onboarding）</td>
          <td>勉強（黑盒子、難維護）</td>
          <td>適合（白盒子、可 review）</td>
      </tr>
      <tr>
          <td>在多台機器維持一致</td>
          <td>不適合（每台都要拍照）</td>
          <td>適合（一份 repo、多台 apply）</td>
      </tr>
      <tr>
          <td>做實驗後回滾（改壞了想恢復）</td>
          <td>適合（秒級回滾）</td>
          <td>要靠 git revert + 重新 apply</td>
      </tr>
      <tr>
          <td>讓桌面配置進 review 流程</td>
          <td>不適合（二進制映像無法 diff）</td>
          <td>適合（純文字、可 diff、可 PR）</td>
      </tr>
  </tbody>
</table>
<p>兩者不互斥——常見的組合是：用 dotfile 管理配置（長期可維護的基線），VM 快照用於短期實驗保護（改爛了可以秒回）。</p>
]]></content:encoded></item><item><title>手動環境的可控底線與納管準備</title><link>https://tarrragon.github.io/blog/infra/before-infra/manual-environment-baseline/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/before-infra/manual-environment-baseline/</guid><description>&lt;p>手動起家是絕大多數服務的常態起點。從一個人在 Console 點出第一台 EC2 驗證想法，到小團隊接手開始長出更多資源，環境會經歷一段「全部靠手動、沒有任何程式碼描述」的階段。這個階段在&lt;a href="https://tarrragon.github.io/blog/infra/00-infra-mindset/#%e6%88%90%e7%86%9f%e5%ba%a6%e9%9a%8e%e6%a2%af" data-link-title="模組零：infra 是什麼，為什麼 day 1 就要鋪地基" data-link-desc="基礎設施的責任邊界、成熟度階梯，以及地基為什麼總在環境爆炸時才被看見">成熟度階梯&lt;/a>（從全手動到全程式碼治理的五階分級）上屬於第零階，它的責任是把自己管理成「可控的手動」，而不是假裝已經納管。可控意味著三件事：高風險操作有護欄、關鍵變更有痕跡、現實長什麼樣有紀錄。做好這三件事，當下出事的成本降低，未來把資源 import 進 IaC 的成本也跟著降低。&lt;/p>
&lt;h2 id="判讀自己是否可控">判讀自己是否可控&lt;/h2>
&lt;p>可控的手動環境能在五分鐘內回答以下問題：&lt;/p>
&lt;ol>
&lt;li>production 有哪些對外開放的 port？&lt;/li>
&lt;li>上週誰動過資料庫參數，動了什麼？&lt;/li>
&lt;li>刪掉某台機器會不會連帶弄壞別的東西？&lt;/li>
&lt;li>現在用了幾把長期 access key，每把用在哪裡？&lt;/li>
&lt;li>有沒有一份清單能對照 Console 上的資源，確認沒有漏掉的？&lt;/li>
&lt;/ol>
&lt;p>五題都能答的團隊不多，目標也不是一次全通。辨識出哪些區域不可見，按傷害代價從高到低逐一收斂，就是這一章的路線。&lt;/p>
&lt;h2 id="護欄先上在回退代價最高的操作">護欄先上在回退代價最高的操作&lt;/h2>
&lt;p>手動環境沒有 IaC 的 &lt;code>plan&lt;/code> / &lt;code>diff&lt;/code> 當預檢，人為操作直接生效。護欄的優先級看的是失誤的回退代價，不是操作頻率。回退代價最高的三類操作各自有最低成本的防線。&lt;/p>
&lt;h3 id="長期憑證外洩">長期憑證外洩&lt;/h3>
&lt;p>長期 access key 一旦外流，攻擊者拿到的是不會過期的權限。回退代價高的原因不只是撤銷這把 key 本身，而是要找出所有使用它的地方同步更換 — 而「所有使用它的地方」在手動環境裡幾乎沒有完整清單。一把用了半年的 key 可能已經被複製到 CI 環境變數、某個同事的測試腳本、一個早已被遺忘但還在跑的 cron job 裡。&lt;/p>
&lt;p>最低成本的護欄分三步。第一步是盤點：列出帳號裡所有長期 access key，記下建立時間、上次使用時間與對應用途。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">aws iam generate-credential-report
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">aws iam get-credential-report --output text --query Content &lt;span class="p">|&lt;/span> base64 -d&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>第二步是替換路徑。對人類操作者，改用會過期的登入工作階段（如 AWS IAM Identity Center 的臨時憑證，幾小時後自動失效）。對跑在雲上的自動化（EC2 上的腳本、ECS task），改用平台原生的角色綁定 — instance profile 或 task role 會自動輪替短期憑證，程式碼不需要存任何 key。對跑在雲外的 CI/CD（如 GitHub Actions），改用 OIDC 聯合（見&lt;a href="https://tarrragon.github.io/blog/infra/02-identity-credentials/" data-link-title="模組二：身分與憑證地基 — IAM 與 OIDC" data-link-desc="IAM role / policy 設計、最小權限，以及用 OIDC 短期憑證取代長期 access key">模組二：身分與憑證地基&lt;/a>）。&lt;/p>
&lt;p>第三步是輪替紀律。把還在用的長期 key 設定定期輪替提醒（60 天或 90 天，對齊 AWS IAM credential report 的建議週期），每次輪替時問自己：這把 key 能不能這次就換成臨時憑證，讓它成為最後一次輪替？&lt;/p>
&lt;h3 id="刪除-production-資源">刪除 production 資源&lt;/h3>
&lt;p>在 Console 選中一個 security group 按刪除，平台可能只問「確定嗎？」就直接執行，不會告訴你有三個 EC2 instance 正在引用這個 group。EBS volume 被刪除後，上面的資料就不存在了 — 除非之前有做 snapshot，而手動環境裡有沒有做 snapshot 通常取決於某個人的記憶。&lt;/p>
&lt;p>對承載狀態的資源，最低成本的護欄是開啟平台的刪除保護：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">aws rds modify-db-instance &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --db-instance-identifier payments-prod &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --deletion-protection &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --apply-immediately
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">aws ec2 modify-instance-attribute &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --instance-id i-0abc123 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --disable-api-termination&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>RDS 有 &lt;code>deletion_protection&lt;/code>，EC2 有 &lt;code>termination_protection&lt;/code>，S3 bucket 可以開 MFA delete。這些機制把「一鍵刪除」變成「先關保護再刪除」兩步操作，擋不住蓄意刪除，但能擋住手滑跟批次操作的誤傷。&lt;/p>
&lt;p>刪除保護之外，備份是另一道防線。手動環境裡至少確認 RDS 的自動備份是開著的（預設保留 7 天），以及 S3 bucket 的 versioning 是開著的。S3 bucket 的 versioning 預設是關的，一個沒開 versioning 的 bucket，覆寫或刪除物件後就回不去了。&lt;/p>
&lt;h3 id="網路規則的大改">網路規則的大改&lt;/h3>
&lt;p>手動調整 VPC 路由、subnet 關聯的 route table、或 security group 的入站規則，影響範圍跨越多個服務，而且在手動環境裡沒有版本控制可以 diff 改了什麼。一條路由改錯，某些 private subnet 的服務可能瞬間失去出站能力。&lt;/p>
&lt;p>最低成本的護欄是「改之前先把現況存下來」：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">aws ec2 describe-security-groups &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --group-ids sg-0abc123 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --output json &amp;gt; sg-backup-&lt;span class="k">$(&lt;/span>date +%Y%m%d&lt;span class="k">)&lt;/span>.json&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>用 CLI 把當前的 security group 規則、route table 設定匯出一份 JSON。改完後如果出問題，這份 JSON 就是回退的依據。這不是自動回退 — 手動環境沒有那個能力 — 但至少讓「改回去」有個明確的目標狀態。網路地基的系統性設計在&lt;a href="https://tarrragon.github.io/blog/infra/03-network-foundation/" data-link-title="模組三：網路地基 — VPC 與分層" data-link-desc="VPC、public / private subnet 切分、route table、NAT、security group 設計">模組三：網路地基&lt;/a>展開。&lt;/p></description><content:encoded><![CDATA[<p>手動起家是絕大多數服務的常態起點。從一個人在 Console 點出第一台 EC2 驗證想法，到小團隊接手開始長出更多資源，環境會經歷一段「全部靠手動、沒有任何程式碼描述」的階段。這個階段在<a href="/blog/infra/00-infra-mindset/#%e6%88%90%e7%86%9f%e5%ba%a6%e9%9a%8e%e6%a2%af" data-link-title="模組零：infra 是什麼，為什麼 day 1 就要鋪地基" data-link-desc="基礎設施的責任邊界、成熟度階梯，以及地基為什麼總在環境爆炸時才被看見">成熟度階梯</a>（從全手動到全程式碼治理的五階分級）上屬於第零階，它的責任是把自己管理成「可控的手動」，而不是假裝已經納管。可控意味著三件事：高風險操作有護欄、關鍵變更有痕跡、現實長什麼樣有紀錄。做好這三件事，當下出事的成本降低，未來把資源 import 進 IaC 的成本也跟著降低。</p>
<h2 id="判讀自己是否可控">判讀自己是否可控</h2>
<p>可控的手動環境能在五分鐘內回答以下問題：</p>
<ol>
<li>production 有哪些對外開放的 port？</li>
<li>上週誰動過資料庫參數，動了什麼？</li>
<li>刪掉某台機器會不會連帶弄壞別的東西？</li>
<li>現在用了幾把長期 access key，每把用在哪裡？</li>
<li>有沒有一份清單能對照 Console 上的資源，確認沒有漏掉的？</li>
</ol>
<p>五題都能答的團隊不多，目標也不是一次全通。辨識出哪些區域不可見，按傷害代價從高到低逐一收斂，就是這一章的路線。</p>
<h2 id="護欄先上在回退代價最高的操作">護欄先上在回退代價最高的操作</h2>
<p>手動環境沒有 IaC 的 <code>plan</code> / <code>diff</code> 當預檢，人為操作直接生效。護欄的優先級看的是失誤的回退代價，不是操作頻率。回退代價最高的三類操作各自有最低成本的防線。</p>
<h3 id="長期憑證外洩">長期憑證外洩</h3>
<p>長期 access key 一旦外流，攻擊者拿到的是不會過期的權限。回退代價高的原因不只是撤銷這把 key 本身，而是要找出所有使用它的地方同步更換 — 而「所有使用它的地方」在手動環境裡幾乎沒有完整清單。一把用了半年的 key 可能已經被複製到 CI 環境變數、某個同事的測試腳本、一個早已被遺忘但還在跑的 cron job 裡。</p>
<p>最低成本的護欄分三步。第一步是盤點：列出帳號裡所有長期 access key，記下建立時間、上次使用時間與對應用途。</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">aws iam generate-credential-report
</span></span><span class="line"><span class="ln">2</span><span class="cl">aws iam get-credential-report --output text --query Content <span class="p">|</span> base64 -d</span></span></code></pre></div><p>第二步是替換路徑。對人類操作者，改用會過期的登入工作階段（如 AWS IAM Identity Center 的臨時憑證，幾小時後自動失效）。對跑在雲上的自動化（EC2 上的腳本、ECS task），改用平台原生的角色綁定 — instance profile 或 task role 會自動輪替短期憑證，程式碼不需要存任何 key。對跑在雲外的 CI/CD（如 GitHub Actions），改用 OIDC 聯合（見<a href="/blog/infra/02-identity-credentials/" data-link-title="模組二：身分與憑證地基 — IAM 與 OIDC" data-link-desc="IAM role / policy 設計、最小權限，以及用 OIDC 短期憑證取代長期 access key">模組二：身分與憑證地基</a>）。</p>
<p>第三步是輪替紀律。把還在用的長期 key 設定定期輪替提醒（60 天或 90 天，對齊 AWS IAM credential report 的建議週期），每次輪替時問自己：這把 key 能不能這次就換成臨時憑證，讓它成為最後一次輪替？</p>
<h3 id="刪除-production-資源">刪除 production 資源</h3>
<p>在 Console 選中一個 security group 按刪除，平台可能只問「確定嗎？」就直接執行，不會告訴你有三個 EC2 instance 正在引用這個 group。EBS volume 被刪除後，上面的資料就不存在了 — 除非之前有做 snapshot，而手動環境裡有沒有做 snapshot 通常取決於某個人的記憶。</p>
<p>對承載狀態的資源，最低成本的護欄是開啟平台的刪除保護：</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">aws rds modify-db-instance <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --db-instance-identifier payments-prod <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --deletion-protection <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --apply-immediately
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl">aws ec2 modify-instance-attribute <span class="se">\
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="se"></span>  --instance-id i-0abc123 <span class="se">\
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="se"></span>  --disable-api-termination</span></span></code></pre></div><p>RDS 有 <code>deletion_protection</code>，EC2 有 <code>termination_protection</code>，S3 bucket 可以開 MFA delete。這些機制把「一鍵刪除」變成「先關保護再刪除」兩步操作，擋不住蓄意刪除，但能擋住手滑跟批次操作的誤傷。</p>
<p>刪除保護之外，備份是另一道防線。手動環境裡至少確認 RDS 的自動備份是開著的（預設保留 7 天），以及 S3 bucket 的 versioning 是開著的。S3 bucket 的 versioning 預設是關的，一個沒開 versioning 的 bucket，覆寫或刪除物件後就回不去了。</p>
<h3 id="網路規則的大改">網路規則的大改</h3>
<p>手動調整 VPC 路由、subnet 關聯的 route table、或 security group 的入站規則，影響範圍跨越多個服務，而且在手動環境裡沒有版本控制可以 diff 改了什麼。一條路由改錯，某些 private subnet 的服務可能瞬間失去出站能力。</p>
<p>最低成本的護欄是「改之前先把現況存下來」：</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">aws ec2 describe-security-groups <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --group-ids sg-0abc123 <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --output json &gt; sg-backup-<span class="k">$(</span>date +%Y%m%d<span class="k">)</span>.json</span></span></code></pre></div><p>用 CLI 把當前的 security group 規則、route table 設定匯出一份 JSON。改完後如果出問題，這份 JSON 就是回退的依據。這不是自動回退 — 手動環境沒有那個能力 — 但至少讓「改回去」有個明確的目標狀態。網路地基的系統性設計在<a href="/blog/infra/03-network-foundation/" data-link-title="模組三：網路地基 — VPC 與分層" data-link-desc="VPC、public / private subnet 切分、route table、NAT、security group 設計">模組三：網路地基</a>展開。</p>
<h3 id="該先做什麼">該先做什麼</h3>
<p>這三類護欄的共同判準是：護欄成本低（幾條 CLI 指令或 Console 設定）、失誤代價高（憑證外洩、資料遺失、服務中斷）。判讀某個資源該不該現在就加護欄，問自己一個問題：「這個資源出事的回退時間是分鐘級、小時級、還是不可回退？」不可回退的（資料刪除、key 外洩）優先加；分鐘級可回退的（重啟一個 stateless service）可以排後面。</p>
<h2 id="讓變更留下痕跡">讓變更留下痕跡</h2>
<p>變更留痕的責任是讓「誰、在什麼時候、改了什麼、為什麼」事後可追溯。IaC 的 git history 天然提供這件事，手動環境得靠人為紀律補上。</p>
<h3 id="人工變更日誌">人工變更日誌</h3>
<p>最低限度是一份變更日誌，可以只是 repo 裡的一個 markdown 檔或團隊共用文件。一條記錄至少包含四個欄位：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln">1</span><span class="cl"><span class="gu">## 2026-06-20
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">-</span> **操作者**：alice
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="k">-</span> **資源**：sg-0abc123 (payments-api-prod)
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">-</span> **變更**：新增 ingress rule, port 8080 from 10.0.0.0/16
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="k">-</span> **原因**：內部監控服務需要存取 health check endpoint
</span></span><span class="line"><span class="ln">7</span><span class="cl">- <span class="gs">**回退方式**</span>：刪除該 ingress rule</span></span></code></pre></div><p>格式不需要精美，需要的是「每次都寫」。常見陷阱是只在「大改動」時才記錄，結果真正出事的往往是某次以為無關緊要的小調整 — 改了一個 parameter group 的值、調了一條路由的目標、把某個 instance 的 security group 換了一個。判準簡化成一句：只要這個操作別人事後可能需要知道，就記。</p>
<h3 id="平台稽核日誌">平台稽核日誌</h3>
<p>和人工日誌互補的是平台的稽核日誌（如 AWS CloudTrail、GCP Audit Log）。稽核日誌自動記錄 API 層級「發生了什麼」— 某個 IAM user 在某個時間對某個資源呼叫了哪個 API — 不依賴人為紀律、也不會漏。但它只記錄事實，不記錄意圖。它告訴你 security group 在幾點被改，卻不告訴你改的原因。人寫的變更日誌補上的正是「為什麼」這一段。</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">aws cloudtrail describe-trails <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;trailList[].{Name:Name,S3Bucket:S3BucketName}&#39;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl">aws cloudtrail lookup-events <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --lookup-attributes <span class="nv">AttributeKey</span><span class="o">=</span>EventName,AttributeValue<span class="o">=</span>AuthorizeSecurityGroupIngress <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  --max-items <span class="m">10</span></span></span></code></pre></div><p>CloudTrail 在 AWS 帳號裡預設開啟 management event 的 90 天查閱。手動環境裡至少確認 management event 的 trail 存在且在寫入 — 這是事後回推「到底誰動了什麼」的最後防線。兩者一起，事故排查時才能從「哪裡變了」一路追到「為什麼改、能不能安全回退」。</p>
<h2 id="命名與-tagging-從手動階段就開始">命名與 tagging 從手動階段就開始</h2>
<p>命名規範與資源標籤讓每個資源自帶「我是誰、屬於哪個服務、誰負責、哪個環境」的身分資訊。手動點出來的資源若名稱是 <code>test-2</code>、<code>new-db-final</code>、<code>temp-sg</code>，日後納管時得靠人逐一辨認哪個還在用、屬於哪條業務線，考古成本遠高於當初多打幾個字。</p>
<h3 id="命名規範">命名規範</h3>
<p>從手動階段就固定一套命名規則，讓名稱本身攜帶足夠的上下文。一個實用的格式是 <code>{service}-{component}-{env}</code>：</p>
<table>
  <thead>
      <tr>
          <th>資源類型</th>
          <th>命名範例</th>
          <th>攜帶的資訊</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>EC2 instance</td>
          <td><code>payments-api-prod</code></td>
          <td>服務 + 角色 + 環境</td>
      </tr>
      <tr>
          <td>Security group</td>
          <td><code>payments-api-prod-sg</code></td>
          <td>同上 + 資源類型</td>
      </tr>
      <tr>
          <td>RDS instance</td>
          <td><code>payments-db-prod</code></td>
          <td>服務 + 資源類型 + 環境</td>
      </tr>
      <tr>
          <td>S3 bucket</td>
          <td><code>acme-payments-assets-dev</code></td>
          <td>組織 + 服務 + 用途 + 環境</td>
      </tr>
  </tbody>
</table>
<p>命名不需要完美或涵蓋所有維度，需要的是一致。同類資源都用同一套格式，人眼掃一頁 Console 就能分辨「這個屬於 payments 的 prod」跟「這個屬於 auth 的 dev」。不一致的命名（有些用底線、有些用連字號、有些帶 env 有些不帶）會在日後盤點時讓每個資源都變成需要考古的謎題。</p>
<h3 id="最小-tag-集合">最小 tag 集合</h3>
<p>標籤至少包含三個維度：</p>
<table>
  <thead>
      <tr>
          <th>Tag</th>
          <th>問的問題</th>
          <th>典型值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>service</code></td>
          <td>這屬於誰</td>
          <td><code>payments-api</code> / <code>auth</code></td>
      </tr>
      <tr>
          <td><code>env</code></td>
          <td>哪個環境</td>
          <td><code>prod</code> / <code>staging</code> / <code>dev</code></td>
      </tr>
      <tr>
          <td><code>owner</code></td>
          <td>出事找誰</td>
          <td><code>team-payments</code> / <code>platform</code></td>
      </tr>
  </tbody>
</table>
<p>手動階段的 tag 靠人工填。在 Console 建資源時順手加 tag 幾乎零成本 — 多打三行字而已。但如果沒有約定「哪些 tag 是必填」，多數人會跳過。最低限度的紀律是：在團隊文件裡寫下「建任何資源前先填這三個 tag」，並在每次盤點時檢查有沒有漏標的資源。</p>
<p>這套規則在導入 IaC 後直接升級成 Terraform 的 <code>default_tags</code> — 自動套用、不靠人記（見<a href="/blog/infra/08-governance-habits/" data-link-title="模組八：治理好習慣 — 規模長大後不失控的最小節奏" data-link-desc="tagging 規範、secrets 不進 code、成本可見性、最小可行節奏，規模長大後不失控">模組八：治理好習慣</a>）。先在手動階段建立習慣，導入 IaC 時只是換一個強制機制，而不是從零學起一套分類法。</p>
<h2 id="盤點現有資源作為納管輸入">盤點現有資源作為納管輸入</h2>
<p>資源盤點把「現實長什麼樣」寫成一份清單，它是日後納管的直接輸入。接手別人建的環境時，盤點的範圍和方法更完整的版本見<a href="/blog/infra/takeover/" data-link-title="接手維運：別人建的環境怎麼接管" data-link-desc="接手前人的專案時，怎麼在不搞壞東西的前提下盤點現況、建立維運能力、逐步正規化 — 從無 SSH 的 FTP 環境到有半套 IaC 的雲端環境都適用">接手維運模組</a>。手動環境裡最難管理的是未標記的閒置資源 — 測試用的 EC2、實驗用的 RDS — 持續計費但沒有標籤，無法用查詢系統性找出，也無法確認是否仍有服務依賴。</p>
<h3 id="盤點方法">盤點方法</h3>
<p>按資源類型分批拉，每批存一份 JSON 或 CSV 進 repo：</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">aws ec2 describe-instances <span class="se">\
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;Reservations[].Instances[].[InstanceId,InstanceType,State.Name,Tags[?Key==`Name`].Value|[0],Tags[?Key==`env`].Value|[0]]&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  --output table
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">aws rds describe-db-instances <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;DBInstances[].[DBInstanceIdentifier,Engine,DBInstanceClass,MultiAZ,DeletionProtection]&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>  --output table
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">aws ec2 describe-security-groups <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;SecurityGroups[].[GroupId,GroupName,IpPermissions]&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="se"></span>  --output json &gt; security-groups-<span class="k">$(</span>date +%Y%m%d<span class="k">)</span>.json
</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">aws s3api list-buckets --query <span class="s1">&#39;Buckets[].Name&#39;</span></span></span></code></pre></div><h3 id="盤點後的三件事">盤點後的三件事</h3>
<p>這份清單同時服務三個目的。</p>
<p><strong>當下的安全盤查</strong>：security group 清單裡有沒有不該開的對外 port？有沒有 EC2 直接掛著公網 IP 卻不是 load balancer？用 <code>0.0.0.0/0</code> 搜一遍 security group 的輸出，命中的每一條都要能說出「這個全開是故意的、理由是什麼」。</p>
<p><strong>未來 IaC import 的範圍界定</strong>：哪些資源該先 import。判準是「改動頻率」與「改錯代價」的乘積 — 頻繁改動且改錯代價高的（security group、IAM role）先排進來，很少動的（一個已經穩定的 S3 bucket）可以排後面。</p>
<p><strong>成熟度評估的事實基礎</strong>：成熟度階梯的定位（見<a href="/blog/infra/00-infra-mindset/" data-link-title="模組零：infra 是什麼，為什麼 day 1 就要鋪地基" data-link-desc="基礎設施的責任邊界、成熟度階梯，以及地基為什麼總在環境爆炸時才被看見">模組零：infra 是什麼</a>）需要知道「全手動到底有多少資源、分布在幾個帳號、跨幾個 region」，這份清單就是評估的輸入。</p>
<h3 id="盤點的節奏">盤點的節奏</h3>
<p>第一次盤點最花時間，因為很多資源的用途需要考古。之後每月或每季重跑一次比對差異 — 重點是看「上次到這次之間長出了什麼新資源」。如果每次比對都發現大量未標記的新資源，這本身就是一個訊號：手動操作的可見性不足，該考慮導入 IaC 了。</p>
<h2 id="資源與信任不足下的高槓桿取捨">資源與信任不足下的高槓桿取捨</h2>
<p>當時間、人力或上層信任都不足，無法一次把上面每件事做齊時，取捨原則是先做「失誤代價高且護欄成本低」的少數幾件：</p>
<table>
  <thead>
      <tr>
          <th>護欄</th>
          <th>實施成本</th>
          <th>失誤代價</th>
          <th>優先級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>長期 key 盤點</td>
          <td>低</td>
          <td>極高</td>
          <td>立刻做</td>
      </tr>
      <tr>
          <td>刪除保護</td>
          <td>低</td>
          <td>極高</td>
          <td>立刻做</td>
      </tr>
      <tr>
          <td>變更日誌</td>
          <td>低</td>
          <td>中</td>
          <td>第二順位</td>
      </tr>
      <tr>
          <td>命名規範</td>
          <td>近零</td>
          <td>累積</td>
          <td>新資源立刻套用</td>
      </tr>
      <tr>
          <td>資源盤點</td>
          <td>中</td>
          <td>累積</td>
          <td>有空就做</td>
      </tr>
      <tr>
          <td>存量重命名</td>
          <td>高</td>
          <td>累積</td>
          <td>等有餘力</td>
      </tr>
  </tbody>
</table>
<p>長期憑證盤點與刪除保護兩者加起來的實施時間可能不到一小時。命名與 tagging 的策略是「新的一律照規範、舊的等有餘力再補」，而不是停下來先整理全部存量。資源不足時怎麼跟上層談這些工作的優先級，在<a href="/blog/infra/09-driving-adoption/" data-link-title="模組九：怎麼把 infra 推動起來" data-link-desc="技術正確不等於推得動 — 信任赤字、期望值對齊、知識共享，infra 落地的組織課題">模組九：怎麼把 infra 推動起來</a>展開。</p>
<h2 id="該開始導入-iac-的訊號">該開始導入 IaC 的訊號</h2>
<p>手動環境到了某些訊號出現時，繼續手動的邊際成本會超過導入 IaC 的一次性成本。訊號是規模與協作的函數，不是時間的函數 — 一個人運維一個簡單服務，手動可能撐很久；三個人同時動一個稍微複雜的環境，幾週內就會踩到手動的極限。</p>
<p><strong>環境數量變多</strong>：當需要 dev、staging、production 三套幾乎一樣的環境，手動複製會在環境之間留下難以察覺的差異。某個人在 staging 加了一條 security group 規則，忘了在 prod 也加，結果 staging 測通了、prod 部署後服務連不上。IaC 用同一份程式碼複製環境，環境差異只存在於參數值。</p>
<p><strong>多人同時動資源</strong>：一個人手動操作還能靠記憶維護，兩三個人並行時，沒有 plan / review 的手動變更會互相覆蓋。A 改了一個設定解了自己的問題，B 幾天後改了另一個設定把 A 的修正覆蓋掉，事故原因得靠翻 CloudTrail 才查得到。</p>
<p><strong>環境爆炸頻率上升</strong>：如果「改一個設定結果弄壞別的東西」這類事故開始每月發生，代表手動環境的隱性依賴已經超過人腦能追蹤的上限。一個典型的隱性依賴：security group A 被 instance X 和 instance Y 同時引用，改 A 時只想著 X 的需求、忘了 Y 也依賴它，改完 Y 就斷了。</p>
<p><strong>合規或稽核要求</strong>：外部稽核（SOC 2、ISO 27001）開始要求「列出所有對外暴露的服務」「提供存取權限的變更紀錄」「證明 production 環境的變更有經過審查」。手動環境回答這些問題時，每次都是一場考古工程。IaC 加上 PR 流程後，答案就在 repo 裡。</p>
<p>任一訊號穩定出現，就是把第一個資源納入 IaC 的起點 — 前面做的命名、tagging、資源盤點此時直接成為 import 的輸入。第一步怎麼跨進去在<a href="/blog/infra/01-minimal-iac/" data-link-title="模組一：最小可行 IaC — state 地基與 Console 唯讀鐵律" data-link-desc="Terraform / OpenTofu 選型、remote state 與 lock，以及「Console 只能看不能改」鐵律">模組一：最小可行 IaC</a>。</p>
<p>在訊號出現前過早導入 IaC 也有代價：單人、單環境、低變更頻率時，IaC 的學習與維護成本可能高於它省下的手動工 — 寫一份 HCL、配一個 state backend、設一條 pipeline 的固定成本，在只有三個資源的環境裡不一定划得來。這裡的判準是等訊號、不是趕進度。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/takeover/" data-link-title="接手維運：別人建的環境怎麼接管" data-link-desc="接手前人的專案時，怎麼在不搞壞東西的前提下盤點現況、建立維運能力、逐步正規化 — 從無 SSH 的 FTP 環境到有半套 IaC 的雲端環境都適用">接手維運</a>：如果這個手動環境是接手來的，先走接手維運的盤點流程</li>
<li>→ <a href="/blog/infra/00-infra-mindset/" data-link-title="模組零：infra 是什麼，為什麼 day 1 就要鋪地基" data-link-desc="基礎設施的責任邊界、成熟度階梯，以及地基為什麼總在環境爆炸時才被看見">模組零：infra 是什麼</a>：成熟度階梯上「全手動」這一階的定位</li>
<li>→ <a href="/blog/infra/01-minimal-iac/" data-link-title="模組一：最小可行 IaC — state 地基與 Console 唯讀鐵律" data-link-desc="Terraform / OpenTofu 選型、remote state 與 lock，以及「Console 只能看不能改」鐵律">模組一：最小可行 IaC</a>：訊號出現後，第一步怎麼跨進 IaC</li>
<li>→ <a href="/blog/infra/02-identity-credentials/" data-link-title="模組二：身分與憑證地基 — IAM 與 OIDC" data-link-desc="IAM role / policy 設計、最小權限，以及用 OIDC 短期憑證取代長期 access key">模組二：身分與憑證地基</a>：長期憑證護欄的系統性設計</li>
<li>→ <a href="/blog/infra/03-network-foundation/" data-link-title="模組三：網路地基 — VPC 與分層" data-link-desc="VPC、public / private subnet 切分、route table、NAT、security group 設計">模組三：網路地基</a>：手動階段網路大改的回退考量、之後的系統性設計</li>
<li>→ <a href="/blog/infra/08-governance-habits/" data-link-title="模組八：治理好習慣 — 規模長大後不失控的最小節奏" data-link-desc="tagging 規範、secrets 不進 code、成本可見性、最小可行節奏，規模長大後不失控">模組八：治理好習慣</a>：tagging 在成本歸因與批次操作的後續價值</li>
<li>→ <a href="/blog/infra/09-driving-adoption/" data-link-title="模組九：怎麼把 infra 推動起來" data-link-desc="技術正確不等於推得動 — 信任赤字、期望值對齊、知識共享，infra 落地的組織課題">模組九：怎麼把 infra 推動起來</a>：資源不足時怎麼跟上層談優先級</li>
</ul>
]]></content:encoded></item><item><title>Bootstrap Script 與套件清單管理</title><link>https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/bootstrap-script-packages/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/bootstrap-script-packages/</guid><description>&lt;p>一份 bootstrap script 是重建指令的入口。它做三件事：安裝套件、部署配置檔、執行初始化設定。&lt;/p>
&lt;h2 id="範例結構">範例結構&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="cp">#!/usr/bin/env bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="cp">&lt;/span>&lt;span class="nb">set&lt;/span> -euo pipefail
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="nv">DOTFILES_DIR&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">cd&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>dirname &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$0&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">)&lt;/span>&lt;span class="s2">/..&amp;#34;&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">pwd&lt;/span>&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1"># --- 偵測 OS ---&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="nv">OS&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>uname -s&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">install_packages&lt;span class="o">()&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="o">[[&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$OS&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;Darwin&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="nb">command&lt;/span> -v brew &amp;gt;/dev/null &lt;span class="o">||&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;Installing Homebrew...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> /bin/bash -c &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> brew bundle --file&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$DOTFILES_DIR&lt;/span>&lt;span class="s2">/Brewfile&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="o">[[&lt;/span> -f /etc/arch-release &lt;span class="o">]]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> sudo pacman -Syu --noconfirm
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> sudo pacman -S --needed - &amp;lt; &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$DOTFILES_DIR&lt;/span>&lt;span class="s2">/packages.txt&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="c1"># AUR 套件需要 AUR helper（假設已安裝 yay）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nb">command&lt;/span> -v yay &amp;gt;/dev/null &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="o">[[&lt;/span> -f &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$DOTFILES_DIR&lt;/span>&lt;span class="s2">/aur-packages.txt&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl"> yay -S --needed - &amp;lt; &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$DOTFILES_DIR&lt;/span>&lt;span class="s2">/aur-packages.txt&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl"> &lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="o">[[&lt;/span> -f /etc/debian_version &lt;span class="o">]]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl"> sudo apt update
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl"> xargs -a &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$DOTFILES_DIR&lt;/span>&lt;span class="s2">/apt-packages.txt&amp;#34;&lt;/span> sudo apt install -y
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">28&lt;/span>&lt;span class="cl"> &lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">29&lt;/span>&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">30&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">31&lt;/span>&lt;span class="cl">deploy_configs&lt;span class="o">()&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">32&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> ! &lt;span class="nb">command&lt;/span> -v stow &amp;gt;/dev/null&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">33&lt;/span>&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;stow not found, skipping config deployment&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">34&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">35&lt;/span>&lt;span class="cl"> &lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">36&lt;/span>&lt;span class="cl"> &lt;span class="nb">cd&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$DOTFILES_DIR&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">37&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> dir in zsh git nvim tmux hypr waybar&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">38&lt;/span>&lt;span class="cl"> &lt;span class="o">[[&lt;/span> -d &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$dir&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> stow -v --target&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$HOME&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$dir&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">39&lt;/span>&lt;span class="cl"> &lt;span class="k">done&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">40&lt;/span>&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">41&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">42&lt;/span>&lt;span class="cl">post_setup&lt;span class="o">()&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">43&lt;/span>&lt;span class="cl"> &lt;span class="c1"># 切換預設 shell&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">44&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="o">[[&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$SHELL&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> !&lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">command&lt;/span> -v zsh&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">command&lt;/span> -v zsh &amp;gt;/dev/null&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">45&lt;/span>&lt;span class="cl"> chsh -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">command&lt;/span> -v zsh&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">46&lt;/span>&lt;span class="cl"> &lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">47&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">48&lt;/span>&lt;span class="cl"> &lt;span class="c1"># neovim plugin 安裝（headless 模式）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">49&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nb">command&lt;/span> -v nvim &amp;gt;/dev/null&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">50&lt;/span>&lt;span class="cl"> nvim --headless &lt;span class="s2">&amp;#34;+Lazy! sync&amp;#34;&lt;/span> +qa 2&amp;gt;/dev/null &lt;span class="o">||&lt;/span> &lt;span class="nb">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">51&lt;/span>&lt;span class="cl"> &lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">52&lt;/span>&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">53&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">54&lt;/span>&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;=== Installing packages ===&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">55&lt;/span>&lt;span class="cl">install_packages
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">56&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">57&lt;/span>&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;=== Deploying configs ===&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">58&lt;/span>&lt;span class="cl">deploy_configs
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">59&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">60&lt;/span>&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;=== Post-setup ===&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">61&lt;/span>&lt;span class="cl">post_setup
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">62&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">63&lt;/span>&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;Done. Log out and back in for shell changes to take effect.&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="設計原則">設計原則&lt;/h2>
&lt;p>&lt;strong>冪等性&lt;/strong>是最重要的性質。跑一次和跑十次結果相同。&lt;code>pacman -S --needed&lt;/code> 只安裝缺少的套件、&lt;code>stow&lt;/code> 只建立不存在的 symlink、&lt;code>command -v&lt;/code> 在工具已存在時跳過安裝（用 &lt;code>command -v&lt;/code> 不用 &lt;code>which&lt;/code>——後者在最小系統可能不存在）。冪等的 script 可以放心重跑——改了一個配置後重新 deploy，不會弄壞其他已經正確的部分。&lt;/p></description><content:encoded><![CDATA[<p>一份 bootstrap script 是重建指令的入口。它做三件事：安裝套件、部署配置檔、執行初始化設定。</p>
<h2 id="範例結構">範例結構</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="cp"></span><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nv">DOTFILES_DIR</span><span class="o">=</span><span class="s2">&#34;</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 class="s2">&#34;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># --- 偵測 OS ---</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nv">OS</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>uname -s<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">install_packages<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$OS</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;Darwin&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="nb">command</span> -v brew &gt;/dev/null <span class="o">||</span> <span class="o">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">            <span class="nb">echo</span> <span class="s2">&#34;Installing Homebrew...&#34;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">            /bin/bash -c <span class="s2">&#34;</span><span class="k">$(</span>curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="o">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        brew bundle --file<span class="o">=</span><span class="s2">&#34;</span><span class="nv">$DOTFILES_DIR</span><span class="s2">/Brewfile&#34;</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="k">elif</span> <span class="o">[[</span> -f /etc/arch-release <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        sudo pacman -Syu --noconfirm
</span></span><span class="line"><span class="ln">19</span><span class="cl">        sudo pacman -S --needed - &lt; <span class="s2">&#34;</span><span class="nv">$DOTFILES_DIR</span><span class="s2">/packages.txt&#34;</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">        <span class="c1"># AUR 套件需要 AUR helper（假設已安裝 yay）</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">        <span class="k">if</span> <span class="nb">command</span> -v yay &gt;/dev/null <span class="o">&amp;&amp;</span> <span class="o">[[</span> -f <span class="s2">&#34;</span><span class="nv">$DOTFILES_DIR</span><span class="s2">/aur-packages.txt&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">            yay -S --needed - &lt; <span class="s2">&#34;</span><span class="nv">$DOTFILES_DIR</span><span class="s2">/aur-packages.txt&#34;</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">        <span class="k">fi</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="k">elif</span> <span class="o">[[</span> -f /etc/debian_version <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">        sudo apt update
</span></span><span class="line"><span class="ln">27</span><span class="cl">        xargs -a <span class="s2">&#34;</span><span class="nv">$DOTFILES_DIR</span><span class="s2">/apt-packages.txt&#34;</span> sudo apt install -y
</span></span><span class="line"><span class="ln">28</span><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">
</span></span><span class="line"><span class="ln">31</span><span class="cl">deploy_configs<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">    <span class="k">if</span> ! <span class="nb">command</span> -v stow &gt;/dev/null<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">        <span class="nb">echo</span> <span class="s2">&#34;stow not found, skipping config deployment&#34;</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">        <span class="k">return</span> <span class="m">1</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">    <span class="nb">cd</span> <span class="s2">&#34;</span><span class="nv">$DOTFILES_DIR</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">    <span class="k">for</span> dir in zsh git nvim tmux hypr waybar<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">        <span class="o">[[</span> -d <span class="s2">&#34;</span><span class="nv">$dir</span><span class="s2">&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> stow -v --target<span class="o">=</span><span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$dir</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">    <span class="k">done</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="ln">41</span><span class="cl">
</span></span><span class="line"><span class="ln">42</span><span class="cl">post_setup<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="ln">43</span><span class="cl">    <span class="c1"># 切換預設 shell</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl">    <span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$SHELL</span><span class="s2">&#34;</span> !<span class="o">=</span> <span class="s2">&#34;</span><span class="k">$(</span><span class="nb">command</span> -v zsh<span class="k">)</span><span class="s2">&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">command</span> -v zsh &gt;/dev/null<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">45</span><span class="cl">        chsh -s <span class="s2">&#34;</span><span class="k">$(</span><span class="nb">command</span> -v zsh<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">46</span><span class="cl">    <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="c1"># neovim plugin 安裝（headless 模式）</span>
</span></span><span class="line"><span class="ln">49</span><span class="cl">    <span class="k">if</span> <span class="nb">command</span> -v nvim &gt;/dev/null<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">50</span><span class="cl">        nvim --headless <span class="s2">&#34;+Lazy! sync&#34;</span> +qa 2&gt;/dev/null <span class="o">||</span> <span class="nb">true</span>
</span></span><span class="line"><span class="ln">51</span><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="ln">52</span><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="ln">53</span><span class="cl">
</span></span><span class="line"><span class="ln">54</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;=== Installing packages ===&#34;</span>
</span></span><span class="line"><span class="ln">55</span><span class="cl">install_packages
</span></span><span class="line"><span class="ln">56</span><span class="cl">
</span></span><span class="line"><span class="ln">57</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;=== Deploying configs ===&#34;</span>
</span></span><span class="line"><span class="ln">58</span><span class="cl">deploy_configs
</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="nb">echo</span> <span class="s2">&#34;=== Post-setup ===&#34;</span>
</span></span><span class="line"><span class="ln">61</span><span class="cl">post_setup
</span></span><span class="line"><span class="ln">62</span><span class="cl">
</span></span><span class="line"><span class="ln">63</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Done. Log out and back in for shell changes to take effect.&#34;</span></span></span></code></pre></div><h2 id="設計原則">設計原則</h2>
<p><strong>冪等性</strong>是最重要的性質。跑一次和跑十次結果相同。<code>pacman -S --needed</code> 只安裝缺少的套件、<code>stow</code> 只建立不存在的 symlink、<code>command -v</code> 在工具已存在時跳過安裝（用 <code>command -v</code> 不用 <code>which</code>——後者在最小系統可能不存在）。冪等的 script 可以放心重跑——改了一個配置後重新 deploy，不會弄壞其他已經正確的部分。</p>
<p><strong>失敗可診斷</strong>是這支範例為了聚焦結構而省略、但實際該有的性質。bootstrap 在陌生機器上失敗是常態，怎麼讓它在某一步掛掉時留下可定位的痕跡（全輸出 tee 落地 + ERR trap 點名出錯的行與指令），見 <a href="/blog/linux/install/observable-bootstrap/" data-link-title="可除錯的 bootstrap：把可觀測性內建進安裝腳本" data-link-desc="安裝腳本中途失敗卻只能對著終端機捲動瞎找原因、想在 bootstrap 設計階段就讓失敗可定位時回來讀">可除錯的 bootstrap</a>——那篇是這支腳本的「失敗時看得見」那一層。</p>
<p><strong>偵測 OS 分流</strong>處理的是跨平台差異。macOS 用 Homebrew、Arch 用 pacman、Debian 系用 apt——套件管理器不同、套件名稱有時也不同（macOS 的 <code>coreutils</code> 在 Linux 是預裝的）。分流邏輯集中在 bootstrap script 裡，配置檔本身盡量保持跨平台一致。</p>
<p><strong>最少依賴</strong>原則：script 本身只依賴 bash 和 curl（幾乎所有系統預裝），其他工具由 script 自己安裝。這確保你可以在一台只有 base system 的機器上直接跑 script。不過「base system 直接跑」有個前提——最小安裝可能連 <code>sudo</code> 都沒有，而 script 裝套件正要靠它。跑這支 script 之前該驗證並補齊的前置工具，見 <a href="/blog/linux/install/minimal-install-verify/" data-link-title="最小安裝後的工具驗證與補足" data-link-desc="最小化安裝的 Linux 裝完發現連 sudo 或 which 都沒有、bootstrap 腳本第一行就炸、需要先確認系統缺哪些必要工具再補時回來讀">最小安裝後的工具驗證與補足</a>。</p>
<p><strong>交付完整可用的環境</strong>：script 的職責是讓部署完的配置「能直接用」，所以它必須裝齊配置實際引用的每一樣東西，而不是假設它們已經在。一個常見的破綻是把依賴寫進 README 的「dependencies」清單、卻沒在 script 裡實作安裝——例如 <code>.zshrc</code> 引用了 oh-my-zsh、主題、外掛，但 install script 只裝了 zsh 本身，結果 stow 部署完、第一次開 shell 就因為找不到那些東西而報錯。README 列依賴是給人看的、不會被執行；要讓配置真的能用，那些依賴得由 script 自己裝（例如把外掛 git clone 進對應位置）。檢查方式是反過來從配置出發：把每個 config 會 source 或引用的外部東西列出來，逐一確認 script 有沒有負責把它裝上。</p>
<p><strong>可部分執行</strong>的結構：拆成 function，允許只跑某一段。除錯時只想重新 deploy 配置、不想重裝套件，直接呼叫 <code>deploy_configs</code> 就好。進一步可以把每段拆成獨立 script（<code>scripts/install-packages.sh</code>、<code>scripts/deploy-configs.sh</code>），bootstrap 入口只是依序呼叫它們。</p>
<h2 id="套件清單管理">套件清單管理</h2>
<p>dotfile repo 管的是「配置」，但配置的前提是軟體已安裝。沒有附帶套件清單的 dotfile repo 是不完整的重建指令——你 clone 下來卻不知道該先裝什麼。</p>
<h3 id="macosbrewfile">macOS：Brewfile</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># Brewfile</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">tap</span> <span class="s2">&#34;homebrew/cask-fonts&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># CLI 工具</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">brew</span> <span class="s2">&#34;git&#34;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">brew</span> <span class="s2">&#34;neovim&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">brew</span> <span class="s2">&#34;tmux&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">brew</span> <span class="s2">&#34;stow&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">brew</span> <span class="s2">&#34;ripgrep&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">brew</span> <span class="s2">&#34;fd&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">brew</span> <span class="s2">&#34;fzf&#34;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="n">brew</span> <span class="s2">&#34;zsh&#34;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># GUI app</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="n">cask</span> <span class="s2">&#34;wezterm&#34;</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="n">cask</span> <span class="s2">&#34;rectangle&#34;</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="n">cask</span> <span class="s2">&#34;font-jetbrains-mono-nerd-font&#34;</span></span></span></code></pre></div><p><code>brew bundle dump</code> 從當前系統產生 Brewfile、<code>brew bundle</code> 照 Brewfile 安裝。Brewfile 區分三種來源：<code>brew</code>（CLI formula）、<code>cask</code>（GUI app）、<code>tap</code>（第三方 repo）。把 Brewfile 放在 dotfile repo 根目錄，bootstrap script 用 <code>brew bundle --file=./Brewfile</code> 安裝。</p>
<h3 id="arch-linuxpackagestxt">Arch Linux：packages.txt</h3>





<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"># 匯出已安裝的 explicitly installed 套件</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">pacman -Qqe &gt; packages.txt
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># AUR 套件另外記</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">pacman -Qqem &gt; aur-packages.txt</span></span></code></pre></div><p><code>-Qqe</code> 只列出使用者主動安裝的套件（不含被依賴自動拉進來的），這是你實際需要管理的範圍。<code>-Qqem</code> 進一步篩出外部來源（AUR）。還原時用 <code>pacman -S --needed - &lt; packages.txt</code>，<code>--needed</code> 跳過已安裝的。</p>
<h3 id="ubuntudebian">Ubuntu/Debian</h3>
<p>apt 的匯出格式比較雜。務實做法是手動維護一份清單檔（<code>apt-packages.txt</code>），每行一個套件名，用 <code>xargs -a apt-packages.txt sudo apt install -y</code> 安裝。比起 <code>apt list --installed</code> 的完整匯出（包含大量系統依賴），手動維護的清單更乾淨、更容易讀懂。</p>
<h3 id="為什麼套件清單要進-repo">為什麼套件清單要進 repo</h3>
<p>一個常見的失敗模式：dotfile repo 裡有完整的 neovim 配置，clone 到新機器後發現 neovim 沒裝、ripgrep 沒裝、字型沒裝，配置跑起來全是 error。套件清單跟配置檔放在同一個 repo，bootstrap script 才能先裝套件再 deploy 配置，形成完整的重建鏈路。</p>
]]></content:encoded></item><item><title>最小安裝後的工具驗證與補足</title><link>https://tarrragon.github.io/blog/linux/install/minimal-install-verify/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/install/minimal-install-verify/</guid><description>&lt;p>最小化安裝給你的是一台能開機的系統，但「能開機」跟「能用」之間隔著一組「大家都假設存在」但其實沒被裝進去的工具。最小安裝（多數發行版的 &lt;code>base&lt;/code> 之類的套件組）刻意只裝開機與基本運作所需的東西，把工具的選擇權留給你。代價是許多你以為理所當然會在的指令——&lt;code>sudo&lt;/code>、&lt;code>which&lt;/code>、&lt;code>rsync&lt;/code>——一個都沒有。驗證它們在不在，比假設它們在安全。&lt;/p>
&lt;p>這層落差最常在你跑自動化腳本時引爆。一支 bootstrap script 的第一行可能就是 &lt;code>sudo pacman -S ...&lt;/code>，在一台連 &lt;code>sudo&lt;/code> 都沒有的機器上，它連第一步都跨不過去。所以裝好系統後、跑任何自動化之前，先過一輪工具驗證，把缺的補上。&lt;/p>
&lt;h2 id="sudo先有雞還是先有蛋">sudo：先有雞還是先有蛋&lt;/h2>
&lt;p>&lt;code>sudo&lt;/code> 是最容易被假設存在、卻最常缺席的工具，而且它的缺席有一個結構性的麻煩：補它的動作本身需要 root 權限。最小安裝通常不含 sudo。某些安裝程式（如本例的 archboot）即使你勾了「把這個使用者設為管理員」，那個動作也往往只是把使用者加進 &lt;code>wheel&lt;/code> 群組，並沒有真的裝上 sudo、也沒有啟用 sudoers 裡 wheel 群組的授權行。結果就是使用者「名義上是管理員」，但系統裡並沒有 sudo 這支指令。&lt;/p>
&lt;p>這形成一個先有雞還是先有蛋的關卡：bootstrap script 要靠 sudo 來裝套件，但 sudo 自己得先存在。它的解法不能是「把 sudo 寫進套件清單」——那份清單正是靠 sudo 來安裝的。sudo 只能是「跑 bootstrap 之前的前置」，用 root 身分手動補上（&lt;code>su -&lt;/code> 成為 root、&lt;code>echo &amp;gt; 檔案&lt;/code> 重導向、&lt;code>chmod&lt;/code> 設權限這些基礎操作不熟的話，見 &lt;a href="../basic-operations/">安裝過程用到的基礎操作&lt;/a>）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">su - &lt;span class="c1"># 切到 root（輸入 root 密碼）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">pacman -S --needed sudo &lt;span class="c1"># root 身分裝 sudo，不需要 sudo&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s1">&amp;#39;%wheel ALL=(ALL:ALL) ALL&amp;#39;&lt;/span> &amp;gt; /etc/sudoers.d/10-wheel &lt;span class="c1"># 啟用 wheel 群組授權&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">chmod &lt;span class="m">440&lt;/span> /etc/sudoers.d/10-wheel
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">visudo -c &lt;span class="c1"># 驗證 sudoers 語法，印 parsed OK 才安全&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">exit&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>切回一般使用者後用 &lt;code>sudo -v&lt;/code> 確認——能輸入密碼、沒報「不在 sudoers 檔」就成。這一步揭示一條通則：凡是 bootstrap 自身要依賴的工具，都不能由 bootstrap 來裝，必須當成前置先備好。&lt;code>sudo&lt;/code> 是這類前置最典型的一個。&lt;/p>
&lt;p>上面的指令以 Arch 的 &lt;code>pacman&lt;/code> 為例。Fedora 用 &lt;code>dnf&lt;/code>、Debian/Ubuntu 用 &lt;code>apt&lt;/code>；而 Debian 系的桌面與伺服器映像多半預設就裝了 sudo、也設好了授權，這個缺口主要出現在刻意精簡的 minimal 安裝。換句話說「sudo 是前置」這條判讀軸跨發行版成立，但「你這台到底缺不缺」要靠驗證、不是假設。&lt;/p>
&lt;h2 id="which腳本裡的隱形地雷">which：腳本裡的隱形地雷&lt;/h2>
&lt;p>&lt;code>which&lt;/code> 是另一個最小系統常缺、卻被腳本大量引用的指令，它的缺席會以一種隱晦的方式讓腳本出錯。很多腳本用 &lt;code>$(which zsh)&lt;/code> 之類的寫法取一支程式的完整路徑；在沒有 &lt;code>which&lt;/code> 的系統上，這個命令替換會吐出空字串，而下游拿到空字串的指令可能不會立刻報「找不到 which」，而是報一個看似無關的錯。實測中就遇過 &lt;code>chsh -s &amp;quot;$(which zsh)&amp;quot;&lt;/code> 因為 &lt;code>which&lt;/code> 不存在而變成 &lt;code>chsh -s &amp;quot;&amp;quot;&lt;/code>，最後報的是 &lt;code>chsh: shell must be a full path name&lt;/code>——錯誤訊息完全沒提到真正的元兇。&lt;/p>
&lt;p>正確的做法是用 &lt;code>command -v&lt;/code> 取代 &lt;code>which&lt;/code>。&lt;code>command -v&lt;/code> 是 POSIX 規範的 shell 內建，不依賴任何外部套件，在最小系統上一定存在。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nb">command&lt;/span> -v zsh &lt;span class="c1"># 印出 /usr/bin/zsh；找不到則回傳非零、不印東西&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這條判讀對你自己寫的腳本是「把 &lt;code>which&lt;/code> 全換成 &lt;code>command -v&lt;/code>」，對別人的腳本是「在缺 &lt;code>which&lt;/code> 的系統上，先補 &lt;code>which&lt;/code> 套件或改腳本」。它跟 sudo 的差別在於：&lt;code>which&lt;/code> 的缺席會悄悄製造一個誤導性的下游錯誤，而不是當場大聲報錯，所以更值得在驗證階段主動排掉。&lt;/p>
&lt;h2 id="其他常缺的工具">其他常缺的工具&lt;/h2>
&lt;p>除了 sudo 與 which，最小系統還常缺幾類在自動化裡會用到的工具，各有各的補法。它們不像 sudo 是硬前置，但缺了會在特定步驟卡住。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>工具&lt;/th>
 &lt;th>缺了會怎樣&lt;/th>
 &lt;th>補法&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>rsync&lt;/code>&lt;/td>
 &lt;td>從本機同步 dotfile 進機器時 &lt;code>rsync: command not found&lt;/code>&lt;/td>
 &lt;td>進套件清單；急用時改用 &lt;code>tar&lt;/code> over SSH 過渡&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>ca-certificates&lt;/code>&lt;/td>
 &lt;td>HTTPS / 任何 TLS 連線在憑證驗證直接失敗（沒有信任根）&lt;/td>
 &lt;td>進套件清單；它是下一篇 HTTPS clone 的隱性前置&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>hostname&lt;/code>&lt;/td>
 &lt;td>某些腳本呼叫 &lt;code>hostname&lt;/code> 取主機名時失敗&lt;/td>
 &lt;td>補 &lt;code>inetutils&lt;/code>，或改用 &lt;code>hostnamectl&lt;/code> / 讀 &lt;code>/etc/hostname&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>編譯工具鏈&lt;/td>
 &lt;td>從原始碼或社群套件庫編譯時缺 &lt;code>gcc&lt;/code> / &lt;code>make&lt;/code>&lt;/td>
 &lt;td>補發行版的開發工具組（如 &lt;code>base-devel&lt;/code>）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>rsync&lt;/code> 的缺席要特別點出，因為它常被當成理所當然的傳輸工具。最小系統沒有它時，第一次把檔案弄進機器可以用兩邊都有的 &lt;code>tar&lt;/code> 搭配 SSH 過渡：&lt;/p></description><content:encoded><![CDATA[<p>最小化安裝給你的是一台能開機的系統，但「能開機」跟「能用」之間隔著一組「大家都假設存在」但其實沒被裝進去的工具。最小安裝（多數發行版的 <code>base</code> 之類的套件組）刻意只裝開機與基本運作所需的東西，把工具的選擇權留給你。代價是許多你以為理所當然會在的指令——<code>sudo</code>、<code>which</code>、<code>rsync</code>——一個都沒有。驗證它們在不在，比假設它們在安全。</p>
<p>這層落差最常在你跑自動化腳本時引爆。一支 bootstrap script 的第一行可能就是 <code>sudo pacman -S ...</code>，在一台連 <code>sudo</code> 都沒有的機器上，它連第一步都跨不過去。所以裝好系統後、跑任何自動化之前，先過一輪工具驗證，把缺的補上。</p>
<h2 id="sudo先有雞還是先有蛋">sudo：先有雞還是先有蛋</h2>
<p><code>sudo</code> 是最容易被假設存在、卻最常缺席的工具，而且它的缺席有一個結構性的麻煩：補它的動作本身需要 root 權限。最小安裝通常不含 sudo。某些安裝程式（如本例的 archboot）即使你勾了「把這個使用者設為管理員」，那個動作也往往只是把使用者加進 <code>wheel</code> 群組，並沒有真的裝上 sudo、也沒有啟用 sudoers 裡 wheel 群組的授權行。結果就是使用者「名義上是管理員」，但系統裡並沒有 sudo 這支指令。</p>
<p>這形成一個先有雞還是先有蛋的關卡：bootstrap script 要靠 sudo 來裝套件，但 sudo 自己得先存在。它的解法不能是「把 sudo 寫進套件清單」——那份清單正是靠 sudo 來安裝的。sudo 只能是「跑 bootstrap 之前的前置」，用 root 身分手動補上（<code>su -</code> 成為 root、<code>echo &gt; 檔案</code> 重導向、<code>chmod</code> 設權限這些基礎操作不熟的話，見 <a href="../basic-operations/">安裝過程用到的基礎操作</a>）：</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">su -                                          <span class="c1"># 切到 root（輸入 root 密碼）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">pacman -S --needed sudo                        <span class="c1"># root 身分裝 sudo，不需要 sudo</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;%wheel ALL=(ALL:ALL) ALL&#39;</span> &gt; /etc/sudoers.d/10-wheel   <span class="c1"># 啟用 wheel 群組授權</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">chmod <span class="m">440</span> /etc/sudoers.d/10-wheel
</span></span><span class="line"><span class="ln">5</span><span class="cl">visudo -c                                      <span class="c1"># 驗證 sudoers 語法，印 parsed OK 才安全</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">exit</span></span></code></pre></div><p>切回一般使用者後用 <code>sudo -v</code> 確認——能輸入密碼、沒報「不在 sudoers 檔」就成。這一步揭示一條通則：凡是 bootstrap 自身要依賴的工具，都不能由 bootstrap 來裝，必須當成前置先備好。<code>sudo</code> 是這類前置最典型的一個。</p>
<p>上面的指令以 Arch 的 <code>pacman</code> 為例。Fedora 用 <code>dnf</code>、Debian/Ubuntu 用 <code>apt</code>；而 Debian 系的桌面與伺服器映像多半預設就裝了 sudo、也設好了授權，這個缺口主要出現在刻意精簡的 minimal 安裝。換句話說「sudo 是前置」這條判讀軸跨發行版成立，但「你這台到底缺不缺」要靠驗證、不是假設。</p>
<h2 id="which腳本裡的隱形地雷">which：腳本裡的隱形地雷</h2>
<p><code>which</code> 是另一個最小系統常缺、卻被腳本大量引用的指令，它的缺席會以一種隱晦的方式讓腳本出錯。很多腳本用 <code>$(which zsh)</code> 之類的寫法取一支程式的完整路徑；在沒有 <code>which</code> 的系統上，這個命令替換會吐出空字串，而下游拿到空字串的指令可能不會立刻報「找不到 which」，而是報一個看似無關的錯。實測中就遇過 <code>chsh -s &quot;$(which zsh)&quot;</code> 因為 <code>which</code> 不存在而變成 <code>chsh -s &quot;&quot;</code>，最後報的是 <code>chsh: shell must be a full path name</code>——錯誤訊息完全沒提到真正的元兇。</p>
<p>正確的做法是用 <code>command -v</code> 取代 <code>which</code>。<code>command -v</code> 是 POSIX 規範的 shell 內建，不依賴任何外部套件，在最小系統上一定存在。</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="nb">command</span> -v zsh        <span class="c1"># 印出 /usr/bin/zsh；找不到則回傳非零、不印東西</span></span></span></code></pre></div><p>這條判讀對你自己寫的腳本是「把 <code>which</code> 全換成 <code>command -v</code>」，對別人的腳本是「在缺 <code>which</code> 的系統上，先補 <code>which</code> 套件或改腳本」。它跟 sudo 的差別在於：<code>which</code> 的缺席會悄悄製造一個誤導性的下游錯誤，而不是當場大聲報錯，所以更值得在驗證階段主動排掉。</p>
<h2 id="其他常缺的工具">其他常缺的工具</h2>
<p>除了 sudo 與 which，最小系統還常缺幾類在自動化裡會用到的工具，各有各的補法。它們不像 sudo 是硬前置，但缺了會在特定步驟卡住。</p>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>缺了會怎樣</th>
          <th>補法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>rsync</code></td>
          <td>從本機同步 dotfile 進機器時 <code>rsync: command not found</code></td>
          <td>進套件清單；急用時改用 <code>tar</code> over SSH 過渡</td>
      </tr>
      <tr>
          <td><code>ca-certificates</code></td>
          <td>HTTPS / 任何 TLS 連線在憑證驗證直接失敗（沒有信任根）</td>
          <td>進套件清單；它是下一篇 HTTPS clone 的隱性前置</td>
      </tr>
      <tr>
          <td><code>hostname</code></td>
          <td>某些腳本呼叫 <code>hostname</code> 取主機名時失敗</td>
          <td>補 <code>inetutils</code>，或改用 <code>hostnamectl</code> / 讀 <code>/etc/hostname</code></td>
      </tr>
      <tr>
          <td>編譯工具鏈</td>
          <td>從原始碼或社群套件庫編譯時缺 <code>gcc</code> / <code>make</code></td>
          <td>補發行版的開發工具組（如 <code>base-devel</code>）</td>
      </tr>
  </tbody>
</table>
<p><code>rsync</code> 的缺席要特別點出，因為它常被當成理所當然的傳輸工具。最小系統沒有它時，第一次把檔案弄進機器可以用兩邊都有的 <code>tar</code> 搭配 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">tar czf - --exclude <span class="s1">&#39;.git&#39;</span> . <span class="p">|</span> ssh user@host <span class="s1">&#39;mkdir -p ~/dest &amp;&amp; tar xzf - -C ~/dest&#39;</span></span></span></code></pre></div><p>這條的好處是不依賴目標機有 rsync；缺點是它每次都傳全部、沒有 rsync 的增量。在反覆同步的工作流裡，值得早點把 rsync 補進套件清單換取增量傳輸。</p>
<p><code>ca-certificates</code> 最容易在下一步咬人。最小系統可能沒有 CA 信任根，這時任何 HTTPS 連線——包括下一篇主推的「公開 repo 用 HTTPS clone」——會在 TLS 憑證驗證直接失敗，而錯誤訊息常指向 SSL handshake 而非「缺信任根」，容易誤判成網路問題。打算走 HTTPS 取得 dotfile 的機器，先確認 <code>ca-certificates</code> 在。<code>git</code> 與 <code>curl</code> 同理：它們是 bootstrap 取得程式碼的基本工具，下面的驗證迴圈也會檢查，最小系統若沒有要一併補。</p>
<p>剩下兩項的缺席各有觸發時機。<code>hostname</code> 只在腳本明確呼叫它取主機名時才會浮現缺失，而用 <code>hostnamectl</code> 或直接讀 <code>/etc/hostname</code> 可以繞過，所以它常被當成「補了省事、不補也有替代」的軟缺口。編譯工具鏈則是在你要從原始碼或社群套件庫編譯時才需要——純跑預編譯套件的機器可以不裝，但只要你的 dotfile 流程會編譯任何東西（例如從社群套件庫裝桌面元件），它就得進清單。</p>
<h2 id="系統性的驗證">系統性的驗證</h2>
<p>裝好系統後先跑一輪集中驗證、把缺口一次盤出來，比等腳本跑到一半才逐一踩雷省事。驗證的對象是「你接下來的流程會用到、但最小系統可能沒有」的工具。</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="k">for</span> cmd in sudo git curl rsync tar zsh<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="k">if</span> <span class="nb">command</span> -v <span class="s2">&#34;</span><span class="nv">$cmd</span><span class="s2">&#34;</span> &gt;/dev/null 2&gt;<span class="p">&amp;</span>1<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;OK   </span><span class="nv">$cmd</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="k">else</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;缺   </span><span class="nv">$cmd</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="k">done</span></span></span></code></pre></div><p>這段刻意用 <code>command -v</code> 來檢查（而不是 <code>which</code>），因為要檢查的對象之一正是「外部工具在不在」，用一個一定存在的內建來檢查才不會自己先掛掉。盤出來的缺口分兩類處理：bootstrap 自身依賴的（如 sudo）當前置手動補；其餘的（如 rsync、編譯工具）進套件清單，交給 bootstrap 一起裝。</p>
<h2 id="跟-bootstrap-套件清單的界線">跟 Bootstrap 套件清單的界線</h2>
<p>這篇的驗證跟 <a href="/blog/linux/dotfile/08-sync-bootstrap/bootstrap-script-packages/" data-link-title="Bootstrap Script 與套件清單管理" data-link-desc="寫 dotfile 的 install script、或整理「這台機器裝了什麼」的套件清單時回來讀">模組八的 bootstrap script 設計</a> 是兩件互補的事，界線在「假設」上。bootstrap script 的套件清單假設一個前提：機器已經有能力執行安裝（有 sudo、有 package manager、清單裡的東西都能被裝上）。這篇處理的正是那個前提成立之前的階段——最小系統到底有沒有滿足那些假設，缺的補上，讓 bootstrap 的假設變成事實。</p>
<p>換句話說，套件清單回答「這台機器最終要有哪些套件」，工具驗證回答「這台機器現在夠不夠資格開始跑那份清單」。把兩者分清楚，才不會把 sudo 這種前置誤塞進靠 sudo 安裝的清單裡。</p>
<h2 id="下一步">下一步</h2>
<p>工具補齊、機器有能力執行安裝之後，你還困在一個地方：擠在機器的主控台手打。怎麼從舒適的本機終端機操作它，以及還沒有 SSH key 時怎麼把 dotfile 弄進去，<a href="../ssh-keyless-bootstrap/">外部連入、SSH key 與無 key 的 bootstrap 路徑</a> 處理這兩件事。</p>
]]></content:encoded></item><item><title>環境建置的操作順序</title><link>https://tarrragon.github.io/blog/linux/dotfile/00-dotfile-mindset/setup-order-guide/</link><pubDate>Tue, 30 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/00-dotfile-mindset/setup-order-guide/</guid><description>&lt;p>Dotfile 教學模組按主題組織（shell、終端機、視窗管理），適合理解各層概念。但第一次建環境時需要的是另一種順序——&lt;strong>按依賴關係排列的操作清單&lt;/strong>，因為有些步驟是後續步驟的前提。&lt;/p>
&lt;p>SSH key 是典型例子：管理工具和 shell 配置的知識在模組一和模組二，但實際操作時 SSH key 比這兩者都早——因為 &lt;code>git clone git@github.com:...&lt;/code> 本身就需要 SSH key。如果照模組順序走，到模組二才發現 dotfile repo clone 不下來。&lt;/p>
&lt;p>這篇是路線圖，告訴你每一步做什麼、為什麼這個順序、以及去哪個模組看具體操作。&lt;/p>
&lt;h2 id="階段一基礎設施後續所有步驟的前提">階段一：基礎設施（後續所有步驟的前提）&lt;/h2>
&lt;p>這些步驟在任何配置之前完成，因為它們是 Git、遠端存取、dotfile clone 的前提。&lt;/p>
&lt;h3 id="1-安裝作業系統--建立使用者帳號">1. 安裝作業系統 + 建立使用者帳號&lt;/h3>
&lt;p>macOS：開箱即用。Linux：選發行版（Arch 如果要用 Hyprland）、完成安裝、建立非 root 使用者。安裝程式每個選項該怎麼判讀、裝完最小系統缺哪些必要工具（連 &lt;code>sudo&lt;/code> 都可能沒有），見 &lt;a href="https://tarrragon.github.io/blog/linux/install/install-option-decisions/" data-link-title="Linux 安裝選項判讀" data-link-desc="在 Linux 安裝程式面對 locale、網路、磁碟分割、檔案系統、bootloader 等選項、需要判斷依據而非靠預設值硬選時回來讀">Linux 安裝選項判讀&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/linux/install/minimal-install-verify/" data-link-title="最小安裝後的工具驗證與補足" data-link-desc="最小化安裝的 Linux 裝完發現連 sudo 或 which 都沒有、bootstrap 腳本第一行就炸、需要先確認系統缺哪些必要工具再補時回來讀">最小安裝後的工具驗證與補足&lt;/a>——這一步是整個系列展開最深、卻最常被一句帶過的地基。&lt;/p>
&lt;h3 id="2-生成-ssh-key-pair">2. 生成 SSH key pair&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">ssh-keygen -t ed25519 -C &lt;span class="s2">&amp;#34;your-email@example.com&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>為什麼這麼早做：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Git 操作&lt;/strong>：GitHub / GitLab 的 SSH 認證需要 public key。dotfile repo 通常用 SSH URL（&lt;code>git@github.com:...&lt;/code>），clone 前要先把 key 部署到 GitHub。用 HTTPS URL 可以繞過 SSH key，但長期來看 SSH key 是更省事的認證方式。還沒有 key、或想用 HTTPS / PAT 把 dotfile 弄進一台新機器的幾種路徑，見 &lt;a href="https://tarrragon.github.io/blog/linux/install/ssh-keyless-bootstrap/" data-link-title="外部連入、SSH key 與無 key 的 bootstrap 路徑" data-link-desc="要從本機終端機操作新裝好的 Linux 機器、設 SSH key 免密碼、或還沒有 key 就想把 dotfile 弄進機器跑 install.sh 時回來讀">外部連入、SSH key 與無 key 的 bootstrap 路徑&lt;/a>。&lt;/li>
&lt;li>&lt;strong>遠端救援&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/" data-link-title="模組七：桌面環境維護與故障排除" data-link-desc="桌面凍結、compositor 掛了、或某個工具不回應時回來讀 — Linux 桌面的故障隔離模型、常見故障場景的恢復操作、日誌判讀與診斷工具">模組七&lt;/a>的場景三（GPU hang）依賴 SSH 作為桌面凍結時的救生通道。key 提前設好，出問題時才有路可走。&lt;/li>
&lt;li>&lt;strong>跨機器操作&lt;/strong>：筆電連桌機、桌機連 VM、VS Code Remote SSH——都靠這把 key。&lt;/li>
&lt;/ul>
&lt;h3 id="3-部署-public-key">3. 部署 public key&lt;/h3>
&lt;p>把 &lt;code>~/.ssh/id_ed25519.pub&lt;/code> 加到需要的服務：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 加到 GitHub&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">cat ~/.ssh/id_ed25519.pub
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1"># 複製輸出，貼到 GitHub → Settings → SSH and GPG keys → New SSH key&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># 加到另一台機器（可選，用於跨機器 SSH）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">ssh-copy-id user@target-machine&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="4-package-manager--git">4. Package manager + Git&lt;/h3>
&lt;p>macOS：先裝 Homebrew（macOS 的套件管理器，後續安裝 stow、tmux 等工具都靠它），再裝 Git：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 安裝 Homebrew&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">/bin/bash -c &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 安裝 Git（或用 xcode-select --install，會一併裝 Apple 的 Git）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">brew install git&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Arch：&lt;code>pacman&lt;/code> 隨 OS 安裝已可用，直接裝 Git：&lt;code>pacman -S git&lt;/code>。&lt;/p></description><content:encoded><![CDATA[<p>Dotfile 教學模組按主題組織（shell、終端機、視窗管理），適合理解各層概念。但第一次建環境時需要的是另一種順序——<strong>按依賴關係排列的操作清單</strong>，因為有些步驟是後續步驟的前提。</p>
<p>SSH key 是典型例子：管理工具和 shell 配置的知識在模組一和模組二，但實際操作時 SSH key 比這兩者都早——因為 <code>git clone git@github.com:...</code> 本身就需要 SSH key。如果照模組順序走，到模組二才發現 dotfile repo clone 不下來。</p>
<p>這篇是路線圖，告訴你每一步做什麼、為什麼這個順序、以及去哪個模組看具體操作。</p>
<h2 id="階段一基礎設施後續所有步驟的前提">階段一：基礎設施（後續所有步驟的前提）</h2>
<p>這些步驟在任何配置之前完成，因為它們是 Git、遠端存取、dotfile clone 的前提。</p>
<h3 id="1-安裝作業系統--建立使用者帳號">1. 安裝作業系統 + 建立使用者帳號</h3>
<p>macOS：開箱即用。Linux：選發行版（Arch 如果要用 Hyprland）、完成安裝、建立非 root 使用者。安裝程式每個選項該怎麼判讀、裝完最小系統缺哪些必要工具（連 <code>sudo</code> 都可能沒有），見 <a href="/blog/linux/install/install-option-decisions/" data-link-title="Linux 安裝選項判讀" data-link-desc="在 Linux 安裝程式面對 locale、網路、磁碟分割、檔案系統、bootloader 等選項、需要判斷依據而非靠預設值硬選時回來讀">Linux 安裝選項判讀</a> 與 <a href="/blog/linux/install/minimal-install-verify/" data-link-title="最小安裝後的工具驗證與補足" data-link-desc="最小化安裝的 Linux 裝完發現連 sudo 或 which 都沒有、bootstrap 腳本第一行就炸、需要先確認系統缺哪些必要工具再補時回來讀">最小安裝後的工具驗證與補足</a>——這一步是整個系列展開最深、卻最常被一句帶過的地基。</p>
<h3 id="2-生成-ssh-key-pair">2. 生成 SSH key pair</h3>





<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-keygen -t ed25519 -C <span class="s2">&#34;your-email@example.com&#34;</span></span></span></code></pre></div><p>為什麼這麼早做：</p>
<ul>
<li><strong>Git 操作</strong>：GitHub / GitLab 的 SSH 認證需要 public key。dotfile repo 通常用 SSH URL（<code>git@github.com:...</code>），clone 前要先把 key 部署到 GitHub。用 HTTPS URL 可以繞過 SSH key，但長期來看 SSH key 是更省事的認證方式。還沒有 key、或想用 HTTPS / PAT 把 dotfile 弄進一台新機器的幾種路徑，見 <a href="/blog/linux/install/ssh-keyless-bootstrap/" data-link-title="外部連入、SSH key 與無 key 的 bootstrap 路徑" data-link-desc="要從本機終端機操作新裝好的 Linux 機器、設 SSH key 免密碼、或還沒有 key 就想把 dotfile 弄進機器跑 install.sh 時回來讀">外部連入、SSH key 與無 key 的 bootstrap 路徑</a>。</li>
<li><strong>遠端救援</strong>：<a href="/blog/linux/dotfile/07-desktop-maintenance/" data-link-title="模組七：桌面環境維護與故障排除" data-link-desc="桌面凍結、compositor 掛了、或某個工具不回應時回來讀 — Linux 桌面的故障隔離模型、常見故障場景的恢復操作、日誌判讀與診斷工具">模組七</a>的場景三（GPU hang）依賴 SSH 作為桌面凍結時的救生通道。key 提前設好，出問題時才有路可走。</li>
<li><strong>跨機器操作</strong>：筆電連桌機、桌機連 VM、VS Code Remote SSH——都靠這把 key。</li>
</ul>
<h3 id="3-部署-public-key">3. 部署 public key</h3>
<p>把 <code>~/.ssh/id_ed25519.pub</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"># 加到 GitHub</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">cat ~/.ssh/id_ed25519.pub
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 複製輸出，貼到 GitHub → Settings → SSH and GPG keys → New SSH key</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 加到另一台機器（可選，用於跨機器 SSH）</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">ssh-copy-id user@target-machine</span></span></code></pre></div><h3 id="4-package-manager--git">4. Package manager + Git</h3>
<p>macOS：先裝 Homebrew（macOS 的套件管理器，後續安裝 stow、tmux 等工具都靠它），再裝 Git：</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"># 安裝 Homebrew</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">/bin/bash -c <span class="s2">&#34;</span><span class="k">$(</span>curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 安裝 Git（或用 xcode-select --install，會一併裝 Apple 的 Git）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">brew install git</span></span></code></pre></div><p>Arch：<code>pacman</code> 隨 OS 安裝已可用，直接裝 Git：<code>pacman -S git</code>。</p>
<h3 id="5-clone-dotfile-repo">5. Clone dotfile repo</h3>





<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">git clone git@github.com:yourname/dotfiles.git ~/dotfiles</span></span></code></pre></div><p>如果是第一次建 dotfile repo（還沒有 repo），先建一個空的再開始往裡面加配置。具體做法見<a href="/blog/linux/dotfile/01-dotfile-management/" data-link-title="模組一：管理工具與目錄結構" data-link-desc="要把散落在家目錄的配置檔集中版控時，選 bare repo、stow 還是 chezmoi、目錄該怎麼組織">模組一：管理工具與目錄結構</a>。</p>
<h2 id="階段二shell-與終端機日常操作的基礎">階段二：Shell 與終端機（日常操作的基礎）</h2>
<p>Shell 是所有操作的介面，終端機是 shell 的容器。這兩層配置好，後續的安裝、設定、除錯效率會高很多。</p>
<h3 id="6-安裝管理工具stow--chezmoi">6. 安裝管理工具（stow / chezmoi）</h3>
<p>把 dotfile repo 裡的配置 symlink 到正確位置。具體選型和操作見<a href="/blog/linux/dotfile/01-dotfile-management/" data-link-title="模組一：管理工具與目錄結構" data-link-desc="要把散落在家目錄的配置檔集中版控時，選 bare repo、stow 還是 chezmoi、目錄該怎麼組織">模組一</a>。</p>
<h3 id="7-shell-配置zshrc--bashrc">7. Shell 配置（.zshrc / .bashrc）</h3>
<p>模組化拆分、PATH 設定、alias、prompt。做完這一步，終端機操作才順手。見<a href="/blog/linux/dotfile/02-shell-config/" data-link-title="模組二：Shell 配置" data-link-desc="shell 配置檔長成一坨不敢動時回來讀 — .zshrc/.bashrc 的結構化拆分、alias/function/PATH 的模組化設計">模組二：Shell 配置</a>。</p>
<h3 id="8-終端機--編輯器">8. 終端機 + 編輯器</h3>
<p>Terminal emulator 選型、tmux/zellij、neovim 基礎配置。見<a href="/blog/linux/dotfile/03-terminal-ecosystem/" data-link-title="模組三：終端機與編輯器" data-link-desc="終端機相關工具的配置檔散落在不同位置、不確定哪些該進 dotfile repo 時回來讀">模組三：終端機與編輯器</a>。</p>
<p>macOS 用戶到階段二完成後就有一個完整的工作環境。下一步依序讀<a href="/blog/linux/dotfile/01-dotfile-management/" data-link-title="模組一：管理工具與目錄結構" data-link-desc="要把散落在家目錄的配置檔集中版控時，選 bare repo、stow 還是 chezmoi、目錄該怎麼組織">模組一</a>（管理工具選型）、<a href="/blog/linux/dotfile/02-shell-config/" data-link-title="模組二：Shell 配置" data-link-desc="shell 配置檔長成一坨不敢動時回來讀 — .zshrc/.bashrc 的結構化拆分、alias/function/PATH 的模組化設計">模組二</a>（shell 配置）、<a href="/blog/linux/dotfile/03-terminal-ecosystem/" data-link-title="模組三：終端機與編輯器" data-link-desc="終端機相關工具的配置檔散落在不同位置、不確定哪些該進 dotfile repo 時回來讀">模組三</a>（終端機），然後跳到階段四的 bootstrap script。階段三是 Linux 桌面環境的設定，macOS 用戶跳過。</p>
<h2 id="階段三桌面環境linux-限定">階段三：桌面環境（Linux 限定）</h2>
<p>macOS 用戶到階段二就有一個完整的工作環境了。以下步驟是 Linux 桌面環境的設定，macOS 用戶可以跳到階段四。</p>
<h3 id="9-視窗管理器">9. 視窗管理器</h3>
<p>平鋪式 vs 浮動式的選型，Hyprland 安裝和核心配置。見<a href="/blog/linux/dotfile/04-window-management/" data-link-title="模組四：視窗管理與平鋪式工作流" data-link-desc="同時開多個視窗時的排列策略 — 手動貼齊跟自動平鋪的差距在哪、macOS 和 Linux 各有哪些工具、多螢幕怎麼處理、什麼情境值得從浮動切換到平鋪">模組四</a>和<a href="/blog/linux/dotfile/05-hyprland-config/" data-link-title="模組五：Hyprland 配置" data-link-desc="要在 Linux 上設定 Hyprland 平鋪式桌面時回來讀">模組五</a>。</p>
<h3 id="10-桌面配套工具--rice">10. 桌面配套工具 + Rice</h3>
<p>waybar、wofi、mako、配色系統。見<a href="/blog/linux/dotfile/06-rice-design/" data-link-title="模組六：桌面 Rice 設計" data-link-desc="Hyprland 桌面從能用到好看好用 — 狀態列、啟動器、通知、鎖屏、配色系統的設計與配置">模組六：桌面 Rice 設計</a>。</p>
<h3 id="11-啟用-ssh-server--預防措施">11. 啟用 SSH server + 預防措施</h3>
<p>桌面環境可用之後，設定遠端救援通道和預防性配置：</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"># SSH server（出問題時可以從另一台機器救援）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">sudo systemctl <span class="nb">enable</span> sshd
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">sudo systemctl start sshd
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># 停用密碼登入（確保 SSH key 已設好）</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># 編輯 /etc/ssh/sshd_config：PasswordAuthentication no</span>
</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"><span class="c1"># swap（OOM 緩衝）</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">sudo fallocate -l 4G /swapfile
</span></span><span class="line"><span class="ln">10</span><span class="cl">sudo chmod <span class="m">600</span> /swapfile <span class="o">&amp;&amp;</span> sudo mkswap /swapfile <span class="o">&amp;&amp;</span> sudo swapon /swapfile
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># systemd-oomd</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">sudo systemctl <span class="nb">enable</span> systemd-oomd</span></span></code></pre></div><p>為什麼放在桌面設定之後：SSH server 和 swap 是預防措施，桌面能用了才有東西要保護。但 SSH key pair（階段一步驟 2-3）要提前做——key pair 是認證基礎設施，server 只是把門打開。</p>
<p>詳細的故障場景和預防措施見<a href="/blog/linux/dotfile/07-desktop-maintenance/" data-link-title="模組七：桌面環境維護與故障排除" data-link-desc="桌面凍結、compositor 掛了、或某個工具不回應時回來讀 — Linux 桌面的故障隔離模型、常見故障場景的恢復操作、日誌判讀與診斷工具">模組七：桌面環境維護與故障排除</a>。</p>
<h2 id="階段四同步與可攜性">階段四：同步與可攜性</h2>
<p>環境建好之後，確保這份配置能搬到下一台機器。</p>
<h3 id="12-bootstrap-script">12. Bootstrap script</h3>
<p>把階段一到三的操作自動化成 script，下次換機器跑一次就好。見<a href="/blog/linux/dotfile/08-sync-bootstrap/" data-link-title="模組八：同步、Bootstrap 與環境重建" data-link-desc="換機器或重灌時怎麼還原工作環境 — bootstrap script 設計、套件清單管理、跨機器同步策略、secret 排除，以及 VM 快照和 dotfile 重建兩種思路的場景判讀">模組八：同步、Bootstrap 與環境重建</a>。</p>
<h3 id="13-secret-管理">13. Secret 管理</h3>
<p>哪些東西該進 repo、哪些要排除（SSH private key、API token、密碼）。同樣見<a href="/blog/linux/dotfile/08-sync-bootstrap/" data-link-title="模組八：同步、Bootstrap 與環境重建" data-link-desc="換機器或重灌時怎麼還原工作環境 — bootstrap script 設計、套件清單管理、跨機器同步策略、secret 排除，以及 VM 快照和 dotfile 重建兩種思路的場景判讀">模組八</a>。</p>
<h2 id="依賴關係速查">依賴關係速查</h2>





<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">OS 安裝
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  └─ SSH key pair ← 後續所有 Git / SSH 操作的前提
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">       └─ Git 安裝
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">            └─ dotfile repo clone
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">                 └─ 管理工具（stow link）
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">                      ├─ Shell 配置
</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">                      └─ 桌面環境（Linux，macOS 到此為止 → 直接跳階段四）
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">                           └─ SSH server + 預防措施
</span></span><span class="line"><span class="ln">10</span><span class="cl">                                └─ Bootstrap script 自動化</span></span></code></pre></div><h2 id="可以亂序的步驟">可以亂序的步驟</h2>
<p>依賴圖裡<strong>同一層級的步驟</strong>可以調換順序。具體來說：</p>
<ul>
<li>Shell 配置、終端機配置、編輯器配置三者互不依賴，先做哪個都行</li>
<li>視窗管理器和桌面配套工具可以交替設定（先裝 Hyprland 再裝 waybar，或反過來）</li>
<li>swap 和 SSH server 互不依賴，先做哪個都行</li>
</ul>
<p>跨層級的依賴必須按順序：SSH key 是 clone repo 的前提，repo 是 stow link 的前提，stow link 是 shell 配置生效的前提。</p>
]]></content:encoded></item><item><title>跨機器同步、Secret 管理與環境重建流程</title><link>https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/sync-strategy-secret/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/sync-strategy-secret/</guid><description>&lt;h2 id="跨機器同步策略">跨機器同步策略&lt;/h2>
&lt;p>多台機器共用 dotfile repo 時，需要一套同步策略來處理「改了配置後怎麼讓其他機器也更新」。&lt;/p>
&lt;h3 id="git-pushpull手動">Git push/pull（手動）&lt;/h3>
&lt;p>最基本的做法：改了就 commit + push，另一台機器 pull + 重新 apply。優點是簡單、沒有額外依賴。缺點是容易忘記——在公司機器上改了一個 alias，回家忘記 push，隔天公司又改了一版，兩邊 diverge。&lt;/p>
&lt;p>適合只有一兩台機器、改動不頻繁的人。&lt;/p>
&lt;h3 id="自動同步">自動同步&lt;/h3>
&lt;p>chezmoi 內建 &lt;code>chezmoi update&lt;/code> 指令（pull + apply 一步完成），搭配 cron 或 systemd timer 定期執行：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ini" data-lang="ini">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># ~/.config/systemd/user/chezmoi-update.timer&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="k">[Unit]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="na">Description&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">Update dotfiles daily&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="k">[Timer]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="na">OnCalendar&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">daily&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="na">Persistent&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="k">[Install]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="na">WantedBy&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">timers.target&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ini" data-lang="ini">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># ~/.config/systemd/user/chezmoi-update.service&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="k">[Unit]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="na">Description&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">chezmoi update&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="k">[Service]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="na">Type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">oneshot&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="na">ExecStart&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">/usr/bin/chezmoi update --no-tty&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>自動同步減少手動操作，但要注意衝突處理——如果兩台機器同時改了同一個檔案且都 push，後面那台的自動 pull 會遇到 merge conflict。實務上 dotfile 很少有真正的衝突（兩台機器同時改同一行的機率低），但偶爾發生時需要手動介入。&lt;/p>
&lt;h3 id="機器差異的處理">機器差異的處理&lt;/h3>
&lt;p>推薦的模式是 main branch 放所有共用配置，機器差異用條件判斷處理。&lt;/p>
&lt;p>用 shell 的 OS 判斷：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># ~/.zshrc&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="o">[[&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>uname -s&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;Darwin&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nb">export&lt;/span> &lt;span class="nv">PATH&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/opt/homebrew/bin:&lt;/span>&lt;span class="nv">$PATH&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nb">alias&lt;/span> &lt;span class="nv">ls&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;ls -G&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="k">else&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="nb">alias&lt;/span> &lt;span class="nv">ls&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;ls --color=auto&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="k">fi&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>用 chezmoi template（Go template 語法）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># chezmoi 管理的 .zshrc.tmpl&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="o">{{&lt;/span> &lt;span class="k">if&lt;/span> eq .chezmoi.os &lt;span class="s2">&amp;#34;darwin&amp;#34;&lt;/span> -&lt;span class="o">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">PATH&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/opt/homebrew/bin:&lt;/span>&lt;span class="nv">$PATH&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="o">{{&lt;/span> end -&lt;span class="o">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="o">{{&lt;/span> &lt;span class="k">if&lt;/span> eq .chezmoi.hostname &lt;span class="s2">&amp;#34;work-laptop&amp;#34;&lt;/span> -&lt;span class="o">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">HTTP_PROXY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;http://proxy.corp:8080&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="o">{{&lt;/span> end -&lt;span class="o">}}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>chezmoi template 的優勢是條件判斷發生在 apply 階段，產出的檔案裡看不到 template 語法，乾淨且不依賴 shell 的 runtime 判斷。&lt;/p>
&lt;p>不推薦每台機器一個 branch 的做法。短期可行，長期一定 diverge——main 加了新配置，各 branch 要 rebase 或 merge，忘了就漂移。一份 main + template 條件判斷是長期可維護的結構。&lt;/p>
&lt;h2 id="secret-排除與管理">Secret 排除與管理&lt;/h2>
&lt;p>dotfile repo 通常是 public 或至少多人可見的。以下東西進了 repo 等於把鑰匙掛在門口：&lt;/p>
&lt;ul>
&lt;li>SSH 私鑰（&lt;code>~/.ssh/id_*&lt;/code>、&lt;code>*.pem&lt;/code>）&lt;/li>
&lt;li>API token、password、.env 檔案&lt;/li>
&lt;li>GPG 私鑰&lt;/li>
&lt;li>cloud provider 的 credential 檔案（&lt;code>~/.aws/credentials&lt;/code>、&lt;code>~/.config/gcloud/application_default_credentials.json&lt;/code>）&lt;/li>
&lt;li>browser profile 裡的 cookie / session&lt;/li>
&lt;/ul>
&lt;h3 id="gitignore-是第一道防線">.gitignore 是第一道防線&lt;/h3>





&lt;pre tabindex="0">&lt;code class="language-gitignore" data-lang="gitignore"># SSH 私鑰
*.pem
id_*
known_hosts
authorized_keys

# 環境變數
.env
.env.*

# Cloud credentials
credentials
application_default_credentials.json&lt;/code>&lt;/pre>&lt;p>但 .gitignore 只防「不小心 add」，不防「故意 add -f」。更重要的是建立習慣：repo 裡永遠只放「看到了也沒關係」的東西。&lt;/p>
&lt;h3 id="ssh-config-的特殊處理">SSH config 的特殊處理&lt;/h3>
&lt;p>&lt;code>~/.ssh/config&lt;/code>（host alias、ProxyJump 設定、port forwarding）本身不含 secret，可以進 repo——它記錄的是「連線要怎麼走」而不是「憑證是什麼」。但同一個 &lt;code>~/.ssh/&lt;/code> 目錄下的私鑰絕對排除。&lt;/p></description><content:encoded><![CDATA[<h2 id="跨機器同步策略">跨機器同步策略</h2>
<p>多台機器共用 dotfile repo 時，需要一套同步策略來處理「改了配置後怎麼讓其他機器也更新」。</p>
<h3 id="git-pushpull手動">Git push/pull（手動）</h3>
<p>最基本的做法：改了就 commit + push，另一台機器 pull + 重新 apply。優點是簡單、沒有額外依賴。缺點是容易忘記——在公司機器上改了一個 alias，回家忘記 push，隔天公司又改了一版，兩邊 diverge。</p>
<p>適合只有一兩台機器、改動不頻繁的人。</p>
<h3 id="自動同步">自動同步</h3>
<p>chezmoi 內建 <code>chezmoi update</code> 指令（pull + apply 一步完成），搭配 cron 或 systemd timer 定期執行：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># ~/.config/systemd/user/chezmoi-update.timer</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">Update dotfiles daily</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="k">[Timer]</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="na">OnCalendar</span><span class="o">=</span><span class="s">daily</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="na">Persistent</span><span class="o">=</span><span class="s">true</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="k">[Install]</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="na">WantedBy</span><span class="o">=</span><span class="s">timers.target</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># ~/.config/systemd/user/chezmoi-update.service</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">chezmoi update</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">oneshot</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">/usr/bin/chezmoi update --no-tty</span></span></span></code></pre></div><p>自動同步減少手動操作，但要注意衝突處理——如果兩台機器同時改了同一個檔案且都 push，後面那台的自動 pull 會遇到 merge conflict。實務上 dotfile 很少有真正的衝突（兩台機器同時改同一行的機率低），但偶爾發生時需要手動介入。</p>
<h3 id="機器差異的處理">機器差異的處理</h3>
<p>推薦的模式是 main branch 放所有共用配置，機器差異用條件判斷處理。</p>
<p>用 shell 的 OS 判斷：</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"># ~/.zshrc</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="k">$(</span>uname -s<span class="k">)</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;Darwin&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nb">export</span> <span class="nv">PATH</span><span class="o">=</span><span class="s2">&#34;/opt/homebrew/bin:</span><span class="nv">$PATH</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nb">alias</span> <span class="nv">ls</span><span class="o">=</span><span class="s2">&#34;ls -G&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nb">alias</span> <span class="nv">ls</span><span class="o">=</span><span class="s2">&#34;ls --color=auto&#34;</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="k">fi</span></span></span></code></pre></div><p>用 chezmoi template（Go template 語法）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># chezmoi 管理的 .zshrc.tmpl</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="o">{{</span> <span class="k">if</span> eq .chezmoi.os <span class="s2">&#34;darwin&#34;</span> -<span class="o">}}</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nb">export</span> <span class="nv">PATH</span><span class="o">=</span><span class="s2">&#34;/opt/homebrew/bin:</span><span class="nv">$PATH</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="o">{{</span> end -<span class="o">}}</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="o">{{</span> <span class="k">if</span> eq .chezmoi.hostname <span class="s2">&#34;work-laptop&#34;</span> -<span class="o">}}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="nb">export</span> <span class="nv">HTTP_PROXY</span><span class="o">=</span><span class="s2">&#34;http://proxy.corp:8080&#34;</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="o">{{</span> end -<span class="o">}}</span></span></span></code></pre></div><p>chezmoi template 的優勢是條件判斷發生在 apply 階段，產出的檔案裡看不到 template 語法，乾淨且不依賴 shell 的 runtime 判斷。</p>
<p>不推薦每台機器一個 branch 的做法。短期可行，長期一定 diverge——main 加了新配置，各 branch 要 rebase 或 merge，忘了就漂移。一份 main + template 條件判斷是長期可維護的結構。</p>
<h2 id="secret-排除與管理">Secret 排除與管理</h2>
<p>dotfile repo 通常是 public 或至少多人可見的。以下東西進了 repo 等於把鑰匙掛在門口：</p>
<ul>
<li>SSH 私鑰（<code>~/.ssh/id_*</code>、<code>*.pem</code>）</li>
<li>API token、password、.env 檔案</li>
<li>GPG 私鑰</li>
<li>cloud provider 的 credential 檔案（<code>~/.aws/credentials</code>、<code>~/.config/gcloud/application_default_credentials.json</code>）</li>
<li>browser profile 裡的 cookie / session</li>
</ul>
<h3 id="gitignore-是第一道防線">.gitignore 是第一道防線</h3>





<pre tabindex="0"><code class="language-gitignore" data-lang="gitignore"># SSH 私鑰
*.pem
id_*
known_hosts
authorized_keys

# 環境變數
.env
.env.*

# Cloud credentials
credentials
application_default_credentials.json</code></pre><p>但 .gitignore 只防「不小心 add」，不防「故意 add -f」。更重要的是建立習慣：repo 裡永遠只放「看到了也沒關係」的東西。</p>
<h3 id="ssh-config-的特殊處理">SSH config 的特殊處理</h3>
<p><code>~/.ssh/config</code>（host alias、ProxyJump 設定、port forwarding）本身不含 secret，可以進 repo——它記錄的是「連線要怎麼走」而不是「憑證是什麼」。但同一個 <code>~/.ssh/</code> 目錄下的私鑰絕對排除。</p>
<p>stow 管理時的目錄結構範例：</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">dotfiles/
</span></span><span class="line"><span class="ln">2</span><span class="cl">└── ssh/
</span></span><span class="line"><span class="ln">3</span><span class="cl">    └── .ssh/
</span></span><span class="line"><span class="ln">4</span><span class="cl">        └── config        # 進 repo
</span></span><span class="line"><span class="ln">5</span><span class="cl">        # id_rsa 不放這裡
</span></span><span class="line"><span class="ln">6</span><span class="cl">        # known_hosts 不放這裡</span></span></code></pre></div><h3 id="三個層級的-secret-管理">三個層級的 secret 管理</h3>
<p><strong>層級一：手動</strong>。.gitignore 排除 secret 檔案，在 README 記錄「這些東西需要在新機器手動設定」。最低成本、對只有一兩台機器的人足夠。</p>
<p><strong>層級二：密碼管理器整合</strong>。chezmoi 支援從 1Password、Bitwarden、pass（Unix password manager）等拉取 secret：</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"># chezmoi template 語法
</span></span><span class="line"><span class="ln">2</span><span class="cl">{{ (onepasswordRead &#34;op://Personal/SSH Key/private key&#34;).value }}</span></span></code></pre></div><p>配置檔的 template 裡引用密碼管理器的條目，apply 時自動填入。secret 不在 repo 裡，但 repo 知道去哪拉。</p>
<p><strong>層級三：加密存放</strong>。用 age 或 sops 把 secret 加密後直接存在 repo 裡。解密需要對應的 key。chezmoi 原生支援 age 加密：</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"># 加密一個檔案</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">chezmoi add --encrypt ~/.ssh/id_rsa
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># repo 裡看到的是加密後的內容</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">cat ~/.local/share/chezmoi/private_dot_ssh/id_rsa.age</span></span></code></pre></div><p>加密存放的好處是 secret 跟著 repo 走、不用另外設密碼管理器。風險是加密 key 本身變成唯一的依賴——丟了 key，加密的 secret 就拿不回來。</p>
<p>層級選擇取決於安全需求和便利需求的平衡。多數情況從層級一開始，覺得手動處理太煩再往上升級。</p>
<h2 id="環境重建的實際流程">環境重建的實際流程</h2>
<p>假設拿到一台全新的 Arch Linux 機器，要從零重建完整的 Hyprland 桌面環境。以下是 end-to-end 的步驟，對應 <a href="/blog/linux/dotfile/08-sync-bootstrap/bootstrap-script-packages/" data-link-title="Bootstrap Script 與套件清單管理" data-link-desc="寫 dotfile 的 install script、或整理「這台機器裝了什麼」的套件清單時回來讀">bootstrap script</a> 的每個階段。</p>
<h3 id="階段一最小可用環境">階段一：最小可用環境</h3>





<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"># Arch 安裝完成後，base system 只有 bash 和基本工具</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo pacman -S git base-devel</span></span></code></pre></div><p>這是 bootstrap script 的唯一外部前提：有 Git 能 clone repo、有 base-devel 能編譯 AUR 套件。其他一切由 script 處理。</p>
<h3 id="階段二取得-dotfile-repo">階段二：取得 dotfile repo</h3>





<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">git clone https://github.com/you/dotfiles ~/dotfiles
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">cd</span> ~/dotfiles</span></span></code></pre></div><p>如果 repo 是 private，這一步需要先設定 SSH key 或用 HTTPS + token。這是前面提到的 secret 雞生蛋問題——clone 含有 SSH config 的 repo 本身就需要 SSH key。解法通常是：第一次用 HTTPS clone，deploy 完 SSH config 後把 remote 改成 SSH。</p>
<h3 id="階段三執行-bootstrap">階段三：執行 bootstrap</h3>





<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">./scripts/install.sh</span></span></code></pre></div><p>script 依序：安裝套件（Hyprland、waybar、rofi、wezterm、zsh、neovim、stow 等）、用 stow 部署配置到 <code>$HOME</code>、執行初始化（換 shell、安裝 neovim plugin）。</p>
<h3 id="階段四手動處理">階段四：手動處理</h3>
<p>bootstrap 處理不了（或不該處理）的部分：</p>
<ul>
<li><strong>SSH 私鑰</strong>：從備份或密碼管理器取回，放到 <code>~/.ssh/</code>，設定正確權限（<code>chmod 600</code>）</li>
<li><strong>Git 簽署用的 GPG key</strong>：如果有用 commit signing</li>
<li><strong>密碼管理器登入</strong>：如果 secret 管理用了層級二或三</li>
</ul>
<h3 id="階段五硬體相關調整">階段五：硬體相關調整</h3>
<p>Hyprland 的 monitor 設定（解析度、縮放、排列位置）跟實際接的螢幕有關，這部分配置每台機器都不同：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># ~/.config/hypr/hyprland.conf 的 monitor 段</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 這幾行在每台機器上都要調</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">monitor</span><span class="o">=</span><span class="s">DP-1, 2560x1440@144, 0x0, 1</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="na">monitor</span><span class="o">=</span><span class="s">HDMI-A-1, 1920x1080@60, 2560x0, 1</span></span></span></code></pre></div><p>處理方式有兩種：把 monitor 設定拆成獨立的 <code>monitor.conf</code>，主配置用 <code>source</code> 引入，<code>monitor.conf</code> 不進 repo（加進 .gitignore）、每台機器本地維護；或者用 chezmoi template 按 hostname 判斷。</p>
<p>顯卡驅動（Intel/AMD 通常自動、NVIDIA 需要額外安裝 <code>nvidia-dkms</code> 和設定環境變數）也是硬體相關的步驟，可以放在 bootstrap script 的 OS 判斷裡，但通常 Arch 安裝階段就已經處理。</p>
<h3 id="階段六驗證">階段六：驗證</h3>





<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"># 登出 TTY，重新用 Hyprland 登入</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 或者直接在 TTY 執行</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">Hyprland</span></span></code></pre></div><p>登入後確認：視窗管理器正常運作、keybind 正確、狀態列出現、字型正確渲染、終端機配色正常。如果某個元件沒反應，通常是套件沒裝或配置路徑不對——回去檢查 bootstrap 的套件清單和 stow 的 symlink。</p>
<h3 id="時間預估">時間預估</h3>
<p>整個流程在網路順暢的情況下，大約 30 分鐘到 1 小時，取決於套件數量和下載速度。主要時間花在套件安裝（pacman 下載 + 編譯 AUR 套件）。配置 deploy 本身是秒級操作（stow 只建 symlink）。</p>
<p>對比沒有 dotfile 管理時的重建：邊想邊裝、裝了忘記某個工具的名稱、配置靠記憶手打、兩天後還在調某個快捷鍵為什麼不對。差距在「可預期 vs 碰運氣」。</p>
<h2 id="維護節奏">維護節奏</h2>
<p>環境重建能力需要持續維護，不是設定完就一勞永逸。</p>
<p>日常習慣：新裝一個工具時，順手更新套件清單（<code>brew bundle dump</code> 或手動加一行到 <code>packages.txt</code>）。改了一個配置後，commit + push。這個習慣的建立成本低，但需要刻意練幾週才會變成反射動作。</p>
<p>定期檢查：每隔幾個月在 VM 或 container 裡跑一次完整的 bootstrap，驗證 script 還能從零跑通。配置會演進、套件會改名或被取代、script 裡硬寫的路徑可能失效——定期驗證才能確保「這份重建指令真的能重建」，而不是一份過期的紀錄。</p>
]]></content:encoded></item><item><title>外部連入、SSH key 與無 key 的 bootstrap 路徑</title><link>https://tarrragon.github.io/blog/linux/install/ssh-keyless-bootstrap/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/install/ssh-keyless-bootstrap/</guid><description>&lt;p>操作一台新機器，從你本機的終端機透過 SSH 連進去是阻力最小的位置。直接在主控台操作有兩個實際的痛點：純文字的主控台（TTY 或虛擬機的序列 console）往往不能貼上，長指令只能手打、還容易掉字；畫面也通常擠、不能捲。把機器的 sshd 跑起來、從本機 SSH 進去之後，貼上、捲動、補全全部回到你熟悉的環境，而且這條路本身就貼近真實的遠端維運。&lt;/p>
&lt;p>這篇處理三件事：把 sshd 跑起來並從本機連入、設 SSH key 達到免密碼、以及一個容易被卡住的情境——你還沒有 SSH key 時，怎麼把 dotfile 弄進機器、跑完基礎安裝。&lt;/p>
&lt;h2 id="啟用-sshd-並從本機連入">啟用 sshd 並從本機連入&lt;/h2>
&lt;p>讓機器能被 SSH 連入只需要兩步：裝 SSH 伺服器、啟動它的服務。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">pacman -S openssh &lt;span class="c1"># 剛裝好的系統套件資料庫是新的，-S 不必先 -Sy&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">systemctl &lt;span class="nb">enable&lt;/span> --now sshd &lt;span class="c1"># enable 開機自啟、--now 立刻啟動&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>指令以 Arch 為例。換發行版時套件管理器不同（Fedora &lt;code>dnf&lt;/code>、Debian/Ubuntu &lt;code>apt&lt;/code>），服務名也可能不同——Debian 系的 OpenSSH 服務叫 &lt;code>ssh&lt;/code> 不是 &lt;code>sshd&lt;/code>，那邊要 &lt;code>systemctl enable --now ssh&lt;/code>。&lt;/p>
&lt;p>從本機連的時候用一般使用者、不要用 root：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">ssh user@&amp;lt;機器 IP&amp;gt; &lt;span class="c1"># IP 來自機器上的 ip -brief a&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>用一般使用者是因為多數發行版的 sshd 預設擋 root 密碼登入（&lt;code>PermitRootLogin prohibit-password&lt;/code>）——root 只能用 key、不能用密碼。這個預設是好的安全姿態，順著它走、用你裝系統時建的一般使用者連即可。連進去之後，後續所有需要長指令、需要貼上的操作都在這個 session 裡做，不再回主控台手打。&lt;/p>
&lt;p>這裡啟用 sshd 是為了 bootstrap 期間從本機連入操作，跟 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/00-dotfile-mindset/setup-order-guide/" data-link-title="環境建置的操作順序" data-link-desc="第一次從零建立 Linux 或 macOS 開發環境、不確定先做什麼後做什麼時讀 — 依賴順序路線圖，每一步附對應模組連結">操作順序指引&lt;/a> 後段把 sshd 當「桌面就緒後的常駐遠端救援通道」是兩個不同的時間點與目的——同一個 &lt;code>systemctl enable sshd&lt;/code> 動作，這裡是為了「現在好操作」，那裡是為了「之後好救援」。&lt;/p>
&lt;h2 id="ssh-key-免密碼">SSH key 免密碼&lt;/h2>
&lt;p>每次連線都打密碼很快會變成阻力，尤其當你要反覆同步檔案或跑自動化時。SSH key 讓本機免密碼連入，做法是生一把金鑰、把公鑰放進機器、本機用私鑰認證。&lt;/p>
&lt;p>生 key 時建議生一把專用的、不要佔用本機的預設金鑰槽，並在 SSH 設定裡給它一個好記的別名：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">ssh-keygen -t ed25519 -f ~/.ssh/vm_arch -N &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span> -C &lt;span class="s2">&amp;#34;vm_arch host-&amp;gt;target&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># 在 ~/.ssh/config 加一段別名：&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1"># Host vm&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># HostName &amp;lt;機器 IP&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># User &amp;lt;你的使用者&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1"># IdentityFile ~/.ssh/vm_arch&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1"># IdentitiesOnly yes&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>專用 key 的好處是它的權限範圍清楚——這把只給這台機器用，跟你其他身分的金鑰互不牽連。設好別名後，&lt;code>ssh vm&lt;/code> 就免密碼連入，後面的 &lt;code>rsync&lt;/code>、&lt;code>scp&lt;/code> 也跟著免密碼。&lt;/p>
&lt;p>把公鑰放進機器有兩條路。標準工具是 &lt;code>ssh-copy-id&lt;/code>，它會在本機跑、要你輸入一次目標機的密碼。另一條省一次切換的路是：當你已經用密碼連進機器、且這個 session 在真終端機裡（貼上可用），直接把公鑰內容貼進機器的 &lt;code>authorized_keys&lt;/code>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">mkdir -p ~/.ssh &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> chmod &lt;span class="m">700&lt;/span> ~/.ssh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;ssh-ed25519 AAAA... 你的公鑰內容&amp;#34;&lt;/span> &amp;gt;&amp;gt; ~/.ssh/authorized_keys
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">chmod &lt;span class="m">600&lt;/span> ~/.ssh/authorized_keys&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>兩條路等價，選哪條看你當下在哪——還沒連上就用 &lt;code>ssh-copy-id&lt;/code>，已經連上就直接貼，少一次切換。&lt;/p>
&lt;h2 id="還沒有-ssh-key-時怎麼把-dotfile-弄進去">還沒有 SSH key 時，怎麼把 dotfile 弄進去&lt;/h2>
&lt;p>設 SSH key 是讓「之後」連線變方便，但 bootstrap 的第一步——把 dotfile repo 弄進機器——並不一定需要 key。常見的卡點是把「clone repo」跟「有 SSH key」綁在一起，但 clone 有不需要 key 的路徑。怎麼把 dotfile 弄進去，取決於這份 dotfile 放在哪。&lt;/p>
&lt;p>&lt;strong>repo 是公開的（在 GitHub 之類）&lt;/strong>：用 HTTPS clone，公開 repo 的唯讀 clone 不需要任何認證。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">git clone https://github.com/&amp;lt;帳號&amp;gt;/dotfiles ~/dotfiles
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nb">cd&lt;/span> ~/dotfiles &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> ./scripts/install.sh&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這是最直接的路——機器只要能上網就能拉到 dotfile，完全繞過 key 的問題。clone URL 裡的帳號要對；用錯帳號（例如把 email handle 當成 GitHub 帳號）會 clone 失敗或抓到別的 repo，這類筆誤在只看 README 範例時很容易漏掉。SSH key 在這個情境只有「之後要從機器 push 回去」才需要，純粹跑部署用不到。&lt;/p></description><content:encoded><![CDATA[<p>操作一台新機器，從你本機的終端機透過 SSH 連進去是阻力最小的位置。直接在主控台操作有兩個實際的痛點：純文字的主控台（TTY 或虛擬機的序列 console）往往不能貼上，長指令只能手打、還容易掉字；畫面也通常擠、不能捲。把機器的 sshd 跑起來、從本機 SSH 進去之後，貼上、捲動、補全全部回到你熟悉的環境，而且這條路本身就貼近真實的遠端維運。</p>
<p>這篇處理三件事：把 sshd 跑起來並從本機連入、設 SSH key 達到免密碼、以及一個容易被卡住的情境——你還沒有 SSH key 時，怎麼把 dotfile 弄進機器、跑完基礎安裝。</p>
<h2 id="啟用-sshd-並從本機連入">啟用 sshd 並從本機連入</h2>
<p>讓機器能被 SSH 連入只需要兩步：裝 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">pacman -S openssh             <span class="c1"># 剛裝好的系統套件資料庫是新的，-S 不必先 -Sy</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">systemctl <span class="nb">enable</span> --now sshd   <span class="c1"># enable 開機自啟、--now 立刻啟動</span></span></span></code></pre></div><p>指令以 Arch 為例。換發行版時套件管理器不同（Fedora <code>dnf</code>、Debian/Ubuntu <code>apt</code>），服務名也可能不同——Debian 系的 OpenSSH 服務叫 <code>ssh</code> 不是 <code>sshd</code>，那邊要 <code>systemctl enable --now ssh</code>。</p>
<p>從本機連的時候用一般使用者、不要用 root：</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">ssh user@&lt;機器 IP&gt;            <span class="c1"># IP 來自機器上的 ip -brief a</span></span></span></code></pre></div><p>用一般使用者是因為多數發行版的 sshd 預設擋 root 密碼登入（<code>PermitRootLogin prohibit-password</code>）——root 只能用 key、不能用密碼。這個預設是好的安全姿態，順著它走、用你裝系統時建的一般使用者連即可。連進去之後，後續所有需要長指令、需要貼上的操作都在這個 session 裡做，不再回主控台手打。</p>
<p>這裡啟用 sshd 是為了 bootstrap 期間從本機連入操作，跟 <a href="/blog/linux/dotfile/00-dotfile-mindset/setup-order-guide/" data-link-title="環境建置的操作順序" data-link-desc="第一次從零建立 Linux 或 macOS 開發環境、不確定先做什麼後做什麼時讀 — 依賴順序路線圖，每一步附對應模組連結">操作順序指引</a> 後段把 sshd 當「桌面就緒後的常駐遠端救援通道」是兩個不同的時間點與目的——同一個 <code>systemctl enable sshd</code> 動作，這裡是為了「現在好操作」，那裡是為了「之後好救援」。</p>
<h2 id="ssh-key-免密碼">SSH key 免密碼</h2>
<p>每次連線都打密碼很快會變成阻力，尤其當你要反覆同步檔案或跑自動化時。SSH key 讓本機免密碼連入，做法是生一把金鑰、把公鑰放進機器、本機用私鑰認證。</p>
<p>生 key 時建議生一把專用的、不要佔用本機的預設金鑰槽，並在 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">ssh-keygen -t ed25519 -f ~/.ssh/vm_arch -N <span class="s2">&#34;&#34;</span> -C <span class="s2">&#34;vm_arch host-&gt;target&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 在 ~/.ssh/config 加一段別名：</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">#   Host vm</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">#       HostName &lt;機器 IP&gt;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">#       User &lt;你的使用者&gt;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1">#       IdentityFile ~/.ssh/vm_arch</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1">#       IdentitiesOnly yes</span></span></span></code></pre></div><p>專用 key 的好處是它的權限範圍清楚——這把只給這台機器用，跟你其他身分的金鑰互不牽連。設好別名後，<code>ssh vm</code> 就免密碼連入，後面的 <code>rsync</code>、<code>scp</code> 也跟著免密碼。</p>
<p>把公鑰放進機器有兩條路。標準工具是 <code>ssh-copy-id</code>，它會在本機跑、要你輸入一次目標機的密碼。另一條省一次切換的路是：當你已經用密碼連進機器、且這個 session 在真終端機裡（貼上可用），直接把公鑰內容貼進機器的 <code>authorized_keys</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">mkdir -p ~/.ssh <span class="o">&amp;&amp;</span> chmod <span class="m">700</span> ~/.ssh
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;ssh-ed25519 AAAA... 你的公鑰內容&#34;</span> &gt;&gt; ~/.ssh/authorized_keys
</span></span><span class="line"><span class="ln">3</span><span class="cl">chmod <span class="m">600</span> ~/.ssh/authorized_keys</span></span></code></pre></div><p>兩條路等價，選哪條看你當下在哪——還沒連上就用 <code>ssh-copy-id</code>，已經連上就直接貼，少一次切換。</p>
<h2 id="還沒有-ssh-key-時怎麼把-dotfile-弄進去">還沒有 SSH key 時，怎麼把 dotfile 弄進去</h2>
<p>設 SSH key 是讓「之後」連線變方便，但 bootstrap 的第一步——把 dotfile repo 弄進機器——並不一定需要 key。常見的卡點是把「clone repo」跟「有 SSH key」綁在一起，但 clone 有不需要 key 的路徑。怎麼把 dotfile 弄進去，取決於這份 dotfile 放在哪。</p>
<p><strong>repo 是公開的（在 GitHub 之類）</strong>：用 HTTPS clone，公開 repo 的唯讀 clone 不需要任何認證。</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">git clone https://github.com/&lt;帳號&gt;/dotfiles ~/dotfiles
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">cd</span> ~/dotfiles <span class="o">&amp;&amp;</span> ./scripts/install.sh</span></span></code></pre></div><p>這是最直接的路——機器只要能上網就能拉到 dotfile，完全繞過 key 的問題。clone URL 裡的帳號要對；用錯帳號（例如把 email handle 當成 GitHub 帳號）會 clone 失敗或抓到別的 repo，這類筆誤在只看 README 範例時很容易漏掉。SSH key 在這個情境只有「之後要從機器 push 回去」才需要，純粹跑部署用不到。</p>
<p><strong>repo 是私有的、但機器能上網</strong>：機器可以直接 clone，用 GitHub Personal Access Token（PAT）走 HTTPS——這是私有 repo 免 SSH key 的標準解。clone 時把 PAT 當密碼填進認證，機器就拉得到，一樣不必在它上面設 SSH key。</p>
<p><strong>repo 還沒推到任何遠端、或機器離線</strong>：從本機把檔案傳進去。如果本機到機器的 SSH 已經能用（即使只是密碼登入），用 <code>tar</code> over SSH 一次傳進去（跟 <code>scp -r</code> 等價，差別只在 tar 能一次打包、又好控制要不要帶 <code>.git</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">tar czf - --exclude <span class="s1">&#39;.git&#39;</span> . <span class="p">|</span> ssh user@host <span class="s1">&#39;mkdir -p ~/dotfiles &amp;&amp; tar xzf - -C ~/dotfiles&#39;</span></span></span></code></pre></div><p>這條只需要兩邊都有的 <code>ssh</code> 跟 <code>tar</code>，不依賴目標機有 rsync。從 macOS 傳的時候要關掉 AppleDouble 中繼檔，否則會夾帶一堆 <code>._</code> 開頭的中繼檔到 Linux 上：在指令前加 <code>COPYFILE_DISABLE=1</code>。完全離線、連 SSH 都還沒通時，最後手段是把 repo 放進 USB、掛載到機器上複製出來。</p>
<p>把 dotfile 弄進去之後，跑它的 <code>install.sh</code> 完成基礎安裝。如果安裝腳本一開始就要用 sudo，記得 sudo 必須在工具驗證階段就備好——它是 <a href="../minimal-install-verify/">最小安裝後的工具驗證與補足</a> 的前置，bootstrap 自身補不了。</p>
<h2 id="換一台新機器或重裝時ssh-為什麼突然連不上">換一台新機器（或重裝）時，SSH 為什麼突然連不上</h2>
<p>SSH 的別名、金鑰、<code>known_hosts</code> 都是綁在「某一台特定機器」上的，所以當你重裝、或換一台新 VM，先前設好的 <code>ssh &lt;別名&gt;</code> 往往會以看似無關的錯誤失敗——那套設定是為舊機器建的，而重裝後是另一台機器：不同的 IP、不同的 SSH host key、還沒裝 sshd、<code>authorized_keys</code> 也是空的。判讀的起點是把重裝後的機器當成全新的一台，重做第一次連線的設定，而不是沿用舊別名。</p>
<p>失敗會以三種形式出現，各對應不同層、各有各的修法：</p>
<p><code>Permission denied (publickey)</code> 是認證被拒，代表 sshd 有在跑、連線有到（這是進度），卡在金鑰這關。常見於你用的別名設了 <code>IdentitiesOnly yes</code> 只送某一把 key，而新機器的 <code>authorized_keys</code> 還沒有它。修法是改用帳號加 IP 直連、走密碼，繞過那個鎖死金鑰的別名：<code>ssh user@&lt;新 IP&gt;</code>，密碼是「這次安裝」為該使用者設的（每次重裝各自獨立，不是舊機器那個）。連進去後再把公鑰貼回新機器的 <code>authorized_keys</code>、把別名的 <code>HostName</code> 更新成新 IP，免密碼才會恢復。</p>
<p><code>Host key verification failed</code>（或 <code>REMOTE HOST IDENTIFICATION HAS CHANGED</code>）發生在新機器剛好拿到跟舊機器一樣的 IP 時：你本機 <code>known_hosts</code> 存的是舊機器的 host key，SSH 偵測到同一個 IP 換了 key、當成可能的中間人攻擊而拒連。修法是刪掉那筆舊紀錄，再重連時接受新 key：</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">ssh-keygen -R &lt;IP&gt;       <span class="c1"># 刪掉該 IP 的舊 host key</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">ssh-keygen -R &lt;別名&gt;     <span class="c1"># 有用別名的話一併刪</span></span></span></code></pre></div><p><code>Connection refused</code> 代表沒有 sshd 在監聽，也就是新機器還沒把 SSH server 起來。修法回到最開始——在新機器的 console 裝 openssh、啟動服務（見本篇開頭「啟用 sshd」），這一步在每台全新機器上都要重做。</p>
<p>三個症狀的共同根因是同一件事：SSH 的便利設定（別名、金鑰、host key 快取）綁的是機器身分、不會跟著「重裝」自動轉移。把它們當成「為某一台機器設好的」，換機器就重做第一次連線，能省下對著看似無關的錯誤瞎猜的時間。</p>
<h2 id="連入後可能遇到的兩個終端機問題">連入後可能遇到的兩個終端機問題</h2>
<p>SSH 連線本身通了之後，互動 shell 還可能因為終端機環境不對而出現「打字變亂碼、prompt 重繪錯位」。這類問題在你用現代終端機（如 Ghostty、Kitty）連進一台剛裝好的最小 Linux、又跑了 unicode 較重的 prompt（如 Powerlevel10k）時最容易出現，根源是兩個跟字元處理有關的終端機設定，跟你的 shell 配置無關。</p>
<p>第一個是 locale。macOS 的終端機 SSH 連線時常把 <code>LC_CTYPE=UTF-8</code> 送到遠端，但 <code>UTF-8</code> 不是合法的 Linux locale 名稱，Linux 收到後 fallback 成 <code>POSIX</code>/C locale——於是 shell 的行編輯器把輸入當單位元組處理，配上 unicode 字元的 prompt 就重繪成一個字母重複好幾次的累加亂碼。判讀方式是在遠端跑 <code>locale</code>，看 <code>LANG</code> 是不是空的、<code>LC_CTYPE</code> 是不是 <code>POSIX</code>。修法是在 shell 設定裡強制一個合法的 UTF-8 locale（前提是該 locale 已生成，見 <a href="../install-option-decisions/">安裝選項判讀</a> 的 locale 段）：</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="nb">export</span> <span class="nv">LANG</span><span class="o">=</span>en_US.UTF-8
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">export</span> <span class="nv">LC_CTYPE</span><span class="o">=</span>en_US.UTF-8</span></span></code></pre></div><p>第二個是 terminfo。現代終端機會把 <code>TERM</code> 設成自己的值（Ghostty 是 <code>xterm-ghostty</code>、Kitty 是 <code>xterm-kitty</code>），而一台剛裝好的 Linux 的 terminfo 資料庫沒有這些條目，shell 的行編輯器做「清行重繪」時找不到對應的控制序列、就把畫面畫壞。判讀方式是在遠端 <code>echo $TERM</code> 看是哪個值、<code>toe | grep &lt;值&gt;</code> 看遠端認不認得。修法有兩條：把你終端機的 terminfo 灌進遠端（保留完整功能），或退而求其次強制一個遠端一定有的 <code>TERM</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"># 把本機終端機的 terminfo 灌進遠端的 ~/.terminfo（推薦）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">infocmp -x <span class="s2">&#34;</span><span class="nv">$TERM</span><span class="s2">&#34;</span> <span class="p">|</span> ssh remote <span class="s1">&#39;tic -x -&#39;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 或：連線時強制遠端一定有的 TERM（功能略降，但保證能用）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">ssh -t remote <span class="s1">&#39;TERM=xterm-256color exec zsh -l&#39;</span></span></span></code></pre></div><p>這兩個問題的共同點是：它們在你裝了 unicode 較重的互動 shell 之後才浮現，而陽春的 shell（ASCII prompt）即使 locale 跟 terminfo 都不對也照樣能用。所以排查時，先確認是不是這層、而不是去懷疑剛裝的 shell 配置壞了。</p>
<h2 id="連入傳輸安裝的順序">連入、傳輸、安裝的順序</h2>
<p>這三件事有一個固定的先後，順序錯了會在中間卡住。先把 sshd 跑起來、從本機連入，取得一個能貼上、可捲動的 session；再把 dotfile 弄進機器（公開 repo 走 HTTPS clone、私有或本地走傳輸）；最後在機器上跑 install.sh 完成安裝。SSH key 是讓「連入」從每次打密碼變成免密碼的優化，可以在任何時候補，不是這條鏈的必要環節、也不是 bootstrap 的前置。</p>
<p><a href="/blog/linux/dotfile/00-dotfile-mindset/setup-order-guide/" data-link-title="環境建置的操作順序" data-link-desc="第一次從零建立 Linux 或 macOS 開發環境、不確定先做什麼後做什麼時讀 — 依賴順序路線圖，每一步附對應模組連結">模組零的操作順序指引</a> 把「生成 SSH key、部署公鑰」列為標準流程的一環，那是預設你會建 key 的主路徑。這篇補的是它沒展開的另一面：當你手上還沒有 key、或這台機器的 dotfile 根本不需要 key 就能取得時，怎麼一樣把 bootstrap 跑完。</p>
<h2 id="下一步">下一步</h2>
<p>連入、傳輸、安裝都跑通之後，真正的考驗是當 install.sh 中途失敗時——而它遲早會撞到失敗——你能不能快速看出哪裡錯了。這取決於安裝腳本有沒有把可觀測性內建進去，<a href="../observable-bootstrap/">可除錯的 bootstrap</a> 談的就是怎麼內建。</p>
]]></content:encoded></item><item><title>可除錯的 bootstrap：把可觀測性內建進安裝腳本</title><link>https://tarrragon.github.io/blog/linux/install/observable-bootstrap/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/install/observable-bootstrap/</guid><description>&lt;p>Bootstrap 腳本失敗是常態，所以它的設計目標之一應該是「失敗時可診斷」：把失敗當成會發生的事來設計，預先留好定位問題的痕跡。一支自動化安裝腳本要跨越的環境差異很多——機器缺某個工具、套件清單有筆誤、某個指令在這個發行版的行為跟預期不同——任何一處都可能讓它中斷。決定你是「三分鐘看出哪裡錯」還是「對著終端機捲半天瞎猜」的，是這支腳本有沒有在設計時就把可觀測性內建進去，跟運氣無關。&lt;/p>
&lt;p>可觀測性要事先設計，是因為失敗發生的當下，你能拿到的資訊就已經定型了。如果腳本只把輸出丟到終端機、失敗時只留下一句通用的錯誤，那當下你就只有那句話可看；如果它一路把帶時間戳的紀錄寫進檔案、失敗時主動印出出錯的位置，那同一個失敗就變得可定位。差別不在失敗本身，在失敗前你準備了什麼。如果你寫的是自己的 bootstrap（例如部署 dotfile 的那支 &lt;code>install.sh&lt;/code>），這層要在你第一次跑它之前就設計進去，而不是等它出事才回頭加；就算腳本不是你寫的、你只是來 debug 一次失敗，下一段「找程式自己的 log」一樣適用。&lt;/p>
&lt;h2 id="為什麼會瞎找">為什麼會瞎找&lt;/h2>
&lt;p>不可觀測的腳本失敗時，你手上只有終端機捲動過的那些輸出，而那往往不足以定位真正的原因。終端機的輸出是易逝的、會被後續輸出沖掉、多個來源的訊息交錯在一起；更麻煩的是，很多失敗的「表面錯誤」離「真正原因」隔了好幾層。一個指令因為前面某個變數是空的而失敗，但它報出來的錯可能完全沒提到那個空變數——你看著一個誤導性的症狀，往上游找不到源頭。&lt;/p>
&lt;p>破解這種瞎找的，常常是一份你一開始沒看的 log。很多程式在終端機只印一段摘要，卻同時把詳細的執行紀錄寫進一個 log 檔；當終端機的訊息不足以定位時，那份程式自己寫的 log 裡往往就有答案。除錯時養成「找程式自己的 log，而不是只盯著終端機捲動」的習慣，是把瞎找變成定位的關鍵一步——這也是 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/log-reading-diagnostic-tools/" data-link-title="日誌判讀與診斷工具" data-link-desc="知道桌面出了問題但不確定原因時回來讀 — journalctl、dmesg、hyprctl、systemctl 的使用方式和常見 log pattern">模組七日誌判讀&lt;/a> 的核心。而對你自己寫的 bootstrap，你可以更進一步：在設計時就讓它產生這樣一份 log。&lt;/p>
&lt;h2 id="三個內建可觀測性的手法">三個內建可觀測性的手法&lt;/h2>
&lt;p>讓一支 bootstrap 腳本可診斷，有三個低成本、效果明顯的手法，它們合起來把「失敗了」變成「失敗在第幾行、哪個指令、什麼狀態」。&lt;/p>
&lt;h3 id="log-落地把全部輸出-tee-進帶時間戳的檔案">log 落地：把全部輸出 tee 進帶時間戳的檔案&lt;/h3>
&lt;p>第一個手法是讓腳本的全部輸出同時進終端機跟一個 log 檔，而不是只進終端機。終端機的捲動是易逝的，log 檔是持久的——可以事後 &lt;code>grep&lt;/code>、可以貼給別人看、可以比對前後兩次跑的差異。在 bash 裡，一行 &lt;code>exec&lt;/code> 就能把後續所有 stdout 與 stderr 都導去 &lt;code>tee&lt;/code>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nv">LOG_DIR&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">XDG_STATE_HOME&lt;/span>&lt;span class="k">:-&lt;/span>&lt;span class="nv">$HOME&lt;/span>&lt;span class="p">/.local/state&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">/dotfiles&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">mkdir -p &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$LOG_DIR&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nv">LOG_FILE&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$LOG_DIR&lt;/span>&lt;span class="s2">/install-&lt;/span>&lt;span class="k">$(&lt;/span>date +%Y%m%d-%H%M%S&lt;span class="k">)&lt;/span>&lt;span class="s2">.log&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="nb">exec&lt;/span> &amp;gt; &amp;gt;&lt;span class="o">(&lt;/span>tee -a &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$LOG_FILE&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span> 2&amp;gt;&lt;span class="p">&amp;amp;&lt;/span>&lt;span class="m">1&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>帶時間戳的檔名讓每次跑各留一份、不互相覆蓋，事後可以回溯「上一次成功跟這次失敗差在哪」。log 檔放在 &lt;code>XDG_STATE_HOME&lt;/code>（狀態資料的標準位置）底下，符合慣例、也不污染家目錄。&lt;/p>
&lt;h3 id="錯誤定位用-err-trap-印出出錯的行與指令">錯誤定位：用 ERR trap 印出出錯的行與指令&lt;/h3>
&lt;p>第二個手法是讓腳本在中斷的瞬間，主動報出「是哪一行、哪個指令、什麼結束碼」失敗的。配合 &lt;code>set -e&lt;/code>（出錯即停）的腳本，預設只會默默地停，不告訴你停在哪。加一個 &lt;code>ERR&lt;/code> trap，就能在 &lt;code>set -e&lt;/code> 中斷之前先印出定位資訊：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nb">set&lt;/span> -Eeuo pipefail &lt;span class="c1"># -E 讓 ERR trap 在函式/子 shell 也生效&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nb">trap&lt;/span> &lt;span class="s1">&amp;#39;log &amp;#34;ERROR line $LINENO: [$BASH_COMMAND] exit=$?&amp;#34;&amp;#39;&lt;/span> ERR&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>$LINENO&lt;/code> 是出錯的行號、&lt;code>$BASH_COMMAND&lt;/code> 是當下正在執行的那條指令、&lt;code>$?&lt;/code> 是它的結束碼。三者合起來，輸出會長這樣：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">[00:06:51] ERROR line 40: [sudo pacman -S --needed stow git zsh] exit=1&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>範例裡的 &lt;code>pacman&lt;/code> 換發行版會不同，這裡只是示意 trap 輸出的格式——手法本身（行號 + 指令 + 結束碼）跟發行版無關。這一行直接點名元兇。前面提過的那類「表面錯誤離真正原因隔好幾層」的情況——例如某個指令因為 &lt;code>which&lt;/code> 不存在而拿到空字串、最後報一個看似無關的錯——有了這行，你會直接看到是哪一行的哪條指令掛了，不必從誤導性的症狀往回猜。&lt;code>set -E&lt;/code>（&lt;code>-E&lt;/code> 旗標）是為了讓 trap 在函式跟子 shell 裡也照樣觸發，少了它，包在函式裡的錯誤會漏掉。&lt;/p>
&lt;h3 id="步驟標記用帶時間戳的-log-函式標出進度">步驟標記：用帶時間戳的 log 函式標出進度&lt;/h3>
&lt;p>第三個手法是在關鍵步驟前印一行帶時間戳的標記，讓你能看出腳本跑到哪、哪一步慢。一個極簡的 log 函式就夠：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">log&lt;span class="o">()&lt;/span> &lt;span class="o">{&lt;/span> &lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;[%s] %s\n&amp;#39;&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>date +%H:%M:%S&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$*&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">log &lt;span class="s2">&amp;#34;install.sh start | OS=&lt;/span>&lt;span class="nv">$OS&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">log &lt;span class="s2">&amp;#34;Installing base packages...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">log &lt;span class="s2">&amp;#34;Stowing configs...&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>時間戳的價值在於它同時給你「進度」跟「效能」兩種資訊：失敗時，最後一行成功的 log 告訴你它跨過了哪些步驟、卡在哪一步之後；正常時，相鄰兩行的時間差告訴你哪一步耗時最久。這比沒有標記、只能從一堆套件下載輸出裡猜「現在到底在幹嘛」清楚得多。&lt;/p>
&lt;h2 id="失敗可診斷是設計目標">失敗可診斷是設計目標&lt;/h2>
&lt;p>把這三個手法合起來，一支原本「失敗時只留一句通用錯誤」的腳本，會變成「每次跑都留一份完整 log、失敗時直接點名第幾行哪個指令、過程中每步都有時間戳」。成本是腳本開頭多幾行，回報是把未來每一次除錯從瞎找變成定位。這層可觀測性是 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/bootstrap-script-packages/" data-link-title="Bootstrap Script 與套件清單管理" data-link-desc="寫 dotfile 的 install script、或整理「這台機器裝了什麼」的套件清單時回來讀">模組八 bootstrap script 設計&lt;/a> 的延伸——那篇給安裝腳本的骨架與套件清單，這篇給它加上失敗時的診斷能力，兩篇處理的是同一支腳本的兩個層面。&lt;/p>
&lt;p>這是設計階段的決定，不是事後能補的。當一支沒有可觀測性的腳本在一台陌生機器上失敗，你沒辦法回到過去讓它記錄當時的狀態——資訊在失敗的瞬間就已經流失了。所以「失敗可診斷」要跟功能一起設計進去，把它當成 bootstrap 的基本屬性，而不是出事之後才想加的補丁。&lt;/p>
&lt;h2 id="回到系列">回到系列&lt;/h2>
&lt;p>這幾篇合起來，是把一台機器從「空的」帶到「能接收 dotfile、且部署過程可診斷」的完整地基：&lt;a href="../install-option-decisions/">安裝選項判讀&lt;/a> 處理 OS 怎麼裝、&lt;a href="../minimal-install-verify/">工具驗證與補足&lt;/a> 處理裝完缺什麼、&lt;a href="../ssh-keyless-bootstrap/">外部連入與無 key bootstrap&lt;/a> 處理怎麼連進去把 dotfile 弄進來，這一篇處理當部署失敗時怎麼快速看出原因。再往前一步，把這套地基用在無人值守的長任務上、讓機器在你離開後自己跑完工作，見 &lt;a href="../unattended-remote-work/">讓機器跑無人值守的長任務&lt;/a>——無人盯著的任務尤其依賴這篇談的可觀測性。地基打好，後面 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/01-dotfile-management/" data-link-title="模組一：管理工具與目錄結構" data-link-desc="要把散落在家目錄的配置檔集中版控時，選 bare repo、stow 還是 chezmoi、目錄該怎麼組織">模組一到八&lt;/a> 的 dotfile 管理才有立足點。&lt;/p></description><content:encoded><![CDATA[<p>Bootstrap 腳本失敗是常態，所以它的設計目標之一應該是「失敗時可診斷」：把失敗當成會發生的事來設計，預先留好定位問題的痕跡。一支自動化安裝腳本要跨越的環境差異很多——機器缺某個工具、套件清單有筆誤、某個指令在這個發行版的行為跟預期不同——任何一處都可能讓它中斷。決定你是「三分鐘看出哪裡錯」還是「對著終端機捲半天瞎猜」的，是這支腳本有沒有在設計時就把可觀測性內建進去，跟運氣無關。</p>
<p>可觀測性要事先設計，是因為失敗發生的當下，你能拿到的資訊就已經定型了。如果腳本只把輸出丟到終端機、失敗時只留下一句通用的錯誤，那當下你就只有那句話可看；如果它一路把帶時間戳的紀錄寫進檔案、失敗時主動印出出錯的位置，那同一個失敗就變得可定位。差別不在失敗本身，在失敗前你準備了什麼。如果你寫的是自己的 bootstrap（例如部署 dotfile 的那支 <code>install.sh</code>），這層要在你第一次跑它之前就設計進去，而不是等它出事才回頭加；就算腳本不是你寫的、你只是來 debug 一次失敗，下一段「找程式自己的 log」一樣適用。</p>
<h2 id="為什麼會瞎找">為什麼會瞎找</h2>
<p>不可觀測的腳本失敗時，你手上只有終端機捲動過的那些輸出，而那往往不足以定位真正的原因。終端機的輸出是易逝的、會被後續輸出沖掉、多個來源的訊息交錯在一起；更麻煩的是，很多失敗的「表面錯誤」離「真正原因」隔了好幾層。一個指令因為前面某個變數是空的而失敗，但它報出來的錯可能完全沒提到那個空變數——你看著一個誤導性的症狀，往上游找不到源頭。</p>
<p>破解這種瞎找的，常常是一份你一開始沒看的 log。很多程式在終端機只印一段摘要，卻同時把詳細的執行紀錄寫進一個 log 檔；當終端機的訊息不足以定位時，那份程式自己寫的 log 裡往往就有答案。除錯時養成「找程式自己的 log，而不是只盯著終端機捲動」的習慣，是把瞎找變成定位的關鍵一步——這也是 <a href="/blog/linux/dotfile/07-desktop-maintenance/log-reading-diagnostic-tools/" data-link-title="日誌判讀與診斷工具" data-link-desc="知道桌面出了問題但不確定原因時回來讀 — journalctl、dmesg、hyprctl、systemctl 的使用方式和常見 log pattern">模組七日誌判讀</a> 的核心。而對你自己寫的 bootstrap，你可以更進一步：在設計時就讓它產生這樣一份 log。</p>
<h2 id="三個內建可觀測性的手法">三個內建可觀測性的手法</h2>
<p>讓一支 bootstrap 腳本可診斷，有三個低成本、效果明顯的手法，它們合起來把「失敗了」變成「失敗在第幾行、哪個指令、什麼狀態」。</p>
<h3 id="log-落地把全部輸出-tee-進帶時間戳的檔案">log 落地：把全部輸出 tee 進帶時間戳的檔案</h3>
<p>第一個手法是讓腳本的全部輸出同時進終端機跟一個 log 檔，而不是只進終端機。終端機的捲動是易逝的，log 檔是持久的——可以事後 <code>grep</code>、可以貼給別人看、可以比對前後兩次跑的差異。在 bash 裡，一行 <code>exec</code> 就能把後續所有 stdout 與 stderr 都導去 <code>tee</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="nv">LOG_DIR</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">XDG_STATE_HOME</span><span class="k">:-</span><span class="nv">$HOME</span><span class="p">/.local/state</span><span class="si">}</span><span class="s2">/dotfiles&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">mkdir -p <span class="s2">&#34;</span><span class="nv">$LOG_DIR</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nv">LOG_FILE</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$LOG_DIR</span><span class="s2">/install-</span><span class="k">$(</span>date +%Y%m%d-%H%M%S<span class="k">)</span><span class="s2">.log&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nb">exec</span> &gt; &gt;<span class="o">(</span>tee -a <span class="s2">&#34;</span><span class="nv">$LOG_FILE</span><span class="s2">&#34;</span><span class="o">)</span> 2&gt;<span class="p">&amp;</span><span class="m">1</span></span></span></code></pre></div><p>帶時間戳的檔名讓每次跑各留一份、不互相覆蓋，事後可以回溯「上一次成功跟這次失敗差在哪」。log 檔放在 <code>XDG_STATE_HOME</code>（狀態資料的標準位置）底下，符合慣例、也不污染家目錄。</p>
<h3 id="錯誤定位用-err-trap-印出出錯的行與指令">錯誤定位：用 ERR trap 印出出錯的行與指令</h3>
<p>第二個手法是讓腳本在中斷的瞬間，主動報出「是哪一行、哪個指令、什麼結束碼」失敗的。配合 <code>set -e</code>（出錯即停）的腳本，預設只會默默地停，不告訴你停在哪。加一個 <code>ERR</code> trap，就能在 <code>set -e</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="nb">set</span> -Eeuo pipefail   <span class="c1"># -E 讓 ERR trap 在函式/子 shell 也生效</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">trap</span> <span class="s1">&#39;log &#34;ERROR line $LINENO: [$BASH_COMMAND] exit=$?&#34;&#39;</span> ERR</span></span></code></pre></div><p><code>$LINENO</code> 是出錯的行號、<code>$BASH_COMMAND</code> 是當下正在執行的那條指令、<code>$?</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">[00:06:51] ERROR line 40: [sudo pacman -S --needed stow git zsh] exit=1</span></span></code></pre></div><p>範例裡的 <code>pacman</code> 換發行版會不同，這裡只是示意 trap 輸出的格式——手法本身（行號 + 指令 + 結束碼）跟發行版無關。這一行直接點名元兇。前面提過的那類「表面錯誤離真正原因隔好幾層」的情況——例如某個指令因為 <code>which</code> 不存在而拿到空字串、最後報一個看似無關的錯——有了這行，你會直接看到是哪一行的哪條指令掛了，不必從誤導性的症狀往回猜。<code>set -E</code>（<code>-E</code> 旗標）是為了讓 trap 在函式跟子 shell 裡也照樣觸發，少了它，包在函式裡的錯誤會漏掉。</p>
<h3 id="步驟標記用帶時間戳的-log-函式標出進度">步驟標記：用帶時間戳的 log 函式標出進度</h3>
<p>第三個手法是在關鍵步驟前印一行帶時間戳的標記，讓你能看出腳本跑到哪、哪一步慢。一個極簡的 log 函式就夠：</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">log<span class="o">()</span> <span class="o">{</span> <span class="nb">printf</span> <span class="s1">&#39;[%s] %s\n&#39;</span> <span class="s2">&#34;</span><span class="k">$(</span>date +%H:%M:%S<span class="k">)</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$*</span><span class="s2">&#34;</span><span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl">log <span class="s2">&#34;install.sh start | OS=</span><span class="nv">$OS</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">log <span class="s2">&#34;Installing base packages...&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">log <span class="s2">&#34;Stowing configs...&#34;</span></span></span></code></pre></div><p>時間戳的價值在於它同時給你「進度」跟「效能」兩種資訊：失敗時，最後一行成功的 log 告訴你它跨過了哪些步驟、卡在哪一步之後；正常時，相鄰兩行的時間差告訴你哪一步耗時最久。這比沒有標記、只能從一堆套件下載輸出裡猜「現在到底在幹嘛」清楚得多。</p>
<h2 id="失敗可診斷是設計目標">失敗可診斷是設計目標</h2>
<p>把這三個手法合起來，一支原本「失敗時只留一句通用錯誤」的腳本，會變成「每次跑都留一份完整 log、失敗時直接點名第幾行哪個指令、過程中每步都有時間戳」。成本是腳本開頭多幾行，回報是把未來每一次除錯從瞎找變成定位。這層可觀測性是 <a href="/blog/linux/dotfile/08-sync-bootstrap/bootstrap-script-packages/" data-link-title="Bootstrap Script 與套件清單管理" data-link-desc="寫 dotfile 的 install script、或整理「這台機器裝了什麼」的套件清單時回來讀">模組八 bootstrap script 設計</a> 的延伸——那篇給安裝腳本的骨架與套件清單，這篇給它加上失敗時的診斷能力，兩篇處理的是同一支腳本的兩個層面。</p>
<p>這是設計階段的決定，不是事後能補的。當一支沒有可觀測性的腳本在一台陌生機器上失敗，你沒辦法回到過去讓它記錄當時的狀態——資訊在失敗的瞬間就已經流失了。所以「失敗可診斷」要跟功能一起設計進去，把它當成 bootstrap 的基本屬性，而不是出事之後才想加的補丁。</p>
<h2 id="回到系列">回到系列</h2>
<p>這幾篇合起來，是把一台機器從「空的」帶到「能接收 dotfile、且部署過程可診斷」的完整地基：<a href="../install-option-decisions/">安裝選項判讀</a> 處理 OS 怎麼裝、<a href="../minimal-install-verify/">工具驗證與補足</a> 處理裝完缺什麼、<a href="../ssh-keyless-bootstrap/">外部連入與無 key bootstrap</a> 處理怎麼連進去把 dotfile 弄進來，這一篇處理當部署失敗時怎麼快速看出原因。再往前一步，把這套地基用在無人值守的長任務上、讓機器在你離開後自己跑完工作，見 <a href="../unattended-remote-work/">讓機器跑無人值守的長任務</a>——無人盯著的任務尤其依賴這篇談的可觀測性。地基打好，後面 <a href="/blog/linux/dotfile/01-dotfile-management/" data-link-title="模組一：管理工具與目錄結構" data-link-desc="要把散落在家目錄的配置檔集中版控時，選 bare repo、stow 還是 chezmoi、目錄該怎麼組織">模組一到八</a> 的 dotfile 管理才有立足點。</p>
]]></content:encoded></item><item><title>模組八：同步、Bootstrap 與環境重建</title><link>https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/</guid><description>&lt;p>環境重建是 dotfile 管理的最終目的：拿到一台空白機器，能在可預期的時間內還原成你熟悉的工作環境。這件事有兩條根本不同的路線——「拍照」（VM 快照）和「重建指令」（dotfile + install script），選哪條決定了你之後所有的管理策略。&lt;/p>
&lt;h2 id="章節文章">章節文章&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>文章&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/snapshot-vs-rebuild/" data-link-title="拍照 vs 重建指令：環境重建的兩種思路" data-link-desc="猶豫該用 VM 快照還是 dotfile 重建來管理環境時回來讀">拍照 vs 重建指令：環境重建的兩種思路&lt;/a>&lt;/td>
 &lt;td>VM 快照和 dotfile 重建的本質差異、各自的守備範圍與場景判讀&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/bootstrap-script-packages/" data-link-title="Bootstrap Script 與套件清單管理" data-link-desc="寫 dotfile 的 install script、或整理「這台機器裝了什麼」的套件清單時回來讀">Bootstrap Script 與套件清單管理&lt;/a>&lt;/td>
 &lt;td>install script 的冪等性設計、OS 分流、Brewfile / packages.txt 套件清單管理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/sync-strategy-secret/" data-link-title="跨機器同步、Secret 管理與環境重建流程" data-link-desc="多台機器的 dotfile 怎麼同步、哪些東西不該進 repo 時回來讀">跨機器同步、Secret 管理與環境重建流程&lt;/a>&lt;/td>
 &lt;td>Git push/pull vs 自動同步、secret 三層級管理、從空白機器到完整工作環境的 end-to-end（範例用 Arch + Hyprland，macOS 同樣適用）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="跨分類引用">跨分類引用&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/01-dotfile-management/" data-link-title="模組一：管理工具與目錄結構" data-link-desc="要把散落在家目錄的配置檔集中版控時，選 bare repo、stow 還是 chezmoi、目錄該怎麼組織">模組一：管理工具與目錄結構&lt;/a>：stow / chezmoi 選型與跨平台三層模型&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/05-hyprland-config/" data-link-title="模組五：Hyprland 配置" data-link-desc="要在 Linux 上設定 Hyprland 平鋪式桌面時回來讀">模組五：Hyprland 配置&lt;/a>：環境重建流程裡 monitor 設定的硬體相關調整&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/09-team-environment/" data-link-title="模組九：從個人到團隊" data-link-desc="個人 dotfile 管理的思想要延伸到團隊開發環境標準化時回來讀 — devcontainer、nix、商業環境配置管理">模組九：從個人到團隊&lt;/a>：個人 bootstrap 的思想怎麼延伸到團隊 onboarding&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>環境重建是 dotfile 管理的最終目的：拿到一台空白機器，能在可預期的時間內還原成你熟悉的工作環境。這件事有兩條根本不同的路線——「拍照」（VM 快照）和「重建指令」（dotfile + install script），選哪條決定了你之後所有的管理策略。</p>
<h2 id="章節文章">章節文章</h2>
<table>
  <thead>
      <tr>
          <th>文章</th>
          <th>主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/linux/dotfile/08-sync-bootstrap/snapshot-vs-rebuild/" data-link-title="拍照 vs 重建指令：環境重建的兩種思路" data-link-desc="猶豫該用 VM 快照還是 dotfile 重建來管理環境時回來讀">拍照 vs 重建指令：環境重建的兩種思路</a></td>
          <td>VM 快照和 dotfile 重建的本質差異、各自的守備範圍與場景判讀</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/08-sync-bootstrap/bootstrap-script-packages/" data-link-title="Bootstrap Script 與套件清單管理" data-link-desc="寫 dotfile 的 install script、或整理「這台機器裝了什麼」的套件清單時回來讀">Bootstrap Script 與套件清單管理</a></td>
          <td>install script 的冪等性設計、OS 分流、Brewfile / packages.txt 套件清單管理</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/08-sync-bootstrap/sync-strategy-secret/" data-link-title="跨機器同步、Secret 管理與環境重建流程" data-link-desc="多台機器的 dotfile 怎麼同步、哪些東西不該進 repo 時回來讀">跨機器同步、Secret 管理與環境重建流程</a></td>
          <td>Git push/pull vs 自動同步、secret 三層級管理、從空白機器到完整工作環境的 end-to-end（範例用 Arch + Hyprland，macOS 同樣適用）</td>
      </tr>
  </tbody>
</table>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/linux/dotfile/01-dotfile-management/" data-link-title="模組一：管理工具與目錄結構" data-link-desc="要把散落在家目錄的配置檔集中版控時，選 bare repo、stow 還是 chezmoi、目錄該怎麼組織">模組一：管理工具與目錄結構</a>：stow / chezmoi 選型與跨平台三層模型</li>
<li>→ <a href="/blog/linux/dotfile/05-hyprland-config/" data-link-title="模組五：Hyprland 配置" data-link-desc="要在 Linux 上設定 Hyprland 平鋪式桌面時回來讀">模組五：Hyprland 配置</a>：環境重建流程裡 monitor 設定的硬體相關調整</li>
<li>→ <a href="/blog/linux/dotfile/09-team-environment/" data-link-title="模組九：從個人到團隊" data-link-desc="個人 dotfile 管理的思想要延伸到團隊開發環境標準化時回來讀 — devcontainer、nix、商業環境配置管理">模組九：從個人到團隊</a>：個人 bootstrap 的思想怎麼延伸到團隊 onboarding</li>
</ul>
]]></content:encoded></item><item><title>平台與發行版差異的判讀地圖</title><link>https://tarrragon.github.io/blog/linux/install/platform-divergence-map/</link><pubDate>Thu, 02 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/install/platform-divergence-map/</guid><description>&lt;p>同一個工作環境要在多台機器上復現時，差異集中在四個層次：套件管理器、套件名稱、套件存在性、版本節奏。這四層決定了 bootstrap 腳本哪些部分能共用、哪些必須按平台獨立維護，也決定了除錯時要先確認自己站在哪個平台上——很多「工具行為不對」的問題，根因是把 A 平台的經驗直接套到 B 平台。&lt;/p>
&lt;h2 id="差異的四個層次">差異的四個層次&lt;/h2>
&lt;h3 id="套件管理器每個平台各有原生解">套件管理器：每個平台各有原生解&lt;/h3>
&lt;p>macOS 用 Homebrew、Arch 用 pacman、Debian/Ubuntu 用 apt、Fedora 用 dnf。安裝指令、確認旗標、資料庫同步模型都不同，其中兩個差異會直接咬到自動化腳本：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>非互動旗標不對稱&lt;/strong>：apt 的慣例是 &lt;code>-y&lt;/code>，pacman 是 &lt;code>--noconfirm&lt;/code>。腳本只寫了其中一邊，換平台就會卡在確認提示——非 TTY 環境下（SSH 一行式、CI、無人值守）沒人回答 &lt;code>[Y/n]&lt;/code>，pacman 直接以錯誤結束。&lt;/li>
&lt;li>&lt;strong>資料庫同步模型不同&lt;/strong>：Arch 是 rolling release 且鏡像不保留舊版檔案，裝機當下的套件資料庫幾天內就會指向已被輪替掉的檔名，安裝時收到 404（&lt;code>failed retrieving file&lt;/code>）。修法是安裝前先 &lt;code>pacman -Syu&lt;/code> 同步資料庫並全系統升級——只 &lt;code>-Sy&lt;/code> 不 &lt;code>-u&lt;/code> 會造成 partial upgrade（新資料庫裝新套件、舊系統缺新依賴）。Debian stable 的套件庫凍結、沒有這個時序問題，但代價是版本舊。&lt;/li>
&lt;/ul>
&lt;h3 id="套件名稱同一個工具各發行版各叫各的">套件名稱：同一個工具、各發行版各叫各的&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>工具&lt;/th>
 &lt;th>Arch&lt;/th>
 &lt;th>Debian/Ubuntu&lt;/th>
 &lt;th>Fedora&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>fd&lt;/td>
 &lt;td>&lt;code>fd&lt;/code>&lt;/td>
 &lt;td>&lt;code>fd-find&lt;/code>（執行檔叫 &lt;code>fdfind&lt;/code>）&lt;/td>
 &lt;td>&lt;code>fd-find&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>bat&lt;/td>
 &lt;td>&lt;code>bat&lt;/code>&lt;/td>
 &lt;td>&lt;code>bat&lt;/code>（執行檔叫 &lt;code>batcat&lt;/code>）&lt;/td>
 &lt;td>&lt;code>bat&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>gh&lt;/td>
 &lt;td>&lt;code>github-cli&lt;/code>&lt;/td>
 &lt;td>&lt;code>gh&lt;/code>&lt;/td>
 &lt;td>&lt;code>gh&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CJK 字型&lt;/td>
 &lt;td>&lt;code>noto-fonts-cjk&lt;/code>&lt;/td>
 &lt;td>&lt;code>fonts-noto-cjk&lt;/code>&lt;/td>
 &lt;td>&lt;code>google-noto-sans-cjk-fonts&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Meslo Nerd Font&lt;/td>
 &lt;td>&lt;code>ttf-meslo-nerd&lt;/code>&lt;/td>
 &lt;td>未打包（手動裝）&lt;/td>
 &lt;td>未打包&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Debian 的重命名還會連執行檔一起改（&lt;code>fdfind&lt;/code>、&lt;code>batcat&lt;/code>），所以連 shell alias 與腳本內的指令呼叫都要跟著分歧。維護跨發行版清單的可靠做法是逐台實測建立——憑印象抄一份對照表，漂移只是時間問題。&lt;/p>
&lt;h3 id="套件存在性有些概念只存在於特定平台">套件存在性：有些概念只存在於特定平台&lt;/h3>
&lt;p>Hyprland 在 Arch 官方 repo、Fedora 要 COPR、Debian stable 沒有；Quickshell 只有 Arch 打包。反過來，macOS 的 cask app（GUI 應用程式）概念在 Linux 對應的是各桌面環境自己的生態。這層差異沒有翻譯的空間——桌面層的清單是平台專屬的維護對象。&lt;/p>
&lt;p>存在性差異還有一個容易漏看的軸：&lt;strong>CPU 架構&lt;/strong>。發行版 repo 有這個工具、不代表它在你的架構上存在——尤其是專有軟體的二進位發行。實測案例：Arch aarch64（ALARM）的 repo 有 &lt;code>spotify-launcher&lt;/code>（工具本身有 aarch64 建置），但它要下載的 Spotify 官方 client 只發 x86_64/i386 deb，實跑直接回報 &lt;code>There are no packages for your cpu's architecture (cpu=&amp;quot;aarch64&amp;quot;, supported=[&amp;quot;amd64&amp;quot;, &amp;quot;i386&amp;quot;])&lt;/code>。這類失敗的判讀重點是分清「工具沒打包」跟「工具打包了、它依賴的專有 blob 沒有這個架構」——前者可能有 AUR / 第三方 repo 補、後者只能找替代路徑（Spotify 的替代是 Web Player + 從 ChromeOS 鏡像抽出的 arm64 Widevine CDM）。DRM、GPU driver、印表機 driver 這類含專有二進位的軟體，在非 x86_64 架構上都要先查架構支援再排進安裝清單。&lt;/p>
&lt;h3 id="版本節奏rolling-與-stable-的行為差">版本節奏：rolling 與 stable 的行為差&lt;/h3>
&lt;p>Arch rolling 永遠最新，Debian stable 的同名工具可能舊兩年。版本差會讓 config 語法對不上（新版工具的設定選項在舊版不存在）、也會讓「照著文件做卻失敗」——文件寫的是新版行為。除錯時看到「同一份 config 在 A 機器能跑、B 機器報錯」，先比對兩邊的工具版本再懷疑 config 本身。&lt;/p>
&lt;h2 id="除錯前先定平台">除錯前先定平台&lt;/h2>
&lt;p>跨平台差異對除錯的意義：&lt;strong>判讀工具與修法都是平台相依的，先確認自己站在哪，再開始查。&lt;/strong> 三條指令建立座標：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">cat /etc/os-release &lt;span class="c1"># 發行版與版本（Linux）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">uname -m &lt;span class="c1"># CPU 架構：x86_64 / aarch64（套件生態差很多）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nb">command&lt;/span> -v pacman apt-get dnf brew &lt;span class="c1"># 哪個套件管理器在場&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>架構那條容易被忽略：aarch64（ARM）的套件生態比 x86_64 小——Homebrew on Linux 對 aarch64 沒有預編譯 bottle、AUR 部分套件不支援 ARM。在 ARM 機器上照 x86 的教學走，會在意想不到的地方碰壁。&lt;/p></description><content:encoded><![CDATA[<p>同一個工作環境要在多台機器上復現時，差異集中在四個層次：套件管理器、套件名稱、套件存在性、版本節奏。這四層決定了 bootstrap 腳本哪些部分能共用、哪些必須按平台獨立維護，也決定了除錯時要先確認自己站在哪個平台上——很多「工具行為不對」的問題，根因是把 A 平台的經驗直接套到 B 平台。</p>
<h2 id="差異的四個層次">差異的四個層次</h2>
<h3 id="套件管理器每個平台各有原生解">套件管理器：每個平台各有原生解</h3>
<p>macOS 用 Homebrew、Arch 用 pacman、Debian/Ubuntu 用 apt、Fedora 用 dnf。安裝指令、確認旗標、資料庫同步模型都不同，其中兩個差異會直接咬到自動化腳本：</p>
<ul>
<li><strong>非互動旗標不對稱</strong>：apt 的慣例是 <code>-y</code>，pacman 是 <code>--noconfirm</code>。腳本只寫了其中一邊，換平台就會卡在確認提示——非 TTY 環境下（SSH 一行式、CI、無人值守）沒人回答 <code>[Y/n]</code>，pacman 直接以錯誤結束。</li>
<li><strong>資料庫同步模型不同</strong>：Arch 是 rolling release 且鏡像不保留舊版檔案，裝機當下的套件資料庫幾天內就會指向已被輪替掉的檔名，安裝時收到 404（<code>failed retrieving file</code>）。修法是安裝前先 <code>pacman -Syu</code> 同步資料庫並全系統升級——只 <code>-Sy</code> 不 <code>-u</code> 會造成 partial upgrade（新資料庫裝新套件、舊系統缺新依賴）。Debian stable 的套件庫凍結、沒有這個時序問題，但代價是版本舊。</li>
</ul>
<h3 id="套件名稱同一個工具各發行版各叫各的">套件名稱：同一個工具、各發行版各叫各的</h3>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>Arch</th>
          <th>Debian/Ubuntu</th>
          <th>Fedora</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>fd</td>
          <td><code>fd</code></td>
          <td><code>fd-find</code>（執行檔叫 <code>fdfind</code>）</td>
          <td><code>fd-find</code></td>
      </tr>
      <tr>
          <td>bat</td>
          <td><code>bat</code></td>
          <td><code>bat</code>（執行檔叫 <code>batcat</code>）</td>
          <td><code>bat</code></td>
      </tr>
      <tr>
          <td>gh</td>
          <td><code>github-cli</code></td>
          <td><code>gh</code></td>
          <td><code>gh</code></td>
      </tr>
      <tr>
          <td>CJK 字型</td>
          <td><code>noto-fonts-cjk</code></td>
          <td><code>fonts-noto-cjk</code></td>
          <td><code>google-noto-sans-cjk-fonts</code></td>
      </tr>
      <tr>
          <td>Meslo Nerd Font</td>
          <td><code>ttf-meslo-nerd</code></td>
          <td>未打包（手動裝）</td>
          <td>未打包</td>
      </tr>
  </tbody>
</table>
<p>Debian 的重命名還會連執行檔一起改（<code>fdfind</code>、<code>batcat</code>），所以連 shell alias 與腳本內的指令呼叫都要跟著分歧。維護跨發行版清單的可靠做法是逐台實測建立——憑印象抄一份對照表，漂移只是時間問題。</p>
<h3 id="套件存在性有些概念只存在於特定平台">套件存在性：有些概念只存在於特定平台</h3>
<p>Hyprland 在 Arch 官方 repo、Fedora 要 COPR、Debian stable 沒有；Quickshell 只有 Arch 打包。反過來，macOS 的 cask app（GUI 應用程式）概念在 Linux 對應的是各桌面環境自己的生態。這層差異沒有翻譯的空間——桌面層的清單是平台專屬的維護對象。</p>
<p>存在性差異還有一個容易漏看的軸：<strong>CPU 架構</strong>。發行版 repo 有這個工具、不代表它在你的架構上存在——尤其是專有軟體的二進位發行。實測案例：Arch aarch64（ALARM）的 repo 有 <code>spotify-launcher</code>（工具本身有 aarch64 建置），但它要下載的 Spotify 官方 client 只發 x86_64/i386 deb，實跑直接回報 <code>There are no packages for your cpu's architecture (cpu=&quot;aarch64&quot;, supported=[&quot;amd64&quot;, &quot;i386&quot;])</code>。這類失敗的判讀重點是分清「工具沒打包」跟「工具打包了、它依賴的專有 blob 沒有這個架構」——前者可能有 AUR / 第三方 repo 補、後者只能找替代路徑（Spotify 的替代是 Web Player + 從 ChromeOS 鏡像抽出的 arm64 Widevine CDM）。DRM、GPU driver、印表機 driver 這類含專有二進位的軟體，在非 x86_64 架構上都要先查架構支援再排進安裝清單。</p>
<h3 id="版本節奏rolling-與-stable-的行為差">版本節奏：rolling 與 stable 的行為差</h3>
<p>Arch rolling 永遠最新，Debian stable 的同名工具可能舊兩年。版本差會讓 config 語法對不上（新版工具的設定選項在舊版不存在）、也會讓「照著文件做卻失敗」——文件寫的是新版行為。除錯時看到「同一份 config 在 A 機器能跑、B 機器報錯」，先比對兩邊的工具版本再懷疑 config 本身。</p>
<h2 id="除錯前先定平台">除錯前先定平台</h2>
<p>跨平台差異對除錯的意義：<strong>判讀工具與修法都是平台相依的，先確認自己站在哪，再開始查。</strong> 三條指令建立座標：</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">cat /etc/os-release        <span class="c1"># 發行版與版本（Linux）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">uname -m                   <span class="c1"># CPU 架構：x86_64 / aarch64（套件生態差很多）</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nb">command</span> -v pacman apt-get dnf brew   <span class="c1"># 哪個套件管理器在場</span></span></span></code></pre></div><p>架構那條容易被忽略：aarch64（ARM）的套件生態比 x86_64 小——Homebrew on Linux 對 aarch64 沒有預編譯 bottle、AUR 部分套件不支援 ARM。在 ARM 機器上照 x86 的教學走，會在意想不到的地方碰壁。</p>
<h2 id="bootstrap-的分歧設計判準">Bootstrap 的分歧設計判準</h2>
<p>把差異收進腳本架構的三條判準，決定每段邏輯住在哪：</p>
<ol>
<li><strong>安裝手段跨平台一致</strong>（git clone、curl installer、stow 部署）→ 進共通層，一份邏輯全平台用</li>
<li><strong>只是套件名或套件管理器不同</strong> → 各平台一份安裝腳本 + 一份套件清單，獨立維護、分歧不寫進共通層的 if/else</li>
<li><strong>概念只存在於某平台</strong>（Hyprland、cask）→ 只出現在該平台清單的桌面層</li>
</ol>
<p>這個切法的維護成本結構：共通層改一次全平台生效；平台層只在你真的用那個平台時才付維護成本。沒有實測機器的發行版不預先建清單——那種清單沒有實測支撐、注定漂移。</p>
<h2 id="統一層的誘惑與代價">統一層的誘惑與代價</h2>
<p>「用一個跨平台套件管理器統一所有機器」聽起來能消掉整個分歧層，實際的適用邊界很窄。Homebrew 支援 Linux，但它在 Arch 上會建一套與 pacman 平行的套件世界（獨立 prefix、重複的函式庫、PATH 互搶），而且對 aarch64 Linux 沒有 bottle、全部從原始碼編譯。它真正的適用場景是「發行版套件太舊」（如 Ubuntu LTS 上要新版工具）或「沒有 root 權限」。Nix 能做到真正的跨平台一致，代價是整套心智模型重學。判準是：分歧層的維護成本（每個發行版一份清單）低於統一層的引入成本時，保持原生套件管理器 + 分平台清單。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Bootstrap 腳本本身的設計（log 落地、錯誤定位）見<a href="/blog/linux/install/observable-bootstrap/" data-link-title="可除錯的 bootstrap：把可觀測性內建進安裝腳本" data-link-desc="安裝腳本中途失敗卻只能對著終端機捲動瞎找原因、想在 bootstrap 設計階段就讓失敗可定位時回來讀">可除錯的 bootstrap</a></li>
<li>最小系統缺什麼、怎麼驗證見<a href="/blog/linux/install/minimal-install-verify/" data-link-title="最小安裝後的工具驗證與補足" data-link-desc="最小化安裝的 Linux 裝完發現連 sudo 或 which 都沒有、bootstrap 腳本第一行就炸、需要先確認系統缺哪些必要工具再補時回來讀">最小安裝後的工具驗證與補足</a></li>
<li>出問題時的判讀紀律見 <a href="/blog/linux/debug/" data-link-title="Linux 除錯與診斷" data-link-desc="遠端或本地除錯 Linux 時，一個現象看起來像 A 卻可能是 B，想建立一套先讀權威狀態再下判斷的紀律、按症狀分流到對的檢查與工具時回來讀">Linux 除錯與診斷</a></li>
<li>dotfile repo 怎麼同時服務 macOS 與 Linux 見<a href="/blog/linux/dotfile/01-dotfile-management/cross-platform-one-repo/" data-link-title="跨平台共用一個 Repo" data-link-desc="macOS 跟 Linux 要共用同一個 dotfile repo、不想維護兩份時回來讀">一個 repo 管理跨平台環境</a></li>
</ul>
]]></content:encoded></item></channel></rss>