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 (fonte de dados por agora - 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. O provider é
código aberto sob a mesma licença do resto da Elido e encontra-se em
tools/terraform-provider-elido/.
Por que as ligações curtas pertencem ao Terraform#
O argumento é breve. Se está a gerir redirecionamentos de marketing, já tem outras peças de infraestrutura que convergem para a mesma campanha:
- Um registo DNS no Cloudflare apontando para uma página de destino.
- Um bucket S3 e uma distribuição CloudFront a servir essa página.
- Um Lambda ou um serviço Cloud Run a gerar 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 utilizador 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 contactar 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 dar-lhe
um provider Terraform real, onde o redirecionamento é um bloco resource
ao lado do seu registo 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 defeito 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 deteçã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á a falar 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 equipas:
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 que vale a pena destacar.
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 para packages/sdk-go,
que é o mesmo SDK que publicamos para integrações em Go puro. 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 são os domínios personalizados. O api-core não
expõe ainda POST/DELETE para /v1/workspaces/{id}/domains; a
via de escrita vive no domain-manager por trás do dashboard. Enquanto o
api-core não proxiar as escritas, o SDK não tem Domains.Create, e
o provider não tem um recurso elido_custom_domain - apenas uma fonte
de dados que procura um existente pelo hostname. Fecharemos
essa lacuna em v0.2.0; o shim proxy é uma alteração de menos de uma semana e o
PR do SDK + provider já está redigido.
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.
Deteção de desvio: a parte mais difícil do que parece#
Se chegou até aqui, esta é a secção que vale a pena ler. O diffing do Terraform é
fundamentalmente uma questão de: dado o que o utilizador 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 utilizador 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 dá-lhe 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 utilizador 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 a ler
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 recebe-lhe
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 hoje) cobre a camada de validação de schema
mais os helpers de funções puras - splitImportID, linkToModel,
apiErrorString, optString. Corre em 0,5 segundos e faz gate
em cada PR através da mesma matrix go que constrói os nossos 13 serviços.
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 atualmente em falta é a chave GPG - estamos a cunhar uma
dedicada a releases de providers esta semana, o que significa que
v0.1.0 aterra no registry aproximadamente ao mesmo tempo que este artigo.
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 desenvolvimento de providers
e funciona igualmente bem como caminho de instalação provisório até v1.0.0.
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 aguarda o
api-core. ETA: v0.2.0, meados de 2026.
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 utilizador 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 utilizador, 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 utilizador é 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 em Go é o que o próprio provider usa internamente; pode incluí-lo como biblioteca.
- Os SDKs em TypeScript e Python cobrem a mesma superfície para a linguagem em que se encontrar.
- O endpoint GraphQL cobre as mesmas leituras com uma única ida e volta quando precisar de os moldar ao seu ecrã.
Escolha o que se adapta à forma do problema. O Terraform é certo quando tem um ciclo de vida a gerir. O SDK é certo quando tem um script. A API REST é certa quando está a fazer 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 a gerir múltiplos tenants - abra um issue no repositório GitHub com o
rótulo area:terraform. 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: instale o provider de acordo com o
guia, aponte-o para um workspace de sandbox,
escreva resource "elido_link" para o redirecionamento que sempre quis
declarar em código, e 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 usamos o ClickHouse para análise de cliques (e não Postgres)
- 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.
Grátis, sem necessidade de registo · 2 por dia