Cómo almacenar en S3 los logs de los ALB generados por los Ingress de EKS

Hi world 🙂

En el multiuniverso de Kubernetes, los Ingress son uno de los recursos más importantes, ya que representan una de las formas principales de exponer nuestros servicios hacia el exterior. Para gestionarlos existen distintos controladores, siendo NGINX uno de los más conocidos. Sin embargo, recientemente se anunció que dejará de recibir soporte oficial en 2026.

En esta entrada trabajaremos con el AWS Load Balancer Controller, la opción nativa y soportada por AWS, la cual nos permite crear y administrar Elastic Load Balancers (ALB/NLB) directamente desde nuestros clústeres de Kubernetes.

El Ingress es fundamental porque todos los requests externos hacia nuestras aplicaciones llegarán primero al balanceador de carga. En escenarios reales, con tráfico de clientes entrando constantemente, es normal que necesitemos verificar si los requests están llegando correctamente o realizar troubleshooting cuando se nos reporta algún problema. Para esto, habilitar ciertas features en los balanceadores —como el almacenamiento de logs— se vuelve indispensable.

Para mantener homogeneidad y asegurarnos de que todos los balanceadores creados desde el clúster tengan estas funcionalidades activadas por defecto, la mejor práctica es aplicar algunos ajustes directamente en el controlador.

En esta entrada veremos cómo almacenar en un bucket de Amazon S3 los logs de acceso, conexión y health checks generados por los Application Load Balancers (ALB) creados a través de los Ingress de Kubernetes. De esta forma, implementaremos un mecanismo centralizado de auditoría y observabilidad que nos permitirá consultar los eventos del balanceador, comprender con mayor precisión el comportamiento del tráfico y diagnosticar de manera más eficiente los errores reportados.

Requisitos

  • Tener una cuenta de AWS.
  • Tener instalado Terraform.
  • Tener instalado Helm.

Esta es la estrcura del proyecto:

Primero necesitamos exportar nuestras credenciales de AWS en nuestra consola local;

    • export AWS_ACCESS_KEY_ID="XXXXXXXXXX"
      export AWS_SECRET_ACCESS_KEY="XXXXXXXXXX"
    • Comprobamos que estemos conectados de forma correcta
      •  aws sts get-caller-identity

Nota: Antes de continuar, te recomiendo revisar esta entrada y completar todo lo relacionado con la creación del IAM Role necesario para desplegar el clúster.

A continuación, iremos creando cada uno de los archivos de Terraform necesarios para definir y desplegar la infraestructura de este proyecto.

1- 0-provider.tf, donde definiremos los proveedores que utilizaremos en este proyecto.

terraform {
  required_version = ">= 1.3.2"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 5.95, < 6.0.0"
    }
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = ">= 2.20"
    }
    helm = {
      source  = "hashicorp/helm"
      version = "= 2.17.0"
    }
    kubectl = {
      source  = "gavinbunney/kubectl"
      version = ">= 1.14"
    }
  }
}
provider "aws" {
  region = local.region
  default_tags {
   tags = local.common_tags
  }
  assume_role {
    role_arn = var.terraformrole
  }
}
provider "kubernetes" {
  host                   = module.eks.cluster_endpoint
  cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data)
  exec {
    api_version = "client.authentication.k8s.io/v1beta1"
    command     = "aws"
    args = ["eks", "get-token", "--cluster-name", module.eks.cluster_name]
  }
}
provider "helm" {
  debug = true
  kubernetes {
    host                   = module.eks.cluster_endpoint
    cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data)
    exec {
      api_version = "client.authentication.k8s.io/v1beta1"
      command     = "aws"
      args = ["eks", "get-token", "--cluster-name", module.eks.cluster_name]
    }  
  }
}
provider "kubectl" {
  apply_retry_count      = 10
  host                   = module.eks.cluster_endpoint
  cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data)
  load_config_file       = false
  exec {
    api_version = "client.authentication.k8s.io/v1beta1"
    command     = "aws"
    args = ["eks", "get-token", "--cluster-name", module.eks.cluster_name]
    }
}

2- 01-locals.tf, definiremos las variables locales del proyecto. Estas variables ayudan a centralizar valores que se utilizarán en múltiples recursos, simplificando la configuración y facilitando el mantenimiento del código.

locals {
  ng_name       = "demo"
  region        = var.region
  cluster_name  = var.cluster_name
  tags          = var.tags
  cluster_version      = var.cluster_version
  bucket_source        = "${var.environment}-${var.region}-${var.project_name}-source"
  common_tags = {
    Environment = var.environment
    Terraform   = "true"  
  }
}

3- En el archivo 02-s3.tf definiremos la creación del bucket de Amazon S3, junto con las configuraciones necesarias para que los Application Load Balancers (ALB) puedan almacenar allí sus logs de manera segura y centralizada.

data "aws_elb_service_account" "main" {}
data "aws_caller_identity" "current" {}
resource "aws_s3_bucket" "source_bucket" {
  bucket = local.bucket_source
  tags = local.tags
}
resource "aws_s3_bucket_versioning" "source_bucket_versioning" {
  bucket = aws_s3_bucket.source_bucket.id
  versioning_configuration {
    status = "Enabled"
  }
}
resource "aws_s3_bucket_policy" "source_bucket_policy" {
  bucket = aws_s3_bucket.source_bucket.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          AWS = data.aws_elb_service_account.main.arn
        }
        Action   = "s3:PutObject"
        Resource = "arn:aws:s3:::${local.bucket_source}/*"
      },
      {
        Effect = "Allow"
        Principal = {
          Service = "logdelivery.elasticloadbalancing.amazonaws.com"
        }
        Action   = "s3:PutObject"
        Resource = "arn:aws:s3:::${local.bucket_source}/*"
      },
      {
        Effect = "Allow"
        Principal = {
          Service = "logdelivery.elasticloadbalancing.amazonaws.com"
        }
        Action   = "s3:GetBucketAcl"
        Resource = "arn:aws:s3:::${local.bucket_source}"
      }
    ]
  })
}
resource "aws_s3_bucket_lifecycle_configuration" "source_bucket_lifecycle" {
  bucket = aws_s3_bucket.source_bucket.id
  rule {
    id = "rule-1"
    filter {
      prefix = ""
    }
    status = "Enabled"
    transition {
      days          = 30
      storage_class = "STANDARD_IA"
    }
    expiration {
      days = 365
    }
  }
}
4- 03-vpc.tf, definiremos todos los recursos y configuraciones necesarios para la creación de la VPC que servirá como base de la infraestructura del clúster.
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.2.0"
  name                   = var.vpc_name
  cidr                   = var.vpc_base_cidr
  azs                    = var.availability_zones == [] ? var.availability_zones : data.aws_availability_zones.available.names
  private_subnets        = split(",", var.private_subnets)
  private_subnet_tags    = merge(var.private_subnet_tags, local.common_tags)
  public_subnets         = split(",", var.public_subnets)
  public_subnet_tags     = merge(var.public_subnet_tags, local.common_tags)
  database_subnets       = split(",", var.isolated_subnets)
  database_subnet_tags   = merge(var.isolated_subnet_tags, local.common_tags)
  enable_nat_gateway     = var.enable_nat_gateway
  single_nat_gateway     = var.single_nat_gateway
  enable_vpn_gateway     = var.enable_vpn_gateway
  one_nat_gateway_per_az = var.one_nat_gateway_per_az
  enable_dns_hostnames   = var.enable_dns_hostnames
  enable_dns_support     = var.enable_dns_support
  tags                   = merge(var.vpc_tags, local.common_tags)
}
data "aws_availability_zones" "available" {}
5- 04-eks.tf, definiremos todos los recursos y configuraciones necesarios para la correcta creación del clúster. En este caso utilizaremos AMIs basadas en Amazon Linux 2023 (AL2023), ya que Amazon Linux 2 (AL2) para EKS será deprecado próximamente. Además, habilitaremos el AWS Load Balancer Controller mediante el módulo eks_kubernetes_addons, junto con otras configuraciones fundamentales para garantizar el correcto funcionamiento del clúster.
module "eks" {
  source                          = "terraform-aws-modules/eks/aws"
  version                         = "20.37.2"
  cluster_name                    = var.cluster_name
  cluster_version                 = var.cluster_version
  vpc_id                          = module.vpc.vpc_id
  subnet_ids                      = module.vpc.private_subnets
  cluster_endpoint_private_access = var.cluster_endpoint_private_access
  cluster_endpoint_public_access  = var.cluster_endpoint_public_access
  enable_irsa                     = var.enable_irsa
  authentication_mode             = "API_AND_CONFIG_MAP"
  cluster_enabled_log_types       = var.cluster_enabled_log_types
  cluster_service_ipv4_cidr       = var.service_cidr_block
  enable_cluster_creator_admin_permissions = true
  cloudwatch_log_group_retention_in_days   = var.cloudwatch_log_group_retention_in_days
  cluster_encryption_config = { resources = ["secrets"] }
  create_kms_key            = true
  # Add-ons administrados por EKS
  cluster_addons = {
    # VPC CNI primero, así los nodos ya nacen con la config correcta
    vpc-cni = {
      most_recent    = true
      before_compute = true
    }
    coredns    = { most_recent = true }
    kube-proxy = { most_recent = true }
  }
  # ===== Managed Node Group con AMI personalizada AL2023 =====
  eks_managed_node_groups = {
    ng-custom = {
      ami_type = "AL2023_x86_64_STANDARD" # Esto es para AL2023
      name     = "custom-demo-al2023"
      capacity_type              = "SPOT"
      ami_id   = data.aws_ami.eks_default_al2023.image_id
      instance_types = var.instance_types
      min_size       = 1
      max_size       = 3
      desired_size   = 1
      enable_bootstrap_user_data = true
      enable_monitoring       = true
      update_config = {
        max_unavailable_percentage = 33
      }
      tags = {
        "k8s.io/cluster-autoscaler/enabled"             = "true"
        "k8s.io/cluster-autoscaler/${var.cluster_name}" = "owned"
      }
      iam_role_additional_policies = {
        AmazonSSMManagedInstanceCore = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
      }
    },
  }
  tags = merge(var.tags, local.common_tags)
  # Agregar una regla nueva al SG del los worker node
  node_security_group_additional_rules = {
    ingress_allow_access_from_control_plane = {
      type                          = "ingress"
      protocol                      = "-1"
      from_port                     = 0
      to_port                       = 0
      source_cluster_security_group = true
      description                   = "Allow access from control plane to webhook port of AWS load balancer controller"
    }
  }
}
module "aws_auth" {
  source = "github.com/terraform-aws-modules/terraform-aws-eks//modules/aws-auth?ref=v20.37.2"
  manage_aws_auth_configmap = true
  aws_auth_roles = concat(
    [
      {
        rolearn  = var.terraformrole
        username = "terraform-role-Eks"
        groups   = ["system:masters"]
      },
      {
        rolearn  = var.admin_sso_role_arn
        username = "admin-role"
        groups   = ["system:masters"]
      }
    ],
    [
      for ng in module.eks.eks_managed_node_groups :
      {
        rolearn  = ng.iam_role_arn
        username = "system:node:{{EC2PrivateDNSName}}"
        groups   = ["system:bootstrappers", "system:nodes"]
      }
    ]
  )
  depends_on = [module.eks]
}
module "eks_kubernetes_addons" {
  source = "github.com/aws-ia/terraform-aws-eks-blueprints//modules/kubernetes-addons?ref=v4.32.1"
  depends_on = [null_resource.kubeconfig]
  eks_cluster_id       = module.eks.cluster_name
  eks_cluster_endpoint = module.eks.cluster_endpoint
  eks_oidc_provider    = module.eks.oidc_provider
  eks_cluster_version  = module.eks.cluster_version
  enable_cluster_autoscaler = true
  enable_metrics_server      = true
  metrics_server_helm_config = {
    name        = "metrics-server"
    namespace   = "kube-system"
    description = "Metrics Server Helm chart deployment configuration"
  }
  enable_aws_load_balancer_controller      = true
  aws_load_balancer_controller_helm_config = {
    name       = "aws-load-balancer-controller"
    namespace  = "kube-system"
    description = "aws-load-balancer-controller Helm chart deployment configuration"
     version     = "3.1.0"
    values = [templatefile("${path.module}/values/aws_lb_controller_values.yaml", {
      cluster_name = var.cluster_name
      tags_values = indent(2, yamlencode(local.tags))
      bucket_name = local.bucket_source
    })]
  }
  tags = local.tags
}
resource "null_resource" "kubeconfig" {
  depends_on = [module.eks]
  provisioner "local-exec" {
    interpreter = ["/bin/bash", "-c"]
    command = <<EOT
      set -e
      echo 'Updating Kube config file'
      # aws sts get-caller-identity
      export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" \
             $(aws sts assume-role \
                   --role-arn ${var.terraformrole} \
                   --role-session-name Jenkins-EKS-Setup \
                   --query "Credentials.[AccessKeyId,SecretAccessKey,SessionToken]" \
                   --output text))
      aws sts get-caller-identity
      aws eks wait cluster-active --name '${module.eks.cluster_name}'  --region '${local.region}'
      aws eks --region ${local.region} update-kubeconfig --name ${module.eks.cluster_name} --alias ${module.eks.cluster_name}
    EOT
  }
}
data "aws_ami" "eks_default_al2023" {
  most_recent = true
  owners      = ["amazon"]
  filter {
    name   = "name"
    #amazon-eks-node-al2023-x86_64-standard-1.31-v20250212
    values = ["amazon-eks-node-al2023-x86_64-standard-${var.cluster_version}-v*"]
  }
}
data "aws_iam_role" "alb_controller" {
  name = "${var.cluster_name}-aws-load-balancer-controller-sa-irsa"
  depends_on = [module.eks_kubernetes_addons]
}
resource "aws_iam_policy" "alb_extra" {
  name = "${local.cluster_name}-alb-controller-extra"
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "elasticloadbalancing:AddTags",
          "elasticloadbalancing:DescribeListenerAttributes",
          "elasticloadbalancing:RemoveTags"
        ]
        Resource = "*"
      }
    ]
  })
}
resource "aws_iam_role_policy_attachment" "alb_extra" {
  role       = data.aws_iam_role.alb_controller.name
  policy_arn = aws_iam_policy.alb_extra.arn
  depends_on = [module.eks_kubernetes_addons]
}
6- 05-variables.tf, definiremos todas las variables necesarias para parametrizar el proyecto, mientras que en 06-outputs.tf configuraremos las salidas que Terraform mostrará una vez ejecutemos terraform apply. El contenido completo de ambos archivos lo puedes consultar en el repositorio de GitHub, ya que en esta entrada omitiremos su detalle para no hacerla demasiado extensa.
7-07-demo-eks.auto.tfvars, estableceremos todos los valores necesarios para desplegar nuestro entorno. Esto nos permitirá adoptar un enfoque más dinámico, flexible y reutilizable.
region          = "us-east-1"
environment     = "poc"
project_name    = "demo"
terraformrole   = "arn:aws:iam::123456789012:role/TerraformRole-Eks"
days            = 60
admin_sso_role_arn = "arn:aws:iam::123456789012:role/AWSReservedSSO_AWSAdministratorAccess_j45jkasdlyhg56"
vpc_name               = "demo"
vpc_base_cidr          = "10.1.0.0/16"
private_subnets        = "10.1.11.0/24,10.1.12.0/24,10.1.13.0/24"
public_subnets         = "10.1.21.0/24,10.1.22.0/24,10.1.23.0/24"
isolated_subnets       = "10.1.31.0/24,10.1.32.0/24,10.1.33.0/24"
public_subnet_tags = {
  "Name"                              = "Public-Subnet"
  "kubernetes.io/role/elb"            = 1
  "kubernetes.io/role/internal-elb"   = 1
  "kubernetes.io/cluster/demo" = "owned" 
}
private_subnet_tags = {
  "Name"                              = "Private-Subnet"
  "kubernetes.io/role/elb"            = 1
  "kubernetes.io/role/internal-elb"   = 1
  "kubernetes.io/cluster/demo" = "owned"
}
cluster_name                    = "demo"
cluster_version                 = "1.35"
cluster_endpoint_private_access = false # To change in real case
cluster_endpoint_public_access  = true  # To change in real case
instance_types = ["t3.small","t3.medium","t3.large"]
desired_size = 2
min_size     = 1
max_size     = 5
cloudwatch_log_group_retention_in_days = 5
service_cidr_block = "192.168.0.0/22"
tags = {
  Country  = "UY"
  Region   = "AMERICA"
}
8-values/aws_lb_controller_values.yaml definiremos la configuración del AWS Load Balancer Controller, incluyendo los parámetros necesarios para que los nuevos ALB creados por los Ingress almacenen automáticamente sus logs en el bucket de S3 definido previamente.
clusterName: ${cluster_name}
# Enable Shield addon for ALB (default true)
enableShield: false
# Enable WAF addon for ALB (default true)
enableWaf: false
#
# Enable WAF V2 addon for ALB (default true)
enableWafv2: false
defaultTags:
  ${tags_values}
# Habilitando S3 para salvar los logs de todos los ALB por default
ingressClassParams:
  create: true
  name: alb
  spec:
    loadBalancerAttributes:
      # access_logs
      - key: access_logs.s3.enabled
        value: "true"
      - key: access_logs.s3.bucket
        value: "${bucket_name}"
      - key: access_logs.s3.prefix
        value: "poc-alb-accesslogs"
      # connection_logs
      - key: connection_logs.s3.enabled
        value: "true"
      - key: connection_logs.s3.bucket
        value: "${bucket_name}"
      - key: connection_logs.s3.prefix
        value: "poc-alb-connectionlogs"
      # health_check
      - key: health_check_logs.s3.enabled
        value: "true"
      - key: health_check_logs.s3.bucket
        value: "${bucket_name}"
      - key: health_check_logs.s3.prefix
        value: "poc-alb-healthchecklogs"
13- Con todos los archivos configurados, ya estamos listos para ejecutar Terraform.
  • terraform init
  • terraform plan
  • terraform apply
Una ves que el cluster este creado,  en la carpeta Test-Ingress crearemos el fichero app.yaml, en el que definiremos los recursos de Kubernetes necesarios para validar que toda la configuración funciona correctamente. Una vez desplegado este manifiesto, se creará automáticamente un Ingress asociado a un ALB, y dicho balanceador tendrá habilitada la configuración necesaria para almacenar sus logs en el bucket de S3 que hemos creado previamente.
apiVersion: v1
kind: Namespace
metadata:
  creationTimestamp: null
  name: demo
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: example-app
  namespace: demo
  labels:
    app: example-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: example-app
  template:
    metadata:
      labels:
        app: example-app
    spec:
      containers:
        - name: example-app
          image: nginx:latest
          ports:
            - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: example-app-service
  namespace: demo
  labels:
    app: example-app
spec:
  type: ClusterIP
  selector:
    app: example-app
  ports:
    - port: 80
      targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: example-app-ingress
  namespace: demo
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP":80}]'
    alb.ingress.kubernetes.io/group.name: "demo"
    alb.ingress.kubernetes.io/subnets: subnet-id, subnet-id, subnet-id # Public Subnet
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/actions.ssl-redirect: |
      {"Type": "redirect", "RedirectConfig": {"Protocol": "HTTPS", "Port": "443", "StatusCode": "HTTP_301"}}
  labels:
    app: example-app
spec:
  ingressClassName: alb
  rules:
    - host: demo.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: example-app-service
                port:
                  number: 80

Para desplegar este manifiesto, solo debes ejecutar el siguiente comando: kubectl apply -f Test-Ingress/app.yaml.

Después de esto, podemos verificar que los recursos se hayan creado correctamente ejecutando el siguiente comando:

  •  kubectl get pod, svc, ing -n demo

Una vez validado el despliegue, también podemos generar algo de tráfico hacia el ALB para comprobar su funcionamiento y forzar la generación de logs. Para ello, podemos utilizar los siguientes comandos:

  • Probar conectividad básica usando el DNS del ALB: curl -v http://ALB-DNS
  • Con el header Host para simular el dominio: curl -v -H "Host: demo.example.com" http://ALB-DNS

A continuación, desde la consola de AWS validaremos que la configuración del ALB se haya aplicado correctamente y que los logs se estén guardando de forma exitosa en el bucket de S3 definido previamente.

 

Otros comandos para obtener información,

Conclusiones

En esta entrada vimos cómo habilitar de forma automática el almacenamiento de los logs generados por cada Ingress del clúster en un bucket de Amazon S3. Implementar este mecanismo es una práctica muy importante a nivel operativo, ya que nos permite centralizar la información de acceso y disponer de un historial de eventos que puede ser clave durante tareas de monitoreo, auditoría y troubleshooting.

Contar con estos logs resulta especialmente útil cuando es necesario investigar incidentes de conectividad, errores de acceso o comportamientos inesperados reportados por clientes o equipos internos. Al tener los registros almacenados de forma persistente en S3, no solo mejoramos la trazabilidad de las solicitudes que llegan a nuestras aplicaciones, sino que también facilitamos el análisis posterior de la información.

Además, una de las grandes ventajas de almacenar estos logs en S3 es que luego pueden ser consultados con servicios como Amazon Athena, lo que nos permite ejecutar consultas SQL directamente sobre los archivos almacenados sin necesidad de aprovisionar infraestructura adicional. De esta forma, es posible interpretar los logs de una manera mucho más legible, filtrar eventos específicos, identificar patrones de error y acelerar considerablemente el proceso de diagnóstico.

En definitiva, automatizar la publicación de logs de los Ingress hacia S3 no solo mejora la capacidad de observabilidad de la plataforma, sino que también aporta una base sólida para realizar análisis más avanzados y responder de forma más eficiente ante incidentes en producción.

¡Nos vemos en la próxima entrada!
Nota: Todo el código lo pueden encontrar en el siguiente repositorio.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.