0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

初めてのGCP構築――GitHubからCloud Run+Cloud SQLへデプロイするまで

0
Posted at

ローカルで動く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コンソールから作成したほうが、料金見積もりと各項目を確認しやすくなります。

  1. Cloud SQLを開く
  2. 「インスタンスを作成」を選ぶ
  3. PostgreSQLを選ぶ
  4. リージョンをasia-northeast1にする
  5. 開発・ステージングに必要な最小構成を選ぶ
  6. 自動バックアップの設定を確認する
  7. インスタンス名を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駆動開発で利用する技術スタックについては、次の記事で紹介しています。

本番公開前のセキュリティと運用については、次の記事にまとめています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?