skip to content
A covered up pug in the woodsBlog

AWS EKS Fargate with Terraform (1)

/ 11 min read

En este tutorial vamos a configurar un cluster de Kubernetes en AWS EKS. Esta es la primera parte de 3, donde crearemos el cluster para desplegar nuestras apps.

repositorio parte 2 parte 3

Intro

  • Crear una AWS VPC para nuestros recursos usando Terrafom
  • Crear un cluster de AWS EKS con Fargate usando Terraform
  • Actualizar CoreDNS para que pueda correr en Fargate
  • Crear un registry para nuestras imagenes de las aplicaciones frontend y backend en AWS ECR
  • Publicar nuestras imagenes en nuestro registry de ECR
  • Desplegar nuestros pods (Frontend y Backend) con sus respectivos secrets
  • Crear servicios para nuestros pods
  • Mejorar la estabilidad de nuestros pods con Pod Disruption Budget
  • Crear un IAM OIDC provieder usando Terraform
  • Desplegar un AWS Load Balancer controller (que creara un ALB por cada ingress que tengamos) en nuestro cluster usando Terraform
  • Crear un ingress para nuestros pods (frontend y backend)
  • Adjuntar dominios a nuestro ALB
  • Secure Ingress con SSL/TLS
  • Habilitar Fargate Loggin para logear a CloudWatch

Y vamos a automatizar todos estos pasos con GitHub Actions.

Pre requisitos

Crear un repositorio en GitHub

Lo primero es crear el repositorio de GitHub para poder automatizar el proceso de provisioning con Terraform. Para eso debemos crear un repositorio.

Para eso ejecutaremos los siguiente comandos:

ldamore@Desktop:~ $ mkdir aws-eks-fargate
ldamore@Desktop:~ $ cd aws-eks-fargate
ldamore@Desktop/aws-eks-fargate:~ $ echo "# aws-eks-fargate" >> README.md
ldamore@Desktop/aws-eks-fargate:~ $ git init
ldamore@Desktop/aws-eks-fargate:~ $ git add README.md
ldamore@Desktop/aws-eks-fargate:~ $ git commit -m "first commit"
ldamore@Desktop/aws-eks-fargate:~ $ git branch -M main
ldamore@Desktop/aws-eks-fargate:~ $ git remote add origin [your-origin]
ldamore@Desktop/aws-eks-fargate:~ $ git push -u origin main

Crear un Terraform Provider

Para comenzar a usar terraform primero necesitamos declarar un aws terraform provider. Para eso crearemos una carpeta llamada terraform y un archivo dentro de ella llamado 0-provider.tf con el siguiente codigo.

Dentro de terraform/0-provider.tf

provider "aws" {
  region = "us-east-1"
  access_key = var.aws_access_key
  secret_key = var.aws_secret_key
}

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

  backend "s3" {
    bucket   = "ldamore-terraform-state" // Put your bucket name
    key      = "terraform.tfstate"
    region   = "us-east-1"
    encrypt  = true
  }
}

variable "aws_access_key" {
  type = string
}

variable "aws_secret_key" {
  type = string
}

variable "cluster_name" {
  type = string
  default = "demo"
}

variable "cluster_version" {
  type = string
  default = "1.22"
}

Este archivo iniciara nuestro provider de terraform y guardara el estado de los recursos que vamos creando en un bucket de S3 en nuestro AWS. Recuerde tener un access key y un secret key con los roles necesarios para crear recursos.

Debemos crear una carpeta en la raiz del proyecto llamada .github y dentro de ella otra carpeta llamada workflows. En la carpeta workflows crearemos un archivo provisioning.yml con el siguiente codigo dentro. Este archivo se ejecutara cada vez pusheemos y que cambiemos algo dentro de la carpeta terraform.

Dentro de .github/workflows/provisioning.yml

on:
  push:
    branches:
      - main
    paths:
      - terraform/*

jobs:
  provisioning-staging:
    runs-on: ubuntu-latest
    env:
      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    steps:
      - name: Checkout repository
        uses: actions/checkout@v2
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v2
      - name: Apply terraform
        run: |
          cd ${GITHUB_WORKSPACE}/terraform
          terraform init -backend-config "access_key=${AWS_ACCESS_KEY_ID}" -backend-config "secret_key=${AWS_SECRET_ACCESS_KEY}"
          terraform plan -var="aws_access_key=${AWS_ACCESS_KEY_ID}" -var="aws_secret_key=${AWS_SECRET_ACCESS_KEY}" -out="terraform.tfplan"
          terraform apply -auto-approve terraform.tfplan

El proposito de este job es aplicar los archivos terraform para crear los recursos necesarios en aws.

Para que este archivo se ejecute correctamente debemos agregar nuestro AWS_ACCESS_KEY_ID y AWS_SECRET_ACCESS_KEY como secrets en nuestro repositorio.

secrets

Una vez configurado los secrets podemos commitear y pushear nuestro codigo. Esto activara por primera vez el pipeline y iniciara el terraform provider y creara el archivo con nuestro estado de terraform en el bucket.

Bucket

bucket

Crear una AWS VPC para nuestros componentes usando Terraform

Dentro de esta seccion crearemos algunos recursos de networking como:

  • VPC
  • Subnets (Publics & Privates)
  • Internet Gateway
  • NAT Gateway
  • Route Tables

VPC

Ahora es momento de crear una VPC (Virtual Private Cloud) para nuestros componentes.

Para eso dentro de la carpeta terraform debemos crear el siguiente archivo 1-vpc.tf y dentro:

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"

  tags = {
    Name = "main"
  }
}

Internet Gateway

Luego un Internet Gateway, este sera usado para proveer acceso a internet directo a las Subnets publicas y acceso a internet indirecto a las Subnets privadas usando un NAT Gateway.

Crearemos un archivo terraform/2-igw.tf con**:**

resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "igw"
  }
}

Subnets

Ahora debemos crear nuestras subnets, 2 publicas y 2 privadas.

Crear el archivo terraform/3-subnets.tf con el siguiente codigo:

resource "aws_subnet" "private-us-east-1a" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.0.0/19"
  availability_zone = "us-east-1a"

  tags = {
    "Name"                                      = "private-us-east-1a"
    "kubernetes.io/role/internal-elb"           = "1"
    "kubernetes.io/cluster/${var.cluster_name}" = "owned"
  }
}

resource "aws_subnet" "private-us-east-1b" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.32.0/19"
  availability_zone = "us-east-1b"

  tags = {
    "Name"                                      = "private-us-east-1b"
    "kubernetes.io/role/internal-elb"           = "1"
    "kubernetes.io/cluster/${var.cluster_name}" = "owned"
  }
}

resource "aws_subnet" "public-us-east-1a" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.64.0/19"
  availability_zone       = "us-east-1a"
  map_public_ip_on_launch = true

  tags = {
    "Name"                                      = "public-us-east-1a"
    "kubernetes.io/role/elb"                    = "1"
    "kubernetes.io/cluster/${var.cluster_name}" = "owned"
  }
}

resource "aws_subnet" "public-us-east-1b" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.96.0/19"
  availability_zone       = "us-east-1b"
  map_public_ip_on_launch = true

  tags = {
    "Name"                                      = "public-us-east-1b"
    "kubernetes.io/role/elb"                    = "1"
    "kubernetes.io/cluster/${var.cluster_name}" = "owned"
  }
}

Se necesitan al menos dos subnets publicas y dos subnets privadas porque los load balancer que crearemos necesitan al menos 2 subredes.

El Internal-elb tag es usado por EKS para seleccionar las subnets privada y crearles private load balancer y el elb tag para crear public load balancers. Ademas necesitamos un cluster tag con un valor de owned or shared.

NAT

Para el NAT Gateway crearemos una ip elastica para no ocuparnos de alocarlo en una ip especifica (que podria estar en uso) y luego el NAT Gateway en si especificando sobre que Internet Gateway y que public subnet depende.

Crear archivo terraform/4-nat.tf con:

resource "aws_eip" "nat" {
  vpc = true

  tags = {
    Name = "nat"
  }
}

resource "aws_nat_gateway" "nat" {
  allocation_id = aws_eip.nat.id
  subnet_id     = aws_subnet.public-us-east-1a.id

  tags = {
    Name = "nat"
  }

  depends_on = [aws_internet_gateway.igw]
}

Route Tables

Por ultimo antes de empezar a crear nuestro EKS Cluster debemos crear Route Tables.

Crearemos el archivo terraform/5-routes.tf con el codigo:

resource "aws_route_table" "private" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.nat.id
  }

  tags = {
    Name = "private"
  }
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }

  tags = {
    Name = "public"
  }
}

resource "aws_route_table_association" "private-us-east-1a" {
  subnet_id      = aws_subnet.private-us-east-1a.id
  route_table_id = aws_route_table.private.id
}

resource "aws_route_table_association" "private-us-east-1b" {
  subnet_id      = aws_subnet.private-us-east-1b.id
  route_table_id = aws_route_table.private.id
}

resource "aws_route_table_association" "public-us-east-1a" {
  subnet_id      = aws_subnet.public-us-east-1a.id
  route_table_id = aws_route_table.public.id
}

resource "aws_route_table_association" "public-us-east-1b" {
  subnet_id      = aws_subnet.public-us-east-1b.id
  route_table_id = aws_route_table.public.id
}

La primera es una tabla de ruta privada con ruta default al NAT Gateway. La segunda es una tabla de ruta publica con ruta default al Internet Gateway. Finalmente, debemos asociar las subnets creadas anteriormente a estas route tables.

Luego de crear estos archivos para el networking, pusheamos para que se active el pipeline y terraform cree la VPC por nosotros.

Create a AWS EKS with Fargate using Terraform

El siguiente paso es crear el cluster de Kubernetes en EKS con nodos gestionados por Fargate. Para eso debemos crear el EKS control plane sin nodos (luego fargate los creara por nosotros).

El control plane consiste en nodos que corren Kubernetes software como etcd o la Kubernetes API server.

Lo primero de todo es crear un IAM role para EKS. Este rol se usara para poder crear el cluster. Recordemos que ante cada creacion de recurso necesitamos tener ciertos permisos para crearlos. Para eso creamos un archivo terraform/6-eks.tf con esto:

resource "aws_iam_role" "eks-cluster" {
  name = "eks-cluster-${var.cluster_name}"

  assume_role_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "eks.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
POLICY
}

Despues tenemos que adjuntarle la AmazonEKSClusterPolicy a este rol.

Dentro de 6-eks.tf

resource "aws_iam_role_policy_attachment" "amazon-eks-cluster-policy" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
  role       = aws_iam_role.eks-cluster.name
}

Y, por supuesto, el propio EKS control plane. Debemos usar el rol creado para crear el cluster.

Además, hay que especificar al cluster las dos subnets privadas y las dos públicas. AWS Fargate solo puede usar subnets privadas con NAT Gateway para deployar nuestros pods. Las subnets públicas se pueden usar para que los load balancers que expongan la aplicación a Internet.

Creamos 6terraform/6-eks.tf con

resource "aws_eks_cluster" "cluster" {
  name     = var.cluster_name
  version  = var.cluster_version
  role_arn = aws_iam_role.eks-cluster.arn

  vpc_config {

    endpoint_private_access = false
    endpoint_public_access  = true
    public_access_cidrs     = ["0.0.0.0/0"]

    subnet_ids = [
      aws_subnet.private-us-east-1a.id,
      aws_subnet.private-us-east-1b.id,
      aws_subnet.public-us-east-1a.id,
      aws_subnet.public-us-east-1b.id
    ]
  }

  depends_on = [aws_iam_role_policy_attachment.amazon-eks-cluster-policy]
}

Una vez pusheado y creado el cluster deberiamos poder verlo en nuestro EKS.

Y desde nuestra consola de comandos podemos conectarnos al cluster y ejecutar comandos de kubernetes. Para eso debemos correr

ldamore@Desktop/aws-eks-fargate:~ aws configure //para configurar el aws-cli con nuestra cuenta
ldamore@Desktop/aws-eks-fargate:~ aws eks update-kubeconfig --name demo --region us-east-1 //para darle acceso de nuestro cluster a kubectl
ldamore@Desktop/aws-eks-fargate:~ kubectl get pods -A // para listar los pods creados hasta le momento

Si todo fue correcto, podemos listar todos los pods de nuestro cluster (que por ahora son 2 de CoreDNS que necesita nuestro cluster de Kubernetes para funcionar)

pods

Si podemos observar, los pods estan en status Pending, eso lo solucionaremos en el siguiente paso.

Actualizar CoreDNS

Para que Fargate pueda crear nodos para los pods de CoreDNS debemos hacer lo siguiente.

Primero debemos crear un perfil de Fargate. Un Fargate profile, configurado en un cluster de EKS, permite crear nodos de manera automatica, para los pods de un namespace de cluster.

Necesitamos crear un único rol de IAM que se pueda compartir entre todos los perfiles de Fargate. Al igual que EKS, Fargate necesita permisos para activar los nodos y conectarlos al EKS control plane.

Para eso crearemos el archivo 7-kube-system-profile.tf con lo siguiente:

resource "aws_iam_role" "eks-fargate-profile" {
  name = "eks-fargate-profile"

  assume_role_policy = jsonencode({
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        Service = "eks-fargate-pods.amazonaws.com"
      }
    }]
    Version = "2012-10-17"
  })
}

Luego tenemos que adjuntar la IAM policy llamada AmazonEKSFargatePodExecutionRolePolicy.

resource "aws_iam_role_policy_attachment" "eks-fargate-profile" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSFargatePodExecutionRolePolicy"
  role       = aws_iam_role.eks-fargate-profile.name
}

Y por ultimo el Fargate profile con el rol

resource "aws_eks_fargate_profile" "kube-system" {
  cluster_name           = aws_eks_cluster.cluster.name
  fargate_profile_name   = "kube-system"
  pod_execution_role_arn = aws_iam_role.eks-fargate-profile.arn

  # These subnets must have the following resource tag:
  # kubernetes.io/cluster/<CLUSTER_NAME>.
  subnet_ids = [
    aws_subnet.private-us-east-1a.id,
    aws_subnet.private-us-east-1b.id
  ]

  selector {
    namespace = "kube-system"
  }
}

Ahora, si obtenemos los pods nuevamente, esperariamos que CoreDNS ya este con los pods corriendo y con nodos asignados. Pero lo más probable es que, si el equipo de EKS no lo soluciona en versiones posteriores, esos pods de coreDNS seguirán en estado pendiente, ya que no tienen nodos asignados.

Podemos intentar describir el pod para obtener algún tipo de error de Kubernetes (kubectl describe pod). Deberíamos ver algo como: no hay nodos disponibles. Si te scrolleamos hacia arriba, veremos la razón. Estos pods vienen con la anotacion compute-type: ec2 que evita que Fargate cree los nodos para dichos pods. La solución es simple, simplemente hay que eliminar la anotación.

Para eliminar debemos correr el siguiente comando

kubectl patch deployment coredns \
-n kube-system \
--type json \
-p='[{"op": "remove", "path": "/spec/template/metadata/annotations/eks.amazonaws.com~1compute-type"}]'

Una vez eliminada la anotacion, AWS Fargate proveera un par de nodos para que puedan correr los pods.

Luego de unos minutos si volvemos a correr kubectl get pods -A podremos ver que no estos pods estan status ready.