[メモ]GitHub Actionsでterraform plan & applyする
Github Actionsを使ってPRベースでterraform planを検証し、masterマージされた定義をterraform applyでインフラに適用する。
最終的なyaml
name: "terraform"
on:
push:
branches:
- master
pull_request:
types: [opened, synchronize, reopened]
env:
GOOGLE_APPLICATION_CREDENTIALS_KEY: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS_KEY }}
GOOGLE_APPLICATION_CREDENTIALS: /tmp/credentials.json
jobs:
terraform:
name: "terraform"
runs-on: "ubuntu-latest"
env:
GOOGLE_APPLICATION_CREDENTIALS: /tmp/credentials.json
steps:
- name: checkout
uses: actions/checkout@v2
- name: setup terraform
uses: hashicorp/setup-terraform@v1
- name: copy credentials
run: echo ${GOOGLE_APPLICATION_CREDENTIALS_KEY} > ${GOOGLE_APPLICATION_CREDENTIALS}
- name: init
id: init
run: terraform init
- name: validate
id: validate
run: terraform validate -no-color
continue-on-error: true
- name: format check
id: fmt
run: terraform fmt -check -diff
continue-on-error: true
- name: plan
id: plan
run: terraform plan -no-color
continue-on-error: true
- name: remove old comment
id: remove_old_comment
uses: actions/github-script@0.9.0
if: github.event_name == 'pull_request'
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
opts = github.issues.listComments.endpoint.merge({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
per_page: 100,
})
const comments = await github.paginate(opts)
for(const comment of comments) {
if (comment.user.login === "github-actions[bot]" && comment.body.startsWith("#### Terraform Format and Style")) {
github.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: comment.id,
})
}
}
- name: show plan
uses: actions/github-script@0.9.0
if: github.event_name == 'pull_request'
env:
PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const output = `#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\`
#### Terraform Initialization: \`${{ steps.init.outcome }}\`
#### Terraform Validation: ${{ steps.validate.outputs.stdout }}
#### Terraform Format: ${{ steps.fmt.outputs.stdout }}
#### Terraform Plan: \`${{ steps.plan.outcome }}\`
<details><summary>Show Plan</summary>
\`\`\`${process.env.PLAN}\`\`\`
</details>
*Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`, Workflow: \`${{ github.workflow }}\`*`;
github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
})
- name: check on failure
if: steps.validate.outcome == 'failure' || steps.fmt.outcome == 'failure' || steps.plan.outcome == 'failure'
run: |
echo ${{ steps.validate.outputs.status }}
echo ${{ steps.fmt.outputs.status }}
echo ${{ steps.plan.outputs.status }}
exit 1
- name: apply
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
run: terraform apply -auto-approve
説明
Actionを発火させるイベント
on:
push:
branches:
- master
pull_request:
types: [opened, synchronize, reopened]
masterにマージされた時にterraform applyしたいのでmasterに対するpushイベントを、 PRが作成、更新されたterraform planしたいのでpull_requestのopen/synchronize/reopenedイベントを対象にする(実際のところpull_requestとだけ指定すればこの3つを対象にしたことになる) pull_requestのsynchronizeはPRのソースブランチが更新されたときに発行される。
credential
env:
GOOGLE_APPLICATION_CREDENTIALS_KEY: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS_KEY }}
GOOGLE_APPLICATION_CREDENTIALS: /tmp/credentials.json
Secretsに予めservice accountのjsonを登録しておく。
GOOGLE_APPLICATION_CREDENTIALS
を定義しておくとterraformがそれを読み取ってくれるので今回はこの方法でcredentialを渡すことにした。
workflowの説明
checkoutまで
jobs:
terraform:
name: "terraform"
runs-on: "ubuntu-latest"
env:
GOOGLE_APPLICATION_CREDENTIALS: /tmp/credentials.json
steps:
- name: checkout
uses: actions/checkout@v2
ここまではテンプレ
terraformの導入
- name: setup terraform
uses: hashicorp/setup-terraform@v1
Hashicorp公式のactionがあるのでこれを使う。 terraformのversion指定などもできるので必要なら設定する。READMEに使い方は大体書いてある。
credentialの書き出し
- name: copy credentials
run: echo ${GOOGLE_APPLICATION_CREDENTIALS_KEY} > ${GOOGLE_APPLICATION_CREDENTIALS}
GOOGLE_APPLICATION_CREDENTIALS
で指定したパスにservice accountキーを書き出しておく。
terraformの各種チェックとplanの実行
- name: init
id: init
run: terraform init
- name: validate
id: validate
run: terraform validate -no-color
continue-on-error: true
- name: format check
id: fmt
run: terraform fmt -check -diff
continue-on-error: true
- name: plan
id: plan
run: terraform plan -no-color
continue-on-error: true
init/validate/fmt/planをそれぞれ実行して結果を収集する。
validate/planで -no-color
をつけ忘れると出力にカラーコードが入り込むので注意。
continue-on-error: true
としているのは各種チェックの結果をPRにコメントとして書き出す処理を行うため。
この記述を省くといずれかのチェックで失敗するとそこで中断する。
コメントを書き出す必要がなければそうしてしまえば良い。
古いコメントの削除
- name: remove old comment
id: remove_old_comment
uses: actions/github-script@0.9.0
if: github.event_name == 'pull_request'
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
opts = github.issues.listComments.endpoint.merge({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
per_page: 100,
})
const comments = await github.paginate(opts)
for(const comment of comments) {
if (comment.user.login === "github-actions[bot]" && comment.body.startsWith("#### Terraform Format and Style")) {
github.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: comment.id,
})
}
}
このactionを複数回実行すると都度コメントが増えてしまうので、 github-scriptを用いて古いコメントを削除する。 通常そんなにたくさんコメントが付くことは無いと思うが、 念の為ページネーションして全部のコメントを取り出して、github actionsからつけられたコメントかつ本文がこのactionで付与したものと類似するものを削除している。
github-scriptの使い方はこのあたりを参考にした。
- https://github.com/actions/github-script#welcome-a-first-time-contributor
- https://octokit.github.io/rest.js/
- https://maku.blog/p/7r6gr3d/
チェック、planの結果をコメントとして書き出す
- name: show plan
uses: actions/github-script@0.9.0
if: github.event_name == 'pull_request'
env:
PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const output = `#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\`
#### Terraform Initialization: \`${{ steps.init.outcome }}\`
#### Terraform Validation: ${{ steps.validate.outputs.stdout }}
#### Terraform Format: ${{ steps.fmt.outputs.stdout }}
#### Terraform Plan: \`${{ steps.plan.outcome }}\`
<details><summary>Show Plan</summary>
\`\`\`${process.env.PLAN}\`\`\`
</details>
*Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`, Workflow: \`${{ github.workflow }}\`*`;
github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
})
適当なフォーマットでPRにコメントとして結果を書き出す。
当然ながら書き出す先のPRが無いと動かないので、if
でevent_nameをチェックしてpull_requestだけを対象にする。
チェック、planに不備がある場合はエラー終了させる
- name: check on failure
if: steps.validate.outcome == 'failure' || steps.fmt.outcome == 'failure' || steps.plan.outcome == 'failure'
run: exit 1
不備がある場合はマージできないようにしたいので、終了ステータスをそれぞれチェックしてどれか一つでも失敗していたらエラー終了させる。
terraform applyする
- name: apply
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
run: terraform apply -auto-approve
無事にmasterにマージされたらapplyして変更内容をインフラに反映させる。
実行確認が出ないように -auto-approve
をつけて実行する。