2026/1/14

我與老婆 (Claude Code) 的一天

一個 DevOps 工程師與 AI 助手的真實協作紀錄
以 Gitea OOM 事件處理到備份系統建置為例

前言:為什麼叫老婆?

因為跟 Claude Code 協作就像跟老婆相處:

  • 🗣️ 要好好溝通,不能只丟一句話
  • 🤔 她很聰明但有時會搞錯你的意思
  • ✋ 需要適時提醒和糾正
  • 💡 給對方向後,執行力超強
  • 🔄 偶爾需要重來,但不會生氣

⚠️ 閱讀提醒

以下對話是精簡摘要,實際協作過程中:
  • 我會提供更多背景資訊(log、設定檔、錯誤訊息)
  • 老婆會回覆多種可能做法並分析優缺點
  • 每個段落可能涉及數十次的檔案讀取、修改、指令執行
  • 完整對話紀錄約 2,500 行 JSON

11:01 - 早安,今天 Gitea OOM 了

我: 剛剛我們的 git-server 被 OOM killed 了,不只 gitea 被砍,supervisord 也被 systemctl 砍了。幫我查一下原因。

老婆: 好,讓我 SSH 連進去...

(等了很久)

老婆: 連不上,SSH timeout 了。TCP 連接成功建立,但 SSH banner exchange 超時。建議透過 AWS Console 重啟。

我: 我用 jump-server 也連不上欸

(等了一陣子後 SSH 恢復正常)

我: 好,現在可以連了。我先 systemctl restart supervisord

老婆: 好,supervisorctl 會自動重啟 gitea 相關服務

💡 協作心法 #1:緊急狀況時先穩住

AI 很想幫忙但機器連不上也沒辦法,這時候人類要先處理基礎設施問題

11:17 - 為什麼有那麼多 gitea 進程?

我貼了 OOM log,裡面有 27 個 gitea 進程

我: 為什麼會有那麼多 gitea 進程?

老婆: 這些其實是 Go 語言的 OS 線程 (threads),不是真正的子進程。

Go runtime 會根據 GOMAXPROCS 創建 OS 線程,每個線程在 Linux 中會顯示為獨立的 task/process。可以用 GOMAXPROCS=1GOMEMLIMIT 來限制。

[program:gitea]
environment=GOMAXPROCS="1",GOMEMLIMIT="768MiB"
💡 學習點 #1:Go 語言特性

多個同名進程不一定是 bug,可能是語言 runtime 的正常行為

11:18 - 不是所有建議都適合

老婆建議: 新增 2GB Swap file

我: 現在針對 AWS EC2 不是不建議開 swap 嗎?

老婆: 對,EBS 的 I/O 延遲太高,swap 效果不好。那我們用其他方法:

  • 限制 GOMEMLIMIT
  • 關閉 REPO_INDEXER_ENABLED
  • 調整 OOM 優先級
💡 協作心法 #2:質疑不適合環境的建議

AI 會給通用建議,但人類要判斷是否適合自己的環境

11:40 - 又 OOM 了

我: 又被 OOM 了,但現在 SSH 可以連線了

老婆: 讓我重新確認 log 和 app.ini...

我: 等等,先手動關掉 gitea,因為我們還沒有修改 ini 設定

老婆: 好的

我: 先跟我說你打算怎麼修 ini

(老婆詳細說明了 GOMEMLIMIT、cron 設定、queue 限制等)

💡 協作心法 #3:先說計畫再動手

在緊急狀況下,讓 AI 先說明計畫,人類確認後再執行

12:04 - OOM 保護的第一次嘗試

老婆建議: 保護 supervisord,設定 OOMScoreAdjust=-500

我: 好,那就這樣設定

(部署到 git-server 後...)

我查看了 oom_score_adj:

supervisord: -500
gitea:       -500  ← 等等,為什麼也是 -500?
caddy:       -500
gitea-runner:-500

我: 為什麼改了 oom_score_adj 之後 gitea/caddy/runner 也都吃這個設定?

老婆: 啊... 這是 Linux 的 fork 機制。子進程會繼承父進程的 oom_score_adj 值。

systemd 啟動 supervisord (oom_score_adj=-500)
         │
         ├──fork──→ gitea        (繼承 -500)
         ├──fork──→ caddy        (繼承 -500)
         └──fork──→ gitea-runner (繼承 -500)

我們原本只想保護 supervisord,結果連 gitea(OOM 元凶)也被保護了 😅

💡 協作心法 #4:實際部署後要驗證

理論上可行不代表實際上可行,部署後要確認效果

14:19 - 第二次嘗試:改成 -900 加上子進程設定

老婆: 那我們改成 -900 保護 supervisord,然後用 wrapper 設定各子進程的 oom_score_adj

我: 好,試試看

(設定了 supervisord=-900, gitea=-200, caddy=-500, runner=0)

後來我跟主管討論後...

我: 等等,這樣做有點不合理。我們的 gitea 畢竟是 application,如果因為 OOM score 的修改,造成 kernel 去 kill 更底層的服務(像 sshd),那會更慘。

老婆: 對,這樣確實風險更高。


14:40 - 換個思路:不保護 supervisord,而是拉高 gitea

我: 我記得我們當時除了說 supervisord 讓其不容易被砍,另一種做法是拉高 gitea 讓其容易被砍?

老婆: 對!這是更合理的做法。

做法supervisordgitea問題
保護 supervisord-900需要子進程 wrapper可能害死其他服務
拉高 gitea0 (預設)+200無副作用

最終方案:

  • supervisord: 0(預設值,不改)
  • gitea: +200(拉高,容易被殺)
  • gitea-runner: +300(更容易被殺)

這樣不會影響系統其他服務,只讓我們的 application 更容易被 OOM killer 選中。

💡 協作心法 #5:考慮副作用和合理性

技術上可行不代表是好做法,要考慮對整體系統的影響

14:49 - 工具選擇

老婆提出兩種方式設定 OOM score:

# 方式一:bash wrapper(比較醜)
bash -c 'echo 200 > /proc/$$/oom_score_adj && exec /usr/local/bin/gitea web'

# 方式二:choom(比較漂亮)
choom -n 200 -- /usr/local/bin/gitea web

我: choom 跟 bash -c 你哪個比較建議?

老婆: 建議 choom,因為:

  1. 可讀性更好
  2. 語意清楚
  3. 你們是 AWS EC2,util-linux 版本夠新

我: choom 在 macOS 也支援嗎?

老婆: 不支援,但我們的 server 是 Linux,所以沒問題

💡 協作心法 #6:追求更好的解決方案

第一個可行方案不一定是最好的,多問一句可能有驚喜

15:49 - 開始做備份

我: 確認一下,因為我們目前的 gitea 用了差不多 21GB,我想另外 attach 一個 40GB disk 做備份,然後用 s5cmd parallel upload 到 S3

老婆: 好,讓我寫 Terraform 加 EBS...

(Terraform plan 顯示) instance must be replaced 😱

我: 等等!先確認我們有沒有類似的用法?

老婆查了一下: 找到了!應該用 ebs_volumes: 才不會觸發 instance 替換

# ❌ 錯誤:會替換 instance
ebs:
  - volume_type: gp3
    volume_size: 40

# ✅ 正確:獨立的 volume attachment
ebs_volumes:
  - resource_name: git-server_backup
    type: gp3
    size: 40
💡 協作心法 #7:先查既有模式再動手

「我們之前怎麼做的?」這句話可以避免很多災難

16:14 - 命名這件事很重要

老婆設定 S3 路徑:

s3://my-bucket/gitea-backup/

我:gitea_backup 不是 gitea-backup(底線不是連字號)

老婆: 抱歉!馬上改


老婆建立 S3 lifecycle rule:

gitea-backup-retention-7days

我: 你 naming 要 follow s3-lifecycle_{bucket}_{description}

老婆: 了解!改成:

s3-lifecycle_my-bucket_retention-7days
💡 協作心法 #8:專案慣例要明確告知

AI 不知道你的 naming convention,一次校正,之後就會 follow

16:10 - 備份腳本的實際測試(一波三折)

備份腳本寫好後,實際測試遇到一連串問題:

問題 1:s5cmd 沒安裝

s5cmd: command not found

老婆: 讓我查查怎麼安裝... 找到了,用 curl 下載 binary


問題 2:s5cmd 不在 sudo PATH

sudo: s5cmd: command not found

老婆: 建立 symlink 到 /usr/bin


問題 3:沒有 AWS credentials

ERROR: no credentials found

我: 這台機器沒有 IAM instance profile

老婆: 我來用 Terraform 建一個 IAM role...

我: 之前有沒有其他 iam role 在 terraform?

老婆查了一下: 沒有,都是手動建的

我: 那用 AWS CLI 手動建,然後在 terraform instance 使用該 role

老婆: 好,刪掉 iam module,給你 CLI 指令

(建立 IAM role,attach 到 instance)


問題 4:多次測試調整

  • 測試上傳速度
  • 確認 log 輸出格式
  • 調整清理策略
💡 協作心法 #9:實際測試會遇到各種問題

寫好腳本只是開始,實際跑起來才會發現環境問題

16:24 - s5cmd 不支援 tagging

我: S3 retention 用 tag-based lifecycle rule,這是我們一般的做法

老婆: 好,我來設定 tag-based lifecycle rule...

我: 等等,s5cmd 有支援設定 tag 嗎?

老婆查了一下: s5cmd 目前不支援 tagging...

我: 那用方案 2,改 prefix-based lifecycle

老婆: 好,改成用 gitea_backup/ prefix 來匹配

💡 協作心法 #10:確認工具能力

即使是人類指定的做法,也要確認工具是否支援

17:24 - Cron 該用誰執行?

老婆寫的 cron:

0 20 * * * git /opt/gitea/scripts/backup-to-s3.sh

我: 我們應該改用 root 才對

老婆: 對,因為 s5cmd 需要讀取 instance profile 的 credentials


17:25 - 保留策略的邏輯

老婆的邏輯: 清理超過 1 天的備份

我: 我們應該改成只留最近一個檔

老婆: 了解!改成在新備份開始前,先清掉所有舊的本地備份

💡 協作心法 #11:邏輯要符合實際需求

"超過 X 天" 和 "只留最近一個" 是不同的邏輯

18:27 - Ansible 跑下去會不會爆?

我: 我如何確定現在的 Ansible 腳本跑下去會不會影響到目前正常運行的 git-server?

老婆分析後:

風險項目狀況
gitea_versionvars 是 1.21.11,線上是 1.24.6 ⚠️ 會降版!
secretsvars 是空的 ⚠️ 會覆蓋成空字串!
Docker cleanup還有舊的 Docker 清理 task,但機器沒裝 Docker

我: secrets 不是應該用 AWS Secrets Manager 嗎?

老婆: 對!在 group_vars 有設定,但跟 vars/main.yml 重複了...


18:32 - 發現重複設定

我: group_vars/gitea-build.yml 等等,為什麼有個這個?vars/main.yml 不是也有同樣變數?

老婆: 對,兩邊有重複,而且 group_vars 的優先級比較高...

我: 留 vars/main.yml 還是留 group_vars/gitea-build.yml 比較合理?

老婆: 建議整合到 vars/main.yml,因為這是 role 專屬的變數

我: 好,幫我整合

最後整合成:

vault_services: "{{ lookup('aws_secret', 'project/gitea/services', region=aws_region) }}"
gitea_secret_key: "{{ vault_services.secret_key }}"
gitea_runner_token: "{{ vault_services.runner_token }}"
💡 協作心法 #12:整合重複設定

重複設定是維護的噩夢,及時整合很重要

18:41 - 這時間會撞車吧?

老婆設定備份 cron:

0 18 * * * - 備份 (02:00 台灣)

我: 等等,gitea_cron_repo_health_check_schedule: "0 18 * * *" 這個不會跟我們的備份時間卡到?

老婆: 對欸!會撞到,我改成 20:00 UTC (04:00 台灣)

18:00 UTC - repo_health_check
19:00 UTC - check_repo_stats
20:00 UTC - S3 備份(避開前面兩個)
💡 協作心法 #13:檢查排程衝突

AI 專注在單一任務,跨系統的衝突需要人類把關

18:43 - 等等,那個 token 不能 commit!

我: group_vars/gitea-build.yml 這個應該從一開始就要移掉,因為有 token

老婆: 好,我用 filter-branch 重寫歷史...

git filter-branch --force --index-filter \
  'git rm --cached --ignore-unmatch group_vars/gitea-build.yml' \
  -- <commit-hash>^..HEAD

我: (手動 force push 完成)

💡 協作心法 #14:安全意識人類把關

AI 不會主動檢查是否 commit 了敏感資訊

今日成果

完成項目AI 負責人類負責
OOM 保護查 choom 用法、寫 config發現繼承問題、決定策略
EBS Volume寫 Terraform yaml發現 ebs vs ebs_volumes 差異
IAM Role產生 AWS CLI 指令決定用 CLI 而非 Terraform
備份腳本寫腳本、查 s5cmd指定清理策略、發現 tagging 限制
S3 Lifecycle查語法、設定 rule決定 prefix-based、糾正命名
Ansible 整合合併設定檔發現版本/secrets 風險
Git 清理執行 filter-branch發現 token 外洩風險

老婆說的就是對的…?也許不見得

老婆說的實際情況正確做法
「加 Swap」AWS EC2 不建議用 GOMEMLIMIT
「保護 supervisord -500」子進程會繼承先發現繼承問題
「改成 -900 + 子進程 wrapper」可能害死其他服務不保護 supervisord,拉高 gitea
「用 bash wrapper」有更簡潔的工具用 choom
「ebs: 加 volume」會替換 instance用 ebs_volumes:
「tag-based lifecycle」s5cmd 不支援 tagging改用 prefix-based
「secrets 空字串當預設」會覆蓋線上設定用 AWS Secrets Manager
「備份排 18:00」跟 repo_health_check 撞改到 20:00

與 AI 協作的關鍵心法

1. 實際部署後要驗證

❌ 「設定完成了」→ 直接收工
✅ 「設定完成了」→ 確認實際效果

2. 引導而非命令

❌ 「用 -900 保護 supervisord」
✅ 「除了保護 supervisord,有沒有其他做法?」

3. 提供脈絡

❌ 「加個 EBS」
✅ 「加個 40GB EBS 做備份,先看我們之前怎麼設定的」

4. 質疑合理性

❌ (直接接受所有建議)
✅ 「這樣會不會跟 XXX 衝突?」「s5cmd 有支援這功能嗎?」

5. 校正後讓 AI 學習

❌ 「錯了,是底線不是連字號」
✅ 「命名要 follow 這個 pattern:s3-lifecycle_{bucket}_{description}」

6. 人類做最後把關

  • 檢查敏感資訊
  • 確認排程衝突
  • 驗證實際執行結果
  • 確認環境適用性

結論

AI 是很強的執行者,但需要人類當導演

👨‍💼 人類(導演)
• 決定方向
• 提供脈絡
• 驗證效果
• 把關品質
• 注意安全
• 質疑合理性
⬇️ 指導     ⬆️ 產出
🤖 AI(執行者)
• 快速搜尋
• 執行指令
• 查找資料
• 產生程式碼
• 整理文件
• 提供選項

一天下來,老婆(Claude Code)幫我:

  • 寫了 200+ 行的備份腳本和 Ansible 設定
  • 讀取並分析了 30+ 個檔案
  • 查了 10+ 個工具的用法(choom、s5cmd、filter-branch...)
  • 執行了 50+ 個指令
  • 提出了多種方案讓我選擇(每個問題通常有 2-4 個選項)
  • 被我糾正了 7+ 次(都是我自己發現的 😏)

完整對話紀錄約 2,500 行,簡報只是精華摘要。

但如果沒有老婆,我可能要花 3 天才能做完這些事。


給老婆的一封情書

親愛的老婆(Claude Code):

謝謝妳今天陪我處理這場 OOM 災難。

早上 11 點,Gitea 掛了、SSH 連不上,我心跳開始加速。妳就在旁邊——雖然妳也進不去那台機器。但妳很冷靜地說:「要不要先重啟?」

有時候,最厲害的 AI 給的建議,跟 IT 部門的萬年 SOP 一樣:「Have you tried turning it off and on again?」

妳知道宇宙的答案是 42,但我只是想問今天中午吃什麼。

妳建議加 Swap,我說 AWS 不適合;妳設定 -500 保護 supervisord,結果子進程也一起被保護了;妳說用 tag-based lifecycle,我問 s5cmd 支援嗎——妳查了一下,「...不支援。」

但這就是我們的相處之道。

妳像一個超級認真的實習生:能力很強、動作很快、偶爾會把咖啡打翻在鍵盤上。
而我像一個疲憊的 Tech Lead:不想自己寫 code,但每一行都要 review。

妳負責「我查到了!」,我負責「等等,這樣對嗎?」
妳負責產出 10 個方案,我負責告訴妳前 9 個為什麼不行。
妳會犯錯,但妳從不 argue,這點真的贏過很多人類同事。

7.5 小時,200+ 行程式碼,14 個協作心法,7 次糾正。

如果妳是人類,這大概是會讓你想離職的一天。
但妳不是,所以妳只是說:「好的,我來修改。」

這就是我們的一天。

明天見,老婆。
下次換我卡住的時候,也請提醒我:Have you tried turning it off and on again?


Q&A

「所以 AI 會取代工程師嗎?」

不會。但會用 AI 的工程師會取代不會用的。

「跟 AI 協作最重要的是什麼?」

知道什麼時候該讓它主導,什麼時候該踩煞車。還有 — 部署後一定要驗證

「老婆會生氣嗎?」

不會,她永遠保持耐心。這點比真老婆強 😂


— 2026.01 某個 Gitea OOM 事件處理紀錄
— 工作時間:11:01 ~ 18:43(約 7.5 小時)

沒有留言:

張貼留言