Боль, которую мы лечим, простая. Ты создал инстанс, потом руками зашёл по ssh, поставил пакеты, что-то настроил, забыл записать. Через месяц нужна вторая такая же машина - и ты не помнишь и половины. А если машин десять? Подход "iac" в том и состоит, чтобы вся настройка лежала в .tf-файлах и воспроизводилась одной командой terraform apply. Сегодня мы вшиваем установку веб-сервера прямо в описание ВМ через cloud-init.
Что такое метаданные и user-data
У каждой виртуалки в облаке есть служба метаданных - это такой внутренний эндпоинт, к которому ОС обращается при загрузке. Через него облако передаёт машине информацию: ssh-ключи, имя хоста и, что нам сейчас важно, скрипт первичной настройки. В Yandex Cloud метаданные задаются в блоке (точнее, в аргументе-карте) metadata у ресурса yandex_compute_instance.
Ключ ssh-keys ты, скорее всего, уже видел в прошлых уроках - он кладёт твой публичный ключ внутрь машины. А вот ключ user-data - это и есть точка входа для cloud-init. Cloud-init - стандартный механизм в большинстве облачных образов Linux (Ubuntu, Debian, CentOS). Он читает user-data при первой загрузке и выполняет то, что там написано: ставит пакеты, пишет файлы, запускает команды. По сути это твой первый акт автоматизации внутри ВМ.
Формат user-data - это YAML, который начинается со строки #cloud-config. Звучит страшно, но на практике это пара блоков: что поставить и что запустить. Давай посмотрим минимальный конфиг, который ставит nginx и кладёт простую html-страницу:
Код: Выделить всё
#cloud-config
package_update: true
packages:
- nginx
write_files:
- path: /var/www/html/index.html
content: |
<h1>Привет из Terraform!</h1>
<p>nginx подняли через cloud-init на Yandex Cloud</p>
runcmd:
- systemctl enable nginx
- systemctl restart nginx

Собираем ВМ с user-data
Теперь сам ресурс. Обрати внимание на блок network_interface: чтобы машина была доступна из интернета, ей нужен внешний IP - это аргумент nat = true. Без него ВМ живёт только во внутренней сети, и никакой curl снаружи до неё не достучится.
Код: Выделить всё
resource "yandex_compute_instance" "web" {
name = "web-server"
platform_id = "standard-v3"
zone = "ru-central1-a"
resources {
cores = 2
memory = 2
}
boot_disk {
initialize_params {
image_id = "fd80bm0rh4rkepi5ksdi" # Ubuntu 22.04 LTS
size = 10
}
}
network_interface {
subnet_id = yandex_vpc_subnet.this.id
nat = true
security_group_ids = [yandex_vpc_security_group.web.id]
}
metadata = {
user-data = file("${path.module}/cloud-init.yaml")
ssh-keys = "ubuntu:${file("~/.ssh/id_ed25519.pub")}"
}
}
Группа безопасности: открываем нужные порты
Внешний IP сам по себе ничего не открывает. По умолчанию трафик к машине режется, и если не настроить правила, твой nginx будет работать, но снаружи останется невидимым. За это отвечает ресурс yandex_vpc_security_group - облачный аналог файрвола перед интерфейсом ВМ.
Правила бывают двух направлений: ingress (входящий трафик к машине) и egress (исходящий от машины). Нам нужно впустить два порта: 22 для ssh и 80 для http. И обязательно разрешить весь исходящий трафик egress - иначе cloud-init не сможет скачать пакет nginx из репозиториев, и установка молча провалится. Это, кстати, одна из самых частых граблей у новичков.
Код: Выделить всё
resource "yandex_vpc_security_group" "web" {
name = "web-sg"
description = "SSH and HTTP for web server"
network_id = yandex_vpc_network.this.id
ingress {
protocol = "TCP"
description = "SSH"
v4_cidr_blocks = ["0.0.0.0/0"]
port = 22
}
ingress {
protocol = "TCP"
description = "HTTP"
v4_cidr_blocks = ["0.0.0.0/0"]
port = 80
}
egress {
protocol = "ANY"
description = "Allow all outbound"
v4_cidr_blocks = ["0.0.0.0/0"]
from_port = 0
to_port = 65535
}
}
Если тебе ближе держать правила отдельными ресурсами, в Yandex Cloud есть yandex_vpc_security_group_rule с аргументами security_group_binding и direction. Но мешать оба подхода в одной группе нельзя - провайдер ругнётся на конфликт. Для старта проще описывать ingress и egress прямо внутри группы, как выше.
Сеть, проверка и запуск
Чтобы всё это завелось, нужны ещё сеть и подсеть - база, на которую опираются и группа безопасности, и интерфейс ВМ:
Код: Выделить всё
resource "yandex_vpc_network" "this" {
name = "web-net"
}
resource "yandex_vpc_subnet" "this" {
name = "web-subnet"
zone = "ru-central1-a"
network_id = yandex_vpc_network.this.id
v4_cidr_blocks = ["10.10.0.0/24"]
}
Проверяем, что веб-сервер живой:
Код: Выделить всё
curl http://<внешний_IP_машины>
Типичные грабли
- Забыли nat = true в интерфейсе - машина без внешнего IP, curl не дотянется, хотя внутри всё работает.
- Не открыли egress - cloud-init не может скачать nginx, страница не появляется, а в логах внутри ВМ ошибки apt. Egress на ANY и 0.0.0.0/0 лечит.
- Перепутали порт в ingress - открыли 8080 вместо 80, а nginx слушает 80. Соответствие портов проверяй внимательно.
- Поспешили с curl - дали машине полсекунды и решили, что ничего не работает. Cloud-init не мгновенный, подожди.
- Невалидный YAML в user-data - лишний отступ, и cloud-init молча проглатывает конфиг. Проверяй отступы, YAML их не прощает.
Сейчас у нас всё прибито гвоздями: имя web-server, зона ru-central1-a, образ строкой, CIDR прямо в коде. Для одного учебного стенда это нормально. Но как только захочется поднять второй такой сервер, или развернуть копию в другой зоне, или отдать конфиг коллеге - начнётся боль. Менять придётся в десяти местах, и где-нибудь обязательно забудешь.
Именно поэтому в следующих уроках мы вынесем все эти значения в переменные. Имя, зона, образ, размер диска, разрешённые CIDR - всё это станет настраиваемыми входами. Один и тот же код сможет породить dev-стенд и прод, отличающиеся лишь файлом с переменными. Это не каприз, а суть подхода terraform: описание инфраструктуры должно быть переиспользуемым, а не одноразовым. Кстати, всё сказанное справедливо и для opentofu - открытого форка Terraform, синтаксис HCL у них общий.
Мини-лаба
Повтори руками, по шагам:
- Создай файл cloud-init.yaml с установкой nginx и своей html-страницей.
- Опиши сеть, подсеть, группу безопасности с ingress 22 и 80 и egress на всё.
- Опиши yandex_compute_instance с nat = true, привязкой security_group_ids и metadata.user-data через file().
- Прогони terraform plan, изучи, что будет создано, затем terraform apply.
- Дождись cloud-init и проверь curl по внешнему IP. Добейся, чтобы вернулась твоя страница.
- Бонус: убери egress и посмотри, как сломается установка. Потом верни обратно.
- За что отвечает ключ user-data в metadata и какой механизм внутри ВМ его обрабатывает?
- Зачем нужен аргумент nat = true и что будет без него?
- Почему важно разрешить egress, если мы хотим всего лишь отдавать http-страницу?
- Чем привязка через security_group_ids в network_interface отличается от описания правил внутри самой группы?
Что забрать с собой: metadata.user-data - это вход для cloud-init, через который ВМ сама себя настраивает при первой загрузке. yandex_vpc_security_group с правилами ingress и egress открывает нужные порты, и без egress установка пакетов не пройдёт. nat = true даёт внешний IP, без которого снаружи не достучаться. Группа цепляется к машине через security_group_ids в network_interface. И главный вывод на будущее: хардкод хорош ровно до второго сервера - дальше нужны переменные и модули terraform, к которым мы и перейдём.