ローカルで動くWebアプリができても、Google Cloudへ公開しようとすると急に多くのサービス名が出てきます。
Cloud Run
Cloud SQL
Artifact Registry
Secret Manager
IAM
サービスアカウント
Workload Identity Federation
それぞれを個別に調べると、GitHubにあるDockerfileがどうやってCloud Runまで届くのか分かりにくいかもしれません。
この記事では、初めてGoogle Cloud上へWebアプリを構築する人を対象に、次の状態を目標とします。
GitHubの
mainブランチへpushすると、コンテナイメージが作成され、Cloud Runへデプロイされる。アプリはSecret Managerから秘密情報を受け取り、Cloud SQL for PostgreSQLへ接続する。
Terraformは使用しません。最初はGoogle Cloudコンソールとgcloudで構築し、各サービスの役割を理解することを優先します。
完成する構成
GitHub
│ push
▼
GitHub Actions
│ Dockerfileからビルド
▼
Artifact Registry
│ コンテナイメージ
▼
Cloud Run
├── Secret Manager
└── Cloud SQL for PostgreSQL
各サービスの役割は次のとおりです。
| サービス | 役割 |
|---|---|
| GitHub | ソースコードとDockerfileを保存する |
| GitHub Actions | テスト、ビルド、デプロイを実行する |
| Artifact Registry | 完成したコンテナイメージを保存する |
| Cloud Run | コンテナをHTTPSで実行する |
| Cloud SQL | PostgreSQLを管理する |
| Secret Manager | DBパスワードなどを保管する |
| IAM | 各サービスが行える操作を制限する |
Cloud RunがGitHubからDockerfileを直接取得するわけではありません。GitHub Actionsがリポジトリを取得してコンテナを作り、Artifact Registryへ保存し、そのイメージをCloud Runへ渡します。
前提
この記事では、次のものが用意されていることを前提にします。
- Google Cloudの請求先アカウント
- GitHubリポジトリ
- ローカルで動作確認済みのWebアプリ
- アプリを起動できる
Dockerfile - Google Cloud CLI(
gcloud) - Dockerを利用できる開発環境
アプリは、Cloud Runから渡されるPORT環境変数のポートで、0.0.0.0に対してHTTPリクエストを待ち受ける必要があります。
コンテナ内へ、アップロードファイルやDBを永続保存しないでください。Cloud Runのインスタンスは停止・再作成されます。
この記事で使用する名前
以下の名前を例として使用します。実際のプロジェクトに合わせて変更してください。
GCPプロジェクトID example-project
リージョン asia-northeast1
Artifact Registry application-images
Cloud Runサービス customer-app
Cloud SQLインスタンス customer-db
データベース名 customer_app
実行用サービスアカウント cloud-run-runtime
デプロイ用アカウント github-deployer
コマンドを実行する場合は、自分の値を設定します。
export PROJECT_ID="example-project"
export REGION="asia-northeast1"
export REPOSITORY="application-images"
export SERVICE="customer-app"
export SQL_INSTANCE="customer-db"
gcloud config set project "$PROJECT_ID"
本番とステージングは、プロジェクトまたは少なくとも各リソースと認証情報を分けます。最初から本番DBを開発用として使わないようにします。
1. GCPプロジェクトと予算通知を用意する
Google Cloudコンソールでプロジェクトを作成し、請求先アカウントを関連付けます。
次に「請求先」から予算と通知を設定します。予算通知は、費用が設定額へ近づいたことを知らせる機能です。自動的に利用を停止する上限ではありません。
今回利用するリージョンは東京のasia-northeast1へ揃えます。Cloud Run、Artifact Registry、Cloud SQLを同じリージョンへ置くと、構成と通信経路を理解しやすくなります。
2. 必要なAPIを有効にする
Google Cloudでは、利用するサービスのAPIをプロジェクトごとに有効化します。
gcloud services enable \
run.googleapis.com \
artifactregistry.googleapis.com \
cloudbuild.googleapis.com \
sqladmin.googleapis.com \
secretmanager.googleapis.com \
iamcredentials.googleapis.com \
sts.googleapis.com
今回は次の用途で使います。
| API | 用途 |
|---|---|
| Cloud Run API | コンテナの実行 |
| Artifact Registry API | コンテナイメージの保存 |
| Cloud Build API | 最初のイメージ作成 |
| Cloud SQL Admin API | PostgreSQLの作成と接続 |
| Secret Manager API | 秘密情報の保管 |
| IAM Credentials/STS API | GitHubからの短期認証 |
3. Artifact Registryを作る
Artifact Registryは、ビルド済みのコンテナイメージを保存する場所です。
gcloud artifacts repositories create "$REPOSITORY" \
--repository-format=docker \
--location="$REGION" \
--description="Cloud Run application images"
コンテナイメージの完全な名前は、次の形式になります。
asia-northeast1-docker.pkg.dev/PROJECT_ID/REPOSITORY/IMAGE:TAG
今回の例では次のようになります。
asia-northeast1-docker.pkg.dev/example-project/application-images/customer-app:TAG
公式資料: Artifact RegistryへDockerイメージを保存する
4. Cloud SQL for PostgreSQLを作る
Cloud SQLは、Google CloudがPostgreSQLの基盤を管理するサービスです。OSへ自分でPostgreSQLをインストールする構成とは異なり、バックアップ、更新、監視などの機能を利用できます。
最初はGoogle Cloudコンソールから作成したほうが、料金見積もりと各項目を確認しやすくなります。
- Cloud SQLを開く
- 「インスタンスを作成」を選ぶ
- PostgreSQLを選ぶ
- リージョンを
asia-northeast1にする - 開発・ステージングに必要な最小構成を選ぶ
- 自動バックアップの設定を確認する
- インスタンス名を
customer-dbにする
Cloud SQLは、Cloud Runのようにアクセスがなければ完全にゼロまで縮小するサービスではありません。インスタンスが動いている間は料金が発生するため、作成画面の月額見積もりを確認してください。
次に、アプリ用のデータベースとDBユーザーを作ります。
データベース名 customer_app
DBユーザー名 customer_app
DBパスワード 十分に長いランダムな値
DBパスワードをソースコード、.env.example、GitHub ActionsのYAMLへ書かないでください。
5. Secret Managerへ秘密情報を登録する
少なくとも次の値をSecret Managerへ保存します。
db-password Cloud SQLのDBパスワード
auth-secret 認証ライブラリが使用するランダムな秘密値
Google CloudコンソールのSecret Managerから、シークレットと最初のバージョンを作成します。
db-password
└─ バージョン1: 実際のDBパスワード
auth-secret
└─ バージョン1: 開発環境とは異なるランダムな値
アプリには次のように渡します。
DB_PASSWORD ← db-password
AUTH_SECRET ← auth-secret
この記事では、環境変数として渡すシークレットのバージョンをlatestではなく番号で固定します。Secret Managerで値を更新した後、Cloud Runへ新しいリビジョンをデプロイして切り替えます。
公式資料: Cloud RunでSecret Managerを利用する
6. Cloud Run実行用サービスアカウントを作る
サービスアカウントは、Cloud Run上のアプリがGoogle Cloud内で何を行えるかを表すIDです。
gcloud iam service-accounts create cloud-run-runtime \
--display-name="Cloud Run runtime identity"
Cloud SQLへ接続できるようにします。
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:cloud-run-runtime@${PROJECT_ID}.iam.gserviceaccount.com" \
--role="roles/cloudsql.client"
Secret Managerの値も読めるようにします。プロジェクト全体ではなく、利用するシークレットごとに権限を付けます。
gcloud secrets add-iam-policy-binding db-password \
--member="serviceAccount:cloud-run-runtime@${PROJECT_ID}.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"
gcloud secrets add-iam-policy-binding auth-secret \
--member="serviceAccount:cloud-run-runtime@${PROJECT_ID}.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"
実行用サービスアカウントに、OwnerやEditorのような広い権限を与える必要はありません。
Cloud RunがGoogle Cloud APIへアクセスするときは、この実行用サービスアカウントが利用されます。デプロイを行うGitHubのサービスアカウントとは別のものです。
公式資料: Cloud RunのサービスID
7. アプリをCloud SQL接続へ対応させる
Cloud RunとCloud SQLを接続すると、コンテナ内に次のUnixソケットが用意されます。
/cloudsql/PROJECT_ID:REGION:INSTANCE_NAME
今回の例では次の値です。
/cloudsql/example-project:asia-northeast1:customer-db
アプリは、環境変数から接続情報を受け取るようにします。
DB_HOST
DB_PORT
DB_NAME
DB_USER
DB_PASSWORD
Cloud Runでは次のように設定します。
DB_HOST=/cloudsql/example-project:asia-northeast1:customer-db
DB_NAME=customer_app
DB_USER=customer_app
DB_PASSWORD=Secret Managerから取得
Node.jsのpgを利用する場合の考え方は次のようになります。
import { Pool } from "pg";
export const pool = new Pool({
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT ?? "5432"),
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
max: 5,
});
Drizzle ORMを使う場合も、この接続プールをDrizzleへ渡せます。
Cloud Runは複数インスタンスへ増えるため、1インスタンスあたりのDB接続数を大きくしすぎないようにします。例えば最大3インスタンス、各5接続なら、アプリ側だけで最大15接続になる可能性があります。
Cloud SQLのDBポートを0.0.0.0/0へ公開する必要はありません。Cloud Runの実行用サービスアカウントへCloud SQL Client権限を付け、Cloud RunのCloud SQL接続設定を利用します。
公式資料: Cloud RunからCloud SQL for PostgreSQLへ接続する
8. 最初のコンテナを手動でデプロイする
GitHub Actionsを作る前に、一度手動でデプロイします。アプリ、Dockerfile、Cloud SQL、Secret Managerの問題と、GitHub認証の問題を分けて確認できるためです。
ソースコードとDockerfileがあるディレクトリで、Cloud Buildを使ってイメージを作成します。
export IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPOSITORY}/${SERVICE}:manual-1"
gcloud builds submit --tag "$IMAGE" .
最後の.は、現在のディレクトリをビルド対象として渡すという意味です。Cloud Buildはこの中にあるDockerfileを利用します。
次にCloud Runへデプロイします。
gcloud run deploy "$SERVICE" \
--image="$IMAGE" \
--region="$REGION" \
--service-account="cloud-run-runtime@${PROJECT_ID}.iam.gserviceaccount.com" \
--add-cloudsql-instances="${PROJECT_ID}:${REGION}:${SQL_INSTANCE}" \
--set-env-vars="DB_HOST=/cloudsql/${PROJECT_ID}:${REGION}:${SQL_INSTANCE},DB_PORT=5432,DB_NAME=customer_app,DB_USER=customer_app,NODE_ENV=production" \
--set-secrets="DB_PASSWORD=db-password:1,AUTH_SECRET=auth-secret:1" \
--max-instances=3
初回デプロイ時に、未認証アクセスを許可するか確認されることがあります。
- 一般公開サービスなら、アプリ側の認証と公開範囲を確認して判断する
- 社内システムなら、理由なく未認証アクセスを許可しない
- 限定公開する場合は、Cloud Run IAMやゼロトラスト型のアクセス制御を別途設計する
「ログイン画面がある」ことと、「Cloud Run自体を誰でも呼び出せる」ことは別の設定です。
9. 動作とログを確認する
デプロイされたサービスを確認します。
gcloud run services describe "$SERVICE" \
--region="$REGION"
ログは次のコマンドでも確認できます。
gcloud run services logs read "$SERVICE" \
--region="$REGION" \
--limit=100
次の項目を確認します。
- コンテナが正常に起動する
- ヘルスチェックまたはトップページが応答する
- Cloud SQLへ接続できる
- マイグレーションが意図したDBへ適用されている
- Secret Managerの値がログへ表示されていない
- 一般利用者と管理者の権限が分かれている
- Cloud Runの公開範囲が意図どおりである
DBマイグレーションをアプリ起動時に毎回自動実行すると、複数インスタンスが同時に適用する可能性があります。本番では、デプロイとは別の一度だけ実行される工程に分ける方法を検討します。
10. GitHub用のデプロイサービスアカウントを作る
手動デプロイが成功したら、GitHub Actionsからデプロイできるようにします。
gcloud iam service-accounts create github-deployer \
--display-name="GitHub Actions deployer"
デプロイ用アカウントには、コンテナイメージの登録とCloud Runの更新に必要な権限を与えます。
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:github-deployer@${PROJECT_ID}.iam.gserviceaccount.com" \
--role="roles/artifactregistry.writer"
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:github-deployer@${PROJECT_ID}.iam.gserviceaccount.com" \
--role="roles/run.admin"
さらに、Cloud Runへ実行用サービスアカウントを割り当てる権限を与えます。
gcloud iam service-accounts add-iam-policy-binding \
"cloud-run-runtime@${PROJECT_ID}.iam.gserviceaccount.com" \
--member="serviceAccount:github-deployer@${PROJECT_ID}.iam.gserviceaccount.com" \
--role="roles/iam.serviceAccountUser"
デプロイ用アカウントには、DBパスワードを読む権限を与えていません。Cloud Runの実行用アカウントだけが、実行時にSecret Managerから値を取得します。
11. GitHubとGoogle Cloudを短期認証で接続する
サービスアカウントのJSON鍵を発行してGitHub Secretsへ保存する方法もありますが、長期間有効な鍵が増えます。
この記事では、GitHub Actionsが実行されたときだけ短期的な認証情報を取得するWorkload Identity Federationを利用します。
GitHub Actions
│ OIDCトークン
▼
Workload Identity Provider
│ 許可したリポジトリか確認
▼
github-deployerとして短時間だけ認証
Identity Poolを作る
gcloud iam workload-identity-pools create github \
--location="global" \
--display-name="GitHub Actions"
GitHub用Providerを作る
OWNER/REPOSITORYを実際のGitHubリポジトリへ置き換えます。
gcloud iam workload-identity-pools providers create-oidc github-repository \
--location="global" \
--workload-identity-pool="github" \
--issuer-uri="https://token.actions.githubusercontent.com/" \
--attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.ref=assertion.ref" \
--attribute-condition="assertion.repository=='OWNER/REPOSITORY' && assertion.ref=='refs/heads/main'"
GitHubのOIDC発行元は、多くのGitHub利用者で共通です。そのため、属性条件で利用を許可するリポジトリとブランチを限定します。
GitHubからサービスアカウントを利用できるようにする
プロジェクト番号を確認します。
gcloud projects describe "$PROJECT_ID" \
--format="value(projectNumber)"
表示された番号をPROJECT_NUMBERへ入れ、OWNER/REPOSITORYも置き換えます。
gcloud iam service-accounts add-iam-policy-binding \
"github-deployer@${PROJECT_ID}.iam.gserviceaccount.com" \
--role="roles/iam.workloadIdentityUser" \
--member="principalSet://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/github/attribute.repository/OWNER/REPOSITORY"
Providerの完全な名前も確認します。
gcloud iam workload-identity-pools providers describe github-repository \
--location="global" \
--workload-identity-pool="github" \
--format="value(name)"
GitHubリポジトリの「Settings → Secrets and variables → Actions → Variables」へ、次の値を登録します。
GCP_WIF_PROVIDER
projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/github/providers/github-repository
GCP_DEPLOY_SERVICE_ACCOUNT
github-deployer@example-project.iam.gserviceaccount.com
これらは秘密鍵ではありませんが、リポジトリごとの設定値としてVariablesで管理します。
公式資料: デプロイパイプラインでWorkload Identity Federationを使用する
12. GitHub Actionsを作る
リポジトリへ次のファイルを作ります。
.github/workflows/deploy.yml
name: Deploy to Cloud Run
on:
push:
branches:
- main
permissions:
contents: read
id-token: write
env:
PROJECT_ID: example-project
REGION: asia-northeast1
REPOSITORY: application-images
SERVICE: customer-app
SQL_INSTANCE: customer-db
RUNTIME_SERVICE_ACCOUNT: cloud-run-runtime@example-project.iam.gserviceaccount.com
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v3
with:
workload_identity_provider: ${{ vars.GCP_WIF_PROVIDER }}
service_account: ${{ vars.GCP_DEPLOY_SERVICE_ACCOUNT }}
- name: Set up gcloud
uses: google-github-actions/setup-gcloud@v3
- name: Configure Docker authentication
run: gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet
- name: Build container
run: |
docker build \
-t "${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPOSITORY}/${SERVICE}:${GITHUB_SHA}" \
.
- name: Push container
run: |
docker push \
"${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPOSITORY}/${SERVICE}:${GITHUB_SHA}"
- name: Deploy to Cloud Run
run: |
gcloud run deploy "$SERVICE" \
--project="$PROJECT_ID" \
--region="$REGION" \
--image="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPOSITORY}/${SERVICE}:${GITHUB_SHA}" \
--service-account="$RUNTIME_SERVICE_ACCOUNT" \
--add-cloudsql-instances="${PROJECT_ID}:${REGION}:${SQL_INSTANCE}" \
--set-env-vars="DB_HOST=/cloudsql/${PROJECT_ID}:${REGION}:${SQL_INSTANCE},DB_PORT=5432,DB_NAME=customer_app,DB_USER=customer_app,NODE_ENV=production" \
--set-secrets="DB_PASSWORD=db-password:1,AUTH_SECRET=auth-secret:1" \
--max-instances=3 \
--quiet
処理は次の順番です。
actions/checkout
→ GitHub上のDockerfileとソースコードを取得
google-github-actions/auth
→ JSON鍵を使わずGoogle Cloudへ短期認証
docker build
→ Dockerfileからコンテナイメージを作成
docker push
→ Artifact Registryへ保存
gcloud run deploy
→ そのイメージでCloud Runを更新
公式のGitHub Actionを使って直接デプロイする方法もあります。
13. テストをデプロイより前に実行する
実際のワークフローでは、コンテナを作る前にテストを実行します。
- name: Install dependencies
run: npm ci
- name: Type check
run: npm run typecheck
- name: Lint
run: npm run lint
- name: Test
run: npm test
プロジェクトで実際に定義しているコマンドへ合わせてください。存在しないスクリプトを記事のままコピーすると失敗します。
デプロイは、これらに成功した場合だけ実行されます。
テスト成功
↓
コンテナをビルド
↓
Artifact Registryへ保存
↓
Cloud Runへデプロイ
14. ステージングと本番を分ける
ステージングと本番で、同じDBや秘密情報を共有しないようにします。
ステージング
Cloud Run: customer-app-staging
Cloud SQL: customer-db-staging
Secret: db-password-staging
本番
Cloud Run: customer-app-production
Cloud SQL: customer-db-production
Secret: db-password-production
可能であればGCPプロジェクト自体を分けると、IAM、請求、API、誤操作の影響範囲を分離しやすくなります。
GitHub Actionsでは、ブランチやGitHub Environmentsを利用できます。
developへのpush
→ ステージングへデプロイ
mainへのpush+承認
→ 本番へデプロイ
開発PCへ本番用.envを置き、手作業で切り替える運用は避けます。
15. 公開後に確認すること
Cloud Run
- 最小・最大インスタンス数
- タイムアウトとメモリ
- 同時実行数
- 未認証アクセスの可否
- 実行用サービスアカウント
- エラー率と応答時間
Cloud SQL
- 自動バックアップ
- 復旧方法
- ストレージ使用量
- 最大接続数
- 高可用構成の要否
- DBマイグレーションの実行方法
Secret Manager
- 実行用サービスアカウントだけが読めるか
- バージョンを固定しているか
- ローテーション方法が決まっているか
- 古い値をいつ無効化するか
GitHub Actions
- Workload Identity Providerが対象リポジトリへ限定されているか
-
id-token: writeがデプロイJobだけに付いているか - 本番デプロイに承認が必要か
- ログへ秘密情報を出力していないか
監視と費用
- アプリ停止と5xxエラーの通知
- Cloud SQLのCPU、メモリ、ストレージ
- バックアップ失敗の確認
- 請求予算の通知
- Artifact Registryの古いイメージの整理
16. よくある失敗
サービスアカウントの役割を混ぜる
デプロイ用アカウントとCloud Run実行用アカウントを同じにすると、アプリがデプロイ権限を持つなど、不要な権限が増えます。
github-deployer
→ ビルド・イメージ登録・Cloud Run更新
cloud-run-runtime
→ Cloud SQL接続・必要なSecretの取得
JSON鍵をGitHubへ登録する
動作させるだけなら簡単ですが、長期鍵の漏えい、失効、更新を管理する必要があります。GitHub ActionsではWorkload Identity Federationを優先します。
秘密情報をDockerfileへ書く
ENVやARGで秘密情報をビルドへ渡すと、イメージやビルドログへ残る可能性があります。秘密情報は実行時にSecret Managerから渡します。
DBをインターネットへ公開する
接続できないからといって、PostgreSQLのポートを全世界へ許可しないでください。Cloud SQL接続設定とIAMを確認します。
Cloud Runのインスタンスを増やしすぎる
Cloud Runが増えると、アプリからCloud SQLへの接続も増えます。接続プールと最大インスタンス数を合わせて設計します。
マイグレーションを全インスタンスで実行する
コンテナ起動時に自動実行すると、複数インスタンスから同じマイグレーションを実行する可能性があります。一度だけ実行するデプロイ工程やCloud Run Jobへ分けます。
17. 試した環境を残さない場合
学習用に作成した環境を使わない場合は、料金が発生し続けないように削除します。
特にCloud SQLは、アプリへアクセスしていなくてもインスタンス料金が発生します。
削除前に、必要なデータとバックアップがないことを必ず確認してください。
Cloud Runサービス
Cloud SQLインスタンス
Artifact Registryのイメージ
Secret Managerのシークレット
不要になったサービスアカウント
Workload Identity Pool/Provider
削除操作は取り消せない場合があります。プロジェクト名、リソース名、バックアップを確認してから実行します。
まとめ
GitHubからCloud Runへデプロイするとき、Cloud Runが直接Dockerfileを取得するわけではありません。
GitHub Actionsがソースコードを取得する
→ Dockerfileからコンテナを作る
→ Artifact Registryへ保存する
→ Cloud Runへイメージを指定してデプロイする
アプリの実行時には、専用のサービスアカウントを使ってCloud SQLとSecret Managerへ接続します。GitHub Actionsのデプロイ権限とは分離します。
最初は手動で一度デプロイし、アプリとGCP側の設定を確認します。その後、Workload Identity Federationを使ってGitHub Actionsから自動化すると、問題が起きた場所を切り分けやすくなります。
AI駆動開発で利用する技術スタックについては、次の記事で紹介しています。
本番公開前のセキュリティと運用については、次の記事にまとめています。