1. Código de infraestrutura também quebra
Nos dois artigos anteriores desta série, vimos o OpenTofu como alternativa para provisionar infraestrutura e o Ansible para configurá-la depois de criada. Mas há uma pergunta que fica no ar em qualquer um desses fluxos: como saber, antes de rodar apply em produção, que um módulo Terraform não vai abrir uma porta que não deveria, destruir um recurso por engano, ou simplesmente ter um erro de sintaxe? Testar infraestrutura como código é tão importante quanto testar qualquer outro software — só que, diferente de uma função pura, os "efeitos colaterais" de um teste malfeito aqui podem ser uma conta de nuvem inesperada ou um serviço em produção fora do ar.
Este artigo fecha a série cobrindo três camadas complementares de teste: análise estática com tflint, verificação de segurança e compliance com checkov, e testes de integração de verdade com Terratest.
2. As camadas de teste em IaC
Vale pensar nessas ferramentas como camadas que rodam em momentos diferentes do ciclo de vida do código, da mais rápida/barata para a mais lenta/cara:
-
Lint e análise estática (tflint): roda em segundos, sem precisar de credenciais de nuvem nem de rodar
terraform plan. Pega erros de sintaxe, más práticas e problemas específicos de cada provider. -
Análise de segurança e compliance (checkov): também estática, mas focada em identificar configurações inseguras (bucket público, criptografia desabilitada, security group aberto para
0.0.0.0/0) comparando o código contra um catálogo de políticas. -
Testes de integração (Terratest): a camada mais próxima da realidade — de fato roda
terraform applynum ambiente isolado, valida o resultado, e depois rodaterraform destroy. Mais lento e mais caro (usa recursos reais de nuvem), mas é o único jeito de garantir que o módulo realmente funciona de ponta a ponta.
Um pipeline de CI/CD maduro roda as três, nessa ordem, falhando rápido nas camadas mais baratas antes de chegar nas mais caras.
3. tflint na prática
O tflint foca em problemas que o terraform validate não pega, porque validate só garante que a sintaxe HCL e os tipos estão corretos — não que o uso de um recurso específico faz sentido para aquele provider.
# Instalação (Linux/macOS via script oficial)
curl -s https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash
# Rodar no diretório do módulo
tflint --init # baixa o plugin do provider (ex.: AWS)
tflint
Exemplo de configuração habilitando o plugin da AWS, que traz regras específicas (tipos de instância inválidos, uso de AMIs deprecated, etc.):
# .tflint.hcl
plugin "aws" {
enabled = true
version = "0.31.0"
source = "github.com/terraform-linters/tflint-ruleset-aws"
}
rule "terraform_deprecated_interpolation" {
enabled = true
}
rule "terraform_unused_declarations" {
enabled = true
}
Com isso, tflint sinaliza coisas como variáveis declaradas e nunca usadas, ou um instance_type que não existe na AWS — erros que só apareceriam em produção, na hora do apply, sem essa checagem prévia.
4. checkov na prática
O checkov (da Bridgecrew/Prisma Cloud) analisa o código Terraform contra centenas de políticas de segurança e compliance prontas (CIS Benchmarks, PCI-DSS, HIPAA, entre outras), sem precisar aplicar nada:
pip install checkov
checkov -d ./terraform
Exemplo de um recurso com problema de segurança óbvio:
resource "aws_s3_bucket" "data" {
bucket = "minha-empresa-dados"
}
resource "aws_s3_bucket_public_access_block" "data" {
bucket = aws_s3_bucket.data.id
block_public_acls = false
block_public_policy = false
ignore_public_acls = false
restrict_public_buckets = false
}
Rodar checkov contra esse arquivo reporta uma falha correspondente à política CKV_AWS_53 ("Ensure S3 bucket has block public ACLs enabled"), com o número da linha e uma explicação do risco. É possível suprimir uma regra pontualmente quando a exceção é intencional e documentada:
resource "aws_s3_bucket_public_access_block" "data" {
# checkov:skip=CKV_AWS_53:bucket de assets públicos do site, intencional
bucket = aws_s3_bucket.data.id
block_public_acls = false
block_public_policy = false
ignore_public_acls = false
restrict_public_buckets = false
}
Isso documenta a exceção diretamente no código, em vez de silenciar o alerta de forma invisível em algum lugar do pipeline.
5. Terratest na prática
O Terratest é uma biblioteca Go (mantida pela Gruntwork) para escrever testes de integração reais contra código Terraform. O padrão típico de um teste é: apply → validar → destroy, sempre em um ambiente descartável.
// test/webserver_test.go
package test
import (
"testing"
"time"
"github.com/gruntwork-io/terratest/modules/http-helper"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)
func TestWebServerModule(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../examples/webserver",
Vars: map[string]interface{}{
"environment": "test",
},
}
// Garante que 'terraform destroy' roda no final,
// mesmo se o teste falhar no meio do caminho.
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
publicIP := terraform.Output(t, terraformOptions, "public_ip")
url := "http://" + publicIP
http_helper.HttpGetWithRetry(
t,
url,
nil,
200,
"Welcome",
30, // tentativas
5*time.Second, // intervalo entre tentativas
)
assert.NotEmpty(t, publicIP, "o output public_ip não deveria estar vazio")
}
Rodando:
cd test
go test -v -timeout 30m
O defer terraform.Destroy(...) logo após criar as opções é o detalhe mais importante desse padrão: garante que o destroy roda mesmo se uma das validações (HttpGetWithRetry, assert) falhar no meio do teste, evitando deixar recursos reais de nuvem órfãos e gerando custo desnecessário.
6. Juntando tudo num pipeline de CI/CD
# .github/workflows/terraform-test.yml
name: Terraform Test Pipeline
on:
pull_request:
paths:
- "terraform/**"
jobs:
static-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: tflint
uses: terraform-linters/setup-tflint@v4
- run: tflint --init && tflint --chdir=terraform
- name: checkov
uses: bridgecrewio/checkov-action@master
with:
directory: terraform
integration-tests:
needs: static-checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.22"
- name: Terratest
working-directory: test
run: go test -v -timeout 30m
O job integration-tests só roda depois que static-checks passa — falhar rápido nas checagens estáticas (segundos) evita gastar tempo e dinheiro rodando apply/destroy reais (minutos) para um código que já tinha um problema óbvio de lint ou segurança.
7. Evitando drift em produção
Testes automatizados evitam que código com problema chegue à produção, mas não evitam drift — divergência entre o que o state do Terraform diz que existe e o que de fato existe na nuvem (alguém mudou algo manual pelo console, por exemplo). Para isso, a prática recomendada é rodar terraform plan (ou tofu plan) periodicamente contra o ambiente de produção, fora do fluxo de deploy, e alertar se houver diferença:
# Rodado por um cron/job agendado, não por um deploy
terraform plan -detailed-exitcode
# exit code 0 = sem mudanças, 1 = erro, 2 = há diferenças (drift)
O -detailed-exitcode é o que torna isso automatizável: um pipeline pode checar o código de saída e abrir um alerta apenas quando o valor for 2, sem precisar fazer parsing do output textual do plano.
8. Conclusão da série
Fechamos esta série de três artigos indo além do Terraform puro: o OpenTofu como alternativa de provisionamento nascida de uma crise de licenciamento, o Ansible cobrindo a configuração do que já foi provisionado, e agora as três camadas de teste — tflint, checkov e Terratest — que dão confiança de que o código de infraestrutura faz exatamente o que deveria antes de chegar em produção. Nenhuma dessas ferramentas substitui o Terraform; todas ampliam o que é possível fazer em volta dele, e é essa combinação — provisionar, configurar e testar — que sustenta um pipeline de IaC realmente confiável.
Imagem de capa: Logo oficial do Terraform — Wikimedia Commons
Referências:
Top comments (0)