Cloud Intelligence™Cloud Intelligence™

Cloud Intelligence™

Atribua IPs públicos estáticos aos workers do Amazon EKS em Local Zones com o KubeIP v2

By Wiriyang (Pup) PipatsakulrojDec 10, 20247 min read

Esta página também está disponível em English, Deutsch, Español, Français, Italiano e 日本語.

Este post mostra como atribuir um Elastic IP a nodes do EKS em uma Local Zone usando o KubeIP v2.

Introdução

Clientes da AWS usam o Amazon EKS em AWS Local Zones para ter acesso de baixa latência e atender a exigências de localização de dados. Alguns casos de uso exigem endereços IP públicos estáticos para workloads que se comunicam com parceiros regulados. Só que recursos do Kubernetes (k8s), como worker nodes, são efêmeros, e isso faz o IP mudar a cada evento como uma atualização de versão. O KubeIP atribui endereços IP estáticos aproveitando recursos do provedor de nuvem, garantindo um endereçamento consistente mesmo com as mudanças no ciclo de vida dos nodes.

Visão geral da solução

Este post mostra como configurar um cluster do Amazon EKS com um managed-node group em uma região e um self-managed node group na Local Zone. A ideia é demonstrar como atribuir um Elastic IP a um node do EKS na Local Zone usando o KubeIP v2.

O diagrama de arquitetura abaixo mostra um edge service rodando na Local Zone e dois serviços de backend rodando na região.

Pré-requisitos

Passo a passo

  1. O artefato de exemplo tem 4 pastas: 01-vpc, 02-eks, 03-kubeip e 04-app. Vamos aplicá-las em ordem, da 01-vpc até a 04-app.

Clone o artefato de exemplo do github no seu diretório de trabalho.

git clone https://github.com/duoh/kubeip-eks-localzone
cd kubeip-eks-localzone
  1. Primeiro, provisionamos uma VPC e subnets na região e na Local Zone. No arquivo main.tf, usamos o módulo vpc para criar subnets públicas e privadas em uma região. Já as subnets na Local Zone são definidas em recursos aws_subnet.
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)]

  ...

  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
}

resource "aws_subnet" "private-subnet-lz" {
  ...
}

...

Defina as variáveis de entrada. No exemplo, o nome da VPC é `kubeip-lz-eks-vpc`, a região é `ap-southeast-1` e a Local Zone é `ap-southeast-1-bkk-1a`.

cd 01-vpc
vi example.auto.tfvars
region          = "ap-southeast-1"
lzs             = ["ap-southeast-1-bkk-1a"]
name            = "kubeip-eks-lz-vpc"
vpc_cidr        = "10.0.0.0/16"
cluster_name    = "kubeip-eks-lz-cluster"

Faça o deploy da VPC, das subnets e das route tables com o terraform.

terraform init
terraform apply -auto-approve

Anote o output.

Outputs:
private_subnets = [\
  "subnet-09139a5ea9dcd335d",\
  "subnet-0d7cc25e5066687be",\
  "subnet-04bb1e787c9b6e28f",\
]
public_subnets_local_zone = "subnet-0edd1b731c48fb7d7"
vpc_id = "vpc-061f434818a666d8b"
  1. Em seguida, criamos um cluster EKS com um managed node group na região. Também criamos um self-managed node group na Local Zone. Nesse self-managed node group, os nodes recebem as labels ‘eks.amazonaws.com/nodegroup=public-lz-ng’ e ‘kubeip=use’. Além disso, usamos o módulo kubeip_role para criar uma IRSA (IAM Role for Service Accounts) para o DaemonSet do KubeIP.
module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  cluster_name    = var.cluster_name
  cluster_version = "1.30"

  vpc_id                         = var.vpc_id
  subnet_ids                     = var.private_subnets
  cluster_endpoint_public_access = true
  enable_irsa = true
  ...

  eks_managed_node_groups = {
    region-ng = {
      ...
      subnet_ids    = var.private_subnets
      labels = {
        region  = "true"
      }
      ...
    }
  }

  self_managed_node_groups = {
    local-ng = {
      ...
      subnet_ids    = [var.public_subnets_local_zone]
      bootstrap_extra_args = "--kubelet-extra-args '--node-labels=eks.amazonaws.com/nodegroup=public-lz-ng,kubeip=use'"
      ...
    }
  }
  enable_cluster_creator_admin_permissions = true
}

resource "aws_iam_policy" "kubeip-policy" {
  name        = "kubeip-policy"
  description = "KubeIP required permissions"
  policy = jsonencode({
    ...
  })
}

module "kubeip_role" {
  source    = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
  role_name = var.kubeip_role_name
  role_policy_arns = {
    "kubeip-policy" = aws_iam_policy.kubeip-policy.arn
  }
  ...
}

...

Defina as variáveis de entrada. Em vpc_id, private_subnets e public_subnets_local_zone, copie os valores do output anterior.

cd ../02-eks
vi example.auto.tfvars
region                     = "ap-southeast-1"
vpc_id                     = "vpc-061f434818a666d8b"
private_subnets            = [\
                              "subnet-09139a5ea9dcd335d",\
                              "subnet-0d7cc25e5066687be",\
                              "subnet-04bb1e787c9b6e28f",\
                             ]
public_subnets_local_zone  = "subnet-0edd1b731c48fb7d7"
cluster_name               = "kubeip-eks-lz-cluster"
kubeip_role_name           = "kubeip-agent-role"
kubeip_sa_name             = "kubeip-agent-sa"

Faça o deploy dos recursos com o terraform.

terraform init
terraform apply -auto-approve

Anote os outputs.

Outputs:
eks_cluster_name = "kubeip-eks-lz-cluster"
kubeip_role_arn = "arn:aws:iam::xxxxxxxxxxxx:role/kubeip-agent-role"
  1. Depois, provisionamos um Elastic IP e recursos do k8s, como um DaemonSet e uma service account. O Elastic IP definido no recurso aws_eip é provisionado dentro da Local Zone por meio do argumento network_border_group. Também atribuímos o DaemonSet do KubeIP ao node na Local Zone usando o campo node_selector. Um recurso interessante do KubeIP é a possibilidade de selecionar EIPs filtrados por tags da AWS. Vale destacar que a service account do KubeIP precisa estar vinculada à IRSA e a uma RBAC role.
resource "aws_eip" "kubeip" {
  count = 1
  tags = {
    Name        = "kubeip-${count.index}"
    environment = "demo"
    kubeip      = "reserved"
  }
  network_border_group = var.network_border_group
}

resource "kubernetes_daemonset" "kubeip_daemonset" {
  metadata {
    name      = "kubeip-agent"
    ...
  }
  spec {
    ...

    template {
      ...
      spec {
        service_account_name             = var.kubeip_sa_name
        ...
        container {
          name  = "kubeip-agent"
          image = "doitintl/kubeip-agent"
          env {
            name  = "FILTER"
            value = "Name=tag:kubeip,Values=reserved;Name=tag:environment,Values=demo"
          }
          ...
        }
        node_selector = {
          "eks.amazonaws.com/nodegroup" = "public-lz-ng"
          kubeip = "use"
        }
      }
    }
  }
  depends_on = [kubernetes_service_account.kubeip_service_account]
}

resource "kubernetes_service_account" "kubeip_service_account" {
  metadata {
    name        = var.kubeip_sa_name
    namespace   = "kube-system"
    annotations = {
      "eks.amazonaws.com/role-arn" = var.kubeip_role_arn
    }
  }
}

resource "kubernetes_cluster_role" "kubeip_cluster_role" {
  ...
}

resource "kubernetes_cluster_role_binding" "kubeip_cluster_role_binding" {
  ...
}

Defina as variáveis de entrada. Em kubeip_role_arn, copie o valor do output anterior.

cd ../03-kubeip
vi example.auto.tfvars
region                  = "ap-southeast-1"
network_border_group    = "ap-southeast-1-bkk-1"
cluster_name            = "kubeip-eks-lz-cluster"
kubeip_role_arn         = "arn:aws:iam::xxxxxxxxxxxx:role/kubeip-agent-role"
kubeip_sa_name          = "kubeip-agent-sa"

Faça o deploy de um Elastic IP (EIP) e dos recursos do Kubernetes (k8s) com o Terraform.

terraform init
terraform apply -auto-approve

Anote o endereço IP do output para usar nos testes mais adiante.

elastic_ips = [\
  "15.220.243.225",\
]
  1. Por fim, mostramos um cenário em que duas aplicações rodam em um node regional e uma aplicação roda em um node na Local Zone. A aplicação na Local Zone funciona como edge service: recebe o tráfego web dos usuários finais e o roteia para as aplicações na região.

Os arquivos app_a.yaml e app_b.yaml definem as aplicações de back-end que rodam na região.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-a-deployment
  ...
spec:
  ...
    spec:
      containers:
      - name: app-a
        image: hashicorp/http-echo
        ports:
        - containerPort: 5678
        args: ["-text=<h1>I'm APP <em>A</em></h1>"]
      nodeSelector:
        region: "true"
---
apiVersion: v1
kind: Service
metadata:
  name: app-a-service
spec:
  type: ClusterIP
  ...

O arquivo edge_svc.yaml define uma aplicação edge na Local Zone.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: edge-deployment
  ...
spec:
  ...
    spec:
      containers:
      - name: edge-svc
        image: nginx
        ports:
        - containerPort: 80
        volumeMounts:
        - name: indexfile
          mountPath: /usr/share/nginx/html/
          readOnly: true
        - name: nginx-conf
          mountPath: /etc/nginx/conf.d/
      nodeSelector:
        kubeip: use
      volumes:
      ...

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-indexfile-configmap
data:
  index.html: |
    <h1>I am EDGE SERVICE</h1>
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-conf-configmap
data:
  default.conf: |
    server {
      resolver kube-dns.kube-system.svc.cluster.local valid=1s
      ...
    }
---
apiVersion: v1
kind: Service
metadata:
  name: edge-service
spec:
  type: NodePort
  ...

Vá até o diretório 04-app e execute o aws cli conforme a sua região para atualizar o arquivo kubeconfig e autenticar no cluster do EKS.

cd ../04-app
aws eks update-kubeconfig --name kubeip-lz-cluster --region ap-southeast-1

Rode o kubectl para confirmar se o acesso funcionou.

kubectl get no
NAME                                             STATUS   ROLES    AGE     VERSION
ip-10-0-10-213.ap-southeast-1.compute.internal   Ready    <none>   4m      v1.30.0-eks-036c24b
ip-10-0-5-76.ap-southeast-1.compute.internal     Ready    <none>   3m11s   v1.30.0-eks-036c24b

Faça o deploy das aplicações e confira se elas estão rodando.

kubectl apply -f edge_svc.yaml
kubectl apply -f app_a.yaml
kubectl apply -f app_b.yaml

kubectl get po
NAME                                READY   STATUS    RESTARTS   AGE
app-a-deployment-587b484997-k6f8q   1/1     Running   0          12s
app-b-deployment-78bc6675db-2mk2s   1/1     Running   0          12s
edge-deployment-f6b9f4d5f-5j6f7     1/1     Running   0          13s

Confirme se a aplicação está funcionando usando o endereço IP público do passo anterior.

Teste com curl.

% curl 15.220.243.225:30000
<h1>I am EDGE SERVICE</h1>

% curl 15.220.243.225:30000/app-a
<h1>I'm APP <em>A</em></h1>

% curl 15.220.243.225:30000/app-b
<h1>I'm APP <em>B</em></h1>

Ou teste pelo navegador.

Conclusão

As Local Zones na AWS só permitem o uso de Application Load Balancers (ALBs) do serviço Elastic Load Balancing (ELB). Além disso, os IPs públicos atribuídos aos ALBs podem mudar com o tempo. Clusters EKS rodando em Local Zones precisam de uma automação personalizada para manter IPs públicos estáticos nos worker nodes — e é aí que o KubeIP entra para resolver esse problema.

A DoiT combina tecnologia inteligente e expertise multi-cloud para ajudar empresas a entender e tirar o máximo de nuvens públicas como Amazon Web Services (AWS), Google Cloud (GCP) e Microsoft Azure, impulsionando o crescimento do negócio. Conheça as soluções da DoiT em doit.com.