記事の内容
本記事では、GitHub Actions を使った Terraform のCI/CD 環境構築について説明します。
GCP環境を利用します。
GitHub Actions 導入後のTerraformアクションのフロー
- main ブランチへのPR作成
- ファイル変更の検知(ファイルの変更のある環境のみにフィルタリング)
- ファイル変更を検知した環境にて、terraform planを実行
- mainにマージをトリガーに、terraform applyを実行
ディレクトリ構造
rep/
├── modules/
│ └── ...
├── environments/
│ └── develop/
│ │ ├── main.tf
│ │ ├── locals.tf
│ │ ├── variables.tf
│ │ └── backend.tf
│ └── staging/
│ ├── main.tf
│ ├── locals.tf
│ ├── variables.tf
│ └── backend.tf
└── .github/
│ └── workflows/
│ └── terraform.yml
└── .gitignore
構築手順
- GCSバケットを手動作成(Terraformのstate用)
- TerraformでWorkload Identityリソース一式をapply
- GitHub Actionsのworkflow.ymlを作成
- GitHub上でSecretsを設定する
- develop環境でVPC作成PRを作成 → develop環境でplanが自動実行される
- PRをmainにマージし、applyを実行
実際に構築
1. GCSバケットを手動作成(Terraformのstate用)
2. TerraformでWorkload Identityリソース一式をapply
状態ファイル保存先を先ほど作成したGCSバケットに設定の上、下記リソースを作成します。
- `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.repository_owner" = "assertion.repository_owner"
}
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}"
}
// variables.tf
variable "project_id" {
default="qiita-dev-458806"
type = string
}
// locals.tf
locals {
github_repo = "<org/repo>"
}
3. GitHub Actionsのworkflow.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ログインに必要な権限
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)
env:
tf_actions_working_dir_dev: environments/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/${{ secrets.GCP_PROJECT_NUMBER_DEV }}/locations/global/workloadIdentityPools/github-pool/providers/github" #このWorkload Identity Providerを作成したGCPプロジェクトの「プロジェクト番号」を指定
service_account: "terraform-cicd@qiita-dev-458806.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 }}"
GCP_PROJECT_ID_DEV: ${{ secrets.GCP_PROJECT_ID_DEV }}
GCP_PROJECT_NUMBER_DEV: ${{ secrets.GCP_PROJECT_NUMBER_DEV }}
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環境と同様に設定)
参考:
4. GitHub上でSecretsを設定する
上記.github/workflows/terraform.ymlで定義したSecret値について、GitHubリポジトリ上で設定します。
- GCP_PROJECT_NUMBER_DEV:プロジェクト番号
(gcloud projects describe <プロジェクトID> --format="value(projectNumber)" コマンドで確認可能) - GCP_PROJECT_ID_DEV:プロジェクトID
なぜプロジェクトID・プロジェクト番号を Secret にするのか?
workflow.yml
に記述される GCP 認証設定では、環境ごとに異なる GCP プロジェクトの ID(文字列)および プロジェクト番号(数値)を使用します。
これらは環境によって変わる上、Terraformの apply 対象を決める重要な値であるため、以下の理由から Secret 化しています:
- 🔐 セキュリティ:コード上に公開されるリスクを防止
- 🔁 環境の切り替えを簡単に:staging / production で再利用可能に
- 🚫 誤デプロイ防止:対象環境を Secret で明示することで、意図しないプロジェクトへの操作を防ぐ
5. develop環境でVPC作成PRを作成 → develop環境でplanが自動実行される
environments/develop配下のファイルでVPC作成コードを記載し、PRを作成します。
ファイル変更を検知しなかったstaging環境はplanの実行をスキップします。
PRのコメントにて、変更を検知したdevelop環境でterraform planが自動実行され、Plan結果が出力されます。
6. PRをmainにマージし、applyを実行
構築を終えての感想
GitHub Actionsの作成について、何となく難しいと思っており、抵抗があったが、意外と公式ドキュメントを確認しながら手軽に構築できました。
AtlantisなどGitHub Actions以外にterraformのCICD環境があるが、昨今、セキュリティ問題が多い中、Atlantisのように自前でサーバーを構築する必要ないことがメリットだと感じました。