Blog de Amazon Web Services (AWS)

Picturesocial – ¿Cómo desplegar un clúster de Kubernetes en AWS usando Terraform?

José Yapur, Senior Developer Advocate en AWS

En nuestro último post descubrimos sobre Kubernetes y por qué lo estamos usando en Picturesocial. En este post vamos a aprender sobre infraestructura cómo código, y específicamente sobre cómo desplegar un Amazon Elastic Kubernetes Service usando Terraform.

He trabajado en proyectos de TI por varios años y algo recurrente, en mi experiencia, ha sido cómo los desarrolladores trabajan junto con los Sysadmins, especialmente cuando la aplicación fuerza cambios en la infraestructura y en la forma en la que las cosas son hechas tradicionalmente, ¿La aplicación tiene que adaptarse a la infraestructura o la infraestructura debe adaptarse a la aplicación?, ¿Qué pasa si los cambios en infraestructura representan problemas para hacer rollback a versiones anteriores de la aplicación de forma sencilla?

En el pasado, diseñamos aplicaciones sabiendo que en la mayoría de los casos la infraestructura era estática, que teníamos que lidiar con las limitaciones como algo axiomático e inamovible. A medida que avanzamos en nuestro camino a la nube, ese paradigma comenzó a romperse con la posibilidad de tener teóricamente todo el poder de cómputo que necesitábamos al alcance de nuestras manos y casi de forma inmediata. Ese cambio ayudó a crear un nuevo set de soluciones diseñadas para esas nuevas capacidades, una de ellas fue Infraestructura como Código (IaC)

Pero, ¿Qué es infraestructura como código?

Cuando estaba en el colegio me gustaba escribir historias de Calabozos y Dragones, donde el personaje principal tenía que tomar decisiones irreversibles que eran seleccionadas por el lector, y dependiendo de la elección podrías ir a través una serie de aventuras que podían ser potencialmente largas y complejas o simplemente hacían que la historia termine en ese momento. Así es como suceden las cosas con la infraestructura, comienzas con algunos supuestos que te obligan a elegir el tamaño de tu infra, dimensionamiento de redes, uso de balanceadores, etc. La mayoría de cambios que hagas sobre la infraestructura base son potencialmente irreversibles a menos que conozcas exactamente lo que pasó desde el inicio y cada acción que se realizó. Esto significa que mantener versionamiento de la infraestructura y su configuración en conjunto con la aplicación puede ser posible, pero se vuelve bastante difícil, y es ahí donde la Infraestructura como Código entra a la acción.

La Infraestructura como Código te permite definir tu infraestructura y configuración de manera similar a como los desarrolladores de software definen sus aplicaciones y está compuesto por archivos de configuración. Estos archivos de configuración son interpretados y transformados en infraestructura en tu ambiente de nube pública o híbrida. IaC te permite mantener versionamiento de cada cambio, así como hacer rollback hacia versiones anteriores, incluso puedes crear pruebas para entender qué representan esos cambios antes de aplicarlos.

Para Picturesocial decidí usar Hashicorp Terraform, para nuestras definiciones de IaC, debido a que es una herramienta que vengo usando por años y me siento seguro de poder escalar mi arquitectura e infraestructura sin gastar mucho tiempo aprendiendo una nueva herramienta. Sin embargo, hay otras excelentes herramientas en el mercado como AWS Cloud Development Kit, AWS CloudFormation, Pulumi, Ansible, Chef, Puppet, entre otras. Estas herramientas también pueden ayudarte, la mejor opción es la que te haga sentir más cómodo y productivo.

¿Qué es Hashicorp Terraform?

Terraform es una herramienta de IaC creada por Hashicorp que te ayuda a definir una infraestructura completa de una forma en la que puedes versionarla y reutilizarla. Usa Hashicorp Configuration Language (HCL) para su estructura. Todos los archivos de configuración de Terraform deben ser guardados con la extensión .tf

Algunas definiciones básicas a tener en cuenta son:

  • Providers: Aquí es donde le dices a Terraform que estás usando un proveedor de nube específico [providers], por ejemplo, si quieres desplegar tu infraestructura a AWS, necesitas definir un proveedor como en el ejemplo a continuación. Tener en cuenta que especificar la versión del proveedor es opcional, si no la especificas usará la última disponible como versión por defecto.

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 3.20.0"
    }
  }
}

  • Resources: Acá es donde defines la infraestructura. Puedes definir los recursos como piezas de infraestructura tipo: instancias, redes, storage, etc. Un recurso necesita dos parámetros declarativos: 1/ el tipo de recurso y 2/ el id del recurso. Por ejemplo, abajo estamos definiendo una instancia de AWS con un AMI específico y tipo de instancia t2.micro.

resource "aws_instance" "web" {
  ami           = "ami-a1b2c3d4"
  instance_type = "t2.micro"
}

  • Variables:
    • Input: Estas son las variables que usas para solicitar información de los usuarios o runtime antes de aplicar algún cambio. Este tipo de variables son parámetros que debes ingresar en el Terraform CLI [CLI]

variable "project_code" {
  type = string
}

variable "availability_zone" {
  type    = list(string)
  default = ["us-east-1a"]
}

  • Output: Estas son variables que retornarán información de la ejecución. Por ejemplo, nombre del repositorio, id del clúster, etc.

output "ec2_ip" {  value = aws_instance.server.private_ip}

  • Locals: Estas son variables que estableces en tu código y pueden ser referenciadas en cualquier parte del proyecto. El ejemplo a continuación creará una etiqueta común para todos los recursos que creemos y concatenará los valores de las variables project_code y environment

locals {
  project_code = "pso"
  environment        = "dev"
}
common_tags = {
  project_name = "pe-${local.project_code}-${local.environment}01"
}

  • Modules: Usamos los módulos para agrupar diferentes recursos que son utilizados de forma conjunta en una arquitectura, de esa forma en vez de tener una plantilla de Terraform de 30 páginas y 2 volúmenes, podemos estandarizar escenarios como un solo objeto. Por ejemplo: Cada Amazon EKS Clúster necesita una VPC con 6 subnets, 2 Elastic Load Balancers, 2 Worker Groups con al menos 3 instancias de EC2 cada grupo, etc. En vez de crear todos los recursos por clúster, podemos crear un módulo y reutilizarlo para simplificar las creaciones futuras.

module "aws-vpc" {
  source = "./mods/aws-vpc"
  base_cidr_block = "11.0.0.0/16"
}

Me gusta pensar en Terraform como una herramienta de IaC de 4 pasos (inclusive si tenemos más opciones), vamos a usar 4 comandos básicos que aplicarán a todos nuestros proyectos, esos pasos necesitan ser ejecutados en el siguiente orden:

  1. terraform init
    Usa este comando para inicializar tu proyecto de Terraform, solo debes ejecutar este comando una vez por proyecto.
  2. terraform plan
    Este comando se usa para probar qué es lo que se creará, actualizará o eliminará en tu ambiente de nube y antes de ejecutar cualquier cambio.
  3. terraform apply
    Este comando ejecuta terraform plan como paso 1 y luego en base a tu confirmación, ejecuta los cambios finales en tu ambiente de nube.
  4. terraform destroy
    Cuando ya no necesitas tu ambiente de nube, puedes destruirlo por completo. Esto es súper útil para ambientes de certificación que solo son usados cuando sale un nuevo release, por ejemplo.

Ahora que entendemos algunos conceptos básicos de IaC y Terraform ¡Vamos a desplegar un Clúster de Amazon EKS desde el inicio!

Pre-requisitos

Paso a Paso

En este paso a paso vamos a crear un Clúster de Amazon EKS en la región us-east-1 usando 3 zonas de disponibilidad, nuestra propia VPC, un worker group con 3 instancias t2.small y security rules para prevenir acceso no restringido a nuestro worker group. He creado este repositorio en Github https://github.com/aws-samples/picture-social-sample con todo el código necesario para seguir este paso a paso. Asegúrate de seleccionar la branch “ep3”.

  • Primero, vamos a clonar nuestro repositorio base para tener todos los archivos de terraform que necesitaremos para crear nuestro clúster.
git clone https://github.com/aws-samples/picture-social-sample --branch ep3
  • Una vez clonado, vayamos al directorio creado. Aseguremonos de siempre estar dentro de este directorio por el resto de esta guía, de esa forma todo se ejecutará sin problemas.
cd picture-social-sample
  • Ahora que estamos en el directorio correcto y hemos clonado la branch, vamos a iniciar con el archivo tf. Este archivo contiene toda la configuración básica de nuestro proyecto de Terraform, como: región por defecto de AWS, nombre del clúster, variables para generar valores aleatorios, etc. Podemos referenciar cualquiera de estos valores en el proyecto.
variable "region" {
  default     = "us-east-1"
  description = "Region of AWS"
}

provider "aws" {
  region = var.region
}

data "aws_availability_zones" "available" {}

locals {
  clúster_name = "picturesocial-${random_integer.suffix.result}"
}

resource "random_integer" "suffix" {
  min = 100
  max = 999
}
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "3.2.0"

  name                 = "picturesocial-vpc"
  cidr                 = "10.0.0.0/16"
  azs                  = data.aws_availability_zones.available.names
  private_subnets      = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  public_subnets       = ["10.0.4.0/24", "10.0.5.0/24", "10.0.6.0/24"]
  enable_nat_gateway   = true
  single_nat_gateway   = true
  enable_dns_hostnames = true

  tags = {
    "kubernetes.io/clúster/${local.cluster_name}" = "shared"
  }

  public_subnet_tags = {
    "kubernetes.io/clúster/${local.cluster_name}" = "shared"
    "kubernetes.io/role/elb"                      = "1"
  }

  private_subnet_tags = {
    "kubernetes.io/clúster/${local.cluster_name}" = "shared"
    "kubernetes.io/role/internal-elb"             = "1"
  }
}
  • Ahora que hemos configurado nuestra VPC, vamos a crear los Security Groups. Estos serán los que tienen a cargo la autorización o denegación del tráfico a las instancias de EC2 de nuestro Amazon EKS. Para este caso habilitaremos el tráfico al puerto 22 de entrada desde algunos segmentos de red llamados cidr_blocks
resource "aws_security_group" "worker_group_mgmt_one" {
  name_prefix = "worker_group_mgmt_one"
  vpc_id      = module.vpc.vpc_id

  ingress {
    from_port = 22
    to_port   = 22
    protocol  = "tcp"

    cidr_blocks = [
      "10.0.0.0/8",
    ]
  }
}

resource "aws_security_group" "worker_group_mgmt_two" {
  name_prefix = "worker_group_mgmt_two"
  vpc_id      = module.vpc.vpc_id

  ingress {
    from_port = 22
    to_port   = 22
    protocol  = "tcp"

    cidr_blocks = [
      "192.168.0.0/16",
    ]
  }
}

resource "aws_security_group" "all_worker_mgmt" {
  name_prefix = "all_worker_management"
  vpc_id      = module.vpc.vpc_id

  ingress {
    from_port = 22
    to_port   = 22
    protocol  = "tcp"

    cidr_blocks = [
      "10.0.0.0/8",
      "172.16.0.0/12",
      "192.168.0.0/16",
    ]
  }
}
  • El archivo eks-cluster.tfes donde se une todo lo que creamos en los pasos anteriores, usa un módulo público de AWS que podemos reutilizar para simplificar la creación de nuestro clúster. Vamos a establecer el nombre del clúster referenciando las variables que creamos en el tf, seleccionamos la versión de Kubernetes y referenciamos la VPC y subnets definidas en el archivo vpc.tf

module "eks" {
  source          = "terraform-aws-modules/eks/aws"
  version         = "17.24.0"
  clúster_name    = local.cluster_name
  cluster_version = "1.20"
  subnets         = module.vpc.private_subnets

  vpc_id = module.vpc.vpc_id

  workers_group_defaults = {
    root_volume_type = "gp2"
  }

  worker_groups = [
    {
      name                          = "group-1"
      instance_type                 = "t2.small"
      additional_security_group_ids = [aws_security_group.worker_group_mgmt_one.id]
      asg_desired_capacity          = 3
    },
  ]
}

data "aws_eks_cluster" "cluster" {
  name = module.eks.cluster_id
}

data "aws_eks_cluster_auth" "cluster" {
  name = module.eks.cluster_id
}

  • Y finalmente vamos a revisar los outputs luego que se entregarán al finalizar la ejecución del proyecto de Terraform en el archivo tfEste archivo producirá el archivo Config de Kubernetes o kubeconfig, esto es necesario para que Kubectl o el Kubernetes REST API sepa quién eres, si tienes acceso y si puede confiar en ti. Y además es una de las piezas más importantes para obtener acceso al clúster de Amazon EKS.
output "cluster_id" {
  value       = module.eks.cluster_id
}

output "cluster_endpoint" {
  value       = module.eks.cluster_endpoint
}

output "cluster_security_group_id" {
  value       = module.eks.cluster_security_group_id
}

output "kubectl_config" {
  value       = module.eks.kubeconfig
}

output "config_map_aws_auth" {
  value       = module.eks.config_map_aws_auth
}

output "region" {
  value       = var.region
}

output "cluster_name" {
  value       = local.cluster_name
}
  • Ahora asegurémonos que tenemos terraform correctamente instalado, vamos a ejecutar el comando a continuación en el terminal que prefieras. Si todo está bien deberías recibir la versión de Terraform instalada, necesitas al menos la versión 1.1.7.

terraform —version

  • Ahora vamos a inicializar nuestro proyecto al ejecutar el siguiente comando:

terraform init

  • El comando anterior descargará todos los módulos públicos y archivos que necesita el proveedor de Terraform para AWS. Si recibes el siguiente mensaje en la línea de comandos “Terraform has been successfully initialized!” entonces estamos listos para continuar. Si recibes algún error, revisa el mensaje de error donde te dirán la línea y archivo que tienen problemas, no te preocupes el error suele ser un error en nombre de variable o que te falta una comilla :)
  • Ahora vamos a probar nuestra configuración:

terraform plan

  • El comando plan retornará un resumen de todas las cosas que se agregarán, cambiarán o destruirán. Esto nos dará una buena idea de cómo funcionará todo antes de ejecutar cambios.
Terraform used the selected providers to generate the following execution plan. Resource
actions are indicated with the following symbols:
  + create
 <= read (data resources)

Terraform will perform the following actions:

  # data.aws_eks_cluster.cluster will be read during apply
  # (config refers to values not yet known)
 <= data "aws_eks_cluster" "cluster"  {
      + arn                       = (known after apply)
      + certificate_authority     = (known after apply)
      + created_at                = (known after apply)
      + enabled_cluster_log_types = (known after apply)
      + endpoint                  = (known after apply)
      + id                        = (known after apply)
      + identity                  = (known after apply)
      + kubernetes_network_config = (known after apply)
      + name                      = (known after apply)
      + platform_version          = (known after apply)
      + role_arn                  = (known after apply)
      + status                    = (known after apply)
      + tags                      = (known after apply)
      + version                   = (known after apply)
      + vpc_config                = (known after apply)
    }

  #,
 .
 .
 .
Plan: 50 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + cluster_endpoint          = (known after apply)
  + cluster_id                = (known after apply)
  + cluster_name              = (known after apply)
  + cluster_security_group_id = (known after apply)
  + config_map_aws_auth       = [
      + {
          + binary_data = null
          + data        = (known after apply)
          + id          = (known after apply)
          + metadata    = [
              + {
                  + annotations      = null
                  + generate_name    = null
                  + generation       = (known after apply)
                  + labels           = {
                      + "app.kubernetes.io/managed-by" = "Terraform"
                      + "terraform.io/module"          = "terraform-aws-modules.eks.aws"
                    }
                  + name             = "aws-auth"
                  + namespace        = "kube-system"
                  + resource_version = (known after apply)
                  + uid              = (known after apply)
                },
            ]
        },
    ]
  + kubectl_config            = (known after apply)
  + region                    = "us-east-1"

───────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take
exactly these actions if you run "terraform apply"
  • Ahora que estamos seguros, vamos a ejecutar los cambios en AWS. Para ello ejecutaremos el comando a continuación. Deberás escribir explícitamente “yes” para confirmar que estás de acuerdo con los cambios antes de ejecutarlos.

terraform apply

  • Si recibes algún timeout cuando intentas ejecutar los cambios, intenta ejecutar el comando nuevamente. Podría ser que el tipo de instancia seleccionada tiene un tiempo de aprovisionamiento mayor o que tienes un problema de conexión a internet desde tu terminal.
  • Este proceso tardará alrededor de 15 a 20 minutos, pero dependiendo de tu propia configuración puede ser significativamente más o menos tiempo. Ten en cuenta que tu terminal necesita ser accesible y estar conectada a internet mientras el comando se ejecuta y finaliza la ejecución. Es un bueno momento para hacerte un buen café y ver videos de gatos cantando.
  • Una vez que el comando termine de ejecutarse, deberás recibir el siguiente mensaje:
    Apply complete! Resources: 50 added, 0 changed, 0 destroyed.
  • Vamos a extraer todos los outputs que configuramos en el archivo tf, esos outputs además, son parte del mensaje que sale abajo de Apply complete! Todo esto será usado para construir el kubeconfig. Para extraer esos valores usaremos el comando a continuación:

aws eks --region $(terraform output -raw region) update-kubeconfig --name $(terraform output -raw cluster_name)

  • ¡Estamos Listos! Ahora confirmemos que el clúster ha sido creado correctamente ejecutando el siguiente comando:

aws eks list-clusters

  • Si todo funcionó como lo esperábamos entonces deberías tener un output similar a este:

{"clusters": ["picturesocial-129"]}

  • Si tienes experiencia con Kubernetes y tienes una carga de trabajo que quieras probar, siéntete libre de comenzar a jugar. Pero, si no tienes experiencia en cómo desplegar aplicaciones en Kubernetes te sugiero que elimines toda la infraestructura y la re-despliegues en el siguiente episodio, de esa forma evitas cargos innecesarios en tu cuenta por recursos que no usarás. Puedes borrar todo ejecutando el siguiente comando:

terraform destroy

¡Wow! Realmente, este fue un artículo largo, ¡Pero lo hicimos! Puedes reutilizar esta plantilla para crear tus propios clústeres o incluso para crear tus propios módulos en el futuro.

En el siguiente post aprenderemos a desplegar una aplicación a Kubernetes y creo que es una de las partes más satisfactorias de la serie porque es cuando vemos la aplicación finalmente ejecutándose y aprovechando las ventajas de Kubernetes como self-healing, auto escalamiento, balanceo de carga, etc. Además, si quieres aprender más sobre los Blueprints de Amazon EKS para Terraform te recomiendo revisar este enlace https://aws-ia.github.io/terraform-aws-eks-blueprints/ con módulos listos para facilitar tus despliegues de Kubernetes en tu entorno de producción.

¡Espero que aprendieras leyendo este post, nos vemos en el siguiente! Además, no olvides que puedes ver el video de este episodio aquí:


Sobre el autor

José Yapur es Senior Developer Advocate en AWS con experiencia en Arquitectura de Software y pasión por el desarrollo especialmente en .NET y PHP. Trabajó como Arquitecto de Soluciones por varios años, ayudando a empresas y personas en LATAM.