From 2cb1ed3f24a3b85256648623c3de7e47c8755a4b Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Thu, 24 Sep 2026 02:15:48 +0200 Subject: [PATCH 01/44] fix(terraform): complete SSM provider wiring --- .github/workflows/terraform.yml | 10 +- main.tf | 15 ++- .../aws/ec2/control-plane.tf | 6 +- modules/compute-providers/aws/ec2/logging.tf | 2 +- .../aws/ec2/policies-runner.tf | 5 +- .../aws/ec2/runner-config.tf | 12 ++ .../aws/ec2/runner-instances.tf | 9 +- .../aws/ec2/tests/provider.tftest.hcl | 10 +- .../config.experimental.resolved.tf | 4 +- .../config.experimental.translation.tf | 94 ++++++------- modules/multi-runner/main.tf | 2 +- modules/multi-runner/runners.experimental.tf | 10 +- modules/multi-runner/runners.tf | 2 +- modules/multi-runner/storage-provider.tf | 43 ++++++ .../tests/config-translation.tftest.hcl | 2 +- ...variables.experimental.storage-provider.tf | 2 +- modules/multi-runner/webhook.tf | 6 +- .../webhook/job-retry/iam-policies.tf | 31 ++--- .../webhook/job-retry/job-retry.tf | 4 +- .../job-retry/tests/job-retry.tftest.hcl | 8 ++ .../webhook/job-retry/variables.tf | 5 +- .../webhook/pool/iam-policies.tf | 84 +++++------- .../webhook/pool/pool.tf | 4 +- .../webhook/pool/tests/provider.tftest.hcl | 8 +- .../webhook/pool/variables.tf | 5 +- .../scale-runners/scale-down-iam-policies.tf | 29 ++-- .../webhook/scale-runners/scale-down.tf | 58 ++++---- .../scale-runners/scale-up-iam-policies.tf | 63 +++++---- .../webhook/scale-runners/scale-up.tf | 77 ++++++----- .../webhook/scale-runners/variables.tf | 5 +- .../webhook/variables.tf | 4 +- modules/runner-config/common-config.tf | 39 +----- .../runner-config/orchestration-provider.tf | 6 +- ...keeper.tf => runner-config-housekeeper.tf} | 26 ++-- .../runner-config-housekeeper/housekeeper.tf | 127 ++++++++++++++++++ .../iam-policies.tf | 24 ++-- .../runner-config-housekeeper/outputs.tf | 8 ++ .../tests/housekeeper.tftest.hcl} | 109 +++++++++------ .../variables.tf | 36 +++-- .../versions.tf | 0 .../runner-config/runner-ssm-parameters.tf | 28 ---- .../runner-config/ssm-housekeeper/outputs.tf | 8 -- .../ssm-housekeeper/ssm-housekeeper.tf | 119 ---------------- .../runner-config/storage-provider.aws.ssm.tf | 72 ++++++++++ .../tests/computed-iam-inputs.tftest.hcl | 4 +- modules/runner-config/tests/pool.tftest.hcl | 6 +- modules/runner-config/tests/tags.tftest.hcl | 4 +- modules/runner-config/variables.tf | 32 ++++- modules/webhook/direct/variables.tf | 14 +- modules/webhook/direct/webhook.tf | 21 ++- modules/webhook/eventbridge/dispatcher.tf | 19 ++- modules/webhook/eventbridge/variables.tf | 18 ++- modules/webhook/eventbridge/webhook.tf | 19 ++- modules/webhook/variables.tf | 19 ++- modules/webhook/webhook.tf | 22 +-- 55 files changed, 824 insertions(+), 575 deletions(-) create mode 100644 modules/multi-runner/storage-provider.tf rename modules/runner-config/{ssm-housekeeper.tf => runner-config-housekeeper.tf} (72%) create mode 100644 modules/runner-config/runner-config-housekeeper/housekeeper.tf rename modules/runner-config/{ssm-housekeeper => runner-config-housekeeper}/iam-policies.tf (67%) create mode 100644 modules/runner-config/runner-config-housekeeper/outputs.tf rename modules/runner-config/{ssm-housekeeper/tests/ssm-housekeeper.tftest.hcl => runner-config-housekeeper/tests/housekeeper.tftest.hcl} (63%) rename modules/runner-config/{ssm-housekeeper => runner-config-housekeeper}/variables.tf (80%) rename modules/runner-config/{ssm-housekeeper => runner-config-housekeeper}/versions.tf (100%) delete mode 100644 modules/runner-config/runner-ssm-parameters.tf delete mode 100644 modules/runner-config/ssm-housekeeper/outputs.tf delete mode 100644 modules/runner-config/ssm-housekeeper/ssm-housekeeper.tf create mode 100644 modules/runner-config/storage-provider.aws.ssm.tf diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml index e4bdec3720..43b8c74f6e 100644 --- a/.github/workflows/terraform.yml +++ b/.github/workflows/terraform.yml @@ -29,7 +29,7 @@ env: modules/orchestration-providers/webhook/scale-runners modules/runner-binaries-syncer modules/runner-config - modules/runner-config/ssm-housekeeper + modules/runner-config/runner-config-housekeeper modules/runners modules/runners/job-retry modules/runners/pool @@ -51,8 +51,16 @@ env: multi-runner-v2 external-managed-ssm-secrets TEST_MODULES: | + modules/compute-providers/aws/ec2 + modules/compute-providers/aws/ec2/trust-policy modules/runners modules/multi-runner + modules/orchestration-providers/webhook + modules/orchestration-providers/webhook/job-retry + modules/orchestration-providers/webhook/pool + modules/orchestration-providers/webhook/scale-runners + modules/runner-config + modules/runner-config/runner-config-housekeeper jobs: verify_modules: name: Verify modules (${{ matrix.iac.name }} ${{ matrix.iac.version }}) diff --git a/main.tf b/main.tf index 8f2236f8d1..4c946d6cf7 100644 --- a/main.tf +++ b/main.tf @@ -113,7 +113,6 @@ module "webhook" { storage_provider = { aws = { - kms_key_id = var.kms_key_arn ssm = { paths = { root = local.ssm_root_path @@ -121,6 +120,20 @@ module "webhook" { } } } + direct = { + environment_variables = {} + iam_policy_json = null + } + eventbridge = { + webhook = { + environment_variables = {} + iam_policy_json = null + } + dispatcher = { + environment_variables = {} + iam_policy_json = null + } + } } prefix = var.prefix tags = local.tags diff --git a/modules/compute-providers/aws/ec2/control-plane.tf b/modules/compute-providers/aws/ec2/control-plane.tf index 1b25442032..acc7b7ebad 100644 --- a/modules/compute-providers/aws/ec2/control-plane.tf +++ b/modules/compute-providers/aws/ec2/control-plane.tf @@ -191,7 +191,7 @@ data "aws_iam_policy_document" "service_linked_role" { } locals { - scale_up_environment_variables = { + scale_up_environment_variables = merge({ AMI_ID_SSM_PARAMETER_NAME = local.ami_id_ssm_parameter_name INSTANCE_ALLOCATION_STRATEGY = var.config.instance_allocation_strategy INSTANCE_MAX_SPOT_PRICE = var.config.instance_max_spot_price @@ -203,7 +203,9 @@ locals { ENABLE_ON_DEMAND_FAILOVER_FOR_ERRORS = jsonencode(var.config.on_demand_failover_for_errors) SCALE_ERRORS = jsonencode(var.config.scale_errors) USE_DEDICATED_HOST = var.config.use_dedicated_host - } + }, var.storage_provider.aws.ssm == null ? { + EC2_INSTANCE_ARN_PREFIX = local.ec2_instance_arn_prefix + } : {}) scale_down_environment_variables = {} diff --git a/modules/compute-providers/aws/ec2/logging.tf b/modules/compute-providers/aws/ec2/logging.tf index 2f5c45a10c..1a58d6f080 100644 --- a/modules/compute-providers/aws/ec2/logging.tf +++ b/modules/compute-providers/aws/ec2/logging.tf @@ -56,7 +56,7 @@ locals { resource "aws_ssm_parameter" "cloudwatch_agent_config_runner" { - count = var.config.cloudwatch_agent.enabled ? 1 : 0 + 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", { diff --git a/modules/compute-providers/aws/ec2/policies-runner.tf b/modules/compute-providers/aws/ec2/policies-runner.tf index f416661b71..b1841f11f2 100644 --- a/modules/compute-providers/aws/ec2/policies-runner.tf +++ b/modules/compute-providers/aws/ec2/policies-runner.tf @@ -4,7 +4,8 @@ data "aws_caller_identity" "current" {} locals { 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_parameter_arn_prefix}${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.config}" + ec2_instance_arn_prefix = "arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:instance/" + ssm_config_arn = "${local.ssm_parameter_arn_prefix}${local.ssm_config_path}" cloudwatch_config_arn = "${local.ssm_config_arn}/cloudwatch_agent_config_runner" } @@ -17,7 +18,7 @@ data "aws_iam_policy_document" "ssm_parameters" { "ssm:GetParameter", ] resources = [ - "${local.ssm_parameter_arn_prefix}${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.tokens}/*", + "${local.ssm_parameter_arn_prefix}${local.ssm_root_path}/${var.storage_provider.aws.ssm.paths.tokens}/*", ] condition { diff --git a/modules/compute-providers/aws/ec2/runner-config.tf b/modules/compute-providers/aws/ec2/runner-config.tf index 8685a7822a..ca91996cae 100644 --- a/modules/compute-providers/aws/ec2/runner-config.tf +++ b/modules/compute-providers/aws/ec2/runner-config.tf @@ -1,4 +1,5 @@ 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 @@ -6,8 +7,19 @@ resource "aws_ssm_parameter" "runner_config_run_as" { } 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] +} diff --git a/modules/compute-providers/aws/ec2/runner-instances.tf b/modules/compute-providers/aws/ec2/runner-instances.tf index f4968665d2..6a69f4f838 100644 --- a/modules/compute-providers/aws/ec2/runner-instances.tf +++ b/modules/compute-providers/aws/ec2/runner-instances.tf @@ -29,12 +29,14 @@ locals { var.config.tags, { "ghr:environment" = var.prefix - "ghr:ssm_config_path" = "${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.config}" + "ghr:ssm_config_path" = local.ssm_config_path "ghr:runner_name_prefix" = var.runner.name_prefix }, ) role_path = var.runner.iam.path == null ? "/${var.prefix}/" : var.runner.iam.path + ssm_root_path = var.storage_provider.aws.ssm.paths.root + ssm_config_path = "${local.ssm_root_path}/${var.storage_provider.aws.ssm.paths.config}" instance_profile_path = var.config.instance_profile_path == null ? "/${var.prefix}/" : var.config.instance_profile_path userdata_template = var.config.user_data.template == null ? local.default_userdata_template[var.runner.os] : var.config.user_data.template s3_location_runner_distribution = var.config.binaries_syncer.enabled ? "s3://${try(var.config.binaries_syncer.s3.id, "")}/${try(var.config.binaries_syncer.s3.key, "")}" : "" @@ -90,7 +92,8 @@ locals { hook_job_started = var.runner.hooks.job_started hook_job_completed = var.runner.hooks.job_completed start_runner = templatefile(local.userdata_start_runner[var.runner.os], { - metadata_tags = var.config.metadata_options != null ? var.config.metadata_options.instance_metadata_tags : "enabled" + metadata_tags = var.config.metadata_options != null ? var.config.metadata_options.instance_metadata_tags : "enabled" + enable_cloudwatch_agent = var.config.cloudwatch_agent.enabled }) ghes_url = var.github.enterprise_server.url ghes_ssl_verify = var.github.enterprise_server.ssl_verify @@ -163,7 +166,7 @@ data "aws_ami" "runner" { resource "aws_ssm_parameter" "runner_ami_id" { count = local.ami_id_ssm_module_managed ? 1 : 0 - name = "${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.config}/ami_id" + name = "${local.ssm_config_path}/ami_id" type = "String" data_type = "aws:ec2:image" value = data.aws_ami.runner[0].id diff --git a/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl b/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl index 3a7504e32b..a6a060f1ce 100644 --- a/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl +++ b/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl @@ -375,11 +375,11 @@ run "separates_provider_runner_and_ssm_tags" { assert { condition = ( - aws_ssm_parameter.runner_config_run_as.tags["Name"] == "ssm-name" - && aws_ssm_parameter.runner_config_run_as.tags["Scope"] == "ssm" - && aws_ssm_parameter.runner_config_run_as.tags["SsmOnly"] == "ssm" - && !contains(keys(aws_ssm_parameter.runner_config_run_as.tags), "RunnerOnly") - && !contains(keys(aws_ssm_parameter.runner_config_run_as.tags), "ghr:environment") + aws_ssm_parameter.runner_config_run_as[0].tags["Name"] == "ssm-name" + && aws_ssm_parameter.runner_config_run_as[0].tags["Scope"] == "ssm" + && aws_ssm_parameter.runner_config_run_as[0].tags["SsmOnly"] == "ssm" + && !contains(keys(aws_ssm_parameter.runner_config_run_as[0].tags), "RunnerOnly") + && !contains(keys(aws_ssm_parameter.runner_config_run_as[0].tags), "ghr:environment") ) error_message = "EC2 SSM parameters must merge SSM component tags over provider tags." } diff --git a/modules/multi-runner/config.experimental.resolved.tf b/modules/multi-runner/config.experimental.resolved.tf index 7a2342e3a6..b53ddd1fbd 100644 --- a/modules/multi-runner/config.experimental.resolved.tf +++ b/modules/multi-runner/config.experimental.resolved.tf @@ -106,11 +106,11 @@ locals { ), null) managed_policy_arns = try(coalesce( v.runner.iam.managed_policy_arns, - (v.runner.iam.role != null || local.normalized_config.runner.iam.role != null) ? {} : local.normalized_config.runner.iam.managed_policy_arns, + (try(v.runner.iam.role.arn, null) != null || try(local.normalized_config.runner.iam.role.arn, null) != null) ? {} : local.normalized_config.runner.iam.managed_policy_arns, ), {}) additional_trust_policy_json = try(coalesce( v.runner.iam.additional_trust_policy_json, - (v.runner.iam.role != null || local.normalized_config.runner.iam.role != null) ? null : local.normalized_config.runner.iam.additional_trust_policy_json, + (try(v.runner.iam.role.arn, null) != null || try(local.normalized_config.runner.iam.role.arn, null) != null) ? null : local.normalized_config.runner.iam.additional_trust_policy_json, ), null) path = try(coalesce( v.runner.iam.path, diff --git a/modules/multi-runner/config.experimental.translation.tf b/modules/multi-runner/config.experimental.translation.tf index 02c8880c69..f9595b2d7e 100644 --- a/modules/multi-runner/config.experimental.translation.tf +++ b/modules/multi-runner/config.experimental.translation.tf @@ -1,5 +1,7 @@ # Translate stable v1 inputs into the experimental v2 structure. locals { + stable_empty_tags = zipmap(slice(keys(var.tags), 0, 0), slice(values(var.tags), 0, 0)) + stable_to_v2_tags = var.tags stable_to_v2_roles = { @@ -8,26 +10,26 @@ locals { } stable_to_v2_runner = { - os = null - architecture = null + os = tostring(null) + architecture = tostring(null) disable_default_labels = false - extra_labels = [] + extra_labels = slice(keys(var.tags), 0, 0) group_name = "Default" name_prefix = "" run_as_root = false run_as = "ec2-user" auto_update_disabled = false - tags = {} + tags = local.stable_empty_tags hooks = { job_started = "" job_completed = "" } iam = { - role = null - managed_policy_arns = {} - additional_trust_policy_json = null - path = null - permissions_boundary = null + role = { arn = tostring(null) } + managed_policy_arns = local.stable_empty_tags + additional_trust_policy_json = tostring(null) + path = tostring(null) + permissions_boundary = tostring(null) } } @@ -54,8 +56,8 @@ locals { security_group_ids = var.lambda_security_group_ids tags = var.lambda_tags role = { - path = null - permissions_boundary = null + path = tostring(null) + permissions_boundary = tostring(null) } } @@ -70,14 +72,14 @@ locals { config = "${var.ssm_paths.runners}/config" } kms_key_id = var.kms_key_arn - tags = {} + tags = local.stable_empty_tags parameters = { tags = var.parameter_store_tags } housekeeper = { schedule_expression = var.runners_ssm_housekeeper.schedule_expression state = var.runners_ssm_housekeeper.enabled ? "ENABLED" : "DISABLED" - tags = {} + tags = local.stable_empty_tags lambda = { artifact = { zip = var.lambda_s3_bucket == null ? var.runners_lambda_zip : null @@ -110,8 +112,8 @@ locals { runner = { boot_time_in_minutes = 5 ephemeral = false - jit_config_enabled = null - maximum_count = null + jit_config_enabled = tobool(null) + maximum_count = tonumber(null) } github = { repository_white_list = var.repository_white_list @@ -129,21 +131,21 @@ locals { memory_size = var.scale_up_lambda_memory_size timeout = var.runners_scale_up_lambda_timeout reserved_concurrent_executions = 1 - job_queued_check_enabled = null + job_queued_check_enabled = tobool(null) event_source_mapping = { batch_size = var.lambda_event_source_mapping_batch_size maximum_batching_window_in_seconds = var.lambda_event_source_mapping_maximum_batching_window_in_seconds } - tags = {} + tags = local.stable_empty_tags } down = { memory_size = var.scale_down_lambda_memory_size timeout = var.runners_scale_down_lambda_timeout schedule_expression = "cron(*/5 * * * ? *)" - minimum_running_time_in_minutes = null - idle_config = [] + minimum_running_time_in_minutes = tonumber(null) + idle_config = var.global_config_orchestration_provider.webhook.lambda.scale.down.idle_config idle_confirmation_seconds = 0 - tags = {} + tags = local.stable_empty_tags } } webhook = { @@ -157,16 +159,16 @@ locals { api_gateway_access_log_settings = var.webhook_lambda_apigateway_access_log_settings memory_size = var.webhook_lambda_memory_size timeout = var.webhook_lambda_timeout - tags = {} + tags = local.stable_empty_tags } pool = { memory_size = 512 timeout = var.pool_lambda_timeout reserved_concurrent_executions = var.pool_lambda_reserved_concurrent_executions - config = [] + config = var.global_config_orchestration_provider.webhook.lambda.pool.config include_busy_runners = false - runner_owner = null - tags = {} + runner_owner = tostring(null) + tags = local.stable_empty_tags } } queue = { @@ -175,9 +177,9 @@ locals { visibility_timeout_seconds = var.runners_scale_up_lambda_timeout redrive_build_queue = { enabled = false - maxReceiveCount = null + maxReceiveCount = tonumber(null) } - tags = {} + tags = local.stable_empty_tags encryption = var.queue_encryption } } @@ -189,7 +191,7 @@ locals { retention_in_days = var.logging_retention_in_days kms_key_id = var.logging_kms_key_id class = var.log_class - tags = {} + tags = local.stable_empty_tags } tracing = var.tracing_config metrics = { @@ -210,7 +212,7 @@ locals { } stable_to_v2_compute_provider = { - selections = null + selections = var.global_config_compute_provider.selections aws = { ec2 = { vpc_id = var.vpc_id @@ -224,7 +226,7 @@ locals { instance_profile_path = var.instance_profile_path key_name = var.key_name associate_public_ipv4_address = var.associate_public_ipv4_address - tags = {} + tags = var.tags ami = { housekeeper = { enabled = var.enable_ami_housekeeper @@ -276,15 +278,15 @@ locals { s3 = { encryption = { enabled = var.runner_binaries_s3_sse_configuration != null - bucket_key_enabled = try(var.runner_binaries_s3_sse_configuration.rule.bucket_key_enabled, null) - sse_algorithm = try(var.runner_binaries_s3_sse_configuration.rule.apply_server_side_encryption_by_default.sse_algorithm, "AES256") - kms_master_key_id = try(var.runner_binaries_s3_sse_configuration.rule.apply_server_side_encryption_by_default.kms_master_key_id, null) + bucket_key_enabled = tobool(try(var.runner_binaries_s3_sse_configuration.rule.bucket_key_enabled, null)) + sse_algorithm = tostring(try(var.runner_binaries_s3_sse_configuration.rule.apply_server_side_encryption_by_default.sse_algorithm, "AES256")) + kms_master_key_id = tostring(try(var.runner_binaries_s3_sse_configuration.rule.apply_server_side_encryption_by_default.kms_master_key_id, null)) } tags = var.runner_binaries_s3_tags versioning = var.runner_binaries_s3_versioning logging = { - bucket = null - prefix = null + bucket = tostring(null) + prefix = tostring(null) } } syncer = { @@ -311,7 +313,7 @@ locals { stable_to_v2_multi_runner_config = { for k, v in local.legacy_multi_runner_config : k => { - tags = {} + tags = local.stable_empty_tags runner = { os = v.runner_config.runner_os @@ -323,7 +325,7 @@ locals { run_as_root = v.runner_config.runner_as_root run_as = v.runner_config.runner_run_as auto_update_disabled = v.runner_config.disable_runner_autoupdate - tags = {} + tags = local.stable_empty_tags hooks = { job_started = v.runner_config.runner_hook_job_started job_completed = v.runner_config.runner_hook_job_completed @@ -347,7 +349,7 @@ locals { architecture = null subnet_ids = null security_group_ids = null - tags = {} + tags = local.stable_empty_tags role = { path = null permissions_boundary = null @@ -387,7 +389,7 @@ locals { batch_size = v.runner_config.lambda_event_source_mapping_batch_size maximum_batching_window_in_seconds = v.runner_config.lambda_event_source_mapping_maximum_batching_window_in_seconds } - tags = {} + tags = local.stable_empty_tags } down = { memory_size = null @@ -396,7 +398,7 @@ locals { minimum_running_time_in_minutes = v.runner_config.minimum_running_time_in_minutes idle_config = v.runner_config.idle_config idle_confirmation_seconds = v.runner_config.scale_down_idle_confirmation_seconds - tags = {} + tags = local.stable_empty_tags } } pool = { @@ -406,7 +408,7 @@ locals { config = v.runner_config.pool_config include_busy_runners = false runner_owner = v.runner_config.pool_runner_owner - tags = {} + tags = local.stable_empty_tags } } @@ -415,7 +417,7 @@ locals { job_queue_retention_in_seconds = v.runner_config.job_queue_retention_in_seconds visibility_timeout_seconds = var.runners_scale_up_lambda_timeout redrive_build_queue = v.redrive_build_queue - tags = {} + tags = local.stable_empty_tags } job_retry = { @@ -423,7 +425,7 @@ locals { delay_in_seconds = v.runner_config.job_retry.delay_in_seconds delay_backoff = v.runner_config.job_retry.delay_backoff max_attempts = v.runner_config.job_retry.max_attempts - tags = {} + tags = local.stable_empty_tags lambda = { memory_size = v.runner_config.job_retry.lambda_memory_size reserved_concurrent_executions = 1 @@ -444,14 +446,14 @@ locals { tokens = null config = null } - tags = {} + tags = local.stable_empty_tags parameters = { - tags = {} + tags = local.stable_empty_tags } housekeeper = { schedule_expression = null state = null - tags = {} + tags = local.stable_empty_tags lambda = { artifact = { zip = null @@ -476,7 +478,7 @@ locals { retention_in_days = null kms_key_id = null class = null - tags = {} + tags = local.stable_empty_tags } tracing = { mode = null diff --git a/modules/multi-runner/main.tf b/modules/multi-runner/main.tf index d154bdad7f..0c935cd242 100644 --- a/modules/multi-runner/main.tf +++ b/modules/multi-runner/main.tf @@ -16,7 +16,7 @@ locals { additional_app_parameter_arns = flatten([ for p in module.ssm.additional_app_parameters : concat( [p.id.arn, p.key_base64.arn], - p.installation_id != null ? [p.installation_id.arn] : [] + p.installation_id != null ? [p.installation_id.arn] : [], ) ]) } diff --git a/modules/multi-runner/runners.experimental.tf b/modules/multi-runner/runners.experimental.tf index c94afd2db6..c03ce40faf 100644 --- a/modules/multi-runner/runners.experimental.tf +++ b/modules/multi-runner/runners.experimental.tf @@ -38,7 +38,15 @@ module "runner_configs" { job_retry = each.value.orchestration_provider.webhook.job_retry } } - storage_provider = each.value.storage_provider + storage_provider = merge(each.value.storage_provider, { + aws = { + ssm = each.value.storage_provider.aws.ssm + } + scale_up = local.storage_provider_capabilities.entries[each.key].scale_up + scale_down = local.storage_provider_capabilities.entries[each.key].scale_down + pool = local.storage_provider_capabilities.entries[each.key].pool + job_retry = local.storage_provider_capabilities.entries[each.key].job_retry + }) observability = each.value.observability compute_provider = each.value.compute_provider } diff --git a/modules/multi-runner/runners.tf b/modules/multi-runner/runners.tf index 1311ee9dc9..9b21e3d0fd 100644 --- a/modules/multi-runner/runners.tf +++ b/modules/multi-runner/runners.tf @@ -121,7 +121,7 @@ module "runners" { iam_overrides = { override_instance_profile = each.value.compute_provider.aws.ec2.instance_profile != null instance_profile_name = try(each.value.compute_provider.aws.ec2.instance_profile.name, null) - override_runner_role = each.value.runner.iam.role != null + override_runner_role = try(each.value.runner.iam.role.arn, null) != null runner_role_arn = try(each.value.runner.iam.role.arn, null) } diff --git a/modules/multi-runner/storage-provider.tf b/modules/multi-runner/storage-provider.tf new file mode 100644 index 0000000000..a4e5a54236 --- /dev/null +++ b/modules/multi-runner/storage-provider.tf @@ -0,0 +1,43 @@ +locals { + # Storage-provider capabilities are empty for SSM. The capability boundary + # remains here so a future provider can add environment variables and IAM + # policy fragments without changing the runner wiring. + storage_provider_capabilities = { + webhook = { + direct = { + environment_variables = tomap({}) + iam_policy_json = null + } + eventbridge = { + webhook = { + environment_variables = tomap({}) + iam_policy_json = null + } + dispatcher = { + environment_variables = tomap({}) + iam_policy_json = null + } + } + } + entries = { + for entry_id in keys(local.effective_config.multi_runner_config) : entry_id => { + scale_up = { + environment_variables = tomap({}) + iam_policy_json = null + } + scale_down = { + environment_variables = tomap({}) + iam_policy_json = null + } + pool = { + environment_variables = tomap({}) + iam_policy_json = null + } + job_retry = { + environment_variables = tomap({}) + iam_policy_json = null + } + } + } + } +} diff --git a/modules/multi-runner/tests/config-translation.tftest.hcl b/modules/multi-runner/tests/config-translation.tftest.hcl index f786fa573c..488396f673 100644 --- a/modules/multi-runner/tests/config-translation.tftest.hcl +++ b/modules/multi-runner/tests/config-translation.tftest.hcl @@ -429,7 +429,7 @@ run "empty_v2_map_translates_stable_inputs" { && local.stable_to_v2.github.user_agent == var.user_agent && local.stable_to_v2.lambda.artifact.s3.bucket == var.lambda_s3_bucket && local.stable_to_v2.orchestration_provider.webhook.lambda.scale.up.event_source_mapping.batch_size == var.lambda_event_source_mapping_batch_size - && local.stable_to_v2.orchestration_provider.webhook.lambda.scale.down.idle_config == [] + && length(local.stable_to_v2.orchestration_provider.webhook.lambda.scale.down.idle_config) == 0 && local.stable_to_v2.storage_provider.aws.ssm.parameters.tags.owner == var.parameter_store_tags.owner && local.stable_to_v2.storage_provider.aws.ssm.housekeeper.lambda.memory_size == var.runners_ssm_housekeeper.lambda_memory_size && local.stable_to_v2.compute_provider.aws.ec2.runner_binaries.s3.encryption.sse_algorithm == "aws:kms" diff --git a/modules/multi-runner/variables.experimental.storage-provider.tf b/modules/multi-runner/variables.experimental.storage-provider.tf index a76c07a45f..0adb7236d9 100644 --- a/modules/multi-runner/variables.experimental.storage-provider.tf +++ b/modules/multi-runner/variables.experimental.storage-provider.tf @@ -61,7 +61,7 @@ variable "global_config_storage_provider" { dryRun = optional(bool, false) }), {}) }), {}) - }), {}) + }), null) }), {}) }) default = {} diff --git a/modules/multi-runner/webhook.tf b/modules/multi-runner/webhook.tf index 250af4a214..e64ebf8913 100644 --- a/modules/multi-runner/webhook.tf +++ b/modules/multi-runner/webhook.tf @@ -20,16 +20,14 @@ locals { } } - webhook_storage_kms_key_arn = local.effective_config.storage_provider.aws.ssm.kms_key_id } module "webhook" { source = "../webhook" prefix = var.prefix tags = local.tags - storage_provider = { + storage_provider = merge(local.storage_provider_capabilities.webhook, { aws = { - kms_key_id = local.webhook_storage_kms_key_arn ssm = { paths = { root = local.ssm_root_path @@ -37,7 +35,7 @@ module "webhook" { } } } - } + }) eventbridge = { enable = local.effective_config.orchestration_provider.webhook.eventbridge.enabled accept_events = local.effective_config.orchestration_provider.webhook.eventbridge.accept_events diff --git a/modules/orchestration-providers/webhook/job-retry/iam-policies.tf b/modules/orchestration-providers/webhook/job-retry/iam-policies.tf index 57bf5dbec2..6e27720961 100644 --- a/modules/orchestration-providers/webhook/job-retry/iam-policies.tf +++ b/modules/orchestration-providers/webhook/job-retry/iam-policies.tf @@ -54,23 +54,18 @@ data "aws_iam_policy_document" "lambda_xray" { data "aws_iam_policy_document" "job_retry" { source_policy_documents = compact([var.storage_provider.iam_policy_json]) - statement { - sid = "WebhookJobRetryReadGitHubAppParameters" - effect = "Allow" - - actions = [ - "ssm:GetParameter", - "ssm:GetParameters", - ] - - resources = concat( - [ - var.config.github.app_parameters.id.arn, - var.config.github.app_parameters.key_base64.arn, - ], - var.config.github.app_parameters.additional_app_parameter_arns, - var.config.github.app_parameters.additional_apps_manifest != null ? [var.config.github.app_parameters.additional_apps_manifest.arn] : [], - ) + dynamic "statement" { + for_each = var.storage_provider.aws.ssm != null ? [true] : [] + content { + sid = "WebhookJobRetryReadGitHubAppParameters" + effect = "Allow" + actions = ["ssm:GetParameter", "ssm:GetParameters"] + resources = concat( + [var.config.github.app_parameters.id.arn, var.config.github.app_parameters.key_base64.arn], + var.config.github.app_parameters.additional_app_parameter_arns, + var.config.github.app_parameters.additional_apps_manifest != null ? [var.config.github.app_parameters.additional_apps_manifest.arn] : [], + ) + } } statement { @@ -99,7 +94,7 @@ data "aws_iam_policy_document" "job_retry" { } dynamic "statement" { - for_each = var.storage_provider.aws.ssm.kms_key_id != null ? [var.storage_provider.aws.ssm.kms_key_id] : [] + for_each = var.storage_provider.aws.ssm != null && var.storage_provider.aws.ssm.kms_key_id != null ? [var.storage_provider.aws.ssm.kms_key_id] : [] iterator = kms_key content { diff --git a/modules/orchestration-providers/webhook/job-retry/job-retry.tf b/modules/orchestration-providers/webhook/job-retry/job-retry.tf index 725c53d3d8..88a6e68bfe 100644 --- a/modules/orchestration-providers/webhook/job-retry/job-retry.tf +++ b/modules/orchestration-providers/webhook/job-retry/job-retry.tf @@ -29,11 +29,11 @@ locals { RUNNER_NAME_PREFIX = var.config.runner.name_prefix } - ssm_environment_variables = { + ssm_environment_variables = var.storage_provider.aws.ssm != null ? { PARAMETER_GITHUB_APP_ID_NAME = var.config.github.app_parameters.id.name PARAMETER_GITHUB_APP_KEY_BASE64_NAME = var.config.github.app_parameters.key_base64.name PARAMETER_GITHUB_APPS_MANIFEST_NAME = var.config.github.app_parameters.additional_apps_manifest != null ? var.config.github.app_parameters.additional_apps_manifest.name : "" - } + } : {} environment_variables = merge( local.lambda_environment_variables, diff --git a/modules/orchestration-providers/webhook/job-retry/tests/job-retry.tftest.hcl b/modules/orchestration-providers/webhook/job-retry/tests/job-retry.tftest.hcl index 0e797b9a04..f54d0b4387 100644 --- a/modules/orchestration-providers/webhook/job-retry/tests/job-retry.tftest.hcl +++ b/modules/orchestration-providers/webhook/job-retry/tests/job-retry.tftest.hcl @@ -230,6 +230,14 @@ run "does_not_enable_partial_vpc_configuration" { command = plan variables { + storage_provider = merge(var.storage_provider, { + aws = merge(var.storage_provider.aws, { + ssm = merge(var.storage_provider.aws.ssm, { + kms_key_id = null + }) + }) + }) + config = { prefix = "job-retry-test" aws_partition = "aws" diff --git a/modules/orchestration-providers/webhook/job-retry/variables.tf b/modules/orchestration-providers/webhook/job-retry/variables.tf index 863324152b..795e9183b6 100644 --- a/modules/orchestration-providers/webhook/job-retry/variables.tf +++ b/modules/orchestration-providers/webhook/job-retry/variables.tf @@ -152,12 +152,13 @@ variable "storage_provider" { description = "Resolved storage-provider configuration and capability used by the job-retry Lambda." type = object({ aws = object({ - ssm = object({ + ssm = optional(object({ kms_key_id = optional(string, null) - }) + }), null) }) environment_variables = optional(map(string), {}) iam_policy_json = optional(string, null) }) nullable = false + } diff --git a/modules/orchestration-providers/webhook/pool/iam-policies.tf b/modules/orchestration-providers/webhook/pool/iam-policies.tf index ce4f5a4b11..3a3662af3e 100644 --- a/modules/orchestration-providers/webhook/pool/iam-policies.tf +++ b/modules/orchestration-providers/webhook/pool/iam-policies.tf @@ -1,59 +1,49 @@ # IAM policies attached to the pool Lambda role. data "aws_iam_policy_document" "pool_common" { - statement { - sid = "WebhookPoolWriteRuntimeParameters" - effect = "Allow" - - actions = [ - "ssm:AddTagsToResource", - "ssm:PutParameter", - ] - - resources = [ - var.storage_provider.aws.ssm.token_path_arn, - "${var.storage_provider.aws.ssm.token_path_arn}/*", - var.storage_provider.aws.ssm.config_path_arn, - "${var.storage_provider.aws.ssm.config_path_arn}/*", - ] + dynamic "statement" { + for_each = var.storage_provider.aws.ssm != null ? [true] : [] + content { + sid = "WebhookPoolWriteRuntimeParameters" + effect = "Allow" + actions = ["ssm:AddTagsToResource", "ssm:PutParameter"] + resources = [ + var.storage_provider.aws.ssm.token_path_arn, + "${var.storage_provider.aws.ssm.token_path_arn}/*", + var.storage_provider.aws.ssm.config_path_arn, + "${var.storage_provider.aws.ssm.config_path_arn}/*", + ] + } } - statement { - sid = "WebhookPoolReadRunnerConfigParameters" - effect = "Allow" - - actions = [ - "ssm:GetParameter", - "ssm:GetParameters", - "ssm:GetParametersByPath", - ] - - resources = [ - var.storage_provider.aws.ssm.config_path_arn, - "${var.storage_provider.aws.ssm.config_path_arn}/*", - ] + dynamic "statement" { + for_each = var.storage_provider.aws.ssm != null ? [true] : [] + content { + sid = "WebhookPoolReadRunnerConfigParameters" + effect = "Allow" + actions = ["ssm:GetParameter", "ssm:GetParameters", "ssm:GetParametersByPath"] + resources = [ + var.storage_provider.aws.ssm.config_path_arn, + "${var.storage_provider.aws.ssm.config_path_arn}/*", + ] + } } - statement { - sid = "WebhookPoolReadGitHubAppParameters" - effect = "Allow" - - actions = [ - "ssm:GetParameter", - "ssm:GetParameters", - ] - - resources = concat( - [ - var.config.github_app_parameters.id.arn, - var.config.github_app_parameters.key_base64.arn, - ], - var.config.github_app_parameters.additional_app_parameter_arns, - var.config.github_app_parameters.additional_apps_manifest != null ? [var.config.github_app_parameters.additional_apps_manifest.arn] : [], - ) + dynamic "statement" { + for_each = var.storage_provider.aws.ssm != null ? [true] : [] + content { + sid = "WebhookPoolReadGitHubAppParameters" + effect = "Allow" + actions = ["ssm:GetParameter", "ssm:GetParameters"] + resources = concat( + [var.config.github_app_parameters.id.arn, var.config.github_app_parameters.key_base64.arn], + var.config.github_app_parameters.additional_app_parameter_arns, + var.config.github_app_parameters.additional_apps_manifest != null ? [var.config.github_app_parameters.additional_apps_manifest.arn] : [], + ) + } } dynamic "statement" { - for_each = var.storage_provider.aws.ssm.kms_key_id != null ? [var.storage_provider.aws.ssm.kms_key_id] : [] + for_each = var.storage_provider.aws.ssm != null && var.storage_provider.aws.ssm.kms_key_id != null ? [var.storage_provider.aws.ssm.kms_key_id] : [] iterator = kms_key content { diff --git a/modules/orchestration-providers/webhook/pool/pool.tf b/modules/orchestration-providers/webhook/pool/pool.tf index 1ff7249fd7..7ced65e194 100644 --- a/modules/orchestration-providers/webhook/pool/pool.tf +++ b/modules/orchestration-providers/webhook/pool/pool.tf @@ -29,14 +29,14 @@ locals { INCLUDE_BUSY_RUNNERS = var.config.include_busy_runners } - ssm_environment_variables = { + ssm_environment_variables = var.storage_provider.aws.ssm != null ? { PARAMETER_GITHUB_APP_ID_NAME = var.config.github_app_parameters.id.name PARAMETER_GITHUB_APP_KEY_BASE64_NAME = var.config.github_app_parameters.key_base64.name PARAMETER_GITHUB_APPS_MANIFEST_NAME = var.config.github_app_parameters.additional_apps_manifest != null ? var.config.github_app_parameters.additional_apps_manifest.name : "" SSM_TOKEN_PATH = var.storage_provider.aws.ssm.token_path SSM_CONFIG_PATH = var.storage_provider.aws.ssm.config_path SSM_PARAMETER_STORE_TAGS = var.storage_provider.aws.ssm.parameter_store_tags - } + } : {} } resource "aws_lambda_function" "pool" { diff --git a/modules/orchestration-providers/webhook/pool/tests/provider.tftest.hcl b/modules/orchestration-providers/webhook/pool/tests/provider.tftest.hcl index 25fa6c8a14..84c91cfef9 100644 --- a/modules/orchestration-providers/webhook/pool/tests/provider.tftest.hcl +++ b/modules/orchestration-providers/webhook/pool/tests/provider.tftest.hcl @@ -228,8 +228,12 @@ run "omits_optional_kms_statement" { command = plan variables { - config = merge(var.config, { - kms_key_id = null + storage_provider = merge(var.storage_provider, { + aws = merge(var.storage_provider.aws, { + ssm = merge(var.storage_provider.aws.ssm, { + kms_key_id = null + }) + }) }) } diff --git a/modules/orchestration-providers/webhook/pool/variables.tf b/modules/orchestration-providers/webhook/pool/variables.tf index 8ccb8fa16d..af63d04e03 100644 --- a/modules/orchestration-providers/webhook/pool/variables.tf +++ b/modules/orchestration-providers/webhook/pool/variables.tf @@ -135,19 +135,20 @@ variable "storage_provider" { description = "Resolved storage-provider configuration and capability used by the pool Lambda." type = object({ aws = object({ - ssm = object({ + ssm = optional(object({ token_path = string token_path_arn = string config_path = string config_path_arn = string kms_key_id = optional(string, null) parameter_store_tags = string - }) + }), null) }) environment_variables = optional(map(string), {}) iam_policy_json = optional(string, null) }) nullable = false + } variable "aws_partition" { diff --git a/modules/orchestration-providers/webhook/scale-runners/scale-down-iam-policies.tf b/modules/orchestration-providers/webhook/scale-runners/scale-down-iam-policies.tf index a9b277d69f..7e7d0c24cf 100644 --- a/modules/orchestration-providers/webhook/scale-runners/scale-down-iam-policies.tf +++ b/modules/orchestration-providers/webhook/scale-runners/scale-down-iam-policies.tf @@ -1,23 +1,20 @@ data "aws_iam_policy_document" "scale_down_common" { - statement { - sid = "WebhookScaleDownReadGitHubAppParameters" - effect = "Allow" - actions = [ - "ssm:GetParameter", - "ssm:GetParameters", - ] - resources = concat( - [ - var.config.github.app_parameters.id.arn, - var.config.github.app_parameters.key_base64.arn, - ], - var.config.github.app_parameters.additional_app_parameter_arns, - var.config.github.app_parameters.additional_apps_manifest != null ? [var.config.github.app_parameters.additional_apps_manifest.arn] : [], - ) + dynamic "statement" { + for_each = var.storage_provider.aws.ssm != null ? [true] : [] + content { + sid = "WebhookScaleDownReadGitHubAppParameters" + effect = "Allow" + actions = ["ssm:GetParameter", "ssm:GetParameters"] + resources = concat( + [var.config.github.app_parameters.id.arn, var.config.github.app_parameters.key_base64.arn], + var.config.github.app_parameters.additional_app_parameter_arns, + var.config.github.app_parameters.additional_apps_manifest != null ? [var.config.github.app_parameters.additional_apps_manifest.arn] : [], + ) + } } dynamic "statement" { - for_each = var.storage_provider.aws.ssm.kms_key_id != null ? [var.storage_provider.aws.ssm.kms_key_id] : [] + for_each = var.storage_provider.aws.ssm != null && var.storage_provider.aws.ssm.kms_key_id != null ? [var.storage_provider.aws.ssm.kms_key_id] : [] iterator = kms_key content { diff --git a/modules/orchestration-providers/webhook/scale-runners/scale-down.tf b/modules/orchestration-providers/webhook/scale-runners/scale-down.tf index efe57570f6..660d10c248 100644 --- a/modules/orchestration-providers/webhook/scale-runners/scale-down.tf +++ b/modules/orchestration-providers/webhook/scale-runners/scale-down.tf @@ -1,3 +1,32 @@ +locals { + scale_down_common_environment_variables = { + ENVIRONMENT = var.config.prefix + ENABLE_METRIC_GITHUB_APP_RATE_LIMIT = var.config.observability.metrics.enabled && var.config.observability.metrics.metric.github_app_rate_limit.enabled + GHES_URL = var.config.github.enterprise_server.url + USER_AGENT = var.config.github.user_agent + LOG_LEVEL = upper(var.config.observability.logs.level) + MINIMUM_RUNNING_TIME_IN_MINUTES = coalesce(var.config.scale_down.minimum_running_time_in_minutes, local.min_runtime_defaults[var.config.runner.os]) + SCALE_DOWN_IDLE_CONFIRMATION_SECONDS = var.config.scale_down.idle_confirmation_seconds + NODE_TLS_REJECT_UNAUTHORIZED = var.config.github.enterprise_server.url != null && !var.config.github.enterprise_server.ssl_verify ? 0 : 1 + POWERTOOLS_LOGGER_LOG_EVENT = var.config.observability.logs.level == "debug" ? "true" : "false" + SCALE_DOWN_CONFIG = jsonencode(var.config.scale_down.idle_config) + POWERTOOLS_SERVICE_NAME = "${var.config.prefix}-scale-down" + POWERTOOLS_METRICS_NAMESPACE = var.config.observability.metrics.namespace + POWERTOOLS_TRACE_ENABLED = var.config.observability.tracing.mode != null + POWERTOOLS_TRACER_CAPTURE_HTTPS_REQUESTS = var.config.observability.tracing.capture_http_requests + POWERTOOLS_TRACER_CAPTURE_ERROR = var.config.observability.tracing.capture_error + COMPUTE_PROVIDER_TYPE = var.runner_provider.type + RUNNER_BOOT_TIME_IN_MINUTES = var.config.runner.boot_time_in_minutes + } + + scale_down_ssm_environment_variables = var.storage_provider.aws.ssm != null ? { + PARAMETER_GITHUB_APP_ID_NAME = var.config.github.app_parameters.id.name + PARAMETER_GITHUB_APP_KEY_BASE64_NAME = var.config.github.app_parameters.key_base64.name + PARAMETER_GITHUB_APPS_MANIFEST_NAME = var.config.github.app_parameters.additional_apps_manifest != null ? var.config.github.app_parameters.additional_apps_manifest.name : "" + SSM_TOKEN_PATH = var.storage_provider.aws.ssm.token_path + } : {} +} + resource "aws_lambda_function" "scale_down" { s3_bucket = var.config.lambda.artifact.s3.bucket s3_key = var.config.lambda.artifact.s3.key @@ -14,29 +43,12 @@ resource "aws_lambda_function" "scale_down" { architectures = [var.config.lambda.architecture] environment { - variables = merge(var.runner_provider.scale_down.environment_variables, { - ENVIRONMENT = var.config.prefix - ENABLE_METRIC_GITHUB_APP_RATE_LIMIT = var.config.observability.metrics.enabled && var.config.observability.metrics.metric.github_app_rate_limit.enabled - GHES_URL = var.config.github.enterprise_server.url - USER_AGENT = var.config.github.user_agent - LOG_LEVEL = upper(var.config.observability.logs.level) - MINIMUM_RUNNING_TIME_IN_MINUTES = coalesce(var.config.scale_down.minimum_running_time_in_minutes, local.min_runtime_defaults[var.config.runner.os]) - SCALE_DOWN_IDLE_CONFIRMATION_SECONDS = var.config.scale_down.idle_confirmation_seconds - NODE_TLS_REJECT_UNAUTHORIZED = var.config.github.enterprise_server.url != null && !var.config.github.enterprise_server.ssl_verify ? 0 : 1 - POWERTOOLS_LOGGER_LOG_EVENT = var.config.observability.logs.level == "debug" ? "true" : "false" - SCALE_DOWN_CONFIG = jsonencode(var.config.scale_down.idle_config) - POWERTOOLS_SERVICE_NAME = "${var.config.prefix}-scale-down" - POWERTOOLS_METRICS_NAMESPACE = var.config.observability.metrics.namespace - POWERTOOLS_TRACE_ENABLED = var.config.observability.tracing.mode != null - POWERTOOLS_TRACER_CAPTURE_HTTPS_REQUESTS = var.config.observability.tracing.capture_http_requests - POWERTOOLS_TRACER_CAPTURE_ERROR = var.config.observability.tracing.capture_error - COMPUTE_PROVIDER_TYPE = var.runner_provider.type - RUNNER_BOOT_TIME_IN_MINUTES = var.config.runner.boot_time_in_minutes - }, { - PARAMETER_GITHUB_APP_ID_NAME = var.config.github.app_parameters.id.name - PARAMETER_GITHUB_APP_KEY_BASE64_NAME = var.config.github.app_parameters.key_base64.name - PARAMETER_GITHUB_APPS_MANIFEST_NAME = var.config.github.app_parameters.additional_apps_manifest != null ? var.config.github.app_parameters.additional_apps_manifest.name : "" - }, var.storage_provider.scale_down.environment_variables) + variables = merge( + var.runner_provider.scale_down.environment_variables, + local.scale_down_common_environment_variables, + local.scale_down_ssm_environment_variables, + var.storage_provider.scale_down.environment_variables, + ) } dynamic "vpc_config" { diff --git a/modules/orchestration-providers/webhook/scale-runners/scale-up-iam-policies.tf b/modules/orchestration-providers/webhook/scale-runners/scale-up-iam-policies.tf index eb74cc4da3..e636a8c671 100644 --- a/modules/orchestration-providers/webhook/scale-runners/scale-up-iam-policies.tf +++ b/modules/orchestration-providers/webhook/scale-runners/scale-up-iam-policies.tf @@ -1,38 +1,35 @@ data "aws_iam_policy_document" "scale_up_common" { - statement { - sid = "WebhookScaleUpWriteRuntimeParameters" - effect = "Allow" - actions = [ - "ssm:PutParameter", - "ssm:AddTagsToResource", - ] - resources = [ - var.storage_provider.aws.ssm.token_path_arn, - "${var.storage_provider.aws.ssm.token_path_arn}/*", - var.storage_provider.aws.ssm.config_path_arn, - "${var.storage_provider.aws.ssm.config_path_arn}/*", - ] - } - - statement { - sid = "WebhookScaleUpReadGitHubAppAndRunnerConfigParameters" - effect = "Allow" - actions = [ - "ssm:GetParameter", - "ssm:GetParameters", - ] - resources = concat( - [ - var.config.github.app_parameters.id.arn, - var.config.github.app_parameters.key_base64.arn, - ], - var.config.github.app_parameters.additional_app_parameter_arns, - var.config.github.app_parameters.additional_apps_manifest != null ? [var.config.github.app_parameters.additional_apps_manifest.arn] : [], - [ + dynamic "statement" { + for_each = var.storage_provider.aws.ssm != null ? [true] : [] + content { + sid = "WebhookScaleUpWriteRuntimeParameters" + effect = "Allow" + actions = ["ssm:PutParameter", "ssm:AddTagsToResource"] + resources = [ + var.storage_provider.aws.ssm.token_path_arn, + "${var.storage_provider.aws.ssm.token_path_arn}/*", var.storage_provider.aws.ssm.config_path_arn, "${var.storage_provider.aws.ssm.config_path_arn}/*", - ], - ) + ] + } + } + + dynamic "statement" { + for_each = var.storage_provider.aws.ssm != null ? [true] : [] + content { + sid = "WebhookScaleUpReadGitHubAppAndRunnerConfigParameters" + effect = "Allow" + actions = ["ssm:GetParameter", "ssm:GetParameters"] + resources = concat( + [var.config.github.app_parameters.id.arn, var.config.github.app_parameters.key_base64.arn], + var.config.github.app_parameters.additional_app_parameter_arns, + var.config.github.app_parameters.additional_apps_manifest != null ? [var.config.github.app_parameters.additional_apps_manifest.arn] : [], + [ + var.storage_provider.aws.ssm.config_path_arn, + "${var.storage_provider.aws.ssm.config_path_arn}/*", + ], + ) + } } @@ -49,7 +46,7 @@ data "aws_iam_policy_document" "scale_up_common" { } dynamic "statement" { - for_each = var.storage_provider.aws.ssm.kms_key_id != null ? [var.storage_provider.aws.ssm.kms_key_id] : [] + for_each = var.storage_provider.aws.ssm != null && var.storage_provider.aws.ssm.kms_key_id != null ? [var.storage_provider.aws.ssm.kms_key_id] : [] iterator = kms_key content { diff --git a/modules/orchestration-providers/webhook/scale-runners/scale-up.tf b/modules/orchestration-providers/webhook/scale-runners/scale-up.tf index 3b66812f83..d298041606 100644 --- a/modules/orchestration-providers/webhook/scale-runners/scale-up.tf +++ b/modules/orchestration-providers/webhook/scale-runners/scale-up.tf @@ -1,3 +1,41 @@ +locals { + scale_up_common_environment_variables = { + DISABLE_RUNNER_AUTOUPDATE = var.config.runner.auto_update_disabled + ENABLE_EPHEMERAL_RUNNERS = var.config.runner.ephemeral + ENABLE_JIT_CONFIG = var.config.runner.jit_config_enabled + ENABLE_JOB_QUEUED_CHECK = var.config.scale_up.job_queued_check_enabled + ENABLE_METRIC_GITHUB_APP_RATE_LIMIT = var.config.observability.metrics.enabled && var.config.observability.metrics.metric.github_app_rate_limit.enabled + ENABLE_ORGANIZATION_RUNNERS = var.config.github.organization_runners + ENVIRONMENT = var.config.prefix + GHES_URL = var.config.github.enterprise_server.url + USER_AGENT = var.config.github.user_agent + LOG_LEVEL = upper(var.config.observability.logs.level) + MINIMUM_RUNNING_TIME_IN_MINUTES = coalesce(var.config.scale_down.minimum_running_time_in_minutes, local.min_runtime_defaults[var.config.runner.os]) + NODE_TLS_REJECT_UNAUTHORIZED = var.config.github.enterprise_server.url != null && !var.config.github.enterprise_server.ssl_verify ? 0 : 1 + POWERTOOLS_LOGGER_LOG_EVENT = var.config.observability.logs.level == "debug" ? "true" : "false" + POWERTOOLS_METRICS_NAMESPACE = var.config.observability.metrics.namespace + POWERTOOLS_TRACE_ENABLED = var.config.observability.tracing.mode != null + POWERTOOLS_TRACER_CAPTURE_HTTPS_REQUESTS = var.config.observability.tracing.capture_http_requests + POWERTOOLS_TRACER_CAPTURE_ERROR = var.config.observability.tracing.capture_error + RUNNER_LABELS = lower(join(",", var.config.runner.labels)) + RUNNER_GROUP_NAME = var.config.runner.group_name + RUNNER_NAME_PREFIX = var.config.runner.name_prefix + COMPUTE_PROVIDER_TYPE = var.runner_provider.type + RUNNERS_MAXIMUM_COUNT = var.config.runner.maximum_count + POWERTOOLS_SERVICE_NAME = "${var.config.prefix}-scale-up" + JOB_RETRY_CONFIG = jsonencode(local.job_retry_config) + } + + scale_up_ssm_environment_variables = var.storage_provider.aws.ssm != null ? { + PARAMETER_GITHUB_APP_ID_NAME = var.config.github.app_parameters.id.name + PARAMETER_GITHUB_APP_KEY_BASE64_NAME = var.config.github.app_parameters.key_base64.name + PARAMETER_GITHUB_APPS_MANIFEST_NAME = var.config.github.app_parameters.additional_apps_manifest != null ? var.config.github.app_parameters.additional_apps_manifest.name : "" + SSM_TOKEN_PATH = var.storage_provider.aws.ssm.token_path + SSM_CONFIG_PATH = var.storage_provider.aws.ssm.config_path + SSM_PARAMETER_STORE_TAGS = var.storage_provider.aws.ssm.parameter_store_tags + } : {} +} + resource "aws_lambda_function" "scale_up" { s3_bucket = var.config.lambda.artifact.s3.bucket s3_key = var.config.lambda.artifact.s3.key @@ -15,39 +53,12 @@ resource "aws_lambda_function" "scale_up" { architectures = [var.config.lambda.architecture] environment { - variables = merge(var.runner_provider.scale_up.environment_variables, { - DISABLE_RUNNER_AUTOUPDATE = var.config.runner.auto_update_disabled - ENABLE_EPHEMERAL_RUNNERS = var.config.runner.ephemeral - ENABLE_JIT_CONFIG = var.config.runner.jit_config_enabled - ENABLE_JOB_QUEUED_CHECK = var.config.scale_up.job_queued_check_enabled - ENABLE_METRIC_GITHUB_APP_RATE_LIMIT = var.config.observability.metrics.enabled && var.config.observability.metrics.metric.github_app_rate_limit.enabled - ENABLE_ORGANIZATION_RUNNERS = var.config.github.organization_runners - ENVIRONMENT = var.config.prefix - GHES_URL = var.config.github.enterprise_server.url - USER_AGENT = var.config.github.user_agent - LOG_LEVEL = upper(var.config.observability.logs.level) - MINIMUM_RUNNING_TIME_IN_MINUTES = coalesce(var.config.scale_down.minimum_running_time_in_minutes, local.min_runtime_defaults[var.config.runner.os]) - NODE_TLS_REJECT_UNAUTHORIZED = var.config.github.enterprise_server.url != null && !var.config.github.enterprise_server.ssl_verify ? 0 : 1 - POWERTOOLS_LOGGER_LOG_EVENT = var.config.observability.logs.level == "debug" ? "true" : "false" - POWERTOOLS_METRICS_NAMESPACE = var.config.observability.metrics.namespace - POWERTOOLS_TRACE_ENABLED = var.config.observability.tracing.mode != null - POWERTOOLS_TRACER_CAPTURE_HTTPS_REQUESTS = var.config.observability.tracing.capture_http_requests - POWERTOOLS_TRACER_CAPTURE_ERROR = var.config.observability.tracing.capture_error - RUNNER_LABELS = lower(join(",", var.config.runner.labels)) - RUNNER_GROUP_NAME = var.config.runner.group_name - RUNNER_NAME_PREFIX = var.config.runner.name_prefix - COMPUTE_PROVIDER_TYPE = var.runner_provider.type - RUNNERS_MAXIMUM_COUNT = var.config.runner.maximum_count - POWERTOOLS_SERVICE_NAME = "${var.config.prefix}-scale-up" - JOB_RETRY_CONFIG = jsonencode(local.job_retry_config) - }, { - PARAMETER_GITHUB_APP_ID_NAME = var.config.github.app_parameters.id.name - PARAMETER_GITHUB_APP_KEY_BASE64_NAME = var.config.github.app_parameters.key_base64.name - PARAMETER_GITHUB_APPS_MANIFEST_NAME = var.config.github.app_parameters.additional_apps_manifest != null ? var.config.github.app_parameters.additional_apps_manifest.name : "" - SSM_TOKEN_PATH = var.storage_provider.aws.ssm.token_path - SSM_CONFIG_PATH = var.storage_provider.aws.ssm.config_path - SSM_PARAMETER_STORE_TAGS = var.storage_provider.aws.ssm.parameter_store_tags - }, var.storage_provider.scale_up.environment_variables) + variables = merge( + var.runner_provider.scale_up.environment_variables, + local.scale_up_common_environment_variables, + local.scale_up_ssm_environment_variables, + var.storage_provider.scale_up.environment_variables, + ) } dynamic "vpc_config" { diff --git a/modules/orchestration-providers/webhook/scale-runners/variables.tf b/modules/orchestration-providers/webhook/scale-runners/variables.tf index 982fb4e844..21cb20b7e2 100644 --- a/modules/orchestration-providers/webhook/scale-runners/variables.tf +++ b/modules/orchestration-providers/webhook/scale-runners/variables.tf @@ -235,14 +235,14 @@ variable "storage_provider" { description = "Resolved storage-provider configuration and capabilities for scale-up and scale-down." type = object({ aws = object({ - ssm = object({ + ssm = optional(object({ token_path = string token_path_arn = string config_path = string config_path_arn = string parameter_store_tags = string kms_key_id = optional(string, null) - }) + }), null) }) scale_up = optional(object({ environment_variables = map(string) @@ -260,4 +260,5 @@ variable "storage_provider" { }) }) nullable = false + } diff --git a/modules/orchestration-providers/webhook/variables.tf b/modules/orchestration-providers/webhook/variables.tf index e74602fd52..fec47a37ca 100644 --- a/modules/orchestration-providers/webhook/variables.tf +++ b/modules/orchestration-providers/webhook/variables.tf @@ -223,14 +223,14 @@ variable "storage_provider" { EOT type = object({ aws = object({ - ssm = object({ + ssm = optional(object({ token_path = string token_path_arn = string config_path = string config_path_arn = string kms_key_id = optional(string, null) parameter_store_tags = string - }) + }), null) }) scale_up = optional(object({ environment_variables = map(string) diff --git a/modules/runner-config/common-config.tf b/modules/runner-config/common-config.tf index 2f842ab9a3..09a0db9a6c 100644 --- a/modules/runner-config/common-config.tf +++ b/modules/runner-config/common-config.tf @@ -4,9 +4,9 @@ locals { { "Name" = format("%s-action-runner", var.prefix) }, - { - "ghr:ssm_config_path" = "${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.config}" - }, + var.storage_provider.aws.ssm != null ? { + "ghr:ssm_config_path" = local.ssm_config_path + } : {}, var.tags, ) @@ -14,40 +14,7 @@ locals { lambda_tags = merge(local.common_tags, var.lambda.tags) observability_log_tags = merge(local.common_tags, var.observability.logs.tags) - ssm_tags = merge(local.common_tags, var.storage_provider.aws.ssm.tags) - ssm_parameter_tags = merge(local.ssm_tags, var.storage_provider.aws.ssm.parameters.tags) - ssm_housekeeper_tags = merge(local.ssm_tags, var.storage_provider.aws.ssm.housekeeper.tags) - ssm_housekeeper_lambda_tags = merge(local.lambda_tags, var.storage_provider.aws.ssm.tags, var.storage_provider.aws.ssm.housekeeper.tags) - ssm_housekeeper_log_tags = merge(local.observability_log_tags, var.storage_provider.aws.ssm.tags, var.storage_provider.aws.ssm.housekeeper.tags) - lambda_role_path = var.lambda.role.path == null ? "/${var.prefix}/" : var.lambda.role.path runner_role_path = var.runner.iam.path == null ? "/${var.prefix}/" : var.runner.iam.path packaged_runners_lambda_zip = "${path.module}/../../lambdas/functions/control-plane/runners.zip" - ssm_housekeeper_artifact_s3_selected = ( - var.storage_provider.aws.ssm.housekeeper.lambda.artifact.s3 != null - ) - ssm_housekeeper_artifact = { - zip = local.ssm_housekeeper_artifact_s3_selected ? null : coalesce( - var.storage_provider.aws.ssm.housekeeper.lambda.artifact.zip, - local.packaged_runners_lambda_zip, - ) - s3 = { - bucket = local.ssm_housekeeper_artifact_s3_selected ? var.lambda.artifact.s3.bucket : null - key = try(var.storage_provider.aws.ssm.housekeeper.lambda.artifact.s3.key, null) - object_version = try(var.storage_provider.aws.ssm.housekeeper.lambda.artifact.s3.object_version, null) - } - } - kms_key_id = var.storage_provider.aws.ssm.kms_key_id - token_path = "${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.tokens}" - arn_ssm_parameters_path_tokens = "arn:${var.aws_partition}:ssm:${var.aws_region}:${data.aws_caller_identity.current.account_id}:parameter${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.tokens}" - arn_ssm_parameters_path_config = "arn:${var.aws_partition}:ssm:${var.aws_region}:${data.aws_caller_identity.current.account_id}:parameter${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.config}" - - parameter_store_tags = jsonencode([ - for key, value in local.ssm_parameter_tags : { - Key = key - Value = value - } - ]) } - -data "aws_caller_identity" "current" {} diff --git a/modules/runner-config/orchestration-provider.tf b/modules/runner-config/orchestration-provider.tf index 447ebea4a3..bb1d96c0c2 100644 --- a/modules/runner-config/orchestration-provider.tf +++ b/modules/runner-config/orchestration-provider.tf @@ -41,7 +41,7 @@ module "orchestration_webhook" { } storage_provider = { aws = { - ssm = { + ssm = var.storage_provider.aws.ssm == null ? null : { token_path = local.token_path token_path_arn = local.arn_ssm_parameters_path_tokens config_path = "${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.config}" @@ -50,6 +50,10 @@ module "orchestration_webhook" { parameter_store_tags = local.parameter_store_tags } } + scale_up = var.storage_provider.scale_up + scale_down = var.storage_provider.scale_down + pool = var.storage_provider.pool + job_retry = var.storage_provider.job_retry } observability = var.observability diff --git a/modules/runner-config/ssm-housekeeper.tf b/modules/runner-config/runner-config-housekeeper.tf similarity index 72% rename from modules/runner-config/ssm-housekeeper.tf rename to modules/runner-config/runner-config-housekeeper.tf index 2490e6dba1..e3c41094c3 100644 --- a/modules/runner-config/ssm-housekeeper.tf +++ b/modules/runner-config/runner-config-housekeeper.tf @@ -1,12 +1,26 @@ locals { - ssm_housekeeper_token_path = coalesce(var.storage_provider.aws.ssm.housekeeper.config.tokenPath, local.token_path) + ssm_housekeeper_token_path = coalesce(try(var.storage_provider.aws.ssm.housekeeper.config.tokenPath, null), local.token_path) ssm_housekeeper_parameter_path_arn = ( "arn:${var.aws_partition}:ssm:${var.aws_region}:${data.aws_caller_identity.current.account_id}:parameter${local.ssm_housekeeper_token_path}*" ) + ssm_housekeeper_cleanup = { + token_path = local.ssm_housekeeper_token_path + parameter_path_arn = local.ssm_housekeeper_parameter_path_arn + minimum_days_old = var.storage_provider.aws.ssm.housekeeper.config.minimumDaysOld + dry_run = var.storage_provider.aws.ssm.housekeeper.config.dryRun + } } -module "ssm_housekeeper" { - source = "./ssm-housekeeper" +module "runner_config_housekeeper" { + source = "./runner-config-housekeeper" + + storage_provider = { + aws = { + ssm = var.storage_provider.aws.ssm == null ? null : { + cleanup = local.ssm_housekeeper_cleanup + } + } + } config = { prefix = var.prefix @@ -15,12 +29,6 @@ module "ssm_housekeeper" { expression = var.storage_provider.aws.ssm.housekeeper.schedule_expression state = var.storage_provider.aws.ssm.housekeeper.state } - cleanup = { - token_path = local.ssm_housekeeper_token_path - parameter_path_arn = local.ssm_housekeeper_parameter_path_arn - minimum_days_old = var.storage_provider.aws.ssm.housekeeper.config.minimumDaysOld - dry_run = var.storage_provider.aws.ssm.housekeeper.config.dryRun - } lambda = { # The housekeeper resolves only its component-owned selector and never # inherits the selected orchestration provider's runner-control artifact. diff --git a/modules/runner-config/runner-config-housekeeper/housekeeper.tf b/modules/runner-config/runner-config-housekeeper/housekeeper.tf new file mode 100644 index 0000000000..47062a4dee --- /dev/null +++ b/modules/runner-config/runner-config-housekeeper/housekeeper.tf @@ -0,0 +1,127 @@ +locals { + vpc_enabled = ( + length(var.config.lambda.vpc.subnet_ids) > 0 && + length(var.config.lambda.vpc.security_group_ids) > 0 + ) + + cleanup_config = var.storage_provider.aws.ssm == null ? null : { + tokenPath = var.storage_provider.aws.ssm.cleanup.token_path + minimumDaysOld = var.storage_provider.aws.ssm.cleanup.minimum_days_old + dryRun = var.storage_provider.aws.ssm.cleanup.dry_run + } + + common_environment_variables = { + ENVIRONMENT = var.config.prefix + LOG_LEVEL = upper(var.config.observability.logs.level) + POWERTOOLS_SERVICE_NAME = "${var.config.prefix}-rc-housekeeper" + POWERTOOLS_TRACE_ENABLED = var.config.observability.tracing.mode != null + POWERTOOLS_TRACER_CAPTURE_HTTPS_REQUESTS = var.config.observability.tracing.capture_http_requests + POWERTOOLS_TRACER_CAPTURE_ERROR = var.config.observability.tracing.capture_error + } + + ssm_environment_variables = var.storage_provider.aws.ssm != null ? { + SSM_CLEANUP_CONFIG = jsonencode(local.cleanup_config) + } : {} +} + +resource "aws_lambda_function" "housekeeper" { + s3_bucket = var.config.lambda.artifact.s3.bucket + s3_key = var.config.lambda.artifact.s3.key + s3_object_version = var.config.lambda.artifact.s3.object_version + filename = var.config.lambda.artifact.s3.bucket == null ? var.config.lambda.artifact.zip : null + source_code_hash = var.config.lambda.artifact.s3.bucket == null ? filebase64sha256(var.config.lambda.artifact.zip) : null + function_name = "${var.config.prefix}-rc-housekeeper" + role = aws_iam_role.housekeeper.arn + handler = "index.runnerConfigHousekeeper" + runtime = var.config.lambda.runtime + timeout = var.config.lambda.timeout + tags = var.config.tags.lambda + memory_size = var.config.lambda.memory_size + architectures = [var.config.lambda.architecture] + + environment { + variables = merge( + local.common_environment_variables, + local.ssm_environment_variables, + ) + } + + dynamic "vpc_config" { + for_each = local.vpc_enabled ? [true] : [] + + content { + security_group_ids = var.config.lambda.vpc.security_group_ids + subnet_ids = var.config.lambda.vpc.subnet_ids + } + } + + dynamic "tracing_config" { + for_each = var.config.observability.tracing.mode != null ? [true] : [] + + content { + mode = var.config.observability.tracing.mode + } + } +} + +resource "aws_cloudwatch_log_group" "housekeeper" { + name = "/aws/lambda/${aws_lambda_function.housekeeper.function_name}" + retention_in_days = var.config.observability.logs.retention_in_days + kms_key_id = var.config.observability.logs.kms_key_id + log_group_class = var.config.observability.logs.class + tags = var.config.tags.log_group +} + +resource "aws_cloudwatch_event_rule" "housekeeper" { + name = "${var.config.prefix}-rc-housekeeper" + schedule_expression = var.config.schedule.expression + state = var.config.schedule.state + tags = var.config.tags.resources +} + +resource "aws_cloudwatch_event_target" "housekeeper" { + rule = aws_cloudwatch_event_rule.housekeeper.name + arn = aws_lambda_function.housekeeper.arn +} + +resource "aws_lambda_permission" "housekeeper" { + statement_id = "AllowExecutionFromCloudWatch" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.housekeeper.function_name + principal = "events.amazonaws.com" + source_arn = aws_cloudwatch_event_rule.housekeeper.arn +} + +resource "aws_iam_role" "housekeeper" { + name = "${substr("${var.config.prefix}-rc-hk-lambda", 0, 54)}-${substr(md5("${var.config.prefix}-rc-hk-lambda"), 0, 8)}" + description = "Lambda role for Runner Config Housekeeper (${var.config.prefix})" + assume_role_policy = data.aws_iam_policy_document.lambda_assume_role.json + path = var.config.lambda.role.path + permissions_boundary = var.config.lambda.role.permissions_boundary + tags = var.config.tags.resources +} + +resource "aws_iam_role_policy" "housekeeper" { + name = "housekeeper-policy" + role = aws_iam_role.housekeeper.name + policy = data.aws_iam_policy_document.housekeeper.json +} + +resource "aws_iam_role_policy" "housekeeper_logging" { + name = "logging-policy" + role = aws_iam_role.housekeeper.name + policy = data.aws_iam_policy_document.housekeeper_logging.json +} + +resource "aws_iam_role_policy_attachment" "housekeeper_vpc_execution_role" { + count = local.vpc_enabled ? 1 : 0 + role = aws_iam_role.housekeeper.name + policy_arn = "arn:${var.config.aws_partition}:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole" +} + +resource "aws_iam_role_policy" "housekeeper_xray" { + count = var.config.observability.tracing.mode != null ? 1 : 0 + name = "xray-policy" + policy = data.aws_iam_policy_document.lambda_xray[0].json + role = aws_iam_role.housekeeper.name +} diff --git a/modules/runner-config/ssm-housekeeper/iam-policies.tf b/modules/runner-config/runner-config-housekeeper/iam-policies.tf similarity index 67% rename from modules/runner-config/ssm-housekeeper/iam-policies.tf rename to modules/runner-config/runner-config-housekeeper/iam-policies.tf index 8d3bab2865..cc43f1f5e7 100644 --- a/modules/runner-config/ssm-housekeeper/iam-policies.tf +++ b/modules/runner-config/runner-config-housekeeper/iam-policies.tf @@ -35,24 +35,28 @@ data "aws_iam_policy_document" "lambda_xray" { } } -data "aws_iam_policy_document" "ssm_housekeeper" { - statement { - effect = "Allow" - actions = [ - "ssm:DeleteParameter", - "ssm:GetParametersByPath", - ] - resources = [var.config.cleanup.parameter_path_arn] +data "aws_iam_policy_document" "housekeeper" { + dynamic "statement" { + for_each = var.storage_provider.aws.ssm != null ? [true] : [] + + content { + effect = "Allow" + actions = [ + "ssm:DeleteParameter", + "ssm:GetParametersByPath", + ] + resources = [var.storage_provider.aws.ssm.cleanup.parameter_path_arn] + } } } -data "aws_iam_policy_document" "ssm_housekeeper_logging" { +data "aws_iam_policy_document" "housekeeper_logging" { statement { effect = "Allow" actions = [ "logs:CreateLogStream", "logs:PutLogEvents", ] - resources = ["${aws_cloudwatch_log_group.ssm_housekeeper.arn}*"] + resources = ["${aws_cloudwatch_log_group.housekeeper.arn}*"] } } diff --git a/modules/runner-config/runner-config-housekeeper/outputs.tf b/modules/runner-config/runner-config-housekeeper/outputs.tf new file mode 100644 index 0000000000..d72a23ac6a --- /dev/null +++ b/modules/runner-config/runner-config-housekeeper/outputs.tf @@ -0,0 +1,8 @@ +output "housekeeper" { + description = "Runner-config housekeeper Lambda resources." + value = { + lambda = aws_lambda_function.housekeeper + log_group = aws_cloudwatch_log_group.housekeeper + role = aws_iam_role.housekeeper + } +} diff --git a/modules/runner-config/ssm-housekeeper/tests/ssm-housekeeper.tftest.hcl b/modules/runner-config/runner-config-housekeeper/tests/housekeeper.tftest.hcl similarity index 63% rename from modules/runner-config/ssm-housekeeper/tests/ssm-housekeeper.tftest.hcl rename to modules/runner-config/runner-config-housekeeper/tests/housekeeper.tftest.hcl index bac30c6752..c12e81dc46 100644 --- a/modules/runner-config/ssm-housekeeper/tests/ssm-housekeeper.tftest.hcl +++ b/modules/runner-config/runner-config-housekeeper/tests/housekeeper.tftest.hcl @@ -7,43 +7,50 @@ mock_provider "aws" { mock_resource "aws_iam_role" { defaults = { - arn = "arn:aws:iam::123456789012:role/ssm-housekeeper-test" + arn = "arn:aws:iam::123456789012:role/housekeeper-test" } } mock_resource "aws_lambda_function" { defaults = { - arn = "arn:aws:lambda:eu-west-1:123456789012:function:ssm-housekeeper-test" + arn = "arn:aws:lambda:eu-west-1:123456789012:function:housekeeper-test" } } mock_resource "aws_cloudwatch_event_rule" { defaults = { - arn = "arn:aws:events:eu-west-1:123456789012:rule/ssm-housekeeper-test" + arn = "arn:aws:events:eu-west-1:123456789012:rule/housekeeper-test" } } mock_resource "aws_cloudwatch_log_group" { defaults = { - arn = "arn:aws:logs:eu-west-1:123456789012:log-group:/aws/lambda/ssm-housekeeper-test" + arn = "arn:aws:logs:eu-west-1:123456789012:log-group:/aws/lambda/housekeeper-test" } } } variables { + storage_provider = { + aws = { + ssm = { + cleanup = { + token_path = "/custom/runner/tokens" + parameter_path_arn = "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/custom/runner/tokens*" + minimum_days_old = 7 + dry_run = true + } + } + } + } + config = { - prefix = "ssm-housekeeper-test" + prefix = "housekeeper-test" aws_partition = "aws-us-gov" schedule = { expression = "rate(6 hours)" state = "DISABLED" } - cleanup = { - token_path = "/custom/runner/tokens" - parameter_path_arn = "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/custom/runner/tokens*" - minimum_days_old = 7 - dry_run = true - } lambda = { artifact = { zip = "unused-with-s3.zip" @@ -112,24 +119,24 @@ run "configures_schedule_cleanup_and_outputs" { assert { condition = ( - aws_cloudwatch_event_rule.ssm_housekeeper.schedule_expression == "rate(6 hours)" && - aws_cloudwatch_event_rule.ssm_housekeeper.state == "DISABLED" + aws_cloudwatch_event_rule.housekeeper.schedule_expression == "rate(6 hours)" && + aws_cloudwatch_event_rule.housekeeper.state == "DISABLED" ) error_message = "The housekeeper EventBridge rule must use the configured schedule and state." } assert { condition = ( - jsondecode(aws_lambda_function.ssm_housekeeper.environment[0].variables["SSM_CLEANUP_CONFIG"]).tokenPath == "/custom/runner/tokens" && - jsondecode(aws_lambda_function.ssm_housekeeper.environment[0].variables["SSM_CLEANUP_CONFIG"]).minimumDaysOld == 7 && - jsondecode(aws_lambda_function.ssm_housekeeper.environment[0].variables["SSM_CLEANUP_CONFIG"]).dryRun + jsondecode(aws_lambda_function.housekeeper.environment[0].variables["SSM_CLEANUP_CONFIG"]).tokenPath == "/custom/runner/tokens" && + jsondecode(aws_lambda_function.housekeeper.environment[0].variables["SSM_CLEANUP_CONFIG"]).minimumDaysOld == 7 && + jsondecode(aws_lambda_function.housekeeper.environment[0].variables["SSM_CLEANUP_CONFIG"]).dryRun ) error_message = "The Lambda cleanup configuration must preserve the configured path override, age, and dry-run setting." } assert { condition = contains( - data.aws_iam_policy_document.ssm_housekeeper.statement[0].resources, + data.aws_iam_policy_document.housekeeper.statement[0].resources, "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/custom/runner/tokens*", ) error_message = "The housekeeper IAM policy must authorize the same overridden Parameter Store path supplied to the Lambda." @@ -159,32 +166,56 @@ run "configures_schedule_cleanup_and_outputs" { assert { condition = ( - length(aws_lambda_function.ssm_housekeeper.vpc_config) == 0 && - length(aws_iam_role_policy_attachment.ssm_housekeeper_vpc_execution_role) == 0 && - length(aws_lambda_function.ssm_housekeeper.tracing_config) == 0 && - length(aws_iam_role_policy.ssm_housekeeper_xray) == 0 + length(aws_lambda_function.housekeeper.vpc_config) == 0 && + length(aws_iam_role_policy_attachment.housekeeper_vpc_execution_role) == 0 && + length(aws_lambda_function.housekeeper.tracing_config) == 0 && + length(aws_iam_role_policy.housekeeper_xray) == 0 ) error_message = "Empty VPC configuration and disabled tracing must not create their optional Lambda or IAM configuration." } } +run "omits_ssm_cleanup_configuration_without_ssm" { + command = plan + + variables { + storage_provider = { + aws = { + ssm = null + } + } + } + + assert { + condition = !contains(keys(aws_lambda_function.housekeeper.environment[0].variables), "SSM_CLEANUP_CONFIG") + error_message = "The housekeeper Lambda must not publish SSM cleanup configuration when SSM is not selected." + } +} + run "enables_vpc_and_xray_together" { command = plan variables { + storage_provider = { + aws = { + ssm = { + cleanup = { + token_path = "/github-runner/tokens" + parameter_path_arn = "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/tokens*" + minimum_days_old = 1 + dry_run = false + } + } + } + } + config = { - prefix = "ssm-housekeeper-vpc-test" + prefix = "housekeeper-vpc-test" aws_partition = "aws-us-gov" schedule = { expression = "rate(1 day)" state = "ENABLED" } - cleanup = { - token_path = "/github-runner/tokens" - parameter_path_arn = "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/tokens*" - minimum_days_old = 1 - dry_run = false - } lambda = { artifact = { zip = "unused-with-s3.zip" @@ -229,23 +260,23 @@ run "enables_vpc_and_xray_together" { assert { condition = ( - length(aws_lambda_function.ssm_housekeeper.vpc_config) == 1 && - aws_lambda_function.ssm_housekeeper.vpc_config[0].subnet_ids == toset(["subnet-12345678"]) && - aws_lambda_function.ssm_housekeeper.vpc_config[0].security_group_ids == toset(["sg-12345678"]) && - length(aws_iam_role_policy_attachment.ssm_housekeeper_vpc_execution_role) == 1 && - aws_iam_role_policy_attachment.ssm_housekeeper_vpc_execution_role[0].policy_arn == "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole" + length(aws_lambda_function.housekeeper.vpc_config) == 1 && + aws_lambda_function.housekeeper.vpc_config[0].subnet_ids == toset(["subnet-12345678"]) && + aws_lambda_function.housekeeper.vpc_config[0].security_group_ids == toset(["sg-12345678"]) && + length(aws_iam_role_policy_attachment.housekeeper_vpc_execution_role) == 1 && + aws_iam_role_policy_attachment.housekeeper_vpc_execution_role[0].policy_arn == "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole" ) error_message = "A complete VPC configuration must configure the Lambda and attach the partition-aware VPC execution policy." } assert { condition = ( - length(aws_lambda_function.ssm_housekeeper.tracing_config) == 1 && - aws_lambda_function.ssm_housekeeper.tracing_config[0].mode == "Active" && - length(aws_iam_role_policy.ssm_housekeeper_xray) == 1 && - aws_lambda_function.ssm_housekeeper.environment[0].variables["POWERTOOLS_TRACE_ENABLED"] == "true" && - aws_lambda_function.ssm_housekeeper.environment[0].variables["POWERTOOLS_TRACER_CAPTURE_HTTPS_REQUESTS"] == "true" && - aws_lambda_function.ssm_housekeeper.environment[0].variables["POWERTOOLS_TRACER_CAPTURE_ERROR"] == "true" + length(aws_lambda_function.housekeeper.tracing_config) == 1 && + aws_lambda_function.housekeeper.tracing_config[0].mode == "Active" && + length(aws_iam_role_policy.housekeeper_xray) == 1 && + aws_lambda_function.housekeeper.environment[0].variables["POWERTOOLS_TRACE_ENABLED"] == "true" && + aws_lambda_function.housekeeper.environment[0].variables["POWERTOOLS_TRACER_CAPTURE_HTTPS_REQUESTS"] == "true" && + aws_lambda_function.housekeeper.environment[0].variables["POWERTOOLS_TRACER_CAPTURE_ERROR"] == "true" ) error_message = "Active tracing must configure Lambda tracing, X-Ray IAM permissions, and tracing-helper environment variables." } diff --git a/modules/runner-config/ssm-housekeeper/variables.tf b/modules/runner-config/runner-config-housekeeper/variables.tf similarity index 80% rename from modules/runner-config/ssm-housekeeper/variables.tf rename to modules/runner-config/runner-config-housekeeper/variables.tf index 64848fc33c..db51a0cbc1 100644 --- a/modules/runner-config/ssm-housekeeper/variables.tf +++ b/modules/runner-config/runner-config-housekeeper/variables.tf @@ -6,10 +6,6 @@ variable "config" { - `aws_partition`: AWS partition used to construct IAM policy ARNs. - `schedule.expression`: EventBridge schedule expression that invokes the housekeeper. - `schedule.state`: State of the EventBridge rule. - - `cleanup.token_path`: Parameter Store token path supplied to the Lambda. - - `cleanup.parameter_path_arn`: IAM resource ARN matching `cleanup.token_path`. - - `cleanup.minimum_days_old`: Minimum parameter age before deletion. - - `cleanup.dry_run`: Reports eligible parameters without deleting them when true. - `lambda.artifact.zip`: Resolved local control-plane archive. - `lambda.artifact.s3.bucket`: Optional S3 bucket containing the Lambda archive. - `lambda.artifact.s3.key`: Object key of the Lambda archive. @@ -37,12 +33,6 @@ variable "config" { expression = string state = string }) - cleanup = object({ - token_path = string - parameter_path_arn = string - minimum_days_old = number - dry_run = bool - }) lambda = object({ artifact = object({ zip = string @@ -91,3 +81,29 @@ variable "config" { nullable = false } + +variable "storage_provider" { + description = <<-EOT + Storage-provider selection used to gate provider-specific housekeeper IAM statements. + + - `aws.ssm.cleanup.token_path`: Parameter Store token path supplied to the Lambda. + - `aws.ssm.cleanup.parameter_path_arn`: IAM resource ARN matching `aws.ssm.cleanup.token_path`. + - `aws.ssm.cleanup.minimum_days_old`: Minimum parameter age before deletion. + - `aws.ssm.cleanup.dry_run`: Reports eligible parameters without deleting them when true. + EOT + + type = object({ + aws = object({ + ssm = optional(object({ + cleanup = object({ + token_path = string + parameter_path_arn = string + minimum_days_old = number + dry_run = bool + }) + }), null) + }) + }) + + nullable = false +} diff --git a/modules/runner-config/ssm-housekeeper/versions.tf b/modules/runner-config/runner-config-housekeeper/versions.tf similarity index 100% rename from modules/runner-config/ssm-housekeeper/versions.tf rename to modules/runner-config/runner-config-housekeeper/versions.tf diff --git a/modules/runner-config/runner-ssm-parameters.tf b/modules/runner-config/runner-ssm-parameters.tf deleted file mode 100644 index 770adaf8c9..0000000000 --- a/modules/runner-config/runner-ssm-parameters.tf +++ /dev/null @@ -1,28 +0,0 @@ -# Shared runner configuration stored in SSM Parameter Store. -resource "aws_ssm_parameter" "runner_agent_mode" { - name = "${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.config}/agent_mode" - type = "String" - value = local.orchestration_provider_runner_lifecycle.ephemeral ? "ephemeral" : "persistent" - tags = local.ssm_parameter_tags -} - -resource "aws_ssm_parameter" "disable_default_labels" { - name = "${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.config}/disable_default_labels" - type = "String" - value = var.runner.disable_default_labels - tags = local.ssm_parameter_tags -} - -resource "aws_ssm_parameter" "jit_config_enabled" { - name = "${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.config}/enable_jit_config" - type = "String" - value = local.orchestration_provider_runner_lifecycle.jit_config_enabled - tags = local.ssm_parameter_tags -} - -resource "aws_ssm_parameter" "token_path" { - name = "${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.config}/token_path" - type = "String" - value = "${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.tokens}" - tags = local.ssm_parameter_tags -} diff --git a/modules/runner-config/ssm-housekeeper/outputs.tf b/modules/runner-config/ssm-housekeeper/outputs.tf deleted file mode 100644 index 064f5a1ab1..0000000000 --- a/modules/runner-config/ssm-housekeeper/outputs.tf +++ /dev/null @@ -1,8 +0,0 @@ -output "housekeeper" { - description = "SSM housekeeper Lambda resources." - value = { - lambda = aws_lambda_function.ssm_housekeeper - log_group = aws_cloudwatch_log_group.ssm_housekeeper - role = aws_iam_role.ssm_housekeeper - } -} diff --git a/modules/runner-config/ssm-housekeeper/ssm-housekeeper.tf b/modules/runner-config/ssm-housekeeper/ssm-housekeeper.tf deleted file mode 100644 index bcafed201a..0000000000 --- a/modules/runner-config/ssm-housekeeper/ssm-housekeeper.tf +++ /dev/null @@ -1,119 +0,0 @@ -locals { - vpc_enabled = ( - length(var.config.lambda.vpc.subnet_ids) > 0 && - length(var.config.lambda.vpc.security_group_ids) > 0 - ) - - cleanup_config = { - tokenPath = var.config.cleanup.token_path - minimumDaysOld = var.config.cleanup.minimum_days_old - dryRun = var.config.cleanup.dry_run - } -} - -resource "aws_lambda_function" "ssm_housekeeper" { - s3_bucket = var.config.lambda.artifact.s3.bucket - s3_key = var.config.lambda.artifact.s3.key - s3_object_version = var.config.lambda.artifact.s3.object_version - filename = var.config.lambda.artifact.s3.bucket == null ? var.config.lambda.artifact.zip : null - source_code_hash = var.config.lambda.artifact.s3.bucket == null ? filebase64sha256(var.config.lambda.artifact.zip) : null - function_name = "${var.config.prefix}-ssm-housekeeper" - role = aws_iam_role.ssm_housekeeper.arn - handler = "index.ssmHousekeeper" - runtime = var.config.lambda.runtime - timeout = var.config.lambda.timeout - tags = var.config.tags.lambda - memory_size = var.config.lambda.memory_size - architectures = [var.config.lambda.architecture] - - environment { - variables = { - ENVIRONMENT = var.config.prefix - LOG_LEVEL = upper(var.config.observability.logs.level) - SSM_CLEANUP_CONFIG = jsonencode(local.cleanup_config) - POWERTOOLS_SERVICE_NAME = "${var.config.prefix}-ssm-housekeeper" - POWERTOOLS_TRACE_ENABLED = var.config.observability.tracing.mode != null - POWERTOOLS_TRACER_CAPTURE_HTTPS_REQUESTS = var.config.observability.tracing.capture_http_requests - POWERTOOLS_TRACER_CAPTURE_ERROR = var.config.observability.tracing.capture_error - } - } - - dynamic "vpc_config" { - for_each = local.vpc_enabled ? [true] : [] - - content { - security_group_ids = var.config.lambda.vpc.security_group_ids - subnet_ids = var.config.lambda.vpc.subnet_ids - } - } - - dynamic "tracing_config" { - for_each = var.config.observability.tracing.mode != null ? [true] : [] - - content { - mode = var.config.observability.tracing.mode - } - } -} - -resource "aws_cloudwatch_log_group" "ssm_housekeeper" { - name = "/aws/lambda/${aws_lambda_function.ssm_housekeeper.function_name}" - retention_in_days = var.config.observability.logs.retention_in_days - kms_key_id = var.config.observability.logs.kms_key_id - log_group_class = var.config.observability.logs.class - tags = var.config.tags.log_group -} - -resource "aws_cloudwatch_event_rule" "ssm_housekeeper" { - name = "${var.config.prefix}-ssm-housekeeper" - schedule_expression = var.config.schedule.expression - state = var.config.schedule.state - tags = var.config.tags.resources -} - -resource "aws_cloudwatch_event_target" "ssm_housekeeper" { - rule = aws_cloudwatch_event_rule.ssm_housekeeper.name - arn = aws_lambda_function.ssm_housekeeper.arn -} - -resource "aws_lambda_permission" "ssm_housekeeper" { - statement_id = "AllowExecutionFromCloudWatch" - action = "lambda:InvokeFunction" - function_name = aws_lambda_function.ssm_housekeeper.function_name - principal = "events.amazonaws.com" - source_arn = aws_cloudwatch_event_rule.ssm_housekeeper.arn -} - -resource "aws_iam_role" "ssm_housekeeper" { - name = "${substr("${var.config.prefix}-ssm-hk-lambda", 0, 54)}-${substr(md5("${var.config.prefix}-ssm-hk-lambda"), 0, 8)}" - description = "Lambda role for SSM Housekeeper (${var.config.prefix})" - assume_role_policy = data.aws_iam_policy_document.lambda_assume_role.json - path = var.config.lambda.role.path - permissions_boundary = var.config.lambda.role.permissions_boundary - tags = var.config.tags.resources -} - -resource "aws_iam_role_policy" "ssm_housekeeper" { - name = "ssm-policy" - role = aws_iam_role.ssm_housekeeper.name - policy = data.aws_iam_policy_document.ssm_housekeeper.json -} - -resource "aws_iam_role_policy" "ssm_housekeeper_logging" { - name = "logging-policy" - role = aws_iam_role.ssm_housekeeper.name - policy = data.aws_iam_policy_document.ssm_housekeeper_logging.json -} - -resource "aws_iam_role_policy_attachment" "ssm_housekeeper_vpc_execution_role" { - count = local.vpc_enabled ? 1 : 0 - role = aws_iam_role.ssm_housekeeper.name - policy_arn = "arn:${var.config.aws_partition}:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole" -} - -resource "aws_iam_role_policy" "ssm_housekeeper_xray" { - count = var.config.observability.tracing.mode != null ? 1 : 0 - name = "xray-policy" - policy = data.aws_iam_policy_document.lambda_xray[0].json - role = aws_iam_role.ssm_housekeeper.name -} diff --git a/modules/runner-config/storage-provider.aws.ssm.tf b/modules/runner-config/storage-provider.aws.ssm.tf new file mode 100644 index 0000000000..39b593c697 --- /dev/null +++ b/modules/runner-config/storage-provider.aws.ssm.tf @@ -0,0 +1,72 @@ +# AWS Systems Manager-specific control-plane configuration. +locals { + ssm_config_path = try("${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.config}", "") + + ssm_tags = merge(local.common_tags, try(var.storage_provider.aws.ssm.tags, {})) + ssm_parameter_tags = merge(local.ssm_tags, try(var.storage_provider.aws.ssm.parameters.tags, {})) + ssm_housekeeper_tags = merge(local.ssm_tags, try(var.storage_provider.aws.ssm.housekeeper.tags, {})) + ssm_housekeeper_lambda_tags = merge(local.lambda_tags, try(var.storage_provider.aws.ssm.tags, {}), try(var.storage_provider.aws.ssm.housekeeper.tags, {})) + ssm_housekeeper_log_tags = merge(local.observability_log_tags, try(var.storage_provider.aws.ssm.tags, {}), try(var.storage_provider.aws.ssm.housekeeper.tags, {})) + + ssm_housekeeper_artifact_s3_selected = ( + try(var.storage_provider.aws.ssm.housekeeper.lambda.artifact.s3, null) != null + ) + ssm_housekeeper_artifact = { + zip = local.ssm_housekeeper_artifact_s3_selected ? null : coalesce( + try(var.storage_provider.aws.ssm.housekeeper.lambda.artifact.zip, null), + local.packaged_runners_lambda_zip, + ) + s3 = { + bucket = local.ssm_housekeeper_artifact_s3_selected ? var.lambda.artifact.s3.bucket : null + key = try(var.storage_provider.aws.ssm.housekeeper.lambda.artifact.s3.key, null) + object_version = try(var.storage_provider.aws.ssm.housekeeper.lambda.artifact.s3.object_version, null) + } + } + + kms_key_id = try(var.storage_provider.aws.ssm.kms_key_id, null) + token_path = try("${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.tokens}", "") + arn_ssm_parameters_path_tokens = try("arn:${var.aws_partition}:ssm:${var.aws_region}:${data.aws_caller_identity.current.account_id}:parameter${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.tokens}", "") + arn_ssm_parameters_path_config = try("arn:${var.aws_partition}:ssm:${var.aws_region}:${data.aws_caller_identity.current.account_id}:parameter${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.config}", "") + + parameter_store_tags = jsonencode([ + for key, value in local.ssm_parameter_tags : { + Key = key + Value = value + } + ]) +} + +data "aws_caller_identity" "current" {} + +# Shared runner configuration stored in SSM Parameter Store. +resource "aws_ssm_parameter" "runner_agent_mode" { + count = var.storage_provider.aws.ssm != null ? 1 : 0 + name = "${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.config}/agent_mode" + type = "String" + value = local.orchestration_provider_runner_lifecycle.ephemeral ? "ephemeral" : "persistent" + tags = local.ssm_parameter_tags +} + +resource "aws_ssm_parameter" "disable_default_labels" { + count = var.storage_provider.aws.ssm != null ? 1 : 0 + name = "${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.config}/disable_default_labels" + type = "String" + value = var.runner.disable_default_labels + tags = local.ssm_parameter_tags +} + +resource "aws_ssm_parameter" "jit_config_enabled" { + 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_jit_config" + type = "String" + value = local.orchestration_provider_runner_lifecycle.jit_config_enabled + tags = local.ssm_parameter_tags +} + +resource "aws_ssm_parameter" "token_path" { + count = var.storage_provider.aws.ssm != null ? 1 : 0 + name = "${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.config}/token_path" + type = "String" + value = "${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.tokens}" + tags = local.ssm_parameter_tags +} diff --git a/modules/runner-config/tests/computed-iam-inputs.tftest.hcl b/modules/runner-config/tests/computed-iam-inputs.tftest.hcl index 3e08eeb84b..055127e7ba 100644 --- a/modules/runner-config/tests/computed-iam-inputs.tftest.hcl +++ b/modules/runner-config/tests/computed-iam-inputs.tftest.hcl @@ -16,11 +16,11 @@ run "computed_external_values_keep_plan_shape_known" { # The packaged runner archive is added by the release build, so the computed # IAM fixture isolates the two common housekeeper children in a source checkout. override_module { - target = module.external_iam.module.ssm_housekeeper + target = module.external_iam.module.runner_config_housekeeper } override_module { - target = module.generated_policy.module.ssm_housekeeper + target = module.generated_policy.module.runner_config_housekeeper } assert { diff --git a/modules/runner-config/tests/pool.tftest.hcl b/modules/runner-config/tests/pool.tftest.hcl index 7f2a520cd2..6993212d91 100644 --- a/modules/runner-config/tests/pool.tftest.hcl +++ b/modules/runner-config/tests/pool.tftest.hcl @@ -21,7 +21,7 @@ mock_provider "aws" { # The runner archive is injected during packaging, so isolate the common # housekeeper child in source-checkout tests where that build artifact is absent. override_module { - target = module.ssm_housekeeper + target = module.runner_config_housekeeper } variables { @@ -161,8 +161,8 @@ run "plan_with_pool_enabled" { assert { condition = ( - aws_ssm_parameter.runner_agent_mode.value == "ephemeral" - && aws_ssm_parameter.jit_config_enabled.value == "true" + aws_ssm_parameter.runner_agent_mode[0].value == "ephemeral" + && aws_ssm_parameter.jit_config_enabled[0].value == "true" ) error_message = "Runner-config must serialize the webhook provider's resolved lifecycle contract without duplicating its JIT fallback." } diff --git a/modules/runner-config/tests/tags.tftest.hcl b/modules/runner-config/tests/tags.tftest.hcl index 8f9cbb2d0d..34c78acd4e 100644 --- a/modules/runner-config/tests/tags.tftest.hcl +++ b/modules/runner-config/tests/tags.tftest.hcl @@ -21,7 +21,7 @@ mock_provider "aws" { # The runner archive is injected during packaging, so model the common # housekeeper output while testing parent-level tag composition from source. override_module { - target = module.ssm_housekeeper + target = module.runner_config_housekeeper } variables { @@ -255,7 +255,7 @@ run "layered_component_tags" { } assert { - condition = aws_ssm_parameter.runner_agent_mode.tags == tomap({ + condition = aws_ssm_parameter.runner_agent_mode[0].tags == tomap({ Name = "github-actions-action-runner" "ghr:ssm_config_path" = "/github-runner/config" precedence = "ssm-parameter" diff --git a/modules/runner-config/variables.tf b/modules/runner-config/variables.tf index 5b3cdb0102..36a9fac8ee 100644 --- a/modules/runner-config/variables.tf +++ b/modules/runner-config/variables.tf @@ -165,7 +165,7 @@ variable "storage_provider" { EOT type = object({ aws = object({ - ssm = object({ + ssm = optional(object({ paths = object({ root = string tokens = string @@ -197,7 +197,35 @@ variable "storage_provider" { dryRun = optional(bool, false) }), {}) }), {}) - }) + }), null) + }) + scale_up = optional(object({ + environment_variables = map(string) + iam_policy_json = optional(string, null) + }), { + environment_variables = {} + iam_policy_json = null + }) + scale_down = optional(object({ + environment_variables = map(string) + iam_policy_json = optional(string, null) + }), { + environment_variables = {} + iam_policy_json = null + }) + pool = optional(object({ + environment_variables = map(string) + iam_policy_json = optional(string, null) + }), { + environment_variables = {} + iam_policy_json = null + }) + job_retry = optional(object({ + environment_variables = map(string) + iam_policy_json = optional(string, null) + }), { + environment_variables = {} + iam_policy_json = null }) }) diff --git a/modules/webhook/direct/variables.tf b/modules/webhook/direct/variables.tf index 671c911f70..fd15dae9ce 100644 --- a/modules/webhook/direct/variables.tf +++ b/modules/webhook/direct/variables.tf @@ -28,13 +28,13 @@ variable "config" { }), null) repository_white_list = optional(list(string), []) queue_selection_strategy = optional(string, "first") - storage_provider = object({ - aws = object({ - ssm = object({ - kms_key_id = optional(string, null) - }) - }) - }) + storage_provider = optional(object({ + aws = optional(object({ + ssm = optional(object({}), null) + }), {}) + environment_variables = map(string) + iam_policy_json = optional(string, null) + })) log_level = optional(string, "info") lambda_runtime = optional(string, "nodejs24.x") aws_partition = optional(string, "aws") diff --git a/modules/webhook/direct/webhook.tf b/modules/webhook/direct/webhook.tf index 3a4658ce77..7cadf8dede 100644 --- a/modules/webhook/direct/webhook.tf +++ b/modules/webhook/direct/webhook.tf @@ -26,13 +26,13 @@ resource "aws_lambda_function" "webhook" { POWERTOOLS_TRACE_ENABLED = var.config.tracing_config.mode != null ? true : false POWERTOOLS_TRACER_CAPTURE_HTTPS_REQUESTS = var.config.tracing_config.capture_http_requests POWERTOOLS_TRACER_CAPTURE_ERROR = var.config.tracing_config.capture_error - PARAMETER_GITHUB_APP_WEBHOOK_SECRET = var.config.github_app_parameters.webhook_secret.name + PARAMETER_GITHUB_APP_WEBHOOK_SECRET = var.config.storage_provider.aws.ssm != null ? var.config.github_app_parameters.webhook_secret.name : null REPOSITORY_ALLOW_LIST = jsonencode(var.config.repository_white_list) QUEUE_SELECTION_STRATEGY = var.config.queue_selection_strategy - PARAMETER_RUNNER_MATCHER_CONFIG_PATH = join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.name]) - PARAMETER_RUNNER_MATCHER_VERSION = join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.version]) # enforce cold start after Changes in SSM parameter + PARAMETER_RUNNER_MATCHER_CONFIG_PATH = var.config.storage_provider.aws.ssm != null ? join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.name]) : null + PARAMETER_RUNNER_MATCHER_VERSION = var.config.storage_provider.aws.ssm != null ? join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.version]) : null # enforce cold start after Changes in SSM parameter } : k => v if v != null - }) + }, var.config.storage_provider.environment_variables) } dynamic "vpc_config" { @@ -125,11 +125,13 @@ resource "aws_iam_role_policy" "webhook_sqs" { } resource "aws_iam_role_policy" "webhook_kms" { + count = var.config.storage_provider.aws.ssm != null ? 1 : 0 + name = "kms-policy" role = aws_iam_role.webhook_lambda.name policy = templatefile("${path.module}/../policies/lambda-kms.json", { - kms_key_arn = var.config.storage_provider.aws.ssm.kms_key_id != null ? var.config.storage_provider.aws.ssm.kms_key_id : "arn:${var.config.aws_partition}:kms:::CMK_NOT_IN_USE" + kms_key_arn = "arn:${var.config.aws_partition}:kms:::CMK_NOT_IN_USE" }) } @@ -137,14 +139,19 @@ resource "aws_iam_role_policy" "webhook_ssm" { name = "publish-ssm-policy" role = aws_iam_role.webhook_lambda.name - policy = templatefile("${path.module}/../policies/lambda-ssm.json", { + policy = var.config.storage_provider.aws.ssm != null ? templatefile("${path.module}/../policies/lambda-ssm.json", { resource_arns = jsonencode( concat( [var.config.github_app_parameters.webhook_secret.arn], [for p in var.config.ssm_parameter_runner_matcher_config : p.arn] ) ) - }) + }) : var.config.storage_provider.iam_policy_json +} + +moved { + from = aws_iam_role_policy.webhook_kms + to = aws_iam_role_policy.webhook_kms[0] } resource "aws_iam_role_policy" "xray" { diff --git a/modules/webhook/eventbridge/dispatcher.tf b/modules/webhook/eventbridge/dispatcher.tf index de633b242a..8cca84e900 100644 --- a/modules/webhook/eventbridge/dispatcher.tf +++ b/modules/webhook/eventbridge/dispatcher.tf @@ -49,12 +49,12 @@ resource "aws_lambda_function" "dispatcher" { POWERTOOLS_TRACER_CAPTURE_HTTPS_REQUESTS = var.config.tracing_config.capture_http_requests POWERTOOLS_TRACER_CAPTURE_ERROR = var.config.tracing_config.capture_error # Parameters required for lambda configuration - PARAMETER_RUNNER_MATCHER_CONFIG_PATH = join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.name]) - PARAMETER_RUNNER_MATCHER_VERSION = join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.version]) # enforce cold start after Changes in SSM parameter + PARAMETER_RUNNER_MATCHER_CONFIG_PATH = var.config.storage_provider.aws.ssm != null ? join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.name]) : null + PARAMETER_RUNNER_MATCHER_VERSION = var.config.storage_provider.aws.ssm != null ? join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.version]) : null # enforce cold start after Changes in SSM parameter REPOSITORY_ALLOW_LIST = jsonencode(var.config.repository_white_list) QUEUE_SELECTION_STRATEGY = var.config.queue_selection_strategy } : k => v if v != null - }) + }, var.config.storage_provider.dispatcher.environment_variables) } dynamic "vpc_config" { @@ -123,11 +123,13 @@ resource "aws_iam_role_policy" "dispatcher_sqs" { } resource "aws_iam_role_policy" "dispatcher_kms" { + count = var.config.storage_provider.aws.ssm != null ? 1 : 0 + name = "kms-policy" role = aws_iam_role.dispatcher_lambda.name policy = templatefile("${path.module}/../policies/lambda-kms.json", { - kms_key_arn = var.config.storage_provider.aws.ssm.kms_key_id != null ? var.config.storage_provider.aws.ssm.kms_key_id : "arn:${var.config.aws_partition}:kms:::CMK_NOT_IN_USE" + kms_key_arn = "arn:${var.config.aws_partition}:kms:::CMK_NOT_IN_USE" }) } @@ -135,13 +137,18 @@ resource "aws_iam_role_policy" "dispatcher_ssm" { name = "publish-ssm-policy" role = aws_iam_role.dispatcher_lambda.name - policy = templatefile("${path.module}/../policies/lambda-ssm.json", { + policy = var.config.storage_provider.aws.ssm != null ? templatefile("${path.module}/../policies/lambda-ssm.json", { resource_arns = jsonencode( concat( [for p in var.config.ssm_parameter_runner_matcher_config : p.arn] ) ) - }) + }) : var.config.storage_provider.dispatcher.iam_policy_json +} + +moved { + from = aws_iam_role_policy.dispatcher_kms + to = aws_iam_role_policy.dispatcher_kms[0] } resource "aws_iam_role_policy" "dispatcher_xray" { diff --git a/modules/webhook/eventbridge/variables.tf b/modules/webhook/eventbridge/variables.tf index c6b2c660ba..f362e08b3a 100644 --- a/modules/webhook/eventbridge/variables.tf +++ b/modules/webhook/eventbridge/variables.tf @@ -28,13 +28,19 @@ variable "config" { }), null) repository_white_list = optional(list(string), []) queue_selection_strategy = optional(string, "first") - storage_provider = object({ - aws = object({ - ssm = object({ - kms_key_id = optional(string, null) - }) + storage_provider = optional(object({ + aws = optional(object({ + ssm = optional(object({}), null) + }), {}) + webhook = object({ + environment_variables = map(string) + iam_policy_json = optional(string, null) }) - }) + dispatcher = object({ + environment_variables = map(string) + iam_policy_json = optional(string, null) + }) + })) log_level = optional(string, "info") lambda_runtime = optional(string, "nodejs24.x") aws_partition = optional(string, "aws") diff --git a/modules/webhook/eventbridge/webhook.tf b/modules/webhook/eventbridge/webhook.tf index 9777188187..b42e076907 100644 --- a/modules/webhook/eventbridge/webhook.tf +++ b/modules/webhook/eventbridge/webhook.tf @@ -35,10 +35,10 @@ resource "aws_lambda_function" "webhook" { # Parameters required for lambda configuration ACCEPT_EVENTS = jsonencode(var.config.accept_events) EVENT_BUS_NAME = aws_cloudwatch_event_bus.main.name - PARAMETER_GITHUB_APP_WEBHOOK_SECRET = var.config.github_app_parameters.webhook_secret.name - PARAMETER_RUNNER_MATCHER_CONFIG_PATH = join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.name]) + PARAMETER_GITHUB_APP_WEBHOOK_SECRET = var.config.storage_provider.aws.ssm != null ? var.config.github_app_parameters.webhook_secret.name : null + PARAMETER_RUNNER_MATCHER_CONFIG_PATH = var.config.storage_provider.aws.ssm != null ? join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.name]) : null } : k => v if v != null - }) + }, var.config.storage_provider.webhook.environment_variables) } dynamic "vpc_config" { @@ -129,20 +129,27 @@ resource "aws_iam_role_policy" "webhook_ssm" { name = "publish-ssm-policy" role = aws_iam_role.webhook_lambda.name - policy = templatefile("${path.module}/../policies/lambda-ssm.json", { + policy = var.config.storage_provider.aws.ssm != null ? templatefile("${path.module}/../policies/lambda-ssm.json", { resource_arns = jsonencode([var.config.github_app_parameters.webhook_secret.arn]) - }) + }) : var.config.storage_provider.webhook.iam_policy_json } resource "aws_iam_role_policy" "webhook_kms" { + count = var.config.storage_provider.aws.ssm != null ? 1 : 0 + name = "kms-policy" role = aws_iam_role.webhook_lambda.name policy = templatefile("${path.module}/../policies/lambda-kms.json", { - kms_key_arn = var.config.storage_provider.aws.ssm.kms_key_id != null ? var.config.storage_provider.aws.ssm.kms_key_id : "arn:${var.config.aws_partition}:kms:::CMK_NOT_IN_USE" + kms_key_arn = "arn:${var.config.aws_partition}:kms:::CMK_NOT_IN_USE" }) } +moved { + from = aws_iam_role_policy.webhook_kms + to = aws_iam_role_policy.webhook_kms[0] +} + resource "aws_iam_role_policy" "xray" { count = var.config.tracing_config.mode != null ? 1 : 0 name = "xray-policy" diff --git a/modules/webhook/variables.tf b/modules/webhook/variables.tf index d4b25d7d70..758c9c1070 100644 --- a/modules/webhook/variables.tf +++ b/modules/webhook/variables.tf @@ -238,15 +238,28 @@ variable "matcher_config_parameter_store_tier" { } variable "storage_provider" { - description = "Storage-provider configuration used by the webhook resources." + description = "Resolved storage-provider marker and provider-owned webhook capabilities." type = object({ aws = object({ - kms_key_id = optional(string, null) - ssm = object({ + ssm = optional(object({ paths = object({ root = string webhook = string }) + }), null) + }) + direct = object({ + environment_variables = map(string) + iam_policy_json = optional(string, null) + }) + eventbridge = object({ + webhook = object({ + environment_variables = map(string) + iam_policy_json = optional(string, null) + }) + dispatcher = object({ + environment_variables = map(string) + iam_policy_json = optional(string, null) }) }) }) diff --git a/modules/webhook/webhook.tf b/modules/webhook/webhook.tf index 418ee9cfb0..6a06065a10 100644 --- a/modules/webhook/webhook.tf +++ b/modules/webhook/webhook.tf @@ -43,7 +43,7 @@ locals { } resource "aws_ssm_parameter" "runner_matcher_config" { - count = local.total_chunks + count = var.storage_provider.aws.ssm != null ? local.total_chunks : 0 name = "${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.webhook}/runner-matcher-config${local.total_chunks > 1 ? "-${count.index}" : ""}" type = "String" @@ -76,13 +76,9 @@ module "direct" { lambda_apigateway_access_log_settings = var.webhook_lambda_apigateway_access_log_settings, repository_white_list = var.repository_white_list, queue_selection_strategy = var.queue_selection_strategy, - storage_provider = { - aws = { - ssm = { - kms_key_id = var.storage_provider.aws.kms_key_id - } - } - } + storage_provider = merge(var.storage_provider.direct, { + aws = var.storage_provider.aws + }) log_level = var.log_level, lambda_runtime = var.lambda_runtime, aws_partition = var.aws_partition, @@ -125,13 +121,9 @@ module "eventbridge" { lambda_apigateway_access_log_settings = var.webhook_lambda_apigateway_access_log_settings, repository_white_list = var.repository_white_list, queue_selection_strategy = var.queue_selection_strategy, - storage_provider = { - aws = { - ssm = { - kms_key_id = var.storage_provider.aws.kms_key_id - } - } - } + storage_provider = merge(var.storage_provider.eventbridge, { + aws = var.storage_provider.aws + }) log_level = var.log_level, lambda_runtime = var.lambda_runtime, aws_partition = var.aws_partition, From 07094af06c05c953f2dc6b4a7d55d86645b373c5 Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Thu, 24 Sep 2026 02:34:24 +0200 Subject: [PATCH 02/44] fix(multi-runner): guard optional SSM configuration --- modules/multi-runner/config.experimental.resolved.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/multi-runner/config.experimental.resolved.tf b/modules/multi-runner/config.experimental.resolved.tf index b53ddd1fbd..e31a2ac758 100644 --- a/modules/multi-runner/config.experimental.resolved.tf +++ b/modules/multi-runner/config.experimental.resolved.tf @@ -361,11 +361,11 @@ locals { ), null) minimumDaysOld = coalesce( v.storage_provider.aws.ssm.housekeeper.config.minimumDaysOld, - local.normalized_config.storage_provider.aws.ssm.housekeeper.config.minimumDaysOld, + try(local.normalized_config.storage_provider.aws.ssm.housekeeper.config.minimumDaysOld, null), ) dryRun = coalesce( v.storage_provider.aws.ssm.housekeeper.config.dryRun, - local.normalized_config.storage_provider.aws.ssm.housekeeper.config.dryRun, + try(local.normalized_config.storage_provider.aws.ssm.housekeeper.config.dryRun, null), ) } } From 911eb5aa205c213a6dc5d1c489db98c0619ecb19 Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Thu, 24 Sep 2026 11:59:52 +0200 Subject: [PATCH 03/44] fix(multi-runner): default omitted SSM settings --- .../config.experimental.resolved.tf | 65 ++++++++++++------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/modules/multi-runner/config.experimental.resolved.tf b/modules/multi-runner/config.experimental.resolved.tf index e31a2ac758..4041255221 100644 --- a/modules/multi-runner/config.experimental.resolved.tf +++ b/modules/multi-runner/config.experimental.resolved.tf @@ -302,70 +302,87 @@ locals { ssm = merge(v.storage_provider.aws.ssm, { paths = { root = "${trimsuffix(coalesce( - v.storage_provider.aws.ssm.paths.root, - local.normalized_config.storage_provider.aws.ssm.paths.root, + try(v.storage_provider.aws.ssm.paths.root, null), + try(local.normalized_config.storage_provider.aws.ssm.paths.root, null), "/github-action-runners/${var.prefix}", ), "/")}/${k}" tokens = coalesce( - v.storage_provider.aws.ssm.paths.tokens, - local.normalized_config.storage_provider.aws.ssm.paths.tokens, + try(v.storage_provider.aws.ssm.paths.tokens, null), + try(local.normalized_config.storage_provider.aws.ssm.paths.tokens, null), + "runners/tokens", ) config = coalesce( - v.storage_provider.aws.ssm.paths.config, - local.normalized_config.storage_provider.aws.ssm.paths.config, + try(v.storage_provider.aws.ssm.paths.config, null), + try(local.normalized_config.storage_provider.aws.ssm.paths.config, null), + "runners/config", ) } - tags = merge(local.normalized_config.storage_provider.aws.ssm.tags, v.storage_provider.aws.ssm.tags) + tags = merge( + try(local.normalized_config.storage_provider.aws.ssm.tags, {}), + try(v.storage_provider.aws.ssm.tags, {}), + ) parameters = { - tags = merge(local.normalized_config.storage_provider.aws.ssm.parameters.tags, v.storage_provider.aws.ssm.parameters.tags) + tags = merge( + try(local.normalized_config.storage_provider.aws.ssm.parameters.tags, {}), + try(v.storage_provider.aws.ssm.parameters.tags, {}), + ) } housekeeper = { schedule_expression = coalesce( - v.storage_provider.aws.ssm.housekeeper.schedule_expression, - local.normalized_config.storage_provider.aws.ssm.housekeeper.schedule_expression, + try(v.storage_provider.aws.ssm.housekeeper.schedule_expression, null), + try(local.normalized_config.storage_provider.aws.ssm.housekeeper.schedule_expression, null), + "rate(1 day)", ) state = coalesce( - v.storage_provider.aws.ssm.housekeeper.state, - local.normalized_config.storage_provider.aws.ssm.housekeeper.state, + try(v.storage_provider.aws.ssm.housekeeper.state, null), + try(local.normalized_config.storage_provider.aws.ssm.housekeeper.state, null), + "ENABLED", + ) + tags = merge( + try(local.normalized_config.storage_provider.aws.ssm.housekeeper.tags, {}), + try(v.storage_provider.aws.ssm.housekeeper.tags, {}), ) - tags = merge(local.normalized_config.storage_provider.aws.ssm.housekeeper.tags, v.storage_provider.aws.ssm.housekeeper.tags) lambda = { # Artifact precedence: lane ZIP, lane S3, global ZIP, then global # S3. - artifact = v.storage_provider.aws.ssm.housekeeper.lambda.artifact.zip != null ? { + artifact = try(v.storage_provider.aws.ssm.housekeeper.lambda.artifact.zip, null) != null ? { zip = v.storage_provider.aws.ssm.housekeeper.lambda.artifact.zip s3 = null - } : v.storage_provider.aws.ssm.housekeeper.lambda.artifact.s3 != null ? { + } : try(v.storage_provider.aws.ssm.housekeeper.lambda.artifact.s3, null) != null ? { zip = null s3 = v.storage_provider.aws.ssm.housekeeper.lambda.artifact.s3 - } : local.normalized_config.storage_provider.aws.ssm.housekeeper.lambda.artifact.zip != null ? { + } : try(local.normalized_config.storage_provider.aws.ssm.housekeeper.lambda.artifact.zip, null) != null ? { zip = local.normalized_config.storage_provider.aws.ssm.housekeeper.lambda.artifact.zip s3 = null } : { zip = null - s3 = local.normalized_config.storage_provider.aws.ssm.housekeeper.lambda.artifact.s3 + s3 = try(local.normalized_config.storage_provider.aws.ssm.housekeeper.lambda.artifact.s3, null) } memory_size = coalesce( - v.storage_provider.aws.ssm.housekeeper.lambda.memory_size, - local.normalized_config.storage_provider.aws.ssm.housekeeper.lambda.memory_size, + try(v.storage_provider.aws.ssm.housekeeper.lambda.memory_size, null), + try(local.normalized_config.storage_provider.aws.ssm.housekeeper.lambda.memory_size, null), + 512, ) timeout = coalesce( - v.storage_provider.aws.ssm.housekeeper.lambda.timeout, - local.normalized_config.storage_provider.aws.ssm.housekeeper.lambda.timeout, + try(v.storage_provider.aws.ssm.housekeeper.lambda.timeout, null), + try(local.normalized_config.storage_provider.aws.ssm.housekeeper.lambda.timeout, null), + 60, ) } config = { tokenPath = try(coalesce( - v.storage_provider.aws.ssm.housekeeper.config.tokenPath, + try(v.storage_provider.aws.ssm.housekeeper.config.tokenPath, null), local.normalized_config.storage_provider.aws.ssm.housekeeper.config.tokenPath, ), null) minimumDaysOld = coalesce( - v.storage_provider.aws.ssm.housekeeper.config.minimumDaysOld, + try(v.storage_provider.aws.ssm.housekeeper.config.minimumDaysOld, null), try(local.normalized_config.storage_provider.aws.ssm.housekeeper.config.minimumDaysOld, null), + 1, ) dryRun = coalesce( - v.storage_provider.aws.ssm.housekeeper.config.dryRun, + try(v.storage_provider.aws.ssm.housekeeper.config.dryRun, null), try(local.normalized_config.storage_provider.aws.ssm.housekeeper.config.dryRun, null), + false, ) } } From 6600dc96d8960a98f5eaffd8e2f91e8dbdf1c059 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 24 Sep 2026 10:00:38 +0000 Subject: [PATCH 04/44] docs: auto update terraform docs --- modules/multi-runner/README.md | 2 +- .../orchestration-providers/webhook/README.md | 2 +- .../webhook/job-retry/README.md | 2 +- .../webhook/pool/README.md | 2 +- .../webhook/scale-runners/README.md | 2 +- modules/runner-config/README.md | 4 +- .../runner-config-housekeeper/README.md | 50 +++++++++++++++++++ modules/webhook/README.md | 2 +- modules/webhook/direct/README.md | 2 +- modules/webhook/eventbridge/README.md | 2 +- 10 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 modules/runner-config/runner-config-housekeeper/README.md diff --git a/modules/multi-runner/README.md b/modules/multi-runner/README.md index 850d065838..c6d7da3f8e 100644 --- a/modules/multi-runner/README.md +++ b/modules/multi-runner/README.md @@ -167,7 +167,7 @@ module "multi-runner" { | [global\_config\_lambda](#input\_global\_config\_lambda) | Global Lambda configuration shared by all runner lanes.

global\_config\_lambda = {
artifact.s3.bucket: "S3 bucket containing Lambda deployment artifacts."
runtime: "Default Lambda runtime."
architecture: "Default Lambda instruction-set architecture."
principals: "Additional AWS principals allowed to invoke the Lambda functions."
principals.type: "Principal type, such as AWS account, service, or organization."
principals.identifiers: "Identifiers allowed for the principal type."
subnet\_ids: "Subnets used by Lambda functions."
security\_group\_ids: "Security groups attached to Lambda functions."
tags: "Tags applied to Lambda functions and related resources."
role.path: "IAM path used for Lambda execution roles."
role.permissions\_boundary: "Optional IAM permissions boundary ARN for Lambda execution roles."
} |
object({
artifact = optional(object({
s3 = optional(object({
bucket = optional(string, null)
}), {})
}), {})
runtime = optional(string, "nodejs24.x")
architecture = optional(string, "arm64")
principals = optional(list(object({
type = string
identifiers = list(string)
})), [])
subnet_ids = optional(list(string), [])
security_group_ids = optional(list(string), [])
tags = optional(map(string), {})
role = optional(object({
path = optional(string, null)
permissions_boundary = optional(string, null)
}), {})
})
| `{}` | no | | [global\_config\_observability](#input\_global\_config\_observability) | Global observability configuration shared by all runner lanes.

global\_config\_observability = {
logs.level: "Log level for module resources."
logs.retention\_in\_days: "CloudWatch log retention period in days."
logs.kms\_key\_id: "KMS key ID used to encrypt CloudWatch log groups."
logs.class: "CloudWatch log group class."
logs.tags: "Tags applied to CloudWatch log groups."
tracing.mode: "Tracing mode used by instrumented resources."
tracing.capture\_http\_requests: "Whether HTTP requests are captured by tracing."
tracing.capture\_error: "Whether errors are captured by tracing."
metrics.enabled: "Whether module metrics are enabled."
metrics.namespace: "CloudWatch namespace used for module metrics."
metrics.metric.github\_app\_rate\_limit.enabled: "Whether GitHub App rate-limit metrics are emitted."
metrics.metric.job\_retry.enabled: "Whether job-retry metrics are emitted."
metrics.metric.spot\_termination\_warning.enabled: "Whether spot-termination warning metrics are emitted."
} |
object({
logs = optional(object({
level = optional(string, "info")
retention_in_days = optional(number, 180)
kms_key_id = optional(string, null)
class = optional(string, "STANDARD")
tags = optional(map(string), {})
}), {})
tracing = optional(object({
mode = optional(string, null)
capture_http_requests = optional(bool, false)
capture_error = optional(bool, false)
}), {})
metrics = optional(object({
enabled = optional(bool, false)
namespace = optional(string, "GitHub Runners")
metric = optional(object({
github_app_rate_limit = optional(object({
enabled = optional(bool, true)
}), {})
job_retry = optional(object({
enabled = optional(bool, true)
}), {})
spot_termination_warning = optional(object({
enabled = optional(bool, true)
}), {})
}), {})
}), {})
})
| `{}` | no | | [global\_config\_orchestration\_provider](#input\_global\_config\_orchestration\_provider) | Global orchestration-provider configuration shared by all runner lanes.

global\_config\_orchestration\_provider = {
webhook: {
queue\_selection\_strategy: "Strategy used to select the build queue for a webhook event."
eventbridge.enabled: "Whether EventBridge integration is enabled for webhook events."
eventbridge.accept\_events: "Event types accepted by the EventBridge integration."
matcher\_config\_parameter\_store\_tier: "SSM Parameter Store tier used for matcher configuration."
runner.boot\_time\_in\_minutes: "Expected runner boot time used by orchestration."
runner.ephemeral: "Whether runners created by the orchestration provider are ephemeral."
runner.jit\_config\_enabled: "Whether JIT runner configuration is enabled."
runner.maximum\_count: "Maximum number of runners that orchestration may create."
github.repository\_white\_list: "Repositories allowed to use the webhook configuration."
lambda.artifact.zip: "Local ZIP artifact used for orchestration Lambda functions."
lambda.artifact.s3.key: "S3 object key for the orchestration Lambda artifact."
lambda.artifact.s3.object\_version: "Optional S3 object version for the orchestration Lambda artifact."
lambda.scale.up.memory\_size: "Memory allocated to the scale-up Lambda."
lambda.scale.up.timeout: "Timeout in seconds for the scale-up Lambda."
lambda.scale.up.reserved\_concurrent\_executions: "Reserved concurrent executions for the scale-up Lambda."
lambda.scale.up.job\_queued\_check\_enabled: "Whether the scale-up Lambda checks queued jobs."
lambda.scale.up.event\_source\_mapping.batch\_size: "Maximum records passed to one scale-up Lambda invocation."
lambda.scale.up.event\_source\_mapping.maximum\_batching\_window\_in\_seconds: "Maximum time to batch records before invoking the scale-up Lambda."
lambda.scale.up.tags: "Tags applied to the scale-up Lambda."
lambda.scale.down.memory\_size: "Memory allocated to the scale-down Lambda."
lambda.scale.down.timeout: "Timeout in seconds for the scale-down Lambda."
lambda.scale.down.schedule\_expression: "Schedule expression for scale-down processing."
lambda.scale.down.minimum\_running\_time\_in\_minutes: "Minimum runner lifetime before scale-down."
lambda.scale.down.idle\_confirmation\_seconds: "Seconds a runner must consistently report not-busy before scale-down terminates it; 0 disables the confirmation window."
lambda.scale.down.idle\_config: "Scheduled minimum idle-runner pool settings."
lambda.scale.down.idle\_config.cron: "Cron expression defining when the idle-runner count applies."
lambda.scale.down.idle\_config.timeZone: "Time zone used to evaluate the idle-runner schedule."
lambda.scale.down.idle\_config.idleCount: "Minimum number of idle runners maintained during the schedule."
lambda.scale.down.idle\_config.evictionStrategy: "Strategy used when evicting idle runners."
lambda.scale.down.tags: "Tags applied to the scale-down Lambda."
lambda.webhook.artifact.zip: "Local ZIP artifact used for the webhook Lambda."
lambda.webhook.artifact.s3.key: "S3 object key for the webhook Lambda artifact."
lambda.webhook.artifact.s3.object\_version: "Optional S3 object version for the webhook Lambda artifact."
lambda.webhook.api\_gateway\_access\_log\_settings: "API Gateway access-log destination and format."
lambda.webhook.api\_gateway\_access\_log\_settings.destination\_arn: "ARN of the API Gateway access-log destination."
lambda.webhook.api\_gateway\_access\_log\_settings.format: "API Gateway access-log format."
lambda.webhook.memory\_size: "Memory allocated to the webhook Lambda."
lambda.webhook.timeout: "Timeout in seconds for the webhook Lambda."
lambda.webhook.tags: "Tags applied to the webhook Lambda."
lambda.pool.memory\_size: "Memory allocated to the pool Lambda."
lambda.pool.timeout: "Timeout in seconds for the pool Lambda."
lambda.pool.reserved\_concurrent\_executions: "Reserved concurrent executions for the pool Lambda."
lambda.pool.config: "Scheduled runner-pool size configuration."
lambda.pool.config.schedule\_expression: "Schedule expression for the pool size."
lambda.pool.config.schedule\_expression\_timezone: "Time zone used to evaluate the pool schedule."
lambda.pool.config.size: "Runner pool size applied by the schedule."
lambda.pool.include\_busy\_runners: "Whether busy runners are included in pool sizing."
lambda.pool.runner\_owner: "GitHub organization that owns the runner pool."
lambda.pool.tags: "Tags applied to the pool Lambda."
queue.delay\_webhook\_event: "Seconds a webhook event remains invisible in the build queue before processing."
queue.job\_queue\_retention\_in\_seconds: "Seconds a queued job is retained before it is purged."
queue.visibility\_timeout\_seconds: "Build queue visibility timeout in seconds."
queue.redrive\_build\_queue.enabled: "Whether the build queue dead-letter queue is enabled."
queue.redrive\_build\_queue.maxReceiveCount: "Maximum receives before a message is moved to the dead-letter queue."
queue.tags: "Tags applied to build queues."
queue.encryption.kms\_data\_key\_reuse\_period\_seconds: "KMS data-key reuse period for queue encryption."
queue.encryption.kms\_master\_key\_id: "KMS key ID used for queue encryption."
queue.encryption.sqs\_managed\_sse\_enabled: "Whether SQS-managed server-side encryption is enabled."
}
} |
object({
webhook = optional(object({
queue_selection_strategy = optional(string, "first")
eventbridge = optional(object({
enabled = optional(bool, true)
accept_events = optional(list(string), [])
}), {})
matcher_config_parameter_store_tier = optional(string, "Standard")
runner = optional(object({
boot_time_in_minutes = optional(number, 5)
ephemeral = optional(bool, false)
jit_config_enabled = optional(bool, null)
maximum_count = optional(number, null)
}), {})

github = optional(object({
repository_white_list = optional(list(string), [])
}), {})

lambda = optional(object({
artifact = optional(object({
zip = optional(string, null)
s3 = optional(object({
key = string
object_version = optional(string, null)
}), null)
}), {})
scale = optional(object({
up = optional(object({
memory_size = optional(number, 512)
timeout = optional(number, 30)
reserved_concurrent_executions = optional(number, 1)
job_queued_check_enabled = optional(bool, null)
event_source_mapping = optional(object({
batch_size = optional(number, 10)
maximum_batching_window_in_seconds = optional(number, 0)
}), {})
tags = optional(map(string), {})
}), {})
down = optional(object({
memory_size = optional(number, 512)
timeout = optional(number, 60)
schedule_expression = optional(string, "cron(*/5 * * * ? *)")
minimum_running_time_in_minutes = optional(number, null)
idle_confirmation_seconds = optional(number, 0)
idle_config = optional(list(object({
cron = string
timeZone = string
idleCount = number
evictionStrategy = optional(string, "oldest_first")
})), [])
tags = optional(map(string), {})
}), {})
}), {})
webhook = optional(object({
artifact = optional(object({
zip = optional(string, null)
s3 = optional(object({
key = string
object_version = optional(string, null)
}), null)
}), {})
api_gateway_access_log_settings = optional(object({
destination_arn = string
format = string
}), null)
memory_size = optional(number, 256)
timeout = optional(number, 10)
tags = optional(map(string), {})
}), {})
pool = optional(object({
memory_size = optional(number, 512)
timeout = optional(number, 60)
reserved_concurrent_executions = optional(number, 1)
config = optional(list(object({
schedule_expression = string
schedule_expression_timezone = optional(string)
size = number
})), [])
include_busy_runners = optional(bool, false)
runner_owner = optional(string, null)
tags = optional(map(string), {})
}), {})
}), {})

queue = optional(object({
delay_webhook_event = optional(number, 30)
job_queue_retention_in_seconds = optional(number, 86400)
visibility_timeout_seconds = optional(number, 180)
redrive_build_queue = optional(object({
enabled = optional(bool, false)
maxReceiveCount = optional(number, null)
}), {
enabled = false
maxReceiveCount = null
})
tags = optional(map(string), {})
encryption = optional(object({
kms_data_key_reuse_period_seconds = number
kms_master_key_id = string
sqs_managed_sse_enabled = bool
}), {
kms_data_key_reuse_period_seconds = null
kms_master_key_id = null
sqs_managed_sse_enabled = true
})
}), {})
}), {})
})
| `{}` | no | -| [global\_config\_storage\_provider](#input\_global\_config\_storage\_provider) | Global storage-provider configuration shared by all runner lanes.

global\_config\_storage\_provider = {
aws.ssm.paths.root: "Root path for SSM parameters."
aws.ssm.paths.app: "Path segment for application parameters."
aws.ssm.paths.webhook: "Path segment for webhook parameters."
aws.ssm.paths.tokens: "Path segment for runner token parameters."
aws.ssm.paths.config: "Path segment for runner configuration parameters."
aws.ssm.kms\_key\_id: "KMS key ID used to encrypt SSM parameters."
aws.ssm.tags: "Tags applied to SSM resources."
aws.ssm.parameters.tags: "Tags applied to runner configuration parameters."
aws.ssm.housekeeper.schedule\_expression: "Schedule for the SSM parameter housekeeper."
aws.ssm.housekeeper.state: "EventBridge rule state for the SSM housekeeper."
aws.ssm.housekeeper.tags: "Tags applied to the SSM housekeeper resources."
aws.ssm.housekeeper.lambda.artifact.zip: "Local ZIP artifact used for the SSM housekeeper Lambda."
aws.ssm.housekeeper.lambda.artifact.s3.key: "S3 object key for the SSM housekeeper Lambda."
aws.ssm.housekeeper.lambda.artifact.s3.object\_version: "Optional S3 object version for the SSM housekeeper artifact."
aws.ssm.housekeeper.lambda.memory\_size: "Memory allocated to the SSM housekeeper Lambda."
aws.ssm.housekeeper.lambda.timeout: "Timeout in seconds for the SSM housekeeper Lambda."
aws.ssm.housekeeper.config.tokenPath: "Parameter path containing runner tokens to clean up."
aws.ssm.housekeeper.config.minimumDaysOld: "Minimum age in days before an old token is eligible for cleanup."
aws.ssm.housekeeper.config.dryRun: "Whether the SSM housekeeper reports cleanup without deleting parameters."
} |
object({
aws = optional(object({
ssm = optional(object({
paths = optional(object({
root = optional(string, null)
app = optional(string, "app")
webhook = optional(string, "webhook")
tokens = optional(string, "runners/tokens")
config = optional(string, "runners/config")
}), {})
kms_key_id = optional(string, null)
tags = optional(map(string), {})
parameters = optional(object({
tags = optional(map(string), {})
}), {})
housekeeper = optional(object({
schedule_expression = optional(string, "rate(1 day)")
state = optional(string, "ENABLED")
tags = optional(map(string), {})
lambda = optional(object({
artifact = optional(object({
zip = optional(string, null)
s3 = optional(object({
key = string
object_version = optional(string, null)
}), null)
}), {})
memory_size = optional(number, 512)
timeout = optional(number, 60)
}), {})
config = optional(object({
tokenPath = optional(string, null)
minimumDaysOld = optional(number, 1)
dryRun = optional(bool, false)
}), {})
}), {})
}), {})
}), {})
})
| `{}` | no | +| [global\_config\_storage\_provider](#input\_global\_config\_storage\_provider) | Global storage-provider configuration shared by all runner lanes.

global\_config\_storage\_provider = {
aws.ssm.paths.root: "Root path for SSM parameters."
aws.ssm.paths.app: "Path segment for application parameters."
aws.ssm.paths.webhook: "Path segment for webhook parameters."
aws.ssm.paths.tokens: "Path segment for runner token parameters."
aws.ssm.paths.config: "Path segment for runner configuration parameters."
aws.ssm.kms\_key\_id: "KMS key ID used to encrypt SSM parameters."
aws.ssm.tags: "Tags applied to SSM resources."
aws.ssm.parameters.tags: "Tags applied to runner configuration parameters."
aws.ssm.housekeeper.schedule\_expression: "Schedule for the SSM parameter housekeeper."
aws.ssm.housekeeper.state: "EventBridge rule state for the SSM housekeeper."
aws.ssm.housekeeper.tags: "Tags applied to the SSM housekeeper resources."
aws.ssm.housekeeper.lambda.artifact.zip: "Local ZIP artifact used for the SSM housekeeper Lambda."
aws.ssm.housekeeper.lambda.artifact.s3.key: "S3 object key for the SSM housekeeper Lambda."
aws.ssm.housekeeper.lambda.artifact.s3.object\_version: "Optional S3 object version for the SSM housekeeper artifact."
aws.ssm.housekeeper.lambda.memory\_size: "Memory allocated to the SSM housekeeper Lambda."
aws.ssm.housekeeper.lambda.timeout: "Timeout in seconds for the SSM housekeeper Lambda."
aws.ssm.housekeeper.config.tokenPath: "Parameter path containing runner tokens to clean up."
aws.ssm.housekeeper.config.minimumDaysOld: "Minimum age in days before an old token is eligible for cleanup."
aws.ssm.housekeeper.config.dryRun: "Whether the SSM housekeeper reports cleanup without deleting parameters."
} |
object({
aws = optional(object({
ssm = optional(object({
paths = optional(object({
root = optional(string, null)
app = optional(string, "app")
webhook = optional(string, "webhook")
tokens = optional(string, "runners/tokens")
config = optional(string, "runners/config")
}), {})
kms_key_id = optional(string, null)
tags = optional(map(string), {})
parameters = optional(object({
tags = optional(map(string), {})
}), {})
housekeeper = optional(object({
schedule_expression = optional(string, "rate(1 day)")
state = optional(string, "ENABLED")
tags = optional(map(string), {})
lambda = optional(object({
artifact = optional(object({
zip = optional(string, null)
s3 = optional(object({
key = string
object_version = optional(string, null)
}), null)
}), {})
memory_size = optional(number, 512)
timeout = optional(number, 60)
}), {})
config = optional(object({
tokenPath = optional(string, null)
minimumDaysOld = optional(number, 1)
dryRun = optional(bool, false)
}), {})
}), {})
}), null)
}), {})
})
| `{}` | no | | [iam\_overrides](#input\_iam\_overrides) | This map provides the possibility to override some IAM defaults. The following attributes are supported: `instance_profile_name` overrides the instance profile name used in the launch template. `runner_role_arn` overrides the IAM role ARN used for the runner instances. |
object({
override_instance_profile = optional(bool, null)
instance_profile_name = optional(string, null)
override_runner_role = optional(bool, null)
runner_role_arn = optional(string, null)
})
|
{
"instance_profile_name": null,
"override_instance_profile": false,
"override_runner_role": false,
"runner_role_arn": null
}
| no | | [instance\_profile\_path](#input\_instance\_profile\_path) | The path that will be added to the instance\_profile, if not set the environment name will be used. | `string` | `null` | no | | [instance\_termination\_watcher](#input\_instance\_termination\_watcher) | Configuration for the spot termination watcher lambda function. This feature is Beta, changes will not trigger a major release as long in beta.

`enable`: Enable or disable the spot termination watcher.
`enable_runner_deregistration`: Enable or disable deregistering the runner from GitHub when its EC2 instance is terminated.
`environment_variables`: Additional environment variables to merge into the Lambda configuration.
`memory_size`: Memory size limit in MB of the lambda.
`s3_key`: S3 key for syncer lambda function. Required if using S3 bucket to specify lambdas.
`s3_object_version`: S3 object version for syncer lambda function. Useful if S3 versioning is enabled on source bucket.
`timeout`: Time out of the lambda in seconds.
`zip`: File location of the lambda zip file. |
object({
enable = optional(bool, false)
features = optional(object({
enable_spot_termination_handler = optional(bool, true)
enable_spot_termination_notification_watcher = optional(bool, true)
}), {})
enable_runner_deregistration = optional(bool, true)
environment_variables = optional(map(string), {})
memory_size = optional(number, null)
s3_key = optional(string, null)
s3_object_version = optional(string, null)
timeout = optional(number, null)
zip = optional(string, null)
})
| `{}` | no | diff --git a/modules/orchestration-providers/webhook/README.md b/modules/orchestration-providers/webhook/README.md index a03e8b4f1d..c4ff48f7bf 100644 --- a/modules/orchestration-providers/webhook/README.md +++ b/modules/orchestration-providers/webhook/README.md @@ -46,7 +46,7 @@ The scale-down lifecycle is documented in the [scale-down state diagram](./scale | [prefix](#input\_prefix) | Prefix used to identify resources created for this webhook orchestration provider. | `string` | n/a | yes | | [runner](#input\_runner) | Common runner registration values consumed by webhook demand controls. Lifecycle, boot timeout, and capacity remain provider-owned under config.runner. |
object({
os = string
auto_update_disabled = bool
labels = list(string)
group_name = string
name_prefix = string
})
| n/a | yes | | [runner\_provider](#input\_runner\_provider) | Selected compute-provider capabilities consumed by webhook scale-up, scale-down, and pool controls. |
object({
type = string
scale_up = object({
environment_variables = map(string)
iam_policy_json = string
additional_iam_policy_json = optional(string, null)
managed_policy = optional(object({
arn = string
}), null)
})
scale_down = object({
environment_variables = map(string)
iam_policy_json = string
})
pool = object({
environment_variables = map(string)
iam_policy_json = string
managed_policy_enabled = bool
managed_policy_arn = optional(string, null)
})
})
| n/a | yes | -| [storage\_provider](#input\_storage\_provider) | Resolved storage-provider values and optional provider-owned Lambda capabilities.

- `storage_provider.aws.ssm.token_path`: Resolved Parameter Store path for registration tokens.
- `storage_provider.aws.ssm.token_path_arn`: ARN of the registration-token Parameter Store path.
- `storage_provider.aws.ssm.config_path`: Resolved Parameter Store path for runner configuration.
- `storage_provider.aws.ssm.config_path_arn`: ARN of the runner-configuration Parameter Store path.
- `storage_provider.aws.ssm.kms_key_id`: Optional KMS key used to decrypt shared parameters.
- `storage_provider.aws.ssm.parameter_store_tags`: JSON-encoded tags applied to runtime parameters.
- `scale_up`, `scale_down`, `pool`, and `job_retry`: Provider-owned environment variables and IAM policy fragments. |
object({
aws = object({
ssm = object({
token_path = string
token_path_arn = string
config_path = string
config_path_arn = string
kms_key_id = optional(string, null)
parameter_store_tags = string
})
})
scale_up = optional(object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
}), {
environment_variables = {}
iam_policy_json = null
})
scale_down = optional(object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
}), {
environment_variables = {}
iam_policy_json = null
})
pool = optional(object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
}), {
environment_variables = {}
iam_policy_json = null
})
job_retry = optional(object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
}), {
environment_variables = {}
iam_policy_json = null
})
})
| n/a | yes | +| [storage\_provider](#input\_storage\_provider) | Resolved storage-provider values and optional provider-owned Lambda capabilities.

- `storage_provider.aws.ssm.token_path`: Resolved Parameter Store path for registration tokens.
- `storage_provider.aws.ssm.token_path_arn`: ARN of the registration-token Parameter Store path.
- `storage_provider.aws.ssm.config_path`: Resolved Parameter Store path for runner configuration.
- `storage_provider.aws.ssm.config_path_arn`: ARN of the runner-configuration Parameter Store path.
- `storage_provider.aws.ssm.kms_key_id`: Optional KMS key used to decrypt shared parameters.
- `storage_provider.aws.ssm.parameter_store_tags`: JSON-encoded tags applied to runtime parameters.
- `scale_up`, `scale_down`, `pool`, and `job_retry`: Provider-owned environment variables and IAM policy fragments. |
object({
aws = object({
ssm = optional(object({
token_path = string
token_path_arn = string
config_path = string
config_path_arn = string
kms_key_id = optional(string, null)
parameter_store_tags = string
}), null)
})
scale_up = optional(object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
}), {
environment_variables = {}
iam_policy_json = null
})
scale_down = optional(object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
}), {
environment_variables = {}
iam_policy_json = null
})
pool = optional(object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
}), {
environment_variables = {}
iam_policy_json = null
})
job_retry = optional(object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
}), {
environment_variables = {}
iam_policy_json = null
})
})
| n/a | yes | | [tags](#input\_tags) | Base tags available to webhook-provider resources. Component-specific tags override this map within their documented scopes. | `map(string)` | `{}` | no | ## Outputs diff --git a/modules/orchestration-providers/webhook/job-retry/README.md b/modules/orchestration-providers/webhook/job-retry/README.md index 03836db4d8..42f520dd7a 100644 --- a/modules/orchestration-providers/webhook/job-retry/README.md +++ b/modules/orchestration-providers/webhook/job-retry/README.md @@ -53,7 +53,7 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [config](#input\_config) | Provider-neutral job-retry configuration assembled by runner-config.

- `prefix`: Prefix used to name job-retry resources.
- `aws_partition`: AWS partition used to construct the Lambda VPC managed-policy ARN.
- `lambda.artifact.zip`: Resolved local control-plane archive.
- `lambda.artifact.s3.bucket`: Optional S3 bucket containing the Lambda archive.
- `lambda.artifact.s3.key`: Object key of the Lambda archive.
- `lambda.artifact.s3.object_version`: Optional object version of the Lambda archive.
- `lambda.runtime`: Runtime used by the job-retry Lambda.
- `lambda.architecture`: Instruction-set architecture used by the job-retry Lambda.
- `lambda.memory_size`: Memory allocated to the job-retry Lambda.
- `lambda.timeout`: Lambda timeout and retry-queue visibility timeout in seconds.
- `lambda.reserved_concurrent_executions`: Reserved concurrency for the Lambda. Use `-1` for unreserved concurrency.
- `lambda.environment_variables`: Additional Lambda environment variables. Required job-retry variables override matching keys.
- `lambda.vpc.subnet_ids`: Subnets used for Lambda VPC configuration.
- `lambda.vpc.security_group_ids`: Security groups used for Lambda VPC configuration.
- `lambda.role.path`: IAM path used for the job-retry Lambda role.
- `lambda.role.permissions_boundary`: Optional permissions boundary for the Lambda role.
- `lambda.role.principals`: Extra principals allowed to assume the Lambda role, for example during local testing.
- `runner.name_prefix`: Prefix used to identify runners belonging to this runner configuration.
- `github.organization_runners`: Enables organization runners.
- `github.enterprise_server.url`: Optional GitHub Enterprise Server URL.
- `github.enterprise_server.ssl_verify`: Enables TLS certificate verification for GitHub Enterprise Server requests.
- `github.user_agent`: Optional User-Agent sent to GitHub.
- `github.app_parameters.key_base64`: Parameter Store reference for the primary GitHub App private key.
- `github.app_parameters.id`: Parameter Store reference for the primary GitHub App ID.
- `github.app_parameters.additional_apps_manifest`: Optional Parameter Store reference containing the additional GitHub App manifest.
- `github.app_parameters.additional_app_parameter_arns`: ARNs of the additional GitHub App credential parameters.
- `queue.build`: URL and ARN of the build queue to which retry messages are published.
- `queue.kms_key_id`: Optional KMS key ARN used to encrypt the build queue. This is distinct from the Parameter Store key.
- `queue.event_source_mapping.batch_size`: Maximum records delivered per job-retry invocation.
- `queue.event_source_mapping.maximum_batching_window_in_seconds`: Maximum event batching window.
- `queue.encryption`: Server-side encryption configuration for the retry queue.
- `storage_provider.aws.ssm.kms_key_id`: Optional KMS key ARN used by the job-retry IAM policy. Its value may be unknown until apply.
- `observability.logs`: Logging level, retention, encryption, and log-class configuration.
- `observability.tracing`: Lambda X-Ray and tracing-helper configuration.
- `observability.metrics`: Metrics enablement, namespace, and job-retry metric configuration.
- `tags.resources`: Tags for the job-retry Lambda role and component resources.
- `tags.lambda`: Tags for the job-retry Lambda function.
- `tags.log_group`: Tags for the job-retry log group.
- `tags.queue`: Tags for the retry queue.
- `tags.event_source_mapping`: Tags for the retry-queue event-source mapping. |
object({
prefix = string
aws_partition = string
lambda = object({
artifact = object({
zip = string
s3 = object({
bucket = optional(string, null)
key = optional(string, null)
object_version = optional(string, null)
})
})
runtime = string
architecture = string
memory_size = number
timeout = number
reserved_concurrent_executions = number
environment_variables = map(string)
vpc = object({
subnet_ids = list(string)
security_group_ids = list(string)
})
role = object({
path = string
permissions_boundary = optional(string, null)
principals = list(object({
type = string
identifiers = list(string)
}))
})
})
runner = object({
name_prefix = string
})
github = object({
organization_runners = bool
enterprise_server = object({
url = optional(string, null)
ssl_verify = optional(bool, true)
})
user_agent = optional(string, null)
app_parameters = object({
key_base64 = map(string)
id = map(string)
additional_apps_manifest = optional(object({
name = string
arn = string
}), null)
additional_app_parameter_arns = optional(list(string), [])
})
})
queue = object({
build = object({
url = string
arn = string
})
kms_key_id = optional(string, null)
event_source_mapping = object({
batch_size = number
maximum_batching_window_in_seconds = number
})
encryption = object({
sqs_managed_sse_enabled = bool
kms_master_key_id = optional(string, null)
kms_data_key_reuse_period_seconds = optional(number, null)
})
})
observability = object({
logs = object({
level = string
retention_in_days = number
kms_key_id = optional(string, null)
class = string
})
tracing = object({
mode = optional(string, null)
capture_http_requests = bool
capture_error = bool
})
metrics = object({
enabled = bool
namespace = string
metric = object({
github_app_rate_limit = object({
enabled = bool
})
job_retry = object({
enabled = bool
})
})
})
})
tags = object({
resources = map(string)
lambda = map(string)
log_group = map(string)
queue = map(string)
event_source_mapping = map(string)
})
})
| n/a | yes | -| [storage\_provider](#input\_storage\_provider) | Resolved storage-provider configuration and capability used by the job-retry Lambda. |
object({
aws = object({
ssm = object({
kms_key_id = optional(string, null)
})
})
environment_variables = optional(map(string), {})
iam_policy_json = optional(string, null)
})
| n/a | yes | +| [storage\_provider](#input\_storage\_provider) | Resolved storage-provider configuration and capability used by the job-retry Lambda. |
object({
aws = object({
ssm = optional(object({
kms_key_id = optional(string, null)
}), null)
})
environment_variables = optional(map(string), {})
iam_policy_json = optional(string, null)
})
| n/a | yes | ## Outputs diff --git a/modules/orchestration-providers/webhook/pool/README.md b/modules/orchestration-providers/webhook/pool/README.md index a6f7a0e086..48d7af5203 100644 --- a/modules/orchestration-providers/webhook/pool/README.md +++ b/modules/orchestration-providers/webhook/pool/README.md @@ -56,7 +56,7 @@ No modules. | [aws\_partition](#input\_aws\_partition) | (optional) partition for the arn if not 'aws' | `string` | `"aws"` | no | | [config](#input\_config) | Configuration passed from the webhook orchestration provider to the pool Lambda and scheduler.

- `lambda`: Pool Lambda runtime and deployment configuration.
- `lambda.log_level`: Logging level used by the pool Lambda.
- `lambda.logging_retention_in_days`: Number of days to retain events in the pool Lambda log group.
- `lambda.logging_kms_key_id`: KMS key ID used to encrypt the pool Lambda log group.
- `lambda.log_class`: CloudWatch Logs class for the pool Lambda log group.
- `lambda.reserved_concurrent_executions`: Reserved concurrency for the pool Lambda. Use -1 for no reservation.
- `lambda.s3_bucket`: S3 bucket containing the pool Lambda deployment package.
- `lambda.s3_key`: S3 key of the pool Lambda deployment package.
- `lambda.s3_object_version`: S3 object version of the pool Lambda deployment package.
- `lambda.security_group_ids`: Security group IDs associated with the pool Lambda.
- `lambda.runtime`: AWS Lambda runtime used by the pool Lambda.
- `lambda.architecture`: AWS Lambda architecture used by the pool Lambda.
- `lambda.memory_size`: Memory allocated to the pool Lambda in MB.
- `lambda.timeout`: Pool Lambda timeout in seconds.
- `lambda.zip`: Local path to the pool Lambda deployment package when S3 is not used.
- `lambda.subnet_ids`: Subnet IDs in which the pool Lambda runs.
- `lambda.principals`: Additional principals allowed to assume the pool Lambda role.
- `tags`: Common tags added to pool resources.
- `ghes`: GitHub Enterprise Server connection configuration.
- `ghes.url`: GitHub Enterprise Server URL; null when using public GitHub.
- `ghes.ssl_verify`: Whether the pool Lambda verifies the GitHub Enterprise Server TLS certificate.
- `github_app_parameters`: SSM parameter metadata for the primary and additional GitHub App credentials.
- `github_app_parameters.key_base64`: Parameter Store reference for the primary GitHub App private key.
- `github_app_parameters.id`: Parameter Store reference for the primary GitHub App ID.
- `github_app_parameters.additional_apps_manifest`: Optional Parameter Store reference containing the additional GitHub App manifest.
- `github_app_parameters.additional_app_parameter_arns`: ARNs of the additional GitHub App credential parameters.
- `runner`: Runner registration configuration used by the pool Lambda.
- `runner.disable_runner_autoupdate`: Whether GitHub runner automatic updates are disabled.
- `runner.ephemeral`: Whether runners register as ephemeral runners.
- `runner.enable_jit_config`: Whether runners use just-in-time registration configuration.
- `runner.labels`: Labels assigned to runners created by the pool Lambda.
- `runner.group_name`: GitHub runner group assigned to runners created by the pool Lambda.
- `runner.name_prefix`: Prefix used for runner names.
- `runner.pool_owner`: GitHub organization or repository that owns the runner pool.
- `runner.boot_time_in_minutes`: Webhook-provider runner boot timeout used by pool reconciliation.
- `runners_maximum_count`: Webhook-provider runner capacity limit enforced by the pool Lambda.
- `prefix`: Prefix used to name pool resources.
- `pool`: Scheduled pool targets.
- `pool[*].schedule_expression`: EventBridge Scheduler expression for a pool target.
- `pool[*].schedule_expression_timezone`: Time zone used to evaluate the schedule expression.
- `pool[*].size`: Desired runner count for the scheduled pool target.
- `include_busy_runners`: Whether busy runners count toward the desired pool size.
- `role_permissions_boundary`: Permissions boundary applied to IAM roles created for the pool.
- `role_path`: IAM path applied to roles created for the pool.
- `lambda_tags`: Tags added specifically to the pool Lambda function, overriding common tags with the same key.
- `log_group_tags`: Tags added specifically to the pool Lambda log group, overriding common tags with the same key.
- `user_agent`: User-Agent header used for GitHub API requests. |
object({
lambda = object({
log_level = string
logging_retention_in_days = number
logging_kms_key_id = string
log_class = string
reserved_concurrent_executions = number
s3_bucket = string
s3_key = string
s3_object_version = string
security_group_ids = list(string)
runtime = string
architecture = string
memory_size = number
timeout = number
zip = string
subnet_ids = list(string)
principals = optional(list(object({
type = string
identifiers = list(string)
})), [])
})
tags = map(string)
ghes = object({
url = string
ssl_verify = string
})
github_app_parameters = object({
key_base64 = map(string)
id = map(string)
additional_apps_manifest = optional(object({
name = string
arn = string
}), null)
additional_app_parameter_arns = optional(list(string), [])
})
runner = object({
disable_runner_autoupdate = bool
ephemeral = bool
enable_jit_config = bool
labels = list(string)
group_name = string
name_prefix = string
pool_owner = string
boot_time_in_minutes = number
})
runners_maximum_count = number
prefix = string
pool = list(object({
schedule_expression = string
schedule_expression_timezone = string
size = number
}))
include_busy_runners = bool
role_permissions_boundary = string
role_path = string
lambda_tags = map(string)
log_group_tags = optional(map(string), {})
user_agent = string
})
| n/a | yes | | [runner\_provider](#input\_runner\_provider) | Compute provider integration used by the pool Lambda.

- `type`: Compute provider type passed to scheduled pool invocations.
- `environment_variables`: Provider-specific environment variables added to the pool Lambda.
- `iam_policy_json`: Provider-specific IAM policy document merged into the pool Lambda policy.
- `managed_policy_enabled`: Whether to attach a provider-specific managed IAM policy to the pool Lambda role.
- `managed_policy_arn`: ARN of the provider-specific managed IAM policy to attach when enabled. |
object({
type = string
environment_variables = map(string)
iam_policy_json = string
managed_policy_enabled = bool
managed_policy_arn = optional(string, null)
})
| n/a | yes | -| [storage\_provider](#input\_storage\_provider) | Resolved storage-provider configuration and capability used by the pool Lambda. |
object({
aws = object({
ssm = object({
token_path = string
token_path_arn = string
config_path = string
config_path_arn = string
kms_key_id = optional(string, null)
parameter_store_tags = string
})
})
environment_variables = optional(map(string), {})
iam_policy_json = optional(string, null)
})
| n/a | yes | +| [storage\_provider](#input\_storage\_provider) | Resolved storage-provider configuration and capability used by the pool Lambda. |
object({
aws = object({
ssm = optional(object({
token_path = string
token_path_arn = string
config_path = string
config_path_arn = string
kms_key_id = optional(string, null)
parameter_store_tags = string
}), null)
})
environment_variables = optional(map(string), {})
iam_policy_json = optional(string, null)
})
| n/a | yes | | [tracing\_config](#input\_tracing\_config) | Tracing configuration for the pool Lambda.

- `mode`: AWS X-Ray tracing mode. A null value disables tracing.
- `capture_http_requests`: Whether Powertools tracing captures outgoing HTTP requests.
- `capture_error`: Whether Powertools tracing captures errors as tracing metadata. |
object({
mode = optional(string, null)
capture_http_requests = optional(bool, false)
capture_error = optional(bool, false)
})
| `{}` | no | ## Outputs diff --git a/modules/orchestration-providers/webhook/scale-runners/README.md b/modules/orchestration-providers/webhook/scale-runners/README.md index 87e3c1cefb..50fa51e79a 100644 --- a/modules/orchestration-providers/webhook/scale-runners/README.md +++ b/modules/orchestration-providers/webhook/scale-runners/README.md @@ -69,7 +69,7 @@ No modules. | [aws\_partition](#input\_aws\_partition) | AWS partition used to construct IAM policy ARNs. | `string` | `"aws"` | no | | [config](#input\_config) | Provider-neutral scale-up and scale-down configuration assembled by runner-config.

- `prefix`: Prefix used to name scaling resources.
- `lambda.artifact.zip`: Resolved local control-plane archive.
- `lambda.artifact.s3.bucket`: Optional S3 bucket containing the Lambda archive.
- `lambda.artifact.s3.key`: Object key of the Lambda archive.
- `lambda.artifact.s3.object_version`: Optional object version of the Lambda archive.
- `lambda.runtime`: Runtime used by both scaling Lambdas.
- `lambda.architecture`: Instruction-set architecture used by both scaling Lambdas.
- `lambda.vpc.subnet_ids`: Subnets used for Lambda VPC configuration.
- `lambda.vpc.security_group_ids`: Security groups used for Lambda VPC configuration.
- `lambda.role.path`: IAM path used for the scaling Lambda roles.
- `lambda.role.permissions_boundary`: Optional permissions boundary for the scaling Lambda roles.
- `lambda.role.principals`: Additional principals allowed to assume the scaling Lambda roles.
- `runner.os`: Runner operating system used for the minimum-runtime default.
- `runner.auto_update_disabled`: Disables the GitHub runner application's built-in updater.
- `runner.ephemeral`: Registers runners in ephemeral mode.
- `runner.jit_config_enabled`: Enables or disables just-in-time runner configuration.
- `runner.labels`: Labels supplied when a runner is registered.
- `runner.group_name`: GitHub runner group used during registration.
- `runner.name_prefix`: Prefix added to registered runner names.
- `runner.boot_time_in_minutes`: Webhook-provider runner boot timeout used by scale-down.
- `runner.maximum_count`: Webhook-provider runner capacity limit for this runner configuration.
- `github.organization_runners`: Registers organization runners when true.
- `github.enterprise_server.url`: Optional GitHub Enterprise Server URL.
- `github.enterprise_server.ssl_verify`: Enables TLS verification for GitHub Enterprise Server.
- `github.user_agent`: Optional User-Agent sent to GitHub.
- `github.app_parameters.key_base64`: Parameter Store reference for the primary GitHub App private key.
- `github.app_parameters.id`: Parameter Store reference for the primary GitHub App ID.
- `github.app_parameters.additional_apps_manifest`: Optional Parameter Store reference containing the additional GitHub App manifest.
- `github.app_parameters.additional_app_parameter_arns`: ARNs of the additional GitHub App credential parameters.
- `queue.build.arn`: ARN of the build queue consumed by scale-up.
- `queue.kms_key_id`: Optional KMS key ARN used to encrypt the build queue. This is distinct from the Parameter Store key.
- `queue.event_source_mapping.batch_size`: Maximum records delivered per scale-up invocation.
- `queue.event_source_mapping.maximum_batching_window_in_seconds`: Maximum event batching window.
- `storage_provider.aws.ssm.token_path`: Parameter Store path used for registration tokens.
- `storage_provider.aws.ssm.token_path_arn`: ARN of the Parameter Store path used for registration tokens.
- `storage_provider.aws.ssm.config_path`: Parameter Store path used for persistent runner configuration.
- `storage_provider.aws.ssm.config_path_arn`: ARN of the persistent runner configuration path.
- `storage_provider.aws.ssm.kms_key_id`: Optional KMS key ARN used to decrypt shared parameters. Its value may be unknown until apply.
- `storage_provider.aws.ssm.parameter_store_tags`: JSON-encoded tags applied to parameters created at runtime.
- `observability.logs`: Shared logging level, retention, encryption, and log-class configuration.
- `observability.tracing`: Lambda X-Ray and tracing-helper configuration.
- `observability.metrics`: Metrics enablement, namespace, and GitHub rate-limit metric configuration.
- `scale_up`: Scale-up Lambda sizing, concurrency, queued-job behavior, and resolved resource tag maps.
- `scale_up.tags.resources`: Tags for the scale-up IAM role and other component resources.
- `scale_up.tags.lambda`: Tags for the scale-up Lambda function.
- `scale_up.tags.log_group`: Tags for the scale-up log group.
- `scale_up.tags.event_source_mapping`: Tags for the build-queue event-source mapping.
- `scale_down`: Scale-down Lambda sizing, schedule, idle configuration, minimum runtime, and resolved resource tag maps.
- `scale_down.idle_confirmation_seconds`: Number of seconds a runner must consistently report not-busy before scale-down terminates it. GitHub's busy flag can be stale (it can read false for a runner that is actively executing a job), so a single not-busy reading is not sufficient evidence a runner is idle. Set to at least one scale-down schedule interval to require two consecutive not-busy evaluations; a busy reading resets the window. 0 keeps the previous single-reading behaviour.
- `scale_down.tags.resources`: Tags for the scale-down IAM role and EventBridge rule.
- `scale_down.tags.lambda`: Tags for the scale-down Lambda function.
- `scale_down.tags.log_group`: Tags for the scale-down log group.
- `job_retry.enabled`: Enables publishing retry checks from scale-up.
- `job_retry.queue`: Retry queue ARN and URL. Required when job retry is enabled.
- `job_retry.max_attempts`: Maximum queued-job retry attempts.
- `job_retry.delay_in_seconds`: Initial delay before checking the queued job.
- `job_retry.delay_backoff`: Multiplier applied to subsequent delays. |
object({
prefix = string
lambda = object({
artifact = object({
zip = string
s3 = object({
bucket = optional(string, null)
key = optional(string, null)
object_version = optional(string, null)
})
})
runtime = string
architecture = string
vpc = object({
subnet_ids = list(string)
security_group_ids = list(string)
})
role = object({
path = string
permissions_boundary = optional(string, null)
principals = optional(list(object({
type = string
identifiers = list(string)
})), [])
})
})
runner = object({
os = string
auto_update_disabled = bool
ephemeral = bool
jit_config_enabled = optional(bool, null)
labels = list(string)
group_name = string
name_prefix = string
boot_time_in_minutes = number
maximum_count = number
})
github = object({
organization_runners = bool
enterprise_server = object({
url = optional(string, null)
ssl_verify = bool
})
user_agent = optional(string, null)
app_parameters = object({
key_base64 = map(string)
id = map(string)
additional_apps_manifest = optional(object({
name = string
arn = string
}), null)
additional_app_parameter_arns = optional(list(string), [])
})
})
queue = object({
build = object({
arn = string
})
kms_key_id = optional(string, null)
event_source_mapping = object({
batch_size = number
maximum_batching_window_in_seconds = number
})
})
observability = object({
logs = object({
level = string
retention_in_days = number
kms_key_id = optional(string, null)
class = string
})
tracing = object({
mode = optional(string, null)
capture_http_requests = bool
capture_error = bool
})
metrics = object({
enabled = bool
namespace = string
metric = object({
github_app_rate_limit = object({
enabled = bool
})
})
})
})
scale_up = object({
memory_size = number
timeout = number
reserved_concurrent_executions = number
job_queued_check_enabled = bool
tags = object({
resources = map(string)
lambda = map(string)
log_group = map(string)
event_source_mapping = map(string)
})
})
scale_down = object({
memory_size = number
timeout = number
schedule_expression = string
minimum_running_time_in_minutes = optional(number, null)
idle_confirmation_seconds = optional(number, 0)
idle_config = list(object({
cron = string
timeZone = string
idleCount = number
evictionStrategy = string
}))
tags = object({
resources = map(string)
lambda = map(string)
log_group = map(string)
})
})
job_retry = object({
enabled = bool
max_attempts = number
delay_in_seconds = number
delay_backoff = number
queue = optional(object({
arn = string
url = string
}), null)
})
})
| n/a | yes | | [runner\_provider](#input\_runner\_provider) | Selected compute-provider integration for the scaling control plane.

- `type`: Compute-provider discriminator supplied to both Lambdas.
- `scale_up.environment_variables`: Provider-specific scale-up environment variables.
- `scale_up.iam_policy_json`: Provider-specific IAM policy merged into the common scale-up policy.
- `scale_up.additional_iam_policy_json`: Optional additional provider policy attached separately to the scale-up role.
- `scale_up.managed_policy`: Optional provider-managed policy attachment. Object presence controls attachment creation.
- `scale_up.managed_policy.arn`: ARN of the provider-managed policy. The ARN may remain unknown until apply.
- `scale_down.environment_variables`: Provider-specific scale-down environment variables.
- `scale_down.iam_policy_json`: Provider-specific IAM policy merged into the common scale-down policy. |
object({
type = string
scale_up = object({
environment_variables = map(string)
iam_policy_json = string
additional_iam_policy_json = optional(string, null)
managed_policy = optional(object({
arn = string
}), null)
})
scale_down = object({
environment_variables = map(string)
iam_policy_json = string
})
})
| n/a | yes | -| [storage\_provider](#input\_storage\_provider) | Resolved storage-provider configuration and capabilities for scale-up and scale-down. |
object({
aws = object({
ssm = object({
token_path = string
token_path_arn = string
config_path = string
config_path_arn = string
parameter_store_tags = string
kms_key_id = optional(string, null)
})
})
scale_up = optional(object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
}), {
environment_variables = {}
iam_policy_json = null
})
scale_down = optional(object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
}), {
environment_variables = {}
iam_policy_json = null
})
})
| n/a | yes | +| [storage\_provider](#input\_storage\_provider) | Resolved storage-provider configuration and capabilities for scale-up and scale-down. |
object({
aws = object({
ssm = optional(object({
token_path = string
token_path_arn = string
config_path = string
config_path_arn = string
parameter_store_tags = string
kms_key_id = optional(string, null)
}), null)
})
scale_up = optional(object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
}), {
environment_variables = {}
iam_policy_json = null
})
scale_down = optional(object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
}), {
environment_variables = {}
iam_policy_json = null
})
})
| n/a | yes | ## Outputs diff --git a/modules/runner-config/README.md b/modules/runner-config/README.md index 622632f3b5..3c1ca3ad93 100644 --- a/modules/runner-config/README.md +++ b/modules/runner-config/README.md @@ -87,7 +87,7 @@ yarn run dist | [compute\_aws\_ec2](#module\_compute\_aws\_ec2) | ../compute-providers/aws/ec2 | n/a | | [compute\_aws\_ec2\_trust\_policy](#module\_compute\_aws\_ec2\_trust\_policy) | ../compute-providers/aws/ec2/trust-policy | n/a | | [orchestration\_webhook](#module\_orchestration\_webhook) | ../orchestration-providers/webhook | n/a | -| [ssm\_housekeeper](#module\_ssm\_housekeeper) | ./ssm-housekeeper | n/a | +| [runner\_config\_housekeeper](#module\_runner\_config\_housekeeper) | ./runner-config-housekeeper | n/a | ## Resources @@ -117,7 +117,7 @@ yarn run dist | [orchestration\_provider](#input\_orchestration\_provider) | Runner demand-orchestration provider configuration. Exactly one provider block must be non-null. Wrapper presence selects the provider and must therefore be known during planning; values inside the selected provider may remain unknown until apply.

- `webhook`: Selects the workflow-job webhook control plane. It owns runner lifecycle and capacity, the build queue reference, the runner-control artifact, scale-up, scale-down, scheduled pool, and optional job-retry controls. Future providers can be added as sibling blocks without moving this contract.
- `webhook.runner`: Runner lifecycle, boot timeout, and capacity settings owned by webhook orchestration.
- `webhook.runner.boot_time_in_minutes`: Expected runner boot duration used by scale-down and pool controls. The default is `5`.
- `webhook.runner.ephemeral`: Registers runners in ephemeral mode. The default is `false`.
- `webhook.runner.jit_config_enabled`: Explicitly enables or disables just-in-time configuration. The default is null, which follows `runner.ephemeral`.
- `webhook.runner.maximum_count`: Maximum number of runners managed for this runner configuration. The default is `3`.
- `webhook.github.organization_runners`: Registers runners at organization scope when true; otherwise registration is repository-scoped.
- `webhook.queue.build.arn`: ARN of the runner configuration's build queue.
- `webhook.queue.build.url`: URL of the runner configuration's build queue.
- `webhook.queue.kms_key_id`: Optional KMS key ARN encrypting the build queue. The default is null and is independent from the Parameter Store KMS key.
- `webhook.queue.tags`: Tags inherited by queue-related provider resources before component-specific overrides. The default is `{}`.
- `webhook.lambda.artifact`: Runner-control artifact shared by scale, pool, and job-retry components. Set at most one of `zip` or `s3`; no selection uses the packaged runner archive.
- `webhook.lambda.artifact.zip`: Optional local path to the runner-control Lambda archive. The default is null.
- `webhook.lambda.artifact.s3`: Optional S3 object selector in the common `lambda.artifact.s3.bucket`. Wrapper presence must be known during planning, selecting it requires a non-null common bucket, and the default is null.
- `webhook.lambda.artifact.s3.key`: Object key of the runner-control Lambda archive.
- `webhook.lambda.artifact.s3.object_version`: Optional object version of the runner-control Lambda archive. The default is null.
- `webhook.lambda.scale.up.memory_size`: Memory allocated to the scale-up Lambda in MB. The default is `512`.
- `webhook.lambda.scale.up.timeout`: Scale-up Lambda timeout in seconds. The default is `60`.
- `webhook.lambda.scale.up.reserved_concurrent_executions`: Reserved concurrency for scale-up. The default is `1`; use `-1` for unreserved concurrency.
- `webhook.lambda.scale.up.job_queued_check_enabled`: Enables queued-job verification before scaling. The default is null, which follows the resolved runner mode.
- `webhook.lambda.scale.up.event_source_mapping.batch_size`: Maximum build-queue records delivered per scale-up invocation. The default is `10`.
- `webhook.lambda.scale.up.event_source_mapping.maximum_batching_window_in_seconds`: Maximum batching window for build-queue records. The default is `0`.
- `webhook.lambda.scale.up.tags`: Tags applied within scale-up resource scopes after common provider tags. The default is `{}`.
- `webhook.lambda.scale.down.memory_size`: Memory allocated to the scale-down Lambda in MB. The default is `512`.
- `webhook.lambda.scale.down.timeout`: Scale-down Lambda timeout in seconds. The default is `60`.
- `webhook.lambda.scale.down.schedule_expression`: EventBridge schedule expression that invokes scale-down. The default is `cron(*/5 * * * ? *)`.
- `webhook.lambda.scale.down.minimum_running_time_in_minutes`: Optional minimum runner age before scale-down may terminate it. The default is null, which selects the operating-system default.
- `webhook.lambda.scale.down.idle_confirmation_seconds`: Number of seconds a runner must consistently report not-busy before scale-down terminates it. The default is `0`, which preserves the single-reading behavior.
- `webhook.lambda.scale.down.idle_config`: Time-based desired idle-runner configurations. The default is `[]`.
- `webhook.lambda.scale.down.idle_config[].cron`: Cron expression identifying when the idle configuration applies.
- `webhook.lambda.scale.down.idle_config[].timeZone`: IANA time zone used to evaluate the cron expression.
- `webhook.lambda.scale.down.idle_config[].idleCount`: Number of idle runners retained during the matching period.
- `webhook.lambda.scale.down.idle_config[].evictionStrategy`: Selection strategy used when excess idle runners are removed. The default is `oldest_first`.
- `webhook.lambda.scale.down.tags`: Tags applied within scale-down resource scopes after common provider tags. The default is `{}`.
- `webhook.lambda.pool.memory_size`: Memory allocated to the pool Lambda in MB. The default is `512`.
- `webhook.lambda.pool.timeout`: Pool Lambda timeout in seconds. The default is `60`.
- `webhook.lambda.pool.reserved_concurrent_executions`: Reserved concurrency for the pool Lambda. The default is `1`; use `-1` for unreserved concurrency.
- `webhook.lambda.pool.config`: Scheduled target pool sizes. The default is `[]`, which disables the pool component.
- `webhook.lambda.pool.config[].schedule_expression`: Scheduler expression that activates the target size.
- `webhook.lambda.pool.config[].schedule_expression_timezone`: Optional IANA time zone used to evaluate the schedule.
- `webhook.lambda.pool.config[].size`: Desired number of runners for the schedule.
- `webhook.lambda.pool.include_busy_runners`: Includes busy runners when reconciling scheduled pool capacity. The default is `false`.
- `webhook.lambda.pool.runner_owner`: Optional GitHub organization or repository owner used for pooled runners. The default is null.
- `webhook.lambda.pool.tags`: Tags applied within pool resource scopes after common provider tags. The default is `{}`.
- `webhook.job_retry.enabled`: Creates the retry queue, Lambda function, event-source mapping, and related IAM resources. The default is `false`.
- `webhook.job_retry.delay_in_seconds`: Initial delay before a queued-job retry check. The default is `300`.
- `webhook.job_retry.delay_backoff`: Multiplier applied to the delay after each unsuccessful check. The default is `2`.
- `webhook.job_retry.max_attempts`: Maximum retry-check attempts before the message is no longer republished. The default is `1`.
- `webhook.job_retry.tags`: Tags applied within job-retry resource scopes after common provider tags. The default is `{}`.
- `webhook.job_retry.lambda.memory_size`: Memory allocated to the job-retry Lambda in MB. The default is `256`.
- `webhook.job_retry.lambda.reserved_concurrent_executions`: Reserved concurrency for job retry. The default is `1`; use `-1` for unreserved concurrency.
- `webhook.job_retry.lambda.timeout`: Job-retry Lambda timeout in seconds and visibility timeout for its retry queue. The default is `30`. |
object({
webhook = optional(object({
runner = optional(object({
boot_time_in_minutes = optional(number, 5)
ephemeral = optional(bool, false)
jit_config_enabled = optional(bool, null)
maximum_count = optional(number, 3)
}), {})
github = object({
organization_runners = bool
})
queue = object({
build = object({
arn = string
url = string
})
kms_key_id = optional(string, null)
tags = optional(map(string), {})
})
lambda = optional(object({
artifact = optional(object({
zip = optional(string, null)
s3 = optional(object({
key = string
object_version = optional(string, null)
}), null)
}), {})
scale = optional(object({
up = optional(object({
memory_size = optional(number, 512)
timeout = optional(number, 60)
reserved_concurrent_executions = optional(number, 1)
job_queued_check_enabled = optional(bool, null)
event_source_mapping = optional(object({
batch_size = optional(number, 10)
maximum_batching_window_in_seconds = optional(number, 0)
}), {})
tags = optional(map(string), {})
}), {})
down = optional(object({
memory_size = optional(number, 512)
timeout = optional(number, 60)
schedule_expression = optional(string, "cron(*/5 * * * ? *)")
minimum_running_time_in_minutes = optional(number, null)
idle_confirmation_seconds = optional(number, 0)
idle_config = optional(list(object({
cron = string
timeZone = string
idleCount = number
evictionStrategy = optional(string, "oldest_first")
})), [])
tags = optional(map(string), {})
}), {})
}), {})
pool = optional(object({
memory_size = optional(number, 512)
timeout = optional(number, 60)
reserved_concurrent_executions = optional(number, 1)
config = optional(list(object({
schedule_expression = string
schedule_expression_timezone = optional(string)
size = number
})), [])
include_busy_runners = optional(bool, false)
runner_owner = optional(string, null)
tags = optional(map(string), {})
}), {})
}), {})
job_retry = optional(object({
enabled = optional(bool, false)
delay_in_seconds = optional(number, 300)
delay_backoff = optional(number, 2)
max_attempts = optional(number, 1)
tags = optional(map(string), {})
lambda = optional(object({
memory_size = optional(number, 256)
reserved_concurrent_executions = optional(number, 1)
timeout = optional(number, 30)
}), {})
}), {})
}), null)
})
| n/a | yes | | [prefix](#input\_prefix) | The prefix used for naming resources. | `string` | `"github-actions"` | no | | [runner](#input\_runner) | Provider-neutral GitHub runner configuration.

- `os`: Runner operating system. Supported values are `linux`, `osx`, and `windows`.
- `architecture`: Runner distribution architecture, such as `x64` or `arm64`.
- `disable_default_labels`: Prevents GitHub's default self-hosted, operating-system, and architecture labels from being registered.
- `labels`: Complete set of labels supplied to the control-plane functions.
- `group_name`: GitHub runner group used during registration.
- `name_prefix`: Prefix added to registered runner names.
- `run_as_root`: Runs the runner service as root when supported by the compute provider.
- `run_as`: Operating-system user used when `run_as_root` is false.
- `auto_update_disabled`: Disables the GitHub runner application's built-in updater.
- `tags`: Additional tags for common runner resources, currently the managed runner IAM role. These override module-level `tags` with the same key.
- `hooks.job_started`: Script content installed as the runner job-started hook.
- `hooks.job_completed`: Script content installed as the runner job-completed hook.
- `iam.role.arn`: ARN of an externally managed runner role. When set, this module does not create or modify that role.
- `iam.managed_policy_arns`: Named managed-policy ARNs attached to the module-managed runner role.
- `iam.additional_trust_policy_json`: Optional IAM policy document merged with the selected compute provider's default runner-role trust policy.
- `iam.path`: IAM path for the module-managed runner role. Defaults to a path derived from `prefix`.
- `iam.permissions_boundary`: Permissions-boundary ARN for the module-managed runner role. |
object({
os = optional(string, "linux")
architecture = optional(string, "x64")
disable_default_labels = optional(bool, false)
labels = list(string)
group_name = optional(string, "Default")
name_prefix = optional(string, "")
run_as_root = optional(bool, false)
run_as = optional(string, "ec2-user")
auto_update_disabled = optional(bool, false)
tags = optional(map(string), {})
hooks = optional(object({
job_started = optional(string, "")
job_completed = optional(string, "")
}), {})
iam = optional(object({
role = optional(object({
arn = string
}), null)
managed_policy_arns = optional(map(string), {})
additional_trust_policy_json = optional(string, null)
path = optional(string, null)
permissions_boundary = optional(string, null)
}), {})
})
| n/a | yes | -| [storage\_provider](#input\_storage\_provider) | Parameter Store paths, encryption, tag scopes, and housekeeper configuration.

- `storage_provider.aws.ssm.paths.root`: Root Parameter Store path for this runner configuration.
- `storage_provider.aws.ssm.paths.tokens`: Path segment under `paths.root` used for registration tokens and just-in-time configuration.
- `storage_provider.aws.ssm.paths.config`: Path segment under `paths.root` used for persistent runner configuration.
- `storage_provider.aws.ssm.kms_key_id`: Optional customer-managed KMS key ARN used by control-plane IAM policies to decrypt shared GitHub App parameters. The ARN may be unknown until apply; null omits the provider-owned KMS statements. It does not select encryption for runtime-created runner parameters.
- `storage_provider.aws.ssm.tags`: Shared tags for SSM-related resources. These override module-level `tags` and are inherited by parameter and housekeeper resources.
- `storage_provider.aws.ssm.parameters.tags`: Tags for Terraform-managed runner configuration parameters and temporary parameters created by the scale-up and pool Lambdas. These override module-level and `storage_provider.aws.ssm.tags` values with the same key.
- `storage_provider.aws.ssm.housekeeper.schedule_expression`: EventBridge schedule expression that invokes the SSM housekeeper.
- `storage_provider.aws.ssm.housekeeper.state`: EventBridge rule state, such as `ENABLED` or `DISABLED`.
- `storage_provider.aws.ssm.housekeeper.tags`: Tags for housekeeper resources, including the Lambda function, log group, EventBridge rule, and IAM role. These override module-level, `storage_provider.aws.ssm.tags`, shared Lambda, and shared log tags when keys conflict.
- `storage_provider.aws.ssm.housekeeper.lambda.artifact`: Component-owned SSM-housekeeper artifact selection. Set at most one of `zip` or `s3`; when neither is selected, the module uses its packaged runner control-plane archive. This selector does not inherit an orchestration-provider artifact.
- `storage_provider.aws.ssm.housekeeper.lambda.artifact.zip`: Optional local path to the SSM-housekeeper Lambda archive.
- `storage_provider.aws.ssm.housekeeper.lambda.artifact.s3`: Optional object key and version in the shared `lambda.artifact.s3.bucket`. Selecting S3 requires that common bucket.
- `storage_provider.aws.ssm.housekeeper.lambda.artifact.s3.key`: Object key of the SSM-housekeeper Lambda archive.
- `storage_provider.aws.ssm.housekeeper.lambda.artifact.s3.object_version`: Optional object version of the SSM-housekeeper Lambda archive.
- `storage_provider.aws.ssm.housekeeper.lambda.memory_size`: Memory allocated to the SSM housekeeper Lambda in MB.
- `storage_provider.aws.ssm.housekeeper.lambda.timeout`: SSM housekeeper Lambda timeout in seconds.
- `storage_provider.aws.ssm.housekeeper.config.tokenPath`: Parameter Store token path cleaned by the housekeeper. When omitted, the configured runner token path is used.
- `storage_provider.aws.ssm.housekeeper.config.minimumDaysOld`: Minimum parameter age in days before deletion is allowed.
- `storage_provider.aws.ssm.housekeeper.config.dryRun`: Reports eligible parameters without deleting them when true. |
object({
aws = object({
ssm = object({
paths = object({
root = string
tokens = string
config = string
})
kms_key_id = optional(string, null)
tags = optional(map(string), {})
parameters = optional(object({
tags = optional(map(string), {})
}), {})
housekeeper = optional(object({
schedule_expression = optional(string, "rate(1 day)")
state = optional(string, "ENABLED")
tags = optional(map(string), {})
lambda = optional(object({
artifact = optional(object({
zip = optional(string, null)
s3 = optional(object({
key = string
object_version = optional(string, null)
}), null)
}), {})
memory_size = optional(number, 512)
timeout = optional(number, 60)
}), {})
config = optional(object({
tokenPath = optional(string)
minimumDaysOld = optional(number, 1)
dryRun = optional(bool, false)
}), {})
}), {})
})
})
})
| n/a | yes | +| [storage\_provider](#input\_storage\_provider) | Parameter Store paths, encryption, tag scopes, and housekeeper configuration.

- `storage_provider.aws.ssm.paths.root`: Root Parameter Store path for this runner configuration.
- `storage_provider.aws.ssm.paths.tokens`: Path segment under `paths.root` used for registration tokens and just-in-time configuration.
- `storage_provider.aws.ssm.paths.config`: Path segment under `paths.root` used for persistent runner configuration.
- `storage_provider.aws.ssm.kms_key_id`: Optional customer-managed KMS key ARN used by control-plane IAM policies to decrypt shared GitHub App parameters. The ARN may be unknown until apply; null omits the provider-owned KMS statements. It does not select encryption for runtime-created runner parameters.
- `storage_provider.aws.ssm.tags`: Shared tags for SSM-related resources. These override module-level `tags` and are inherited by parameter and housekeeper resources.
- `storage_provider.aws.ssm.parameters.tags`: Tags for Terraform-managed runner configuration parameters and temporary parameters created by the scale-up and pool Lambdas. These override module-level and `storage_provider.aws.ssm.tags` values with the same key.
- `storage_provider.aws.ssm.housekeeper.schedule_expression`: EventBridge schedule expression that invokes the SSM housekeeper.
- `storage_provider.aws.ssm.housekeeper.state`: EventBridge rule state, such as `ENABLED` or `DISABLED`.
- `storage_provider.aws.ssm.housekeeper.tags`: Tags for housekeeper resources, including the Lambda function, log group, EventBridge rule, and IAM role. These override module-level, `storage_provider.aws.ssm.tags`, shared Lambda, and shared log tags when keys conflict.
- `storage_provider.aws.ssm.housekeeper.lambda.artifact`: Component-owned SSM-housekeeper artifact selection. Set at most one of `zip` or `s3`; when neither is selected, the module uses its packaged runner control-plane archive. This selector does not inherit an orchestration-provider artifact.
- `storage_provider.aws.ssm.housekeeper.lambda.artifact.zip`: Optional local path to the SSM-housekeeper Lambda archive.
- `storage_provider.aws.ssm.housekeeper.lambda.artifact.s3`: Optional object key and version in the shared `lambda.artifact.s3.bucket`. Selecting S3 requires that common bucket.
- `storage_provider.aws.ssm.housekeeper.lambda.artifact.s3.key`: Object key of the SSM-housekeeper Lambda archive.
- `storage_provider.aws.ssm.housekeeper.lambda.artifact.s3.object_version`: Optional object version of the SSM-housekeeper Lambda archive.
- `storage_provider.aws.ssm.housekeeper.lambda.memory_size`: Memory allocated to the SSM housekeeper Lambda in MB.
- `storage_provider.aws.ssm.housekeeper.lambda.timeout`: SSM housekeeper Lambda timeout in seconds.
- `storage_provider.aws.ssm.housekeeper.config.tokenPath`: Parameter Store token path cleaned by the housekeeper. When omitted, the configured runner token path is used.
- `storage_provider.aws.ssm.housekeeper.config.minimumDaysOld`: Minimum parameter age in days before deletion is allowed.
- `storage_provider.aws.ssm.housekeeper.config.dryRun`: Reports eligible parameters without deleting them when true. |
object({
aws = object({
ssm = optional(object({
paths = object({
root = string
tokens = string
config = string
})
kms_key_id = optional(string, null)
tags = optional(map(string), {})
parameters = optional(object({
tags = optional(map(string), {})
}), {})
housekeeper = optional(object({
schedule_expression = optional(string, "rate(1 day)")
state = optional(string, "ENABLED")
tags = optional(map(string), {})
lambda = optional(object({
artifact = optional(object({
zip = optional(string, null)
s3 = optional(object({
key = string
object_version = optional(string, null)
}), null)
}), {})
memory_size = optional(number, 512)
timeout = optional(number, 60)
}), {})
config = optional(object({
tokenPath = optional(string)
minimumDaysOld = optional(number, 1)
dryRun = optional(bool, false)
}), {})
}), {})
}), null)
})
scale_up = optional(object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
}), {
environment_variables = {}
iam_policy_json = null
})
scale_down = optional(object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
}), {
environment_variables = {}
iam_policy_json = null
})
pool = optional(object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
}), {
environment_variables = {}
iam_policy_json = null
})
job_retry = optional(object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
}), {
environment_variables = {}
iam_policy_json = null
})
})
| n/a | yes | | [tags](#input\_tags) | Base tags added to taggable resources created by this runner configuration. Shared, component, and compute-provider tag maps override matching keys within their documented resource scopes. | `map(string)` | `{}` | no | ## Outputs diff --git a/modules/runner-config/runner-config-housekeeper/README.md b/modules/runner-config/runner-config-housekeeper/README.md new file mode 100644 index 0000000000..0568f36d34 --- /dev/null +++ b/modules/runner-config/runner-config-housekeeper/README.md @@ -0,0 +1,50 @@ + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.5.6 | +| [aws](#requirement\_aws) | >= 6.33 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 6.33 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [aws_cloudwatch_event_rule.housekeeper](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_rule) | resource | +| [aws_cloudwatch_event_target.housekeeper](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_target) | resource | +| [aws_cloudwatch_log_group.housekeeper](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | +| [aws_iam_role.housekeeper](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role_policy.housekeeper](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_iam_role_policy.housekeeper_logging](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_iam_role_policy.housekeeper_xray](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_iam_role_policy_attachment.housekeeper_vpc_execution_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_lambda_function.housekeeper](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function) | resource | +| [aws_lambda_permission.housekeeper](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_permission) | resource | +| [aws_iam_policy_document.housekeeper](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.housekeeper_logging](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.lambda_assume_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.lambda_xray](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [config](#input\_config) | Provider-neutral SSM housekeeper configuration assembled by runner-config.

- `prefix`: Prefix used to name the housekeeper resources.
- `aws_partition`: AWS partition used to construct IAM policy ARNs.
- `schedule.expression`: EventBridge schedule expression that invokes the housekeeper.
- `schedule.state`: State of the EventBridge rule.
- `lambda.artifact.zip`: Resolved local control-plane archive.
- `lambda.artifact.s3.bucket`: Optional S3 bucket containing the Lambda archive.
- `lambda.artifact.s3.key`: Object key of the Lambda archive.
- `lambda.artifact.s3.object_version`: Optional object version of the Lambda archive.
- `lambda.runtime`: Runtime used by the housekeeper Lambda.
- `lambda.architecture`: Instruction-set architecture used by the housekeeper Lambda.
- `lambda.memory_size`: Memory allocated to the housekeeper Lambda.
- `lambda.timeout`: Housekeeper Lambda timeout in seconds.
- `lambda.vpc.subnet_ids`: Subnets used for Lambda VPC configuration.
- `lambda.vpc.security_group_ids`: Security groups used for Lambda VPC configuration.
- `lambda.role.path`: IAM path used for the housekeeper Lambda role.
- `lambda.role.permissions_boundary`: Optional permissions boundary for the housekeeper role.
- `lambda.role.principals`: Additional principals allowed to assume the housekeeper Lambda role.
- `observability.logs`: Logging level, retention, encryption, and log-class configuration.
- `observability.tracing`: Lambda X-Ray and tracing-helper configuration.
- `tags.resources`: Tags for the housekeeper role and EventBridge rule.
- `tags.lambda`: Tags for the housekeeper Lambda function.
- `tags.log_group`: Tags for the housekeeper log group. |
object({
prefix = string
aws_partition = string
schedule = object({
expression = string
state = string
})
lambda = object({
artifact = object({
zip = string
s3 = object({
bucket = optional(string, null)
key = optional(string, null)
object_version = optional(string, null)
})
})
runtime = string
architecture = string
memory_size = number
timeout = number
vpc = object({
subnet_ids = list(string)
security_group_ids = list(string)
})
role = object({
path = string
permissions_boundary = optional(string, null)
principals = optional(list(object({
type = string
identifiers = list(string)
})), [])
})
})
observability = object({
logs = object({
level = string
retention_in_days = number
kms_key_id = optional(string, null)
class = string
})
tracing = object({
mode = optional(string, null)
capture_http_requests = bool
capture_error = bool
})
})
tags = object({
resources = map(string)
lambda = map(string)
log_group = map(string)
})
})
| n/a | yes | +| [storage\_provider](#input\_storage\_provider) | Storage-provider selection used to gate provider-specific housekeeper IAM statements.

- `aws.ssm.cleanup.token_path`: Parameter Store token path supplied to the Lambda.
- `aws.ssm.cleanup.parameter_path_arn`: IAM resource ARN matching `aws.ssm.cleanup.token_path`.
- `aws.ssm.cleanup.minimum_days_old`: Minimum parameter age before deletion.
- `aws.ssm.cleanup.dry_run`: Reports eligible parameters without deleting them when true. |
object({
aws = object({
ssm = optional(object({
cleanup = object({
token_path = string
parameter_path_arn = string
minimum_days_old = number
dry_run = bool
})
}), null)
})
})
| n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [housekeeper](#output\_housekeeper) | Runner-config housekeeper Lambda resources. | + \ No newline at end of file diff --git a/modules/webhook/README.md b/modules/webhook/README.md index b4a7d3ba28..cc2a42f204 100644 --- a/modules/webhook/README.md +++ b/modules/webhook/README.md @@ -89,7 +89,7 @@ yarn run dist | [role\_path](#input\_role\_path) | The path that will be added to the role; if not set, the environment name will be used. | `string` | `null` | no | | [role\_permissions\_boundary](#input\_role\_permissions\_boundary) | Permissions boundary that will be added to the created role for the lambda. | `string` | `null` | no | | [runner\_matcher\_config](#input\_runner\_matcher\_config) | SQS queue to publish accepted build events based on the runner type. `computeProvider` defaults to `ec2`; EC2 is the only provider currently implemented. When exact match is disabled the webhook accepts the event if one of the workflow job labels is part of the matcher. The priority defines the order the matchers are applied. Optional `matcherConfig.enableDynamicLabels` and `matcherConfig.awsDynamicLabelsPolicy` are evaluated by the dispatcher to gate provider dynamic labels per runner. The policy supports `allowed_keys = []`, `blocked_keys = []` (cannot be used together with `allowed_keys`), and `restricted_keys = { = { allowed = [globs], denied = [globs], max = number|string } }`; keys use the provider dynamic label suffix form, for example `instance-type` for `ghr-ec2-instance-type`. |
map(object({
arn = string
id = string
computeProvider = optional(string, "ec2")
matcherConfig = object({
labelMatchers = list(list(string))
exactMatch = bool
bidirectionalLabelMatch = optional(bool, false)
priority = optional(number, 999)
enableDynamicLabels = optional(bool, false)
awsDynamicLabelsPolicy = optional(object({
allowed_keys = optional(list(string), [])
blocked_keys = optional(list(string), [])
restricted_keys = optional(map(object({
allowed = optional(list(string), [])
denied = optional(list(string), [])
max = optional(string, null)
})), {})
}), null)
})
}))
| n/a | yes | -| [storage\_provider](#input\_storage\_provider) | Storage-provider configuration used by the webhook resources. |
object({
aws = object({
kms_key_id = optional(string, null)
ssm = object({
paths = object({
root = string
webhook = string
})
})
})
})
| n/a | yes | +| [storage\_provider](#input\_storage\_provider) | Resolved storage-provider marker and provider-owned webhook capabilities. |
object({
aws = object({
ssm = optional(object({
paths = object({
root = string
webhook = string
})
}), null)
})
direct = object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
})
eventbridge = object({
webhook = object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
})
dispatcher = object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
})
})
})
| n/a | yes | | [tags](#input\_tags) | Map of tags that will be added to created resources. By default resources will be tagged with name and environment. | `map(string)` | `{}` | no | | [tracing\_config](#input\_tracing\_config) | Configuration for lambda tracing. |
object({
mode = optional(string, null)
capture_http_requests = optional(bool, false)
capture_error = optional(bool, false)
})
| `{}` | no | | [webhook\_lambda\_apigateway\_access\_log\_settings](#input\_webhook\_lambda\_apigateway\_access\_log\_settings) | Access log settings for webhook API gateway. |
object({
destination_arn = string
format = string
})
| `null` | no | diff --git a/modules/webhook/direct/README.md b/modules/webhook/direct/README.md index 67fea936dc..1e4240b855 100644 --- a/modules/webhook/direct/README.md +++ b/modules/webhook/direct/README.md @@ -40,7 +40,7 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [config](#input\_config) | Configuration object for all variables. |
object({
prefix = string
archive = optional(object({
enable = optional(bool, true)
retention_days = optional(number, 7)
}), {})
tags = optional(map(string), {})

lambda_subnet_ids = optional(list(string), [])
lambda_security_group_ids = optional(list(string), [])
sqs_job_queues_arns = list(string)
lambda_zip = optional(string, null)
lambda_memory_size = optional(number, 256)
lambda_timeout = optional(number, 10)
role_permissions_boundary = optional(string, null)
role_path = optional(string, null)
logging_retention_in_days = optional(number, 180)
logging_kms_key_id = optional(string, null)
log_class = optional(string, "STANDARD")
lambda_s3_bucket = optional(string, null)
lambda_s3_key = optional(string, null)
lambda_s3_object_version = optional(string, null)
lambda_apigateway_access_log_settings = optional(object({
destination_arn = string
format = string
}), null)
repository_white_list = optional(list(string), [])
queue_selection_strategy = optional(string, "first")
storage_provider = object({
aws = object({
ssm = object({
kms_key_id = optional(string, null)
})
})
})
log_level = optional(string, "info")
lambda_runtime = optional(string, "nodejs24.x")
aws_partition = optional(string, "aws")
lambda_architecture = optional(string, "arm64")
github_app_parameters = object({
webhook_secret = map(string)
})
tracing_config = optional(object({
mode = optional(string, null)
capture_http_requests = optional(bool, false)
capture_error = optional(bool, false)
}), {})
lambda_tags = optional(map(string), {})
api_gw_source_arn = string
ssm_parameter_runner_matcher_config = list(object({
name = string
arn = string
version = string
}))
})
| n/a | yes | +| [config](#input\_config) | Configuration object for all variables. |
object({
prefix = string
archive = optional(object({
enable = optional(bool, true)
retention_days = optional(number, 7)
}), {})
tags = optional(map(string), {})

lambda_subnet_ids = optional(list(string), [])
lambda_security_group_ids = optional(list(string), [])
sqs_job_queues_arns = list(string)
lambda_zip = optional(string, null)
lambda_memory_size = optional(number, 256)
lambda_timeout = optional(number, 10)
role_permissions_boundary = optional(string, null)
role_path = optional(string, null)
logging_retention_in_days = optional(number, 180)
logging_kms_key_id = optional(string, null)
log_class = optional(string, "STANDARD")
lambda_s3_bucket = optional(string, null)
lambda_s3_key = optional(string, null)
lambda_s3_object_version = optional(string, null)
lambda_apigateway_access_log_settings = optional(object({
destination_arn = string
format = string
}), null)
repository_white_list = optional(list(string), [])
queue_selection_strategy = optional(string, "first")
storage_provider = optional(object({
aws = optional(object({
ssm = optional(object({}), null)
}), {})
environment_variables = map(string)
iam_policy_json = optional(string, null)
}))
log_level = optional(string, "info")
lambda_runtime = optional(string, "nodejs24.x")
aws_partition = optional(string, "aws")
lambda_architecture = optional(string, "arm64")
github_app_parameters = object({
webhook_secret = map(string)
})
tracing_config = optional(object({
mode = optional(string, null)
capture_http_requests = optional(bool, false)
capture_error = optional(bool, false)
}), {})
lambda_tags = optional(map(string), {})
api_gw_source_arn = string
ssm_parameter_runner_matcher_config = list(object({
name = string
arn = string
version = string
}))
})
| n/a | yes | ## Outputs diff --git a/modules/webhook/eventbridge/README.md b/modules/webhook/eventbridge/README.md index 987f370b67..74d1cfd8a3 100644 --- a/modules/webhook/eventbridge/README.md +++ b/modules/webhook/eventbridge/README.md @@ -54,7 +54,7 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [config](#input\_config) | Configuration object for all variables. |
object({
prefix = string
archive = optional(object({
enable = optional(bool, true)
retention_days = optional(number, 7)
}), {})
tags = optional(map(string), {})

lambda_subnet_ids = optional(list(string), [])
lambda_security_group_ids = optional(list(string), [])
sqs_job_queues_arns = list(string)
lambda_zip = optional(string, null)
lambda_memory_size = optional(number, 256)
lambda_timeout = optional(number, 10)
role_permissions_boundary = optional(string, null)
role_path = optional(string, null)
logging_retention_in_days = optional(number, 180)
logging_kms_key_id = optional(string, null)
log_class = optional(string, "STANDARD")
lambda_s3_bucket = optional(string, null)
lambda_s3_key = optional(string, null)
lambda_s3_object_version = optional(string, null)
lambda_apigateway_access_log_settings = optional(object({
destination_arn = string
format = string
}), null)
repository_white_list = optional(list(string), [])
queue_selection_strategy = optional(string, "first")
storage_provider = object({
aws = object({
ssm = object({
kms_key_id = optional(string, null)
})
})
})
log_level = optional(string, "info")
lambda_runtime = optional(string, "nodejs24.x")
aws_partition = optional(string, "aws")
lambda_architecture = optional(string, "arm64")
github_app_parameters = object({
webhook_secret = map(string)
})
tracing_config = optional(object({
mode = optional(string, null)
capture_http_requests = optional(bool, false)
capture_error = optional(bool, false)
}), {})
lambda_tags = optional(map(string), {})
api_gw_source_arn = string
ssm_parameter_runner_matcher_config = list(object({
name = string
arn = string
version = string
}))
accept_events = optional(list(string), null)
})
| n/a | yes | +| [config](#input\_config) | Configuration object for all variables. |
object({
prefix = string
archive = optional(object({
enable = optional(bool, true)
retention_days = optional(number, 7)
}), {})
tags = optional(map(string), {})

lambda_subnet_ids = optional(list(string), [])
lambda_security_group_ids = optional(list(string), [])
sqs_job_queues_arns = list(string)
lambda_zip = optional(string, null)
lambda_memory_size = optional(number, 256)
lambda_timeout = optional(number, 10)
role_permissions_boundary = optional(string, null)
role_path = optional(string, null)
logging_retention_in_days = optional(number, 180)
logging_kms_key_id = optional(string, null)
log_class = optional(string, "STANDARD")
lambda_s3_bucket = optional(string, null)
lambda_s3_key = optional(string, null)
lambda_s3_object_version = optional(string, null)
lambda_apigateway_access_log_settings = optional(object({
destination_arn = string
format = string
}), null)
repository_white_list = optional(list(string), [])
queue_selection_strategy = optional(string, "first")
storage_provider = optional(object({
aws = optional(object({
ssm = optional(object({}), null)
}), {})
webhook = object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
})
dispatcher = object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
})
}))
log_level = optional(string, "info")
lambda_runtime = optional(string, "nodejs24.x")
aws_partition = optional(string, "aws")
lambda_architecture = optional(string, "arm64")
github_app_parameters = object({
webhook_secret = map(string)
})
tracing_config = optional(object({
mode = optional(string, null)
capture_http_requests = optional(bool, false)
capture_error = optional(bool, false)
}), {})
lambda_tags = optional(map(string), {})
api_gw_source_arn = string
ssm_parameter_runner_matcher_config = list(object({
name = string
arn = string
version = string
}))
accept_events = optional(list(string), null)
})
| n/a | yes | ## Outputs From aecf2231047312a4d5d476879a05ddc85289e84f Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Thu, 24 Sep 2026 12:28:42 +0200 Subject: [PATCH 05/44] refactor(ec2): isolate SSM runner policy --- .../aws/ec2/policies-runner.aws.ssm.tf | 41 ++++++++++++++++++ .../aws/ec2/policies-runner.tf | 43 ++----------------- 2 files changed, 45 insertions(+), 39 deletions(-) create mode 100644 modules/compute-providers/aws/ec2/policies-runner.aws.ssm.tf diff --git a/modules/compute-providers/aws/ec2/policies-runner.aws.ssm.tf b/modules/compute-providers/aws/ec2/policies-runner.aws.ssm.tf new file mode 100644 index 0000000000..c16f603a55 --- /dev/null +++ b/modules/compute-providers/aws/ec2/policies-runner.aws.ssm.tf @@ -0,0 +1,41 @@ +# AWS Systems Manager Parameter Store permissions returned to runner-config. +locals { + 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_parameter_arn_prefix}${local.ssm_config_path}" + cloudwatch_config_arn = "${local.ssm_config_arn}/cloudwatch_agent_config_runner" +} + +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}/*", + ] + } +} diff --git a/modules/compute-providers/aws/ec2/policies-runner.tf b/modules/compute-providers/aws/ec2/policies-runner.tf index b1841f11f2..8e16d58bbe 100644 --- a/modules/compute-providers/aws/ec2/policies-runner.tf +++ b/modules/compute-providers/aws/ec2/policies-runner.tf @@ -3,43 +3,7 @@ data "aws_caller_identity" "current" {} locals { - ssm_parameter_arn_prefix = "arn:${var.aws_partition}:ssm:${var.aws_region}:${data.aws_caller_identity.current.account_id}:parameter" - ec2_instance_arn_prefix = "arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:instance/" - ssm_config_arn = "${local.ssm_parameter_arn_prefix}${local.ssm_config_path}" - cloudwatch_config_arn = "${local.ssm_config_arn}/cloudwatch_agent_config_runner" -} - -data "aws_iam_policy_document" "ssm_parameters" { - statement { - effect = "Allow" - actions = [ - "ssm:DeleteParameter", - "ssm:GetParameters", - "ssm:GetParameter", - ] - resources = [ - "${local.ssm_parameter_arn_prefix}${local.ssm_root_path}/${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}/*", - ] - } + ec2_instance_arn_prefix = "arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:instance/" } data "aws_iam_policy_document" "session_manager" { @@ -181,11 +145,12 @@ locals { policy_json = data.aws_iam_policy_document.terminate_self.json } }, - { + var.storage_provider.aws.ssm != null ? { ssm_parameters = { name = "runner-ssm-parameters" - policy_json = data.aws_iam_policy_document.ssm_parameters.json + policy_json = data.aws_iam_policy_document.ssm_parameters[0].json } + } : { }, var.config.ssm_enabled ? { session_manager = { From 7e1d87f1e228592b6265a267e77f477981569baf Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Thu, 24 Sep 2026 12:29:05 +0200 Subject: [PATCH 06/44] refactor: decouple ssm in ec2 --- modules/compute-providers/aws/ec2/README.md | 14 +++++++------- .../compute-providers/aws/ec2/runner-instances.tf | 4 ++-- modules/compute-providers/aws/ec2/variables.tf | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/modules/compute-providers/aws/ec2/README.md b/modules/compute-providers/aws/ec2/README.md index 26d9f2d8cc..8374424267 100644 --- a/modules/compute-providers/aws/ec2/README.md +++ b/modules/compute-providers/aws/ec2/README.md @@ -10,15 +10,15 @@ EC2 is the only active compute provider. The parent runner configuration selects ## Requirements | Name | Version | -|------|---------| +| ---- | ------- | | [terraform](#requirement\_terraform) | >= 1.5.6 | | [aws](#requirement\_aws) | >= 6.33 | ## Providers | Name | Version | -|------|---------| -| [aws](#provider\_aws) | >= 6.33 | +| ---- | ------- | +| [aws](#provider\_aws) | 6.66.0 | | [terraform](#provider\_terraform) | n/a | ## Modules @@ -28,7 +28,7 @@ No modules. ## Resources | Name | Type | -|------|------| +| ---- | ---- | | [aws_cloudwatch_log_group.gh_runners](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | | [aws_iam_instance_profile.runner](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_instance_profile) | resource | | [aws_iam_policy.ami_id_ssm_parameter_read](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | @@ -58,7 +58,7 @@ No modules. ## Inputs | Name | Description | Type | Default | Required | -|------|-------------|------|---------|:--------:| +| ---- | ----------- | ---- | ------- | :------: | | [aws\_partition](#input\_aws\_partition) | AWS partition used to construct IAM ARNs. | `string` | `"aws"` | no | | [aws\_region](#input\_aws\_region) | AWS region used by compute-provider resources and policy documents. | `string` | n/a | yes | | [config](#input\_config) | EC2 compute-provider configuration. Paths match `compute_provider.aws.ec2` in the runner configuration.

- `ami`: Optional AMI discovery and encryption configuration. Null selects defaults for `runner.os` and `runner.architecture`.
- `ami.filter`: AMI filter names mapped to accepted values and merged over the provider defaults.
- `ami.owners`: AWS account IDs or aliases allowed to own the selected AMI.
- `ami.id_ssm_parameter`: Optional externally managed SSM parameter containing the AMI ID. Its object presence is the plan-time ownership discriminator.
- `ami.id_ssm_parameter.arn`: ARN of the external AMI-ID parameter. The ARN may remain unknown until apply.
- `ami.kms_key`: Optional customer-managed KMS key required for encrypted AMIs or snapshots. Its object presence is the plan-time policy discriminator.
- `ami.kms_key.arn`: ARN of the AMI KMS key. The ARN may remain unknown until apply.
- `vpc_id`: VPC in which runner networking resources are created.
- `subnet_ids`: Subnets from which the control plane may launch runners.
- `overrides.name_runner`: Optional Name tag override for runner compute resources.
- `overrides.name_sg`: Optional Name tag override for the managed security group.
- `instance_profile`: Optional externally managed instance profile. Its object presence is the plan-time ownership discriminator.
- `instance_profile.name`: Name of the external instance profile. The name may remain unknown until apply.
- `instance_profile_path`: IAM path for the provider-managed instance profile. Null derives the path from `prefix`.
- `binaries_syncer.enabled`: Uses the synchronized runner distribution from S3 during bootstrap.
- `binaries_syncer.s3`: S3 object containing the synchronized runner distribution. Required when synchronization is enabled.
- `binaries_syncer.s3.arn`: Runner-distribution bucket ARN used by IAM policies.
- `binaries_syncer.s3.id`: Runner-distribution bucket name used in the bootstrap URI.
- `binaries_syncer.s3.key`: Runner-distribution object key.
- `block_device_mappings`: EBS mappings added to the launch template.
- `block_device_mappings[].delete_on_termination`: Deletes the volume when its runner terminates.
- `block_device_mappings[].device_name`: Device name exposed to the runner instance.
- `block_device_mappings[].encrypted`: Enables EBS encryption.
- `block_device_mappings[].iops`: Provisioned IOPS for volume types that support configurable IOPS.
- `block_device_mappings[].kms_key_id`: KMS key ID or ARN used to encrypt the volume.
- `block_device_mappings[].snapshot_id`: Snapshot used to initialize the volume.
- `block_device_mappings[].throughput`: Provisioned throughput for volume types that support it.
- `block_device_mappings[].volume_initialization_rate`: Fixed initialization rate for supported snapshot-backed volumes.
- `block_device_mappings[].volume_size`: EBS volume size in GiB.
- `block_device_mappings[].volume_type`: EBS volume type.
- `ebs_optimized`: Requests EBS-optimized instances.
- `instance_target_capacity_type`: Primary capacity type, either `spot` or `on-demand`.
- `instance_allocation_strategy`: EC2 Fleet allocation strategy.
- `instance_type_priorities`: Optional numeric priorities keyed by instance type.
- `instance_max_spot_price`: Optional maximum hourly Spot price.
- `instance_types`: EC2 instance types available to the control plane.
- `user_data`: Runner bootstrap user-data configuration.
- `user_data.enabled`: Enables launch-template user data.
- `user_data.template`: Optional path to a custom user-data template.
- `user_data.content`: Optional complete user-data content used instead of a template.
- `user_data.pre_install`: Script inserted before runner installation.
- `user_data.post_install`: Script inserted after runner installation.
- `user_data.debug_logging_enabled`: Enables verbose user-data tracing, which can expose secrets.
- `ssm_enabled`: Includes Session Manager permissions in the provider's runner policy group.
- `create_service_linked_role_spot`: Allows scale-up to create the EC2 Spot service-linked role.
- `cloudwatch_agent.enabled`: Enables CloudWatch agent configuration for runner instances.
- `cloudwatch_agent.config`: Optional complete CloudWatch agent configuration.
- `managed_security_group_enabled`: Creates and attaches the provider-managed security group.
- `log_files`: Optional files collected by the CloudWatch agent. Null uses provider defaults.
- `log_files[].log_group_name`: CloudWatch log-group name before optional prefixing.
- `log_files[].prefix_log_group`: Prefixes the log-group name with the runner configuration path.
- `log_files[].file_path`: File or glob read by the CloudWatch agent.
- `log_files[].log_stream_name`: CloudWatch log-stream name template.
- `log_files[].log_class`: CloudWatch log-group class for the collected file.
- `key_name`: Optional EC2 key-pair name.
- `additional_security_group_ids`: Existing security groups attached to runners.
- `detailed_monitoring_enabled`: Enables detailed EC2 monitoring.
- `egress_rules`: Rules created on the managed security group.
- `egress_rules[].cidr_blocks`: IPv4 CIDR destinations.
- `egress_rules[].ipv6_cidr_blocks`: IPv6 CIDR destinations.
- `egress_rules[].prefix_list_ids`: AWS prefix-list destinations.
- `egress_rules[].from_port`: First destination port in the permitted range.
- `egress_rules[].protocol`: IP protocol name or number. Use `-1` for all protocols.
- `egress_rules[].security_groups`: Destination security-group IDs.
- `egress_rules[].self`: Allows traffic to the managed security group itself.
- `egress_rules[].to_port`: Last destination port in the permitted range.
- `egress_rules[].description`: Optional rule description.
- `tags`: Runner instance, volume, network-interface, and eligible Spot-request tags. Provider-required bootstrap tags take final precedence.
- `metadata_options`: Instance Metadata Service configuration.
- `metadata_options.instance_metadata_tags`: Exposes instance tags through Instance Metadata Service when enabled.
- `metadata_options.http_endpoint`: Enables or disables the Instance Metadata Service endpoint.
- `metadata_options.http_tokens`: Controls whether IMDSv2 session tokens are optional or required.
- `metadata_options.http_put_response_hop_limit`: Network hop limit for Instance Metadata Service token responses.
- `credit_specification`: CPU credit mode for burstable instance types.
- `cpu_options`: CPU topology and processor-feature configuration.
- `cpu_options.core_count`: Number of CPU cores exposed to the runner instance.
- `cpu_options.threads_per_core`: Number of hardware threads exposed per CPU core.
- `cpu_options.amd_sev_snp`: Enables or disables AMD SEV-SNP on supported instance types.
- `cpu_options.nested_virtualization`: Enables or disables nested virtualization on supported instance types.
- `placement`: EC2 placement configuration.
- `placement.affinity`: Dedicated Host affinity setting.
- `placement.availability_zone`: Availability Zone in which runner instances are placed.
- `placement.group_id`: Placement-group ID.
- `placement.group_name`: Placement-group name.
- `placement.host_id`: Dedicated Host ID.
- `placement.host_resource_group_arn`: ARN of the host resource group used for placement.
- `placement.spread_domain`: Spread-domain placement value.
- `placement.tenancy`: Instance tenancy, such as `default`, `dedicated`, or `host`.
- `placement.partition_number`: Placement-group partition number.
- `license_specifications`: License Manager configurations added to the launch template.
- `license_specifications[].license_configuration_arn`: ARN of an AWS License Manager license configuration.
- `associate_public_ipv4_address`: Associates a public IPv4 address with runner network interfaces.
- `network_interfaces`: Advanced network interface configuration for the launch template. Leave empty to keep using `associate_public_ipv4_address` for a simple single-interface setup.
- `on_demand_failover_for_errors`: EC2 errors that trigger on-demand fallback after a Spot failure.
- `scale_errors`: EC2 errors treated as retryable scale-up failures.
- `use_dedicated_host`: Enables the dedicated-host launch path. |
object({
ami = optional(object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
id_ssm_parameter = optional(object({
arn = string
}), null)
kms_key = optional(object({
arn = string
}), null)
}), null)
vpc_id = string
subnet_ids = list(string)
overrides = optional(object({
name_runner = optional(string, "")
name_sg = optional(string, "")
}), {})
instance_profile = optional(object({
name = string
}), null)
instance_profile_path = optional(string, null)
binaries_syncer = optional(object({
enabled = optional(bool, true)
s3 = optional(object({
arn = string
id = string
key = string
}), null)
}), {})
block_device_mappings = optional(list(object({
delete_on_termination = optional(bool, true)
device_name = optional(string, "/dev/xvda")
encrypted = optional(bool, true)
iops = optional(number)
kms_key_id = optional(string)
snapshot_id = optional(string)
throughput = optional(number)
volume_initialization_rate = optional(number)
volume_size = number
volume_type = optional(string, "gp3")
})), [{ volume_size = 30 }])
ebs_optimized = optional(bool, false)
instance_target_capacity_type = optional(string, "spot")
instance_allocation_strategy = optional(string, "lowest-price")
instance_type_priorities = optional(map(number), null)
instance_max_spot_price = optional(string, null)
instance_types = list(string)
user_data = optional(object({
enabled = optional(bool, true)
template = optional(string, null)
content = optional(string, null)
pre_install = optional(string, "")
post_install = optional(string, "")
debug_logging_enabled = optional(bool, false)
}), {})
ssm_enabled = optional(bool, false)
create_service_linked_role_spot = optional(bool, false)
cloudwatch_agent = optional(object({
enabled = optional(bool, true)
config = optional(string, null)
}), {})
managed_security_group_enabled = optional(bool, true)
log_files = optional(list(object({
log_group_name = string
prefix_log_group = bool
file_path = string
log_stream_name = string
log_class = optional(string, "STANDARD")
})), null)
key_name = optional(string, null)
additional_security_group_ids = optional(list(string), [])
detailed_monitoring_enabled = optional(bool, false)
egress_rules = optional(list(object({
cidr_blocks = list(string)
ipv6_cidr_blocks = list(string)
prefix_list_ids = list(string)
from_port = number
protocol = string
security_groups = list(string)
self = bool
to_port = number
description = string
})), [{
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
prefix_list_ids = null
from_port = 0
protocol = "-1"
security_groups = null
self = null
to_port = 0
description = null
}])
tags = optional(map(string), {})
metadata_options = optional(object({
instance_metadata_tags = optional(string, "enabled")
http_endpoint = optional(string, "enabled")
http_tokens = optional(string, "required")
http_put_response_hop_limit = optional(number, 1)
}), {})
credit_specification = optional(string, null)
cpu_options = optional(object({
core_count = optional(number)
threads_per_core = optional(number)
amd_sev_snp = optional(string)
nested_virtualization = optional(string)
}), null)
placement = optional(object({
affinity = optional(string)
availability_zone = optional(string)
group_id = optional(string)
group_name = optional(string)
host_id = optional(string)
host_resource_group_arn = optional(string)
spread_domain = optional(string)
tenancy = optional(string)
partition_number = optional(number)
}), null)
license_specifications = optional(list(object({
license_configuration_arn = string
})), [])
associate_public_ipv4_address = optional(bool, false)
network_interfaces = optional(list(object({
associate_carrier_ip_address = optional(bool)
associate_public_ip_address = optional(bool)
delete_on_termination = optional(bool)
description = optional(string)
device_index = optional(number)
interface_type = optional(string)
ipv4_address_count = optional(number)
ipv4_addresses = optional(list(string))
ipv4_prefix_count = optional(number)
ipv4_prefixes = optional(list(string))
ipv6_address_count = optional(number)
ipv6_addresses = optional(list(string))
ipv6_prefix_count = optional(number)
ipv6_prefixes = optional(list(string))
network_card_index = optional(number)
network_interface_id = optional(string)
primary_ipv6 = optional(bool)
private_ip_address = optional(string)
security_groups = optional(list(string))
subnet_id = optional(string)
connection_tracking_specification = optional(object({
tcp_established_timeout = optional(number)
udp_stream_timeout = optional(number)
udp_timeout = optional(number)
}))
ena_srd_specification = optional(object({
ena_srd_enabled = optional(bool)
ena_srd_udp_specification = optional(object({
ena_srd_udp_enabled = optional(bool)
}))
}))
})), [])
on_demand_failover_for_errors = optional(list(string), [])
scale_errors = optional(list(string), [
"UnfulfillableCapacity",
"MaxSpotInstanceCountExceeded",
"TargetCapacityLimitExceededException",
"RequestLimitExceeded",
"ResourceLimitExceeded",
"MaxSpotInstanceCountExceeded",
"MaxSpotFleetRequestCountExceeded",
"InsufficientInstanceCapacity",
"InsufficientCapacityOnHost",
])
use_dedicated_host = optional(bool, false)
})
| n/a | yes | @@ -66,13 +66,13 @@ No modules. | [observability](#input\_observability) | CloudWatch Logs settings available to compute-provider runner log groups.

- `logs.retention_in_days`: Retention period for provider-owned runner log groups.
- `logs.kms_key_id`: Optional KMS key ID or ARN used to encrypt runner log groups.
- `logs.tags`: Shared log-group tags that override module-level `tags`. |
object({
logs = optional(object({
retention_in_days = optional(number, 180)
kms_key_id = optional(string, null)
tags = optional(map(string), {})
}), {})
})
| `{}` | no | | [prefix](#input\_prefix) | Prefix used to identify resources created for the runner configuration. | `string` | `"github-actions"` | no | | [runner](#input\_runner) | Provider-neutral runner settings consumed by compute providers.

- `os`: Runner operating system. Supported values are `linux`, `osx`, and `windows`.
- `architecture`: Runner distribution architecture.
- `name_prefix`: Prefix added to registered runner names.
- `run_as_root`: Runs the runner service as root.
- `run_as`: Operating-system user used when `run_as_root` is false.
- `hooks.job_started`: Script installed as the runner job-started hook.
- `hooks.job_completed`: Script installed as the runner job-completed hook.
- `iam.role.arn`: Resolved runner-role ARN referenced by provider policies and resources.
- `iam.role.name`: Resolved runner-role name used by provider resources.
- `iam.role.managed`: Whether runner-config manages the resolved runner role.
- `iam.managed_policy_arns`: Common managed-policy ARNs returned with the provider-specific runner policies for attachment by runner-config.
- `iam.path`: IAM path available to provider-managed IAM resources. Null derives the path from `prefix`. |
object({
os = optional(string, "linux")
architecture = optional(string, "x64")
name_prefix = optional(string, "")
run_as_root = optional(bool, false)
run_as = optional(string, "ec2-user")
hooks = optional(object({
job_started = optional(string, "")
job_completed = optional(string, "")
}), {})
iam = object({
role = object({
arn = string
name = string
managed = optional(bool, true)
})
managed_policy_arns = optional(map(string), {})
path = optional(string, null)
})
})
| n/a | yes | -| [storage\_provider](#input\_storage\_provider) | Storage-provider configuration available to compute-provider bootstrap resources.

- `aws.ssm`: AWS Systems Manager Parameter Store configuration, when SSM is selected.
- `aws.ssm.paths.root`: Root Parameter Store path for the runner configuration.
- `aws.ssm.paths.tokens`: Path segment used for registration tokens and just-in-time configuration.
- `aws.ssm.paths.config`: Path segment used for persistent runner and provider configuration.
- `aws.ssm.tags`: Shared SSM tags that override module-level `tags`.
- `aws.ssm.parameters.tags`: Parameter-specific tags that override module-level and shared SSM tags. |
object({
aws = object({
ssm = object({
paths = object({
root = string
tokens = string
config = string
})
tags = optional(map(string), {})
parameters = optional(object({
tags = optional(map(string), {})
}), {})
})
})
})
| n/a | yes | +| [storage\_provider](#input\_storage\_provider) | Storage-provider configuration available to compute-provider bootstrap resources.

- `aws.ssm`: AWS Systems Manager Parameter Store configuration, when SSM is selected.
- `aws.ssm.paths.root`: Root Parameter Store path for the runner configuration.
- `aws.ssm.paths.tokens`: Path segment used for registration tokens and just-in-time configuration.
- `aws.ssm.paths.config`: Path segment used for persistent runner and provider configuration.
- `aws.ssm.tags`: Shared SSM tags that override module-level `tags`.
- `aws.ssm.parameters.tags`: Parameter-specific tags that override module-level and shared SSM tags. |
object({
aws = object({
ssm = optional(object({
paths = object({
root = string
tokens = string
config = string
})
tags = optional(map(string), {})
parameters = optional(object({
tags = optional(map(string), {})
}), {})
}), null)
})
})
| n/a | yes | | [tags](#input\_tags) | Base tags available to taggable compute-provider resources. Provider-specific tags override this map within their documented scopes. | `map(string)` | `{}` | no | ## Outputs | Name | Description | -|------|-------------| +| ---- | ----------- | | [environment\_variables](#output\_environment\_variables) | Provider-specific Lambda environment variable fragments consumed by runner-config. | | [policies](#output\_policies) | Provider-specific IAM policy fragments consumed by runner-config. | | [provider](#output\_provider) | Nested EC2 compute-provider contract consumed by runner-config. | diff --git a/modules/compute-providers/aws/ec2/runner-instances.tf b/modules/compute-providers/aws/ec2/runner-instances.tf index 6a69f4f838..8f5865edd9 100644 --- a/modules/compute-providers/aws/ec2/runner-instances.tf +++ b/modules/compute-providers/aws/ec2/runner-instances.tf @@ -10,8 +10,8 @@ locals { ssm_parameter_tags = merge( local.provider_tags, - var.storage_provider.aws.ssm.tags, - var.storage_provider.aws.ssm.parameters.tags, + try(var.storage_provider.aws.ssm.tags, {}), + try(var.storage_provider.aws.ssm.parameters.tags, {}), ) log_group_tags = merge( diff --git a/modules/compute-providers/aws/ec2/variables.tf b/modules/compute-providers/aws/ec2/variables.tf index 448185a853..1473dd88b6 100644 --- a/modules/compute-providers/aws/ec2/variables.tf +++ b/modules/compute-providers/aws/ec2/variables.tf @@ -367,7 +367,7 @@ variable "storage_provider" { EOT type = object({ aws = object({ - ssm = object({ + ssm = optional(object({ paths = object({ root = string tokens = string @@ -377,7 +377,7 @@ variable "storage_provider" { parameters = optional(object({ tags = optional(map(string), {}) }), {}) - }) + }), null) }) }) From cec4c2667ffecd16cc37dd1ee54ff23f60a3d0e1 Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Thu, 24 Sep 2026 13:42:33 +0200 Subject: [PATCH 07/44] refactor: cleanup ec2 module --- modules/compute-providers/aws/ec2/ami.tf | 58 ++++++++++ .../aws/ec2/control-plane.tf | 30 +++--- modules/compute-providers/aws/ec2/logging.tf | 11 -- .../aws/ec2/policies-runner.aws.ssm.tf | 41 ------- .../aws/ec2/policies-runner.tf | 18 +--- .../aws/ec2/runner-config.tf | 25 ----- .../aws/ec2/runner-instances.tf | 74 +------------ .../aws/ec2/storage-provider.aws.ssm.tf | 101 ++++++++++++++++++ .../aws/ec2/tests/provider.tftest.hcl | 14 +-- .../compute-providers/aws/ec2/variables.tf | 27 ++--- 10 files changed, 194 insertions(+), 205 deletions(-) create mode 100644 modules/compute-providers/aws/ec2/ami.tf delete mode 100644 modules/compute-providers/aws/ec2/policies-runner.aws.ssm.tf delete mode 100644 modules/compute-providers/aws/ec2/runner-config.tf create mode 100644 modules/compute-providers/aws/ec2/storage-provider.aws.ssm.tf diff --git a/modules/compute-providers/aws/ec2/ami.tf b/modules/compute-providers/aws/ec2/ami.tf new file mode 100644 index 0000000000..e96364b859 --- /dev/null +++ b/modules/compute-providers/aws/ec2/ami.tf @@ -0,0 +1,58 @@ +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 + } + 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) + ami_id_ssm_external = local.ami_config.ssm_parameter != null && local.ami_config.ssm_parameter.path == null + ami_id_ssm_module_managed = local.ami_config.ssm_parameter != null && local.ami_config.ssm_parameter.path != null + 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_config.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 + } + ) +} diff --git a/modules/compute-providers/aws/ec2/control-plane.tf b/modules/compute-providers/aws/ec2/control-plane.tf index acc7b7ebad..a910a8c840 100644 --- a/modules/compute-providers/aws/ec2/control-plane.tf +++ b/modules/compute-providers/aws/ec2/control-plane.tf @@ -63,10 +63,14 @@ data "aws_iam_policy_document" "scale_up" { resources = [var.runner.iam.role.arn] } - statement { - effect = "Allow" - 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_id_ssm_module_managed || local.ami_id_ssm_external ? [1] : [] + + content { + effect = "Allow" + 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" { @@ -147,10 +151,14 @@ data "aws_iam_policy_document" "pool" { resources = [var.runner.iam.role.arn] } - statement { - effect = "Allow" - actions = ["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_id_ssm_module_managed || local.ami_id_ssm_external ? [1] : [] + + content { + effect = "Allow" + actions = ["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" { @@ -191,7 +199,7 @@ data "aws_iam_policy_document" "service_linked_role" { } locals { - scale_up_environment_variables = merge({ + scale_up_environment_variables = { AMI_ID_SSM_PARAMETER_NAME = local.ami_id_ssm_parameter_name INSTANCE_ALLOCATION_STRATEGY = var.config.instance_allocation_strategy INSTANCE_MAX_SPOT_PRICE = var.config.instance_max_spot_price @@ -203,9 +211,7 @@ locals { ENABLE_ON_DEMAND_FAILOVER_FOR_ERRORS = jsonencode(var.config.on_demand_failover_for_errors) SCALE_ERRORS = jsonencode(var.config.scale_errors) USE_DEDICATED_HOST = var.config.use_dedicated_host - }, var.storage_provider.aws.ssm == null ? { - EC2_INSTANCE_ARN_PREFIX = local.ec2_instance_arn_prefix - } : {}) + } scale_down_environment_variables = {} diff --git a/modules/compute-providers/aws/ec2/logging.tf b/modules/compute-providers/aws/ec2/logging.tf index 1a58d6f080..736b90040b 100644 --- a/modules/compute-providers/aws/ec2/logging.tf +++ b/modules/compute-providers/aws/ec2/logging.tf @@ -54,17 +54,6 @@ locals { } - -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 -} - resource "aws_cloudwatch_log_group" "gh_runners" { count = length(local.loggroups_names) name = local.loggroups_names[count.index] diff --git a/modules/compute-providers/aws/ec2/policies-runner.aws.ssm.tf b/modules/compute-providers/aws/ec2/policies-runner.aws.ssm.tf deleted file mode 100644 index c16f603a55..0000000000 --- a/modules/compute-providers/aws/ec2/policies-runner.aws.ssm.tf +++ /dev/null @@ -1,41 +0,0 @@ -# AWS Systems Manager Parameter Store permissions returned to runner-config. -locals { - 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_parameter_arn_prefix}${local.ssm_config_path}" - cloudwatch_config_arn = "${local.ssm_config_arn}/cloudwatch_agent_config_runner" -} - -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}/*", - ] - } -} diff --git a/modules/compute-providers/aws/ec2/policies-runner.tf b/modules/compute-providers/aws/ec2/policies-runner.tf index 8e16d58bbe..bfb16fc8da 100644 --- a/modules/compute-providers/aws/ec2/policies-runner.tf +++ b/modules/compute-providers/aws/ec2/policies-runner.tf @@ -2,10 +2,6 @@ # the common runner role. data "aws_caller_identity" "current" {} -locals { - ec2_instance_arn_prefix = "arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:instance/" -} - data "aws_iam_policy_document" "session_manager" { statement { effect = "Allow" @@ -121,16 +117,11 @@ data "aws_iam_policy_document" "cloudwatch" { ] resources = ["*"] } - - statement { - effect = "Allow" - actions = ["ssm:GetParameter"] - resources = ["${local.cloudwatch_config_arn}/*"] - } } locals { runner_inline_policies = merge( + local.ssm_runner_inline_policies, { describe_tags = { name = "runner-describe-tags" @@ -145,13 +136,6 @@ locals { policy_json = data.aws_iam_policy_document.terminate_self.json } }, - var.storage_provider.aws.ssm != null ? { - ssm_parameters = { - name = "runner-ssm-parameters" - policy_json = data.aws_iam_policy_document.ssm_parameters[0].json - } - } : { - }, var.config.ssm_enabled ? { session_manager = { name = "runner-ssm-session" diff --git a/modules/compute-providers/aws/ec2/runner-config.tf b/modules/compute-providers/aws/ec2/runner-config.tf deleted file mode 100644 index ca91996cae..0000000000 --- a/modules/compute-providers/aws/ec2/runner-config.tf +++ /dev/null @@ -1,25 +0,0 @@ -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] -} diff --git a/modules/compute-providers/aws/ec2/runner-instances.tf b/modules/compute-providers/aws/ec2/runner-instances.tf index 8f5865edd9..70cba54bce 100644 --- a/modules/compute-providers/aws/ec2/runner-instances.tf +++ b/modules/compute-providers/aws/ec2/runner-instances.tf @@ -8,12 +8,6 @@ locals { var.tags, ) - ssm_parameter_tags = merge( - local.provider_tags, - try(var.storage_provider.aws.ssm.tags, {}), - try(var.storage_provider.aws.ssm.parameters.tags, {}), - ) - log_group_tags = merge( local.provider_tags, var.observability.logs.tags, @@ -29,14 +23,12 @@ locals { var.config.tags, { "ghr:environment" = var.prefix - "ghr:ssm_config_path" = local.ssm_config_path "ghr:runner_name_prefix" = var.runner.name_prefix }, + local.ssm_runner_tags ) role_path = var.runner.iam.path == null ? "/${var.prefix}/" : var.runner.iam.path - ssm_root_path = var.storage_provider.aws.ssm.paths.root - ssm_config_path = "${local.ssm_root_path}/${var.storage_provider.aws.ssm.paths.config}" instance_profile_path = var.config.instance_profile_path == null ? "/${var.prefix}/" : var.config.instance_profile_path userdata_template = var.config.user_data.template == null ? local.default_userdata_template[var.runner.os] : var.config.user_data.template s3_location_runner_distribution = var.config.binaries_syncer.enabled ? "s3://${try(var.config.binaries_syncer.s3.id, "")}/${try(var.config.binaries_syncer.s3.key, "")}" : "" @@ -64,22 +56,6 @@ locals { "osx" = "${path.module}/templates/start-runner-osx.sh" } - # Handle AMI configuration - ami_config = var.config.ami != null ? var.config.ami : { - filter = local.default_ami[var.runner.os] - owners = ["amazon"] - id_ssm_parameter = null - kms_key = null - } - 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) - ami_id_ssm_external = local.ami_config.id_ssm_parameter != null - ami_id_ssm_module_managed = !local.ami_id_ssm_external - ami_id_ssm_parameter_arn = local.ami_id_ssm_external ? local.ami_config.id_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 - user_data = var.config.user_data.enabled ? (var.config.user_data.content == null ? templatefile(local.userdata_template, { enable_debug_logging = var.config.user_data.debug_logging_enabled s3_location_runner_distribution = local.s3_location_runner_distribution @@ -95,13 +71,6 @@ locals { metadata_tags = var.config.metadata_options != null ? var.config.metadata_options.instance_metadata_tags : "enabled" enable_cloudwatch_agent = var.config.cloudwatch_agent.enabled }) - ghes_url = var.github.enterprise_server.url - ghes_ssl_verify = var.github.enterprise_server.ssl_verify - - ## retain these for backwards compatibility - environment = var.prefix - enable_cloudwatch_agent = var.config.cloudwatch_agent.enabled - ssm_key_cloudwatch_agent_config = var.config.cloudwatch_agent.enabled ? aws_ssm_parameter.cloudwatch_agent_config_runner[0].name : "" }) : var.config.user_data.content) : "" encoded_user_data = ( @@ -148,45 +117,6 @@ locals { ) } -data "aws_ami" "runner" { - count = local.ami_id_ssm_module_managed ? 1 : 0 - - 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.ssm_config_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 - } - ) -} - resource "aws_launch_template" "runner" { name = "${var.prefix}-action-runner" @@ -276,7 +206,7 @@ resource "aws_launch_template" "runner" { } instance_initiated_shutdown_behavior = "terminate" - image_id = "resolve:ssm:${local.ami_id_ssm_module_managed ? aws_ssm_parameter.runner_ami_id[0].arn : local.ami_id_ssm_parameter_arn}" + image_id = local.image_id key_name = var.config.key_name ebs_optimized = var.config.ebs_optimized diff --git a/modules/compute-providers/aws/ec2/storage-provider.aws.ssm.tf b/modules/compute-providers/aws/ec2/storage-provider.aws.ssm.tf new file mode 100644 index 0000000000..ef9d51c58f --- /dev/null +++ b/modules/compute-providers/aws/ec2/storage-provider.aws.ssm.tf @@ -0,0 +1,101 @@ +# 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}/*", + ] + } +} + +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 = var.storage_provider.aws.ssm.paths.root + ssm_config_path = "${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_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 = { + "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] +} diff --git a/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl b/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl index a6a060f1ce..65289baa32 100644 --- a/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl +++ b/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl @@ -46,7 +46,7 @@ variables { ami = { filter = { state = ["available"] } owners = ["amazon"] - id_ssm_parameter = { + ssm_parameter = { arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/ami-id" } kms_key = null @@ -214,7 +214,7 @@ run "accepts_partial_typed_compute_options" { ami = { filter = { state = ["available"] } owners = ["amazon"] - id_ssm_parameter = { + ssm_parameter = { arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/ami-id" } kms_key = null @@ -271,10 +271,12 @@ run "separates_provider_runner_and_ssm_tags" { subnet_ids = ["subnet-12345678"] instance_types = ["m5.large"] ami = { - filter = { state = ["available"] } - owners = ["amazon"] - id_ssm_parameter = null - kms_key = null + filter = { state = ["available"] } + owners = ["amazon"] + ssm_parameter = { + path = "/github-runner/provider-test/config" + } + kms_key = null } binaries_syncer = { enabled = false diff --git a/modules/compute-providers/aws/ec2/variables.tf b/modules/compute-providers/aws/ec2/variables.tf index 1473dd88b6..7681ff321d 100644 --- a/modules/compute-providers/aws/ec2/variables.tf +++ b/modules/compute-providers/aws/ec2/variables.tf @@ -28,8 +28,9 @@ variable "config" { - `ami`: Optional AMI discovery and encryption configuration. Null selects defaults for `runner.os` and `runner.architecture`. - `ami.filter`: AMI filter names mapped to accepted values and merged over the provider defaults. - `ami.owners`: AWS account IDs or aliases allowed to own the selected AMI. - - `ami.id_ssm_parameter`: Optional externally managed SSM parameter containing the AMI ID. Its object presence is the plan-time ownership discriminator. - - `ami.id_ssm_parameter.arn`: ARN of the external AMI-ID parameter. The ARN may remain unknown until apply. + - `ami.ssm_parameter`: Optional AMI-ID SSM parameter configuration. Set `arn` to use an existing parameter or `path` to create one managed by this module. + - `ami.ssm_parameter.path`: Parent path under which the module creates the `ami_id` parameter. + - `ami.ssm_parameter.arn`: ARN of an existing AMI-ID parameter. - `ami.kms_key`: Optional customer-managed KMS key required for encrypted AMIs or snapshots. Its object presence is the plan-time policy discriminator. - `ami.kms_key.arn`: ARN of the AMI KMS key. The ARN may remain unknown until apply. - `vpc_id`: VPC in which runner networking resources are created. @@ -127,8 +128,9 @@ variable "config" { ami = optional(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 @@ -337,23 +339,6 @@ variable "runner" { nullable = false } -variable "github" { - description = <<-EOT - GitHub Enterprise Server settings available to compute-provider bootstrap data. - - - `enterprise_server.url`: Optional GitHub Enterprise Server base URL. Null selects GitHub.com. - - `enterprise_server.ssl_verify`: Enables TLS certificate verification for GitHub Enterprise Server. - EOT - type = object({ - enterprise_server = optional(object({ - url = optional(string, null) - ssl_verify = optional(bool, true) - }), {}) - }) - default = {} - nullable = false -} - variable "storage_provider" { description = <<-EOT Storage-provider configuration available to compute-provider bootstrap resources. From 024511499807c4c4003ea58ad386086d20c5b908 Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Thu, 24 Sep 2026 13:44:17 +0200 Subject: [PATCH 08/44] refactor: cleanip runner-config --- ...-runner-orchestration-provider-boundary.md | 2 +- modules/multi-runner/.terraform.lock.hcl | 71 ------------------- .../runner-config/compute-provider.aws.ec2.tf | 1 - .../runner-config-housekeeper/README.md | 10 ++- .../computed-iam-inputs.tf | 2 +- modules/runner-config/tests/pool.tftest.hcl | 2 +- modules/runner-config/tests/tags.tftest.hcl | 2 +- .../variables.compute-provider.tf | 10 +-- modules/runners/ssm-housekeeper.tf | 2 +- scripts/migrate_multi_runner_state.py | 20 +++--- 10 files changed, 30 insertions(+), 92 deletions(-) delete mode 100644 modules/multi-runner/.terraform.lock.hcl diff --git a/docs/adr/002-runner-orchestration-provider-boundary.md b/docs/adr/002-runner-orchestration-provider-boundary.md index 3d77e2f3cd..728ba417ad 100644 --- a/docs/adr/002-runner-orchestration-provider-boundary.md +++ b/docs/adr/002-runner-orchestration-provider-boundary.md @@ -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//` | Owns provider-specific capacity resources and returns policy, environment, managed-policy, and resource capabilities. | Provider leaf modules live below `modules/orchestration-providers/`, diff --git a/modules/multi-runner/.terraform.lock.hcl b/modules/multi-runner/.terraform.lock.hcl deleted file mode 100644 index 9559f5fbd8..0000000000 --- a/modules/multi-runner/.terraform.lock.hcl +++ /dev/null @@ -1,71 +0,0 @@ -# This file is maintained automatically by "terraform init". -# Manual edits may be lost in future updates. - -provider "registry.terraform.io/hashicorp/aws" { - version = "6.63.0" - constraints = ">= 6.21.0, >= 6.33.0" - hashes = [ - "h1:9cre7jh1lSs/9igpgAcENMUAUlYW3HCtkav3up4oit0=", - "h1:dRlYHkc+r6fgzF57WC7Zjcmb6sF/6TTGDEgwGK+LAZY=", - "zh:005d56736afd17d963998c405cee6f434dbc23a415109f9435ff1542879ae611", - "zh:026ef126321a86ad7080b5d858e2527f96f5289678cbcd8856296e229c43339d", - "zh:06e0b58b2d1eddb5137fc86bee7ad2d07953c0bc3f57cccfc5ae0d2456068a3a", - "zh:07221735d61ababed84734e5ffcfc5bd59d01f29f029166ba5f2175895dceed1", - "zh:1a72db00583112bdb8c19b213a78a3f5de754fffc08f07e061f4e326289fab7d", - "zh:32968e74a53b03e97a084dc7050c22ef661fb5b3ea8a44f5a63e47bc45ad0e7c", - "zh:4b357dfe4b820e3e4acd2881cff8288b2186491e63416751f0d12692ba478ceb", - "zh:81e30884d7de686265e7d87bb92527e802878c65a378470ede2a1e9f4e40ccc9", - "zh:82e137297f6a5a08b9ce2138f7aabea245ad99495d9d9eff502f752d6ca90dbd", - "zh:8eb83b67099f0ea9df238a979dff933ff50ce06a2e3ff05a48556a10f10dd204", - "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:d0ba30886cbe41850fee689f51ef9088578f323cfd21817bb409951d43c465eb", - "zh:dd48e7089784454bc03d713e9057f5ca0ea1613bd402125054a51894957b7925", - "zh:f250fa81e54cf60fcb0e9c0fc4ac043f1ecc2ac24967f628b3609364fcab3d04", - "zh:f38fc09fc25a8d2cf89a4d4cd6a5ef7cb1aad72798dbdcad58b8876b6a551a54", - "zh:f7c7380fdf126e1901f2084588dbfd724c76cb131ccfa795a541219111103c06", - ] -} - -provider "registry.terraform.io/hashicorp/null" { - version = "3.3.1" - constraints = "~> 3.0, ~> 3.2" - hashes = [ - "h1:TuxJq10DVnRP7c5HBZPyyvQGcckNVfijyU1eXEu5e4M=", - "h1:m5FqidbIgh+E9OigiZh8/xbkvpUQFSj3hZo/jqNLCLQ=", - "zh:08c59776542ea16e5a8545752787b17ff412922182b4cfabe16139197be8ac44", - "zh:123109cc7e5ed6d515787fbc212f2a3fd5e75647bb24ab7c801ccd4d4ed42451", - "zh:14b3fa4372754b54844b41d5dbd4671a292d8d6828b90169061feb4d7b15dd05", - "zh:56a4daaa3212f57b764bf3d1f333141c6610c5f21abb240e0111221f7c7fa4d4", - "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:7e888a026dbacd2474a42264227ae35f639780f0f0c613529d10a95cd61988b3", - "zh:85a53646267e87d600df7124e4767ffde9bba3b6356d45d961618bdd68131cc7", - "zh:8ffa0e9c7c39b2ab0905b472465d6e35ef0b776b3f6273bb34c150340b61bff1", - "zh:9846510a1841530d4403f4818e233f91e3b3bade7441047599fbf800742f65be", - "zh:afa98d44860875f037c6def0a7e6ff208e042712ba771f620482b143cd336891", - "zh:bdca130d9ef27488ae0b13bc8fd8019e8bbdd4f2ceff29da066bd333165d68c5", - "zh:cb3b94cbca88210dd0d1f11e2b8a89333f48c3857faf8f70f589072ce7c28610", - "zh:f0c0ba87925fe32f84b80f7513b1efb1b0866f51f899ba825e95ad59ff09b018", - ] -} - -provider "registry.terraform.io/hashicorp/random" { - version = "3.9.0" - constraints = "~> 3.0" - hashes = [ - "h1:OO+IuvQJSPmWdN8AyyIEvPJbLvDQpgX/zbktoa9KsJE=", - "h1:UlBuNVuCGJ39tTv2c5gz2NRZnQbXfbIWbTzWcth5o74=", - "zh:161ad0bd9a75768c82f53fb6e7172a9d8be2d4889b012645a34795031aaf1bf1", - "zh:19dc9a5b17729725ccfc4f45b0500af0ee5bc6b6b160c7adb8f2bf617d2c80ea", - "zh:269eda8fe42daa7974d5a34d166c3ba9defe80cde86c01e4dadcfdf2e1f05e5f", - "zh:373f7c65566f8f2cc7f45d698654feb9d988996957e1266a69ca00c52d6d16d0", - "zh:5599d16804c41c83009ec621b6d6b6f74e102f5827678a4750f8809055546b61", - "zh:583be0440469a22bff70dcfa56593b01566860b29607437264adb51060cf46fc", - "zh:5f211d8ec3f2e1f414870d9584bfe26e6995560ef81c748f8447a48164767398", - "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:7b547fd16216761ef86efc3ed516ac5ac0c5c42b7c7eb24a08cef2d93f69ed5e", - "zh:7e7c0679daf2a382151d05068c8c3f0dae6b7b7dccf818827b73dd08638df2ef", - "zh:8089dec888a8038b9b4fb23b3df7e1057293dbc5b60b42cc47ff690d69d4b61b", - "zh:c51f15a031edfd6f23ce8ced3446ca7f8d8d647e2499890d7d5d10d5016d7257", - "zh:c94784f005708890dc6895afd53636ec00ec1e430b15d41e5aebfb1d4b39bd04", - ] -} diff --git a/modules/runner-config/compute-provider.aws.ec2.tf b/modules/runner-config/compute-provider.aws.ec2.tf index f359d0cd47..fdf2eab828 100644 --- a/modules/runner-config/compute-provider.aws.ec2.tf +++ b/modules/runner-config/compute-provider.aws.ec2.tf @@ -21,7 +21,6 @@ module "compute_aws_ec2" { managed_policy_arns = local.common_runner_managed_policy_arns }) }) - github = var.github observability = var.observability storage_provider = var.storage_provider } diff --git a/modules/runner-config/runner-config-housekeeper/README.md b/modules/runner-config/runner-config-housekeeper/README.md index 0568f36d34..4127273a46 100644 --- a/modules/runner-config/runner-config-housekeeper/README.md +++ b/modules/runner-config/runner-config-housekeeper/README.md @@ -1,3 +1,11 @@ +# Runner-config housekeeper module + +> This module is treated as an internal module; breaking changes do not trigger a major release bump. + +This provider-neutral child module owns the Lambda function, EventBridge schedule, IAM policies, and CloudWatch log group used to remove expired runner registration parameters from Parameter Store. + +The module is an implementation detail of the experimental runner configuration. It is composed by `runner-config` and is not intended to be called directly. + ## Requirements @@ -47,4 +55,4 @@ No modules. | Name | Description | |------|-------------| | [housekeeper](#output\_housekeeper) | Runner-config housekeeper Lambda resources. | - \ No newline at end of file + diff --git a/modules/runner-config/tests/fixtures/computed-iam-inputs/computed-iam-inputs.tf b/modules/runner-config/tests/fixtures/computed-iam-inputs/computed-iam-inputs.tf index 1e2cfd30c8..e06b972926 100644 --- a/modules/runner-config/tests/fixtures/computed-iam-inputs/computed-iam-inputs.tf +++ b/modules/runner-config/tests/fixtures/computed-iam-inputs/computed-iam-inputs.tf @@ -23,7 +23,7 @@ module "external_iam" { subnet_ids = ["subnet-12345678"] instance_types = ["m5.large"] ami = { - id_ssm_parameter = { + ssm_parameter = { arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/external-ami-${random_id.external.hex}" } kms_key = { diff --git a/modules/runner-config/tests/pool.tftest.hcl b/modules/runner-config/tests/pool.tftest.hcl index 6993212d91..ba3a76a317 100644 --- a/modules/runner-config/tests/pool.tftest.hcl +++ b/modules/runner-config/tests/pool.tftest.hcl @@ -36,7 +36,7 @@ variables { ami = { filter = { state = ["available"] } owners = ["amazon"] - id_ssm_parameter = { + ssm_parameter = { arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/external-ami-id" } kms_key = null diff --git a/modules/runner-config/tests/tags.tftest.hcl b/modules/runner-config/tests/tags.tftest.hcl index 34c78acd4e..db85b11c7a 100644 --- a/modules/runner-config/tests/tags.tftest.hcl +++ b/modules/runner-config/tests/tags.tftest.hcl @@ -41,7 +41,7 @@ variables { ami = { filter = { state = ["available"] } owners = ["amazon"] - id_ssm_parameter = { + ssm_parameter = { arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/external-ami-id" } kms_key = null diff --git a/modules/runner-config/variables.compute-provider.tf b/modules/runner-config/variables.compute-provider.tf index 954bc18c61..e0ad549a7b 100644 --- a/modules/runner-config/variables.compute-provider.tf +++ b/modules/runner-config/variables.compute-provider.tf @@ -22,8 +22,9 @@ variable "compute_provider" { - `aws.ec2.ami`: Optional AMI discovery or external AMI-parameter configuration. Null uses the operating-system and architecture defaults. - `aws.ec2.ami.filter`: EC2 AMI filters combined with the provider's default AMI-name filter. - `aws.ec2.ami.owners`: AWS account IDs or aliases allowed to own the selected AMI. - - `aws.ec2.ami.id_ssm_parameter`: Optional externally managed SSM parameter containing the AMI ID. Null creates a provider-managed AMI-ID parameter. The wrapper's presence is the plan-time ownership discriminator, so keep the object literal even when its ARN comes from another resource. - - `aws.ec2.ami.id_ssm_parameter.arn`: ARN of the externally managed SSM parameter. The ARN may be unknown until apply. + - `aws.ec2.ami.ssm_parameter`: Optional AMI-ID SSM parameter configuration. Set `arn` to use an existing parameter or `path` to create one managed by the provider. + - `aws.ec2.ami.ssm_parameter.path`: Parent path under which the provider creates the `ami_id` parameter. + - `aws.ec2.ami.ssm_parameter.arn`: ARN of an existing AMI-ID parameter. The ARN may be unknown until apply. - `aws.ec2.ami.kms_key`: Optional KMS key required to launch encrypted AMIs or snapshots. The wrapper's presence is the plan-time policy discriminator. - `aws.ec2.ami.kms_key.arn`: ARN of the KMS key. The ARN may be unknown until apply. - `aws.ec2.vpc_id`: VPC in which runner networking resources are created. @@ -126,8 +127,9 @@ variable "compute_provider" { ami = optional(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 diff --git a/modules/runners/ssm-housekeeper.tf b/modules/runners/ssm-housekeeper.tf index 378a752264..109ec24600 100644 --- a/modules/runners/ssm-housekeeper.tf +++ b/modules/runners/ssm-housekeeper.tf @@ -21,7 +21,7 @@ resource "aws_lambda_function" "ssm_housekeeper" { source_code_hash = var.lambda_s3_bucket == null ? filebase64sha256(local.lambda_zip) : null function_name = local.ssm_housekeeper_lambda_name role = aws_iam_role.ssm_housekeeper.arn - handler = "index.ssmHousekeeper" + handler = "index.runnerConfigHousekeeper" runtime = var.lambda_runtime timeout = local.ssm_housekeeper.lambda_timeout tags = merge(local.tags, var.lambda_tags) diff --git a/scripts/migrate_multi_runner_state.py b/scripts/migrate_multi_runner_state.py index 065bf0c72c..06266d643b 100644 --- a/scripts/migrate_multi_runner_state.py +++ b/scripts/migrate_multi_runner_state.py @@ -98,16 +98,16 @@ ('module.runners.module.job_retry[0].aws_lambda_event_source_mapping.job_retry', 'module.runner_configs.module.orchestration_webhook[0].module.job_retry[0].aws_lambda_event_source_mapping.job_retry'), ('module.runners.module.job_retry[0].aws_lambda_permission.job_retry', 'module.runner_configs.module.orchestration_webhook[0].module.job_retry[0].aws_lambda_permission.job_retry'), ('module.runners.module.job_retry[0].aws_iam_role_policy.job_retry', 'module.runner_configs.module.orchestration_webhook[0].module.job_retry[0].aws_iam_role_policy.job_retry'), - ('module.runners.aws_lambda_function.ssm_housekeeper', 'module.runner_configs.module.ssm_housekeeper.aws_lambda_function.ssm_housekeeper'), - ('module.runners.aws_cloudwatch_log_group.ssm_housekeeper', 'module.runner_configs.module.ssm_housekeeper.aws_cloudwatch_log_group.ssm_housekeeper'), - ('module.runners.aws_cloudwatch_event_rule.ssm_housekeeper', 'module.runner_configs.module.ssm_housekeeper.aws_cloudwatch_event_rule.ssm_housekeeper'), - ('module.runners.aws_cloudwatch_event_target.ssm_housekeeper', 'module.runner_configs.module.ssm_housekeeper.aws_cloudwatch_event_target.ssm_housekeeper'), - ('module.runners.aws_lambda_permission.ssm_housekeeper', 'module.runner_configs.module.ssm_housekeeper.aws_lambda_permission.ssm_housekeeper'), - ('module.runners.aws_iam_role.ssm_housekeeper', 'module.runner_configs.module.ssm_housekeeper.aws_iam_role.ssm_housekeeper'), - ('module.runners.aws_iam_role_policy.ssm_housekeeper', 'module.runner_configs.module.ssm_housekeeper.aws_iam_role_policy.ssm_housekeeper'), - ('module.runners.aws_iam_role_policy.ssm_housekeeper_logging', 'module.runner_configs.module.ssm_housekeeper.aws_iam_role_policy.ssm_housekeeper_logging'), - ('module.runners.aws_iam_role_policy_attachment.ssm_housekeeper_vpc_execution_role', 'module.runner_configs.module.ssm_housekeeper.aws_iam_role_policy_attachment.ssm_housekeeper_vpc_execution_role'), - ('module.runners.aws_iam_role_policy.ssm_housekeeper_xray', 'module.runner_configs.module.ssm_housekeeper.aws_iam_role_policy.ssm_housekeeper_xray'), + ('module.runners.aws_lambda_function.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper[0].aws_lambda_function.ssm_housekeeper'), + ('module.runners.aws_cloudwatch_log_group.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper[0].aws_cloudwatch_log_group.ssm_housekeeper'), + ('module.runners.aws_cloudwatch_event_rule.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper[0].aws_cloudwatch_event_rule.ssm_housekeeper'), + ('module.runners.aws_cloudwatch_event_target.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper[0].aws_cloudwatch_event_target.ssm_housekeeper'), + ('module.runners.aws_lambda_permission.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper[0].aws_lambda_permission.ssm_housekeeper'), + ('module.runners.aws_iam_role.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper[0].aws_iam_role.ssm_housekeeper'), + ('module.runners.aws_iam_role_policy.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper[0].aws_iam_role_policy.ssm_housekeeper'), + ('module.runners.aws_iam_role_policy.ssm_housekeeper_logging', 'module.runner_configs.module.runner_config_housekeeper[0].aws_iam_role_policy.ssm_housekeeper_logging'), + ('module.runners.aws_iam_role_policy_attachment.ssm_housekeeper_vpc_execution_role', 'module.runner_configs.module.runner_config_housekeeper[0].aws_iam_role_policy_attachment.ssm_housekeeper_vpc_execution_role'), + ('module.runners.aws_iam_role_policy.ssm_housekeeper_xray', 'module.runner_configs.module.runner_config_housekeeper[0].aws_iam_role_policy.ssm_housekeeper_xray'), ) # These resources are outside the dynamic runner-key modules and therefore From 9920e145137bd8a350cc9117d1a07b581e819eba Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Thu, 24 Sep 2026 13:55:33 +0200 Subject: [PATCH 09/44] fix: add back kms key --- modules/runner-config/variables.tf | 20 ++++---------------- modules/webhook/direct/variables.tf | 4 +++- modules/webhook/direct/webhook.tf | 2 +- modules/webhook/eventbridge/dispatcher.tf | 2 +- modules/webhook/eventbridge/variables.tf | 4 +++- modules/webhook/eventbridge/webhook.tf | 2 +- modules/webhook/variables.tf | 1 + 7 files changed, 14 insertions(+), 21 deletions(-) diff --git a/modules/runner-config/variables.tf b/modules/runner-config/variables.tf index 36a9fac8ee..f0ce9bed0c 100644 --- a/modules/runner-config/variables.tf +++ b/modules/runner-config/variables.tf @@ -202,31 +202,19 @@ variable "storage_provider" { scale_up = optional(object({ environment_variables = map(string) iam_policy_json = optional(string, null) - }), { - environment_variables = {} - iam_policy_json = null - }) + })) scale_down = optional(object({ environment_variables = map(string) iam_policy_json = optional(string, null) - }), { - environment_variables = {} - iam_policy_json = null - }) + })) pool = optional(object({ environment_variables = map(string) iam_policy_json = optional(string, null) - }), { - environment_variables = {} - iam_policy_json = null - }) + })) job_retry = optional(object({ environment_variables = map(string) iam_policy_json = optional(string, null) - }), { - environment_variables = {} - iam_policy_json = null - }) + })) }) } diff --git a/modules/webhook/direct/variables.tf b/modules/webhook/direct/variables.tf index fd15dae9ce..9dc6f2331e 100644 --- a/modules/webhook/direct/variables.tf +++ b/modules/webhook/direct/variables.tf @@ -30,7 +30,9 @@ variable "config" { queue_selection_strategy = optional(string, "first") storage_provider = optional(object({ aws = optional(object({ - ssm = optional(object({}), null) + ssm = optional(object({ + kms_key_id = optional(string, null) + }), null) }), {}) environment_variables = map(string) iam_policy_json = optional(string, null) diff --git a/modules/webhook/direct/webhook.tf b/modules/webhook/direct/webhook.tf index 7cadf8dede..63b04f8be6 100644 --- a/modules/webhook/direct/webhook.tf +++ b/modules/webhook/direct/webhook.tf @@ -131,7 +131,7 @@ resource "aws_iam_role_policy" "webhook_kms" { role = aws_iam_role.webhook_lambda.name policy = templatefile("${path.module}/../policies/lambda-kms.json", { - kms_key_arn = "arn:${var.config.aws_partition}:kms:::CMK_NOT_IN_USE" + kms_key_arn = var.config.storage_provider.aws.ssm.kms_key_id != null ? var.config.storage_provider.aws.ssm.kms_key_id : "arn:${var.config.aws_partition}:kms:::CMK_NOT_IN_USE" }) } diff --git a/modules/webhook/eventbridge/dispatcher.tf b/modules/webhook/eventbridge/dispatcher.tf index 8cca84e900..607ad85b53 100644 --- a/modules/webhook/eventbridge/dispatcher.tf +++ b/modules/webhook/eventbridge/dispatcher.tf @@ -129,7 +129,7 @@ resource "aws_iam_role_policy" "dispatcher_kms" { role = aws_iam_role.dispatcher_lambda.name policy = templatefile("${path.module}/../policies/lambda-kms.json", { - kms_key_arn = "arn:${var.config.aws_partition}:kms:::CMK_NOT_IN_USE" + kms_key_arn = var.config.storage_provider.aws.ssm.kms_key_id != null ? var.config.storage_provider.aws.ssm.kms_key_id : "arn:${var.config.aws_partition}:kms:::CMK_NOT_IN_USE" }) } diff --git a/modules/webhook/eventbridge/variables.tf b/modules/webhook/eventbridge/variables.tf index f362e08b3a..ab1c227693 100644 --- a/modules/webhook/eventbridge/variables.tf +++ b/modules/webhook/eventbridge/variables.tf @@ -30,7 +30,9 @@ variable "config" { queue_selection_strategy = optional(string, "first") storage_provider = optional(object({ aws = optional(object({ - ssm = optional(object({}), null) + ssm = optional(object({ + kms_key_id = optional(string, null) + }), null) }), {}) webhook = object({ environment_variables = map(string) diff --git a/modules/webhook/eventbridge/webhook.tf b/modules/webhook/eventbridge/webhook.tf index b42e076907..60ef70c6f9 100644 --- a/modules/webhook/eventbridge/webhook.tf +++ b/modules/webhook/eventbridge/webhook.tf @@ -141,7 +141,7 @@ resource "aws_iam_role_policy" "webhook_kms" { role = aws_iam_role.webhook_lambda.name policy = templatefile("${path.module}/../policies/lambda-kms.json", { - kms_key_arn = "arn:${var.config.aws_partition}:kms:::CMK_NOT_IN_USE" + kms_key_arn = var.config.storage_provider.aws.ssm.kms_key_id != null ? var.config.storage_provider.aws.ssm.kms_key_id : "arn:${var.config.aws_partition}:kms:::CMK_NOT_IN_USE" }) } diff --git a/modules/webhook/variables.tf b/modules/webhook/variables.tf index 758c9c1070..164d25afae 100644 --- a/modules/webhook/variables.tf +++ b/modules/webhook/variables.tf @@ -246,6 +246,7 @@ variable "storage_provider" { root = string webhook = string }) + kms_key_id = optional(string, null) }), null) }) direct = object({ From e8ebc000999feb7089fb4c922a12222067a8452c Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Thu, 24 Sep 2026 14:08:38 +0200 Subject: [PATCH 10/44] refactor: cleanup control plane file --- modules/compute-providers/aws/ec2/ami.tf | 59 ++++++++++++ .../aws/ec2/control-plane.tf | 95 +------------------ 2 files changed, 61 insertions(+), 93 deletions(-) diff --git a/modules/compute-providers/aws/ec2/ami.tf b/modules/compute-providers/aws/ec2/ami.tf index e96364b859..b0a55a27cc 100644 --- a/modules/compute-providers/aws/ec2/ami.tf +++ b/modules/compute-providers/aws/ec2/ami.tf @@ -56,3 +56,62 @@ resource "aws_ssm_parameter" "runner_ami_id" { } ) } + +data "aws_iam_policy_document" "ami_id_ssm" { + + + dynamic "statement" { + for_each = local.ami_id_ssm_module_managed || local.ami_id_ssm_external ? [1] : [] + + content { + effect = "Allow" + 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" + 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" + actions = ["kms:CreateGrant"] + resources = [statement.value] + + condition { + test = "Bool" + variable = "aws:ViaAWSService" + values = ["true"] + } + } + } +} + +data "aws_iam_policy_document" "ami_id_ssm_parameter_read" { + count = local.ami_id_ssm_external ? 1 : 0 + + statement { + effect = "Allow" + 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 +} \ No newline at end of file diff --git a/modules/compute-providers/aws/ec2/control-plane.tf b/modules/compute-providers/aws/ec2/control-plane.tf index a910a8c840..82370bb98d 100644 --- a/modules/compute-providers/aws/ec2/control-plane.tf +++ b/modules/compute-providers/aws/ec2/control-plane.tf @@ -1,24 +1,5 @@ # EC2-specific IAM and environment fragments consumed by the common control # plane in runner-config. -data "aws_iam_policy_document" "ami_id_ssm_parameter_read" { - count = local.ami_id_ssm_external ? 1 : 0 - - statement { - effect = "Allow" - 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" "scale_up" { statement { effect = "Allow" @@ -62,42 +43,6 @@ data "aws_iam_policy_document" "scale_up" { actions = ["iam:PassRole"] resources = [var.runner.iam.role.arn] } - - dynamic "statement" { - for_each = local.ami_id_ssm_module_managed || local.ami_id_ssm_external ? [1] : [] - - content { - effect = "Allow" - 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" - 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" - actions = ["kms:CreateGrant"] - resources = [statement.value] - - condition { - test = "Bool" - variable = "aws:ViaAWSService" - values = ["true"] - } - } - } } data "aws_iam_policy_document" "scale_down" { @@ -150,42 +95,6 @@ data "aws_iam_policy_document" "pool" { actions = ["iam:PassRole"] resources = [var.runner.iam.role.arn] } - - dynamic "statement" { - for_each = local.ami_id_ssm_module_managed || local.ami_id_ssm_external ? [1] : [] - - content { - effect = "Allow" - actions = ["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" - 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" - actions = ["kms:CreateGrant"] - resources = [statement.value] - - condition { - test = "Bool" - variable = "aws:ViaAWSService" - values = ["true"] - } - } - } } data "aws_iam_policy_document" "service_linked_role" { @@ -217,8 +126,8 @@ locals { pool_environment_variables = local.scale_up_environment_variables - scale_up_iam_policy_json = data.aws_iam_policy_document.scale_up.json + scale_up_iam_policy_json = merge(data.aws_iam_policy_document.scale_up.json, data.aws_iam_policy_document.ami_id_ssm.json) scale_down_iam_policy_json = data.aws_iam_policy_document.scale_down.json - pool_iam_policy_json = data.aws_iam_policy_document.pool.json + pool_iam_policy_json = merge(data.aws_iam_policy_document.pool.json, data.aws_iam_policy_document.ami_id_ssm.json) service_linked_role_policy_json = var.config.create_service_linked_role_spot ? data.aws_iam_policy_document.service_linked_role[0].json : null } From e0034af8aec8d16c4eef11223d052b1d4e3f170f Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Thu, 24 Sep 2026 14:19:10 +0200 Subject: [PATCH 11/44] fix: fix tf validate --- .pre-commit-config.yaml | 16 ++++++++++++++++ modules/compute-providers/aws/ec2/ami.tf | 3 ++- .../compute-providers/aws/ec2/control-plane.tf | 8 ++++++-- modules/runner-config/orchestration-provider.tf | 4 ---- modules/runner-config/variables.tf | 16 ---------------- 5 files changed, 24 insertions(+), 23 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1502b6ab8e..7d44d22d60 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,6 +6,22 @@ repos: - id: terraform_tflint args: - --args=--config=__GIT_WORKING_DIR__/.tflint.hcl --var-file __GIT_WORKING_DIR__/.github/lint/tflint.tfvars + - id: terraform_validate + name: Terraform · Validate + always_run: true + args: + - --hook-config=--retry-once-with-cleanup=true + - --tf-init-args=-backend=false + - --tf-init-args=--upgrade=true + - --hook-config=--tf-path=terraform + - id: terraform_validate + name: Terraform · Validate + always_run: true + args: + - --hook-config=--retry-once-with-cleanup=true + - --tf-init-args=-backend=false + - --tf-init-args=--upgrade=true + - --hook-config=--tf-path=tofu - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: diff --git a/modules/compute-providers/aws/ec2/ami.tf b/modules/compute-providers/aws/ec2/ami.tf index b0a55a27cc..1a294a9430 100644 --- a/modules/compute-providers/aws/ec2/ami.tf +++ b/modules/compute-providers/aws/ec2/ami.tf @@ -64,7 +64,8 @@ data "aws_iam_policy_document" "ami_id_ssm" { for_each = local.ami_id_ssm_module_managed || local.ami_id_ssm_external ? [1] : [] content { - effect = "Allow" + effect = "Allow" + # TODO: Validate if "ssm:GetParameters" still needed 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] } diff --git a/modules/compute-providers/aws/ec2/control-plane.tf b/modules/compute-providers/aws/ec2/control-plane.tf index 82370bb98d..2e84a9fbc9 100644 --- a/modules/compute-providers/aws/ec2/control-plane.tf +++ b/modules/compute-providers/aws/ec2/control-plane.tf @@ -1,6 +1,8 @@ # EC2-specific IAM and environment fragments consumed by the common control # plane in runner-config. data "aws_iam_policy_document" "scale_up" { + source_policy_documents = [data.aws_iam_policy_document.ami_id_ssm.json] + statement { effect = "Allow" actions = [ @@ -78,6 +80,8 @@ data "aws_iam_policy_document" "scale_down" { } data "aws_iam_policy_document" "pool" { + source_policy_documents = [data.aws_iam_policy_document.ami_id_ssm.json] + statement { effect = "Allow" actions = [ @@ -126,8 +130,8 @@ locals { pool_environment_variables = local.scale_up_environment_variables - scale_up_iam_policy_json = merge(data.aws_iam_policy_document.scale_up.json, data.aws_iam_policy_document.ami_id_ssm.json) + scale_up_iam_policy_json = data.aws_iam_policy_document.scale_up.json scale_down_iam_policy_json = data.aws_iam_policy_document.scale_down.json - pool_iam_policy_json = merge(data.aws_iam_policy_document.pool.json, data.aws_iam_policy_document.ami_id_ssm.json) + pool_iam_policy_json = data.aws_iam_policy_document.pool.json service_linked_role_policy_json = var.config.create_service_linked_role_spot ? data.aws_iam_policy_document.service_linked_role[0].json : null } diff --git a/modules/runner-config/orchestration-provider.tf b/modules/runner-config/orchestration-provider.tf index bb1d96c0c2..eec98fb6b9 100644 --- a/modules/runner-config/orchestration-provider.tf +++ b/modules/runner-config/orchestration-provider.tf @@ -50,10 +50,6 @@ module "orchestration_webhook" { parameter_store_tags = local.parameter_store_tags } } - scale_up = var.storage_provider.scale_up - scale_down = var.storage_provider.scale_down - pool = var.storage_provider.pool - job_retry = var.storage_provider.job_retry } observability = var.observability diff --git a/modules/runner-config/variables.tf b/modules/runner-config/variables.tf index f0ce9bed0c..c11f52f546 100644 --- a/modules/runner-config/variables.tf +++ b/modules/runner-config/variables.tf @@ -199,22 +199,6 @@ variable "storage_provider" { }), {}) }), null) }) - scale_up = optional(object({ - environment_variables = map(string) - iam_policy_json = optional(string, null) - })) - scale_down = optional(object({ - environment_variables = map(string) - iam_policy_json = optional(string, null) - })) - pool = optional(object({ - environment_variables = map(string) - iam_policy_json = optional(string, null) - })) - job_retry = optional(object({ - environment_variables = map(string) - iam_policy_json = optional(string, null) - })) }) } From 118cfa0545e8eeaa96baf1589706696461d4a309 Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Thu, 24 Sep 2026 14:43:25 +0200 Subject: [PATCH 12/44] fix: fix tf validation issue --- .pre-commit-config.yaml | 9 --------- modules/multi-runner/config.experimental.effective.tf | 4 ++-- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7d44d22d60..b0f72307ef 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,16 +12,7 @@ repos: args: - --hook-config=--retry-once-with-cleanup=true - --tf-init-args=-backend=false - - --tf-init-args=--upgrade=true - --hook-config=--tf-path=terraform - - id: terraform_validate - name: Terraform · Validate - always_run: true - args: - - --hook-config=--retry-once-with-cleanup=true - - --tf-init-args=-backend=false - - --tf-init-args=--upgrade=true - - --hook-config=--tf-path=tofu - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: diff --git a/modules/multi-runner/config.experimental.effective.tf b/modules/multi-runner/config.experimental.effective.tf index bc2958db33..9fe33f28bb 100644 --- a/modules/multi-runner/config.experimental.effective.tf +++ b/modules/multi-runner/config.experimental.effective.tf @@ -39,8 +39,8 @@ locals { storage_provider = merge(v.storage_provider, { aws = merge(v.storage_provider.aws, { - ssm = merge(v.storage_provider.aws.ssm, { - kms_key_id = local.normalized_config.storage_provider.aws.ssm.kms_key_id + ssm = v.storage_provider.aws.ssm == null ? null : merge(v.storage_provider.aws.ssm, { + kms_key_id = try(local.normalized_config.storage_provider.aws.ssm.kms_key_id, null) }) }) }) From 4f71e3aa4daae45dad5361e51b3a0adeb52afa9f Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Thu, 24 Sep 2026 14:54:45 +0200 Subject: [PATCH 13/44] fix: remove change in webhook for now --- main.tf | 15 +-------------- modules/multi-runner/runners.experimental.tf | 10 +--------- modules/webhook/direct/variables.tf | 2 -- modules/webhook/direct/webhook.tf | 2 +- modules/webhook/eventbridge/dispatcher.tf | 2 +- modules/webhook/eventbridge/variables.tf | 8 -------- modules/webhook/eventbridge/webhook.tf | 2 +- modules/webhook/variables.tf | 16 +--------------- modules/webhook/webhook.tf | 20 +++++++++++++++----- 9 files changed, 21 insertions(+), 56 deletions(-) diff --git a/main.tf b/main.tf index 4c946d6cf7..8f2236f8d1 100644 --- a/main.tf +++ b/main.tf @@ -113,6 +113,7 @@ module "webhook" { storage_provider = { aws = { + kms_key_id = var.kms_key_arn ssm = { paths = { root = local.ssm_root_path @@ -120,20 +121,6 @@ module "webhook" { } } } - direct = { - environment_variables = {} - iam_policy_json = null - } - eventbridge = { - webhook = { - environment_variables = {} - iam_policy_json = null - } - dispatcher = { - environment_variables = {} - iam_policy_json = null - } - } } prefix = var.prefix tags = local.tags diff --git a/modules/multi-runner/runners.experimental.tf b/modules/multi-runner/runners.experimental.tf index c03ce40faf..c94afd2db6 100644 --- a/modules/multi-runner/runners.experimental.tf +++ b/modules/multi-runner/runners.experimental.tf @@ -38,15 +38,7 @@ module "runner_configs" { job_retry = each.value.orchestration_provider.webhook.job_retry } } - storage_provider = merge(each.value.storage_provider, { - aws = { - ssm = each.value.storage_provider.aws.ssm - } - scale_up = local.storage_provider_capabilities.entries[each.key].scale_up - scale_down = local.storage_provider_capabilities.entries[each.key].scale_down - pool = local.storage_provider_capabilities.entries[each.key].pool - job_retry = local.storage_provider_capabilities.entries[each.key].job_retry - }) + storage_provider = each.value.storage_provider observability = each.value.observability compute_provider = each.value.compute_provider } diff --git a/modules/webhook/direct/variables.tf b/modules/webhook/direct/variables.tf index 9dc6f2331e..0c0c99c317 100644 --- a/modules/webhook/direct/variables.tf +++ b/modules/webhook/direct/variables.tf @@ -34,8 +34,6 @@ variable "config" { kms_key_id = optional(string, null) }), null) }), {}) - environment_variables = map(string) - iam_policy_json = optional(string, null) })) log_level = optional(string, "info") lambda_runtime = optional(string, "nodejs24.x") diff --git a/modules/webhook/direct/webhook.tf b/modules/webhook/direct/webhook.tf index 63b04f8be6..dc75636367 100644 --- a/modules/webhook/direct/webhook.tf +++ b/modules/webhook/direct/webhook.tf @@ -32,7 +32,7 @@ resource "aws_lambda_function" "webhook" { PARAMETER_RUNNER_MATCHER_CONFIG_PATH = var.config.storage_provider.aws.ssm != null ? join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.name]) : null PARAMETER_RUNNER_MATCHER_VERSION = var.config.storage_provider.aws.ssm != null ? join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.version]) : null # enforce cold start after Changes in SSM parameter } : k => v if v != null - }, var.config.storage_provider.environment_variables) + }) } dynamic "vpc_config" { diff --git a/modules/webhook/eventbridge/dispatcher.tf b/modules/webhook/eventbridge/dispatcher.tf index 607ad85b53..f8059e0a5e 100644 --- a/modules/webhook/eventbridge/dispatcher.tf +++ b/modules/webhook/eventbridge/dispatcher.tf @@ -54,7 +54,7 @@ resource "aws_lambda_function" "dispatcher" { REPOSITORY_ALLOW_LIST = jsonencode(var.config.repository_white_list) QUEUE_SELECTION_STRATEGY = var.config.queue_selection_strategy } : k => v if v != null - }, var.config.storage_provider.dispatcher.environment_variables) + }) } dynamic "vpc_config" { diff --git a/modules/webhook/eventbridge/variables.tf b/modules/webhook/eventbridge/variables.tf index ab1c227693..71ce3e47ef 100644 --- a/modules/webhook/eventbridge/variables.tf +++ b/modules/webhook/eventbridge/variables.tf @@ -34,14 +34,6 @@ variable "config" { kms_key_id = optional(string, null) }), null) }), {}) - webhook = object({ - environment_variables = map(string) - iam_policy_json = optional(string, null) - }) - dispatcher = object({ - environment_variables = map(string) - iam_policy_json = optional(string, null) - }) })) log_level = optional(string, "info") lambda_runtime = optional(string, "nodejs24.x") diff --git a/modules/webhook/eventbridge/webhook.tf b/modules/webhook/eventbridge/webhook.tf index 60ef70c6f9..4936a56e57 100644 --- a/modules/webhook/eventbridge/webhook.tf +++ b/modules/webhook/eventbridge/webhook.tf @@ -38,7 +38,7 @@ resource "aws_lambda_function" "webhook" { PARAMETER_GITHUB_APP_WEBHOOK_SECRET = var.config.storage_provider.aws.ssm != null ? var.config.github_app_parameters.webhook_secret.name : null PARAMETER_RUNNER_MATCHER_CONFIG_PATH = var.config.storage_provider.aws.ssm != null ? join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.name]) : null } : k => v if v != null - }, var.config.storage_provider.webhook.environment_variables) + }) } dynamic "vpc_config" { diff --git a/modules/webhook/variables.tf b/modules/webhook/variables.tf index 164d25afae..7cdfc73ed6 100644 --- a/modules/webhook/variables.tf +++ b/modules/webhook/variables.tf @@ -238,7 +238,7 @@ variable "matcher_config_parameter_store_tier" { } variable "storage_provider" { - description = "Resolved storage-provider marker and provider-owned webhook capabilities." + description = "Resolved AWS storage-provider marker used by webhook resources." type = object({ aws = object({ ssm = optional(object({ @@ -249,20 +249,6 @@ variable "storage_provider" { kms_key_id = optional(string, null) }), null) }) - direct = object({ - environment_variables = map(string) - iam_policy_json = optional(string, null) - }) - eventbridge = object({ - webhook = object({ - environment_variables = map(string) - iam_policy_json = optional(string, null) - }) - dispatcher = object({ - environment_variables = map(string) - iam_policy_json = optional(string, null) - }) - }) }) nullable = false } diff --git a/modules/webhook/webhook.tf b/modules/webhook/webhook.tf index 6a06065a10..43af75a7fc 100644 --- a/modules/webhook/webhook.tf +++ b/modules/webhook/webhook.tf @@ -76,9 +76,11 @@ module "direct" { lambda_apigateway_access_log_settings = var.webhook_lambda_apigateway_access_log_settings, repository_white_list = var.repository_white_list, queue_selection_strategy = var.queue_selection_strategy, - storage_provider = merge(var.storage_provider.direct, { - aws = var.storage_provider.aws - }) + storage_provider = { + aws = var.storage_provider.aws + environment_variables = {} + iam_policy_json = null + } log_level = var.log_level, lambda_runtime = var.lambda_runtime, aws_partition = var.aws_partition, @@ -121,9 +123,17 @@ module "eventbridge" { lambda_apigateway_access_log_settings = var.webhook_lambda_apigateway_access_log_settings, repository_white_list = var.repository_white_list, queue_selection_strategy = var.queue_selection_strategy, - storage_provider = merge(var.storage_provider.eventbridge, { + storage_provider = { aws = var.storage_provider.aws - }) + webhook = { + environment_variables = {} + iam_policy_json = null + } + dispatcher = { + environment_variables = {} + iam_policy_json = null + } + } log_level = var.log_level, lambda_runtime = var.lambda_runtime, aws_partition = var.aws_partition, From a4c0f5f89ece7fe6cddc9614b886cce4834f89d6 Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Thu, 24 Sep 2026 15:05:53 +0200 Subject: [PATCH 14/44] fix: remove changes --- modules/multi-runner/storage-provider.tf | 43 ---------------- modules/multi-runner/webhook.tf | 17 ++----- .../webhook/job-retry.tf | 9 +--- .../orchestration-providers/webhook/pool.tf | 13 ++--- .../webhook/scale-runners.tf | 8 +-- .../scale-runners/scale-down-iam-policies.tf | 2 - .../webhook/scale-runners/scale-down.tf | 1 - .../scale-runners/scale-up-iam-policies.tf | 2 - .../webhook/scale-runners/scale-up.tf | 1 - .../webhook/scale-runners/variables.tf | 14 ------ .../webhook/variables.tf | 20 ++------ modules/webhook/webhook.tf | 50 +++++++------------ 12 files changed, 31 insertions(+), 149 deletions(-) delete mode 100644 modules/multi-runner/storage-provider.tf diff --git a/modules/multi-runner/storage-provider.tf b/modules/multi-runner/storage-provider.tf deleted file mode 100644 index a4e5a54236..0000000000 --- a/modules/multi-runner/storage-provider.tf +++ /dev/null @@ -1,43 +0,0 @@ -locals { - # Storage-provider capabilities are empty for SSM. The capability boundary - # remains here so a future provider can add environment variables and IAM - # policy fragments without changing the runner wiring. - storage_provider_capabilities = { - webhook = { - direct = { - environment_variables = tomap({}) - iam_policy_json = null - } - eventbridge = { - webhook = { - environment_variables = tomap({}) - iam_policy_json = null - } - dispatcher = { - environment_variables = tomap({}) - iam_policy_json = null - } - } - } - entries = { - for entry_id in keys(local.effective_config.multi_runner_config) : entry_id => { - scale_up = { - environment_variables = tomap({}) - iam_policy_json = null - } - scale_down = { - environment_variables = tomap({}) - iam_policy_json = null - } - pool = { - environment_variables = tomap({}) - iam_policy_json = null - } - job_retry = { - environment_variables = tomap({}) - iam_policy_json = null - } - } - } - } -} diff --git a/modules/multi-runner/webhook.tf b/modules/multi-runner/webhook.tf index e64ebf8913..225038f11d 100644 --- a/modules/multi-runner/webhook.tf +++ b/modules/multi-runner/webhook.tf @@ -23,19 +23,10 @@ locals { } module "webhook" { - source = "../webhook" - prefix = var.prefix - tags = local.tags - storage_provider = merge(local.storage_provider_capabilities.webhook, { - aws = { - ssm = { - paths = { - root = local.ssm_root_path - webhook = local.effective_config.storage_provider.aws.ssm.paths.webhook - } - } - } - }) + source = "../webhook" + prefix = var.prefix + tags = local.tags + storage_provider = local.effective_config.storage_provider eventbridge = { enable = local.effective_config.orchestration_provider.webhook.eventbridge.enabled accept_events = local.effective_config.orchestration_provider.webhook.eventbridge.accept_events diff --git a/modules/orchestration-providers/webhook/job-retry.tf b/modules/orchestration-providers/webhook/job-retry.tf index a6365f2ea1..ec4cba0e08 100644 --- a/modules/orchestration-providers/webhook/job-retry.tf +++ b/modules/orchestration-providers/webhook/job-retry.tf @@ -43,12 +43,5 @@ module "job_retry" { } } - storage_provider = merge( - { - aws = { - ssm = local.resolved_config.storage_provider.aws.ssm - } - }, - local.resolved_config.storage_provider.job_retry, - ) + storage_provider = local.resolved_config.storage_provider } diff --git a/modules/orchestration-providers/webhook/pool.tf b/modules/orchestration-providers/webhook/pool.tf index 6fe8d93913..1da6676e62 100644 --- a/modules/orchestration-providers/webhook/pool.tf +++ b/modules/orchestration-providers/webhook/pool.tf @@ -48,16 +48,9 @@ module "pool" { log_group_tags = local.pool_log_tags } - aws_partition = var.aws_partition - tracing_config = local.resolved_config.observability.tracing - storage_provider = merge( - { - aws = { - ssm = local.resolved_config.storage_provider.aws.ssm - } - }, - local.resolved_config.storage_provider.pool, - ) + aws_partition = var.aws_partition + tracing_config = local.resolved_config.observability.tracing + storage_provider = local.resolved_config.storage_provider runner_provider = { type = var.runner_provider.type environment_variables = var.runner_provider.pool.environment_variables diff --git a/modules/orchestration-providers/webhook/scale-runners.tf b/modules/orchestration-providers/webhook/scale-runners.tf index 03cf36d528..004efd540f 100644 --- a/modules/orchestration-providers/webhook/scale-runners.tf +++ b/modules/orchestration-providers/webhook/scale-runners.tf @@ -53,13 +53,7 @@ module "scale_runners" { } } - storage_provider = { - aws = { - ssm = local.resolved_config.storage_provider.aws.ssm - } - scale_up = local.resolved_config.storage_provider.scale_up - scale_down = local.resolved_config.storage_provider.scale_down - } + storage_provider = local.resolved_config.storage_provider runner_provider = { type = var.runner_provider.type diff --git a/modules/orchestration-providers/webhook/scale-runners/scale-down-iam-policies.tf b/modules/orchestration-providers/webhook/scale-runners/scale-down-iam-policies.tf index 7e7d0c24cf..f0ae7454f9 100644 --- a/modules/orchestration-providers/webhook/scale-runners/scale-down-iam-policies.tf +++ b/modules/orchestration-providers/webhook/scale-runners/scale-down-iam-policies.tf @@ -29,8 +29,6 @@ data "aws_iam_policy_document" "scale_down_common" { data "aws_iam_policy_document" "scale_down" { source_policy_documents = compact([ data.aws_iam_policy_document.scale_down_common.json, - var.runner_provider.scale_down.iam_policy_json, - var.storage_provider.scale_down.iam_policy_json, ]) } diff --git a/modules/orchestration-providers/webhook/scale-runners/scale-down.tf b/modules/orchestration-providers/webhook/scale-runners/scale-down.tf index 660d10c248..520db5ca73 100644 --- a/modules/orchestration-providers/webhook/scale-runners/scale-down.tf +++ b/modules/orchestration-providers/webhook/scale-runners/scale-down.tf @@ -47,7 +47,6 @@ resource "aws_lambda_function" "scale_down" { var.runner_provider.scale_down.environment_variables, local.scale_down_common_environment_variables, local.scale_down_ssm_environment_variables, - var.storage_provider.scale_down.environment_variables, ) } diff --git a/modules/orchestration-providers/webhook/scale-runners/scale-up-iam-policies.tf b/modules/orchestration-providers/webhook/scale-runners/scale-up-iam-policies.tf index e636a8c671..5d012886e6 100644 --- a/modules/orchestration-providers/webhook/scale-runners/scale-up-iam-policies.tf +++ b/modules/orchestration-providers/webhook/scale-runners/scale-up-iam-policies.tf @@ -73,8 +73,6 @@ data "aws_iam_policy_document" "scale_up_common" { data "aws_iam_policy_document" "scale_up" { source_policy_documents = compact([ data.aws_iam_policy_document.scale_up_common.json, - var.runner_provider.scale_up.iam_policy_json, - var.storage_provider.scale_up.iam_policy_json, ]) } diff --git a/modules/orchestration-providers/webhook/scale-runners/scale-up.tf b/modules/orchestration-providers/webhook/scale-runners/scale-up.tf index d298041606..b89830d320 100644 --- a/modules/orchestration-providers/webhook/scale-runners/scale-up.tf +++ b/modules/orchestration-providers/webhook/scale-runners/scale-up.tf @@ -57,7 +57,6 @@ resource "aws_lambda_function" "scale_up" { var.runner_provider.scale_up.environment_variables, local.scale_up_common_environment_variables, local.scale_up_ssm_environment_variables, - var.storage_provider.scale_up.environment_variables, ) } diff --git a/modules/orchestration-providers/webhook/scale-runners/variables.tf b/modules/orchestration-providers/webhook/scale-runners/variables.tf index 21cb20b7e2..d39b728189 100644 --- a/modules/orchestration-providers/webhook/scale-runners/variables.tf +++ b/modules/orchestration-providers/webhook/scale-runners/variables.tf @@ -244,20 +244,6 @@ variable "storage_provider" { kms_key_id = optional(string, null) }), null) }) - scale_up = optional(object({ - environment_variables = map(string) - iam_policy_json = optional(string, null) - }), { - environment_variables = {} - iam_policy_json = null - }) - scale_down = optional(object({ - environment_variables = map(string) - iam_policy_json = optional(string, null) - }), { - environment_variables = {} - iam_policy_json = null - }) }) nullable = false diff --git a/modules/orchestration-providers/webhook/variables.tf b/modules/orchestration-providers/webhook/variables.tf index fec47a37ca..4ffffc5a72 100644 --- a/modules/orchestration-providers/webhook/variables.tf +++ b/modules/orchestration-providers/webhook/variables.tf @@ -235,31 +235,19 @@ variable "storage_provider" { scale_up = optional(object({ environment_variables = map(string) iam_policy_json = optional(string, null) - }), { - environment_variables = {} - iam_policy_json = null - }) + })) scale_down = optional(object({ environment_variables = map(string) iam_policy_json = optional(string, null) - }), { - environment_variables = {} - iam_policy_json = null - }) + })) pool = optional(object({ environment_variables = map(string) iam_policy_json = optional(string, null) - }), { - environment_variables = {} - iam_policy_json = null - }) + })) job_retry = optional(object({ environment_variables = map(string) iam_policy_json = optional(string, null) - }), { - environment_variables = {} - iam_policy_json = null - }) + })) }) nullable = false } diff --git a/modules/webhook/webhook.tf b/modules/webhook/webhook.tf index 43af75a7fc..a155f3617b 100644 --- a/modules/webhook/webhook.tf +++ b/modules/webhook/webhook.tf @@ -76,19 +76,15 @@ module "direct" { lambda_apigateway_access_log_settings = var.webhook_lambda_apigateway_access_log_settings, repository_white_list = var.repository_white_list, queue_selection_strategy = var.queue_selection_strategy, - storage_provider = { - aws = var.storage_provider.aws - environment_variables = {} - iam_policy_json = null - } - log_level = var.log_level, - lambda_runtime = var.lambda_runtime, - aws_partition = var.aws_partition, - lambda_architecture = var.lambda_architecture, - github_app_parameters = var.github_app_parameters, - tracing_config = var.tracing_config, - lambda_tags = var.lambda_tags, - api_gw_source_arn = "${aws_apigatewayv2_api.webhook.execution_arn}/*/*/${local.webhook_endpoint}" + storage_provider = var.storage_provider + log_level = var.log_level, + lambda_runtime = var.lambda_runtime, + aws_partition = var.aws_partition, + lambda_architecture = var.lambda_architecture, + github_app_parameters = var.github_app_parameters, + tracing_config = var.tracing_config, + lambda_tags = var.lambda_tags, + api_gw_source_arn = "${aws_apigatewayv2_api.webhook.execution_arn}/*/*/${local.webhook_endpoint}" ssm_parameter_runner_matcher_config = [ for p in aws_ssm_parameter.runner_matcher_config : { name = p.name @@ -123,25 +119,15 @@ module "eventbridge" { lambda_apigateway_access_log_settings = var.webhook_lambda_apigateway_access_log_settings, repository_white_list = var.repository_white_list, queue_selection_strategy = var.queue_selection_strategy, - storage_provider = { - aws = var.storage_provider.aws - webhook = { - environment_variables = {} - iam_policy_json = null - } - dispatcher = { - environment_variables = {} - iam_policy_json = null - } - } - log_level = var.log_level, - lambda_runtime = var.lambda_runtime, - aws_partition = var.aws_partition, - lambda_architecture = var.lambda_architecture, - github_app_parameters = var.github_app_parameters, - tracing_config = var.tracing_config, - lambda_tags = var.lambda_tags, - api_gw_source_arn = "${aws_apigatewayv2_api.webhook.execution_arn}/*/*/${local.webhook_endpoint}" + storage_provider = var.storage_provider + log_level = var.log_level, + lambda_runtime = var.lambda_runtime, + aws_partition = var.aws_partition, + lambda_architecture = var.lambda_architecture, + github_app_parameters = var.github_app_parameters, + tracing_config = var.tracing_config, + lambda_tags = var.lambda_tags, + api_gw_source_arn = "${aws_apigatewayv2_api.webhook.execution_arn}/*/*/${local.webhook_endpoint}" ssm_parameter_runner_matcher_config = [ for p in aws_ssm_parameter.runner_matcher_config : { name = p.name From 96dcefbec1d658741d1a7b26083b6caf69b19471 Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Thu, 24 Sep 2026 15:23:40 +0200 Subject: [PATCH 15/44] fix: clean up config --- .../webhook/job-retry/iam-policies.tf | 1 - .../webhook/job-retry/job-retry.tf | 1 - .../webhook/job-retry/variables.tf | 2 -- .../orchestration-providers/webhook/pool/pool.tf | 2 -- .../webhook/pool/variables.tf | 2 -- .../orchestration-providers/webhook/variables.tf | 16 ---------------- 6 files changed, 24 deletions(-) diff --git a/modules/orchestration-providers/webhook/job-retry/iam-policies.tf b/modules/orchestration-providers/webhook/job-retry/iam-policies.tf index 6e27720961..d6973f9f5d 100644 --- a/modules/orchestration-providers/webhook/job-retry/iam-policies.tf +++ b/modules/orchestration-providers/webhook/job-retry/iam-policies.tf @@ -52,7 +52,6 @@ data "aws_iam_policy_document" "lambda_xray" { } data "aws_iam_policy_document" "job_retry" { - source_policy_documents = compact([var.storage_provider.iam_policy_json]) dynamic "statement" { for_each = var.storage_provider.aws.ssm != null ? [true] : [] diff --git a/modules/orchestration-providers/webhook/job-retry/job-retry.tf b/modules/orchestration-providers/webhook/job-retry/job-retry.tf index 88a6e68bfe..a03aa962b1 100644 --- a/modules/orchestration-providers/webhook/job-retry/job-retry.tf +++ b/modules/orchestration-providers/webhook/job-retry/job-retry.tf @@ -40,7 +40,6 @@ locals { var.config.lambda.environment_variables, local.job_retry_environment_variables, local.ssm_environment_variables, - var.storage_provider.environment_variables, ) } diff --git a/modules/orchestration-providers/webhook/job-retry/variables.tf b/modules/orchestration-providers/webhook/job-retry/variables.tf index 795e9183b6..a4dc71fec4 100644 --- a/modules/orchestration-providers/webhook/job-retry/variables.tf +++ b/modules/orchestration-providers/webhook/job-retry/variables.tf @@ -156,8 +156,6 @@ variable "storage_provider" { kms_key_id = optional(string, null) }), null) }) - environment_variables = optional(map(string), {}) - iam_policy_json = optional(string, null) }) nullable = false diff --git a/modules/orchestration-providers/webhook/pool/pool.tf b/modules/orchestration-providers/webhook/pool/pool.tf index 7ced65e194..4ebe585bd3 100644 --- a/modules/orchestration-providers/webhook/pool/pool.tf +++ b/modules/orchestration-providers/webhook/pool/pool.tf @@ -61,7 +61,6 @@ resource "aws_lambda_function" "pool" { var.runner_provider.environment_variables, local.common_environment_variables, local.ssm_environment_variables, - var.storage_provider.environment_variables, ) } @@ -107,7 +106,6 @@ data "aws_iam_policy_document" "pool" { source_policy_documents = compact([ data.aws_iam_policy_document.pool_common.json, var.runner_provider.iam_policy_json, - var.storage_provider.iam_policy_json, ]) } diff --git a/modules/orchestration-providers/webhook/pool/variables.tf b/modules/orchestration-providers/webhook/pool/variables.tf index af63d04e03..5a386ea637 100644 --- a/modules/orchestration-providers/webhook/pool/variables.tf +++ b/modules/orchestration-providers/webhook/pool/variables.tf @@ -144,8 +144,6 @@ variable "storage_provider" { parameter_store_tags = string }), null) }) - environment_variables = optional(map(string), {}) - iam_policy_json = optional(string, null) }) nullable = false diff --git a/modules/orchestration-providers/webhook/variables.tf b/modules/orchestration-providers/webhook/variables.tf index 4ffffc5a72..9dc4211af6 100644 --- a/modules/orchestration-providers/webhook/variables.tf +++ b/modules/orchestration-providers/webhook/variables.tf @@ -232,22 +232,6 @@ variable "storage_provider" { parameter_store_tags = string }), null) }) - scale_up = optional(object({ - environment_variables = map(string) - iam_policy_json = optional(string, null) - })) - scale_down = optional(object({ - environment_variables = map(string) - iam_policy_json = optional(string, null) - })) - pool = optional(object({ - environment_variables = map(string) - iam_policy_json = optional(string, null) - })) - job_retry = optional(object({ - environment_variables = map(string) - iam_policy_json = optional(string, null) - })) }) nullable = false } From 17b2b32a3be403e281f5d95da096573b4f9cd126 Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Thu, 24 Sep 2026 15:47:34 +0200 Subject: [PATCH 16/44] refactor: cleanup webhook --- .../direct/storage-provider.aws.ssm.tf | 37 ++++++++++++ modules/webhook/direct/variables.tf | 8 +-- modules/webhook/direct/webhook.tf | 38 ++---------- modules/webhook/eventbridge/dispatcher.tf | 29 --------- .../eventbridge/storage-provider.aws.ssm.tf | 60 +++++++++++++++++++ modules/webhook/eventbridge/variables.tf | 8 +-- modules/webhook/eventbridge/webhook.tf | 35 ++--------- modules/webhook/variables.tf | 4 +- 8 files changed, 116 insertions(+), 103 deletions(-) create mode 100644 modules/webhook/direct/storage-provider.aws.ssm.tf create mode 100644 modules/webhook/eventbridge/storage-provider.aws.ssm.tf diff --git a/modules/webhook/direct/storage-provider.aws.ssm.tf b/modules/webhook/direct/storage-provider.aws.ssm.tf new file mode 100644 index 0000000000..7e9d489404 --- /dev/null +++ b/modules/webhook/direct/storage-provider.aws.ssm.tf @@ -0,0 +1,37 @@ +resource "aws_iam_role_policy" "webhook_kms" { + count = var.config.storage_provider.aws.ssm != null ? 1 : 0 + + name = "kms-policy" + role = aws_iam_role.webhook_lambda.name + + policy = templatefile("${path.module}/../policies/lambda-kms.json", { + kms_key_arn = var.config.storage_provider.aws.ssm.kms_key_id != null ? var.config.storage_provider.aws.ssm.kms_key_id : "arn:${var.config.aws_partition}:kms:::CMK_NOT_IN_USE" + }) +} + +resource "aws_iam_role_policy" "webhook_ssm" { + name = "publish-ssm-policy" + role = aws_iam_role.webhook_lambda.name + + policy = var.config.storage_provider.aws.ssm != null ? templatefile("${path.module}/../policies/lambda-ssm.json", { + resource_arns = jsonencode( + concat( + [var.config.github_app_parameters.webhook_secret.arn], + [for p in var.config.ssm_parameter_runner_matcher_config : p.arn] + ) + ) + }) : var.config.storage_provider.iam_policy_json +} + +moved { + from = aws_iam_role_policy.webhook_kms + to = aws_iam_role_policy.webhook_kms[0] +} + +locals { + ssm_environment_variables = var.config.storage_provider.aws.ssm != null ? { + PARAMETER_GITHUB_APP_WEBHOOK_SECRET = var.config.github_app_parameters.webhook_secret.name + PARAMETER_RUNNER_MATCHER_CONFIG_PATH = join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.name]) + PARAMETER_RUNNER_MATCHER_VERSION = join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.version]) + } : {} +} \ No newline at end of file diff --git a/modules/webhook/direct/variables.tf b/modules/webhook/direct/variables.tf index 0c0c99c317..e8d32dd6e9 100644 --- a/modules/webhook/direct/variables.tf +++ b/modules/webhook/direct/variables.tf @@ -28,13 +28,13 @@ variable "config" { }), null) repository_white_list = optional(list(string), []) queue_selection_strategy = optional(string, "first") - storage_provider = optional(object({ - aws = optional(object({ + storage_provider = object({ + aws = object({ ssm = optional(object({ kms_key_id = optional(string, null) }), null) - }), {}) - })) + }) + }) log_level = optional(string, "info") lambda_runtime = optional(string, "nodejs24.x") aws_partition = optional(string, "aws") diff --git a/modules/webhook/direct/webhook.tf b/modules/webhook/direct/webhook.tf index dc75636367..e0583bbb4e 100644 --- a/modules/webhook/direct/webhook.tf +++ b/modules/webhook/direct/webhook.tf @@ -26,13 +26,13 @@ resource "aws_lambda_function" "webhook" { POWERTOOLS_TRACE_ENABLED = var.config.tracing_config.mode != null ? true : false POWERTOOLS_TRACER_CAPTURE_HTTPS_REQUESTS = var.config.tracing_config.capture_http_requests POWERTOOLS_TRACER_CAPTURE_ERROR = var.config.tracing_config.capture_error - PARAMETER_GITHUB_APP_WEBHOOK_SECRET = var.config.storage_provider.aws.ssm != null ? var.config.github_app_parameters.webhook_secret.name : null REPOSITORY_ALLOW_LIST = jsonencode(var.config.repository_white_list) QUEUE_SELECTION_STRATEGY = var.config.queue_selection_strategy - PARAMETER_RUNNER_MATCHER_CONFIG_PATH = var.config.storage_provider.aws.ssm != null ? join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.name]) : null - PARAMETER_RUNNER_MATCHER_VERSION = var.config.storage_provider.aws.ssm != null ? join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.version]) : null # enforce cold start after Changes in SSM parameter } : k => v if v != null - }) + }, + local.ssm_environment_variables + ) + } dynamic "vpc_config" { @@ -124,36 +124,6 @@ resource "aws_iam_role_policy" "webhook_sqs" { }) } -resource "aws_iam_role_policy" "webhook_kms" { - count = var.config.storage_provider.aws.ssm != null ? 1 : 0 - - name = "kms-policy" - role = aws_iam_role.webhook_lambda.name - - policy = templatefile("${path.module}/../policies/lambda-kms.json", { - kms_key_arn = var.config.storage_provider.aws.ssm.kms_key_id != null ? var.config.storage_provider.aws.ssm.kms_key_id : "arn:${var.config.aws_partition}:kms:::CMK_NOT_IN_USE" - }) -} - -resource "aws_iam_role_policy" "webhook_ssm" { - name = "publish-ssm-policy" - role = aws_iam_role.webhook_lambda.name - - policy = var.config.storage_provider.aws.ssm != null ? templatefile("${path.module}/../policies/lambda-ssm.json", { - resource_arns = jsonencode( - concat( - [var.config.github_app_parameters.webhook_secret.arn], - [for p in var.config.ssm_parameter_runner_matcher_config : p.arn] - ) - ) - }) : var.config.storage_provider.iam_policy_json -} - -moved { - from = aws_iam_role_policy.webhook_kms - to = aws_iam_role_policy.webhook_kms[0] -} - resource "aws_iam_role_policy" "xray" { count = var.config.tracing_config.mode != null ? 1 : 0 name = "xray-policy" diff --git a/modules/webhook/eventbridge/dispatcher.tf b/modules/webhook/eventbridge/dispatcher.tf index f8059e0a5e..6f2cf12cb5 100644 --- a/modules/webhook/eventbridge/dispatcher.tf +++ b/modules/webhook/eventbridge/dispatcher.tf @@ -122,35 +122,6 @@ resource "aws_iam_role_policy" "dispatcher_sqs" { }) } -resource "aws_iam_role_policy" "dispatcher_kms" { - count = var.config.storage_provider.aws.ssm != null ? 1 : 0 - - name = "kms-policy" - role = aws_iam_role.dispatcher_lambda.name - - policy = templatefile("${path.module}/../policies/lambda-kms.json", { - kms_key_arn = var.config.storage_provider.aws.ssm.kms_key_id != null ? var.config.storage_provider.aws.ssm.kms_key_id : "arn:${var.config.aws_partition}:kms:::CMK_NOT_IN_USE" - }) -} - -resource "aws_iam_role_policy" "dispatcher_ssm" { - name = "publish-ssm-policy" - role = aws_iam_role.dispatcher_lambda.name - - policy = var.config.storage_provider.aws.ssm != null ? templatefile("${path.module}/../policies/lambda-ssm.json", { - resource_arns = jsonencode( - concat( - [for p in var.config.ssm_parameter_runner_matcher_config : p.arn] - ) - ) - }) : var.config.storage_provider.dispatcher.iam_policy_json -} - -moved { - from = aws_iam_role_policy.dispatcher_kms - to = aws_iam_role_policy.dispatcher_kms[0] -} - resource "aws_iam_role_policy" "dispatcher_xray" { count = var.config.tracing_config.mode != null ? 1 : 0 name = "xray-policy" diff --git a/modules/webhook/eventbridge/storage-provider.aws.ssm.tf b/modules/webhook/eventbridge/storage-provider.aws.ssm.tf new file mode 100644 index 0000000000..4d39763ebc --- /dev/null +++ b/modules/webhook/eventbridge/storage-provider.aws.ssm.tf @@ -0,0 +1,60 @@ +resource "aws_iam_role_policy" "webhook_ssm" { + name = "publish-ssm-policy" + role = aws_iam_role.webhook_lambda.name + + policy = var.config.storage_provider.aws.ssm != null ? templatefile("${path.module}/../policies/lambda-ssm.json", { + resource_arns = jsonencode([var.config.github_app_parameters.webhook_secret.arn]) + }) : var.config.storage_provider.webhook.iam_policy_json +} + +resource "aws_iam_role_policy" "webhook_kms" { + count = var.config.storage_provider.aws.ssm != null ? 1 : 0 + + name = "kms-policy" + role = aws_iam_role.webhook_lambda.name + + policy = templatefile("${path.module}/../policies/lambda-kms.json", { + kms_key_arn = var.config.storage_provider.aws.ssm.kms_key_id != null ? var.config.storage_provider.aws.ssm.kms_key_id : "arn:${var.config.aws_partition}:kms:::CMK_NOT_IN_USE" + }) +} + +resource "aws_iam_role_policy" "dispatcher_kms" { + count = var.config.storage_provider.aws.ssm != null ? 1 : 0 + + name = "kms-policy" + role = aws_iam_role.dispatcher_lambda.name + + policy = templatefile("${path.module}/../policies/lambda-kms.json", { + kms_key_arn = var.config.storage_provider.aws.ssm.kms_key_id != null ? var.config.storage_provider.aws.ssm.kms_key_id : "arn:${var.config.aws_partition}:kms:::CMK_NOT_IN_USE" + }) +} + +resource "aws_iam_role_policy" "dispatcher_ssm" { + name = "publish-ssm-policy" + role = aws_iam_role.dispatcher_lambda.name + + policy = var.config.storage_provider.aws.ssm != null ? templatefile("${path.module}/../policies/lambda-ssm.json", { + resource_arns = jsonencode( + concat( + [for p in var.config.ssm_parameter_runner_matcher_config : p.arn] + ) + ) + }) : var.config.storage_provider.dispatcher.iam_policy_json +} + +moved { + from = aws_iam_role_policy.dispatcher_kms + to = aws_iam_role_policy.dispatcher_kms[0] +} + +moved { + from = aws_iam_role_policy.webhook_kms + to = aws_iam_role_policy.webhook_kms[0] +} + +locals { + ssm_environment_variables = var.config.storage_provider.aws.ssm != null ? { + PARAMETER_GITHUB_APP_WEBHOOK_SECRET = var.config.github_app_parameters.webhook_secret.name + PARAMETER_RUNNER_MATCHER_CONFIG_PATH = join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.name]) + } : {} +} diff --git a/modules/webhook/eventbridge/variables.tf b/modules/webhook/eventbridge/variables.tf index 71ce3e47ef..c134ab0bd1 100644 --- a/modules/webhook/eventbridge/variables.tf +++ b/modules/webhook/eventbridge/variables.tf @@ -28,13 +28,13 @@ variable "config" { }), null) repository_white_list = optional(list(string), []) queue_selection_strategy = optional(string, "first") - storage_provider = optional(object({ - aws = optional(object({ + storage_provider = object({ + aws = object({ ssm = optional(object({ kms_key_id = optional(string, null) }), null) - }), {}) - })) + }) + }) log_level = optional(string, "info") lambda_runtime = optional(string, "nodejs24.x") aws_partition = optional(string, "aws") diff --git a/modules/webhook/eventbridge/webhook.tf b/modules/webhook/eventbridge/webhook.tf index 4936a56e57..7edf3ee8de 100644 --- a/modules/webhook/eventbridge/webhook.tf +++ b/modules/webhook/eventbridge/webhook.tf @@ -33,12 +33,12 @@ resource "aws_lambda_function" "webhook" { POWERTOOLS_TRACER_CAPTURE_HTTPS_REQUESTS = var.config.tracing_config.capture_http_requests POWERTOOLS_TRACER_CAPTURE_ERROR = var.config.tracing_config.capture_error # Parameters required for lambda configuration - ACCEPT_EVENTS = jsonencode(var.config.accept_events) - EVENT_BUS_NAME = aws_cloudwatch_event_bus.main.name - PARAMETER_GITHUB_APP_WEBHOOK_SECRET = var.config.storage_provider.aws.ssm != null ? var.config.github_app_parameters.webhook_secret.name : null - PARAMETER_RUNNER_MATCHER_CONFIG_PATH = var.config.storage_provider.aws.ssm != null ? join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.name]) : null + ACCEPT_EVENTS = jsonencode(var.config.accept_events) + EVENT_BUS_NAME = aws_cloudwatch_event_bus.main.name } : k => v if v != null - }) + }, + local.ssm_environment_variables + ) } dynamic "vpc_config" { @@ -125,31 +125,6 @@ resource "aws_iam_role_policy" "webhook_eventbridge" { }) } -resource "aws_iam_role_policy" "webhook_ssm" { - name = "publish-ssm-policy" - role = aws_iam_role.webhook_lambda.name - - policy = var.config.storage_provider.aws.ssm != null ? templatefile("${path.module}/../policies/lambda-ssm.json", { - resource_arns = jsonencode([var.config.github_app_parameters.webhook_secret.arn]) - }) : var.config.storage_provider.webhook.iam_policy_json -} - -resource "aws_iam_role_policy" "webhook_kms" { - count = var.config.storage_provider.aws.ssm != null ? 1 : 0 - - name = "kms-policy" - role = aws_iam_role.webhook_lambda.name - - policy = templatefile("${path.module}/../policies/lambda-kms.json", { - kms_key_arn = var.config.storage_provider.aws.ssm.kms_key_id != null ? var.config.storage_provider.aws.ssm.kms_key_id : "arn:${var.config.aws_partition}:kms:::CMK_NOT_IN_USE" - }) -} - -moved { - from = aws_iam_role_policy.webhook_kms - to = aws_iam_role_policy.webhook_kms[0] -} - resource "aws_iam_role_policy" "xray" { count = var.config.tracing_config.mode != null ? 1 : 0 name = "xray-policy" diff --git a/modules/webhook/variables.tf b/modules/webhook/variables.tf index 7cdfc73ed6..5a39fcf515 100644 --- a/modules/webhook/variables.tf +++ b/modules/webhook/variables.tf @@ -238,15 +238,15 @@ variable "matcher_config_parameter_store_tier" { } variable "storage_provider" { - description = "Resolved AWS storage-provider marker used by webhook resources." + description = "Storage-provider configuration used by the webhook resources." type = object({ aws = object({ ssm = optional(object({ + kms_key_id = optional(string, null) paths = object({ root = string webhook = string }) - kms_key_id = optional(string, null) }), null) }) }) From 0bfa30c44e02ba77e8c5d7f7a9bbdf3654e3c44a Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Thu, 24 Sep 2026 16:05:21 +0200 Subject: [PATCH 17/44] refactor: split ssm into files --- .../webhook/job-retry/iam-policies.tf | 41 +------- .../webhook/job-retry/job-retry.tf | 6 -- .../job-retry/storage-provider.aws.ssm.tf | 51 ++++++++++ .../webhook/pool/iam-policies.tf | 54 +---------- .../webhook/pool/pool.tf | 12 ++- .../webhook/pool/storage-provider.aws.ssm.tf | 55 +++++++++++ .../scale-runners/scale-down-iam-policies.tf | 29 +----- .../webhook/scale-runners/scale-down.tf | 17 ++-- .../scale-runners/scale-up-iam-policies.tf | 45 +-------- .../webhook/scale-runners/scale-up.tf | 19 ++-- .../scale-runners/storage-provider.aws.ssm.tf | 93 +++++++++++++++++++ 11 files changed, 222 insertions(+), 200 deletions(-) create mode 100644 modules/orchestration-providers/webhook/job-retry/storage-provider.aws.ssm.tf create mode 100644 modules/orchestration-providers/webhook/pool/storage-provider.aws.ssm.tf create mode 100644 modules/orchestration-providers/webhook/scale-runners/storage-provider.aws.ssm.tf diff --git a/modules/orchestration-providers/webhook/job-retry/iam-policies.tf b/modules/orchestration-providers/webhook/job-retry/iam-policies.tf index d6973f9f5d..cb3de73557 100644 --- a/modules/orchestration-providers/webhook/job-retry/iam-policies.tf +++ b/modules/orchestration-providers/webhook/job-retry/iam-policies.tf @@ -53,19 +53,7 @@ data "aws_iam_policy_document" "lambda_xray" { data "aws_iam_policy_document" "job_retry" { - dynamic "statement" { - for_each = var.storage_provider.aws.ssm != null ? [true] : [] - content { - sid = "WebhookJobRetryReadGitHubAppParameters" - effect = "Allow" - actions = ["ssm:GetParameter", "ssm:GetParameters"] - resources = concat( - [var.config.github.app_parameters.id.arn, var.config.github.app_parameters.key_base64.arn], - var.config.github.app_parameters.additional_app_parameter_arns, - var.config.github.app_parameters.additional_apps_manifest != null ? [var.config.github.app_parameters.additional_apps_manifest.arn] : [], - ) - } - } + source_policy_documents = [data.aws_iam_policy_document.ssm_job_retry.json] statement { sid = "WebhookJobRetryConsumeRetryQueue" @@ -91,31 +79,4 @@ data "aws_iam_policy_document" "job_retry" { resources = [var.config.queue.build.arn] } - - dynamic "statement" { - for_each = var.storage_provider.aws.ssm != null && var.storage_provider.aws.ssm.kms_key_id != null ? [var.storage_provider.aws.ssm.kms_key_id] : [] - iterator = kms_key - - content { - sid = "WebhookJobRetryDecryptParameterStore" - effect = "Allow" - actions = ["kms:Decrypt"] - resources = [kms_key.value] - } - } - - dynamic "statement" { - for_each = var.config.queue.kms_key_id == null ? [] : [var.config.queue.kms_key_id] - iterator = kms_key - - content { - sid = "WebhookJobRetryEncryptBuildQueueMessage" - effect = "Allow" - actions = [ - "kms:Decrypt", - "kms:GenerateDataKey", - ] - resources = [kms_key.value] - } - } } diff --git a/modules/orchestration-providers/webhook/job-retry/job-retry.tf b/modules/orchestration-providers/webhook/job-retry/job-retry.tf index a03aa962b1..0ece785781 100644 --- a/modules/orchestration-providers/webhook/job-retry/job-retry.tf +++ b/modules/orchestration-providers/webhook/job-retry/job-retry.tf @@ -29,12 +29,6 @@ locals { RUNNER_NAME_PREFIX = var.config.runner.name_prefix } - ssm_environment_variables = var.storage_provider.aws.ssm != null ? { - PARAMETER_GITHUB_APP_ID_NAME = var.config.github.app_parameters.id.name - PARAMETER_GITHUB_APP_KEY_BASE64_NAME = var.config.github.app_parameters.key_base64.name - PARAMETER_GITHUB_APPS_MANIFEST_NAME = var.config.github.app_parameters.additional_apps_manifest != null ? var.config.github.app_parameters.additional_apps_manifest.name : "" - } : {} - environment_variables = merge( local.lambda_environment_variables, var.config.lambda.environment_variables, diff --git a/modules/orchestration-providers/webhook/job-retry/storage-provider.aws.ssm.tf b/modules/orchestration-providers/webhook/job-retry/storage-provider.aws.ssm.tf new file mode 100644 index 0000000000..fef13a1567 --- /dev/null +++ b/modules/orchestration-providers/webhook/job-retry/storage-provider.aws.ssm.tf @@ -0,0 +1,51 @@ +locals { + ssm_environment_variables = var.storage_provider.aws.ssm != null ? { + PARAMETER_GITHUB_APP_ID_NAME = var.config.github.app_parameters.id.name + PARAMETER_GITHUB_APP_KEY_BASE64_NAME = var.config.github.app_parameters.key_base64.name + PARAMETER_GITHUB_APPS_MANIFEST_NAME = var.config.github.app_parameters.additional_apps_manifest != null ? var.config.github.app_parameters.additional_apps_manifest.name : "" + } : {} +} + +data "aws_iam_policy_document" "ssm_job_retry" { + + dynamic "statement" { + for_each = var.storage_provider.aws.ssm != null ? [true] : [] + content { + sid = "WebhookJobRetryReadGitHubAppParameters" + effect = "Allow" + actions = ["ssm:GetParameter", "ssm:GetParameters"] + resources = concat( + [var.config.github.app_parameters.id.arn, var.config.github.app_parameters.key_base64.arn], + var.config.github.app_parameters.additional_app_parameter_arns, + var.config.github.app_parameters.additional_apps_manifest != null ? [var.config.github.app_parameters.additional_apps_manifest.arn] : [], + ) + } + } + + dynamic "statement" { + for_each = var.storage_provider.aws.ssm != null && var.storage_provider.aws.ssm.kms_key_id != null ? [var.storage_provider.aws.ssm.kms_key_id] : [] + iterator = kms_key + + content { + sid = "WebhookJobRetryDecryptParameterStore" + effect = "Allow" + actions = ["kms:Decrypt"] + resources = [kms_key.value] + } + } + + dynamic "statement" { + for_each = var.config.queue.kms_key_id == null ? [] : [var.config.queue.kms_key_id] + iterator = kms_key + + content { + sid = "WebhookJobRetryEncryptBuildQueueMessage" + effect = "Allow" + actions = [ + "kms:Decrypt", + "kms:GenerateDataKey", + ] + resources = [kms_key.value] + } + } +} diff --git a/modules/orchestration-providers/webhook/pool/iam-policies.tf b/modules/orchestration-providers/webhook/pool/iam-policies.tf index 3a3662af3e..c5f433f6eb 100644 --- a/modules/orchestration-providers/webhook/pool/iam-policies.tf +++ b/modules/orchestration-providers/webhook/pool/iam-policies.tf @@ -1,58 +1,6 @@ # IAM policies attached to the pool Lambda role. data "aws_iam_policy_document" "pool_common" { - dynamic "statement" { - for_each = var.storage_provider.aws.ssm != null ? [true] : [] - content { - sid = "WebhookPoolWriteRuntimeParameters" - effect = "Allow" - actions = ["ssm:AddTagsToResource", "ssm:PutParameter"] - resources = [ - var.storage_provider.aws.ssm.token_path_arn, - "${var.storage_provider.aws.ssm.token_path_arn}/*", - var.storage_provider.aws.ssm.config_path_arn, - "${var.storage_provider.aws.ssm.config_path_arn}/*", - ] - } - } - - dynamic "statement" { - for_each = var.storage_provider.aws.ssm != null ? [true] : [] - content { - sid = "WebhookPoolReadRunnerConfigParameters" - effect = "Allow" - actions = ["ssm:GetParameter", "ssm:GetParameters", "ssm:GetParametersByPath"] - resources = [ - var.storage_provider.aws.ssm.config_path_arn, - "${var.storage_provider.aws.ssm.config_path_arn}/*", - ] - } - } - - dynamic "statement" { - for_each = var.storage_provider.aws.ssm != null ? [true] : [] - content { - sid = "WebhookPoolReadGitHubAppParameters" - effect = "Allow" - actions = ["ssm:GetParameter", "ssm:GetParameters"] - resources = concat( - [var.config.github_app_parameters.id.arn, var.config.github_app_parameters.key_base64.arn], - var.config.github_app_parameters.additional_app_parameter_arns, - var.config.github_app_parameters.additional_apps_manifest != null ? [var.config.github_app_parameters.additional_apps_manifest.arn] : [], - ) - } - } - - dynamic "statement" { - for_each = var.storage_provider.aws.ssm != null && var.storage_provider.aws.ssm.kms_key_id != null ? [var.storage_provider.aws.ssm.kms_key_id] : [] - iterator = kms_key - - content { - sid = "WebhookPoolDecryptParameterStore" - effect = "Allow" - actions = ["kms:Decrypt"] - resources = [kms_key.value] - } - } + source_policy_documents = [data.aws_iam_policy_document.ssm_pool_common.json] } data "aws_iam_policy_document" "pool_logging" { diff --git a/modules/orchestration-providers/webhook/pool/pool.tf b/modules/orchestration-providers/webhook/pool/pool.tf index 4ebe585bd3..93796040a4 100644 --- a/modules/orchestration-providers/webhook/pool/pool.tf +++ b/modules/orchestration-providers/webhook/pool/pool.tf @@ -37,6 +37,12 @@ locals { SSM_CONFIG_PATH = var.storage_provider.aws.ssm.config_path SSM_PARAMETER_STORE_TAGS = var.storage_provider.aws.ssm.parameter_store_tags } : {} + + environment_variables = merge( + var.runner_provider.environment_variables, + local.common_environment_variables, + local.ssm_environment_variables, + ) } resource "aws_lambda_function" "pool" { @@ -57,11 +63,7 @@ resource "aws_lambda_function" "pool" { tags = merge(var.config.tags, var.config.lambda_tags) environment { - variables = merge( - var.runner_provider.environment_variables, - local.common_environment_variables, - local.ssm_environment_variables, - ) + variables = local.environment_variables } dynamic "vpc_config" { diff --git a/modules/orchestration-providers/webhook/pool/storage-provider.aws.ssm.tf b/modules/orchestration-providers/webhook/pool/storage-provider.aws.ssm.tf new file mode 100644 index 0000000000..b38a425c2e --- /dev/null +++ b/modules/orchestration-providers/webhook/pool/storage-provider.aws.ssm.tf @@ -0,0 +1,55 @@ +data "aws_iam_policy_document" "ssm_pool_common" { + dynamic "statement" { + for_each = var.storage_provider.aws.ssm != null ? [true] : [] + content { + sid = "WebhookPoolWriteRuntimeParameters" + effect = "Allow" + actions = ["ssm:AddTagsToResource", "ssm:PutParameter"] + resources = [ + var.storage_provider.aws.ssm.token_path_arn, + "${var.storage_provider.aws.ssm.token_path_arn}/*", + var.storage_provider.aws.ssm.config_path_arn, + "${var.storage_provider.aws.ssm.config_path_arn}/*", + ] + } + } + + dynamic "statement" { + for_each = var.storage_provider.aws.ssm != null ? [true] : [] + content { + sid = "WebhookPoolReadRunnerConfigParameters" + effect = "Allow" + actions = ["ssm:GetParameter", "ssm:GetParameters", "ssm:GetParametersByPath"] + resources = [ + var.storage_provider.aws.ssm.config_path_arn, + "${var.storage_provider.aws.ssm.config_path_arn}/*", + ] + } + } + + dynamic "statement" { + for_each = var.storage_provider.aws.ssm != null ? [true] : [] + content { + sid = "WebhookPoolReadGitHubAppParameters" + effect = "Allow" + actions = ["ssm:GetParameter", "ssm:GetParameters"] + resources = concat( + [var.config.github_app_parameters.id.arn, var.config.github_app_parameters.key_base64.arn], + var.config.github_app_parameters.additional_app_parameter_arns, + var.config.github_app_parameters.additional_apps_manifest != null ? [var.config.github_app_parameters.additional_apps_manifest.arn] : [], + ) + } + } + + dynamic "statement" { + for_each = var.storage_provider.aws.ssm != null && var.storage_provider.aws.ssm.kms_key_id != null ? [var.storage_provider.aws.ssm.kms_key_id] : [] + iterator = kms_key + + content { + sid = "WebhookPoolDecryptParameterStore" + effect = "Allow" + actions = ["kms:Decrypt"] + resources = [kms_key.value] + } + } +} \ No newline at end of file diff --git a/modules/orchestration-providers/webhook/scale-runners/scale-down-iam-policies.tf b/modules/orchestration-providers/webhook/scale-runners/scale-down-iam-policies.tf index f0ae7454f9..a2f52c86a6 100644 --- a/modules/orchestration-providers/webhook/scale-runners/scale-down-iam-policies.tf +++ b/modules/orchestration-providers/webhook/scale-runners/scale-down-iam-policies.tf @@ -1,34 +1,7 @@ -data "aws_iam_policy_document" "scale_down_common" { - dynamic "statement" { - for_each = var.storage_provider.aws.ssm != null ? [true] : [] - content { - sid = "WebhookScaleDownReadGitHubAppParameters" - effect = "Allow" - actions = ["ssm:GetParameter", "ssm:GetParameters"] - resources = concat( - [var.config.github.app_parameters.id.arn, var.config.github.app_parameters.key_base64.arn], - var.config.github.app_parameters.additional_app_parameter_arns, - var.config.github.app_parameters.additional_apps_manifest != null ? [var.config.github.app_parameters.additional_apps_manifest.arn] : [], - ) - } - } - - dynamic "statement" { - for_each = var.storage_provider.aws.ssm != null && var.storage_provider.aws.ssm.kms_key_id != null ? [var.storage_provider.aws.ssm.kms_key_id] : [] - iterator = kms_key - - content { - sid = "WebhookScaleDownDecryptParameterStore" - effect = "Allow" - actions = ["kms:Decrypt"] - resources = [kms_key.value] - } - } -} data "aws_iam_policy_document" "scale_down" { source_policy_documents = compact([ - data.aws_iam_policy_document.scale_down_common.json, + data.aws_iam_policy_document.ssm_scale_down_common.json, ]) } diff --git a/modules/orchestration-providers/webhook/scale-runners/scale-down.tf b/modules/orchestration-providers/webhook/scale-runners/scale-down.tf index 520db5ca73..7b8032f40f 100644 --- a/modules/orchestration-providers/webhook/scale-runners/scale-down.tf +++ b/modules/orchestration-providers/webhook/scale-runners/scale-down.tf @@ -19,12 +19,11 @@ locals { RUNNER_BOOT_TIME_IN_MINUTES = var.config.runner.boot_time_in_minutes } - scale_down_ssm_environment_variables = var.storage_provider.aws.ssm != null ? { - PARAMETER_GITHUB_APP_ID_NAME = var.config.github.app_parameters.id.name - PARAMETER_GITHUB_APP_KEY_BASE64_NAME = var.config.github.app_parameters.key_base64.name - PARAMETER_GITHUB_APPS_MANIFEST_NAME = var.config.github.app_parameters.additional_apps_manifest != null ? var.config.github.app_parameters.additional_apps_manifest.name : "" - SSM_TOKEN_PATH = var.storage_provider.aws.ssm.token_path - } : {} + scale_down_environment_variables = merge( + var.runner_provider.scale_down.environment_variables, + local.scale_down_common_environment_variables, + local.scale_down_ssm_environment_variables, + ) } resource "aws_lambda_function" "scale_down" { @@ -43,11 +42,7 @@ resource "aws_lambda_function" "scale_down" { architectures = [var.config.lambda.architecture] environment { - variables = merge( - var.runner_provider.scale_down.environment_variables, - local.scale_down_common_environment_variables, - local.scale_down_ssm_environment_variables, - ) + variables = local.scale_down_environment_variables } dynamic "vpc_config" { diff --git a/modules/orchestration-providers/webhook/scale-runners/scale-up-iam-policies.tf b/modules/orchestration-providers/webhook/scale-runners/scale-up-iam-policies.tf index 5d012886e6..4a23198707 100644 --- a/modules/orchestration-providers/webhook/scale-runners/scale-up-iam-policies.tf +++ b/modules/orchestration-providers/webhook/scale-runners/scale-up-iam-policies.tf @@ -1,38 +1,6 @@ data "aws_iam_policy_document" "scale_up_common" { - dynamic "statement" { - for_each = var.storage_provider.aws.ssm != null ? [true] : [] - content { - sid = "WebhookScaleUpWriteRuntimeParameters" - effect = "Allow" - actions = ["ssm:PutParameter", "ssm:AddTagsToResource"] - resources = [ - var.storage_provider.aws.ssm.token_path_arn, - "${var.storage_provider.aws.ssm.token_path_arn}/*", - var.storage_provider.aws.ssm.config_path_arn, - "${var.storage_provider.aws.ssm.config_path_arn}/*", - ] - } - } - - dynamic "statement" { - for_each = var.storage_provider.aws.ssm != null ? [true] : [] - content { - sid = "WebhookScaleUpReadGitHubAppAndRunnerConfigParameters" - effect = "Allow" - actions = ["ssm:GetParameter", "ssm:GetParameters"] - resources = concat( - [var.config.github.app_parameters.id.arn, var.config.github.app_parameters.key_base64.arn], - var.config.github.app_parameters.additional_app_parameter_arns, - var.config.github.app_parameters.additional_apps_manifest != null ? [var.config.github.app_parameters.additional_apps_manifest.arn] : [], - [ - var.storage_provider.aws.ssm.config_path_arn, - "${var.storage_provider.aws.ssm.config_path_arn}/*", - ], - ) - } - } - + source_policy_documents = [data.aws_iam_policy_document.ssm_scale_up_common.json] statement { sid = "WebhookScaleUpConsumeBuildQueue" @@ -45,17 +13,6 @@ data "aws_iam_policy_document" "scale_up_common" { resources = [var.config.queue.build.arn] } - dynamic "statement" { - for_each = var.storage_provider.aws.ssm != null && var.storage_provider.aws.ssm.kms_key_id != null ? [var.storage_provider.aws.ssm.kms_key_id] : [] - iterator = kms_key - - content { - sid = "WebhookScaleUpDecryptParameterStore" - effect = "Allow" - actions = ["kms:Decrypt"] - resources = [kms_key.value] - } - } dynamic "statement" { for_each = var.config.queue.kms_key_id == null ? [] : [var.config.queue.kms_key_id] diff --git a/modules/orchestration-providers/webhook/scale-runners/scale-up.tf b/modules/orchestration-providers/webhook/scale-runners/scale-up.tf index b89830d320..d9be5539c8 100644 --- a/modules/orchestration-providers/webhook/scale-runners/scale-up.tf +++ b/modules/orchestration-providers/webhook/scale-runners/scale-up.tf @@ -26,14 +26,11 @@ locals { JOB_RETRY_CONFIG = jsonencode(local.job_retry_config) } - scale_up_ssm_environment_variables = var.storage_provider.aws.ssm != null ? { - PARAMETER_GITHUB_APP_ID_NAME = var.config.github.app_parameters.id.name - PARAMETER_GITHUB_APP_KEY_BASE64_NAME = var.config.github.app_parameters.key_base64.name - PARAMETER_GITHUB_APPS_MANIFEST_NAME = var.config.github.app_parameters.additional_apps_manifest != null ? var.config.github.app_parameters.additional_apps_manifest.name : "" - SSM_TOKEN_PATH = var.storage_provider.aws.ssm.token_path - SSM_CONFIG_PATH = var.storage_provider.aws.ssm.config_path - SSM_PARAMETER_STORE_TAGS = var.storage_provider.aws.ssm.parameter_store_tags - } : {} + scale_up_environment_variables = merge( + var.runner_provider.scale_up.environment_variables, + local.scale_up_common_environment_variables, + local.scale_up_ssm_environment_variables, + ) } resource "aws_lambda_function" "scale_up" { @@ -53,11 +50,7 @@ resource "aws_lambda_function" "scale_up" { architectures = [var.config.lambda.architecture] environment { - variables = merge( - var.runner_provider.scale_up.environment_variables, - local.scale_up_common_environment_variables, - local.scale_up_ssm_environment_variables, - ) + variables = local.scale_up_environment_variables } dynamic "vpc_config" { diff --git a/modules/orchestration-providers/webhook/scale-runners/storage-provider.aws.ssm.tf b/modules/orchestration-providers/webhook/scale-runners/storage-provider.aws.ssm.tf new file mode 100644 index 0000000000..5114ad51b0 --- /dev/null +++ b/modules/orchestration-providers/webhook/scale-runners/storage-provider.aws.ssm.tf @@ -0,0 +1,93 @@ +data "aws_iam_policy_document" "ssm_scale_down_common" { + dynamic "statement" { + for_each = var.storage_provider.aws.ssm != null ? [true] : [] + content { + sid = "WebhookScaleDownReadGitHubAppParameters" + effect = "Allow" + actions = ["ssm:GetParameter", "ssm:GetParameters"] + resources = concat( + [var.config.github.app_parameters.id.arn, var.config.github.app_parameters.key_base64.arn], + var.config.github.app_parameters.additional_app_parameter_arns, + var.config.github.app_parameters.additional_apps_manifest != null ? [var.config.github.app_parameters.additional_apps_manifest.arn] : [], + ) + } + } + + dynamic "statement" { + for_each = var.storage_provider.aws.ssm != null && var.storage_provider.aws.ssm.kms_key_id != null ? [var.storage_provider.aws.ssm.kms_key_id] : [] + iterator = kms_key + + content { + sid = "WebhookScaleDownDecryptParameterStore" + effect = "Allow" + actions = ["kms:Decrypt"] + resources = [kms_key.value] + } + } +} + +data "aws_iam_policy_document" "ssm_scale_up_common" { + dynamic "statement" { + for_each = var.storage_provider.aws.ssm != null ? [true] : [] + content { + sid = "WebhookScaleUpWriteRuntimeParameters" + effect = "Allow" + actions = ["ssm:PutParameter", "ssm:AddTagsToResource"] + resources = [ + var.storage_provider.aws.ssm.token_path_arn, + "${var.storage_provider.aws.ssm.token_path_arn}/*", + var.storage_provider.aws.ssm.config_path_arn, + "${var.storage_provider.aws.ssm.config_path_arn}/*", + ] + } + } + + dynamic "statement" { + for_each = var.storage_provider.aws.ssm != null ? [true] : [] + content { + sid = "WebhookScaleUpReadGitHubAppAndRunnerConfigParameters" + effect = "Allow" + actions = ["ssm:GetParameter", "ssm:GetParameters"] + resources = concat( + [var.config.github.app_parameters.id.arn, var.config.github.app_parameters.key_base64.arn], + var.config.github.app_parameters.additional_app_parameter_arns, + var.config.github.app_parameters.additional_apps_manifest != null ? [var.config.github.app_parameters.additional_apps_manifest.arn] : [], + [ + var.storage_provider.aws.ssm.config_path_arn, + "${var.storage_provider.aws.ssm.config_path_arn}/*", + ], + ) + } + } + + dynamic "statement" { + for_each = var.storage_provider.aws.ssm != null && var.storage_provider.aws.ssm.kms_key_id != null ? [var.storage_provider.aws.ssm.kms_key_id] : [] + iterator = kms_key + + content { + sid = "WebhookScaleUpDecryptParameterStore" + effect = "Allow" + actions = ["kms:Decrypt"] + resources = [kms_key.value] + } + } +} + + +locals { + scale_up_ssm_environment_variables = var.storage_provider.aws.ssm != null ? { + PARAMETER_GITHUB_APP_ID_NAME = var.config.github.app_parameters.id.name + PARAMETER_GITHUB_APP_KEY_BASE64_NAME = var.config.github.app_parameters.key_base64.name + PARAMETER_GITHUB_APPS_MANIFEST_NAME = var.config.github.app_parameters.additional_apps_manifest != null ? var.config.github.app_parameters.additional_apps_manifest.name : "" + SSM_TOKEN_PATH = var.storage_provider.aws.ssm.token_path + SSM_CONFIG_PATH = var.storage_provider.aws.ssm.config_path + SSM_PARAMETER_STORE_TAGS = var.storage_provider.aws.ssm.parameter_store_tags + } : {} + + scale_down_ssm_environment_variables = var.storage_provider.aws.ssm != null ? { + PARAMETER_GITHUB_APP_ID_NAME = var.config.github.app_parameters.id.name + PARAMETER_GITHUB_APP_KEY_BASE64_NAME = var.config.github.app_parameters.key_base64.name + PARAMETER_GITHUB_APPS_MANIFEST_NAME = var.config.github.app_parameters.additional_apps_manifest != null ? var.config.github.app_parameters.additional_apps_manifest.name : "" + SSM_TOKEN_PATH = var.storage_provider.aws.ssm.token_path + } : {} +} \ No newline at end of file From bac5fe4eaf9d0791e0f74d7dc0defcf6ccd0a689 Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Thu, 24 Sep 2026 16:33:45 +0200 Subject: [PATCH 18/44] refactor: move ssm references to ssm file --- .../aws/ec2/storage-provider.aws.ssm.tf | 4 ++-- .../webhook/pool/pool.tf | 9 --------- .../webhook/pool/storage-provider.aws.ssm.tf | 11 ++++++++++ modules/runner-config/common-config.tf | 4 +--- .../runner-config-housekeeper/housekeeper.tf | 13 ++++++------ .../runner-config-housekeeper/iam-policies.tf | 15 -------------- .../storage-provider.aws.ssm.tf | 20 +++++++++++++++++++ .../runner-config/storage-provider.aws.ssm.tf | 4 ++++ modules/webhook/eventbridge/dispatcher.tf | 11 +++++----- .../eventbridge/storage-provider.aws.ssm.tf | 7 ++++++- modules/webhook/storage-provider.aws.ssm.tf | 9 +++++++++ modules/webhook/webhook.tf | 10 ---------- scripts/migrate_multi_runner_state.py | 20 +++++++++---------- 13 files changed, 74 insertions(+), 63 deletions(-) create mode 100644 modules/runner-config/runner-config-housekeeper/storage-provider.aws.ssm.tf create mode 100644 modules/webhook/storage-provider.aws.ssm.tf diff --git a/modules/compute-providers/aws/ec2/storage-provider.aws.ssm.tf b/modules/compute-providers/aws/ec2/storage-provider.aws.ssm.tf index ef9d51c58f..5a6945e1d7 100644 --- a/modules/compute-providers/aws/ec2/storage-provider.aws.ssm.tf +++ b/modules/compute-providers/aws/ec2/storage-provider.aws.ssm.tf @@ -68,9 +68,9 @@ locals { try(var.storage_provider.aws.ssm.parameters.tags, {}), ) - ssm_runner_tags = { + ssm_runner_tags = var.storage_provider.aws.ssm != null ? { "ghr:ssm_config_path" = local.ssm_config_path - } + } : {} } diff --git a/modules/orchestration-providers/webhook/pool/pool.tf b/modules/orchestration-providers/webhook/pool/pool.tf index 93796040a4..3a38648e27 100644 --- a/modules/orchestration-providers/webhook/pool/pool.tf +++ b/modules/orchestration-providers/webhook/pool/pool.tf @@ -29,15 +29,6 @@ locals { INCLUDE_BUSY_RUNNERS = var.config.include_busy_runners } - ssm_environment_variables = var.storage_provider.aws.ssm != null ? { - PARAMETER_GITHUB_APP_ID_NAME = var.config.github_app_parameters.id.name - PARAMETER_GITHUB_APP_KEY_BASE64_NAME = var.config.github_app_parameters.key_base64.name - PARAMETER_GITHUB_APPS_MANIFEST_NAME = var.config.github_app_parameters.additional_apps_manifest != null ? var.config.github_app_parameters.additional_apps_manifest.name : "" - SSM_TOKEN_PATH = var.storage_provider.aws.ssm.token_path - SSM_CONFIG_PATH = var.storage_provider.aws.ssm.config_path - SSM_PARAMETER_STORE_TAGS = var.storage_provider.aws.ssm.parameter_store_tags - } : {} - environment_variables = merge( var.runner_provider.environment_variables, local.common_environment_variables, diff --git a/modules/orchestration-providers/webhook/pool/storage-provider.aws.ssm.tf b/modules/orchestration-providers/webhook/pool/storage-provider.aws.ssm.tf index b38a425c2e..eafcb497ae 100644 --- a/modules/orchestration-providers/webhook/pool/storage-provider.aws.ssm.tf +++ b/modules/orchestration-providers/webhook/pool/storage-provider.aws.ssm.tf @@ -1,3 +1,14 @@ +locals { + ssm_environment_variables = var.storage_provider.aws.ssm != null ? { + PARAMETER_GITHUB_APP_ID_NAME = var.config.github_app_parameters.id.name + PARAMETER_GITHUB_APP_KEY_BASE64_NAME = var.config.github_app_parameters.key_base64.name + PARAMETER_GITHUB_APPS_MANIFEST_NAME = var.config.github_app_parameters.additional_apps_manifest != null ? var.config.github_app_parameters.additional_apps_manifest.name : "" + SSM_TOKEN_PATH = var.storage_provider.aws.ssm.token_path + SSM_CONFIG_PATH = var.storage_provider.aws.ssm.config_path + SSM_PARAMETER_STORE_TAGS = var.storage_provider.aws.ssm.parameter_store_tags + } : {} +} + data "aws_iam_policy_document" "ssm_pool_common" { dynamic "statement" { for_each = var.storage_provider.aws.ssm != null ? [true] : [] diff --git a/modules/runner-config/common-config.tf b/modules/runner-config/common-config.tf index 09a0db9a6c..e5d2a179cf 100644 --- a/modules/runner-config/common-config.tf +++ b/modules/runner-config/common-config.tf @@ -4,9 +4,7 @@ locals { { "Name" = format("%s-action-runner", var.prefix) }, - var.storage_provider.aws.ssm != null ? { - "ghr:ssm_config_path" = local.ssm_config_path - } : {}, + local.ssm_common_tags, var.tags, ) diff --git a/modules/runner-config/runner-config-housekeeper/housekeeper.tf b/modules/runner-config/runner-config-housekeeper/housekeeper.tf index 47062a4dee..9becdb2b3e 100644 --- a/modules/runner-config/runner-config-housekeeper/housekeeper.tf +++ b/modules/runner-config/runner-config-housekeeper/housekeeper.tf @@ -19,9 +19,11 @@ locals { POWERTOOLS_TRACER_CAPTURE_ERROR = var.config.observability.tracing.capture_error } - ssm_environment_variables = var.storage_provider.aws.ssm != null ? { - SSM_CLEANUP_CONFIG = jsonencode(local.cleanup_config) - } : {} + environment_variables = merge( + local.common_environment_variables, + local.ssm_environment_variables, + ) + } resource "aws_lambda_function" "housekeeper" { @@ -40,10 +42,7 @@ resource "aws_lambda_function" "housekeeper" { architectures = [var.config.lambda.architecture] environment { - variables = merge( - local.common_environment_variables, - local.ssm_environment_variables, - ) + variables = local.environment_variables } dynamic "vpc_config" { diff --git a/modules/runner-config/runner-config-housekeeper/iam-policies.tf b/modules/runner-config/runner-config-housekeeper/iam-policies.tf index cc43f1f5e7..fd889e0514 100644 --- a/modules/runner-config/runner-config-housekeeper/iam-policies.tf +++ b/modules/runner-config/runner-config-housekeeper/iam-policies.tf @@ -35,21 +35,6 @@ data "aws_iam_policy_document" "lambda_xray" { } } -data "aws_iam_policy_document" "housekeeper" { - dynamic "statement" { - for_each = var.storage_provider.aws.ssm != null ? [true] : [] - - content { - effect = "Allow" - actions = [ - "ssm:DeleteParameter", - "ssm:GetParametersByPath", - ] - resources = [var.storage_provider.aws.ssm.cleanup.parameter_path_arn] - } - } -} - data "aws_iam_policy_document" "housekeeper_logging" { statement { effect = "Allow" diff --git a/modules/runner-config/runner-config-housekeeper/storage-provider.aws.ssm.tf b/modules/runner-config/runner-config-housekeeper/storage-provider.aws.ssm.tf new file mode 100644 index 0000000000..bfda1393a2 --- /dev/null +++ b/modules/runner-config/runner-config-housekeeper/storage-provider.aws.ssm.tf @@ -0,0 +1,20 @@ +data "aws_iam_policy_document" "housekeeper" { + dynamic "statement" { + for_each = var.storage_provider.aws.ssm != null ? [true] : [] + + content { + effect = "Allow" + actions = [ + "ssm:DeleteParameter", + "ssm:GetParametersByPath", + ] + resources = [var.storage_provider.aws.ssm.cleanup.parameter_path_arn] + } + } +} + +locals { + ssm_environment_variables = var.storage_provider.aws.ssm != null ? { + SSM_CLEANUP_CONFIG = jsonencode(local.cleanup_config) + } : {} +} \ No newline at end of file diff --git a/modules/runner-config/storage-provider.aws.ssm.tf b/modules/runner-config/storage-provider.aws.ssm.tf index 39b593c697..bbf9cf66b0 100644 --- a/modules/runner-config/storage-provider.aws.ssm.tf +++ b/modules/runner-config/storage-provider.aws.ssm.tf @@ -34,6 +34,10 @@ locals { Value = value } ]) + + ssm_common_tags = var.storage_provider.aws.ssm != null ? { + "ghr:ssm_config_path" = local.ssm_config_path + } : {} } data "aws_caller_identity" "current" {} diff --git a/modules/webhook/eventbridge/dispatcher.tf b/modules/webhook/eventbridge/dispatcher.tf index 6f2cf12cb5..3ff3055553 100644 --- a/modules/webhook/eventbridge/dispatcher.tf +++ b/modules/webhook/eventbridge/dispatcher.tf @@ -48,13 +48,12 @@ resource "aws_lambda_function" "dispatcher" { POWERTOOLS_TRACE_ENABLED = var.config.tracing_config.mode != null ? true : false POWERTOOLS_TRACER_CAPTURE_HTTPS_REQUESTS = var.config.tracing_config.capture_http_requests POWERTOOLS_TRACER_CAPTURE_ERROR = var.config.tracing_config.capture_error - # Parameters required for lambda configuration - PARAMETER_RUNNER_MATCHER_CONFIG_PATH = var.config.storage_provider.aws.ssm != null ? join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.name]) : null - PARAMETER_RUNNER_MATCHER_VERSION = var.config.storage_provider.aws.ssm != null ? join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.version]) : null # enforce cold start after Changes in SSM parameter - REPOSITORY_ALLOW_LIST = jsonencode(var.config.repository_white_list) - QUEUE_SELECTION_STRATEGY = var.config.queue_selection_strategy + REPOSITORY_ALLOW_LIST = jsonencode(var.config.repository_white_list) + QUEUE_SELECTION_STRATEGY = var.config.queue_selection_strategy } : k => v if v != null - }) + }, + local.ssm_dispatcher_environment_variables + ) } dynamic "vpc_config" { diff --git a/modules/webhook/eventbridge/storage-provider.aws.ssm.tf b/modules/webhook/eventbridge/storage-provider.aws.ssm.tf index 4d39763ebc..acf7b24640 100644 --- a/modules/webhook/eventbridge/storage-provider.aws.ssm.tf +++ b/modules/webhook/eventbridge/storage-provider.aws.ssm.tf @@ -57,4 +57,9 @@ locals { PARAMETER_GITHUB_APP_WEBHOOK_SECRET = var.config.github_app_parameters.webhook_secret.name PARAMETER_RUNNER_MATCHER_CONFIG_PATH = join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.name]) } : {} -} + + ssm_dispatcher_environment_variables = var.config.storage_provider.aws.ssm != null ? { + PARAMETER_RUNNER_MATCHER_CONFIG_PATH = join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.name]) + PARAMETER_RUNNER_MATCHER_VERSION = join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.version]) + } : {} +} \ No newline at end of file diff --git a/modules/webhook/storage-provider.aws.ssm.tf b/modules/webhook/storage-provider.aws.ssm.tf new file mode 100644 index 0000000000..9f912164f6 --- /dev/null +++ b/modules/webhook/storage-provider.aws.ssm.tf @@ -0,0 +1,9 @@ +resource "aws_ssm_parameter" "runner_matcher_config" { + count = var.storage_provider.aws.ssm != null ? local.total_chunks : 0 + + name = "${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.webhook}/runner-matcher-config${local.total_chunks > 1 ? "-${count.index}" : ""}" + type = "String" + value = local.matcher_json_chunks[count.index] + tier = var.matcher_config_parameter_store_tier + tags = var.tags +} \ No newline at end of file diff --git a/modules/webhook/webhook.tf b/modules/webhook/webhook.tf index a155f3617b..fc3a9d53cc 100644 --- a/modules/webhook/webhook.tf +++ b/modules/webhook/webhook.tf @@ -42,16 +42,6 @@ locals { matcher_json_chunks = [for i in range(0, length(local.matcher_json), local.chunk_size) : substr(local.matcher_json, i, local.chunk_size)] } -resource "aws_ssm_parameter" "runner_matcher_config" { - count = var.storage_provider.aws.ssm != null ? local.total_chunks : 0 - - name = "${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.webhook}/runner-matcher-config${local.total_chunks > 1 ? "-${count.index}" : ""}" - type = "String" - value = local.matcher_json_chunks[count.index] - tier = var.matcher_config_parameter_store_tier - tags = var.tags -} - module "direct" { count = var.eventbridge.enable ? 0 : 1 source = "./direct" diff --git a/scripts/migrate_multi_runner_state.py b/scripts/migrate_multi_runner_state.py index 06266d643b..f1a58edd7d 100644 --- a/scripts/migrate_multi_runner_state.py +++ b/scripts/migrate_multi_runner_state.py @@ -98,16 +98,16 @@ ('module.runners.module.job_retry[0].aws_lambda_event_source_mapping.job_retry', 'module.runner_configs.module.orchestration_webhook[0].module.job_retry[0].aws_lambda_event_source_mapping.job_retry'), ('module.runners.module.job_retry[0].aws_lambda_permission.job_retry', 'module.runner_configs.module.orchestration_webhook[0].module.job_retry[0].aws_lambda_permission.job_retry'), ('module.runners.module.job_retry[0].aws_iam_role_policy.job_retry', 'module.runner_configs.module.orchestration_webhook[0].module.job_retry[0].aws_iam_role_policy.job_retry'), - ('module.runners.aws_lambda_function.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper[0].aws_lambda_function.ssm_housekeeper'), - ('module.runners.aws_cloudwatch_log_group.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper[0].aws_cloudwatch_log_group.ssm_housekeeper'), - ('module.runners.aws_cloudwatch_event_rule.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper[0].aws_cloudwatch_event_rule.ssm_housekeeper'), - ('module.runners.aws_cloudwatch_event_target.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper[0].aws_cloudwatch_event_target.ssm_housekeeper'), - ('module.runners.aws_lambda_permission.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper[0].aws_lambda_permission.ssm_housekeeper'), - ('module.runners.aws_iam_role.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper[0].aws_iam_role.ssm_housekeeper'), - ('module.runners.aws_iam_role_policy.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper[0].aws_iam_role_policy.ssm_housekeeper'), - ('module.runners.aws_iam_role_policy.ssm_housekeeper_logging', 'module.runner_configs.module.runner_config_housekeeper[0].aws_iam_role_policy.ssm_housekeeper_logging'), - ('module.runners.aws_iam_role_policy_attachment.ssm_housekeeper_vpc_execution_role', 'module.runner_configs.module.runner_config_housekeeper[0].aws_iam_role_policy_attachment.ssm_housekeeper_vpc_execution_role'), - ('module.runners.aws_iam_role_policy.ssm_housekeeper_xray', 'module.runner_configs.module.runner_config_housekeeper[0].aws_iam_role_policy.ssm_housekeeper_xray'), + ('module.runners.aws_lambda_function.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper[0].aws_lambda_function.housekeeper'), + ('module.runners.aws_cloudwatch_log_group.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper[0].aws_cloudwatch_log_group.housekeeper'), + ('module.runners.aws_cloudwatch_event_rule.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper[0].aws_cloudwatch_event_rule.housekeeper'), + ('module.runners.aws_cloudwatch_event_target.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper[0].aws_cloudwatch_event_target.housekeeper'), + ('module.runners.aws_lambda_permission.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper[0].aws_lambda_permission.housekeeper'), + ('module.runners.aws_iam_role.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper[0].aws_iam_role.housekeeper'), + ('module.runners.aws_iam_role_policy.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper[0].aws_iam_role_policy.housekeeper'), + ('module.runners.aws_iam_role_policy.ssm_housekeeper_logging', 'module.runner_configs.module.runner_config_housekeeper[0].aws_iam_role_policy.housekeeper_logging'), + ('module.runners.aws_iam_role_policy_attachment.ssm_housekeeper_vpc_execution_role', 'module.runner_configs.module.runner_config_housekeeper[0].aws_iam_role_policy_attachment.housekeeper_vpc_execution_role'), + ('module.runners.aws_iam_role_policy.ssm_housekeeper_xray', 'module.runner_configs.module.runner_config_housekeeper[0].aws_iam_role_policy.housekeeper_xray'), ) # These resources are outside the dynamic runner-key modules and therefore From 4a7fda7fa29abff28efe2aab9ebcd4473838858d Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Thu, 24 Sep 2026 16:46:02 +0200 Subject: [PATCH 19/44] fix: fix ssm null --- modules/compute-providers/aws/ec2/ami.tf | 8 +-- modules/multi-runner/main.tf | 2 +- .../multi-runner/storage-provider.aws.ssm.tf | 6 +- .../tests/config-resolution.tftest.hcl | 57 +++++++++++++++++++ modules/multi-runner/webhook.tf | 18 ++++-- .../scale-runners/scale-down-iam-policies.tf | 1 + .../scale-runners/scale-up-iam-policies.tf | 1 + .../direct/storage-provider.aws.ssm.tf | 7 ++- .../eventbridge/storage-provider.aws.ssm.tf | 12 +++- 9 files changed, 95 insertions(+), 17 deletions(-) diff --git a/modules/compute-providers/aws/ec2/ami.tf b/modules/compute-providers/aws/ec2/ami.tf index 1a294a9430..472fbbe5a0 100644 --- a/modules/compute-providers/aws/ec2/ami.tf +++ b/modules/compute-providers/aws/ec2/ami.tf @@ -9,9 +9,9 @@ locals { 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) - ami_id_ssm_external = local.ami_config.ssm_parameter != null && local.ami_config.ssm_parameter.path == null - ami_id_ssm_module_managed = local.ami_config.ssm_parameter != null && local.ami_config.ssm_parameter.path != null - ami_id_ssm_parameter_arn = local.ami_id_ssm_external ? local.ami_config.ssm_parameter.arn : null + ami_id_ssm_external = try(local.ami_config.ssm_parameter.path == null, false) + ami_id_ssm_module_managed = try(local.ami_config.ssm_parameter.path != null, false) + ami_id_ssm_parameter_arn = local.ami_id_ssm_external ? try(local.ami_config.ssm_parameter.arn, null) : 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 @@ -115,4 +115,4 @@ resource "aws_iam_policy" "ami_id_ssm_parameter_read" { 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 -} \ No newline at end of file +} diff --git a/modules/multi-runner/main.tf b/modules/multi-runner/main.tf index 0c935cd242..f88d69c766 100644 --- a/modules/multi-runner/main.tf +++ b/modules/multi-runner/main.tf @@ -22,7 +22,7 @@ locals { } ssm_root_path = trimsuffix(coalesce( - local.effective_config.storage_provider.aws.ssm.paths.root, + try(local.effective_config.storage_provider.aws.ssm.paths.root, null), "/github-action-runners/${var.prefix}", ), "/") } diff --git a/modules/multi-runner/storage-provider.aws.ssm.tf b/modules/multi-runner/storage-provider.aws.ssm.tf index adccf6bd09..1ff1b8195d 100644 --- a/modules/multi-runner/storage-provider.aws.ssm.tf +++ b/modules/multi-runner/storage-provider.aws.ssm.tf @@ -1,11 +1,11 @@ module "ssm" { source = "../storage-providers/aws/ssm" - kms_key_arn = local.effective_config.storage_provider.aws.ssm.kms_key_id - path_prefix = "${local.ssm_root_path}/${local.effective_config.storage_provider.aws.ssm.paths.app}" + kms_key_arn = try(local.effective_config.storage_provider.aws.ssm.kms_key_id, null) + path_prefix = "${local.ssm_root_path}/${try(local.effective_config.storage_provider.aws.ssm.paths.app, "app")}" github_app = local.effective_config.github.app additional_github_apps = local.effective_config.github.additional_apps tags = merge( local.tags, - local.effective_config.storage_provider.aws.ssm.tags, + try(local.effective_config.storage_provider.aws.ssm.tags, {}), ) } diff --git a/modules/multi-runner/tests/config-resolution.tftest.hcl b/modules/multi-runner/tests/config-resolution.tftest.hcl index 8d17415945..f31f6d2468 100644 --- a/modules/multi-runner/tests/config-resolution.tftest.hcl +++ b/modules/multi-runner/tests/config-resolution.tftest.hcl @@ -460,6 +460,63 @@ run "v2_inputs_do_not_require_legacy_arguments" { } } +run "v2_inputs_allow_null_ssm_storage_provider" { + command = plan + + variables { + experimental_features = ["multi-runner-v2"] + + global_config_storage_provider = { + aws = { + ssm = null + } + } + + global_config_compute_provider = { + aws = { + ec2 = { + vpc_id = "vpc-v2" + subnet_ids = ["subnet-v2"] + runner_binaries = { + enabled = false + } + } + } + } + + multi_runner_config = { + lane = { + orchestration_provider = { + webhook = { + matcherConfig = { + labelMatchers = [["self-hosted", "linux", "x64"]] + } + } + } + compute_provider = { + aws = { + ec2 = { + instance_types = ["m5.large"] + binaries_syncer = { + enabled = false + } + } + } + } + } + } + } + + assert { + condition = ( + local.use_v2_config + && local.effective_config.storage_provider.aws.ssm == null + && keys(module.runner_configs) == ["lane"] + ) + error_message = "The v2 configuration must plan successfully when SSM is not selected as the storage provider." + } +} + run "v2_inputs_require_experimental_feature" { command = plan diff --git a/modules/multi-runner/webhook.tf b/modules/multi-runner/webhook.tf index 225038f11d..0df26c8a3d 100644 --- a/modules/multi-runner/webhook.tf +++ b/modules/multi-runner/webhook.tf @@ -23,10 +23,20 @@ locals { } module "webhook" { - source = "../webhook" - prefix = var.prefix - tags = local.tags - storage_provider = local.effective_config.storage_provider + source = "../webhook" + prefix = var.prefix + tags = local.tags + storage_provider = { + aws = { + ssm = try(local.effective_config.storage_provider.aws.ssm, null) == null ? null : { + kms_key_id = try(local.effective_config.storage_provider.aws.ssm.kms_key_id, null) + paths = { + root = local.ssm_root_path + webhook = try(local.effective_config.storage_provider.aws.ssm.paths.webhook, "webhook") + } + } + } + } eventbridge = { enable = local.effective_config.orchestration_provider.webhook.eventbridge.enabled accept_events = local.effective_config.orchestration_provider.webhook.eventbridge.accept_events diff --git a/modules/orchestration-providers/webhook/scale-runners/scale-down-iam-policies.tf b/modules/orchestration-providers/webhook/scale-runners/scale-down-iam-policies.tf index a2f52c86a6..4c318daaac 100644 --- a/modules/orchestration-providers/webhook/scale-runners/scale-down-iam-policies.tf +++ b/modules/orchestration-providers/webhook/scale-runners/scale-down-iam-policies.tf @@ -2,6 +2,7 @@ data "aws_iam_policy_document" "scale_down" { source_policy_documents = compact([ data.aws_iam_policy_document.ssm_scale_down_common.json, + var.runner_provider.scale_down.iam_policy_json, ]) } diff --git a/modules/orchestration-providers/webhook/scale-runners/scale-up-iam-policies.tf b/modules/orchestration-providers/webhook/scale-runners/scale-up-iam-policies.tf index 4a23198707..d861fe93d0 100644 --- a/modules/orchestration-providers/webhook/scale-runners/scale-up-iam-policies.tf +++ b/modules/orchestration-providers/webhook/scale-runners/scale-up-iam-policies.tf @@ -30,6 +30,7 @@ data "aws_iam_policy_document" "scale_up_common" { data "aws_iam_policy_document" "scale_up" { source_policy_documents = compact([ data.aws_iam_policy_document.scale_up_common.json, + var.runner_provider.scale_up.iam_policy_json, ]) } diff --git a/modules/webhook/direct/storage-provider.aws.ssm.tf b/modules/webhook/direct/storage-provider.aws.ssm.tf index 7e9d489404..48ec6643b4 100644 --- a/modules/webhook/direct/storage-provider.aws.ssm.tf +++ b/modules/webhook/direct/storage-provider.aws.ssm.tf @@ -20,7 +20,10 @@ resource "aws_iam_role_policy" "webhook_ssm" { [for p in var.config.ssm_parameter_runner_matcher_config : p.arn] ) ) - }) : var.config.storage_provider.iam_policy_json + }) : jsonencode({ + Version = "2012-10-17" + Statement = [] + }) } moved { @@ -34,4 +37,4 @@ locals { PARAMETER_RUNNER_MATCHER_CONFIG_PATH = join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.name]) PARAMETER_RUNNER_MATCHER_VERSION = join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.version]) } : {} -} \ No newline at end of file +} diff --git a/modules/webhook/eventbridge/storage-provider.aws.ssm.tf b/modules/webhook/eventbridge/storage-provider.aws.ssm.tf index acf7b24640..06753b7675 100644 --- a/modules/webhook/eventbridge/storage-provider.aws.ssm.tf +++ b/modules/webhook/eventbridge/storage-provider.aws.ssm.tf @@ -4,7 +4,10 @@ resource "aws_iam_role_policy" "webhook_ssm" { policy = var.config.storage_provider.aws.ssm != null ? templatefile("${path.module}/../policies/lambda-ssm.json", { resource_arns = jsonencode([var.config.github_app_parameters.webhook_secret.arn]) - }) : var.config.storage_provider.webhook.iam_policy_json + }) : jsonencode({ + Version = "2012-10-17" + Statement = [] + }) } resource "aws_iam_role_policy" "webhook_kms" { @@ -39,7 +42,10 @@ resource "aws_iam_role_policy" "dispatcher_ssm" { [for p in var.config.ssm_parameter_runner_matcher_config : p.arn] ) ) - }) : var.config.storage_provider.dispatcher.iam_policy_json + }) : jsonencode({ + Version = "2012-10-17" + Statement = [] + }) } moved { @@ -62,4 +68,4 @@ locals { PARAMETER_RUNNER_MATCHER_CONFIG_PATH = join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.name]) PARAMETER_RUNNER_MATCHER_VERSION = join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.version]) } : {} -} \ No newline at end of file +} From 96e1e2a929d8a5d8ef5127c0b74bf778067d59d9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 24 Sep 2026 14:47:38 +0000 Subject: [PATCH 20/44] docs: auto update terraform docs --- modules/compute-providers/aws/ec2/README.md | 16 ++++++++-------- modules/multi-runner/README.md | 4 ++-- .../orchestration-providers/webhook/README.md | 2 +- .../webhook/job-retry/README.md | 3 ++- .../webhook/pool/README.md | 3 ++- .../webhook/scale-runners/README.md | 5 +++-- modules/runner-config/README.md | 4 ++-- modules/webhook/README.md | 2 +- modules/webhook/direct/README.md | 2 +- modules/webhook/eventbridge/README.md | 2 +- 10 files changed, 23 insertions(+), 20 deletions(-) diff --git a/modules/compute-providers/aws/ec2/README.md b/modules/compute-providers/aws/ec2/README.md index 8374424267..6e50cf82ed 100644 --- a/modules/compute-providers/aws/ec2/README.md +++ b/modules/compute-providers/aws/ec2/README.md @@ -10,15 +10,15 @@ EC2 is the only active compute provider. The parent runner configuration selects ## Requirements | Name | Version | -| ---- | ------- | +|------|---------| | [terraform](#requirement\_terraform) | >= 1.5.6 | | [aws](#requirement\_aws) | >= 6.33 | ## Providers | Name | Version | -| ---- | ------- | -| [aws](#provider\_aws) | 6.66.0 | +|------|---------| +| [aws](#provider\_aws) | >= 6.33 | | [terraform](#provider\_terraform) | n/a | ## Modules @@ -28,7 +28,7 @@ No modules. ## Resources | Name | Type | -| ---- | ---- | +|------|------| | [aws_cloudwatch_log_group.gh_runners](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | | [aws_iam_instance_profile.runner](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_instance_profile) | resource | | [aws_iam_policy.ami_id_ssm_parameter_read](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | @@ -42,6 +42,7 @@ No modules. | [terraform_data.validate_runner](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource | | [aws_ami.runner](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ami) | data source | | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_iam_policy_document.ami_id_ssm](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.ami_id_ssm_parameter_read](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.cloudwatch](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.create_tags](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | @@ -58,11 +59,10 @@ No modules. ## Inputs | Name | Description | Type | Default | Required | -| ---- | ----------- | ---- | ------- | :------: | +|------|-------------|------|---------|:--------:| | [aws\_partition](#input\_aws\_partition) | AWS partition used to construct IAM ARNs. | `string` | `"aws"` | no | | [aws\_region](#input\_aws\_region) | AWS region used by compute-provider resources and policy documents. | `string` | n/a | yes | -| [config](#input\_config) | EC2 compute-provider configuration. Paths match `compute_provider.aws.ec2` in the runner configuration.

- `ami`: Optional AMI discovery and encryption configuration. Null selects defaults for `runner.os` and `runner.architecture`.
- `ami.filter`: AMI filter names mapped to accepted values and merged over the provider defaults.
- `ami.owners`: AWS account IDs or aliases allowed to own the selected AMI.
- `ami.id_ssm_parameter`: Optional externally managed SSM parameter containing the AMI ID. Its object presence is the plan-time ownership discriminator.
- `ami.id_ssm_parameter.arn`: ARN of the external AMI-ID parameter. The ARN may remain unknown until apply.
- `ami.kms_key`: Optional customer-managed KMS key required for encrypted AMIs or snapshots. Its object presence is the plan-time policy discriminator.
- `ami.kms_key.arn`: ARN of the AMI KMS key. The ARN may remain unknown until apply.
- `vpc_id`: VPC in which runner networking resources are created.
- `subnet_ids`: Subnets from which the control plane may launch runners.
- `overrides.name_runner`: Optional Name tag override for runner compute resources.
- `overrides.name_sg`: Optional Name tag override for the managed security group.
- `instance_profile`: Optional externally managed instance profile. Its object presence is the plan-time ownership discriminator.
- `instance_profile.name`: Name of the external instance profile. The name may remain unknown until apply.
- `instance_profile_path`: IAM path for the provider-managed instance profile. Null derives the path from `prefix`.
- `binaries_syncer.enabled`: Uses the synchronized runner distribution from S3 during bootstrap.
- `binaries_syncer.s3`: S3 object containing the synchronized runner distribution. Required when synchronization is enabled.
- `binaries_syncer.s3.arn`: Runner-distribution bucket ARN used by IAM policies.
- `binaries_syncer.s3.id`: Runner-distribution bucket name used in the bootstrap URI.
- `binaries_syncer.s3.key`: Runner-distribution object key.
- `block_device_mappings`: EBS mappings added to the launch template.
- `block_device_mappings[].delete_on_termination`: Deletes the volume when its runner terminates.
- `block_device_mappings[].device_name`: Device name exposed to the runner instance.
- `block_device_mappings[].encrypted`: Enables EBS encryption.
- `block_device_mappings[].iops`: Provisioned IOPS for volume types that support configurable IOPS.
- `block_device_mappings[].kms_key_id`: KMS key ID or ARN used to encrypt the volume.
- `block_device_mappings[].snapshot_id`: Snapshot used to initialize the volume.
- `block_device_mappings[].throughput`: Provisioned throughput for volume types that support it.
- `block_device_mappings[].volume_initialization_rate`: Fixed initialization rate for supported snapshot-backed volumes.
- `block_device_mappings[].volume_size`: EBS volume size in GiB.
- `block_device_mappings[].volume_type`: EBS volume type.
- `ebs_optimized`: Requests EBS-optimized instances.
- `instance_target_capacity_type`: Primary capacity type, either `spot` or `on-demand`.
- `instance_allocation_strategy`: EC2 Fleet allocation strategy.
- `instance_type_priorities`: Optional numeric priorities keyed by instance type.
- `instance_max_spot_price`: Optional maximum hourly Spot price.
- `instance_types`: EC2 instance types available to the control plane.
- `user_data`: Runner bootstrap user-data configuration.
- `user_data.enabled`: Enables launch-template user data.
- `user_data.template`: Optional path to a custom user-data template.
- `user_data.content`: Optional complete user-data content used instead of a template.
- `user_data.pre_install`: Script inserted before runner installation.
- `user_data.post_install`: Script inserted after runner installation.
- `user_data.debug_logging_enabled`: Enables verbose user-data tracing, which can expose secrets.
- `ssm_enabled`: Includes Session Manager permissions in the provider's runner policy group.
- `create_service_linked_role_spot`: Allows scale-up to create the EC2 Spot service-linked role.
- `cloudwatch_agent.enabled`: Enables CloudWatch agent configuration for runner instances.
- `cloudwatch_agent.config`: Optional complete CloudWatch agent configuration.
- `managed_security_group_enabled`: Creates and attaches the provider-managed security group.
- `log_files`: Optional files collected by the CloudWatch agent. Null uses provider defaults.
- `log_files[].log_group_name`: CloudWatch log-group name before optional prefixing.
- `log_files[].prefix_log_group`: Prefixes the log-group name with the runner configuration path.
- `log_files[].file_path`: File or glob read by the CloudWatch agent.
- `log_files[].log_stream_name`: CloudWatch log-stream name template.
- `log_files[].log_class`: CloudWatch log-group class for the collected file.
- `key_name`: Optional EC2 key-pair name.
- `additional_security_group_ids`: Existing security groups attached to runners.
- `detailed_monitoring_enabled`: Enables detailed EC2 monitoring.
- `egress_rules`: Rules created on the managed security group.
- `egress_rules[].cidr_blocks`: IPv4 CIDR destinations.
- `egress_rules[].ipv6_cidr_blocks`: IPv6 CIDR destinations.
- `egress_rules[].prefix_list_ids`: AWS prefix-list destinations.
- `egress_rules[].from_port`: First destination port in the permitted range.
- `egress_rules[].protocol`: IP protocol name or number. Use `-1` for all protocols.
- `egress_rules[].security_groups`: Destination security-group IDs.
- `egress_rules[].self`: Allows traffic to the managed security group itself.
- `egress_rules[].to_port`: Last destination port in the permitted range.
- `egress_rules[].description`: Optional rule description.
- `tags`: Runner instance, volume, network-interface, and eligible Spot-request tags. Provider-required bootstrap tags take final precedence.
- `metadata_options`: Instance Metadata Service configuration.
- `metadata_options.instance_metadata_tags`: Exposes instance tags through Instance Metadata Service when enabled.
- `metadata_options.http_endpoint`: Enables or disables the Instance Metadata Service endpoint.
- `metadata_options.http_tokens`: Controls whether IMDSv2 session tokens are optional or required.
- `metadata_options.http_put_response_hop_limit`: Network hop limit for Instance Metadata Service token responses.
- `credit_specification`: CPU credit mode for burstable instance types.
- `cpu_options`: CPU topology and processor-feature configuration.
- `cpu_options.core_count`: Number of CPU cores exposed to the runner instance.
- `cpu_options.threads_per_core`: Number of hardware threads exposed per CPU core.
- `cpu_options.amd_sev_snp`: Enables or disables AMD SEV-SNP on supported instance types.
- `cpu_options.nested_virtualization`: Enables or disables nested virtualization on supported instance types.
- `placement`: EC2 placement configuration.
- `placement.affinity`: Dedicated Host affinity setting.
- `placement.availability_zone`: Availability Zone in which runner instances are placed.
- `placement.group_id`: Placement-group ID.
- `placement.group_name`: Placement-group name.
- `placement.host_id`: Dedicated Host ID.
- `placement.host_resource_group_arn`: ARN of the host resource group used for placement.
- `placement.spread_domain`: Spread-domain placement value.
- `placement.tenancy`: Instance tenancy, such as `default`, `dedicated`, or `host`.
- `placement.partition_number`: Placement-group partition number.
- `license_specifications`: License Manager configurations added to the launch template.
- `license_specifications[].license_configuration_arn`: ARN of an AWS License Manager license configuration.
- `associate_public_ipv4_address`: Associates a public IPv4 address with runner network interfaces.
- `network_interfaces`: Advanced network interface configuration for the launch template. Leave empty to keep using `associate_public_ipv4_address` for a simple single-interface setup.
- `on_demand_failover_for_errors`: EC2 errors that trigger on-demand fallback after a Spot failure.
- `scale_errors`: EC2 errors treated as retryable scale-up failures.
- `use_dedicated_host`: Enables the dedicated-host launch path. |
object({
ami = optional(object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
id_ssm_parameter = optional(object({
arn = string
}), null)
kms_key = optional(object({
arn = string
}), null)
}), null)
vpc_id = string
subnet_ids = list(string)
overrides = optional(object({
name_runner = optional(string, "")
name_sg = optional(string, "")
}), {})
instance_profile = optional(object({
name = string
}), null)
instance_profile_path = optional(string, null)
binaries_syncer = optional(object({
enabled = optional(bool, true)
s3 = optional(object({
arn = string
id = string
key = string
}), null)
}), {})
block_device_mappings = optional(list(object({
delete_on_termination = optional(bool, true)
device_name = optional(string, "/dev/xvda")
encrypted = optional(bool, true)
iops = optional(number)
kms_key_id = optional(string)
snapshot_id = optional(string)
throughput = optional(number)
volume_initialization_rate = optional(number)
volume_size = number
volume_type = optional(string, "gp3")
})), [{ volume_size = 30 }])
ebs_optimized = optional(bool, false)
instance_target_capacity_type = optional(string, "spot")
instance_allocation_strategy = optional(string, "lowest-price")
instance_type_priorities = optional(map(number), null)
instance_max_spot_price = optional(string, null)
instance_types = list(string)
user_data = optional(object({
enabled = optional(bool, true)
template = optional(string, null)
content = optional(string, null)
pre_install = optional(string, "")
post_install = optional(string, "")
debug_logging_enabled = optional(bool, false)
}), {})
ssm_enabled = optional(bool, false)
create_service_linked_role_spot = optional(bool, false)
cloudwatch_agent = optional(object({
enabled = optional(bool, true)
config = optional(string, null)
}), {})
managed_security_group_enabled = optional(bool, true)
log_files = optional(list(object({
log_group_name = string
prefix_log_group = bool
file_path = string
log_stream_name = string
log_class = optional(string, "STANDARD")
})), null)
key_name = optional(string, null)
additional_security_group_ids = optional(list(string), [])
detailed_monitoring_enabled = optional(bool, false)
egress_rules = optional(list(object({
cidr_blocks = list(string)
ipv6_cidr_blocks = list(string)
prefix_list_ids = list(string)
from_port = number
protocol = string
security_groups = list(string)
self = bool
to_port = number
description = string
})), [{
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
prefix_list_ids = null
from_port = 0
protocol = "-1"
security_groups = null
self = null
to_port = 0
description = null
}])
tags = optional(map(string), {})
metadata_options = optional(object({
instance_metadata_tags = optional(string, "enabled")
http_endpoint = optional(string, "enabled")
http_tokens = optional(string, "required")
http_put_response_hop_limit = optional(number, 1)
}), {})
credit_specification = optional(string, null)
cpu_options = optional(object({
core_count = optional(number)
threads_per_core = optional(number)
amd_sev_snp = optional(string)
nested_virtualization = optional(string)
}), null)
placement = optional(object({
affinity = optional(string)
availability_zone = optional(string)
group_id = optional(string)
group_name = optional(string)
host_id = optional(string)
host_resource_group_arn = optional(string)
spread_domain = optional(string)
tenancy = optional(string)
partition_number = optional(number)
}), null)
license_specifications = optional(list(object({
license_configuration_arn = string
})), [])
associate_public_ipv4_address = optional(bool, false)
network_interfaces = optional(list(object({
associate_carrier_ip_address = optional(bool)
associate_public_ip_address = optional(bool)
delete_on_termination = optional(bool)
description = optional(string)
device_index = optional(number)
interface_type = optional(string)
ipv4_address_count = optional(number)
ipv4_addresses = optional(list(string))
ipv4_prefix_count = optional(number)
ipv4_prefixes = optional(list(string))
ipv6_address_count = optional(number)
ipv6_addresses = optional(list(string))
ipv6_prefix_count = optional(number)
ipv6_prefixes = optional(list(string))
network_card_index = optional(number)
network_interface_id = optional(string)
primary_ipv6 = optional(bool)
private_ip_address = optional(string)
security_groups = optional(list(string))
subnet_id = optional(string)
connection_tracking_specification = optional(object({
tcp_established_timeout = optional(number)
udp_stream_timeout = optional(number)
udp_timeout = optional(number)
}))
ena_srd_specification = optional(object({
ena_srd_enabled = optional(bool)
ena_srd_udp_specification = optional(object({
ena_srd_udp_enabled = optional(bool)
}))
}))
})), [])
on_demand_failover_for_errors = optional(list(string), [])
scale_errors = optional(list(string), [
"UnfulfillableCapacity",
"MaxSpotInstanceCountExceeded",
"TargetCapacityLimitExceededException",
"RequestLimitExceeded",
"ResourceLimitExceeded",
"MaxSpotInstanceCountExceeded",
"MaxSpotFleetRequestCountExceeded",
"InsufficientInstanceCapacity",
"InsufficientCapacityOnHost",
])
use_dedicated_host = optional(bool, false)
})
| n/a | yes | -| [github](#input\_github) | GitHub Enterprise Server settings available to compute-provider bootstrap data.

- `enterprise_server.url`: Optional GitHub Enterprise Server base URL. Null selects GitHub.com.
- `enterprise_server.ssl_verify`: Enables TLS certificate verification for GitHub Enterprise Server. |
object({
enterprise_server = optional(object({
url = optional(string, null)
ssl_verify = optional(bool, true)
}), {})
})
| `{}` | no | +| [config](#input\_config) | EC2 compute-provider configuration. Paths match `compute_provider.aws.ec2` in the runner configuration.

- `ami`: Optional AMI discovery and encryption configuration. Null selects defaults for `runner.os` and `runner.architecture`.
- `ami.filter`: AMI filter names mapped to accepted values and merged over the provider defaults.
- `ami.owners`: AWS account IDs or aliases allowed to own the selected AMI.
- `ami.ssm_parameter`: Optional AMI-ID SSM parameter configuration. Set `arn` to use an existing parameter or `path` to create one managed by this module.
- `ami.ssm_parameter.path`: Parent path under which the module creates the `ami_id` parameter.
- `ami.ssm_parameter.arn`: ARN of an existing AMI-ID parameter.
- `ami.kms_key`: Optional customer-managed KMS key required for encrypted AMIs or snapshots. Its object presence is the plan-time policy discriminator.
- `ami.kms_key.arn`: ARN of the AMI KMS key. The ARN may remain unknown until apply.
- `vpc_id`: VPC in which runner networking resources are created.
- `subnet_ids`: Subnets from which the control plane may launch runners.
- `overrides.name_runner`: Optional Name tag override for runner compute resources.
- `overrides.name_sg`: Optional Name tag override for the managed security group.
- `instance_profile`: Optional externally managed instance profile. Its object presence is the plan-time ownership discriminator.
- `instance_profile.name`: Name of the external instance profile. The name may remain unknown until apply.
- `instance_profile_path`: IAM path for the provider-managed instance profile. Null derives the path from `prefix`.
- `binaries_syncer.enabled`: Uses the synchronized runner distribution from S3 during bootstrap.
- `binaries_syncer.s3`: S3 object containing the synchronized runner distribution. Required when synchronization is enabled.
- `binaries_syncer.s3.arn`: Runner-distribution bucket ARN used by IAM policies.
- `binaries_syncer.s3.id`: Runner-distribution bucket name used in the bootstrap URI.
- `binaries_syncer.s3.key`: Runner-distribution object key.
- `block_device_mappings`: EBS mappings added to the launch template.
- `block_device_mappings[].delete_on_termination`: Deletes the volume when its runner terminates.
- `block_device_mappings[].device_name`: Device name exposed to the runner instance.
- `block_device_mappings[].encrypted`: Enables EBS encryption.
- `block_device_mappings[].iops`: Provisioned IOPS for volume types that support configurable IOPS.
- `block_device_mappings[].kms_key_id`: KMS key ID or ARN used to encrypt the volume.
- `block_device_mappings[].snapshot_id`: Snapshot used to initialize the volume.
- `block_device_mappings[].throughput`: Provisioned throughput for volume types that support it.
- `block_device_mappings[].volume_initialization_rate`: Fixed initialization rate for supported snapshot-backed volumes.
- `block_device_mappings[].volume_size`: EBS volume size in GiB.
- `block_device_mappings[].volume_type`: EBS volume type.
- `ebs_optimized`: Requests EBS-optimized instances.
- `instance_target_capacity_type`: Primary capacity type, either `spot` or `on-demand`.
- `instance_allocation_strategy`: EC2 Fleet allocation strategy.
- `instance_type_priorities`: Optional numeric priorities keyed by instance type.
- `instance_max_spot_price`: Optional maximum hourly Spot price.
- `instance_types`: EC2 instance types available to the control plane.
- `user_data`: Runner bootstrap user-data configuration.
- `user_data.enabled`: Enables launch-template user data.
- `user_data.template`: Optional path to a custom user-data template.
- `user_data.content`: Optional complete user-data content used instead of a template.
- `user_data.pre_install`: Script inserted before runner installation.
- `user_data.post_install`: Script inserted after runner installation.
- `user_data.debug_logging_enabled`: Enables verbose user-data tracing, which can expose secrets.
- `ssm_enabled`: Includes Session Manager permissions in the provider's runner policy group.
- `create_service_linked_role_spot`: Allows scale-up to create the EC2 Spot service-linked role.
- `cloudwatch_agent.enabled`: Enables CloudWatch agent configuration for runner instances.
- `cloudwatch_agent.config`: Optional complete CloudWatch agent configuration.
- `managed_security_group_enabled`: Creates and attaches the provider-managed security group.
- `log_files`: Optional files collected by the CloudWatch agent. Null uses provider defaults.
- `log_files[].log_group_name`: CloudWatch log-group name before optional prefixing.
- `log_files[].prefix_log_group`: Prefixes the log-group name with the runner configuration path.
- `log_files[].file_path`: File or glob read by the CloudWatch agent.
- `log_files[].log_stream_name`: CloudWatch log-stream name template.
- `log_files[].log_class`: CloudWatch log-group class for the collected file.
- `key_name`: Optional EC2 key-pair name.
- `additional_security_group_ids`: Existing security groups attached to runners.
- `detailed_monitoring_enabled`: Enables detailed EC2 monitoring.
- `egress_rules`: Rules created on the managed security group.
- `egress_rules[].cidr_blocks`: IPv4 CIDR destinations.
- `egress_rules[].ipv6_cidr_blocks`: IPv6 CIDR destinations.
- `egress_rules[].prefix_list_ids`: AWS prefix-list destinations.
- `egress_rules[].from_port`: First destination port in the permitted range.
- `egress_rules[].protocol`: IP protocol name or number. Use `-1` for all protocols.
- `egress_rules[].security_groups`: Destination security-group IDs.
- `egress_rules[].self`: Allows traffic to the managed security group itself.
- `egress_rules[].to_port`: Last destination port in the permitted range.
- `egress_rules[].description`: Optional rule description.
- `tags`: Runner instance, volume, network-interface, and eligible Spot-request tags. Provider-required bootstrap tags take final precedence.
- `metadata_options`: Instance Metadata Service configuration.
- `metadata_options.instance_metadata_tags`: Exposes instance tags through Instance Metadata Service when enabled.
- `metadata_options.http_endpoint`: Enables or disables the Instance Metadata Service endpoint.
- `metadata_options.http_tokens`: Controls whether IMDSv2 session tokens are optional or required.
- `metadata_options.http_put_response_hop_limit`: Network hop limit for Instance Metadata Service token responses.
- `credit_specification`: CPU credit mode for burstable instance types.
- `cpu_options`: CPU topology and processor-feature configuration.
- `cpu_options.core_count`: Number of CPU cores exposed to the runner instance.
- `cpu_options.threads_per_core`: Number of hardware threads exposed per CPU core.
- `cpu_options.amd_sev_snp`: Enables or disables AMD SEV-SNP on supported instance types.
- `cpu_options.nested_virtualization`: Enables or disables nested virtualization on supported instance types.
- `placement`: EC2 placement configuration.
- `placement.affinity`: Dedicated Host affinity setting.
- `placement.availability_zone`: Availability Zone in which runner instances are placed.
- `placement.group_id`: Placement-group ID.
- `placement.group_name`: Placement-group name.
- `placement.host_id`: Dedicated Host ID.
- `placement.host_resource_group_arn`: ARN of the host resource group used for placement.
- `placement.spread_domain`: Spread-domain placement value.
- `placement.tenancy`: Instance tenancy, such as `default`, `dedicated`, or `host`.
- `placement.partition_number`: Placement-group partition number.
- `license_specifications`: License Manager configurations added to the launch template.
- `license_specifications[].license_configuration_arn`: ARN of an AWS License Manager license configuration.
- `associate_public_ipv4_address`: Associates a public IPv4 address with runner network interfaces.
- `network_interfaces`: Advanced network interface configuration for the launch template. Leave empty to keep using `associate_public_ipv4_address` for a simple single-interface setup.
- `on_demand_failover_for_errors`: EC2 errors that trigger on-demand fallback after a Spot failure.
- `scale_errors`: EC2 errors treated as retryable scale-up failures.
- `use_dedicated_host`: Enables the dedicated-host launch path. |
object({
ami = optional(object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
ssm_parameter = optional(object({
path = optional(string, null)
arn = optional(string, null)
}), null)
kms_key = optional(object({
arn = string
}), null)
}), null)
vpc_id = string
subnet_ids = list(string)
overrides = optional(object({
name_runner = optional(string, "")
name_sg = optional(string, "")
}), {})
instance_profile = optional(object({
name = string
}), null)
instance_profile_path = optional(string, null)
binaries_syncer = optional(object({
enabled = optional(bool, true)
s3 = optional(object({
arn = string
id = string
key = string
}), null)
}), {})
block_device_mappings = optional(list(object({
delete_on_termination = optional(bool, true)
device_name = optional(string, "/dev/xvda")
encrypted = optional(bool, true)
iops = optional(number)
kms_key_id = optional(string)
snapshot_id = optional(string)
throughput = optional(number)
volume_initialization_rate = optional(number)
volume_size = number
volume_type = optional(string, "gp3")
})), [{ volume_size = 30 }])
ebs_optimized = optional(bool, false)
instance_target_capacity_type = optional(string, "spot")
instance_allocation_strategy = optional(string, "lowest-price")
instance_type_priorities = optional(map(number), null)
instance_max_spot_price = optional(string, null)
instance_types = list(string)
user_data = optional(object({
enabled = optional(bool, true)
template = optional(string, null)
content = optional(string, null)
pre_install = optional(string, "")
post_install = optional(string, "")
debug_logging_enabled = optional(bool, false)
}), {})
ssm_enabled = optional(bool, false)
create_service_linked_role_spot = optional(bool, false)
cloudwatch_agent = optional(object({
enabled = optional(bool, true)
config = optional(string, null)
}), {})
managed_security_group_enabled = optional(bool, true)
log_files = optional(list(object({
log_group_name = string
prefix_log_group = bool
file_path = string
log_stream_name = string
log_class = optional(string, "STANDARD")
})), null)
key_name = optional(string, null)
additional_security_group_ids = optional(list(string), [])
detailed_monitoring_enabled = optional(bool, false)
egress_rules = optional(list(object({
cidr_blocks = list(string)
ipv6_cidr_blocks = list(string)
prefix_list_ids = list(string)
from_port = number
protocol = string
security_groups = list(string)
self = bool
to_port = number
description = string
})), [{
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
prefix_list_ids = null
from_port = 0
protocol = "-1"
security_groups = null
self = null
to_port = 0
description = null
}])
tags = optional(map(string), {})
metadata_options = optional(object({
instance_metadata_tags = optional(string, "enabled")
http_endpoint = optional(string, "enabled")
http_tokens = optional(string, "required")
http_put_response_hop_limit = optional(number, 1)
}), {})
credit_specification = optional(string, null)
cpu_options = optional(object({
core_count = optional(number)
threads_per_core = optional(number)
amd_sev_snp = optional(string)
nested_virtualization = optional(string)
}), null)
placement = optional(object({
affinity = optional(string)
availability_zone = optional(string)
group_id = optional(string)
group_name = optional(string)
host_id = optional(string)
host_resource_group_arn = optional(string)
spread_domain = optional(string)
tenancy = optional(string)
partition_number = optional(number)
}), null)
license_specifications = optional(list(object({
license_configuration_arn = string
})), [])
associate_public_ipv4_address = optional(bool, false)
network_interfaces = optional(list(object({
associate_carrier_ip_address = optional(bool)
associate_public_ip_address = optional(bool)
delete_on_termination = optional(bool)
description = optional(string)
device_index = optional(number)
interface_type = optional(string)
ipv4_address_count = optional(number)
ipv4_addresses = optional(list(string))
ipv4_prefix_count = optional(number)
ipv4_prefixes = optional(list(string))
ipv6_address_count = optional(number)
ipv6_addresses = optional(list(string))
ipv6_prefix_count = optional(number)
ipv6_prefixes = optional(list(string))
network_card_index = optional(number)
network_interface_id = optional(string)
primary_ipv6 = optional(bool)
private_ip_address = optional(string)
security_groups = optional(list(string))
subnet_id = optional(string)
connection_tracking_specification = optional(object({
tcp_established_timeout = optional(number)
udp_stream_timeout = optional(number)
udp_timeout = optional(number)
}))
ena_srd_specification = optional(object({
ena_srd_enabled = optional(bool)
ena_srd_udp_specification = optional(object({
ena_srd_udp_enabled = optional(bool)
}))
}))
})), [])
on_demand_failover_for_errors = optional(list(string), [])
scale_errors = optional(list(string), [
"UnfulfillableCapacity",
"MaxSpotInstanceCountExceeded",
"TargetCapacityLimitExceededException",
"RequestLimitExceeded",
"ResourceLimitExceeded",
"MaxSpotInstanceCountExceeded",
"MaxSpotFleetRequestCountExceeded",
"InsufficientInstanceCapacity",
"InsufficientCapacityOnHost",
])
use_dedicated_host = optional(bool, false)
})
| n/a | yes | | [observability](#input\_observability) | CloudWatch Logs settings available to compute-provider runner log groups.

- `logs.retention_in_days`: Retention period for provider-owned runner log groups.
- `logs.kms_key_id`: Optional KMS key ID or ARN used to encrypt runner log groups.
- `logs.tags`: Shared log-group tags that override module-level `tags`. |
object({
logs = optional(object({
retention_in_days = optional(number, 180)
kms_key_id = optional(string, null)
tags = optional(map(string), {})
}), {})
})
| `{}` | no | | [prefix](#input\_prefix) | Prefix used to identify resources created for the runner configuration. | `string` | `"github-actions"` | no | | [runner](#input\_runner) | Provider-neutral runner settings consumed by compute providers.

- `os`: Runner operating system. Supported values are `linux`, `osx`, and `windows`.
- `architecture`: Runner distribution architecture.
- `name_prefix`: Prefix added to registered runner names.
- `run_as_root`: Runs the runner service as root.
- `run_as`: Operating-system user used when `run_as_root` is false.
- `hooks.job_started`: Script installed as the runner job-started hook.
- `hooks.job_completed`: Script installed as the runner job-completed hook.
- `iam.role.arn`: Resolved runner-role ARN referenced by provider policies and resources.
- `iam.role.name`: Resolved runner-role name used by provider resources.
- `iam.role.managed`: Whether runner-config manages the resolved runner role.
- `iam.managed_policy_arns`: Common managed-policy ARNs returned with the provider-specific runner policies for attachment by runner-config.
- `iam.path`: IAM path available to provider-managed IAM resources. Null derives the path from `prefix`. |
object({
os = optional(string, "linux")
architecture = optional(string, "x64")
name_prefix = optional(string, "")
run_as_root = optional(bool, false)
run_as = optional(string, "ec2-user")
hooks = optional(object({
job_started = optional(string, "")
job_completed = optional(string, "")
}), {})
iam = object({
role = object({
arn = string
name = string
managed = optional(bool, true)
})
managed_policy_arns = optional(map(string), {})
path = optional(string, null)
})
})
| n/a | yes | @@ -72,7 +72,7 @@ No modules. ## Outputs | Name | Description | -| ---- | ----------- | +|------|-------------| | [environment\_variables](#output\_environment\_variables) | Provider-specific Lambda environment variable fragments consumed by runner-config. | | [policies](#output\_policies) | Provider-specific IAM policy fragments consumed by runner-config. | | [provider](#output\_provider) | Nested EC2 compute-provider contract consumed by runner-config. | diff --git a/modules/multi-runner/README.md b/modules/multi-runner/README.md index c6d7da3f8e..92cf3fe141 100644 --- a/modules/multi-runner/README.md +++ b/modules/multi-runner/README.md @@ -109,8 +109,8 @@ module "multi-runner" { | Name | Version | |------|---------| -| [aws](#provider\_aws) | 6.63.0 | -| [random](#provider\_random) | 3.9.0 | +| [aws](#provider\_aws) | >= 6.33 | +| [random](#provider\_random) | ~> 3.0 | | [terraform](#provider\_terraform) | n/a | ## Modules diff --git a/modules/orchestration-providers/webhook/README.md b/modules/orchestration-providers/webhook/README.md index c4ff48f7bf..cab0c4d755 100644 --- a/modules/orchestration-providers/webhook/README.md +++ b/modules/orchestration-providers/webhook/README.md @@ -46,7 +46,7 @@ The scale-down lifecycle is documented in the [scale-down state diagram](./scale | [prefix](#input\_prefix) | Prefix used to identify resources created for this webhook orchestration provider. | `string` | n/a | yes | | [runner](#input\_runner) | Common runner registration values consumed by webhook demand controls. Lifecycle, boot timeout, and capacity remain provider-owned under config.runner. |
object({
os = string
auto_update_disabled = bool
labels = list(string)
group_name = string
name_prefix = string
})
| n/a | yes | | [runner\_provider](#input\_runner\_provider) | Selected compute-provider capabilities consumed by webhook scale-up, scale-down, and pool controls. |
object({
type = string
scale_up = object({
environment_variables = map(string)
iam_policy_json = string
additional_iam_policy_json = optional(string, null)
managed_policy = optional(object({
arn = string
}), null)
})
scale_down = object({
environment_variables = map(string)
iam_policy_json = string
})
pool = object({
environment_variables = map(string)
iam_policy_json = string
managed_policy_enabled = bool
managed_policy_arn = optional(string, null)
})
})
| n/a | yes | -| [storage\_provider](#input\_storage\_provider) | Resolved storage-provider values and optional provider-owned Lambda capabilities.

- `storage_provider.aws.ssm.token_path`: Resolved Parameter Store path for registration tokens.
- `storage_provider.aws.ssm.token_path_arn`: ARN of the registration-token Parameter Store path.
- `storage_provider.aws.ssm.config_path`: Resolved Parameter Store path for runner configuration.
- `storage_provider.aws.ssm.config_path_arn`: ARN of the runner-configuration Parameter Store path.
- `storage_provider.aws.ssm.kms_key_id`: Optional KMS key used to decrypt shared parameters.
- `storage_provider.aws.ssm.parameter_store_tags`: JSON-encoded tags applied to runtime parameters.
- `scale_up`, `scale_down`, `pool`, and `job_retry`: Provider-owned environment variables and IAM policy fragments. |
object({
aws = object({
ssm = optional(object({
token_path = string
token_path_arn = string
config_path = string
config_path_arn = string
kms_key_id = optional(string, null)
parameter_store_tags = string
}), null)
})
scale_up = optional(object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
}), {
environment_variables = {}
iam_policy_json = null
})
scale_down = optional(object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
}), {
environment_variables = {}
iam_policy_json = null
})
pool = optional(object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
}), {
environment_variables = {}
iam_policy_json = null
})
job_retry = optional(object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
}), {
environment_variables = {}
iam_policy_json = null
})
})
| n/a | yes | +| [storage\_provider](#input\_storage\_provider) | Resolved storage-provider values and optional provider-owned Lambda capabilities.

- `storage_provider.aws.ssm.token_path`: Resolved Parameter Store path for registration tokens.
- `storage_provider.aws.ssm.token_path_arn`: ARN of the registration-token Parameter Store path.
- `storage_provider.aws.ssm.config_path`: Resolved Parameter Store path for runner configuration.
- `storage_provider.aws.ssm.config_path_arn`: ARN of the runner-configuration Parameter Store path.
- `storage_provider.aws.ssm.kms_key_id`: Optional KMS key used to decrypt shared parameters.
- `storage_provider.aws.ssm.parameter_store_tags`: JSON-encoded tags applied to runtime parameters.
- `scale_up`, `scale_down`, `pool`, and `job_retry`: Provider-owned environment variables and IAM policy fragments. |
object({
aws = object({
ssm = optional(object({
token_path = string
token_path_arn = string
config_path = string
config_path_arn = string
kms_key_id = optional(string, null)
parameter_store_tags = string
}), null)
})
})
| n/a | yes | | [tags](#input\_tags) | Base tags available to webhook-provider resources. Component-specific tags override this map within their documented scopes. | `map(string)` | `{}` | no | ## Outputs diff --git a/modules/orchestration-providers/webhook/job-retry/README.md b/modules/orchestration-providers/webhook/job-retry/README.md index 42f520dd7a..26bb38cce1 100644 --- a/modules/orchestration-providers/webhook/job-retry/README.md +++ b/modules/orchestration-providers/webhook/job-retry/README.md @@ -47,13 +47,14 @@ No modules. | [aws_iam_policy_document.job_retry_logging](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.lambda_assume_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.lambda_xray](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.ssm_job_retry](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [config](#input\_config) | Provider-neutral job-retry configuration assembled by runner-config.

- `prefix`: Prefix used to name job-retry resources.
- `aws_partition`: AWS partition used to construct the Lambda VPC managed-policy ARN.
- `lambda.artifact.zip`: Resolved local control-plane archive.
- `lambda.artifact.s3.bucket`: Optional S3 bucket containing the Lambda archive.
- `lambda.artifact.s3.key`: Object key of the Lambda archive.
- `lambda.artifact.s3.object_version`: Optional object version of the Lambda archive.
- `lambda.runtime`: Runtime used by the job-retry Lambda.
- `lambda.architecture`: Instruction-set architecture used by the job-retry Lambda.
- `lambda.memory_size`: Memory allocated to the job-retry Lambda.
- `lambda.timeout`: Lambda timeout and retry-queue visibility timeout in seconds.
- `lambda.reserved_concurrent_executions`: Reserved concurrency for the Lambda. Use `-1` for unreserved concurrency.
- `lambda.environment_variables`: Additional Lambda environment variables. Required job-retry variables override matching keys.
- `lambda.vpc.subnet_ids`: Subnets used for Lambda VPC configuration.
- `lambda.vpc.security_group_ids`: Security groups used for Lambda VPC configuration.
- `lambda.role.path`: IAM path used for the job-retry Lambda role.
- `lambda.role.permissions_boundary`: Optional permissions boundary for the Lambda role.
- `lambda.role.principals`: Extra principals allowed to assume the Lambda role, for example during local testing.
- `runner.name_prefix`: Prefix used to identify runners belonging to this runner configuration.
- `github.organization_runners`: Enables organization runners.
- `github.enterprise_server.url`: Optional GitHub Enterprise Server URL.
- `github.enterprise_server.ssl_verify`: Enables TLS certificate verification for GitHub Enterprise Server requests.
- `github.user_agent`: Optional User-Agent sent to GitHub.
- `github.app_parameters.key_base64`: Parameter Store reference for the primary GitHub App private key.
- `github.app_parameters.id`: Parameter Store reference for the primary GitHub App ID.
- `github.app_parameters.additional_apps_manifest`: Optional Parameter Store reference containing the additional GitHub App manifest.
- `github.app_parameters.additional_app_parameter_arns`: ARNs of the additional GitHub App credential parameters.
- `queue.build`: URL and ARN of the build queue to which retry messages are published.
- `queue.kms_key_id`: Optional KMS key ARN used to encrypt the build queue. This is distinct from the Parameter Store key.
- `queue.event_source_mapping.batch_size`: Maximum records delivered per job-retry invocation.
- `queue.event_source_mapping.maximum_batching_window_in_seconds`: Maximum event batching window.
- `queue.encryption`: Server-side encryption configuration for the retry queue.
- `storage_provider.aws.ssm.kms_key_id`: Optional KMS key ARN used by the job-retry IAM policy. Its value may be unknown until apply.
- `observability.logs`: Logging level, retention, encryption, and log-class configuration.
- `observability.tracing`: Lambda X-Ray and tracing-helper configuration.
- `observability.metrics`: Metrics enablement, namespace, and job-retry metric configuration.
- `tags.resources`: Tags for the job-retry Lambda role and component resources.
- `tags.lambda`: Tags for the job-retry Lambda function.
- `tags.log_group`: Tags for the job-retry log group.
- `tags.queue`: Tags for the retry queue.
- `tags.event_source_mapping`: Tags for the retry-queue event-source mapping. |
object({
prefix = string
aws_partition = string
lambda = object({
artifact = object({
zip = string
s3 = object({
bucket = optional(string, null)
key = optional(string, null)
object_version = optional(string, null)
})
})
runtime = string
architecture = string
memory_size = number
timeout = number
reserved_concurrent_executions = number
environment_variables = map(string)
vpc = object({
subnet_ids = list(string)
security_group_ids = list(string)
})
role = object({
path = string
permissions_boundary = optional(string, null)
principals = list(object({
type = string
identifiers = list(string)
}))
})
})
runner = object({
name_prefix = string
})
github = object({
organization_runners = bool
enterprise_server = object({
url = optional(string, null)
ssl_verify = optional(bool, true)
})
user_agent = optional(string, null)
app_parameters = object({
key_base64 = map(string)
id = map(string)
additional_apps_manifest = optional(object({
name = string
arn = string
}), null)
additional_app_parameter_arns = optional(list(string), [])
})
})
queue = object({
build = object({
url = string
arn = string
})
kms_key_id = optional(string, null)
event_source_mapping = object({
batch_size = number
maximum_batching_window_in_seconds = number
})
encryption = object({
sqs_managed_sse_enabled = bool
kms_master_key_id = optional(string, null)
kms_data_key_reuse_period_seconds = optional(number, null)
})
})
observability = object({
logs = object({
level = string
retention_in_days = number
kms_key_id = optional(string, null)
class = string
})
tracing = object({
mode = optional(string, null)
capture_http_requests = bool
capture_error = bool
})
metrics = object({
enabled = bool
namespace = string
metric = object({
github_app_rate_limit = object({
enabled = bool
})
job_retry = object({
enabled = bool
})
})
})
})
tags = object({
resources = map(string)
lambda = map(string)
log_group = map(string)
queue = map(string)
event_source_mapping = map(string)
})
})
| n/a | yes | -| [storage\_provider](#input\_storage\_provider) | Resolved storage-provider configuration and capability used by the job-retry Lambda. |
object({
aws = object({
ssm = optional(object({
kms_key_id = optional(string, null)
}), null)
})
environment_variables = optional(map(string), {})
iam_policy_json = optional(string, null)
})
| n/a | yes | +| [storage\_provider](#input\_storage\_provider) | Resolved storage-provider configuration and capability used by the job-retry Lambda. |
object({
aws = object({
ssm = optional(object({
kms_key_id = optional(string, null)
}), null)
})
})
| n/a | yes | ## Outputs diff --git a/modules/orchestration-providers/webhook/pool/README.md b/modules/orchestration-providers/webhook/pool/README.md index 48d7af5203..cca5e0b97d 100644 --- a/modules/orchestration-providers/webhook/pool/README.md +++ b/modules/orchestration-providers/webhook/pool/README.md @@ -48,6 +48,7 @@ No modules. | [aws_iam_policy_document.pool_logging](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.scheduler](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.scheduler_assume](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.ssm_pool_common](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | ## Inputs @@ -56,7 +57,7 @@ No modules. | [aws\_partition](#input\_aws\_partition) | (optional) partition for the arn if not 'aws' | `string` | `"aws"` | no | | [config](#input\_config) | Configuration passed from the webhook orchestration provider to the pool Lambda and scheduler.

- `lambda`: Pool Lambda runtime and deployment configuration.
- `lambda.log_level`: Logging level used by the pool Lambda.
- `lambda.logging_retention_in_days`: Number of days to retain events in the pool Lambda log group.
- `lambda.logging_kms_key_id`: KMS key ID used to encrypt the pool Lambda log group.
- `lambda.log_class`: CloudWatch Logs class for the pool Lambda log group.
- `lambda.reserved_concurrent_executions`: Reserved concurrency for the pool Lambda. Use -1 for no reservation.
- `lambda.s3_bucket`: S3 bucket containing the pool Lambda deployment package.
- `lambda.s3_key`: S3 key of the pool Lambda deployment package.
- `lambda.s3_object_version`: S3 object version of the pool Lambda deployment package.
- `lambda.security_group_ids`: Security group IDs associated with the pool Lambda.
- `lambda.runtime`: AWS Lambda runtime used by the pool Lambda.
- `lambda.architecture`: AWS Lambda architecture used by the pool Lambda.
- `lambda.memory_size`: Memory allocated to the pool Lambda in MB.
- `lambda.timeout`: Pool Lambda timeout in seconds.
- `lambda.zip`: Local path to the pool Lambda deployment package when S3 is not used.
- `lambda.subnet_ids`: Subnet IDs in which the pool Lambda runs.
- `lambda.principals`: Additional principals allowed to assume the pool Lambda role.
- `tags`: Common tags added to pool resources.
- `ghes`: GitHub Enterprise Server connection configuration.
- `ghes.url`: GitHub Enterprise Server URL; null when using public GitHub.
- `ghes.ssl_verify`: Whether the pool Lambda verifies the GitHub Enterprise Server TLS certificate.
- `github_app_parameters`: SSM parameter metadata for the primary and additional GitHub App credentials.
- `github_app_parameters.key_base64`: Parameter Store reference for the primary GitHub App private key.
- `github_app_parameters.id`: Parameter Store reference for the primary GitHub App ID.
- `github_app_parameters.additional_apps_manifest`: Optional Parameter Store reference containing the additional GitHub App manifest.
- `github_app_parameters.additional_app_parameter_arns`: ARNs of the additional GitHub App credential parameters.
- `runner`: Runner registration configuration used by the pool Lambda.
- `runner.disable_runner_autoupdate`: Whether GitHub runner automatic updates are disabled.
- `runner.ephemeral`: Whether runners register as ephemeral runners.
- `runner.enable_jit_config`: Whether runners use just-in-time registration configuration.
- `runner.labels`: Labels assigned to runners created by the pool Lambda.
- `runner.group_name`: GitHub runner group assigned to runners created by the pool Lambda.
- `runner.name_prefix`: Prefix used for runner names.
- `runner.pool_owner`: GitHub organization or repository that owns the runner pool.
- `runner.boot_time_in_minutes`: Webhook-provider runner boot timeout used by pool reconciliation.
- `runners_maximum_count`: Webhook-provider runner capacity limit enforced by the pool Lambda.
- `prefix`: Prefix used to name pool resources.
- `pool`: Scheduled pool targets.
- `pool[*].schedule_expression`: EventBridge Scheduler expression for a pool target.
- `pool[*].schedule_expression_timezone`: Time zone used to evaluate the schedule expression.
- `pool[*].size`: Desired runner count for the scheduled pool target.
- `include_busy_runners`: Whether busy runners count toward the desired pool size.
- `role_permissions_boundary`: Permissions boundary applied to IAM roles created for the pool.
- `role_path`: IAM path applied to roles created for the pool.
- `lambda_tags`: Tags added specifically to the pool Lambda function, overriding common tags with the same key.
- `log_group_tags`: Tags added specifically to the pool Lambda log group, overriding common tags with the same key.
- `user_agent`: User-Agent header used for GitHub API requests. |
object({
lambda = object({
log_level = string
logging_retention_in_days = number
logging_kms_key_id = string
log_class = string
reserved_concurrent_executions = number
s3_bucket = string
s3_key = string
s3_object_version = string
security_group_ids = list(string)
runtime = string
architecture = string
memory_size = number
timeout = number
zip = string
subnet_ids = list(string)
principals = optional(list(object({
type = string
identifiers = list(string)
})), [])
})
tags = map(string)
ghes = object({
url = string
ssl_verify = string
})
github_app_parameters = object({
key_base64 = map(string)
id = map(string)
additional_apps_manifest = optional(object({
name = string
arn = string
}), null)
additional_app_parameter_arns = optional(list(string), [])
})
runner = object({
disable_runner_autoupdate = bool
ephemeral = bool
enable_jit_config = bool
labels = list(string)
group_name = string
name_prefix = string
pool_owner = string
boot_time_in_minutes = number
})
runners_maximum_count = number
prefix = string
pool = list(object({
schedule_expression = string
schedule_expression_timezone = string
size = number
}))
include_busy_runners = bool
role_permissions_boundary = string
role_path = string
lambda_tags = map(string)
log_group_tags = optional(map(string), {})
user_agent = string
})
| n/a | yes | | [runner\_provider](#input\_runner\_provider) | Compute provider integration used by the pool Lambda.

- `type`: Compute provider type passed to scheduled pool invocations.
- `environment_variables`: Provider-specific environment variables added to the pool Lambda.
- `iam_policy_json`: Provider-specific IAM policy document merged into the pool Lambda policy.
- `managed_policy_enabled`: Whether to attach a provider-specific managed IAM policy to the pool Lambda role.
- `managed_policy_arn`: ARN of the provider-specific managed IAM policy to attach when enabled. |
object({
type = string
environment_variables = map(string)
iam_policy_json = string
managed_policy_enabled = bool
managed_policy_arn = optional(string, null)
})
| n/a | yes | -| [storage\_provider](#input\_storage\_provider) | Resolved storage-provider configuration and capability used by the pool Lambda. |
object({
aws = object({
ssm = optional(object({
token_path = string
token_path_arn = string
config_path = string
config_path_arn = string
kms_key_id = optional(string, null)
parameter_store_tags = string
}), null)
})
environment_variables = optional(map(string), {})
iam_policy_json = optional(string, null)
})
| n/a | yes | +| [storage\_provider](#input\_storage\_provider) | Resolved storage-provider configuration and capability used by the pool Lambda. |
object({
aws = object({
ssm = optional(object({
token_path = string
token_path_arn = string
config_path = string
config_path_arn = string
kms_key_id = optional(string, null)
parameter_store_tags = string
}), null)
})
})
| n/a | yes | | [tracing\_config](#input\_tracing\_config) | Tracing configuration for the pool Lambda.

- `mode`: AWS X-Ray tracing mode. A null value disables tracing.
- `capture_http_requests`: Whether Powertools tracing captures outgoing HTTP requests.
- `capture_error`: Whether Powertools tracing captures errors as tracing metadata. |
object({
mode = optional(string, null)
capture_http_requests = optional(bool, false)
capture_error = optional(bool, false)
})
| `{}` | no | ## Outputs diff --git a/modules/orchestration-providers/webhook/scale-runners/README.md b/modules/orchestration-providers/webhook/scale-runners/README.md index 50fa51e79a..5388a312fa 100644 --- a/modules/orchestration-providers/webhook/scale-runners/README.md +++ b/modules/orchestration-providers/webhook/scale-runners/README.md @@ -55,12 +55,13 @@ No modules. | [aws_iam_policy_document.lambda_assume_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.lambda_xray](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.scale_down](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | -| [aws_iam_policy_document.scale_down_common](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.scale_down_logging](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.scale_up](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.scale_up_common](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.scale_up_job_retry_publish](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.scale_up_logging](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.ssm_scale_down_common](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.ssm_scale_up_common](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | ## Inputs @@ -69,7 +70,7 @@ No modules. | [aws\_partition](#input\_aws\_partition) | AWS partition used to construct IAM policy ARNs. | `string` | `"aws"` | no | | [config](#input\_config) | Provider-neutral scale-up and scale-down configuration assembled by runner-config.

- `prefix`: Prefix used to name scaling resources.
- `lambda.artifact.zip`: Resolved local control-plane archive.
- `lambda.artifact.s3.bucket`: Optional S3 bucket containing the Lambda archive.
- `lambda.artifact.s3.key`: Object key of the Lambda archive.
- `lambda.artifact.s3.object_version`: Optional object version of the Lambda archive.
- `lambda.runtime`: Runtime used by both scaling Lambdas.
- `lambda.architecture`: Instruction-set architecture used by both scaling Lambdas.
- `lambda.vpc.subnet_ids`: Subnets used for Lambda VPC configuration.
- `lambda.vpc.security_group_ids`: Security groups used for Lambda VPC configuration.
- `lambda.role.path`: IAM path used for the scaling Lambda roles.
- `lambda.role.permissions_boundary`: Optional permissions boundary for the scaling Lambda roles.
- `lambda.role.principals`: Additional principals allowed to assume the scaling Lambda roles.
- `runner.os`: Runner operating system used for the minimum-runtime default.
- `runner.auto_update_disabled`: Disables the GitHub runner application's built-in updater.
- `runner.ephemeral`: Registers runners in ephemeral mode.
- `runner.jit_config_enabled`: Enables or disables just-in-time runner configuration.
- `runner.labels`: Labels supplied when a runner is registered.
- `runner.group_name`: GitHub runner group used during registration.
- `runner.name_prefix`: Prefix added to registered runner names.
- `runner.boot_time_in_minutes`: Webhook-provider runner boot timeout used by scale-down.
- `runner.maximum_count`: Webhook-provider runner capacity limit for this runner configuration.
- `github.organization_runners`: Registers organization runners when true.
- `github.enterprise_server.url`: Optional GitHub Enterprise Server URL.
- `github.enterprise_server.ssl_verify`: Enables TLS verification for GitHub Enterprise Server.
- `github.user_agent`: Optional User-Agent sent to GitHub.
- `github.app_parameters.key_base64`: Parameter Store reference for the primary GitHub App private key.
- `github.app_parameters.id`: Parameter Store reference for the primary GitHub App ID.
- `github.app_parameters.additional_apps_manifest`: Optional Parameter Store reference containing the additional GitHub App manifest.
- `github.app_parameters.additional_app_parameter_arns`: ARNs of the additional GitHub App credential parameters.
- `queue.build.arn`: ARN of the build queue consumed by scale-up.
- `queue.kms_key_id`: Optional KMS key ARN used to encrypt the build queue. This is distinct from the Parameter Store key.
- `queue.event_source_mapping.batch_size`: Maximum records delivered per scale-up invocation.
- `queue.event_source_mapping.maximum_batching_window_in_seconds`: Maximum event batching window.
- `storage_provider.aws.ssm.token_path`: Parameter Store path used for registration tokens.
- `storage_provider.aws.ssm.token_path_arn`: ARN of the Parameter Store path used for registration tokens.
- `storage_provider.aws.ssm.config_path`: Parameter Store path used for persistent runner configuration.
- `storage_provider.aws.ssm.config_path_arn`: ARN of the persistent runner configuration path.
- `storage_provider.aws.ssm.kms_key_id`: Optional KMS key ARN used to decrypt shared parameters. Its value may be unknown until apply.
- `storage_provider.aws.ssm.parameter_store_tags`: JSON-encoded tags applied to parameters created at runtime.
- `observability.logs`: Shared logging level, retention, encryption, and log-class configuration.
- `observability.tracing`: Lambda X-Ray and tracing-helper configuration.
- `observability.metrics`: Metrics enablement, namespace, and GitHub rate-limit metric configuration.
- `scale_up`: Scale-up Lambda sizing, concurrency, queued-job behavior, and resolved resource tag maps.
- `scale_up.tags.resources`: Tags for the scale-up IAM role and other component resources.
- `scale_up.tags.lambda`: Tags for the scale-up Lambda function.
- `scale_up.tags.log_group`: Tags for the scale-up log group.
- `scale_up.tags.event_source_mapping`: Tags for the build-queue event-source mapping.
- `scale_down`: Scale-down Lambda sizing, schedule, idle configuration, minimum runtime, and resolved resource tag maps.
- `scale_down.idle_confirmation_seconds`: Number of seconds a runner must consistently report not-busy before scale-down terminates it. GitHub's busy flag can be stale (it can read false for a runner that is actively executing a job), so a single not-busy reading is not sufficient evidence a runner is idle. Set to at least one scale-down schedule interval to require two consecutive not-busy evaluations; a busy reading resets the window. 0 keeps the previous single-reading behaviour.
- `scale_down.tags.resources`: Tags for the scale-down IAM role and EventBridge rule.
- `scale_down.tags.lambda`: Tags for the scale-down Lambda function.
- `scale_down.tags.log_group`: Tags for the scale-down log group.
- `job_retry.enabled`: Enables publishing retry checks from scale-up.
- `job_retry.queue`: Retry queue ARN and URL. Required when job retry is enabled.
- `job_retry.max_attempts`: Maximum queued-job retry attempts.
- `job_retry.delay_in_seconds`: Initial delay before checking the queued job.
- `job_retry.delay_backoff`: Multiplier applied to subsequent delays. |
object({
prefix = string
lambda = object({
artifact = object({
zip = string
s3 = object({
bucket = optional(string, null)
key = optional(string, null)
object_version = optional(string, null)
})
})
runtime = string
architecture = string
vpc = object({
subnet_ids = list(string)
security_group_ids = list(string)
})
role = object({
path = string
permissions_boundary = optional(string, null)
principals = optional(list(object({
type = string
identifiers = list(string)
})), [])
})
})
runner = object({
os = string
auto_update_disabled = bool
ephemeral = bool
jit_config_enabled = optional(bool, null)
labels = list(string)
group_name = string
name_prefix = string
boot_time_in_minutes = number
maximum_count = number
})
github = object({
organization_runners = bool
enterprise_server = object({
url = optional(string, null)
ssl_verify = bool
})
user_agent = optional(string, null)
app_parameters = object({
key_base64 = map(string)
id = map(string)
additional_apps_manifest = optional(object({
name = string
arn = string
}), null)
additional_app_parameter_arns = optional(list(string), [])
})
})
queue = object({
build = object({
arn = string
})
kms_key_id = optional(string, null)
event_source_mapping = object({
batch_size = number
maximum_batching_window_in_seconds = number
})
})
observability = object({
logs = object({
level = string
retention_in_days = number
kms_key_id = optional(string, null)
class = string
})
tracing = object({
mode = optional(string, null)
capture_http_requests = bool
capture_error = bool
})
metrics = object({
enabled = bool
namespace = string
metric = object({
github_app_rate_limit = object({
enabled = bool
})
})
})
})
scale_up = object({
memory_size = number
timeout = number
reserved_concurrent_executions = number
job_queued_check_enabled = bool
tags = object({
resources = map(string)
lambda = map(string)
log_group = map(string)
event_source_mapping = map(string)
})
})
scale_down = object({
memory_size = number
timeout = number
schedule_expression = string
minimum_running_time_in_minutes = optional(number, null)
idle_confirmation_seconds = optional(number, 0)
idle_config = list(object({
cron = string
timeZone = string
idleCount = number
evictionStrategy = string
}))
tags = object({
resources = map(string)
lambda = map(string)
log_group = map(string)
})
})
job_retry = object({
enabled = bool
max_attempts = number
delay_in_seconds = number
delay_backoff = number
queue = optional(object({
arn = string
url = string
}), null)
})
})
| n/a | yes | | [runner\_provider](#input\_runner\_provider) | Selected compute-provider integration for the scaling control plane.

- `type`: Compute-provider discriminator supplied to both Lambdas.
- `scale_up.environment_variables`: Provider-specific scale-up environment variables.
- `scale_up.iam_policy_json`: Provider-specific IAM policy merged into the common scale-up policy.
- `scale_up.additional_iam_policy_json`: Optional additional provider policy attached separately to the scale-up role.
- `scale_up.managed_policy`: Optional provider-managed policy attachment. Object presence controls attachment creation.
- `scale_up.managed_policy.arn`: ARN of the provider-managed policy. The ARN may remain unknown until apply.
- `scale_down.environment_variables`: Provider-specific scale-down environment variables.
- `scale_down.iam_policy_json`: Provider-specific IAM policy merged into the common scale-down policy. |
object({
type = string
scale_up = object({
environment_variables = map(string)
iam_policy_json = string
additional_iam_policy_json = optional(string, null)
managed_policy = optional(object({
arn = string
}), null)
})
scale_down = object({
environment_variables = map(string)
iam_policy_json = string
})
})
| n/a | yes | -| [storage\_provider](#input\_storage\_provider) | Resolved storage-provider configuration and capabilities for scale-up and scale-down. |
object({
aws = object({
ssm = optional(object({
token_path = string
token_path_arn = string
config_path = string
config_path_arn = string
parameter_store_tags = string
kms_key_id = optional(string, null)
}), null)
})
scale_up = optional(object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
}), {
environment_variables = {}
iam_policy_json = null
})
scale_down = optional(object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
}), {
environment_variables = {}
iam_policy_json = null
})
})
| n/a | yes | +| [storage\_provider](#input\_storage\_provider) | Resolved storage-provider configuration and capabilities for scale-up and scale-down. |
object({
aws = object({
ssm = optional(object({
token_path = string
token_path_arn = string
config_path = string
config_path_arn = string
parameter_store_tags = string
kms_key_id = optional(string, null)
}), null)
})
})
| n/a | yes | ## Outputs diff --git a/modules/runner-config/README.md b/modules/runner-config/README.md index 3c1ca3ad93..559085a186 100644 --- a/modules/runner-config/README.md +++ b/modules/runner-config/README.md @@ -109,7 +109,7 @@ yarn run dist |------|-------------|------|---------|:--------:| | [aws\_partition](#input\_aws\_partition) | AWS partition used to construct ARNs. | `string` | `"aws"` | no | | [aws\_region](#input\_aws\_region) | AWS region. | `string` | n/a | yes | -| [compute\_provider](#input\_compute\_provider) | Typed compute-provider configuration. Provider-owned settings remain inside the selected compute-provider block.

Exactly one compute-provider block must be non-null. The populated block selects the provider, and its presence must be known during planning. Values inside the selected block may remain unknown until apply.

- `aws`: AWS compute-provider configurations.
- `aws.ec2`: EC2 compute-provider configuration.
- `aws.ec2.ami`: Optional AMI discovery or external AMI-parameter configuration. Null uses the operating-system and architecture defaults.
- `aws.ec2.ami.filter`: EC2 AMI filters combined with the provider's default AMI-name filter.
- `aws.ec2.ami.owners`: AWS account IDs or aliases allowed to own the selected AMI.
- `aws.ec2.ami.id_ssm_parameter`: Optional externally managed SSM parameter containing the AMI ID. Null creates a provider-managed AMI-ID parameter. The wrapper's presence is the plan-time ownership discriminator, so keep the object literal even when its ARN comes from another resource.
- `aws.ec2.ami.id_ssm_parameter.arn`: ARN of the externally managed SSM parameter. The ARN may be unknown until apply.
- `aws.ec2.ami.kms_key`: Optional KMS key required to launch encrypted AMIs or snapshots. The wrapper's presence is the plan-time policy discriminator.
- `aws.ec2.ami.kms_key.arn`: ARN of the KMS key. The ARN may be unknown until apply.
- `aws.ec2.vpc_id`: VPC in which runner networking resources are created.
- `aws.ec2.subnet_ids`: Subnets from which scale-up may launch runner instances.
- `aws.ec2.overrides`: Optional resource-name overrides.
- `aws.ec2.overrides.name_runner`: Name tag used for runner compute resources. An empty value uses the generated provider name.
- `aws.ec2.overrides.name_sg`: Name tag used for the managed runner security group. An empty value uses the generated provider name.
- `aws.ec2.instance_profile`: Optional externally managed instance profile used by the launch template.
- `aws.ec2.instance_profile.name`: Name of the externally managed instance profile.
- `aws.ec2.instance_profile_path`: IAM path for the provider-managed instance profile. Null uses a path derived from the runner-configuration prefix.
- `aws.ec2.binaries_syncer`: Runner-distribution synchronization configuration.
- `aws.ec2.binaries_syncer.enabled`: Enables use of a synchronized runner distribution from S3.
- `aws.ec2.binaries_syncer.s3`: S3 object containing the synchronized runner distribution. Required when synchronization is enabled.
- `aws.ec2.binaries_syncer.s3.arn`: ARN of the runner-distribution bucket, used by IAM policies.
- `aws.ec2.binaries_syncer.s3.id`: Bucket name used to construct the runner-distribution S3 URI.
- `aws.ec2.binaries_syncer.s3.key`: Object key of the runner distribution.
- `aws.ec2.block_device_mappings`: EBS mappings added to the runner launch template.
- `aws.ec2.block_device_mappings[].delete_on_termination`: Deletes the volume when its runner instance terminates.
- `aws.ec2.block_device_mappings[].device_name`: Device name exposed to the runner instance.
- `aws.ec2.block_device_mappings[].encrypted`: Enables EBS encryption.
- `aws.ec2.block_device_mappings[].iops`: Provisioned IOPS for volume types that support it.
- `aws.ec2.block_device_mappings[].kms_key_id`: KMS key ID or ARN used to encrypt the volume.
- `aws.ec2.block_device_mappings[].snapshot_id`: Snapshot used to initialize the volume.
- `aws.ec2.block_device_mappings[].throughput`: Provisioned throughput for volume types that support it.
- `aws.ec2.block_device_mappings[].volume_initialization_rate`: Fixed initialization rate in MiB/s for supported snapshot-backed volumes.
- `aws.ec2.block_device_mappings[].volume_size`: Volume size in GiB.
- `aws.ec2.block_device_mappings[].volume_type`: EBS volume type.
- `aws.ec2.ebs_optimized`: Requests EBS-optimized runner instances.
- `aws.ec2.instance_target_capacity_type`: Primary capacity type, either `spot` or `on-demand`.
- `aws.ec2.instance_allocation_strategy`: EC2 Fleet allocation strategy used to select instance capacity.
- `aws.ec2.instance_type_priorities`: Optional numeric priorities keyed by instance type.
- `aws.ec2.instance_max_spot_price`: Optional maximum hourly Spot price.
- `aws.ec2.instance_types`: EC2 instance types available to the scale-up and pool functions.
- `aws.ec2.user_data`: Runner bootstrap user-data configuration.
- `aws.ec2.user_data.enabled`: Enables launch-template user data.
- `aws.ec2.user_data.template`: Optional path to a custom user-data template.
- `aws.ec2.user_data.content`: Optional complete user-data content. When set, it is used instead of rendering a template.
- `aws.ec2.user_data.pre_install`: Script content inserted before runner installation in the default template.
- `aws.ec2.user_data.post_install`: Script content inserted after runner installation in the default template.
- `aws.ec2.user_data.debug_logging_enabled`: Enables verbose user-data tracing, which can expose secrets in logs.
- `aws.ec2.ssm_enabled`: Attaches runner permissions and policies required for AWS Systems Manager access.
- `aws.ec2.create_service_linked_role_spot`: Allows scale-up to create the EC2 Spot service-linked role.
- `aws.ec2.cloudwatch_agent`: CloudWatch agent configuration for runner instances.
- `aws.ec2.cloudwatch_agent.enabled`: Installs and configures the CloudWatch agent through the default bootstrap flow.
- `aws.ec2.cloudwatch_agent.config`: Optional complete CloudWatch agent configuration. Null renders the provider default from `log_files`.
- `aws.ec2.managed_security_group_enabled`: Creates and attaches the provider-managed runner security group.
- `aws.ec2.log_files`: Optional log files collected by the CloudWatch agent. Null uses the provider defaults.
- `aws.ec2.log_files[].log_group_name`: CloudWatch log-group name, before optional prefixing.
- `aws.ec2.log_files[].prefix_log_group`: Prefixes the log-group name with the runner configuration path when true.
- `aws.ec2.log_files[].file_path`: File or glob read by the CloudWatch agent.
- `aws.ec2.log_files[].log_stream_name`: CloudWatch log-stream name template.
- `aws.ec2.log_files[].log_class`: CloudWatch log-group class for the collected file.
- `aws.ec2.key_name`: Optional EC2 key-pair name added to the launch template.
- `aws.ec2.additional_security_group_ids`: Existing security groups attached in addition to the managed security group.
- `aws.ec2.detailed_monitoring_enabled`: Enables detailed EC2 monitoring for runner instances.
- `aws.ec2.egress_rules`: Egress rules created on the managed runner security group.
- `aws.ec2.egress_rules[].cidr_blocks`: IPv4 CIDR destinations.
- `aws.ec2.egress_rules[].ipv6_cidr_blocks`: IPv6 CIDR destinations.
- `aws.ec2.egress_rules[].prefix_list_ids`: AWS prefix-list destinations.
- `aws.ec2.egress_rules[].from_port`: First destination port in the permitted range.
- `aws.ec2.egress_rules[].protocol`: IP protocol name or number. Use `-1` for all protocols.
- `aws.ec2.egress_rules[].security_groups`: Destination security-group IDs.
- `aws.ec2.egress_rules[].self`: Allows traffic to the managed security group itself when true.
- `aws.ec2.egress_rules[].to_port`: Last destination port in the permitted range.
- `aws.ec2.egress_rules[].description`: Optional rule description.
- `aws.ec2.tags`: Additional tags for runner instances, EBS volumes, network interfaces, and eligible Spot instance requests created from the launch template. They override module-level tags and the generated runner `Name`; the provider-managed `ghr:environment`, `ghr:ssm_config_path`, and `ghr:runner_name_prefix` bootstrap tags take final precedence. These tags do not apply to static provider resources such as the launch template, security group, IAM resources, SSM parameters, or log groups.
- `aws.ec2.metadata_options`: Instance Metadata Service configuration in the launch template.
- `aws.ec2.metadata_options.instance_metadata_tags`: Exposes instance tags through Instance Metadata Service when `enabled`.
- `aws.ec2.metadata_options.http_endpoint`: Enables or disables the Instance Metadata Service endpoint.
- `aws.ec2.metadata_options.http_tokens`: Controls whether IMDSv2 session tokens are optional or required.
- `aws.ec2.metadata_options.http_put_response_hop_limit`: Network hop limit for Instance Metadata Service token responses.
- `aws.ec2.credit_specification`: CPU credit mode for burstable instance types, either `standard` or `unlimited`.
- `aws.ec2.cpu_options`: CPU topology and processor-feature configuration.
- `aws.ec2.cpu_options.core_count`: Number of CPU cores exposed to the runner instance.
- `aws.ec2.cpu_options.threads_per_core`: Number of hardware threads exposed per CPU core.
- `aws.ec2.cpu_options.amd_sev_snp`: Enables or disables AMD SEV-SNP on supported instance types.
- `aws.ec2.cpu_options.nested_virtualization`: Enables or disables nested virtualization on supported instance types.
- `aws.ec2.placement`: EC2 placement configuration for runner instances.
- `aws.ec2.placement.affinity`: Host affinity setting.
- `aws.ec2.placement.availability_zone`: Availability Zone in which the instance is placed.
- `aws.ec2.placement.group_id`: Placement-group ID.
- `aws.ec2.placement.group_name`: Placement-group name.
- `aws.ec2.placement.host_id`: Dedicated Host ID.
- `aws.ec2.placement.host_resource_group_arn`: ARN of the host resource group used for placement.
- `aws.ec2.placement.spread_domain`: Spread-domain placement value.
- `aws.ec2.placement.tenancy`: Instance tenancy, such as `default`, `dedicated`, or `host`.
- `aws.ec2.placement.partition_number`: Placement-group partition number.
- `aws.ec2.license_specifications`: License Manager configurations added to the launch template.
- `aws.ec2.license_specifications[].license_configuration_arn`: ARN of a License Manager license configuration.
- `aws.ec2.associate_public_ipv4_address`: Associates a public IPv4 address with runner network interfaces.
- `aws.ec2.network_interfaces`: Advanced network interface configuration for the launch template. Leave empty to keep using `associate_public_ipv4_address` for a simple single-interface setup.
- `aws.ec2.on_demand_failover_for_errors`: EC2 error codes that trigger an on-demand fallback after a Spot launch failure.
- `aws.ec2.scale_errors`: EC2 error codes treated as retryable scale-up failures.
- `aws.ec2.use_dedicated_host`: Enables the dedicated-host launch path, required for macOS runners. |
object({
aws = optional(object({
ec2 = optional(object({
ami = optional(object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
id_ssm_parameter = optional(object({
arn = string
}), null)
kms_key = optional(object({
arn = string
}), null)
}), null)
vpc_id = string
subnet_ids = list(string)
overrides = optional(object({
name_runner = optional(string, "")
name_sg = optional(string, "")
}), {})
instance_profile = optional(object({
name = string
}), null)
instance_profile_path = optional(string, null)
binaries_syncer = optional(object({
enabled = optional(bool, true)
s3 = optional(object({
arn = string
id = string
key = string
}), null)
}), {})
block_device_mappings = optional(list(object({
delete_on_termination = optional(bool, true)
device_name = optional(string, "/dev/xvda")
encrypted = optional(bool, true)
iops = optional(number)
kms_key_id = optional(string)
snapshot_id = optional(string)
throughput = optional(number)
volume_initialization_rate = optional(number)
volume_size = number
volume_type = optional(string, "gp3")
})), [{ volume_size = 30 }])
ebs_optimized = optional(bool, false)
instance_target_capacity_type = optional(string, "spot")
instance_allocation_strategy = optional(string, "lowest-price")
instance_type_priorities = optional(map(number), null)
instance_max_spot_price = optional(string, null)
instance_types = list(string)
user_data = optional(object({
enabled = optional(bool, true)
template = optional(string, null)
content = optional(string, null)
pre_install = optional(string, "")
post_install = optional(string, "")
debug_logging_enabled = optional(bool, false)
}), {})
ssm_enabled = optional(bool, false)
create_service_linked_role_spot = optional(bool, false)
cloudwatch_agent = optional(object({
enabled = optional(bool, true)
config = optional(string, null)
}), {})
managed_security_group_enabled = optional(bool, true)
log_files = optional(list(object({
log_group_name = string
prefix_log_group = bool
file_path = string
log_stream_name = string
log_class = optional(string, "STANDARD")
})), null)
key_name = optional(string, null)
additional_security_group_ids = optional(list(string), [])
detailed_monitoring_enabled = optional(bool, false)
egress_rules = optional(list(object({
cidr_blocks = list(string)
ipv6_cidr_blocks = list(string)
prefix_list_ids = list(string)
from_port = number
protocol = string
security_groups = list(string)
self = bool
to_port = number
description = string
})), [{
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
prefix_list_ids = null
from_port = 0
protocol = "-1"
security_groups = null
self = null
to_port = 0
description = null
}])
tags = optional(map(string), {})
metadata_options = optional(object({
instance_metadata_tags = optional(string, "enabled")
http_endpoint = optional(string, "enabled")
http_tokens = optional(string, "required")
http_put_response_hop_limit = optional(number, 1)
}), {})
credit_specification = optional(string, null)
cpu_options = optional(object({
core_count = optional(number)
threads_per_core = optional(number)
amd_sev_snp = optional(string)
nested_virtualization = optional(string)
}), null)
placement = optional(object({
affinity = optional(string)
availability_zone = optional(string)
group_id = optional(string)
group_name = optional(string)
host_id = optional(string)
host_resource_group_arn = optional(string)
spread_domain = optional(string)
tenancy = optional(string)
partition_number = optional(number)
}), null)
license_specifications = optional(list(object({
license_configuration_arn = string
})), [])
associate_public_ipv4_address = optional(bool, false)
network_interfaces = optional(list(object({
associate_carrier_ip_address = optional(bool)
associate_public_ip_address = optional(bool)
delete_on_termination = optional(bool)
description = optional(string)
device_index = optional(number)
interface_type = optional(string)
ipv4_address_count = optional(number)
ipv4_addresses = optional(list(string))
ipv4_prefix_count = optional(number)
ipv4_prefixes = optional(list(string))
ipv6_address_count = optional(number)
ipv6_addresses = optional(list(string))
ipv6_prefix_count = optional(number)
ipv6_prefixes = optional(list(string))
network_card_index = optional(number)
network_interface_id = optional(string)
primary_ipv6 = optional(bool)
private_ip_address = optional(string)
security_groups = optional(list(string))
subnet_id = optional(string)
connection_tracking_specification = optional(object({
tcp_established_timeout = optional(number)
udp_stream_timeout = optional(number)
udp_timeout = optional(number)
}))
ena_srd_specification = optional(object({
ena_srd_enabled = optional(bool)
ena_srd_udp_specification = optional(object({
ena_srd_udp_enabled = optional(bool)
}))
}))
})), [])
on_demand_failover_for_errors = optional(list(string), [])
scale_errors = optional(list(string), [
"UnfulfillableCapacity",
"MaxSpotInstanceCountExceeded",
"TargetCapacityLimitExceededException",
"RequestLimitExceeded",
"ResourceLimitExceeded",
"MaxSpotInstanceCountExceeded",
"MaxSpotFleetRequestCountExceeded",
"InsufficientInstanceCapacity",
"InsufficientCapacityOnHost",
])
use_dedicated_host = optional(bool, false)
}), null)
}), {})
})
| n/a | yes | +| [compute\_provider](#input\_compute\_provider) | Typed compute-provider configuration. Provider-owned settings remain inside the selected compute-provider block.

Exactly one compute-provider block must be non-null. The populated block selects the provider, and its presence must be known during planning. Values inside the selected block may remain unknown until apply.

- `aws`: AWS compute-provider configurations.
- `aws.ec2`: EC2 compute-provider configuration.
- `aws.ec2.ami`: Optional AMI discovery or external AMI-parameter configuration. Null uses the operating-system and architecture defaults.
- `aws.ec2.ami.filter`: EC2 AMI filters combined with the provider's default AMI-name filter.
- `aws.ec2.ami.owners`: AWS account IDs or aliases allowed to own the selected AMI.
- `aws.ec2.ami.ssm_parameter`: Optional AMI-ID SSM parameter configuration. Set `arn` to use an existing parameter or `path` to create one managed by the provider.
- `aws.ec2.ami.ssm_parameter.path`: Parent path under which the provider creates the `ami_id` parameter.
- `aws.ec2.ami.ssm_parameter.arn`: ARN of an existing AMI-ID parameter. The ARN may be unknown until apply.
- `aws.ec2.ami.kms_key`: Optional KMS key required to launch encrypted AMIs or snapshots. The wrapper's presence is the plan-time policy discriminator.
- `aws.ec2.ami.kms_key.arn`: ARN of the KMS key. The ARN may be unknown until apply.
- `aws.ec2.vpc_id`: VPC in which runner networking resources are created.
- `aws.ec2.subnet_ids`: Subnets from which scale-up may launch runner instances.
- `aws.ec2.overrides`: Optional resource-name overrides.
- `aws.ec2.overrides.name_runner`: Name tag used for runner compute resources. An empty value uses the generated provider name.
- `aws.ec2.overrides.name_sg`: Name tag used for the managed runner security group. An empty value uses the generated provider name.
- `aws.ec2.instance_profile`: Optional externally managed instance profile used by the launch template.
- `aws.ec2.instance_profile.name`: Name of the externally managed instance profile.
- `aws.ec2.instance_profile_path`: IAM path for the provider-managed instance profile. Null uses a path derived from the runner-configuration prefix.
- `aws.ec2.binaries_syncer`: Runner-distribution synchronization configuration.
- `aws.ec2.binaries_syncer.enabled`: Enables use of a synchronized runner distribution from S3.
- `aws.ec2.binaries_syncer.s3`: S3 object containing the synchronized runner distribution. Required when synchronization is enabled.
- `aws.ec2.binaries_syncer.s3.arn`: ARN of the runner-distribution bucket, used by IAM policies.
- `aws.ec2.binaries_syncer.s3.id`: Bucket name used to construct the runner-distribution S3 URI.
- `aws.ec2.binaries_syncer.s3.key`: Object key of the runner distribution.
- `aws.ec2.block_device_mappings`: EBS mappings added to the runner launch template.
- `aws.ec2.block_device_mappings[].delete_on_termination`: Deletes the volume when its runner instance terminates.
- `aws.ec2.block_device_mappings[].device_name`: Device name exposed to the runner instance.
- `aws.ec2.block_device_mappings[].encrypted`: Enables EBS encryption.
- `aws.ec2.block_device_mappings[].iops`: Provisioned IOPS for volume types that support it.
- `aws.ec2.block_device_mappings[].kms_key_id`: KMS key ID or ARN used to encrypt the volume.
- `aws.ec2.block_device_mappings[].snapshot_id`: Snapshot used to initialize the volume.
- `aws.ec2.block_device_mappings[].throughput`: Provisioned throughput for volume types that support it.
- `aws.ec2.block_device_mappings[].volume_initialization_rate`: Fixed initialization rate in MiB/s for supported snapshot-backed volumes.
- `aws.ec2.block_device_mappings[].volume_size`: Volume size in GiB.
- `aws.ec2.block_device_mappings[].volume_type`: EBS volume type.
- `aws.ec2.ebs_optimized`: Requests EBS-optimized runner instances.
- `aws.ec2.instance_target_capacity_type`: Primary capacity type, either `spot` or `on-demand`.
- `aws.ec2.instance_allocation_strategy`: EC2 Fleet allocation strategy used to select instance capacity.
- `aws.ec2.instance_type_priorities`: Optional numeric priorities keyed by instance type.
- `aws.ec2.instance_max_spot_price`: Optional maximum hourly Spot price.
- `aws.ec2.instance_types`: EC2 instance types available to the scale-up and pool functions.
- `aws.ec2.user_data`: Runner bootstrap user-data configuration.
- `aws.ec2.user_data.enabled`: Enables launch-template user data.
- `aws.ec2.user_data.template`: Optional path to a custom user-data template.
- `aws.ec2.user_data.content`: Optional complete user-data content. When set, it is used instead of rendering a template.
- `aws.ec2.user_data.pre_install`: Script content inserted before runner installation in the default template.
- `aws.ec2.user_data.post_install`: Script content inserted after runner installation in the default template.
- `aws.ec2.user_data.debug_logging_enabled`: Enables verbose user-data tracing, which can expose secrets in logs.
- `aws.ec2.ssm_enabled`: Attaches runner permissions and policies required for AWS Systems Manager access.
- `aws.ec2.create_service_linked_role_spot`: Allows scale-up to create the EC2 Spot service-linked role.
- `aws.ec2.cloudwatch_agent`: CloudWatch agent configuration for runner instances.
- `aws.ec2.cloudwatch_agent.enabled`: Installs and configures the CloudWatch agent through the default bootstrap flow.
- `aws.ec2.cloudwatch_agent.config`: Optional complete CloudWatch agent configuration. Null renders the provider default from `log_files`.
- `aws.ec2.managed_security_group_enabled`: Creates and attaches the provider-managed runner security group.
- `aws.ec2.log_files`: Optional log files collected by the CloudWatch agent. Null uses the provider defaults.
- `aws.ec2.log_files[].log_group_name`: CloudWatch log-group name, before optional prefixing.
- `aws.ec2.log_files[].prefix_log_group`: Prefixes the log-group name with the runner configuration path when true.
- `aws.ec2.log_files[].file_path`: File or glob read by the CloudWatch agent.
- `aws.ec2.log_files[].log_stream_name`: CloudWatch log-stream name template.
- `aws.ec2.log_files[].log_class`: CloudWatch log-group class for the collected file.
- `aws.ec2.key_name`: Optional EC2 key-pair name added to the launch template.
- `aws.ec2.additional_security_group_ids`: Existing security groups attached in addition to the managed security group.
- `aws.ec2.detailed_monitoring_enabled`: Enables detailed EC2 monitoring for runner instances.
- `aws.ec2.egress_rules`: Egress rules created on the managed runner security group.
- `aws.ec2.egress_rules[].cidr_blocks`: IPv4 CIDR destinations.
- `aws.ec2.egress_rules[].ipv6_cidr_blocks`: IPv6 CIDR destinations.
- `aws.ec2.egress_rules[].prefix_list_ids`: AWS prefix-list destinations.
- `aws.ec2.egress_rules[].from_port`: First destination port in the permitted range.
- `aws.ec2.egress_rules[].protocol`: IP protocol name or number. Use `-1` for all protocols.
- `aws.ec2.egress_rules[].security_groups`: Destination security-group IDs.
- `aws.ec2.egress_rules[].self`: Allows traffic to the managed security group itself when true.
- `aws.ec2.egress_rules[].to_port`: Last destination port in the permitted range.
- `aws.ec2.egress_rules[].description`: Optional rule description.
- `aws.ec2.tags`: Additional tags for runner instances, EBS volumes, network interfaces, and eligible Spot instance requests created from the launch template. They override module-level tags and the generated runner `Name`; the provider-managed `ghr:environment`, `ghr:ssm_config_path`, and `ghr:runner_name_prefix` bootstrap tags take final precedence. These tags do not apply to static provider resources such as the launch template, security group, IAM resources, SSM parameters, or log groups.
- `aws.ec2.metadata_options`: Instance Metadata Service configuration in the launch template.
- `aws.ec2.metadata_options.instance_metadata_tags`: Exposes instance tags through Instance Metadata Service when `enabled`.
- `aws.ec2.metadata_options.http_endpoint`: Enables or disables the Instance Metadata Service endpoint.
- `aws.ec2.metadata_options.http_tokens`: Controls whether IMDSv2 session tokens are optional or required.
- `aws.ec2.metadata_options.http_put_response_hop_limit`: Network hop limit for Instance Metadata Service token responses.
- `aws.ec2.credit_specification`: CPU credit mode for burstable instance types, either `standard` or `unlimited`.
- `aws.ec2.cpu_options`: CPU topology and processor-feature configuration.
- `aws.ec2.cpu_options.core_count`: Number of CPU cores exposed to the runner instance.
- `aws.ec2.cpu_options.threads_per_core`: Number of hardware threads exposed per CPU core.
- `aws.ec2.cpu_options.amd_sev_snp`: Enables or disables AMD SEV-SNP on supported instance types.
- `aws.ec2.cpu_options.nested_virtualization`: Enables or disables nested virtualization on supported instance types.
- `aws.ec2.placement`: EC2 placement configuration for runner instances.
- `aws.ec2.placement.affinity`: Host affinity setting.
- `aws.ec2.placement.availability_zone`: Availability Zone in which the instance is placed.
- `aws.ec2.placement.group_id`: Placement-group ID.
- `aws.ec2.placement.group_name`: Placement-group name.
- `aws.ec2.placement.host_id`: Dedicated Host ID.
- `aws.ec2.placement.host_resource_group_arn`: ARN of the host resource group used for placement.
- `aws.ec2.placement.spread_domain`: Spread-domain placement value.
- `aws.ec2.placement.tenancy`: Instance tenancy, such as `default`, `dedicated`, or `host`.
- `aws.ec2.placement.partition_number`: Placement-group partition number.
- `aws.ec2.license_specifications`: License Manager configurations added to the launch template.
- `aws.ec2.license_specifications[].license_configuration_arn`: ARN of a License Manager license configuration.
- `aws.ec2.associate_public_ipv4_address`: Associates a public IPv4 address with runner network interfaces.
- `aws.ec2.network_interfaces`: Advanced network interface configuration for the launch template. Leave empty to keep using `associate_public_ipv4_address` for a simple single-interface setup.
- `aws.ec2.on_demand_failover_for_errors`: EC2 error codes that trigger an on-demand fallback after a Spot launch failure.
- `aws.ec2.scale_errors`: EC2 error codes treated as retryable scale-up failures.
- `aws.ec2.use_dedicated_host`: Enables the dedicated-host launch path, required for macOS runners. |
object({
aws = optional(object({
ec2 = optional(object({
ami = optional(object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
ssm_parameter = optional(object({
path = optional(string, null)
arn = optional(string, null)
}), null)
kms_key = optional(object({
arn = string
}), null)
}), null)
vpc_id = string
subnet_ids = list(string)
overrides = optional(object({
name_runner = optional(string, "")
name_sg = optional(string, "")
}), {})
instance_profile = optional(object({
name = string
}), null)
instance_profile_path = optional(string, null)
binaries_syncer = optional(object({
enabled = optional(bool, true)
s3 = optional(object({
arn = string
id = string
key = string
}), null)
}), {})
block_device_mappings = optional(list(object({
delete_on_termination = optional(bool, true)
device_name = optional(string, "/dev/xvda")
encrypted = optional(bool, true)
iops = optional(number)
kms_key_id = optional(string)
snapshot_id = optional(string)
throughput = optional(number)
volume_initialization_rate = optional(number)
volume_size = number
volume_type = optional(string, "gp3")
})), [{ volume_size = 30 }])
ebs_optimized = optional(bool, false)
instance_target_capacity_type = optional(string, "spot")
instance_allocation_strategy = optional(string, "lowest-price")
instance_type_priorities = optional(map(number), null)
instance_max_spot_price = optional(string, null)
instance_types = list(string)
user_data = optional(object({
enabled = optional(bool, true)
template = optional(string, null)
content = optional(string, null)
pre_install = optional(string, "")
post_install = optional(string, "")
debug_logging_enabled = optional(bool, false)
}), {})
ssm_enabled = optional(bool, false)
create_service_linked_role_spot = optional(bool, false)
cloudwatch_agent = optional(object({
enabled = optional(bool, true)
config = optional(string, null)
}), {})
managed_security_group_enabled = optional(bool, true)
log_files = optional(list(object({
log_group_name = string
prefix_log_group = bool
file_path = string
log_stream_name = string
log_class = optional(string, "STANDARD")
})), null)
key_name = optional(string, null)
additional_security_group_ids = optional(list(string), [])
detailed_monitoring_enabled = optional(bool, false)
egress_rules = optional(list(object({
cidr_blocks = list(string)
ipv6_cidr_blocks = list(string)
prefix_list_ids = list(string)
from_port = number
protocol = string
security_groups = list(string)
self = bool
to_port = number
description = string
})), [{
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
prefix_list_ids = null
from_port = 0
protocol = "-1"
security_groups = null
self = null
to_port = 0
description = null
}])
tags = optional(map(string), {})
metadata_options = optional(object({
instance_metadata_tags = optional(string, "enabled")
http_endpoint = optional(string, "enabled")
http_tokens = optional(string, "required")
http_put_response_hop_limit = optional(number, 1)
}), {})
credit_specification = optional(string, null)
cpu_options = optional(object({
core_count = optional(number)
threads_per_core = optional(number)
amd_sev_snp = optional(string)
nested_virtualization = optional(string)
}), null)
placement = optional(object({
affinity = optional(string)
availability_zone = optional(string)
group_id = optional(string)
group_name = optional(string)
host_id = optional(string)
host_resource_group_arn = optional(string)
spread_domain = optional(string)
tenancy = optional(string)
partition_number = optional(number)
}), null)
license_specifications = optional(list(object({
license_configuration_arn = string
})), [])
associate_public_ipv4_address = optional(bool, false)
network_interfaces = optional(list(object({
associate_carrier_ip_address = optional(bool)
associate_public_ip_address = optional(bool)
delete_on_termination = optional(bool)
description = optional(string)
device_index = optional(number)
interface_type = optional(string)
ipv4_address_count = optional(number)
ipv4_addresses = optional(list(string))
ipv4_prefix_count = optional(number)
ipv4_prefixes = optional(list(string))
ipv6_address_count = optional(number)
ipv6_addresses = optional(list(string))
ipv6_prefix_count = optional(number)
ipv6_prefixes = optional(list(string))
network_card_index = optional(number)
network_interface_id = optional(string)
primary_ipv6 = optional(bool)
private_ip_address = optional(string)
security_groups = optional(list(string))
subnet_id = optional(string)
connection_tracking_specification = optional(object({
tcp_established_timeout = optional(number)
udp_stream_timeout = optional(number)
udp_timeout = optional(number)
}))
ena_srd_specification = optional(object({
ena_srd_enabled = optional(bool)
ena_srd_udp_specification = optional(object({
ena_srd_udp_enabled = optional(bool)
}))
}))
})), [])
on_demand_failover_for_errors = optional(list(string), [])
scale_errors = optional(list(string), [
"UnfulfillableCapacity",
"MaxSpotInstanceCountExceeded",
"TargetCapacityLimitExceededException",
"RequestLimitExceeded",
"ResourceLimitExceeded",
"MaxSpotInstanceCountExceeded",
"MaxSpotFleetRequestCountExceeded",
"InsufficientInstanceCapacity",
"InsufficientCapacityOnHost",
])
use_dedicated_host = optional(bool, false)
}), null)
}), {})
})
| n/a | yes | | [compute\_provider\_key](#input\_compute\_provider\_key) | Optional plan-known compute-provider dispatch key. Null discovers the key from the exactly one populated compute\_provider block. | `string` | `null` | no | | [github](#input\_github) | GitHub API and runner-registration configuration.

- `app_parameters.key_base64`: Parameter Store reference for the primary GitHub App private key.
- `app_parameters.id`: Parameter Store reference for the primary GitHub App ID.
- `app_parameters.additional_apps_manifest`: Optional Parameter Store reference containing the additional GitHub App manifest.
- `app_parameters.additional_app_parameter_arns`: ARNs of the additional GitHub App credential parameters.
- `enterprise_server.url`: Optional GitHub Enterprise Server base URL. Null selects GitHub.com.
- `enterprise_server.ssl_verify`: Enables TLS certificate verification for GitHub Enterprise Server requests.
- `user_agent`: Optional User-Agent value added to GitHub API requests. |
object({
app_parameters = object({
key_base64 = map(string)
id = map(string)
additional_apps_manifest = optional(object({
name = string
arn = string
}), null)
additional_app_parameter_arns = optional(list(string), [])
})
enterprise_server = optional(object({
url = optional(string, null)
ssl_verify = optional(bool, true)
}), {})
user_agent = optional(string, null)
})
| n/a | yes | | [lambda](#input\_lambda) | Common Lambda substrate independent of the selected runner orchestration provider.

- `artifact.s3.bucket`: Optional shared S3 bucket containing component-owned Lambda artifacts. An orchestration provider selects its own object key and version; the bucket alone selects no artifact.
- `runtime`: Runtime used by the control-plane Lambda functions.
- `architecture`: Instruction-set architecture used by the control-plane Lambda functions. Supported values are `arm64` and `x86_64`.
- `subnet_ids`: Subnets used for Lambda VPC configuration.
- `security_group_ids`: Security groups used for Lambda VPC configuration.
- `tags`: Shared tags applied to Lambda function resources only. These override module-level `tags`; component `tags` override this map when keys conflict.
- `principals`: Additional principals allowed to assume the control-plane Lambda roles.
- `role.path`: IAM path for module-managed Lambda execution roles. Defaults to a path derived from `prefix`.
- `role.permissions_boundary`: Permissions-boundary ARN applied to module-managed Lambda execution roles. |
object({
artifact = optional(object({
s3 = optional(object({
bucket = optional(string, null)
}), {})
}), {})
runtime = optional(string, "nodejs24.x")
architecture = optional(string, "arm64")
subnet_ids = optional(list(string), [])
security_group_ids = optional(list(string), [])
tags = optional(map(string), {})
principals = optional(list(object({
type = string
identifiers = list(string)
})), [])
role = optional(object({
path = optional(string, null)
permissions_boundary = optional(string, null)
}), {})
})
| `{}` | no | @@ -117,7 +117,7 @@ yarn run dist | [orchestration\_provider](#input\_orchestration\_provider) | Runner demand-orchestration provider configuration. Exactly one provider block must be non-null. Wrapper presence selects the provider and must therefore be known during planning; values inside the selected provider may remain unknown until apply.

- `webhook`: Selects the workflow-job webhook control plane. It owns runner lifecycle and capacity, the build queue reference, the runner-control artifact, scale-up, scale-down, scheduled pool, and optional job-retry controls. Future providers can be added as sibling blocks without moving this contract.
- `webhook.runner`: Runner lifecycle, boot timeout, and capacity settings owned by webhook orchestration.
- `webhook.runner.boot_time_in_minutes`: Expected runner boot duration used by scale-down and pool controls. The default is `5`.
- `webhook.runner.ephemeral`: Registers runners in ephemeral mode. The default is `false`.
- `webhook.runner.jit_config_enabled`: Explicitly enables or disables just-in-time configuration. The default is null, which follows `runner.ephemeral`.
- `webhook.runner.maximum_count`: Maximum number of runners managed for this runner configuration. The default is `3`.
- `webhook.github.organization_runners`: Registers runners at organization scope when true; otherwise registration is repository-scoped.
- `webhook.queue.build.arn`: ARN of the runner configuration's build queue.
- `webhook.queue.build.url`: URL of the runner configuration's build queue.
- `webhook.queue.kms_key_id`: Optional KMS key ARN encrypting the build queue. The default is null and is independent from the Parameter Store KMS key.
- `webhook.queue.tags`: Tags inherited by queue-related provider resources before component-specific overrides. The default is `{}`.
- `webhook.lambda.artifact`: Runner-control artifact shared by scale, pool, and job-retry components. Set at most one of `zip` or `s3`; no selection uses the packaged runner archive.
- `webhook.lambda.artifact.zip`: Optional local path to the runner-control Lambda archive. The default is null.
- `webhook.lambda.artifact.s3`: Optional S3 object selector in the common `lambda.artifact.s3.bucket`. Wrapper presence must be known during planning, selecting it requires a non-null common bucket, and the default is null.
- `webhook.lambda.artifact.s3.key`: Object key of the runner-control Lambda archive.
- `webhook.lambda.artifact.s3.object_version`: Optional object version of the runner-control Lambda archive. The default is null.
- `webhook.lambda.scale.up.memory_size`: Memory allocated to the scale-up Lambda in MB. The default is `512`.
- `webhook.lambda.scale.up.timeout`: Scale-up Lambda timeout in seconds. The default is `60`.
- `webhook.lambda.scale.up.reserved_concurrent_executions`: Reserved concurrency for scale-up. The default is `1`; use `-1` for unreserved concurrency.
- `webhook.lambda.scale.up.job_queued_check_enabled`: Enables queued-job verification before scaling. The default is null, which follows the resolved runner mode.
- `webhook.lambda.scale.up.event_source_mapping.batch_size`: Maximum build-queue records delivered per scale-up invocation. The default is `10`.
- `webhook.lambda.scale.up.event_source_mapping.maximum_batching_window_in_seconds`: Maximum batching window for build-queue records. The default is `0`.
- `webhook.lambda.scale.up.tags`: Tags applied within scale-up resource scopes after common provider tags. The default is `{}`.
- `webhook.lambda.scale.down.memory_size`: Memory allocated to the scale-down Lambda in MB. The default is `512`.
- `webhook.lambda.scale.down.timeout`: Scale-down Lambda timeout in seconds. The default is `60`.
- `webhook.lambda.scale.down.schedule_expression`: EventBridge schedule expression that invokes scale-down. The default is `cron(*/5 * * * ? *)`.
- `webhook.lambda.scale.down.minimum_running_time_in_minutes`: Optional minimum runner age before scale-down may terminate it. The default is null, which selects the operating-system default.
- `webhook.lambda.scale.down.idle_confirmation_seconds`: Number of seconds a runner must consistently report not-busy before scale-down terminates it. The default is `0`, which preserves the single-reading behavior.
- `webhook.lambda.scale.down.idle_config`: Time-based desired idle-runner configurations. The default is `[]`.
- `webhook.lambda.scale.down.idle_config[].cron`: Cron expression identifying when the idle configuration applies.
- `webhook.lambda.scale.down.idle_config[].timeZone`: IANA time zone used to evaluate the cron expression.
- `webhook.lambda.scale.down.idle_config[].idleCount`: Number of idle runners retained during the matching period.
- `webhook.lambda.scale.down.idle_config[].evictionStrategy`: Selection strategy used when excess idle runners are removed. The default is `oldest_first`.
- `webhook.lambda.scale.down.tags`: Tags applied within scale-down resource scopes after common provider tags. The default is `{}`.
- `webhook.lambda.pool.memory_size`: Memory allocated to the pool Lambda in MB. The default is `512`.
- `webhook.lambda.pool.timeout`: Pool Lambda timeout in seconds. The default is `60`.
- `webhook.lambda.pool.reserved_concurrent_executions`: Reserved concurrency for the pool Lambda. The default is `1`; use `-1` for unreserved concurrency.
- `webhook.lambda.pool.config`: Scheduled target pool sizes. The default is `[]`, which disables the pool component.
- `webhook.lambda.pool.config[].schedule_expression`: Scheduler expression that activates the target size.
- `webhook.lambda.pool.config[].schedule_expression_timezone`: Optional IANA time zone used to evaluate the schedule.
- `webhook.lambda.pool.config[].size`: Desired number of runners for the schedule.
- `webhook.lambda.pool.include_busy_runners`: Includes busy runners when reconciling scheduled pool capacity. The default is `false`.
- `webhook.lambda.pool.runner_owner`: Optional GitHub organization or repository owner used for pooled runners. The default is null.
- `webhook.lambda.pool.tags`: Tags applied within pool resource scopes after common provider tags. The default is `{}`.
- `webhook.job_retry.enabled`: Creates the retry queue, Lambda function, event-source mapping, and related IAM resources. The default is `false`.
- `webhook.job_retry.delay_in_seconds`: Initial delay before a queued-job retry check. The default is `300`.
- `webhook.job_retry.delay_backoff`: Multiplier applied to the delay after each unsuccessful check. The default is `2`.
- `webhook.job_retry.max_attempts`: Maximum retry-check attempts before the message is no longer republished. The default is `1`.
- `webhook.job_retry.tags`: Tags applied within job-retry resource scopes after common provider tags. The default is `{}`.
- `webhook.job_retry.lambda.memory_size`: Memory allocated to the job-retry Lambda in MB. The default is `256`.
- `webhook.job_retry.lambda.reserved_concurrent_executions`: Reserved concurrency for job retry. The default is `1`; use `-1` for unreserved concurrency.
- `webhook.job_retry.lambda.timeout`: Job-retry Lambda timeout in seconds and visibility timeout for its retry queue. The default is `30`. |
object({
webhook = optional(object({
runner = optional(object({
boot_time_in_minutes = optional(number, 5)
ephemeral = optional(bool, false)
jit_config_enabled = optional(bool, null)
maximum_count = optional(number, 3)
}), {})
github = object({
organization_runners = bool
})
queue = object({
build = object({
arn = string
url = string
})
kms_key_id = optional(string, null)
tags = optional(map(string), {})
})
lambda = optional(object({
artifact = optional(object({
zip = optional(string, null)
s3 = optional(object({
key = string
object_version = optional(string, null)
}), null)
}), {})
scale = optional(object({
up = optional(object({
memory_size = optional(number, 512)
timeout = optional(number, 60)
reserved_concurrent_executions = optional(number, 1)
job_queued_check_enabled = optional(bool, null)
event_source_mapping = optional(object({
batch_size = optional(number, 10)
maximum_batching_window_in_seconds = optional(number, 0)
}), {})
tags = optional(map(string), {})
}), {})
down = optional(object({
memory_size = optional(number, 512)
timeout = optional(number, 60)
schedule_expression = optional(string, "cron(*/5 * * * ? *)")
minimum_running_time_in_minutes = optional(number, null)
idle_confirmation_seconds = optional(number, 0)
idle_config = optional(list(object({
cron = string
timeZone = string
idleCount = number
evictionStrategy = optional(string, "oldest_first")
})), [])
tags = optional(map(string), {})
}), {})
}), {})
pool = optional(object({
memory_size = optional(number, 512)
timeout = optional(number, 60)
reserved_concurrent_executions = optional(number, 1)
config = optional(list(object({
schedule_expression = string
schedule_expression_timezone = optional(string)
size = number
})), [])
include_busy_runners = optional(bool, false)
runner_owner = optional(string, null)
tags = optional(map(string), {})
}), {})
}), {})
job_retry = optional(object({
enabled = optional(bool, false)
delay_in_seconds = optional(number, 300)
delay_backoff = optional(number, 2)
max_attempts = optional(number, 1)
tags = optional(map(string), {})
lambda = optional(object({
memory_size = optional(number, 256)
reserved_concurrent_executions = optional(number, 1)
timeout = optional(number, 30)
}), {})
}), {})
}), null)
})
| n/a | yes | | [prefix](#input\_prefix) | The prefix used for naming resources. | `string` | `"github-actions"` | no | | [runner](#input\_runner) | Provider-neutral GitHub runner configuration.

- `os`: Runner operating system. Supported values are `linux`, `osx`, and `windows`.
- `architecture`: Runner distribution architecture, such as `x64` or `arm64`.
- `disable_default_labels`: Prevents GitHub's default self-hosted, operating-system, and architecture labels from being registered.
- `labels`: Complete set of labels supplied to the control-plane functions.
- `group_name`: GitHub runner group used during registration.
- `name_prefix`: Prefix added to registered runner names.
- `run_as_root`: Runs the runner service as root when supported by the compute provider.
- `run_as`: Operating-system user used when `run_as_root` is false.
- `auto_update_disabled`: Disables the GitHub runner application's built-in updater.
- `tags`: Additional tags for common runner resources, currently the managed runner IAM role. These override module-level `tags` with the same key.
- `hooks.job_started`: Script content installed as the runner job-started hook.
- `hooks.job_completed`: Script content installed as the runner job-completed hook.
- `iam.role.arn`: ARN of an externally managed runner role. When set, this module does not create or modify that role.
- `iam.managed_policy_arns`: Named managed-policy ARNs attached to the module-managed runner role.
- `iam.additional_trust_policy_json`: Optional IAM policy document merged with the selected compute provider's default runner-role trust policy.
- `iam.path`: IAM path for the module-managed runner role. Defaults to a path derived from `prefix`.
- `iam.permissions_boundary`: Permissions-boundary ARN for the module-managed runner role. |
object({
os = optional(string, "linux")
architecture = optional(string, "x64")
disable_default_labels = optional(bool, false)
labels = list(string)
group_name = optional(string, "Default")
name_prefix = optional(string, "")
run_as_root = optional(bool, false)
run_as = optional(string, "ec2-user")
auto_update_disabled = optional(bool, false)
tags = optional(map(string), {})
hooks = optional(object({
job_started = optional(string, "")
job_completed = optional(string, "")
}), {})
iam = optional(object({
role = optional(object({
arn = string
}), null)
managed_policy_arns = optional(map(string), {})
additional_trust_policy_json = optional(string, null)
path = optional(string, null)
permissions_boundary = optional(string, null)
}), {})
})
| n/a | yes | -| [storage\_provider](#input\_storage\_provider) | Parameter Store paths, encryption, tag scopes, and housekeeper configuration.

- `storage_provider.aws.ssm.paths.root`: Root Parameter Store path for this runner configuration.
- `storage_provider.aws.ssm.paths.tokens`: Path segment under `paths.root` used for registration tokens and just-in-time configuration.
- `storage_provider.aws.ssm.paths.config`: Path segment under `paths.root` used for persistent runner configuration.
- `storage_provider.aws.ssm.kms_key_id`: Optional customer-managed KMS key ARN used by control-plane IAM policies to decrypt shared GitHub App parameters. The ARN may be unknown until apply; null omits the provider-owned KMS statements. It does not select encryption for runtime-created runner parameters.
- `storage_provider.aws.ssm.tags`: Shared tags for SSM-related resources. These override module-level `tags` and are inherited by parameter and housekeeper resources.
- `storage_provider.aws.ssm.parameters.tags`: Tags for Terraform-managed runner configuration parameters and temporary parameters created by the scale-up and pool Lambdas. These override module-level and `storage_provider.aws.ssm.tags` values with the same key.
- `storage_provider.aws.ssm.housekeeper.schedule_expression`: EventBridge schedule expression that invokes the SSM housekeeper.
- `storage_provider.aws.ssm.housekeeper.state`: EventBridge rule state, such as `ENABLED` or `DISABLED`.
- `storage_provider.aws.ssm.housekeeper.tags`: Tags for housekeeper resources, including the Lambda function, log group, EventBridge rule, and IAM role. These override module-level, `storage_provider.aws.ssm.tags`, shared Lambda, and shared log tags when keys conflict.
- `storage_provider.aws.ssm.housekeeper.lambda.artifact`: Component-owned SSM-housekeeper artifact selection. Set at most one of `zip` or `s3`; when neither is selected, the module uses its packaged runner control-plane archive. This selector does not inherit an orchestration-provider artifact.
- `storage_provider.aws.ssm.housekeeper.lambda.artifact.zip`: Optional local path to the SSM-housekeeper Lambda archive.
- `storage_provider.aws.ssm.housekeeper.lambda.artifact.s3`: Optional object key and version in the shared `lambda.artifact.s3.bucket`. Selecting S3 requires that common bucket.
- `storage_provider.aws.ssm.housekeeper.lambda.artifact.s3.key`: Object key of the SSM-housekeeper Lambda archive.
- `storage_provider.aws.ssm.housekeeper.lambda.artifact.s3.object_version`: Optional object version of the SSM-housekeeper Lambda archive.
- `storage_provider.aws.ssm.housekeeper.lambda.memory_size`: Memory allocated to the SSM housekeeper Lambda in MB.
- `storage_provider.aws.ssm.housekeeper.lambda.timeout`: SSM housekeeper Lambda timeout in seconds.
- `storage_provider.aws.ssm.housekeeper.config.tokenPath`: Parameter Store token path cleaned by the housekeeper. When omitted, the configured runner token path is used.
- `storage_provider.aws.ssm.housekeeper.config.minimumDaysOld`: Minimum parameter age in days before deletion is allowed.
- `storage_provider.aws.ssm.housekeeper.config.dryRun`: Reports eligible parameters without deleting them when true. |
object({
aws = object({
ssm = optional(object({
paths = object({
root = string
tokens = string
config = string
})
kms_key_id = optional(string, null)
tags = optional(map(string), {})
parameters = optional(object({
tags = optional(map(string), {})
}), {})
housekeeper = optional(object({
schedule_expression = optional(string, "rate(1 day)")
state = optional(string, "ENABLED")
tags = optional(map(string), {})
lambda = optional(object({
artifact = optional(object({
zip = optional(string, null)
s3 = optional(object({
key = string
object_version = optional(string, null)
}), null)
}), {})
memory_size = optional(number, 512)
timeout = optional(number, 60)
}), {})
config = optional(object({
tokenPath = optional(string)
minimumDaysOld = optional(number, 1)
dryRun = optional(bool, false)
}), {})
}), {})
}), null)
})
scale_up = optional(object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
}), {
environment_variables = {}
iam_policy_json = null
})
scale_down = optional(object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
}), {
environment_variables = {}
iam_policy_json = null
})
pool = optional(object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
}), {
environment_variables = {}
iam_policy_json = null
})
job_retry = optional(object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
}), {
environment_variables = {}
iam_policy_json = null
})
})
| n/a | yes | +| [storage\_provider](#input\_storage\_provider) | Parameter Store paths, encryption, tag scopes, and housekeeper configuration.

- `storage_provider.aws.ssm.paths.root`: Root Parameter Store path for this runner configuration.
- `storage_provider.aws.ssm.paths.tokens`: Path segment under `paths.root` used for registration tokens and just-in-time configuration.
- `storage_provider.aws.ssm.paths.config`: Path segment under `paths.root` used for persistent runner configuration.
- `storage_provider.aws.ssm.kms_key_id`: Optional customer-managed KMS key ARN used by control-plane IAM policies to decrypt shared GitHub App parameters. The ARN may be unknown until apply; null omits the provider-owned KMS statements. It does not select encryption for runtime-created runner parameters.
- `storage_provider.aws.ssm.tags`: Shared tags for SSM-related resources. These override module-level `tags` and are inherited by parameter and housekeeper resources.
- `storage_provider.aws.ssm.parameters.tags`: Tags for Terraform-managed runner configuration parameters and temporary parameters created by the scale-up and pool Lambdas. These override module-level and `storage_provider.aws.ssm.tags` values with the same key.
- `storage_provider.aws.ssm.housekeeper.schedule_expression`: EventBridge schedule expression that invokes the SSM housekeeper.
- `storage_provider.aws.ssm.housekeeper.state`: EventBridge rule state, such as `ENABLED` or `DISABLED`.
- `storage_provider.aws.ssm.housekeeper.tags`: Tags for housekeeper resources, including the Lambda function, log group, EventBridge rule, and IAM role. These override module-level, `storage_provider.aws.ssm.tags`, shared Lambda, and shared log tags when keys conflict.
- `storage_provider.aws.ssm.housekeeper.lambda.artifact`: Component-owned SSM-housekeeper artifact selection. Set at most one of `zip` or `s3`; when neither is selected, the module uses its packaged runner control-plane archive. This selector does not inherit an orchestration-provider artifact.
- `storage_provider.aws.ssm.housekeeper.lambda.artifact.zip`: Optional local path to the SSM-housekeeper Lambda archive.
- `storage_provider.aws.ssm.housekeeper.lambda.artifact.s3`: Optional object key and version in the shared `lambda.artifact.s3.bucket`. Selecting S3 requires that common bucket.
- `storage_provider.aws.ssm.housekeeper.lambda.artifact.s3.key`: Object key of the SSM-housekeeper Lambda archive.
- `storage_provider.aws.ssm.housekeeper.lambda.artifact.s3.object_version`: Optional object version of the SSM-housekeeper Lambda archive.
- `storage_provider.aws.ssm.housekeeper.lambda.memory_size`: Memory allocated to the SSM housekeeper Lambda in MB.
- `storage_provider.aws.ssm.housekeeper.lambda.timeout`: SSM housekeeper Lambda timeout in seconds.
- `storage_provider.aws.ssm.housekeeper.config.tokenPath`: Parameter Store token path cleaned by the housekeeper. When omitted, the configured runner token path is used.
- `storage_provider.aws.ssm.housekeeper.config.minimumDaysOld`: Minimum parameter age in days before deletion is allowed.
- `storage_provider.aws.ssm.housekeeper.config.dryRun`: Reports eligible parameters without deleting them when true. |
object({
aws = object({
ssm = optional(object({
paths = object({
root = string
tokens = string
config = string
})
kms_key_id = optional(string, null)
tags = optional(map(string), {})
parameters = optional(object({
tags = optional(map(string), {})
}), {})
housekeeper = optional(object({
schedule_expression = optional(string, "rate(1 day)")
state = optional(string, "ENABLED")
tags = optional(map(string), {})
lambda = optional(object({
artifact = optional(object({
zip = optional(string, null)
s3 = optional(object({
key = string
object_version = optional(string, null)
}), null)
}), {})
memory_size = optional(number, 512)
timeout = optional(number, 60)
}), {})
config = optional(object({
tokenPath = optional(string)
minimumDaysOld = optional(number, 1)
dryRun = optional(bool, false)
}), {})
}), {})
}), null)
})
})
| n/a | yes | | [tags](#input\_tags) | Base tags added to taggable resources created by this runner configuration. Shared, component, and compute-provider tag maps override matching keys within their documented resource scopes. | `map(string)` | `{}` | no | ## Outputs diff --git a/modules/webhook/README.md b/modules/webhook/README.md index cc2a42f204..3a31d212be 100644 --- a/modules/webhook/README.md +++ b/modules/webhook/README.md @@ -89,7 +89,7 @@ yarn run dist | [role\_path](#input\_role\_path) | The path that will be added to the role; if not set, the environment name will be used. | `string` | `null` | no | | [role\_permissions\_boundary](#input\_role\_permissions\_boundary) | Permissions boundary that will be added to the created role for the lambda. | `string` | `null` | no | | [runner\_matcher\_config](#input\_runner\_matcher\_config) | SQS queue to publish accepted build events based on the runner type. `computeProvider` defaults to `ec2`; EC2 is the only provider currently implemented. When exact match is disabled the webhook accepts the event if one of the workflow job labels is part of the matcher. The priority defines the order the matchers are applied. Optional `matcherConfig.enableDynamicLabels` and `matcherConfig.awsDynamicLabelsPolicy` are evaluated by the dispatcher to gate provider dynamic labels per runner. The policy supports `allowed_keys = []`, `blocked_keys = []` (cannot be used together with `allowed_keys`), and `restricted_keys = { = { allowed = [globs], denied = [globs], max = number|string } }`; keys use the provider dynamic label suffix form, for example `instance-type` for `ghr-ec2-instance-type`. |
map(object({
arn = string
id = string
computeProvider = optional(string, "ec2")
matcherConfig = object({
labelMatchers = list(list(string))
exactMatch = bool
bidirectionalLabelMatch = optional(bool, false)
priority = optional(number, 999)
enableDynamicLabels = optional(bool, false)
awsDynamicLabelsPolicy = optional(object({
allowed_keys = optional(list(string), [])
blocked_keys = optional(list(string), [])
restricted_keys = optional(map(object({
allowed = optional(list(string), [])
denied = optional(list(string), [])
max = optional(string, null)
})), {})
}), null)
})
}))
| n/a | yes | -| [storage\_provider](#input\_storage\_provider) | Resolved storage-provider marker and provider-owned webhook capabilities. |
object({
aws = object({
ssm = optional(object({
paths = object({
root = string
webhook = string
})
}), null)
})
direct = object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
})
eventbridge = object({
webhook = object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
})
dispatcher = object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
})
})
})
| n/a | yes | +| [storage\_provider](#input\_storage\_provider) | Storage-provider configuration used by the webhook resources. |
object({
aws = object({
ssm = optional(object({
kms_key_id = optional(string, null)
paths = object({
root = string
webhook = string
})
}), null)
})
})
| n/a | yes | | [tags](#input\_tags) | Map of tags that will be added to created resources. By default resources will be tagged with name and environment. | `map(string)` | `{}` | no | | [tracing\_config](#input\_tracing\_config) | Configuration for lambda tracing. |
object({
mode = optional(string, null)
capture_http_requests = optional(bool, false)
capture_error = optional(bool, false)
})
| `{}` | no | | [webhook\_lambda\_apigateway\_access\_log\_settings](#input\_webhook\_lambda\_apigateway\_access\_log\_settings) | Access log settings for webhook API gateway. |
object({
destination_arn = string
format = string
})
| `null` | no | diff --git a/modules/webhook/direct/README.md b/modules/webhook/direct/README.md index 1e4240b855..bd179ca2a1 100644 --- a/modules/webhook/direct/README.md +++ b/modules/webhook/direct/README.md @@ -40,7 +40,7 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [config](#input\_config) | Configuration object for all variables. |
object({
prefix = string
archive = optional(object({
enable = optional(bool, true)
retention_days = optional(number, 7)
}), {})
tags = optional(map(string), {})

lambda_subnet_ids = optional(list(string), [])
lambda_security_group_ids = optional(list(string), [])
sqs_job_queues_arns = list(string)
lambda_zip = optional(string, null)
lambda_memory_size = optional(number, 256)
lambda_timeout = optional(number, 10)
role_permissions_boundary = optional(string, null)
role_path = optional(string, null)
logging_retention_in_days = optional(number, 180)
logging_kms_key_id = optional(string, null)
log_class = optional(string, "STANDARD")
lambda_s3_bucket = optional(string, null)
lambda_s3_key = optional(string, null)
lambda_s3_object_version = optional(string, null)
lambda_apigateway_access_log_settings = optional(object({
destination_arn = string
format = string
}), null)
repository_white_list = optional(list(string), [])
queue_selection_strategy = optional(string, "first")
storage_provider = optional(object({
aws = optional(object({
ssm = optional(object({}), null)
}), {})
environment_variables = map(string)
iam_policy_json = optional(string, null)
}))
log_level = optional(string, "info")
lambda_runtime = optional(string, "nodejs24.x")
aws_partition = optional(string, "aws")
lambda_architecture = optional(string, "arm64")
github_app_parameters = object({
webhook_secret = map(string)
})
tracing_config = optional(object({
mode = optional(string, null)
capture_http_requests = optional(bool, false)
capture_error = optional(bool, false)
}), {})
lambda_tags = optional(map(string), {})
api_gw_source_arn = string
ssm_parameter_runner_matcher_config = list(object({
name = string
arn = string
version = string
}))
})
| n/a | yes | +| [config](#input\_config) | Configuration object for all variables. |
object({
prefix = string
archive = optional(object({
enable = optional(bool, true)
retention_days = optional(number, 7)
}), {})
tags = optional(map(string), {})

lambda_subnet_ids = optional(list(string), [])
lambda_security_group_ids = optional(list(string), [])
sqs_job_queues_arns = list(string)
lambda_zip = optional(string, null)
lambda_memory_size = optional(number, 256)
lambda_timeout = optional(number, 10)
role_permissions_boundary = optional(string, null)
role_path = optional(string, null)
logging_retention_in_days = optional(number, 180)
logging_kms_key_id = optional(string, null)
log_class = optional(string, "STANDARD")
lambda_s3_bucket = optional(string, null)
lambda_s3_key = optional(string, null)
lambda_s3_object_version = optional(string, null)
lambda_apigateway_access_log_settings = optional(object({
destination_arn = string
format = string
}), null)
repository_white_list = optional(list(string), [])
queue_selection_strategy = optional(string, "first")
storage_provider = object({
aws = object({
ssm = optional(object({
kms_key_id = optional(string, null)
}), null)
})
})
log_level = optional(string, "info")
lambda_runtime = optional(string, "nodejs24.x")
aws_partition = optional(string, "aws")
lambda_architecture = optional(string, "arm64")
github_app_parameters = object({
webhook_secret = map(string)
})
tracing_config = optional(object({
mode = optional(string, null)
capture_http_requests = optional(bool, false)
capture_error = optional(bool, false)
}), {})
lambda_tags = optional(map(string), {})
api_gw_source_arn = string
ssm_parameter_runner_matcher_config = list(object({
name = string
arn = string
version = string
}))
})
| n/a | yes | ## Outputs diff --git a/modules/webhook/eventbridge/README.md b/modules/webhook/eventbridge/README.md index 74d1cfd8a3..90e81380e4 100644 --- a/modules/webhook/eventbridge/README.md +++ b/modules/webhook/eventbridge/README.md @@ -54,7 +54,7 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [config](#input\_config) | Configuration object for all variables. |
object({
prefix = string
archive = optional(object({
enable = optional(bool, true)
retention_days = optional(number, 7)
}), {})
tags = optional(map(string), {})

lambda_subnet_ids = optional(list(string), [])
lambda_security_group_ids = optional(list(string), [])
sqs_job_queues_arns = list(string)
lambda_zip = optional(string, null)
lambda_memory_size = optional(number, 256)
lambda_timeout = optional(number, 10)
role_permissions_boundary = optional(string, null)
role_path = optional(string, null)
logging_retention_in_days = optional(number, 180)
logging_kms_key_id = optional(string, null)
log_class = optional(string, "STANDARD")
lambda_s3_bucket = optional(string, null)
lambda_s3_key = optional(string, null)
lambda_s3_object_version = optional(string, null)
lambda_apigateway_access_log_settings = optional(object({
destination_arn = string
format = string
}), null)
repository_white_list = optional(list(string), [])
queue_selection_strategy = optional(string, "first")
storage_provider = optional(object({
aws = optional(object({
ssm = optional(object({}), null)
}), {})
webhook = object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
})
dispatcher = object({
environment_variables = map(string)
iam_policy_json = optional(string, null)
})
}))
log_level = optional(string, "info")
lambda_runtime = optional(string, "nodejs24.x")
aws_partition = optional(string, "aws")
lambda_architecture = optional(string, "arm64")
github_app_parameters = object({
webhook_secret = map(string)
})
tracing_config = optional(object({
mode = optional(string, null)
capture_http_requests = optional(bool, false)
capture_error = optional(bool, false)
}), {})
lambda_tags = optional(map(string), {})
api_gw_source_arn = string
ssm_parameter_runner_matcher_config = list(object({
name = string
arn = string
version = string
}))
accept_events = optional(list(string), null)
})
| n/a | yes | +| [config](#input\_config) | Configuration object for all variables. |
object({
prefix = string
archive = optional(object({
enable = optional(bool, true)
retention_days = optional(number, 7)
}), {})
tags = optional(map(string), {})

lambda_subnet_ids = optional(list(string), [])
lambda_security_group_ids = optional(list(string), [])
sqs_job_queues_arns = list(string)
lambda_zip = optional(string, null)
lambda_memory_size = optional(number, 256)
lambda_timeout = optional(number, 10)
role_permissions_boundary = optional(string, null)
role_path = optional(string, null)
logging_retention_in_days = optional(number, 180)
logging_kms_key_id = optional(string, null)
log_class = optional(string, "STANDARD")
lambda_s3_bucket = optional(string, null)
lambda_s3_key = optional(string, null)
lambda_s3_object_version = optional(string, null)
lambda_apigateway_access_log_settings = optional(object({
destination_arn = string
format = string
}), null)
repository_white_list = optional(list(string), [])
queue_selection_strategy = optional(string, "first")
storage_provider = object({
aws = object({
ssm = optional(object({
kms_key_id = optional(string, null)
}), null)
})
})
log_level = optional(string, "info")
lambda_runtime = optional(string, "nodejs24.x")
aws_partition = optional(string, "aws")
lambda_architecture = optional(string, "arm64")
github_app_parameters = object({
webhook_secret = map(string)
})
tracing_config = optional(object({
mode = optional(string, null)
capture_http_requests = optional(bool, false)
capture_error = optional(bool, false)
}), {})
lambda_tags = optional(map(string), {})
api_gw_source_arn = string
ssm_parameter_runner_matcher_config = list(object({
name = string
arn = string
version = string
}))
accept_events = optional(list(string), null)
})
| n/a | yes | ## Outputs From e474ce5e8f8da72327d5b7423dd1ca21dad6963a Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Thu, 24 Sep 2026 19:38:35 +0200 Subject: [PATCH 21/44] fix: fix empty statement --- .../direct/storage-provider.aws.ssm.tf | 26 ++++--- .../eventbridge/storage-provider.aws.ssm.tf | 68 +++++++++++-------- scripts/migrate_multi_runner_state.py | 20 +++--- 3 files changed, 63 insertions(+), 51 deletions(-) diff --git a/modules/webhook/direct/storage-provider.aws.ssm.tf b/modules/webhook/direct/storage-provider.aws.ssm.tf index 48ec6643b4..164c293cc3 100644 --- a/modules/webhook/direct/storage-provider.aws.ssm.tf +++ b/modules/webhook/direct/storage-provider.aws.ssm.tf @@ -1,3 +1,11 @@ +locals { + ssm_environment_variables = var.config.storage_provider.aws.ssm != null ? { + PARAMETER_GITHUB_APP_WEBHOOK_SECRET = var.config.github_app_parameters.webhook_secret.name + PARAMETER_RUNNER_MATCHER_CONFIG_PATH = join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.name]) + PARAMETER_RUNNER_MATCHER_VERSION = join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.version]) + } : {} +} + resource "aws_iam_role_policy" "webhook_kms" { count = var.config.storage_provider.aws.ssm != null ? 1 : 0 @@ -10,19 +18,18 @@ resource "aws_iam_role_policy" "webhook_kms" { } resource "aws_iam_role_policy" "webhook_ssm" { + count = var.config.storage_provider.aws.ssm != null ? 1 : 0 + name = "publish-ssm-policy" role = aws_iam_role.webhook_lambda.name - policy = var.config.storage_provider.aws.ssm != null ? templatefile("${path.module}/../policies/lambda-ssm.json", { + policy = templatefile("${path.module}/../policies/lambda-ssm.json", { resource_arns = jsonencode( concat( [var.config.github_app_parameters.webhook_secret.arn], [for p in var.config.ssm_parameter_runner_matcher_config : p.arn] ) ) - }) : jsonencode({ - Version = "2012-10-17" - Statement = [] }) } @@ -31,10 +38,7 @@ moved { to = aws_iam_role_policy.webhook_kms[0] } -locals { - ssm_environment_variables = var.config.storage_provider.aws.ssm != null ? { - PARAMETER_GITHUB_APP_WEBHOOK_SECRET = var.config.github_app_parameters.webhook_secret.name - PARAMETER_RUNNER_MATCHER_CONFIG_PATH = join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.name]) - PARAMETER_RUNNER_MATCHER_VERSION = join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.version]) - } : {} -} +moved { + from = aws_iam_role_policy.webhook_ssm + to = aws_iam_role_policy.webhook_ssm[0] +} \ No newline at end of file diff --git a/modules/webhook/eventbridge/storage-provider.aws.ssm.tf b/modules/webhook/eventbridge/storage-provider.aws.ssm.tf index 06753b7675..752df9240f 100644 --- a/modules/webhook/eventbridge/storage-provider.aws.ssm.tf +++ b/modules/webhook/eventbridge/storage-provider.aws.ssm.tf @@ -1,12 +1,23 @@ +locals { + ssm_environment_variables = var.config.storage_provider.aws.ssm != null ? { + PARAMETER_GITHUB_APP_WEBHOOK_SECRET = var.config.github_app_parameters.webhook_secret.name + PARAMETER_RUNNER_MATCHER_CONFIG_PATH = join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.name]) + } : {} + + ssm_dispatcher_environment_variables = var.config.storage_provider.aws.ssm != null ? { + PARAMETER_RUNNER_MATCHER_CONFIG_PATH = join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.name]) + PARAMETER_RUNNER_MATCHER_VERSION = join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.version]) + } : {} +} + resource "aws_iam_role_policy" "webhook_ssm" { + count = var.config.storage_provider.aws.ssm != null ? 1 : 0 + name = "publish-ssm-policy" role = aws_iam_role.webhook_lambda.name - policy = var.config.storage_provider.aws.ssm != null ? templatefile("${path.module}/../policies/lambda-ssm.json", { + policy = templatefile("${path.module}/../policies/lambda-ssm.json", { resource_arns = jsonencode([var.config.github_app_parameters.webhook_secret.arn]) - }) : jsonencode({ - Version = "2012-10-17" - Statement = [] }) } @@ -21,36 +32,30 @@ resource "aws_iam_role_policy" "webhook_kms" { }) } -resource "aws_iam_role_policy" "dispatcher_kms" { +resource "aws_iam_role_policy" "dispatcher_ssm" { count = var.config.storage_provider.aws.ssm != null ? 1 : 0 - name = "kms-policy" - role = aws_iam_role.dispatcher_lambda.name - - policy = templatefile("${path.module}/../policies/lambda-kms.json", { - kms_key_arn = var.config.storage_provider.aws.ssm.kms_key_id != null ? var.config.storage_provider.aws.ssm.kms_key_id : "arn:${var.config.aws_partition}:kms:::CMK_NOT_IN_USE" - }) -} - -resource "aws_iam_role_policy" "dispatcher_ssm" { name = "publish-ssm-policy" role = aws_iam_role.dispatcher_lambda.name - policy = var.config.storage_provider.aws.ssm != null ? templatefile("${path.module}/../policies/lambda-ssm.json", { + policy = templatefile("${path.module}/../policies/lambda-ssm.json", { resource_arns = jsonencode( concat( [for p in var.config.ssm_parameter_runner_matcher_config : p.arn] ) ) - }) : jsonencode({ - Version = "2012-10-17" - Statement = [] }) } -moved { - from = aws_iam_role_policy.dispatcher_kms - to = aws_iam_role_policy.dispatcher_kms[0] +resource "aws_iam_role_policy" "dispatcher_kms" { + count = var.config.storage_provider.aws.ssm != null ? 1 : 0 + + name = "kms-policy" + role = aws_iam_role.dispatcher_lambda.name + + policy = templatefile("${path.module}/../policies/lambda-kms.json", { + kms_key_arn = var.config.storage_provider.aws.ssm.kms_key_id != null ? var.config.storage_provider.aws.ssm.kms_key_id : "arn:${var.config.aws_partition}:kms:::CMK_NOT_IN_USE" + }) } moved { @@ -58,14 +63,17 @@ moved { to = aws_iam_role_policy.webhook_kms[0] } -locals { - ssm_environment_variables = var.config.storage_provider.aws.ssm != null ? { - PARAMETER_GITHUB_APP_WEBHOOK_SECRET = var.config.github_app_parameters.webhook_secret.name - PARAMETER_RUNNER_MATCHER_CONFIG_PATH = join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.name]) - } : {} +moved { + from = aws_iam_role_policy.webhook_ssm + to = aws_iam_role_policy.webhook_ssm[0] +} - ssm_dispatcher_environment_variables = var.config.storage_provider.aws.ssm != null ? { - PARAMETER_RUNNER_MATCHER_CONFIG_PATH = join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.name]) - PARAMETER_RUNNER_MATCHER_VERSION = join(":", [for p in var.config.ssm_parameter_runner_matcher_config : p.version]) - } : {} +moved { + from = aws_iam_role_policy.dispatcher_ssm + to = aws_iam_role_policy.dispatcher_ssm[0] } + +moved { + from = aws_iam_role_policy.dispatcher_kms + to = aws_iam_role_policy.dispatcher_kms[0] +} \ No newline at end of file diff --git a/scripts/migrate_multi_runner_state.py b/scripts/migrate_multi_runner_state.py index f1a58edd7d..48cbec87a2 100644 --- a/scripts/migrate_multi_runner_state.py +++ b/scripts/migrate_multi_runner_state.py @@ -98,16 +98,16 @@ ('module.runners.module.job_retry[0].aws_lambda_event_source_mapping.job_retry', 'module.runner_configs.module.orchestration_webhook[0].module.job_retry[0].aws_lambda_event_source_mapping.job_retry'), ('module.runners.module.job_retry[0].aws_lambda_permission.job_retry', 'module.runner_configs.module.orchestration_webhook[0].module.job_retry[0].aws_lambda_permission.job_retry'), ('module.runners.module.job_retry[0].aws_iam_role_policy.job_retry', 'module.runner_configs.module.orchestration_webhook[0].module.job_retry[0].aws_iam_role_policy.job_retry'), - ('module.runners.aws_lambda_function.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper[0].aws_lambda_function.housekeeper'), - ('module.runners.aws_cloudwatch_log_group.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper[0].aws_cloudwatch_log_group.housekeeper'), - ('module.runners.aws_cloudwatch_event_rule.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper[0].aws_cloudwatch_event_rule.housekeeper'), - ('module.runners.aws_cloudwatch_event_target.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper[0].aws_cloudwatch_event_target.housekeeper'), - ('module.runners.aws_lambda_permission.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper[0].aws_lambda_permission.housekeeper'), - ('module.runners.aws_iam_role.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper[0].aws_iam_role.housekeeper'), - ('module.runners.aws_iam_role_policy.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper[0].aws_iam_role_policy.housekeeper'), - ('module.runners.aws_iam_role_policy.ssm_housekeeper_logging', 'module.runner_configs.module.runner_config_housekeeper[0].aws_iam_role_policy.housekeeper_logging'), - ('module.runners.aws_iam_role_policy_attachment.ssm_housekeeper_vpc_execution_role', 'module.runner_configs.module.runner_config_housekeeper[0].aws_iam_role_policy_attachment.housekeeper_vpc_execution_role'), - ('module.runners.aws_iam_role_policy.ssm_housekeeper_xray', 'module.runner_configs.module.runner_config_housekeeper[0].aws_iam_role_policy.housekeeper_xray'), + ('module.runners.aws_lambda_function.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper.aws_lambda_function.housekeeper'), + ('module.runners.aws_cloudwatch_log_group.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper.aws_cloudwatch_log_group.housekeeper'), + ('module.runners.aws_cloudwatch_event_rule.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper.aws_cloudwatch_event_rule.housekeeper'), + ('module.runners.aws_cloudwatch_event_target.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper.aws_cloudwatch_event_target.housekeeper'), + ('module.runners.aws_lambda_permission.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper.aws_lambda_permission.housekeeper'), + ('module.runners.aws_iam_role.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper.aws_iam_role.housekeeper'), + ('module.runners.aws_iam_role_policy.ssm_housekeeper', 'module.runner_configs.module.runner_config_housekeeper.aws_iam_role_policy.housekeeper'), + ('module.runners.aws_iam_role_policy.ssm_housekeeper_logging', 'module.runner_configs.module.runner_config_housekeeper.aws_iam_role_policy.housekeeper_logging'), + ('module.runners.aws_iam_role_policy_attachment.ssm_housekeeper_vpc_execution_role', 'module.runner_configs.module.runner_config_housekeeper.aws_iam_role_policy_attachment.housekeeper_vpc_execution_role'), + ('module.runners.aws_iam_role_policy.ssm_housekeeper_xray', 'module.runner_configs.module.runner_config_housekeeper.aws_iam_role_policy.housekeeper_xray'), ) # These resources are outside the dynamic runner-key modules and therefore From a59d50d68a69856b5b851babe8843e89293bb7b7 Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Thu, 24 Sep 2026 19:48:53 +0200 Subject: [PATCH 22/44] fix: fix migration issue --- modules/compute-providers/aws/ec2/ami.tf | 6 +++--- modules/runners/scale-down.tf | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/compute-providers/aws/ec2/ami.tf b/modules/compute-providers/aws/ec2/ami.tf index 472fbbe5a0..941128bced 100644 --- a/modules/compute-providers/aws/ec2/ami.tf +++ b/modules/compute-providers/aws/ec2/ami.tf @@ -9,9 +9,9 @@ locals { 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) - ami_id_ssm_external = try(local.ami_config.ssm_parameter.path == null, false) - ami_id_ssm_module_managed = try(local.ami_config.ssm_parameter.path != null, false) - ami_id_ssm_parameter_arn = local.ami_id_ssm_external ? try(local.ami_config.ssm_parameter.arn, null) : null + ami_id_ssm_external = try(local.ami_config.ssm_parameter.arn, 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 diff --git a/modules/runners/scale-down.tf b/modules/runners/scale-down.tf index ff7c91dff8..5a3945f686 100644 --- a/modules/runners/scale-down.tf +++ b/modules/runners/scale-down.tf @@ -39,6 +39,7 @@ resource "aws_lambda_function" "scale_down" { PARAMETER_GITHUB_APPS_MANIFEST_NAME = var.github_app_parameters.additional_apps_manifest != null ? var.github_app_parameters.additional_apps_manifest.name : "" POWERTOOLS_LOGGER_LOG_EVENT = var.log_level == "debug" ? "true" : "false" RUNNER_BOOT_TIME_IN_MINUTES = var.runner_boot_time_in_minutes + SSM_TOKEN_PATH = local.token_path SCALE_DOWN_CONFIG = jsonencode(var.idle_config) SCALE_DOWN_IDLE_CONFIRMATION_SECONDS = var.scale_down_idle_confirmation_seconds POWERTOOLS_SERVICE_NAME = "${var.prefix}-scale-down" From 201135f1c470539ac83918702d3d6a451921bdbc Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Thu, 24 Sep 2026 21:21:24 +0200 Subject: [PATCH 23/44] fix: fix mismatch type --- examples/multi-runner-v2/main.tf | 6 +++--- examples/multi-runner-v2/variables.tf | 5 +++-- modules/compute-providers/aws/ec2/ami.tf | 18 ++++++++++-------- .../config.experimental.translation.tf | 5 +++-- modules/multi-runner/runners.tf | 2 +- modules/multi-runner/variables.tf | 5 +++-- 6 files changed, 23 insertions(+), 18 deletions(-) diff --git a/examples/multi-runner-v2/main.tf b/examples/multi-runner-v2/main.tf index 4f63974f11..40749dac13 100644 --- a/examples/multi-runner-v2/main.tf +++ b/examples/multi-runner-v2/main.tf @@ -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 }) } } diff --git a/examples/multi-runner-v2/variables.tf b/examples/multi-runner-v2/variables.tf index fe104758b9..47bf7084af 100644 --- a/examples/multi-runner-v2/variables.tf +++ b/examples/multi-runner-v2/variables.tf @@ -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 diff --git a/modules/compute-providers/aws/ec2/ami.tf b/modules/compute-providers/aws/ec2/ami.tf index 941128bced..2595674541 100644 --- a/modules/compute-providers/aws/ec2/ami.tf +++ b/modules/compute-providers/aws/ec2/ami.tf @@ -1,17 +1,19 @@ locals { - # Handle AMI configuration + # 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 } - 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) - ami_id_ssm_external = try(local.ami_config.ssm_parameter.arn, 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 + 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) + ami_id_ssm_external = try(local.ami_config.ssm_parameter.arn, 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 @@ -36,7 +38,7 @@ data "aws_ami" "runner" { resource "aws_ssm_parameter" "runner_ami_id" { count = local.ami_id_ssm_module_managed ? 1 : 0 - name = "${local.ami_config.ssm_parameter.path}/ami_id" + name = "${local.ami_ssm_parameter_path}/ami_id" type = "String" data_type = "aws:ec2:image" value = data.aws_ami.runner[0].id diff --git a/modules/multi-runner/config.experimental.translation.tf b/modules/multi-runner/config.experimental.translation.tf index f9595b2d7e..31640b3d69 100644 --- a/modules/multi-runner/config.experimental.translation.tf +++ b/modules/multi-runner/config.experimental.translation.tf @@ -514,8 +514,9 @@ locals { ami = v.runner_config.ami == null ? null : { filter = v.runner_config.ami.filter owners = v.runner_config.ami.owners - id_ssm_parameter = v.runner_config.ami.id_ssm_parameter_arn == null ? null : { - arn = v.runner_config.ami.id_ssm_parameter_arn + ssm_parameter = v.runner_config.ami.id_ssm_parameter_arn != null ? null : { + path = null + arn = v.runner_config.ami.id_ssm_parameter_arn } kms_key = v.runner_config.ami.kms_key_arn == null ? null : { arn = v.runner_config.ami.kms_key_arn diff --git a/modules/multi-runner/runners.tf b/modules/multi-runner/runners.tf index 9b21e3d0fd..ad2351600f 100644 --- a/modules/multi-runner/runners.tf +++ b/modules/multi-runner/runners.tf @@ -34,7 +34,7 @@ module "runners" { ami = try(each.value.compute_provider.aws.ec2.ami == null ? null : { filter = each.value.compute_provider.aws.ec2.ami.filter owners = each.value.compute_provider.aws.ec2.ami.owners - id_ssm_parameter_arn = try(each.value.compute_provider.aws.ec2.ami.id_ssm_parameter.arn, null) + id_ssm_parameter_arn = try(each.value.compute_provider.aws.ec2.ami.ssm_parameter.arn, null) kms_key_arn = try(each.value.compute_provider.aws.ec2.ami.kms_key.arn, null) }, null) diff --git a/modules/multi-runner/variables.tf b/modules/multi-runner/variables.tf index 8c4bde3668..23d8712647 100644 --- a/modules/multi-runner/variables.tf +++ b/modules/multi-runner/variables.tf @@ -507,8 +507,9 @@ variable "multi_runner_config" { ami = optional(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 From acb36bcc1f5539fb1ebb215b61f21f061bc38375 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 24 Sep 2026 19:22:13 +0000 Subject: [PATCH 24/44] docs: auto update terraform docs --- examples/multi-runner-v2/README.md | 2 +- modules/multi-runner/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/multi-runner-v2/README.md b/examples/multi-runner-v2/README.md index 2755c8fcf5..3a8b994788 100644 --- a/examples/multi-runner-v2/README.md +++ b/examples/multi-runner-v2/README.md @@ -63,7 +63,7 @@ variable source in real deployments rather than committed to configuration. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [ami](#input\_ami) | Optional AMI configuration keyed by runner lane. |
map(object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
id_ssm_parameter = optional(object({
arn = string
}), null)
kms_key = optional(object({
arn = string
}), null)
}))
| `{}` | no | +| [ami](#input\_ami) | Optional AMI configuration keyed by runner lane. |
map(object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
ssm_parameter = optional(object({
path = optional(string, null)
arn = optional(string, null)
}), null)
kms_key = optional(object({
arn = string
}), null)
}))
| `{}` | no | | [aws\_region](#input\_aws\_region) | AWS region to deploy to. | `string` | `"eu-west-1"` | no | | [environment](#input\_environment) | Environment name, used as prefix. | `string` | `null` | no | | [github\_app](#input\_github\_app) | GitHub App ID and base64-encoded private key. |
object({
id = string
key_base64 = string
})
| n/a | yes | diff --git a/modules/multi-runner/README.md b/modules/multi-runner/README.md index 92cf3fe141..565d0b9454 100644 --- a/modules/multi-runner/README.md +++ b/modules/multi-runner/README.md @@ -188,7 +188,7 @@ module "multi-runner" { | [logging\_retention\_in\_days](#input\_logging\_retention\_in\_days) | Specifies the number of days you want to retain log events for the lambda log group. Possible values are: 0, 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, and 3653. | `number` | `180` | no | | [matcher\_config\_parameter\_store\_tier](#input\_matcher\_config\_parameter\_store\_tier) | The tier of the parameter store for the matcher configuration. Valid values are `Standard`, and `Advanced`. | `string` | `"Standard"` | no | | [metrics](#input\_metrics) | Configuration for metrics created by the module, by default metrics are disabled to avoid additional costs. When metrics are enable all metrics are created unless explicit configured otherwise. |
object({
enable = optional(bool, false)
namespace = optional(string, "GitHub Runners")
metric = optional(object({
enable_github_app_rate_limit = optional(bool, true)
enable_job_retry = optional(bool, true)
enable_spot_termination_warning = optional(bool, true)
}), {})
})
| `{}` | no | -| [multi\_runner\_config](#input\_multi\_runner\_config) | Accepts either the stable v1 runner configuration shape or the provider-boundary v2 shape. Entries with `runner_config` use the v1 shape; entries without `runner_config` use the v2 shape. A v2 entry does not need matcher configuration. A v2 entry must be acknowledged with `experimental_features = ["multi-runner-v2"]`; the v2 shape is experimental and may change before graduation.

multi\_runner\_config = {
runner\_config: {
runner\_os: "The EC2 Operating System type to use for action runner instances (linux, osx, windows)."
runner\_architecture: "The platform architecture of the runner instance\_type."
runner\_metadata\_options: "(Optional) Metadata options for the ec2 runner instances."
ami: "(Optional) AMI configuration for the action runner instances. This object allows you to specify all AMI-related settings in one place."
create\_service\_linked\_role\_spot: (Optional) create the serviced linked role for spot instances that is required by the scale-up lambda.
credit\_specification: "(Optional) The credit specification of the runner instance\_type. Can be unset, `standard` or `unlimited`.
delay\_webhook\_event: "The number of seconds the event accepted by the webhook is invisible on the queue before the scale up lambda will receive the event."
disable\_runner\_autoupdate: "Disable the auto update of the github runner agent. Be aware there is a grace period of 30 days, see also the [GitHub article](https://github.blog/changelog/2022-02-01-github-actions-self-hosted-runners-can-now-disable-automatic-updates/)"
ebs\_optimized: "The EC2 EBS optimized configuration."
enable\_ephemeral\_runners: "Enable ephemeral runners, runners will only be used once."
enable\_job\_queued\_check: Enables JIT configuration for creating runners instead of registration token based registraton. JIT configuration will only be applied for ephemeral runners. By default JIT configuration is enabled for ephemeral runners an can be disabled via this override. When running on GHES without support for JIT configuration this variable should be set to true for ephemeral runners."
enable\_on\_demand\_failover\_for\_errors: "Enable on-demand failover. For example to fall back to on demand when no spot capacity is available the variable can be set to `InsufficientInstanceCapacity`. When not defined the default behavior is to retry later."
scale\_errors: "List of AWS error codes that should trigger retry during scale up. This list replaces the module default scale-up retry errors"
enable\_organization\_runners: "Register runners to organization, instead of repo level"
enable\_runner\_binaries\_syncer: "Option to disable the lambda to sync GitHub runner distribution, useful when using a pre-build AMI."
enable\_ssm\_on\_runners: "Enable to allow access the runner instances for debugging purposes via SSM. Note that this adds additional permissions to the runner instances."
enable\_userdata: "Should the userdata script be enabled for the runner. Set this to false if you are using your own prebuilt AMI."
instance\_allocation\_strategy: "The allocation strategy for creating instances. For spot, AWS recommends `price-capacity-optimized`; for on-demand, use `lowest-price` or `prioritized`. The AWS default is `lowest-price`."
instance\_type\_priorities: "A map of instance type to priority for the `prioritized` and `capacity-optimized-prioritized` allocation strategies. Lower numbers mean higher priority. If not provided, priorities are assigned based on the order of `instance_types`."
instance\_max\_spot\_price: "Max price price for spot instances per hour. This variable will be passed to the create fleet as max spot price for the fleet."
instance\_target\_capacity\_type: "Default lifecycle used for runner instances, can be either `spot` or `on-demand`."
instance\_types: "List of instance types for the action runner. Defaults are based on runner\_os (al2023 for linux, macOS Sequoia for osx, Windows Server Core for win)."
job\_queue\_retention\_in\_seconds: "The number of seconds the job is held in the queue before it is purged"
minimum\_running\_time\_in\_minutes: "The time an ec2 action runner should be running at minimum before terminated if not busy."
pool\_runner\_owner: "The pool will deploy runners to the GitHub org ID, set this value to the org to which you want the runners deployed. Repo level is not supported."
runner\_additional\_security\_group\_ids: "List of additional security groups IDs to apply to the runner. If added outside the multi\_runner\_config block, the additional security group(s) will be applied to all runner configs. If added inside the multi\_runner\_config, the additional security group(s) will be applied to the individual runner."
runner\_as\_root: "Run the action runner under the root user. Variable `runner_run_as` will be ignored."
runner\_boot\_time\_in\_minutes: "The minimum time for an EC2 runner to boot and register as a runner."
scale\_down\_idle\_confirmation\_seconds: "Number of seconds a runner must consistently report not-busy before scale-down terminates it. GitHub's busy flag can be stale, so a single not-busy reading is not sufficient evidence a runner is idle. 0 keeps the previous single-reading behaviour."
runner\_disable\_default\_labels: "Disable default labels for the runners (os, architecture and `self-hosted`). If enabled, the runner will only have the extra labels provided in `runner_extra_labels`. In case you on own start script is used, this configuration parameter needs to be parsed via SSM."
runner\_extra\_labels: "Extra (custom) labels for the runners (GitHub). Separate each label by a comma. Labels checks on the webhook can be enforced by setting `multi_runner_config.matcherConfig.exactMatch`. GitHub read-only labels should not be provided."
runner\_group\_name: "Name of the runner group."
runner\_name\_prefix: "Prefix for the GitHub runner name."
runner\_run\_as: "Run the GitHub actions agent as user."
runners\_maximum\_count: "The maximum number of runners that will be created. Setting the variable to `-1` disables the maximum check."
scale\_down\_schedule\_expression: "Scheduler expression to check every x for scale down."
scale\_up\_reserved\_concurrent\_executions: "Amount of reserved concurrent executions for the scale-up lambda function. A value of 0 disables lambda from being triggered and -1 removes any concurrency limitations."
lambda\_event\_source\_mapping\_batch\_size: "(Optional) Maximum number of records per Lambda invocation for this runner flavor. Overrides the module-level `lambda_event_source_mapping_batch_size` when set."
lambda\_event\_source\_mapping\_maximum\_batching\_window\_in\_seconds: "(Optional) Maximum seconds to gather records before invoking Lambda for this runner flavor. Overrides the module-level `lambda_event_source_mapping_maximum_batching_window_in_seconds` when set."
userdata\_template: "Alternative user-data template, replacing the default template. By providing your own user\_data you have to take care of installing all required software, including the action runner. Variables userdata\_pre/post\_install are ignored."
enable\_jit\_config: "Overwrite the default behavior for JIT configuration. By default JIT configuration is enabled for ephemeral runners and disabled for non-ephemeral runners. In case of GHES check first if the JIT config API is available. In case you are upgrading from 3.x to 4.x you can set `enable_jit_config` to `false` to avoid a breaking change when having your own AMI."
enable\_runner\_detailed\_monitoring: "Should detailed monitoring be enabled for the runner. Set this to true if you want to use detailed monitoring. See https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-cloudwatch-new.html for details."
enable\_cloudwatch\_agent: "Enabling the cloudwatch agent on the ec2 runner instances, the runner contains default config. Configuration can be overridden via `cloudwatch_config`."
cloudwatch\_config: "(optional) Replaces the module default cloudwatch log config. See https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Agent-Configuration-File-Details.html for details."
userdata\_pre\_install: "Script to be ran before the GitHub Actions runner is installed on the EC2 instances"
userdata\_post\_install: "Script to be ran after the GitHub Actions runner is installed on the EC2 instances"
runner\_hook\_job\_started: "Script to be ran in the runner environment at the beginning of every job"
runner\_hook\_job\_completed: "Script to be ran in the runner environment at the end of every job"
runner\_ec2\_tags: "Map of tags that will be added to the launch template instance tag specifications."
runner\_iam\_role\_managed\_policy\_arns: "Attach AWS or customer-managed IAM policies (by ARN) to the runner IAM role"
vpc\_id: "The VPC for security groups of the action runners. If not set uses the value of `var.vpc_id`."
subnet\_ids: "List of subnets in which the action runners will be launched, the subnets needs to be subnets in the `vpc_id`. If not set, uses the value of `var.subnet_ids`."
idle\_config: "List of time period that can be defined as cron expression to keep a minimum amount of runners active instead of scaling down to 0. By defining this list you can ensure that in time periods that match the cron expression within 5 seconds a runner is kept idle."
license\_specifications: "Optional EC2 License Manager license configuration ARNs for the runner launch template. Required for macOS dedicated-host runners when the host resource group uses a Mac dedicated host license configuration."
use\_dedicated\_host: "Experimental! Can be removed / changed without trigger a major release. Whether to use EC2 dedicated hosts for the runners. Needed for macos runners Note that using dedicated hosts can increase cost significantly."
runner\_log\_files: "(optional) Replaces the module default cloudwatch log config. See https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Agent-Configuration-File-Details.html for details."
block\_device\_mappings: "The EC2 instance block device configuration. Takes the following keys: `device_name`, `delete_on_termination`, `volume_type`, `volume_size`, `encrypted`, `iops`, `throughput`, `kms_key_id`, `snapshot_id`, `volume_initialization_rate`."
job\_retry: "Experimental! Can be removed / changed without trigger a major release. Configure job retries. The configuration enables job retries (for ephemeral runners). After creating the instances a message will be published to a job retry queue. The job retry check lambda is checking after a delay if the job is queued. If not the message will be published again on the scale-up (build queue). Using this feature can impact the rate limit of the GitHub app."
pool\_config: "The configuration for updating the pool. The `pool_size` to adjust to by the events triggered by the `schedule_expression`. For example you can configure a cron expression for week days to adjust the pool to 10 and another expression for the weekend to adjust the pool to 1. Use `schedule_expression_timezone` to override the schedule time zone (defaults to UTC)."
ssm\_ttl\_seconds.tokens: "Optional TTL in seconds for the SSM parameters holding the runner registration token / JIT config. When set, the parameters are created with an SSM expiration policy so SSM deletes them itself after the TTL passes. Requires the Advanced parameter tier for every token parameter, which incurs additional costs. Expiration is enforced asynchronously by SSM; the SSM housekeeper lambda remains as a backstop. Must be a positive number, and should comfortably exceed the runner boot time so the config does not expire before the instance reads it."
iam\_overrides: "Allows to (optionally) override the instance profile and runner role created by the module. Set `override_instance_profile` to true and provide the `instance_profile_name` to use an existing instance profile. Set `override_runner_role` to true and provide the `runner_role_arn` to use an existing role for the runner instances."
}
# V2 contract
tags: "Tags applied to resources created for this runner configuration."
runner: "Runner settings such as the operating system, architecture, labels, hooks, runner group, name prefix, and IAM role configuration."
lambda: "Lambda settings such as runtime, architecture, networking, tags, and execution-role options for this runner configuration."
# Webhook, queue, and scale-up/scale-down orchestration settings.
orchestration\_provider: {
webhook: {
matcherConfig: "Label matching and dynamic-label policy used to route workflow jobs to this runner configuration."
runner: "Runner lifecycle settings including boot time, ephemeral mode, JIT configuration, and maximum runner count."
queue: "Build queue delay, retention, visibility timeout, redrive, and tags."
}
}
ssm: "SSM parameter paths, tags, and housekeeper settings for runner configuration storage."
observability: "Logging, tracing, and metric settings for the resources in this runner configuration."
# Compute settings for the runner provider.
compute\_provider: {
aws: {
ec2: "AWS EC2 runner settings, including AMI selection, instance types, capacity strategy, VPC and subnet placement, storage, user data, and runner access."
}
}
matcherConfig: {
labelMatchers: "The list of list of labels supported by the runner configuration. `[[self-hosted, linux, x64, example]]`"
exactMatch: "DEPRECATED: Use `bidirectionalLabelMatch` instead. If set to true all labels in the workflow job must match the GitHub labels (os, architecture and `self-hosted`). When false if __any__ workflow label matches it will trigger the webhook. Note: this only checks that workflow labels are a subset of runner labels, not the reverse."
bidirectionalLabelMatch: "If set to true, the runner labels and workflow job labels must be an exact two-way match (same set, any order, no extras or missing labels). This is stricter than `exactMatch` which only checks that workflow labels are a subset of runner labels. When false, if __any__ workflow label matches it will trigger the webhook."
priority: "If set it defines the priority of the matcher, the matcher with the lowest priority will be evaluated first. Default is 999, allowed values 0-999."
enableDynamicLabels: "Experimental! When true the dispatcher allows `ghr-*` dynamic labels for jobs routed to this runner. Default false."
awsDynamicLabelsPolicy: "Optional AWS dynamic label policy evaluated by the dispatcher. Only effective when `enableDynamicLabels = true`. Jobs whose provider dynamic labels violate every matching runner's policy are rejected with a 202 (a warning is logged). Evaluation: if `allowed_keys` is set, only those keys are accepted; keys in `blocked_keys` are always rejected (cannot be used together with `allowed_keys`); keys in `restricted_keys` are allowed only when their value passes the rule; a key not listed anywhere is allowed. Schema: `{ allowed_keys = [], blocked_keys = [], restricted_keys = { = { allowed = [globs], denied = [globs], max = number|string } } }`. Keys use the dynamic label suffix, e.g. `instance-type` for `ghr-ec2-instance-type`."
}
redrive\_build\_queue: "Set options to attach (optional) a dead letter queue to the build queue, the queue between the webhook and the scale up lambda. You have the following options. 1. Disable by setting `enabled` to false. 2. Enable by setting `enabled` to `true`, `maxReceiveCount` to a number of max retries."
} |
map(object({
# V1 contract
runner_config = optional(object({
runner_os = string
runner_architecture = string
runner_metadata_options = optional(map(any), {
instance_metadata_tags = "enabled"
http_endpoint = "enabled"
http_tokens = "required"
http_put_response_hop_limit = 1
})
ami = optional(object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
id_ssm_parameter_arn = optional(string, null)
kms_key_arn = optional(string, null)
}), null)
create_service_linked_role_spot = optional(bool, false)
credit_specification = optional(string, null)
delay_webhook_event = optional(number, 30)
disable_runner_autoupdate = optional(bool, false)
ebs_optimized = optional(bool, false)
enable_ephemeral_runners = optional(bool, false)
enable_job_queued_check = optional(bool, null)
enable_on_demand_failover_for_errors = optional(list(string), [])
scale_errors = optional(list(string), [
"UnfulfillableCapacity",
"MaxSpotInstanceCountExceeded",
"TargetCapacityLimitExceededException",
"RequestLimitExceeded",
"ResourceLimitExceeded",
"MaxSpotInstanceCountExceeded",
"MaxSpotFleetRequestCountExceeded",
"InsufficientInstanceCapacity",
"InsufficientCapacityOnHost",
])
enable_organization_runners = optional(bool, false)
enable_runner_binaries_syncer = optional(bool, true)
enable_ssm_on_runners = optional(bool, false)
enable_userdata = optional(bool, true)
instance_allocation_strategy = optional(string, "lowest-price")
instance_type_priorities = optional(map(number), null)
instance_max_spot_price = optional(string, null)
instance_target_capacity_type = optional(string, "spot")
instance_types = list(string)
job_queue_retention_in_seconds = optional(number, 86400)
minimum_running_time_in_minutes = optional(number, null)
pool_runner_owner = optional(string, null)
runner_as_root = optional(bool, false)
runner_boot_time_in_minutes = optional(number, 5)
scale_down_idle_confirmation_seconds = optional(number, 0)
runner_disable_default_labels = optional(bool, false)
runner_extra_labels = optional(list(string), [])
runner_group_name = optional(string, "Default")
runner_name_prefix = optional(string, "")
runner_run_as = optional(string, "ec2-user")
runners_maximum_count = number
runner_additional_security_group_ids = optional(list(string), [])
scale_down_schedule_expression = optional(string, "cron(*/5 * * * ? *)")
scale_up_reserved_concurrent_executions = optional(number, 1)
lambda_event_source_mapping_batch_size = optional(number, null)
lambda_event_source_mapping_maximum_batching_window_in_seconds = optional(number, null)
userdata_template = optional(string, null)
userdata_content = optional(string, null)
enable_jit_config = optional(bool, null)
enable_runner_detailed_monitoring = optional(bool, false)
enable_cloudwatch_agent = optional(bool, true)
cloudwatch_config = optional(string, null)
userdata_pre_install = optional(string, "")
userdata_post_install = optional(string, "")
runner_hook_job_started = optional(string, "")
runner_hook_job_completed = optional(string, "")
runner_ec2_tags = optional(map(string), {})
runner_iam_role_managed_policy_arns = optional(list(string), [])
vpc_id = optional(string, null)
subnet_ids = optional(list(string), null)
idle_config = optional(list(object({
cron = string
timeZone = string
idleCount = number
evictionStrategy = optional(string, "oldest_first")
})), [])
cpu_options = optional(object({
core_count = optional(number)
threads_per_core = optional(number)
amd_sev_snp = optional(string)
nested_virtualization = optional(string)
}), null)
network_interfaces = optional(list(object({
associate_carrier_ip_address = optional(bool)
associate_public_ip_address = optional(bool)
delete_on_termination = optional(bool)
description = optional(string)
device_index = optional(number)
interface_type = optional(string)
ipv4_address_count = optional(number)
ipv4_addresses = optional(list(string))
ipv4_prefix_count = optional(number)
ipv4_prefixes = optional(list(string))
ipv6_address_count = optional(number)
ipv6_addresses = optional(list(string))
ipv6_prefix_count = optional(number)
ipv6_prefixes = optional(list(string))
network_card_index = optional(number)
network_interface_id = optional(string)
primary_ipv6 = optional(bool)
private_ip_address = optional(string)
security_groups = optional(list(string))
subnet_id = optional(string)
connection_tracking_specification = optional(object({
tcp_established_timeout = optional(number)
udp_stream_timeout = optional(number)
udp_timeout = optional(number)
}))
ena_srd_specification = optional(object({
ena_srd_enabled = optional(bool)
ena_srd_udp_specification = optional(object({
ena_srd_udp_enabled = optional(bool)
}))
}))
})), [])
placement = optional(object({
affinity = optional(string)
availability_zone = optional(string)
group_id = optional(string)
group_name = optional(string)
host_id = optional(string)
host_resource_group_arn = optional(string)
spread_domain = optional(string)
tenancy = optional(string)
partition_number = optional(number)
}), null)
license_specifications = optional(list(object({
license_configuration_arn = string
})), [])
use_dedicated_host = optional(bool, false)
runner_log_files = optional(list(object({
log_group_name = string
prefix_log_group = bool
file_path = string
log_stream_name = string
log_class = optional(string, "STANDARD")
})), null)
block_device_mappings = optional(list(object({
delete_on_termination = optional(bool, true)
device_name = optional(string, "/dev/xvda")
encrypted = optional(bool, true)
iops = optional(number)
kms_key_id = optional(string)
snapshot_id = optional(string)
throughput = optional(number)
volume_initialization_rate = optional(number)
volume_size = number
volume_type = optional(string, "gp3")
})), [{
volume_size = 30
}])
pool_config = optional(list(object({
schedule_expression = string
schedule_expression_timezone = optional(string)
size = number
})), [])
ssm_ttl_seconds = optional(object({
tokens = optional(number, null)
}), {})
job_retry = optional(object({
enable = optional(bool, false)
delay_in_seconds = optional(number, 300)
delay_backoff = optional(number, 2)
lambda_memory_size = optional(number, 256)
lambda_timeout = optional(number, 30)
max_attempts = optional(number, 1)
}), {})
iam_overrides = optional(object({
override_instance_profile = optional(bool, null)
instance_profile_name = optional(string, null)
override_runner_role = optional(bool, null)
runner_role_arn = optional(string, null)
}), {
override_instance_profile = false
instance_profile_name = null
override_runner_role = false
runner_role_arn = null
})
}), null)
matcherConfig = optional(object({
labelMatchers = list(list(string))
exactMatch = optional(bool, false)
bidirectionalLabelMatch = optional(bool, false)
priority = optional(number, 999)
enableDynamicLabels = optional(bool, false)
awsDynamicLabelsPolicy = optional(any, null)
}), null)
redrive_build_queue = optional(object({
enabled = bool
maxReceiveCount = number
}), {
enabled = false
maxReceiveCount = null
})

# V2 Contract
tags = optional(map(string), {})

runner = optional(object({
os = optional(string, null)
architecture = optional(string, null)
disable_default_labels = optional(bool, null)
extra_labels = optional(list(string), null)
group_name = optional(string, null)
name_prefix = optional(string, null)
run_as_root = optional(bool, null)
run_as = optional(string, null)
auto_update_disabled = optional(bool, null)
tags = optional(map(string), {})
hooks = optional(object({
job_started = optional(string, null)
job_completed = optional(string, null)
}), {})
iam = optional(object({
role = optional(object({
arn = string
}), null)
managed_policy_arns = optional(map(string), null)
additional_trust_policy_json = optional(string, null)
path = optional(string, null)
permissions_boundary = optional(string, null)
}), {})
}), {})

lambda = optional(object({
runtime = optional(string, null)
architecture = optional(string, null)
subnet_ids = optional(list(string), null)
security_group_ids = optional(list(string), null)
tags = optional(map(string), {})
role = optional(object({
path = optional(string, null)
permissions_boundary = optional(string, null)
}), {})
}), {})

orchestration_provider = optional(object({
webhook = optional(object({
runner = optional(object({
boot_time_in_minutes = optional(number, null)
ephemeral = optional(bool, null)
jit_config_enabled = optional(bool, null)
maximum_count = optional(number, null)
}), {})
github = optional(object({
organization_runners = optional(bool, false)
}), {})
matcherConfig = optional(object({
labelMatchers = list(list(string))
exactMatch = optional(bool, false)
bidirectionalLabelMatch = optional(bool, false)
priority = optional(number, 999)
dynamic_labels_enabled = optional(bool, false)
awsDynamicLabelsPolicy = optional(object({
allowed_keys = optional(list(string), [])
blocked_keys = optional(list(string), [])
restricted_keys = optional(map(object({
allowed = optional(list(string), [])
denied = optional(list(string), [])
max = optional(string, null)
})), {})
}), null)
}), null)
queue = optional(object({
delay_webhook_event = optional(number, null)
job_queue_retention_in_seconds = optional(number, null)
visibility_timeout_seconds = optional(number, null)
redrive_build_queue = optional(object({
enabled = optional(bool, null)
maxReceiveCount = optional(number, null)
}), null)
tags = optional(map(string), {})
}), {})
lambda = optional(object({
scale = optional(object({
up = optional(object({
memory_size = optional(number, null)
timeout = optional(number, null)
reserved_concurrent_executions = optional(number, null)
job_queued_check_enabled = optional(bool, null)
event_source_mapping = optional(object({
batch_size = optional(number, null)
maximum_batching_window_in_seconds = optional(number, null)
}), {})
tags = optional(map(string), {})
}), {})
down = optional(object({
memory_size = optional(number, null)
timeout = optional(number, null)
schedule_expression = optional(string, null)
minimum_running_time_in_minutes = optional(number, null)
idle_confirmation_seconds = optional(number, null)
idle_config = optional(list(object({
cron = string
timeZone = string
idleCount = number
evictionStrategy = optional(string, "oldest_first")
})), null)
tags = optional(map(string), {})
}), {})
}), {})
pool = optional(object({
memory_size = optional(number, null)
timeout = optional(number, null)
reserved_concurrent_executions = optional(number, null)
config = optional(list(object({
schedule_expression = string
schedule_expression_timezone = optional(string)
size = number
})), null)
include_busy_runners = optional(bool, null)
runner_owner = optional(string, null)
tags = optional(map(string), {})
}), {})
}), {})
job_retry = optional(object({
enabled = optional(bool, false)
delay_in_seconds = optional(number, 300)
delay_backoff = optional(number, 2)
max_attempts = optional(number, 1)
tags = optional(map(string), {})
lambda = optional(object({
memory_size = optional(number, 256)
reserved_concurrent_executions = optional(number, 1)
timeout = optional(number, 30)
}), {})
}), {})
}), null)
}), {})

storage_provider = optional(object({
aws = optional(object({
ssm = optional(object({
ttl_seconds = optional(object({
tokens = optional(number, null)
}), {})
paths = optional(object({
root = optional(string, null)
tokens = optional(string, null)
config = optional(string, null)
}), {})
tags = optional(map(string), {})
parameters = optional(object({
tags = optional(map(string), {})
}), {})
housekeeper = optional(object({
schedule_expression = optional(string, null)
state = optional(string, null)
tags = optional(map(string), {})
lambda = optional(object({
artifact = optional(object({
zip = optional(string, null)
s3 = optional(object({
key = string
object_version = optional(string, null)
}), null)
}), {})
memory_size = optional(number, null)
timeout = optional(number, null)
}), {})
config = optional(object({
tokenPath = optional(string, null)
minimumDaysOld = optional(number, null)
dryRun = optional(bool, null)
}), {})
}), {})
}), {})
}), {})
}), {})

observability = optional(object({
logs = optional(object({
level = optional(string, null)
retention_in_days = optional(number, null)
kms_key_id = optional(string, null)
class = optional(string, null)
tags = optional(map(string), {})
}), {})
tracing = optional(object({
mode = optional(string, null)
capture_http_requests = optional(bool, null)
capture_error = optional(bool, null)
}), {})
metrics = optional(object({
enabled = optional(bool, null)
namespace = optional(string, null)
metric = optional(object({
github_app_rate_limit = optional(object({
enabled = optional(bool, null)
}), {})
job_retry = optional(object({
enabled = optional(bool, null)
}), {})
spot_termination_warning = optional(object({
enabled = optional(bool, null)
}), {})
}), {})
}), {})
}), {})

compute_provider = optional(object({
aws = optional(object({
ec2 = optional(object({
metadata_options = optional(object({
instance_metadata_tags = optional(string, "enabled")
http_endpoint = optional(string, "enabled")
http_tokens = optional(string, "required")
http_put_response_hop_limit = optional(number, 1)
}), {})
ami = optional(object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
id_ssm_parameter = optional(object({
arn = string
}), null)
kms_key = optional(object({
arn = string
}), null)
}), null)
block_device_mappings = optional(list(object({
delete_on_termination = optional(bool, true)
device_name = optional(string, "/dev/xvda")
encrypted = optional(bool, true)
iops = optional(number)
kms_key_id = optional(string)
snapshot_id = optional(string)
throughput = optional(number)
volume_initialization_rate = optional(number)
volume_size = number
volume_type = optional(string, "gp3")
})), [{ volume_size = 30 }])
create_service_linked_role_spot = optional(bool, false)
credit_specification = optional(string, null)
ebs_optimized = optional(bool, false)
cloudwatch_agent = optional(object({
enabled = optional(bool, true)
config = optional(string, null)
}), {})
binaries_syncer = optional(object({
enabled = optional(bool, null)
}), {})
detailed_monitoring_enabled = optional(bool, false)
ssm_enabled = optional(bool, false)
user_data = optional(object({
enabled = optional(bool, true)
template = optional(string, null)
content = optional(string, null)
pre_install = optional(string, "")
post_install = optional(string, "")
debug_logging_enabled = optional(bool, false)
}), {})
instance_allocation_strategy = optional(string, "lowest-price")
instance_max_spot_price = optional(string, null)
instance_target_capacity_type = optional(string, "spot")
instance_type_priorities = optional(map(number), null)
instance_types = optional(list(string), [])
additional_security_group_ids = optional(list(string), null)
managed_security_group_enabled = optional(bool, null)
egress_rules = optional(list(object({
cidr_blocks = list(string)
ipv6_cidr_blocks = list(string)
prefix_list_ids = list(string)
from_port = number
protocol = string
security_groups = list(string)
self = bool
to_port = number
description = string
})), null)
instance_profile_path = optional(string, null)
key_name = optional(string, null)
associate_public_ipv4_address = optional(bool, null)
instance_profile = optional(object({
name = string
}), null)
on_demand_failover_for_errors = optional(list(string), [])
scale_errors = optional(list(string), [
"UnfulfillableCapacity",
"MaxSpotInstanceCountExceeded",
"TargetCapacityLimitExceededException",
"RequestLimitExceeded",
"ResourceLimitExceeded",
"MaxSpotInstanceCountExceeded",
"MaxSpotFleetRequestCountExceeded",
"InsufficientInstanceCapacity",
"InsufficientCapacityOnHost",
])
subnet_ids = optional(list(string), null)
vpc_id = optional(string, null)
cpu_options = optional(object({
core_count = optional(number)
threads_per_core = optional(number)
amd_sev_snp = optional(string)
nested_virtualization = optional(string)
}), null)
network_interfaces = optional(list(object({
associate_carrier_ip_address = optional(bool)
associate_public_ip_address = optional(bool)
delete_on_termination = optional(bool)
description = optional(string)
device_index = optional(number)
interface_type = optional(string)
ipv4_address_count = optional(number)
ipv4_addresses = optional(list(string))
ipv4_prefix_count = optional(number)
ipv4_prefixes = optional(list(string))
ipv6_address_count = optional(number)
ipv6_addresses = optional(list(string))
ipv6_prefix_count = optional(number)
ipv6_prefixes = optional(list(string))
network_card_index = optional(number)
network_interface_id = optional(string)
primary_ipv6 = optional(bool)
private_ip_address = optional(string)
security_groups = optional(list(string))
subnet_id = optional(string)
connection_tracking_specification = optional(object({
tcp_established_timeout = optional(number)
udp_stream_timeout = optional(number)
udp_timeout = optional(number)
}))
ena_srd_specification = optional(object({
ena_srd_enabled = optional(bool)
ena_srd_udp_specification = optional(object({
ena_srd_udp_enabled = optional(bool)
}))
}))
})), [])
placement = optional(object({
affinity = optional(string)
availability_zone = optional(string)
group_id = optional(string)
group_name = optional(string)
host_id = optional(string)
host_resource_group_arn = optional(string)
spread_domain = optional(string)
tenancy = optional(string)
partition_number = optional(number)
}), null)
license_specifications = optional(list(object({
license_configuration_arn = string
})), [])
use_dedicated_host = optional(bool, false)
log_files = optional(list(object({
log_group_name = string
prefix_log_group = bool
file_path = string
log_stream_name = string
log_class = optional(string, "STANDARD")
})), null)
tags = optional(map(string), {})
}), null)
}), {})
}), {})
}))
| `{}` | no | +| [multi\_runner\_config](#input\_multi\_runner\_config) | Accepts either the stable v1 runner configuration shape or the provider-boundary v2 shape. Entries with `runner_config` use the v1 shape; entries without `runner_config` use the v2 shape. A v2 entry does not need matcher configuration. A v2 entry must be acknowledged with `experimental_features = ["multi-runner-v2"]`; the v2 shape is experimental and may change before graduation.

multi\_runner\_config = {
runner\_config: {
runner\_os: "The EC2 Operating System type to use for action runner instances (linux, osx, windows)."
runner\_architecture: "The platform architecture of the runner instance\_type."
runner\_metadata\_options: "(Optional) Metadata options for the ec2 runner instances."
ami: "(Optional) AMI configuration for the action runner instances. This object allows you to specify all AMI-related settings in one place."
create\_service\_linked\_role\_spot: (Optional) create the serviced linked role for spot instances that is required by the scale-up lambda.
credit\_specification: "(Optional) The credit specification of the runner instance\_type. Can be unset, `standard` or `unlimited`.
delay\_webhook\_event: "The number of seconds the event accepted by the webhook is invisible on the queue before the scale up lambda will receive the event."
disable\_runner\_autoupdate: "Disable the auto update of the github runner agent. Be aware there is a grace period of 30 days, see also the [GitHub article](https://github.blog/changelog/2022-02-01-github-actions-self-hosted-runners-can-now-disable-automatic-updates/)"
ebs\_optimized: "The EC2 EBS optimized configuration."
enable\_ephemeral\_runners: "Enable ephemeral runners, runners will only be used once."
enable\_job\_queued\_check: Enables JIT configuration for creating runners instead of registration token based registraton. JIT configuration will only be applied for ephemeral runners. By default JIT configuration is enabled for ephemeral runners an can be disabled via this override. When running on GHES without support for JIT configuration this variable should be set to true for ephemeral runners."
enable\_on\_demand\_failover\_for\_errors: "Enable on-demand failover. For example to fall back to on demand when no spot capacity is available the variable can be set to `InsufficientInstanceCapacity`. When not defined the default behavior is to retry later."
scale\_errors: "List of AWS error codes that should trigger retry during scale up. This list replaces the module default scale-up retry errors"
enable\_organization\_runners: "Register runners to organization, instead of repo level"
enable\_runner\_binaries\_syncer: "Option to disable the lambda to sync GitHub runner distribution, useful when using a pre-build AMI."
enable\_ssm\_on\_runners: "Enable to allow access the runner instances for debugging purposes via SSM. Note that this adds additional permissions to the runner instances."
enable\_userdata: "Should the userdata script be enabled for the runner. Set this to false if you are using your own prebuilt AMI."
instance\_allocation\_strategy: "The allocation strategy for creating instances. For spot, AWS recommends `price-capacity-optimized`; for on-demand, use `lowest-price` or `prioritized`. The AWS default is `lowest-price`."
instance\_type\_priorities: "A map of instance type to priority for the `prioritized` and `capacity-optimized-prioritized` allocation strategies. Lower numbers mean higher priority. If not provided, priorities are assigned based on the order of `instance_types`."
instance\_max\_spot\_price: "Max price price for spot instances per hour. This variable will be passed to the create fleet as max spot price for the fleet."
instance\_target\_capacity\_type: "Default lifecycle used for runner instances, can be either `spot` or `on-demand`."
instance\_types: "List of instance types for the action runner. Defaults are based on runner\_os (al2023 for linux, macOS Sequoia for osx, Windows Server Core for win)."
job\_queue\_retention\_in\_seconds: "The number of seconds the job is held in the queue before it is purged"
minimum\_running\_time\_in\_minutes: "The time an ec2 action runner should be running at minimum before terminated if not busy."
pool\_runner\_owner: "The pool will deploy runners to the GitHub org ID, set this value to the org to which you want the runners deployed. Repo level is not supported."
runner\_additional\_security\_group\_ids: "List of additional security groups IDs to apply to the runner. If added outside the multi\_runner\_config block, the additional security group(s) will be applied to all runner configs. If added inside the multi\_runner\_config, the additional security group(s) will be applied to the individual runner."
runner\_as\_root: "Run the action runner under the root user. Variable `runner_run_as` will be ignored."
runner\_boot\_time\_in\_minutes: "The minimum time for an EC2 runner to boot and register as a runner."
scale\_down\_idle\_confirmation\_seconds: "Number of seconds a runner must consistently report not-busy before scale-down terminates it. GitHub's busy flag can be stale, so a single not-busy reading is not sufficient evidence a runner is idle. 0 keeps the previous single-reading behaviour."
runner\_disable\_default\_labels: "Disable default labels for the runners (os, architecture and `self-hosted`). If enabled, the runner will only have the extra labels provided in `runner_extra_labels`. In case you on own start script is used, this configuration parameter needs to be parsed via SSM."
runner\_extra\_labels: "Extra (custom) labels for the runners (GitHub). Separate each label by a comma. Labels checks on the webhook can be enforced by setting `multi_runner_config.matcherConfig.exactMatch`. GitHub read-only labels should not be provided."
runner\_group\_name: "Name of the runner group."
runner\_name\_prefix: "Prefix for the GitHub runner name."
runner\_run\_as: "Run the GitHub actions agent as user."
runners\_maximum\_count: "The maximum number of runners that will be created. Setting the variable to `-1` disables the maximum check."
scale\_down\_schedule\_expression: "Scheduler expression to check every x for scale down."
scale\_up\_reserved\_concurrent\_executions: "Amount of reserved concurrent executions for the scale-up lambda function. A value of 0 disables lambda from being triggered and -1 removes any concurrency limitations."
lambda\_event\_source\_mapping\_batch\_size: "(Optional) Maximum number of records per Lambda invocation for this runner flavor. Overrides the module-level `lambda_event_source_mapping_batch_size` when set."
lambda\_event\_source\_mapping\_maximum\_batching\_window\_in\_seconds: "(Optional) Maximum seconds to gather records before invoking Lambda for this runner flavor. Overrides the module-level `lambda_event_source_mapping_maximum_batching_window_in_seconds` when set."
userdata\_template: "Alternative user-data template, replacing the default template. By providing your own user\_data you have to take care of installing all required software, including the action runner. Variables userdata\_pre/post\_install are ignored."
enable\_jit\_config: "Overwrite the default behavior for JIT configuration. By default JIT configuration is enabled for ephemeral runners and disabled for non-ephemeral runners. In case of GHES check first if the JIT config API is available. In case you are upgrading from 3.x to 4.x you can set `enable_jit_config` to `false` to avoid a breaking change when having your own AMI."
enable\_runner\_detailed\_monitoring: "Should detailed monitoring be enabled for the runner. Set this to true if you want to use detailed monitoring. See https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-cloudwatch-new.html for details."
enable\_cloudwatch\_agent: "Enabling the cloudwatch agent on the ec2 runner instances, the runner contains default config. Configuration can be overridden via `cloudwatch_config`."
cloudwatch\_config: "(optional) Replaces the module default cloudwatch log config. See https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Agent-Configuration-File-Details.html for details."
userdata\_pre\_install: "Script to be ran before the GitHub Actions runner is installed on the EC2 instances"
userdata\_post\_install: "Script to be ran after the GitHub Actions runner is installed on the EC2 instances"
runner\_hook\_job\_started: "Script to be ran in the runner environment at the beginning of every job"
runner\_hook\_job\_completed: "Script to be ran in the runner environment at the end of every job"
runner\_ec2\_tags: "Map of tags that will be added to the launch template instance tag specifications."
runner\_iam\_role\_managed\_policy\_arns: "Attach AWS or customer-managed IAM policies (by ARN) to the runner IAM role"
vpc\_id: "The VPC for security groups of the action runners. If not set uses the value of `var.vpc_id`."
subnet\_ids: "List of subnets in which the action runners will be launched, the subnets needs to be subnets in the `vpc_id`. If not set, uses the value of `var.subnet_ids`."
idle\_config: "List of time period that can be defined as cron expression to keep a minimum amount of runners active instead of scaling down to 0. By defining this list you can ensure that in time periods that match the cron expression within 5 seconds a runner is kept idle."
license\_specifications: "Optional EC2 License Manager license configuration ARNs for the runner launch template. Required for macOS dedicated-host runners when the host resource group uses a Mac dedicated host license configuration."
use\_dedicated\_host: "Experimental! Can be removed / changed without trigger a major release. Whether to use EC2 dedicated hosts for the runners. Needed for macos runners Note that using dedicated hosts can increase cost significantly."
runner\_log\_files: "(optional) Replaces the module default cloudwatch log config. See https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Agent-Configuration-File-Details.html for details."
block\_device\_mappings: "The EC2 instance block device configuration. Takes the following keys: `device_name`, `delete_on_termination`, `volume_type`, `volume_size`, `encrypted`, `iops`, `throughput`, `kms_key_id`, `snapshot_id`, `volume_initialization_rate`."
job\_retry: "Experimental! Can be removed / changed without trigger a major release. Configure job retries. The configuration enables job retries (for ephemeral runners). After creating the instances a message will be published to a job retry queue. The job retry check lambda is checking after a delay if the job is queued. If not the message will be published again on the scale-up (build queue). Using this feature can impact the rate limit of the GitHub app."
pool\_config: "The configuration for updating the pool. The `pool_size` to adjust to by the events triggered by the `schedule_expression`. For example you can configure a cron expression for week days to adjust the pool to 10 and another expression for the weekend to adjust the pool to 1. Use `schedule_expression_timezone` to override the schedule time zone (defaults to UTC)."
ssm\_ttl\_seconds.tokens: "Optional TTL in seconds for the SSM parameters holding the runner registration token / JIT config. When set, the parameters are created with an SSM expiration policy so SSM deletes them itself after the TTL passes. Requires the Advanced parameter tier for every token parameter, which incurs additional costs. Expiration is enforced asynchronously by SSM; the SSM housekeeper lambda remains as a backstop. Must be a positive number, and should comfortably exceed the runner boot time so the config does not expire before the instance reads it."
iam\_overrides: "Allows to (optionally) override the instance profile and runner role created by the module. Set `override_instance_profile` to true and provide the `instance_profile_name` to use an existing instance profile. Set `override_runner_role` to true and provide the `runner_role_arn` to use an existing role for the runner instances."
}
# V2 contract
tags: "Tags applied to resources created for this runner configuration."
runner: "Runner settings such as the operating system, architecture, labels, hooks, runner group, name prefix, and IAM role configuration."
lambda: "Lambda settings such as runtime, architecture, networking, tags, and execution-role options for this runner configuration."
# Webhook, queue, and scale-up/scale-down orchestration settings.
orchestration\_provider: {
webhook: {
matcherConfig: "Label matching and dynamic-label policy used to route workflow jobs to this runner configuration."
runner: "Runner lifecycle settings including boot time, ephemeral mode, JIT configuration, and maximum runner count."
queue: "Build queue delay, retention, visibility timeout, redrive, and tags."
}
}
ssm: "SSM parameter paths, tags, and housekeeper settings for runner configuration storage."
observability: "Logging, tracing, and metric settings for the resources in this runner configuration."
# Compute settings for the runner provider.
compute\_provider: {
aws: {
ec2: "AWS EC2 runner settings, including AMI selection, instance types, capacity strategy, VPC and subnet placement, storage, user data, and runner access."
}
}
matcherConfig: {
labelMatchers: "The list of list of labels supported by the runner configuration. `[[self-hosted, linux, x64, example]]`"
exactMatch: "DEPRECATED: Use `bidirectionalLabelMatch` instead. If set to true all labels in the workflow job must match the GitHub labels (os, architecture and `self-hosted`). When false if __any__ workflow label matches it will trigger the webhook. Note: this only checks that workflow labels are a subset of runner labels, not the reverse."
bidirectionalLabelMatch: "If set to true, the runner labels and workflow job labels must be an exact two-way match (same set, any order, no extras or missing labels). This is stricter than `exactMatch` which only checks that workflow labels are a subset of runner labels. When false, if __any__ workflow label matches it will trigger the webhook."
priority: "If set it defines the priority of the matcher, the matcher with the lowest priority will be evaluated first. Default is 999, allowed values 0-999."
enableDynamicLabels: "Experimental! When true the dispatcher allows `ghr-*` dynamic labels for jobs routed to this runner. Default false."
awsDynamicLabelsPolicy: "Optional AWS dynamic label policy evaluated by the dispatcher. Only effective when `enableDynamicLabels = true`. Jobs whose provider dynamic labels violate every matching runner's policy are rejected with a 202 (a warning is logged). Evaluation: if `allowed_keys` is set, only those keys are accepted; keys in `blocked_keys` are always rejected (cannot be used together with `allowed_keys`); keys in `restricted_keys` are allowed only when their value passes the rule; a key not listed anywhere is allowed. Schema: `{ allowed_keys = [], blocked_keys = [], restricted_keys = { = { allowed = [globs], denied = [globs], max = number|string } } }`. Keys use the dynamic label suffix, e.g. `instance-type` for `ghr-ec2-instance-type`."
}
redrive\_build\_queue: "Set options to attach (optional) a dead letter queue to the build queue, the queue between the webhook and the scale up lambda. You have the following options. 1. Disable by setting `enabled` to false. 2. Enable by setting `enabled` to `true`, `maxReceiveCount` to a number of max retries."
} |
map(object({
# V1 contract
runner_config = optional(object({
runner_os = string
runner_architecture = string
runner_metadata_options = optional(map(any), {
instance_metadata_tags = "enabled"
http_endpoint = "enabled"
http_tokens = "required"
http_put_response_hop_limit = 1
})
ami = optional(object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
id_ssm_parameter_arn = optional(string, null)
kms_key_arn = optional(string, null)
}), null)
create_service_linked_role_spot = optional(bool, false)
credit_specification = optional(string, null)
delay_webhook_event = optional(number, 30)
disable_runner_autoupdate = optional(bool, false)
ebs_optimized = optional(bool, false)
enable_ephemeral_runners = optional(bool, false)
enable_job_queued_check = optional(bool, null)
enable_on_demand_failover_for_errors = optional(list(string), [])
scale_errors = optional(list(string), [
"UnfulfillableCapacity",
"MaxSpotInstanceCountExceeded",
"TargetCapacityLimitExceededException",
"RequestLimitExceeded",
"ResourceLimitExceeded",
"MaxSpotInstanceCountExceeded",
"MaxSpotFleetRequestCountExceeded",
"InsufficientInstanceCapacity",
"InsufficientCapacityOnHost",
])
enable_organization_runners = optional(bool, false)
enable_runner_binaries_syncer = optional(bool, true)
enable_ssm_on_runners = optional(bool, false)
enable_userdata = optional(bool, true)
instance_allocation_strategy = optional(string, "lowest-price")
instance_type_priorities = optional(map(number), null)
instance_max_spot_price = optional(string, null)
instance_target_capacity_type = optional(string, "spot")
instance_types = list(string)
job_queue_retention_in_seconds = optional(number, 86400)
minimum_running_time_in_minutes = optional(number, null)
pool_runner_owner = optional(string, null)
runner_as_root = optional(bool, false)
runner_boot_time_in_minutes = optional(number, 5)
scale_down_idle_confirmation_seconds = optional(number, 0)
runner_disable_default_labels = optional(bool, false)
runner_extra_labels = optional(list(string), [])
runner_group_name = optional(string, "Default")
runner_name_prefix = optional(string, "")
runner_run_as = optional(string, "ec2-user")
runners_maximum_count = number
runner_additional_security_group_ids = optional(list(string), [])
scale_down_schedule_expression = optional(string, "cron(*/5 * * * ? *)")
scale_up_reserved_concurrent_executions = optional(number, 1)
lambda_event_source_mapping_batch_size = optional(number, null)
lambda_event_source_mapping_maximum_batching_window_in_seconds = optional(number, null)
userdata_template = optional(string, null)
userdata_content = optional(string, null)
enable_jit_config = optional(bool, null)
enable_runner_detailed_monitoring = optional(bool, false)
enable_cloudwatch_agent = optional(bool, true)
cloudwatch_config = optional(string, null)
userdata_pre_install = optional(string, "")
userdata_post_install = optional(string, "")
runner_hook_job_started = optional(string, "")
runner_hook_job_completed = optional(string, "")
runner_ec2_tags = optional(map(string), {})
runner_iam_role_managed_policy_arns = optional(list(string), [])
vpc_id = optional(string, null)
subnet_ids = optional(list(string), null)
idle_config = optional(list(object({
cron = string
timeZone = string
idleCount = number
evictionStrategy = optional(string, "oldest_first")
})), [])
cpu_options = optional(object({
core_count = optional(number)
threads_per_core = optional(number)
amd_sev_snp = optional(string)
nested_virtualization = optional(string)
}), null)
network_interfaces = optional(list(object({
associate_carrier_ip_address = optional(bool)
associate_public_ip_address = optional(bool)
delete_on_termination = optional(bool)
description = optional(string)
device_index = optional(number)
interface_type = optional(string)
ipv4_address_count = optional(number)
ipv4_addresses = optional(list(string))
ipv4_prefix_count = optional(number)
ipv4_prefixes = optional(list(string))
ipv6_address_count = optional(number)
ipv6_addresses = optional(list(string))
ipv6_prefix_count = optional(number)
ipv6_prefixes = optional(list(string))
network_card_index = optional(number)
network_interface_id = optional(string)
primary_ipv6 = optional(bool)
private_ip_address = optional(string)
security_groups = optional(list(string))
subnet_id = optional(string)
connection_tracking_specification = optional(object({
tcp_established_timeout = optional(number)
udp_stream_timeout = optional(number)
udp_timeout = optional(number)
}))
ena_srd_specification = optional(object({
ena_srd_enabled = optional(bool)
ena_srd_udp_specification = optional(object({
ena_srd_udp_enabled = optional(bool)
}))
}))
})), [])
placement = optional(object({
affinity = optional(string)
availability_zone = optional(string)
group_id = optional(string)
group_name = optional(string)
host_id = optional(string)
host_resource_group_arn = optional(string)
spread_domain = optional(string)
tenancy = optional(string)
partition_number = optional(number)
}), null)
license_specifications = optional(list(object({
license_configuration_arn = string
})), [])
use_dedicated_host = optional(bool, false)
runner_log_files = optional(list(object({
log_group_name = string
prefix_log_group = bool
file_path = string
log_stream_name = string
log_class = optional(string, "STANDARD")
})), null)
block_device_mappings = optional(list(object({
delete_on_termination = optional(bool, true)
device_name = optional(string, "/dev/xvda")
encrypted = optional(bool, true)
iops = optional(number)
kms_key_id = optional(string)
snapshot_id = optional(string)
throughput = optional(number)
volume_initialization_rate = optional(number)
volume_size = number
volume_type = optional(string, "gp3")
})), [{
volume_size = 30
}])
pool_config = optional(list(object({
schedule_expression = string
schedule_expression_timezone = optional(string)
size = number
})), [])
ssm_ttl_seconds = optional(object({
tokens = optional(number, null)
}), {})
job_retry = optional(object({
enable = optional(bool, false)
delay_in_seconds = optional(number, 300)
delay_backoff = optional(number, 2)
lambda_memory_size = optional(number, 256)
lambda_timeout = optional(number, 30)
max_attempts = optional(number, 1)
}), {})
iam_overrides = optional(object({
override_instance_profile = optional(bool, null)
instance_profile_name = optional(string, null)
override_runner_role = optional(bool, null)
runner_role_arn = optional(string, null)
}), {
override_instance_profile = false
instance_profile_name = null
override_runner_role = false
runner_role_arn = null
})
}), null)
matcherConfig = optional(object({
labelMatchers = list(list(string))
exactMatch = optional(bool, false)
bidirectionalLabelMatch = optional(bool, false)
priority = optional(number, 999)
enableDynamicLabels = optional(bool, false)
awsDynamicLabelsPolicy = optional(any, null)
}), null)
redrive_build_queue = optional(object({
enabled = bool
maxReceiveCount = number
}), {
enabled = false
maxReceiveCount = null
})

# V2 Contract
tags = optional(map(string), {})

runner = optional(object({
os = optional(string, null)
architecture = optional(string, null)
disable_default_labels = optional(bool, null)
extra_labels = optional(list(string), null)
group_name = optional(string, null)
name_prefix = optional(string, null)
run_as_root = optional(bool, null)
run_as = optional(string, null)
auto_update_disabled = optional(bool, null)
tags = optional(map(string), {})
hooks = optional(object({
job_started = optional(string, null)
job_completed = optional(string, null)
}), {})
iam = optional(object({
role = optional(object({
arn = string
}), null)
managed_policy_arns = optional(map(string), null)
additional_trust_policy_json = optional(string, null)
path = optional(string, null)
permissions_boundary = optional(string, null)
}), {})
}), {})

lambda = optional(object({
runtime = optional(string, null)
architecture = optional(string, null)
subnet_ids = optional(list(string), null)
security_group_ids = optional(list(string), null)
tags = optional(map(string), {})
role = optional(object({
path = optional(string, null)
permissions_boundary = optional(string, null)
}), {})
}), {})

orchestration_provider = optional(object({
webhook = optional(object({
runner = optional(object({
boot_time_in_minutes = optional(number, null)
ephemeral = optional(bool, null)
jit_config_enabled = optional(bool, null)
maximum_count = optional(number, null)
}), {})
github = optional(object({
organization_runners = optional(bool, false)
}), {})
matcherConfig = optional(object({
labelMatchers = list(list(string))
exactMatch = optional(bool, false)
bidirectionalLabelMatch = optional(bool, false)
priority = optional(number, 999)
dynamic_labels_enabled = optional(bool, false)
awsDynamicLabelsPolicy = optional(object({
allowed_keys = optional(list(string), [])
blocked_keys = optional(list(string), [])
restricted_keys = optional(map(object({
allowed = optional(list(string), [])
denied = optional(list(string), [])
max = optional(string, null)
})), {})
}), null)
}), null)
queue = optional(object({
delay_webhook_event = optional(number, null)
job_queue_retention_in_seconds = optional(number, null)
visibility_timeout_seconds = optional(number, null)
redrive_build_queue = optional(object({
enabled = optional(bool, null)
maxReceiveCount = optional(number, null)
}), null)
tags = optional(map(string), {})
}), {})
lambda = optional(object({
scale = optional(object({
up = optional(object({
memory_size = optional(number, null)
timeout = optional(number, null)
reserved_concurrent_executions = optional(number, null)
job_queued_check_enabled = optional(bool, null)
event_source_mapping = optional(object({
batch_size = optional(number, null)
maximum_batching_window_in_seconds = optional(number, null)
}), {})
tags = optional(map(string), {})
}), {})
down = optional(object({
memory_size = optional(number, null)
timeout = optional(number, null)
schedule_expression = optional(string, null)
minimum_running_time_in_minutes = optional(number, null)
idle_confirmation_seconds = optional(number, null)
idle_config = optional(list(object({
cron = string
timeZone = string
idleCount = number
evictionStrategy = optional(string, "oldest_first")
})), null)
tags = optional(map(string), {})
}), {})
}), {})
pool = optional(object({
memory_size = optional(number, null)
timeout = optional(number, null)
reserved_concurrent_executions = optional(number, null)
config = optional(list(object({
schedule_expression = string
schedule_expression_timezone = optional(string)
size = number
})), null)
include_busy_runners = optional(bool, null)
runner_owner = optional(string, null)
tags = optional(map(string), {})
}), {})
}), {})
job_retry = optional(object({
enabled = optional(bool, false)
delay_in_seconds = optional(number, 300)
delay_backoff = optional(number, 2)
max_attempts = optional(number, 1)
tags = optional(map(string), {})
lambda = optional(object({
memory_size = optional(number, 256)
reserved_concurrent_executions = optional(number, 1)
timeout = optional(number, 30)
}), {})
}), {})
}), null)
}), {})

storage_provider = optional(object({
aws = optional(object({
ssm = optional(object({
ttl_seconds = optional(object({
tokens = optional(number, null)
}), {})
paths = optional(object({
root = optional(string, null)
tokens = optional(string, null)
config = optional(string, null)
}), {})
tags = optional(map(string), {})
parameters = optional(object({
tags = optional(map(string), {})
}), {})
housekeeper = optional(object({
schedule_expression = optional(string, null)
state = optional(string, null)
tags = optional(map(string), {})
lambda = optional(object({
artifact = optional(object({
zip = optional(string, null)
s3 = optional(object({
key = string
object_version = optional(string, null)
}), null)
}), {})
memory_size = optional(number, null)
timeout = optional(number, null)
}), {})
config = optional(object({
tokenPath = optional(string, null)
minimumDaysOld = optional(number, null)
dryRun = optional(bool, null)
}), {})
}), {})
}), {})
}), {})
}), {})

observability = optional(object({
logs = optional(object({
level = optional(string, null)
retention_in_days = optional(number, null)
kms_key_id = optional(string, null)
class = optional(string, null)
tags = optional(map(string), {})
}), {})
tracing = optional(object({
mode = optional(string, null)
capture_http_requests = optional(bool, null)
capture_error = optional(bool, null)
}), {})
metrics = optional(object({
enabled = optional(bool, null)
namespace = optional(string, null)
metric = optional(object({
github_app_rate_limit = optional(object({
enabled = optional(bool, null)
}), {})
job_retry = optional(object({
enabled = optional(bool, null)
}), {})
spot_termination_warning = optional(object({
enabled = optional(bool, null)
}), {})
}), {})
}), {})
}), {})

compute_provider = optional(object({
aws = optional(object({
ec2 = optional(object({
metadata_options = optional(object({
instance_metadata_tags = optional(string, "enabled")
http_endpoint = optional(string, "enabled")
http_tokens = optional(string, "required")
http_put_response_hop_limit = optional(number, 1)
}), {})
ami = optional(object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
ssm_parameter = optional(object({
path = optional(string, null)
arn = optional(string, null)
}), null)
kms_key = optional(object({
arn = string
}), null)
}), null)
block_device_mappings = optional(list(object({
delete_on_termination = optional(bool, true)
device_name = optional(string, "/dev/xvda")
encrypted = optional(bool, true)
iops = optional(number)
kms_key_id = optional(string)
snapshot_id = optional(string)
throughput = optional(number)
volume_initialization_rate = optional(number)
volume_size = number
volume_type = optional(string, "gp3")
})), [{ volume_size = 30 }])
create_service_linked_role_spot = optional(bool, false)
credit_specification = optional(string, null)
ebs_optimized = optional(bool, false)
cloudwatch_agent = optional(object({
enabled = optional(bool, true)
config = optional(string, null)
}), {})
binaries_syncer = optional(object({
enabled = optional(bool, null)
}), {})
detailed_monitoring_enabled = optional(bool, false)
ssm_enabled = optional(bool, false)
user_data = optional(object({
enabled = optional(bool, true)
template = optional(string, null)
content = optional(string, null)
pre_install = optional(string, "")
post_install = optional(string, "")
debug_logging_enabled = optional(bool, false)
}), {})
instance_allocation_strategy = optional(string, "lowest-price")
instance_max_spot_price = optional(string, null)
instance_target_capacity_type = optional(string, "spot")
instance_type_priorities = optional(map(number), null)
instance_types = optional(list(string), [])
additional_security_group_ids = optional(list(string), null)
managed_security_group_enabled = optional(bool, null)
egress_rules = optional(list(object({
cidr_blocks = list(string)
ipv6_cidr_blocks = list(string)
prefix_list_ids = list(string)
from_port = number
protocol = string
security_groups = list(string)
self = bool
to_port = number
description = string
})), null)
instance_profile_path = optional(string, null)
key_name = optional(string, null)
associate_public_ipv4_address = optional(bool, null)
instance_profile = optional(object({
name = string
}), null)
on_demand_failover_for_errors = optional(list(string), [])
scale_errors = optional(list(string), [
"UnfulfillableCapacity",
"MaxSpotInstanceCountExceeded",
"TargetCapacityLimitExceededException",
"RequestLimitExceeded",
"ResourceLimitExceeded",
"MaxSpotInstanceCountExceeded",
"MaxSpotFleetRequestCountExceeded",
"InsufficientInstanceCapacity",
"InsufficientCapacityOnHost",
])
subnet_ids = optional(list(string), null)
vpc_id = optional(string, null)
cpu_options = optional(object({
core_count = optional(number)
threads_per_core = optional(number)
amd_sev_snp = optional(string)
nested_virtualization = optional(string)
}), null)
network_interfaces = optional(list(object({
associate_carrier_ip_address = optional(bool)
associate_public_ip_address = optional(bool)
delete_on_termination = optional(bool)
description = optional(string)
device_index = optional(number)
interface_type = optional(string)
ipv4_address_count = optional(number)
ipv4_addresses = optional(list(string))
ipv4_prefix_count = optional(number)
ipv4_prefixes = optional(list(string))
ipv6_address_count = optional(number)
ipv6_addresses = optional(list(string))
ipv6_prefix_count = optional(number)
ipv6_prefixes = optional(list(string))
network_card_index = optional(number)
network_interface_id = optional(string)
primary_ipv6 = optional(bool)
private_ip_address = optional(string)
security_groups = optional(list(string))
subnet_id = optional(string)
connection_tracking_specification = optional(object({
tcp_established_timeout = optional(number)
udp_stream_timeout = optional(number)
udp_timeout = optional(number)
}))
ena_srd_specification = optional(object({
ena_srd_enabled = optional(bool)
ena_srd_udp_specification = optional(object({
ena_srd_udp_enabled = optional(bool)
}))
}))
})), [])
placement = optional(object({
affinity = optional(string)
availability_zone = optional(string)
group_id = optional(string)
group_name = optional(string)
host_id = optional(string)
host_resource_group_arn = optional(string)
spread_domain = optional(string)
tenancy = optional(string)
partition_number = optional(number)
}), null)
license_specifications = optional(list(object({
license_configuration_arn = string
})), [])
use_dedicated_host = optional(bool, false)
log_files = optional(list(object({
log_group_name = string
prefix_log_group = bool
file_path = string
log_stream_name = string
log_class = optional(string, "STANDARD")
})), null)
tags = optional(map(string), {})
}), null)
}), {})
}), {})
}))
| `{}` | no | | [parameter\_store\_tags](#input\_parameter\_store\_tags) | Map of tags that will be added to all the SSM Parameter Store parameters created by the Lambda function. | `map(string)` | `{}` | no | | [pool\_lambda\_reserved\_concurrent\_executions](#input\_pool\_lambda\_reserved\_concurrent\_executions) | Amount of reserved concurrent executions for the scale-up lambda function. A value of 0 disables lambda from being triggered and -1 removes any concurrency limitations. | `number` | `1` | no | | [pool\_lambda\_timeout](#input\_pool\_lambda\_timeout) | Time out for the pool lambda in seconds. | `number` | `60` | no | From ecb6543dacdc55aaa9d84c7b85832bbb1dcd0c64 Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Thu, 24 Sep 2026 23:26:11 +0200 Subject: [PATCH 25/44] test: fix tests to meet the new format --- .github/workflows/terraform.yml | 8 + modules/compute-providers/aws/ec2/ami.tf | 12 +- .../aws/ec2/tests/provider.tftest.hcl | 32 +++ .../compute-providers/aws/ec2/variables.tf | 4 +- .../config.experimental.resolved.tf | 15 ++ .../webhook/job-retry/iam-policies.tf | 15 ++ .../job-retry/storage-provider.aws.ssm.tf | 15 -- .../job-retry/tests/job-retry.tftest.hcl | 158 +++++++++++- .../webhook/pool/tests/provider.tftest.hcl | 171 ++++++++++++- .../tests/scale-runners.tftest.hcl | 231 ++++++++++++++++-- modules/runner-config/tests/pool.tftest.hcl | 5 + 11 files changed, 612 insertions(+), 54 deletions(-) diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml index 43b8c74f6e..d5694fc02f 100644 --- a/.github/workflows/terraform.yml +++ b/.github/workflows/terraform.yml @@ -221,6 +221,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 diff --git a/modules/compute-providers/aws/ec2/ami.tf b/modules/compute-providers/aws/ec2/ami.tf index 2595674541..fc29db329f 100644 --- a/modules/compute-providers/aws/ec2/ami.tf +++ b/modules/compute-providers/aws/ec2/ami.tf @@ -1,19 +1,23 @@ locals { - # Handle AMI configuration + # 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) - ami_id_ssm_external = try(local.ami_config.ssm_parameter.arn, 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 + + # 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 diff --git a/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl b/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl index 65289baa32..891c4a59ed 100644 --- a/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl +++ b/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl @@ -417,6 +417,14 @@ run "network_interfaces_default_matches_associate_public_ipv4_address" { variables { config = { + ami = { + filter = { state = ["available"] } + owners = ["amazon"] + ssm_parameter = { + arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/ami-id" + } + kms_key = null + } vpc_id = "vpc-12345678" subnet_ids = ["subnet-12345678"] instance_types = ["m5.large"] @@ -442,6 +450,14 @@ run "network_interfaces_accepts_explicit_configuration" { variables { config = { + ami = { + filter = { state = ["available"] } + owners = ["amazon"] + ssm_parameter = { + arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/ami-id" + } + kms_key = null + } vpc_id = "vpc-12345678" subnet_ids = ["subnet-12345678"] instance_types = ["m5.large"] @@ -479,6 +495,14 @@ run "rejects_external_instance_profile_with_managed_role" { variables { config = { + ami = { + filter = { state = ["available"] } + owners = ["amazon"] + ssm_parameter = { + arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/ami-id" + } + kms_key = null + } vpc_id = "vpc-12345678" subnet_ids = ["subnet-12345678"] instance_types = ["m5.large"] @@ -509,6 +533,14 @@ run "requires_distribution_object_when_sync_is_enabled" { variables { config = { + ami = { + filter = { state = ["available"] } + owners = ["amazon"] + ssm_parameter = { + arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/ami-id" + } + kms_key = null + } vpc_id = "vpc-12345678" subnet_ids = ["subnet-12345678"] instance_types = ["m5.large"] diff --git a/modules/compute-providers/aws/ec2/variables.tf b/modules/compute-providers/aws/ec2/variables.tf index 7681ff321d..e27ec942bf 100644 --- a/modules/compute-providers/aws/ec2/variables.tf +++ b/modules/compute-providers/aws/ec2/variables.tf @@ -125,7 +125,7 @@ variable "config" { EOT type = object({ - ami = optional(object({ + ami = object({ filter = optional(map(list(string)), { state = ["available"] }) owners = optional(list(string), ["amazon"]) ssm_parameter = optional(object({ @@ -135,7 +135,7 @@ variable "config" { kms_key = optional(object({ arn = string }), null) - }), null) + }) vpc_id = string subnet_ids = list(string) overrides = optional(object({ diff --git a/modules/multi-runner/config.experimental.resolved.tf b/modules/multi-runner/config.experimental.resolved.tf index 4041255221..c4fe8c9000 100644 --- a/modules/multi-runner/config.experimental.resolved.tf +++ b/modules/multi-runner/config.experimental.resolved.tf @@ -459,6 +459,21 @@ locals { compute_provider = { aws = { ec2 = v.compute_provider.aws.ec2 == null ? null : merge(v.compute_provider.aws.ec2, { + ami = v.compute_provider.aws.ec2.ami == null ? { + ssm_parameter = { + path = "/github-action-runners/${var.prefix}/runners/config" + } + } : merge( + v.compute_provider.aws.ec2.ami, + { + ssm_parameter = { + path = coalesce( + try(v.compute_provider.aws.ec2.ami.ssm_parameter.path, null), + "/github-action-runners/${var.prefix}/runners/config", + ) + } + } + ) vpc_id = try(coalesce( v.compute_provider.aws.ec2.vpc_id, local.normalized_config.compute_provider.aws.ec2.vpc_id, diff --git a/modules/orchestration-providers/webhook/job-retry/iam-policies.tf b/modules/orchestration-providers/webhook/job-retry/iam-policies.tf index cb3de73557..28a42b3d1b 100644 --- a/modules/orchestration-providers/webhook/job-retry/iam-policies.tf +++ b/modules/orchestration-providers/webhook/job-retry/iam-policies.tf @@ -79,4 +79,19 @@ data "aws_iam_policy_document" "job_retry" { resources = [var.config.queue.build.arn] } + + dynamic "statement" { + for_each = var.config.queue.kms_key_id == null ? [] : [var.config.queue.kms_key_id] + iterator = kms_key + + content { + sid = "WebhookJobRetryEncryptBuildQueueMessage" + effect = "Allow" + actions = [ + "kms:Decrypt", + "kms:GenerateDataKey", + ] + resources = [kms_key.value] + } + } } diff --git a/modules/orchestration-providers/webhook/job-retry/storage-provider.aws.ssm.tf b/modules/orchestration-providers/webhook/job-retry/storage-provider.aws.ssm.tf index fef13a1567..0b4de9638f 100644 --- a/modules/orchestration-providers/webhook/job-retry/storage-provider.aws.ssm.tf +++ b/modules/orchestration-providers/webhook/job-retry/storage-provider.aws.ssm.tf @@ -33,19 +33,4 @@ data "aws_iam_policy_document" "ssm_job_retry" { resources = [kms_key.value] } } - - dynamic "statement" { - for_each = var.config.queue.kms_key_id == null ? [] : [var.config.queue.kms_key_id] - iterator = kms_key - - content { - sid = "WebhookJobRetryEncryptBuildQueueMessage" - effect = "Allow" - actions = [ - "kms:Decrypt", - "kms:GenerateDataKey", - ] - resources = [kms_key.value] - } - } } diff --git a/modules/orchestration-providers/webhook/job-retry/tests/job-retry.tftest.hcl b/modules/orchestration-providers/webhook/job-retry/tests/job-retry.tftest.hcl index f54d0b4387..bfe7fa89df 100644 --- a/modules/orchestration-providers/webhook/job-retry/tests/job-retry.tftest.hcl +++ b/modules/orchestration-providers/webhook/job-retry/tests/job-retry.tftest.hcl @@ -10,7 +10,6 @@ mock_provider "aws" { arn = "arn:aws:iam::123456789012:role/job-retry-test" } } - } variables { @@ -135,6 +134,37 @@ variables { run "preserves_nested_job_retry_configuration" { command = plan + override_data { + target = data.aws_iam_policy_document.ssm_job_retry + + values = { + json = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "WebhookJobRetryReadGitHubAppParameters" + Effect = "Allow" + Action = ["ssm:GetParameter", "ssm:GetParameters"] + Resource = [ + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-id", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/key-base64", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-id-2", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/key-base64-2", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/installation-id-2", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/additional-apps-manifest", + ] + }, + { + Sid = "WebhookJobRetryDecryptParameterStore" + Effect = "Allow" + Action = ["kms:Decrypt"] + Resource = ["arn:aws:kms:eu-west-1:123456789012:key/job-retry-test"] + }, + ] + }) + } + } + assert { condition = output.lambda.function.environment[0].variables["CUSTOM_ENV"] == "preserved" error_message = "Caller-provided job-retry environment variables must be preserved." @@ -153,10 +183,22 @@ run "preserves_nested_job_retry_configuration" { && output.lambda.function.environment[0].variables["PARAMETER_GITHUB_APP_ID_NAME"] == "/github-runner/app-id" && output.lambda.function.environment[0].variables["PARAMETER_GITHUB_APP_KEY_BASE64_NAME"] == "/github-runner/key-base64" && output.lambda.function.environment[0].variables["PARAMETER_GITHUB_APPS_MANIFEST_NAME"] == "/github-runner/additional-apps-manifest" - && contains(data.aws_iam_policy_document.job_retry.statement[0].resources, "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-id-2") - && contains(data.aws_iam_policy_document.job_retry.statement[0].resources, "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/key-base64-2") - && contains(data.aws_iam_policy_document.job_retry.statement[0].resources, "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/installation-id-2") - && contains(data.aws_iam_policy_document.job_retry.statement[0].resources, "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/additional-apps-manifest") + && contains(one([ + for statement in data.aws_iam_policy_document.ssm_job_retry.statement : statement.resources + if statement.sid == "WebhookJobRetryReadGitHubAppParameters" + ]), "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-id-2") + && contains(one([ + for statement in data.aws_iam_policy_document.ssm_job_retry.statement : statement.resources + if statement.sid == "WebhookJobRetryReadGitHubAppParameters" + ]), "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/key-base64-2") + && contains(one([ + for statement in data.aws_iam_policy_document.ssm_job_retry.statement : statement.resources + if statement.sid == "WebhookJobRetryReadGitHubAppParameters" + ]), "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/installation-id-2") + && contains(one([ + for statement in data.aws_iam_policy_document.ssm_job_retry.statement : statement.resources + if statement.sid == "WebhookJobRetryReadGitHubAppParameters" + ]), "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/additional-apps-manifest") ) error_message = "Job retry must receive the new GitHub App parameter format and grant access to every corresponding SSM ARN." } @@ -185,13 +227,12 @@ run "preserves_nested_job_retry_configuration" { assert { condition = ( output.lambda.log_group.log_group_class == "INFREQUENT_ACCESS" - && length(data.aws_iam_policy_document.job_retry.statement) == 5 && one([ - for statement in data.aws_iam_policy_document.job_retry.statement : statement + for statement in data.aws_iam_policy_document.ssm_job_retry.statement : statement if statement.sid == "WebhookJobRetryDecryptParameterStore" ]).resources == toset(["arn:aws:kms:eu-west-1:123456789012:key/job-retry-test"]) && one([ - for statement in data.aws_iam_policy_document.job_retry.statement : statement + for statement in data.aws_iam_policy_document.ssm_job_retry.statement : statement if statement.sid == "WebhookJobRetryDecryptParameterStore" ]).actions == toset(["kms:Decrypt"]) && one([ @@ -226,6 +267,70 @@ run "preserves_nested_job_retry_configuration" { } +run "omits_xray_policy_when_tracing_is_disabled" { + command = plan + + variables { + config = merge(var.config, { + observability = merge(var.config.observability, { + tracing = merge(var.config.observability.tracing, { + mode = null + }) + }) + }) + } + + assert { + condition = ( + length(aws_iam_role_policy.job_retry_xray) == 0 + && length(data.aws_iam_policy_document.lambda_xray) == 0 + ) + error_message = "The job-retry X-Ray role policy must be omitted when tracing is disabled." + } +} + +run "merges_ssm_job_retry_policy" { + command = plan + + override_data { + target = data.aws_iam_policy_document.ssm_job_retry + + values = { + json = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "WebhookJobRetryReadGitHubAppParameters" + Effect = "Allow" + Action = ["ssm:GetParameter", "ssm:GetParameters"] + Resource = ["arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-id"] + }, + { + Sid = "WebhookJobRetryDecryptParameterStore" + Effect = "Allow" + Action = ["kms:Decrypt"] + Resource = ["arn:aws:kms:eu-west-1:123456789012:key/job-retry-test"] + }, + ] + }) + } + } + + assert { + condition = ( + length(data.aws_iam_policy_document.job_retry.source_policy_documents) == 1 + && data.aws_iam_policy_document.job_retry.source_policy_documents[0] == data.aws_iam_policy_document.ssm_job_retry.json + && contains([ + for statement in jsondecode(data.aws_iam_policy_document.job_retry.source_policy_documents[0]).Statement : statement.Sid + ], "WebhookJobRetryReadGitHubAppParameters") + && contains([ + for statement in jsondecode(data.aws_iam_policy_document.job_retry.source_policy_documents[0]).Statement : statement.Sid + ], "WebhookJobRetryDecryptParameterStore") + ) + error_message = "The job-retry policy must merge the SSM policy document and retain both SSM statements." + } +} + run "does_not_enable_partial_vpc_configuration" { command = plan @@ -331,7 +436,7 @@ run "does_not_enable_partial_vpc_configuration" { condition = ( length(aws_lambda_function.job_retry.vpc_config) == 0 && length(aws_iam_role_policy_attachment.job_retry_vpc_execution_role) == 0 - && length(data.aws_iam_policy_document.job_retry.statement) == 3 + && length(data.aws_iam_policy_document.job_retry.statement) == 2 && length([ for statement in data.aws_iam_policy_document.job_retry.statement : statement if contains(statement.actions, "kms:Decrypt") @@ -341,6 +446,41 @@ run "does_not_enable_partial_vpc_configuration" { } } +run "does_not_grant_ssm_permissions_when_storage_provider_is_null" { + command = plan + + variables { + storage_provider = { + aws = { + ssm = null + } + } + } + + assert { + condition = ( + length([ + for statement in data.aws_iam_policy_document.ssm_job_retry.statement : statement + if anytrue([for action in statement.actions : startswith(action, "ssm:")]) + ]) == 0 + && !contains(keys(output.lambda.function.environment[0].variables), "PARAMETER_GITHUB_APP_ID_NAME") + && !contains(keys(output.lambda.function.environment[0].variables), "PARAMETER_GITHUB_APP_KEY_BASE64_NAME") + && !contains(keys(output.lambda.function.environment[0].variables), "PARAMETER_GITHUB_APPS_MANIFEST_NAME") + && length([ + for statement in data.aws_iam_policy_document.job_retry.statement : statement + if anytrue([for action in statement.actions : startswith(action, "ssm:")]) + ]) == 0 + && !contains([ + for statement in jsondecode(data.aws_iam_policy_document.job_retry.source_policy_documents[0]).Statement : statement.Sid + ], "WebhookJobRetryReadGitHubAppParameters") + && !contains([ + for statement in jsondecode(data.aws_iam_policy_document.job_retry.source_policy_documents[0]).Statement : statement.Sid + ], "WebhookJobRetryDecryptParameterStore") + ) + error_message = "A null SSM storage provider must not expose SSM environment variables or permissions." + } +} + run "rejects_unsupported_lambda_architecture" { command = plan diff --git a/modules/orchestration-providers/webhook/pool/tests/provider.tftest.hcl b/modules/orchestration-providers/webhook/pool/tests/provider.tftest.hcl index 84c91cfef9..431c8f0a1a 100644 --- a/modules/orchestration-providers/webhook/pool/tests/provider.tftest.hcl +++ b/modules/orchestration-providers/webhook/pool/tests/provider.tftest.hcl @@ -119,6 +119,68 @@ variables { run "provider_supplies_only_compute_specific_pool_configuration" { command = plan + override_data { + target = data.aws_iam_policy_document.lambda_assume_role_policy + + values = { + json = jsonencode({ + Version = "2012-10-17" + Statement = [] + }) + } + } + + override_data { + target = data.aws_iam_policy_document.ssm_pool_common + + values = { + json = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "WebhookPoolWriteRuntimeParameters" + Effect = "Allow" + Action = ["ssm:AddTagsToResource", "ssm:PutParameter"] + Resource = [ + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/tokens", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/tokens/*", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/config", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/config/*", + ] + }, + { + Sid = "WebhookPoolReadRunnerConfigParameters" + Effect = "Allow" + Action = ["ssm:GetParameter", "ssm:GetParameters", "ssm:GetParametersByPath"] + Resource = [ + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/config", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/config/*", + ] + }, + { + Sid = "WebhookPoolReadGitHubAppParameters" + Effect = "Allow" + Action = ["ssm:GetParameter", "ssm:GetParameters"] + Resource = [ + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-id", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/key-base64", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-id-2", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/key-base64-2", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/installation-id-2", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/additional-apps-manifest", + ] + }, + { + Sid = "WebhookPoolDecryptParameterStore" + Effect = "Allow" + Action = ["kms:Decrypt"] + Resource = ["arn:aws:kms:eu-west-1:123456789012:key/pool-test"] + }, + ] + }) + } + } + assert { condition = ( length(data.aws_iam_policy_document.lambda_assume_role_policy.statement[0].principals) == 2 && @@ -146,10 +208,22 @@ run "provider_supplies_only_compute_specific_pool_configuration" { aws_lambda_function.pool.environment[0].variables["PARAMETER_GITHUB_APP_ID_NAME"] == "/github-runner/app-id" && aws_lambda_function.pool.environment[0].variables["PARAMETER_GITHUB_APP_KEY_BASE64_NAME"] == "/github-runner/key-base64" && aws_lambda_function.pool.environment[0].variables["PARAMETER_GITHUB_APPS_MANIFEST_NAME"] == "/github-runner/additional-apps-manifest" - && contains(data.aws_iam_policy_document.pool_common.statement[2].resources, "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-id-2") - && contains(data.aws_iam_policy_document.pool_common.statement[2].resources, "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/key-base64-2") - && contains(data.aws_iam_policy_document.pool_common.statement[2].resources, "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/installation-id-2") - && contains(data.aws_iam_policy_document.pool_common.statement[2].resources, "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/additional-apps-manifest") + && contains(one([ + for statement in data.aws_iam_policy_document.ssm_pool_common.statement : statement.resources + if statement.sid == "WebhookPoolReadGitHubAppParameters" + ]), "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-id-2") + && contains(one([ + for statement in data.aws_iam_policy_document.ssm_pool_common.statement : statement.resources + if statement.sid == "WebhookPoolReadGitHubAppParameters" + ]), "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/key-base64-2") + && contains(one([ + for statement in data.aws_iam_policy_document.ssm_pool_common.statement : statement.resources + if statement.sid == "WebhookPoolReadGitHubAppParameters" + ]), "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/installation-id-2") + && contains(one([ + for statement in data.aws_iam_policy_document.ssm_pool_common.statement : statement.resources + if statement.sid == "WebhookPoolReadGitHubAppParameters" + ]), "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/additional-apps-manifest") ) error_message = "Pool must receive the new GitHub App parameter format and grant access to every corresponding SSM ARN." } @@ -176,9 +250,9 @@ run "provider_supplies_only_compute_specific_pool_configuration" { assert { condition = ( - length(data.aws_iam_policy_document.pool_common.statement) == 4 + length(data.aws_iam_policy_document.ssm_pool_common.statement) == 4 && one([ - for statement in data.aws_iam_policy_document.pool_common.statement : statement + for statement in data.aws_iam_policy_document.ssm_pool_common.statement : statement if statement.sid == "WebhookPoolDecryptParameterStore" ]).resources == toset(["arn:aws:kms:eu-west-1:123456789012:key/pool-test"]) ) @@ -188,7 +262,7 @@ run "provider_supplies_only_compute_specific_pool_configuration" { assert { condition = ( one([ - for statement in data.aws_iam_policy_document.pool_common.statement : statement + for statement in data.aws_iam_policy_document.ssm_pool_common.statement : statement if statement.sid == "WebhookPoolWriteRuntimeParameters" ]).resources == toset([ "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/tokens", @@ -197,7 +271,7 @@ run "provider_supplies_only_compute_specific_pool_configuration" { "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/config/*", ]) && !contains(one([ - for statement in data.aws_iam_policy_document.pool_common.statement : statement + for statement in data.aws_iam_policy_document.ssm_pool_common.statement : statement if statement.sid == "WebhookPoolWriteRuntimeParameters" ]).resources, "*") ) @@ -227,6 +301,51 @@ run "provider_supplies_only_compute_specific_pool_configuration" { run "omits_optional_kms_statement" { command = plan + override_data { + target = data.aws_iam_policy_document.ssm_pool_common + + values = { + json = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "WebhookPoolWriteRuntimeParameters" + Effect = "Allow" + Action = ["ssm:AddTagsToResource", "ssm:PutParameter"] + Resource = [ + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/tokens", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/tokens/*", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/config", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/config/*", + ] + }, + { + Sid = "WebhookPoolReadRunnerConfigParameters" + Effect = "Allow" + Action = ["ssm:GetParameter", "ssm:GetParameters", "ssm:GetParametersByPath"] + Resource = [ + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/config", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/config/*", + ] + }, + { + Sid = "WebhookPoolReadGitHubAppParameters" + Effect = "Allow" + Action = ["ssm:GetParameter", "ssm:GetParameters"] + Resource = [ + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-id", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/key-base64", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-id-2", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/key-base64-2", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/installation-id-2", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/additional-apps-manifest", + ] + }, + ] + }) + } + } + variables { storage_provider = merge(var.storage_provider, { aws = merge(var.storage_provider.aws, { @@ -239,9 +358,9 @@ run "omits_optional_kms_statement" { assert { condition = ( - length(data.aws_iam_policy_document.pool_common.statement) == 3 + length(data.aws_iam_policy_document.ssm_pool_common.statement) == 3 && length([ - for statement in data.aws_iam_policy_document.pool_common.statement : statement + for statement in data.aws_iam_policy_document.ssm_pool_common.statement : statement if anytrue([for action in statement.actions : startswith(action, "kms:")]) ]) == 0 ) @@ -249,6 +368,38 @@ run "omits_optional_kms_statement" { } } +run "does_not_grant_ssm_permissions_when_storage_provider_is_null" { + command = plan + + variables { + storage_provider = { + aws = { + ssm = null + } + } + } + + assert { + condition = ( + length([ + for statement in data.aws_iam_policy_document.ssm_pool_common.statement : statement + if anytrue([for action in statement.actions : startswith(action, "ssm:")]) + ]) == 0 + && !contains(keys(aws_lambda_function.pool.environment[0].variables), "PARAMETER_GITHUB_APP_ID_NAME") + && !contains(keys(aws_lambda_function.pool.environment[0].variables), "PARAMETER_GITHUB_APP_KEY_BASE64_NAME") + && !contains(keys(aws_lambda_function.pool.environment[0].variables), "PARAMETER_GITHUB_APPS_MANIFEST_NAME") + && !contains(keys(aws_lambda_function.pool.environment[0].variables), "SSM_TOKEN_PATH") + && !contains(keys(aws_lambda_function.pool.environment[0].variables), "SSM_CONFIG_PATH") + && !contains(keys(aws_lambda_function.pool.environment[0].variables), "SSM_PARAMETER_STORE_TAGS") + && length([ + for statement in data.aws_iam_policy_document.pool.statement : statement + if anytrue([for action in statement.actions : startswith(action, "ssm:")]) + ]) == 0 + ) + error_message = "A null SSM storage provider must not expose SSM environment variables or permissions." + } +} + run "rejects_empty_compute_provider_type" { command = plan diff --git a/modules/orchestration-providers/webhook/scale-runners/tests/scale-runners.tftest.hcl b/modules/orchestration-providers/webhook/scale-runners/tests/scale-runners.tftest.hcl index 22b695aac2..f1e6c012db 100644 --- a/modules/orchestration-providers/webhook/scale-runners/tests/scale-runners.tftest.hcl +++ b/modules/orchestration-providers/webhook/scale-runners/tests/scale-runners.tftest.hcl @@ -10,6 +10,7 @@ mock_provider "aws" { arn = "arn:aws:iam::123456789012:role/scale-runners-test" } } + } variables { @@ -213,6 +214,92 @@ variables { run "assembles_provider_neutral_scaling_control_plane" { command = plan + override_data { + target = data.aws_iam_policy_document.lambda_assume_role + + values = { + json = jsonencode({ + Version = "2012-10-17" + Statement = [] + }) + } + } + + override_data { + target = data.aws_iam_policy_document.ssm_scale_up_common + + values = { + json = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "WebhookScaleUpWriteRuntimeParameters" + Effect = "Allow" + Action = ["ssm:PutParameter", "ssm:AddTagsToResource"] + Resource = [ + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/tokens", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/tokens/*", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/config", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/config/*", + ] + }, + { + Sid = "WebhookScaleUpReadGitHubAppAndRunnerConfigParameters" + Effect = "Allow" + Action = ["ssm:GetParameter", "ssm:GetParameters"] + Resource = [ + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/app-id", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/key-base64", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/app-id-2", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/key-base64-2", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/installation-id-2", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/additional-apps-manifest", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/config", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/config/*", + ] + }, + { + Sid = "WebhookScaleUpDecryptParameterStore" + Effect = "Allow" + Action = ["kms:Decrypt"] + Resource = ["arn:aws-us-gov:kms:us-gov-west-1:123456789012:key/scale-runners-test"] + }, + ] + }) + } + } + + override_data { + target = data.aws_iam_policy_document.ssm_scale_down_common + + values = { + json = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "WebhookScaleDownReadGitHubAppParameters" + Effect = "Allow" + Action = ["ssm:GetParameter", "ssm:GetParameters"] + Resource = [ + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/app-id", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/key-base64", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/app-id-2", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/key-base64-2", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/installation-id-2", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/additional-apps-manifest", + ] + }, + { + Sid = "WebhookScaleDownDecryptParameterStore" + Effect = "Allow" + Action = ["kms:Decrypt"] + Resource = ["arn:aws-us-gov:kms:us-gov-west-1:123456789012:key/scale-runners-test"] + }, + ] + }) + } + } + assert { condition = ( length(data.aws_iam_policy_document.lambda_assume_role.statement[0].principals) == 2 && @@ -259,10 +346,22 @@ run "assembles_provider_neutral_scaling_control_plane" { aws_lambda_function.scale_up.environment[0].variables["PARAMETER_GITHUB_APP_ID_NAME"] == "/github-runner/app-id" && aws_lambda_function.scale_down.environment[0].variables["PARAMETER_GITHUB_APP_KEY_BASE64_NAME"] == "/github-runner/key-base64" && aws_lambda_function.scale_up.environment[0].variables["PARAMETER_GITHUB_APPS_MANIFEST_NAME"] == "/github-runner/additional-apps-manifest" - && contains(data.aws_iam_policy_document.scale_up_common.statement[1].resources, "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/app-id-2") - && contains(data.aws_iam_policy_document.scale_down_common.statement[0].resources, "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/key-base64-2") - && contains(data.aws_iam_policy_document.scale_down_common.statement[0].resources, "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/installation-id-2") - && contains(data.aws_iam_policy_document.scale_up_common.statement[1].resources, "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/additional-apps-manifest") + && contains(one([ + for statement in data.aws_iam_policy_document.ssm_scale_up_common.statement : statement.resources + if statement.sid == "WebhookScaleUpReadGitHubAppAndRunnerConfigParameters" + ]), "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/app-id-2") + && contains(one([ + for statement in data.aws_iam_policy_document.ssm_scale_down_common.statement : statement.resources + if statement.sid == "WebhookScaleDownReadGitHubAppParameters" + ]), "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/key-base64-2") + && contains(one([ + for statement in data.aws_iam_policy_document.ssm_scale_down_common.statement : statement.resources + if statement.sid == "WebhookScaleDownReadGitHubAppParameters" + ]), "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/installation-id-2") + && contains(one([ + for statement in data.aws_iam_policy_document.ssm_scale_up_common.statement : statement.resources + if statement.sid == "WebhookScaleUpReadGitHubAppAndRunnerConfigParameters" + ]), "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/additional-apps-manifest") ) error_message = "Scale-up and scale-down must receive the new GitHub App parameter format and grant access to every corresponding SSM ARN." } @@ -348,10 +447,10 @@ run "assembles_provider_neutral_scaling_control_plane" { condition = ( length(data.aws_iam_policy_document.scale_up.source_policy_documents) == 2 && length(data.aws_iam_policy_document.scale_down.source_policy_documents) == 2 - && length(data.aws_iam_policy_document.scale_up_common.statement) == 5 - && length(data.aws_iam_policy_document.scale_down_common.statement) == 2 + && length(data.aws_iam_policy_document.ssm_scale_up_common.statement) == 3 + && length(data.aws_iam_policy_document.ssm_scale_down_common.statement) == 2 && one([ - for statement in data.aws_iam_policy_document.scale_up_common.statement : statement + for statement in data.aws_iam_policy_document.ssm_scale_up_common.statement : statement if statement.sid == "WebhookScaleUpDecryptParameterStore" ]).resources == toset(["arn:aws-us-gov:kms:us-gov-west-1:123456789012:key/scale-runners-test"]) && one([ @@ -363,7 +462,7 @@ run "assembles_provider_neutral_scaling_control_plane" { if statement.sid == "WebhookScaleUpDecryptBuildQueue" ]).actions == toset(["kms:Decrypt"]) && one([ - for statement in data.aws_iam_policy_document.scale_down_common.statement : statement + for statement in data.aws_iam_policy_document.ssm_scale_down_common.statement : statement if statement.sid == "WebhookScaleDownDecryptParameterStore" ]).resources == toset(["arn:aws-us-gov:kms:us-gov-west-1:123456789012:key/scale-runners-test"]) && length(data.aws_iam_policy_document.scale_up_job_retry_publish) == 1 @@ -374,7 +473,7 @@ run "assembles_provider_neutral_scaling_control_plane" { assert { condition = ( one([ - for statement in data.aws_iam_policy_document.scale_up_common.statement : statement + for statement in data.aws_iam_policy_document.ssm_scale_up_common.statement : statement if statement.sid == "WebhookScaleUpWriteRuntimeParameters" ]).resources == toset([ "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/tokens", @@ -383,7 +482,7 @@ run "assembles_provider_neutral_scaling_control_plane" { "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/config/*", ]) && !contains(one([ - for statement in data.aws_iam_policy_document.scale_up_common.statement : statement + for statement in data.aws_iam_policy_document.ssm_scale_up_common.statement : statement if statement.sid == "WebhookScaleUpWriteRuntimeParameters" ]).resources, "*") ) @@ -408,6 +507,67 @@ run "assembles_provider_neutral_scaling_control_plane" { run "omits_optional_kms_statements" { command = plan + override_data { + target = data.aws_iam_policy_document.ssm_scale_up_common + + values = { + json = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "WebhookScaleUpWriteRuntimeParameters" + Effect = "Allow" + Action = ["ssm:PutParameter", "ssm:AddTagsToResource"] + Resource = [ + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/tokens", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/tokens/*", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/config", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/config/*", + ] + }, + { + Sid = "WebhookScaleUpReadGitHubAppAndRunnerConfigParameters" + Effect = "Allow" + Action = ["ssm:GetParameter", "ssm:GetParameters"] + Resource = [ + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/app-id", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/key-base64", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/app-id-2", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/key-base64-2", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/installation-id-2", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/additional-apps-manifest", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/config", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/config/*", + ] + }, + ] + }) + } + } + + override_data { + target = data.aws_iam_policy_document.ssm_scale_down_common + + values = { + json = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Sid = "WebhookScaleDownReadGitHubAppParameters" + Effect = "Allow" + Action = ["ssm:GetParameter", "ssm:GetParameters"] + Resource = [ + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/app-id", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/key-base64", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/app-id-2", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/key-base64-2", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/installation-id-2", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/additional-apps-manifest", + ] + }] + }) + } + } + variables { config = merge(var.config, { queue = merge(var.config.queue, { @@ -425,14 +585,14 @@ run "omits_optional_kms_statements" { assert { condition = ( - length(data.aws_iam_policy_document.scale_up_common.statement) == 3 - && length(data.aws_iam_policy_document.scale_down_common.statement) == 1 + length(data.aws_iam_policy_document.ssm_scale_up_common.statement) == 2 + && length(data.aws_iam_policy_document.ssm_scale_down_common.statement) == 1 && length([ - for statement in data.aws_iam_policy_document.scale_up_common.statement : statement + for statement in data.aws_iam_policy_document.ssm_scale_up_common.statement : statement if anytrue([for action in statement.actions : startswith(action, "kms:")]) ]) == 0 && length([ - for statement in data.aws_iam_policy_document.scale_down_common.statement : statement + for statement in data.aws_iam_policy_document.ssm_scale_down_common.statement : statement if anytrue([for action in statement.actions : startswith(action, "kms:")]) ]) == 0 ) @@ -440,6 +600,49 @@ run "omits_optional_kms_statements" { } } +run "does_not_grant_ssm_permissions_when_storage_provider_is_null" { + command = plan + + variables { + storage_provider = { + aws = { + ssm = null + } + } + } + + assert { + condition = ( + length([ + for statement in data.aws_iam_policy_document.ssm_scale_up_common.statement : statement + if anytrue([for action in statement.actions : startswith(action, "ssm:")]) + ]) == 0 + && length([ + for statement in data.aws_iam_policy_document.ssm_scale_down_common.statement : statement + if anytrue([for action in statement.actions : startswith(action, "ssm:")]) + ]) == 0 + && !contains(keys(aws_lambda_function.scale_up.environment[0].variables), "PARAMETER_GITHUB_APP_ID_NAME") + && !contains(keys(aws_lambda_function.scale_up.environment[0].variables), "PARAMETER_GITHUB_APP_KEY_BASE64_NAME") + && !contains(keys(aws_lambda_function.scale_up.environment[0].variables), "PARAMETER_GITHUB_APPS_MANIFEST_NAME") + && !contains(keys(aws_lambda_function.scale_up.environment[0].variables), "SSM_TOKEN_PATH") + && !contains(keys(aws_lambda_function.scale_up.environment[0].variables), "SSM_CONFIG_PATH") + && !contains(keys(aws_lambda_function.scale_down.environment[0].variables), "PARAMETER_GITHUB_APP_ID_NAME") + && !contains(keys(aws_lambda_function.scale_down.environment[0].variables), "PARAMETER_GITHUB_APP_KEY_BASE64_NAME") + && !contains(keys(aws_lambda_function.scale_down.environment[0].variables), "PARAMETER_GITHUB_APPS_MANIFEST_NAME") + && !contains(keys(aws_lambda_function.scale_down.environment[0].variables), "SSM_TOKEN_PATH") + && length([ + for statement in data.aws_iam_policy_document.scale_up.statement : statement + if anytrue([for action in statement.actions : startswith(action, "ssm:")]) + ]) == 0 + && length([ + for statement in data.aws_iam_policy_document.scale_down.statement : statement + if anytrue([for action in statement.actions : startswith(action, "ssm:")]) + ]) == 0 + ) + error_message = "A null SSM storage provider must not expose SSM environment variables or permissions." + } +} + run "requires_job_retry_queue_when_enabled" { command = plan diff --git a/modules/runner-config/tests/pool.tftest.hcl b/modules/runner-config/tests/pool.tftest.hcl index ba3a76a317..f5619d5394 100644 --- a/modules/runner-config/tests/pool.tftest.hcl +++ b/modules/runner-config/tests/pool.tftest.hcl @@ -493,6 +493,11 @@ run "external_runner_role_and_profile_remain_external" { compute_provider = { aws = { ec2 = { + ami = { + ssm_parameter = { + path = "/github-runner/provider-test/ami" + } + } vpc_id = "vpc-12345678" subnet_ids = ["subnet-12345678"] instance_types = ["m5.large"] From d4e54e3c0a19b6467921b4d1eaff2739f93d20bf Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Thu, 24 Sep 2026 23:47:48 +0200 Subject: [PATCH 26/44] fix: fix tests --- .github/workflows/ministack.yml | 2 +- .github/workflows/smoke-tests.yml | 2 +- .github/workflows/terraform.yml | 8 + .../config.experimental.resolved.tf | 25 +- .../job-retry/tests/job-retry.tftest.hcl | 86 +++--- .../webhook/pool/tests/provider.tftest.hcl | 196 +++++++------ .../tests/scale-runners.tftest.hcl | 265 ++++++++++-------- .../runner-config-housekeeper/housekeeper.tf | 8 +- 8 files changed, 335 insertions(+), 257 deletions(-) diff --git a/.github/workflows/ministack.yml b/.github/workflows/ministack.yml index 717fdf5dc3..508f46c868 100644 --- a/.github/workflows/ministack.yml +++ b/.github/workflows/ministack.yml @@ -57,7 +57,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: diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml index 5b519480c3..1ba1eef486 100644 --- a/.github/workflows/smoke-tests.yml +++ b/.github/workflows/smoke-tests.yml @@ -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 diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml index d5694fc02f..b53d31553b 100644 --- a/.github/workflows/terraform.yml +++ b/.github/workflows/terraform.yml @@ -337,6 +337,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 diff --git a/modules/multi-runner/config.experimental.resolved.tf b/modules/multi-runner/config.experimental.resolved.tf index c4fe8c9000..2df0b4e177 100644 --- a/modules/multi-runner/config.experimental.resolved.tf +++ b/modules/multi-runner/config.experimental.resolved.tf @@ -459,21 +459,20 @@ locals { compute_provider = { aws = { ec2 = v.compute_provider.aws.ec2 == null ? null : merge(v.compute_provider.aws.ec2, { - ami = v.compute_provider.aws.ec2.ami == null ? { + ami = { + filter = try(v.compute_provider.aws.ec2.ami.filter, { state = ["available"] }) + owners = try(v.compute_provider.aws.ec2.ami.owners, ["amazon"]) + ssm_parameter = { - path = "/github-action-runners/${var.prefix}/runners/config" - } - } : merge( - v.compute_provider.aws.ec2.ami, - { - ssm_parameter = { - path = coalesce( - try(v.compute_provider.aws.ec2.ami.ssm_parameter.path, null), - "/github-action-runners/${var.prefix}/runners/config", - ) - } + path = coalesce( + try(v.compute_provider.aws.ec2.ami.ssm_parameter.path, null), + "/github-action-runners/${var.prefix}/runners/config", + ) + arn = try(v.compute_provider.aws.ec2.ami.ssm_parameter.arn, null) } - ) + + kms_key = try(v.compute_provider.aws.ec2.ami.kms_key, null) + } vpc_id = try(coalesce( v.compute_provider.aws.ec2.vpc_id, local.normalized_config.compute_provider.aws.ec2.vpc_id, diff --git a/modules/orchestration-providers/webhook/job-retry/tests/job-retry.tftest.hcl b/modules/orchestration-providers/webhook/job-retry/tests/job-retry.tftest.hcl index bfe7fa89df..e72b4b5484 100644 --- a/modules/orchestration-providers/webhook/job-retry/tests/job-retry.tftest.hcl +++ b/modules/orchestration-providers/webhook/job-retry/tests/job-retry.tftest.hcl @@ -138,30 +138,32 @@ run "preserves_nested_job_retry_configuration" { target = data.aws_iam_policy_document.ssm_job_retry values = { - json = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Sid = "WebhookJobRetryReadGitHubAppParameters" - Effect = "Allow" - Action = ["ssm:GetParameter", "ssm:GetParameters"] - Resource = [ - "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-id", - "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/key-base64", - "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-id-2", - "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/key-base64-2", - "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/installation-id-2", - "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/additional-apps-manifest", - ] - }, - { - Sid = "WebhookJobRetryDecryptParameterStore" - Effect = "Allow" - Action = ["kms:Decrypt"] - Resource = ["arn:aws:kms:eu-west-1:123456789012:key/job-retry-test"] - }, - ] - }) + json = <<-JSON + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "WebhookJobRetryReadGitHubAppParameters", + "Effect": "Allow", + "Action": ["ssm:GetParameter", "ssm:GetParameters"], + "Resource": [ + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-id", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/key-base64", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-id-2", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/key-base64-2", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/installation-id-2", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/additional-apps-manifest" + ] + }, + { + "Sid": "WebhookJobRetryDecryptParameterStore", + "Effect": "Allow", + "Action": ["kms:Decrypt"], + "Resource": ["arn:aws:kms:eu-west-1:123456789012:key/job-retry-test"] + } + ] + } + JSON } } @@ -296,23 +298,25 @@ run "merges_ssm_job_retry_policy" { target = data.aws_iam_policy_document.ssm_job_retry values = { - json = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Sid = "WebhookJobRetryReadGitHubAppParameters" - Effect = "Allow" - Action = ["ssm:GetParameter", "ssm:GetParameters"] - Resource = ["arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-id"] - }, - { - Sid = "WebhookJobRetryDecryptParameterStore" - Effect = "Allow" - Action = ["kms:Decrypt"] - Resource = ["arn:aws:kms:eu-west-1:123456789012:key/job-retry-test"] - }, - ] - }) + json = <<-JSON + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "WebhookJobRetryReadGitHubAppParameters", + "Effect": "Allow", + "Action": ["ssm:GetParameter", "ssm:GetParameters"], + "Resource": ["arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-id"] + }, + { + "Sid": "WebhookJobRetryDecryptParameterStore", + "Effect": "Allow", + "Action": ["kms:Decrypt"], + "Resource": ["arn:aws:kms:eu-west-1:123456789012:key/job-retry-test"] + } + ] + } + JSON } } diff --git a/modules/orchestration-providers/webhook/pool/tests/provider.tftest.hcl b/modules/orchestration-providers/webhook/pool/tests/provider.tftest.hcl index 431c8f0a1a..06868aa78c 100644 --- a/modules/orchestration-providers/webhook/pool/tests/provider.tftest.hcl +++ b/modules/orchestration-providers/webhook/pool/tests/provider.tftest.hcl @@ -123,10 +123,9 @@ run "provider_supplies_only_compute_specific_pool_configuration" { target = data.aws_iam_policy_document.lambda_assume_role_policy values = { - json = jsonencode({ - Version = "2012-10-17" - Statement = [] - }) + json = <<-JSON + {"Version":"2012-10-17","Statement":[]} + JSON } } @@ -134,50 +133,52 @@ run "provider_supplies_only_compute_specific_pool_configuration" { target = data.aws_iam_policy_document.ssm_pool_common values = { - json = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Sid = "WebhookPoolWriteRuntimeParameters" - Effect = "Allow" - Action = ["ssm:AddTagsToResource", "ssm:PutParameter"] - Resource = [ - "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/tokens", - "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/tokens/*", - "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/config", - "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/config/*", - ] - }, - { - Sid = "WebhookPoolReadRunnerConfigParameters" - Effect = "Allow" - Action = ["ssm:GetParameter", "ssm:GetParameters", "ssm:GetParametersByPath"] - Resource = [ - "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/config", - "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/config/*", - ] - }, - { - Sid = "WebhookPoolReadGitHubAppParameters" - Effect = "Allow" - Action = ["ssm:GetParameter", "ssm:GetParameters"] - Resource = [ - "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-id", - "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/key-base64", - "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-id-2", - "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/key-base64-2", - "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/installation-id-2", - "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/additional-apps-manifest", - ] - }, - { - Sid = "WebhookPoolDecryptParameterStore" - Effect = "Allow" - Action = ["kms:Decrypt"] - Resource = ["arn:aws:kms:eu-west-1:123456789012:key/pool-test"] - }, - ] - }) + json = <<-JSON + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "WebhookPoolWriteRuntimeParameters", + "Effect": "Allow", + "Action": ["ssm:AddTagsToResource", "ssm:PutParameter"], + "Resource": [ + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/tokens", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/tokens/*", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/config", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/config/*" + ] + }, + { + "Sid": "WebhookPoolReadRunnerConfigParameters", + "Effect": "Allow", + "Action": ["ssm:GetParameter", "ssm:GetParameters", "ssm:GetParametersByPath"], + "Resource": [ + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/config", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/config/*" + ] + }, + { + "Sid": "WebhookPoolReadGitHubAppParameters", + "Effect": "Allow", + "Action": ["ssm:GetParameter", "ssm:GetParameters"], + "Resource": [ + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-id", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/key-base64", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-id-2", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/key-base64-2", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/installation-id-2", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/additional-apps-manifest" + ] + }, + { + "Sid": "WebhookPoolDecryptParameterStore", + "Effect": "Allow", + "Action": ["kms:Decrypt"], + "Resource": ["arn:aws:kms:eu-west-1:123456789012:key/pool-test"] + } + ] + } + JSON } } @@ -248,6 +249,21 @@ run "provider_supplies_only_compute_specific_pool_configuration" { error_message = "The pool role policy must merge the common and compute-provider policy documents." } + assert { + condition = ( + length(data.aws_iam_policy_document.pool_common.source_policy_documents) == 1 + && data.aws_iam_policy_document.pool_common.source_policy_documents[0] == data.aws_iam_policy_document.ssm_pool_common.json + && data.aws_iam_policy_document.pool.source_policy_documents[0] == data.aws_iam_policy_document.pool_common.json + && contains([ + for statement in jsondecode(data.aws_iam_policy_document.pool_common.source_policy_documents[0]).Statement : statement.Sid + ], "WebhookPoolReadGitHubAppParameters") + && contains([ + for statement in jsondecode(data.aws_iam_policy_document.pool_common.source_policy_documents[0]).Statement : statement.Sid + ], "WebhookPoolDecryptParameterStore") + ) + error_message = "The pool final policy must retain the SSM source policy and its SSM statements." + } + assert { condition = ( length(data.aws_iam_policy_document.ssm_pool_common.statement) == 4 @@ -305,44 +321,46 @@ run "omits_optional_kms_statement" { target = data.aws_iam_policy_document.ssm_pool_common values = { - json = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Sid = "WebhookPoolWriteRuntimeParameters" - Effect = "Allow" - Action = ["ssm:AddTagsToResource", "ssm:PutParameter"] - Resource = [ - "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/tokens", - "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/tokens/*", - "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/config", - "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/config/*", - ] - }, - { - Sid = "WebhookPoolReadRunnerConfigParameters" - Effect = "Allow" - Action = ["ssm:GetParameter", "ssm:GetParameters", "ssm:GetParametersByPath"] - Resource = [ - "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/config", - "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/config/*", - ] - }, - { - Sid = "WebhookPoolReadGitHubAppParameters" - Effect = "Allow" - Action = ["ssm:GetParameter", "ssm:GetParameters"] - Resource = [ - "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-id", - "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/key-base64", - "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-id-2", - "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/key-base64-2", - "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/installation-id-2", - "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/additional-apps-manifest", - ] - }, - ] - }) + json = <<-JSON + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "WebhookPoolWriteRuntimeParameters", + "Effect": "Allow", + "Action": ["ssm:AddTagsToResource", "ssm:PutParameter"], + "Resource": [ + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/tokens", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/tokens/*", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/config", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/config/*" + ] + }, + { + "Sid": "WebhookPoolReadRunnerConfigParameters", + "Effect": "Allow", + "Action": ["ssm:GetParameter", "ssm:GetParameters", "ssm:GetParametersByPath"], + "Resource": [ + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/config", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/config/*" + ] + }, + { + "Sid": "WebhookPoolReadGitHubAppParameters", + "Effect": "Allow", + "Action": ["ssm:GetParameter", "ssm:GetParameters"], + "Resource": [ + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-id", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/key-base64", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-id-2", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/key-base64-2", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/installation-id-2", + "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/additional-apps-manifest" + ] + } + ] + } + JSON } } @@ -395,6 +413,12 @@ run "does_not_grant_ssm_permissions_when_storage_provider_is_null" { for statement in data.aws_iam_policy_document.pool.statement : statement if anytrue([for action in statement.actions : startswith(action, "ssm:")]) ]) == 0 + && !contains([ + for statement in jsondecode(data.aws_iam_policy_document.pool_common.source_policy_documents[0]).Statement : statement.Sid + ], "WebhookPoolReadGitHubAppParameters") + && !contains([ + for statement in jsondecode(data.aws_iam_policy_document.pool_common.source_policy_documents[0]).Statement : statement.Sid + ], "WebhookPoolDecryptParameterStore") ) error_message = "A null SSM storage provider must not expose SSM environment variables or permissions." } diff --git a/modules/orchestration-providers/webhook/scale-runners/tests/scale-runners.tftest.hcl b/modules/orchestration-providers/webhook/scale-runners/tests/scale-runners.tftest.hcl index f1e6c012db..dbbef4a5f6 100644 --- a/modules/orchestration-providers/webhook/scale-runners/tests/scale-runners.tftest.hcl +++ b/modules/orchestration-providers/webhook/scale-runners/tests/scale-runners.tftest.hcl @@ -218,10 +218,9 @@ run "assembles_provider_neutral_scaling_control_plane" { target = data.aws_iam_policy_document.lambda_assume_role values = { - json = jsonencode({ - Version = "2012-10-17" - Statement = [] - }) + json = <<-JSON + {"Version":"2012-10-17","Statement":[]} + JSON } } @@ -229,43 +228,45 @@ run "assembles_provider_neutral_scaling_control_plane" { target = data.aws_iam_policy_document.ssm_scale_up_common values = { - json = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Sid = "WebhookScaleUpWriteRuntimeParameters" - Effect = "Allow" - Action = ["ssm:PutParameter", "ssm:AddTagsToResource"] - Resource = [ - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/tokens", - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/tokens/*", - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/config", - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/config/*", - ] - }, - { - Sid = "WebhookScaleUpReadGitHubAppAndRunnerConfigParameters" - Effect = "Allow" - Action = ["ssm:GetParameter", "ssm:GetParameters"] - Resource = [ - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/app-id", - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/key-base64", - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/app-id-2", - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/key-base64-2", - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/installation-id-2", - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/additional-apps-manifest", - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/config", - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/config/*", - ] - }, - { - Sid = "WebhookScaleUpDecryptParameterStore" - Effect = "Allow" - Action = ["kms:Decrypt"] - Resource = ["arn:aws-us-gov:kms:us-gov-west-1:123456789012:key/scale-runners-test"] - }, - ] - }) + json = <<-JSON + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "WebhookScaleUpWriteRuntimeParameters", + "Effect": "Allow", + "Action": ["ssm:PutParameter", "ssm:AddTagsToResource"], + "Resource": [ + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/tokens", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/tokens/*", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/config", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/config/*" + ] + }, + { + "Sid": "WebhookScaleUpReadGitHubAppAndRunnerConfigParameters", + "Effect": "Allow", + "Action": ["ssm:GetParameter", "ssm:GetParameters"], + "Resource": [ + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/app-id", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/key-base64", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/app-id-2", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/key-base64-2", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/installation-id-2", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/additional-apps-manifest", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/config", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/config/*" + ] + }, + { + "Sid": "WebhookScaleUpDecryptParameterStore", + "Effect": "Allow", + "Action": ["kms:Decrypt"], + "Resource": ["arn:aws-us-gov:kms:us-gov-west-1:123456789012:key/scale-runners-test"] + } + ] + } + JSON } } @@ -273,30 +274,32 @@ run "assembles_provider_neutral_scaling_control_plane" { target = data.aws_iam_policy_document.ssm_scale_down_common values = { - json = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Sid = "WebhookScaleDownReadGitHubAppParameters" - Effect = "Allow" - Action = ["ssm:GetParameter", "ssm:GetParameters"] - Resource = [ - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/app-id", - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/key-base64", - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/app-id-2", - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/key-base64-2", - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/installation-id-2", - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/additional-apps-manifest", - ] - }, - { - Sid = "WebhookScaleDownDecryptParameterStore" - Effect = "Allow" - Action = ["kms:Decrypt"] - Resource = ["arn:aws-us-gov:kms:us-gov-west-1:123456789012:key/scale-runners-test"] - }, - ] - }) + json = <<-JSON + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "WebhookScaleDownReadGitHubAppParameters", + "Effect": "Allow", + "Action": ["ssm:GetParameter", "ssm:GetParameters"], + "Resource": [ + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/app-id", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/key-base64", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/app-id-2", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/key-base64-2", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/installation-id-2", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/additional-apps-manifest" + ] + }, + { + "Sid": "WebhookScaleDownDecryptParameterStore", + "Effect": "Allow", + "Action": ["kms:Decrypt"], + "Resource": ["arn:aws-us-gov:kms:us-gov-west-1:123456789012:key/scale-runners-test"] + } + ] + } + JSON } } @@ -470,6 +473,28 @@ run "assembles_provider_neutral_scaling_control_plane" { error_message = "Common, provider, distinct Parameter Store/build-queue KMS, and retry IAM fragments must retain their conditional plan shape." } + assert { + condition = ( + length(data.aws_iam_policy_document.scale_up_common.source_policy_documents) == 1 + && data.aws_iam_policy_document.scale_up_common.source_policy_documents[0] == data.aws_iam_policy_document.ssm_scale_up_common.json + && data.aws_iam_policy_document.scale_up.source_policy_documents[0] == data.aws_iam_policy_document.scale_up_common.json + && data.aws_iam_policy_document.scale_down.source_policy_documents[0] == data.aws_iam_policy_document.ssm_scale_down_common.json + && contains([ + for statement in jsondecode(data.aws_iam_policy_document.scale_up_common.source_policy_documents[0]).Statement : statement.Sid + ], "WebhookScaleUpReadGitHubAppAndRunnerConfigParameters") + && contains([ + for statement in jsondecode(data.aws_iam_policy_document.scale_down.source_policy_documents[0]).Statement : statement.Sid + ], "WebhookScaleDownReadGitHubAppParameters") + && contains([ + for statement in jsondecode(data.aws_iam_policy_document.scale_up_common.source_policy_documents[0]).Statement : statement.Sid + ], "WebhookScaleUpDecryptParameterStore") + && contains([ + for statement in jsondecode(data.aws_iam_policy_document.scale_down.source_policy_documents[0]).Statement : statement.Sid + ], "WebhookScaleDownDecryptParameterStore") + ) + error_message = "The final scale-up and scale-down policies must retain their SSM source policies and statements." + } + assert { condition = ( one([ @@ -511,37 +536,39 @@ run "omits_optional_kms_statements" { target = data.aws_iam_policy_document.ssm_scale_up_common values = { - json = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Sid = "WebhookScaleUpWriteRuntimeParameters" - Effect = "Allow" - Action = ["ssm:PutParameter", "ssm:AddTagsToResource"] - Resource = [ - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/tokens", - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/tokens/*", - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/config", - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/config/*", - ] - }, - { - Sid = "WebhookScaleUpReadGitHubAppAndRunnerConfigParameters" - Effect = "Allow" - Action = ["ssm:GetParameter", "ssm:GetParameters"] - Resource = [ - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/app-id", - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/key-base64", - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/app-id-2", - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/key-base64-2", - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/installation-id-2", - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/additional-apps-manifest", - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/config", - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/config/*", - ] - }, - ] - }) + json = <<-JSON + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "WebhookScaleUpWriteRuntimeParameters", + "Effect": "Allow", + "Action": ["ssm:PutParameter", "ssm:AddTagsToResource"], + "Resource": [ + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/tokens", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/tokens/*", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/config", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/config/*" + ] + }, + { + "Sid": "WebhookScaleUpReadGitHubAppAndRunnerConfigParameters", + "Effect": "Allow", + "Action": ["ssm:GetParameter", "ssm:GetParameters"], + "Resource": [ + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/app-id", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/key-base64", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/app-id-2", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/key-base64-2", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/installation-id-2", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/additional-apps-manifest", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/config", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/config/*" + ] + } + ] + } + JSON } } @@ -549,22 +576,26 @@ run "omits_optional_kms_statements" { target = data.aws_iam_policy_document.ssm_scale_down_common values = { - json = jsonencode({ - Version = "2012-10-17" - Statement = [{ - Sid = "WebhookScaleDownReadGitHubAppParameters" - Effect = "Allow" - Action = ["ssm:GetParameter", "ssm:GetParameters"] - Resource = [ - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/app-id", - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/key-base64", - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/app-id-2", - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/key-base64-2", - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/installation-id-2", - "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/additional-apps-manifest", + json = <<-JSON + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "WebhookScaleDownReadGitHubAppParameters", + "Effect": "Allow", + "Action": ["ssm:GetParameter", "ssm:GetParameters"], + "Resource": [ + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/app-id", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/key-base64", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/app-id-2", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/key-base64-2", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/installation-id-2", + "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/additional-apps-manifest" + ] + } ] - }] - }) + } + JSON } } @@ -638,6 +669,18 @@ run "does_not_grant_ssm_permissions_when_storage_provider_is_null" { for statement in data.aws_iam_policy_document.scale_down.statement : statement if anytrue([for action in statement.actions : startswith(action, "ssm:")]) ]) == 0 + && !contains([ + for statement in jsondecode(data.aws_iam_policy_document.scale_up_common.source_policy_documents[0]).Statement : statement.Sid + ], "WebhookScaleUpReadGitHubAppAndRunnerConfigParameters") + && !contains([ + for statement in jsondecode(data.aws_iam_policy_document.scale_down.source_policy_documents[0]).Statement : statement.Sid + ], "WebhookScaleDownReadGitHubAppParameters") + && !contains([ + for statement in jsondecode(data.aws_iam_policy_document.scale_up_common.source_policy_documents[0]).Statement : statement.Sid + ], "WebhookScaleUpDecryptParameterStore") + && !contains([ + for statement in jsondecode(data.aws_iam_policy_document.scale_down.source_policy_documents[0]).Statement : statement.Sid + ], "WebhookScaleDownDecryptParameterStore") ) error_message = "A null SSM storage provider must not expose SSM environment variables or permissions." } diff --git a/modules/runner-config/runner-config-housekeeper/housekeeper.tf b/modules/runner-config/runner-config-housekeeper/housekeeper.tf index 9becdb2b3e..4682f63836 100644 --- a/modules/runner-config/runner-config-housekeeper/housekeeper.tf +++ b/modules/runner-config/runner-config-housekeeper/housekeeper.tf @@ -13,7 +13,7 @@ locals { common_environment_variables = { ENVIRONMENT = var.config.prefix LOG_LEVEL = upper(var.config.observability.logs.level) - POWERTOOLS_SERVICE_NAME = "${var.config.prefix}-rc-housekeeper" + POWERTOOLS_SERVICE_NAME = "${var.config.prefix}-ssm-housekeeper" POWERTOOLS_TRACE_ENABLED = var.config.observability.tracing.mode != null POWERTOOLS_TRACER_CAPTURE_HTTPS_REQUESTS = var.config.observability.tracing.capture_http_requests POWERTOOLS_TRACER_CAPTURE_ERROR = var.config.observability.tracing.capture_error @@ -32,7 +32,7 @@ resource "aws_lambda_function" "housekeeper" { s3_object_version = var.config.lambda.artifact.s3.object_version filename = var.config.lambda.artifact.s3.bucket == null ? var.config.lambda.artifact.zip : null source_code_hash = var.config.lambda.artifact.s3.bucket == null ? filebase64sha256(var.config.lambda.artifact.zip) : null - function_name = "${var.config.prefix}-rc-housekeeper" + function_name = "${var.config.prefix}-ssm-housekeeper" role = aws_iam_role.housekeeper.arn handler = "index.runnerConfigHousekeeper" runtime = var.config.lambda.runtime @@ -72,7 +72,7 @@ resource "aws_cloudwatch_log_group" "housekeeper" { } resource "aws_cloudwatch_event_rule" "housekeeper" { - name = "${var.config.prefix}-rc-housekeeper" + name = "${var.config.prefix}-ssm-housekeeper" schedule_expression = var.config.schedule.expression state = var.config.schedule.state tags = var.config.tags.resources @@ -92,7 +92,7 @@ resource "aws_lambda_permission" "housekeeper" { } resource "aws_iam_role" "housekeeper" { - name = "${substr("${var.config.prefix}-rc-hk-lambda", 0, 54)}-${substr(md5("${var.config.prefix}-rc-hk-lambda"), 0, 8)}" + name = "${substr("${var.config.prefix}-ssm-hk-lambda", 0, 54)}-${substr(md5("${var.config.prefix}-ssm-hk-lambda"), 0, 8)}" description = "Lambda role for Runner Config Housekeeper (${var.config.prefix})" assume_role_policy = data.aws_iam_policy_document.lambda_assume_role.json path = var.config.lambda.role.path From 8469d581114cb5746562400bf23e1e4c8acb925f Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Fri, 25 Sep 2026 00:09:08 +0200 Subject: [PATCH 27/44] test: fix tests --- .../webhook/pool/tests/provider.tftest.hcl | 7 +------ .../runner-config/runner-config-housekeeper/housekeeper.tf | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/modules/orchestration-providers/webhook/pool/tests/provider.tftest.hcl b/modules/orchestration-providers/webhook/pool/tests/provider.tftest.hcl index 06868aa78c..8518a52d8e 100644 --- a/modules/orchestration-providers/webhook/pool/tests/provider.tftest.hcl +++ b/modules/orchestration-providers/webhook/pool/tests/provider.tftest.hcl @@ -413,12 +413,7 @@ run "does_not_grant_ssm_permissions_when_storage_provider_is_null" { for statement in data.aws_iam_policy_document.pool.statement : statement if anytrue([for action in statement.actions : startswith(action, "ssm:")]) ]) == 0 - && !contains([ - for statement in jsondecode(data.aws_iam_policy_document.pool_common.source_policy_documents[0]).Statement : statement.Sid - ], "WebhookPoolReadGitHubAppParameters") - && !contains([ - for statement in jsondecode(data.aws_iam_policy_document.pool_common.source_policy_documents[0]).Statement : statement.Sid - ], "WebhookPoolDecryptParameterStore") + && length(regexall("ssm:", data.aws_iam_policy_document.pool_common.source_policy_documents[0])) == 0 ) error_message = "A null SSM storage provider must not expose SSM environment variables or permissions." } diff --git a/modules/runner-config/runner-config-housekeeper/housekeeper.tf b/modules/runner-config/runner-config-housekeeper/housekeeper.tf index 4682f63836..9df294e1e7 100644 --- a/modules/runner-config/runner-config-housekeeper/housekeeper.tf +++ b/modules/runner-config/runner-config-housekeeper/housekeeper.tf @@ -93,7 +93,7 @@ resource "aws_lambda_permission" "housekeeper" { resource "aws_iam_role" "housekeeper" { name = "${substr("${var.config.prefix}-ssm-hk-lambda", 0, 54)}-${substr(md5("${var.config.prefix}-ssm-hk-lambda"), 0, 8)}" - description = "Lambda role for Runner Config Housekeeper (${var.config.prefix})" + description = "Lambda role for SSM Housekeeper (${var.config.prefix})" assume_role_policy = data.aws_iam_policy_document.lambda_assume_role.json path = var.config.lambda.role.path permissions_boundary = var.config.lambda.role.permissions_boundary From ccaba6587ebc812e560ced24d2f75239fe3de658 Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Fri, 25 Sep 2026 00:46:40 +0200 Subject: [PATCH 28/44] test: fix tests in tofu --- .pre-commit-config.yaml | 10 +++- .../tests/fixtures/base-inputs/main.tf | 21 +++++++++ .../job-retry/tests/job-retry.tftest.hcl | 46 ++++++++++++++----- .../pool/tests/fixtures/base-inputs/main.tf | 21 +++++++++ .../webhook/pool/tests/provider.tftest.hcl | 32 ++++++++++--- .../tests/fixtures/base-inputs/main.tf | 21 +++++++++ .../tests/scale-runners.tftest.hcl | 34 +++++++++++--- .../tests/fixtures/base-inputs/main.tf | 31 +++++++++++++ .../webhook/tests/webhook.tftest.hcl | 34 ++++++++++++-- .../tests/computed-iam-inputs.tftest.hcl | 32 +++++++++++++ modules/runner-config/tests/pool.tftest.hcl | 26 +++++++++++ modules/runner-config/tests/tags.tftest.hcl | 26 +++++++++++ 12 files changed, 304 insertions(+), 30 deletions(-) create mode 100644 modules/orchestration-providers/webhook/job-retry/tests/fixtures/base-inputs/main.tf create mode 100644 modules/orchestration-providers/webhook/pool/tests/fixtures/base-inputs/main.tf create mode 100644 modules/orchestration-providers/webhook/scale-runners/tests/fixtures/base-inputs/main.tf create mode 100644 modules/orchestration-providers/webhook/tests/fixtures/base-inputs/main.tf diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b0f72307ef..c3cd0b83ea 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/antonbabenko/pre-commit-terraform - rev: v1.96.2 + rev: 581213484f4262600d17c71e272176d4eb6724a5 # frozen: v1.109.0 hooks: - id: terraform_fmt - id: terraform_tflint @@ -14,6 +14,12 @@ repos: - --tf-init-args=-backend=false - --hook-config=--tf-path=terraform - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.0.0 hooks: - id: check-merge-conflict + - repo: https://github.com/hadolint/hadolint + rev: 2eece55955ced00200be9729e9728cb7dacca505 # frozen: v2.15.1 + hooks: + - id: hadolint + name: Docker · Linter + exclude: (docs/operations/repo-blueprints/) \ No newline at end of file diff --git a/modules/orchestration-providers/webhook/job-retry/tests/fixtures/base-inputs/main.tf b/modules/orchestration-providers/webhook/job-retry/tests/fixtures/base-inputs/main.tf new file mode 100644 index 0000000000..699060b83f --- /dev/null +++ b/modules/orchestration-providers/webhook/job-retry/tests/fixtures/base-inputs/main.tf @@ -0,0 +1,21 @@ +# tflint-ignore: terraform_required_version + +# tflint-ignore: terraform_documented_variables +variable "config" { + type = any + default = null +} + +# tflint-ignore: terraform_documented_variables +variable "storage_provider" { + type = any + default = null +} + +output "config" { + value = var.config +} + +output "storage_provider" { + value = var.storage_provider +} diff --git a/modules/orchestration-providers/webhook/job-retry/tests/job-retry.tftest.hcl b/modules/orchestration-providers/webhook/job-retry/tests/job-retry.tftest.hcl index e72b4b5484..adf902353b 100644 --- a/modules/orchestration-providers/webhook/job-retry/tests/job-retry.tftest.hcl +++ b/modules/orchestration-providers/webhook/job-retry/tests/job-retry.tftest.hcl @@ -10,6 +10,28 @@ mock_provider "aws" { arn = "arn:aws:iam::123456789012:role/job-retry-test" } } + + mock_resource "aws_lambda_function" { + defaults = { + arn = "arn:aws:lambda:eu-west-1:123456789012:function:job-retry-test" + } + } + + mock_resource "aws_sqs_queue" { + defaults = { + arn = "arn:aws:sqs:eu-west-1:123456789012:job-retry-test" + id = "https://sqs.eu-west-1.amazonaws.com/123456789012/job-retry-test" + url = "https://sqs.eu-west-1.amazonaws.com/123456789012/job-retry-test" + } + } +} + +run "base_inputs" { + command = apply + + module { + source = "./tests/fixtures/base-inputs" + } } variables { @@ -273,9 +295,9 @@ run "omits_xray_policy_when_tracing_is_disabled" { command = plan variables { - config = merge(var.config, { - observability = merge(var.config.observability, { - tracing = merge(var.config.observability.tracing, { + config = merge(run.base_inputs.config, { + observability = merge(run.base_inputs.config.observability, { + tracing = merge(run.base_inputs.config.observability.tracing, { mode = null }) }) @@ -339,9 +361,9 @@ run "does_not_enable_partial_vpc_configuration" { command = plan variables { - storage_provider = merge(var.storage_provider, { - aws = merge(var.storage_provider.aws, { - ssm = merge(var.storage_provider.aws.ssm, { + storage_provider = merge(run.base_inputs.storage_provider, { + aws = merge(run.base_inputs.storage_provider.aws, { + ssm = merge(run.base_inputs.storage_provider.aws.ssm, { kms_key_id = null }) }) @@ -493,8 +515,8 @@ run "rejects_unsupported_lambda_architecture" { } variables { - config = merge(var.config, { - lambda = merge(var.config.lambda, { + config = merge(run.base_inputs.config, { + lambda = merge(run.base_inputs.config.lambda, { architecture = "unsupported" }) }) @@ -511,9 +533,9 @@ run "rejects_unsupported_log_level" { } variables { - config = merge(var.config, { - observability = merge(var.config.observability, { - logs = merge(var.config.observability.logs, { + config = merge(run.base_inputs.config, { + observability = merge(run.base_inputs.config.observability, { + logs = merge(run.base_inputs.config.observability.logs, { level = "verbose" }) }) @@ -531,7 +553,7 @@ run "rejects_resource_prefix_longer_than_aws_limit" { } variables { - config = merge(var.config, { + config = merge(run.base_inputs.config, { prefix = "1234567890123456789012345678901234567890123456789012345" }) } diff --git a/modules/orchestration-providers/webhook/pool/tests/fixtures/base-inputs/main.tf b/modules/orchestration-providers/webhook/pool/tests/fixtures/base-inputs/main.tf new file mode 100644 index 0000000000..2374840af3 --- /dev/null +++ b/modules/orchestration-providers/webhook/pool/tests/fixtures/base-inputs/main.tf @@ -0,0 +1,21 @@ +# tflint-ignore: terraform_required_version + +# tflint-ignore: terraform_documented_variables +variable "storage_provider" { + type = any + default = null +} + +# tflint-ignore: terraform_documented_variables +variable "runner_provider" { + type = any + default = null +} + +output "storage_provider" { + value = var.storage_provider +} + +output "runner_provider" { + value = var.runner_provider +} diff --git a/modules/orchestration-providers/webhook/pool/tests/provider.tftest.hcl b/modules/orchestration-providers/webhook/pool/tests/provider.tftest.hcl index 8518a52d8e..857614093f 100644 --- a/modules/orchestration-providers/webhook/pool/tests/provider.tftest.hcl +++ b/modules/orchestration-providers/webhook/pool/tests/provider.tftest.hcl @@ -4,6 +4,26 @@ mock_provider "aws" { json = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":\"logs:CreateLogStream\",\"Resource\":\"*\"}]}" } } + + mock_resource "aws_iam_role" { + defaults = { + arn = "arn:aws:iam::123456789012:role/pool-test" + } + } + + mock_resource "aws_lambda_function" { + defaults = { + arn = "arn:aws:lambda:eu-west-1:123456789012:function:pool-test" + } + } +} + +run "base_inputs" { + command = apply + + module { + source = "./tests/fixtures/base-inputs" + } } variables { @@ -365,9 +385,9 @@ run "omits_optional_kms_statement" { } variables { - storage_provider = merge(var.storage_provider, { - aws = merge(var.storage_provider.aws, { - ssm = merge(var.storage_provider.aws.ssm, { + storage_provider = merge(run.base_inputs.storage_provider, { + aws = merge(run.base_inputs.storage_provider.aws, { + ssm = merge(run.base_inputs.storage_provider.aws.ssm, { kms_key_id = null }) }) @@ -427,7 +447,7 @@ run "rejects_empty_compute_provider_type" { } variables { - runner_provider = merge(var.runner_provider, { + runner_provider = merge(run.base_inputs.runner_provider, { type = " " }) } @@ -443,7 +463,7 @@ run "rejects_invalid_compute_provider_policy" { } variables { - runner_provider = merge(var.runner_provider, { + runner_provider = merge(run.base_inputs.runner_provider, { iam_policy_json = "not-json" }) } @@ -459,7 +479,7 @@ run "requires_enabled_compute_provider_managed_policy_arn" { } variables { - runner_provider = merge(var.runner_provider, { + runner_provider = merge(run.base_inputs.runner_provider, { managed_policy_enabled = true managed_policy_arn = null }) diff --git a/modules/orchestration-providers/webhook/scale-runners/tests/fixtures/base-inputs/main.tf b/modules/orchestration-providers/webhook/scale-runners/tests/fixtures/base-inputs/main.tf new file mode 100644 index 0000000000..699060b83f --- /dev/null +++ b/modules/orchestration-providers/webhook/scale-runners/tests/fixtures/base-inputs/main.tf @@ -0,0 +1,21 @@ +# tflint-ignore: terraform_required_version + +# tflint-ignore: terraform_documented_variables +variable "config" { + type = any + default = null +} + +# tflint-ignore: terraform_documented_variables +variable "storage_provider" { + type = any + default = null +} + +output "config" { + value = var.config +} + +output "storage_provider" { + value = var.storage_provider +} diff --git a/modules/orchestration-providers/webhook/scale-runners/tests/scale-runners.tftest.hcl b/modules/orchestration-providers/webhook/scale-runners/tests/scale-runners.tftest.hcl index dbbef4a5f6..8d5827a45d 100644 --- a/modules/orchestration-providers/webhook/scale-runners/tests/scale-runners.tftest.hcl +++ b/modules/orchestration-providers/webhook/scale-runners/tests/scale-runners.tftest.hcl @@ -11,6 +11,26 @@ mock_provider "aws" { } } + mock_resource "aws_lambda_function" { + defaults = { + arn = "arn:aws:lambda:eu-west-1:123456789012:function:scale-runners-test" + } + } + + mock_resource "aws_cloudwatch_event_rule" { + defaults = { + arn = "arn:aws:events:eu-west-1:123456789012:rule/scale-runners-test" + } + } + +} + +run "base_inputs" { + command = apply + + module { + source = "./tests/fixtures/base-inputs" + } } variables { @@ -600,14 +620,14 @@ run "omits_optional_kms_statements" { } variables { - config = merge(var.config, { - queue = merge(var.config.queue, { + config = merge(run.base_inputs.config, { + queue = merge(run.base_inputs.config.queue, { kms_key_id = null }) }) - storage_provider = merge(var.storage_provider, { - aws = merge(var.storage_provider.aws, { - ssm = merge(var.storage_provider.aws.ssm, { + storage_provider = merge(run.base_inputs.storage_provider, { + aws = merge(run.base_inputs.storage_provider.aws, { + ssm = merge(run.base_inputs.storage_provider.aws.ssm, { kms_key_id = null }) }) @@ -694,8 +714,8 @@ run "requires_job_retry_queue_when_enabled" { } variables { - config = merge(var.config, { - job_retry = merge(var.config.job_retry, { + config = merge(run.base_inputs.config, { + job_retry = merge(run.base_inputs.config.job_retry, { enabled = true queue = null }) diff --git a/modules/orchestration-providers/webhook/tests/fixtures/base-inputs/main.tf b/modules/orchestration-providers/webhook/tests/fixtures/base-inputs/main.tf new file mode 100644 index 0000000000..eef9be328e --- /dev/null +++ b/modules/orchestration-providers/webhook/tests/fixtures/base-inputs/main.tf @@ -0,0 +1,31 @@ +# tflint-ignore: terraform_required_version + +# tflint-ignore: terraform_documented_variables +variable "config" { + type = any + default = null +} + +# tflint-ignore: terraform_documented_variables +variable "storage_provider" { + type = any + default = null +} + +# tflint-ignore: terraform_documented_variables +variable "runner_provider" { + type = any + default = null +} + +output "config" { + value = var.config +} + +output "storage_provider" { + value = var.storage_provider +} + +output "runner_provider" { + value = var.runner_provider +} diff --git a/modules/orchestration-providers/webhook/tests/webhook.tftest.hcl b/modules/orchestration-providers/webhook/tests/webhook.tftest.hcl index 5d8ca6a68a..908a772d88 100644 --- a/modules/orchestration-providers/webhook/tests/webhook.tftest.hcl +++ b/modules/orchestration-providers/webhook/tests/webhook.tftest.hcl @@ -10,6 +10,34 @@ mock_provider "aws" { arn = "arn:aws:iam::123456789012:role/webhook-orchestration-test" } } + + mock_resource "aws_lambda_function" { + defaults = { + arn = "arn:aws:lambda:eu-west-1:123456789012:function:webhook-orchestration-test" + } + } + + mock_resource "aws_sqs_queue" { + defaults = { + arn = "arn:aws:sqs:eu-west-1:123456789012:webhook-orchestration-test" + id = "https://sqs.eu-west-1.amazonaws.com/123456789012/webhook-orchestration-test" + url = "https://sqs.eu-west-1.amazonaws.com/123456789012/webhook-orchestration-test" + } + } + + mock_resource "aws_cloudwatch_event_rule" { + defaults = { + arn = "arn:aws:events:eu-west-1:123456789012:rule/webhook-orchestration-test" + } + } +} + +run "base_inputs" { + command = apply + + module { + source = "./tests/fixtures/base-inputs" + } } variables { @@ -305,9 +333,9 @@ run "rejects_conflicting_artifact_sources" { } variables { - config = merge(var.config, { - lambda = merge(var.config.lambda, { - artifact = merge(var.config.lambda.artifact, { + config = merge(run.base_inputs.config, { + lambda = merge(run.base_inputs.config.lambda, { + artifact = merge(run.base_inputs.config.lambda.artifact, { zip = "runners.zip" }) }) diff --git a/modules/runner-config/tests/computed-iam-inputs.tftest.hcl b/modules/runner-config/tests/computed-iam-inputs.tftest.hcl index 055127e7ba..0838c818df 100644 --- a/modules/runner-config/tests/computed-iam-inputs.tftest.hcl +++ b/modules/runner-config/tests/computed-iam-inputs.tftest.hcl @@ -4,6 +4,38 @@ mock_provider "aws" { json = "{\"Version\":\"2012-10-17\",\"Statement\":[]}" } } + + mock_resource "aws_iam_role" { + defaults = { + arn = "arn:aws:iam::123456789012:role/runner-test" + } + } + + mock_resource "aws_iam_policy" { + defaults = { + arn = "arn:aws:iam::123456789012:policy/runner-test" + } + } + + mock_resource "aws_lambda_function" { + defaults = { + arn = "arn:aws:lambda:eu-west-1:123456789012:function:runner-test" + } + } + + mock_resource "aws_sqs_queue" { + defaults = { + arn = "arn:aws:sqs:eu-west-1:123456789012:runner-test" + id = "https://sqs.eu-west-1.amazonaws.com/123456789012/runner-test" + url = "https://sqs.eu-west-1.amazonaws.com/123456789012/runner-test" + } + } + + mock_resource "aws_cloudwatch_event_rule" { + defaults = { + arn = "arn:aws:events:eu-west-1:123456789012:rule/runner-test" + } + } } run "computed_external_values_keep_plan_shape_known" { diff --git a/modules/runner-config/tests/pool.tftest.hcl b/modules/runner-config/tests/pool.tftest.hcl index f5619d5394..aff9ee869c 100644 --- a/modules/runner-config/tests/pool.tftest.hcl +++ b/modules/runner-config/tests/pool.tftest.hcl @@ -11,6 +11,32 @@ mock_provider "aws" { } } + mock_resource "aws_iam_policy" { + defaults = { + arn = "arn:aws:iam::123456789012:policy/runner-test" + } + } + + mock_resource "aws_lambda_function" { + defaults = { + arn = "arn:aws:lambda:eu-west-1:123456789012:function:runner-test" + } + } + + mock_resource "aws_sqs_queue" { + defaults = { + arn = "arn:aws:sqs:eu-west-1:123456789012:runner-test" + id = "https://sqs.eu-west-1.amazonaws.com/123456789012/runner-test" + url = "https://sqs.eu-west-1.amazonaws.com/123456789012/runner-test" + } + } + + mock_resource "aws_cloudwatch_event_rule" { + defaults = { + arn = "arn:aws:events:eu-west-1:123456789012:rule/runner-test" + } + } + mock_resource "aws_ssm_parameter" { defaults = { arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/ami-id" diff --git a/modules/runner-config/tests/tags.tftest.hcl b/modules/runner-config/tests/tags.tftest.hcl index db85b11c7a..4bc955233c 100644 --- a/modules/runner-config/tests/tags.tftest.hcl +++ b/modules/runner-config/tests/tags.tftest.hcl @@ -11,6 +11,32 @@ mock_provider "aws" { } } + mock_resource "aws_iam_policy" { + defaults = { + arn = "arn:aws:iam::123456789012:policy/runner-test" + } + } + + mock_resource "aws_lambda_function" { + defaults = { + arn = "arn:aws:lambda:eu-west-1:123456789012:function:runner-test" + } + } + + mock_resource "aws_sqs_queue" { + defaults = { + arn = "arn:aws:sqs:eu-west-1:123456789012:runner-test" + id = "https://sqs.eu-west-1.amazonaws.com/123456789012/runner-test" + url = "https://sqs.eu-west-1.amazonaws.com/123456789012/runner-test" + } + } + + mock_resource "aws_cloudwatch_event_rule" { + defaults = { + arn = "arn:aws:events:eu-west-1:123456789012:rule/runner-test" + } + } + mock_resource "aws_ssm_parameter" { defaults = { arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/config" From 223a5d6793b45fc9f1033e98a78887fad9a85d99 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 24 Sep 2026 22:50:15 +0000 Subject: [PATCH 29/44] docs: auto update terraform docs --- modules/compute-providers/aws/ec2/README.md | 2 +- .../tests/fixtures/base-inputs/README.md | 31 +++++++++++++++++ .../pool/tests/fixtures/base-inputs/README.md | 31 +++++++++++++++++ .../tests/fixtures/base-inputs/README.md | 31 +++++++++++++++++ .../tests/fixtures/base-inputs/README.md | 33 +++++++++++++++++++ 5 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 modules/orchestration-providers/webhook/job-retry/tests/fixtures/base-inputs/README.md create mode 100644 modules/orchestration-providers/webhook/pool/tests/fixtures/base-inputs/README.md create mode 100644 modules/orchestration-providers/webhook/scale-runners/tests/fixtures/base-inputs/README.md create mode 100644 modules/orchestration-providers/webhook/tests/fixtures/base-inputs/README.md diff --git a/modules/compute-providers/aws/ec2/README.md b/modules/compute-providers/aws/ec2/README.md index 6e50cf82ed..39b32a631a 100644 --- a/modules/compute-providers/aws/ec2/README.md +++ b/modules/compute-providers/aws/ec2/README.md @@ -62,7 +62,7 @@ No modules. |------|-------------|------|---------|:--------:| | [aws\_partition](#input\_aws\_partition) | AWS partition used to construct IAM ARNs. | `string` | `"aws"` | no | | [aws\_region](#input\_aws\_region) | AWS region used by compute-provider resources and policy documents. | `string` | n/a | yes | -| [config](#input\_config) | EC2 compute-provider configuration. Paths match `compute_provider.aws.ec2` in the runner configuration.

- `ami`: Optional AMI discovery and encryption configuration. Null selects defaults for `runner.os` and `runner.architecture`.
- `ami.filter`: AMI filter names mapped to accepted values and merged over the provider defaults.
- `ami.owners`: AWS account IDs or aliases allowed to own the selected AMI.
- `ami.ssm_parameter`: Optional AMI-ID SSM parameter configuration. Set `arn` to use an existing parameter or `path` to create one managed by this module.
- `ami.ssm_parameter.path`: Parent path under which the module creates the `ami_id` parameter.
- `ami.ssm_parameter.arn`: ARN of an existing AMI-ID parameter.
- `ami.kms_key`: Optional customer-managed KMS key required for encrypted AMIs or snapshots. Its object presence is the plan-time policy discriminator.
- `ami.kms_key.arn`: ARN of the AMI KMS key. The ARN may remain unknown until apply.
- `vpc_id`: VPC in which runner networking resources are created.
- `subnet_ids`: Subnets from which the control plane may launch runners.
- `overrides.name_runner`: Optional Name tag override for runner compute resources.
- `overrides.name_sg`: Optional Name tag override for the managed security group.
- `instance_profile`: Optional externally managed instance profile. Its object presence is the plan-time ownership discriminator.
- `instance_profile.name`: Name of the external instance profile. The name may remain unknown until apply.
- `instance_profile_path`: IAM path for the provider-managed instance profile. Null derives the path from `prefix`.
- `binaries_syncer.enabled`: Uses the synchronized runner distribution from S3 during bootstrap.
- `binaries_syncer.s3`: S3 object containing the synchronized runner distribution. Required when synchronization is enabled.
- `binaries_syncer.s3.arn`: Runner-distribution bucket ARN used by IAM policies.
- `binaries_syncer.s3.id`: Runner-distribution bucket name used in the bootstrap URI.
- `binaries_syncer.s3.key`: Runner-distribution object key.
- `block_device_mappings`: EBS mappings added to the launch template.
- `block_device_mappings[].delete_on_termination`: Deletes the volume when its runner terminates.
- `block_device_mappings[].device_name`: Device name exposed to the runner instance.
- `block_device_mappings[].encrypted`: Enables EBS encryption.
- `block_device_mappings[].iops`: Provisioned IOPS for volume types that support configurable IOPS.
- `block_device_mappings[].kms_key_id`: KMS key ID or ARN used to encrypt the volume.
- `block_device_mappings[].snapshot_id`: Snapshot used to initialize the volume.
- `block_device_mappings[].throughput`: Provisioned throughput for volume types that support it.
- `block_device_mappings[].volume_initialization_rate`: Fixed initialization rate for supported snapshot-backed volumes.
- `block_device_mappings[].volume_size`: EBS volume size in GiB.
- `block_device_mappings[].volume_type`: EBS volume type.
- `ebs_optimized`: Requests EBS-optimized instances.
- `instance_target_capacity_type`: Primary capacity type, either `spot` or `on-demand`.
- `instance_allocation_strategy`: EC2 Fleet allocation strategy.
- `instance_type_priorities`: Optional numeric priorities keyed by instance type.
- `instance_max_spot_price`: Optional maximum hourly Spot price.
- `instance_types`: EC2 instance types available to the control plane.
- `user_data`: Runner bootstrap user-data configuration.
- `user_data.enabled`: Enables launch-template user data.
- `user_data.template`: Optional path to a custom user-data template.
- `user_data.content`: Optional complete user-data content used instead of a template.
- `user_data.pre_install`: Script inserted before runner installation.
- `user_data.post_install`: Script inserted after runner installation.
- `user_data.debug_logging_enabled`: Enables verbose user-data tracing, which can expose secrets.
- `ssm_enabled`: Includes Session Manager permissions in the provider's runner policy group.
- `create_service_linked_role_spot`: Allows scale-up to create the EC2 Spot service-linked role.
- `cloudwatch_agent.enabled`: Enables CloudWatch agent configuration for runner instances.
- `cloudwatch_agent.config`: Optional complete CloudWatch agent configuration.
- `managed_security_group_enabled`: Creates and attaches the provider-managed security group.
- `log_files`: Optional files collected by the CloudWatch agent. Null uses provider defaults.
- `log_files[].log_group_name`: CloudWatch log-group name before optional prefixing.
- `log_files[].prefix_log_group`: Prefixes the log-group name with the runner configuration path.
- `log_files[].file_path`: File or glob read by the CloudWatch agent.
- `log_files[].log_stream_name`: CloudWatch log-stream name template.
- `log_files[].log_class`: CloudWatch log-group class for the collected file.
- `key_name`: Optional EC2 key-pair name.
- `additional_security_group_ids`: Existing security groups attached to runners.
- `detailed_monitoring_enabled`: Enables detailed EC2 monitoring.
- `egress_rules`: Rules created on the managed security group.
- `egress_rules[].cidr_blocks`: IPv4 CIDR destinations.
- `egress_rules[].ipv6_cidr_blocks`: IPv6 CIDR destinations.
- `egress_rules[].prefix_list_ids`: AWS prefix-list destinations.
- `egress_rules[].from_port`: First destination port in the permitted range.
- `egress_rules[].protocol`: IP protocol name or number. Use `-1` for all protocols.
- `egress_rules[].security_groups`: Destination security-group IDs.
- `egress_rules[].self`: Allows traffic to the managed security group itself.
- `egress_rules[].to_port`: Last destination port in the permitted range.
- `egress_rules[].description`: Optional rule description.
- `tags`: Runner instance, volume, network-interface, and eligible Spot-request tags. Provider-required bootstrap tags take final precedence.
- `metadata_options`: Instance Metadata Service configuration.
- `metadata_options.instance_metadata_tags`: Exposes instance tags through Instance Metadata Service when enabled.
- `metadata_options.http_endpoint`: Enables or disables the Instance Metadata Service endpoint.
- `metadata_options.http_tokens`: Controls whether IMDSv2 session tokens are optional or required.
- `metadata_options.http_put_response_hop_limit`: Network hop limit for Instance Metadata Service token responses.
- `credit_specification`: CPU credit mode for burstable instance types.
- `cpu_options`: CPU topology and processor-feature configuration.
- `cpu_options.core_count`: Number of CPU cores exposed to the runner instance.
- `cpu_options.threads_per_core`: Number of hardware threads exposed per CPU core.
- `cpu_options.amd_sev_snp`: Enables or disables AMD SEV-SNP on supported instance types.
- `cpu_options.nested_virtualization`: Enables or disables nested virtualization on supported instance types.
- `placement`: EC2 placement configuration.
- `placement.affinity`: Dedicated Host affinity setting.
- `placement.availability_zone`: Availability Zone in which runner instances are placed.
- `placement.group_id`: Placement-group ID.
- `placement.group_name`: Placement-group name.
- `placement.host_id`: Dedicated Host ID.
- `placement.host_resource_group_arn`: ARN of the host resource group used for placement.
- `placement.spread_domain`: Spread-domain placement value.
- `placement.tenancy`: Instance tenancy, such as `default`, `dedicated`, or `host`.
- `placement.partition_number`: Placement-group partition number.
- `license_specifications`: License Manager configurations added to the launch template.
- `license_specifications[].license_configuration_arn`: ARN of an AWS License Manager license configuration.
- `associate_public_ipv4_address`: Associates a public IPv4 address with runner network interfaces.
- `network_interfaces`: Advanced network interface configuration for the launch template. Leave empty to keep using `associate_public_ipv4_address` for a simple single-interface setup.
- `on_demand_failover_for_errors`: EC2 errors that trigger on-demand fallback after a Spot failure.
- `scale_errors`: EC2 errors treated as retryable scale-up failures.
- `use_dedicated_host`: Enables the dedicated-host launch path. |
object({
ami = optional(object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
ssm_parameter = optional(object({
path = optional(string, null)
arn = optional(string, null)
}), null)
kms_key = optional(object({
arn = string
}), null)
}), null)
vpc_id = string
subnet_ids = list(string)
overrides = optional(object({
name_runner = optional(string, "")
name_sg = optional(string, "")
}), {})
instance_profile = optional(object({
name = string
}), null)
instance_profile_path = optional(string, null)
binaries_syncer = optional(object({
enabled = optional(bool, true)
s3 = optional(object({
arn = string
id = string
key = string
}), null)
}), {})
block_device_mappings = optional(list(object({
delete_on_termination = optional(bool, true)
device_name = optional(string, "/dev/xvda")
encrypted = optional(bool, true)
iops = optional(number)
kms_key_id = optional(string)
snapshot_id = optional(string)
throughput = optional(number)
volume_initialization_rate = optional(number)
volume_size = number
volume_type = optional(string, "gp3")
})), [{ volume_size = 30 }])
ebs_optimized = optional(bool, false)
instance_target_capacity_type = optional(string, "spot")
instance_allocation_strategy = optional(string, "lowest-price")
instance_type_priorities = optional(map(number), null)
instance_max_spot_price = optional(string, null)
instance_types = list(string)
user_data = optional(object({
enabled = optional(bool, true)
template = optional(string, null)
content = optional(string, null)
pre_install = optional(string, "")
post_install = optional(string, "")
debug_logging_enabled = optional(bool, false)
}), {})
ssm_enabled = optional(bool, false)
create_service_linked_role_spot = optional(bool, false)
cloudwatch_agent = optional(object({
enabled = optional(bool, true)
config = optional(string, null)
}), {})
managed_security_group_enabled = optional(bool, true)
log_files = optional(list(object({
log_group_name = string
prefix_log_group = bool
file_path = string
log_stream_name = string
log_class = optional(string, "STANDARD")
})), null)
key_name = optional(string, null)
additional_security_group_ids = optional(list(string), [])
detailed_monitoring_enabled = optional(bool, false)
egress_rules = optional(list(object({
cidr_blocks = list(string)
ipv6_cidr_blocks = list(string)
prefix_list_ids = list(string)
from_port = number
protocol = string
security_groups = list(string)
self = bool
to_port = number
description = string
})), [{
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
prefix_list_ids = null
from_port = 0
protocol = "-1"
security_groups = null
self = null
to_port = 0
description = null
}])
tags = optional(map(string), {})
metadata_options = optional(object({
instance_metadata_tags = optional(string, "enabled")
http_endpoint = optional(string, "enabled")
http_tokens = optional(string, "required")
http_put_response_hop_limit = optional(number, 1)
}), {})
credit_specification = optional(string, null)
cpu_options = optional(object({
core_count = optional(number)
threads_per_core = optional(number)
amd_sev_snp = optional(string)
nested_virtualization = optional(string)
}), null)
placement = optional(object({
affinity = optional(string)
availability_zone = optional(string)
group_id = optional(string)
group_name = optional(string)
host_id = optional(string)
host_resource_group_arn = optional(string)
spread_domain = optional(string)
tenancy = optional(string)
partition_number = optional(number)
}), null)
license_specifications = optional(list(object({
license_configuration_arn = string
})), [])
associate_public_ipv4_address = optional(bool, false)
network_interfaces = optional(list(object({
associate_carrier_ip_address = optional(bool)
associate_public_ip_address = optional(bool)
delete_on_termination = optional(bool)
description = optional(string)
device_index = optional(number)
interface_type = optional(string)
ipv4_address_count = optional(number)
ipv4_addresses = optional(list(string))
ipv4_prefix_count = optional(number)
ipv4_prefixes = optional(list(string))
ipv6_address_count = optional(number)
ipv6_addresses = optional(list(string))
ipv6_prefix_count = optional(number)
ipv6_prefixes = optional(list(string))
network_card_index = optional(number)
network_interface_id = optional(string)
primary_ipv6 = optional(bool)
private_ip_address = optional(string)
security_groups = optional(list(string))
subnet_id = optional(string)
connection_tracking_specification = optional(object({
tcp_established_timeout = optional(number)
udp_stream_timeout = optional(number)
udp_timeout = optional(number)
}))
ena_srd_specification = optional(object({
ena_srd_enabled = optional(bool)
ena_srd_udp_specification = optional(object({
ena_srd_udp_enabled = optional(bool)
}))
}))
})), [])
on_demand_failover_for_errors = optional(list(string), [])
scale_errors = optional(list(string), [
"UnfulfillableCapacity",
"MaxSpotInstanceCountExceeded",
"TargetCapacityLimitExceededException",
"RequestLimitExceeded",
"ResourceLimitExceeded",
"MaxSpotInstanceCountExceeded",
"MaxSpotFleetRequestCountExceeded",
"InsufficientInstanceCapacity",
"InsufficientCapacityOnHost",
])
use_dedicated_host = optional(bool, false)
})
| n/a | yes | +| [config](#input\_config) | EC2 compute-provider configuration. Paths match `compute_provider.aws.ec2` in the runner configuration.

- `ami`: Optional AMI discovery and encryption configuration. Null selects defaults for `runner.os` and `runner.architecture`.
- `ami.filter`: AMI filter names mapped to accepted values and merged over the provider defaults.
- `ami.owners`: AWS account IDs or aliases allowed to own the selected AMI.
- `ami.ssm_parameter`: Optional AMI-ID SSM parameter configuration. Set `arn` to use an existing parameter or `path` to create one managed by this module.
- `ami.ssm_parameter.path`: Parent path under which the module creates the `ami_id` parameter.
- `ami.ssm_parameter.arn`: ARN of an existing AMI-ID parameter.
- `ami.kms_key`: Optional customer-managed KMS key required for encrypted AMIs or snapshots. Its object presence is the plan-time policy discriminator.
- `ami.kms_key.arn`: ARN of the AMI KMS key. The ARN may remain unknown until apply.
- `vpc_id`: VPC in which runner networking resources are created.
- `subnet_ids`: Subnets from which the control plane may launch runners.
- `overrides.name_runner`: Optional Name tag override for runner compute resources.
- `overrides.name_sg`: Optional Name tag override for the managed security group.
- `instance_profile`: Optional externally managed instance profile. Its object presence is the plan-time ownership discriminator.
- `instance_profile.name`: Name of the external instance profile. The name may remain unknown until apply.
- `instance_profile_path`: IAM path for the provider-managed instance profile. Null derives the path from `prefix`.
- `binaries_syncer.enabled`: Uses the synchronized runner distribution from S3 during bootstrap.
- `binaries_syncer.s3`: S3 object containing the synchronized runner distribution. Required when synchronization is enabled.
- `binaries_syncer.s3.arn`: Runner-distribution bucket ARN used by IAM policies.
- `binaries_syncer.s3.id`: Runner-distribution bucket name used in the bootstrap URI.
- `binaries_syncer.s3.key`: Runner-distribution object key.
- `block_device_mappings`: EBS mappings added to the launch template.
- `block_device_mappings[].delete_on_termination`: Deletes the volume when its runner terminates.
- `block_device_mappings[].device_name`: Device name exposed to the runner instance.
- `block_device_mappings[].encrypted`: Enables EBS encryption.
- `block_device_mappings[].iops`: Provisioned IOPS for volume types that support configurable IOPS.
- `block_device_mappings[].kms_key_id`: KMS key ID or ARN used to encrypt the volume.
- `block_device_mappings[].snapshot_id`: Snapshot used to initialize the volume.
- `block_device_mappings[].throughput`: Provisioned throughput for volume types that support it.
- `block_device_mappings[].volume_initialization_rate`: Fixed initialization rate for supported snapshot-backed volumes.
- `block_device_mappings[].volume_size`: EBS volume size in GiB.
- `block_device_mappings[].volume_type`: EBS volume type.
- `ebs_optimized`: Requests EBS-optimized instances.
- `instance_target_capacity_type`: Primary capacity type, either `spot` or `on-demand`.
- `instance_allocation_strategy`: EC2 Fleet allocation strategy.
- `instance_type_priorities`: Optional numeric priorities keyed by instance type.
- `instance_max_spot_price`: Optional maximum hourly Spot price.
- `instance_types`: EC2 instance types available to the control plane.
- `user_data`: Runner bootstrap user-data configuration.
- `user_data.enabled`: Enables launch-template user data.
- `user_data.template`: Optional path to a custom user-data template.
- `user_data.content`: Optional complete user-data content used instead of a template.
- `user_data.pre_install`: Script inserted before runner installation.
- `user_data.post_install`: Script inserted after runner installation.
- `user_data.debug_logging_enabled`: Enables verbose user-data tracing, which can expose secrets.
- `ssm_enabled`: Includes Session Manager permissions in the provider's runner policy group.
- `create_service_linked_role_spot`: Allows scale-up to create the EC2 Spot service-linked role.
- `cloudwatch_agent.enabled`: Enables CloudWatch agent configuration for runner instances.
- `cloudwatch_agent.config`: Optional complete CloudWatch agent configuration.
- `managed_security_group_enabled`: Creates and attaches the provider-managed security group.
- `log_files`: Optional files collected by the CloudWatch agent. Null uses provider defaults.
- `log_files[].log_group_name`: CloudWatch log-group name before optional prefixing.
- `log_files[].prefix_log_group`: Prefixes the log-group name with the runner configuration path.
- `log_files[].file_path`: File or glob read by the CloudWatch agent.
- `log_files[].log_stream_name`: CloudWatch log-stream name template.
- `log_files[].log_class`: CloudWatch log-group class for the collected file.
- `key_name`: Optional EC2 key-pair name.
- `additional_security_group_ids`: Existing security groups attached to runners.
- `detailed_monitoring_enabled`: Enables detailed EC2 monitoring.
- `egress_rules`: Rules created on the managed security group.
- `egress_rules[].cidr_blocks`: IPv4 CIDR destinations.
- `egress_rules[].ipv6_cidr_blocks`: IPv6 CIDR destinations.
- `egress_rules[].prefix_list_ids`: AWS prefix-list destinations.
- `egress_rules[].from_port`: First destination port in the permitted range.
- `egress_rules[].protocol`: IP protocol name or number. Use `-1` for all protocols.
- `egress_rules[].security_groups`: Destination security-group IDs.
- `egress_rules[].self`: Allows traffic to the managed security group itself.
- `egress_rules[].to_port`: Last destination port in the permitted range.
- `egress_rules[].description`: Optional rule description.
- `tags`: Runner instance, volume, network-interface, and eligible Spot-request tags. Provider-required bootstrap tags take final precedence.
- `metadata_options`: Instance Metadata Service configuration.
- `metadata_options.instance_metadata_tags`: Exposes instance tags through Instance Metadata Service when enabled.
- `metadata_options.http_endpoint`: Enables or disables the Instance Metadata Service endpoint.
- `metadata_options.http_tokens`: Controls whether IMDSv2 session tokens are optional or required.
- `metadata_options.http_put_response_hop_limit`: Network hop limit for Instance Metadata Service token responses.
- `credit_specification`: CPU credit mode for burstable instance types.
- `cpu_options`: CPU topology and processor-feature configuration.
- `cpu_options.core_count`: Number of CPU cores exposed to the runner instance.
- `cpu_options.threads_per_core`: Number of hardware threads exposed per CPU core.
- `cpu_options.amd_sev_snp`: Enables or disables AMD SEV-SNP on supported instance types.
- `cpu_options.nested_virtualization`: Enables or disables nested virtualization on supported instance types.
- `placement`: EC2 placement configuration.
- `placement.affinity`: Dedicated Host affinity setting.
- `placement.availability_zone`: Availability Zone in which runner instances are placed.
- `placement.group_id`: Placement-group ID.
- `placement.group_name`: Placement-group name.
- `placement.host_id`: Dedicated Host ID.
- `placement.host_resource_group_arn`: ARN of the host resource group used for placement.
- `placement.spread_domain`: Spread-domain placement value.
- `placement.tenancy`: Instance tenancy, such as `default`, `dedicated`, or `host`.
- `placement.partition_number`: Placement-group partition number.
- `license_specifications`: License Manager configurations added to the launch template.
- `license_specifications[].license_configuration_arn`: ARN of an AWS License Manager license configuration.
- `associate_public_ipv4_address`: Associates a public IPv4 address with runner network interfaces.
- `network_interfaces`: Advanced network interface configuration for the launch template. Leave empty to keep using `associate_public_ipv4_address` for a simple single-interface setup.
- `on_demand_failover_for_errors`: EC2 errors that trigger on-demand fallback after a Spot failure.
- `scale_errors`: EC2 errors treated as retryable scale-up failures.
- `use_dedicated_host`: Enables the dedicated-host launch path. |
object({
ami = object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
ssm_parameter = optional(object({
path = optional(string, null)
arn = optional(string, null)
}), null)
kms_key = optional(object({
arn = string
}), null)
})
vpc_id = string
subnet_ids = list(string)
overrides = optional(object({
name_runner = optional(string, "")
name_sg = optional(string, "")
}), {})
instance_profile = optional(object({
name = string
}), null)
instance_profile_path = optional(string, null)
binaries_syncer = optional(object({
enabled = optional(bool, true)
s3 = optional(object({
arn = string
id = string
key = string
}), null)
}), {})
block_device_mappings = optional(list(object({
delete_on_termination = optional(bool, true)
device_name = optional(string, "/dev/xvda")
encrypted = optional(bool, true)
iops = optional(number)
kms_key_id = optional(string)
snapshot_id = optional(string)
throughput = optional(number)
volume_initialization_rate = optional(number)
volume_size = number
volume_type = optional(string, "gp3")
})), [{ volume_size = 30 }])
ebs_optimized = optional(bool, false)
instance_target_capacity_type = optional(string, "spot")
instance_allocation_strategy = optional(string, "lowest-price")
instance_type_priorities = optional(map(number), null)
instance_max_spot_price = optional(string, null)
instance_types = list(string)
user_data = optional(object({
enabled = optional(bool, true)
template = optional(string, null)
content = optional(string, null)
pre_install = optional(string, "")
post_install = optional(string, "")
debug_logging_enabled = optional(bool, false)
}), {})
ssm_enabled = optional(bool, false)
create_service_linked_role_spot = optional(bool, false)
cloudwatch_agent = optional(object({
enabled = optional(bool, true)
config = optional(string, null)
}), {})
managed_security_group_enabled = optional(bool, true)
log_files = optional(list(object({
log_group_name = string
prefix_log_group = bool
file_path = string
log_stream_name = string
log_class = optional(string, "STANDARD")
})), null)
key_name = optional(string, null)
additional_security_group_ids = optional(list(string), [])
detailed_monitoring_enabled = optional(bool, false)
egress_rules = optional(list(object({
cidr_blocks = list(string)
ipv6_cidr_blocks = list(string)
prefix_list_ids = list(string)
from_port = number
protocol = string
security_groups = list(string)
self = bool
to_port = number
description = string
})), [{
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
prefix_list_ids = null
from_port = 0
protocol = "-1"
security_groups = null
self = null
to_port = 0
description = null
}])
tags = optional(map(string), {})
metadata_options = optional(object({
instance_metadata_tags = optional(string, "enabled")
http_endpoint = optional(string, "enabled")
http_tokens = optional(string, "required")
http_put_response_hop_limit = optional(number, 1)
}), {})
credit_specification = optional(string, null)
cpu_options = optional(object({
core_count = optional(number)
threads_per_core = optional(number)
amd_sev_snp = optional(string)
nested_virtualization = optional(string)
}), null)
placement = optional(object({
affinity = optional(string)
availability_zone = optional(string)
group_id = optional(string)
group_name = optional(string)
host_id = optional(string)
host_resource_group_arn = optional(string)
spread_domain = optional(string)
tenancy = optional(string)
partition_number = optional(number)
}), null)
license_specifications = optional(list(object({
license_configuration_arn = string
})), [])
associate_public_ipv4_address = optional(bool, false)
network_interfaces = optional(list(object({
associate_carrier_ip_address = optional(bool)
associate_public_ip_address = optional(bool)
delete_on_termination = optional(bool)
description = optional(string)
device_index = optional(number)
interface_type = optional(string)
ipv4_address_count = optional(number)
ipv4_addresses = optional(list(string))
ipv4_prefix_count = optional(number)
ipv4_prefixes = optional(list(string))
ipv6_address_count = optional(number)
ipv6_addresses = optional(list(string))
ipv6_prefix_count = optional(number)
ipv6_prefixes = optional(list(string))
network_card_index = optional(number)
network_interface_id = optional(string)
primary_ipv6 = optional(bool)
private_ip_address = optional(string)
security_groups = optional(list(string))
subnet_id = optional(string)
connection_tracking_specification = optional(object({
tcp_established_timeout = optional(number)
udp_stream_timeout = optional(number)
udp_timeout = optional(number)
}))
ena_srd_specification = optional(object({
ena_srd_enabled = optional(bool)
ena_srd_udp_specification = optional(object({
ena_srd_udp_enabled = optional(bool)
}))
}))
})), [])
on_demand_failover_for_errors = optional(list(string), [])
scale_errors = optional(list(string), [
"UnfulfillableCapacity",
"MaxSpotInstanceCountExceeded",
"TargetCapacityLimitExceededException",
"RequestLimitExceeded",
"ResourceLimitExceeded",
"MaxSpotInstanceCountExceeded",
"MaxSpotFleetRequestCountExceeded",
"InsufficientInstanceCapacity",
"InsufficientCapacityOnHost",
])
use_dedicated_host = optional(bool, false)
})
| n/a | yes | | [observability](#input\_observability) | CloudWatch Logs settings available to compute-provider runner log groups.

- `logs.retention_in_days`: Retention period for provider-owned runner log groups.
- `logs.kms_key_id`: Optional KMS key ID or ARN used to encrypt runner log groups.
- `logs.tags`: Shared log-group tags that override module-level `tags`. |
object({
logs = optional(object({
retention_in_days = optional(number, 180)
kms_key_id = optional(string, null)
tags = optional(map(string), {})
}), {})
})
| `{}` | no | | [prefix](#input\_prefix) | Prefix used to identify resources created for the runner configuration. | `string` | `"github-actions"` | no | | [runner](#input\_runner) | Provider-neutral runner settings consumed by compute providers.

- `os`: Runner operating system. Supported values are `linux`, `osx`, and `windows`.
- `architecture`: Runner distribution architecture.
- `name_prefix`: Prefix added to registered runner names.
- `run_as_root`: Runs the runner service as root.
- `run_as`: Operating-system user used when `run_as_root` is false.
- `hooks.job_started`: Script installed as the runner job-started hook.
- `hooks.job_completed`: Script installed as the runner job-completed hook.
- `iam.role.arn`: Resolved runner-role ARN referenced by provider policies and resources.
- `iam.role.name`: Resolved runner-role name used by provider resources.
- `iam.role.managed`: Whether runner-config manages the resolved runner role.
- `iam.managed_policy_arns`: Common managed-policy ARNs returned with the provider-specific runner policies for attachment by runner-config.
- `iam.path`: IAM path available to provider-managed IAM resources. Null derives the path from `prefix`. |
object({
os = optional(string, "linux")
architecture = optional(string, "x64")
name_prefix = optional(string, "")
run_as_root = optional(bool, false)
run_as = optional(string, "ec2-user")
hooks = optional(object({
job_started = optional(string, "")
job_completed = optional(string, "")
}), {})
iam = object({
role = object({
arn = string
name = string
managed = optional(bool, true)
})
managed_policy_arns = optional(map(string), {})
path = optional(string, null)
})
})
| n/a | yes | diff --git a/modules/orchestration-providers/webhook/job-retry/tests/fixtures/base-inputs/README.md b/modules/orchestration-providers/webhook/job-retry/tests/fixtures/base-inputs/README.md new file mode 100644 index 0000000000..43b9e37efb --- /dev/null +++ b/modules/orchestration-providers/webhook/job-retry/tests/fixtures/base-inputs/README.md @@ -0,0 +1,31 @@ + +## Requirements + +No requirements. + +## Providers + +No providers. + +## Modules + +No modules. + +## Resources + +No resources. + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [config](#input\_config) | tflint-ignore: terraform\_documented\_variables | `any` | `null` | no | +| [storage\_provider](#input\_storage\_provider) | tflint-ignore: terraform\_documented\_variables | `any` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [config](#output\_config) | n/a | +| [storage\_provider](#output\_storage\_provider) | n/a | + \ No newline at end of file diff --git a/modules/orchestration-providers/webhook/pool/tests/fixtures/base-inputs/README.md b/modules/orchestration-providers/webhook/pool/tests/fixtures/base-inputs/README.md new file mode 100644 index 0000000000..dd18849db4 --- /dev/null +++ b/modules/orchestration-providers/webhook/pool/tests/fixtures/base-inputs/README.md @@ -0,0 +1,31 @@ + +## Requirements + +No requirements. + +## Providers + +No providers. + +## Modules + +No modules. + +## Resources + +No resources. + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [runner\_provider](#input\_runner\_provider) | tflint-ignore: terraform\_documented\_variables | `any` | `null` | no | +| [storage\_provider](#input\_storage\_provider) | tflint-ignore: terraform\_documented\_variables | `any` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [runner\_provider](#output\_runner\_provider) | n/a | +| [storage\_provider](#output\_storage\_provider) | n/a | + \ No newline at end of file diff --git a/modules/orchestration-providers/webhook/scale-runners/tests/fixtures/base-inputs/README.md b/modules/orchestration-providers/webhook/scale-runners/tests/fixtures/base-inputs/README.md new file mode 100644 index 0000000000..43b9e37efb --- /dev/null +++ b/modules/orchestration-providers/webhook/scale-runners/tests/fixtures/base-inputs/README.md @@ -0,0 +1,31 @@ + +## Requirements + +No requirements. + +## Providers + +No providers. + +## Modules + +No modules. + +## Resources + +No resources. + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [config](#input\_config) | tflint-ignore: terraform\_documented\_variables | `any` | `null` | no | +| [storage\_provider](#input\_storage\_provider) | tflint-ignore: terraform\_documented\_variables | `any` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [config](#output\_config) | n/a | +| [storage\_provider](#output\_storage\_provider) | n/a | + \ No newline at end of file diff --git a/modules/orchestration-providers/webhook/tests/fixtures/base-inputs/README.md b/modules/orchestration-providers/webhook/tests/fixtures/base-inputs/README.md new file mode 100644 index 0000000000..1bdf3acee1 --- /dev/null +++ b/modules/orchestration-providers/webhook/tests/fixtures/base-inputs/README.md @@ -0,0 +1,33 @@ + +## Requirements + +No requirements. + +## Providers + +No providers. + +## Modules + +No modules. + +## Resources + +No resources. + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [config](#input\_config) | tflint-ignore: terraform\_documented\_variables | `any` | `null` | no | +| [runner\_provider](#input\_runner\_provider) | tflint-ignore: terraform\_documented\_variables | `any` | `null` | no | +| [storage\_provider](#input\_storage\_provider) | tflint-ignore: terraform\_documented\_variables | `any` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [config](#output\_config) | n/a | +| [runner\_provider](#output\_runner\_provider) | n/a | +| [storage\_provider](#output\_storage\_provider) | n/a | + \ No newline at end of file From f8c5a8984cdc90d68b2bbe83301060ee8f27d3e8 Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Fri, 25 Sep 2026 01:07:07 +0200 Subject: [PATCH 30/44] test: fix migration test --- modules/compute-providers/aws/ec2/tests/provider.tftest.hcl | 4 ++-- modules/multi-runner/config.experimental.resolved.tf | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl b/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl index 891c4a59ed..6c37d7123a 100644 --- a/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl +++ b/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl @@ -495,7 +495,7 @@ run "rejects_external_instance_profile_with_managed_role" { variables { config = { - ami = { + ami = { filter = { state = ["available"] } owners = ["amazon"] ssm_parameter = { @@ -533,7 +533,7 @@ run "requires_distribution_object_when_sync_is_enabled" { variables { config = { - ami = { + ami = { filter = { state = ["available"] } owners = ["amazon"] ssm_parameter = { diff --git a/modules/multi-runner/config.experimental.resolved.tf b/modules/multi-runner/config.experimental.resolved.tf index 2df0b4e177..8892b70259 100644 --- a/modules/multi-runner/config.experimental.resolved.tf +++ b/modules/multi-runner/config.experimental.resolved.tf @@ -466,7 +466,7 @@ locals { ssm_parameter = { path = coalesce( try(v.compute_provider.aws.ec2.ami.ssm_parameter.path, null), - "/github-action-runners/${var.prefix}/runners/config", + "/github-action-runners/${var.prefix}/${k}/runners/config", ) arn = try(v.compute_provider.aws.ec2.ami.ssm_parameter.arn, null) } From dca83438b68cb5575b2c410e1f2e66bd940270a5 Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Fri, 25 Sep 2026 02:04:52 +0200 Subject: [PATCH 31/44] fix: fix mismatch between v1 and v2 --- modules/compute-providers/aws/ec2/ami.tf | 9 +- .../aws/ec2/policies-runner.tf | 4 + .../aws/ec2/storage-provider.aws.ssm.tf | 20 +++- .../aws/ec2/templates/start-runner.sh | 2 +- .../aws/ec2/tests/provider.tftest.hcl | 109 ++++++++++++++++++ 5 files changed, 137 insertions(+), 7 deletions(-) diff --git a/modules/compute-providers/aws/ec2/ami.tf b/modules/compute-providers/aws/ec2/ami.tf index fc29db329f..31fd294101 100644 --- a/modules/compute-providers/aws/ec2/ami.tf +++ b/modules/compute-providers/aws/ec2/ami.tf @@ -70,9 +70,9 @@ data "aws_iam_policy_document" "ami_id_ssm" { for_each = local.ami_id_ssm_module_managed || local.ami_id_ssm_external ? [1] : [] content { - effect = "Allow" - # TODO: Validate if "ssm:GetParameters" still needed - actions = ["ssm:GetParameter", "ssm:GetParameters"] + effect = "Allow" + sid = "AllowSSMParameterRead" + actions = ["ssm:GetParameters"] resources = [local.ami_id_ssm_module_managed ? aws_ssm_parameter.runner_ami_id[0].arn : local.ami_id_ssm_parameter_arn] } } @@ -82,6 +82,7 @@ data "aws_iam_policy_document" "ami_id_ssm" { content { effect = "Allow" + sid = "AllowKMSKeyUsage" actions = ["kms:DescribeKey", "kms:ReEncrypt*", "kms:Decrypt"] resources = [statement.value] } @@ -92,6 +93,7 @@ data "aws_iam_policy_document" "ami_id_ssm" { content { effect = "Allow" + sid = "AllowKMSKeyGrant" actions = ["kms:CreateGrant"] resources = [statement.value] @@ -109,6 +111,7 @@ data "aws_iam_policy_document" "ami_id_ssm_parameter_read" { statement { effect = "Allow" + sid = "AllowSSMParameterRead" actions = ["ssm:GetParameter"] resources = [local.ami_id_ssm_parameter_arn] } diff --git a/modules/compute-providers/aws/ec2/policies-runner.tf b/modules/compute-providers/aws/ec2/policies-runner.tf index bfb16fc8da..3d589cc5b7 100644 --- a/modules/compute-providers/aws/ec2/policies-runner.tf +++ b/modules/compute-providers/aws/ec2/policies-runner.tf @@ -104,6 +104,10 @@ data "aws_iam_policy_document" "terminate_self" { data "aws_iam_policy_document" "cloudwatch" { count = var.config.cloudwatch_agent.enabled ? 1 : 0 + source_policy_documents = var.storage_provider.aws.ssm != null ? [ + data.aws_iam_policy_document.ssm_cloudwatch[0].json + ] : [] + statement { effect = "Allow" actions = [ diff --git a/modules/compute-providers/aws/ec2/storage-provider.aws.ssm.tf b/modules/compute-providers/aws/ec2/storage-provider.aws.ssm.tf index 5a6945e1d7..dbedac16b2 100644 --- a/modules/compute-providers/aws/ec2/storage-provider.aws.ssm.tf +++ b/modules/compute-providers/aws/ec2/storage-provider.aws.ssm.tf @@ -45,6 +45,20 @@ data "aws_iam_policy_document" "ssm_parameters" { } } +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 = { @@ -57,10 +71,10 @@ locals { # runner config locals { - ssm_root_path = var.storage_provider.aws.ssm.paths.root - ssm_config_path = "${local.ssm_root_path}/${var.storage_provider.aws.ssm.paths.config}" + 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_parameter_arn_prefix}${local.ssm_config_path}" + 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, diff --git a/modules/compute-providers/aws/ec2/templates/start-runner.sh b/modules/compute-providers/aws/ec2/templates/start-runner.sh index 7f2c0f82c5..3260a5edf6 100644 --- a/modules/compute-providers/aws/ec2/templates/start-runner.sh +++ b/modules/compute-providers/aws/ec2/templates/start-runner.sh @@ -95,7 +95,7 @@ cleanup() { if [ "$exit_code" -ne 0 ]; then echo "ERROR: runner-start-failed with exit code $exit_code occurred on $error_location" - create_xray_error_segment "$SEGMENT" "runner-start-failed with exit code $exit_code occurred on $error_location - $error_lineno" + create_xray_error_segment "$${SEGMENT:-}" "runner-start-failed with exit code $exit_code occurred on $error_location - $error_lineno" fi # allows to flush the cloud watch logs and traces sleep 10 diff --git a/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl b/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl index 6c37d7123a..3145a1700c 100644 --- a/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl +++ b/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl @@ -19,6 +19,12 @@ mock_provider "aws" { account_id = "123456789012" } } + + mock_resource "aws_ssm_parameter" { + defaults = { + arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/provider-test/config/cloudwatch_agent_config_runner" + } + } } override_data { @@ -91,6 +97,109 @@ variables { } } +run "merges_ssm_cloudwatch_policy_when_enabled" { + command = plan + + assert { + condition = ( + length(data.aws_iam_policy_document.cloudwatch) == 1 + && length(data.aws_iam_policy_document.ssm_cloudwatch) == 1 + && length(data.aws_iam_policy_document.cloudwatch[0].source_policy_documents) == 1 + && contains(data.aws_iam_policy_document.cloudwatch[0].statement[0].actions, "cloudwatch:PutMetricData") + ) + error_message = "An enabled CloudWatch agent with SSM must merge the SSM CloudWatch policy document." + } +} + +run "does_not_create_cloudwatch_policy_when_disabled_with_ssm" { + command = plan + + variables { + config = { + ami = { + filter = { state = ["available"] } + owners = ["amazon"] + ssm_parameter = { + arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/ami-id" + } + kms_key = null + } + vpc_id = "vpc-12345678" + subnet_ids = ["subnet-12345678"] + cloudwatch_agent = { + enabled = false + } + } + } + + assert { + condition = ( + length(data.aws_iam_policy_document.cloudwatch) == 0 + && length(data.aws_iam_policy_document.ssm_cloudwatch) == 0 + && !contains(keys(output.provider.policies.runner.inline_policies), "cloudwatch") + ) + error_message = "A disabled CloudWatch agent must not create or attach CloudWatch policies, even with SSM enabled." + } +} + +run "does_not_merge_ssm_cloudwatch_policy_without_ssm" { + command = plan + + variables { + storage_provider = { + aws = { + ssm = null + } + } + } + + assert { + condition = ( + length(data.aws_iam_policy_document.cloudwatch) == 1 + && length(data.aws_iam_policy_document.ssm_cloudwatch) == 0 + && length(data.aws_iam_policy_document.cloudwatch[0].source_policy_documents) == 0 + && contains(data.aws_iam_policy_document.cloudwatch[0].statement[0].actions, "cloudwatch:PutMetricData") + ) + error_message = "An enabled CloudWatch agent without SSM must retain only its base CloudWatch policy." + } +} + +run "does_not_create_cloudwatch_policy_when_disabled_without_ssm" { + command = plan + + variables { + config = { + ami = { + filter = { state = ["available"] } + owners = ["amazon"] + ssm_parameter = { + arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/ami-id" + } + kms_key = null + } + vpc_id = "vpc-12345678" + subnet_ids = ["subnet-12345678"] + cloudwatch_agent = { + enabled = false + } + } + storage_provider = { + aws = { + ssm = null + } + } + } + + assert { + condition = ( + length(data.aws_iam_policy_document.cloudwatch) == 0 + && length(data.aws_iam_policy_document.ssm_cloudwatch) == 0 + && !contains(keys(output.provider.policies.runner.inline_policies), "cloudwatch") + ) + error_message = "A disabled CloudWatch agent without SSM must not create any CloudWatch policy." + } +} + run "separates_control_plane_contract_from_ec2_resources" { command = plan From d7e875fe2d6076ce7d0f480c16225c31488d6a1c Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Fri, 25 Sep 2026 02:07:27 +0200 Subject: [PATCH 32/44] test: fix test --- .../aws/ec2/tests/provider.tftest.hcl | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl b/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl index 3145a1700c..426e5ea15a 100644 --- a/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl +++ b/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl @@ -124,8 +124,13 @@ run "does_not_create_cloudwatch_policy_when_disabled_with_ssm" { } kms_key = null } - vpc_id = "vpc-12345678" - subnet_ids = ["subnet-12345678"] + vpc_id = "vpc-12345678" + subnet_ids = ["subnet-12345678"] + instance_types = ["m5.large"] + binaries_syncer = { + enabled = false + s3 = null + } cloudwatch_agent = { enabled = false } @@ -177,8 +182,13 @@ run "does_not_create_cloudwatch_policy_when_disabled_without_ssm" { } kms_key = null } - vpc_id = "vpc-12345678" - subnet_ids = ["subnet-12345678"] + vpc_id = "vpc-12345678" + subnet_ids = ["subnet-12345678"] + instance_types = ["m5.large"] + binaries_syncer = { + enabled = false + s3 = null + } cloudwatch_agent = { enabled = false } From c7fc200f283229298c36d65297b1631e6842e0ae Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 25 Sep 2026 00:08:07 +0000 Subject: [PATCH 33/44] docs: auto update terraform docs --- modules/compute-providers/aws/ec2/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/compute-providers/aws/ec2/README.md b/modules/compute-providers/aws/ec2/README.md index 39b32a631a..c81c5a139d 100644 --- a/modules/compute-providers/aws/ec2/README.md +++ b/modules/compute-providers/aws/ec2/README.md @@ -53,6 +53,7 @@ No modules. | [aws_iam_policy_document.scale_up](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.service_linked_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.session_manager](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.ssm_cloudwatch](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.ssm_parameters](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.terminate_self](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | From e0e0177c091f716866c8dc92d995ef6f8ded3dde Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Fri, 25 Sep 2026 02:32:08 +0200 Subject: [PATCH 34/44] fix: fix last mismatch --- modules/compute-providers/aws/ec2/storage-provider.aws.ssm.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/compute-providers/aws/ec2/storage-provider.aws.ssm.tf b/modules/compute-providers/aws/ec2/storage-provider.aws.ssm.tf index dbedac16b2..ad380efc06 100644 --- a/modules/compute-providers/aws/ec2/storage-provider.aws.ssm.tf +++ b/modules/compute-providers/aws/ec2/storage-provider.aws.ssm.tf @@ -54,7 +54,7 @@ data "aws_iam_policy_document" "ssm_cloudwatch" { "ssm:GetParameter", ] resources = [ - aws_ssm_parameter.cloudwatch_agent_config_runner[0].arn, + "${aws_ssm_parameter.cloudwatch_agent_config_runner[0].arn}/*", ] } } From 9db63f4d935177d39db63b68b763f272929422ba Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 25 Sep 2026 13:44:36 +0000 Subject: [PATCH 35/44] docs: auto update terraform docs --- modules/multi-runner/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/multi-runner/README.md b/modules/multi-runner/README.md index 1dff8bb6ef..1e86c7bf61 100644 --- a/modules/multi-runner/README.md +++ b/modules/multi-runner/README.md @@ -167,7 +167,7 @@ module "multi-runner" { | [global\_config\_github](#input\_global\_config\_github) | Global GitHub configuration shared by all runner lanes.

global\_config\_github = {
app: {
key\_base64: "Base64-encoded GitHub App private key."
key\_base64\_ssm: "SSM parameter containing the Base64-encoded GitHub App private key."
key\_base64\_ssm.arn: "ARN of the SSM parameter containing the GitHub App private key."
key\_base64\_ssm.name: "Name of the SSM parameter containing the GitHub App private key."
id: "GitHub App ID."
id\_ssm: "SSM parameter containing the GitHub App ID."
id\_ssm.arn: "ARN of the SSM parameter containing the GitHub App ID."
id\_ssm.name: "Name of the SSM parameter containing the GitHub App ID."
installation\_id: "GitHub App installation ID for the primary scale-set installation."
installation\_id\_ssm: "SSM parameter containing the primary GitHub App installation ID."
installation\_id\_ssm.arn: "ARN of the SSM parameter containing the primary GitHub App installation ID."
installation\_id\_ssm.name: "Name of the SSM parameter containing the primary GitHub App installation ID."
webhook\_secret: "GitHub App webhook secret."
webhook\_secret\_ssm: "SSM parameter containing the GitHub App webhook secret."
webhook\_secret\_ssm.arn: "ARN of the SSM parameter containing the GitHub App webhook secret."
webhook\_secret\_ssm.name: "Name of the SSM parameter containing the GitHub App webhook secret."
}
additional\_apps: "Additional GitHub Apps used to distribute GitHub API requests."
additional\_apps.key\_base64: "Base64-encoded private key for an additional GitHub App."
additional\_apps.key\_base64\_ssm: "SSM parameter containing an additional App private key."
additional\_apps.key\_base64\_ssm.arn: "ARN of the SSM parameter containing an additional App private key."
additional\_apps.key\_base64\_ssm.name: "Name of the SSM parameter containing an additional App private key."
additional\_apps.id: "ID of an additional GitHub App."
additional\_apps.id\_ssm: "SSM parameter containing an additional GitHub App ID."
additional\_apps.id\_ssm.arn: "ARN of the SSM parameter containing an additional GitHub App ID."
additional\_apps.id\_ssm.name: "Name of the SSM parameter containing an additional GitHub App ID."
additional\_apps.installation\_id: "Optional installation ID for an additional GitHub App."
additional\_apps.installation\_id\_ssm: "SSM parameter containing an additional App installation ID."
additional\_apps.installation\_id\_ssm.arn: "ARN of the SSM parameter containing an additional App installation ID."
additional\_apps.installation\_id\_ssm.name: "Name of the SSM parameter containing an additional App installation ID."
enterprise\_server.url: "GitHub Enterprise Server URL."
enterprise\_server.ssl\_verify: "Whether to verify the GitHub Enterprise Server TLS certificate."
runner\_owner: "GitHub organization or owner/repository path for organization- or repository-level scale-set registration."
runner\_registration\_level: "GitHub scale-set registration scope: organization or repository."
user\_agent: "User-Agent value sent with GitHub API requests."
} |
object({
app = optional(object({
key_base64 = optional(string)
key_base64_ssm = optional(object({
arn = string
name = string
}))
id = optional(string)
id_ssm = optional(object({
arn = string
name = string
}))
installation_id = optional(string)
installation_id_ssm = optional(object({
arn = string
name = string
}))
webhook_secret = optional(string)
webhook_secret_ssm = optional(object({
arn = string
name = string
}))
}), null)
additional_apps = optional(list(object({
key_base64 = optional(string)
key_base64_ssm = optional(object({ arn = string, name = string }))
id = optional(string)
id_ssm = optional(object({ arn = string, name = string }))
installation_id = optional(string)
installation_id_ssm = optional(object({ arn = string, name = string }))
})), [])
enterprise_server = optional(object({
url = optional(string, null)
ssl_verify = optional(bool, true)
}), {})
runner_owner = optional(string, null)
runner_registration_level = optional(string, "organization")
user_agent = optional(string, "github-aws-runners")
})
| `{}` | no | | [global\_config\_lambda](#input\_global\_config\_lambda) | Global Lambda configuration shared by all runner lanes.

global\_config\_lambda = {
artifact.s3.bucket: "S3 bucket containing Lambda deployment artifacts."
runtime: "Default Lambda runtime."
architecture: "Default Lambda instruction-set architecture."
principals: "Additional AWS principals allowed to invoke the Lambda functions."
principals.type: "Principal type, such as AWS account, service, or organization."
principals.identifiers: "Identifiers allowed for the principal type."
subnet\_ids: "Subnets used by Lambda functions."
security\_group\_ids: "Security groups attached to Lambda functions."
tags: "Tags applied to Lambda functions and related resources."
role.path: "IAM path used for Lambda execution roles."
role.permissions\_boundary: "Optional IAM permissions boundary ARN for Lambda execution roles."
} |
object({
artifact = optional(object({
s3 = optional(object({
bucket = optional(string, null)
}), {})
}), {})
runtime = optional(string, "nodejs24.x")
architecture = optional(string, "arm64")
principals = optional(list(object({
type = string
identifiers = list(string)
})), [])
subnet_ids = optional(list(string), [])
security_group_ids = optional(list(string), [])
tags = optional(map(string), {})
role = optional(object({
path = optional(string, null)
permissions_boundary = optional(string, null)
}), {})
})
| `{}` | no | | [global\_config\_observability](#input\_global\_config\_observability) | Global observability configuration shared by all runner lanes.

global\_config\_observability = {
logs.level: "Log level for module resources."
logs.retention\_in\_days: "CloudWatch log retention period in days."
logs.kms\_key\_id: "KMS key ID used to encrypt CloudWatch log groups."
logs.class: "CloudWatch log group class."
logs.tags: "Tags applied to CloudWatch log groups."
tracing.mode: "Tracing mode used by instrumented resources."
tracing.capture\_http\_requests: "Whether HTTP requests are captured by tracing."
tracing.capture\_error: "Whether errors are captured by tracing."
metrics.enabled: "Whether module metrics are enabled."
metrics.namespace: "CloudWatch namespace used for module metrics."
metrics.metric.github\_app\_rate\_limit.enabled: "Whether GitHub App rate-limit metrics are emitted."
metrics.metric.job\_retry.enabled: "Whether job-retry metrics are emitted."
metrics.metric.spot\_termination\_warning.enabled: "Whether spot-termination warning metrics are emitted."
} |
object({
logs = optional(object({
level = optional(string, "info")
retention_in_days = optional(number, 180)
kms_key_id = optional(string, null)
class = optional(string, "STANDARD")
tags = optional(map(string), {})
}), {})
tracing = optional(object({
mode = optional(string, null)
capture_http_requests = optional(bool, false)
capture_error = optional(bool, false)
}), {})
metrics = optional(object({
enabled = optional(bool, false)
namespace = optional(string, "GitHub Runners")
metric = optional(object({
github_app_rate_limit = optional(object({
enabled = optional(bool, true)
}), {})
job_retry = optional(object({
enabled = optional(bool, true)
}), {})
spot_termination_warning = optional(object({
enabled = optional(bool, true)
}), {})
}), {})
}), {})
})
| `{}` | no | -| [global\_config\_orchestration\_provider](#input\_global\_config\_orchestration\_provider) | Global orchestration-provider configuration shared by all runner lanes.

global\_config\_orchestration\_provider = {
webhook: {
queue\_selection\_strategy: "Strategy used to select the build queue for a webhook event."
eventbridge.enabled: "Whether EventBridge integration is enabled for webhook events."
eventbridge.accept\_events: "Event types accepted by the EventBridge integration."
matcher\_config\_parameter\_store\_tier: "SSM Parameter Store tier used for matcher configuration."
runner.boot\_time\_in\_minutes: "Expected runner boot time used by orchestration."
runner.ephemeral: "Whether runners created by the orchestration provider are ephemeral."
runner.jit\_config\_enabled: "Whether JIT runner configuration is enabled."
runner.maximum\_count: "Maximum number of runners that orchestration may create."
github.repository\_white\_list: "Repositories allowed to use the webhook configuration."
lambda.artifact.zip: "Local ZIP artifact used for orchestration Lambda functions."
lambda.artifact.s3.key: "S3 object key for the orchestration Lambda artifact."
lambda.artifact.s3.object\_version: "Optional S3 object version for the orchestration Lambda artifact."
lambda.scale.up.memory\_size: "Memory allocated to the scale-up Lambda."
lambda.scale.up.timeout: "Timeout in seconds for the scale-up Lambda."
lambda.scale.up.reserved\_concurrent\_executions: "Reserved concurrent executions for the scale-up Lambda."
lambda.scale.up.job\_queued\_check\_enabled: "Whether the scale-up Lambda checks queued jobs."
lambda.scale.up.event\_source\_mapping.batch\_size: "Maximum records passed to one scale-up Lambda invocation."
lambda.scale.up.event\_source\_mapping.maximum\_batching\_window\_in\_seconds: "Maximum time to batch records before invoking the scale-up Lambda."
lambda.scale.up.tags: "Tags applied to the scale-up Lambda."
lambda.scale.down.memory\_size: "Memory allocated to the scale-down Lambda."
lambda.scale.down.timeout: "Timeout in seconds for the scale-down Lambda."
lambda.scale.down.schedule\_expression: "Schedule expression for scale-down processing."
lambda.scale.down.minimum\_running\_time\_in\_minutes: "Minimum runner lifetime before scale-down."
lambda.scale.down.idle\_confirmation\_seconds: "Seconds a runner must consistently report not-busy before scale-down terminates it; 0 disables the confirmation window."
lambda.scale.down.idle\_config: "Scheduled minimum idle-runner pool settings."
lambda.scale.down.idle\_config.cron: "Cron expression defining when the idle-runner count applies."
lambda.scale.down.idle\_config.timeZone: "Time zone used to evaluate the idle-runner schedule."
lambda.scale.down.idle\_config.idleCount: "Minimum number of idle runners maintained during the schedule."
lambda.scale.down.idle\_config.evictionStrategy: "Strategy used when evicting idle runners."
lambda.scale.down.tags: "Tags applied to the scale-down Lambda."
lambda.webhook.artifact.zip: "Local ZIP artifact used for the webhook Lambda."
lambda.webhook.artifact.s3.key: "S3 object key for the webhook Lambda artifact."
lambda.webhook.artifact.s3.object\_version: "Optional S3 object version for the webhook Lambda artifact."
lambda.webhook.api\_gateway\_access\_log\_settings: "API Gateway access-log destination and format."
lambda.webhook.api\_gateway\_access\_log\_settings.destination\_arn: "ARN of the API Gateway access-log destination."
lambda.webhook.api\_gateway\_access\_log\_settings.format: "API Gateway access-log format."
lambda.webhook.memory\_size: "Memory allocated to the webhook Lambda."
lambda.webhook.timeout: "Timeout in seconds for the webhook Lambda."
lambda.webhook.tags: "Tags applied to the webhook Lambda."
lambda.pool.memory\_size: "Memory allocated to the pool Lambda."
lambda.pool.timeout: "Timeout in seconds for the pool Lambda."
lambda.pool.reserved\_concurrent\_executions: "Reserved concurrent executions for the pool Lambda."
lambda.pool.config: "Scheduled runner-pool size configuration."
lambda.pool.config.schedule\_expression: "Schedule expression for the pool size."
lambda.pool.config.schedule\_expression\_timezone: "Time zone used to evaluate the pool schedule."
lambda.pool.config.size: "Runner pool size applied by the schedule."
lambda.pool.include\_busy\_runners: "Whether busy runners are included in pool sizing."
lambda.pool.runner\_owner: "GitHub organization that owns the runner pool."
lambda.pool.tags: "Tags applied to the pool Lambda."
queue.delay\_webhook\_event: "Seconds a webhook event remains invisible in the build queue before processing."
queue.job\_queue\_retention\_in\_seconds: "Seconds a queued job is retained before it is purged."
queue.visibility\_timeout\_seconds: "Build queue visibility timeout in seconds."
queue.redrive\_build\_queue.enabled: "Whether the build queue dead-letter queue is enabled."
queue.redrive\_build\_queue.maxReceiveCount: "Maximum receives before a message is moved to the dead-letter queue."
queue.tags: "Tags applied to build queues."
queue.encryption.kms\_data\_key\_reuse\_period\_seconds: "KMS data-key reuse period for queue encryption."
queue.encryption.kms\_master\_key\_id: "KMS key ID used for queue encryption."
queue.encryption.sqs\_managed\_sse\_enabled: "Whether SQS-managed server-side encryption is enabled."
}
} |
object({
webhook = optional(object({
queue_selection_strategy = optional(string, "first")
eventbridge = optional(object({
enabled = optional(bool, true)
accept_events = optional(list(string), [])
}), {})
matcher_config_parameter_store_tier = optional(string, "Standard")
runner = optional(object({
boot_time_in_minutes = optional(number, 5)
ephemeral = optional(bool, false)
jit_config_enabled = optional(bool, null)
maximum_count = optional(number, null)
}), {})

github = optional(object({
repository_white_list = optional(list(string), [])
}), {})

lambda = optional(object({
artifact = optional(object({
zip = optional(string, null)
s3 = optional(object({
key = string
object_version = optional(string, null)
}), null)
}), {})
scale = optional(object({
up = optional(object({
memory_size = optional(number, 512)
timeout = optional(number, 30)
reserved_concurrent_executions = optional(number, 1)
job_queued_check_enabled = optional(bool, null)
event_source_mapping = optional(object({
batch_size = optional(number, 10)
maximum_batching_window_in_seconds = optional(number, 0)
}), {})
tags = optional(map(string), {})
}), {})
down = optional(object({
memory_size = optional(number, 512)
timeout = optional(number, 60)
schedule_expression = optional(string, "cron(*/5 * * * ? *)")
minimum_running_time_in_minutes = optional(number, null)
idle_confirmation_seconds = optional(number, 0)
idle_config = optional(list(object({
cron = string
timeZone = string
idleCount = number
evictionStrategy = optional(string, "oldest_first")
})), [])
tags = optional(map(string), {})
}), {})
}), {})
webhook = optional(object({
artifact = optional(object({
zip = optional(string, null)
s3 = optional(object({
key = string
object_version = optional(string, null)
}), null)
}), {})
api_gateway_access_log_settings = optional(object({
destination_arn = string
format = string
}), null)
memory_size = optional(number, 256)
timeout = optional(number, 10)
tags = optional(map(string), {})
}), {})
pool = optional(object({
memory_size = optional(number, 512)
timeout = optional(number, 60)
reserved_concurrent_executions = optional(number, 1)
config = optional(list(object({
schedule_expression = string
schedule_expression_timezone = optional(string)
size = number
})), [])
include_busy_runners = optional(bool, false)
runner_owner = optional(string, null)
tags = optional(map(string), {})
}), {})
}), {})

queue = optional(object({
delay_webhook_event = optional(number, 30)
job_queue_retention_in_seconds = optional(number, 86400)
visibility_timeout_seconds = optional(number, 180)
redrive_build_queue = optional(object({
enabled = optional(bool, false)
maxReceiveCount = optional(number, null)
}), {
enabled = false
maxReceiveCount = null
})
tags = optional(map(string), {})
encryption = optional(object({
kms_data_key_reuse_period_seconds = number
kms_master_key_id = string
sqs_managed_sse_enabled = bool
}), {
kms_data_key_reuse_period_seconds = null
kms_master_key_id = null
sqs_managed_sse_enabled = true
})
}), {})
}), {})
})
| `{}` | no | +| [global\_config\_orchestration\_provider](#input\_global\_config\_orchestration\_provider) | Global orchestration-provider configuration shared by all runner lanes.

global\_config\_orchestration\_provider = {
webhook: {
queue\_selection\_strategy: "Strategy used to select the build queue for a webhook event."
eventbridge.enabled: "Whether EventBridge integration is enabled for webhook events."
eventbridge.accept\_events: "Event types accepted by the EventBridge integration."
matcher\_config\_parameter\_store\_tier: "SSM Parameter Store tier used for matcher configuration."
runner.boot\_time\_in\_minutes: "Expected runner boot time used by orchestration."
runner.ephemeral: "Whether runners created by the orchestration provider are ephemeral."
runner.jit\_config\_enabled: "Whether JIT runner configuration is enabled."
runner.maximum\_count: "Maximum number of runners that orchestration may create."
github.repository\_white\_list: "Repositories allowed to use the webhook configuration."
lambda.artifact.zip: "Local ZIP artifact used for orchestration Lambda functions."
lambda.artifact.s3.key: "S3 object key for the orchestration Lambda artifact."
lambda.artifact.s3.object\_version: "Optional S3 object version for the orchestration Lambda artifact."
lambda.scale.up.memory\_size: "Memory allocated to the scale-up Lambda."
lambda.scale.up.timeout: "Timeout in seconds for the scale-up Lambda."
lambda.scale.up.reserved\_concurrent\_executions: "Reserved concurrent executions for the scale-up Lambda."
lambda.scale.up.job\_queued\_check\_enabled: "Whether the scale-up Lambda checks queued jobs."
lambda.scale.up.event\_source\_mapping.batch\_size: "Maximum records passed to one scale-up Lambda invocation."
lambda.scale.up.event\_source\_mapping.maximum\_batching\_window\_in\_seconds: "Maximum time to batch records before invoking the scale-up Lambda."
lambda.scale.up.tags: "Tags applied to the scale-up Lambda."
lambda.scale.down.memory\_size: "Memory allocated to the scale-down Lambda."
lambda.scale.down.timeout: "Timeout in seconds for the scale-down Lambda."
lambda.scale.down.schedule\_expression: "Schedule expression for scale-down processing."
lambda.scale.down.minimum\_running\_time\_in\_minutes: "Minimum runner lifetime before scale-down."
lambda.scale.down.idle\_confirmation\_seconds: "Seconds a runner must consistently report not-busy before scale-down terminates it; 0 disables the confirmation window."
lambda.scale.down.idle\_config: "Scheduled minimum idle-runner pool settings."
lambda.scale.down.idle\_config.cron: "Cron expression defining when the idle-runner count applies."
lambda.scale.down.idle\_config.timeZone: "Time zone used to evaluate the idle-runner schedule."
lambda.scale.down.idle\_config.idleCount: "Minimum number of idle runners maintained during the schedule."
lambda.scale.down.idle\_config.evictionStrategy: "Strategy used when evicting idle runners."
lambda.scale.down.tags: "Tags applied to the scale-down Lambda."
lambda.webhook.artifact.zip: "Local ZIP artifact used for the webhook Lambda."
lambda.webhook.artifact.s3.key: "S3 object key for the webhook Lambda artifact."
lambda.webhook.artifact.s3.object\_version: "Optional S3 object version for the webhook Lambda artifact."
lambda.webhook.api\_gateway\_access\_log\_settings: "API Gateway access-log destination and format."
lambda.webhook.api\_gateway\_access\_log\_settings.destination\_arn: "ARN of the API Gateway access-log destination."
lambda.webhook.api\_gateway\_access\_log\_settings.format: "API Gateway access-log format."
lambda.webhook.memory\_size: "Memory allocated to the webhook Lambda."
lambda.webhook.timeout: "Timeout in seconds for the webhook Lambda."
lambda.webhook.tags: "Tags applied to the webhook Lambda."
lambda.pool.memory\_size: "Memory allocated to the pool Lambda."
lambda.pool.timeout: "Timeout in seconds for the pool Lambda."
lambda.pool.reserved\_concurrent\_executions: "Reserved concurrent executions for the pool Lambda."
lambda.pool.config: "Scheduled runner-pool size configuration."
lambda.pool.config.schedule\_expression: "Schedule expression for the pool size."
lambda.pool.config.schedule\_expression\_timezone: "Time zone used to evaluate the pool schedule."
lambda.pool.config.size: "Runner pool size applied by the schedule."
lambda.pool.include\_busy\_runners: "Whether busy runners are included in pool sizing."
lambda.pool.runner\_owner: "GitHub organization that owns the runner pool."
lambda.pool.tags: "Tags applied to the pool Lambda."
queue.delay\_webhook\_event: "Seconds a webhook event remains invisible in the build queue before processing."
queue.job\_queue\_retention\_in\_seconds: "Seconds a queued job is retained before it is purged."
queue.visibility\_timeout\_seconds: "Build queue visibility timeout in seconds."
queue.redrive\_build\_queue.enabled: "Whether the build queue dead-letter queue is enabled."
queue.redrive\_build\_queue.maxReceiveCount: "Maximum receives before a message is moved to the dead-letter queue."
queue.tags: "Tags applied to build queues."
queue.encryption.kms\_data\_key\_reuse\_period\_seconds: "KMS data-key reuse period for queue encryption."
queue.encryption.kms\_master\_key\_id: "KMS key ID used for queue encryption."
queue.encryption.sqs\_managed\_sse\_enabled: "Whether SQS-managed server-side encryption is enabled."
}
} |
object({
webhook = optional(object({
queue_selection_strategy = optional(string, "first")
eventbridge = optional(object({
enabled = optional(bool, true)
accept_events = optional(list(string), [])
}), {})
matcher_config_parameter_store_tier = optional(string, "Standard")
runner = optional(object({
boot_time_in_minutes = optional(number, 5)
ephemeral = optional(bool, false)
jit_config_enabled = optional(bool, null)
maximum_count = optional(number, null)
}), {})

github = optional(object({
repository_white_list = optional(list(string), [])
}), {})

lambda = optional(object({
artifact = optional(object({
zip = optional(string, null)
s3 = optional(object({
key = string
object_version = optional(string, null)
}), null)
}), {})
scale = optional(object({
up = optional(object({
memory_size = optional(number, 512)
timeout = optional(number, 30)
reserved_concurrent_executions = optional(number, 1)
job_queued_check_enabled = optional(bool, null)
event_source_mapping = optional(object({
batch_size = optional(number, 10)
maximum_batching_window_in_seconds = optional(number, 0)
}), {})
tags = optional(map(string), {})
}), {})
down = optional(object({
memory_size = optional(number, 512)
timeout = optional(number, 60)
schedule_expression = optional(string, "cron(*/5 * * * ? *)")
minimum_running_time_in_minutes = optional(number, null)
idle_confirmation_seconds = optional(number, 0)
idle_config = optional(list(object({
cron = string
timeZone = string
idleCount = number
evictionStrategy = optional(string, "oldest_first")
})), [])
tags = optional(map(string), {})
}), {})
}), {})
webhook = optional(object({
artifact = optional(object({
zip = optional(string, null)
s3 = optional(object({
key = string
object_version = optional(string, null)
}), null)
}), {})
api_gateway_access_log_settings = optional(object({
destination_arn = string
format = string
}), null)
memory_size = optional(number, 256)
timeout = optional(number, 10)
tags = optional(map(string), {})
}), {})
pool = optional(object({
memory_size = optional(number, 512)
timeout = optional(number, 60)
reserved_concurrent_executions = optional(number, 1)
config = optional(list(object({
schedule_expression = string
schedule_expression_timezone = optional(string)
size = number
})), [])
include_busy_runners = optional(bool, false)
runner_owner = optional(string, null)
tags = optional(map(string), {})
}), {})
}), {})

queue = optional(object({
delay_webhook_event = optional(number, 30)
job_queue_retention_in_seconds = optional(number, 86400)
visibility_timeout_seconds = optional(number, 180)
redrive_build_queue = optional(object({
enabled = optional(bool, false)
maxReceiveCount = optional(number, null)
}), {
enabled = false
maxReceiveCount = null
})
tags = optional(map(string), {})
encryption = optional(object({
kms_data_key_reuse_period_seconds = number
kms_master_key_id = string
sqs_managed_sse_enabled = bool
}), {
kms_data_key_reuse_period_seconds = null
kms_master_key_id = null
sqs_managed_sse_enabled = true
})
}), {})

}), {})

scale_set = optional(object({
grouping = optional(object({
strategy = optional(string, "compute_provider")
custom = optional(object({
groups = map(object({
runner_configs = set(string)
}))
}), null)
}), {})
container = optional(object({
image = optional(string, null)
user = optional(string, "10001:10001")
health_port = optional(number, 8080)
health_path = optional(string, "/healthz")
health_check_command = optional(list(string), null)
health_check_interval = optional(number, 30)
health_check_timeout = optional(number, 5)
health_check_retries = optional(number, 3)
health_check_start_period = optional(number, 30)
health_stale_after_seconds = optional(number, 180)
shutdown_timeout_seconds = optional(number, 110)
session_close_timeout_seconds = optional(number, 10)
reconnect_initial_backoff_seconds = optional(number, 1)
reconnect_max_backoff_seconds = optional(number, 30)
stop_timeout_seconds = optional(number, 120)
}), {})
config_store = optional(object({
path_prefix = optional(string, null)
tier = optional(string, "Standard")
tags = optional(map(string), {})
}), {})
ecs = optional(object({
cluster = optional(object({
mode = optional(string, "managed")
arn = optional(string, null)
name = optional(string, null)
container_insights = optional(bool, true)
}), {})
task = optional(object({
cpu = optional(number, 512)
memory = optional(number, 1024)
cpu_architecture = optional(string, "X86_64")
ephemeral_storage = optional(object({
size_in_gib = number
}), null)
}), {})
service = optional(object({
platform_version = optional(string, "LATEST")
}), {})
iam = optional(object({
path = optional(string, "/")
permissions_boundary = optional(string, null)
}), {})
}), {})
network = optional(object({
vpc_id = optional(string, null)
subnet_ids = optional(set(string), null)
https_egress = optional(object({
ipv4_cidrs = optional(set(string), ["0.0.0.0/0"])
ipv6_cidrs = optional(set(string), [])
}), {})
}), {})
logging = optional(object({
retention_in_days = optional(number, 30)
kms_key_arn = optional(string, null)
log_group_class = optional(string, "STANDARD")
tags = optional(map(string), {})
}), {})
tags = optional(map(string), {})
}), {})
})
| `{}` | no | | [global\_config\_storage\_provider](#input\_global\_config\_storage\_provider) | Global storage-provider configuration shared by all runner lanes.

global\_config\_storage\_provider = {
aws.ssm.paths.root: "Root path for SSM parameters."
aws.ssm.paths.app: "Path segment for application parameters."
aws.ssm.paths.webhook: "Path segment for webhook parameters."
aws.ssm.paths.tokens: "Path segment for runner token parameters."
aws.ssm.paths.config: "Path segment for runner configuration parameters."
aws.ssm.kms\_key\_id: "KMS key ID used to encrypt SSM parameters."
aws.ssm.tags: "Tags applied to SSM resources."
aws.ssm.parameters.tags: "Tags applied to runner configuration parameters."
aws.ssm.housekeeper.schedule\_expression: "Schedule for the SSM parameter housekeeper."
aws.ssm.housekeeper.state: "EventBridge rule state for the SSM housekeeper."
aws.ssm.housekeeper.tags: "Tags applied to the SSM housekeeper resources."
aws.ssm.housekeeper.lambda.artifact.zip: "Local ZIP artifact used for the SSM housekeeper Lambda."
aws.ssm.housekeeper.lambda.artifact.s3.key: "S3 object key for the SSM housekeeper Lambda."
aws.ssm.housekeeper.lambda.artifact.s3.object\_version: "Optional S3 object version for the SSM housekeeper artifact."
aws.ssm.housekeeper.lambda.memory\_size: "Memory allocated to the SSM housekeeper Lambda."
aws.ssm.housekeeper.lambda.timeout: "Timeout in seconds for the SSM housekeeper Lambda."
aws.ssm.housekeeper.config.tokenPath: "Parameter path containing runner tokens to clean up."
aws.ssm.housekeeper.config.minimumDaysOld: "Minimum age in days before an old token is eligible for cleanup."
aws.ssm.housekeeper.config.dryRun: "Whether the SSM housekeeper reports cleanup without deleting parameters."
} |
object({
aws = optional(object({
ssm = optional(object({
paths = optional(object({
root = optional(string, null)
app = optional(string, "app")
webhook = optional(string, "webhook")
tokens = optional(string, "runners/tokens")
config = optional(string, "runners/config")
}), {})
kms_key_id = optional(string, null)
tags = optional(map(string), {})
parameters = optional(object({
tags = optional(map(string), {})
}), {})
housekeeper = optional(object({
schedule_expression = optional(string, "rate(1 day)")
state = optional(string, "ENABLED")
tags = optional(map(string), {})
lambda = optional(object({
artifact = optional(object({
zip = optional(string, null)
s3 = optional(object({
key = string
object_version = optional(string, null)
}), null)
}), {})
memory_size = optional(number, 512)
timeout = optional(number, 60)
}), {})
config = optional(object({
tokenPath = optional(string, null)
minimumDaysOld = optional(number, 1)
dryRun = optional(bool, false)
}), {})
}), {})
}), null)
}), {})
})
| `{}` | no | | [iam\_overrides](#input\_iam\_overrides) | This map provides the possibility to override some IAM defaults. The following attributes are supported: `instance_profile_name` overrides the instance profile name used in the launch template. `runner_role_arn` overrides the IAM role ARN used for the runner instances. |
object({
override_instance_profile = optional(bool, null)
instance_profile_name = optional(string, null)
override_runner_role = optional(bool, null)
runner_role_arn = optional(string, null)
})
|
{
"instance_profile_name": null,
"override_instance_profile": false,
"override_runner_role": false,
"runner_role_arn": null
}
| no | | [instance\_profile\_path](#input\_instance\_profile\_path) | The path that will be added to the instance\_profile, if not set the environment name will be used. | `string` | `null` | no | @@ -189,7 +189,7 @@ module "multi-runner" { | [logging\_retention\_in\_days](#input\_logging\_retention\_in\_days) | Specifies the number of days you want to retain log events for the lambda log group. Possible values are: 0, 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, and 3653. | `number` | `180` | no | | [matcher\_config\_parameter\_store\_tier](#input\_matcher\_config\_parameter\_store\_tier) | The tier of the parameter store for the matcher configuration. Valid values are `Standard`, and `Advanced`. | `string` | `"Standard"` | no | | [metrics](#input\_metrics) | Configuration for metrics created by the module, by default metrics are disabled to avoid additional costs. When metrics are enable all metrics are created unless explicit configured otherwise. |
object({
enable = optional(bool, false)
namespace = optional(string, "GitHub Runners")
metric = optional(object({
enable_github_app_rate_limit = optional(bool, true)
enable_job_retry = optional(bool, true)
enable_spot_termination_warning = optional(bool, true)
}), {})
})
| `{}` | no | -| [multi\_runner\_config](#input\_multi\_runner\_config) | Accepts either the stable v1 runner configuration shape or the provider-boundary v2 shape. Entries with `runner_config` use the v1 shape; entries without `runner_config` use the v2 shape. A v2 entry does not need matcher configuration. A v2 entry must be acknowledged with `experimental_features = ["multi-runner-v2"]`; the v2 shape is experimental and may change before graduation.

multi\_runner\_config = {
runner\_config: {
runner\_os: "The EC2 Operating System type to use for action runner instances (linux, osx, windows)."
runner\_architecture: "The platform architecture of the runner instance\_type."
runner\_metadata\_options: "(Optional) Metadata options for the ec2 runner instances."
ami: "(Optional) AMI configuration for the action runner instances. This object allows you to specify all AMI-related settings in one place."
create\_service\_linked\_role\_spot: (Optional) create the serviced linked role for spot instances that is required by the scale-up lambda.
credit\_specification: "(Optional) The credit specification of the runner instance\_type. Can be unset, `standard` or `unlimited`.
delay\_webhook\_event: "The number of seconds the event accepted by the webhook is invisible on the queue before the scale up lambda will receive the event."
disable\_runner\_autoupdate: "Disable the auto update of the github runner agent. Be aware there is a grace period of 30 days, see also the [GitHub article](https://github.blog/changelog/2022-02-01-github-actions-self-hosted-runners-can-now-disable-automatic-updates/)"
ebs\_optimized: "The EC2 EBS optimized configuration."
enable\_ephemeral\_runners: "Enable ephemeral runners, runners will only be used once."
enable\_job\_queued\_check: Enables JIT configuration for creating runners instead of registration token based registraton. JIT configuration will only be applied for ephemeral runners. By default JIT configuration is enabled for ephemeral runners an can be disabled via this override. When running on GHES without support for JIT configuration this variable should be set to true for ephemeral runners."
enable\_on\_demand\_failover\_for\_errors: "Enable on-demand failover. For example to fall back to on demand when no spot capacity is available the variable can be set to `InsufficientInstanceCapacity`. When not defined the default behavior is to retry later."
scale\_errors: "List of AWS error codes that should trigger retry during scale up. This list replaces the module default scale-up retry errors"
enable\_organization\_runners: "Register runners to organization, instead of repo level"
enable\_runner\_binaries\_syncer: "Option to disable the lambda to sync GitHub runner distribution, useful when using a pre-build AMI."
enable\_ssm\_on\_runners: "Enable to allow access the runner instances for debugging purposes via SSM. Note that this adds additional permissions to the runner instances."
enable\_userdata: "Should the userdata script be enabled for the runner. Set this to false if you are using your own prebuilt AMI."
instance\_allocation\_strategy: "The allocation strategy for creating instances. For spot, AWS recommends `price-capacity-optimized`; for on-demand, use `lowest-price` or `prioritized`. The AWS default is `lowest-price`."
instance\_type\_priorities: "A map of instance type to priority for the `prioritized` and `capacity-optimized-prioritized` allocation strategies. Lower numbers mean higher priority. If not provided, priorities are assigned based on the order of `instance_types`."
instance\_max\_spot\_price: "Max price price for spot instances per hour. This variable will be passed to the create fleet as max spot price for the fleet."
instance\_target\_capacity\_type: "Default lifecycle used for runner instances, can be either `spot` or `on-demand`."
instance\_types: "List of instance types for the action runner. Defaults are based on runner\_os (al2023 for linux, macOS Sequoia for osx, Windows Server Core for win)."
job\_queue\_retention\_in\_seconds: "The number of seconds the job is held in the queue before it is purged"
minimum\_running\_time\_in\_minutes: "The time an ec2 action runner should be running at minimum before terminated if not busy."
pool\_runner\_owner: "The pool will deploy runners to the GitHub org ID, set this value to the org to which you want the runners deployed. Repo level is not supported."
runner\_additional\_security\_group\_ids: "List of additional security groups IDs to apply to the runner. If added outside the multi\_runner\_config block, the additional security group(s) will be applied to all runner configs. If added inside the multi\_runner\_config, the additional security group(s) will be applied to the individual runner."
runner\_as\_root: "Run the action runner under the root user. Variable `runner_run_as` will be ignored."
runner\_boot\_time\_in\_minutes: "The minimum time for an EC2 runner to boot and register as a runner."
scale\_down\_idle\_confirmation\_seconds: "Number of seconds a runner must consistently report not-busy before scale-down terminates it. GitHub's busy flag can be stale, so a single not-busy reading is not sufficient evidence a runner is idle. 0 keeps the previous single-reading behaviour."
runner\_disable\_default\_labels: "Disable default labels for the runners (os, architecture and `self-hosted`). If enabled, the runner will only have the extra labels provided in `runner_extra_labels`. In case you on own start script is used, this configuration parameter needs to be parsed via SSM."
runner\_extra\_labels: "Extra (custom) labels for the runners (GitHub). Separate each label by a comma. Labels checks on the webhook can be enforced by setting `multi_runner_config.matcherConfig.exactMatch`. GitHub read-only labels should not be provided."
runner\_group\_name: "Name of the runner group."
runner\_name\_prefix: "Prefix for the GitHub runner name."
runner\_run\_as: "Run the GitHub actions agent as user."
runners\_maximum\_count: "The maximum number of runners that will be created. Setting the variable to `-1` disables the maximum check."
scale\_down\_schedule\_expression: "Scheduler expression to check every x for scale down."
scale\_up\_reserved\_concurrent\_executions: "Amount of reserved concurrent executions for the scale-up lambda function. A value of 0 disables lambda from being triggered and -1 removes any concurrency limitations."
lambda\_event\_source\_mapping\_batch\_size: "(Optional) Maximum number of records per Lambda invocation for this runner flavor. Overrides the module-level `lambda_event_source_mapping_batch_size` when set."
lambda\_event\_source\_mapping\_maximum\_batching\_window\_in\_seconds: "(Optional) Maximum seconds to gather records before invoking Lambda for this runner flavor. Overrides the module-level `lambda_event_source_mapping_maximum_batching_window_in_seconds` when set."
userdata\_template: "Alternative user-data template, replacing the default template. By providing your own user\_data you have to take care of installing all required software, including the action runner. Variables userdata\_pre/post\_install are ignored."
enable\_jit\_config: "Overwrite the default behavior for JIT configuration. By default JIT configuration is enabled for ephemeral runners and disabled for non-ephemeral runners. In case of GHES check first if the JIT config API is available. In case you are upgrading from 3.x to 4.x you can set `enable_jit_config` to `false` to avoid a breaking change when having your own AMI."
enable\_runner\_detailed\_monitoring: "Should detailed monitoring be enabled for the runner. Set this to true if you want to use detailed monitoring. See https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-cloudwatch-new.html for details."
enable\_cloudwatch\_agent: "Enabling the cloudwatch agent on the ec2 runner instances, the runner contains default config. Configuration can be overridden via `cloudwatch_config`."
cloudwatch\_config: "(optional) Replaces the module default cloudwatch log config. See https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Agent-Configuration-File-Details.html for details."
userdata\_pre\_install: "Script to be ran before the GitHub Actions runner is installed on the EC2 instances"
userdata\_post\_install: "Script to be ran after the GitHub Actions runner is installed on the EC2 instances"
runner\_hook\_job\_started: "Script to be ran in the runner environment at the beginning of every job"
runner\_hook\_job\_completed: "Script to be ran in the runner environment at the end of every job"
runner\_ec2\_tags: "Map of tags that will be added to the launch template instance tag specifications."
runner\_iam\_role\_managed\_policy\_arns: "Attach AWS or customer-managed IAM policies (by ARN) to the runner IAM role"
vpc\_id: "The VPC for security groups of the action runners. If not set uses the value of `var.vpc_id`."
subnet\_ids: "List of subnets in which the action runners will be launched, the subnets needs to be subnets in the `vpc_id`. If not set, uses the value of `var.subnet_ids`."
idle\_config: "List of time period that can be defined as cron expression to keep a minimum amount of runners active instead of scaling down to 0. By defining this list you can ensure that in time periods that match the cron expression within 5 seconds a runner is kept idle."
license\_specifications: "Optional EC2 License Manager license configuration ARNs for the runner launch template. Required for macOS dedicated-host runners when the host resource group uses a Mac dedicated host license configuration."
use\_dedicated\_host: "Experimental! Can be removed / changed without trigger a major release. Whether to use EC2 dedicated hosts for the runners. Needed for macos runners Note that using dedicated hosts can increase cost significantly."
runner\_log\_files: "(optional) Replaces the module default cloudwatch log config. See https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Agent-Configuration-File-Details.html for details."
block\_device\_mappings: "The EC2 instance block device configuration. Takes the following keys: `device_name`, `delete_on_termination`, `volume_type`, `volume_size`, `encrypted`, `iops`, `throughput`, `kms_key_id`, `snapshot_id`, `volume_initialization_rate`."
job\_retry: "Experimental! Can be removed / changed without trigger a major release. Configure job retries. The configuration enables job retries (for ephemeral runners). After creating the instances a message will be published to a job retry queue. The job retry check lambda is checking after a delay if the job is queued. If not the message will be published again on the scale-up (build queue). Using this feature can impact the rate limit of the GitHub app."
pool\_config: "The configuration for updating the pool. The `pool_size` to adjust to by the events triggered by the `schedule_expression`. For example you can configure a cron expression for week days to adjust the pool to 10 and another expression for the weekend to adjust the pool to 1. Use `schedule_expression_timezone` to override the schedule time zone (defaults to UTC)."
ssm\_ttl\_seconds.tokens: "Optional TTL in seconds for the SSM parameters holding the runner registration token / JIT config. When set, the parameters are created with an SSM expiration policy so SSM deletes them itself after the TTL passes. Requires the Advanced parameter tier for every token parameter, which incurs additional costs. Expiration is enforced asynchronously by SSM; the SSM housekeeper lambda remains as a backstop. Must be a positive number, and should comfortably exceed the runner boot time so the config does not expire before the instance reads it."
iam\_overrides: "Allows to (optionally) override the instance profile and runner role created by the module. Set `override_instance_profile` to true and provide the `instance_profile_name` to use an existing instance profile. Set `override_runner_role` to true and provide the `runner_role_arn` to use an existing role for the runner instances."
}
# V2 contract
tags: "Tags applied to resources created for this runner configuration."
runner: "Runner settings such as the operating system, architecture, labels, hooks, runner group, name prefix, and IAM role configuration."
lambda: "Lambda settings such as runtime, architecture, networking, tags, and execution-role options for this runner configuration."
# Webhook, queue, and scale-up/scale-down orchestration settings.
orchestration\_provider: {
webhook: {
matcherConfig: "Label matching and dynamic-label policy used to route workflow jobs to this runner configuration."
runner: "Runner lifecycle settings including boot time, ephemeral mode, JIT configuration, and maximum runner count."
queue: "Build queue delay, retention, visibility timeout, redrive, and tags."
}
}
ssm: "SSM parameter paths, tags, and housekeeper settings for runner configuration storage."
observability: "Logging, tracing, and metric settings for the resources in this runner configuration."
# Compute settings for the runner provider.
compute\_provider: {
aws: {
ec2: "AWS EC2 runner settings, including AMI selection, instance types, capacity strategy, VPC and subnet placement, storage, user data, and runner access."
}
}
matcherConfig: {
labelMatchers: "The list of list of labels supported by the runner configuration. `[[self-hosted, linux, x64, example]]`"
exactMatch: "DEPRECATED: Use `bidirectionalLabelMatch` instead. If set to true all labels in the workflow job must match the GitHub labels (os, architecture and `self-hosted`). When false if __any__ workflow label matches it will trigger the webhook. Note: this only checks that workflow labels are a subset of runner labels, not the reverse."
bidirectionalLabelMatch: "If set to true, the runner labels and workflow job labels must be an exact two-way match (same set, any order, no extras or missing labels). This is stricter than `exactMatch` which only checks that workflow labels are a subset of runner labels. When false, if __any__ workflow label matches it will trigger the webhook."
priority: "If set it defines the priority of the matcher, the matcher with the lowest priority will be evaluated first. Default is 999, allowed values 0-999."
enableDynamicLabels: "Experimental! When true the dispatcher allows `ghr-*` dynamic labels for jobs routed to this runner. Default false."
awsDynamicLabelsPolicy: "Optional AWS dynamic label policy evaluated by the dispatcher. Only effective when `enableDynamicLabels = true`. Jobs whose provider dynamic labels violate every matching runner's policy are rejected with a 202 (a warning is logged). Evaluation: if `allowed_keys` is set, only those keys are accepted; keys in `blocked_keys` are always rejected (cannot be used together with `allowed_keys`); keys in `restricted_keys` are allowed only when their value passes the rule; a key not listed anywhere is allowed. Schema: `{ allowed_keys = [], blocked_keys = [], restricted_keys = { = { allowed = [globs], denied = [globs], max = number|string } } }`. Keys use the dynamic label suffix, e.g. `instance-type` for `ghr-ec2-instance-type`."
}
redrive\_build\_queue: "Set options to attach (optional) a dead letter queue to the build queue, the queue between the webhook and the scale up lambda. You have the following options. 1. Disable by setting `enabled` to false. 2. Enable by setting `enabled` to `true`, `maxReceiveCount` to a number of max retries."
} |
map(object({
# V1 contract
runner_config = optional(object({
runner_os = string
runner_architecture = string
runner_metadata_options = optional(map(any), {
instance_metadata_tags = "enabled"
http_endpoint = "enabled"
http_tokens = "required"
http_put_response_hop_limit = 1
})
ami = optional(object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
id_ssm_parameter_arn = optional(string, null)
kms_key_arn = optional(string, null)
}), null)
create_service_linked_role_spot = optional(bool, false)
credit_specification = optional(string, null)
delay_webhook_event = optional(number, 30)
disable_runner_autoupdate = optional(bool, false)
ebs_optimized = optional(bool, false)
enable_ephemeral_runners = optional(bool, false)
enable_job_queued_check = optional(bool, null)
enable_on_demand_failover_for_errors = optional(list(string), [])
scale_errors = optional(list(string), [
"UnfulfillableCapacity",
"MaxSpotInstanceCountExceeded",
"TargetCapacityLimitExceededException",
"RequestLimitExceeded",
"ResourceLimitExceeded",
"MaxSpotInstanceCountExceeded",
"MaxSpotFleetRequestCountExceeded",
"InsufficientInstanceCapacity",
"InsufficientCapacityOnHost",
])
enable_organization_runners = optional(bool, false)
enable_runner_binaries_syncer = optional(bool, true)
enable_ssm_on_runners = optional(bool, false)
enable_userdata = optional(bool, true)
instance_allocation_strategy = optional(string, "lowest-price")
instance_type_priorities = optional(map(number), null)
instance_max_spot_price = optional(string, null)
instance_target_capacity_type = optional(string, "spot")
instance_types = list(string)
job_queue_retention_in_seconds = optional(number, 86400)
minimum_running_time_in_minutes = optional(number, null)
pool_runner_owner = optional(string, null)
runner_as_root = optional(bool, false)
runner_boot_time_in_minutes = optional(number, 5)
scale_down_idle_confirmation_seconds = optional(number, 0)
runner_disable_default_labels = optional(bool, false)
runner_extra_labels = optional(list(string), [])
runner_group_name = optional(string, "Default")
runner_name_prefix = optional(string, "")
runner_run_as = optional(string, "ec2-user")
runners_maximum_count = number
runner_additional_security_group_ids = optional(list(string), [])
scale_down_schedule_expression = optional(string, "cron(*/5 * * * ? *)")
scale_up_reserved_concurrent_executions = optional(number, 1)
lambda_event_source_mapping_batch_size = optional(number, null)
lambda_event_source_mapping_maximum_batching_window_in_seconds = optional(number, null)
userdata_template = optional(string, null)
userdata_content = optional(string, null)
enable_jit_config = optional(bool, null)
enable_runner_detailed_monitoring = optional(bool, false)
enable_cloudwatch_agent = optional(bool, true)
cloudwatch_config = optional(string, null)
userdata_pre_install = optional(string, "")
userdata_post_install = optional(string, "")
runner_hook_job_started = optional(string, "")
runner_hook_job_completed = optional(string, "")
runner_ec2_tags = optional(map(string), {})
runner_iam_role_managed_policy_arns = optional(list(string), [])
vpc_id = optional(string, null)
subnet_ids = optional(list(string), null)
idle_config = optional(list(object({
cron = string
timeZone = string
idleCount = number
evictionStrategy = optional(string, "oldest_first")
})), [])
cpu_options = optional(object({
core_count = optional(number)
threads_per_core = optional(number)
amd_sev_snp = optional(string)
nested_virtualization = optional(string)
}), null)
network_interfaces = optional(list(object({
associate_carrier_ip_address = optional(bool)
associate_public_ip_address = optional(bool)
delete_on_termination = optional(bool)
description = optional(string)
device_index = optional(number)
interface_type = optional(string)
ipv4_address_count = optional(number)
ipv4_addresses = optional(list(string))
ipv4_prefix_count = optional(number)
ipv4_prefixes = optional(list(string))
ipv6_address_count = optional(number)
ipv6_addresses = optional(list(string))
ipv6_prefix_count = optional(number)
ipv6_prefixes = optional(list(string))
network_card_index = optional(number)
network_interface_id = optional(string)
primary_ipv6 = optional(bool)
private_ip_address = optional(string)
security_groups = optional(list(string))
subnet_id = optional(string)
connection_tracking_specification = optional(object({
tcp_established_timeout = optional(number)
udp_stream_timeout = optional(number)
udp_timeout = optional(number)
}))
ena_srd_specification = optional(object({
ena_srd_enabled = optional(bool)
ena_srd_udp_specification = optional(object({
ena_srd_udp_enabled = optional(bool)
}))
}))
})), [])
placement = optional(object({
affinity = optional(string)
availability_zone = optional(string)
group_id = optional(string)
group_name = optional(string)
host_id = optional(string)
host_resource_group_arn = optional(string)
spread_domain = optional(string)
tenancy = optional(string)
partition_number = optional(number)
}), null)
license_specifications = optional(list(object({
license_configuration_arn = string
})), [])
use_dedicated_host = optional(bool, false)
runner_log_files = optional(list(object({
log_group_name = string
prefix_log_group = bool
file_path = string
log_stream_name = string
log_class = optional(string, "STANDARD")
})), null)
block_device_mappings = optional(list(object({
delete_on_termination = optional(bool, true)
device_name = optional(string, "/dev/xvda")
encrypted = optional(bool, true)
iops = optional(number)
kms_key_id = optional(string)
snapshot_id = optional(string)
throughput = optional(number)
volume_initialization_rate = optional(number)
volume_size = number
volume_type = optional(string, "gp3")
})), [{
volume_size = 30
}])
pool_config = optional(list(object({
schedule_expression = string
schedule_expression_timezone = optional(string)
size = number
})), [])
ssm_ttl_seconds = optional(object({
tokens = optional(number, null)
}), {})
job_retry = optional(object({
enable = optional(bool, false)
delay_in_seconds = optional(number, 300)
delay_backoff = optional(number, 2)
lambda_memory_size = optional(number, 256)
lambda_timeout = optional(number, 30)
max_attempts = optional(number, 1)
}), {})
iam_overrides = optional(object({
override_instance_profile = optional(bool, null)
instance_profile_name = optional(string, null)
override_runner_role = optional(bool, null)
runner_role_arn = optional(string, null)
}), {
override_instance_profile = false
instance_profile_name = null
override_runner_role = false
runner_role_arn = null
})
}), null)
matcherConfig = optional(object({
labelMatchers = list(list(string))
exactMatch = optional(bool, false)
bidirectionalLabelMatch = optional(bool, false)
priority = optional(number, 999)
enableDynamicLabels = optional(bool, false)
awsDynamicLabelsPolicy = optional(any, null)
}), null)
redrive_build_queue = optional(object({
enabled = bool
maxReceiveCount = number
}), {
enabled = false
maxReceiveCount = null
})

# V2 Contract
tags = optional(map(string), {})

runner = optional(object({
os = optional(string, null)
architecture = optional(string, null)
disable_default_labels = optional(bool, null)
extra_labels = optional(list(string), null)
group_name = optional(string, null)
name_prefix = optional(string, null)
run_as_root = optional(bool, null)
run_as = optional(string, null)
auto_update_disabled = optional(bool, null)
tags = optional(map(string), {})
hooks = optional(object({
job_started = optional(string, null)
job_completed = optional(string, null)
}), {})
iam = optional(object({
role = optional(object({
arn = string
}), null)
managed_policy_arns = optional(map(string), null)
additional_trust_policy_json = optional(string, null)
path = optional(string, null)
permissions_boundary = optional(string, null)
}), {})
}), {})

lambda = optional(object({
runtime = optional(string, null)
architecture = optional(string, null)
subnet_ids = optional(list(string), null)
security_group_ids = optional(list(string), null)
tags = optional(map(string), {})
role = optional(object({
path = optional(string, null)
permissions_boundary = optional(string, null)
}), {})
}), {})

orchestration_provider = optional(object({
webhook = optional(object({
runner = optional(object({
boot_time_in_minutes = optional(number, null)
ephemeral = optional(bool, null)
jit_config_enabled = optional(bool, null)
maximum_count = optional(number, null)
}), {})
github = optional(object({
organization_runners = optional(bool, false)
}), {})
matcherConfig = optional(object({
labelMatchers = list(list(string))
exactMatch = optional(bool, false)
bidirectionalLabelMatch = optional(bool, false)
priority = optional(number, 999)
dynamic_labels_enabled = optional(bool, false)
awsDynamicLabelsPolicy = optional(object({
allowed_keys = optional(list(string), [])
blocked_keys = optional(list(string), [])
restricted_keys = optional(map(object({
allowed = optional(list(string), [])
denied = optional(list(string), [])
max = optional(string, null)
})), {})
}), null)
}), null)
queue = optional(object({
delay_webhook_event = optional(number, null)
job_queue_retention_in_seconds = optional(number, null)
visibility_timeout_seconds = optional(number, null)
redrive_build_queue = optional(object({
enabled = optional(bool, null)
maxReceiveCount = optional(number, null)
}), null)
tags = optional(map(string), {})
}), {})
lambda = optional(object({
scale = optional(object({
up = optional(object({
memory_size = optional(number, null)
timeout = optional(number, null)
reserved_concurrent_executions = optional(number, null)
job_queued_check_enabled = optional(bool, null)
event_source_mapping = optional(object({
batch_size = optional(number, null)
maximum_batching_window_in_seconds = optional(number, null)
}), {})
tags = optional(map(string), {})
}), {})
down = optional(object({
memory_size = optional(number, null)
timeout = optional(number, null)
schedule_expression = optional(string, null)
minimum_running_time_in_minutes = optional(number, null)
idle_confirmation_seconds = optional(number, null)
idle_config = optional(list(object({
cron = string
timeZone = string
idleCount = number
evictionStrategy = optional(string, "oldest_first")
})), null)
tags = optional(map(string), {})
}), {})
}), {})
pool = optional(object({
memory_size = optional(number, null)
timeout = optional(number, null)
reserved_concurrent_executions = optional(number, null)
config = optional(list(object({
schedule_expression = string
schedule_expression_timezone = optional(string)
size = number
})), null)
include_busy_runners = optional(bool, null)
runner_owner = optional(string, null)
tags = optional(map(string), {})
}), {})
}), {})
job_retry = optional(object({
enabled = optional(bool, false)
delay_in_seconds = optional(number, 300)
delay_backoff = optional(number, 2)
max_attempts = optional(number, 1)
tags = optional(map(string), {})
lambda = optional(object({
memory_size = optional(number, 256)
reserved_concurrent_executions = optional(number, 1)
timeout = optional(number, 30)
}), {})
}), {})
}), null)
}), {})

storage_provider = optional(object({
aws = optional(object({
ssm = optional(object({
ttl_seconds = optional(object({
tokens = optional(number, null)
}), {})
paths = optional(object({
root = optional(string, null)
tokens = optional(string, null)
config = optional(string, null)
}), {})
tags = optional(map(string), {})
parameters = optional(object({
tags = optional(map(string), {})
}), {})
housekeeper = optional(object({
schedule_expression = optional(string, null)
state = optional(string, null)
tags = optional(map(string), {})
lambda = optional(object({
artifact = optional(object({
zip = optional(string, null)
s3 = optional(object({
key = string
object_version = optional(string, null)
}), null)
}), {})
memory_size = optional(number, null)
timeout = optional(number, null)
}), {})
config = optional(object({
tokenPath = optional(string, null)
minimumDaysOld = optional(number, null)
dryRun = optional(bool, null)
}), {})
}), {})
}), {})
}), {})
}), {})

observability = optional(object({
logs = optional(object({
level = optional(string, null)
retention_in_days = optional(number, null)
kms_key_id = optional(string, null)
class = optional(string, null)
tags = optional(map(string), {})
}), {})
tracing = optional(object({
mode = optional(string, null)
capture_http_requests = optional(bool, null)
capture_error = optional(bool, null)
}), {})
metrics = optional(object({
enabled = optional(bool, null)
namespace = optional(string, null)
metric = optional(object({
github_app_rate_limit = optional(object({
enabled = optional(bool, null)
}), {})
job_retry = optional(object({
enabled = optional(bool, null)
}), {})
spot_termination_warning = optional(object({
enabled = optional(bool, null)
}), {})
}), {})
}), {})
}), {})

compute_provider = optional(object({
aws = optional(object({
ec2 = optional(object({
metadata_options = optional(object({
instance_metadata_tags = optional(string, "enabled")
http_endpoint = optional(string, "enabled")
http_tokens = optional(string, "required")
http_put_response_hop_limit = optional(number, 1)
}), {})
ami = optional(object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
ssm_parameter = optional(object({
path = optional(string, null)
arn = optional(string, null)
}), null)
kms_key = optional(object({
arn = string
}), null)
}), null)
block_device_mappings = optional(list(object({
delete_on_termination = optional(bool, true)
device_name = optional(string, "/dev/xvda")
encrypted = optional(bool, true)
iops = optional(number)
kms_key_id = optional(string)
snapshot_id = optional(string)
throughput = optional(number)
volume_initialization_rate = optional(number)
volume_size = number
volume_type = optional(string, "gp3")
})), [{ volume_size = 30 }])
create_service_linked_role_spot = optional(bool, false)
credit_specification = optional(string, null)
ebs_optimized = optional(bool, false)
cloudwatch_agent = optional(object({
enabled = optional(bool, true)
config = optional(string, null)
}), {})
binaries_syncer = optional(object({
enabled = optional(bool, null)
}), {})
detailed_monitoring_enabled = optional(bool, false)
ssm_enabled = optional(bool, false)
user_data = optional(object({
enabled = optional(bool, true)
template = optional(string, null)
content = optional(string, null)
pre_install = optional(string, "")
post_install = optional(string, "")
debug_logging_enabled = optional(bool, false)
}), {})
instance_allocation_strategy = optional(string, "lowest-price")
instance_max_spot_price = optional(string, null)
instance_target_capacity_type = optional(string, "spot")
instance_type_priorities = optional(map(number), null)
instance_types = optional(list(string), [])
additional_security_group_ids = optional(list(string), null)
managed_security_group_enabled = optional(bool, null)
egress_rules = optional(list(object({
cidr_blocks = list(string)
ipv6_cidr_blocks = list(string)
prefix_list_ids = list(string)
from_port = number
protocol = string
security_groups = list(string)
self = bool
to_port = number
description = string
})), null)
instance_profile_path = optional(string, null)
key_name = optional(string, null)
associate_public_ipv4_address = optional(bool, null)
instance_profile = optional(object({
name = string
}), null)
on_demand_failover_for_errors = optional(list(string), [])
scale_errors = optional(list(string), [
"UnfulfillableCapacity",
"MaxSpotInstanceCountExceeded",
"TargetCapacityLimitExceededException",
"RequestLimitExceeded",
"ResourceLimitExceeded",
"MaxSpotInstanceCountExceeded",
"MaxSpotFleetRequestCountExceeded",
"InsufficientInstanceCapacity",
"InsufficientCapacityOnHost",
])
subnet_ids = optional(list(string), null)
vpc_id = optional(string, null)
cpu_options = optional(object({
core_count = optional(number)
threads_per_core = optional(number)
amd_sev_snp = optional(string)
nested_virtualization = optional(string)
}), null)
network_interfaces = optional(list(object({
associate_carrier_ip_address = optional(bool)
associate_public_ip_address = optional(bool)
delete_on_termination = optional(bool)
description = optional(string)
device_index = optional(number)
interface_type = optional(string)
ipv4_address_count = optional(number)
ipv4_addresses = optional(list(string))
ipv4_prefix_count = optional(number)
ipv4_prefixes = optional(list(string))
ipv6_address_count = optional(number)
ipv6_addresses = optional(list(string))
ipv6_prefix_count = optional(number)
ipv6_prefixes = optional(list(string))
network_card_index = optional(number)
network_interface_id = optional(string)
primary_ipv6 = optional(bool)
private_ip_address = optional(string)
security_groups = optional(list(string))
subnet_id = optional(string)
connection_tracking_specification = optional(object({
tcp_established_timeout = optional(number)
udp_stream_timeout = optional(number)
udp_timeout = optional(number)
}))
ena_srd_specification = optional(object({
ena_srd_enabled = optional(bool)
ena_srd_udp_specification = optional(object({
ena_srd_udp_enabled = optional(bool)
}))
}))
})), [])
placement = optional(object({
affinity = optional(string)
availability_zone = optional(string)
group_id = optional(string)
group_name = optional(string)
host_id = optional(string)
host_resource_group_arn = optional(string)
spread_domain = optional(string)
tenancy = optional(string)
partition_number = optional(number)
}), null)
license_specifications = optional(list(object({
license_configuration_arn = string
})), [])
use_dedicated_host = optional(bool, false)
log_files = optional(list(object({
log_group_name = string
prefix_log_group = bool
file_path = string
log_stream_name = string
log_class = optional(string, "STANDARD")
})), null)
tags = optional(map(string), {})
}), null)
}), {})
}), {})
}))
| `{}` | no | +| [multi\_runner\_config](#input\_multi\_runner\_config) | Accepts either the stable v1 runner configuration shape or the provider-boundary v2 shape. Entries with `runner_config` use the v1 shape; entries without `runner_config` use the v2 shape. A v2 entry does not need matcher configuration. A v2 entry must be acknowledged with `experimental_features = ["multi-runner-v2"]`; the v2 shape is experimental and may change before graduation.

multi\_runner\_config = {
runner\_config: {
runner\_os: "The EC2 Operating System type to use for action runner instances (linux, osx, windows)."
runner\_architecture: "The platform architecture of the runner instance\_type."
runner\_metadata\_options: "(Optional) Metadata options for the ec2 runner instances."
ami: "(Optional) AMI configuration for the action runner instances. This object allows you to specify all AMI-related settings in one place."
create\_service\_linked\_role\_spot: (Optional) create the serviced linked role for spot instances that is required by the scale-up lambda.
credit\_specification: "(Optional) The credit specification of the runner instance\_type. Can be unset, `standard` or `unlimited`.
delay\_webhook\_event: "The number of seconds the event accepted by the webhook is invisible on the queue before the scale up lambda will receive the event."
disable\_runner\_autoupdate: "Disable the auto update of the github runner agent. Be aware there is a grace period of 30 days, see also the [GitHub article](https://github.blog/changelog/2022-02-01-github-actions-self-hosted-runners-can-now-disable-automatic-updates/)"
ebs\_optimized: "The EC2 EBS optimized configuration."
enable\_ephemeral\_runners: "Enable ephemeral runners, runners will only be used once."
enable\_job\_queued\_check: Enables JIT configuration for creating runners instead of registration token based registraton. JIT configuration will only be applied for ephemeral runners. By default JIT configuration is enabled for ephemeral runners an can be disabled via this override. When running on GHES without support for JIT configuration this variable should be set to true for ephemeral runners."
enable\_on\_demand\_failover\_for\_errors: "Enable on-demand failover. For example to fall back to on demand when no spot capacity is available the variable can be set to `InsufficientInstanceCapacity`. When not defined the default behavior is to retry later."
scale\_errors: "List of AWS error codes that should trigger retry during scale up. This list replaces the module default scale-up retry errors"
enable\_organization\_runners: "Register runners to organization, instead of repo level"
enable\_runner\_binaries\_syncer: "Option to disable the lambda to sync GitHub runner distribution, useful when using a pre-build AMI."
enable\_ssm\_on\_runners: "Enable to allow access the runner instances for debugging purposes via SSM. Note that this adds additional permissions to the runner instances."
enable\_userdata: "Should the userdata script be enabled for the runner. Set this to false if you are using your own prebuilt AMI."
instance\_allocation\_strategy: "The allocation strategy for creating instances. For spot, AWS recommends `price-capacity-optimized`; for on-demand, use `lowest-price` or `prioritized`. The AWS default is `lowest-price`."
instance\_type\_priorities: "A map of instance type to priority for the `prioritized` and `capacity-optimized-prioritized` allocation strategies. Lower numbers mean higher priority. If not provided, priorities are assigned based on the order of `instance_types`."
instance\_max\_spot\_price: "Max price price for spot instances per hour. This variable will be passed to the create fleet as max spot price for the fleet."
instance\_target\_capacity\_type: "Default lifecycle used for runner instances, can be either `spot` or `on-demand`."
instance\_types: "List of instance types for the action runner. Defaults are based on runner\_os (al2023 for linux, macOS Sequoia for osx, Windows Server Core for win)."
job\_queue\_retention\_in\_seconds: "The number of seconds the job is held in the queue before it is purged"
minimum\_running\_time\_in\_minutes: "The time an ec2 action runner should be running at minimum before terminated if not busy."
pool\_runner\_owner: "The pool will deploy runners to the GitHub org ID, set this value to the org to which you want the runners deployed. Repo level is not supported."
runner\_additional\_security\_group\_ids: "List of additional security groups IDs to apply to the runner. If added outside the multi\_runner\_config block, the additional security group(s) will be applied to all runner configs. If added inside the multi\_runner\_config, the additional security group(s) will be applied to the individual runner."
runner\_as\_root: "Run the action runner under the root user. Variable `runner_run_as` will be ignored."
runner\_boot\_time\_in\_minutes: "The minimum time for an EC2 runner to boot and register as a runner."
scale\_down\_idle\_confirmation\_seconds: "Number of seconds a runner must consistently report not-busy before scale-down terminates it. GitHub's busy flag can be stale, so a single not-busy reading is not sufficient evidence a runner is idle. 0 keeps the previous single-reading behaviour."
runner\_disable\_default\_labels: "Disable default labels for the runners (os, architecture and `self-hosted`). If enabled, the runner will only have the extra labels provided in `runner_extra_labels`. In case you on own start script is used, this configuration parameter needs to be parsed via SSM."
runner\_extra\_labels: "Extra (custom) labels for the runners (GitHub). Separate each label by a comma. Labels checks on the webhook can be enforced by setting `multi_runner_config.matcherConfig.exactMatch`. GitHub read-only labels should not be provided."
runner\_group\_name: "Name of the runner group."
runner\_name\_prefix: "Prefix for the GitHub runner name."
runner\_run\_as: "Run the GitHub actions agent as user."
runners\_maximum\_count: "The maximum number of runners that will be created. Setting the variable to `-1` disables the maximum check."
scale\_down\_schedule\_expression: "Scheduler expression to check every x for scale down."
scale\_up\_reserved\_concurrent\_executions: "Amount of reserved concurrent executions for the scale-up lambda function. A value of 0 disables lambda from being triggered and -1 removes any concurrency limitations."
lambda\_event\_source\_mapping\_batch\_size: "(Optional) Maximum number of records per Lambda invocation for this runner flavor. Overrides the module-level `lambda_event_source_mapping_batch_size` when set."
lambda\_event\_source\_mapping\_maximum\_batching\_window\_in\_seconds: "(Optional) Maximum seconds to gather records before invoking Lambda for this runner flavor. Overrides the module-level `lambda_event_source_mapping_maximum_batching_window_in_seconds` when set."
userdata\_template: "Alternative user-data template, replacing the default template. By providing your own user\_data you have to take care of installing all required software, including the action runner. Variables userdata\_pre/post\_install are ignored."
enable\_jit\_config: "Overwrite the default behavior for JIT configuration. By default JIT configuration is enabled for ephemeral runners and disabled for non-ephemeral runners. In case of GHES check first if the JIT config API is available. In case you are upgrading from 3.x to 4.x you can set `enable_jit_config` to `false` to avoid a breaking change when having your own AMI."
enable\_runner\_detailed\_monitoring: "Should detailed monitoring be enabled for the runner. Set this to true if you want to use detailed monitoring. See https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-cloudwatch-new.html for details."
enable\_cloudwatch\_agent: "Enabling the cloudwatch agent on the ec2 runner instances, the runner contains default config. Configuration can be overridden via `cloudwatch_config`."
cloudwatch\_config: "(optional) Replaces the module default cloudwatch log config. See https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Agent-Configuration-File-Details.html for details."
userdata\_pre\_install: "Script to be ran before the GitHub Actions runner is installed on the EC2 instances"
userdata\_post\_install: "Script to be ran after the GitHub Actions runner is installed on the EC2 instances"
runner\_hook\_job\_started: "Script to be ran in the runner environment at the beginning of every job"
runner\_hook\_job\_completed: "Script to be ran in the runner environment at the end of every job"
runner\_ec2\_tags: "Map of tags that will be added to the launch template instance tag specifications."
runner\_iam\_role\_managed\_policy\_arns: "Attach AWS or customer-managed IAM policies (by ARN) to the runner IAM role"
vpc\_id: "The VPC for security groups of the action runners. If not set uses the value of `var.vpc_id`."
subnet\_ids: "List of subnets in which the action runners will be launched, the subnets needs to be subnets in the `vpc_id`. If not set, uses the value of `var.subnet_ids`."
idle\_config: "List of time period that can be defined as cron expression to keep a minimum amount of runners active instead of scaling down to 0. By defining this list you can ensure that in time periods that match the cron expression within 5 seconds a runner is kept idle."
license\_specifications: "Optional EC2 License Manager license configuration ARNs for the runner launch template. Required for macOS dedicated-host runners when the host resource group uses a Mac dedicated host license configuration."
use\_dedicated\_host: "Experimental! Can be removed / changed without trigger a major release. Whether to use EC2 dedicated hosts for the runners. Needed for macos runners Note that using dedicated hosts can increase cost significantly."
runner\_log\_files: "(optional) Replaces the module default cloudwatch log config. See https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Agent-Configuration-File-Details.html for details."
block\_device\_mappings: "The EC2 instance block device configuration. Takes the following keys: `device_name`, `delete_on_termination`, `volume_type`, `volume_size`, `encrypted`, `iops`, `throughput`, `kms_key_id`, `snapshot_id`, `volume_initialization_rate`."
job\_retry: "Experimental! Can be removed / changed without trigger a major release. Configure job retries. The configuration enables job retries (for ephemeral runners). After creating the instances a message will be published to a job retry queue. The job retry check lambda is checking after a delay if the job is queued. If not the message will be published again on the scale-up (build queue). Using this feature can impact the rate limit of the GitHub app."
pool\_config: "The configuration for updating the pool. The `pool_size` to adjust to by the events triggered by the `schedule_expression`. For example you can configure a cron expression for week days to adjust the pool to 10 and another expression for the weekend to adjust the pool to 1. Use `schedule_expression_timezone` to override the schedule time zone (defaults to UTC)."
ssm\_ttl\_seconds.tokens: "Optional TTL in seconds for the SSM parameters holding the runner registration token / JIT config. When set, the parameters are created with an SSM expiration policy so SSM deletes them itself after the TTL passes. Requires the Advanced parameter tier for every token parameter, which incurs additional costs. Expiration is enforced asynchronously by SSM; the SSM housekeeper lambda remains as a backstop. Must be a positive number, and should comfortably exceed the runner boot time so the config does not expire before the instance reads it."
iam\_overrides: "Allows to (optionally) override the instance profile and runner role created by the module. Set `override_instance_profile` to true and provide the `instance_profile_name` to use an existing instance profile. Set `override_runner_role` to true and provide the `runner_role_arn` to use an existing role for the runner instances."
}
# V2 contract
tags: "Tags applied to resources created for this runner configuration."
runner: "Runner settings such as the operating system, architecture, labels, hooks, runner group, name prefix, and IAM role configuration."
lambda: "Lambda settings such as runtime, architecture, networking, tags, and execution-role options for this runner configuration."
# Webhook, queue, and scale-up/scale-down orchestration settings.
orchestration\_provider: {
webhook: {
matcherConfig: "Label matching and dynamic-label policy used to route workflow jobs to this runner configuration."
runner: "Runner lifecycle settings including boot time, ephemeral mode, JIT configuration, and maximum runner count."
queue: "Build queue delay, retention, visibility timeout, redrive, and tags."
}
}
ssm: "SSM parameter paths, tags, and housekeeper settings for runner configuration storage."
observability: "Logging, tracing, and metric settings for the resources in this runner configuration."
# Compute settings for the runner provider.
compute\_provider: {
aws: {
ec2: "AWS EC2 runner settings, including AMI selection, instance types, capacity strategy, VPC and subnet placement, storage, user data, and runner access."
}
}
matcherConfig: {
labelMatchers: "The list of list of labels supported by the runner configuration. `[[self-hosted, linux, x64, example]]`"
exactMatch: "DEPRECATED: Use `bidirectionalLabelMatch` instead. If set to true all labels in the workflow job must match the GitHub labels (os, architecture and `self-hosted`). When false if __any__ workflow label matches it will trigger the webhook. Note: this only checks that workflow labels are a subset of runner labels, not the reverse."
bidirectionalLabelMatch: "If set to true, the runner labels and workflow job labels must be an exact two-way match (same set, any order, no extras or missing labels). This is stricter than `exactMatch` which only checks that workflow labels are a subset of runner labels. When false, if __any__ workflow label matches it will trigger the webhook."
priority: "If set it defines the priority of the matcher, the matcher with the lowest priority will be evaluated first. Default is 999, allowed values 0-999."
enableDynamicLabels: "Experimental! When true the dispatcher allows `ghr-*` dynamic labels for jobs routed to this runner. Default false."
awsDynamicLabelsPolicy: "Optional AWS dynamic label policy evaluated by the dispatcher. Only effective when `enableDynamicLabels = true`. Jobs whose provider dynamic labels violate every matching runner's policy are rejected with a 202 (a warning is logged). Evaluation: if `allowed_keys` is set, only those keys are accepted; keys in `blocked_keys` are always rejected (cannot be used together with `allowed_keys`); keys in `restricted_keys` are allowed only when their value passes the rule; a key not listed anywhere is allowed. Schema: `{ allowed_keys = [], blocked_keys = [], restricted_keys = { = { allowed = [globs], denied = [globs], max = number|string } } }`. Keys use the dynamic label suffix, e.g. `instance-type` for `ghr-ec2-instance-type`."
}
redrive\_build\_queue: "Set options to attach (optional) a dead letter queue to the build queue, the queue between the webhook and the scale up lambda. You have the following options. 1. Disable by setting `enabled` to false. 2. Enable by setting `enabled` to `true`, `maxReceiveCount` to a number of max retries."
} |
map(object({
# V1 contract
runner_config = optional(object({
runner_os = string
runner_architecture = string
runner_metadata_options = optional(map(any), {
instance_metadata_tags = "enabled"
http_endpoint = "enabled"
http_tokens = "required"
http_put_response_hop_limit = 1
})
ami = optional(object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
id_ssm_parameter_arn = optional(string, null)
kms_key_arn = optional(string, null)
}), null)
create_service_linked_role_spot = optional(bool, false)
credit_specification = optional(string, null)
delay_webhook_event = optional(number, 30)
disable_runner_autoupdate = optional(bool, false)
ebs_optimized = optional(bool, false)
enable_ephemeral_runners = optional(bool, false)
enable_job_queued_check = optional(bool, null)
enable_on_demand_failover_for_errors = optional(list(string), [])
scale_errors = optional(list(string), [
"UnfulfillableCapacity",
"MaxSpotInstanceCountExceeded",
"TargetCapacityLimitExceededException",
"RequestLimitExceeded",
"ResourceLimitExceeded",
"MaxSpotInstanceCountExceeded",
"MaxSpotFleetRequestCountExceeded",
"InsufficientInstanceCapacity",
"InsufficientCapacityOnHost",
])
enable_organization_runners = optional(bool, false)
enable_runner_binaries_syncer = optional(bool, true)
enable_ssm_on_runners = optional(bool, false)
enable_userdata = optional(bool, true)
instance_allocation_strategy = optional(string, "lowest-price")
instance_type_priorities = optional(map(number), null)
instance_max_spot_price = optional(string, null)
instance_target_capacity_type = optional(string, "spot")
instance_types = list(string)
job_queue_retention_in_seconds = optional(number, 86400)
minimum_running_time_in_minutes = optional(number, null)
pool_runner_owner = optional(string, null)
runner_as_root = optional(bool, false)
runner_boot_time_in_minutes = optional(number, 5)
scale_down_idle_confirmation_seconds = optional(number, 0)
runner_disable_default_labels = optional(bool, false)
runner_extra_labels = optional(list(string), [])
runner_group_name = optional(string, "Default")
runner_name_prefix = optional(string, "")
runner_run_as = optional(string, "ec2-user")
runners_maximum_count = number
runner_additional_security_group_ids = optional(list(string), [])
scale_down_schedule_expression = optional(string, "cron(*/5 * * * ? *)")
scale_up_reserved_concurrent_executions = optional(number, 1)
lambda_event_source_mapping_batch_size = optional(number, null)
lambda_event_source_mapping_maximum_batching_window_in_seconds = optional(number, null)
userdata_template = optional(string, null)
userdata_content = optional(string, null)
enable_jit_config = optional(bool, null)
enable_runner_detailed_monitoring = optional(bool, false)
enable_cloudwatch_agent = optional(bool, true)
cloudwatch_config = optional(string, null)
userdata_pre_install = optional(string, "")
userdata_post_install = optional(string, "")
runner_hook_job_started = optional(string, "")
runner_hook_job_completed = optional(string, "")
runner_ec2_tags = optional(map(string), {})
runner_iam_role_managed_policy_arns = optional(list(string), [])
vpc_id = optional(string, null)
subnet_ids = optional(list(string), null)
idle_config = optional(list(object({
cron = string
timeZone = string
idleCount = number
evictionStrategy = optional(string, "oldest_first")
})), [])
cpu_options = optional(object({
core_count = optional(number)
threads_per_core = optional(number)
amd_sev_snp = optional(string)
nested_virtualization = optional(string)
}), null)
network_interfaces = optional(list(object({
associate_carrier_ip_address = optional(bool)
associate_public_ip_address = optional(bool)
delete_on_termination = optional(bool)
description = optional(string)
device_index = optional(number)
interface_type = optional(string)
ipv4_address_count = optional(number)
ipv4_addresses = optional(list(string))
ipv4_prefix_count = optional(number)
ipv4_prefixes = optional(list(string))
ipv6_address_count = optional(number)
ipv6_addresses = optional(list(string))
ipv6_prefix_count = optional(number)
ipv6_prefixes = optional(list(string))
network_card_index = optional(number)
network_interface_id = optional(string)
primary_ipv6 = optional(bool)
private_ip_address = optional(string)
security_groups = optional(list(string))
subnet_id = optional(string)
connection_tracking_specification = optional(object({
tcp_established_timeout = optional(number)
udp_stream_timeout = optional(number)
udp_timeout = optional(number)
}))
ena_srd_specification = optional(object({
ena_srd_enabled = optional(bool)
ena_srd_udp_specification = optional(object({
ena_srd_udp_enabled = optional(bool)
}))
}))
})), [])
placement = optional(object({
affinity = optional(string)
availability_zone = optional(string)
group_id = optional(string)
group_name = optional(string)
host_id = optional(string)
host_resource_group_arn = optional(string)
spread_domain = optional(string)
tenancy = optional(string)
partition_number = optional(number)
}), null)
license_specifications = optional(list(object({
license_configuration_arn = string
})), [])
use_dedicated_host = optional(bool, false)
runner_log_files = optional(list(object({
log_group_name = string
prefix_log_group = bool
file_path = string
log_stream_name = string
log_class = optional(string, "STANDARD")
})), null)
block_device_mappings = optional(list(object({
delete_on_termination = optional(bool, true)
device_name = optional(string, "/dev/xvda")
encrypted = optional(bool, true)
iops = optional(number)
kms_key_id = optional(string)
snapshot_id = optional(string)
throughput = optional(number)
volume_initialization_rate = optional(number)
volume_size = number
volume_type = optional(string, "gp3")
})), [{
volume_size = 30
}])
pool_config = optional(list(object({
schedule_expression = string
schedule_expression_timezone = optional(string)
size = number
})), [])
ssm_ttl_seconds = optional(object({
tokens = optional(number, null)
}), {})
job_retry = optional(object({
enable = optional(bool, false)
delay_in_seconds = optional(number, 300)
delay_backoff = optional(number, 2)
lambda_memory_size = optional(number, 256)
lambda_timeout = optional(number, 30)
max_attempts = optional(number, 1)
}), {})
iam_overrides = optional(object({
override_instance_profile = optional(bool, null)
instance_profile_name = optional(string, null)
override_runner_role = optional(bool, null)
runner_role_arn = optional(string, null)
}), {
override_instance_profile = false
instance_profile_name = null
override_runner_role = false
runner_role_arn = null
})
}), null)
matcherConfig = optional(object({
labelMatchers = list(list(string))
exactMatch = optional(bool, false)
bidirectionalLabelMatch = optional(bool, false)
priority = optional(number, 999)
enableDynamicLabels = optional(bool, false)
awsDynamicLabelsPolicy = optional(any, null)
}), null)
redrive_build_queue = optional(object({
enabled = bool
maxReceiveCount = number
}), {
enabled = false
maxReceiveCount = null
})

# V2 Contract
tags = optional(map(string), {})

runner = optional(object({
os = optional(string, null)
architecture = optional(string, null)
disable_default_labels = optional(bool, null)
extra_labels = optional(list(string), null)
group_name = optional(string, null)
name_prefix = optional(string, null)
run_as_root = optional(bool, null)
run_as = optional(string, null)
auto_update_disabled = optional(bool, null)
tags = optional(map(string), {})
hooks = optional(object({
job_started = optional(string, null)
job_completed = optional(string, null)
}), {})
iam = optional(object({
role = optional(object({
arn = string
}), null)
managed_policy_arns = optional(map(string), null)
additional_trust_policy_json = optional(string, null)
path = optional(string, null)
permissions_boundary = optional(string, null)
}), {})
}), {})

lambda = optional(object({
runtime = optional(string, null)
architecture = optional(string, null)
subnet_ids = optional(list(string), null)
security_group_ids = optional(list(string), null)
tags = optional(map(string), {})
role = optional(object({
path = optional(string, null)
permissions_boundary = optional(string, null)
}), {})
}), {})

orchestration_provider = optional(object({
webhook = optional(object({
runner = optional(object({
boot_time_in_minutes = optional(number, null)
ephemeral = optional(bool, null)
jit_config_enabled = optional(bool, null)
maximum_count = optional(number, null)
}), {})
github = optional(object({
organization_runners = optional(bool, false)
}), {})
matcherConfig = optional(object({
labelMatchers = list(list(string))
exactMatch = optional(bool, false)
bidirectionalLabelMatch = optional(bool, false)
priority = optional(number, 999)
dynamic_labels_enabled = optional(bool, false)
awsDynamicLabelsPolicy = optional(object({
allowed_keys = optional(list(string), [])
blocked_keys = optional(list(string), [])
restricted_keys = optional(map(object({
allowed = optional(list(string), [])
denied = optional(list(string), [])
max = optional(string, null)
})), {})
}), null)
}), null)
queue = optional(object({
delay_webhook_event = optional(number, null)
job_queue_retention_in_seconds = optional(number, null)
visibility_timeout_seconds = optional(number, null)
redrive_build_queue = optional(object({
enabled = optional(bool, null)
maxReceiveCount = optional(number, null)
}), null)
tags = optional(map(string), {})
}), {})
lambda = optional(object({
scale = optional(object({
up = optional(object({
memory_size = optional(number, null)
timeout = optional(number, null)
reserved_concurrent_executions = optional(number, null)
job_queued_check_enabled = optional(bool, null)
event_source_mapping = optional(object({
batch_size = optional(number, null)
maximum_batching_window_in_seconds = optional(number, null)
}), {})
tags = optional(map(string), {})
}), {})
down = optional(object({
memory_size = optional(number, null)
timeout = optional(number, null)
schedule_expression = optional(string, null)
minimum_running_time_in_minutes = optional(number, null)
idle_confirmation_seconds = optional(number, null)
idle_config = optional(list(object({
cron = string
timeZone = string
idleCount = number
evictionStrategy = optional(string, "oldest_first")
})), null)
tags = optional(map(string), {})
}), {})
}), {})
pool = optional(object({
memory_size = optional(number, null)
timeout = optional(number, null)
reserved_concurrent_executions = optional(number, null)
config = optional(list(object({
schedule_expression = string
schedule_expression_timezone = optional(string)
size = number
})), null)
include_busy_runners = optional(bool, null)
runner_owner = optional(string, null)
tags = optional(map(string), {})
}), {})
}), {})
job_retry = optional(object({
enabled = optional(bool, false)
delay_in_seconds = optional(number, 300)
delay_backoff = optional(number, 2)
max_attempts = optional(number, 1)
tags = optional(map(string), {})
lambda = optional(object({
memory_size = optional(number, 256)
reserved_concurrent_executions = optional(number, 1)
timeout = optional(number, 30)
}), {})
}), {})
}), null)
scale_set = optional(object({
name = string
runner = optional(object({
min_runners = optional(number, 0)
max_runners = optional(number, 10)
boot_time_in_minutes = optional(number, 10)
}), {})
}), null)
}), {})

storage_provider = optional(object({
aws = optional(object({
ssm = optional(object({
ttl_seconds = optional(object({
tokens = optional(number, null)
}), {})
paths = optional(object({
root = optional(string, null)
tokens = optional(string, null)
config = optional(string, null)
}), {})
tags = optional(map(string), {})
parameters = optional(object({
tags = optional(map(string), {})
}), {})
housekeeper = optional(object({
schedule_expression = optional(string, null)
state = optional(string, null)
tags = optional(map(string), {})
lambda = optional(object({
artifact = optional(object({
zip = optional(string, null)
s3 = optional(object({
key = string
object_version = optional(string, null)
}), null)
}), {})
memory_size = optional(number, null)
timeout = optional(number, null)
}), {})
config = optional(object({
tokenPath = optional(string, null)
minimumDaysOld = optional(number, null)
dryRun = optional(bool, null)
}), {})
}), {})
}), {})
}), {})
}), {})

observability = optional(object({
logs = optional(object({
level = optional(string, null)
retention_in_days = optional(number, null)
kms_key_id = optional(string, null)
class = optional(string, null)
tags = optional(map(string), {})
}), {})
tracing = optional(object({
mode = optional(string, null)
capture_http_requests = optional(bool, null)
capture_error = optional(bool, null)
}), {})
metrics = optional(object({
enabled = optional(bool, null)
namespace = optional(string, null)
metric = optional(object({
github_app_rate_limit = optional(object({
enabled = optional(bool, null)
}), {})
job_retry = optional(object({
enabled = optional(bool, null)
}), {})
spot_termination_warning = optional(object({
enabled = optional(bool, null)
}), {})
}), {})
}), {})
}), {})

compute_provider = optional(object({
aws = optional(object({
ec2 = optional(object({
metadata_options = optional(object({
instance_metadata_tags = optional(string, "enabled")
http_endpoint = optional(string, "enabled")
http_tokens = optional(string, "required")
http_put_response_hop_limit = optional(number, 1)
}), {})
ami = optional(object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
ssm_parameter = optional(object({
path = optional(string, null)
arn = optional(string, null)
}), null)
kms_key = optional(object({
arn = string
}), null)
}), null)
block_device_mappings = optional(list(object({
delete_on_termination = optional(bool, true)
device_name = optional(string, "/dev/xvda")
encrypted = optional(bool, true)
iops = optional(number)
kms_key_id = optional(string)
snapshot_id = optional(string)
throughput = optional(number)
volume_initialization_rate = optional(number)
volume_size = number
volume_type = optional(string, "gp3")
})), [{ volume_size = 30 }])
create_service_linked_role_spot = optional(bool, false)
credit_specification = optional(string, null)
ebs_optimized = optional(bool, false)
cloudwatch_agent = optional(object({
enabled = optional(bool, true)
config = optional(string, null)
}), {})
binaries_syncer = optional(object({
enabled = optional(bool, null)
}), {})
detailed_monitoring_enabled = optional(bool, false)
ssm_enabled = optional(bool, false)
user_data = optional(object({
enabled = optional(bool, true)
template = optional(string, null)
content = optional(string, null)
pre_install = optional(string, "")
post_install = optional(string, "")
debug_logging_enabled = optional(bool, false)
}), {})
instance_allocation_strategy = optional(string, "lowest-price")
instance_max_spot_price = optional(string, null)
instance_target_capacity_type = optional(string, "spot")
instance_type_priorities = optional(map(number), null)
instance_types = optional(list(string), [])
additional_security_group_ids = optional(list(string), null)
managed_security_group_enabled = optional(bool, null)
egress_rules = optional(list(object({
cidr_blocks = list(string)
ipv6_cidr_blocks = list(string)
prefix_list_ids = list(string)
from_port = number
protocol = string
security_groups = list(string)
self = bool
to_port = number
description = string
})), null)
instance_profile_path = optional(string, null)
key_name = optional(string, null)
associate_public_ipv4_address = optional(bool, null)
instance_profile = optional(object({
name = string
}), null)
on_demand_failover_for_errors = optional(list(string), [])
scale_errors = optional(list(string), [
"UnfulfillableCapacity",
"MaxSpotInstanceCountExceeded",
"TargetCapacityLimitExceededException",
"RequestLimitExceeded",
"ResourceLimitExceeded",
"MaxSpotInstanceCountExceeded",
"MaxSpotFleetRequestCountExceeded",
"InsufficientInstanceCapacity",
"InsufficientCapacityOnHost",
])
subnet_ids = optional(list(string), null)
vpc_id = optional(string, null)
cpu_options = optional(object({
core_count = optional(number)
threads_per_core = optional(number)
amd_sev_snp = optional(string)
nested_virtualization = optional(string)
}), null)
network_interfaces = optional(list(object({
associate_carrier_ip_address = optional(bool)
associate_public_ip_address = optional(bool)
delete_on_termination = optional(bool)
description = optional(string)
device_index = optional(number)
interface_type = optional(string)
ipv4_address_count = optional(number)
ipv4_addresses = optional(list(string))
ipv4_prefix_count = optional(number)
ipv4_prefixes = optional(list(string))
ipv6_address_count = optional(number)
ipv6_addresses = optional(list(string))
ipv6_prefix_count = optional(number)
ipv6_prefixes = optional(list(string))
network_card_index = optional(number)
network_interface_id = optional(string)
primary_ipv6 = optional(bool)
private_ip_address = optional(string)
security_groups = optional(list(string))
subnet_id = optional(string)
connection_tracking_specification = optional(object({
tcp_established_timeout = optional(number)
udp_stream_timeout = optional(number)
udp_timeout = optional(number)
}))
ena_srd_specification = optional(object({
ena_srd_enabled = optional(bool)
ena_srd_udp_specification = optional(object({
ena_srd_udp_enabled = optional(bool)
}))
}))
})), [])
placement = optional(object({
affinity = optional(string)
availability_zone = optional(string)
group_id = optional(string)
group_name = optional(string)
host_id = optional(string)
host_resource_group_arn = optional(string)
spread_domain = optional(string)
tenancy = optional(string)
partition_number = optional(number)
}), null)
license_specifications = optional(list(object({
license_configuration_arn = string
})), [])
use_dedicated_host = optional(bool, false)
log_files = optional(list(object({
log_group_name = string
prefix_log_group = bool
file_path = string
log_stream_name = string
log_class = optional(string, "STANDARD")
})), null)
tags = optional(map(string), {})
}), null)
}), {})
}), {})
}))
| `{}` | no | | [parameter\_store\_tags](#input\_parameter\_store\_tags) | Map of tags that will be added to all the SSM Parameter Store parameters created by the Lambda function. | `map(string)` | `{}` | no | | [pool\_lambda\_reserved\_concurrent\_executions](#input\_pool\_lambda\_reserved\_concurrent\_executions) | Amount of reserved concurrent executions for the scale-up lambda function. A value of 0 disables lambda from being triggered and -1 removes any concurrency limitations. | `number` | `1` | no | | [pool\_lambda\_timeout](#input\_pool\_lambda\_timeout) | Time out for the pool lambda in seconds. | `number` | `60` | no | From b91b1581d87b0a595f3358ab4c186a8142c5a74c Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Fri, 25 Sep 2026 16:57:09 +0200 Subject: [PATCH 36/44] fix: adjust conflict --- examples/multi-runner-scale-set/variables.tf | 7 ++++--- modules/multi-runner/orchestration-provider.scale-set.tf | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/examples/multi-runner-scale-set/variables.tf b/examples/multi-runner-scale-set/variables.tf index f0f9f66c4d..ca2bc9d3d3 100644 --- a/examples/multi-runner-scale-set/variables.tf +++ b/examples/multi-runner-scale-set/variables.tf @@ -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 = {} -} \ No newline at end of file +} diff --git a/modules/multi-runner/orchestration-provider.scale-set.tf b/modules/multi-runner/orchestration-provider.scale-set.tf index 7254a20a48..300e0d714b 100644 --- a/modules/multi-runner/orchestration-provider.scale-set.tf +++ b/modules/multi-runner/orchestration-provider.scale-set.tf @@ -7,17 +7,17 @@ locals { app_id = { name = local.primary_app_id.name arn = local.primary_app_id.arn - kms_key_arn = local.effective_config.storage_provider.aws.ssm.kms_key_id + kms_key_arn = try(local.effective_config.storage_provider.aws.ssm.kms_key_id, null) } private_key = { name = local.primary_app_key_base64.name arn = local.primary_app_key_base64.arn - kms_key_arn = local.effective_config.storage_provider.aws.ssm.kms_key_id + kms_key_arn = try(local.effective_config.storage_provider.aws.ssm.kms_key_id, null) } installation_id = local.primary_app_installation_id == null ? null : { name = local.primary_app_installation_id.name arn = local.primary_app_installation_id.arn - kms_key_arn = local.effective_config.storage_provider.aws.ssm.kms_key_id + kms_key_arn = try(local.effective_config.storage_provider.aws.ssm.kms_key_id, null) } } runner_owner = local.effective_config.github.runner_owner From 2b6f2210860a74ee82d541df9aaca817e5518a14 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 25 Sep 2026 15:03:04 +0000 Subject: [PATCH 37/44] docs: auto update terraform docs --- examples/multi-runner-scale-set/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/multi-runner-scale-set/README.md b/examples/multi-runner-scale-set/README.md index a2ca69cff4..69a9a4f533 100644 --- a/examples/multi-runner-scale-set/README.md +++ b/examples/multi-runner-scale-set/README.md @@ -69,7 +69,7 @@ The GitHub App must be installed for the configured GitHub account. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [ami](#input\_ami) | Optional AMI configuration keyed by runner lane. |
map(object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
id_ssm_parameter = optional(object({
arn = string
}), null)
kms_key = optional(object({
arn = string
}), null)
}))
| `{}` | no | +| [ami](#input\_ami) | Optional AMI configuration keyed by runner lane. |
map(object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
ssm_parameter = optional(object({
path = optional(string, null)
arn = optional(string, null)
}), null)
kms_key = optional(object({
arn = string
}), null)
}))
| `{}` | no | | [aws\_region](#input\_aws\_region) | AWS region to deploy to. | `string` | `"eu-west-1"` | no | | [environment](#input\_environment) | Environment name, used as prefix. | `string` | n/a | yes | | [github](#input\_github) | Optional GitHub endpoint and scale-set ownership settings. |
object({
url = optional(string, null)
ssl_verify = optional(bool, true)
runner_owner = optional(string, null)
registration_level = optional(string, "organization")
})
| `{}` | no | From a0f1cadc488ba1b1b5f12430488b2cd7d56ed8f2 Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Fri, 25 Sep 2026 17:28:53 +0200 Subject: [PATCH 38/44] chore: rename files --- .../aws/ec2/{scale-set.tf => orchestration.scale-set.tf} | 0 .../aws/ec2/{control-plane.tf => orchestration.webhook.tf} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename modules/compute-providers/aws/ec2/{scale-set.tf => orchestration.scale-set.tf} (100%) rename modules/compute-providers/aws/ec2/{control-plane.tf => orchestration.webhook.tf} (100%) diff --git a/modules/compute-providers/aws/ec2/scale-set.tf b/modules/compute-providers/aws/ec2/orchestration.scale-set.tf similarity index 100% rename from modules/compute-providers/aws/ec2/scale-set.tf rename to modules/compute-providers/aws/ec2/orchestration.scale-set.tf diff --git a/modules/compute-providers/aws/ec2/control-plane.tf b/modules/compute-providers/aws/ec2/orchestration.webhook.tf similarity index 100% rename from modules/compute-providers/aws/ec2/control-plane.tf rename to modules/compute-providers/aws/ec2/orchestration.webhook.tf From 1f6983ec99c999536b9c3a669833f9e5a7f9b9b1 Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Sat, 26 Sep 2026 18:04:22 +0200 Subject: [PATCH 39/44] refactor: decouple ssm from scaleset in ec2 --- .../aws/ec2/orchestration.scale-set.tf | 257 ------------------ .../aws/ec2/{ami.tf => policies.ami.tf} | 55 ++-- .../aws/ec2/policies.common.tf | 10 + ....aws.ssm.tf => policies.runner.aws.ssm.tf} | 0 ...{policies-runner.tf => policies.runner.tf} | 0 .../aws/ec2/policies.scale-set.aws.ssm.tf | 19 ++ .../aws/ec2/policies.scale-set.tf | 246 +++++++++++++++++ ...tration.webhook.tf => policies.webhook.tf} | 10 - .../aws/ec2/tests/provider.tftest.hcl | 2 +- .../scale-set/README.md | 2 +- .../orchestration-providers/scale-set/iam.tf | 20 +- .../tests/fixtures/computed-inputs/main.tf | 12 +- .../scale-set/tests/scale-set.tftest.hcl | 48 ++-- .../scale-set/validations.tf | 24 +- .../scale-set/variables.tf | 10 +- 15 files changed, 360 insertions(+), 355 deletions(-) delete mode 100644 modules/compute-providers/aws/ec2/orchestration.scale-set.tf rename modules/compute-providers/aws/ec2/{ami.tf => policies.ami.tf} (91%) create mode 100644 modules/compute-providers/aws/ec2/policies.common.tf rename modules/compute-providers/aws/ec2/{storage-provider.aws.ssm.tf => policies.runner.aws.ssm.tf} (100%) rename modules/compute-providers/aws/ec2/{policies-runner.tf => policies.runner.tf} (100%) create mode 100644 modules/compute-providers/aws/ec2/policies.scale-set.aws.ssm.tf create mode 100644 modules/compute-providers/aws/ec2/policies.scale-set.tf rename modules/compute-providers/aws/ec2/{orchestration.webhook.tf => policies.webhook.tf} (92%) diff --git a/modules/compute-providers/aws/ec2/orchestration.scale-set.tf b/modules/compute-providers/aws/ec2/orchestration.scale-set.tf deleted file mode 100644 index fd07d13ab2..0000000000 --- a/modules/compute-providers/aws/ec2/orchestration.scale-set.tf +++ /dev/null @@ -1,257 +0,0 @@ -# Provider-owned runtime and IAM fragments for the additive scale-set -# orchestration capability. GitHub credentials, GitHub scope, desired capacity, -# and boot timeout remain orchestration-owned and are not serialized here. -locals { - scale_set_ec2_instance_criteria = merge( - { - instanceTypes = var.config.instance_types - targetCapacityType = var.config.instance_target_capacity_type - instanceAllocationStrategy = var.config.instance_allocation_strategy - }, - var.config.instance_type_priorities == null ? {} : { - instanceTypePriorities = var.config.instance_type_priorities - }, - var.config.instance_max_spot_price == null ? {} : { - maxSpotPrice = var.config.instance_max_spot_price - }, - ) - - scale_set_runtime_configuration = merge( - { - region = var.aws_region - environment = var.prefix - runnerNamePrefix = var.runner.name_prefix - jitConfigParameterPath = "${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.tokens}" - subnets = var.config.subnet_ids - launchTemplateName = aws_launch_template.runner.name - ec2instanceCriteria = local.scale_set_ec2_instance_criteria - onDemandFailoverOnError = var.config.on_demand_failover_for_errors - useDedicatedHost = var.config.use_dedicated_host - ssmParameterTags = [ - for key in sort(keys(local.ssm_parameter_tags)) : { - Key = key - Value = local.ssm_parameter_tags[key] - } - ] - }, - local.ami_id_ssm_external ? { - amiIdSsmParameterName = local.ami_id_ssm_parameter_name - } : {}, - ) - - scale_set_owned_instance_conditions = [ - { - test = "StringEquals" - variable = "ec2:ResourceTag/ghr:Application" - values = toset(["github-action-runner"]) - }, - { - test = "StringEquals" - variable = "ec2:ResourceTag/ghr:created_by" - values = toset(["scale-set-service"]) - }, - { - test = "StringEquals" - variable = "ec2:ResourceTag/ghr:environment" - values = toset([var.prefix]) - }, - ] - - scale_set_owned_request_conditions = [ - { - test = "StringEquals" - variable = "aws:RequestTag/ghr:Application" - values = toset(["github-action-runner"]) - }, - { - test = "StringEquals" - variable = "aws:RequestTag/ghr:created_by" - values = toset(["scale-set-service"]) - }, - { - test = "StringEquals" - variable = "aws:RequestTag/ghr:environment" - values = toset([var.prefix]) - }, - ] - - scale_set_launch_dependency_resources = toset(concat( - [ - "arn:${var.aws_partition}:ec2:${var.aws_region}::image/*", - "arn:${var.aws_partition}:ec2:${var.aws_region}:*:snapshot/*", - "arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:dedicated-host/*", - "arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:network-interface/*", - "arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:placement-group/*", - "arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:security-group/*", - aws_launch_template.runner.arn, - ], - [ - for subnet_id in var.config.subnet_ids : - "arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:subnet/${subnet_id}" - ], - var.config.key_name == null ? [] : [ - "arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:key-pair/${var.config.key_name}", - ], - )) - - scale_set_create_fleet_dependency_resources = toset(concat( - [ - "arn:${var.aws_partition}:ec2:${var.aws_region}::image/*", - "arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:placement-group/*", - aws_launch_template.runner.arn, - ], - [ - for subnet_id in var.config.subnet_ids : - "arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:subnet/${subnet_id}" - ], - )) - - scale_set_iam_statements = merge( - { - describe_ec2 = { - actions = toset([ - "ec2:DescribeInstances", - "ec2:DescribeLaunchTemplateVersions", - "ec2:DescribeTags", - ]) - # These EC2 Describe APIs do not support resource-level permissions. - resources = toset(["*"]) - conditions = [] - } - create_fleet_dependencies = { - actions = toset(["ec2:CreateFleet"]) - resources = local.scale_set_create_fleet_dependency_resources - conditions = [] - } - create_owned_fleet_capacity = { - actions = toset(["ec2:CreateFleet"]) - resources = toset([ - "arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:fleet/*", - "arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:instance/*", - "arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:volume/*", - ]) - conditions = local.scale_set_owned_request_conditions - } - run_instances_dependencies = { - actions = toset(["ec2:RunInstances"]) - resources = local.scale_set_launch_dependency_resources - conditions = [] - } - run_owned_instances = { - actions = toset(["ec2:RunInstances"]) - resources = toset([ - "arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:instance/*", - "arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:volume/*", - ]) - conditions = local.scale_set_owned_request_conditions - } - tag_runners_on_create = { - actions = toset(["ec2:CreateTags"]) - resources = toset(["arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:*/*"]) - conditions = [ - { - test = "StringEquals" - variable = "ec2:CreateAction" - values = toset(["CreateFleet", "RunInstances"]) - }, - ] - } - update_owned_runner_tags = { - actions = toset(["ec2:CreateTags"]) - resources = toset(["arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:instance/*"]) - conditions = concat(local.scale_set_owned_instance_conditions, [ - { - test = "ForAllValues:StringEquals" - variable = "aws:TagKeys" - values = toset([ - "ghr:github_runner_id", - "ghr:runner_name", - "ghr:scale_set_state", - ]) - }, - ]) - } - terminate_owned_runners = { - actions = toset(["ec2:TerminateInstances"]) - resources = toset(["arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:instance/*"]) - conditions = local.scale_set_owned_instance_conditions - } - pass_runner_role = { - actions = toset(["iam:PassRole"]) - resources = toset([var.runner.iam.role.arn]) - conditions = [ - { - test = "StringEquals" - variable = "iam:PassedToService" - values = toset(["ec2.amazonaws.com"]) - }, - ] - } - publish_runner_jit_configuration = { - actions = toset([ - "ssm:AddTagsToResource", - "ssm:DeleteParameter", - "ssm:PutParameter", - ]) - resources = toset([ - "${local.ssm_parameter_arn_prefix}${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.tokens}/*", - ]) - conditions = [] - } - read_ami_parameter = { - actions = toset([ - "ssm:GetParameter", - "ssm:GetParameters", - ]) - resources = toset([ - local.ami_id_ssm_module_managed ? aws_ssm_parameter.runner_ami_id[0].arn : local.ami_id_ssm_parameter_arn, - ]) - conditions = [] - } - }, - local.ami_kms_key_enabled ? { - use_ami_kms_key = { - actions = toset([ - "kms:Decrypt", - "kms:DescribeKey", - "kms:ReEncryptFrom", - "kms:ReEncryptTo", - ]) - resources = toset([local.ami_kms_key_arn]) - conditions = [] - } - create_ami_kms_grant = { - actions = toset(["kms:CreateGrant"]) - resources = toset([local.ami_kms_key_arn]) - conditions = [ - { - test = "Bool" - variable = "kms:GrantIsForAWSResource" - values = toset(["true"]) - }, - ] - } - } : {}, - var.config.create_service_linked_role_spot ? { - create_spot_service_linked_role = { - actions = toset(["iam:CreateServiceLinkedRole"]) - resources = toset([ - "arn:${var.aws_partition}:iam::${data.aws_caller_identity.current.account_id}:role/aws-service-role/spot.amazonaws.com/AWSServiceRoleForEC2Spot", - ]) - conditions = [ - { - test = "StringEquals" - variable = "iam:AWSServiceName" - values = toset(["spot.amazonaws.com"]) - }, - ] - } - } : {}, - ) - - scale_set_capability = { - configuration_json = jsonencode(local.scale_set_runtime_configuration) - environment_variables = {} - iam_statements = local.scale_set_iam_statements - } -} diff --git a/modules/compute-providers/aws/ec2/ami.tf b/modules/compute-providers/aws/ec2/policies.ami.tf similarity index 91% rename from modules/compute-providers/aws/ec2/ami.tf rename to modules/compute-providers/aws/ec2/policies.ami.tf index 31fd294101..f2a2de4b2b 100644 --- a/modules/compute-providers/aws/ec2/ami.tf +++ b/modules/compute-providers/aws/ec2/policies.ami.tf @@ -63,18 +63,35 @@ resource "aws_ssm_parameter" "runner_ami_id" { ) } -data "aws_iam_policy_document" "ami_id_ssm" { +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] + } +} - dynamic "statement" { - for_each = local.ami_id_ssm_module_managed || local.ami_id_ssm_external ? [1] : [] +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 +} - content { - effect = "Allow" - sid = "AllowSSMParameterRead" - actions = ["ssm:GetParameters"] - resources = [local.ami_id_ssm_module_managed ? aws_ssm_parameter.runner_ami_id[0].arn : local.ami_id_ssm_parameter_arn] - } +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" { @@ -105,23 +122,3 @@ data "aws_iam_policy_document" "ami_id_ssm" { } } } - -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 -} diff --git a/modules/compute-providers/aws/ec2/policies.common.tf b/modules/compute-providers/aws/ec2/policies.common.tf new file mode 100644 index 0000000000..77b99a727e --- /dev/null +++ b/modules/compute-providers/aws/ec2/policies.common.tf @@ -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/*"] + } +} diff --git a/modules/compute-providers/aws/ec2/storage-provider.aws.ssm.tf b/modules/compute-providers/aws/ec2/policies.runner.aws.ssm.tf similarity index 100% rename from modules/compute-providers/aws/ec2/storage-provider.aws.ssm.tf rename to modules/compute-providers/aws/ec2/policies.runner.aws.ssm.tf diff --git a/modules/compute-providers/aws/ec2/policies-runner.tf b/modules/compute-providers/aws/ec2/policies.runner.tf similarity index 100% rename from modules/compute-providers/aws/ec2/policies-runner.tf rename to modules/compute-providers/aws/ec2/policies.runner.tf diff --git a/modules/compute-providers/aws/ec2/policies.scale-set.aws.ssm.tf b/modules/compute-providers/aws/ec2/policies.scale-set.aws.ssm.tf new file mode 100644 index 0000000000..be88fdb7ea --- /dev/null +++ b/modules/compute-providers/aws/ec2/policies.scale-set.aws.ssm.tf @@ -0,0 +1,19 @@ +# Scale-set SSM policy shared with the provider-owned AMI read policy. +data "aws_iam_policy_document" "scale_set_ssm_parameters" { + source_policy_documents = [data.aws_iam_policy_document.ami_id_ssm.json] + + dynamic "statement" { + for_each = var.storage_provider.aws.ssm != null ? [1] : [] + + content { + sid = "PublishRunnerJitConfiguration" + effect = "Allow" + actions = [ + "ssm:AddTagsToResource", + "ssm:DeleteParameter", + "ssm:PutParameter", + ] + resources = ["${local.ssm_parameter_arn_prefix}${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.tokens}/*"] + } + } +} diff --git a/modules/compute-providers/aws/ec2/policies.scale-set.tf b/modules/compute-providers/aws/ec2/policies.scale-set.tf new file mode 100644 index 0000000000..5afe2a0278 --- /dev/null +++ b/modules/compute-providers/aws/ec2/policies.scale-set.tf @@ -0,0 +1,246 @@ +# Provider-owned runtime and IAM fragments for the additive scale-set +# orchestration capability. GitHub credentials, GitHub scope, desired capacity, +# and boot timeout remain orchestration-owned and are not serialized here. +data "aws_iam_policy_document" "scale_set_capacity_launch" { + statement { + sid = "ScaleSetDescribeEC2" + effect = "Allow" + actions = [ + "ec2:DescribeInstances", + "ec2:DescribeLaunchTemplateVersions", + "ec2:DescribeTags", + ] + # These EC2 Describe APIs do not support resource-level permissions. + resources = ["*"] + } + + statement { + sid = "CreateFleetDependencies" + effect = "Allow" + actions = ["ec2:CreateFleet"] + resources = concat( + [ + "arn:${var.aws_partition}:ec2:${var.aws_region}::image/*", + "arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:placement-group/*", + aws_launch_template.runner.arn, + ], + [ + for subnet_id in var.config.subnet_ids : + "arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:subnet/${subnet_id}" + ], + ) + } + + statement { + sid = "CreateOwnedFleetCapacity" + effect = "Allow" + actions = ["ec2:CreateFleet"] + resources = [ + "arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:fleet/*", + "arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:instance/*", + "arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:volume/*", + ] + + condition { + test = "StringEquals" + variable = "aws:RequestTag/ghr:Application" + values = ["github-action-runner"] + } + condition { + test = "StringEquals" + variable = "aws:RequestTag/ghr:created_by" + values = ["scale-set-service"] + } + condition { + test = "StringEquals" + variable = "aws:RequestTag/ghr:environment" + values = [var.prefix] + } + } + + statement { + sid = "RunInstancesDependencies" + effect = "Allow" + actions = ["ec2:RunInstances"] + resources = concat( + [ + "arn:${var.aws_partition}:ec2:${var.aws_region}::image/*", + "arn:${var.aws_partition}:ec2:${var.aws_region}:*:snapshot/*", + "arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:dedicated-host/*", + "arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:network-interface/*", + "arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:placement-group/*", + "arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:security-group/*", + aws_launch_template.runner.arn, + ], + [ + for subnet_id in var.config.subnet_ids : + "arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:subnet/${subnet_id}" + ], + var.config.key_name == null ? [] : [ + "arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:key-pair/${var.config.key_name}", + ], + ) + } + + statement { + sid = "RunOwnedInstances" + effect = "Allow" + actions = ["ec2:RunInstances"] + resources = [ + "arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:instance/*", + "arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:volume/*", + ] + + condition { + test = "StringEquals" + variable = "aws:RequestTag/ghr:Application" + values = ["github-action-runner"] + } + condition { + test = "StringEquals" + variable = "aws:RequestTag/ghr:created_by" + values = ["scale-set-service"] + } + condition { + test = "StringEquals" + variable = "aws:RequestTag/ghr:environment" + values = [var.prefix] + } + } + + statement { + sid = "TagRunnersOnCreate" + effect = "Allow" + actions = ["ec2:CreateTags"] + resources = ["arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:*/*"] + + condition { + test = "StringEquals" + variable = "ec2:CreateAction" + values = ["CreateFleet", "RunInstances"] + } + } + + statement { + sid = "PassRunnerRole" + effect = "Allow" + actions = ["iam:PassRole"] + resources = [var.runner.iam.role.arn] + + condition { + test = "StringEquals" + variable = "iam:PassedToService" + values = ["ec2.amazonaws.com"] + } + } +} + +data "aws_iam_policy_document" "scale_set_runner_lifecycle" { + statement { + sid = "UpdateOwnedRunnerTags" + effect = "Allow" + actions = ["ec2:CreateTags"] + resources = ["arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:instance/*"] + + condition { + test = "StringEquals" + variable = "ec2:ResourceTag/ghr:Application" + values = ["github-action-runner"] + } + condition { + test = "StringEquals" + variable = "ec2:ResourceTag/ghr:created_by" + values = ["scale-set-service"] + } + condition { + test = "StringEquals" + variable = "ec2:ResourceTag/ghr:environment" + values = [var.prefix] + } + condition { + test = "ForAllValues:StringEquals" + variable = "aws:TagKeys" + values = ["ghr:github_runner_id", "ghr:runner_name", "ghr:scale_set_state"] + } + } + + statement { + sid = "TerminateOwnedRunners" + effect = "Allow" + actions = ["ec2:TerminateInstances"] + resources = ["arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:instance/*"] + + condition { + test = "StringEquals" + variable = "ec2:ResourceTag/ghr:Application" + values = ["github-action-runner"] + } + condition { + test = "StringEquals" + variable = "ec2:ResourceTag/ghr:created_by" + values = ["scale-set-service"] + } + condition { + test = "StringEquals" + variable = "ec2:ResourceTag/ghr:environment" + values = [var.prefix] + } + } +} + +locals { + + scale_set_ec2_instance_criteria = merge( + { + instanceTypes = var.config.instance_types + targetCapacityType = var.config.instance_target_capacity_type + instanceAllocationStrategy = var.config.instance_allocation_strategy + }, + var.config.instance_type_priorities == null ? {} : { + instanceTypePriorities = var.config.instance_type_priorities + }, + var.config.instance_max_spot_price == null ? {} : { + maxSpotPrice = var.config.instance_max_spot_price + }, + ) + + scale_set_runtime_configuration = merge( + { + region = var.aws_region + environment = var.prefix + runnerNamePrefix = var.runner.name_prefix + jitConfigParameterPath = "${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.tokens}" + subnets = var.config.subnet_ids + launchTemplateName = aws_launch_template.runner.name + ec2instanceCriteria = local.scale_set_ec2_instance_criteria + onDemandFailoverOnError = var.config.on_demand_failover_for_errors + useDedicatedHost = var.config.use_dedicated_host + ssmParameterTags = [ + for key in sort(keys(local.ssm_parameter_tags)) : { + Key = key + Value = local.ssm_parameter_tags[key] + } + ] + }, + local.ami_id_ssm_external ? { + amiIdSsmParameterName = local.ami_id_ssm_parameter_name + } : {}, + ) + + scale_set_iam_statements = merge( + { + capacity_launch = data.aws_iam_policy_document.scale_set_capacity_launch.json + runner_lifecycle = data.aws_iam_policy_document.scale_set_runner_lifecycle.json + ssm_parameters = data.aws_iam_policy_document.scale_set_ssm_parameters.json + }, + var.config.create_service_linked_role_spot ? { + spot_service_linked_role = data.aws_iam_policy_document.service_linked_role[0].json + } : {}, + ) + + scale_set_capability = { + configuration_json = jsonencode(local.scale_set_runtime_configuration) + environment_variables = {} + iam_statements = local.scale_set_iam_statements + } +} diff --git a/modules/compute-providers/aws/ec2/orchestration.webhook.tf b/modules/compute-providers/aws/ec2/policies.webhook.tf similarity index 92% rename from modules/compute-providers/aws/ec2/orchestration.webhook.tf rename to modules/compute-providers/aws/ec2/policies.webhook.tf index 2e84a9fbc9..8c348c7be5 100644 --- a/modules/compute-providers/aws/ec2/orchestration.webhook.tf +++ b/modules/compute-providers/aws/ec2/policies.webhook.tf @@ -101,16 +101,6 @@ data "aws_iam_policy_document" "pool" { } } -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/*"] - } -} - locals { scale_up_environment_variables = { AMI_ID_SSM_PARAMETER_NAME = local.ami_id_ssm_parameter_name diff --git a/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl b/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl index 5510272d20..cbea982e0a 100644 --- a/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl +++ b/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl @@ -214,7 +214,7 @@ run "separates_control_plane_contract_from_ec2_resources" { command = plan assert { - condition = toset(keys(output.provider)) == toset(["environment_variables", "policies", "resources"]) + condition = toset(keys(output.provider)) == toset(["capabilities", "environment_variables", "policies", "resources", "type"]) error_message = "The EC2 provider contract must expose only integration and resource data; its module identity must not be repeated in the output." } diff --git a/modules/orchestration-providers/scale-set/README.md b/modules/orchestration-providers/scale-set/README.md index 495a8cc314..762029073c 100644 --- a/modules/orchestration-providers/scale-set/README.md +++ b/modules/orchestration-providers/scale-set/README.md @@ -242,7 +242,7 @@ No modules. | [logging](#input\_logging) | CloudWatch Logs configuration. CloudWatch encrypts logs at rest with an AWS-owned key by default; set `kms_key_arn` to use a customer-managed key. |
object({
retention_in_days = optional(number, 30)
kms_key_arn = optional(string, null)
log_group_class = optional(string, "STANDARD")
tags = optional(map(string), {})
})
| `{}` | no | | [network](#input\_network) | Private Fargate networking. Tasks never receive public IP addresses and the managed security groups have no ingress. HTTPS egress defaults to IPv4 Internet access because GitHub endpoints cannot be represented as security-group destinations; route it through controlled NAT, firewall, or proxy infrastructure when required. |
object({
vpc_id = string
subnet_ids = set(string)
https_egress = optional(object({
ipv4_cidrs = optional(set(string), ["0.0.0.0/0"])
ipv6_cidrs = optional(set(string), [])
}), {})
})
| n/a | yes | | [prefix](#input\_prefix) | Stable prefix used for scale-set controller resources. | `string` | `"github-actions"` | no | -| [runner\_configs](#input\_runner\_configs) | Normalized scale-set runner configurations keyed by stable runner-config name.

Map keys must be known during planning. Credential values are never accepted: `github.app` contains only the exact GitHub App Parameter Store references used by the runtime. `github.enterprise_server` and `github.user_agent` carry the global GitHub settings needed to render each reconciler configuration. `scale_set.runner.group_name` selects the GitHub runner group. `runner_registration_level` selects organization or repository registration, and `runner_owner` supplies the corresponding organization or owner/repository path. Enterprise-level registration is not supported by this module. `compute_provider` carries the provider-neutral scale-set capability contract for this runner configuration. Parameter and optional KMS ARNs, scale-set names, and other inner values may remain unknown until apply. |
map(object({
github = object({
enterprise_server = object({
url = optional(string, null)
ssl_verify = optional(bool, true)
})
app = object({
app_id = object({
name = string
arn = string
kms_key_arn = optional(string, null)
})
private_key = object({
name = string
arn = string
kms_key_arn = optional(string, null)
})
installation_id = object({
name = string
arn = string
kms_key_arn = optional(string, null)
})
})
runner_owner = string
runner_registration_level = string
user_agent = string
})
scale_set = object({
name = string
runner = optional(object({
labels = optional(list(string), [])
group_name = optional(string, "Default")
min_runners = optional(number, 0)
max_runners = optional(number, 10)
boot_time_in_minutes = optional(number, 10)
}), {})
})
compute_provider = object({
type = string
capabilities = object({
scale_set = object({
role_arn = optional(string, null)
configuration_json = optional(string, "{}")
environment_variables = optional(map(string), {})
iam_statements = optional(map(object({
actions = set(string)
resources = set(string)
conditions = optional(list(object({
test = string
variable = string
values = set(string)
})), [])
})), {})
})
})
})
}))
| n/a | yes | +| [runner\_configs](#input\_runner\_configs) | Normalized scale-set runner configurations keyed by stable runner-config name.

Map keys must be known during planning. Credential values are never accepted: `github.app` contains only the exact GitHub App Parameter Store references used by the runtime. `github.enterprise_server` and `github.user_agent` carry the global GitHub settings needed to render each reconciler configuration. `scale_set.runner.group_name` selects the GitHub runner group. `runner_registration_level` selects organization or repository registration, and `runner_owner` supplies the corresponding organization or owner/repository path. Enterprise-level registration is not supported by this module. `compute_provider` carries the provider-neutral scale-set capability contract for this runner configuration. Parameter and optional KMS ARNs, scale-set names, and other inner values may remain unknown until apply. |
map(object({
github = object({
enterprise_server = object({
url = optional(string, null)
ssl_verify = optional(bool, true)
})
app = object({
app_id = object({
name = string
arn = string
kms_key_arn = optional(string, null)
})
private_key = object({
name = string
arn = string
kms_key_arn = optional(string, null)
})
installation_id = object({
name = string
arn = string
kms_key_arn = optional(string, null)
})
})
runner_owner = string
runner_registration_level = string
user_agent = string
})
scale_set = object({
name = string
runner = optional(object({
labels = optional(list(string), [])
group_name = optional(string, "Default")
min_runners = optional(number, 0)
max_runners = optional(number, 10)
boot_time_in_minutes = optional(number, 10)
}), {})
})
compute_provider = object({
type = string
capabilities = object({
scale_set = object({
role_arn = optional(string, null)
configuration_json = optional(string, "{}")
environment_variables = optional(map(string), {})
iam_statements = optional(map(string), {})
})
})
})
}))
| n/a | yes | | [tags](#input\_tags) | Tags applied to scale-set orchestration resources. | `map(string)` | `{}` | no | ## Outputs diff --git a/modules/orchestration-providers/scale-set/iam.tf b/modules/orchestration-providers/scale-set/iam.tf index 6065fff396..3b1b6c848e 100644 --- a/modules/orchestration-providers/scale-set/iam.tf +++ b/modules/orchestration-providers/scale-set/iam.tf @@ -128,25 +128,7 @@ resource "aws_iam_role" "compute" { data "aws_iam_policy_document" "compute" { for_each = local.compute_role_configs - dynamic "statement" { - for_each = local.reconciler_compute_iam_statements[each.key] - - content { - effect = "Allow" - actions = statement.value.actions - resources = statement.value.resources - - dynamic "condition" { - for_each = statement.value.conditions - - content { - test = condition.value.test - variable = condition.value.variable - values = condition.value.values - } - } - } - } + source_policy_documents = values(local.reconciler_compute_iam_statements[each.key]) } resource "aws_iam_role_policy" "compute" { diff --git a/modules/orchestration-providers/scale-set/tests/fixtures/computed-inputs/main.tf b/modules/orchestration-providers/scale-set/tests/fixtures/computed-inputs/main.tf index db8ae2b225..3eafd18484 100644 --- a/modules/orchestration-providers/scale-set/tests/fixtures/computed-inputs/main.tf +++ b/modules/orchestration-providers/scale-set/tests/fixtures/computed-inputs/main.tf @@ -63,10 +63,14 @@ module "subject" { scaleErrors = [] }) iam_statements = { - run_instances = { - actions = [terraform_data.computed.output.action] - resources = [terraform_data.computed.output.resource] - } + run_instances = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Action = [terraform_data.computed.output.action] + Resource = [terraform_data.computed.output.resource] + }] + }) } } } diff --git a/modules/orchestration-providers/scale-set/tests/scale-set.tftest.hcl b/modules/orchestration-providers/scale-set/tests/scale-set.tftest.hcl index b939c0310b..88c4e0ca93 100644 --- a/modules/orchestration-providers/scale-set/tests/scale-set.tftest.hcl +++ b/modules/orchestration-providers/scale-set/tests/scale-set.tftest.hcl @@ -96,14 +96,22 @@ variables { EC2_CONTROLLER_MODE = "grouped" } iam_statements = { - run_instances = { - actions = ["ec2:RunInstances"] - resources = ["arn:aws:ec2:eu-west-1:123456789012:launch-template/lt-small"] - } - read_ami = { - actions = ["ssm:GetParameters"] - resources = ["arn:aws:ssm:eu-west-1:123456789012:parameter/scale-set-test/runners/config/ami_id"] - } + run_instances = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Action = ["ec2:RunInstances"] + Resource = ["arn:aws:ec2:eu-west-1:123456789012:launch-template/lt-small"] + }] + }) + read_ami = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Action = ["ssm:GetParameters"] + Resource = ["arn:aws:ssm:eu-west-1:123456789012:parameter/scale-set-test/runners/config/ami_id"] + }] + }) } } } @@ -163,10 +171,14 @@ variables { EC2_CONTROLLER_MODE = "grouped" } iam_statements = { - run_instances = { - actions = ["ec2:RunInstances"] - resources = ["arn:aws:ec2:eu-west-1:123456789012:launch-template/lt-large"] - } + run_instances = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Action = ["ec2:RunInstances"] + Resource = ["arn:aws:ec2:eu-west-1:123456789012:launch-template/lt-large"] + }] + }) } } } @@ -207,10 +219,14 @@ variables { scale_set = { configuration_json = jsonencode({ image_arn = "arn:aws:lambda:eu-west-1:123456789012:runtime-management-config:microvm" }) iam_statements = { - run_microvm = { - actions = ["lambda:InvokeFunction"] - resources = ["arn:aws:lambda:eu-west-1:123456789012:function:microvm"] - } + run_microvm = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Action = ["lambda:InvokeFunction"] + Resource = ["arn:aws:lambda:eu-west-1:123456789012:function:microvm"] + }] + }) } } } diff --git a/modules/orchestration-providers/scale-set/validations.tf b/modules/orchestration-providers/scale-set/validations.tf index 9553581597..2ff4e517e4 100644 --- a/modules/orchestration-providers/scale-set/validations.tf +++ b/modules/orchestration-providers/scale-set/validations.tf @@ -158,17 +158,23 @@ resource "terraform_data" "validate_contract" { ) ]) && alltrue([ - for statement_name, statement in runner_config.compute_provider.capabilities.scale_set.iam_statements : ( + for statement_name, policy_json in runner_config.compute_provider.capabilities.scale_set.iam_statements : ( can(regex("^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$", statement_name)) && - length(statement.actions) > 0 && - length(statement.resources) > 0 && - alltrue([for action in statement.actions : !strcontains(action, "*")]) && + can(jsondecode(policy_json).Statement) && alltrue([ - for condition in statement.conditions : ( - length(condition.test) > 0 && - length(condition.variable) > 0 && - length(condition.values) > 0 - ) + for statement in try(tolist(jsondecode(policy_json).Statement), [jsondecode(policy_json).Statement]) : try(( + lower(statement.Effect) == "allow" && + length(try(tolist(statement.Action), [statement.Action])) > 0 && + length(try(tolist(statement.Resource), [statement.Resource])) > 0 && + alltrue([ + for action in try(tolist(statement.Action), [statement.Action]) : + length(action) > 0 && !strcontains(action, "*") + ]) && + alltrue([ + for resource in try(tolist(statement.Resource), [statement.Resource]) : + length(resource) > 0 + ]) + ), false) ]) ) ]) diff --git a/modules/orchestration-providers/scale-set/variables.tf b/modules/orchestration-providers/scale-set/variables.tf index 236dfc8213..3b9c332c41 100644 --- a/modules/orchestration-providers/scale-set/variables.tf +++ b/modules/orchestration-providers/scale-set/variables.tf @@ -62,15 +62,7 @@ variable "runner_configs" { role_arn = optional(string, null) configuration_json = optional(string, "{}") environment_variables = optional(map(string), {}) - iam_statements = optional(map(object({ - actions = set(string) - resources = set(string) - conditions = optional(list(object({ - test = string - variable = string - values = set(string) - })), []) - })), {}) + iam_statements = optional(map(string), {}) }) }) }) From 1455de7174a570be7854d523ee73ac7bd4f5b250 Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Sat, 26 Sep 2026 23:49:19 +0200 Subject: [PATCH 40/44] test: fix tests --- .../aws/ec2/policies.scale-set.tf | 4 +++- .../aws/ec2/tests/provider.tftest.hcl | 16 ++++++++-------- .../scale-set/tests/scale-set.tftest.hcl | 6 +++--- modules/runner-config/tests/pool.tftest.hcl | 1 + 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/modules/compute-providers/aws/ec2/policies.scale-set.tf b/modules/compute-providers/aws/ec2/policies.scale-set.tf index 5afe2a0278..3bcb8c03d7 100644 --- a/modules/compute-providers/aws/ec2/policies.scale-set.tf +++ b/modules/compute-providers/aws/ec2/policies.scale-set.tf @@ -209,7 +209,6 @@ locals { region = var.aws_region environment = var.prefix runnerNamePrefix = var.runner.name_prefix - jitConfigParameterPath = "${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.tokens}" subnets = var.config.subnet_ids launchTemplateName = aws_launch_template.runner.name ec2instanceCriteria = local.scale_set_ec2_instance_criteria @@ -222,6 +221,9 @@ locals { } ] }, + var.storage_provider.aws.ssm != null ? { + jitConfigParameterPath = "${var.storage_provider.aws.ssm.paths.root}/${var.storage_provider.aws.ssm.paths.tokens}" + } : {}, local.ami_id_ssm_external ? { amiIdSsmParameterName = local.ami_id_ssm_parameter_name } : {}, diff --git a/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl b/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl index cbea982e0a..85a50b01ca 100644 --- a/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl +++ b/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl @@ -1,7 +1,7 @@ mock_provider "aws" { mock_data "aws_iam_policy_document" { defaults = { - json = "{}" + json = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":[\"ssm:GetParameter\",\"ssm:GetParameters\"],\"Resource\":\"arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/ami-id\"}]}" } } @@ -263,11 +263,11 @@ run "separates_control_plane_contract_from_ec2_resources" { assert { condition = ( - contains(output.provider.capabilities.scale_set.iam_statements.read_ami_parameter.actions, "ssm:GetParameter") - && contains(output.provider.capabilities.scale_set.iam_statements.read_ami_parameter.actions, "ssm:GetParameters") - && contains(output.provider.capabilities.scale_set.iam_statements.read_ami_parameter.resources, "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/ami-id") + strcontains(output.provider.capabilities.scale_set.iam_statements.ssm_parameters, "ssm:GetParameter") + && strcontains(output.provider.capabilities.scale_set.iam_statements.ssm_parameters, "ssm:GetParameters") + && strcontains(output.provider.capabilities.scale_set.iam_statements.ssm_parameters, "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/ami-id") ) - error_message = "The scale-set compute role must read an external AMI parameter with both single and batched SSM actions." + error_message = "The grouped scale-set SSM policy must read an external AMI parameter with both single and batched SSM actions." } assert { @@ -344,10 +344,10 @@ run "includes_managed_ami_read_in_scale_set_contract" { assert { condition = ( - contains(output.provider.capabilities.scale_set.iam_statements.read_ami_parameter.actions, "ssm:GetParameters") - && output.provider.capabilities.scale_set.iam_statements.read_ami_parameter.resources != toset([]) + strcontains(output.provider.capabilities.scale_set.iam_statements.ssm_parameters, "ssm:GetParameters") + && strcontains(output.provider.capabilities.scale_set.iam_statements.ssm_parameters, ":parameter/") ) - error_message = "The scale-set compute role must read the module-managed AMI parameter." + error_message = "The grouped scale-set SSM policy must read the module-managed AMI parameter." } } diff --git a/modules/orchestration-providers/scale-set/tests/scale-set.tftest.hcl b/modules/orchestration-providers/scale-set/tests/scale-set.tftest.hcl index 88c4e0ca93..7f3f3b5ba9 100644 --- a/modules/orchestration-providers/scale-set/tests/scale-set.tftest.hcl +++ b/modules/orchestration-providers/scale-set/tests/scale-set.tftest.hcl @@ -19,7 +19,7 @@ mock_provider "aws" { mock_data "aws_iam_policy_document" { defaults = { - json = "{\"Version\":\"2012-10-17\",\"Statement\":[]}" + json = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":[\"ssm:GetParameters\"],\"Resource\":\"arn:aws:ssm:eu-west-1:123456789012:parameter/scale-set-test/runners/config/ami_id\"}]}" } } @@ -411,8 +411,8 @@ run "groups_by_compute_provider_and_hardens_each_task" { contains(flatten([for statement in data.aws_iam_policy_document.task["ec2"].statement : statement.resources]), "arn:aws:ssm:eu-west-1:123456789012:parameter/scale-set-test/scale-set-controller/ec2/*") && contains(flatten([for statement in data.aws_iam_policy_document.task["ec2"].statement : statement.actions]), "sts:AssumeRole") && !contains(flatten([for statement in data.aws_iam_policy_document.task["ec2"].statement : statement.resources]), "arn:aws:ssm:eu-west-1:123456789012:parameter/scale-set-test/runners/config/ami_id") && - contains(flatten([for statement in data.aws_iam_policy_document.compute["ec2/linux-small"].statement : statement.actions]), "ssm:GetParameters") && - contains(flatten([for statement in data.aws_iam_policy_document.compute["ec2/linux-small"].statement : statement.resources]), "arn:aws:ssm:eu-west-1:123456789012:parameter/scale-set-test/runners/config/ami_id") + strcontains(data.aws_iam_policy_document.compute["ec2/linux-small"].json, "ssm:GetParameters") && + strcontains(data.aws_iam_policy_document.compute["ec2/linux-small"].json, "arn:aws:ssm:eu-west-1:123456789012:parameter/scale-set-test/runners/config/ami_id") ) error_message = "Controller IAM must contain only controller permissions, while provider permissions such as AMI SSM reads must be attached to the compute role." } diff --git a/modules/runner-config/tests/pool.tftest.hcl b/modules/runner-config/tests/pool.tftest.hcl index bbf3ae4d24..3b5913bd40 100644 --- a/modules/runner-config/tests/pool.tftest.hcl +++ b/modules/runner-config/tests/pool.tftest.hcl @@ -59,6 +59,7 @@ variables { vpc_id = "vpc-12345678" subnet_ids = ["subnet-12345678"] instance_types = ["m5.large"] + ssm_enabled = true ami = { filter = { state = ["available"] } owners = ["amazon"] From 8ffe3404446a9aca5b5ac85fcc600c6a32cc4d9a Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Sat, 26 Sep 2026 23:57:49 +0200 Subject: [PATCH 41/44] fix: fix assume role --- .../compute-providers/aws/ec2/trust-policy/assume-role.tf | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/compute-providers/aws/ec2/trust-policy/assume-role.tf b/modules/compute-providers/aws/ec2/trust-policy/assume-role.tf index bea81c1b9e..f800f73c7a 100644 --- a/modules/compute-providers/aws/ec2/trust-policy/assume-role.tf +++ b/modules/compute-providers/aws/ec2/trust-policy/assume-role.tf @@ -13,6 +13,10 @@ data "aws_iam_policy_document" "default" { data "aws_iam_policy_document" "assume_role" { source_policy_documents = compact([ data.aws_iam_policy_document.default.json, - var.additional_trust_policy_json, + var.additional_trust_policy_json == null ? null : ( + can(jsondecode(var.additional_trust_policy_json)) + ? var.additional_trust_policy_json + : null + ), ]) -} +} \ No newline at end of file From ad78451300be35e95fb1db97cd6959746510e931 Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Sun, 27 Sep 2026 00:25:17 +0200 Subject: [PATCH 42/44] fix: fix after merge --- .../scale-set/tests/scale-set.tftest.hcl | 4 ++-- modules/runner-config/tests/pool.tftest.hcl | 12 ++---------- modules/runner-config/validations.tf | 8 -------- modules/runner-config/variables.compute-provider.tf | 8 ++++++++ 4 files changed, 12 insertions(+), 20 deletions(-) diff --git a/modules/orchestration-providers/scale-set/tests/scale-set.tftest.hcl b/modules/orchestration-providers/scale-set/tests/scale-set.tftest.hcl index 3a6d9d0a3a..8683e02750 100644 --- a/modules/orchestration-providers/scale-set/tests/scale-set.tftest.hcl +++ b/modules/orchestration-providers/scale-set/tests/scale-set.tftest.hcl @@ -415,8 +415,8 @@ run "groups_by_compute_provider_and_hardens_each_task" { contains(flatten([for statement in data.aws_iam_policy_document.task["ec2"].statement : statement.resources]), "arn:aws:ssm:eu-west-1:123456789012:parameter/scale-set-test/scale-set-controller/ec2/*") && contains(flatten([for statement in data.aws_iam_policy_document.task["ec2"].statement : statement.actions]), "sts:AssumeRole") && !contains(flatten([for statement in data.aws_iam_policy_document.task["ec2"].statement : statement.resources]), "arn:aws:ssm:eu-west-1:123456789012:parameter/scale-set-test/runners/config/ami_id") && - contains(flatten([for statement in data.aws_iam_policy_document.compute["ec2/linux-small"].statement : statement.actions]), "ssm:GetParameters") && - contains(flatten([for statement in data.aws_iam_policy_document.compute["ec2/linux-small"].statement : statement.resources]), "arn:aws:ssm:eu-west-1:123456789012:parameter/scale-set-test/runners/config/ami_id") && + strcontains(data.aws_iam_policy_document.compute["ec2/linux-small"].json, "ssm:GetParameters") && + strcontains(data.aws_iam_policy_document.compute["ec2/linux-small"].json, "arn:aws:ssm:eu-west-1:123456789012:parameter/scale-set-test/runners/config/ami_id") && !contains(flatten([for statement in data.aws_iam_policy_document.execution["ec2"].statement : statement.actions]), "ecr:GetAuthorizationToken") && !contains(flatten([for statement in data.aws_iam_policy_document.execution["ec2"].statement : statement.actions]), "ecr:BatchGetImage") ) diff --git a/modules/runner-config/tests/pool.tftest.hcl b/modules/runner-config/tests/pool.tftest.hcl index 3b5913bd40..8fc488c06f 100644 --- a/modules/runner-config/tests/pool.tftest.hcl +++ b/modules/runner-config/tests/pool.tftest.hcl @@ -637,11 +637,7 @@ run "rejects_empty_compute_provider" { compute_provider = {} } - plan_options { - target = [terraform_data.validate_config] - } - - expect_failures = [terraform_data.validate_config] + expect_failures = [var.compute_provider] } run "rejects_empty_aws_compute_provider_namespace" { @@ -653,11 +649,7 @@ run "rejects_empty_aws_compute_provider_namespace" { } } - plan_options { - target = [terraform_data.validate_config] - } - - expect_failures = [terraform_data.validate_config] + expect_failures = [var.compute_provider] } run "job_retry_uses_common_runner_configuration_identity" { diff --git a/modules/runner-config/validations.tf b/modules/runner-config/validations.tf index df0671e93f..3cf19438c7 100644 --- a/modules/runner-config/validations.tf +++ b/modules/runner-config/validations.tf @@ -69,14 +69,6 @@ resource "terraform_data" "validate_config" { error_message = "observability.logs.level must be one of silly, trace, debug, info, warn, error, or fatal." } - precondition { - condition = length([ - for provider_key, provider_config in local.compute_providers : provider_key - if provider_config != null - ]) == 1 - error_message = "Exactly one compute-provider block must be set. Supported compute-provider blocks: aws.ec2." - } - precondition { condition = var.compute_provider_key == null || try( local.compute_providers[var.compute_provider_key] != null, diff --git a/modules/runner-config/variables.compute-provider.tf b/modules/runner-config/variables.compute-provider.tf index e0ad549a7b..9946d6999e 100644 --- a/modules/runner-config/variables.compute-provider.tf +++ b/modules/runner-config/variables.compute-provider.tf @@ -296,4 +296,12 @@ variable "compute_provider" { }), {}) }) + validation { + condition = length([ + for provider_key, provider_config in { + aws_ec2 = var.compute_provider.aws.ec2 + } : provider_key if provider_config != null + ]) == 1 + error_message = "Exactly one compute-provider block must be set. Supported compute-provider blocks: aws.ec2." + } } From 7ae309a719434ff98ee149fd525df260b1ef2e46 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 26 Sep 2026 22:29:44 +0000 Subject: [PATCH 43/44] docs: auto update terraform docs --- modules/compute-providers/aws/ec2/README.md | 3 +++ modules/multi-runner/README.md | 2 +- modules/orchestration-providers/scale-set/README.md | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/modules/compute-providers/aws/ec2/README.md b/modules/compute-providers/aws/ec2/README.md index c81c5a139d..a757a9b970 100644 --- a/modules/compute-providers/aws/ec2/README.md +++ b/modules/compute-providers/aws/ec2/README.md @@ -50,6 +50,9 @@ No modules. | [aws_iam_policy_document.distribution_bucket](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.pool](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.scale_down](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.scale_set_capacity_launch](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.scale_set_runner_lifecycle](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.scale_set_ssm_parameters](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.scale_up](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.service_linked_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.session_manager](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | diff --git a/modules/multi-runner/README.md b/modules/multi-runner/README.md index 1e86c7bf61..c67333c3d6 100644 --- a/modules/multi-runner/README.md +++ b/modules/multi-runner/README.md @@ -167,7 +167,7 @@ module "multi-runner" { | [global\_config\_github](#input\_global\_config\_github) | Global GitHub configuration shared by all runner lanes.

global\_config\_github = {
app: {
key\_base64: "Base64-encoded GitHub App private key."
key\_base64\_ssm: "SSM parameter containing the Base64-encoded GitHub App private key."
key\_base64\_ssm.arn: "ARN of the SSM parameter containing the GitHub App private key."
key\_base64\_ssm.name: "Name of the SSM parameter containing the GitHub App private key."
id: "GitHub App ID."
id\_ssm: "SSM parameter containing the GitHub App ID."
id\_ssm.arn: "ARN of the SSM parameter containing the GitHub App ID."
id\_ssm.name: "Name of the SSM parameter containing the GitHub App ID."
installation\_id: "GitHub App installation ID for the primary scale-set installation."
installation\_id\_ssm: "SSM parameter containing the primary GitHub App installation ID."
installation\_id\_ssm.arn: "ARN of the SSM parameter containing the primary GitHub App installation ID."
installation\_id\_ssm.name: "Name of the SSM parameter containing the primary GitHub App installation ID."
webhook\_secret: "GitHub App webhook secret."
webhook\_secret\_ssm: "SSM parameter containing the GitHub App webhook secret."
webhook\_secret\_ssm.arn: "ARN of the SSM parameter containing the GitHub App webhook secret."
webhook\_secret\_ssm.name: "Name of the SSM parameter containing the GitHub App webhook secret."
}
additional\_apps: "Additional GitHub Apps used to distribute GitHub API requests."
additional\_apps.key\_base64: "Base64-encoded private key for an additional GitHub App."
additional\_apps.key\_base64\_ssm: "SSM parameter containing an additional App private key."
additional\_apps.key\_base64\_ssm.arn: "ARN of the SSM parameter containing an additional App private key."
additional\_apps.key\_base64\_ssm.name: "Name of the SSM parameter containing an additional App private key."
additional\_apps.id: "ID of an additional GitHub App."
additional\_apps.id\_ssm: "SSM parameter containing an additional GitHub App ID."
additional\_apps.id\_ssm.arn: "ARN of the SSM parameter containing an additional GitHub App ID."
additional\_apps.id\_ssm.name: "Name of the SSM parameter containing an additional GitHub App ID."
additional\_apps.installation\_id: "Optional installation ID for an additional GitHub App."
additional\_apps.installation\_id\_ssm: "SSM parameter containing an additional App installation ID."
additional\_apps.installation\_id\_ssm.arn: "ARN of the SSM parameter containing an additional App installation ID."
additional\_apps.installation\_id\_ssm.name: "Name of the SSM parameter containing an additional App installation ID."
enterprise\_server.url: "GitHub Enterprise Server URL."
enterprise\_server.ssl\_verify: "Whether to verify the GitHub Enterprise Server TLS certificate."
runner\_owner: "GitHub organization or owner/repository path for organization- or repository-level scale-set registration."
runner\_registration\_level: "GitHub scale-set registration scope: organization or repository."
user\_agent: "User-Agent value sent with GitHub API requests."
} |
object({
app = optional(object({
key_base64 = optional(string)
key_base64_ssm = optional(object({
arn = string
name = string
}))
id = optional(string)
id_ssm = optional(object({
arn = string
name = string
}))
installation_id = optional(string)
installation_id_ssm = optional(object({
arn = string
name = string
}))
webhook_secret = optional(string)
webhook_secret_ssm = optional(object({
arn = string
name = string
}))
}), null)
additional_apps = optional(list(object({
key_base64 = optional(string)
key_base64_ssm = optional(object({ arn = string, name = string }))
id = optional(string)
id_ssm = optional(object({ arn = string, name = string }))
installation_id = optional(string)
installation_id_ssm = optional(object({ arn = string, name = string }))
})), [])
enterprise_server = optional(object({
url = optional(string, null)
ssl_verify = optional(bool, true)
}), {})
runner_owner = optional(string, null)
runner_registration_level = optional(string, "organization")
user_agent = optional(string, "github-aws-runners")
})
| `{}` | no | | [global\_config\_lambda](#input\_global\_config\_lambda) | Global Lambda configuration shared by all runner lanes.

global\_config\_lambda = {
artifact.s3.bucket: "S3 bucket containing Lambda deployment artifacts."
runtime: "Default Lambda runtime."
architecture: "Default Lambda instruction-set architecture."
principals: "Additional AWS principals allowed to invoke the Lambda functions."
principals.type: "Principal type, such as AWS account, service, or organization."
principals.identifiers: "Identifiers allowed for the principal type."
subnet\_ids: "Subnets used by Lambda functions."
security\_group\_ids: "Security groups attached to Lambda functions."
tags: "Tags applied to Lambda functions and related resources."
role.path: "IAM path used for Lambda execution roles."
role.permissions\_boundary: "Optional IAM permissions boundary ARN for Lambda execution roles."
} |
object({
artifact = optional(object({
s3 = optional(object({
bucket = optional(string, null)
}), {})
}), {})
runtime = optional(string, "nodejs24.x")
architecture = optional(string, "arm64")
principals = optional(list(object({
type = string
identifiers = list(string)
})), [])
subnet_ids = optional(list(string), [])
security_group_ids = optional(list(string), [])
tags = optional(map(string), {})
role = optional(object({
path = optional(string, null)
permissions_boundary = optional(string, null)
}), {})
})
| `{}` | no | | [global\_config\_observability](#input\_global\_config\_observability) | Global observability configuration shared by all runner lanes.

global\_config\_observability = {
logs.level: "Log level for module resources."
logs.retention\_in\_days: "CloudWatch log retention period in days."
logs.kms\_key\_id: "KMS key ID used to encrypt CloudWatch log groups."
logs.class: "CloudWatch log group class."
logs.tags: "Tags applied to CloudWatch log groups."
tracing.mode: "Tracing mode used by instrumented resources."
tracing.capture\_http\_requests: "Whether HTTP requests are captured by tracing."
tracing.capture\_error: "Whether errors are captured by tracing."
metrics.enabled: "Whether module metrics are enabled."
metrics.namespace: "CloudWatch namespace used for module metrics."
metrics.metric.github\_app\_rate\_limit.enabled: "Whether GitHub App rate-limit metrics are emitted."
metrics.metric.job\_retry.enabled: "Whether job-retry metrics are emitted."
metrics.metric.spot\_termination\_warning.enabled: "Whether spot-termination warning metrics are emitted."
} |
object({
logs = optional(object({
level = optional(string, "info")
retention_in_days = optional(number, 180)
kms_key_id = optional(string, null)
class = optional(string, "STANDARD")
tags = optional(map(string), {})
}), {})
tracing = optional(object({
mode = optional(string, null)
capture_http_requests = optional(bool, false)
capture_error = optional(bool, false)
}), {})
metrics = optional(object({
enabled = optional(bool, false)
namespace = optional(string, "GitHub Runners")
metric = optional(object({
github_app_rate_limit = optional(object({
enabled = optional(bool, true)
}), {})
job_retry = optional(object({
enabled = optional(bool, true)
}), {})
spot_termination_warning = optional(object({
enabled = optional(bool, true)
}), {})
}), {})
}), {})
})
| `{}` | no | -| [global\_config\_orchestration\_provider](#input\_global\_config\_orchestration\_provider) | Global orchestration-provider configuration shared by all runner lanes.

global\_config\_orchestration\_provider = {
webhook: {
queue\_selection\_strategy: "Strategy used to select the build queue for a webhook event."
eventbridge.enabled: "Whether EventBridge integration is enabled for webhook events."
eventbridge.accept\_events: "Event types accepted by the EventBridge integration."
matcher\_config\_parameter\_store\_tier: "SSM Parameter Store tier used for matcher configuration."
runner.boot\_time\_in\_minutes: "Expected runner boot time used by orchestration."
runner.ephemeral: "Whether runners created by the orchestration provider are ephemeral."
runner.jit\_config\_enabled: "Whether JIT runner configuration is enabled."
runner.maximum\_count: "Maximum number of runners that orchestration may create."
github.repository\_white\_list: "Repositories allowed to use the webhook configuration."
lambda.artifact.zip: "Local ZIP artifact used for orchestration Lambda functions."
lambda.artifact.s3.key: "S3 object key for the orchestration Lambda artifact."
lambda.artifact.s3.object\_version: "Optional S3 object version for the orchestration Lambda artifact."
lambda.scale.up.memory\_size: "Memory allocated to the scale-up Lambda."
lambda.scale.up.timeout: "Timeout in seconds for the scale-up Lambda."
lambda.scale.up.reserved\_concurrent\_executions: "Reserved concurrent executions for the scale-up Lambda."
lambda.scale.up.job\_queued\_check\_enabled: "Whether the scale-up Lambda checks queued jobs."
lambda.scale.up.event\_source\_mapping.batch\_size: "Maximum records passed to one scale-up Lambda invocation."
lambda.scale.up.event\_source\_mapping.maximum\_batching\_window\_in\_seconds: "Maximum time to batch records before invoking the scale-up Lambda."
lambda.scale.up.tags: "Tags applied to the scale-up Lambda."
lambda.scale.down.memory\_size: "Memory allocated to the scale-down Lambda."
lambda.scale.down.timeout: "Timeout in seconds for the scale-down Lambda."
lambda.scale.down.schedule\_expression: "Schedule expression for scale-down processing."
lambda.scale.down.minimum\_running\_time\_in\_minutes: "Minimum runner lifetime before scale-down."
lambda.scale.down.idle\_confirmation\_seconds: "Seconds a runner must consistently report not-busy before scale-down terminates it; 0 disables the confirmation window."
lambda.scale.down.idle\_config: "Scheduled minimum idle-runner pool settings."
lambda.scale.down.idle\_config.cron: "Cron expression defining when the idle-runner count applies."
lambda.scale.down.idle\_config.timeZone: "Time zone used to evaluate the idle-runner schedule."
lambda.scale.down.idle\_config.idleCount: "Minimum number of idle runners maintained during the schedule."
lambda.scale.down.idle\_config.evictionStrategy: "Strategy used when evicting idle runners."
lambda.scale.down.tags: "Tags applied to the scale-down Lambda."
lambda.webhook.artifact.zip: "Local ZIP artifact used for the webhook Lambda."
lambda.webhook.artifact.s3.key: "S3 object key for the webhook Lambda artifact."
lambda.webhook.artifact.s3.object\_version: "Optional S3 object version for the webhook Lambda artifact."
lambda.webhook.api\_gateway\_access\_log\_settings: "API Gateway access-log destination and format."
lambda.webhook.api\_gateway\_access\_log\_settings.destination\_arn: "ARN of the API Gateway access-log destination."
lambda.webhook.api\_gateway\_access\_log\_settings.format: "API Gateway access-log format."
lambda.webhook.memory\_size: "Memory allocated to the webhook Lambda."
lambda.webhook.timeout: "Timeout in seconds for the webhook Lambda."
lambda.webhook.tags: "Tags applied to the webhook Lambda."
lambda.pool.memory\_size: "Memory allocated to the pool Lambda."
lambda.pool.timeout: "Timeout in seconds for the pool Lambda."
lambda.pool.reserved\_concurrent\_executions: "Reserved concurrent executions for the pool Lambda."
lambda.pool.config: "Scheduled runner-pool size configuration."
lambda.pool.config.schedule\_expression: "Schedule expression for the pool size."
lambda.pool.config.schedule\_expression\_timezone: "Time zone used to evaluate the pool schedule."
lambda.pool.config.size: "Runner pool size applied by the schedule."
lambda.pool.include\_busy\_runners: "Whether busy runners are included in pool sizing."
lambda.pool.runner\_owner: "GitHub organization that owns the runner pool."
lambda.pool.tags: "Tags applied to the pool Lambda."
queue.delay\_webhook\_event: "Seconds a webhook event remains invisible in the build queue before processing."
queue.job\_queue\_retention\_in\_seconds: "Seconds a queued job is retained before it is purged."
queue.visibility\_timeout\_seconds: "Build queue visibility timeout in seconds."
queue.redrive\_build\_queue.enabled: "Whether the build queue dead-letter queue is enabled."
queue.redrive\_build\_queue.maxReceiveCount: "Maximum receives before a message is moved to the dead-letter queue."
queue.tags: "Tags applied to build queues."
queue.encryption.kms\_data\_key\_reuse\_period\_seconds: "KMS data-key reuse period for queue encryption."
queue.encryption.kms\_master\_key\_id: "KMS key ID used for queue encryption."
queue.encryption.sqs\_managed\_sse\_enabled: "Whether SQS-managed server-side encryption is enabled."
}
} |
object({
webhook = optional(object({
queue_selection_strategy = optional(string, "first")
eventbridge = optional(object({
enabled = optional(bool, true)
accept_events = optional(list(string), [])
}), {})
matcher_config_parameter_store_tier = optional(string, "Standard")
runner = optional(object({
boot_time_in_minutes = optional(number, 5)
ephemeral = optional(bool, false)
jit_config_enabled = optional(bool, null)
maximum_count = optional(number, null)
}), {})

github = optional(object({
repository_white_list = optional(list(string), [])
}), {})

lambda = optional(object({
artifact = optional(object({
zip = optional(string, null)
s3 = optional(object({
key = string
object_version = optional(string, null)
}), null)
}), {})
scale = optional(object({
up = optional(object({
memory_size = optional(number, 512)
timeout = optional(number, 30)
reserved_concurrent_executions = optional(number, 1)
job_queued_check_enabled = optional(bool, null)
event_source_mapping = optional(object({
batch_size = optional(number, 10)
maximum_batching_window_in_seconds = optional(number, 0)
}), {})
tags = optional(map(string), {})
}), {})
down = optional(object({
memory_size = optional(number, 512)
timeout = optional(number, 60)
schedule_expression = optional(string, "cron(*/5 * * * ? *)")
minimum_running_time_in_minutes = optional(number, null)
idle_confirmation_seconds = optional(number, 0)
idle_config = optional(list(object({
cron = string
timeZone = string
idleCount = number
evictionStrategy = optional(string, "oldest_first")
})), [])
tags = optional(map(string), {})
}), {})
}), {})
webhook = optional(object({
artifact = optional(object({
zip = optional(string, null)
s3 = optional(object({
key = string
object_version = optional(string, null)
}), null)
}), {})
api_gateway_access_log_settings = optional(object({
destination_arn = string
format = string
}), null)
memory_size = optional(number, 256)
timeout = optional(number, 10)
tags = optional(map(string), {})
}), {})
pool = optional(object({
memory_size = optional(number, 512)
timeout = optional(number, 60)
reserved_concurrent_executions = optional(number, 1)
config = optional(list(object({
schedule_expression = string
schedule_expression_timezone = optional(string)
size = number
})), [])
include_busy_runners = optional(bool, false)
runner_owner = optional(string, null)
tags = optional(map(string), {})
}), {})
}), {})

queue = optional(object({
delay_webhook_event = optional(number, 30)
job_queue_retention_in_seconds = optional(number, 86400)
visibility_timeout_seconds = optional(number, 180)
redrive_build_queue = optional(object({
enabled = optional(bool, false)
maxReceiveCount = optional(number, null)
}), {
enabled = false
maxReceiveCount = null
})
tags = optional(map(string), {})
encryption = optional(object({
kms_data_key_reuse_period_seconds = number
kms_master_key_id = string
sqs_managed_sse_enabled = bool
}), {
kms_data_key_reuse_period_seconds = null
kms_master_key_id = null
sqs_managed_sse_enabled = true
})
}), {})

}), {})

scale_set = optional(object({
grouping = optional(object({
strategy = optional(string, "compute_provider")
custom = optional(object({
groups = map(object({
runner_configs = set(string)
}))
}), null)
}), {})
container = optional(object({
image = optional(string, null)
user = optional(string, "10001:10001")
health_port = optional(number, 8080)
health_path = optional(string, "/healthz")
health_check_command = optional(list(string), null)
health_check_interval = optional(number, 30)
health_check_timeout = optional(number, 5)
health_check_retries = optional(number, 3)
health_check_start_period = optional(number, 30)
health_stale_after_seconds = optional(number, 180)
shutdown_timeout_seconds = optional(number, 110)
session_close_timeout_seconds = optional(number, 10)
reconnect_initial_backoff_seconds = optional(number, 1)
reconnect_max_backoff_seconds = optional(number, 30)
stop_timeout_seconds = optional(number, 120)
}), {})
config_store = optional(object({
path_prefix = optional(string, null)
tier = optional(string, "Standard")
tags = optional(map(string), {})
}), {})
ecs = optional(object({
cluster = optional(object({
mode = optional(string, "managed")
arn = optional(string, null)
name = optional(string, null)
container_insights = optional(bool, true)
}), {})
task = optional(object({
cpu = optional(number, 512)
memory = optional(number, 1024)
cpu_architecture = optional(string, "X86_64")
ephemeral_storage = optional(object({
size_in_gib = number
}), null)
}), {})
service = optional(object({
platform_version = optional(string, "LATEST")
}), {})
iam = optional(object({
path = optional(string, "/")
permissions_boundary = optional(string, null)
}), {})
}), {})
network = optional(object({
vpc_id = optional(string, null)
subnet_ids = optional(set(string), null)
https_egress = optional(object({
ipv4_cidrs = optional(set(string), ["0.0.0.0/0"])
ipv6_cidrs = optional(set(string), [])
}), {})
}), {})
logging = optional(object({
retention_in_days = optional(number, 30)
kms_key_arn = optional(string, null)
log_group_class = optional(string, "STANDARD")
tags = optional(map(string), {})
}), {})
tags = optional(map(string), {})
}), {})
})
| `{}` | no | +| [global\_config\_orchestration\_provider](#input\_global\_config\_orchestration\_provider) | Global orchestration-provider configuration shared by all runner lanes.

global\_config\_orchestration\_provider = {
webhook: {
queue\_selection\_strategy: "Strategy used to select the build queue for a webhook event."
eventbridge.enabled: "Whether EventBridge integration is enabled for webhook events."
eventbridge.accept\_events: "Event types accepted by the EventBridge integration."
matcher\_config\_parameter\_store\_tier: "SSM Parameter Store tier used for matcher configuration."
runner.boot\_time\_in\_minutes: "Expected runner boot time used by orchestration."
runner.ephemeral: "Whether runners created by the orchestration provider are ephemeral."
runner.jit\_config\_enabled: "Whether JIT runner configuration is enabled."
runner.maximum\_count: "Maximum number of runners that orchestration may create."
github.repository\_white\_list: "Repositories allowed to use the webhook configuration."
lambda.artifact.zip: "Local ZIP artifact used for orchestration Lambda functions."
lambda.artifact.s3.key: "S3 object key for the orchestration Lambda artifact."
lambda.artifact.s3.object\_version: "Optional S3 object version for the orchestration Lambda artifact."
lambda.scale.up.memory\_size: "Memory allocated to the scale-up Lambda."
lambda.scale.up.timeout: "Timeout in seconds for the scale-up Lambda."
lambda.scale.up.reserved\_concurrent\_executions: "Reserved concurrent executions for the scale-up Lambda."
lambda.scale.up.job\_queued\_check\_enabled: "Whether the scale-up Lambda checks queued jobs."
lambda.scale.up.event\_source\_mapping.batch\_size: "Maximum records passed to one scale-up Lambda invocation."
lambda.scale.up.event\_source\_mapping.maximum\_batching\_window\_in\_seconds: "Maximum time to batch records before invoking the scale-up Lambda."
lambda.scale.up.tags: "Tags applied to the scale-up Lambda."
lambda.scale.down.memory\_size: "Memory allocated to the scale-down Lambda."
lambda.scale.down.timeout: "Timeout in seconds for the scale-down Lambda."
lambda.scale.down.schedule\_expression: "Schedule expression for scale-down processing."
lambda.scale.down.minimum\_running\_time\_in\_minutes: "Minimum runner lifetime before scale-down."
lambda.scale.down.idle\_confirmation\_seconds: "Seconds a runner must consistently report not-busy before scale-down terminates it; 0 disables the confirmation window."
lambda.scale.down.idle\_config: "Scheduled minimum idle-runner pool settings."
lambda.scale.down.idle\_config.cron: "Cron expression defining when the idle-runner count applies."
lambda.scale.down.idle\_config.timeZone: "Time zone used to evaluate the idle-runner schedule."
lambda.scale.down.idle\_config.idleCount: "Minimum number of idle runners maintained during the schedule."
lambda.scale.down.idle\_config.evictionStrategy: "Strategy used when evicting idle runners."
lambda.scale.down.tags: "Tags applied to the scale-down Lambda."
lambda.webhook.artifact.zip: "Local ZIP artifact used for the webhook Lambda."
lambda.webhook.artifact.s3.key: "S3 object key for the webhook Lambda artifact."
lambda.webhook.artifact.s3.object\_version: "Optional S3 object version for the webhook Lambda artifact."
lambda.webhook.api\_gateway\_access\_log\_settings: "API Gateway access-log destination and format."
lambda.webhook.api\_gateway\_access\_log\_settings.destination\_arn: "ARN of the API Gateway access-log destination."
lambda.webhook.api\_gateway\_access\_log\_settings.format: "API Gateway access-log format."
lambda.webhook.memory\_size: "Memory allocated to the webhook Lambda."
lambda.webhook.timeout: "Timeout in seconds for the webhook Lambda."
lambda.webhook.tags: "Tags applied to the webhook Lambda."
lambda.pool.memory\_size: "Memory allocated to the pool Lambda."
lambda.pool.timeout: "Timeout in seconds for the pool Lambda."
lambda.pool.reserved\_concurrent\_executions: "Reserved concurrent executions for the pool Lambda."
lambda.pool.config: "Scheduled runner-pool size configuration."
lambda.pool.config.schedule\_expression: "Schedule expression for the pool size."
lambda.pool.config.schedule\_expression\_timezone: "Time zone used to evaluate the pool schedule."
lambda.pool.config.size: "Runner pool size applied by the schedule."
lambda.pool.include\_busy\_runners: "Whether busy runners are included in pool sizing."
lambda.pool.runner\_owner: "GitHub organization that owns the runner pool."
lambda.pool.tags: "Tags applied to the pool Lambda."
queue.delay\_webhook\_event: "Seconds a webhook event remains invisible in the build queue before processing."
queue.job\_queue\_retention\_in\_seconds: "Seconds a queued job is retained before it is purged."
queue.visibility\_timeout\_seconds: "Build queue visibility timeout in seconds."
queue.redrive\_build\_queue.enabled: "Whether the build queue dead-letter queue is enabled."
queue.redrive\_build\_queue.maxReceiveCount: "Maximum receives before a message is moved to the dead-letter queue."
queue.tags: "Tags applied to build queues."
queue.encryption.kms\_data\_key\_reuse\_period\_seconds: "KMS data-key reuse period for queue encryption."
queue.encryption.kms\_master\_key\_id: "KMS key ID used for queue encryption."
queue.encryption.sqs\_managed\_sse\_enabled: "Whether SQS-managed server-side encryption is enabled."
}
} |
object({
webhook = optional(object({
queue_selection_strategy = optional(string, "first")
eventbridge = optional(object({
enabled = optional(bool, true)
accept_events = optional(list(string), [])
}), {})
matcher_config_parameter_store_tier = optional(string, "Standard")
runner = optional(object({
boot_time_in_minutes = optional(number, 5)
ephemeral = optional(bool, false)
jit_config_enabled = optional(bool, null)
maximum_count = optional(number, null)
}), {})

github = optional(object({
repository_white_list = optional(list(string), [])
}), {})

lambda = optional(object({
artifact = optional(object({
zip = optional(string, null)
s3 = optional(object({
key = string
object_version = optional(string, null)
}), null)
}), {})
scale = optional(object({
up = optional(object({
memory_size = optional(number, 512)
timeout = optional(number, 30)
reserved_concurrent_executions = optional(number, 1)
job_queued_check_enabled = optional(bool, null)
event_source_mapping = optional(object({
batch_size = optional(number, 10)
maximum_batching_window_in_seconds = optional(number, 0)
}), {})
tags = optional(map(string), {})
}), {})
down = optional(object({
memory_size = optional(number, 512)
timeout = optional(number, 60)
schedule_expression = optional(string, "cron(*/5 * * * ? *)")
minimum_running_time_in_minutes = optional(number, null)
idle_confirmation_seconds = optional(number, 0)
idle_config = optional(list(object({
cron = string
timeZone = string
idleCount = number
evictionStrategy = optional(string, "oldest_first")
})), [])
tags = optional(map(string), {})
}), {})
}), {})
webhook = optional(object({
artifact = optional(object({
zip = optional(string, null)
s3 = optional(object({
key = string
object_version = optional(string, null)
}), null)
}), {})
api_gateway_access_log_settings = optional(object({
destination_arn = string
format = string
}), null)
memory_size = optional(number, 256)
timeout = optional(number, 10)
tags = optional(map(string), {})
}), {})
pool = optional(object({
memory_size = optional(number, 512)
timeout = optional(number, 60)
reserved_concurrent_executions = optional(number, 1)
config = optional(list(object({
schedule_expression = string
schedule_expression_timezone = optional(string)
size = number
})), [])
include_busy_runners = optional(bool, false)
runner_owner = optional(string, null)
tags = optional(map(string), {})
}), {})
}), {})

queue = optional(object({
delay_webhook_event = optional(number, 30)
job_queue_retention_in_seconds = optional(number, 86400)
visibility_timeout_seconds = optional(number, 180)
redrive_build_queue = optional(object({
enabled = optional(bool, false)
maxReceiveCount = optional(number, null)
}), {
enabled = false
maxReceiveCount = null
})
tags = optional(map(string), {})
encryption = optional(object({
kms_data_key_reuse_period_seconds = number
kms_master_key_id = string
sqs_managed_sse_enabled = bool
}), {
kms_data_key_reuse_period_seconds = null
kms_master_key_id = null
sqs_managed_sse_enabled = true
})
}), {})

}), {})

scale_set = optional(object({
grouping = optional(object({
strategy = optional(string, "compute_provider")
custom = optional(object({
groups = map(object({
runner_configs = set(string)
}))
}), null)
}), {})
container = optional(object({
image = optional(string, null)
user = optional(string, "10001:10001")
health_port = optional(number, 8080)
health_path = optional(string, "/healthz")
health_check_command = optional(list(string), null)
health_check_interval = optional(number, 30)
health_check_timeout = optional(number, 5)
health_check_retries = optional(number, 3)
health_check_start_period = optional(number, 30)
health_stale_after_seconds = optional(number, 180)
shutdown_timeout_seconds = optional(number, 110)
session_close_timeout_seconds = optional(number, 10)
reconnect_initial_backoff_seconds = optional(number, 1)
reconnect_max_backoff_seconds = optional(number, 30)
stop_timeout_seconds = optional(number, 120)
}), {})
config_store = optional(object({
path_prefix = optional(string, null)
tier = optional(string, "Standard")
tags = optional(map(string), {})
}), {})
ecs = optional(object({
cluster = optional(object({
mode = optional(string, "managed")
arn = optional(string, null)
name = optional(string, null)
container_insights = optional(bool, true)
}), {})
task = optional(object({
cpu = optional(number, 512)
memory = optional(number, 1024)
cpu_architecture = optional(string, "X86_64")
ephemeral_storage = optional(object({
size_in_gib = number
}), null)
}), {})
service = optional(object({
platform_version = optional(string, "LATEST")
}), {})
iam = optional(object({
path = optional(string, "/")
permissions_boundary = optional(string, null)
}), {})
}), {})
network = optional(object({
vpc_id = optional(string, null)
subnet_ids = optional(set(string), null)
https_egress = optional(object({
ipv4_cidrs = optional(set(string), ["0.0.0.0/0"])
ipv6_cidrs = optional(set(string), [])
}), {})
}), {})
logging = optional(object({
retention_in_days = optional(number, 180)
kms_key_id = optional(string, null)
log_group_class = optional(string, "STANDARD")
tags = optional(map(string), {})
}), {})
tags = optional(map(string), {})
}), {})
})
| `{}` | no | | [global\_config\_storage\_provider](#input\_global\_config\_storage\_provider) | Global storage-provider configuration shared by all runner lanes.

global\_config\_storage\_provider = {
aws.ssm.paths.root: "Root path for SSM parameters."
aws.ssm.paths.app: "Path segment for application parameters."
aws.ssm.paths.webhook: "Path segment for webhook parameters."
aws.ssm.paths.tokens: "Path segment for runner token parameters."
aws.ssm.paths.config: "Path segment for runner configuration parameters."
aws.ssm.kms\_key\_id: "KMS key ID used to encrypt SSM parameters."
aws.ssm.tags: "Tags applied to SSM resources."
aws.ssm.parameters.tags: "Tags applied to runner configuration parameters."
aws.ssm.housekeeper.schedule\_expression: "Schedule for the SSM parameter housekeeper."
aws.ssm.housekeeper.state: "EventBridge rule state for the SSM housekeeper."
aws.ssm.housekeeper.tags: "Tags applied to the SSM housekeeper resources."
aws.ssm.housekeeper.lambda.artifact.zip: "Local ZIP artifact used for the SSM housekeeper Lambda."
aws.ssm.housekeeper.lambda.artifact.s3.key: "S3 object key for the SSM housekeeper Lambda."
aws.ssm.housekeeper.lambda.artifact.s3.object\_version: "Optional S3 object version for the SSM housekeeper artifact."
aws.ssm.housekeeper.lambda.memory\_size: "Memory allocated to the SSM housekeeper Lambda."
aws.ssm.housekeeper.lambda.timeout: "Timeout in seconds for the SSM housekeeper Lambda."
aws.ssm.housekeeper.config.tokenPath: "Parameter path containing runner tokens to clean up."
aws.ssm.housekeeper.config.minimumDaysOld: "Minimum age in days before an old token is eligible for cleanup."
aws.ssm.housekeeper.config.dryRun: "Whether the SSM housekeeper reports cleanup without deleting parameters."
} |
object({
aws = optional(object({
ssm = optional(object({
paths = optional(object({
root = optional(string, null)
app = optional(string, "app")
webhook = optional(string, "webhook")
tokens = optional(string, "runners/tokens")
config = optional(string, "runners/config")
}), {})
kms_key_id = optional(string, null)
tags = optional(map(string), {})
parameters = optional(object({
tags = optional(map(string), {})
}), {})
housekeeper = optional(object({
schedule_expression = optional(string, "rate(1 day)")
state = optional(string, "ENABLED")
tags = optional(map(string), {})
lambda = optional(object({
artifact = optional(object({
zip = optional(string, null)
s3 = optional(object({
key = string
object_version = optional(string, null)
}), null)
}), {})
memory_size = optional(number, 512)
timeout = optional(number, 60)
}), {})
config = optional(object({
tokenPath = optional(string, null)
minimumDaysOld = optional(number, 1)
dryRun = optional(bool, false)
}), {})
}), {})
}), null)
}), {})
})
| `{}` | no | | [iam\_overrides](#input\_iam\_overrides) | This map provides the possibility to override some IAM defaults. The following attributes are supported: `instance_profile_name` overrides the instance profile name used in the launch template. `runner_role_arn` overrides the IAM role ARN used for the runner instances. |
object({
override_instance_profile = optional(bool, null)
instance_profile_name = optional(string, null)
override_runner_role = optional(bool, null)
runner_role_arn = optional(string, null)
})
|
{
"instance_profile_name": null,
"override_instance_profile": false,
"override_runner_role": false,
"runner_role_arn": null
}
| no | | [instance\_profile\_path](#input\_instance\_profile\_path) | The path that will be added to the instance\_profile, if not set the environment name will be used. | `string` | `null` | no | diff --git a/modules/orchestration-providers/scale-set/README.md b/modules/orchestration-providers/scale-set/README.md index 6dc371fc32..e454b77b1a 100644 --- a/modules/orchestration-providers/scale-set/README.md +++ b/modules/orchestration-providers/scale-set/README.md @@ -248,7 +248,7 @@ No modules. | [logging](#input\_logging) | CloudWatch Logs configuration. CloudWatch encrypts logs at rest with an AWS-owned key by default; set `kms_key_id` to a customer-managed key ID or ARN. |
object({
retention_in_days = optional(number, 180)
kms_key_id = optional(string, null)
log_group_class = optional(string, "STANDARD")
tags = optional(map(string), {})
})
| `{}` | no | | [network](#input\_network) | Private Fargate networking. Tasks never receive public IP addresses and the managed security groups have no ingress. HTTPS egress defaults to full IPv4 Internet access for reachability. GitHub publishes outbound ranges at `https://api.github.com/meta`; restrict egress to those ranges, a NAT gateway, firewall, or proxy when your security posture requires it. |
object({
vpc_id = string
subnet_ids = set(string)
https_egress = optional(object({
ipv4_cidrs = optional(set(string), ["0.0.0.0/0"])
ipv6_cidrs = optional(set(string), [])
}), {})
})
| n/a | yes | | [prefix](#input\_prefix) | Stable prefix used for scale-set controller resources. | `string` | `"github-actions"` | no | -| [runner\_configs](#input\_runner\_configs) | Normalized scale-set runner configurations keyed by stable runner-config name.

Map keys must be known during planning. Credential values are never accepted: `github.app` contains only the exact GitHub App Parameter Store references used by the runtime. `github.enterprise_server` and `github.user_agent` carry the global GitHub settings needed to render each reconciler configuration. `scale_set.runner.group_name` selects the GitHub runner group. `runner_registration_level` selects organization or repository registration, and `runner_owner` supplies the corresponding organization or owner/repository path. Enterprise-level registration is not supported by this module. `compute_provider` carries the provider-neutral scale-set capability contract for this runner configuration. Parameter and optional KMS ARNs, scale-set names, and other inner values may remain unknown until apply. |
map(object({
github = object({
enterprise_server = object({
url = optional(string, null)
ssl_verify = optional(bool, true)
})
app = object({
app_id = object({
name = string
arn = string
kms_key_arn = optional(string, null)
})
private_key = object({
name = string
arn = string
kms_key_arn = optional(string, null)
})
installation_id = object({
name = string
arn = string
kms_key_arn = optional(string, null)
})
})
runner_owner = string
runner_registration_level = string
user_agent = string
})
scale_set = object({
name = string
runner = optional(object({
labels = optional(list(string), [])
group_name = optional(string, "Default")
min_runners = optional(number, 0)
max_runners = optional(number, 10)
boot_time_in_minutes = optional(number, 10)
}), {})
})
compute_provider = object({
type = string
capabilities = object({
scale_set = object({
role_arn = optional(string, null)
configuration_json = optional(string, "{}")
environment_variables = optional(map(string), {})
iam_statements = optional(map(string), {})
})
})
})
}))
| n/a | yes | +| [runner\_configs](#input\_runner\_configs) | Normalized scale-set runner configurations keyed by stable runner-config name.

Map keys must be known during planning. Credential values are never accepted: `github.app` contains only the exact GitHub App Parameter Store references used by the runtime. `github.enterprise_server` and `github.user_agent` carry the global GitHub settings needed to render each reconciler configuration. `scale_set.runner.group_name` selects the GitHub runner group. `runner_registration_level` selects organization or repository registration, and `runner_owner` supplies the corresponding organization or owner/repository path. Enterprise-level registration is not supported by this module. `compute_provider` carries the provider-neutral scale-set capability contract for this runner configuration. Parameter and optional KMS ARNs, scale-set names, and other inner values may remain unknown until apply. |
map(object({
github = object({
enterprise_server = object({
url = optional(string, null)
ssl_verify = optional(bool, true)
})
app = object({
app_id = object({
name = string
arn = string
kms_key_arn = optional(string, null)
})
private_key = object({
name = string
arn = string
kms_key_arn = optional(string, null)
})
installation_id = object({
name = string
arn = string
kms_key_arn = optional(string, null)
})
})
runner_owner = string
runner_registration_level = string
user_agent = string
})
scale_set = object({
name = string
runner = optional(object({
labels = optional(list(string), [])
group_name = optional(string, "Default")
min_runners = optional(number, 0)
max_runners = optional(number, 10)
boot_time_in_minutes = optional(number, 10)
}), {})
})
compute_provider = object({
type = string
capabilities = object({
scale_set = object({
role_arn = optional(string, null)
configuration_json = optional(string, "{}")
environment_variables = optional(map(string), {})
iam_statements = optional(map(string), {})
})
})
})
}))
| n/a | yes | | [tags](#input\_tags) | Tags applied to scale-set orchestration resources. | `map(string)` | `{}` | no | ## Outputs From 696db5ff9a8b789d1a61b29c8e0d21ded268a2ff Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 27 Sep 2026 01:20:16 +0000 Subject: [PATCH 44/44] docs: auto update terraform docs --- modules/runner-config/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/runner-config/README.md b/modules/runner-config/README.md index 2039c0d23f..2ca9605c6d 100644 --- a/modules/runner-config/README.md +++ b/modules/runner-config/README.md @@ -110,7 +110,6 @@ yarn run dist | [aws\_partition](#input\_aws\_partition) | AWS partition used to construct ARNs. | `string` | `"aws"` | no | | [aws\_region](#input\_aws\_region) | AWS region. | `string` | n/a | yes | | [compute\_provider](#input\_compute\_provider) | Typed compute-provider configuration. Provider-owned settings remain inside the selected compute-provider block.

Exactly one compute-provider block must be non-null. The populated block selects the provider, and its presence must be known during planning. Values inside the selected block may remain unknown until apply.

- `aws`: AWS compute-provider configurations.
- `aws.ec2`: EC2 compute-provider configuration.
- `aws.ec2.ami`: Optional AMI discovery or external AMI-parameter configuration. Null uses the operating-system and architecture defaults.
- `aws.ec2.ami.filter`: EC2 AMI filters combined with the provider's default AMI-name filter.
- `aws.ec2.ami.owners`: AWS account IDs or aliases allowed to own the selected AMI.
- `aws.ec2.ami.ssm_parameter`: Optional AMI-ID SSM parameter configuration. Set `arn` to use an existing parameter or `path` to create one managed by the provider.
- `aws.ec2.ami.ssm_parameter.path`: Parent path under which the provider creates the `ami_id` parameter.
- `aws.ec2.ami.ssm_parameter.arn`: ARN of an existing AMI-ID parameter. The ARN may be unknown until apply.
- `aws.ec2.ami.kms_key`: Optional KMS key required to launch encrypted AMIs or snapshots. The wrapper's presence is the plan-time policy discriminator.
- `aws.ec2.ami.kms_key.arn`: ARN of the KMS key. The ARN may be unknown until apply.
- `aws.ec2.vpc_id`: VPC in which runner networking resources are created.
- `aws.ec2.subnet_ids`: Subnets from which scale-up may launch runner instances.
- `aws.ec2.overrides`: Optional resource-name overrides.
- `aws.ec2.overrides.name_runner`: Name tag used for runner compute resources. An empty value uses the generated provider name.
- `aws.ec2.overrides.name_sg`: Name tag used for the managed runner security group. An empty value uses the generated provider name.
- `aws.ec2.instance_profile`: Optional externally managed instance profile used by the launch template.
- `aws.ec2.instance_profile.name`: Name of the externally managed instance profile.
- `aws.ec2.instance_profile_path`: IAM path for the provider-managed instance profile. Null uses a path derived from the runner-configuration prefix.
- `aws.ec2.binaries_syncer`: Runner-distribution synchronization configuration.
- `aws.ec2.binaries_syncer.enabled`: Enables use of a synchronized runner distribution from S3.
- `aws.ec2.binaries_syncer.s3`: S3 object containing the synchronized runner distribution. Required when synchronization is enabled.
- `aws.ec2.binaries_syncer.s3.arn`: ARN of the runner-distribution bucket, used by IAM policies.
- `aws.ec2.binaries_syncer.s3.id`: Bucket name used to construct the runner-distribution S3 URI.
- `aws.ec2.binaries_syncer.s3.key`: Object key of the runner distribution.
- `aws.ec2.block_device_mappings`: EBS mappings added to the runner launch template.
- `aws.ec2.block_device_mappings[].delete_on_termination`: Deletes the volume when its runner instance terminates.
- `aws.ec2.block_device_mappings[].device_name`: Device name exposed to the runner instance.
- `aws.ec2.block_device_mappings[].encrypted`: Enables EBS encryption.
- `aws.ec2.block_device_mappings[].iops`: Provisioned IOPS for volume types that support it.
- `aws.ec2.block_device_mappings[].kms_key_id`: KMS key ID or ARN used to encrypt the volume.
- `aws.ec2.block_device_mappings[].snapshot_id`: Snapshot used to initialize the volume.
- `aws.ec2.block_device_mappings[].throughput`: Provisioned throughput for volume types that support it.
- `aws.ec2.block_device_mappings[].volume_initialization_rate`: Fixed initialization rate in MiB/s for supported snapshot-backed volumes.
- `aws.ec2.block_device_mappings[].volume_size`: Volume size in GiB.
- `aws.ec2.block_device_mappings[].volume_type`: EBS volume type.
- `aws.ec2.ebs_optimized`: Requests EBS-optimized runner instances.
- `aws.ec2.instance_target_capacity_type`: Primary capacity type, either `spot` or `on-demand`.
- `aws.ec2.instance_allocation_strategy`: EC2 Fleet allocation strategy used to select instance capacity.
- `aws.ec2.instance_type_priorities`: Optional numeric priorities keyed by instance type.
- `aws.ec2.instance_max_spot_price`: Optional maximum hourly Spot price.
- `aws.ec2.instance_types`: EC2 instance types available to the scale-up and pool functions.
- `aws.ec2.user_data`: Runner bootstrap user-data configuration.
- `aws.ec2.user_data.enabled`: Enables launch-template user data.
- `aws.ec2.user_data.template`: Optional path to a custom user-data template.
- `aws.ec2.user_data.content`: Optional complete user-data content. When set, it is used instead of rendering a template.
- `aws.ec2.user_data.pre_install`: Script content inserted before runner installation in the default template.
- `aws.ec2.user_data.post_install`: Script content inserted after runner installation in the default template.
- `aws.ec2.user_data.debug_logging_enabled`: Enables verbose user-data tracing, which can expose secrets in logs.
- `aws.ec2.ssm_enabled`: Attaches runner permissions and policies required for AWS Systems Manager access.
- `aws.ec2.create_service_linked_role_spot`: Allows scale-up to create the EC2 Spot service-linked role.
- `aws.ec2.cloudwatch_agent`: CloudWatch agent configuration for runner instances.
- `aws.ec2.cloudwatch_agent.enabled`: Installs and configures the CloudWatch agent through the default bootstrap flow.
- `aws.ec2.cloudwatch_agent.config`: Optional complete CloudWatch agent configuration. Null renders the provider default from `log_files`.
- `aws.ec2.managed_security_group_enabled`: Creates and attaches the provider-managed runner security group.
- `aws.ec2.log_files`: Optional log files collected by the CloudWatch agent. Null uses the provider defaults.
- `aws.ec2.log_files[].log_group_name`: CloudWatch log-group name, before optional prefixing.
- `aws.ec2.log_files[].prefix_log_group`: Prefixes the log-group name with the runner configuration path when true.
- `aws.ec2.log_files[].file_path`: File or glob read by the CloudWatch agent.
- `aws.ec2.log_files[].log_stream_name`: CloudWatch log-stream name template.
- `aws.ec2.log_files[].log_class`: CloudWatch log-group class for the collected file.
- `aws.ec2.key_name`: Optional EC2 key-pair name added to the launch template.
- `aws.ec2.additional_security_group_ids`: Existing security groups attached in addition to the managed security group.
- `aws.ec2.detailed_monitoring_enabled`: Enables detailed EC2 monitoring for runner instances.
- `aws.ec2.egress_rules`: Egress rules created on the managed runner security group.
- `aws.ec2.egress_rules[].cidr_blocks`: IPv4 CIDR destinations.
- `aws.ec2.egress_rules[].ipv6_cidr_blocks`: IPv6 CIDR destinations.
- `aws.ec2.egress_rules[].prefix_list_ids`: AWS prefix-list destinations.
- `aws.ec2.egress_rules[].from_port`: First destination port in the permitted range.
- `aws.ec2.egress_rules[].protocol`: IP protocol name or number. Use `-1` for all protocols.
- `aws.ec2.egress_rules[].security_groups`: Destination security-group IDs.
- `aws.ec2.egress_rules[].self`: Allows traffic to the managed security group itself when true.
- `aws.ec2.egress_rules[].to_port`: Last destination port in the permitted range.
- `aws.ec2.egress_rules[].description`: Optional rule description.
- `aws.ec2.tags`: Additional tags for runner instances, EBS volumes, network interfaces, and eligible Spot instance requests created from the launch template. They override module-level tags and the generated runner `Name`; the provider-managed `ghr:environment`, `ghr:ssm_config_path`, and `ghr:runner_name_prefix` bootstrap tags take final precedence. These tags do not apply to static provider resources such as the launch template, security group, IAM resources, SSM parameters, or log groups.
- `aws.ec2.metadata_options`: Instance Metadata Service configuration in the launch template.
- `aws.ec2.metadata_options.instance_metadata_tags`: Exposes instance tags through Instance Metadata Service when `enabled`.
- `aws.ec2.metadata_options.http_endpoint`: Enables or disables the Instance Metadata Service endpoint.
- `aws.ec2.metadata_options.http_tokens`: Controls whether IMDSv2 session tokens are optional or required.
- `aws.ec2.metadata_options.http_put_response_hop_limit`: Network hop limit for Instance Metadata Service token responses.
- `aws.ec2.credit_specification`: CPU credit mode for burstable instance types, either `standard` or `unlimited`.
- `aws.ec2.cpu_options`: CPU topology and processor-feature configuration.
- `aws.ec2.cpu_options.core_count`: Number of CPU cores exposed to the runner instance.
- `aws.ec2.cpu_options.threads_per_core`: Number of hardware threads exposed per CPU core.
- `aws.ec2.cpu_options.amd_sev_snp`: Enables or disables AMD SEV-SNP on supported instance types.
- `aws.ec2.cpu_options.nested_virtualization`: Enables or disables nested virtualization on supported instance types.
- `aws.ec2.placement`: EC2 placement configuration for runner instances.
- `aws.ec2.placement.affinity`: Host affinity setting.
- `aws.ec2.placement.availability_zone`: Availability Zone in which the instance is placed.
- `aws.ec2.placement.group_id`: Placement-group ID.
- `aws.ec2.placement.group_name`: Placement-group name.
- `aws.ec2.placement.host_id`: Dedicated Host ID.
- `aws.ec2.placement.host_resource_group_arn`: ARN of the host resource group used for placement.
- `aws.ec2.placement.spread_domain`: Spread-domain placement value.
- `aws.ec2.placement.tenancy`: Instance tenancy, such as `default`, `dedicated`, or `host`.
- `aws.ec2.placement.partition_number`: Placement-group partition number.
- `aws.ec2.license_specifications`: License Manager configurations added to the launch template.
- `aws.ec2.license_specifications[].license_configuration_arn`: ARN of a License Manager license configuration.
- `aws.ec2.associate_public_ipv4_address`: Associates a public IPv4 address with runner network interfaces.
- `aws.ec2.network_interfaces`: Advanced network interface configuration for the launch template. Leave empty to keep using `associate_public_ipv4_address` for a simple single-interface setup.
- `aws.ec2.on_demand_failover_for_errors`: EC2 error codes that trigger an on-demand fallback after a Spot launch failure.
- `aws.ec2.scale_errors`: EC2 error codes treated as retryable scale-up failures.
- `aws.ec2.use_dedicated_host`: Enables the dedicated-host launch path, required for macOS runners. |
object({
aws = optional(object({
ec2 = optional(object({
ami = optional(object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
ssm_parameter = optional(object({
path = optional(string, null)
arn = optional(string, null)
}), null)
kms_key = optional(object({
arn = string
}), null)
}), null)
vpc_id = string
subnet_ids = list(string)
overrides = optional(object({
name_runner = optional(string, "")
name_sg = optional(string, "")
}), {})
instance_profile = optional(object({
name = string
}), null)
instance_profile_path = optional(string, null)
binaries_syncer = optional(object({
enabled = optional(bool, true)
s3 = optional(object({
arn = string
id = string
key = string
}), null)
}), {})
block_device_mappings = optional(list(object({
delete_on_termination = optional(bool, true)
device_name = optional(string, "/dev/xvda")
encrypted = optional(bool, true)
iops = optional(number)
kms_key_id = optional(string)
snapshot_id = optional(string)
throughput = optional(number)
volume_initialization_rate = optional(number)
volume_size = number
volume_type = optional(string, "gp3")
})), [{ volume_size = 30 }])
ebs_optimized = optional(bool, false)
instance_target_capacity_type = optional(string, "spot")
instance_allocation_strategy = optional(string, "lowest-price")
instance_type_priorities = optional(map(number), null)
instance_max_spot_price = optional(string, null)
instance_types = list(string)
user_data = optional(object({
enabled = optional(bool, true)
template = optional(string, null)
content = optional(string, null)
pre_install = optional(string, "")
post_install = optional(string, "")
debug_logging_enabled = optional(bool, false)
}), {})
ssm_enabled = optional(bool, false)
create_service_linked_role_spot = optional(bool, false)
cloudwatch_agent = optional(object({
enabled = optional(bool, true)
config = optional(string, null)
}), {})
managed_security_group_enabled = optional(bool, true)
log_files = optional(list(object({
log_group_name = string
prefix_log_group = bool
file_path = string
log_stream_name = string
log_class = optional(string, "STANDARD")
})), null)
key_name = optional(string, null)
additional_security_group_ids = optional(list(string), [])
detailed_monitoring_enabled = optional(bool, false)
egress_rules = optional(list(object({
cidr_blocks = list(string)
ipv6_cidr_blocks = list(string)
prefix_list_ids = list(string)
from_port = number
protocol = string
security_groups = list(string)
self = bool
to_port = number
description = string
})), [{
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
prefix_list_ids = null
from_port = 0
protocol = "-1"
security_groups = null
self = null
to_port = 0
description = null
}])
tags = optional(map(string), {})
metadata_options = optional(object({
instance_metadata_tags = optional(string, "enabled")
http_endpoint = optional(string, "enabled")
http_tokens = optional(string, "required")
http_put_response_hop_limit = optional(number, 1)
}), {})
credit_specification = optional(string, null)
cpu_options = optional(object({
core_count = optional(number)
threads_per_core = optional(number)
amd_sev_snp = optional(string)
nested_virtualization = optional(string)
}), null)
placement = optional(object({
affinity = optional(string)
availability_zone = optional(string)
group_id = optional(string)
group_name = optional(string)
host_id = optional(string)
host_resource_group_arn = optional(string)
spread_domain = optional(string)
tenancy = optional(string)
partition_number = optional(number)
}), null)
license_specifications = optional(list(object({
license_configuration_arn = string
})), [])
associate_public_ipv4_address = optional(bool, false)
network_interfaces = optional(list(object({
associate_carrier_ip_address = optional(bool)
associate_public_ip_address = optional(bool)
delete_on_termination = optional(bool)
description = optional(string)
device_index = optional(number)
interface_type = optional(string)
ipv4_address_count = optional(number)
ipv4_addresses = optional(list(string))
ipv4_prefix_count = optional(number)
ipv4_prefixes = optional(list(string))
ipv6_address_count = optional(number)
ipv6_addresses = optional(list(string))
ipv6_prefix_count = optional(number)
ipv6_prefixes = optional(list(string))
network_card_index = optional(number)
network_interface_id = optional(string)
primary_ipv6 = optional(bool)
private_ip_address = optional(string)
security_groups = optional(list(string))
subnet_id = optional(string)
connection_tracking_specification = optional(object({
tcp_established_timeout = optional(number)
udp_stream_timeout = optional(number)
udp_timeout = optional(number)
}))
ena_srd_specification = optional(object({
ena_srd_enabled = optional(bool)
ena_srd_udp_specification = optional(object({
ena_srd_udp_enabled = optional(bool)
}))
}))
})), [])
on_demand_failover_for_errors = optional(list(string), [])
scale_errors = optional(list(string), [
"UnfulfillableCapacity",
"MaxSpotInstanceCountExceeded",
"TargetCapacityLimitExceededException",
"RequestLimitExceeded",
"ResourceLimitExceeded",
"MaxSpotInstanceCountExceeded",
"MaxSpotFleetRequestCountExceeded",
"InsufficientInstanceCapacity",
"InsufficientCapacityOnHost",
])
use_dedicated_host = optional(bool, false)
}), null)
}), {})
})
| n/a | yes | -| [compute\_provider\_key](#input\_compute\_provider\_key) | Optional plan-known compute-provider dispatch key. Null discovers the key from the exactly one populated compute\_provider block. | `string` | `null` | no | | [github](#input\_github) | GitHub API and runner-registration configuration.

- `app_parameters.key_base64`: Parameter Store reference for the primary GitHub App private key.
- `app_parameters.id`: Parameter Store reference for the primary GitHub App ID.
- `app_parameters.additional_apps_manifest`: Optional Parameter Store reference containing the additional GitHub App manifest.
- `app_parameters.additional_app_parameter_arns`: ARNs of the additional GitHub App credential parameters.
- `enterprise_server.url`: Optional GitHub Enterprise Server base URL. Null selects GitHub.com.
- `enterprise_server.ssl_verify`: Enables TLS certificate verification for GitHub Enterprise Server requests.
- `user_agent`: Optional User-Agent value added to GitHub API requests. |
object({
app_parameters = object({
key_base64 = map(string)
id = map(string)
additional_apps_manifest = optional(object({
name = string
arn = string
}), null)
additional_app_parameter_arns = optional(list(string), [])
})
enterprise_server = optional(object({
url = optional(string, null)
ssl_verify = optional(bool, true)
}), {})
user_agent = optional(string, null)
})
| n/a | yes | | [lambda](#input\_lambda) | Common Lambda substrate independent of the selected runner orchestration provider.

- `artifact.s3.bucket`: Optional shared S3 bucket containing component-owned Lambda artifacts. An orchestration provider selects its own object key and version; the bucket alone selects no artifact.
- `runtime`: Runtime used by the control-plane Lambda functions.
- `architecture`: Instruction-set architecture used by the control-plane Lambda functions. Supported values are `arm64` and `x86_64`.
- `subnet_ids`: Subnets used for Lambda VPC configuration.
- `security_group_ids`: Security groups used for Lambda VPC configuration.
- `tags`: Shared tags applied to Lambda function resources only. These override module-level `tags`; component `tags` override this map when keys conflict.
- `principals`: Additional principals allowed to assume the control-plane Lambda roles.
- `role.path`: IAM path for module-managed Lambda execution roles. Defaults to a path derived from `prefix`.
- `role.permissions_boundary`: Permissions-boundary ARN applied to module-managed Lambda execution roles. |
object({
artifact = optional(object({
s3 = optional(object({
bucket = optional(string, null)
}), {})
}), {})
runtime = optional(string, "nodejs24.x")
architecture = optional(string, "arm64")
subnet_ids = optional(list(string), [])
security_group_ids = optional(list(string), [])
tags = optional(map(string), {})
principals = optional(list(object({
type = string
identifiers = list(string)
})), [])
role = optional(object({
path = optional(string, null)
permissions_boundary = optional(string, null)
}), {})
})
| `{}` | no | | [observability](#input\_observability) | Logging, tracing, and metrics configuration for control-plane and provider resources.

- `logs.level`: Application log level supplied to the control-plane functions.
- `logs.retention_in_days`: CloudWatch Logs retention period.
- `logs.kms_key_id`: Optional KMS key ID or ARN used to encrypt CloudWatch log groups.
- `logs.class`: CloudWatch log-group class. Supported values are `STANDARD` and `INFREQUENT_ACCESS`.
- `logs.tags`: Shared tags for CloudWatch log groups. These override module-level `tags`; component `tags` override this map when keys conflict.
- `tracing.mode`: Optional Lambda active-tracing mode. Null disables X-Ray tracing configuration.
- `tracing.capture_http_requests`: Enables HTTP request capture in the tracing helper.
- `tracing.capture_error`: Enables error capture in the tracing helper.
- `metrics.enabled`: Enables module-emitted metrics.
- `metrics.namespace`: CloudWatch namespace used for emitted metrics.
- `metrics.metric.github_app_rate_limit.enabled`: Emits GitHub App rate-limit metrics.
- `metrics.metric.job_retry.enabled`: Emits job-retry metrics.
- `metrics.metric.spot_termination_warning.enabled`: Emits spot-termination warning metrics where supported. |
object({
logs = optional(object({
level = optional(string, "info")
retention_in_days = optional(number, 180)
kms_key_id = optional(string, null)
class = optional(string, "STANDARD")
tags = optional(map(string), {})
}), {})
tracing = optional(object({
mode = optional(string, null)
capture_http_requests = optional(bool, false)
capture_error = optional(bool, false)
}), {})
metrics = optional(object({
enabled = optional(bool, false)
namespace = optional(string, "GitHub Runners")
metric = optional(object({
github_app_rate_limit = optional(object({
enabled = optional(bool, true)
}), {})
job_retry = optional(object({
enabled = optional(bool, true)
}), {})
spot_termination_warning = optional(object({
enabled = optional(bool, true)
}), {})
}), {})
}), {})
})
| `{}` | no |