Renovate と GitHub Actions で Terraform のバージョンアップを自動化する

はじめに

Retty 株式会社でインフラエンジニアをしている幸田です。

IaC (Infrastructure as Code) が一般的になってきた昨今ですが、AWSGCP などクラウドプロバイダーだけでなく、Datadog や PagerDuty など SaaS も管理できるという点で Terraform を採用している会社も多いのではないでしょうか?弊社でも新しく作成するサービスは全て Terraform でコード化しています。

最近社内で CI の改善活動を行っており、その一環で Terraform のバージョンアップを自動化する仕組みを整えたので紹介したいと思います。
今回行った自動化で以下の内容を実現できました。

  • Terraform 本体 / Provider のバージョン自動アップデート
  • tfenv のバージョン自動アップデート
  • .terraform.lock.hcl の自動アップデート
  • バージョンアップ時に差分がないかを PR 上で確認

Terraform のバージョンアップについて

Terraform ではバージョンアップすべきものが大きく2つあります。
1つ目は Terraform そのもののバージョンです。先日ようやく バージョン 1.0 に到達して 正式版となりましたね。この記事を執筆している段階では最新バージョンは v1.0.7 です。

2つ目は Provider のバージョンです。Terraform 本体だけでは各クラウドプロバイダなどの Resource の作成を行うことはできず、各種クラウドプロバイダーや SaaSAPI とやり取りするためのプラグインである Provider と呼ばれるものを追加する必要があります。
AWS などが有名ですが、それ以外にも様々なプロバイダが用意されています。

これら Provider は Terraform 本体のバージョンとは別のサイクルで更新されるため、本体に加えて Provider 側もバージョンアップする必要があります。

どうやってアップデートするか?

Terraform に限らず、各種プログラミング言語やライブラリのアップデートを自動的に行なってくれるツールとして有名なのが Dependabot です。2019年に GitHub によって 買収され、現在は GitHub 公式の機能として提供されています。
Dependabot は指定した周期で自動的に各種ライブラリなどのアップデートを行い、PR を作成してくれます。

dependabot.com

弊社でも言語やライブラリのバージョンアップデートに Dependabot を使用しており、Alpha 版ではありますが Terraform にも対応 しています。

社内での利用実績や GitHub 公式であるという点から、まずは Dependabot を導入してみましたが「Terraform 本体のバージョンアップに対応していない (Provider のバージョンアップのみ)」という点ですぐに乗り換えを検討しました。

次に検討したのが Dependabot と並んで挙げられることの多い Renovate です。こちらも Dependabot と同様に各種アップデートの PR を自動的に作成してくれます。

www.whitesourcesoftware.com

Terraform にも対応 しており、社内でも一部のリポジトリで使用していたため導入してみた所、下記のような点から採用することにしました。

  • Terraform Provider のバージョンアップに対応している (この点は Dependabot も同様)
    • .terraform.lock.hcl の更新も対応
  • Terraform 本体のバージョンアップに対応している
  • tfenv の設定ファイル (.terraform-version) に対応している
  • Dependabot 以上に細かいカスタマイズが可能

Renovate と GitHub Actions の組み合わせについて

Renovate や Dependabot を導入するだけでもある程度は自動化を行うことができますが、少し足りない部分があったため GitHub Actions と組み合わせてアップデートを行っています。

Provider の更新と .terraform.lock.hcl ファイルの更新自動化

Terraform v0.14 から導入されたもので、依存ロックファイルの .terraform.lock.hcl ファイルというものがあります。このファイルには Provider のバージョン情報などが含まれています。 そして リリース当初の記事 にもかかれているように、このファイルはバージョン管理することが推奨されています。

The generated lockfile should be committed into version control systems so that Terraform can guarantee to select exactly the same provider versions on future runs.

Provider のバージョンアップを行えばこのファイルの中身も当然変わる訳ですが、Renovate はこのロックファイルの更新も行ってくれます。

しかしロックファイルには Provider のバージョン情報のほかに、LinuxMac など Terraform を実行するプラットフォームの情報も記載されています。そのため Renovate(linux) によって更新された .terraform.lock.hcl を手元の Mac に pull して terraform init などを実行すると、.terraform.lock.hcl の内容が変わってしまいます。具体的には darwin_amd64 の情報が追加されます。

そうすると結局手元で差分をコミットして再度 push する必要があるため、これを CI で自動化することにしました。

まずは renovate.json で下記のように、Provider が更新される際に Renovate が作成するブランチの名前を変更します。分かりやすいように terraform-provider-version ラベルも付与しています。
(renovate.json に関する設定項目は ドキュメント を参照してください)

{
  "extends": [
    "config:base"
  ],
  "packageRules": [
    {
        "matchPackageNames": ["aws"],
        "addLabels": ["terraform-provider-version"],
        "branchPrefix": "renovate/terraform-provider-version/"
    }
  ]
}

この設定により、Renovate が terraform-provider-aws の PR を作成する際は renovate/terraform-provider-version/aws-3.x のようなブランチ名で PR を作成してくれるようになりました。

次に GitHub Actions 側で、この PR を検知してコミットする設定を行います。GitHub Actions では下記のように if: startsWith() を使用することで、ブランチ名の prefix でワークフローをトリガーすることができます。

name: Terraform Renovate

on:
  pull_request:

jobs:
  terraform-provider-version:
    name: Terraform provider version update
    runs-on: ubuntu-latest
    if:  startsWith(github.head_ref, 'renovate/terraform-provider-version')

    steps:
      - name: Checkout
        uses: actions/checkout@v2
    ...

CI の Terraform バージョンについて

terraform init -upgrade などの操作を実行するため、CI 上に Terraform の実行環境を用意する必要があります。
GitHub Actions では hashicorp/setup-terraform という Action が用意されているため、これを利用することで手軽に Terraform の実行環境を構築することができます。

README にもあるように下記のような記述で任意のバージョンをインストールすることができますが、 Revanote は setup-terraform のバージョンは更新してくれない ため、Revanote が PR に出した tf ファイルのバージョンと、CI 側の実行環境のバージョンに差異が出てしまいます。これでは terraform コマンドを実行することができません。

steps:
- uses: hashicorp/setup-terraform@v1
  with:
    terraform_version: 0.12.25

都合のいいことに Renovate は tfenv の設定ファイルである .terraform-version ファイルの更新も同時に行ってくれるため、これを利用して下記のようなワークアラウンドでこの問題を解決しました。

- name: Detect Terraform version
  run: |
    printf "TF_VERSION=%s" $(cat .terraform-version) >> $GITHUB_ENV

- name: Setup terraform
  uses: hashicorp/setup-terraform@v1
  with:
    terraform_version: ${{ env.TF_VERSION }}

.terraform-version ファイルはバージョン番号のみが記載されたシンプルなファイルであるため、このファイルを cat した内容を環境変数に格納するコマンドを setup-terraform の手前に配置し、setup-terraform ではその環境変数でバージョンを指定することにしました。この方法を使用すると setup-terraform 単体で .terraform-version のバージョンをインストールすることができます。

肝心のロックファイルの更新は次のようにして行いました。Mac の場合は terraform provider lock コマンドに -platform=darwin_amd64 オプションを付与することで、.terraform.lock.hcl にプラットフォームの情報を追加することができます。
ファイルのコミットには EndBug/add-and-commit という Action を使用しました。

- name: Terraform init
  run: |
    terraform init -upgrade
    terraform providers lock -platform=darwin_amd64 -platform=linux_amd64

- name: Commit lock file
  uses: EndBug/add-and-commit@v7
  with:
    add: '.terraform.lock.hcl'
    message: '[GitHub Actions] Add platform darwin_amd64 in terraform.lock.hcl'
    default_author: github_actions

以上で手元の Mac に pull してきても差分がでない .terraform.lock.hcl を自動的に作ることができました!

PR

CI 上での動作確認

Terraform 本体のバージョンアップや .terraform.lock.hcl ファイルの更新まで含めて Provider のバージョンアップを自動化することができましたが、問題なく実行できるか動作確認も行いたくなります。
例えば Provider のアップデートや本体側のアップデートによって、tf ファイルの記述方法が変わった場合は terraform planterraform apply を実行することができません。

バージョンを上げても期待通りに動作することを確認するために、アップデートに加えて terraform plan の実行も自動化し、PR 上で確認できるようにしています。

terraform plan の内容を PR にコメントとして追加する方法はいくつかありますが、弊社では tfcmt を使用しています。 こちらのツールは mercari/tfnotify の fork で、ほぼ設定いらずでいい感じに PR のコメントを作ってくれます。めちゃくちゃ便利…!

github.com

バージョンアップの処理の後に、次のような設定を加えています。

- name: Install tfcmt
  run: |
    sudo curl -fL -o tfcmt.tar.gz https://github.com/suzuki-shunsuke/tfcmt/releases/download/v1.0.0/tfcmt_linux_amd64.tar.gz
    sudo tar -C /usr/bin -xzf ./tfcmt.tar.gz

- name: Terraform plan
  run: |
    if [ -n "$PR_HEAD_SHA" ]; then
      export GITHUB_SHA=$PR_HEAD_SHA
    fi
    tfcmt -owner "RettyInc" -repo ${GITHUB_REPOSITORY#*/} -pr "$PR_NUMBER" plan -- terraform plan
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
    PR_NUMBER: ${{ github.event.number }}

Renovate によって PR が作成されると、それをトリガーにアップデートと terraform plan を実行し、コメントが追加されます。

これで「バージョンアップによるリソースの差分がないこと」「バージョンアップを行っても terraform plan が正常に行えること」を確認できます。

f:id:rettydev:20210918004021p:plain
tfcmt がコメントを残している様子

余談ですが、terraform apply のコメントも残してくれます。便利!

実際に使用している Renovate 用のワークフローファイルである renovate.yml の全体像は下記のような形です。ディレクトリやブランチなどを変更していただくと、そのまま利用できるかと思います。

name: Terraform Renovate

on:
  pull_request:

jobs:
  terraform-version:
    name: Terraform version update
    runs-on: ubuntu-latest
    if: startsWith(github.head_ref, 'renovate/terraform-version')

    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Detect Terraform version
        run: |
          printf "TF_VERSION=%s" $(cat .terraform-version) >> $GITHUB_ENV

      - name: Setup terraform
        uses: hashicorp/setup-terraform@v1
        with:
          terraform_version: ${{ env.TF_VERSION }}

      - name: Install tfcmt
        run: |
          sudo curl -fL -o tfcmt.tar.gz https://github.com/suzuki-shunsuke/tfcmt/releases/download/v1.0.0/tfcmt_linux_amd64.tar.gz
          sudo tar -C /usr/bin -xzf ./tfcmt.tar.gz

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
          role-external-id: ${{ secrets.AWS_ROLE_EXTERNAL_ID }}
          role-duration-seconds: 1200
          role-session-name: github-actions
          aws-region: ap-northeast-1

      - name: Decrypt terraform.tfvars.encrypt
        run: ./encrypt_decrypt.sh decrypt
        env:
          KMS_KEY_ID: ${{ secrets.KMS_KEY_ID }}
          AWS_REGION: ap-northeast-1

      - name: Terraform init
        run: |
          terraform init

      - name: Terraform plan
        run: |
          if [ -n "$PR_HEAD_SHA" ]; then
            export GITHUB_SHA=$PR_HEAD_SHA
          fi
          tfcmt -owner "RettyInc" -repo ${GITHUB_REPOSITORY#*/} -pr "$PR_NUMBER" plan -- terraform plan
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
          PR_NUMBER: ${{ github.event.number }}

  terraform-provider-version:
    name: Terraform provider version
    runs-on: ubuntu-latest
    if: startsWith(github.head_ref, 'renovate/terraform-provider-version')

    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Detect Terraform version
        run: |
          printf "TF_VERSION=%s" $(cat .terraform-version) >> $GITHUB_ENV

      - name: Setup terraform
        uses: hashicorp/setup-terraform@v1
        with:
          terraform_version: ${{ env.TF_VERSION }}

      - name: Install tfcmt
        run: |
          sudo curl -fL -o tfcmt.tar.gz https://github.com/suzuki-shunsuke/tfcmt/releases/download/v1.0.0/tfcmt_linux_amd64.tar.gz
          sudo tar -C /usr/bin -xzf ./tfcmt.tar.gz

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
          role-external-id: ${{ secrets.AWS_ROLE_EXTERNAL_ID }}
          role-duration-seconds: 1200
          role-session-name: github-actions
          aws-region: ap-northeast-1

      - name: Decrypt terraform.tfvars.encrypt
        run: ./encrypt_decrypt.sh decrypt
        env:
          KMS_KEY_ID: ${{ secrets.KMS_KEY_ID }}
          AWS_REGION: ap-northeast-1

      - name: Terraform init
        run: |
          terraform init -upgrade
          terraform providers lock -platform=darwin_amd64 -platform=linux_amd64

      - name: Commit lock file
        uses: EndBug/add-and-commit@v7
        with:
          add: '.terraform.lock.hcl'
          message: '[GitHub Actions] Add platform darwin_amd64 in terraform.lock.hcl'
          default_author: github_actions

      - name: Terraform plan
        run: |
          if [ -n "$PR_HEAD_SHA" ]; then
            export GITHUB_SHA=$PR_HEAD_SHA
          fi
          tfcmt -owner "RettyInc" -repo ${GITHUB_REPOSITORY#*/} -pr "$PR_NUMBER" plan -- terraform plan
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
          PR_NUMBER: ${{ github.event.number }}

終わりに

Renovate と GitHub Actions を使うことで、滞りがちな Terraform のバージョンアップを自動化することができました。
今後も Terraform の利用頻度は増えていくと思うので、このような整備を続けていきたいと思います。