変数の優先順位、出力、そして機密値の錯覚
一言でいうと
同じコードでdev・stage・prodをまかなうには、値がコードの外に出る必要があります。問題は、値を入れられる場所が複数あり、その間に決まった強弱があることです。
なぜ必要なのか
環境が3つで、違うのはインスタンスのサイズとレプリカ数だけだとします。最も簡単な解決策は、ディレクトリを3つコピーすることです。そして半年後、3つのディレクトリは、互いに別の生き物になっています。prodにだけ入ったセキュリティ設定がstageにはなく、devで直したバグがprodには反映されていません。事故は、いつもその隙間で起きます。
変数は、このコピーを防ぐ仕組みです。コードは1つにして、値だけを差し替えれば、環境間の違いが「3つのファイルのdiff」から「値いくつか」に減ります。すると、人が違いを目で確認できるようになります。
どう動くのか
値を入れる場所は複数あり、あとに来るものが勝ちます。
| 強さ | 場所 | 性格 |
|---|---|---|
| 最も弱い | 変数宣言のdefault |
「誰も与えなければこの値」 |
| 弱い | TF_VAR_이름環境変数 |
CIランナーやシェルから注入 |
| 中程度 | terraform.tfvars |
このディレクトリの標準の値 |
| やや強い | *.auto.tfvars(ファイル名の辞書順) |
自動的に一緒に読み込まれる値 |
| 強い | -var-file=파일 |
今回の実行に選んだ値のまとまり |
| 最も強い | -var 이름=값 |
今回1回限りの例外 |
表のコードに含まれる韓国語のプレースホルダーは、名前・ファイル・値を表します。
変数宣言には、値のほかに付けられるものが3つあります。typeは、誤った形を早い段階で防ぎ、descriptionは、この値が何かを知らせ、validationは、許容範囲をコードで固定します。validationの本当の価値は、失敗する時点を早めることです。検証がなければ、誤った値が適用の途中でプロバイダーのエラーとして爆発し、そのときはすでに半分が作られた状態です。
localsは変数に見えますが、性格が違います。外から注入できない、名前を付けた式です。複数の変数を組み合わせた名前の接頭辞のように、1回定義して複数の場所で使いたい値に使います。
outputは、この設定の公開インターフェースです。モジュールとして使われるときは、他のコードが参照する唯一の窓口なので、すべての出力にdescriptionを付けるのがよいです。そして、-jsonオプションを付けると、機械が読む形式で出力されて、パイプラインで使いやすいです。
最後にsensitive = trueです。これは出力を画面で隠すだけです。状態ファイルには値が平文で残り、output -jsonにも値がそのまま入っています。この事実を知らないと、「機密の指定をしたから安全だ」という錯覚で、状態ファイルをどこにでも置いてしまいます。
現場での姿
1つ目は、-varで消して、忘れた火です。障害対応中に-var replicas=10でサービスを救ったなら、その値は必ずリポジトリにコミットされる必要があります。そうしないと、次の通常のデプロイが、静かに以前の値に戻します。コマンドラインの値が最も強いという性質は、緊急時に役立つ分、記録が残らないという代償も一緒に支払います。
2つ目は、デフォルト値の落とし穴です。「とりあえず動くように」するために、すべての変数にデフォルト値を入れると、値を与えなかったときに、静かに開発用の設定で本番が作られます。環境ごとに必ず違う必要がある値には、デフォルト値を与えないほうが安全です。ツールは、値がなければ尋ねるか失敗しますが、その失敗は、事故よりずっと安くつきます。
3つ目は、プランのログに残る秘密情報です。planの出力には、リソースの属性がそのまま出力されます。CIのログを社内の誰でも見られる場所にためておけば、それがそのまま流出経路です。機密の指定とは別に、ログの保管ポリシーも一緒に見る必要がある理由です。
値がどこから来るかをはっきりさせる
変数1つに値を与えられる経路が6つあります。優先順位を知らないと、「確かに変えたのに変わらない」を経験します。下にいくほど強いです。
- デフォルト値(
default) - 環境変数
TF_VAR_이름 terraform.tfvars*.auto.tfvars(ファイル名の順序)-var-file=で与えたファイル-var=で直接与えた値
同じファイルの中で2回書くとエラーですが、別の経路なら静かに上書きされます。 CIがTF_VAR_を仕込んであるのに、ローカルでterraform.tfvarsを直しているなら、3つ目が2つ目に勝つので、ローカルでは動いて、CIでは動かないことになります。値がどこから来たかを確認するには、プランをファイルとして取得して読みます。
terraform plan -out=tfplan
terraform show -json tfplan | jq '.variables'
検証は、変数の宣言に付けます。 誤った値をapplyの途中で見つけたら、すでに半分は作られたあとです。
variable "env" {
type = string
validation {
condition = contains(["dev", "stg", "prod"], var.env)
error_message = "env 는 dev, stg, prod 중 하나여야 합니다."
}
}
sensitive = trueは、画面だけを隠します。 状態ファイルとプランファイルには、値がそのまま入ります。秘密情報は、変数で渡さずに、シークレットストアからデータソースで読むほうがよく、それでも状態には残ることを前提に、リポジトリを扱います。
出力は、次の人に残すインターフェースです。 モジュールの出力は、そのモジュールを使う側が依存するようになるので、一度決めたら変えにくいです。内部で使うリソース名をそのまま出力する代わりに、意味で名前を付けます。instance_idよりapp_server_idが、ipよりprivate_endpointが、長持ちします。
terraform output -json | jq -r '.app_endpoint.value'
次のラボですること
/root/tf/varsで、同じ変数に、デフォルト値・tfvars・コマンドライン・環境変数の4つの方法で値を入れて、どれが勝つかをファイルで確認します。validationで誤った値を拒否させて、そのメッセージを保存します。localsで名前の接頭辞を組み合わせ、説明が付いた出力を4つ以上作ります。最後に、機密の変数を宣言して、人が見る出力では隠されるのに、JSONには値が残るという事実を目で確認します。