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/
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
modulenovo, uma entrada no mapalocals.runtimese 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_typeno mesmo workspace faz o Terraform destruir a implementação antiga e criar a nova. Migração sem indisponibilidade exige workspace separado + cutover de DNS, outerraform 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 planmostrando zero mudanças.
Checklist para módulo novo
- [ ]
_interface.tfcom seleção condicional porcount - [ ] variáveis genéricas, sem prefixo, no orquestrador
- [ ] cada engine recebe apenas as suas variáveis
- [ ]
locals.activeselecionando 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
- Abstração do provider de Kubernetes — a aplicação mais consequente deste padrão.
src/modules/providers/{cache,database,messagebroker}— implementações de referência.