Je vais faire une petite affirmation, puis la soutenir. Aucun raccourcisseur
d'URL ne livre actuellement un provider Terraform de premier rang. Bitly,
TinyURL, Rebrandly, Short.io, Dub.co - les cinq publient des APIs REST,
plusieurs publient des webhooks, aucun ne publie de terraform-provider-*. Un
provider communautaire pour l'API v3 de Bitly existe sur GitHub ; il n'est
pas maintenu et couvre peut-être un quart de la surface de l'API. C'est
le fossé.
Il y a quelques semaines, nous nous sommes assis pour le combler. Le résultat est terraform-provider-elido, que l'API Elido expose aujourd'hui sous elido_link (resource), elido_workspace (data source) et elido_custom_domain (une data source en v0.1.0 - lisez la suite). Ce qui suit est un tour de ce qui a été livré, les choix d'ingénierie derrière, et les parties que nous n'avons délibérément pas livrées en v0.1.0. Mise à jour, novembre 2026 : le provider en arbre est désormais en v0.2.0, qui ajoute elido_custom_domain comme resource. Le provider n'est pas encore publié : il se trouve dans notre monorepo sous tools/terraform-provider-elido/, et la publication publique arrive bientôt.
Pourquoi les liens courts ont leur place dans Terraform
L'argument est court. Si vous faites tourner des redirections marketing, vous avez déjà d'autres pièces d'infrastructure qui convergent sur la même campagne :
- Un enregistrement DNS Cloudflare pointant vers un lander.
- Un bucket S3 et une distribution CloudFront servant ce lander.
- Un service Lambda ou Cloud Run générant des URLs signées.
- Un tag de campagne intégré dans Google Tag Manager ou Segment.
Les cinq, en 2026, sont gérés comme du Terraform. Le lien court qui se trouve à l'avant du funnel - le point d'entrée réel sur lequel un utilisateur clique - est dans un Google Doc. Ce fossé est d'où vient le drift. Un lander est déprécié et la redirection qui pointe vers lui vit, accumulant des 404, jusqu'à ce que quelqu'un ping marketing sur Slack.
Vous pouvez corriger ce fossé de deux façons. Vous pouvez écrire un
script de glue en TypeScript qui se trouve entre votre sortie Terraform
et notre API REST. Ça fonctionne ; nous avons des clients qui font
exactement ça. Ou nous pouvons vous donner un vrai provider Terraform,
où la redirection est un bloc resource à côté de votre enregistrement
Cloudflare, et terraform plan / terraform destroy sachent à son
sujet de la même façon qu'ils savent tout le reste. Nous avons choisi
le second chemin. Le premier était déjà sur vous.
Ce que fait terraform-provider-elido aujourd'hui
La surface minimale v0.1.0, en 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 et vous avez terminé. (La source elidoapp/elido est l'adresse de registre prévue et n'est pas encore publiée ; voir la section d'installation ci-dessous.) La détection de drift fonctionne sur les champs que l'API renvoie. Renommer le label de ressource Terraform, ou changer le slug à la volée, ne force pas un remplacement - le provider émet un PATCH contre le même ID numérique. Changer workspace_id ou domain_id force le remplacement, parce qu'à ce point vous parlez d'une route edge différente. C'est le cycle de vie de bon sens, et c'est ce vers quoi les guides du plugin framework de HashiCorp vous poussent.
La forme de bulk-rollout est la partie qui justifie le travail pour la plupart des équipes :
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]]
}
Vingt liens, un apply. Supprimez le bloc, vingt deletes, un apply. C'est à peu près le cas d'usage qui est ressorti dans trois notes d'appels clients le trimestre dernier : marketing veut des liens UTM par canal et par région pour un lancement, l'ingénierie construit un script Sheets-to-API à chaque fois, le script devient obsolète, l'auteur du script quitte l'entreprise. La force de Terraform ici n'est pas la nouveauté - c'est que nous avons rendu le pattern ennuyeux.
Le guide complet avec référence d'attributs et exemples d'import est à
/docs/guides/terraform. La source du provider
est livrée avec examples/main.tf qui est une version plus élaborée du
snippet ci-dessus.
Comment le provider est construit
Environ 600 lignes de Go, dont ~200 sont des définitions de schéma. La forme :
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
Quelques choix qui valent d'être appelés.
Nous utilisons le plugin framework, pas l'ancien SDK. HashiCorp a
explicitement orienté les nouveaux providers vers
terraform-plugin-framework
en 2023. La plupart des providers populaires (aws, cloudflare,
google) sont en cours de migration ; les plus petits et plus récents
sont framework-natifs. Construire greenfield sur l'ancien SDK aurait
signifié prendre en charge une tâche de migration au moment où nous
livrions. Nous avons évité la migration en n'en créant pas. Le framework
a un système de types plus strict, une vraie validation de schéma au
niveau du protocole plugin, et un modèle de planification beaucoup plus
propre (PlanModifiers au lieu de callbacks CustomizeDiff). Pour un
petit provider, l'écart d'ergonomie est large.
Le provider ne duplique pas le SDK. Chaque méthode de ressource délègue à packages/sdk-go, le SDK Go, publié comme module github.com/elidoapp/elido-go (v0.1.0). Le provider est, par conception, un fin adaptateur Schema-vers-SDK. Cela a deux conséquences. La bonne : tout bug que nous corrigeons dans le SDK atterrit dans le provider gratuitement. La mauvaise : tout trou dans le SDK est un trou dans le provider. L'exemple honnête était les domaines personnalisés. Quand la v0.1.0 est sortie, api-core n'exposait pas POST/DELETE pour /v1/workspaces/{id}/domains ; le chemin d'écriture vivait dans le service séparé domain-manager derrière le dashboard, si bien que le provider n'avait pas de resource elido_custom_domain - seulement une data source qui en cherche une existante par hostname. Ce fossé est comblé : domain-manager a depuis été intégré à api-core (ADR-0047), api-core sert désormais les routes de création, de vérification et de suppression de domaine, et le provider v0.2.0 en arbre a une resource elido_custom_domain qui enregistre un hostname et attend la vérification DNS.
L'auth est la même forme que tout autre client Elido. Bearer API
key dans le header Authorization, retombant sur ELIDO_API_TOKEN dans
l'environnement. Nous n'exposons pas l'auth par cookie ou X-Dev-User-ID
dans le provider ; ce sont des commodités de développement local qui
n'ont rien à faire dans IaC où la config se trouve dans le version
control et tourne en CI. Votre CI a soit un token, soit elle n'en a pas.
Détection de drift : la partie plus dure qu'il n'y paraît
Si vous avez lu au-delà des évidences, c'est la section qui vaut la
peine d'être lue. Le diffing Terraform est fondamentalement une
question de : étant donné ce que l'utilisateur a écrit (Plan), et ce
que le serveur a retourné la dernière fois (State), et ce que le
serveur retourne maintenant (Read), que devrions-nous proposer de
faire ?
Pour une ressource comme elido_link, trois choses rendent ceci non
trivial :
Champs Optional + Computed avec défauts serveur. L'utilisateur peut
omettre redirect_status. Le serveur le remplit avec 302. Le Read
suivant retourne 302. Sans soin, cela ressemble à du drift à chaque
plan - « j'ai demandé rien, j'ai obtenu 302 en retour, proposez de le
remettre à rien ». Le framework vous donne un plan modifier
UseStateForUnknown qui dit « si je n'ai pas de valeur planifiée,
garde ce qui est en state ». Nous l'utilisons sur chaque champ avec
défaut serveur. Ça paraît trivial ; c'est la source des bugs de
provider les plus fréquents dans l'écosystème (« provider produced
inconsistent result after apply »).
Tags avec normalisation côté serveur. Notre API stocke les tags
comme un set ; Terraform les voit comme une liste ordonnée. En ce
moment, nous esquivons ceci. Le serveur préserve l'ordre à l'écho,
donc le diff est stable en pratique, mais un utilisateur qui réordonne
les tags en HCL verra une mise à jour no-op. C'est le comportement
correct ; l'alternative - trier silencieusement à l'entrée - signifierait
que terraform plan et terraform apply désaccordent sur ce qui
change, ce qui est le péché cardinal Terraform. Nous y reviendrons si
de vrais clients se plaignent. Le guide best-practices de
HashiCorp
est fermement du côté « ne fais rien de surprenant » ici.
Status en tri-state. Un lien peut être active, paused ou
archived. Définir status = "paused" en HCL mais pas au Create
(le serveur defaults à active) signifie que nous devons émettre un
PATCH de suivi à l'intérieur du même Create. C'est implémenté comme
une étape de réconciliation post-Create - gardez-le en tête si vous
lisez la source. L'alternative - exposer le status comme une ressource
séparée (elido_link_status keyée par link_id) - est ce que le
provider AWS fait pour quelques ressources. Nous l'avons considéré ;
pour un champ optionnel, le coût dépasse le bénéfice. Si nous ajoutons
un second bouton post-Create, nous repenserons.
Import. terraform import elido_link.spring_campaign 42:7 -
c'est <workspace_id>:<link_id>. Nous choisissons la forme séparée
par deux-points parce que le callback ImportState du framework vous
donne une seule chaîne et vous la parsez vous-même. La forme <id>:<id>
est courante dans les providers qui keyent les ressources par un tuple
- voir la documentation d'import
google_compute_instancepour la référence canonique. Nous sommes délibérés à ne pas surcharger lesluglisible par l'humain ; l'état de la ressource est keyé par l'ID numérique, et c'est la seule chose que vous devriez mettre dans un import.
Tests, CI, le registry
La suite unitaire (7 tests en v0.1.0) couvre la couche de validation
de schéma plus les helpers de fonction pure - splitImportID,
linkToModel, apiErrorString, optString ; la v0.2.0 ajoute des tests pour la
resource de domaine personnalisé. Elle tourne en une demi-seconde environ, dans la
même matrice go qui build nos six services Go. Il y a aussi une cible testacc qui tourne contre un
api-core live quand TF_ACC=1 est défini, mais c'est opt-in : elle
nécessite un token, et nous ne la faisons pas tourner à chaque commit
parce que chaque test crée et supprime un vrai lien. Le framework de
test
de HashiCorp documente le pattern ; nous ne dévions pas.
Le pipeline de release est câblé à goreleaser avec la matrice de build exacte que le Terraform Registry attend : linux, darwin, freebsd, windows × amd64/arm64 (plus arm et 386 sur Linux), SHA256SUMS sur les archives, signature GPG sur les SHA256SUMS, et un terraform-registry-manifest.json déclarant protocol_versions: ["6.0"]. Taggez un commit terraform-provider-vX.Y.Z, le workflow GitHub Actions exécute goreleaser release --clean, et la GitHub Release passe en live. Le Terraform Registry poll la release sur son propre planning et ingère la version. La seule chose qui manque actuellement est la clé GPG, donc le provider n'est pas encore sur le registre ; la publication publique arrive bientôt.
En attendant, installez via dev_overrides dans ~/.terraformrc :
provider_installation {
dev_overrides {
"elidoapp/elido" = "/Users/<you>/.terraform.d/plugins/elidoapp/elido"
}
direct {}
}
Puis make install-local depuis tools/terraform-provider-elido/, et terraform plan résout le binaire directement sans terraform init. C'est le schéma officiel de HashiCorp pour développer un provider, mais il exige une compilation locale du provider ; en attendant la publication publique, l'API REST est la voie pratique.
Ce qui n'est délibérément pas en v0.1.0
Trois choses que nous avons considérées, n'avons pas livrées, et voulons appeler pour que personne ne soit surpris.
Pas d'elido_custom_domain comme ressource. Discuté ci-dessus. La
data source suffisait pour chaîner domain_id dans elido_link,
qui est le cas d'usage porteur. La gestion full-lifecycle est depuis
arrivée dans la v0.2.0 en arbre comme resource elido_custom_domain ;
comme le reste du provider, elle n'est pas encore sur le registre.
Pas d'elido_folder, pas d'elido_api_key. Le SDK a les deux ;
nous avons choisi de ne pas ajouter de Schemas en v0.1.0 parce que
leurs cycles de vie ne sont pas là où est la douleur client. Les
folders sont des métadonnées organisationnelles ; les API keys sont
typiquement émises une fois et tournées via le dashboard. Nous les
ajouterons quand quelqu'un demandera.
Pas de génération de code depuis la spec OpenAPI. HashiCorp livre
terraform-plugin-codegen-openapi
comme outil bêta. Nous l'avons essayé sur notre spec ; les Schemas
générés sont médiocres - chaque champ nullable devient Optional + Computed, chaque liste devient un Set, le résultat nécessite autant
de retouche qu'un Schema écrit à la main et est plus dur à faire
évoluer. Avec trois ressources sur la table, l'écrit à la main gagne.
Nous reverrons le générateur dans six mois quand plus de nos pairs
l'auront battle-testé.
Ce qui a cassé pendant que nous le construisions
Trois choses que nous avons faites mal au premier passage.
La première était l'état sur Optional + Computed. Nous avons
initialement modélisé title comme une plain string Optional. Les
clients qui l'omettaient de HCL obtenaient un Create propre - et
ensuite chaque terraform plan ultérieur proposait de le remettre à
null, parce que le serveur stockait une chaîne vide et Terraform lisait
cela comme du drift. La correction était le plan modifier
UseStateForUnknown ; la leçon était que l'interprétation du provider
de « l'utilisateur n'a pas spécifié » doit correspondre à l'idée du
serveur de « valeur par défaut ». La documentation du framework avertit
à ce sujet dans l'introduction ; nous avons lu au-delà de l'avertissement
la première fois. Je vous épargne la gêne en l'écrivant ici.
La seconde était le format d'import. Nous avons initialement livré
<workspace_id>/<link_id> avec un slash, sur la théorie que les chemins
se lisent plus naturellement. Le framework n'avait aucun problème
avec ; les linters HCL et les terminaux si. Un chemin avec deux slashs
à l'intérieur d'un seul argument shell-quoté se transforme en quelque
chose qui ressemble à une faute de frappe dans les tickets de support.
Nous avons basculé sur un deux-points, qui a zéro ambiguïté et
correspond aux conventions du provider de Google. Leçon : les chaînes
d'import sont une UI face à l'utilisateur, concevez-les comme une UI.
La troisième était l'ordre des tags. Discuté ci-dessus - nous avons
esquivé, et nous continuerons à esquiver jusqu'à ce que quelqu'un
demande. La version que nous avons presque livrée triait silencieusement
les tags à l'entrée, ce qui faisait que terraform plan rapportait pas
de changements quand le client les avait clairement réordonnés. C'est
une pire expérience qu'un diff bruyant ; nous l'avons attrapé pendant
les tests internes. Vaut la peine d'être dit parce que la tentation
d'« être utile » en normalisant l'entrée utilisateur est constante quand
vous écrivez un provider, et c'est presque toujours le mauvais choix.
Comment utiliser ceci avec le reste d'Elido
Le provider est une forme. Les autres formes existent toujours et ne vont nulle part :
- L'API REST est la source de vérité.
Tout ce que fait le provider est aussi faisable avec
curl. - Le SDK Go est ce que le provider utilise en interne ; il est public sous
github.com/elidoapp/elido-go(v0.1.0). - Les SDK TypeScript et Python couvriront la même surface pour votre langage une fois publiés.
- Le endpoint GraphQL couvre les mêmes lectures avec un seul round-trip quand vous en avez besoin shaped à votre écran.
Choisissez ce qui correspond à la forme du problème. Terraform a raison quand vous avez un cycle de vie à gérer. Le SDK a raison quand vous avez un script. L'API REST a raison quand vous faites une chose une fois. Nous pensons que ça devrait être aussi évident ; nous garderons les quatre fonctionnels.
Si vous avez un pattern Terraform favori que nous manquons - imports bulk depuis CSV via for_each sur un bloc data "external", un for_each shaped à une API Linear pour le suivi de campagne, un module wrapping pour le cas agence-gérant-plusieurs-tenants - dites-nous lesquels vous voulez. Le provider existe pour rendre ces patterns ennuyeux ; nous voulons savoir lesquels semblent encore surprenants.
Par où commencer
Si vous avez lu ceci et voulez essayer : le provider n'est pas encore public, lisez donc pour l'instant le guide et utilisez l'API REST ; une fois la publication faite, pointez-le sur un espace de travail bac à sable, écrivez resource "elido_link" pour la redirection que vous avez toujours voulu déclarer en code, et lancez terraform apply. Nous parions un café que la première chose qui vous surprend, dans le bon sens, est terraform destroy fonctionnant exactement comme vous l'attendez.
Si vous lisez ceci et voulez nous comparer aux alternatives - il y a une analyse plus longue dans notre billet Bitly alternatives feature gap, et la comparaison côte-à-côte à /compare/vs-bitly montre où Terraform se trouve sur la matrice. La matrice s'est raccourcie pour eux depuis que ce billet est sorti.
- Marius
Pour aller plus loin sur le blog
- Atteindre p95 < 15ms pour les redirections depuis FRA, ASH et SGP
- Pourquoi un magasin en colonnes bat Postgres pour les analytics de clic
- Import bulk de liens courts depuis un Google Sheet (le vrai workflow de campagne)
- Configurer des liens courts brandés : choisir un domaine, livrer en une après-midi
Essayer Elido
Collez une URL, obtenez un lien court
Sans inscription. Lien actif 30 jours. Inscrivez-vous pour le garder pour toujours.
Sans inscription. Créez un compte gratuit pour 50 liens par mois.