A maioria das comparações entre Terraform e Pulumi fica-se por "o HCL é declarativo, o Pulumi usa linguagens reais, escolhe pelas competências da equipa". É verdade, e não ajuda a decidir. As diferenças que importam aparecem mais tarde: no que o ficheiro de estado guarda, na forma como testas, no que um revisor vê antes de uma alteração chegar a produção e na dificuldade de sair.
Por isso, este artigo constrói um stack pequeno e realista nas duas ferramentas e corre-o. Cada listagem HCL abaixo passou o validate no Terraform e no OpenTofu (o único bloco exclusivo do OpenTofu está assinalado), e os testes passaram a verde em ambos; cada listagem TypeScript passou o tsc, e os respetivos testes unitários passaram a verde.
O stack: uma equipa de plataforma dá a cada equipa de produto o seu próprio namespace de Kubernetes, com uma ResourceQuota e um LimitRange. Assim que uma quota cobre CPU ou memória, o Kubernetes pode rejeitar qualquer pod novo que não declare requests ou limits para esses recursos, por isso o LimitRange fornece valores por omissão. Um mapa de equipas obriga a um ciclo, e três recursos por equipa pedem reutilização.
O stack em HCL
O módulo raiz chama um módulo filho uma vez por entrada do mapa com for_each. A regra de validação rejeita nomes que o Kubernetes recusaria como nomes de namespace antes de qualquer coisa chegar à API.
variable "teams" {
description = "Team name => namespace quota"
type = map(object({
cpu = string
memory = string
pods = optional(number, 50)
}))
validation {
condition = alltrue([for name in keys(var.teams) : can(regex("^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?$", name))])
error_message = "Team names must be valid namespace names (DNS-1123 labels)."
}
}
module "team" {
source = "./modules/team-namespace"
for_each = var.teams
name = each.key
cpu = each.value.cpu
memory = each.value.memory
pods = each.value.pods
}O módulo contém os três recursos e dois outputs para os testes:
variable "name" { type = string }
variable "cpu" { type = string }
variable "memory" { type = string }
variable "pods" { type = number }
resource "kubernetes_namespace_v1" "this" {
metadata {
name = var.name
labels = { team = var.name }
}
}
resource "kubernetes_resource_quota_v1" "this" {
metadata {
name = "team-quota"
namespace = kubernetes_namespace_v1.this.metadata[0].name
}
spec {
hard = {
"requests.cpu" = var.cpu
"requests.memory" = var.memory
pods = tostring(var.pods)
}
}
}
resource "kubernetes_limit_range_v1" "this" {
metadata {
name = "container-defaults"
namespace = kubernetes_namespace_v1.this.metadata[0].name
}
spec {
limit {
type = "Container"
default_request = { cpu = "100m", memory = "128Mi" }
default = { cpu = "500m", memory = "512Mi" }
}
}
}
output "namespace" { value = kubernetes_namespace_v1.this.metadata[0].name }
output "quota" { value = kubernetes_resource_quota_v1.this.spec[0].hard }A versão fixada do provider fica em versions.tf e a lista de equipas num ficheiro .tfvars. Cada instância recebe um endereço como module.team["payments"]: a chave do mapa é a sua identidade, o que volta a importar na secção de revisão.
O mesmo stack em Pulumi com TypeScript
O programa lê o mesmo mapa da configuração do stack e percorre-o num ciclo:
import * as pulumi from "@pulumi/pulumi";
import { TeamNamespace, TeamArgs } from "./team";
const config = new pulumi.Config();
const teams = config.requireObject<Record<string, TeamArgs>>("teams");
export const namespaces = Object.entries(teams).map(
([name, args]) => new TeamNamespace(name, args).namespace,
);As três chamadas new podiam ficar dentro desse ciclo. Assim que um segundo stack precisar delas, move-as para um ComponentResource, a unidade de reutilização do Pulumi:
import * as pulumi from "@pulumi/pulumi";
import * as k8s from "@pulumi/kubernetes";
export interface TeamArgs {
cpu: string;
memory: string;
pods?: number;
}
export class TeamNamespace extends pulumi.ComponentResource {
public readonly namespace: pulumi.Output<string>;
public readonly quota: k8s.core.v1.ResourceQuota;
constructor(name: string, args: TeamArgs, opts?: pulumi.ComponentResourceOptions) {
if (!/^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?$/.test(name)) {
throw new Error(`team name "${name}" is not a DNS-1123 label`);
}
super("platform:k8s:TeamNamespace", name, {}, opts);
const child = { parent: this };
const ns = new k8s.core.v1.Namespace(name, {
metadata: { name, labels: { team: name } },
}, child);
this.quota = new k8s.core.v1.ResourceQuota(`${name}-quota`, {
metadata: { name: "team-quota", namespace: ns.metadata.name },
spec: {
hard: {
"requests.cpu": args.cpu,
"requests.memory": args.memory,
pods: String(args.pods ?? 50),
},
},
}, child);
new k8s.core.v1.LimitRange(`${name}-defaults`, {
metadata: { name: "container-defaults", namespace: ns.metadata.name },
spec: {
limits: [{
type: "Container",
defaultRequest: { cpu: "100m", memory: "128Mi" },
default: { cpu: "500m", memory: "512Mi" },
}],
},
}, child);
this.namespace = ns.metadata.name;
this.registerOutputs({ namespace: this.namespace });
}
}Três detalhes diferem da versão HCL:
- A validação é código normal. A verificação do nome lança uma exceção no construtor, por isso um nome inválido faz falhar o
pulumi previewtal como a validação HCL faz falhar oterraform plan. - A identidade é o nome lógico.
payments-quotae o componente pai formam o URN do recurso, o equivalente no Pulumi ao endereço do módulo. Muda o nome lógico e o Pulumi planeia uma eliminação e uma criação. - Define tu o
metadata.name. Sem ele, o Pulumi atribui um nome automático ao objeto: um Namespace declarado comopaymentssem nome ficoupayments-7dfa2b14quando o corremos. Útil quando dois stacks fazem deploy do mesmo programa; uma surpresa para um namespace que os programadores escrevem todos os dias.
Estado, bloqueios e os segredos que contém
As duas ferramentas guardam um estado que liga o código aos objetos reais, incluindo os atributos dos recursos.
- O Terraform e o OpenTofu escolhem um backend num bloco
backend. Duas opções neutras em relação ao fabricante:kubernetesguarda o estado num Secret e bloqueia com um Lease;pgguarda-o numa tabela PostgreSQL e bloqueia com advisory locks. O armazenamento de objetos e o serviço alojado do fabricante são as alternativas habituais. - O Pulumi escolhe um com
pulumi login: por omissão, o serviço gerido do fabricante, ou então um backend autogerido (uma diretoria local, armazenamento de objetos ou PostgreSQL). Os backends autogeridos bloqueiam por omissão com um ficheiro de bloqueio e guardam o histórico de checkpoints em.pulumi/history/.
É nos segredos que diferem. Fizemos a mesma experiência nas duas: passar uma palavra-passe marcada como sensível, aplicar e procurá-la no estado.
# Terraform: a variable with sensitive = true, passed to a resource
$ grep -c 's3cr3t-demo' terraform.tfstate
2
# Pulumi: the same value, set with pulumi config set --secret
$ grep -c 's3cr3t-demo' .pulumi/stacks/secrets-demo/dev.json
0No Terraform, sensitive = true apenas esconde o valor no output da CLI; o estado guarda-o em texto simples, aqui duas vezes. O Pulumi encripta todos os valores marcados como segredo, e todos os valores derivados deles, com uma chave por stack: por omissão, a chave do serviço gerido; nos backends autogeridos, por omissão, uma frase-passe; ou então o serviço de gestão de chaves da tua cloud. O provider de Kubernetes marca automaticamente como segredo os campos data e stringData dos Secrets. Os valores não marcados como segredo ficam em JSON simples ao lado do texto cifrado.
As ferramentas HCL encurtaram a distância:
- O Terraform 1.10 acrescentou os valores efémeros e a 1.11 os argumentos só de escrita, que nunca são escritos nos ficheiros de estado nem de plano. O recurso Secret do provider de Kubernetes tem um,
data_wo; incrementasdata_wo_revisionpara enviar um valor novo. O OpenTofu 1.11 acrescentou o mesmo. - O OpenTofu 1.7 acrescentou a encriptação do lado do cliente dos ficheiros de estado e de plano. O Terraform 1.16 rejeita o bloco abaixo como tipo de bloco não suportado. Com o OpenTofu, o ficheiro de estado em disco passou a ser um blob
encrypted_data:
terraform {
encryption {
key_provider "pbkdf2" "main" {
passphrase = var.state_passphrase
}
method "aes_gcm" "main" {
keys = key_provider.pbkdf2.main
}
state {
method = method.aes_gcm.main
enforced = true
}
plan {
method = method.aes_gcm.main
enforced = true
}
}
}Se perderes a frase-passe, o estado é irrecuperável; guarda uma cópia antes.
A regra que daqui resulta: quem consegue ler um backend de Terraform não encriptado consegue ler todos os segredos que lá estão, por isso acesso ao estado é acesso às credenciais de produção. Com uma frase-passe do Pulumi ou o teu próprio serviço de gestão de chaves, ou com o estado do OpenTofu encriptado, podes conceder a chave separadamente do acesso ao backend.
Testes sem cluster
O Terraform 1.7 acrescentou mock_provider ao terraform test: um provider falso preenche os atributos calculados com valores gerados, por isso os testes não precisam de cluster nem de credenciais. Este ficheiro fica em tests/:
mock_provider "kubernetes" {}
variables {
teams = {
payments = { cpu = "8", memory = "16Gi" }
search = { cpu = "4", memory = "8Gi", pods = 20 }
}
}
run "one_namespace_per_team" {
command = plan
assert {
condition = toset(keys(module.team)) == toset(["payments", "search"])
error_message = "Expected one module instance per team."
}
assert {
condition = module.team["search"].quota["pods"] == "20"
error_message = "The pods override did not reach the quota."
}
assert {
condition = module.team["payments"].quota["pods"] == "50"
error_message = "Teams without an override should get 50 pods."
}
}
run "rejects_invalid_team_name" {
command = plan
variables {
teams = { "Payments_EU" = { cpu = "1", memory = "1Gi" } }
}
expect_failures = [var.teams]
}$ terraform test
tests/teams.tftest.hcl... in progress
run "one_namespace_per_team"... pass
run "rejects_invalid_team_name"... pass
tests/teams.tftest.hcl... tearing down
tests/teams.tftest.hcl... pass
Success! 2 passed, 0 failed.expect_failures transforma a regra de validação num teste: o run só passa se a regra rejeitar a entrada. O mesmo ficheiro passou sem alterações com tofu test no OpenTofu 1.13. Para provar que o teste consegue falhar, mudámos o valor por omissão para 40 pods, e a terceira asserção falhou com "40" contra "50".
Os testes unitários do Pulumi correm no test runner habitual da tua linguagem, com pulumi.runtime.setMocks a substituir o motor. Corremos isto com o runner integrado do Node (npx tsx --test team.test.ts) numa máquina sem a CLI do Pulumi, sem backend e sem cluster:
import { describe, it, before } from "node:test";
import assert from "node:assert/strict";
import * as pulumi from "@pulumi/pulumi";
import { TeamNamespace } from "./team";
// Resolve an Output in a test. Mocks make every value known.
const valueOf = <T>(o: pulumi.Output<T>) =>
new Promise<T>((resolve) => o.apply(resolve));
describe("TeamNamespace", () => {
before(async () => {
await pulumi.runtime.setMocks({
newResource: (args) => ({ id: `${args.name}-id`, state: args.inputs }),
call: (args) => args.inputs,
});
});
it("defaults the pod quota to 50", async () => {
const team = new TeamNamespace("payments", { cpu: "8", memory: "16Gi" });
const hard = await valueOf(team.quota.spec.hard);
assert.equal(hard["pods"], "50");
assert.equal(hard["requests.cpu"], "8");
});
it("passes a pod override through", async () => {
const team = new TeamNamespace("search", { cpu: "4", memory: "8Gi", pods: 20 });
assert.equal((await valueOf(team.quota.spec.hard))["pods"], "20");
});
it("rejects names that are not valid namespaces", () => {
assert.throws(
() => new TeamNamespace("Payments_EU", { cpu: "1", memory: "1Gi" }),
/DNS-1123/,
);
});
});Os três passaram; a alteração do valor por omissão para 40 fez falhar o primeiro. Se o ficheiro em teste criar recursos ao ser importado, como faz um index.ts típico, carrega-o com um import() dinâmico depois de setMocks.
Em que é boa cada abordagem:
terraform testverifica o plano ou o estado que o motor real produz, incluindo a ligação entre módulos e a validação. Aplicam-se as regras de tipos do HCL: comparar um output do tipo lista com["Container"]falhou até o envolvermos emtolist(), porque um literal entre parênteses retos é um tuplo.- Os testes do Pulumi são código normal, com fixtures e helpers. Mas o mock devolve as entradas tal como estão, por isso testas a tua lógica, não o comportamento do provider.
- Nenhum substitui um apply real contra um cluster descartável antes de uma release.
Queres comparar as duas no teu próprio código? Pede uma avaliação gratuita de Infraestrutura como Código: lemos os teus módulos ou programas, verificamos o que está no teu estado, corremos os teus testes e entregamos-te uma lista de riscos ordenada por impacto.
Ciclos, condicionais e o problema do "dinâmico"
Os ciclos simples são fáceis nas duas; os blocos aninhados opcionais e os ciclos aninhados, não. Duas extensões mostram-no: um limite de tamanho para PersistentVolumeClaim só nas equipas que definem max_pvc_size, e um RoleBinding edit por cada grupo da equipa.
Em HCL, um bloco opcional é um bloco dynamic sobre uma lista de zero ou um elementos, e um ciclo aninhado precisa de flatten mais uma expressão de mapa para ter chaves de for_each estáveis:
# modules/team-namespace/main.tf, inside the LimitRange spec
dynamic "limit" {
for_each = var.max_pvc_size == null ? [] : [var.max_pvc_size]
content {
type = "PersistentVolumeClaim"
max = { storage = limit.value }
}
}
# main.tf: one RoleBinding per team and group
locals {
team_groups = flatten([
for team, cfg in var.teams : [
for group in cfg.groups : { team = team, group = group }
]
])
}
resource "kubernetes_role_binding_v1" "edit" {
for_each = { for tg in local.team_groups : "${tg.team}/${tg.group}" => tg }
metadata {
name = "edit-${each.value.group}"
namespace = module.team[each.value.team].namespace
}
role_ref {
api_group = "rbac.authorization.k8s.io"
kind = "ClusterRole"
name = "edit"
}
subject {
api_group = "rbac.authorization.k8s.io"
kind = "Group"
name = each.value.group
}
}O TypeScript é o código que escreverias de qualquer forma:
// team.ts, the LimitRange spec
limits: [
{
type: "Container",
defaultRequest: { cpu: "100m", memory: "128Mi" },
default: { cpu: "500m", memory: "512Mi" },
},
...(args.maxPvcSize
? [{ type: "PersistentVolumeClaim", max: { storage: args.maxPvcSize } }]
: []),
],
// index.ts: one RoleBinding per team and group
for (const [team, cfg] of Object.entries(teams)) {
const t = new TeamNamespace(team, cfg);
for (const group of cfg.groups ?? []) {
new k8s.rbac.v1.RoleBinding(`${team}-${group}`, {
metadata: { name: `edit-${group}`, namespace: t.namespace },
roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: "ClusterRole", name: "edit" },
subjects: [{ apiGroup: "rbac.authorization.k8s.io", kind: "Group", name: group }],
});
}
}As duas versões passaram nos testes, com o limite de PVC apenas em payments, e os bindings do HCL ficaram com as chaves payments/payments-devs, payments/payments-oncall e search/search-devs. O HCL funciona, mas cada um destes padrões é mais uma coisa que o revisor tem de aprender.
O perigo vai no sentido contrário. Um programa Pulumi corre por inteiro em cada preview:
- Chamadas de rede durante o plano. Se fores buscar a lista de equipas a uma API interna, cada preview depende de essa API responder sempre da mesma forma. Guarda os dados no repositório ou usa a configuração do stack.
- Não determinismo. Marcas temporais ou valores aleatórios em nomes ou entradas produzem um diff a cada execução.
- Efeitos secundários. O código que escreve ficheiros ou chama webhooks também corre durante o
pulumi preview, e não só nopulumi up.
O HCL torna estes erros mais difíceis, não impossíveis: também tem timestamp() e uuid(). A nossa regra para código Pulumi: as entradas vêm da configuração, de ficheiros versionados ou dos outputs de outros recursos, e a revisão rejeita tudo o resto.
Reutilização e revisão: o que vê quem revê
Na reutilização, um módulo de Terraform é fixado por tag de Git ou versão do registry e serve tanto o Terraform como o OpenTofu; desenhar essas fronteiras entre módulos é boa parte do nosso trabalho de consultoria de Terraform. Um componente Pulumi em TypeScript é distribuído como pacote npm para outros programas TypeScript.
Na revisão, fizemos a mesma alteração nas duas: subir o CPU de payments de 8 para 12 e mudar o nome da equipa search para discovery. O plano do Terraform, obtido com terraform test -verbose e o mock provider (excerto):
# module.team["payments"].kubernetes_resource_quota_v1.this will be updated in-place
~ resource "kubernetes_resource_quota_v1" "this" {
id = "74w79olp"
~ spec {
~ hard = {
~ "requests.cpu" = "8" -> "12"
# (2 unchanged elements hidden)
}
}
# (1 unchanged block hidden)
}
# module.team["search"].kubernetes_namespace_v1.this will be destroyed
# (because module.team["search"] is not in configuration)
Plan: 3 to add, 1 to change, 3 to destroy.O preview do Pulumi, corrido contra um provider que escreve os manifestos numa diretoria em vez de num cluster (excerto):
~ kubernetes:core/v1:ResourceQuota: (update)
[id=payments/team-quota]
~ spec: {
~ hard: {
~ requests.cpu: "8" => "12"
}
}
- kubernetes:core/v1:Namespace: (delete)
[id=search]O resumo indica 4 a criar, 1 a atualizar e 4 a eliminar: o Pulumi conta o próprio componente como um recurso.
Ambos mostram a armadilha: mudar o nome de uma chave do mapa elimina um namespace e, com ele, tudo o que lá corre. Há duas diferenças que importam a quem revê:
- Um plano do Terraform guardado com
-outé aplicado exatamente como foi aprovado, eterraform show -jsontransforma-o numa evidência de alteração que se pode arquivar. O--save-plando Pulumi continua marcado como funcionalidade em preview, por isso opulumi upnormalmente volta a calcular o diff no momento de aplicar. - Revê o plano, e não só o diff do código. No Pulumi isto importa mais: uma alteração numa função auxiliar pode mexer em recursos longe das linhas que um pull request toca.
Não deixes a armadilha entregue ao olho humano. Este passo de CI falha quando uma alteração eliminaria um namespace:
# Terraform or OpenTofu
terraform plan -input=false -out=tfplan
terraform show -json tfplan | jq -r '.resource_changes[]
| select(.type == "kubernetes_namespace_v1")
| select(.change.actions | index("delete")) | .address' > deleted.txt
# Pulumi
pulumi preview --json | jq -r '.steps[]
| select(.op | test("delete|replace"))
| select(.oldState.type == "kubernetes:core/v1:Namespace") | .urn' > deleted.txt
# Either tool
if [ -s deleted.txt ]; then echo "This change deletes namespaces:"; cat deleted.txt; exit 1; fiNo código, lifecycle { prevent_destroy = true } faz o Terraform recusar esses planos; remover uma equipa falha então com "Instance cannot be destroyed". A opção protect: true do Pulumi faz o mesmo. Para refatorizações que mantêm o objeto, como mover um recurso para um módulo ou componente, usa blocos moved em HCL ou a opção aliases no Pulumi; mudar o nome do próprio namespace recria-o sempre. Integrar estes controlos nos pipelines faz parte do nosso trabalho de consultoria de CI/CD.
Drift, importação e migração entre ferramentas
O drift é uma alteração feita fora da ferramenta. Verifica-o de forma agendada:
# Terraform or OpenTofu: show drift without proposing changes
terraform plan -refresh-only
# Exit code 0 = no changes, 1 = error, 2 = changes
terraform plan -detailed-exitcode
# Pulumi: show what a refresh would change, without writing state
pulumi refresh --preview-only
# Fail if code and reality differ
pulumi preview --refresh --expect-no-changesAceitar o drift (apply -refresh-only ou pulumi refresh escrevem a realidade no estado) e revertê-lo (aplicar o código) são decisões diferentes; toma uma de propósito.
Para adotar namespaces existentes, o Terraform 1.5 acrescentou os blocos import e a 1.7 passou a permitir for_each neles. Os formatos de ID são os que o provider documenta: o nome para um namespace e NAMESPACE/NAME para uma quota.
import {
for_each = var.teams
to = module.team[each.key].kubernetes_namespace_v1.this
id = each.key
}
import {
for_each = var.teams
to = module.team[each.key].kubernetes_resource_quota_v1.this
id = "${each.key}/team-quota"
}terraform plan -generate-config-out=generated.tf escreve HCL para os recursos importados sem configuração; as duas CLIs continuam a marcá-lo como experimental. No Pulumi, pulumi import TYPE NAME ID adota um recurso e imprime o respetivo código, --file aceita uma lista em JSON e pulumi preview --import-file import.json cria um rascunho dessa lista a partir das criações que o teu programa planeia. O que é importado fica protegido por omissão.
Migrar entre ferramentas:
- Do Terraform para o OpenTofu: o mesmo HCL; o nosso módulo e os nossos testes correram sem alterações. As funcionalidades acrescentadas desde o fork diferem nos dois sentidos, por isso verifica quais o teu código usa.
- Do Terraform para o Pulumi:
pulumi convert --from terraform --language typescript --out DIRtraduz HCL. No stack acima (plugin do conversor 1.4.0) gerou um componente e um ciclo, mas otsccom o próprio tsconfig do conversor reportou 11 erros, entre eles:tostring()passou a ser uma chamada anotImplemented(...)que lança uma exceção em tempo de execução, as referências ao namespace passaram a ser umthis.apply(...)inválido e o blocolimitdo LimitRange não foi renomeado paralimits. Trata o resultado como um primeiro rascunho.pulumi import --from terraform ./terraform.tfstateadota os recursos registados num ficheiro de estado do Terraform. - Lado a lado: um programa Pulumi consegue ler os outputs raiz de um estado do Terraform, por isso podes migrar camada a camada. Isto leu o nosso estado de teste e exportou
["payments", "search"]:
import * as terraform from "@pulumi/terraform";
const legacy = terraform.state.getLocalReferenceOutput({
path: "../../tf-out/terraform.tfstate",
});
export const legacyNamespaces = legacy.outputs.apply((o) => o["namespaces"] as string[]);Para estado remoto, o pacote tem getRemoteReference. Um caminho de saída testado é o que impede a escolha de ferramenta de se tornar lock-in; o nosso guia para evitar o vendor lock-in aplica o mesmo teste aos serviços cloud.
Licenças: Terraform, OpenTofu e Pulumi
- Terraform: a 10 de agosto de 2023, o fabricante anunciou que as versões futuras passariam da Mozilla Public License 2.0 para a Business Source License 1.1. O ficheiro de licença indica o Terraform 1.6.0 e posteriores. O uso em produção é permitido, exceto oferecer o Terraform a terceiros de forma alojada ou embebida em concorrência com as versões pagas do fabricante. Cada versão passa a MPL 2.0 quatro anos depois de ser publicada.
- OpenTofu: o fork comunitário do código do Terraform com licença MPL, e continua MPL 2.0. Desde 23 de abril de 2025 é um projeto sandbox da fundação que também aloja o Kubernetes.
- Pulumi: a CLI, o motor e os SDKs são Apache 2.0. O serviço gerido do fabricante é opcional, com um escalão gratuito e escalões pagos; os backends autogeridos não têm restrições.
Se o Terraform gere a tua própria infraestrutura, a BSL não muda nada no dia a dia. Importa se vendes um produto ou serviço alojado construído sobre ele, ou se a tua política jurídica restringe as licenças sobre as quais podes construir. Nesse caso, pede aos teus juristas que leiam a cláusula de utilização, ou muda para o OpenTofu, que não obriga a reescrever nada.
Uma regra de decisão
Responde por ordem e para na primeira que decidir:
- 1Já usas Terraform com módulos que funcionam e revisão de planos? Fica no HCL. Se o problema for a licença, muda para o OpenTofu, não para uma reescrita.
- 2Precisas de ficheiros de plano como evidência de alterações, porque um auditor recolhe amostras das alterações? Escolhe Terraform ou OpenTofu: um plano guardado e aplicado tal como foi aprovado é mais fácil de defender do que um diff recalculado no momento de aplicar.
- 3Quem o vai manter daqui a dois anos? Uma equipa de plataforma ou de operações cujos membros não escrevem todos TypeScript, Python ou Go diariamente: HCL. Engenheiros de produto que são donos tanto dos serviços como da infraestrutura e reveem código nessa linguagem diariamente: Pulumi. O nosso guia para criar uma equipa de platform engineering trata de quem deve ser dono desta camada.
- 4Quanta lógica dinâmica? Escreve primeiro o módulo mais difícil. Se precisar de mais do que um par de blocos
dynamicou de padrões comflatten, o Pulumi vai ler-se melhor. Umfor_eachsobre um mapa não é motivo para mudar. - 5Estás a construir uma plataforma de autosserviço que aprovisiona a partir do teu próprio serviço? A Automation API do Pulumi corre operações do Pulumi a partir do teu código em vez de chamar uma CLI, uma necessidade comum em platform engineering.
- 6Os segredos do estado têm de ser ilegíveis para quem lê o backend? Os segredos do Pulumi ou a encriptação de estado do OpenTofu resolvem isso hoje; com o Terraform, usa argumentos só de escrita onde os providers os oferecerem e restringe o acesso ao backend.
Se nada disto decidir, a nossa opção por omissão é HCL com OpenTofu ou Terraform: quem revê pode aprovar uma alteração lendo o plano, não um programa. Escolhas o que escolheres, escreve os testes e o controlo de eliminações no primeiro mês; são eles, e não a linguagem, que decidem como corre o segundo ano.
Pede uma avaliação gratuita de Infraestrutura como Código
Se estás a escolher entre Terraform, OpenTofu e Pulumi, ou a viver com uma escolha feita há anos, a nossa equipa de consultoria de Terraform revê a tua configuração sem custos. A avaliação cobre:
- Estado e segredos: onde vive o teu estado, quem o consegue ler, que segredos lá estão em texto simples e como se comportam os bloqueios.
- Testes e revisão: se os teus módulos ou componentes têm testes que correm sem cluster, e se a CI bloqueia alterações destrutivas antes de chegarem a produção.
- Adequação da ferramenta e migração: como o teu código se posiciona face à regra de decisão acima e o que implicaria passar para o OpenTofu ou para o Pulumi, se se justificar.
Recebes por escrito uma lista de conclusões e próximos passos, ordenada por risco, sem compromisso.
Pede a tua avaliação gratuita de Infraestrutura como Código, ou vê como fazemos platform engineering sobre essa base.