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:
- Crear un cluster EKS (Elastic Kubernetes Service) en AWS con Terraform
- Configurar node groups con auto-scaling
- Desplegar recursos de Kubernetes (Deployments, Services, Namespaces) con Terraform
- 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 kubeconfigCó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-identityPaso 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 tfplanPaso 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.xPaso 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-nginxPaso 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 intentarloGestió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.shEjercicios 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 -wErrores 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:
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_releasepermite 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.
Curso de Terraform
Módulo 1: Introducción a Terraform
- ¿Qué es Terraform?
- Instalando Terraform
- Conceptos Básicos de Terraform
- Primera Configuración de Terraform
Módulo 2: Lenguaje de Configuración de Terraform
Módulo 3: Gestión del Estado
Módulo 4: Módulos de Terraform
Módulo 5: Aprovisionamiento de Recursos
- Conceptos Básicos de Aprovisionamiento
- Aprovisionamiento de Recursos AWS
- Aprovisionamiento de Recursos Azure
- Aprovisionamiento de Recursos GCP
Módulo 6: Funcionalidades Avanzadas de Terraform
Módulo 7: Mejores Prácticas de Terraform
- Organización del Código
- Control de Versiones
- Pruebas del Código de Terraform
- Mejores Prácticas de Seguridad
Módulo 8: Terraform en CI/CD
- Integración de Terraform con CI/CD
- Automatización de Terraform con Jenkins
- Uso de Terraform con GitHub Actions
- Terraform Cloud y Enterprise
