Unattended ISO Installer Tutorial#

This guide builds an ISO that installs a fully configured Ubuntu system with no operator input: the installer picks the target disk, partitions it, installs the OS from packages carried on the ISO, configures users, SSH keys, proxy, cloud-init, and boot-time provisioning, makes the installed disk the first boot device, and reboots.

The worked examples are the Intel EdgePack unattended ISO templates. Each one installs the package set of the matching EdgePack raw template plus acpica-tools, in a desktop and a headless server variant for Ubuntu 24.04 and Ubuntu 26.04. They pin kernel 7.0.0-38, which edge-gfx-dkms 7.0 builds against; the raw templates still pin 7.0.0-34.

Field details are in the Image Template Reference.

How the installer works#

An imageType: iso build resolves and downloads every package at compose time and stores them on the ISO as a local package repository, together with the merged template. On the target, the ISO boots into an installer environment whose systemConfig.initramfs.template is default-initrd-unattended-x86_64.yml; it runs ICT’s live-installer, which installs the same system a raw build would produce. The install needs no network access to package repositories.

1. Describe the system#

Everything below is set in the ISO template.

Target disk#

disk:
  path: ""                 # empty: choose by policy
  selectionPolicy:
    strategy: largest      # first | largest | fastest
    excludeRemovable: true
    requireEmpty: true     # only disks without partitions

Set path to install onto a specific disk instead. The installer refuses disks too small for the partition layout.

Partitions that adapt to the disk size#

A negative offset is measured back from the end of the selected disk, so root fills whatever space is left while the data and swap partitions keep a fixed size:

  partitions:
    - id: rootfs
      type: linux-root-amd64
      start: 513MiB
      end: "-20GiB"
      fsType: ext4
      mountPoint: /
    - id: data
      type: linux
      start: "-20GiB"
      end: "-4GiB"
      fsType: ext4
      mountPoint: /data
      mountOptions: defaults,nofail
    - id: swap
      type: linux-swap
      start: "-4GiB"
      end: "0"
      fsType: linux-swap

Each mounted partition and each swap partition gets an /etc/fstab entry. Because the partitions are created on the real target disk, no resize is needed after installation.

Users and SSH keys#

systemConfig:
  packages:
    - openssh-server
  users:
    - name: admin
      password: "$6$<salt>$<hash>"   # openssl passwd -6
      groups: [sudo]
      sudo: true
      shell: /bin/bash
      home: /home/admin
      sshAuthorizedKeysFiles:
        - additionalfiles/base-platform/admin.pub

Key files are read at build time; keys can also be listed inline under sshAuthorizedKeys, or added per build with --ssh-authorized-key admin=$HOME/.ssh/id_ed25519.pub.

Proxy#

  proxy:
    httpProxy: http://proxy.example.com:3128
    httpsProxy: http://proxy.example.com:3128
    noProxy: localhost,127.0.0.1,.example.com

The proxy is written to /etc/environment, apt’s configuration, and the default environment of every systemd service.

Cloud-init#

  cloudInit:
    enabled: true
    userDataFile: additionalfiles/base-platform/user-data

cloud-init is installed and reads the user-data from the NoCloud seed on first boot. Meta-data with a unique instance ID is generated for each installed system.

Boot-time provisioning scripts#

  provisioning:
    scripts:
      - name: platform-bootstrap
        local: additionalfiles/base-platform/10-platform-bootstrap.sh
        final: /usr/local/sbin/platform-bootstrap.sh
        stage: first-boot     # or every-boot
        order: 10

Each script runs from its own systemd unit (ict-provision-<NNN>-<name>.service) after the network is online, in order. A first-boot script that fails is retried on the next boot.

Developer source archives and other artifacts#

Copy any file with additionalFiles. Files larger than 4 GiB are supported on the ISO.

  additionalFiles:
    - local: additionalfiles/base-platform/platform-src.tar.gz
      final: /opt/platform/src/platform-src.tar.gz

APT upgrade policy#

packageRepositories:
  - codename: platform-stack
    url: https://ppa.example.com/platform/ubuntu
    pkey: https://ppa.example.com/platform/key.gpg

systemConfig:
  aptPolicy:
    upgradeAllowedRepos:
      - platform-stack

Only the listed repositories (plus the Ubuntu archives) may upgrade installed packages. The Ubuntu archives, including security updates, are only blocked when systemConfig.immutability.enabled is true, in which case nothing upgrades.

Boot entry#

  bootloader:
    bootEntryPolicy: preserve   # or exclusive

The installer adds an ICT firmware boot entry and puts it first in BootOrder. preserve keeps the existing BootOrder after it. Existing boot entries are kept, except entries labelled exactly ICT from an earlier install, which are replaced once the new entry is in place.

2. Build#

go build -buildmode=pie -o ./build/live-installer ./cmd/live-installer
go build -o ./build/image-composer-tool ./cmd/image-composer-tool   # or: earthly +build

./build/image-composer-tool validate --merged image-templates/ubuntu24/ubuntu24-x86_64-edgepack-unattended-iso.yml
sudo -E ./build/image-composer-tool build \
  --ssh-authorized-key "admin=$HOME/.ssh/id_ed25519.pub" \
  image-templates/ubuntu24/ubuntu24-x86_64-edgepack-unattended-iso.yml

The shipped additionalfiles/base-platform/admin.pub contains only comments and the template sets no password, so the --ssh-authorized-key flag (or a real key in that file) is what lets you log in to the installed system. Create a key first with ssh-keygen -t ed25519 if you do not have one.

The build output directory (<work_dir>/ubuntu-ubuntu24-x86_64/imagebuild/<system-config-name>/) contains:

File

Content

edgepack-os-image-ubuntu-unattended-24.04.iso

The installer ISO

spdx_manifest_deb_*.json

SPDX SBOM of the system the ISO installs

edgepack-os-image-ubuntu-unattended-24.04.composition.json

Composition manifest: base OS, base and template repositories, packages with versions and checksums, kernel, provisioning inputs, and every additional file with its SHA-256

template-dump.yaml

The merged template the installer uses

The installed system also carries its own SBOM in /usr/share/sbom/.

The shipped templates create a sudo admin user with no password, so a build needs a credential: set password in the template, or pass a public key with --ssh-authorized-key admin=$HOME/.ssh/id_ed25519.pub on the build command. Without one, the build stops before creating the ISO instead of producing an installer that cannot finish.

3. Verify in QEMU#

qemu-img create -f raw /tmp/target.raw 60G
cp /usr/share/OVMF/OVMF_VARS_4M.fd /tmp/vars.fd
sudo qemu-system-x86_64 -m 4096 -enable-kvm -cpu host \
  -drive if=pflash,format=raw,readonly=on,file=/usr/share/OVMF/OVMF_CODE_4M.fd \
  -drive if=pflash,format=raw,file=/tmp/vars.fd \
  -drive file=/tmp/target.raw,format=raw,if=virtio \
  -cdrom <built.iso> -nic user,model=virtio-net-pci,hostfwd=tcp::2222-:22 \
  -nographic -no-reboot

The installer logs Unattended install completed successfully and QEMU exits on the reboot. Boot the installed disk (same command without -cdrom and -no-reboot), then check:

ssh -p 2222 admin@localhost                       # key-based login
id admin; getent passwd admin                     # groups, shell, home
grep -i proxy /etc/environment; apt-config dump | grep -i proxy
systemctl status 'ict-provision-*'                # provisioning ran
cloud-init status --long                          # status: done
lsblk; findmnt /data; swapon --show; df -h /      # layout fills the disk
efibootmgr                                        # ICT first in BootOrder
apt-cache policy; apt-get -s upgrade              # upgrade policy

Troubleshooting#

  • The installer log is /tmp/unattended-installer.log in the installer environment. On failure the installer drops to a shell instead of rebooting.

  • A failed first-boot script: journalctl -u ict-provision-<NNN>-<name>. Fix the cause and reboot, or remove the stamp in /var/lib/image-composer-tool/provisioned/ to run it again.

  • cloud-init did not run: check /var/lib/cloud/seed/nocloud/ and /etc/cloud/cloud.cfg.d/90_ict_datasource.cfg on the installed system.