diff --git a/modules/multi-runner/README.md b/modules/multi-runner/README.md
index 2866af815e..691a4c09da 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.
@@ -162,7 +164,7 @@ module "multi-runner" {
| [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 `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 |
| [global\_config](#input\_global\_config) | 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 |
-| [global\_config\_compute\_provider](#input\_global\_config\_compute\_provider) | Global compute-provider configuration shared by all runner lanes.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 |
+| [global\_config\_compute\_provider](#input\_global\_config\_compute\_provider) | Global compute-provider configuration shared by all runner lanes.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 |
| [global\_config\_github](#input\_global\_config\_github) | Global GitHub configuration shared by all runner lanes.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 |
| [global\_config\_lambda](#input\_global\_config\_lambda) | Global Lambda configuration shared by all runner lanes.object({
artifact = optional(object({
s3 = optional(object({
bucket = optional(string, null)
}), {})
}), {})
runtime = optional(string, "nodejs24.x")
architecture = optional(string, "arm64")
principals = optional(list(object({
type = string
identifiers = list(string)
})), [])
subnet_ids = optional(list(string), [])
security_group_ids = optional(list(string), [])
tags = optional(map(string), {})
role = optional(object({
path = optional(string, null)
permissions_boundary = optional(string, null)
}), {})
}) | `{}` | no |
| [global\_config\_observability](#input\_global\_config\_observability) | Global observability configuration shared by all runner lanes.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 |
@@ -188,7 +190,7 @@ module "multi-runner" {
| [logging\_retention\_in\_days](#input\_logging\_retention\_in\_days) | Specifies the number of days you want to retain log events for the lambda log group. Possible values are: 0, 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, and 3653. | `number` | `180` | no |
| [matcher\_config\_parameter\_store\_tier](#input\_matcher\_config\_parameter\_store\_tier) | The tier of the parameter store for the matcher configuration. Valid values are `Standard`, and `Advanced`. | `string` | `"Standard"` | no |
| [metrics](#input\_metrics) | Configuration for metrics created by the module, by default metrics are disabled to avoid additional costs. When metrics are enable all metrics are created unless explicit configured otherwise. | object({
enable = optional(bool, false)
namespace = optional(string, "GitHub Runners")
metric = optional(object({
enable_github_app_rate_limit = optional(bool, true)
enable_job_retry = optional(bool, true)
enable_spot_termination_warning = optional(bool, true)
}), {})
}) | `{}` | no |
-| [multi\_runner\_config](#input\_multi\_runner\_config) | Accepts either the stable v1 runner configuration shape or the provider-boundary v2 shape. Entries with `runner_config` use the v1 shape; entries without `runner_config` use the v2 shape. A v2 entry does not need matcher configuration. A v2 entry must be acknowledged with `experimental_features = ["multi-runner-v2"]`; the v2 shape is experimental and may change before graduation.map(object({
# V1 contract
runner_config = optional(object({
runner_os = string
runner_architecture = string
runner_metadata_options = optional(map(any), {
instance_metadata_tags = "enabled"
http_endpoint = "enabled"
http_tokens = "required"
http_put_response_hop_limit = 1
})
ami = optional(object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
id_ssm_parameter_arn = optional(string, null)
kms_key_arn = optional(string, null)
}), null)
create_service_linked_role_spot = optional(bool, false)
credit_specification = optional(string, null)
delay_webhook_event = optional(number, 30)
disable_runner_autoupdate = optional(bool, false)
ebs_optimized = optional(bool, false)
enable_ephemeral_runners = optional(bool, false)
enable_job_queued_check = optional(bool, null)
enable_on_demand_failover_for_errors = optional(list(string), [])
scale_errors = optional(list(string), [
"UnfulfillableCapacity",
"MaxSpotInstanceCountExceeded",
"TargetCapacityLimitExceededException",
"RequestLimitExceeded",
"ResourceLimitExceeded",
"MaxSpotInstanceCountExceeded",
"MaxSpotFleetRequestCountExceeded",
"InsufficientInstanceCapacity",
"InsufficientCapacityOnHost",
])
enable_organization_runners = optional(bool, false)
enable_runner_binaries_syncer = optional(bool, true)
enable_ssm_on_runners = optional(bool, false)
enable_userdata = optional(bool, true)
instance_allocation_strategy = optional(string, "lowest-price")
instance_type_priorities = optional(map(number), null)
instance_max_spot_price = optional(string, null)
instance_target_capacity_type = optional(string, "spot")
instance_types = list(string)
job_queue_retention_in_seconds = optional(number, 86400)
minimum_running_time_in_minutes = optional(number, null)
pool_runner_owner = optional(string, null)
runner_as_root = optional(bool, false)
runner_boot_time_in_minutes = optional(number, 5)
scale_down_idle_confirmation_seconds = optional(number, 0)
runner_disable_default_labels = optional(bool, false)
runner_extra_labels = optional(list(string), [])
runner_group_name = optional(string, "Default")
runner_name_prefix = optional(string, "")
runner_run_as = optional(string, "ec2-user")
runners_maximum_count = number
runner_additional_security_group_ids = optional(list(string), [])
scale_down_schedule_expression = optional(string, "cron(*/5 * * * ? *)")
scale_up_reserved_concurrent_executions = optional(number, 1)
lambda_event_source_mapping_batch_size = optional(number, null)
lambda_event_source_mapping_maximum_batching_window_in_seconds = optional(number, null)
userdata_template = optional(string, null)
userdata_content = optional(string, null)
enable_jit_config = optional(bool, null)
enable_runner_detailed_monitoring = optional(bool, false)
enable_cloudwatch_agent = optional(bool, true)
cloudwatch_config = optional(string, null)
userdata_pre_install = optional(string, "")
userdata_post_install = optional(string, "")
runner_hook_job_started = optional(string, "")
runner_hook_job_completed = optional(string, "")
runner_ec2_tags = optional(map(string), {})
runner_iam_role_managed_policy_arns = optional(list(string), [])
vpc_id = optional(string, null)
subnet_ids = optional(list(string), null)
idle_config = optional(list(object({
cron = string
timeZone = string
idleCount = number
evictionStrategy = optional(string, "oldest_first")
})), [])
cpu_options = optional(object({
core_count = optional(number)
threads_per_core = optional(number)
amd_sev_snp = optional(string)
nested_virtualization = optional(string)
}), null)
placement = optional(object({
affinity = optional(string)
availability_zone = optional(string)
group_id = optional(string)
group_name = optional(string)
host_id = optional(string)
host_resource_group_arn = optional(string)
spread_domain = optional(string)
tenancy = optional(string)
partition_number = optional(number)
}), null)
license_specifications = optional(list(object({
license_configuration_arn = string
})), [])
use_dedicated_host = optional(bool, false)
runner_log_files = optional(list(object({
log_group_name = string
prefix_log_group = bool
file_path = string
log_stream_name = string
log_class = optional(string, "STANDARD")
})), null)
block_device_mappings = optional(list(object({
delete_on_termination = optional(bool, true)
device_name = optional(string, "/dev/xvda")
encrypted = optional(bool, true)
iops = optional(number)
kms_key_id = optional(string)
snapshot_id = optional(string)
throughput = optional(number)
volume_initialization_rate = optional(number)
volume_size = number
volume_type = optional(string, "gp3")
})), [{
volume_size = 30
}])
pool_config = optional(list(object({
schedule_expression = string
schedule_expression_timezone = optional(string)
size = number
})), [])
job_retry = optional(object({
enable = optional(bool, false)
delay_in_seconds = optional(number, 300)
delay_backoff = optional(number, 2)
lambda_memory_size = optional(number, 256)
lambda_timeout = optional(number, 30)
max_attempts = optional(number, 1)
}), {})
iam_overrides = optional(object({
override_instance_profile = optional(bool, null)
instance_profile_name = optional(string, null)
override_runner_role = optional(bool, null)
runner_role_arn = optional(string, null)
}), {
override_instance_profile = false
instance_profile_name = null
override_runner_role = false
runner_role_arn = null
})
}), null)
matcherConfig = optional(object({
labelMatchers = list(list(string))
exactMatch = optional(bool, false)
bidirectionalLabelMatch = optional(bool, false)
priority = optional(number, 999)
enableDynamicLabels = optional(bool, false)
awsDynamicLabelsPolicy = optional(any, null)
}), null)
redrive_build_queue = optional(object({
enabled = bool
maxReceiveCount = number
}), {
enabled = false
maxReceiveCount = null
})
# V2 Contract
tags = optional(map(string), {})
runner = optional(object({
os = optional(string, null)
architecture = optional(string, null)
disable_default_labels = optional(bool, null)
extra_labels = optional(list(string), null)
group_name = optional(string, null)
name_prefix = optional(string, null)
run_as_root = optional(bool, null)
run_as = optional(string, null)
auto_update_disabled = optional(bool, null)
tags = optional(map(string), {})
hooks = optional(object({
job_started = optional(string, null)
job_completed = optional(string, null)
}), {})
iam = optional(object({
role = optional(object({
arn = string
}), null)
managed_policy_arns = optional(map(string), null)
additional_trust_policy_json = optional(string, null)
path = optional(string, null)
permissions_boundary = optional(string, null)
}), {})
}), {})
lambda = optional(object({
runtime = optional(string, null)
architecture = optional(string, null)
subnet_ids = optional(list(string), null)
security_group_ids = optional(list(string), null)
tags = optional(map(string), {})
role = optional(object({
path = optional(string, null)
permissions_boundary = optional(string, null)
}), {})
}), {})
orchestration_provider = optional(object({
webhook = optional(object({
runner = optional(object({
boot_time_in_minutes = optional(number, null)
ephemeral = optional(bool, null)
jit_config_enabled = optional(bool, null)
maximum_count = optional(number, null)
}), {})
github = optional(object({
organization_runners = optional(bool, false)
}), {})
matcherConfig = optional(object({
labelMatchers = list(list(string))
exactMatch = optional(bool, false)
bidirectionalLabelMatch = optional(bool, false)
priority = optional(number, 999)
dynamic_labels_enabled = optional(bool, false)
awsDynamicLabelsPolicy = optional(object({
blocked_keys = optional(list(string), [])
restricted_keys = optional(map(object({
allowed = optional(list(string), [])
denied = optional(list(string), [])
max = optional(string, null)
})), {})
}), null)
}), null)
queue = optional(object({
delay_webhook_event = optional(number, null)
job_queue_retention_in_seconds = optional(number, null)
visibility_timeout_seconds = optional(number, null)
redrive_build_queue = optional(object({
enabled = optional(bool, null)
maxReceiveCount = optional(number, null)
}), null)
tags = optional(map(string), {})
}), {})
lambda = optional(object({
scale = optional(object({
up = optional(object({
memory_size = optional(number, null)
timeout = optional(number, null)
reserved_concurrent_executions = optional(number, null)
job_queued_check_enabled = optional(bool, null)
event_source_mapping = optional(object({
batch_size = optional(number, null)
maximum_batching_window_in_seconds = optional(number, null)
}), {})
tags = optional(map(string), {})
}), {})
down = optional(object({
memory_size = optional(number, null)
timeout = optional(number, null)
schedule_expression = optional(string, null)
minimum_running_time_in_minutes = optional(number, null)
idle_confirmation_seconds = optional(number, null)
idle_config = optional(list(object({
cron = string
timeZone = string
idleCount = number
evictionStrategy = optional(string, "oldest_first")
})), null)
tags = optional(map(string), {})
}), {})
}), {})
pool = optional(object({
memory_size = optional(number, null)
timeout = optional(number, null)
reserved_concurrent_executions = optional(number, null)
config = optional(list(object({
schedule_expression = string
schedule_expression_timezone = optional(string)
size = number
})), null)
include_busy_runners = optional(bool, null)
runner_owner = optional(string, null)
tags = optional(map(string), {})
}), {})
}), {})
job_retry = optional(object({
enabled = optional(bool, false)
delay_in_seconds = optional(number, 300)
delay_backoff = optional(number, 2)
max_attempts = optional(number, 1)
tags = optional(map(string), {})
lambda = optional(object({
memory_size = optional(number, 256)
reserved_concurrent_executions = optional(number, 1)
timeout = optional(number, 30)
}), {})
}), {})
}), null)
}), {})
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 = optional(object({
aws = optional(object({
ec2 = optional(object({
metadata_options = optional(object({
instance_metadata_tags = optional(string, "enabled")
http_endpoint = optional(string, "enabled")
http_tokens = optional(string, "required")
http_put_response_hop_limit = optional(number, 1)
}), {})
ami = optional(object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
id_ssm_parameter = optional(object({
arn = string
}), null)
kms_key = optional(object({
arn = string
}), null)
}), null)
block_device_mappings = optional(list(object({
delete_on_termination = optional(bool, true)
device_name = optional(string, "/dev/xvda")
encrypted = optional(bool, true)
iops = optional(number)
kms_key_id = optional(string)
snapshot_id = optional(string)
throughput = optional(number)
volume_initialization_rate = optional(number)
volume_size = number
volume_type = optional(string, "gp3")
})), [{ volume_size = 30 }])
create_service_linked_role_spot = optional(bool, false)
credit_specification = optional(string, null)
ebs_optimized = optional(bool, false)
cloudwatch_agent = optional(object({
enabled = optional(bool, true)
config = optional(string, null)
}), {})
binaries_syncer = optional(object({
enabled = optional(bool, null)
}), {})
detailed_monitoring_enabled = optional(bool, false)
ssm_enabled = optional(bool, false)
user_data = optional(object({
enabled = optional(bool, true)
template = optional(string, null)
content = optional(string, null)
pre_install = optional(string, "")
post_install = optional(string, "")
debug_logging_enabled = optional(bool, false)
}), {})
instance_allocation_strategy = optional(string, "lowest-price")
instance_max_spot_price = optional(string, null)
instance_target_capacity_type = optional(string, "spot")
instance_type_priorities = optional(map(number), null)
instance_types = optional(list(string), [])
additional_security_group_ids = optional(list(string), null)
managed_security_group_enabled = optional(bool, null)
egress_rules = optional(list(object({
cidr_blocks = list(string)
ipv6_cidr_blocks = list(string)
prefix_list_ids = list(string)
from_port = number
protocol = string
security_groups = list(string)
self = bool
to_port = number
description = string
})), null)
instance_profile_path = optional(string, null)
key_name = optional(string, null)
associate_public_ipv4_address = optional(bool, null)
instance_profile = optional(object({
name = string
}), null)
on_demand_failover_for_errors = optional(list(string), [])
scale_errors = optional(list(string), [
"UnfulfillableCapacity",
"MaxSpotInstanceCountExceeded",
"TargetCapacityLimitExceededException",
"RequestLimitExceeded",
"ResourceLimitExceeded",
"MaxSpotInstanceCountExceeded",
"MaxSpotFleetRequestCountExceeded",
"InsufficientInstanceCapacity",
"InsufficientCapacityOnHost",
])
subnet_ids = optional(list(string), null)
vpc_id = optional(string, null)
cpu_options = optional(object({
core_count = optional(number)
threads_per_core = optional(number)
amd_sev_snp = optional(string)
nested_virtualization = optional(string)
}), null)
placement = optional(object({
affinity = optional(string)
availability_zone = optional(string)
group_id = optional(string)
group_name = optional(string)
host_id = optional(string)
host_resource_group_arn = optional(string)
spread_domain = optional(string)
tenancy = optional(string)
partition_number = optional(number)
}), null)
license_specifications = optional(list(object({
license_configuration_arn = string
})), [])
use_dedicated_host = optional(bool, false)
log_files = optional(list(object({
log_group_name = string
prefix_log_group = bool
file_path = string
log_stream_name = string
log_class = optional(string, "STANDARD")
})), null)
tags = optional(map(string), {})
}), null)
}), {})
}), {})
})) | `{}` | no |
+| [multi\_runner\_config](#input\_multi\_runner\_config) | Accepts either the stable v1 runner configuration shape or the provider-boundary v2 shape. Entries with `runner_config` use the v1 shape; entries without `runner_config` use the v2 shape. A v2 entry does not need matcher configuration. A v2 entry must be acknowledged with `experimental_features = ["multi-runner-v2"]`; the v2 shape is experimental and may change before graduation.map(object({
# V1 contract
runner_config = optional(object({
runner_os = string
runner_architecture = string
runner_metadata_options = optional(map(any), {
instance_metadata_tags = "enabled"
http_endpoint = "enabled"
http_tokens = "required"
http_put_response_hop_limit = 1
})
ami = optional(object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
id_ssm_parameter_arn = optional(string, null)
kms_key_arn = optional(string, null)
}), null)
create_service_linked_role_spot = optional(bool, false)
credit_specification = optional(string, null)
delay_webhook_event = optional(number, 30)
disable_runner_autoupdate = optional(bool, false)
ebs_optimized = optional(bool, false)
enable_ephemeral_runners = optional(bool, false)
enable_job_queued_check = optional(bool, null)
enable_on_demand_failover_for_errors = optional(list(string), [])
scale_errors = optional(list(string), [
"UnfulfillableCapacity",
"MaxSpotInstanceCountExceeded",
"TargetCapacityLimitExceededException",
"RequestLimitExceeded",
"ResourceLimitExceeded",
"MaxSpotInstanceCountExceeded",
"MaxSpotFleetRequestCountExceeded",
"InsufficientInstanceCapacity",
"InsufficientCapacityOnHost",
])
enable_organization_runners = optional(bool, false)
enable_runner_binaries_syncer = optional(bool, true)
enable_ssm_on_runners = optional(bool, false)
enable_userdata = optional(bool, true)
instance_allocation_strategy = optional(string, "lowest-price")
instance_type_priorities = optional(map(number), null)
instance_max_spot_price = optional(string, null)
instance_target_capacity_type = optional(string, "spot")
instance_types = list(string)
job_queue_retention_in_seconds = optional(number, 86400)
minimum_running_time_in_minutes = optional(number, null)
pool_runner_owner = optional(string, null)
runner_as_root = optional(bool, false)
runner_boot_time_in_minutes = optional(number, 5)
scale_down_idle_confirmation_seconds = optional(number, 0)
runner_disable_default_labels = optional(bool, false)
runner_extra_labels = optional(list(string), [])
runner_group_name = optional(string, "Default")
runner_name_prefix = optional(string, "")
runner_run_as = optional(string, "ec2-user")
runners_maximum_count = number
runner_additional_security_group_ids = optional(list(string), [])
scale_down_schedule_expression = optional(string, "cron(*/5 * * * ? *)")
scale_up_reserved_concurrent_executions = optional(number, 1)
lambda_event_source_mapping_batch_size = optional(number, null)
lambda_event_source_mapping_maximum_batching_window_in_seconds = optional(number, null)
userdata_template = optional(string, null)
userdata_content = optional(string, null)
enable_jit_config = optional(bool, null)
enable_runner_detailed_monitoring = optional(bool, false)
enable_cloudwatch_agent = optional(bool, true)
cloudwatch_config = optional(string, null)
userdata_pre_install = optional(string, "")
userdata_post_install = optional(string, "")
runner_hook_job_started = optional(string, "")
runner_hook_job_completed = optional(string, "")
runner_ec2_tags = optional(map(string), {})
runner_iam_role_managed_policy_arns = optional(list(string), [])
vpc_id = optional(string, null)
subnet_ids = optional(list(string), null)
idle_config = optional(list(object({
cron = string
timeZone = string
idleCount = number
evictionStrategy = optional(string, "oldest_first")
})), [])
cpu_options = optional(object({
core_count = optional(number)
threads_per_core = optional(number)
amd_sev_snp = optional(string)
nested_virtualization = optional(string)
}), null)
placement = optional(object({
affinity = optional(string)
availability_zone = optional(string)
group_id = optional(string)
group_name = optional(string)
host_id = optional(string)
host_resource_group_arn = optional(string)
spread_domain = optional(string)
tenancy = optional(string)
partition_number = optional(number)
}), null)
license_specifications = optional(list(object({
license_configuration_arn = string
})), [])
use_dedicated_host = optional(bool, false)
runner_log_files = optional(list(object({
log_group_name = string
prefix_log_group = bool
file_path = string
log_stream_name = string
log_class = optional(string, "STANDARD")
})), null)
block_device_mappings = optional(list(object({
delete_on_termination = optional(bool, true)
device_name = optional(string, "/dev/xvda")
encrypted = optional(bool, true)
iops = optional(number)
kms_key_id = optional(string)
snapshot_id = optional(string)
throughput = optional(number)
volume_initialization_rate = optional(number)
volume_size = number
volume_type = optional(string, "gp3")
})), [{
volume_size = 30
}])
pool_config = optional(list(object({
schedule_expression = string
schedule_expression_timezone = optional(string)
size = number
})), [])
job_retry = optional(object({
enable = optional(bool, false)
delay_in_seconds = optional(number, 300)
delay_backoff = optional(number, 2)
lambda_memory_size = optional(number, 256)
lambda_timeout = optional(number, 30)
max_attempts = optional(number, 1)
}), {})
iam_overrides = optional(object({
override_instance_profile = optional(bool, null)
instance_profile_name = optional(string, null)
override_runner_role = optional(bool, null)
runner_role_arn = optional(string, null)
}), {
override_instance_profile = false
instance_profile_name = null
override_runner_role = false
runner_role_arn = null
})
}), null)
matcherConfig = optional(object({
labelMatchers = list(list(string))
exactMatch = optional(bool, false)
bidirectionalLabelMatch = optional(bool, false)
priority = optional(number, 999)
enableDynamicLabels = optional(bool, false)
awsDynamicLabelsPolicy = optional(any, null)
}), null)
redrive_build_queue = optional(object({
enabled = bool
maxReceiveCount = number
}), {
enabled = false
maxReceiveCount = null
})
# V2 Contract
tags = optional(map(string), {})
runner = optional(object({
os = optional(string, null)
architecture = optional(string, null)
disable_default_labels = optional(bool, null)
extra_labels = optional(list(string), null)
group_name = optional(string, null)
name_prefix = optional(string, null)
run_as_root = optional(bool, null)
run_as = optional(string, null)
auto_update_disabled = optional(bool, null)
tags = optional(map(string), {})
hooks = optional(object({
job_started = optional(string, null)
job_completed = optional(string, null)
}), {})
iam = optional(object({
role = optional(object({
arn = string
}), null)
managed_policy_arns = optional(map(string), null)
additional_trust_policy_json = optional(string, null)
path = optional(string, null)
permissions_boundary = optional(string, null)
}), {})
}), {})
lambda = optional(object({
runtime = optional(string, null)
architecture = optional(string, null)
subnet_ids = optional(list(string), null)
security_group_ids = optional(list(string), null)
tags = optional(map(string), {})
role = optional(object({
path = optional(string, null)
permissions_boundary = optional(string, null)
}), {})
}), {})
orchestration_provider = optional(object({
webhook = optional(object({
runner = optional(object({
boot_time_in_minutes = optional(number, null)
ephemeral = optional(bool, null)
jit_config_enabled = optional(bool, null)
maximum_count = optional(number, null)
}), {})
github = optional(object({
organization_runners = optional(bool, false)
}), {})
matcherConfig = optional(object({
labelMatchers = list(list(string))
exactMatch = optional(bool, false)
bidirectionalLabelMatch = optional(bool, false)
priority = optional(number, 999)
dynamic_labels_enabled = optional(bool, false)
awsDynamicLabelsPolicy = optional(object({
blocked_keys = optional(list(string), [])
restricted_keys = optional(map(object({
allowed = optional(list(string), [])
denied = optional(list(string), [])
max = optional(string, null)
})), {})
}), null)
}), null)
queue = optional(object({
delay_webhook_event = optional(number, null)
job_queue_retention_in_seconds = optional(number, null)
visibility_timeout_seconds = optional(number, null)
redrive_build_queue = optional(object({
enabled = optional(bool, null)
maxReceiveCount = optional(number, null)
}), null)
tags = optional(map(string), {})
}), {})
lambda = optional(object({
scale = optional(object({
up = optional(object({
memory_size = optional(number, null)
timeout = optional(number, null)
reserved_concurrent_executions = optional(number, null)
job_queued_check_enabled = optional(bool, null)
event_source_mapping = optional(object({
batch_size = optional(number, null)
maximum_batching_window_in_seconds = optional(number, null)
}), {})
tags = optional(map(string), {})
}), {})
down = optional(object({
memory_size = optional(number, null)
timeout = optional(number, null)
schedule_expression = optional(string, null)
minimum_running_time_in_minutes = optional(number, null)
idle_confirmation_seconds = optional(number, null)
idle_config = optional(list(object({
cron = string
timeZone = string
idleCount = number
evictionStrategy = optional(string, "oldest_first")
})), null)
tags = optional(map(string), {})
}), {})
}), {})
pool = optional(object({
memory_size = optional(number, null)
timeout = optional(number, null)
reserved_concurrent_executions = optional(number, null)
config = optional(list(object({
schedule_expression = string
schedule_expression_timezone = optional(string)
size = number
})), null)
include_busy_runners = optional(bool, null)
runner_owner = optional(string, null)
tags = optional(map(string), {})
}), {})
}), {})
job_retry = optional(object({
enabled = optional(bool, false)
delay_in_seconds = optional(number, 300)
delay_backoff = optional(number, 2)
max_attempts = optional(number, 1)
tags = optional(map(string), {})
lambda = optional(object({
memory_size = optional(number, 256)
reserved_concurrent_executions = optional(number, 1)
timeout = optional(number, 30)
}), {})
}), {})
}), null)
}), {})
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 = optional(object({
aws = optional(object({
ec2 = optional(object({
metadata_options = optional(object({
instance_metadata_tags = optional(string, "enabled")
http_endpoint = optional(string, "enabled")
http_tokens = optional(string, "required")
http_put_response_hop_limit = optional(number, 1)
}), {})
ami = optional(object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
id_ssm_parameter = optional(object({
arn = string
}), null)
kms_key = optional(object({
arn = string
}), null)
}), null)
block_device_mappings = optional(list(object({
delete_on_termination = optional(bool, true)
device_name = optional(string, "/dev/xvda")
encrypted = optional(bool, true)
iops = optional(number)
kms_key_id = optional(string)
snapshot_id = optional(string)
throughput = optional(number)
volume_initialization_rate = optional(number)
volume_size = number
volume_type = optional(string, "gp3")
})), [{ volume_size = 30 }])
create_service_linked_role_spot = optional(bool, false)
credit_specification = optional(string, null)
ebs_optimized = optional(bool, false)
cloudwatch_agent = optional(object({
enabled = optional(bool, true)
config = optional(string, null)
}), {})
binaries_syncer = optional(object({
enabled = optional(bool, null)
}), {})
detailed_monitoring_enabled = optional(bool, false)
ssm_enabled = optional(bool, false)
user_data = optional(object({
enabled = optional(bool, true)
template = optional(string, null)
content = optional(string, null)
pre_install = optional(string, "")
post_install = optional(string, "")
debug_logging_enabled = optional(bool, false)
}), {})
instance_allocation_strategy = optional(string, "lowest-price")
instance_max_spot_price = optional(string, null)
instance_target_capacity_type = optional(string, "spot")
instance_type_priorities = optional(map(number), null)
instance_types = optional(list(string), [])
additional_security_group_ids = optional(list(string), null)
managed_security_group_enabled = optional(bool, null)
egress_rules = optional(list(object({
cidr_blocks = list(string)
ipv6_cidr_blocks = list(string)
prefix_list_ids = list(string)
from_port = number
protocol = string
security_groups = list(string)
self = bool
to_port = number
description = string
})), null)
instance_profile_path = optional(string, null)
key_name = optional(string, null)
associate_public_ipv4_address = optional(bool, null)
instance_profile = optional(object({
name = string
}), null)
on_demand_failover_for_errors = optional(list(string), [])
scale_errors = optional(list(string), [
"UnfulfillableCapacity",
"MaxSpotInstanceCountExceeded",
"TargetCapacityLimitExceededException",
"RequestLimitExceeded",
"ResourceLimitExceeded",
"MaxSpotInstanceCountExceeded",
"MaxSpotFleetRequestCountExceeded",
"InsufficientInstanceCapacity",
"InsufficientCapacityOnHost",
])
subnet_ids = optional(list(string), null)
vpc_id = optional(string, null)
cpu_options = optional(object({
core_count = optional(number)
threads_per_core = optional(number)
amd_sev_snp = optional(string)
nested_virtualization = optional(string)
}), null)
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 |
| [parameter\_store\_tags](#input\_parameter\_store\_tags) | Map of tags that will be added to all the SSM Parameter Store parameters created by the Lambda function. | `map(string)` | `{}` | no |
| [pool\_lambda\_reserved\_concurrent\_executions](#input\_pool\_lambda\_reserved\_concurrent\_executions) | Amount of reserved concurrent executions for the scale-up lambda function. A value of 0 disables lambda from being triggered and -1 removes any concurrency limitations. | `number` | `1` | no |
| [pool\_lambda\_timeout](#input\_pool\_lambda\_timeout) | Time out for the pool lambda in seconds. | `number` | `60` | no |
diff --git a/modules/multi-runner/config.experimental.resolved.tf b/modules/multi-runner/config.experimental.resolved.tf
index fc29e81a8d..d737beb1dc 100644
--- a/modules/multi-runner/config.experimental.resolved.tf
+++ b/modules/multi-runner/config.experimental.resolved.tf
@@ -484,6 +484,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 301d37ec4d..915b2d9276 100644
--- a/modules/multi-runner/config.experimental.translation.tf
+++ b/modules/multi-runner/config.experimental.translation.tf
@@ -302,6 +302,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
+ }
+ }
+ }
}
}
@@ -554,6 +578,30 @@ locals {
log_files = v.runner_config.runner_log_files
tags = v.runner_config.runner_ec2_tags
}
+ 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
+ }
+ }
+ }
}
}
}
diff --git a/modules/multi-runner/tests/config-resolution.tftest.hcl b/modules/multi-runner/tests/config-resolution.tftest.hcl
index 40aa71a426..bfaf13d6ab 100644
--- a/modules/multi-runner/tests/config-resolution.tftest.hcl
+++ b/modules/multi-runner/tests/config-resolution.tftest.hcl
@@ -192,6 +192,9 @@ run "v1_stable_inputs_translate_into_effective_base" {
&& local.resolved_config.multi_runner_config["stable"].runner.group_name == "v1-lane"
&& local.resolved_config.multi_runner_config["stable"].orchestration_provider.webhook.runner.maximum_count == 2
&& toset(local.resolved_config.multi_runner_config["stable"].compute_provider.aws.ec2.instance_types) == toset(["m5.large"])
+ && local.resolved_config.multi_runner_config["stable"].compute_provider.aws.microvm.image_arn == null
+ && local.resolved_config.multi_runner_config["stable"].compute_provider.aws.microvm.cloudwatch_agent.enabled
+ && length(local.resolved_config.multi_runner_config["stable"].compute_provider.aws.microvm.environment_variables) == 0
&& toset(local.effective_config.multi_runner_config["stable"].runner.labels) == toset(["linux", "self-hosted", "x64"])
)
error_message = "Stable v1 inputs must translate into the effective experimental base without leaking v2 globals."
@@ -513,3 +516,126 @@ run "v2_inputs_reject_legacy_runner_config" {
}
}
}
+
+run "v2_microvm_inputs_route_to_microvm_provider" {
+ command = plan
+
+ variables {
+ global_config = {
+ runner = {
+ os = "linux"
+ architecture = "arm64"
+ }
+ }
+
+ global_config_github = {
+ app = {
+ key_base64 = "experimental-app-key"
+ id = "experimental-app-id"
+ webhook_secret = "experimental-webhook-secret"
+ }
+ }
+
+ global_config_lambda = {
+ artifact = {
+ s3 = {
+ bucket = "global-lambda-artifacts"
+ }
+ }
+ }
+
+ global_config_orchestration_provider = {
+ webhook = {
+ eventbridge = {
+ enabled = false
+ }
+ runner = {
+ ephemeral = true
+ jit_config_enabled = true
+ }
+ lambda = {
+ artifact = {
+ s3 = {
+ key = "global-runners.zip"
+ }
+ }
+ webhook = {
+ artifact = {
+ s3 = {
+ key = "global-webhook.zip"
+ }
+ }
+ }
+ }
+ }
+ }
+
+ global_config_ssm = {
+ housekeeper = {
+ lambda = {
+ artifact = {
+ s3 = {
+ key = "global-housekeeper.zip"
+ }
+ }
+ }
+ }
+ }
+
+ global_config_compute_provider = {
+ aws = {
+ microvm = {
+ image_arn = "arn:aws:lambda:eu-west-1:123456789012:microvm-image:global"
+ image_version = "7"
+ }
+ }
+ }
+
+ 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"
+ && local.effective_config.orchestration_provider.webhook.lambda.webhook.artifact.s3.key == "global-webhook.zip"
+ )
+ error_message = "Experimental MicroVM lanes must resolve Linux ARM64 settings, inherit global provider values, and place the webhook artifact key under lambda.webhook.artifact."
+ }
+
+ 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."
+ }
+}
\ No newline at end of file
diff --git a/modules/multi-runner/tests/config-translation.tftest.hcl b/modules/multi-runner/tests/config-translation.tftest.hcl
index f5a78a9b13..b444db3c1a 100644
--- a/modules/multi-runner/tests/config-translation.tftest.hcl
+++ b/modules/multi-runner/tests/config-translation.tftest.hcl
@@ -522,54 +522,6 @@ run "non_empty_v2_map_is_authoritative" {
}
-run "v2_entry_without_matcher_config_is_authoritative" {
- command = plan
-
- variables {
- experimental_features = ["multi-runner-v2"]
-
- global_config_compute_provider = {
- aws = {
- ec2 = {
- runner_binaries = {
- enabled = false
- }
- }
- }
- }
-
- multi_runner_config = {
- no_matcher = {
- runner = {
- os = "linux"
- architecture = "x64"
- }
- orchestration_provider = {
- webhook = {}
- }
- compute_provider = {
- aws = {
- ec2 = {
- vpc_id = "vpc-no-matcher"
- subnet_ids = ["subnet-no-matcher"]
- instance_types = ["m5.large"]
- }
- }
- }
- }
- }
- }
-
- assert {
- condition = (
- local.use_v2_config
- && toset(keys(local.normalized_config.multi_runner_config)) == toset(["no_matcher"])
- && try(local.normalized_config.multi_runner_config["no_matcher"].orchestration_provider.webhook.matcherConfig, null) == null
- )
- error_message = "A v2 runner entry must be recognized without requiring matcher configuration."
- }
-}
-
run "lane_values_override_experimental_globals" {
command = plan
diff --git a/modules/multi-runner/validations.tf b/modules/multi-runner/validations.tf
index 257d479b56..1f003de8ba 100644
--- a/modules/multi-runner/validations.tf
+++ b/modules/multi-runner/validations.tf
@@ -81,13 +81,45 @@ resource "terraform_data" "validate_v2" {
condition = alltrue([
for config in local.resolved_config.multi_runner_config : (
try(config.orchestration_provider.webhook != null, false) &&
- try(config.compute_provider.aws.ec2 != null, false) &&
- try(length(config.compute_provider.aws.ec2.instance_types) > 0, false) &&
- try(config.compute_provider.aws.ec2.vpc_id != null, false) &&
- try(length(config.compute_provider.aws.ec2.subnet_ids) > 0, false)
+ try(length(config.orchestration_provider.webhook.matcherConfig.labelMatchers) > 0, false)
)
])
- error_message = "Each experimental v2 runner lane requires a webhook provider, EC2 instance_types, vpc_id, and at least one subnet."
+ error_message = "Each experimental v2 runner lane requires a webhook matcher."
+ }
+
+ precondition {
+ condition = alltrue([
+ for config in local.resolved_config.multi_runner_config : length([
+ for provider_config in [
+ try(config.compute_provider.aws.ec2, null),
+ try(config.compute_provider.aws.microvm, null),
+ ] : provider_config if provider_config != null
+ ]) == 1
+ ])
+ error_message = "Each experimental v2 runner lane requires exactly one compute provider. Supported providers: aws.ec2, aws.microvm."
+ }
+
+ precondition {
+ condition = alltrue([
+ for config in local.resolved_config.multi_runner_config : (
+ !try(config.compute_provider.aws.ec2 != null, false) || (
+ try(length(config.compute_provider.aws.ec2.instance_types) > 0, false) &&
+ try(config.compute_provider.aws.ec2.vpc_id != null, false) &&
+ try(length(config.compute_provider.aws.ec2.subnet_ids) > 0, false)
+ )
+ )
+ ])
+ error_message = "Each experimental v2 EC2 runner lane requires instance_types, vpc_id, and at least one subnet."
+ }
+
+ precondition {
+ condition = alltrue([
+ for config in local.resolved_config.multi_runner_config : (
+ !try(config.compute_provider.aws.microvm != null, false) ||
+ try(config.compute_provider.aws.microvm.image_arn != null, false)
+ )
+ ])
+ error_message = "Each experimental v2 MicroVM runner lane requires image_arn."
}
}
}
diff --git a/modules/multi-runner/variables.experimental.compute-provider.tf b/modules/multi-runner/variables.experimental.compute-provider.tf
index 4780a156c3..1684ef8e74 100644
--- a/modules/multi-runner/variables.experimental.compute-provider.tf
+++ b/modules/multi-runner/variables.experimental.compute-provider.tf
@@ -199,6 +199,40 @@ variable "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.tf b/modules/multi-runner/variables.tf
index 793e126e4e..3f48967a32 100644
--- a/modules/multi-runner/variables.tf
+++ b/modules/multi-runner/variables.tf
@@ -570,6 +570,40 @@ variable "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/orchestration-providers/webhook/README.md b/modules/orchestration-providers/webhook/README.md
index 798f22ec8e..1264ceafa5 100644
--- a/modules/orchestration-providers/webhook/README.md
+++ b/modules/orchestration-providers/webhook/README.md
@@ -58,4 +58,4 @@ The scale-down lifecycle is documented in the [scale-down state diagram](./scale
| [runner\_lifecycle](#output\_runner\_lifecycle) | Effective webhook-owned runner lifecycle consumed by runner-config bootstrap parameters. |
| [scale\_down](#output\_scale\_down) | Scale-down control-plane resources. |
| [scale\_up](#output\_scale\_up) | Scale-up control-plane resources. |
-
+
\ No newline at end of file
diff --git a/modules/orchestration-providers/webhook/job-retry/README.md b/modules/orchestration-providers/webhook/job-retry/README.md
index 999535a4e2..b50585d5b6 100644
--- a/modules/orchestration-providers/webhook/job-retry/README.md
+++ b/modules/orchestration-providers/webhook/job-retry/README.md
@@ -60,4 +60,4 @@ No modules.
|------|-------------|
| [job\_retry\_check\_queue](#output\_job\_retry\_check\_queue) | Queue consumed by the job-retry Lambda. |
| [lambda](#output\_lambda) | Job-retry Lambda resources. |
-
+
\ No newline at end of file
diff --git a/modules/orchestration-providers/webhook/job-retry/variables.tf b/modules/orchestration-providers/webhook/job-retry/variables.tf
index 9b2c2610ab..c2144c70d4 100644
--- a/modules/orchestration-providers/webhook/job-retry/variables.tf
+++ b/modules/orchestration-providers/webhook/job-retry/variables.tf
@@ -149,4 +149,4 @@ variable "config" {
})
nullable = false
-}
+}
\ No newline at end of file
diff --git a/modules/orchestration-providers/webhook/pool/README.md b/modules/orchestration-providers/webhook/pool/README.md
index 03a9c0d15d..6a2d6934e4 100644
--- a/modules/orchestration-providers/webhook/pool/README.md
+++ b/modules/orchestration-providers/webhook/pool/README.md
@@ -63,4 +63,4 @@ No modules.
| Name | Description |
|------|-------------|
| [pool](#output\_pool) | Scheduled pool Lambda resources. |
-
+
\ No newline at end of file
diff --git a/modules/orchestration-providers/webhook/pool/variables.tf b/modules/orchestration-providers/webhook/pool/variables.tf
index d4c07fee63..9c229844dd 100644
--- a/modules/orchestration-providers/webhook/pool/variables.tf
+++ b/modules/orchestration-providers/webhook/pool/variables.tf
@@ -163,4 +163,4 @@ variable "tracing_config" {
capture_error = optional(bool, false)
})
default = {}
-}
+}
\ No newline at end of file
diff --git a/modules/orchestration-providers/webhook/scale-runners/README.md b/modules/orchestration-providers/webhook/scale-runners/README.md
index eee03f94da..1bd5dab141 100644
--- a/modules/orchestration-providers/webhook/scale-runners/README.md
+++ b/modules/orchestration-providers/webhook/scale-runners/README.md
@@ -76,4 +76,4 @@ No modules.
|------|-------------|
| [scale\_down](#output\_scale\_down) | Scale-down Lambda resources. |
| [scale\_up](#output\_scale\_up) | Scale-up Lambda resources. |
-
+
\ No newline at end of file
diff --git a/modules/orchestration-providers/webhook/scale-runners/variables.tf b/modules/orchestration-providers/webhook/scale-runners/variables.tf
index 4134b2e1b7..90a6a0b4c5 100644
--- a/modules/orchestration-providers/webhook/scale-runners/variables.tf
+++ b/modules/orchestration-providers/webhook/scale-runners/variables.tf
@@ -237,4 +237,4 @@ variable "runner_provider" {
})
nullable = false
-}
+}
\ No newline at end of file
diff --git a/modules/orchestration-providers/webhook/variables.tf b/modules/orchestration-providers/webhook/variables.tf
index 0e7ecd37c8..630c4c1a69 100644
--- a/modules/orchestration-providers/webhook/variables.tf
+++ b/modules/orchestration-providers/webhook/variables.tf
@@ -275,4 +275,4 @@ variable "runner_provider" {
})
})
nullable = false
-}
+}
\ No newline at end of file
diff --git a/modules/runner-config/README.md b/modules/runner-config/README.md
index 7c1585dca2..ed2e25ea47 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 |
@@ -109,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.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.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.object({
app_parameters = object({
key_base64 = map(string)
id = map(string)
additional_apps_manifest = optional(object({
name = string
arn = string
}), null)
additional_app_parameter_arns = optional(list(string), [])
})
enterprise_server = optional(object({
url = optional(string, null)
ssl_verify = optional(bool, true)
}), {})
user_agent = optional(string, null)
}) | n/a | yes |
| [lambda](#input\_lambda) | Common Lambda substrate independent of the selected runner orchestration provider.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 |
@@ -130,4 +140,4 @@ yarn run dist
| [runner](#output\_runner) | Common runner resources. The role is null when an external runner role is used. |
| [scale\_down](#output\_scale\_down) | Scale-down control-plane resources. Null when webhook orchestration is not configured. |
| [scale\_up](#output\_scale\_up) | Scale-up control-plane resources. Null when webhook orchestration is not configured. |
-
+
\ No newline at end of file
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/orchestration-provider.tf b/modules/runner-config/orchestration-provider.tf
index 16238fa6c6..42a7216ba0 100644
--- a/modules/runner-config/orchestration-provider.tf
+++ b/modules/runner-config/orchestration-provider.tf
@@ -70,4 +70,4 @@ module "orchestration_webhook" {
managed_policy_arn = local.provider_contract.policies.pool.managed_policy_arn
}
}
-}
+}
\ No newline at end of file
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 3e08eeb84b..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."
@@ -33,4 +37,8 @@ run "computed_external_values_keep_plan_shape_known" {
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 cf16de0bb5..a624bd03fe 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 b442e56166..c67ccf17b2 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 = "../../.."
@@ -212,6 +216,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
}
@@ -219,3 +315,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 5639c0d217..602ab9e273 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\"}]}"
@@ -176,7 +182,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."
}
@@ -194,6 +201,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."
@@ -655,3 +664,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/tests/tags.tftest.hcl b/modules/runner-config/tests/tags.tftest.hcl
index 509abfebfa..48d8be6476 100644
--- a/modules/runner-config/tests/tags.tftest.hcl
+++ b/modules/runner-config/tests/tags.tftest.hcl
@@ -366,4 +366,4 @@ run "layered_component_tags" {
})
error_message = "Job-retry tags must layer module, shared resource, and component tags with the component taking precedence."
}
-}
+}
\ No newline at end of file
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)
}), {})
})
diff --git a/modules/runner-config/variables.tf b/modules/runner-config/variables.tf
index 2273dc4769..8c7d785b44 100644
--- a/modules/runner-config/variables.tf
+++ b/modules/runner-config/variables.tf
@@ -248,4 +248,4 @@ variable "observability" {
})
default = {}
-}
+}
\ No newline at end of file