Fleet installation guide
Deploy RunsOn Fleet on AWS with Terraform or OpenTofu.
RunsOn Fleet is deployed with the official Terraform module. The module is available on the Terraform Registry ↗ and the OpenTofu Registry ↗.
Use Fleet when a platform team wants to publish approved runner fleets such as linux-small or linux-large and have repositories target those runner fleets with stable GitHub runner labels.
Prerequisites
- AWS credentials with permission to create ECS, IAM, EC2, S3, Secrets Manager, and CloudWatch resources.
- Terraform or OpenTofu.
- A VPC and at least one public subnet (or use the VPC module to create one). Private subnets are optional.
- A RunsOn license key.
- GitHub credentials: a GitHub App for organization mode, or a classic PAT for enterprise mode (see below).
GitHub credentials
Fleet connects to GitHub with a single credential, depending on which boundary it manages:
- Organization mode — a GitHub App installed on one organization.
- Enterprise mode — a classic PAT scoped to the enterprise.
Fleet registers runners into a GitHub runner group but never creates one. Create the runner group yourself (in Terraform or the GitHub UI) and reference it by name from fleets.<name>.runner_group. The examples below show this for both single-organization and enterprise topologies.
Organization mode: create a GitHub App
Open the following URL, replacing <ORG> with the organization that will own the App. The query string pre-fills the App name, permissions, and the organization self-hosted-runners write scope:
https://github.com/organizations/<ORG>/settings/apps/new?name=RunsOn%20Fleet%20%5B<ORG>%5D&url=https%3A%2F%2Fruns-on.com&public=false&webhook_active=false&organization_self_hosted_runners=write&actions=readOr enter your organization name to generate the link directly:
Enter an organization name to generate the link. Then:
- Generate a private key and save the
.pemfile. - Install the App on the organization (install it on exactly one).
- Pass the App ID and the
.pemcontents asgithub_app_idandgithub_app_private_keyin the module.
Enterprise mode: create a classic PAT
Open the following URL, replacing <ENTERPRISE> with your enterprise slug. The query string pre-fills the token description and the manage_runners:enterprise scope:
https://github.com/settings/tokens/new?description=RunsOn%20Fleet%20%5B<ENTERPRISE>%5D&scopes=manage_runners%3AenterprisePass this PAT as github_enterprise_pat in the module.
For GHES, replace https://github.com in either URL with your GHES host root, and pass that host root as github_base_url to the module.
Single organization
This example creates an organization runner group, a small-x64 runner shape, and one linux-small fleet that registers into that runner group.
provider "github" { # GitHub App auth, or a PAT scoped to manage the org runner group.}
resource "github_actions_runner_group" "platform" { name = "platform" visibility = "all"}
module "runs_on_fleet" { source = "runs-on/runs-on/aws//modules/fleet" version = "v3.1.0"
stack_name = "runs-on-fleet" license_key = var.license_key environment = "production"
github_app_id = var.github_app_id github_app_private_key = var.github_app_private_key
vpc_id = var.vpc_id public_subnet_ids = var.public_subnet_ids
# Optional: ubuntu24-full-x64 is a built-in image name. Declare it only # when you want to override the default AMI lookup pattern or owner. images = { ubuntu24-full-x64 = { name = "runs-on-v2.2-ubuntu24-full-x64-*" owner = "898082745236" platform = "linux" arch = "x64" } }
runners = { small-x64 = { cpu = [] extras = [] family = ["t3a.micro"] image = "ubuntu24-full-x64" } }
fleets = { linux-small = { runner = "small-x64" runner_group = github_actions_runner_group.platform.name } }}To restrict the runner group to specific repositories, set visibility = "selected" and pass selected_repository_ids on the github_actions_runner_group resource. Org-level runner group access is fully manageable in Terraform.
After terraform apply, target the fleet from a workflow:
jobs: build: runs-on: runs-on/fleet=linux-small/env=production steps: - uses: actions/checkout@v6 - run: echo "Hello from RunsOn Fleet"Enterprise
In enterprise mode, Fleet registers enterprise-owned runner scale sets. This example creates an enterprise runner group with the integrations/github provider and connects two runner fleets to it.
The example also enables private networking (private_mode = "only") to show what a more locked-down deployment looks like. Private mode is not required: you can run enterprise mode with public subnets exactly like the single-org example, just by setting github_enterprise_pat / github_enterprise_name and dropping the private-mode and NAT pieces.
terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 6.0" } github = { source = "integrations/github" version = "~> 6.0" } }}
provider "aws" { region = var.aws_region}
provider "github" { token = var.github_enterprise_pat}
data "aws_availability_zones" "available" { state = "available"}
module "vpc" { source = "terraform-aws-modules/vpc/aws" version = "~> 6.0"
name = "runs-on-fleet-vpc" cidr = "10.20.0.0/16"
azs = slice(data.aws_availability_zones.available.names, 0, 2) private_subnets = ["10.20.128.0/20", "10.20.144.0/20"] public_subnets = ["10.20.0.0/20", "10.20.16.0/20"]
enable_nat_gateway = true single_nat_gateway = true # Use false for one NAT Gateway per AZ.
enable_dns_hostnames = true enable_dns_support = true}
module "vpc_endpoints" { source = "terraform-aws-modules/vpc/aws//modules/vpc-endpoints" version = "~> 6.0"
vpc_id = module.vpc.vpc_id
endpoints = { s3 = { service = "s3" service_type = "Gateway" route_table_ids = module.vpc.private_route_table_ids } }}
resource "github_enterprise_actions_runner_group" "runs_on" { name = "runs-on-fleet" enterprise_slug = var.github_enterprise_name
visibility = "all" allows_public_repositories = false}
module "runs_on_fleet" { source = "runs-on/runs-on/aws//modules/fleet" version = "v3.1.0"
license_key = var.license_key environment = "production" github_enterprise_pat = var.github_enterprise_pat github_enterprise_name = var.github_enterprise_name
vpc_id = module.vpc.vpc_id public_subnet_ids = module.vpc.public_subnets private_subnet_ids = module.vpc.private_subnets private_mode = "only"
# Optional: ubuntu24-full-x64 is a built-in image name. Declare it only # when you want to override the default AMI lookup pattern or owner. images = { ubuntu24-full-x64 = { name = "runs-on-v2.2-ubuntu24-full-x64-*" owner = "898082745236" platform = "linux" arch = "x64" } }
runners = { linux-small = { cpu = [] extras = [] family = ["t3a.micro"] image = "ubuntu24-full-x64" }
windows-large = { cpu = 8 ram = 16 extras = [] family = ["m7i"] image = "windows25-full-x64" } }
fleets = { linux-small = { runner = "linux-small" runner_group = github_enterprise_actions_runner_group.runs_on.name }
windows-large = { runner = "windows-large" runner_group = github_enterprise_actions_runner_group.runs_on.name timezone = "UTC" schedule = [ { name = "default" hot = 1 stopped = 3 }, ] } }}This creates two runner fleets:
runs-on/fleet=linux-small/env=productionruns-on/fleet=windows-large/env=production
windows25-full-x64 is also a predefined image name, so it does not need an images entry.
Picking which orgs and repos can use the runner group
visibility = "all" lets every org in the enterprise see the runner group. To restrict to specific orgs, set visibility = "selected" and pass selected_organization_ids.
There is no field for selecting specific repositories on an enterprise runner group, neither in Terraform nor in the GitHub UI. To restrict repos within a permitted org, an org admin has to open the org’s Settings → Actions → Runner groups, click the shared enterprise runner group, and pick repositories from there.
NAT Gateway and private mode
Because this example uses private_mode = "only", runners launch in private subnets and need a route to the public internet for github.com, container registries, and package mirrors. That is what the NAT Gateway provides. If you don’t want a NAT Gateway, switch to public subnets by removing private_mode and private_subnet_ids.
The S3 gateway endpoint keeps S3 cache traffic on the AWS network path without adding hourly endpoint cost.
Next steps
- Define runner fleets.
- Review Fleet licensing and usage.
- Use Flex instead when workflows should choose runner details dynamically per job.