Создание инфраструктуры в Proxmox с помощью Terraform или OpenTofu
Приветствую!

В этой статье мы будем использовать OpenTofu с провайдером bpg для управления инфраструктурой Proxmox.

TLDR

Proxmox:

BASH
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
Нажмите, чтобы развернуть и увидеть больше

Клиент:

BASH
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.

В качестве примера мы автоматизируем процесс создания и подготовки виртуальных машин для кластерной инфраструктуры k8s (kubernetes).

Данная инфра включает в себя 3 роли ВМ:

Все параметры ВМ удобно задаются с помощью разделённых по окружениям var файлов.

В проекте также предусмотрены пользовательские сценарии подготовки серверов, после их развёртывания. В качестве bootstrap сценария используется Cloud init - доступны шаблоны для каждой роли, которые легко кастомизировать.

Вводные данные

ПО, используемое в статье:

ПОВерсия
Proxmox9.0.3 (Debian 13)
OpenTofu1.11.1
Провайдер bpg0.87.0
Клиент с OpenTofuDebian 13

Ну, хватит разговоров, погнали делать автоматизацию🚘.

Подготовка Proxmox

Начнём с гипервизора🖥️.

Подготовка образа ВМ в формате qcow2

Подключаемся к серверу Proxmox по SSH:

BASH
ssh root@proxmox.home.lan
Нажмите, чтобы развернуть и увидеть больше

Обновляем кэш пакетов и устанавливаем необходимые утилиты:

BASH
apt update && apt install -y libguestfs-tools
Нажмите, чтобы развернуть и увидеть больше

Скачиваем актуальный базовый образ Debian 13 в формате qcow2:

BASH
wcurl https://cloud.debian.org/images/cloud/trixie/latest/debian-13-generic-amd64.qcow2

ls -l ./debian-13-generic-amd64.qcow2
Нажмите, чтобы развернуть и увидеть больше

Выполняем ресайз диска (укажите подходящее вам значение):

BASH
qemu-img resize ./debian-13-generic-amd64.qcow2 20G
Нажмите, чтобы развернуть и увидеть больше
TXT
Image resized.
Нажмите, чтобы развернуть и увидеть больше

Теперь кастомизируем наш образ под свои предпочтения. Я выполню обновление пакетов, настройку локали и установку джентльменского набора утилит для виртуальной машины:

BASH
# Процесс может занять некоторое время
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
Нажмите, чтобы развернуть и увидеть больше

Подготовка шаблона VM в Proxmox

Переходим к импорту диска в Proxmox:

BASH
# Создаём новую виртуальную машину с 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
Нажмите, чтобы развернуть и увидеть больше

Выводим список файлов в директории ВМ (хранилище storage), в моём случае это: /mnt/storage/images/7777/:

BASH
ls -l /mnt/storage/images/7777/
Нажмите, чтобы развернуть и увидеть больше

Видим сам образ и cloud-init диск👌.

Создание реквизитов доступа к Proxmox

Создаём отдельную роль для работы с OpenTofu:

BASH
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:

BASH
pveum user add tfuser@pve

pveum aclmod / -user tfuser@pve -role TFUser
Нажмите, чтобы развернуть и увидеть больше

Создаём токен для нашей сервисной УЗ:

BASH
# токен со всеми привилегиями, как у пользователя tfuser
pveum user token add tfuser@pve tf --privsep 0
Нажмите, чтобы развернуть и увидеть больше

Получим такой вывод:

Сохраните где-нибудь full-tokenid и value самого токена. Они нам понадобятся во время настройки OpenTofu.

Все действия выше можно было выполнить в GUI Proxmox, в разделе Datacenter —> Permissions:

Но через командную строку быстрее, а для инструкции - нагляднее🙃.

Подготовка OpenTofu

Установка OpenTofu в Debian

В соответствии с оф. документацией подключаем репозиторий OpenTofu и выполняем установку нативно:

BASH
# Обновляем кэш
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:

BASH
sudo apt update

sudo apt install -y tofu

command -v tofu

tofu --version
Нажмите, чтобы развернуть и увидеть больше

Теперь в нашей системе появилась новая утилита командной строки: tofu.

Создание файлов проекта для кластера k8s

Переходим к подготовке файлов проекта для кластера k8s. Первым делом создаём директорию, например, в хомяке и переходим в неё:

BASH
mkdir -vp ~/TF/k8s && cd ~/TF/k8s
Нажмите, чтобы развернуть и увидеть больше

Прежде, чем начать, хочу отметить еще пару моментов:

  1. Файлы OpenTofu/Terraform имеют расширение tf и пишутся на специальном языке HCL - HashiCorp Configuration Language.
  2. В представленной мной конфигурации источником истины являются файлы переменных (могут иметь произвольное расширение, в моём случае *.tfvars). Именно из них берутся все нужные значения и подставляются в конфигурацию OpenTofu в процессе работы.

Файл описания провайдера - provider.tf

Что ж, а начнём мы с файла провайдера для Proxmox:

BASH
vim ./provider.tf
Нажмите, чтобы развернуть и увидеть больше

Наполняем:

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

Создаём файл:

BASH
vim ./backend.tf
Нажмите, чтобы развернуть и увидеть больше

Наполняем:

backend.tf
terraform {
  backend "local" {}
}
Нажмите, чтобы развернуть и увидеть больше

С помощью данной конфигурации мы определяем локальное хранение state файлов, которые будут хранить состояние инфраструктуры и информацию о созданных ресурсах.

В нашем случае это необходимо, чтобы управлять двумя разными окружениями (dev и prod) без конфликтов.

Файл описания конфигурации ВМ - main.tf

Теперь создаём основной файл конфигурации серверов наших 3-х ролей: балансировщики, мастер и воркер ноды:

BASH
vim ./main.tf
Нажмите, чтобы развернуть и увидеть больше
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 объявлять переменные и указывать их значения принято в разных файлах. Это необязательное условие, но так проще организовывать проект. К тому же в файле, где объявляются переменные можно указать дефолтные значения и описание:

BASH
vim ./variables.tf
Нажмите, чтобы развернуть и увидеть больше
variables.tf
variable "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

Теперь заполняем файл со значениями. В нашем примере будем использовать два разных файла: для тестового окружения и для продуктивного. Начнём с тестового:

BASH
vim ./dev.tfvars
Нажмите, чтобы развернуть и увидеть больше
dev.tfvars
project_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..."
Нажмите, чтобы развернуть и увидеть больше

Отдельно стоит отметить чувствительные данные в переменных proxmox_api_token_secret и user_password. Вы можете заполнить их тут, но я рекомендую использовать для этого отдельный .env файл и подгружать его перед работой через source. Про него поговорим чуть позже. При хранении конфигурации OpenTofu в git репозитории просто добавьте файл .env в список .gitignore.

Теперь аналогично заполняем значения для продакшена:

BASH
vim ./prod.tfvars
Нажмите, чтобы развернуть и увидеть больше
dev.tfvars
project_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..."
Нажмите, чтобы развернуть и увидеть больше

Скорректируйте все параметры под ваши предпочтения.

Файл для хранения чувствительных данных - .env

Как уже говорил ранее, чувствительные данные лучше хранить в отдельном файле и подключать в качестве переменных окружения оболочки перед работой с OpenTofu:

BASH
vim ./.env
Нажмите, чтобы развернуть и увидеть больше

Тут используется специальный формат переменных с префиксом TF_VAR и затем имя и значение переменной:

.env
export TF_VAR_user_password="SecretPassword"
export TF_VAR_proxmox_api_token_secret="12345-qwerty-qwerty-qwerty-12345"
Нажмите, чтобы развернуть и увидеть больше

Укажите тут API токен сервера Proxmox, который мы получили на этапе подготовки гипервизора. Также тут задайте пароль будущего пользователя, который создастся на серверах с помощью cloud-init.

Экспортировать переменные в окружение можно командой:

BASH
source ./.env
Нажмите, чтобы развернуть и увидеть больше

Файлы сценарии cloud-init - {balancer,master,worker}.tftpl

Для удобства хранения cloud-init файлов создаём отдельную директорию:

BASH
mkdir ./cloudinit
Нажмите, чтобы развернуть и увидеть больше

Далее создаём отдельный файл для каждой роли.

Балансировщики:

BASH
vim ./cloudinit/balancer.tftpl
Нажмите, чтобы развернуть и увидеть больше
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
Нажмите, чтобы развернуть и увидеть больше

Мастер:

BASH
vim ./cloudinit/master.tftpl
Нажмите, чтобы развернуть и увидеть больше
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
Нажмите, чтобы развернуть и увидеть больше

Воркер:

BASH
vim ./cloudinit/worker.tftpl
Нажмите, чтобы развернуть и увидеть больше
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 c примерами.

С файлами проекта мы завершили. Переходим тестированию подготовленной нами конфигурации.

Инициализация модуля proxmox/bpg

Первым делом необходимо инициализировать проект. В процессе инициализации OpenTofu скачает провайдер нужной версии, если он еще не установлен:

BASH
tofu init -upgrade
Нажмите, чтобы развернуть и увидеть больше

В текущей директории появятся системные файлы, в т.ч. исполняемый файл провайдера:

BASH
tree ./.terraform
Нажмите, чтобы развернуть и увидеть больше

Теперь выполним валидацию нашей конфигурации:

BASH
tofu validate
Нажмите, чтобы развернуть и увидеть больше

Если видите подобное:

TXT
Success! The configuration is valid.
Нажмите, чтобы развернуть и увидеть больше

Продолжаем🏃.

Запуск создания dev инфраструктуры

Первым делом всегда генерируется план вносимых изменений. Но перед этим мы определим контекст окружения (dev, prod), про который мы говорили во время заполнения файла backend.tf:

BASH
tofu init -reconfigure -backend-config="path=./dev.tfstate"
Нажмите, чтобы развернуть и увидеть больше

Теперь смотрим план, указывая при этом файл переменных окружения dev:

BASH
tofu plan -var-file=./dev.tfvars
Нажмите, чтобы развернуть и увидеть больше

Если все значения вы заполнили верно, то команда выполнится без ошибок и покажет вам будущие изменения/добавления в Proxmox:

СимволЧто значит
+создать
~изменить
-удалить
-/+удалить и создать заново
<=только чтение (data source)

Также популярной практикой является сохранение плана во внешний файл специального формата, так называемый plan файл:

BASH
tofu plan -var-file=./dev.tfvars -out ./dev.tfplan
Нажмите, чтобы развернуть и увидеть больше

Если вы согласны с планом, то для применения конфигурации выполняем:

BASH
tofu apply -var-file=./dev.tfvars
Нажмите, чтобы развернуть и увидеть больше

Еще раз всё проверяем и вводим yes, а затем Enter:

Или в случае plan файла команда такая:

BASH
tofu apply ./dev.tfplan
Нажмите, чтобы развернуть и увидеть больше

После запуска ожидаем завершение процедуры⏳.

В вебе Proxmox можно наблюдать появление новых машин:

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

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

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

Проверка серверов

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

И доступ по SSH:

Если на серверах создался файл /greetings - то всё работает корректно👍.

Запуск создания prod инфраструктуры

Аналогичным образом создаём продакшн инфраструктуру.

Обязательно пред применением меняем контекст выполнения на prod:

BASH
tofu init -reconfigure -backend-config="path=./prod.tfstate"
Нажмите, чтобы развернуть и увидеть больше

Смотрим план:

BASH
tofu plan -var-file=./prod.tfvars
# или с plan файлом
tofu plan -var-file=./prod.tfvars -out ./prod.tfplan
Нажмите, чтобы развернуть и увидеть больше

Если всё ок - применяем изменения:

BASH
tofu apply -var-file=./prod.tfvars -parallelism=2
# или
tofu apply ./prod.tfplan -parallelism=2
Нажмите, чтобы развернуть и увидеть больше

В итоге вот такой “зоопарк” появляется через несколько минут:

Теперь после тщательной подготовки создать сложную инфраструктуру можно буквально в считанные минуты. Как по мне очень удобно, особенно для тестов😌.

Изменение существующей инфраструктуры

Для изменения текущей инфраструктуры просто внесите нужные изменения в конфигурацию и примените её.

Например, увеличим объем ОЗУ для балансировщиков dev окружения с 2048 до 4096:

BASH
sed -i '/^vm_ram_balancer/s/2048/4096/' ./dev.tfvars
Нажмите, чтобы развернуть и увидеть больше

И смотрим план:

BASH
tofu init -reconfigure -backend-config="path=./dev.tfstate"

tofu plan -var-file=./dev.tfvars
Нажмите, чтобы развернуть и увидеть больше

Если всё ок, применяем изменения:

BASH
tofu apply -var-file=./dev.tfvars
Нажмите, чтобы развернуть и увидеть больше

Проверяем:

Всё отлично.

Уничтожение инфраструктуры

Для удаления инфраструктуры переключите нужный контекст и используйте команду destroy.

Например, удаляем dev окружение:

Немного про Debug OpenTofu/Terraform

Валидация конфигурации

Команда валидации синтаксиса файлов:

BASH
tofu validate
Нажмите, чтобы развернуть и увидеть больше

Должно быть:

TXT
Success! The configuration is valid.
Нажмите, чтобы развернуть и увидеть больше

Форматирование

Команда tofu fmt выравнивает отступы, сортирует аргументы и приводит кавычки / списки к каноничному виду:

BASH
tofu fmt -check        # проверяет, нужно ли форматирование
tofu fmt               # форматирует файлы
tofu fmt -recursive    # рекурсивно по каталогам

# тест
tofu plan -var-file=./dev.tfvars
Нажмите, чтобы развернуть и увидеть больше

Логирование

Включить логирование для текущего сеанса оболочки:

BASH
export TF_LOG=DEBUG

export TF_LOG_PATH="tofu.log"
Нажмите, чтобы развернуть и увидеть больше

При последующей работе с OpenTofu создастся файл в текущей директории.

Смотреть лог:

BASH
tail -f ./tofu.log
Нажмите, чтобы развернуть и увидеть больше

Отключить логирование:

BASH
unset TF_LOG TF_LOG_PATH
Нажмите, чтобы развернуть и увидеть больше

Заключение

Мы с вами изучили, как реализовать концепцию “Инфраструктура как код” на примере использования OpenTofu для автоматического развертывания инфраструктуры под условный Kubernetes кластер.

В следующих заметках мы займемся как раз установкой оного на инфре из этой статьи: настройка балансировки, установка и настройка control plane и workers.

Обязательно подписывайтесь на телегу😉, чтобы ничего не пропустить. + у нас там проходят Linux викторины🐧.

Спасибо, что читаете. Удачи вам в автоматизации процессов!

Используемые материалы

Авторские права

Автор: Иван Чёрный

Ссылка: https://r4ven.me/virtualization/sozdanie-infrastruktury-v-proxmox-s-pomoshchyu-terraform-ili-opentofu/

Лицензия: CC BY-NC-SA 4.0

Использование материалов блога разрешается при условии: указания авторства/источника, некоммерческого использования и сохранения лицензии.

Начать поиск

Введите ключевые слова для поиска статей

↑↓
ESC
⌘K Горячая клавиша