- September 14, 2026
- 7 min read
Provisioning Devices and Onboarding to FlightCTL
This post picks up where the last one left off. With FlightCTL running on EKS behind Keycloak OIDC and RBAC groups wired up, the piece still missing is the other half of the fleet: an operating system image with the FlightCTL agent baked in, an AMI built from that image, and a handful of test devices that enroll into the fleet on first boot.
All the Make targets, Ansible playbooks, and cloud-init templates referenced below live in the edge-fleet-management repo, alongside the Terraform and Helm values used for the EKS setup.
Building a bootc image with the FlightCTL agent
bootc treats the operating system itself as a container image: the base OS, the FlightCTL agent, and any other packages get built into a single OCI image, and updates are applied transactionally with rpm-ostree rather than through package-by-package drift. The base image target builds and pushes that image to a registry:
make base REGISTRY=<specify>Under the hood, base is three podman commands: build images/fedora-bootc-base/Containerfile for --arch amd64, tag it for the registry, and push. REGISTRY, BOOTC_BASE_IMAGE, and BOOTC_BASE_IMAGE_TAG are Makefile variables (defaulting to quay.io/rprakashg, fedora-bootc-base, and latest), which is why REGISTRY=<specify> on the command line is enough to point the push at your own registry.
Layering in cloud-init
The base image doesn't know anything about first-boot configuration on its own, so a second target layers cloud-init support on top of it. This is what lets us inject the admin user, SSH key, and — most importantly — the FlightCTL enrollment credentials at boot time, instead of baking device-specific secrets into the image itself:
make cloudinitConcretely, cloudinit builds images/cloud-init/Containerfile with the base image passed in as a build arg, then tags and pushes the result as ${BOOTC_BASE_IMAGE}:aws. That :aws-tagged image is the artifact make ami turns into an actual EC2 image. Cool thing about cloud-init is that it provides a standard mechanism to perform first boot configurations regardless you are provisioning in public/private cloud or virtual or bare metal instances. If you are new to cloud-init please check out this article
Building the AMI
Turning the bootc image into something EC2 can boot means importing it as an AMI, which in turn requires the AWS account to have a vmimport service role configured. An Ansible playbook handles that one-time setup:
export VAULT_SECRET=<redacted>
ansible-playbook --vault-password-file <(echo "$VAULT_SECRET") configure_aws.ymlWith vmimport in place, build the AMI:
make amiami pulls the :aws-tagged image and hands it to quay.io/centos-bootc/bootc-image-builder, which does the actual OS-image-to-AMI conversion: --type ami --rootfs xfs, your AWS credentials and container storage bind-mounted in, and an intermediate S3 bucket (bootc-images) it stages through along the way.
Vaulting device secrets
The cloud-init template used later on needs a few real secrets — an admin username, password, SSH public key, and an EC2 key pair name — so those go into an Ansible vault rather than a plain vars file:
admin_user: <redacted>
admin_user_password: <redacted>
admin_user_ssh_pubkey: <redacted>
key_name: <redacted>Create the vault and keep its password in an environment variable so the playbooks below can unlock it non-interactively:
cd playbooks
ansible-vault create vars/secrets.yamlexport VAULT_SECRET=<redacted>Provisioning test devices
The repo provisions test devices via an Ansible playbook driven by the device-without-microshift.yaml vars file — ideal for demonstrating Podman, legacy VMs, and RPM apps:
device_count in the vars file controls how many instances get launched — bump it to provision a larger batch. Update the AMI ID, subnet, and security group in the vars file to match your environment first, then run:
ansible-playbook --vault-password-file <(echo "$VAULT_SECRET") launch_instance.yaml -e @vars/device-without-microshift.yamlHow the enrollment credentials actually get onto the device
Credentials are injected using a late-binding approach rather than baked into the image at build time. The enrollment certificate generated in the previous post (flightctl certificate request --signer=enrollment --expiration=365d --output=embedded > config.yaml) produces a config.yaml containing the device's client cert, key, and the FlightCTL CA bundle. A Jinja template turns that file into cloud-init user-data:
#cloud-config
users:
- name: "{{ admin_user }}"
ssh_authorized_keys:
- "{{ admin_user_ssh_pubkey }}"
groups: ["wheel"]
passwd: "{{ admin_user_password | password_hash('sha512') }}"
shell: /bin/bash
sudo: ['ALL=(ALL) NOPASSWD:ALL']
runcmd:
- echo "Welcome to bootc on fedora" > /etc/motd
- systemctl enable --now flightctl-agent.service
write_files:
- path: /etc/flightctl/config.yaml
permissions: '0755'
owner: root:root
content: |
{{ lookup('file', 'config.yaml') | from_yaml | to_nice_yaml | indent(4) }}The Ansible EC2 module just points user_data at that template:
- name: Launch instance
amazon.aws.ec2_instance:
name: "{{ instance_name }}"
instance_type: "{{ instance_type }}"
image_id: "{{ ami }}"
subnet_id: "{{ subnet_id }}"
security_group: "{{ security_group_id }}"
count: "{{ device_count }}"
user_data: "{{ lookup('ansible.builtin.template', './templates/user-data.j2') }}"
tags:
Name: "{{ instance_name }}"No device-specific secret ever lives in the AMI itself — it's assembled fresh at launch time from the vault and the enrollment config generated for that batch of devices.
Verifying enrollment
Once an instance boots, cloud-init writes /etc/flightctl/config.yaml and starts flightctl-agent.service, which immediately calls home over the mTLS-only agent endpoint set up in the last post. Back in the FlightCTL UI, the device shows up as a pending enrollment request, waiting for an operator or admin in one of the RBAC groups created earlier to approve it.

That's the full loop: bootc image → AMI → Ansible-provisioned EC2 device → cloud-init-injected credentials → pending enrollment in FlightCTL.
In a follow up post we will look at injecting enrollment credentials into device in a FDO device onboarding flow
Related Posts
RAM GOPINATHAN
September 17, 2026
Onboarding Edge Devices to FlightCTL with FIDO Device Onboarding
How I wired FDO's owner service, a bootc image layering go-fdo-client on the FlightCTL agent base, and two Ansible playbooks together so edge devices enroll into FlightCTL with zero manual steps.
RAM GOPINATHAN
September 12, 2026
Deploying FlightCTL on EKS: Edge Fleet Management with Keycloak OIDC
A step-by-step walkthrough of standing up FlightCTL on Amazon EKS with Keycloak OIDC auth, ALB ingress, and a dedicated NLB for mTLS device enrollment.
RAM GOPINATHAN
September 9, 2026
Building an all in one FDO server infrastructure on Image Mode RHEL
This post walks through building an all in one FDO server infrastructure on Image Mode RHEL for testing purposes