apply するたびにサービスが再起動した
目標
宣言的なツールの中に付けた副作用(再起動・一回限りの作業)がいつ起きるかを、トリガーで制御し、失敗した作業が残すtainted状態と置き換えの順序を確認したうえで、2回適用テストで冪等性を証明します。
なぜ重要なのか
ファイルを書くことは、何回行っても結果が同じですが、サービスの再起動やデータのマイグレーションは、回るたびに痕跡が残ります。そのため、「何が変わったときだけ回す」という条件が冪等性の核心で、宣言的なツールでは、その条件がトリガーの値として表現されます。トリガーに毎回変わる値を入れると、プランが決して空にならず、CIの2回適用テストが失敗し、失敗した作業はtaintedとして表示されて、次の適用でもう一度回るので、その作業自体がリトライに対して安全でなければなりません。置き換えが、停止と開始の順序で起きることも、無停止設計の出発点です。
ステップ
/root/iac-prov/noisy/main.tfに、port変数(デフォルト8080)、out/app.confにport=<값>を書くlocal_file.conf、そしてtriggers_replace = timestamp()のterraform_data.restartを置いてください(プレースホルダーは値です)。restartは、作成されるときにlocal-execでout/restarts.logにrestartを1行追記します。initしたあと、applyを2回行ってください。採点ツールは、このディレクトリでプランを新しく立てて、毎回変更が出るかを見ます。/root/iac-prov/app/main.tfをステップ1と同じにして、restartのトリガーを、設定ファイルの内容のハッシュ(local_file.confのcontent_sha256)に変更してください。initしたあと、applyを2回行って、プランがきれいかを確認します。採点ツールは、コピーでportを別の値にしたプランを立てて、restartが置き換えられるか、同じ値なら何も変わらないかを見ます。/root/iac-prov/app/out/restarts.logの行数を数えたあと、applyをさらに2回行って、もう一度数え、/root/iac-prov/app/proof.txtにbefore=<수> after=<수>を1行で書いてください(プレースホルダーは件数です)。採点ツールは、コピーでapplyを2回回して、記録が実際に増えないかも確認します。/root/iac-prov/app/terraform.tfvarsにport = 8081を置いて、applyしてください。out/app.confがport=8081になり、restarts.logが、proof.txtのafterよりちょうど1行増えている必要があります。app/main.tfにterraform_data.migrateを追加してください。作成されるときに、local-execで${path.module}/ready.flagがあるかを検査し、あればout/migrate.logにmigratedを1行追記します(なければコマンドが失敗します)。ready.flagがないままapplyして、出力を/root/iac-prov/app/taint.txtに保存し、続けてtofu planの出力を/root/iac-prov/app/taint-plan.txtに保存してください。採点ツールは、コピーでready.flagなしにmigrateを再作成して、taintedになるかも見ます。/root/iac-prov/app/ready.flagを作成して、applyしてください。migrateが置き換えられ、今度は成功する必要があり、そのあとのプランはきれいでなければなりません。restartは、この過程で再び回ってはいけません(restarts.logの行数が、ステップ4のあとと同じ)。app/main.tfにworker_version変数(デフォルトv1)とterraform_data.workerを追加してください。inputとtriggers_replaceは、どちらもその変数で、作成時にout/worker.logにstart <버전>を、when = destroyのプロビジョナーでstop <버전>を追記します(プレースホルダーはバージョンです)。v1で適用したあと、terraform.tfvarsにworker_version = "v2"を追加して、もう一度適用してください。worker.logがstart v1、stop v1、start v2の順序になっている必要があります。/root/iac-prov/twice.sh <작업디렉터리>を作成してください(プレースホルダーは作業ディレクトリです)。applyしたあと、plan -detailed-exitcodeで2回目のプランを確認して、空ならidempotentと0、変更が残っていればnot-idempotentで始まる行と2、applyやplanが失敗したらerrorで始まる行と1で終了します。採点ツールは、/root/iac-prov/appと/root/iac-prov/noisyのコピー、そして設定が壊れた一時ディレクトリで確認します。
参考
- Podには、OpenTofu 1.9.0とlocalプロバイダーのミラーがあるので、インターネットなしで動きます。terraform_dataは組み込みのリソースなので、プロバイダーは不要です。
- プロビジョナーは、作成時(デフォルト)と
when = destroyの2種類で、destroyプロビジョナーはselfしか参照できません。 - よくある間違い: 状態を消したり、コピーを元と混ぜたりして、restarts.logの行数の比較が食い違うこと。採点用のコピーは、採点ツールが別に作ります。
- 公式ドキュメントも、プロビジョナーを最後の手段として勧めています。ここでは、副作用がいつ回るかを目で見るために使います。
- Provisioners・lifecycle・tofu plan・Resource Behavior
適用のたびに再起動が回る様子を見る
/root/iac-prov/noisy/main.tfに、port変数(デフォルト8080)、out/app.confにport=<값>を書くlocal_file.conf、そしてtriggers_replace = timestamp()のterraform_data.restartを置いてください(プレースホルダーは値です)。restartは、作成されるときにlocal-execでout/restarts.logにrestartを1行追記します。initしたあと、applyを2回行ってください。採点ツールは、このディレクトリでプランを新しく立てて、毎回変更が出るかを見ます。
timestamp()は、実行するたびに違う値なので、トリガーがいつも変わります。triggers_replaceが変わると、terraform_dataは置き換えられ、置き換えはすなわち作成なので、作成時のプロビジョナーがもう一度回ります。
設定が変わるときだけ再起動するようにトリガーを設定する
/root/iac-prov/app/main.tfをステップ1と同じにして、restartのトリガーを、設定ファイルの内容のハッシュ(local_file.confのcontent_sha256)に変更してください。initしたあと、applyを2回行って、プランがきれいかを確認します。採点ツールは、コピーでportを別の値にしたプランを立てて、restartが置き換えられるか、同じ値なら何も変わらないかを見ます。
local_fileは、content_sha256のような計算された属性を出力します。トリガーに使う値が、「何が変わったら副作用が必要か」の定義です。
2回さらに適用しても再起動の記録が増えないことを残す
/root/iac-prov/app/out/restarts.logの行数を数えたあと、applyをさらに2回行って、もう一度数え、/root/iac-prov/app/proof.txtにbefore=<수> after=<수>を1行で書いてください(プレースホルダーは件数です)。採点ツールは、コピーでapplyを2回回して、記録が実際に増えないかも確認します。
変更のない適用では、どのリソースも作成されないので、作成時のプロビジョナーも回りません。行数は、wc -l < 파일で数字だけを得られます(プレースホルダーはファイルです)。
設定が本当に変わったら、ちょうど1回だけ再起動する
/root/iac-prov/app/terraform.tfvarsにport = 8081を置いて、applyしてください。out/app.confがport=8081になり、restarts.logが、proof.txtのafterよりちょうど1行増えている必要があります。
terraform.tfvarsは自動的に読み込まれます。プランで、local_file.confとterraform_data.restartが一緒に置き換えられることを確認してください。
失敗したプロビジョナーはリソースをtaintedのまま残す
app/main.tfにterraform_data.migrateを追加してください。作成されるときに、local-execで${path.module}/ready.flagがあるかを検査し、あればout/migrate.logにmigratedを1行追記します(なければコマンドが失敗します)。ready.flagがないままapplyして、出力を/root/iac-prov/app/taint.txtに保存し、続けてtofu planの出力を/root/iac-prov/app/taint-plan.txtに保存してください。採点ツールは、コピーでready.flagなしにmigrateを再作成して、taintedになるかも見ます。
作成時のプロビジョナーが失敗すると、リソース自体は作られていても、「正しく終わっていないもの」として表示され、次のプランはそれを置き換えようとします。tofu state showや、状態のJSONのinstances[].statusでも見えます。
原因を直して再適用すると、1回だけ置き換えられる
/root/iac-prov/app/ready.flagを作成して、applyしてください。migrateが置き換えられ、今度は成功する必要があり、そのあとのプランはきれいでなければなりません。restartは、この過程で再び回ってはいけません(restarts.logの行数が、ステップ4のあとと同じ)。
taintedのリソースは、次の適用で置き換えられます。リトライが安全であるためには、その作業が何回回っても問題ない必要があります。このmigrateが2回回ると何が残るかを、migrate.logで確認してください。
置き換えは、古いものの停止のあとに新しいものの開始
app/main.tfにworker_version変数(デフォルトv1)とterraform_data.workerを追加してください。inputとtriggers_replaceは、どちらもその変数で、作成時にout/worker.logにstart <버전>を、when = destroyのプロビジョナーでstop <버전>を追記します(プレースホルダーはバージョンです)。v1で適用したあと、terraform.tfvarsにworker_version = "v2"を追加して、もう一度適用してください。worker.logがstart v1、stop v1、start v2の順序になっている必要があります。
destroyプロビジョナーは、自分自身(self)しか参照できません。変数を直接使うとエラーです。デフォルト値のcreate_before_destroy=falseでは、古いオブジェクトを先に削除して、新しいオブジェクトを作ります。
CIに入れる2回適用テストを作る
/root/iac-prov/twice.sh <작업디렉터리>を作成してください(プレースホルダーは作業ディレクトリです)。applyしたあと、plan -detailed-exitcodeで2回目のプランを確認して、空ならidempotentと0、変更が残っていればnot-idempotentで始まる行と2、applyやplanが失敗したらerrorで始まる行と1で終了します。採点ツールは、/root/iac-prov/appと/root/iac-prov/noisyのコピー、そして設定が壊れた一時ディレクトリで確認します。
理論で述べた「CIでプレイブックを2回回すチーム」を、宣言的なツールに置き換えたものです。set -eを使うと、2のところで先に終了してしまうので、終了コードを受け取って判断してください。