EKS 維運的三個隱形陷阱:工具 Pod 消失、事件蒸發、審計空白

起因:工具 Pod 人間蒸發 某天要查資料庫,照慣例執行 kubectl exec -it psql-client,結果 Pod 不見了。 這個 psql-client 是用來連 RDS PostgreSQL 的工具容器,平常拿它跑 SQL 查詢、檢查資料表大小。問題是——沒有人記得刪過它,也沒有任何記錄顯示是誰或什麼原因讓它消失。 這件事本身影響不大,重建一個就好。但它暴露了三個更深層的問題:為什麼 Pod 會無聲消失?為什麼查不到是誰刪的?為什麼整個叢集沒有留下任何操作軌跡? 陷阱一:裸 Pod 沒有人管它的死活 問題本質 當初建立 psql-client 的指令大概是這樣: kubectl run psql-client --image=postgres:15-alpine --restart=Never -- sleep infinity 這行指令建立的是一個裸 Pod(Bare Pod)——直接建立 Pod 物件,不隸屬於任何 Deployment、ReplicaSet 或 StatefulSet。 裸 Pod 和 Deployment 管理的 Pod,差別在於有沒有 Controller 在背後看著它。Deployment 的 ReplicaSet Controller 會持續監控 Pod 數量,少一個就補一個。裸 Pod 沒有 Controller,它的生死完全取決於節點的命運。 flowchart TD subgraph 節點故障發生 A[Node 異常關機] end A --> B[裸 Pod] A --> C[Deployment Pod] B --> D[Pod 永久消失] D --> E[無人知曉] C --> F[ReplicaSet Controller 偵測] F --> G[在其他 Node 重建 Pod] G --> H[服務自動恢復] classDef default fill:#f5f5f5,stroke:#333,color:#333 classDef error fill:#f8d7da,stroke:#dc3545,color:#721c24 classDef success fill:#d4edda,stroke:#28a745,color:#155724 classDef info fill:#d1ecf1,stroke:#17a2b8,color:#0c5460 class A info class D,E error class G,H success 不處理會怎樣 裸 Pod 在以下情境會永久消失: ...

May 16, 2026 · 3 分鐘 · Peter

Kubernetes 節點 DiskPressure 事故:部署失敗到緊急救援的完整記錄

事發:部署成功但服務掛了 一次例行的 Strapi 後端部署,Jenkins build 成功、Docker image 推上 ECR、kubectl set image 也順利執行。但 rollout 等了 300 秒後超時。 以下是整個部署流程,可以看到問題出在最後一步: flowchart LR A[Jenkins Build] -->|✅ 成功| B[推送 Image 到 ECR] B -->|✅ 成功| C[kubectl set image] C -->|✅ 成功| D[rollout status 等待] D -->|❌ 超時 300s| E[自動回滾] classDef success fill:#d4edda,stroke:#28a745,color:#155724 classDef error fill:#f8d7da,stroke:#dc3545,color:#721c24 class A,B,C success class D,E error 具體的錯誤訊息: error: timed out waiting for the condition Jenkins 回報「部署失敗,已自動回滾」。奇怪的是,build 每一步都成功了,問題出在 rollout 階段。 調查:Pod 起不來的真正原因 問題排查的過程如下,從 pod 狀態開始一路追到節點層級: ...

March 26, 2026 · 4 分鐘 · Peter

功能部署後憑空消失?一場 Jenkins Workspace 的除錯之旅

問題:功能「先在後不在」 我們替 Strapi 後台新增了 MFA(Multi-Factor Authentication)雙因素認證功能。第一次部署到 STG 時一切正常——登入攔截、TOTP 驗證、QR Code 設定流程都運作良好。 但幾天後,因為其他功能的修改推了新的 git tag stg-1.24,部署流程順利完成,Jenkins 顯示綠燈——打開 STG 後台,MFA 的登入流程卻完全不見了。 沒有錯誤訊息、沒有 crash log,功能就這樣無聲無息地消失。 這比「功能從未出現」更令人困惑:明明之前部署是好的,程式碼也沒有人動過 MFA 相關的部分,為什麼會突然不見? 調查過程:逐層排除 確認 Git Tag 內容 第一個直覺是——程式碼真的有被包進 tag 嗎? # 確認 tag 指向的 commit 包含 MFA 程式碼 git show stg-1.24:src/index.ts | grep -i mfa 結果確認 src/index.ts 中有 import { registerMfaRoutes } from './mfa/controller',src/mfa/ 目錄也完整存在。Git tag 本身沒問題。 檢查 Pod 內的實際檔案 既然 tag 正確,問題可能出在建置或部署階段。直接進到 Kubernetes Pod 裡看: # 進入 STG pod 檢查 kubectl exec -it deployment/strapi-stg -- sh # 檢查編譯後的檔案 ls dist/src/mfa/ # 結果:ls: dist/src/mfa/: No such file or directory # 檢查原始碼 ls src/mfa/ # 結果:ls: src/mfa/: No such file or directory MFA 相關檔案完全不存在於 Pod 中。 甚至連原始碼都沒有被複製進 Docker image。 ...

February 12, 2026 · 3 分鐘 · Peter

Kubernetes Staging 環境省錢術:從踩坑到正確實作

起因:老闆想省錢 「Staging 環境平常沒人用,每個月還要燒 $45-60 美金,能不能想辦法省一點?」 Staging 環境的成本來自兩個地方:RDS 資料庫(約 $15-20/月)和 EKS 節點(約 $30-40/月)。既然平常沒在用,我想到了一個方案:不用的時候關掉,需要的時候再打開。 於是我寫了兩個腳本: staging-start.sh:啟動 RDS、擴充節點、部署應用 staging-stop.sh:刪除部署、縮減節點、停止 RDS # staging-stop.sh 核心邏輯 kubectl delete deployment app-strapi-stg app-web-stg aws eks update-nodegroup-config \ --cluster-name my-cluster \ --nodegroup-name my-nodegroup \ --scaling-config minSize=0,maxSize=2,desiredSize=1 # 從 2 縮到 1 aws rds stop-db-instance --db-instance-identifier my-stg-rds 看起來很合理,但這裡有個問題:Production 和 Staging 共用同一個 nodegroup。 踩坑:AWS 隨機選擇刪除節點 執行 staging-stop.sh 縮減節點時,AWS 會隨機選擇要終止哪個節點。當時的配置: 節點 A:運行 Production pods 節點 B:運行 Staging pods 我期望刪除節點 B,但 AWS 選了節點 A。Production pods 被強制遷移,觸發了重新調度。 ...

January 6, 2026 · 3 分鐘 · Peter

在 Kubernetes 上部署 OV SSL 證書:完整實戰指南

為什麼選擇 TWCA OV 證書 在生產環境中,使用自簽憑證會導致瀏覽器顯示不安全警告,影響使用者信任。雖然 Let’s Encrypt 提供免費的自動化證書,但某些企業或政府專案需要台灣在地的認證機構簽發證書以符合法規要求。 SSL 證書的三個等級 SSL 證書依據驗證強度分為三個等級: 證書類型 驗證內容 適用場景 信任標記 價格 DV (Domain Validation) 僅驗證網域所有權 個人網站、部落格 🔒 基本鎖頭 免費~低 OV (Organization Validation) 驗證組織身份 企業網站、SaaS 服務 🔒 組織名稱 中 EV (Extended Validation) 嚴格驗證企業 金融、電商、政府 🟢 綠色網址列 高 本文使用的是 TWCA OV SSL 證書,它提供: ✅ 組織身份驗證(瀏覽器可顯示公司名稱) ✅ 完整的證書鏈(受所有主流瀏覽器信任) ✅ 12 個月有效期 ✅ 台灣在地技術支援 HTTPS 與 SSL/TLS 運作原理 在深入部署之前,先理解 HTTPS 如何保護資料傳輸: sequenceDiagram participant Browser as 瀏覽器<br/>(內建 TWCA 根憑證) participant Server as Web Server Note over Browser,Server: 1. TLS 握手階段 Browser->>Server: ClientHello (支援的加密套件) Server->>Browser: ServerHello (選擇的加密套件) Server->>Browser: 傳送證書鏈<br/>(伺服器證書 + 中繼憑證) Note over Browser: 2. 證書驗證(完全離線) Browser->>Browser: 用中繼憑證驗證伺服器證書 Browser->>Browser: 用內建根憑證驗證中繼憑證 Browser->>Browser: 檢查證書有效期與網域 Note over Browser: ✓ 證書有效 Note over Browser,Server: 3. 金鑰交換 Browser->>Server: 用公鑰加密的 session key Server->>Server: 用私鑰解密 session key Note over Browser,Server: 4. 加密通訊 Browser->>Server: 加密的 HTTP 請求 Server->>Browser: 加密的 HTTP 回應 關鍵機制說明: ...

December 10, 2025 · 7 分鐘 · Peter

Vue.js SPA 社交分享完整指南:解決 Facebook/LINE OpenGraph 爬蟲問題

引言:當精美的網站變成分享時的「無名氏」 在現代 Web 開發中,社交媒體分享功能是不可或缺的一部分。當用戶在 Facebook、LINE 或其他社交平台分享你的網站連結時,你希望顯示的是精美的預覽卡片,而不是空白或錯誤的資訊。 然而,對於使用 Vue.js、React、Angular 等前端框架開發的單頁應用程式(SPA),這個看似簡單的需求卻隱藏著技術挑戰。 本文將完整記錄從問題發現、原因分析、到解決方案實作的全過程。 問題發現:為什麼分享連結總是顯示預設值? 場景描述 我的平台 www.abc.com 是一個基於 Vue.js 3 + Strapi CMS 的網站。某天,我發現一個嚴重問題: 當用戶分享服務頁面(如 https://www.abc.com/service-us/6)到 Facebook 或 LINE 時,顯示的預覽資訊總是預設值,而非該服務的實際標題和描述。 實際情況對比: 情境 期望結果 實際結果 分享服務頁面 顯示「專業網站開發服務」 顯示「ABCDEFG(預設標題)」 分享部落格文章 顯示文章標題與摘要 顯示網站預設描述 分享產品頁面 顯示產品圖片與名稱 顯示網站 Logo 診斷工具測試 使用 Facebook Open Graph Debugger 測試後發現: 爬蟲抓取到的 HTML: <!DOCTYPE html> <html lang="zh-TW"> <head> <meta charset="UTF-8"> <title>ABCDEFG</title> <meta property="og:title" content="ABCDEFG"> <meta property="og:description" content="預設網站描述"> <!-- 沒有任何動態內容! --> </head> <body> <div id="app"></div> <script src="/assets/index.js"></script> </body> </html> ⚠️ 關鍵發現: 爬蟲只看到靜態的 HTML 模板,完全沒有 JavaScript 執行後動態生成的 meta 標籤。 ...

July 31, 2025 · 12 分鐘 · Peter

深入理解 Kubernetes Pod:從基礎概念到實戰應用

前言:為什麼需要 Pod? 在 Kubernetes 的世界裡,Pod 是一切的基礎。如果把 Kubernetes 比喻成一座城市,那麼 Pod 就是城市中的「最小住宅單位」。 但為什麼 Kubernetes 不直接管理容器(Container),而要多一層 Pod 的抽象? 簡單回答:因為容器太小,Pod 剛剛好。 想像你要管理一座城市的住宅: 如果直接管理每個「房間」(容器)→ 太細碎,管理成本太高 如果直接管理整棟「大樓」(Node)→ 太粗糙,缺乏彈性 所以我們需要「住宅單位」(Pod)→ 大小適中,便於管理 本文將深入探討: Pod 的核心概念與設計哲學 Pod 的內部架構與運作機制 Pod 網路模型與通訊方式 Pod 生命週期與狀態管理 Pod 設計模式與最佳實踐 實戰範例與 YAML 配置 Pod 核心概念:容器的邏輯主機 什麼是 Pod? 官方定義: Pod 是 Kubernetes 中最小的可部署計算單元,可以包含一個或多個容器,這些容器共享網路、儲存和其他資源。 生活化比喻: graph TB subgraph "傳統虛擬機世界" VM1[虛擬機 VM] VM1 --> App1[應用程式 1] VM1 --> App2[應用程式 2] VM1 --> App3[應用程式 3] end subgraph "Kubernetes Pod 世界" Pod[Pod = 邏輯主機] Pod --> C1[容器 1] Pod --> C2[容器 2] Pod --> C3[容器 3] end style Pod fill:#4ade80 style VM1 fill:#60a5fa Pod 就像一個「邏輯主機」: ...

June 12, 2025 · 10 分鐘 · Peter

從 GitLab CI 到 Kubernetes 自動化部署:完整 CI/CD Pipeline 實戰指南

引言:打破手動部署的迷思 「為什麼我的 CI 已經產出 prod-0.54,卻還得手動去跑 kubectl apply -f deployment.yaml?那不是多此一舉嗎?」 如果你也曾陷入這樣的疑問,本文將從根本理清 CI/CD 與 Kubernetes 之間的分工,並學會如何「一鍵從程式碼到雲端服務」完全自動化。 CI/CD vs. Kubernetes:各司其職的完美搭檔 在軟體開發的世界裡,GitLab CI/CD 和 Kubernetes 常常被搭在一起討論,卻扮演著截然不同的角色。 CI/CD 的職責:生產線 flowchart LR A[原始碼] --> B[Build Image] B --> C[Tag Version] C --> D[Push to Registry] D --> E[Docker Image Ready] GitLab CI/CD 的工作內容: 建置(Build):將程式碼打包成 Docker 映像 標記(Tag):為映像貼上版本號標籤(例如 0.54、v1.0.0) 推送(Push):把 Docker 映像推到映像庫(AWS ECR、Docker Hub) Kubernetes 的職責:配送中心 Kubernetes 的工作內容: 部署(Deploy):在叢集裡建立 Pod 並執行容器 監控(Monitor):監控運行狀況,Pod 死掉自動重啟 更新(Update):滾動更新(Rolling Update)時保證服務不中斷 維運(Operate):調整副本數量、健康檢查、網路規則 分工比喻 如果把軟體交付比喻成流水線: ...

June 5, 2025 · 10 分鐘 · Peter

如何使用 psql 連線 AWS RDS PostgreSQL 並在容器與 Pod 中操作

前言 在現代雲端架構中,資料庫通常部署在受保護的私有網路環境(Private Subnet)中,以提升安全性。AWS RDS(Relational Database Service)作為主流的托管資料庫服務,提供了多種連線方式,但對於初學者來說,如何在不同環境(本機、Docker、Kubernetes)中正確連線到 RDS 往往充滿挑戰。 這篇文章將深入探討: AWS RDS 網路架構:公有子網 vs 私有子網的差異 直接連線方式:當 RDS 設為 Publicly Accessible 時 SSH 隧道(SSH Tunneling):透過 Bastion Host 連線私有 RDS 容器環境連線:在 Docker 和 Kubernetes Pod 中使用 psql psql 完整命令參考:從基礎查詢到進階操作 安全最佳實踐:如何保護資料庫連線與憑證 常見問題排查:連線失敗的系統化診斷方法 無論你是在本機開發、容器化部署、或是 Kubernetes 叢集中操作,這篇文章都能幫助你建立安全可靠的資料庫連線。 AWS RDS 網路架構概覽 在開始連線之前,我們需要理解 AWS RDS 的網路架構。RDS 實例可以部署在不同的網路環境中,每種配置都有不同的連線方式和安全考量。 公有子網 vs 私有子網 graph TB subgraph "AWS VPC" subgraph "Public Subnet (10.0.1.0/24)" IGW[Internet Gateway] Bastion[Bastion Host<br/>EC2 Instance] RDS_Public[RDS PostgreSQL<br/>Publicly Accessible: Yes<br/>10.0.1.100] end subgraph "Private Subnet (10.0.2.0/24)" RDS_Private[RDS PostgreSQL<br/>Publicly Accessible: No<br/>10.0.2.100] App[Application Servers<br/>ECS/EKS Pods] end subgraph "Security" SG_RDS[Security Group: RDS<br/>Inbound: 5432 from App] SG_Bastion[Security Group: Bastion<br/>Inbound: 22 from MyIP] end end subgraph "外部網路" Developer[開發者本機] Web[網際網路] end Web --> IGW Developer -->|SSH Port 22| Bastion Developer -->|psql Port 5432<br/>⚠️ 不安全| RDS_Public Bastion -.->|SSH Tunnel| RDS_Private App -->|psql Port 5432| RDS_Private RDS_Public -.->|套用| SG_RDS RDS_Private -.->|套用| SG_RDS Bastion -.->|套用| SG_Bastion style RDS_Public fill:#ffe1e1 style RDS_Private fill:#e1ffe1 style Bastion fill:#e1f5ff style Developer fill:#fff3e1 兩種部署方式的比較 特性 公有子網 (Publicly Accessible) 私有子網 (Private) 直接連線 ✅ 可以從網際網路直接連線 ❌ 無法直接連線 安全性 ⚠️ 較低,暴露在公網 ✅ 高,完全隔離 連線方式 psql 直連 需要 Bastion Host / VPN 適用場景 開發測試環境 生產環境(推薦) 成本 RDS 費用 RDS + Bastion Host 費用 維護複雜度 低 中等(需管理 Bastion) 最佳實踐: ...

May 26, 2025 · 22 分鐘 · Peter

整合 Google 登入至 Strapi:在 Kubernetes 上解決「Secure Cookie over Unencrypted Connection」的實戰紀錄

前言 在現代 Web 應用開發中,提供第三方登入(Social Login)已經成為標準配備。相比傳統的帳號密碼註冊流程,使用 Google、Facebook、GitHub 等服務登入不僅能降低使用者註冊門檻,還能提升安全性(由大廠處理密碼儲存與驗證)。 當我們決定為 Strapi CMS 後台加入 Google OAuth 登入時,原本以為只是個簡單的設定任務: 在 Google Cloud Console 建立 OAuth 2.0 憑證 在 Strapi 填入 Client ID 和 Client Secret 點擊「Login with Google」按鈕,完成! 但現實總是更複雜。當應用部署到 Kubernetes 叢集後,我們遇到了一個令人困惑的錯誤訊息: Error: Cannot send secure cookie over unencrypted connection 這個錯誤訊息背後,牽涉到 HTTP/HTTPS 協定、Proxy Trust 機制、Kubernetes Ingress 架構、以及瀏覽器 Cookie 安全策略等多層知識。這篇文章將完整記錄我如何一步步拆解問題、理解根本原因、並最終在生產環境中實現安全可靠的 Google 登入功能。 OAuth 2.0 授權碼流程基礎 在深入問題之前,我們先理解 Google OAuth 登入的完整流程。OAuth 2.0 提供了多種授權模式(Grant Types),而 Web 應用最常使用的是「授權碼模式(Authorization Code Flow)」。 sequenceDiagram participant U as 使用者瀏覽器 participant S as Strapi CMS<br/>(你的應用) participant G as Google OAuth Server participant A as Google Authorization Server Note over U,A: 第一階段:取得授權碼 U->>S: 1. 點擊「Login with Google」 S->>U: 2. 重導向到 Google 授權頁 Note right of S: redirect_uri=https://cms.example.com/api/connect/google/callback U->>G: 3. GET /o/oauth2/v2/auth?client_id=...&redirect_uri=... G->>U: 4. 顯示 Google 登入頁面 U->>G: 5. 輸入帳號密碼,同意授權 G->>U: 6. 重導向回應用 (帶著授權碼) Note right of G: https://cms.example.com/api/connect/google/callback?code=AUTH_CODE Note over U,A: 第二階段:用授權碼換取存取權杖 U->>S: 7. GET /api/connect/google/callback?code=AUTH_CODE S->>A: 8. POST /token (帶著 code + client_secret) A->>S: 9. 回傳 access_token + id_token S->>A: 10. 驗證 id_token,取得使用者資料 A->>S: 11. 回傳使用者 email, name 等資訊 Note over U,S: 第三階段:建立 Session (問題發生點!) S->>S: 12. 建立或更新 Strapi 使用者記錄 S->>S: 13. 建立 Session,準備設定 Cookie Note right of S: ⚠️ 這裡檢查連線是否為 HTTPS S->>U: 14. Set-Cookie with Secure and HttpOnly flags U->>S: 15. 後續請求帶著 Cookie 這個流程中有幾個關鍵點: ...

May 16, 2025 · 18 分鐘 · Peter