OpenStack Ironic: first contact
A simplified guide to getting started with OpenStack Ironic “standalone”
OpenStack Ironic: first contact
A simplified guide to getting started with OpenStack Ironic “standalone”

Obi-Wan Ironic orchestrating the deployment of hundreds of physical nodes…
Managing physical servers as if they were virtual machines (VMs) in a cloud environment? Infrastructure-as-Code (IaC) and CI/CD Pipelines with physical nodes? Let’s try to demystify some preconceptions about private cloud infrastructures and bare metal lifecycle with practical, simple, and reproducible examples, starting with a minimal OpenStack Ironic “standalone” setup.
By the end of this lab, we will be able to:
- Install and manage a simple Bare Metal Provisioning system using OpenStack Kolla and Ironic.
- Manage a generic lifecycle for physical servers: onboarding, resource collection, operating system installation, recovery in case of malfunction, decommissioning.
- Apply Infrastructure as Code methodologies for node and application provisioning.
Subsequently, once familiar with Ironic in “standalone” mode, it will be simpler to integrate it with other OpenStack services such as Keystone, Cinder, Nova, and Neutron, to expand its functionalities and transform the data center into a real private cloud capable of managing bare metal workloads as well as VMs.
Introduction
Anyone who deals daily with physical servers, manual provisioning, drivers, BIOS, and the complexity of keeping an extensive fleet of machines updated, knows how time-consuming and error-prone bare metal management (i.e., physical servers without a pre-installed operating system) can be.
You’re surely familiar, at least in theory, with **OpenStack, the open-source cloud service suite that allows you to manage virtual resources (VMs, network, storage) programmatically and “as-a-Service.” [Ironic](https://wiki.openstack.org/wiki/Ironic)** is simply the OpenStack component that extends this philosophy to the physical world.
OpenStack Ironic is a bare metal provisioning and management service that integrates physical servers into the OpenStack framework, treating them as cloud resources. This means a physical server can be requested, allocated, configured, and released via API, just as you would with a virtual machine.
This isn’t about virtualization: Ironic doesn’t create VMs on physical hardware. Ironic manages the physical hardware itself, installing the operating system directly on the machine, without intermediate virtualization layers.
Managing the Bare Metal Lifecycle
Traditionally, managing the lifecycle of a bare metal server is a process involving several phases, often manual and time-consuming:
- Purchase and Physical Installation: Receiving, mounting, cabling.
- BIOS/UEFI Configuration: Manual access, specific settings.
- RAID Controller Configuration: Array creation, disk configuration.
- Operating System Installation: Boot from CD/USB/PXE, manual or semi-automatic installation.
- Post-Installation Configuration: Drivers, software, network, security hardening.
- Maintenance and Updates: Patching, firmware upgrades.
- Decommissioning: Disk wipe, removal.
With Ironic, the entire process transforms, embracing the “as-a-Service” paradigm:
- Discovery and Registration (Onboarding): Machines are “discovered” by Ironic (often via PXE boot and an agent) and registered in its inventory. Once registered, Ironic can directly interact with their BMC (Baseboard Management Controller, such as iLO, iDRAC, IMM).
- “As-a-Service” Provisioning: A user (or an automated service) can request a physical server with specific characteristics (CPU, RAM, storage) via OpenStack APIs. Ironic selects an available server, powers it on, installs the chosen operating system (GNU/Linux, Windows, VMware ESXi, etc.), configures the network, and makes it available in a short time.
- Remote and Automated Management: Thanks to integration with BMCs, Ironic can programmatically perform operations such as power on/off, reboot, reset, boot device management, firmware updates (via plugins), and even BIOS/UEFI settings, all without any manual intervention.
- Decommissioning and Cleaning: When a server is no longer needed, Ironic can perform secure disk wiping and reset BIOS/UEFI settings, returning the server to a “clean” state available for reuse or final decommissioning, all automatically.
- Integration with Other OpenStack Services: Last but not least, Ironic can integrate with **Neutron for physical network management, Glance for operating system image management, Cinder to provide persistent storage, and Horizon** for a unified GUI. This allows for building complex solutions where bare metal is an extension of your cloud infrastructure, enabling not only the consumption of resources as-a-Service but also automated interactions like CI/CD pipelines.
1 Lab Environment
Let’s consider a simplified environment, so we can set it up with minimal effort and focus primarily on the bare metal provisioning mechanism:

- LAN Network
192.168.0.0/24: Also referred to as the public network, this will be unified for clients and servers and connected to the internet via a router (.254) that issues IP addresses via DHCP within the range x.x.x.101-199. - OOB/IB Network
172.19.74.0/24: This separate network is managed by a small server (labeled as “bms” in the figure) to control the provisioning of bare metal nodes. These nodes are connected via BMC for Out-of-Band management (iLO, iDRAC, IMM, etc.) and via an Ethernet network card for In-Band management (deployment, inspection, rescue, etc.).
Both switches are unmanaged and without VLANs, allowing for the use of “off-the-shelf” hardware or easy simulation with enterprise equipment or, alternatively, with a virtualized environment.
1.1 Server for the Bare Metal Service
The bms server, where the bare metal provisioning environment will be installed, doesn’t require extensive resources. The minimum necessary specifications are 4GB of RAM, 2 CPU cores, 2 NICs, and at least 200GB of disk space (needed to host and build OS images and containers). However, it’s recommended to use at least 8GB of RAM and 4 CPU cores to allow for future expansion of the environment with additional services.
On the software side, the following main components will be used:
- **OpenStack 2026.1 (Gazpacho)**: As of this writing, this is the most recent OpenStack release for which ready-to-use containers have been released for Kolla and Kolla Ansible (the system used here for deploying OpenStack itself).
- **Debian GNU/Linux 13 (Trixie)**: Although OpenStack is largely agnostic to various GNU/Linux distributions (especially when installed via containers), Debian will be used to avoid favoring commercial distributions.
- **Docker: Docker is the de facto standard to build and share containerized apps; [Podman](https://podman.io)** is a valuable alternative.
2 Creating the Lab Environment
- Installation of the bms server;
- Installation of the Docker Engine;
- Creation of a kolla administration user;
- Creation of a Python Virtual Environment;
- Installation of OpenStack Kolla Ansible;
- Minimal configuration for Bare Metal Provisioning;
- Deployment of OpenStack.
2.1 Server Installation (bms)
For the operating system installation, choose your preferred GNU/Linux version and adapt the configurations accordingly:
root@bms:~# cat /etc/network/interfaces
auto lo
iface lo inet loopback
auto enp1s0
iface enp1s0 inet static
address 192.168.0.13/24
gateway 192.168.0.254
auto enp2s0
iface enp2s0 inet static
address 172.19.74.1/24
root@bms:~# cat /etc/hosts
127.0.0.1 localhost
172.19.74.1 bms.ironic.lab bms
root@bms:~# hostname -i
172.19.74.1
root@bms:~# hostname -f
bms.ironic.lab
enp1s0 (192.168.0.13/24): LAN networkenp2s0 (172.19.74.1/24): OOB/IB network
It’s crucial for the Kolla Ansible project that the server name — bms and bms.ironic.lab in this scenario – resolves to the IP address where the internal OpenStack services will be listening (172.19.74.1). In the proposed lab, this IP address coincides with the OOB/IB management network.
2.2 Docker
The Docker Engine can be installed very easily in quite any GNU/Linux distribution. Please, refers to the official documentation for specific details:
root@bms:~# curl -fsSL https://download.docker.com/linux/debian/gpg \
-o /etc/apt/keyrings/docker.asc
root@bms:~# cat >/etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/debian
Suites: $(. /etc/os-release && echo "$VERSION_CODENAME")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
root@bms:~# apt update && apt install -y \
docker-ce \
docker-ce-cli \
containerd.io
...
root@bms:~# docker info
Client: Docker Engine - Community
Version: 29.5.3
Context: default
Debug Mode: false
...
Alternatively, **Podman is an easy to use drop-in replacement for Docker**. Installation is quite straightforward as it’s included as a standard package in most GNU/Linux distributions.
2.3 Administration User
Creating an administrative user (e.g. kolla), with which to perform various operations, is always a good practice in terms of security and functionality to keep the deployment environment separate from your personal user or the root user:
root@bms:~# useradd --system --add-subids-for-system \
-d /var/lib/kolla \
-c 'Kolla Administrator' \
-s /bin/bash \
-m kolla
root@bms:~# passwd kolla
New password: *********
Retype new password: *********
passwd: password updated successfully
root@bms:~# usermod -aG docker kolla
NOTE: the
--add-subids-for-systemoption is necessary to allow the execution of **rootless containers**, even for system users, when using Podman as an alternative to Docker
Ensure that the user is able to execute commands as root via sudo:
root@bms:~# echo 'kolla ALL=(ALL:ALL) NOPASSWD:ALL' >/etc/sudoers.d/kolla
Now we can connect with the kolla administration user and proceed with the server configuration.
2.4 Python Virtual Environment (venv)
The OpenStack components for bare metal provisioning and for deploying OpenStack itself are also available as **PyPI packages. In most recent GNU/Linux distributions, installing Python packages at the system level is strongly discouraged. The most convenient way to have a Python environment with the necessary software, without “polluting” the system’s Python environment, is to create a Python virtual environment **associated with the previously created administration user:
kolla@bms:~$ sudo apt install -y \
python3-dbus \
python3-venv
...
kolla@bms:~$ python3 -m venv --system-site-packages $HOME
...
kolla@bms:~$ cat >>~/.profile <<EOF
# activate the Python Virtual Environment
if [ -f "\$HOME/bin/activate" ]; then
. "\$HOME/bin/activate"
fi
EOF
kolla@bms:~$ source ~/bin/activate
kolla@bms:~$ python3 -m pip install -U pip
Modifying the .profile file in the user’s home directory enables automatic activation of the virtual environment upon each login. Meanwhile, using the --system-site-packages option allows access to system Python libraries (e.g. python3-dbus) without needing to reinstall them.
NOTE: To install the
dbus-pythonpackage (the equivalent of the system packagepython3-dbus) within the virtual environment, thereby avoiding the--system-site-packagesoption, you need to install the development tools:sudo apt install build-essential pkg-config libdbus-1-dev libglib2.0-dev python3-dev. If you choose this path, it’s a good idea to remove the compilers after installation in production environments.
2.5 OpenStack Kolla Ansible
The primary goal of the Kolla Ansible project is to simplify and maximize the efficiency of OpenStack’s configuration and management. To achieve this, it leverages **Ansible for automation and reproducibility, and encapsulates OpenStack components within Docker containers** to streamline their deployment, updating, and scalability.
Kolla Ansible relies on a specific and strict chaining of Python packages and their respective versions. Using the previously configured Python Virtual Environment helps maintain separation and consistency between various versions of the kolla-ansible package, particularly Ansible itself and its various Ansible Galaxy library dependencies.
kolla@bms:~$ sudo apt install -y git tree yq
...
kolla@bms:~$ python3 -m pip install kolla-ansible docker podman
...
kolla@bms:~$ kolla-ansible --version
kolla-ansible 22.0.0
kolla@bms:~$ kolla-ansible install-deps
Installing Ansible Galaxy dependencies
Starting galaxy collection install process
Process install dependency map
...
kolla@bms:~$ ansible --version
ansible [core 2.20.6]
config file = /var/lib/kolla/.ansible.cfg
configured module search path = ['/var/lib/kolla/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /var/lib/kolla/lib/python3.13/site-packages/ansible
ansible collection location = /var/lib/kolla/.ansible/collections:/usr/share/ansible/collections
executable location = /var/lib/kolla/bin/ansible
python version = 3.13.5 (main, May 5 2026, 21:05:52) [GCC 14.2.0] (/var/lib/kolla/bin/python3)
jinja version = 3.1.6
pyyaml version = 6.0.2 (with libyaml v0.2.5)
NOTE: The
treeandyqpackages are not essential; they contain utilities used in this article for directory inspection and simplifying the management of YAML and JSON files.
Let’s install the OpenStack command-line client:
kolla@bms:~$ python3 -m pip install \
-c https://releases.openstack.org/constraints/upper/master \
python-openstackclient \
python-ironicclient
Optionally, if you want to enable command auto-completion via the [tab] key upon login, you can create the following files and directories:
kolla@bms:~$ mkdir ~/.bash_completion.d
kolla@bms:~$ openstack complete >~/.bash_completion.d/openstack
kolla@bms:~$ cat >~/.bash_completion <<EOF
# load local bash completion
if [[ -d ~/.bash_completion.d ]]; then
for i in ~/.bash_completion.d/*; do
[[ -f \$i && -r \$i ]] && . "\$i"
done
fi
EOF
To configure Kolla Ansible, you need to provide a pair of YAML files, globals.yml and passwords.yml, which define the configuration and the set of credentials associated with the system users of the various enabled components. Additionally, an Ansible inventory file is required. All these files should be placed in the default directory /etc/kolla or in the directory specified by the $KOLLA_CONFIG_PATH environment variable.
Leveraging the kolla system user’s home directory, we’ll place all configuration files in /var/lib/kolla/etc.
NOTE: Having a configuration directory different from the default (
/etc/kolla) prevents accidental deletion of its files if the Kolla environment is reset (e.g. using thekolla-ansible destroycommand).
For the inventory, we’ll use the provided example **all-in-one** file, similar to the passwords.yml file. The latter is a template that will be populated later with the kolla-genpwd command:
kolla@bms:~$ export KOLLA_CONFIG_PATH=~/etc
kolla@bms:~$ cat >>~/.bashrc <<EOF
# Kolla Ansible default config path
export KOLLA_CONFIG_PATH=~/etc
EOF
kolla@bms:~$ cp -a ~/share/kolla-ansible/etc_examples/kolla $KOLLA_CONFIG_PATH
kolla@bms:~$ kolla-genpwd --passwords $KOLLA_CONFIG_PATH/passwords.yml
kolla@bms:~$ mkdir -p $KOLLA_CONFIG_PATH/ansible/inventory
kolla@bms:~$ ln -s ~/share/kolla-ansible/ansible/inventory/all-in-one $KOLLA_CONFIG_PATH/ansible/inventory/
Meanwhile, we will rewrite the globals.yml configuration file with a minimal version:
---
# Host config
kolla_base_distro: "debian"
kolla_container_engine: "docker"
#kolla_container_engine: "podman"
# Use quay.io/openstack.kolla images (testing purpose)
kolla_test_images: true
# OpenStack 'master' or release version
openstack_release: "2026.1"
#openstack_tag_suffix: "-aarch64"
# OpenStack services
enable_fluentd: false
enable_haproxy: false
enable_memcached: false
enable_proxysql: false
enable_openstack_core: false
enable_ironic: true
# Kolla config
config_owner_user: "kolla"
network_interface: "enp2s0"
kolla_internal_vip_address: "172.19.74.1"
# Ironic config
ironic_dnsmasq_interface: "{{ network_interface }}"
ironic_dnsmasq_dhcp_ranges:
- range: "172.19.74.101,172.19.74.199,255.255.255.0"
NOTE: If you’re using a system based on ARM processors, the containers must also adhere to the same architecture. Simply uncomment the line
openstack_tag_suffix: "-aarch64"to download the correct containers.
Before using the Ansible playbooks and trying to decipher any error messages expressed in JSON, it might be convenient to use a slightly more readable output format, such as YAML:
kolla@bms:~$ cat >~/.ansible.cfg <<EOF
[defaults]
callback_result_format = yaml
EOF
To verify that everything is correct, let’s run the kolla-ansible prechecks command:

The error message we receive in red indicates the absence of two files that represent the Ironic Python Agent (IPA) kernel and ramdisk:
failed: [localhost] (item=ironic-agent.kernel) =>
ansible_loop_var: item
changed: false
failed_when_result: true
item: ironic-agent.kernel
msg: 'Task failed: Action failed: Unknown error.'
stat:
exists: false
failed: [localhost] (item=ironic-agent.initramfs) =>
ansible_loop_var: item
changed: false
failed_when_result: true
item: ironic-agent.initramfs
msg: 'Task failed: Action failed: Unknown error.'
stat:
exists: false
As the name suggests, it is an agent written in Python, which acts as an intermediary between the bare metal server and OpenStack Ironic. It consists of a live, in-memory GNU/Linux operating system that is loaded onto the server to be managed via network boot (PXE).
For now, we will download a pre-built one from the OpenStack archive (https://tarballs.opendev.org/openstack/ironic-python-agent/dib/files/):
kolla@bms:~$ mkdir -p $KOLLA_CONFIG_PATH/config/ironic
kolla@bms:~$ curl https://tarballs.opendev.org/openstack/ironic-python-agent/dib/files/ipa-debian-stable-2026.1.kernel \
-o $KOLLA_CONFIG_PATH/config/ironic/ironic-agent.kernel
kolla@bms:~$ curl https://tarballs.opendev.org/openstack/ironic-python-agent/dib/files/ipa-debian-stable-2026.1.initramfs \
-o $KOLLA_CONFIG_PATH/config/ironic/ironic-agent.initramfs
These two files (ironic-agent.kernel and ironic-agent.initramfs) must be placed in the $KOLLA_CONFIG_PATH/config/ironic/ directory. They will later be replaced with a custom version created specifically for our needs.
Now we can rerun the kolla-ansible prechecks command:

NOTE: Every reported error must be corrected, and the prechecks step re-executed until a clean result (
**errors=0**) is achieved.
2.6 OpenStack Ironic “Standalone” Configuration
In “standalone” mode, Ironic can operate independently of other OpenStack services. This allows you to use its functionalities without needing to install the entire cloud platform (though you do forgo a range of advanced features that a Private Cloud environment could offer). For this reason, the $KOLLA_CONFIG_PATH/globals.yml file contains the following lines:
...<SNIP>...
enable_openstack_core: false
enable_ironic: true
...<SNIP>...
The enable_openstack_core: **false option disables all components that would otherwise be automatically installed by Kolla Ansible, including Keystone, Glance, Nova, Neutron, Heat, and Horizon. Conversely, `enable_ironic: true`** enables only Ironic.
2.6.2 Ironic Configuration
The $KOLLA_CONFIG_PATH/config/ironic.conf configuration file must contain specific directives to enable the types of BMCs to be supported (Ironic refers to these as Hardware Types) and other instructions on how to manage specific areas.
For each area managed by Ironic, there are corresponding “interfaces” to control actions:
- BIOS: Manages BIOS settings.
- Boot: Provides boot mechanisms (e.g. PXE, virtual-media, etc.).
- Console: Accesses the serial console.
- Deploy: Manages node installation and cleaning.
- Firmware: Updates BIOS/UEFI, RAID controllers, etc.
- Inspect: Collects hardware configurations.
- Management: Manages server boot mode.
- Network: Interacts with OpenStack’s networking service.
- Power: Manages power state.
- RAID: Configures RAID volumes.
- Rescue: Provides recovery functionalities.
- Storage: Interacts with OpenStack’s storage system.
- Vendor: Offers additional vendor-specific functionalities.
[DEFAULT]
enabled_hardware_types = ipmi,redfish
enabled_bios_interfaces = no-bios
enabled_boot_interfaces = ipxe
enabled_console_interface = no-console
enabled_deploy_interfaces = direct
enabled_firmware_interfaces = no-firmware
enabled_inspect_interfaces = agent
enabled_management_interfaces = ipmitool,redfish
enabled_network_interfaces = noop
enabled_power_interfaces = ipmitool,redfish
enabled_raid_interfaces = agent
enabled_rescue_interfaces = agent
enabled_storage_interfaces = noop
enabled_vendor_interfaces = ipmitool,redfish
[conductor]
deploy_kernel = http://{{ ironic_http_interface_address }}:{{ ironic_http_port }}/ironic-agent.kernel
deploy_ramdisk = http://{{ ironic_http_interface_address }}:{{ ironic_http_port }}/ironic-agent.initramfs
rescue_kernel = http://{{ ironic_http_interface_address }}:{{ ironic_http_port }}/ironic-agent.kernel
rescue_ramdisk = http://{{ ironic_http_interface_address }}:{{ ironic_http_port }}/ironic-agent.initramfs
In the [conductor] section, we can specify the default kernel and ramdisk to use for system installation and recovery. The variables ironic_http_interface_address and ironic_http_port – whose names are sufficiently self-explanatory – will be expanded by Ansible with their respective values during the actual configuration file creation process.
2.7 OpenStack Deploy
Let’s check the configuration directory $KOLLA_CONFIG_PATH, which should contain the following files:
kolla@bms:~$ tree $KOLLA_CONFIG_PATH
/var/lib/kolla/etc
├── ansible
│ └── inventory
│ └── all-in-one
├── config
│ ├── ironic
│ │ ├── ironic-agent.initramfs
│ │ └── ironic-agent.kernel
│ └── ironic.conf
├── globals.yml
└── passwords.yml
5 directories, 6 files
**globals.yml**: YAML configuration file for OpenStack Kolla;**passwords.yml**: YAML with users and passwords used by the various components;**ansible/inventory/all-in-one**: Default Ansible inventory;**config/ironic.conf**: INI file containing specific configuration options for OpenStack Ironic;**config/ironic/ironic-agent.{kernel,initramfs}**: Binaries related to the Ironic Python Agent.
To pre-validate the configuration before the actual deployment, you can run the kolla-ansible validate-config command, and, if there are no errors to correct, finally proceed with the kolla-ansible deploy command:

NOTE: In case of errors, to get more debug information, you can increase the “verbosity” of the Ansible messages by re-running the command with a proportional number of “-v” options (e.g.
kolla-ansible deploy -vvv).
Upon completion of the deployment, various containers will be active: some are service containers, others are specific to OpenStack Ironic:
- kolla_toolbox: This is a management “proxy” container.
- cron: Used to execute scheduled batch actions (e.g. logrotate).
- mariadb[tcp/3306]: Database for OpenStack’s persistent data.
- rabbitmq[tcp/5672]: Message queue used to track transactional or stateful activities.
- ironic_conductor: Ironic’s business logic.
- ironic_api[tcp/6385]: RESTful API front-end (Bare Metal API).
- ironic_tftp[udp/69]: Trivial FTP (FTP over UDP) used to provide kernel and ramdisk via PXE.
- ironic_http[tcp/8089]: HTTP repository.
- ironic_dnsmasq[udp/67]: DHCP server.
kolla@bms:~$ docker container ps -a
CONTAINER ID IMAGE CREATED STATUS NAMES
be427175421b dnsmasq:2026.1 3 minutes ago Up 3 minutes ago ironic_dnsmasq
26f5c058b419 httpd:2026.1 3 minutes ago Up 3 minutes ago (healthy) ironic_http
6a3956ff2c25 ironic-pxe:2026.1 3 minutes ago Up 3 minutes ago ironic_tftp
040e1ae3254e ironic-api:2026.1 3 minutes ago Up 3 minutes ago (healthy) ironic_api
fd7169161d76 ironic-conductor:2026.1 3 minutes ago Up 3 minutes ago (healthy) ironic_conductor
efe4ebcce42c rabbitmq:2026.1 4 minutes ago Up 4 minutes ago (healthy) rabbitmq
e222e740d7ce mariadb-server:2026.1 4 minutes ago Up 4 minutes ago (healthy) mariadb
6d0150fa51e2 cron:2026.1 5 minutes ago Up 5 minutes ago cron
978e287862b4 kolla-toolbox:2026.1 5 minutes ago Up 5 minutes ago kolla_toolbox
The definitive configuration for the various components in question can be found in the /etc/kolla directory, which is only accessible with root privileges:
/etc/kolla
├── cron
│ ├── config.json
│ └── logrotate.conf
├── ironic-api
│ ├── config.json
│ ├── ironic-api-uwsgi.ini
│ └── ironic.conf
├── ironic-conductor
│ ├── config.json
│ └── ironic.conf
├── ironic-dnsmasq
│ ├── config.json
│ └── dnsmasq.conf
├── ironic-http
│ ├── config.json
│ ├── httpd.conf
│ ├── ipa.ipxe
│ ├── ironic-agent.initramfs
│ └── ironic-agent.kernel
├── ironic-tftp
│ └── config.json
├── kolla-toolbox
│ ├── clouds.yaml
│ ├── config.json
│ ├── erl_inetrc
│ ├── rabbitmq-env.conf
│ └── rabbitmq-erlang.cookie
├── mariadb
│ ├── config.json
│ ├── galera.cnf
│ └── healthcheck.cnf
└── rabbitmq
├── advanced.config
├── config.json
├── definitions.json
├── enabled_plugins
├── erl_inetrc
├── rabbitmq-env.conf
└── rabbitmq.conf
10 directories, 30 files
This consists of the default OpenStack Kolla configurations combined with the specific variations from files located in the config folder under the directory identified by the $KOLLA_CONFIG_PATH variable. In this lab, that’s /var/lib/kolla/etc/config, though by default it would be /etc/kolla/config (Kolla’s Deployment Philosophy).
Alternatively, the directory hosting the customized configuration files can be defined within the globals.yml file by setting the node_custom_config variable (OpenStack Service Configuration in Kolla).
NOTE: These configuration files can be generated independently of the deployment process using the
kolla-ansible genconfigcommand.
To check if the Ironic service is up and running, we can directly query the API with the command curl http://172.19.74.1:6385 | yq -y:
name: OpenStack Ironic API
description: Ironic is an OpenStack project which enables the provision and management
of baremetal machines.
default_version:
id: v1
links:
- href: http://172.19.74.1:6385/v1/
rel: self
status: CURRENT
min_version: '1.1'
version: '1.111'
versions:
- id: v1
links:
- href: http://172.19.74.1:6385/v1/
rel: self
status: CURRENT
min_version: '1.1'
version: '1.111'
Let’s also test the client via the command line:
kolla@bms:~$ openstack \
--os-endpoint=http://172.19.74.1:6385 \
--os-auth-type=none \
baremetal driver list
+---------------------+----------------+
| Supported driver(s) | Active host(s) |
+---------------------+----------------+
| ipmi | bms |
| redfish | bms |
+---------------------+----------------+
kolla@bms:~$ openstack \
--os-endpoint=http://172.19.74.1:6385 \
--os-auth-type=none \
baremetal conductor list
+----------------+-----------------+-------+
| Hostname. | Conductor Group | Alive |
+----------------+-----------------+-------+
| bms | | True |
+----------------+-----------------+-------+
kolla@bms:~$ openstack \
--os-endpoint=http://172.19.74.1:6385 \
--os-auth-type=none \
baremetal conductor show bms
+-----------------+---------------------------+
| Field | Value |
+-----------------+---------------------------+
| created_at | 2026-03-31T17:50:26+00:00 |
| updated_at | 2026-04-01T17:16:56+00:00 |
| hostname | bms |
| conductor_group | |
| drivers | ['ipmi', 'redfish'] |
| alive | True |
+-----------------+---------------------------+
As a second option, you can replace the --os-endpoint and --auth-type parameters with the **$OS_ENDPOINT and `$OS_AUTH_TYPE`** environment variables:
kolla@bms:~$ export OS_ENDPOINT=http://172.19.74.1:6385/
kolla@bms:~$ export OS_AUTH_TYPE=none
kolla@bms:~$ openstack baremetal driver show ipmi -f yaml | grep ^def
default_bios_interface: no-bios
default_boot_interface: ipxe
default_console_interface: no-console
default_deploy_interface: direct
default_firmware_interface: no-firmware
default_inspect_interface: agent
default_management_interface: ipmitool
default_network_interface: noop
default_power_interface: ipmitool
default_raid_interface: agent
default_rescue_interface: agent
default_storage_interface: noop
default_vendor_interface: ipmitool
kolla@bms:~$ openstack baremetal driver show redfish -f yaml | grep ^def
default_bios_interface: no-bios
default_boot_interface: ipxe
default_console_interface: no-console
default_deploy_interface: direct
default_firmware_interface: no-firmware
default_inspect_interface: agent
default_management_interface: redfish
default_network_interface: noop
default_power_interface: redfish
default_raid_interface: agent
default_rescue_interface: agent
default_storage_interface: noop
default_vendor_interface: redfish
Alternatively, OpenStack clients use a file named **clouds.yaml** in the following filesystem locations, in a specific order:
- Current directory
~/.config/openstackdirectory/etc/openstackdirectory
kolla@bms:~$ mkdir -p ~/.config/openstack
kolla@bms:~$ cat >~/.config/openstack/clouds.yaml <<-EOF
clouds:
ironic-standalone:
endpoint: http://172.19.74.1:6385
auth_type: none
EOF
NOTE: If there are multiple configurations within the
clouds.yamlfile, it is necessary to specify the relevant entry using the--os-cloudcommand-line option (e.g.openstack --os-cloud=ironic ...) or through theOS_CLOUDenvironment variable (e.g.export OS_CLOUD=ironic).
For more information, consult the following link: Configuring os-client-config Applications.
3 Bare Metal Management
- Configuring the BMCs of the servers to be managed;
- Onboarding nodes;
- Inspecting nodes;
- Cleaning (optional);
- Deployment;
- Recovering an inaccessible node.
3.1 BMC Configuration
For Ironic to manage physical nodes, the various BMCs (Baseboard Management Controllers) must be accessible from the bms server via the OOB/IB network 172.19.74.0/24. (For the lab, a single LAN network, 192.168.0.0/24, where all services collapse, is also acceptable) with their respective access credentials.
The Ethernet card dedicated to operating system installation and in-band management must be PXE-boot enabled via the BIOS.
On many servers, the BMC’s MAC address and access credentials are listed on a dedicated label.



Setup of a HPE iLO BMC from BIOS
Let’s gather the information for the servers to be managed in a convenient location:
| Server | BMC address | BMC type | BMC user | BMC pass | PXE MAC address |
| -------- | ------------- | -------- | -------- | --------- | ----------------- |
| server01 | 172.19.74.201 | redfish | ironic | baremetal | 9c:b6:54:b2:b0:ca |
| server02 | 172.19.74.202 | ipmi | ironic | baremetal | dc:f4:01:ec:7c:c4 |
| server03 | 172.19.74.203 | redfish | ironic | baremetal | 9c:b6:54:3a:55:10 |
To check if the BMCs are accessible via IPMI and/or Redfish protocols, let’s install the respective command-line tools:
kolla@bms:~$ sudo apt install ipmitool redfishtool
...
kolla@bms:~$ redfishtool -r 172.19.74.201 -u ironic -p baremetal Systems
{
"@odata.context": "/redfish/v1/$metadata#Systems",
"@odata.id": "/redfish/v1/Systems/",
"@odata.type": "#ComputerSystemCollection.ComputerSystemCollection",
"Description": "Computer Systems view",
"Members": [
{
"@odata.id": "/redfish/v1/Systems/1/"
}
],
"Members@odata.count": 1,
"Name": "Computer Systems"
}
kolla@bms:~$ ipmitool -C 3 -I lanplus -H 172.19.74.202 \
-U ironic -P baremetal chassis status
System Power : off
Power Overload : false
Power Interlock : inactive
Main Power Fault : false
Power Control Fault : false
Power Restore Policy : previous
Last Power Event :
Chassis Intrusion : inactive
Front-Panel Lockout : inactive
Drive Fault : false
Cooling/Fan Fault : false
Front Panel Control : none
NOTE: On some BMCs, using IPMI and Redfish protocols might require explicit enablement within the BIOS settings.
The enrollment phase occurs when a new server is onboarded and managed by Ironic using the command openstack baremetal node create --driver ....

Ironic Bare Metal State Machine
The information required to proceed is:
- BMC type (e.g., ipmi, redfish, ilo, idrac, etc.)
- BMC IP address with its corresponding credentials (out-of-band)
- MAC address of the network card that will be used for inspection, installation, and potential recovery (in-band)
kolla@bms:~$ openstack baremetal node create \
--name server01 --driver redfish \
--driver-info redfish_address=https://172.19.74.201 \
--driver-info redfish_verify_ca=False \
--driver-info redfish_username=ironic \
--driver-info redfish_password=baremetal \
-f value -c uuid
5113ab44-faf2-4f76-bb41-ea8b14b7aa92
kolla@bms:~$ openstack baremetal node create \
--name server02 --driver ipmi \
--driver-info ipmi_address=172.19.74.202 \
--driver-info ipmi_username=ironic \
--driver-info ipmi_password=baremetal \
-f value -c uuid
5294ec18-0dae-449e-913e-24a0638af8e5
kolla@bms:~$ openstack baremetal node list
+--------------------------------------+----------+---------------+-------------+--------------------+-------------+
| UUID | Name | Instance UUID | Power State | Provisioning State | Maintenance |
+--------------------------------------+----------+---------------+-------------+--------------------+-------------+
| 5113ab44-faf2-4f76-bb41-ea8b14b7aa92 | server01 | None | None | enroll | False |
| 5294ec18-0dae-449e-913e-24a0638af8e5 | server02 | None | None | enroll | False |
+--------------------------------------+----------+---------------+-------------+--------------------+-------------+
To manage the servers, they must be declared manageable using the command openstack baremetal node manage ...:

Ironic will use the specified hardware driver (in this case, redfish) to query the BMC and will transition from the enrollstate to verifying, and finally to manageable, populating some server properties that can be queried with the command openstack baremetal node show <UUID|NAME> -f value -c properties.
kolla@bms:~$ openstack baremetal node manage server01
kolla@bms:~$ openstack baremetal node list
+--------------------------------------+----------+---------------+-------------+--------------------+-------------+
| UUID | Name | Instance UUID | Power State | Provisioning State | Maintenance |
+--------------------------------------+----------+---------------+-------------+--------------------+-------------+
| 5113ab44-faf2-4f76-bb41-ea8b14b7aa92 | server01 | None | power off | verifying | False |
| 5294ec18-0dae-449e-913e-24a0638af8e5 | server02 | None | None | enroll | False |
+--------------------------------------+----------+---------------+-------------+--------------------+-------------+
kolla@bms:~$ openstack baremetal node list
+--------------------------------------+----------+---------------+-------------+--------------------+-------------+
| UUID | Name | Instance UUID | Power State | Provisioning State | Maintenance |
+--------------------------------------+----------+---------------+-------------+--------------------+-------------+
| 5113ab44-faf2-4f76-bb41-ea8b14b7aa92 | server01 | None | power off | manageable | False |
| 5294ec18-0dae-449e-913e-24a0638af8e5 | server02 | None | None | enroll | False |
+--------------------------------------+----------+---------------+-------------+--------------------+-------------+
kolla@bms:~$ openstack baremetal node show server01 -f value -c properties
{'vendor': 'HPE'}
NOTE: Unfortunately, in this case, it’s not possible to use the “canonical” name (e.g.
server01); you must explicitly use its corresponding UUID.
3.3 Inspection
The “inspection” process in OpenStack Ironic is used to automatically catalog the hardware characteristics of a bare metal server.

This process allows us to gather various useful pieces of information, such as disks, CPU type, RAM, and network cards, before the actual installation. This data can then be used to guide and customize the installation itself.
kolla@bms:~$ time openstack baremetal node inspect server01 --wait
Waiting for provision state manageable on node(s) server01
real 6m26.435s
user 0m1.213s
sys 0m0.073s
With the node inventory thus generated, you can inspect the node using the command openstack baremetal node inventory save <UUID|NAME>, which returns a JSON collection. To make it easier to interpret, it’s advisable to use the jq or yq command:
kolla@bms:~$ openstack baremetal node inventory save server01 | \
yq -r 'keys[]'
inventory
plugin_data
kolla@bms:~$ openstack baremetal node inventory save server01 | \
yq -r '.inventory|keys[]'
bmc_address
bmc_mac
bmc_v6address
boot
cpu
disks
hostname
interfaces
memory
system_vendor
kolla@bms:~$ openstack baremetal node inventory save server01 | \
yq -y '.inventory.system_vendor'
product_name: ProLiant ML350 Gen9 (776971-035)
serial_number: CZ24421875
manufacturer: HP
firmware:
vendor: HP
version: P92
build_date: 08/29/2024
kolla@bms:~$ openstack baremetal node inventory save server01 | \
yq -y '.inventory.boot'
current_boot_mode: uefi
pxe_interface: 9c:b6:54:b2:b0:ca
To list the network cards and their characteristics, we could filter the output using the command yq -y '.inventory.interfaces', or even filter only the network cards that have a connected link (has_carrier==true):
kolla@bms:~$ openstack baremetal node inventory save server01 | \
yq -y '.inventory.interfaces|map_values(select(.has_carrier==true))'
- name: eno1
mac_address: 9c:b6:54:b2:b0:ca
ipv4_address: 172.19.74.193
ipv6_address: fe80::9eb6:54ff:feb2:b0ca%eno1
has_carrier: true
lldp: null
vendor: '0x14e4'
product: '0x1657'
client_id: null
biosdevname: null
speed_mbps: 1000
pci_address: '0000:02:00.0'
driver: tg3
- name: eno2
mac_address: 9c:b6:54:b2:b0:cb
ipv4_address: 192.168.0.195
ipv6_address: fe80::9eb6:54ff:feb2:b0cb%eno2
has_carrier: true
lldp: null
vendor: '0x14e4'
product: '0x1657'
client_id: null
biosdevname: null
speed_mbps: 1000
pci_address: '0000:02:00.1'
driver: tg3
With this information, you can choose the best candidate disk on which to install the operating system. To specify the use of disk /dev/sdc, which is the SanDisk Ultra Fit of approximately 30GB with serial 4C530001220528100484, you can set the **root_device** property:
kolla@bms:~$ openstack baremetal node set server01 \
--property root_device='{"serial": "4C530001220528100484"}'
kolla@bms:~$ openstack baremetal node show server01 -f json | \
yq -y '.properties'
vendor: HPE
cpu_arch: x86_64
root_device:
serial: 4C530001220528100484
3.4 Cleaning (Optional)

If the hardware isn’t new, the disks might contain sensitive data or, worse, conflict with the new deployment (e.g., a previous operating system installed on a disk other than the intended destination).
To prevent these situations, it’s useful to use the node cleaning functionality provided by Ironic via the agent with the command openstack baremetal node clean <UUID>:
kolla@bms:~$ openstack baremetal node clean server01 \
--clean-steps '[{"interface": "deploy", "step": "erase_devices_metadata"}]'
The mandatory option --clean-steps is used to specify actions through a particular interface from the following: power, management, deploy, firmware, bios, and raid.
For the deploy interface, which is managed via the Ironic Python Agent (IPA), the available steps are:
**erase_devices**: Ensures data is wiped from disks.**erase_devices_metadata**: Wipes only disk metadata.**erase_devices_express**: Hardware-assisted data wiping, currently only supported by NVMe.
NOTE: For all other “cleaning” options, which we’ll skip for now (e.g. firmware upgrades, BIOS reset, RAID setup, etc.), you can refer to the Node Cleaning chapter in the Ironic documentation.
3.5 Deployment
Now that we’ve added, analyzed, and configured the systems to be managed (openstack baremetal node [create|manage|inspect|clean]), we’ll have a list of nodes in a manageable state:
kolla@bms:~$ openstack baremetal node list
+--------------------------------------+----------+---------------+-------------+--------------------+-------------+
| UUID | Name | Instance UUID | Power State | Provisioning State | Maintenance |
+--------------------------------------+----------+---------------+-------------+--------------------+-------------+
| 5113ab44-faf2-4f76-bb41-ea8b14b7aa92 | server01 | None | power off | manageable | False |
| 5294ec18-0dae-449e-913e-24a0638af8e5 | server02 | None | power off | manageable | False |
| f0d3f50a-d543-4e3d-9b9a-ba719373ab58 | server03 | None | power off | manageable | False |
+--------------------------------------+----------+---------------+-------------+--------------------+-------------+
To make a physical node available for deployment, you need to declare it as available:

Any node in a manageable state is eligible to become available via the openstack baremetal node provide <UUID|NAME> command. The inverse operation is performed with openstack baremetal node manage <UUID|NAME>.
kolla@bms:~$ openstack baremetal node provide server01
kolla@bms:~$ openstack baremetal node list
+--------------------------------------+----------+---------------+-------------+--------------------+-------------+
| UUID | Name | Instance UUID | Power State | Provisioning State | Maintenance |
+--------------------------------------+----------+---------------+-------------+--------------------+-------------+
| 5113ab44-faf2-4f76-bb41-ea8b14b7aa92 | server01 | None | power off | available | False |
| 5294ec18-0dae-449e-913e-24a0638af8e5 | server02 | None | power off | manageable | False |
| f0d3f50a-d543-4e3d-9b9a-ba719373ab58 | server03 | None | power off | manageable | False |
+--------------------------------------+----------+---------------+-------------+--------------------+-------------+
kolla@bms:~$ openstack baremetal node validate server01
+------------+--------+------------------------------------------------------------------------------------------------+
| Interface | Result | Reason |
+------------+--------+------------------------------------------------------------------------------------------------+
| bios | False | Driver redfish does not support bios (disabled or not implemented). |
| boot | True | |
| console | False | Driver redfish does not support console (disabled or not implemented). |
| deploy | False | Node 5113ab44-faf2-4f76-bb41-ea8b14b7aa92 failed to validate deploy image info. Some |
| | | parameters were missing. Missing are: ['instance_info.image_source'] |
| firmware | False | Driver redfish does not support firmware (disabled or not implemented). |
| inspect | True | |
| management | True | |
| network | True | |
| power | True | |
| raid | True | |
| rescue | False | Node 5113ab44-faf2-4f76-bb41-ea8b14b7aa92 is missing 'instance_info/rescue_password'. It is |
| | | required for rescuing node. |
| storage | True | |
+------------+--------+------------------------------------------------------------------------------------------------+
We can perform a final check on the node to be installed using the openstack baremetal node validate <UUID|NAME>command (output shown above). The result indicates that some areas aren’t managed (False value):
- bios: In this case, the functionality was explicitly disabled with
enabled_bios_interfaces = no-bios. - console: This functionality is also disabled (
enabled_console_interfaces = no-console). - deploy: The
image_sourceparameter is missing, which specifies where to get the operating system image to install. - firmware: Functionality disabled (
enabled_firmware_interfaces = no-firmware). - rescue: The
rescue_passwordparameter is missing.
Without a surrounding Cloud environment, Ironic needs to know where to get the image to install, either directly via a file or a URL. The image_source parameter, in the --instance-info section of the openstack baremetal node setcommand, indicates exactly this.
Although it’s possible to use a URL on the Internet, for bandwidth optimization, it’s preferable to use a local repository. In this case, we’ll use the web server in the ironic_http container and the persistent ironic volume mounted to it:
kolla@bms:~$ docker volume inspect ironic
[
{
"CreatedAt": "2026-06-18T07:21:07Z",
"Driver": "local",
"Labels": {
"kolla_managed": "true"
},
"Mountpoint": "/var/lib/docker/volumes/ironic/_data",
"Name": "ironic",
"Options": null,
"Scope": "local"
}
]
kolla@bms:~$ docker container inspect ironic_http | \
yq '.[]|.Mounts[]|select(.Name=="ironic")'
{
"Type": "volume",
"Name": "ironic",
"Source": "/var/lib/docker/volumes/ironic/_data",
"Destination": "/var/lib/ironic",
"Driver": "local",
"Mode": "rw",
"RW": true,
"Propagation": ""
}
kolla@bms:~$ sudo cat /etc/kolla/ironic-http/httpd.conf
Listen 172.19.74.1:8089
TraceEnable off
<VirtualHost *:8089>
LogLevel warn
ErrorLog "/var/log/kolla/ironic/ironic-http-error.log"
LogFormat "%h %l %u %t \"%r\" %>s %b %D \"%{Referer}i\" \"%{User-Agent}i\"" logformat
CustomLog "/var/log/kolla/ironic/ironic-http-access.log" logformat
DocumentRoot "/var/lib/ironic/httpboot"
<Directory /var/lib/ironic/httpboot>
Options FollowSymLinks
AllowOverride None
Require all granted
</Directory>
</VirtualHost>
Since the volume /var/lib/docker/volumes/ironic/_data is mounted under /var/lib/ironic in the ironic_http container and exposes the /var/lib/ironic/httpboot directory (DocumentRoot) via http://172.19.74.1:8089 (Listen), we can copy the various operating system images in raw or qcow2 format to the directory /var/lib/docker/volumes/ironic/_data/httpboot/<IMAGE> and reference them as http://172.19.74.1:8089/<IMAGE>:
kolla@bms:~$ sudo wget \
-P /var/lib/containers/docker/ironic/_data/httpboot/ \
https://cloud.debian.org/images/cloud/trixie/latest/debian-13-nocloud-amd64.qcow2
...
debian-13-nocloud-amd64.qcow2 100%[========================================>] 388.56M
kolla@bms:~$ sudo wget \
-P /var/lib/containers/docker/ironic/_data/httpboot/ \
https://cloud.debian.org/images/cloud/trixie/latest/debian-13-generic-amd64.qcow2
...
debian-13-generic-amd64.qcow2 100%[========================================>] 415.69M
kolla@bms:~$ docker exec --workdir=/var/lib/ironic/httpboot \
ironic_http sh -c 'sha256sum *.qcow2 >CHECKSUM'
kolla@bms:~$ docker exec ironic_http ls -la /var/lib/ironic/httpboot
total 1154760
drwxr-xr-x 2 ironic ironic 4096 Jun 18 07:48 .
drwxr-xr-x 5 ironic ironic 4096 Jun 18 07:22 ..
-rw-r--r-- 1 ironic ironic 1004 Jun 18 07:22 boot.ipxe
-rw-r--r-- 1 root root 192 Jun 18 07:50 CHECKSUM
-rw-r--r-- 1 root root 436469760 Jun 17 18:17 debian-13-generic-amd64.qcow2
-rw-r--r-- 1 root root 407175168 Jun 17 18:32 debian-13-nocloud-amd64.qcow2
-rw-r--r-- 1 ironic ironic 8388608 Jun 18 07:21 esp.img
-rw-r--r-- 1 root root 530 Jun 18 07:22 ipa.ipxe
-rw-r--r-- 1 root root 318289274 Jun 18 07:22 ironic-agent.initramfs
-rw-r--r-- 1 root root 12117952 Jun 18 07:22 ironic-agent.kernel
kolla@bms:~$ curl -I http://172.19.74.1:8089/debian-13-nocloud-amd64.qcow2
HTTP/1.1 200 OK
Date: Sun, 14 Jun 2026 12:33:43 GMT
Server: Apache/2.4.67 (Debian)
Last-Modified: Mon, 01 Jun 2026 16:06:26 GMT
ETag: "18490000-6533360660480"
Accept-Ranges: bytes
Content-Length: 407175168
NOTE: Creating the CHECKSUM file is necessary to allow Ironic to verify that the image has not been altered during the transfer process from the source server to the bare metal node.
We’re ready to perform the first test deployment:

3.5.1 Test
As a first test, we can use the Debian Cloud image named “nocloud,” referenced as debian-12-nocloud-amd64.qcow2, to certify that the process works. Unfortunately, this image doesn’t have cloud-init installed, and it only allows root access directly from the console without a password, as the SSH service isn’t active.
kolla@bms:~$ openstack baremetal node set server01 \
--instance-info image_source=http://172.19.74.1:8089/debian-12-nocloud-amd64.qcow2 \
--instance-info image_checksum=http://172.19.74.1:8089/CHECKSUM
kolla@bms:~$ openstack baremetal node deploy server01
kolla@bms:~$ openstack baremetal node list
+--------------------------------------+----------+---------------+-------------+--------------------+-------------+
| UUID | Name | Instance UUID | Power State | Provisioning State | Maintenance |
+--------------------------------------+----------+---------------+-------------+--------------------+-------------+
| 5113ab44-faf2-4f76-bb41-ea8b14b7aa92 | server01 | None | power on | wait call-back | False |
| 5294ec18-0dae-449e-913e-24a0638af8e5 | server02 | None | power off | manageable | False |
| f0d3f50a-d543-4e3d-9b9a-ba719373ab58 | server03 | None | power off | manageable | False |
+--------------------------------------+----------+---------------+-------------+--------------------+-------------+
kolla@bms:~$ openstack baremetal node list
+--------------------------------------+----------+---------------+-------------+--------------------+-------------+
| UUID | Name | Instance UUID | Power State | Provisioning State | Maintenance |
+--------------------------------------+----------+---------------+-------------+--------------------+-------------+
| 5113ab44-faf2-4f76-bb41-ea8b14b7aa92 | server01 | None | power on | deploying | False |
| 5294ec18-0dae-449e-913e-24a0638af8e5 | server02 | None | power off | manageable | False |
| f0d3f50a-d543-4e3d-9b9a-ba719373ab58 | server03 | None | power off | manageable | False |
+--------------------------------------+----------+---------------+-------------+--------------------+-------------+
kolla@bms:~$ openstack baremetal node list
+--------------------------------------+----------+---------------+-------------+--------------------+-------------+
| UUID | Name | Instance UUID | Power State | Provisioning State | Maintenance |
+--------------------------------------+----------+---------------+-------------+--------------------+-------------+
| 5113ab44-faf2-4f76-bb41-ea8b14b7aa92 | server01 | None | power on | active | False |
| 5294ec18-0dae-449e-913e-24a0638af8e5 | server02 | None | power off | manageable | False |
| f0d3f50a-d543-4e3d-9b9a-ba719373ab58 | server03 | None | power off | manageable | False |
+--------------------------------------+----------+---------------+-------------+--------------------+-------------+
Upon completion of the process, when the Provisioning State of the node in question becomes active, we can verify the outcome directly from the server’s console or via its BMC:

To decommission this test instance:
kolla@bms:~$ openstack baremetal node undeploy server01
kolla@bms:~$ openstack baremetal node list
+--------------------------------------+----------+---------------+-------------+--------------------+-------------+
| UUID | Name | Instance UUID | Power State | Provisioning State | Maintenance |
+--------------------------------------+----------+---------------+-------------+--------------------+-------------+
| 5113ab44-faf2-4f76-bb41-ea8b14b7aa92 | server01 | None | power on | deleting | False |
| 5294ec18-0dae-449e-913e-24a0638af8e5 | server02 | None | power off | manageable | False |
| f0d3f50a-d543-4e3d-9b9a-ba719373ab58 | server03 | None | power off | manageable | False |
+--------------------------------------+----------+---------------+-------------+--------------------+-------------+
kolla@bms:~$ openstack baremetal node list
+--------------------------------------+----------+---------------+-------------+--------------------+-------------+
| UUID | Name | Instance UUID | Power State | Provisioning State | Maintenance |
+--------------------------------------+----------+---------------+-------------+--------------------+-------------+
| 5113ab44-faf2-4f76-bb41-ea8b14b7aa92 | server01 | None | power off | available | False |
| 5294ec18-0dae-449e-913e-24a0638af8e5 | server02 | None | power off | manageable | False |
| f0d3f50a-d543-4e3d-9b9a-ba719373ab58 | server03 | None | power off | manageable | False |
+--------------------------------------+----------+---------------+-------------+--------------------+-------------+
The node will be powered off, and its Provisioning State will transition from active to deleting, and then back to available.
3.5.2 cloud-init
Static images, like the previous debian-12-nocloud, don’t provide configuration flexibility. Unless an image is specifically crafted to meet your needs (partitioning, networking, users, software, etc.), they tend to be of limited use.
We can achieve a compromise by using images that include cloud-init:
“Cloud images are operating system templates and every instance starts out as an identical clone of every other instance. It is the user data that gives every cloud instance its personality and cloud-init is the tool that applies user data to your instances automatically.” (ref. https://cloud-init.io)
To provide personalization instructions to the bare metal node, we’ll use a portion of the node’s own disk, known as a **Config Drive**. During deployment, necessary information will be downloaded to this Config Drive in the form of structured files within a filesystem.
Assuming we want to install the server01 node with the following characteristics:
**ironic** user with a pre-installed public SSH key andsudoprivileges to execute root commands;- Software to be automatically installed:
bashandgit; - Network interface on the LAN configured with a static IP address
192.168.0.11/24, default gateway, and public DNS;
we can proceed as follows:
kolla@bms:~$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/kolla/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
...
kolla@bms:~$ RSA_PUB=$(cat ~/.ssh/id_rsa.pub)
kolla@bms:~$ NODE=server01
kolla@bms:~$ mkdir -p ~/ConfigDrive/$NODE/openstack/latest
kolla@bms:~$ cat >~/ConfigDrive/$NODE/openstack/latest/meta_data.json <<EOF
{
"uuid": "$(openstack baremetal node show $NODE -f value -c uuid)",
"hostname": "$NODE"
}
EOF
kolla@bms:~$ cat >~/ConfigDrive/$NODE/openstack/latest/user_data <<EOF
#cloud-config
package_update: true
packages:
- bash
- git
users:
- name: ironic
shell: /bin/bash
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
ssh_authorized_keys:
- '$RSA_PUB'
EOF
kolla@bms:~$ cat >~/ConfigDrive/$NODE/openstack/latest/network_data.json <<EOF
{
"links": [
{
"id": "oob0",
"type": "phy",
"ethernet_mac_address": "9c:b6:54:b2:b0:ca"
},
{
"id": "lan0",
"type": "phy",
"ethernet_mac_address": "9c:b6:54:b2:b0:cb"
}
],
"networks": [
{
"id": "oob",
"type": "ipv4_dhcp",
"link": "oob0",
"network_id": "oob"
},
{
"id": "lan",
"type": "ipv4",
"link": "lan0",
"ip_address": "192.168.0.11/24",
"gateway": "192.168.0.254",
"network_id": "lan"
}
],
"services": [
{
"type": "dns",
"address": "8.8.8.8"
},
{
"type": "dns",
"address": "8.8.4.4"
}
]
}
EOF
NOTE: For more information on the structure of the
network_data.jsonfile, refer to its schema: https://opendev.org/openstack/nova/src/branch/master/doc/api_schemas/network_data.json
To perform a new deployment leveraging cloud-init, we change the reference image_source to debian-12-generic-amd64.qcow2 and specify the directory containing the files to generate the Config Drive:
kolla@bms:~$ openstack baremetal node set server01 \
--instance-info image_source=http://172.19.74.1:8089/debian-12-generic-amd64.qcow2 \
--instance-info image_checksum=http://172.19.74.1:8089/CHECKSUM
kolla@bms:~$ time openstack baremetal node deploy server01 \
--config-drive=$HOME/ConfigDrive/server01 --wait
Waiting for provision state active on node(s) server01
real 12m3.542s
user 0m0.863s
sys 0m0.085s
NOTE: If we hadn’t already deleted the previous deployment with the
openstack baremetal node undeploy <UUID|NAME>command, it’s still possible to reset theimage_sourcevariable usingopenstack baremetal node set <UUID|NAME> --instance-info image_source=...and then request a rebuild:openstack baremetal node rebuild <UUID|NAME> --config-drive=<PATH>.
3.6 Recovering an Inaccessible Node (rescue)
Sometimes things go wrong… the IPA (the Ironic Python Agent) loads, the operating system seems to write correctly to disk, and the Config Drive appears installed. Yet the node doesn’t boot, or maybe it boots, but the network card doesn’t configure, the admin user credentials seem incorrect, an error message flashes too quickly on the server monitor… in short, Murphy is always ready to contribute.

To leverage the IPA as a recovery system, the node must be in the active state and can be invoked with the following command:
kolla@bms:~$ openstack baremetal node show server01 -c provision_state
+-----------------+--------+
| Field | Value |
+-----------------+--------+
| provision_state | active |
+-----------------+--------+
kolla@bms:~$ time baremetal node rescue server01 \
--rescue-password=<PASSWORD> --wait
Waiting for provision state rescue on node(s) server01
real 5m42.284s
user 0m0.745s
sys 0m0.045s
kolla@bms:~$ openstack baremetal node show server01 -c provision_state
+-----------------+--------+
| Field | Value |
+-----------------+--------+
| provision_state | rescue |
+-----------------+--------+
We can identify the assigned IP address from the DHCP server logs of Ironic (which consists of a container running dnsmasq), starting from the MAC address of the network card used for deployment:
kolla@bms:~$ openstack baremetal port list -f value -c address \
--node $(openstack baremetal node show server01 -f value -c uuid)
9c:b6:54:b2:b0:ca
kolla@bms:~$ sudo grep -i 9c:b6:54:b2:b0:ca /var/log/kolla/ironic/dnsmasq.log | tail -1
May 31 11:13:58 dnsmasq-dhcp[2]: DHCPACK(enp2s0) 172.19.74.192 9c:b6:54:b2:b0:ca
And finally, connect with the **rescue** user and the previously defined password to analyze the disk content, system and cloud-init logs, etc.:
kolla@bms:~$ ssh rescue@172.19.74.192
...
$ sudo -i
# lsblk -o+fstype,label
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS FSTYPE LABEL
sda 8:0 0 3.3T 0 disk
sdb. 8:16 1 28.6G 0 disk
|-sdb1 8:17 1 28.5G 0 part ext4
|-sdb2 8:18 1 64.4M 0 part iso9660 config-2
|-sdb14 8:30 1 3M 0 part
`-sdb15 8:31 1 124M 0 part vfat
sdc 8:32 0 838.3G 0 disk
sr0 11:0 1 1024M 0 rom
# mount /dev/sdb1 /mnt
# mount --bind /dev /mnt/dev
# mount --bind /sys /mnt/sys
# mount --bind /proc /mnt/proc
# chroot /mnt
root@localhost:/# journalctl --list-boots
IDX BOOT ID FIRST ENTRY LAST ENTRY
0 34c9893dd3ab4a928207f7da41ec1226 Tue 2025-06-03 10:31:39 UTC Tue 2025-06-03 12:17:50 UTC
root@localhost:/# journalctl -b 34c9893dd3ab4a928207f7da41ec1226
...
root@localhost:/# less /var/log/cloud-init.log
...
To check how the Config Drive was written:
# mount -r LABEL=config-2 /media
# find /media
/media
/media/openstack
/media/openstack/latest
/media/openstack/latest/meta_data.json
/media/openstack/latest/network_data.json
/media/openstack/latest/user_data
# cat /media/openstack/latest/meta_data.json
{
"uuid": "5113ab44-faf2-4f76-bb41-ea8b14b7aa92",
"hostname": "server01"
}
# cat /media/openstack/latest/user_data
#cloud-config
package_update: true
packages:
- bash
- git
users:
- name: ironic
shell: /bin/bash
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
ssh_authorized_keys:
- 'sha-rsa ...'
# umount /media
NOTE: To exit the rescue state, DO NOT restart the system from within it (e.g.,
reboot,shutdown -r now, etc.); it would simply boot the operating system on the disk without changing the state in Ironic’s database. Instead, use theopenstack baremetal node unrescue <UUID|NAME>command from the management server.
4 Automation and Infrastructure as Code
Infrastructure as Code (IaC) is an approach to IT infrastructure management where servers, networks, databases, and other components are defined and configured through code, rather than being created and managed manually. Infrastructure effectively becomes a set of declarative, versionable, and automatable files, allowing complex environments to be built in a repeatable, reliable, and scalable manner.
For demonstration purposes, among the hundreds of commercial and open-source projects addressing this topic, we will use two of the most well-known: Terraform and Ansible.
4.1 Terraform/OpenTofu
In this chapter, the main objective will be to reproduce the activities we’ve carried out so far on the command line to install the Debian 12 cloud image using Terraform/OpenTofu in the simplest and most straightforward way possible.
**Terraform**, developed by HashiCorp, is one of the most widely used tools for orchestrating cloud resources (AWS, Azure, GCP, OpenStack) and “traditional” environments (VMware, Proxmox, etc.). Its language allows for clearly and repeatably describing the resources needed for an infrastructure: servers, networks, DNS, firewalls, storage, and much more.
**OpenTofu**, born as a fork of Terraform after a license change by HashiCorp (recently acquired by IBM), fully maintains compatibility. Governed by the open-source community, its mission is to ensure that IaC remains open, accessible, and independent of commercial logic.
To install Terraform, follow the instructions at the following link: https://developer.hashicorp.com/terraform/install;
To install OpenTofu, run the command apt install -y tofu.
Let’s start by searching for a provider for OpenStack Ironic in their respective registries:
- Terraform: https://registry.terraform.io/browse/providers
- OpenTofu: https://search.opentofu.org/
We will find a common provider, supplied by Appkins Org and derived from the OpenShift project “Terraform provider for Ironic”: [appkins-org/ironic](https://github.com/appkins-org/terraform-provider-ironic).
Subsequently, in a directory specifically created to host Terraform/OpenTofu files, create a file named main.tf with the following content:
terraform {
required_providers {
ironic = {
source = "appkins-org/ironic"
version = "0.6.1"
}
}
}
provider "ironic" {
url = "http://172.19.74.1:6385/v1"
auth_strategy = "noauth"
microversion = "1.96"
timeout = 900
}
resource "ironic_node_v1" "server01" {
name = "server01"
inspect = true # Perform inspection
clean = false # Do not clean the node
available = true # Make the node 'available'
ports = [
{
"address" = "9c:b6:54:b2:b0:ca"
"pxe_enabled" = "true"
},
]
driver = "redfish"
driver_info = {
"redfish_address" = "https://172.19.74.201"
"redfish_verify_ca" = "False"
"redfish_username" = "ironic"
"redfish_password" = "baremetal"
}
}
resource "ironic_deployment" "server01" {
node_uuid = ironic_node_v1.server01.id
instance_info = {
image_source = "http://172.19.74.1:8089/debian-12-generic-amd64.qcow2"
image_checksum = "http://172.19.74.1:8089/CHECKSUM"
}
metadata = {
uuid = ironic_node_v1.server01.id
hostname = ironic_node_v1.server01.name
}
user_data = <<-EOT
#cloud-config
package_update: true
packages:
- bash
- git
users:
- name: ironic
shell: /bin/bash
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
ssh_authorized_keys:
- 'ssh-rsa ...'
EOT
}
In this file, besides specifying the appkins-org/ironic provider and its location, two resources are defined. The first, of type **ironic_node_v1, represents the enrollment phase combined with the inspection phase. The second, of type `ironic_deployment`, will handle the deployment phase** and the creation of the associated Config Drive.
To download the provider and its dependencies, simply call the Terraform/OpenTofu init procedure:
kolla@bms:~/TF$ terraform init
- OR -
kolla@bms:~/TF$ tofu init
Initializing the backend...
Initializing provider plugins...
- Finding appkins-org/ironic versions matching "0.6.1"...
- Installing appkins-org/ironic v0.6.1...
- Installed appkins-org/ironic v0.6.1...
...
Terraform/OpenTofu has been successfully initialized!
Meanwhile, to execute the actual deployment, you use the apply procedure:
kolla@bms:~/TF$ terraform apply
- OR -
kolla@bms:~/TF$ tofu apply
...
Plan: 2 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
...
ironic_deployment.server01: Creation complete after 10m32s [id=12016c80-be15-48ed-9b55-af00911c66b5]
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
4.2 Ansible
In this chapter, the goal is to achieve a more complex deployment that includes not only operating system provisioning but also the installation of a container orchestration platform like Kubernetes in an all-in-one (single node) setup.
**Ansible is an open-source automation platform renowned for its incredible simplicity and remarkable power. Unlike other automation tools, Ansible is agentless**, meaning it doesn’t require any additional software installation on the nodes it manages.
That said, other automation systems can be equally valid or even more suitable for your environment, such as **Chef, [Juju](https://juju.is/), [Puppet](https://puppet.com/), or [SaltStack](https://saltproject.io/)**, to name just a few.
4.2.1 Ansible Playbook
The playbook structure will be as follows:
kolla@bms:~$ tree Ansible
Ansible
├── config.yml
├── deploy.yml
├── group_vars
│ └── all.yml
└── roles
├── ironic
│ └── tasks
│ └── main.yml
└── k8s-aio
├── defaults
│ └── main.yml
└── tasks
├── configure.yml
├── install.yml
├── main.yml
└── prepare.yml
8 directories, 9 files
In the config.yml file, we can create a data structure representing the list of nodes (nodes: []) to provision, trying to adhere as closely as possible to the form required by Ironic (bmc, root_device, instance_info, network_data, etc.), while adding some extra information like ansible_host and ansible_roles:
# config.yml
---
user_data: |
#cloud-config
users:
- name: {{ default_user }}
lock_passwd: false
sudo: ['ALL=(ALL) NOPASSWD:ALL']
ssh_authorized_keys:
- '{{ public_key }}'
mounts:
- ['swap', null]
nodes:
- name: server01
ansible_host: "192.168.0.11"
ansible_roles:
- k8s-aio
bmc:
driver: redfish
driver_info:
redfish_address: "https://172.19.74.201"
redfish_verify_ca: false
redfish_username: "ironic"
redfish_password: "baremetal"
pxe:
mac: "9c:b6:54:b2:b0:ca"
root_device:
serial: "4C530001220528100484"
instance_info:
image_source: "http://172.19.74.1:8089/debian-12-generic-amd64.qcow2"
image_checksum: "http://172.19.74.1:8089/CHECKSUM"
network_data:
links:
- id: "oob0"
type: "phy"
ethernet_mac_address: "9c:b6:54:b2:b0:ca"
- id: "lan0"
type: "phy"
ethernet_mac_address: "9c:b6:54:b2:b0:cb"
networks:
- id: "oob"
type: "ipv4_dhcp"
link: "oob0"
network_id: "oob"
- id: "lan"
type: "ipv4"
link: "lan0"
ip_address: "192.168.0.11"
netmask: "255.255.255.0"
gateway: "192.168.0.254"
network_id: "lan"
services:
- type: "dns"
address: "8.8.8.8"
- type: "dns"
address: "8.8.4.4"
NOTE: Although the
ansible_hostvariable is redundant (as it can be derived from processing thenetwork_datastructure), it’s preferable to keep it explicit to improve playbook readability and avoid complex JSON Queries. If you prefer not to duplicate the information, you would still need to specify which interface to use for the connection (e.g.,ansible_link: "lan0") to enable the query.
As a workaround to dynamically configure individual nodes, we’ll place the list of Ansible roles to apply to each node within the ansible_roles variable, as defined in the deploy.yml playbook:
# deploy.yml
---
- hosts: localhost
connection: local
gather_facts: true
tasks:
- name: Provision baremetal nodes
ansible.builtin.include_role:
name: ironic
loop: "{{ nodes }}"
loop_control:
loop_var: node
- hosts: ironic
gather_facts: false
tasks:
- name: Wait for node to become reachable
ansible.builtin.wait_for_connection:
- name: Gather facts for first time
ansible.builtin.setup:
- name: Provision node {{ ansible_hostname }}
ansible.builtin.include_role:
name: "{{ role }}"
loop: "{{ ansible_roles }}"
loop_control:
loop_var: role
You’ll notice two groups (under hosts: elements) on which actions will be performed:
**localhost: Through this, we’ll control OpenStack Ironic** via its API for each node defined in ourconfig.ymlconfiguration file.**ironic**: This is a dynamic group that will be formed as theironicrole from the first group completes the operating system deployment for a node.
4.2.2 The “ironic” Role
The ironic role (found in the roles/ironic subdirectory) includes the enrollment phase (using the openstack.cloud.baremetal_node module) and the actual deployment phase (using the openstack.cloud.baremetal_node_action module). It also handles adding the node itself to the ironic group in the inventory (using the ansible.builtin.add_host module):
# roles/ironic/tasks/main.yml
---
- name: "Enroll {{ node.name }}"
register: baremetal_create
openstack.cloud.baremetal_node:
cloud: "ironic-standalone"
driver: "{{ node.bmc.driver }}"
driver_info: "{{ node.bmc.driver_info }}"
name: "{{ node.name }}"
uuid: "{{ node.uuid|default(omit) }}"
nics:
- mac: "{{ node.pxe.mac }}"
properties:
capabilities: "boot_option:local"
root_device: "{{ node.root_device }}"
- name: "Deploy {{ node.name }}"
when: baremetal_create is changed
openstack.cloud.baremetal_node_action:
cloud: "ironic-standalone"
id: "{{ baremetal_create.node.id }}"
timeout: 1800
instance_info: "{{ node.instance_info }}"
config_drive:
meta_data:
uuid: "{{ baremetal_create.node.id }}"
hostname: "{{ node.name }}"
user_data: "{{ cloud_config }}"
network_data: "{{ node.network_data }}"
- name: "Add {{ node.name }} to inventory"
ansible.builtin.add_host:
name: "{{ node.name }}"
groups: ironic
ansible_host: "{{ node.ansible_host }}"
ansible_user: "{{ default_user }}"
ansible_ssh_extra_args: "-o StrictHostKeyChecking=no"
ansible_roles: "{{ node.ansible_roles }}"
Each module within the **Openstack.Cloud collection uses the connection parameters referenced by the `cloud** variable, found in the~/.config/openstack/clouds.yaml`file (as discussed in Chapter 2.6, OpenStack Ironic “Standalone” Configuration in part 1).
Alternatively, instead of using the cloud parameter, you can explicitly specify the connection parameters:
- name: ...
openstack.cloud.baremetal_...:
auth:
endpoint: "http://172.19.74.1:6385"
auth_type: None
...
NOTE: The additional parameters
-o StrictHostKeyChecking=noare necessary due to the potential overlap of SSH server certificates generated randomly with each deployment (if you delete and then redeploy the same server with the same IP address, the SSH server certificate will not be identical).
The creation of the ironic system user and the prevention of swap area activation (as required by Kubernetes) are addressed through the use of user_data with the cloud_config variable defined in the previous config.yml file.
4.2.3 The “k8s-aio” Role
The k8s-aio role (in the roles/k8s-aio subdirectory) has been structured to reproduce the Kubernetes installation phases described in the official documentation (Bootstrapping clusters with kubeadm), in addition to the Flannel CNI and the Nginxingress controller.
For each physical node requiring this role, the actions contained in prepare.yml and install.yml will be executed in succession with privileged user rights, and configure.yml will be executed as the ironic user:
# roles/k8s-aio/tasks/main.yml
---
- name: Prepare server
ansible.builtin.include_tasks:
file: prepare.yml
apply:
become: true
- name: Install Kubernetes
ansible.builtin.include_tasks:
file: install.yml
apply:
become: true
- name: Configure Kubernetes
ansible.builtin.include_tasks:
file: configure.yml
The prepare.yml file focuses on system requirements, such as kernel modules to load, network parameters, and necessary software packages:
# roles/k8s-aio/tasks/prepare.yml
---
- name: Enable Kernel modules
community.general.modprobe:
name: "{{ item }}"
state: present
persistent: present
loop:
- overlay
- br_netfilter
- name: Setup networking stack
ansible.posix.sysctl:
name: "{{ item.name }}"
value: "{{ item.value }}"
sysctl_file: /etc/sysctl.d/kubernetes.conf
loop:
- name: net.bridge.bridge-nf-call-ip6tables
value: 1
- name: net.bridge.bridge-nf-call-iptables
value: 1
- name: net.ipv4.ip_forward
value: 1
- name: Install dependencies
ansible.builtin.apt:
pkg:
- apt-transport-https
- ca-certificates
- containerd
- curl
- gpg
- reserialize
update_cache: yes
The installation phase follows the rules described in the documentation, resulting in a single-node Kubernetes cluster that can respond via its standard APIs:
# roles/k8s-aio/tasks/install.yml
---
- name: Download the public key for Kubernetes repository
ansible.builtin.shell: |
curl -fsSL {{ k8s_pkgs_url }}/Release.key | \
gpg --dearmor -o /etc/apt/keyrings/kubernetes.gpg
args:
creates: /etc/apt/keyrings/kubernetes.gpg
- name: Add Kubernetes APT repository
ansible.builtin.apt_repository:
repo: "deb [signed-by=/etc/apt/keyrings/kubernetes.gpg] {{ k8s_pkgs_url }} /"
filename: kubernetes
state: present
- name: Install Kubernetes packages
ansible.builtin.apt:
pkg:
- jq
- kubeadm
- kubectl
- kubelet
- name: Check SystemdCgroup in containerd
register: systemd_cgroup
ansible.builtin.shell:
crictl info | \
jq -r '.config.containerd.runtimes.runc.options.SystemdCgroup'
- name: Enable SystemdCgroup in containerd
when: systemd_cgroup.stdout == "false"
ansible.builtin.shell: |
containerd config default | \
reserialize toml2json | \
jq '.plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options.SystemdCgroup=true' | \
reserialize json2toml >/etc/containerd/config.toml
- name: Restart containerd service
when: systemd_cgroup.stdout == "false"
ansible.builtin.systemd_service:
name: containerd.service
state: restarted
- name: Pull the required Kubernetes images
ansible.builtin.command:
cmd: kubeadm config images pull
creates: /etc/kubernetes/admin.conf
- name: Initialize Kubernetes
ansible.builtin.command:
cmd: kubeadm init --pod-network-cidr={{ k8s_network }} --control-plane-endpoint {{ ansible_host }}:6443
creates: /etc/kubernetes/admin.conf
Finally, in the configuration phase, the non-administrative user completes the Kubernetes setup by installing Flannel and Ingress-Nginx:
# roles/k8s-aio/tasks/configure.yml
---
- name: Read /etc/kubernetes/admin.conf
become: true
register: kube_conf
ansible.builtin.slurp:
src: /etc/kubernetes/admin.conf
- name: Create ~/.kube
ansible.builtin.file:
path: ~/.kube
state: directory
mode: 0700
- name: Create ~/.kube/config
ansible.builtin.copy:
content: "{{ kube_conf['content']|b64decode }}"
dest: "~/.kube/config"
mode: 0600
- name: Install Flannel
# https://github.com/flannel-io/flannel?tab=readme-ov-file#deploying-flannel-with-kubectl
ansible.builtin.command:
cmd: kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml
- name: Remove NoSchedule taint for control-plane
failed_when: false
ansible.builtin.command:
cmd: kubectl taint nodes {{ ansible_hostname }} node-role.kubernetes.io/control-plane:NoSchedule-
- name: Install Ingress-Nginx Controller
ansible.builtin.command:
cmd: kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/cloud/deploy.yaml
Some default values for the k8s-aio role can be found in the roles/k8s-aio/defaults/main.yml file:
# roles/k8s-aio/defaults/main.yml
---
k8s_version: "1.33"
k8s_network: "10.244.0.0/16"
k8s_pkgs_url: "https://pkgs.k8s.io/core:/stable:/v{{ k8s_version }}/deb"
The playbook concludes with the global variables file, group_vars/all.yml, which contains the default value for the public SSH key, the default name of the user to create, the content of the user_data, and the initialization of the node list:
# group_vars/all.yml
---
public_key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
default_user: ironic
cloud_config: |
#cloud-config
users:
- name: {{ default_user }}
shell: /bin/bash
sudo: ['ALL=(ALL) NOPASSWD:ALL']
ssh_authorized_keys:
- '{{ public_key }}'
nodes: []
4.2.4 Deployment
So, let’s launch the playbook with the command ansible-playbook -e @config.yml deploy.yaml:

Some Considerations
Even though OpenStack Ironic was configured in a “standalone” mode, the availability of OS cloud-ready images that include cloud-init, combined with the use of Infrastructure-as-Code (IaC) and Automation systems, allows for implementing effective Hardware Lifecycle management, application layer included.
메타데이터
- post_id
- 39cc782e205d
- slug
- openstack-ironic-first-contact-39cc782e205d
- url
- https://medium.com/@signal-9/openstack-ironic-first-contact-39cc782e205d
- canonical_url
- https://medium.com/@signal-9/openstack-ironic-first-contact-39cc782e205d
- author_url
- https://medium.com/@signal-9
- status
- ok
- fetched_at
- 2026-07-10 08:54:07