Controlling With Lifecycle and Reverting Drift
Goal
You control replacement, deletion, and ignoring with the four lifecycle arguments, detect changes that arose outside the code and respond in two directions, and then automate that verdict with a script.
Why it matters
The way infrastructure code goes out of step is always the same — something urgent comes up, someone fixes it directly in the console, and that change does not come back into the code. After a few days in that state, an apply run for an entirely different reason quietly reverts the early-morning emergency action. So the truly hard part of responding to drift is not the commands but the judgment that decides the direction. If the emergency action was wrong, revert to the code; if it was a right judgment, fix the code to match the real thing. Running automatic recovery without this judgment is the most dangerous. The lifecycle block is a device that intervenes in this flow in advance. In particular, ignore_changes removes values that intentionally change outside the code, like an autoscaler's, from detection to reduce false alarms, and prevent_destroy puts a brake on resources that must not be deleted. Finally, the verdict must be made with -detailed-exitcode, not with human eyes, for it to be attached to cron and CI.
Steps
- Declare the
local_fileresourceendpointin/root/tf/lifecycle/main.tf. The file path is/root/tf/lifecycle/out/endpoint.txt, and the contents are a one-line string with no newline. Putcreate_before_destroy = truein this resource'slifecycleblock, runtofu init, and apply. - Add the
local_fileresourcestatefile_guard(file/root/tf/lifecycle/out/guard.txt), putprevent_destroy = truein itslifecycle, and apply. Then try to delete only that resource (tofu destroy -target=local_file.statefile_guard) and save the output, including standard error, to/root/tf/lifecycle/out/prevent.txt. The refusal message must remain, andstatefile_guardin the state andguard.txtmust still be alive. - Create one
local_fileresource as the file/root/tf/lifecycle/out/managed-note.txtwith the contentsignored-valueand apply. Then putignore_changes = [content]in itslifecycle, change only the code's content value to a different string, and save thetofu planoutput to/root/tf/lifecycle/out/ignore-plan.txt. The plan must beNo changes, and the actual file contents must still beignored-value. - Make one
null_resourcehold a version value intriggers, and attachreplace_triggered_by = [null_resource.<이름>](where the placeholder is that resource's name) to thelocal_fileresourcechecksum(file/root/tf/lifecycle/out/checksum.txt). Leaving checksum's own arguments as they are, change only the version value, save withtofu plan -out=<계획파일>(where the placeholder is the plan file), and then save the result oftofu show -json <계획파일>to/root/tf/lifecycle/out/trigger-plan.json. Inresource_changesof the JSON, the actions oflocal_file.checksummust be two (delete and create). After checking, apply. - Without going through code, edit
/root/tf/lifecycle/out/endpoint.txtdirectly so that its contents includehand-edited. Then runtofu plan -refresh-only -detailed-exitcode -out=<계획파일>(where the placeholder is the plan file), write only the exit code to/root/tf/lifecycle/out/drift-exit.txt(it must be2), and save the result oftofu show -json <계획파일>to/root/tf/lifecycle/out/drift.json.resource_driftof the JSON must include endpoint. Do not apply this plan. - Revert the real thing to match the code with
tofu applyand save the output to/root/tf/lifecycle/out/remediate.txt. The phraseApply completemust remain,hand-editedmust be gone fromendpoint.txt, and the file contents must be exactly equal to thecontentvalue recorded in the state. - Apply the
local_fileresourcequota(file/root/tf/lifecycle/out/quota.txt) with the content256. This time, edit the file by hand to512, do not revert it, but change the value inmain.tfto512to absorb it into the code, and apply. Then save thetofu planoutput to/root/tf/lifecycle/out/accept-plan.txt; it must beNo changes, and bothquota.txtandmain.tfmust contain512. - Create
/root/tf/lifecycle/drift-report.shand give it execute permission. This script must judge drift with a plan command that uses-detailed-exitcodeand leave the result in/root/tf/lifecycle/out/drift-report.jsonwith four fields:exit_code,drift,checked_at, andaddresses. There must be no drift by the time you finish the lab, soexit_codemust be0anddriftmust befalse.
Notes
- The state file for this lab is
/root/tf/lifecycle/terraform.tfstate. The drift judgment comes from comparing this state with the real thing. -detailed-exitcodeis 0 for no changes, 1 for an error, and 2 for changes present. Thedriftfield must betrueonly when this value is 2.tofu show -json <계획파일>(where the placeholder is the plan file) spits out the plan in a machine-readable form.resource_changesis what will be done, andresource_driftis what has already happened outside the code.- Keep the contents of
local_fileon one line with no newline. Step 6 compares the file contents and the value recorded in the state character by character. - Common mistake 1: putting
set -ein the step 8 script. The script dies on exit code 2 and no report gets made. - Common mistake 2: putting
ignore_changesin first and applying in step 3. The effect shows only if you change the code after the value to ignore has gone into the state. - Common mistake 3: editing the code in step 5 and calling that drift. Drift means only changes that did not go through code.
Apply create_before_destroy
Declare the local_file resource endpoint in /root/tf/lifecycle/main.tf. The file path is /root/tf/lifecycle/out/endpoint.txt, and the contents are a one-line string with no newline. Put create_before_destroy = true in this resource's lifecycle block, run tofu init, and apply.
The lifecycle block goes inside the resource. Find the argument that makes the new one get created first when replacement is needed.
Block deletion with prevent_destroy
Add the local_file resource statefile_guard (file /root/tf/lifecycle/out/guard.txt), put prevent_destroy = true in its lifecycle, and apply. Then try to delete only that resource (tofu destroy -target=local_file.statefile_guard) and save the output, including standard error, to /root/tf/lifecycle/out/prevent.txt. The refusal message must remain, and statefile_guard in the state and guard.txt must still be alive.
This argument produces an error at the plan stage. Try to delete with only one target specified and save the whole output.
Ignore a specific attribute with ignore_changes
Create one local_file resource as the file /root/tf/lifecycle/out/managed-note.txt with the contents ignored-value and apply. Then put ignore_changes = [content] in its lifecycle, change only the code's content value to a different string, and save the tofu plan output to /root/tf/lifecycle/out/ignore-plan.txt. The plan must be No changes, and the actual file contents must still be ignored-value.
You see the effect only if you apply first to put the value into the state and then change the code value. The real file's contents must remain the value from the initial apply.
Trigger recreation through another resource's change
Make one null_resource hold a version value in triggers, and attach replace_triggered_by = [null_resource.<이름>] (where the placeholder is that resource's name) to the local_file resource checksum (file /root/tf/lifecycle/out/checksum.txt). Leaving checksum's own arguments as they are, change only the version value, save with tofu plan -out=<계획파일> (where the placeholder is the plan file), and then save the result of tofu show -json <계획파일> to /root/tf/lifecycle/out/trigger-plan.json. In resource_changes of the JSON, the actions of local_file.checksum must be two (delete and create). After checking, apply.
The key is that it is replaced because of someone else's change even though its own arguments are unchanged. Save the plan to a file and then convert it to JSON.
Detect a change made outside the code as drift
Without going through code, edit /root/tf/lifecycle/out/endpoint.txt directly so that its contents include hand-edited. Then run tofu plan -refresh-only -detailed-exitcode -out=<계획파일> (where the placeholder is the plan file), write only the exit code to /root/tf/lifecycle/out/drift-exit.txt (it must be 2), and save the result of tofu show -json <계획파일> to /root/tf/lifecycle/out/drift.json. resource_drift of the JSON must include endpoint. Do not apply this plan.
It is drift only if you fix the real thing directly without going through code. Use a refresh-only plan together with the detailed exit code.
Revert the real thing to match the code
Revert the real thing to match the code with tofu apply and save the output to /root/tf/lifecycle/out/remediate.txt. The phrase Apply complete must remain, hand-edited must be gone from endpoint.txt, and the file contents must be exactly equal to the content value recorded in the state.
This is the first of the response directions. After reverting, the value recorded in the state and the real contents must be the same.
Absorb the drift into the code
Apply the local_file resource quota (file /root/tf/lifecycle/out/quota.txt) with the content 256. This time, edit the file by hand to 512, do not revert it, but change the value in main.tf to 512 to absorb it into the code, and apply. Then save the tofu plan output to /root/tf/lifecycle/out/accept-plan.txt; it must be No changes, and both quota.txt and main.tf must contain 512.
This time it is the opposite direction. If you judge that the value changed by hand is right, fix the code to that value.
Build a drift detection script and report
Create /root/tf/lifecycle/drift-report.sh and give it execute permission. This script must judge drift with a plan command that uses -detailed-exitcode and leave the result in /root/tf/lifecycle/out/drift-report.json with four fields: exit_code, drift, checked_at, and addresses. There must be no drift by the time you finish the lab, so exit_code must be 0 and drift must be false.
Make the verdict by exit code, not by human eyes. Be careful that set -e does not kill the script at exit code 2.