Docs / Automation & IaC / How to Automate Server Provisioning with Cloud-Init

How to Automate Server Provisioning with Cloud-Init

By Admin · Mar 2, 2026 · Updated Apr 24, 2026 · 28 views · 3 min read

How to Automate Server Provisioning with Cloud-Init

Cloud-init is the industry standard for automating the initial setup of cloud instances. When you launch a new Breeze instance, cloud-init runs during the first boot to configure networking, users, packages, and custom scripts without manual intervention.

How Cloud-Init Works

Cloud-init reads configuration from a user-data payload provided at launch time. It runs in multiple stages during boot:

  1. Generator — determines the datasource (cloud platform)
  2. Local — applies networking configuration
  3. Network — fetches user-data and metadata after networking is up
  4. Config — runs configuration modules (packages, users, files)
  5. Final — runs user scripts and final modules

Writing User-Data

User-data is typically written in YAML format with a #cloud-config header:

#cloud-config

# Set the hostname
hostname: breeze-web-01
fqdn: breeze-web-01.example.com

# Create users
users:
  - name: deploy
    groups: sudo
    shell: /bin/bash
    sudo: ALL=(ALL) NOPASSWD:ALL
    ssh_authorized_keys:
      - ssh-ed25519 AAAA... deploy@workstation

# Install packages
package_update: true
package_upgrade: true
packages:
  - nginx
  - ufw
  - fail2ban
  - unattended-upgrades
  - htop
  - curl

# Write configuration files
write_files:
  - path: /etc/nginx/sites-available/default
    content: |
      server {
          listen 80 default_server;
          root /var/www/html;
          index index.html;
          server_name _;
          location / {
              try_files $uri $uri/ =404;
          }
      }
    owner: root:root
    permissions: '0644'

  - path: /etc/ssh/sshd_config.d/hardened.conf
    content: |
      PermitRootLogin no
      PasswordAuthentication no
      MaxAuthTries 3
    owner: root:root
    permissions: '0644'

Running Commands

Execute commands during provisioning with runcmd:

# runcmd runs after packages are installed
runcmd:
  - ufw allow 22/tcp
  - ufw allow 80/tcp
  - ufw allow 443/tcp
  - ufw --force enable
  - systemctl enable fail2ban
  - systemctl start fail2ban
  - systemctl restart sshd
  - systemctl enable nginx
  - systemctl start nginx
  - echo "Provisioning complete at $(date)" >> /var/log/cloud-init-custom.log

Setting Up Automatic Security Updates

Configure unattended upgrades as part of your cloud-init:

write_files:
  - path: /etc/apt/apt.conf.d/50unattended-upgrades
    content: |
      Unattended-Upgrade::Allowed-Origins {
          "${distro_id}:${distro_codename}-security";
      };
      Unattended-Upgrade::Automatic-Reboot "false";
      Unattended-Upgrade::Mail "admin@example.com";

Verifying Cloud-Init

After your Breeze instance boots, verify cloud-init ran successfully:

# Check overall status
cloud-init status --long

# View logs
cat /var/log/cloud-init-output.log
cat /var/log/cloud-init.log

# Re-run cloud-init (for testing)
sudo cloud-init clean --logs
sudo cloud-init init

Best Practices

  • Keep user-data idempotent — scripts should be safe to run multiple times
  • Validate YAML syntax — use cloud-init schema --config-file user-data.yml
  • Avoid embedding secrets — pull secrets from a vault or secrets manager at runtime
  • Use write_files — deploy config files directly instead of using sed/awk in runcmd
  • Test locally — use multipass or LXD to test cloud-init configs before deploying

Cloud-init lets you spin up fully configured Breeze instances in minutes, ensuring every server starts with a consistent, secure baseline.

Was this article helpful?