Я зроблю невелику заяву, а потім підкріплю її фактами. Жоден сервіс скорочення URL наразі не пропонує першокласний Terraform провайдер. Bitly, TinyURL, Rebrandly, Short.io, Dub.co - усі п'ять надають REST API, деякі мають вебхуки, але жоден не публікує terraform-provider-*. На GitHub існує ком'юніті-провайдер для Bitly v3 API; він не підтримується і охоплює хіба що чверть можливостей API. Це та сама прогалина.
Кілька тижнів тому ми взялися її закрити. Результатом став terraform-provider-elido, який API Elido сьогодні представляє як elido_link (ресурс), elido_workspace (джерело даних) та elido_custom_domain (наразі лише джерело даних - читайте далі). Далі буде огляд того, що ми випустили, інженерних рішень та частин, які ми свідомо не додали у v0.1.0. Провайдер має відкритий вихідний код під тією ж ліцензією, що й решта Elido, і знаходиться за адресою tools/terraform-provider-elido/.
Чому короткі посилання мають бути в Terraform#
Аргумент простий. Якщо ви запускаєте маркетингові редиректи, у вас вже є інші частини інфраструктури, що сходяться на тій самій кампанії:
- DNS-запис Cloudflare, що вказує на лендінг.
- S3 бакет та дистрибуція CloudFront, що обслуговують цей лендінг.
- Сервіс Lambda або Cloud Run, що генерує підписані URL.
- Тег кампанії, вбудований у Google Tag Manager або Segment.
Усі ці п'ять елементів у 2026 році керуються як Terraform. Коротке посилання, що стоїть на початку воронки - точка входу, на яку клікає користувач - знаходиться в Google Sheets. Ця прогалина і є джерелом розбіжностей (drift). Лендінг стає застарілим, а редирект, що вказує на нього, продовжує жити, збираючи помилки 404, поки хтось не напише маркетингу в Slack.
Виправити цю прогалину можна двома способами. Можна написати скрипт на TypeScript, який буде посередником між вашим виводом Terraform та нашим REST API. Це працює; у нас є клієнти, які роблять саме так. Або ми можемо дати вам справжній Terraform провайдер, де редирект - це блок resource поруч із вашим записом Cloudflare, а terraform plan / terraform destroy знають про нього так само як і про все інше. Ми обрали другий шлях. Перший і так залишався за вами.
Що terraform-provider-elido робить сьогодні#
Мінімальне охоплення v0.1.0 у форматі 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 - і готово. Виявлення розбіжностей (drift detection) працює для полів, які повертає API. Перейменування мітки ресурсу Terraform або зміна slug під час роботи не вимагає заміни (replacement) - провайдер надсилає PATCH-запит до того самого цифрового ID. Зміна workspace_id або domain_id вимагає заміни, бо в цей момент мова йде про інший маршрут edge. Це логічний життєвий цикл, і саме до цього підштовхує фреймворк плагінів від HashiCorp.
Можливість масового розгортання - це та частина, яка виправдовує роботу для більшості команд:
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]]
}
Двадцять посилань, один apply. Видаляєте блок - двадцять видалень, один apply. Це приблизно той випадок використання, який згадувався у звітах трьох дзвінків з клієнтами минулого кварталу: маркетингу потрібні UTM-посилання для кожного каналу та регіону для запуску, інженерія щоразу пише скрипт Sheets-to-API, скрипт застаріває, його автор звільняється. Сила Terraform тут не в новизні, а в тому, що ми робимо цей паттерн звичайним.
Повний посібник з довідником атрибутів та прикладами імпорту знаходиться за адресою /docs/guides/terraform. Сирці провайдера містять examples/main.tf, який є більш розширеною версією наведеного вище фрагмента.
Як побудований провайдер#
Приблизно 600 рядків коду на Go, з яких ~200 - це визначення схем. Структура:
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
Кілька рішень, на яких варто зупинитися.
Ми використовуємо plugin framework, а не застарілий SDK. HashiCorp прямо рекомендувала переходити на terraform-plugin-framework у 2023 році. Більшість популярних провайдерів (aws, cloudflare, google) знаходяться в процесі міграції; менші та новіші провайдери є нативними для фреймворку. Створення нового продукту на застарілому SDK означало б необхідність міграції відразу після випуску. Ми уникли міграції, не створюючи її. Фреймворк має суворішу систему типів, реальну валідацію схем на рівні протоколу плагіна та набагато чистішу модель планування (PlanModifiers замість колбеків CustomizeDiff). Для невеликого провайдера різниця в ергономіці величезна.
Провайдер не дублює SDK. Кожен метод ресурсу делегує виконання packages/sdk-go, який є тим самим SDK, що ми публікуємо для звичайної інтеграції на Go. Провайдер за задумом є тонким адаптером Schema-to-SDK. Це має два наслідки. Хороший: будь-яке виправлення в SDK автоматично потрапляє в провайдер. Поганий: будь-яка прогалина в SDK є прогалиною в провайдері. Чесний приклад - кастомні домени. api-core поки не підтримує POST/DELETE для /v1/workspaces/{id}/domains; шлях запису знаходиться в domain-manager за панеллю керування. Поки api-core не проксіює запис, в SDK немає Domains.Create, а в провайдері немає ресурсу elido_custom_domain - лише джерело даних, яке шукає існуючий домен за ім'ям хоста. Ми закриємо цю прогалину у v0.2.0; проксі-шар - це завдання на кілька днів, а PR для SDK та провайдера вже в чернетках.
Авторизація має ту саму форму, що й у будь-якого іншого клієнта Elido. Токен API Bearer у заголовку Authorization з резервним використанням ELIDO_API_TOKEN із оточення. Ми не додаємо авторизацію через cookie або X-Dev-User-ID у провайдер; це зручності для локальної розробки, яким не місце в IaC, де конфігурація зберігається в системі контролю версій і запускається в CI. Ваш CI або має токен, або ні.
Виявлення відхилень (Drift detection): частина, яка складніша, ніж здається#
Якщо ви прочитали все до цього моменту, цей розділ вартий вашої уваги. Диффінг у Terraform - це фундаментальне питання: маючи те, що написав користувач (Plan), що сервер повернув минулого разу (State) та що сервер повертає зараз (Read), що ми маємо запропонувати зробити?
Для такого ресурсу як elido_link, три речі роблять це нетривіальним:
Поля Optional + Computed із серверними значеннями за замовчуванням. Користувач може пропустити redirect_status. Сервер встановлює 302. Наступний Read повертає 302. Без належної обробки це виглядає як розбіжність при кожному плані - «Я нічого не просив, отримав 302, пропоную знову встановити нічого». Фреймворк надає модифікатор плану UseStateForUnknown, який каже: «якщо у мене немає запланованого значення, залиш те, що є у стані». Ми використовуємо його для кожного поля, яке має значення за замовчуванням на сервері. Це звучить тривіально, але це джерело найчастіших багів у екосистемі провайдерів («провайдер видав несумісний результат після apply»).
Теги із серверною нормалізацією. Наш API зберігає теги як набір (set); Terraform бачить їх як впорядкований список. Наразі ми це відклали. Сервер зберігає порядок при відповіді, тому на практиці дифф стабільний, але користувач, який змінить порядок тегів у HCL, побачить оновлення без змін (no-op). Це правильна поведінка; альтернатива - неявне сортування на вході - означала б, що terraform plan та terraform apply розходяться у поглядах на зміни, що є смертним гріхом у Terraform. Ми повернемося до цього, якщо клієнти будуть скаржитися. Посібник з найкращих практик від HashiCorp чітко стоїть на позиції «не робіть нічого несподіваного».
Статус як трипозиційний перемикач. Посилання може бути active, paused або archived. Встановлення status = "paused" в HCL, але не при створенні (сервер за замовчуванням ставить active), означає, що ми маємо надіслати наступний PATCH-запит у межах того самого Create. Це реалізовано як крок звірки після створення - майте це на увазі, якщо читаєте сирці. Альтернатива - представлення статусу як окремого ресурсу (elido_link_status із ключем link_id) - це те, що робить провайдер AWS для деяких ресурсів. Ми розглядали це; для одного опціонального поля витрати перевищують вигоду. Якщо ми додамо другий такий параметр, то передумаємо.
Імпорт. terraform import elido_link.spring_campaign 42:7 - це <workspace_id>:<link_id>. Ми обрали формат через двокрапку, бо колбек ImportState у фреймворку дає один рядок, який ви парсите самі. Формат <id>:<id> є поширеним у провайдерах, де ресурси ідентифікуються кортежем - див. документацію з імпорту google_compute_instance для канонічного прикладу. Ми свідомо не перевантажуємо людиночитаний slug; стан ресурсу прив'язаний до цифрового ID, і це єдине, що ви повинні вказувати при імпорті.
Тести, CI, реєстр#
Юніт-тести (на сьогодні 7 тестів) охоплюють рівень валідації схем та чисті допоміжні функції - splitImportID, linkToModel, apiErrorString, optString. Вони виконуються за 0.5 секунди і перевіряють кожен PR через ту саму матрицю go, що будує наші 13 сервісів. Також є ціль testacc, яка запускається проти працюючого api-core, коли встановлено TF_ACC=1, але це опціонально: для цього потрібен токен, і ми не запускаємо це при кожному коміті, бо кожен тест створює та видаляє справжнє посилання. Тестувальний фреймворк HashiCorp описує цей паттерн; ми його дотримуємося.
Пайплайн релізу підключений до goreleaser з точною матрицею збірок, яку очікує Terraform Registry: linux, darwin, freebsd, windows × amd64/arm64 (плюс arm та 386 на Linux), SHA256SUMS архівів, GPG-підпис для SHA256SUMS та terraform-registry-manifest.json із оголошенням protocol_versions: ["6.0"]. Позначте коміт тегом terraform-provider-vX.Y.Z, воркфлоу GitHub Actions запустить goreleaser release --clean, і GitHub Release опублікується. Terraform Registry опитує релізи за власним графіком та додає нову версію. Єдине, чого зараз не вистачає - це GPG-ключ; ми створюємо спеціальний ключ для релізів провайдера цього тижня, а це означає, що v0.1.0 з'явиться в реєстрі приблизно в той самий час, що й цей допис.
Тим часом встановлюйте через dev_overrides у ~/.terraformrc:
provider_installation {
dev_overrides {
"elidoapp/elido" = "/Users/<you>/.terraform.d/plugins/elidoapp/elido"
}
direct {}
}
Потім виконайте make install-local у tools/terraform-provider-elido/, і terraform plan знайде бінарний файл безпосередньо без terraform init. Це офіційний паттерн HashiCorp для розробки провайдерів, і він так само добре працює як тимчасовий шлях встановлення до виходу v1.0.0.
Що ми свідомо не включили у v0.1.0#
Три речі, які ми розглядали, не випустили і хочемо озвучити, щоб ніхто не дивувався.
Немає elido_custom_domain як ресурсу. Обговорено вище. Джерела даних достатньо, щоб передати domain_id у elido_link, що є основним випадком використання; повне керування життєвим циклом чекає на api-core. Очікуваний час: v0.2.0, середина 2026 року.
Немає elido_folder, немає elido_api_key. В SDK є обидва; ми вирішили не додавати схеми у v0.1.0, бо їхні життєві цикли не є головним болем для клієнтів. Папки - це організаційні метадані; API-ключі зазвичай видаються один раз і ротуються через панель керування. Ми додамо їх, коли хтось попросить.
Жодної генерації коду зі специфікації OpenAPI. HashiCorp пропонує terraform-plugin-codegen-openapi як бета-інструмент. Ми спробували його на нашій специфікації; згенеровані схеми посередні - кожне поле, що може бути null, стає Optional + Computed, кожен список стає Set, результат вимагає стільки ж доопрацювання, скільки й написана вручну схема, і його важче розвивати. Маючи лише три ресурси, ручне написання виграє. Ми повернемося до генератора через шість місяців, коли більше наших колег протестують його в бою.
Що зламалося під час розробки#
Три речі, які ми зробили неправильно з першої спроби.
Першою була робота зі станом для Optional + Computed. Спочатку ми змоделювали title як звичайний рядок Optional. Клієнти, які пропускали його в HCL, отримували чистий Create - а потім кожен наступний terraform plan пропонував встановити його назад у null, бо сервер зберігав порожній рядок, а Terraform зчитував це як розбіжність. Виправленням став модифікатор плану UseStateForUnknown; урок полягає в тому, що інтерпретація провайдером фрази «користувач не вказав» має збігатися з уявленням сервера про «значення за замовчуванням». Документація фреймворку попереджає про це у вступі; ми пропустили це попередження першого разу. Позбавили вас сорому, описавши це тут.
Другою була помилка у форматі імпорту. Спочатку ми випустили <workspace_id>/<link_id> через слеш, виходячи з теорії, що шляхи читаються природніше. У фреймворку з цим не було проблем, а у лінтерів HCL та терміналів - були. Шлях із двома слешами всередині одного аргументу в лапках перетворюється на щось, що виглядає як помилка у тікетах підтримки. Ми перейшли на двокрапку, яка не має двозначності та відповідає конвенціям провайдера Google. Урок: рядки імпорту - це інтерфейс користувача, проектуйте їх як інтерфейс.
Третьою була помилка з порядком тегів. Обговорювалося вище - ми це відклали і будемо відкладати далі, поки хтось не попросить. Версія, яку ми майже випустили, неявно сортувала теги на вході, через що terraform plan повідомляв про відсутність змін, коли клієнт явно змінив їх порядок. Це гірший досвід, ніж шумний дифф; ми помітили це під час внутрішнього тестування. Варто про це сказати, бо спокуса «бути корисним», нормалізуючи ввід користувача, постійно виникає при написанні провайдера, і це майже завжди неправильне рішення.
Як використовувати це з іншими частинами Elido#
Провайдер - це лише одна форма. Інші форми все ще існують і нікуди не зникнуть:
- REST API - це першоджерело істини. Усе, що робить провайдер, можна зробити і через
curl. - Go SDK - це те, що провайдер використовує всередині; ви можете підключити його як бібліотеку.
- TypeScript та Python SDK охоплюють ті ж самі можливості для мов, якими ви користуєтесь.
- GraphQL endpoint охоплює ті самі запити на читання за один запит, коли вам потрібно підлаштувати дані під ваш екран.
Обирайте те, що підходить для вашої задачі. Terraform ідеальний, коли потрібно керувати життєвим циклом. SDK підходить для скриптів. REST API - коли потрібно зробити щось один раз. Ми вважаємо, що це має бути очевидним; ми підтримуватимемо роботу всіх чотирьох варіантів.
Якщо у вас є улюблений паттерн Terraform, якого нам не вистачає - масовий імпорт із CSV через for_each над блоком data "external", for_each підключений до Linear API для відстеження кампаній, модуль-обгортка для управління кількома тенантами в агентстві - відкрийте issue у репозиторії GitHub з міткою area:terraform. Провайдер існує, щоб зробити ці паттерни звичайними; ми хочемо знати, які з них все ще здаються складними.
З чого почати#
Якщо ви прочитали це і хочете спробувати: встановіть провайдер згідно з посібником, спрямуйте його на тестовий воркспейс, напишіть resource "elido_link" для редиректу, який ви завжди хотіли описати в коді, і виконайте terraform apply. Ми готові закластися на каву, що перше, що вас приємно здивує - це те, що terraform destroy працює саме так, як ви очікуєте.
Якщо ви прочитали це і хочете порівняти нас із альтернативами - є розгорнутий допис про прогалини у функціоналі альтернатив Bitly, а порівняння пліч-о-пліч на /compare/vs-bitly показує місце Terraform у матриці. Після виходу цього допису матриця для них стала ще коротшою.
- Marius
Схоже у блозі#
Спробуйте Elido
Вставте URL - отримайте коротке посилання
Без реєстрації. Посилання живе 30 днів. Зареєструйтесь, щоб зберегти назавжди.
Безкоштовно, без реєстрації · 2 на день