Boas práticas na utilização do Kubernetes em produção
O Kubernetes é uma ferramenta para orquestração de containers extremamente consolidada no mercado e com muitos recursos que facilitam a administração, diminuindo o overhead operacional de se manter ambientes com containers. Contudo, quando falamos de ambientes de produção, alguns pontos como segurança, disponibilidade, escalabilidade e outros precisam ser tratados com muito afinco para que tenhamos uma plataforma resiliente e preparada para receber grandes cargas de trabalho. A ideia desse post é apresentar alguns dos vários pontos de atenção que precisamos ter na hora de implementar/utilizar nosso cluster.
Autenticação e autorização
Existem várias estrátegias de autenticação no Kubernetes
. Aqui no Elo7 usamos certificados x509 que são gerados pelo vault. No nosso caso, a autorização é feita pelo vault
com base no usuário do GitHub. Independentemente da estratégia adotada, a ideia aqui é não deixar o cluster acessível sem autenticação e, preferencialmente, que seja adotado um mecanismo de autorização com base nas permissões que cada usuário precise.
Liveness e Readiness probes
Para o Kubernetes
, por padrão, o seu container estará apto a receber requisições a partir do momento em que o pod estiver com o status de Ready, mas não necessariamente ele está de fato “pronto”. Um bom exemplo disso seria uma aplicação Java com Spring que pode levar alguns segundos até carregar todas as suas dependências e iniciar a app. Nesse meio tempo, as requisições que forem encaminhadas para esse pod retornaram erro 500 para o Service e consequentemente para o usuário. Pensando nisso, existe uma técnica para garantir que o pod só receberá requisições quando a app estiver realmente pronta. Podemos fazer isso usando readiness probe. Vejamos abaixo um exemplo:
readinessProbe:
httpGet:
path: "/health"
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
successThreshold: 3
initialDelaySeconds = Tempo em segundos que o Kubernetes
aguarda antes de realizar uma operação de readiness ou liveness, após o container ser iniciado.
periodSeconds = De quantos em quantos segundos o Kubernetes
deve realizar uma operação de readiness ou liveness.
successThreshold = Quantas vezes consecutivas o probe precisa responder com OK para ser considerado pronto para receber tráfego.
No nosso exemplo estamos realizando um GET em POD_IP:8080/health
Assim como temos o readinessProbe
para avaliar se o pod está pronto, também temos o livenessProbe
para dizer se o pod está vivo, ou seja, para dizer se a app está de pé ou não. O funcionamento é basicamente o mesmo do readinessProbe
. É importante destacar que essas checagens podem ser feitas com comandos também. Vejamos um exemplo:
readinessProbe:
exec:
command:
- test
- -e
- /tmp/endpoints.json
initialDelaySeconds: 15
periodSeconds: 30
Nesse caso o que será avaliado para considerar esse container como saudável é a existencia do arquivo /tmp/endpoints.json
Quanto de recurso eu defino aqui?
Por padrão, se nada foi informado, o Kubernetes
irá subir seus containers sem limites para uso de CPU e memória (por exemplo). Quando estamos definindo alguns objetos como deployments, temos a possibilidade de configurar valores de requests e limits na sessão de resources
. Vejamos abaixo o que cada um significa e um exemplo de implementação:
requests = Qual o valor de CPU e memória que o Kubernetes
precisa reservar para o meu container ser iniciado.
limits = Qual o valor máximo de CPU e memória que meu container pode consumir no Kubernetes
.
resources:
requests:
memory: "128Mi"
cpu: "1000m"
limits:
memory: "512Mi"
cpu: "2000m"
No exemplo acima, estamos definindo 512MB de memória/2000 millicores para limits e 128 MB de memória/1000 millicores para requests (lembrando que 1m representa 1/1000 = 0,001 de um core de CPU, assim como 500m = 500/1000 = 0,5 core de uma CPU ou 50% de uma CPU).
Estamos falando para o Kubernetes reservar 1 core de uma CPU para nosso container, mas a grande pergunta é: precisamos realmente reservar 1 core de CPU para esse container? Provavelmente a resposta vai ser não, mas a única forma de termos certeza é acompanharmos o consumo de recursos no container (podemos utilizar ferramentas como o cAdivisor e o Prometheus para nos ajudar nessa tarefa).
O grande problema de usar valores “altos” na configuração das requests é que podemos estar superdimensionando nosso cluster, logo precisaremos de mais nodes e consequentemente teremos um desperdício de recursos, visto que o Kubernetes
está pré-alocando 1 core de CPU, mas na verdade estamos usando apenas 30% disso, por exemplo. Isso pode parecer pouco, mas se pensarmos em um cenário com centenas de containers com esse mesmo perfil de configuração, ao final do mês estaremos rodando alguns nodes sem necessidade.
Definindo limits padrão
Como dito anteriormente, em configurações padrões e se não for informado nada na criação do deployment, o Kubernetes
irá criar o mesmo com recursos “ilimitados”. Para contornar essa situação, o Kubernetes
disponibiliza um recurso que define valores de requests e limits defaults, caso não sejam informados. Isso pode ser feito utilizando o objeto LimitRange
apiVersion: v1
kind: LimitRange
metadata:
name: ns-limits-range
spec:
limits:
- default:
cpu: 500m
memory: 512Mi
defaultRequest:
cpu: 200m
memory: 256Mi
type: Container
nodeSelector
A namespace é um recurso que permite criar clusters “virtuais”. A idéia aqui seria organizar seus objetos (deployments, secrets, configMaps e afins) em ambientes separados como produção e desenvolvimento ou até mesmo por projeto ou times. Mas a namespace em si é só um recurso lógico, ou seja, ela não vai garantir que seus deployments de produção não rodem no mesmo node que os deployments de desenvolvimento. Para resolver isso, umas das alternativas seria usar nodeSelector.
O nodeSelector
é um recurso que permite especificar em qual node seu container será executado. Vejamos um exemplo disso na declaração de um pod:
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
env: test
spec:
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent
nodeSelector:
elo7-environment: dev
Essa declaração irá criar um pod usando a imagem do nginx e criará o mesmo em qualquer node que tenha a label elo-environment=dev
. Quando estamos adicionando um novo node no nosso cluster, temos a possibilidade de passar uma flag para o kubelet que irá informar as labels que aquele node terá. No exemplo acima, ao criar o node passariamos a seguinte flag:
--node-labels=elo7-environment=dev
Qual é a melhor forma de autorizar minhas aplicações rodando no k8s para usar recursos na AWS?
No nosso caso, nosso Cloud Provider é a AWS. Existem uma série de serviços externos que talvez nossa aplicação precise acessar (como S3, SQS e afins). Para consumir tais serviços precisamos ser autorizados, e esse mecanismo de autorização pode ser feito usando AWS Access e Secret Keys ou roles. Por questões de segurança e boas práticas não é recomendado usar Access Keys para aplicações na AWS. Ao invés disso, é recomendado usar IAM roles. Pensando nisso, foi criado o plugin kube2iam. Esse plugin permite que você adicione no campo de annotations
qual role estará vinculada com sua app, permitindo que a mesma acesse recursos da AWS. Lembrando que esse plugin é implementado no Kubernetes
como um DaemonSet. Para maiores informações de como configurá-lo, recomendo a leitura da configuração disponível no Readme do projeto no GitHub. Abaixo temos um exemplo simples de como usar esse recurso:
apiVersion: v1
kind: Pod
metadata:
name: aws-cli
labels:
name: aws-cli
annotations:
iam.amazonaws.com/role: role-arn
spec:
containers:
- image: fstab/aws-cli
command:
- "/usr/local/bin/aws"
- "s3"
- "ls"
- "some-bucket"
name: aws-cli
Conclusão
Nesse post elencamos alguns dos muitos tópicos que precisam ser tratados com a implementação do Kubernetes
em produção. E você? Como está lidando com os desafios e dilemas na implementação em sua empresa? Aproveite o espaço no campo de comentários para deixar seu ponto de vista ou compartilhar como você lidou com esses desafios. Um abraço e até a próxima!