13 Min. LesezeitEngineering
Eckpfeiler

Verwalten Sie Ihre Short Links als Terraform

Wir haben den einzigen Terraform-Provider der URL-Shortener-Branche ausgeliefert: terraform-provider-elido.

Marius Voß
DevRel · edge infra

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.

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.

Die Elido-Integrationsseite für Terraform mit den Provider-Einrichtungsschritten zum Verwalten von Kurzlinks als Code

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
}
Deklarativer Link-Lebenszyklus: Ein HCL-elido_link-Block wird in terraform plan übergeben, das einen Diff erzeugt, dann ruft terraform apply die Elido-api-core-REST-Oberfläche auf, um den Link zu erstellen, zu aktualisieren oder zu löschen - PATCH bei einer Slug-Änderung.

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?

Drift-Erkennung vergleicht drei Eingaben: den gewünschten Zustand im HCL-Plan, den aufgezeichneten Terraform-State und den Live-Read von api-core.

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.

GitOps-Workflow für Short Links: Eine Änderung an der HCL-Konfiguration öffnet einen Pull Request, CI führt terraform plan und Go-Unit-Tests aus, ein Reviewer genehmigt den Diff und merged in main, und der Merge löst terraform apply gegen api-core aus.

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 curl machbar.
  • 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.

Elido testen

URL-Shortener mit EU-Hosting: eigene Domains, tiefe Analytik und eine offene API. Kostenloser Tarif - keine Kreditkarte nötig.

Tags
terraform
infrastructure as code
url shortener
developer experience
devops
iac

Weiterlesen