Boot-Time Configuration for On-Premise Linux Servers: Unlocking the Power of cloud-init

Share this post on:

Back in the early days, configuring systems at boot time was almost unheard of. Users who required custom configurations often had to rely on pre-built images or run shell scripts manually after the system had fully started. For those who needed a streamlined setup, these methods were far from ideal, demanding a lot of repetitive manual effort.

The introduction of configuration management tools like Ansible and Puppet brought some relief, as users could automate system configuration after the machine was up and running. However, even with these tools, there was still a gap: how to reliably trigger the configuration process as soon as the system boots.

This is where cloud-init comes in. Originally developed to address configuration needs in cloud environments, cloud-init allows Linux systems to be configured automatically at boot. It does this in two primary ways: through cloud-config files and user data. User data, especially popularized by AWS EC2, lets users inject commands or scripts that execute at boot, enabling custom setups on demand.

In this article, we’ll explore how cloud-init can be used to configure systems at boot, giving you the power to automate customizations for on-premise setups just as easily as for cloud deployments.

Installing cloud-init for RHEL and Debian Based Linux Systems

To use cloud-init on your on-premise systems, you’ll need to ensure it’s installed. The installation process is straightforward and supported on both RHEL and Debian-based distributions.

For RHEL-based Distributions (e.g., RHEL, CentOS, Fedora)

  1. Update the Package Repository and proceed to install cloud-init:
sudo yum update -y
sudo yum install -y cloud-init
sudo systemctl enable cloud-init
sudo systemctl start cloud-init
cloud-init --version

For Debian-based Distributions (e.g., Ubuntu, Debian)

  1. Update the Package Repository and proceed to install cloud-init:
sudo apt update
sudo apt install -y cloud-init
sudo systemctl enable cloud-init
sudo systemctl start cloud-init
cloud-init --version

The cloud-init directory structure is organized to support its configuration, logging, and modular operation. Here’s an overview of the key directories and their purposes:

  1. /etc/cloud/
    This is the main configuration directory for cloud-init. It contains subdirectories and files that define cloud-init’s behavior on the system.
    • /etc/cloud/cloud.cfg: The primary configuration file for cloud-init, which specifies the default configuration modules and order of operations.
    • /etc/cloud/cloud.cfg.d/: Contains additional configuration snippets to customize cloud-init’s behavior without modifying the main cloud.cfg file.
    • /etc/cloud/templates/: Stores templates used by cloud-init for generating files, like network configurations.
  2. /var/lib/cloud/
    This directory is where cloud-init stores its runtime data, including instance metadata and state files, which track cloud-init’s progress and avoid re-running certain tasks.
    • /var/lib/cloud/scripts/: Contains folders where we can store shell scripts that will run at boot time. Our focus will concentrate here later.
    • /var/lib/cloud/seed/: Stores seed data, which includes metadata and user data that cloud-init uses to configure the instance at boot.
    • /var/lib/cloud/instance/: Holds data for the current instance, such as user data scripts and cloud-config files.
  3. /var/log/cloud-init-output.log
    This log file records cloud-init’s operations and is helpful for troubleshooting configuration or boot-time errors.

These directories and files provide cloud-init with the configuration it needs to manage and track system initialization processes. The modular structure allows administrators to easily customize or troubleshoot cloud-init’s behavior.

How can we use Cloud-init to our advantage in On-premise environments?

For on-premise systems, there are three key ways to implement boot-time configuration using cloud-init. Each offers a unique approach to customizing and automating setups:

Using cloud-init declarative approach to system configuration

When creating custom images with a tool like Packer from Hasicorp, you can include cloud-init configuration files baked directly into the image. This method allows you to pre-define system settings, user accounts, and initial packages, ensuring that every instance launched from this image starts with a consistent configuration. The example below explorers this approach.
Create a YAML file named 99_custom_config.cfg with the following content to configure basic settings like creating a user, setting the hostname, and installing packages.

#cloud-config
# Set the hostname
hostname: my-onprem-instance

# Create a new user with sudo privileges
users:
  - name: myuser
    gecos: "OnPrem User"
    sudo: ['ALL=(ALL) NOPASSWD:ALL']
    shell: /bin/bash
    ssh_authorized_keys:
      - ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAr3...

# Update and install packages
package_update: true
packages:
  - git
  - vim
  - curl

# Run additional commands at first boot
runcmd:
  - echo "Welcome to your pre-configured instance!" > /etc/motd
  - systemctl restart sshd

At build time, we can copy this configuration file to /etc/cloud/cloud.cfg.d/ for example /etc/cloud/cloud.cfg.d/99_custom_config.cfg
As mentioned already, the configuration itself is straight forward;
– We create a user: myuser
– We install a couple of packages – git, vim and curl
– Finally we create a custom motd and restart sshd service.
Once baked into an image for our environment, every instance created off this image will automatically apply these settings at boot.

Running shell scripts with cloud-init

If we prefer not to use the declarative style of cloud-init configuration, we can also execute shell scripts at boot through cloud-init. This approach allows us to reuse existing, functional shell scripts without needing to convert them into the cloud-init declarative format. Let’s look at the example shell script below.

#!/bin/bash

# Set the hostname
hostnamectl set-hostname my-onprem-instance

# Create a new user with sudo privileges
USERNAME="myuser"
USER_SSH_KEY="ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAr3..."

# Check if the user already exists
if id "$USERNAME" &>/dev/null; then
    echo "User $USERNAME already exists."
else
    echo "Creating user $USERNAME..."
    useradd -m -s /bin/bash -G sudo "$USERNAME"
    echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/$USERNAME
    mkdir -p /home/$USERNAME/.ssh
    echo "$USER_SSH_KEY" > /home/$USERNAME/.ssh/authorized_keys
    chmod 700 /home/$USERNAME/.ssh
    chmod 600 /home/$USERNAME/.ssh/authorized_keys
    chown -R $USERNAME:$USERNAME /home/$USERNAME/.ssh
fi

# Update package list and install packages
apt update -y && apt install -y git vim curl

# Display a custom message in the MOTD
echo "Welcome to your pre-configured instance!" | sudo tee /etc/motd

# Restart the SSH service to apply any SSH changes
systemctl restart sshd

echo "Configuration complete."

During the image baking process, we can copy this configuration file to /var/lib/cloud/scripts/per-boot/ for example/var/lib/cloud/scripts/per-boot/boot-script.sh. Make sure its executable (chmod +x ). Just like in the previous example, every instance created from this image will run this shell script at boot.

Combining cloud-init with Ansible for Extensible Configuration

This is my favorite one! Combining cloud-init with Ansible offers a flexible, scalable approach to system configuration. This method leverages cloud-init for essential boot-time setup and then invokes Ansible to perform more complex or variable configurations, providing a seamless, automated workflow for provisioning.

Let’s look at how this combination might work. The idea here is to use cloud-init to prepare the system, then install and execute an Ansible playbook to complete the configuration.

Step 1: Create a cloud-init Configuration File to Install Ansible and Run a Playbook

Here’s an example cloud-init configuration that installs Ansible and runs an initial playbook:

#cloud-config
# Basic system setup using cloud-init

# Set hostname and create a user
hostname: my-onprem-instance
users:
  - name: ansible_user
    gecos: "Ansible User"
    sudo: ['ALL=(ALL) NOPASSWD:ALL']
    shell: /bin/bash
    ssh_authorized_keys:
      - ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAr3...

# Install necessary packages
packages:
  - ansible
  - git

# Run additional commands after package installation
runcmd:
  # Clone the Ansible playbook repository
  - git clone https://github.com/example/ansible-playbook-repo.git /home/ansible_user/ansible_playbook

  # Change to the playbook directory and run the playbook
  - cd /home/ansible_user/ansible_playbook
  - ansible-playbook -i inventory playbook.yml

In this setup, Ansible and Git are installed as the instance boots, after which we pull an Ansible playbook (playbook in the example below) from our Git repository. This can also be an internal Git repository if preferred.

Step 2: Create an Ansible Playbook for Advanced Configuration

Now, let’s assume you have an Ansible playbook named playbook.yml in your Git repository. This playbook can perform additional configuration tasks, like setting up services, managing firewall rules, or installing applications.

Here’s a basic example of what playbook.yml might look like:

---
- name: Post-Boot Configuration
  hosts: localhost
  become: yes

  tasks:
    - name: Install Nginx
      apt:
        name: nginx
        state: present
      when: ansible_os_family == 'Debian'

    - name: Start and enable Nginx
      service:
        name: nginx
        state: started
        enabled: yes

Final Result

By combining cloud-init and Ansible, you achieve a layered approach:

  1. cloud-init prepares the system with basic configurations and installs Ansible.
  2. Ansible playbooks are then run, handling complex or specific configuration needs that go beyond what cloud-init typically addresses.

This approach is especially useful because if our requirements evolve, we can simply modify the Ansible playbook without the need to rebuild the image. This flexibility saves time and effort, ensuring that our configurations stay up-to-date and adapt easily to changing needs.

Conclusion

In conclusion, cloud-init offers a powerful solution for on-premise systems, bringing the same level of automated, boot-time configuration commonly associated with cloud environments. By using cloud-init in various ways—whether through declarative configurations in pre-baked images, executing shell scripts at boot, or in combination with Ansible for more extensible setups—IT administrators gain a flexible, scalable approach to system management and customization.

Share this post on: