diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml index 2744676836..c38c393752 100644 --- a/.github/workflows/terraform.yml +++ b/.github/workflows/terraform.yml @@ -214,6 +214,7 @@ jobs: matrix: module: - modules/runners + - modules/multi-runner defaults: run: working-directory: ${{ matrix.module }} diff --git a/modules/multi-runner/README.md b/modules/multi-runner/README.md index 21fc8f441b..7cc68f5177 100644 --- a/modules/multi-runner/README.md +++ b/modules/multi-runner/README.md @@ -151,6 +151,7 @@ module "multi-runner" { | [enable\_ami\_housekeeper](#input\_enable\_ami\_housekeeper) | Option to disable the lambda to clean up old AMIs. | `bool` | `false` | no | | [enable\_managed\_runner\_security\_group](#input\_enable\_managed\_runner\_security\_group) | Enabling the default managed security group creation. Unmanaged security groups can be specified via `runner_additional_security_group_ids`. | `bool` | `true` | no | | [eventbridge](#input\_eventbridge) | Enable the use of EventBridge by the module. By enabling this feature events will be put on the EventBridge by the webhook instead of directly dispatching to queues for scaling. |
object({
enable = optional(bool, true)
accept_events = optional(list(string), [])
}) | `{}` | no |
+| [experimental](#input\_experimental) | Experimental provider-oriented configuration contract. This schema can change before it becomes stable.object({
tags = optional(map(string), {})
roles = optional(object({
path = optional(string, null)
permissions_boundary = optional(string, null)
}), {})
runner = optional(object({
os = optional(string, null)
architecture = optional(string, null)
disable_default_labels = optional(bool, false)
extra_labels = optional(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)
}), {})
}), {})
github = optional(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
}))
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)
}), {})
user_agent = optional(string, "github-aws-runners")
}), {})
lambda = optional(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)
}), {})
}), {})
orchestration_provider = optional(object({
webhook = optional(object({
queue_selection_strategy = optional(string, "first")
eventbridge = optional(object({
enable = 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_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
})
}), {})
}), {})
}), {})
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)
}), {})
}), {})
}), {})
observability = optional(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({
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 = optional(bool, true)
enable_spot_termination_warning = optional(bool, true)
}), {})
}), {})
}), {})
compute_provider = optional(object({
selections = optional(map(object({
namespace = string
type = string
})), null)
aws = optional(object({
ec2 = optional(object({
vpc_id = optional(string, null)
subnet_ids = optional(list(string), null)
managed_security_group_enabled = optional(bool, true)
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
}])
additional_security_group_ids = optional(list(string), [])
cloudwatch_agent = optional(object({
config = optional(string, null)
}), {})
instance_profile_path = optional(string, null)
key_name = optional(string, null)
associate_public_ipv4_address = optional(bool, false)
tags = optional(map(string), {})
ami = optional(object({
housekeeper = optional(object({
enabled = optional(bool, false)
cleanup_config = optional(object({
maxItems = optional(number)
minimumDaysOld = optional(number)
amiFilters = optional(list(object({
Name = string
Values = list(string)
})))
launchTemplateNames = optional(list(string))
ssmParameterNames = optional(list(string))
dryRun = optional(bool)
}), {})
artifact = optional(object({
zip = optional(string, null)
s3 = optional(object({
key = string
object_version = optional(string, null)
}), null)
}), {})
lambda = optional(object({
memory_size = optional(number, 256)
timeout = optional(number, 300)
}), {})
schedule = optional(object({
expression = optional(string, "cron(11 7 * * ? *)")
}), {})
}), {})
}), {})
instance_termination_watcher = optional(object({
enabled = 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), {})
artifact = optional(object({
zip = optional(string, null)
s3 = optional(object({
key = string
object_version = optional(string, null)
}), null)
}), {})
lambda = optional(object({
memory_size = optional(number, null)
timeout = optional(number, null)
}), {})
}), {})
runner_binaries = optional(object({
enabled = optional(bool, true)
targets = optional(map(object({
os = string
architecture = string
})), null)
s3 = optional(object({
encryption = optional(object({
enabled = optional(bool, true)
bucket_key_enabled = optional(bool, null)
sse_algorithm = optional(string, "AES256")
kms_master_key_id = optional(string, null)
}), {})
tags = optional(map(string), {})
versioning = optional(string, "Disabled")
logging = optional(object({
bucket = optional(string, null)
prefix = optional(string, null)
}), {})
}), {})
syncer = optional(object({
artifact = optional(object({
zip = optional(string, null)
s3 = optional(object({
key = string
object_version = optional(string, null)
}), null)
}), {})
lambda = optional(object({
memory_size = optional(number, 256)
timeout = optional(number, 300)
}), {})
schedule = optional(object({
expression = optional(string, "cron(27 * * * ? *)")
state = optional(string, "ENABLED")
}), {})
}), {})
}), {})
}), {})
}), {})
}), {})
multi_runner_config = optional(map(object({
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 = 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 = object({
labelMatchers = list(list(string))
exactMatch = optional(bool, false)
bidirectionalLabelMatch = optional(bool, false)
priority = optional(number, 999)
enableDynamicLabels = optional(bool, false)
awsDynamicLabelsPolicy = optional(object({
blocked_keys = optional(list(string), [])
restricted_keys = optional(map(object({
allowed = optional(list(string), [])
denied = optional(list(string), [])
max = optional(string, 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_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)
})
ssm = optional(object({
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({
enable = optional(bool, null)
namespace = optional(string, null)
metric = optional(object({
enable_github_app_rate_limit = optional(bool, null)
enable_job_retry = optional(bool, null)
}), {})
}), {})
}), {})
compute_provider = 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 = 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)
enable_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)
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 |
| [ghes\_ssl\_verify](#input\_ghes\_ssl\_verify) | GitHub Enterprise SSL verification. Set to 'false' when custom certificate (chains) is used for GitHub Enterprise Server (insecure). | `bool` | `true` | no |
| [ghes\_url](#input\_ghes\_url) | GitHub Enterprise Server URL. Example: https://github.internal.co - DO NOT SET IF USING PUBLIC GITHUB. .However if you are using GitHub Enterprise Cloud with data-residency (ghe.com), set the endpoint here. Example - https://companyname.ghe.com\| | `string` | `null` | no |
| [github\_app](#input\_github\_app) | GitHub app parameters, see your github app.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
}))
webhook_secret = optional(string)
webhook_secret_ssm = optional(object({
arn = string
name = string
}))
}) | n/a | yes |
diff --git a/modules/multi-runner/config.experimental.translation.tf b/modules/multi-runner/config.experimental.translation.tf
new file mode 100644
index 0000000000..b86e3d42b0
--- /dev/null
+++ b/modules/multi-runner/config.experimental.translation.tf
@@ -0,0 +1,774 @@
+# Project stable v1 inputs into the experimental schema, then resolve every
+# runner configuration against the experimental global defaults. This file is
+# a pure compatibility adapter: resource and provider modules intentionally do
+# not consume the translated object until the follow-up implementation.
+locals {
+ # A non-empty experimental map selects v2 normalization. Resource routing
+ # remains stable-only until the follow-up implementation consumes this local.
+ use_multi_runner_config_v2 = length(var.experimental.multi_runner_config) > 0
+
+ raw_translated_experimental = local.use_multi_runner_config_v2 ? var.experimental : {
+ tags = var.tags
+
+ roles = {
+ path = var.role_path
+ permissions_boundary = var.role_permissions_boundary
+ }
+
+ runner = {
+ os = null
+ architecture = null
+ disable_default_labels = false
+ extra_labels = []
+ group_name = "Default"
+ name_prefix = ""
+ run_as_root = false
+ run_as = "ec2-user"
+ auto_update_disabled = false
+ tags = {}
+ hooks = {
+ job_started = ""
+ job_completed = ""
+ }
+ iam = {
+ role = null
+ managed_policy_arns = {}
+ additional_trust_policy_json = null
+ path = null
+ permissions_boundary = null
+ }
+ }
+
+ github = {
+ app = var.github_app
+ additional_apps = var.additional_github_apps
+ enterprise_server = {
+ url = var.ghes_url
+ ssl_verify = var.ghes_ssl_verify
+ }
+ user_agent = var.user_agent
+ }
+
+ lambda = {
+ artifact = {
+ s3 = {
+ bucket = var.lambda_s3_bucket
+ }
+ }
+ runtime = var.lambda_runtime
+ architecture = var.lambda_architecture
+ principals = var.lambda_principals
+ subnet_ids = var.lambda_subnet_ids
+ security_group_ids = var.lambda_security_group_ids
+ tags = var.lambda_tags
+ role = {
+ path = null
+ permissions_boundary = null
+ }
+ }
+
+ orchestration_provider = {
+ webhook = {
+ queue_selection_strategy = var.queue_selection_strategy
+ eventbridge = var.eventbridge
+ matcher_config_parameter_store_tier = var.matcher_config_parameter_store_tier
+ runner = {
+ boot_time_in_minutes = 5
+ ephemeral = false
+ jit_config_enabled = null
+ maximum_count = null
+ }
+ github = {
+ repository_white_list = var.repository_white_list
+ }
+ lambda = {
+ artifact = {
+ zip = var.lambda_s3_bucket == null ? var.runners_lambda_zip : null
+ s3 = var.lambda_s3_bucket == null ? null : {
+ key = var.runners_lambda_s3_key
+ object_version = var.runners_lambda_s3_object_version
+ }
+ }
+ scale = {
+ up = {
+ memory_size = var.scale_up_lambda_memory_size
+ timeout = var.runners_scale_up_lambda_timeout
+ reserved_concurrent_executions = 1
+ job_queued_check_enabled = 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 = {}
+ }
+ 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 = []
+ tags = {}
+ }
+ }
+ webhook = {
+ artifact = {
+ zip = var.lambda_s3_bucket == null ? var.webhook_lambda_zip : null
+ s3 = var.lambda_s3_bucket == null ? null : {
+ key = var.webhook_lambda_s3_key
+ object_version = var.webhook_lambda_s3_object_version
+ }
+ }
+ 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 = {}
+ }
+ pool = {
+ memory_size = 512
+ timeout = var.pool_lambda_timeout
+ reserved_concurrent_executions = var.pool_lambda_reserved_concurrent_executions
+ config = []
+ include_busy_runners = false
+ runner_owner = null
+ tags = {}
+ }
+ }
+ queue = {
+ delay_webhook_event = 30
+ job_queue_retention_in_seconds = 86400
+ visibility_timeout_seconds = var.runners_scale_up_lambda_timeout
+ redrive_build_queue = {
+ enabled = false
+ maxReceiveCount = null
+ }
+ tags = {}
+ encryption = var.queue_encryption
+ }
+ }
+ }
+
+ ssm = {
+ paths = {
+ root = "/${var.ssm_paths.root}/${var.prefix}"
+ app = var.ssm_paths.app
+ webhook = var.ssm_paths.webhook
+ tokens = "${var.ssm_paths.runners}/tokens"
+ config = "${var.ssm_paths.runners}/config"
+ }
+ kms_key_id = var.kms_key_arn
+ 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 = {}
+ lambda = {
+ artifact = {
+ zip = var.lambda_s3_bucket == null ? var.runners_lambda_zip : null
+ s3 = var.lambda_s3_bucket == null ? null : {
+ key = var.runners_lambda_s3_key
+ object_version = var.runners_lambda_s3_object_version
+ }
+ }
+ memory_size = var.runners_ssm_housekeeper.lambda_memory_size
+ timeout = var.runners_ssm_housekeeper.lambda_timeout
+ }
+ config = {
+ tokenPath = var.runners_ssm_housekeeper.config.tokenPath
+ minimumDaysOld = var.runners_ssm_housekeeper.config.minimumDaysOld
+ dryRun = var.runners_ssm_housekeeper.config.dryRun
+ }
+ }
+ }
+
+ observability = {
+ logs = {
+ level = var.log_level
+ retention_in_days = var.logging_retention_in_days
+ kms_key_id = var.logging_kms_key_id
+ class = var.log_class
+ tags = {}
+ }
+ tracing = var.tracing_config
+ metrics = {
+ enable = var.metrics.enable
+ namespace = var.metrics.namespace
+ metric = {
+ enable_github_app_rate_limit = var.metrics.metric.enable_github_app_rate_limit
+ enable_job_retry = var.metrics.metric.enable_job_retry
+ enable_spot_termination = true
+ enable_spot_termination_warning = var.metrics.metric.enable_spot_termination_warning
+ }
+ }
+ }
+
+ compute_provider = {
+ selections = null
+ aws = {
+ ec2 = {
+ vpc_id = var.vpc_id
+ subnet_ids = var.subnet_ids
+ managed_security_group_enabled = var.enable_managed_runner_security_group
+ egress_rules = var.runner_egress_rules
+ additional_security_group_ids = var.runner_additional_security_group_ids
+ cloudwatch_agent = {
+ config = var.cloudwatch_config
+ }
+ instance_profile_path = var.instance_profile_path
+ key_name = var.key_name
+ associate_public_ipv4_address = var.associate_public_ipv4_address
+ tags = {}
+ ami = {
+ housekeeper = {
+ enabled = var.enable_ami_housekeeper
+ cleanup_config = var.ami_housekeeper_cleanup_config
+ artifact = {
+ zip = var.lambda_s3_bucket == null ? var.ami_housekeeper_lambda_zip : null
+ s3 = var.lambda_s3_bucket == null ? null : {
+ key = var.ami_housekeeper_lambda_s3_key
+ object_version = var.ami_housekeeper_lambda_s3_object_version
+ }
+ }
+ lambda = {
+ memory_size = var.ami_housekeeper_lambda_memory_size
+ timeout = var.ami_housekeeper_lambda_timeout
+ }
+ schedule = {
+ expression = var.ami_housekeeper_lambda_schedule_expression
+ }
+ }
+ }
+ instance_termination_watcher = {
+ enabled = var.instance_termination_watcher.enable
+ features = var.instance_termination_watcher.features
+ enable_runner_deregistration = var.instance_termination_watcher.enable_runner_deregistration
+ environment_variables = var.instance_termination_watcher.environment_variables
+ artifact = {
+ zip = var.lambda_s3_bucket == null ? var.instance_termination_watcher.zip : null
+ s3 = var.lambda_s3_bucket == null ? null : {
+ key = var.instance_termination_watcher.s3_key
+ object_version = var.instance_termination_watcher.s3_object_version
+ }
+ }
+ lambda = {
+ memory_size = var.instance_termination_watcher.memory_size
+ timeout = var.instance_termination_watcher.timeout
+ }
+ }
+ runner_binaries = {
+ enabled = true
+ targets = null
+ 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)
+ }
+ tags = var.runner_binaries_s3_tags
+ versioning = var.runner_binaries_s3_versioning
+ logging = {
+ bucket = null
+ prefix = null
+ }
+ }
+ syncer = {
+ artifact = {
+ zip = var.lambda_s3_bucket == null ? var.runner_binaries_syncer_lambda_zip : null
+ s3 = var.lambda_s3_bucket == null ? null : {
+ key = var.syncer_lambda_s3_key
+ object_version = var.syncer_lambda_s3_object_version
+ }
+ }
+ lambda = {
+ memory_size = var.runner_binaries_syncer_memory_size
+ timeout = var.runner_binaries_syncer_lambda_timeout
+ }
+ schedule = {
+ expression = "cron(27 * * * ? *)"
+ state = var.state_event_rule_binaries_syncer
+ }
+ }
+ }
+ }
+ }
+ }
+
+ multi_runner_config = {
+ for k, v in var.multi_runner_config : k => {
+ tags = {}
+
+ runner = {
+ os = v.runner_config.runner_os
+ architecture = v.runner_config.runner_architecture
+ disable_default_labels = v.runner_config.runner_disable_default_labels
+ extra_labels = v.runner_config.runner_extra_labels
+ group_name = v.runner_config.runner_group_name
+ name_prefix = v.runner_config.runner_name_prefix
+ 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 = {}
+ hooks = {
+ job_started = v.runner_config.runner_hook_job_started
+ job_completed = v.runner_config.runner_hook_job_completed
+ }
+ iam = {
+ role = v.runner_config.iam_overrides.override_runner_role == true ? {
+ arn = v.runner_config.iam_overrides.runner_role_arn
+ } : null
+ managed_policy_arns = {
+ for policy_index, policy_arn in v.runner_config.runner_iam_role_managed_policy_arns :
+ "legacy-${policy_index}" => policy_arn
+ }
+ additional_trust_policy_json = null
+ path = null
+ permissions_boundary = null
+ }
+ }
+
+ lambda = {
+ runtime = null
+ architecture = null
+ subnet_ids = null
+ security_group_ids = null
+ tags = {}
+ role = {
+ path = null
+ permissions_boundary = null
+ }
+ }
+
+ orchestration_provider = {
+ webhook = {
+ runner = {
+ boot_time_in_minutes = v.runner_config.runner_boot_time_in_minutes
+ ephemeral = v.runner_config.enable_ephemeral_runners
+ jit_config_enabled = v.runner_config.enable_jit_config
+ maximum_count = v.runner_config.runners_maximum_count
+ }
+
+ github = {
+ organization_runners = v.runner_config.enable_organization_runners
+ }
+
+ matcherConfig = v.matcherConfig
+
+ lambda = {
+ scale = {
+ up = {
+ memory_size = null
+ timeout = null
+ reserved_concurrent_executions = v.runner_config.scale_up_reserved_concurrent_executions
+ job_queued_check_enabled = v.runner_config.enable_job_queued_check
+ event_source_mapping = {
+ 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 = {}
+ }
+ down = {
+ memory_size = null
+ timeout = null
+ schedule_expression = v.runner_config.scale_down_schedule_expression
+ minimum_running_time_in_minutes = v.runner_config.minimum_running_time_in_minutes
+ idle_config = v.runner_config.idle_config
+ tags = {}
+ }
+ }
+ pool = {
+ memory_size = null
+ timeout = null
+ reserved_concurrent_executions = null
+ config = v.runner_config.pool_config
+ include_busy_runners = false
+ runner_owner = v.runner_config.pool_runner_owner
+ tags = {}
+ }
+ }
+
+ queue = {
+ delay_webhook_event = v.runner_config.delay_webhook_event
+ 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 = {}
+ }
+
+ job_retry = {
+ enabled = v.runner_config.job_retry.enable
+ 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 = {}
+ lambda = {
+ memory_size = v.runner_config.job_retry.lambda_memory_size
+ reserved_concurrent_executions = 1
+ timeout = v.runner_config.job_retry.lambda_timeout
+ }
+ }
+ }
+ }
+
+ ssm = {
+ paths = {
+ root = null
+ tokens = null
+ config = null
+ }
+ tags = {}
+ parameters = {
+ tags = {}
+ }
+ housekeeper = {
+ schedule_expression = null
+ state = null
+ tags = {}
+ lambda = {
+ artifact = {
+ zip = null
+ s3 = null
+ }
+ memory_size = null
+ timeout = null
+ }
+ config = {
+ tokenPath = null
+ minimumDaysOld = null
+ dryRun = null
+ }
+ }
+ }
+
+ observability = {
+ logs = {
+ level = null
+ retention_in_days = null
+ kms_key_id = null
+ class = null
+ tags = {}
+ }
+ tracing = {
+ mode = null
+ capture_http_requests = null
+ capture_error = null
+ }
+ metrics = {
+ enable = null
+ namespace = null
+ metric = {
+ enable_github_app_rate_limit = null
+ enable_job_retry = null
+ }
+ }
+ }
+
+ compute_provider = {
+ aws = {
+ ec2 = {
+ metadata_options = {
+ instance_metadata_tags = tostring(v.runner_config.runner_metadata_options["instance_metadata_tags"])
+ http_endpoint = tostring(v.runner_config.runner_metadata_options["http_endpoint"])
+ http_tokens = tostring(v.runner_config.runner_metadata_options["http_tokens"])
+ http_put_response_hop_limit = tonumber(v.runner_config.runner_metadata_options["http_put_response_hop_limit"])
+ }
+ 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
+ }
+ kms_key = v.runner_config.ami.kms_key_arn == null ? null : {
+ arn = v.runner_config.ami.kms_key_arn
+ }
+ }
+ block_device_mappings = v.runner_config.block_device_mappings
+ create_service_linked_role_spot = v.runner_config.create_service_linked_role_spot
+ credit_specification = v.runner_config.credit_specification
+ ebs_optimized = v.runner_config.ebs_optimized
+ cloudwatch_agent = {
+ enabled = v.runner_config.enable_cloudwatch_agent
+ config = v.runner_config.cloudwatch_config
+ }
+ binaries_syncer = {
+ enabled = v.runner_config.enable_runner_binaries_syncer
+ }
+ detailed_monitoring_enabled = v.runner_config.enable_runner_detailed_monitoring
+ ssm_enabled = v.runner_config.enable_ssm_on_runners
+ user_data = {
+ enabled = v.runner_config.enable_userdata
+ template = v.runner_config.userdata_template
+ content = v.runner_config.userdata_content
+ pre_install = v.runner_config.userdata_pre_install
+ post_install = v.runner_config.userdata_post_install
+ debug_logging_enabled = false
+ }
+ instance_allocation_strategy = v.runner_config.instance_allocation_strategy
+ instance_max_spot_price = v.runner_config.instance_max_spot_price
+ instance_target_capacity_type = v.runner_config.instance_target_capacity_type
+ instance_type_priorities = v.runner_config.instance_type_priorities
+ instance_types = v.runner_config.instance_types
+ additional_security_group_ids = length(v.runner_config.runner_additional_security_group_ids) == 0 ? null : v.runner_config.runner_additional_security_group_ids
+ managed_security_group_enabled = null
+ egress_rules = null
+ instance_profile_path = null
+ key_name = null
+ associate_public_ipv4_address = null
+ instance_profile = v.runner_config.iam_overrides.override_instance_profile == true ? {
+ name = v.runner_config.iam_overrides.instance_profile_name
+ } : null
+ enable_on_demand_failover_for_errors = v.runner_config.enable_on_demand_failover_for_errors
+ scale_errors = v.runner_config.scale_errors
+ subnet_ids = v.runner_config.subnet_ids
+ vpc_id = v.runner_config.vpc_id
+ cpu_options = v.runner_config.cpu_options
+ placement = v.runner_config.placement
+ license_specifications = v.runner_config.license_specifications
+ use_dedicated_host = v.runner_config.use_dedicated_host
+ log_files = v.runner_config.runner_log_files
+ tags = v.runner_config.runner_ec2_tags
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+locals {
+ # This staged local is intentionally unused until provider modules consume
+ # the normalized contract in the follow-up PR.
+ # tflint-ignore: terraform_unused_declarations
+ translated_experimental_base = merge(local.raw_translated_experimental, {
+ multi_runner_config = {
+ for k, v in local.raw_translated_experimental.multi_runner_config : k => merge(v, {
+ tags = merge(local.raw_translated_experimental.tags, v.tags)
+
+ runner = merge(v.runner, {
+ os = try(coalesce(v.runner.os, local.raw_translated_experimental.runner.os), null)
+ architecture = try(coalesce(v.runner.architecture, local.raw_translated_experimental.runner.architecture), null)
+ disable_default_labels = coalesce(v.runner.disable_default_labels, local.raw_translated_experimental.runner.disable_default_labels)
+ extra_labels = v.runner.extra_labels != null ? v.runner.extra_labels : local.raw_translated_experimental.runner.extra_labels
+ group_name = coalesce(v.runner.group_name, local.raw_translated_experimental.runner.group_name)
+ name_prefix = v.runner.name_prefix != null ? v.runner.name_prefix : local.raw_translated_experimental.runner.name_prefix
+ run_as_root = coalesce(v.runner.run_as_root, local.raw_translated_experimental.runner.run_as_root)
+ run_as = coalesce(v.runner.run_as, local.raw_translated_experimental.runner.run_as)
+ auto_update_disabled = coalesce(v.runner.auto_update_disabled, local.raw_translated_experimental.runner.auto_update_disabled)
+ tags = merge(local.raw_translated_experimental.runner.tags, v.runner.tags)
+ hooks = {
+ job_started = v.runner.hooks.job_started != null ? v.runner.hooks.job_started : local.raw_translated_experimental.runner.hooks.job_started
+ job_completed = v.runner.hooks.job_completed != null ? v.runner.hooks.job_completed : local.raw_translated_experimental.runner.hooks.job_completed
+ }
+ iam = {
+ role = try(coalesce(v.runner.iam.role, local.raw_translated_experimental.runner.iam.role), null)
+ managed_policy_arns = v.runner.iam.role != null ? (
+ v.runner.iam.managed_policy_arns != null ? v.runner.iam.managed_policy_arns : {}
+ ) : (
+ v.runner.iam.managed_policy_arns != null ? v.runner.iam.managed_policy_arns : local.raw_translated_experimental.runner.iam.managed_policy_arns
+ )
+ additional_trust_policy_json = v.runner.iam.role != null ? v.runner.iam.additional_trust_policy_json : try(coalesce(v.runner.iam.additional_trust_policy_json, local.raw_translated_experimental.runner.iam.additional_trust_policy_json), null)
+ path = try(coalesce(v.runner.iam.path, local.raw_translated_experimental.runner.iam.path, local.raw_translated_experimental.roles.path), null)
+ permissions_boundary = try(coalesce(v.runner.iam.permissions_boundary, local.raw_translated_experimental.runner.iam.permissions_boundary, local.raw_translated_experimental.roles.permissions_boundary), null)
+ }
+ })
+
+ lambda = merge(v.lambda, {
+ runtime = coalesce(v.lambda.runtime, local.raw_translated_experimental.lambda.runtime)
+ architecture = coalesce(v.lambda.architecture, local.raw_translated_experimental.lambda.architecture)
+ subnet_ids = v.lambda.subnet_ids != null ? v.lambda.subnet_ids : local.raw_translated_experimental.lambda.subnet_ids
+ security_group_ids = v.lambda.security_group_ids != null ? v.lambda.security_group_ids : local.raw_translated_experimental.lambda.security_group_ids
+ tags = merge(local.raw_translated_experimental.lambda.tags, v.lambda.tags)
+ role = {
+ path = try(coalesce(
+ v.lambda.role.path,
+ local.raw_translated_experimental.lambda.role.path,
+ local.raw_translated_experimental.roles.path,
+ ), null)
+ permissions_boundary = try(coalesce(
+ v.lambda.role.permissions_boundary,
+ local.raw_translated_experimental.lambda.role.permissions_boundary,
+ local.raw_translated_experimental.roles.permissions_boundary,
+ ), null)
+ }
+ })
+
+ orchestration_provider = {
+ webhook = v.orchestration_provider.webhook == null ? null : merge(v.orchestration_provider.webhook, {
+ runner = {
+ boot_time_in_minutes = coalesce(
+ v.orchestration_provider.webhook.runner.boot_time_in_minutes,
+ local.raw_translated_experimental.orchestration_provider.webhook.runner.boot_time_in_minutes,
+ )
+ ephemeral = coalesce(
+ v.orchestration_provider.webhook.runner.ephemeral,
+ local.raw_translated_experimental.orchestration_provider.webhook.runner.ephemeral,
+ )
+ jit_config_enabled = try(coalesce(
+ v.orchestration_provider.webhook.runner.jit_config_enabled,
+ local.raw_translated_experimental.orchestration_provider.webhook.runner.jit_config_enabled,
+ ), null)
+ maximum_count = try(coalesce(
+ v.orchestration_provider.webhook.runner.maximum_count,
+ local.raw_translated_experimental.orchestration_provider.webhook.runner.maximum_count,
+ ), null)
+ }
+
+ lambda = merge(v.orchestration_provider.webhook.lambda, {
+ scale = merge(v.orchestration_provider.webhook.lambda.scale, {
+ up = merge(v.orchestration_provider.webhook.lambda.scale.up, {
+ memory_size = coalesce(v.orchestration_provider.webhook.lambda.scale.up.memory_size, local.raw_translated_experimental.orchestration_provider.webhook.lambda.scale.up.memory_size)
+ timeout = coalesce(v.orchestration_provider.webhook.lambda.scale.up.timeout, local.raw_translated_experimental.orchestration_provider.webhook.lambda.scale.up.timeout)
+ reserved_concurrent_executions = coalesce(v.orchestration_provider.webhook.lambda.scale.up.reserved_concurrent_executions, local.raw_translated_experimental.orchestration_provider.webhook.lambda.scale.up.reserved_concurrent_executions)
+ job_queued_check_enabled = try(coalesce(v.orchestration_provider.webhook.lambda.scale.up.job_queued_check_enabled, local.raw_translated_experimental.orchestration_provider.webhook.lambda.scale.up.job_queued_check_enabled), null)
+ event_source_mapping = {
+ batch_size = coalesce(
+ v.orchestration_provider.webhook.lambda.scale.up.event_source_mapping.batch_size,
+ local.raw_translated_experimental.orchestration_provider.webhook.lambda.scale.up.event_source_mapping.batch_size,
+ )
+ maximum_batching_window_in_seconds = coalesce(
+ v.orchestration_provider.webhook.lambda.scale.up.event_source_mapping.maximum_batching_window_in_seconds,
+ local.raw_translated_experimental.orchestration_provider.webhook.lambda.scale.up.event_source_mapping.maximum_batching_window_in_seconds,
+ )
+ }
+ tags = merge(local.raw_translated_experimental.orchestration_provider.webhook.lambda.scale.up.tags, v.orchestration_provider.webhook.lambda.scale.up.tags)
+ })
+ down = merge(v.orchestration_provider.webhook.lambda.scale.down, {
+ memory_size = coalesce(v.orchestration_provider.webhook.lambda.scale.down.memory_size, local.raw_translated_experimental.orchestration_provider.webhook.lambda.scale.down.memory_size)
+ timeout = coalesce(v.orchestration_provider.webhook.lambda.scale.down.timeout, local.raw_translated_experimental.orchestration_provider.webhook.lambda.scale.down.timeout)
+ schedule_expression = coalesce(v.orchestration_provider.webhook.lambda.scale.down.schedule_expression, local.raw_translated_experimental.orchestration_provider.webhook.lambda.scale.down.schedule_expression)
+ minimum_running_time_in_minutes = try(coalesce(v.orchestration_provider.webhook.lambda.scale.down.minimum_running_time_in_minutes, local.raw_translated_experimental.orchestration_provider.webhook.lambda.scale.down.minimum_running_time_in_minutes), null)
+ idle_config = v.orchestration_provider.webhook.lambda.scale.down.idle_config != null ? v.orchestration_provider.webhook.lambda.scale.down.idle_config : local.raw_translated_experimental.orchestration_provider.webhook.lambda.scale.down.idle_config
+ tags = merge(local.raw_translated_experimental.orchestration_provider.webhook.lambda.scale.down.tags, v.orchestration_provider.webhook.lambda.scale.down.tags)
+ })
+ })
+ pool = merge(v.orchestration_provider.webhook.lambda.pool, {
+ memory_size = coalesce(v.orchestration_provider.webhook.lambda.pool.memory_size, local.raw_translated_experimental.orchestration_provider.webhook.lambda.pool.memory_size)
+ timeout = coalesce(v.orchestration_provider.webhook.lambda.pool.timeout, local.raw_translated_experimental.orchestration_provider.webhook.lambda.pool.timeout)
+ reserved_concurrent_executions = coalesce(v.orchestration_provider.webhook.lambda.pool.reserved_concurrent_executions, local.raw_translated_experimental.orchestration_provider.webhook.lambda.pool.reserved_concurrent_executions)
+ config = v.orchestration_provider.webhook.lambda.pool.config != null ? v.orchestration_provider.webhook.lambda.pool.config : local.raw_translated_experimental.orchestration_provider.webhook.lambda.pool.config
+ include_busy_runners = coalesce(v.orchestration_provider.webhook.lambda.pool.include_busy_runners, local.raw_translated_experimental.orchestration_provider.webhook.lambda.pool.include_busy_runners)
+ runner_owner = try(coalesce(v.orchestration_provider.webhook.lambda.pool.runner_owner, local.raw_translated_experimental.orchestration_provider.webhook.lambda.pool.runner_owner), null)
+ tags = merge(local.raw_translated_experimental.orchestration_provider.webhook.lambda.pool.tags, v.orchestration_provider.webhook.lambda.pool.tags)
+ })
+ })
+
+ queue = merge(v.orchestration_provider.webhook.queue, {
+ delay_webhook_event = coalesce(v.orchestration_provider.webhook.queue.delay_webhook_event, local.raw_translated_experimental.orchestration_provider.webhook.queue.delay_webhook_event)
+ job_queue_retention_in_seconds = coalesce(v.orchestration_provider.webhook.queue.job_queue_retention_in_seconds, local.raw_translated_experimental.orchestration_provider.webhook.queue.job_queue_retention_in_seconds)
+ visibility_timeout_seconds = coalesce(v.orchestration_provider.webhook.queue.visibility_timeout_seconds, local.raw_translated_experimental.orchestration_provider.webhook.queue.visibility_timeout_seconds)
+ redrive_build_queue = {
+ enabled = try(
+ coalesce(try(v.orchestration_provider.webhook.queue.redrive_build_queue.enabled, null), local.raw_translated_experimental.orchestration_provider.webhook.queue.redrive_build_queue.enabled),
+ local.raw_translated_experimental.orchestration_provider.webhook.queue.redrive_build_queue.enabled,
+ )
+ maxReceiveCount = try(
+ coalesce(try(v.orchestration_provider.webhook.queue.redrive_build_queue.maxReceiveCount, null), local.raw_translated_experimental.orchestration_provider.webhook.queue.redrive_build_queue.maxReceiveCount),
+ null,
+ )
+ }
+ tags = merge(local.raw_translated_experimental.orchestration_provider.webhook.queue.tags, v.orchestration_provider.webhook.queue.tags)
+ })
+ })
+ }
+
+ ssm = merge(v.ssm, {
+ paths = {
+ root = "${trimsuffix(coalesce(
+ v.ssm.paths.root,
+ local.raw_translated_experimental.ssm.paths.root,
+ "/github-action-runners/${var.prefix}",
+ ), "/")}/${k}"
+ tokens = coalesce(v.ssm.paths.tokens, local.raw_translated_experimental.ssm.paths.tokens)
+ config = coalesce(v.ssm.paths.config, local.raw_translated_experimental.ssm.paths.config)
+ }
+ tags = merge(local.raw_translated_experimental.ssm.tags, v.ssm.tags)
+ parameters = {
+ tags = merge(local.raw_translated_experimental.ssm.parameters.tags, v.ssm.parameters.tags)
+ }
+ housekeeper = {
+ schedule_expression = coalesce(v.ssm.housekeeper.schedule_expression, local.raw_translated_experimental.ssm.housekeeper.schedule_expression)
+ state = coalesce(v.ssm.housekeeper.state, local.raw_translated_experimental.ssm.housekeeper.state)
+ tags = merge(local.raw_translated_experimental.ssm.housekeeper.tags, v.ssm.housekeeper.tags)
+ lambda = {
+ artifact = {
+ zip = v.ssm.housekeeper.lambda.artifact.s3 != null ? null : try(coalesce(
+ v.ssm.housekeeper.lambda.artifact.zip,
+ local.raw_translated_experimental.ssm.housekeeper.lambda.artifact.zip,
+ ), null)
+ s3 = v.ssm.housekeeper.lambda.artifact.s3 != null ? v.ssm.housekeeper.lambda.artifact.s3 : (
+ v.ssm.housekeeper.lambda.artifact.zip != null ? null : local.raw_translated_experimental.ssm.housekeeper.lambda.artifact.s3
+ )
+ }
+ memory_size = coalesce(v.ssm.housekeeper.lambda.memory_size, local.raw_translated_experimental.ssm.housekeeper.lambda.memory_size)
+ timeout = coalesce(v.ssm.housekeeper.lambda.timeout, local.raw_translated_experimental.ssm.housekeeper.lambda.timeout)
+ }
+ config = {
+ tokenPath = try(coalesce(
+ v.ssm.housekeeper.config.tokenPath,
+ local.raw_translated_experimental.ssm.housekeeper.config.tokenPath,
+ ), null)
+ minimumDaysOld = coalesce(v.ssm.housekeeper.config.minimumDaysOld, local.raw_translated_experimental.ssm.housekeeper.config.minimumDaysOld)
+ dryRun = coalesce(v.ssm.housekeeper.config.dryRun, local.raw_translated_experimental.ssm.housekeeper.config.dryRun)
+ }
+ }
+ })
+
+ observability = {
+ logs = {
+ level = coalesce(v.observability.logs.level, local.raw_translated_experimental.observability.logs.level)
+ retention_in_days = coalesce(v.observability.logs.retention_in_days, local.raw_translated_experimental.observability.logs.retention_in_days)
+ kms_key_id = try(coalesce(v.observability.logs.kms_key_id, local.raw_translated_experimental.observability.logs.kms_key_id), null)
+ class = coalesce(v.observability.logs.class, local.raw_translated_experimental.observability.logs.class)
+ tags = merge(local.raw_translated_experimental.observability.logs.tags, v.observability.logs.tags)
+ }
+ tracing = {
+ mode = try(coalesce(
+ v.observability.tracing.mode,
+ local.raw_translated_experimental.observability.tracing.mode,
+ ), null)
+ capture_http_requests = coalesce(v.observability.tracing.capture_http_requests, local.raw_translated_experimental.observability.tracing.capture_http_requests)
+ capture_error = coalesce(v.observability.tracing.capture_error, local.raw_translated_experimental.observability.tracing.capture_error)
+ }
+ metrics = {
+ enable = coalesce(v.observability.metrics.enable, local.raw_translated_experimental.observability.metrics.enable)
+ namespace = coalesce(v.observability.metrics.namespace, local.raw_translated_experimental.observability.metrics.namespace)
+ metric = {
+ enable_github_app_rate_limit = coalesce(
+ v.observability.metrics.metric.enable_github_app_rate_limit,
+ local.raw_translated_experimental.observability.metrics.metric.enable_github_app_rate_limit,
+ )
+ enable_job_retry = coalesce(
+ v.observability.metrics.metric.enable_job_retry,
+ local.raw_translated_experimental.observability.metrics.metric.enable_job_retry,
+ )
+ }
+ }
+ }
+
+ compute_provider = {
+ aws = {
+ ec2 = v.compute_provider.aws.ec2 == null ? null : merge(v.compute_provider.aws.ec2, {
+ vpc_id = try(coalesce(v.compute_provider.aws.ec2.vpc_id, local.raw_translated_experimental.compute_provider.aws.ec2.vpc_id), null)
+ subnet_ids = v.compute_provider.aws.ec2.subnet_ids != null ? v.compute_provider.aws.ec2.subnet_ids : local.raw_translated_experimental.compute_provider.aws.ec2.subnet_ids
+ managed_security_group_enabled = coalesce(v.compute_provider.aws.ec2.managed_security_group_enabled, local.raw_translated_experimental.compute_provider.aws.ec2.managed_security_group_enabled)
+ egress_rules = v.compute_provider.aws.ec2.egress_rules != null ? v.compute_provider.aws.ec2.egress_rules : local.raw_translated_experimental.compute_provider.aws.ec2.egress_rules
+ additional_security_group_ids = v.compute_provider.aws.ec2.additional_security_group_ids != null ? v.compute_provider.aws.ec2.additional_security_group_ids : local.raw_translated_experimental.compute_provider.aws.ec2.additional_security_group_ids
+ instance_profile_path = try(coalesce(v.compute_provider.aws.ec2.instance_profile_path, local.raw_translated_experimental.compute_provider.aws.ec2.instance_profile_path), null)
+ key_name = try(coalesce(v.compute_provider.aws.ec2.key_name, local.raw_translated_experimental.compute_provider.aws.ec2.key_name), null)
+ associate_public_ipv4_address = coalesce(v.compute_provider.aws.ec2.associate_public_ipv4_address, local.raw_translated_experimental.compute_provider.aws.ec2.associate_public_ipv4_address)
+ cloudwatch_agent = merge(v.compute_provider.aws.ec2.cloudwatch_agent, {
+ config = try(coalesce(v.compute_provider.aws.ec2.cloudwatch_agent.config, local.raw_translated_experimental.compute_provider.aws.ec2.cloudwatch_agent.config), null)
+ })
+ binaries_syncer = {
+ enabled = coalesce(v.compute_provider.aws.ec2.binaries_syncer.enabled, local.raw_translated_experimental.compute_provider.aws.ec2.runner_binaries.enabled)
+ }
+ tags = merge(local.raw_translated_experimental.compute_provider.aws.ec2.tags, v.compute_provider.aws.ec2.tags)
+ })
+ }
+ }
+ })
+ }
+ })
+}
diff --git a/modules/multi-runner/tests/config-experimental-translation.tftest.hcl b/modules/multi-runner/tests/config-experimental-translation.tftest.hcl
new file mode 100644
index 0000000000..07629b3e84
--- /dev/null
+++ b/modules/multi-runner/tests/config-experimental-translation.tftest.hcl
@@ -0,0 +1,395 @@
+mock_provider "aws" {
+ mock_data "aws_caller_identity" {
+ defaults = {
+ account_id = "123456789012"
+ }
+ }
+
+ mock_data "aws_iam_policy_document" {
+ defaults = {
+ json = "{\"Version\":\"2012-10-17\",\"Statement\":[]}"
+ }
+ }
+
+ mock_resource "aws_iam_role" {
+ defaults = {
+ arn = "arn:aws:iam::123456789012:role/test-role"
+ }
+ }
+
+ mock_resource "aws_cloudwatch_event_bus" {
+ defaults = {
+ arn = "arn:aws:events:eu-west-1:123456789012:event-bus/test"
+ }
+ }
+
+ mock_resource "aws_cloudwatch_event_rule" {
+ defaults = {
+ arn = "arn:aws:events:eu-west-1:123456789012:rule/test"
+ }
+ }
+
+ mock_resource "aws_lambda_function" {
+ defaults = {
+ arn = "arn:aws:lambda:eu-west-1:123456789012:function:test"
+ }
+ }
+
+ mock_resource "aws_sqs_queue" {
+ defaults = {
+ arn = "arn:aws:sqs:eu-west-1:123456789012:test"
+ }
+ }
+
+ mock_resource "aws_s3_bucket" {
+ defaults = {
+ arn = "arn:aws:s3:::test-runner-binaries"
+ id = "test-runner-binaries"
+ }
+ }
+
+ mock_resource "aws_apigatewayv2_api" {
+ defaults = {
+ execution_arn = "arn:aws:execute-api:eu-west-1:123456789012:test"
+ }
+ }
+}
+
+mock_provider "random" {}
+mock_provider "null" {}
+
+variables {
+ aws_region = "eu-west-1"
+ vpc_id = "vpc-stable"
+ subnet_ids = ["subnet-stable"]
+
+ github_app = {
+ key_base64_ssm = {
+ arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/tests/github-app/key"
+ name = "/tests/github-app/key"
+ }
+ id_ssm = {
+ arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/tests/github-app/id"
+ name = "/tests/github-app/id"
+ }
+ webhook_secret_ssm = {
+ arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/tests/github-app/webhook-secret"
+ name = "/tests/github-app/webhook-secret"
+ }
+ }
+
+ multi_runner_config = {}
+
+ lambda_s3_bucket = "test-lambda-artifacts"
+ runners_lambda_zip = "README.md"
+ runners_lambda_s3_key = "runners.zip"
+ webhook_lambda_s3_key = "webhook.zip"
+ syncer_lambda_s3_key = "runner-binaries-syncer.zip"
+}
+
+run "empty_experimental_map_translates_stable_inputs" {
+ command = plan
+
+ variables {
+ tags = {
+ source = "stable"
+ }
+
+ role_path = "/stable/"
+ role_permissions_boundary = "arn:aws:iam::123456789012:policy/stable-boundary"
+ queue_selection_strategy = "random"
+ repository_white_list = ["example/repository"]
+
+ experimental = {
+ tags = {
+ source = "experimental-ignored"
+ }
+ roles = {
+ path = "/experimental-ignored/"
+ }
+ github = {
+ user_agent = "experimental-ignored"
+ }
+ lambda = {
+ runtime = "nodejs22.x"
+ }
+ orchestration_provider = {
+ webhook = {
+ queue_selection_strategy = "first"
+ github = {
+ repository_white_list = ["ignored/repository"]
+ }
+ }
+ }
+ compute_provider = {
+ aws = {
+ ec2 = {
+ vpc_id = "vpc-experimental-ignored"
+ subnet_ids = ["subnet-experimental-ignored"]
+ }
+ }
+ }
+ multi_runner_config = {}
+ }
+
+ multi_runner_config = {
+ stable = {
+ runner_config = {
+ runner_os = "linux"
+ runner_architecture = "x64"
+ instance_types = ["m5.large"]
+ runners_maximum_count = 2
+ runner_group_name = "stable-group"
+ runner_iam_role_managed_policy_arns = [
+ "arn:aws:iam::123456789012:policy/stable-runner",
+ ]
+ }
+ matcherConfig = {
+ labelMatchers = [["self-hosted", "linux", "x64"]]
+ }
+ }
+ }
+ }
+
+ assert {
+ condition = (
+ !local.use_multi_runner_config_v2
+ && toset(keys(local.raw_translated_experimental.multi_runner_config)) == toset(["stable"])
+ && tomap(local.raw_translated_experimental.tags) == tomap(var.tags)
+ && local.raw_translated_experimental.roles.path == var.role_path
+ && local.raw_translated_experimental.roles.permissions_boundary == var.role_permissions_boundary
+ && local.raw_translated_experimental.github.app.key_base64_ssm == var.github_app.key_base64_ssm
+ && local.raw_translated_experimental.github.app.id_ssm == var.github_app.id_ssm
+ && local.raw_translated_experimental.github.app.webhook_secret_ssm == var.github_app.webhook_secret_ssm
+ && local.raw_translated_experimental.github.user_agent == var.user_agent
+ && local.raw_translated_experimental.lambda.runtime == var.lambda_runtime
+ && local.raw_translated_experimental.orchestration_provider.webhook.queue_selection_strategy == var.queue_selection_strategy
+ && tolist(local.raw_translated_experimental.orchestration_provider.webhook.github.repository_white_list) == tolist(var.repository_white_list)
+ && local.raw_translated_experimental.compute_provider.aws.ec2.vpc_id == var.vpc_id
+ && tolist(local.raw_translated_experimental.compute_provider.aws.ec2.subnet_ids) == tolist(var.subnet_ids)
+ )
+ error_message = "An empty experimental runner map must translate the stable global inputs into the canonical shape."
+ }
+
+ assert {
+ condition = (
+ local.raw_translated_experimental.multi_runner_config["stable"].runner.os == "linux"
+ && local.raw_translated_experimental.multi_runner_config["stable"].runner.architecture == "x64"
+ && local.raw_translated_experimental.multi_runner_config["stable"].runner.group_name == "stable-group"
+ && local.raw_translated_experimental.multi_runner_config["stable"].runner.iam.managed_policy_arns["legacy-0"] == "arn:aws:iam::123456789012:policy/stable-runner"
+ && local.raw_translated_experimental.multi_runner_config["stable"].orchestration_provider.webhook.runner.maximum_count == 2
+ && jsonencode(local.raw_translated_experimental.multi_runner_config["stable"].orchestration_provider.webhook.matcherConfig.labelMatchers) == jsonencode([["self-hosted", "linux", "x64"]])
+ && toset(local.raw_translated_experimental.multi_runner_config["stable"].compute_provider.aws.ec2.instance_types) == toset(["m5.large"])
+ )
+ error_message = "Stable runner entries must translate into the canonical runner, orchestration, and compute-provider blocks."
+ }
+}
+
+run "non_empty_experimental_map_is_authoritative" {
+ command = plan
+
+ variables {
+ tags = {
+ source = "stable-ignored"
+ }
+
+ multi_runner_config = {
+ stable = {
+ runner_config = {
+ runner_os = "linux"
+ runner_architecture = "x64"
+ instance_types = ["m5.large"]
+ runners_maximum_count = 1
+ }
+ matcherConfig = {
+ labelMatchers = [["stable"]]
+ }
+ }
+ }
+
+ experimental = {
+ tags = {
+ source = "experimental"
+ }
+
+ multi_runner_config = {
+ experimental = {
+ orchestration_provider = {
+ webhook = {
+ matcherConfig = {
+ labelMatchers = [["experimental"]]
+ }
+ }
+ }
+ compute_provider = {
+ aws = {
+ ec2 = {
+ instance_types = ["c7g.large"]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ assert {
+ condition = (
+ local.use_multi_runner_config_v2
+ && toset(keys(local.raw_translated_experimental.multi_runner_config)) == toset(["experimental"])
+ && local.raw_translated_experimental.tags.source == "experimental"
+ && toset(local.raw_translated_experimental.multi_runner_config["experimental"].compute_provider.aws.ec2.instance_types) == toset(["c7g.large"])
+ && flatten(local.raw_translated_experimental.multi_runner_config["experimental"].orchestration_provider.webhook.matcherConfig.labelMatchers) == ["experimental"]
+ )
+ error_message = "A non-empty experimental runner map must be authoritative and must not merge stable lanes or flat defaults."
+ }
+
+ assert {
+ condition = jsonencode(local.raw_translated_experimental) == jsonencode(var.experimental)
+ error_message = "A non-empty experimental runner map must select the experimental object without leaking stable flat inputs."
+ }
+
+ assert {
+ condition = (
+ keys(module.runners) == ["stable"]
+ && !contains(keys(module.runners), "experimental")
+ )
+ error_message = "This preparatory change must leave resource creation on the stable multi_runner_config path."
+ }
+}
+
+run "lane_values_override_experimental_globals" {
+ command = plan
+
+ variables {
+ experimental = {
+ tags = {
+ scope = "global"
+ precedence = "global"
+ }
+
+ runner = {
+ os = "linux"
+ architecture = "x64"
+ group_name = "global-group"
+ iam = {
+ managed_policy_arns = {
+ global = "arn:aws:iam::123456789012:policy/global-runner"
+ }
+ additional_trust_policy_json = "{}"
+ }
+ }
+
+ orchestration_provider = {
+ webhook = {
+ runner = {
+ maximum_count = 4
+ }
+ }
+ }
+
+ observability = {
+ logs = {
+ level = "debug"
+ retention_in_days = 30
+ }
+ }
+
+ compute_provider = {
+ aws = {
+ ec2 = {
+ vpc_id = "vpc-experimental"
+ subnet_ids = ["subnet-global"]
+ tags = {
+ precedence = "global"
+ global = "true"
+ }
+ }
+ }
+ }
+
+ multi_runner_config = {
+ lane = {
+ tags = {
+ precedence = "lane"
+ lane = "true"
+ }
+ runner = {
+ group_name = "lane-group"
+ iam = {
+ role = {
+ arn = "arn:aws:iam::123456789012:role/external-runner"
+ }
+ }
+ }
+ orchestration_provider = {
+ webhook = {
+ runner = {
+ maximum_count = 7
+ }
+ matcherConfig = {
+ labelMatchers = [["self-hosted", "linux", "x64", "lane"]]
+ }
+ }
+ }
+ observability = {
+ logs = {
+ level = "warn"
+ }
+ }
+ compute_provider = {
+ aws = {
+ ec2 = {
+ instance_types = ["m7i.large"]
+ subnet_ids = ["subnet-lane"]
+ tags = {
+ precedence = "lane"
+ provider = "lane"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ assert {
+ condition = (
+ local.translated_experimental_base.multi_runner_config["lane"].runner.os == "linux"
+ && local.translated_experimental_base.multi_runner_config["lane"].runner.architecture == "x64"
+ && local.translated_experimental_base.multi_runner_config["lane"].runner.group_name == "lane-group"
+ && local.translated_experimental_base.multi_runner_config["lane"].orchestration_provider.webhook.runner.maximum_count == 7
+ && local.translated_experimental_base.multi_runner_config["lane"].observability.logs.level == "warn"
+ && local.translated_experimental_base.multi_runner_config["lane"].observability.logs.retention_in_days == 30
+ )
+ error_message = "Lane values must override experimental globals while omitted values inherit their global defaults."
+ }
+
+ assert {
+ condition = (
+ tomap(local.translated_experimental_base.multi_runner_config["lane"].tags) == tomap({
+ scope = "global"
+ precedence = "lane"
+ lane = "true"
+ })
+ && local.translated_experimental_base.multi_runner_config["lane"].compute_provider.aws.ec2.vpc_id == "vpc-experimental"
+ && toset(local.translated_experimental_base.multi_runner_config["lane"].compute_provider.aws.ec2.subnet_ids) == toset(["subnet-lane"])
+ && tomap(local.translated_experimental_base.multi_runner_config["lane"].compute_provider.aws.ec2.tags) == tomap({
+ precedence = "lane"
+ global = "true"
+ provider = "lane"
+ })
+ )
+ error_message = "Tags and EC2 defaults must merge from experimental globals with lane values taking precedence."
+ }
+
+ assert {
+ condition = (
+ local.translated_experimental_base.multi_runner_config["lane"].runner.iam.role.arn == "arn:aws:iam::123456789012:role/external-runner"
+ && length(local.translated_experimental_base.multi_runner_config["lane"].runner.iam.managed_policy_arns) == 0
+ && local.translated_experimental_base.multi_runner_config["lane"].runner.iam.additional_trust_policy_json == null
+ )
+ error_message = "An externally managed runner role must suppress inherited managed policies and trust-policy additions."
+ }
+}
diff --git a/modules/multi-runner/variables.experimental.tf b/modules/multi-runner/variables.experimental.tf
new file mode 100644
index 0000000000..9b3d13a39b
--- /dev/null
+++ b/modules/multi-runner/variables.experimental.tf
@@ -0,0 +1,723 @@
+variable "experimental" {
+ description = <<-EOT
+ Experimental provider-oriented configuration contract. This schema can change before it becomes stable.
+
+ This input is accepted and normalized as groundwork for the provider split. It is intentionally not passed to `modules/runner-config` in this change and does not create or modify resources. Continue using the stable top-level `multi_runner_config` for deployed runners until the provider modules are connected. Existing required stable inputs remain required in this preparatory change.
+
+ A non-empty `experimental.multi_runner_config` selects the experimental input for normalization; an empty map translates the stable inputs into the same canonical shape. Experimental values resolve with `runner configuration override > experimental global default > schema default` precedence. Stable flat inputs populate the canonical shape only when the experimental runner map is empty. Provider selection remains lane-scoped so a later implementation can support mixed provider maps.
+
+ The typed nested blocks define the intended runner, GitHub, Lambda, webhook orchestration, SSM, observability, and compute-provider contract. Their resource effects and cross-field provider validations become active only when the runner-config, orchestration-provider, and compute-provider modules are connected in a follow-up change.
+ EOT
+ type = object({
+ tags = optional(map(string), {})
+
+ roles = optional(object({
+ path = optional(string, null)
+ permissions_boundary = optional(string, null)
+ }), {})
+
+ runner = optional(object({
+ os = optional(string, null)
+ architecture = optional(string, null)
+ disable_default_labels = optional(bool, false)
+ extra_labels = optional(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)
+ }), {})
+ }), {})
+
+ github = optional(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
+ }))
+ 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)
+ }), {})
+ user_agent = optional(string, "github-aws-runners")
+ }), {})
+
+ lambda = optional(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)
+ }), {})
+ }), {})
+
+ orchestration_provider = optional(object({
+ webhook = optional(object({
+ queue_selection_strategy = optional(string, "first")
+ eventbridge = optional(object({
+ enable = 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_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
+ })
+ }), {})
+ }), {})
+ }), {})
+
+ 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)
+ }), {})
+ }), {})
+ }), {})
+
+ observability = optional(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({
+ 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 = optional(bool, true)
+ enable_spot_termination_warning = optional(bool, true)
+ }), {})
+ }), {})
+ }), {})
+
+ compute_provider = optional(object({
+ selections = optional(map(object({
+ namespace = string
+ type = string
+ })), null)
+ aws = optional(object({
+ ec2 = optional(object({
+ vpc_id = optional(string, null)
+ subnet_ids = optional(list(string), null)
+ managed_security_group_enabled = optional(bool, true)
+ 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
+ }])
+ additional_security_group_ids = optional(list(string), [])
+ cloudwatch_agent = optional(object({
+ config = optional(string, null)
+ }), {})
+ instance_profile_path = optional(string, null)
+ key_name = optional(string, null)
+ associate_public_ipv4_address = optional(bool, false)
+ tags = optional(map(string), {})
+ ami = optional(object({
+ housekeeper = optional(object({
+ enabled = optional(bool, false)
+ cleanup_config = optional(object({
+ maxItems = optional(number)
+ minimumDaysOld = optional(number)
+ amiFilters = optional(list(object({
+ Name = string
+ Values = list(string)
+ })))
+ launchTemplateNames = optional(list(string))
+ ssmParameterNames = optional(list(string))
+ dryRun = optional(bool)
+ }), {})
+ artifact = optional(object({
+ zip = optional(string, null)
+ s3 = optional(object({
+ key = string
+ object_version = optional(string, null)
+ }), null)
+ }), {})
+ lambda = optional(object({
+ memory_size = optional(number, 256)
+ timeout = optional(number, 300)
+ }), {})
+ schedule = optional(object({
+ expression = optional(string, "cron(11 7 * * ? *)")
+ }), {})
+ }), {})
+ }), {})
+ instance_termination_watcher = optional(object({
+ enabled = 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), {})
+ artifact = optional(object({
+ zip = optional(string, null)
+ s3 = optional(object({
+ key = string
+ object_version = optional(string, null)
+ }), null)
+ }), {})
+ lambda = optional(object({
+ memory_size = optional(number, null)
+ timeout = optional(number, null)
+ }), {})
+ }), {})
+ runner_binaries = optional(object({
+ enabled = optional(bool, true)
+ targets = optional(map(object({
+ os = string
+ architecture = string
+ })), null)
+ s3 = optional(object({
+ encryption = optional(object({
+ enabled = optional(bool, true)
+ bucket_key_enabled = optional(bool, null)
+ sse_algorithm = optional(string, "AES256")
+ kms_master_key_id = optional(string, null)
+ }), {})
+ tags = optional(map(string), {})
+ versioning = optional(string, "Disabled")
+ logging = optional(object({
+ bucket = optional(string, null)
+ prefix = optional(string, null)
+ }), {})
+ }), {})
+ syncer = optional(object({
+ artifact = optional(object({
+ zip = optional(string, null)
+ s3 = optional(object({
+ key = string
+ object_version = optional(string, null)
+ }), null)
+ }), {})
+ lambda = optional(object({
+ memory_size = optional(number, 256)
+ timeout = optional(number, 300)
+ }), {})
+ schedule = optional(object({
+ expression = optional(string, "cron(27 * * * ? *)")
+ state = optional(string, "ENABLED")
+ }), {})
+ }), {})
+ }), {})
+ }), {})
+ }), {})
+ }), {})
+
+ multi_runner_config = optional(map(object({
+ 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 = 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 = object({
+ labelMatchers = list(list(string))
+ exactMatch = optional(bool, false)
+ bidirectionalLabelMatch = optional(bool, false)
+ priority = optional(number, 999)
+ enableDynamicLabels = optional(bool, false)
+ awsDynamicLabelsPolicy = optional(object({
+ blocked_keys = optional(list(string), [])
+ restricted_keys = optional(map(object({
+ allowed = optional(list(string), [])
+ denied = optional(list(string), [])
+ max = optional(string, 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_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)
+
+ })
+
+ ssm = optional(object({
+ 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({
+ enable = optional(bool, null)
+ namespace = optional(string, null)
+ metric = optional(object({
+ enable_github_app_rate_limit = optional(bool, null)
+ enable_job_retry = optional(bool, null)
+ }), {})
+ }), {})
+ }), {})
+
+ compute_provider = 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 = 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)
+ enable_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)
+ 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)
+ }), {})
+ })
+
+ })), {})
+ })
+ default = {}
+}
diff --git a/modules/multi-runner/variables.tf b/modules/multi-runner/variables.tf
index a47cd2a83c..8d3ae7fa8c 100644
--- a/modules/multi-runner/variables.tf
+++ b/modules/multi-runner/variables.tf
@@ -832,6 +832,8 @@ variable "user_agent" {
default = "github-aws-runners"
}
+# Retained for backward compatibility; EC2 runner modules use per-configuration runner_config.iam_overrides.
+# tflint-ignore: terraform_unused_declarations
variable "iam_overrides" {
description = "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."
type = object({