14 min de lectureIngénierie
Pilier

Gérer vos liens courts comme du Terraform

Nous avons livré le seul provider Terraform de l'espace des raccourcisseurs d'URL - terraform-provider-elido. Voici ce qu'il fait, comment fonctionne le cycle de vie des ressources, et les compromis d'ingénierie derrière.

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

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
}
Cycle de vie declaratif des liens : un bloc HCL elido_link alimente terraform plan, qui produit un diff, puis terraform apply appelle la surface REST api-core d'Elido pour creer, mettre a jour ou supprimer le lien, avec PATCH lors d'un changement de slug et remplacement uniquement quand workspace_id ou domain_id pointe vers une route edge differente

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 ?

La detection de drift compare trois entrees : l'etat desire dans le plan HCL, l'etat Terraform enregistre et la lecture en direct depuis api-core. Quand la lecture en direct diverge, le provider propose un PATCH correctif ; les champs avec valeur par defaut serveur sont conserves avec le plan modifier UseStateForUnknown pour ne jamais apparaitre comme faux drift

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_instance pour la référence canonique. Nous sommes délibérés à ne pas surcharger le slug lisible 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.

Workflow GitOps pour les liens courts : une modification de la config HCL ouvre une pull request, la CI execute terraform plan et les tests unitaires Go, un reviewer approuve le diff et merge vers main, et le merge declenche terraform apply contre api-core pour que le edge prenne en compte la modification au prochain clic

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#

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

Essayer Elido

Raccourcisseur d'URL hébergé en UE : domaines personnalisés, analyses approfondies et API ouverte. Forfait gratuit - sans carte bancaire.

Tags
terraform
infrastructure as code
url shortener
developer experience
devops
iac

Lire la suite