はじめに
いざTerraformでインフラを管理しようとなった際に、どうすれば安全なパイプラインを構築できるものか、迷うことかと思います。
この記事ではTerraformのCI/CDをGitHub Actionsで構築する方法を紹介します。
前提
この記事は、以下の記事の続きとなっております。
もしも前提となるコンテキストが必要となる場合、こちらの記事もご覧ください。
また、本パイプラインは以下を前提としています。
- AWSのリソースを管理する
- 環境はstgとprdの2環境を想定する
- planは自動で、applyはGitHub Actionsから手動でキックする
概要
結論として、以下のリポジトリにコードを置いています。
https://github.com/gonkunkun/terraform-template/tree/main/.github/workflows
Planについて
Planの実行は以下の仕様となっています。
- PRの作成/PRの更新時にPlanが実行される
- stgとprdの2環境に対するPlanが同時に実行される
- コードの変更がない場合にはPlanをスキップする(クレジットの節約のため)
- Planの実行結果はPRのコメントに貼り付けられる
Applyについて
CDにおいては、GitHubの無料枠の制約を受けています。
具体的には、有料枠のみで提供されている以下の機能が使えない制約がありました。
- ブランチの保護ルールを設定する
- GitHub ActionsのApply前のStepに承認プロセス(Approve)を設ける
そこで、いくつか妥協を重ねつつ、Applyの実行を以下の仕様としました。
- mainブランチの状態に対してApplyを実行する(これは仕様というか運用ルールです)
- ここは運用対処で凌いでいる
- mainブランチとApply対象の環境を指定して、apply用のCDをキックする
- CDキック後は、Plan -> 数秒間のwait(Planの確認用) -> Applyの順でパイプラインが動作する
- 承認プロセスを挟めないので、手動でキックしている
(複数人が同時にmainにマージすると、PRのPlan結果と異なるApplyが適用されるリスクがあります) - 擬似的に承認プロセスを挟めるようなモジュールもあったものの、承認待ちの間もクレジットを消費してしまうので、節約の観点で諦めました
- 承認プロセスを挟めないので、手動でキックしている
詳細
重複したコードが多いので、もっと綺麗に整理することもできると思います。
事前準備
- Terraformユーザ用のクレデンシャルはGitHubリポジトリ側に設定する
都度都度クレデンシャルを持ってくる方がいいのかもしれないですが、今回は楽なやり方を選んでます。
Plan
name: Terraform Plan
on:
pull_request:
types:
- opened
- synchronize
branches:
- '*'
permissions:
contents: read
pull-requests: write
jobs:
check_changed_dirs:
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix_dirs }}
steps:
- run: echo "fetch_depth=$(( commits + 1 ))" >> $GITHUB_ENV
env:
commits: ${{ github.event.pull_request.commits }}
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: ${{ env.fetch_depth }}
- name: Set Matrix
id: set-matrix
run: |
git fetch origin ${{ github.base_ref }}
changed_files=$(git diff --name-only origin/${{ github.base_ref }})
terraform_dirs=$(echo "$changed_files" | grep -E '^[^/]+/.*\.tf$' | awk -F/ '{print $1}' | sort -u || echo "")
if [ -z "$terraform_dirs" ]; then
echo "No Terraform directories changed."
else
echo "Changed directories: $terraform_dirs"
fi
echo "matrix_dirs=$(echo $terraform_dirs | jq -R -s -c 'split("\n") | map(select(. != ""))')" >> "$GITHUB_OUTPUT"
shell: bash
plan:
name: Plan
runs-on: ubuntu-latest
needs: check_changed_dirs
if: ${{ needs.check_changed_dirs.outputs.matrix != '[]' && needs.check_changed_dirs.outputs.matrix != '' }}
strategy:
fail-fast: false
matrix:
dir: ${{ fromJson(needs.check_changed_dirs.outputs.matrix) }}
env: [stg, prd]
steps:
# Setup
- name: Checkout code
uses: actions/checkout@v4
- name: Setup AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: "ap-northeast-1"
- name: Get Terraform version
id: terraform-version
uses: bigwheel/get-terraform-version-action@v1.2.0
with:
path: ${{ matrix.dir }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ steps.terraform-version.outputs.terraform-version }}
# Cache Terraform plugins
- name: Cache Terraform plugin directory
uses: actions/cache@v3
with:
path: .terraform
key: terraform-plugin-${{ runner.os }}-${{ hashFiles('**/*.tf*') }}
- name: Run Terraform Format Check
run: terraform fmt -recursive -check
# Run Terraform Init
- name: Terraform Init
run: |
terraform init -backend-config="envs/${{ matrix.env }}/backend.hcl"
working-directory: ${{ matrix.dir }}
- name: Run Terraform Validate Check
run: terraform validate
working-directory: ${{ matrix.dir }}
# Run Terraform Plan
- name: Terraform Plan
run: |
terraform plan -var="env=${{ matrix.env }}" -out=tfplan-${{ matrix.dir }}-${{ matrix.env }}.out
working-directory: ${{ matrix.dir }}
# Post results to PR
- name: Post Plan Results to PR
uses: borchero/terraform-plan-comment@v2
with:
token: ${{ github.token }}
header: "terraform-plan Dir: ${{ matrix.dir }} (${{ matrix.env }})"
planfile: tfplan-${{ matrix.dir }}-${{ matrix.env }}.out
working-directory: ${{ matrix.dir }}
ref: https://github.com/gonkunkun/terraform-template/blob/main/.github/workflows/terraform-plan.yml
アウトプットのイメージとしては以下の感じです。
planの結果のポストは、以下の記事で解説しています。
Apply
name: Terraform Apply (manual)
on:
push:
branches:
- main
workflow_dispatch:
inputs:
environment:
description: 'Select the environment (e.g., stg, prd)'
required: true
type: choice
options:
- stg
- prd
default: stg
permissions:
contents: read
pull-requests: write
jobs:
pre-plan:
name: Pre-Plan
runs-on: ubuntu-latest
# needs: check_terraform_dirs
if: github.event_name != 'workflow_dispatch'
strategy:
fail-fast: false
matrix:
dir: ["workloads"]
env: [stg, prd]
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.base_ref }}
- name: Setup AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: "ap-northeast-1"
- name: Get Terraform version
id: terraform-version
uses: bigwheel/get-terraform-version-action@v1.2.0
with:
path: ${{ matrix.dir }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ steps.terraform-version.outputs.terraform-version }}
# Cache Terraform plugins
- name: Cache Terraform plugin directory
uses: actions/cache@v3
with:
path: .terraform
key: terraform-plugin-${{ runner.os }}-${{ hashFiles('**/*.tf*') }}
# Run Terraform Init
- name: Terraform Init
run: |
terraform init -backend-config="envs/${{ matrix.env }}/backend.hcl"
working-directory: ${{ matrix.dir }}
# Run Terraform Plan
- name: Terraform Plan
run: |
terraform plan -var="env=${{ matrix.env }}"
working-directory: ${{ matrix.dir }}
apply:
name: Apply to specific env
runs-on: ubuntu-latest
# needs: check_terraform_dirs
if: github.event_name == 'workflow_dispatch'
strategy:
matrix:
dir: ["workloads"]
env: ["${{ github.event.inputs.environment }}"]
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.ref_name }}
- name: Ensure Main is Up-to-Date
run: |
git fetch origin ${{ github.ref_name }}
git diff --quiet origin/${{ github.ref_name }} || (echo "Main has diverged. Please rebase your branch." && exit 1)
- name: Setup AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: "ap-northeast-1"
- name: Get Terraform version
id: terraform-version
uses: bigwheel/get-terraform-version-action@v1.2.0
with:
path: ${{ matrix.dir }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ steps.terraform-version.outputs.terraform-version }}
# Run Terraform Init
- name: Terraform Init - ${{ matrix.env }}
run: |
terraform init -backend-config="envs/${{ matrix.env }}/backend.hcl"
working-directory: ${{ matrix.dir }}
# Run Terraform Plan
- name: Terraform Plan
if: github.event_name == 'workflow_dispatch'
run: |
terraform plan -var="env=${{ matrix.env }}"
sleep 6
working-directory: ${{ matrix.dir }}
# Run Terraform Apply
- name: Terraform Apply - ${{ matrix.env }}
if: github.event_name == 'workflow_dispatch'
run: |
terraform apply -var="env=${{ matrix.env }}" -auto-approve
working-directory: ${{ matrix.dir }}
ref: https://github.com/gonkunkun/terraform-template/blob/main/.github/workflows/terraform-apply.yml
GitHub Actionsから手動でワークフローを実行します。
おわりに
ここまで読んでいただきありがとうございました。
良いTerraformライフを!