4. Advanced provisioning
Networking and multi-part payloads¶
In the previous chapter, you mastered the core cloud-init modules for managing users, packages, and files. You can now build a well-configured server declaratively. Now, it is time to explore more advanced techniques that give you even greater control over your instance's configuration.
This chapter covers two powerful, advanced topics:
- Declarative Network Configuration: How to move beyond DHCP and define static network configurations for your instances.
- Multi-Part MIME Payloads: How to combine different types of user-data, such as shell scripts and
#cloud-configfiles, into a single, powerful payload.
1. Declarative network configuration¶
By default, the configuration of most cloud images is to acquire an IP address by DHCP. While convenient, many production environments require servers to have predictable, static IP addresses. The cloud-init network configuration system provides a platform-agnostic, declarative way to manage this.
The specification of the network configurations is in a separate YAML document from your main #cloud-config. cloud-init processes both from the same file, using the standard YAML document separator (---) to distinguish between them.
How cloud-init applies network state
On Rocky Linux, cloud-init does not directly configure the network interfaces. Instead, it acts as a translator, converting its network configuration into files that NetworkManager (the default network service) can understand. It then hands off control to NetworkManager to apply the configuration. You can inspect the resulting connection profiles in /etc/NetworkManager/system-connections/.
Example 1: Configuring a single static IP¶
In this exercise, we will configure our virtual machine with a static IP address, a default gateway, and custom DNS servers.
-
Create
user-data.yml:This file contains two distinct YAML documents, separated by
---. The first is our standard#cloud-config. The second defines the network state.cat <<EOF > user-data.yml #cloud-config # We can still include standard modules. # Let's install a network troubleshooting tool. packages: - traceroute --- # This second document defines the network configuration. network: version: 2 ethernets: eth0: dhcp4: no addresses: - 192.168.122.100/24 gateway4: 192.168.122.1 nameservers: addresses: [8.8.8.8, 8.8.4.4] EOF -
Key directives explained:
network:: The top-level key for network configuration.version: 2: Specifies that we are using the modern, Netplan-like syntax.ethernets:: A dictionary of physical network interfaces to configure, keyed by the interface name (e.g.,eth0).dhcp4: no: Disables DHCP for IPv4 on this interface.addresses: A list of static IP addresses to assign, specified in CIDR notation.gateway4: The default gateway for IPv4 traffic.nameservers: A dictionary containing a list of IP addresses for DNS resolution.
-
Boot and verify:
Verification is different this time, as the VM will not get a dynamic IP address. You must connect to the VM's console directly.
# Use a new disk image for this exercise qemu-img create -f qcow2 -F qcow2 -b Rocky-10-GenericCloud.qcow2 static-ip-vm.qcow2 virt-install --name rocky10-static-ip \ --memory 2048 --vcpus 2 \ --disk path=static-ip-vm.qcow2,format=qcow2 \ --cloud-init user-data=user-data.yml,hostname=network-server \ --os-variant rockylinux10 \ --import --noautoconsole # Connect to the virtual console virsh console rocky10-static-ip # Once logged in, check the IP address [rocky@network-server ~]$ ip a show eth0The output should show that
eth0has the static IP address192.168.122.100/24.
Example 2: Multi-interface configuration¶
A common real-world scenario is a server with multiple network interfaces. Here, we will create a VM with two interfaces: eth0 will use DHCP, and eth1 will have a static IP.
-
Create
user-data.ymlfor two interfaces:cat <<EOF > user-data.yml #cloud-config packages: [iperf3] --- network: version: 2 ethernets: eth0: dhcp4: yes eth1: dhcp4: no addresses: [192.168.200.10/24] EOF -
Boot a VM with two NICs: We add a second
--networkflag to thevirt-installcommand.virt-install --name rocky10-multi-nic \ --memory 2048 --vcpus 2 \ --disk path=... \ --network network=default,model=virtio \ --network network=default,model=virtio \ --cloud-init user-data=user-data.yml,hostname=multi-nic-server \ --os-variant rockylinux10 --import --noautoconsole -
Verify: SSH to the DHCP-assigned address on
eth0and then check the static IP oneth1withip a show eth1.
2. Unifying payloads with multi-part MIME¶
Sometimes, you need to run a setup script before the main #cloud-config modules execute. MIME multi-part files are the solution, allowing you to bundle different content types into one ordered payload.
You can visualize the structure of a MIME file as follows:
+-----------------------------------------+
| Main Header (multipart/mixed; boundary) |
+-----------------------------------------+
|
| --boundary |
| +-------------------------------------+
| | Part 1 Header (e.g., text/x-shellscript) |
| +-------------------------------------+
| | Part 1 Content (#/bin/sh...) |
| +-------------------------------------+
|
| --boundary |
| +-------------------------------------+
| | Part 2 Header (e.g., text/cloud-config) |
| +-------------------------------------+
| | Part 2 Content (#cloud-config...) |
| +-------------------------------------+
|
| --boundary-- (closing) |
+-----------------------------------------+
Hands-on: A pre-flight check script¶
We will create a multi-part file that first runs a shell script and then proceeds to the main #cloud-config.
-
Create the multi-part
user-data.mimefile:This is a specially formatted text file that uses a "boundary" string to separate the parts.
cat <<EOF > user-data.mime Content-Type: multipart/mixed; boundary="//" MIME-Version: 1.0 --// Content-Type: text/x-shellscript; charset="us-ascii" #!/bin/sh echo "Running pre-flight checks..." # In a real script, you might check disk space or memory. # If checks failed, you could 'exit 1' to halt cloud-init. echo "Pre-flight checks passed." > /tmp/pre-flight-status.txt --// Content-Type: text/cloud-config; charset="us-ascii" #cloud-config packages: - htop runcmd: - [ sh, -c, "echo 'Main cloud-config ran successfully' > /tmp/main-config-status.txt" ] --//-- EOFAbout the MIME boundary
The boundary string (
//in this case) is an arbitrary string that must not appear in the content of any part. It is used to separate the different sections of the file. -
Boot and verify:
You pass this file to
virt-installin the same way as a standarduser-data.ymlfile.# Use a new disk image qemu-img create -f qcow2 -F qcow2 -b Rocky-10-GenericCloud.qcow2 mime-vm.qcow2 virt-install --name rocky10-mime-test \ --memory 2048 --vcpus 2 \ --disk path=mime-vm.qcow2,format=qcow2 \ --cloud-init user-data=user-data.mime,hostname=mime-server \ --os-variant rockylinux10 \ --import --noautoconsoleAfter booting, SSH into the VM and check that both parts ran by looking for the files they created:
cat /tmp/pre-flight-status.txt cat /tmp/main-config-status.txt
Other Multi-Part Content Types
cloud-init supports other content types for advanced use cases, such as text/cloud-boothook for very early boot scripts or text/part-handler for running custom Python code. Refer to the official documentation for more details.
What's next¶
You have now learned two powerful, advanced cloud-init techniques. You can now define static networks and orchestrate complex provisioning workflows with multi-part user-data.
In the next chapter, we will shift our perspective from consuming cloud-init on a per-instance basis to customizing its default behavior for creating your own pre-configured "golden images".
Author: Wale Soyinka
Contributors: Steven Spencer