Subindo um cluster Kubernetes no AWS EKS com eksctl
Tags: aws, eks, kubernetes, devops
Published: 2022-06-21 15:06:44

Por mais surpreendente que seja, subir um *cluster* Kubernetes no AWS EKS há uns tempos atrás não era uma tarefa lá muito fácil, ainda mais comparado com outros *cloud providers* como o GCP. Foi então que surgiu o [eksctl](https://eksctl.io/), ferramenta desenvolvida em Go pela comunidade e que hoje é patrocinada pela [Weave](https://weave.works/), sendo o CLI oficial da AWS para gestão do EKS. Com ela, nós podemos criar e gerenciar *clusters* utilizando arquivos YAML e por debaixo dos panos são utilizadas *stacks* no CloudFormation para gestão dos recursos necessários.

## Instalação

Assim como outros binários escritos em Go, a instalação é super simples:

```bash
curl --silent --location "https://github.com/weaveworks/eksctl/releases/latest/download/eksctl_$(uname -s)_amd64.tar.gz" | tar xz -C /tmp
sudo mv m/tmp/eksctl /usr/local/bin
eksctl version
```

## Configuração de uma conta AWS

Não utilize sua conta **root** para a criação do *cluster*. Aliás, não utilize sua conta **root** para nada que não seja criar outras contas ou habilitar e gerir recursos muito específicos da AWS.

Caso seja sua conta pessoal, crie um outro usuário. Agora, caso seja uma conta corporativa, eu sugiro que habilite o AWS Organizations.

Crie um novo usuário no IAM com as seguintes *policies* atreladas a ele:

- AmazonEC2FullAccess
  
- AWSCloudFormationFullAccess
  
- EksAllAccess
  
- IamLimitedAccess
  

As duas primeiras são *policies* gerenciadas pela própria AWS e você não precisará criá-las. Já as duas últimas terão o seguinte conteúdo:

**EkaAllAccess**:

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "eks:*",
            "Resource": "*"
        },
        {
            "Action": [
                "ssm:GetParameter",
                "ssm:GetParameters"
            ],
            "Resource": [
                "arn:aws:ssm:*:<account_id>:parameter/aws/*",
                "arn:aws:ssm:*::parameter/aws/*"
            ],
            "Effect": "Allow"
        },
        {
             "Action": [
               "kms:CreateGrant",
               "kms:DescribeKey"
             ],
             "Resource": "*",
             "Effect": "Allow"
        },
        {
             "Action": [
               "logs:PutRetentionPolicy"
             ],
             "Resource": "*",
             "Effect": "Allow"
        }        
    ]
}
```

**IamLimitedAccess**:

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "iam:CreateInstanceProfile",
                "iam:DeleteInstanceProfile",
                "iam:GetInstanceProfile",
                "iam:RemoveRoleFromInstanceProfile",
                "iam:GetRole",
                "iam:CreateRole",
                "iam:DeleteRole",
                "iam:AttachRolePolicy",
                "iam:PutRolePolicy",
                "iam:ListInstanceProfiles",
                "iam:AddRoleToInstanceProfile",
                "iam:ListInstanceProfilesForRole",
                "iam:PassRole",
                "iam:DetachRolePolicy",
                "iam:DeleteRolePolicy",
                "iam:GetRolePolicy",
                "iam:GetOpenIDConnectProvider",
                "iam:CreateOpenIDConnectProvider",
                "iam:DeleteOpenIDConnectProvider",
                "iam:TagOpenIDConnectProvider",
                "iam:ListAttachedRolePolicies",
                "iam:TagRole",
                "iam:GetPolicy",
                "iam:CreatePolicy",
                "iam:DeletePolicy",
                "iam:ListPolicyVersions"
            ],
            "Resource": [
                "arn:aws:iam::<account_id>:instance-profile/eksctl-*",
                "arn:aws:iam::<account_id>:role/eksctl-*",
                "arn:aws:iam::<account_id>:policy/eksctl-*",
                "arn:aws:iam::<account_id>:oidc-provider/*",
                "arn:aws:iam::<account_id>:role/aws-service-role/eks-nodegroup.amazonaws.com/AWSServiceRoleForAmazonEKSNodegroup",
                "arn:aws:iam::<account_id>:role/eksctl-managed-*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "iam:GetRole"
            ],
            "Resource": [
                "arn:aws:iam::<account_id>:role/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "iam:CreateServiceLinkedRole"
            ],
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "iam:AWSServiceName": [
                        "eks.amazonaws.com",
                        "eks-nodegroup.amazonaws.com",
                        "eks-fargate.amazonaws.com"
                    ]
                }
            }
        }
    ]
}
```

Substitua `<account_id>` pelo ID da sua conta AWS.

Gere agora para o usuário uma credencial e guarde a ***access key ID*** e a ***secret key***. O **eksctl** necessita que o **AWS CLI** esteja instalado. Caso não o tenha, execute:

```bash
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install
aws configure
```

Serão solicitados dados como região padrão da AWS (us-east-1, por exemplo), a *access key ID* e a *secret key* para criação do perfil padrão (*default*). Agora, caso você já tenha o AWS CLI instalado e configurado, você pode criar o novo perfil manualmente editando o arquivo `~/.aws/credentials`.

> Atenção: quando possuir múltiplos *profiles* dentro do seu arquivo `~/.aws/credentials`, informe qual você quer utilizar setando o valor da variável `AWS_PROFILE` com o comando `export AWS_PROFILE=meu_perfil`.

## Criação do cluster

O **eksctl** por padrão criará o *cluster* em um VPC separado utilizando instâncias do tipo **m5.large**. Isso traz algumas implicações:

- Pela quota padrão, pode-se ter no máximo cinco VPCs por conta;
  
- Caso esteja utilizando um ambiente de estudos, laboratórios ou testes, esse tipo de instância pode ser considerado um pouco caro;
  
- Caso você precise que as aplicações do *cluster* comuniquem-se com recursos da AWS - uma instância no RDS, por exemplo - localizados em outro VPC, é necessária a configuração de *VPC Peering* e de rotas para que isso ocorra.

Nosso *cluster* terá as seguintes características:

- Ele será chamado de **demo-eks** e estará localizado em **us-east-1** (Virgínia do Norte);
  
- Quatro subredes serão criadas no novo VPC sendo duas públicas e duas privadas. Por padrão, o **eksctl** aloca os nós do *cluster* nas subredes privadas;
  
- Será criado um *nodegroup* chamado **ng-01** com um *auto scaling group*, com no mínimo uma instância e no máximo 5 do tipo **t3.medium**;
  
- *Policies* do IAM para **external-dns**, **auto-scaling**, **cert-manager**, **ebs** e **efs**. Falaremos mais sobre esses caras logo menos.

```yaml
---
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
  name: demo-eks
  region: us-east-1

managedNodeGroups:
  - name: ng-01
    instanceType: t3.medium
    desiredCapacity: 2
    minSize: 1
    maxSize: 5
    iam:
      withAddonPolicies:
        autoScaler: true
        externalDNS: true
        certManager: true
        ebs: true
        efs: true
```

> Como pode-se notar, o YAML é um manifesto muito parecido com os utilizados no Kubernetes.

Salve o arquivo acima como `demo-eks.yaml` e agora apenas execute `eksctl cluster create -f demo-eks.yaml --kubeconfig=demo-eks-kubeconfig` e deixe a mágica acontecer (naturalmente). O **eksctl** gerará a *stack* (ou *stacks*) dentro do CloudFormation e criará todos os recursos necessários, o que demorará cerca de quinze a vinte minutos (sim, é um pouco demorado) e pronto, você terá um *cluster* Kubernetes para uso no AWS EKS, com as credenciais para acesso ao mesmo no arquivo `demo-eks-kubeconfig.yaml`.

> Importante: Caso a opção `--kubeconfig` seja omitida, o **eksctl** gerará o arquivo de acesso em `~/.kube/config`, o que pode ser um problema caso você já possua outros *clusters* configurados. Caso seja o caso, você pode criar um diretório chamado `~/.kube/configs`, para armazenar os arquivos e alternar entre os mesmos setando a variável de `KUBECONFIG` apontando para o caminho do arquivo desejado (ex: `export KUBECONFIG=${HOME}/.kube/configs/demo-eks-kubeconfig`). Este passo é essencial para que o `kubectl` consiga conectar-se ao *cluster*.

## Instalação do Helm

Muito provavelmente você já ouviu falar do Helm, mas se isso não ocorreu, relaxa. O Helm é a grosso modo um gerenciador de pacotes para o Kubernetes. Esses pacotes são disponibilizados na forma de *charts* e com ele podemos criar os manifestos das nossas aplicações de uma forma mais dinâmica, organizada, parametrizável, e posteriormente hospedando-o em repositórios para *deployment*.

> Curiosidade inútil: "*chart*" pode ser traduzido para "*carta de navegação*". "*Kubernetes*" = "*timão*". Sacou, sacou? Hehehe.

Sua instalação é super simples:

```bash
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
```

Agora, caso você utilize uma distribuição *Debian-like* e deseje utilizar um pacote:

```bash
curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/null
sudo apt-get install apt-transport-https --yes
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
sudo apt-get update
sudo apt-get install helm
```

## Instalação de add-ons

### Metrics Server

O Metrics Server é um recurso necessário para o uso de HPAs (_Horizontal Pod Autoscalers_). HPAs são extremamente úteis quando nós queremos escalonar nossas aplicações horizontalmente quando as mesmas necessitam de mais recursos.

```bash
helm repo add metrics-server https://kubernetes-sigs.github.io/metrics-server/
helm upgrade --install --create-namespace -n metrics-server metrics-server metrics-server/metrics-server
```

### Prometheus

O Prometheus é uma solução de monitoramento. Caso você utilize o Lens ou outro *dashboard* para monitoramento dos seus *clusters*, ele irá fornecer gráficos de uso de CPU, memória, rede e disco para o *cluster*, *nodes*, *controllers* e *pods*. O Prometheus é comumente utilizado junto com o Grafana.

```bash
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install --create-namespace -n kube-prometheus prometheus bitnami/kube-prometheus
```

### ExternalDNS

O **ExternalDNS** sincroniza recursos do Kubernetes com registros DNS externos hospedados em _providers_ como AWS Route 53, Google Cloud DNS, AzureDNS, etc. Com ele, nós não precisamos manualmente criar entradas DNS toda vez que subimos uma nova aplicação no nosso *cluster*. Bacana né?

```bash
helm install external-dns stable/external-dns \
  --create-namespace \
  --namespace external-dns \
  --set provider=aws \
  --set aws.zoneType=public \
  --set txtOwnerId=<ZONE_ID>
  --set policy=sync
  --set registry=txt
  --set interval=20s
  --set domainFilters[0]=<DOMAIN>
```

Altere `<ZONE_ID>` para o ID da sua zona no AWS Route 53 e `<DOMAIN>` para o seu domínio no formato `dominio.com`.

### cert-manager

O **cert-manager** é um CRD (*Custom Resource Definition*) que gerará dinamicamente certificados TLS/SSL para as nossas aplicações utilizando o [Let's Encrypt](https://letsencrypt.org) (embora ele também suporte outros _issuers_).

```bash
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install --create-namespace -n cert-manager cert-manager bitnami/cert-manager \
  --create-namespace \
  --namespace cert-manager \
  --set installCRDs=true
```

Com o _chart_ instalado, é necessária a criação de dois CRDs, cada qual representando um *issuer* diferente do Let's Encrypt, um para produção e outro para *staging*. A diferença entre ambos é basicamente a limite de requisições que nós podemos realizar. Em ambiente de testes, prefira utilizar o ambiente de *staging*.

```yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: <meu_email>
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
      - http01:
          ingress:
            class: nginx
```

```yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    email: <meu_email>
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
      - http01:
          ingress:
            class: nginx
```

> Nota: Substitua `<meu_email>` pelo seu endereço de e-mail. Caso você esteja utilizando um Ingress diferente do Nginx, é necessário alterar os manifestos acima setando a classe adequada.

Execute o comando `kubectl apply -f cert-manager-staging.yaml -f cert-manager-prod.yaml` para criá-los no *cluster*.

### NGINX Ingress Controller

É por meio do NGINX Ingress Controller que nós permitiremos acesso às nossas aplicações. Você pode enxergar um Ingress como um proxy reverso.

```bash
helm upgrade --install ingress-nginx ingress-nginx \
  --repo https://kubernetes.github.io/ingress-nginx \
  --namespace ingress-nginx \
  --create-namespace
```

### Auto Scaling Group

Um dos recursos mais legais de _cloud computing_ é a **elasticidade**. Imagine que durante o ano todo nós temos uma demanda relativamente constante, mas em certas épocas -  como *Black Friday* - elas podem aumentar consideravelmente e logo você vai precisar de mais nós dentro dos seus *nodegroups* para suprir as necessidades dos seus *pods*. Para que não tenhamos que fazer isso manualmente, vamos adicionar a funcionalidade de escalonamento automático do nosso _nodegroup_.

```bash
helm repo add autoscaler https://kubernetes.github.io/autoscaler
helm install --create-namespace -n cluster-autoscaler autoscaler/cluster-autoscaler \
  --set 'autoDiscovery.clusterName=<cluster_name>'
```

> Nota: Substitua `<cluster_name>` pelo nome do seu _cluster_ (neste caso, `demo-eks`).

Para testar, após subir uma aplicação qualquer, aumente a quantidade de réplicas para um número não atendido pelos recursos disponíveis no seu *nodegroup*. O **cluster-autoscaler** disponibilizará novos nós até o número máximo setado no *nodegroup*. Quando a quantidade de *pods* consumir menos do que 50% dos recursos de um nó por mais do que 10 minutos, este nó será removido do *nodegroup*. Bacana né?

## Subindo uma aplicação

Com o nosso *cluster* montado e os *add-ons* instalados, está na hora de subir uma aplicação teste. Abaixo os manifestos dentro de um único arquivo `hello-kubernetes.yaml`:

```yaml
apiVersion: v1
kind: Namespace
metadata:
  name: hello-kubernetes
---
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: hello-kubernetes
  name: hello-kubernetes
spec:
  replicas: 2
  selector:
    matchLabels:
      app: hello-kubernetes
  template:
    metadata:
      labels:
        app: hello-kubernetes
    spec:
      containers:
      - name: hello-kubernetes
        image: paulbouwer/hello-kubernetes:1.5
        ports:
        - containerPort: 8080
        resources:
          requests:
            memory: "128Mi"
            cpu: "250m"
          limits:
            memory: "256Mi"
            cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
  namespace: hello-kubernetes
  name: hello-kubernetes
spec:
  type: ClusterIP
  selector:
    app: hello-kubernetes
  ports:
  - port: 80
    targetPort: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  namespace: hello-kubernetes
  name: hello-kubernetes-ingress
  annotations:
    kubernetes.io/ingress.class: nginx
    external-dns.alpha.kubernetes.io/hostname: <app.DOMINIO>
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  tls:
    - hosts:
        - <app.DOMINIO>
      secretName: <app.DOMINIO>
  rules:
    - host: <app.DOMINIO>
      http:
        paths:
        - path: /
          pathType: Prefix
          backend:
            service:
              name: hello-kubernetes
              port: 
                number: 80
```

> Nota: Substitua `<app.DOMINIO> pelo domínio que você quer utilizar na sua aplicação, algo como *app-01.juquinha.net*.

Se tudo der certo, eis o que devera acontecer:

- Entradas serão criadas dentro da sua zona DNS lá dentro do AWS Route 53 de acordo com o que foi configurado no seu ingress e nas configurações do seu ExternalDNS e dentro de alguns minutos você poderá acessar sua aplicação por meio desse nome. Você pode monitorar a propagação por meio da ferramenta `dig`;
  
- Um certificado será gerado automaticamente para sua aplicação. Isso pode demorar um tempo para aplicações novas;
  
- Nós alocamos duas réplicas para a aplicação e aqui cabe uma ressalva muito importante: O Kubernetes por padrão fará um balanceamento do tipo *round-robin* entre os *pods*. Em grande parte das aplicações *web* atuais nós trabalhamos com sessões e como consequência disso, pode ocorrer o cenário em que o usuário se autentica em um *pod*, e na próxima requisição ser redirecionado ao outro, onde a sessão não existe. Como consequência, são apresentados comportamentos estranhos para o usuário, como o redirecionamento para a tela de login, mensagens de "Não autorizado", intermitência no acesso, etc. Para que isso não ocorra, nós normalmente utilizamos algum serviço como o **Redis**, **memcached** (estes podendo ou não serem no AWS ElastiCache) ou a funcionalidade de *sticky-sessions*, em que um usuário é constantemente enviado para o mesmo *pod*.

## Conclusão

Kubernetes é um assunto extremamente extenso e complexo é há uma infinidade de possibilidades com ele. Apresentei aqui algumas delas que podem ser utilizadas não só no AWS EKS, mas em instalações *on-premises* (com excessão do **cluster-autoscaler**, a menos que você esteja utilizando **OpenStack** na sua empresa). Existe também uma série de melhorias de segurança que podem ser realizadas no *cluster* montado neste artigo. Espero que vocês tenham curtido!

## Referências

- [eksctl](https://eksctl.io)