はじめに
この記事は Cocone Advent Calendar 2023 7日目の記事です。
GitHub Actions と Google Cloud Deploy を使用して Cloud Run へデプロイします。おおまかな流れは以下の図のようになります。

この図を説明すると、
- リポジトリの
main
ブランチにプッシュ(かプルリクエストをマージ)されるとワークフローが実行され、 Cloud Deploy の環境とステージング用 Cloud Run サービスが作成(デプロイ)されます。(①②③) - そのあと、 Cloud Deploy のプロモートを行うと本番用の Cloud Run サービスへデプロイされます。(④⑤)
本番環境をデプロイするまでに手動で行うことは①のプッシュと④のプロモートだけになります。
GCP の設定
GitHub から GCP へアクセスさせるため準備を行います。 gcloud
コマンドの初期設定と必要な API の有効化は済んでいるものとして割愛します。また、プロジェクト ID とプロジェクト番号の表記をそれぞれ ${GCP_PROJECT_ID}
と ${GCP_PROJECT_NUM}
としてあります。それらは実際の値に置き換えます。
IAM サービスアカウント
GitHub から接続するためのサービスアカウントを作成します。
gcloud iam service-accounts create my-sa-gha
もう1つ、 Cloud Run 実行時に指定するサービスアカウントを作成します。ただ今回 Cloud Run のコンテナでは特別なことはしないのでこのアカウントには何も権限を付与しません。
gcloud iam service-accounts create my-sa-run
Workload Identity 連携
Workload Identity 連携を設定します。これを行うことでサービスアカウントのシークレットキーを発行することなく GitHub から GCP へアクセスできるようになります。シークレットキーの漏洩による事故がなくなり、セキュリティが強化されます。詳しくは こちら や こちら を参考にしてください。
Workload Identity プールを作成します。
gcloud iam workload-identity-pools create my-wi-pool --location="global"
GitHub Actions の OIDC プロバイダを作成します。
gcloud iam workload-identity-pools providers create-oidc my-oidc-gha \
--location="global" \
--workload-identity-pool="my-wi-pool" \
--issuer-uri="https://token.actions.githubusercontent.com" \
--attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository"
サービスアカウントと Workload Identity プールを接続します。この ${USERNAME}/${REPOSITORY}
は実際の GitHub ユーザー名/リポジトリに置き換えます。
gcloud iam service-accounts add-iam-policy-binding my-sa-gha@${GCP_PROJECT_ID}.iam.gserviceaccount.com \
--role="roles/iam.workloadIdentityUser" \
--member="principalSet://iam.googleapis.com/projects/${GCP_PROJECT_NUM}/locations/global/workloadIdentityPools/my-wi-pool/attribute.repository/${USERNAME}/${REPOSITORY}"
IAM カスタムロール
Cloud Deploy の操作に必要な権限を持つカスタムロールを作成します。権限を記述したファイルを作成し、それを指定してカスタムロールを作成します。 Cloud Run サービスの作成は別のサービスアカウントになり代わって行われるため、ここではそれらの権限は付与していません。
title: myCloudDeployRole
stage: GA
includedPermissions:
- clouddeploy.deliveryPipelines.create
- clouddeploy.deliveryPipelines.get
- clouddeploy.deliveryPipelines.update
- clouddeploy.operations.get
- clouddeploy.releases.create
- clouddeploy.releases.get
- clouddeploy.rollouts.create
- clouddeploy.rollouts.get
- clouddeploy.rollouts.list
- clouddeploy.targets.update
- iam.serviceAccounts.actAs
- storage.buckets.create
- storage.buckets.get
- storage.buckets.list
- storage.objects.create
ロールを作成します。
gcloud iam roles create myCloudDeployRole --project="${GCP_PROJECT_ID}" --file="permissions.yaml"
サービスアカウントにロールを割り当てます。
gcloud projects add-iam-policy-binding ${GCP_PROJECT_ID} \
--member="serviceAccount:my-sa-gha@${GCP_PROJECT_ID}.iam.gserviceaccount.com" \
--role="projects/${GCP_PROJECT_ID}/roles/myCloudDeployRole"
Artifact Registry
コンテナイメージのプッシュ先となる Artifact Registry のリポジトリを作成します。
gcloud artifacts repositories create my-app --repository-format="docker" --location="asia-northeast1"
この Artifact Registry リポジトリを操作する権限をサービスアカウントに付与します。
gcloud artifacts repositories add-iam-policy-binding my-app \
--location="asia-northeast1" \
--member="serviceAccount:my-sa-gha@${GCP_PROJECT_ID}.iam.gserviceaccount.com" \
--role="roles/artifactregistry.writer"
GitHub の設定
リポジトリの変数とシークレットを設定します。ここの ${GCP_PROJECT_ID}
と ${GCP_PROJECT_NUM}
も実際の値に置き換えます。
種類 | 名前 | 値 |
---|---|---|
変数 | APP |
my-app |
変数 | GAR_LOCATION |
asia-northeast1 |
シークレット | GCP_PROJECT_ID |
${GCP_PROJECT_ID} |
シークレット | RUN_SERVICE_ACCOUNT |
my-sa-run@${GCP_PROJECT_ID}.iam.gserviceaccount.com |
シークレット | WIF_PROVIDER |
projects/${GCP_PROJECT_NUM}/locations/global/workloadIdentityPools/my-wi-pool/providers/my-oidc-gha |
シークレット | WIF_SERVICE_ACCOUNT |
my-sa-gha@${GCP_PROJECT_ID}.iam.gserviceaccount.com |
Git で管理するファイルを用意します。
各種ファイルは このリポジトリのサンプル をベースに作成しています。
- main.go
- Dockerfile
- cloud-deploy.template.yaml
- skaffold.template.yaml
- app-stg.template.yaml
- app-prod.template.yaml
- .github/workflows/cloud-deploy-to-cloud-run.yml
それぞれのファイルを説明していきます。
main.go
Hello world を返すだけのシンプルなHTTPサーバーです。確認のために環境変数も表示しています。 TARGET
は Stg か Prod 、 SHA
には Git のコミットハッシュが入ります。
package main
import (
"fmt"
"net/http"
"os"
)
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, world %s %s\n", os.Getenv("TARGET"), os.Getenv("SHA"))
}
func main() {
http.HandleFunc("/", hello)
http.ListenAndServe(":8080", nil)
}
Dockerfile
こちらもビルドと実行だけのシンプルなものです。
FROM golang:1.21.4-alpine AS builder
COPY main.go ./
RUN CGO_ENABLED=0 go build -trimpath -ldflags='-w -s' -o /hello main.go
FROM scratch
COPY --from=builder /hello /
USER 65534:65534
EXPOSE 8080
CMD ["/hello"]
cloud-deploy.template.yaml
Cloud Deploy デリバリーパイプラインとターゲットを作成するための構成ファイルのテンプレートです。他に設定できる項目については こちらのページ を参照してください。
また、構成ファイルやテンプレート内にある ${APP}
のようなシェル変数の記述は置き換えずそのままにします。これらはワークフロー実行時に置き換わります。テンプレートはステップ途中で envsubst
コマンドで展開され cloud-deploy.yaml
のような名前で構成ファイルが生成されます。
apiVersion: deploy.cloud.google.com/v1
kind: DeliveryPipeline
metadata:
name: '${APP}'
description: 'Deployment pipeline for ${APP}'
serialPipeline:
stages:
- targetId: 'stg'
profiles: ['stg']
- targetId: 'prod'
profiles: ['prod']
---
apiVersion: deploy.cloud.google.com/v1
kind: Target
metadata:
name: 'stg'
description: 'Staging target'
run:
location: 'projects/${PROJECT_ID}/locations/${REGION}'
---
apiVersion: deploy.cloud.google.com/v1
kind: Target
metadata:
name: 'prod'
description: 'Production target'
run:
location: 'projects/${PROJECT_ID}/locations/${REGION}'
skaffold.template.yaml
Cloud Deploy リリースを作成するための Skaffold 構成ファイルのテンプレートです。デプロイ先を Cloud Run にしてステージングと本番用のCloud Run サービス構成ファイルを指定しています。設定できる項目は こちらのページ にあります。
apiVersion: skaffold/v4beta7
kind: Config
metadata:
name: '${APP}'
deploy:
cloudrun: {}
profiles:
- name: 'stg'
manifests:
rawYaml:
- 'app-stg.yaml'
- name: 'prod'
manifests:
rawYaml:
- 'app-prod.yaml'
app-stg.template.yaml と app-prod.template.yaml
ステージング環境と本番環境の Cloud Run サービス構成ファイルのテンプレートです。 image: 'app'
は Cloud Deploy リリースを作成するステップ実行時に置き換えられます。 設定できる項目は こちらのページ が参考にしてください。
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: '${APP}-stg'
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/maxScale: '1'
run.googleapis.com/execution-environment: gen2
spec:
serviceAccountName: ${RUN_SERVICE_ACCOUNT}
containers:
- name: '${APP}'
image: 'app'
env:
- name: 'TARGET'
value: 'Stg'
- name: 'SHA'
value: '${SHA}'
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: '${APP}-prod'
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/maxScale: '1'
run.googleapis.com/execution-environment: gen2
spec:
serviceAccountName: ${RUN_SERVICE_ACCOUNT}
containers:
- name: '${APP}'
image: 'app'
env:
- name: 'TARGET'
value: 'Prod'
- name: 'SHA'
value: '${SHA}'
.github/workflows/cloud-deploy-to-cloud-run.yml
GitHub Actions ワークフローの構成ファイルです。
name: Cloud Deploy to Cloud Run
on:
push:
branches:
- main
jobs:
deploy:
permissions:
contents: 'read'
id-token: 'write'
runs-on: ubuntu-latest
steps:
- name: 'Checkout'
uses: 'actions/checkout@v4'
- name: 'Google auth'
id: 'auth'
uses: 'google-github-actions/auth@v1'
with:
workload_identity_provider: '${{ secrets.WIF_PROVIDER }}'
service_account: '${{ secrets.WIF_SERVICE_ACCOUNT }}'
- name: 'Set up Cloud SDK'
uses: 'google-github-actions/setup-gcloud@v1'
with:
project_id: '${{ secrets.GCP_PROJECT_ID }}'
- name: 'Docker auth'
run: |-
gcloud auth configure-docker ${{ vars.GAR_LOCATION }}-docker.pkg.dev
- name: 'Build and push container'
run: |-
docker build -t "${{ vars.GAR_LOCATION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/${{ vars.APP }}/${{ vars.APP }}:${{ github.sha }}" .
docker push "${{ vars.GAR_LOCATION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/${{ vars.APP }}/${{ vars.APP }}:${{ github.sha }}"
- name: 'Render templatised config manifests'
run: |-
export PROJECT_ID="${{ secrets.GCP_PROJECT_ID }}"
export REGION="${{ vars.GAR_LOCATION }}"
export APP="${{ vars.APP }}"
export SHA="${GITHUB_SHA::7}"
export RUN_SERVICE_ACCOUNT="${{ secrets.RUN_SERVICE_ACCOUNT }}"
for template in $(ls *.template.yaml); do envsubst < ${template} > ${template%%.*}.yaml ; done
- name: 'Create Cloud Deploy delivery pipeline'
run: |-
gcloud deploy apply --file cloud-deploy.yaml --region ${{ vars.GAR_LOCATION }}
- name: 'Create release name'
run: |-
echo "RELEASE_NAME=${{ vars.APP }}-${GITHUB_SHA::7}-${GITHUB_RUN_NUMBER}" >> ${GITHUB_ENV}
- name: 'Create Cloud Deploy release'
id: 'release'
uses: 'google-github-actions/create-cloud-deploy-release@v0'
with:
delivery_pipeline: '${{ vars.APP }}'
name: '${{ env.RELEASE_NAME }}'
region: '${{ vars.GAR_LOCATION }}'
description: '${{ env.GITHUB_COMMIT_MSG }}'
skaffold_file: 'skaffold.yaml'
images: 'app=${{ vars.GAR_LOCATION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/${{ vars.APP }}/${{ vars.APP }}:${{ github.sha }}'
- name: 'Report Cloud Deploy release'
run: |-
echo "Created release ${{ steps.release.outputs.name }} "
echo "Release link ${{ steps.release.outputs.link }} "
このワークフローで行われていることはおおまかに、
- 認証
- コンテナをビルドして Artifact Registry へプッシュ
- テンプレートから構成ファイルを生成
- Cloud Deploy デリバリーパイプライン作成
- Cloud Deploy リリース作成
となっています。
これらの7つのファイルが main
リポジトリへプッシュされるとワークフローの実行を通して、 Cloud Deploy デリバリーパイプラインとリリース、ステージング用の Cloud Run サービスがデプロイされます。
GCP のリソースを確認
まだ本番用の Cloud Run サービスはデプロイされていません。 Cloud Deploy デリバリーパイプラインのプロモートを行うことによってデプロイされます。
Cloud Deploy デリバリーパイプラインの Web UI 画面では以下の画像のようになっています。プロモートをクリックして進行します。

プロモートが完了すると以下のようになり本番用の Cloud Run サービスがデプロイされます。

作成された Cloud Run サービスへのアクセスは制限されています。必要に応じて開放します。例えばインターネットに公開する場合は以下のようにコマンドを実行します。
gcloud run services add-iam-policy-binding my-app-prod \
--region="asia-northeast1" \
--member="allUsers" \
--role="roles/run.invoker"
実際にアクセスするとこのように表示されます。

おわりに
GitHub Actions + Cloud Deploy を使用して Cloud Run へデプロイを行いました。
これで main
ブランチにプッシュされるたびにステージング環境へデプロイが行われ、本番環境へのデプロイは Web UI から数クリックで行えるようになりました。ロールバックしたい時も同じく Web UI から数クリックで行えます。
今回は試していませんが Cloud Deploy デリバリーパイプラインには他にも、複数のターゲットに並列にデプロイする、プロモート時に承認を設ける、カナリアデプロイ、自動化の構成などもあり、まだまだ色々なことができそうで興味深いです。
参考リンク
- GitHub Actions と Google Cloud Deploy が連携 | Google Cloud 公式ブログ
- GitHub Actions を使用して Cloud Run にデプロイする | Google Cloud 公式ブログ
- GitHub - google-github-actions/example-workflows: Repository to demonstrate example workflows.
- Google Cloud Platform での OpenID Connect の構成 - GitHub Docs
- GitHub - google-github-actions/auth: A GitHub Action for authenticating to Google Cloud.
- Cloud Run サービスまたはジョブをデプロイする | Cloud Deploy | Google Cloud
- デリバリー パイプライン、ターゲット、自動化構成 | Cloud Deploy | Google Cloud
- skaffold.yaml | Skaffold
- Cloud Run YAML Reference | Cloud Run Documentation | Google Cloud