Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
2cb1ed3
fix(terraform): complete SSM provider wiring
edersonbrilhante Sep 24, 2026
07094af
fix(multi-runner): guard optional SSM configuration
edersonbrilhante Sep 24, 2026
911eb5a
fix(multi-runner): default omitted SSM settings
edersonbrilhante Sep 24, 2026
6600dc9
docs: auto update terraform docs
github-actions[bot] Sep 24, 2026
aecf223
refactor(ec2): isolate SSM runner policy
edersonbrilhante Sep 24, 2026
7e1d87f
refactor: decouple ssm in ec2
edersonbrilhante Sep 24, 2026
cec4c26
refactor: cleanup ec2 module
edersonbrilhante Sep 24, 2026
0245114
refactor: cleanip runner-config
edersonbrilhante Sep 24, 2026
9920e14
fix: add back kms key
edersonbrilhante Sep 24, 2026
e8ebc00
refactor: cleanup control plane file
edersonbrilhante Sep 24, 2026
e0034af
fix: fix tf validate
edersonbrilhante Sep 24, 2026
118cfa0
fix: fix tf validation issue
edersonbrilhante Sep 24, 2026
4f71e3a
fix: remove change in webhook for now
edersonbrilhante Sep 24, 2026
a4c0f5f
fix: remove changes
edersonbrilhante Sep 24, 2026
96dcefb
fix: clean up config
edersonbrilhante Sep 24, 2026
17b2b32
refactor: cleanup webhook
edersonbrilhante Sep 24, 2026
0bfa30c
refactor: split ssm into files
edersonbrilhante Sep 24, 2026
bac5fe4
refactor: move ssm references to ssm file
edersonbrilhante Sep 24, 2026
baffab4
Merge branch 'main' into feat/dynamodb-storage-provider-terraform
edersonbrilhante Sep 24, 2026
4a7fda7
fix: fix ssm null
edersonbrilhante Sep 24, 2026
96e1e2a
docs: auto update terraform docs
github-actions[bot] Sep 24, 2026
e474ce5
fix: fix empty statement
edersonbrilhante Sep 24, 2026
a59d50d
fix: fix migration issue
edersonbrilhante Sep 24, 2026
201135f
fix: fix mismatch type
edersonbrilhante Sep 24, 2026
acb36bc
docs: auto update terraform docs
github-actions[bot] Sep 24, 2026
ecb6543
test: fix tests to meet the new format
edersonbrilhante Sep 24, 2026
d4e54e3
fix: fix tests
edersonbrilhante Sep 24, 2026
8469d58
test: fix tests
edersonbrilhante Sep 24, 2026
ccaba65
test: fix tests in tofu
edersonbrilhante Sep 24, 2026
223a5d6
docs: auto update terraform docs
github-actions[bot] Sep 24, 2026
f8c5a89
test: fix migration test
edersonbrilhante Sep 24, 2026
dca8343
fix: fix mismatch between v1 and v2
edersonbrilhante Sep 25, 2026
d7e875f
test: fix test
edersonbrilhante Sep 25, 2026
c7fc200
docs: auto update terraform docs
github-actions[bot] Sep 25, 2026
e0e0177
fix: fix last mismatch
edersonbrilhante Sep 25, 2026
c03df37
Merge branch 'main' into feat/dynamodb-storage-provider-terraform
edersonbrilhante Sep 25, 2026
9db63f4
docs: auto update terraform docs
github-actions[bot] Sep 25, 2026
b91b158
fix: adjust conflict
edersonbrilhante Sep 25, 2026
2b6f221
docs: auto update terraform docs
github-actions[bot] Sep 25, 2026
a0f1cad
chore: rename files
edersonbrilhante Sep 25, 2026
1f6983e
refactor: decouple ssm from scaleset in ec2
edersonbrilhante Sep 26, 2026
1455de7
test: fix tests
edersonbrilhante Sep 26, 2026
8ffe340
fix: fix assume role
edersonbrilhante Sep 26, 2026
ff23bae
Merge remote-tracking branch 'origin/main' into feat/dynamodb-storage…
edersonbrilhante Sep 26, 2026
ad78451
fix: fix after merge
edersonbrilhante Sep 26, 2026
7ae309a
docs: auto update terraform docs
github-actions[bot] Sep 26, 2026
5e8a109
Merge branch 'enable-all-tf-tests' into feat/dynamodb-storage-provide…
edersonbrilhante Sep 27, 2026
696db5f
docs: auto update terraform docs
github-actions[bot] Sep 27, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/ministack.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
- termination-watcher
services:
ministack:
image: ghcr.io/ministackorg/ministack:1.5.13@sha256:ce3c906f2866ff953ce4c56f06b1fa3e453bc32e41c00de17b5f5a8672c5a42c
image: ghcr.io/ministackorg/ministack:1.5.16@sha256:528fbf57148665b5b0c679dc65eac8c33f7e7dded1b80bc151bd85c7420b6cd4
ports:
- 4566:4566
env:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/smoke-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
timeout-minutes: 30
services:
ministack:
image: ghcr.io/ministackorg/ministack:1.5.13@sha256:ce3c906f2866ff953ce4c56f06b1fa3e453bc32e41c00de17b5f5a8672c5a42c
image: ghcr.io/ministackorg/ministack:1.5.16@sha256:528fbf57148665b5b0c679dc65eac8c33f7e7dded1b80bc151bd85c7420b6cd4
ports:
- 4566:4566
options: --add-host=host.docker.internal:host-gateway
Expand Down
16 changes: 16 additions & 0 deletions .github/workflows/terraform.yml
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,14 @@ jobs:
mkdir -p "$HOME/.terraform.d/plugin"
echo "TF_PLUGIN_CACHE_DIR=$HOME/.terraform.d/plugin" >> "$GITHUB_ENV"

- name: "Fake zip files" # Validate will fail if it cannot find the zip files
run: |
touch lambdas/functions/webhook/webhook.zip
touch lambdas/functions/control-plane/runners.zip
touch lambdas/functions/gh-agent-syncer/runner-binaries-syncer.zip
touch lambdas/functions/ami-housekeeper/ami-housekeeper.zip
touch lambdas/functions/termination-watcher/termination-watcher.zip

- name: Setup Terraform
if: matrix.iac.command == 'terraform'
uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
Expand Down Expand Up @@ -383,6 +391,14 @@ jobs:
mkdir -p "$HOME/.terraform.d/plugin"
echo "TF_PLUGIN_CACHE_DIR=$HOME/.terraform.d/plugin" >> "$GITHUB_ENV"

- name: "Fake zip files" # Validate will fail if it cannot find the zip files
run: |
touch lambdas/functions/webhook/webhook.zip
touch lambdas/functions/control-plane/runners.zip
touch lambdas/functions/gh-agent-syncer/runner-binaries-syncer.zip
touch lambdas/functions/ami-housekeeper/ami-housekeeper.zip
touch lambdas/functions/termination-watcher/termination-watcher.zip

- name: Setup Terraform
if: matrix.iac.command == 'terraform'
uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0002-runner-orchestration-provider-boundary.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ configuration it helps produce.
| `modules/orchestration-providers/webhook/scale-runners` | Owns scale-up and scale-down Lambdas, schedules, queue integration, IAM, and outputs. |
| `modules/orchestration-providers/webhook/pool` | Owns optional scheduled pool resources and IAM. |
| `modules/orchestration-providers/webhook/job-retry` | Owns optional queued-job retry resources and IAM. |
| `modules/runner-config/ssm-housekeeper` | Owns provider-neutral cleanup of runner token and configuration parameters. |
| `modules/runner-config/runner-config-housekeeper` | Owns provider-neutral cleanup of runner token and configuration parameters. |
| `modules/compute-providers/<namespace>/<provider>` | Owns provider-specific capacity resources and returns policy, environment, managed-policy, and resource capabilities. |

Provider leaf modules live below `modules/orchestration-providers/<provider>`,
Expand Down
2 changes: 1 addition & 1 deletion examples/multi-runner-scale-set/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ The GitHub App must be installed for the configured GitHub account.

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_ami"></a> [ami](#input\_ami) | Optional AMI configuration keyed by runner lane. | <pre>map(object({<br/> filter = optional(map(list(string)), { state = ["available"] })<br/> owners = optional(list(string), ["amazon"])<br/> id_ssm_parameter = optional(object({<br/> arn = string<br/> }), null)<br/> kms_key = optional(object({<br/> arn = string<br/> }), null)<br/> }))</pre> | `{}` | no |
| <a name="input_ami"></a> [ami](#input\_ami) | Optional AMI configuration keyed by runner lane. | <pre>map(object({<br/> filter = optional(map(list(string)), { state = ["available"] })<br/> owners = optional(list(string), ["amazon"])<br/> ssm_parameter = optional(object({<br/> path = optional(string, null)<br/> arn = optional(string, null)<br/> }), null)<br/> kms_key = optional(object({<br/> arn = string<br/> }), null)<br/> }))</pre> | `{}` | no |
| <a name="input_aws_region"></a> [aws\_region](#input\_aws\_region) | AWS region to deploy to. | `string` | `"eu-west-1"` | no |
| <a name="input_environment"></a> [environment](#input\_environment) | Environment name, used as prefix. | `string` | n/a | yes |
| <a name="input_github"></a> [github](#input\_github) | Optional GitHub endpoint and scale-set ownership settings. | <pre>object({<br/> url = optional(string, null)<br/> ssl_verify = optional(bool, true)<br/> runner_owner = optional(string, null)<br/> registration_level = optional(string, "organization")<br/> })</pre> | `{}` | no |
Expand Down
7 changes: 4 additions & 3 deletions examples/multi-runner-scale-set/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,13 @@ variable "ami" {
type = map(object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
id_ssm_parameter = optional(object({
arn = string
ssm_parameter = optional(object({
path = optional(string, null)
arn = optional(string, null)
}), null)
kms_key = optional(object({
arn = string
}), null)
}))
default = {}
}
}
2 changes: 1 addition & 1 deletion examples/multi-runner-v2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ variable source in real deployments rather than committed to configuration.

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_ami"></a> [ami](#input\_ami) | Optional AMI configuration keyed by runner lane. | <pre>map(object({<br/> filter = optional(map(list(string)), { state = ["available"] })<br/> owners = optional(list(string), ["amazon"])<br/> id_ssm_parameter = optional(object({<br/> arn = string<br/> }), null)<br/> kms_key = optional(object({<br/> arn = string<br/> }), null)<br/> }))</pre> | `{}` | no |
| <a name="input_ami"></a> [ami](#input\_ami) | Optional AMI configuration keyed by runner lane. | <pre>map(object({<br/> filter = optional(map(list(string)), { state = ["available"] })<br/> owners = optional(list(string), ["amazon"])<br/> ssm_parameter = optional(object({<br/> path = optional(string, null)<br/> arn = optional(string, null)<br/> }), null)<br/> kms_key = optional(object({<br/> arn = string<br/> }), null)<br/> }))</pre> | `{}` | no |
| <a name="input_aws_region"></a> [aws\_region](#input\_aws\_region) | AWS region to deploy to. | `string` | `"eu-west-1"` | no |
| <a name="input_environment"></a> [environment](#input\_environment) | Environment name, used as prefix. | `string` | `null` | no |
| <a name="input_github_app"></a> [github\_app](#input\_github\_app) | GitHub App ID and base64-encoded private key. | <pre>object({<br/> id = string<br/> key_base64 = string<br/> })</pre> | n/a | yes |
Expand Down
6 changes: 3 additions & 3 deletions examples/multi-runner-v2/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,9 @@ module "runners" {
name = ["Windows_Server-2022-English-Full-ECS_Optimized-*"]
state = ["available"]
}
owners = ["amazon"]
id_ssm_parameter = null
kms_key = null
owners = ["amazon"]
ssm_parameter = null
kms_key = null
})
}
}
Expand Down
5 changes: 3 additions & 2 deletions examples/multi-runner-v2/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ variable "ami" {
type = map(object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
id_ssm_parameter = optional(object({
arn = string
ssm_parameter = optional(object({
path = optional(string, null)
arn = optional(string, null)
}), null)
kms_key = optional(object({
arn = string
Expand Down
10 changes: 7 additions & 3 deletions modules/compute-providers/aws/ec2/README.md

Large diffs are not rendered by default.

11 changes: 0 additions & 11 deletions modules/compute-providers/aws/ec2/logging.tf
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,6 @@ locals {

}


resource "aws_ssm_parameter" "cloudwatch_agent_config_runner" {
count = var.config.cloudwatch_agent.enabled ? 1 : 0
name = "${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.config}/cloudwatch_agent_config_runner"
type = "String"
value = var.config.cloudwatch_agent.config != null ? var.config.cloudwatch_agent.config : templatefile("${path.module}/templates/cloudwatch_config.json", {
logfiles = jsonencode(local.logfiles)
})
tags = local.ssm_parameter_tags
}

resource "aws_cloudwatch_log_group" "gh_runners" {
count = length(local.loggroups_names)
name = local.loggroups_names[count.index]
Expand Down
124 changes: 124 additions & 0 deletions modules/compute-providers/aws/ec2/policies.ami.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
locals {
# Handle AMI configuration
ami_config = var.config.ami != null ? var.config.ami : {
filter = local.default_ami[var.runner.os]
owners = ["amazon"]
ssm_parameter = null
kms_key = null
}

default_ssm_parameter_path = "/github-action-runners/${var.prefix}/runners/config"
ami_ssm_parameter_path = try(local.ami_config.ssm_parameter.path, null) != null ? local.ami_config.ssm_parameter.path : local.default_ssm_parameter_path
ami_kms_key_enabled = local.ami_config.kms_key != null
ami_kms_key_arn = local.ami_kms_key_enabled ? local.ami_config.kms_key.arn : null
ami_filter = merge(local.default_ami[var.runner.os], local.ami_config.filter)

# The path is plan-known and distinguishes an external parameter (ARN only)
# from a provider-managed parameter (explicit path or omitted configuration).
ami_id_ssm_external = local.ami_config.ssm_parameter != null && try(local.ami_config.ssm_parameter.path, null) == null
ami_id_ssm_module_managed = !local.ami_id_ssm_external
ami_id_ssm_parameter_arn = local.ami_id_ssm_external ? local.ami_config.ssm_parameter.arn : null
# Extract parameter name from ARN (format: arn:aws:ssm:region:account:parameter/path/to/param)
ami_id_ssm_parameter_name = local.ami_id_ssm_external ? try(regex("parameter(/.+)$", local.ami_id_ssm_parameter_arn)[0], null) : null

image_id = local.ami_id_ssm_module_managed ? "resolve:ssm:${aws_ssm_parameter.runner_ami_id[0].arn}" : local.ami_id_ssm_external ? "resolve:ssm:${local.ami_id_ssm_parameter_arn}" : data.aws_ami.runner[0].id
}

data "aws_ami" "runner" {
count = local.ami_id_ssm_external ? 0 : 1

most_recent = "true"

dynamic "filter" {
for_each = local.ami_filter
content {
name = filter.key
values = filter.value
}
}

owners = local.ami_config.owners
}

resource "aws_ssm_parameter" "runner_ami_id" {
count = local.ami_id_ssm_module_managed ? 1 : 0
name = "${local.ami_ssm_parameter_path}/ami_id"
type = "String"
data_type = "aws:ec2:image"
value = data.aws_ami.runner[0].id

tags = merge(
local.provider_tags,
local.ssm_parameter_tags,
{
# Remove parentheses from AMI name to comply with AWS tag constraints
"ghr:ami_name" = replace(data.aws_ami.runner[0].name, "/[()]/", "")
},
{
"ghr:ami_creation_date" = data.aws_ami.runner[0].creation_date
},
{
"ghr:ami_deprecation_time" = data.aws_ami.runner[0].deprecation_time
}
)
}

data "aws_iam_policy_document" "ami_id_ssm_parameter_read" {
count = local.ami_id_ssm_external ? 1 : 0

statement {
effect = "Allow"
sid = "AllowSSMParameterRead"
actions = ["ssm:GetParameter"]
resources = [local.ami_id_ssm_parameter_arn]
}
}

resource "aws_iam_policy" "ami_id_ssm_parameter_read" {
count = local.ami_id_ssm_external ? 1 : 0
name = "${var.prefix}-ami-id-ssm-parameter-read"
path = local.role_path
description = "Allows for reading ${var.prefix} GitHub runner AMI ID from an SSM parameter"
tags = local.provider_tags
policy = data.aws_iam_policy_document.ami_id_ssm_parameter_read[0].json
}

data "aws_iam_policy_document" "ami_id_ssm" {
statement {
effect = "Allow"
sid = "AllowSSMParameterRead"
actions = [
"ssm:GetParameter",
"ssm:GetParameters",
]
resources = [local.ami_id_ssm_module_managed ? aws_ssm_parameter.runner_ami_id[0].arn : local.ami_id_ssm_parameter_arn]
}

dynamic "statement" {
for_each = local.ami_kms_key_enabled ? [local.ami_kms_key_arn] : []

content {
effect = "Allow"
sid = "AllowKMSKeyUsage"
actions = ["kms:DescribeKey", "kms:ReEncrypt*", "kms:Decrypt"]
resources = [statement.value]
}
}

dynamic "statement" {
for_each = local.ami_kms_key_enabled ? [local.ami_kms_key_arn] : []

content {
effect = "Allow"
sid = "AllowKMSKeyGrant"
actions = ["kms:CreateGrant"]
resources = [statement.value]

condition {
test = "Bool"
variable = "aws:ViaAWSService"
values = ["true"]
}
}
}
}
10 changes: 10 additions & 0 deletions modules/compute-providers/aws/ec2/policies.common.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# IAM policy documents shared by the EC2 orchestration capabilities.
data "aws_iam_policy_document" "service_linked_role" {
count = var.config.create_service_linked_role_spot ? 1 : 0

statement {
effect = "Allow"
actions = ["iam:CreateServiceLinkedRole"]
resources = ["arn:${var.aws_partition}:iam::*:role/aws-service-role/*"]
}
}
115 changes: 115 additions & 0 deletions modules/compute-providers/aws/ec2/policies.runner.aws.ssm.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# extends logging.tf
resource "aws_ssm_parameter" "cloudwatch_agent_config_runner" {
count = var.storage_provider.aws.ssm != null && var.config.cloudwatch_agent.enabled ? 1 : 0
name = "${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.config}/cloudwatch_agent_config_runner"
type = "String"
value = var.config.cloudwatch_agent.config != null ? var.config.cloudwatch_agent.config : templatefile("${path.module}/templates/cloudwatch_config.json", {
logfiles = jsonencode(local.logfiles)
})
tags = local.ssm_parameter_tags
}

# extends policies-runner.tf
data "aws_iam_policy_document" "ssm_parameters" {
count = var.storage_provider.aws.ssm != null ? 1 : 0

statement {
effect = "Allow"
actions = [
"ssm:DeleteParameter",
"ssm:GetParameters",
"ssm:GetParameter",
]
resources = [
"${local.ssm_parameter_arn_prefix}${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.tokens}/*",
]

condition {
test = "StringLike"
variable = "ec2:SourceInstanceARN"
values = ["*/&{aws:ResourceTag/InstanceId}"]
}
}

statement {
effect = "Allow"
actions = [
"ssm:GetParameter",
"ssm:GetParameters",
"ssm:GetParametersByPath",
]
resources = [
local.ssm_config_arn,
"${local.ssm_config_arn}/*",
]
}
}

data "aws_iam_policy_document" "ssm_cloudwatch" {
count = var.storage_provider.aws.ssm != null && var.config.cloudwatch_agent.enabled ? 1 : 0

statement {
effect = "Allow"
actions = [
"ssm:GetParameter",
]
resources = [
"${aws_ssm_parameter.cloudwatch_agent_config_runner[0].arn}/*",
]
}
}

locals {
ssm_runner_inline_policies = var.storage_provider.aws.ssm != null ? {
ssm_parameters = {
name = "runner-ssm-parameters"
policy_json = data.aws_iam_policy_document.ssm_parameters[0].json
}
} : {
}
}

# runner config
locals {
ssm_root_path = try(var.storage_provider.aws.ssm.paths.root, null)
ssm_config_path = local.ssm_root_path == null ? null : "${local.ssm_root_path}/${var.storage_provider.aws.ssm.paths.config}"
ssm_parameter_arn_prefix = "arn:${var.aws_partition}:ssm:${var.aws_region}:${data.aws_caller_identity.current.account_id}:parameter"
ssm_config_arn = local.ssm_config_path == null ? null : "${local.ssm_parameter_arn_prefix}${local.ssm_config_path}"

ssm_parameter_tags = merge(
local.provider_tags,
try(var.storage_provider.aws.ssm.tags, {}),
try(var.storage_provider.aws.ssm.parameters.tags, {}),
)

ssm_runner_tags = var.storage_provider.aws.ssm != null ? {
"ghr:ssm_config_path" = local.ssm_config_path
} : {}

}

resource "aws_ssm_parameter" "runner_config_run_as" {
count = var.storage_provider.aws.ssm != null ? 1 : 0
name = "${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.config}/run_as"
type = "String"
value = var.runner.run_as_root ? "root" : var.runner.run_as
tags = local.ssm_parameter_tags
}

resource "aws_ssm_parameter" "runner_enable_cloudwatch" {
count = var.storage_provider.aws.ssm != null ? 1 : 0
name = "${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.config}/enable_cloudwatch"
type = "String"
value = var.config.cloudwatch_agent.enabled
tags = local.ssm_parameter_tags
}

moved {
from = aws_ssm_parameter.runner_config_run_as
to = aws_ssm_parameter.runner_config_run_as[0]
}

moved {
from = aws_ssm_parameter.runner_enable_cloudwatch
to = aws_ssm_parameter.runner_enable_cloudwatch[0]
}
Loading