Custom AMIs for BYOC Runners

Build your own AMI for WarpBuild BYOC runners on AWS. Linux and Windows image requirements, a packer template, the add-image step, and the rebuild cost model.

A custom AMI lets a WarpBuild BYOC runner boot with your compilers, SDKs, and internal certificates already installed, so a GitHub Actions job starts working instead of installing. On AWS the path is: build the AMI in your own account, register it on the custom images page, attach it to a custom runner set, then point runs-on at the resulting warp-custom- label.

This page covers the documented image requirements for Linux and Windows AMIs, a packer template and the workflow that builds it, the registration step, and the maintenance model with the rebuild cadence priced against per-job install time.

Overview

BYOC runs on AWS, GCP, and Azure, and each cloud takes its own image format. On AWS the unit is an AMI, and the custom VM images guide supports Linux and Windows based AMIs. The custom AMI path applies to the Linux and Windows runner sets you declare inside your own AWS account, and starts from a connected cloud account and stack as described on BYOC runners on AWS.

Three properties are worth knowing before you build anything.

Images are scoped to the stack region. The add-image dialog lists the images in the region your stack was created in. A stack region cannot be changed after creation, so an AMI built in us-east-1 is only selectable by a stack in us-east-1. Multi-region fleets copy the AMI per region and register each copy.

A custom image carries no image fee. The custom VM images guide states there is no additional cost for using custom VM images. A BYOC runner set on your own AMI bills $0.002 per runner minute in WarpBuild fees for Linux and $0.002 per runner minute for Windows, the same as a stock image, with EC2 and EBS on your own AWS bill (WarpBuild pricing, checked 2026-08-13).

The base image is yours to keep valid. WarpBuild launches the instance and registers a just-in-time runner against your GitHub organization; everything above the agent is what you baked in. The term itself is defined on runner image, and custom runner images for GitHub Actions covers the container image path for hosted runners.

Architecture

The WarpBuild agent is what turns your AMI into a GitHub Actions runner, and its requirements are the whole contract.

RequirementLinuxWindowsWhy it exists
Init systemsystemd required, so Ubuntu and Amazon Linux 2023 based AMIs workNot applicableThe WarpBuild agent runs as a systemd unit
Must be installedcurl, wget, bash, jq, libicuaria2, reachable through the system PATHlibicu is a .NET runtime dependency of the GitHub Actions runner; aria2 replaces the slow default Windows download method
Must not be removedtar, gzip, coreutils (id, chpasswd, chown, tr, sed), shadow-utils (useradd, usermod), systemdDefault system toolingPresent on supported distros by default and used during runner setup
GeneralizationNot requiredEC2 instance must be sysprepped before the image is createdAn unsysprepped Windows AMI produces instances that share machine identity
Job userrunner user with passwordless sudoJobs run as runneradmin, the same user GitHub's Windows runners useAdded if absent, so user-scoped environment variables set under another account are not visible

Two Windows details catch teams during the first boot test. User-scoped environment variables set while you were building the image belong to the account you were logged in as, and jobs run as runneradmin, so move them to machine level. RDP port 3389 is not opened by a new stack, so debugging a Windows AMI interactively means adding an inbound rule for your own CIDR block first. The Windows BYOC runners on AWS page covers the instance sizing and disk throughput those runners want.

Configuration

Build the AMI with packer against a supported base, install the required packages, and create the runner user and the tool cache directories the GitHub Actions runner expects.

variable "aws_region" {
  type    = string
  default = "us-east-1"
}

locals {
  version = "1.0.0"
}

source "amazon-ebs" "my-custom-ci" {
  region        = var.aws_region
  instance_type = "t3.micro"
  ami_name      = "my-custom-ci-v${local.version}"

  tags = {
    Name       = "my-custom-ci-v${local.version}"
    team       = "platform"
    build_date = "{{timestamp}}"
    version    = local.version
    provider   = "packer"
    product    = "github-actions"
  }

  source_ami_filter {
    filters = {
      name                = "ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.*-amd64-server-*"
      root-device-type    = "ebs"
      virtualization-type = "hvm"
    }
    owners      = ["099720109477"]
    most_recent = true
  }

  ssh_username = "ubuntu"

  launch_block_device_mappings {
    device_name           = "/dev/sda1"
    volume_type           = "gp3"
    volume_size           = 8
    delete_on_termination = true
  }
}

build {
  sources = ["source.amazon-ebs.my-custom-ci"]

  provisioner "shell" {
    inline = [
      "sudo groupadd runner || echo 'Group runner already exists'",
      "sudo useradd -m -g runner -s /bin/bash runner || echo 'User runner already exists'",
      "echo 'runner ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/runner",
      "sudo chmod 440 /etc/sudoers.d/runner",
      "sudo apt-get update",
      "sudo apt-get install -y curl wget unzip git jq libicu-dev",
      "sudo mkdir -p /opt/hostedtoolcache/Python /opt/hostedtoolcache/Node /opt/hostedtoolcache/go",
      "sudo chown -R runner:runner /opt/hostedtoolcache",
      "sudo mkdir -p /home/runner/_work /home/runner/_tool /home/runner/_temp",
      "sudo chown -R runner:runner /home/runner/_work /home/runner/_tool /home/runner/_temp",
    ]
  }
}

Build it from GitHub Actions so the image has the same review path as the rest of the repository.

name: build-runner-ami
on:
  workflow_dispatch:
  push:
    branches: [main]
    paths:
      - "packer/github-actions-ami/**"

permissions:
  contents: read
  id-token: write

jobs:
  build-ami:
    runs-on: warp-ubuntu-latest-x64-4x
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/PackerBuildRole
          aws-region: us-east-1
      - uses: hashicorp/setup-packer@v3
      - run: packer plugins install github.com/hashicorp/amazon
      - run: packer validate packer/github-actions-ami/my-custom-ci.pkr.hcl
      - run: packer build packer/github-actions-ami/my-custom-ci.pkr.hcl

Registration is four steps, and only the first is a one-time setup.

  1. Create a WarpBuild stack in the region the AMI lives in.
  2. Open the custom images page, press Add Image, and pick the AMI from the list of images in that region.
  3. Create a custom runner set that uses the image, along with the instance type priority list and the disk configuration.
  4. Reference the runner set in runs-on using the full runner ID, which is the runner name with the warp-custom- prefix.
jobs:
  build:
    runs-on: warp-custom-ci-linux-large
    steps:
      - uses: actions/checkout@v4
      - run: make build

If you run per-job setup or teardown scripts, bake the paths into the image rather than into every workflow. GitHub's ACTIONS_RUNNER_HOOK_JOB_STARTED and ACTIONS_RUNNER_HOOK_JOB_COMPLETED variables are already used by WarpBuild to orchestrate runners, so set the WARPBUILD_ prefixed alternatives system wide instead, in /etc/environment on Linux or as machine level variables on Windows. Scripts must end in .sh or .ps1, take an absolute path, and a non-zero exit fails the job.

Operations

The maintenance question is how often to rebuild the AMI rather than install per job, and it prices out cleanly.

Take a fleet of 20,000 Linux jobs per month, each spending 90 seconds installing the same toolchain. That is 30,000 runner minutes per month, or 500 EC2 instance hours, spent on work an image could have done once.

Line itemVolumeWarpBuild feeBilled by your AWS account
Per-job install, 90 s across 20,000 jobs30,000 runner minutes$60 per month at $0.002 per BYOC runner minute500 instance hours plus attached EBS
Weekly AMI rebuild on warp-ubuntu-latest-x64-4x4 builds of 15 minutes$0.48 per month at $0.008 per minuteNone, the build runs on a hosted runner

Rates are from the WarpBuild pricing page, checked 2026-08-13. The breakeven is low: a weekly rebuild costs $0.48 in runner minutes, which is 240 BYOC runner minutes at $0.002, so the image pays for itself once the pipeline spends more than four hours a month on install steps the image could carry. Below that threshold, per-job installation is the cheaper operating model and the AMI stays a thin base.

Three operational habits keep the rebuild loop boring.

Version the AMI name and never mutate one in place. The template above stamps my-custom-ci-v1.0.0 and tags build_date and version. Registering the new AMI and repointing the runner set makes rollback a two-minute change back to the previous image ID.

Rebuild on a schedule and on dependency change. The workflow above triggers on pushes under packer/. Adding a weekly schedule trigger picks up base image security updates without anyone filing a ticket for it.

Automate the Windows sysprep. The GUI path works for a first image and does not scale. HashiCorp documents sysprep commands for Windows AMIs that run from packer or from a plain PowerShell session.

For the scope question in one paragraph, see does BYOC support custom VM images.

FAQ

What does a custom AMI cost on WarpBuild BYOC?

Nothing extra. The custom VM images documentation states there is no additional cost for using custom VM images, so a runner set on your own AMI bills the same $0.002 per runner minute in WarpBuild fees as one on a stock image, plus the EC2 and EBS charges on your own AWS account (WarpBuild pricing, checked 2026-08-13).

Which packages must be present in a Linux AMI?

curl, wget, bash, jq, and libicu, which the GitHub Actions runner needs as a .NET runtime dependency. The distro also has to use systemd, because the WarpBuild agent runs as a systemd unit, and tar, gzip, coreutils, shadow-utils, and systemd must not be removed from the base image.

Why does a Windows AMI need aria2 and a sysprep pass?

WarpBuild uses aria2 to download runner artifacts on Windows because the default Windows method is slow, so aria2 has to be present and reachable through the system PATH. On AWS the EC2 instance must also be sysprepped before you create the image, otherwise the AMI is not generalized and instances launched from it collide on machine identity.

Start with $10 in free credits

Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.