Skip to content

Terraform module for setting up RunsOn on AWS. RunsOn allows to easily manage GitHub Actions self-hosted runners, for a fraction of the price.

License

Notifications You must be signed in to change notification settings

temap/terraform-aws-runs-on

 
 

Repository files navigation

RunsOn Terraform Module

Deploy RunsOn self-hosted GitHub Actions runners on AWS with Terraform/OpenTofu.

Usage

terraform {
  required_version = ">= 1.6.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

# Get available AZs
data "aws_availability_zones" "available" {
  state = "available"
}

# VPC Module - Creates networking infrastructure
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"

  name = "runs-on-vpc"
  cidr = "10.0.0.0/16"

  azs             = slice(data.aws_availability_zones.available.names, 0, 3)
  private_subnets = ["10.0.128.0/20", "10.0.144.0/20", "10.0.160.0/20"]
  public_subnets  = ["10.0.0.0/20", "10.0.16.0/20", "10.0.32.0/20"]

  # NAT Gateway for private subnets (required for private networking)
  # enable_nat_gateway = true
  # single_nat_gateway = true

  enable_dns_hostnames = true
  enable_dns_support   = true
}

# RunsOn Module - Deploys RunsOn infrastructure with smart defaults
module "runs-on" {
  source  = "runs-on/runs-on/aws"
  version = "v2.11.0"

  # Required: GitHub and License
  github_organization = "my-org"
  license_key         = "your-license-key"
  email               = "[email protected]"

  # Required: Network configuration (BYOV - Bring Your Own VPC)
  vpc_id             = module.vpc.vpc_id
  public_subnet_ids  = module.vpc.public_subnets
  private_subnet_ids = module.vpc.private_subnets
}

The module assumes you have your own VPC already configured.

Versioning

This module follows a versioning scheme that maps to the main RunsOn application version:

v{MAJOR}.{MINOR}.{PATCH}
  • v{MAJOR}.{MINOR}.{PATCH} - Matches the RunsOn application version (e.g., v2.11.0)

Examples:

  • v2.10.0 - Terraform release for RunsOn v2.10.0
  • v2.11.0 - Terraform release for RunsOn v2.11.0

When upgrading, check:

  1. The RunsOn version changelog at runs-on.com/changelog
  2. The Terraform module release notes in this repository

Tip

Cost Estimates:

  • RunsOn base: ~$3/mo (App Runner)
  • NAT Gateway: ~$32/mo per gateway + data transfer charges (required for private networking)
  • VPC Endpoints: ~$7/mo per interface endpoint + data transfer charges (S3 gateway endpoint is free)
  • EFS: ~$0.30/GB-month for storage
  • ECR: ~$0.10/GB-month for storage
  • Runners: EC2 costs vary by instance type and usage (pay only for what you use)

Resource Tags

All resources are tagged with runs-on-stack-name for discovery by the CLI. Key resources also have a runs-on-resource tag for identification:

  • apprunner-service - App Runner service
  • config-bucket - Configuration S3 bucket
  • cache-bucket - Cache S3 bucket
  • logging-bucket - Logging S3 bucket
  • ec2-log-group - EC2 instances CloudWatch log group

Do not remove these tags.

Architecture

flowchart TB
    subgraph AWS["Your AWS Infrastructure"]
        subgraph Core["Core Infrastructure (Basic)"]
            direction TB
            AppRunner["App Runner<br/><i>RunsOn Service</i>"]
            SQS["SQS Queues<br/><i>Job Processing</i>"]
            DynamoDB["DynamoDB<br/><i>State & Locks</i>"]
            S3["S3 Buckets<br/><i>Config & Cache</i>"]
            EC2["EC2 Launch Templates<br/><i>Linux & Windows</i>"]
            IAM["IAM Roles<br/><i>Permissions</i>"]
            
            subgraph Monitoring["Monitoring"]
                SNS["SNS Topics<br/><i>Alerts</i>"]
                CWLogs["CloudWatch Logs"]
                CWDashboard["CloudWatch Dashboard<br/>"]
            end
        end

        subgraph Optional["Optional Plug-ins"]
            direction TB
            EFS["EFS<br/><i>Shared Storage</i>"]
            ECR["ECR<br/><i>Image Cache</i>"]
            Private["Private Networking<br/><i>NAT Gateway</i>"]
            OTEL["OTEL / Prometheus<br/><i>Metrics Export</i>"]
        end

        VPC["VPC & Subnets"]
    end

    GitHub["GitHub"]:::github
    Alerts["Slack / Email"]

    GitHub <-->|API & webhooks| AppRunner
    AppRunner --> SQS
    AppRunner --> DynamoDB
    AppRunner --> S3
    AppRunner -->|launches| EC2
    EC2 --> IAM
    AppRunner --> CWLogs
    EC2 --> CWLogs
    SNS -.-> Alerts

    VPC -.->|network| Core

    EFS -.->|enable_efs| EC2
    ECR -.->|enable_ecr| EC2
    Private -.->|private_mode| EC2
    OTEL -.->|otel_exporter_endpoint| AppRunner

    style AWS fill:#8881,stroke:#888
    style Core fill:#0969da22,stroke:#0969da
    style Optional fill:#d2992222,stroke:#d29922
    style Monitoring fill:#23863622,stroke:#238636
    classDef github fill:#8b5cf6,stroke:#7c3aed,color:#fff,stroke-width:2px
Loading

Examples

Basic

Standard deployment with smart defaults:

module "runs-on" {
  source  = "runs-on/runs-on/aws"
  version = "v2.11.0"

  github_organization = "my-org"
  license_key         = "your-license-key"
  email               = "[email protected]"

  vpc_id            = "vpc-xxxxxxxx"
  public_subnet_ids = ["subnet-pub1", "subnet-pub2", "subnet-pub3"]
}

Private Networking

Enable private networking for static egress IPs (requires NAT Gateway):

module "runs-on" {
  source  = "runs-on/runs-on/aws"
  version = "v2.11.0"

  github_organization = "my-org"
  license_key         = "your-license-key"
  email               = "[email protected]"

  vpc_id             = "vpc-xxxxxxxx"
  public_subnet_ids  = ["subnet-pub1", "subnet-pub2", "subnet-pub3"]
  private_subnet_ids = ["subnet-priv1", "subnet-priv2", "subnet-priv3"]

  # Private networking mode options:
  #   "false"  - Disabled (default)
  #   "true"   - Opt-in: runners can use private=true label
  #   "always" - Default with opt-out: runners use private by default
  #   "only"   - Forced: all runners must use private subnets
  private_mode = "true"
}

EFS Enabled

Enable shared persistent storage across all runners for storing and sharing large files/artifacts:

module "runs-on" {
  source  = "runs-on/runs-on/aws"
  version = "v2.11.0"

  github_organization = "my-org"
  license_key         = "your-license-key"
  email               = "[email protected]"

  vpc_id            = "vpc-xxxxxxxx"
  public_subnet_ids = ["subnet-pub1", "subnet-pub2", "subnet-pub3"]

  # Enables persistent shared filesystem across all runners
  enable_efs = true
}

ECR Enabled

Enable image cache across workflow jobs, including Docker build cache:

module "runs-on" {
  source  = "runs-on/runs-on/aws"
  version = "v2.11.0"

  github_organization = "my-org"
  license_key         = "your-license-key"
  email               = "[email protected]"

  vpc_id            = "vpc-xxxxxxxx"
  public_subnet_ids = ["subnet-pub1", "subnet-pub2", "subnet-pub3"]

  # Creates private ECR for build cache
  enable_ecr = true
}

Full Featured

All features enabled together, with VPC endpoints for improved security and reduced data transfer costs:

# VPC with endpoints for private connectivity
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"

  name = "runs-on-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["us-east-1a", "us-east-1b", "us-east-1c"]
  private_subnets = ["10.0.128.0/20", "10.0.144.0/20", "10.0.160.0/20"]
  public_subnets  = ["10.0.0.0/20", "10.0.16.0/20", "10.0.32.0/20"]

  enable_nat_gateway = true
  single_nat_gateway = true # 'false' for High Availibility

  enable_dns_hostnames = true
  enable_dns_support   = true

  # VPC Endpoints 
  # Enable only if you're using private networking in RunsOn for full intra-VPC traffic to AWS APIs (avoids NAT Gateway data transfer costs).
  
  # S3 gateway endpoint is free and recommended
  enable_s3_endpoint = true
  
  # ECR endpoints are useful if you push/pull lots of images (enable_ecr = true)
  enable_ecr_api_endpoint     = false # For ECR API calls
  enable_ecr_dkr_endpoint     = false # For ECR image pulls

  # Interface endpoints below cost ~$7/mo each. 
  enable_ec2_endpoint         = false # For EC2 API calls
  enable_logs_endpoint        = false # For CloudWatch Logs
  enable_ssm_endpoint         = false # For SSM access
  enable_ssmmessages_endpoint = false # For SSM Session Manager
}

module "runs-on" {
  source  = "runs-on/runs-on/aws"
  version = "v2.11.0"

  github_organization = "my-org"
  license_key         = "your-license-key"
  email               = "[email protected]"

  vpc_id             = module.vpc.vpc_id
  public_subnet_ids  = module.vpc.public_subnets
  private_subnet_ids = module.vpc.private_subnets

  # Private networking (opt-in mode)
  private_mode = "true"

  # EFS shared storage
  enable_efs = true

  # ECR container registry
  enable_ecr = true

  # CloudWatch dashboard for monitoring
  enable_dashboard = true
}

Requirements

Name Version
terraform >= 1.6.0
aws >= 6.0
time >= 0.9

Providers

Name Version
aws 6.24.0
time 0.13.1

Modules

Name Source Version
compute ./modules/compute n/a
core ./modules/core n/a
optional ./modules/optional n/a
storage ./modules/storage n/a

Resources

Name Type
aws_security_group.runners resource
aws_vpc_security_group_egress_rule.all_ipv4 resource
aws_vpc_security_group_egress_rule.all_ipv6 resource
aws_vpc_security_group_ingress_rule.ssh resource
time_sleep.wait_for_nat resource

Inputs

Name Description Type Default Required
email Email address for alerts and notifications (requires confirmation) string n/a yes
github_organization GitHub organization or username for RunsOn integration string n/a yes
license_key RunsOn license key obtained from runs-on.com string n/a yes
public_subnet_ids List of public subnet IDs for runner instances (requires at least 1) list(string) n/a yes
vpc_id VPC ID where RunsOn infrastructure will be deployed string n/a yes
alert_https_endpoint HTTPS endpoint for alert notifications (optional) string "" no
alert_slack_webhook_url Slack webhook URL for alert notifications (optional) string "" no
app_alarm_daily_minutes Daily budget in minutes for the App Runner service before triggering an alarm number 4000 no
app_cpu CPU units for App Runner service (256, 512, 1024, 2048, 4096) number 256 no
app_debug Enable debug mode for RunsOn stack (prevents auto-shutdown of failed runner instances) bool false no
app_image App Runner container image for RunsOn service string "public.ecr.aws/c5h5o9k1/runs-on/runs-on:v2.10.0" no
app_memory Memory in MB for App Runner service (512, 1024, 2048, 3072, 4096, 6144, 8192, 10240, 12288) number 512 no
app_tag Application version tag for RunsOn service string "v2.10.0" no
bootstrap_tag Bootstrap script version tag string "v0.1.12" no
cache_expiration_days Number of days to retain cache artifacts in S3 before expiration number 10 no
cost_allocation_tag Name of the tag key used for cost allocation and tracking string "stack" no
default_admins Comma-separated list of default admin usernames string "" no
detailed_monitoring_enabled Enable detailed CloudWatch monitoring for EC2 instances (increases costs) bool false no
ebs_encryption_enabled Enable encryption for EBS volumes on runner instances bool false no
ebs_encryption_key_id KMS key ID for EBS volume encryption (leave empty for AWS managed key) string "" no
ec2_queue_size Maximum number of EC2 instances in queue number 2 no
enable_cost_reports Enable automated cost reports sent to alert email bool true no
enable_dashboard Create a CloudWatch dashboard for monitoring RunsOn operations (number of jobs processed, rate limit status, last error messages, etc.) bool true no
enable_ecr Enable ECR repository for ephemeral Docker image storage bool false no
enable_efs Enable EFS file system for shared storage across runners bool false no
environment Environment name used for resource tagging and RunsOn job filtering. RunsOn will only process jobs with an 'env' label matching this value. See https://runs-on.com/configuration/environments/ for details. string "production" no
force_delete_ecr Allow ECR repository to be deleted even when it contains images. Set to true for testing environments. bool false no
force_destroy_buckets Allow S3 buckets to be destroyed even when not empty. Set to false for production environments to prevent accidental data loss. bool false no
github_api_strategy Strategy for GitHub API calls (normal, conservative) string "normal" no
github_enterprise_url GitHub Enterprise Server URL (optional, leave empty for github.com) string "" no
integration_step_security_api_key API key for StepSecurity integration (optional) string "" no
ipv6_enabled Enable IPv6 support for runner instances bool false no
log_retention_days Number of days to retain CloudWatch logs for EC2 instances number 7 no
logger_level Logging level for RunsOn service (debug, info, warn, error) string "info" no
otel_exporter_endpoint OpenTelemetry exporter endpoint for observability (optional) string "" no
otel_exporter_headers OpenTelemetry exporter headers (optional) string "" no
permission_boundary_arn IAM permissions boundary ARN to attach to all IAM roles (optional) string "" no
prevent_destroy_optional_resources Prevent destruction of EFS and ECR resources. Set to true for production environments to protect against accidental data loss. bool true no
private_mode Private networking mode: 'false' (disabled), 'true' (opt-in with label), 'always' (default with opt-out), 'only' (forced, no public option) string "false" no
private_subnet_ids List of private subnet IDs for runner instances (required if private_mode is not 'false') list(string) [] no
runner_config_auto_extends_from Auto-extend runner configuration from this base config string ".github-private" no
runner_custom_tags Custom tags to apply to runner instances (comma-separated list) list(string) [] no
runner_default_disk_size Default EBS volume size in GB for runner instances number 40 no
runner_default_volume_throughput Default EBS volume throughput in MiB/s (gp3 volumes only) number 400 no
runner_large_disk_size Large EBS volume size in GB for runner instances requiring more storage number 80 no
runner_large_volume_throughput Large EBS volume throughput in MiB/s (gp3 volumes only) number 750 no
runner_max_runtime Maximum runtime in minutes for runners before forced termination number 720 no
security_group_ids Security group IDs for runner instances and App Runner service. If empty list provided, security groups will be created automatically. list(string) [] no
server_password Password for RunsOn server admin interface (optional) string "" no
spot_circuit_breaker Spot instance circuit breaker configuration (e.g., '2/15/30' = 2 failures in 15min, block for 30min) string "2/15/30" no
sqs_queue_oldest_message_threshold_seconds Threshold in seconds for oldest message in SQS queues before triggering an alarm (0 to disable) number 0 no
ssh_allowed Allow SSH access to runner instances bool true no
ssh_cidr_range CIDR range allowed for SSH access to runner instances (only applies if ssh_allowed is true) string "0.0.0.0/0" no
stack_name Name for the RunsOn stack (used for resource naming) string "runs-on" no
tags Tags to apply to all resources. Note: 'runs-on-stack-name' is added automatically for resource discovery. map(string)
{
"ManagedBy": "opentofu/terraform"
}
no

Outputs

Name Description
apprunner_log_group_name CloudWatch log group name for App Runner service
apprunner_service_arn ARN of the RunsOn App Runner service
apprunner_service_status Status of the RunsOn App Runner service
apprunner_service_url URL of the RunsOn App Runner service
aws_account_id AWS Account ID where RunsOn is deployed
aws_region AWS region where RunsOn is deployed
cache_bucket_name Name of the S3 cache bucket
config_bucket_name Name of the S3 configuration bucket
dashboard_name Name of the CloudWatch Dashboard (if enabled)
dashboard_url URL to the CloudWatch Dashboard (if enabled)
dynamodb_locks_table_name Name of the DynamoDB locks table
dynamodb_workflow_jobs_table_name Name of the DynamoDB workflow jobs table
ec2_instance_log_group_name CloudWatch log group name for EC2 instances
ec2_instance_profile_arn ARN of the EC2 instance profile
ec2_instance_role_arn ARN of the EC2 instance IAM role
ec2_instance_role_name Name of the EC2 instance IAM role
ecr_repository_name Name of the ECR repository (if enabled)
ecr_repository_url URL of the ECR repository (if enabled)
efs_file_system_dns_name DNS name of the EFS file system (if enabled)
efs_file_system_id ID of the EFS file system (if enabled)
getting_started Quick start guide for using this RunsOn deployment
launch_template_linux_default_id ID of the Linux default launch template
launch_template_linux_private_id ID of the Linux private launch template (if private networking enabled)
launch_template_windows_default_id ID of the Windows default launch template
launch_template_windows_private_id ID of the Windows private launch template (if private networking enabled)
logging_bucket_name Name of the S3 logging bucket
security_group_ids Security group IDs being used (created or provided)
sns_topic_arn ARN of the SNS alerts topic
sqs_queue_events_url URL of the events SQS queue
sqs_queue_github_url URL of the GitHub SQS queue
sqs_queue_housekeeping_url URL of the housekeeping SQS queue
sqs_queue_jobs_url URL of the jobs SQS queue
sqs_queue_main_url URL of the main SQS queue
sqs_queue_pool_url URL of the pool SQS queue
sqs_queue_termination_url URL of the termination SQS queue
stack_name The stack name used for this deployment

License

MIT

About

Terraform module for setting up RunsOn on AWS. RunsOn allows to easily manage GitHub Actions self-hosted runners, for a fraction of the price.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • HCL 66.2%
  • Go 30.9%
  • Makefile 1.6%
  • Other 1.3%