Cloud-init is an open source initialisation tool to get your systems configured and operational during the provisioning stage of the systems life cycle.

Cloud-init can handle a range of tasks including configuring the hostname, network interfaces, creating user accounts, installing packages and running scripts. Cloud-init can also hand-off to external configuration management platforms such as Chef, Ansible and SaltStack to enrol the system into the through-life configuration management platform.

Cloud-Init has become the defacto standard for system initialisation and is used by most if not all commercial cloud providers. The explanation below will refer to commercial cloud providers for ease of understanding however cloud-init is also used by platforms including VMWare, OpenStack, Proxmox, KVM and others.

Before diving into the detail I will cover off the essentials that aided in my understanding. This is not a comprehensive explanation but it will be sufficient to get you up and running, I will cover off the;

  1. Cloud-Init data types
  2. Cloud-Init execution sequence at a high level and
  3. The NoCloud datasource which allows cloud-init to function with no external dependencies
  4. Tips and tricks I learnt along the way

Cloud-Init Data Types

Cloud-Init offers three different data types. Cloud-Init was developed from the perspective of a commercial cloud provider where certain configuration options are required to be defined by the commercial cloud provider whereas others may be optional and if desired could e overridden by the user operating the instance.

Metadata

Metadata is used to bootstrap the cloud-init execution flow and defines amongst other things the location of the cloud-init vendor-data and user-data files. In the context of a commercial cloud provider the metadata file commonly includes information about the instance such as the operating system type, architecture and version, region and IP addressing. Metadata provides information that can be referenced by vendor-data or user-data to further customise the configuration based on these values. Commercial cloud providers typically run a metadata service that serves the metadata and vendor-data as well as the user-data over HTTP directly to the instance.

Vendor Data

Vendor-data is data provided by the commercial cloud provider. Vendor data allows the cloud provider to configure the instance with platform or region specific configuration such as local package mirrors or NTP server details. Vendor data different to metadata in that it optional and user-data configuration always take precedence over vendor-data.

User Data

User-data is data provided by the user who operates the instance. User-data configuration options are expansive. User-data may include the creation of users, groups, files and directories, the configuration of networking, the additional and removal of packages and more. It is important to take into consideration that cloud-init is intended to be executed only once during system initialisation, cloud-init should not be treated as a replacement for platforms such as Ansible but rather complimentary to.

Cloud-Init Execution

Cloud-init commences it’s execution and attempts to discover the datasource that will provide the cloud-init configuration data. Typically the datasource will be provided through a metadata service operated by the commercial cloud provider however the datasource can be defined manually for on-premise as well offline deployments.

Once the datasource is identified, cloud-init fetches the vendor-data and user-data configuration files.

Internally, cloud-init builds a single configuration from multiple source files such that if a key is defined in multiple sources, the higher priority source overrides the lower priority source. This behaviour tripped me up when dividing user-data across multiple files which resulted in a user account defined in one file taking precedence over a user account defined in another file. The solution to this problem is using cloud-inits merge feature to append select files.

When debugging errors due to precedence keep in mind that from lowest priority to highest, configuration sources are:

Hardcoded Values: Hardcoded config that lives within the source code of cloud-init and cannot be changed.

Configuration Files: Anything defined in /etc/cloud/cloud.cfg and /etc/cloud/cloud.cfg.d/*.cfg which is further prioritised based on the numerical value in the filename

Runtime Configuration: Anything defined in ‘/run/cloud-init/cloud.cfg’.

Kernel Command Line: On the kernel command line, anything found between cc: and end_cc will be interpreted as cloud-config user-data.

With the essentials out the way let’s move into configuring cloud-init for a local on-disk deployment.

The NoCloud Datasource

In my specific scenario I am migrating my infrastructure to use bootc and wanted an approach that enabled me to build my bootable containers and drop in cloud-init configuration files in both my customised bootc base container and then additional cloud-init configuration files based on the customised required for the specific instance.

I have opted to include all my configuration in the default location of /etc/cloud/cloud.cfg.d/ but a custom path can be used if desired. I then divided by configuration across files for ease of maintenance.

The first file 00_nocloud.cfg is used by cloud-init it to identify where to locate the user-data file. This replaces the metadata service operated by the commercial cloud provider.

datasource:
  NoCloud:
  seedfrom: file:///etc/cloud/cloud.cfg.d/

The second file 09_default_redhat_settings.cfg mirrors the defaults provided in /etc/cloud/cloud.cfg that are Red Hat specific with some customisation aligned to my preferences. Note that keys in /etc/cloud/cloud.cfg.d/ are given priority over /etc/cloud/cloud.cfg thereby ensuring the values here are applied.

# Default redhat settings:
ssh_deletekeys: true
ssh_genkeytypes: ['ed25519']
syslog_fix_perms: ~
disable_vmware_customization: true

Beyond these two files I have a series of files to customise the base image as I see fit. I will include an example below for completeness. Keep in mind that the configurable options in cloud-init are expansive. I have opted to limit my use of cloud-init based on the limitations and recommendations for bootc. I generally have a preference for using drop-in files which are better managed through the life of the instance.

11_base_cloud-init_ansible.cfg is used to create a user named Ansible for use my my through-life configuration management platform. I have also opted to use cloud-init to handle any user specific file creation. In this case I use systemd-tmpfiles to keep the Ansible users home directory clean of any cruft, I like to actively discourage any files sitting in users home directories to drive myself toward automation and git.

Note the highlighted lines 2-6, the merge_how option enables mutiple user-data files to be appended preventing one file clobbering another.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#cloud-config
merge_how:
 - name: list
   settings: [append]
 - name: dict
   settings: [no_replace, recurse_list]

users:
# Create ansible user account
  - name: ansible
    homedir: /var/home/ansible
    sudo: ["ALL=(ALL) ALL"]
    ssh_authorized_keys:
      - ecdsa-sha2-nistp521 AAABE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHG1MjEAAACFBAFOKC0ezYBxRIntsWR3Ii0JSTYLEVpxiUbkuEcaNAF9fcWThEdANZfgxq7z1eDmmrzqgmTZuz7oK0rFcGphskZqNwHhfAyYuoSIgpHxosBDXjtiuHfvbgK0XHLzzHiTTaBAq43Y6Uqdc64x/7GwaEwurGY2JZ1qTLuTwaGyouS4esfLNw== ansible@automation

# Setup tmpfiles.d to clean ansibles home directory
write_files:
  - path: /var/home/ansible/.local/share/user-tmpfiles.d/home.conf
    permissions: "0644"
    content: |
      #Type	Path			Mode	User	Group	Age
      d	/var/home/ansible/		-	-	-	1w
      x	/var/home/ansible/.*	-	-	-	-
    owner: ansible:ansible
    defer: true

With all the above in place all that is required is to ensure that the cloud-init package is installed and you’re off to the races.

Tips and Tricks

  1. Ensure that the first line in every user-data (and vendor-data) file contains the string #cloud-config. Without this marker cloud-init will silently fail. Reference the cloud-init documentation to understand what configuration items are sourced from which data type. For example the file specifying the datasource location is a metadata file and does not require #cloud-config to present.

  2. Prior to attempting to use each user-data, make sure that the file is formatted properly.

# cloud-init schema --config-file /etc/cloud/cloud.cfg.d/11_base_cloud-init_ansible.cfg 
Valid schema /etc/cloud/cloud.cfg.d/11_base_cloud-init_ansible.cfg
  1. Use the cloud-init status option to quickly determine whether cloud-init execution was successful
# cloud-init status
status: done
  1. To debug cloud-init further you can review the logs files at /var/log/cloud-init*