Сорок клиентских сайтов. У каждого четыре проверки: доступность, сертификат, срок домена, битые ссылки. Сто шестьдесят проверок, заведённых руками через веб-интерфейс.
Пока их двадцать, это терпимо. Когда приходит сорок первый клиент, ты открываешь кабинет и снова кликаешь: создать проверку, интервал, порог, теги, сохранить. Четыре раза. А потом кто-то спрашивает: «а почему у этого сайта порог два, а у соседнего три?» — и честный ответ звучит как «не помню».
Инфраструктуру мы описываем кодом уже лет десять. Мониторинг — почему-то нет. Хотя это ровно такая же конфигурация: её надо ревьюить, версионировать и уметь воспроизвести.
Я написал Terraform-провайдер для сервиса мониторинга, который делаю (раскрываю сразу: это Pingvera), и довёл его до публикации в публичном Terraform Registry. Ниже — решения, о которых пришлось подумать, и грабли, на которые я наступил: где держать валидацию, зачем отдельный репозиторий, как подхватить то, что уже создано мышкой, и что именно требует реестр.
Ссылка на продукт в тексте одна — в конце, на исходники. Всё остальное применимо к любому своему провайдеру, мониторинг тут просто предметная область.
Первое решение: сколько логики класть в провайдер
Когда садишься писать провайдер, первый же вопрос — что он вообще должен знать.
Соблазн понятный: сделать провайдер умным. Пусть проверяет, что интервал не меньше минимума тарифа, что мониторов не больше лимита, что тип tcp требует порт в адресе. Тогда ошибки видны на terraform plan, до всякого обращения к серверу. Красиво.
Я выбрал обратное — провайдер знает ровно ничего. Он тонкий клиент существующего write-API: сериализовал HCL в JSON, отправил, вернул ошибку сервера как есть. Вся валидация, квоты и лимиты остаются там, где они уже написаны и уже покрыты тестами.
Цена этого решения честная и заметная. Если у пользователя кончился лимит мониторов, terraform plan покажет аккуратный список изменений, а apply упадёт на середине. Неприятно.
Но альтернатива хуже. Продублировать логику квот в провайдере — значит завести второй источник правды, который начнёт расходиться с сервером в первый же день, когда я поменяю тариф и забуду про репозиторий провайдера. Расхождение будет тихим: провайдер уверенно скажет «всё хорошо», а сервер откажет. Или наоборот — провайдер запретит то, что сервер разрешает, и пользователь будет ругаться на пустом месте.
Из двух зол я выбрал то, которое хотя бы честное: сервер — единственный, кто знает правила.
Второе решение: отдельный репозиторий
Провайдер живёт в github.com/pingvera/terraform-provider-pingvera — отдельном публичном репозитории со своим go.mod. Сначала он лежал в монорепо основного приложения, и это выглядело удобным: одна сборка, одни зависимости, всё под рукой.
Ровно до go mod tidy.
Terraform Plugin Framework тянет собственный граф зависимостей — hashicorp/go-plugin, свои версии gRPC и protobuf. Основное приложение тоже использует gRPC и protobuf, только других версий. Go разруливает такое одним общим набором версий на модуль, то есть кто-то один обязательно едет на неродной версии. Работает, пока не сломается — и ломается это в самом неудобном месте.
Отдельный go.mod снимает вопрос целиком: обновление Plugin Framework больше не касается основного приложения, а обновление gRPC в приложении не трогает провайдер.
А потом выяснилось, что выбора и не было. Terraform Registry требует, чтобы репозиторий назывался ровно terraform-provider-NAME. Не «желательно», а иначе публикация просто невозможна. Так что если планируете реестр — отдельный репозиторий у вас будет в любом случае, вопрос только в том, узнаете вы об этом до переноса кода или после.
Framework, а не SDK
HashiCorp поддерживает два способа писать провайдеры: старый Plugin SDK и Plugin Framework. Для нового провайдера в 2026-м это выбор без интриги, но пару вещей стоит назвать.
Схема ресурса в Framework описывается типами, и компилятор ловит расхождения на сборке, а не в рантайме на чужой машине. Операции разнесены по методам: Create, Read, Update, Delete — отдельные функции интерфейса, а не поля структуры с колбэками. Импорт поддержан из коробки, что мне понадобилось буквально сразу.
И приятная мелочь, которую замечаешь не сразу: диагностики накапливаются. Провайдер возвращает не первую попавшуюся ошибку, а все найденные разом. Пользователь чинит конфиг за один заход, а не методом «исправил — запустил — узнал про следующую».
SDK никуда не делся и поддерживается. Но начинать новый провайдер на нём сегодня — примерно как начинать новый проект на Python 2.
Что провайдер умеет
Два ресурса. Мониторы и статус-страницы — всё, что нужно, чтобы описать наблюдение за клиентским сайтом.
Монитор — одна проверка. Тип задаётся полем type: http, tcp, dns, tls, wp, domain, links, heartbeat.
resource "pingvera_monitor" "site" { type = "http" name = "Основной сайт" target = "https://example.com" interval_s = 60 fail_threshold = 2 degraded_latency_ms = 800 tags = ["prod", "website"] config = jsonencode({ fail_on_4xx = true })}
Здесь стоит остановиться на config. Это сырой JSON, а не набор типизированных полей — и это тоже осознанно.
У каждого типа проверки свои настройки: у http — реагировать ли на 4xx, у links — глубина обхода, у domain — за сколько дней предупреждать. Если описать их атрибутами схемы, провайдер начнёт знать про внутренности каждого типа. Добавили настройку на сервере — надо выпускать новую версию провайдера, иначе её не задать. Один jsonencode избавляет от этой связки: сервер эволюционирует, провайдер не мешает.
Минус ровно один и предсказуемый: опечатку внутри config Terraform не поймает, про неё скажет сервер. Меня это устраивает — см. первое решение.
Остальные проверки для того же клиента выглядят скучно, и это хорошо:
resource "pingvera_monitor" "ssl" { type = "tls" name = "SSL-сертификат" target = "example.com:443"}resource "pingvera_monitor" "domain" { type = "domain" name = "Срок домена" target = "example.com"}resource "pingvera_monitor" "links" { type = "links" name = "Битые ссылки" target = "https://example.com"}
Статус-страница собирается из мониторов по их идентификаторам:
resource "pingvera_status_page" "example" { slug = "example" title = "Статус example.com" theme = "dark" monitors = [ pingvera_monitor.site.id, pingvera_monitor.ssl.id, ] brand_color = "#4f46e5" footer_md = "Вопросы — support@example.com"}
Вот тут Terraform показывает себя: добавили монитор — он сам появился на статус-странице, потому что ссылка живёт в коде, а не в чьей-то голове.
Самое интересное: что делать с тем, что уже создано мышкой
Провайдер появился не в первый день жизни сервиса. К этому моменту у людей уже были заведены мониторы — те самые сто шестьдесят.
И тут выясняется неприятное: просто начать писать .tf нельзя. Terraform про существующие ресурсы не знает и на первом же apply создаст их заново. В лучшем случае получите дубли и двойные оповещения, в худшем — ошибку на середине применения.
Классический ответ — terraform import. Пишешь пустой блок ресурса, выполняешь команду с идентификатором, повторяешь. Для ста шестидесяти проверок это сто шестьдесят команд, и где-то на сороковой вы перепутаете идентификаторы местами.
Terraform 1.5 добавил config-driven import — блок import прямо в конфигурации:
import { to = pingvera_monitor.site id = "mon_01JQ8ZK3X9"}resource "pingvera_monitor" "site" { type = "http" name = "Основной сайт" target = "https://example.com"}
Теперь plan читает реальное состояние ресурса и сравнивает с тем, что вы написали, а apply берёт его под управление, ничего не пересоздавая.
Критерий, что миграция удалась, простой и жёсткий: после apply план должен быть пустым. Если Terraform всё ещё что-то хочет изменить — значит, в коде вы описали не то, что реально настроено, и это надо чинить сейчас, а не через полгода в три часа ночи.
Но писать блоки import руками для ста шестидесяти проверок — та же ручная работа, только переехавшая в редактор. Поэтому в CLI появилась команда:
pingvera terraform generate > pingvera.tf
Она читает read-API аккаунта и печатает готовый файл: и resource-блоки с текущими настройками, и import-блоки с идентификаторами. Дальше — terraform apply, потом terraform plan и проверка, что он пустой.
Это одноразовая операция, а не режим работы. Сгенерировали, убедились, что план пуст, — и дальше конфигурация живёт в git и меняется только через Terraform.
Публикация в реестре: три вещи, о которых лучше знать заранее
Публикация оказалась не сложной, но с сюрпризами по мелочи. Три вещи стоит сделать до того, как соберётесь релизить.
Имя репозитория. Ровно terraform-provider-NAME, публичный, на GitHub. Про это я уже говорил, но повторю здесь: люди узнают об этом в момент публикации, когда код уже написан и лежит не там.
GPG-подпись. Реестр принимает только подписанные релизы. Нужно создать ключ, добавить публичную часть в настройки аккаунта на registry.terraform.io, а приватную — в секреты GitHub Actions. Отпечаток ключа при этом попадает в переменную окружения сборки — на него ссылается конфигурация goreleaser.
Goreleaser. Он собирает бинарники под пять платформ, пакует в zip нужного формата, считает контрольные суммы и подписывает их. Конфигурация выглядит так:
builds: - env: [CGO_ENABLED=0] mod_timestamp: "{{ .CommitTimestamp }}" flags: [-trimpath] ldflags: ["-s -w -X main.version={{.Version}}"] goos: [linux, darwin, windows] goarch: [amd64, arm64]archives: - format: zip name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"signs: - artifacts: checksum args: ["--batch", "--local-user", "{{ .Env.GPG_FINGERPRINT }}", "--output", "${signature}", "--detach-sign", "${artifact}"]checksum: name_template: "{{ .ProjectName }}_{{ .Version }}_SHA256SUMS" algorithm: sha256
Дальше всё происходит само: пушите тег v0.1.0 — CI собирает релиз, реестр подхватывает его в течение нескольких минут. Мой провайдер появился там 20 июля, версия 0.1.0, тир community.
Что бы я сказал себе в начале
Если соберётесь писать свой провайдер — четыре вещи, которые стоят раздумий до первой строчки кода.
Решите, где живёт валидация. Тонкий клиент проще и честнее, но ошибки уезжают из plan в apply, и с этим надо смириться заранее, а не спорить потом в issues.
Заведите отдельный репозиторий сразу. Если планируете реестр — это не вопрос вкуса, а требование, и переносить код позже неприятно.
Подумайте про уже существующие ресурсы. Провайдер, которым нельзя взять под управление то, что создано до него, — половина провайдера. Config-driven import и генератор конфигурации решают это малой кровью.
Настройте GPG и goreleaser до первого релиза. Иначе первый релиз придётся переделывать, а теги, как известно, лучше не двигать.
Исходники открыты: github.com/pingvera/terraform-provider-pingvera. Если делаете свой — забирайте оттуда что пригодится, там ровно то, что описано выше.
ссылка на оригинал статьи https://habr.com/ru/articles/1063658/