記事の内容
本記事では、GitHub Actions を使った Terraform のCI/CD パイプライン構築について説明します。
GCP環境を利用します。
GitHub Actions 導入後のTerraformアクションのフロー
- main ブランチへのPR作成
- ファイル変更の検知(ファイルの変更のある環境のみにフィルタリング)
- ファイル変更を検知した環境にて、
terraform plan
を実行 - 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
構築手順
-
GCSバケットを手動作成(Terraformのstate用)
-
TerraformでWorkload Identityリソース一式をapply
-
GitHub Actionsの
.github/workflows/terraform.yml
を作成 -
develop環境でVPC作成PRを作成 → develop環境でplanが自動実行される
-
PRをmainにマージし、applyを実行
実際に構築
1. GCSバケットを手動作成(Terraformのstate用)
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
を作成
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結果が出力されます。
5. PRをmainにマージし、applyを実行
構築を終えての感想
GitHub Actionsの作成について、何となく難しいと思っており、抵抗がありましたが、意外と公式ドキュメントを確認しながら手軽に構築できました。
GitHub Actions以外にAtlantisなどのterraformのCICDパイプラインがありますが、昨今、セキュリティ問題が多い中、Atlantisのように自前でサーバーを構築する必要ないことがメリットだと感じました。