Oops!!

プログラミングとかのラフなブログ

Workload Identityを用いてGitLab CIからCloud Runへデプロイする

Workload Identityを用いてGitLab CIからCloud Runへデプロイする

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ポリシーを対象のサービスアカウントへ設定しておきます。

以下が大まかな流れになります。

  1. GitLabリポジトリ(project/repository)にgit push
  2. CI/CDジョブでトークン発行
  3. トークンをGoogle Cloudに送信
  4. Google CloudでWorkload Identity連携の設定をされているプロバイダーやプロジェクトであるかを検証
  5. 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に反映されるようになります。

プロフィール画像

すずき ゆうた

愛知県でフリーランスのフロントエンド・エンジニアをしています。Reactを用いた開発が得意です。 他にもプロジェクトマネジメントや組織マネジメントも行ってきました。エビデンスのない事でも自分の経験から書いていくので話半分くらいでお願いします。