<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jimmy</title>
    <description>The latest articles on DEV Community by Jimmy (@jimmy_3c703d).</description>
    <link>https://dev.to/jimmy_3c703d</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4178874%2F6970ff37-9525-47b9-b4be-bde759b864a9.jpg</url>
      <title>DEV Community: Jimmy</title>
      <link>https://dev.to/jimmy_3c703d</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jimmy_3c703d"/>
    <language>en</language>
    <item>
      <title>ArgoCD App of Apps 實作：一個 root Application 管理叢集裡所有的 App</title>
      <dc:creator>Jimmy</dc:creator>
      <pubDate>Fri, 16 Oct 2026 04:00:12 +0000</pubDate>
      <link>https://dev.to/jimmy_3c703d/argocd-app-of-apps-shi-zuo-ge-root-application-guan-li-cong-ji-li-suo-you-de-app-dcg</link>
      <guid>https://dev.to/jimmy_3c703d/argocd-app-of-apps-shi-zuo-ge-root-application-guan-li-cong-ji-li-suo-you-de-app-dcg</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;一句話結論：把 ArgoCD 裝好之後，只手動 apply &lt;strong&gt;一個&lt;/strong&gt; root Application，讓它掃描 Git 裡的某個目錄；之後要新增、修改、移除任何 App，都只要改 Git，不用再碰 &lt;code&gt;kubectl apply&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  遇到的問題
&lt;/h2&gt;

&lt;p&gt;GitOps 的第一步，通常是為每個服務建立一個 ArgoCD Application。&lt;/p&gt;

&lt;p&gt;服務少的時候沒問題。服務一多，就變成每新增一個，都要手動 &lt;code&gt;kubectl apply&lt;/code&gt; 一份 Application 的 YAML。&lt;/p&gt;

&lt;p&gt;這等於在 GitOps 裡留了一個沒被 GitOps 管的缺口：Application 本身不在 Git 的管理範圍內。&lt;/p&gt;

&lt;p&gt;解法是 &lt;strong&gt;App of Apps&lt;/strong&gt;：用一個 Application，去管理其他所有 Application。&lt;/p&gt;

&lt;p&gt;這篇用最小的例子，從安裝 ArgoCD 開始，走完整個流程。&lt;/p&gt;

&lt;h2&gt;
  
  
  手把手步驟
&lt;/h2&gt;

&lt;h3&gt;
  
  
  步驟一：用 Helm 裝 ArgoCD
&lt;/h3&gt;

&lt;p&gt;前提是手邊已經有一個 Kubernetes 叢集，minikube、kind 或雲端的都可以。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm repo add argo https://argoproj.github.io/argo-helm
helm repo update

helm upgrade &lt;span class="nt"&gt;--install&lt;/span&gt; argocd argo/argo-cd &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; argocd &lt;span class="nt"&gt;--create-namespace&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--wait&lt;/span&gt; &lt;span class="nt"&gt;--timeout&lt;/span&gt; 15m
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;確認 ArgoCD 起來了：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; argocd get pods
kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; argocd rollout status deploy/argocd-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;官方的 Getting Started 是用 &lt;code&gt;kubectl apply&lt;/code&gt; 安裝 manifest。這裡改用 Helm，之後想調整參數（資源限制、Ingress 等）比較方便，也方便升級。&lt;/p&gt;

&lt;h3&gt;
  
  
  步驟二：登入 UI 看一下
&lt;/h3&gt;

&lt;p&gt;最簡單的方式是 port-forward：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl port-forward svc/argocd-server &lt;span class="nt"&gt;-n&lt;/span&gt; argocd 8080:443
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;然後用瀏覽器打開 &lt;code&gt;https://localhost:8080&lt;/code&gt;（憑證是自簽的，瀏覽器會警告）。&lt;/p&gt;

&lt;p&gt;帳號是 &lt;code&gt;admin&lt;/code&gt;，初始密碼這樣取：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;argocd admin initial-password &lt;span class="nt"&gt;-n&lt;/span&gt; argocd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;沒有裝 &lt;code&gt;argocd&lt;/code&gt; CLI 的話，也可以直接讀 Secret：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; argocd get secret argocd-initial-admin-secret &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.data.password}'&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;官方文件提醒：登入並改完密碼之後，要把 &lt;code&gt;argocd-initial-admin-secret&lt;/code&gt; 刪掉。這個 Secret 只是初始用，留著沒有意義。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  步驟三：準備 Git repo 的結構
&lt;/h3&gt;

&lt;p&gt;建一個 Git repo，結構長這樣：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gitops/
├── bootstrap/
│   └── root-app.yaml     # 手動 apply 一次
└── apps/                  # root 會掃描這個目錄
    └── guestbook.yaml     # 一個子 Application
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;兩個目錄要分開：&lt;code&gt;bootstrap/&lt;/code&gt; 放只需要手動套用一次的東西，&lt;code&gt;apps/&lt;/code&gt; 放 root 要持續掃描的東西。&lt;/p&gt;

&lt;p&gt;不要把 root 自己放進 &lt;code&gt;apps/&lt;/code&gt;，否則會變成 root 在管理自己，行為難以預期，也很難理解。&lt;/p&gt;

&lt;h3&gt;
  
  
  步驟四：寫一個子 Application
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;apps/guestbook.yaml&lt;/code&gt;，使用 ArgoCD 官方的範例 repo，大家都能拿到：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argoproj.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Application&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;guestbook&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd&lt;/span&gt;
  &lt;span class="na"&gt;finalizers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;resources-finalizer.argocd.argoproj.io&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;repoURL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/argoproj/argocd-example-apps.git&lt;/span&gt;
    &lt;span class="na"&gt;targetRevision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HEAD&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;guestbook&lt;/span&gt;
  &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://kubernetes.default.svc&lt;/span&gt;
    &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;guestbook&lt;/span&gt;
  &lt;span class="na"&gt;syncPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;automated&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;prune&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;selfHeal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;syncOptions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;CreateNamespace=true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;幾個欄位值得說明一下。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;namespace&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Application 這個物件本身預設要放在 &lt;code&gt;argocd&lt;/code&gt; namespace。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;spec.destination.namespace&lt;/code&gt; 才是這個 App 的資源要部署到哪裡。兩個很容易搞混。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;finalizer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;resources-finalizer.argocd.argoproj.io&lt;/code&gt; 的作用是：刪除這個 Application 時，連它部署出來的資源也一起刪掉（預設是前景的 cascading deletion）。&lt;/p&gt;

&lt;p&gt;沒有加的話，刪掉 Application 只會移除 ArgoCD 的紀錄，資源還留在叢集裡。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;syncPolicy&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;automated&lt;/code&gt; 打開自動同步。&lt;code&gt;prune: true&lt;/code&gt; 代表 Git 裡移除的資源，叢集裡也會被刪掉。&lt;code&gt;selfHeal: true&lt;/code&gt; 代表有人手動改了叢集內容，ArgoCD 會把它改回 Git 的樣子。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CreateNamespace=true&lt;/code&gt; 讓 ArgoCD 在目標 namespace 不存在時自動建立。&lt;/p&gt;

&lt;h3&gt;
  
  
  步驟五：寫 root Application
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;bootstrap/root-app.yaml&lt;/code&gt;：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argoproj.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Application&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;root&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd&lt;/span&gt;
  &lt;span class="na"&gt;finalizers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;resources-finalizer.argocd.argoproj.io&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;repoURL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/&amp;lt;你的帳號&amp;gt;/gitops.git&lt;/span&gt;
    &lt;span class="na"&gt;targetRevision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps&lt;/span&gt;
    &lt;span class="na"&gt;directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;recurse&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://kubernetes.default.svc&lt;/span&gt;
    &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd&lt;/span&gt;
  &lt;span class="na"&gt;syncPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;automated&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;prune&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;selfHeal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;這份 YAML 跟子 Application 長得幾乎一樣，差別有兩個。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;path: apps&lt;/code&gt; 加 &lt;code&gt;directory.recurse: true&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;path&lt;/code&gt; 指向要掃描的目錄。ArgoCD 預設只讀該目錄第一層的檔案，要往下一層一層讀，要設 &lt;code&gt;recurse: true&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;所以之後可以在 &lt;code&gt;apps/&lt;/code&gt; 底下用子資料夾整理，root 一樣讀得到。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;destination 的 namespace 是 &lt;code&gt;argocd&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;因為 root 部署出來的東西就是「Application 這種物件」，而它們預設要在 &lt;code&gt;argocd&lt;/code&gt; namespace。&lt;/p&gt;

&lt;p&gt;把這個 repo 推到 Git，repoURL 換成自己的。如果是私有 repo，要先在 ArgoCD 設定 repo 的存取憑證。&lt;/p&gt;

&lt;h3&gt;
  
  
  步驟六：套用 root，看它長出子 App
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; bootstrap/root-app.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;這是&lt;strong&gt;唯一&lt;/strong&gt;一次要手動 apply 的東西。&lt;/p&gt;

&lt;p&gt;看一下結果：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get applications &lt;span class="nt"&gt;-n&lt;/span&gt; argocd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;預期會看到 &lt;code&gt;root&lt;/code&gt; 與 &lt;code&gt;guestbook&lt;/code&gt; 兩個，狀態慢慢變成 &lt;code&gt;Synced&lt;/code&gt; 與 &lt;code&gt;Healthy&lt;/code&gt;：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME        SYNC STATUS   HEALTH STATUS
guestbook   Synced        Healthy
root        Synced        Healthy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ArgoCD 預設大約每 3 分鐘輪詢一次 Git，所以 push 之後可能要等一下。急的話，在 UI 上按 Refresh 就會立刻檢查。&lt;/p&gt;

&lt;h3&gt;
  
  
  步驟七：新增、移除 App，只改 Git
&lt;/h3&gt;

&lt;p&gt;要新增一個 App，在 &lt;code&gt;apps/&lt;/code&gt; 加一個 Application 的 YAML，commit、push。&lt;/p&gt;

&lt;p&gt;等下一輪輪詢（或按 Refresh），root 就會偵測到新檔案，建立對應的 Application，新的 App 再各自同步。&lt;/p&gt;

&lt;p&gt;要移除，把檔案刪掉再 push。因為 root 有 &lt;code&gt;prune: true&lt;/code&gt;，那個 Application 會被刪除，加上子 Application 的 finalizer，它部署的資源也會一起清掉。&lt;/p&gt;

&lt;p&gt;全程不需要再對叢集下 &lt;code&gt;kubectl apply&lt;/code&gt;。&lt;/p&gt;

&lt;h2&gt;
  
  
  踩到的坑
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. 刪掉 root，會連帶刪光所有子 App 和它們的資源&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;root 有加 finalizer，所以刪除它會級聯往下刪：先刪子 Application，子 Application 因為也有 finalizer，再刪它們部署的資源。&lt;/p&gt;

&lt;p&gt;換句話說，一個 &lt;code&gt;kubectl delete application root&lt;/code&gt;，就能把整個叢集的服務清空。&lt;/p&gt;

&lt;p&gt;官方的 Cluster Bootstrapping 文件有專門一節在講這件事。&lt;/p&gt;

&lt;p&gt;如果不想要這種風險，可以考慮不給 root 加 finalizer，或是關掉 root 的 &lt;code&gt;prune&lt;/code&gt;。取捨是：以後刪除 root 或移除子 Application 時，要自己處理殘留的資源。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. 子 Application 本身的設定，是由 root 負責同步的&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;這個坑很隱蔽。假設改了某個子 Application 的設定（例如 plugin 的環境變數、&lt;code&gt;syncOptions&lt;/code&gt;），push 之後，如果 root 還沒同步這個改動，子 Application 會繼續用舊的設定渲染，狀態還顯示 &lt;code&gt;Synced&lt;/code&gt;，但新開的資源就是沒有出現。&lt;/p&gt;

&lt;p&gt;我就是這樣被騙過：看起來一切正常，卻怎麼等都沒有東西。&lt;/p&gt;

&lt;p&gt;處理方式是：先確認 root 已經是 &lt;code&gt;Synced&lt;/code&gt;，再對子 Application 做一次 hard refresh：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; argocd annotate application &amp;lt;名稱&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  argocd.argoproj.io/refresh&lt;span class="o"&gt;=&lt;/span&gt;hard &lt;span class="nt"&gt;--overwrite&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;hard&lt;/code&gt; 的意思是連 manifest 快取都清掉再重新評估。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. &lt;code&gt;recurse: true&lt;/code&gt; 會讀到目錄裡所有的 YAML&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;root 掃描的目錄裡，只應該放 Application 的 manifest。&lt;/p&gt;

&lt;p&gt;如果裡面混了其他 YAML（文件、範例、值檔），ArgoCD 會把它們都當成要部署的資源，可能直接報錯，或是意外建立不該存在的東西。&lt;/p&gt;

&lt;p&gt;把「手動套用一次的東西」和「持續掃描的東西」分在不同目錄，就是為了避免這個問題。&lt;/p&gt;

&lt;h2&gt;
  
  
  為什麼這樣選
&lt;/h2&gt;

&lt;p&gt;官方的 Cluster Bootstrapping 文件，推薦的其實是 &lt;strong&gt;ApplicationSet&lt;/strong&gt;，App of Apps 被列為替代方案。&lt;/p&gt;

&lt;p&gt;ApplicationSet 適合「很多個長得很像的 App」或「要部署到很多個叢集」，用 generator 一次產生一堆 Application，不用寫一堆 YAML。&lt;/p&gt;

&lt;p&gt;我選 App of Apps 的原因很單純：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;一個 App 一個檔案，看檔案就知道它的設定，容易讀。&lt;/li&gt;
&lt;li&gt;每個 App 的設定可以各自不同（有的自動同步，有的手動）。&lt;/li&gt;
&lt;li&gt;概念簡單，不需要先學 generator 和模板語法。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;單一叢集、App 數量不多、每個差異不小的情況，這樣就夠了。要管很多叢集或很多相似的 App，可以直接看 ApplicationSet。&lt;/p&gt;

&lt;p&gt;另外有一個官方的安全警告值得記住：&lt;strong&gt;App of Apps 是管理員專用的工具&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;能在任意 Project 底下建立 Application，等於是管理員等級的權限。所以只有管理員能對 root 所指向的 repo 推送，改動也應該經過審核。&lt;/p&gt;

&lt;h2&gt;
  
  
  延伸閱讀
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://argo-cd.readthedocs.io/en/stable/operator-manual/cluster-bootstrapping/" rel="noopener noreferrer"&gt;Argo CD：Cluster Bootstrapping&lt;/a&gt;：App of Apps 的官方說明、Helm 範例、級聯刪除，以及 ApplicationSet 的推薦做法。&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://argo-cd.readthedocs.io/en/stable/user-guide/auto_sync/" rel="noopener noreferrer"&gt;Argo CD：Automated Sync Policy&lt;/a&gt;：&lt;code&gt;prune&lt;/code&gt;、&lt;code&gt;selfHeal&lt;/code&gt;，以及輪詢間隔（預設 120 秒加最多 60 秒的抖動）。&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://argo-cd.readthedocs.io/en/stable/user-guide/app_deletion/" rel="noopener noreferrer"&gt;Argo CD：App Deletion&lt;/a&gt;：&lt;code&gt;resources-finalizer.argocd.argoproj.io&lt;/code&gt; 與級聯刪除的行為。&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://argo-cd.readthedocs.io/en/stable/user-guide/directory/" rel="noopener noreferrer"&gt;Argo CD：Directory Application Source&lt;/a&gt;：&lt;code&gt;directory.recurse&lt;/code&gt; 的用法。&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://argo-cd.readthedocs.io/en/stable/getting_started/" rel="noopener noreferrer"&gt;Argo CD：Getting Started&lt;/a&gt;：初始管理員密碼、port-forward 與登入。&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://argo-cd.readthedocs.io/en/stable/user-guide/annotations-and-labels/" rel="noopener noreferrer"&gt;Argo CD：Annotations and Labels&lt;/a&gt;：&lt;code&gt;argocd.argoproj.io/refresh&lt;/code&gt; 的 &lt;code&gt;normal&lt;/code&gt; 與 &lt;code&gt;hard&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/argoproj/argocd-example-apps" rel="noopener noreferrer"&gt;argocd-example-apps&lt;/a&gt;：官方範例 repo，裡面的 &lt;code&gt;guestbook&lt;/code&gt; 就是本篇用的，&lt;code&gt;apps&lt;/code&gt; 目錄是官方的 App of Apps 範例。&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/argoproj/argo-helm/tree/main/charts/argo-cd" rel="noopener noreferrer"&gt;argo-helm：argo-cd chart&lt;/a&gt;：用 Helm 安裝與調整參數。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;這篇用的是 ArgoCD 的 &lt;code&gt;stable&lt;/code&gt; 文件與 argo-helm 的 &lt;code&gt;argo-cd&lt;/code&gt; chart。版本不同的話，記得對一下欄位名稱。&lt;/p&gt;

</description>
      <category>argocd</category>
      <category>gitops</category>
      <category>kubernetes</category>
      <category>devops</category>
    </item>
    <item>
      <title>在單機用 k3d 建一個 3 節點 Kubernetes 叢集：從裝 Docker 到資料不會消失的儲存設定</title>
      <dc:creator>Jimmy</dc:creator>
      <pubDate>Thu, 15 Oct 2026 08:12:15 +0000</pubDate>
      <link>https://dev.to/jimmy_3c703d/zai-dan-ji-yong-k3d-jian-ge-3-jie-dian-kubernetes-cong-ji-cong-zhuang-docker-dao-zi-liao-bu-hui-xiao-shi-de-chu-cun-she-ding-3bfo</link>
      <guid>https://dev.to/jimmy_3c703d/zai-dan-ji-yong-k3d-jian-ge-3-jie-dian-kubernetes-cong-ji-cong-zhuang-docker-dao-zi-liao-bu-hui-xiao-shi-de-chu-cun-she-ding-3bfo</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;一句話結論：用 k3d 可以在一台機器上用 Docker 容器模擬多節點的 Kubernetes，但要記得兩件事：&lt;strong&gt;PV 的資料要掛到主機上&lt;/strong&gt;，還有 &lt;strong&gt;k3s 本來就內建 local-path-provisioner，不要再裝一套&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  遇到的問題
&lt;/h2&gt;

&lt;p&gt;需求很單純：一個可以隨便玩、隨便砍、但資料不會跟著消失的 Kubernetes 環境。&lt;/p&gt;

&lt;p&gt;放在自己的機器上，不用每個月付雲端的錢。&lt;/p&gt;

&lt;p&gt;最省事的選擇其實是 minikube 或 kind。&lt;/p&gt;

&lt;p&gt;但我這次要的是&lt;strong&gt;多節點&lt;/strong&gt;，想試 Pod 排程、節點故障這些行為，而且要接近真實的 k3s。&lt;/p&gt;

&lt;p&gt;k3d 剛好符合：它把 k3s 包在 Docker 容器裡，一個容器就是一個節點，建一個 3 節點叢集大概一分鐘。&lt;/p&gt;

&lt;p&gt;這篇從一台乾淨的 Ubuntu 開始，走完整個建置過程，也會講到中間踩到的幾個坑。&lt;/p&gt;

&lt;h2&gt;
  
  
  手把手步驟
&lt;/h2&gt;

&lt;h3&gt;
  
  
  步驟一：裝 Docker
&lt;/h3&gt;

&lt;p&gt;k3d 的節點是 Docker 容器，所以 Docker 是第一個要裝的。&lt;/p&gt;

&lt;p&gt;用 Docker 官方的 apt 套件庫來裝（不是 Ubuntu 預設套件庫裡那個舊版）：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; ca-certificates curl
&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 &lt;span class="nt"&gt;-d&lt;/span&gt; /etc/apt/keyrings
&lt;span class="nb"&gt;sudo &lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://download.docker.com/linux/ubuntu/gpg &lt;span class="nt"&gt;-o&lt;/span&gt; /etc/apt/keyrings/docker.asc
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;a+r /etc/apt/keyrings/docker.asc

&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/docker.sources &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; /etc/os-release &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;UBUNTU_CODENAME&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;$VERSION_CODENAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
Components: stable
Architectures: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dpkg &lt;span class="nt"&gt;--print-architecture&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
Signed-By: /etc/apt/keyrings/docker.asc
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; docker-ce docker-ce-cli containerd.io &lt;span class="se"&gt;\&lt;/span&gt;
  docker-buildx-plugin docker-compose-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;這是照 Docker 官方文件現在寫的方式：金鑰直接存成 &lt;code&gt;docker.asc&lt;/code&gt;，套件庫用 &lt;code&gt;.sources&lt;/code&gt; 檔。&lt;/p&gt;

&lt;p&gt;網路上很多教學還在用比較舊的 &lt;code&gt;gpg --dearmor&lt;/code&gt; 加 &lt;code&gt;docker.list&lt;/code&gt; 那一套，現在應該也還能用。&lt;/p&gt;

&lt;p&gt;新裝的話，建議跟著官方文件走。&lt;/p&gt;

&lt;p&gt;然後把自己加進 &lt;code&gt;docker&lt;/code&gt; 群組，這樣之後不用每次 &lt;code&gt;sudo&lt;/code&gt;：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;這一步做完要登出再登入一次&lt;/strong&gt;，群組才會生效。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;要注意：&lt;code&gt;docker&lt;/code&gt; 群組等於給了該使用者 root 等級的權限，Docker 官方文件有特別提醒。&lt;/p&gt;

&lt;p&gt;單人使用的機器可以接受這個取捨；共用的機器請改用官方介紹的 rootless 模式。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  步驟二：先設定 Docker 的 log 輪替
&lt;/h3&gt;

&lt;p&gt;這一步很容易被跳過，但建議在建叢集&lt;strong&gt;之前&lt;/strong&gt;就做。&lt;/p&gt;

&lt;p&gt;Docker 預設不會限制容器 log 的大小，叢集跑久了，每個節點容器的 log 會一直長大。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/docker/daemon.json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "5"
  }
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;意思是：每個容器的 log 最多 5 個檔案，每個最多 10 MB。&lt;/p&gt;

&lt;p&gt;如果你的機器上已經有 &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt;，不要直接覆蓋，用 &lt;code&gt;jq -s 'add'&lt;/code&gt; 把兩份合併。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;小提醒：Docker 重啟之後，已經存在的 k3d 叢集不一定會自己起來，要用 &lt;code&gt;k3d cluster start &amp;lt;名稱&amp;gt;&lt;/code&gt; 把它叫起來。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  步驟三：裝 kubectl、helm、k3d
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# kubectl&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSLo&lt;/span&gt; /tmp/kubectl &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://dl.k8s.io/release/&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; https://dl.k8s.io/release/stable.txt&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/bin/linux/amd64/kubectl"&lt;/span&gt;
&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 /tmp/kubectl /usr/local/bin/kubectl

&lt;span class="c"&gt;# helm&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

&lt;span class="c"&gt;# k3d（固定版本，避免哪天自動升級後行為變了）&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | &lt;span class="nv"&gt;TAG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;v5.8.3 bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;確認都裝好了：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nt"&gt;--version&lt;/span&gt;
kubectl version &lt;span class="nt"&gt;--client&lt;/span&gt;
helm version
k3d version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  步驟四：準備一個放 PV 資料的資料夾
&lt;/h3&gt;

&lt;p&gt;這一步先講結論，後面「踩到的坑」會解釋為什麼。&lt;/p&gt;

&lt;p&gt;先在主機上建一個資料夾，之後會把它掛進每個節點：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /srv/k3s-storage
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;:&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; /srv/k3s-storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;路徑請換成你自己的。如果你有第二顆比較大的硬碟，放在那顆上會更好。&lt;/p&gt;

&lt;h3&gt;
  
  
  步驟五：寫叢集設定檔
&lt;/h3&gt;

&lt;p&gt;k3d 支援用一份 YAML 描述整個叢集，比一長串命令列參數好維護，也方便放進 Git。新建 &lt;code&gt;cluster.yaml&lt;/code&gt;：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;k3d.io/v1alpha5&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Simple&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev&lt;/span&gt;
&lt;span class="na"&gt;servers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="na"&gt;agents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rancher/k3s:v1.31.5-k3s1&lt;/span&gt;
&lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;80:80&lt;/span&gt;
    &lt;span class="na"&gt;nodeFilters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;loadbalancer&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;443:443&lt;/span&gt;
    &lt;span class="na"&gt;nodeFilters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;loadbalancer&lt;/span&gt;
&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;volume&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/srv/k3s-storage:/srv/k3s-storage&lt;/span&gt;
    &lt;span class="na"&gt;nodeFilters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;
&lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;k3d&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;wait&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;300s"&lt;/span&gt;
  &lt;span class="na"&gt;kubeconfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;updateDefaultKubeconfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;switchCurrentContext&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;k3s&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;extraArgs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;arg&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;--disable=traefik&lt;/span&gt;
        &lt;span class="na"&gt;nodeFilters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;server:*&lt;/span&gt;
  &lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;serversMemory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6GB"&lt;/span&gt;
    &lt;span class="na"&gt;agentsMemory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6GB"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;逐段說明一下。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;節點數量&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;servers: 1&lt;/code&gt;、&lt;code&gt;agents: 2&lt;/code&gt;，也就是 1 個控制平面節點加 2 個工作節點，共 3 個。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;k3s 版本&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;image&lt;/code&gt; 固定在某個版本。不固定的話，k3d 會用它的預設版本，哪天升級後行為可能就不一樣了。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;對外埠&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ports&lt;/code&gt; 把主機的 80 和 443 轉進叢集，之後放 Ingress 才有入口。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;資料夾掛載&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;volumes&lt;/code&gt; 把剛剛那個資料夾掛進&lt;strong&gt;所有&lt;/strong&gt;節點。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Traefik&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;k3s 預設內建 Traefik 當 Ingress Controller。&lt;/p&gt;

&lt;p&gt;如果之後想自己選別的，就用 &lt;code&gt;--disable=traefik&lt;/code&gt; 先關掉。沒有特別偏好的話，直接用 Traefik 也完全可以。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;記憶體上限&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;runtime.serversMemory&lt;/code&gt; 和 &lt;code&gt;agentsMemory&lt;/code&gt; 用來限制每個節點容器的記憶體，這個很容易漏掉。&lt;/p&gt;

&lt;p&gt;沒設的話，&lt;strong&gt;每個節點容器都會回報「整台主機的記憶體」當自己的上限&lt;/strong&gt;，三個節點加起來就是三倍的假容量。&lt;/p&gt;

&lt;p&gt;排程器以為資源很多，實際一起吃的時候，主機自己會被擠到沒記憶體。&lt;/p&gt;

&lt;p&gt;請依機器大小設定，並且留一些給主機本身。&lt;/p&gt;

&lt;h3&gt;
  
  
  步驟六：建叢集
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;k3d cluster create &lt;span class="nt"&gt;--config&lt;/span&gt; cluster.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;大概一分鐘後完成。確認：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get nodes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;應該會看到 3 個節點，狀態都是 &lt;code&gt;Ready&lt;/code&gt;：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME               STATUS   ROLES                  AGE   VERSION
k3d-dev-server-0   Ready    control-plane,master   1m    v1.31.5+k3s1
k3d-dev-agent-0    Ready    &amp;lt;none&amp;gt;                 1m    v1.31.5+k3s1
k3d-dev-agent-1    Ready    &amp;lt;none&amp;gt;                 1m    v1.31.5+k3s1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  步驟七：把 local-path-provisioner 的資料指到主機上（重點）
&lt;/h3&gt;

&lt;p&gt;k3s 內建一個 &lt;code&gt;local-path-provisioner&lt;/code&gt;，PVC 建立後，它會在節點上建一個資料夾存資料。&lt;strong&gt;預設路徑是節點容器裡的 &lt;code&gt;/var/lib/rancher/k3s/storage&lt;/code&gt;&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;而這個路徑沒有掛到主機上。&lt;/p&gt;

&lt;p&gt;也就是說，只要節點容器被刪掉重建，PV 的資料就跟著消失了。這對一個「隨時會砍掉重練」的 k3d 叢集來說非常危險。&lt;/p&gt;

&lt;p&gt;修法是把它的設定改指向步驟四掛進去的那個資料夾：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;wait&lt;/span&gt; &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;available deployment/local-path-provisioner &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; kube-system &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;120s

kubectl patch configmap local-path-config &lt;span class="nt"&gt;-n&lt;/span&gt; kube-system &lt;span class="nt"&gt;--type&lt;/span&gt; merge &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'{"data":{"config.json":"{\"nodePathMap\":[{\"node\":\"DEFAULT_PATH_FOR_NON_LISTED_NODES\",\"paths\":[\"/srv/k3s-storage\"]}]}"}}'&lt;/span&gt;

kubectl rollout restart deployment local-path-provisioner &lt;span class="nt"&gt;-n&lt;/span&gt; kube-system
kubectl rollout status  deployment local-path-provisioner &lt;span class="nt"&gt;-n&lt;/span&gt; kube-system &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;60s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;再順手把它設成預設的 StorageClass（k3s 通常已經設好了，這行只是確保）：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl patch storageclass local-path &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;之後建立的 PVC，資料就會落在主機的 &lt;code&gt;/srv/k3s-storage&lt;/code&gt; 底下。你可以建一個測試 PVC 驗證一下，再 &lt;code&gt;ls /srv/k3s-storage&lt;/code&gt; 看看有沒有出現資料夾。&lt;/p&gt;

&lt;h4&gt;
  
  
  另一種做法：直接用 k3s 的官方旗標
&lt;/h4&gt;

&lt;p&gt;翻 k3s 文件才發現，官方其實有提供更直接的旗標：在 server 節點加上 &lt;code&gt;--default-local-storage-path&lt;/code&gt;，就能指定預設的儲存路徑。放進 &lt;code&gt;cluster.yaml&lt;/code&gt; 大概是這樣：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;k3s&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;extraArgs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;arg&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;--default-local-storage-path=/srv/k3s-storage&lt;/span&gt;
        &lt;span class="na"&gt;nodeFilters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;server:*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;本篇用的是上面 patch ConfigMap 的做法，這個旗標&lt;strong&gt;沒有實際跑過&lt;/strong&gt;，要用的話請先在臨時叢集上驗證一下。兩種擇一就好，不要同時用。&lt;/p&gt;

&lt;h3&gt;
  
  
  日常操作
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;k3d cluster stop  dev    &lt;span class="c"&gt;# 停止（資料保留）&lt;/span&gt;
k3d cluster start dev    &lt;span class="c"&gt;# 啟動&lt;/span&gt;
k3d cluster delete dev   &lt;span class="c"&gt;# 刪除（掛到主機的資料還在）&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  踩到的坑
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. 不要再另外裝一套 local-path-provisioner&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;我當初照著網路上的教學，apply 了官方社群版的 &lt;code&gt;local-path-storage.yaml&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;結果新裝的那套一直 &lt;code&gt;CrashLoopBackOff&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;後來才搞懂：k3s 本來就內建一套，兩邊的 ClusterRole 與 ClusterRoleBinding &lt;strong&gt;名稱一樣&lt;/strong&gt;，而且是 cluster 層級的資源。&lt;/p&gt;

&lt;p&gt;k3s 的 Addon controller 會一直把它們 reconcile 回內建的版本，新裝那套的 subject namespace 就被蓋掉，沒有權限。&lt;/p&gt;

&lt;p&gt;正確做法是上面的步驟七：直接改內建那套的設定，不要再裝新的。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. PV 資料預設會隨節點容器一起消失&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;這個我差點中招。&lt;/p&gt;

&lt;p&gt;k3d 的節點是容器，容器被刪除重建，容器內的檔案系統就沒了。&lt;/p&gt;

&lt;p&gt;一定要用 &lt;code&gt;volumes&lt;/code&gt; 把主機資料夾掛進去，再讓 provisioner 寫到那裡。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. 重啟 Docker 之後要手動把叢集叫起來&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;改了 &lt;code&gt;daemon.json&lt;/code&gt; 重啟 Docker，叢集的容器不一定會自動恢復運作。&lt;/p&gt;

&lt;p&gt;記得 &lt;code&gt;k3d cluster start&lt;/code&gt;。&lt;/p&gt;

&lt;h2&gt;
  
  
  為什麼這樣選
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;k3d 而不是 kind／minikube&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;要的是多節點加接近真實的 k3s，k3d 在這兩點上最順手。&lt;/p&gt;

&lt;p&gt;只要單節點的快速環境的話，minikube 或 kind 都比較簡單。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;用設定檔而不是命令列參數&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;設定檔可以進 Git，之後要重建叢集、跟別人分享，都只要一個檔案。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;固定版本&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;k3d 和 k3s 都固定版本，什麼時候換由自己決定，不是某天自動發生的。&lt;/p&gt;

&lt;h2&gt;
  
  
  延伸閱讀
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://k3d.io/stable/usage/configfile/" rel="noopener noreferrer"&gt;k3d：Using config files&lt;/a&gt;：&lt;code&gt;Simple&lt;/code&gt; 設定檔的用法與範例。這一頁沒有列出 &lt;code&gt;runtime.serversMemory&lt;/code&gt;／&lt;code&gt;agentsMemory&lt;/code&gt;，完整欄位請看它連到的 &lt;a href="https://github.com/k3d-io/k3d/blob/main/pkg/config/v1alpha5/schema.json" rel="noopener noreferrer"&gt;JSON Schema&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.k3s.io/add-ons/storage" rel="noopener noreferrer"&gt;K3s：Volumes and Storage&lt;/a&gt;：預設的儲存路徑是 &lt;code&gt;/var/lib/rancher/k3s/storage&lt;/code&gt;，以及 &lt;code&gt;--default-local-storage-path&lt;/code&gt; 旗標。&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/rancher/local-path-provisioner" rel="noopener noreferrer"&gt;Local Path Provisioner 的 README&lt;/a&gt;：&lt;code&gt;nodePathMap&lt;/code&gt; 與 &lt;code&gt;DEFAULT_PATH_FOR_NON_LISTED_NODES&lt;/code&gt; 的說明（步驟七 patch 的就是這份設定）。&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.k3s.io/cli/server" rel="noopener noreferrer"&gt;K3s：&lt;code&gt;k3s server&lt;/code&gt; 參數&lt;/a&gt;：&lt;code&gt;--disable&lt;/code&gt; 可關閉的內建元件（coredns、servicelb、traefik、local-storage、metrics-server、runtimes）。&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.docker.com/engine/install/ubuntu/" rel="noopener noreferrer"&gt;Docker：Install Docker Engine on Ubuntu&lt;/a&gt;：官方安裝步驟，上面的指令就是依這份。&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.docker.com/engine/logging/drivers/json-file/" rel="noopener noreferrer"&gt;Docker：JSON File logging driver&lt;/a&gt;：&lt;code&gt;max-size&lt;/code&gt;（預設不限制）、&lt;code&gt;max-file&lt;/code&gt;（要搭配 &lt;code&gt;max-size&lt;/code&gt; 才有效）的說明。&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.docker.com/engine/install/linux-postinstall/" rel="noopener noreferrer"&gt;Docker：Linux post-installation steps&lt;/a&gt;：不用 &lt;code&gt;sudo&lt;/code&gt; 執行 Docker，以及 &lt;code&gt;docker&lt;/code&gt; 群組的安全性提醒。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;這篇的環境是 Ubuntu LTS、k3d v5.8.3、k3s v1.31.5。你的版本不同的話，記得對一下欄位名稱。&lt;/p&gt;

</description>
      <category>k3d</category>
      <category>kubernetes</category>
      <category>devops</category>
      <category>ubuntu</category>
    </item>
  </channel>
</rss>
