Google CloudとGitHub Actions(Terraform)を連携するDirect Workload Identityを作成するbashスクリプト

記事タイトルとURLをコピーする

G-gen の武井です。当記事では Google Cloud と GitHub Actions (Terraform) を連携する Direct Workload Identity を作成する bash スクリプトを紹介します。

はじめに

概要

当記事で紹介するのは、Google Cloud と GitHub Actions (Terraform) との連携に必要な Direct Workload Identity リソースを作成する bash スクリプトです。

以前の記事との違い

以前執筆した記事で紹介したのは、サービスアカウントの権限を借用する形式の Workload Identity リソースを作成するスクリプトです。

blog.g-gen.co.jp

今回ご紹介するのは、Workload Identity プールに必要な権限 (IAM ロール) を直接付与する形式の Workload Identity リソースを作成するスクリプトです。

この方式は、サービスアカウントの払い出しやサービスアカウントを借用するための権限付与が必要ないため、従来よりもセキュアな連携が可能で、Google Cloud、ならびに GitHub の公式ドキュメント上でも推奨されています。

制限事項

推奨される形式ではあるものの、Direct Workload Identity には対応可能なプロダクトや機能に制限があります。

対応していないプロダクトやその機能を管理したい場合、従来形式 (サービスアカウントの権限を借用する形式) の Workload Identity をご利用ください。

前提条件

当 bash スクリプトは、Debian GNU/Linux 12 (bookworm) 上で開発され、動作確認されています。

また、以下のソフトウェアがインストールされていることが前提です。カッコ内は開発時のバージョンです。

  • gcloud(Google Cloud SDK 486.0.0

スクリプト実行時は、実行先のプロジェクトに対して gcloud CLI を認証する必要があります。

blog.g-gen.co.jp

免責事項

当記事で紹介するプログラムのソースコードは、ご自身の責任のもと、使用、引用、改変、再配布して構いません。

ただし、同ソースコードが原因で発生した不利益やトラブルについては、当社は一切の責任を負いません。

ソースコード

前述の 免責事項 をご理解のうえ、ご利用ください。

init.sh

#!/bin/bash
  
# エラーハンドリング: エラーが発生したらスクリプトを終了
set -e
  
# 変数の設定
PROJECT_ID="" # プロジェクトID (ex: gha-demo-prj)   
PROJECT_NUMBER="" # プロジェクト番号 (ex: 1234567890)
ORGANIZATION_ID="" # プロジェクトの組織ID (ex: 0123456789)
WORKLOAD_IDENTITY_POOL="" # Workload Identityプール名 (ex: gha-demo-pool)
WORKLOAD_IDENTITY_PROVIDER="" # Workload Identityプロバイダ名 (ex: gha-demo-provider)
GITHUB_REPO="" # GitHubリポジトリ名 (ex: gha-demo-org/gha-demo-repo)
  
# ログ出力関数
log() {
    echo "[INFO] $1"
}
  
log_error() {
    echo "[ERROR] $1" >&2
}
  
# 1. IAM Credential API を有効化
if ! gcloud services list --enabled --filter="name:iamcredentials.googleapis.com" --format="value(name)" | grep "iamcredentials.googleapis.com" >/dev/null 2>&1; then
    log "IAM Credential API を有効にしています..."
    gcloud services enable iamcredentials.googleapis.com --project="$PROJECT_ID"
else
    log "IAM Credential API は既に有効化されています"
fi
  
# 2. Workload Identity プールの作成
if ! gcloud iam workload-identity-pools describe $WORKLOAD_IDENTITY_POOL --location="global" --project="$PROJECT_ID" >/dev/null 2>&1; then
    log "Workload Identity プールを作成中: $WORKLOAD_IDENTITY_POOL"
    gcloud iam workload-identity-pools create $WORKLOAD_IDENTITY_POOL \
        --project="$PROJECT_ID" \
        --location="global" \
        --display-name="$WORKLOAD_IDENTITY_POOL"
else
    log "Workload Identity プールは既に存在します: $WORKLOAD_IDENTITY_POOL"
fi
  
# 3. Workload Identity プロバイダの作成
if ! gcloud iam workload-identity-pools providers describe $WORKLOAD_IDENTITY_PROVIDER --workload-identity-pool="$WORKLOAD_IDENTITY_POOL" --location="global" --project="$PROJECT_ID" >/dev/null 2>&1; then
    log "Workload Identity プロバイダを作成中: $WORKLOAD_IDENTITY_PROVIDER"
    gcloud iam workload-identity-pools providers create-oidc $WORKLOAD_IDENTITY_PROVIDER \
        --project="$PROJECT_ID" \
        --location="global" \
        --workload-identity-pool="$WORKLOAD_IDENTITY_POOL" \
        --display-name="$WORKLOAD_IDENTITY_PROVIDER" \
        --issuer-uri="https://token.actions.githubusercontent.com" \
        --attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository" \
        --attribute-condition="assertion.repository=='$GITHUB_REPO'"
else
    log "Workload Identity プロバイダは既に存在します: $WORKLOAD_IDENTITY_PROVIDER"
fi
  
# 4. 組織レベルでのロール付与
log "組織レベルでのロール付与の確認"
for role in "roles/resourcemanager.organizationAdmin" "roles/owner"; do
    if ! gcloud organizations get-iam-policy $ORGANIZATION_ID --flatten="bindings[].members" --filter="bindings.members:principalSet://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$WORKLOAD_IDENTITY_POOL AND bindings.role:$role" --format="value(bindings.role)" | grep "$role" >/dev/null 2>&1; then
        log "$role をWorkload Identity プールに付与中: $WORKLOAD_IDENTITY_POOL"
        gcloud organizations add-iam-policy-binding $ORGANIZATION_ID \
            --member="principalSet://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$WORKLOAD_IDENTITY_POOL/attribute.repository/$GITHUB_REPO" \
            --role="$role"
    else
        log "$role は既にWorkload Identity プールに付与されています: $WORKLOAD_IDENTITY_POOL"
    fi
done
  
log "Direct Workload Identity 設定が完了しました。"

スクリプトの使い方

認証

まずは実行先のプロジェクトに対して gcloud CLI の認証を通します。

# 実行先プロジェクトの確認
$ gcloud config list
[core]
account = test-user@demo.g-gen.co.jp
disable_usage_reporting = True
project = gha-demo-prj
  
Your active configuration is: [gha-demo-prj]
   
# gcloud CLI の認証
$ gcloud auth login
  
~~中略~~
  
You are now logged in as [test-user@demo.g-gen.co.jp].
Your current project is [gha-demo-prj]. 

変数設定

7~12行目の変数に環境情報を入力します。

※ 当スクリプトでは、サービスアカウントの変数定義はありません。

実行

スクリプトに実行権限を付与して実行します。

※ 当スクリプトでは、以下のリソースは作成しません。

  • サービスアカウント
  • サービスアカウントを借用するための IAM Policy
  • Workload Identity プールとサービスアカウントの紐づけ
# 実行権限付与
$ chmod +x init.sh 
$ ls -l
-rwxr-xr-x 1 test-user test-user 3784 Nov 12 14:27 init.sh
# スクリプト実行
$ ./init.sh 
[INFO] IAM Credential API は既に有効化されています
[INFO] Workload Identity プールを作成中: gha-demo-pool
Created workload identity pool [gha-demo-pool].
[INFO] Workload Identity プロバイダを作成中: gha-demo-provider
Created workload identity pool provider [gha-demo-provider].
[INFO] 組織レベルでのロール付与の確認
[INFO] roles/resourcemanager.organizationAdmin をWorkload Identity プールに付与中: gha-demo-pool
Updated IAM policy for organization [0123456789].
  
~~中略~~
  
[INFO] roles/owner をWorkload Identity プールに付与中: gha-demo-pool
Updated IAM policy for organization [0123456789].
  
~~中略~~
  
[INFO] Workload Identity 設定が完了しました。

リソースの確認

Workload Identity プール・プロバイダー

以下のように作成されます。

Workload Identity プール (1/2)
Workload Identity プール (2/2)、サービスアカウントを使用していない
Workload Identity プロバイダー (1/2)
Workload Identity プロバイダー (2/2)

サービスアカウント

前述の通り、Direct Workload Identity にサービスアカウントは不要なため、当スクリプトでは作成しません。

Workload Identity プールの IAM Policy

以下のように作成されます。
※ IAM ロールは適用先のセキュリティポリシーに応じて調整してください。

組織レベルの IAM Policy

構成

当スクリプトで作成される Workload Identity を使い、Google Cloud プロジェクトに対する terraform planterraform apply を、GitHub Actions で自動化します。

ワークフローや Terraform ソースコードは次項に記載のものを使用します。
ご利用される場合は、前述の 免責事項 をご理解のうえ、ご利用ください。

ソースコード (Terraform)

Terraform ディレクトリ構成

.
├── .github
│   └── workflows
│       └── terraform.yaml
├── env
│   └── demo
│       ├── backend.tf
│       ├── locals.tf
│       ├── main.tf
│       └── versions.tf
├── modules
│   └── apis
│       ├── main.tf
│       ├── outputs.tf
│       └── variables.tf
├── .gitignore
├── init.sh
└── README.md

ワークフロー (terraform.yaml)

以下の値をご自身の環境で作成したリソースに置き換えてください。

  • 38行目: Workload Identity プロバイダー

Direct Workload Identity では、google-github-actions/auth@v2 でサービスアカウントを定義する必要はありません。

name: terraform
  
# main ブランチへの Pull request と Merge
on:
  pull_request:
    branches:
      - main
  push:
    branches:
      - main
  
# ジョブ (GitHUb runners で実行)
jobs:
  terraform-workflow:
    runs-on: ubuntu-latest
  
    permissions:
      id-token: write
      contents: read
      pull-requests: write
  
    strategy:
      matrix:
        # tf_working_dir に main.tf (呼び出し側) のディレクトリを指定
        tf_working_dir:
          - ./env/demo
  
    steps:
      - uses: actions/checkout@v4
        name: Checkout
        id: checkout

      # Workload Identity 連携
      # https://cloud.google.com/iam/docs/using-workload-identity-federation#generate-automatic
      - id: 'auth'
        name: 'Authenticate to Google Cloud'
        uses: 'google-github-actions/auth@v2'
        with:
          workload_identity_provider: 'projects/1234567890/locations/global/workloadIdentityPools/gha-demo-pool/providers/gha-demo-provider'
  
      # https://github.com/marketplace/actions/setup-tfcmt      
      - uses: shmokmt/actions-setup-tfcmt@v2
        name: Setup tfcmt
  
      # https://github.com/marketplace/actions/setup-github-comment
      - uses: shmokmt/actions-setup-github-comment@v2
        name: Setup github-comment
        
      # https://github.com/actions/setup-node
      # https://github.com/hashicorp/setup-terraform/issues/84
      - uses: actions/setup-node@v4
        with:
          node-version: '18'
  
      - uses: hashicorp/setup-terraform@v3
        name: Setup terraform
  
      - name: Terraform fmt
        id: fmt
        run: |
          cd ${{ matrix.tf_working_dir }}
          terraform fmt -recursive
        continue-on-error: true
      
      - name: Terraform Init
        id: init
        run: |
          cd ${{ matrix.tf_working_dir }}
          terraform init -upgrade
  
      - name: Terraform Validate
        id: validate
        run: |
          cd ${{ matrix.tf_working_dir }}
          terraform validate
  
      # main ブランチへ pull request した際に terraform plan を実行
      - name: Terraform Plan
        id: plan
        if: github.event_name == 'pull_request'
        run: |
          cd ${{ matrix.tf_working_dir }}
          export GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}
          tfcmt -var target:${{ matrix.tf_working_dir }} plan -- terraform plan --parallelism=50
          github-comment hide -condition 'Comment.Body contains "No changes."'
        continue-on-error: true
  
      # terraform status で失敗した際に workflow を停止
      - name: Terraform Plan Status
        id: status
        if: steps.plan.outcome == 'failure'
        run: exit 1
  
      # main ブランチへ push した際に terraform apply を実行
      - name: Terraform Apply
        id: apply
        if: github.ref == 'refs/heads/main' && github.event_name == 'push'
        run: |
          cd ${{ matrix.tf_working_dir }}
          export GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}
          tfcmt -var target:${{ matrix.tf_working_dir }} apply -- terraform apply -auto-approve -input=false --parallelism=50

env/demo 配下 (呼び出し側)

# backend.tf
terraform {
  backend "gcs" {
    bucket = "gha-demo-prj-tfstate"
    prefix = "terraform/state"
  }
}
  
# locals.tf
locals {
  project_id = "gha-demo-prj"
  
  apis = [
    "artifactregistry.googleapis.com",
    "cloudapis.googleapis.com",
    "cloudasset.googleapis.com",
    "cloudresourcemanager.googleapis.com",
    "iam.googleapis.com",
    "iamcredentials.googleapis.com",
    "servicemanagement.googleapis.com",
    "serviceusage.googleapis.com",
    "sts.googleapis.com",
  ]
}
  
# main.tf
module "apis" {
  source     = "../../modules/apis"
  project_id = local.project_id
  apis       = local.apis
}
  
# versions.tf
terraform {
  required_version = "~> 1.9.7"
  
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 6.6.0"
    }
  }
}
  
provider "google" {
  user_project_override = true
}

modules/apis 配下 (モジュール)

# main.tf
resource "google_project_service" "apis" {
  for_each           = toset(var.apis)
  project            = var.project_id
  service            = each.value
  disable_on_destroy = false
}
  
resource "null_resource" "delay" {
  provisioner "local-exec" {
    command = "sleep 180"
  }
  
  depends_on = [google_project_service.apis]
}
  
# outputs.tf
output "enabled_apis" {
  description = "List of enabled APIs for the project"
  value       = [for service in google_project_service.apis : service.id]
}
  
# variables.tf
variable "apis" {
  description = "List of APIs to enable"
  type = list(string)
}
  
variable "project_id" {
  description = "The ID of the project to create resources in"
  type        = string
}

プルリクエスト (terraform plan)

Direct Workload Identity でも、main ブランチへのプルリクエストをトリガーに terraform plan が実行されました。
※ プルリクエストの場合、terraform apply はスキップされます。

プルリクエストをトリガーに terraform plan が自動実行

マージ (terraform apply)

Direct Workload Identity でも、main ブランチへのマージをトリガーに terraform apply が実行されました。
※ マージの場合、terraform plan はスキップされます。

マージをトリガーに terraform apply が自動実行

武井 祐介 (記事一覧)

クラウドソリューション部所属。G-gen唯一の山梨県在住エンジニア

Google Cloud Partner Top Engineer 2025 選出。IaC や CI/CD 周りのサービスやプロダクトが興味分野です。

趣味はロードバイク、ロードレースやサッカー観戦です。