Elido
12 Min. LesezeitEngineering
Eckpfeiler

Verwalte deine Short Links als Terraform

Wir haben den einzigen Terraform-Provider im URL-Shortener-Bereich ausgeliefert - terraform-provider-elido. Hier ist, was er tut, wie der Ressourcen-Lebenszyklus funktioniert und welche Engineering-Trade-offs dahinterstecken.

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

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 vorerst - lies 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. Der Provider ist Open Source unter derselben Lizenz wie der Rest von Elido und liegt unter tools/terraform-provider-elido/.

Das Argument ist kurz. Wenn du Marketing-Redirects betreibst, hast du 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.

Du kannst diese Lücke auf zwei Wegen schließen. Du kannst ein Klebescript in TypeScript schreiben, das zwischen deiner Terraform-Ausgabe und unserer REST-API sitzt. Das funktioniert; wir haben Kunden, die genau das tun. Oder wir geben dir einen echten Terraform-Provider, in dem der Redirect ein resource-Block neben deinem 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 dir.

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, Replacement nur wenn workspace_id oder domain_id auf eine andere Edge-Route wechselt

terraform apply und du bist fertig. 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 sprichst du über eine andere Edge-Route. Das ist der Common-Sense-Lebenszyklus, und das ist es, worauf die Plugin-Framework-Anleitungen von HashiCorp dich 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ösche 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 findest du 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 Resource-Methode delegiert an packages/sdk-go, welches dasselbe SDK ist, das wir für reine Go-Integrationen veröffentlichen. 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 sind Custom Domains. api-core macht POST/DELETE für /v1/workspaces/{id}/domains noch nicht verfügbar; der Schreibpfad liegt im domain-manager hinter dem Dashboard. Bis api-core die Writes durchreicht, hat das SDK kein Domains.Create, und der Provider hat keine elido_custom_domain-Resource - nur eine Data Source, die eine existierende per Hostname nachschlägt. Wir werden diese Lücke in v0.2.0 schließen; der Proxy-Shim ist eine Sub-Wochen-Änderung und der SDK- + Provider-PR ist bereits entworfen.

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. Deine CI hat entweder ein Token oder nicht.

Drift-Erkennung: der Teil, der schwerer ist, als es aussieht#

Wenn du an den offensichtlichen Teilen vorbeigelesen hast, 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. Wenn der Live-Read abweicht, schlägt der Provider ein korrigierendes PATCH vor; server-defaulted Felder werden mit dem UseStateForUnknown-Plan-Modifier gehalten, damit sie nie als False-Drift erscheinen

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, schlage vor, es wieder auf nichts zu setzen". Das Framework gibt dir einen UseStateForUnknown-Plan-Modifier, der sagt: "Wenn ich keinen geplanten Wert habe, behalte 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 du die Quelle liest. 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 dir einen einzelnen String gibt und du 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 du in einen Import schreiben solltest.

Tests, CI, das Registry#

Die Unit-Suite (heute 7 Tests) deckt die Schema-Validierungsschicht plus die reinen Funktions-Helper ab - splitImportID, linkToModel, apiErrorString, optString. Sie läuft in 0,5 Sekunden und gated jeden PR durch dieselbe go-Matrix, die unsere 13 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, sodass der Edge die Änderung beim nächsten Click übernimmt

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. Tagge 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-Key - wir prägen diese Woche einen, der dediziert für Provider-Releases ist, was bedeutet, dass v0.1.0 etwa zur gleichen Zeit wie dieser Post im Registry landet.

Installiere 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 und funktioniert genauso gut als Übergangs- Installationspfad bis v1.0.0.

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.

Keine elido_custom_domain als Resource. Oben besprochen. Die Data Source reicht aus, um domain_id in elido_link zu verketten, was der tragende Use Case ist; das Full-Lifecycle-Management wartet auf api-core. ETA: v0.2.0, Mitte 2026.

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. Habe dir 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, gestalte 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 ist das, was der Provider selbst intern nutzt; du kannst es als Bibliothek einbinden.
  • Die TypeScript- und Python-SDKs decken dieselbe Oberfläche für die Sprache ab, in der du gerade bist.
  • Der GraphQL-Endpunkt deckt dieselben Reads mit einem einzigen Round-Trip ab, wenn du sie auf deinen Bildschirm zugeschnitten brauchst.

Wähle, was zur Form des Problems passt. Terraform ist richtig, wenn du einen Lebenszyklus zu verwalten hast. Das SDK ist richtig, wenn du ein Script hast. Die REST-API ist richtig, wenn du eine Sache einmal tust. Wir denken, das sollte so offensichtlich sein; wir werden alle vier am Laufen halten.

Wenn du ein Lieblings-Terraform-Muster hast, 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 - eröffne ein Issue im GitHub-Repo mit dem area:terraform-Label. Der Provider existiert, um diese Muster langweilig zu machen; wir wollen wissen, welche sich noch überraschend anfühlen.

Wo anfangen#

Wenn du das gelesen hast und es ausprobieren willst: Installiere den Provider gemäß der Anleitung, richte ihn auf einen Sandbox-Workspace, schreibe resource "elido_link" für den Redirect, den du schon immer in Code deklarieren wolltest, und terraform apply. Wir wetten einen Kaffee, dass das Erste, was dich überrascht, auf eine gute Art, ist, dass terraform destroy genau so funktioniert, wie du es erwartest.

Wenn du das gelesen hast 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.

Kostenlos, keine Anmeldung erforderlich · 2 pro Tag

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