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
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.
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 } }}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" {}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]}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.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 = 60admin_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 casecluster_endpoint_public_access = true # To change in real caseinstance_types = ["t3.small","t3.medium","t3.large"]desired_size = 2min_size = 1max_size = 5cloudwatch_log_group_retention_in_days = 5service_cidr_block = "192.168.0.0/22"tags = { Country = "UY" Region = "AMERICA"}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: falsedefaultTags: ${tags_values}# Habilitando S3 para salvar los logs de todos los ALB por defaultingressClassParams: 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"terraform initterraform planterraform apply
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: v1kind: Namespacemetadata: creationTimestamp: null name: demo---apiVersion: apps/v1kind: Deploymentmetadata: name: example-app namespace: demo labels: app: example-appspec: 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: v1kind: Servicemetadata: name: example-app-service namespace: demo labels: app: example-appspec: type: ClusterIP selector: app: example-app ports: - port: 80 targetPort: 80---apiVersion: networking.k8s.io/v1kind: Ingressmetadata: 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-appspec: ingressClassName: alb rules: - host: demo.example.com http: paths: - path: / pathType: Prefix backend: service: name: example-app-service port: number: 80Despué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
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.
Nota: Todo el código lo pueden encontrar en el siguiente repositorio.











