Skip to main content

GitHub Actions 自動部署(CD)到 GCP Cloud Run 完整教學

本文聚焦在「程式碼推上 GitHub → 自動 build → 自動部署到 Cloud Run」這條 CD 管線的設定。

為了安全,所有專案 ID、專案編號、Service Account email、WIF 路徑等敏感資訊都以佔位符(<...>)取代,實際操作時請替換成自己的值,切勿把明碼寫進公開文件。


整體流程

git push origin main | v GitHub Actions 觸發 | +- 透過 Workload Identity Federation 認證 GCP(免金鑰) +- docker build 映像檔 +- docker push 到 Artifact Registry +- gcloud run deploy 部署到 Cloud Run | v 瀏覽器開 Cloud Run URL 看到網站 ```

                                                                    核心觀念:用 **Workload Identity Federation(WIF)** 建立 GitHub 與 GCP 的信任關係,GitHub Actions 不需要持有任何金鑰即可取得部署權限。

                                                                    ---

                                                                    ## 前置準備

                                                                    ### 1. 啟用必要的 GCP API

                                                                    於 GCP Console 啟用:

                                                                    - Cloud Run Admin API
                                                                    - Artifact Registry API
                                                                    - Cloud Build API
                                                                    - Service Networking API
                                                                    - Secret Manager API

                                                                    ### 2. 建立 Artifact Registry(存放 Docker 映像檔)

                                                                    - Format:Docker
                                                                    - Region:`asia-east1`
                                                                    - Mode:Standard

                                                                    完整路徑格式:

                                                                    ```
                                                                    asia-east1-docker.pkg.dev/<PROJECT_ID>/<REPOSITORY>
                                                                    ```

                                                                    ### 3. 建立 Service Account 並授予角色

                                                                    建立一個專供 GitHub 推送/部署使用的 Service Account,逐一加上以下 3 個角色:

                                                                    | 角色 | 用途 |
                                                                    |---|---|
                                                                    | Artifact Registry **寫入者**(Writer) | 推送映像檔(注意不是「服務代理」) |
                                                                    | Cloud Run 管理員 | 部署服務 |
                                                                    | 服務帳戶使用者 | 部署時模擬執行身分 |

                                                                    ---

                                                                    ## 設定 Workload Identity Federation(免金鑰認證)

                                                                    ### 建立 Workload Identity Pool

                                                                    - Pool ID:`github-pool`

                                                                    ### 新增 OIDC Provider

                                                                    - Provider type:OpenID Connect (OIDC)
                                                                    - Provider Name:`github-provider`
                                                                    - Issuer URL:`https://token.actions.githubusercontent.com`

                                                                    ### 屬性對應(Attribute Mapping)

                                                                    | Google | OIDC |
                                                                    |---|---|
                                                                    | `google.subject` | `assertion.sub` |
                                                                    | `attribute.repository` | `assertion.repository` |
                                                                    | `attribute.repository_owner` | `assertion.repository_owner` |

                                                                    ### 屬性條件(關鍵安全設定)

                                                                    限制只有自己的 GitHub 帳號/組織能使用此 Provider:

                                                                    ```
                                                                    assertion.repository_owner == '<GITHUB_OWNER>'
                                                                    ```

                                                                    ### 綁定 Service Account 與 WIF

                                                                    到 Service Account 的「**具備存取權的主體(Principals with access)**」分頁,點「**授予存取權**」,新增主體:

                                                                    ```
                                                                    principalSet://iam.googleapis.com/projects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/github-pool/attribute.repository/<GITHUB_OWNER>/<REPO_NAME>
                                                                    ```

                                                                    角色:**Workload Identity 使用者**

                                                                    > 注意:「這個 SA 能做什麼」在 IAM 頁編輯;「誰能用這個 SA」在 SA 的「具備存取權的主體」頁編輯,兩者方向不同,容易搞混。

                                                                    ---

                                                                    ## GitHub Secrets 設定

                                                                    到 GitHub repo 的 `Settings -> Secrets and variables -> Actions`,新增 3 個 Secrets(實際值請填自己的,勿外流):

                                                                    | Name | 內容說明 |
                                                                    |---|---|
                                                                    | `GCP_PROJECT_ID` | 你的 GCP 專案 ID |
                                                                    | `GCP_WIF_PROVIDER` | `projects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/github-pool/providers/github-provider` |
                                                                    | `GCP_SA_EMAIL` | 部署用 Service Account 的 email |

                                                                    ---

                                                                    ## GitHub Actions Workflow

                                                                    於 `.github/workflows/push-image.yml`:

                                                                    ```yaml
                                                                    name: Build, Push and Deploy to Cloud Run

                                                                    on:
                                                                      push:
                                                                          branches: [main]
                                                                            workflow_dispatch:

                                                                            permissions:
                                                                              contents: read
                                                                                id-token: write   # WIF 取得 OIDC token 必須

                                                                                env:
                                                                                  REGION: asia-east1
                                                                                    REPOSITORY: <REPOSITORY>
                                                                                      IMAGE_NAME: <IMAGE_NAME>
                                                                                        SERVICE_NAME: <SERVICE_NAME>

                                                                                        jobs:
                                                                                          build-push-deploy:
                                                                                              runs-on: ubuntu-latest
                                                                                                  steps:
                                                                                                        - name: Checkout code
                                                                                                                uses: actions/checkout@v4
                                                                                                                
                                                                                                                      - name: Authenticate to GCP
                                                                                                                              uses: google-github-actions/auth@v2
                                                                                                                                      with:
                                                                                                                                                workload_identity_provider: ${{ secrets.GCP_WIF_PROVIDER }}
                                                                                                                                                          service_account: ${{ secrets.GCP_SA_EMAIL }}
                                                                                                                                                          
                                                                                                                                                                - name: Set up gcloud
                                                                                                                                                                        uses: google-github-actions/setup-gcloud@v2
                                                                                                                                                                        
                                                                                                                                                                              - name: Configure Docker auth
                                                                                                                                                                                      run: gcloud auth configure-docker ${{ env.REGION }}-docker.pkg.dev --quiet
                                                                                                                                                                                      
                                                                                                                                                                                            - name: Build Docker image
                                                                                                                                                                                                    run: |
                                                                                                                                                                                                              docker build -t ${{ env.REGION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} .
                                                                                                                                                                                                              
                                                                                                                                                                                                                    - name: Push to Artifact Registry
                                                                                                                                                                                                                            run: |
                                                                                                                                                                                                                                      docker push ${{ env.REGION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
                                                                                                                                                                                                                                      
                                                                                                                                                                                                                                            - name: Deploy to Cloud Run
                                                                                                                                                                                                                                                    run: |
                                                                                                                                                                                                                                                              gcloud run deploy ${{ env.SERVICE_NAME }} \
                                                                                                                                                                                                                                                                          --image=${{ env.REGION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
                                                                                                                                                                                                                                                                                      --region=${{ env.REGION }} \
                                                                                                                                                                                                                                                                                                  --platform=managed \
                                                                                                                                                                                                                                                                                                              --allow-unauthenticated \
                                                                                                                                                                                                                                                                                                                          --port=8080 \
                                                                                                                                                                                                                                                                                                                                      --memory=256Mi \
                                                                                                                                                                                                                                                                                                                                                  --cpu=1 \
                                                                                                                                                                                                                                                                                                                                                              --min-instances=0 \
                                                                                                                                                                                                                                                                                                                                                                          --max-instances=2
                                                                                                                                                                                                                                                                                                                                                                          
                                                                                                                                                                                                                                                                                                                                                                                - name: Show service URL
                                                                                                                                                                                                                                                                                                                                                                                        run: |
                                                                                                                                                                                                                                                                                                                                                                                                  URL=$(gcloud run services describe ${{ env.SERVICE_NAME }} --region=${{ env.REGION }} --format='value(status.url)')
                                                                                                                                                                                                                                                                                                                                                                                                            echo "Service URL: $URL"
                                                                                                                                                                                                                                                                                                                                                                                                            ```
                                                                                                                                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                                                                                                                                            ---
                                                                                                                                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                                                                                                                                            ## 觸發部署
                                                                                                                                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                                                                                                                                            ```bash
                                                                                                                                                                                                                                                                                                                                                                                                            git add .
                                                                                                                                                                                                                                                                                                                                                                                                            git commit -m "feat: setup CI/CD pipeline"
                                                                                                                                                                                                                                                                                                                                                                                                            git push origin main
                                                                                                                                                                                                                                                                                                                                                                                                            ```
                                                                                                                                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                                                                                                                                            到 GitHub repo 的 **Actions** 分頁看執行狀況,跑完後開 Cloud Run 配發的 URL 即可看到部署結果。
                                                                                                                                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                                                                                                                                            ---
                                                                                                                                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                                                                                                                                            ## 踩過的坑(CD 相關)
                                                                                                                                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                                                                                                                                            ### 坑 1:組織政策禁止建立 SA 金鑰
                                                                                                                                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                                                                                                                                            錯誤訊息:`服務帳戶金鑰建立功能已停用`(組織政策 `iam.disableServiceAccountKeyCreation`)。
                                                                                                                                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                                                                                                                                            解法:改用 Workload Identity Federation,完全不用金鑰 —— 這其實是更安全的業界最佳實踐。
                                                                                                                                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                                                                                                                                            ### 坑 2:Artifact Registry 角色加錯
                                                                                                                                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                                                                                                                                            push 階段報錯:`denied: Permission 'artifactregistry.repositories.uploadArtifacts' denied`。
                                                                                                                                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                                                                                                                                            | 錯誤的角色 | 正確的角色 |
                                                                                                                                                                                                                                                                                                                                                                                                            |---|---|
                                                                                                                                                                                                                                                                                                                                                                                                            | Artifact Registry 服務代理(Service Agent) | Artifact Registry **寫入者**(Writer) |
                                                                                                                                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                                                                                                                                            ### 坑 3:授予存取權的入口位置
                                                                                                                                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                                                                                                                                            要在 SA 詳情頁分頁列最右邊的「**具備存取權的主體 / Principals with access**」,才會看到正確的「+ 授予存取權」按鈕,而不是「權限」分頁。
                                                                                                                                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                                                                                                                                            ### 坑 4:Cloud Run 要求容器必須監聽 HTTP port
                                                                                                                                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                                                                                                                                            只「跑完就結束」的容器會一直重啟。Cloud Run 是「無狀態 HTTP 服務」,容器必須監聽 port(預設 `8080`)。
                                                                                                                                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                                                                                                                                            ---
                                                                                                                                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                                                                                                                                            ## 安全提醒
                                                                                                                                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                                                                                                                                            - 切勿把專案 ID、專案編號、SA email、WIF 完整路徑等寫進公開文件;統一以 Secrets 管理。
                                                                                                                                                                                                                                                                                                                                                                                                            - WIF 的屬性條件(`repository_owner`)務必設定,避免其他人的 repo 冒用你的 Provider。
                                                                                                                                                                                                                                                                                                                                                                                                            - `permissions: id-token: write` 是 WIF 運作的必要條件,別漏掉。