Introducción

Kubernetes (K8s) se ha convertido en el estándar para orquestar contenedores en producción. Terraform añade una capa de gestión de infraestructura que complementa perfectamente a Kubernetes: mientras kubectl gestiona las cargas de trabajo dentro del cluster, Terraform gestiona el cluster en sí y puede también gestionar recursos de Kubernetes directamente.

En este proyecto aprenderemos a:

  1. Crear un cluster EKS (Elastic Kubernetes Service) en AWS con Terraform
  2. Configurar node groups con auto-scaling
  3. Desplegar recursos de Kubernetes (Deployments, Services, Namespaces) con Terraform
  4. Instalar aplicaciones con Helm via Terraform

Por Qué Gestionar Kubernetes con Terraform

Tabla Comparativa: kubectl vs Terraform para K8s

Aspecto kubectl / YAML Terraform
Gestión del cluster (EKS/GKE/AKS) No aplica Si, recurso aws_eks_cluster
Despliegue de aplicaciones kubectl apply -f kubernetes_deployment resource
Estado deseado vs actual No trackea estado Estado completo en terraform.tfstate
Rollback kubectl rollout undo terraform apply con versión anterior
Variables dinámicas ConfigMaps manuales Variables de Terraform en todos los recursos
Integración con cloud resources Manual (ARNs hardcodeados) Referencias directas entre recursos
Plan de cambios No (aplica directamente) terraform plan antes de cambiar
Control de versiones YAML en git HCL en git (misma cadena de CI/CD)
Secretos Secrets de K8s (base64) Variables sensibles + AWS Secrets Manager
Helm helm install helm_release resource

Cuándo Usar Cada Enfoque

  • Terraform para gestionar K8s: Ideal cuando Kubernetes es solo una parte de tu infraestructura más amplia (junto con RDS, S3, etc.)
  • kubectl/Helm directamente: Mejor para despliegues frecuentes de aplicaciones que cambian mucho
  • Enfoque híbrido (recomendado): Terraform para el cluster y recursos estáticos, kubectl/Helm para los microservicios que se despliegan en CI/CD

Arquitectura del Proyecto

                    ┌─────────────────────────────────────────────┐
                    │              AWS Cloud                       │
                    │                                              │
                    │  ┌──────────────────────────────────────┐   │
                    │  │           EKS Cluster                │   │
                    │  │                                      │   │
                    │  │  ┌─────────────┐  ┌─────────────┐   │   │
                    │  │  │  Node Group │  │  Node Group │   │   │
                    │  │  │  AZ-a       │  │  AZ-b       │   │   │
                    │  │  │  t3.medium  │  │  t3.medium  │   │   │
                    │  │  │  (2-5 nodos)│  │  (2-5 nodos)│   │   │
                    │  │  └─────────────┘  └─────────────┘   │   │
                    │  │                                      │   │
                    │  │  Namespaces:                        │   │
                    │  │  ├── production                     │   │
                    │  │  │   ├── [Deployment: webapp]       │   │
                    │  │  │   └── [Service: LoadBalancer]    │   │
                    │  │  └── monitoring                     │   │
                    │  │      └── [Helm: prometheus-stack]   │   │
                    │  │                                      │   │
                    │  └──────────────────────────────────────┘   │
                    │                                              │
                    │  VPC: 10.0.0.0/16                           │
                    │  ├── Subredes Públicas (para LBs)           │
                    │  └── Subredes Privadas (para nodos EKS)     │
                    │                                              │
                    └─────────────────────────────────────────────┘

  [Terraform]  ──── gestiona ────▶  EKS Cluster + Nodos + K8s Resources + Helm
  [kubectl]    ──── gestiona ────▶  Workloads de aplicación (CI/CD)

Estructura del Proyecto

terraform-kubernetes/
├── main.tf                   # Configuración de providers
├── variables.tf              # Variables del proyecto
├── outputs.tf                # Outputs: kubeconfig, endpoints
├── terraform.tfvars          # Valores concretos
│
├── vpc.tf                    # VPC y subredes para EKS
├── eks-cluster.tf            # EKS Control Plane
├── eks-node-groups.tf        # Grupos de nodos worker
├── eks-iam.tf                # IAM Roles para EKS
│
├── kubernetes.tf             # Recursos nativos de Kubernetes
├── helm.tf                   # Despliegues de Helm charts
│
└── scripts/
    └── get-kubeconfig.sh     # Script para obtener el kubeconfig

Código Terraform Completo

main.tf - Providers de Terraform, Kubernetes y Helm

# main.tf
# Este proyecto usa tres providers:
# 1. AWS: para crear el cluster EKS y la infraestructura cloud
# 2. Kubernetes: para gestionar recursos dentro del cluster (Deployments, Services, etc.)
# 3. Helm: para instalar charts de aplicaciones en el cluster

terraform {
  required_version = ">= 1.3"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }

    # Provider de Kubernetes
    # Se conecta al cluster usando el kubeconfig
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = "~> 2.20"
    }

    # Provider de Helm
    # Permite instalar Helm charts como recursos de Terraform
    helm = {
      source  = "hashicorp/helm"
      version = "~> 2.10"
    }

    # TLS para generar certificados (si es necesario)
    tls = {
      source  = "hashicorp/tls"
      version = "~> 4.0"
    }
  }
}

# Provider de AWS
provider "aws" {
  region = var.aws_region

  default_tags {
    tags = {
      Project     = var.project_name
      ManagedBy   = "Terraform"
      Environment = var.environment
    }
  }
}

# Provider de Kubernetes
# IMPORTANTE: La configuración de este provider depende del cluster EKS
# que se crea con el provider de AWS. Terraform resuelve esta dependencia
# automáticamente gracias a las referencias a los outputs del cluster.
provider "kubernetes" {
  host                   = aws_eks_cluster.main.endpoint
  cluster_ca_certificate = base64decode(aws_eks_cluster.main.certificate_authority[0].data)

  # Autenticación via token de AWS (más seguro que guardar el kubeconfig)
  exec {
    api_version = "client.authentication.k8s.io/v1beta1"
    command     = "aws"
    args = [
      "eks",
      "get-token",
      "--cluster-name",
      aws_eks_cluster.main.name,
      "--region",
      var.aws_region
    ]
  }
}

# Provider de Helm (usa la misma configuración que Kubernetes)
provider "helm" {
  kubernetes {
    host                   = aws_eks_cluster.main.endpoint
    cluster_ca_certificate = base64decode(aws_eks_cluster.main.certificate_authority[0].data)

    exec {
      api_version = "client.authentication.k8s.io/v1beta1"
      command     = "aws"
      args = [
        "eks",
        "get-token",
        "--cluster-name",
        aws_eks_cluster.main.name,
        "--region",
        var.aws_region
      ]
    }
  }
}

# AZs disponibles en la región
data "aws_availability_zones" "available" {
  state = "available"
}

# Versión más reciente de EKS disponible
data "aws_eks_cluster_auth" "main" {
  name = aws_eks_cluster.main.name
}

variables.tf

# variables.tf

variable "project_name" {
  description = "Nombre del proyecto"
  type        = string
  default     = "eks-project"
}

variable "environment" {
  description = "Entorno (dev, staging, prod)"
  type        = string
  default     = "dev"
}

variable "aws_region" {
  description = "Región de AWS"
  type        = string
  default     = "eu-west-1"
}

# --- VPC ---
variable "vpc_cidr" {
  description = "CIDR de la VPC"
  type        = string
  default     = "10.0.0.0/16"
}

# --- EKS ---
variable "eks_cluster_version" {
  description = "Versión de Kubernetes para EKS"
  type        = string
  default     = "1.28"
}

variable "node_instance_types" {
  description = "Tipos de instancia para los nodos worker"
  type        = list(string)
  default     = ["t3.medium"]
}

variable "node_min_size" {
  description = "Número mínimo de nodos"
  type        = number
  default     = 1
}

variable "node_max_size" {
  description = "Número máximo de nodos"
  type        = number
  default     = 5
}

variable "node_desired_size" {
  description = "Número deseado de nodos"
  type        = number
  default     = 2
}

# --- Aplicación ---
variable "app_image" {
  description = "Imagen Docker de la aplicación"
  type        = string
  default     = "nginx:latest"
}

variable "app_replicas" {
  description = "Número de réplicas de la aplicación"
  type        = number
  default     = 2
}

variable "app_namespace" {
  description = "Namespace de Kubernetes para la aplicación"
  type        = string
  default     = "production"
}

vpc.tf - VPC para EKS

# vpc.tf
# La VPC para EKS requiere tags especiales que el cluster necesita
# para descubrir subredes y crear load balancers automáticamente

# VPC principal
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true  # Requerido para EKS
  enable_dns_support   = true  # Requerido para EKS

  tags = {
    Name = "${var.project_name}-vpc"
    # Tag requerido por EKS para descubrir la VPC
    "kubernetes.io/cluster/${var.project_name}" = "shared"
  }
}

# Internet Gateway
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id
  tags   = { Name = "${var.project_name}-igw" }
}

# Subredes PÚBLICAS (para Load Balancers creados por K8s Services de tipo LoadBalancer)
resource "aws_subnet" "public" {
  count = 2  # Una por AZ para alta disponibilidad

  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(var.vpc_cidr, 8, count.index)  # 10.0.0.0/24, 10.0.1.0/24
  availability_zone = data.aws_availability_zones.available.names[count.index]

  map_public_ip_on_launch = true

  tags = {
    Name = "${var.project_name}-public-${count.index + 1}"
    # Tag requerido por EKS para crear Load Balancers EXTERNOS en estas subredes
    "kubernetes.io/cluster/${var.project_name}" = "shared"
    "kubernetes.io/role/elb"                     = "1"
  }
}

# Subredes PRIVADAS (para nodos worker de EKS)
# Los nodos están en subredes privadas por seguridad
resource "aws_subnet" "private" {
  count = 2

  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(var.vpc_cidr, 8, count.index + 10)  # 10.0.10.0/24, 10.0.11.0/24
  availability_zone = data.aws_availability_zones.available.names[count.index]

  tags = {
    Name = "${var.project_name}-private-${count.index + 1}"
    # Tag para Load Balancers INTERNOS
    "kubernetes.io/cluster/${var.project_name}" = "shared"
    "kubernetes.io/role/internal-elb"            = "1"
  }
}

# EIPs para NAT Gateways
resource "aws_eip" "nat" {
  count  = 2
  domain = "vpc"
  depends_on = [aws_internet_gateway.main]
}

# NAT Gateways (uno por AZ)
resource "aws_nat_gateway" "main" {
  count         = 2
  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.public[count.index].id
  tags          = { Name = "${var.project_name}-nat-${count.index + 1}" }
}

# Tabla de rutas pública
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }
  tags = { Name = "${var.project_name}-public-rt" }
}

resource "aws_route_table_association" "public" {
  count          = 2
  subnet_id      = aws_subnet.public[count.index].id
  route_table_id = aws_route_table.public.id
}

# Tablas de rutas privadas
resource "aws_route_table" "private" {
  count  = 2
  vpc_id = aws_vpc.main.id
  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.main[count.index].id
  }
  tags = { Name = "${var.project_name}-private-rt-${count.index + 1}" }
}

resource "aws_route_table_association" "private" {
  count          = 2
  subnet_id      = aws_subnet.private[count.index].id
  route_table_id = aws_route_table.private[count.index].id
}

eks-iam.tf - IAM Roles para EKS

# eks-iam.tf
# EKS requiere roles IAM específicos:
# 1. Rol para el Control Plane (gestiona el cluster)
# 2. Rol para los Node Groups (permisos de los nodos worker)

# ========================================
# IAM Role para el Control Plane de EKS
# ========================================

resource "aws_iam_role" "eks_cluster" {
  name = "${var.project_name}-eks-cluster-role"

  # EKS asume este rol para gestionar recursos de AWS en tu nombre
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "eks.amazonaws.com"
        }
      }
    ]
  })
}

# Política gestionada por AWS para el control plane de EKS
# Incluye permisos para crear/gestionar ENIs, security groups, etc.
resource "aws_iam_role_policy_attachment" "eks_cluster_policy" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
  role       = aws_iam_role.eks_cluster.name
}

# ========================================
# IAM Role para los Nodos Worker de EKS
# ========================================

resource "aws_iam_role" "eks_nodes" {
  name = "${var.project_name}-eks-nodes-role"

  # EC2 asume este rol (los nodos son instancias EC2)
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ec2.amazonaws.com"
        }
      }
    ]
  })
}

# Política para que los nodos se unan al cluster EKS
resource "aws_iam_role_policy_attachment" "eks_worker_node_policy" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
  role       = aws_iam_role.eks_nodes.name
}

# Política para que los nodos puedan descargar imágenes de ECR (registro privado)
resource "aws_iam_role_policy_attachment" "eks_cni_policy" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
  role       = aws_iam_role.eks_nodes.name
}

# Política para acceso de solo lectura a ECR (descargar imágenes Docker)
resource "aws_iam_role_policy_attachment" "ecr_read_only" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
  role       = aws_iam_role.eks_nodes.name
}

# Política para SSM (acceso a nodos sin SSH)
resource "aws_iam_role_policy_attachment" "ssm_policy" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
  role       = aws_iam_role.eks_nodes.name
}

# OIDC Provider: permite que los pods de K8s asuman roles IAM
# Esto se llama "IRSA" (IAM Roles for Service Accounts)
data "tls_certificate" "eks" {
  url = aws_eks_cluster.main.identity[0].oidc[0].issuer
}

resource "aws_iam_openid_connect_provider" "eks" {
  client_id_list  = ["sts.amazonaws.com"]
  thumbprint_list = [data.tls_certificate.eks.certificates[0].sha1_fingerprint]
  url             = aws_eks_cluster.main.identity[0].oidc[0].issuer
}

eks-cluster.tf - Control Plane de EKS

# eks-cluster.tf
# El control plane de EKS es gestionado por AWS
# (los nodos master son invisibles para el usuario, AWS los gestiona)

# Security Group para el control plane de EKS
resource "aws_security_group" "eks_cluster" {
  name        = "${var.project_name}-eks-cluster-sg"
  description = "Security group para el control plane de EKS"
  vpc_id      = aws_vpc.main.id

  # Comunicación bidireccional entre el control plane y los nodos
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = { Name = "${var.project_name}-eks-cluster-sg" }
}

# Cluster EKS (Control Plane)
resource "aws_eks_cluster" "main" {
  name    = var.project_name
  version = var.eks_cluster_version  # Ej: "1.28"

  # El cluster asume el IAM Role del control plane
  role_arn = aws_iam_role.eks_cluster.arn

  # Configuración de la VPC
  vpc_config {
    # Los nodos se comunican con el control plane via estos security groups
    security_group_ids = [aws_security_group.eks_cluster.id]

    # Los nodos se crearán en las subredes privadas
    subnet_ids = concat(
      aws_subnet.public[*].id,   # Subredes públicas (para LBs)
      aws_subnet.private[*].id   # Subredes privadas (para nodos)
    )

    # El API server de Kubernetes es accesible desde Internet
    # En prod: endpoint_public_access = false (solo acceso desde VPN)
    endpoint_public_access  = true
    endpoint_private_access = true

    # Restricción de IPs que pueden acceder al API server
    # En prod: limitar a IPs de tu empresa
    # public_access_cidrs = ["tu-ip-publica/32"]
  }

  # Logging del control plane a CloudWatch
  enabled_cluster_log_types = ["api", "audit", "authenticator"]

  # Garantizar que los roles IAM existan antes de crear el cluster
  depends_on = [
    aws_iam_role_policy_attachment.eks_cluster_policy,
  ]

  tags = {
    Name = "${var.project_name}-eks"
  }
}

# Add-ons de EKS: componentes core del cluster
# CoreDNS: DNS interno del cluster
resource "aws_eks_addon" "coredns" {
  cluster_name      = aws_eks_cluster.main.name
  addon_name        = "coredns"
  resolve_conflicts = "OVERWRITE"

  depends_on = [aws_eks_node_group.main]  # CoreDNS necesita nodos para ejecutarse
}

# kube-proxy: enrutamiento de red entre pods
resource "aws_eks_addon" "kube_proxy" {
  cluster_name      = aws_eks_cluster.main.name
  addon_name        = "kube-proxy"
  resolve_conflicts = "OVERWRITE"
}

# VPC CNI: plugin de red de AWS para pods
resource "aws_eks_addon" "vpc_cni" {
  cluster_name      = aws_eks_cluster.main.name
  addon_name        = "vpc-cni"
  resolve_conflicts = "OVERWRITE"
}

eks-node-groups.tf - Nodos Worker

# eks-node-groups.tf
# Los Node Groups son los nodos worker donde corren los pods

# Security Group para los nodos
resource "aws_security_group" "eks_nodes" {
  name        = "${var.project_name}-eks-nodes-sg"
  description = "Security group para nodos worker de EKS"
  vpc_id      = aws_vpc.main.id

  # Los nodos se comunican entre ellos (tráfico de pod a pod)
  ingress {
    from_port = 0
    to_port   = 0
    protocol  = "-1"
    self      = true  # Permitir tráfico dentro del mismo SG
  }

  # El control plane se comunica con los nodos
  ingress {
    from_port       = 1025
    to_port         = 65535
    protocol        = "tcp"
    security_groups = [aws_security_group.eks_cluster.id]
    description     = "Control plane a nodos"
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = { Name = "${var.project_name}-eks-nodes-sg" }
}

# Node Group principal
# Un Node Group es un conjunto de instancias EC2 gestionadas por EKS
resource "aws_eks_node_group" "main" {
  cluster_name    = aws_eks_cluster.main.name
  node_group_name = "${var.project_name}-nodes"
  node_role_arn   = aws_iam_role.eks_nodes.arn

  # Los nodos se crean en subredes privadas
  subnet_ids = aws_subnet.private[*].id

  # Tipos de instancia (los pods se distribuyen entre ellos)
  instance_types = var.node_instance_types

  # Configuración de auto-scaling del node group
  scaling_config {
    min_size     = var.node_min_size
    max_size     = var.node_max_size
    desired_size = var.node_desired_size
  }

  # Política de actualización de nodos
  update_config {
    # Número máximo de nodos que pueden estar no disponibles durante una actualización
    max_unavailable = 1
  }

  # AMI optimizada para EKS (AWS gestiona las actualizaciones)
  ami_type = "AL2_x86_64"

  # Tipo de capacidad: ON_DEMAND o SPOT (más barato pero puede interrumpirse)
  capacity_type = "ON_DEMAND"

  # Tamaño del disco de los nodos
  disk_size = 20

  # Las políticas IAM deben estar adjuntas antes de crear los nodos
  depends_on = [
    aws_iam_role_policy_attachment.eks_worker_node_policy,
    aws_iam_role_policy_attachment.eks_cni_policy,
    aws_iam_role_policy_attachment.ecr_read_only,
  ]

  tags = {
    Name = "${var.project_name}-node-group"
  }
}

# Node Group de SPOT (opcional: para cargas que pueden tolerar interrupciones)
resource "aws_eks_node_group" "spot" {
  cluster_name    = aws_eks_cluster.main.name
  node_group_name = "${var.project_name}-spot-nodes"
  node_role_arn   = aws_iam_role.eks_nodes.arn

  subnet_ids     = aws_subnet.private[*].id
  instance_types = ["t3.medium", "t3.large", "t3a.medium"]  # Múltiples tipos para SPOT

  scaling_config {
    min_size     = 0
    max_size     = 10
    desired_size = 0  # Empezar con 0 nodos SPOT
  }

  update_config {
    max_unavailable = 2
  }

  capacity_type = "SPOT"  # Instancias SPOT: hasta 70% más baratas

  depends_on = [
    aws_iam_role_policy_attachment.eks_worker_node_policy,
    aws_iam_role_policy_attachment.eks_cni_policy,
    aws_iam_role_policy_attachment.ecr_read_only,
  ]

  # Label de Kubernetes para identificar nodos SPOT
  labels = {
    "node-type" = "spot"
  }

  # Taint: los pods deben tolerar este taint para ejecutarse en nodos SPOT
  taint {
    key    = "spot"
    value  = "true"
    effect = "NO_SCHEDULE"
  }

  tags = {
    Name = "${var.project_name}-spot-node-group"
  }
}

kubernetes.tf - Recursos Nativos de Kubernetes

# kubernetes.tf
# Gestiona recursos de Kubernetes directamente con Terraform
# IMPORTANTE: Este archivo depende del cluster EKS ya existente

# ========================================
# Namespaces
# ========================================

# Namespace de producción
resource "kubernetes_namespace" "production" {
  metadata {
    name = var.app_namespace

    # Labels para identificar el namespace
    labels = {
      name        = var.app_namespace
      environment = "production"
      managed-by  = "terraform"
    }

    # Anotaciones para políticas de red
    annotations = {
      "description" = "Namespace para la aplicacion en produccion"
    }
  }

  depends_on = [aws_eks_node_group.main]
}

# Namespace de monitorización
resource "kubernetes_namespace" "monitoring" {
  metadata {
    name = "monitoring"
    labels = {
      name       = "monitoring"
      managed-by = "terraform"
    }
  }

  depends_on = [aws_eks_node_group.main]
}

# ========================================
# ConfigMap: configuración de la aplicación
# ========================================

resource "kubernetes_config_map" "app_config" {
  metadata {
    name      = "app-config"
    namespace = kubernetes_namespace.production.metadata[0].name
  }

  # Los ConfigMaps almacenan configuración no sensible
  data = {
    APP_ENV           = var.environment
    APP_PORT          = "8080"
    LOG_LEVEL         = "info"
    # Esta referencia conecta K8s config con información de AWS
    AWS_REGION        = var.aws_region
    CLUSTER_NAME      = aws_eks_cluster.main.name
  }
}

# ========================================
# Secret: credenciales sensibles
# ========================================

resource "kubernetes_secret" "app_secrets" {
  metadata {
    name      = "app-secrets"
    namespace = kubernetes_namespace.production.metadata[0].name
  }

  # Los secrets se almacenan en base64 automáticamente por Terraform
  # NO incluir secretos reales en el código: usar variables de Terraform
  data = {
    # En producción, estos valores vendrían de variables sensibles
    # o de AWS Secrets Manager
    DB_PASSWORD = base64encode("placeholder-cambiar-en-prod")
    API_KEY     = base64encode("placeholder-cambiar-en-prod")
  }

  type = "Opaque"  # Tipo genérico de Secret
}

# ========================================
# Deployment: la aplicación
# ========================================

resource "kubernetes_deployment" "app" {
  metadata {
    name      = "${var.project_name}-app"
    namespace = kubernetes_namespace.production.metadata[0].name
    labels = {
      app         = var.project_name
      version     = "1.0"
      managed-by  = "terraform"
    }
  }

  spec {
    replicas = var.app_replicas  # Número de instancias del pod

    # Selector: identifica qué pods pertenecen a este deployment
    selector {
      match_labels = {
        app = var.project_name
      }
    }

    # Estrategia de actualización
    strategy {
      type = "RollingUpdate"  # Actualizar gradualmente sin downtime
      rolling_update {
        max_unavailable = "25%"  # Max 25% de pods no disponibles durante actualización
        max_surge       = "25%"  # Max 25% de pods extra durante actualización
      }
    }

    # Template del pod
    template {
      metadata {
        labels = {
          app = var.project_name
        }
      }

      spec {
        # Anti-afinidad: distribuir pods en diferentes nodos para resilencia
        affinity {
          pod_anti_affinity {
            preferred_during_scheduling_ignored_during_execution {
              weight = 100
              pod_affinity_term {
                label_selector {
                  match_expressions {
                    key      = "app"
                    operator = "In"
                    values   = [var.project_name]
                  }
                }
                topology_key = "kubernetes.io/hostname"
              }
            }
          }
        }

        container {
          name  = "app"
          image = var.app_image  # Imagen Docker de la aplicación

          # Recursos del contenedor
          resources {
            requests = {
              cpu    = "100m"   # 0.1 CPU cores
              memory = "128Mi"  # 128 MB RAM
            }
            limits = {
              cpu    = "500m"   # 0.5 CPU cores máximo
              memory = "256Mi"  # 256 MB RAM máximo
            }
          }

          # Variables de entorno desde ConfigMap
          env_from {
            config_map_ref {
              name = kubernetes_config_map.app_config.metadata[0].name
            }
          }

          # Variables de entorno individuales desde Secrets
          env {
            name = "DB_PASSWORD"
            value_from {
              secret_key_ref {
                name = kubernetes_secret.app_secrets.metadata[0].name
                key  = "DB_PASSWORD"
              }
            }
          }

          # Puerto del contenedor
          port {
            container_port = 80
            protocol       = "TCP"
          }

          # Health checks
          liveness_probe {
            http_get {
              path = "/"
              port = 80
            }
            initial_delay_seconds = 30  # Esperar 30s antes del primer check
            period_seconds        = 10  # Verificar cada 10 segundos
            failure_threshold     = 3   # Reiniciar después de 3 fallos consecutivos
          }

          readiness_probe {
            http_get {
              path = "/"
              port = 80
            }
            initial_delay_seconds = 5
            period_seconds        = 5
            failure_threshold     = 3
          }
        }

        # Tolerations para ejecutar en nodos con taints (ej: nodos SPOT)
        # toleration {
        #   key    = "spot"
        #   value  = "true"
        #   effect = "NoSchedule"
        # }
      }
    }
  }

  depends_on = [
    kubernetes_namespace.production,
    aws_eks_node_group.main
  ]
}

# ========================================
# Service: expone la aplicación
# ========================================

resource "kubernetes_service" "app" {
  metadata {
    name      = "${var.project_name}-service"
    namespace = kubernetes_namespace.production.metadata[0].name

    # Anotaciones para configurar el Load Balancer de AWS
    annotations = {
      "service.beta.kubernetes.io/aws-load-balancer-type"   = "nlb"
      "service.beta.kubernetes.io/aws-load-balancer-scheme" = "internet-facing"
    }
  }

  spec {
    # LoadBalancer: crea un Network Load Balancer de AWS automáticamente
    type = "LoadBalancer"

    # Selector: enviar tráfico a pods con este label
    selector = {
      app = var.project_name
    }

    port {
      name        = "http"
      port        = 80      # Puerto del Service (externo)
      target_port = 80      # Puerto del contenedor (interno)
      protocol    = "TCP"
    }
  }

  depends_on = [kubernetes_deployment.app]
}

helm.tf - Despliegues con Helm

# helm.tf
# Instala aplicaciones usando Helm charts
# Helm es el "gestor de paquetes" de Kubernetes

# ========================================
# Ingress Controller: NGINX
# ========================================
# El Ingress Controller permite exponer múltiples servicios
# a través de un solo Load Balancer, usando reglas de enrutamiento HTTP

resource "helm_release" "nginx_ingress" {
  name       = "nginx-ingress"
  repository = "https://kubernetes.github.io/ingress-nginx"
  chart      = "ingress-nginx"
  version    = "4.8.0"
  namespace  = "ingress-nginx"

  create_namespace = true  # Crear el namespace si no existe

  # Valores de configuración del chart
  values = [
    yamlencode({
      controller = {
        replicaCount = 2  # Alta disponibilidad: 2 replicas del ingress
        service = {
          type = "LoadBalancer"
          annotations = {
            "service.beta.kubernetes.io/aws-load-balancer-type"   = "nlb"
            "service.beta.kubernetes.io/aws-load-balancer-scheme" = "internet-facing"
          }
        }
        resources = {
          requests = {
            cpu    = "100m"
            memory = "90Mi"
          }
          limits = {
            cpu    = "500m"
            memory = "256Mi"
          }
        }
      }
    })
  ]

  depends_on = [aws_eks_node_group.main]
}

# ========================================
# Metrics Server: métricas de pods y nodos
# ========================================
# Requerido para que funcione el Horizontal Pod Autoscaler

resource "helm_release" "metrics_server" {
  name       = "metrics-server"
  repository = "https://kubernetes-sigs.github.io/metrics-server/"
  chart      = "metrics-server"
  version    = "3.11.0"
  namespace  = "kube-system"  # Namespace de sistema de K8s

  set {
    name  = "args[0]"
    value = "--kubelet-insecure-tls"  # Necesario en EKS
  }

  depends_on = [aws_eks_node_group.main]
}

outputs.tf - Outputs del Cluster

# outputs.tf

output "cluster_name" {
  description = "Nombre del cluster EKS"
  value       = aws_eks_cluster.main.name
}

output "cluster_endpoint" {
  description = "Endpoint del API server de Kubernetes"
  value       = aws_eks_cluster.main.endpoint
}

output "cluster_version" {
  description = "Versión de Kubernetes del cluster"
  value       = aws_eks_cluster.main.version
}

output "kubeconfig_command" {
  description = "Comando para configurar kubectl para este cluster"
  value       = "aws eks update-kubeconfig --name ${aws_eks_cluster.main.name} --region ${var.aws_region}"
}

output "app_service_name" {
  description = "Nombre del Service de Kubernetes de la aplicación"
  value       = kubernetes_service.app.metadata[0].name
}

output "node_group_status" {
  description = "Estado del Node Group principal"
  value       = aws_eks_node_group.main.status
}

output "oidc_provider_arn" {
  description = "ARN del OIDC Provider (para IRSA)"
  value       = aws_iam_openid_connect_provider.eks.arn
}

Instrucciones de Despliegue Paso a Paso

Paso 1: Verificar Prerequisitos

# Verificar Terraform
terraform version  # >= 1.3

# Verificar AWS CLI
aws --version  # >= 2.0

# Verificar kubectl
kubectl version --client  # >= 1.27

# Verificar Helm
helm version  # >= 3.12

# Autenticarse en AWS
aws configure
aws sts get-caller-identity

Paso 2: Inicializar y Desplegar

cd terraform-kubernetes

# Inicializar: descarga los providers aws, kubernetes y helm
terraform init

# Validar configuración
terraform validate

# Planificar (verás recursos de AWS, K8s y Helm)
terraform plan -out=tfplan

# Aplicar el plan
# EKS tarda ~15 minutos en crear el control plane
terraform apply tfplan

Paso 3: Configurar kubectl

# Obtener el comando de configuración del output
KUBECONFIG_CMD=$(terraform output -raw kubeconfig_command)
echo $KUBECONFIG_CMD

# Ejecutar el comando para configurar kubectl
aws eks update-kubeconfig \
  --name $(terraform output -raw cluster_name) \
  --region eu-west-1

# Verificar que kubectl puede hablar con el cluster
kubectl get nodes

# Salida esperada:
# NAME                                        STATUS   ROLES    AGE   VERSION
# ip-10-0-10-x.eu-west-1.compute.internal    Ready    <none>   5m    v1.28.x
# ip-10-0-11-x.eu-west-1.compute.internal    Ready    <none>   5m    v1.28.x

Paso 4: Verificar los Recursos de Kubernetes

# Ver todos los namespaces
kubectl get namespaces

# Ver los pods en el namespace de producción
kubectl get pods -n production

# Ver el estado del deployment
kubectl get deployment -n production

# Ver el Service y su IP/DNS de Load Balancer
kubectl get service -n production

# Esperar a que el Load Balancer tenga una IP asignada
kubectl get service -n production -w  # Ctrl+C para salir

# Ver logs de la aplicación
kubectl logs -n production -l app=eks-project

# Verificar el Ingress Controller
kubectl get pods -n ingress-nginx
kubectl get service -n ingress-nginx

Paso 5: Probar la Aplicación

# Obtener el DNS del Load Balancer del Service
LB_DNS=$(kubectl get service \
  -n production \
  eks-project-service \
  -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')

echo "Aplicación disponible en: http://$LB_DNS"

# Probar la aplicación
curl http://$LB_DNS

# El Load Balancer puede tardar 1-3 minutos en estar disponible
# Si no hay respuesta, espera y vuelve a intentarlo

Gestión del kubeconfig con Terraform

# scripts/get-kubeconfig.sh
#!/bin/bash
# Script para obtener el kubeconfig del cluster EKS

CLUSTER_NAME=$(terraform output -raw cluster_name)
REGION=$(terraform output -raw aws_region 2>/dev/null || echo "eu-west-1")

echo "Configurando kubectl para el cluster: $CLUSTER_NAME"

aws eks update-kubeconfig \
  --name "$CLUSTER_NAME" \
  --region "$REGION" \
  --alias "$CLUSTER_NAME"

echo "kubectl configurado. Probando conexion..."
kubectl get nodes
# Hacer el script ejecutable y correrlo
chmod +x scripts/get-kubeconfig.sh
./scripts/get-kubeconfig.sh

Ejercicios de Extensión

Ejercicio 1: Añadir Monitoring con Prometheus via Helm

Instala el stack completo de monitoring (Prometheus + Grafana) usando el chart kube-prometheus-stack.

Solución:

# Añadir en helm.tf

resource "helm_release" "prometheus_stack" {
  name       = "prometheus-stack"
  repository = "https://prometheus-community.github.io/helm-charts"
  chart      = "kube-prometheus-stack"
  version    = "55.0.0"
  namespace  = kubernetes_namespace.monitoring.metadata[0].name

  # Configuración personalizada
  values = [
    yamlencode({
      grafana = {
        enabled         = true
        adminPassword   = "admin123"  # Cambiar en producción
        service = {
          type = "LoadBalancer"
        }
        # Dashboard de EKS preinstalado
        dashboardProviders = {
          "dashboardproviders.yaml" = {
            apiVersion = 1
            providers = [{
              name            = "default"
              orgId           = 1
              folder          = ""
              type            = "file"
              disableDeletion = false
              editable        = true
              options = {
                path = "/var/lib/grafana/dashboards/default"
              }
            }]
          }
        }
      }
      prometheus = {
        prometheusSpec = {
          retention    = "7d"  # Retener métricas 7 días
          storageSpec = {
            volumeClaimTemplate = {
              spec = {
                resources = {
                  requests = {
                    storage = "10Gi"
                  }
                }
              }
            }
          }
        }
      }
      alertmanager = {
        enabled = true
      }
    })
  ]

  depends_on = [
    aws_eks_node_group.main,
    kubernetes_namespace.monitoring
  ]
}
# Verificar que Prometheus y Grafana se desplegaron
kubectl get pods -n monitoring

# Obtener la contraseña de Grafana
kubectl get secret \
  -n monitoring \
  prometheus-stack-grafana \
  -o jsonpath="{.data.admin-password}" | base64 --decode

# Acceder a Grafana (si hay un Service tipo LoadBalancer)
GRAFANA_LB=$(kubectl get service \
  -n monitoring \
  prometheus-stack-grafana \
  -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')

echo "Grafana: http://$GRAFANA_LB"

Ejercicio 2: Implementar HPA (Horizontal Pod Autoscaler)

El HPA escala automáticamente el número de réplicas basándose en el uso de CPU o memoria.

Solución:

# Añadir en kubernetes.tf

resource "kubernetes_horizontal_pod_autoscaler_v2" "app" {
  metadata {
    name      = "${var.project_name}-hpa"
    namespace = kubernetes_namespace.production.metadata[0].name
  }

  spec {
    # El HPA gestiona este Deployment
    scale_target_ref {
      api_version = "apps/v1"
      kind        = "Deployment"
      name        = kubernetes_deployment.app.metadata[0].name
    }

    # Límites de réplicas
    min_replicas = 2
    max_replicas = 10

    # Métricas para el escalado
    metric {
      type = "Resource"
      resource {
        name = "cpu"
        target {
          type                = "Utilization"
          average_utilization = 70  # Escalar si CPU > 70%
        }
      }
    }

    metric {
      type = "Resource"
      resource {
        name = "memory"
        target {
          type                = "Utilization"
          average_utilization = 80  # Escalar si Memoria > 80%
        }
      }
    }

    # Política de comportamiento del escalado
    behavior {
      # Escalar hacia ARRIBA rápido
      scale_up {
        stabilization_window_seconds = 60  # Esperar 60s antes de escalar
        select_policy                = "Max"
        policy {
          type          = "Percent"
          value         = 100    # Doblar el número de pods
          period_seconds = 60
        }
      }

      # Escalar hacia ABAJO lento (evitar flapping)
      scale_down {
        stabilization_window_seconds = 300  # Esperar 5 min antes de reducir
        select_policy                = "Min"
        policy {
          type           = "Pods"
          value          = 1     # Reducir de a 1 pod por vez
          period_seconds = 60
        }
      }
    }
  }

  depends_on = [
    helm_release.metrics_server,  # Metrics Server debe existir para HPA
    kubernetes_deployment.app
  ]
}
# Verificar el HPA
kubectl get hpa -n production

# Salida esperada:
# NAME              REFERENCE                  TARGETS         MINPODS   MAXPODS   REPLICAS
# eks-project-hpa   Deployment/eks-project-app   5%/70%          2         10        2

# Generar carga para ver el auto-scaling en acción
kubectl run -n production load-generator \
  --image=busybox \
  --restart=Never \
  -- /bin/sh -c "while true; do wget -q -O- http://eks-project-service/; done"

# En otra terminal, observar el HPA reaccionar
kubectl get hpa -n production -w

Errores Comunes y Soluciones

Error: "Error: Kubernetes cluster unreachable"

Causa: El provider de Kubernetes no puede conectarse al cluster porque aún no existe.

Solución: Usar depends_on correctamente. Los providers de Kubernetes y Helm deben esperar a que el cluster EKS esté listo:

resource "kubernetes_namespace" "production" {
  # ...
  depends_on = [
    aws_eks_cluster.main,
    aws_eks_node_group.main  # Los nodos también deben estar listos
  ]
}

Error: "Error: configmaps 'aws-auth' already exists"

Causa: El aws-auth ConfigMap ya existe (EKS lo crea automáticamente).

Solución: Importar el ConfigMap existente:

terraform import kubernetes_config_map.aws_auth kube-system/aws-auth

Resumen y Lecciones Aprendidas

Este proyecto ha demostrado cómo Terraform puede gestionar toda la pila de Kubernetes, desde el cluster hasta las aplicaciones:

  • Providers encadenados: El provider de Kubernetes depende del cluster creado por el provider de AWS. Terraform resuelve automáticamente estas dependencias.
  • IAM para EKS: EKS requiere roles IAM específicos para el control plane y los nodos. La configuración correcta de IAM es crítica.
  • Tags de subredes: Los tags de Kubernetes en las subredes son obligatorios para que EKS cree Load Balancers correctamente.
  • Helm como gestor de paquetes: helm_release permite instalar aplicaciones complejas (como Prometheus) con una configuración declarativa.
  • HPA y Metrics Server: El escalado automático de pods requiere que el Metrics Server esté instalado.
  • IRSA (IAM Roles for Service Accounts): El OIDC Provider permite que los pods asuman roles IAM de forma segura, sin hardcodear credenciales.

En el siguiente y último proyecto construiremos una arquitectura completamente serverless con Lambda, API Gateway, DynamoDB y más servicios, todos gestionados por Terraform.

© Copyright 2026. Todos los derechos reservados