Terraform Series
- Part 1: Introduction
- Part 2: Creating the Server
- Part 3: Provisioning the Server
- Part 4: Managing Terraform State
- Part 5: Cleaner Code with Terraform Modules
- Part 6: Loops with Terraform
- Part 7: Conditionals with Terraform
- Part 8: Testing Terraform Code
We conclude the series by putting a cherry on top: Testing Terraform code.
Manual Testing vs. Automated Testing
Testing is a crucial part of CI/CD cycles. Since the DevOps movement is fundamentally for delivering software, what could be more important than testing infrastructure-as-code?
Here we are going to mention two strategies. Manual testing will help us test our infrastructure code by running it on the cloud infrastructure. Automated testing will allow us to automate these manual tests by using a Go-based library: Terratest.
One thing to keep in mind is that there is no local environment for testing Terraform code since it's mostly dependent on third party services like AWS, GCP, DigitalOcean, etc. That's why there is always be resource creation & deletion in the cloud.
Manual Testing
The following statement might seem like a no-brainer, but we've already done many manual testing throughout this series. Running terraform plan
and terraform apply
consecutively were merely manual tests. We write the code, and then we apply it to the cloud. If everything goes well and the changes are made, our manual testing was a success.
There is a rule of thumb to make our codes more testable, though. Take a look at this resource creation example from Part 2 below. Its purpose is to declare a droplet (server):
resource "digitalocean_droplet" "web" {
image = "ubuntu-20-04-x64"
name = "terraform-sandbox"
region = "ams3"
size = "s-1vcpu-1gb"
}
The declarative code we see above is very much in a frozen state. No matter how you want to test it, it's going to create a single CPU machine with 1 GB of RAM running Ubuntu 20.04 in Amsterdam's AMS3 region. However, it would be better if we could test this piece of code with different inputs.
For that, we could apply our knowledge from Part 5 and make them variables like below. Thus, we'll be able to test our code with different inputs manually.
resource "digitalocean_droplet" "web" {
image = var.droplet_web_image
name = var.droplet_web_name
region = var.droplet_web_region
size = var.droplet_web_size
}
variable "var.droplet_web_image" {
description = "Web droplet's image"
type = string
}
variable "var.droplet_web_name" {
description = "Web droplet's name"
type = string
}
variable "var.droplet_web_region" {
description = "Web droplet's region"
type = string
}
variable "var.droplet_web_size" {
description = "Web droplet's size"
type = string
}
Note that it's a better idea to encapsulate your values in a different file, as we talked about in Part 5. The example above is only a simplified version.
Automated Testing
Manual testing is a useful and important part of testing infrastructure-as-code (IaC). However, a test should be run as frequently as possible. That's why an automated test is much more beneficial in the long run.
Automated testing of the Terraform code is nothing but a collection of examples of manual testing. There is an excellent open-source tool called Terratest written in Go, which helps you do exactly that. You write examples for your Terraform code, and then you use Terratest to initialize and apply your IaC. You can learn more about how to start using the tool Terratest via their documentation, but here, I'd like to show you how an example test looks like.
The example below is not directly related to DigitalOcean, because Terratest support for DigitalOcean is still yet to come at the time of this writing. However, you can always use Terratest extensively for generic Terraform code as well as other parts by making use of outputs.
Very briefly from official Terratest docs, let's say we have this output resource in the main.tf
file under the examples
directory:
output "hello_world" {
value = "Hello World!"
}
Then, a simple automated test written with Terratest looks like this:
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)
func TestTerraformHelloWorldExample(t *testing.T) {
// retryable errors in terraform testing.
terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
TerraformDir: "../examples",
})
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
output := terraform.Output(t, terraformOptions, "hello_world")
assert.Equal(t, "Hello World!", output)
}
Terratest follows the same testing guidelines as Go. Hence, it's a good idea to save your test file as *_test.go
and name your test functions with Test
prefixes like in our example above: TestTerraformHelloWorldExample
. Then you can run go test
to start your test suite.
Let's go through the test code now:
terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{TerraformDir: "../examples",})
Since Terraform is all about dealing with third-party providers, we should also consider network errors. If a test fails on one occasion due to a network error, it doesn't mean it will fail the second time. That's why retrying tests automatically helps a lot. Terratest makes it easy to do that with WithDefaultRetryableErrors
. We enable the retrying and also define the directory for our Terraform code above.
defer terraform.Destroy(t, terraformOptions)
While testing the Terraform code, we create real resources on the cloud. When the test is done, we should destroy them to avoid unnecessary costs and future test dependencies. Go's defer
keyword here helps us to run this destroy keyword at the end. By starting our code with the destroy directive, we make sure Go will destroy any resources created; because even if we face errors during the test, Go will tear it down since it already has the deferred directive.
terraform.InitAndApply(t, terraformOptions)
Basically, this directive initializes the resources defined in the Terraform code on a temporary directory and then applies it.
output := terraform.Output(t, terraformOptions, "hello_world")
assert.Equal(t, "Hello World!", output)
The last part of our test code is the assertion of the expected and actual values. Our expected output resource is "Hello World!" while we get the actual value directly from the Terraform output by using terraform.Output
.
Cover photo by Chris Liverani
Part 7...................................................................................................................
Top comments (0)