DevOps

Capstone: Implementando os Microsserviços Já leu

16 min de leitura

Capstone: Implementando os Microsserviços
A diferença entre código de demonstração e código de produção não está na lógica de negócio — está nas camadas que envolvem essa lógica. Um endpoint de API que busca um produto no banco de dados é trivial de escrever. O

A diferença entre código de demonstração e código de produção não está na lógica de negócio — está nas camadas que envolvem essa lógica. Um endpoint de API que busca um produto no banco de dados é trivial de escrever. O mesmo endpoint preparado para produção inclui validação de entrada, tratamento de erros estruturado, logging com contexto de rastreamento, métricas expostas para o Prometheus, circuit breaker para o banco de dados, graceful shutdown, healthchecks calibrados e testes que verificam não apenas o caminho feliz mas os cenários de falha.

Este artigo implementa os cinco microsserviços do capstone com todas essas camadas integradas. Cada serviço é apresentado com seu núcleo funcional e as práticas operacionais que o tornam adequado para produção — não como adições posteriores, mas como parte intrínseca do design.


Estrutura Compartilhada: O Pacote Core

Código repetido entre serviços é um passivo. O pacote @loja/core contém as abstrações compartilhadas — logging, métricas, tracing, resiliência e configuração — que todos os serviços consomem:

// packages/core/src/index.js
module.exports = {
  ...require('./logger'),
  ...require('./metrics'),
  ...require('./tracing'),
  ...require('./resilience'),
  ...require('./config'),
  ...require('./health'),
};
// packages/core/src/logger.js
const pino = require('pino');

function criarLogger(contexto = {}) {
  return pino({
    level: process.env.LOG_LEVEL || 'info',
    formatters: {
      level: (label) => ({ level: label }),
    },
    base: {
      servico: process.env.SERVICE_NAME,
      versao:  process.env.APP_VERSION || 'desconhecida',
      ambiente: process.env.NODE_ENV,
      ...contexto,
    },
    timestamp: pino.stdTimeFunctions.isoTime,
    redact: {
      paths: [
        'password', 'senha', 'token', 'secret',
        'authorization', 'cookie', 'cartao',
        '*.password', '*.senha', '*.token',
      ],
      censor: '[REDACTED]',
    },
  });
}

module.exports = { criarLogger };
// packages/core/src/metrics.js
const client = require('prom-client');

// Registra métricas padrão do Node.js (CPU, memória, event loop)
client.collectDefaultMetrics({
  prefix: `${process.env.SERVICE_NAME?.replace(/-/g, '_')}_`,
});

// Métricas HTTP padrão para todos os serviços
const httpRequestDuration = new client.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duração das requisições HTTP em segundos',
  labelNames: ['method', 'route', 'status_code', 'service'],
  buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
});

const httpRequestsTotal = new client.Counter({
  name: 'http_requests_total',
  help: 'Total de requisições HTTP',
  labelNames: ['method', 'route', 'status_code', 'service'],
});

function middlewareMetricas(req, res, next) {
  const inicio = Date.now();

  res.on('finish', () => {
    const duracao = (Date.now() - inicio) / 1000;
    const labels = {
      method: req.method,
      route: req.route?.path || req.path,
      status_code: res.statusCode,
      service: process.env.SERVICE_NAME,
    };

    httpRequestDuration.observe(labels, duracao);
    httpRequestsTotal.inc(labels);
  });

  next();
}

async function handleMetricas(req, res) {
  res.set('Content-Type', client.register.contentType);
  res.end(await client.register.metrics());
}

module.exports = { middlewareMetricas, handleMetricas, client };

Serviço de Catálogo

O serviço de catálogo tem as maiores demandas de leitura do sistema. O design prioriza caching agressivo com Redis e uma Read Replica do PostgreSQL para escalar leituras horizontalmente:

// services/catalog-service/src/server.js
const express = require('express');
const { criarLogger, middlewareMetricas, handleMetricas } = require('@loja/core');
const { configurarTracing } = require('./tracing');
const produtosRouter = require('./routes/produtos.routes');
const healthRouter = require('./routes/health.routes');
const { inicializarConexoes, encerrarConexoes } = require('./db/conexoes');

configurarTracing();

const app = express();
const logger = criarLogger({ servico: 'catalog-service' });

app.use(express.json({ limit: '1mb' }));
app.use(middlewareMetricas);

// Middleware de contexto de rastreamento
app.use((req, res, next) => {
  req.logger = logger.child({
    traceId: req.headers['x-trace-id'] || crypto.randomUUID(),
    method:  req.method,
    path:    req.path,
  });
  next();
});

app.use('/health', healthRouter);
app.use('/metrics', handleMetricas);
app.use('/produtos', produtosRouter);

// Handler de erros global
app.use((err, req, res, next) => {
  const status = err.status || 500;

  req.logger.error({
    err: {
      message: err.message,
      stack: process.env.NODE_ENV !== 'production' ? err.stack : undefined,
    },
    status,
  }, 'Erro não tratado');

  res.status(status).json({
    erro: status < 500 ? err.message : 'Erro interno do servidor',
    codigo: err.codigo || 'ERRO_INTERNO',
    traceId: req.logger.bindings().traceId,
  });
});

let servidor;

async function iniciar() {
  await inicializarConexoes();

  servidor = app.listen(process.env.PORT || 3001, () => {
    logger.info({ porta: process.env.PORT || 3001 }, 'Catalog service iniciado');
  });
}

async function encerrar(sinal) {
  logger.info({ sinal }, 'Encerrando catalog service...');

  servidor.close(async () => {
    await encerrarConexoes();
    logger.info('Encerramento concluído');
    process.exit(0);
  });

  setTimeout(() => {
    logger.error('Timeout de encerramento — forçando saída');
    process.exit(1);
  }, 30000);
}

process.on('SIGTERM', () => encerrar('SIGTERM'));
process.on('SIGINT',  () => encerrar('SIGINT'));

iniciar().catch(err => {
  logger.fatal({ err }, 'Falha na inicialização');
  process.exit(1);
});
// services/catalog-service/src/routes/produtos.routes.js
const router = require('express').Router();
const { param, query, validationResult } = require('express-validator');
const ProdutoService = require('../services/produto.service');

const validarUUID = (campo) =>
  param(campo).isUUID(4).withMessage(`${campo} deve ser um UUID válido`);

function checarValidacao(req, res, next) {
  const erros = validationResult(req);
  if (!erros.isEmpty()) {
    return res.status(400).json({
      erro: 'Dados inválidos',
      detalhes: erros.array(),
    });
  }
  next();
}

// GET /produtos — lista com paginação e filtros
router.get('/',
  query('pagina').optional().isInt({ min: 1 }).toInt(),
  query('limite').optional().isInt({ min: 1, max: 100 }).toInt(),
  query('categoria_id').optional().isUUID(4),
  query('busca').optional().isString().trim().isLength({ max: 100 }),
  checarValidacao,
  async (req, res, next) => {
    try {
      const resultado = await ProdutoService.listar({
        pagina:      req.query.pagina || 1,
        limite:      req.query.limite || 20,
        categoriaId: req.query.categoria_id,
        busca:       req.query.busca,
      }, req.logger);

      res.json(resultado);
    } catch (err) {
      next(err);
    }
  }
);

// GET /produtos/:id
router.get('/:id',
  validarUUID('id'),
  checarValidacao,
  async (req, res, next) => {
    try {
      const produto = await ProdutoService.buscarPorId(
        req.params.id,
        req.logger
      );

      if (!produto) {
        return res.status(404).json({
          erro: 'Produto não encontrado',
          codigo: 'PRODUTO_NAO_ENCONTRADO',
        });
      }

      res.json(produto);
    } catch (err) {
      next(err);
    }
  }
);

// GET /produtos/:id/estoque — endpoint crítico para o order-service
router.get('/:id/estoque',
  validarUUID('id'),
  query('quantidade').isInt({ min: 1 }).toInt(),
  checarValidacao,
  async (req, res, next) => {
    try {
      const estoque = await ProdutoService.verificarEstoque(
        req.params.id,
        req.query.quantidade,
        req.logger
      );
      res.json(estoque);
    } catch (err) {
      next(err);
    }
  }
);

module.exports = router;
// services/catalog-service/src/services/produto.service.js
const { CircuitBreaker } = require('@loja/core');
const cache = require('../cache/cache.service');
const ProdutoRepository = require('../repositories/produto.repository');
const { Histogram } = require('prom-client');

const duracaoCacheOp = new Histogram({
  name: 'catalog_cache_operation_seconds',
  help: 'Duração das operações de cache no catalog-service',
  labelNames: ['operacao', 'hit'],
  buckets: [0.001, 0.005, 0.01, 0.025, 0.05],
});

const dbCircuitBreaker = new CircuitBreaker({
  nome: 'catalog-postgres',
  limiarFalhas: 5,
  timeoutAbertura: 30000,
  timeoutRequisicao: 3000,
});

class ProdutoService {
  async buscarPorId(id, logger) {
    const chaveCache = `produto:${id}`;
    const timer = duracaoCacheOp.startTimer({ operacao: 'get' });

    // Tenta o cache primeiro
    const cached = await cache.get(chaveCache);
    if (cached) {
      timer({ hit: 'true' });
      logger.debug({ id, fonte: 'cache' }, 'Produto encontrado no cache');
      return cached;
    }
    timer({ hit: 'false' });

    // Cache miss — busca no banco com circuit breaker
    const produto = await dbCircuitBreaker.executar(
      () => ProdutoRepository.buscarPorId(id),
      // Fallback: retorna null em vez de propagar o erro
      () => null
    );

    if (produto) {
      // Armazena no cache com TTL de 10 minutos
      await cache.set(chaveCache, produto, 600);
      logger.debug({ id, fonte: 'banco' }, 'Produto buscado do banco');
    }

    return produto;
  }

  async listar(filtros, logger) {
    const chaveCache = `produtos:lista:${JSON.stringify(filtros)}`;

    return cache.getOuBuscar(
      chaveCache,
      () => dbCircuitBreaker.executar(
        () => ProdutoRepository.listar(filtros)
      ),
      120 // TTL de 2 minutos para listas
    );
  }

  async verificarEstoque(produtoId, quantidade, logger) {
    // Estoque não é cacheado — precisa ser sempre atual
    const estoque = await dbCircuitBreaker.executar(
      () => ProdutoRepository.buscarEstoque(produtoId)
    );

    if (!estoque) {
      return { disponivel: false, quantidade_disponivel: 0 };
    }

    return {
      disponivel: estoque.quantidade >= quantidade,
      quantidade_disponivel: estoque.quantidade,
    };
  }
}

module.exports = new ProdutoService();

Serviço de Pedidos

O serviço de pedidos é o mais crítico do sistema — orquestra o checkout coordenando chamadas ao serviço de catálogo, ao gateway de pagamento e às filas SQS:

// services/order-service/src/services/checkout.service.js
const { ClienteHTTPResilient, criarLogger } = require('@loja/core');
const PedidoRepository = require('../repositories/pedido.repository');
const AuditoriaService = require('../auditoria/auditoria.service');
const SQSPublisher = require('../queue/sqs.publisher');

const logger = criarLogger({ servico: 'order-service' });

const clienteCatalogo = new ClienteHTTPResilient(
  process.env.CATALOG_SERVICE_URL,
  { nome: 'catalog-service', limiarFalhas: 5 }
);

const clientePagamento = new ClienteHTTPResilient(
  process.env.PAYMENT_GATEWAY_URL,
  {
    nome: 'payment-gateway',
    limiarFalhas: 3,
    timeoutRequisicao: 10000, // Pagamento pode ser lento
  }
);

class CheckoutService {
  async iniciarCheckout(usuarioId, itens, enderecoEntrega, traceId) {
    const logCtx = logger.child({ traceId, usuarioId });

    logCtx.info({ qtdItens: itens.length }, 'Iniciando checkout');

    // 1. Valida disponibilidade de todos os itens em paralelo
    const verificacoes = await Promise.allSettled(
      itens.map(item =>
        clienteCatalogo.get(
          `/produtos/${item.produto_id}/estoque?quantidade=${item.quantidade}`
        )
      )
    );

    const itensSemEstoque = [];
    for (let i = 0; i < verificacoes.length; i++) {
      const { status, value, reason } = verificacoes[i];

      if (status === 'rejected') {
        logCtx.warn(
          { produtoId: itens[i].produto_id, erro: reason?.message },
          'Falha ao verificar estoque — assumindo disponível'
        );
        // Falha na verificação não bloqueia o checkout
        // O sistema de estoque tem sua própria consistência
        continue;
      }

      if (!value.disponivel) {
        itensSemEstoque.push({
          produto_id: itens[i].produto_id,
          solicitado: itens[i].quantidade,
          disponivel: value.quantidade_disponivel,
        });
      }
    }

    if (itensSemEstoque.length > 0) {
      logCtx.info({ itensSemEstoque }, 'Itens sem estoque suficiente');
      return {
        sucesso: false,
        erro: 'ESTOQUE_INSUFICIENTE',
        itens_sem_estoque: itensSemEstoque,
      };
    }

    // 2. Busca os dados dos produtos para o snapshot do pedido
    const dadosProdutos = await Promise.all(
      itens.map(item =>
        clienteCatalogo.get(`/produtos/${item.produto_id}`)
      )
    );

    // 3. Calcula o total do pedido
    const itensProcessados = itens.map((item, i) => ({
      produto_id:     item.produto_id,
      nome_produto:   dadosProdutos[i].nome,
      preco_unitario: dadosProdutos[i].preco_promocional || dadosProdutos[i].preco,
      quantidade:     item.quantidade,
      subtotal:       (dadosProdutos[i].preco_promocional || dadosProdutos[i].preco)
                        * item.quantidade,
    }));

    const subtotal = itensProcessados.reduce((acc, item) => acc + item.subtotal, 0);
    const frete    = this._calcularFrete(enderecoEntrega, subtotal);
    const total    = subtotal + frete;

    // 4. Cria o pedido no banco com status "aguardando_pagamento"
    const pedido = await PedidoRepository.criar({
      usuario_id:       usuarioId,
      status:           'aguardando_pagamento',
      itens:            itensProcessados,
      subtotal,
      frete,
      total,
      endereco_entrega: enderecoEntrega,
      metadata:         { traceId },
    });

    logCtx.info({ pedidoId: pedido.id, total }, 'Pedido criado');

    await AuditoriaService.registrar({
      tipo: 'pedido.criado',
      resultado: 'sucesso',
      ator: { id: usuarioId, tipo: 'usuario' },
      recurso: { tipo: 'pedido', id: pedido.id },
      detalhes: { total, qtdItens: itens.length },
      traceId,
    });

    return { sucesso: true, pedido };
  }

  async processarPagamento(pedidoId, dadosPagamento, traceId) {
    const logCtx = logger.child({ traceId, pedidoId });
    const pedido = await PedidoRepository.buscarPorId(pedidoId);

    if (!pedido || pedido.status !== 'aguardando_pagamento') {
      return { sucesso: false, erro: 'PEDIDO_NAO_ENCONTRADO_OU_STATUS_INVALIDO' };
    }

    logCtx.info({ valor: pedido.total }, 'Processando pagamento');

    let resultadoPagamento;
    try {
      resultadoPagamento = await clientePagamento.post('/cobrar', {
        valor:       pedido.total,
        moeda:       'BRL',
        descricao:   `Pedido #${pedidoId}`,
        referencia:  pedidoId,
        pagamento:   dadosPagamento,
      });
    } catch (erro) {
      logCtx.error({ erro: erro.message }, 'Falha ao processar pagamento');

      await PedidoRepository.atualizarStatus(pedidoId, 'cancelado', {
        motivo_cancelamento: 'falha_pagamento',
        erro_pagamento: erro.message,
      });

      return { sucesso: false, erro: 'FALHA_PAGAMENTO' };
    }

    if (!resultadoPagamento.aprovado) {
      await PedidoRepository.atualizarStatus(pedidoId, 'cancelado', {
        motivo_cancelamento: 'pagamento_recusado',
        codigo_recusa: resultadoPagamento.codigo_recusa,
      });

      return {
        sucesso: false,
        erro: 'PAGAMENTO_RECUSADO',
        codigo_recusa: resultadoPagamento.codigo_recusa,
      };
    }

    // 5. Atualiza o pedido para "pago" e publica evento
    await PedidoRepository.atualizarStatus(pedidoId, 'pago', {
      id_transacao: resultadoPagamento.id_transacao,
      confirmado_em: new Date(),
    });

    // Publica evento de pedido confirmado — consome assincronamente
    // pelos serviços de notificação e catálogo (atualização de estoque)
    await SQSPublisher.publicar('pedido-confirmado', {
      pedidoId,
      usuarioId:   pedido.usuario_id,
      total:       pedido.total,
      itens:       pedido.itens,
      confirmedAt: new Date().toISOString(),
      traceId,
    });

    logCtx.info({ transacaoId: resultadoPagamento.id_transacao }, 'Pagamento aprovado');

    return {
      sucesso: true,
      pedido: await PedidoRepository.buscarPorId(pedidoId),
    };
  }

  _calcularFrete(endereco, subtotal) {
    // Frete grátis acima de R$ 200
    if (subtotal >= 200) return 0;
    // Simplificado — em produção integraria com serviço de frete
    return 15.90;
  }
}

module.exports = new CheckoutService();

Serviço de Notificações

O serviço de notificações é um consumidor SQS puro — sem endpoints HTTP de negócio, apenas processamento de mensagens de fila:

// services/notification-service/src/consumer/sqs.consumer.js
const { SQSClient, ReceiveMessageCommand,
        DeleteMessageCommand, ChangeMessageVisibilityCommand } = require('@aws-sdk/client-sqs');
const { criarLogger } = require('@loja/core');
const EmailService    = require('../services/email.service');
const { Counter, Histogram } = require('prom-client');

const logger = criarLogger({ servico: 'notification-service' });
const sqs    = new SQSClient({ region: process.env.AWS_REGION });

const mensagensProcessadas = new Counter({
  name: 'notifications_messages_processed_total',
  help: 'Total de mensagens SQS processadas',
  labelNames: ['fila', 'resultado'],
});

const duracaoProcessamento = new Histogram({
  name: 'notifications_processing_duration_seconds',
  help: 'Duração do processamento de mensagens',
  labelNames: ['fila', 'tipo'],
  buckets: [0.1, 0.5, 1, 2, 5, 10],
});

const FILAS = {
  'pedido-confirmado': {
    url:     process.env.SQS_URL_PEDIDO_CONFIRMADO,
    handler: processarPedidoConfirmado,
  },
  'pedido-cancelado': {
    url:     process.env.SQS_URL_PEDIDO_CANCELADO,
    handler: processarPedidoCancelado,
  },
};

async function processarPedidoConfirmado(corpo, msgLogger) {
  const { pedidoId, usuarioId, total, traceId } = corpo;

  msgLogger.info({ pedidoId, total }, 'Enviando confirmação de pedido');

  // Busca email do usuário via user-service
  const usuario = await fetch(
    `${process.env.USER_SERVICE_URL}/usuarios/${usuarioId}`,
    { headers: { 'x-trace-id': traceId } }
  ).then(r => r.json());

  await EmailService.enviar({
    para:    usuario.email,
    assunto: `Pedido #${pedidoId} confirmado!`,
    template: 'pedido-confirmado',
    dados: {
      nome:     usuario.nome,
      pedidoId,
      total:    total.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' }),
      linkAcompanhamento: `${process.env.APP_URL}/pedidos/${pedidoId}`,
    },
  });

  msgLogger.info({ pedidoId, email: usuario.email }, 'Email de confirmação enviado');
}

async function processarPedidoCancelado(corpo, msgLogger) {
  const { pedidoId, usuarioId, motivoCancelamento } = corpo;

  const usuario = await fetch(
    `${process.env.USER_SERVICE_URL}/usuarios/${usuarioId}`
  ).then(r => r.json());

  await EmailService.enviar({
    para:    usuario.email,
    assunto: `Pedido #${pedidoId} cancelado`,
    template: 'pedido-cancelado',
    dados: {
      nome:    usuario.nome,
      pedidoId,
      motivo:  motivoCancelamento,
    },
  });
}

// Loop de polling da fila
async function iniciarConsumidor(nomeFila, config) {
  logger.info({ fila: nomeFila }, 'Iniciando consumidor SQS');

  while (true) {
    try {
      const { Messages = [] } = await sqs.send(new ReceiveMessageCommand({
        QueueUrl:            config.url,
        MaxNumberOfMessages: 10,
        WaitTimeSeconds:     20, // Long polling — reduz custos e latência
        MessageAttributeNames: ['All'],
      }));

      await Promise.allSettled(
        Messages.map(msg => processarMensagem(msg, nomeFila, config))
      );

    } catch (erro) {
      logger.error({ erro: erro.message, fila: nomeFila }, 'Erro no loop de polling');
      // Backoff em caso de erro para não sobrecarregar a fila
      await new Promise(r => setTimeout(r, 5000));
    }
  }
}

async function processarMensagem(msg, nomeFila, config) {
  const timer = duracaoProcessamento.startTimer({ fila: nomeFila });
  const corpo = JSON.parse(msg.Body);
  const msgLogger = logger.child({
    messageId: msg.MessageId,
    fila: nomeFila,
    traceId: corpo.traceId,
  });

  try {
    await config.handler(corpo, msgLogger);

    // Remove a mensagem da fila após processamento bem-sucedido
    await sqs.send(new DeleteMessageCommand({
      QueueUrl:      config.url,
      ReceiptHandle: msg.ReceiptHandle,
    }));

    mensagensProcessadas.inc({ fila: nomeFila, resultado: 'sucesso' });
    timer({ tipo: corpo.tipo || 'desconhecido' });

  } catch (erro) {
    msgLogger.error({ erro: erro.message }, 'Falha ao processar mensagem');
    mensagensProcessadas.inc({ fila: nomeFila, resultado: 'falha' });

    // Não deleta a mensagem — voltará para a fila após o timeout de visibilidade
    // Após maxReceiveCount tentativas, vai para a DLQ automaticamente
  }
}

// Inicia todos os consumidores em paralelo
async function iniciar() {
  await Promise.all(
    Object.entries(FILAS).map(([nome, config]) =>
      iniciarConsumidor(nome, config)
    )
  );
}

module.exports = { iniciar };

Manifestos Kubernetes: Deployment Completo

# infrastructure/kubernetes/services/order-service/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
  namespace: producao
  annotations:
    kubernetes.io/change-cause: "Deploy inicial do order-service"
spec:
  replicas: 3
  selector:
    matchLabels:
      app: order-service
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: order-service
        versao: "1.0.0"
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port:   "3003"
        prometheus.io/path:   "/metrics"
    spec:
      serviceAccountName: order-service
      terminationGracePeriodSeconds: 60

      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: order-service

      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        fsGroup: 1000
        seccompProfile:
          type: RuntimeDefault

      containers:
        - name: order-service
          image: ghcr.io/empresa/order-service:1.0.0
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 3003
              name: http

          env:
            - name: NODE_ENV
              value: production
            - name: PORT
              value: "3003"
            - name: SERVICE_NAME
              value: order-service
            - name: CATALOG_SERVICE_URL
              value: http://catalog-service.producao.svc.cluster.local
            - name: USER_SERVICE_URL
              value: http://user-service.producao.svc.cluster.local

          # Secrets injetados via External Secrets Operator
          envFrom:
            - secretRef:
                name: order-service-secrets

          resources:
            requests:
              memory: "256Mi"
              cpu: "200m"
            limits:
              memory: "512Mi"
              cpu: "1000m"

          livenessProbe:
            httpGet:
              path: /health/live
              port: 3003
            initialDelaySeconds: 30
            periodSeconds: 15
            failureThreshold: 3

          readinessProbe:
            httpGet:
              path: /health/ready
              port: 3003
            initialDelaySeconds: 15
            periodSeconds: 5
            failureThreshold: 3

          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 10"]

          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]

          volumeMounts:
            - name: tmp
              mountPath: /tmp

      volumes:
        - name: tmp
          emptyDir: {}

      imagePullSecrets:
        - name: ghcr-credentials
---
apiVersion: v1
kind: Service
metadata:
  name: order-service
  namespace: producao
spec:
  selector:
    app: order-service
  ports:
    - name: http
      port: 80
      targetPort: 3003
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: order-service
  namespace: producao
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: order-service
  minReplicas: 3
  maxReplicas: 15
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60
    scaleDown:
      stabilizationWindowSeconds: 300
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: order-service-secrets
  namespace: producao
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
    kind: ClusterSecretStore
  target:
    name: order-service-secrets
    creationPolicy: Owner
  dataFrom:
    - extract:
        key: loja/producao/order

Ambiente de Desenvolvimento Local

# docker-compose.yml — desenvolvimento local completo
version: '3.8'

services:
  postgres-catalog:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: catalog_db
      POSTGRES_USER: catalog_admin
      POSTGRES_PASSWORD: dev_password
    ports:
      - "5432:5432"
    volumes:
      - postgres_catalog:/var/lib/postgresql/data
      - ./services/catalog-service/migrations:/docker-entrypoint-initdb.d

  postgres-order:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: order_db
      POSTGRES_USER: order_admin
      POSTGRES_PASSWORD: dev_password
    ports:
      - "5433:5432"
    volumes:
      - postgres_order:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru

  localstack:
    image: localstack/localstack:latest
    ports:
      - "4566:4566"
    environment:
      SERVICES: sqs,secretsmanager
      DEFAULT_REGION: us-east-1
    volumes:
      - ./scripts/local-dev/localstack-init.sh:/etc/localstack/init/ready.d/init.sh

  catalog-service:
    build:
      context: .
      dockerfile: services/catalog-service/Dockerfile
      target: development
    ports:
      - "3001:3001"
    environment:
      NODE_ENV: development
      DATABASE_URL: postgresql://catalog_admin:dev_password@postgres-catalog:5432/catalog_db
      REDIS_URL: redis://redis:6379
    volumes:
      - ./services/catalog-service/src:/app/src
    depends_on:
      - postgres-catalog
      - redis

  order-service:
    build:
      context: .
      dockerfile: services/order-service/Dockerfile
      target: development
    ports:
      - "3003:3003"
    environment:
      NODE_ENV: development
      DATABASE_URL: postgresql://order_admin:dev_password@postgres-order:5432/order_db
      CATALOG_SERVICE_URL: http://catalog-service:3001
      USER_SERVICE_URL: http://user-service:3002
      SQS_URL_PEDIDO_CONFIRMADO: http://localstack:4566/000000000000/pedido-confirmado
      AWS_ENDPOINT_URL: http://localstack:4566
    volumes:
      - ./services/order-service/src:/app/src
    depends_on:
      - postgres-order
      - catalog-service
      - localstack

  notification-service:
    build:
      context: .
      dockerfile: services/notification-service/Dockerfile
      target: development
    environment:
      NODE_ENV: development
      SQS_URL_PEDIDO_CONFIRMADO: http://localstack:4566/000000000000/pedido-confirmado
      USER_SERVICE_URL: http://user-service:3002
      AWS_ENDPOINT_URL: http://localstack:4566
    volumes:
      - ./services/notification-service/src:/app/src
    depends_on:
      - localstack

volumes:
  postgres_catalog:
  postgres_order:

O Que Vem a Seguir

Com a infraestrutura e os serviços implementados, o próximo artigo constrói o pipeline completo de CI/CD que conecta um commit no repositório a um deploy em produção — com testes, scanning de segurança, build de imagem, deploy via ArgoCD e verificação pós-deploy automatizada.


Referências para Aprofundamento

Node.js em produção - Node.js Best Practices — github.com/goldbergyoni — Repositório com mais de 80 boas práticas para Node.js em produção, organizadas por categoria com exemplos e justificativas detalhadas. - Pino Logger Documentation — getpino.io — Documentação do Pino com guia de configuração para produção, serializers customizados e integração com sistemas de logging centralizados.

Padrões de microsserviços - Saga Pattern — microservices.io — Descrição completa do padrão Saga para transações distribuídas em microsserviços, com exemplos de implementação coreografada e orquestrada. - AWS SQS Best Practices — docs.aws.amazon.com — Guia oficial de boas práticas para SQS, incluindo configuração de dead-letter queues, idempotência de consumidores e otimização de custos.

Comentários

Mais em DevOps

Geradores de Senhas Seguras: Implementações em 5 Linguagens de Programação
Geradores de Senhas Seguras: Implementações em 5 Linguagens de Programação

A geração de senhas fortes é um dos pilares da segurança da informação. Uma s...

Protegendo Branches e Revisando Código com Pull Requests
Protegendo Branches e Revisando Código com Pull Requests

Em equipes sem processo de revis&atilde;o, &eacute; comum encontrar c&oacute;...

Capstone: Operações em Produção e Retrospectiva da Jornada
Capstone: Operações em Produção e Retrospectiva da Jornada

Um sistema de software não termina quando o último deploy é feito. Ele começa...