Что такое Terraform?
Terraform — это один из лучших и популярных инструментов для Infrastructure as Code.
Если для вас Terraform пустой звук, рекомендую ознакомиться с ним на канале ADV-IT. Того, что он рассказывает, будет достаточно для начала знакомства и понимания всего, что я буду делать (ну или почти всего). Ссылка на плейлист
Я не буду расписывать основы данного инструмента в этой статье и сосредоточусь на связке Terraform + Proxmox через наиболее удачный, на мой взгляд, провайдер: bpg/proxmox
Выбор провайдера для Proxmox
Провайдер — это прослойка, которая объясняет Terraform, как работать с API облака (или Proxmox в нашем случае).
Есть много официальных провайдеров для облаков: AWS, Google Cloud, Yandex Cloud, Cloud.ru и так далее. Но официального провайдера для Proxmox просто не существует. Существует несколько неофициальных.
Методом проб и ошибок для себя был выбран bpg/proxmox — он очень редко ломает обратную совместимость, в отличие от некоторых других. А также он более 6 лет в разработке, и последняя версия 0.83.2 вышла за день до написания этой статьи.
Изучение возможностей провайдера
Очень просто — к каждому приличному провайдеру существует документация, где описаны все сущности, которыми можно с его помощью управлять. Как правило, этой документации достаточно.

Настройка Terraform для работы с Proxmox
В первую очередь нам необходимо определиться с тем, где мы будем хранить наш state. В данном примере я буду использовать бесплатное объектное хранилище от Cloud.ru.
Создание S3-бакета для хранения state
Создаём S3-бакет:

И ключ доступа для него:

Конфигурация backend и провайдера
Создаём файл backend.tf с конфигурацией провайдера:
hclterraform {
backend "s3" {
bucket = "terraform-state"
key = "blog/terraform.tfstate"
region = "ru-central-1"
endpoint = "https://s3.cloud.ru"
skip_region_validation = true
skip_credentials_validation = true
force_path_style = true
skip_metadata_api_check = true
}
}
Также нам необходимо экспортировать 2 переменные:
bashexport AWS_ACCESS_KEY_ID="<tennant_id>:<access_key>"
export AWS_SECRET_ACCESS_KEY="<secret_key>"
Внимательно! В access key нужно положить и tennant_id и access_key через двоеточие!
После этого мы можем инициализировать terraform:
bashterraform init

Добавляем описание провайдера и необходимые переменные: provider.tf
hcl# https://registry.terraform.io/providers/bpg/proxmox/latest/docs
terraform {
required_providers {
proxmox = {
source = "bpg/proxmox"
}
}
}
provider "proxmox" {
endpoint = var.endpoint
insecure = true
username = var.proxmox_username
password = var.main_password
}
hclvariable "endpoint" {
description = "Hostname or IP of Proxmox server"
type = string
}
variable "proxmox_username" {
description = "User for Proxmox API"
type = string
}
variable "main_password" {
description = "Password for Proxmox API"
type = string
sensitive = true
}
и экспортируем переменные для безопасной передачи в переменные:
shexport TF_VAR_endpoint="https://XX.XX.XX.XX:8006"
export TF_VAR_proxmox_username="ваш_логин"
export TF_VAR_main_password="ваш_пароль"
После чего выполняем:
bashterraform init
И видим:

Это всё, что нам необходимо для начала управления ресурсами в Proxmox через Terraform!
Создание виртуальных машин в Proxmox
Пришло время создать нашу первую ВМ. Но из чего? Нам нужен образ. Давайте его опишем.
Загрузка образа Ubuntu
Создаём файл images.tf для загрузки образа Ubuntu:
hclresource "proxmox_virtual_environment_download_file" "latest_ubuntu_22_jammy_qcow2_img" {
content_type = "import"
datastore_id = "local"
node_name = "pve"
url = "https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img"
# need to rename the file to *.qcow2 to indicate the actual file format for import
file_name = "jammy-server-cloudimg-amd64.qcow2"
}
Сделаем план, чтобы увидеть, что создастся в нашем кластере:
bashterraform plan

То что нужно, применяем:
bashterraform apply
Можем понаблюдать, что происходит через веб:

Отлично, мы успешно импортировали образ:

Создание первой виртуальной машины
Давайте наконец создадим нашу первую ВМ!
Создаём файл vm.tf и копипастим в него конфиг из документации провайдера, чуть подправив:
hclresource "proxmox_virtual_environment_vm" "ubuntu_vm" {
name = "first-vm"
description = "Managed by Terraform"
tags = ["terraform", "ubuntu"]
node_name = "pve1"
vm_id = 4321
agent {
enabled = false
}
startup {
order = "3"
up_delay = "60"
down_delay = "60"
}
cpu {
cores = 2
type = "x86-64-v2-AES" # recommended for modern CPUs
}
memory {
dedicated = 2048
floating = 2048 # set equal to dedicated to enable ballooning
}
disk {
datastore_id = "local-lvm"
import_from = proxmox_virtual_environment_download_file.latest_ubuntu_22_jammy_qcow2_img.id
interface = "scsi0"
}
initialization {
# uncomment and specify the datastore for cloud-init disk if default `local-lvm` is not available
datastore_id = "local-lvm"
ip_config {
ipv4 {
address = "dhcp"
}
}
user_account {
keys = [trimspace(var.pc_public_key)]
password = random_password.ubuntu_vm_password.result
username = "ubuntu"
}
}
network_device {
bridge = "vmbr0"
}
resource "random_password" "ubuntu_vm_password" {
length = 16
override_special = "_%@"
special = true
}
Положим наш ключ в переменные:
Создаём файл variables.tf с дополнительной переменной:
hclvariable "pc_public_key" {
description = "Public key for VM's SSH"
type = string
sensitive = true
}
Экспортируем его для безопасной передачи:
bashexport TF_VAR_pc_public_key=$(cat ~/.ssh/id_rsa.pub)
Проверяем через terraform plan, что создается 2 ресурса:
- ВМ
- Случайный пароль для пользователя ubuntu
Применяем через terraform apply:

Наша машина готова, но какой же пароль у пользователя Ubuntu? Давайте узнаем:
Создаём файл output.tf:
hcloutput "ubuntu_vm_password" {
value = random_password.ubuntu_vm_password.result
sensitive = true
}
Выполним ещё раз terraform apply и затем:
bashterraform output -raw ubuntu_vm_password
Вот и наш пароль!

Не рекомендую использовать DHCP в инфраструктуре, но для первой ВМ подойдёт. Найдём её адрес на роутере:

И зайдём на неё по SSH:

Ура, мы внутри! Всё благодаря тому, что мы указали наш публичный SSH-ключ в настройках. Удобно? Я думаю, да!
Продвинутые возможности: Модули Terraform
Итак, мы умеем создавать ВМ, но выглядит это, откровенно говоря, слишком массивно. Можно ли создавать ВМ значительно компактнее? Для этого нам нужны модули. Давайте напишем свой первый модуль!
Подробнее о создании модулей Terraform читайте в статье Создание модулей Terraform, где я показываю, как создать модуль для автоматического скачивания cloud-образов.
Создание модуля для виртуальных машин
Создадим папку modules, а в ней папку vms и уже внутри неё 2 файла:
Создаём файл modules/vms/vms.tf:
hclterraform {
required_providers {
proxmox = {
source = "bpg/proxmox"
}
}
}
resource "proxmox_virtual_environment_vm" "vm" {
name = var.vm_name
tags = var.tags
node_name = var.node_name
vm_id = var.vm_id
boot_order = ["sata0"]
description = var.description
pool_id = var.pool_id
agent { enabled = true }
cpu {
cores = var.cores
type = "host"
}
memory { dedicated = var.ram }
startup {
order = "2"
up_delay = "5"
}
disk {
datastore_id = var.datastore_id
file_id = var.image_file
interface = "sata0"
size = var.disk_size
}
initialization {
dns {
servers = var.dns_servers
}
ip_config {
ipv4 {
address = var.address
gateway = var.gateway
}
}
user_account {
keys = [trimspace(var.pc_public_key)]
password = var.vm_password
username = "root"
}
}
dynamic "usb" {
for_each = var.usb != null ? [var.usb] : []
content {
host = usb.value.host
mapping = usb.value.mapping
usb3 = usb.value.usb3
}
}
network_device { bridge = "vmbr0" }
operating_system { type = "l26" }
lifecycle {
ignore_changes = [
cpu["architecture"],
initialization[0].dns[0].servers,
initialization[0].user_account[0].keys,
]
}
}
output "vm_id" {
value = proxmox_virtual_environment_vm.vm.id
}
hclvariable "vm_name" {
description = "Name of the VM"
type = string
default = null
}
variable "node_name" {
description = "Name of the node where the VM will be created"
type = string
default = null
}
variable "tags" {
description = "List of tags to be associated with the VM"
type = list(string)
default = null
}
variable "vm_id" {
description = "ID of the VM"
type = number
default = null
}
variable "cores" {
description = "Number of CPU cores for the VM"
type = number
default = null
}
variable "ram" {
description = "Amount of RAM for the VM"
type = number
default = null
}
variable "disk_size" {
description = "Size of the disk for the VM"
type = number
default = null
}
variable "address" {
description = "IP address for the VM"
type = string
default = null
}
variable "pc_public_key" {
description = "Public key for SSH access"
type = string
default = null
}
variable "vm_password" {
description = "Password for the VM"
type = string
default = null
}
variable "image_file" {
description = "Path to the image file"
type = string
default = null
}
variable "pool_id" {
description = "ID of the pool where the VM will be created"
type = string
default = null
}
variable "usb" {
description = "Map a host USB device to a VM"
type = object({
host = string
mapping = string
usb3 = bool
})
default = null
}
variable "description" {
description = "Description of the VM"
type = string
default = null
}
variable "gateway" {
description = "Gateway IP address for the VM network"
type = string
default = "10.11.12.52"
}
variable "dns_servers" {
description = "List of DNS servers for the VM"
type = list(string)
default = ["10.11.12.170", "10.11.12.52"]
}
variable "datastore_id" {
description = "Datastore ID for VM disk storage"
type = string
default = "local-lvm"
}
Добавим файл в корень проекта:
Создаём файл vm_resources.tf:
hcllocals {
vms_config = yamldecode(file("./configs/vms.yaml"))
}
module "vms" {
for_each = { for vm in(local.vms_config.vms != null ? local.vms_config.vms : []) : vm.vm_name => vm }
source = "./modules/vms"
vm_name = each.value.vm_name
node_name = try(each.value.node_name, "pve5")
vm_id = each.value.vm_id
cores = try(each.value.cores, "2")
ram = try(each.value.ram, "2048")
disk_size = try(each.value.disk_size, 50)
address = each.value.address
tags = concat(local.vms_config.tags, each.value.tags)
vm_password = var.vm_password
pc_public_key = file("~/.ssh/id_rsa.pub")
image_file = try(module.images[each.value.image_name].images[each.value.node_name].id, module.images["ol94"].images[each.value.node_name].id, module.images["ol94"].images["pve5"].id)
pool_id = try(each.value.pool_id, null)
usb = try(each.value.usb, null)
description = try(each.value.description, null)
}
Создадим папку configs и в ней файл vms.yaml:
yamltags:
- terraform
vms:
- vm_id: 4322
vm_name: second-vm
address: 10.11.12.160/24
node_name: pve3
cores: 2
ram: 2048
disk_size: 20
tags: [modules, yaml_config]
description: "Modules are awesome!"
- vm_id: 4323
vm_name: third-vm
address: 10.11.12.161/24
node_name: pve4
cores: 2
ram: 2048
disk_size: 20
tags: [modules, yaml_config]
description: "Modules are awesome!"
После добавления модуля необходимо сделать terraform init, чтобы он установил наши модули:

И попробуем теперь сделать terraform plan:

Магия сработала!
Теперь чтобы создать новую ВМ, нам всего лишь надо добавить её в vms.yaml в человекочитаемом формате!
Домашнее задание
Разберись сам, как работают модули и переделай его так, как будет удобно тебе!
Итоги
Мы научились инициализировать терраформ с нуля, создавать виртуальные машины, посмотрели, как пишутся модули, и как с их помощью можно сильно улучшить читаемость конфигов.
Все примеры, лежат в этом репозитории.
Мой боевой домашний конфиг с несколькими дополнительными модулями тут