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 (data source pour l'instant - 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. Le provider est open source sous la même licence que le reste
d'Elido et vit à
tools/terraform-provider-elido/.
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 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,
qui est le même SDK que nous publions pour les intégrations plain-Go. 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 est les domaines
personnalisés. api-core n'expose pas encore POST/DELETE pour
/v1/workspaces/{id}/domains ; le chemin d'écriture vit dans
domain-manager derrière le dashboard. Jusqu'à ce qu'api-core proxie
les écritures, le SDK n'a pas de Domains.Create, et le provider
n'a pas de resource elido_custom_domain - seulement une data source
qui en cherche une existante par hostname. Nous fermerons ce fossé en
v0.2.0 ; le proxy shim est un changement sub-week et la PR SDK + provider
est déjà brouillée.
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 aujourd'hui) couvre la couche de validation
de schéma plus les helpers de fonction pure - splitImportID,
linkToModel, apiErrorString, optString. Elle tourne en 0,5
seconde et gate chaque PR à travers la même matrice go qui build nos
13 services. 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 actuellement manquante est la clé GPG - nous en mintons une dédiée
aux releases de provider cette semaine, ce qui signifie que v0.1.0
atterrit sur le registry à peu près en même temps que ce billet.
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 pattern officiel HashiCorp pour le développement de provider
et fonctionne tout aussi bien comme chemin d'installation intérimaire
jusqu'à v1.0.0.
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 est suffisante pour chaîner domain_id dans elido_link,
qui est le cas d'usage porteur ; la gestion full-lifecycle attend
api-core. ETA : v0.2.0, mi-2026.
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 lui-même utilise en interne ; vous pouvez le tirer comme bibliothèque.
- Les SDKs TypeScript et Python couvrent la même surface pour le langage dans lequel vous vous trouvez.
- 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 - ouvrez une issue
sur le repo GitHub avec le label
area:terraform. Le provider existe pour rendre ces patterns ennuyeux ;
nous voulons savoir lesquels semblent encore surprenants.
Par où commencer#
Si vous lisez ceci et voulez l'essayer : installez le provider selon le
guide, pointez-le vers un workspace sandbox,
écrivez resource "elido_link" pour la redirection que vous avez
toujours voulu déclarer en code, et 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 nous utilisons ClickHouse pour les analytics de clic (et pas Postgres)
- 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.
Gratuit, sans inscription · 2 par jour