Vou fazer uma pequena afirmação e depois comprová-la. Nenhum encurtador de URL
dispõe atualmente de um provider Terraform de primeira classe. Bitly, TinyURL,
Rebrandly, Short.io, Dub.co - todos os cinco publicam APIs REST, vários
publicam webhooks, nenhum publica um terraform-provider-*. Existe um provider
da comunidade para a API v3 do Bitly no GitHub; não tem manutenção e
cobre talvez um quarto da superfície da API. Essa é a lacuna.
Há algumas semanas sentámos para a fechar. O resultado é terraform-provider-elido, que a API da Elido expõe hoje como elido_link (recurso), elido_workspace (fonte de dados) e elido_custom_domain (uma fonte de dados em v0.1.0 - continue a ler). O que se segue é uma visita ao que foi lançado, às escolhas de engenharia por trás disso, e às partes que deliberadamente não lançámos em v0.1.0. Atualização, novembro de 2026: o provider no repositório está agora na v0.2.0, que adiciona elido_custom_domain como recurso. O provider ainda não está publicado: vive no nosso monorepo em tools/terraform-provider-elido/, e o lançamento público vem em breve.
Por que as ligações curtas pertencem ao Terraform
O argumento é breve. Se está gerenciando redirecionamentos de marketing, já tem outras peças de infraestrutura que convergem para a mesma campanha:
- Um registro DNS no Cloudflare apontando para uma página de destino.
- Um bucket S3 e uma distribuição CloudFront servindo essa página.
- Um Lambda ou um serviço Cloud Run gerando URLs assinados.
- Uma tag de campanha integrada no Google Tag Manager ou no Segment.
Todas as cinco, em 2026, são geridas como Terraform. A ligação curta que se situa na frente do funil - o ponto de entrada real em que um usuário clica - está num Google Doc. Essa lacuna é de onde vem o desvio. Uma página de destino é descontinuada e o redirecionamento que aponta para ela continua ativo, a acumular erros 404, até alguém contatar marketing no Slack.
Pode corrigir essa lacuna de duas formas. Pode escrever um script de ligação em
TypeScript que fica entre o output do seu Terraform e a nossa API REST.
Funciona; temos clientes a fazer exatamente isso. Ou podemos lhe dar
um provider Terraform real, onde o redirecionamento é um bloco resource
ao lado do seu registro Cloudflare, e o terraform plan / terraform destroy conhece-o da mesma forma que conhece todo o resto.
Escolhemos o segundo caminho. O primeiro já estava do seu lado.
O que o terraform-provider-elido faz hoje
A superfície mínima de v0.1.0, em HCL:
terraform {
required_providers {
elido = {
source = "elidoapp/elido"
version = "~> 0.1"
}
}
}
provider "elido" {
# api_url por padrão https://api.elido.app
# api_token lê ELIDO_API_TOKEN
}
data "elido_workspace" "main" {
id = 42
}
data "elido_custom_domain" "links" {
workspace_id = data.elido_workspace.main.id
hostname = "links.example.com"
}
resource "elido_link" "spring_campaign" {
workspace_id = data.elido_workspace.main.id
domain_id = data.elido_custom_domain.links.id
slug = "spring-2026"
destination_url = "https://example.com/landing/spring"
title = "Spring 2026 email campaign"
tags = ["spring-2026", "email"]
redirect_status = 301
}
terraform apply e está feito. (A origem elidoapp/elido é o endereço previsto no registro e ainda não está publicada; veja a seção de instalação abaixo.) A detecção de desvio funciona nos campos que a API devolve. Renomear o rótulo do recurso Terraform, ou alterar o slug a meio da execução, não força uma substituição - o provider emite um PATCH contra o mesmo ID numérico. Alterar workspace_id ou domain_id força a substituição, porque nesse ponto está falando de uma rota de edge diferente. Este é o ciclo de vida de bom senso, e é para onde os guias do plugin framework da HashiCorp o orientam.
A forma de implantação em massa é a parte que justifica o trabalho para a maioria das equipes:
locals {
channels = ["email", "twitter", "linkedin", "reddit", "hn"]
regions = ["us", "eu", "apac", "latam"]
}
resource "elido_link" "campaign_launch" {
for_each = {
for pair in setproduct(local.channels, local.regions) :
"${pair[0]}-${pair[1]}" => pair
}
workspace_id = data.elido_workspace.main.id
domain_id = data.elido_custom_domain.links.id
slug = "launch-${each.key}"
destination_url = "https://example.com/launch?ch=${each.value[0]}&r=${each.value[1]}"
tags = ["launch-2026", each.value[0], each.value[1]]
}
Vinte ligações, um apply. Apague o bloco, vinte eliminações, um apply. Este é aproximadamente o caso de uso que apareceu em três notas de chamadas de clientes no último trimestre: o marketing quer ligações UTM por canal e por região para um lançamento, a engenharia constrói um script do Sheets para a API de cada vez, o script fica desatualizado, o autor do script sai da empresa. A força do Terraform aqui não é a novidade - é que tornámos o padrão enfadonho.
O guia completo com referência de atributos e exemplos de importação está em
/docs/guides/terraform. O código-fonte do provider
inclui examples/main.tf, que é uma versão mais elaborada do
snippet acima.
Como o provider é construído
Aproximadamente 600 linhas de Go, das quais ~200 são definições de schema. A estrutura:
tools/terraform-provider-elido/
├── main.go # ponto de entrada do plugin
├── internal/provider/
│ ├── provider.go # configuração + autenticação
│ ├── link_resource.go # CRUD + importação
│ ├── workspace_data_source.go # GET /v1/workspaces/{id}
│ ├── custom_domain_data_source.go # GET /v1/workspaces/{id}/domains
│ ├── helpers.go # conversão de tags
│ └── provider_test.go # 7 testes unitários
├── go.mod # depende de packages/sdk-go
├── .goreleaser.yml # builds com checksum assinado
├── terraform-registry-manifest.json # protocol_versions: ["6.0"]
├── Makefile # build + install-local + testacc
└── examples/main.tf
Algumas escolhas merecem explicação.
Usamos o plugin framework, não o SDK legado. A HashiCorp
orientou explicitamente os novos providers para o
terraform-plugin-framework
em 2023. A maioria dos providers populares (aws, cloudflare,
google) estão a meio da migração; os mais pequenos e recentes são
nativos do framework. Construir do zero com o SDK legado teria significado
assumir uma tarefa de migração no momento em que lançássemos. Evitámos
a migração não a criando. O framework tem um sistema de tipos mais rigoroso,
validação real de schema ao nível do protocolo do plugin,
e um modelo de planeamento muito mais limpo (PlanModifiers em vez de
callbacks CustomizeDiff). Para um provider pequeno, a diferença ergonómica é grande.
O provider não duplica o SDK. Cada método de recurso delega em packages/sdk-go, o SDK de Go, publicado como o módulo github.com/elidoapp/elido-go (v0.1.0). O provider é, por design, um adaptador fino de Schema para SDK. Isso tem duas consequências. A boa: qualquer bug que corrijamos no SDK chega ao provider gratuitamente. A má: qualquer lacuna no SDK é uma lacuna no provider. O exemplo honesto foram os domínios personalizados. Quando a v0.1.0 foi lançada, o api-core não expunha POST/DELETE para /v1/workspaces/{id}/domains; a via de escrita vivia no serviço separado domain-manager por trás do dashboard, então o provider não tinha um recurso elido_custom_domain - apenas uma fonte de dados que procura um existente pelo hostname. Essa lacuna está fechada: o domain-manager foi incorporado ao api-core (ADR-0047), o api-core agora serve as rotas de criação, verificação e remoção de domínios, e o provider v0.2.0 no repositório tem um recurso elido_custom_domain que registra um hostname e espera a verificação de DNS.
A autenticação tem a mesma forma de todos os outros clientes Elido. Bearer API
key no cabeçalho Authorization, com recuo para ELIDO_API_TOKEN
no ambiente. Não expõe auth por cookie nem X-Dev-User-ID
no provider; esses são utilitários de desenvolvimento local que não têm lugar
no IaC, onde a configuração fica em controlo de versão e
corre em CI. O seu CI ou tem um token ou não tem.
Detecção de desvio: a parte mais difícil do que parece
Se chegou até aqui, esta é a seção que vale a pena ler. O diffing do Terraform é
fundamentalmente uma questão de: dado o que o usuário escreveu (Plan), e o que
o servidor devolveu da última vez (State), e o que o servidor devolve agora (Read), o que
devemos propor fazer?
Para um recurso como elido_link, três coisas tornam isso não trivial:
Campos Optional + Computed com valores padrão do servidor. O usuário pode
omitir redirect_status. O servidor preenche com 302. O próximo Read
devolve 302. Sem cuidado, isso parece desvio em cada plan -
"não pedi nada, recebi 302 de volta, proponho defini-lo para nada
novamente". O framework lhe dá um modificador de plan UseStateForUnknown
que diz "se não tenho um valor planeado, mantenha o que está no
state". Usamo-lo em todos os campos com padrão do servidor. Isso parece
trivial; é a fonte dos bugs de provider mais frequentes no
ecossistema ("o provider produziu um resultado inconsistente após apply").
Tags com normalização do lado do servidor. A nossa API armazena tags como um
conjunto; o Terraform vê-as como uma lista ordenada. Por agora cedemos neste ponto.
O servidor preserva a ordem no echo, por isso o diff é estável na
prática, mas um usuário que reordene tags em HCL verá uma
atualização sem operações. Esse é o comportamento correto; a alternativa - ordenar silenciosamente
na entrada - significaria que terraform plan e terraform apply discordariam sobre o que muda, o que é o pecado cardinal do Terraform.
Revisaremos se clientes reais se queixarem. O
guia de boas práticas
da HashiCorp está firmemente do lado do "não fazer nada surpreendente".
Status como tri-estado. Uma ligação pode ser active, paused ou
archived. Definir status = "paused" em HCL mas não no Create
(o servidor tem como padrão active) significa que temos de emitir um
PATCH de seguimento dentro do mesmo Create. Isso está implementado como uma
etapa de reconciliação pós-Create - tenha isso em mente se estiver lendo
o código-fonte. A alternativa - expor o status como um recurso separado
(elido_link_status indexado por link_id) - é o que o provider da AWS
faz para alguns recursos. Considerámos; para um
campo opcional, o custo supera o benefício. Se adicionarmos uma segunda
definição pós-Create, repensar-nos-emos.
Importação. terraform import elido_link.spring_campaign 42:7 -
isso é <workspace_id>:<link_id>. Escolhemos a forma separada por dois pontos
porque o callback ImportState do framework lhe recebe
uma única string e você analisa-a você mesmo. A forma <id>:<id> é
comum em providers que indexam recursos por uma tupla - veja a
documentação de importação do google_compute_instance
para a referência canônica. Somos deliberados em não sobrecarregar
o slug legível por humanos; o estado do recurso é indexado pelo
ID numérico, e isso é a única coisa que deve colocar numa importação.
Testes, CI, o registry
A suite de testes unitários (7 testes na v0.1.0) cobre a camada de validação de schema
mais os helpers de funções puras - splitImportID, linkToModel,
apiErrorString, optString; a v0.2.0 acrescenta testes para o recurso de domínio personalizado. Corre em cerca de meio segundo, na mesma matrix go
que constrói os nossos seis serviços Go.
Há também um alvo testacc que corre contra um api-core real
quando TF_ACC=1 está definido, mas isso é opt-in: requer um token,
e não o corremos em cada commit porque cada teste cria e
elimina uma ligação real. A
framework de testes
da HashiCorp documenta o padrão; não nos desviamos.
O pipeline de release está ligado ao goreleaser com a matrix de build exata que o Terraform Registry espera: linux, darwin, freebsd, windows × amd64/arm64 (mais arm e 386 no Linux), SHA256SUMS sobre os arquivos, assinatura GPG no SHA256SUMS, e um terraform-registry-manifest.json declarando protocol_versions: ["6.0"]. Faça tag de um commit terraform-provider-vX.Y.Z, o workflow do GitHub Actions corre goreleaser release --clean, e o GitHub Release fica disponível. O Terraform Registry verifica o release no seu próprio calendário e ingere a versão. A única coisa que falta atualmente é a chave GPG, por isso o provider ainda não está no registro; o lançamento público vem em breve.
Entretanto, instale via dev_overrides em ~/.terraformrc:
provider_installation {
dev_overrides {
"elidoapp/elido" = "/Users/<you>/.terraform.d/plugins/elidoapp/elido"
}
direct {}
}
Depois make install-local a partir de tools/terraform-provider-elido/, e o terraform plan resolve o binário diretamente sem terraform init. Este é o padrão oficial da HashiCorp para desenvolver providers, mas exige uma compilação local do provider; até ao lançamento público, o caminho prático é a API REST.
O que está deliberadamente fora da v0.1.0
Três coisas que considerámos, não lançámos, e queremos mencionar para que ninguém fique surpreendido.
Nenhum elido_custom_domain como recurso. Discutido acima. A
fonte de dados é suficiente para encadear domain_id em elido_link, que
é o caso de uso essencial. A gestão completa do ciclo de vida já
chegou à v0.2.0 do repositório como o recurso elido_custom_domain;
como o resto do provider, ainda não está no registry.
Nenhum elido_folder, nenhum elido_api_key. O SDK tem ambos; escolhemos
não adicionar Schemas em v0.1.0 porque os seus ciclos de vida não são
onde está a dor do cliente. As pastas são metadados organizacionais; as chaves API
são tipicamente emitidas uma vez e rotacionadas através do dashboard.
Adicioná-los-emos quando alguém o solicitar.
Nenhuma geração de código a partir da especificação OpenAPI. A HashiCorp fornece
terraform-plugin-codegen-openapi
como ferramenta beta. Experimentámo-la na nossa spec; os Schemas gerados são
medíocres - cada campo nulo torna-se Optional + Computed,
cada lista torna-se um Set, o resultado requer tanto trabalho de correção quanto um
Schema escrito manualmente e é mais difícil de evoluir. Com três recursos
em cima da mesa, o manual ganha. Revisitaremos o gerador daqui a
seis meses quando mais dos nossos pares o tiverem testado em produção.
O que falhou enquanto o construíamos
Três coisas que errámos na primeira tentativa.
A primeira foi o state em Optional + Computed. Inicialmente modelámos
title como uma simples string Optional. Os clientes que o omitiam do
HCL obtinham um Create limpo - e depois cada terraform plan subsequente
propunha defini-lo de volta para nulo, porque o servidor armazenava uma string vazia
e o Terraform lia isso como desvio. A correção foi o
modificador de plan UseStateForUnknown; a lição foi que a
interpretação do provider sobre "o usuário não especificou" tem de corresponder
à ideia do servidor sobre "valor padrão". A documentação do framework
avisa sobre isso na introdução; passámos pelo aviso
da primeira vez. Poupámo-lhe o embaraço ao escrevê-lo aqui.
A segunda foi o formato de importação. Inicialmente lançámos
<workspace_id>/<link_id> com uma barra, com o argumento de que os caminhos
se leem mais naturalmente. O framework não teve problemas com isso; os
linters HCL e os terminais tiveram. Um caminho com duas barras dentro de um único
argumento entre aspas no shell torna-se algo que parece um erro de digitação
nos tickets de suporte. Mudámos para dois pontos, que não têm ambiguidade
e correspondem às convenções do provider da Google. Lição: as strings de importação
são UI voltada para o usuário, projete-as como UI.
A terceira foi a ordenação de tags. Discutida acima - cedemos, e vamos
continuar a ceder até alguém pedir. A versão que quase
lançámos ordenava silenciosamente as tags na entrada, o que fazia o terraform plan
reportar sem alterações quando o cliente as tinha claramente reordenado.
Essa é uma experiência pior do que um diff ruidoso; apanhámo-lo durante
os testes internos. Vale a pena dizer porque a tentação de "ser
prestável" ao normalizar a entrada do usuário é constante quando se escreve um
provider, e é quase sempre a escolha errada.
Como usar isto com o resto da Elido
O provider é uma forma. As outras formas ainda existem e não vão a lado nenhum:
- A API REST é a fonte de verdade.
Tudo o que o provider faz também é possível com
curl. - O SDK de Go é o que o provider usa internamente; é público como
github.com/elidoapp/elido-go(v0.1.0). - Os SDKs de TypeScript e Python cobrirão a mesma superfície para a sua linguagem, quando forem lançados.
- O endpoint GraphQL cobre as mesmas leituras com uma única ida e volta quando precisar de os moldar ao seu tela.
Escolha o que se adapta à forma do problema. O Terraform é certo quando tem um ciclo de vida gerenciando. O SDK é certo quando tem um script. A API REST é certa quando está fazendo uma coisa uma vez. Achamos que deve ser assim tão óbvio; vamos manter os quatro a funcionar.
Se tiver um padrão Terraform favorito que nos falta - importações em massa de CSV via for_each sobre um bloco data "external", um for_each moldado a uma API Linear para rastreamento de campanhas, um módulo de encapsulamento para o caso de agências gerenciando múltiplos tenants - diga-nos quais quer. O provider existe para tornar esses padrões enfadonhos; queremos saber quais ainda parecem surpreendentes.
Por onde começar
Se leu isto e quer experimentar: o provider ainda não é público, por isso, por agora, leia o guia e use a API REST; quando o lançamento sair, aponte-o a um espaço de trabalho de testes, escreva resource "elido_link" para o redirecionamento que sempre quis declarar em código e execute terraform apply. Apostamos um café que a primeira coisa que o surpreende, de forma positiva, é o terraform destroy a funcionar exatamente como esperava.
Se leu isto e quer comparar-nos com as alternativas - há uma análise mais detalhada no nosso artigo sobre lacunas de funcionalidades nas alternativas ao Bitly, e a comparação lado a lado em /compare/vs-bitly mostra onde o Terraform se situa na matriz. A matriz ficou mais curta para eles desde que este artigo foi publicado.
- Marius
Relacionado no blog
- Atingir p95 < 15ms para redirecionamentos de FRA, ASH e SGP
- Por que um armazenamento colunar supera o Postgres para análise de cliques
- Importação em massa de ligações curtas a partir de uma Google Sheet (o fluxo real de campanha)
- Configurar ligações curtas com marca própria: escolha um domínio, lance numa tarde
Experimente Elido
Cole uma URL, obtenha um link curto
Sem cadastro. O link vive 30 dias. Cadastre-se para mantê-lo para sempre.
Sem registro. Crie uma conta grátis e obtenha 50 links por mês.