Terraform/OpenTofu Fundamentals
Reading a Plan and Judging It Automatically
Goal
You handle the plan in two forms, a file and JSON, and have a script judge "a person must look at this plan" based on actions, exit codes, and drift.
Why it matters
The most expensive mistake in infrastructure code is reading a plan wrongly. If you mistake a recreation (-/+) for an in-place update (~), the resource is deleted and created again, and the data in between does not come back. But plan output is long and deployments usually happen at night, so a method that leaves it only to the human eye will surely fail someday. So two devices are needed. First, saving the plan to a file and applying exactly the plan that was reviewed — if you do not save it, what was reviewed and what is applied can differ. Second, extracting the plan in a machine-readable format and judging it by rules — the 0/1/2 of -detailed-exitcode and the resource_changes and resource_drift of the JSON are the materials for that. When this lab is over, you have in hand the materials to put a rule such as "if there is even one deletion, a person must approve" into a pipeline.
Steps
- In
/root/tf/plan, declare two or more resources, including onelocal_fileand oneterraform_data, and initialize. Save the plan to the file/root/tf/plan/plan.tfplan, then convert that plan file to JSON and save it to/root/tf/plan/out/plan.json. There must be 2 or more entries in.resource_changes. - In
plan.json, count the.resource_changes[].change.actionsand create/root/tf/plan/out/actions.json. It is JSON with the three keyscreate,update, anddelete, and the numbers must be exactly the same as the values counted in the plan. - Before applying yet, run the plan with
-detailed-exitcodeattached and write only its exit code to/root/tf/plan/out/exitcode-before.txt. Then apply, run the same command again, and write the exit code to/root/tf/plan/out/exitcode-after.txt. Each of the two files contains just one number. - Change an argument of
local_fileso that a delete-then-recreate appears, and at the same time, interraform_data, change onlyinputso that an in-place update appears. Make a plan in which both changes are inside one plan, and save it as JSON to/root/tf/plan/out/replace.json. - After applying to bring the state in line, fix the content of the managed
local_filedirectly in the shell without going through Terraform. Make a plan in that state and save it as JSON to/root/tf/plan/out/drift.json. In.resource_drift, the address of thatlocal_filemust be caught. - After creating a state with changes pending in two or more places, make a plan that aims at exactly one with
-targetand save it as JSON to/root/tf/plan/out/target.json(exactly 1 change that is notno-op). Leave the warning text the tool issues in the same run in/root/tf/plan/out/target-warning.txt. - Copy
/opt/lab/fixtures/terraform/broken/main.tfto/root/tf/plan/broken/main.tfand run it as it is, and save the error output to/root/tf/plan/out/broken.txt(it must contain the message that the argument namecontentsis the problem). Then copy the same file to/root/tf/plan/fixed/main.tf, fixcontentsto the correct argumentcontent, and save the plan of the fixed configuration as JSON to/root/tf/plan/out/fixed.json. - Finally, in the configuration of
/root/tf/plan, delete one resource entirely to make a plan that includes a deletion, and save it as JSON to/root/tf/plan/out/final.json. Read that JSON and create/root/tf/plan/out/review.json. Include the three numbersadd(the number of changes whose actions contain create),change(the number of changes whose actions are["update"]), anddestroy(the number of changes whose actions contain delete), adestructive_addressesarray holding all the addresses to be deleted, andverdictset toneeds-review.
Notes
- A plan file is not in a human-readable format. Convert it with
terraform show -json plan.tfplanand work with it usingjq. - The exit codes of
-detailed-exitcodeare 0 (no changes), 1 (error), and 2 (changes present). You must read$?right after the command, and in a script withset -eyou have to handle it so that it does not stop early. terraform_datais a resource built into the core without a provider, so it can be used in this offline environment as well. Changing onlyinputis an in-place update, and changingtriggers_replaceis a recreation. Conversely, forlocal_file, changing any argument is a recreation.- The
broken/andfixed/in step 7 are each independent working directories. You must initialize each directory separately for the plan to come out, and the error goes to standard error, so put it in the file together with2>&1. - Common mistake 1: using
-targetas a normal workflow. The reason the tool issues a warning is that only part of the graph is reflected and the state can be left diverged. - Common mistake 2: counting a recreation as one in the summary line. A recreation counts as 1 on both the add and destroy sides, so the numbers in step 8 must also be counted by that standard.
Save the plan to a file and convert it to JSON
In /root/tf/plan, declare two or more resources, including one local_file and one terraform_data, and initialize. Save the plan to the file /root/tf/plan/plan.tfplan, then convert that plan file to JSON and save it to /root/tf/plan/out/plan.json. There must be 2 or more entries in .resource_changes.
There is an option that leaves the plan as a file, and that file is not in a human-readable format. Find the subcommand that converts it to JSON and pass its standard output into a file.
Count by action and record
In plan.json, count the .resource_changes[].change.actions and create /root/tf/plan/out/actions.json. It is JSON with the three keys create, update, and delete, and the numbers must be exactly the same as the values counted in the plan.
The actions of each change come in an array. With jq, just pick and count the entries whose array contains a particular value. All three keys must be present.
Check the two values of -detailed-exitcode
Before applying yet, run the plan with -detailed-exitcode attached and write only its exit code to /root/tf/plan/out/exitcode-before.txt. Then apply, run the same command again, and write the exit code to /root/tf/plan/out/exitcode-after.txt. Each of the two files contains just one number.
The exit code can be read only right after the command ends. If another command gets in between, the value changes, and depending on the shell options the script may also stop early.
Put an in-place update and a recreation in one plan
Change an argument of local_file so that a delete-then-recreate appears, and at the same time, in terraform_data, change only input so that an in-place update appears. Make a plan in which both changes are inside one plan, and save it as JSON to /root/tf/plan/out/replace.json.
For local_file, changing any argument is a recreation. An in-place update needs a different kind of resource, and there is one built into the core without a provider.
Catch a change outside the code as drift
After applying to bring the state in line, fix the content of the managed local_file directly in the shell without going through Terraform. Make a plan in that state and save it as JSON to /root/tf/plan/out/drift.json. In .resource_drift, the address of that local_file must be caught.
After applying to bring the state in line, fix only the product in the shell. In the plan JSON, the planned changes and the changes found in the query are held in different fields.
Narrow the target and read the warning
After creating a state with changes pending in two or more places, make a plan that aims at exactly one with -target and save it as JSON to /root/tf/plan/out/target.json (exactly 1 change that is not no-op). Leave the warning text the tool issues in the same run in /root/tf/plan/out/target-warning.txt.
Aim at just one when two or more changes are pending. Choose a resource to aim at that does not reference other resources — what it depends on gets pulled in along with it.
Read the error of a broken configuration and fix it
Copy /opt/lab/fixtures/terraform/broken/main.tf to /root/tf/plan/broken/main.tf and run it as it is, and save the error output to /root/tf/plan/out/broken.txt (it must contain the message that the argument name contents is the problem). Then copy the same file to /root/tf/plan/fixed/main.tf, fix contents to the correct argument content, and save the plan of the fixed configuration as JSON to /root/tf/plan/out/fixed.json.
The error message tells you the argument name as it is. Error output goes to standard error, so you must pass it along too when you put it in a file.
Build a review report for destructive changes
Finally, in the configuration of /root/tf/plan, delete one resource entirely to make a plan that includes a deletion, and save it as JSON to /root/tf/plan/out/final.json. Read that JSON and create /root/tf/plan/out/review.json. Include the three numbers add (the number of changes whose actions contain create), change (the number of changes whose actions are ["update"]), and destroy (the number of changes whose actions contain delete), a destructive_addresses array holding all the addresses to be deleted, and verdict set to needs-review.
Just count by the same standard as add, change, and destroy in the summary line. Note that a recreation is caught on both the add and destroy sides, and that you must not miss even one address to be deleted.