Customizing a Flatcar image

    While Ignition cloud instance userdata is the preferred way of customizing an installation, it can be limiting when the customization concerns the kernel boot arguments or when no cloud instance userdata mechanism is in place. The partition with the OS /usr filesystem can’t be modified because it is signed and gets auto-updated. Other partitions like the boot partition, the OEM partition, or even the root partition are open for customization. The boot partition can hold an additional EFI boot loader, the OEM partition can hold a GRUB file for the kernel arguments and possibly a default and/or base Ignition configuration, the root partition can hold the OS configuration and additional binaries. Note: Important is that you never boot the image because the first-boot initialization would make all your instances identical, causing problems with the update server, skips the regeneration of SSH host keys, and prevents Ignition from running. In case you have to do so for running Packer and/or Ansible, see the last section for common problems.

    Mounting a partition for customization

    The generic Flatcar Container Linux image (.bin) can be directly attached as loop device on a Linux host and mounted after decompression with bunzip2 or lbunzip2. The partition to modify needs to be specified by its number:

    # PART can be 1 (boot), 6 (OEM), 9 (ROOT)
    PART=1
    LOOP=$(sudo losetup --partscan --find --show flatcar_production_image.bin)
    TARGET=$(sudo mktemp -d -p /mnt --suffix -flatcar)
    sudo mount "${LOOP}p${PART}" "$TARGET"
    # Now do your changes on "$TARGET"...
    # Cleanup:
    sudo umount "${TARGET}"
    sudo rmdir "${TARGET}"
    sudo losetup -d "${LOOP}"
    

    If you need to modify the QEMU qcow2 image or an vmdk image, you need to either convert it first to a raw image:

    qemu-img convert -f qcow2 -O raw flatcar_production_qemu_image.img flatcar_production_qemu_image.bin
    

    Or you need to use the guestmount utility (from libguestfs ), it can run as regular user:

    # PART can be 1 (boot), 6 (OEM), 9 (ROOT)
    PART=1
    TARGET=$(mktemp -d -p /tmp --suffix -flatcar)
    guestmount -m "/dev/sda${PART}" -a flatcar_production_qemu_image.img "$TARGET"
    # Now do your changes on "$TARGET"...
    guestunmount "$TARGET"
    rmdir "${TARGET}"
    

    In case you converted the raw image for regular loop device mounting, you can also convert it back to qcow2 with qemu-img convert -O qcow2 INPUT OUTPUT.

    Example for legacy cgroup AMIs

    An example script that generates CGroup V1 AWS EC2 images to be uploaded as AMIs can be found here .

    Customizing the boot partition

    Using the above command the boot partition with the EFI binaries can be mounted to place additional firmware on it, e.g., Raspberry Pi 4 UEFI Firmware or similar.

    Customizing the OEM partition

    The OEM partition is the most common place for modifications, it also is what makes the various offered Flatcar cloud images different because it can hold mandatory and/or fallback Ignition configurations, and the grub.cfg file for kernel arguments.

    The OEM partition is also useful to force a particular Ignition configuration to be used. For example, flatcar-install offers to write a config.ign Ignition file to the OEM partition through the -i flag. This file is used as preferred Ignition configuration even when Ignition cloud instance userdata is present. With the special oem:/// file URL the config can copy files from the OEM Partition to the root filesystem (note: in case you have many binaries, the OEM partition may be too small and you have to either host them somewhere or place them directly on the root filesystem, see the next section).

    As done on most offered Flatcar cloud images, two additional Ignition files can be placed on the OEM partition and have broader purpose, independent of whether a config.ign Ignition file is used, the Ignition kernel command line URL, or Ignition cloud instance userdata. The first is base/base.ign which is always executed as basic mandatory setup. The second file base/default.ign has a special fallback function and gets executed only if the found instance userdata is not Ignition JSON. The common content of the file is to define a systemd service via Ignition that runs coreos-cloudinit to process the instance userdata later. Good examples are base/base.ign and base/default.ign files used for Equinix Metal images as they also make use of the oem:/// source URL to refer to a file placed on the OEM partition.

    The grub.cfg file gets sourced by GRUB to set up the OEM ID which is used by systemd units to be started conditionally, or to set up kernel parameters like the Ignition config URL (ignition.config.url, to fetch the preferred config remotely), or settings required for the hardware. Again, a good example is the grub.cfg file used for Equinix Metal images to set the OEM ID and the kernel parameter flatcar.autologin to be able to use the serial console without having to configure a user password.

    Customizing the root partition

    To pre-configure the OS you can place binaries and configuration files directly on the root filesystem. The recommended way, however, is to use a base/base.ign or config.ign Ignition file in the OEM partition. The advantage is that a base/base.ign file even works when the user has the root filesystem recreation option specified in Ignition which reformats the root filesystem and discards any changes placed there directly.

    When modifying the root filesystem you should make sure that you only copy files over that are safe to copy, e.g., you can place binaries into /opt/bin or configuration files under /etc but you shouldn’t initialize the root filesystem by booting it, even with a chroot (and calling systemctl there or even booting it up as container because this leads to the traps described in the next section, be aware). When you place systemd services under /etc/systemd/system/my.service and they have WantedBy=multi-user.target in the [Install] section you can pre-enable them with a symlink from /etc/systemd/system/multi-user.target.wants/my.service to /etc/systemd/system/my.service.

    You can even pre-populate the container image story by copying the folders /var/lib/docker and /var/lib/containerd over from a booted Flatcar instance.

    Customize /var/lib/docker

    You can pre-populate /var/lib/docker to provide a ready-to-use docker environment with images and containers.

    One solution is to setup the Docker environment on another Flatcar instance and archive /var/lib/docker with tar for example, then use the method above to un-tar into root partition (9). This requires setting up a Flatcar instance and communicate with the OS to copy the content of /var/lib/docker to your build machine.

    A more convenient way is to use Docker-in-Docker on any Docker environment on which you have privileged access.

    You start by running a Docker-in-Docker container:

    # Run docker-in-docker in the backgroud.
    # We mount local directory as a location to send /var/lib/docker archive
    # Do NOT try to bind a directory to /var/lib/docker directly as this might
    # produce incompatible images (vfs instead of overlay2) depending on your
    # environment.
    docker run --name dind --privileged --rm -d -v $(pwd):/build docker:dind
    

    Then you can interact with the docker-in-docker environment and prepare images:

    docker exec -it dind sh
    docker pull nginx
    

    Create the tar archive that contains your Docker environment:

    # We mounted the /build directory to copy the archive
    tar -cf /build/docker-images.tar /var/lib/docker
    

    During the build of your Flatcar image, you can mount the root partition (9) and extract the tar archive:

    # We mounted root partition (9) on /mnt
    tar -xf /build/docker-images.tar -C /mnt
    

    You can now unmount /mnt and finish preparing your final image.

    Customization through booting with Packer, VMware base VMs, or chroot/systemd-nspawn

    This section serves as a big warning. If you use a booted image, even if it was only booted by being a chroot or a systemd-nspawn container, you will get a lot of problems. Please check the OEM and the root partition section above for a saner way of pre-configuring the image. If you try to use Packer to customize the image, or want to use a once booted VMware base VM, or even just accidentially booted the image once for testing, you created an OS state that is hard to get rid off. It causes security issues and difficult to debug behavior changes, please use the above mechanisms to modify the image through mounting and copying because this is easier, safer, and faster.

    If you still want to continue with customization through booting, here are some common traps, but there can be more depending on the software components that are involved and if you are not an expert on the software components and their respective state files, you should reconsider your choice. The first and easiest problem is that the /boot/flatcar/first_boot flag file is lost which normally triggers Ignition to run on first boot. You would have to recreate this file. More tricky is the /etc/machine-id file which you have to delete, not only truncate, because this file is used not only for the identification of the instance but also to trigger systemd first-boot semantics which take care of enabling services through presets. The machine ID must also be unique for the update server to work correctly, otherwise it will not hand out updates to your instance. Another problem are the generated SSH host keys which you have to delete, otherwise each instance base on this image will have the same host keys and once the image is accessible everyone can impersonate your servers. More problems come with weak account credentials used for the setup, e.g., when you have a dummy account with a password you have to remove the account again, and if you set up dummy SSH keys for the core user as common with Vagrant, you have to remove them, too. If for bootstrapping you used a config.ign file in the OEM partition, this, too, has to be removed. You can have a look at the image-builder Packer and Ansible configuration which avoids most of the common pitfalls but, again, this is not a complete list because it depends on the software components you interact with.