- 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
173 lines
10 KiB
Markdown
173 lines
10 KiB
Markdown
# OpenTofu — пилот provisioning LXC
|
||
|
||
Экспериментальный каталог. Ни один боевой контейнер здесь не описан: цель —
|
||
выяснить, что провайдер `bpg/proxmox` реально может в этой инфраструктуре,
|
||
прежде чем принимать решение о переезде.
|
||
|
||
Активных ресурсов сейчас нет: пилотный контейнер снесён, а его описание лежит
|
||
в `pilot.tf.example` — расширение не даёт Tofu его загрузить. Это рабочий
|
||
образец формы ресурса для реальных сервисов, см.
|
||
[`../docs/ai/migration-tofu.md`](../docs/ai/migration-tofu.md).
|
||
|
||
```bash
|
||
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` наоборот коммитится — он закрепляет версию
|
||
провайдера. Отдельного бэкенда нет; при переходе к боевому использованию
|
||
состояние нужно куда-то бэкапить.
|