Why cloud-init?
Last year I deployed thirty physical servers for use in an Openstack cluster, using Canonical’s MAAS. This was my first exposure to a technology that has been around for years now, cloud-init.
It’s not that I hadn’t heard the name before, I even had a vague idea about what it could do, but I had never used it and had no idea exactly how powerful it could be and how much time it could save on repeatable tasks.
Below I’ve included a few examples of scripts I have used to make my life easy.
Example 1 – Setup Extra Users & Install Packages
#cloud-config
---
ssh_pwauth: false
users:
- gecos: additional-user
groups: adm,cdrom,dip,lxd,plugdev,sudo
lock_passwd: false
sudo: ['ALL=(ALL) NOPASSWD:ALL']
name: additional-user
passwd: $6$kahwie4Faiyeiloh$m0N8nQF91Nf5a6P7mUSRtmf2iQLgrjyNAtlUv2nEye2HCsYKiq37fK4OCfJ4s.6yRJei9xj4xy3uOTePNpTx90
shell: /bin/bash
ssh-authorized-keys:
- ssh-rsa YourKeyHere
- gecos: ansible
groups: adm,cdrom,dip,lxd,plugdev,sudo
lock_passwd: false
sudo: ['ALL=(ALL) NOPASSWD:ALL']
name: ansible
passwd: $6$aiQui1aeloo4che0$atHuXsRnrVcVXnmyC0UwrVwKnSkxRovIq/ehmEajILX4EzfSjH6Sa8HR0OmcCuQ/sNk1cgItRoB1oiAlVVLTE1
shell: /bin/bash
ssh-authorized-keys:
- ssh-rsa YourKeyHere
package_upgrade: true
packages:
- ca-certificates
- snmpd
- unzip
- net-tools
- git
- make
- gcc
...
So what exactly are we doing?
- Disable password auth for SSH
- Create two new users, additional-user and ansible
- Both users have passwordless sudo
- Set authorized_keys for both
- Upgrade all packages on first boot
- Install the additional listed packages
As an aside, the passwords included in a cloud-init document are generated as follows
mkpasswd -m sha-512
Example 2 – CA Certificates & bash Scripts
#cloud-config
---
package_upgrade: true
packages:
- ca-certificates
ca_certs:
trusted:
- |
-----BEGIN CERTIFICATE-----
YourCertificateHere
-----END CERTIFICATE-----
runcmd:
- [ wget, "https://example.com/scripts/script.sh" ]
- [ chmod, +x, script.sh ]
- [ /bin/bash, script.sh ]
...
In this example we:
- Upgrade all packages on boot
- Install the ca-certificates package
- Add a local CA as trusted
- Download a bash script and run it
This is a great example, as the runcmd module allows us to do pretty much anything that is not part of an existing cloud-init module
Example 3 – Early Boot Commands, Write Files and Advanced Run Commands
#cloud-config
---
bootcmd:
- [ install, -m, "0755", -d, /etc/apt/keyrings ]
- [ curl, -fsSL, "https://download.docker.com/linux/ubuntu/gpg", -o, /etc/apt/keyrings/docker.asc ]
- [ chmod, a+r, /etc/apt/keyrings/docker.asc ]
- [ sh, -c, 'echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null' ]
- [ wget, -q, "https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb", -O, /tmp/microsoft.deb ]
- [ dpkg, -i, /tmp/microsoft.deb ]
package_upgrade: true
packages:
- wget
- jq
- apt-transport-https
- software-properties-common
- powershell
- docker-ce
- docker-ce-cli
- containerd.io
- docker-buildx-plugin
- docker-compose-plugin
write_files:
- path: /root/.bashrc
content: |
export OS_AUTH_URL=https://openstack.example.com
export OS_USER_DOMAIN_NAME="default"
export OS_PROJECT_DOMAIN_ID="projectdomainid"
export OS_USERNAME="admin-user"
export OS_PASSWORD="Password"
export OS_REGION_NAME="RegionOne"
export OS_INTERFACE=public
export OS_IDENTITY_API_VERSION=3
export OS_DOMAIN_NAME="default"
append: true
disable_root: false
users:
- name: root
lock_passwd: false
name: root
passwd: $6$Nv.R3ddl9EkAOJY2$9654rBbm9/gvbIxy0nXJdHkndFFKNbqTGfhIubMP2RswSzQ/GqNpgvhhBmNM8tXkFQabCjtEQsvVFnKq1Z4Gl0
shell: /bin/bash
ssh-authorized-keys:
- ssh-rsa authorizedkey1
- ssh-rsa authorizedkey2
runcmd:
- [ cd, /usr/lib64 ]
- [ wget, "https://example.com/downloads/VMware-vix-disklib.tar.gz" ]
- [ tar, xzvf, VMware-vix-disklib.tar.gz ]
- [ mv, vmware-vix-disklib-distrib/, vmware-vix-disklib/ ]
- [ rm, VMware-vix-disklib.tar.gz ]
- [ mkdir, -p, /mnt/config ]
- [ mount, /dev/sr0, /mnt/config ]
- [ sh, -c, 'projid=$(cat /mnt/config/openstack/latest/meta_data.json | jq -r .project_id) && echo "export OS_PROJECT_ID=${projid}" >> /root/.bashrc' ]
- [ umount, /mnt/config ]
- [ docker, pull, registry.atmosphere.dev/library/migratekit ]
- [ pwsh, -Command, "Set-PSRepository -Name PSGallery -InstallationPolicy Trusted" ]
- [ pwsh, -Command, "$ProgressPreference = 'Continue'; Write-Host 'Installing VMware.PowerCLI module - this may take several minutes...'; Install-Module -Name VMware.PowerCLI -Force -Scope AllUsers -Verbose" ]
- [ rm, -f, /tmp/microsoft.deb ]
- [ reboot ]
...
This is a very specific example to show the power of what cloud-init can achieve. The above script I used to set up migration environments to move VMs from VMWare to Openstack.
It does the following:
- Early in the boot process
- Adds the Docker and Microsoft repos
- Updates all packages
- Installs listed packages, including docker and powershell
- Writes Openstack details to bashrc for root
- Sets the root user to enabled, sets it’s password and authorized keys
- Downloads the VMWare virtual disk development kit and unpacks it
- Mounts the Openstack ConfigDrive and retrieves the parent projects ID
- Adds said ID to bashrc as well
- Pulls Vexxhost’s MigrateKit docker container
- Installs PowerCLI
- Reboots the machine
I had to set up quite a few of these VMs and using a cloud-init script like the above saved me a TON of time.
I want more what now?
You can check out the cloud-init documentation for dozens more examples, and a complete list of all included modules. There is so much more than I covered here and you could be reading for days
Alternatively for those so inclined, I have found asking LLMs to write these for you can have pretty good results as well.