Я сделаю небольшое заявление, а затем его обосную. Ни один сокращатель ссылок на данный момент не предоставляет первоклассный 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 и источника данных в v0.1.0 elido_custom_domain - подробнее об этом ниже. Далее мы разберем, что именно вошло в релиз, какие инженерные решения мы приняли и что сознательно решили не включать в 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 - это планируемый адрес в реестре, он ещё не опубликован; см. раздел об установке ниже.) Обнаружение дрейфа работает на основе полей, которые API возвращает в ответ. Переименование метки ресурса Terraform или изменение slug на лету не приводит к пересозданию ресурса - провайдер отправляет PATCH-запрос к тому же числовому ID. Изменение workspace_id или domain_id вызывает принудительную замену (force replacement), потому что в этот момент речь идет уже о другом пограничном (edge) маршруте. Это логичный жизненный цикл, к которому подталкивает plugin framework от 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-метками для каждого канала и региона для запуска, инженеры каждый раз пишут скрипт для переноса из Google Sheets в 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. В 2023 году HashiCorp явно направила разработчиков новых провайдеров на использование terraform-plugin-framework. Большинство популярных провайдеров (aws, cloudflare, google) находятся в процессе миграции; небольшие и новые проекты создаются сразу на базе фреймворка. Создание нового провайдера на базе старого SDK означало бы планирование задачи по миграции сразу после релиза. Мы избежали миграции, просто не создавая для нее причин. У фреймворка более строгая типизация, реальная валидация схем на уровне протокола плагина и гораздо более чистая модель планирования (PlanModifiers вместо колбэков CustomizeDiff). Для небольшого провайдера разница в эргономике огромна.
Провайдер не дублирует SDK. Каждый метод ресурса делегирует выполнение packages/sdk-go, то есть Go SDK, опубликованному как модуль github.com/elidoapp/elido-go (v0.1.0). Провайдер по задумке является тонким адаптером между Schema и 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 передается в заголовке Authorization, с возможностью использования переменной окружения ELIDO_API_TOKEN. Мы не открываем авторизацию через куки или X-Dev-User-ID в провайдере; это удобства для локальной разработки, которым не место в IaC, где конфигурация хранится в системе контроля версий и запускается в CI. У вашего CI либо есть токен, либо его нет.
Обнаружение дрейфа: часть, которая сложнее, чем кажется
Если вы дочитали до этого момента, этот раздел заслуживает внимания. Диффинг в Terraform - это принципиальный вопрос: имея то, что написал пользователь (Plan), то, что сервер вернул в прошлый раз (State), и то, что сервер возвращает сейчас (Read), что именно мы должны предложить сделать?
Для такого ресурса, как elido_link, три момента делают эту задачу нетривиальной:
Поля Optional + Computed со значениями по умолчанию на стороне сервера. Пользователь может не указывать redirect_status. Сервер подставляет 302. При следующем вызове Read возвращается 302. Без должной осторожности это будет выглядеть как дрейф при каждом планировании: «Я ничего не просил, получил 302, предлагаю снова сбросить в "ничего"». Фреймворк предоставляет модификатор плана UseStateForUnknown, который говорит: «Если у меня нет запланированного значения, сохрани то, что есть в состоянии». Мы используем его для каждого поля, имеющего значение по умолчанию на сервере. Это звучит просто, но именно здесь чаще всего возникают ошибки в экосистеме провайдеров («provider produced inconsistent result after apply»).
Теги с нормализацией на стороне сервера. Наш API хранит теги как множество (set); Terraform видит их как упорядоченный список. На данный момент мы не решаем эту проблему радикально. Сервер сохраняет порядок при ответе, поэтому на практике дифф остается стабильным, но пользователь, который изменит порядок тегов в HCL, увидит обновление, не вносящее изменений (no-op update). Это корректное поведение; альтернатива - неявная сортировка на входе - означала бы, что 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), контрольные суммы SHA256 для архивов, GPG-подпись для SHA256SUMS и файл terraform-registry-manifest.json с объявлением protocol_versions: ["6.0"]. Стоит пометить коммит тегом terraform-provider-vX.Y.Z, как ворклоу GitHub Actions запускает goreleaser release --clean, и релиз на GitHub становится доступен. 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, создание проходило успешно, но затем каждый последующий terraform plan предлагал сбросить его в null, так как сервер сохранял пустую строку, а Terraform видел в этом дрейф. Решением стал модификатор плана UseStateForUnknown; урок в том, что интерпретация провайдером фразы «пользователь не указал значение» должна совпадать с серверным представлением о значении по умолчанию. Документация фреймворка предупреждает об этом во вступлении; в первый раз мы просмотрели это предупреждение. Записываю это здесь, чтобы избавить вас от неловкости.
Вторым был формат импорта. Изначально мы использовали <workspace_id>/<link_id> со слэшем, исходя из теории, что пути читаются естественнее. У фреймворка не было с этим проблем, а вот у HCL-линтеров и терминалов - были. Путь с двумя слэшами внутри одного аргумента командной строки в тикетах поддержки выглядит как опечатка. Мы перешли на двоеточием, которое исключает неоднозначность и соответствует конвенциям провайдера Google. Урок: строки импорта - это пользовательский интерфейс, проектируйте их как UI.
Третьим был порядок тегов. Об этом уже говорилось выше - мы решили оставить всё как есть, пока кто-нибудь не попросит об обратном. Версия, которую мы чуть не выпустили, негласно сортировала теги на входе, из-за чего terraform plan не сообщал об изменениях, когда клиент явно поменял их порядок. Это хуже, чем «шумный» дифф; мы поймали это на внутреннем тестировании. Стоит сказать об этом, потому что искушение «помочь» пользователю, нормализовав ввод, возникает постоянно при написании провайдера, и почти всегда это неверное решение.
Как использовать это с остальными частями Elido
Провайдер - это лишь одна из форм. Остальные никуда не делись и продолжают работать:
- REST API остается первоисточником. Всё, что делает провайдер, можно сделать и через
curl. - Go SDK - это то, что провайдер использует внутри; он публичен как
github.com/elidoapp/elido-go(v0.1.0). - TypeScript и Python SDK будут покрывать те же возможности для вашего языка, как только выйдут.
- GraphQL-эндпоинт позволяет выполнять те же операции чтения за один запрос, когда данные нужны в специфической форме для интерфейса.
Выбирайте то, что лучше подходит для решения вашей задачи. 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 ссылок в месяц.