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/.
Perché i short link appartengono a Terraform#
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
}
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?
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.
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