Next.js + Gitlab CI + Cloud Runで環境構築 という記事で同じような環境構築を行いましたが、Googleはセキュリティの観点からサービスアカウントキーを利用を非推奨にし、Workload Identityを利用することを推奨しています。
サービスアカウントキーはファイル自体があることで漏洩のリスクがありますが、Workload Identityはより堅牢なセキュリティ環境と言えると思います。
そのため、今回はWorkload Identityを用いたデプロイ環境を構築していきたいと思います。
Workload Identityとは
Workload Identityは外部サービスからGoogle Cloudサービスへのアクセスするできるアカウントの設定です。
今までのサービスアカウントキー(service-account.jsonみたいなファイル)というなの設定ファイルなどは必要なく、CIの中でWorkload IdentityのIDを受け取り、それを使ってユーザーログインさせて認証し、安全に処理を進めることができます。
Workload Identityの仕組み
ここではGitLabを用いて説明します。
Workload Identity連携は、OIDC (OpenID Connect) を利用して外部のIDプロバイダ(今回はGitLab)とGoogle Cloudの間で信頼関係を構築する仕組みです。
GitLabでCIのジョブが実行される度に、リポジトリのパス、ブランチ名など含んだトークンが発行され、それをGoogle Cloudへ送信します。
Google Cloud側では、「このリポジトリからのトークンであれば、このサービスアカウントとして振る舞うことを許可する」というIAMポリシーを対象のサービスアカウントへ設定しておきます。
以下が大まかな流れになります。
- GitLabリポジトリ(project/repository)にgit push
- CI/CDジョブでトークン発行
- トークンをGoogle Cloudに送信
- Google CloudでWorkload Identity連携の設定をされているプロバイダーやプロジェクトであるかを検証
- Google Cloudがトークンの署名とリポジトリパスを検証し、問題なければサービスアカウントの権限を一時的に貸与する
プロジェクト
プロジェクト構成
プロジェクトは以下のようなmonorepoで構成しています。PrismaのSchemaを共有したいので、この構成にすることが多いです。
├── apps
│ └── web
│ ├── Dockerfile
│ ├── next-env.d.ts
│ ├── next.config.ts
│ ├── package.json
│ ├── public
│ ├── README.md
│ ├── src
│ └── tsconfig.json
├── .gitlab-ci.yml
├── cloudbuild.yaml
├── setup.sh
├── package-lock.json
├── package.json
└── packages
└── db
├── package.json
└── prisma
setup.sh
シェルからGoogle Cloudの設定と権限の追加を置き換えます。 こちらは今後Terraformに変更したいと思います
#!/bin/bash
# --- 設定値 (ここを編集する) ---
PROJECT_ID="project-name-1234"
SERVICE_ACCOUNT_NAME="gitlab-ci-sa"
WORKLOAD_POOL_ID="gitlab-pool"
WORKLOAD_PROVIDER_ID="gitlab"
GITLAB_PROJECT_PATH="group/project" # 例: group/project
ARTIFACT_REPO_NAME="repo-name"
REGION="asia-northeast1"
# --- 実行スクリプト (ここから下は編集不要) ---
echo "--- 設定を開始します: Project $PROJECT_ID ---"
# 1. 必要なAPIを有効化
echo "--- 1. APIを有効化中... ---"
gcloud services enable \
iamcredentials.googleapis.com \
artifactregistry.googleapis.com \
cloudbuild.googleapis.com \
run.googleapis.com \
logging.googleapis.com \
--project=$PROJECT_ID
# 2. CI/CD用サービスアカウントを作成
echo "--- 2. サービスアカウントを作成中: $SERVICE_ACCOUNT_NAME ---"
gcloud iam service-accounts create $SERVICE_ACCOUNT_NAME \
--project=$PROJECT_ID \
--display-name="GitLab CI Service Account"
SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"
# 3. Workload Identity プールとプロバイダを作成 (存在しない場合のみ)
echo "--- 3. Workload Identity連携を設定中... ---"
gcloud iam workload-identity-pools describe $WORKLOAD_POOL_ID --location=global --project=$PROJECT_ID > /dev/null 2>&1 || \
gcloud iam workload-identity-pools create $WORKLOAD_POOL_ID \
--project=$PROJECT_ID \
--location="global" \
--display-name="GitLab Pool"
gcloud iam workload-identity-pools providers describe $WORKLOAD_PROVIDER_ID --location=global --project=$PROJECT_ID --workload-identity-pool=$WORKLOAD_POOL_ID > /dev/null 2>&1 || \
gcloud iam workload-identity-pools providers create-oidc $WORKLOAD_PROVIDER_ID \
--project=$PROJECT_ID \
--location="global" \
--workload-identity-pool=$WORKLOAD_POOL_ID \
--issuer-uri="https://gitlab.com" \
--allowed-audiences="projects/$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)')/locations/global/workloadIdentityPools/${WORKLOAD_POOL_ID}/providers/${WORKLOAD_PROVIDER_ID}"
# 4. サービスアカウントに必要な権限を付与
echo "--- 4. サービスアカウントにIAMロールを付与中... ---"
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:${SERVICE_ACCOUNT_EMAIL}" \
--role="roles/cloudbuild.builds.builder"
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:${SERVICE_ACCOUNT_EMAIL}" \
--role="roles/run.developer"
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:${SERVICE_ACCOUNT_EMAIL}" \
--role="roles/iam.serviceAccountUser"
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:${SERVICE_ACCOUNT_EMAIL}" \
--role="roles/logging.viewer"
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:${SERVICE_ACCOUNT_EMAIL}" \
--role="roles/viewer"
# 5. GitLabリポジトリとサービスアカウントを紐付け
echo "--- 5. GitLabリポジトリとサービスアカウントを紐付け中... ---"
PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)')
gcloud iam service-accounts add-iam-policy-binding $SERVICE_ACCOUNT_EMAIL \
--project=$PROJECT_ID \
--role="roles/iam.workloadIdentityUser" \
--member="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${WORKLOAD_POOL_ID}/attribute.project_path/${GITLAB_PROJECT_PATH}"
# 6. Artifact Registryリポジトリを作成 (存在しない場合のみ)
echo "--- 6. Artifact Registryリポジトリを作成中... ---"
gcloud artifacts repositories describe $ARTIFACT_REPO_NAME --location=$REGION --project=$PROJECT_ID > /dev/null 2>&1 || \
gcloud artifacts repositories create $ARTIFACT_REPO_NAME \
--repository-format=docker \
--location=$REGION \
--description="Docker repository" \
--project=$PROJECT_ID
echo "--- 全ての設定が完了しました! ---"
Dockerfile
# Multi-stage build for Next.js app
FROM node:22-alpine AS base
# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat openssl
WORKDIR /app
# Copy only web app package files
COPY apps/web/package*.json ./
# Install dependencies (use npm install since no lock file)
RUN npm install --omit=dev
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
# Copy web app source code
COPY apps/web/ ./
# Build the Next.js app
RUN npm run build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy the built application
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV HOSTNAME="0.0.0.0"
# Start the application
CMD ["node", "server.js"]
確認とクリーンアップ
docker build . -f apps/web/Dockerfile --tag xxx
docker run -d --rm -p 8080:3000 --name nextjs-app xxx
open http://localhost:8080
docker stop nextjs-app
docker rmi xxx
cloudbuild.yaml
_PROJECT_NAME、_SERVICE_NAME、_IMAGE_TAGは変数化しており、.gitlab-ci.ymlで定義する形になります。
steps:
# 1. Dockerイメージをビルドする
- name: 'gcr.io/cloud-builders/docker'
args: [
'build',
'-t', 'asia-northeast1-docker.pkg.dev/$PROJECT_ID/${_PROJECT_NAME}/${_SERVICE_NAME}:${_IMAGE_TAG}',
'-f', 'apps/web/Dockerfile',
'.'
]
# 2. ビルドしたイメージをArtifact Registryにプッシュする
- name: 'gcr.io/cloud-builders/docker'
args: [
'push',
'asia-northeast1-docker.pkg.dev/$PROJECT_ID/${_PROJECT_NAME}/${_SERVICE_NAME}:${_IMAGE_TAG}'
]
# 3. 新しいイメージをCloud Runにデプロイする
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
entrypoint: gcloud
args: [
'run', 'deploy', '${_SERVICE_NAME}',
'--image', 'asia-northeast1-docker.pkg.dev/$PROJECT_ID/${_PROJECT_NAME}/${_SERVICE_NAME}:${_IMAGE_TAG}',
'--region', 'asia-northeast1',
'--platform', 'managed',
'--allow-unauthenticated'
]
# ビルドしたイメージを記録
images:
- 'asia-northeast1-docker.pkg.dev/$PROJECT_ID/${_PROJECT_NAME}/${_SERVICE_NAME}:${_IMAGE_TAG}'
確認
substitutionsで変数に注入して、cloudbuildをbuildします。
gcloud builds submit . --config cloudbuild.yaml --substitutions=_PROJECT_NAME=sample-project,_SERVICE_NAME=web,_IMAGE_TAG=test-001
.gitlab-ci.yml
[PROJECT_ID]、[PROJECT_NUMBER]、[LOCATION]、[WORKLOAD_IDENTITY_PROVIDER_NAME]は任意のもの置き換えてください。
GCP_WORKLOAD_IDENTITY_PROVIDER
についてはGoogle Cloudのプロバイダの編集のデフォルトのオーディエンスから確認できます。ただURLになっていると上手く動かなかったので許可するオーディエンスにし、projects以下にしています。
GitLabのCI/CD用にgitlab-ci-sa
というアカウントを利用していますが、saはservice accountの略です。
variables:
GCP_PROJECT_ID: "[PROJECT_ID]"
GCP_WORKLOAD_IDENTITY_PROVIDER: "projects/[PROJECT_NUMBER]/locations/global/workloadIdentityPools/[WORKLOAD_IDENTITY_PROVIDER_NAME]/providers/gitlab"
GCP_SERVICE_ACCOUNT: "gitlab-ci-sa@[PROJECT_ID].iam.gserviceaccount.com"
default:
image: google/cloud-sdk:latest
stages:
- build-and-deploy
deploy-to-cloud-run:
stage: build-and-deploy
id_tokens:
GITLAB_OIDC_TOKEN:
# ステップ1で更新したプロバイダ設定と完全に一致させる
aud: "$GCP_WORKLOAD_IDENTITY_PROVIDER"
script:
- echo "$GITLAB_OIDC_TOKEN" > /tmp/oidc_token.json
- gcloud iam workload-identity-pools create-cred-config "$GCP_WORKLOAD_IDENTITY_PROVIDER" --service-account="$GCP_SERVICE_ACCOUNT" --output-file=/tmp/gcp_cred.json --credential-source-file=/tmp/oidc_token.json
- gcloud auth login --cred-file=/tmp/gcp_cred.json --update-adc
- gcloud config set project $GCP_PROJECT_ID
- echo "Submitting build to Cloud Build..."
- gcloud builds submit . --config cloudbuild.yaml --substitutions=_PROJECT_NAME=sample-project,_SERVICE_NAME=web,_IMAGE_TAG=$CI_COMMIT_SHORT_SHA
rules:
- if: $CI_COMMIT_BRANCH == "main"
これでmainブランチにpushかmergeするとCloud Runに反映されるようになります。