Skip to main content

常にいまいち

[メモ]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の使い方はこのあたりを参考にした。

チェック、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 をつけて実行する。