root@homelab

Первые шаги миграции на IaC: SDN.Use, разбитые ARP-проверки и перенос Caddy

Токен 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), а следующая заметная веха — второй, уже не примитивный сервис.

proxmox opentofu ansible iac

Спасибо, что дочитали до конца <3