Ich werde eine kleine Behauptung aufstellen und sie dann belegen. Kein
URL-Shortener liefert derzeit einen erstklassigen Terraform-Provider aus.
Bitly, TinyURL, Rebrandly, Short.io, Dub.co - alle fünf veröffentlichen
REST-APIs, mehrere veröffentlichen Webhooks, keiner veröffentlicht einen
terraform-provider-*. Ein Community-Provider für Bitlys v3-API existiert
auf GitHub; er wird nicht gepflegt und deckt vielleicht ein Viertel der
API-Oberfläche ab. Das ist die Lücke.
Vor ein paar Wochen haben wir uns hingesetzt, um sie zu schließen. Das Ergebnis ist terraform-provider-elido, den die Elido-API heute als elido_link (Resource), elido_workspace (Data Source) und elido_custom_domain (Data Source in v0.1.0 - lesen Sie weiter) verfügbar macht. Was folgt, ist eine Tour durch das, was ausgeliefert wurde, die Engineering-Entscheidungen dahinter und die Teile, die wir in v0.1.0 bewusst nicht ausgeliefert haben. Update, November 2026: Der Provider im Repository steht jetzt bei v0.2.0 und ergänzt elido_custom_domain als Resource. Der Provider ist noch nicht veröffentlicht: Er liegt in unserem Monorepo unter tools/terraform-provider-elido/, die öffentliche Veröffentlichung folgt in Kürze.
Warum Short Links zu Terraform gehören
Das Argument ist kurz. Wenn Sie Marketing-Redirects betreiben, haben Sie bereits andere Infrastrukturkomponenten, die auf dieselbe Kampagne konvergieren:
- Einen Cloudflare-DNS-Eintrag, der auf eine Lander-Seite zeigt.
- Einen S3-Bucket und eine CloudFront-Distribution, die diese Lander-Seite ausliefern.
- Eine Lambda- oder Cloud-Run-Service, die signierte URLs erzeugt.
- Ein Kampagnen-Tag, das in Google Tag Manager oder Segment eingebrannt ist.
Alle fünf werden 2026 als Terraform verwaltet. Der Short Link, der ganz vorne im Funnel sitzt - der tatsächliche Einstiegspunkt, auf den ein Nutzer klickt - liegt in einem Google Doc. Diese Lücke ist, wo Drift herkommt. Eine Lander-Seite wird deprecated und der Redirect, der darauf zeigt, lebt weiter, sammelt 404er, bis jemand das Marketing-Team auf Slack anpingt.
Sie können diese Lücke auf zwei Wegen schließen. Sie können ein
Klebescript in TypeScript schreiben, das zwischen Ihrer Terraform-Ausgabe
und unserer REST-API sitzt. Das funktioniert; wir haben Kunden, die genau
das tun. Oder wir geben Ihnen einen echten Terraform-Provider, in dem der
Redirect ein resource-Block neben Ihrem Cloudflare-Eintrag ist, und
terraform plan / terraform destroy wissen davon genauso wie von allem
anderen. Wir haben den zweiten Weg gewählt. Der erste lag schon bei Ihnen.
Was terraform-provider-elido heute tut
Die minimale v0.1.0-Oberfläche 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 und Sie sind fertig. (Die Quelle elidoapp/elido ist die geplante Registry-Adresse und noch nicht veröffentlicht; siehe den Installationsabschnitt unten.) Drift-Erkennung funktioniert auf den Feldern, die die API zurückspiegelt. Das Umbenennen des Terraform-Resource-Labels oder das Ändern des slug im laufenden Betrieb erzwingt kein Replacement - der Provider sendet ein PATCH gegen dieselbe numerische ID. Das Ändern von workspace_id oder domain_id erzwingt ein Replacement, denn an diesem Punkt sprechen Sie über eine andere Edge-Route. Das ist der Common-Sense-Lebenszyklus, und das ist es, worauf die Plugin-Framework-Anleitungen von HashiCorp Sie hindrängen.
Die Bulk-Rollout-Form ist der Teil, der die Arbeit für die meisten Teams rechtfertigt:
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]]
}
Zwanzig Links, ein Apply. Löschen Sie den Block, zwanzig Löschungen, ein Apply. Das ist ungefähr der Use Case, der im letzten Quartal in drei Kundengesprächs-Notizen aufgetaucht ist: Marketing will pro-Kanal-pro-Region UTM-Links für einen Launch, Engineering baut jedes Mal ein Sheets-to-API-Script, das Script veraltet, der Autor des Scripts verlässt das Unternehmen. Terraforms Stärke hier ist nicht die Neuheit
- es ist, dass wir das Muster langweilig gemacht haben.
Die vollständige Anleitung mit Attribut-Referenz und Import-Beispielen
finden Sie unter /docs/guides/terraform. Die
Provider-Quelle liefert eine examples/main.tf mit, die eine
ausführlichere Version des Snippets oben enthält.
Wie der Provider gebaut ist
Etwa 600 Zeilen Go, davon ~200 Schema-Definitionen. Die Form:
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
Ein paar Entscheidungen, die es wert sind, hervorgehoben zu werden.
Wir nutzen das Plugin-Framework, nicht das Legacy-SDK. HashiCorp
hat neue Provider 2023 explizit zu
terraform-plugin-framework
gelenkt. Die meisten beliebten Provider (aws, cloudflare, google)
befinden sich mitten in der Migration; die kleineren, neueren sind
framework-nativ. Auf der grünen Wiese mit dem Legacy-SDK zu bauen,
hätte bedeutet, eine Migrationsaufgabe in dem Moment zu übernehmen,
in dem wir ausgeliefert haben. Wir haben die Migration vermieden,
indem wir keine erzeugt haben. Das Framework hat ein strengeres
Typsystem, echte Schema-Validierung auf der Plugin-Protokoll-Ebene
und ein viel saubereres Plan-Modell (PlanModifiers statt
CustomizeDiff-Callbacks). Für einen kleinen Provider ist die
Ergonomie-Lücke groß.
Der Provider dupliziert das SDK nicht. Jede Ressourcenmethode delegiert an packages/sdk-go, das Go-SDK, veröffentlicht als Modul github.com/elidoapp/elido-go (v0.1.0). Der Provider ist by-design ein dünner Schema-zu-SDK-Adapter. Das hat zwei Konsequenzen. Die gute: Jeder Bug, den wir im SDK fixen, landet kostenlos im Provider. Die schlechte: Jede Lücke im SDK ist eine Lücke im Provider. Das ehrliche Beispiel waren Custom Domains. Als v0.1.0 erschien, machte api-core POST/DELETE für /v1/workspaces/{id}/domains nicht verfügbar; der Schreibpfad lag im separaten domain-manager-Service hinter dem Dashboard, sodass der Provider keine elido_custom_domain-Resource hatte - nur eine Data Source, die eine existierende per Hostname nachschlägt. Diese Lücke ist geschlossen: domain-manager wurde inzwischen in api-core integriert (ADR-0047), api-core bedient jetzt die Routen zum Anlegen, Verifizieren und Löschen von Domains, und der Provider v0.2.0 im Repository hat eine elido_custom_domain-Resource, die einen Hostnamen registriert und auf die DNS-Verifizierung wartet.
Auth ist genauso geformt wie bei jedem anderen Elido-Client.
Bearer-API-Key im Authorization-Header, mit Fallback auf
ELIDO_API_TOKEN in der Umgebung. Wir geben weder Cookie-Auth noch
X-Dev-User-ID im Provider frei; das sind lokale
Entwicklungsbequemlichkeiten, die in IaC, wo die Konfiguration in
der Versionskontrolle liegt und in CI läuft, nichts zu suchen haben.
Ihre CI hat entweder ein Token oder nicht.
Drift-Erkennung: der Teil, der schwerer ist, als es aussieht
Wenn Sie an den offensichtlichen Teilen vorbeigelesen haben, ist das
der Abschnitt, der das Lesen wert ist. Terraform-Diffing ist
grundsätzlich eine Frage von: Gegeben, was der Nutzer geschrieben
hat (Plan), und was der Server beim letzten Mal zurückgegeben hat
(State), und was der Server jetzt zurückgibt (Read), was sollten
wir vorschlagen zu tun?
Für eine Resource wie elido_link machen drei Dinge das nichttrivial:
Optional + Computed-Felder mit Server-Defaults. Der Nutzer kann
redirect_status weglassen. Der Server füllt 302 ein. Das nächste
Read gibt 302 zurück. Ohne Vorsicht sieht das wie Drift bei
jedem Plan aus - "Ich habe um nichts gebeten, ich habe 302 zurück
bekommen, schlagen Sie vor, es wieder auf nichts zu setzen". Das Framework
gibt Ihnen einen UseStateForUnknown-Plan-Modifier, der sagt: "Wenn
ich keinen geplanten Wert habe, behalten Sie das, was im State ist."
Wir verwenden ihn auf jedem server-default-gesetzten Feld. Das
klingt trivial; es ist die Quelle der häufigsten Provider-Bugs im
Ökosystem ("provider produced inconsistent result after apply").
Tags mit serverseitiger Normalisierung. Unsere API speichert
Tags als Set; Terraform sieht sie als geordnete Liste. Aktuell
weichen wir hier aus. Der Server bewahrt die Reihenfolge beim
Echo, also ist der Diff in der Praxis stabil, aber ein Nutzer,
der Tags in HCL umordnet, sieht ein No-op-Update. Das ist korrektes
Verhalten; die Alternative - beim Input still zu sortieren - würde
bedeuten, dass terraform plan und terraform apply sich darüber
uneinig sind, was sich ändert, was die kardinale Terraform-Sünde
ist. Wir werden das überdenken, wenn echte Kunden sich beschweren.
Der HashiCorp-Leitfaden zu
Best Practices
steht hier fest auf der "Tu nichts Überraschendes"-Seite.
Status als Tri-State. Ein Link kann active, paused oder
archived sein. status = "paused" in HCL zu setzen, aber nicht
beim Create (der Server defaultet auf active), bedeutet, dass wir
ein Follow-up-PATCH innerhalb desselben Create absetzen müssen.
Das ist als Post-Create-Reconciliation-Schritt implementiert - hab
das im Hinterkopf, falls Sie die Quelle lesen. Die Alternative -
Status als separate Resource (elido_link_status mit link_id
als Schlüssel) zu exponieren - ist das, was der AWS-Provider für
einige Resources tut. Wir haben es in Betracht gezogen; für ein
optionales Feld überwiegen die Kosten den Nutzen. Wenn wir einen
zweiten Post-Create-Knopf hinzufügen, denken wir um.
Import. terraform import elido_link.spring_campaign 42:7 -
das ist <workspace_id>:<link_id>. Wir wählen die
durch-Doppelpunkt-getrennte Form, weil der ImportState-Callback
des Frameworks Ihnen einen einzelnen String gibt und Sie ihn selbst
parst. Die <id>:<id>-Form ist üblich bei Providern, die Resources
durch ein Tupel verschlüsseln - siehe die
Import-Dokumentation von google_compute_instance
für die kanonische Referenz. Wir sind bewusst dabei, den
menschenlesbaren slug nicht zu überladen; der Resource-State ist
mit der numerischen ID verschlüsselt, und das ist das Einzige, was
Sie in einen Import schreiben sollten.
Tests, CI, das Registry
Die Unit-Suite (7 Tests in v0.1.0) deckt die Schema-Validierungsschicht
plus die reinen Funktions-Helper ab - splitImportID, linkToModel,
apiErrorString, optString; v0.2.0 ergänzt Tests für die Custom-Domain-Resource. Sie läuft in etwa einer halben Sekunde
in derselben go-Matrix, die unsere sechs Go-Services baut.
Es gibt auch ein testacc-Target, das gegen ein lebendiges
api-core läuft, wenn TF_ACC=1 gesetzt ist, aber das ist opt-in:
Es benötigt ein Token, und wir führen es nicht bei jedem Commit
aus, weil jeder Test einen echten Link erstellt und löscht. Das
Testing-Framework
von HashiCorp dokumentiert das Muster; wir weichen nicht davon ab.
Die Release-Pipeline ist mit goreleaser verdrahtet, mit der exakten Build-Matrix, die das Terraform Registry erwartet: linux, darwin, freebsd, windows × amd64/arm64 (plus arm und 386 auf Linux), SHA256SUMS über die Archive, GPG-Signatur auf den SHA256SUMS und eine terraform-registry-manifest.json, die protocol_versions: ["6.0"] deklariert. Taggen Sie einen Commit terraform-provider-vX.Y.Z, der GitHub-Actions-Workflow führt goreleaser release --clean aus, und der GitHub-Release geht live. Das Terraform Registry pollt den Release nach seinem eigenen Zeitplan und nimmt die Version auf. Das Einzige, was aktuell fehlt, ist der GPG-Schlüssel, daher ist der Provider noch nicht in der Registry; die öffentliche Veröffentlichung folgt in Kürze.
Installieren Sie in der Zwischenzeit über dev_overrides in
~/.terraformrc:
provider_installation {
dev_overrides {
"elidoapp/elido" = "/Users/<you>/.terraform.d/plugins/elidoapp/elido"
}
direct {}
}
Dann make install-local aus tools/terraform-provider-elido/, und terraform plan löst die Binary direkt ohne terraform init auf. Das ist das offizielle HashiCorp-Muster für die Provider-Entwicklung, erfordert aber einen lokalen Build des Providers; bis zur öffentlichen Veröffentlichung ist die REST-API der praktische Weg.
Was bewusst nicht in v0.1.0 ist
Drei Dinge, die wir in Betracht gezogen, nicht ausgeliefert haben und hervorheben wollen, damit niemand überrascht ist.
elido_custom_domain als Resource. Oben besprochen. In v0.1.0 reichte die
Data Source aus, um domain_id in elido_link zu verketten,
was der tragende Use Case ist. Das Full-Lifecycle-Management
ist inzwischen in v0.2.0 im Repository als Resource elido_custom_domain gelandet; wie der Rest des Providers ist es noch nicht in der Registry.
Keine elido_folder, kein elido_api_key. Das SDK hat beide;
wir haben uns dagegen entschieden, Schemas in v0.1.0 hinzuzufügen,
weil ihre Lebenszyklen nicht dort sind, wo der Kundenschmerz ist.
Ordner sind organisatorische Metadaten; API-Keys werden typischerweise
einmal ausgegeben und über das Dashboard rotiert. Wir fügen sie
hinzu, wenn jemand fragt.
Keine Code-Generierung aus der OpenAPI-Spezifikation. HashiCorp
liefert
terraform-plugin-codegen-openapi
als Beta-Tool aus. Wir haben es mit unserer Spezifikation probiert;
die generierten Schemas sind mittelmäßig - jedes Nullable-Feld wird
zu Optional + Computed, jede Liste wird zu einem Set, das
Ergebnis erfordert genauso viel Nachbesserung wie ein
handgeschriebenes Schema und ist schwerer weiterzuentwickeln. Mit
drei Resources auf dem Tisch gewinnt das Handgeschriebene. Wir
werden den Generator in sechs Monaten erneut prüfen, wenn mehr
unserer Peers ihn kampferprobt haben.
Was kaputtging, während wir ihn bauten
Drei Dinge, die wir im ersten Durchgang falsch gemacht haben.
Das erste war State auf Optional + Computed. Wir haben title
zunächst als einfachen Optional-String modelliert. Kunden, die
es aus HCL weggelassen haben, bekamen einen sauberen Create - und
dann schlug jedes nachfolgende terraform plan vor, ihn zurück
auf null zu setzen, weil der Server einen leeren String gespeichert
hat und Terraform das als Drift gelesen hat. Der Fix war der
UseStateForUnknown-Plan-Modifier; die Lektion war, dass die
Interpretation des Providers von "der Nutzer hat nichts angegeben"
mit der Idee des Servers von "Default-Wert" übereinstimmen muss.
Die Framework-Dokumentation warnt davor in der Einleitung; wir
haben die Warnung beim ersten Mal überlesen. Ich habe Ihnen die
Peinlichkeit erspart, indem ich es hier aufgeschrieben habe.
Das zweite war das Import-Format. Wir haben zunächst
<workspace_id>/<link_id> mit einem Schrägstrich ausgeliefert,
unter der Annahme, dass sich Pfade natürlicher lesen. Das Framework
hatte kein Problem damit; HCL-Linter und Terminals schon. Ein Pfad
mit zwei Schrägstrichen innerhalb eines einzelnen shell-quotierten
Arguments wird zu etwas, das in Support-Tickets wie ein Tippfehler
aussieht. Wir sind zu einem Doppelpunkt gewechselt, der null
Mehrdeutigkeit hat und Googles Provider-Konventionen entspricht.
Lektion: Import-Strings sind nutzerseitige UI, gestalten Sie sie wie UI.
Das dritte war die Tag-Reihenfolge. Oben besprochen - wir sind
ausgewichen, und wir werden weiter ausweichen, bis jemand fragt.
Die Version, die wir fast ausgeliefert hätten, sortierte Tags
beim Input still, was dazu führte, dass terraform plan keine
Änderungen meldete, wenn der Kunde sie eindeutig umgeordnet hatte.
Das ist eine schlechtere Erfahrung als ein lauter Diff; wir haben
es beim internen Testen gefangen. Wert zu erwähnen, weil die
Versuchung, "hilfreich zu sein", indem man Nutzer-Input
normalisiert, konstant ist, wenn man einen Provider schreibt, und
es ist fast immer die falsche Entscheidung.
Wie man das mit dem Rest von Elido nutzt
Der Provider ist eine Form. Die anderen Formen existieren weiterhin und gehen nirgendwohin:
- Die REST-API ist die Source of Truth.
Alles, was der Provider tut, ist auch mit
curlmachbar. - Das Go-SDK nutzt der Provider selbst intern; es ist öffentlich als
github.com/elidoapp/elido-go(v0.1.0). - Die TypeScript- und Python-SDKs decken nach ihrer Veröffentlichung dieselbe Oberfläche für Ihre Sprache ab.
- Der GraphQL-Endpunkt deckt dieselben Reads mit einem einzigen Round-Trip ab, wenn Sie sie auf Ihren Bildschirm zugeschnitten brauchst.
Wählen Sie, was zur Form des Problems passt. Terraform ist richtig, wenn Sie einen Lebenszyklus zu verwalten haben. Das SDK ist richtig, wenn Sie ein Script haben. Die REST-API ist richtig, wenn Sie eine Sache einmal tust. Wir denken, das sollte so offensichtlich sein; wir werden alle vier am Laufen halten.
Wenn Sie ein Lieblings-Terraform-Muster haben, das uns fehlt - Bulk- Imports aus CSV via for_each über einen data "external"-Block, ein for_each, das auf eine Linear-API für Kampagnen-Tracking zugeschnitten ist, ein Wrapping-Modul für den Agentur-die-mehrere-Mandanten-verwaltet-Fall - sagen Sie uns, welche Sie sich wünschen. Der Provider existiert, um diese Muster langweilig zu machen; wir wollen wissen, welche sich noch überraschend anfühlen.
Wo anfangen
Wenn Sie das gelesen haben und es ausprobieren möchten: Der Provider ist noch nicht öffentlich, lesen Sie daher vorerst den Leitfaden und nutzen Sie die REST-API; sobald die Veröffentlichung erfolgt ist, richten Sie ihn auf einen Sandbox-Workspace, schreiben resource "elido_link" für die Weiterleitung, die Sie schon immer als Code deklarieren wollten, und führen terraform apply aus. Wir wetten einen Kaffee, dass das Erste, was Sie überrascht, auf eine gute Art, ist, dass terraform destroy genau so funktioniert, wie Sie es erwarten.
Wenn Sie das gelesen haben und uns mit den Alternativen vergleichen willst - es gibt einen längeren Bericht in unserem Post Bitly Alternatives feature gap, und der Side-by-side unter /compare/vs-bitly zeigt, wo Terraform in der Matrix sitzt. Die Matrix ist seit diesem Post für sie kürzer geworden.
- Marius
Verwandtes im Blog
Elido testen
URL einfügen, kurzer Link in Sekunden
Kein Konto nötig. Link bleibt 30 Tage aktiv. Konto erstellen, um ihn dauerhaft zu behalten.
Keine Anmeldung nötig. Kostenlos registrieren für 50 Links im Monat.