Elido
13 min di letturaIngegneria
Pilastro

Gestisci i tuoi short link come Terraform

Abbiamo rilasciato l'unico provider Terraform nello spazio degli URL shortener - terraform-provider-elido. Ecco cosa fa, come funziona il ciclo di vita delle risorse e i compromessi ingegneristici dietro di esso.

Marius Voß
DevRel · edge infra
Three-stage diagram: HCL config flowing into terraform-provider-elido, then into the api-core REST surface, with a side panel listing the four resources and data sources shipped today

Farò una piccola affermazione, poi la supporterò. Nessun URL shortener attualmente spedisce un provider Terraform di prima classe. Bitly, TinyURL, Rebrandly, Short.io, Dub.co - tutti e cinque pubblicano API REST, diversi pubblicano webhook, nessuno pubblica un terraform-provider-*. Su GitHub esiste un provider community per l'API v3 di Bitly; non è mantenuto e copre forse un quarto della superficie API. Questo è il gap.

Qualche settimana fa ci siamo messi a lavorare per colmarlo. Il risultato è terraform-provider-elido, che l'API Elido espone oggi come elido_link (risorsa), elido_workspace (data source) e elido_custom_domain (data source per ora - continua a leggere). Quello che segue è un tour di cosa è stato rilasciato, le scelte ingegneristiche dietro di esso e le parti che abbiamo deliberatamente non rilasciato in v0.1.0. Il provider è open source con la stessa licenza del resto di Elido e risiede in tools/terraform-provider-elido/.

L'argomento è breve. Se gestisci redirect di marketing, hai già altri pezzi di infrastruttura che convergono sulla stessa campagna:

  • Un record DNS Cloudflare che punta a una landing page.
  • Un bucket S3 e una distribuzione CloudFront che serve quella landing page.
  • Un servizio Lambda o Cloud Run che genera URL firmati.
  • Un tag di campagna incorporato in Google Tag Manager o Segment.

Tutti e cinque, nel 2026, sono gestiti come Terraform. Il short link che si trova in cima al funnel - il vero punto di ingresso su cui un utente clicca - è in un documento Google. Questo gap è da dove viene il drift. Una landing page viene deprecata e il redirect che vi punta continua a esistere, accumulando 404, fino a quando qualcuno non invia un messaggio al marketing su Slack.

Puoi correggere questo gap in due modi. Puoi scrivere uno script glue in TypeScript che si interpone tra l'output Terraform e la nostra API REST. Funziona; abbiamo clienti che lo fanno esattamente così. Oppure possiamo darti un vero provider Terraform, dove il redirect è un blocco resource accanto al tuo record Cloudflare, e terraform plan / terraform destroy lo conoscono allo stesso modo in cui conoscono tutto il resto. Abbiamo scelto il secondo percorso. Il primo era già a tuo carico.

Cosa fa terraform-provider-elido oggi#

La superficie minima della v0.1.0, in HCL:

terraform {
  required_providers {
    elido = {
      source  = "elidoapp/elido"
      version = "~> 0.1"
    }
  }
}

provider "elido" {
  # api_url   defaults to https://api.elido.app
  # api_token reads 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 di vita dichiarativo del link: un blocco HCL elido_link alimenta terraform plan, che produce un diff, poi terraform apply chiama la superficie REST api-core di Elido per creare, aggiornare o eliminare il link, con PATCH su una modifica dello slug e sostituzione solo quando workspace_id o domain_id si sposta su un route edge diverso

terraform apply e hai finito. Il rilevamento del drift funziona sui campi che l'API restituisce. Rinominare l'etichetta della risorsa Terraform, o cambiare lo slug durante l'esecuzione, non forza una sostituzione - il provider emette un PATCH contro lo stesso ID numerico. Cambiare workspace_id o domain_id forza la sostituzione, perché in quel punto stai parlando di un route edge diverso. Questo è il ciclo di vita di buon senso, ed è ciò verso cui le guide del plugin framework di HashiCorp ti spingono.

La forma del rollout in blocco è la parte che giustifica il lavoro per la maggior parte dei team:

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]]
}

Venti link, un apply. Elimina il blocco, venti eliminazioni, un apply. Questo è più o meno il caso d'uso emerso in tre note di chiamata clienti lo scorso trimestre: il marketing vuole link UTM per canale e per regione per un lancio, l'ingegneria costruisce uno script Sheets-to-API ogni volta, lo script diventa obsoleto, l'autore dello script lascia l'azienda. La forza di Terraform qui non è la novità - è che abbiamo reso noioso il pattern.

La guida completa con il riferimento agli attributi e gli esempi di importazione si trova su /docs/guides/terraform. Il sorgente del provider include examples/main.tf che è una versione più elaborata dello snippet precedente.

Come è costruito il provider#

Circa 600 righe di Go, di cui ~200 sono definizioni di schema. La struttura:

tools/terraform-provider-elido/
├── main.go                                   # plugin entrypoint
├── internal/provider/
│   ├── provider.go                           # config + auth
│   ├── link_resource.go                      # CRUD + import
│   ├── workspace_data_source.go              # GET /v1/workspaces/{id}
│   ├── custom_domain_data_source.go          # GET /v1/workspaces/{id}/domains
│   ├── helpers.go                            # tag conversion
│   └── provider_test.go                      # 7 unit tests
├── go.mod                                    # depends on packages/sdk-go
├── .goreleaser.yml                           # signed-checksum builds
├── terraform-registry-manifest.json          # protocol_versions: ["6.0"]
├── Makefile                                  # build + install-local + testacc
└── examples/main.tf

Alcune scelte che vale la pena evidenziare.

Usiamo il plugin framework, non il legacy SDK. HashiCorp ha esplicitamente indirizzato i nuovi provider verso terraform-plugin-framework nel 2023. La maggior parte dei provider popolari (aws, cloudflare, google) sono in fase di migrazione; quelli più piccoli e più recenti sono nativi del framework. Costruire greenfield sul legacy SDK avrebbe significato accollarsi un compito di migrazione nel momento stesso del rilascio. Abbiamo evitato la migrazione non creandola. Il framework ha un sistema di tipi più rigoroso, una vera validazione dello schema a livello di protocollo del plugin e un modello di pianificazione molto più pulito (PlanModifiers invece di callback CustomizeDiff). Per un provider piccolo, il gap ergonomico è ampio.

Il provider non duplica l'SDK. Ogni metodo delle risorse delega a packages/sdk-go, che è lo stesso SDK che pubblichiamo per le integrazioni Go semplici. Il provider è, per design, un sottile adattatore Schema-to-SDK. Questo ha due conseguenze. Quella positiva: qualsiasi bug che risolviamo nell'SDK arriva nel provider gratuitamente. Quella negativa: qualsiasi lacuna nell'SDK è una lacuna nel provider. L'esempio onesto è sui domini personalizzati. api-core non espone ancora POST/DELETE per /v1/workspaces/{id}/domains; il percorso di scrittura risiede in domain-manager dietro la dashboard. Finché api-core non fa il proxy delle scritture, l'SDK non ha Domains.Create, e il provider non ha una risorsa elido_custom_domain - solo un data source che cerca un dominio esistente per hostname. Colmeremo quella lacuna nella v0.2.0; lo shim proxy è una modifica di meno di una settimana e la PR dell'SDK + provider è già bozzata.

L'auth ha la stessa forma di ogni altro client Elido. Bearer API key nell'intestazione Authorization, con fallback su ELIDO_API_TOKEN nell'ambiente. Non esponiamo l'auth tramite cookie o X-Dev-User-ID nel provider; queste sono comodità di sviluppo locale che non hanno nulla da fare in IaC dove la configurazione risiede nel version control e viene eseguita in CI. Il tuo CI o ha un token o non ce l'ha.

Rilevamento del drift: la parte più difficile di quanto sembra#

Se hai letto oltre le parti ovvie, questa è la sezione che vale la pena leggere. Il diffing di Terraform è fondamentalmente una questione di: dato quello che l'utente ha scritto (Plan), e quello che il server ha restituito l'ultima volta (State), e quello che il server restituisce ora (Read), cosa dovremmo proporre di fare?

Il rilevamento del drift confronta tre input: lo stato desiderato nel plan HCL, lo stato Terraform registrato e la lettura live da api-core. Quando la lettura live diverge, il provider propone un PATCH correttivo; i campi con default del server vengono mantenuti con il plan modifier UseStateForUnknown in modo da non apparire mai come falso drift

Per una risorsa come elido_link, tre cose rendono questo non banale:

Campi Optional + Computed con default del server. L'utente può omettere redirect_status. Il server imposta 302. La successiva Read restituisce 302. Senza attenzione, questo sembra drift ad ogni plan - "ho richiesto niente, ho ricevuto 302, propongo di impostarlo di nuovo su niente". Il framework ti dà un plan modifier UseStateForUnknown che dice "se non ho un valore pianificato, mantieni quello che è nello stato". Lo usiamo su ogni campo con default lato server. Suona banale; è la fonte dei bug più frequenti nei provider nell'ecosistema ("provider produced inconsistent result after apply").

Tag con normalizzazione lato server. La nostra API archivia i tag come un set; Terraform li vede come una lista ordinata. Per ora puntiamo su questo. Il server preserva l'ordine all'echo, quindi il diff è stabile in pratica, ma un utente che riordina i tag in HCL vedrà un aggiornamento no-op. Questo è il comportamento corretto; l'alternativa - ordinare silenziosamente in input - significherebbe che terraform plan e terraform apply non concordano su cosa cambia, che è il peccato capitale di Terraform. Rivedremo se i clienti reali si lamentano. La guida alle best practice di HashiCorp è fermamente dalla parte del "non fare nulla di sorprendente".

Status come tri-state. Un link può essere active, paused o archived. Impostare status = "paused" in HCL ma non al Create (il server usa come default active) significa che dobbiamo emettere un PATCH successivo all'interno dello stesso Create. Questo è implementato come un passaggio di riconciliazione post-Create - tienilo a mente se stai leggendo il sorgente. L'alternativa - esporre lo status come risorsa separata (elido_link_status con chiave link_id) - è quello che fa il provider AWS per alcune risorse. L'abbiamo considerato; per un campo opzionale, il costo supera il beneficio. Se aggiungiamo una seconda manopola post-Create, ripensaremo.

Import. terraform import elido_link.spring_campaign 42:7 - ovvero <workspace_id>:<link_id>. Scegliamo la forma separata da due punti perché il callback ImportState del framework ti dà una singola stringa e la analizzi tu stesso. La forma <id>:<id> è comune nei provider che indicizzano le risorse per una tupla - vedi la documentazione di importazione di google_compute_instance per il riferimento canonico. Siamo deliberati nel non sovraccaricare lo slug human-readable; lo stato della risorsa è indicizzato dall'ID numerico, ed è l'unica cosa che dovresti mettere in un import.

Test, CI, il registry#

La suite di unit test (7 test oggi) copre il livello di validazione dello schema più gli helper a funzione pura - splitImportID, linkToModel, apiErrorString, optString. Viene eseguita in 0,5 secondi e presidia ogni PR attraverso la stessa matrice go che costruisce i nostri 13 servizi. C'è anche un target testacc che gira contro un api-core live quando è impostato TF_ACC=1, ma è opt-in: richiede un token e non lo eseguiamo ad ogni commit perché ogni test crea e cancella un link reale. Il testing framework di HashiCorp documenta il pattern; non ci discostiamo.

Workflow GitOps per i short link: una modifica alla configurazione HCL apre una pull request, la CI esegue terraform plan e i test unitari Go, un revisore approva il diff e fa il merge su main, e il merge attiva terraform apply contro api-core in modo che l'edge recepisca la modifica al prossimo clic

La pipeline di release è collegata a goreleaser con la matrice di build esatta che il Terraform Registry si aspetta: linux, darwin, freebsd, windows × amd64/arm64 (più arm e 386 su Linux), SHA256SUMS sugli archivi, firma GPG sul SHA256SUMS e un terraform-registry-manifest.json che dichiara protocol_versions: ["6.0"]. Crea un tag per un commit terraform-provider-vX.Y.Z, il workflow di GitHub Actions esegue goreleaser release --clean, e la Release GitHub va live. Il Terraform Registry fa il polling della release con la propria schedulazione e ne fa l'ingestione. L'unica cosa attualmente mancante è la chiave GPG - stiamo coniando una dedicata ai release del provider questa settimana, il che significa che v0.1.0 arriverà sul registry circa in contemporanea con questo articolo.

Nel frattempo, installa tramite dev_overrides in ~/.terraformrc:

provider_installation {
  dev_overrides {
    "elidoapp/elido" = "/Users/<you>/.terraform.d/plugins/elidoapp/elido"
  }
  direct {}
}

Poi make install-local da tools/terraform-provider-elido/, e terraform plan risolve il binario direttamente senza terraform init. Questo è il pattern ufficiale HashiCorp per lo sviluppo del provider e funziona altrettanto bene come percorso di installazione provvisorio fino alla v1.0.0.

Cosa è deliberatamente non nella v0.1.0#

Tre cose che abbiamo considerato, non rilasciato e che vogliamo menzionare in modo che nessuno sia sorpreso.

Nessun elido_custom_domain come risorsa. Discusso sopra. Il data source è sufficiente per concatenare domain_id in elido_link, che è il caso d'uso portante; la gestione del ciclo di vita completo attende api-core. ETA: v0.2.0, metà 2026.

Nessun elido_folder, nessun elido_api_key. L'SDK li ha entrambi; abbiamo scelto di non aggiungere Schema nella v0.1.0 perché i loro cicli di vita non sono dove si trova il dolore del cliente. Le cartelle sono metadati organizzativi; le API key vengono tipicamente emesse una volta e ruotate tramite la dashboard. Le aggiungeremo quando qualcuno lo chiederà.

Nessuna generazione di codice dalla spec OpenAPI. HashiCorp spedisce terraform-plugin-codegen-openapi come tool beta. Lo abbiamo provato sulla nostra spec; gli Schema generati sono mediocri - ogni campo nullable diventa Optional + Computed, ogni lista diventa un Set, il risultato richiede quanto meno fixup di uno Schema scritto a mano ed è più difficile da far evolvere. Con tre risorse sul tavolo, quello scritto a mano vince. Rivisiteremo il generatore tra sei mesi quando più colleghi lo avranno testato in produzione.

Cosa si è rotto mentre lo costruivamo#

Tre cose che abbiamo sbagliato al primo tentativo.

La prima riguardava lo stato su Optional + Computed. Inizialmente modellavamo title come una semplice stringa Optional. I clienti che la omettevano dall' HCL ottenevano un Create pulito - e poi ogni successivo terraform plan proponeva di impostarla di nuovo su null, perché il server archiviava una stringa vuota e Terraform la leggeva come drift. La correzione era il plan modifier UseStateForUnknown; la lezione era che l'interpretazione del provider di "l'utente non ha specificato" deve corrispondere all'idea del server di "valore predefinito". La documentazione del framework avverte di questo nell'introduzione; la prima volta l'abbiamo letta distrattamente. Te lo scriviamo per risparmiarti l'imbarazzo.

La seconda riguardava il formato di importazione. Inizialmente avevamo rilasciato <workspace_id>/<link_id> con uno slash, con l'idea che i percorsi si leggano più naturalmente. Al framework non dava problemi; i linter HCL e i terminali sì. Un percorso con due slash all'interno di un singolo argomento quotato dalla shell diventa qualcosa che sembra un errore di battitura nei ticket di supporto. Siamo passati ai due punti, che non hanno ambiguità e corrispondono alle convenzioni del provider di Google. Lezione: le stringhe di importazione sono UI rivolta agli utenti, progettale come UI.

La terza riguardava l'ordinamento dei tag. Discusso sopra - abbiamo puntato, e continueremo a puntare finché qualcuno non chiede. La versione che stavamo quasi per rilasciare ordinava silenziosamente i tag in input, il che faceva sì che terraform plan non riportasse modifiche quando il cliente li aveva chiaramente riordinati. È un'esperienza peggiore di un diff rumoroso; l'abbiamo rilevato durante il test interno. Vale la pena dirlo perché la tentazione di "essere utili" normalizzando l'input dell'utente è costante quando scrivi un provider, ed è quasi sempre la scelta sbagliata.

Come usarlo con il resto di Elido#

Il provider è una forma. Le altre forme esistono ancora e non stanno andando da nessuna parte:

  • L'API REST è la fonte di verità. Tutto quello che fa il provider è realizzabile anche con curl.
  • Il Go SDK è quello che il provider stesso usa internamente; puoi includerlo come libreria.
  • I SDK TypeScript e Python coprono la stessa superficie per il linguaggio in cui ti trovi.
  • L'endpoint GraphQL copre le stesse letture con un singolo round-trip quando ne hai bisogno conformato al tuo schermo.

Scegli quello che si adatta alla forma del problema. Terraform è giusto quando hai un ciclo di vita da gestire. L'SDK è giusto quando hai uno script. L'API REST è giusta quando fai una cosa una volta sola. Pensiamo che dovrebbe essere così ovvio; manterremo tutti e quattro funzionanti.

Se hai un pattern Terraform preferito che ci manca - importazioni in blocco da CSV tramite for_each su un blocco data "external", un for_each conformato a un'API Linear per il tracciamento delle campagne, un modulo wrapper per il caso agenzia-che-gestisce-più-tenant - apri una issue nel repository GitHub con il label area:terraform. Il provider esiste per rendere noiosi quei pattern; vogliamo sapere quali sembrano ancora sorprendenti.

Da dove iniziare#

Se hai letto questo e vuoi provarlo: installa il provider secondo la guida, puntalo a un workspace sandbox, scrivi resource "elido_link" per il redirect che hai sempre voluto dichiarare nel codice, e terraform apply. Scommettiamo un caffè che la prima cosa che ti sorprende, in senso positivo, è che terraform destroy funziona esattamente come ti aspettavi.

Se hai letto questo e vuoi confrontarci con le alternative - c'è un write-up più lungo nel nostro post Bitly alternatives feature gap, e il confronto fianco a fianco su /compare/vs-bitly mostra dove si colloca Terraform nella matrice. La matrice si è accorciata per loro da quando è uscito questo articolo.

  • Marius

Correlato sul blog#

Prova Elido

Incolla un URL, ottieni un link breve

Senza registrazione. Il link vive 30 giorni. Iscriviti per conservarlo.

Gratis, nessuna registrazione richiesta · 2 al giorno

Prova Elido

Accorciatore di URL ospitato nell'UE: domini personalizzati, analisi approfondite e API aperta. Piano gratuito - senza carta di credito.

Tag
terraform
infrastructure as code
url shortener
developer experience
devops
iac

Continua a leggere