Первые шаги миграции на IaC: SDN.Use, разбитые ARP-проверки и перенос Caddy
22 июля 2026
Токен root@pam не спас от ошибки прав в Proxmox — хотя, казалось бы, куда уж дальше. Разгадка заняла час и оказалась вообще не про права. Расписал, как поднимал первую IaC-инфраструктуру для домашнего Proxmox-кластера: control-нода, OpenTofu, Ansible и первый по-настоящему перенесённый сервис. Спойлер: SDN.Use внезапно требуется для банального VLAN-тега, Rocky Linux выбрал ради портфолио и наполовину от неё отказался, а собственный скрипт проверки IP молчал как рыба, стоило адресу оказаться в соседнем VLAN.
Домашний Proxmox-кластер живёт в режиме «настроил руками и забыл» — VM и LXC без единой версии конфигурации, без возможности воспроизвести с нуля. Ну в общем это уже было сказано тут. Эта статья — первый шаг: нода, с которого будет разворачиваться всё остальное, первичная конфигурация на нём и переезд первого, самого простого из используемых контейнеров.
Управляющая нода и первые проблемы
Для самой управляющей ноды выбрал дистрибутив «посложнее» — Rocky вместо привычного Debian/Ubuntu. Решение в духе «хочу немного пострадать ради портфолио». Это решение позже напомнило о себе.
Внутри поставил ansible-core, tofu через
официальный install-скрипт и создал репозиторий под всю конфигурацию.
В целом на этом этапе ничего особенного и не происходило, кроме как
WARN: Systemd 257 detected. You may need to enable nesting,
что и не было критичным... по крайней мере я так думал.
Proxmox ACL — SDN.Use и жёсткий root@pam-only на feature-флаги
Завёл отдельного Proxmox-пользователя terraform-prov@pve
с ограниченными ролями (PVEVMAdmin +
PVEDatastoreAdmin) вместо того, чтобы просто выдать
OpenTofu токен root@pam. Всё-таки принцип «своя
идентичность под каждую роль» не просто так придумали, позже сделал
отдельный SSH-ключ для Ansible.
Первая проблема с правами оказалась неожиданной: создание LXC с
VLAN-тегом на обычном линукс-бридже (без настроенных SDN-зон)
внезапно потребовало привилегию SDN.Use. Proxmox,
начиная с какой-то версии, заворачивает даже банальный VLAN-тег в
модель прав SDN. Чинится одной командой:
pveum acl modify /sdn --users terraform-prov@pve --roles PVESDNUser
Вторая проблема оказалась куда интереснее. Попытка добавить
features { nesting = true } к уже
существующему контейнеру (через tofu apply на
уже созданный ресурс) упала с:
Permission check failed (changing feature flags (except nesting) is only allowed for root@pam)
Первая реакция — очевидная: раз ограниченному пользователю не
хватает прав, выдать провайдеру токен root@pam вместо
terraform-prov@pve. Не помогло. Та же
самая ошибка, слово в слово, несмотря на то что токен теперь
принадлежал root'у.
Разгадка (спасибо форумам): проверка ждёт аутентифицированного
пользователя со строкой root@pam, а API-токен — даже
выпущенный на root — аутентифицируется как
root@pam!<token-name>. То есть ограничение в
принципе непреодолимо через API-токен, вне зависимости от того, чей
это токен.
Реальная разгадка оказалась вообще не про права, а про
create vs. modify: то же самое поле
features.nesting, объявленное в ресурсе с самого
начала, до первого apply, спокойно проходит как часть
вызова «создать контейнер» — ограничение стреляет только на
«изменить существующий». Как только это стало понятно — просто
пересоздал контейнер (tofu apply -replace=...) с
nesting, заданным сразу в коде, без единого обходного пути. Откатил
токен обратно на ограниченного terraform-prov@pve —
полный круг за час, но с чётким уроком: если ограниченному
пользователю не хватает прав, это не всегда значит, что нужно больше
прав.
Rocky → Ubuntu (частичный отказ от изначального решения)
Тот же самый nesting-warning всплыл заново на Ubuntu
26.04 (systemd 259 вместо 257) —
как выяснилось, это не особенность конкретного дистрибутива, а
поведение самого ядра на новых версиях systemd, независимо от
гостевой ОС.
Но независимо от этого — минимальный Rocky-темплейт без SSH из коробки стал последней каплей. Поэтому Rocky остался осознанным выбором только для пары хостов, а остальные будут на Ubuntu/Debian.
Скрипт проверки занятости IP
Для варианта «статический IP прямо в .tf-коде»
(осознанно выбран вместо DHCP + ручное закрепление на
маршрутизаторе, ради полной воспроизводимости «с нуля одной
командой») понадобилась защита от коллизий адресов. Решил проблему
написанием скрипта с уведомлением в Telegram при конфликте адресов.
Первая версия — локальный arping с самой
iac-control. Тест на адресе из другого
VLAN молча ничего не находил, хотя адрес был реально занят. Причина:
конечно же я забыл, что ARP — протокол L2 и физически не может
пересечь границу VLAN, в отличие от ping (L3,
маршрутизируемый). А в планах — развёртывать хосты в разные VLAN.
Решение — не спрашивать локально, а спрашивать непосредственно
маршрутизатор (благо в сети он пока что один), единственный узел,
реально присутствующий во всех VLAN сразу: SSH на роутер под
отдельным read-only пользователем и запрос count-only
и по ARP-таблице, и по таблице DHCP-аренд (первая ловит «живое
сейчас», вторая — «зарезервировано, но выключено»).
Первый реально перенесённый сервис: Caddy
В отличие от построения инфраструктуры «с нуля», тут задача была
другая — перенести реальный существующий конфиг с
вручную настроенного контейнера в Ansible-роль, а не выдумывать
заново. Конфиг оказался простым (два reverse_proxy-блока),
перенёс его полностью в roles/caddy/files/Caddyfile.
Роль ставит Caddy через официальный apt-репозиторий, а не пакет
дистрибутива — версия в дистрибутивных пакетах обычно сильно
отстаёт.
Проверять готовность решил не через curl (первая
мысль, но так себе тест — долгий и не особо информативный), а сразу
переключил NAT-правило на роутере со старого IP на новый и убедился,
что GitLab, который висит за этим reverse proxy, нормально
открывается через новый контейнер. Только после этого — снёс старый.
Что в итоге
Главный урок этого захода — про токен root@pam,
который не сработал. Интуиция «не хватает прав → дай побольше»
подвела: дело было не в правах вообще, а в том, что Proxmox
по-разному относится к полю, заданному при создании ресурса, и к
тому же полю, добавленному задним числом. Проще говоря — иногда
стоит остановиться и перепроверить формулировку проблемы, а не
сразу тянуться за более широким токеном.
Для проверки доступности адресов решение казалось очевидным ровно до тех пор, пока не понадобилось выйти за пределы одного VLAN (о чём я изначально и забыл), и тогда пришлось разбираться с маршрутизатором.
Caddy для первого сервиса выбрал специально — скучный, маленький, почти нечему ломаться. Задача была не «мигрировать что-то важное», а подтвердить, что сама связка «руками настроенный конфиг → Ansible-роль → осознанный cutover» вообще работает. Сработала.
Дальше — по мере переноса остальных сервисов расскажу про структуру самого репозитория (роли, инвентарь, секреты через ansible-vault), а следующая заметная веха — второй, уже не примитивный сервис.
Спасибо, что дочитали до конца <3