Nesta aula, você vai aprender boas práticas fundamentais para operar aplicações em Kubernetes de forma confiável e segura. Essas práticas são amplamente adotadas em ambientes de produção e ajudam a evitar problemas comuns como indisponibilidade, estouro de memória e vulnerabilidades de segurança. Vamos explorar quatro pilares: health checks, limites de recursos, namespaces e segurança básica. Cada um desses tópicos é crítico para garantir que seus workloads sejam resilientes, eficientes e protegidos.

Antes de mergulharmos nos detalhes, é importante entender que Kubernetes é uma plataforma complexa, e essas boas práticas não são opcionais — elas são pré-requisitos para operar com confiança. Ao final desta aula, você terá um conhecimento prático e profundo para aplicar em seus próprios clusters.

Health checks (liveness/readiness)

Health checks são mecanismos que o Kubernetes usa para monitorar a saúde dos seus containers e tomar ações automáticas. Existem dois tipos principais: livenessProbe e readinessProbe. O livenessProbe determina se o container está vivo ou se precisa ser reiniciado. Se falhar, o kubelet mata o container e o reinicia de acordo com a política de restart. Já o readinessProbe determina se o pod está pronto para receber tráfego. Se falhar, o pod é removido dos endpoints do Service, mas não é reiniciado.

É essencial configurar ambos os probes para que o Kubernetes possa gerenciar seus aplicativos de forma inteligente. Sem eles, o Kubernetes não sabe se seu aplicativo está realmente saudável e pode enviar tráfego para pods que ainda não estão prontos, ou não reiniciar containers que estão travados. Vamos ver um exemplo de configuração:

apiVersion: v1
kind: Pod
metadata:
  name: meu-app
spec:
  containers:
  - name: app
    image: nginx:latest
    livenessProbe:
      httpGet:
        path: /healthz
        port: 80
      initialDelaySeconds: 3
      periodSeconds: 3
    readinessProbe:
      httpGet:
        path: /ready
        port: 80
      initialDelaySeconds: 1
      periodSeconds: 5

Neste exemplo, usamos httpGet para verificar endpoints HTTP. Você pode usar também exec para executar comandos dentro do container ou tcpSocket para testar conexões TCP. É importante definir initialDelaySeconds para dar tempo ao aplicativo de iniciar, e periodSeconds para controlar a frequência das verificações. Outros parâmetros como timeoutSeconds e failureThreshold também são úteis para ajustar o comportamento.

Uma boa prática é criar endpoints dedicados para health checks no seu aplicativo, como /healthz e /ready, que não exponham informações sensíveis e sejam leves em termos de processamento. Lembre-se de que o livenessProbe deve ser mais permissivo para evitar reinicializações desnecessárias, enquanto o readinessProbe pode ser mais rigoroso para garantir que apenas pods prontos recebam tráfego.

Limites de recursos

Definir limites de recursos (CPU e memória) é crucial para garantir que seus pods não consumam recursos demais do cluster, causando problemas de performance e estabilidade. O Kubernetes permite especificar requests e limits. Os requests são a quantidade mínima de recursos que o pod precisa, e o scheduler usa isso para decidir em qual nó alocar o pod. Os limits são o máximo que o pod pode consumir; se exceder, o container pode ser morto (no caso de memória) ou throttled (no caso de CPU).

Sem limites, um pod com um vazamento de memória pode derrubar o nó inteiro, afetando outros workloads. Portanto, sempre defina limits e requests para evitar o efeito "noisy neighbor". Vamos ver um exemplo:

apiVersion: v1
kind: Pod
metadata:
  name: meu-app
spec:
  containers:
  - name: app
    image: nginx
    resources:
      requests:
        memory: "64Mi"
        cpu: "250m"
      limits:
        memory: "128Mi"
        cpu: "500m"

Neste exemplo, o pod pede 64 MiB de memória e 250 mili-CPUs, mas pode usar até 128 MiB e 500m de CPU. É importante ajustar esses valores com base no perfil do seu aplicativo. Use ferramentas de monitoramento para entender o consumo real e definir valores adequados. Além disso, você pode configurar LimitRange e ResourceQuota em nível de namespace para impor limites padrão e totais.

Uma boa prática é começar com requests conservadores e limites generosos, mas sempre monitorar o uso para ajustar. Lembre-se de que limites de CPU são compressíveis (o pod é throttled, mas não morto), enquanto limites de memória são incompressíveis (o pod pode ser morto se exceder). Portanto, evite definir limites de memória muito baixos para aplicações que precisam de picos de memória.

Namespaces

Namespaces são uma forma de dividir o cluster em ambientes virtuais isolados. Eles permitem que você organize recursos, aplique políticas de segurança e gerencie cotas de recursos de forma separada. Por exemplo, você pode ter namespaces para desenvolvimento, staging e produção, ou para diferentes equipes que compartilham o mesmo cluster. Isso evita conflitos de nomes e facilita a administração.

Sem namespaces, todos os recursos ficam em um único espaço, o que pode levar a confusão e riscos de segurança. Com namespaces, você pode controlar quem tem acesso a quais recursos usando RBAC (Role-Based Access Control). Vamos ver como criar e usar namespaces:

# Criar um namespace
kubectl create namespace meu-namespace

# Listar namespaces
kubectl get namespaces

# Executar comandos em um namespace específico
kubectl get pods -n meu-namespace

# Definir o namespace padrão no contexto atual
kubectl config set-context --current --namespace=meu-namespace

Você também pode definir namespaces nos manifestos YAML, usando o campo metadata.namespace. Por exemplo:

apiVersion: v1
kind: Pod
metadata:
  name: meu-pod
  namespace: meu-namespace
spec:
  containers:
  - name: app
    image: nginx

Uma boa prática é usar namespaces para separar ambientes e equipes, e aplicar ResourceQuota e LimitRange em cada namespace para controlar o consumo de recursos. Além disso, use NetworkPolicy para isolar o tráfego de rede entre namespaces, aumentando a segurança.

Segurança básica

Segurança em Kubernetes é um tópico vasto, mas existem boas práticas básicas que você deve aplicar imediatamente. Primeiro, execute containers como usuário não-root. Por padrão, muitos containers rodam como root, o que é perigoso se o container for comprometido. Use o campo securityContext para definir o usuário e grupo. Segundo, evite privilégios desnecessários: defina allowPrivilegeEscalation: false e use capabilities para remover capacidades não necessárias.

Além disso, use imagens de containers confiáveis e verifique se não há vulnerabilidades conhecidas. Ferramentas como Trivy ou Anchore podem escanear imagens. Também é importante usar segredos para informações sensíveis, como senhas e chaves de API, em vez de colocá-los em variáveis de ambiente ou no código. O Kubernetes oferece o recurso Secret para isso.

Vamos ver um exemplo de securityContext:

apiVersion: v1
kind: Pod
metadata:
  name: meu-app
spec:
  containers:
  - name: app
    image: nginx
    securityContext:
      runAsUser: 1000
      runAsGroup: 3000
      allowPrivilegeEscalation: false
      capabilities:
        drop: ["ALL"]

Neste exemplo, o container roda como usuário 1000, sem escalonamento de privilégios e sem capacidades. Isso reduz a superfície de ataque. Outra boa prática é usar readOnlyRootFilesystem: true para tornar o sistema de arquivos raiz somente leitura, forçando o aplicativo a escrever em volumes montados. Isso dificulta a persistência de malware.

Por fim, considere usar políticas de rede (NetworkPolicy) para controlar o tráfego entre pods, e use RBAC para limitar o acesso à API do Kubernetes. Essas são camadas essenciais de defesa em profundidade.

Boas práticas adicionais e observações finais

Além dos tópicos principais, algumas práticas adicionais podem melhorar a robustez do seu cluster. Sempre use replicas em Deployments para garantir alta disponibilidade. Utilize PodDisruptionBudget para evitar que muitos pods sejam interrompidos simultaneamente durante manutenções. Configure HorizontalPodAutoscaler para escalar automaticamente com base em métricas. E monitore seu cluster com ferramentas como Prometheus e Grafana.

Lembre-se de que Kubernetes é dinâmico e essas práticas evoluem. Mantenha-se atualizado com a documentação oficial e participe da comunidade. Aplicar essas boas práticas desde o início evitará dores de cabeça futuras e garantirá que suas aplicações sejam confiáveis e seguras.

Exercícios

  1. Escreva um manifesto YAML para um Deployment com um container nginx, incluindo livenessProbe e readinessProbe usando endpoints HTTP em /healthz e /ready, respectivamente. Use initialDelaySeconds de 5 para liveness e 3 para readiness, e periodSeconds de 10 para ambos.
  2. No manifesto do exercício 1, adicione limites de recursos: requests de 64Mi de memória e 250m de CPU, limits de 128Mi e 500m. Explique por que é importante definir limites de memória.
  3. Crie um namespace chamado 'dev' e um Pod nesse namespace usando kubectl e um arquivo YAML. Liste os pods do namespace 'dev' e mude o contexto para usar 'dev' como namespace padrão.
  4. Escreva um securityContext para um container que rode como usuário 1000, sem escalonamento de privilégios, com readOnlyRootFilesystem ativado e drop de todas as capacidades. Justifique cada configuração.
  5. Pesquise sobre NetworkPolicy e escreva um exemplo de política que permita tráfego de entrada apenas de pods com label 'app: frontend' para pods com label 'app: backend' na porta 8080. Explique como isso melhora a segurança.

✓ Resposta:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: meu-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: meu-app
  template:
    metadata:
      labels:
        app: meu-app
    spec:
      containers:
      - name: app
        image: nginx
        livenessProbe:
          httpGet:
            path: /healthz
            port: 80
          initialDelaySeconds: 5
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 80
          initialDelaySeconds: 3
          periodSeconds: 10

✓ Resposta:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: meu-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: meu-app
  template:
    metadata:
      labels:
        app: meu-app
    spec:
      containers:
      - name: app
        image: nginx
        resources:
          requests:
            memory: "64Mi"
            cpu: "250m"
          limits:
            memory: "128Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /healthz
            port: 80
          initialDelaySeconds: 5
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 80
          initialDelaySeconds: 3
          periodSeconds: 10

É importante definir limites de memória porque, se um pod exceder o limite de memória, o kernel do nó pode matar o processo (OOMKill), causando indisponibilidade. Limites previnem que um único pod consuma toda a memória do nó, afetando outros workloads.

✓ Resposta:
# Criar namespace
kubectl create namespace dev

# Criar pod no namespace dev
kubectl run meu-pod --image=nginx --namespace=dev

# Listar pods no namespace dev
kubectl get pods -n dev

# Mudar o namespace padrão do contexto
kubectl config set-context --current --namespace=dev

Ou usando um arquivo YAML:

# pod-dev.yaml
apiVersion: v1
kind: Pod
metadata:
  name: meu-pod
  namespace: dev
spec:
  containers:
  - name: app
    image: nginx

Depois, kubectl apply -f pod-dev.yaml.

✓ Resposta:
securityContext:
  runAsUser: 1000
  runAsGroup: 3000
  allowPrivilegeEscalation: false
  readOnlyRootFilesystem: true
  capabilities:
    drop: ["ALL"]

Justificativa: rodar como usuário não-root reduz o impacto de uma exploração; allowPrivilegeEscalation: false impede que processos ganhem mais privilégios; readOnlyRootFilesystem impede que o container escreva no sistema de arquivos raiz, mitigando persistência de malware; drop: ["ALL"] remove todas as capabilities do Linux, minimizando a superfície de ataque.

✓ Resposta:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-backend
spec:
  podSelector:
    matchLabels:
      app: backend
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend
    ports:
    - protocol: TCP
      port: 8080

Essa política garante que apenas pods com label app: frontend possam acessar pods com label app: backend na porta 8080. Isso limita o tráfego de rede, reduzindo a superfície de ataque e impedindo que pods não autorizados acessem o backend.

Referências