Skip to content
Open
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions README.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/examples/enterprise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--8<-- "examples/enterprise/README.md"
3 changes: 2 additions & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ module "github-runner" {
webhook_lambda_zip = "lambdas-download/webhook.zip"
runner_binaries_syncer_lambda_zip = "lambdas-download/runner-binaries-syncer.zip"
runners_lambda_zip = "lambdas-download/runners.zip"
enable_organization_runners = true

runner_registration_level = "org"
}
```

Expand Down
4 changes: 2 additions & 2 deletions examples/default/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ module "runners" {
# runner_binaries_syncer_lambda_zip = "../lambdas-download/runner-binaries-syncer.zip"
# runners_lambda_zip = "../lambdas-download/runners.zip"

enable_organization_runners = true
runner_extra_labels = ["default", "example"]
runner_registration_level = "org"
runner_extra_labels = ["default", "example"]

# enable access to the runners via SSM
enable_ssm_on_runners = true
Expand Down
129 changes: 129 additions & 0 deletions examples/enterprise/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Enterprise Runner Example

This example demonstrates how to deploy GitHub self-hosted runners at the **Enterprise level** using PAT-based authentication.

## Prerequisites

1. **GitHub Enterprise Account** — You need a GitHub Enterprise Cloud account with admin access.
2. **Enterprise PAT** — Create one or more Personal Access Tokens with the `manage_runners:enterprise` scope:
- Go to your GitHub account settings → Developer settings → Personal access tokens (classic)
- Create a new token with the `manage_runners:enterprise` scope
- Save the token securely — you'll need it for the `enterprise_pat` variable
- **Tip**: To distribute API calls and avoid rate limiting, create multiple PATs (from different accounts if possible) and provide them as a comma-separated string. The Lambda functions will randomly select one PAT per invocation.
3. **Enterprise Webhook** — Configure an enterprise-level webhook to send `workflow_job` events to the module's webhook endpoint. Choose a random secret for the webhook — you'll need it for the `webhook_secret` parameter.

> **Note**: Enterprise runners do **not** require a GitHub App. Only a webhook secret is needed to verify incoming webhook payloads. The PAT handles all GitHub API interactions.

## Configuration

```hcl
module "runners" {
source = "../../"

# Enterprise runner registration
runner_registration_level = "enterprise"
enterprise_slug = "my-enterprise"

# Enterprise PAT — stored in AWS SSM Parameter Store as SecureString
# Single PAT:
enterprise_pat = {
pat = var.enterprise_pat
}

# Multiple PATs (comma-separated) for rate limit distribution:
# enterprise_pat = {
# pat = "ghp_token1,ghp_token2,ghp_token3"
# }

# No GitHub App is required for enterprise runners.
# Only the webhook_secret is needed to verify incoming webhook payloads.
github_app = {
webhook_secret = random_id.random.hex
}

# ... other configuration
}
```

## Variables

| Name | Description | Type | Required |
|------|-------------|------|----------|
| `enterprise_slug` | The slug of the GitHub Enterprise account | string | Yes |
| `enterprise_pat` | PAT with `manage_runners:enterprise` scope | string (sensitive) | Yes |
| `environment` | Environment name prefix | string | No |
| `aws_region` | AWS region for deployment | string | No |

The `github_app` block only requires `webhook_secret` — the `key_base64` and `id` fields are **not** needed for enterprise runners.

## Verification

After deployment:

1. Check the webhook endpoint in the Terraform outputs
2. Configure the enterprise webhook to point to the endpoint
3. Trigger a workflow run in any repository under the enterprise
4. Verify runners appear in **Enterprise Settings → Actions → Runners**

## Migration from Organization Runners

If you're migrating from organization-level runners:

```hcl
# Before
enable_organization_runners = true

# After
runner_registration_level = "enterprise"
enterprise_slug = "my-enterprise"
enterprise_pat = {
pat = var.enterprise_pat
}
```

<!-- BEGIN_TF_DOCS -->
## Requirements

| Name | Version |
|------|---------|
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.3.0 |
| <a name="requirement_aws"></a> [aws](#requirement\_aws) | >= 6.21 |
| <a name="requirement_local"></a> [local](#requirement\_local) | ~> 2.0 |
| <a name="requirement_random"></a> [random](#requirement\_random) | ~> 3.0 |

## Providers

| Name | Version |
|------|---------|
| <a name="provider_random"></a> [random](#provider\_random) | ~> 3.0 |

## Modules

| Name | Source | Version |
|------|--------|---------|
| <a name="module_base"></a> [base](#module\_base) | ../base | n/a |
| <a name="module_runners"></a> [runners](#module\_runners) | ../../ | n/a |

## Resources

| Name | Type |
|------|------|
| [random_id.random](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/id) | resource |

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_aws_region"></a> [aws\_region](#input\_aws\_region) | AWS region. | `string` | `"eu-west-1"` | no |
| <a name="input_enterprise_pat"></a> [enterprise\_pat](#input\_enterprise\_pat) | Personal Access Token with 'manage\_runners:enterprise' scope for enterprise runner management. | `string` | n/a | yes |
| <a name="input_enterprise_slug"></a> [enterprise\_slug](#input\_enterprise\_slug) | The slug of the GitHub Enterprise account. Example: 'my-enterprise'. | `string` | n/a | yes |
| <a name="input_environment"></a> [environment](#input\_environment) | Environment name, used as prefix. | `string` | `null` | no |

## Outputs

| Name | Description |
|------|-------------|
| <a name="output_runners"></a> [runners](#output\_runners) | n/a |
| <a name="output_webhook_endpoint"></a> [webhook\_endpoint](#output\_webhook\_endpoint) | n/a |
| <a name="output_webhook_secret"></a> [webhook\_secret](#output\_webhook\_secret) | n/a |
<!-- END_TF_DOCS -->
61 changes: 61 additions & 0 deletions examples/enterprise/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
locals {
environment = var.environment != null ? var.environment : "enterprise"
aws_region = var.aws_region
}

resource "random_id" "random" {
byte_length = 20
}

module "base" {
source = "../base"

prefix = local.environment
aws_region = local.aws_region
}

module "runners" {
source = "../../"
create_service_linked_role_spot = true
aws_region = local.aws_region
vpc_id = module.base.vpc.vpc_id
subnet_ids = module.base.vpc.private_subnets

prefix = local.environment
tags = {
Project = "Enterprise Runners"
}

# Enterprise runners do not require a GitHub App.
# Only the webhook_secret is needed to verify incoming webhook payloads.
github_app = {
webhook_secret = random_id.random.hex
}

# Enterprise runner registration level
runner_registration_level = "enterprise"
enterprise_slug = var.enterprise_slug

# Enterprise PAT for authentication
enterprise_pat = {
pat = var.enterprise_pat
}

# Runner labels
runner_extra_labels = ["enterprise", "example"]

# enable access to the runners via SSM
enable_ssm_on_runners = true

instance_types = ["m7a.large", "m5.large"]

# override delay of events in seconds
delay_webhook_event = 5
runners_maximum_count = 5

# override scaling down
scale_down_schedule_expression = "cron(* * * * ? *)"

enable_user_data_debug_logging_runner = true
}

15 changes: 15 additions & 0 deletions examples/enterprise/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
output "runners" {
value = {
lambda_syncer_name = module.runners.binaries_syncer.lambda.function_name
}
}

output "webhook_endpoint" {
value = module.runners.webhook.endpoint
}

output "webhook_secret" {
sensitive = true
value = random_id.random.hex
}

10 changes: 10 additions & 0 deletions examples/enterprise/providers.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
provider "aws" {
region = local.aws_region

default_tags {
tags = {
Example = local.environment
}
}
}

23 changes: 23 additions & 0 deletions examples/enterprise/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
variable "enterprise_slug" {
description = "The slug of the GitHub Enterprise account. Example: 'my-enterprise'."
type = string
}

variable "enterprise_pat" {
description = "Personal Access Token with 'manage_runners:enterprise' scope for enterprise runner management."
type = string
sensitive = true
}

variable "environment" {
description = "Environment name, used as prefix."
type = string
default = null
}

variable "aws_region" {
description = "AWS region."
type = string
default = "eu-west-1"
}

18 changes: 18 additions & 0 deletions examples/enterprise/versions.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 6.21"
}
local = {
source = "hashicorp/local"
version = "~> 2.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.0"
}
}
required_version = ">= 1.3.0"
}

4 changes: 2 additions & 2 deletions examples/ephemeral/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ module "runners" {
# runner_binaries_syncer_lambda_zip = "../lambdas-download/runner-binaries-syncer.zip"
# runners_lambda_zip = "../lambdas-download/runners.zip"

enable_organization_runners = true
runner_extra_labels = ["default", "example"]
runner_registration_level = "org"
runner_extra_labels = ["default", "example"]

# enable access to the runners via SSM
enable_ssm_on_runners = true
Expand Down
4 changes: 2 additions & 2 deletions examples/external-managed-ssm-secrets/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ module "runners" {
webhook_secret_ssm = var.github_app_ssm_parameters.webhook_secret
}

enable_organization_runners = true
runner_extra_labels = ["default", "example"]
runner_registration_level = "org"
runner_extra_labels = ["default", "example"]

# enable access to the runners via SSM
enable_ssm_on_runners = true
Expand Down
2 changes: 1 addition & 1 deletion examples/permissions-boundary/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ module "runners" {
webhook_lambda_zip = "../lambdas-download/webhook.zip"
runner_binaries_syncer_lambda_zip = "../lambdas-download/runner-binaries-syncer.zip"
runners_lambda_zip = "../lambdas-download/runners.zip"
enable_organization_runners = false
runner_registration_level = "repo"
runner_extra_labels = ["default", "example"]

instance_profile_path = "/runners/"
Expand Down
4 changes: 2 additions & 2 deletions examples/prebuilt/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ module "runners" {
vpc_id = module.base.vpc.vpc_id
subnet_ids = module.base.vpc.private_subnets

prefix = local.environment
enable_organization_runners = false
prefix = local.environment
runner_registration_level = "repo"

github_app = {
key_base64 = var.github_app.key_base64
Expand Down
4 changes: 3 additions & 1 deletion lambdas/functions/control-plane/src/aws/runners.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DefaultTargetCapacityType, SpotAllocationStrategy } from '@aws-sdk/client-ec2';
import { LambdaRunnerSource } from '../scale-runners/scale-up';

export type RunnerType = 'Org' | 'Repo';
export type RunnerType = 'Org' | 'Repo' | 'Enterprise';

export interface RunnerList {
instanceId: string;
Expand All @@ -10,6 +10,7 @@ export interface RunnerList {
type?: string;
repo?: string;
org?: string;
enterprise?: string;
orphan?: boolean;
runnerId?: string;
bypassRemoval?: boolean;
Expand All @@ -34,6 +35,7 @@ export interface RunnerInputParameters {
environment: string;
runnerType: RunnerType;
runnerOwner: string;
enterpriseSlug?: string;
subnets: string[];
launchTemplateName: string;
ec2instanceCriteria: {
Expand Down
4 changes: 4 additions & 0 deletions lambdas/functions/control-plane/src/aws/runners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ function getRunnerInfo(runningInstances: DescribeInstancesResult) {
type: i.Tags?.find((e) => e.Key === 'ghr:Type')?.Value as string,
repo: i.Tags?.find((e) => e.Key === 'ghr:Repo')?.Value as string,
org: i.Tags?.find((e) => e.Key === 'ghr:Org')?.Value as string,
enterprise: i.Tags?.find((e) => e.Key === 'ghr:enterprise')?.Value as string,
orphan: i.Tags?.find((e) => e.Key === 'ghr:orphan')?.Value === 'true',
runnerId: i.Tags?.find((e) => e.Key === 'ghr:github_runner_id')?.Value as string,
bypassRemoval: i.Tags?.find((e) => e.Key === 'ghr:bypass-removal')?.Value === 'true',
Expand Down Expand Up @@ -244,6 +245,9 @@ async function createInstances(
{ Key: 'ghr:created_by', Value: runnerParameters.source },
{ Key: 'ghr:Type', Value: runnerParameters.runnerType },
{ Key: 'ghr:Owner', Value: runnerParameters.runnerOwner },
...(runnerParameters.runnerType === 'Enterprise' && runnerParameters.enterpriseSlug
? [{ Key: 'ghr:enterprise', Value: runnerParameters.enterpriseSlug }]
: []),
];

if (runnerParameters.tracingEnabled) {
Expand Down
Loading