В этой статье мы будем использовать OpenTofu с провайдером bpg для управления инфраструктурой Proxmox.
🖐️Эй!
Подписывайтесь на наш телеграм @r4ven_me📱, чтобы не пропустить новые публикации на сайте😉. А если есть вопросы или желание пообщаться по тематике — заглядывайте в Вороний чат @r4ven_me_chat🧐.
TLDR
Proxmox:
apt update && apt install -y libguestfs-tools
wcurl https://cloud.debian.org/images/cloud/trixie/latest/debian-13-generic-amd64.qcow2
qemu-img resize ./debian-13-generic-amd64.qcow2 20G
qm create 7777 --name "debian13-k8s-template" \
--memory 2048 --cores 2 --net0 virtio,bridge=vmbr0
qm importdisk 7777 ./debian-13-generic-amd64.qcow2 storage --format qcow2
qm set 7777 --scsihw virtio-scsi-single \
--scsi0 storage:7777/vm-7777-disk-0.qcow2
qm set 7777 --boot order=scsi0
qm set 7777 --ide0 storage:cloudinit
qm set 7777 --serial0 socket --vga serial0
qm set 7777 --agent enabled=1
qm template 7777
qm rescan --vmid 7777
qm config 7777
pveum role add TFUser -privs "Pool.Allocate VM.Console VM.Allocate VM.Clone VM.Config.CDROM VM.Config.CPU VM.Config.Cloudinit VM.Config.Disk VM.Config.HWType VM.Config.Memory VM.Config.Network VM.Config.Options VM.Audit VM.PowerMgmt VM.GuestAgent.Audit Datastore.AllocateSpace Datastore.Allocate Datastore.Audit SDN.Use"
pveum user add tfuser@pve
pveum aclmod / -user tfuser@pve -role TFUser
pveum user token add tfuser@pve tf --privsep 0Клиент:
sudo apt update
sudo apt install -y apt-transport-https ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://get.opentofu.org/opentofu.gpg | \
sudo tee /etc/apt/keyrings/opentofu.gpg > /dev/null
curl -fsSL https://packages.opentofu.org/opentofu/tofu/gpgkey | \
sudo gpg --no-tty --batch --dearmor \
--output /etc/apt/keyrings/opentofu-repo.gpg > /dev/null
sudo chmod a+r /etc/apt/keyrings/opentofu.gpg /etc/apt/keyrings/opentofu-repo.gpg
echo \
"deb [signed-by=/etc/apt/keyrings/opentofu.gpg,/etc/apt/keyrings/opentofu-repo.gpg] https://packages.opentofu.org/opentofu/tofu/any/ any main
deb-src [signed-by=/etc/apt/keyrings/opentofu.gpg,/etc/apt/keyrings/opentofu-repo.gpg] https://packages.opentofu.org/opentofu/tofu/any/ any main" | \
sudo tee /etc/apt/sources.list.d/opentofu.list > /dev/null
sudo chmod a+r /etc/apt/sources.list.d/opentofu.list
sudo apt update && sudo apt install -y tofu
command -v tofu && tofu --version
mkdir -p ~/TF
git clone https://github.com/r4ven-me/opentofu.git /tmp/opentofu
cp -r /tmp/opentofu/proxmox/bpg/k8s ~/TF && cd ~/TF/k8s/
vim dev.tfvars
vim prod.tfvars
vim .env && source .env
tofu init -upgrade
tofu validate
tofu init -reconfigure -backend-config="path=./dev.tfstate"
tofu plan -var-file=./dev.tfvars
tofu apply -var-file=./dev.tfvars
tofu init -reconfigure -backend-config="path=./prod.tfstate"
tofu plan -var-file=./prod.tfvars
tofu apply -var-file=./prod.tfvars -parallelism=2
tofu init -reconfigure -backend-config="path=./dev.tfstate"
tofu destroy -var-file=./dev.tfvarsВведение
Ниже короткая справка для тех, кто впервые сталкивается с упомянутым в заголовке ПО и концепцией IaC.
Что такое IaC?
Инфраструктура как код (IaC) - это подход к управлению и предоставлению инфраструктуры (серверов, сетей, баз данных, хранилищ и т.д.) с использованием файлов конфигурации и программного кода, а не ручных процессов или интерактивных инструментов.
Что такое Proxmox?
Proxmox Virtual Environment (Proxmox VE) – это Open source-платформа виртуализации, предназначенная для управления виртуальными машинами (на базе KVM) и контейнерами (на базе LXC). Он работает как гипервизор первого типа (bare-metal), устанавливаясь непосредственно на аппаратное обеспечение, и предоставляет удобный веб-интерфейс для централизованного управления всеми ресурсами.
Что такое Terraform?
Terraform - это инструмент Infrastructure as Code (IaC), который позволяет описывать, создавать, изменять и удалять облачную и локальную инфраструктуру (серверы, базы данных, сети и т.д.) с помощью декларативного языка конфигурации HashiCorp Configuration Language (HCL). Его основное преимущество - возможность управлять инфраструктурой различных провайдеров (AWS, Azure, Proxmox и др.) единообразным способом, обеспечивая воспроизводимость, версионирование и автоматизацию процессов развертывания. В общем, штука полезная👍.
Что такое OpenTofu?
OpenTofu - это открытая, управляемая сообществом альтернатива Terraform, которая возникла как форк после того, как HashiCorp изменила лицензию Terraform с MPL на BSL. Проект находится под эгидой Linux Foundation и нацелен на обеспечение постоянного развития инструмента Infrastructure as Code (IaC) с гарантированной открытой лицензией и прозрачным управлением.
Что такое Провайдер Terraform/OpenTofu?
Провайдер Terraform/OpenTofu - это плагин, который является основным интерфейсом для взаимодействия с конкретной платформой или сервисом. Будь то облачный провайдер, например, Google Cloud, локальная инфраструктура - Proxmox, VMware или SaaS-сервис, как GitHub. Если кратко: провайдер абстрагирует сложности API этой платформы, позволяя создавать, читать, обновлять и удалять (CRUD) ресурсы описываемые в конфигурации. Штука очень универсальная🤷♂️.
В качестве примера мы автоматизируем процесс создания и подготовки виртуальных машин для кластерной инфраструктуры k8s (kubernetes).
Данная инфра включает в себя 3 роли ВМ:
- балансировщики
- мастер-ноды (control plane)
- рабочие
лошадкиноды (workers)
Все параметры ВМ удобно задаются с помощью разделённых по окружениям var файлов.
В проекте также предусмотрены пользовательские сценарии подготовки серверов, после их развёртывания. В качестве bootstrap сценария используется Cloud init - доступны шаблоны для каждой роли, которые легко кастомизировать.
Вводные данные
ПО, используемое в статье:
| ПО | Версия |
|---|---|
| Proxmox | 9.0.3 (Debian 13) |
| OpenTofu | 1.11.1 |
| Провайдер bpg | 0.87.0 |
| Клиент с OpenTofu | Debian 13 |
Ну, хватит разговоров, погнали делать автоматизацию🚘.
Подготовка Proxmox
Начнём с гипервизора🖥️.
Подготовка образа ВМ в формате qcow2
Подключаемся к серверу Proxmox по SSH:
ssh root@proxmox.home.lan⚠️Предупреждение
В моём примере я подключаюсь по SSH пользователем root. Это оправдано в случае тестовых стендов. Для работы на продакшене крайне рекомендую использовать sudo.
☝️Следующие действия выполняются на Proxmox сервере (proxmox.home.lan в моём случае) от root пользователя.
Обновляем кэш пакетов и устанавливаем необходимые утилиты:
apt update && apt install -y libguestfs-toolsСкачиваем актуальный базовый образ Debian 13 в формате qcow2:
wcurl https://cloud.debian.org/images/cloud/trixie/latest/debian-13-generic-amd64.qcow2
ls -l ./debian-13-generic-amd64.qcow2
Выполняем ресайз диска (укажите подходящее вам значение):
qemu-img resize ./debian-13-generic-amd64.qcow2 20GImage resized.Теперь кастомизируем наш образ под свои предпочтения. Я выполню обновление пакетов, настройку локали и установку джентльменского набора утилит для виртуальной машины:
# Процесс может занять некоторое время
virt-customize -a ./debian-13-generic-amd64.qcow2 \
--run-command "echo 'en_US.UTF-8 UTF-8' >> /etc/locale.gen" \
--run-command "echo 'ru_RU.UTF-8 UTF-8' >> /etc/locale.gen" \
--run-command "locale-gen" \
--run-command "echo 'Europe/Moscow' > /etc/timezone" \
--run-command "ln -sf /usr/share/zoneinfo/Europe/Moscow /etc/localtime" \
--run-command "dpkg-reconfigure --frontend noninteractive tzdata" \
--update --install qemu-guest-agent,sudo,gpg,git,curl,vim
💡 Подробнее про подготовку образа виртуальной машины в формате qcow2 на примере Debian читайте в отдельной статье.
Подготовка шаблона VM в Proxmox
Переходим к импорту диска в Proxmox:
# Создаём новую виртуальную машину с ID 7777 и базовыми настройками
qm create 7777 --name "debian13-k8s-template" \
--memory 2048 --cores 2 --net0 virtio,bridge=vmbr0
# Импортируем образ диска в хранилище 'storage' (замените на своё)
qm importdisk 7777 ./debian-13-generic-amd64.qcow2 storage --format qcow2
# Устанавливаем SCSI контроллер и добавляем диск к ВМ как 'scsi0'
qm set 7777 --scsihw virtio-scsi-single \
--scsi0 storage:7777/vm-7777-disk-0.qcow2
# Устанавливаем порядок загрузки: 'scsi0' как первый загрузочный диск
qm set 7777 --boot order=scsi0
# Добавляем cloud-init диск к виртуальной машине как 'ide0'
qm set 7777 --ide0 storage:cloudinit
# Настраиваем последовательный порт и видеокарту, связанную с ним
qm set 7777 --serial0 socket --vga serial0
# Включаем 'QEMU Guest Agent' для взаимодействия хоста и гостя
qm set 7777 --agent enabled=1
# Преобразуем виртуальную машину в шаблон
qm template 7777
# Пересканируем конфигурацию ВМ
qm rescan --vmid 7777
# Смотрим полный конфиг нашего шаблона
qm config 7777
💡 Подробнее про создание шаблона виртуальной машины в Proxmox читайте в этой статье.
Выводим список файлов в директории ВМ (хранилище storage), в моём случае это: /mnt/storage/images/7777/:
ls -l /mnt/storage/images/7777/
Видим сам образ и cloud-init диск👌.
❗️ Осторожно
При необходимости уничтожить ВМ-шаблон используйте команду:
qm destroy 7777Создание реквизитов доступа к Proxmox
Создаём отдельную роль для работы с OpenTofu:
pveum role add TFUser -privs "Pool.Allocate VM.Console VM.Allocate VM.Clone VM.Config.CDROM VM.Config.CPU VM.Config.Cloudinit VM.Config.Disk VM.Config.HWType VM.Config.Memory VM.Config.Network VM.Config.Options VM.Audit VM.PowerMgmt VM.GuestAgent.Audit Datastore.AllocateSpace Datastore.Allocate Datastore.Audit SDN.Use"Создаём сервисного пользователя и назначаем ему новую роль TFUser:
pveum user add tfuser@pve
pveum aclmod / -user tfuser@pve -role TFUserСоздаём токен для нашей сервисной УЗ:
# токен со всеми привилегиями, как у пользователя tfuser
pveum user token add tfuser@pve tf --privsep 0Получим такой вывод:

Сохраните где-нибудь full-tokenid и value самого токена. Они нам понадобятся во время настройки OpenTofu.
Все действия выше можно было выполнить в GUI Proxmox, в разделе Datacenter —> Permissions:

Но через командную строку быстрее, а для инструкции - нагляднее🙃.
Подготовка OpenTofu
Установка OpenTofu в Debian
☝️Следующие действия выполняются на клиентской машине от обычного пользователя.
В соответствии с оф. документацией подключаем репозиторий OpenTofu и выполняем установку нативно:
# Обновляем кэш
sudo apt update
# Устанавливаем вспомогательные утилиты
sudo apt install -y apt-transport-https ca-certificates curl gnupg
# Создаём директории для ключей gpg
sudo install -m 0755 -d /etc/apt/keyrings
# Устанавливаем ключи репозитория OpenTofu
curl -fsSL https://get.opentofu.org/opentofu.gpg | \
sudo tee /etc/apt/keyrings/opentofu.gpg > /dev/null
curl -fsSL https://packages.opentofu.org/opentofu/tofu/gpgkey | \
sudo gpg --no-tty --batch --dearmor \
--output /etc/apt/keyrings/opentofu-repo.gpg > /dev/null
# Добавляем права на чтение для всех
sudo chmod a+r /etc/apt/keyrings/opentofu.gpg /etc/apt/keyrings/opentofu-repo.gpg
# Добавляем адреса репозиториев в список
echo \
"deb [signed-by=/etc/apt/keyrings/opentofu.gpg,/etc/apt/keyrings/opentofu-repo.gpg] https://packages.opentofu.org/opentofu/tofu/any/ any main
deb-src [signed-by=/etc/apt/keyrings/opentofu.gpg,/etc/apt/keyrings/opentofu-repo.gpg] https://packages.opentofu.org/opentofu/tofu/any/ any main" | \
sudo tee /etc/apt/sources.list.d/opentofu.list > /dev/null
# Также добавляем права на чтение
sudo chmod a+r /etc/apt/sources.list.d/opentofu.list
Теперь обновляем кэш пакетов и устанавливаем OpenTofu:
sudo apt update
sudo apt install -y tofu
command -v tofu
tofu --version⚠️ Возможны проблемы с доступом к онлайн ресурсам OpenTofu из РФ.

Теперь в нашей системе появилась новая утилита командной строки: tofu.
Создание файлов проекта для кластера k8s
Переходим к подготовке файлов проекта для кластера k8s. Первым делом создаём директорию, например, в хомяке и переходим в неё:
mkdir -vp ~/TF/k8s && cd ~/TF/k8s☝️ Чтобы не увеличивать объем статьи я не буду подробно описывать синтаксис каждого файла. Лишь обозначу их назначение. При необходимости рекомендую уточнять непонятные моменты у нейронок.
Прежде, чем начать, хочу отметить еще пару моментов:
- Файлы OpenTofu/Terraform имеют расширение
tfи пишутся на специальном языке HCL - HashiCorp Configuration Language. - В представленной мной конфигурации источником истины являются файлы переменных (могут иметь произвольное расширение, в моём случае
*.tfvars). Именно из них берутся все нужные значения и подставляются в конфигурацию OpenTofu в процессе работы.
Файл описания провайдера - provider.tf
Что ж, а начнём мы с файла провайдера для Proxmox:
vim ./provider.tfНаполняем:
terraform {
required_providers {
proxmox = {
source = "bpg/proxmox"
version = "0.87.0"
}
}
}
provider "proxmox" {
endpoint = var.proxmox_api_url
api_token = "${var.proxmox_api_token_id}=${var.proxmox_api_token_secret}"
insecure = true
ssh {
agent = false
username = var.proxmox_ssh_user
private_key = file(var.proxmox_ssh_key)
}
}Тут указывается сам провайдер - bpg, его версия, а также адрес Proxmox сервера и реквизиты доступа к нему: API для манипуляции виртуальными машинами и SSH для использования внешних cloud-init сценариев. Все значения параметров представляют собой переменные, которые мы с вами заполним чуть позже.
Файл управления состоянием - backend.tf
Создаём файл:
vim ./backend.tfНаполняем:
terraform {
backend "local" {}
}С помощью данной конфигурации мы определяем локальное хранение state файлов, которые будут хранить состояние инфраструктуры и информацию о созданных ресурсах.
В нашем случае это необходимо, чтобы управлять двумя разными окружениями (dev и prod) без конфликтов.
Файл описания конфигурации ВМ - main.tf
Теперь создаём основной файл конфигурации серверов наших 3-х ролей: балансировщики, мастер и воркер ноды:
vim ./main.tf############################################################
# Cloud-Init Files (Snippets)
############################################################
resource "proxmox_virtual_environment_file" "balancer_cloud_init" {
count = var.vm_count_balancer
content_type = "snippets"
datastore_id = var.proxmox_snippets_storage
node_name = var.proxmox_node
source_raw {
data = templatefile("${path.module}/cloudinit/balancer.tftpl", {
hostname = "${var.vm_name_balancer}-${var.project_env}-${count.index + 1}"
user = var.user_name
password = var.user_password
ssh_key = var.user_ssh_key
})
file_name = "balancer-${var.project_env}-${count.index + 1}-user-data.yaml"
}
}
resource "proxmox_virtual_environment_file" "master_cloud_init" {
count = var.vm_count_master
content_type = "snippets"
datastore_id = var.proxmox_snippets_storage
node_name = var.proxmox_node
source_raw {
data = templatefile("${path.module}/cloudinit/master.tftpl", {
hostname = "${var.vm_name_master}-${var.project_env}-${count.index + 1}"
user = var.user_name
password = var.user_password
ssh_key = var.user_ssh_key
})
file_name = "master-${var.project_env}-${count.index + 1}-user-data.yaml"
}
}
resource "proxmox_virtual_environment_file" "worker_cloud_init" {
count = var.vm_count_worker
content_type = "snippets"
datastore_id = var.proxmox_snippets_storage
node_name = var.proxmox_node
source_raw {
data = templatefile("${path.module}/cloudinit/worker.tftpl", {
hostname = "${var.vm_name_worker}-${var.project_env}-${count.index + 1}"
user = var.user_name
password = var.user_password
ssh_key = var.user_ssh_key
})
file_name = "worker-${var.project_env}-${count.index + 1}-user-data.yaml"
}
}
############################################################
# k8s balancer
############################################################
resource "proxmox_virtual_environment_vm" "k8s_balancer" {
count = var.vm_count_balancer
name = "${var.vm_name_balancer}-${var.project_env}-${count.index + 1}"
node_name = var.proxmox_node
vm_id = var.vm_id_first + 100 + count.index + 1
tags = split(",", "${var.vm_tags_balancer},${var.project_env}")
on_boot = false
agent {
enabled = true
}
cpu {
cores = var.vm_cpu_balancer
type = "x86-64-v2-AES"
}
memory {
dedicated = var.vm_ram_balancer
}
clone {
vm_id = var.vm_template_id
full = true
}
disk {
datastore_id = var.vm_disk_storage_balancer
interface = "scsi0"
file_format = var.vm_disk_format
size = var.vm_disk_size_balancer
}
initialization {
datastore_id = var.vm_disk_storage_balancer
user_data_file_id = proxmox_virtual_environment_file.balancer_cloud_init[count.index].id
dns {
servers = [var.vm_ip_dns]
}
ip_config {
ipv4 {
#address = "dhcp"
address = "${var.vm_ip_prefix}.${var.vm_ip_first_balancer + count.index + 1}/${var.vm_ip_cidr}"
gateway = var.vm_ip_gateway
}
}
}
network_device {
bridge = "vmbr0"
model = "virtio"
}
#lifecycle {
# prevent_destroy = true
# create_before_destroy = true
# ignore_changes = [agent, disk, initialization,]
#}
}
############################################################
# k8s master
############################################################
resource "proxmox_virtual_environment_vm" "k8s_master" {
count = var.vm_count_master
name = "${var.vm_name_master}-${var.project_env}-${count.index + 1}"
node_name = var.proxmox_node
vm_id = var.vm_id_first + 200 + count.index + 1
tags = split(",", "${var.vm_tags_master},${var.project_env}")
on_boot = false
agent {
enabled = true
}
cpu {
cores = var.vm_cpu_master
type = "x86-64-v2-AES"
}
memory {
dedicated = var.vm_ram_master
}
clone {
vm_id = var.vm_template_id
full = true
}
disk {
datastore_id = var.vm_disk_storage_master
interface = "scsi0"
file_format = var.vm_disk_format
size = var.vm_disk_size_master
}
initialization {
datastore_id = var.vm_disk_storage_master
user_data_file_id = proxmox_virtual_environment_file.master_cloud_init[count.index].id
dns {
servers = [var.vm_ip_dns]
}
ip_config {
ipv4 {
#address = "dhcp"
address = "${var.vm_ip_prefix}.${var.vm_ip_first_master + count.index + 1}/${var.vm_ip_cidr}"
gateway = var.vm_ip_gateway
}
}
}
network_device {
bridge = "vmbr0"
model = "virtio"
}
#lifecycle {
# prevent_destroy = true
# create_before_destroy = true
# ignore_changes = [agent, disk, initialization,]
#}
}
############################################################
# k8s worker
############################################################
resource "proxmox_virtual_environment_vm" "k8s_worker" {
count = var.vm_count_worker
name = "${var.vm_name_worker}-${var.project_env}-${count.index + 1}"
node_name = var.proxmox_node
vm_id = var.vm_id_first + 300 + count.index + 1
tags = split(",", "${var.vm_tags_worker},${var.project_env}")
on_boot = false
agent {
enabled = true
}
cpu {
cores = var.vm_cpu_worker
type = "x86-64-v2-AES"
}
memory {
dedicated = var.vm_ram_worker
}
clone {
vm_id = var.vm_template_id
full = true
}
disk {
datastore_id = var.vm_disk_storage_worker
interface = "scsi0"
file_format = var.vm_disk_format
size = var.vm_disk_size_worker
}
disk {
datastore_id = var.vm_disk_storage_worker
interface = "scsi1"
file_format = var.vm_disk_format
size = var.vm_disk_size_worker
}
initialization {
datastore_id = var.vm_disk_storage_worker
user_data_file_id = proxmox_virtual_environment_file.worker_cloud_init[count.index].id
dns {
servers = [var.vm_ip_dns]
}
ip_config {
ipv4 {
#address = "dhcp"
address = "${var.vm_ip_prefix}.${var.vm_ip_first_worker + count.index + 1}/${var.vm_ip_cidr}"
gateway = var.vm_ip_gateway
}
}
}
network_device {
bridge = "vmbr0"
model = "virtio"
}
#lifecycle {
# prevent_destroy = true
# create_before_destroy = true
# ignore_changes = [agent, disk, initialization,]
#}
}Вначале файла указан блок cloud-init для каждой роли, который определяет где искать нужные файлы сценариев, а также определяются переменные OpenTofu, которые будут переданы в сценарии cloud-init: hostname, user, password и ssh_key.
Следующие блоки описывают непосредственно конфигурацию виртуальных машин в Proxmox. Все значения также берутся из переменных.
Файл определения переменных - variables.tf
В OpenTofu/Terraform объявлять переменные и указывать их значения принято в разных файлах. Это необязательное условие, но так проще организовывать проект. К тому же в файле, где объявляются переменные можно указать дефолтные значения и описание:
vim ./variables.tfvariable "project_env" {
type = string
default = "dev"
}
variable "proxmox_node" {
type = string
default = "proxmox"
}
variable "proxmox_api_url" {
type = string
default = "https://proxmox.example.com:8006/api2/json"
description = "Proxmox API url"
}
variable "proxmox_api_token_id" {
type = string
}
variable "proxmox_api_token_secret" {
type = string
sensitive = true
}
variable "proxmox_ssh_user" {
type = string
default = "root"
description = "SSH user to manage cloud-init snippets"
}
variable "proxmox_ssh_key" {
type = string
default = "~/.ssh/id_ed25519"
description = "Path to SSH private key"
}
variable "proxmox_snippets_storage" {
type = string
default = "local"
description = "Storage ID for Cloud-Init snippets (must support 'snippets' content type)"
}
variable "vm_id_first" {
type = number
default = 1000
}
variable "vm_template_id" {
type = number
default = 7777
}
variable "vm_tags_balancer" {
type = string
default = "k8s,balancer"
}
variable "vm_tags_master" {
type = string
default = "k8s,master"
}
variable "vm_tags_worker" {
type = string
default = "k8s,worker"
}
variable "vm_name_balancer" {
type = string
default = "k8s-balancer"
}
variable "vm_name_master" {
type = string
default = "k8s-master"
}
variable "vm_name_worker" {
type = string
default = "k8s-worker"
}
variable "vm_count_balancer" {
type = number
default = 1
}
variable "vm_count_master" {
type = number
default = 1
}
variable "vm_count_worker" {
type = number
default = 1
}
variable "vm_cpu_balancer" {
type = number
default = 2
}
variable "vm_cpu_master" {
type = number
default = 2
}
variable "vm_cpu_worker" {
type = number
default = 2
}
variable "vm_ram_balancer" {
type = number
default = 2048
}
variable "vm_ram_master" {
type = number
default = 2048
}
variable "vm_ram_worker" {
type = number
default = 2048
}
variable "vm_disk_storage_balancer" {
type = string
default = "storage"
}
variable "vm_disk_storage_master" {
type = string
default = "storage"
}
variable "vm_disk_storage_worker" {
type = string
default = "storage"
}
variable "vm_disk_format" {
type = string
default = "qcow2"
}
variable "vm_disk_size_balancer" {
type = number
default = 20
}
variable "vm_disk_size_master" {
type = number
default = 20
}
variable "vm_disk_size_worker" {
type = number
default = 20
}
variable "vm_ip_prefix" {
type = string
default = "192.168.122"
}
variable "vm_ip_first_balancer" {
type = number
default = 10
}
variable "vm_ip_first_master" {
type = number
default = 20
}
variable "vm_ip_first_worker" {
type = number
default = 30
}
variable "vm_ip_cidr" {
type = number
default = 24
}
variable "vm_ip_gateway" {
type = string
default = "192.168.122.1"
}
variable "vm_ip_dns" {
type = string
default = "8.8.8.8"
}
variable "vm_cloud_init_file" {
type = string
default = "null"
}
variable "user_name" {
type = string
default = "terraform"
}
variable "user_password" {
type = string
sensitive = true
}
variable "user_ssh_key" {
type = string
}Файлы со значениями переменных - dev.tfvars и prod.tfvars
Теперь заполняем файл со значениями. В нашем примере будем использовать два разных файла: для тестового окружения и для продуктивного. Начнём с тестового:
vim ./dev.tfvarsproject_env = "dev"
proxmox_node = "proxmox"
proxmox_api_url = "https://proxmox.example.com:8006/api2/json"
proxmox_api_token_id = "tfuser@pve!tf"
#proxmox_api_token_secret = "12345-qwerty-qwerty-qwerty-12345"
proxmox_ssh_user = "root"
proxmox_ssh_key = "~/.ssh/id_ed25519"
proxmox_snippets_storage = "local" # /var/lib/vz/snippets/
vm_id_first = "1000"
vm_template_id = "7777" # debian13-test-template
vm_tags_balancer = "k8s,balancer"
vm_tags_master = "k8s,master"
vm_tags_worker = "k8s,worker"
vm_name_balancer = "k8s-balancer"
vm_name_master = "k8s-master"
vm_name_worker = "k8s-worker"
vm_count_balancer = "1"
vm_count_master = "1"
vm_count_worker = "1"
vm_cpu_balancer = "2"
vm_cpu_master = "2"
vm_cpu_worker = "2"
vm_ram_balancer = "2048"
vm_ram_master = "4096"
vm_ram_worker = "4096"
vm_disk_storage_balancer = "storage"
vm_disk_storage_master = "storage"
vm_disk_storage_worker = "storage2"
vm_disk_format = "qcow2"
vm_disk_size_balancer = "20"
vm_disk_size_master = "20"
vm_disk_size_worker = "20"
vm_ip_prefix = "192.168.122"
vm_ip_first_balancer = "10"
vm_ip_first_master = "20"
vm_ip_first_worker = "30"
vm_ip_cidr = "24"
vm_ip_gateway = "192.168.122.1"
vm_ip_dns = "8.8.8.8"
user_name = "ivan"
#user_password = "SecretPassword"
user_ssh_key = "ssh-ed25519 AAAA..."project_env- произвольное название рабочей среды (dev / stage / prod)proxmox_node- имя ноды Proxmox, где будут развернуты ВМproxmox_api_url- API адрес Proxmox сервераproxmox_api_token_id- ID API-токена, который мы создали на этапе подготовки Proxmoxproxmox_api_token_secret- сам API токен (тут лучше не указывать, см. далее)proxmox_ssh_user- SSH-пользователь Proxmox (оболочка пользователя должна бытьbash)proxmox_ssh_key- SSH-ключ для доступа к серверу Proxmox (нужен для работы с cloud-init user data файлами)proxmox_snippets_storage- datastore для cloud-init сниппетов, в моём примере используетсяlocal, что по умолчанию эквивалентно/var/lib/vz/snippets/vm_id_first- базовый ID виртуальных машин в Proxmoxvm_template_id- ID шаблона ВМ из которого будут клонированы будущие сервераvm_tags_balancer- теги в Proxmox для серверов роли balancervm_tags_master- теги в Proxmox для серверов роли mastervm_tags_worker- теги в Proxmox для серверов роли workervm_name_balancer- префикс имени серверов роли balancervm_name_master- префикс имени серверов роли mastervm_name_worker- префикс имени серверов роли workervm_count_balancer- количество серверов роли balancervm_count_master- количество серверов роли mastervm_count_worker- количество серверов роли workervm_cpu_balancer- количество ядер CPU для серверов роли balancer (шт.)vm_cpu_master- количество ядер CPU для серверов роли master (шт.)vm_cpu_worker- количество ядер CPU для серверов роли worker (шт.)vm_ram_balancer- объем ОЗУ для серверов роли balancer (MB)vm_ram_master- объем ОЗУ для серверов роли master (MB)vm_ram_worker- объем ОЗУ для серверов роли worker (MB)vm_disk_storage_balancer- название хранилища для серверов роли balancervm_disk_storage_master- название хранилища для серверов роли mastervm_disk_storage_worker- название хранилища для серверов роли workervm_disk_format- формат диска (qcow2 / raw)vm_disk_size_balancer- размер диска balancer (GB)vm_disk_size_master- размер диска master (GB)vm_disk_size_worker- размер диска worker (GB) (у воркеров их будет 2 шт.)vm_ip_prefix- префикс IP подсетиvm_ip_first_balancer- суффикс IP для сервера роли balancer, далее +1vm_ip_first_master- суффикс IP для сервера роли master, далее +1vm_ip_first_worker- суффикс IP для сервера роли worker, далее +1vm_ip_cidr- маска сети в формате CIDRvm_ip_gateway- IP адрес шлюзаvm_ip_dns- IP адрес DNS сервераuser_name- имя пользователя, созданного с помощью cloud-inituser_password- пароль пользователя, созданного с помощью cloud-init (тут лучше не указывать, см. далее)user_ssh_key- публичный SSH ключ пользователя, созданного с помощью cloud-init
Отдельно стоит отметить чувствительные данные в переменных proxmox_api_token_secret и user_password. Вы можете заполнить их тут, но я рекомендую использовать для этого отдельный .env файл и подгружать его перед работой через source. Про него поговорим чуть позже. При хранении конфигурации OpenTofu в git репозитории просто добавьте файл .env в список .gitignore.
Если для конфигурирования сети виртуальных машин у вас используется DHCP сервер, то просто измените параметр address секции ip_config в файле main.tf для каждой роли:
ip_config {
ipv4 {
address = "dhcp"
gateway = var.vm_ip_gateway
}
}💡 При необходимости создайте SSH ключ с помощью команды:
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519⚠️ Еще раз повторю: оболочка пользователя SSH обязательно должна быть bash! Это требование провайдера bpg.
Теперь аналогично заполняем значения для продакшена:
vim ./prod.tfvarsproject_env = "prod"
proxmox_node = "proxmox"
proxmox_api_url = "https://proxmox.example.com:8006/api2/json"
proxmox_api_token_id = "tfuser@pve!tf"
#proxmox_api_token_secret = "12345-qwerty-qwerty-qwerty-12345"
proxmox_ssh_user = "root"
proxmox_ssh_key = "~/.ssh/id_ed25519"
proxmox_snippets_storage = "local" # /var/lib/vz/snippets/
vm_id_first = "2000"
vm_template_id = "7777" # debian13-test-template
vm_tags_balancer = "k8s,balancer"
vm_tags_master = "k8s,master"
vm_tags_worker = "k8s,worker"
vm_name_balancer = "k8s-balancer"
vm_name_master = "k8s-master"
vm_name_worker = "k8s-worker"
vm_count_balancer = "2"
vm_count_master = "3"
vm_count_worker = "5"
vm_cpu_balancer = "2"
vm_cpu_master = "2"
vm_cpu_worker = "4"
vm_ram_balancer = "2048"
vm_ram_master = "4096"
vm_ram_worker = "4096"
vm_disk_storage_balancer = "storage"
vm_disk_storage_master = "storage"
vm_disk_storage_worker = "storage2"
vm_disk_format = "qcow2"
vm_disk_size_balancer = "20"
vm_disk_size_master = "20"
vm_disk_size_worker = "20"
vm_ip_prefix = "192.168.122"
vm_ip_first_balancer = "110"
vm_ip_first_master = "120"
vm_ip_first_worker = "130"
vm_ip_cidr = "24"
vm_ip_gateway = "192.168.122.1"
vm_ip_dns = "8.8.8.8"
user_name = "ivan"
#user_password = "SecretPassword"
user_ssh_key = "ssh-ed25519 AAAA..."⚠️ При добавлении новых переменных или изменения метаданных существующих необходимо корректировать как файл variables.tf, так и файлы *.tfvars. Иначе можете получить ошибку.
Скорректируйте все параметры под ваши предпочтения.
Файл для хранения чувствительных данных - .env
Как уже говорил ранее, чувствительные данные лучше хранить в отдельном файле и подключать в качестве переменных окружения оболочки перед работой с OpenTofu:
vim ./.envТут используется специальный формат переменных с префиксом TF_VAR и затем имя и значение переменной:
export TF_VAR_user_password="SecretPassword"
export TF_VAR_proxmox_api_token_secret="12345-qwerty-qwerty-qwerty-12345"Укажите тут API токен сервера Proxmox, который мы получили на этапе подготовки гипервизора. Также тут задайте пароль будущего пользователя, который создастся на серверах с помощью cloud-init.
Экспортировать переменные в окружение можно командой:
source ./.envФайлы сценарии cloud-init - {balancer,master,worker}.tftpl
Для удобства хранения cloud-init файлов создаём отдельную директорию:
mkdir ./cloudinitДалее создаём отдельный файл для каждой роли.
Балансировщики:
vim ./cloudinit/balancer.tftpl#cloud-config
hostname: ${hostname}
manage_etc_hosts: true
users:
- name: ${user}
groups: sudo
shell: /usr/bin/bash
sudo: ['ALL=(ALL) NOPASSWD:ALL']
ssh_authorized_keys:
- ${ssh_key}
chpasswd:
list: |
${user}:${password}
expire: false
package_update: true
package_upgrade: true
packages:
- vim
- curl
- mtr-tiny
- haproxy
runcmd:
- systemctl enable haproxy
- echo "Hello from Balancer node" > /greetingsМастер:
vim ./cloudinit/master.tftpl#cloud-config
hostname: ${hostname}
manage_etc_hosts: true
users:
- name: ${user}
groups: sudo
shell: /usr/bin/bash
sudo: ['ALL=(ALL) NOPASSWD:ALL']
ssh_authorized_keys:
- ${ssh_key}
chpasswd:
list: |
${user}:${password}
expire: false
package_update: true
package_upgrade: true
packages:
- vim
- curl
- mtr-tiny
runcmd:
- echo "Hello from Master node" > /greetingsВоркер:
vim ./cloudinit/worker.tftpl#cloud-config
hostname: ${hostname}
manage_etc_hosts: true
users:
- name: ${user}
groups: sudo
shell: /usr/bin/bash
sudo: ['ALL=(ALL) NOPASSWD:ALL']
ssh_authorized_keys:
- ${ssh_key}
chpasswd:
list: |
${user}:${password}
expire: false
package_update: true
package_upgrade: true
packages:
- vim
- curl
- mtr-tiny
runcmd:
- echo "Hello from Worker node" > /greetingsНаполнение - произвольное. Кастомизируйте сценарии под ваши нужды и предпочтения.
⚠️ В файлах-сценариях cloud-init обязательно наличие первой строки вида:
#cloud-configБез неё cloud-init проигнорирует файлы.
Ссылка на документацию по cloud-init c примерами.
С файлами проекта мы завершили. Переходим тестированию подготовленной нами конфигурации.
💡 Актуальные версии исходных файлов из статьи вы всегда найдёте на моём GitHub:
Инициализация модуля proxmox/bpg
Первым делом необходимо инициализировать проект. В процессе инициализации OpenTofu скачает провайдер нужной версии, если он еще не установлен:
tofu init -upgrade
В текущей директории появятся системные файлы, в т.ч. исполняемый файл провайдера:
tree ./.terraform
Теперь выполним валидацию нашей конфигурации:
tofu validateЕсли видите подобное:
Success! The configuration is valid.Продолжаем🏃.
Запуск создания dev инфраструктуры
Первым делом всегда генерируется план вносимых изменений. Но перед этим мы определим контекст окружения (dev, prod), про который мы говорили во время заполнения файла backend.tf:
tofu init -reconfigure -backend-config="path=./dev.tfstate"Теперь смотрим план, указывая при этом файл переменных окружения dev:
tofu plan -var-file=./dev.tfvarsЕсли все значения вы заполнили верно, то команда выполнится без ошибок и покажет вам будущие изменения/добавления в Proxmox:

| Символ | Что значит |
|---|---|
+ | создать |
~ | изменить |
- | удалить |
-/+ | удалить и создать заново |
<= | только чтение (data source) |
Также популярной практикой является сохранение плана во внешний файл специального формата, так называемый plan файл:
tofu plan -var-file=./dev.tfvars -out ./dev.tfplanДля чего нужен plan файл?
Файл tfplan - это сериализованное представление плана выполнения, которое генерируется командой tofu plan -out <filename>. Он фиксирует точные изменения, которые OpenTofu (или Terraform) намеревается внести в вашу инфраструктуру. Основное его назначение - гарантировать, что команда tofu apply выполнит именно те операции, которые были показаны и одобрены на этапе планирования, предотвращая любые расхождения, если конфигурация или переменные изменятся между выполнением plan и apply. Это критически важно для процессов ревью, аудита и автоматизированных конвейеров CI/CD, где план может быть сгенерирован, сохранен как артефакт, а затем применен позже, обеспечивая предсказуемое и контролируемое развертывание.
Посмотреть содержимое plan файла можно командой:
tofu show ./dev.tfplanЕсли вы согласны с планом, то для применения конфигурации выполняем:
tofu apply -var-file=./dev.tfvarsЕще раз всё проверяем и вводим yes, а затем Enter:

Или в случае plan файла команда такая:
tofu apply ./dev.tfplan⚠️ В случае применения plan файла интерактивного подтверждения не будет! Будьте внимательны.
После запуска ожидаем завершение процедуры⏳.
В вебе Proxmox можно наблюдать появление новых машин:

После завершения ВМ будут автоматически запущены и начнётся выполнение сценария cloud-init:

Как видно, виртуальные машины имеют желаемые конфигурации: node, name, ID, CPU, RAM, Disk, IP:

Также обратите внимание, что конфигурация Worker node подразумевает наличие двух дисков (описано в main.tf):

Проверка серверов
Первым делом проверяем доступ к серверам по паролю с консоли гипервизора:

И доступ по SSH:

Если на серверах создался файл /greetings - то всё работает корректно👍.
💡 Совет
Для дебага cloud-init на целевых серверах используйте команды:
cloud-init status
journalctl -u cloud-initЗапуск создания prod инфраструктуры
Аналогичным образом создаём продакшн инфраструктуру.
Обязательно пред применением меняем контекст выполнения на prod:
tofu init -reconfigure -backend-config="path=./prod.tfstate"☝️ Важно. Для работы с dev окружением необходимо будет также сменить контекст обратно с помощью команды:
tofu init -reconfigure -backend-config="path=./dev.tfstate"Смотрим план:
tofu plan -var-file=./prod.tfvars
# или с plan файлом
tofu plan -var-file=./prod.tfvars -out ./prod.tfplanЕсли всё ок - применяем изменения:
tofu apply -var-file=./prod.tfvars -parallelism=2
# или
tofu apply ./prod.tfplan -parallelism=2📝 В OpenTofu / Terraform параметр parallelism определяет максимальное количество ресурсов, которые могут создаваться / изменяться / удаляться одновременно. При создании большой инфраструктуры в Proxmox рекомендуется указывать значение 1-2, чтобы избежать ошибок вида:
TASK ERROR: clone failed: can't lock file '/var/lock/pve-manager/pve-storage-storage' - got timeoutОсобенно при работе с большими дисками.
В итоге вот такой “зоопарк” появляется через несколько минут:

Теперь после тщательной подготовки создать сложную инфраструктуру можно буквально в считанные минуты. Как по мне очень удобно, особенно для тестов😌.
Изменение существующей инфраструктуры
Для изменения текущей инфраструктуры просто внесите нужные изменения в конфигурацию и примените её.
Например, увеличим объем ОЗУ для балансировщиков dev окружения с 2048 до 4096:
sed -i '/^vm_ram_balancer/s/2048/4096/' ./dev.tfvarsИ смотрим план:
tofu init -reconfigure -backend-config="path=./dev.tfstate"
tofu plan -var-file=./dev.tfvars
Если всё ок, применяем изменения:
⚠️ При изменении объема ОЗУ сервер будет перезапущен.
tofu apply -var-file=./dev.tfvarsПроверяем:

Всё отлично.
❗️ Осторожно
Изменение некоторых параметров может привести к пересозданию ресурсов, т.е. уничтожению работающих виртуальных машин.
Рассмотрите использование полезных опций управления жизненным циклом ресурсов:
lifecycle {
prevent_destroy = true # запретить удалять ресурс
create_before_destroy = true # создать новый ресурс, перед удалением старого
ignore_changes = [agent, disk, initialization,] # список блоков, изменения которых нужно игнорировать
}Уничтожение инфраструктуры
Для удаления инфраструктуры переключите нужный контекст и используйте команду destroy.
Например, удаляем dev окружение:
# Обязательно меняем контекст окружения
tofu init -reconfigure -backend-config="path=./dev.tfstate"
tofu destroy -var-file=./dev.tfvarsНемного про Debug OpenTofu/Terraform
Валидация конфигурации
Команда валидации синтаксиса файлов:
tofu validateДолжно быть:
Success! The configuration is valid.Форматирование
Команда tofu fmt выравнивает отступы, сортирует аргументы и приводит кавычки / списки к каноничному виду:
tofu fmt -check # проверяет, нужно ли форматирование
tofu fmt # форматирует файлы
tofu fmt -recursive # рекурсивно по каталогам
# тест
tofu plan -var-file=./dev.tfvarsЛогирование
Включить логирование для текущего сеанса оболочки:
export TF_LOG=DEBUG
export TF_LOG_PATH="tofu.log"📝 Уровни логирования:
- TRACE
- DEBUG
- INFO
- WARN
- ERROR
При последующей работе с OpenTofu создастся файл в текущей директории.
Смотреть лог:
tail -f ./tofu.logОтключить логирование:
unset TF_LOG TF_LOG_PATHЗаключение
Мы с вами изучили, как реализовать концепцию “Инфраструктура как код” на примере использования OpenTofu для автоматического развертывания инфраструктуры под условный Kubernetes кластер.
В следующих заметках мы займемся как раз установкой оного на инфре из этой статьи: настройка балансировки, установка и настройка control plane и workers.
Обязательно подписывайтесь на телегу😉, чтобы ничего не пропустить. + у нас там проходят Linux викторины🐧.
Спасибо, что читаете. Удачи вам в автоматизации процессов!


