Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
37 changes: 37 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: build

on:
push:
branches: [main]
paths-ignore:
- "infrastructure/**"
- "docs/**"
- "**.md"
pull_request:
paths-ignore:
- "infrastructure/**"
- "docs/**"
- "**.md"

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Format check
run: |
diff <(gofmt -l .) <(echo -n "")

- name: Vet
run: go vet ./...

- name: Build (all cmd/* Lambdas)
run: ./scripts/build.sh

- name: Test
run: go test ./...
90 changes: 90 additions & 0 deletions .github/workflows/terraform.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
name: terraform

on:
push:
branches: [main]
paths:
- "infrastructure/**"
- "configs/runner.yaml"
pull_request:
paths:
- "infrastructure/**"
- "configs/runner.yaml"

jobs:
fmt-and-validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: hashicorp/setup-terraform@v3
with:
terraform_version: "~> 1.16"
terraform_wrapper: false

- uses: autero1/action-terragrunt@v3
with:
terragrunt-version: "1.1.4"

- name: Format check (Terragrunt HCL)
run: terragrunt hcl fmt --working-dir infrastructure --check --diff

- name: Format check (the single Terraform module)
run: terraform fmt -recursive -check infrastructure/modules

- name: Validate Terragrunt HCL
run: terragrunt hcl validate --working-dir infrastructure

# `terraform validate` on the module directly, not `terragrunt
# validate` — the latter runs `terraform init` against the real S3
# state backend, which doesn't exist yet for a repo that hasn't been
# deployed. This still fully validates the module's HCL/types without
# needing AWS credentials or a state bucket.
- name: Validate Terraform module
working-directory: infrastructure/modules/github-runner-on-aws
run: |
terraform init -backend=false -input=false
terraform validate

# Plan requires this repo's own AWS account (for the S3 state backend and
# to talk to the AWS API), so it only runs once AWS_ROLE_TO_ASSUME is set
# as a repository secret (OIDC — see
# https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services).
plan:
needs: fmt-and-validate
if: ${{ secrets.AWS_ROLE_TO_ASSUME != '' }}
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Build Lambda binaries
run: |
./scripts/build.sh
./scripts/package.sh

- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
aws-region: ${{ vars.AWS_REGION || 'ap-southeast-1' }}

- uses: hashicorp/setup-terraform@v3
with:
terraform_version: "~> 1.16"
terraform_wrapper: false

- uses: autero1/action-terragrunt@v3
with:
terragrunt-version: "1.1.4"

- name: Plan
working-directory: infrastructure/environments/main
run: |
terragrunt init -input=false
terragrunt plan -input=false
16 changes: 15 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
.terraform
.terragrunt-cache
lambda_function.zip
*.tfstate.backup
*.tfstate
*.tfstate

# Generated by each environment's terragrunt.hcl `generate` blocks — never
# hand-edited, always overwritten on the next terragrunt run.
infrastructure/environments/*/backend.tf
infrastructure/environments/*/provider.tf
infrastructure/root.hcl

# Go Lambda build artifacts (produced by scripts/build.sh + scripts/package.sh)
lambda/*/bootstrap
lambda/*.zip

secrets.yaml
.terraform.lock.hcl
1 change: 1 addition & 0 deletions .terraform-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1.16.2
1 change: 1 addition & 0 deletions .terragrunt-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1.1.4
39 changes: 39 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
.PHONY: build package deploy-plan deploy-apply fmt vet test tf-fmt tf-validate clean

build:
./scripts/build.sh

package: build
./scripts/package.sh

deploy-plan: package
./scripts/deploy.sh plan

deploy-apply: package
./scripts/deploy.sh apply

fmt:
gofmt -l .

vet:
go vet ./...

test:
go test ./...

tf-fmt:
terragrunt hcl fmt --working-dir infrastructure --check
terraform fmt -recursive -check infrastructure/modules

# Credential-free: validates the Terragrunt HCL itself, then the Terraform
# module directly (no provider config or state backend needed — see
# infrastructure/modules/github-runner-on-aws). This does NOT need a real S3
# state bucket; `terragrunt validate` (which does) only runs as part of
# `make deploy-plan`/`deploy-apply`.
tf-validate:
terragrunt hcl validate --working-dir infrastructure
cd infrastructure/modules/github-runner-on-aws && terraform init -backend=false -input=false >/dev/null && terraform validate
rm -rf infrastructure/modules/github-runner-on-aws/.terraform infrastructure/modules/github-runner-on-aws/.terraform.lock.hcl

clean:
rm -rf lambda/*/bootstrap lambda/*.zip
111 changes: 77 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,54 +1,97 @@
# Provision self-hosted GitHub runners on-demand in AWS

![shield](https://img.shields.io/badge/Scope-github_runners-blue)
![shield](https://img.shields.io/badge/Cloud_provider-AWS-orange)
![shield](https://img.shields.io/badge/Terrafrom->=v1.0-orange)
![shield](https://img.shields.io/badge/Terraform->=1.10-orange)
![shield](https://img.shields.io/badge/Terragrunt-1.x-blueviolet)
![shield](https://img.shields.io/badge/Language-Go-00ADD8)
![shield](https://img.shields.io/badge/Type-spot_instance-purple)
![shield](https://img.shields.io/badge/Permission-full_control-purple)

## Purpose
This project aims to provision self-hosted GitHub runners on-demand in AWS. It automates the setup and scaling of GitHub runners to handle CI/CD workloads efficiently.

## Features
- Scaling & Sustainability: Automatically calculating runners, scale up and down based on requesting.
- Life cycles: Self-killed when no jobs are running after particularly time.
- Security: Runners are created on-demand and terminated after use (ephemeral runners).
- Cost optimization: Runners are created on AWS spot instances.
- Runner level: Support organization and repository level runners. Enterprise level runners are not supported (yet).
- Multi-Runner: Create multiple runner configurations with a single deployment
This project provisions self-hosted GitHub Actions runners on-demand in AWS: event-driven, idempotent, and enterprise-grade. It replaces an earlier Python/TypeScript prototype (kept for reference, untouched, in [src_backup/](./src_backup)) with a Go implementation built around three Lambda functions, a single DynamoDB table for state, and a GitHub App for authentication — **no personal access token, ever**.

## Architecture
The architecture of this project is illustrated below:

![img](./docs/github-runner.drawio.png)
See [docs/infrastructure/enterprise-standard-upgrade.md](./docs/infrastructure/enterprise-standard-upgrade.md) for the full design and [docs/infrastructure/request-github-runner-token-architecture.md](./docs/infrastructure/request-github-runner-token-architecture.md) for the GitHub App authentication flow. In short:

## Scope
The scope of this project includes:
- Provisioning AWS resources required for GitHub runners.
- Configuring GitHub runners to scale based on demand.
- Ensuring secure and efficient communication between GitHub and AWS.
```text
GitHub (workflow_job webhook)
│
▼
API Gateway -> webhook Lambda (verify signature, dedupe, normalize)
│
▼
SQS (+ DLQ)
│
▼
provision Lambda (profile + group resolution, allocate-or-provision)
│ │
▼ ▼
DynamoDB GitHub App (registration token) -> EC2 Spot (runner)
▲
│
EventBridge (rate(3m) + spot interruption) -> cleanup Lambda
```

## Requirements
- AWS Account with appropriate permissions.
- GitHub account with repository access or organization access.
- Terraform v1.0 or later.
- AWS CLI configured with appropriate credentials.
- Docker installed on the local machine.
Developers only declare capability in their workflow — the label(s) just
need to match one of a profile's `labels` in `configs/runner.yaml`:

## Usage
Once the setup is complete, the GitHub runners will automatically scale based on the CI/CD workload. You can monitor and manage the runners through the AWS Management Console and GitHub repository settings. Follow these documents to install:
```yaml
runs-on: [self-hosted, erp-sport-amd64]
```

1. [Install AutoRunner - Worker](./src/lambda-runner-worker/README.md)
2. [Install AutoRunner - API](./src/lambda-runner-api/README.md)
The platform — not the developer — decides AMI, instance type, Spot strategy, subnet, security group, IAM role, runner group, and registration token.

## Log
You can view logs through AWS CloudWatch - Log Groups:
```
/aws/lambda/autorunner
/aws/lambda/autorunner-worker
## Repository layout

```text
cmd/ Lambda entrypoints: webhook, provision, cleanup
internal/ Domain logic: config, github, runner, aws, store, webhook, cleanup
configs/runner.yaml Single source of truth: app policy AND infra settings
infrastructure/modules/github-runner-on-aws/ The one Terraform module — every AWS resource this project needs
infrastructure/root.hcl Shared Terragrunt config: S3 state backend (native locking, no DynamoDB)
infrastructure/environments/main/terragrunt.hcl The single environment: reads runner.yaml, calls the module
scripts/ build.sh, package.sh, deploy.sh
src_backup/ Prior implementation — reference only, not part of this build
```

## Setup

**Full walkthrough: [docs/SETUP.md](./docs/SETUP.md)** — create the GitHub
App, configure `configs/runner.yaml`, deploy with Terragrunt, populate
secrets, and verify end-to-end. Condensed version:

1. Create a GitHub App (App ID, private key, webhook secret) — see [request-github-runner-token-architecture.md](./docs/infrastructure/request-github-runner-token-architecture.md). Install it on the target organization/repository.
2. Edit [configs/runner.yaml](./configs/runner.yaml) — the single source of truth for both the app policy (`github`/`webhook`/`runner`: App ID, secret names, scope/group, profiles and their labels) and the deployment knobs (`infrastructure.main`: region, existing-VPC IDs, Lambda timeouts, tags) that `infrastructure/environments/main/terragrunt.hcl` reads and passes to the Terraform module. Every field is documented inline (Helm `values.yaml`-style `-- comment` convention).
3. Copy [configs/secrets-example.yaml](./configs/secrets-example.yaml) to `configs/secrets.yaml` and fill in the GitHub App's private key and webhook secret from step 1. This file is gitignored and never committed; Terraform reads it and applies both secrets' values directly.
4. Fill in your state bucket/region in `infrastructure/root.hcl` (this is the one thing that can't come from `configs/runner.yaml` — Terraform's backend block can't read a file that might itself be the file telling it where to find its state).
5. Build and deploy:

```sh
make deploy-plan # builds + packages Lambdas, then terragrunt plan
make deploy-apply # ... then terragrunt apply
```

This also writes both secrets' values to Secrets Manager — no separate step needed.
6. Point the GitHub App's webhook URL at the `webhook_url` output (`cd infrastructure/environments/main && terragrunt output -raw webhook_url`).

## Requirements

- Go 1.26+ (see go.mod)
- Terraform >= 1.10 (native S3 state locking), AWS provider ~> 5.0
- Terragrunt >= 1.1
- AWS account with appropriate permissions
- A GitHub App installed on the target organization or repository

## Log

CloudWatch Log Groups: `/aws/lambda/<project_name>-main-webhook`, `-provision`, `-cleanup`.

## Contributing
Contributions are welcome! Please create pull request to this repository.

Contributions are welcome! Please create a pull request to this repository.

## License
This project is licensed under the MIT License. See the LICENSE file for more details.

This project is licensed under the MIT License. See the LICENSE file for more details.
Loading
Loading