値を手でつなげて引用符に負けた
目標
文字列・コレクション・条件・変換・テンプレート・シリアライズの関数を、コンソールで1行ずつ確認したあと、そのまま設定に移して、実際のファイルを作ります。最後に、値を1つも直接書かず、変数からだけ引いてくる設定を作って、別の入力にも耐えられるかを確認します。
なぜ重要なのか
設定ファイルが長くなる理由は、たいてい同じ値を複数の場所に書くからです。名前を組み合わせる規則、環境ごとに違う値、マップを合成するときの優先順位のようなものを、関数で書いておけば、値を1つ変えるときに直す箇所が1か所に減ります。そして、関数はコンソールで1行で確認できるので、式が紛らわしいときに適用を繰り返すコストがなくなります。失敗を扱う関数も重要です。外から入ってきたデータは、いつでも壊れる可能性があるのに、そのたびに適用全体が止まると、運用が他人のミスに縛られます。失敗を値に変える関数を知っていれば、デフォルト値に落ち着く設計ができます。最後に、シリアライズの関数は、引用符とエスケープを、人が責任を持たなくて済むようにしてくれます。手でつないだJSONは、いつか必ず壊れます。
ステップ
/root/tfb-func/playを作成して、localプロバイダーだけを宣言したmain.tfでinitしてください。/root/tfb-func/strings-in.txtに6行を書いてください。join("-", ["web", "prod", "01"])、format("%s-%03d", "node", 7)、replace("db-prod-01", "prod", "stage")、trimspace(" padded ")、substr("abcdefgh", 2, 3)、upper(join("_", sort(["b", "a"])))です。そのファイルをtofu consoleに流し込んで、結果を/root/tfb-func/strings-out.txtに保存してください。/root/tfb-func/coll/main.tfに、map(number)の変数ports(web 80、api 8080)と、mergeで2つのタグのマップを合成したlocal.tags、sort(keys(...))で作ったlocal.names、lookupで読み取ったlocal.api(あるキー)とlocal.db(ないキー、デフォルト値5432)、distinctで重複をなくしたlocal.uniqueを置いて、5つの出力として出力してください。init・applyしたあと、tofu output -jsonを/root/tfb-func/collections.jsonに保存してください。/root/tfb-func/cond/main.tfに、デフォルト値がprodの変数envと、var.env == "prod"を入れたlocal.is_prod、それで選ぶlocal.replicas(3または1)、local.log_level(warnまたはdebug)、local.backup(dailyまたはnone)、formatで作ったlocal.endpoint(ポートは443または8080)を置いて、4つの値を키=값の4行で書くapp.confを作成してください(プレースホルダーは、順にキーと値です)。init・applyしたあと、tofu console -var env=devで同じ4つの値を評価して、/root/tfb-func/cond-dev.txtに保存してください。/root/tfb-func/guard/に、正しいJSONのgood.json(nameがapi、portが8080)と、壊れたJSONのbad.jsonを作成してください。main.tfで、2つのファイルをtry(jsondecode(...), {})で読み、あるキーとないキーを、それぞれtry(..., "unknown")で取り出し、can(jsondecode(...))で2つのファイルの成否を入れて、5つの出力(name・miss・bad_keys・ok_good・ok_bad)として出力してください。applyしたあと、tofu output -jsonを/root/tfb-func/guard.jsonに保存してください。/root/tfb-func/tpl/site.tftplに、名前の1行、ポートのリストを回す繰り返し、TLSかどうかによる条件を含んだテンプレートを書いて、main.tfで、templatefileでname = "api"、ports = [80, 443]、tls = trueを渡して、site.confを作成してください。結果は、server {で始まり、name = api、listen 80;、listen 443;、tls on;、}の6行でなければなりません。/root/tfb-func/enc/main.tfに、local.data(name= api、ports= [80, 443]、tls= true)を置いて、jsonencodeでdata.json、yamlencodeでdata.yamlを作成してください。applyしたあと、jq -c .で読んだ結果を/root/tfb-func/enc-json.txtに、yq -o=json -I=0 .で読んだ結果を/root/tfb-func/enc-yaml.txtに保存してください。2つのファイルの内容は、同じである必要があります。/root/tfb-func/convert-in.txtに6行を書いてください。parseint("ff", 16)、tonumber("007")、tostring(true)、try(tonumber("abc"), -1)、can(tonumber("abc"))、format("%s has %d ports", "api", length([80, 443]))です。/root/tfb-func/playで、コンソールに流し込んで、結果を/root/tfb-func/convert-out.txtに保存してください。/root/tfb-func/compose/main.tfに、list(string)の変数names(デフォルトapi、web、db)と、list(number)の変数ports(デフォルト8080、80、5432)を置いて、formatlistで이름=포트の行のリストを作り、joinでつなぎ、formatとlengthで# <개수> servicesという見出しを作って、services.confを作成してください(プレースホルダーは、順に名前とポート、件数です)。1行目は見出しで、その後ろがサービスの行です。値を直接書かず、変数からだけ引いてきてください。採点ツールが、別の入力で同じ設定を適用してみます。
参考
- コンソールは、現在のディレクトリの変数とlocalsを読むので、initが終わったディレクトリで実行する必要があります。
- コンソールは、最初のエラーで止まり、0ではないコードで終了します。複数行を流し込むときは、前の行が成功してはじめて、後ろの行が評価されます。
- よくある間違い: ステップ4の壊れたJSONを、直してしまうこと。そのファイルは、壊れたままにしておいてはじめて、吸収を確認できます。
- よくある間違い: ステップ8で、サービスの個数を3と書いておくこと。入力が変わると、見出しだけが間違いになります。
- 関数の一覧・templatefile・try・can・jsonencode・yamlencode・条件式
文字列関数をコンソールで確認する
/root/tfb-func/playを作成して、localプロバイダーだけを宣言したmain.tfでinitしてください。/root/tfb-func/strings-in.txtに6行を書いてください。join("-", ["web", "prod", "01"])、format("%s-%03d", "node", 7)、replace("db-prod-01", "prod", "stage")、trimspace(" padded ")、substr("abcdefgh", 2, 3)、upper(join("_", sort(["b", "a"])))です。そのファイルをtofu consoleに流し込んで、結果を/root/tfb-func/strings-out.txtに保存してください。
コンソールは、ファイルを流し込むと、1行ずつ評価して、結果だけを出力します。formatのプレースホルダーは、書式文字列の規則にそのまま従い、substrの2つ目の引数は、長さではなく開始位置です。
マップとリストを合成して、取り出す
/root/tfb-func/coll/main.tfに、map(number)の変数ports(web 80、api 8080)と、mergeで2つのタグのマップを合成したlocal.tags、sort(keys(...))で作ったlocal.names、lookupで読み取ったlocal.api(あるキー)とlocal.db(ないキー、デフォルト値5432)、distinctで重複をなくしたlocal.uniqueを置いて、5つの出力として出力してください。init・applyしたあと、tofu output -jsonを/root/tfb-func/collections.jsonに保存してください。
mergeは、後ろに来たマップが勝ちます。lookupの3つ目の引数は、キーがないときのデフォルト値で、これを抜かすと、ないキーでエラーになります。keysは、並び順を保証しないので、sortで包みます。
条件式で環境ごとの値を選ぶ
/root/tfb-func/cond/main.tfに、デフォルト値がprodの変数envと、var.env == "prod"を入れたlocal.is_prod、それで選ぶlocal.replicas(3または1)、local.log_level(warnまたはdebug)、local.backup(dailyまたはnone)、formatで作ったlocal.endpoint(ポートは443または8080)を置いて、4つの値を키=값の4行で書くapp.confを作成してください(プレースホルダーは、順にキーと値です)。init・applyしたあと、tofu console -var env=devで同じ4つの値を評価して、/root/tfb-func/cond-dev.txtに保存してください。
条件式は、真のときと偽のときの型が同じである必要があります。コンソールに変数の値を与えると、設定を直さなくても、別の分岐の結果を見られるので、適用を繰り返さなくても確認が終わります。
壊れた入力を値として吸収する
/root/tfb-func/guard/に、正しいJSONのgood.json(nameがapi、portが8080)と、壊れたJSONのbad.jsonを作成してください。main.tfで、2つのファイルをtry(jsondecode(...), {})で読み、あるキーとないキーを、それぞれtry(..., "unknown")で取り出し、can(jsondecode(...))で2つのファイルの成否を入れて、5つの出力(name・miss・bad_keys・ok_good・ok_bad)として出力してください。applyしたあと、tofu output -jsonを/root/tfb-func/guard.jsonに保存してください。
tryは、前の引数の評価が失敗したら、次の引数を返し、canは、成功したかどうかを真偽に変えてくれます。どちらも、実行中に生じるエラーだけを捕まえます。宣言すらない参照のような、静的なエラーは捕まえられません。
テンプレートで設定ファイルを出力する
/root/tfb-func/tpl/site.tftplに、名前の1行、ポートのリストを回す繰り返し、TLSかどうかによる条件を含んだテンプレートを書いて、main.tfで、templatefileでname = "api"、ports = [80, 443]、tls = trueを渡して、site.confを作成してください。結果は、server {で始まり、name = api、listen 80;、listen 443;、tls on;、}の6行でなければなりません。
テンプレートの繰り返しと条件は、パーセント記号で始まるディレクティブです。ディレクティブの行が、結果に空行として残らないようにするには、閉じる波括弧の前に、空白除去マーカーを付けます。テンプレートファイルの拡張子は自由ですが、慣例があります。
同じデータを2つの形式で出力する
/root/tfb-func/enc/main.tfに、local.data(name = api、ports = [80, 443]、tls = true)を置いて、jsonencodeでdata.json、yamlencodeでdata.yamlを作成してください。applyしたあと、jq -c .で読んだ結果を/root/tfb-func/enc-json.txtに、yq -o=json -I=0 .で読んだ結果を/root/tfb-func/enc-yaml.txtに保存してください。2つのファイルの内容は、同じである必要があります。
2つの関数は、同じ値を別の表記に移すだけなので、読み直すと同じデータが出てきます。設定ファイルを手でつなぐ代わりに、これらの関数を使えば、引用符とエスケープを、ツールが責任を持ちます。
変換関数とその失敗を確認する
/root/tfb-func/convert-in.txtに6行を書いてください。parseint("ff", 16)、tonumber("007")、tostring(true)、try(tonumber("abc"), -1)、can(tonumber("abc"))、format("%s has %d ports", "api", length([80, 443]))です。/root/tfb-func/playで、コンソールに流し込んで、結果を/root/tfb-func/convert-out.txtに保存してください。
変換関数は、変換できない値に出会うと、エラーを出します。そのエラーを値に変えるのがtryで、真偽に変えるのがcanです。進法が10ではない文字列には、専用の関数が別にあります。
関数を組み合わせて、設定ファイル1つにまとめる
/root/tfb-func/compose/main.tfに、list(string)の変数names(デフォルトapi、web、db)と、list(number)の変数ports(デフォルト8080、80、5432)を置いて、formatlistで이름=포트の行のリストを作り、joinでつなぎ、formatとlengthで# <개수> servicesという見出しを作って、services.confを作成してください(プレースホルダーは、順に名前とポート、件数です)。1行目は見出しで、その後ろがサービスの行です。値を直接書かず、変数からだけ引いてきてください。採点ツールが、別の入力で同じ設定を適用してみます。
formatlistは、複数のリストを並べて回しながら、同じ書式を適用します。見出しの個数は、数えて入れてはじめて、入力が変わっても合います。ここまでくれば、値を1つ変えるために、ファイルを複数箇所直すことがなくなります。