TT Lab
はじめる
学ぶ 学習パス コース

Terraform実戦

サービス三つ × 環境三つを手で展開したら二十ブロックになった

TT Labで続きを見る

目標

ネストしたオブジェクトの変数を受け取ってマップに変換し、二重の繰り返しをflattenとsetproductの2つの方式で展開して同じ結果を出し、dynamicブロックでオプションのブロックを作って、その限界をエラーで確認したあと、展開した行列をファイルとレポートにレンダリングします。

なぜ重要なのか

インフラコードが長くなる本当の理由は、リソースが多いからではなく、同じ形が繰り返されるからです。その繰り返しを手で展開すると、直す場所が20か所になり、レビューする人は、20個のブロックが本当に同じかを、目で突き合わせる必要があります。HCLは、これを2つの方向で解決します。値を作る側と、ブロックを作る側です。値の側(for式・flatten・setproduct・オブジェクト型)は、いくらでも畳んだり広げたりでき、ブロックの側(dynamic)は、プロバイダーのスキーマが許す範囲の中でしか作れません。この違いを知らないと、dynamicですべてを組もうとして、読めない設定を作ることになります。実務でdynamicが最も価値のある使われ方をするのは、実は繰り返しではなく、「このブロックを入れるかどうか」という選択であり、繰り返しは、たいていfor_eachと値の式が担当します。

ステップ

  1. /root/tfa-dyn/variables.tfに、list(object({ name = string, port = number, envs = list(string) }))型のservices変数を宣言し、名前が重複していないかを確認するvalidationを付けてください。/root/tfa-dyn/terraform.tfvarsには、api(8080、dev・prod)、web(8081、dev)、batch(9090、dev・stage・prod)の3つを書きます。/root/tfa-dyn/main.tfには、プロバイダーの宣言と、jsonencode(var.services)をout/services.jsonに書くlocal_file.servicesを置いて、init・applyしてください。
  2. /root/tfa-dyn/svc.tfにlocal.by_name(名前をキーとしたマップ)を作成し、それでfor_eachを回して、サービスごとに/root/tfa-dyn/out/svc/<이름>.confを書くlocal_file.svcを宣言してください。ファイルの内容は、name=<이름>とport=<번호>の2行です(プレースホルダーは順に、サービス名とポート番号です)。applyしたあと、状態のインスタンスのキーが何かを見てください。
  3. /root/tfa-dyn/flat.tfに、サービス×そのサービスの環境をすべて展開したlocal.pairs(各項目は、env・name・portを持つオブジェクト)をflattenで作成し、"<env>/<name>"をキーとしたlocal.by_keyマップをjsonencodeして、/root/tfa-dyn/out/flat.jsonに書いてください。項目は6つになる必要があります。
  4. /root/tfa-dyn/product.tfにlocal.all_envs(すべての環境の重複のない一覧)を作成し、setproduct(local.all_envs, var.services)のすべての組み合わせのうち、そのサービスが実際に使う環境だけを残してlocal.combo_by_keyを作成し、/root/tfa-dyn/out/product.jsonに書いてください。ステップ3の結果と、完全に同じである必要があります。
  5. /root/tfa-dyn/cert.tfに、var.with_subject(bool、デフォルトtrue)とED25519の秘密鍵、そして、dynamic "subject"でサブジェクトブロックをオン・オフするtls_self_signed_cert.cを置いてください。サブジェクトは、common_name = "api.internal"、organization = "LabHub"です。証明書は、/root/tfa-dyn/out/cert.pemに書きます。まず、-var with_subject=falseで適用して、openssl x509 -noout -subjectの結果を/root/tfa-dyn/out/no-subject.txtに保存し、そのあと、デフォルト値で再度適用して、同じコマンドの結果を/root/tfa-dyn/out/subject.txtに保存してください。
  6. /root/tfa-dyn/cert.tfのdynamic "subject"が、2つのブロックを作るように一時的に修正してapplyし、失敗の出力を/root/tfa-dyn/out/limit-error.txtに保存してください。そのあと、元に戻して再度適用し、プランがきれいな状態で終えます。
  7. /root/tfa-dyn/rendered.tfに、ステップ3のlocal.by_keyでfor_eachを回して、/root/tfa-dyn/rendered/<env>-<name>.confを6つ作成してください。内容は、env=、name=、port=の3行です。
  8. /root/tfa-dyn/summary.tfに、環境ごとの項目数(by_env)、名前をソートした一覧(services)、サービスのポートの合計(total_ports)の3つを入れた、/root/tfa-dyn/out/summary.jsonを作成してください。3つの値は、すべてHCLの式で計算します。

参考

ネストしたオブジェクトを変数の型で受け取る

/root/tfa-dyn/variables.tfに、list(object({ name = string, port = number, envs = list(string) }))型のservices変数を宣言し、名前が重複していないかを確認するvalidationを付けてください。/root/tfa-dyn/terraform.tfvarsには、api(8080、dev・prod)、web(8081、dev)、batch(9090、dev・stage・prod)の3つを書きます。/root/tfa-dyn/main.tfには、プロバイダーの宣言と、jsonencode(var.services)をout/services.jsonに書くlocal_file.servicesを置いて、init・applyしてください。

型をobjectで書いておけば、tfvarsでスペルが間違っていたときに、applyの前に止まります。validationのconditionは、真のときに通過です。名前の数と、重複を除いた数が同じかを見ればよいです。jsonencodeは、HCLの値をそのままJSONに変換してくれます。

リストをマップに変換して、for_eachに渡す

/root/tfa-dyn/svc.tfにlocal.by_name(名前をキーとしたマップ)を作成し、それでfor_eachを回して、サービスごとに/root/tfa-dyn/out/svc/<이름>.confを書くlocal_file.svcを宣言してください。ファイルの内容は、name=<이름>とport=<번호>の2行です(プレースホルダーは順に、サービス名とポート番号です)。applyしたあと、状態のインスタンスのキーが何かを見てください。

for_eachは、リストを受け取りません。セットかマップである必要があります。リストでcountを使うと、真ん中の項目を削除したときに、後ろがすべてずれて再作成されますが、マップのキーを使えば、その項目だけが削除されます。状態アドレスが、角括弧の中のキーで書かれることを確認してください。

二重の繰り返しを、flattenで1層に展開する

/root/tfa-dyn/flat.tfに、サービス×そのサービスの環境をすべて展開したlocal.pairs(各項目は、env・name・portを持つオブジェクト)をflattenで作成し、"<env>/<name>"をキーとしたlocal.by_keyマップをjsonencodeして、/root/tfa-dyn/out/flat.jsonに書いてください。項目は6つになる必要があります。

内側のforがリストを返すため、外側のforの結果は、リストのリストになります。flattenが、その1層を剥がします。マップに変換しておけば、次のステップでfor_eachにそのまま渡せ、キーがそのまま、人が読める名前になります。

同じ行列を、setproductで作り直す

/root/tfa-dyn/product.tfにlocal.all_envs(すべての環境の重複のない一覧)を作成し、setproduct(local.all_envs, var.services)のすべての組み合わせのうち、そのサービスが実際に使う環境だけを残してlocal.combo_by_keyを作成し、/root/tfa-dyn/out/product.jsonに書いてください。ステップ3の結果と、完全に同じである必要があります。

setproductは、絞り込みをしてくれません。すべての組み合わせを作ってから、for式のifで残すものを選びます。組み合わせをすべて作ってから捨てる方式と、最初から必要なものだけを展開する方式のうち、どちらが読みやすいかを、2つのファイルを並べて判断してみてください。

dynamicで、ブロックをあり・なしにする

/root/tfa-dyn/cert.tfに、var.with_subject(bool、デフォルトtrue)とED25519の秘密鍵、そして、dynamic "subject"でサブジェクトブロックをオン・オフするtls_self_signed_cert.cを置いてください。サブジェクトは、common_name = "api.internal"、organization = "LabHub"です。証明書は、/root/tfa-dyn/out/cert.pemに書きます。まず、-var with_subject=falseで適用して、openssl x509 -noout -subjectの結果を/root/tfa-dyn/out/no-subject.txtに保存し、そのあと、デフォルト値で再度適用して、同じコマンドの結果を/root/tfa-dyn/out/subject.txtに保存してください。

dynamicのfor_eachに空のコレクションを渡すと、そのブロックはまったく作られません。これが、実務でdynamicを使う最もよくある理由です。繰り返しではなく、オプションのブロックです。証明書のサブジェクトが本当に空かは、設定ではなく、作られた証明書を開いて確認してください。

dynamicは、スキーマが許す分だけ作る

/root/tfa-dyn/cert.tfのdynamic "subject"が、2つのブロックを作るように一時的に修正してapplyし、失敗の出力を/root/tfa-dyn/out/limit-error.txtに保存してください。そのあと、元に戻して再度適用し、プランがきれいな状態で終えます。

dynamicは、ブロックをいくつでも作ってくれる仕組みではなく、「設定値からブロックを作る」仕組みです。いくつまで許可されるかは、プロバイダーのスキーマが決めます。エラーメッセージが、許可範囲と実際の個数を一緒に言ってくれるかを見てください。

展開した行列をファイルにレンダリングする

/root/tfa-dyn/rendered.tfに、ステップ3のlocal.by_keyでfor_eachを回して、/root/tfa-dyn/rendered/<env>-<name>.confを6つ作成してください。内容は、env=、name=、port=の3行です。

キーがすでに環境と名前を含んでいるので、各ファイル名は、each.valueから取り出して組み立てればよいです。あとで、サービス1つが環境を1つ追加で使うようになったら、tfvarsを1行直すだけで、ファイルが1つ増えるかを確認してみてください。それが、この構造を使う理由です。

展開した値を再び畳んで、レポートを作る

/root/tfa-dyn/summary.tfに、環境ごとの項目数(by_env)、名前をソートした一覧(services)、サービスのポートの合計(total_ports)の3つを入れた、/root/tfa-dyn/out/summary.jsonを作成してください。3つの値は、すべてHCLの式で計算します。

by_envは、環境ごとにpairsを絞り込んで長さを数えればよく、ポートの合計は、sum関数で求めます。ここでソートを行う理由は、人が見やすいからではなく、ソートしないと、同じ入力が実行ごとに異なる順序で出て、比較が不可能になるためです。