Elido
14 min de leituraEngenharia
Essencial

Gerir as suas ligações curtas como Terraform

Lançámos o único provider Terraform no espaço dos encurtadores de URL - terraform-provider-elido. Eis o que faz, como funciona o ciclo de vida do recurso e as escolhas de engenharia por trás dele.

Marius Voß
DevRel · edge infra
Diagrama de três etapas: configuração HCL a fluir para terraform-provider-elido, depois para a superfície REST do api-core, com um painel lateral a listar os quatro recursos e fontes de dados lançados hoje

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
}
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, com PATCH numa alteração de slug e substituição apenas quando workspace_id ou domain_id muda para uma rota de edge diferente

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?

A deteção de desvio compara três entradas: o estado desejado no plano HCL, o estado registado no Terraform e a leitura em tempo real do api-core. Quando a leitura em tempo real diverge, o provider propõe um PATCH corretivo; os campos com valores padrão do servidor são mantidos com o modificador de plan UseStateForUnknown para que nunca apareçam como desvio falso

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.

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 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#

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

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