Postawię małą tezę, a potem ją udowodnię. Żaden skracz URL nie udostępnia dziś providera Terraform pierwszej klasy. Bitly, TinyURL, Rebrandly, Short.io, Dub.co - wszyscy pięciu publikują REST API, kilku publikuje webhooki, żaden nie publikuje terraform-provider-*. Na GitHubie istnieje społecznościowy provider dla v3 API Bitly; jest niekonserwowany i pokrywa może ćwierć powierzchni API. To ta luka.
Kilka tygodni temu usiedliśmy, żeby ją zamknąć. Efektem jest terraform-provider-elido, który API Elido udostępnia dziś jako elido_link (zasób), elido_workspace (źródło danych) i elido_custom_domain (na razie źródło danych - czytaj dalej). Poniżej jest omówienie tego, co zostało wydane, wyborów inżynierskich za tym stojących i części, których celowo nie uwzględniliśmy w v0.1.0. Provider jest open source na tej samej licencji co reszta Elido i mieszka w tools/terraform-provider-elido/.
Dlaczego krótkie linki należą do Terraform#
Argument jest krótki. Jeśli prowadzisz przekierowania marketingowe, masz już inne elementy infrastruktury zbiegające się na tej samej kampanii:
- Rekord DNS Cloudflare wskazujący na stronę landing.
- Kubełek S3 i dystrybucja CloudFront serwujące tę stronę.
- Lambda lub Cloud Run generujące podpisane URL-e.
- Tag kampanii wbudowany w Google Tag Manager lub Segment.
Wszystkie pięć z nich, w roku 2026, jest zarządzane przez Terraform. Krótki link stojący na początku lejka - faktyczny punkt wejścia, który użytkownik klika - jest w Google Doc. Z tej luki bierze się dryfowanie konfiguracji. Strona landing zostaje wycofana, a przekierowanie na nią żyje dalej, zbierając 404, dopóki ktoś nie pingnie marketingu na Slacku.
Tę lukę można naprawić na dwa sposoby. Można napisać skrypt klejący w TypeScript, który siada między outputem Terraform a naszym REST API. To działa - mamy klientów, którzy dokładnie tak robią. Albo możemy dać ci prawdziwego providera Terraform, gdzie przekierowanie jest blokiem resource obok Twojego rekordu Cloudflare, a terraform plan / terraform destroy wie o nim tak samo jak o wszystkim innym. Wybraliśmy drugą ścieżkę. Pierwsza była już po Twojej stronie.
Co terraform-provider-elido robi dziś#
Minimalna powierzchnia v0.1.0, w 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 i gotowe. Wykrywanie dryftu działa na polach, które API odsyła z powrotem. Zmiana nazwy etykiety zasobu Terraform lub zmiana slug w trakcie działania nie wymusza zastąpienia - provider wysyła PATCH przeciwko temu samemu numerycznemu ID. Zmiana workspace_id lub domain_id wymusza zastąpienie, bo w tym momencie mówisz o innej trasie edge. To jest logiczny cykl życia i to właśnie framework pluginów HashiCorpa do tego zachęca.
Kształt masowego wdrożenia to część uzasadniająca tę pracę dla większości zespołów:
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]]
}
Dwadzieścia linków, jedno apply. Usuń blok, dwadzieścia delete, jedno apply. To mniej więcej przypadek użycia, który pojawił się w trzech notatkach z rozmów z klientami w poprzednim kwartale: marketing chce linków UTM per kanał per region dla launchu, engineering buduje skrypt Sheets-do-API za każdym razem, skrypt się dezaktualizuje, jego autor odchodzi z firmy. Siła Terraform nie leży tu w nowości - leży w tym, że ten wzorzec staje się nudny.
Pełny przewodnik z referencją atrybutów i przykładami importu jest na /docs/guides/terraform. Źródło providera zawiera examples/main.tf będące bardziej rozbudowaną wersją powyższego fragmentu.
Jak provider jest zbudowany#
Mniej więcej 600 linii Go, z czego ~200 to definicje schematów. Struktura:
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
Kilka wyborów wartych wyróżnienia.
Używamy frameworka pluginów, a nie legacy SDK. HashiCorp wyraźnie skierował nowych providerów do terraform-plugin-framework w 2023 roku. Większość popularnych providerów (aws, cloudflare, google) jest w trakcie migracji; mniejsze, nowsze są natywne dla frameworka. Budowanie od zera na legacy SDK oznaczałoby przyjęcie zadania migracji w momencie wysyłki. Uniknęliśmy migracji, nie tworząc jej. Framework ma bardziej rygorystyczny system typów, prawdziwą walidację schematu na poziomie protokołu pluginu i znacznie czystszy model planowania (PlanModifiers zamiast callbacków CustomizeDiff). Dla małego providera różnica ergonomiki jest duża.
Provider nie duplikuje SDK. Każda metoda zasobu deleguje do packages/sdk-go, który jest tym samym SDK, które publikujemy dla integracji plain-Go. Provider jest z założenia cienkim adapterem Schema-do-SDK. Ma to dwie konsekwencje. Dobra: każdy błąd naprawiony w SDK trafia do providera za darmo. Zła: każda luka w SDK jest luką w providerze. Uczciwy przykład to domeny własne. api-core nie udostępnia jeszcze POST/DELETE dla /v1/workspaces/{id}/domains; ścieżka zapisu żyje w domain-manager za dashboardem. Dopóki api-core nie proxuje zapisów, SDK nie ma Domains.Create, a provider nie ma zasobu elido_custom_domain - tylko źródło danych wyszukujące istniejący rekord po nazwie hosta. Zamkniemy tę lukę w v0.2.0; shim proxy to zmiana poniżej tygodnia, a PR dla SDK i providera jest już w szkicu.
Auth ma ten sam kształt co każdy inny klient Elido. Klucz Bearer API w nagłówku Authorization, z fallbackiem do ELIDO_API_TOKEN w środowisku. W providerze nie udostępniamy auth ciasteczkowego ani X-Dev-User-ID; to wygody do lokalnego rozwoju, które nie mają nic do roboty w IaC, gdzie konfiguracja siedzi w kontroli wersji i działa w CI. Twoje CI albo ma token, albo nie.
Wykrywanie dryftu: część trudniejsza niż wygląda#
Jeśli doczytałeś do tej części, ten rozdział jest wart przeczytania. Diffing w Terraform to zasadniczo pytanie: biorąc pod uwagę to, co napisał użytkownik (Plan), co serwer zwrócił ostatnim razem (State) i co serwer zwraca teraz (Read) - co powinniśmy zaproponować?
Dla zasobu takiego jak elido_link trzy rzeczy sprawiają, że to jest nietrywializne:
Pola Optional + Computed z domyślnymi wartościami serwera. Użytkownik może pominąć redirect_status. Serwer wypełnia 302. Następny Read zwraca 302. Bez ostrożności wygląda to jak dryfowanie przy każdym planie - „niczego nie prosiłem, dostałem 302, proponuję ustawić z powrotem na nic". Framework daje plan modifier UseStateForUnknown, który mówi „jeśli nie mam planowanej wartości, zachowaj to, co jest w stanie". Używamy go na każdym polu z domyślną wartością serwera. Brzmi trywialnie; to źródło najczęstszych błędów providerów w ekosystemie („provider produced inconsistent result after apply").
Tagi z normalizacją po stronie serwera. Nasze API przechowuje tagi jako zbiór; Terraform widzi je jako uporządkowaną listę. Na razie to odkładamy. Serwer zachowuje kolejność przy echo, więc diff jest w praktyce stabilny, ale użytkownik, który zmieni kolejność tagów w HCL, zobaczy aktualizację bez operacji. To jest poprawne zachowanie; alternatywa - ciche sortowanie na wejściu - oznaczałaby, że terraform plan i terraform apply nie zgadzają się co do zmian, co jest kardynalnym grzechem Terraform. Wrócimy do tego, jeśli prawdziwi klienci będą narzekać. Przewodnik dobrych praktyk HashiCorpa jest zdecydowanie po stronie „nie rób niczego zaskakującego".
Status jako tri-state. Link może być active, paused lub archived. Ustawienie status = "paused" w HCL, ale nie przy Create (serwer domyślnie ustawia active), oznacza, że musimy wykonać dodatkowy PATCH wewnątrz tego samego Create. Jest to zaimplementowane jako krok rekoncyliacji po Create - miej to na uwadze, czytając źródło. Alternatywa - wystawienie statusu jako osobnego zasobu (elido_link_status kluczowanego przez link_id) - to co provider AWS robi dla kilku zasobów. Rozważaliśmy to; dla jednego opcjonalnego pola koszt przewyższa korzyść. Jeśli dodamy drugi przełącznik post-Create, przemyślimy to.
Import. terraform import elido_link.spring_campaign 42:7 - to jest <workspace_id>:<link_id>. Wybieramy formę z dwukropkiem, bo callback ImportState frameworka daje jeden string i parsuje się go samodzielnie. Kształt <id>:<id> jest powszechny w providerach, które kluczują zasoby przez krotki - patrz dokumentacja importu google_compute_instance jako kanoniczna referencja. Celowo nie przeciążamy czytelnego dla człowieka slug; stan zasobu jest kluczowany przez numeryczne ID i to jest jedyna rzecz, którą powinieneś umieszczać w imporcie.
Testy, CI, rejestr#
Zestaw testów jednostkowych (dziś 7 testów) obejmuje warstwę walidacji schematu oraz pomocniki czystych funkcji - splitImportID, linkToModel, apiErrorString, optString. Działa w 0,5 sekundy i bramkuje każdy PR przez tę samą macierz go, która buduje nasze 13 serwisów. Istnieje też cel testacc działający przeciwko żywemu api-core gdy ustawione jest TF_ACC=1, ale jest to opt-in: wymaga tokenu i nie uruchamiamy go przy każdym commicie, bo każdy test tworzy i usuwa prawdziwy link. Framework testowy HashiCorpa dokumentuje ten wzorzec; nie odbiegamy od niego.
Pipeline wydań jest podłączony do goreleaser z dokładną macierzą buildów, jakiej oczekuje Terraform Registry: linux, darwin, freebsd, windows × amd64/arm64 (plus arm i 386 na Linuksie), SHA256SUMS nad archiwami, podpis GPG na SHA256SUMS i terraform-registry-manifest.json deklarujący protocol_versions: ["6.0"]. Otaguj commit terraform-provider-vX.Y.Z, workflow GitHub Actions uruchamia goreleaser release --clean, a GitHub Release wychodzi na żywo. Terraform Registry samodzielnie odpytuje release według własnego harmonogramu i ingestuje wersję. Jedyna brakująca rzecz to klucz GPG - w tym tygodniu tworzymy dedykowany do wydań providera, co oznacza, że v0.1.0 trafia do rejestru mniej więcej w tym samym czasie co ten wpis.
W międzyczasie instaluj przez dev_overrides w ~/.terraformrc:
provider_installation {
dev_overrides {
"elidoapp/elido" = "/Users/<you>/.terraform.d/plugins/elidoapp/elido"
}
direct {}
}
Następnie make install-local z tools/terraform-provider-elido/, a terraform plan rozwiązuje binarkę bezpośrednio bez terraform init. To oficjalny wzorzec HashiCorpa dla rozwoju providerów i działa równie dobrze jako tymczasowa ścieżka instalacji do v1.0.0.
Co celowo nie trafiło do v0.1.0#
Trzy rzeczy, które rozważaliśmy, nie wysłaliśmy i chcemy wspomnieć, żeby nikt nie był zaskoczony.
Brak elido_custom_domain jako zasobu. Omówione powyżej. Źródło danych wystarczy do łańcuchowania domain_id w elido_link, co jest podstawowym przypadkiem użycia; pełne zarządzanie cyklem życia czeka na api-core. ETA: v0.2.0, połowa 2026.
Brak elido_folder, brak elido_api_key. SDK ma oba; zdecydowaliśmy się nie dodawać Schematów w v0.1.0, bo ich cykle życia nie są miejscem bólu klienta. Foldery to metadane organizacyjne; klucze API są typowo wydawane raz i rotowane przez dashboard. Dodamy je, gdy ktoś poprosi.
Brak generowania kodu z OpenAPI spec. HashiCorp dostarcza terraform-plugin-codegen-openapi jako narzędzie beta. Wypróbowaliśmy je na naszej specyfikacji; wygenerowane Schematy są miernej jakości - każde pole nullable staje się Optional + Computed, każda lista staje się Set, wynik wymaga tyle samo poprawek co ręcznie pisany Schemat i jest trudniejszy do rozwijania. Przy trzech zasobach na stole, ręczne pisanie wygrywa. Wrócimy do generatora za sześć miesięcy, kiedy więcej naszych rówieśników go przetestuje bojowo.
Co się psuło podczas budowania#
Trzy rzeczy, które zrobiliśmy źle za pierwszym razem.
Pierwsza to stan przy Optional + Computed. Początkowo modelowaliśmy title jako zwykły string Optional. Klienci, którzy go pominęli w HCL, dostawali czysty Create - a potem każdy kolejny terraform plan proponował ustawienie go z powrotem na null, bo serwer przechowywał pusty string i Terraform odczytywał to jako dryfowanie. Naprawą był plan modifier UseStateForUnknown; lekcja była taka, że interpretacja providera „użytkownik nie podał" musi pasować do idei serwera „wartość domyślna". Dokumentacja frameworka ostrzega o tym we wstępie; za pierwszym razem przeczytaliśmy przez to ostrzeżenie. Opisujemy to tutaj, żebyś uniknął tego wstydu.
Druga to format importu. Początkowo wysłaliśmy <workspace_id>/<link_id> ze slashem, opierając się na tym, że ścieżki czyta się naturalniej. Framework nie miał z tym problemu; lintery HCL i terminale miały. Ścieżka z dwoma slashami wewnątrz jednego argumentu w cudzysłowie powłoki wygląda jak literówka w zgłoszeniach supportowych. Przeszliśmy na dwukropek, który nie budzi żadnych wątpliwości i pasuje do konwencji providera Google. Lekcja: stringi importu to UI skierowane do użytkownika, projektuj je jak UI.
Trzecia to kolejność tagów. Omówiona powyżej - odkładamy to i będziemy odkładać, dopóki ktoś nie zapyta. Wersja, którą prawie wysłaliśmy, cicho sortowała tagi na wejściu, co sprawiało, że terraform plan nie raportował zmian, gdy klient wyraźnie je przekolejkował. To gorsze doświadczenie niż głośny diff; złapaliśmy to podczas testów wewnętrznych. Warto to powiedzieć, bo pokusa bycia „pomocnym" przez normalizowanie wejścia użytkownika jest stała przy pisaniu providera i prawie zawsze jest złym wyborem.
Jak używać tego z resztą Elido#
Provider to jeden kształt. Inne kształty nadal istnieją i nigdzie nie idą:
- REST API jest źródłem prawdy. Wszystko co robi provider, da się też zrobić przez
curl. - Go SDK to co provider sam używa wewnętrznie; możesz go wciągnąć jako bibliotekę.
- SDK TypeScript i Python pokrywają tę samą powierzchnię dla języka, w którym akurat pracujesz.
- Endpoint GraphQL pokrywa te same odczyty w jednej rundzie, gdy potrzebujesz danych dopasowanych do ekranu.
Wybierz to, co pasuje do kształtu problemu. Terraform jest dobry, gdy masz cykl życia do zarządzania. SDK jest dobry, gdy masz skrypt. REST API jest dobre, gdy robisz jedną rzecz raz. Uważamy, że powinno to być tak oczywiste; wszystkie cztery będziemy utrzymywać działające.
Jeśli masz ulubiony wzorzec Terraform, który nam umknął - masowe importy z CSV przez for_each nad blokiem data "external", for_each dopasowany do Linear API do śledzenia kampanii, opakowujący moduł dla przypadku agencji zarządzającej wieloma najemcami - otwórz issue w repozytorium GitHub z etykietą area:terraform. Provider istnieje po to, żeby te wzorce były nudne; chcemy wiedzieć, które nadal wydają się zaskakujące.
Od czego zacząć#
Jeśli przeczytałeś to i chcesz spróbować: zainstaluj provider zgodnie z przewodnikiem, wskaż go na sandbox workspace, napisz resource "elido_link" dla przekierowania, które zawsze chciałeś deklarować w kodzie, i wykonaj terraform apply. Stawiamy kawę, że pierwszą rzeczą, która Cię zaskoczy - w dobry sposób - będzie terraform destroy działające dokładnie tak, jak się spodziewasz.
Jeśli przeczytałeś to i chcesz porównać nas z alternatywami - jest dłuższy opis w naszym wpisie o lukach funkcji alternatyw Bitly i zestawienie na /compare/vs-bitly pokazuje gdzie Terraform siedzi w tej macierzy. Macierz skróciła się dla nich od czasu opublikowania tego wpisu.
- Marius
Powiązane na blogu#
Wypróbuj Elido
Wklej URL, otrzymaj krótki link
Bez rejestracji. Link działa 30 dni. Zarejestruj się, aby zachować go na zawsze.
Za darmo, bez rejestracji · 2 dziennie