BYOC Runners on AWS for GitHub Actions

WarpBuild BYOC runs GitHub Actions runners as ephemeral EC2 instances inside your own AWS account. Architecture, IAM surface, configuration, and cost.

Last verified:

Self-hosted GitHub Actions runners on AWS mean EC2 instances that register against your repositories and execute workflow jobs inside your own VPC. WarpBuild BYOC gives you that placement without the controller you would otherwise operate: WarpBuild runs the control plane, your AWS account runs the instances, and a workflow moves onto them by changing one runs-on label.

This page covers the AWS architecture, the IAM surface WarpBuild asks for, the stack and runner configuration knobs, and the split of responsibility between your team and WarpBuild. BYOC runs on AWS, GCP, and Azure; the AWS path is the one with import mode and Terraform coverage.

Architecture

BYOC on AWS splits into a control plane that WarpBuild operates and a data plane that lives entirely in your AWS account.

The control plane holds the GitHub App connection, the runner configuration, the job queue watch, and the reconciliation loop that keeps runner capacity matched to queued jobs. It never holds your build data. When GitHub queues a job whose runs-on label matches one of your custom runners, the control plane calls the AWS API with the IAM role you granted and launches an EC2 instance in your VPC.

The data plane is a CloudFormation stack in one AWS region of your account. Creating it is documented in the AWS BYOC configuration guide, and the resource graph is drawn out in the BYOC AWS architecture reference. The stack owns:

  • A VPC with public and private subnets across availability zones, and route tables carrying internet-bound traffic through the Internet Gateway.
  • A security group attached to every runner instance the stack launches. In create mode CloudFormation provisions it. In import mode you select an existing one.
  • An S3 bucket in the same region as the stack, used for the artifact cache, container layer cache, runner telemetry, and other workflow artifacts.
  • EC2 instances, EBS volumes, and launch templates created per job and destroyed after it.

Three stack properties are immutable after creation: the stack name, the S3 bucket, and the region. Pick them deliberately. A naming convention that carries the product and the region reads well six months later, for example payments-use1 and payments-use2.

The S3 bucket has two documented key layouts, which matters when you write a lifecycle policy:

<bucket_name>/<org_id>/artifact_cache/<vcs_org>/<vcs_repo>/<vcs_ref>/<version>/<key>
<bucket_name>/runner/logs/all/<runner_id>.<log_file_name>

Both prefixes grow without bound unless you expire them. The documented recommendation is a 7 day retention lifecycle rule, adjusted to your own retention policy.

Instance lifecycle

Each GitHub Actions job runs on a distinct, freshly provisioned EC2 instance that is terminated when the job completes. There is no shared state between jobs and no runner that survives to serve a second job. That property is what makes autoscaling correct rather than merely convenient: the queue depth sets the instance count, and a finished job leaves nothing behind for the next one to inherit.

Cold boot time on BYOC is handled by standby disks. A standby disk is a VM of the same instance type as the runner, booted once so that networking and machine setup complete, then shut down. When GitHub requests a runner, WarpBuild boots a standby disk if the pool has one, which puts job start at roughly 15 seconds. If the pool is empty, a fresh VM is created instead. The pool size is configured per custom runner. WarpBuild does not charge for standby disks; the shut-down VM stops billing compute while its EBS volume keeps billing storage in your account. Spot instances are not supported for standby disks, because a Spot instance can be reclaimed at any time and the pool would stop being a guarantee.

Resource naming and tagging

Everything the stack creates is discoverable by name and tag, which is what makes cost attribution work in AWS Cost Explorer.

Resource typeNaming pattern
S3 bucketswarpbuild-*
EC2 instanceswarp-*
EBS volumeswarp-*
Launch templatestmpl-warp-*

WarpBuild adds a managed-by: warpbuild tag to provisioned resources, plus a documented tag set on each runner instance: warpbuild-github-org, warpbuild-runner-labels, warpbuild-runner-id, warpbuild-stack-id, and warpbuild-stack-name. Custom tags you define on the stack are applied to everything it creates. Filtering a cost report by warpbuild-runner-labels gives you spend per runner class without any extra instrumentation.

Permissions and Access

Connecting an AWS account creates one IAM role, named warpbuild-<integration-id>, through a CloudFormation template you apply yourself. The role is the entire access surface. Its permissions are grouped into named policy contexts so that you can read, edit, or reject them one block at a time.

Policy contextWhat it is used for
FineGrainedEC2PermissionsLaunch just-in-time runners, describe instance types and offerings per region and availability zone, manage launch templates, create and deregister images, tag instances
SpotServiceLinkedRolePermissionsCreate and manage the EC2 Spot service-linked role, and pass roles to runner instances
NetworkPermissionsDescribe regions, VPCs, subnets, route tables, security groups, and internet gateways; create and delete the endpoints the stack needs in import mode
StoragePermissionsEBS snapshot actions, plus the S3 actions behind the WarpBuild cache action and runner system logs
CloudFormationPermissionsSubmit changeset requests when a connection or a stack needs an upgrade

The EC2 block is the one worth reading closely, because it is where launch authority lives:

Policies:
  - PolicyName: "FineGrainedEC2Permissions"
    PolicyDocument:
      Version: "2012-10-17"
      Statement:
        - Effect: Allow
          Action:
            - "ec2:DescribeInstanceTypes"
            - "ec2:DescribeInstanceTypeOfferings"
            - "ec2:DescribeInstances"
            - "ec2:RunInstances"
            - "ec2:CreateFleet"
            - "ec2:RequestSpotInstances"
            - "ec2:CancelSpotInstanceRequests"
            - "ec2:DescribeSpotInstanceRequests"
            - "ec2:DescribeSpotPriceHistory"
            - "ec2:CreateLaunchTemplate"
            - "ec2:DeleteLaunchTemplate"
            - "ec2:ModifyLaunchTemplate"
            - "ec2:TerminateInstances"
            - "ec2:CreateImage"
            - "ec2:DeregisterImage"
            - "ec2:DescribeImages"
            - "ec2:CreateTags"
            - "ec2:DeleteTags"
          Resource: "*"
  - PolicyName: "SpotServiceLinkedRolePermissions"
    PolicyDocument:
      Version: "2012-10-17"
      Statement:
        - Effect: Allow
          Action:
            - "iam:CreateServiceLinkedRole"
            - "iam:PassRole"
          Resource: "arn:aws:iam::*:role/aws-service-role/spot.amazonaws.com/AWSServiceRoleForEC2Spot"

Two customization paths are supported. You can edit the CloudFormation template on the AWS redirect page before you apply the connection stack, or you can edit the role after it exists. Teams that want a hard boundary use IAM tag-based access control keyed on managed-by: warpbuild, which scopes the role to resources WarpBuild created. The full policy document is expanded in the configuration docs.

What the runner itself can reach

The connection role governs what WarpBuild can do to your account. A separate control governs what your build steps can do: the instance profile.

WarpBuild injects no permissions into runner workloads. The permissions a job has are exactly the ones carried by the instance profile you attach, configured per runner under Custom Runner configuration and the Instance Profile ARN field. Because the setting is per runner, a deploy runner and a unit-test runner can carry different AWS authority under the same stack.

Attaching a profile takes one grant on the WarpBuild connection role:

aws iam put-role-policy \
    --role-name warpbuild-<integration-id> \
    --policy-name PassRolePolicy \
    --policy-document '{
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": "iam:PassRole",
                "Resource": "<role linked to the runner instance profile>",
                "Condition": {
                    "StringEquals": {
                        "iam:PassedToService": "ec2.amazonaws.com"
                    }
                }
            }
        ]
    }'

Verify it with aws iam simulate-principal-policy before you wire a workflow to it. The full walkthrough is in the instance profile guide.

Runner instances support both IMDSv1 and IMDSv2 by default. Setting Require IMDSv2 on the runner, either at creation time or on the Update Runner page, forces token-authenticated metadata requests and closes the classic path where a request-forgery bug in a build step reads instance credentials. If your build steps read instance metadata directly, confirm the client library sends the IMDSv2 token before you flip it.

Network posture rounds it out. The runner agent polls outbound, so runner instances accept inbound connections from nowhere, including from inside the VPC. The security hardening guide recommends a security group with an empty inbound rule set and outbound access to api.warpbuild.com, github.com, api.github.com, and your package registries. Network ACLs handle the second question, which is whether one runner can reach another; runner instances never need to talk to each other, so denying intra-subnet traffic is safe.

Configuration

Bring the AWS side up first, then create the stack, then create runners.

Prerequisites. You need permission to run a CloudFormation stack, a VPC with at least one public and one private subnet with internet connectivity, and an S3 bucket in the stack region. The documented recommendation is three public and three private subnets in different availability zones, which widens instance type availability, and at least 250 usable IP addresses per subnet so that concurrency is not capped by address exhaustion.

Quotas. The runner resources are created in your account and consume your quotas. For a target of $CON concurrent jobs, per stack:

ResourceQuota to request
EC2 instances$CON x vCPU per job
EBS volumes$CON x disk TB per job
Elastic IPs3 plus $CON, when static IPs are enabled
S3 buckets1, shared by artifact cache, layer cache, and telemetry
VPCs1

Quota increases apply per region, so raise them in the region where the stack lives. The BYOC AWS docs carry the full list and the console links.

Stack modes. Easy create builds a new VPC and its resources to the documented best practices. Import mode takes an existing VPC, subnets, and security group as inputs and is supported on AWS only.

Custom runners. A custom runner is a named instance configuration: one or more instance types in priority order, disk size and performance, IP configuration, standby disk count, and an optional instance profile. Listing several instance types matters more than it looks. The workflow references a single label, and WarpBuild picks an available instance type from your ordered list, so a capacity gap in one type does not park jobs in the queue. Choose types that are close in price and performance, for example m7a.xlarge and m7i.xlarge, so job duration stays predictable.

Disk configuration is the most common cause of slow BYOC jobs. The documented minimum is 100 GB, 125 MBps, and 3000 IOPS. The documented recommendation is 150 GB, 400 MBps, and 4000 IOPS. Windows Server 2022 x86-64 runners on AWS want at least 8 vCPU, the m7a series, 6000 IOPS, and 500 MBps.

Spot instances suit short jobs that tolerate interruption, and AWS bills them at its published Spot rates in your account, where AWS states Spot Instances run at up to a 90 percent discount compared to On-Demand prices (AWS EC2 Spot Instances, checked on 2026-08-13). Pair Spot with a longer fallback instance type list, since availability is what strands a queued job.

Two behaviors need no configuration. If the instance types you select expose local NVMe instance store volumes, such as the i3, c5d, m5d, and r5d families, the runner detects them at boot and mounts them at the work directory; multiple devices are combined into a RAID-0 array first. This is Linux images only. Nested virtualization is enabled automatically when every instance type in the runner set supports it.

Referencing runners in a workflow

A BYOC runner is addressed by its Runner ID, which is the runner name with the warp-custom- prefix. Nothing else in the workflow changes.

.github/workflows/ci.yml
name: ci
on:
  push:
    branches: [main]
  pull_request:

jobs:
  unit-tests:
    runs-on: warp-custom-payments-use1-8x
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: npm test

  integration-tests:
    needs: unit-tests
    runs-on: warp-custom-payments-use1-16x
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/integration.sh

Mixed fleets are common while a migration is in flight. WarpBuild-hosted labels and BYOC labels coexist in one workflow, so a matrix can send the heavy leg into your account and leave the rest on hosted capacity:

.github/workflows/build.yml
name: build
on: [push]

jobs:
  lint:
    runs-on: warp-ubuntu-latest-x64-2x
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run lint

  package:
    strategy:
      matrix:
        include:
          - runner: warp-ubuntu-latest-x64-8x
            target: linux-amd64
          - runner: warp-ubuntu-latest-arm64-8x
            target: linux-arm64
          - runner: warp-windows-latest-x64-8x
            target: windows-amd64
    runs-on: ${{ matrix.runner }}
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/package.sh ${{ matrix.target }}

Those hosted labels and their rates come from the runner catalog:

LabelOSvCPURAMStorageHosted rate per minute
warp-ubuntu-latest-x64-2xUbuntu 24.0428 GB150GB SSD$0.004
warp-ubuntu-latest-x64-4xUbuntu 24.04416 GB150GB SSD$0.008
warp-ubuntu-latest-x64-8xUbuntu 24.04832 GB150GB SSD$0.016
warp-ubuntu-latest-x64-16xUbuntu 24.041664 GB150GB SSD$0.032
warp-ubuntu-latest-x64-32xUbuntu 24.0432128 GB150GB SSD$0.064
warp-ubuntu-latest-arm64-8xUbuntu 24.04832 GB150GB SSD$0.012
warp-windows-latest-x64-8xWindows Server 2022832 GB256GB SSD$0.032
warp-windows-latest-x64-16xWindows Server 20221664 GB256GB SSD$0.064

On AWS BYOC the supported guest operating systems are Linux and Windows Server 2022 x86-64; the AWS hypervisor does not expose the Hyper-V features their Azure equivalents do, and macOS runners stay on WarpBuild-hosted capacity.

Custom VM images

BYOC runners can boot your own AMI instead of a WarpBuild image, which is how teams that must ship a hardened base or a preloaded toolchain keep BYOC compliant with their image policy. Linux and Windows AMIs are supported. A Linux image has to use systemd, because the WarpBuild agent runs as a unit, and it has to carry curl, wget, bash, jq, and libicu, the last of which the GitHub Actions runner needs for its .NET runtime. Ubuntu and Amazon Linux 2023 both qualify. Keep tar, gzip, coreutils, and shadow-utils in place. The custom VM images guide includes a working Packer template.

Owning the image also means owning its patch cycle. WarpBuild-managed images are updated by WarpBuild; a custom AMI is rebuilt on your schedule.

The Terraform path

Terraform support exists for BYOC on AWS. The WarpBuild Terraform provider is published on the Terraform Registry and manages BYOC runner images and custom runner sets, the same surface as the automation API. It authenticates with a WarpBuild API key carrying the ci scope, supplied as WARPBUILD_API_KEY. The provider is in beta, so pin the version exactly:

terraform {
  required_providers {
    warpbuild = {
      source  = "WarpBuilds/warpbuild"
      version = "0.2.0-beta"
    }
  }
}

Terraform coverage is scoped to AWS BYOC. Resource reference, data sources, and a full example live on the AWS BYOC Terraform page.

Who Manages What

The reason to read a BYOC page carefully is to find out which team gets paged at 2am. Here is the split.

ResponsibilityYour AWS accountWarpBuild
Provisioning the VPC, subnets, security group, and S3 bucketYes, through the CloudFormation stack you applyAuthors the template and the create and import flows
Launching and terminating runner instancesInstances run here and consume your quotasWatches the job queue and drives the AWS API
Runner image patchingOnly for a custom AMI you supplyPatches WarpBuild-managed runner images
Cloud bill for EC2, EBS, and data transferYes, billed by AWS at your ratesNot billed
WarpBuild fee$0.002 per runner minuteMetered and invoiced
Job data, caches, logs, and artifactsHeld in your S3 bucket and on your instancesControl plane metadata only
IAM authority granted to build stepsInstance profile you attach per runnerInjects no permissions into workloads
Quota increases and capacity planningYes, per regionDocuments the quota model
Runner lifecycle bugs and control plane incidentsEscalateOwns

Two consequences follow. Build data and cache contents stay inside your account and your region, which is the usual reason a regulated team picks BYOC over hosted capacity. And capacity limits become AWS quota questions rather than vendor questions, which is why the quota table above is worth filing before the first migration rather than after it. If your requirement is regional placement rather than account placement, WarpBuild-hosted runners cover it too: US and EU are the regions WarpBuild names publicly, and the US data residency page covers that path.

A worked cost model

Three ways to run the same 8 vCPU Linux job, priced against GitHub's published list prices. GitHub rates come from the GitHub Actions billing reference and the plan page at github.com/pricing, checked on 2026-08-13. WarpBuild rates come from the WarpBuild pricing page.

ShapeGitHub-hosted list price per minuteWarpBuild-hosted rate per minuteWarpBuild BYOC fee per minute
Linux 2 vCPU, 8 GB$0.006 (ubuntu-latest, private repositories)$0.004$0.002
Linux 4 vCPU, 16 GB$0.012 (4-core larger runner)$0.008$0.002
Linux 8 vCPU, 32 GB$0.022 (8-core larger runner)$0.016$0.002
Linux 16 vCPU, 64 GB$0.042 (16-core larger runner)$0.032$0.002
Linux 32 vCPU, 128 GB$0.082 (32-core larger runner)$0.064$0.002
Windows 8 vCPU, 32 GB$0.042 (8-core larger runner)$0.032$0.002
Windows 16 vCPU, 64 GB$0.082 (16-core larger runner)$0.064$0.002

Take a team burning 200,000 Linux minutes a month on an 8 vCPU shape.

  • GitHub-hosted 8-core larger runner: 200,000 x $0.022 = $4,400 per month.
  • WarpBuild-hosted warp-ubuntu-latest-x64-8x: 200,000 x $0.016 = $3,200 per month. Arithmetic on the two list prices: (0.022 - 0.016) / 0.022 = 27 percent lower list price, GitHub list price checked on 2026-08-13.
  • WarpBuild BYOC on AWS: 200,000 x $0.002 = $400 per month in WarpBuild fees, plus whatever AWS bills your account for the EC2 instances, EBS volumes, and data transfer those jobs consume, at your own On-Demand, Spot, or Savings Plan rates.

Add 50,000 Windows minutes on an 8 vCPU shape and the same three lines read $2,100, $1,600, and $100 plus your AWS bill. The BYOC line covers the WarpBuild fee alone, so the honest comparison is $400 plus your EC2 bill against $4,400, and whether it lands depends on your instance mix, your Spot ratio, and your committed-use discounts. Model it with a real month of workflow minutes before you migrate, and price the storage the cache and telemetry prefixes will hold.

Use the table above with your measured job duration and AWS instance rates. WarpBuild's per-size arithmetic is also on the pricing page.

BYOC add-ons are included in the per-minute fee. Egress costs during deploy-heavy jobs are handled on the enterprise tier; see zero egress on the WarpBuild enterprise tier.

Operating it after the migration

CI observability streams OpenTelemetry system metrics from the runner agent and correlates them with GitHub Actions job logs, which is how you tell an undersized disk from a slow test suite. The Action Debugger pauses a workflow and opens an SSH session on the runner, which is the fastest way to reproduce a failure that only happens on the fleet. Snapshot runners are the one product surface BYOC does not carry; they do not support BYOC, Windows, or macOS runners, and standby disks fill the cold-start role instead.

SSO is available for a flat $250 per month, whatever the user count, listed with the rest of the rates on the pricing page.

The other two clouds are covered on the GCP BYOC page and the Azure BYOC page.

FAQ

Does WarpBuild need inbound network access to my AWS account?

No. The GitHub Actions runner agent holds an outbound long poll, so the recommended security group blocks all inbound traffic and allows outbound traffic to api.warpbuild.com, github.com, api.github.com, and your package registries. WarpBuild reaches your account through the IAM role created by the CloudFormation connection stack, and the instances expose no inbound path.

What does WarpBuild charge for BYOC runners on AWS?

$0.002 per runner minute for Linux runners and $0.002 per runner minute for Windows runners. AWS bills your own account for EC2, EBS, and data transfer at your rates.

Can I manage AWS BYOC runners with Terraform?

Yes. Terraform support exists for BYOC on AWS. The WarpBuild provider is published on the Terraform Registry, covers BYOC runner images and custom runner sets, and authenticates with a WarpBuild API key carrying the ci scope. Terraform coverage is scoped to AWS BYOC.

Are snapshot runners available on BYOC?

No. Snapshot runners do not support BYOC runners, Windows runners, or macOS runners. On BYOC the equivalent start-time lever is a standby disk pool, which pre-initializes VMs of the runner instance type so a queued job boots in about 15 seconds.

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.