El Imperativo de las Pruebas de Infraestructura como Código
La Infraestructura como Código (IaC) ha revolucionado cómo aprovisionamos y gestionamos la infraestructura, pero con gran poder viene gran responsabilidad. Sin pruebas adecuadas, IaC puede propagar configuraciones erróneas a escala, llevando a vulnerabilidades de seguridad, violaciones de cumplimiento y cortes catastróficos. Las pruebas de IaC ya no son opcionales: son un requisito crítico para mantener una infraestructura confiable, segura y conforme.
La complejidad de los entornos cloud modernos demanda estrategias de prueba sofisticadas que van más allá de la simple validación de sintaxis. Necesitamos verificar configuraciones de seguridad, asegurar el cumplimiento de políticas organizacionales, validar integraciones entre servicios y confirmar que nuestra infraestructura se comporta correctamente bajo varias condiciones. Este enfoque integral de las pruebas de IaC previene errores costosos y acelera la entrega mientras mantiene la calidad.
Pirámide de Pruebas para Infraestructura como Código
Análisis Estático y Linting
La base de las pruebas de IaC comienza con el análisis estático. Estas pruebas se ejecutan rápidamente y detectan problemas comunes temprano en el ciclo de desarrollo:
# terraform/modules/web-server/variables.tf
variable "instance_type" {
description = "Tipo de instancia EC2"
type = string
default = "t3.micro"
validation {
condition = contains([
"t3.micro",
"t3.small",
"t3.medium",
"t3.large"
], var.instance_type)
error_message = "El tipo de instancia debe ser uno de los tamaños aprobados."
}
}
variable "subnet_ids" {
description = "Lista de IDs de subnet para el despliegue"
type = list(string)
validation {
condition = length(var.subnet_ids) >= 2
error_message = "Se requieren al menos 2 subnets para alta disponibilidad."
}
}
variable "environment" {
description = "Nombre del entorno"
type = string
validation {
condition = can(regex("^(dev|staging|prod)$", var.environment))
error_message = "El entorno debe ser dev, staging o prod."
}
}
Pipeline de Validación Terraform
# .github/workflows/terraform-validation.yml
name: Pipeline de Validación Terraform
on:
pull_request:
paths:
- 'terraform/**'
- '.github/workflows/terraform-validation.yml'
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Configurar Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.5.0
- name: Verificación de Formato Terraform
run: |
terraform fmt -check -recursive terraform/
- name: TFLint - Linter de Terraform
run: |
curl -s https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash
# Configurar TFLint
cat > .tflint.hcl <<EOF
config {
module = true
force = false
}
plugin "aws" {
enabled = true
version = "0.24.0"
source = "github.com/terraform-linters/tflint-ruleset-aws"
}
rule "aws_instance_invalid_type" {
enabled = true
}
rule "aws_resource_missing_tags" {
enabled = true
tags = ["Environment", "Owner", "CostCenter"]
}
EOF
tflint --init
tflint --recursive
- name: Escaneo de Seguridad con Checkov
run: |
pip install checkov
checkov -d terraform/ --framework terraform --output json --output-file-path checkov-report.json
# Parsear y fallar en problemas de alta severidad
python3 <<EOF
import json
with open('checkov-report.json', 'r') as f:
report = json.load(f)
failed_checks = report.get('summary', {}).get('failed', 0)
if failed_checks > 0:
print(f"Se encontraron {failed_checks} problemas de seguridad")
for check in report.get('results', {}).get('failed_checks', []):
print(f" - {check['check_id']}: {check['check_name']}")
exit(1)
EOF
- name: Validar Terraform
run: |
for dir in terraform/environments/*/; do
echo "Validando $dir"
cd $dir
terraform init -backend=false
terraform validate
cd -
done
- name: Generar Documentación
run: |
# Instalar terraform-docs
curl -sSLo terraform-docs.tar.gz https://github.com/terraform-docs/terraform-docs/releases/download/v0.16.0/terraform-docs-v0.16.0-linux-amd64.tar.gz
tar -xzf terraform-docs.tar.gz
chmod +x terraform-docs
# Generar docs para cada módulo
for module in terraform/modules/*/; do
./terraform-docs markdown table --output-file README.md --output-mode inject $module
done
Pruebas Unitarias con Terratest
Implementación Integral de Terratest
// test/terraform_aws_vpc_test.go
package test
import (
"fmt"
"strings"
"testing"
"time"
"github.com/gruntwork-io/terratest/modules/aws"
"github.com/gruntwork-io/terratest/modules/random"
"github.com/gruntwork-io/terratest/modules/retry"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTerraformAwsVpc(t *testing.T) {
t.Parallel()
// Generar identificadores únicos
uniqueId := random.UniqueId()
region := "us-east-1"
vpcCidr := "10.0.0.0/16"
terraformOptions := &terraform.Options{
TerraformDir: "../terraform/modules/vpc",
Vars: map[string]interface{}{
"vpc_cidr": vpcCidr,
"environment": "test",
"name": fmt.Sprintf("test-vpc-%s", uniqueId),
"region": region,
"enable_nat": true,
"single_nat": false,
},
EnvVars: map[string]string{
"AWS_DEFAULT_REGION": region,
},
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
// Validar salidas
vpcId := terraform.Output(t, terraformOptions, "vpc_id")
assert.NotEmpty(t, vpcId)
publicSubnetIds := terraform.OutputList(t, terraformOptions, "public_subnet_ids")
privateSubnetIds := terraform.OutputList(t, terraformOptions, "private_subnet_ids")
assert.Equal(t, 3, len(publicSubnetIds))
assert.Equal(t, 3, len(privateSubnetIds))
// Verificar configuración VPC
vpc := aws.GetVpcById(t, vpcId, region)
assert.Equal(t, vpcCidr, vpc.CidrBlock)
assert.True(t, vpc.EnableDnsSupport)
assert.True(t, vpc.EnableDnsHostnames)
// Probar configuraciones de subnet
for _, subnetId := range publicSubnetIds {
subnet := aws.GetSubnetById(t, subnetId, region)
assert.True(t, subnet.MapPublicIpOnLaunch)
assert.Contains(t, subnet.AvailabilityZone, region)
}
for _, subnetId := range privateSubnetIds {
subnet := aws.GetSubnetById(t, subnetId, region)
assert.False(t, subnet.MapPublicIpOnLaunch)
// Verificar conectividad NAT gateway
routeTable := aws.GetRouteTableForSubnet(t, subnet, region)
hasNatRoute := false
for _, route := range routeTable.Routes {
if route.DestinationCidrBlock == "0.0.0.0/0" && route.NatGatewayId != "" {
hasNatRoute = true
break
}
}
assert.True(t, hasNatRoute, "La subnet privada debe tener ruta NAT gateway")
}
// Probar ACLs de red
testNetworkAcls(t, vpcId, region)
// Probar grupos de seguridad
testSecurityGroups(t, terraformOptions, region)
}
func testNetworkAcls(t *testing.T, vpcId string, region string) {
nacls := aws.GetNetworkAclsForVpc(t, vpcId, region)
for _, nacl := range nacls {
// Verificar reglas de entrada
for _, rule := range nacl.IngressRules {
if rule.RuleNumber == 100 {
assert.Equal(t, "tcp", rule.Protocol)
assert.Equal(t, "0.0.0.0/0", rule.CidrBlock)
}
}
// Verificar reglas de salida
for _, rule := range nacl.EgressRules {
if rule.RuleNumber == 100 {
assert.Equal(t, "-1", rule.Protocol) // Todos los protocolos
assert.Equal(t, "0.0.0.0/0", rule.CidrBlock)
}
}
}
}
func TestTerraformAwsEcs(t *testing.T) {
t.Parallel()
terraformOptions := &terraform.Options{
TerraformDir: "../terraform/modules/ecs-cluster",
Vars: map[string]interface{}{
"cluster_name": fmt.Sprintf("test-cluster-%s", random.UniqueId()),
"capacity_providers": []string{"FARGATE", "FARGATE_SPOT"},
"container_insights": true,
"enable_execute_command": true,
},
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
// Probar creación del cluster
clusterArn := terraform.Output(t, terraformOptions, "cluster_arn")
assert.Contains(t, clusterArn, "cluster/test-cluster")
// Probar despliegue del servicio
deployTestService(t, terraformOptions)
}
func deployTestService(t *testing.T, terraformOptions *terraform.Options) {
serviceOptions := &terraform.Options{
TerraformDir: "../terraform/modules/ecs-service",
Vars: map[string]interface{}{
"cluster_id": terraform.Output(t, terraformOptions, "cluster_id"),
"service_name": "test-service",
"task_cpu": 256,
"task_memory": 512,
"desired_count": 2,
"container_port": 8080,
},
}
defer terraform.Destroy(t, serviceOptions)
terraform.InitAndApply(t, serviceOptions)
// Esperar a que el servicio se estabilice
retry.DoWithRetry(t, "Esperar servicio ECS", 30, 10*time.Second, func() (string, error) {
// Verificar estado del servicio
return "", nil
})
}
Pruebas de Ansible con Molecule
Configuración de Pruebas con Molecule
# molecule/default/molecule.yml
---
dependency:
name: galaxy
driver:
name: docker
platforms:
- name: ubuntu-2204
image: ubuntu:22.04
pre_build_image: false
dockerfile: Dockerfile.j2
privileged: true
command: /lib/systemd/systemd
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:ro
tmpfs:
- /tmp
- /run
capabilities:
- SYS_ADMIN
- name: centos-8
image: centos:8
pre_build_image: false
dockerfile: Dockerfile.j2
privileged: true
command: /usr/sbin/init
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:ro
tmpfs:
- /tmp
- /run
provisioner:
name: ansible
config_options:
defaults:
callback_whitelist: profile_tasks
fact_caching: jsonfile
fact_caching_connection: /tmp/ansible_cache
inventory:
host_vars:
ubuntu-2204:
ansible_python_interpreter: /usr/bin/python3
lint:
name: ansible-lint
playbooks:
prepare: prepare.yml
converge: converge.yml
verify: verify.yml
verifier:
name: testinfra
options:
verbose: true
lint:
name: flake8
scenario:
name: default
test_sequence:
- dependency
- lint
- cleanup
- destroy
- syntax
- create
- prepare
- converge
- idempotence
- side_effect
- verify
- cleanup
- destroy
Pruebas de Playbook de Ansible
# molecule/default/tests/test_nginx.py
import os
import pytest
import testinfra.utils.ansible_runner
testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
os.environ['MOLECULE_INVENTORY_FILE']
).get_hosts('all')
def test_nginx_instalado(host):
"""Probar que nginx está instalado"""
nginx = host.package('nginx')
assert nginx.is_installed
assert nginx.version.startswith('1.')
def test_servicio_nginx(host):
"""Probar que el servicio nginx está ejecutándose y habilitado"""
nginx = host.service('nginx')
assert nginx.is_running
assert nginx.is_enabled
def test_configuracion_nginx(host):
"""Probar configuración de nginx"""
config = host.file('/etc/nginx/nginx.conf')
assert config.exists
assert config.is_file
assert config.user == 'root'
assert config.group == 'root'
assert oct(config.mode) == '0o644'
# Validar sintaxis de configuración
cmd = host.run('nginx -t')
assert cmd.rc == 0
assert 'syntax is ok' in cmd.stderr
assert 'test is successful' in cmd.stderr
def test_puertos_nginx(host):
"""Probar que nginx escucha en los puertos esperados"""
assert host.socket('tcp://0.0.0.0:80').is_listening
# Probar SSL si está configurado
if host.file('/etc/nginx/sites-enabled/ssl').exists:
assert host.socket('tcp://0.0.0.0:443').is_listening
def test_procesos_nginx(host):
"""Probar que los procesos nginx están ejecutándose"""
master = host.process.filter(comm='nginx', user='root')
assert len(master) == 1
workers = host.process.filter(comm='nginx', user='www-data')
assert len(workers) >= 1
def test_archivos_log_nginx(host):
"""Probar que los archivos de log se crean con permisos correctos"""
access_log = host.file('/var/log/nginx/access.log')
error_log = host.file('/var/log/nginx/error.log')
for log in [access_log, error_log]:
assert log.exists
assert log.is_file
assert log.user == 'www-data'
assert oct(log.mode) == '0o640'
def test_cabeceras_seguridad_nginx(host):
"""Probar que las cabeceras de seguridad están establecidas"""
response = host.run('curl -I http://localhost')
assert response.rc == 0
headers = response.stdout
assert 'X-Frame-Options: SAMEORIGIN' in headers
assert 'X-Content-Type-Options: nosniff' in headers
assert 'X-XSS-Protection: 1; mode=block' in headers
@pytest.mark.parametrize('site', [
'default',
'app.example.com'
])
def test_sitios_nginx(host, site):
"""Probar configuraciones de sitios nginx"""
site_config = host.file(f'/etc/nginx/sites-available/{site}')
assert site_config.exists
site_enabled = host.file(f'/etc/nginx/sites-enabled/{site}')
assert site_enabled.exists
assert site_enabled.is_symlink
assert site_enabled.linked_to == f'/etc/nginx/sites-available/{site}'
Políticas como Código con Open Policy Agent
Políticas OPA para Terraform
# policies/terraform/security.rego
package terraform.security
import future.keywords.if
import future.keywords.in
default allow = false
# Denegar buckets S3 públicos
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_s3_bucket_public_access_block"
resource.change.after.block_public_acls == false
msg := sprintf("El bucket S3 %s debe bloquear ACLs públicas", [resource.address])
}
# Requerir encriptación para instancias RDS
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_db_instance"
not resource.change.after.storage_encrypted
msg := sprintf("La instancia RDS %s debe tener almacenamiento encriptado", [resource.address])
}
# Hacer cumplir requisitos de etiquetado
required_tags := ["Environment", "Owner", "CostCenter", "Project"]
deny[msg] {
resource := input.resource_changes[_]
required_tag := required_tags[_]
not resource.change.after.tags[required_tag]
msg := sprintf("Al recurso %s le falta la etiqueta requerida: %s", [resource.address, required_tag])
}
# Validación de reglas de grupo de seguridad
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_security_group_rule"
resource.change.after.type == "ingress"
resource.change.after.cidr_blocks[_] == "0.0.0.0/0"
resource.change.after.from_port == 22
msg := sprintf("La regla del grupo de seguridad %s permite SSH desde cualquier lugar", [resource.address])
}
# Restricciones de tipo de instancia
allowed_instance_types := [
"t3.micro", "t3.small", "t3.medium",
"m5.large", "m5.xlarge"
]
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_instance"
not resource.change.after.instance_type in allowed_instance_types
msg := sprintf("La instancia %s usa un tipo no aprobado: %s", [
resource.address,
resource.change.after.instance_type
])
}
# Requisitos de segmentación de red
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_instance"
resource.change.after.associate_public_ip_address == true
contains(resource.change.after.tags.Environment, "prod")
msg := sprintf("La instancia de producción %s no puede tener IP pública", [resource.address])
}
Políticas Sentinel para Terraform Cloud
# policies/control-costos.sentinel
import "tfplan/v2" as tfplan
import "decimal"
# Umbral máximo de costo mensual
max_costo_mensual = decimal.new(1000)
# Calcular costo mensual estimado
costo_estimado = decimal.new(tfplan.cost_estimate.total_monthly_cost)
# Regla principal
main = rule {
costo_estimado.less_than_or_equal_to(max_costo_mensual)
}
# Límites de costo de instancia por tipo
limites_costo_instancia = {
"t3.micro": 50,
"t3.small": 100,
"t3.medium": 200,
"t3.large": 300,
"m5.large": 400,
"m5.xlarge": 800,
}
# Validar costos individuales de instancias
validar_costos_instancias = func() {
validado = true
for tfplan.resource_changes as _, rc {
if rc.type is "aws_instance" and rc.change.actions contains "create" {
tipo_instancia = rc.change.after.instance_type
if tipo_instancia in keys(limites_costo_instancia) {
# Estimar costo por hora (simplificado)
costo_hora = limites_costo_instancia[tipo_instancia] / 730
if costo_hora > limites_costo_instancia[tipo_instancia] / 730 {
print("La instancia", rc.address, "excede el límite de costo")
validado = false
}
}
}
}
return validado
}
# Verificaciones de cumplimiento
regla_cumplimiento = rule {
validar_costos_instancias()
}
Pruebas de Integración con Kitchen-Terraform
Configuración de Kitchen
# .kitchen.yml
---
driver:
name: terraform
root_module_directory: test/fixtures/wrapper
command_timeout: 1800
provisioner:
name: terraform
verifier:
name: terraform
systems:
- name: default
backend: ssh
hosts_output: public_ip
user: ubuntu
key_files:
- test/assets/id_rsa
- name: aws
backend: awspec
platforms:
- name: ubuntu-2204
driver:
variables:
platform: ubuntu-2204
- name: amazon-linux-2
driver:
variables:
platform: amazon-linux-2
suites:
- name: default
driver:
variables:
instance_count: 2
enable_monitoring: true
verifier:
inspec_tests:
- test/integration/default
lifecycle:
pre_converge:
- local: echo "Ejecutando tareas pre-convergencia"
post_converge:
- local: echo "Ejecutando tareas post-convergencia"
Pruebas de Integración con InSpec
# test/integration/default/controls/infrastructure.rb
control 'infraestructura-01' do
title 'Verificar Configuración VPC'
desc 'Asegurar que la VPC está configurada correctamente'
describe aws_vpc(vpc_id: attribute('vpc_id')) do
it { should exist }
its('cidr_block') { should eq '10.0.0.0/16' }
its('state') { should eq 'available' }
its('enable_dns_support') { should eq true }
its('enable_dns_hostnames') { should eq true }
end
end
control 'infraestructura-02' do
title 'Verificar Grupos de Seguridad'
desc 'Asegurar que los grupos de seguridad están configurados de forma segura'
aws_security_groups.where(vpc_id: attribute('vpc_id')).entries.each do |sg|
describe aws_security_group(group_id: sg.group_id) do
it { should_not allow_ingress_from_internet_on_port(22) }
it { should_not allow_ingress_from_internet_on_port(3389) }
# Matchers personalizados
its('ingress_rules') { should_not include(
from_port: 0,
to_port: 65535,
cidr_blocks: ['0.0.0.0/0']
)}
end
end
end
control 'infraestructura-03' do
title 'Verificar Instancias EC2'
desc 'Asegurar que las instancias EC2 cumplen los requisitos de seguridad'
aws_ec2_instances.where(vpc_id: attribute('vpc_id')).entries.each do |instance|
describe aws_ec2_instance(instance_id: instance.instance_id) do
it { should be_running }
it { should have_encrypted_root_volume }
its('monitoring_state') { should eq 'enabled' }
its('instance_type') { should be_in %w[t3.micro t3.small t3.medium] }
# Verificar que IMDSv2 es requerido
its('metadata_options.http_tokens') { should eq 'required' }
its('metadata_options.http_endpoint') { should eq 'enabled' }
end
end
end
control 'infraestructura-04' do
title 'Verificar Buckets S3'
desc 'Asegurar que los buckets S3 son seguros'
aws_s3_buckets.entries.each do |bucket|
describe aws_s3_bucket(bucket_name: bucket.name) do
it { should have_default_encryption_enabled }
it { should have_versioning_enabled }
it { should_not be_public }
its('bucket_acl.grants') { should_not include(
grantee: { type: 'Group', uri: 'http://acs.amazonaws.com/groups/global/AllUsers' }
)}
end
end
end
control 'infraestructura-05' do
title 'Verificar Instancias RDS'
desc 'Asegurar que las instancias RDS están configuradas de forma segura'
aws_rds_instances.entries.each do |db|
describe aws_rds_instance(db_instance_identifier: db.db_instance_identifier) do
it { should be_encrypted }
it { should have_automated_backups_enabled }
its('backup_retention_period') { should be >= 7 }
its('multi_az') { should eq true }
its('publicly_accessible') { should eq false }
its('deletion_protection') { should eq true }
end
end
end
Pipeline de Cumplimiento Continuo
Pipeline CI/CD Completo para IaC
# .github/workflows/iac-pipeline.yml
name: Pipeline de Infraestructura como Código
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
schedule:
- cron: '0 2 * * *' # Verificación diaria de cumplimiento
env:
TF_VERSION: '1.5.0'
ANSIBLE_VERSION: '2.15.0'
AWS_DEFAULT_REGION: 'us-east-1'
jobs:
analisis-estatico:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Configurar Herramientas
run: |
# Instalar herramientas requeridas
pip install checkov ansible-lint yamllint
# Instalar tflint
curl -s https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash
# Instalar tfsec
curl -s https://raw.githubusercontent.com/aquasecurity/tfsec/master/scripts/install_linux.sh | bash
- name: YAML Lint
run: yamllint -c .yamllint .
- name: Ansible Lint
run: ansible-lint ansible/
- name: Escaneo de Seguridad Terraform
run: |
tfsec terraform/ --format json --out tfsec-report.json
checkov -d terraform/ --output json --output-file-path checkov-report.json
- name: Subir Reportes de Seguridad
uses: actions/upload-artifact@v3
with:
name: reportes-seguridad
path: |
tfsec-report.json
checkov-report.json
pruebas-unitarias:
runs-on: ubuntu-latest
needs: analisis-estatico
strategy:
matrix:
suite_pruebas: [vpc, ecs, rds, s3]
steps:
- uses: actions/checkout@v3
- name: Configurar Go
uses: actions/setup-go@v4
with:
go-version: '1.20'
- name: Ejecutar Terratest
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
cd test
go mod download
go test -v -timeout 30m -run Test${{ matrix.suite_pruebas }}
validacion-politicas:
runs-on: ubuntu-latest
needs: analisis-estatico
steps:
- uses: actions/checkout@v3
- name: Configurar OPA
run: |
curl -L -o opa https://openpolicyagent.org/downloads/latest/opa_linux_amd64_static
chmod +x opa
sudo mv opa /usr/local/bin/
- name: Plan Terraform
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
cd terraform/environments/dev
terraform init
terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.json
- name: Verificación de Políticas OPA
run: |
opa eval -d policies/ -i terraform/environments/dev/tfplan.json \
"data.terraform.security.deny[_]" --format pretty
# Fallar si se violan políticas
VIOLACIONES=$(opa eval -d policies/ -i terraform/environments/dev/tfplan.json \
"data.terraform.security.deny" --format json | jq '.result[0].expressions[0].value | length')
if [ "$VIOLACIONES" -gt 0 ]; then
echo "¡Se encontraron violaciones de política!"
exit 1
fi
pruebas-integracion:
runs-on: ubuntu-latest
needs: [pruebas-unitarias, validacion-politicas]
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Configurar Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.0'
bundler-cache: true
- name: Ejecutar Pruebas Kitchen
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
bundle exec kitchen test --parallel
escaneo-cumplimiento:
runs-on: ubuntu-latest
needs: pruebas-integracion
steps:
- uses: actions/checkout@v3
- name: Ejecutar Cumplimiento InSpec
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
# Instalar InSpec
curl https://omnitruck.chef.io/install.sh | sudo bash -s -- -P inspec
# Ejecutar perfiles de cumplimiento
inspec exec compliance/ --reporter json:compliance-report.json
- name: Generar Reporte de Cumplimiento
run: |
python3 scripts/generar_reporte_cumplimiento.py \
--input compliance-report.json \
--output reporte-cumplimiento.html
- name: Subir Reporte de Cumplimiento
uses: actions/upload-artifact@v3
with:
name: reporte-cumplimiento
path: reporte-cumplimiento.html
Conclusión
Las pruebas de Infraestructura como Código no se tratan solo de prevenir cortes, sino de construir confianza en nuestros cambios de infraestructura, asegurar seguridad y cumplimiento, y permitir despliegues rápidos y seguros. Las estrategias de pruebas integrales presentadas aquí forman un marco robusto que captura problemas en cada nivel, desde errores de sintaxis hasta problemas complejos de integración.
La combinación de análisis estático, pruebas unitarias, pruebas de integración y validación de políticas crea una red de seguridad que permite a los equipos moverse rápido sin romper cosas. Al implementar estas prácticas, las organizaciones pueden lograr el santo grial de la gestión de infraestructura: infraestructura auto-documentada, auto-validada y auto-reparable que escala de manera confiable.
Recuerda que las pruebas de IaC son una disciplina en evolución. A medida que tu infraestructura crece en complejidad, también deberían hacerlo tus estrategias de prueba. Comienza con lo básico—linting y validación—luego agrega gradualmente capas de pruebas más sofisticadas. La inversión en pruebas integrales de IaC rinde dividendos en incidentes reducidos, despliegues más rápidos y mayor confianza del equipo.