たぶん仕事の役に立たないTerraform v0.12の鑑賞ポイント

Terraform職人のみなさん待望のTerraform v0.12がリリースされました。わーい。

# TL;DR

3行でまとめると

- この記事は、当初v0.12の変更点についてまとめようかと思って書き始めたのですが、
- 公式の情報がいろいろまとまっており、なんだかただ翻訳するだけみたいな感じになってしまい、つまらなくなってしまったのでやめました。
- 仕事に役立つ情報をお探しの方は参考資料を読んでください。

おわり

## 参考資料
- [Announcing Terraform 0.12](https://www.hashicorp.com/blog/announcing-terraform-0-12)
- [HashiCorp Terraform 0.12 Preview](https://www.hashicorp.com/blog/terraform-0-1-2-preview)
- [Upgrading to Terraform v0.12](https://www.terraform.io/upgrade-guides/0-12.html)
- [CHANGELOG](https://github.com/hashicorp/terraform/blob/v0.12.0/CHANGELOG.md)

# 余談
あとは長い余談です。暇な人だけ読んで下さい。
たぶん仕事の役に立たないTerraform v0.12の鑑賞ポイントをただひたすら紹介するという、
細かすぎて伝わらないポエムのようななにかです。

## v0.12-devブランチのマージコミット
Terraform v0.12は通常のリリースとは異なる開発フローで、長いあいだ開発用のv0.12-devブランチで開発が進められ、alpha版のリリースのちょっと前に一気にmasterブランチにマージされました。

そのコミットがこちらです。

https://github.com/hashicorp/terraform/commit/86e6481cc661b2ba694d353aab6d11838c066c7c

```
3,043 changed files with 406,513 additions and 296,866 deletions.
```

ってなってて、これはもう完全に草w
とか言ってたのが2018年の10月のことで、懐かしいです。

で、結局最終的なv0.11とv0.12のgitの差分がどうなったのか気になるじゃないですか。
というわけで計測してみると、これぐらいです。

```
[terraform@master|✔]$ git diff --shortstat v0.11.14...v0.12.0
 5255 files changed, 606655 insertions(+), 1336138 deletions(-)
warning: inexact rename detection was skipped due to too many files.
warning: you may want to set your diff.renameLimit variable to at least 1922 and retry the command.
```

あれ、なんかエラーが出ました。
変更が多すぎてリネームが正しく計測できていないようです。

gitの設定値を一時的に引き上げて計測してみます。

```
[terraform@master|✔]$ git config diff.renameLimit 999999

[terraform@master|✔]$ git diff --shortstat v0.11.14...v0.12.0
 5144 files changed, 563295 insertions(+), 1292778 deletions(-)

[terraform@master|✔]$ git config --unset diff.renameLimit
```

だいたい50万行ぐらい増えて100万行ぐらい減ってる(!?) これはもはや別物でわ。。。なんか削除行数の方が多くてあってるのかこれ自信がない。vendor配下の差分？
いや、むしろこれでよく表面上はv0.11と互換性保ってるよね。

## 0.12upgradeは何をしているのか
とはいえ0.11と0.12でいろいろ非互換な変更は入ってるので、移行を手助けするために0.12upgradeというサブコマンドがTerraform v0.12に付いてます。（あ、うっかり仕事に役立つ情報を書いてしまった。ごめんね。

手元の3万行ぐらいのv0.11のコードベースに0.12upgradeを流したら6000行ぐらいdiffが出ました。0.12upgradeがなかったら死んでいた。0.12upgradeすごい（小並感

さて、こいつは一体何をしてるのでしょう？気になりますよね？

実装を読むよりもテストケースに入出力のデータが置いてあるのでこっちを見たほうが分かりやすいです。
https://github.com/hashicorp/terraform/tree/v0.12.0/configs/configupgrade/test-fixtures/valid

テストケースを眺めてると、いろいろおもしろいものが混じってますが、
v0.11でもblockにmapのlistを渡すと、型チェックをすり抜けて疑似的にv0.12のdynamicブロックみたいなことができたようです（？）
https://github.com/hashicorp/terraform/tree/v0.12.0/configs/configupgrade/test-fixtures/valid/block-as-list-dynamic
HILの組み込みの非公開関数（？）を呼び出してるコードもちゃんと対応してたり。誰だよこんなコード書いてるやつ。
https://github.com/hashicorp/terraform/blob/v0.12.0/configs/configupgrade/test-fixtures/valid/funcs-replaced/input/funcs-replaced.tf#L17-L25

## Terraform v0.11以前の型システム
v0.11以前の型システムにはいろいろ問題がありました。その闇の根深さを垣間見れるのが以下のissueです。

string literal "false" evaluates to "0" using null_data_source
https://github.com/hashicorp/terraform/issues/13512

"false" という文字列がいろんなレイヤを通ると結果的に"0"という文字列になっちゃうという問題に対して、なぜこれが起きるのかをすごく詳細に解説しています。
解説が長すぎて読んでてもなるほど？という気持ちでちっともよくわかりません。俺はTerraformのことなど何も分かっていなかった。。。という気持ちになります。Terraform完全に理解した人はぜひ上のissueを読んでみて下さい。理解できたら教えて下さい。

とりあえずわかることは、Terraform v0.11以前で使われている `"${hoge}"` のような式の評価に使われているHIL(HashiCorp Interpolation Language)は内部的にデータ型を持っているけど、評価結果はすべて文字列になってしまうので、そこで型の情報が失われてしまうようです。

HCL2がHCL1とHILを統合してFirst-class expression syntaxを導入した理由はまさにここにあるのだと思います。HCL1とHILにコンテキストが分断されていると、適切な型チェックができないのです。

## zclconf/go-ctyとはなんなのか
ところでhcl2のリポジトリを眺めてると、コアとなる型システムは別のctyというライブラリを使ってることに気づきます。

https://github.com/zclconf/go-cty

ctyは動的な型システムです。Goの型システムをそのまま使わずに、あえて自前で型システムを再実装している理由は、値が未定であるUnknown Valuesと、型も未定であるDynamic Pseudo-TypeをHCL2で扱えるようにするためです。
hcl2とは独立したライブラリとして実装されていますが、hcl2を実装するために開発されたようです。

さてGitHubのorgのzclconfというのはなんでしょう？
hcl2のリポジトリのgitの履歴を漁ると、もともとzclconf/go-zclだったのが、hashicorp/hcl2に書き換えられていることがわかります。
https://github.com/hashicorp/hcl2/commit/c3ca111fff25b8babf9eaff4d1b348be3767e926

どうやらこれはこれはHILのparserをyaccからGoに再実装したapparentlymartさんが、HILの改良に限界を感じHCLとHILを統合したZCLという言語のプロトタイプを作って実験していたのがHCL2の原型となっているようです。ZCLなんか ~~中二感~~ 最強感があっていいですね。（個人の感想です）
その後apparentlymartさんはHashiCorpに雇われてHCL2対応の主担当になり、リポジトリがhashicorp/hcl2にリネームされました。
しかしながらhcl2とは独立したライブラリとして切り出されていたctyは、hashicorpのorg配下には移されませんでした。その名残がimportパスに残っているのです。zclがhcl2にリネームされたgitのコミットに思いを馳せると胸熱ですね。importパスを見るたびにニヤニヤできるので特にオススメの鑑賞ポイントです。

## HCL2の情報モデルとJSON互換の意味
HCL2の仕様と実装は以下にまとまっています
https://github.com/hashicorp/hcl2

HCL1は仕様が明文化されていなかったので、これは大きな進歩ですが、
HCL2はさらに仕様として概念的な情報モデルと構文を明確に分離しているのが特徴です。

HCL Syntax-Agnostic Information Model
https://github.com/hashicorp/hcl2/blob/master/hcl/spec.md

HCL Native Syntax Specification
https://github.com/hashicorp/hcl2/blob/master/hcl/hclsyntax/spec.md

HCL JSON Syntax Specification
https://github.com/hashicorp/hcl2/blob/master/hcl/json/spec.md

Native Syntaxはすべての機能をサポートしますが、JSONはその部分セットです。
HCL1は他のツールと連携できるよう、JSON互換を謳っていましたが、仕様が明文化されておらず、たびたび利用者の混乱を招きました。私自身はJSON入力を食わしたりしてなかったので、あんまり気にしてなかったのですが、v0.11以前のJSONサポートはいろいろ問題があったようです。

ところで、json2hclというJSONとHCLを相互変換するツールなどもあるようですが、
https://github.com/kvz/json2hcl
JSONとHCLはそもそも1対1に相互変換できません。（えっ

JSONで属性がネストした場合に、block内のattributeとmapのキーで区別が曖昧だからです。
blockはproviderのschemaでキーが定義されているもの、一方mapはユーザ入力で自由にキーが指定できるものです。

Terraform はこれをどうやって区別しているかというと、providerのschemaによって解釈しています。
つまりこれはアプリケーションに依存した知識であって、純粋な意味でJSONとHCLを1対1に相互変換は理論上できません。
つまりHCLのJSON互換とは、アプリケーションの入力としてJSONを受け付けることができるという意味です。
なるほどー。行間長すぎるでしょ。普通JSON互換って言われたら1対1に相互に変換できると思うよね。

## HCL2の拡張機能
HCL2の仕様を眺めているとdynamicはJSON構文ではサポートされていないようです。このようなNative Syntaxでしかサポートされていない機能は拡張機能として区別されているようです。
他にどんなのがあるのかなーってコード眺めてると、

ユーザ定義関数（？）とか
https://github.com/hashicorp/hcl2/blob/master/ext/userfunc
include文（？）とか
https://github.com/hashicorp/hcl2/tree/master/ext/include

作ろうとしてる形跡があって、
Terraformのmoduleとは別の何かライブラリっぽい仕組みでも作ろうとしてるのかなー？夢が広がりング。

## resourceレベルfor_each
HashiCorpの公式ブログでfor_eachの説明をしてる記事があります。
この記事はdynamicブロックの説明をしてるんですけど、
https://www.hashicorp.com/blog/hashicorp-terraform-0-12-preview-for-and-for-each

よく読むとresourceレベルのfor_eachというのも将来的に実装しようと思ってて、v0.12には入れられないけど、そのための裏側の準備とかはいろいろしてるよって書いてあります。
dynamicブロックのインパクが強すぎて、最初読んだときにこの重要性がイマイチよく分かってなかったんですけど、このresourceレベルfor_eachとはcountの拡張です。要するに、countの添字が数字じゃなくて文字列でインスタンスを区別できるようになるということですね。途中のインスタンスを消して添字がずれたりすることがありません。これはいますぐ欲しいやつですね。顧客が求めていたものだ。続報を待ちましょう。

## tfstateファイルのフォーマット変更
resourceのfor_eachのためだけというわけでは全然ないんですが、もっといろんな問題を根本的に解決するため、
tfstateのデータ構造が変わって、tfstateのschemaバージョンは3から4になりました。

states: New packages for state, state files, and state managers
https://github.com/hashicorp/terraform/pull/18223

tfstate職人の皆さんは温かみのある手作業でtfstateを日々エディットしていることかと思いますので、この変更点はちゃんと抑えておきたいですよね？

resourceのアドレスや型などのメタデータとresourceのインスタンスを区別するようになった。
これによりcount=0のときでもresourceのメタデータを保持することができるようになった。

またdeposedとtaintの状態をresourceのインスタンスごとに厳密にコントロールできるようになった。
deposedなインスタンスは配列で管理せずに乱数をキーとしたmapで区別できるようになった。

resourceのattributesはこれまでフラットなmap[string]stringで保持していて、
複雑なオブジェクトはkeyをドットで連結した文字列にして階層を表現してたけど、
もっと直接的にネストしたオブジェクト構造ををそのまま記録できるようになった。

resourceのインスタンスが持つattributesのschemaバージョンを保持することができるようになった。
これによりproviderのresourceのschemaが変わった場合に、
古いバージョンのproviderでtfstateに記録されたattributesを、新しいproviderでmigrateできるようになった。

moduleのoutputはトップレベルのrootモジュールのもののみ永続化され、子レベルのものは永続化されなくなった。（必要に応じてメモリ上で評価される）
また複雑なオブジェクト構造をそのままoutputできるようになった。

module単位でresourceがグループ化されていたが、トップレベルにresourceが来て、アドレスに対応するmoduleのパスをメタデータとして保持するようになった。

## tfplanファイルのフォーマット変更
tfstateファイルだけじゃなくtfplanファイルのフォーマット変更されました。

plans: New packages for plans and plan files
https://github.com/hashicorp/terraform/pull/18288

以前はconfig/state/planのメモリ上の表現をそのままgobエンコードでdumpしてたけど、普通のファイルをそのままzipで固めて保持するようにして、configとstateはオリジナルのファイルフォーマットをそのまま使えるようにした。planの部分はgobをやめてprotobufを使うようになったっぽい。plugin周りもgRPCにしてproviderのschemaがprotobufになってるので、それに合わせたいというかんじじゃなかろうか。
tfstateと違って、tfplanそのものを利用者が読むことはあんまりないと思うんで、フォーマット変更そのものはあんまり興味はないんだけど、データ構造がいろいろ変わってるので、興味深い。
0.12からterraform planのdiffがめっちゃ見やすくなってるけど、
diffの表示はUIの関心事なので、内部的には前後のインスタンスの状態全部を保持するようになってるよう。
tfplanファイルの中身を見たい場合は、 `terraform show` で見れます。

ちなみにv0.12でいいかんじになったplanのdiffですが、あれ当初初期リリースのスコープに入ってなかったみたい。内部のデータ構造が変わりすぎて逆にv0.11のflatmapっぽいdiffを再現する方がめんどくさくて、いいかんじのdiffを出すのもv0.12に入ったらしい。これ豆な。

## pluginプロトコルがgRPCになった
providerと通信するpluginのプロトコルバージョンが4から5になって、RPCからgRPCに変更。

grpc proto definitions
https://github.com/hashicorp/terraform/pull/18550

grpc plugins
https://github.com/hashicorp/terraform/pull/18638

変更の意図はちゃんと説明されていないんだけど、v0.10でproviderがcoreのリポジトリから分離されたものの、内部的なアーキテクチャはRPCでgobエンコードしたGoのオブジェクトをそのまま投げつけており、今後v0.12にみたいな非互換がぶっこむときにproviderがcoreの内部実装に依存しすぎるのうれしくなかったんだろうと思う。
これの何が問題かというと、providerがvendorしたcoreのライブラリのバージョンが異なると型が一致しないんですね。つらい。

プロトコルとしてgRPCを選んだのも夢がある感じで、gRPCだと双方向のストリーミングができるので、coreのUIとproviderをもっとインタラクティブに接続できるようになる可能性が広がったとか、
インターフェースがgRPCでGo依存がなくなったので、理論上Go以外でもproviderが実装できるようになったとか（現実的にはproviderのhelperがGo実装しかないのでかなり厳しいとは思うが）

## helper/schema周りの変更
providerのSDKまわりはあんまりよくわかってない。誰か解説して下さい。

add UpgradeState to schema.Resource
https://github.com/hashicorp/terraform/pull/18579

StateUpgrade redux
https://github.com/hashicorp/terraform/pull/18586

MigrateStateがdeprecatedになり、StateUpgradersという新しいschemaバージョンのアップグレードの仕組みが導入されたらしい？

Can't declare empty lists of nested blocks in 0.12 SDK
https://github.com/hashicorp/terraform/issues/20505

helper/schema: ConfigMode field in *Schema
https://github.com/hashicorp/terraform/pull/20626

schema.ResourceでOptional: trueかつComputed: trueな省略可能なNestedBlockで暗黙のデフォルト値を設定していた場合、
v0.11ではブロックに空のリストを渡すとデフォルト値をクリアできるというハックがあったようで、
v0.12で型が厳密になった結果この技が使えなくなった。
v0.11/v0.12で互換性を維持しつつ回避策として、設定上はBlockのように見えるけど、内部的にはオブジェクト型のAttributeとして扱うSchemaConfigModeAttrというパラメータが増えた。
基本的にproviderを自作してる人しか知らなくていい情報。

なんでここだけ妙に詳しいかというと自作の[tfschema](https://github.com/minamijoyo/tfschema)というツールがこの変更で壊れたからですね。それ以外にもいっぱい非互換な変更はあったが。tfschemaはTerraform v0.11/v0.12両方で動くようにいいかんじに直しておいたので、もし気に入ったらスター下さい（突然の宣伝）

Controlling optional argument in a resource
https://github.com/hashicorp/terraform/issues/17968

特定のhelperの改善というよりもHCL2で型システム全体の整合性が整理された結果だけど、attributeの値を明示的に未設定(null)を設定できるようになった。
これまではGoのデフォルト値例えばstringだと""を設定することで、これを空文字として扱うか、未設定として扱うかはprovider側の実装に依存していた。
明示的に未設定を設定する意味はないんだけど、conditionalで特定の条件のときだけ設定したいみたいな時にうれしい。プログラマに嫌われるnullですが、顧客が求めていたものはnullだったのだ。

## registoryの拡張

PITR
https://github.com/hashicorp/terraform/pull/18625

特になんの説明もなくPITRとだけ書かれた謎めいたタイトルのPRですが、コードのdiffを見ると、Moduleを配布してるTerraform ResitoryからProviderのバイナリも配布できるようにしてるようです。
つまり Provider Installation from Terraform Resitoryとかそーゆー意味だと思う。若干単語は違うかもだけど。これはそのうち公式provider以外にもサードパーティにも開放されて、野良providerの配布がしやすくなるんじゃなかろうかと期待。
v0.12でデバッグログを出しながらterraform initすると気づくんですけど、 `releases.hashicorp.com` を見る前に、先に `registry.terraform.io` にダウンロード先を聞きに行くようになってますね。勘のいい人はこれを見ておおおって思ったはず。

サードパーティのプロバイダの配布については、以下のIssueでもやるつもりだけどまだちょっと仕組みが整っていないというようなコメントをされてますね。今後に期待。

Support alternate plugin release download hosts
https://github.com/hashicorp/terraform/issues/15252

# おわりに
これ書いてて一体誰得なんだろうってかんじですが、最後まで読んだあなたも相当な暇人ですね。
Terraformの内部実装を調べれば調べるほど何もわからないので、なんか間違ったこと書いてたら教えてください。