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
| Field | Meaning | Guardrail |
|---|---|---|
scale_set_name | Stable GitHub workflow label and scale-set identity | Unique per deployment; changing it creates a new routing target |
architecture | x64 or arm64 | Must match AMI, instance type, and runner image manifest |
capacity_mode | On-Demand, Spot, or Spot with fallback | Choose from job retryability and start-time requirements |
subnet_ids | Ordered runner launch locations | Use reviewed egress and enough addresses; multiple zones for Spot |
security_group_ids | Existing runner network policy | Empty creates an outbound-only group using approved egress CIDRs |
policy_arns | Policies attached to a generated runner role | Prefer empty plus job-scoped GitHub OIDC |
iam_instance_profile_arn | Existing runner profile | Requires its matching iam_role_arn |
min_runners | Connected idle capacity to retain | Zero scales to zero; positive values incur continuous cost |
max_runners | Maximum concurrent instances | Integer from 1 to 1,000 and not below the minimum |
max_runner_lifetime | Independent hard-expiry duration | Default six hours; must be long enough for valid jobs |
root_volume_size | Encrypted root volume in GiB | At 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 verifyAdditional 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
| Mode | Behavior | Use when |
|---|---|---|
on-demand | Launches On-Demand capacity in the next configured subnet. | Start reliability matters more than compute discount. |
spot | Tries Spot across configured subnets and fails if capacity is unavailable. | Jobs are safely retryable and price-sensitive. |
spot-with-on-demand-fallback | Tries 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-requestNo standing AWS access
Untrusted validation
buildRead-only artifact inputs
Normal CI compilation
releaseRestricted 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
- Identify whether the change alters compatibility, trust, cost, or only implementation detail. Create a new label for a breaking change.
- Pin the replacement runner image by digest and confirm it publishes the fleet architecture.
- Review the generated launch-template version, IAM diff, network diff, and maximum capacity in
terraform plan. - Apply in a non-production repository and run a job that exercises the image, network, and required job-scoped permissions.
- Verify termination and cost tags before moving protected workflows. Keep the previous digest and fleet record for rollback.
Cost controls
- Keep
min_runners = 0to 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.