100 以上の Terraform 環境をいい感じに v0.14 に upgrade した方法

こんにちは。 SRE の @suzuki-shunsuke です。

100 個以上の Terraform state がある Monorepo で Terraform を v0.14 に upgrade しつつ Terraform Provider の自動 update を実現した方法を紹介したいと思います。 Terraform v0.14 の新機能とかにはあまり触れず、 upgrade するために何をしたかという点にフォーカスします。 Terraform 公式のドキュメントも参照してください。

なお、弊社の Terraform の CI/CD に関しては下記の記事も参照してください。

quipper.hatenablog.com quipper.hatenablog.com

Terraform v0.13 から v0.14 への update では、 Terraform のコード(*.tf)の変更はほとんど必要ありませんでした。 terraform 0.14upgrade のようなコマンドも提供されていません。 ただし、 lock ファイル .terraform.lock.hcl が導入されています。 そのため、 terraform init を実行し、 lock ファイルを生成する必要があります。

また、幾つか deprecation warning にも対応しました。 例えば、 provider configuration block の中で provider のバージョンを指定しないようにする必要がありました。

provider "aws" {
  region      = "ap-northeast-1"
  max_retries = 3
  version     = "3.31.0"
}
Warning: Version constraints inside provider configuration blocks are deprecated

  on provider.tf line 4, in provider "aws":
   4:   version     = "3.31.0"

Terraform 0.13 and earlier allowed provider version constraints inside the
provider configuration block, but that is now deprecated and will be removed
in a future version of Terraform. To silence this warning, move the provider
version constraint into the required_providers block.

代わりに required_providers で指定するようにしました。

terraform {
  required_version = ">= 0.14"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "3.31.0"
    }
  }
}

弊社の場合 state が 110 個程度あり、全部手作業で対応するのは辛いので、簡単なスクリプトを書いて terraform init の実行を自動化しました。

まず対象の state のパスのリストを 10 個ずつ程度にファイルに分けておきます。 そのファイルを list.txt とします。

予め terraform.tfstate*, .terraform を消しておきます。

$ cat list.txt | xargs -I{} rm -Rf {}/terraform.tfstate* {}/.terraform

PR 用に default branch から branch を予め切っておきます。 そしてスクリプトを実行してコミットを追加していきます。

#!/usr/bin/env bash

set -eux
set -o pipefail

while read -r target; do
  cp provider.tf versions.tf .terraform-version "$target"
  pushd "$target"
  terraform init
  git add .
  git commit -m "chore: update Terraform of $target"
  popd
done < list.txt

あとは PR を投げるだけです。これを 10 回くらい繰り返しました。

Renovate による Terraform Provider update の際に lock ファイルの更新とマージを自動化

Renovate の Tips - Quipper Product Team Blog でも紹介したように、 Terraform Provider の update を Renovate で行い、 terraform plan の実行結果が no change であれば Renovate によって automerge され、 そうでなければ CI を失敗させるようにしています。

しかし lock ファイルが導入されたことにより、 lock ファイルも更新しないといけなくなりました。 required_providers のバージョン指定だけ更新して terraform init すると失敗します。

$ terraform init

Error: Failed to query available provider packages

Could not retrieve the list of available versions for provider hashicorp/aws:
locked provider registry.terraform.io/hashicorp/aws 3.28.0 does not match
configured version constraint 3.29.0; must use terraform init -upgrade to
allow selection of new versions

Go module を Renovate で update する場合、 go mod tidy を自動実行できますが *1、 Terraform の lock ファイルはそうもいきません。 Renovate をセルフホストしていれば postUpgradeTasks を設定して出来るかもしれませんが、 自分たちはセルフホストしていないのでそれも出来ません。

そこで Renovate で Terraform provider が update された場合、 CI で自動的に terraform init -upgrade を実行し、更新された lock ファイルをブランチに push するようにしました。 Renovate では PR の label にテンプレート変数が使えます*2

https://docs.renovatebot.com/templates/

そこで次のような label を設定します。

  "labels": [
    "datasource:{{datasource}}"
  ]

Terraform Provider が update されると datasource:terraform-provider という label がセットされます。

自動で terraform init -upgrade を実行するコードは次のようになります。 以下のツールを使っています

root_dir=$PWD
cd "$STATE_DIR"

if [ "$CI_INFO_PR_AUTHOR" = "renovate[bot]" ] && grep "datasource:terraform-provider" "$CI_INFO_TEMP_DIR/labels.txt"; then
  if ! terraform init -input=false; then
    # we have to run `terraform init -upgrade` to update `.terraform.lock.hcl`
    terraform init -input=false -upgrade
    ghcp commit -u "$CI_INFO_REPO_OWNER" -r "$CI_INFO_REPO_NAME" -b "$CI_INFO_HEAD_REF" \
      -m "chore(terraform-provider): terraform init -upgrade" \
      -C "$root_dir" "$STATE_DIR/.terraform.lock.hcl"
    exit 1
  fi
else
  terraform init -input=false
fi

GitHub の automerge 機能を活用

こうすることで自動で lock ファイルを更新することが出来るようになりましたが、まだ問題があります。 Renovate が作成した PR に対してコミットを追加すると、 Renovate の automerge が有効になっていても、自動でマージしてくれなくなります。 これはドキュメントを読んでもそのような記述は見られないので自分の勘違いかもしれませんが、そのように思われます。

そこで、 terraform plan の結果が no change であれば自動でマージされるようにしました。 GitHub の automerge 機能を使います。 リポジトリの設定で Allow auto-merge を有効化する必要があります。

image

GitHub CLIgh pr merge コマンドを使い automerge を有効化することで、 CI が終わったら勝手に merge されます。 --auto をつけずに即マージしようとすると、 Required status check が pending の場合に失敗するので、 --auto を指定しています。 弊社の設定の場合、 merge には 1 approve 必要ですが、そこは renovate-approve が approve してくれるので問題ありません。

automerge を有効化する際には、安全な場合のみ automerge するように if 文で制限しています。

if [ "$CI_INFO_PR_MERGED" = "false" ] && [ "$CI_INFO_PR_AUTHOR" = "renovate[bot]" ] && [ "$(bash ci/list-all-updated-states.sh | wc -l)" -eq 1 ]; then
  echo "===> This pull request would be merged automatically" >&2
  # --auto option is needed because if other platform's CI has not been finished it would fail to merge the pull request.
  # > Required status check "AWS CodeBuild ap-northeast-1 (sapuri-terraform)" is pending.
  gh pr merge --merge --auto "$CI_INFO_PR_NUMBER" || :
fi
  • PR がマージされていない
  • PR の author が Renovate
  • PR で対象になっている state が 1 つだけ
    • Monorepo になっており複数の state の CI が同時に実行されうるので、 1 つだけの場合のみ automerge
    • 基本的に Renovate の PR では複数の state の CI は同時に走らないようになっているので、この制約があっても問題にならない
  • plan の結果が no change (これは if 文に反映されていませんが、そもそも no change でなければここに来ないようになっている)

こうすることで、 Renovate の PR にコミットを追加している場合でも安全に自動で PR をマージできるようになりました。 Renovate の automerge 機能はマージされるまでに数時間かかることもよくあるのが不満だったのですが、このやり方だと即時にマージされるため、とても快適になりました。

最後に

以上、 Terraform を v0.14 に upgrade するさいに何をしたか説明しました。 特に Renovate を使って .terraform.lock.hcl を自動で更新するやり方や、 GitHub の automerge 機能を活用して自動マージするやり方は参考になればと思います。

*1:postUpdateOptionsを参照

*2:使えるようにしたのは自分です https://github.com/renovatebot/renovate/pull/8138