runs-fleet

module
v1.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 3, 2025 License: MIT

README

runs-fleet

CI Coverage

Self-hosted ephemeral GitHub Actions runners on AWS/K8s, orchestrating spot EC2 instances or Kubernetes pods for workflow jobs

Inspired by runs-on. Fleet orchestration with warm pools, S3 caching, and spot-first cost optimization.

Architecture

GitHub Webhook → API Gateway → SQS FIFO
                                   ↓
                Orchestrator (Go on Fargate)
                ├── Queue processors (main, pool, events)
                ├── Pool manager (warm instances)
                └── Provider (EC2 Fleet / Kubernetes)
                                   ↓
                EC2 Spot/On-demand Fleet  OR  Kubernetes Pods
                                   ↓
                Agent (bootstrap, execute, self-terminate)

Flows:

  • Cold-start (~60s): Webhook → SQS → Provider creates runner → Agent registers with GitHub → Job executes → Self-terminate
  • Warm pool (~10s): Webhook with pool= label → Assign running instance OR start stopped → Job executes → Reconciliation replaces
  • Spot interruption: EventBridge 2-min warning → Re-queue job with ForceOnDemand=true

Tradeoffs:

  • Spot-first (70% savings) with on-demand fallback and circuit breaker
  • Ephemeral instances avoid state accumulation
  • Warm pools trade idle cost for <10s latency

Quick Start

Prerequisites: Nix with flakes, direnv (optional), AWS credentials, Terraform infrastructure deployed

git clone https://github.com/Shavakan/runs-fleet.git && cd runs-fleet

# With direnv
cp .envrc.example .envrc && direnv allow

# Without direnv
nix develop

Build:

# Nix (recommended)
nix build .#server        # Server binary
nix build .#agent-amd64   # AMD64 agent
nix build .#agent-arm64   # ARM64 agent
nix build .#docker        # Docker image

# Make (alternative)
make build    # All binaries
make test     # Tests with coverage
make lint     # golangci-lint

Run locally:

export AWS_REGION=ap-northeast-1
export RUNS_FLEET_QUEUE_URL=https://sqs.ap-northeast-1.amazonaws.com/...
export RUNS_FLEET_GITHUB_WEBHOOK_SECRET=...
go run cmd/server/main.go

Project Structure

cmd/
  server/         # Fargate orchestrator (webhooks, queue processing, fleet mgmt)
  agent/          # EC2/K8s bootstrap binary (registers runner, executes job)
pkg/
  provider/       # Runner provisioning abstraction (EC2, Kubernetes)
  fleet/          # EC2 fleet creation (spot strategy, launch templates)
  pools/          # Warm pool reconciliation (hot/stopped instances)
  github/         # Webhook validation, JIT tokens, label parsing
  runner/         # Runner lifecycle management
  cache/          # S3-backed GitHub Actions cache protocol
  queue/          # SQS FIFO processing (batch, DLQ)
  db/             # DynamoDB state management
  events/         # EventBridge (spot interruptions)
  termination/    # Instance shutdown notifications
  housekeeping/   # Cleanup (orphaned instances, stale SSM)
  circuit/        # Circuit breaker for spot failures
  coordinator/    # Distributed leader election (DynamoDB)
  cost/           # Pricing calculations
  metrics/        # CloudWatch metrics
  tracing/        # OpenTelemetry tracing
  gitops/         # GitOps configuration management
  config/         # Env parsing, AWS clients
  agent/          # Agent-side logic (bootstrap, self-terminate)

Workflow Configuration

Replace GitHub-hosted runners with runs-fleet by changing the runs-on field:

# Before (GitHub-hosted)
runs-on: ubuntu-latest

# After (runs-fleet)
runs-on: "runs-fleet=${{ github.run_id }}/runner=2cpu-linux-arm64"
Runner Specs
Spec Instance vCPU RAM Disk
2cpu-linux-arm64 t4g.medium 2 4GB 30GB
4cpu-linux-arm64 c7g.xlarge 4 8GB 50GB
8cpu-linux-arm64 c7g.2xlarge 8 16GB 100GB
2cpu-linux-amd64 t3.medium 2 4GB 30GB
4cpu-linux-amd64 c6i.xlarge 4 8GB 50GB
8cpu-linux-amd64 c6i.2xlarge 8 16GB 100GB

Custom disk size: disk=<size> in GiB (1-16384), e.g., runner=4cpu-linux-arm64/disk=200

Cost note: gp3 EBS storage costs ~$0.08/GB-month. Large disks add up quickly (e.g., 1TB = $80/month).

Flexible Specs

For advanced instance selection with spot diversification, use flexible specs instead of fixed runner specs:

# Flexible: EC2 Fleet chooses best instance from multiple families
runs-on: "runs-fleet=${{ github.run_id }}/cpu=4/arch=arm64"

# Architecture-agnostic: EC2 Fleet chooses between ARM64 and AMD64 based on spot availability
runs-on: "runs-fleet=${{ github.run_id }}/cpu=4"
Label Description
cpu=<n> Minimum vCPUs required
cpu=<min>-<max> vCPU range (e.g., cpu=4-8)
ram=<n> Minimum RAM in GB
arch=<arch> Architecture: arm64 or amd64 (omit for both)
family=<list> Instance families (e.g., family=c7g+m7g)
disk=<size> Disk size in GiB (1-16384)

When arch is omitted, runs-fleet creates multiple EC2 Fleet launch template configurations (one per architecture) and lets EC2 choose based on price-capacity-optimized strategy. This maximizes spot availability across both ARM64 and AMD64 instance pools.

Default families by architecture:

  • ARM64: c7g, m7g, t4g
  • AMD64: c6i, c7i, m6i, m7i, t3
  • No arch: all of the above
Label Reference
Label Description
runs-fleet=<run-id> Workflow run identifier (required)
runner=<spec> <cpu>cpu-<os>-<arch>[/<modifier>]
pool=<name> Warm pool for fast start (~10s vs ~60s)
private=true Private subnet with static egress IP
spot=false Force on-demand (skip spot instances)
Examples

Basic CI workflow:

name: CI
on: [push, pull_request]

jobs:
  test:
    runs-on: "runs-fleet=${{ github.run_id }}/runner=4cpu-linux-arm64"
    steps:
      - uses: actions/checkout@v4
      - run: make test

Fast start with warm pool:

jobs:
  build:
    # Pool provides ~10s start time vs ~60s cold start
    runs-on: "runs-fleet=${{ github.run_id }}/runner=4cpu-linux-arm64/pool=default"

AMD64 for x86-specific builds:

jobs:
  build-amd64:
    runs-on: "runs-fleet=${{ github.run_id }}/runner=4cpu-linux-amd64"

Private networking (static egress IP):

jobs:
  deploy:
    # Use private subnet when calling APIs that whitelist IPs
    runs-on: "runs-fleet=${{ github.run_id }}/runner=2cpu-linux-arm64/private=true"

Force on-demand instance:

jobs:
  critical-deploy:
    # Skip spot for critical jobs that can't tolerate interruption
    runs-on: "runs-fleet=${{ github.run_id }}/runner=4cpu-linux-arm64/spot=false"

Matrix build:

jobs:
  build:
    strategy:
      matrix:
        arch: [arm64, amd64]
    runs-on: "runs-fleet=${{ github.run_id }}/runner=4cpu-linux-${{ matrix.arch }}"

Configuration

Required env vars (see .envrc.example for complete list):

# GitHub App
RUNS_FLEET_GITHUB_APP_ID, RUNS_FLEET_GITHUB_APP_PRIVATE_KEY, RUNS_FLEET_GITHUB_WEBHOOK_SECRET

# AWS (from Terraform)
AWS_REGION, RUNS_FLEET_QUEUE_URL, RUNS_FLEET_POOL_QUEUE_URL, RUNS_FLEET_EVENTS_QUEUE_URL
RUNS_FLEET_LOCKS_TABLE, RUNS_FLEET_JOBS_TABLE, RUNS_FLEET_CACHE_BUCKET, RUNS_FLEET_CONFIG_BUCKET
RUNS_FLEET_VPC_ID, RUNS_FLEET_PUBLIC_SUBNET_IDS, RUNS_FLEET_SECURITY_GROUP_ID, RUNS_FLEET_INSTANCE_PROFILE_ARN

# Behavior
RUNS_FLEET_SPOT_ENABLED=true, RUNS_FLEET_MAX_RUNTIME_MINUTES=360

Development

make test           # Run tests (or: nix flake check)
make lint           # Lint
make docker-build   # Build Docker image
make docker-push    # Push to ECR

Deploy: Infrastructure in shavakan-terraform/terraform/runs-fleet/. Push image to ECR, then terraform apply.

Cost Model

Target: ~$55-65/month for 100 jobs/day @ 10 min avg runtime

Component Cost
EC2 spot ~$15-20/month
Fargate orchestrator $36/month (1 vCPU, 2GB)
S3 cache $1-5/month
Supporting services $2-3/month

Compare to GitHub hosted: $80/month. Cost reporting in pkg/cost/ covers t4g, c7g, m7g families with fixed 70% spot discount assumption.

Security

  • Webhook HMAC-SHA256 validation
  • IMDSv2 required, least-privilege IAM
  • S3 pre-signed URLs (15-min), encrypted EBS
  • Secrets in AWS Secrets Manager

License

MIT — Inspired by runs-on by Cyril Rohr.

Directories

Path Synopsis
cmd
agent command
Package main implements the runs-fleet agent that runs on EC2 instances or K8s pods to execute GitHub Actions jobs.
Package main implements the runs-fleet agent that runs on EC2 instances or K8s pods to execute GitHub Actions jobs.
server command
Package main implements the runs-fleet orchestrator server that processes GitHub webhook events.
Package main implements the runs-fleet orchestrator server that processes GitHub webhook events.
pkg
agent
Package agent provides GitHub Actions runner agent functionality.
Package agent provides GitHub Actions runner agent functionality.
cache
Package cache implements GitHub Actions cache protocol backed by S3.
Package cache implements GitHub Actions cache protocol backed by S3.
circuit
Package circuit implements circuit breaker for spot instance interruptions.
Package circuit implements circuit breaker for spot instance interruptions.
config
Package config manages application configuration from environment variables.
Package config manages application configuration from environment variables.
coordinator
Package coordinator implements distributed leader election using DynamoDB.
Package coordinator implements distributed leader election using DynamoDB.
cost
Package cost provides cost reporting functionality for runs-fleet.
Package cost provides cost reporting functionality for runs-fleet.
db
Package db provides DynamoDB client operations for pool configuration and state management.
Package db provides DynamoDB client operations for pool configuration and state management.
events
Package events handles EventBridge events from SQS.
Package events handles EventBridge events from SQS.
fleet
Package fleet manages EC2 fleet creation and lifecycle for runner instances.
Package fleet manages EC2 fleet creation and lifecycle for runner instances.
github
Package github provides webhook validation and label parsing for GitHub Actions workflow jobs.
Package github provides webhook validation and label parsing for GitHub Actions workflow jobs.
gitops
Package gitops provides configuration-as-code support for runs-fleet.
Package gitops provides configuration-as-code support for runs-fleet.
housekeeping
Package housekeeping handles scheduled cleanup tasks for runs-fleet.
Package housekeeping handles scheduled cleanup tasks for runs-fleet.
metrics
Package metrics publishes CloudWatch metrics.
Package metrics publishes CloudWatch metrics.
pools
Package pools manages warm pool operations for maintaining pre-provisioned EC2 instances.
Package pools manages warm pool operations for maintaining pre-provisioned EC2 instances.
provider
Package provider defines interfaces for compute backends (EC2, K8s).
Package provider defines interfaces for compute backends (EC2, K8s).
provider/ec2
Package ec2 implements the provider interfaces for AWS EC2.
Package ec2 implements the provider interfaces for AWS EC2.
provider/k8s
Package k8s implements the provider interfaces for Kubernetes.
Package k8s implements the provider interfaces for Kubernetes.
queue
Package queue provides message queue abstractions for job orchestration.
Package queue provides message queue abstractions for job orchestration.
runner
Package runner manages GitHub Actions runner registration and configuration.
Package runner manages GitHub Actions runner registration and configuration.
state
Package state provides state storage backends for pool configuration and job tracking.
Package state provides state storage backends for pool configuration and job tracking.
termination
Package termination handles instance termination notifications from agents.
Package termination handles instance termination notifications from agents.
tracing
Package tracing provides OpenTelemetry integration for distributed tracing.
Package tracing provides OpenTelemetry integration for distributed tracing.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL