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:
- Generator — determines the datasource (cloud platform)
- Local — applies networking configuration
- Network — fetches user-data and metadata after networking is up
- Config — runs configuration modules (packages, users, files)
- 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
multipassor 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.