- providers.tf / variables.tf / versions.tf: bpg/proxmox ~> 0.84, endpoint and credentials from TF_VAR_* (set by the Makefile tofu-* targets from the repo-root .env). Two auth modes: root@pam by password (privileged: features beyond nesting, device passthrough, datastore mount points) or the ansible@pve token. - README.md: pilot results on VMID 199 - what the token can and cannot do, why a root token still fails the literal `$authuser eq 'root@pam'` check, the cmode/console drift finding, and the chosen root@pam-by-password mode. - pilot.tf.example: reference resource shape (features, device_passthrough, mount_point), not loaded (.example). - .terraform.lock.hcl: pin the provider. State has no backend yet; tofu/*.tfstate stays local and git-ignored. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012uoq5AVK8mkBgg83Mq6o5V
10 KiB
OpenTofu — пилот provisioning LXC
Экспериментальный каталог. Ни один боевой контейнер здесь не описан: цель —
выяснить, что провайдер bpg/proxmox реально может в этой инфраструктуре,
прежде чем принимать решение о переезде.
Активных ресурсов сейчас нет: пилотный контейнер снесён, а его описание лежит
в pilot.tf.example — расширение не даёт Tofu его загрузить. Это рабочий
образец формы ресурса для реальных сервисов, см.
../docs/ai/migration-tofu.md.
make -C ansible tofu-init
make -C ansible tofu-plan
make -C ansible tofu-apply CONFIRM=1
make -C ansible tofu-destroy CONFIRM=1
Результат пилота (2026-09-02, provider 0.84, PVE-кластер homelab)
Всё проверено на живом кластере контейнером VMID 199 tofu-pilot.
Что API-токен МОЖЕТ
| Возможность | Проверено |
|---|---|
| Создание LXC: vmid, node, hostname, cores, memory, swap, unprivileged | да |
| Сеть: bridge, статический IP, шлюз, firewall | да |
| rootfs на datastore | да |
| Mount point как volume на datastore | да — mp0: data:199/vm-199-disk-1.raw,mp=/opt/pilot-data,size=4G |
features { nesting } |
да |
| onboot, startup order, tags | да |
Идемпотентность (plan -> No changes) |
да |
Строка про mount point — ответ на вопрос про mp0 у gitea. Bind mount каталога
хоста (mp0: /opt/data/gitea,mp=...) токеном недоступен, а volume на
datastore — доступен. То есть при пересоздании gitea хранилище можно
перевести на volume и снять зависимость от root@pam.
Чего API-токен НЕ МОЖЕТ
Proxmox отвечает HTTP 403:
configuring device passthrough is only allowed for root@pam
changing feature flags (except nesting) is only allowed for root@pam
То есть keyctl, fuse, mount и device_passthrough (dev0:) требуют
root@pam. Это ограничение Proxmox, а не Tofu: Ansible упирается в ровно ту
же стену, поэтому в pve-monitoring.yml, pve-grimmory.yml и
pve-hermes-ai.yml есть отдельный шаг pct set <vmid> --features nesting=1,keyctl=1 по SSH под root, а /dev/fuse и /dev/net/tun
пробрасываются правкой /etc/pve/lxc/<vmid>.conf.
Вывод: по этому пункту Tofu не хуже нынешнего состояния — ему нужен такой же дошаг.
Ловушка гибридной схемы
Если Tofu создаёт контейнер, а Ansible потом добавляет keyctl через
pct set, Tofu видит это как дрейф: plan даёт 1 to change и следующий
apply откатил бы features обратно, лишив контейнер возможности запускать
Docker. Проверено на пилоте.
Обход — lifecycle { ignore_changes = [features] }, он в pilot.tf. После
него plan снова даёт No changes, а keyctl=1 на узле сохраняется. Плата:
features перестают управляться декларативно.
cmode — не в списке атрибутов ресурса, но провайдер его отслеживает через console
Обнаружено 2026-09-02 при переезде emergency-bot (сервис №1), исправлено в
тот же день. roles/pve_lxc всем создаваемым контейнерам ставит
cmode: shell (pct console заходит сразу в root shell, а не в getty-логин)
— pve_lxc_cmode в roles/pve_lxc/defaults/main.yml. У ресурса
proxmox_virtual_environment_container провайдера bpg/proxmox нет
top-level атрибута cmode — он скрыт в блоке console { type = ... },
который в plan/apply при создании не показывается, если не задан явно.
Первая ошибка: контейнер создан без блока console, получил дефолт Proxmox
(cmode: tty), исправлено одноразовым pct set <vmid> --cmode shell по SSH.
Это была ошибка: следующий tofu-plan (уже на шаге 4.5 этого же сервиса)
показал console { type = "shell" -> null } — провайдер ЗАМЕТИЛ ручную правку
через refresh state и на следующем apply откатил бы её обратно на tty. То
есть cmode ведёт себя ровно как keyctl в гибридной схеме ниже: правится
вручную — ловит drift.
Правильное решение — объявить блок в ресурсе явно:
console {
type = "shell"
}
После этого tofu-plan не показывает console как diff, ручная правка не
нужна. Добавлять этот блок в описание любого сервиса на шаге 4.2 сразу же,
не как отдельный ручной пост-шаг.
Рутовый токен не помогает — проверено по исходникам
Проверка в /usr/share/perl5/PVE/LXC.pm:1658 буквальная:
return 1 if $authuser eq 'root@pam';
А $authuser при токенной аутентификации — это полный token-ID. Из
/usr/share/perl5/PVE/HTTPServer.pm:86:
# the token-ID `<user>@<realm>!<tokenname>` is the user for token based authentication
$username = eval { PVE::AccessControl::verify_token($api_token); };
verify_token возвращает $tokenid, то есть user@realm!tokenname. Строка
root@pam!mytoken не равна root@pam, поэтому токен root проверку НЕ проходит.
Следствия:
- выдать токену больше прав бесполезно: это не проверка привилегий, а сравнение имени пользователя, ACL на неё не влияют;
- единственный вариант «сделать всё через Tofu» — аутентификация root@pam по
паролю. В realm
pamэто системный пароль root узла Proxmox: его нельзя ограничить по scope и нельзя отозвать отдельно от смены пароля root.
Решение: root@pam по паролю (принято и проверено 2026-09-02)
Выбран привилегированный режим. Проверено на пилоте — всё привилегированное выставляется декларативно, без единого шага Ansible:
dev0: deny-write=0,path=/dev/net/tun,uid=0,gid=0,mode=0660
features: fuse=1,keyctl=1,nesting=1,mknod=0
mp0: data:199/vm-199-disk-1.raw,mp=/opt/pilot-data,size=4G
Повторный plan даёт No changes.
Следствия для миграции сервисов:
- дошаг
pct set <vmid> --features nesting=1,keyctl=1по SSH больше не нужен — при переезде сервиса его можно удалять из соответствующегоpve-*.yml; - правка
/etc/pve/lxc/<vmid>.confчерезlineinfile(девять контейнеров) заменяется блокомdevice_passthrough; lifecycle { ignore_changes = [features] }не нужен: он был обходом для гибридной схемы, от которой отказались;- apply одной фазой, без чередования Tofu и Ansible.
Отвергнутая альтернатива — гибрид: Tofu создаёт то, что доступно токену, Ansible доводит привилегированное по SSH через существующий sudo. Не требует новых секретов, но оставляет декларативность частичной и apply двухфазным.
Аутентификация
Способ выбирает Makefile по корневому .env:
| Условие | Режим | Что доступно |
|---|---|---|
PROXMOX_ROOT_PASSWORD задан и не replace-me |
root@pam по паролю | всё, включая features и device passthrough |
| иначе | токен ansible@pve |
всё кроме features (кроме nesting), device passthrough и bind mount |
Выбранный режим печатается в stderr перед запуском, чтобы из вывода было видно,
какими правами шёл apply. В pilot.tf привилегированные поля включаются через
local.privileged, поэтому конфигурация валидна в обоих режимах.
Сеть
Tofu ходит в Proxmox API напрямую по HTTPS и не умеет ProxyJump из
ssh_config. Вне LAN узлы недоступны, поэтому цели tofu-* в Makefile сами
поднимают SSH-туннель через ru-vps на время команды и направляют провайдер в
127.0.0.1. Отсюда же insecure = true: сертификат выписан на узел, а
обращение идёт к localhost.
State
tofu/*.tfstate* в .gitignore: состояние содержит фактическую конфигурацию
гостей. .terraform.lock.hcl наоборот коммитится — он закрепляет версию
провайдера. Отдельного бэкенда нет; при переходе к боевому использованию
состояние нужно куда-то бэкапить.