EKS y las Zonas Locales usando Terraform

Hello World 🙂

Seguimos avanzando con nuestros ejercicios 100% prácticos sobre Terraform y la creación de recursos en AWS. En esta ocasión, nos enfocaremos en documentar un caso de uso muy interesante que tuve el placer de aplicar en un proyecto real. Durante este ejercicio, crearemos una VPC que extenderemos hacia las Zonas Locales de AWS.

Pero antes de entrar en detalle, es fundamental definir este concepto y entender qué son las Zonas Locales y cómo pueden beneficiar a los clientes.

Las zonas locales de AWS son un tipo de despliegue de infraestructura que coloca determinados servicios de AWS más cerca de sus usuarios finales y cargas de trabajo.

Nos permite ejecutar aplicaciones que requieran una latencia de milisegundos de un solo dígito o un procesamiento de datos local al acercar la infraestructura de AWS a sus usuarios finales y centros de negocios.

Tomado de AWS.

En muchas ocasiones, ya sea por requisitos de la aplicación o del cliente, es necesario que los datos o la aplicación estén en el país o lo más cerca posible del usuario final. Para cumplir con este objetivo, AWS nos ofrece las Zonas Locales, una solución que permite extender nuestras VPCs a ubicaciones geográficas cercanas. En esta ocasión, vamos a combinar una vez más la VPC con un clúster de EKS, pero esta vez nuestros nodos de trabajo o worker nodes estarán específicamente en una Zona Local. También crearemos un EFS, que se integra fácilmente con EKS para compartir información, y configuraremos un JumBox que nos permitirá administrar el clúster.

Es importante destacar que no todos los servicios están disponibles en las Zonas Locales, por lo que es fundamental revisar la documentación para verificar qué servicios están habilitados en la zona en la que queremos trabajar. Por ejemplo, EFS no está disponible en ninguna Zona Local y tampoco todos los tipos de instancias están soportados. Aunque EBS está disponible en todas las Zonas Locales, en muchas de ellas solo podemos usar volúmenes GP2. Por esto, es crucial consultar la documentación antes de comenzar.

Una nota aparte para EKS. Aunque la documentación indica que EKS es compatible con todas las Zonas Locales, es importante aclarar que las subredes principales del clúster deben estar ubicadas en las AZs (Availability Zones) principales de la región. Por ejemplo, en el caso de us-east-1, estas subredes serían us-east-1a, us-east-1b, us-east-1c, us-east-1d, us-east-1e, y us-east-1f. Los nodos trabajadores, en cambio, serán los que se ubiquen en la Zona Local.

También es importante señalar que los nodos trabajadores en las Zonas Locales son de tipo self_managed_node_groups, ya que la documentación de AWS sobre los managed_node_groups lo deja claro: Managed node groups can’t be deployed on AWS Outposts, AWS Wavelength, or AWS Local Zones. No se preocupen, veremos todo esto más adelante de forma práctica en el código. Sin embargo, es fundamental tener estos conceptos claros para evitar confusiones a lo largo del proceso.

En el ejercicio de hoy, trabajaremos con la Zona Local de Buenos Aires(us-east-1-bue-1a). Supongamos que tenemos un cliente en Argentina cuyo requisito es minimizar la latencia. Utilizando una Zona Local, podremos cumplir con esa necesidad de forma efectiva. En esta zona local los tipos de instancias permitidos son T3, C5, R5, G4dn y M5 y solo podemos usar GP2.

¡Prepárate para adentrarte en el mundo de la automatización de infraestructura con Terraform!

Requisitos:

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

En esta ocasión el proyecto va a estar formado por 4 carpetas;

    • En la carpeta ‘1-iam-role-eks’, definiremos todo lo necesario para crear el rol que se encargará de gestionar la creación del clúster EKS y los otros recursos.
    • En la carpeta ‘2-vpc’, configuraremos todo lo necesario para crear la VPC.
    • En la carpeta ‘3-eks-efs-ec2’, estableceremos los elementos requeridos para desplegar el clúster EKS, el sistema de archivos EFS, y una instancia EC2 que servirá como Jumbox, además de otros recursos como los grupos de seguridad.
    • Finalmente, en la carpeta ‘4-aws-auth’, configuraremos el módulo que editará el config mapaws-authdentro del clúster.

1- Primero debemos acceder a la UI de AWS para activar la zona local que queremos usar, ya que por defecto vienen deshabilitadas;

  • Vamos a la consola de EC2
    • Seleccionamos la Zona local de Buenos Aires.
    • Despues de un par de minutos ya tendremos la Zona Loca habilitada.

2- 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

3- Crear la carpeta 1-iam-role-eks y dentro de ella los siguientes ficheros;

  • 1-iam.tf; definiremos el provider, el rol que vamos a crear y las políticas que le vamos a adjuntar.
    • provider "aws" {
        region = local.region
      }
      terraform {
        required_version = ">= 1.0"
        required_providers {
          aws = {
            source  = "hashicorp/aws"
            version = ">= 4.47"
          }
        }
      }
      locals {
        region      = "us-east-1"
        role_name     = "TerraformRole-Eks"
      }
      resource "aws_iam_role" "terraform_role" {
        name               = local.role_name
        assume_role_policy = jsonencode({
          "Version" : "2012-10-17",
          "Statement" : [
            {
              "Effect" : "Allow",
              "Principal" : {
                "AWS" : [
                  "arn:aws:iam::012345678901:user/poc", # To change
                  "arn:aws:iam::012345678901:root" # To change
                ]
              },
              "Action" : "sts:AssumeRole"
            }
          ]
        })
      }
      resource "aws_iam_role_policy_attachment" "attachment_policy" {
        role       = aws_iam_role.terraform_role.name
        policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"
      }
  • 2-output.tf, definiremos la salida del ARN del rol.
    • output "provisioner_role_arn" {
        value = aws_iam_role.terraform_role.arn
      }
  • Después de crear esos ficheros ya estamos en condiciones de ejecutar Terraform
    • terraform init
    • terraform plan
    • terraform apply
        • Copiar el ARN que se imprime en la salida.
  • Una vez que hemos creado el rol, es necesario editarlo para que pueda asumirse a sí mismo. Esto se logra tomando el ARN del rol y añadiendo assumed-role. El formato sería algo como: arn:aws:sts::012345678901:assumed-role/TerraformRole-Eks/demo.Este ajuste puede hacerse desde la consola de AWS. Simplemente navega a IAM, busca el rol, y en la sección Trust relationships agrega una nueva entrada con la información mencionada. Alternativamente, también puedes realizar este cambio directamente desde Terraform.Este procedimiento es una buena práctica de seguridad, ya que permite evitar la creación de recursos mediante un usuario específico. Al usar este rol para la creación de recursos, obtenemos mucho más control y seguridad sobre nuestra infraestructura.
    • A continuación, te muestro cómo quedaría el rol en AWS.
  • Nota: Este paso es crucial, ya que este rol será responsable de crear nuestro clúster de EKS y todos los recursos asociados. Si no lo configuramos correctamente, podríamos encontrarnos con problemas más adelante durante la creación de estos recursos. Asegurarnos de que esté bien configurado desde el principio nos evitará inconvenientes en el futuro.

4- Crear la carpeta 2-vpc y dentro de ella los siguientes ficheros;

  • En el fichero 1-vpc.tf, definiremos todo lo necesario para crear nuestra VPC con sus respectivas subredes, incluyendo las subredes en la zona local.
    • rovider "aws" {
        region = "us-east-1"
      }
      locals {
        name   = var.name
        region = var.region
        vpc_cidr     = var.vpc_cidr
        cluster_name = var.cluster_name
        azs          = ["${local.region}a", "${local.region}b", "${local.region}c"]
        lzs          = var.lzs
      }
      module "vpc" {
        source = "terraform-aws-modules/vpc/aws"
        name            = var.name
        cidr            = var.vpc_cidr
        azs             = local.azs
        public_subnets  = [for k, v in local.azs : cidrsubnet(var.vpc_cidr, 8, k)]
        private_subnets = [for k, v in local.azs : cidrsubnet(var.vpc_cidr, 8, k + 10)]
        enable_nat_gateway = true
        single_nat_gateway = true
        create_igw         = true
        enable_dns_hostnames = true
        public_subnet_tags = {
          "kubernetes.io/cluster/${var.cluster_name}" = "shared"
          "kubernetes.io/role/elb"                    = "1"
        }
        private_subnet_tags = {
          "kubernetes.io/cluster/${var.cluster_name}" = "shared"
          "kubernetes.io/role/internal-elb"           = "1"
        }
      }
      resource "aws_subnet" "public-subnet-lz" {
        vpc_id                  = module.vpc.vpc_id
        cidr_block              = cidrsubnet(var.vpc_cidr, 8, 5)
        availability_zone       = local.lzs[0]
        map_public_ip_on_launch = true
        tags = merge(
          { "Name" = "${module.vpc.name}-public-${local.lzs[0]}" },
        )
      }
      resource "aws_subnet" "private-subnet-lz" {
        vpc_id            = module.vpc.vpc_id
        cidr_block        = cidrsubnet(var.vpc_cidr, 8, 10 + 5)
        availability_zone = local.lzs[0]
        tags = merge(
          { "Name" = "${module.vpc.name}-private-${local.lzs[0]}" },
        )
      }
      resource "aws_route_table_association" "public-subnet-lz-rta" {
        subnet_id      = aws_subnet.public-subnet-lz.id
        route_table_id = module.vpc.public_route_table_ids[0]
      }
      resource "aws_route_table_association" "private-subnet-lz-rta" {
        subnet_id      = aws_subnet.private-subnet-lz.id
        route_table_id = module.vpc.private_route_table_ids[0]
      }
  • 2-variables.tf, definiremos las variables necesarias para parametrizar nuestra VPC.
    • variable "name" {
        type = string
      }
      variable "vpc_cidr" {
        type = string
      }
      variable "cluster_name" {
        type = string
      }
      variable "region" {
        type = string
      }
      variable "lzs" {
        type = list(string)
      }
  • 3-demo.auto.tfvars, definiremos todos los parámetros necesarios para crear la VPC en nuestro entorno específico. Este archivo es dinámico y puede variar según el entorno en el que estemos trabajando, permitiéndonos ajustar fácilmente configuraciones como el rango de CIDR, las subred de la local zone, region, etc.
    • name         = "eks-lz"
      vpc_cidr     = "10.0.0.0/16"
      cluster_name = "demo-lz"
      region       = "us-east-1" 
      lzs          = ["us-east-1-bue-1a"]  # Zona Local de Buenos Aires
  • 4-outputs.tf, defineremos las salidas que queremos obtener al ejecutar terraform apply.
    • output "vpc_id" {
        description = "The ID of the VPC"
        value       = try(module.vpc.vpc_id, "")
      }
      output "vpc_id_cidr" {
        value = try(module.vpc.vpc_cidr_block)
      }
      output "public_subnets" {
        description = "List of IDs of public subnets"
        value       = module.vpc.public_subnets
      }
      output "private_subnets" {
        description = "List of IDs of private subnets"
        value       = module.vpc.private_subnets
      }
      output "public_subnets_local_zone" {
        value = aws_subnet.public-subnet-lz.id
      }
      output "private_subnets_local_zone" {
        value = aws_subnet.private-subnet-lz.id
      }
  • Después de crear esos ficheros ya estamos en condiciones de ejecutar Terraform
    • terraform init
    • terraform plan
    • terraform apply
        • Copiar los ID de las subredes y el de la VPC que lo vamos a precisar más adelante.

5- Crear la carpeta 3-eks-efs-ec2 y dentro de ella los siguientes ficheros;

  • 0-provider.tf, definiremos los proveedores que utilizaremos en este proyecto y el rol con el cual vamos a crear el clúster. Estos proveedores son esenciales, ya que permiten a Terraform interactuar con los distintos servicios de infraestructura en la nube necesarios para desplegar los recursos correctamente.
    • terraform {
        required_version = ">= 1.0"
        required_providers {
          aws = {
            source  = "hashicorp/aws"
            version = ">= 4.47"
          }
          kubernetes = {
            source  = "hashicorp/kubernetes"
            version = ">= 2.10"
          }
          helm = {
            source  = "hashicorp/helm"
            version = ">= 2.5.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 "helm" {
        kubernetes {
          host = module.eks_blueprints.eks_cluster_endpoint
          cluster_ca_certificate = base64decode(module.eks_blueprints.eks_cluster_certificate_authority_data)
          exec {
            api_version = "client.authentication.k8s.io/v1beta1"
            command     = "aws"
            # This requires the awscli to be installed locally where Terraform is executed
            args = ["eks", "get-token", "--cluster-name", module.eks_blueprints.eks_cluster_id]
          }
        }
      }
      provider "kubernetes" {
        host = module.eks_blueprints.eks_cluster_endpoint
        cluster_ca_certificate = base64decode(module.eks_blueprints.eks_cluster_certificate_authority_data)
        exec {
          api_version = "client.authentication.k8s.io/v1beta1"
          command     = "aws"
          # This requires the awscli to be installed locally where Terraform is executed
          args = ["eks", "get-token", "--cluster-name", module.eks_blueprints.eks_cluster_id]
        }
      }
      provider "kubectl" {
        apply_retry_count      = 10
        host  = module.eks_blueprints.eks_cluster_endpoint
        cluster_ca_certificate = base64decode(module.eks_blueprints.eks_cluster_certificate_authority_data)
        load_config_file       = false
        exec {
          api_version = "client.authentication.k8s.io/v1beta1"
          command     = "aws"
          # This requires the awscli to be installed locally where Terraform is executed
          args = ["eks", "get-token", "--cluster-name", module.eks_blueprints.eks_cluster_id]
          }
      }
  • 1-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-localzone"
        region        = var.region
        cluster_name  = var.cluster_name
        name          = basename(path.cwd)
        tags          = var.tags
        cluster_version    = var.cluster_version
        domain_for_route53 = var.domain_name_in_route53
        common_tags = {
          Environment = var.environment
          Terraform   = "true"  
        }
      }
  • 2-eks.tf, en este fichero, definiremos todo lo necesario para crear nuestro clúster de EKS. A diferencia de la entrada anterior donde hablamos de EKS y utilizamos el módulo terraform-aws-modules/eks/aws, en esta ocasión haremos uso del módulo terraform-aws-eks-blueprints. Este módulo nos permitirá crear nuestros nodos de trabajo sin inconvenientes en la zona local utilizando el tipo self_managed_node_groups, ya que como mencionamos al principio, según la documentación de AWS, los nodos de tipo managed_node_groups no se pueden crear en una Zona Local.
    • module "eks_blueprints" {
        source = "github.com/aws-ia/terraform-aws-eks-blueprints?ref=v4.25.0"
        cluster_name       = var.cluster_name
        vpc_id             = var.vpc_id
        private_subnet_ids = var.private_subnets
        cluster_version                        = var.cluster_version
        cluster_service_ipv4_cidr              = var.service_cidr_block
        cloudwatch_log_group_retention_in_days = var.cloudwatch_log_group_retention_in_days
        cluster_enabled_log_types              = ["audit", "api", "authenticator", "controllerManager", "scheduler"]
        cluster_endpoint_public_access         = var.cluster_endpoint_public_access
        cluster_endpoint_private_access        = var.cluster_endpoint_private_access
        #EKS LOCAL ZONE NODE GROUP
        self_managed_node_groups = {
          smng = {
            node_group_name       = "smng"
            instance_type         = "t3.medium" # Tipo de instancia soportado en la zona local de Buenos Aires
            subnet_ids            = [var.private_subnets_local_zone]
            launch_template_os    = "amazonlinux2eks" # amazonlinux2eks  or bottlerocket or windows
            enable_monitoring     = true
            min_size              = "1"
            max_size              = "5"
            desired_size          = "3"
            block_device_mappings = [
              {
                device_name = "/dev/xvda"
                volume_type = "gp2"
                volume_size = "20"
              },
            ]
          },
        }
        cluster_security_group_additional_rules = {
            ingress_nodes = {
            description                = "Allow all connections from nodes"
            protocol                   = "-1"
            from_port                  = 0
            to_port                    = 0
            type                       = "ingress"
            source_node_security_group = true
          }
        }
        node_security_group_additional_rules = {
          # Extend node-to-node security group rules. Recommended and required for the Add-ons
          ingress_self_all = {
            description = "Node to node all ports/protocols"
            protocol    = "-1"
            from_port   = 0
            to_port     = 0
            type        = "ingress"
            self        = true
          }
          # Recommended outbound traffic for Node groups
          egress_all = {
            description      = "Node all egress"
            protocol         = "-1"
            from_port        = 0
            to_port          = 0
            type             = "egress"
            cidr_blocks      = ["0.0.0.0/0"]
            ipv6_cidr_blocks = ["::/0"]
          }
          # Custom rule
          egress_artifactory = {
            description      = "CIDR artifactory"
            protocol         = "tcp"
            from_port        = 80
            to_port          = 80
            type             = "egress"
            cidr_blocks      = ["10.2.0.0/24"]
          }
          ingress_cluster_to_node_all_traffic = {
            description                   = "Cluster API to Nodegroup all traffic"
            protocol                      = "-1"
            from_port                     = 0
            to_port                       = 0
            type                          = "ingress"
            source_cluster_security_group = true
          }
        }
      }
      #Agregar reglas al SG del cluster
      resource "aws_security_group_rule" "allow_node_sg_to_cluster_sg" {
        description = "Self-managed Nodegroup to Cluster API/Managed Nodegroup all traffic"
        source_security_group_id = module.eks_blueprints.worker_node_security_group_id
        security_group_id        = module.eks_blueprints.cluster_primary_security_group_id
        type                     = "ingress"
        protocol                 = "-1"
        from_port                = 0
        to_port                  = 0
        depends_on = [
          module.eks_blueprints
        ]
      }
      resource "aws_security_group_rule" "allow_node_sg_from_cluster_sg" {
        description = "Cluster API/Managed Nodegroup to Self-Managed Nodegroup all traffic"
        source_security_group_id = module.eks_blueprints.cluster_primary_security_group_id
        security_group_id        = module.eks_blueprints.worker_node_security_group_id
        type                     = "ingress"
        protocol                 = "-1"
        from_port                = 0
        to_port                  = 0
        depends_on = [
          module.eks_blueprints
        ]
      }
      #Agregar reglas al SG de los nodos
      resource "aws_security_group_rule" "worker_node_sg_update_egress" {
        depends_on = [module.eks_blueprints]
        description = "Allow artifactory.vficloud.net CIDR"
        from_port         = 443
        to_port           = 443
        protocol          = "tcp"
        cidr_blocks       = var.artifactory
        type              = "egress"
        security_group_id = module.eks_blueprints.worker_node_security_group_id
      }
      resource "aws_security_group_rule" "worker_node_sg_update_ingress" {
        depends_on = [module.eks_blueprints]
        description = "Allow artifactory.vficloud.net CIDR"
        from_port         = 443
        to_port           = 443
        protocol          = "tcp"
        cidr_blocks       = var.artifactory
        type              = "ingress"
        security_group_id = module.eks_blueprints.worker_node_security_group_id
      }
      # Aquí pasamos los roles o usuarios que queremos que interactúan con el clúster.
      module "eks_auth" {
        source     = "../4-aws-auth/"
        eks        = module.eks_blueprints
        depends_on = [null_resource.kubeconfig]
        map_roles = [
          {
            rolearn  = "arn:aws:iam::012345678901:role/TerraformRole-Eks" # To change
            username = "admin-role"
            groups   = ["system:masters"]
          }
        ]
        map_users = [
          {
            userarn = "arn:aws:iam::012345678901:root" # To change
            username = "root"
            groups   = ["system:masters"]
          },
          {
            userarn = "arn:aws:iam::012345678901:user/poc" # To change
            username = "poc"
            groups   = ["system:masters"]
          }
        ]
        map_accounts = []
      }
      resource "null_resource" "kubeconfig" {
        depends_on = [module.eks_blueprints]
        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 TerraformRole \
                         --query "Credentials.[AccessKeyId,SecretAccessKey,SessionToken]" \
                         --output text))
            aws sts get-caller-identity
            aws eks wait cluster-active --name '${module.eks_blueprints.eks_cluster_id}'  --region '${local.region}'
            aws eks --region ${local.region} update-kubeconfig --name ${module.eks_blueprints.eks_cluster_id} --alias ${module.eks_blueprints.eks_cluster_id}
          EOT
        }
      }
  • 3-efs.tf, definiremos todo lo necesario para la creación del EFS, permitiendo que esté preparado para integrarse con el clúster EKS.
    • resource "aws_efs_file_system" "demo_efs" {
        creation_token = "demo"
        performance_mode = "generalPurpose"
        throughput_mode  = "bursting"
        encrypted        = true
        tags = {
          Name = "demo"
        }
      }
      resource "aws_efs_mount_target" "aws_efs" {
        for_each = toset(var.private_subnets)
        file_system_id  = aws_efs_file_system.demo_efs.id
        subnet_id       = each.key
        security_groups = [aws_security_group.efs.id]
      }
  • 4-ec2.tf, configuraremos nuestro servidor JumpBox, que nos permitirá acceder al clúster de EKS. A través de user_data, instalaremos las herramientas necesarias para la gestión y administración del clúster, como Git, kubectl, Helm y Terraform. Este servidor funcionará como un punto de control seguro y centralizado para nuestras operaciones en el entorno de Kubernetes.
    • resource "aws_instance" "jumbox" {
          ami = data.aws_ami.amazon_linux.id
          instance_type = "t2.micro"
          iam_instance_profile = "${aws_iam_instance_profile.demo_profile.name}"
          subnet_id =  "subnet-id" # To change
          associate_public_ip_address= true
          vpc_security_group_ids = [aws_security_group.ec2.id]
          tags= {
              Name = "jumbox"
          }
          user_data = <<-EOF
                      #!/bin/bash
                      sudo yum update
                      sudo yum -y install telnet git
                      # Install Kubectl
                      curl -O https://s3.us-west-2.amazonaws.com/amazon-eks/1.28.8/2024-04-19/bin/linux/amd64/kubectl
                      curl -O https://s3.us-west-2.amazonaws.com/amazon-eks/1.28.8/2024-04-19/bin/linux/amd64/kubectl.sha256
                      openssl sha1 -sha256 kubectl
                      chmod +x ./kubectl
                      mkdir -p $HOME/bin && cp ./kubectl $HOME/bin/kubectl && export PATH=$HOME/bin:$PATH
                      echo 'export PATH=$HOME/bin:$PATH' >> ~/.bashrc
                      kubectl version --client
                      # Install Helm
                      curl https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 > get_helm.sh
                      chmod 700 get_helm.sh
                      ./get_helm.sh
                      helm version
                      # Install Terraform
                      sudo yum install -y yum-utils
                      sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo
                      sudo yum -y install terraform
                      EOF
      }
      data "aws_ami" "amazon_linux" {
        most_recent = true
        filter {
          name   = "name"
          values = ["al202*-ami-202*"]
        }
        filter {
          name   = "architecture"
          values = ["x86_64"]
        }
        owners = ["amazon"]
  • 5-security.tf, configuraremos los grupos de seguridad necesarios para el correcto funcionamiento del EFS y del servidor JumpBox. Estos grupos de seguridad permitirán gestionar de manera segura el acceso dentro de nuestra infraestructura.
    • resource "aws_security_group" "efs" {
         name = "efs-sg"
         description= "Allow inbound efs traffic from ec2"
         vpc_id = "vpc-id" # To change
         ingress {
           cidr_blocks = ["10.0.0.0/16"] # VPC CIDR
           from_port = 2049
           to_port=2049
           protocol = "tcp"
         }
         ingress {
           security_groups = [aws_security_group.ec2.id]
           from_port = 2049
           to_port=2049
           protocol = "tcp"
         }
         egress {
           cidr_blocks = ["10.0.0.0/16"] # VPC CIDR
           from_port = 0
           to_port = 0
           protocol = "-1"
         }      
             
         egress {
           security_groups = [aws_security_group.ec2.id]
           from_port = 0
           to_port = 0
           protocol = "-1"
         }
         tags = {
          Name = "efs-sg"
        }
       }
       
      resource "aws_security_group" "ec2" {
        name        = "ec2-sg"
        description = "Allow efs outbound traffic"
        vpc_id = "vpc-id"  # To change
        ingress {
           cidr_blocks = ["0.0.0.0/0"]
           from_port = 22
           to_port = 22
           protocol = "tcp"
         }
        egress {
          from_port       = 0
          to_port         = 0
          protocol        = "-1"
          cidr_blocks     = ["0.0.0.0/0"]
        }
        tags = {
          Name = "ec2-sg"
        }
      }
  • 6-iam.tf, en este fichero definiremos el rol, las políticas y el instance_profile que se asociará al servidor Jumbox. Esto nos permitirá gestionar permisos y conectarnos a la instancia de manera segura desde la consola de AWS, asegurando así un acceso adecuado y controlado al servidor.
    • resource "aws_iam_role" "demo_role" {
        name = "demo_role"
        assume_role_policy = <<EOF
      {
        "Version": "2012-10-17",
        "Statement": [
          {
            "Action": "sts:AssumeRole",
            "Principal": {
              "Service": "ec2.amazonaws.com"
            },
            "Effect": "Allow",
            "Sid": ""
          }
        ]
      }
      EOF
        tags = {
            tag-key = "demo"
        }
      }
      resource "aws_iam_policy_attachment" "attach_amazon_ssm_full_access-demo_role" {
        name       = "attach-amazon-ssm-full-access-demo_role"
        roles      = [aws_iam_role.demo_role.name]
        policy_arn = "arn:aws:iam::aws:policy/AmazonSSMFullAccess"
      }
      resource "aws_iam_policy_attachment" "attach_amazon_ec2_role_for_ssm-demo_role" {
        name       = "attach-amazon-ec2-role-for-ssm-demo_role"
        roles      = [aws_iam_role.demo_role.name]
        policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM"
      }
      resource "aws_iam_instance_profile" "demo_profile" {
        name = "demo_profile"
        role = "${aws_iam_role.demo_role.name}"
      }
  • 7-variables.tf, definiremos todas las variables que necesitaremos para parametrizar el proyecto. Estas variables permitirán una mayor flexibilidad y reutilización del código, facilitando la personalización de los recursos y haciendo que el despliegue sea más adaptable a diferentes entornos y requisitos específicos.
    • variable "region" {
        description = "Region"
        type        = string
        default     = ""
      }
      variable "environment" {
        description = "Environment Name"
        type        = string
        default     = "Dev"
      }
      variable "terraformrole" {
        description = "Terraform Role"
        type        = string
        default     = ""
      }
      variable "vpc_id" {
        type = string
        description = "The IP of the VPC"
      }
      variable "private_subnets" {
        type        = list(string)
        description = "Private subnet of the AZs"
      }
      variable "private_subnets_local_zone" {
        type        = string
        description = "Private subnet of the Local zone"
      }
      variable "cluster_name" {
        type        = string
        description = "Cluste name"
      }
      variable "cluster_version" {
        description = "EKS cluster version to use"
        type        = string
        default     = ""
      }
      variable "service_cidr_block" {
        type        = string
        description = "EKS Service CIDR Block for EKS"
      }
      variable "cloudwatch_log_group_retention_in_days" {
        type        = number
        description = "Number of days to retain EKS logs for control plane"
      }
      variable "domain_name_in_route53" {
        type = string
      }
      variable "cluster_endpoint_public_access" {
        description = "Default to EKS resource and it is true"
        type        = bool
        default     = true
      }
      variable "cluster_endpoint_private_access" {
        description = "Default to EKS resource and it is false"
        type        = bool
        default     = false
      }
      variable "artifactory" {
        type        = list
        description = "Allow artifactory connection with to connect to eks"
      }
      variable "tags" {
        description = "A map of tags that get added to all resources"
        type        = map(string)
        default     = {}
      }
  • 8-demo-eks.auto.tfvars, estableceremos todos los valores necesarios para generar nuestro entorno. Esto nos permite un enfoque más dinámico y flexible, ya que cualquier ajuste de valores en el futuro solo requerirá modificar este archivo, facilitando la administración y personalización de nuestro entorno según sea necesario.
    • region          = "us-east-1"
      environment     = "poc"
      terraformrole   = "arn:aws:iam::012345678901:role/TerraformRole-Eks" # To change
      domain_name_in_route53 = "demo.com"
      vpc_id = "vpc-id" # To change
      private_subnets = [
        "subnet-id", # To change
        "subnet-id", # To change
        "subnet-id", # To change
      ]
      private_subnets_local_zone = "subnet-id" # To change
      cluster_name = "demo-lz"
      cluster_version = "1.30"
      artifactory        = ["172.16.200.0/24"]
      service_cidr_block = "172.25.200.0/22"
      cloudwatch_log_group_retention_in_days = 180
      tags = {
        App_Name = "Kubernetes"
        Country  = "UY"
        Region   = "AMERICA"
      }
  • 9-outputs.tf, definiremos las salidas que Terraform nos mostrará tras ejecutar terraform apply. Estas salidas incluirán detalles de los recursos creados, que nos ayudarán a verificar rápidamente el estado del despliegue y a acceder fácilmente a los recursos configurados en el proyecto.
    • output "eks_cluster_id" {
        value = module.eks_blueprints.eks_cluster_id
      }
      output "eks_cluster_arn" {
        description = "Amazon EKS Cluster Name"
        value       = module.eks_blueprints.eks_cluster_arn
      }
      output "eks_cluster_name" {
        description = "Amazon EKS Cluster Name"
        value       = module.eks_blueprints.eks_cluster_id
      }
      output "eks_cluster_endpoint" {
        description = "Endpoint for your Kubernetes API server"
        value       = module.eks_blueprints.eks_cluster_endpoint
      }
      output "oidc_provider" {
        description = "The OpenID Connect identity provider)"
        value       = module.eks_blueprints.oidc_provider
      }
      output "eks_cluster_version" {
        description = "The Kubernetes version for the cluster"
        value       = module.eks_blueprints.eks_cluster_version
      }
      output "cluster_primary_security_group_id" {
        description = "Cluster security group..."
        value       = module.eks_blueprints.cluster_primary_security_group_id
      }
      output "cluster_security_group_id" {
        description = "EKS Control Plane Security Group ID"
        value       = module.eks_blueprints.cluster_security_group_id
      }
      output "cluster_security_group_arn" {
        description = "Amazon Resource Name (ARN) of the cluster security group"
        value       = module.eks_blueprints.cluster_security_group_arn
      }
      output "worker_node_security_group_arn" {
        description = "Amazon Resource Name (ARN) of the worker node shared security group"
        value       = module.eks_blueprints.worker_node_security_group_arn
      }
      output "worker_node_security_group_id" {
        description = "ID of the worker node shared security group"
        value       = module.eks_blueprints.worker_node_security_group_id
      }
      output "self_managed_node_group_iam_instance_profile_id" {
        description = "IAM instance profile id of managed node groups"
        value       = module.eks_blueprints.self_managed_node_group_iam_instance_profile_id
      }
      output "configure_kubectl" {
        description = "Configure kubectl using AWS cli"
        value       = "aws eks --region ${local.region} update-kubeconfig --name ${local.cluster_name} --alias ${local.cluster_name}"
      }
      output "efs_id" {
        description = "EFS ID"
        value = aws_efs_file_system.demo_efs.id
      }
      output "ec2_id" {
        description = "EC2 ID"
        value = aws_instance.jumbox.id
      }

6- Crear la carpeta 4-aws-auth y dentro de ella los siguientes ficheros;

  • Este módulo nos va a permitir editar el configmap aws-auth, que es donde están todos los usuarios y roles autorizados a interactuar con el cluster.
    • 1-main.tf
      • resource "kubernetes_config_map_v1_data" "aws_auth" {
          force = true
          metadata {
            name      = "aws-auth"
            namespace = "kube-system"
          }
          data = {
            mapRoles    = yamlencode(local.merged_map_roles)
            mapUsers    = yamlencode(var.map_users)
            mapAccounts = yamlencode(var.map_accounts)
          }
        }
    • 2-locals.tf
      • locals {
          merged_map_roles = distinct(concat(
            try(yamldecode(yamldecode(var.eks.aws_auth_configmap_yaml).data.mapRoles), []),
            var.map_roles,
          ))
        }
    • 3-outputs.tf
      • output "map_accounts" {
          description = "The aws-auth map accounts."
          value       = var.map_accounts
        }
        output "map_roles" {
          description = "The aws-auth map roles ..."
          value       = local.merged_map_roles
        }
        output "map_users" {
          description = "The aws-auth map users."
          value       = var.map_users
        }
    • 4-variables.tf
      • variable "eks" {
          description = "The outputs from the `terraform-aws-eks` module."
          type        = any
        }
        variable "map_accounts" {
          description = "Additional AWS account numbers to add to the aws-auth configmap."
          type        = list(string)
          default     = []
        }
        variable "map_roles" {
          description = "Additional IAM roles to add to the aws-auth configmap."
          type = list(object({
            rolearn  = string
            username = string
            groups   = list(string)
          }))
          default = []
        }
        variable "map_users" {
          description = "Additional IAM users to add to the aws-auth configmap."
          type = list(object({
            userarn  = string
            username = string
            groups   = list(string)
          }))
          default = []
        }
  • Antes de ejecutar terraform debemos exportar el rol creado anteriormente de la siguiente forma;
    • credentials=$(aws sts assume-role –role-arn arn:aws:iam::012345678901:role/TerraformRole-Eks –role-session-name «demo» | jq «.Credentials»)
      export AWS_ACCESS_KEY_ID=$(echo $credentials | jq -r «.AccessKeyId»)
      export AWS_SECRET_ACCESS_KEY=$(echo $credentials | jq -r «.SecretAccessKey»)
      export AWS_SESSION_TOKEN=$(echo $credentials | jq -r «.SessionToken»)
  • Después de crear esos ficheros ya estamos en condiciones de ejecutar Terraform para crear nuestro clúster y demás recursos,
    • terraform init
    • terraform plan
    • terraform apply
  • Actualizar el kubeconfig
    • aws eks --region us-west-1 update-kubeconfig --name demo-eks --alias demo-eks
    • Aquí podemos ver el jonbox y los nodos trabajadores creados de forma correcta y en sus respectivas subredes.
    • Asi podemos ver los nodos trabajadores en el clúster.
  • Una ves validado que todo esta bien, vamos ejecutar un deployment en el clúster y validar que nuesta app esta funcionando sin problemas en los nodos trabajadores.
  • Si queremos deployar una app de prueba que use el EFS podemos ir al articulo anterior sobre EKS y copiar el codigo relacionado con este apartado en la parte final del articulo.
  • Terraform destroy
    • Como siempre, al finalizar tus pruebas, recuerda eliminar todos los recursos creados ejecutando terraform destroy --auto-approve. Este paso es crucial para evitar sorpresas desagradables en tu factura a final del mes. No subestimes la importancia de esta tarea, ya que la acumulación de recursos no utilizados puede generar costos innecesarios. Tómate el tiempo para limpiar tu entorno, eso te ahorrará dolores de cabeza en el futuro 🙂

Conclusiones

Las Zonas Locales se presentan como una solución clave para muchos de nuestros clientes, especialmente en el contexto de instituciones financieras y entidades gubernamentales, donde la latencia y el cumplimiento normativo son esenciales. Mantener los datos dentro del país del cliente no solo mejora la experiencia del usuario, sino que también ayuda a cumplir en determinadas ocasiones con las regulaciones locales. Sin embargo, es fundamental estar al tanto de las particularidades de su implementación, como la necesidad de habilitar estas zonas y las limitaciones de ciertos servicios. Comprender estos aspectos nos permitirá ofrecer soluciones efectivas y adaptadas a las necesidades específicas de cada cliente.

Personalmente, cuando tuve la oportunidad de trabajar con las Zonas Locales, fue todo un desafío, ya que nunca había tenido experiencia previa con ellas. Me encontré con varios detalles pequeños pero significativos. Esta experiencia me enseñó que, antes de implementar cualquier funcionalidad en las Zonas Locales, es crucial consultar la documentación y verificar si el servicio que deseamos utilizar está habilitado, estar bien informado es clave para aprovechar al máximo estas configuraciones.

¡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.