今天的目標其實很單純:設定一個 GitHub Actions workflow,讓它能夠自動建置 Docker、推送到 Artifact Registry,然後部署到 GCE 實例上。聽起來就是個標準的 CI/CD 流程,應該半小時搞定的事情。
結果花了幾個小時在各種奇怪的權限和認證問題上打轉,不過回頭看,這整個過程暴露了不少平時容易忽略的雷點,也讓我重新思考了雲端資源管理的重要性。
一開始就卡在 GitHub Actions 無法 SSH 到 GCE 實例。錯誤訊息說缺少 compute.instances.setMetadata
權限,無法將 SSH 金鑰添加到實例的 metadata。這個問題的根本原因是服務帳號權限不足,解決方法是添加 Compute Instance Admin
角色,或者使用 gcloud compute config-ssh
預先設置 SSH 配置。
SSH 問題解決後,遇到更頭痛的問題:GCE 實例無法從 Artifact Registry 拉取映像。錯誤訊息是經典的 "Unauthenticated request",但問題的複雜度遠超預期。
最初以為是服務帳號缺少 artifactregistry.reader
權限,但添加後問題依然存在。接著發現是 GCE 實例的 OAuth scope 不夠,只有 devstorage.read_only
等基本權限,缺少關鍵的 cloud-platform
scope。
最讓人困惑的是,系統一直顯示錯誤的專案 ID 1067582438577
,但這個專案根本不存在。一開始懷疑是服務帳號金鑰檔案有問題,重新生成了好幾次,但問題依然存在。
後來發現是 GCE 實例內部的 gcloud 配置有舊的設定殘留,導致認證時會嘗試存取錯誤的專案。解決方法是清除實例內的 gcloud 配置:
rm -rf ~/.config/gcloud
即使解決了專案 ID 問題,Docker pull 還是失敗。根本原因是 GitHub Actions 的認證沒有正確傳遞到 GCE 實例內部。最終的解決方案是將認證檔案複製到實例,然後重新認證:
gcloud compute scp $GOOGLE_APPLICATION_CREDENTIALS $GCE_INSTANCE:/tmp/sa-key.json --zone=$GCE_ZONE
gcloud compute ssh $GCE_INSTANCE --zone=$GCE_ZONE --command="
export GOOGLE_CLOUD_PROJECT=$PROJECT_ID
export CLOUDSDK_CORE_PROJECT=$PROJECT_ID
gcloud auth activate-service-account --key-file=/tmp/sa-key.json
ACCESS_TOKEN=\$(gcloud auth print-access-token)
echo \$ACCESS_TOKEN | sudo docker login -u oauth2accesstoken --password-stdin $GAR_LOCATION-docker.pkg.dev
sudo docker pull $IMAGE_URL
# ... 其他部署指令
"
最後一個問題是 Docker 認證在 sudo
環境下無效。一般用戶的 Docker 認證無法被 sudo docker
命令使用,所以需要直接為 root 用戶設定認證。
這次的踩雷經驗讓我學到幾個重要的教訓:
雲端環境下,各種資源(服務帳號、IAM 權限、API scope、專案設定)之間的關係錯綜複雜。一個小小的配置錯誤就可能導致整個流程失效。提前規劃和文檔化這些資源配置是絕對必要的,不能等到出問題才臨時抱佛腳。
在多層架構中(GitHub Actions → GCE 實例 → Docker),認證資訊的傳遞不是理所當然的。每一層都可能有自己的認證機制和權限限制,需要明確地處理認證傳遞問題。
sudo
命令會切換到不同的用戶環境,很多環境變數和配置都會失效。這種環境隔離是安全機制,但也容易被忽略。
"Unauthenticated request" 可能是權限問題,也可能是 scope 問題,或者是認證傳遞問題。需要系統性地排除各種可能性,而不是只看表面的錯誤訊息。