feat(blog): add opentofu/terraform module testing
All checks were successful
/ all (push) Successful in 28s
All checks were successful
/ all (push) Successful in 28s
This commit is contained in:
parent
f67eb029a9
commit
8a303476d7
1 changed files with 188 additions and 0 deletions
188
content/blog/terraform/testing.md
Normal file
188
content/blog/terraform/testing.md
Normal file
|
@ -0,0 +1,188 @@
|
|||
---
|
||||
title: 'OpenTofu/Terraform module testing'
|
||||
description: 'Infrastructure testing is fun!'
|
||||
date: '2025-04-26'
|
||||
tags:
|
||||
- 'aws'
|
||||
- 'OpenTofu'
|
||||
- 'terraform'
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
For the last two months, I finally got around to playing with OpenTofu tests.
|
||||
Infrastructure tests have been teasing me for such a long time! Having mostly
|
||||
dealt with terraform mono-repos in my career, this was never a big deal because
|
||||
it is easy to test on just part of an existing environment and therefore never
|
||||
became a priority.
|
||||
|
||||
In the last six months I started building a multi repository system and
|
||||
thoroughly experimented with testing modules. Here are my thoughts on testing
|
||||
infrastructure code and a few examples.
|
||||
|
||||
## Static checks
|
||||
|
||||
Static checks are cheap and useful. Before running terraform tests, [my CI
|
||||
action](https://git.adyxax.org/adyxax/action-tofu-aws-test) runs the following:
|
||||
|
||||
- a `tofu fmt` check to ensure files are properly formatted.
|
||||
- a `tflint` check to lint the tofu code.
|
||||
- a `tofu providers lock` to check that the provider locks files all have the
|
||||
required platform signatures.
|
||||
|
||||
Only after all these steps succeed do I run `tofu test`.
|
||||
|
||||
## Testing OpenTofu/Terraform code
|
||||
|
||||
The OpenTofu/Terraform [test
|
||||
command](https://opentofu.org/docs/cli/commands/test/) lets you test a
|
||||
configuration by creating real infrastructure. This means one or more
|
||||
instantiations of the module in need of testing, as well as support
|
||||
infrastructure or resources.
|
||||
|
||||
Once the test infrastructure is created, assertions are performed to check that
|
||||
all the conditions relevant to the module's behavior are met. This involves
|
||||
checking the module's outputs but one usually also use the outputs of various
|
||||
data-sources.
|
||||
|
||||
Once the test is complete, OpenTofu destroys the resources it created. Know
|
||||
already that this can fail if the test is not written correctly. All terraform
|
||||
failures are properly handled, but a common failure case I met was when using
|
||||
the
|
||||
[external](https://registry.terraform.io/providers/hashicorp/external/latest/docs)
|
||||
data-source to run some shell commands and capture their outputs.
|
||||
|
||||
## Basic example
|
||||
|
||||
The simplest form of tofu test is to create a `main.tftest.hcl` file inside a
|
||||
module. Here is an example for [a module that creates an AWS IAM
|
||||
role](https://git.adyxax.org/adyxax/tofu-module-aws-iam-role):
|
||||
|
||||
``` hcl
|
||||
provider "aws" {
|
||||
profile = "tests"
|
||||
region = "eu-west-3"
|
||||
}
|
||||
|
||||
run "main" {
|
||||
assert {
|
||||
condition = output.arn != null
|
||||
error_message = "invalid IAM role ARN"
|
||||
}
|
||||
}
|
||||
|
||||
variables {
|
||||
name = "tftest-role"
|
||||
}
|
||||
```
|
||||
|
||||
The module is quite simple: its only purpose being to add a bunch of policy
|
||||
entries. Testing the correct provisioning of the role would be way more code
|
||||
than the role itself, so this simple test only does a dummy check to confirm
|
||||
that all the module's resources instantiate properly.
|
||||
|
||||
## More elaborate example
|
||||
|
||||
A more complete example that actually tests the correct behavior is what I do
|
||||
with [a module that creates an AWS IAM
|
||||
user](https://git.adyxax.org/adyxax/tofu-module-aws-iam-user). Here the test is
|
||||
to log in with the user's access key and check its identity.
|
||||
|
||||
The `main.tftest.hcl` is simpler because it relies on a support module:
|
||||
|
||||
``` hcl
|
||||
provider "aws" {
|
||||
profile = "tests"
|
||||
region = "eu-west-3"
|
||||
}
|
||||
|
||||
run "main" {
|
||||
assert {
|
||||
condition = data.external.main.result.Arn == local.expected_arn
|
||||
error_message = "user ARN mismatch"
|
||||
}
|
||||
module {
|
||||
source = "./test"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The [support
|
||||
module](https://git.adyxax.org/adyxax/tofu-module-aws-iam-user/src/branch/main/test)
|
||||
contains multiple files. The most important one is `main.tf`:
|
||||
|
||||
``` hcl
|
||||
module "main" {
|
||||
source = "../"
|
||||
|
||||
name = "tftest-user"
|
||||
}
|
||||
|
||||
data "aws_caller_identity" "current" {}
|
||||
|
||||
# tflint-ignore: terraform_unused_declarations
|
||||
data "external" "main" {
|
||||
program = ["${path.module}/test.sh"]
|
||||
|
||||
depends_on = [local_file.aws_config]
|
||||
}
|
||||
|
||||
locals {
|
||||
# tflint-ignore: terraform_unused_declarations
|
||||
expected_arn = format(
|
||||
"arn:aws:iam::%s:user/tftest-user",
|
||||
data.aws_caller_identity.current.account_id,
|
||||
)
|
||||
}
|
||||
|
||||
resource "local_file" "aws_config" {
|
||||
filename = "${path.module}/aws_config"
|
||||
file_permission = "0600"
|
||||
content = templatefile("${path.module}/aws_config.tftpl", {
|
||||
aws_access_key_id = module.main.access_key_id
|
||||
aws_access_key_secret = module.main.access_key_secret
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
The module `main` is the instantiation of the module we are testing. The other
|
||||
resources are here to allow a login via the AWS CLI in order to test the access.
|
||||
Note the `tflint-ignore` directives: They are annoying but needed since tflint
|
||||
does not know how to reconcile that these are used in of `main.tftest.hcl` file.
|
||||
|
||||
The test relies on a `aws_config.tftpl` file containing:
|
||||
|
||||
``` ini
|
||||
[default]
|
||||
aws_access_key_id = ${aws_access_key_id}
|
||||
aws_secret_access_key = ${aws_access_key_secret}
|
||||
region = eu-west-3
|
||||
```
|
||||
|
||||
It also relies on this script:
|
||||
|
||||
``` shell
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Wait a bit for the ACCESS KEY to be usable on AWS
|
||||
sleep 10
|
||||
|
||||
export AWS_CONFIG_FILE="${PWD}/test/aws_config"
|
||||
aws sts get-caller-identity
|
||||
```
|
||||
|
||||
Note that the `external` data-source works with scripts that take JSON input and
|
||||
JSON output. Luckily this is what the AWS CLI outputs by default, but if you
|
||||
changed it you will have to tweak this.
|
||||
|
||||
## Conclusion
|
||||
|
||||
Writing module tests is worthwhile, even if just to validate the proper
|
||||
instantiation of a module in its most used configurations. I am now validating
|
||||
that I can properly spawn VPCs, databases, load balancers, generate
|
||||
certificates... I have never felt more confident in my OpenTofu/Terraform code!
|
||||
|
||||
Though I have found that writing the tofu testing code was not the hardest part:
|
||||
making it all work in CI was. In a next article I will present the test
|
||||
infrastructure I use to run all this.
|
Loading…
Add table
Add a link
Reference in a new issue