はじめに
Retty 株式会社でインフラエンジニアをしている幸田です。
IaC (Infrastructure as Code) が一般的になってきた昨今ですが、AWS や GCP などクラウドプロバイダーだけでなく、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 の作成を行うことはできず、各種クラウドプロバイダーや SaaS の API とやり取りするためのプラグインである Provider と呼ばれるものを追加する必要があります。
AWS などが有名ですが、それ以外にも様々なプロバイダが用意されています。
これら Provider は Terraform 本体のバージョンとは別のサイクルで更新されるため、本体に加えて Provider 側もバージョンアップする必要があります。
どうやってアップデートするか?
Terraform に限らず、各種プログラミング言語やライブラリのアップデートを自動的に行なってくれるツールとして有名なのが Dependabot です。2019年に GitHub によって 買収され、現在は GitHub 公式の機能として提供されています。
Dependabot は指定した周期で自動的に各種ライブラリなどのアップデートを行い、PR を作成してくれます。
弊社でも言語やライブラリのバージョンアップデートに Dependabot を使用しており、Alpha 版ではありますが Terraform にも対応 しています。
社内での利用実績や GitHub 公式であるという点から、まずは Dependabot を導入してみましたが「Terraform 本体のバージョンアップに対応していない (Provider のバージョンアップのみ)」という点ですぐに乗り換えを検討しました。
次に検討したのが Dependabot と並んで挙げられることの多い Renovate です。こちらも Dependabot と同様に各種アップデートの PR を自動的に作成してくれます。
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 のバージョン情報のほかに、Linux や Mac など 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
を自動的に作ることができました!
CI 上での動作確認
Terraform 本体のバージョンアップや .terraform.lock.hcl
ファイルの更新まで含めて Provider のバージョンアップを自動化することができましたが、問題なく実行できるか動作確認も行いたくなります。
例えば Provider のアップデートや本体側のアップデートによって、tf ファイルの記述方法が変わった場合は terraform plan
や terraform apply
を実行することができません。
バージョンを上げても期待通りに動作することを確認するために、アップデートに加えて terraform plan
の実行も自動化し、PR 上で確認できるようにしています。
terraform plan
の内容を PR にコメントとして追加する方法はいくつかありますが、弊社では tfcmt を使用しています。
こちらのツールは mercari/tfnotify の fork で、ほぼ設定いらずでいい感じに PR のコメントを作ってくれます。めちゃくちゃ便利…!
バージョンアップの処理の後に、次のような設定を加えています。
- 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
が正常に行えること」を確認できます。
余談ですが、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 の利用頻度は増えていくと思うので、このような整備を続けていきたいと思います。