decadence

個人のメモ帳

GitHub Actionsとtfcmtのみで実現する、atlantis風のterraform workflow

これは何

Atlantis を利用すると、PullRequestを利用したterraform実行環境として以下のようなWorkflowが実現出来る。

  • terraform の変更を含む PR を作成
  • PR上でplanをコメントで表示
  • atlantis apply のコメントをPRにすることでterraform applyを実施して、コメントで表示

とはいえatlantisはサーバを立てる必要もあるし、GitHub Actionsのみで簡易的なものを実現してみた事例が、今回述べたいこと。

今回実現したこと

以下のことを実現している

  • PRでterraform planの結果を表示
  • PRにコメントをすることで、terraform applyを実施して、結果をコメントに表示

一方で、atlantisで出来るような以下のことについては対応していない。

  • plan結果をもとにしたapply実施
  • approve済みかどうかの確認
  • 複数のworking directory対応
  • 等々

事前準備

実施環境によるが、GCP等に対してOIDCで認証情報を得てGitHub Actionを実行する際の設定等を事前に実施する。

▷ GitHub ActionsでGCPを操作する際における実装例はここを展開

GitHub Actions と Terraform Cloud に対応した Workload Identity 連携の構成 | Google Cloud 公式ブログ

resource "google_iam_workload_identity_pool" "github_actions" {
  project                   = var.project_id
  workload_identity_pool_id = "github-actions"
  display_name              = "github-actions"
}

resource "google_iam_workload_identity_pool_provider" "github_actions" {
  project                            = var.project_id
  workload_identity_pool_id          = google_iam_workload_identity_pool.github_actions.workload_identity_pool_id
  workload_identity_pool_provider_id = "github-actions"
  display_name                       = "github-actions"

  attribute_condition = "assertion.repository_owner=='${local.repository_owner}'"
  attribute_mapping = {
    "google.subject"             = "assertion.sub"
    "attribute.repository"       = "assertion.repository"
    "attribute.repository_owner" = "assertion.repository_owner"
    "attribute.branch"           = "assertion.sub.extract('/heads/{branch}/')"
  }

  oidc {
    issuer_uri = "https://token.actions.githubusercontent.com"
  }
}

resource "google_service_account" "terraform_plan" {
  project      = var.project_id
  account_id   = "github-actions-terraform-plan"
  display_name = "github-actions-terraform-plan"
}

resource "google_service_account_iam_member" "terraform_plan" {
  service_account_id = google_service_account.terraform_plan.id
  role               = "roles/iam.workloadIdentityUser"
  member             = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.github_actions.name}/attribute.repository/${local.repository_owner}/${local.repository_name}"
}

resource "google_project_iam_member" "terraform_plan" {
  for_each = toset([
    "roles/viewer",
  ])

  role    = each.key
  member  = "serviceAccount:${google_service_account.terraform_plan.email}"
  project = var.project_id
}

GitHub Actionsにおける設定

planを実施するworkflow

以下のようなworkflowを用意することで、PRに対してterraform planを実施してコメントをすることは可能。ここまではよくある基本例。

  • base_ref に応じた設定の変更
  • GCPに対するOIDC
  • aquaを利用した各種ツールのinstall
  • terraform planを実施して、tfcmtでコメントを付与
name: terraform plan

on:
  pull_request:
    branches:
    - dev
    - prd
    types:
    - opened
    - synchronize
    paths:
    - infra/**
    - .github/workflows/terraform-plan.yml

jobs:
  plan:
    runs-on: ubuntu-latest
    timeout-minutes: 20
    permissions:
      id-token: write
      contents: read
      pull-requests: write
    steps:
    - name: env for dev
      if: github.base_ref == 'dev'
      run: |
        echo "WORK_DIR=infra/gcp/envs/dev" >> $GITHUB_ENV
        echo "PROJECT_ID=something-dev" >> $GITHUB_ENV
        echo "PROJECT_NUMBER=1234567890" >> $GITHUB_ENV

    - uses: actions/checkout@v4
    - uses: actions/cache@v4
      with:
        path: ~/.local/share/aquaproj-aqua
        key: v1-aqua-installer-terraform-${{runner.os}}-${{runner.arch}}-${{hashFiles('aqua.yaml')}}
        restore-keys: |
          v1-aqua-installer-terraform-${{runner.os}}-${{runner.arch}}-
    - uses: aquaproj/aqua-installer@v3.0.1
      with:
        aqua_version: v2.28.0

    - uses: google-github-actions/auth@v2
      with:
        workload_identity_provider: 'projects/${{ env.PROJECT_NUMBER }}/locations/global/workloadIdentityPools/YOUR_POOL_NAME/providers/YOUR_PROVIDER_ID'
        service_account: github-actions-terraform-plan@${{ env.PROJECT_ID }}.iam.gserviceaccount.com
    - working-directory: ${{ env.WORK_DIR }}
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      run: |
        terraform init -reconfigure -no-color -input=false
        tfcmt plan -patch -- terraform plan -no-color -input=false -lock=false

applyを実施するworkflow

以下のようなworkflowを用意することで、PRに /terraform apply というコメントを付与することで、以下のことを実施する

  • base_ref に応じた設定の変更
  • GCPに対するOIDC
  • aquaを利用した各種ツールのinstall
  • terraformを実施して、tfcmtでコメントを付与
name: terraform apply

on:
  # https://docs.github.com/ja/webhooks/webhook-events-and-payloads#issue_comment
  issue_comment:
    types: [created]

jobs:
  apply:
    runs-on: ubuntu-latest
    timeout-minutes: 20
    if: ${{ github.event.issue.pull_request }} && github.event.comment.body == '/terraform apply'
    permissions:
      id-token: write
      contents: read
      pull-requests: write
    steps:
    - uses: actions/checkout@v4
      with:
        ref: refs/pull/${{ github.event.issue.number }}/head
    - name: get pull request info
      run: |
        echo "BASE_REF_NAME=$(gh pr view $PR_NO --repo $REPO --json baseRefName --jq '.baseRefName')" >> $GITHUB_ENV
        echo "HEAD_REF_NAME=$(gh pr view $PR_NO --repo $REPO --json headRefName --jq '.headRefName')" >> $GITHUB_ENV
      env:
        REPO: ${{ github.repository }}
        PR_NO: ${{ github.event.issue.number }}
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    - name: env for dev
      if: env.BASE_REF_NAME == 'dev'
      run: |
        echo "WORK_DIR=infra/gcp/envs/dev" >> $GITHUB_ENV
        echo "PROJECT_ID=something-dev" >> $GITHUB_ENV
        echo "PROJECT_NUMBER=1234567890" >> $GITHUB_ENV

    - uses: actions/cache@v4
      with:
        path: ~/.local/share/aquaproj-aqua
        key: v1-aqua-installer-terraform-${{runner.os}}-${{runner.arch}}-${{hashFiles('aqua.yaml')}}
        restore-keys: |
          v1-aqua-installer-infra-${{runner.os}}-${{runner.arch}}-
    - uses: aquaproj/aqua-installer@v3.0.1
      with:
        aqua_version: v2.28.0

    - uses: google-github-actions/auth@v2
      with:
        workload_identity_provider: 'projects/${{ env.PROJECT_NUMBER }}/locations/global/workloadIdentityPools/YOUR_POOL_NAME/providers/YOUR_POOL_ID'
        service_account: github-actions-terraform-apply@${{ env.PROJECT_ID }}.iam.gserviceaccount.com
    - working-directory: ${{ env.WORK_DIR }}
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      run: |
        terraform init -reconfigure -no-color -input=false
        tfcmt -pr ${{ github.event.issue.number }} apply -- terraform apply -no-color -input=false -auto-approve

issue_commentをtriggerにしてGitHub Actionsを実行している。issue_comment には以下のようなクセがある。

  • default branchにおけるworkflowが実施される
    • 検証を行う際にも、default branchへ内容を取り込む必要がある
    • actions/checkoutで対応されるのも、未指定の場合default branchとなるため、 ref: refs/pull/${{ github.event.issue.number }}/head のようにPRに対する変更を含めたものをcheckoutする
  • pull requestに関する情報が少ない
    • issue_numberは取得出来るものの、eventにはbase_refなどが含まれていない
    • Webhook のイベントとペイロード - GitHub Docs に定義がある
    • そのため、各種情報を取得するには permissions.pull-requests の設定を追加した上で、ghコマンドで取得する必要がある
  • tfcmtにおいてもどのPRに対してコメントをするのかを明示する必要がある

Webhook のイベントとペイロード - GitHub Docs にある情報は利用出来るものの、approve済みかどうかなどもデフォルトのeventからは取得出来ない。こちらの機能に加えてatlantisのようにreview済みのみのものしか反映させたくない等の実装を加えたい場合は、随時 gh コマンドなどで情報を取得するのが良い。

PRをmergeした際にterraform applyを実施するという運用もあるが、以下の理由からmerge前に実施したいこともあると思う。

  • terraform applyは失敗しうる
  • merge後に走るcdの内容を、事前に変更したい

様々な運用がある中で、こういったやり方を求める人は参考にしてもらえると幸いである。

tfaction | tfaction で十分という人はそれでよい。