GitHub ActionsでWorkload Identity 連携を使ってTerraformを実行する

この記事は MicroAd Advent Calendar 2022 の13日目の記事です。

https://qiita.com/advent-calendar/2022/microad

# 概要

GitHub ActionsでTerraformを実行する手順について記載します。
Terraformで作成する対象はGoogle Cloud のリソースを想定し、認証にはWorkload Identity 連携を利用します。
情報としては珍しいものではありませんが、実際に動かすまでに時間がかかったので備忘録として投稿します。
（TerraformやActionsについて基本な説明もありませんので、ご承知ください。）

# Workload Identity 連携とは

Workload Identity 連携とは、OpenID Connect（OIDC）やAWSなどのGoogle Cloud以外のIDを利用し、Google Cloudのサービスアカウントの権限を借用する仕組みです。

従来はCIでGoogle Cloudにアクセスするには、サービスアカウントのキーをCIツールに登録する必要がありましたが、Workload Identity 連携を利用することで、CI側にはキーなどのクレデンシャル情報を登録せずにサービスアカウントの権限を利用することができます。そのため、キーの漏洩リスクや管理コストを減らすことができます。

https://cloud.google.com/iam/docs/workload-identity-federation

GitHub ActionsからWorkload Identity 連携を利用するために、Workload Identity プールとプロバイダの準備をします。

準備の手順は以下に記載されていますが、今回はTerraformを用いて作成します。
サイト内には、管理コンソールとgcloudコマンドによる手順が記載されていますので、最初は手順を確認しつつ手動での構築がおすすめです。

[ID 連携により有効期間の短い認証情報を取得する | IAM のドキュメント | Google Cloud](https://cloud.google.com/iam/docs/using-workload-identity-federation)

Terraformでの構築に使ったコードを以下に載せます。

```providers.tf
terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "4.45.0"
    }
  }
}

provider "google" {
  project = local.project_id
  region  = local.region
  zone    = local.zone
}
```

```locals.tf
locals {
  project_id = "<your-project-id>"
  region     = "us-central1"
  zone       = "us-central1-a"
}
locals {
  terraform_sa = "<Actions実行時に借用したい権限を持つサービスアカウント>"
  repo_name    = "<GitHubユーザ名>/<GitHubリポジトリ名>"
}

```

```main.tf
resource "google_project_service" "project" {
  project = local.project_id
  service = "iamcredentials.googleapis.com"
}

resource "google_iam_workload_identity_pool" "github_actions_pool" {
  provider                  = google-beta
  project                   = local.project_id
  workload_identity_pool_id = "github-oidc-pool"
  display_name              = "github-oidc-pool"
  description               = "GitHub Actionsで使用"
}

resource "google_iam_workload_identity_pool_provider" "github_actions" {
  provider                           = google-beta
  project                            = local.project_id
  workload_identity_pool_id          = google_iam_workload_identity_pool.github_actions_pool.workload_identity_pool_id
  workload_identity_pool_provider_id = "github-actions"
  display_name                       = "github-actions"
  description                        = "GitHub Actionsで使用"
  attribute_mapping = {
    "google.subject"       = "assertion.sub"
    "attribute.repository" = "assertion.repository"
  }
  oidc {
    issuer_uri = "https://token.actions.githubusercontent.com"
  }
}

# Actionsで借用したい権限を持つサービスアカウント
data "google_service_account" "terraform_sa" {
  account_id = local.terraform_sa
}

# principalSetでどのリポジトリで利用するかを指定している
resource "google_service_account_iam_member" "terraform_sa_workload_identity_user" {
  service_account_id = data.google_service_account.terraform_sa.id
  role               = "roles/iam.workloadIdentityUser"
  member             = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.github_actions_pool.name}/attribute.repository/${local.repo_name}"
}
```


# GitHub Actions 実行例

Workload Identity プール・プロバイダの準備ができたら、ActionsのWorkflowを追加し、サービスアカウトの権限でTerraformが実行できる試します。

以下にWorkflowの例を示します。
参考にした情報は以下の通りになっており、その他細かい注意点はコード中に記載します。

前半の認証に関わる部分は、前節にも記載している「[ID 連携により有効期間の短い認証情報を取得する](https://cloud.google.com/iam/docs/using-workload-identity-federation)」にActionsでの記述例が記載されています。

Terraformの実行部分は以下を参考にしました。PRへのコメントなど見やすくなっておりいい感じです。

https://developer.hashicorp.com/terraform/tutorials/automation/github-actions


```.github/workflows/terraform.yml
name: terraform

# PR作成とマージ後のmainブランチへのPushをトリガーとする
on:
  push:
    branches:
      - main
  pull_request:

# ./infra ディレクトリ配下に tf ファイルがある想定
defaults:
  run:
    working-directory: ./infra

jobs:
  terraform-workflow:
    runs-on: ubuntu-latest
    permissions:
      # workload identity連携によるToken発行には以下権限が必要
      id-token: write
      contents: read
      # PR画面でterraform plan結果を投稿させるために以下権限が必要
      pull-requests: write

    steps:
      # https://github.com/google-github-actions/auth
      - uses: actions/checkout@v3
      - id: 'auth'
        name: 'Authenticate to Google Cloud'
        # 古いバージョンのactionを利用する例がWebで見かけたので最新になっているか注意
        uses: 'google-github-actions/auth@v1'
        with:
          # 以下、プロバイダのIDは管理コンソールから確認できる
          workload_identity_provider: 'projects/<project number>/locations/global/workloadIdentityPools/<workload Identity Poolの名前>/providers/<プロバイダ名>'
          service_account: '<サービスアカウントをE-mail形式で記載>'

      # ここから terraform 
      # https://github.com/hashicorp/setup-terraform
      - uses: hashicorp/setup-terraform@v2
      # 上記と別に古いAction（hashicorp/terraform-github-actions）があるので、そちらを使わないように注意（すでにアーカイブ済み）

      - name: Terraform fmt
        id: fmt
        run: terraform fmt -check -recursive
        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: Comment Terraform Plan
        uses: actions/github-script@v6
        if: github.event_name == 'pull_request'
        env:
          PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            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 }}\`*`;

            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: output
            })
      # terraform status で失敗した際に workflow も止める
      - name: Terraform Plan Status
        if: steps.plan.outcome == 'failure'
        run: exit 1

      # main ブランチへの push した際のみ terraform apply を実行する
      - name: Terraform Apply
        if: github.ref == 'refs/heads/main' && github.event_name == 'push'
        run: terraform apply -auto-approve -input=false

```

上記Workflowと`./infra`配下にtfファイルを追加し、PR作成するとActionsが実行され、TerraformのPlan結果がPR画面に投稿されます。

![pr-comment.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510411/d7c89121-f773-12c0-6776-8e7f94b4fe5f.png)