Terratest es una biblioteca de Go de Gruntwork para probar código de infraestructura de verdad: el test ejecuta terraform init y terraform apply, comprueba el resultado y destruye los recursos al terminar. En este tutorial instalarás Go y Terraform en Ubuntu 24.04, crearás un pequeño módulo de Terraform, escribirás un test con Terratest que lo despliega y valida sus salidas, y lo ejecutarás en un pipeline de GitHub Actions.
El módulo de ejemplo usa el proveedor local de HashiCorp, que crea archivos en disco, así que puedes seguir la guía sin credenciales de ningún proveedor cloud. El mismo patrón sirve después para módulos que crean servidores, redes o registros DNS reales.
Requisitos previos
- Un servidor o equipo con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath.
- Un usuario no root con privilegios
sudo. gitycurlinstalados (sudo apt install git curl).- Conocimientos básicos de Terraform (recursos, variables y salidas).
Paso 1: Instalar Go
Terratest sigue las versiones recientes de Go, y el paquete golang-go de Ubuntu 24.04 (Go 1.22) suele quedarse corto. Instala la última versión estable desde go.dev. Primero consulta cuál es:
GO_VERSION=$(curl -fsSL 'https://go.dev/VERSION?m=text' | head -n1)
echo "$GO_VERSION"
go1.27.1
Descarga el archivo para tu arquitectura (amd64 en la mayoría de servidores, arm64 en ARM) y descomprímelo en /usr/local:
curl -fsSLO "https://go.dev/dl/${GO_VERSION}.linux-amd64.tar.gz"
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf "${GO_VERSION}.linux-amd64.tar.gz"
Añade Go al PATH de tu usuario y recarga el perfil:
echo 'export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin' >> ~/.profile
source ~/.profile
Comprueba la instalación:
go version
go version go1.27.1 linux/amd64
Paso 2: Instalar Terraform
Instala Terraform desde el repositorio oficial de HashiCorp. Descarga la clave de firma en /etc/apt/keyrings:
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/hashicorp.gpg
Añade el repositorio e instala el paquete:
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update
sudo apt install terraform
Verifica la versión:
terraform version
Terraform v1.16.4
on linux_amd64
Paso 3: Crear el módulo de Terraform que vas a probar
Crea la estructura del proyecto. Terratest espera que los tests vivan en un directorio aparte (por convención test/) que apunta al código de Terraform:
mkdir -p ~/terratest-demo/{module,test}
cd ~/terratest-demo
Crea el módulo:
nano module/main.tf
terraform {
required_version = ">= 1.5"
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
variable "environment" {
type = string
description = "Nombre del entorno"
}
variable "output_dir" {
type = string
description = "Directorio donde se escribe el archivo de configuración"
}
resource "local_file" "app_config" {
filename = "${var.output_dir}/app-${var.environment}.conf"
content = "environment=${var.environment}\n"
file_permission = "0640"
}
output "config_path" {
value = local_file.app_config.filename
}
output "environment" {
value = var.environment
}
El módulo recibe dos variables, crea un archivo y expone su ruta como salida. Comprueba que la sintaxis es válida antes de escribir el test:
cd module && terraform init -backend=false && terraform validate && cd ..
Success! The configuration is valid.
Paso 4: Inicializar el módulo de Go e instalar Terratest
Los tests son código Go normal, así que necesitan su propio go.mod. Inicialízalo dentro de test/:
cd ~/terratest-demo/test
go mod init github.com/your_user/terratest-demo/test
Añade Terratest como dependencia. Pide el módulo raíz (github.com/gruntwork-io/terratest) y no un subpaquete concreto como modules/terraform, porque este último da un error de ambiguous import:
go get github.com/gruntwork-io/terratest@latest
go: added github.com/gruntwork-io/terratest v1.0.1
testify, la biblioteca de aserciones que usarás en el test, se añadirá automáticamente en el siguiente paso con go mod tidy.
Sustituye your_user por tu usuario u organización de GitHub. La ruta del módulo solo sirve para identificarlo; no hace falta que el repositorio exista todavía.
Paso 5: Escribir el primer test
Crea el archivo del test. En Go los tests viven en archivos que terminan en _test.go y las funciones empiezan por Test:
nano module_test.go
package test
import (
"os"
"path/filepath"
"testing"
"github.com/gruntwork-io/terratest/modules/random"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAppConfigModule(t *testing.T) {
t.Parallel()
// Nombre único para que varios tests en paralelo no choquen.
environment := "test-" + random.UniqueId()
outputDir := t.TempDir()
opts := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
TerraformDir: "../module",
Vars: map[string]interface{}{
"environment": environment,
"output_dir": outputDir,
},
NoColor: true,
})
// Destroy se ejecuta al final aunque el test falle.
defer terraform.Destroy(t, opts)
terraform.InitAndApply(t, opts)
// Validar las salidas del módulo.
configPath := terraform.Output(t, opts, "config_path")
assert.Equal(t, filepath.Join(outputDir, "app-"+environment+".conf"), configPath)
assert.Equal(t, environment, terraform.Output(t, opts, "environment"))
// Validar el recurso real, no solo el estado de Terraform.
content, err := os.ReadFile(configPath)
require.NoError(t, err)
assert.Equal(t, "environment="+environment+"\n", string(content))
info, err := os.Stat(configPath)
require.NoError(t, err)
assert.Equal(t, os.FileMode(0o640), info.Mode().Perm())
}
Los puntos clave de este patrón son:
terraform.Optionsindica dónde está el código (TerraformDir) y qué variables pasarle.WithDefaultRetryableErrorsreintenta errores transitorios conocidos, como fallos al descargar proveedores.defer terraform.Destroygarantiza la limpieza. Sin él, un test fallido deja recursos creados (y, en un cloud real, facturando).- El test comprueba el recurso real (el archivo en disco), no solo lo que Terraform cree que ha creado.
Descarga las dependencias que usa el test (incluida testify) y ordena go.mod:
go mod tidy
Paso 6: Ejecutar el test
Ejecuta el test con salida detallada. Sube el tiempo máximo, porque el valor por defecto de Go (10 minutos) se queda corto en cuanto el módulo crea infraestructura real:
go test -v -timeout 30m ./...
Terratest muestra cada comando de Terraform que ejecuta. El final de la salida debería ser parecido a este:
TestAppConfigModule 2026-09-25T10:12:41Z logger.go:67: Destroy complete! Resources: 1 destroyed.
--- PASS: TestAppConfigModule (4.87s)
PASS
ok github.com/your_user/terratest-demo/test 4.901s
Para comprobar que el test detecta errores de verdad, cambia temporalmente file_permission a "0644" en module/main.tf y vuelve a ejecutarlo:
Error: Not equal:
expected: 0x1a0
actual : 0x1a4
--- FAIL: TestAppConfigModule (4.62s)
Deja de nuevo el valor en "0640" antes de continuar. Fíjate en que, aunque el test falla, la salida también muestra Destroy complete!: la limpieza se ha ejecutado igualmente.
TipPara ejecutar un único test de un paquete con muchos, usa
go test -v -timeout 30m -run TestAppConfigModule ./....
Paso 7: Probar módulos que crean servicios de red
Con infraestructura real no basta con leer salidas: hay que comprobar que el servicio responde. Terratest incluye el paquete http_helper, que reintenta una petición hasta que devuelve el código y el cuerpo esperados. Suponiendo un módulo que devuelve la IP pública de un servidor web en la salida public_ip, la validación quedaría así dentro del test:
import (
"fmt"
"time"
http_helper "github.com/gruntwork-io/terratest/modules/http-helper"
)
// ...después de terraform.InitAndApply(t, opts)
publicIP := terraform.Output(t, opts, "public_ip")
url := fmt.Sprintf("http://%s", publicIP)
// 30 intentos separados 10 segundos: el servidor puede tardar en arrancar.
http_helper.HttpGetWithRetry(t, url, nil, 200, "Hello, World!", 30, 10*time.Second)
Los reintentos son necesarios porque un apply termina cuando el proveedor crea la máquina, no cuando el servicio que corre dentro está listo. Para módulos de este tipo usa siempre un proyecto o cuenta de pruebas separado de producción, y nombres con random.UniqueId() para que dos ejecuciones simultáneas no colisionen.
Paso 8: Ejecutar los tests en GitHub Actions
Sube el proyecto a un repositorio de GitHub y crea el workflow:
mkdir -p ~/terratest-demo/.github/workflows
nano ~/terratest-demo/.github/workflows/terratest.yml
name: terratest
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: test/go.mod
cache-dependency-path: test/go.sum
- uses: hashicorp/setup-terraform@v3
with:
terraform_wrapper: false
- name: Run Terratest
working-directory: test
run: go test -v -timeout 30m ./...
terraform_wrapper: false es importante: el wrapper que instala setup-terraform por defecto añade texto a la salida de los comandos y Terratest no puede leer bien los terraform output. Si tus módulos usan un proveedor cloud real, pasa sus credenciales como secretos del repositorio en la sección env del paso de test.
Tras hacer push, la pestaña Actions del repositorio mostrará el job test con el mismo --- PASS que viste en local.
Solución de problemas
panic: test timed out after 10m0s: falta-timeout 30m(o un valor mayor) engo test.- El test falla leyendo salidas en CI con caracteres extraños: activa
terraform_wrapper: falseenhashicorp/setup-terraform. - Quedan recursos creados tras un fallo: asegúrate de que
defer terraform.Destroyestá justo antes deInitAndApply, no después. Si el proceso se mata (por ejemplo, cancelando el job),deferno llega a ejecutarse; revisa la cuenta de pruebas periódicamente. Error: Failed to query available provider packages: problema de red al descargar proveedores.WithDefaultRetryableErrorsreintenta estos casos; si persiste, comprueba el acceso aregistry.terraform.io.
Conclusión
Has creado un módulo de Terraform, un test de Terratest que lo despliega, valida tanto las salidas como el recurso real y lo destruye siempre al terminar, y un pipeline que lo ejecuta en cada pull request. Como siguientes pasos, puedes separar las fases de despliegue y validación con el paquete test_structure para acelerar la depuración de tests largos, añadir terraform validate y tflint como comprobaciones rápidas antes de Terratest, y aplicar el mismo enfoque a manifiestos de Kubernetes con el paquete k8s de Terratest.
