Platform Engineering: Construindo a Plataforma Interna de Desenvolvimento

[314] Platform Engineering: Construindo a Plataforma Interna de Desenvolvimento

A disciplina que nasce quando o time de plataforma vira gargalo: o conceito de Internal Developer Platform, o Backstage como portal e catálogo de serviços, templates de scaffolding, self-service de infraestrutura com módulos Terraform e Crossplane, e os golden paths como opinião — não mandato.
DevOps

24 min de leitura

Quando uma organização tem dois ou três times de desenvolvimento, o modelo DevOps tradicional funciona bem: cada time opera sua própria infraestrutura, define seus próprios pipelines e resolve seus próprios problemas operacionais. O conhecimento é compartilhado informalmente, as ferramentas são razoavelmente padronizadas e o atrito é manejável.

À medida que a organização cresce — dez times, vinte, cinquenta — esse modelo começa a se fragmentar. Cada time reinventa a roda de maneiras ligeiramente diferentes: pipelines de CI/CD com estruturas inconsistentes, configurações de infraestrutura copiadas de repositórios uns dos outros com pequenas variações, práticas de segurança aplicadas de forma irregular, onboarding de novos desenvolvedores que leva semanas porque não há um caminho padronizado. O time de plataforma ou SRE se torna um gargalo — respondendo a perguntas repetitivas, revisando configurações similares, resolvendo os mesmos problemas em contextos diferentes.

O Platform Engineering é a disciplina que resolve esse problema construindo uma Internal Developer Platform — uma plataforma interna que abstrai a complexidade operacional e oferece aos times de desenvolvimento uma experiência de autoatendimento bem definida. O objetivo não é centralizar o controle, mas criar "paved roads" — caminhos pavimentados que tornam a maneira correta de fazer as coisas também a mais fácil.

O Conceito de Internal Developer Platform

Uma IDP — Internal Developer Platform — não é um único produto ou ferramenta. É um conjunto de capacidades, APIs, ferramentas e fluxos de trabalho que o time de plataforma constrói e mantém para que os times de produto possam ser autônomos sem precisar ser especialistas em infraestrutura.

As capacidades típicas de uma IDP madura incluem:

Self-service de infraestrutura — um desenvolvedor consegue provisionar um ambiente completo — banco de dados, cache, filas, monitoramento — sem abrir um ticket para o time de infraestrutura.

Templates de aplicação — novos serviços são criados a partir de templates que já incluem as melhores práticas de segurança, observabilidade e CI/CD.

Portal do desenvolvedor — uma interface centralizada onde o desenvolvedor encontra toda a informação relevante sobre os serviços que opera: status, métricas, logs, dependências, documentação, contato do time.

Catálogo de serviços — visibilidade de todos os serviços da organização, quem os possui, em que estado estão e como se comunicam entre si.

Backstage: O Portal do Desenvolvedor da Spotify

O Backstage é a plataforma de portal do desenvolvedor open-source criada pela Spotify e doada à CNCF. É a base sobre a qual a maioria das organizações constrói sua IDP — um framework extensível que oferece o catálogo de serviços, um sistema de templates e um marketplace de plugins.

Instalando e Configurando o Backstage

# Cria um novo app Backstage
npx @backstage/create-app@latest --path minha-plataforma

cd minha-plataforma

# Instala dependências
yarn install

# Inicia em modo de desenvolvimento
yarn dev

A estrutura do projeto Backstage:

minha-plataforma/
├── app-config.yaml          # Configuração principal
├── app-config.production.yaml
├── packages/
│   ├── app/                 # Frontend (React)
│   │   └── src/
│   │       ├── App.tsx
│   │       └── components/
│   └── backend/             # Backend (Node.js/Express)
│       └── src/
│           └── index.ts
└── plugins/                 # Plugins customizados

Configuração principal do Backstage:

# app-config.yaml
app:
  title: Plataforma de Desenvolvimento — Empresa
  baseUrl: https://plataforma.empresa.com

organization:
  name: Empresa S.A.

backend:
  baseUrl: https://plataforma.empresa.com
  listen:
    port: 7007

  database:
    client: pg
    connection:
      host: ${POSTGRES_HOST}
      port: ${POSTGRES_PORT}
      user: ${POSTGRES_USER}
      password: ${POSTGRES_PASSWORD}
      database: backstage

  cache:
    store: redis
    connection: ${REDIS_URL}

# Autenticação via GitHub OAuth
auth:
  providers:
    github:
      development:
        clientId: ${GITHUB_CLIENT_ID}
        clientSecret: ${GITHUB_CLIENT_SECRET}

# Integração com GitHub para importar componentes
integrations:
  github:
    - host: github.com
      token: ${GITHUB_TOKEN}

# Catálogo — onde o Backstage descobre os componentes
catalog:
  import:
    entityFilename: catalog-info.yaml
    pullRequestBranchName: backstage-integration

  rules:
    - allow:
        [
          Component,
          System,
          API,
          Resource,
          Location,
          Domain,
          Group,
          User,
        ]

  locations:
    # Importa todos os repositórios da organização com catalog-info.yaml
    - type: github-org
      target: https://github.com/empresa
      rules:
        - allow: [Group, User]

    # Importa templates de scaffolding
    - type: url
      target: https://github.com/empresa/plataforma-templates/blob/main/all-templates.yaml

# TechDocs — documentação integrada
techdocs:
  builder: external
  generator:
    runIn: local
  publisher:
    type: awsS3
    awsS3:
      bucketName: ${TECHDOCS_BUCKET}
      region: us-east-1

# Configuração do Kubernetes para mostrar workloads
kubernetes:
  serviceLocatorMethod:
    type: multiTenant
  clusterLocatorMethods:
    - type: config
      clusters:
        - url: ${K8S_CLUSTER_URL}
          name: producao
          authProvider: serviceAccount
          skipTLSVerify: false
          serviceAccountToken: ${K8S_SERVICE_ACCOUNT_TOKEN}
          caData: ${K8S_CA_DATA}

Registrando um Serviço no Catálogo

Cada repositório de serviço inclui um arquivo catalog-info.yaml que descreve o componente para o Backstage:

# catalog-info.yaml — na raiz do repositório do serviço
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: minha-api
  title: API de Pedidos
  description: |
    API REST responsável pelo gerenciamento do ciclo de vida de pedidos,
    incluindo criação, atualização de status, cancelamento e consultas.

  # Etiquetas para filtragem no catálogo
  tags:
    - nodejs
    - api
    - postgres
    - redis

  # Links úteis para o time
  links:
    - url: https://grafana.empresa.com/d/minha-api
      title: Dashboard Grafana
      icon: dashboard
    - url: https://minha-api.empresa.com/docs
      title: Documentação da API
      icon: docs
    - url: https://empresa.pagerduty.com/service-directory/minha-api
      title: PagerDuty
      icon: alert

  annotations:
    # Integração com GitHub
    github.com/project-slug: empresa/minha-api

    # Integração com Kubernetes — mostra os pods no Backstage
    backstage.io/kubernetes-id: minha-api
    backstage.io/kubernetes-namespace: producao

    # Integração com PagerDuty
    pagerduty.com/service-id: PXXXXXX

    # Integração com Sentry
    sentry.io/project-slug: minha-api-producao

    # Documentação técnica via TechDocs
    backstage.io/techdocs-ref: dir:.

spec:
  type: service
  lifecycle: production  # experimental | deprecated | production
  owner: group:checkout-team

  # Sistemas que este componente integra
  system: sistema-pedidos

  # Dependências declaradas
  dependsOn:
    - component:banco-usuarios
    - resource:rds-producao
    - resource:redis-producao

  # APIs que este serviço provê e consome
  providesApis:
    - api-pedidos-v2

  consumesApis:
    - api-catalogo-v1
    - api-pagamentos-v1

A API fornecida pelo serviço:

# api-definition.yaml
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
  name: api-pedidos-v2
  title: API de Pedidos v2
  description: API REST para gestão de pedidos
  tags:
    - rest
    - v2
spec:
  type: openapi
  lifecycle: production
  owner: group:checkout-team
  system: sistema-pedidos

  definition:
    $text: ./openapi.yaml  # Referencia a spec OpenAPI do repositório

Templates de Scaffolding: Criando Novos Serviços

Os Software Templates do Backstage permitem criar novos serviços a partir de templates pré-configurados que já incluem as melhores práticas de segurança, observabilidade e CI/CD:

# templates/nodejs-api/template.yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
  name: nodejs-api-template
  title: API Node.js com Express e PostgreSQL
  description: |
    Cria uma nova API Node.js com Express, PostgreSQL, Redis,
    observabilidade com OpenTelemetry e pipeline de CI/CD completo.
  tags:
    - nodejs
    - api
    - postgres
    - recommended

spec:
  owner: group:plataforma
  type: service

  # Parâmetros coletados do desenvolvedor
  parameters:
    - title: Informações do Serviço
      required: [nome, descricao, owner]
      properties:
        nome:
          title: Nome do serviço
          type: string
          description: Nome em kebab-case (ex: api-pagamentos)
          pattern: '^[a-z][a-z0-9-]*$'
          ui:autofocus: true

        descricao:
          title: Descrição
          type: string
          description: Descreva brevemente a responsabilidade deste serviço

        owner:
          title: Time responsável
          type: string
          description: Time que será dono deste serviço
          ui:field: OwnerPicker
          ui:options:
            catalogFilter:
              kind: Group

    - title: Configuração de Infraestrutura
      properties:
        banco_dados:
          title: Banco de dados PostgreSQL
          type: boolean
          default: true

        redis:
          title: Cache Redis
          type: boolean
          default: false

        filas:
          title: Filas SQS
          type: boolean
          default: false

        ambiente_inicial:
          title: Ambiente inicial
          type: string
          enum: [staging, production]
          default: staging

    - title: Repositório
      required: [repo_url]
      properties:
        repo_url:
          title: Localização do repositório
          type: string
          ui:field: RepoUrlPicker
          ui:options:
            allowedHosts: [github.com]
            allowedOwners: [empresa]

  # Passos de criação do serviço
  steps:
    - id: fetch-base
      name: Baixa o template base
      action: fetch:template
      input:
        url: ./skeleton
        values:
          nome: ${{ parameters.nome }}
          descricao: ${{ parameters.descricao }}
          owner: ${{ parameters.owner }}
          banco_dados: ${{ parameters.banco_dados }}
          redis: ${{ parameters.redis }}
          filas: ${{ parameters.filas }}

    - id: publish
      name: Publica no GitHub
      action: publish:github
      input:
        allowedHosts: [github.com]
        description: ${{ parameters.descricao }}
        repoUrl: ${{ parameters.repo_url }}
        defaultBranch: main
        repoVisibility: private
        requireCodeOwnerReviews: true
        requiredApprovingReviewCount: 1
        topics:
          - nodejs
          - api
          - ${{ parameters.owner }}

    - id: provision-infra
      name: Provisiona infraestrutura via Terraform
      action: http:backstage:request
      input:
        method: POST
        path: /api/terraform-runner/provision
        body:
          servico: ${{ parameters.nome }}
          banco_dados: ${{ parameters.banco_dados }}
          redis: ${{ parameters.redis }}
          ambiente: ${{ parameters.ambiente_inicial }}

    - id: register
      name: Registra no catálogo
      action: catalog:register
      input:
        repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }}
        catalogInfoPath: /catalog-info.yaml

  # O que mostrar ao desenvolvedor após a criação
  output:
    links:
      - title: Repositório GitHub
        url: ${{ steps.publish.output.remoteUrl }}
      - title: Abrir no Catálogo
        entityRef: ${{ steps.register.output.entityRef }}
    text:
      - title: Próximos passos
        content: |
          Seu novo serviço foi criado! 🎉

          **O que foi configurado automaticamente:**
          - Repositório GitHub com branch protection
          - Pipeline de CI/CD (GitHub Actions)
          - Infraestrutura AWS provisionada via Terraform
          - Observabilidade com OpenTelemetry
          - Registro no catálogo de serviços

          **Próximos passos:**
          1. Clone o repositório e explore a estrutura
          2. Configure os secrets no repositório GitHub
          3. Execute o primeiro deploy via pipeline

Skeleton do Template

O diretório skeleton contém a estrutura do serviço com placeholders substituídos pelo Backstage:

templates/nodejs-api/skeleton/
├── catalog-info.yaml
├── package.json
├── Dockerfile
├── .github/
│   └── workflows/
│       ├── ci.yml
│       └── deploy.yml
├── src/
│   ├── server.js
│   ├── health/
│   │   └── health.routes.js
│   └── observability/
│       └── tracing.js
├── terraform/
│   ├── main.tf
│   ├── variables.tf
│   └── outputs.tf
├── docs/
│   ├── index.md
│   └── mkdocs.yml
└── ${{ values.nome }}.md  # README gerado com o nome do serviço

Self-Service de Infraestrutura com Terraform e Crossplane

O self-service de infraestrutura permite que desenvolvedores provisionem recursos sem interagir com o time de plataforma. Duas abordagens complementares:

Módulos Terraform Aprovados

O time de plataforma publica módulos Terraform que encapsulam as melhores práticas e os desenvolvedores os consomem:

# Módulo publicado pelo time de plataforma
# terraform-registry.empresa.com/empresa/api-stack/aws

variable "nome_servico" {
  description = "Nome do serviço — usado como prefixo de todos os recursos"
  type        = string
}

variable "ambiente" {
  type    = string
  default = "staging"
}

variable "banco_dados" {
  description = "Configuração do banco de dados"
  type = object({
    habilitado     = bool
    classe         = optional(string, "db.t3.micro")
    multi_az       = optional(bool, false)
    storage_gb     = optional(number, 20)
  })
  default = { habilitado = false }
}

variable "redis" {
  description = "Configuração do Redis"
  type = object({
    habilitado  = bool
    tipo_no     = optional(string, "cache.t3.micro")
    num_nos     = optional(number, 1)
  })
  default = { habilitado = false }
}

# O módulo cria todos os recursos necessários para um serviço
module "banco" {
  count  = var.banco_dados.habilitado ? 1 : 0
  source = "./modules/rds"

  nome       = "${var.nome_servico}-${var.ambiente}"
  classe     = var.banco_dados.classe
  multi_az   = var.ambiente == "production" ? true : var.banco_dados.multi_az
  storage_gb = var.banco_dados.storage_gb
}

module "cache" {
  count  = var.redis.habilitado ? 1 : 0
  source = "./modules/elasticache"

  nome    = "${var.nome_servico}-${var.ambiente}"
  tipo_no = var.redis.tipo_no
  num_nos = var.ambiente == "production" ? max(var.redis.num_nos, 2) : var.redis.num_nos
}

module "observabilidade" {
  source = "./modules/observabilidade"
  nome   = var.nome_servico
}

# Outputs padronizados que o serviço consome
output "database_url_secret_arn" {
  value = var.banco_dados.habilitado ? module.banco[0].secret_arn : null
}

output "redis_endpoint" {
  value = var.redis.habilitado ? module.cache[0].endpoint : null
}

output "namespace_kubernetes" {
  value = kubernetes_namespace.servico.metadata[0].name
}

Uso pelo time de produto — um arquivo platform.tf simples:

# Na raiz do repositório do serviço — minha-api/terraform/platform.tf
module "plataforma" {
  source  = "terraform-registry.empresa.com/empresa/api-stack/aws"
  version = "~> 2.0"

  nome_servico = "minha-api"
  ambiente     = var.environment

  banco_dados = {
    habilitado = true
    classe     = var.environment == "production" ? "db.r6g.large" : "db.t3.micro"
    multi_az   = var.environment == "production"
  }

  redis = {
    habilitado = true
  }
}

Crossplane: Infraestrutura como Recursos Kubernetes

O Crossplane é uma extensão do Kubernetes que permite criar recursos de cloud — RDS, S3, ElastiCache — usando o mesmo modelo declarativo de manifests YAML que os desenvolvedores já conhecem:

# kubernetes/infra/banco-dados.yaml
# O desenvolvedor declara o banco que precisa como um recurso Kubernetes
apiVersion: database.empresa.com/v1alpha1
kind: PostgreSQLDatabase
metadata:
  name: minha-api-db
  namespace: producao
spec:
  # Classe de composição define o que "staging" vs "production" significa
  compositionSelector:
    matchLabels:
      ambiente: production

  parameters:
    classe: db.r6g.large
    storageGb: 100
    multiAz: true
    backupRetentionDays: 14

  writeConnectionSecretToRef:
    name: minha-api-db-credenciais
    namespace: producao

O Crossplane recebe esse manifest, provisiona o RDS na AWS via Terraform ou AWS Provider, e armazena as credenciais como um Kubernetes Secret — tudo sem que o desenvolvedor precise interagir com o console da AWS ou escrever Terraform.

Métricas de Saúde da Plataforma

O time de plataforma usa métricas próprias para medir a eficácia da IDP:

DORA Metrics por time — mede se a plataforma está realmente acelerando a entrega dos times que a usam.

Tempo de onboarding — quanto tempo um novo desenvolvedor leva para fazer o primeiro deploy. O objetivo é reduzir de dias para horas.

Adoção de templates — percentual de novos serviços criados via templates vs criados manualmente. Alta adoção indica que os templates são úteis e de baixo atrito.

Toil reduction — redução do trabalho repetitivo e manual do time de plataforma medida em tickets, tempo de resposta e número de perguntas repetitivas.

# Painel de saúde da plataforma no Backstage
# plugins/platform-health/src/components/PlatformHealthCard.tsx

# Métricas exibidas no dashboard do time de plataforma:
# - Serviços no catálogo: 127
# - Serviços criados via template: 89% (últimos 6 meses)
# - Tempo médio de onboarding: 4 horas (meta: < 2h)
# - Tickets de infraestrutura/semana: 12 (baseline: 45)
# - DORA — Deploy Frequency: 8.2/dia (todos os times)
# - DORA — Lead Time: 42min (meta: < 60min)
# - DORA — Change Failure Rate: 1.8% (meta: < 5%)
# - DORA — MTTR: 18min (meta: < 60min)

Golden Paths: Opiniões, Não Mandatos

Um princípio fundamental de Platform Engineering é que os "paved roads" — caminhos pavimentados — devem ser a opção mais fácil, não a única. Um time com requisitos genuinamente diferentes deve poder sair do caminho padrão, desde que documente o motivo e aceite a responsabilidade adicional.

Essa filosofia se manifesta em como a plataforma é construída:

Os templates oferecem uma experiência de alta qualidade para o caso de uso mais comum — uma API Node.js com PostgreSQL e Redis, por exemplo. Times que precisam de Python, Go ou uma arquitetura radicalmente diferente podem criar seus próprios templates ou usar os módulos Terraform diretamente.

As restrições de segurança são aplicadas em camadas — algumas são mandatórias e aplicadas no nível de organização (criptografia em repouso, não expor portas desnecessárias ao mundo), outras são recomendações que os times podem sobrescrever com justificativa documentada.

O catálogo de serviços captura a realidade — não apenas o estado ideal. Serviços legados, serviços em modo experimental e serviços que deliberadamente saíram do caminho padrão são todos registrados, com suas exceções documentadas.

O Que Vem a Seguir

O próximo artigo cobre o penúltimo tema da série: Cultura e Práticas Organizacionais em DevOps — como medir a maturidade de uma organização DevOps, como conduzir revisões pós-incidente que geram aprendizado real e como construir uma cultura de melhoria contínua que sustenta as práticas técnicas ao longo do tempo.

Referências para Aprofundamento

Backstage

  • Backstage Documentation — backstage.io — Documentação oficial do Backstage cobrindo instalação, configuração do catálogo, software templates e desenvolvimento de plugins.
  • Backstage Plugins — backstage.io — Catálogo oficial de plugins do Backstage com integrações para GitHub, PagerDuty, Kubernetes, Grafana, SonarQube e centenas de outras ferramentas.

Platform Engineering

Crossplane

Exercícios

Exercício 1

No módulo api-stack, duas linhas tratam o ambiente de produção de forma especial:

multi_az = var.ambiente == "production" ? true : var.banco_dados.multi_az
num_nos  = var.ambiente == "production" ? max(var.redis.num_nos, 2) : var.redis.num_nos

Um time declara multi_az = false e num_nos = 5 com ambiente = "production". O que ele recebe? As duas linhas se comportam da mesma maneira?

Ver resposta

✓ Resposta: Ele recebe multi_az = true (o que pediu foi ignorado) e num_nos = 5 (o que pediu foi respeitado). As duas linhas parecem simétricas e não são.

A primeira é uma imposição: em produção o valor é true, ponto — o input do time não é sequer consultado. A segunda é um piso: max(5, 2) devolve 5, porque o time pediu mais do que o mínimo. Se tivesse pedido 1, receberia 2.

A distinção é exatamente a filosofia de paved roads que o artigo defende, expressa em código. O max() diz "não deixo você ficar abaixo do seguro, mas acima é escolha sua"; o ternário fixo diz "isto não se negocia". E cada um é adequado a um tipo de requisito: alta disponibilidade de banco em produção é regra da organização, enquanto o número de nós de cache é dimensionamento, que pertence a quem conhece a carga.

Vale notar o que acontece do lado de quem consome. O time escreve multi_az = false, o terraform plan mostra multi_az = true, e não há nada no plano explicando por quê — a decisão está enterrada em um módulo remoto versionado. Um guard-rail silencioso confunde tanto quanto protege. A saída é tornar a regra visível: uma precondition que falha com mensagem explícita é mais honesta que uma sobrescrita muda.

lifecycle {
  precondition {
    condition     = var.ambiente != "production" || var.banco_dados.multi_az
    error_message = "Banco em produção exige multi_az = true (política de disponibilidade da plataforma)."
  }
}

Repare também no optional(string, "db.t3.micro") das variáveis: ele permite que o time declare apenas { habilitado = true } e receba defaults sensatos no resto — outro mecanismo de caminho pavimentado, agora por omissão em vez de imposição.

Exercício 2

Os steps do template de scaffolding rodam em sequência: fetch-base, publish (cria o repositório no GitHub), provision-infra (chama o runner do Terraform) e register (registra no catálogo). O passo provision-infra falha porque a conta AWS atingiu o limite de instâncias RDS. Em que estado o desenvolvedor fica?

Ver resposta

✓ Resposta: Em um estado parcial e órfão. O repositório já existe no GitHub, com branch protection, code owners e o pipeline configurado — o publish concluiu antes. A infraestrutura não existe. E o register nunca rodou, então o serviço não aparece no catálogo: existe um repositório novo que a plataforma desconhece.

O Backstage não desfaz os passos anteriores — o scaffolder não é transacional. Não há rollback automático, e o desenvolvedor vê apenas uma tela de erro no meio do fluxo, sem indicação clara do que sobrou para trás.

Pior é o que costuma acontecer em seguida: a pessoa tenta de novo. O publish falha agora porque o repositório já existe, e a execução para ainda mais cedo — sem nunca chegar ao provisionamento. O fluxo fica travado até alguém apagar o repositório manualmente, que é exatamente o tipo de ticket ao time de plataforma que a IDP existia para eliminar.

Duas correções tornam isso administrável. A primeira é de ordem: colocar os passos que podem falhar por motivos externos antes dos que criam artefatos duradouros, ou dividir o template em uma etapa de validação (checar quota, permissões, disponibilidade do nome) que roda antes de qualquer criação. A segunda é tornar cada passo idempotente, de modo que repetir a execução converge em vez de colidir — o publish reaproveitando um repositório vazio já existente, o provision-infra reconhecendo um estado Terraform já iniciado.

É também um argumento a favor do Crossplane, apresentado adiante no artigo: como a infraestrutura vira um recurso do Kubernetes, quem faz a convergência é o controller, com reconciliação contínua. Um pedido que falha por quota fica pendente e é atendido sozinho quando a quota liberar — em vez de exigir que o desenvolvedor recomece um fluxo de quatro passos.

Exercício 3

No manifesto do Crossplane, o desenvolvedor declara compositionSelector: matchLabels: ambiente: production e, logo abaixo, parameters com classe: db.r6g.large e multiAz: true. Se ele já especifica a classe e o multi-AZ, para que serve o seletor de composição?

Ver resposta

✓ Resposta: Eles operam em níveis diferentes: os parameters são o que o time pede; a Composition é o que a organização entrega quando alguém pede aquilo em produção.

O PostgreSQLDatabase é um tipo criado pela empresa — não existe na AWS nem no Crossplane. Por trás dele, a Composition selecionada define tudo o que o desenvolvedor não escreveu e provavelmente nem deveria decidir: em qual VPC e subnets o banco nasce, qual security group o protege, qual chave KMS o criptografa, se há réplica de leitura, para onde vão os backups, quais tags de custo são aplicadas, qual janela de manutenção vale. Uma Composition rotulada ambiente: staging pode materializar o mesmo pedido com uma instância pequena, sem multi-AZ e com retenção de 1 dia.

É a diferença entre declarar intenção e declarar implementação. O time diz "preciso de um banco PostgreSQL de produção com essa capacidade"; a plataforma decide como isso se traduz em recursos reais — e pode mudar essa tradução (trocar a família de instância, adotar Aurora, migrar de região) sem que nenhum time altere seu manifesto.

O writeConnectionSecretToRef completa o desenho: as credenciais geradas aparecem como um Secret do Kubernetes no namespace do time, prontas para o Deployment consumir. O desenvolvedor nunca vê o console da AWS, nunca escreve Terraform e nunca manipula a senha.

Vale o contraponto honesto: nada impede que ele peça classe: db.r6g.16xlarge. Um parâmetro exposto é um parâmetro que alguém vai exagerar — por isso Compositions maduras validam faixas aceitáveis no schema da Composite Resource Definition, e não apenas confiam no bom senso de quem preenche o YAML.

Exercício 4

Uma migração vai derrubar o rds-producao por 40 minutos numa madrugada. Usando exclusivamente o que está declarado no catalog-info.yaml do artigo, como descobrir quem precisa ser avisado? E qual o ponto fraco desse método?

Ver resposta

✓ Resposta: Pelo grafo de dependências. O manifesto declara dependsOn: [component:banco-usuarios, resource:rds-producao, resource:redis-producao], então basta consultar o catálogo por todos os componentes que dependem de resource:rds-producao. Cada um traz seu owner — aqui group:checkout-team —, e o grupo no catálogo carrega os membros e canais de contato. Os links ainda dão o PagerDuty de cada serviço, o que resolve a pergunta seguinte: quem está de plantão naquela madrugada.

O consumesApis estende o alcance para o segundo grau: quem não usa o banco diretamente, mas depende de uma API de quem usa, também sofre o impacto. É justamente a pergunta que ninguém consegue responder de memória em uma organização com 127 serviços — e a razão principal de o catálogo existir. Sem ele, a alternativa é mandar mensagem no canal geral e torcer para a pessoa certa estar lendo.

O ponto fraco é que a declaração é manual e não é verificada. O dependsOn reflete o que alguém escreveu no YAML no dia em que criou o serviço — não o que o código realmente faz hoje. Dependências adicionadas depois raramente voltam ao manifesto, e dependências removidas continuam listadas. O grafo apodrece em silêncio, e o modo de falha é traiçoeiro: ele parece completo. Confia-se nele para a comunicação da migração, e o time que faltava no grafo é surpreendido às 3 da manhã.

Mitigações que funcionam: derivar parte do grafo de fontes que não mentem — rastros do OpenTelemetry mostram quem realmente chamou quem no último mês, e Network Policies declaram conexões permitidas —, e transformar a manutenção do manifesto em rotina de revisão, e não em formulário de cadastro. Um item de checklist no template de PR ("mudou uma dependência? atualize o catalog-info.yaml") custa pouco e mantém o dado vivo.

Exercício 5

O painel de saúde da plataforma mostra "Serviços criados via template: 89%". Suponha que, seis meses depois, esse número caia para 40%. Segundo a filosofia de golden paths defendida no artigo, o que o time de plataforma deve concluir?

Ver resposta

✓ Resposta: Que o template deixou de servir — e não que os times ficaram indisciplinados.

Essa é a inversão que define Platform Engineering. O artigo afirma que os caminhos pavimentados devem ser "a opção mais fácil, não a única": se são de fato os mais fáceis, a adoção acontece por conveniência, sem precisar ser cobrada. Quando 60% dos times escolhem o caminho mais trabalhoso — escrever pipeline do zero, montar Terraform à mão, configurar observabilidade manualmente —, isso significa que, para eles, sair do padrão ainda compensa. É um dado sobre o produto, não sobre as pessoas.

A adoção funciona como métrica justamente por ser voluntária. Uma métrica que se pode cumprir por decreto para de medir qualquer coisa: tornar o template obrigatório levaria a adoção a 100% instantaneamente, e o número perderia todo o valor diagnóstico — ao mesmo tempo em que empurraria os times para workarounds, gerando serviços que nascem do template e são desfigurados no primeiro commit.

As causas prováveis são investigáveis, e é isso que o time deve fazer antes de qualquer coisa: o template cobre uma stack que deixou de ser a dominante (chegaram times de Go ou Python); ele carrega opiniões que envelheceram; está desatualizado a ponto de exigir correções logo após a geração; ou o fluxo de quatro passos falha com frequência — como no cenário do exercício 2 — e ganhou fama de não funcionar.

As outras métricas do painel ajudam a separar as hipóteses. Se os tickets de infraestrutura por semana voltarem a subir junto com a queda, os times estão sofrendo fora do caminho pavimentado — a plataforma perdeu alcance. Se os tickets seguirem baixos e as métricas DORA dos times continuarem boas, eles encontraram formas melhores de trabalhar, e o papel do time de plataforma passa a ser aprender com elas e incorporá-las ao template — que é exatamente o que "opiniões, não mandatos" significa na prática.

Comentários

Mais em DevOps

Rastreamento Distribuído com OpenTelemetry
Rastreamento Distribuído com OpenTelemetry

O que fazer quando a latência sobe e os logs não acusam nada: a instrumentação…

Docker Compose: Orquestrando Múltiplos Serviços Localmente
Docker Compose: Orquestrando Múltiplos Serviços Localmente

Docker Compose para orquestrar múltiplos serviços localmente: a anatomia do…

Terraform + Ansible: Do Provisionamento à Configuração
Terraform + Ansible: Do Provisionamento à Configuração

A fronteira entre provisionar e configurar, e como cruzá-la sem trabalho…