Creando una arquitectura robusta en AWS con Terraform

Hello World 🙂
Continuando con nuestra serie de ejercicios sobre Terraform y la creación de recursos en AWS, les presento otro ejercicio completamente práctico. En esta ocasión, nos adentraremos en la creación de una VPC, que como siempre, servirá como base para el despliegue de nuestros servicios. Sin embargo, en este caso, daremos un paso más al desplegar un clúster de EKS (Amazon Elastic Kubernetes Service), la distribución de Kubernetes soportada por AWS.
Una de las ventajas clave de desplegar Kubernetes en AWS es su integración con una amplia gama de servicios de la plataforma. En este ejercicio, mostraremos cómo integrar EKS con EFS (Amazon Elastic File System), una opción de almacenamiento altamente distribuido, escalable y de alto rendimiento.
Además, crearemos una instancia EC2 que fungirá como Jumpbox, desde la cual nos conectaremos a nuestro clúster de EKS, para finalizar desplegaremos un breve ejemplo para verificar la integración y el correcto funcionamiento de estos servicios.
Para este ejemplo, seguiremos las mejores prácticas. Crearemos nuestro clúster EKS y nuestro EFS en subredes privadas, y configuraremos los Security Groups con los accesos mínimos necesarios. Nuestra instancia EC2 será creada en subredes públicas. Asimismo, configuraremos nuestro clúster de EKS con su API privada, un requisito crucial si nuestro entorno será auditado por PCI(Payment Card Industry).
Esta arquitectura desplegada hoy representa un escenario común que se puede implementar en entornos reales, ofreciendo una sólida base para infraestructuras seguras y eficientes.
¡Prepárate para adentrarte en el mundo de la automatización de infraestructura con Terraform!
Requisitos:
En esta ocasión el proyecto va a estar formado por 3 carpetas;
- La primera carpeta es 1-iam-role-eks, donde definiremos los archivos necesarios para crear el Rol IAM que Terraform utilizará posteriormente para aprovisionar el clúster EKS. Este rol proporcionará los permisos necesarios para la creación y gestión de los recursos del clúster de manera segura y controlada.
- La segunda carpeta es 2-eks-cluster, en esta carpeta vamos a definir todos los archivos necesarios para crear el clúster de EKS, el EFS y la instancia EC2.
- La tercera carpeta es 3-k8s, aquí vamos a tener los ficheros necesarios para validar el funcionamiento y la integración de nuestro clúster EKS con el EFS.
1- Primero necesitamos exporta nuestras credenciales de AWS en nuestra consola;
export AWS_ACCESS_KEY_ID="XXXXXXXXXX"
export AWS_SECRET_ACCESS_KEY="XXXXXXXXXX"- Comprobamos que estemos conectados de forma correcta
aws sts get-caller-identity
2- 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.
- Nota: Si tienes algún Rol creado anteriormente que deseas autorizar para que asuma este Rol en otro momento lo debes definir aquí.
-
terraform {required_version = ">= 1.0"required_providers {aws = {source = "hashicorp/aws"version = ">= 4.47"}}}
-
- Nota: Si tienes algún Rol creado anteriormente que deseas autorizar para que asuma este Rol en otro momento lo debes definir aquí.
-
-
-
resource "aws_iam_role" "terraform_role" {name = local.role_nameassume_role_policy = jsonencode({"Version" : "2012-10-17","Statement" : [{"Effect" : "Allow","Principal" : {"AWS" : ["arn:aws:iam::012345678901:role/#########","arn:aws:iam::012345678901:root"]},"Action" : "sts:AssumeRole"}]})}
-
-
resource "aws_iam_role_policy_attachment" "attachment_policy" {role = aws_iam_role.terraform_role.namepolicy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"}- En este caso adjuntamos la política
AdministratorAccess, pero si queremos podemos ser un poco más específico y adjuntar solo las políticas que necesitemos.
- En este caso adjuntamos la política
-
- 3-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 initterraform planterraform apply- 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.
3- Crear la carpeta 2-eks-cluster y dentro de ella los siguientes ficheros;
- En el archivo 0-provider.tf, definiremos los proveedores y la región que utilizaremos en este proyecto. 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.
- En el caso del provider AWS vamos a definirle el rol queremos que utilice. Esto es particularmente útil cuando necesitas ejecutar Terraform con permisos específicos que están otorgados a un rol IAM.
-
provider "aws" {region = local.regiondefault_tags {tags = local.tags}assume_role {role_arn = var.terraformrole}}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 "kubernetes" {host = module.eks.cluster_endpointcluster_ca_certificate = base64decode(module.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 executedargs = ["eks", "get-token", "--cluster-name", module.eks.cluster_name]}}
- 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-eks"region = var.regiontags = var.tagscluster_name = var.cluster_namecommon_tags = {Environment = var.environmentproject_name = var.project_nameTerraform = "true"}}
-
- 2-vpc.tf; en este fichero definiremos todo lo necesario para crear nuestra VPC, que servirá como la base de la infraestructura del proyecto. Aquí, definiremos subredes y demás componentes claves que permitirán una red segura y eficiente para los recursos que desplegaremos más adelante.
-
module "vpc" {source = "terraform-aws-modules/vpc/aws"version = "5.2.0"name = var.vpc_namecidr = var.vpc_base_cidrazs = var.availability_zones == [] ? var.availability_zones : data.aws_availability_zones.available.namesprivate_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_gatewaysingle_nat_gateway = var.single_nat_gatewayenable_vpn_gateway = var.enable_vpn_gatewayone_nat_gateway_per_az = var.one_nat_gateway_per_azenable_dns_hostnames = var.enable_dns_hostnamesenable_dns_support = var.enable_dns_supporttags = merge(var.vpc_tags, local.common_tags)}data "aws_availability_zones" "available" {}
-
- 3-eks.tf; definiremos todo lo necesario para la creación de nuestro clúster EKS, incluyendo las configuraciones base y los ajustes requeridos para desplegarlo correctamente en AWS.
-
module "eks" {source = "terraform-aws-modules/eks/aws"version = "19.12.0"cluster_name = var.cluster_namecluster_version = var.cluster_versioncluster_endpoint_private_access = var.cluster_endpoint_private_accesscluster_endpoint_public_access = var.cluster_endpoint_public_accessvpc_id = module.vpc.vpc_idsubnet_ids = module.vpc.private_subnetsenable_irsa = var.enable_irsa# Aquí defino los valores predeterminados que van a tener todos los "eks_managed_node_groups"eks_managed_node_group_defaults = {name = local.ng_nameinstance_types = var.instance_typesami_id = data.aws_ami.eks_default.image_idmin_size = var.min_sizemax_size = var.max_sizedesired_size = var.desired_sizeenable_bootstrap_user_data = trueenable_monitoring = trueblock_device_mappings = {xvda = {device_name = "/dev/xvda"ebs = {volume_size = 25volume_type = "gp3"iops = 3000throughput = 150delete_on_termination = true}}}}eks_managed_node_groups = var.eks_managed_node_groupsmanage_aws_auth_configmap = var.manage_aws_auth_configmapcreate_aws_auth_configmap = var.create_aws_auth_configmapcluster_enabled_log_types = var.cluster_enabled_log_typestags = merge(var.tags, local.common_tags)} -
Aquí vamos a permitir el tráfico proveniente de los puertos 2049 para poder usar el servicio de EFS y 443(HTTS) para poder administrar el clúster desde nuestro Jumbox.
-
resource "aws_security_group_rule" "eks_sg_efs" {depends_on = [module.eks]description = "Allow EFS traffic"type = "ingress"from_port = 2049to_port = 2049protocol = "tcp"cidr_blocks = [var.vpc_base_cidr]security_group_id = module.eks.cluster_primary_security_group_id}resource "aws_security_group_rule" "eks_sg_https" {depends_on = [module.eks]description = "Allow https traffic to manage the cluster fromthe Jumbox"type = "ingress"from_port = 443to_port = 443protocol = "tcp"cidr_blocks = [var.vpc_base_cidr]security_group_id = module.eks.cluster_primary_security_group_id}
-
-
Aquí vamos a permitir que el tráfico proveniente del EFS llegue a los nodos del clúster.
-
resource "aws_security_group_rule" "node_sg_efs" {depends_on = [module.eks]description = "Allow EFS traffic"type = "ingress"from_port = 2049to_port = 2049protocol = "tcp"cidr_blocks = [var.vpc_base_cidr]security_group_id=module.eks.node_security_group_id}
-
- Aquí vamos a buscar el AMI que va a hacer usada por los nodos del clúster, si tuviésemos algún Golden AMI también la pudiéramos filtrar.
-
data "aws_ami" "eks_default" {most_recent = trueowners = ["amazon"]filter {name = "name"values = ["amazon-eks-node-${var.cluster_version}-v*"]}}
-
- 4-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 = truetags = {Name = "demo"}} -
resource "aws_efs_mount_target" "zone" {for_each = { for idx, subnet in module.vpc.private_subnets : idx => subnet }file_system_id = aws_efs_file_system.demo_efs.idsubnet_id = each.valuesecurity_groups = [aws_security_group.efs.id]}
-
- 5-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.idinstance_type = "t2.micro"iam_instance_profile = "${aws_iam_instance_profile.demo_profile.name}"subnet_id = module.vpc.public_subnets[0]associate_public_ip_address= truevpc_security_group_ids = [aws_security_group.ec2.id]tags= {Name = "demo"}user_data = <<-EOF#!/bin/bashsudo yum updatesudo yum -y install telnet git# Install Kubectlcurl -O https://s3.us-west-2.amazonaws.com/amazon-eks/1.28.8/2024-04-19/bin/linux/amd64/kubectlcurl -O https://s3.us-west-2.amazonaws.com/amazon-eks/1.28.8/2024-04-19/bin/linux/amd64/kubectl.sha256openssl sha1 -sha256 kubectlchmod +x ./kubectlmkdir -p $HOME/bin && cp ./kubectl $HOME/bin/kubectl && export PATH=$HOME/bin:$PATHecho 'export PATH=$HOME/bin:$PATH' >> ~/.bashrckubectl version --client# Install Helmcurl https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 > get_helm.shchmod 700 get_helm.sh./get_helm.shhelm version# Install Terraformsudo yum install -y yum-utilssudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.reposudo yum -y install terraformEOF} - Filtramos el AMI que va hacer utilizada para la creacion del servidor Jumbox
-
data "aws_ami" "amazon_linux" {most_recent = truefilter {name = "name"values = ["al202*-ami-202*"]}filter {name = "architecture"values = ["x86_64"]}owners = ["amazon"]}
-
-
- 6-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 = module.vpc.vpc_idingress {cidr_blocks = [var.vpc_base_cidr]from_port = 2049to_port=2049protocol = "tcp"}ingress {security_groups = [aws_security_group.ec2.id]from_port = 2049to_port=2049protocol = "tcp"}egress {cidr_blocks = [var.vpc_base_cidr]from_port = 0to_port = 0protocol = "-1"}egress {security_groups = [aws_security_group.ec2.id]from_port = 0to_port = 0protocol = "-1"}tags = {Name = "efs-sg"}} -
resource "aws_security_group" "ec2" {name = "ec2-sg"description = "Allow efs outbound traffic"vpc_id = module.vpc.vpc_idingress {cidr_blocks = ["0.0.0.0/0"]from_port = 22to_port = 22protocol = "tcp"}egress {from_port = 0to_port = 0protocol = "-1"cidr_blocks = ["0.0.0.0/0"]}tags = {Name = "ec2-sg"}}
-
- 7-iam.tf; en este fichero vamos a definir el rol, las políticas y el
instance_profileque 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": ""}]}EOFtags = {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}"}
-
- 8-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.
- Como son muchas variables les dejo el enlace directo a este fichero en el repositorio.
- 9-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.
-
#################### General Inputs###################
region = "us-west-1"environment = "Demo"project_name = "demo-eks"terraformrole = "arn:aws:iam::012345678901:role/TerraformRole-Eks"Nota: Aquí debemos poner el ARN del rol creado anteriormente
#################### VPC Inputs###################vpc_name = "demo-eks"vpc_base_cidr = "10.1.0.0/16"availability_zones = ["us-west-1a", "us-west-1b"]private_subnets = "10.1.11.0/24,10.1.12.0/24"public_subnets = "10.1.21.0/24,10.1.22.0/24"isolated_subnets = "10.1.31.0/24,10.1.32.0/24"public_subnet_tags = {"Name" = "Public-Subnet""kubernetes.io/role/elb" = 1"kubernetes.io/cluster/demo" = "owned"}private_subnet_tags = {"Name" = "Private-Subnet""kubernetes.io/role/internal-elb" = 1"kubernetes.io/cluster/demo" = "owned"}#################### EKS Inputs###################cluster_name = "demo-eks"cluster_version = "1.28"cluster_endpoint_private_access = truecluster_endpoint_public_access = falseinstance_types = ["t3.medium"]desired_size = 1min_size = 1max_size = 3eks_managed_node_groups = {on_demand = {labels = {role = "on_demand"}capacity_type = "ON_DEMAND"}spot = {desired_size = 1min_size = 1max_size = 5labels = {role = "spot"}instance_types = ["t3.micro"]capacity_type = "SPOT"}}tags = {App_Name = "Kubernetes"Country = "UY"Region = "AMERICA"}
-
- 10-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.
- este es un fichero opcional, por lo que les dejo el enlace directo al fichero por si quieren copiar su contenido.
- Después de crear esos ficheros ya estamos en condiciones de ejecutar Terraform;
3- Como la API del clúster EKS es privada, debemos ir a nuestro Jumbox, creado en la misma VPC del clúster y que nos va a permitir administrar el mismo sin inconvenientes.
- Vamos a la consola de AWS y nos conectamos a la instancia
- Primero necesitamos exporta nuestras credenciales de AWS para poder asumir el rol que creamos anteriormente y que nos permitirá administrar el clúster;
export AWS_ACCESS_KEY_ID="XXXXXXXXXX"
export AWS_SECRET_ACCESS_KEY="XXXXXXXXXX"
- Comprobamos que estemos conectados de forma correcta
aws sts get-caller-identity
- Después exportamos el rol de la siguiente manera;
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")
- Actualizar el kubeconfig
aws eks --region us-west-1 update-kubeconfig --name demo-eks --alias demo-eks
4-Para poder utilizar el EFS desde amazon tenemos que instalar el Amazon EFS CSI driver, en nuestro caso lo vamos hacer usando Helm.
helm repo add aws-efs-csi-driver https://kubernetes-sigs.github.io/aws-efs-csi-driver/helm install aws-efs-csi-driver aws-efs-csi-driver/aws-efs-csi-driver -n kube-system- Ejecutar el siguiente comando para verificar que todo está bien.
4- Crear la carpeta 3-k8s y dentro de ella los siguientes ficheros;
- sc.yaml; aquí vamos a definir el
StorageClassque vamos a utilizar.-
kind: StorageClassapiVersion: storage.k8s.io/v1metadata:name: efs-scprovisioner: efs.csi.aws.com
-
- Obtenemos el id del EFS creado y agregarlo en el fichero pv.yaml generado a continuación;
aws efs describe-file-systems --query "FileSystems[*].FileSystemId" --output text --region us-west-1
- pv.yaml, aquí vamos a definir todo lo necesario para generar el
PersistentVolume.-
apiVersion: v1kind: PersistentVolumemetadata:name: efs-pvspec:capacity:storage: 5GivolumeMode: FilesystemaccessModes:- ReadWriteManypersistentVolumeReclaimPolicy: RetainstorageClassName: efs-sccsi:driver: efs.csi.aws.comvolumeHandle: efs-id
-
- pvc.yaml; aquí vamos a definir todo lo necesario para generar el
PersistentVolumeClaim.-
apiVersion: v1kind: PersistentVolumeClaimmetadata:name: efs-claimspec:accessModes:- ReadWriteManystorageClassName: efs-scresources:requests:storage: 5Gi
-
- Vamos a definir un par de
podque van a montar como volumen el PVC creado anteriormente;- pod1.yaml;
-
apiVersion: v1kind: Podmetadata:name: app1spec:containers:- name: app1image: busyboxcommand: ["/bin/sh"]args: ["-c", "while true; do echo Printing the date $(date -u) from the pod $(hostname) >> /data/out1.txt; sleep 5; done"]volumeMounts:- name: persistent-storagemountPath: /datavolumes:- name: persistent-storagepersistentVolumeClaim:claimName: efs-claim
-
- pod2.yaml;
-
apiVersion: v1kind: Podmetadata:name: app2spec:containers:- name: app2image: busyboxcommand: ["/bin/sh"]args: ["-c", "while true; do echo Printing the date $(date -u) from the pod $(hostname) >> /data/out2.txt; sleep 5; done"]volumeMounts:- name: persistent-storagemountPath: /datavolumes:- name: persistent-storagepersistentVolumeClaim:claimName: efs-claim
-
- pod1.yaml;
- Ahora ya podemos crear todos estos recursos;
- Verificamos que los datos estén escritos en el sistema de archivos de Amazon EFS.
4- Terraform destroy
- Al finalizar tus pruebas, recuerda eliminar todos los recursos creados. Este paso es crucial para evitar sorpresas desagradables en tu factura al final del mes. No subestimes la importancia de esta tarea, ya que la acumulación de recursos no utilizados puede generar costos innecesarios. Tomate el tiempo para limpiar tu entorno, eso te ahorrará dolores de cabeza en el futuro 🙂
Conclusiones
Hemos llegado al final de esta entrada, donde hemos explorado un ejercicio práctico en su totalidad, aplicable en situaciones reales. Durante este recorrido, hemos construido una arquitectura sólida y bien diseñada. Nuestro clúster se ha configurado con su API privada, un aspecto vital que destacamos al inicio de la entrada. Además, hemos accedido a este clúster desde nuestro servidor Jumbox, permitiéndonos una gestión segura y eficiente.
Durante nuestras pruebas, confirmamos el correcto funcionamiento de nuestro EFS al crear y utilizar recursos en el clúster de EKS. Desplegamos varios pods y presenciamos cómo pueden utilizar almacenamiento compartido de manera efectiva.
Espero que esta entrada haya sido útil y que lo hayan disfrutado tanto como yo, si tienen alguna pregunta o comentario, no duden en compartirlo.
Nota: Todo el código utilizado lo pueden encontrar en este repositorio.















