feat(tofu): OpenTofu provisioning scaffold, auth modes and pilot notes

- 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
This commit is contained in:
Dmitry
2026-09-03 07:04:23 +03:00
co-authored by Claude Sonnet 5
parent e0c53a1b1b
commit 646f2bbc8f
6 changed files with 405 additions and 0 deletions
+134
View File
@@ -0,0 +1,134 @@
# ============================================================================
# ОБРАЗЕЦ, НЕ ЗАГРУЖАЕТСЯ. Расширение .example выбрано намеренно: сам пилот
# (VMID 199) снесён 2026-09-02, и будь этот файл активным, каждый `tofu-plan`
# предлагал бы создать его заново.
#
# Держим как рабочий пример формы ресурса: по нему описываются реальные
# сервисы при переезде. См. docs/ai/migration-tofu.md, шаг 4.2.
# ============================================================================
#
# Пилот: одноразовый LXC, созданный OpenTofu.
# ============================================================================
#
# ЗАЧЕМ
# Проверить на живом кластере ровно те места, из-за которых переезд на Tofu
# выглядел рискованным, не трогая ни один боевой контейнер:
#
# 1. Проброс /dev/fuse. В pve-*.yml он сделан правкой /etc/pve/lxc/<vmid>.conf
# (lxc.cgroup2.devices.allow + lxc.mount.entry), потому что Proxmox API
# сырые lxc.* ключи не принимает. У провайдера для этого есть
# features.fuse — штатный флаг PVE, а не обход.
# 2. Проброс /dev/net/tun — блок device_passthrough (dev0: в PVE 8.2+).
# 3. Mount point на storage вместо bind mount каталога хоста. Именно bind
# mount у gitea (mp0: /opt/data/gitea) требует прав root@pam и не
# создаётся API-токеном; volume на datastore — создаётся.
#
# Контейнер намеренно не входит в inventory и не автозапускается.
# Снести после проверки: make tofu-destroy CONFIRM=1
#
# VMID 199 и 192.168.1.39 выбраны свободными на 2026-09-02 и лежат вне
# диапазона сервисов, но внутри маршрутов ru-vps (192.168.1.5-40).
locals {
# Привилегированный режим = аутентификация root@pam по паролю. Makefile
# выбирает его, когда в корневом .env задан PROXMOX_ROOT_PASSWORD.
privileged = var.pve_password != ""
}
resource "proxmox_virtual_environment_container" "pilot" {
node_name = "cloud-pc"
vm_id = 199
unprivileged = true
start_on_boot = false
started = true
tags = ["tofu", "pilot"]
initialization {
hostname = "tofu-pilot"
ip_config {
ipv4 {
address = "192.168.1.39/24"
gateway = "192.168.1.1"
}
}
dns {
servers = ["1.1.1.1"]
}
user_account {
keys = [trimspace(file(pathexpand("~/.ssh/id_ed25519_homelab.pub")))]
}
}
operating_system {
template_file_id = "local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst"
type = "debian"
}
cpu {
cores = 1
}
memory {
dedicated = 512
swap = 512
}
disk {
datastore_id = "data"
size = 8
}
# Проверка №1 и №2: то, что в pve-*.yml делается правкой конфига на узле.
# Токену Proxmox разрешает только nesting; keyctl и fuse требуют root@pam.
# null означает «не задавать поле», а не «выключить».
features {
nesting = true
keyctl = local.privileged ? true : null
fuse = local.privileged ? true : null
}
# Проброс устройства Proxmox разрешает только root@pam (403 для токена,
# проверено на пилоте 2026-09-02), поэтому блок появляется лишь в
# привилегированном режиме.
dynamic "device_passthrough" {
for_each = local.privileged ? [1] : []
content {
path = "/dev/net/tun"
}
}
# Проверка №3: volume на datastore, а не bind mount каталога хоста.
mount_point {
volume = "data"
size = "4G"
path = "/opt/pilot-data"
}
network_interface {
name = "eth0"
bridge = "vmbr0"
firewall = true
}
startup {
order = 999
}
# ignore_changes = [features] здесь НЕТ намеренно. Он нужен только в
# гибридной схеме, где features выставляет Ansible через `pct set`: иначе
# Tofu на следующем apply откатывает их и ломает Docker в контейнере
# (проверено на пилоте). В привилегированном режиме features описаны выше
# декларативно, и обход не требуется.
}
output "pilot" {
description = "Что проверять на узле после apply"
value = {
vmid = proxmox_virtual_environment_container.pilot.vm_id
node = proxmox_virtual_environment_container.pilot.node_name
verify = "ssh cloud-pc sudo pct config ${proxmox_virtual_environment_container.pilot.vm_id}"
}
}