feat: add minimal PROD VM example

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Repinoid
2026-10-17 21:04:00 +03:00
co-authored by Copilot
parent 9731ba672f
commit 88242af900
12 changed files with 660 additions and 5 deletions
+127
View File
@@ -0,0 +1,127 @@
# Минимальная цепочка для одной ВМ (PROD)
Пример создаёт одну ВМ и только необходимые для неё услуги:
```text
организация (создана вручную в ЛК)
-> vDC
-> Edge с routed-сетью
-> выделение IP организации (минимум 3)
-> SNAT
-> vApp
-> одна ВМ
```
Штурвал, базы данных и приложения в примере отсутствуют. Он основан на успешно
запущенной в PROD конфигурации `VM_TF_Docs`, а не на `fullpipe_chain`.
Провайдер зафиксирован на PROD-версии `1.5.0`.
## Важное условие: организация создаётся вручную
До запуска создайте организацию **«Организация в Cloud Director»** через ЛК. Terraform
не создаёт и не удаляет организацию: `nubes_vc_vdc` получает её имя или UUID через
`organization`.
В ЛК проверьте, что организации доступны выбранные resource pool, Provider VDC,
storage policy и IP space. Значения из примера (`msk1`, `Intel Cascade Lake 3.1`,
`SSD SDS`) подтверждены для использованного PROD-пула; для другого пула их нужно
заменить на значения из ЛК.
## Почему выделяется 3 IP
Для проверенной цепочки с одной ВМ минимальное значение `org_vip_count` — **3**.
При значении 2 API сообщил, что свободных IP — 0, а для операции требуется ещё 1;
при значении 3 развёртывание ВМ запустилось. Поэтому в этом примере не уменьшайте
квоту до 2, даже если из дополнительных потребителей IP оставлена только одна ВМ.
`org_vip_count` задаёт количество IP в выбранном `ip_space_name` на организации —
это не количество адресов, которое сама ВМ получит. Точное внутреннее распределение
резервов платформой эта конфигурация не показывает. Если другие услуги используют
адреса из того же IP space или там уже есть занятые адреса, квоту нужно увеличить.
Список услуг со статусами `running`/`suspended` сам по себе не показывает, какие IP
заняты.
В конфигурации VM установлен `same_snat = true`: согласно PROD-схеме при этом для ВМ
создаётся собственный внешний IP. SNAT на Edge также задаётся отдельным ресурсом,
как в проверенной цепочке.
## Подготовка параметров
1. Скопируйте шаблоны:
```bash
cp terraform.tfvars.example terraform.tfvars
cp secrets.auto.tfvars.example secrets.auto.tfvars
```
2. В `secrets.auto.tfvars` замените заглушку на PROD API token из ЛК → Профиль →
Токены → «Технический». Не присылайте и не коммитьте этот файл.
3. В `terraform.tfvars` замените:
- `organization` на имя или UUID вручную созданной организации;
- `ip_space_name` на точное имя IP space из ЛК;
- `vm_user_public_key` на содержимое вашего `~/.ssh/id_ed25519.pub`;
- `vm_access_ip_list` на доверенный публичный IP/CIDR для входящего доступа.
4. Убедитесь, что имена vDC, Edge, vApp и ВМ уникальны в организации. Не оставляйте
значения-заглушки `REPLACE_WITH_...`.
5. Если выбранный resource pool отличается от проверенного PROD-пула, замените
`vdc_network_provider`, `vdc_provider_vdc`, `vdc_storage_config`, DNS и routed subnet
на значения, допустимые в ЛК. Routed subnet `/24` не должен пересекаться с другой сетью.
Все переменные и значения по умолчанию прокомментированы в `variables.tf` и
`terraform.tfvars.example`. Параметры ресурсов сверены с PROD YAML-схемами.
## Запуск
Команды выполняет владелец инфраструктуры; `apply` в этом примере не запускался:
```bash
terraform init
terraform validate
terraform plan
```
Проверьте план: ожидаются шесть услуг — vDC, Edge, выделение IP на организации,
SNAT, vApp и ВМ. Убедитесь, что в плане нет неожиданных изменений уже существующих
услуг. Только после проверки владелец запускает:
```bash
terraform apply
```
После завершения:
```bash
terraform output vm_state_flat
```
`externalConnect` — внешний адрес ВМ, `internalConnect` — адрес в routed-сети,
`fqdn` — DNS-имя, если платформа его вернула.
## Параметры ВМ и операции
- Образ `Ubuntu_22-20G` включает базовый диск 20 GB; `vm_disk = 128` задаёт
**дополнительный** диск.
- `vm_access_port_list` разрешает только заданные порты. `vm_access_ip_list` ограничивает
адреса-источники; не оставляйте доступ SSH открытым для `0.0.0.0/0`, если это не требуется.
- `git_revision` оставлен `null` при создании. Новое значение этого маркера запускает
redeploy при последующем Update.
- `reconcile_trigger` также оставлен `null` при создании. Новое значение запускает
reconcile при Update; при Create reconcile не выполняется.
- В проверке PROD от 17.10.2026 файл-маркер в `/home/ubuntu` отсутствовал после redeploy.
Считайте локальные данные ВМ потенциально теряемыми при redeploy и храните важные данные
вне системного диска.
## Состав файлов
| Файл | Назначение |
|---|---|
| `versions.tf` | Версия Terraform и PROD-провайдера |
| `provider.tf` | Подключение к PROD API |
| `variables.tf` | Типы, значения по умолчанию и пояснения к входным переменным |
| `vdc.tf` | Виртуальный датацентр |
| `edge.tf` | Edge и routed-сеть |
| `network.tf` | Квота IP организации и SNAT |
| `vm.tf` | vApp и одна ВМ |
| `outputs.tf` | Идентификаторы услуг и состояние ВМ |
| `terraform.tfvars.example` | Несекретные значения и параметры примера |
| `secrets.auto.tfvars.example` | Шаблон локального файла для API token |
+35
View File
@@ -0,0 +1,35 @@
# Create the perimeter gateway and its routed network inside the VDC.
resource "nubes_vc_nsxt" "edge" {
# Name shown for this Edge service in the personal account.
resource_name = var.nsxt_resource_name
# Attach the gateway directly to a VDC, not to a VDC group.
vdc_type = "vdc"
# UUID of the VDC created above; this reference also orders creation.
vdc_uid = nubes_vc_vdc.vdc.id
# AVI/ALB is not needed for this standalone VM example.
need_enable_avi = false
# Only required when AVI/ALB is enabled; keep it null when ALB is disabled.
virtual_services_count = null
# Routed subnet and DNS servers assigned to VMs behind this Edge.
routed_net_configuration = {
# Non-overlapping /24 range for the routed network.
ip_addr_pool = var.nsxt_ip_addr_pool
# Primary DNS server configured in VMs.
main_dns = var.nsxt_main_dns
# Secondary DNS server configured in VMs.
second_dns = var.nsxt_second_dns
}
# Allow provider destroy handling to delete the Edge when dependencies permit.
keep_on_destroy = false
# Reuse an existing same-name Edge rather than failing during Create.
adopt_existing_on_create = true
}
+35
View File
@@ -0,0 +1,35 @@
# Allocate organization-level public IP capacity after the VDC and Edge exist.
resource "nubes_vc_org_ip_allocation" "org_ip" {
# Existing organization used by the VDC.
organization = var.organization
# Configure the selected IP space with the tested minimum count of 3.
# The API expects count as a string inside the vIPConfigure JSON array.
vip_configure = jsonencode([
{
name = var.ip_space_name
count = tostring(var.org_vip_count)
}
])
# Keep the organization allocation when this Terraform resource is destroyed.
keep_on_destroy = true
# PROD exposes the organization's usable IP spaces after the VDC and Edge exist.
depends_on = [nubes_vc_nsxt.edge]
}
# Enable the tested SNAT configuration on the Edge using the same IP space.
resource "nubes_vc_nsxt_snat" "snat" {
# UUID of the Edge created above.
nsxt_uid = nubes_vc_nsxt.edge.id
# IP space allocated to the organization in the resource above.
ip_space_name = var.ip_space_name
# Leave SNAT enabled when this Terraform resource is destroyed.
keep_on_destroy = true
# The IP space must be allocated before SNAT is configured.
depends_on = [nubes_vc_org_ip_allocation.org_ip]
}
+38
View File
@@ -0,0 +1,38 @@
# UUID of the created VDC.
output "vdc_id" {
description = "UUID of the VDC."
value = nubes_vc_vdc.vdc.id
}
# UUID of the created Edge.
output "edge_id" {
description = "UUID of the Edge gateway."
value = nubes_vc_nsxt.edge.id
}
# UUID of the created vApp.
output "vapp_id" {
description = "UUID of the vApp."
value = nubes_vapp.vapp.id
}
# UUID of the created VM.
output "vm_id" {
description = "UUID of the VM."
value = nubes_vc_vm_v3.vm.id
}
# VM IP addresses and connection data returned by the API.
output "vm_state_flat" {
description = "Flat VM state; inspect externalConnect, internalConnect, and fqdn."
value = nubes_vc_vm_v3.vm.state_out_flat
}
# The configured organization allocation count, not the count of addresses in use.
output "org_ip_allocation" {
description = "Configured IP-space name and organization allocation count."
value = {
ip_space_name = var.ip_space_name
count = var.org_vip_count
}
}
+6
View File
@@ -0,0 +1,6 @@
# Connect to the Nubes PROD API. The token is stored separately in
# secrets.auto.tfvars and must never be committed.
provider "nubes" {
api_token = var.api_token
api_endpoint = var.api_endpoint
}
@@ -0,0 +1,3 @@
# Copy to secrets.auto.tfvars and insert a Nubes PROD API token.
# secrets.auto.tfvars is ignored by git; never commit the real token.
api_token = "REPLACE_WITH_NUBES_PROD_API_TOKEN"
+74
View File
@@ -0,0 +1,74 @@
# Copy this file to terraform.tfvars and replace organization-specific values.
# Put the API token only in secrets.auto.tfvars, never in this file.
# Existing organization created manually in the Nubes personal account.
organization = "REPLACE_WITH_ORGANIZATION_NAME_OR_UUID"
# Exact IP space visible to this organization in the personal account.
ip_space_name = "internet-no-antiddos-v1"
# Tested minimum for this VM-only PROD chain. Two IPs left no free address
# when the VM requested its external IP; three allowed the deployment to start.
org_vip_count = 3
# API endpoint for the PROD stand.
api_endpoint = "https://lk-api-gateway.ngcloud.ru/api/v1/svc"
# Unique VDC service name.
vdc_resource_name = "vm-minimal-vdc"
# Values from the tested PROD resource pool. Replace with values available
# to the manually created organization if its resource pool differs.
vdc_network_provider = "msk1"
vdc_provider_vdc = "Intel Cascade Lake 3.1"
# VDC capacity: vCPU, guaranteed share (0/50/80), and RAM in GB.
vdc_cpu_allocated = 8
vdc_cpu_guaranteed = 0
vdc_mem_allocated = 200
# JSON array of storage policies available to the selected Provider VDC.
# Confirm the policy name and size in the personal account before applying.
vdc_storage_config = "[{\"name\":\"SSD SDS\",\"size\":\"2000\"}]"
# Unique Edge service name.
nsxt_resource_name = "vm-minimal-edge"
# Non-overlapping /24 routed subnet plus primary and secondary VM DNS servers.
nsxt_ip_addr_pool = "10.10.102.0/24"
nsxt_main_dns = "81.22.46.22"
nsxt_second_dns = "185.247.187.77"
# Unique vApp service name and vApp name.
vapp_resource_name = "vm-minimal-vapp"
vapp_name = "vm-minimal-vapp"
# Unique VM service name and hostname/IP-set name.
vm_resource_name = "vm-minimal"
vm_name = "vm-minimal"
# Tested VM sizing: Ubuntu base image (20 GB) plus 128 GB additional disk.
vm_image = "Ubuntu_22-20G"
vm_cpu = 2
vm_ram = 4
vm_disk = 128
# Linux account name and your OpenSSH PUBLIC key.
vm_user_login = "ubuntu"
vm_user_public_key = "ssh-ed25519 REPLACE_WITH_YOUR_PUBLIC_KEY"
# Inbound ports. Narrow the source CIDR below to trusted addresses.
vm_access_port_list = [
{ port = "22", type = "tcp" },
{ port = "80", type = "tcp" }
]
vm_access_ip_list = ["REPLACE_WITH_YOUR_PUBLIC_IP/32"]
# The tested VM gets its own external IP; do not change without checking
# the consequences for inbound access.
vm_same_snat = true
# Keep both triggers null for initial creation. To request an operation later,
# set its marker to a new, unique value.
git_revision = null
reconcile_trigger = null
+203
View File
@@ -0,0 +1,203 @@
# =============================================================================
# Inputs for the standalone PROD VM example.
# Values that depend on the organization or resource pool are in
# terraform.tfvars.example; the API token is in secrets.auto.tfvars.example.
# =============================================================================
variable "api_token" {
type = string
sensitive = true
description = "Nubes PROD API token from the Technical token section in the personal account."
}
variable "api_endpoint" {
type = string
default = "https://lk-api-gateway.ngcloud.ru/api/v1/svc"
description = "Nubes PROD API endpoint. Change only when targeting another stand."
}
variable "organization" {
type = string
description = "Name or UUID of the Cloud Director organization created manually in the personal account."
}
variable "ip_space_name" {
type = string
description = "Exact IP space name available to this organization; used for the allocation, Edge SNAT, and VM."
}
variable "org_vip_count" {
type = number
default = 3
description = "Organization IP allocation count. Three is the tested minimum for this VM-only deployment; increase if other services need IPs from this space."
validation {
condition = var.org_vip_count >= 3 && floor(var.org_vip_count) == var.org_vip_count
error_message = "This tested VM-only example requires at least 3 IPs; use a whole number and increase it for additional consumers."
}
}
variable "vdc_resource_name" {
type = string
default = "vm-minimal-vdc"
description = "Unique service name for the virtual data center."
}
variable "vdc_network_provider" {
type = string
default = "msk1"
description = "Network provider selected for this organization in the VDC creation form."
}
variable "vdc_provider_vdc" {
type = string
default = "Intel Cascade Lake 3.1"
description = "Provider VDC selected in the personal account; it must be compatible with the network provider."
}
variable "vdc_cpu_allocated" {
type = number
default = 8
description = "Number of vCPUs allocated to the VDC."
}
variable "vdc_cpu_guaranteed" {
type = number
default = 0
description = "Guaranteed vCPU percentage. PROD schema allows 0, 50, or 80; this value is fixed at VDC creation."
validation {
condition = contains([0, 50, 80], var.vdc_cpu_guaranteed)
error_message = "vdc_cpu_guaranteed must be 0, 50, or 80."
}
}
variable "vdc_mem_allocated" {
type = number
default = 200
description = "Memory allocated to the VDC, in GB."
}
variable "vdc_storage_config" {
type = string
default = "[{\"name\":\"SSD SDS\",\"size\":\"2000\"}]"
description = "JSON array of storage policies available to the selected Provider VDC; size is in GB."
}
variable "nsxt_resource_name" {
type = string
default = "vm-minimal-edge"
description = "Unique service name for the perimeter Edge gateway."
}
variable "nsxt_ip_addr_pool" {
type = string
default = "10.10.102.0/24"
description = "Unused /24 routed-network range for this Edge. Verify that it does not overlap another network."
}
variable "nsxt_main_dns" {
type = string
default = "81.22.46.22"
description = "Primary DNS server assigned to VMs on the routed network."
}
variable "nsxt_second_dns" {
type = string
default = "185.247.187.77"
description = "Secondary DNS server assigned to VMs on the routed network."
}
variable "vapp_resource_name" {
type = string
default = "vm-minimal-vapp"
description = "Unique service name for the vApp in the personal account."
}
variable "vapp_name" {
type = string
default = "vm-minimal-vapp"
description = "Unique vApp name matching the platform pattern; it is used when forming the VM DNS name."
}
variable "vm_resource_name" {
type = string
default = "vm-minimal"
description = "Unique service name for the virtual machine in the personal account."
}
variable "vm_name" {
type = string
default = "vm-minimal"
description = "VM name matching the platform pattern; it also identifies the VM IP set."
}
variable "vm_image" {
type = string
default = "Ubuntu_22-20G"
description = "Available PROD VM image. The base disk size is part of the image and cannot be changed here."
}
variable "vm_cpu" {
type = number
default = 2
description = "Number of VM vCPUs (PROD schema range: 1-64)."
}
variable "vm_ram" {
type = number
default = 4
description = "VM memory in GB (PROD schema range: 1-256)."
}
variable "vm_disk" {
type = number
default = 128
description = "Additional VM disk size in GB. The image supplies the separate base disk."
}
variable "vm_user_login" {
type = string
default = "ubuntu"
description = "Linux account created for SSH access; it is set during VM creation."
}
variable "vm_user_public_key" {
type = string
description = "Your OpenSSH public key, such as the contents of ~/.ssh/id_ed25519.pub. Never put a private key here."
}
variable "vm_access_port_list" {
type = list(object({
port = string
type = string
}))
default = [
{ port = "22", type = "tcp" },
{ port = "80", type = "tcp" }
]
description = "Inbound VM ports and protocols. Restrict this list to the services the VM actually needs."
}
variable "vm_access_ip_list" {
type = list(string)
description = "Source IP addresses or CIDRs allowed to reach the VM. Set this to trusted addresses; avoid 0.0.0.0/0 where possible."
}
variable "vm_same_snat" {
type = bool
default = true
description = "When true, the VM is published through its own external IP created during deployment."
}
variable "git_revision" {
type = string
default = null
description = "Optional redeploy trigger. Set a new non-empty marker to request redeploy on Update; leave null for normal VM creation."
}
variable "reconcile_trigger" {
type = string
default = null
description = "Optional reconcile trigger. Change the marker to request reconcile on Update; reconcile does not run on Create."
}
+32
View File
@@ -0,0 +1,32 @@
# Create a VDC inside the organization that already exists in the personal account.
resource "nubes_vc_vdc" "vdc" {
# Name shown for this VDC service in the personal account.
resource_name = var.vdc_resource_name
# Existing organization name or UUID; this example does not create the organization.
organization_uid = var.organization
# Resource-platform network provider chosen for this organization.
network_provider = var.vdc_network_provider
# Provider VDC compatible with the selected network provider.
provider_vdc = var.vdc_provider_vdc
# vCPU capacity allocated to the VDC.
cpu_allocated = var.vdc_cpu_allocated
# Guaranteed share of allocated vCPU; PROD accepts 0, 50, or 80.
cpu_guaranteed = var.vdc_cpu_guaranteed
# Memory capacity allocated to the VDC, in GB.
mem_allocated = var.vdc_mem_allocated
# JSON storage-policy array; each policy name must exist in the selected Provider VDC.
storage_config = var.vdc_storage_config
# Suspend the VDC rather than fully deleting it on terraform destroy.
suspend_on_destroy = true
# Adopt an existing same-name VDC instead of failing during Create.
adopt_existing_on_create = true
}
+11
View File
@@ -0,0 +1,11 @@
# This standalone example targets the tested Nubes PROD provider.
terraform {
required_version = ">= 1.5.0"
required_providers {
nubes = {
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes/nubes"
version = "1.5.0"
}
}
}
+77
View File
@@ -0,0 +1,77 @@
# Create the vApp after the VDC, Edge, organization IP allocation, and SNAT are ready.
resource "nubes_vapp" "vapp" {
# Name shown for this vApp service in the personal account.
resource_name = var.vapp_resource_name
# vApp name; must be unique in the organization and match the platform naming pattern.
vapp_name = var.vapp_name
# UUID of the VDC where the vApp will reside.
vdc_uid = nubes_vc_vdc.vdc.id
# UUID of the Edge whose routed network will be used by the vApp.
nsxt_uid = nubes_vc_nsxt.edge.id
# Suspend the vApp rather than fully deleting it on terraform destroy.
suspend_on_destroy = true
# Adopt an existing same-name vApp rather than failing during Create.
adopt_existing_on_create = true
# The tested PROD vApp deployment requires the IP/SNAT configuration to be ready.
depends_on = [nubes_vc_nsxt_snat.snat]
}
# Create one VM inside the vApp.
resource "nubes_vc_vm_v3" "vm" {
# Name shown for this VM service in the personal account.
resource_name = var.vm_resource_name
# VM hostname/IP-set name; it must match the platform naming pattern.
vm_name = var.vm_name
# UUID of the vApp created above.
vapp_uid = nubes_vapp.vapp.id
# Operating-system image; its base disk is separate from vm_disk.
image_vm = var.vm_image
# Number of vCPUs allocated to the VM.
vm_cpu = var.vm_cpu
# VM memory in GB.
vm_ram = var.vm_ram
# Additional disk size in GB, excluding the image's base disk.
vm_disk = var.vm_disk
# Linux account created for SSH access.
user_login = var.vm_user_login
# Public key installed for that account; never use a private key here.
user_public_key = var.vm_user_public_key
# Organization IP space used by this VM.
ip_space_name = var.ip_space_name
# true gives this VM its own external IP during deployment.
same_snat = var.vm_same_snat
# JSON-encoded inbound port/protocol allow-list.
access_port_list = jsonencode(var.vm_access_port_list)
# JSON-encoded source IP/CIDR allow-list; keep it as narrow as practical.
access_ip_list = jsonencode(var.vm_access_ip_list)
# Optional marker: changing it requests redeploy on Update.
git_revision = var.git_revision
# Optional marker: changing it requests reconcile on Update, not on Create.
reconcile_trigger = var.reconcile_trigger
# Suspend the VM rather than fully deleting it on terraform destroy.
suspend_on_destroy = true
# Adopt an existing same-name VM rather than failing during Create.
adopt_existing_on_create = true
}