14 min de leituraEngenharia
Essencial

Gerencie seus links curtos como código com Terraform

Lançamos o único provider Terraform entre encurtadores, o terraform-provider-elido. O que faz, o ciclo de vida do recurso e as escolhas de engenharia.

Marius Voß
DevRel · edge infra

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.

A página da integração do Terraform no Elido com os passos de configuração do provider para gerenciar links curtos como código

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
}
Ciclo de vida declarativo de uma ligação: um bloco HCL elido_link alimenta o terraform plan, que produz um diff, depois o terraform apply chama a superfície REST do api-core para criar, atualizar ou eliminar a ligação.

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?

A detecção de desvio compara três entradas: o estado desejado no plano HCL, o estado registrado no Terraform e a leitura em tempo real do api-core.

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.

Fluxo GitOps para ligações curtas: uma edição à configuração HCL abre um pull request, o CI corre terraform plan e os testes unitários em Go, um revisor aprova o diff e faz merge para main, e o merge despoleta o terraform apply contra o api-core para que o edge receba a alteração no próximo clique

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

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.

Experimente o Elido

Encurtador de URL hospedado na UE: domínios personalizados, análises profundas e API aberta. Plano gratuito - sem cartão de crédito.

Tags
terraform
infrastructure as code
url shortener
developer experience
devops
iac

Continuar lendo