Butane to Ignition - How to Generate and Convert Your Configuration Files

Jan 29, 2026 · 9 mins read
Butane to Ignition - How to Generate and Convert Your Configuration Files

In the video below, we show you the simpler way to create Ignition files


Fedora CoreOS requires an Ignition file as part of the installation setup, but it’s in JSON and not so easy to read

I asked an AI to create an Ignition file for me but it didn’t work

When I pointed out the error to the AI, it corrected it, but to me that defeated the purpose

Now Butane is the name of the tool which we’re told is used to consume a Butane config file and produce an Ignition file

The Butane config file is written in YAML and much easier for us mere humans to understand

So if you want a basic installation of Fedora CoreOS, how do you use Butane to create the Ignition file you need?

Well, if that’s something you’re interested in finding out, then stick around and watch this video as that’s what we’ll be going over

Useful links:
https://github.com/coreos/butane/blob/main/docs/getting-started.md https://github.com/coreos/butane/blob/main/docs/specs.md

Expectations:
To me, the basic premise behind CoreOS is that once the OS is installed you don’t add or remove software packages

And the OS will automatically keep itself up to date

Instead you should create containers using Podman, which is installed by default, although Docker is another option if you set that up

So my goal is to install CoreOS on a machine and configure it just enough to let Ansible make various changes

In other words, a non-root user will manage containers, and I want Ansible to manage that account as well as to manage containers through that non-root account

In addition, Ansible needs to manage the settings of things like the NTP and DNS server as part of an automated way to standardise settings across platforms

In which case, I need to supply each server with an Ignition file that does just enough

But first we have to create a Butane file

Butane File:
Butane config files are YAML files based on Butane’s schema

So an example of a base config I’ll be using for my own hosting servers would be as follows

nano srv1.bu
variant: fcos
version: 1.6.0
passwd:
  users:
    - name: ansible
      ssh_authorized_keys:
        - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFhu2XQ6mrauquujA37CFoGvihBTC9Y2MpltXDth5250 ansible@homelab.lan

storage:
  links:
    - path: /etc/localtime
      target: ../usr/share/zoneinfo/Europe/London

  files:
    - path: /etc/hostname
      mode: 0644
      contents:
        inline: srv1

    - path: /etc/NetworkManager/system-connections/ens18.nmconnection
      mode: 0600
      contents:
        inline: |
          [connection]
          id=ens18
          type=ethernet
          interface-name=ens18

          [ipv4]
          address1=192.168.1.7/24,192.168.1.254
          dns=192.168.1.10;
          method=manual

          [ipv6]
          method=disabled

    - path: /etc/sysctl.d/60-disable-ipv6.conf
      contents:
        inline: |
          net.ipv6.conf.all.disable_ipv6 = 1
          net.ipv6.conf.default.disable_ipv6 = 1

    - path: /etc/sudoers.d/ansible
      mode: 0440
      contents:
        inline: "ansible ALL=(ALL) NOPASSWD: ALL"

    - path: /etc/chrony.conf
      overwrite: true
      contents:
        inline: |
          server 192.168.1.10 iburst
          driftfile /var/lib/chrony/drift
          makestep 1.0 3
          rtcsync

    - path: /etc/zincati/config.d/50-reboot-strategy.toml
      mode: 0644
      contents:
        inline: |
          [updates]
          strategy = "periodic"

          [updates.periodic]
          time_zone = "localtime"

          [[updates.periodic.window]]
          days = [ "Sun" ]
          start_time = "03:00"
          length_minutes = 60

    - path: /etc/vconsole.conf
      mode: 0644
      contents:
        inline: KEYMAP=uk

Now save and exit

What I want from this is to create a user account for Ansible which will login using SSH key authentication

Typically that means it has no password, and you can only login if you have the SSH private key

Now if you’ve no need for Ansible, put your user account into the wheel group instead, for instance

  users:
    - name: myuser
      groups:
        - wheel
      ssh_authorized_keys:
        - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFhu2XQ6mrauquujA37CFoGvihBTC9Y2MpltXDth5250 myuser@homelab.lan

The timezone is something that Ansible will keep in synch over time, but I still prefer to set that during the initial installation

Each computer needs its own hostname, so we’ll set that in this Butane file

Static IP addressing is preferred, because if the DHCP service goes down, after time so would any server relying on it. So we’ll apply a static IP address along with other IP settings

Bear in mind, the interface name is referenced in three lines of code here so you need to be careful when changing this to suit your own computer

    - path: /etc/NetworkManager/system-connections/ens18.nmconnection
          id=ens18
          interface-name=ens18

During my IT career nobody was really interested in IPv6, and so although I regularly did exams on it, this was never put to use

I still don’t have a need for it myself, so by general consensus I disable IPv6

The argument being, the less code, the less chance for bugs and security vulnerabilities; “If you don’t need it, don’t enable it”

But if you want to use IPv6, I’ve disabled it in the interface section, so you’ll need to change this

    - path: /etc/NetworkManager/system-connections/ens18.nmconnection

          [ipv6]
          method=disabled

As an extra mesasure, I’ve disabled IPv6 elsewhere in the configuration, so you’ll want to remove this as well as I want this disabled system wide

    - path: /etc/sysctl.d/60-disable-ipv6.conf
      contents:
        inline: |
          net.ipv6.conf.all.disable_ipv6 = 1
          net.ipv6.conf.default.disable_ipv6 = 1

Now typically Ansible needs elevated rights and to be able to execute privileged commands without requiring a password, otherwise any automation would fail as it would need intervention

But when it comes to CoreOS, typically an admin user is made a member of wheel, hence why I suggested that before

So why do I not just put this user in the wheel group?

Well it’s because sudo is used on other platforms I have and I want to keep the automation consistent across all of them so I can maintain them from the same playbook

So instead, I’ve granted the Ansible user sudo rights by adding a file in /etc/sudoers.d/

But if you’ve no need for that then delete that section

For security reasons, I block access to Internet NTP servers, so I’ve re-configured Chrony to use an internal NTP server

Granted Ansible might be used to change this at some point in the future, but the computer needs to begin with something to synch its clock to

Now CoreOS will keep itself up to date, but I want to limit when the computer will reboot, otherwise it will update and immediately reboot which can be disruptive

In which case, we’ll set a date and time for when reboots are allowed

So in this example, it means that even if there’s an update staged on Monday, the computer won’t reboot until somewhere between 03:00 and 04:00 on Sunday

But bear in mind, this is very strict

If you reboot the computer prior to that date and time, it will not use the opportunity to switch to the new version

And if misses the window, due to say a power outage on Sunday, you’ll then have to wait until the next Sunday before it will switch over automatically

Fortunately though you can manually override this if an upgrade becomes a priority

Like other minimal operating systems I’ve noticed it complains about the Locale missing during the installation, although it will revert to C.UTF-8

So I looked into changing this but it requires adding a language pack

In addition, Ansible relies on Python 3 on the host to then run a script on the target host

Now I layered the extra software required, as part of an initial build and everything was working fine

But it lead to a whole heap of upgrade problems due to library version mismatches and the way in which CoreOS is designed

The expectation is if you want to run applications, run them as containers

Now I can change the keyboard without issue and that’s extremely important

But since the expectation is to run applications as containers, trying to shoehorn a different Locale into the host OS isn’t worth the changes it would bring

Python3 is a different story as I need to use Ansible. But there are ways and means to deal with that, without making OS changes

As an aside, by default CoreOS has a user called core which you can use, but I create my own super user account because core is public knowledge and it’s usually harder to hack a computer if you don’t even know the user account

Transpiling:
Like I said before, Butane is a tool to consume a Butane config file and produce an Ignition file

But I must admit the word transpiling just doesn’t sit well with me

It sounds more like someone travelling country to country putting telegraph poles in the ground

Yet that’s the definition of using the Butane tool to create an Ignition file

You can set this up as a container but what if you don’t have anything to run containers?

Well you can also download this as a binary and use that instead and that makes more sense when you’re starting from scratch

In which case, we’ll do that

curl -L https://github.com/coreos/butane/releases/latest/download/butane-x86_64-unknown-linux-gnu -o butane
chmod +x butane
sudo mv butane /usr/local/bin/

In other words, we download the file, make it executable then move it to /usr/local/bin where it’s more accessible

First we’ll test that it can be found and used

butane --version

Then we’ll use it to create our Ignition file

butane --strict --output srv1.ign srv1.bu

We use –strict because we want this to fail if there are any issues with this Butane file; The last thing we need is a an Ignition file with syntax errors for instance

And we use –output to create the Ignition file, as otherwise the result will just be output to the screen

To give you some perspective, this is what the Ignition file looks like

cat srv1.ign

{"ignition":{"version":"3.5.0"},"passwd":{"users":[{"name":"ansible","sshAuthorizedKeys":["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFhu2XQ6mrauquujA37CFoGvihBTC9Y2MpltXDth5250 ansible@homelab.lan"]}]},"storage":{"files":[{"path":"/etc/hostname","contents":{"compression":"","source":"data:,srv1"},"mode":420},{"path":"/etc/NetworkManager/system-connections/ens18.nmconnection","contents":{"compression":"gzip","source":"data:;base64,H4sIAAAAAAAC/0zMwarCMBCF4f08y73VKbVWJE9Suhg7RxpoJiUZBd/ehYouz+HjH+dshtljtomiBljlgfyxIcAXFINTNEe5yox/k4Q3oTFu924iUS2olQOf2ob7oeHmuGu7v+9sDx2p1R/A+zMl+JI1JLGbrK9aP31ejVUuK5SeAQAA//9rlDtpnwAAAA=="},"mode":384},{"path":"/etc/sysctl.d/60-disable-ipv6.conf","contents":{"compression":"","source":"data:,net.ipv6.conf.all.disable_ipv6%20%3D%201%0Anet.ipv6.conf.default.disable_ipv6%20%3D%201%0A"}},{"path":"/etc/sudoers.d/ansible","contents":{"compression":"","source":"data:,ansible%20ALL%3D(ALL)%20NOPASSWD%3A%20ALL"},"mode":288},{"overwrite":true,"path":"/etc/chrony.conf","contents":{"compression":"","source":"data:,server%20192.168.1.10%20iburst%0Adriftfile%20%2Fvar%2Flib%2Fchrony%2Fdrift%0Amakestep%201.0%203%0Artcsync%0A"}},{"path":"/etc/zincati/config.d/50-reboot-strategy.toml","contents":{"compression":"gzip","source":"data:;base64,H4sIAAAAAAAC/2TNMQrDMAyF4V2nED5AMBQ6FHKKjsEYE4tU4MjBlgnp6YuGTF1/Pukt48hJqQfo2pLSduGM7qDGNfPqAG4w3S2A8k7xW4WMlrqmYsXsH55OllzPECCnq+OMC7r3EIe2l5pGu7Q3/vHy3kEh2fQTd5ahZP7p4RcAAP//CiAq/qMAAAA="},"mode":420},{"path":"/etc/vconsole.conf","contents":{"compression":"","source":"data:,KEYMAP%3Duk"},"mode":420}],"links":[{"path":"/etc/localtime","target":"../usr/share/zoneinfo/Europe/London"}]}}

That maybe be fine for a computer, but definitely not for us

So if you want one final check run this command

butane --pretty --strict srv1.bu

This time you’ll see the output in JSON but in a more easier to read format

But in any case, hopefully, you’ll now find it much easier to create Ignition files by using Butane

Sharing is caring!