diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml index 059be32d9c..211f2bf37c 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 1b190a4a67..4f34f85a49 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -65,6 +65,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" + } + } +}