Abstração de runtime e provider

TLDR: Todo módulo que tem mais de uma implementação possível é dividido em um orquestrador (que só escolhe) e engines/providers (que só implementam), de modo que trocar de implementação seja mudar uma variável — nunca mudar código.

Contexto

Infraestrutura tende a amarrar código a fornecedor: o nome do recurso, o nome da variável e o formato do output vazam o provedor para dentro de quem consome. Quando o fornecedor muda, a mudança é uma reescrita.

Este projeto já pagou esse preço várias vezes: cluster gerido → EKS, droplet → Amazon MQ, registry antigo → ECR. A resposta arquitetural foi extrair cada peça trocável para um módulo com interface unificada.

Regra de ouro: se você precisa mudar código para trocar de runtime, a abstração está errada.

Decisão

Duas camadas, papéis separados

Aspecto Orquestrador Engine / Provider
Localização src/modules/{providers\|runtimes}/{nome}/ .../{engines\|providers}/{nome}/
Arquivo principal _interface.tf main.tf
Responsabilidade seleção condicional implementação dos recursos
Variáveis genéricas, sem prefixo específicas, sem prefixo
Contém resource? ❌ nunca ✅ sempre

src/modules/runtimes/n8n/ ├── _interface.tf ← orquestrador (seleção de engine) ├── variables.tf ← variáveis genéricas, sem prefixo ├── outputs.tf ← outputs normalizados └── engines/ ← implementações concretas ├── app-platform/ └── kubernetes/

Seleção por count + locals.active

```hcl module “app_platform” { count = var.runtime == “app-platform” ? 1 : 0 source = “./engines/app-platform”

# passa APENAS o que app-platform usa db_host = var.db_host db_cluster_name = var.db_cluster_name # NÃO passa: namespace, vpc_id }

module “kubernetes” { count = var.runtime == “kubernetes” ? 1 : 0 source = “./engines/kubernetes”

db_host = var.db_host namespace = var.namespace vpc_id = var.vpc_id # NÃO passa: db_cluster_name }

locals { runtimes = { app-platform = try(module.app_platform[0], null) kubernetes = try(module.kubernetes[0], null) } active = local.runtimes[var.runtime] } ```

Nenhum prefixo, em nenhum nível

```hcl # ✅ CORRETO — genérico, sem prefixo variable “db_host” {} variable “namespace” { default = “” } # usado só por kubernetes; default vazio = opcional variable “db_cluster_name” { default = “” } # usado só por app-platform

❌ ERRADO — prefixo no orquestrador

variable “kubernetes_namespace” {} variable “app_platform_db_cluster_name” {} ```

O prefixo obriga quem consome a renomear variável ao trocar de runtime — exatamente o que a abstração existe para evitar. Cada engine declara somente as variáveis que usa.

Nome abstrato, não nome de fornecedor

hcl # ✅ messagebroker_host, messagebroker_port, messagebroker_vhost # ❌ rabbitmq_host, kafka_host

Amanhã o RabbitMQ pode virar Kafka ou Redis Streams. A única exceção herdada é redis_host (em vez de cache_host), mantida por histórico e uso amplo — candidata a renomeação num refactor futuro.

Outputs normalizados

Todos os engines expõem a mesma interface de output, e o orquestrador só encaminha:

```hcl # engines//outputs.tf → value = .this.live_url # engines/kubernetes/outputs.tf → value = "https://${var.app_subdomain}.${var.domain}"

orquestrador — outputs.tf

output “app_url” { value = local.active.app_url } output “app_id” { value = local.active.app_id } ```

Quem consome nunca sabe qual engine está ativo.

Consequências

O que isso compra

  • Migração vira troca de variável. Foi assim que a abstração do provider de Kubernetes transformou a migração para EKS de reescrita destrutiva em provider_type = "aws", com rollback igualmente simples.
  • Duas implementações coexistem, viabilizando blue/green e reversão rápida.
  • Adicionar um engine é aditivo: um bloco module novo, uma entrada no mapa locals.runtimes e as variáveis específicas com default vazio. Nada existente muda.

```hcl module “docker_compose” { count = var.runtime == “docker-compose” ? 1 : 0 source = “./engines/docker-compose”

db_host = var.db_host messagebroker_host = var.messagebroker_host compose_file_path = var.compose_file_path # novo, específico }

locals { runtimes = { app-platform = try(module.app_platform[0], null) kubernetes = try(module.kubernetes[0], null) docker-compose = try(module.docker_compose[0], null) } } ```

O que isso custa

  • Variável opcional acumula. Toda variável específica de engine vira default = "" no orquestrador, então a superfície de variável cresce com o número de engines.
  • A troca não é indolor no recurso. Mudar provider_type no mesmo workspace faz o Terraform destruir a implementação antiga e criar a nova. Migração sem indisponibilidade exige workspace separado + cutover de DNS, ou terraform state mv.
  • Refactor exige state mv. Mover recurso existente para dentro do módulo é migração manual de state, feita uma vez, e o critério de aceite é terraform plan mostrando zero mudanças.

Checklist para módulo novo

  • [ ] _interface.tf com seleção condicional por count
  • [ ] variáveis genéricas, sem prefixo, no orquestrador
  • [ ] cada engine recebe apenas as suas variáveis
  • [ ] locals.active selecionando o engine
  • [ ] cada engine declara só o que usa
  • [ ] outputs normalizados — mesma interface em todos os engines
  • [ ] nome abstrato, sem amarração em tecnologia
  • [ ] comentário indicando uso condicional das variáveis opcionais

Referências