Init commit

This commit is contained in:
Iurii Anfinogenov
2026-04-13 17:48:41 +00:00
parent 815cdc5c14
commit e40ee15da1
32 changed files with 2476 additions and 58 deletions

51
.gitignore vendored Normal file
View File

@@ -0,0 +1,51 @@
# OpenTofu / Terraform
**/.terraform/
**/.terraform.lock.hcl
# State
**/*.tfstate
**/*.tfstate.*
**/crash.log
**/crash.*.log
# Variable files with secrets
**/*.tfvars
**/*.tfvars.json
!**/*.tfvars.example
# Optional local override files
**/override.tf
**/override.tf.json
**/*_override.tf
**/*_override.tf.json
# CLI config / credentials
.terraformrc
terraform.rc
# Environment files
.env
.env.*
!.env.example
# Secrets / keys
*.pem
*.key
*.p12
*.pfx
id_rsa
id_rsa.pub
id_ed25519
id_ed25519.pub
# Editors / OS
.DS_Store
.idea/
.vscode/
*.swp
*.swo
# Logs
*.log
notes.md

823
README.md
View File

@@ -1,93 +1,800 @@
# opentofu-standart-vm
# OpenTofu + Proxmox 9.1.7 + Ubuntu Cloud-Init
---
## Обучение
Если хотите разобраться глубже, как это всё работает:
- Курс по OpenTofu / Terraform:
https://stepik.org/a/238385
---
## Контакты
Автор: Юрий Анфиногенов
Telegram: https://t.me/uanfinogenov
---
Подробное описание проекта для создания и управления виртуальными машинами в Proxmox через OpenTofu.
Этот вариант документации рассчитан на структуру, где используется одна рабочая директория `cluster/` и один общий модуль `modules/node`.
## Getting started
Документ описывает:
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
* установку OpenTofu
* оффлайн установку провайдеров
* настройку `~/.tofurc`
* подготовку Ubuntu cloud image / template в Proxmox
* настройку доступа к Proxmox API
* структуру проекта
* работу `cloud-init`
* fallback логику `default.yml`
* использование разных `cloud-init` файлов для разных VM
* работу VLAN
* практические замечания по отладке
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
---
## Add your files
## Совместимость
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
- [ ] [Add files using the command line](https://docs.gitlab.com/topics/git/add_files/#add-files-to-a-git-repository) or push an existing Git repository with the following command:
Проверено в окружении:
```
cd existing_repo
git remote add origin http://git.yur811.ru/devtools/opentofu-standart-vm.git
git branch -M main
git push -uf origin main
* Proxmox VE 9.1.7
* Ubuntu cloud image (noble / 24.04)
* OpenTofu
Если в вашей среде используются другие версии Proxmox, Ubuntu image или провайдеров, поведение может отличаться.
---
## Важно:
- перед использованием необходимо проверить и при необходимости изменить переменные под свою среду
- чаще всего отличаются:
- datastore (local, local-lvm, ssd и т.д.)
- image_datastore и путь к образу
- сетевой bridge (например vmbr0)
- network_base и gateway
- VLAN (если используется)
- значения в примере не универсальны и зависят от конкретного Proxmox окружения
---
## Идея проекта
OpenTofu отвечает за инфраструктуру:
* создание VM
* сеть
* диски
* cloud-init disk
* загрузку cloud-init user-data в Proxmox
Cloud-init отвечает за конфигурацию ОС внутри VM:
* пользователи
* SSH ключи
* hostname
* пакеты
* systemd сервисы
* базовая bootstrap-настройка
Это ключевой принцип проекта:
* OpenTofu не должен подробно конфигурировать ОС
* cloud-init не должен управлять инфраструктурой Proxmox
---
## Структура проекта
Ожидаемая структура:
```text
.
├── README.md
├── terraform.tfvars.example
├── cluster/
│ ├── cloud-config/
│ │ └── default.yml
│ ├── locals.tf
│ ├── main.tf
│ ├── outputs.tf
│ ├── providers.tf
│ ├── terraform.tfvars
│ └── variables.tf
└── modules/
└── node/
├── cloud-config/
│ └── default.yml
├── main.tf
├── outputs.tf
└── variables.tf
```
## Integrate with your tools
Где:
- [ ] [Set up project integrations](http://git.yur811.ru/devtools/opentofu-standart-vm/-/settings/integrations)
* `cluster/` - рабочее окружение
* `cluster/cloud-config/` - project-specific cloud-init файлы
* `modules/node/` - общий модуль для VM
* `modules/node/cloud-config/default.yml` - модульный fallback cloud-init
* `terraform.tfvars.example` - пример переменных без секретов
## Collaborate with your team
---
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
- [ ] [Set auto-merge](https://docs.gitlab.com/user/project/merge_requests/auto_merge/)
## Как работает cloud-init в этом проекте
## Test and Deploy
Для каждой VM модуль выбирает cloud-init файл по следующему правилу:
Use the built-in continuous integration in GitLab.
1. если у ноды задан параметр `cloudinit`, модуль ищет файл в `cluster/cloud-config/<имя файла>`
2. если параметр `cloudinit` не задан, модуль пытается использовать `cluster/cloud-config/default.yml`
3. если файла в `cluster/cloud-config/` нет, используется fallback из модуля: `modules/node/cloud-config/default.yml`
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/)
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
Это даёт три уровня конфигурации:
***
* per-VM cloud-init
* project default cloud-init
* module fallback cloud-init
# Editing this README
---
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
## Пример логики выбора cloud-init
## Suggestions for a good README
Пример в `locals.tf`:
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
```hcl
locals {
nodes = {
worker-1 = {
index = 1
cpu = 2
memory = 2048
disk = 20
datastore = "ssd2"
ip = "192.168.20.101"
cloudinit = "worker.yml"
}
## Name
Choose a self-explaining name for your project.
worker-2 = {
index = 2
cpu = 2
memory = 2048
disk = 20
datastore = "ssd2"
ip = "192.168.20.102"
cloudinit = "worker.yml"
}
## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
master-1 = {
index = 3
cpu = 4
memory = 4096
disk = 40
datastore = "ssd2"
ip = "192.168.20.110"
cloudinit = "master.yml"
}
# ip:
# - опциональный параметр
# - если НЕ задан → вычисляется автоматически
# - формула:
# ${network_base}.${cluster_ip_start + index}
test = {
index = 5
cpu = 1
memory = 1024
disk = 10
datastore = "ssd2"
# ip = "192.168.20.130" -> ip не задан, вычисляется автоматически
# cloudinit не задан -> будет использован default.yml
}
}
}
```
## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
Поведение:
## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
* `worker-1`, `worker-2` -> `cluster/cloud-config/worker.yml`
* `master-1` -> `cluster/cloud-config/master.yml`
* `test` -> `cluster/cloud-config/default.yml`, а если его нет -> `modules/node/cloud-config/default.yml`
## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
---
## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
## Установка OpenTofu
## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
```bash
sudo apt-get update
sudo apt-get install -y apt-transport-https ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.
curl -fsSL https://get.opentofu.org/opentofu.gpg \
| sudo tee /etc/apt/keyrings/opentofu.gpg >/dev/null
## Contributing
State if you are open to contributions and what your requirements are for accepting them.
curl -fsSL https://packages.opentofu.org/opentofu/tofu/gpgkey \
| sudo gpg --no-tty --batch --dearmor -o /etc/apt/keyrings/opentofu-repo.gpg >/dev/null
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
sudo chmod a+r /etc/apt/keyrings/opentofu.gpg /etc/apt/keyrings/opentofu-repo.gpg
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
echo "deb [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
## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.
sudo apt-get update
sudo apt-get install -y tofu
```
## License
For open source projects, say how it is licensed.
Проверка:
## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
```bash
tofu version
```
---
## Установка Golang
Для части оффлайн-провайдеров требуется Go.
Установите Golang удобным для вас способом.
Проверка:
```bash
go version
```
---
## Настройка оффлайн-провайдеров
Если OpenTofu должен работать без выхода в интернет, провайдеры нужно положить в локальное зеркало:
```text
~/.terraform.d/plugins/
```
Создание каталогов:
```bash
mkdir -p ~/.terraform.d/plugins/registry.opentofu.org/bpg/proxmox
mkdir -p ~/.terraform.d/plugins/registry.opentofu.org/hashicorp/{local,random,tls}
```
Ожидаемая структура:
```text
/home/user/.terraform.d/
└── plugins
└── registry.opentofu.org
├── bpg
│ └── proxmox
│ ├── 0.101.1
│ │ └── linux_amd64
│ │ └── terraform-provider-proxmox_v0.101.1
│ ├── 0.86.0
│ │ └── linux_amd64
│ │ └── terraform-provider-proxmox_v0.86.0
│ └── 0.87.0
│ └── linux_amd64
│ └── terraform-provider-proxmox_v0.87.0
└── hashicorp
├── local
│ └── 2.6.1
│ └── linux_amd64
│ └── terraform-provider-local_v2.6.1
├── random
│ └── 3.7.2
│ └── linux_amd64
│ └── terraform-provider-random_v3.7.2
└── tls
└── 4.1.0
└── linux_amd64
└── terraform-provider-tls_v4.1.0
```
### Провайдер bpg/proxmox
Релизы:
```text
https://github.com/bpg/terraform-provider-proxmox/releases
```
Пример для версии `0.86.0`:
```bash
wget https://github.com/bpg/terraform-provider-proxmox/releases/download/v0.86.0/terraform-provider-proxmox_0.86.0_linux_amd64.zip
mkdir -p ~/.terraform.d/plugins/registry.opentofu.org/bpg/proxmox/0.86.0/linux_amd64
unzip terraform-provider-proxmox_0.86.0_linux_amd64.zip -d /tmp
mv /tmp/terraform-provider-proxmox_v0.86.0 \
~/.terraform.d/plugins/registry.opentofu.org/bpg/proxmox/0.86.0/linux_amd64/
chmod +x ~/.terraform.d/plugins/registry.opentofu.org/bpg/proxmox/0.86.0/linux_amd64/terraform-provider-proxmox_v0.86.0
```
Пояснение:
* `bpg/proxmox` распространяется как готовый бинарник
* его не нужно собирать через `go build`
* достаточно скачать ZIP, распаковать и положить бинарник в локальное зеркало
### Провайдер hashicorp/local
```bash
wget https://github.com/hashicorp/terraform-provider-local/archive/refs/tags/v2.6.1.zip
unzip v2.6.1.zip
cd terraform-provider-local-2.6.1/
go build -o terraform-provider-local .
mkdir -p ~/.terraform.d/plugins/registry.opentofu.org/hashicorp/local/2.6.1/linux_amd64
mv terraform-provider-local \
~/.terraform.d/plugins/registry.opentofu.org/hashicorp/local/2.6.1/linux_amd64/terraform-provider-local_v2.6.1
```
### Провайдер hashicorp/random
```bash
wget https://github.com/hashicorp/terraform-provider-random/archive/refs/tags/v3.7.2.zip
unzip v3.7.2.zip
cd terraform-provider-random-3.7.2/
go build -o terraform-provider-random .
mkdir -p ~/.terraform.d/plugins/registry.opentofu.org/hashicorp/random/3.7.2/linux_amd64
mv terraform-provider-random \
~/.terraform.d/plugins/registry.opentofu.org/hashicorp/random/3.7.2/linux_amd64/terraform-provider-random_v3.7.2
```
### Провайдер hashicorp/tls
```bash
wget https://github.com/hashicorp/terraform-provider-tls/archive/refs/tags/v4.1.0.zip
unzip v4.1.0.zip
cd terraform-provider-tls-4.1.0/
go build -o terraform-provider-tls .
mkdir -p ~/.terraform.d/plugins/registry.opentofu.org/hashicorp/tls/4.1.0/linux_amd64
mv terraform-provider-tls \
~/.terraform.d/plugins/registry.opentofu.org/hashicorp/tls/4.1.0/linux_amd64/terraform-provider-tls_v4.1.0
```
---
## Настройка `~/.tofurc`
Файл `~/.tofurc` говорит OpenTofu использовать только локальные провайдеры и не пытаться скачивать их из интернета.
Пример:
```hcl
provider_installation {
filesystem_mirror {
path = "/home/$USER/.terraform.d/plugins"
include = [
"registry.opentofu.org/bpg/proxmox",
"registry.opentofu.org/hashicorp/local",
"registry.opentofu.org/hashicorp/random",
"registry.opentofu.org/hashicorp/tls"
]
}
direct {
exclude = [
"registry.opentofu.org/bpg/proxmox",
"registry.opentofu.org/hashicorp/local",
"registry.opentofu.org/hashicorp/random",
"registry.opentofu.org/hashicorp/tls"
]
}
}
```
Проверка:
```bash
tofu init -reconfigure
```
Ожидаемое поведение:
* OpenTofu берёт провайдеры из local filesystem mirror
* в интернет за ними не выходит
---
## Доступ к Proxmox API
Минимальные права для API token:
* `Datastore.AllocateSpace`
* `VM.Allocate`
* `VM.Audit`
* `VM.Config.*`
Пример отдельного файла с credentials:
```bash
vim ~/.pve-creds
```
```bash
export PVE_TOKEN_ID="root@pam!tofu"
export PVE_TOKEN_SECRET="YOUR_SECRET"
export PVE_HOST="192.168.22.5"
```
Загрузка в shell:
```bash
set -a
source ~/.pve-creds
set +a
```
Проверка:
```bash
curl -k -H "Authorization: PVEAPIToken=${PVE_TOKEN_ID}=${PVE_TOKEN_SECRET}" \
https://$PVE_HOST:8006/api2/json/version
```
В example также показано как можно хранить ключи в файле. Если будете использовать, не забудьте убедится, что файл с переменными в .gitignore.
---
## Подготовка Ubuntu Cloud-Init template в Proxmox
Нужен именно cloud image и корректно подготовленный template.
### Скачать cloud image
```bash
wget https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img -O ubuntu.img
```
### Создать VM под template
```bash
qm create 9001 --name ubuntu-template --memory 2048 --cores 2 --net0 virtio,bridge=vmbr0
qm importdisk 9001 ubuntu.img local-lvm
qm set 9001 --scsihw virtio-scsi-pci --scsi0 local-lvm:vm-9001-disk-0
qm set 9001 --ide2 local-lvm:cloudinit
qm set 9001 --boot c --bootdisk scsi0
qm set 9001 --serial0 socket --vga serial0
```
### Превратить VM в template
```bash
qm template 9001
```
Важно:
* нужен cloud image
* нужен cloud-init disk
* template должен быть корректно подготовлен
Если этого нет, cloud-init может не применяться внутри VM.
---
## Переменные проекта
Пример `terraform.tfvars.example`:
```hcl
# Proxmox API endpoint (формат: https://host:port/api2/json)
proxmox_endpoint = "https://<IP>:<PORT>/api2/json"
# ID API token (формат: user@realm!token_name)
proxmox_token_id = "terraform@ve!user"
# Secret API token
proxmox_token_secret = "<PROXMOX_TOKEN>"
# Стартовый VMID
worker_vmid_start = 1000
# Дефолтные ресурсы worker VM
worker_cpu = 2
worker_memory = 2048
worker_disk = 20
worker_datastore = "ssd2"
# Datastore, где лежит cloud image
image_datastore = "local"
# Путь к образу
image_file = "import/ubuntu-24.qcow2"
# Сеть
cluster_gateway = "192.168.20.1"
network_base = "192.168.20"
network_cidr = "24"
cluster_ip_start = 0
# Datastore для дополнительных дисков
data_datastore = "data1"
```
Практика:
* реальный `terraform.tfvars` не коммитить
* в git хранить только `terraform.tfvars.example`
---
## Пример описания `nodes`
```hcl
# nodes — описание виртуальных машин
#
# vlan_id:
# - опциональный параметр
# - если НЕ указан → VM будет в обычной сети (untagged, vmbr0)
# - если указан → VM попадет в соответствующий VLAN
#
# cloudinit:
# - опциональный параметр
# - указывает имя cloud-init файла для конкретной VM
# - файл должен находиться в cluster/cloud-config/<имя>.yml
# - если НЕ указан → используется default.yml
# - если файл НЕ найден в cluster/cloud-config → используется fallback из модуля
variable "nodes" {
type = map(object({
index = number
cpu = number
memory = number
disk = number
datastore = string
ip_offset = optional(number)
ip = optional(string)
vmid = optional(number)
vlan_id = optional(number)
data_disk = optional(number)
cloudinit = optional(string)
}))
}
```
---
## VLAN
Параметр:
```hcl
vlan_id = 20
```
Поведение:
* если `vlan_id` не указан -> обычная сеть без тегирования
* если `vlan_id` указан -> VM подключается в соответствующий VLAN
Пример:
```hcl
nodes = {
worker-1 = {
index = 1
cpu = 2
memory = 2048
disk = 20
datastore = "ssd2"
ip = "192.168.20.101"
vlan_id = 20
cloudinit = "worker.yml"
}
}
```
---
## Модульный `default.yml`
Модульный `default.yml` нужен как безопасный fallback, если проект не передал отдельный cloud-init файл.
Пример содержимого:
```yaml
#cloud-config
timezone: Europe/Moscow
users:
- default
- name: ubuntu
groups: [sudo]
shell: /bin/bash
lock_passwd: true
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
ssh_authorized_keys:
- ${ssh_key}
ssh_pwauth: false
package_update: true
packages:
- qemu-guest-agent
write_files:
- path: /etc/motd
content: |
Managed by OpenTofu
runcmd:
- systemctl enable --now qemu-guest-agent
- systemctl disable --now packagekit
- systemctl disable --now ModemManager
- systemctl disable --now multipathd
- hostnamectl set-hostname ${hostname}
final_message: "cloud-init finished"
```
Почему именно так:
* пользователь `ubuntu` доступен по SSH ключу
* пароль отключён
* `qemu-guest-agent` включается для работы с Proxmox
* отключаются лишние сервисы, которые обычно не нужны на серверной VM:
* `packagekit` - GUI / D-Bus пакетный сервис
* `ModemManager` - менеджер USB/LTE модемов
* `multipathd` - multipath storage daemon
---
## Пример project-specific cloud-init
Например `cluster/cloud-config/vpn.yml`:
```yaml
#cloud-config
timezone: Europe/Moscow
users:
- name: user
groups: [sudo]
shell: /bin/bash
lock_passwd: true
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
ssh_authorized_keys:
- ${ssh_key}
ssh_pwauth: false
package_update: true
packages:
- qemu-guest-agent
- wireguard
- curl
write_files:
- path: /etc/motd
content: |
VPN node managed by OpenTofu
runcmd:
- systemctl enable --now qemu-guest-agent
- systemctl disable --now packagekit
- systemctl disable --now ModemManager
- systemctl disable --now multipathd
- hostnamectl set-hostname ${hostname}
final_message: "cloud-init finished"
```
---
## Быстрый запуск
Создать рабочий файл переменных:
```bash
cp ../terraform.tfvars.example terraform.tfvars
```
Инициализация:
```bash
cd cluster
tofu init
```
Проверка плана:
```bash
tofu plan
```
Применение:
```bash
tofu apply
```
---
## `.gitignore`
Минимально рекомендуется игнорировать:
```gitignore
**/.terraform/
**/.terraform.lock.hcl
**/*.tfstate
**/*.tfstate.*
**/*.tfvars
!**/*.tfvars.example
.env
.env.*
*.pem
*.key
*.log
.vscode/
.idea/
```
---
## Что хранить в git, а что нет
Хранить в git можно:
* `README.md`
* `terraform.tfvars.example`
* `cluster/*.tf`
* `cluster/cloud-config/*.yml`
* `modules/node/*.tf`
* `modules/node/cloud-config/default.yml`
Не хранить в git:
* `terraform.tfvars`
* `*.tfstate`
* `.terraform/`
* токены Proxmox
* приватные SSH ключи
* `.env`
---
## Отладка
Если cloud-init "не применился", проверять в первую очередь:
1. первая строка файла должна быть `#cloud-config`
2. cloud-init файл должен реально попасть в Proxmox
3. VM должна быть создана заново, если проверяется first boot логика
4. cloud image и template должны быть подготовлены корректно
5. если включён `qemu-guest-agent`, `tofu apply` может висеть в ожидании `guest-ping`, если агент не стартовал
Типовые причины проблем:
* отсутствует `#cloud-config`
* битый YAML
* неправильный override файл
* не тот cloud image / плохо подготовленный template
* ожидание `qemu-guest-agent` при неуспешном cloud-init
---
## Практические замечания
* если нельзя гарантировать интернет, лучше сразу готовить оффлайн провайдеры
* если не нужен парольный вход, лучше использовать только SSH ключи
* если нужен проектный `default.yml`, его можно положить в `cluster/cloud-config/default.yml`
* если нужен отдельный cloud-init для группы машин, можно указывать один и тот же файл в `cloudinit` у нескольких VM
* если нужна полная изоляция, можно задавать отдельный cloud-init на каждую VM
---
## Что обязательно должно быть
Если хотите воспроизвести этот проект в своей среде, вам в любом случае понадобятся:
* Ubuntu cloud image
* корректно подготовленный Proxmox template с cloud-init disk
* установленные OpenTofu провайдеры
Если этих компонентов нет, проект в полном виде воспроизвести нельзя.

View File

@@ -0,0 +1,34 @@
#cloud-config
# Создать passwd hash: openssl passwd -6
timezone: Europe/Moscow
users:
- name: ubuntu
groups: [sudo]
shell: /bin/bash
lock_passwd: false
passwd: "$6$M8xzsYT0lwWjlcdH$HJRhw0rBb2WowCbqg03/WvMUlte0j5SLs9tAVCxCukEqWAt7XG0ceWYc5GRUBmRxFRTbcEDrdCVTXu5jIxN2f0"
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
ssh_authorized_keys:
- ${ssh_key}
package_update: true
packages:
- qemu-guest-agent
runcmd:
- systemctl enable --now qemu-guest-agent
- hostnamectl set-hostname ${hostname}
- systemctl disable --now packagekit
- systemctl disable --now ModemManager
- systemctl disable --now multipathd
write_files:
- path: /etc/motd
content: |
Managed by OpenTofu
final_message: "cloud-init finished"

View File

@@ -0,0 +1,34 @@
#cloud-config
# Создать passwd hash: openssl passwd -6
timezone: Europe/Moscow
users:
- name: iurii
groups: [sudo]
shell: /bin/bash
lock_passwd: false
passwd: "$6$Zc8nwvtw0Kns5.sD$FpQ4aBSeGogefqjM4we4U5QQd4YBtC98tuG3rR4j9ZmbtC1kyFf2sY/IodYW3wG.U81aEntlZrtOTOqw3ZcOc0"
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
ssh_authorized_keys:
- ${ssh_key}
package_update: true
packages:
- qemu-guest-agent
runcmd:
- systemctl enable --now qemu-guest-agent
- hostnamectl set-hostname ${hostname}
- systemctl disable --now packagekit
- systemctl disable --now ModemManager
- systemctl disable --now multipathd
write_files:
- path: /etc/motd
content: |
Managed by OpenTofu
final_message: "cloud-init finished"

View File

@@ -0,0 +1,34 @@
#cloud-config
# Создать passwd hash: openssl passwd -6
timezone: Europe/Moscow
users:
- name: iurii
groups: [sudo]
shell: /bin/bash
lock_passwd: false
passwd: "$6$Zc8nwvtw0Kns5.sD$FpQ4aBSeGogefqjM4we4U5QQd4YBtC98tuG3rR4j9ZmbtC1kyFf2sY/IodYW3wG.U81aEntlZrtOTOqw3ZcOc0"
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
ssh_authorized_keys:
- ${ssh_key}
package_update: true
packages:
- qemu-guest-agent
runcmd:
- systemctl enable --now qemu-guest-agent
- hostnamectl set-hostname ${hostname}
- systemctl disable --now packagekit
- systemctl disable --now ModemManager
- systemctl disable --now multipathd
write_files:
- path: /etc/motd
content: |
Managed by OpenTofu
final_message: "cloud-init finished"

52
cluster/locals.tf Normal file
View File

@@ -0,0 +1,52 @@
# nodes — описание виртуальных машин
#
# vlan_id:
# - опциональный параметр
# - если НЕ указан → VM будет в обычной сети (untagged, vmbr0)
# - если указан → VM попадет в соответствующий VLAN (например 20 → 192.168.20.0/24)
# cloudinit:
# - опциональный параметр
# - указывает имя cloud-init файла для конкретной VM
# - файл должен находиться в root: cloud-config/<имя>.yml
# - если НЕ указан → используется "default.yml"
# - если файл НЕ найден в root → используется fallback из модуля (modules/node/cloud-config/default.yml)
#
# пример:
# - cloudinit = "worker.yml" → будет использован cloud-config/worker.yml
# - cloudinit не задан → будет использован default.yml
locals {
nodes = {
k8s-master-1 = {
cloudinit = "master.yml"
index = 1
cpu = var.worker_cpu
memory = 8192
disk = var.worker_disk
datastore = var.worker_datastore
ip_offset = 10
vlan_id = 20
}
k8s-worker-1 = {
cloudinit = "worker.yml"
index = 2
cpu = 4
memory = 8192
disk = var.worker_disk
datastore = var.worker_datastore
ip_offset = 20
vlan_id = 20
}
k8s-worker-2 = {
cloudinit = "worker.yml"
index = 3
cpu = 4
memory = 8192
disk = var.worker_disk
datastore = var.worker_datastore
ip_offset = 20
vlan_id = 20
}
}
}

27
cluster/main.tf Normal file
View File

@@ -0,0 +1,27 @@
data "local_file" "ssh_key" {
filename = pathexpand("~/.ssh/id_rsa.pub")
}
module "cluster" {
source = "../modules/node"
nodes = local.nodes
ssh_key = trimspace(data.local_file.ssh_key.content)
cluster_ip_start = var.cluster_ip_start
worker_vmid_start = var.worker_vmid_start
cloudinit_datastore = var.cloudinit_datastore
proxmox_node = var.proxmox_node
node_bridge = var.node_bridge
image_datastore = var.image_datastore
image_file = var.image_file
disk_interface = var.disk_interface
network_base = var.network_base
network_cidr = var.network_cidr
cluster_gateway = var.cluster_gateway
data_datastore = var.data_datastore
}

13
cluster/outputs.tf Normal file
View File

@@ -0,0 +1,13 @@
output "nodes_ipv4" {
value = module.cluster.ip_addresses
}
output "nodes_hostnames" {
value = module.cluster.hostnames
}
output "nodes_vmid" {
value = module.cluster.vmids
}

20
cluster/providers.tf Normal file
View File

@@ -0,0 +1,20 @@
terraform {
required_providers {
proxmox = {
source = "registry.opentofu.org/bpg/proxmox"
version = "= 0.101.1"
}
}
}
provider "proxmox" {
endpoint = var.proxmox_endpoint
api_token = "${var.proxmox_token_id}=${var.proxmox_token_secret}"
insecure = true
ssh {
username = "root"
agent = true
private_key = file(pathexpand("~/.ssh/id_rsa"))
}
}

86
cluster/variables.tf Normal file
View File

@@ -0,0 +1,86 @@
variable "proxmox_endpoint" {}
variable "proxmox_token_id" {}
variable "proxmox_token_secret" {}
variable "proxmox_node" {
type = string
default = "ve"
}
variable "cloudinit_datastore" {
type = string
default = "local"
}
variable "disk_interface" {
type = string
default = "virtio0"
}
variable "image_datastore" {
type = string
default = "local"
}
variable "image_file" {
type = string
default = "import/ubuntu-24.qcow2"
}
variable "worker_cpu" {
default = 2
}
variable "worker_memory" {
default = 2048
}
variable "worker_disk" {
default = 20
}
variable "network_base" {
default = "192.168.22"
}
variable "network_cidr" {
default = "24"
}
variable "cluster_gateway" {
default = "192.168.22.1"
}
variable "cluster_ip_start" {
default = 10
}
variable "worker_ip_offset" {
default = 5
}
variable "node_bridge" {
default = "vmbr0"
}
variable "worker_datastore" {
type = string
default = "local-lvm"
}
variable "worker_vmid_start" {
type = number
default = 3000
}
variable "data_datastore" {
type = string
default = "data1"
description = "Datastore for VM data disks"
}

View File

@@ -0,0 +1,34 @@
#cloud-config
# Создать passwd hash: openssl passwd -6
timezone: Europe/Moscow
users:
- name: ubuntu
groups: [sudo]
shell: /bin/bash
lock_passwd: false
passwd: "$6$M8xzsYT0lwWjlcdH$HJRhw0rBb2WowCbqg03/WvMUlte0j5SLs9tAVCxCukEqWAt7XG0ceWYc5GRUBmRxFRTbcEDrdCVTXu5jIxN2f0"
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
ssh_authorized_keys:
- ${ssh_key}
package_update: true
packages:
- qemu-guest-agent
runcmd:
- systemctl enable --now qemu-guest-agent
- hostnamectl set-hostname ${hostname}
- systemctl disable --now packagekit
- systemctl disable --now ModemManager
- systemctl disable --now multipathd
write_files:
- path: /etc/motd
content: |
Managed by OpenTofu
final_message: "cloud-init finished"

34
infra/cloud-config/vm.yml Normal file
View File

@@ -0,0 +1,34 @@
#cloud-config
# vpn
timezone: Europe/Moscow
users:
- name: iurii
groups: [sudo]
shell: /bin/bash
lock_passwd: false
passwd: "$6$Zc8nwvtw0Kns5.sD$FpQ4aBSeGogefqjM4we4U5QQd4YBtC98tuG3rR4j9ZmbtC1kyFf2sY/IodYW3wG.U81aEntlZrtOTOqw3ZcOc0"
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
ssh_authorized_keys:
- ${ssh_key}
package_update: true
packages:
- qemu-guest-agent
runcmd:
- systemctl enable --now qemu-guest-agent
- hostnamectl set-hostname ${hostname}
- systemctl disable --now packagekit
- systemctl disable --now ModemManager
- systemctl disable --now multipathd
write_files:
- path: /etc/motd
content: |
Managed by OpenTofu
final_message: "cloud-init finished"

31
infra/locals.tf Normal file
View File

@@ -0,0 +1,31 @@
# nodes — описание виртуальных машин
#
# vlan_id:
# - опциональный параметр
# - если НЕ указан → VM будет в обычной сети (untagged, vmbr0)
# - если указан → VM попадет в соответствующий VLAN (например 20 → 192.168.20.0/24)
# cloudinit:
# - опциональный параметр
# - указывает имя cloud-init файла для конкретной VM
# - файл должен находиться в root: cloud-config/<имя>.yml
# - если НЕ указан → используется "default.yml"
# - если файл НЕ найден в root → используется fallback из модуля (modules/node/cloud-config/default.yml)
#
# пример:
# - cloudinit = "worker.yml" → будет использован cloud-config/worker.yml
# - cloudinit не задан → будет использован default.yml
locals {
nodes = {
sing-box-tun= {
cloudinit = "vm.yml"
index = 2
cpu = 1
memory = 1024
disk = 7
datastore = "local-lvm"
ip_offset = 0
ip = "192.168.22.50"
}
}
}

27
infra/main.tf Normal file
View File

@@ -0,0 +1,27 @@
data "local_file" "ssh_key" {
filename = pathexpand("~/.ssh/id_rsa.pub")
}
module "cluster" {
source = "../modules/node"
nodes = local.nodes
ssh_key = trimspace(data.local_file.ssh_key.content)
cluster_ip_start = var.cluster_ip_start
worker_vmid_start = var.worker_vmid_start
cloudinit_datastore = var.cloudinit_datastore
proxmox_node = var.proxmox_node
node_bridge = var.node_bridge
image_datastore = var.image_datastore
image_file = var.image_file
disk_interface = var.disk_interface
network_base = var.network_base
network_cidr = var.network_cidr
cluster_gateway = var.cluster_gateway
data_datastore = var.data_datastore
}

13
infra/outputs.tf Normal file
View File

@@ -0,0 +1,13 @@
output "nodes_ipv4" {
value = module.cluster.ip_addresses
}
output "nodes_hostnames" {
value = module.cluster.hostnames
}
output "nodes_vmid" {
value = module.cluster.vmids
}

20
infra/providers.tf Normal file
View File

@@ -0,0 +1,20 @@
terraform {
required_providers {
proxmox = {
source = "registry.opentofu.org/bpg/proxmox"
version = "= 0.101.1"
}
}
}
provider "proxmox" {
endpoint = var.proxmox_endpoint
api_token = "${var.proxmox_token_id}=${var.proxmox_token_secret}"
insecure = true
ssh {
username = "root"
agent = true
private_key = file(pathexpand("~/.ssh/id_rsa"))
}
}

86
infra/variables.tf Normal file
View File

@@ -0,0 +1,86 @@
variable "proxmox_endpoint" {}
variable "proxmox_token_id" {}
variable "proxmox_token_secret" {}
variable "proxmox_node" {
type = string
default = "ve"
}
variable "cloudinit_datastore" {
type = string
default = "local"
}
variable "disk_interface" {
type = string
default = "virtio0"
}
variable "image_datastore" {
type = string
default = "local"
}
variable "image_file" {
type = string
default = "import/ubuntu-24.qcow2"
}
variable "worker_cpu" {
default = 2
}
variable "worker_memory" {
default = 2048
}
variable "worker_disk" {
default = 20
}
variable "network_base" {
default = "192.168.22"
}
variable "network_cidr" {
default = "24"
}
variable "cluster_gateway" {
default = "192.168.22.1"
}
variable "cluster_ip_start" {
default = 10
}
variable "worker_ip_offset" {
default = 5
}
variable "node_bridge" {
default = "vmbr0"
}
variable "worker_datastore" {
type = string
default = "local-lvm"
}
variable "worker_vmid_start" {
type = number
default = 3000
}
variable "data_datastore" {
type = string
default = "data1"
description = "Datastore for VM data disks"
}

View File

@@ -0,0 +1,34 @@
#cloud-config
# Создать passwd hash: openssl passwd -6
timezone: Europe/Moscow
users:
- name: ubuntu
groups: [sudo]
shell: /bin/bash
lock_passwd: false
passwd: "$6$M8xzsYT0lwWjlcdH$HJRhw0rBb2WowCbqg03/WvMUlte0j5SLs9tAVCxCukEqWAt7XG0ceWYc5GRUBmRxFRTbcEDrdCVTXu5jIxN2f0"
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
ssh_authorized_keys:
- ${ssh_key}
package_update: true
packages:
- qemu-guest-agent
runcmd:
- systemctl enable --now qemu-guest-agent
- hostnamectl set-hostname ${hostname}
- systemctl disable --now packagekit
- systemctl disable --now ModemManager
- systemctl disable --now multipathd
write_files:
- path: /etc/motd
content: |
Managed by OpenTofu
final_message: "cloud-init finished"

34
lab/cloud-config/vm.yml Normal file
View File

@@ -0,0 +1,34 @@
#cloud-config
# vpn
timezone: Europe/Moscow
users:
- name: iurii
groups: [sudo]
shell: /bin/bash
lock_passwd: false
passwd: "$6$Zc8nwvtw0Kns5.sD$FpQ4aBSeGogefqjM4we4U5QQd4YBtC98tuG3rR4j9ZmbtC1kyFf2sY/IodYW3wG.U81aEntlZrtOTOqw3ZcOc0"
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
ssh_authorized_keys:
- ${ssh_key}
package_update: true
packages:
- qemu-guest-agent
runcmd:
- systemctl enable --now qemu-guest-agent
- hostnamectl set-hostname ${hostname}
- systemctl disable --now packagekit
- systemctl disable --now ModemManager
- systemctl disable --now multipathd
write_files:
- path: /etc/motd
content: |
Managed by OpenTofu
final_message: "cloud-init finished"

31
lab/locals.tf Normal file
View File

@@ -0,0 +1,31 @@
# nodes — описание виртуальных машин
#
# vlan_id:
# - опциональный параметр
# - если НЕ указан → VM будет в обычной сети (untagged, vmbr0)
# - если указан → VM попадет в соответствующий VLAN (например 20 → 192.168.20.0/24)
# cloudinit:
# - опциональный параметр
# - указывает имя cloud-init файла для конкретной VM
# - файл должен находиться в root: cloud-config/<имя>.yml
# - если НЕ указан → используется "default.yml"
# - если файл НЕ найден в root → используется fallback из модуля (modules/node/cloud-config/default.yml)
#
# пример:
# - cloudinit = "worker.yml" → будет использован cloud-config/worker.yml
# - cloudinit не задан → будет использован default.yml
locals {
nodes = {
vm1 = {
cloudinit = "vm.yml"
index = 1
cpu = 1
memory = 1024
disk = var.worker_disk
datastore = var.worker_datastore
ip_offset = 0
vlan_id = 20
}
}
}

27
lab/main.tf Normal file
View File

@@ -0,0 +1,27 @@
data "local_file" "ssh_key" {
filename = pathexpand("~/.ssh/id_rsa.pub")
}
module "cluster" {
source = "../modules/node"
nodes = local.nodes
ssh_key = trimspace(data.local_file.ssh_key.content)
cluster_ip_start = var.cluster_ip_start
worker_vmid_start = var.worker_vmid_start
cloudinit_datastore = var.cloudinit_datastore
proxmox_node = var.proxmox_node
node_bridge = var.node_bridge
image_datastore = var.image_datastore
image_file = var.image_file
disk_interface = var.disk_interface
network_base = var.network_base
network_cidr = var.network_cidr
cluster_gateway = var.cluster_gateway
data_datastore = var.data_datastore
}

13
lab/outputs.tf Normal file
View File

@@ -0,0 +1,13 @@
output "nodes_ipv4" {
value = module.cluster.ip_addresses
}
output "nodes_hostnames" {
value = module.cluster.hostnames
}
output "nodes_vmid" {
value = module.cluster.vmids
}

20
lab/providers.tf Normal file
View File

@@ -0,0 +1,20 @@
terraform {
required_providers {
proxmox = {
source = "registry.opentofu.org/bpg/proxmox"
version = "= 0.101.1"
}
}
}
provider "proxmox" {
endpoint = var.proxmox_endpoint
api_token = "${var.proxmox_token_id}=${var.proxmox_token_secret}"
insecure = true
ssh {
username = "root"
agent = true
private_key = file(pathexpand("~/.ssh/id_rsa"))
}
}

86
lab/variables.tf Normal file
View File

@@ -0,0 +1,86 @@
variable "proxmox_endpoint" {}
variable "proxmox_token_id" {}
variable "proxmox_token_secret" {}
variable "proxmox_node" {
type = string
default = "ve"
}
variable "cloudinit_datastore" {
type = string
default = "local"
}
variable "disk_interface" {
type = string
default = "virtio0"
}
variable "image_datastore" {
type = string
default = "local"
}
variable "image_file" {
type = string
default = "import/ubuntu-24.qcow2"
}
variable "worker_cpu" {
default = 2
}
variable "worker_memory" {
default = 2048
}
variable "worker_disk" {
default = 20
}
variable "network_base" {
default = "192.168.22"
}
variable "network_cidr" {
default = "24"
}
variable "cluster_gateway" {
default = "192.168.22.1"
}
variable "cluster_ip_start" {
default = 10
}
variable "worker_ip_offset" {
default = 5
}
variable "node_bridge" {
default = "vmbr0"
}
variable "worker_datastore" {
type = string
default = "local-lvm"
}
variable "worker_vmid_start" {
type = number
default = 3000
}
variable "data_datastore" {
type = string
default = "data1"
description = "Datastore for VM data disks"
}

View File

@@ -0,0 +1,35 @@
#cloud-config
timezone: Europe/Moscow
users:
- default
- name: ubuntu
groups: [sudo]
shell: /bin/bash
lock_passwd: true
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
ssh_authorized_keys:
- ${ssh_key}
ssh_pwauth: false
package_update: true
packages:
- qemu-guest-agent
runcmd:
- systemctl enable --now qemu-guest-agent
- hostnamectl set-hostname ${hostname}
- systemctl disable --now packagekit
- systemctl disable --now ModemManager
- systemctl disable --now multipathd
write_files:
- path: /etc/motd
content: |
Managed by OpenTofu
final_message: "cloud-init finished"

29
modules/node/locals.tf Normal file
View File

@@ -0,0 +1,29 @@
locals {
ssh_public_key = var.ssh_key
nodes = {
for name, node in var.nodes :
name => node
}
ip_map = {
for name, node in local.nodes :
name => coalesce(
lookup(node, "ip", null),
"${var.network_base}.${var.cluster_ip_start + node.ip_offset + node.index}"
)
}
vmid_map = {
for name, node in local.nodes :
name => coalesce(
lookup(node, "vmid", null),
var.worker_vmid_start + node.index
)
}
hostname_map = {
for name, node in local.nodes :
name => "${name}"
}
}

92
modules/node/main.tf Normal file
View File

@@ -0,0 +1,92 @@
terraform {
required_providers {
proxmox = {
source = "registry.opentofu.org/bpg/proxmox"
}
}
}
resource "proxmox_virtual_environment_file" "cloudinit" {
for_each = local.nodes
content_type = "snippets"
datastore_id = var.cloudinit_datastore
node_name = var.proxmox_node
source_raw {
file_name = "${each.key}.yml"
data = fileexists("${path.root}/cloud-config/${coalesce(each.value.cloudinit, "default.yml")}") ? templatefile(
"${path.root}/cloud-config/${coalesce(each.value.cloudinit, "default.yml")}",
{
hostname = local.hostname_map[each.key]
ssh_key = local.ssh_public_key
}
) : templatefile(
"${path.module}/cloud-config/default.yml",
{
hostname = local.hostname_map[each.key]
ssh_key = local.ssh_public_key
}
)
}
}
resource "proxmox_virtual_environment_vm" "nodes" {
for_each = local.nodes
name = local.hostname_map[each.key]
node_name = var.proxmox_node
# allow vmid override
vm_id = coalesce(
lookup(local.nodes[each.key], "vmid", null),
local.vmid_map[each.key]
)
agent {
enabled = true
}
cpu {
cores = each.value.cpu
}
memory {
dedicated = each.value.memory
}
network_device {
bridge = var.node_bridge
# vlan_id = try(each.value.vlan_id, null)
vlan_id = each.value.vlan_id
}
disk {
datastore_id = each.value.datastore
import_from = "${var.image_datastore}:${var.image_file}"
interface = var.disk_interface
size = each.value.disk
}
dynamic "disk" {
for_each = try([each.value.data_disk], [])
content {
datastore_id = var.data_datastore
interface = "scsi1"
size = disk.value
}
}
initialization {
datastore_id = each.value.datastore
user_data_file_id = proxmox_virtual_environment_file.cloudinit[each.key].id
ip_config {
ipv4 {
address = "${local.ip_map[each.key]}/${var.network_cidr}"
gateway = var.cluster_gateway
}
}
}
}

17
modules/node/outputs.tf Normal file
View File

@@ -0,0 +1,17 @@
output "ip_addresses" {
description = "IP addresses of all created nodes"
value = {
for name, _ in local.nodes :
name => proxmox_virtual_environment_vm.nodes[name].ipv4_addresses[1][0]
}
}
output "hostnames" {
description = "Hostnames of all created nodes"
value = local.hostname_map
}
output "vmids" {
description = "VMIDs of all created nodes"
value = local.vmid_map
}

71
modules/node/variables.tf Normal file
View File

@@ -0,0 +1,71 @@
variable "ssh_key" {
type = string
}
variable "nodes" {
type = map(object({
# role = string
index = number
cpu = number
memory = number
disk = number
datastore = string
ip_offset = optional(number)
ip = optional(string)
vmid = optional(number)
vlan_id = optional(number)
data_disk = optional(number)
cloudinit = optional(string)
}))
}
variable "cluster_ip_start" {
type = number
}
variable "worker_vmid_start" {
type = number
}
variable "cloudinit_datastore" {
type = string
}
variable "proxmox_node" {
type = string
}
variable "node_bridge" {
type = string
}
variable "image_datastore" {
type = string
}
variable "image_file" {
type = string
}
variable "disk_interface" {
type = string
}
variable "network_base" {
type = string
}
variable "network_cidr" {
type = number
}
variable "cluster_gateway" {
type = string
}
variable "data_datastore" {
type = string
description = "Datastore for data disk"
}

292
start.md Normal file
View File

@@ -0,0 +1,292 @@
# OpenTofu + Proxmox + Ubuntu Cloud-Init
### Полностью воспроизводимая установка (оффлайн провайдеры)
Этот документ позволяет развернуть инфраструктурную машину (Ubuntu), установить OpenTofu, настроить оффлайн-провайдеры, подготовить шаблон Ubuntu Cloud-Init в Proxmox и выполнить первый `tofu plan`.
## 2. Установка OpenTofu
```bash
sudo apt-get update
sudo apt-get 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 -o /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" \
| sudo tee /etc/apt/sources.list.d/opentofu.list >/dev/null
```
```sh
sudo apt-get update
sudo apt-get install -y tofu
```
Проверка:
```bash
tofu version
```
## Установка Golang
Для компиляции должен быть установлен [Golang](https://go.dev/learn/)
---
## 3. Настройка оффлайн‑провайдеров
Для работы OpenTofu без доступа к интернету провайдеры можно разместить локально в каталоге `~/.terraform.d/plugins/`.
### 3.1. Подготовка каталогов
Создаём директории для оффлайн-провайдеров:
```bash
mkdir -p ~/.terraform.d/plugins/registry.opentofu.org/bpg/proxmox
mkdir -p ~/.terraform.d/plugins/registry.opentofu.org/hashicorp/{local,random,tls}
```
Ожидаемая структура после установки:
```sh
/home/user/.terraform.d/
└── plugins
└── registry.opentofu.org
├── bpg
│ └── proxmox
│ ├── 0.86.0
│ │ └── linux_amd64
│ │ └── terraform-provider-proxmox_v0.86.0
│ ├── 0.87.0
│ │ └── linux_amd64
│ │ └── terraform-provider-proxmox_v0.87.0
│ └── terraform-provider-proxmox_0.86.0_linux_amd64.zip
└── hashicorp
├── local
│ └── 2.6.1
│ └── linux_amd64
│ └── terraform-provider-local_v2.6.1
├── random
│ └── 3.7.2
│ └── linux_amd64
│ └── terraform-provider-random_v3.7.2
└── tls
└── 4.1.0
└── linux_amd64
└── terraform-provider-tls_v4.1.0
```
---
### Провайдер **bpg/proxmox**
Релизы: [https://github.com/bpg/terraform-provider-proxmox/releases](https://github.com/bpg/terraform-provider-proxmox/releases)
Пример загрузки версии `0.86.0`:
>>Пояснение: провайдер bpg/proxmox распространяется уже в собранном виде. Его не нужно компилировать - достаточно скачать ZIPфайл с готовым бинарником распаковать и положить его в каталог зеркала. Это отличается от провайдеров hashicorp/local, random, tls, которые распространяются как исходники и требуют сборки через go build.
Загрузить:
```bash
wget https://github.com/bpg/terraform-provider-proxmox/releases/download/v0.86.0/terraform-provider-proxmox_0.86.0_linux_amd64.zip
```
Создать директорию:
```sh
mkdir -p ~/.terraform.d/plugins/registry.opentofu.org/bpg/proxmox/0.86.0/linux_amd64
```
Распаковать и переместить:
```sh
unzip terraform-provider-proxmox_0.86.0_linux_amd64.zip -d /tmp
mv /tmp/terraform-provider-proxmox_v0.86.0 \
~/.terraform.d/plugins/registry.opentofu.org/bpg/proxmox/0.86.0/linux_amd64/
# Делаем исполняемым
chmod +x ~/.terraform.d/plugins/registry.opentofu.org/bpg/proxmox/0.86.0/linux_amd64/terraform-provider-proxmox_v0.86.0
```
---
### 📌 Правильное размещение бинарников Hashicorp после компиляции
После выполнения go build бинарники провайдеров необходимо разместить в строгой структуре, которую ожидает OpenTofu:
```sh
~/.terraform.d/plugins/registry.opentofu.org/hashicorp/<provider>/<version>/linux_amd64/
```
### Провайдер **hashicorp/local**
Релизы: [https://github.com/hashicorp/terraform-provider-local/releases/tag/v2.6.1](https://github.com/hashicorp/terraform-provider-local/releases/tag/v2.6.1)
Загрузка и компиляция:
```bash
wget https://github.com/hashicorp/terraform-provider-local/archive/refs/tags/v2.6.1.zip
unzip v2.6.1.zip
cd terraform-provider-local-2.6.1/
go build -o terraform-provider-local .
```
Переместить бинарник:
```bash
mkdir -p ~/.terraform.d/plugins/registry.opentofu.org/hashicorp/local/2.6.1/linux_amd64
mv terraform-provider-local \
~/.terraform.d/plugins/registry.opentofu.org/hashicorp/local/2.6.1/linux_amd64/terraform-provider-local_v2.6.1
```
---
### Провайдер **hashicorp/random**
Релизы: [https://github.com/hashicorp/terraform-provider-random/releases/tag/v3.7.2](https://github.com/hashicorp/terraform-provider-random/releases/tag/v3.7.2)
```bash
wget https://github.com/hashicorp/terraform-provider-random/archive/refs/tags/v3.7.2.zip
unzip v3.7.2.zip
cd terraform-provider-random-3.7.2/
go build -o terraform-provider-random .
mkdir -p ~/.terraform.d/plugins/registry.opentofu.org/hashicorp/random/3.7.2/linux_amd64
mv terraform-provider-random \
~/.terraform.d/plugins/registry.opentofu.org/hashicorp/random/3.7.2/linux_amd64/terraform-provider-random_v3.7.2
```
---
### Провайдер **hashicorp/tls**
Релизы: [https://github.com/hashicorp/terraform-provider-tls/releases](https://github.com/hashicorp/terraform-provider-tls/releases)
```bash
wget https://github.com/hashicorp/terraform-provider-tls/archive/refs/tags/v4.1.0.zip
unzip v4.1.0.zip
cd terraform-provider-tls-4.1.0/
go build -o terraform-provider-tls .
```
Бинарный файл переместить в каталог `tls`:
```bash
mkdir -p ~/.terraform.d/plugins/registry.opentofu.org/hashicorp/tls/4.1.0/linux_amd64
mv terraform-provider-tls \
~/.terraform.d/plugins/registry.opentofu.org/hashicorp/tls/4.1.0/linux_amd64/terraform-provider-tls_v4.1.0
```
Исходники после сборки можно удалить.
---
## 4. Настройка `~/.tofurc`
Файл конфигурации указывает OpenTofu использовать локальные провайдеры и не пытаться тянуть их из сети.
**$USER** Обязательно замените, на ваш реальный путь.
```bash
provider_installation {
filesystem_mirror {
path = "/home/$USER/.terraform.d/plugins"
include = [
"registry.opentofu.org/bpg/proxmox",
"registry.opentofu.org/hashicorp/local",
"registry.opentofu.org/hashicorp/random",
"registry.opentofu.org/hashicorp/tls"
]
}
direct {
exclude = [
"registry.opentofu.org/bpg/proxmox",
"registry.opentofu.org/hashicorp/local",
"registry.opentofu.org/hashicorp/random",
"registry.opentofu.org/hashicorp/tls"
]
}
}
```
---
Проверка:
```bash
tofu init -reconfigure
```
Ожидаемое: `Installing provider ... from local filesystem`.
---
## 5. Создание API-токена Proxmox
Минимальные права:
* Datastore.AllocateSpace
* VM.Allocate
* VM.Audit
* VM.Config.*
---
## 6. Конфиг токена на Ubuntu
Файл:
```bash
vim ~/.pve-creds
```
Содержимое:
```
export PVE_TOKEN_ID="root@pam!tofu"
export PVE_TOKEN_SECRET="YOUR_SECRET"
export PVE_HOST="192.168.22.5"
```
Загрузить:
```bash
set -a
source ~/.pve-creds
set +a
```
Проверка:
```bash
curl -k -H "Authorization: PVEAPIToken=${PVE_TOKEN_ID}=${PVE_TOKEN_SECRET}" \
https://$PVE_HOST:8006/api2/json/version
```
---
## 7. Подготовка шаблона Ubuntu Cloud-Init в Proxmox
### 7.1. Скачать cloud image
```bash
wget https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img -O ubuntu.img
```
### 7.2. Создать VM под шаблон
```bash
qm create 9001 --name ubuntu-template --memory 2048 --cores 2 --net0 virtio,bridge=vmbr0
qm importdisk 9001 ubuntu.img local-lvm
qm set 9001 --scsihw virtio-scsi-pci --scsi0 local-lvm:vm-9001-disk-0
qm set 9001 --ide2 local-lvm:cloudinit
qm set 9001 --boot c --bootdisk scsi0
qm set 9001 --serial0 socket --vga serial0
```
### 7.3. Превратить VM в шаблон
```bash
qm template 9001
```
---

292
stat2.md Normal file
View File

@@ -0,0 +1,292 @@
# OpenTofu + Proxmox + Ubuntu Cloud-Init
### Полностью воспроизводимая установка (оффлайн провайдеры)
Этот документ позволяет развернуть инфраструктурную машину (Ubuntu), установить OpenTofu, настроить оффлайн-провайдеры, подготовить шаблон Ubuntu Cloud-Init в Proxmox и выполнить первый `tofu plan`.
## 2. Установка OpenTofu
```bash
sudo apt-get update
sudo apt-get 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 -o /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" \
| sudo tee /etc/apt/sources.list.d/opentofu.list >/dev/null
```
```sh
sudo apt-get update
sudo apt-get install -y tofu
```
Проверка:
```bash
tofu version
```
## Установка Golang
Для компиляции должен быть установлен [Golang](https://go.dev/learn/)
---
## 3. Настройка оффлайн‑провайдеров
Для работы OpenTofu без доступа к интернету провайдеры можно разместить локально в каталоге `~/.terraform.d/plugins/`.
### 3.1. Подготовка каталогов
Создаём директории для оффлайн-провайдеров:
```bash
mkdir -p ~/.terraform.d/plugins/registry.opentofu.org/bpg/proxmox
mkdir -p ~/.terraform.d/plugins/registry.opentofu.org/hashicorp/{local,random,tls}
```
Ожидаемая структура после установки:
```sh
/home/user/.terraform.d/
└── plugins
└── registry.opentofu.org
├── bpg
│ └── proxmox
│ ├── 0.86.0
│ │ └── linux_amd64
│ │ └── terraform-provider-proxmox_v0.86.0
│ ├── 0.87.0
│ │ └── linux_amd64
│ │ └── terraform-provider-proxmox_v0.87.0
│ └── terraform-provider-proxmox_0.86.0_linux_amd64.zip
└── hashicorp
├── local
│ └── 2.6.1
│ └── linux_amd64
│ └── terraform-provider-local_v2.6.1
├── random
│ └── 3.7.2
│ └── linux_amd64
│ └── terraform-provider-random_v3.7.2
└── tls
└── 4.1.0
└── linux_amd64
└── terraform-provider-tls_v4.1.0
```
---
### Провайдер **bpg/proxmox**
Релизы: [https://github.com/bpg/terraform-provider-proxmox/releases](https://github.com/bpg/terraform-provider-proxmox/releases)
Пример загрузки версии `0.86.0`:
>>Пояснение: провайдер bpg/proxmox распространяется уже в собранном виде. Его не нужно компилировать - достаточно скачать ZIPфайл с готовым бинарником распаковать и положить его в каталог зеркала. Это отличается от провайдеров hashicorp/local, random, tls, которые распространяются как исходники и требуют сборки через go build.
Загрузить:
```bash
wget https://github.com/bpg/terraform-provider-proxmox/releases/download/v0.86.0/terraform-provider-proxmox_0.86.0_linux_amd64.zip
```
Создать директорию:
```sh
mkdir -p ~/.terraform.d/plugins/registry.opentofu.org/bpg/proxmox/0.86.0/linux_amd64
```
Распаковать и переместить:
```sh
unzip terraform-provider-proxmox_0.86.0_linux_amd64.zip -d /tmp
mv /tmp/terraform-provider-proxmox_v0.86.0 \
~/.terraform.d/plugins/registry.opentofu.org/bpg/proxmox/0.86.0/linux_amd64/
# Делаем исполняемым
chmod +x ~/.terraform.d/plugins/registry.opentofu.org/bpg/proxmox/0.86.0/linux_amd64/terraform-provider-proxmox_v0.86.0
```
---
### 📌 Правильное размещение бинарников Hashicorp после компиляции
После выполнения go build бинарники провайдеров необходимо разместить в строгой структуре, которую ожидает OpenTofu:
```sh
~/.terraform.d/plugins/registry.opentofu.org/hashicorp/<provider>/<version>/linux_amd64/
```
### Провайдер **hashicorp/local**
Релизы: [https://github.com/hashicorp/terraform-provider-local/releases/tag/v2.6.1](https://github.com/hashicorp/terraform-provider-local/releases/tag/v2.6.1)
Загрузка и компиляция:
```bash
wget https://github.com/hashicorp/terraform-provider-local/archive/refs/tags/v2.6.1.zip
unzip v2.6.1.zip
cd terraform-provider-local-2.6.1/
go build -o terraform-provider-local .
```
Переместить бинарник:
```bash
mkdir -p ~/.terraform.d/plugins/registry.opentofu.org/hashicorp/local/2.6.1/linux_amd64
mv terraform-provider-local \
~/.terraform.d/plugins/registry.opentofu.org/hashicorp/local/2.6.1/linux_amd64/terraform-provider-local_v2.6.1
```
---
### Провайдер **hashicorp/random**
Релизы: [https://github.com/hashicorp/terraform-provider-random/releases/tag/v3.7.2](https://github.com/hashicorp/terraform-provider-random/releases/tag/v3.7.2)
```bash
wget https://github.com/hashicorp/terraform-provider-random/archive/refs/tags/v3.7.2.zip
unzip v3.7.2.zip
cd terraform-provider-random-3.7.2/
go build -o terraform-provider-random .
mkdir -p ~/.terraform.d/plugins/registry.opentofu.org/hashicorp/random/3.7.2/linux_amd64
mv terraform-provider-random \
~/.terraform.d/plugins/registry.opentofu.org/hashicorp/random/3.7.2/linux_amd64/terraform-provider-random_v3.7.2
```
---
### Провайдер **hashicorp/tls**
Релизы: [https://github.com/hashicorp/terraform-provider-tls/releases](https://github.com/hashicorp/terraform-provider-tls/releases)
```bash
wget https://github.com/hashicorp/terraform-provider-tls/archive/refs/tags/v4.1.0.zip
unzip v4.1.0.zip
cd terraform-provider-tls-4.1.0/
go build -o terraform-provider-tls .
```
Бинарный файл переместить в каталог `tls`:
```bash
mkdir -p ~/.terraform.d/plugins/registry.opentofu.org/hashicorp/tls/4.1.0/linux_amd64
mv terraform-provider-tls \
~/.terraform.d/plugins/registry.opentofu.org/hashicorp/tls/4.1.0/linux_amd64/terraform-provider-tls_v4.1.0
```
Исходники после сборки можно удалить.
---
## 4. Настройка `~/.tofurc`
Файл конфигурации указывает OpenTofu использовать локальные провайдеры и не пытаться тянуть их из сети.
**$USER** Обязательно замените, на ваш реальный путь.
```bash
provider_installation {
filesystem_mirror {
path = "/home/$USER/.terraform.d/plugins"
include = [
"registry.opentofu.org/bpg/proxmox",
"registry.opentofu.org/hashicorp/local",
"registry.opentofu.org/hashicorp/random",
"registry.opentofu.org/hashicorp/tls"
]
}
direct {
exclude = [
"registry.opentofu.org/bpg/proxmox",
"registry.opentofu.org/hashicorp/local",
"registry.opentofu.org/hashicorp/random",
"registry.opentofu.org/hashicorp/tls"
]
}
}
```
---
Проверка:
```bash
tofu init -reconfigure
```
Ожидаемое: `Installing provider ... from local filesystem`.
---
## 5. Создание API-токена Proxmox
Минимальные права:
* Datastore.AllocateSpace
* VM.Allocate
* VM.Audit
* VM.Config.*
---
## 6. Конфиг токена на Ubuntu
Файл:
```bash
vim ~/.pve-creds
```
Содержимое:
```
export PVE_TOKEN_ID="root@pam!tofu"
export PVE_TOKEN_SECRET="YOUR_SECRET"
export PVE_HOST="192.168.22.5"
```
Загрузить:
```bash
set -a
source ~/.pve-creds
set +a
```
Проверка:
```bash
curl -k -H "Authorization: PVEAPIToken=${PVE_TOKEN_ID}=${PVE_TOKEN_SECRET}" \
https://$PVE_HOST:8006/api2/json/version
```
---
## 7. Подготовка шаблона Ubuntu Cloud-Init в Proxmox
### 7.1. Скачать cloud image
```bash
wget https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img -O ubuntu.img
```
### 7.2. Создать VM под шаблон
```bash
qm create 9001 --name ubuntu-template --memory 2048 --cores 2 --net0 virtio,bridge=vmbr0
qm importdisk 9001 ubuntu.img local-lvm
qm set 9001 --scsihw virtio-scsi-pci --scsi0 local-lvm:vm-9001-disk-0
qm set 9001 --ide2 local-lvm:cloudinit
qm set 9001 --boot c --bootdisk scsi0
qm set 9001 --serial0 socket --vga serial0
```
### 7.3. Превратить VM в шаблон
```bash
qm template 9001
```
---

42
terraform.tfvars.example Normal file
View File

@@ -0,0 +1,42 @@
# Proxmox API endpoint (формат обязателен: https://host:port/api2/json)
proxmox_endpoint = "https://<IP>:<PORT>/api2/json"
# ID API токена (формат: user@realm!token_name)
proxmox_token_id = "terraform@ve!user"
# Секрет API токена (не коммитить в git)
proxmox_token_secret = "<PROXMOX_TOKEN>"
# Стартовый VMID для worker-нод (будут увеличиваться автоматически)
worker_vmid_start = 1000
# Дефолтные ресурсы для worker VM
worker_cpu = 1 # количество CPU ядер
worker_memory = 1024 # RAM в MB
worker_disk = 15 # размер системного диска в GB
# Datastore для системного диска VM
worker_datastore = "local-lvm"
# Datastore, где лежит cloud image (qcow2)
image_datastore = "local"
# Путь к образу внутри datastore (без "local:" если провайдер сам добавляет)
image_file = "import/ubuntu-24.qcow2"
# Сетевые настройки
cluster_gateway = "192.168.0.1" # шлюз сети
network_base = "192.168.0" # база сети (используется для генерации IP)
network_cidr = "24" # маска сети (лучше как number, не string)
# Смещение IP относительно base (например 0 → .0, 10 → .10)
cluster_ip_start = 0
# Datastore для дополнительных дисков (data disk)
data_datastore = "data1"