Я зроблю невелику заяву, а потім підкріплю її фактами. Жоден сервіс скорочення 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 - читайте далі). Далі буде огляд того, що ми випустили, інженерних рішень та частин, які ми свідомо не додали у v0.1.0. Оновлення, листопад 2026: провайдер у дереві репозиторію тепер має версію v0.2.0, яка додає elido_custom_domain як ресурс. Провайдер ще не опублікований: він живе в нашому монорепозиторії за адресою 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 - і готово. (Джерело elidoapp/elido - це запланована адреса в реєстрі, її ще не опубліковано; див. розділ про встановлення нижче.) Виявлення розбіжностей (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, тобто Go SDK, опублікованому як модуль github.com/elidoapp/elido-go (v0.1.0). Провайдер за задумом є тонким адаптером Schema-to-SDK. Це має два наслідки. Хороший: будь-яке виправлення в SDK автоматично потрапляє в провайдер. Поганий: будь-яка прогалина в SDK є прогалиною в провайдері. Чесний приклад - власні домени. Коли вийшла v0.1.0, api-core не надавав POST/DELETE для /v1/workspaces/{id}/domains; шлях запису жив в окремому сервісі domain-manager за панеллю керування, тож у провайдера не було ресурсу elido_custom_domain - лише джерело даних, що шукає наявний домен за іменем хоста. Цю прогалину закрито: domain-manager відтоді влито в api-core (ADR-0047), api-core тепер обслуговує маршрути створення, верифікації та видалення домену, а провайдер v0.2.0 у дереві репозиторію має ресурс elido_custom_domain, який реєструє ім'я хоста й чекає на DNS-верифікацію.
Авторизація має ту саму форму, що й у будь-якого іншого клієнта 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 тестів у v0.1.0) охоплюють рівень валідації схем та чисті допоміжні функції - splitImportID, linkToModel, apiErrorString, optString; v0.2.0 додає тести для ресурсу власного домену. Вони виконуються приблизно за півсекунди через ту саму матрицю go, що будує наші шість Go-сервісів. Також є ціль 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-ключ, тому провайдера ще немає в реєстрі; публічний реліз незабаром.
Тим часом встановлюйте через 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 для розробки провайдерів, але він потребує локальної збірки провайдера, тож до публічного релізу практичний шлях - REST API.
Що ми свідомо не включили у v0.1.0
Три речі, які ми розглядали, не випустили і хочемо озвучити, щоб ніхто не дивувався.
elido_custom_domain як ресурс. Обговорено вище. У v0.1.0 джерела даних було достатньо, щоб передати domain_id у elido_link, що є основним випадком використання. Повне керування життєвим циклом відтоді з'явилося у v0.2.0 у дереві репозиторію як ресурс elido_custom_domain; як і решта провайдера, його ще немає в реєстрі.
Немає 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 - це те, що провайдер використовує всередині; він публічний як
github.com/elidoapp/elido-go(v0.1.0). - TypeScript та Python SDK охоплюватимуть ті ж самі можливості для мов, якими ви користуєтесь, щойно вийдуть.
- GraphQL endpoint охоплює ті самі запити на читання за один запит, коли вам потрібно підлаштувати дані під ваш екран.
Обирайте те, що підходить для вашої задачі. Terraform ідеальний, коли потрібно керувати життєвим циклом. SDK підходить для скриптів. REST API - коли потрібно зробити щось один раз. Ми вважаємо, що це має бути очевидним; ми підтримуватимемо роботу всіх чотирьох варіантів.
Якщо у вас є улюблений паттерн Terraform, якого нам не вистачає - масовий імпорт із CSV через for_each над блоком data "external", for_each підключений до Linear API для відстеження кампаній, модуль-обгортка для управління кількома тенантами в агентстві - розкажіть нам, які з них вам потрібні. Провайдер існує, щоб зробити ці паттерни звичайними; ми хочемо знати, які з них все ще здаються складними.
З чого почати
Якщо ви прочитали це і хочете спробувати: провайдер ще не публічний, тож поки читайте посібник і користуйтесь REST API; коли реліз вийде, спрямуйте його на тестовий робочий простір, напишіть resource "elido_link" для редиректу, який ви завжди хотіли описати в коді, і виконайте terraform apply. Ми готові закластися на каву, що перше, що вас приємно здивує - це те, що terraform destroy працює саме так, як ви очікуєте.
Якщо ви прочитали це і хочете порівняти нас із альтернативами - є розгорнутий допис про прогалини у функціоналі альтернатив Bitly, а порівняння пліч-о-пліч на /compare/vs-bitly показує місце Terraform у матриці. Після виходу цього допису матриця для них стала ще коротшою.
- Marius
Схоже у блозі
Спробуйте Elido
Вставте URL - отримайте коротке посилання
Без реєстрації. Посилання живе 30 днів. Зареєструйтесь, щоб зберегти назавжди.
Без реєстрації. Зареєструйтеся безкоштовно і отримайте 50 посилань на місяць.