🌙

GitHub Actions と Workload Identity 連携による Google Cloud リソース管理のハンズオン

2024/06/19に公開

はじめに

こんにちは、クラウドエース SRE 部所属の大槻です。クラウドエースでは、Terraform による Google Cloud リソース管理を実践する機会が多くあります。
そこで今回は、Google Cloud での CI/CD 手法を 1 つ紹介したいと思います。GitHub Actions のワークフローを使用して、Open ID Connect (以下、OIDC) による認証を行い、 Terraform で Google Cloud リソースを管理する方法をハンズオン形式で紹介していきます。

Workload Identity 連携

GitHub Actions から、Google Cloud リソースへのアクセスのため、Workload Identity 連携を使用して、サービス アカウントの権限を借用します。Workload Identity 連携を使用することで、サービス アカウント キーの代わりにフェデレーション ID を使用して、Google Cloud リソースへのアクセス権を付与できます。

https://cloud.google.com/iam/docs/workload-identity-federation?hl=ja

GitHub Actions では OIDC をサポートしており、有効期限が短いトークンを利用して Google Cloud へのアクセスを制御することができます。

https://docs.github.com/ja/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect

もう一つの接続方法として、サービス アカウント キーを利用する方法がありますが、セキュリティ上のリスクとなるため推奨されておりません。

https://cloud.google.com/iam/docs/best-practices-service-accounts?hl=ja#service-account-keys

Workload Identity 連携の詳しい図などは、こちらの記事の図を参考にしていただけると幸いです。

https://zenn.dev/cloud_ace/articles/7fe428ac4f25c8#各種設定のイメージ

設定手順

早速ですが、以下に具体的な設定手順例を記載していきます。

1. Workload Identity 連携の設定

コンソールから「IAM と管理」、「Workload Identity 連携」を選択し、「プールを作成」をクリックします。
「名前」を入力し、「続行」をクリックします。(ここでは、github-actions-pool とします。)
WorkloadIdentityPool

「プロバイダの選択」で「OpenID Connect(OIDC)」を選択します。
「プロバイダ名」を入力し、「発行元(URL)」に https://token.actions.githubusercontent.com を入力し、「続行」をクリックします。(ここではプロバイダ名をgithub-actions とします。)
WorkloadIdentityProvider

「プロバイダの属性を構成する」で、下表の値を設定し、「保存」をクリックします。

項目名 設定値
google1 google.subject
OIDC1 assertion.sub
google2 attribute.repository
OIDC2 assertion.repository

WorkloadIdentityAssertion
表の項目名設定値の詳細については、リンクを参照ください。

2. サービス アカウントの設定

コンソールから、「IAM と管理」、「サービス アカウント」を選択し、「サービス アカウントを作成」をクリックします。
サービス アカウント名を入力して、「作成して続行」をクリックします。(ここでは、github-actions とします。)
SA作成

「ロールを選択」から権限を付与し、「完了」をクリックします。(ここでは、編集者 権限を付与することとします。)
SA権限

作成したサービス アカウントをクリックして、「権限」タブを開きます。
「アクセス権を付与」をクリックして、下表の値を設定し、「保存」をクリックします。

項目名 設定値
新しいプリンシパル principalSet://iam.googleapis.com/projects/[PROJECT_NUMBER]/locations/global/workloadIdentityPools/github-actions-pool/attribute.repository/[PROJECT_ID]/github-actions
ロール Workload Identity ユーザー

SA権限借用

3. GitHub Actions の設定

.github/workflows/ 配下に yaml ファイルを作成することで、GitHub Actions の設定を行うことができます。
今回は以下の 3 つの yaml ファイルを作成します。

  1. auth-test.yaml
    Workload Identity 連携によるサービス アカウントの権限借用を検証するもので、GitHub Actions を手動で実行します。
  2. terraform-plan.yaml
    terraform plan を実行するもので、main ブランチへの PR 作成時に実行されます。
  3. terraform-apply.yaml
    terraform apply を実行するもので、main ブランチへの v* のタグ付与時に実行されます。

以下の 3 つのファイルを .github/workflows/ 配下に作成します。
ファイル作成後、main ブランチに push します。

# auth-test.yaml
name: Auth test
run-name: Auth test by @${{ github.actor }}

on:
  workflow_dispatch: # Manually run

permissions:
  contents: read
  id-token: write

env:
  PROJECT_ID: [PROJECT_ID]
  PROJECT_NUMBER: [PROJECT_NUMBER]

jobs:
  auth_test:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout
      uses: actions/checkout@v4

    - name: Auth
      uses: google-github-actions/auth@v2
      with:
        project_id: ${{ env.PROJECT_ID }}
        workload_identity_provider: projects/${{ env.PROJECT_NUMBER }}/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions
        service_account: github-actions@${{ env.PROJECT_ID }}.iam.gserviceaccount.com

    - name: Set up Cloud SDK
      uses: google-github-actions/setup-gcloud@v2

    - name: Use Cloud SDK
      run: gcloud config list
# terraform-plan.yaml
name: Terraform Plan
run-name: Terraform Plan by @${{ github.actor }}

on:
  pull_request:
    branches:
      - main

permissions:
  contents: read
  id-token: write
  pull-requests: write

env:
  PROJECT_ID: [PROJECT_ID]
  PROJECT_NUMBER: [PROJECT_NUMBER]
  TERRAFORM_VERSION: 1.8.5

jobs:
  terraform-plan:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: hashicorp/setup-terraform@v3
      with:
        terraform_version: ${{ env.TERRAFORM_VERSION }}

    - uses: google-github-actions/auth@v2
      with:
        project_id: ${{ env.PROJECT_ID }}
        workload_identity_provider: projects/${{ env.PROJECT_NUMBER }}/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions
        service_account: github-actions@${{ env.PROJECT_ID }}.iam.gserviceaccount.com

    - name: Terraform fmt
      id: fmt
      run: terraform fmt -check
      continue-on-error: true

    - name: Terraform Init
      id: init
      run: terraform init

    - name: Terraform Validate
      id: validate
      run: terraform validate -no-color

    - name: Terraform Plan
      id: plan
      run: terraform plan -no-color
      continue-on-error: true

    - uses: actions/github-script@v7
      if: github.event_name == 'pull_request'
      env:
        PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
      with:
        github-token: ${{ secrets.GITHUB_TOKEN }}
        script: |
          // 1. Retrieve existing bot comments for the PR
          const { data: comments } = await github.rest.issues.listComments({
            owner: context.repo.owner,
            repo: context.repo.repo,
            issue_number: context.issue.number,
          })
          const botComment = comments.find(comment => {
            return comment.user.type === 'Bot' && comment.body.includes('Terraform Format and Style')
          })

          // 2. Prepare format of the comment
          const output = `#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\`
          #### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\`
          #### Terraform Validation 🤖\`${{ steps.validate.outcome }}\`
          <details><summary>Validation Output</summary>

          \`\`\`\n
          ${{ steps.validate.outputs.stdout }}
          \`\`\`

          </details>

          #### Terraform Plan 📖\`${{ steps.plan.outcome }}\`

          <details><summary>Show Plan</summary>

          \`\`\`\n
          ${process.env.PLAN}
          \`\`\`

          </details>

          *Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`, Working Directory: \`${{ env.tf_actions_working_dir }}\`, Workflow: \`${{ github.workflow }}\`*`;

          // 3. If we have a comment, update it, otherwise create a new one
          if (botComment) {
            github.rest.issues.updateComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              comment_id: botComment.id,
              body: output
            })
          } else {
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: output
            })
          }
# terraform-apply.yaml
name: Terraform Apply
run-name: Terraform Apply by @${{ github.actor }}

on:
  workflow_dispatch: # Manually run
  push:
    tags:
      - v*

permissions:
  contents: read
  id-token: write

env:
  PROJECT_ID: [PROJECT_ID]
  PROJECT_NUMBER: [PROJECT_NUMBER]
  TERRAFORM_VERSION: 1.8.5

jobs:
  terraform-apply:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: hashicorp/setup-terraform@v3
      with:
        terraform_version: ${{ env.TERRAFORM_VERSION }}

    - uses: google-github-actions/auth@v2
      with:
        project_id: ${{ env.PROJECT_ID }}
        workload_identity_provider: projects/${{ env.PROJECT_NUMBER }}/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions
        service_account: github-actions@${{ env.PROJECT_ID }}.iam.gserviceaccount.com

    - name: Terraform fmt
      id: fmt
      run: terraform fmt -check
      continue-on-error: true

    - name: Terraform Init
      id: init
      run: terraform init

    - name: Terraform Validate
      id: validate
      run: terraform validate -no-color

    - name: Terraform Plan
      id: plan
      run: terraform plan -no-color
      continue-on-error: true

    - name: Terraform apply
      id: apply
      run: terraform apply -auto-approve -no-color

4. Terraform の設定

ブランチを切り替え、以下のファイルをルートフォルダに作成します。
ファイル作成後、動作確認のため main ブランチへの PR を作成します。

resource "google_storage_bucket" "main" {
  name          = "[PROJECT_ID]_static_website_bucket"
  location      = "asia-northeast1"
  storage_class = "STANDARD"
  website {
    main_page_suffix = "index.html"
  }
}

resource "google_storage_bucket_object" "main" {
  name         = "index.html"
  content      = "<html><body>Hello World!</body></html>"
  content_type = "text/html"
  bucket       = google_storage_bucket.main.id
}

terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = ">=5.33.0"
    }
  }
  required_version = "1.8.5"
}

動作確認

1. Workload Identity 連携

GitHub の Actions タブから、「Auth test」を選択し、「Run workflow」をクリックして、「Run workflow」をクリックします。
Actions手動実行

実行結果を確認し、「service account」が「github-actions@[PROJECT_ID].iam.gserviceaccount.com」と表示されていれば OK です。
auth test

2. Terraform plan

「Pull requests」画面を表示し、「Terraform Plan」が成功していることを確認します。
terraform plan

「Show Plan」の内容もしっかり確認しましょう。
ここでは Google Cloud Storage(以下、GCS)バケットを作成し、GCS バケット内に index.html オブジェクトを作成する内容が表示されていれば OK です。
terraform plan detail

上記が問題なければ、「Merge pull request」をクリックし、main ブランチへマージします。
merge

3. Terraform apply

main ブランチにタグ付与し、「Actions」画面から実行結果を確認します。
「Terraform apply」 が成功していれば OK です。
apply

コンソールから「Cloud Storage」、「バケット」を選択し、作成した GCS バケット名をクリックします。
「index.html」を選択し、「認証済み URL」を開きます。
hello world

ブラウザに「Hello World!」と表示されていれば OK です。

おわりに

今回は Workload Identity 連携を利用して、GitHub Actions から Terraform 実行するところまで、手順を紹介しました。参考資料が Google Cloud、GitHub、Terraform と分散しており理解に苦労しましたが、実装自体は案外簡単にできました。

是非、お手元の環境で実践してみてください。
最後まで読んでいただきありがとうございました。

Discussion