TT Lab
Get started
Learn Learning paths Courses

Terraform/OpenTofu Fundamentals

Reading a Plan and Judging It Automatically

Continue in TT Lab

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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.

Notes

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.