Running a script on bootup via Ignition

Introduction

With Fedora CoreOS Ignition is being used to configure nodes on first boot. While Ignition json configs are not intended to be a tool that users typically interact with (we are building tooling like fcct for that) I’ll show you an example of how to deliver a script to a Fedora CoreOS (or RHEL CoreOS) host so that it will be run on first boot.

Write the script

Let’s say we have a small script we want to run that updates the issuegen from console-login-helper-messages to output the node’s public IPv4 address on the serial console during bootup.

Here is the small script:

#!/bin/bash
echo "Detected Public IPv4: is $(curl https://ipv4.icanhazip.com)" > \
         /run/console-login-helper-messages/issue.d/30_public-ipv4.issue

We’ll store this script into /usr/local/bin/public-ipv4.sh when we provision the machine.

Write the systemd unit

We need to call the script we made above by using a systemd unit. Here is one that works for what we want:

[Unit]
Before=console-login-helper-messages-issuegen.service
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/public-ipv4.sh
[Install]
WantedBy=console-login-helper-messages-issuegen.service

We’ll call this unit issuegen-public-ipv4.service.

Construct the Ignition config

We’ll start from a template Ignition config called config.ign.in:

NOTE: You’ll need to substitute in your ssh public key if trying this at home.

{
  "ignition": {
    "version": "3.0.0"
  },
  "passwd": {
    "users": [
      {
        "name": "core",
        "groups": [
          "sudo"
        ],
        "sshAuthorizedKeys": [
          "ssh-rsa AAAA"
        ]
      }
    ]
  },
  "systemd": {
    "units": [
      {
        "contents": "SYSTEMD_UNIT_CONTENTS",
        "enabled": true,
        "name": "issuegen-public-ipv4.service"
      }
    ]
  },
  "storage": {
    "files": [
      {
        "contents": {
          "source": "data:text/plain;base64,SCRIPT_CONTENTS"
        },
        "mode": 493,
        "overwrite": true,
        "path": "/usr/local/bin/public-ipv4.sh"
      }
    ]
  }
}

And then substitute in the two files we created earlier using a small sed script:

$ cat sed.sh 
#!/bin/bash
SCRIPT_CONTENTS=$(base64 --wrap 0 public-ipv4.sh)
SYSTEMD_UNIT_CONTENTS=$(sed 's|$|\\\\n|g' < issuegen-public-ipv4.service | tr -d '\n')
sed -e "s|SYSTEMD_UNIT_CONTENTS|${SYSTEMD_UNIT_CONTENTS}|" \
    -e "s|SCRIPT_CONTENTS|${SCRIPT_CONTENTS}|"
$ bash sed.sh < config.ign.in > config.ign

The input Ignition template and the resulting config.ign can be downloaded: template config.ign.

NOTE It’s always a good idea to run ignition-validate from the same Ignition version as you are targeting on the configs before booting the instances.

Boot an instance

You can then take that Ignition config and boot an instance with it. In my tests it is working and shows something like the following on the serial console right before the login prompt:

SSH host key: SHA256:I/cFFyO5XUOyUw1O5oLLvcvgGzWNhyAPT4O7QKdehgU (ECDSA)
SSH host key: SHA256:3TBdV//KC5pFnyAljGMuMpDlQplBdosz/RwYQUQNSRU (ED25519)
SSH host key: SHA256:x1wkk4/Cc4mq69R5cm41AEzuRnwMXlWkqY8LmpxgFCw (RSA)
eth0: 10.10.10.20 fe80::5054:ff:fe0a:6210
Detected Public IPv4: is 54.91.55.146
localhost login:

And the service shows it was launched successfully:

$ systemctl status issuegen-public-ipv4.service 
● issuegen-public-ipv4.service
   Loaded: loaded (/etc/systemd/system/issuegen-public-ipv4.service; enabled; vendor preset: enabled)
   Active: inactive (dead) since Wed 2019-11-06 17:34:35 UTC; 21min ago
  Process: 1089 ExecStart=/usr/local/bin/public-ipv4.sh (code=exited, status=0/SUCCESS)
 Main PID: 1089 (code=exited, status=0/SUCCESS)

Nov 06 17:34:34 localhost systemd[1]: Starting issuegen-public-ipv4.service...
Nov 06 17:34:34 localhost public-ipv4.sh[1089]:   % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
Nov 06 17:34:34 localhost public-ipv4.sh[1089]:                                  Dload  Upload   Total   Spent    Left  Speed
Nov 06 17:34:35 localhost public-ipv4.sh[1089]: [237B blob data]
Nov 06 17:34:35 localhost systemd[1]: issuegen-public-ipv4.service: Succeeded.
Nov 06 17:34:35 localhost systemd[1]: Started issuegen-public-ipv4.service.

Appendix A: Ignition spec V2

If you are on a platform using Ignition Spec V2 (RHEL CoreOS/OpenShift) then you need a slightly different ignition config. I created a Spec V2 config.ign.in for this example and ran it on RHEL CoreOS to verify it worked. I have created a Spec V2 version of the template and final config: template config.ign.

As always please run ignition-validate from the same Ignition version as you are targeting on the configs before booting the instances.

Appendix B: Making the script only run once

If you’d prefer for your script to only run once (for whatever reason) you can do that too. One very generic way is to lay down a file that can be used to disable future runs:

[Unit]
Before=console-login-helper-messages-issuegen.service
After=network-online.target
ConditionPathExists=!/var/lib/issuegen-public-ipv4
[Service]
Type=oneshot
ExecStart=/usr/local/bin/public-ipv4.sh
ExecStartPost=/usr/bin/touch /var/lib/issuegen-public-ipv4
[Install]
WantedBy=console-login-helper-messages-issuegen.service

Appendix C: Complex directory structures and Ignition

If you have many files you’d like to deliver then you may consider using a tool like filetranspiler.

You can pass it a base Ignition config (Spec 2 or Spec 3) and it will output an updated Ignition config with a files section that will work.

For example, if I have a local directory ./fakeroot with a bunch of files in it then I can call:

./filetranspile -i config.ign -f ./fakeroot/ -p -o new-config.ign

Enjoy!