クラウドワークス エンジニアブログ

日本最大級のクラウドソーシング「クラウドワークス」の開発の裏側をお届けするエンジニアブログ

tfupdateで複数の.terraform.lock.hclを高速に一括更新する

はじめに

Terraform職人の@minamijoyoです。先日、tfupdateが.terraform.lock.hclの更新に対応しました。v0.7.0から tfupdate lock というコマンドが追加されています。

github.com

例えば、あるディレクトリ配下のすべてのAWSプロバイダを指定バージョンに更新しつつ、複数プラットフォーム混在で使う.terraform.lock.hclもまとめて一括更新するには、以下のようなコマンドで簡単にできるようになりました。

$ tfupdate provider aws -v 5.7.0 -r ./
$ tfupdate lock --platform=linux_amd64 \
                --platform=darwin_amd64 \
                --platform=darwin_arm64 \
                -r ./

内部的にterraformコマンドには依存せず、バージョン指定の検出からプロバイダのダウンロード、ハッシュ値の計算なども自前で実装したことで、terraform initを省略したり、計算結果をキャッシュすることが可能になりました。これにより、複数のディレクトリに散らばった.terraform.lock.hclを一括更新するようなユースケースにおいて、大幅にパフォーマンスが改善します。もちろん環境に依存しますが、数万行あるcrowdworks.jpのTerraform設定のリポジトリでは、バージョンアップ時のロックファイルの更新処理が300倍高速化しました。

この記事では、.terraform.lock.hclとは何かについておさらいした後、これまでの技術的な課題とその解決策について説明します。また、実装する上で調べた.terraform.lock.hclに記録されているハッシュ値の計算方法についても解説するので、この記事を読めばあなたも「.terraform.lock.hcl完全に理解した」と言えるでしょう。

本稿執筆時点でのTerraformのバージョンはv1.5.2で、tfupdateはv0.7.2です。最新の情報はGitHubリポジトリを参照してください。

.terraform.lock.hclとは

.terraform.lock.hclとは、Terraform v0.14から導入されたプロバイダの依存のロックファイルです。

Dependency Lock File (.terraform.lock.hcl) - Configuration Language | Terraform | HashiCorp Developer

rootモジュールが要求するバージョン制約を解決した結果、依存するプロバイダのバージョン、制約条件、およびそのハッシュ値が記録されています。以下は、.terraform.lock.hclの例です。

# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.

provider "registry.terraform.io/hashicorp/null" {
  version     = "3.2.1"
  constraints = "3.2.1"
  hashes = [
    "h1:FbGfc+muBsC17Ohy5g806iuI1hQc4SIexpYCrQHQd8w=",
    "h1:tSj1mL6OQ8ILGqR2mDu7OYYYWf+hoir0pf9KAQ8IzO8=",
    "h1:ydA0/SNRVB1o95btfshvYsmxA+jZFRZcvKzZSB+4S1M=",
    "zh:58ed64389620cc7b82f01332e27723856422820cfd302e304b5f6c3436fb9840",
    "zh:62a5cc82c3b2ddef7ef3a6f2fedb7b9b3deff4ab7b414938b08e51d6e8be87cb",
    "zh:63cff4de03af983175a7e37e52d4bd89d990be256b16b5c7f919aff5ad485aa5",
    "zh:74cb22c6700e48486b7cabefa10b33b801dfcab56f1a6ac9b6624531f3d36ea3",
    "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
    "zh:79e553aff77f1cfa9012a2218b8238dd672ea5e1b2924775ac9ac24d2a75c238",
    "zh:a1e06ddda0b5ac48f7e7c7d59e1ab5a4073bbcf876c73c0299e4610ed53859dc",
    "zh:c37a97090f1a82222925d45d84483b2aa702ef7ab66532af6cbcfb567818b970",
    "zh:e4453fbebf90c53ca3323a92e7ca0f9961427d2f0ce0d2b65523cc04d5d999c2",
    "zh:e80a746921946d8b6761e77305b752ad188da60688cfd2059322875d363be5f5",
    "zh:fbdb892d9822ed0e4cb60f2fedbdbb556e4da0d88d3b942ae963ed6ff091e48f",
    "zh:fca01a623d90d0cad0843102f9b8b9fe0d3ff8244593bd817f126582b52dd694",
  ]
}

このロックファイルは、rootモジュール(=ディレクトリ)単位で生成されます。Terraformの公式では、.terraform.lock.hclをGitにコミットすることを推奨していますが、他のプログラミング言語の依存ライブラリのロックファイルと同様に扱うと、意図せぬ動作をすることがあります。

というのも、プロバイダのバイナリはlinux_amd64のようなプラットフォーム(OSとCPUアーキテクチャの組み合わせ)ごとに異なるため、このハッシュ値もプラットフォームごとに異なります。ただロックファイル上はハッシュ値だけ見ても、どのプラットフォームと対応しているかは識別できません。また、ハッシュ値にはzipアーカイブの状態のzhと、展開後のh1の2種類があります。zhは初回のterraform initのタイミングでTerraform Registryから全プラットフォーム分取得できますが、h1は実行したプラットフォームのもののみが記録されます。あらかじめ必要なプラットフォーム分のハッシュ値を記録するには、terraform providers lockコマンドを使用します。

Command: providers lock | Terraform | HashiCorp Developer

マルチプラットフォーム混在環境では、.terraform.lock.hclを正しく管理していないと、環境によってチェックサムミスマッチエラーになったり、ハッシュ値が追記されて意図しないタイミングでgit diffが出てしまったりすることがあります。以前は手元がmacOSだが、CIはLinuxという場合にはハマりポイントだったのですが、最近はIntel macとM1/M2 macの混在により、CIでの自動化まで整備できていない開発チームでもつまづきポイントになりました。

ハッシュ値が2種類ある理由は歴史的な経緯によるものですが、話し始めると長くなるので、Terraform v0.14当時に書いた解説を以下に置いておきます。興味がある人はこちらをどうぞ。

speakerdeck.com

tfupdateとは

tfupdateとは、Terraformの本体/プロバイダ/モジュールのバージョン制約を一括で書き換えるツールです。

github.com

terraform planの再現性を担保するためには、依存するプロバイダなどのバージョンを固定する必要があります。しかし、最新版から離れすぎると、バージョンアップしようとしたときに変更差分が多くなり、追いつくのが大変です。そのため、依存関係を頻繁にバージョンアップすることがベストプラクティスです。バージョン番号を書き換える作業は、ディレクトリの数が少なければ簡単ですが、ディレクトリ数が多くなると煩雑になります。tfupdateは、指定したディレクトリ配下を再帰的にパースして、バージョン制約を一括で書き換えることができます。

例えば以下のような設定がmain.tfにあるとして、

terraform {
  required_version = "1.5.2"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.6.0"
    }
  }
}

以下のコマンドでawsプロバイダをv5.7.0に更新できます。

$ tfupdate provider aws -v 5.7.0 main.tf
terraform {
  required_version = "1.5.2"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.7.0"
    }
  }
}

非常に単純ですが、HCLの構文をパースして書き換えています。また -r オプションで指定のディレクトリ配下を再帰的に一括で書き換えることも可能です。これをfind/xargs/grep/sedなどのシェル芸で実装しようとすると、複数行にまたがるコンテキストを正規表現でマッチさせる必要があり、エッジケースなども考慮すると、かなり面倒くさそうなのは容易に想像できるでしょう。

またtfupdateには、GitHub ReleaseやTerraform Registryなどから最新版のバージョン番号を取得する機能もあるので、これをCIに組み込んで定期的に実行することで、最新版が出たら自動でPullRequestを作成し、CIでplan差分もチェックするというワークフローが自由に作り込めます。あとは人間がCHANGELOGを読んでマージするだけで、常に最新版が使えるという、控えめに言って最高の開発者体験が実現できます。

crowdworks.jpのインフラはAWSで運用されており、インフラの管理に長年Terraformを使っています。Terraformの設定は数万行、ディレクトリ数で300個ぐらいの規模感で運用されていますが、tfupdateを使ってバージョンアップ作業を自動化することで、本稿執筆時点で最新のTerraform v1.5系&AWSプロバイダv5系で運用されており、この規模でも新機能がいつでも試せます。福利厚生です。

同じようなことは DependabotRenovate のようなSaaSでも実現できますが、tfupdateはHCLを書き換えるだけの単機能なCLIツールで、GitHub ActionsやCircleCIなど好きなCI/CDツールに組み込んでワークフローを自由にカスタマイズすることができます。もちろんCI/CDに組み込んで自動化までしなくても、単に手元のローカル環境で使うだけでも十分便利です。歴史的には、tfupdateはDependabotやRenovateがTerraformに対応する以前、Terraform v0.12の時代に書かれましたが、今でも単機能なCLIツールとして、Terraform職人の皆さんに一定の需要があるようです。

そんな便利なtfupdateですが、これまで Terraform v0.14で追加された .terraform.lock.hcl の更新には直接対応しておらず、公式のterraform providers lockコマンドを使って更新することを推奨していました。一方、DependabotやRenovateは.terraform.lock.hcl更新に対応しており、SaaSだとハッシュ値をサーバサイドでキャッシュできてずるいよなぁなどと思いつつ、個人的な懸念点はロックファイルのフォーマットがTerraformの実装詳細なことでした。公式のterraformコマンドでできるものを、あえてtfupdate内に再実装すると、複数のTerraformバージョンをサポートするコストが高く、将来的なメンテナンスの負荷を増やしそうなことは容易に想像できたので、これまで意図的に避けていました。

複数の.terraform.lock.hclを更新する場合の問題点

terraformコマンドは同時に1つのディレクトリしか操作できませんが、現実的にTerraformでインフラを管理しようとすると、1つのディレクトリで完結することはないでしょう。環境ごとにディレクトリを分けたり、論理的なコンポーネント単位でディレクトリを分けたりするのが普通です。またチームで開発する場合、複数のプラットフォームが混在することも普通にあります。

前述の通り、複数のプラットフォーム用の.terraform.lock.hclを生成するには、terraform providers lockコマンドを使用しますが、このコマンドはプロバイダのキャッシュを意図的に無視します。これはバグではなく仕様です。そのため、複数のディレクトリで実行すると都度ダウンロードが発生してしまい冗長です。無駄なダウンロードを回避するには、terraform providers mirrorコマンドを使用して、ローカルのミラーを作成します。

Command: providers mirror | Terraform | HashiCorp Developer

その他の注意点として、プロバイダの依存はモジュールを介して間接的に追加される可能性があるので、あらかじめterraform initが必要です。また、Terraformのバージョンによっては、.terraform.lock.hclを更新する際にチェックサムミスマッチエラーが発生することがあります。エラーの発生有無はTerraformのバージョンによって微妙に挙動が異なります。

これらを考慮して、最終的に完成した「ぼくのかんがえたさいきょうのtflock_generate.sh」は以下の通りです。

#!/bin/bash
set -eo pipefail

# create a plugin cache dir
export TF_PLUGIN_CACHE_DIR="/tmp/terraform.d/plugin-cache"
mkdir -p "${TF_PLUGIN_CACHE_DIR}"

# remove an old lock file before providers mirror command
rm -f .terraform.lock.hcl

# create a local filesystem mirror to avoid duplicate downloads
FS_MIRROR="/tmp/terraform.d/plugins"
terraform providers mirror -platform=linux_amd64 -platform=darwin_amd64 -platform=darwin_arm64 "${FS_MIRROR}"

# update the lock file
ALL_DIRS=$(find . -type f -name '*.tf' | xargs -I {} dirname {} | sort | uniq | grep -v 'modules/')
for dir in ${ALL_DIRS}
do
  pushd "$dir"
  # always create a new lock to avoid duplicate downloads by terraoform init -upgrade
  rm -f .terraform.lock.hcl
  # get modules to detect provider dependencies inside module
  terraform init -input=false -no-color -backend=false -plugin-dir="${FS_MIRROR}"
  # remove a temporary lock file to avoid a checksum mismatch error
  rm -f .terraform.lock.hcl
  # generate h1 hashes for all platforms you need
  # recording zh hashes requires to download from origin, so we intentionally ignore them.
  terraform providers lock -fs-mirror="${FS_MIRROR}" -platform=linux_amd64 -platform=darwin_amd64 -platform=darwin_arm64
  # clean up
  rm -rf .terraform
  popd
done

これまで上記のようなスクリプトをtfupdateと一緒に実行することで、.terraform.lock.hclも更新していました。ただ、本来やりたかったことに対して必要以上に複雑なのは否めません。

プロバイダの依存はディレクトリごとにそんなに変わらないので、1箇所で全部入りのロックファイルを作ってコピーして配ればよいのではないかと思う人もいるかもしれません。しかしながらその戦略はロックファイルがプロバイダだけではなく、将来的にモジュールの依存も管理するようになると破綻するので、個人的には筋が悪そうと思っています。Terraformのソースを読めば分かりますが、ロックファイルをパースする箇所でmoduleというキーワードが将来のために予約されています。そしてモジュールの依存はディレクトリごとに異なるのが普通です。

https://github.com/hashicorp/terraform/blob/v1.5.2/internal/depsfile/locks_file.go#L212-L215

.terraform.lock.hclを完全に理解した結果、必要以上の複雑さは、そもそもTerraform Registryがh1ハッシュを返してくれないのが諸悪の根源であるのは明らかだったので、Terraform v0.14当時に以下のissueで、Terraform Registryのプロトコル変更を提案しました。

github.com

というのがもう2年以上前の話です。時は流れ、状況は日に日に悪化していきました。

Terraform Registryのプロトコル改善の進展がないまま、Terraform 1.4から.terraform.lock.hclの管理が実質的に必須となり、最終手段として.gitignoreに追加するという逃げ道が塞がれました。公式からは大きくアナウンスされていないので、まだTerraform v1.4での挙動の変更に気づいてない人も多いかもしれませんが、.terraform.lock.hclがないとプロバイダのキャッシュが効かなくなりました。詳細は以下にまとめています。

qiita.com

一方、M1 Macの登場により、ハッシュ値を計算すべきプラットフォームが増えたり、管理するインフラの規模拡大に伴いディレクトリ数は増える一方で、必要な計算量は増加し続けています。

AWSプロバイダは毎週新しいバージョンがリリースされますが、バージョンアップ時には、crowdworks.jpでは3つのプラットフォームx約300ディレクトリの.terraform.lock.hclを更新する必要があり、CIでのロックファイルの更新処理時間がいつのまにか1時間20分もかかるようになってしまいました。terraform plan時間を含まずにロックファイルの更新のみでこんなに時間がかかるのは、さすがにどうかと思いますし、なにかが間違っています。

観察したところ、どうやら、プロバイダのミラーを作って冗長なダウンロードを回避しても、依然としてzipファイルの解凍やハッシュ値計算はディレクトリごとに行われているようです。AWSプロバイダはzip圧縮状態でおよそ80MB、解凍すると400MBほどのファイルであり、これをディレクトリごとに再計算するのは純粋に計算コストの無駄であり、ディレクトリ数が多くなるとそれに比例して時間がかかってしまいます。

Terraform Registryのプロトコル変更に関する提案は全く進展が見られませんが、幸か不幸か、terraform.lock.hclのフォーマットも全く変更されていないため、あきらめて自分でロックファイルを更新する方法を検討し始めました。

ハッシュ値の計算方法

さいわいHCLを書き換えるのは得意なので(?)、ハッシュ値の計算方法が分かれば、あとはどうにでもなりそうで、まずハッシュ値の計算方法を調べました。

terraform providers lockコマンドの動作をデバッグログを出しながら観察していると、いくつかのエンドポイントと通信していることが観測できます。

まず最初に、Terraform Registryから指定のプロバイダのメタデータを取得します。わかりやすいように、curlで再現すると以下のようになります。

$ curl -s https://registry.terraform.io/v1/providers/hashicorp/null/3.2.1/download/darwin/arm64 | jq .

出力のJSONを整形すると、以下のようになっています。 ※ascii_armorの部分のみ長いため、一部省略しています。

{
  "protocols": [
    "5.0"
  ],
  "os": "darwin",
  "arch": "arm64",
  "filename": "terraform-provider-null_3.2.1_darwin_arm64.zip",
  "download_url": "https://releases.hashicorp.com/terraform-provider-null/3.2.1/terraform-provider-null_3.2.1_darwin_arm64.zip",
  "shasums_url": "https://releases.hashicorp.com/terraform-provider-null/3.2.1/terraform-provider-null_3.2.1_SHA256SUMS",
  "shasums_signature_url": "https://releases.hashicorp.com/terraform-provider-null/3.2.1/terraform-provider-null_3.2.1_SHA256SUMS.72D7468F.sig",
  "shasum": "e4453fbebf90c53ca3323a92e7ca0f9961427d2f0ce0d2b65523cc04d5d999c2",
  "signing_keys": {
    "gpg_public_keys": [
      {
        "key_id": "34365D9472D7468F",
        "ascii_armor": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n (snip.) -----END PGP PUBLIC KEY BLOCK-----",
        "trust_signature": "",
        "source": "HashiCorp",
        "source_url": "https://www.hashicorp.com/security.html"
      }
    ]
  }
}

download_urlのところにプロバイダのzipアーカイブがあるので、これを取得します。

$ curl -s https://releases.hashicorp.com/terraform-provider-null/3.2.1/terraform-provider-null_3.2.1_darwin_arm64.zip -o tmp/terraform-provider-null_3.2.1_darwin_arm64.zip

zhのハッシュ値の計算方法は公式ドキュメントに記載のあるとおり、ただのzipファイルのsha256sumです。

https://developer.hashicorp.com/terraform/language/files/dependency-lock#zh

zh:: a mnemonic for "zip hash", this is a legacy hash format which is part of the Terraform provider registry protocol and is therefore used for providers that you install directly from an origin registry. This hashing scheme captures a SHA256 hash of each of the official .zip packages indexed in the origin registry.

ダウンロードしたzipのsha256sumを計算すると、先程のメタデータのshasumの項目と一致していることが分かります。

$ sha256sum tmp/terraform-provider-null_3.2.1_darwin_arm64.zip
e4453fbebf90c53ca3323a92e7ca0f9961427d2f0ce0d2b65523cc04d5d999c2  tmp/terraform-provider-null_3.2.1_darwin_arm64.zip

ただzhに関しては、全部のプラットフォーム分のzipをダウンロードする必要はありません。shasums_urlにあるSHA256SUMSファイルから、全プラットフォーム分のzhが取得できます。

$ curl -s https://releases.hashicorp.com/terraform-provider-null/3.2.1/terraform-provider-null_3.2.1_SHA256SUMS
58ed64389620cc7b82f01332e27723856422820cfd302e304b5f6c3436fb9840  terraform-provider-null_3.2.1_freebsd_arm.zip
62a5cc82c3b2ddef7ef3a6f2fedb7b9b3deff4ab7b414938b08e51d6e8be87cb  terraform-provider-null_3.2.1_windows_386.zip
63cff4de03af983175a7e37e52d4bd89d990be256b16b5c7f919aff5ad485aa5  terraform-provider-null_3.2.1_darwin_amd64.zip
74cb22c6700e48486b7cabefa10b33b801dfcab56f1a6ac9b6624531f3d36ea3  terraform-provider-null_3.2.1_linux_amd64.zip
78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3  terraform-provider-null_3.2.1_manifest.json
79e553aff77f1cfa9012a2218b8238dd672ea5e1b2924775ac9ac24d2a75c238  terraform-provider-null_3.2.1_windows_amd64.zip
a1e06ddda0b5ac48f7e7c7d59e1ab5a4073bbcf876c73c0299e4610ed53859dc  terraform-provider-null_3.2.1_freebsd_amd64.zip
c37a97090f1a82222925d45d84483b2aa702ef7ab66532af6cbcfb567818b970  terraform-provider-null_3.2.1_linux_arm.zip
e4453fbebf90c53ca3323a92e7ca0f9961427d2f0ce0d2b65523cc04d5d999c2  terraform-provider-null_3.2.1_darwin_arm64.zip
e80a746921946d8b6761e77305b752ad188da60688cfd2059322875d363be5f5  terraform-provider-null_3.2.1_linux_386.zip
fbdb892d9822ed0e4cb60f2fedbdbb556e4da0d88d3b942ae963ed6ff091e48f  terraform-provider-null_3.2.1_freebsd_386.zip
fca01a623d90d0cad0843102f9b8b9fe0d3ff8244593bd817f126582b52dd694  terraform-provider-null_3.2.1_linux_arm64.zip

shasums_urlのコンテンツの妥当性は、shasums_signature_urlとsigning_keysを使用することで署名の検証ができますが、ここでは割愛します。

一方、h1のハッシュ値の計算方法は、公式ドキュメントによるとzipファイルの中身のsha256sumとのことですが、

https://developer.hashicorp.com/terraform/language/files/dependency-lock#h1

h1:: a mnemonic for "hash scheme 1", which is the current preferred hashing scheme. Hash scheme 1 is also a SHA256 hash, but is one computed from the contents of the provider distribution package, rather than of the .zip archive it's contained within.

単純にダウンロードしたzipを解凍して、プロバイダのバイナリのsha256sumを計算してもロックファイルとハッシュ値が一致しませんし、そもそも桁数も違いそうです。

$ unzip tmp/terraform-provider-null_3.2.1_darwin_arm64.zip -d tmp/terraform-provider-null_3.2.1_darwin_arm64
Archive:  tmp/terraform-provider-null_3.2.1_darwin_arm64.zip
  inflating: tmp/terraform-provider-null_3.2.1_darwin_arm64/terraform-provider-null_v3.2.1_x5

$ sha256sum tmp/terraform-provider-null_3.2.1_darwin_arm64/terraform-provider-null_v3.2.1_x5
1dfc06bc382110e8a252fd748e4d9a877cdb4389f304e2efe6027f2c55fe0155  tmp/terraform-provider-null_3.2.1_darwin_arm64/terraform-provider-null_v3.2.1_x5

h1ハッシュの計算方法は、ドキュメントには詳細は書かれていませんが、コードを読むと分かる通り、Go Modulesで使われているgo.sumのハッシュ値計算と同じ方法を流用しています。

https://github.com/hashicorp/terraform/blob/v1.5.2/internal/getproviders/hash.go#L352

正味のハッシュ値計算の実装はここにあります。

https://github.com/golang/mod/blob/v0.8.0/sumdb/dirhash/hash.go

まずディレクトリ内に含まれるそれぞれのファイルのsha256sumのハッシュ値を算出し、ハッシュ値とファイル名を並べた一覧を作成します。そしてさらにその一覧に対してsha256sumのハッシュ値を計算することで、ディレクトリ全体のハッシュ値を算出しています。go.sumの実装を流用しているので、ライブラリ関数もそのまま流用できそうです。

ちなみに余談ですが、さきほどのコード中のコメントに、以下のような擬似コードが書かれています。

sha256sum $(find . -type f | sort) | sha256sum

試してみたところ、これではハッシュ値は一致しませんでした。Goの実装を忠実にシェル芸で再現することは困難そうですが、以下のissueでいくつかシェルスクリプトでの計算方法が議論されています。

x/mod/sumdb/dirhash: incorrect Unix command example · Issue #48498 · golang/go · GitHub

上記を参考にしつつ、厳密にやろうとすると複雑ですが、Terraformプロバイダの場合zipに含まれるファイルは1つだけです。簡略化して、以下のワンライナーハッシュ値の計算が一致するようになりました。

$ sha256sum terraform-provider-null_v3.2.1_x5 | sha256sum | cut -f1 -d' ' | xxd -r -p | {printf 'h1:'; base64}
h1:ydA0/SNRVB1o95btfshvYsmxA+jZFRZcvKzZSB+4S1M=

元のコメント中の擬似コードは、ディレクトリ内のファイルのハッシュ値を計算し、その全体のハッシュ値を取得することを表現していますが、実際にはその結果をさらにバイト列に変換した後、Base64エンコードしないと計算が合わないようです。なるほど〜。完全に理解した。

設計

というわけでハッシュ値の計算方法がわかったので、あとは地道に実装すればなんとかなりそうな目処は立ちましたが、実装する上でいくつか考慮したポイントを補足しておきます。

まず方針として、Terraform CLI (=terraformコマンド) に依存しないようにしました。もちろん将来的に実装詳細が変わった場合に、複数のTerraformバージョンをサポートするメンテナンスコストが上がるので、これまで避けていたわけですが、ハッシュ値の計算方法は引き続きリバースエンジニアリングできそうですし、terraformブロックのrequired_versionなどからTerraformバージョンも推測できそうです。ヒューリスティックに複数バージョンのシンタックスを解釈できるかは、将来的にどのようにロックフォーマットが変更されるかに依存します。どれぐらい面倒くさいかは、現時点ではなんとも言えないところではあります。

Terraform CLIに依存しないという方針と不可分ですが、terraform initもそこそこ負荷が高い処理なので、いくつかの仮定を置くことで省略することにしました。具体的には、それぞれのディレクトリのrequired_providersブロックで、 version = "3.2.1" のように必要なプロバイダのバージョンを単純に明示することを仮定し、version = "> 3.0" のようなバージョン制約式や、モジュール経由での間接依存は一旦未対応として無視することにしました。もちろん一般的には暗黙に仮定することはできませんが、tfupdateはこれまで.terraform.lock.hclには未対応だったので、tfupdateのユースケースでは、これまでもCIで最新版に更新するには厳密なバージョン指定が必要でした。実運用上は問題ないだろうという実装スコープを最小化するための割り切りです。

とはいえバージョン制約式に関しては、理論上バージョン一覧のメタデータをメモリ上に持っておけば対応可能な気はしており、もしロックファイルだけ更新したい需要が多そうであれば、そのうち気が向いたら対応するかもしれません。一方モジュール経由での間接依存については、実装するにはモジュールの中身を再帰的に解析する必要があり、モジュール参照はローカルだけではなく、リモートとなる可能性があるので、かなり実装のハードルが高そうです。

また、プロバイダのsource指定については、さしあたり公式のTerraform Registryのみを対象とし、既に非推奨となっているTerraform v0.13以前の名前空間の省略記法などのレガシーなバリエーションは、すべて無視するようにしました。.terraform.lock.hclが導入されたのはv0.14からですし、さすがに既に非推奨な古い記法に今さら対応するのもどうかなと思うので。

複数のディレクトリを効率よく更新するため、ハッシュ値の計算結果はメモリ上でキャッシュすることにしましたが、required_providersブロックはディレクトリ単位でパースすることにより、ディレクトリによって依存するプロバイダが異なっても動作するようにしました。

その他細かい挙動として、.terraform.lock.hclの中に該当のproviderブロックが存在しない場合は、providerブロックを追記するようにしましたが、.terraform.lock.hclのファイル自体が存在しない場合は、新規にファイル作成はしない方針としました。これは再帰的に一括更新するようなユースケースを想定した場合に、存在しない場合に作成するというのは誤爆するリスクが高そうだなという安全側に倒した判断で、もし需要がありそうであれば、opt-inフラグであればありかなという気もしています。あとバージョンは更新されていないがハッシュ値の一覧が一部欠けているようなパターンは、技術的には検出できなくはないですが、検出コストが高そうなので、ロックファイルのバージョン番号が変わったときのみ更新する方針としました。

全体的な方針として、tfupdateは複数のディレクトリを一括更新するという特性上、一部のディレクトリでモジュールのコンテキストを正しく解釈できない可能性があり、未対応の場合は可能な限りエラーとせずにDEBUGレベルのログを出すだけで、無視するようにしました。意図せず更新されない場合は、環境変数 TFUPDATE_LOG=DEBUGデバッグログを出せるようにしてあるので、ログを見るとなにかヒントがあるかもしれません。

完成

というわけで、車輪の再発明をなんだかんだ5000行ぐらい書いて、完成したものがこちらです。

github.com

あわせて、tfupdateのCircleCI用のサンプルも更新していますので、各自のCI設定で読み替えてください。Terraform CLIに依存しなくなったので、terraformコマンドのインストールなども不要になったのは地味にうれしいポイントです。

Use native tfupdate lock command for updating .terraform.lock.hcl by minamijoyo · Pull Request #356 · minamijoyo/tfupdate-circleci-example · GitHub

パフォーマンス比較のため、検証当時の最新版のAWSプロバイダv5.6.2のバージョンアップで検証しました。対象はcrowdworks.jpのTerraform設定のコードベース(ディレクトリ数は約300)、プラットフォームは3つ(linux_amd64, darwin_amd64, darwin_arm64)、実行環境はGitHub Actions上のubuntu-latestのデフォルトのRunnner(CPU2コア/メモリ7GB)で実行しました。

結果として、従来のterraform providers mirror & lockを組み合わせた手法では、ロックファイルの更新に1時間20分かかっていたものが、tfupdate lockコマンドを使用すると16秒で完了し、300倍の高速化が確認できました。改善前の処理時間はディレクトリ数に大きく依存しますが、tfupdate lockコマンドの処理時間は、プロバイダのサイズxプラットフォーム数が支配的であり、今後ディレクトリ数が増えても十分にスケーラブルなことが確認できました。

今度こそ.terraform.lock.hcl完全に理解した(n回目)

まとめ

tfupdate v0.7から、tfupdate lockコマンドが追加され、.terraform.lock.hclの更新に対応しました。

既にtfupdateユーザーの皆さんは、簡単に爆速で.terraform.lock.hclも更新できるようになったので、ぜひお試し下さい。またこれまでtfupdateを使ったことがないぞという人も、もし.terraform.lock.hclの更新に困っているのであれば、この機会にちょっと試してみてください。ご意見ご感想をお待ちしております。

気に入ったらスター☆してくれると、今後の開発の励みになります |ω・`)チラッ

github.com

最後になりましたが、クラウドワークスでは、Terraformが大好きなSREを募集しております。

herp.careers

© 2016 CrowdWorks, Inc., All rights reserved.