I have written a lot of Terraform over the years and learned the importance of testing Terraform configurations. Testing is often overlooked but is crucial for ensuring the reliability and correctness of your infrastructure code. In this blog post, I will show you how to write Terraform Tests along with an Azure example.
Why Test Terraform Code?
A solid testing strategy is essential for any form of Infrastructure as Code (IaC). Terraform is no exception.
Implementing a robust test strategy for your Terraform code can:
- Catch errors early in the development process
- Ensure your infrastructure meets specified requirements
- Provide confidence when making changes
- Serve as documentation for expected behavior
Terraform Tests Setup
I do recommend this article from HashiCorp on the terraform test command, its a great overview. I will give a very brief structure of the setup, but do check out the blog post for more detailed explanation
Terraform tests reside in dedicated test files, typically grouped together (I usually bundle a number of tests into the same file. Terraform recognizes these files by their extensions: .tftest.hcl or .tftest.json.
Example folder structure below:
terraform/folder will store the Terraform configuration I want to applytests/folder will store the .tftest.hcl files that will be used byterraform test
terraform/
└── tests/
└── main.tftest.hcl
└── main.tf
└── providers.tf
└── variables.tf
Getting Started with Terraform Tests
Lets start by looking at a basic example, I will be following the same folder structure as above, in main.tf , I will be creating an example.txt file using local_file resource:
# create local example file
resource "local_file" "example" {
content = "This is an example file."
filename = "${path.module}/example.txt"
}
Inside my Terraform tests folder, I have created main.tftest.hcl with tests:
- plan – Checks if file will contain expected content
- plan – Checks if file path will be example.txt
- apply – Confirms file was created at required location
run "verify_local_file_creation" {
command = plan
assert {
condition = local_file.example.content == "This is an example file."
error_message = "File content does not match expected value"
}
assert {
condition = local_file.example.filename == "${path.module}/example.txt"
error_message = "File path does not match expected value"
}
}
run "verify_file_exists" {
command = apply
assert {
condition = fileexists("${path.module}/example.txt")
error_message = "File was not created at expected location"
}
}
Now lets run terraform test
terraform-local-tftest-hcl % terraform test
tests/main.tftest.hcl... in progress
run "verify_local_file_creation"... pass
run "verify_file_exists"... pass
tests/main.tftest.hcl... tearing down
tests/main.tftest.hcl... pass
Success! 2 passed, 0 failed.
Success, both tests have passed!
Full code example in this GitHub respository
Real-World Testing Scenario in Azure
Recently I implemented a disaster recovery solution for Postgresql Flexible server. This solution uses GeoRestore. As part of this, I wrote a number of Terraform tests. I have rewrote some of these tests for this example.
Here is the Terraform resources:
resource "azurerm_resource_group" "tamopsrg" {
name = "tamops-postgres"
location = "Uk South"
}
resource "azurerm_postgresql_flexible_server" "tamopspsql" {
name = "tamops-psqlflexibleserver"
resource_group_name = azurerm_resource_group.tamopsrg.name
location = azurerm_resource_group.tamopsrg.location
version = "16"
administrator_login = "thomas"
administrator_password = "thomasthomas123!"
zone = "2"
storage_mb = 32768
sku_name = "GP_Standard_D4s_v3"
geo_redundant_backup_enabled = true
}
resource "azurerm_postgresql_flexible_server_database" "tamopspsqldb" {
name = "tamopsdb"
server_id = azurerm_postgresql_flexible_server.tamopspsql.id
collation = "en_US.utf8"
charset = "utf8"
}
resource "azurerm_postgresql_flexible_server" "tamopspsqlgeorestore" {
name = "tamops-psqlgeorestore"
resource_group_name = azurerm_resource_group.tamopsrg.name
location = "Uk West"
version = "16"
create_mode = "GeoRestore"
source_server_id = azurerm_postgresql_flexible_server.tamopspsql.id
point_in_time_restore_time_in_utc = timeadd(timestamp(), "5m")
}
.tftest.hcl file that has the tests that Terraform will use:
# main.tftest.hcl
# Test resource group
run "verify_resource_group" {
command = plan
assert {
condition = azurerm_resource_group.tamopsrg.name == "tamops-postgres"
error_message = "Resource group name does not match expected value"
}
assert {
condition = azurerm_resource_group.tamopsrg.location == "uksouth"
error_message = "Resource group location does not match expected value"
}
}
# Test PostgreSQL Flexible Server
run "verify_postgresql_server" {
command = plan
assert {
condition = azurerm_postgresql_flexible_server.tamopspsql.name == "tamops-psqlflexibleserver"
error_message = "PostgreSQL server name does not match expected value"
}
assert {
condition = azurerm_postgresql_flexible_server.tamopspsql.version == "16"
error_message = "PostgreSQL version does not match expected value"
}
assert {
condition = azurerm_postgresql_flexible_server.tamopspsql.zone == "2"
error_message = "Availability zone does not match expected value"
}
assert {
condition = azurerm_postgresql_flexible_server.tamopspsql.storage_mb == 32768
error_message = "Storage size does not match expected value"
}
assert {
condition = azurerm_postgresql_flexible_server.tamopspsql.sku_name == "GP_Standard_D4s_v3"
error_message = "SKU name does not match expected value"
}
assert {
condition = azurerm_postgresql_flexible_server.tamopspsql.geo_redundant_backup_enabled == true
error_message = "Geo-redundant backup setting does not match expected value"
}
}
# Test PostgreSQL Database
run "verify_postgresql_database" {
command = plan
assert {
condition = azurerm_postgresql_flexible_server_database.tamopspsqldb.name == "tamopsdb"
error_message = "Database name does not match expected value"
}
assert {
condition = azurerm_postgresql_flexible_server_database.tamopspsqldb.collation == "en_US.utf8"
error_message = "Database collation does not match expected value"
}
assert {
condition = azurerm_postgresql_flexible_server_database.tamopspsqldb.charset == "utf8"
error_message = "Database charset does not match expected value"
}
}
# Test Geo-Restore Server
run "verify_postgresql_georestore" {
command = plan
assert {
condition = azurerm_postgresql_flexible_server.tamopspsqlgeorestore.name == "tamops-psqlgeorestore"
error_message = "Geo-restore server name does not match expected value"
}
assert {
condition = azurerm_postgresql_flexible_server.tamopspsqlgeorestore.location == "ukwest"
error_message = "Geo-restore server location does not match expected value"
}
assert {
condition = azurerm_postgresql_flexible_server.tamopspsqlgeorestore.create_mode == "GeoRestore"
error_message = "Create mode does not match expected value"
}
}
Running terraform test, we can see successful test output:
terraform test
tests/main.tftest.hcl... in progress
run "verify_resource_group"... pass
run "verify_postgresql_server"... pass
run "verify_postgresql_database"... pass
run "verify_postgresql_georestore"... pass
tests/main.tftest.hcl... tearing down
tests/main.tftest.hcl... pass
Full code example in this GitHub respository
Wrapping up
Writing Terraform tests as part of your deployment can save numerous hours of debugging and even potential production issues. Start small, focus on critical resources, and gradually build up your test coverage.
Remember, even small steps towards comprehensive testing can yield substantial benefits in the long run.
Happy testing!