A reference example illustrating how terratest can be used to programmatically analyze Terraform plan output in a CI/CD pipeline.
Tools like OPA can automate Terraform plan analysis via policy-as-code. Such tools seek to replace -- or at least offset -- the toil associated with manual plan analysis. But what if you'd prefer to write Go?
Traditionally, terratest is leveraged as a tool
for authoring Terraform end-to-end tests that make post-terraform apply
assertions on the correctness of the resulting infrastructure.
However, terratest
can also be used to programmatically analyze Terraform
plan output, effectively offering a Go-based alternative to tools like OPA and
similar policy-as-code tools.
This may be especially compelling when the tests need to dynamic evaluate data
returned by cloud provider APIs, for example. In such instances, Go-based terratest
tests can leverage technologies such as the AWS SDK for Go,
or even one of terratest
's built-in modules, such as its aws module.
terratest
-based Terraform plan analysis may also be especially compelling when
terratest
is already used as an end-to-end testing tool.
Example use cases:
-
fail pull request CI if a Teraform change introduces a destructive action against a production-critical resource
-
verify the correctness of the planned DNS record modifications during a Terraform-orchestrated DNS-based blue/green deployment
-
ensure an ECR repository marked for destruction does not home OCI images used by active ECR task definitions
-
"shift left" on detecting problematic PagerDuty Terraform edits, as some terraform-provider-pagerduty errors don't reveal themselves at
plan
time; they only occur during an attempt toapply
. For example:Error: DELETE API call to https://api.pagerduty.com/users/12345 failed 400 Bad Request. Code: 0, Errors: [The user cannot be deleted as they have 1 incident. Please resolve the following incident to continue.], Message:
In such instances, a
terratest
test of the Terraform plan produced by a pull request CI build can use the PagerDuty API to evaluate whether a user-to-be-deleted is assigned open incidents, in advance of merging the pull request and applying the plan.
terratest-tf-plan-demo
offers an example of how terratest
could be
integrated with a CI/CD pipeline. Its test
directory homes a single terratest
test that fails if the Terraform plan it analyzes indicates any destructive
actions.
The main
branch CI/CD pipeline is composed of three passing jobs:
- ✅ terraform-plan - plans the configuration.
- ✅ test-terraform-plan - runs the
terratest
tests homed intest/*_test.go
against the plan produced by the preceding job. - ✅ terraform-apply - gated by
test-terraform-plan
's succcess, as well the configuration specifying this job only run on themain
branch.
PR 2 introduces a change that passes GitHub Actions CI, as its resulting Terraform plan includes no destructive actions. Again, all three jobs pass:
- ✅ terraform-plan - plans the configuration.
- ✅ test-terraform-plan - runs the
terratest
tests homed intest/*_test.go
against the plan produced by the preceding job. - ✋ terraform-apply - gated by
test-terraform-plan
's succcess, as well the configuration specifying this job only run on themain
branch (the workflow is running against a non-main
branch so this job doesn't run).
By contrast, PR 1 introduces a change whose Terraform plan indicates a destructive action. As such, its GitHub Actions CI fails its test-terraform-plan
job:
- ✅ terraform-plan - plans the configuration successfully.
- ❌ test-terraform-plan - runs the
terratest
tests homed intest/*_test.go
against the plan produced by the preceding job. The tests fail in this case, because the plan introduces a destructive action. - ✋ terraform-apply - gated by
test-terraform-plan
's succcess, as well the configuration specifying this job only run on themain
branch.
terratest-tf-plan-demo
assumes you've installed tfenv and Go.
terratest-tf-plan-demo
also assumes Docker is installed and running.
Clone terratest-tf-plan-demo
:
git clone git@github.com:mdb/terratest-tf-plan-demo.git \
&& cd terratest-tf-plan-demo
Run localstack
to simulate AWS APIs locally:
make up
Create a localstack
terratest-demo
S3 bucket and pre-populate the bucket
with a s3://terratest-demo/terraform.tfstate
object used as the the Terraform
remote state for the demo's root module project.
make bootstrap
Run terraform plan
and save the plan to plan.out
:
make plan
Use terraform show
to save the plan.out
to plan.json
:
make show
Run the terratest
tests against the plan.json
file. Note the tests pass:
make test
Introduce a change to the Terraform configuration by renaming null.foo
to be
null.foo_new_name
:
sed -i "" "s/foo/foo_new_name/g" main.tf
After the change, main.tf
should look like this:
resource "null_resource" "foo_new_name" {}
resource "null_resource" "bar" {}
resource "null_resource" "baz" {}
Run terraform plan
and save the plan to plan.out
:
make plan
Use terraform show
to save the plan.out
to plan.json
:
make show
Run the terratest
tests against the plan.json
file. Note this time the tests
fail, as the plan indicates a destructive action:
make test
Undo the changes to main.tf
:
git checkout .
Introduce another change to the Terraform configuration:
echo "resource \"null_resource\" \"foo_new\" {}" >> main.tf
Now, main.tf
should look like:
resource "null_resource" "foo" {}
resource "null_resource" "bar" {}
resource "null_resource" "baz" {}
resource "null_resource" "foo_new" {}
Run terraform plan
and save the plan to plan.out
:
make plan
Use terraform show
to save the plan.out
to plan.json
:
make show
Run the terratest
tests against the plan.json
file. Note this time the tests
pass, as the plan no longer indicates a destructive action:
make test
Tear down localstack
mock AWS environment:
make down