Быстрый, повторяемый и предсказуемый старт новой VDS — это не «приятно иметь», а базовая гигиена для администраторов и разработчиков. Если каждый сервер вы настраиваете руками, вы платите временем и рисками: от пропущенных пакетов до несовпадающих конфигураций. Решение — облачный провижининг на этапе первого запуска. В этой статье разбираем, как использовать cloud-init и user-data, чтобы за один бут получить: пользователей и SSH ключи, обновления и пакеты, базовую безопасность, Nginx и даже собственные systemd-сервисы.
Зачем cloud-init для VDS
cloud-init — это стандарт де-факто для начальной инициализации образов Linux в облаках и виртуалках. Его задача — один раз на первом старте собрать данные из источника (datasource), выполнить модули (создать пользователей, добавить SSH ключи, установить пакеты, применить write_files, запустить команды) и зафиксировать, что инстанс готов к работе.
Преимущества для VDS:
- повторяемость: один YAML — десятки идентичных инстансов;
- скорость: «всё готово» сразу после запуска;
- прозрачность: провижининг документирован и версионируется;
- безопасность: первичный доступ через SSH ключи, без открытых паролей;
- минимум ручных шагов — меньше шансов на ошибку.
Как это работает: datasources и этапы
На первом старте cloud-init ищет источник метаданных. Для VDS обычно используется NoCloud или совместимые провайдерские источники. В простом варианте NoCloud — это пара файлов user-data и meta-data, доступных системе при старте. meta-data задаёт, например, instance-id и имя хоста, а user-data — сценарий провижининга.
Этапы работы (упрощённо):
- Local: ранние действия до сети;
- Network: сетевые настройки и доступ к datasources;
- Config: применение
user-data, создание пользователей, пакеты; - Final: отложенные команды, сервисы, сообщения.
Важно: cloud-init стремится быть идемпотентным для своих модулей и запускается один раз. Повторный «стартовый» прогон выполняют осознанно командой очистки состояния.

Анатомия user-data
Форматов несколько, но чаще всего используют #cloud-config (YAML). Он декларативен и охватывает 80% типовых задач: пользователи, SSH ключи, пакеты, файлы, команды, перезагрузки. Также возможны сценарии shell (text/x-shellscript) и многочастные MIME-пакеты, если нужно комбинировать.
Минимальный рабочий пример
#cloud-config
hostname: app-1
manage_etc_hosts: true
users:
- name: deploy
gecos: Deploy User
groups: sudo
shell: /bin/bash
sudo: ALL=(ALL) NOPASSWD:ALL
ssh_authorized_keys:
- ssh-ed25519 AAAA...your_public_key... user@laptop
ssh_pwauth: false
disable_root: true
timezone: UTC
package_update: true
package_upgrade: true
packages:
- htop
- curl
- fail2ban
- nginx
- ufw
runcmd:
- ufw default deny incoming
- ufw default allow outgoing
- ufw allow OpenSSH
- ufw allow 80
- ufw allow 443
- ufw --force enable
final_message: "cloud-init complete on $INSTANCE_ID"
После первого бутстрапа у вас будет пользователь deploy с ключом, доступ по паролю выключен, обновления применены, базовые пакеты установлены, файрвол настроен, Nginx готов к старту. Для большинства образов Ubuntu/Debian это уже «почти прод» начало.
Пакеты и обновления: что лучше — packages или runcmd
cloud-init предоставляет директивы package_update и package_upgrade, которые вызывают менеджер пакетов корректно и вовремя. Для установки конкретных пакетов используйте packages. Это предпочтительнее, чем запускать apt-get вручную в runcmd: меньше гонок за блокировки и проблем с порядком модулей.
Если требуется перезагрузка после крупных обновлений, используйте power_state:
power_state:
mode: reboot
message: "Rebooting after upgrades"
timeout: 30
SSH ключи и пользователи без сюрпризов
Правильный минимум — бессессионный root и логин только по ключам. Это делается настройками ssh_pwauth: false и disable_root: true плюс создание одного-двух пользователей с ssh_authorized_keys. Для нескольких ключей достаточно перечислить их списком.
Если вам нужно добавить ключи позже, можно использовать write_files, но безопаснее хранить ключи в системе контроля версий как часть user-data и обновлять инстансы через пересоздание (Immutable подход) или конфигурационный менеджер. Изменение авторизации вручную на первом старте — источник дрейфа конфигурации.
Файлы и конфиги через write_files
write_files позволяет положить любой конфиг, задать права и владельца. Это удобно для единичных шаблонов или unit-файлов systemd.
write_files:
- path: /etc/nginx/sites-available/default
permissions: '0644'
owner: root:root
content: |
server {
listen 80 default_server;
server_name _;
root /var/www/html;
index index.html;
location /health {
return 200 'ok\n';
}
}
Если конфигураций много или они зависят от окружения, подумайте о многочастном MIME, где часть будет cloud-config, а часть — text/x-shellscript для генерации файлов средствами шаблонизации, доступными в вашем окружении.
Команды: bootcmd, runcmd, инициализация сервисов
bootcmd запускается очень рано (при каждом старте), лучше не использовать его для прикладных задач. runcmd — «подходящее место» для одноразовых команд по итогам установки пакетов и файлов. Для управления сервисами (enable, restart) также подойдёт runcmd:
runcmd:
- systemctl enable nginx
- systemctl restart nginx
Для сложной логики используйте systemd units, положив их через write_files и активировав в runcmd — так поведение будет детерминированным и наблюдаемым средствами journalctl.
Пример: веб-окружение за один бут
Ниже — объединённый пример для быстрого веб-узла с Nginx, deploy-пользователем, UFW и автообновлениями безопасности.
#cloud-config
hostname: web-1
manage_etc_hosts: true
users:
- name: deploy
groups: sudo
shell: /bin/bash
sudo: ALL=(ALL) NOPASSWD:ALL
ssh_authorized_keys:
- ssh-ed25519 AAAA...your_public_key... user@laptop
ssh_pwauth: false
disable_root: true
timezone: Europe/Moscow
package_update: true
package_upgrade: true
packages:
- nginx
- ufw
- fail2ban
- unattended-upgrades
write_files:
- path: /etc/apt/apt.conf.d/51unattended-upgrades-fast
permissions: '0644'
content: |
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:30";
- path: /etc/nginx/sites-available/default
permissions: '0644'
content: |
server {
listen 80 default_server;
server_name _;
root /var/www/html;
index index.html;
add_header X-Frame-Options SAMEORIGIN always;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy no-referrer-when-downgrade always;
location / {
try_files $uri $uri/ =404;
}
}
runcmd:
- mkdir -p /var/www/html
- chown -R www-data:www-data /var/www/html
- bash -lc "echo 'Hello from cloud-init' > /var/www/html/index.html"
- systemctl enable nginx
- systemctl restart nginx
- ufw default deny incoming
- ufw default allow outgoing
- ufw allow OpenSSH
- ufw allow 80
- ufw allow 443
- ufw --force enable
final_message: "web-1 is ready"
Когда будете выводить сайт в интернет, сразу подключайте SSL-сертификаты и указывайте домены через регистрацию доменов. Для корректного редиректа на HTTPS и защиты HSTS пригодится чек-лист «301, HSTS и SSL» — см. материал перенос на HTTPS с 301 и HSTS.
Docker за минуту через cloud-init
Если вы предпочитаете контейнеры, базовой установки достаточно для старта, без внешних скриптов.
#cloud-config
packages:
- docker.io
- docker-compose-plugin
runcmd:
- systemctl enable docker
- systemctl start docker
- usermod -aG docker deploy
Далее можно положить файл docker-compose.yml через write_files и запустить docker compose up -d в runcmd. Такой подход избавляет от ручной установки и ускоряет повторные деплои.

Сеть: когда нужен network-config
На большинстве VDS сеть уже настроена провайдером. Но если вы используете NoCloud с собственным seed, можно передать network-config для Netplan (версия 2). Пример для DHCP:
network:
version: 2
ethernets:
ens3:
dhcp4: true
Для статики — задайте адрес, шлюз и DNS. Убедитесь, что имена интерфейсов совпадают с реальными в образе.
Если предпочитаете управлять сервером через панель, посмотрите сравнение популярных решений: панели для VDS в 2025.
MIME multi-part: комбинируем конфиг и скрипты
Когда одного #cloud-config мало (например, нужен короткий shell-скрипт и несколько файлов), используйте многочастный MIME. Пример структуры:
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="//"
--//
Content-Type: text/cloud-config; charset="us-ascii"
#cloud-config
package_update: true
packages:
- nginx
--//
Content-Type: text/x-shellscript; charset="us-ascii"
#!/bin/bash
set -euo pipefail
echo "post-install hook" > /root/post_install.log
--//
Content-Type: text/cloud-config; charset="us-ascii"
#cloud-config
write_files:
- path: /etc/motd
content: |
Provisioned by cloud-init
--//--
cloud-init сам распознает части по заголовкам Content-Type и выполнит их в корректном порядке.
Отладка и диагностика
Если что-то пошло не так, у cloud-init есть понятные точки входа:
- логи:
/var/log/cloud-init.logи/var/log/cloud-init-output.log; - сводка:
cloud-init statusиcloud-init analyze show; - повторный запуск:
cloud-init cleanи затем рестарт; - запрос данных:
cloud-init query --all.
cloud-init --version
cloud-init status --wait
cloud-init analyze show
journalctl -u cloud-init -xe
Для проверки синтаксиса конфигурации в новых версиях доступна команда валидации схемы. Также помогает запуск cloud-init single для отдельного модуля, если нужно локально воспроизвести шаги.
Идемпотентность и порядок действий
Часть модулей запускается «один раз», часть — на каждом старте. Для прикладного провижининга ориентируйтесь на модуль runcmd и создание неизменяемых файлов. Если в runcmd есть операции, которые вы не хотите повторять при ручном перезапуске, оборачивайте их в проверки существования маркерных файлов или системных состояний.
runcmd:
- test -f /opt/.init_done || (do-something && touch /opt/.init_done)
Так вы избежите «двоения» ресурсов и неожиданных перезапусков сервисов.
Типичные ошибки и как их избежать
- Неверные отступы YAML. Проверяйте конфиг линтерами или валидацией cloud-init.
- Смешивание
packagesи ручныхapt-getвruncmd. Отдайте приоритет декларативным директивам. - Блокировки APT из-за параллельных задач. Не запускайте пост-обновления в
runcmdдо завершенияpackage_*. - Отключили пароль и root, но не добавили SSH ключи. Проверяйте доступность ключей перед стартом.
- Неверное имя сетевого интерфейса в
network-config. Сверяйтесь сip link. - Перегруженный
runcmd. Для сложных сценариев лучше положить и активировать systemd unit.
Шаблоны для команд и стейджинга
Хорошая практика — хранить базовый user-data как шаблон и подставлять переменные (имя хоста, список ключей, роли) внешним генератором в вашем CI/CD. Для стейджинга и продакшена меняйте ровно то, что нужно: имя хоста, доменные имена в Nginx, списки авторизованных ключей, набор пакетов.
Чем меньше ручных шагов после первого бута — тем проще масштабирование и тем надёжнее ваш релизный процесс.
Практический чек-лист
- Определите минимальный набор: пользователь, SSH ключи, часовой пояс, обновления, base-пакеты.
- Добавьте безопасность: UFW или nftables, fail2ban, отключение пароля и root-логина.
- Приложение: Nginx, PHP-FPM или Docker — в
packages+runcmd. - Конфиги:
write_filesдля site- и unit-файлов. - Проверка: валидация YAML и просмотр логов cloud-init.
- Идемпотентность: маркеры или systemd-юниты для повторных прогонов.
Вместо финала
cloud-init — простой инструмент, который снимает 80% рутины в первые пять минут жизни VDS. С одним user-data вы закладываете фундамент безопасности, наблюдаемости и повторяемости. Дальше остаётся только наращивать логику: логгеры, агенты мониторинга, ротация ключей, деплой приложений — всё это можно добавить в тот же сценарий. Начните с малого, но сразу делайте это автоматизированно: так ваши серверы перестанут «рождаться вручную» и начнут воспроизводиться так же легко, как сборки в CI.


