1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GitHub ActionsでTerraformのCICDパイプライン構築!

Last updated at Posted at 2025-05-04

記事の内容

本記事では、GitHub Actions を使った Terraform のCI/CD パイプライン構築について説明します。
GCP環境を利用します。

GitHub Actions 導入後のTerraformアクションのフロー

  1. main ブランチへのPR作成
  2. ファイル変更の検知(ファイルの変更のある環境のみにフィルタリング)
  3. ファイル変更を検知した環境にて、terraform planを実行
  4. mainにマージをトリガーに、terraform applyを実行

ディレクトリ構造

repo/
├── modules/
│   └── ...
├── environments/
│   └── develop/
│   │    ├── main.tf
│   │    ├── values.tf
│   │    └── backend.tf
│   └── staging/
│        ├── main.tf
│        ├── values.tf
│        └── backend.tf
└── .github/
│   └── workflows/
│        └── terraform.yml
└── .gitignore     

構築手順

  1. GCSバケットを手動作成(Terraformのstate用)

  2. TerraformでWorkload Identityリソース一式をapply

  3. GitHub Actionsの.github/workflows/terraform.ymlを作成

  4. develop環境でVPC作成PRを作成 → develop環境でplanが自動実行される

  5. PRをmainにマージし、applyを実行

実際に構築

1. GCSバケットを手動作成(Terraformのstate用)

image.png

2. TerraformでWorkload Identityリソース一式をapply

状態ファイル保存先を先ほど作成したGCSバケットにbackend設定の上、下記リソースを作成します。

リソース名 説明
google_service_account GitHub Actions から使うTerraform用サービスアカウント
google_project_iam_member 上記SAに roles/editor の権限を付与
google_iam_workload_identity_pool GitHub OIDC用のIDプール
google_iam_workload_identity_pool_provider GitHub発行のトークンを受け入れるOIDCプロバイダ
google_service_account_iam_member SAとWorkload Identity Federationの紐付け(roles/iam.workloadIdentityUser

以下、上記のリソースをまとめたファイル内容です。

// main.tf
resource "google_project_iam_member" "sa_roles" {
  project = var.project_id
  role    = "roles/editor"
  member  = "serviceAccount:${google_service_account.terraform.email}"
}

resource "google_iam_workload_identity_pool" "github_pool" {
  workload_identity_pool_id = "github-pool"
  display_name              = "GitHub Actions Pool"
  description               = "OIDC identity pool for GitHub Actions"
}

resource "google_iam_workload_identity_pool_provider" "github_provider" {
  workload_identity_pool_id          = google_iam_workload_identity_pool.github_pool.workload_identity_pool_id
  workload_identity_pool_provider_id = "github"
  display_name                       = "GitHub OIDC Provider"

  oidc {
    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 == '${local.github_repo}'"
}

resource "google_service_account" "terraform" {
  account_id   = "terraform-cicd"
  display_name = "Terraform GitHub Actions"
}

resource "google_service_account_iam_member" "wif_binding" {
  service_account_id = google_service_account.terraform.name
  role               = "roles/iam.workloadIdentityUser"
  member             = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.github_pool.name}/attribute.repository/${local.github_repo}"
}

// values.tf
locals {
  github_repo    = "<org/repo>"
  github_repo    = "qiita-dev-458806"
}

// backend.tf
terraform {
  backend "gcs" {
    bucket = "terraform-state-qiita-dev"
    prefix = "tfstate"
  }
}

3. GitHub Actionsの.github/workflows/terraform.ymlを作成

.github/workflows/terraform.yml
name: Terraform

on:
  push:
    branches:
      - main 
    paths:
      - 'environments/**' #environmentsディレクトリ配下の変更時のみをトリガー
  pull_request:
    paths:
      - 'environments/**' #environmentsディレクトリ配下の変更時のみをトリガー

permissions:
  contents: read #リポジトリの読取りに必要な権限
  pull-requests: write #PRコメントに必要な権限
  id-token: write #GCPログインに必要な権限

env:
  GCP_PROJECT_ID_DEV: qiita-dev-458806 #プロジェクトID
  GCP_PROJECT_NUMBER_DEV: 413242293316 #プロジェクト番号(gcloud projects describe <プロジェクトID> --format="value(projectNumber)" コマンドで確認可能)
  tf_actions_working_dir_dev: environments/develop #developディレクトリ
  GCP_PROJECT_ID_STG: <staging環境のプロジェクトID>
  GCP_PROJECT_NUMBER_STG: <staging環境のプロジェクト番号>
  tf_actions_working_dir_stg: environments/staging
  
jobs:
  detect-changes:
    name: Detect changed environments #環境ごとのファイル変更の検知
    runs-on: ubuntu-latest
    outputs:  #各環境のファイルが変更されたかどうかのoutput。trueかfalseが代入される。
      develop: ${{ steps.changes.outputs.develop }}
      staging: ${{ steps.changes.outputs.staging }}
    steps:
      # https://github.com/actions/checkout/tree/v3/
      - uses: actions/checkout@v3 #GitHubのコードをrunnerにクローン。本stepであるファイル変更チェックに必要。

      # https://github.com/dorny/paths-filter/tree/v2/
      - uses: dorny/paths-filter@v2 
        id: changes
        with:
          filters: |
            develop:
              - 'environments/develop/**'
            staging:
              - 'environments/staging/**'

  terraform-develop: #develop環境のファイルで変更を検知した場合にこのjobを実行
    needs: detect-changes #detects-changes jobの後にこのjobを実行することを指定
    if: needs.detect-changes.outputs.develop == 'true'
    runs-on: ubuntu-latest
    name: Terraform (develop)
    defaults:
      run:
        working-directory: ${{ env.tf_actions_working_dir_dev }} #environments/develop配下で本jobを実行することを指定
    steps:
      # https://github.com/actions/checkout/tree/v3/
      - uses: actions/checkout@v3  # このリポジトリの Terraform コードを runner にチェックアウト

      # https://github.com/hashicorp/setup-terraform/tree/v2/
      - uses: hashicorp/setup-terraform@v2 # 指定バージョンの Terraform をインストールして使えるようにする
        with:
          terraform_version: 1.6.6

      - name: Authenticate to Google Cloud via Workload Identity Federation
      # https://github.com/google-github-actions/auth/tree/v1/
        uses: google-github-actions/auth@v1 #OIDC認証
        with:
          workload_identity_provider: "projects/${{ env.GCP_PROJECT_NUMBER_DEV }}/locations/global/workloadIdentityPools/github-pool/providers/github" #このWorkload Identity Providerを作成したGCPプロジェクトの「プロジェクト番号」を指定
          service_account: "terraform-cicd@${{ env.GCP_PROJECT_ID_DEV }}.iam.gserviceaccount.com" #作成したSA

      - 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 | tee plan.txt
          echo "stdout<<EOF" >> $GITHUB_OUTPUT
          cat plan.txt >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT
        continue-on-error: true
        
      - 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 = `### Project: **${process.env.GCP_PROJECT_ID_DEV}** 

            #### 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_dev }}\`, Workflow: \`${{ github.workflow }}\`*`;
      
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: output
            })
      - name: Terraform Apply
        if: github.ref == 'refs/heads/main' && github.event_name == 'push'
        run: terraform apply --auto-approve


  terraform-staging: #staging環境のファイルで変更を検知した場合にこのjobを実行
    needs: detect-changes #detects-changes jobの後にこのjobを実行することを指定
    if: needs.detect-changes.outputs.staging == 'true'
    runs-on: ubuntu-latest
    name: Terraform (staging)
    (下記develop環境と同様に設定)

参考:

トリガー条件

on:
  push:
    branches:
      - main 
    paths:
      - 'environments/**' #environmentsディレクトリ配下の変更時のみをトリガー
  pull_request:
    paths:
      - 'environments/**' #environmentsディレクトリ配下の変更時のみをトリガー

ワークフローのトリガーを「mainブランチにpush時」と「PR作成時」に設定します。

runnnerにダウンロード

    steps:
      # https://github.com/actions/checkout/tree/v3/
      - uses: actions/checkout@v3 #GitHubのコードをrunnerにクローン。本stepであるファイル変更チェックに必要。

actions/checkout アクションを使用して、リポジトリのコードをランナーにチェックアウトします。

変更された環境ディレクトリを検知する

      # https://github.com/dorny/paths-filter/tree/v2/
      - uses: dorny/paths-filter@v2 
        id: changes
        with:
          filters: |
            develop:
              - 'environments/develop/**'
            staging:
              - 'environments/staging/**'

dorny/paths-filter アクションを使用して、develop や staging ディレクトリ配下にファイル変更があったかどうかを検知します。それぞれ true / false を output として返します。これにより、対象環境に変更があった場合のみ、対応する Terraform ジョブを実行できます。

develop環境の変更があった場合のみTerraformジョブを実行

  terraform-develop: #develop環境のファイルで変更を検知した場合にこのjobを実行
    needs: detect-changes #detects-changes jobの後にこのjobを実行することを指定
    if: needs.detect-changes.outputs.develop == 'true'

needs × if を組み合わせて、environments/develop に変更があった場合のみ、このジョブ(terraform plan/apply)を実行するように定義します。

実行ディレクトリの設定

    defaults:
      run:
        working-directory: ${{ env.tf_actions_working_dir_dev }} #environments/develop配下で本jobを実行することを指定

defaults.run.working-directory を指定することで、このジョブ内のすべての run: ステップは environments/develop ディレクトリ内で実行されます。毎回 cd environments/develop などと書く必要がなくなり、YAMLがシンプルになります。

Terraformの準備とGCPへの認証

    steps:
      # https://github.com/actions/checkout/tree/v3/
      - uses: actions/checkout@v3  # このリポジトリの Terraform コードを runner にチェックアウト

      # https://github.com/hashicorp/setup-terraform/tree/v2/
      - uses: hashicorp/setup-terraform@v2 # 指定バージョンの Terraform をインストールして使えるようにする
        with:
          terraform_version: 1.6.6

      - name: Authenticate to Google Cloud via Workload Identity Federation
      # https://github.com/google-github-actions/auth/tree/v1/
        uses: google-github-actions/auth@v1 #OIDC認証
        with:
          workload_identity_provider: "projects/${{ env.GCP_PROJECT_NUMBER_DEV }}/locations/global/workloadIdentityPools/github-pool/providers/github" #このWorkload Identity Providerを作成したGCPプロジェクトの「プロジェクト番号」を指定
          service_account: "terraform-cicd@${{ env.GCP_PROJECT_ID_DEV }}.iam.gserviceaccount.com" #作成したSA
  • actions/checkout:actions/checkout アクションを使用して、リポジトリのコードをランナーにチェックアウト
  • hashicorp/setup-terraform:指定バージョン(ここでは 1.6.6)の Terraform をインストール
  • google-github-actions/auth:GitHub OIDC と GCP の Workload Identity Federation を用いて、認証

OIDC認証の設定

        with:
          workload_identity_provider: "projects/${{ env.GCP_PROJECT_NUMBER_DEV }}/locations/global/workloadIdentityPools/github-pool/providers/github" #このWorkload Identity Providerを作成したGCPプロジェクトの「プロジェクト番号」を指定
          service_account: "terraform-cicd@${{ env.GCP_PROJECT_ID_DEV }}.iam.gserviceaccount.com" #作成したSA
  • workload_identity_provider:GitHub OIDC トークンを受け入れる GCP 側の Workload Identity Provider のリソースパスを指定します。
  • service_account:Terraform の実行に使用する GCP サービスアカウントのメールアドレスを指定します。このサービスアカウントには必要な権限(例:roles/editor)が付与されている必要があります。

terraform fmt~terraform plan

      - 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 | tee plan.txt
          echo "stdout<<EOF" >> $GITHUB_OUTPUT
          cat plan.txt >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT
        continue-on-error: true
        
      - 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 = `### Project: **${process.env.GCP_PROJECT_ID_DEV}** 

            #### 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_dev }}\`, Workflow: \`${{ github.workflow }}\`*`;
      
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: output
            })

このステップでは、Terraform の整形チェック(fmt)、初期化(init)、構文検証(validate)、差分確認(plan)を順に実行しています。
差分確認の結果は、PRコメントに出力されるよう設定しており、Terraform の実行結果をレビューしやすくしています。
こちらの実装は、公式:セットアップ-Terraformをベースにしています。

PRがmainブランチにマージされたときのみTerraformを適用

      - name: Terraform Apply
        if: github.ref == 'refs/heads/main' && github.event_name == 'push'
        run: terraform apply --auto-approve

main ブランチにPRがマージされたタイミングで、terraform apply を実行してインフラ変更を実環境に反映します。
これは push イベントを利用していますが、実際の運用では「マージされたとき」に適用される、という理解で問題ありません。

4. develop環境でVPC作成PRを作成 → develop環境でplanが自動実行される

environments/develop配下のファイルでVPC作成コードを記載し、PRを作成します。
ファイル変更を検知しなかったstaging環境はplanの実行をスキップします。

PRのコメントにて、変更を検知したdevelop環境でterraform planが自動実行され、Plan結果が出力されます。
image.png
image.png

5. PRをmainにマージし、applyを実行

mainにマージする
image.png

Actionsタブにて、Apply結果を確認します。
image.png

構築を終えての感想

GitHub Actionsの作成について、何となく難しいと思っており、抵抗がありましたが、意外と公式ドキュメントを確認しながら手軽に構築できました。
GitHub Actions以外にAtlantisなどのterraformのCICDパイプラインがありますが、昨今、セキュリティ問題が多い中、Atlantisのように自前でサーバーを構築する必要ないことがメリットだと感じました。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?