From 8e2bc5be35f0a4470305ce626f2503c0548c4509 Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Thu, 3 Sep 2026 23:32:05 +0200 Subject: [PATCH 1/3] feat(multi-runner): integrate Lambda MicroVM provider --- modules/multi-runner/README.md | 2 + .../config.experimental.resolved.tf | 48 +++ .../config.experimental.translation.tf | 25 ++ ...variables.experimental.compute-provider.tf | 34 ++ .../variables.experimental.multi-runner.tf | 34 ++ modules/runner-config/README.md | 24 +- .../compute-provider.aws.microvm.tf | 27 ++ modules/runner-config/compute-provider.tf | 12 +- modules/runner-config/outputs.tf | 3 +- .../tests/computed-iam-inputs.tftest.hcl | 9 + .../fixtures/computed-iam-inputs/README.md | 3 + .../computed-iam-inputs.tf | 100 ++++++ modules/runner-config/tests/pool.tftest.hcl | 333 +++++++++++++++++- modules/runner-config/validations.tf | 20 +- .../variables.compute-provider.tf | 38 +- 15 files changed, 696 insertions(+), 16 deletions(-) create mode 100644 modules/runner-config/compute-provider.aws.microvm.tf diff --git a/modules/multi-runner/README.md b/modules/multi-runner/README.md index 007003094b..6c67603ea2 100644 --- a/modules/multi-runner/README.md +++ b/modules/multi-runner/README.md @@ -25,6 +25,8 @@ For each configuration: - When enabled, the [distribution syncer](https://github-aws-runners.github.io/terraform-aws-github-runner/modules/internal/runner-binaries-syncer/) is deployed for each unique combination of OS and architecture. - For each configuration a queue is created and [runner module](https://github-aws-runners.github.io/terraform-aws-github-runner/modules/internal/runners/) is deployed +Experimental runner configurations may select the Lambda MicroVM provider with `compute_provider.aws.microvm`. MicroVM lanes use Linux ARM64 ephemeral runners with JIT configuration and do not participate in EC2 runner-binary synchronization. + ## Matching Matching of the configuration is done based on the labels specified in labelMatchers configuration. The webhook is processing the `workflow_job` event and match the labels against the labels specified in labelMatchers configuration in the order of configuration with exact-match true first, followed by all exact matches false. diff --git a/modules/multi-runner/config.experimental.resolved.tf b/modules/multi-runner/config.experimental.resolved.tf index 0b55f271ca..5e0123ad47 100644 --- a/modules/multi-runner/config.experimental.resolved.tf +++ b/modules/multi-runner/config.experimental.resolved.tf @@ -467,6 +467,54 @@ locals { } tags = merge(local.normalized_config.compute_provider.aws.ec2.tags, v.compute_provider.aws.ec2.tags) }) + microvm = v.compute_provider.aws.microvm == null ? null : merge(v.compute_provider.aws.microvm, { + image_arn = try(coalesce(v.compute_provider.aws.microvm.image_arn, local.normalized_config.compute_provider.aws.microvm.image_arn), null) + image_version = try(coalesce(v.compute_provider.aws.microvm.image_version, local.normalized_config.compute_provider.aws.microvm.image_version), null) + ingress_network_connectors = v.compute_provider.aws.microvm.ingress_network_connectors != null ? ( + v.compute_provider.aws.microvm.ingress_network_connectors + ) : local.normalized_config.compute_provider.aws.microvm.ingress_network_connectors + egress_network_connectors = v.compute_provider.aws.microvm.egress_network_connectors != null ? ( + v.compute_provider.aws.microvm.egress_network_connectors + ) : local.normalized_config.compute_provider.aws.microvm.egress_network_connectors + cloudwatch_agent = { + enabled = coalesce( + v.compute_provider.aws.microvm.cloudwatch_agent.enabled, + local.normalized_config.compute_provider.aws.microvm.cloudwatch_agent.enabled, + ) + config = try(coalesce( + v.compute_provider.aws.microvm.cloudwatch_agent.config, + local.normalized_config.compute_provider.aws.microvm.cloudwatch_agent.config, + ), null) + } + log_files = v.compute_provider.aws.microvm.log_files != null ? ( + v.compute_provider.aws.microvm.log_files + ) : local.normalized_config.compute_provider.aws.microvm.log_files + environment_variables = merge( + local.normalized_config.compute_provider.aws.microvm.environment_variables, + v.compute_provider.aws.microvm.environment_variables, + ) + iam = { + resource_arns = { + images = v.compute_provider.aws.microvm.iam.resource_arns.images != null ? ( + v.compute_provider.aws.microvm.iam.resource_arns.images + ) : local.normalized_config.compute_provider.aws.microvm.iam.resource_arns.images + } + additional_policy_json = { + scale_up = try(coalesce( + v.compute_provider.aws.microvm.iam.additional_policy_json.scale_up, + local.normalized_config.compute_provider.aws.microvm.iam.additional_policy_json.scale_up, + ), null) + } + managed_policies = { + scale_up = v.compute_provider.aws.microvm.iam.managed_policies.scale_up != null ? ( + v.compute_provider.aws.microvm.iam.managed_policies.scale_up + ) : local.normalized_config.compute_provider.aws.microvm.iam.managed_policies.scale_up + pool = v.compute_provider.aws.microvm.iam.managed_policies.pool != null ? ( + v.compute_provider.aws.microvm.iam.managed_policies.pool + ) : local.normalized_config.compute_provider.aws.microvm.iam.managed_policies.pool + } + } + }) } } }) diff --git a/modules/multi-runner/config.experimental.translation.tf b/modules/multi-runner/config.experimental.translation.tf index bcdc14ae4c..f627a68022 100644 --- a/modules/multi-runner/config.experimental.translation.tf +++ b/modules/multi-runner/config.experimental.translation.tf @@ -301,6 +301,30 @@ locals { } } } + microvm = { + image_arn = null + image_version = null + ingress_network_connectors = [] + egress_network_connectors = [] + cloudwatch_agent = { + enabled = true + config = null + } + log_files = null + environment_variables = {} + iam = { + resource_arns = { + images = null + } + additional_policy_json = { + scale_up = null + } + managed_policies = { + scale_up = null + pool = null + } + } + } } } @@ -552,6 +576,7 @@ locals { log_files = v.runner_config.runner_log_files tags = v.runner_config.runner_ec2_tags } + microvm = null } } } diff --git a/modules/multi-runner/variables.experimental.compute-provider.tf b/modules/multi-runner/variables.experimental.compute-provider.tf index 9bd4ea570d..d35beeef27 100644 --- a/modules/multi-runner/variables.experimental.compute-provider.tf +++ b/modules/multi-runner/variables.experimental.compute-provider.tf @@ -131,6 +131,40 @@ variable "experimental_global_config_compute_provider" { }), {}) }), {}) }), {}) + microvm = optional(object({ + image_arn = optional(string, null) + image_version = optional(string, null) + ingress_network_connectors = optional(list(string), []) + egress_network_connectors = optional(list(string), []) + cloudwatch_agent = optional(object({ + enabled = optional(bool, true) + config = optional(string, null) + }), {}) + 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) + environment_variables = optional(map(string), {}) + iam = optional(object({ + resource_arns = optional(object({ + images = optional(list(string), null) + }), {}) + additional_policy_json = optional(object({ + scale_up = optional(string, null) + }), {}) + managed_policies = optional(object({ + scale_up = optional(object({ + arn = string + }), null) + pool = optional(object({ + arn = string + }), null) + }), {}) + }), {}) + }), {}) }), {}) }) default = {} diff --git a/modules/multi-runner/variables.experimental.multi-runner.tf b/modules/multi-runner/variables.experimental.multi-runner.tf index 7a7246bb73..f5cb5d8c31 100644 --- a/modules/multi-runner/variables.experimental.multi-runner.tf +++ b/modules/multi-runner/variables.experimental.multi-runner.tf @@ -325,6 +325,40 @@ variable "experimental_multi_runner_config" { })), null) tags = optional(map(string), {}) }), null) + microvm = optional(object({ + image_arn = optional(string, null) + image_version = optional(string, null) + ingress_network_connectors = optional(list(string), null) + egress_network_connectors = optional(list(string), null) + cloudwatch_agent = optional(object({ + enabled = optional(bool, null) + config = optional(string, null) + }), {}) + 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) + environment_variables = optional(map(string), {}) + iam = optional(object({ + resource_arns = optional(object({ + images = optional(list(string), null) + }), {}) + additional_policy_json = optional(object({ + scale_up = optional(string, null) + }), {}) + managed_policies = optional(object({ + scale_up = optional(object({ + arn = string + }), null) + pool = optional(object({ + arn = string + }), null) + }), {}) + }), {}) + }), null) }), {}) }) diff --git a/modules/runner-config/README.md b/modules/runner-config/README.md index c918a79fa0..e7dfd50d63 100644 --- a/modules/runner-config/README.md +++ b/modules/runner-config/README.md @@ -10,9 +10,13 @@ Runner demand orchestration is selected independently through `orchestration_pro Common `lambda` contains only shared execution substrate and the optional shared artifact bucket. The webhook provider owns the runner-control archive shared by scale, pool, and job-retry at `orchestration_provider.webhook.lambda.artifact` and combines its zip or S3 key/version with that common substrate. The common SSM housekeeper independently owns `ssm.housekeeper.lambda.artifact`: an S3 selection combines its component key/version with the common bucket, a local zip is used otherwise when configured, and the packaged runner control-plane archive is the final fallback. It never inherits the webhook runner-control archive. -Provider-owned settings remain nested under a typed namespace and provider leaf. For example, AMI, VPC, instance-profile, capacity, userdata, and runner-host logging settings live under `compute_provider.aws.ec2`. `multi-runner` resolves experimental globals and runner-configuration overrides first, then its final forwarding adapter preserves the wrapped `{ aws = { ec2 = ... } }` object expected by this module. Exactly one provider leaf must be non-null. The configuration module flattens the selected namespace and type to the Terraform dispatch key `aws_ec2`, while the webhook runtime registry continues to receive the provider type `ec2`. +Provider-owned settings remain nested under a typed namespace and provider leaf. EC2 settings live under `compute_provider.aws.ec2`, while Lambda MicroVM settings live under `compute_provider.aws.microvm`. `multi-runner` resolves experimental globals and runner-configuration overrides first, then preserves the wrapped `{ aws = { ec2 = ..., microvm = ... } }` object expected by this module. Exactly one provider leaf must be non-null. Runner-config flattens the selected namespace and type only for Terraform dispatch: `aws_ec2` maps to runtime type `ec2`, and `aws_microvm` maps to runtime type `microvm`. -The EC2 leaf reaches runner-config with `compute_provider.aws.ec2.binaries_syncer = { enabled, s3 }`; the S3 object is null when synchronization is disabled. Binary discovery and this shape adaptation happen in `multi-runner`, not inside runner-config. Before creating the common runner role, the configuration module calls [`compute-providers/aws/ec2/trust-policy`](../compute-providers/aws/ec2/trust-policy) as `module.compute_aws_ec2_trust_policy[0]` to combine its default trust with `runner.iam.additional_trust_policy_json`. The resulting assume-role policy does not depend on the full [`compute-providers/aws/ec2`](../compute-providers/aws/ec2) module, dispatched at `module.compute_aws_ec2[0]`, which receives the resolved runner role only after it is created. Declarative moved blocks preserve state from the earlier experimental `module.compute_ec2_trust_policy[0]` and `module.compute_ec2[0]` labels. EC2 owns the instance profile, launch template, EC2 bootstrap parameters, runner log groups, and its provider policies and Lambda environment variables. The common configuration module attaches each returned policy group to its runner or webhook-provider role. Provider-specific outputs remain grouped under the matching namespace and provider path, currently `provider.aws.ec2`. Moved blocks do not rewrite output references, so consumers of the former experimental `provider.ec2` path must update their expressions. EC2 is the only implemented Terraform compute provider in this phase. +The EC2 leaf reaches runner-config with `compute_provider.aws.ec2.binaries_syncer = { enabled, s3 }`; the S3 object is null when synchronization is disabled. Binary discovery and this shape adaptation happen in `multi-runner`, not inside runner-config. Runner-config calls [`compute-providers/aws/ec2/trust-policy`](../compute-providers/aws/ec2/trust-policy) as `module.compute_aws_ec2_trust_policy[0]`, then dispatches the full [`compute-providers/aws/ec2`](../compute-providers/aws/ec2) module at `module.compute_aws_ec2[0]`. Declarative moved blocks preserve state from the earlier experimental `module.compute_ec2_trust_policy[0]` and `module.compute_ec2[0]` labels. + +The MicroVM leaf similarly uses [`compute-providers/aws/microvm/trust-policy`](../compute-providers/aws/microvm/trust-policy) at `module.compute_aws_microvm_trust_policy[0]` and the full [`compute-providers/aws/microvm`](../compute-providers/aws/microvm) module at `module.compute_aws_microvm[0]`. It uses the resolved common `runner.iam.role` as the MicroVM execution role and exports the IAM and runtime-environment fragments required by the webhook control plane. MicroVM lanes require Linux on ARM64 and ephemeral webhook orchestration with JIT configuration enabled. MicroVM is introduced directly at its namespaced labels, so the EC2 moved blocks do not apply to it. + +The common configuration module attaches each selected provider's returned policy groups to its managed runner or webhook-provider roles. Provider-specific outputs remain grouped under the matching path: `provider.aws.ec2` or `provider.aws.microvm`. Moved blocks do not rewrite output references, so consumers of the former experimental `provider.ec2` path must update their expressions. ## Tagging @@ -22,19 +26,23 @@ Tags are merged from broadest to narrowest: module tags, shared resource tags, c Provider-specific runner tags remain inside the provider boundary. `compute_provider.aws.ec2.tags` applies to runtime EC2 instance, volume, network-interface, and spot-request tag specifications. `multi-runner` derives that map from global and runner-configuration `compute_provider.aws.ec2.tags` values. The EC2 provider applies the bootstrap tags `ghr:environment`, `ghr:ssm_config_path`, and `ghr:runner_name_prefix` last so they cannot be overridden; those tags are not added to common Lambda, IAM, queue, log-group, or SSM resources. -## Overview +## Provider behavior + +### EC2 runners + +The EC2 provider creates runners from a launch template. Bootstrap is handled by user data, and the runner retrieves its configuration from Parameter Store. -### Action runners on EC2 +### Lambda MicroVM runners -The action runners are created via a launch template; in the launch template only the subnet needs to be provided. During launch the installation is handled via a user data script. The configuration is fetched from SSM parameter store. +The MicroVM provider starts Linux ARM64 capacity with `RunMicrovm`. The control plane generates the ephemeral runner's JIT payload, and the MicroVM image retrieves that payload from Parameter Store through its `/run` hook. The resolved common runner role is the MicroVM execution role. ### Lambda scale up -The scale up lambda is triggered by events on a SQS queue. Events on this queue are delayed, which will give the workflow some time to start running on available runners. For each event the lambda will check if the workflow is still queued and no other limits are reached. In that case the lambda will create a new EC2 instance. The lambda only needs to know which launch template to use and which subnets are available. From the available subnets a random one will be chosen. Once the instance is created the event is assumed as handled, and we assume the workflow wil start at some moment once the created instance is ready. +The scale-up Lambda is triggered by events on the runner configuration's SQS queue. It verifies that the workflow remains queued and that capacity limits permit another runner, then delegates creation to the selected compute provider: EC2 launches from the provider launch template, while MicroVM calls `RunMicrovm` with the resolved image, execution role, connector, duration, and logging settings. ### Lambda scale down -The scale down lambda is triggered via a CloudWatch event. The event is triggered by a cron expression defined in `orchestration_provider.webhook.lambda.scale.down.schedule_expression` (https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html). For scaling down GitHub does not provide a good API yet, therefore we run the scaling down based on this event every x minutes. Each time the lambda is triggered it tries to remove all runners older than x minutes (configurable) managed in this deployment. In case the runner can be removed from GitHub, which means it is not executing a workflow, the lambda will terminate the EC2 instance. +The scale-down Lambda runs on the schedule configured by `orchestration_provider.webhook.lambda.scale.down.schedule_expression`. It lists capacity through the selected compute provider, correlates it with GitHub runner state, and terminates removable EC2 instances or MicroVMs through that provider's API. --8<-- "modules/orchestration-providers/webhook/scale-down-state-diagram.md:mkdocs_scale_down_state_diagram" @@ -86,6 +94,8 @@ yarn run dist |------|--------|---------| | [compute\_aws\_ec2](#module\_compute\_aws\_ec2) | ../compute-providers/aws/ec2 | n/a | | [compute\_aws\_ec2\_trust\_policy](#module\_compute\_aws\_ec2\_trust\_policy) | ../compute-providers/aws/ec2/trust-policy | n/a | +| [compute\_aws\_microvm](#module\_compute\_aws\_microvm) | ../compute-providers/aws/microvm | n/a | +| [compute\_aws\_microvm\_trust\_policy](#module\_compute\_aws\_microvm\_trust\_policy) | ../compute-providers/aws/microvm/trust-policy | n/a | | [orchestration\_webhook](#module\_orchestration\_webhook) | ../orchestration-providers/webhook | n/a | | [ssm\_housekeeper](#module\_ssm\_housekeeper) | ./ssm-housekeeper | n/a | diff --git a/modules/runner-config/compute-provider.aws.microvm.tf b/modules/runner-config/compute-provider.aws.microvm.tf new file mode 100644 index 0000000000..bdfb5a8f30 --- /dev/null +++ b/modules/runner-config/compute-provider.aws.microvm.tf @@ -0,0 +1,27 @@ +module "compute_aws_microvm_trust_policy" { + count = local.provider_key == "aws_microvm" ? 1 : 0 + source = "../compute-providers/aws/microvm/trust-policy" + + additional_trust_policy_json = var.runner.iam.additional_trust_policy_json +} + +module "compute_aws_microvm" { + count = local.provider_key == "aws_microvm" ? 1 : 0 + source = "../compute-providers/aws/microvm" + + aws_partition = var.aws_partition + aws_region = var.aws_region + prefix = var.prefix + tags = var.tags + + config = var.compute_provider.aws.microvm + runner = merge(var.runner, { + iam = merge(var.runner.iam, { + role = local.runner_role + managed_policy_arns = local.common_runner_managed_policy_arns + }) + }) + github = var.github + ssm = var.ssm + observability = var.observability +} diff --git a/modules/runner-config/compute-provider.tf b/modules/runner-config/compute-provider.tf index bffc43b814..c768c47c22 100644 --- a/modules/runner-config/compute-provider.tf +++ b/modules/runner-config/compute-provider.tf @@ -1,6 +1,7 @@ locals { compute_providers = { - aws_ec2 = var.compute_provider.aws.ec2 + aws_ec2 = var.compute_provider.aws.ec2 + aws_microvm = var.compute_provider.aws.microvm } discovered_provider_key = one([ @@ -10,19 +11,22 @@ locals { provider_key = var.compute_provider_key != null ? var.compute_provider_key : local.discovered_provider_key provider_types = { - aws_ec2 = "ec2" + aws_ec2 = "ec2" + aws_microvm = "microvm" } provider_type = local.provider_types[local.provider_key] provider_assume_role_policies = { - aws_ec2 = try(module.compute_aws_ec2_trust_policy[0].assume_role_policy, null) + aws_ec2 = try(module.compute_aws_ec2_trust_policy[0].assume_role_policy, null) + aws_microvm = try(module.compute_aws_microvm_trust_policy[0].assume_role_policy, null) } provider_assume_role_policy = local.provider_assume_role_policies[local.provider_key] provider_contracts = { - aws_ec2 = one(module.compute_aws_ec2[*].provider) + aws_ec2 = one(module.compute_aws_ec2[*].provider) + aws_microvm = one(module.compute_aws_microvm[*].provider) } provider_contract = local.provider_contracts[local.provider_key] diff --git a/modules/runner-config/outputs.tf b/modules/runner-config/outputs.tf index 486e3261eb..93582279da 100644 --- a/modules/runner-config/outputs.tf +++ b/modules/runner-config/outputs.tf @@ -36,7 +36,8 @@ output "provider" { description = "Provider-specific resources grouped under the selected provider namespace and type." value = { aws = { - ec2 = local.provider_key == "aws_ec2" ? local.provider_contract.resources : null + ec2 = local.provider_key == "aws_ec2" ? local.provider_contract.resources : null + microvm = local.provider_key == "aws_microvm" ? local.provider_contract.resources : null } } } diff --git a/modules/runner-config/tests/computed-iam-inputs.tftest.hcl b/modules/runner-config/tests/computed-iam-inputs.tftest.hcl index 9fcea735ed..ff36530377 100644 --- a/modules/runner-config/tests/computed-iam-inputs.tftest.hcl +++ b/modules/runner-config/tests/computed-iam-inputs.tftest.hcl @@ -23,6 +23,10 @@ run "computed_external_values_keep_plan_shape_known" { target = module.generated_policy.module.ssm_housekeeper } + override_module { + target = module.computed_microvm.module.ssm_housekeeper + } + assert { condition = output.external_role_runner_count == 0 error_message = "Computed external AMI parameter, KMS key, role, and profile values must not make resource or policy-block counts unknown." @@ -32,4 +36,9 @@ run "computed_external_values_keep_plan_shape_known" { condition = output.generated_policy_role_runner_count == 1 error_message = "A computed managed-policy ARN under a caller-known map key must keep attachment planning stable." } + + assert { + condition = output.computed_microvm_role_runner_count == 1 + error_message = "A computed MicroVM image ARN and computed managed-policy ARNs inside plan-known wrappers must keep provider dispatch and attachment counts stable." + } } diff --git a/modules/runner-config/tests/fixtures/computed-iam-inputs/README.md b/modules/runner-config/tests/fixtures/computed-iam-inputs/README.md index 3bbf0f9027..46d64bdce2 100644 --- a/modules/runner-config/tests/fixtures/computed-iam-inputs/README.md +++ b/modules/runner-config/tests/fixtures/computed-iam-inputs/README.md @@ -16,6 +16,7 @@ | Name | Source | Version | |------|--------|---------| +| [computed\_microvm](#module\_computed\_microvm) | ../../.. | n/a | | [external\_iam](#module\_external\_iam) | ../../.. | n/a | | [generated\_policy](#module\_generated\_policy) | ../../.. | n/a | @@ -25,6 +26,7 @@ |------|------| | [random_id.external](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/id) | resource | | [random_id.generated_policy](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/id) | resource | +| [random_id.microvm](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/id) | resource | ## Inputs @@ -34,6 +36,7 @@ No inputs. | Name | Description | |------|-------------| +| [computed\_microvm\_role\_runner\_count](#output\_computed\_microvm\_role\_runner\_count) | n/a | | [external\_role\_runner\_count](#output\_external\_role\_runner\_count) | n/a | | [generated\_policy\_role\_runner\_count](#output\_generated\_policy\_role\_runner\_count) | n/a | diff --git a/modules/runner-config/tests/fixtures/computed-iam-inputs/computed-iam-inputs.tf b/modules/runner-config/tests/fixtures/computed-iam-inputs/computed-iam-inputs.tf index e1b763f8bd..a4114c5832 100644 --- a/modules/runner-config/tests/fixtures/computed-iam-inputs/computed-iam-inputs.tf +++ b/modules/runner-config/tests/fixtures/computed-iam-inputs/computed-iam-inputs.tf @@ -10,6 +10,10 @@ resource "random_id" "generated_policy" { byte_length = 4 } +resource "random_id" "microvm" { + byte_length = 4 +} + module "external_iam" { source = "../../.." @@ -205,6 +209,98 @@ module "generated_policy" { } } +module "computed_microvm" { + source = "../../.." + + aws_region = "eu-west-1" + prefix = "computed-microvm" + + compute_provider = { + aws = { + microvm = { + image_arn = "arn:aws:lambda:eu-west-1:123456789012:microvm-image:runner-${random_id.microvm.hex}" + iam = { + managed_policies = { + scale_up = { + arn = "arn:aws:iam::123456789012:policy/microvm-scale-up-${random_id.microvm.hex}" + } + pool = { + arn = "arn:aws:iam::123456789012:policy/microvm-pool-${random_id.microvm.hex}" + } + } + } + } + } + } + + runner = { + os = "linux" + architecture = "arm64" + labels = ["self-hosted", "linux", "arm64", "microvm"] + } + + lambda = { + artifact = { + s3 = { + bucket = "lambda-artifacts" + } + } + } + + github = { + app_parameters = { + key_base64 = [{ + name = "/github-runner/key-base64" + arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/key-base64" + }] + id = [{ + name = "/github-runner/app-id" + arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-id" + }] + installation_id = [null] + } + } + + orchestration_provider = { + webhook = { + runner = { + ephemeral = true + } + github = { + organization_runners = true + } + queue = { + build = { + arn = "arn:aws:sqs:eu-west-1:123456789012:computed-microvm" + url = "https://sqs.eu-west-1.amazonaws.com/123456789012/computed-microvm" + } + } + lambda = { + artifact = { + s3 = { + key = "runners.zip" + } + } + pool = { + runner_owner = "example" + config = [{ + schedule_expression = "cron(0 8 * * ? *)" + size = 1 + }] + } + } + } + } + + ssm = { + paths = { + root = "/github-runner/computed-microvm" + tokens = "tokens" + config = "config" + } + } +} + output "external_role_runner_count" { value = module.external_iam.runner.role == null ? 0 : 1 } @@ -212,3 +308,7 @@ output "external_role_runner_count" { output "generated_policy_role_runner_count" { value = module.generated_policy.runner.role == null ? 0 : 1 } + +output "computed_microvm_role_runner_count" { + value = module.computed_microvm.runner.role == null ? 0 : 1 +} diff --git a/modules/runner-config/tests/pool.tftest.hcl b/modules/runner-config/tests/pool.tftest.hcl index 12a81854c3..c8a0a383f5 100644 --- a/modules/runner-config/tests/pool.tftest.hcl +++ b/modules/runner-config/tests/pool.tftest.hcl @@ -1,4 +1,10 @@ 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\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"lambda.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}" @@ -177,7 +183,8 @@ run "plan_with_pool_enabled" { assert { condition = ( toset(keys(output.provider)) == toset(["aws"]) - && toset(keys(output.provider.aws)) == toset(["ec2"]) + && toset(keys(output.provider.aws)) == toset(["ec2", "microvm"]) + && output.provider.aws.microvm == null ) error_message = "The runner configuration must expose resources under the selected provider namespace and type." } @@ -195,6 +202,8 @@ run "plan_with_pool_enabled" { assert { condition = ( length(module.compute_aws_ec2_trust_policy) == 1 + && length(module.compute_aws_microvm_trust_policy) == 0 + && length(module.compute_aws_microvm) == 0 && aws_iam_role.runner[0].assume_role_policy == module.compute_aws_ec2_trust_policy[0].assume_role_policy ) error_message = "The common runner role must use the selected EC2 trust-policy submodule output." @@ -650,3 +659,325 @@ run "job_retry_uses_common_runner_configuration_identity" { error_message = "Job retry must apply its configured Lambda reserved concurrency." } } + +run "routes_lambda_microvm_provider" { + command = plan + + variables { + compute_provider_key = "aws_microvm" + runner = { + os = "linux" + architecture = "arm64" + labels = ["self-hosted", "linux", "arm64", "microvm"] + } + compute_provider = { + aws = { + microvm = { + image_arn = "arn:aws:lambda:eu-west-1:123456789012:microvm-image:runner" + image_version = "7" + ingress_network_connectors = [ + "arn:aws:lambda:eu-west-1:123456789012:network-connector:private-ingress", + ] + egress_network_connectors = [ + "arn:aws:lambda:eu-west-1:aws:network-connector:aws-network-connector:INTERNET_EGRESS", + ] + } + } + } + orchestration_provider = { + webhook = { + runner = { + ephemeral = true + jit_config_enabled = null + } + github = { + organization_runners = true + } + queue = { + build = { + arn = "arn:aws:sqs:eu-west-1:123456789012:build-queue" + url = "https://sqs.eu-west-1.amazonaws.com/123456789012/build-queue" + } + } + lambda = { + artifact = { + s3 = { + key = "runners.zip" + } + } + } + } + } + } + + assert { + condition = ( + length(module.compute_aws_ec2) == 0 + && length(module.compute_aws_ec2_trust_policy) == 0 + && length(module.compute_aws_microvm) == 1 + && length(module.compute_aws_microvm_trust_policy) == 1 + && aws_iam_role.runner[0].assume_role_policy == module.compute_aws_microvm_trust_policy[0].assume_role_policy + && toset(keys(aws_iam_role_policy.runner_provider)) == toset(["cloudwatch", "runner_metadata", "runtime_logs", "ssm_jit"]) + && aws_iam_role_policy.runner_provider["cloudwatch"].name == "runner-microvm-cloudwatch" + && aws_iam_role_policy.runner_provider["runner_metadata"].name == "runner-microvm-metadata" + && aws_iam_role_policy.runner_provider["runtime_logs"].name == "runner-microvm-runtime-logs" + && aws_iam_role_policy.runner_provider["ssm_jit"].name == "runner-microvm-ssm-jit" + ) + error_message = "The aws.microvm leaf must dispatch only to the namespaced provider modules and attach all required policies to its managed runner role." + } + + assert { + condition = ( + toset(keys(output.provider.aws)) == toset(["ec2", "microvm"]) + && output.provider.aws.ec2 == null + && output.provider.aws.microvm.image_arn == "arn:aws:lambda:eu-west-1:123456789012:microvm-image:runner" + && output.provider.aws.microvm.image_version == "7" + && contains(keys(output.provider.aws.microvm), "execution_role_arn") + && output.provider.aws.microvm.runners_log_groups[0].name == "/github-self-hosted-runners/github-actions/microvm" + && toset(slice(output.provider.aws.microvm.runners_log_groups[*].name, 1, 4)) == toset([ + "/github-self-hosted-runners/github-actions/internal_service", + "/github-self-hosted-runners/github-actions/run", + "/github-self-hosted-runners/github-actions/runner", + ]) + && output.provider.aws.microvm.logfiles[2].file_path == "/opt/actions-runner/_diag/Runner_**.log" + ) + error_message = "The selected MicroVM resources must be exposed only under provider.aws.microvm." + } + + assert { + condition = ( + module.orchestration_webhook[0].scale_up.lambda.environment[0].variables["COMPUTE_PROVIDER_TYPE"] == "microvm" + && module.orchestration_webhook[0].scale_down.lambda.environment[0].variables["COMPUTE_PROVIDER_TYPE"] == "microvm" + && module.orchestration_webhook[0].scale_up.lambda.environment[0].variables["MICROVM_IMAGE_ARN"] == "arn:aws:lambda:eu-west-1:123456789012:microvm-image:runner" + && contains(keys(module.orchestration_webhook[0].scale_up.lambda.environment[0].variables), "MICROVM_EXECUTION_ROLE_ARN") + && module.orchestration_webhook[0].scale_up.lambda.environment[0].variables["MICROVM_LOG_GROUP"] == "/github-self-hosted-runners/github-actions/microvm" + && module.orchestration_webhook[0].scale_up.lambda.environment[0].variables["MICROVM_METADATA_SSM_PATH"] == "/github-runner/config/microvm-metadata" + && module.orchestration_webhook[0].scale_up.lambda.environment[0].variables["SSM_CONFIG_PATH"] == "/github-runner/config" + && module.orchestration_webhook[0].scale_up.lambda.environment[0].variables["ENVIRONMENT"] == "github-actions" + && module.orchestration_webhook[0].scale_up.lambda.environment[0].variables["RUNNER_NAME_PREFIX"] == "" + && module.orchestration_webhook[0].scale_down.lambda.environment[0].variables["MICROVM_METADATA_SSM_PATH"] == "/github-runner/config/microvm-metadata" + && module.orchestration_webhook[0].scale_down.lambda.environment[0].variables["SSM_TOKEN_PATH"] == "/github-runner/tokens" + && module.orchestration_webhook[0].scale_down.lambda.environment[0].variables["RUNNER_BOOT_TIME_IN_MINUTES"] == "5" + && !contains(keys(module.orchestration_webhook[0].scale_up.lambda.environment[0].variables), "MICROVM_RUN_CONFIG") + && !contains(keys(module.orchestration_webhook[0].scale_up.lambda.environment[0].variables), "MICROVM_TAGS") + && !contains(keys(module.orchestration_webhook[0].scale_up.lambda.environment[0].variables), "MICROVM_METADATA_TAGS") + && !contains(keys(module.orchestration_webhook[0].scale_up.lambda.environment[0].variables), "MICROVM_RUNNER_CONFIG_SSM_ARN") + ) + error_message = "Runner-config must preserve the runtime provider type and merge the canonical MicroVM environment with webhook-owned lifecycle values." + } +} + +run "external_microvm_runner_role_remains_unmanaged" { + command = plan + + variables { + runner = { + os = "linux" + architecture = "arm64" + labels = ["self-hosted", "linux", "arm64", "microvm"] + iam = { + role = { + arn = "arn:aws:iam::123456789012:role/external/microvm-runner" + } + } + } + compute_provider = { + aws = { + microvm = { + image_arn = "arn:aws:lambda:eu-west-1:123456789012:microvm-image:runner" + } + } + } + orchestration_provider = { + webhook = { + runner = { + ephemeral = true + } + github = { + organization_runners = true + } + queue = { + build = { + arn = "arn:aws:sqs:eu-west-1:123456789012:build-queue" + url = "https://sqs.eu-west-1.amazonaws.com/123456789012/build-queue" + } + } + lambda = { + artifact = { + s3 = { + key = "runners.zip" + } + } + } + } + } + } + + assert { + condition = ( + length(aws_iam_role.runner) == 0 + && length(aws_iam_role_policy.runner_provider) == 0 + && length(aws_iam_role_policy_attachment.runner) == 0 + && output.provider.aws.microvm.execution_role_arn == "arn:aws:iam::123456789012:role/external/microvm-runner" + ) + error_message = "An external provider-neutral runner role must remain caller-owned while serving as the MicroVM execution role." + } +} + +run "rejects_multiple_aws_compute_providers" { + command = plan + + variables { + compute_provider = { + aws = { + ec2 = { + vpc_id = "vpc-12345678" + subnet_ids = ["subnet-12345678"] + instance_types = ["m5.large"] + } + microvm = { + image_arn = "arn:aws:lambda:eu-west-1:123456789012:microvm-image:runner" + } + } + } + } + + plan_options { + target = [terraform_data.validate_config] + } + + expect_failures = [terraform_data.validate_config] +} + +run "rejects_non_ephemeral_microvm_runner" { + command = plan + + variables { + runner = { + os = "linux" + architecture = "arm64" + labels = ["self-hosted", "linux", "arm64", "microvm"] + } + compute_provider = { + aws = { + microvm = { + image_arn = "arn:aws:lambda:eu-west-1:123456789012:microvm-image:runner" + } + } + } + orchestration_provider = { + webhook = { + runner = { + ephemeral = false + } + github = { + organization_runners = true + } + queue = { + build = { + arn = "arn:aws:sqs:eu-west-1:123456789012:build-queue" + url = "https://sqs.eu-west-1.amazonaws.com/123456789012/build-queue" + } + } + } + } + } + + plan_options { + target = [terraform_data.validate_config] + } + + expect_failures = [terraform_data.validate_config] +} + +run "rejects_microvm_runner_with_jit_disabled" { + command = plan + + variables { + runner = { + os = "linux" + architecture = "arm64" + labels = ["self-hosted", "linux", "arm64", "microvm"] + } + compute_provider = { + aws = { + microvm = { + image_arn = "arn:aws:lambda:eu-west-1:123456789012:microvm-image:runner" + } + } + } + orchestration_provider = { + webhook = { + runner = { + ephemeral = true + jit_config_enabled = false + } + github = { + organization_runners = true + } + queue = { + build = { + arn = "arn:aws:sqs:eu-west-1:123456789012:build-queue" + url = "https://sqs.eu-west-1.amazonaws.com/123456789012/build-queue" + } + } + } + } + } + + plan_options { + target = [terraform_data.validate_config] + } + + expect_failures = [terraform_data.validate_config] +} + +run "rejects_non_arm64_microvm_runner" { + command = plan + + variables { + runner = { + os = "linux" + architecture = "x64" + labels = ["self-hosted", "linux", "x64", "microvm"] + } + compute_provider = { + aws = { + microvm = { + image_arn = "arn:aws:lambda:eu-west-1:123456789012:microvm-image:runner" + } + } + } + } + + plan_options { + target = [terraform_data.validate_config] + } + + expect_failures = [terraform_data.validate_config] +} + +run "rejects_non_linux_microvm_runner" { + command = plan + + variables { + runner = { + os = "windows" + architecture = "arm64" + labels = ["self-hosted", "windows", "arm64", "microvm"] + } + compute_provider = { + aws = { + microvm = { + image_arn = "arn:aws:lambda:eu-west-1:123456789012:microvm-image:runner" + } + } + } + } + + plan_options { + target = [terraform_data.validate_config] + } + + expect_failures = [terraform_data.validate_config] +} diff --git a/modules/runner-config/validations.tf b/modules/runner-config/validations.tf index f510bf0422..2b3bfddccb 100644 --- a/modules/runner-config/validations.tf +++ b/modules/runner-config/validations.tf @@ -74,7 +74,14 @@ resource "terraform_data" "validate_config" { for provider_key, provider_config in local.compute_providers : provider_key if provider_config != null ]) == 1 - error_message = "Exactly one compute-provider block must be set. Supported compute-provider blocks: aws.ec2." + error_message = "Exactly one compute-provider block must be set. Supported compute-provider blocks: aws.ec2, aws.microvm." + } + + precondition { + condition = var.compute_provider.aws.microvm == null ? true : ( + var.runner.os == "linux" && var.runner.architecture == "arm64" + ) + error_message = "compute_provider.aws.microvm requires runner.os = linux and runner.architecture = arm64." } precondition { @@ -93,6 +100,17 @@ resource "terraform_data" "validate_config" { error_message = "Exactly one orchestration provider must be configured. Supported providers: webhook." } + precondition { + condition = var.compute_provider.aws.microvm == null ? true : ( + try(var.orchestration_provider.webhook.runner.ephemeral, false) && + try(coalesce( + var.orchestration_provider.webhook.runner.jit_config_enabled, + var.orchestration_provider.webhook.runner.ephemeral, + ), false) + ) + error_message = "compute_provider.aws.microvm requires webhook orchestration with ephemeral and JIT runner configuration enabled." + } + precondition { condition = var.orchestration_provider.webhook == null ? true : ( var.orchestration_provider.webhook.lambda.scale.up.event_source_mapping.batch_size >= 1 && diff --git a/modules/runner-config/variables.compute-provider.tf b/modules/runner-config/variables.compute-provider.tf index 87c85b7c05..7e07705894 100644 --- a/modules/runner-config/variables.compute-provider.tf +++ b/modules/runner-config/variables.compute-provider.tf @@ -5,8 +5,8 @@ variable "compute_provider_key" { default = null validation { - condition = var.compute_provider_key == null ? true : contains(["aws_ec2"], var.compute_provider_key) - error_message = "compute_provider_key must be null or aws_ec2." + condition = var.compute_provider_key == null ? true : contains(["aws_ec2", "aws_microvm"], var.compute_provider_key) + error_message = "compute_provider_key must be null, aws_ec2, or aws_microvm." } } @@ -257,6 +257,40 @@ variable "compute_provider" { ]) use_dedicated_host = optional(bool, false) }), null) + microvm = optional(object({ + image_arn = string + image_version = optional(string, null) + ingress_network_connectors = optional(list(string), []) + egress_network_connectors = optional(list(string), []) + cloudwatch_agent = optional(object({ + enabled = optional(bool, true) + config = optional(string, null) + }), {}) + 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) + environment_variables = optional(map(string), {}) + iam = optional(object({ + resource_arns = optional(object({ + images = optional(list(string), null) + }), {}) + additional_policy_json = optional(object({ + scale_up = optional(string, null) + }), {}) + managed_policies = optional(object({ + scale_up = optional(object({ + arn = string + }), null) + pool = optional(object({ + arn = string + }), null) + }), {}) + }), {}) + }), null) }), {}) }) From e5d3071bdafc841dc5af46a28c5bc7f4396a9bc7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 21:35:46 +0000 Subject: [PATCH 2/3] docs: auto update terraform docs --- modules/multi-runner/README.md | 4 ++-- modules/runner-config/README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/multi-runner/README.md b/modules/multi-runner/README.md index 6c67603ea2..a3d5ff4607 100644 --- a/modules/multi-runner/README.md +++ b/modules/multi-runner/README.md @@ -160,13 +160,13 @@ module "multi-runner" { | [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\_global\_config](#input\_experimental\_global\_config) | Experimental global defaults shared by all runner lanes. |
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)
}), {})
}), {})
})
| `{}` | no | -| [experimental\_global\_config\_compute\_provider](#input\_experimental\_global\_config\_compute\_provider) | Experimental global compute-provider configuration. |
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({
runner_deregistration = optional(object({
enabled = optional(bool, true)
}), {})
spot_termination_handler = optional(object({
enabled = optional(bool, true)
}), {})
spot_termination_notification_watcher = optional(object({
enabled = 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)
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")
}), {})
}), {})
}), {})
}), {})
}), {})
})
| `{}` | no | +| [experimental\_global\_config\_compute\_provider](#input\_experimental\_global\_config\_compute\_provider) | Experimental global compute-provider configuration. |
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({
runner_deregistration = optional(object({
enabled = optional(bool, true)
}), {})
spot_termination_handler = optional(object({
enabled = optional(bool, true)
}), {})
spot_termination_notification_watcher = optional(object({
enabled = 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)
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")
}), {})
}), {})
}), {})
}), {})
microvm = optional(object({
image_arn = optional(string, null)
image_version = optional(string, null)
ingress_network_connectors = optional(list(string), [])
egress_network_connectors = optional(list(string), [])
cloudwatch_agent = optional(object({
enabled = optional(bool, true)
config = optional(string, null)
}), {})
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)
environment_variables = optional(map(string), {})
iam = optional(object({
resource_arns = optional(object({
images = optional(list(string), null)
}), {})
additional_policy_json = optional(object({
scale_up = optional(string, null)
}), {})
managed_policies = optional(object({
scale_up = optional(object({
arn = string
}), null)
pool = optional(object({
arn = string
}), null)
}), {})
}), {})
}), {})
}), {})
})
| `{}` | no | | [experimental\_global\_config\_github](#input\_experimental\_global\_config\_github) | Experimental global GitHub configuration. |
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")
})
| `{}` | no | | [experimental\_global\_config\_lambda](#input\_experimental\_global\_config\_lambda) | Experimental global Lambda configuration. |
object({
artifact = optional(object({
s3 = optional(object({
bucket = optional(string, null)
}), {})
}), {})
runtime = optional(string, "nodejs24.x")
architecture = optional(string, "arm64")
principals = optional(list(object({
type = string
identifiers = list(string)
})), [])
subnet_ids = optional(list(string), [])
security_group_ids = optional(list(string), [])
tags = optional(map(string), {})
role = optional(object({
path = optional(string, null)
permissions_boundary = optional(string, null)
}), {})
})
| `{}` | no | | [experimental\_global\_config\_observability](#input\_experimental\_global\_config\_observability) | Experimental global observability configuration. |
object({
logs = optional(object({
level = optional(string, "info")
retention_in_days = optional(number, 180)
kms_key_id = optional(string, null)
class = optional(string, "STANDARD")
tags = optional(map(string), {})
}), {})
tracing = optional(object({
mode = optional(string, null)
capture_http_requests = optional(bool, false)
capture_error = optional(bool, false)
}), {})
metrics = optional(object({
enabled = optional(bool, false)
namespace = optional(string, "GitHub Runners")
metric = optional(object({
github_app_rate_limit = optional(object({
enabled = optional(bool, true)
}), {})
job_retry = optional(object({
enabled = optional(bool, true)
}), {})
spot_termination_warning = optional(object({
enabled = optional(bool, true)
}), {})
}), {})
}), {})
})
| `{}` | no | | [experimental\_global\_config\_orchestration\_provider](#input\_experimental\_global\_config\_orchestration\_provider) | Experimental global orchestration-provider configuration. |
object({
webhook = optional(object({
queue_selection_strategy = optional(string, "first")
eventbridge = optional(object({
enabled = optional(bool, true)
accept_events = optional(list(string), [])
}), {})
matcher_config_parameter_store_tier = optional(string, "Standard")
runner = optional(object({
boot_time_in_minutes = optional(number, 5)
ephemeral = optional(bool, false)
jit_config_enabled = optional(bool, null)
maximum_count = optional(number, null)
}), {})

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

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

queue = optional(object({
delay_webhook_event = optional(number, 30)
job_queue_retention_in_seconds = optional(number, 86400)
visibility_timeout_seconds = optional(number, 180)
redrive_build_queue = optional(object({
enabled = optional(bool, false)
maxReceiveCount = optional(number, null)
}), {
enabled = false
maxReceiveCount = null
})
tags = optional(map(string), {})
encryption = optional(object({
kms_data_key_reuse_period_seconds = number
kms_master_key_id = string
sqs_managed_sse_enabled = bool
}), {
kms_data_key_reuse_period_seconds = null
kms_master_key_id = null
sqs_managed_sse_enabled = true
})
}), {})
}), {})
})
| `{}` | no | | [experimental\_global\_config\_ssm](#input\_experimental\_global\_config\_ssm) | Experimental global SSM configuration. |
object({
paths = optional(object({
root = optional(string, null)
app = optional(string, "app")
webhook = optional(string, "webhook")
tokens = optional(string, "runners/tokens")
config = optional(string, "runners/config")
}), {})
kms_key_id = optional(string, null)
tags = optional(map(string), {})
parameters = optional(object({
tags = optional(map(string), {})
}), {})
housekeeper = optional(object({
schedule_expression = optional(string, "rate(1 day)")
state = optional(string, "ENABLED")
tags = optional(map(string), {})
lambda = optional(object({
artifact = optional(object({
zip = optional(string, null)
s3 = optional(object({
key = string
object_version = optional(string, null)
}), null)
}), {})
memory_size = optional(number, 512)
timeout = optional(number, 60)
}), {})
config = optional(object({
tokenPath = optional(string, null)
minimumDaysOld = optional(number, 1)
dryRun = optional(bool, false)
}), {})
}), {})
})
| `{}` | no | -| [experimental\_multi\_runner\_config](#input\_experimental\_multi\_runner\_config) | Experimental per-runner and per-lane overrides. |
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)
dynamic_labels_enabled = 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({
enabled = optional(bool, null)
namespace = optional(string, null)
metric = optional(object({
github_app_rate_limit = optional(object({
enabled = optional(bool, null)
}), {})
job_retry = optional(object({
enabled = optional(bool, null)
}), {})
spot_termination_warning = optional(object({
enabled = optional(bool, null)
}), {})
}), {})
}), {})
}), {})

compute_provider = 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)
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 | +| [experimental\_multi\_runner\_config](#input\_experimental\_multi\_runner\_config) | Experimental per-runner and per-lane overrides. |
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)
dynamic_labels_enabled = 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({
enabled = optional(bool, null)
namespace = optional(string, null)
metric = optional(object({
github_app_rate_limit = optional(object({
enabled = optional(bool, null)
}), {})
job_retry = optional(object({
enabled = optional(bool, null)
}), {})
spot_termination_warning = optional(object({
enabled = optional(bool, null)
}), {})
}), {})
}), {})
}), {})

compute_provider = 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)
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)
microvm = optional(object({
image_arn = optional(string, null)
image_version = optional(string, null)
ingress_network_connectors = optional(list(string), null)
egress_network_connectors = optional(list(string), null)
cloudwatch_agent = optional(object({
enabled = optional(bool, null)
config = optional(string, null)
}), {})
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)
environment_variables = optional(map(string), {})
iam = optional(object({
resource_arns = optional(object({
images = optional(list(string), null)
}), {})
additional_policy_json = optional(object({
scale_up = optional(string, null)
}), {})
managed_policies = optional(object({
scale_up = optional(object({
arn = string
}), null)
pool = optional(object({
arn = string
}), null)
}), {})
}), {})
}), 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 for the stable v1 interface, see your github app.
Omit this value when using the experimental v2 interface and provide the
app through `experimental_global_config_github` instead.
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
}))
})
| `{}` | no | diff --git a/modules/runner-config/README.md b/modules/runner-config/README.md index e7dfd50d63..e7ed1fb14a 100644 --- a/modules/runner-config/README.md +++ b/modules/runner-config/README.md @@ -119,7 +119,7 @@ yarn run dist |------|-------------|------|---------|:--------:| | [aws\_partition](#input\_aws\_partition) | AWS partition used to construct ARNs. | `string` | `"aws"` | no | | [aws\_region](#input\_aws\_region) | AWS region. | `string` | n/a | yes | -| [compute\_provider](#input\_compute\_provider) | Typed compute-provider configuration. Provider-owned settings remain inside the selected compute-provider block.

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

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

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

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

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

- `artifact.s3.bucket`: Optional shared S3 bucket containing component-owned Lambda artifacts. An orchestration provider selects its own object key and version; the bucket alone selects no artifact.
- `runtime`: Runtime used by the control-plane Lambda functions.
- `architecture`: Instruction-set architecture used by the control-plane Lambda functions. Supported values are `arm64` and `x86_64`.
- `subnet_ids`: Subnets used for Lambda VPC configuration.
- `security_group_ids`: Security groups used for Lambda VPC configuration.
- `tags`: Shared tags applied to Lambda function resources only. These override module-level `tags`; component `tags` override this map when keys conflict.
- `principals`: Additional principals allowed to assume the control-plane Lambda roles.
- `role.path`: IAM path for module-managed Lambda execution roles. Defaults to a path derived from `prefix`.
- `role.permissions_boundary`: Permissions-boundary ARN applied to module-managed Lambda execution roles. |
object({
artifact = optional(object({
s3 = optional(object({
bucket = optional(string, null)
}), {})
}), {})
runtime = optional(string, "nodejs24.x")
architecture = optional(string, "arm64")
subnet_ids = optional(list(string), [])
security_group_ids = optional(list(string), [])
tags = optional(map(string), {})
principals = optional(list(object({
type = string
identifiers = list(string)
})), [])
role = optional(object({
path = optional(string, null)
permissions_boundary = optional(string, null)
}), {})
})
| `{}` | no | From 6507ba925f3cbef2b2ad8d1bc8885446fb27ebc0 Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Fri, 4 Sep 2026 20:50:05 +0200 Subject: [PATCH 3/3] test(multi-runner): cover MicroVM provider routing --- .../tests/config-resolution.tftest.hcl | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/modules/multi-runner/tests/config-resolution.tftest.hcl b/modules/multi-runner/tests/config-resolution.tftest.hcl index 0ead028fc3..b53a54a7d4 100644 --- a/modules/multi-runner/tests/config-resolution.tftest.hcl +++ b/modules/multi-runner/tests/config-resolution.tftest.hcl @@ -461,3 +461,118 @@ run "v2_inputs_do_not_require_legacy_arguments" { error_message = "The v2 interface must work without the stable v1 GitHub App, VPC, subnet, or runner configuration inputs." } } + +run "v2_microvm_inputs_route_to_microvm_provider" { + command = plan + + variables { + experimental_global_config = { + runner = { + os = "linux" + architecture = "arm64" + } + } + + experimental_global_config_github = { + app = { + key_base64 = "experimental-app-key" + id = "experimental-app-id" + webhook_secret = "experimental-webhook-secret" + } + } + + experimental_global_config_lambda = { + artifact = { + s3 = { + bucket = "global-lambda-artifacts" + } + } + } + + experimental_global_config_orchestration_provider = { + webhook = { + eventbridge = { + enabled = false + } + runner = { + ephemeral = true + jit_config_enabled = true + } + lambda = { + artifact = { + s3 = { + key = "global-runners.zip" + } + } + } + } + } + + experimental_global_config_ssm = { + housekeeper = { + lambda = { + artifact = { + s3 = { + key = "global-housekeeper.zip" + } + } + } + } + } + + experimental_global_config_compute_provider = { + aws = { + microvm = { + image_arn = "arn:aws:lambda:eu-west-1:123456789012:microvm-image:global" + image_version = "7" + } + } + } + + experimental_multi_runner_config = { + microvm = { + runner = { + name_prefix = "microvm-" + } + orchestration_provider = { + webhook = { + matcherConfig = { + labelMatchers = [["microvm"]] + } + } + } + compute_provider = { + aws = { + microvm = { + image_version = "8" + } + } + } + } + } + } + + assert { + condition = ( + local.use_v2_config + && keys(local.resolved_config.multi_runner_config) == ["microvm"] + && local.resolved_config.multi_runner_config["microvm"].runner.os == "linux" + && local.resolved_config.multi_runner_config["microvm"].runner.architecture == "arm64" + && local.resolved_config.multi_runner_config["microvm"].compute_provider.aws.ec2 == null + && local.resolved_config.multi_runner_config["microvm"].compute_provider.aws.microvm.image_arn == "arn:aws:lambda:eu-west-1:123456789012:microvm-image:global" + && local.resolved_config.multi_runner_config["microvm"].compute_provider.aws.microvm.image_version == "8" + ) + error_message = "Experimental MicroVM lanes must resolve Linux ARM64 settings and inherit global provider values while applying lane overrides." + } + + assert { + condition = ( + length(module.runners) == 0 + && keys(module.runner_configs) == ["microvm"] + && output.runners_map_v2["microvm"].provider.aws.ec2 == null + && output.runners_map_v2["microvm"].provider.aws.microvm.image_arn == "arn:aws:lambda:eu-west-1:123456789012:microvm-image:global" + && output.runners_map_v2["microvm"].provider.aws.microvm.image_version == "8" + ) + error_message = "Experimental MicroVM lanes must route through module.runner_configs and expose the MicroVM provider contract without an EC2 provider." + } +}