[{"content":"","date":"24 June 2026","externalUrl":null,"permalink":"/","section":"Ben's Blog","summary":"","title":"Ben's Blog","type":"page"},{"content":"","date":"24 June 2026","externalUrl":null,"permalink":"/journals/","section":"Journals","summary":"","title":"Journals","type":"journals"},{"content":" 前言🔖 # 最近已經在公司內部陸陸續續的將 api 服務，都轉成了 docker。有時候遇到尖峰時間，有些 api 服務只有兩個可能就會不夠應對線上的大流量，導致需要馬上開新的 vm，接著部署 api 服務再將新的 vm 新增到負載平衡的後端群組裡。這個過程非常的麻煩。有時候 api server 只是幾個特定的 api 快要撐不住了。就需要再花費一台新的 vm 費用，怎麼想都很不划算。所以需要用 docker swarm 幫助我們擴縮 api container，在尖峰時間，特定的 api 能快速擴展更多的 container。過了離峰時間時間時，api container 可以快速縮減。這樣的話就可以省去了繁瑣的 sop，以及兼顧成本。\ndocker swarm 簡介📝 # docker swarm 是透過 manager node 管理 worker node 的方式，來達到快速擴縮 container 的功能。manager 就像是大腦可以調度 container 到指定的 worker node 上運行。\n實作🛠 # 首先要先建立一個 docker swarm 的叢集：\ndocker swarm init # 如果 vm 上有多個 IP 地址，需要使用 --advertise 參數指定要使用的 IP 地址 docker swarm init --advertise-addr \u0026lt;IP_ADDRESS\u0026gt; 初始化建立完成以後，會產生一個 token，這個 token 是用來讓 worker node 加入 swarm 的。\n⭐️ 這裡補充說明一下，可能會有人疑問為什麼要使用 2377 連接埠，這是 manager node 用來跟 worker node 進行通訊的連接埠。其中還有 7946 連接埠用來覆蓋網路節點發現的 TCP/UDP連接埠，4789 連接埠用來覆蓋網路流量的 UDP連接埠。\n# 在 worker node 上使用 token 加入 swarm docker swarm join --token \u0026lt;TOKEN\u0026gt; \u0026lt;MANAGER_IP\u0026gt;:2377 如果忘記 token 的話，可以使用以下指令來查看 token：\n# 查看 worker node 的 token docker swarm join-token worker 如果建立了新的 node 加入 manager node group 的話可以用指令先查看 manager node 的 token：\n# 查看 manager node 的 token docker swarm join-token manager # 使用 manager node 的 token 加入 swarm docker swarm join --token \u0026lt;manager_token\u0026gt; \u0026lt;MANAGER_IP\u0026gt;:2377 完成叢集的建置以後，可以用以下指令來查看 node 的狀態：\ndocker node ls 接著在 manager node 上，開始部署 docker stack。因為我們原來是用 docker compose 來管理 container，所以轉換到 docker swarm 會有幾個需要注意的地方。\n可以參考下面的 yaml 範例，已經轉換成 docker swarm 支援的格式了。\nservices: api: image: my-api:latest ports: - \u0026#34;3000:3000\u0026#34; configs: - source: api_config target: /app/config.json env_file: - .env volumes: - logs:/app/logs deploy: replicas: 2 resources: limits: cpus: \u0026#34;0.5\u0026#34; memory: \u0026#34;512M\u0026#34; reservations: cpus: \u0026#34;0.25\u0026#34; memory: \u0026#34;256M\u0026#34; restart_policy: condition: any placement: constraints: - \u0026#34;node.role == worker\u0026#34; logging: driver: \u0026#34;json-file\u0026#34; options: max-size: \u0026#34;10m\u0026#34; max-file: \u0026#34;3\u0026#34; configs: api_config: external: true volumes: logs: 原本的 api config 我們是透過 volume 掛載到 container 裡面，但是如果換成 docker swarm 的話，每一台 worker node 都會需要建立一個 config 文件，這樣才能讓每一個 container 都能夠讀取到 config 文件。為了要解決這個問題，我們會透過 docker swarm 的 config 功能，將 config 文件上傳到 swarm 叢集裡面，這樣每一個 container 都能夠讀取到 config 文件。\n創建 docker swarm config：\ndocker config create api_config config.json 創建以後，可以用 ls 指令查看 config 有沒有成功創建：\ndocker config ls 過程中遇到的問題❓ # 有遇到當我在 manager node 上已經登入過 docker registry 了，實際執行部署指令的時候，container 沒辦法成功在 worker node 上面運行。\n可以先透過指令查詢 stack 的 task 狀態：\ndocker stack ps \u0026lt;STACK_NAME\u0026gt; --no-trunc \u0026ndash;no-trunc 是為了要顯示完整的錯誤訊息，不被省略。這個層級的 log 資訊可以理解為 kubernetes deployment 的 event log。\n透過 stack 提供的 log 訊息，我發現 worker node 沒有辦法 pull image，因為 worker node 沒有登入 docker registry，那有什麼方式可以解決？\n我們可以在執行部署的時候，透過 \u0026ndash;with-registry-auth 參數，這個參數可以將 manager node 登入 docker registry 的認證資訊，傳遞給 worker node，因為通常我們只會在 manager node 執行部署，所以這個方式我認為是最好的解決方案。\ndocker stack deploy --with-registry-auth -c docker-compose.yml \u0026lt;STACK_NAME\u0026gt; 完成以後，可以透過以下指令查看 stack 的 service 狀態：\ndocker stack services \u0026lt;STACK_NAME\u0026gt; 如果發現 container 一直 crash 重啟，就需要繼續往 container 裡面查看 log，找出 container crash 的原因。\ndocker service logs \u0026lt;SERVICE_NAME\u0026gt; 如果服務都順利啟動了以後，就可以在 manager node 上面 curl 測試看看 api container 是否有正常運作。因為我啟動的 api container 是監聽在 3000 port，所以我就 curl 會測試 3000 port 是否有正常回應。\ncurl http://\u0026lt;MANAGER_IP\u0026gt;:3000/api/health 擴縮 container📦 # 這樣我們就完成了 docker swarm 的部署，接下來就可以嘗試使用 docker swarm 對 container 進行擴縮。\n可以透過以下指令：\ndocker service scale \u0026lt;SERVICE_NAME\u0026gt;=\u0026lt;NUMBER_OF_REPLICAS\u0026gt; 結論🎯 # 透過這次的實作跟了解 docker swarm 的運作方式，我們可以在尖峰時間快速的擴縮 container，這樣就可以省去繁瑣的 sop，減少了時間跟成本。\n","date":"24 June 2026","externalUrl":null,"permalink":"/journals/docker/docker_swarm/","section":"Journals","summary":"","title":"使用 Docker Swarm 管理 Container","type":"journals"},{"content":"","date":"18 June 2026","externalUrl":null,"permalink":"/tags/ci/","section":"Tags","summary":"","title":"Ci","type":"tags"},{"content":"","date":"18 June 2026","externalUrl":null,"permalink":"/tags/git-submodule/","section":"Tags","summary":"","title":"Git Submodule","type":"tags"},{"content":"","date":"18 June 2026","externalUrl":null,"permalink":"/tags/gitlab/","section":"Tags","summary":"","title":"Gitlab","type":"tags"},{"content":"","date":"18 June 2026","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":" 前言🔖 # 最近優化公司的前端專案的部署流程，發現原來的部署流程有點問題，流程大概是這樣的：\nflowchart classDef green fill:#dff0d8,stroke:#3c763d,stroke-width:2px,color:#000 classDef blue fill:#d9edf7,stroke:#31708f,stroke-width:2px,color:#000 classDef yellow fill:#fcf8e3,stroke:#8a6d3b,stroke-width:2px,color:#000 classDef red fill:#f2dede,stroke:#a94442,stroke-width:2px,color:#000 classDef gray fill:#ddd,stroke:#666,stroke-width:2px,color:#000 error([CI 錯誤，終止流程]):::red clone_forntend_success{克隆前端倉庫成功?}:::yellow clone_common_assets_success{克隆公共資源倉庫成功?}:::yellow start([開始]):::gray --\u003e clone_forntend_repo[克隆前端專案]:::green --\u003e clone_forntend_success clone_forntend_success -- 是 --\u003e clone_common_assets_repo[克隆公共資源倉庫]:::green clone_forntend_success -- 否 --\u003e error clone_common_assets_repo --\u003e clone_common_assets_success clone_common_assets_success -- 是 --\u003e mv_common_assets[將公共資源移動到前端專案]:::green clone_common_assets_success -- 否 --\u003e error mv_common_assets --\u003e docker_build[構建 Docker 映像]:::green --\u003e build_success{構建成功?}:::yellow build_success -- 是 --\u003e push_docker_image[推送 Docker 映像到註冊表]:::green --\u003e push_success{推送成功?}:::yellow build_success -- 否 --\u003e error push_success -- 否 --\u003e error push_success -- 是 --\u003e finish([結束]):::gray 從上面的流程圖可以發現，CI 在建構前端專案的 Docker 映像的時候，經歷兩次克隆倉庫的過程，如果我們可以用 Git Submodule 的方式來管理公共資源倉庫，那麼就可以省去一次克隆的過程，從而優化整個部署流程。除此之外也可以確保前端專案使用固定版本的共用資源，避免因為公共資源倉庫的更新而導致前端專案出現問題。整體來說，使用 Git Submodule 的方式可以讓我們的部署流程更加高效和穩定。\nGit Submodule 的使用🚀 # 如何使用 Git Submodule 呢？首先，我們需要在前端專案的 Git 倉庫中添加公共資源倉庫作為子模塊：\ngit submodule add \u0026lt;公共資源倉庫的 URL\u0026gt; \u0026lt;子模塊的路徑\u0026gt; 這裡作者會建議將\u0026lt;公共資源倉庫的 URL\u0026gt;改成 SSH 的 URL，這樣開發者就可以在本機電腦的專案中統一使用 SSH 的方式克隆前端專案跟公共資源倉庫，就不用在 Clone 的過程中還要輸入帳號密碼或個人的 Access Token 了。\n當我們添加完成子模塊之後，Git 會在前端專案的 Git 倉庫中創建一個特殊的文件 .gitmodules，這個文件用來記錄子模塊的信息。接者只需要提交這個文件到 Git 倉庫中，未來其他的小夥伴或者 CI 在克隆前端專案的時候，就可以通過 Git 一次性克隆前端專案和公共資源倉庫了。從 Git 2.13 版本及更高版本開始，\u0026ndash;recurse-submodules 可以使用以下方式取代 \u0026ndash;recursive：\ngit clone --recurse-submodules \u0026lt;前端專案的 URL\u0026gt; 如果子模組是在後期才新增到主專案的 Git 倉庫中，如果在本機電腦上看到主專案的 Git 倉庫中有 .gitmodules 文件，這個時候就可以使用以下方式來初始化子模組：\ngit submodule update --init --recursive 如果未來想要更新子模組的內容，如果團隊是透過 tags 的方式管理版本，可以使用以下方式：\ncd \u0026lt;子模組的路徑\u0026gt; git fetch --tags git checkout tags/\u0026lt;標籤名稱\u0026gt; 此時，主專案會偵測到子模組的指標(Commit ID)已經改變，這個時候需要回到主專案提交這個變更。\n如何撤銷 Git Submodule 的使用❌ # 如果未來想要撤銷 Git Submodule 的使用，可以按照以下步驟進行：\n移除 git submodule：\ngit submodule deinit -f \u0026lt;子模組的路徑\u0026gt; # 如果要一次取消所有的子模組 git submodule deinit --all 當執行完上面的命令之後，隱藏目錄 .git/modules/\u0026lt;子模組的路徑\u0026gt; 也會被刪除，.git/config 文件中關於子模組的配置也會被移除。 這時候就可以手動刪除子模組的目錄了：\nrm -rf \u0026lt;子模組的路徑\u0026gt; 最後，提交這些變更到 Git 倉庫中。\n優化過後的流程圖🗺️ # 導入了 Git Submodule 之後，整個部署流程就變成了下面這樣：\nflowchart classDef green fill:#dff0d8,stroke:#3c763d,stroke-width:2px,color:#000 classDef blue fill:#d9edf7,stroke:#31708f,stroke-width:2px,color:#000 classDef yellow fill:#fcf8e3,stroke:#8a6d3b,stroke-width:2px,color:#000 classDef red fill:#f2dede,stroke:#a94442,stroke-width:2px,color:#000 classDef gray fill:#ddd,stroke:#666,stroke-width:2px,color:#000 error([CI 錯誤，終止流程]):::red start([開始]):::gray --\u003e clone_forntend_repo[克隆前端專案]:::green --\u003e clone_forntend_success{克隆前端倉庫成功?}:::yellow clone_forntend_success -- 是 --\u003e docker_build[構建 Docker 映像]:::green --\u003e build_success{構建成功?}:::yellow clone_forntend_success -- 否 --\u003e error build_success -- 是 --\u003e push_docker_image[推送 Docker 映像到註冊表]:::green --\u003e push_success{推送成功?}:::yellow build_success -- 否 --\u003e error push_success -- 是 --\u003e finish([結束]):::gray push_success -- 否 --\u003e error 可以發現，整個流程變得更加簡潔了，CI 在建構前端專案的 Docker 映像的時候，只需要克隆一次前端專案的 Git 倉庫就可以了，因為公共資源倉庫已經作為子模塊被包含在前端專案的 Git 倉庫中了。\n也解決了未來小夥伴在開發的過程中，可以能會忘記要額外克隆公共資源倉庫的問題。\n過程中遇到的問題💡 # 在實作的過程中，在地端的 Jenkins 上測試 CI 流程的時候，都沒有任何的異常。但是今天我將相同的流程重新用 GitLab CI 來測試的時候，卻遇到了一個問題，GitLab CI 在克隆前端專案的 Git 倉庫的時候，無法克隆公共資源倉庫，出現了以下的錯誤訊息：\nUpdating/initializing submodules recursively with git depth set to 20... Submodule \u0026#39;public/image-assets\u0026#39; (ssh://git@gito.vastplay.online:22222/jackpot/image-assets.git) registered for path \u0026#39;public/image-assets\u0026#39; Synchronizing submodule url for \u0026#39;public/image-assets\u0026#39; Cloning into \u0026#39;/builds/jackpot/replay_og/public/image-assets\u0026#39;... error: cannot run ssh: No such file or directory fatal: unable to fork fatal: clone of \u0026#39;ssh://git@gito.vastplay.online:22222/jackpot/image-assets.git\u0026#39; into submodule path \u0026#39;/builds/jackpot/replay_og/public/image-assets\u0026#39; failed 可以看到，GitLab CI 在克隆公共資源倉庫的時候，出現了 error: cannot run ssh: No such file or directory 的錯誤訊息，代表說 gitlab runner 在執行克隆公共資源倉庫的命令的時候，無法找到 ssh 這個指令。\n最根本的原因有兩個，第一個是 gitlab runner 預設的 runner helper image 根本就沒有安裝 ssh 指令提供我們做使用。如果我們使用 Dockerfile 來建構自己的 runner helper image 的話，也需要將 ssh key 跟 known_hosts 的檔案放到 runner helper image 裡面，這樣才能夠讓 gitlab runner 在執行克隆公共資源倉庫的命令的時候，能夠使用 ssh key 來驗證身份。例如下面的範例：\nFROM registry.gitlab.com/gitlab-org/gitlab-runner/gitlab-runner-helper:x86_64-dcfb4b66 RUN apk add openssh-client-default RUN mkdir -p ~/.ssh RUN ssh-keyscan -H \u0026lt;your git host\u0026gt; \u0026gt;\u0026gt; ~/.ssh/known_hosts COPY id_rsa ~/.ssh/id_rsa RUN chmod 600 ~/.ssh/id_rsa 建構完成 image 之後，可以修改 gitlab runner 的 config.toml，將 helper_image 改成我們自己建構的 image，這樣就可以解決這個問題了。\n[[runners]] [runners.docker] helper_image = \u0026#34;your-custom-helper-image:latest\u0026#34; 問題思考💭 # 雖然說上面的方式可以解決這個問題，但是 gitlab 官方的文件是推薦我們可以使用 CI/CD JOB TOKEN 來克隆子模塊的 Git 倉庫的，這樣就不需要在 runner helper image 裡面安裝 ssh 指令了，也不需要在 runner helper image 裡面放入 ssh key 跟 known_hosts 的檔案了。\n要怎麼實作呢？我們可以在 gitlab-ci.yml 設定以下變數，讓 gitlab runner 在克隆子模塊之前，先將 ssh url 自動替換成 https url。\nvariables: GIT_SUBMODULE_STRATEGY: recursive GIT_SUBMODULE_FORCE_HTTPS: true 接著我們需要透過 CI/CD JOB TOKEN 來驗證身份。\n什麼是 CI/CD JOB TOKEN❓ # 當 CI/CD Pipeline 作業即將運作時，GitLab 會產生一個唯一 Token，並將其作為 $CI_JOB_TOKEN 的預定義變數提供給 Job。此 Token 僅在 Job 執行時有效。Job 結束後，Token 存取權限將被撤銷。\n設定子模組專案的存取權限🔐 # 設定好 gitlab-ci.yml 之後，還需要在子模組專案的設定頁面中，在 Settings \u0026gt; CICD \u0026gt; Job Token Permissions 中，將主專案的設定到子專案的 Job Token Permissions 中，這樣主專案的 CI/CD Pipeline 就可以使用 CI/CD JOB TOKEN 來克隆子模組專案了。\n可以參考以下圖片的設定：\njob token permissions 遇到的問題❓ # 將舊的專案導入 submodule 的時候，使用 git submodule add \u0026lt;公共資源倉庫的 URL\u0026gt; \u0026lt;子模塊的路徑\u0026gt; 的方式，會出現以下的錯誤訊息：\nfatal: \u0026lt;子模塊的路徑\u0026gt; already exists in the index 代表這個路徑可能有先前可能已經被 git 追蹤或者有殘留檔案，這個時候可以使用以下的方式來解決：\ngit rm -r --cached \u0026lt;子模塊的路徑\u0026gt; 結語🎯 # 透過 Git Submodule 的方式來管理公共資源倉庫，減少了冗餘的流程以及步驟。可以更精確的控制子模組的版本，也加速了 CI 的部署流程。當然在實作的過程中也遇到了一些問題，最後也找到了解決方案，讓整個流程變得更加順暢了。\n參考文獻📚 # Docker：錯誤：無法執行 ssh：沒有該檔案或目錄 CI/CD job token ","date":"18 June 2026","externalUrl":null,"permalink":"/journals/gitlab/gitlab-ci-job-token-clone-submodule/","section":"Journals","summary":"","title":"使用 Git Submodule 管理公共資源倉庫，優化 GitLab CI 部署流程","type":"journals"},{"content":"","date":"18 June 2026","externalUrl":null,"permalink":"/tags/%E9%83%A8%E7%BD%B2%E6%B5%81%E7%A8%8B/","section":"Tags","summary":"","title":"部署流程","type":"tags"},{"content":"","date":"13 June 2026","externalUrl":null,"permalink":"/tags/ai/","section":"Tags","summary":"","title":"AI","type":"tags"},{"content":"","date":"13 June 2026","externalUrl":null,"permalink":"/tags/cognitive-load/","section":"Tags","summary":"","title":"Cognitive Load","type":"tags"},{"content":"","date":"13 June 2026","externalUrl":null,"permalink":"/reads/","section":"Reads","summary":"","title":"Reads","type":"reads"},{"content":" 前言 # 最近的 AI 浪潮，相信各位讀者的身邊一定會使用各種 AI Agent 的代理人工具來幫助自己輕鬆的工作吧？但有沒有一種感覺，總覺得好像更累人了。面對 AI 閃電般的產出各式各樣的建議以及答案。各位讀者可能需要花費大量的時間閱讀建議以及答案。理解消化以後，接著在跟 AI 講說你要執行的決策。\n過程中，如果你要一直反覆的去了解新的知識技術，顯然會增加你的認知負荷。面對 AI 所提出的各種術語、最佳實踐等等的建議，我們是不是應該要停下來換一個你現階段擁有的技術力能做到最好的解決方式來處理工作上的任務。而不是盲目地聽從 AI 給你的最佳實踐呢？\n其實在目標明確的狀況下，告訴 AI 應該要做什麼反而也可以省一點錢。來回跟 AI 溝通對話有的時候很燒 token 的😮‍💨\n主導 AI，不要被剝奪主控權 # 現在的 AI 已經可以將一個大的項目拆成一筆一筆小小的任務了。但是這是 AI 的認知負荷，不是你的！AI 在接觸一個全新的陌生的專案項目時可以快速的理解專案架構，但不代表 AI 講給你聽後你就全部都懂了。\n請確實的理解過後，將工作內容開始切分成細小的任務項目，一項一項的交給 AI。AI 當然可以將工作再分成多個任務去執行。但已經不會增加你的認知負荷了。\n曾經在我身邊的同事使用 AI 時，總是喜歡一次丟給 AI 整體的工作內容，AI 會開始將工作內容拆成一筆一筆的任務，我的同事就要去讀一筆一筆的任務內容，如果有其中的任務內容超出了同事的理解能力了，同事問為什麼這樣做，AI 就會說最佳實踐是這樣做的。同事可能就盲目的相信了。\n在過程中，其實有些事情已經超出了同事的理解能力了，這時候要做的應該是將 AI 提供的做法，用現有的技術力能做到最好的解決方式來處理工作上的任務。如果真的必須要學習新的技術了，那麼就先學習這個技術，等到你真正理解了以後再來跟 AI 講說你要執行的決策。不要盲目的聽從 AI 給你的最佳實踐，因為這樣會增加你的認知負荷。\n與 AI 共同拆解任務 # AI 拆解大型任務時真的很快也很方便，過程中要確保你也有參與到任務的拆解過程中，如果某些任務內容你不理解了，也許可以調整成你現有的技術力能做到最好的解決方式來處理工作上的任務，或者是再將這個任務再拆成更小的任務，直到你能理解了為止。\n學習新技術，請按部就班 # 有個時候，AI 總是會提出一兩個你可能不是那麼熟悉的工具或技術。這時候如果要學習新知識或技術，請按部就班，了解基礎後。進行實作並驗證看看。人在掌握一門新的知識或技術時，本來就是需要反覆練習的。這是我們跟 AI 不同的地方。當你掌握了這個新的知識或技術了以後，請 AI 協作時就更不會被牽著鼻子走了。\n之前同事請我 Code Review 的時候，我問他為什麼要這樣寫。他冷冷的回了我一句：AI 說可以這樣寫，我也只是回應了一句：那你驗證過了嗎？他沒有回話，而我默默的把 MR 給 Close 了。\n結論 # 在 AI 的浪潮底下，所有事情都變得快速有效率。但是如果出現問題，我們還是必須要懂問題出在哪裡？或是提前知道問題可能會發生時，就將這件事情阻擋下來。\n最近也有公司使用 AI Agent 導致資料遺失等嚴重的事件，也許你責罵 AI 它可以很快速的跟你道歉，但畢竟是 AI，鍋還是你要背的。\n在這個答案很廉價的時代，如何問一個問題變成最有價值的技能。因為你有了解透過丟給 AI 問題，回覆的答案自然不會增加你的認知負荷，讓你的系統及專案都處在一個更可靠且安全的狀態下。\n參考文章 # 认知负荷才是关键 使用 AI 代理的三大原則。AI Agent 有什麼不確定性？如何限制其操作權限將是關鍵 ","date":"13 June 2026","externalUrl":null,"permalink":"/reads/cognitive-load/","section":"Reads","summary":"","title":"可以使用 AI 幫你工作，但請不要提高你的認知負荷","type":"reads"},{"content":"","date":"2 June 2026","externalUrl":null,"permalink":"/tags/elasticsearch/","section":"Tags","summary":"","title":"ElasticSearch","type":"tags"},{"content":"","date":"2 June 2026","externalUrl":null,"permalink":"/tags/log/","section":"Tags","summary":"","title":"Log","type":"tags"},{"content":"","date":"2 June 2026","externalUrl":null,"permalink":"/tags/log-collection/","section":"Tags","summary":"","title":"Log Collection","type":"tags"},{"content":"","date":"2 June 2026","externalUrl":null,"permalink":"/tags/log-design/","section":"Tags","summary":"","title":"Log Design","type":"tags"},{"content":" 前言🔖 # 為什麼會需要設計 log 的格式？\n不管是在開發過程或者是在運維的過程中，log 提供給開發者跟運維人員許多重要的資訊，可以從 log 中得知系統的狀態、有幾筆錯誤的異常資訊等\u0026hellip;，既然 log 有助於我們排查錯誤，那麼設計良好的 log 格式就可以幫助我們更快的找到問題所在，甚至在 log 中就可以直接看出問題的原因。\n設計原則🎯 # log 設計的基本方針就是根據使用的目的來設計 log 的格式，以下會先介紹幾個基礎的 log 格式設計原則：\n級別🎚 # log 的等級可以幫助我們快速的判斷 log 的重要程度，常見的 log 等級有：\n種類 說明 FATAL 發生致命錯誤的狀態 ERROR 發生錯誤的狀態 WARN 發生警告的狀態 NOTICE 正常但重要的狀態 INFO 系統資訊 DEBUG 有關於系統如何運作的訊息 為什麼會需要分這個多的等級？除了可以透過搜尋引擎快速的定位到重要的 log 以外，也可以幫助在不同的產品環境中調整要輸出的 log 等級，例如在開發環境中可能會輸出 DEBUG 等級的 log，而在生產環境中則只輸出 ERROR 等級的 log。\n如果 log 是蒐集到 SaaS 平台上，例如 Datadog\u0026hellip;，在特定的環境中只輸出特定等級的 log 也可以幫助我們節省成本，因為 SaaS 平台通常是按照 log 的容量來收費的。\n血淚史🥲 # 之前有同事在生產環境中為了要找生產環境的問題，將 log 輸出的級別調整為 TRACE，排查完問題以後忘記調整回來，導致生產環境中輸出大量的 log，在月底時 Datadog 的帳單金額暴增了幾萬美金。\n輸出的位置📂 # log 的輸出位置需要開發人員與運維人員共同協商決定，如果是輸出 log 文件建議創建一個 logs 的資料夾來存放 log 文件，這樣可以幫助我們更好的管理 log 文件。\n目前在內部常見的 log 輸出位置的資料夾路徑有兩個：\n~/app/${app_name}/logs ~/app/${app_name}/log 建議使用第一個路徑，因為目前管理的服務內有些會同時輸出不同的 log 文件，例如 task.log、error.log\u0026hellip;。\nlog 文件名稱的命名建議使用以下的格式：\ndefault.log 不建議的命名格式：\n${app_name}-${date}.log 在相同的設定條件(*.log)下，單一的變數名稱可以幫助 log agnet 在跟讀 file 時，花費較少的記憶體。\ngraph LR log_agent(agent) --\u003e|跟讀| log_file(default.log) graph LR log_agent(log agent) --\u003e|跟讀| log_file1(app-2026-05-02.log) log_agent(log agent) --\u003e|跟讀| log_file2(app-2026-05-02.01.log) log_agent(log agent) --\u003e|跟讀| log_file3(app-2026-05-02.02.log) 問題思考：log 文件如何進行 rotation？🔄 # 使用 logrotate 來進行 log 文件的 rotation，進行 rotation 後的 log 文件會重新命名成以下的結構：\nlogs/ ├── default.log ├── default.log-2026-05-05 ├── default.log-2026-05-04 ├── default.log-2026-05-03 └── default.log-2026-05-02 這樣輪替以後的 log 文件名稱也不會被 log agent 繼續跟讀，因為 log agent 只會跟讀 default.log 這個文件。\nlog 的格式📝 # log 的格式建議使用 JSON 格式，因為 JSON 格式可以幫助我們更好的結構化 log 的內容，並且可以幫助我們更快的解析 log 的內容。\nlog 的格式或是內容通常會因為不同的產品或是服務而有所不同，以下是一個常見的 log 格式範例：\n{ \u0026#34;level\u0026#34;: \u0026#34;error\u0026#34;, string \u0026#34;timestamp\u0026#34;: \u0026#34;2026-06-02T08:53:04+08:00\u0026#34;, datetime \u0026#34;method\u0026#34;: \u0026#34;GET\u0026#34;, string \u0026#34;path\u0026#34;: \u0026#34;/api/v1/users\u0026#34;, string \u0026#34;full_url\u0026#34;: \u0026#34;http://example.com/api/v1/users\u0026#34;, string \u0026#34;status_code\u0026#34;: 500, integer \u0026#34;latency_seconds\u0026#34;: 0.025, float \u0026#34;msg\u0026#34;: \u0026#34;Internal Server Error\u0026#34;, string \u0026#34;request\u0026#34;: {}, object \u0026#34;response\u0026#34;: {}, object \u0026#34;game_id\u0026#34;: \u0026#34;01\u0026#34; string } 注意：大小寫的問題，建議使用小寫的 key，這樣在使用 log agent 解析 log 的時候可以避免大小寫不一致的問題。\n問題思考：log 應該要包含哪些欄位？🤔 # 輸出上我們可以依據 5W1H 的原則來設計，並且加上必要的資訊，不應該過多或不足。\nWhen \u0026ndash;\u0026gt; 何時執行該程序 Who \u0026ndash;\u0026gt; 是誰執行該程序 Where \u0026ndash;\u0026gt; 該程序在哪裡執行 What \u0026ndash;\u0026gt; 該程序執行了什麼 Why \u0026ndash;\u0026gt; 為什麼執行該程序 ElasticSearch 有什麼 log 的限制？⚠️ # ElasticSearch 在處理 log 的時候，會有一些限制需要注意：\nindex 中 log 欄位的型別突然轉換了，例如之前是 string 的欄位突然變成 integer，這樣就會導致 ElasticSearch 拒絕接受新的 log，因為 ElasticSearch 會認為這是一個 mapping 的錯誤。 index 中 log 欄位數量上限，ElasticSearch 預設的 index 中 log 欄位數量上限是 1000。 agent 如何收集特定級別的 log？🤔 # 開發人員定義好 log 文件的分類，例如 default.log、error.log、debug.log，開發人員透過設定決定輸出哪些級別的 log 到特定的 log 文件中，再由 log agent 來跟讀特定的 log 文件，這樣就可以達到收集特定級別的 log 的目的。 graph LR app_config(app config) --\u003e|設定| app(app) app(app) --\u003e|輸出| log_file1(default.log) app(app) --\u003e|輸出| log_file2(error.log) log_agent(log agent) --\u003e|跟讀| log_file1(default.log) log_agent(log agent) --\u003e|跟讀| log_file2(error.log) log_agent(log agent) --\u003e|跟讀| log_file3(debug.log) 開發人員將 log 輸出到同一個 log 文件中，例如 default.log，運維人員透過 log agent 的設定來過濾特定級別的 log，例如只收集 error 級別的 log，這樣就可以達到收集特定級別的 log 的目的。 graph LR app_config(app config) --\u003e|設定| app(app) app(app) --\u003e|輸出| log_file(default.log) log_agent(log agent) --\u003e|跟讀| log_file(default.log) log_agent(log agent) --\u003e|level: error| es(elastic search) 參考文獻 # Day28 - 事到如今問不出口的 Log 基礎和設計指南 ","date":"2 June 2026","externalUrl":null,"permalink":"/posts/elastic/log_format/","section":"Posts","summary":"","title":"Log 設計 \u0026 如何有效收集特定級別的 Log","type":"posts"},{"content":"","date":"2 June 2026","externalUrl":null,"permalink":"/posts/","section":"Posts","summary":"","title":"Posts","type":"posts"},{"content":"","date":"27 May 2026","externalUrl":null,"permalink":"/tags/commitlint/","section":"Tags","summary":"","title":"Commitlint","type":"tags"},{"content":"","date":"27 May 2026","externalUrl":null,"permalink":"/tags/git/","section":"Tags","summary":"","title":"Git","type":"tags"},{"content":"","date":"27 May 2026","externalUrl":null,"permalink":"/tags/%E5%B7%A5%E5%85%B7/","section":"Tags","summary":"","title":"工具","type":"tags"},{"content":" 前言🔖 # 上一篇文章提到了專案中使用 commitlint 工具來規範 commit message 的格式，確保團隊成員在提交代碼時遵循統一的規範。這不僅有助於提高代碼的可讀性，還能促進團隊協作和版本控制的管理。\n為什麼需要導入 Lint 工具?因為人類是有惰性的😩\n就算有文件規範了提交訊息的格式，但如果沒有工具來強制執行，團隊成員可能會忽略這些規範，導致提交訊息不一致，影響代碼庫的整潔和可維護性\n如何使用 commitlint 工具？ # 首先，我們需要安裝 commitlint 以及相關的配置套件：\nyarn add --dev @commitlint/{config-conventional,cli} # 或者使用 npm npm install --save-dev @commitlint/{config-conventional,cli} echo \u0026#34;export default { extends: [\u0026#39;@commitlint/config-conventional\u0026#39;] };\u0026#34; \u0026gt; commitlint.config.js 接著，安裝 husky 來在 git commit 時自動執行 commitlint：\nyarn add --dev husky # 或者使用 npm npm install --save-dev husky 基礎配置⚙️ # 安裝完套件後，我們需要進行一些基礎配置，使 commitlint 和 husky 能夠正常運作。\n配置 commitlint.config.js： module.exports = { extends: [\u0026#39;@commitlint/config-conventional\u0026#39;], }; 這個配置文件告訴 commitlint 使用 @commitlint/config-conventional 的規則來檢查提交訊息。\n配置 husky： yarn husky init 這個命令會在專案根目錄下創建一個 .husky 目錄\n接著，配置 commit-msg hook，使其在提交訊息時自動檢查訊息格式：\necho \u0026#34;yarn commitlint --edit \\$1\u0026#34; \u0026gt; .husky/commit-msg 常用 Rules 規則 # commitlint 允許我們可以自定義規則來檢查提交訊息的格式。以下是作者常用的一些規則：\nconst typeEnum = [ \u0026#34;feat\u0026#34;, // 新功能/修改功能 \u0026#34;fix\u0026#34;, // 修復錯誤 \u0026#34;docs\u0026#34;, // 文檔變更 \u0026#34;style\u0026#34;, // 不影響代碼含義的變更 (空格, 格式化, 缺少分號等) \u0026#34;refactor\u0026#34;, // 代碼重構 \u0026#34;perf\u0026#34;, // 性能優化 \u0026#34;test\u0026#34;, // 添加缺失的測試或更正現有測試 \u0026#34;build\u0026#34;, // 更改構建流程或輔助工具和庫 (例如 webpack, babel, npm) \u0026#34;ci\u0026#34;, // 更改 CI 配置, 腳本 \u0026#34;chore\u0026#34;, // 對構建過程或輔助工具和庫的更改 \u0026#34;revert\u0026#34;, // 撤銷之前的提交 ]; module.exports = { extends: [\u0026#34;@commitlint/config-conventional\u0026#34;], rules: { \u0026#34;type-enum\u0026#34;: [2, \u0026#34;always\u0026#34;, typeEnum], \u0026#34;subject-format\u0026#34;: [2, \u0026#34;always\u0026#34;, /^[a-z]+: .+$/i], \u0026#34;subject-jira-format\u0026#34;: [1, \u0026#34;always\u0026#34;, /^[a-z]+: \\[.*\\] .+$/i], }, parserPreset: { parserOpts: { headerPattern: /^(\\w+): (.+)$/, headerCorrespondence: [\u0026#34;type\u0026#34;, \u0026#34;subject\u0026#34;], }, }, plugins: [ { rules: { \u0026#34;type-enum\u0026#34;: ({ type }) =\u0026gt; { const isValidType = typeEnum.includes(type); return [ isValidType, isValidType ? true : `輸入的 type \u0026#39;${type}\u0026#39; 不在允許的範圍內: ${typeEnum.join( \u0026#34;, \u0026#34; )}。`, ]; }, \u0026#34;subject-format\u0026#34;: (parsed) =\u0026gt; { const regex = /^[a-z]+: .+$/i; const isValidFormat = regex.test(parsed.header); return [ isValidFormat, isValidFormat ? true : `請確保格式為 \u0026lt;type\u0026gt;: \u0026lt;描述\u0026gt;，例如：feat: 新增功能。`, ]; }, \u0026#34;subject-jira-format\u0026#34;: (parsed) =\u0026gt; { const regex = /^[a-z]+: \\[.*\\] .+$/i; const isJiraFormat = regex.test(parsed.header); return [ isJiraFormat, isJiraFormat ? true : `建議格式為 \u0026lt;type\u0026gt;: [JIRA-ISSUE] \u0026lt;描述\u0026gt;，例如：feat: [JIRA-123] 新增功能。`, ]; }, }, }, ], }; 在 rules 中，我們先來解釋陣列內的各個元素代表什麼意思：\n第一個元素（0, 1, 2）代表規則的嚴重程度，0 表示關閉規則，1 表示警告，2 表示錯誤。 第二個元素（\u0026ldquo;never\u0026rdquo;, \u0026ldquo;always\u0026rdquo;）表示規則的觸發條件，\u0026ldquo;never\u0026rdquo; 表示不允許使用某些值，\u0026ldquo;always\u0026rdquo; 表示必須符合規則。 在這個配置中，我們定義了三個規則：\n\u0026ldquo;type-enum\u0026rdquo;: 這個規則檢查提交訊息的 type 是否在允許的範圍內。如果不符合，會返回一個錯誤訊息，提示使用者輸入的 type 不在允許的範圍內。 \u0026ldquo;subject-format\u0026rdquo;: 這個規則檢查提交訊息的格式是否符合 : \u0026lt;描述\u0026gt; 的格式。如果不符合，會返回一個錯誤訊息，提示使用者確保格式正確。 \u0026ldquo;subject-jira-format\u0026rdquo;: 這個規則檢查提交訊息的格式是否符合 : [JIRA-ISSUE] \u0026lt;描述\u0026gt; 的格式。如果不符合，會返回一個警告訊息，建議使用者遵循這個格式。 plugins 插件 # plugins 的作用是用來追加新的自定義規則，與 rules 相輔相成。\n參考文獻📚 # 專業協作開發：使用 Commitlint 規範你的提交訊息 commitlint ","date":"27 May 2026","externalUrl":null,"permalink":"/journals/git/commitlint-tool/","section":"Journals","summary":"","title":"使用 commitlint 工具規範提交訊息格式","type":"journals"},{"content":"","date":"27 May 2026","externalUrl":null,"permalink":"/tags/%E8%A6%8F%E7%AF%84/","section":"Tags","summary":"","title":"規範","type":"tags"},{"content":"","date":"27 May 2026","externalUrl":null,"permalink":"/tags/%E6%8F%90%E4%BA%A4%E8%A8%8A%E6%81%AF/","section":"Tags","summary":"","title":"提交訊息","type":"tags"},{"content":"","date":"25 May 2026","externalUrl":null,"permalink":"/tags/commit-message/","section":"Tags","summary":"","title":"Commit Message","type":"tags"},{"content":"","date":"25 May 2026","externalUrl":null,"permalink":"/tags/conventional-commits/","section":"Tags","summary":"","title":"Conventional Commits","type":"tags"},{"content":" 前言🔖 # 在軟體過程開發中，良好的 Git Commit 規範是維護專案歷史清晰、協作順暢的關鍵。想像一下，如果每次提交都像寫日記一樣，清楚描述改動內容和原因，未來回顧時就能輕鬆理解每次變更的背景。\n如何撰寫優質的 Git Commit Message？ # 在團隊開發中，常常會遇到提交訊息不清晰。會有一大堆的 \u0026ldquo;fix bug\u0026rdquo;、\u0026ldquo;update code\u0026rdquo; 這類模糊的訊息，讓人無法快速了解改動的內容和目的。為了改善這個問題，我們可以遵循 Conventional Commits 的規範，這是一套簡單而強大的提交訊息格式，能夠幫助我們更好地管理專案歷史。\nConventional Commits 的基本格式 # Conventional Commit 的格式其實很簡單，看起來像這樣：\n\u0026lt;type\u0026gt;[optional scope]: \u0026lt;description\u0026gt; [optional body] [optional footer(s)] 讓我們來分解一下每個部分的用途：\ntype(commit 類型)：描述這次 Commit 的性質。常見的類型有：\nfeat: 新功能 fix: 修復 bug docs: 文件變更 style: 代碼格式（不影響功能） refactor: 代碼重構（既不是新增功能也不是修復 bug） test: 添加測試 chore: 其他修改（例如建置流程、工具等） revert: 回退之前的提交 也可以根據團隊需求自定義其他類型，比如説在 SRE 團隊中，我們還有定義其他的類型，例如：\nperf: 性能優化 build: 更改構建流程或輔助工具和庫 (例如 webpack, babel, npm) ci: 持續集成相關的更改 cd: 持續部署相關的更改 scope(影響範圍 可選)：描述這次 Commit 影響的範圍，例如模組名稱、功能區域等。如果影響的範圍明確建議可以加上，可以讓訊息更具體。\ndescription(簡短描述)：簡要描述這次 Commit 的內容，應該是一句話，清晰明了地說明改動的目的。保持在 50 字以內。\nbody(詳細描述 可選)：如果需要，可以在這裡提供更詳細的說明，解釋為什麼要做這些改動，以及改動的具體內容。\nfooter(附註 可選)：可以用來關聯 issue 或 pull request，例如：\nBREAKING CHANGE: 改變了 API 的回傳結構 Resolves #123 實踐範例 # 新增功能提交\nfeat(profile): add user profile page Bug 修復提交\nfix(auth): resolve login issue when using special characters in password Breaking Change\nfeat(api): change user endpoint response format BREAKING CHANGE: 改變了 API 的回傳結構，請更新前端代碼以適應新的格式 文件變更提交\ndocs(readme): update installation instructions 退回某次提交\nrevert: feat(profile): add user profile page This reverts commit abcdef1234567890. 實用工具推薦 # 當然，撰寫團隊的 Git Commit 規範只是第一步，實際上在推動團隊遵守這些規範時，還是會遇到一些挑戰。\n接續的文章段落會延續介紹作者有使用的工具，幫助團隊更好地遵守 Git Commit 規範。\nCommitlint # 功能：Commitlint 是一款校驗工具，用來檢查你的 Commit Message 是否符合規範，避免提交不合規的訊息。 特色：可整合到 CI/CD 流程中，自動化檢查提交訊息，支援自定義規則，適應不同團隊的需求。 Husky # 功能：Husky 是一款 Git hooks 工具，可以在提交前自動執行指定的腳本，例如 Commitlint 的檢查。 特色：配合 Commitlint 使用，實現提交訊息的本地校驗。可用於其他操作，如執行測試、格式化程式碼等，擴展性強。 如果想要在專案中引入這些工具，下一篇文章將會介紹如何安裝和配置 Commitlint 和 Husky，讓你的團隊能夠輕鬆遵守 Git Commit 規範。\n使用 commitlint 工具規範提交訊息格式 27 May 2026\u0026middot;2 分鐘 Git Commitlint 工具 提交訊息 規範 參考文獻📚 # Git Commit Message 這樣寫會更好，替專案引入規範與範例 Conventional Commits 的實踐指南：寫出乾淨的提交訊息 ","date":"25 May 2026","externalUrl":null,"permalink":"/journals/git/commit-message/","section":"Journals","summary":"","title":"Git Commit Message 的撰寫規範與工具推薦","type":"journals"},{"content":"","date":"25 May 2026","externalUrl":null,"permalink":"/tags/%E5%B7%A5%E5%85%B7%E6%8E%A8%E8%96%A6/","section":"Tags","summary":"","title":"工具推薦","type":"tags"},{"content":"","date":"25 May 2026","externalUrl":null,"permalink":"/tags/%E9%96%8B%E7%99%BC%E8%A6%8F%E7%AF%84/","section":"Tags","summary":"","title":"開發規範","type":"tags"},{"content":"","date":"20 April 2026","externalUrl":null,"permalink":"/tags/cluster-deploy/","section":"Tags","summary":"","title":"Cluster-Deploy","type":"tags"},{"content":"","date":"20 April 2026","externalUrl":null,"permalink":"/tags/docker-compose/","section":"Tags","summary":"","title":"Docker-Compose","type":"tags"},{"content":" 前言🔖 # elasticsearch 對於可觀測性來說是非常重要的，尤其是在分散式系統中。它可以幫助我們收集、分析和可視化來自不同來源的數據，從而更好地理解系統的運行狀況和性能。\n以下範例會使用 docker-compose 來部署一個簡單的 elasticsearch 集群，包含三個節點。 使用的 elasticsearch 版本為 8.9.2，kibana 版本為 8.9.2。\n在我的 github 上也有這個範例的完整程式碼，歡迎參考：elasticsearch_example\n部署步驟🚀 # 首先，我們需要創建一個 docker-compose.yml 文件，內容如下：\nservices: es01: image: docker.elastic.co/elasticsearch/elasticsearch:8.9.2 container_name: es01 restart: unless-stopped environment: - node.name=es01 - cluster.name=es-docker-cluster - discovery.seed_hosts=es02,es03 - cluster.initial_master_nodes=es01,es02,es03 - bootstrap.memory_lock=true - \u0026#34;ES_JAVA_OPTS=-Xms2g -Xmx2g\u0026#34; - \u0026#34;xpack.security.enabled=false\u0026#34; - \u0026#34;indices.memory.index_buffer_size=30%\u0026#34; ulimits: memlock: soft: -1 hard: -1 volumes: - ./database/esdata1:/usr/share/elasticsearch/data ports: - \u0026#34;9200:9200\u0026#34; logging: driver: \u0026#34;json-file\u0026#34; options: max-size: \u0026#34;10m\u0026#34; max-file: \u0026#34;3\u0026#34; deploy: resources: limits: cpus: \u0026#34;1\u0026#34; memory: 4G reservations: cpus: \u0026#34;0.5\u0026#34; memory: 2G networks: - elk es02: image: docker.elastic.co/elasticsearch/elasticsearch:8.9.2 container_name: es02 restart: unless-stopped environment: - node.name=es02 - cluster.name=es-docker-cluster - discovery.seed_hosts=es01,es03 - cluster.initial_master_nodes=es01,es02,es03 - bootstrap.memory_lock=true - \u0026#34;ES_JAVA_OPTS=-Xms2g -Xmx2g\u0026#34; - \u0026#34;xpack.security.enabled=false\u0026#34; - \u0026#34;indices.memory.index_buffer_size=30%\u0026#34; ulimits: memlock: soft: -1 hard: -1 volumes: - ./database/esdata2:/usr/share/elasticsearch/data ports: - \u0026#34;9201:9200\u0026#34; logging: driver: \u0026#34;json-file\u0026#34; options: max-size: \u0026#34;10m\u0026#34; max-file: \u0026#34;3\u0026#34; deploy: resources: limits: cpus: \u0026#34;1\u0026#34; memory: 4G reservations: cpus: \u0026#34;0.5\u0026#34; memory: 2G networks: - elk es03: image: docker.elastic.co/elasticsearch/elasticsearch:8.9.2 container_name: es03 restart: unless-stopped environment: - node.name=es03 - cluster.name=es-docker-cluster - discovery.seed_hosts=es01,es02 - cluster.initial_master_nodes=es01,es02,es03 - bootstrap.memory_lock=true - \u0026#34;ES_JAVA_OPTS=-Xms2g -Xmx2g\u0026#34; - \u0026#34;xpack.security.enabled=false\u0026#34; - \u0026#34;indices.memory.index_buffer_size=30%\u0026#34; ulimits: memlock: soft: -1 hard: -1 volumes: - ./database/esdata3:/usr/share/elasticsearch/data ports: - \u0026#34;9202:9200\u0026#34; logging: driver: \u0026#34;json-file\u0026#34; options: max-size: \u0026#34;10m\u0026#34; max-file: \u0026#34;3\u0026#34; deploy: resources: limits: cpus: \u0026#34;1\u0026#34; memory: 4G reservations: cpus: \u0026#34;0.5\u0026#34; memory: 2G networks: - elk kibana: image: docker.elastic.co/kibana/kibana:8.9.2 container_name: kibana restart: unless-stopped environment: - ELASTICSEARCH_HOSTS=[\u0026#34;http://es01:9200\u0026#34;,\u0026#34;http://es02:9200\u0026#34;,\u0026#34;http://es03:9200\u0026#34;] - SERVER_HOST=0.0.0.0 - SERVER_NAME=kibana ports: - \u0026#34;5601:5601\u0026#34; logging: driver: \u0026#34;json-file\u0026#34; options: max-size: \u0026#34;10m\u0026#34; max-file: \u0026#34;3\u0026#34; deploy: resources: limits: cpus: \u0026#34;0.5\u0026#34; memory: 1G reservations: cpus: \u0026#34;0.25\u0026#34; memory: 0.5G depends_on: - es01 - es02 - es03 networks: - elk networks: elk: driver: bridge 接著，說明一下這個 docker-compose.yml 需要注意的細節以及內容：\n服務定義：我們定義了四個服務：es01、es02、es03 和 kibana。前三個是 Elasticsearch 節點，最後一個是 Kibana。 環境變量：每個 Elasticsearch 節點都設置了必要的環境變量，如 node.name、cluster.name、discovery.seed_hosts 和 cluster.initial_master_nodes，這些變量確保了節點之間的通信和集群的正確組建。 資源限制：我們為每個服務設置了 CPU 和內存的限制和預留，以確保它們在運行時有足夠的資源。 數據持久化：每個 Elasticsearch 節點都將數據存儲在主機的 /database/esdataX 目錄中，這樣即使容器重啟，數據也不會丟失。 網絡配置：我們使用了一個名為 elk 的自定義橋接網絡，這樣服務之間可以通過服務名稱互相通信。 補充說明：\nbootstrap.memory_lock=true：這個設置允許 Elasticsearch 鎖定內存，防止它被交換到磁盤上，這對於性能非常重要。 如何驗證記憶體鎖定是否成功： GET _nodes?filter_path=**.mlockall 如果返回 true，則表示記憶體鎖定成功。\nES_JAVA_OPTS=-Xms8g -Xmx8g：這個設置指定了 Elasticsearch 的 Java 堆內存大小，建議設置為物理內存的一半，但不超過 32GB。 xpack.security.enabled=false：這個設置禁用了 Elasticsearch 的安全功能，這樣我們就不需要設置用戶名和密碼來訪問 Elasticsearch。 indices.memory.index_buffer_size=30%：這個設置指定了 Elasticsearch 用於索引緩衝區的內存大小，預設為 10%，因為日誌量較大，所以我們將其增加到 30%。建議根據實際情況調整這個值。 logging：我們使用了 json-file 日誌驅動程序，並設置了日誌文件的最大大小和數量，以防止日誌文件過大。 啟動集群 # 在 docker-compose.yml 文件所在的目錄中運行以下命令來啟動集群：\ndocker-compose up -d 啟動群集後，可以使用以下命令來檢查服務的狀態：\ndocker ps 可以在終端機上面看到四個容器正在運行，分別是 es01、es02、es03 和 kibana。\ndocker ps 訪問 Kibana # 打開瀏覽器，訪問 http://localhost:5601，你應該能看到 Kibana 的歡迎頁面。這表示你的 Elasticsearch 集群已經成功部署並且 Kibana 可以連接到它。\nKibana Welcome Page ","date":"20 April 2026","externalUrl":null,"permalink":"/posts/elastic/cluster-deploy/","section":"Posts","summary":"","title":"Elasticsearch 集群部署指南：使用 Docker Compose 快速搭建","type":"posts"},{"content":"","date":"31 March 2026","externalUrl":null,"permalink":"/tags/gce/","section":"Tags","summary":"","title":"GCE","type":"tags"},{"content":" 前言🔖 # 最近需要幫助 Elasticsearch 的 VM 增加磁碟容量，過程中參考了 Google Cloud 的官方文件，發現增加磁碟容量的過程其實非常簡單，主要分為兩個步驟：第一步是增加磁碟容量，第二步是讓作業系統認識新的磁碟容量。\n實作步驟📌 # 增加磁碟容量🔍 # 首先第一點就是要增加磁碟容量，讀者可以使用 Google Cloud Console 的 UI 介面來增加磁碟容量，或者是使用 gcloud CLI 的方式來增加磁碟容量。\n注意: 這裡的 DISK_NAME 、DISK_SIZE 和 ZONE 都需要替換成實際的磁碟名稱、磁碟大小和所在的區域。DISK_SIZE 的單位是 GB，例如如果要增加到 100GB 的話，就需要將 DISK_SIZE 設定為 100GB。\ngcloud compute disks resize ${DISK_NAME} \\ --size ${DISK_SIZE} \\ --zone=${ZONE} 當然如果你的團隊有使用 IaC 的工具來管理 GCE 的資源的話，例如: Terraform 的話，也可以直接在 Terraform 的配置檔中修改磁碟的大小，然後執行 terraform apply 來增加磁碟容量。\n讓作業系統認識新的磁碟容量🔍 # 當增加磁碟容量以後，接下來就需要讓作業系統認識新的磁碟容量。\n首先使用 df 和 lsblk 指令列出檔案系統的大小，並找出磁碟的裝置名稱。\ndf的輸出結果 lsblk的輸出結果 上面的範例中包含以下磁碟:\n/dev/sda：這是系統磁碟，通常是用來安裝作業系統的磁碟(開機磁碟)。 /dev/sdb：這是資料磁碟，通常是用來存放資料的磁碟(非開機磁碟)。 可以看到 /dev/sdb 在 VM 中掛載的目錄路徑是 /database，而且目前的磁碟容量是 2TB。但是因為已經調整過磁碟容量了，使用 lsblk 指令可以看到磁碟的大小已經變成 4TB 了。\n重新讀取磁碟新的分區，擴增磁碟大小以前。需要先確認分割區類型，可以使用 sudo parted -l 指令來查看磁碟的分割區類型。\nsudo parted -l parted的輸出結果 上面的範例中可以看到 /dev/sdb 的分割區類型是 xfs 的分割區類型。所以需要使用 sudo xfs_growfs 指令來擴增磁碟大小。\nsudo xfs_growfs /database 如果是 ext4 的分割區類型的話，就需要使用 sudo resize2fs 指令來擴增磁碟大小，例如：\nsudo resize2fs /dev/sdb1 當執行完擴增磁碟大小的指令以後，再次使用 df 指令來確認磁碟的容量已經增加了。\ndf的輸出結果 參考文獻📚 # 變更永久磁碟的大小 ","date":"31 March 2026","externalUrl":null,"permalink":"/journals/gcp/gce-increased-disk-capacity/","section":"Journals","summary":"","title":"GCE VM 增加磁碟容量的實作步驟分享","type":"journals"},{"content":"","date":"31 March 2026","externalUrl":null,"permalink":"/tags/gcp/","section":"Tags","summary":"","title":"Gcp","type":"tags"},{"content":"","date":"31 March 2026","externalUrl":null,"permalink":"/tags/google-cloud/","section":"Tags","summary":"","title":"Google Cloud","type":"tags"},{"content":"","date":"31 March 2026","externalUrl":null,"permalink":"/tags/vm/","section":"Tags","summary":"","title":"VM","type":"tags"},{"content":"","date":"31 March 2026","externalUrl":null,"permalink":"/tags/%E7%A3%81%E7%A2%9F%E5%AE%B9%E9%87%8F/","section":"Tags","summary":"","title":"磁碟容量","type":"tags"},{"content":" 前言🔖 # 步入職場後，發現 Git 是一個不可或缺的工具。但是發現對於周遭的同事來說，很多人對 Git 的理解都停留在表面，甚至有些人連基本的 commit、push 都不太清楚。這讓我覺得有必要寫一篇文章來介紹 Git 的基本概念和使用方法，希望能幫助到那些剛接觸 Git 的人。\n以下的教學是作者在工作中實際使用 Git 的經驗總結，並且參考了官方文檔和一些優秀的教程。希望能夠幫助到大家更好地理解和使用 Git。\nGit 是什麼？🤔 # 簡單來說，Git 是一個分布式版本控制系統。它可以幫助我們管理和跟蹤代碼的變化，讓我們能夠輕鬆地協作開發。Git 的核心概念包括：\nRepository（倉庫）：用來存儲代碼和版本歷史的地方。可以是本地的，也可以是遠程的。 Commit（提交）：一次對代碼的修改，包含了修改的內容和相關的元數據（如作者、時間等）。 Branch（分支）：用來實現並行開發的機制，可以讓不同的開發者在不同的分支上工作，最後再合併到主分支。 Merge（合併）：將一個分支的修改合併到另一個分支的過程。 Pull Request（拉取請求）：在遠程倉庫中，當你完成了一個功能的開發，想要將它合併到主分支時，可以創建一個 Pull Request，讓其他人來審核你的代碼。 上述的這幾個功能是在日常開發中與工作實務中最常用到的 Git 功能，當然 Git 還有很多其他的功能和概念，這裡就不一一介紹了。\nGit 的基本使用方法🚀 # 1. 初始化倉庫 # 如果今天管理者分配了一個新的專案給你，你需要在公司的 Git 服務（如 GitHub、GitLab 等）上創建一個新的倉庫，要如何初始化這個倉庫呢？你可以使用以下命令：\ngit init 這個命令會在當前目錄下創建一個新的 Git 倉庫，並且會生成一個 .git 的隱藏文件夾，這個文件夾用來存儲 Git 的相關信息。\n如果先在 Git 服務上創建了倉庫，然後想要將本地的代碼推送到遠程倉庫，可以使用以下命令：\ngit remote add origin \u0026lt;遠程倉庫的URL\u0026gt; git push -u origin master # 或者 main，取決於你的主分支名稱 這樣一來，你就成功地將本地的代碼推送到了遠程倉庫，並且設置了 origin 作為默認的遠程倉庫，接下來就可以開始提交程式碼了。\n2. 建立分支 # 在多人協作開發中，建立分支是一個非常重要的步驟。分支可以讓不同的開發者在不同的分支上工作，避免了直接在主分支上修改代碼可能帶來的風險。\n可以從圖片中看到，今天要為產品開發一個新的功能，為了不影響主分支的穩定性，我們可以創建一個新的分支來開發這個功能。使用以下命令來創建和切換到新的分支：\ngit checkout -b feature/new-feature 切換分支以後，你就可以在這個分支上進行開發了。當你完成了這個功能的開發，並且測試通過了，就可以將這個分支合併回主分支了。\n以上是比較簡單的說明，實務上還是要依照各家公司的流程來進行，例如: 功能分支需要經過 code review 才能合併到主分支，或者是需要先將功能分支合併到 develop 分支，再由 develop 分支合併到主分支等等，這些都是公司內部的流程規定，需要根據實際情況來操作。\n--- title: \"Git 分支示意圖\" --- gitGraph commit commit branch feature/new-feature checkout feature/new-feature commit commit checkout main merge feature/new-feature 3. 提交程式碼 # 當你在功能分支上完成了開發，並且測試通過了，就可以將這些修改提交到 Git 中了。使用以下命令來提交程式碼：\ngit add . # 將所有修改的文件添加到暫存區 git commit -m \u0026#34;feat: add new feature\u0026#34; # 提交修改，並且添加提交信息 上述指令完成後，在本機電腦上就會有一個新的提交，這個提交包含了你剛才修改的內容和提交信息。接下來，你可以將這個提交推送到遠程倉庫中，讓其他人也能看到你的修改：\ngit push origin feature/new-feature 4. 更新主分支 \u0026amp; Rebase # 上面已經學習到了，透過創建分支來開發新的功能，這樣可以避免直接在主分支上修改代碼帶來的風險。但是在實際開發中，主分支可能會不斷地有新的提交，這時候我們需要將主分支的最新變化合併到我們的功能分支中，以確保我們的功能分支是基於最新的主分支進行開發的。\n這點非常重要，建議，固定時間例如每天早上班的第一件事就是將主分支的最新變化合併到自己的功能分支中，這樣可以避免在後續開發中出現太多的衝突。\n有兩種常見的方法可以將主分支的最新變化合併到功能分支中：\nMerge（合併）：使用 git merge 命令將主分支合併到功能分支中。這種方法會保留主分支的提交歷史，並且會產生一個新的合併提交。 git checkout feature/new-feature git merge main 可以看到，這樣會在功能分支上產生一個新的合併提交，這個提交會包含主分支的最新變化。\n--- title: \"Git Merge 示意圖\" --- gitGraph commit commit branch feature/new-feature checkout feature/new-feature commit commit checkout main commit checkout feature/new-feature merge main Rebase（變基）：使用 git rebase 命令將功能分支的提交重新應用到主分支的最新提交之上。這種方法會改變功能分支的提交歷史，使其看起來像是直接從主分支的最新提交開始的。 git checkout feature/new-feature git rebase main 這也是我在工作上比較常用的方法，因為它可以保持提交歷史的整潔，讓我們的功能分支看起來像是直接從主分支的最新提交開始的。以下會展示一下 Rebase 之前和 Rebase 之後的示意圖，讓大家更清楚地理解 Rebase 的作用。\n--- title: \"Git Rebase 示意圖，Rebase 之前\" --- gitGraph commit id: \"init\" commit id: \"update: commit 1\" branch feature/new-feature checkout feature/new-feature commit id: \"feature: commit 2\" commit id: \"feature: commit 3\" checkout main commit id: \"update: commit 4\" --- title: \"Git Rebase 示意圖，Rebase 之後\" --- gitGraph commit id: \"init\" commit id: \"update: commit 1\" commit id: \"update: commit 4\" branch feature/new-feature checkout feature/new-feature commit id: \"feature: commit 2\" commit id: \"feature: commit 3\" 可以看到，Rebase 之前，功能分支的提交歷史是基於主分支的第一個提交（update: commit 1）開始的，而 Rebase 之後，功能分支的提交歷史是基於主分支的最新提交（update: commit 4）開始的。這樣就保持了提交歷史的整潔，讓我們的功能分支看起來像是直接從主分支的最新提交開始的。\n5. 壓縮提交（Squash Commits） # 這也是我在工作中比較常用的一個功能，當我們在功能分支上進行開發的時候，可能會有很多的提交，某些提交可能是一些小的修改或者是一些無意義的提交，這時候我們可以使用 git rebase -i 命令來壓縮這些提交，讓提交歷史看起來更整潔。\n作者在工作上常常看到很多開發者的功能分支上有很多的提交，大量且無意義的提交會讓提交歷史變得非常混亂。也會使團隊的運維變的非常麻煩，因為在進行回滾或者是查看提交歷史的時候，會被這些無意義的提交所干擾，難以找到真正有用的提交。所以建議大家在開發完成後，或者是在提交之前，先使用 git rebase -i 命令來壓縮這些提交，讓提交歷史看起來更整潔。\ngit checkout feature/new-feature git rebase -i HEAD~n # n 是你想要壓縮的提交的數量 這個命令會打開一個交互式的界面，讓你選擇要壓縮的提交。你可以將要壓縮的提交標記為 squash，然後保存並退出，Git 就會將這些提交壓縮成一個提交。 這樣就可以讓提交歷史看起來更整潔，讓團隊的運維變得更簡單。\n--- title: \"Git Rebase -i 示意圖，Squash Commits 之前\" --- gitGraph commit id: \"init\" commit id: \"update: commit 1\" branch feature/new-feature checkout feature/new-feature commit id: \"feature: commit 2\" commit id: \"test: commit 3\" commit id: \"fix: commit 4\" --- title: \"Git Rebase -i 示意圖，Squash Commits 之後\" --- gitGraph commit id: \"init\" commit id: \"update: commit 1\" branch feature/new-feature checkout feature/new-feature commit id: \"feature: commit 2 ~ commit 4\" 從上面的示意圖可以看到，Squash Commits 之前，功能分支上有三個提交（feature: commit 2、test: commit 3、fix: commit 4），這些提交可能是一些小的修改或者是一些無意義的提交。Squash Commits 之後，這三個提交被壓縮成了一個提交（feature: commit 2 ~ commit 4），這樣就讓提交歷史看起來更整潔了。\n6. 暫存修改（Stash） # git stash 是一個非常有用的功能，當你在開發過程中，突然需要切換到另一個分支去處理一些緊急的問題，但是本機電腦上的修改還沒有提交，這時候你就可以使用 git stash 命令來暫存這些修改，讓你可以切換到另一個分支去處理緊急的問題。\ngit stash # 暫存工作區的修改 git stash -u # 暫存工作區的修改和未追蹤的文件 當你處理完緊急的問題，想要回到之前的分支繼續開發的時候，你可以使用以下命令來恢復之前暫存的修改：\ngit stash pop # 恢復暫存的修改，並且從暫存列表中刪除這個暫存 git stash apply # 恢復暫存的修改，但不會從暫存列表中刪除這個暫存 Git Flow 工作流程🌊 # Git Flow 是什麼？為什麼我們需要 Git Flow？\nGit Flow 是一種基於 Git 的分支管理模型，它定義了一套嚴格的分支結構和工作流程，讓團隊能夠更好地協作開發。Git Flow 的主要分支包括：\nmaster（主分支）：用來存儲生產環境的代碼，這個分支上的代碼應該是穩定的，可以隨時部署到生產環境中。 develop（開發分支）：用來存儲開發中的代碼，這個分支上的代碼可能不穩定，只有當開發完成並且測試通過了，才會將這些修改合併到 master 分支中。 feature（功能分支）：用來開發新的功能的分支，這些分支是從 develop 分支上創建的，當功能開發完成並且測試通過了，就會將這些修改合併到 develop 分支中。 release（發布分支）：用來準備發布的分支，這些分支是從 develop 分支上創建的，當發布準備完成並且測試通過了，就會將這些修改合併到 master 分支中，並且打上發布的標籤。 hotfix（熱修復分支）：用來修復生產環境中的緊急問題的分支，這些分支是從 master 分支上創建的，當修復完成並且測試通過了，就會將這些修改合併到 master 分支中，並且打上修復的標籤。 Git Flow 的工作流程如下：\n開發新的功能：從 develop 分支上創建一個新的 feature 分支，進行功能開發，當功能開發完成並且測試通過了，就會將這些修改合併到 develop 分支中。 --- title: \"Git Flow 工作流程，開發新的功能\" --- gitGraph commit id: \"init\" commit id: \"update: commit 1\" branch develop checkout develop commit id: \"develop: commit 2\" branch feature/new-feature checkout feature/new-feature commit id: \"feature: commit 3\" commit id: \"feature: commit 4\" checkout develop merge feature/new-feature 準備發布：從 develop 分支上創建一個新的 release 分支，進行發布準備，當發布準備完成並且測試通過了，就會將這些修改合併到 main 分支中，並且打上發布的標籤。 --- title: \"Git Flow 工作流程，準備發布\" --- gitGraph commit id: \"init\" commit id: \"update: commit 1\" branch develop checkout develop commit id: \"develop: commit 2\" branch feature/new-feature checkout feature/new-feature commit id: \"feature: commit 3\" commit id: \"feature: commit 4\" checkout develop merge feature/new-feature branch release/v1.0 checkout release/v1.0 checkout main merge release/v1.0 修復緊急問題：從 main 分支上創建一個新的 hotfix 分支，進行緊急問題的修復，當修復完成並且測試通過了，就會將這些修改合併到 main 分支中，並且打上修復的標籤。 --- title: \"Git Flow 工作流程，修復緊急問題\" --- gitGraph commit id: \"init\" commit id: \"update: commit 1\" branch hotfix/bug-fix checkout hotfix/bug-fix commit id: \"hotfix: commit 3\" checkout main merge hotfix/bug-fix 上述只是簡單的介紹了 Git Flow 的工作流程，實際上在不同的團隊中，Git Flow 的工作流程可能會有所不同，具體的流程需要根據團隊的實際情況來制定。\n例如: 使用分支區分各環境部署，main 分支用來部署生產環境，develop 分支用來部署開發環境，qa 分支用來部署測試環境等等，這些都是團隊內部的流程規定，需要根據實際情況來操作。\n--- title: \"Git Flow 工作流程，分支區分各環境部署\" --- gitGraph commit id: \"init\" commit id: \"update: commit 1\" branch develop branch qa branch feature/new-feature checkout feature/new-feature commit id: \"feature: commit 2\" commit id: \"feature: commit 3\" checkout develop merge feature/new-feature checkout qa merge feature/new-feature checkout main merge feature/new-feature 上述的示意圖展示了 Git Flow 工作流程中，分支區分各環境部署的情況。也是目前作者在工作中最常使用的流程，因為通常 SRE 會需要管理多個環境的部署，這樣就可以通過分支來區分不同環境的部署，讓 SRE 的工作變得更簡單。\n結語🎉 # Git 是一個非常強大的版本控制系統，學習 Git 的基本概念和使用方法是每個軟體開發者必須掌握的技能。希望這篇文章能夠幫助到那些剛接觸 Git 的人，讓他們能夠快速上手 Git，並且在實務中有效地使用 Git 來管理代碼和協作開發。\n希望每個人都能有一個愉快的 Git 旅程，讓我們在軟體開發的道路上越走越遠！\n參考文獻📚 # 學習 Git 分支 連猴子都能懂的 Git 入門指南 ","date":"23 March 2026","externalUrl":null,"permalink":"/journals/git/","section":"Journals","summary":"","title":"Git 基礎教學：從入門到實務應用","type":"journals"},{"content":"","date":"23 March 2026","externalUrl":null,"permalink":"/tags/%E7%89%88%E6%9C%AC%E6%8E%A7%E5%88%B6/","section":"Tags","summary":"","title":"版本控制","type":"tags"},{"content":"","date":"23 March 2026","externalUrl":null,"permalink":"/tags/%E8%BB%9F%E9%AB%94%E9%96%8B%E7%99%BC/","section":"Tags","summary":"","title":"軟體開發","type":"tags"},{"content":"","date":"11 March 2026","externalUrl":null,"permalink":"/tags/mongodb/","section":"Tags","summary":"","title":"MongoDB","type":"tags"},{"content":" 前言🔖 # 近期因為工作上需要排查 MongoDB 的問題，過程中有使用很多的語法指令幫助自己在過程中了解問題所在，想要將近期的學習過程中所使用的 MongoDB 語法指令做一個整理，方便自己在未來需要使用的時候可以快速找到。\nMongoDB 常用語法指令📌 # 連線指令🔗 # 一開始最重要的就是連線到 MongoDB 的指令。如果是在 linux 環境下直接安裝 mongodb 可以直接透過 mongosh 連線到 MongoDB，例如：\nmongosh mongodb://${username}:${password}@localhost:27017/${dbname}?authSource=admin 如果是使用 docker 的方式建立 MongoDB 的話，可以先進入容器內部，再使用 mongosh 連線到 MongoDB，例如：\ndocker exec -it mongodb sh mongosh -u ${username} -p ${password} --authenticationDatabase admin 這裡說明一下 --authenticationDatabase 參數，是使用來驗證使用者的資料庫，通常會使用 admin 資料庫來驗證使用者的身份。如果有創建其他資料庫來驗證使用者的身份，也可以使用該資料庫來驗證，例如：\nmongosh -u ${username} -p ${password} --authenticationDatabase ${authDB} 基本指令📌 # 成功連線到 MongoDB 後，就可以開始使用資料庫語法指令來操作 MongoDB 了。\n切換資料庫🔄 # 通常成功登入以後會預設在 test 資料庫，如果想要切換到其他資料庫的話，可以使用 use 指令，例如：\nuse ${dbname} 通常會先檢查一下目前有哪些資料庫可以使用，可以使用 show dbs 指令來查看，例如：\nshow dbs 查看資料表🔍 # 成功切換到資料庫以後，可以使用 show collections 指令來查看目前資料庫中有哪些資料表，例如：\nshow collections 資料表有多大📏 # 可以使用 stats 指令來查看資料表的大小，例如：\ndb.${collection}.stats() 預設 scale 的值是 1，所以回傳的的單位是 byte，如果想要以 MB 為單位的話，可以使用 scale 參數來指定單位，例如：\ndb.${collection}.stats({ scale: 1024 * 1024 }) 以下是常用的單位：\ndb.${collection}.stats({ scale: 1024 }) # KB db.${collection}.stats({ scale: 1024 * 1024 }) # MB db.${collection}.stats({ scale: 1024 * 1024 * 1024 }) # GB 查詢資料📊 # 查詢資料的指令是 find，可以使用 find 指令來查詢資料表中的資料，例如：\ndb.${collection}.find() 如果想要查詢特定條件的資料，可以在 find 指令中加入查詢條件，例如：\ndb.${collection}.find({ ${field}: ${value} }) 如果想確認資料結構的話，可以使用 findOne 指令來查詢一筆資料，例如：\ndb.${collection}.findOne() 通常我們可以先查詢一筆資料來確認資料的結構，然後再根據資料的結構來撰寫查詢條件。例如：圖片(一)中的資料結構中有一個欄位叫做 expireAt\n圖片(一) 如果想要查詢 expireAt 這個欄位存在的資料，不管 expireAt 的值為何，可以使用以下指令：\ndb.${collection}.find({ expireAt: { $exists: true } }) 這樣就可以查詢資料中有存在 expireAt 這個欄位的資料了。\n查詢目前正在執行的操作🔍 # 如果想要查詢目前正在執行的操作，可以使用 currentOp 指令，例如：\ndb.currentOp() 更進階一點，可以搭配一些過濾條件來查詢特定的操作，例如：\ndb.currentOp({ $or: [{ \u0026#34;command.createIndexes\u0026#34;: { $exists: true } }, { \u0026#34;msg\u0026#34;: /^Index Build/ }] }) 在這個範例中，使用 $or 來過濾出正在建立索引的操作，只要條件符合陣列中的任一條件，就會被查詢出來。\n條件一是 { \u0026quot;command.createIndexes\u0026quot;: { $exists: true } }\ncommand 欄位通常會記錄該 operation 的指令內容（例如 find、aggregate、createIndexes…）。 command.createIndexes 這種寫法是「點記法（dot notation）」：表示去看 command 物件底下的 createIndexes 欄位。 $exists: true 表示：只要這個欄位存在就匹配，不管值是什麼。 條件二是 { \u0026quot;msg\u0026quot;: /^Index Build/ }\nmsg 是 currentOp 裡常見的訊息欄位，MongoDB 會用它描述一些狀態（例如 index build 的進度訊息）。 /^Index Build/ 是 JavaScript 正規表示式（regex）： ^ 表示字串的開頭 Index Build 是要匹配的文字 所以它會匹配「msg 以 Index Build 開頭」的 operation 查詢集合的索引🔍 # mongodb 每一筆資料都是一個文件(document)，這是 mongodb 中儲存資料的基本單元，這些文件會被儲存在集合(collection)中，而集合中的文件可以有索引(index)來加速查詢的速度，如果想要查看集合中的索引，可以使用 getIndexes 指令，例如：\ndb.${collection}.getIndexes() 補充說明⭐️ # 索引除了直觀的加速查詢速度以外，在 DB 中也扮演資料管理與完整性的角色，索引具備以下特性：\n唯一性：建立 Unique Index 可以確保索引欄位的值是唯一的，例如：e-mail 或者 username 等等。 刪除過期資料：建立 TTL Index 可以自動刪除過期的資料，例如：session 或者 token 等等。詳細的說明可以參考之前的文章 mongoDB TTL Index 自動刪除過期資料。 優化排序：建立索引可以優化資料的排序，例如：按照日期或者價格等等。 覆蓋查詢：當你查詢的欄位剛好都在索引中時，MongoDB 可以直接從索引回傳結果，完全不需要讀取原始的 Document，這樣可以大幅提升查詢效率，減少磁碟 I/O 開銷。 ","date":"11 March 2026","externalUrl":null,"permalink":"/posts/sql/mongodb/cmd/","section":"Posts","summary":"","title":"MongoDB 語法指令整理與使用經驗分享","type":"posts"},{"content":"","date":"11 March 2026","externalUrl":null,"permalink":"/tags/%E6%9F%A5%E8%A9%A2%E8%B3%87%E6%96%99/","section":"Tags","summary":"","title":"查詢資料","type":"tags"},{"content":"","date":"11 March 2026","externalUrl":null,"permalink":"/tags/%E7%B4%A2%E5%BC%95/","section":"Tags","summary":"","title":"索引","type":"tags"},{"content":"","date":"11 March 2026","externalUrl":null,"permalink":"/tags/%E8%B3%87%E6%96%99%E5%BA%AB%E7%AE%A1%E7%90%86/","section":"Tags","summary":"","title":"資料庫管理","type":"tags"},{"content":"","date":"11 March 2026","externalUrl":null,"permalink":"/tags/%E8%AA%9E%E6%B3%95%E6%8C%87%E4%BB%A4/","section":"Tags","summary":"","title":"語法指令","type":"tags"},{"content":" 前言🔖 # 目前在公司內部有 API 服務使用 mongoDB 並且這個 DB 並沒有託付給 DBA 管理，是由 SRE 自己管理的。因此對於這個 DB 的使用上有觀察到磁碟空間的使用有往上升且沒有下降的趨勢。已經觸發了監控告警的上限。因此研究了 mongoDB 的 TTL Index 機制。能有效的節省機器的效能以及控制成本。\n什麼是超時索引❓ # mongoDB 中有一種索引叫做超時索引 ，用來自動清除過期的資料。超時索引的主要特點包括：\n自動過期：不需要手動處理過期文件，MongoDB 會自動處理。 高效性：通過超時索引，MongoDB 可以高效地定期刪除過期文件，減少了性能開銷。 適用於日誌和緩存：超時索引特別適用於存儲日誌數據、緩存數據和其他需要定期清理的情況。 靈活設置：您可以根據需要設置不同的超時時間，以滿足不同數據的需求。 如何在程式碼中新增 TTL❓ # 假設我們有一個名為 \u0026ldquo;session\u0026rdquo; 的集合，用於存儲用戶會話信息。我們希望自動刪除超過一小時未活動的會話。\n首先，在插入會話文檔時，我們需要添加一個 \u0026ldquo;expiry\u0026rdquo; 欄位，該欄位表示會話的過期時間。\nimport pymongo from datetime import datetime, timedelta # 建立到MongoDB的連線 client = pymongo.MongoClient(\u0026#34;mongodb://localhost:27017/\u0026#34;) # 選擇要建立索引的資料庫和集合 db = client[\u0026#34;mydatabase\u0026#34;] collection = db[\u0026#34;session\u0026#34;] # 插入一個會話文檔，並設定過期時間為一小時後 expiry_time = datetime.now() + timedelta(hours=1) session_data = { \u0026#34;user_id\u0026#34;: 123, \u0026#34;session_token\u0026#34;: \u0026#34;abc123\u0026#34;, \u0026#34;expiry\u0026#34;: expiry_time } collection.insert_one(session_data) # 關閉連線 client.close() 接下來，我們需要建立一個超時索引，以便自動刪除過期的會話文檔。我們將在 \u0026ldquo;expiry\u0026rdquo; 欄位上建立索引，並設置超時時間為一小時：\n# 在 \u0026#34;expiry\u0026#34; 欄位上建立超時索引 index_name = collection.create_index([(\u0026#34;expiry\u0026#34;, pymongo.ASCENDING)], expireAfterSeconds=0) print(f\u0026#34;建立的超時索引名稱為: {index_name}\u0026#34;) 這樣，MongoDB 將每分鐘檢查一次 \u0026ldquo;expiry\u0026rdquo; 欄位，並自動刪除過期的會話文檔。\n請注意，expireAfterSeconds 參數的值應該設置為 0，這表示文檔將在 \u0026ldquo;expiry\u0026rdquo; 欄位中指定的時間過後立即過期。\n通過這種方式，我們可以確保過期的會話文檔會自動被 MongoDB 刪除，從而保持數據庫的整潔。\n補充說明⭐️ # 如果只有在 session_data 中新增 expiry 沒有創建超時索引 mongoDB 並不會清除過期的舊資料。這次遇到的問題就是 RD 有在每一筆資料中建立 expiry 但是並沒有建立 index 導致 mongoDB 沒辦法如預期清除舊資料，導致磁碟空間慢慢被佔用。\n遇到的問題❓ # 磁碟空間沒有釋放出來💾 # doucment 已經加上 TTL Index，資料已經清除了，但磁碟空間沒有釋放出來。\n問題思考🧐 # 經過資料查詢以後，得到的結論是 MongoDB 的 WiredTiger 儲存引擎在刪除資料以後，並不會立即釋放磁碟空間，而是會將刪除的資料標記為可重用的空間，這樣可以提高寫入效率，但也會導致磁碟空間沒有釋放出來的問題。通常 SRE 會建立對 VM 的監控告警，當磁碟空間使用率達到一定的百分比（例如 80%）時，就會觸發告警，這樣就可以及時發現磁碟空間不足的問題。遇到上述的狀況要怎麼解決呢？\n解決方案💡 # 可以使用 compact 指令來壓縮資料庫，這樣就可以釋放出磁碟空間了，例如：\ndb.runCommand({ compact: \u0026#39;${collection}\u0026#39; }) 這個指令會對指定的集合進行壓縮，釋放出被標記為可重用的空間，從而降低磁碟空間的使用率。\n參考文獻📚 # 玩轉 Python 與 MongoDB_Day25_超時索引 TTL Index compact（数据库命令） ","date":"6 March 2026","externalUrl":null,"permalink":"/posts/sql/mongodb/ttl/","section":"Posts","summary":"","title":"mongoDB TTL Index 自動刪除過期資料","type":"posts"},{"content":"","date":"6 March 2026","externalUrl":null,"permalink":"/tags/ttl-index/","section":"Tags","summary":"","title":"TTL Index","type":"tags"},{"content":"","date":"6 March 2026","externalUrl":null,"permalink":"/tags/%E8%87%AA%E5%8B%95%E5%88%AA%E9%99%A4/","section":"Tags","summary":"","title":"自動刪除","type":"tags"},{"content":"","date":"6 March 2026","externalUrl":null,"permalink":"/tags/%E9%81%8E%E6%9C%9F%E8%B3%87%E6%96%99/","section":"Tags","summary":"","title":"過期資料","type":"tags"},{"content":" 前言🔖 # 近期在構思雲上部署方案，決定使用 Gitlab CI/CD 來實現自動化部署。因為之後會調整 GitFlow 的部署方式，所以 Gitlab CI/CD 能夠自動依照分支名稱來部署到不同的環境，這樣可以大大提升部署效率和可靠性。\nGitLab Runner 的設置🔧 # 為了讓 GitLab Runner 能夠在 Docker 環境中運行，我們需要確保 Runner 的配置允許使用 Docker-in-Docker（DinD）。這樣 Runner 就能夠在構建過程中使用 Docker 命令來構建和推送映像。\n建置 Runner 容器📦 # 使用 Dockerfile 來建置 Runner 容器，確保安裝了 Docker CLI 並配置好必要的環境變量。\nservices: gitlab-runner: container_name: gitlab-runner image: \u0026#34;gitlab/gitlab-runner:latest\u0026#34; restart: always environment: DOCKER_CACHE_DIR: /cache DOCKER_DISABLE_CACHE: \u0026#34;false\u0026#34; DOCKER_PRIVILEGED: \u0026#34;true\u0026#34; DOCKER_TLS_VERIFY: \u0026#34;false\u0026#34; RUNNER_CACHE_DIR: /cache RUNNER_EXECUTOR: docker volumes: - ./ssl/example_com.crt:/etc/gitlab-runner/certs/example_com.crt:ro - /var/run/docker.sock:/var/run/docker.sock - ./runner/config:/etc/gitlab-runner executor 是 runner 的執行器類型，一般用的比較多的是 shell 或 docker，這裡我們選擇 docker。\n讓人比較困惑的是其它一些 executor，如 Docker-SSH、Docker-SSH+machine 等，這些 executor 從 GitLab Runner 10.0 開始，兩者就被廢棄了，並且在後續的版本中移除。\n還有 Docker machine，這個概念本來是 Docker 提出的，但是後面同樣被 Docker 棄用了，只是 GitLab 為了向前兼容保留了下來。\nDocker in Docker 的配置⚙️ # 在容器內部執行 Docker 指令，一般有兩種方式：\n掛載宿主機的 Docker Socket：這種方式比較簡單，直接將宿主機的 Docker Socket 掛載到 Runner 容器中，Runner 就可以直接使用宿主機的 Docker 服務來構建和推送映像。 容器內部有自己的一套 Docker 環境，使用 docker:dind 的 image 來運行 DinD 服務，可以使用它作為主容器，也可以作為其他容器的服務容器（與其他服務之間通信）。 如前面所述，以 Docker 方式安裝 Runner，且 executor 設置為 docker，那麼就要設置 DinD。因為 Runner 只是啟動新容器，如果不要求 Runner 容器內部啟動 DinD 服務，我們可以採用第一種方式，直接掛載宿主機的 Docker Socket。\n另外，如果 Docker executor 在 CI/CD job 中涉及到 Docker 指令，那麼也要 Docker-in-Docker。記得要配置啟動容器的特權模式（privileged: true），這樣才能讓 DinD 正常運行。\n接下來使用任一種方式實現 DinD。\n在 config.toml 中新增設定 volumes = [\u0026quot;/cache\u0026quot;, \u0026ldquo;/var/run/docker.sock:/var/run/docker.sock\u0026rdquo;]。\n或者在 .gitlab-ci.yml 中指定 docker:dind\nservices: - docker:dind Runner 設定檔⚙️ # Runner 的設定檔位於 ./runner/config 目錄下，這裡需要配置 Runner 的註冊信息，包括 GitLab 的 URL、註冊令牌、執行器類型等。\nconcurrent = 1 check_interval = 0 shutdown_timeout = 0 [session_server] session_timeout = 1800 [[runners]] name = \u0026#34;docker-runner\u0026#34; url = \u0026#34;https://example.com/\u0026#34; id = 1 token = \u0026#34;xxx-xxx.xxxx.xxxxxxx\u0026#34; token_obtained_at = 2026-02-02T08:08:00Z token_expires_at = 0001-01-01T00:00:00Z tls-ca-file = \u0026#34;/etc/gitlab-runner/certs/example_com.crt\u0026#34; executor = \u0026#34;docker\u0026#34; [runners.cache] MaxUploadedArchiveSize = 0 [runners.cache.s3] [runners.cache.gcs] [runners.cache.azure] [runners.docker] tls_verify = false image = \u0026#34;docker:29.2.0-alpine3.23\u0026#34; privileged = true disable_entrypoint_overwrite = false oom_kill_disable = false disable_cache = false volumes = [\u0026#34;/cache\u0026#34;] cache_dir = \u0026#34;/cache\u0026#34; shm_size = 0 network_mtu = 0 參考文獻📚 # gitlab-runner 中的 Docker-in-Docker ","date":"6 February 2026","externalUrl":null,"permalink":"/journals/docker/setting_up_gitlab/","section":"Journals","summary":"","title":"使用 Docker 設置 GitLab CI/CD 與 Runner","type":"journals"},{"content":"","date":"5 February 2026","externalUrl":null,"permalink":"/tags/jenkins/","section":"Tags","summary":"","title":"Jenkins","type":"tags"},{"content":"","date":"5 February 2026","externalUrl":null,"permalink":"/tags/jenkinsfile/","section":"Tags","summary":"","title":"Jenkinsfile","type":"tags"},{"content":"","date":"5 February 2026","externalUrl":null,"permalink":"/tags/pipeline/","section":"Tags","summary":"","title":"Pipeline","type":"tags"},{"content":" 前言🔖 # 最近換到了新的工作環境，重回了 Jenkins 的懷抱。之前在舊公司使用 Jenkins 時，體驗很差，因為 Jenkins 要裝一堆插件，還要在 UI 上配置各種參數，讓人頭痛不已。在新公司中使用 Jenkinsfile 來管理 Jenkins Pipeline，這種方式讓我感覺非常棒，因為它讓我可以把 Jenkins 的配置寫成代碼，放在版本控制系統中，這樣就可以更好地管理和追蹤變更了。\n這篇文章會著重在 Jenkinsfile 的各類常用語法使用，並且會提供一些實際的範例來說明如何使用 Jenkinsfile 來管理 Jenkins Pipeline。\nJenkinsfile 基礎語法📜 # 1️⃣變數 # 在 Jenkinsfile 中，可以使用 def 關鍵字來定義變數，例如：\ndef myVar = \u0026#34;Hello, Jenkins!\u0026#34; def updateList = [\u0026#34;item1\u0026#34;, \u0026#34;item2\u0026#34;, \u0026#34;item3\u0026#34;] def desc = new StringBuilder(\u0026#34;This is a description.\u0026#34;) 第一個變數 myVar 是一個字符串，第二個變數 updateList 是一個陣列，第三個變數 desc 是一個 StringBuilder 對象。StringBuilder 是 Java 中的一個類，可以用來創建可變的字符串。\n2️⃣函數 # 在 Jenkinsfile 中，可以定義函數來封裝一些重複使用的邏輯，例如：\ndef deployToHost(String hostAddr, String sshUsername, List\u0026lt;String\u0026gt; sourceItems) { // 函數體 } 這個函數 deployToHost 接受三個參數：hostAddr 是主機地址，sshUsername 是 SSH 用戶名，sourceItems 是一個字符串列表，表示要部署的項目。\n3️⃣pipeline 定義 # 接下來是整個自動化流程的定義，會使用 pipeline 關鍵字來定義整個 Jenkins Pipeline，例如：\npipeline { agent any stages { stage(\u0026#39;Build\u0026#39;) { steps { echo \u0026#39;Building...\u0026#39; } } stage(\u0026#39;Test\u0026#39;) { steps { echo \u0026#39;Testing...\u0026#39; } } stage(\u0026#39;Deploy\u0026#39;) { steps { echo \u0026#39;Deploying...\u0026#39; } } } } 這個範例定義了一個包含三個階段（Build、Test、Deploy）的 Jenkins Pipeline。在每個階段中，可以使用 steps 來定義具體的操作，例如使用 echo 來輸出信息。\nagent any 表示這個 Pipeline 可以在任何可用的 Jenkins 節點上運行。\nℹ️補充說明：script 區塊必須要放在 steps 裡面，否則會報錯。\n進階語法🛠 # 1️⃣結構化參數 # 在 Jenkins UI 中，可以配置參數化的構建選項，這些參數可以在 Jenkinsfile 中使用。但是如果同一個 Jenkinsfile 提供很多 Pipeline 引用，每個 Pipeline 都需要配置一大堆參數，會讓人覺得很麻煩。參考圖片（一）：\njenkinsfile 參數化構建選項 為了解決這個問題，可以使用 parameters 區塊來定義結構化參數，例如：\npipeline { parameters { text( name: \u0026#39;versions\u0026#39;, defaultValue: \u0026#39;S02:v0.0.1\\nS03:v0.0.1\u0026#39;, description: \u0026#39;遊戲版本列表，格式：遊戲代碼:版本號，每行一個\u0026#39; ) choice( name: \u0026#39;types\u0026#39;, choices: [\u0026#39;gameservers\u0026#39;, \u0026#39;logics\u0026#39;], description: \u0026#39;單款遊戲或 gameServer 選擇\u0026#39; ) } } 2️⃣使用變數儲存 sh 執行結果 # 在 Jenkinsfile 中，可以使用 sh 步驟來執行 shell 命令，但有時候希望能將命令的輸出結果存儲到變數中，以便後續使用。可以使用 returnStdout: true 來實現這一點，例如：\ndef gitCommitHash = sh(script: \u0026#39;git rev-parse HEAD\u0026#39;, returnStdout: true).trim() echo \u0026#34;Current Git Commit Hash: ${gitCommitHash}\u0026#34; ","date":"5 February 2026","externalUrl":null,"permalink":"/journals/jenkins/jenkinsfile/","section":"Journals","summary":"","title":"使用 Jenkinsfile 管理 Jenkins Pipeline","type":"journals"},{"content":"","date":"29 January 2026","externalUrl":null,"permalink":"/tags/database/","section":"Tags","summary":"","title":"Database","type":"tags"},{"content":"","date":"29 January 2026","externalUrl":null,"permalink":"/tags/docker/","section":"Tags","summary":"","title":"Docker","type":"tags"},{"content":" 前言🔖 # 近期因為測試需求，需要在本地搭建一個 MongoDB 環境。考慮到方便性和可移植性，我決定使用 Docker 來搭建這個環境。本文將詳細介紹如何使用 Docker 搭建 MongoDB，包括配置副本集和設置持久化存儲。\nDocker Compose 配置文件📄 # 首先，我們需要創建一個 docker-compose.yaml 文件來定義 MongoDB 服務。以下是我們的配置文件內容：\nversion: \u0026#34;3.8\u0026#34; services: mongodb: image: mongo:8.0.3 container_name: mongodb restart: always ports: - \u0026#34;27017:27017\u0026#34; environment: MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: rootpassword command: \u0026gt; mongod --replSet rs0 --bind_ip 0.0.0.0 --wiredTigerCacheSizeGB 3 --keyFile /data/mongodb.key volumes: - ./data:/data/db - ./configdb:/data/configdb - ./mongodb.key:/data/mongodb.key:ro networks: - mongoNet networks: mongoNet: driver: bridge mongodb 的版本選擇了 8.0.3，並且設置了管理員帳號和密碼。為了確保數據的持久化，我們將數據目錄映射到本地的 ./data 和 ./configdb 目錄。此外，我們還使用了一個密鑰文件 mongodb.key 來配置副本集的安全性。\n--replSet rs0 參數用於啟用副本集功能，將副本集名稱設置為 rs0。。副本集是 MongoDB 提供高可用性和資料冗餘的機制，它會在多個伺服器之間自動複製資料。\n--bind_ip 0.0.0.0 參數允許 MongoDB 接受來自任何 IP 地址的連接，這對於在 Docker 容器中運行的服務非常重要。\n--wiredTigerCacheSizeGB 3 參數設置了 WiredTiger 存儲引擎的緩存(記憶體快取)大小為 3GB，這有助於提升性能。\n--keyFile /data/mongodb.key 參數指定了用於副本集成員之間身份驗證的密鑰文件路徑。\n創建密鑰文件🔐 # 在啟動 MongoDB 容器之前，我們需要創建一個密鑰文件 mongodb.key。這個文件將用於副本集成員之間的身份驗證。可以使用以下命令生成一個隨機的密鑰文件：\n# 生成 keyfile（至少 6 個字符，最多 1024 個字符） openssl rand -base64 756 \u0026gt; mongodb.key # 設置正確的權限（必須是 400 或 600） chmod 400 mongodb.key # 如果需要在 Docker 中使用，確保所有者正確 sudo chown 999:999 mongodb.key 問題排查❓ # 如果在啟動容器後遇到錯誤日誌，先確認 mongodb.key 文件的權限是否正確設置為 999:999，並且權限為 400 或 600。這是因為 MongoDB 需要確保密鑰文件的安全性。\nError creating service context\u0026#34;,\u0026#34;attr\u0026#34;:{\u0026#34;error\u0026#34;:\u0026#34;Location5579201: Unable to acquire security key[s] 測試連接🔌 # 可以使用 MongoDB 客戶端工具或進入容器內部來測試連接：\n# 進入容器 docker exec -it mongodb mongosh -u root -p rootpassword --authenticationDatabase admin # 或直接連接 mongosh mongodb://root:rootpassword@localhost:27017/ 參考文獻📚 # 官方 MongoDB Docker 建置指南 ","date":"29 January 2026","externalUrl":null,"permalink":"/journals/docker/setting_up_mongodb/","section":"Journals","summary":"","title":"使用 Docker 搭建 MongoDB","type":"journals"},{"content":"","date":"21 January 2026","externalUrl":null,"permalink":"/tags/data-streams/","section":"Tags","summary":"","title":"Data Streams","type":"tags"},{"content":"","date":"21 January 2026","externalUrl":null,"permalink":"/tags/elastic/","section":"Tags","summary":"","title":"Elastic","type":"tags"},{"content":" 前言🔖 # 最近有幫公司重新建立一套 Elastic 的 Log 系統，之前的團隊依賴 VM 內部的排程任務來做 Index 的刪除與管理，這樣的方式雖然能達到目的，但卻不夠彈性且容易出錯。後來發現 Elastic 官方提供的 Data Streams 功能，可以自動化管理 Index 的生命週期，讓我們能更輕鬆地維護 Log 系統。\nData Streams 簡介📃 # Data Streams 是 Elastic Stack 中的一個功能，主要用於處理時間序列資料，如日誌、指標等。它允許我們將資料自動分割成多個 Index，並根據設定的生命週期策略自動管理這些 Index 的刪除和轉換。下面會開始介紹如何使用 Data Streams 來自動化管理 Index 的生命週期。\n建立 Lifecycle Policy🛠️ # 第一步，我們需要建立一個 Index Lifecycle Management (ILM) 策略，來定義 Index 的生命週期。以下是一個範例策略，設定 Index 在 30 天後刪除：\nPUT _ilm/policy/my-lifecycle-policy { \u0026#34;policy\u0026#34;: { \u0026#34;phases\u0026#34;: { \u0026#34;hot\u0026#34;: { \u0026#34;actions\u0026#34;: { \u0026#34;rollover\u0026#34;: { \u0026#34;max_size\u0026#34;: \u0026#34;50gb\u0026#34;, \u0026#34;max_age\u0026#34;: \u0026#34;7d\u0026#34; } } }, \u0026#34;delete\u0026#34;: { \u0026#34;min_age\u0026#34;: \u0026#34;30d\u0026#34;, \u0026#34;actions\u0026#34;: { \u0026#34;delete\u0026#34;: {} } } } } } 如果公司內部有建立 Elastic 的 API Domain，可以透過 API Domain 來執行上述的指令。例如：\ncurl -X PUT \u0026#34;https://your-api-domain/_ilm/policy/my-lifecycle-policy\u0026#34; -H \u0026#39;Content-Type: application/json\u0026#39; -d\u0026#39; { \u0026#34;policy\u0026#34;: { \u0026#34;phases\u0026#34;: { \u0026#34;hot\u0026#34;: { \u0026#34;actions\u0026#34;: { \u0026#34;rollover\u0026#34;: { \u0026#34;max_size\u0026#34;: \u0026#34;50gb\u0026#34;, \u0026#34;max_age\u0026#34;: \u0026#34;7d\u0026#34; } } }, \u0026#34;delete\u0026#34;: { \u0026#34;min_age\u0026#34;: \u0026#34;30d\u0026#34;, \u0026#34;actions\u0026#34;: { \u0026#34;delete\u0026#34;: {} } } } } }\u0026#39; 建立 Component Template🔧 # 接下來，我們需要建立一個 Component Template，來定義 Data Stream 的 Mapping 和 Settings。以下是一個範例：\nPUT _component_template/my-logs-settings { \u0026#34;template\u0026#34;: { \u0026#34;settings\u0026#34;: { \u0026#34;index.lifecycle.name\u0026#34;: \u0026#34;my-lifecycle-policy\u0026#34;, // 指定 ILM 策略，自動管理滾動與刪除 \u0026#34;index.codec\u0026#34;: \u0026#34;best_compression\u0026#34; } } } PUT _component_template/my-logs-mappings { \u0026#34;template\u0026#34;: { \u0026#34;mappings\u0026#34;: { \u0026#34;properties\u0026#34;: { \u0026#34;@timestamp\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;date\u0026#34; }, // Data Stream 必須包含 @timestamp 欄位 \u0026#34;message\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34; } } } } } 建立 Index Template📂 # 接著，我們需要建立一個 Index Template，來將 Component Template 套用到 Data Stream 上。以下是一個範例：\nPUT _index_template/my-logs-template { \u0026#34;index_patterns\u0026#34;: [\u0026#34;logs-my-app-*\u0026#34;], // 匹配此模式的寫入將自動轉為 Data Stream \u0026#34;data_stream\u0026#34;: {}, // 啟動 Data Stream 功能 \u0026#34;composed_of\u0026#34;: [ \u0026#34;my-logs-settings\u0026#34;, \u0026#34;my-logs-mappings\u0026#34; ], // 組合上面的組件 \u0026#34;priority\u0026#34;: 500 } 建立完成後，可以開啟 Kibana 的 Management 頁面，確認 Index Template 是否正確建立。如下圖所示：\nIndex Template 範例 補充說明⭐️ # Index Template 使用 composed_of 屬性來組合多個 Component Template，也可以將設定直接寫在 Index Template 裡面。選擇不創建 Component Template 也是可以的。\n測試 Data Stream🚀 # 當以上的準備工作都完成後，我們就可以開始測試 Data Stream 了。使用任何的 Log Agent 工具（如 Filebeat、Fluentd 等）將日誌資料寫入符合 Index Template 模式的 Index 名稱，例如 logs-my-app-000001。Elastic 會自動將其轉換為 Data Stream，並根據我們設定的 ILM 策略來管理 Index 的生命週期。\n下圖是從 Server 收集到的日誌資料，並成功寫入 Data Stream：\nData Stream 日誌寫入範例 補充說明⭐️ # 透過 Data Streams 管理的 Index，會加上 .ds- 前綴來區分一般的 Index。例如，logs-my-app-000001 會變成 .ds-logs-my-app-000001。在 Kibana 頁面上要打開 Include hidden indices 才能看到這些 Index。\n結語📝 # 透過 Elastic 的 Data Streams 功能，我們可以輕鬆地自動化管理 Index 的生命週期，減少手動維護的工作量，並提高系統的穩定性和彈性。希望這篇文章能幫助大家更好地理解和使用 Data Streams 來管理日誌資料。\n參考文獻📚 # Elastic 官方文件 - Data Streams Elastic 官方文件 - Set up a data stream ","date":"21 January 2026","externalUrl":null,"permalink":"/posts/elastic/data_streams/","section":"Posts","summary":"","title":"Elastic Data Streams 自動化管理 Index 的生命週期","type":"posts"},{"content":"","date":"21 January 2026","externalUrl":null,"permalink":"/tags/ilm/","section":"Tags","summary":"","title":"Ilm","type":"tags"},{"content":"","date":"21 January 2026","externalUrl":null,"permalink":"/tags/index-lifecycle-management/","section":"Tags","summary":"","title":"Index Lifecycle Management","type":"tags"},{"content":" 前言🔖 # 近期在公司內部有需要建置 VM 提供給 DBA 的同仁，有提出需要掛載額外的磁碟空間，經過一番研究後，決定使用 GCP 的 GCE 永久磁碟 (Persistent Disk) 來達成這個需求。會使用附加磁碟的方式，主要是因為這樣可以讓 VM 的系統磁碟與資料磁碟分開管理，方便日後的維護與擴充。\n建置步驟🛠 # 一開始會透過 IaC 工具先建立好 VM 與附加磁碟的資源，以下是使用 Terraform 的範例程式碼：\nresource \u0026#34;google_compute_instance\u0026#34; \u0026#34;default\u0026#34; { name = \u0026#34;example-instance\u0026#34; machine_type = \u0026#34;n1-standard-1\u0026#34; zone = \u0026#34;us-central1-a\u0026#34; boot_disk { initialize_params { image = \u0026#34;debian-cloud/debian-9\u0026#34; } } attached_disk { source = google_compute_disk.attached_disk.id mode = \u0026#34;READ_WRITE\u0026#34; } network_interface { network = \u0026#34;default\u0026#34; access_config {} } } 建立完 VM 與附加磁碟後，接下來需要在 VM 內部進行磁碟的初始化與掛載。以下是步驟說明：\n磁碟初始化與掛載🔧 # 將磁碟連接至 VM 如何確認已經連接成功，可以透過 GCP 控制台或是使用 lsblk 指令查看磁碟列表。 控制台的畫面如下圖所示： 在 VM 內部初始化磁碟 已經連接成功後，接下來需要在 VM 內部進行磁碟的初始化。可以先需照序號列出磁碟資訊，確認新磁碟的裝置名稱。\nls -l /dev/disk/by-id/ 從輸出中找到磁碟序號，如下圖所示： 初始化磁碟 使用 mkfs.xfs 指令來初始化磁碟，以下是範例指令：\nsudo mkfs.xfs /dev/sdb 這裡需要注意的是初始化磁碟，因為這裡要使用的檔案系統是 XFS，所以使用 mkfs.xfs 指令。\n建立掛載點並掛載磁碟 接下來需要建立掛載點，並將磁碟掛載到該目錄下：\nmkdir -p /mnt/data mount /dev/sdb /mnt/data 設定開機自動掛載 為了讓磁碟在 VM 重啟後能夠自動掛載，需要編輯 /etc/fstab 檔案，加入以下內容：\necho \u0026#34;UUID=$UUID /mnt/data xfs defaults,discard 0 0\u0026#34; \u0026gt;\u0026gt; /etc/fstab UUID 可以透過 blkid 指令來取得：\nUUID=$(blkid -s UUID -o value /dev/sdb) 結語📝 # 透過以上的步驟，我們成功地在 GCE VM 上掛載了額外的永久磁碟，並且設定了開機自動掛載。這樣的架構不僅提升了資料的管理彈性，也方便日後的擴充與維護。如果有需要更多磁碟空間，只需再新增附加磁碟並重複上述步驟即可。\n參考文獻📚 # Terraform GCE Instance Documentation 將永久磁碟新增至 VM ","date":"20 January 2026","externalUrl":null,"permalink":"/journals/gcp/gce-disk-mount/","section":"Journals","summary":"","title":"GCE 永久磁碟掛載實作過程紀錄","type":"journals"},{"content":"","date":"20 January 2026","externalUrl":null,"permalink":"/tags/persistent-disk/","section":"Tags","summary":"","title":"Persistent Disk","type":"tags"},{"content":" 前言🔖 # 近期開始對公司內部幾個 API 服務進行 Docker 容器化，這些服務的程式碼都有使用共用的 Private GitLab 儲存庫。本文將分享如何在 Dockerfile 中設定 Golang，以便能夠順利存取這些 Private GitLab 儲存庫。\n問題描述❓ # 一開始時，Dockerfile 如下所示：\nARG GO_VERSION=1.22 ARG ALPINE_VERSION=3.18 ARG SERVICE_NAME=api ARG SERVICE_PORT=9090 ARG SERVICE_CMD=api ARG USER=sre ARG TOKEN=your_token_here # 階段 1: 構建階段 FROM golang:${GO_VERSION}-alpine AS builder # 重新宣告 ARG 以在此階段使用 ARG SERVICE_NAME ARG USER ARG TOKEN # 安裝必要的構建工具 RUN apk add --no-cache git make bash # 設置工作目錄 WORKDIR /app # 配置 Git 以使用內部 GitLab 儲存庫，https 替代 http RUN git config --global url.\u0026#34;https://private.gitlab.com/\u0026#34;.insteadOf \u0026#34;http://private.gitlab.com/\u0026#34; # 複製依賴文件 COPY go.mod go.sum ./ # 下載依賴 RUN go mod tidy -download -x # 複製整個專案 COPY . . # 創建輸出目錄並構建應用 RUN mkdir -p /app/bin \u0026amp;\u0026amp; \\ CGO_ENABLED=0 GOOS=linux go build -o /app/bin/${SERVICE_NAME} main.go \u0026amp;\u0026amp; \\ echo \u0026#34;Build completed. Checking output file:\u0026#34; \u0026amp;\u0026amp; \\ ls -lh /app/bin/ \u0026amp;\u0026amp; \\ test -f /app/bin/${SERVICE_NAME} || (echo \u0026#34;ERROR: Binary file not found!\u0026#34; \u0026amp;\u0026amp; exit 1) # 階段 2: 運行階段 FROM alpine:${ALPINE_VERSION} # 重新宣告 ARG 以在此階段使用 ARG SERVICE_NAME ARG SERVICE_PORT ARG SERVICE_CMD # 將 ARG 轉換為 ENV 以便運行時使用 ENV SERVICE_NAME=${SERVICE_NAME} ENV SERVICE_PORT=${SERVICE_PORT} ENV SERVICE_CMD=${SERVICE_CMD} # 安裝 CA 證書和時區數據 RUN apk --no-cache add ca-certificates tzdata # 創建非 root 用戶 RUN addgroup -g 1000 appuser \u0026amp;\u0026amp; \\ adduser -D -u 1000 -G appuser appuser # 設置工作目錄 WORKDIR /app # 從構建階段複製可執行文件 COPY --from=builder --chown=appuser:appuser /app/bin/${SERVICE_NAME} ./${SERVICE_NAME} # 創建日誌目錄 RUN mkdir -p logs \u0026amp;\u0026amp; chown -R appuser:appuser logs # 切換到非 root 用戶 USER appuser # 暴露應用端口 EXPOSE ${SERVICE_PORT} # 啟動應用 CMD [\u0026#34;sh\u0026#34;, \u0026#34;-c\u0026#34;, \u0026#34;./${SERVICE_NAME} ${SERVICE_CMD}\u0026#34;] 這份 Dockerfile 在某些情況下無法成功下載 Private GitLab 儲存庫中的依賴，導致構建失敗。後來詢問後端的 RD 同事，才知道需要在 Dockerfile 中設定 Golang 的私有倉庫環境變量，並且配置 Git 以使用帶有認證的 URL。才能夠順利存取 Private GitLab 儲存庫。\n解決方案✅ # 修改後的 Dockerfile 如下所示：\nARG GO_VERSION=1.22 ARG ALPINE_VERSION=3.18 ARG SERVICE_NAME=api ARG SERVICE_PORT=9090 ARG SERVICE_CMD=api ARG USER=sre ARG TOKEN=your_token_here # 階段 1: 構建階段 FROM golang:${GO_VERSION}-alpine AS builder # 重新宣告 ARG 以在此階段使用 ARG SERVICE_NAME ARG USER ARG TOKEN # 安裝必要的構建工具 RUN apk add --no-cache git make bash # 設置工作目錄 WORKDIR /app # 設置 Go 私有倉庫環境變量 RUN go env -w GOPRIVATE=\u0026#34;private.gitlab.com\u0026#34; \u0026amp;\u0026amp; \\ go env -w GONOSUMDB=\u0026#34;private.gitlab.com\u0026#34; \u0026amp;\u0026amp; \\ go env -w GONOPROXY=\u0026#34;private.gitlab.com\u0026#34; # 配置 Git 以使用內部 GitLab 儲存庫，https 替代 http RUN git config --global url.\u0026#34;https://${USER}:${TOKEN}@private.gitlab.com/\u0026#34;.insteadOf \u0026#34;https://private.gitlab.com/\u0026#34; \u0026amp;\u0026amp; \\ git config --global url.\u0026#34;https://${USER}:${TOKEN}@private.gitlab.com/\u0026#34;.insteadOf \u0026#34;http://private.gitlab.com/\u0026#34; # 複製依賴文件 COPY go.mod go.sum ./ # 下載依賴 RUN go mod tidy -download -x # 複製整個專案 COPY . . # 創建輸出目錄並構建應用 RUN mkdir -p /app/bin \u0026amp;\u0026amp; \\ CGO_ENABLED=0 GOOS=linux go build -o /app/bin/${SERVICE_NAME} main.go \u0026amp;\u0026amp; \\ echo \u0026#34;Build completed. Checking output file:\u0026#34; \u0026amp;\u0026amp; \\ ls -lh /app/bin/ \u0026amp;\u0026amp; \\ test -f /app/bin/${SERVICE_NAME} || (echo \u0026#34;ERROR: Binary file not found!\u0026#34; \u0026amp;\u0026amp; exit 1) # 階段 2: 運行階段 FROM alpine:${ALPINE_VERSION} # 重新宣告 ARG 以在此階段使用 ARG SERVICE_NAME ARG SERVICE_PORT ARG SERVICE_CMD # 將 ARG 轉換為 ENV 以便運行時使用 ENV SERVICE_NAME=${SERVICE_NAME} ENV SERVICE_PORT=${SERVICE_PORT} ENV SERVICE_CMD=${SERVICE_CMD} # 安裝 CA 證書和時區數據 RUN apk --no-cache add ca-certificates tzdata # 創建非 root 用戶 RUN addgroup -g 1000 appuser \u0026amp;\u0026amp; \\ adduser -D -u 1000 -G appuser appuser # 設置工作目錄 WORKDIR /app # 從構建階段複製可執行文件 COPY --from=builder --chown=appuser:appuser /app/bin/${SERVICE_NAME} ./${SERVICE_NAME} # 創建日誌目錄 RUN mkdir -p logs \u0026amp;\u0026amp; chown -R appuser:appuser logs # 切換到非 root 用戶 USER appuser # 暴露應用端口 EXPOSE ${SERVICE_PORT} # 啟動應用 CMD [\u0026#34;sh\u0026#34;, \u0026#34;-c\u0026#34;, \u0026#34;./${SERVICE_NAME} ${SERVICE_CMD}\u0026#34;] 主要的修改點包括：\n設置 Go 私有倉庫環境變量：\nRUN go env -w GOPRIVATE=\u0026#34;private.gitlab.com\u0026#34; \u0026amp;\u0026amp; \\ go env -w GONOSUMDB=\u0026#34;private.gitlab.com\u0026#34; \u0026amp;\u0026amp; \\ go env -w GONOPROXY=\u0026#34;private.gitlab.com\u0026#34; 配置 Git 以使用帶有認證的 URL：\nRUN git config --global url.\u0026#34;https://${USER}:${TOKEN}@private.gitlab.com/\u0026#34;.insteadOf \u0026#34;https://private.gitlab.com/\u0026#34; \u0026amp;\u0026amp; \\ git config --global url.\u0026#34;https://${USER}:${TOKEN}@private.gitlab.com/\u0026#34;.insteadOf \u0026#34;http://private.gitlab.com/\u0026#34; 參考文獻📚 # Golang 從私有的GitLab取得依賴module Unable to get modules from private gitlab repository ","date":"19 January 2026","externalUrl":null,"permalink":"/journals/docker/docker_access_private_gitlab/","section":"Journals","summary":"","title":"Dockerfile 如何設定 Golang 存取 Private GitLab","type":"journals"},{"content":"","date":"19 January 2026","externalUrl":null,"permalink":"/tags/golang/","section":"Tags","summary":"","title":"Golang","type":"tags"},{"content":"","date":"19 January 2026","externalUrl":null,"permalink":"/tags/private-repository/","section":"Tags","summary":"","title":"Private Repository","type":"tags"},{"content":"","date":"22 December 2025","externalUrl":null,"permalink":"/tags/oslogin/","section":"Tags","summary":"","title":"Oslogin","type":"tags"},{"content":" 前言🔖 # 近期在接管公司 GCP 專案，發現 IT 團隊以前使用 OS Login 來管理 VM 的存取權限，因為之前沒有使用過這個功能，所以特別研究了一下 OS Login 的設定方式，並將設定過程記錄在此篇文章中。\n什麼是 OS Login？🤔 # 什麼是 OS Login？ OS Login 跟一般傳統的 SSH 金鑰管理方式有什麼不同？相較之下有哪些優點？接著往下看就知道了！\nOS Login 是 Google Cloud Platform (GCP) 提供的一種身份驗證和授權機制，允許使用者使用其 Google 帳戶來存取和管理 GCP 上的虛擬機器 (VM)。透過 OS Login，使用者可以將其 IAM 角色與 VM 的存取權限綁定，從而實現更細緻的權限控制。\n架構🏗️ # 這邊有一個簡單的架構圖說明公司內部管理 VM 存取的方式：\n使用者可以透過 SSH IAP 連線到堡壘機 (Bastion Host)，但是不能夠直接連線到其他的 VM。堡壘機會使用 OS Login 來驗證使用者的身份，並根據 IAM 角色來決定使用者是否有權限存取其他的 VM。\ngraph LR user[使用者] -- SSH IAP --\u003e bastion[堡壘機] bastion -- OS Login --\u003e jenkins[Jenkins] bastion -- OS Login --\u003e appserver[App Server] bastion -- OS Login --\u003e dbserver[DB Server] OS Login 的優點🌟 # 如果使用傳統的 SSH 金鑰管理方式，通常需要在每台 VM 上面設定使用者的公鑰，當使用者需要存取 VM 時，就會使用對應的私鑰來進行驗證。但是這種方式在管理上會比較麻煩，尤其是當使用者數量增加或是 VM 數量增加時，維護公鑰的工作量會變得很大。\nOS Login 則是將使用者的存取權限與 IAM 角色綁定，當使用者需要存取 VM 時，系統會根據 IAM 角色來決定使用者是否有權限存取該 VM。這樣的好處是可以集中管理使用者的存取權限，並且可以透過 IAM 來設定更細緻的權限控制。\nOS Login 的設定步驟🛠 # 首先需要建置一台堡壘機 (Bastion Host)，因為堡壘機需要能夠存取其他的 VM，所以需要設定 IAM 權限，以及 SCOPES。\n建立一個獨立的 Service Account 提供堡壘機使用，並且賦予以下的 IAM 角色，以便讓堡壘機能夠使用 OS Login 功能： roles/compute.osLogin roles/compute.osAdminLogin (如果需要管理員權限) roles/iam.serviceAccountUser roles/compute.viewer IAM 角色設定範例 在建立堡壘機的時候，設定 SCOPES，確保包含以下的 SCOPES： cloud-platform 補充說明：什麼是 SCOPES❓ SCOPES 是 GCP 中用來定義 VM 可以存取哪些 API 的權限範圍。當 VM 需要存取某些 GCP 服務時，必須先設定相應的 SCOPES，才能夠成功存取這些服務。上述的範例中，cloud-platform SCOPES 代表 VM 可以存取所有的 GCP 服務。在 GCP 控制台畫面可以看到 VM 的設定會如下圖所示： VM SCOPES 設定範例 做完上面兩個步驟之後，就有一台可以使用 OS Login 功能的堡壘機了。接著建立公司內部使用的其他 VM，例如： Jenkins、App Server、DB Server 等等，這些 VM 需要這定開啟 OS Login 功能。這樣的話，當使用者透過堡壘機連線到這些 VM 時，就會使用 OS Login 來進行身份驗證。\n建立其他內部 VM 時，確保在 VM 的 Metadata 中設定 enable-oslogin 為 TRUE，這樣才能啟用 OS Login 功能。 VM Metadata 設定範例 驗證✅ # 首先我們可以檢查堡壘機的設定是不是正確的：\n# 在本地電腦執行，查詢跳板機的 Service Account gcloud compute instances describe bastion-vm \\ --zone=ZONE \\ --format=\u0026#34;get(serviceAccounts[0].email)\u0026#34; # 查詢 scopes gcloud compute instances describe bastion-vm \\ --zone=asia-east1-b \\ --format=\u0026#34;get(serviceAccounts[0].scopes[])\u0026#34; 接著檢查 service account 的 IAM 角色是否正確：\ngcloud projects get-iam-policy PROJECT_ID --flatten=\u0026#34;bindings[].members\u0026#34; --format=\u0026#34;value(bindings.role)\u0026#34; --filter=\u0026#34;bindings.members:BASTION_SERVICE_ACCOUNT_EMAIL\u0026#34; 進入堡壘機，測試連線\n# 使用 gcloud compute ssh 連線到堡壘機 gcloud compute ssh bastion-vm --zone=ZONE # 在跳板機內，查詢 Service Account 的 OS Login 使用者名稱 gcloud compute os-login describe-profile 檢查內部 VM 的設定是否正確：\n# 檢查 Jenkins VM 是否啟用 OS Login gcloud compute instances describe p-vp-jenkins \\ --zone=asia-east1-b \\ --format=\u0026#34;get(metadata.items[]. value)\u0026#34; \\ --flatten=\u0026#34;metadata.items[]\u0026#34; \\ | grep -i oslogin 總結📝 # 上述的步驟完成之後，就可以使用 OS Login 來管理 GCP VM 的存取權限了。公司內部的個人帳號沒有 OS Login 權限，所以無法直接連線到 VM，只能透過堡壘機來進行存取，這樣可以提高整體的安全性。也避免了傳統 SSH 金鑰管理方式的繁瑣，讓權限管理更加集中和方便。\n參考文獻📚 # Google Cloud | 設定 OS 登入\n","date":"22 December 2025","externalUrl":null,"permalink":"/posts/gcp/oslogin/","section":"Posts","summary":"","title":"使用 OS Login 管理 GCP VM 存取權","type":"posts"},{"content":"","date":"26 October 2025","externalUrl":null,"permalink":"/tags/ai-%E5%8D%94%E4%BD%9C%E9%96%8B%E7%99%BC/","section":"Tags","summary":"","title":"AI 協作開發","type":"tags"},{"content":"","date":"26 October 2025","externalUrl":null,"permalink":"/tags/github-copilot/","section":"Tags","summary":"","title":"GitHub Copilot","type":"tags"},{"content":"","date":"26 October 2025","externalUrl":null,"permalink":"/tags/%E8%A6%8F%E6%A0%BC%E9%A9%85%E5%8B%95%E9%96%8B%E7%99%BC/","section":"Tags","summary":"","title":"規格驅動開發","type":"tags"},{"content":" 前言📖 # 在現代軟體開發中，團隊協作是成功的關鍵。隨著人工智慧技術的進步，像 GitHub Copilot 這樣的 AI 工具已經成為開發者的重要助手。本文希望透過規格驅動開發來限制並使團隊在使用 AI 工具進行開發時，能夠有效的避免 AI 工具產生的幻覺以及發散。\n什麼是規格驅動開發❓ # 規格驅動開發是以明確的需求和規格，來告訴並規範 AI 工具專注於明確的需求規格作為上下文實踐。過程中可以分成以下規範：\n憲法（constitution）專案的管理原則和開發指南，這是最高的規範。後面所有的訂定的規範或者任務都必須要遵從憲法。 規範（specify）你想要建立什麼？這個步驟會描述需求和使用者故事。 計畫（plan）使用你選擇的技術棧或技術堆疊來建立技術實現計畫。 任務（task）產生可以執行的任務清單。 實作（implement）依照任務清單執行所有的任務建立功能。 spec-kit💫 # spec-kit 是一個開源的工具，可以幫助我們更好地實踐規格驅動開發。它提供了一套標準化的流程和範本，讓團隊能夠更容易地定義和管理規格。\nGitHub Repository: spec-kit 如何使用 spec-kit❓ # 安裝 首先我們需要將 spec-kit 安裝到我們的開發專案中，使用 uv 套件管理工具進行安裝：\n永久且一次性的安裝 uv tool install specify-cli --from git+https://github.com/github/spec-kit.git # upgrade uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git 臨時安裝不會永久安裝 uvx --from git+https://github.com/github/spec-kit.git specify init \u0026lt;PROJECT_NAME\u0026gt; 初始化專案\n使用以下指令來初始化一個新的規格驅動開發專案：\nspecify init \u0026lt;PROJECT_NAME\u0026gt; 如果已經有一個已經存在的專案，可以使用以下指令來初始化：\ncd \u0026lt;EXISTING_PROJECT_DIRECTORY\u0026gt; specify init --here 目錄結構📂 # 初始化完成後，專案目錄結構會產生以下的目錄和檔案，方便我們進行規格驅動開發：\nprompts AI 工具使用的提示詞範本 copilot-instructions.md 一開始進行初始化時可以選擇使用的 AI，這邊我選擇的是 GitHub Copilot constitution.md 專案憲法 scripts 生成規格驅動開發需要的文件腳本 templates 生成規格驅動開發需要的文件範本 . ├── .github │ ├── prompts # AI 工具使用的提示詞範本 │ │ ├── analyze.prompt.md │ │ ├── clarify.prompt.md │ │ ├── constitution.prompt.md │ │ ... │ └── copilot-instructions.md # 一開始進行初始化時可以選擇使用的 AI 工具 ├── .specify │ ├── memory │ │ └── constitution.md # 專案憲法 │ ├── scripts │ │ └── bash # 生成規格驅動開發需要的文件腳本 │ │ ├── check-prerequisites.sh │ │ ... │ └── templates # 生成規格驅動開發需要的文件範本 │ ├── agent-file-template.md │ ├── plan-template.md │ ... ... 使用 spec-kit 指令與 AI 進行交互🤖 # 當我們已經完成了 spec-kit 在專案中的初始化後，我們就可以使用 spec-kit 提供的指令來與 AI 工具進行交互，並且根據我們的需求來生成相應的規格和任務。\n指令 說明 /speckit.constitution 產生或更新專案憲法 /speckit.specify 產生或更新需求規格(使用者故事) /speckit.plan 產生或更新技術棧或技術堆疊的實現計畫 /speckit.task 依照 plan 計畫，產生或更新任務清單 /speckit.implement 執行所有任務，實現目標功能 提升品質或驗證的指令📈\n這些指令是可選的，不一定要使用，但可以幫助我們提升規格和任務的品質：\n指令 說明 /speckit.clarify 澄清規格或任務中的不明確之處(建議在 speckit.plan 之前使用) /speckit.analyze 分析任務執行時元件之間的相依性以及風險(建議在 speckit.task 之後使用，在 speckit.implement 之前使用) /speckit.checklist 產生一個自訂的檢查清單來驗證規格或任務的完整性(類似單元測試的概念) 範例流程🚀 # 經過上面的講解，我們可以看到使用 spec-kit 來進行規格驅動開發的流程是非常清晰且有條理的。github 專案上有詳細的範例可以參考：spec-kit example\n這個範例非常的詳細，包括了每個步驟的說明以及如何使用 spec-kit 指令來與 AI 工具進行交互。建議大家可以參考這個範例來更好地理解規格驅動開發的流程。\n結論🎯 # 透過規格驅動開發並且在訂定專案憲法（constitution）的前提下，能夠更有效的避免 AI 工具產生的幻覺以及發散。提供給團隊在使用 AI 工具進行協作開發時，一個清晰且有條理的流程，提升開發效率和品質。\n參考文獻📚 # Spec-Driven Development：讓 AI 真正理解你想要什麼 ","date":"26 October 2025","externalUrl":null,"permalink":"/journals/github-copilot/spec-driven_development/","section":"Journals","summary":"","title":"規格驅動開發：使用 AI 工具進行協作開發的實踐","type":"journals"},{"content":"","date":"19 September 2025","externalUrl":null,"permalink":"/tags/network/","section":"Tags","summary":"","title":"Network","type":"tags"},{"content":" OSI 網路七層架構 # 網路七層架構是一個概念模型，用來描述網路通訊系統不同的功能層次。每一個層次都有特定的功能，並且與其他層次進行互動。這個模型有助於理解和設計網路系統，並促進不同系統之間的互操作性。\n第一層：實體層\n實體層通常也稱作物理層，生活中常見的網路數據傳輸設備都屬於實體層的範疇。例如集線器、中繼器、網卡等。\n第二層：資料連階層\n資料連結層主要負責在同一個網路中，不同的裝置之間進行資料傳輸。這過程中會將數位訊號封裝程資料訊框(Data Frame)，使用媒體存取控制(Media Access Control，MAC)來確保資料能夠正確地傳輸到目的地。\n補充：MAC 是 48 位元的硬體位址，用來識別網路介面卡(Network Interface Card，NIC)的唯一識別碼。跟 IP 位址不一樣。\n第三層：網路層\n網路層負責在不同的網路之間進行資料傳輸，會使用網際網路協定(Internet Protocol，IP)，將資料封裝成封包(Packet)，在網路上傳輸。封包裡面的 IP 會告訴網路設備這筆資料的的來源跟目的地。\n第四層：傳輸層\n協助 OSI 前三層與後三層進行溝通，處理流量＆錯誤控制，傳輸層會確保資料能夠完整。\n前面提到的 MAC 跟 IP 都是主機和主機之間的溝通，傳輸層之間的溝通則是使用端口(Port)。\n只要確認「主機跟主機連結」和 「port 和 port 通道」就能實現應用程式之間的溝通。\n第五層：會議層\n建立網路連線，等資料傳輸完後，在中斷連線。\n會有檢查點機制，同步資料傳輸。例如下載 50M 檔案的時候，每 10M 會有一個檢查點。如果傳輸中斷，可以從上次的檢查點繼續下載。\n第六層：展示層\n負責轉譯、加密、壓縮資料。\n設備之間在通訊時可能使用不同的編碼方式，展示層會將資料轉譯成應用程式能夠理解的格式。\n加密是為了確保資料在傳輸過程中的安全性，在傳輸者端進行加密，接收者端進行解密。\n接收應用程式端的資料後，會進行壓縮，傳輸到會議層的時候可以減少資料量，提高傳輸效率。\n第七層：應用層\n透過通訊協定跟資料操作把資訊整合起來，提供給使用者應用程式使用。最常見的例子就是網頁瀏覽器。\n常見的應用層協定有 HTTP、FTP、SMTP 等。\n請求標頭 # 在網路通訊中，請求標頭(Request Header)是指在 HTTP 請求中包含的元數據，用於傳達有關請求本身的信息。\nHost: 請求的目標伺服器主機名稱和端口號。 User-Agent: 發出請求的客戶端軟體資訊，例如瀏覽器類型和版本。 Content-Type: 請求主體的媒體類型，例如 application/json 或 text/html。 參考資料 # OSI 七層架構 ","date":"19 September 2025","externalUrl":null,"permalink":"/posts/network/","section":"Posts","summary":"","title":"網路基礎知識","type":"posts"},{"content":" 🔖前言 # 之前有稍微了解過 cert-manager，最近公司服務壞掉了需要修好它，順便就來寫一篇文章紀錄一下。\n❓什麼是 Cert Manager # Cert Manager 是一個 Kubernetes 的原生資源管理工具，主要用來自動化管理和發放 TLS/SSL 憑證。它可以與多種憑證頒發機構（CA）集成，如 Let\u0026rsquo;s Encrypt、HashiCorp Vault 等，並且能夠自動續期憑證，確保應用程序的安全通信。\n⚙️主要功能 # Cert Manager 會透過設定 Cluster Issuer、Issuer 來決定要使用哪一個 CA 來發放憑證，還有要管理的域名及驗證的方式。\nCluster Issuer 的作用範圍是整個 Kubernetes 叢集，而 Issuer 的作用範圍則是單一命名空間。\nCertificate 是 Cert Manager 的核心資源，用來定義需要管理的憑證。它會指定要使用的 issuer、域名、密鑰等資訊。\n設定完成之後，Cert Manager 會自動向指定的 CA 發出憑證請求，並將獲取的憑證存儲在 Kubernetes 的 Secret 中。在憑證即將過期時，Cert Manager 會自動續期，確保始終擁有有效的憑證。\n📌流程圖 # flowchart manager[cert-manager] --\u003e certificaterequest[Certificate Request] --\u003e issuer[Issuer/ClusterIssuer] issuer --\u003e ca[Let's Encrypt/HashiCorp Vault] ca --\u003e certificate[Certificate] certificate --\u003e secret[Secret] secret --\u003e ingress[Ingress/Service] ❓遇到的問題 # 透過網址訪問 API 服務時出現 SSL certificate problem: certificate has expired 的錯誤訊息，表示憑證已經過期了。那為什麼 cert-manager 沒有自動續期呢？發生了什麼錯誤就是我們要去調查的重點。\n首先從流程圖可以知道，cert-manager 要更新 certificate 的時候會先建立一個 Certificate Request，可以從 describe 觀察到 event 顯示 Referenced ClusterIssuer not found，表示找不到對應的 ClusterIssuer。\n接著我們去查看 ClusterIssuer，可以發現 \u0026ldquo;letsencrypt\u0026rdquo; 這個 ClusterIssuer 根本不存在。\n圖的話忘記截了，總之就是找不到這個 ClusterIssuer。🤣\n因為 helm release 的 values 設定裡是存在的，所以使用 apply 也不會再建立一次。所以用 helmfile sync 重新部署一次。\n完成之後，certificate request 就可以成功透過 ClusterIssuer 來向 Let\u0026rsquo;s Encrypt 申請憑證了。\n📚參考文章 # 使用 cert-manager 管理 K8S TLS 憑證 ","date":"16 September 2025","externalUrl":null,"permalink":"/journals/k8s/cert-manager/","section":"Journals","summary":"","title":"Cert Manager in Kubernetes","type":"journals"},{"content":" 自動模型選擇功能 # github copilot agent 推出自動選擇模型功能，VSCode 會自動選擇一個模型，確保獲得最佳效能並避免速率限制。\n自動模型選擇功能會在 Claude Sonnet 4、GPT-5、GPT-5 mini、GPT-4.1 和 Gemini Pro 2.5 之間進行選擇。除非你停用這些模型的存取權限。\n自動模型選擇功能會提供 10% 的折扣。可以將滑鼠懸停在聊天視圖中來查看所選模型和模型乘數。\n確認敏感文件的編輯 # 當你嘗試編輯敏感文件時，GitHub Copilot 會顯示一個確認對話框，讓你確認是否要繼續編輯。\n在代理模式下，代理可以自主編輯您工作區中的文件。這可能包括意外或惡意修改或刪除重要檔案（例如設定檔），這可能會對您的電腦造成直接的負面影響。\n如果要更改設定的話，可以在 設定 中搜尋 chat.tools.edits.autoApprove。\n設定 agentMDFile(實驗性功能) # 可以在 設定 中搜尋 chat.useAgentsMdFile 來啟用這個實驗性功能。\nAGENTS.md 可以讓你提供 agent 上下文資訊或指令。\n簡單來說 README.md 是提供給人類閱讀的，而 AGENTS.md 是提供給 AI agent 閱讀的。所以你必須在 AGENTS.md 中提供更詳細的資訊。例如：如何建立專案、執行什麼類型的測試、希望遵循的程式碼風格指南等。\n改善了已更改文件的體驗 # 在聊天視圖中已更改的文件預設會摺疊起來，以便為聊天對話提供更多空間。在折疊狀態下，您仍然可以看到已更改檔案的數量以及新增或刪除的行數。 當您保留或接受建議的變更時，該檔案將從檔案變更清單中刪除。 當您使用原始碼控制視圖暫存或提交文件時，它會自動接受建議的文件變更。 現在會顯示清單中每個項目的每個檔案的變更（新增或刪除的行）。 參考 # August 2025 (version 1.104) ","date":"12 September 2025","externalUrl":null,"permalink":"/journals/github-copilot/20250911-release/","section":"Journals","summary":"","title":"GitHub Copilot 實用新功能(version 1.104)","type":"journals"},{"content":"","date":"24 August 2025","externalUrl":null,"permalink":"/tags/kubernetes/","section":"Tags","summary":"","title":"Kubernetes","type":"tags"},{"content":"","date":"24 August 2025","externalUrl":null,"permalink":"/tags/session/","section":"Tags","summary":"","title":"Session","type":"tags"},{"content":"","date":"24 August 2025","externalUrl":null,"permalink":"/tags/sticky-session/","section":"Tags","summary":"","title":"Sticky Session","type":"tags"},{"content":" 前言 # 近期公司有 socket.io 的服務需要可以擴展，由於是聊天室服務，有實時的雙向通信需求（如聊天室、遊戲、實時協作工具）。保持用戶端保持與同一服務器的連接可以：\n確保消息順序一致性 減少連接重建的開銷 維護實時狀態同步 實作 # 建立 backendConfig 設定 sessionAffinity。 apiVersion: cloud.google.com/v1 kind: BackendConfig metadata: name: socket-backendconfig spec: sessionAffinity: affinityType: \u0026#34;GENERATED_COOKIE\u0026#34; # Options: NONE, CLIENT_IP, GENERATED_COOKIE affinityCookieTtlSec: 50 在 kubernetes 的 serivce 中，要先設定 network endpoint group (NEG)，設定的方式有兩種：\n獨立創建 NEG，不管有沒有套用 google load balancer 都會創建一個 NEG。 apiVersion: v1 kind: Service metadata: name: socket-service annotations: cloud.google.com/neg: \u0026#39;{\u0026#34;exposed_ports\u0026#34;: {\u0026#34;80\u0026#34;:{\u0026#34;name\u0026#34;: \u0026#34;NEG_NAME\u0026#34;}}}\u0026#39; cloud.google.com/backend-config: \u0026#39;{\u0026#34;default\u0026#34;: \u0026#34;socket-backendconfig\u0026#34;}\u0026#39; spec: selector: app: socket-app ports: - protocol: TCP port: 80 targetPort: 3000 type: ClusterIP sessionAffinity: ClientIP 依賴於 google load balancer 的自動創建 NEG。 apiVersion: v1 kind: Service metadata: name: socket-service annotations: cloud.google.com/neg: \u0026#39;{\u0026#34;exposed_ports\u0026#34;: {\u0026#34;80\u0026#34;:{\u0026#34;name\u0026#34;: \u0026#34;NEG_NAME\u0026#34;}}}\u0026#39; cloud.google.com/backend-config: \u0026#39;{\u0026#34;default\u0026#34;: \u0026#34;socket-backendconfig\u0026#34;}\u0026#39; spec: selector: app: socket-app ports: - protocol: TCP port: 80 targetPort: 3000 type: ClusterIP sessionAffinity: ClientIP 建立 ingress\n這邊要特別注意的是，這次實作使用的是 GCP 的 NEG，也是要搭配 GCP 的 load balancer 使用。才能達到這次實作的需求。\n因為有使用自己在 github 開源專案建立的 ingress-nginx，進行串接。但是在前端檢查的時候，發現沒有 GCLB 這個 cookie。\napiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: socket-ingress annotations: kubernetes.io/ingress.class: \u0026#34;gce\u0026#34; spec: rules: - host: socket.example.com http: paths: - path: / pathType: Prefix backend: service: name: socket-service port: number: 80 都建立完成以後，可以透過以下方式進行測試\ncurl -i -X GET http://socket.example.com 問題反思 # 這次的實作過程中，雖然這種方式看似可以保持客戶端與同一伺服器的連結。但因為是聊天室服務，可能還是會有一些問題需要注意：\n⭐️ session 親和性只是盡可能保持連接在同一伺服器，但無法保證 100% 的穩定性。如果第一次連線是連到 A 伺服器，之後因為某些原因（如伺服器重啟、負載均衡策略變更等）可能會被導向 B 伺服器，這樣就會導致用戶體驗不一致。\n下面是跟一位同仁討論之後紀錄下來的簡易架構圖：\nclient 端在一開始連線的時候，就是不同的 socket_server client 端同時也會透過 api 對 MQ 或者 Pub/Sub 進行訊息的發送。 server 端可以依據 channel 進行訊息的路由。同步給在不同 socket_server 的 client。 flowchart client01 client02 MQ/Pub_Sub socket_server01 socket_server02 client01 --channel,msg--\u003e MQ/Pub_Sub client02 --channel,msg--\u003e MQ/Pub_Sub MQ/Pub_Sub --\u003e socket_server01 MQ/Pub_Sub --\u003e socket_server02 socket_server01 \u003c--\u003e client01 socket_server02 \u003c--\u003e client02 參考資料 # 在 GCP/GKE 的 Ingress 設定 sticky session 透過獨立的區域 NEG 使用容器原生負載平衡 同事的腦袋 🧠 ","date":"24 August 2025","externalUrl":null,"permalink":"/journals/k8s/sticky-session/","section":"Journals","summary":"","title":"sticky session in kubernetes","type":"journals"},{"content":" 介紹 # Helm 是 Kubernetes 的一個套件管理工具，可以用來簡化應用程式的部署和管理。它使用稱為 Chart 的包裝格式來定義應用程式的各種資源，並提供了一個簡單的命令列介面來安裝、升級和管理這些應用程式。\n函數 # Helm 提供了一些內建函數，可以在 Chart 的模板中使用。這些函數可以用來處理字串、數字、日期等資料類型，並提供了一些常用的功能，可以幫助開發者更方便地編寫和維護 Helm Chart。\nquote # 將字符串使用雙引號包裹起來。\n⭐️ sqoute 表示單引號。\nuserName: User1 {{ .Values.userName | quote }} 得到結果: \u0026ldquo;User1\u0026rdquo;\n{{ .Values.userName | squote }} 得到結果: \u0026lsquo;User1\u0026rsquo;\nreplace # 字串替換。\n\u0026#34;I Am Henry VIII\u0026#34; | replace \u0026#34; \u0026#34; \u0026#34;-\u0026#34; 得到結果: I-Am-Henry-VIII\ndict # 傳遞 key 和 value 來創建一個 map。\n{{ $myDict := dict \u0026#34;key1\u0026#34; \u0026#34;value1\u0026#34; \u0026#34;key2\u0026#34; \u0026#34;value2\u0026#34; }} 得到結果:\n{ \u0026#34;key1\u0026#34;: \u0026#34;value1\u0026#34;, \u0026#34;key2\u0026#34;: \u0026#34;value2\u0026#34; } fromYaml # 將 YAML 字串轉換為 Object。\nfilePath: yamls/person.yaml\nname: Bob age: 25 hobbies: - hiking - fishing - cooking {{- $person := .Files.Get \u0026#34;yamls/person.yaml\u0026#34; | fromYaml }} 得到結果:\n{ \u0026#34;name\u0026#34;: \u0026#34;Bob\u0026#34;, \u0026#34;age\u0026#34;: 25, \u0026#34;hobbies\u0026#34;: [ \u0026#34;hiking\u0026#34;, \u0026#34;fishing\u0026#34;, \u0026#34;cooking\u0026#34; ] } merge, mergeOverwrite # ⚠️ 注意目標 dst 優先\ndst: default: default overwrite: me key: true src: overwrite: overwritten key: false {{- $newdict := merge .Values.dst .Values.src }} 得到結果:\nnewdict: default: default overwrite: me key: true 如果要以 src 為優先，可以使用 mergeOverwrite 函數。\n{{- $newdict := mergeOverwrite .Values.dst .Values.src }} 得到結果:\nnewdict: default: default overwrite: overwritten key: false omit # 類似 pick，用於從 map 中排除指定的 key。\nvolumeMounts: - name: my-volume mountPath: /data claimName: my-pvc {{- range $mount := .volumeMounts }} {{- $cleanMount := omit $mount \u0026#34;claimName\u0026#34; }} {{- end }} 得到結果:\nvolumeMounts: - name: my-volume mountPath: /data append # 在已有陣列中追加一個元素，建立一個新的陣列。\nmyArray: - element1 - element2 - element3 {{- $newArray := append .Values.myArray \u0026#34;newElement\u0026#34; }} 得到結果:\nnewArray: - element1 - element2 - element3 - newElement 參考資料 # 模板函数列表\n","date":"19 August 2025","externalUrl":null,"permalink":"/posts/helm/","section":"Posts","summary":"","title":"Kubernetes 管理工具 Helm","type":"posts"},{"content":"","date":"19 August 2025","externalUrl":null,"permalink":"/tags/nginx/","section":"Tags","summary":"","title":"Nginx","type":"tags"},{"content":"","date":"19 August 2025","externalUrl":null,"permalink":"/tags/redirect/","section":"Tags","summary":"","title":"Redirect","type":"tags"},{"content":"","date":"19 August 2025","externalUrl":null,"permalink":"/tags/rewrite/","section":"Tags","summary":"","title":"Rewrite","type":"tags"},{"content":" 前言 # 近期公司網站要下架舊的前端網頁服務，ingress 調整後端服務路由。\n新的服務網頁使用 nginx rewrite 網址路徑。在檢查過程中發現在瀏覽器頁面會發現有些前端靜態資源渲染不出來，呈現空白的頁面。\n後來使用 redirect 方式，問題解決了。\n所以想要記錄一下 rewrite 和 redirect 的差異。\nrewrite # nginx 的 rewrite 是一種用於修改請求 URL 的規則機制，常用於 URL 重寫、導向（redirect）、隱藏真實路徑或做 SEO 優化。\n基本語法 # regex: 正則表達式，用於匹配原始的請求路徑。 replacement: 要替換成的新路徑或新的 URL。 flag: 可選的標誌，控制 rewrite 行為的標誌，例如 permanent（301）、redirect（302）、break、last 等。 rewrite regex replacement [flag]; 參數 # last: 停止 rewrite，將請求重新在 server 或 location 區塊中尋找匹配（一般用於 location 區塊內）。 break: 停止 rewrite，但不再重新尋找匹配，直接執行後續配置。 redirect: 臨時重定向（302），告知瀏覽器請求新的 URL。 permanent: 永久重定向（301）。 last 跟 break 屬於內部重定向，不會改變瀏覽器的 URL。\n當用戶訪問 /old/path 時，將其重寫為 /new/path。\nlocation / { rewrite ^/old/(.*)$ /new/$1 last; } redirect 跟 permanent 屬於外部重定向，會改變瀏覽器的 URL。\n當用戶訪問 /old/path 時，將其重定向到 /new/path。\nlocation / { rewrite ^/old/(.*)$ /new/$1 redirect; } 問題反思 # 在使用 rewrite 的過程中，發現某些靜態資源無法正確加載，這可能是因為 rewrite 會在內部處理請求，而不會改變瀏覽器的 URL，導致某些資源的請求路徑不正確。相對而言，redirect 會直接告訴瀏覽器新的請求路徑，從而避免了這個問題。\n參考 # 什麼是 Nginx rewrite？轉址的機制，與 return 301 或 redirect 的差別在哪？\n","date":"19 August 2025","externalUrl":null,"permalink":"/journals/nginx/rewrite-vs-redirect/","section":"Journals","summary":"","title":"Rewrite vs Redirect","type":"journals"},{"content":"","date":"13 August 2025","externalUrl":null,"permalink":"/tags/job/","section":"Tags","summary":"","title":"Job","type":"tags"},{"content":" 介紹 # Job 表示一次性的任務，執行完成後就會停止。\nJob 可以建立一個或多個 Pod，並將繼續重試 Pod 的執行，直到指定數量的 Pod 成功終止。\n範例 # 在系統中部署新的服務時，可以使用 Job 透過 command line 工具或腳本來執行一次性的任務，例如資料庫遷移、批次處理等。\nbackoffLimit 指定 Pod 失敗後重試的次數。如果重試次數超過此限制，Job 將被標記為失敗。 restartPolicy Pod 的容器可能因為多種不同的原因失效，\u0026ldquo;OnFailure\u0026rdquo; 使 Pod 留在目前的節點上，但容器會重新運行。 \u0026ldquo;Never\u0026rdquo;，當 Pod 失敗時，Job 控制器會啟動新的 Pod。 只能使用 \u0026ldquo;Never\u0026rdquo; 或 \u0026ldquo;OnFailure\u0026rdquo;。 activeDeadlineSeconds 指定 Job 的最大執行時間（以秒為單位）。如果 Job 在此時間內未完成，則會被終止。 activeDeadlineSeconds 的優先權高於 backoffLimit。 ttlSecondsAfterFinished Job 完成後預設是不會被清理的，可以設定此參數來指定 Job 完成後保留的時間（以秒為單位）。當時間到達後，Job 將被自動清理。 apiVersion: batch/v1 kind: Job metadata: name: example-job spec: ttlSecondsAfterFinished: 60 activeDeadlineSeconds: 30 template: spec: containers: - name: example image: example-image command: [\u0026#34;sh\u0026#34;, \u0026#34;-c\u0026#34;, \u0026#34;echo Hello, Kubernetes! \u0026amp;\u0026amp; sleep 30\u0026#34;] restartPolicy: Never backoffLimit: 4 參考資料 # Kubernetes 文檔/概念/工作負載/工作負載管理/Job\n","date":"13 August 2025","externalUrl":null,"permalink":"/posts/k8s/job/","section":"Posts","summary":"","title":"Kubernetes Job","type":"posts"},{"content":"","date":"18 July 2025","externalUrl":null,"permalink":"/tags/datadog/","section":"Tags","summary":"","title":"Datadog","type":"tags"},{"content":" 事件過程 # 近期公司同仁反應 Datadog 停止收集 Kafka 的 metrics，因此進行調查的過程紀錄下來。未來若有同樣的問題，可以參考這篇文章。\n架構圖 # 先來了解 Kafka Metrics 是如何被 Datadog 收集的。\ndatadog agent 會部署在 Kubernetes 環境中，且在 node 中運行。它會收集 Pod 服務的 metrics，也會收集 Kafka 的 metrics。\n要注意的是 Kafka 的 VM 並沒有部署 Datadog Agent，而是透過 Kubernetes 的 Datadog Agent 來收集 Kafka 的 metrics。\n這麼做的原因是 Datadog Agent 收費是會依照使用的 Agent 數量計算的，可以減少費用的產生。\nflowchart LR subgraph kubernetes subgraph node datadog[Datadog Agent] pod[Pod] end end subgraph GCE kafka[Kafka] end datadog --\u003e|收集| kafka datadog --\u003e|收集| pod 問題調查 # 確認網路連線是否異常\n首先確認 Datadog Agent 是否能夠連線到 Kafka 的 metrics endpoint。要怎麼確認 kafka 的 metrics endpoint 是什麼呢？\n可以在 Kafka 的配置檔中找到，在配置監控系統時，如果採用 JMX Exporter 的方式，則可以在 /etc/systemd/system/kafka.service 中找到。\n-Dcom.sun.management.jmxremote.port 這個參數指定了 JVM 的 JMX 監控端口，通常是 9134。\n-Dcom.sun.management.jmxremote.rmi.port 這個參數指定了 RMI (遠端方法呼叫) 的端口。預設情況下，JMX 會動態選擇一個 RMI 端口，在雲端或防火牆環境下，動態端口會導致難以開放防火牆規則。\n將 jmxremote.port 和 jmxremote.rmi.port 設定相同，可確保 JMX 監控只啟用此連接埠（此處為 9134）例，防火牆設定和安全監控管。\n[Service] Environment=\u0026#34;KAFKA_JMX_OPTS=-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.port=9134 -Djava.rmi.server.hostname=10.106.10.151\u0026#34; 檢查 VM 監控端口是否開啟\nnetstat -an | grep 9134 # 或 lsof -i :9134 檢查 Kubernetes 到 Kafka 的連線是否異常\n在 Kubernetes 環境中，建立一個 Pod，並使用 curl 或 telnet 測試連線到 Kafka 的 JMX 端口。\napiVersion: v1 kind: Pod metadata: name: test-kafka-JMX-connection spec: containers: - name: test-container image: maven:3.8-openjdk-11 command: - /bin/sh - -c args: - | echo \u0026#34;Downloading jmxterm...\u0026#34; curl -L -s -o /tmp/jmxterm.jar https://github.com/jiaqi/jmxterm/releases/download/v1.0.2/jmxterm-1.0.2-uber.jar; echo \u0026#34;Testing JMX connection to Kafka...\u0026#34; get -b kafka.server:type=KafkaServer,name=BrokerState Value | java -jar /tmp/jmxterm.jar -n -l ${KAFKA_JMX_HOST}:9134; echo \u0026#34;JMX connection test completed.\u0026#34; resources: limits: memory: \u0026#34;512Mi\u0026#34; cpu: \u0026#34;500m\u0026#34; requests: memory: \u0026#34;256Mi\u0026#34; cpu: \u0026#34;250m\u0026#34; 確認 Datadog Agent 的配置\n確認 Datadog Agent 的配置檔中是否有正確設定 Kafka 的 JMX 端口。通常在 /etc/datadog-agent/conf.d/kafka.d/conf.yaml 中。\ninit_config: {} instances: - host: \u0026lt;kafka-host\u0026gt; port: 9134 tags: - kafka Datadog Support 建議\nSupport 團隊建議可以使用 flare 傳遞資訊到案件單上，有助於排查問題。\n在 kafka integration 中，設置 checke runner (cluster_check: true) 。因此可以 flare cluster agent 的資訊。\nkubectl exec -it \u0026lt;NAMESPACE\u0026gt; -it \u0026lt;CLUSTER_POD_NAME\u0026gt; -- agent flare \u0026lt;案件編號\u0026gt; 檢查 cluster check runners。\nkubectl exec -n \u0026lt;NAMESPACE\u0026gt; -it \u0026lt;CLUSTER_POD_NAME\u0026gt; -- agent clusterchecks Datadog Support 回饋\n在提供上述的資訊之後，Support 團隊回覆在 flare (status.log) 中發現了 kafka instance 檢查的狀態。\n========= JMX Fetch ========= Information ================== runtime_version : 11.0.23 version : 0.49.1 Initialized checks ================== kafka - instance_name: kafka-02 metric_count: 346 service_check_count: 0 message: \u0026lt;no value\u0026gt; status: OK - instance_name: kafka-01 metric_count: 350 service_check_count: 0 message: Number of returned metrics is too high for instance: kafka-01. Please read http://docs.datadoghq.com/integrations/java/ or get in touch with Datadog Support for more details. Truncating to 350 metrics. status: WARNING - instance_name: kafka-03 metric_count: 350 service_check_count: 0 message: Number of returned metrics is too high for instance: kafka-03. Please read http://docs.datadoghq.com/integrations/java/ or get in touch with Datadog Support for more details. Truncating to 350 metrics. status: WARNING kafka-01 和 kafka-03 出現以下警告訊息：\nNumber of returned metrics is too high for instance: kafka-03. Please read http://docs.datadoghq.com/integrations/java/ or get in touch with Datadog Support for more details. Truncating to 350 metrics. Support 團隊建議由於傳回的指標數量太高，即高於預設值 350。因此，要解決此問題，您可以新增 max_returned_metrics 參數並將值設為高於 350。\n修正範例與驗證 # 在 values.yaml 中我們需要新增 max_returned_metrics 參數，並將其值設為需要的值且大於預設值。\nclusterAgent: confd: kafka.yaml: |- cluster_check: true init_config: is_jmx: true collect_default_metrics: true new_gc_metrics: true instances: - host: \u0026lt;kafka-host\u0026gt; port: 9134 name: kafka-01 max_returned_metrics: 500 # 新增此行 然後重新部署 Datadog Agent。\n使用 agent status 檢查 Kafka 的 metrics count 是否已經超過 350 且 status 從 WARNING 變為 OK。\n因為 agent 有很多台，所以使用腳本的方式去找到收集 Kafka VM Metrics 的 agent。\n#!/bin/bash echo \u0026#34;Found pods:\u0026#34; kubectl get pods -n datadog -o custom-columns=NAME:.metadata.name --no-headers | grep -v cluster read -p \u0026#34;搜尋關鍵字：\u0026#34; SEARCH_KEYWORD PODS=$(kubectl get pods -n datadog -o custom-columns=NAME:.metadata.name --no-headers | grep -v cluster) IFS=$\u0026#39;\\n\u0026#39; read -d \u0026#39;\u0026#39; -r -a POD_ARRAY \u0026lt;\u0026lt;\u0026lt; \u0026#34;$PODS\u0026#34; for POD in \u0026#34;${POD_ARRAY[@]}\u0026#34;; do echo \u0026#34;Checking status of pod: $POD\u0026#34; kubectl exec -n datadog $POD -c agent -- agent status | grep \u0026#34;$SEARCH_KEYWORD\u0026#34; if [ $? -ne 0 ]; then echo \u0026#34;No kafka-common found in $POD\u0026#34; fi echo \u0026#34;----------------------------------------\u0026#34; done echo \u0026#34;Finished checking all pods.\u0026#34; 找到對應的 agent 之後，使用以下指令檢查 Kafka 的 metrics count。\n可以看到 kafka 的 metrics count 已經超過 350 且 status 從 WARNING 變為 OK。\n========= JMX Fetch ========= Information ================== runtime_version : 11.0.23 version : 0.49.1 Initialized checks ================== kafka - instance_name: kafka-02 metric_count: 346 service_check_count: 0 message: \u0026lt;no value\u0026gt; status: OK - instance_name: kafka-01 metric_count: 476 service_check_count: 0 message: \u0026lt;no value\u0026gt; status: OK - instance_name: kafka-03 metric_count: 443 service_check_count: 0 message: \u0026lt;no value\u0026gt; status: OK Failed checks ============= no checks 問題反思 # 這次問題的根本原因是 Kafka 的 metrics 數量超過了 Datadog 的預設限制，導致部分 metrics 無法被收集。\n如果不提高 max_returned_metrics 的上限值，有方法可以降低 metric_count 的數量嗎？\nsupport 提供以下的方式減少 metric_count 的數量：\nmetric_count 值是每個 instance 的 Datadog 指標總數。\n對於 Kafka integration，Datadog 指標是藉助此文件 metrics.yaml （ 連結 ） 生成的，如果集成找到具有匹配的 domain、bean 和 attribute 的 jmx 指標，它將生成 Datadog 指標。\n如果您想要減少 metric_count 值，您可以將此參數變更為 collect_default_metrics 為 false ，這樣就不會使用 metrics.yaml ，然後建立您自己的配置以專門匹配任何 jmx 指標以從此範例（ 連結 ）產生 Datdog 指標。\n參考資料 # 聊聊 Kafka 如何基於 JMX 監控\n","date":"18 July 2025","externalUrl":null,"permalink":"/journals/datadog/datadog-stopped-collecting-kafka-metrics/","section":"Journals","summary":"","title":"Datadog Stopped Collecting Kafka Metrics","type":"journals"},{"content":"","date":"18 July 2025","externalUrl":null,"permalink":"/tags/kafka/","section":"Tags","summary":"","title":"Kafka","type":"tags"},{"content":"","date":"18 July 2025","externalUrl":null,"permalink":"/tags/metrics/","section":"Tags","summary":"","title":"Metrics","type":"tags"},{"content":"","date":"14 July 2025","externalUrl":null,"permalink":"/tags/lint/","section":"Tags","summary":"","title":"Lint","type":"tags"},{"content":"","date":"14 July 2025","externalUrl":null,"permalink":"/tags/yaml/","section":"Tags","summary":"","title":"Yaml","type":"tags"},{"content":" 介紹 # yamllint 不僅檢查語法的有效性，還檢查諸如鍵重複之類的異常情況以及諸如行長度、尾隨空格、縮排等外觀問題。\n有時候尾隨空格可能會導致 YAML 文件解析錯誤，這在處理大型配置文件時尤其重要。\n對於時常維護大量 YAML 文件的開發者來說是一個非常有用的工具。\n安裝 # brew install yamllint 使用 # 當前的目錄下對所有 YAML 文件進行檢查。\n如果有 .yamllint 配置文件，預設會自動載入文件的配置。\nyamllint . 預設 # yamllint 有一些預設檔案配置。\n--- yaml-files: # 這個配置指定了要檢查的 YAML 文件類型。 - \u0026#39;*.yaml\u0026#39; - \u0026#39;*.yml\u0026#39; - \u0026#39;.yamllint\u0026#39; rules: anchors: enable braces: enable brackets: enable colons: enable commas: enable comments: level: warning comments-indentation: level: warning document-end: disable document-start: level: warning empty-lines: enable empty-values: disable float-values: disable hyphens: enable indentation: enable key-duplicates: enable key-ordering: disable line-length: enable new-line-at-end-of-file: enable new-lines: enable octal-values: disable quoted-strings: disable trailing-spaces: enable truthy: level: warning yamllint 還有另一個預定義的配置叫 relaxed，顧名思義就是放寬檢查，寬容性更高。\nyamllint -d relaxed file.yml 配置 # 如果需要自定義配置，可以不必全部的配置都重新定義一次。使用 extends 關鍵字擴展預設配置。\nextends: default line-length: max: 80 level: warning 忽略檢查 # 如果像 helm template 雖然是 YAML 文件，但是使用 go template 語法，這時候可以使用 ignore 來忽略檢查。\nignore: - *.template.yaml 參數規則 # anchers：檢查錨點和別名的使用情況。 braces：控制 Flow Mapping 中大括號的使用。 brackets：控制 Flow Sequence 中方括號的使用。 document-start：檢查文件開頭是否有 ---。 rules: anchers: # 回報重複的錨點跟未聲明的錨點。 forbid-undeclared-aliases: true # 未聲明 forbid-duplicated-anchors: false # 重複 forbid-unused-anchors: false # 未使用 braces: # 控制 Flow Mapping (內行型的鍵值對) 中大括號的使用。 forbid: false # 是否禁止使用 Flow Mapping min-spaces-inside: 0 # 大括號內部的最小空格數 max-spaces-inside: 0 # 大括號內部的最大空格數 min-spaces-inside-empty: -1 # 空大括號內部的最小空格數 max-spaces-inside-empty: -1 # 空大括號內部的最大空格數 brackets: # 控制 Flow Sequence (內行型的列表) 中方括號 forbid: false # 是否禁止使用 Flow Sequence min-spaces-inside: 0 max-spaces-inside: 0 min-spaces-inside-empty: -1 max-spaces-inside-empty: -1 document-start: # 檢查文件開頭是否有 `---`。 present: true # 是否需要 `---` 開頭 empty-lines：檢查空行的使用情況。 為了規範團隊的代碼風格，可以設定空行的數量。很常有人會使用過多的空行來分隔代碼塊，這樣會導致代碼不易閱讀。\nrules: empty-lines: max: 1 # 允許的最大空行數 example\npass\n- foo: - 1 - 2 - bar: [3, 4] fail\n- foo: - 1 - 2 - bar: [3, 4] indentation：控制縮排的使用情況。\nYaml 的文件縮進非常靈活，但為了保持一致性，通常會使用兩個空格或四個空格來縮排。為了更好的規範團隊的代碼風格，可以設定縮排的空格數量。\nindent-sequences 決定對於序列 (array) 的縮排方式，提供四種選擇：true、false、consistent 和 whatever。\nconsistent 要求所有的序列都使用相同的縮排方式，可以全部都縮排或者全部都不縮排。\nwhatever 則允許序列的縮排方式不一致，這樣可以更靈活地處理不同的情況。\nrules: indentation: spaces: 2 # 縮排使用的空格數量 indent-sequences: true example pass\nhistory: - name: Unix date: 1969 - name: Linux date: 1991 進階用法 # 錨點和別名 # YAML 中的錨點 (anchors) 和別名 (aliases) 用於重複使用相同的值或結構，這樣可以減少重複代碼並提高可讀性。\n以下面的 YAML 為例：\n先使用 \u0026amp; 定義一個錨點，然後使用 * 來引用這個錨點。\ndefault_config 錨點設定了 key1 和 key2 會重複使用到的設定值。接著使用 \u0026lt;\u0026lt; 來合併這個錨點到其他配置中。\n\u0026amp;default_config: key1: value1 key2: value2 my_config: \u0026lt;\u0026lt;: *default_config # 使用別名來引用錨點 key3: value3 another_config: \u0026lt;\u0026lt;: *default_config # 再次使用同一個錨點 key4: value4 Flow Mapping # Flow Mapping 是 YAML 中一種使用大括號 {} 來表示鍵值對的方式。這種方式通常用於簡化表示多個鍵值對的情況。\n一般來說，最常見、易讀的寫法是 Block Mapping，即使用縮排的方式來表示層級關係。\n# Block Mapping key1: subkey1: value1 subkey2: value2 # Flow Mapping key1: {subkey1: value1, subkey2: value2} 參考資料 # yamllint 文檔\n用 YAML anchors \u0026amp; aliases 寫出更好維護的 docker compose file\n","date":"14 July 2025","externalUrl":null,"permalink":"/posts/tool/yamllint/","section":"Posts","summary":"","title":"Yamllint","type":"posts"},{"content":"","date":"2 July 2025","externalUrl":null,"permalink":"/tags/autoscaling/","section":"Tags","summary":"","title":"Autoscaling","type":"tags"},{"content":"","date":"2 July 2025","externalUrl":null,"permalink":"/tags/helm/","section":"Tags","summary":"","title":"Helm","type":"tags"},{"content":"最近在升級 helm release 時遇到了一個錯誤，提示 \u0026ldquo;no matches for kind HorizontalPodAutoscaler in version autoscaling/v2beta2\u0026rdquo;。\n原因是因為 Kubernetes 的 API 版本變更，導致舊的資源定義不再匹配新的 API 版本。\n解決方法 # 因為是正式站環境的 log 服務，log 的收集對 RD 在排查問題時非常重要。如果刪除後再重新部署，會導致 log 的丟失。\nHelm3 支援一款插件工具 mapkubeapis，可以將有使用已棄用或已刪除的 API 的 Helm Release metadata 更新為有受支援的 API。\nhelm mapkubeapis -n kube_namespace releaseName 參考資料 # Helm upgrade failure due to deprecated or removed Kubernetes API\n","date":"2 July 2025","externalUrl":null,"permalink":"/journals/k8s/kube-object-no-match-for-kind-version/","section":"Journals","summary":"","title":"Kube Object No Match for Kind Version","type":"journals"},{"content":"","date":"2 July 2025","externalUrl":null,"permalink":"/tags/mapkubeapis/","section":"Tags","summary":"","title":"Mapkubeapis","type":"tags"},{"content":" 介紹 # PersistentVolume (PV) 是 Kubernetes 中的一種資源，用於提供持久化存儲。它是集群級別的存儲資源，與 Pod 的生命週期無關。PV 可以被多個 Pod 共享，並且可以在 Pod 重啟或重新調度時保持數據不變。\n管理員可以事先創建 PV，並將其與特定的存儲系統（如 NFS、iSCSI、雲存儲等）綁定，或使用動態存儲類別（StorageClass）來自動創建 PV。\nPersistentVolumeClaim (PVC) 是用戶請求存儲的方式。概念與 Pod 請求資源類似。用戶可以創建 PVC，指定所需的存儲大小和存取模式 (如 ReadWriteOnce、ReadOnlyMany 等)。Kubernetes 會根據 PVC 的要求來匹配合適的 PV。\n存取模式 # ReadWriteOnce (RWO)：卷只能被單個節點以讀寫方式掛載。 ReadOnlyMany (ROX)：卷可以被多個節點以只讀方式掛載。 ReadWriteMany (RWX)：卷可以被多個節點以讀寫方式掛載。 PersistentVolume 與 PersistentVolumeClaim # graph pod(Pod) volume(Volume) PVC(PersistentVolumeClaim) PV(PersistentVolume) storageClass(StorageClass) storageProvider(StorageProvider) storage(Physical Storage) pod --\u003e|使用| volume --\u003e PVC PVC --\u003e|靜態綁定| PV PVC --\u003e|動態綁定| storageClass PV --\u003e|提供存儲| storage storageClass --\u003e|配置存儲| storageProvider --\u003e|實際存儲| storage 如果 PVC storageClassName 屬性為 \u0026quot;\u0026quot;，則表示使用靜態綁定。這意味著 PVC 自身禁止使用動態製備的卷。\n參考 # [Kubernetes / K8s] PV/ PVC 儲存大小事交給PV/PVC管理\n持久卷\n","date":"2 July 2025","externalUrl":null,"permalink":"/posts/k8s/storage/","section":"Posts","summary":"","title":"PersistentVolume","type":"posts"},{"content":"","date":"2 July 2025","externalUrl":null,"permalink":"/tags/storage/","section":"Tags","summary":"","title":"Storage","type":"tags"},{"content":" 介紹 # 每個 StorageClass 都包含 provisioner、parameters、reclaimPolicy 這些字段會在 StorageClass 需要動態配置 PersistentVolume 以滿足 PersistentVolumeClaim (PVC) 使用到。\napiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: low-latency provisioner: csi-driver.example-vendor.example reclaimPolicy: Retain # 默认值是 Delete allowVolumeExpansion: true mountOptions: - discard # 这可能会在块存储层启用 UNMAP/TRIM volumeBindingMode: WaitForFirstConsumer parameters: guaranteedReadWriteLatency: \u0026#34;true\u0026#34; # 这是服务提供商特定的 磁碟區插件 # 每個 StorageClass 都會有 provisioner ，用來決定使用哪個磁碟區插件。(必要)\n卷插件 內置製備器 配置範例 AzureFile ✓ Azure File CephFS - - FC - - FlexVolume - - iSCSI - - Local - Local NFS - NFS PortworxVolume ✓ Portworx Volume RBD ✓ Ceph RBD VsphereVolume ✓ vSphere 回收策略 # StorageClass 動態建立的 PersistentVolume 會在 reclaimPolicy 指定回收策略，可以是 Delete 或 Retain 。預設是 Delete 。\n擴展 # PersistentVolume 可以配置為可擴充，允許透過 PVC 物件來調整磁碟區大小，申請一個新的、更大的儲存容量。當 allowVolumeExpansion 定義為 true 時，下列類型的磁碟區支援擴充。\n卷類型 卷擴展的Kubernetes 版本要求 Azure File 1.11 CSI 1.24 FlexVolume 1.13 Portworx 1.11 rbd 1.11 掛載選項 # 由 StorageClass 動態建立的 PersistentVolume 將使用類別中 mountOptions指定的掛載選項。\n如果磁碟區插件不支援掛載選項，卻指定了掛載選項，則製備操作會失敗。 掛載選項在 StorageClass 和 PV 上都不會做驗證。如果其中一個掛載選項無效，那麼這個PV 掛載作業就會失敗。\n參考 # 動態卷製備\n","date":"1 July 2025","externalUrl":null,"permalink":"/posts/k8s/storageclass/","section":"Posts","summary":"","title":"StorageClass","type":"posts"},{"content":"","date":"24 September 2023","externalUrl":null,"permalink":"/tags/m1/","section":"Tags","summary":"","title":"M1","type":"tags"},{"content":"","date":"24 September 2023","externalUrl":null,"permalink":"/tags/m2/","section":"Tags","summary":"","title":"M2","type":"tags"},{"content":"","date":"24 September 2023","externalUrl":null,"permalink":"/tags/mac/","section":"Tags","summary":"","title":"Mac","type":"tags"},{"content":"","date":"24 September 2023","externalUrl":null,"permalink":"/tags/minikube/","section":"Tags","summary":"","title":"Minikube","type":"tags"},{"content":" 📝介紹 # Minikube 是一個輕量級的 Kubernetes 環境，主要用於本地開發和測試。它可以在本機電腦上啟動一個單節點的 Kubernetes 集群，讓開發者可以快速地部署和測試應用程式。\nMinikube 支援多種虛擬化技術，包括 VirtualBox、VMware、KVM、Hyper-V 等等。它可以在 Windows、macOS 和 Linux 上運行。\n🖥️Qemu 虛擬化驅動 # 介紹的部分有提到 Minikube 支援多種虛擬化技術，而在 macOS 上，特別是使用 Apple Silicon（M1、M2）晶片的 Mac 電腦上，傳統的虛擬化技術如 VirtualBox 並不支援 ARM 架構（至少我出這篇文章的時候還沒有ＱＱ）。\n因此，我們可以使用 Qemu 作為 Minikube 的虛擬化驅動程式。\n📖Qemu 簡介 # Qemu 是一個開源的虛擬化軟體，可以模擬多種硬體架構，包括 x86、ARM、MIPS 等等。它可以用於虛擬化和模擬不同的作業系統和應用程式。\nQemu 的主要功能包括：\n使用者程式模擬：QEMU 能夠將為一個平台編譯的二進位檔案運行在另一個不同的平台。 系統虛擬化模擬：QEMU 能夠模擬一個完整的系統虛擬機，該虛擬機有自己的虛擬CPU、晶片組、虛擬記憶體以及各種虛擬外部設備，能夠為虛擬機中運行的作業系統和應用軟體呈現出與實體電腦完全一致的硬體視圖。QEMU能夠模擬 x86、ARM、MIPS、PPC 等多個平台。 🔧安裝 # 在 macOS 上安裝 Minikube 和 Qemu，可以使用 Homebrew 來簡化安裝過程。 首先，確保你已經安裝了 Homebrew。如果還沒有安裝，可以參考 Homebrew 官方網站 進行安裝。\nbrew install minikube brew install qemu 🌐Socket_vmnet 網路驅動 # Qemu 提供兩種網路選項：\nsocket_vmnet builtin（內建網路） socket_vmnet 提供 Minikube 完整的網路功能，而 builtin 網路則有一些限制。例如：service 和 tunnel 等指令不可用。\nbrew install socket_vmnet brew tap homebrew/services HOMEBREW=$(which brew) \u0026amp;\u0026amp; sudo ${HOMEBREW} services start socket_vmnet 🚀啟動 # 上述所有的套件安裝完成後，可以使用以下命令啟動 Minikube，並指定使用 Qemu 作為虛擬化驅動程式。\nminikube start --driver qemu --network socket_vmnet 📚參考文獻 # How to Setup Minikube on MAC M1/M2 Minikube QEMU ","date":"24 September 2023","externalUrl":null,"permalink":"/posts/minikube/","section":"Posts","summary":"","title":"使用 Minikube 在本機電腦模擬 Kubernetes 環境（Mac M1/M2）","type":"posts"},{"content":"","date":"22 September 2023","externalUrl":null,"permalink":"/tags/cicd/","section":"Tags","summary":"","title":"Cicd","type":"tags"},{"content":" 介紹 # GitLab 是一個開源的 DevOps 平台，提供了版本控制、CI/CD、代碼審查等功能。它的 CI/CD 功能可以幫助開發者自動化軟體開發流程，提高開發效率和質量。\nGitLab 的 CI/CD 功能基於 GitLab Runner，允許開發者定義自動化的工作流程，從代碼提交到部署生產環境都可以自動化完成。\nGitLab CI/CD 的配置文件是 .gitlab-ci.yml，這個文件定義了 CI/CD 的工作流程，包括各個階段（stages）、任務（jobs）和執行的腳本（scripts）。開發者可以根據自己的需求定義不同的工作流程，並且可以在不同的階段中執行不同的任務。\n基本配置 # 以下是一個簡單的 .gitlab-ci.yml 配置範例，包含了三個階段：test、build 和 deploy。\nstages: - test - build - deploy test: stage: test script: # 定義 test 階段的指令 - echo \u0026#34;Running tests\u0026#34; # 執行單元測試、程式碼品質檢查等 - npm test - npm run lint build: stage: build script: # 定義 build 階段的指令 - echo \u0026#34;Building the application\u0026#34; - docker build -t $IMAGE_NAME:$TAG . # 只有當測試通過後才執行 build needs: [\u0026#34;test\u0026#34;] deploy: stage: deploy script: # 定義 deploy 階段的指令 - echo \u0026#34;Deploying to production\u0026#34; # 在實際情況下，這裡可以是部署到 Kubernetes、AWS、GCP 等的相應指令 # 也可以使用 Helm 進行部署 # 只有當 build 成功後才執行 deploy needs: [\u0026#34;build\u0026#34;] 進階配置 # 在生產環境中，通常需要更複雜的配置，例如：配置環境變數、建構 mysql 資料庫、redis 等服務。\nstages: - test - build - deploy test: stage: test services: - name: mysql:5.7 alias: db - redis:latest alias: redis variables: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: ${MYSQL_DATABASE} REDIS_PASSWORD: ${REDIS_PASSWORD} script: - echo \u0026#34;Running tests\u0026#34; - npm test - npm run lint build: stage: build script: - echo \u0026#34;Building the application\u0026#34; - docker build -t $IMAGE_NAME:$TAG . needs: [\u0026#34;test\u0026#34;] deploy: stage: deploy script: - echo \u0026#34;Deploying to production\u0026#34; needs: [\u0026#34;build\u0026#34;] parallel:matrix # ⚠️ 對於 runner 的配置，必須要有多台 runner 或者配置單台 runner 配置支援同時執行多個任務。\n⚠️ 矩陣的排列數不能超過 200\nparallel 可以在單一的 pipeline 中同時執行多個任務，這對於需要在多個環境或配置下運行相同任務的情況非常有用。\n如果需要在不同的 Node.js 版本和環境下運行測試，可以使用 parallel:matrix 來定義一個矩陣，這樣可以在不同的配置下同時運行相同的任務。\ntest: stage: test script: - echo \u0026#34;Running tests\u0026#34; parallel: matrix: - NODE_VERSION: [10, 12, 14] - ENV: [development, production] 參考 # Parallel ","date":"22 September 2023","externalUrl":null,"permalink":"/posts/gitlab/cicd/","section":"Posts","summary":"","title":"GitLab CICD","type":"posts"},{"content":"","date":"17 September 2023","externalUrl":null,"permalink":"/tags/cni/","section":"Tags","summary":"","title":"Cni","type":"tags"},{"content":"","date":"17 September 2023","externalUrl":null,"permalink":"/tags/node/","section":"Tags","summary":"","title":"Node","type":"tags"},{"content":"","date":"17 September 2023","externalUrl":null,"permalink":"/tags/pod/","section":"Tags","summary":"","title":"Pod","type":"tags"},{"content":" 前言 # 最近公司專案轉雲端架構時，由於服務只能啟用一個 pod 提供線上服務的運作，因此也選擇使用 statefulSet 部署服務，在這過程中發現的問題。\n事件流程 # RD 同仁更新了 code 到 gitLab，但沒有順利完成 CICD。原因是 StatefulSet pod 在關閉時停留在 terminating 狀態。雖然 k8s 有 terminationGracePeriodSeconds 設定，但由於情況特殊，當下的 terminationGracePeriodSeconds 設為 14400 秒，長達四小時。\n因為線上緊急問題，所以針對 terminating pod 採取了強制刪除：kubectl delete pod [pod name] --grace-period=0 --force。之後重新建立的 pod 就會出現以下錯誤訊息 👇\nWarning (combined from similar events): Failed to create pod sandbox: rpo error: code = Unknown desc = failed to setup network for sandbox \u0026#34;14fe0cd3d688aed4ffed4c36ffab1a145230449881bcbe4cac6478a63412b0c*: plugin type=*gke\u0026#34; failed (add): container veth name provided (etho) already exists Google Support # 其實前陣子這個錯誤已經影響到開發和測試環境了。由於這次影響到正式環境，我們把案件等級提升至 P1，並請 Google 協助查找錯誤發生的原因。\n經過 Google 協助分析，這次錯誤的主要原因如下：\n當 pod 進入關閉流程時，由於 terminationGracePeriodSeconds 設置為四小時，pod 仍處於 lifecycle 中 此時使用 kubectl delete pod --force 會導致 pod 雖然消失，但 container 設定仍殘留在 node 上 如果新的 pod 重新在同一顆 node 上啟動，就會造成相同的網路介面設定衝突 雖然改成 Deployment 可以規避此問題，但相對會浪費一組 IP。長久下來一樣會有問題，最重要的還是要讓 pod 完整結束整個 lifecycle，才不會產生後續問題。\n技術細節補充 # Container Network Interface (CNI) 在 Kubernetes 中負責管理 Pod 的網路設定。當 Pod 啟動時，CNI 會為其創建一個虛擬網路介面 (veth pair)，並將其連接到 Pod 的網路命名空間。\n參考資料 # Pods stuck on ContainerCreating after containerd is restarted\n","date":"17 September 2023","externalUrl":null,"permalink":"/journals/k8s/veth-name-provided-already-exists/","section":"Journals","summary":"","title":"解決 Kubernetes Pod 網路衝突：container veth name provided (eth0) already exists","type":"journals"},{"content":"","date":"27 August 2023","externalUrl":null,"permalink":"/tags/dockerfile/","section":"Tags","summary":"","title":"Dockerfile","type":"tags"},{"content":" 前言🔖 # 在上一篇文章中，我們介紹了 Docker 的基本概念和使用方法，也知道了 Docker Hub 上有官方以及社群維護的映像檔。實務上要如何來建構自己的映像檔呢？本文將會介紹 Dockerfile，並動手實作一個簡單的 Dockerfile 來建構一個 Golang 應用程式的映像檔，最後還會介紹一些 Dockerfile 的最佳實踐和安全性考量。\n實作的專案是一個簡單的 Golang 應用程式，這個應用程式會啟動一個 HTTP 伺服器，會顯示計算機的應用程式，實作專案的程式碼放在 GitHub 上，讀者們可以透過以下連結來查看程式碼：\nDockerfile 實作專案程式碼 如果想要複習上一篇文章可以透過以下連結來回顧：\nDocker 入門指南：從基礎概念到實際操作 30 July 2023\u0026middot;2 分鐘 Docker 容器化 虛擬機器 開發效率 什麼是 Dockerfile❓ # Dockerfile 可以想像成是一個工程藍圖，定義了如何建構一個 Docker 映像檔。它是一個純文本文件，包含了一系列的指令，每一條指令都對應著建構過程中的一個步驟。建構的過程中，會照著藍圖上的指令一步步地執行，最終產生一個符合需求的 Docker 映像檔。\nDockerfile 可以保證每次建構出來的映像檔在環境設定和依賴方面都是一致的，這對於部署和維護應用程式非常重要。Dockerfile 的指令包括了基礎映像、工作目錄、複製檔案、安裝套件、設定環境變數、暴露端口以及啟動命令等，這些指令可以組合起來定義出一個完整的建構過程。\nDockerfile 的基本語法結構🏗️ # 接著，開始來逐步的講解 Dockerfile 的基本語法結構，以下面的 Dockerfile 為例：\n# Build stage FROM golang:1.24-alpine AS builder WORKDIR /app COPY go.mod ./ RUN go mod download COPY . . RUN go build -o calculator . # Run stage FROM alpine:3.21 WORKDIR /app COPY --from=builder /app/calculator . COPY --from=builder /app/static ./static EXPOSE 8080 CMD [\u0026#34;./calculator\u0026#34;] FROM 首先第一個指令是 FROM，這個指令用來指定建構過程中使用的基礎映像，在這裡我們使用了官方的 Golang 1.24 的 Alpine 版本作為建構階段的基礎映像，並且給這個階段取了一個名字叫做 builder。為什麼要使用 AS builder 呢？後面補充會提到多階段建構的概念，所以先跳過這個部分，先專注在 FROM 指令的基本用法上。 WORKDIR 接下來是 WORKDIR 指令，這個指令用來設定工作目錄，在這裡我們設定了 /app 作為工作目錄，這樣後續的指令就會在這個目錄下執行。不必要使用 RUN mkdir -p /app 來建立目錄，因為 WORKDIR 指令會自動建立目錄。 COPY 指令用來將檔案從主機複製到容器中。 RUN 指令用來執行命令，這些命令會在建構過程中被執行，並且會產生對應的結果。 EXPOSE 指令用來暴露容器的端口，需要注意的是，EXPOSE 指令只是用來宣告容器會使用的端口，並不會實際地開放端口，實際上要開放端口還需要在啟動容器的時候使用 -p 選項來映射端口。 CMD 指令用來設定容器啟動時執行的命令，這個命令會在容器啟動時被執行，並且會作為容器的主進程，如果這個命令結束了，容器也會跟著結束。 補充說明⭐️ # 上述的說明簡單的介紹了每一個指令的基本用途，實際上還有很多細節和選項可以使用。這裡就先不擴充太多細節，但是有些特別的建構過程會在這裡補充說明。\n多階段建構🏗️ # 什麼是多階段建構呢？在上面的 Dockerfile 中，我們使用了兩個 FROM 指令，第一個 FROM 指令定義了一個建構階段，第二個 FROM 指令定義了一個運行階段。這種使用多個 FROM 指令的方式就叫做多階段建構。\n為什麼要這麼做呢？好處是什麼？\n多階段建構可以讓我們在建構過程中使用不同的基礎映像，從上面的範例來看，第一個階段使用了 Golang 的映像，這個映像包含了 Golang 的編譯工具，可以用來編譯我們的 Golang 程式碼。編譯完成以後，將編譯出來的二進位檔以及靜態資源複製到第二個階段。\n在第二個階段，我們使用了更輕量級的 Alpine 映像，這個映像不包含 Golang 的編譯工具，但是包含了運行 Golang 程式所需要的環境。這樣就可以減少最終映像檔的大小，因為我們不需要在最終映像檔中包含 Golang 的編譯工具。\n所以善用多階段建構可以讓我們在建構過程中使用不同的基礎映像，從而減少最終映像檔的大小，提高運行效率。\n映像檔的層級結構📂 # 在建構階段中，先執行了 COPY go.mod ./，然後執行了 RUN go mod download 指令來下載相依套件，然後又進行了 COPY . .，最後執行了 RUN go build -o calculator . 。\n這時候可能有些讀者會有疑問，為什麼要進行兩次的 COPY 指令呢？為什麼不直接一次性地複製所有的檔案呢？\n因為 Docker 的建構過程是分層的，每一條指令都會產生一個新的層級(layer)，這些層級會被快取起來，如果某一條指令的內容沒有變動，那麼 Docker 就可以直接使用快取的層級，避免重新執行這條指令。\n如果我們先複製 go.mod，然後執行 go mod download，在依賴沒有變動的情況下，Docker 可以使用快取，避免每次都重新下載依賴，從而加快建構速度。\n有效的利用 Docker 的快取機制，可以在生產環境的 CICD 流程中大幅提升建構效率，減少建構時間。\n開始建構 Docker 映像檔🚀 # 講解完 Dockerfile 的基本語法結構以後，接下來就可以開始建構 Docker 映像檔了，建構的過程中會使用 docker build 指令來建構映像檔，這個指令會讀取 Dockerfile 中的指令，然後按照指令的順序來執行建構過程。\ndocker build -t calculator:1.0 . 在終端機上執行上述指令，-t 選項用來指定建構出來的映像檔的名稱和版本號，. 表示 Dockerfile 的位置在當前目錄下。\n建構完成以後，可以使用 docker images 指令來查看已經建構出來的映像檔，或者使用 Docker Desktop 的 UI 來查看。\nDocker Images 啟動容器🚀 # 建構完成以後，就可以使用 docker run 指令來啟動一個容器了，這邊會使用 -p 選項來將容器的 8080 端口映射到本機電腦的 8080 端口，這樣就可以在瀏覽器上訪問 http://localhost:8080 來查看計算機的應用程式了。\ndocker run -p 8080:8080 calculator:1.0 Docker Run Calculator 總結📝 # 相信通過這篇文章，讀者們對 Dockerfile 已經有了初步的認識，了解了 Dockerfile 的基本概念、使用方法以及一些常見的操作技巧。Dockerfile 是一個非常強大的工具，可以幫助開發者更高效地建構出符合需求的 Docker 映像檔，尤其是在微服務架構和 DevOps 流程中，Dockerfile 更是不可或缺的工具。\n","date":"27 August 2023","externalUrl":null,"permalink":"/posts/dockerfile/","section":"Posts","summary":"","title":"Dockerfile 入門指南：從基礎概念到實際操作","type":"posts"},{"content":"","date":"27 August 2023","externalUrl":null,"permalink":"/tags/%E6%98%A0%E5%83%8F%E6%AA%94/","section":"Tags","summary":"","title":"映像檔","type":"tags"},{"content":"","date":"27 August 2023","externalUrl":null,"permalink":"/tags/%E5%AE%B9%E5%99%A8%E5%8C%96/","section":"Tags","summary":"","title":"容器化","type":"tags"},{"content":"","date":"27 August 2023","externalUrl":null,"permalink":"/tags/%E9%96%8B%E7%99%BC%E6%95%88%E7%8E%87/","section":"Tags","summary":"","title":"開發效率","type":"tags"},{"content":"","date":"19 August 2023","externalUrl":null,"permalink":"/tags/ingress/","section":"Tags","summary":"","title":"Ingress","type":"tags"},{"content":"隨著系統架構日益複雜，可能需要使用多個 Ingress 來分離不同服務的流量管理。這樣做的好處是當某個 Ingress 發生異常時，不會影響到所有服務的運作。\n不過缺點是每個 Ingress 都需要使用靜態 IP，在 GCP 等雲端服務中，每個保留的靜態 IP 都會增加成本。\n我們可以使用 Ingress Controller 來統一管理所有的 Ingress 資源，只使用一個或少數幾個靜態 IP，這樣可以降低成本並簡化管理。\n上一篇文章中有提到 ingress，如果想瞭解 ingress 可以先參考或預習這篇文章。\nIngress 6 August 2023\u0026middot;1 分鐘 Kubernetes Ingress Ingress Nginx Controller # Ingress Nginx Controller 結合了 Ingress 的簡潔並支援 Nginx 相關的擴充功能，讓我們能更好的管理所有的 ingress。\nNginx # 高效能的 webServer 遠勝傳統 apache server 的資源與效能 大量的模組與擴充功能 充足的安全性功能 輕量 容易水平擴展 Ingress # Service 連接 LoadBalance 設定 SSL/TLS 終端 虛擬主機設定 安裝 # helm upgrade --install ingress-nginx ingress-nginx \\ --repo https://kubernetes.github.io/ingress-nginx \\ --namespace ingress-nginx --create-namespace 實際範例 # 基本 Ingress 設定 # apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: example-ingress annotations: kubernetes.io/ingress.class: \u0026#34;nginx\u0026#34; nginx.ingress.kubernetes.io/rewrite-target: / spec: rules: - host: example.com http: paths: - path: / pathType: Prefix backend: service: name: example-service port: number: 80 進階設定範例 # apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: advanced-ingress annotations: kubernetes.io/ingress.class: \u0026#34;nginx\u0026#34; nginx.ingress.kubernetes.io/ssl-redirect: \u0026#34;true\u0026#34; nginx.ingress.kubernetes.io/rate-limit: \u0026#34;10\u0026#34; cert-manager.io/cluster-issuer: \u0026#34;letsencrypt-prod\u0026#34; spec: tls: - hosts: - secure.example.com secretName: example-tls rules: - host: secure.example.com http: paths: - path: /api pathType: Prefix backend: service: name: api-service port: number: 8080 graph LR ingress1(Ingress-1) ingress2(Ingress-2) ingress3(Ingress-3) subgraph Ingress Nginx Controller controller(Controller) admission(Admission Webhook) nginx(Nginx) controller --\u003e|配置| nginx controller --\u003e|驗證| admission end ingress1 --\u003e|管理| controller ingress2 --\u003e|管理| controller ingress3 --\u003e|管理| controller 參考資料 # 使用 Helm 來安裝 Ingress Controller\ningress-nginx\n","date":"19 August 2023","externalUrl":null,"permalink":"/posts/k8s/ingress-nginx/","section":"Posts","summary":"","title":"Ingress Nginx Controller","type":"posts"},{"content":" 介紹 # ingress 是 k8s 給 service 提供外部訪問的 URL、SSL、路由等功能。 可以理解為 nginx 或 traefik 等的代理工具。允許透過某個 URL 進入對應的 service，且碰到某些 route 能夠 proxy pass 到其他 service。\n\u0026#x2b50; ingress 又分成外部負載平衡、內部負載平衡。兩者在使用設定上有一些小小的不同。\n外部負載平衡 # 通常對外部的負載平衡入口，都會設置防火牆或是白名單管理但是 k8s ingress 卻不能直接實現。需要透過一些 gcp 元件功能，來設置防火牆或白名單管理。\ncloud armor # armor 的翻譯是指盔甲的意思，這是由 gcp 提供的服務，具有分散式阻斷攻擊(DDos)防護機制、網路應用程式防火牆。可以搭配 loadBalance、Cloud CDN 來強化網路安全服務。\n那麼我們要如何將 cloud armor 應用在我們 ingress 上面呢？接著看下去 \u0026#x1f60e;\n首先我們可以先找官方的使用手冊 Configuring Ingress using the default controller\n接著將重點放在 backendConfig 設定上面，前面的介紹已經知道 ingress 負責依據路由規則代理轉發。文件上 backenConfig 與 service 可以耦合。使用 backendConfig 的設定。\nBackendConfig and FrontendConfig overview 詳細想知道 backendConfig 設定範例可以在文件連接內找到 \u0026#x1f449; Configuring Ingress features through BackendConfig parameters\n接著下面繪製一張外部負載平衡的架構圖。可以看到下圖中 114.1.46.157 的客戶端在訪問的時候被 ingress 給擋下了。\n原因是 ingress 後端 service 有耦合 backendConfig，透過 backendConfig 使用了 cloud armor。\nflowchart LR cli1(client 34.xxx.xxx.xxx) cli2(client 114.1.46.157) cli1 --\u003e ing cli2 --x ing subgraph k8s-cluster ing((ingress)) svc-web(web-service) svc-api(api-service) df-backend(backend-config) armor{{cloud-armor}} ing --\u003e svc-web ing --\u003e svc-api armor -.- df-backend df-backend -.-\u003e svc-web df-backend -.-\u003e svc-api end 內部負載平衡 # 講完了外部負載平衡，接著講 ingress 擔任內部負載平衡時，需要注意的事項。\nNetwork endpoint group # 什麼是 NEG ?\nNEG 是指 Network endpoint group，是一種配置。意思是指定一組後端 endpoint 或 service，借助NEG，Google Cloud 負載均衡器可以為基於 GCE 的工作負載、無服務器工作負載和容器化工作負載提供服務。可以更精細的將流量分配到負載均衡器的後端。\n使用時，對後端的 service 必須要使用 NEG，詳細想要了解可以參考 \u0026#x1f449; Network endpoint groups overview\n","date":"6 August 2023","externalUrl":null,"permalink":"/posts/k8s/ingress/","section":"Posts","summary":"","title":"Ingress","type":"posts"},{"content":" 什麼是 Docker❓ # 在過去，如果在每個開發者的個人電腦上進行開發，很容易會因為環境設定不一致，導致服務部署到生產環境時出現問題。而 Docker 的出現解決了這個問題，它允許開發者將程式碼、運行時環境、系統工具、庫等打包在一個容器中，確保在任何環境中都能以相同的方式運行。\n這時候讀者們可能會有疑問，虛擬機器(VM)也可以達到類似的效果，那麼 Docker 和 VM 有什麼區別呢？下面會討論 Docker 和 VM 的差異，以及它們各自的優缺點。\n可以從下圖來觀察 VM 和 Docker 容器的差異，可以發現 VM 上的應用程式不需要依賴在宿主機上的作業系統運行，而 Docker 容器則需要依賴宿主機的作業系統來運行，這也是 Docker 的一大優勢，因為省下了 OS 的資源使用，讓容器能更快地啟動和運行。\nVM 與 Docker 容器的差異 🔍優點與缺點 # 上面介紹看到這裡，難道 Docker 就完全取代了虛擬機器嗎？其實不然，Docker 和 VM 都有各自的優缺點，適用於不同的場景。下面會從安全性、系統選擇、應用程式拆分、映像檔大小、啟動時間和資源使用等方面來比較 Docker 和 VM 的優缺點。\n兩種技術各有優缺點，選擇哪一種技術取決於具體的使用場景和需求，可以參考下方的表格來決定技術的選型。\n技術 啟動時間 資源使用 系統選擇 安全性 映像檔大小 應用程式拆分 可攜性 虛擬機器 慢 高 高 高 大 不需拆分 低 容器 快 低 限制 低 小 需拆分 高 如何使用 Docker❓ # 對於 Docker 開始有初步的認識後，接下來將會練習如何在自己的本機電腦上使用 Docker 來啟動一個 Nginx 的容器。\n安裝 DockerDesktop🛠️ # 首先，需要安裝工具，作者的電腦是 macOS，套件管理工具是 Homebrew，因此會使用 Homebrew 來安裝 Docker。\nbrew install --cask docker-desktop 在終端機上執行指令安裝 docker-desktop，安裝完成以後，啟動 Docker 應用程式，可以看到 Docker 已成功運行，而且有 UI 的畫面，可以更方便的管理容器和映像檔。\nDocker Desktop UI 拉取容器映像檔📦 # 有了工具以後，就可以開始使用 Docker 來啟動容器了，首先可以到 Docker Hub 上搜尋想要的映像檔，Docker Hub 是一個公共的映像檔倉庫，裡面有很多官方和社群維護的映像檔，可以直接使用。\n這次的練習，需要啟動一個 Nginx 的容器，因此可以在 Docker Hub 上搜尋 Nginx，找到官方的 Nginx 映像檔，然後使用 docker pull 指令來下載映像檔到本機電腦上。\ndocker pull nginx:1.28 需要注意的是，如果沒有加上版本號。例如 docker pull nginx，預設是會下載最新的版本，但是生產環境中通常不建議使用最新版本，因為最新版本可能會有不穩定的問題，所以最好要加上版本號，例如 docker pull nginx:1.28，這樣就會下載 Nginx 1.28 的版本。\n完成以後，可以透過 Docker Desktop 的 UI 來查看已經下載的映像檔，或者使用 docker images 指令來查看。\nDocker Pull Nginx 啟動容器🚀 # 下載完成以後，就可以使用 docker run 指令來啟動一個 Nginx 的容器了，這邊會使用 -p 選項來將容器的 80 端口映射到本機電腦的 8080 端口，這樣就可以在瀏覽器上訪問 http://localhost:8080 來查看 Nginx 的歡迎頁面了。\ndocker run --name mynginx -p 8080:80 -d nginx:1.28 成功啟動以後，可以使用 docker ps 指令來查看正在運行的容器，或者使用 Docker Desktop 的 UI 來查看。\nDocker Run Nginx 確認 Nginx 的容器已經成功啟動以後，就可以在瀏覽器上訪問 http://localhost:8080 來查看 Nginx 的歡迎頁面了。\nNginx Welcome Page 停止容器🛑 # 如果要停止正在運行的容器，可以使用 docker stop 指令來停止容器。或者者使用 Docker Desktop 的 UI 來停止容器。\ndocker stop mynginx 完成以後，可以使用 docker ps -a 指令來查看所有的容器，確認 Nginx 的容器已經停止了。\nDocker Stop Nginx 總結📝 # 相信通過這篇文章，讀者們對 Docker 已經有了初步的認識，了解了 Docker 的基本概念、優缺點、使用方法以及一些常見的操作技巧。Docker 是一個非常強大的工具，可以幫助開發者更高效地開發、測試和部署應用程式，尤其是在微服務架構和 DevOps 流程中，Docker 更是不可或缺的工具。\n參考文獻📚 # Day2 淺談Docker-虛擬機器和容器的差別 ","date":"30 July 2023","externalUrl":null,"permalink":"/posts/docker/","section":"Posts","summary":"","title":"Docker 入門指南：從基礎概念到實際操作","type":"posts"},{"content":"","date":"30 July 2023","externalUrl":null,"permalink":"/tags/%E8%99%9B%E6%93%AC%E6%A9%9F%E5%99%A8/","section":"Tags","summary":"","title":"虛擬機器","type":"tags"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"}]