tl;dr
- terraform Advent Calendar 2023 の2日目の記事です
- 去年の記事では setup-terraform v2 を使用していましたが、2023年10月に v3 になったので最新化しました(毎年してる)
- hashicorp/setup-terraform の個人用(兼業務用)ワークアラウンドです
- いろいろ削るのが面倒になったのでフルセット載せることにした
- issue や PR のコメントに直接差分を出力するとサイズ制限に引っかかるので GITHUB_STEP_SUMMARY に出力するようにした
注意
- apply はさせてなかったんだけどワークフロー側で選択できるようにした(画面から実行する時にも選択可)
- 一応動いてるけど、無保証です
- コードの手直し、質問等歓迎です
composite action yaml
.github/actions/terraform/action.yml
name: "setup-terraform"
description: 'terraform ci. fmt,init,validate,plan,apply'
# see: https://github.com/hashicorp/setup-terraform
inputs:
terraform_version:
description: 'use terraform version'
required: false
default: latest
terraform_apply:
description: 'run terraform apply'
required: false
default: "no"
# "yes" の場合に apply を実行する
secrets_github_token:
description: 'secrets.GITHUB_TOKEN'
required: true
default: ""
working-directory:
description: 'run directory'
required: true
default: ""
outputs:
plan_diff:
description: "plan has diff"
value: ${{ steps.plan.outputs.exitcode }}
# plan 差分があったら 0 以外の値が返る
runs:
using: "composite"
steps:
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ inputs.terraform_version }}
- id: fmt
run: terraform fmt -check -diff 2>&1 | tee -a terraform_fmt_output.txt
shell: bash
working-directory: ${{ inputs.working-directory }}
continue-on-error: true
# see: https://developer.hashicorp.com/terraform/cli/commands/fmt
- id: init
run: terraform init -no-color 2>&1 | tee -a terraform_init_output.txt
shell: bash
working-directory: ${{ inputs.working-directory }}
continue-on-error: true
# see: https://developer.hashicorp.com/terraform/cli/commands/init
- id: validate
run: terraform validate -no-color 2>&1 | tee -a terraform_validate_output.txt
shell: bash
working-directory: ${{ inputs.working-directory }}
continue-on-error: true
# see: https://developer.hashicorp.com/terraform/cli/commands/validate
- id: plan
run: terraform plan -no-color -detailed-exitcode -input=false 2>&1 | tee -a terraform_plan_output.txt
shell: bash
working-directory: ${{ inputs.working-directory }}
continue-on-error: true
# see: https://developer.hashicorp.com/terraform/cli/commands/plan
- id: apply
if: ${{ inputs.terraform_apply == 'yes' }}
run: terraform apply -no-color -input=false -auto-approve 2>&1 | tee -a terraform_apply_output.txt
shell: bash
working-directory: ${{ inputs.working-directory }}
continue-on-error: true
# see: https://developer.hashicorp.com/terraform/cli/commands/apply
- name: Create Comment
uses: actions/github-script@v6
if: ${{ always() && github.event_name == 'pull_request'}}
with:
github-token: ${{ inputs.secrets_github_token }}
script: |
// 見やすいようにアイコンを表示する
const icon_result_fmt = "${{ steps.fmt.outcome }}" == "success" ? ":blue_heart:" : ":broken_heart:";
const icon_result_init = "${{ steps.init.outcome }}" == "success" ? ":blue_heart:" : ":broken_heart:";
const icon_result_validate = "${{ steps.validate.outcome }}" == "success" ? ":blue_heart:" : ":broken_heart:";
// plan 差分を判断するため exitcode を使用する
const icon_result_plan = "${{ steps.plan.outputs.exitcode }}" == 0 ? ":blue_heart:" : ":broken_heart:";
const plan_exitcode_str = "${{ steps.plan.outputs.exitcode }}" == 0 ? "& No changes" : "but any changes !!";
// apply
let icon_result_apply = '';
if ( "${{ inputs.terraform_apply }}" === 'yes') {
icon_result_apply = "${{ steps.apply.outcome }}" == "success" ? ":blue_heart:" : ":broken_heart:";
}
// PR にコメントする内容
const output = `${{ matrix.dir }} ${ icon_result_fmt } ${ icon_result_init } ${ icon_result_validate } ${ icon_result_plan } ${ icon_result_apply }
plan is ${{ steps.plan.outcome }} ${ plan_exitcode_str }
see [details](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output.substring(0,60000)
})
- name: Create Summary
id: summary
if: always()
shell: bash
run: |
# output to GITHUB_STEP_SUMMARY
echo '<details><summary>terraform fmt is ${{ steps.fmt.outcome }}</summary>' >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
touch terraform_fmt_output.txt
cat terraform_fmt_output.txt | sed '/^::/d' >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
echo '</details>' >> $GITHUB_STEP_SUMMARY
#
echo '<details><summary>terraform init is ${{ steps.init.outcome }}</summary>' >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
touch terraform_init_output.txt
cat terraform_init_output.txt | sed '/^::/d' >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
echo '</details>' >> $GITHUB_STEP_SUMMARY
#
echo '<details><summary>terraform validate is ${{ steps.validate.outcome }}</summary>' >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
touch terraform_validate_output.txt
cat terraform_validate_output.txt | sed '/^::/d' >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
echo '</details>' >> $GITHUB_STEP_SUMMARY
#
if [[ "${{steps.plan.outputs.exitcode}}" == "0" ]]
then
echo '<details><summary>terraform plan is ${{ steps.plan.outcome }}</summary>' >> $GITHUB_STEP_SUMMARY
else
echo '<details><summary>terraform plan is ${{ steps.plan.outcome }} and any changes</summary>' >> $GITHUB_STEP_SUMMARY
fi
echo '' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
touch terraform_plan_output.txt
cat terraform_plan_output.txt | sed '/^::/d' >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
echo '</details>' >> $GITHUB_STEP_SUMMARY
#
if [[ "${{inputs.terraform_apply}}" == "yes" ]]
then
echo '<details><summary>terraform apply is ${{ steps.apply.outcome }}</summary>' >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
touch terraform_apply_output.txt
cat terraform_apply_output.txt | sed '/^::/d' >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
echo '</details>' >> $GITHUB_STEP_SUMMARY
fi
working-directory: ${{ inputs.working-directory }}
- name: terraform plan -json
id: plan_json
# Schedule の時だけ実行する。drift を検知するために json 出力して精査する
if: ${{ github.event_name == 'schedule' }}
run: |
terraform plan -json | tee -a terraform_plan_output.json
cat terraform_plan_output.json | sed '/^::/d' | jq -crR 'fromjson?' > output.json
# count https://developer.hashicorp.com/terraform/internals/machine-readable-ui
echo "errors=$( cat output.json | jq -crR 'fromjson? | select(."@level" == "error")' | jq -s 'length')" >> $GITHUB_OUTPUT
echo "warnings=$(cat output.json | jq -crR 'fromjson? | select(."@level" == "warn")' | jq -s 'length')" >> $GITHUB_OUTPUT
echo "changes=$( cat output.json | jq -crR 'fromjson? | select(.type? == "planned_change")' | jq -s 'length')" >> $GITHUB_OUTPUT
shell: bash
working-directory: ${{ inputs.working-directory }}
continue-on-error: true
- name: Find Issue
id: find_issues
if: ${{ github.event_name != 'pull_request' }}
uses: actions-cool/issues-helper@v3
with:
actions: 'find-issues'
token: ${{ inputs.secrets_github_token }}
issue-creator: 'github-actions[bot]'
issue-state: 'open'
title-includes: "Check: ${{ matrix.dir }} terraform plan diff"
continue-on-error: true
- name: Close issue if no drift
if: steps.find_issues.outputs.issues &&
fromJSON(steps.find_issues.outputs.issues)[0] != null && (
steps.plan_json.outputs.errors == 0 &&
steps.plan_json.outputs.warnings == 0 &&
steps.plan_json.outputs.changes == 0 &&
steps.plan.outputs.exitcode == 0
)
uses: actions-cool/issues-helper@v3
with:
actions: 'close-issue'
token: ${{ inputs.secrets_github_token }}
issue-number: ${{ fromJSON(steps.find_issues.outputs.issues)[0].number }}
close-reason: 'completed'
continue-on-error: true
- name: Create comment add an detected issue
if: steps.find_issues.outputs.issues &&
fromJSON(steps.find_issues.outputs.issues)[0] != null && (
steps.plan_json.outputs.errors != 0 ||
steps.plan_json.outputs.warnings != 0 ||
steps.plan_json.outputs.changes != 0 ||
steps.plan.outputs.exitcode != 0
)
uses: actions-cool/issues-helper@v3
with:
actions: 'create-comment'
token: ${{ inputs.secrets_github_token }}
issue-number: ${{ fromJSON(steps.find_issues.outputs.issues)[0].number }}
body: |
まだ差分のままのようです。
see [details](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
continue-on-error: true
- name: Create issue if drift detected
if: ${{ always() && (
github.event_name != 'pull_request' &&
steps.find_issues.outputs.issues &&
fromJSON(steps.find_issues.outputs.issues)[0] == null && (
steps.plan_json.outputs.errors != 0 ||
steps.plan_json.outputs.warnings != 0 ||
steps.plan_json.outputs.changes != 0 ||
steps.plan.outputs.exitcode != 0
)
) }}
uses: actions-cool/issues-helper@v3
with:
actions: 'create-issue'
token: ${{ inputs.secrets_github_token }}
title: "Check: ${{ matrix.dir }} terraform plan diff"
labels: sre, terraform
body: |
${{ matrix.dir }} で差分があるので確認してください
see [details](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
continue-on-error: true
- name: Job status
uses: actions/github-script@v6
with:
script: |
if ("${{ steps.fmt.outcome }}" != "success") {
core.setFailed('terraform fmt error.');
}
if ("${{ steps.init.outcome }}" != "success") {
core.setFailed('terraform init error.');
}
if ("${{ steps.validate.outcome }}" != "success") {
core.setFailed('terraform validate error.');
}
if ("${{inputs.terraform_apply}}" === "yes") {
if ("${{ steps.apply.outcome }}" === "success") {
// apply が success なら plan 差分はエラーではない(もちろん apply も)
core.notice('OK: terrafom plan has diff. but applied.');
} else {
// apply がエラーだった場合はエラー
core.setFailed('terraform apply error.');
}
} else {
// apply は処理しないケースで plan について
if ("${{steps.plan.outputs.exitcode}}" != "0") {
core.setFailed('terraform plan error.');
}
}
# note:
# - step の continue-on-error: true でステップを継続する。付けてないところでエラーになると後続のステップはスキップされる
# - if: always() でエラーやスキップがあっても常にコメント作成する処理をする
# - createComment の body 部は(ドキュメントに記載はないが)サイズ制限があるので
# 大きな出力結果の時にエラーになるため substring() で抑止
# - 最後の bash でワークフローのエラーを決めている。fmt がエラーでもチェックOKにするなら
# (すべきではないと思うが) exit 1 をコメントにすればよい
# apply を追加したので処理順を変えていることに注意
# - actions/github-script の script 中で環境変数を呼び出した際に、
# 規定以上の長さの文字列だとエラーで落ちてしまうため、処理内容は全て summary で出力することにした
workflow yaml
.github/workflows/pr.yml
name: workflow_test
on:
workflow_dispatch:
inputs:
terraform_apply:
description: 'run terraform apply'
required: true
default: "no"
schedule:
- cron: '0 0 * * MON'
pull_request:
paths:
- ".github/workflows/pr.yml"
- "workflow_test/dir1/*.tf"
- "workflow_test/dir2/*.tf"
jobs:
tf:
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
actions: read
contents: read
id-token: write
issues: write
pull-requests: write
strategy:
fail-fast: false
matrix:
dir:
- "workflow_test/dir1"
- "workflow_test/dir2"
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ap-northeast-1
role-to-assume: ${{ vars.AWS_ROLE_OIDC_ARN }}
- name: terraform
uses: ./.github/actions/terraform
with:
secrets_github_token: ${{ secrets.GITHUB_TOKEN }}
working-directory: ${{ matrix.dir }}
terraform_apply: ${{ inputs.terraform_apply }}
まとめ
- 不自由なく使えている
- 手動実行時にapplyするかを入力できるようにしたおかげで、定期実行時に適用すれば済む程度の差分が出た場合にもしゅっと対応できるようになった
- そういえば AWS への接続を OIDC にしたのでセキュリティ面でも強化された(OIDCのARNは機密情報とは言わないと思うのでvarsを使っているが、ベタ書きするのと未だに迷ってる)
おまけ
- 昨年は composite にしてると dependabot が反応しないからバージョンアップに難がある旨書いたけど解消した
- こんな感じ
.github/dependabot.yml
version: 2
updates:
- package-ecosystem: "github-actions" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/.github/actions/terraform"
schedule:
interval: "weekly"