Skip to content
Manual revision 01 · GitHub.com · Linux x64 and ARM64 · AWS

Fleet policy

Give workflows choices, not cloud authority.

A fleet maps one stable GitHub label to infrastructure approved by the platform team. Repository code can request that label; it cannot select an arbitrary subnet, AMI, instance profile, or security group.

Fleet definition

fleets = {
  build_arm64 = {
    scale_set_name     = "example-build-arm64"
    architecture       = "arm64"
    capacity_mode      = "spot-with-on-demand-fallback"
    instance_type      = "m7g.large"
    subnet_ids         = var.arm64_subnet_ids
    security_group_ids = [aws_security_group.build.id]
    policy_arns        = [aws_iam_policy.build_readonly.arn]
    min_runners        = 0
    max_runners        = 40
    max_runner_lifetime = "1h"
  }
}

Each fleet can choose x64 or ARM64, its own AMI and runner image, VPC placement, IAM role, root-volume size, runner limits, and cost tags. One controller manages all configured fleets concurrently.

Field reference

FieldMeaningGuardrail
scale_set_nameStable GitHub workflow label and scale-set identityUnique per deployment; changing it creates a new routing target
architecturex64 or arm64Must match AMI, instance type, and runner image manifest
capacity_modeOn-Demand, Spot, or Spot with fallbackChoose from job retryability and start-time requirements
subnet_idsOrdered runner launch locationsUse reviewed egress and enough addresses; multiple zones for Spot
security_group_idsExisting runner network policyEmpty creates an outbound-only group using approved egress CIDRs
policy_arnsPolicies attached to a generated runner rolePrefer empty plus job-scoped GitHub OIDC
iam_instance_profile_arnExisting runner profileRequires its matching iam_role_arn
min_runnersConnected idle capacity to retainZero scales to zero; positive values incur continuous cost
max_runnersMaximum concurrent instancesInteger from 1 to 1,000 and not below the minimum
max_runner_lifetimeIndependent hard-expiry durationDefault six hours; must be long enough for valid jobs
root_volume_sizeEncrypted root volume in GiBAt least 30 GiB; billed while the runner exists

The label is the routing contract

Workflows request the scale-set name, not cloud parameters. Treat that name as a long-lived platform interface: document its architecture, software image, trust level, intended repository set, and support owner. Add a new fleet when those promises diverge instead of silently changing a widely used label.

jobs:
  verify-arm64:
    runs-on: example-build-arm64
    steps:
      - uses: actions/checkout@<reviewed-sha>
      - run: make verify

Additional labels may describe a fleet to GitHub, but they do not let a workflow override its Terraform-defined AMI, role, network, instance type, or capacity policy.

Capacity modes

ModeBehaviorUse when
on-demandLaunches On-Demand capacity in the next configured subnet.Start reliability matters more than compute discount.
spotTries Spot across configured subnets and fails if capacity is unavailable.Jobs are safely retryable and price-sensitive.
spot-with-on-demand-fallbackTries Spot first, then On-Demand after definitive capacity exhaustion.You want savings without treating Spot shortage as a hard failure.

Spot retry semantics

Gondola rotates through configured subnets only after a definitive AWS capacity response. It does not retry an ambiguous launch result in a second Availability Zone because request idempotency is zonal and a cross-zone retry could create a duplicate. A fallback fleet attempts On-Demand only after definitive Spot-capacity exhaustion.

Separate trust domains

Use distinct fleets when workloads need different secrets, data access, network destinations, or change-control owners. A privileged release fleet should not share an IAM role or broad network policy with pull-request tests.

pull-request

No standing AWS access

Untrusted validation

build

Read-only artifact inputs

Normal CI compilation

release

Restricted publishing role

Protected branches only

Repository trust belongs in the routing decision as well. Workflows triggered by untrusted pull requests should not reach a fleet with publishing credentials, production network routes, or durable caches. Use branch protection and GitHub environment approval in addition to fleet isolation; an EC2 boundary does not repair an unsafe workflow.

Ownership and adoption

Gondola marks every GitHub scale set with a deployment-scoped ownership label and refuses to update or delete an unmarked same-named registration. Use the adoption switch only for a reviewed migration of a scale set already owned by the same Gondola deployment, then turn it off again.

To migrate an older Gondola-owned scale set, enable adopt_existing_scale_sets for one reviewed apply, confirm the ownership marker, then disable it. Never use adoption to take over a scale set managed by another installation.

Change a fleet safely

  1. Identify whether the change alters compatibility, trust, cost, or only implementation detail. Create a new label for a breaking change.
  2. Pin the replacement runner image by digest and confirm it publishes the fleet architecture.
  3. Review the generated launch-template version, IAM diff, network diff, and maximum capacity in terraform plan.
  4. Apply in a non-production repository and run a job that exercises the image, network, and required job-scoped permissions.
  5. Verify termination and cost tags before moving protected workflows. Keep the previous digest and fleet record for rollback.

Cost controls

  • Keep min_runners = 0 to avoid idle EC2 runner cost.
  • Use at least two runner subnets in different Availability Zones when relying on Spot.
  • Activate the gondola:* cost-allocation tags in AWS Billing.
  • Remember that NAT gateways, public IPv4, logs, metrics, and data transfer can exceed the controller baseline.

Next

Read the architecture

Follow a job through coordination, launch, registration, and retirement.