Cloud-init Configuration for Server Provisioning
Cloud-init is a powerful tool for automating initial server configuration across major cloud providers. It provides a standard way to configure instances on first boot using cloud-config YAML syntax, enabling infrastructure-as-code provisioning without external tools. This guide covers cloud-config fundamentals, user data specification, package installation, user management, custom scripts, file provisioning, and advanced features.
Table of Contents
- Cloud-init Overview
- Cloud-Config Syntax
- Package Installation
- User and Group Management
- Running Commands
- File Management
- Network Configuration
- Cloud-init Callbacks
- Debugging Cloud-init
- Conclusion
Cloud-init Overview
Cloud-init is the industry standard for cloud instance initialization. It runs on first boot with root privileges, making it perfect for initial system configuration, package installation, user creation, and custom setup tasks.
Key benefits:
- Standard Across Cloud Providers: Works on AWS, Azure, GCP, DigitalOcean, Linode
- Simple YAML Syntax: Cloud-config provides readable, declarative configuration
- Fast Boot: Configured on first boot, doesn't require additional software
- Repeatable: Same configuration produces consistent results every time
- Extensible: Support for custom scripts and modules
- Integration Ready: Works with Terraform, CloudFormation, Ansible
Cloud-init workflow:
Instance Launch
↓
Cloud-init Agent Starts (root)
↓
Fetch User Data
↓
Parse Cloud-Config
↓
Execute Initialization Steps
│
├── Module: Install Packages
├── Module: Create Users
├── Module: Write Files
├── Module: Run Commands
└── Module: Configure Services
↓
Initialization Complete
Cloud-init stages:
- Generator: Generates additional services if needed
- Local: Early networking setup, mount filesystems
- Network: Full networking available, run network-dependent tasks
- Config: General configuration
- Final: Last stage, run final customization
Cloud-Config Syntax
Cloud-config uses YAML format starting with #cloud-config.
Basic structure:
#cloud-config
# Comments start with #
package_update: true
package_upgrade: true
packages:
- nginx
- curl
- wget
runcmd:
- systemctl start nginx
- systemctl enable nginx
write_files:
- path: /etc/nginx/sites-available/default
content: |
server {
listen 80 default_server;
server_name _;
location / {
return 200 "Hello from $(hostname)";
}
}
Data type conversions:
#cloud-config
# String
region: us-east-1
# Number
port: 8080
# Boolean
enabled: true
disabled: false
# List
packages:
- nginx
- curl
# Dictionary/Map
environment:
APP_ENV: production
LOG_LEVEL: debug
# Multiline string
config: |
This is a multiline
configuration string
that preserves formatting
Package Installation
Manage packages easily with cloud-init.
Basic package management:
#cloud-config
# Update package index
package_update: true
# Upgrade existing packages
package_upgrade: true
# Upgrade distribution packages
distro_upgrade: true
# Install packages
packages:
- nginx
- curl
- wget
- htop
- git
- python3-pip
# Package reboot behavior
package_reboot_if_required: true
# Configure package manager
apt:
conf: |
APT::Acquire::http::Timeout "60";
APT::Acquire::ftp::Timeout "60";
PPA and repository configuration:
#cloud-config
# Add custom repository
apt:
sources:
docker.list:
source: deb [arch=amd64] https://download.docker.com/linux/ubuntu $RELEASE stable
keyid: 9DC858229FC7DD38854AE2D88D81803C0EBFCD88
# Or add PPA
apt_sources:
- source: ppa:nginx/stable
- source: ppa:deadsnakes/ppa
# After adding repos, install packages
packages:
- docker-ce
- docker-ce-cli
- containerd.io
- python3.9
Package configuration:
#cloud-config
packages:
- nginx
- mysql-server
# Configure packages
package_update: true
package_upgrade: true
# Prevent interactive prompts
debconf_selections: |
mysql-server mysql-server/root_password password root
mysql-server mysql-server/root_password_again password root
mysql-server mysql-server/remove_databases_and_users boolean false
mysql-server mysql-server/skip-install boolean false
User and Group Management
Create and configure user accounts.
Create users:
#cloud-config
users:
- name: ubuntu
groups: sudo
shell: /bin/bash
sudo: ['ALL=(ALL) NOPASSWD:ALL']
ssh_authorized_keys:
- ssh-rsa AAAAB3NzaC1... [email protected]
- name: app
groups: appgroup
shell: /usr/sbin/nologin
home: /opt/app
inactive: true
- name: devops
groups: [sudo, docker, wheel]
shell: /bin/bash
ssh_authorized_keys:
- ssh-rsa AAAAB3NzaC1... [email protected]
- ssh-rsa AAAAB3NzaC1... [email protected]
# Create groups
groups:
- docker
- appgroup: [app, ubuntu]
# Ensure default user disabled
disable_root: true
User configuration options:
#cloud-config
users:
- name: appuser
# Set user ID
uid: 1001
# Set group ID
gid: 1001
# Home directory
home: /opt/appuser
# Shell
shell: /bin/bash
# Group membership
groups: [sudo, adm]
# Add to sudoers
sudo: ['ALL=(ALL) NOPASSWD:ALL']
# Don't create home directory
no_create_home: false
# Create home directory with specific permissions
homedir: /home/appuser
# SSH keys for login
ssh_authorized_keys:
- ssh-rsa AAAAB3NzaC1... [email protected]
- ssh-rsa AAAAB3NzaC1... [email protected]
# Disable password login
ssh_import_id: github:username
# Lock account
# lock_passwd: true
# Set expiration
expire: false
# Create with password (usually avoided for security)
# passwd: $1$salt$hashed_password
Group management:
#cloud-config
groups:
- docker
- postgres
- www-data: [ubuntu]
- developers: [app, deploy, ubuntu]
users:
- name: deploy
groups: [developers, sudo]
- name: app
groups: developers
Running Commands
Execute scripts and commands during initialization.
Basic command execution:
#cloud-config
# Run commands at final stage
runcmd:
- /opt/bin/setup.sh
- systemctl start nginx
- systemctl enable nginx
- echo "Server configured" | mail -s "Server Ready" [email protected]
# Run commands conditionally by list index
# First element runs before second, etc.
runcmd:
- ['sh', '-c', 'echo "$HOSTNAME is up" > /tmp/status.txt']
- systemctl restart nginx
Command output redirection:
#cloud-config
runcmd:
# Redirect to file
- 'echo "Configuration log" > /var/log/init-log.txt'
# Append to file
- 'echo "Additional info" >> /var/log/init-log.txt'
# Suppress output
- 'systemctl start nginx > /dev/null 2>&1'
# Log with timestamp
- 'echo "$(date): Server configured" >> /var/log/cloud-init-custom.log'
Bootcmd for early execution:
#cloud-config
# Run commands at boot (before packages installed)
bootcmd:
- 'modprobe zfs'
- 'mkfs -t ext4 /dev/sdb1'
- 'mkdir -p /mnt/data'
- 'mount /dev/sdb1 /mnt/data'
# Then install packages
packages:
- nginx
- curl
# Then run runcmd
runcmd:
- systemctl start nginx
Custom scripts:
#cloud-config
packages:
- python3
# Write custom script
write_files:
- path: /opt/setup.py
permissions: '0755'
owner: root:root
content: |
#!/usr/bin/env python3
import subprocess
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logger.info("Running custom setup")
subprocess.run(['apt-get', 'install', '-y', 'nginx'])
subprocess.run(['systemctl', 'start', 'nginx'])
runcmd:
- /opt/setup.py
File Management
Create and manage files during initialization.
Write files:
#cloud-config
write_files:
- path: /etc/app/config.yml
owner: root:root
permissions: '0644'
content: |
app:
name: myapp
port: 8080
environment: production
logging:
level: info
file: /var/log/app.log
- path: /opt/app/run.sh
owner: app:app
permissions: '0755'
content: |
#!/bin/bash
cd /opt/app
./myapp --config /etc/app/config.yml
- path: /etc/systemd/system/app.service
owner: root:root
permissions: '0644'
content: |
[Unit]
Description=My Application
After=network.target
[Service]
Type=simple
User=app
WorkingDirectory=/opt/app
ExecStart=/opt/app/run.sh
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
- path: /etc/environment
append: true
content: |
APP_ENV=production
LOG_LEVEL=info
File encoding and compression:
#cloud-config
write_files:
- path: /tmp/data.txt
encoding: base64
content: |
VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIGZpbGUK
- path: /opt/archive.tar.gz
encoding: gz+base64
content: H4sIAIWLpGUC/wvJyCzRUMgsS8wpTVWoVkgsS8wpURIqS8wpTlWqAOJcJaX5JUWlJRnpxUX5JUWlJRmpxUWlJRmlmSWpyZkpGalFqWWpqUVQUqsUQGpqamlyUWlpRmlyUmlySmpRVGpJZWlJZllKUWlpZkpSZWlJUXlKZklqMxM9RWZMoRhiEqUSR+LKnFmZlZoKkSzhTLBqJTK0UBMsI0vMBb6yVnFJEkRhWhFgBTr3nMH0QzUSMqGCYHbPTi5JLEktLkksTs0rBsi55hUlJhWlKuSmFqSWlqSWlqQWFpUkKoEUlpQUl6ZmluQVpyZnlmSklmTkFpalJqakFhWnFpQWFqYWJBUVF6VkJUpgSmVOQWpJZUpiRWlyQWpEUsVKKJCOJCniBLSYC1EFWQ1sOQEhyAKG0o9KaLIFaMsRgO6y2oBl8yLmEzaLBqLqWvt9M27L+zJdmRpQMREONuA=
Copy files from source:
#cloud-config
# Note: write_files copies content, not external files
# For copying from source during build, use provisioners in Packer
# or download during execution
write_files:
- path: /opt/setup.sh
permissions: '0755'
content: |
#!/bin/bash
# Download configuration from S3
aws s3 cp s3://config-bucket/app-config.yml /etc/app/config.yml
# Clone application from GitHub
git clone https://github.com/myorg/myapp /opt/app
Network Configuration
Configure networking during initialization.
Network configuration:
#cloud-config
# Configure hostname
hostname: webserver-01
fqdn: webserver-01.example.com
prefer_fqdn_over_hostname: true
# Update /etc/hosts
manage_etc_hosts: true
# NTP configuration
ntp:
enabled: true
ntp_client: systemd-timesyncd
servers:
- 0.ubuntu.pool.ntp.org
- 1.ubuntu.pool.ntp.org
# DNS configuration
resolv_conf:
nameservers:
- 8.8.8.8
- 8.8.4.4
searchdomains:
- example.com
Custom network setup:
#cloud-config
# Wait for network to be ready
wait_for_network: true
wait_for_network_timeout: 60
# Disable IPv6
bootcmd:
- 'echo "net.ipv6.conf.all.disable_ipv6 = 1" >> /etc/sysctl.conf'
- 'sysctl -p'
# Configure static IP (if not using DHCP)
write_files:
- path: /etc/netplan/99-custom-ip.yaml
content: |
network:
version: 2
ethernets:
eth0:
dhcp4: false
addresses:
- 192.168.1.100/24
gateway4: 192.168.1.1
nameservers:
addresses: [8.8.8.8, 8.8.4.4]
runcmd:
- netplan apply
Cloud-init Callbacks
Send notifications after configuration completion.
Phone home to notify completion:
#cloud-config
phone_home:
url: http://example.com/callback?instance_id=$INSTANCE_ID&hostname=$HOSTNAME
post: all
tries: 10
# Or callback with webhook
phone_home:
url: https://webhook.example.com/cloud-init
post: all
tries: 3
retries: 5
Send notification to monitoring:
#cloud-config
packages:
- curl
write_files:
- path: /opt/notify.sh
permissions: '0755'
content: |
#!/bin/bash
INSTANCE_ID=$(ec2-metadata --instance-id | cut -d' ' -f2)
HOSTNAME=$(hostname)
# Send to monitoring system
curl -X POST https://monitoring.example.com/api/events \
-H "Content-Type: application/json" \
-d "{
\"event_type\": \"instance_ready\",
\"instance_id\": \"$INSTANCE_ID\",
\"hostname\": \"$HOSTNAME\",
\"timestamp\": \"$(date -Iseconds)\"
}"
runcmd:
- /opt/notify.sh
Debugging Cloud-init
Troubleshoot cloud-init execution.
View cloud-init logs:
# Main cloud-init log
cat /var/log/cloud-init-output.log
# Detailed log
cat /var/log/cloud-init.log
# System log
journalctl -u cloud-init -n 50
# Watch logs in real-time
tail -f /var/log/cloud-init.log
Check cloud-init status:
# Current status
cloud-init status
# Show all status
cloud-init status --long
# Check if running
cloud-init status | grep -q "running"
# Show available modules
cloud-init modules --list-sources
Enable debug logging:
#cloud-config
# Enable debug output
debug: true
# Set log level
output: {all: '>> /var/log/cloud-init-custom.log'}
Manual execution for testing:
# Clean up cloud-init state
sudo cloud-init clean --logs --seed
# Re-run cloud-init
sudo cloud-init init
sudo cloud-init modules --mode=config
sudo cloud-init modules --mode=final
# Or single run
sudo cloud-init -d single --name=runcmd
Validate cloud-config syntax:
# Check cloud-config validity
cloud-init schema --config-file user-data.txt --annotate
# Validate specific module
cloud-init schema --config-file user-data.txt --annotate | grep runcmd
Conclusion
Cloud-init provides a standard, simple way to automate server provisioning across cloud providers. By mastering cloud-config syntax, package management, user creation, script execution, and file provisioning, you can eliminate manual server setup and create reproducible infrastructure. Combined with infrastructure-as-code tools like Terraform, cloud-init forms a powerful foundation for automated, consistent infrastructure deployment at scale.


