Ir para o conteúdo
Ferramenta para Plataforma1727 palavras

Kubernetes para DigitalOcean: Guia Completo de Deploy

Introdução

Kubernetes se consolidou como o padrão da indústria para orquestração de containers. Mas quem já tentou configurar um cluster do zero em bare metal sabe que a complexidade operacional é enorme: certificados TLS, etcd, networking, upgrades de control plane. É o tipo de trabalho que consome semanas e não entrega valor direto ao produto.

A DigitalOcean resolveu esse problema com o DOKS (DigitalOcean Kubernetes Service), um Kubernetes gerenciado que elimina a gestão do control plane e cobra zero pelo master node. Você paga apenas pelos worker nodes, load balancers e volumes que consumir. Para startups, equipes enxutas e projetos que precisam de infraestrutura robusta sem o custo operacional de um time de SRE dedicado, essa combinação é extremamente competitiva.

Neste guia, vamos cobrir desde a criação do cluster até o deploy de uma aplicação real com TypeScript, passando por configurações de rede, storage e boas práticas que aprendi operando clusters DOKS em produção nos últimos anos.

Se você já tem familiaridade com Docker e conceitos básicos de Kubernetes, este conteúdo vai te levar ao próximo nível. Para uma visão mais ampla sobre o ecossistema DevOps, recomendo o post DevOps para Devs: Docker, CI/CD, Kubernetes e AWS em Produção.

Por que usar Kubernetes na DigitalOcean

Custo previsível e transparente

Diferente da AWS (EKS cobra USD 0,10 por hora só pelo control plane), o DOKS oferece o control plane gratuitamente. Um cluster com três nodes de 4 GB de RAM custa aproximadamente USD 36 por mês. Você consegue rodar aplicações em produção com alta disponibilidade por um valor que cabe no orçamento de um projeto solo ou de uma startup early stage.

Integração nativa com o ecossistema DigitalOcean

O DOKS se integra nativamente com outros serviços da plataforma: DigitalOcean Container Registry (DOCR) para imagens privadas, Managed Databases para PostgreSQL e MySQL, Spaces para armazenamento de objetos, e Load Balancers que são provisionados automaticamente via Kubernetes Services do tipo LoadBalancer. Tudo dentro da mesma VPC, com latência mínima.

Casos de uso onde essa combinação brilha

Na prática, vi o DOKS entregar excelentes resultados em cenários específicos: aplicações SaaS multi-tenant que precisam escalar horizontalmente, APIs de alta disponibilidade para fintechs em estágio inicial, pipelines de processamento de dados com workers que escalam sob demanda, e ambientes de staging que replicam produção com custo reduzido.

Setup do cluster DOKS com doctl

Instalação e autenticação

Primeiro, instale a CLI da DigitalOcean e autentique:

Text
# Instalar doctl (macOS)
brew install doctl
 
# Autenticar com token da API
doctl auth init
 
# Verificar conexão
doctl account get

Criação do cluster Kubernetes

Vamos criar um cluster com pool de nodes otimizado para workloads de API:

Text
# Listar versões disponíveis do Kubernetes
doctl kubernetes options versions
 
# Criar cluster com 3 nodes
doctl kubernetes cluster create vdc-production \
  --region nyc1 \
  --version 1.31.1-do.3 \
  --node-pool "name=api-pool;size=s-4vcpu-8gb;count=3;auto-scale=true;min-nodes=2;max-nodes=6" \
  --tag env:production
 
# Configurar kubeconfig automaticamente
doctl kubernetes cluster kubeconfig save vdc-production
 
# Verificar nodes
kubectl get nodes -o wide

O auto-scale configurado permite que o cluster cresça de 2 até 6 nodes conforme a demanda. O DOKS cuida do provisionamento e draining automaticamente.

Configuração do Container Registry

Para armazenar suas imagens Docker de forma privada:

Text
# Criar registry
doctl registry create vdc-registry --region nyc1
 
# Integrar registry com o cluster (cria o secret de pull automaticamente)
doctl registry kubernetes-manifest | kubectl apply -f -
 
# Login local para push de imagens
doctl registry login

Exemplo prático: Deploy de API TypeScript com Express

Estrutura do projeto

Vamos criar uma API de healthcheck e métricas que serve como base para qualquer microserviço em produção. O código TypeScript a seguir implementa uma API com endpoints de health, readiness e métricas customizadas:

TYPESCRIPT
// src/server.ts
import express, { Request, Response, NextFunction } from "express";
 
interface HealthStatus {
  status: "healthy" | "degraded" | "unhealthy";
  uptime: number;
  timestamp: string;
  version: string;
  checks: Record<string, boolean>;
}
 
const app = express();
const PORT = parseInt(process.env.PORT || "3000", 10);
const VERSION = process.env.APP_VERSION || "1.0.0";
const startTime = Date.now();
 
let isReady = false;
 
// Simula inicialização assíncrona (conexão com DB, warm-up de cache)
setTimeout(() => {
  isReady = true;
  console.log("Application is ready to receive traffic");
}, 5000);
 
// Liveness probe: o processo está rodando?
app.get("/healthz", (_req: Request, res: Response) => {
  const health: HealthStatus = {
    status: "healthy",
    uptime: Math.floor((Date.now() - startTime) / 1000),
    timestamp: new Date().toISOString(),
    version: VERSION,
    checks: {
      server: true,
      memory: process.memoryUsage().heapUsed < 500 * 1024 * 1024,
    },
  };
 
  const isHealthy = Object.values(health.checks).every(Boolean);
  health.status = isHealthy ? "healthy" : "degraded";
 
  res.status(isHealthy ? 200 : 503).json(health);
});
 
// Readiness probe: o pod está pronto para receber tráfego?
app.get("/readyz", (_req: Request, res: Response) => {
  if (isReady) {
    res.status(200).json({ ready: true });
  } else {
    res.status(503).json({ ready: false, reason: "warming up" });
  }
});
 
// Endpoint de métricas para monitoramento
app.get("/metrics", (_req: Request, res: Response) => {
  const memUsage = process.memoryUsage();
  res.json({
    memory: {
      heapUsedMB: Math.round(memUsage.heapUsed / 1024 / 1024),
      heapTotalMB: Math.round(memUsage.heapTotal / 1024 / 1024),
      rssMB: Math.round(memUsage.rss / 1024 / 1024),
    },
    uptime: Math.floor((Date.now() - startTime) / 1000),
    pid: process.pid,
    nodeVersion: process.version,
  });
});
 
// Rota principal da API
app.get("/api/v1/status", (_req: Request, res: Response) => {
  res.json({
    service: "vdc-api",
    version: VERSION,
    environment: process.env.NODE_ENV || "development",
  });
});
 
// Middleware de erro global
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
  console.error(`Unhandled error: ${err.message}`);
  res.status(500).json({ error: "Internal Server Error" });
});
 
app.listen(PORT, "0.0.0.0", () => {
  console.log(`Server running on port ${PORT} (version ${VERSION})`);
});

Dockerfile multi-stage

Text
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json tsconfig.json ./
RUN npm ci
COPY src ./src
RUN npm run build
 
FROM node:20-alpine AS production
RUN addgroup -g 1001 appgroup && adduser -u 1001 -G appgroup -D appuser
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=builder /app/dist ./dist
USER appuser
EXPOSE 3000
CMD ["node", "dist/server.js"]

Manifesto Kubernetes completo

Crie o arquivo k8s/deployment.yaml:

Text
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vdc-api
  labels:
    app: vdc-api
    version: v1
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1
  selector:
    matchLabels:
      app: vdc-api
  template:
    metadata:
      labels:
        app: vdc-api
        version: v1
    spec:
      containers:
        - name: vdc-api
          image: registry.digitalocean.com/vdc-registry/vdc-api:1.0.0
          ports:
            - containerPort: 3000
          env:
            - name: NODE_ENV
              value: "production"
            - name: APP_VERSION
              value: "1.0.0"
          resources:
            requests:
              cpu: "100m"
              memory: "128Mi"
            limits:
              cpu: "500m"
              memory: "256Mi"
          livenessProbe:
            httpGet:
              path: /healthz
              port: 3000
            initialDelaySeconds: 10
            periodSeconds: 15
            failureThreshold: 3
          readinessProbe:
            httpGet:
              path: /readyz
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 5
            failureThreshold: 3
---
apiVersion: v1
kind: Service
metadata:
  name: vdc-api-service
  annotations:
    service.beta.kubernetes.io/do-loadbalancer-size-slug: "lb-small"
    service.beta.kubernetes.io/do-loadbalancer-protocol: "http"
    service.beta.kubernetes.io/do-loadbalancer-healthcheck-path: "/healthz"
spec:
  type: LoadBalancer
  selector:
    app: vdc-api
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3000

Build, push e deploy

Text
# Build da imagem
docker build -t registry.digitalocean.com/vdc-registry/vdc-api:1.0.0 .
 
# Push para o DOCR
docker push registry.digitalocean.com/vdc-registry/vdc-api:1.0.0
 
# Deploy no cluster
kubectl apply -f k8s/deployment.yaml
 
# Acompanhar rollout
kubectl rollout status deployment/vdc-api
 
# Verificar pods e serviço
kubectl get pods -l app=vdc-api
kubectl get svc vdc-api-service

O Load Balancer da DigitalOcean será provisionado automaticamente. Em aproximadamente dois minutos, você terá um IP externo acessível.

Exemplo prático: Script de automação para gestão do cluster

O segundo exemplo demonstra como usar a API da DigitalOcean via TypeScript para automatizar operações de infraestrutura:

TYPESCRIPT
// src/infra/cluster-manager.ts
import { createApiClient } from "dots-wrapper";
 
interface ClusterInfo {
  id: string;
  name: string;
  region: string;
  nodeCount: number;
  status: string;
  version: string;
}
 
interface ScaleResult {
  success: boolean;
  previousCount: number;
  newCount: number;
  message: string;
}
 
async function getClusterStatus(
  client: ReturnType<typeof createApiClient>,
  clusterId: string
): Promise<ClusterInfo> {
  const {
    data: { kubernetes_cluster: cluster },
  } = await client.kubernetes.getKubernetesCluster({ kubernetes_cluster_id: clusterId });
 
  const totalNodes = cluster.node_pools.reduce(
    (sum: number, pool: { count: number }) => sum + pool.count,
    0
  );
 
  return {
    id: cluster.id,
    name: cluster.name,
    region: cluster.region,
    nodeCount: totalNodes,
    status: cluster.status.state,
    version: cluster.version,
  };
}
 
async function scaleNodePool(
  client: ReturnType<typeof createApiClient>,
  clusterId: string,
  nodePoolId: string,
  desiredCount: number
): Promise<ScaleResult> {
  const {
    data: { node_pool: currentPool },
  } = await client.kubernetes.getNodePool({
    kubernetes_cluster_id: clusterId,
    node_pool_id: nodePoolId,
  });
 
  const previousCount = currentPool.count;
 
  if (desiredCount === previousCount) {
    return {
      success: true,
      previousCount,
      newCount: desiredCount,
      message: "Node count already matches desired state",
    };
  }
 
  await client.kubernetes.updateNodePool({
    kubernetes_cluster_id: clusterId,
    node_pool_id: nodePoolId,
    name: currentPool.name,
    count: desiredCount,
    tags: currentPool.tags,
  });
 
  return {
    success: true,
    previousCount,
    newCount: desiredCount,
    message: `Scaled from ${previousCount} to ${desiredCount} nodes`,
  };
}
 
async function main(): Promise<void> {
  const token = process.env.DO_API_TOKEN;
  if (!token) {
    throw new Error("DO_API_TOKEN environment variable is required");
  }
 
  const client = createApiClient({ token });
  const clusterId = process.env.CLUSTER_ID || "";
 
  const status = await getClusterStatus(client, clusterId);
  console.log("Cluster status:", JSON.stringify(status, null, 2));
 
  // Escalar baseado em horário (exemplo: reduz nodes fora do horário comercial)
  const hour = new Date().getHours();
  const isBusinessHours = hour >= 8 && hour < 20;
  const targetNodes = isBusinessHours ? 4 : 2;
 
  console.log(
    `Current hour: ${hour}. Business hours: ${isBusinessHours}. Target: ${targetNodes} nodes`
  );
}
 
main().catch(console.error);

Esse tipo de automação é útil para reduzir custos em ambientes de staging ou desenvolvimento, escalando para baixo fora do horário comercial e voltando ao tamanho adequado durante o dia.

Boas práticas para DOKS em produção

Resource requests e limits são obrigatórios

Nunca faça deploy sem definir requests e limits. Sem eles, o scheduler do Kubernetes não consegue tomar decisões inteligentes de alocação, e um único pod pode consumir todos os recursos de um node, derrubando outros workloads.

Use Pod Disruption Budgets

Para garantir disponibilidade durante upgrades de nodes ou manutenções:

Text
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: vdc-api-pdb
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: vdc-api

Configure o Cluster Autoscaler corretamente

O DOKS suporta Cluster Autoscaler por node pool. No exemplo deste guia, o api-pool já foi criado com autoscaling habilitado, mínimo de dois e máximo de seis nodes. O autoscaler adiciona capacidade quando há pods que não conseguem ser agendados e remove nodes quando consegue realocar os workloads, respeitando as restrições de agendamento e o PodDisruptionBudget. Ajuste os limites à capacidade necessária e ao orçamento, considerando também as quotas da conta.

Esse mecanismo ajusta os nodes. Para variar automaticamente as réplicas da API, configure um Horizontal Pod Autoscaler com as métricas adequadas. Mantenha os requests coerentes com a aplicação e acompanhe pods pendentes e eventos do cluster: habilitar autoscaling não garante que qualquer pod terá capacidade disponível imediatamente.

Guias Relacionados

Explore mais guias técnicos

Mais de 100 guias práticos de integração entre frameworks, ferramentas e plataformas.

Ver todos os guias