Introducción

La integración de Terraform con sistemas de Integración Continua y Entrega Continua (CI/CD) es uno de los pasos más importantes para llevar la gestión de infraestructura a un nivel profesional. En lugar de ejecutar comandos de Terraform manualmente desde tu máquina local, un pipeline CI/CD automatiza todo el proceso: valida el código, genera el plan de cambios, obtiene aprobaciones cuando son necesarias y aplica los cambios de forma controlada y reproducible.

Este módulo te enseñará los conceptos, herramientas y patrones necesarios para integrar Terraform en cualquier sistema CI/CD de forma segura y eficiente.


¿Por qué automatizar Terraform con CI/CD?

Ejecutar Terraform manualmente tiene varios problemas graves en entornos profesionales:

  • Inconsistencia: Cada desarrollador puede tener una versión diferente de Terraform o de los providers instalados localmente.
  • Falta de auditoría: No hay registro claro de quién ejecutó terraform apply y cuándo.
  • Errores humanos: Es fácil olvidarse de hacer terraform plan antes de apply, o aplicar cambios en el ambiente equivocado.
  • Credenciales inseguras: Los desarrolladores necesitan tener credenciales de producción en sus máquinas locales.
  • Sin proceso de revisión: Los cambios de infraestructura no pasan por revisión de código antes de aplicarse.

Los beneficios de automatizar con CI/CD son:

  • Reproducibilidad: Siempre se usa la misma versión de Terraform y los mismos providers en un ambiente limpio.
  • Trazabilidad: Cada ejecución queda registrada con su commit, autor y resultado.
  • Seguridad: Las credenciales viven en el sistema CI/CD, no en las máquinas de los desarrolladores.
  • Calidad: Se pueden añadir validaciones automáticas (formato, linting, tests) antes de aplicar cambios.
  • Colaboración: Los cambios de infraestructura pasan por el mismo proceso de revisión que el código de aplicación.

El pipeline de Terraform típico

El patrón más común en equipos profesionales sigue este flujo:

Fase 1: Pull Request abierto

Cuando un desarrollador abre un Pull Request (PR) con cambios en el código Terraform:

  1. El CI ejecuta terraform fmt -check para verificar el formato
  2. El CI ejecuta terraform validate para verificar la sintaxis
  3. El CI ejecuta terraform plan y publica el resultado como comentario en el PR
  4. Los revisores pueden ver exactamente qué recursos se van a crear, modificar o destruir
  5. Se aprueba o rechaza el PR con base en el plan visible

Fase 2: Merge a la rama principal

Cuando el PR se aprueba y se hace merge a main (o master):

  1. El CI ejecuta nuevamente terraform plan para confirmar que no hay cambios inesperados
  2. (Opcional) Se solicita aprobación manual de una persona autorizada
  3. El CI ejecuta terraform apply de forma automática
  4. Se notifica el resultado (éxito o fallo) al equipo

Este flujo garantiza que nadie pueda aplicar cambios sin que hayan sido revisados y aprobados.

┌─────────────────────────────────────────────────────────────┐
│                    FLUJO CI/CD TERRAFORM                    │
└─────────────────────────────────────────────────────────────┘

  Developer         GitHub/GitLab          CI System         AWS/Azure/GCP
     │                    │                    │                    │
     │── git push ──────> │                   │                    │
     │── Abre PR ───────> │                   │                    │
     │                    │── Trigger CI ───> │                    │
     │                    │                   │── terraform fmt    │
     │                    │                   │── terraform validate│
     │                    │                   │── terraform plan   │
     │                    │<── Comenta Plan ──│                    │
     │<── Notificación ───│                   │                    │
     │                    │                   │                    │
     │── Aprueba PR ────> │                   │                    │
     │── Merge a main ──> │                   │                    │
     │                    │── Trigger CI ───> │                    │
     │                    │                   │── terraform plan   │
     │                    │                   │── [Aprobación?]    │
     │                    │                   │── terraform apply  │
     │                    │                   │──────────────────> │
     │                    │                   │<── Resultado ──────│
     │<── Notificación ───│<── Resultado ─────│                    │

Herramientas CI/CD compatibles con Terraform

Terraform es agnóstico al sistema CI/CD: funciona con cualquier herramienta que pueda ejecutar comandos de shell. Las más populares son:

Herramienta Tipo Fortalezas para Terraform Consideraciones
GitHub Actions SaaS Integración nativa con GitHub, workflows en YAML, actions de la comunidad Solo para repos en GitHub
GitLab CI SaaS/Self-hosted Integración con GitLab, runners propios, environments Ecosistema GitLab
Jenkins Self-hosted Muy flexible, plugins para todo, control total Requiere mantenimiento de infraestructura
CircleCI SaaS Configuración simple, orbs para Terraform Costo basado en créditos
Azure DevOps SaaS Integración con Azure, pipelines gráficos Mejor para equipos Microsoft
Atlantis Self-hosted Especializado para Terraform, comentarios en PR Requiere servidor propio
Digger SaaS/Self-hosted GitOps para Terraform, fácil configuración Más nuevo, menos maduro

Consideraciones de seguridad en CI/CD

La seguridad es el aspecto más crítico al integrar Terraform con CI/CD. Los pipelines necesitan credenciales de nube con permisos elevados, lo que los convierte en un objetivo de alto valor.

Gestión de credenciales

Regla de oro: Las credenciales NUNCA deben estar en el código.

Las credenciales deben almacenarse como secretos del sistema CI/CD:

  • En GitHub Actions: Settings > Secrets and variables > Actions
  • En GitLab CI: Settings > CI/CD > Variables
  • En Jenkins: Manage Jenkins > Manage Credentials
  • En CircleCI: Project Settings > Environment Variables
# MAL - Nunca hagas esto
export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"

# BIEN - Usa secretos del CI que se inyectan como variables de entorno
# La variable $AWS_ACCESS_KEY_ID viene del sistema CI como secreto
terraform plan

Nunca imprimir credenciales en logs

Los sistemas CI/CD modernos enmascaran automáticamente los secretos en los logs, pero hay que tener cuidado con:

# MAL - Esto puede exponer credenciales en logs
env | grep AWS
printenv

# BIEN - Solo ejecutar los comandos necesarios
terraform init
terraform plan -out=tfplan
terraform apply tfplan

Principio de mínimo privilegio

El rol IAM o Service Principal usado por el CI/CD debe tener solo los permisos necesarios para crear los recursos que Terraform gestiona. No uses credenciales de administrador.

Rotación de credenciales

Prefiere métodos de autenticación sin credenciales estáticas:

  • AWS: OIDC (OpenID Connect) - GitHub Actions puede asumir roles de IAM sin claves de acceso
  • Azure: Workload Identity Federation - similar a OIDC
  • GCP: Workload Identity - autenticación sin Service Account keys

Gestión del estado remoto en pipelines

Cuando Terraform se ejecuta en CI/CD, múltiples agentes pueden ejecutarse en paralelo. El estado debe almacenarse de forma remota y con bloqueo (locking) para evitar corrupción.

# backend.tf - Configuración de estado remoto para CI/CD

# Ejemplo con S3 (AWS)
terraform {
  backend "s3" {
    bucket         = "mi-empresa-terraform-state"
    key            = "produccion/infraestructura/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-state-lock"  # Para el bloqueo
  }
}

# Ejemplo con Azure Blob Storage
# terraform {
#   backend "azurerm" {
#     resource_group_name  = "rg-terraform-state"
#     storage_account_name = "terrastateempresa"
#     container_name       = "tfstate"
#     key                  = "produccion.terraform.tfstate"
#   }
# }

# Ejemplo con GCS (Google Cloud)
# terraform {
#   backend "gcs" {
#     bucket = "mi-empresa-terraform-state"
#     prefix = "produccion/infraestructura"
#   }
# }

El bloqueo automático del estado garantiza que si un pipeline está ejecutando terraform apply, otro pipeline que intente hacer lo mismo fallará en lugar de corromper el estado.


Aprobaciones manuales antes del apply

En entornos de producción, es una buena práctica requerir que un humano apruebe el plan antes de aplicarlo. Esto agrega una capa de control sobre los cambios automáticos.

Los sistemas CI/CD implementan esto de formas diferentes:

# GitHub Actions - Usando environments con required reviewers
jobs:
  apply:
    environment: produccion  # Este environment requiere aprobación manual
    runs-on: ubuntu-latest
    steps:
      - name: Terraform Apply
        run: terraform apply -auto-approve

# El "environment" de GitHub Actions puede configurarse para requerir
# aprobación de revisores específicos antes de que el job continúe
// Jenkins - Usando el paso "input"
stage('Aprobación Manual') {
    steps {
        input message: '¿Aprobar terraform apply en producción?',
              ok: 'Aprobar',
              submitter: 'admin,tech-lead'
    }
}

Herramientas especializadas para CI/CD con Terraform

Atlantis

Atlantis es una herramienta open-source diseñada específicamente para automatizar Terraform a través de pull requests. Se ejecuta como un servidor que escucha webhooks de GitHub/GitLab/Bitbucket.

Funcionamiento:

  • Cuando abres un PR, comentas /atlantis plan y Atlantis ejecuta el plan
  • Cuando quieres aplicar, comentas /atlantis apply
  • Atlantis controla los bloqueos para evitar conflictos
# atlantis.yaml - Configuración de Atlantis
version: 3
projects:
  - name: produccion-vpc
    dir: environments/produccion/vpc
    workspace: default
    autoplan:
      when_modified:
        - "**/*.tf"
      enabled: true

  - name: staging-app
    dir: environments/staging/app
    workspace: default
    autoplan:
      when_modified:
        - "**/*.tf"
      enabled: true

Digger

Digger es una alternativa más moderna a Atlantis, diseñada para funcionar directamente dentro de GitHub Actions/GitLab CI sin necesitar un servidor separado.

# .github/workflows/digger.yml
name: Digger
on:
  pull_request:
    types: [opened, synchronize]
  issue_comment:
    types: [created]

jobs:
  digger:
    runs-on: ubuntu-latest
    steps:
      - uses: diggerhq/[email protected]
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

Flujo de trabajo GitOps con Terraform

GitOps es un paradigma donde Git es la única fuente de verdad para el estado deseado de la infraestructura. Los principios son:

  1. Todo el estado deseado vive en Git: Configuraciones, variables, políticas
  2. Los cambios se hacen solo a través de Git: Pull requests, no comandos manuales
  3. El sistema converge automáticamente: El CI/CD detecta divergencias y las corrige
┌──────────────────────────────────────────────────────┐
│                  GITOPS CON TERRAFORM                │
└──────────────────────────────────────────────────────┘

  Git Repository          CI/CD System            Infraestructura
  (Fuente de Verdad)
       │                       │                        │
       │  Estado Deseado ────> │                        │
       │                       │── Compara con ──────> │
       │                       │<── Estado Actual ──────│
       │                       │                        │
       │                       │── Si hay diferencia:  │
       │                       │── terraform apply ──> │
       │                       │                        │
       │                       │   Infraestructura      │
       │                       │   converge al estado   │
       │                       │   deseado en Git       │

Ejemplo de pipeline genérico con etapas

Aquí hay un pipeline genérico que ilustra las etapas típicas. Puede adaptarse a cualquier sistema CI/CD:

# pipeline-terraform-generico.yml
# Este es un pseudocódigo que ilustra las etapas
# La sintaxis real varía según el sistema CI/CD

pipeline:
  # ETAPA 1: Preparación del entorno
  setup:
    - checkout del repositorio
    - instalar Terraform versión X.Y.Z
    - configurar credenciales de nube (desde secretos del CI)
    - ejecutar: terraform init -backend-config="..."

  # ETAPA 2: Validación de código
  validate:
    - ejecutar: terraform fmt -check -recursive
      # Falla si el código no está formateado correctamente
    - ejecutar: terraform validate
      # Verifica la sintaxis y consistencia del código
    - ejecutar: tflint  (opcional)
      # Linter adicional para mejores prácticas
    - ejecutar: tfsec   (opcional)
      # Análisis de seguridad estático

  # ETAPA 3: Plan
  plan:
    - ejecutar: terraform plan -out=tfplan -no-color
    - guardar tfplan como artefacto
    - si es un PR: publicar el output del plan como comentario

  # ETAPA 4: Aprobación (solo en rama principal)
  approval:
    condicion: solo si es merge a main
    - esperar aprobación manual de persona autorizada
    timeout: 24 horas

  # ETAPA 5: Apply (solo en rama principal)
  apply:
    condicion: solo si es merge a main Y aprobación recibida
    - ejecutar: terraform apply tfplan
    - publicar resultado (éxito/fallo)
    - notificar al equipo

Ejemplo concreto en YAML (genérico)

# Este ejemplo muestra la estructura con pasos más concretos

stages:
  - name: "Inicializar"
    commands:
      - |
        # Instalar Terraform si no está disponible
        terraform version || {
          wget https://releases.hashicorp.com/terraform/1.9.0/terraform_1.9.0_linux_amd64.zip
          unzip terraform_1.9.0_linux_amd64.zip
          mv terraform /usr/local/bin/
        }

      - |
        # Inicializar Terraform con backend remoto
        terraform init \
          -backend-config="bucket=${TF_STATE_BUCKET}" \
          -backend-config="key=${TF_STATE_KEY}" \
          -backend-config="region=${AWS_REGION}"

  - name: "Validar"
    commands:
      - terraform fmt -check -recursive
      - terraform validate

  - name: "Plan"
    commands:
      - |
        # Generar el plan y capturar el output
        terraform plan \
          -var-file="environments/${ENVIRONMENT}.tfvars" \
          -out=tfplan \
          -no-color 2>&1 | tee plan-output.txt

      - |
        # Publicar el plan como comentario en el PR
        # (Esto varía según la plataforma)
        post_pr_comment "$(cat plan-output.txt)"

  - name: "Apply"
    condicion: "rama == main"
    commands:
      - |
        # Aplicar el plan guardado
        terraform apply -auto-approve tfplan

      - |
        # Notificar resultado
        if [ $? -eq 0 ]; then
          notify_slack "✅ Terraform apply exitoso en ${ENVIRONMENT}"
        else
          notify_slack "❌ Terraform apply falló en ${ENVIRONMENT}"
          exit 1
        fi

Errores comunes en CI/CD con Terraform

Error 1: Estado desactualizado

Problema: El pipeline falla con "Error locking state" o aplica sobre un estado desactualizado.

Causa: Otro pipeline está corriendo concurrentemente, o un pipeline anterior falló y no liberó el lock.

# Solución: Liberar el lock manualmente (con cuidado)
terraform force-unlock LOCK_ID

# Verificar el estado del lock en DynamoDB (AWS)
aws dynamodb get-item \
  --table-name terraform-state-lock \
  --key '{"LockID": {"S": "mi-empresa-terraform-state/produccion/terraform.tfstate"}}'

Error 2: Credenciales expiradas o incorrectas

Problema: Error: No valid credential sources found

Solución: Verificar que los secretos del CI estén configurados correctamente y que el rol IAM tenga los permisos necesarios.

# Para debugging (temporal, nunca en producción)
aws sts get-caller-identity
# Muestra qué identidad está usando Terraform

Error 3: Versión de Terraform inconsistente

Problema: El plan funciona localmente pero falla en CI porque las versiones difieren.

Solución: Fijar la versión de Terraform en el código y en el pipeline.

# versions.tf
terraform {
  required_version = "= 1.9.0"  # Versión exacta, no rango

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "= 5.50.0"
    }
  }
}
# En el pipeline, instalar la versión exacta
tfenv install 1.9.0
tfenv use 1.9.0

Error 4: Variables de entorno vs variables de Terraform

Problema: Las variables no se pasan correctamente al pipeline.

# MAL - La variable no se pasa a Terraform
export DB_PASSWORD="secreto"
terraform apply

# BIEN - Usar el prefijo TF_VAR_ para variables de Terraform
export TF_VAR_db_password="secreto"
terraform apply
# Ahora Terraform puede usar la variable "db_password"

Error 5: Aplicar sin plan guardado

Problema: Usando -auto-approve sin un plan previo puede aplicar cambios inesperados si el estado cambió entre el plan y el apply.

# MAL - Puede aplicar cambios diferentes a los planeados
terraform apply -auto-approve

# BIEN - Siempre usar el plan guardado
terraform plan -out=tfplan
terraform apply tfplan
# Esto garantiza que se aplica exactamente lo que se revisó

Ejercicio práctico

Enunciado

Diseña un pipeline CI/CD completo para un proyecto Terraform que gestiona infraestructura en AWS. El proyecto tiene dos ambientes: staging y produccion. Los requisitos son:

  1. En cualquier Pull Request: ejecutar fmt, validate y plan para el ambiente staging
  2. En merge a main: aplicar automáticamente en staging
  3. En creación de tag v*.*.*: ejecutar plan en produccion y esperar aprobación manual antes de aplicar
  4. Las credenciales de AWS deben ser secretos del CI
  5. El estado debe estar en S3 con bloqueo en DynamoDB

Solución

┌────────────────────────────────────────────────────────────────┐
│              DIAGRAMA DE FLUJO DEL PIPELINE                    │
└────────────────────────────────────────────────────────────────┘

Pull Request abierto/actualizado:
  ┌──────────────────────────────────────────────┐
  │  1. Checkout del código                      │
  │  2. Setup Terraform 1.9.0                    │
  │  3. terraform init (backend staging)         │
  │  4. terraform fmt -check                     │
  │  5. terraform validate                       │
  │  6. terraform plan -var-file=staging.tfvars  │
  │  7. Comentar plan en el PR                   │
  └──────────────────────────────────────────────┘

Merge a main:
  ┌──────────────────────────────────────────────┐
  │  1. Checkout del código                      │
  │  2. Setup Terraform 1.9.0                    │
  │  3. terraform init (backend staging)         │
  │  4. terraform plan -var-file=staging.tfvars  │
  │  5. terraform apply (staging)                │
  └──────────────────────────────────────────────┘

Push de tag v*.*.*:
  ┌──────────────────────────────────────────────┐
  │  1. Checkout del código                      │
  │  2. Setup Terraform 1.9.0                    │
  │  3. terraform init (backend produccion)      │
  │  4. terraform plan -var-file=prod.tfvars     │
  │  5. ⏸ ESPERAR APROBACIÓN MANUAL (24h)       │
  │  6. terraform apply (produccion)             │
  └──────────────────────────────────────────────┘
# Configuración del pipeline (pseudocódigo estructurado)

secretos_requeridos:
  - AWS_ACCESS_KEY_ID_STAGING
  - AWS_SECRET_ACCESS_KEY_STAGING
  - AWS_ACCESS_KEY_ID_PRODUCCION
  - AWS_SECRET_ACCESS_KEY_PRODUCCION
  - TF_STATE_BUCKET  # Bucket S3 para el estado
  - SLACK_WEBHOOK    # Para notificaciones

variables_globales:
  TF_VERSION: "1.9.0"
  AWS_REGION: "us-east-1"
  DYNAMODB_TABLE: "terraform-locks"

jobs:

  # Job 1: Validación y plan para PRs
  pr-check:
    trigger: pull_request
    ambiente: staging
    pasos:
      - checkout
      - setup terraform ${TF_VERSION}
      - configurar credenciales AWS staging
      - terraform init -backend-config="key=staging/terraform.tfstate"
      - terraform fmt -check
      - terraform validate
      - terraform plan -var-file=environments/staging.tfvars -out=tfplan
      - publicar output del plan como comentario en PR

  # Job 2: Apply automático en staging
  staging-apply:
    trigger: push a main
    ambiente: staging
    pasos:
      - checkout
      - setup terraform ${TF_VERSION}
      - configurar credenciales AWS staging
      - terraform init -backend-config="key=staging/terraform.tfstate"
      - terraform plan -var-file=environments/staging.tfvars -out=tfplan
      - terraform apply tfplan
      - notificar en Slack resultado

  # Job 3: Plan y apply en producción con aprobación
  produccion-plan:
    trigger: push de tag v*.*.*
    ambiente: produccion
    pasos:
      - checkout
      - setup terraform ${TF_VERSION}
      - configurar credenciales AWS produccion
      - terraform init -backend-config="key=produccion/terraform.tfstate"
      - terraform plan -var-file=environments/produccion.tfvars -out=tfplan
      - guardar tfplan como artefacto
      - notificar en Slack: "Plan listo, pendiente aprobación"

  produccion-apply:
    trigger: aprobación manual después de produccion-plan
    depende_de: produccion-plan
    ambiente: produccion
    timeout_aprobacion: 24 horas
    aprobadores: ["tech-lead", "devops-manager"]
    pasos:
      - descargar tfplan del artefacto
      - terraform apply tfplan
      - notificar en Slack resultado

# Estructura de archivos del proyecto
estructura_proyecto:
  ├── main.tf
  ├── variables.tf
  ├── outputs.tf
  ├── versions.tf
  ├── backend.tf
  └── environments/
      ├── staging.tfvars
      └── produccion.tfvars
# environments/staging.tfvars
environment    = "staging"
instance_type  = "t3.micro"
replica_count  = 1
min_capacity   = 1
max_capacity   = 3

# environments/produccion.tfvars
environment    = "produccion"
instance_type  = "t3.large"
replica_count  = 3
min_capacity   = 3
max_capacity   = 10

Resumen y próximos pasos

En este módulo aprendiste:

  • Por qué es esencial integrar Terraform con CI/CD: elimina inconsistencias, mejora la seguridad y agrega trazabilidad a los cambios de infraestructura.
  • El patrón fundamental: plan en PR (para revisión), apply en merge (automatizado).
  • Las herramientas disponibles: GitHub Actions, GitLab CI, Jenkins, CircleCI, Azure DevOps, Atlantis y Digger.
  • Seguridad en CI/CD: credenciales como secretos, principio de mínimo privilegio, OIDC para evitar claves estáticas.
  • Estado remoto: imprescindible para pipelines con múltiples agentes, siempre con bloqueo habilitado.
  • Aprobaciones manuales: para cambios en producción, siempre es mejor tener una revisión humana.
  • Errores comunes: locks de estado, credenciales incorrectas, versiones inconsistentes.

En los próximos módulos veremos implementaciones concretas de estos conceptos:

  • 08-02: Implementación completa con Jenkins y Jenkinsfile
  • 08-03: Implementación completa con GitHub Actions
  • 08-04: Terraform Cloud como alternativa gestionada que incluye CI/CD integrado

La automatización es el puente entre escribir código Terraform y operarlo de forma profesional y segura en una organización real.

© Copyright 2026. Todos los derechos reservados