From 8a303476d7cfe28a2b908d4ec432e90d23d699ca Mon Sep 17 00:00:00 2001 From: Julien Dessaux Date: Sat, 26 Apr 2025 18:17:34 +0200 Subject: [PATCH] feat(blog): add opentofu/terraform module testing --- content/blog/terraform/testing.md | 188 ++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 content/blog/terraform/testing.md diff --git a/content/blog/terraform/testing.md b/content/blog/terraform/testing.md new file mode 100644 index 0000000..891e7d4 --- /dev/null +++ b/content/blog/terraform/testing.md @@ -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.