From 208de6c8bd81b21438b04e59e5ded30a6ca0fd2e Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Wed, 19 Aug 2026 22:04:54 +0200 Subject: [PATCH] feat(multi-runner): add experimental config translation --- .github/workflows/terraform.yml | 1 + modules/multi-runner/README.md | 1 + .../config.experimental.translation.tf | 774 ++++++++++++++++++ ...config-experimental-translation.tftest.hcl | 395 +++++++++ .../multi-runner/variables.experimental.tf | 723 ++++++++++++++++ modules/multi-runner/variables.tf | 2 + 6 files changed, 1896 insertions(+) create mode 100644 modules/multi-runner/config.experimental.translation.tf create mode 100644 modules/multi-runner/tests/config-experimental-translation.tftest.hcl create mode 100644 modules/multi-runner/variables.experimental.tf 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.

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. |
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.
You can optionally create the SSM parameters yourself and provide the ARN and name here, through the `*_ssm` attributes.
If you chose to provide the configuration values directly here,
please ensure the key is the base64-encoded `.pem` file (the output of `base64 app.private-key.pem`, not the content of `private-key.pem`).
Note: the provided SSM parameters arn and name have a precedence over the actual value (i.e `key_base64_ssm` has a precedence over `key_base64` etc). |
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({