From 760fea3ced05ead0c4c098acb2828cc54ee9a1f1 Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Fri, 4 Sep 2026 23:17:04 +0200 Subject: [PATCH 1/4] feat(microvm): add foundation module example --- .github/workflows/terraform.yml | 4 +- docs/examples/index.md | 2 + docs/examples/microvm-foundation.md | 3 + docs/modules/public/microvm-foundation.md | 3 + .../microvm-foundation/.terraform.lock.hcl | 47 ++++++ examples/microvm-foundation/README.md | 77 +++++++++ examples/microvm-foundation/main.tf | 15 ++ examples/microvm-foundation/outputs.tf | 24 +++ examples/microvm-foundation/providers.tf | 4 + .../terraform.tfvars.example | 15 ++ examples/microvm-foundation/variables.tf | 79 +++++++++ examples/microvm-foundation/versions.tf | 10 ++ mkdocs.yaml | 1 + modules/microvm-foundation/README.md | 134 +++++++++++++++ modules/microvm-foundation/build.tf | 84 ++++++++++ modules/microvm-foundation/data.tf | 31 ++++ .../microvm-foundation/network_connector.tf | 31 ++++ .../network_connector_operator.tf | 41 +++++ .../network_connector_security_group.tf | 40 +++++ modules/microvm-foundation/outputs.tf | 34 ++++ modules/microvm-foundation/storage.tf | 103 ++++++++++++ modules/microvm-foundation/usage_policy.tf | 58 +++++++ modules/microvm-foundation/variables.tf | 152 ++++++++++++++++++ modules/microvm-foundation/versions.tf | 14 ++ 24 files changed, 1005 insertions(+), 1 deletion(-) create mode 100644 docs/examples/microvm-foundation.md create mode 100644 docs/modules/public/microvm-foundation.md create mode 100644 examples/microvm-foundation/.terraform.lock.hcl create mode 100644 examples/microvm-foundation/README.md create mode 100644 examples/microvm-foundation/main.tf create mode 100644 examples/microvm-foundation/outputs.tf create mode 100644 examples/microvm-foundation/providers.tf create mode 100644 examples/microvm-foundation/terraform.tfvars.example create mode 100644 examples/microvm-foundation/variables.tf create mode 100644 examples/microvm-foundation/versions.tf create mode 100644 modules/microvm-foundation/README.md create mode 100644 modules/microvm-foundation/build.tf create mode 100644 modules/microvm-foundation/data.tf create mode 100644 modules/microvm-foundation/network_connector.tf create mode 100644 modules/microvm-foundation/network_connector_operator.tf create mode 100644 modules/microvm-foundation/network_connector_security_group.tf create mode 100644 modules/microvm-foundation/outputs.tf create mode 100644 modules/microvm-foundation/storage.tf create mode 100644 modules/microvm-foundation/usage_policy.tf create mode 100644 modules/microvm-foundation/variables.tf create mode 100644 modules/microvm-foundation/versions.tf diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml index 405f05388a..f89bdfe742 100644 --- a/.github/workflows/terraform.yml +++ b/.github/workflows/terraform.yml @@ -87,6 +87,7 @@ jobs: "multi-runner", "compute-providers/aws/microvm", "compute-providers/aws/microvm/trust-policy", + "microvm-foundation", "runner-binaries-syncer", "runners", "setup-iam-permissions", @@ -158,7 +159,8 @@ jobs: "termination-watcher", "multi-runner", "multi-runner-v2", - "external-managed-ssm-secrets" + "external-managed-ssm-secrets", + "microvm-foundation" ] defaults: run: diff --git a/docs/examples/index.md b/docs/examples/index.md index f0558966bd..b7bdf60811 100644 --- a/docs/examples/index.md +++ b/docs/examples/index.md @@ -11,3 +11,5 @@ Examples are located in the [examples](https://github.com/github-aws-runners/ter - _[Termination watcher](termination-watcher.md)_: Example usages of termination watcher. - _[Dedicated Mac Hosts](dedicated-mac-hosts.md)_: Example usage of setting up dedicated hosts for macOS runners. - _[Externally managed SSM secrets](external-managed-ssm-secrets.md)_: Example usage of externally managed SSM secrets for the GitHub App credentials. +- _[MicroVM foundation](microvm-foundation.md)_: Example usage of the regional Lambda MicroVM image-build and Network Connector prerequisites. +- _[Lambda MicroVM](microvm.md)_: Example usage of Linux ARM64 ephemeral runners backed by Lambda MicroVM images. diff --git a/docs/examples/microvm-foundation.md b/docs/examples/microvm-foundation.md new file mode 100644 index 0000000000..b92a148a9c --- /dev/null +++ b/docs/examples/microvm-foundation.md @@ -0,0 +1,3 @@ +# Lambda MicroVM foundation + +--8<-- "examples/microvm-foundation/README.md" diff --git a/docs/modules/public/microvm-foundation.md b/docs/modules/public/microvm-foundation.md new file mode 100644 index 0000000000..17129c131e --- /dev/null +++ b/docs/modules/public/microvm-foundation.md @@ -0,0 +1,3 @@ +# Lambda MicroVM regional foundation + +--8<-- "modules/microvm-foundation/README.md" diff --git a/examples/microvm-foundation/.terraform.lock.hcl b/examples/microvm-foundation/.terraform.lock.hcl new file mode 100644 index 0000000000..2fc14d932b --- /dev/null +++ b/examples/microvm-foundation/.terraform.lock.hcl @@ -0,0 +1,47 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.63.0" + constraints = ">= 6.61.0" + hashes = [ + "h1:dRlYHkc+r6fgzF57WC7Zjcmb6sF/6TTGDEgwGK+LAZY=", + "zh:005d56736afd17d963998c405cee6f434dbc23a415109f9435ff1542879ae611", + "zh:026ef126321a86ad7080b5d858e2527f96f5289678cbcd8856296e229c43339d", + "zh:06e0b58b2d1eddb5137fc86bee7ad2d07953c0bc3f57cccfc5ae0d2456068a3a", + "zh:07221735d61ababed84734e5ffcfc5bd59d01f29f029166ba5f2175895dceed1", + "zh:1a72db00583112bdb8c19b213a78a3f5de754fffc08f07e061f4e326289fab7d", + "zh:32968e74a53b03e97a084dc7050c22ef661fb5b3ea8a44f5a63e47bc45ad0e7c", + "zh:4b357dfe4b820e3e4acd2881cff8288b2186491e63416751f0d12692ba478ceb", + "zh:81e30884d7de686265e7d87bb92527e802878c65a378470ede2a1e9f4e40ccc9", + "zh:82e137297f6a5a08b9ce2138f7aabea245ad99495d9d9eff502f752d6ca90dbd", + "zh:8eb83b67099f0ea9df238a979dff933ff50ce06a2e3ff05a48556a10f10dd204", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:d0ba30886cbe41850fee689f51ef9088578f323cfd21817bb409951d43c465eb", + "zh:dd48e7089784454bc03d713e9057f5ca0ea1613bd402125054a51894957b7925", + "zh:f250fa81e54cf60fcb0e9c0fc4ac043f1ecc2ac24967f628b3609364fcab3d04", + "zh:f38fc09fc25a8d2cf89a4d4cd6a5ef7cb1aad72798dbdcad58b8876b6a551a54", + "zh:f7c7380fdf126e1901f2084588dbfd724c76cb131ccfa795a541219111103c06", + ] +} + +provider "registry.terraform.io/hashicorp/time" { + version = "0.14.1" + constraints = ">= 0.13.0" + hashes = [ + "h1:r93SxP++6gUlwCHDQ5OkRmcU8B0yv6ZA9nF0Dh6NJmA=", + "zh:0837ca5b057e5cff94dff7de2fcccafb4abaa33c45de193fe2853e684818a267", + "zh:15a122f72d9e0f34fc5384cc7ec089319641fee5c319748a3aa02fc42f459969", + "zh:342fb83093a280ea7ee0654feae1f5867c62eb8eebc1ab46f9a7ab0b4c878a62", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:99f169834d3370b8341381c6a9c7a8b01fb26027531faa38e6fb49cc23916f68", + "zh:9f482917c7a28cf2436578be7aa9f04f8c811aba8b5949e0223ea987a2757a91", + "zh:ac6b5b8732826f2d1129a8a4a038ac7a7a9ca7b77d2a4608e5703be1a1e2bff0", + "zh:c54782a27d58ce04f6696c6fc0b2cf1e2fba6bed239fb520521a7bce7d7193cb", + "zh:c8d0ddc8f575ecb44f025d54edbfe118e26397fe328a67be62325766f31eb6e7", + "zh:d043b96f204edd2353bf6b2a34e645ffdee2e9634d9bb747331320444810a538", + "zh:e32c288501ca9a6c9d22b52e839dd391fc7083d54ee6b8dc296ce0e6bd3e57ef", + "zh:e47fcc7bb4e9ab5cc522c3b06e4fa9c0bf94b84be8210bc6b1655c44acb2addc", + "zh:f61bf218322bcbe0bd2d56bba738e7fa485e9b54244e13aa12de741b37d450c0", + ] +} diff --git a/examples/microvm-foundation/README.md b/examples/microvm-foundation/README.md new file mode 100644 index 0000000000..aa54e94fa0 --- /dev/null +++ b/examples/microvm-foundation/README.md @@ -0,0 +1,77 @@ +# MicroVM foundation example + +This example creates the regional dependencies required by the Lambda MicroVM +image build and runner runtime using the reusable module in this repository. + +Set real VPC and subnet IDs in `terraform.tfvars` (copy +`terraform.tfvars.example`). The module validates that every selected subnet +belongs to its configured VPC. + +```bash +terraform init +terraform apply +terraform output +``` + +Apply this foundation before building an image with the direct Packer commands +documented in `../../images/microvm/README.md`. Use the outputs as the build inputs: + +- `artifact_bucket_name` -> `MICROVM_ARTIFACT_BUCKET` +- `build_role_arn` -> `MICROVM_BUILD_ROLE_ARN` +- `connector_arns.cicd` -> `MICROVM_EGRESS_NETWORK_CONNECTOR_ARN` +- `usage_policy_arn` -> attach to the control-plane role used by the runner example + +The foundation module owns regional storage, build IAM, Network Connectors, +and the reusable runtime policy. It does not publish an image or create the +runner control plane; those steps remain explicit and can be performed after +the foundation is available. + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.4.0 | +| [aws](#requirement\_aws) | >= 6.61 | + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [microvm\_foundation](#module\_microvm\_foundation) | ../../modules/microvm-foundation | n/a | + +## Resources + +No resources. + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [artifact\_bucket\_name](#input\_artifact\_bucket\_name) | Optional globally unique S3 bucket name. When null, AWS generates the bucket name. | `string` | `null` | no | +| [artifact\_retention\_days](#input\_artifact\_retention\_days) | Number of days to retain current and noncurrent build artifacts. | `number` | `30` | no | +| [aws\_profile](#input\_aws\_profile) | Optional local AWS CLI profile. Leave null when credentials are provided by the environment or role. | `string` | `null` | no | +| [aws\_region](#input\_aws\_region) | AWS region in which to create the MicroVM foundation. | `string` | `"eu-west-1"` | no | +| [build\_policy\_name\_prefix](#input\_build\_policy\_name\_prefix) | Name prefix for the Lambda MicroVM build policy. | `string` | `"github-actions-runner-microvm-build-policy-"` | no | +| [build\_role\_name\_prefix](#input\_build\_role\_name\_prefix) | Name prefix for the Lambda MicroVM build role. | `string` | `"github-actions-runner-microvm-build-"` | no | +| [ecr\_repository\_arns](#input\_ecr\_repository\_arns) | Optional private ECR repository ARNs used by the image build. | `set(string)` | `[]` | no | +| [image\_name\_prefix](#input\_image\_name\_prefix) | Reserved Lambda MicroVM image-name namespace used by the runtime policy. | `string` | `"github-actions-runner-ubuntu-arm64"` | no | +| [network\_connector\_operator\_role\_name\_prefix](#input\_network\_connector\_operator\_role\_name\_prefix) | Name prefix for the Lambda Network Connector operator role. | `string` | `"github-actions-runner-microvm-network-operator-"` | no | +| [network\_connectors](#input\_network\_connectors) | VPC and subnet configuration for regional Lambda MicroVM egress connectors. |
map(object({
name = string
vpc_id = string
subnet_ids = set(string)
network_protocol = optional(string, "IPv4")
}))
| n/a | yes | +| [tags](#input\_tags) | Additional tags applied by the foundation module. | `map(string)` |
{
"Component": "microvm-foundation"
}
| no | +| [usage\_policy\_name\_prefix](#input\_usage\_policy\_name\_prefix) | Name prefix for the Lambda MicroVM runtime usage policy. | `string` | `"github-actions-runner-microvm-runtime-usage-policy-"` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [artifact\_bucket\_name](#output\_artifact\_bucket\_name) | S3 bucket to pass to the MicroVM image build. | +| [artifact\_prefix](#output\_artifact\_prefix) | S3 prefix used for MicroVM build artifacts. | +| [build\_role\_arn](#output\_build\_role\_arn) | Lambda build role ARN to pass to the image build. | +| [connector\_arns](#output\_connector\_arns) | Regional Network Connector ARNs keyed by configuration name. | +| [usage\_policy\_arn](#output\_usage\_policy\_arn) | Unattached runtime usage policy for the runner control-plane role. | + diff --git a/examples/microvm-foundation/main.tf b/examples/microvm-foundation/main.tf new file mode 100644 index 0000000000..27ed22c816 --- /dev/null +++ b/examples/microvm-foundation/main.tf @@ -0,0 +1,15 @@ +module "microvm_foundation" { + source = "../../modules/microvm-foundation" + + aws_region = var.aws_region + tags = var.tags + build_policy_name_prefix = var.build_policy_name_prefix + build_role_name_prefix = var.build_role_name_prefix + network_connector_operator_role_name_prefix = var.network_connector_operator_role_name_prefix + usage_policy_name_prefix = var.usage_policy_name_prefix + artifact_bucket_name = var.artifact_bucket_name + artifact_retention_days = var.artifact_retention_days + image_name_prefix = var.image_name_prefix + ecr_repository_arns = var.ecr_repository_arns + network_connectors = var.network_connectors +} diff --git a/examples/microvm-foundation/outputs.tf b/examples/microvm-foundation/outputs.tf new file mode 100644 index 0000000000..709d43f933 --- /dev/null +++ b/examples/microvm-foundation/outputs.tf @@ -0,0 +1,24 @@ +output "artifact_bucket_name" { + description = "S3 bucket to pass to the MicroVM image build." + value = module.microvm_foundation.artifact_bucket_name +} + +output "artifact_prefix" { + description = "S3 prefix used for MicroVM build artifacts." + value = module.microvm_foundation.artifact_prefix +} + +output "build_role_arn" { + description = "Lambda build role ARN to pass to the image build." + value = module.microvm_foundation.build_role_arn +} + +output "connector_arns" { + description = "Regional Network Connector ARNs keyed by configuration name." + value = module.microvm_foundation.connector_arns +} + +output "usage_policy_arn" { + description = "Unattached runtime usage policy for the runner control-plane role." + value = module.microvm_foundation.usage_policy_arn +} diff --git a/examples/microvm-foundation/providers.tf b/examples/microvm-foundation/providers.tf new file mode 100644 index 0000000000..9e8a8a7627 --- /dev/null +++ b/examples/microvm-foundation/providers.tf @@ -0,0 +1,4 @@ +provider "aws" { + region = var.aws_region + profile = var.aws_profile +} diff --git a/examples/microvm-foundation/terraform.tfvars.example b/examples/microvm-foundation/terraform.tfvars.example new file mode 100644 index 0000000000..ef864c384c --- /dev/null +++ b/examples/microvm-foundation/terraform.tfvars.example @@ -0,0 +1,15 @@ +aws_region = "eu-west-1" + +network_connectors = { + cicd = { + name = "github-actions-runner-egress" + vpc_id = "vpc-0123456789abcdef0" + subnet_ids = ["subnet-0123456789abcdef0", "subnet-0fedcba9876543210"] + } +} + +# Add the private ECR repository that contains the regional Ubuntu base image +# when the image build pulls from ECR. +# ecr_repository_arns = [ +# "arn:aws:ecr:eu-west-1:123456789012:repository/actions-runner-base-image", +# ] diff --git a/examples/microvm-foundation/variables.tf b/examples/microvm-foundation/variables.tf new file mode 100644 index 0000000000..4afd3804a5 --- /dev/null +++ b/examples/microvm-foundation/variables.tf @@ -0,0 +1,79 @@ +variable "aws_profile" { + type = string + description = "Optional local AWS CLI profile. Leave null when credentials are provided by the environment or role." + default = null + nullable = true +} + +variable "aws_region" { + type = string + description = "AWS region in which to create the MicroVM foundation." + default = "eu-west-1" +} + +variable "tags" { + type = map(string) + description = "Additional tags applied by the foundation module." + default = { + Component = "microvm-foundation" + } +} + +variable "build_policy_name_prefix" { + type = string + description = "Name prefix for the Lambda MicroVM build policy." + default = "github-actions-runner-microvm-build-policy-" +} + +variable "usage_policy_name_prefix" { + type = string + description = "Name prefix for the Lambda MicroVM runtime usage policy." + default = "github-actions-runner-microvm-runtime-usage-policy-" +} + +variable "build_role_name_prefix" { + type = string + description = "Name prefix for the Lambda MicroVM build role." + default = "github-actions-runner-microvm-build-" +} + +variable "network_connector_operator_role_name_prefix" { + type = string + description = "Name prefix for the Lambda Network Connector operator role." + default = "github-actions-runner-microvm-network-operator-" +} + +variable "artifact_bucket_name" { + type = string + description = "Optional globally unique S3 bucket name. When null, AWS generates the bucket name." + default = null + nullable = true +} + +variable "artifact_retention_days" { + type = number + description = "Number of days to retain current and noncurrent build artifacts." + default = 30 +} + +variable "image_name_prefix" { + type = string + description = "Reserved Lambda MicroVM image-name namespace used by the runtime policy." + default = "github-actions-runner-ubuntu-arm64" +} + +variable "ecr_repository_arns" { + type = set(string) + description = "Optional private ECR repository ARNs used by the image build." + default = [] +} + +variable "network_connectors" { + type = map(object({ + name = string + vpc_id = string + subnet_ids = set(string) + network_protocol = optional(string, "IPv4") + })) + description = "VPC and subnet configuration for regional Lambda MicroVM egress connectors." +} diff --git a/examples/microvm-foundation/versions.tf b/examples/microvm-foundation/versions.tf new file mode 100644 index 0000000000..e72a26b153 --- /dev/null +++ b/examples/microvm-foundation/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.4.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 6.61" + } + } +} diff --git a/mkdocs.yaml b/mkdocs.yaml index f9680b9d2b..0188c772eb 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -67,6 +67,7 @@ nav: - AMI Housekeeper: modules/public/ami-housekeeper.md - Lambda Downloader: modules/public/download-lambda.md - Setup IAM permissions: modules/public/setup-iam-permissions.md + - MicroVM foundation: modules/public/microvm-foundation.md - Submodules (internal): - Runners: modules/internal/runners.md - Syncer: modules/internal/runner-binaries-syncer.md diff --git a/modules/microvm-foundation/README.md b/modules/microvm-foundation/README.md new file mode 100644 index 0000000000..fadbedc0f7 --- /dev/null +++ b/modules/microvm-foundation/README.md @@ -0,0 +1,134 @@ +# Lambda MicroVM Regional Foundation + +This module creates the regional AWS prerequisites for building and running +Lambda MicroVM GitHub Actions runners and is intended to be deployed once per +AWS Region. + +It manages: + +- A private, encrypted, versioned S3 bucket for content-addressed image build artifacts. +- A Lambda-trusted build role with scoped S3, CloudWatch Logs, and optional ECR pull access. +- Dedicated no-ingress security groups and native Lambda Network Connector resources for each configured VPC/subnet set. +- A Lambda-trusted Network Connector operator role and propagation barrier. +- An unattached runtime usage policy for the reserved image namespace and connector inventory. + +The module does not create MicroVM images, runner execution roles, or the +runner control plane. Attach `usage_policy_arn` to the control-plane role that +owns the runtime launch operations. The caller must also grant the Terraform +identity `iam:PassRole` for the operator role with +`iam:PassedToService=lambda.amazonaws.com`. + +The module deliberately does not configure an AWS provider. Configure the +provider in the root module or example so credentials and account selection +remain caller-owned. + +```hcl +provider "aws" { + region = "eu-west-1" +} + +module "microvm_foundation" { + source = "../../modules/microvm-foundation" + + aws_region = "eu-west-1" + tags = { Environment = "example" } + build_policy_name_prefix = "github-actions-runner-microvm-build-policy-" + build_role_name_prefix = "github-actions-runner-microvm-build-" + network_connector_operator_role_name_prefix = "github-actions-runner-microvm-network-operator-" + usage_policy_name_prefix = "github-actions-runner-microvm-runtime-usage-policy-" + + image_name_prefix = "github-actions-runner-ubuntu-arm64" + + network_connectors = { + cicd = { + name = "github-actions-runner-egress" + vpc_id = "vpc-0123456789abcdef0" + subnet_ids = ["subnet-0123456789abcdef0"] + } + } +} +``` + +The companion `examples/microvm-foundation` directory is a complete setup +example. Apply it before following the direct Packer build instructions in +`images/microvm/README.md` or using the `examples/microvm` runner example. + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.4.0 | +| [aws](#requirement\_aws) | >= 6.61 | +| [time](#requirement\_time) | >= 0.13 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 6.61 | +| [time](#provider\_time) | >= 0.13 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [aws_iam_policy.build](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.usage](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_role.build](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role.operator](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role_policy_attachment.build](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.operator](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_lambdacore_network_connector.connector](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambdacore_network_connector) | resource | +| [aws_s3_bucket.artifacts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket) | resource | +| [aws_s3_bucket_lifecycle_configuration.artifacts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_lifecycle_configuration) | resource | +| [aws_s3_bucket_ownership_controls.artifacts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_ownership_controls) | resource | +| [aws_s3_bucket_policy.artifacts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_policy) | resource | +| [aws_s3_bucket_public_access_block.artifacts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource | +| [aws_s3_bucket_server_side_encryption_configuration.artifacts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_server_side_encryption_configuration) | resource | +| [aws_s3_bucket_versioning.artifacts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_versioning) | resource | +| [aws_security_group.connector](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | +| [aws_vpc_security_group_egress_rule.ipv4](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_egress_rule) | resource | +| [aws_vpc_security_group_egress_rule.ipv6](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_egress_rule) | resource | +| [time_sleep.operator_role_propagation](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/sleep) | resource | +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_iam_policy_document.artifact_bucket](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.build](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.lambda_service_assume_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.network_connector_assume_operator_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.usage](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | +| [aws_subnet.selected](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/subnet) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [artifact\_bucket\_name](#input\_artifact\_bucket\_name) | Optional name for the regional MicroVM build-artifact bucket. When null, AWS generates the bucket name. | `string` | `null` | no | +| [artifact\_retention\_days](#input\_artifact\_retention\_days) | Number of days to retain current and noncurrent MicroVM build artifacts. | `number` | `30` | no | +| [aws\_region](#input\_aws\_region) | AWS region in which to create the Lambda MicroVM prerequisites. | `string` | n/a | yes | +| [build\_policy\_name\_prefix](#input\_build\_policy\_name\_prefix) | Name prefix for the Lambda MicroVM build policy. | `string` | n/a | yes | +| [build\_role\_name\_prefix](#input\_build\_role\_name\_prefix) | Name prefix for the Lambda MicroVM build role. | `string` | n/a | yes | +| [ecr\_repository\_arns](#input\_ecr\_repository\_arns) | Optional regional ECR repository ARNs from which MicroVM image builds can pull runner base images. | `set(string)` | `[]` | no | +| [image\_name\_prefix](#input\_image\_name\_prefix) | IAM namespace prefix reserved for externally published Lambda MicroVM image names. This module does not create or enumerate images. | `string` | n/a | yes | +| [network\_connector\_operator\_role\_name\_prefix](#input\_network\_connector\_operator\_role\_name\_prefix) | Name prefix for the Lambda Network Connector operator role. | `string` | n/a | yes | +| [network\_connectors](#input\_network\_connectors) | Regional Lambda MicroVM Network Connectors keyed by a stable consumer-defined identity. |
map(object({
name = string
vpc_id = string
subnet_ids = set(string)
network_protocol = optional(string, "IPv4")
}))
| n/a | yes | +| [tags](#input\_tags) | A map of module-specific tags to apply to resources. | `map(string)` | n/a | yes | +| [usage\_policy\_name\_prefix](#input\_usage\_policy\_name\_prefix) | Name prefix for the Lambda MicroVM runtime usage policy. | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [artifact\_bucket\_arn](#output\_artifact\_bucket\_arn) | ARN of the regional S3 bucket used for Lambda MicroVM build artifacts. | +| [artifact\_bucket\_name](#output\_artifact\_bucket\_name) | Name of the regional S3 bucket used for Lambda MicroVM build artifacts. | +| [artifact\_prefix](#output\_artifact\_prefix) | Bucket prefix to which the MicroVM image publisher uploads content-addressed build artifacts. | +| [build\_role\_arn](#output\_build\_role\_arn) | ARN of the Lambda-trusted role used during MicroVM image builds. | +| [connector\_arns](#output\_connector\_arns) | Map of connector key to the ARN of each Lambda Network Connector. | +| [security\_group\_ids](#output\_security\_group\_ids) | Map of connector key to its dedicated no-ingress security group ID. | +| [usage\_policy\_arn](#output\_usage\_policy\_arn) | ARN of the reusable regional policy for operating MicroVM images in the reserved namespace and passing their Network Connectors. | + diff --git a/modules/microvm-foundation/build.tf b/modules/microvm-foundation/build.tf new file mode 100644 index 0000000000..160f3d9399 --- /dev/null +++ b/modules/microvm-foundation/build.tf @@ -0,0 +1,84 @@ +# Lambda assumes this role while building an image snapshot. +data "aws_iam_policy_document" "build" { + statement { + sid = "ReadRegionalBuildArtifact" + effect = "Allow" + actions = ["s3:GetObject"] + resources = ["${aws_s3_bucket.artifacts.arn}/${local.artifact_prefix}/*"] + } + + statement { + sid = "CreateMicrovmBuildLogGroups" + effect = "Allow" + actions = ["logs:CreateLogGroup"] + resources = [local.log_group_arn_pattern] + } + + statement { + sid = "WriteMicrovmBuildLogs" + effect = "Allow" + actions = [ + "logs:CreateLogStream", + "logs:PutLogEvents", + ] + resources = [local.log_stream_arn_pattern] + } + + dynamic "statement" { + for_each = length(var.ecr_repository_arns) > 0 ? [true] : [] + content { + sid = "AuthorizePrivateEcrPull" + effect = "Allow" + actions = ["ecr:GetAuthorizationToken"] + resources = ["*"] + } + } + + dynamic "statement" { + for_each = length(var.ecr_repository_arns) > 0 ? [true] : [] + content { + sid = "PullPrivateEcrImage" + effect = "Allow" + actions = [ + "ecr:BatchCheckLayerAvailability", + "ecr:BatchGetImage", + "ecr:GetDownloadUrlForLayer", + ] + resources = var.ecr_repository_arns + } + } +} + +resource "aws_iam_policy" "build" { + name_prefix = var.build_policy_name_prefix + description = "Regional permissions used by Lambda while building MicroVM images." + policy = data.aws_iam_policy_document.build.json + tags = var.tags +} + +resource "aws_iam_role_policy_attachment" "build" { + role = aws_iam_role.build.name + policy_arn = aws_iam_policy.build.arn +} + +data "aws_iam_policy_document" "lambda_service_assume_role" { + statement { + sid = "LambdaMicrovmService" + effect = "Allow" + actions = [ + "sts:AssumeRole", + "sts:TagSession", + ] + + principals { + type = "Service" + identifiers = ["lambda.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "build" { + name_prefix = var.build_role_name_prefix + assume_role_policy = data.aws_iam_policy_document.lambda_service_assume_role.json + tags = var.tags +} diff --git a/modules/microvm-foundation/data.tf b/modules/microvm-foundation/data.tf new file mode 100644 index 0000000000..dc90004cc6 --- /dev/null +++ b/modules/microvm-foundation/data.tf @@ -0,0 +1,31 @@ +data "aws_caller_identity" "current" {} + +data "aws_partition" "current" {} + +data "aws_subnet" "selected" { + for_each = local.network_connector_subnets + id = each.value.subnet_id +} + +locals { + artifact_prefix = "lambda-microvms" + + image_arn_pattern = "arn:${data.aws_partition.current.partition}:lambda:${var.aws_region}:${data.aws_caller_identity.current.account_id}:microvm-image:${var.image_name_prefix}-*" + log_group_arn_pattern = "arn:${data.aws_partition.current.partition}:logs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:log-group:/aws/lambda/microvms/${var.image_name_prefix}-*" + log_stream_arn_pattern = "${local.log_group_arn_pattern}:log-stream:*" + + network_connector_subnets = merge({}, [ + for connector_key, connector in var.network_connectors : { + for subnet_id in connector.subnet_ids : + "${connector_key}/${subnet_id}" => { + connector_key = connector_key + subnet_id = subnet_id + } + } + ]...) + + connector_arns = { + for connector_key, connector in aws_lambdacore_network_connector.connector : + connector_key => connector.arn + } +} diff --git a/modules/microvm-foundation/network_connector.tf b/modules/microvm-foundation/network_connector.tf new file mode 100644 index 0000000000..3e41d161b3 --- /dev/null +++ b/modules/microvm-foundation/network_connector.tf @@ -0,0 +1,31 @@ +resource "aws_lambdacore_network_connector" "connector" { + for_each = var.network_connectors + + name = each.value.name + operator_role = aws_iam_role.operator.arn + + configuration { + vpc_egress_configuration { + associated_compute_resource_types = ["MicroVm"] + network_protocol = each.value.network_protocol + security_group_ids = [aws_security_group.connector[each.key].id] + subnet_ids = sort(tolist(each.value.subnet_ids)) + } + } + + lifecycle { + precondition { + condition = alltrue([ + for subnet_id in each.value.subnet_ids : + data.aws_subnet.selected["${each.key}/${subnet_id}"].vpc_id == each.value.vpc_id + ]) + error_message = "Every subnet in network_connectors[${each.key}] must belong to its configured vpc_id." + } + } + + depends_on = [ + time_sleep.operator_role_propagation, + aws_vpc_security_group_egress_rule.ipv4, + aws_vpc_security_group_egress_rule.ipv6, + ] +} diff --git a/modules/microvm-foundation/network_connector_operator.tf b/modules/microvm-foundation/network_connector_operator.tf new file mode 100644 index 0000000000..fc7d03fd74 --- /dev/null +++ b/modules/microvm-foundation/network_connector_operator.tf @@ -0,0 +1,41 @@ +data "aws_iam_policy_document" "network_connector_assume_operator_role" { + statement { + sid = "LambdaNetworkConnectorService" + effect = "Allow" + actions = ["sts:AssumeRole"] + + principals { + type = "Service" + identifiers = ["network-connectors.lambda.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "operator" { + name_prefix = var.network_connector_operator_role_name_prefix + assume_role_policy = data.aws_iam_policy_document.network_connector_assume_operator_role.json + tags = var.tags +} + +resource "aws_iam_role_policy_attachment" "operator" { + role = aws_iam_role.operator.name + policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/AWSLambdaNetworkConnectorOperatorPolicy" +} + +# IAM reports role and policy writes before they are consistently available to +# Lambda. Wait before allowing the native Network Connector resource to create +# any connector. +resource "time_sleep" "operator_role_propagation" { + depends_on = [aws_iam_role_policy_attachment.operator] + + create_duration = "30s" + + triggers = { + operator_role_unique_id = aws_iam_role.operator.unique_id + operator_trust_policy_sha256 = sha256(aws_iam_role.operator.assume_role_policy) + } + + lifecycle { + replace_triggered_by = [aws_iam_role_policy_attachment.operator] + } +} diff --git a/modules/microvm-foundation/network_connector_security_group.tf b/modules/microvm-foundation/network_connector_security_group.tf new file mode 100644 index 0000000000..7d88e37d2d --- /dev/null +++ b/modules/microvm-foundation/network_connector_security_group.tf @@ -0,0 +1,40 @@ +# A connector gets its own no-ingress security group. Route tables and NACLs on +# the caller-selected subnets determine actual destinations reachable through +# the IPv4 or dual-stack egress rules. +resource "aws_security_group" "connector" { + #checkov:skip=CKV2_AWS_5:The security group is consumed by the Lambda Network Connector rather than by a Terraform-native ENI resource. + for_each = var.network_connectors + + name = "microvm-${each.value.name}-${var.aws_region}" + description = "Outbound egress for the ${each.value.name} Lambda MicroVM Network Connector" + vpc_id = each.value.vpc_id + + tags = merge(var.tags, { + Name = "microvm-${each.value.name}-${var.aws_region}" + }) +} + +resource "aws_vpc_security_group_egress_rule" "ipv4" { + #checkov:skip=CKV_AWS_382:The connector requires outbound access; subnet routes and NACLs provide the network destination boundary. + for_each = var.network_connectors + + security_group_id = aws_security_group.connector[each.key].id + description = "Lambda MicroVM connector IPv4 egress" + ip_protocol = "-1" + cidr_ipv4 = "0.0.0.0/0" + tags = var.tags +} + +resource "aws_vpc_security_group_egress_rule" "ipv6" { + #checkov:skip=CKV_AWS_382:Dual-stack connector egress is intentional; subnet routes and NACLs provide the network destination boundary. + for_each = { + for connector_key, connector in var.network_connectors : + connector_key => connector if connector.network_protocol == "DualStack" + } + + security_group_id = aws_security_group.connector[each.key].id + description = "Lambda MicroVM connector IPv6 egress" + ip_protocol = "-1" + cidr_ipv6 = "::/0" + tags = var.tags +} diff --git a/modules/microvm-foundation/outputs.tf b/modules/microvm-foundation/outputs.tf new file mode 100644 index 0000000000..9fd52ab4bf --- /dev/null +++ b/modules/microvm-foundation/outputs.tf @@ -0,0 +1,34 @@ +output "artifact_bucket_name" { + description = "Name of the regional S3 bucket used for Lambda MicroVM build artifacts." + value = aws_s3_bucket.artifacts.id +} + +output "artifact_bucket_arn" { + description = "ARN of the regional S3 bucket used for Lambda MicroVM build artifacts." + value = aws_s3_bucket.artifacts.arn +} + +output "artifact_prefix" { + description = "Bucket prefix to which the MicroVM image publisher uploads content-addressed build artifacts." + value = local.artifact_prefix +} + +output "build_role_arn" { + description = "ARN of the Lambda-trusted role used during MicroVM image builds." + value = aws_iam_role.build.arn +} + +output "usage_policy_arn" { + description = "ARN of the reusable regional policy for operating MicroVM images in the reserved namespace and passing their Network Connectors." + value = aws_iam_policy.usage.arn +} + +output "connector_arns" { + description = "Map of connector key to the ARN of each Lambda Network Connector." + value = local.connector_arns +} + +output "security_group_ids" { + description = "Map of connector key to its dedicated no-ingress security group ID." + value = { for connector_key, security_group in aws_security_group.connector : connector_key => security_group.id } +} diff --git a/modules/microvm-foundation/storage.tf b/modules/microvm-foundation/storage.tf new file mode 100644 index 0000000000..73ff52e172 --- /dev/null +++ b/modules/microvm-foundation/storage.tf @@ -0,0 +1,103 @@ +# Lambda MicroVM image source artifacts must be stored in an S3 bucket in the +# same region as the image. A separate helper deployment owns the bucket in each +# supported region. +resource "aws_s3_bucket" "artifacts" { + #checkov:skip=CKV_AWS_145:SSE-S3 protects ephemeral content-addressed build inputs; this helper has no CMK artifact contract. + #checkov:skip=CKV_AWS_144:Lambda MicroVM builds require same-region artifacts, so this regional bucket intentionally has no cross-region replication. + #checkov:skip=CKV_AWS_18:CloudTrail records control-plane access and the bucket contains short-lived build inputs; separate S3 access logging is not required. + #checkov:skip=CKV2_AWS_62:The publisher uploads artifacts synchronously and no event-driven consumer requires S3 notifications. + bucket = var.artifact_bucket_name + tags = var.tags +} + +resource "aws_s3_bucket_ownership_controls" "artifacts" { + bucket = aws_s3_bucket.artifacts.id + + rule { + object_ownership = "BucketOwnerEnforced" + } +} + +resource "aws_s3_bucket_versioning" "artifacts" { + bucket = aws_s3_bucket.artifacts.id + + versioning_configuration { + status = "Enabled" + } +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "artifacts" { + bucket = aws_s3_bucket.artifacts.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } +} + +resource "aws_s3_bucket_public_access_block" "artifacts" { + bucket = aws_s3_bucket.artifacts.id + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true + skip_destroy = true +} + +resource "aws_s3_bucket_lifecycle_configuration" "artifacts" { + bucket = aws_s3_bucket.artifacts.id + + rule { + id = "expire-microvm-build-artifacts" + + # The bucket is dedicated to MicroVM build artifacts, so lifecycle cleanup + # applies to every object, including abandoned uploads outside the expected + # publisher prefix. + filter {} + + expiration { + days = var.artifact_retention_days + } + + noncurrent_version_expiration { + noncurrent_days = var.artifact_retention_days + } + + abort_incomplete_multipart_upload { + days_after_initiation = 7 + } + + status = "Enabled" + } + + depends_on = [aws_s3_bucket_versioning.artifacts] +} + +data "aws_iam_policy_document" "artifact_bucket" { + statement { + sid = "DenyInsecureTransport" + effect = "Deny" + actions = ["s3:*"] + resources = [ + aws_s3_bucket.artifacts.arn, + "${aws_s3_bucket.artifacts.arn}/*", + ] + + principals { + type = "*" + identifiers = ["*"] + } + + condition { + test = "Bool" + variable = "aws:SecureTransport" + values = ["false"] + } + } +} + +resource "aws_s3_bucket_policy" "artifacts" { + bucket = aws_s3_bucket.artifacts.id + policy = data.aws_iam_policy_document.artifact_bucket.json +} diff --git a/modules/microvm-foundation/usage_policy.tf b/modules/microvm-foundation/usage_policy.tf new file mode 100644 index 0000000000..8abe004bee --- /dev/null +++ b/modules/microvm-foundation/usage_policy.tf @@ -0,0 +1,58 @@ +# Consumer modules can attach this policy to a control-plane role they own. +# This helper deliberately leaves the managed policy unattached. +data "aws_iam_policy_document" "usage" { + statement { + sid = "UseConfiguredMicrovmImages" + effect = "Allow" + actions = [ + "lambda:CreateMicrovmAuthToken", + "lambda:GetMicrovm", + "lambda:GetMicrovmImage", + "lambda:GetMicrovmImageVersion", + "lambda:ListMicrovmImageVersions", + "lambda:ResumeMicrovm", + "lambda:RunMicrovm", + "lambda:SuspendMicrovm", + "lambda:TerminateMicrovm", + ] + resources = [local.image_arn_pattern] + } + + #checkov:skip=CKV_AWS_111:ListMicrovms and ListMicrovmImages do not support resource-level permissions. + #checkov:skip=CKV_AWS_356:Lambda MicroVM account-level list actions require Resource '*'. + statement { + sid = "DiscoverMicrovmRuntimeState" + effect = "Allow" + actions = [ + "lambda:ListMicrovmImages", + "lambda:ListMicrovms", + ] + resources = ["*"] + } + + statement { + sid = "ReadConfiguredNetworkConnectors" + effect = "Allow" + actions = ["lambda:GetNetworkConnector"] + resources = values(local.connector_arns) + } + + #checkov:skip=CKV_AWS_111:PassNetworkConnector and ListNetworkConnectors do not support resource-level permissions. + #checkov:skip=CKV_AWS_356:Lambda requires Resource '*' for PassNetworkConnector and the account-level list operation. + statement { + sid = "PassAndDiscoverNetworkConnectors" + effect = "Allow" + actions = [ + "lambda:ListNetworkConnectors", + "lambda:PassNetworkConnector", + ] + resources = ["*"] + } +} + +resource "aws_iam_policy" "usage" { + name_prefix = var.usage_policy_name_prefix + description = "Permissions to discover and operate configured Lambda MicroVM images and to read and pass their regional Network Connectors." + policy = data.aws_iam_policy_document.usage.json + tags = var.tags +} diff --git a/modules/microvm-foundation/variables.tf b/modules/microvm-foundation/variables.tf new file mode 100644 index 0000000000..81c84d4fd1 --- /dev/null +++ b/modules/microvm-foundation/variables.tf @@ -0,0 +1,152 @@ +variable "aws_region" { + type = string + description = "AWS region in which to create the Lambda MicroVM prerequisites." +} + +variable "tags" { + type = map(string) + description = "A map of module-specific tags to apply to resources." +} + +variable "build_policy_name_prefix" { + type = string + description = "Name prefix for the Lambda MicroVM build policy." + + validation { + condition = length(var.build_policy_name_prefix) >= 1 && length(var.build_policy_name_prefix) <= 64 && can(regex("^[a-zA-Z0-9-_]+-$", var.build_policy_name_prefix)) + error_message = "build_policy_name_prefix must be 1 to 64 characters, contain only letters, numbers, hyphens, or underscores, and end with a hyphen." + } +} + +variable "usage_policy_name_prefix" { + type = string + description = "Name prefix for the Lambda MicroVM runtime usage policy." + + validation { + condition = length(var.usage_policy_name_prefix) >= 1 && length(var.usage_policy_name_prefix) <= 64 && can(regex("^[a-zA-Z0-9-_]+-$", var.usage_policy_name_prefix)) + error_message = "usage_policy_name_prefix must be 1 to 64 characters, contain only letters, numbers, hyphens, or underscores, and end with a hyphen." + } +} + +variable "build_role_name_prefix" { + type = string + description = "Name prefix for the Lambda MicroVM build role." + + validation { + condition = length(var.build_role_name_prefix) >= 1 && length(var.build_role_name_prefix) <= 64 && can(regex("^[a-zA-Z0-9-_]+-$", var.build_role_name_prefix)) + error_message = "build_role_name_prefix must be 1 to 64 characters, contain only letters, numbers, hyphens, or underscores, and end with a hyphen." + } +} + +variable "network_connector_operator_role_name_prefix" { + type = string + description = "Name prefix for the Lambda Network Connector operator role." + + validation { + condition = length(var.network_connector_operator_role_name_prefix) >= 1 && length(var.network_connector_operator_role_name_prefix) <= 64 && can(regex("^[a-zA-Z0-9-_]+-$", var.network_connector_operator_role_name_prefix)) + error_message = "network_connector_operator_role_name_prefix must be 1 to 64 characters, contain only letters, numbers, hyphens, or underscores, and end with a hyphen." + } +} + +variable "artifact_bucket_name" { + type = string + description = "Optional name for the regional MicroVM build-artifact bucket. When null, AWS generates the bucket name." + default = null + nullable = true + + validation { + condition = var.artifact_bucket_name == null || length(var.artifact_bucket_name) > 0 + error_message = "artifact_bucket_name must be null or a non-empty string." + } +} + +variable "artifact_retention_days" { + type = number + description = "Number of days to retain current and noncurrent MicroVM build artifacts." + default = 30 + + validation { + condition = var.artifact_retention_days >= 1 && var.artifact_retention_days <= 3650 + error_message = "artifact_retention_days must be between 1 and 3650." + } +} + +variable "image_name_prefix" { + type = string + description = "IAM namespace prefix reserved for externally published Lambda MicroVM image names. This module does not create or enumerate images." + + validation { + condition = ( + length(var.image_name_prefix) >= 1 + && length(var.image_name_prefix) <= 62 + && can(regex("^[a-zA-Z0-9-_]+$", var.image_name_prefix)) + ) + error_message = "image_name_prefix must be a 1 to 62 character IAM namespace containing only letters, numbers, hyphens, or underscores; the publisher validates each complete image name." + } +} + +variable "ecr_repository_arns" { + type = set(string) + description = "Optional regional ECR repository ARNs from which MicroVM image builds can pull runner base images." + default = [] +} + +variable "network_connectors" { + type = map(object({ + name = string + vpc_id = string + subnet_ids = set(string) + network_protocol = optional(string, "IPv4") + })) + description = "Regional Lambda MicroVM Network Connectors keyed by a stable consumer-defined identity." + + validation { + condition = length(var.network_connectors) > 0 + error_message = "network_connectors must contain at least one connector." + } + + validation { + condition = alltrue([ + for connector in values(var.network_connectors) : ( + length(connector.name) >= 1 + && length(connector.name) <= 64 + && can(regex("^[a-zA-Z0-9_-]+$", connector.name)) + ) + ]) + error_message = "Each network connector name must contain only letters, numbers, hyphens, or underscores and be at most 64 characters." + } + + validation { + condition = ( + length(distinct([for connector in values(var.network_connectors) : connector.name])) == length(var.network_connectors) + ) + error_message = "Each network connector name must be unique within the region." + } + + validation { + condition = alltrue([ + for connector in values(var.network_connectors) : can(regex("^vpc-[0-9a-f]+$", connector.vpc_id)) + ]) + error_message = "Each network connector vpc_id must be a valid VPC ID." + } + + validation { + condition = alltrue([ + for connector in values(var.network_connectors) : ( + length(connector.subnet_ids) >= 1 + && length(connector.subnet_ids) <= 16 + && alltrue([ + for subnet_id in connector.subnet_ids : can(regex("^subnet-[0-9a-f]+$", subnet_id)) + ]) + ) + ]) + error_message = "Each network connector must contain 1 to 16 valid subnet IDs." + } + + validation { + condition = alltrue([ + for connector in values(var.network_connectors) : contains(["IPv4", "DualStack"], connector.network_protocol) + ]) + error_message = "Each network connector network_protocol must be IPv4 or DualStack." + } +} diff --git a/modules/microvm-foundation/versions.tf b/modules/microvm-foundation/versions.tf new file mode 100644 index 0000000000..ccc4e5a4ef --- /dev/null +++ b/modules/microvm-foundation/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.4.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 6.61" + } + time = { + source = "hashicorp/time" + version = ">= 0.13" + } + } +} From fb78a00eb05008f5d9344b0c6fd26c19ab1fa512 Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Tue, 8 Sep 2026 21:16:16 +0200 Subject: [PATCH 2/4] test(ministack): run microvm foundation example --- .github/workflows/ministack.yml | 1 + tests/ministack/README.md | 10 +++++++--- tests/ministack/microvm-foundation.tfvars | 3 +++ tests/ministack/run-example.sh | 6 +++--- 4 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 tests/ministack/microvm-foundation.tfvars diff --git a/.github/workflows/ministack.yml b/.github/workflows/ministack.yml index 0b61d75c7e..accaa6c824 100644 --- a/.github/workflows/ministack.yml +++ b/.github/workflows/ministack.yml @@ -54,6 +54,7 @@ jobs: - ephemeral - multi-runner - multi-runner-v2 + - microvm-foundation - termination-watcher terraform: - "1.4.0" diff --git a/tests/ministack/README.md b/tests/ministack/README.md index 12f5b4809e..eb35b954a7 100644 --- a/tests/ministack/README.md +++ b/tests/ministack/README.md @@ -1,12 +1,14 @@ # MiniStack example tests The MiniStack workflow runs the `base`, `prebuilt`, `default`, `ephemeral`, -`multi-runner`, `multi-runner-v2`, and `termination-watcher` examples directly +`multi-runner`, `multi-runner-v2`, `microvm-foundation`, and `termination-watcher` examples directly with Terraform 1.4.0 and the latest Terraform release. The examples with input variables get their inputs from their own tfvars files in this directory. The `termination-watcher` example has no input variables -and uses the configuration checked into the example itself. No override files, -setup module, or Terraform fixture configuration is checked in. The helper +and uses the configuration checked into the example itself. The +`microvm-foundation` lane uses an isolated tfvars file with no network +connectors because MiniStack does not provide the regional Lambda Network +Connector API. No override files or setup module are checked in. The helper creates and removes a temporary AMI override for `default` and `ephemeral`, temporary SSM parameters for `multi-runner`, and temporary AMI fixtures plus an override for `multi-runner-v2`. @@ -26,6 +28,8 @@ tests/ministack/run-example.sh apply multi-runner # or tests/ministack/run-example.sh apply multi-runner-v2 # or +tests/ministack/run-example.sh apply microvm-foundation +# or tests/ministack/run-example.sh apply termination-watcher ``` diff --git a/tests/ministack/microvm-foundation.tfvars b/tests/ministack/microvm-foundation.tfvars new file mode 100644 index 0000000000..9d576d77c7 --- /dev/null +++ b/tests/ministack/microvm-foundation.tfvars @@ -0,0 +1,3 @@ +aws_region = "eu-west-1" + +network_connectors = {} diff --git a/tests/ministack/run-example.sh b/tests/ministack/run-example.sh index 3f8be94d19..8f748f9425 100755 --- a/tests/ministack/run-example.sh +++ b/tests/ministack/run-example.sh @@ -14,14 +14,14 @@ example="${2:-}" tfvars_file="${3:-${MINISTACK_TFVARS_FILE:-}}" case "$example" in - base | prebuilt | default | ephemeral | multi-runner | multi-runner-v2) + base | prebuilt | default | ephemeral | multi-runner | multi-runner-v2 | microvm-foundation) use_tfvars=true ;; termination-watcher) use_tfvars=false ;; *) - echo "Supported examples for the runner are: base, prebuilt, default, ephemeral, multi-runner, multi-runner-v2, termination-watcher" >&2 + echo "Supported examples for the runner are: base, prebuilt, default, ephemeral, multi-runner, multi-runner-v2, microvm-foundation, termination-watcher" >&2 exit 64 ;; esac @@ -29,7 +29,7 @@ esac case "$action" in init | plan | apply | destroy) ;; *) - echo "Usage: $0 {init|plan|apply|destroy} {base|prebuilt|default|ephemeral|multi-runner|multi-runner-v2|termination-watcher} [TFVARS_FILE]" >&2 + echo "Usage: $0 {init|plan|apply|destroy} {base|prebuilt|default|ephemeral|multi-runner|multi-runner-v2|microvm-foundation|termination-watcher} [TFVARS_FILE]" >&2 exit 64 ;; esac From 8e636c0be6073a20e791af5c2b769c6c0109a358 Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Tue, 8 Sep 2026 23:13:01 +0200 Subject: [PATCH 3/4] fix(microvm): add CI provider checksums --- examples/microvm-foundation/.terraform.lock.hcl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/microvm-foundation/.terraform.lock.hcl b/examples/microvm-foundation/.terraform.lock.hcl index 2fc14d932b..78cf3de9bd 100644 --- a/examples/microvm-foundation/.terraform.lock.hcl +++ b/examples/microvm-foundation/.terraform.lock.hcl @@ -5,6 +5,7 @@ provider "registry.terraform.io/hashicorp/aws" { version = "6.63.0" constraints = ">= 6.61.0" hashes = [ + "h1:9cre7jh1lSs/9igpgAcENMUAUlYW3HCtkav3up4oit0=", "h1:dRlYHkc+r6fgzF57WC7Zjcmb6sF/6TTGDEgwGK+LAZY=", "zh:005d56736afd17d963998c405cee6f434dbc23a415109f9435ff1542879ae611", "zh:026ef126321a86ad7080b5d858e2527f96f5289678cbcd8856296e229c43339d", @@ -29,6 +30,7 @@ provider "registry.terraform.io/hashicorp/time" { version = "0.14.1" constraints = ">= 0.13.0" hashes = [ + "h1:GJig5pIwiKDsiF73KLs7vWvDs76/x6DeNSxKrfqlA40=", "h1:r93SxP++6gUlwCHDQ5OkRmcU8B0yv6ZA9nF0Dh6NJmA=", "zh:0837ca5b057e5cff94dff7de2fcccafb4abaa33c45de193fe2853e684818a267", "zh:15a122f72d9e0f34fc5384cc7ec089319641fee5c319748a3aa02fc42f459969", From 79e929a60fa54e92d5da1dde6c4456c5e9458adf Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Tue, 8 Sep 2026 23:40:33 +0200 Subject: [PATCH 4/4] fix(microvm): align foundation example inputs --- examples/microvm-foundation/README.md | 2 +- examples/microvm-foundation/variables.tf | 2 +- modules/microvm-foundation/README.md | 4 ++-- modules/microvm-foundation/variables.tf | 13 ++++--------- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/examples/microvm-foundation/README.md b/examples/microvm-foundation/README.md index aa54e94fa0..bdc531bc12 100644 --- a/examples/microvm-foundation/README.md +++ b/examples/microvm-foundation/README.md @@ -60,7 +60,7 @@ No resources. | [build\_role\_name\_prefix](#input\_build\_role\_name\_prefix) | Name prefix for the Lambda MicroVM build role. | `string` | `"github-actions-runner-microvm-build-"` | no | | [ecr\_repository\_arns](#input\_ecr\_repository\_arns) | Optional private ECR repository ARNs used by the image build. | `set(string)` | `[]` | no | | [image\_name\_prefix](#input\_image\_name\_prefix) | Reserved Lambda MicroVM image-name namespace used by the runtime policy. | `string` | `"github-actions-runner-ubuntu-arm64"` | no | -| [network\_connector\_operator\_role\_name\_prefix](#input\_network\_connector\_operator\_role\_name\_prefix) | Name prefix for the Lambda Network Connector operator role. | `string` | `"github-actions-runner-microvm-network-operator-"` | no | +| [network\_connector\_operator\_role\_name\_prefix](#input\_network\_connector\_operator\_role\_name\_prefix) | Name prefix for the Lambda Network Connector operator role. | `string` | `"github-actions-microvm-net-operator-"` | no | | [network\_connectors](#input\_network\_connectors) | VPC and subnet configuration for regional Lambda MicroVM egress connectors. |
map(object({
name = string
vpc_id = string
subnet_ids = set(string)
network_protocol = optional(string, "IPv4")
}))
| n/a | yes | | [tags](#input\_tags) | Additional tags applied by the foundation module. | `map(string)` |
{
"Component": "microvm-foundation"
}
| no | | [usage\_policy\_name\_prefix](#input\_usage\_policy\_name\_prefix) | Name prefix for the Lambda MicroVM runtime usage policy. | `string` | `"github-actions-runner-microvm-runtime-usage-policy-"` | no | diff --git a/examples/microvm-foundation/variables.tf b/examples/microvm-foundation/variables.tf index 4afd3804a5..aa8d9fb002 100644 --- a/examples/microvm-foundation/variables.tf +++ b/examples/microvm-foundation/variables.tf @@ -40,7 +40,7 @@ variable "build_role_name_prefix" { variable "network_connector_operator_role_name_prefix" { type = string description = "Name prefix for the Lambda Network Connector operator role." - default = "github-actions-runner-microvm-network-operator-" + default = "github-actions-microvm-net-operator-" } variable "artifact_bucket_name" { diff --git a/modules/microvm-foundation/README.md b/modules/microvm-foundation/README.md index fadbedc0f7..77b620c107 100644 --- a/modules/microvm-foundation/README.md +++ b/modules/microvm-foundation/README.md @@ -34,7 +34,7 @@ module "microvm_foundation" { tags = { Environment = "example" } build_policy_name_prefix = "github-actions-runner-microvm-build-policy-" build_role_name_prefix = "github-actions-runner-microvm-build-" - network_connector_operator_role_name_prefix = "github-actions-runner-microvm-network-operator-" + network_connector_operator_role_name_prefix = "github-actions-microvm-net-operator-" usage_policy_name_prefix = "github-actions-runner-microvm-runtime-usage-policy-" image_name_prefix = "github-actions-runner-ubuntu-arm64" @@ -116,7 +116,7 @@ No modules. | [ecr\_repository\_arns](#input\_ecr\_repository\_arns) | Optional regional ECR repository ARNs from which MicroVM image builds can pull runner base images. | `set(string)` | `[]` | no | | [image\_name\_prefix](#input\_image\_name\_prefix) | IAM namespace prefix reserved for externally published Lambda MicroVM image names. This module does not create or enumerate images. | `string` | n/a | yes | | [network\_connector\_operator\_role\_name\_prefix](#input\_network\_connector\_operator\_role\_name\_prefix) | Name prefix for the Lambda Network Connector operator role. | `string` | n/a | yes | -| [network\_connectors](#input\_network\_connectors) | Regional Lambda MicroVM Network Connectors keyed by a stable consumer-defined identity. |
map(object({
name = string
vpc_id = string
subnet_ids = set(string)
network_protocol = optional(string, "IPv4")
}))
| n/a | yes | +| [network\_connectors](#input\_network\_connectors) | Optional regional Lambda MicroVM Network Connectors keyed by a stable consumer-defined identity. |
map(object({
name = string
vpc_id = string
subnet_ids = set(string)
network_protocol = optional(string, "IPv4")
}))
| n/a | yes | | [tags](#input\_tags) | A map of module-specific tags to apply to resources. | `map(string)` | n/a | yes | | [usage\_policy\_name\_prefix](#input\_usage\_policy\_name\_prefix) | Name prefix for the Lambda MicroVM runtime usage policy. | `string` | n/a | yes | diff --git a/modules/microvm-foundation/variables.tf b/modules/microvm-foundation/variables.tf index 81c84d4fd1..72c1db000b 100644 --- a/modules/microvm-foundation/variables.tf +++ b/modules/microvm-foundation/variables.tf @@ -43,8 +43,8 @@ variable "network_connector_operator_role_name_prefix" { description = "Name prefix for the Lambda Network Connector operator role." validation { - condition = length(var.network_connector_operator_role_name_prefix) >= 1 && length(var.network_connector_operator_role_name_prefix) <= 64 && can(regex("^[a-zA-Z0-9-_]+-$", var.network_connector_operator_role_name_prefix)) - error_message = "network_connector_operator_role_name_prefix must be 1 to 64 characters, contain only letters, numbers, hyphens, or underscores, and end with a hyphen." + condition = length(var.network_connector_operator_role_name_prefix) >= 1 && length(var.network_connector_operator_role_name_prefix) <= 38 && can(regex("^[a-zA-Z0-9-_]+-$", var.network_connector_operator_role_name_prefix)) + error_message = "network_connector_operator_role_name_prefix must be 1 to 38 characters, contain only letters, numbers, hyphens, or underscores, and end with a hyphen." } } @@ -55,7 +55,7 @@ variable "artifact_bucket_name" { nullable = true validation { - condition = var.artifact_bucket_name == null || length(var.artifact_bucket_name) > 0 + condition = var.artifact_bucket_name == null ? true : length(var.artifact_bucket_name) > 0 error_message = "artifact_bucket_name must be null or a non-empty string." } } @@ -98,12 +98,7 @@ variable "network_connectors" { subnet_ids = set(string) network_protocol = optional(string, "IPv4") })) - description = "Regional Lambda MicroVM Network Connectors keyed by a stable consumer-defined identity." - - validation { - condition = length(var.network_connectors) > 0 - error_message = "network_connectors must contain at least one connector." - } + description = "Optional regional Lambda MicroVM Network Connectors keyed by a stable consumer-defined identity." validation { condition = alltrue([