diff --git a/.github/workflows/ministack.yml b/.github/workflows/ministack.yml index 80af872e42..2e7c69d235 100644 --- a/.github/workflows/ministack.yml +++ b/.github/workflows/ministack.yml @@ -70,9 +70,11 @@ jobs: - prebuilt - default - ephemeral + - microvm + - microvm-foundation + - migration-test - multi-runner - multi-runner-v2 - - migration-test - termination-watcher services: ministack: diff --git a/.github/workflows/packer-build.yml b/.github/workflows/packer-build.yml index 8dcff4efb6..729b1fc52e 100644 --- a/.github/workflows/packer-build.yml +++ b/.github/workflows/packer-build.yml @@ -26,12 +26,6 @@ jobs: runs-on: ubuntu-latest container: image: index.docker.io/hashicorp/packer@sha256:12c441b8a3994e7df9f0e2692d9298f14c387e70bcc06139420977dbf80a137b # 1.11.2 - strategy: - matrix: - image: ["linux-al2023", "windows-core-2019", "windows-core-2022", "ubuntu-focal", "ubuntu-jammy", "ubuntu-jammy-arm64"] - defaults: - run: - working-directory: images/${{ matrix.image }} steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 @@ -42,9 +36,29 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - name: packer init - run: packer init . - - name: check packer formatting - run: packer fmt -recursive -check=true . - - name: packer validate - run: packer validate -evaluate-datasources . + + - name: Verify images + run: | + set -- "${GITHUB_WORKSPACE}"/images/* + found_image=false + + while [ "$#" -gt 0 ]; do + image_dir="$1" + shift + [ -d "${image_dir}" ] || continue + + found_image=true + image="${image_dir##*/}" + + echo "::group::Verifying ${image}" + cd "${image_dir}" + packer init . + packer fmt -recursive -check=true . + packer validate -evaluate-datasources . + echo "::endgroup::" + done + + [ "${found_image}" = true ] || { + echo "No image directories found under ${GITHUB_WORKSPACE}/images" + exit 1 + } diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml index 744727c3bc..cb36c9bbcf 100644 --- a/.github/workflows/terraform.yml +++ b/.github/workflows/terraform.yml @@ -50,6 +50,8 @@ env: prebuilt ephemeral termination-watcher + microvm + microvm-foundation multi-runner multi-runner-v2 external-managed-ssm-secrets 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/examples/microvm.md b/docs/examples/microvm.md new file mode 100644 index 0000000000..4014781114 --- /dev/null +++ b/docs/examples/microvm.md @@ -0,0 +1,3 @@ +# Lambda MicroVM + +--8<-- "examples/microvm/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..78cf3de9bd --- /dev/null +++ b/examples/microvm-foundation/.terraform.lock.hcl @@ -0,0 +1,49 @@ +# 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:9cre7jh1lSs/9igpgAcENMUAUlYW3HCtkav3up4oit0=", + "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:GJig5pIwiKDsiF73KLs7vWvDs76/x6DeNSxKrfqlA40=", + "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/.terraform.lock.hcl.tofu b/examples/microvm-foundation/.terraform.lock.hcl.tofu new file mode 100644 index 0000000000..045ca37e02 --- /dev/null +++ b/examples/microvm-foundation/.terraform.lock.hcl.tofu @@ -0,0 +1,76 @@ +# This file is maintained automatically by "tofu init". +# Manual edits may be lost in future updates. + +provider "registry.opentofu.org/hashicorp/aws" { + version = "6.63.0" + constraints = ">= 6.61.0" + hashes = [ + "h1:1jhQJPHOPu2mzDG/ke3tK8PNcEqQHA4vhF05WWlM/yg=", + "h1:3+pvT0KN/bkJ6TBuExj+gxptEozhnpo80Ztblwq85eo=", + "h1:5aTequ87wZS7Mh4dEIayDGKcFdaFgHtw74NtqY5Idi0=", + "h1:AMRlrrM3z1SmrslOtotqKq02zapxLKtXaSN9Jbs0Oho=", + "h1:OTjECFWTDxsjcUfOKCNBp75Z5lGrW/KplRDsjTZYT2g=", + "h1:b8LORLOKMOOl+nK1M2UhCjELSjjziClJuAv6hYuySHs=", + "h1:bUfTX1giRLOyfDbBvsDbwR3tJmsTFRWcOTQdj2npDWA=", + "h1:dzs4kwx+itVGAH7yEOyeoWcE3LNRMnWtlt4ROgyAa0M=", + "h1:lnjou+SiwpYJ+j9PXWozXPHSPlhxIZb0RqpsSEBzfGw=", + "h1:pqzUeHAQj9NctgkwaynaF2aB+3QiZXcoslzMGjT743w=", + "h1:qTXEWOWxA6sfUpC29UXrsbHnNzWH7+j1RTUVG4YCm+U=", + "h1:qdHKOKt/ISn9RLjUe22OZBpN3F7H2DFeHJL/CSc2x8E=", + "h1:tpNzIZBzzUW7/kLU3BhYf3jhdO5uNwYfNmgC9B8kvMM=", + "h1:uVVlFgjg6GyxJLbCsTO1+R5fTNbZ73mLpVpSd0mMrFk=", + "h1:xGJsV5IFf7c11cXzJrsY40hiJCghp4odT0eJyTyAUYY=", + "zh:039a03e920e55f14a691feb67216a2d142bfee603128e15f9c5138f9ecd85016", + "zh:14e060b7f46ca7b0fa009b91aef419c58cbdff854de96e9a1d853166f8d902fd", + "zh:18803e8fe2c291c8db5526c71b3287ff7c81453f10ca6d8e69cdf9c535b00783", + "zh:1b83fce6e31a6095e932d80a7c3f47ac04252653a2de2b98ec6204563310fcba", + "zh:2add7bc976ceebb1a94d84598762c9b9cf281ca52ec83deeb4e95e90aa200a12", + "zh:2f22cd5372408f11937fa5513a7b960d3cebc334c5ec65fc5322c3bac1c1f664", + "zh:41c5e857dacfd83b7ca12a435204957ff6ca8830b9efefd0d381ad4d63b19779", + "zh:4eace6246e46999782d219bc4f50f83d19ef9156bacf5ca1528da12da4918015", + "zh:5e1c1281c3f929399e2ed3dbdce03426fd57a9ec55cd36e04acf1712aa5954ba", + "zh:608272b1f5d75ead123c9d933aa1fed7dc832cedd1506019046b4c8fdcc91dce", + "zh:6b3680f8a2f7be2c171953aba89d639fb2624b9cf52ec304e16434874566601d", + "zh:99aa1006f2141f3341a02020e1c91abfb02280e57c77e0415c98b8d900353d88", + "zh:9ad235bef34a89a8dd9943f9fa9f05cc729bb52a4e0dc926a31bb13cb0ae2418", + "zh:e0e3ac361e04748a4ca0c1cdbb6abab2aa817f4ad67e1692817d16e370161d59", + "zh:f60962c982a41fde956e796425e7194b4311741c179c060c1c8b5e16a557d635", + ] +} + +provider "registry.opentofu.org/hashicorp/time" { + version = "0.14.1" + constraints = ">= 0.13.0" + hashes = [ + "h1:+anTsiSl8j75hcu7gKWF2ZlKS/qZDk4Ll0Oq2mVoArU=", + "h1:BZtorvSdYDM6pFE8nz7yUVVR2Pe1i0MxZFyBnaKlguY=", + "h1:Do/MjWRafefFS6RumnUVbIDH9MyMLuwQXs0kEc4Evrs=", + "h1:RoJeKHJjlqMikWdptWEWOBfBv4YxXf8KtZcg6oS8OWE=", + "h1:Ssb164oIHIO9VWGljof/xqbxbnPmrT5jyJ+WegAj2+k=", + "h1:WGgegEyoMb7nzXr09OvAxaSJls6honSdJiXfNaPTSkw=", + "h1:jfH6FAhiYd3hKB29s8cxk9PUKGdEN0fPB3zQ0IE5pdQ=", + "h1:lVTdvsa16YmLYJOmGq6ryESpdeWhLyo7Y4MAPPh/gIc=", + "h1:nD1nfDyZxI1PgTNT5Zs9G+R9PIiS25xQw9y7tuOD2G0=", + "h1:nPu3DGOZfwDier2k8DjNq2ZK3GQDzHYLghIUjb4/KJc=", + "h1:o7oRgk39V8okQzrI9DX4AKMTZrIqo6oBj1zQ3tMZJiI=", + "h1:oJSgnSkg9lXMISdrA+pXkV8FhgLPk+mAzbFE209jar0=", + "h1:oT/ffb2Uy19qRCLu9QSbuAguWwJI9rwW1j6RPjluqRY=", + "h1:vWWQpPBXFR9AVZM1o22/pHjNQjRa/IWSVL1mZgMqgLk=", + "h1:yTRqKp4efJyAq7bPZjFGcnR1KUDUl2/dBMzILxn5Z+c=", + "zh:032ea0f53759a5ade64286ad8a403956bb390860429de3647c6652701c2fff8b", + "zh:204581f170c50a579357b1a067f407b890adfc0404952cb922fefe2aba7655a8", + "zh:331119864191614a81ce9e8d1ec3ea6fe13da0bea6130f9e1dfd94e3b16ddaaa", + "zh:373a8b1b227a92b5e5fee611fef03df7aea82f51c3b1a62b33ac29a1b0ee927f", + "zh:46796e7616d511fa264a367ee447f6d0de64e8145f315a12271f4ec5c183f044", + "zh:5e3199e6dcc9bb99868764339c35aa169903baa4e150490da2980b2620cdfdbc", + "zh:5f95ee94a83a13e6b1e26d3d9f0297ed1036387d7a8adbab90b4ef990b6a1331", + "zh:68e1f75602423236d947d9464d62c4cc0a6312ff2206b9306067faf03bed7011", + "zh:72ee59f4f859abe6288b59eaacf9a838b8490a132c1c17f393dd8355401704df", + "zh:993775eeb0f0b4c898e305dead3cd6cd732b48c1c0fab20c2e4431ef6b678626", + "zh:a7b38fea85a1edea7f9afa33e9585970219a788d0300ca095e80b19eec39b291", + "zh:b7a8ef0dbee5b76295b1a11d88452ad2798e8d2598925af193884ce405093497", + "zh:d3ff8770f4b7cc4dccd4b1b7b40c933b468b7e07a05c704b7ea4c1673c74bf9c", + "zh:e59043c6f98aa986956a79c47f1d6bc150d409bbd9c88c9c42d41713bc539f04", + "zh:e6c2ae3bdccf6a2e3f106b61895cf7f125684204c1406b29f74fa408671f81cc", + ] +} diff --git a/examples/microvm-foundation/README.md b/examples/microvm-foundation/README.md new file mode 100644 index 0000000000..7417f3c70e --- /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 `aws_region` and `artifact_bucket_name` in `terraform.tfvars` (copy +`terraform.tfvars.example`). The example creates its VPC and private subnets +through the reusable `base` example and wires them into the Network Connector. + +```bash +terraform init +terraform apply +terraform output +``` + +Apply this foundation before building an image with the direct Packer commands +documented in `../../images/microvm-ubuntu/README.md`. Use the outputs as the build inputs: + +- `artifact_bucket_name` -> `MICROVM_ARTIFACT_BUCKET` +- `build_role_arn` -> `MICROVM_BUILD_ROLE_ARN` +- `connector_arns.ministack` -> `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.5.6 | +| [aws](#requirement\_aws) | >= 6.61 | + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [base](#module\_base) | ../base | n/a | +| [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) | Name for the regional MicroVM build-artifact bucket. | `string` | n/a | yes | +| [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` | `"gha-microvm-build-policy-"` | no | +| [build\_role\_name\_prefix](#input\_build\_role\_name\_prefix) | Name prefix for the Lambda MicroVM build role. | `string` | `"gha-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` | `"gha-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` | `"gha-microvm-network-operator-"` | no | +| [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` | `"gha-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..fd19d191ef
--- /dev/null
+++ b/examples/microvm-foundation/main.tf
@@ -0,0 +1,35 @@
+locals {
+ environment = "microvm-foundation"
+ aws_region = var.aws_region
+
+ network_connectors = {
+ ministack = {
+ name = "ministack"
+ vpc_id = module.base.vpc.vpc_id
+ subnet_ids = module.base.vpc.private_subnets
+ }
+ }
+}
+
+module "base" {
+ source = "../base"
+
+ prefix = local.environment
+ aws_region = local.aws_region
+}
+
+module "microvm_foundation" {
+ source = "../../modules/microvm-foundation"
+
+ aws_region = local.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 = local.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..9e3d766661
--- /dev/null
+++ b/examples/microvm-foundation/terraform.tfvars.example
@@ -0,0 +1,8 @@
+aws_region = "eu-west-1"
+artifact_bucket_name = "microvm-foundation-artifacts-eu-west-1"
+
+# 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..9163e2e81f
--- /dev/null
+++ b/examples/microvm-foundation/variables.tf
@@ -0,0 +1,67 @@
+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 = "gha-microvm-build-policy-"
+}
+
+variable "usage_policy_name_prefix" {
+ type = string
+ description = "Name prefix for the Lambda MicroVM runtime usage policy."
+ default = "gha-microvm-runtime-usage-policy-"
+}
+
+variable "build_role_name_prefix" {
+ type = string
+ description = "Name prefix for the Lambda MicroVM build role."
+ default = "gha-microvm-build-"
+}
+
+variable "network_connector_operator_role_name_prefix" {
+ type = string
+ description = "Name prefix for the Lambda Network Connector operator role."
+ default = "gha-microvm-network-operator-"
+}
+
+variable "artifact_bucket_name" {
+ type = string
+ description = "Name for the regional MicroVM build-artifact bucket."
+}
+
+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 = "gha-ubuntu-arm64"
+}
+
+variable "ecr_repository_arns" {
+ type = set(string)
+ description = "Optional private ECR repository ARNs used by the image build."
+ default = []
+}
diff --git a/examples/microvm-foundation/versions.tf b/examples/microvm-foundation/versions.tf
new file mode 100644
index 0000000000..23dd3947d6
--- /dev/null
+++ b/examples/microvm-foundation/versions.tf
@@ -0,0 +1,10 @@
+terraform {
+ required_version = ">= 1.5.6"
+
+ required_providers {
+ aws = {
+ source = "hashicorp/aws"
+ version = ">= 6.61"
+ }
+ }
+}
diff --git a/examples/microvm/.terraform.lock.hcl b/examples/microvm/.terraform.lock.hcl
new file mode 100644
index 0000000000..e46d40b514
--- /dev/null
+++ b/examples/microvm/.terraform.lock.hcl
@@ -0,0 +1,71 @@
+# 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 = ">= 5.0.0, >= 6.21.0, >= 6.33.0"
+ hashes = [
+ "h1:9cre7jh1lSs/9igpgAcENMUAUlYW3HCtkav3up4oit0=",
+ "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/null" {
+ version = "3.3.1"
+ constraints = "~> 3.0, ~> 3.2"
+ hashes = [
+ "h1:TuxJq10DVnRP7c5HBZPyyvQGcckNVfijyU1eXEu5e4M=",
+ "h1:m5FqidbIgh+E9OigiZh8/xbkvpUQFSj3hZo/jqNLCLQ=",
+ "zh:08c59776542ea16e5a8545752787b17ff412922182b4cfabe16139197be8ac44",
+ "zh:123109cc7e5ed6d515787fbc212f2a3fd5e75647bb24ab7c801ccd4d4ed42451",
+ "zh:14b3fa4372754b54844b41d5dbd4671a292d8d6828b90169061feb4d7b15dd05",
+ "zh:56a4daaa3212f57b764bf3d1f333141c6610c5f21abb240e0111221f7c7fa4d4",
+ "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
+ "zh:7e888a026dbacd2474a42264227ae35f639780f0f0c613529d10a95cd61988b3",
+ "zh:85a53646267e87d600df7124e4767ffde9bba3b6356d45d961618bdd68131cc7",
+ "zh:8ffa0e9c7c39b2ab0905b472465d6e35ef0b776b3f6273bb34c150340b61bff1",
+ "zh:9846510a1841530d4403f4818e233f91e3b3bade7441047599fbf800742f65be",
+ "zh:afa98d44860875f037c6def0a7e6ff208e042712ba771f620482b143cd336891",
+ "zh:bdca130d9ef27488ae0b13bc8fd8019e8bbdd4f2ceff29da066bd333165d68c5",
+ "zh:cb3b94cbca88210dd0d1f11e2b8a89333f48c3857faf8f70f589072ce7c28610",
+ "zh:f0c0ba87925fe32f84b80f7513b1efb1b0866f51f899ba825e95ad59ff09b018",
+ ]
+}
+
+provider "registry.terraform.io/hashicorp/random" {
+ version = "3.9.0"
+ constraints = "~> 3.0"
+ hashes = [
+ "h1:OO+IuvQJSPmWdN8AyyIEvPJbLvDQpgX/zbktoa9KsJE=",
+ "h1:UlBuNVuCGJ39tTv2c5gz2NRZnQbXfbIWbTzWcth5o74=",
+ "zh:161ad0bd9a75768c82f53fb6e7172a9d8be2d4889b012645a34795031aaf1bf1",
+ "zh:19dc9a5b17729725ccfc4f45b0500af0ee5bc6b6b160c7adb8f2bf617d2c80ea",
+ "zh:269eda8fe42daa7974d5a34d166c3ba9defe80cde86c01e4dadcfdf2e1f05e5f",
+ "zh:373f7c65566f8f2cc7f45d698654feb9d988996957e1266a69ca00c52d6d16d0",
+ "zh:5599d16804c41c83009ec621b6d6b6f74e102f5827678a4750f8809055546b61",
+ "zh:583be0440469a22bff70dcfa56593b01566860b29607437264adb51060cf46fc",
+ "zh:5f211d8ec3f2e1f414870d9584bfe26e6995560ef81c748f8447a48164767398",
+ "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
+ "zh:7b547fd16216761ef86efc3ed516ac5ac0c5c42b7c7eb24a08cef2d93f69ed5e",
+ "zh:7e7c0679daf2a382151d05068c8c3f0dae6b7b7dccf818827b73dd08638df2ef",
+ "zh:8089dec888a8038b9b4fb23b3df7e1057293dbc5b60b42cc47ff690d69d4b61b",
+ "zh:c51f15a031edfd6f23ce8ced3446ca7f8d8d647e2499890d7d5d10d5016d7257",
+ "zh:c94784f005708890dc6895afd53636ec00ec1e430b15d41e5aebfb1d4b39bd04",
+ ]
+}
diff --git a/examples/microvm/.terraform.lock.hcl.tofu b/examples/microvm/.terraform.lock.hcl.tofu
new file mode 100644
index 0000000000..7aa235531d
--- /dev/null
+++ b/examples/microvm/.terraform.lock.hcl.tofu
@@ -0,0 +1,113 @@
+# This file is maintained automatically by "tofu init".
+# Manual edits may be lost in future updates.
+
+provider "registry.opentofu.org/hashicorp/aws" {
+ version = "6.63.0"
+ constraints = ">= 5.0.0, >= 6.21.0, >= 6.33.0"
+ hashes = [
+ "h1:1jhQJPHOPu2mzDG/ke3tK8PNcEqQHA4vhF05WWlM/yg=",
+ "h1:3+pvT0KN/bkJ6TBuExj+gxptEozhnpo80Ztblwq85eo=",
+ "h1:5aTequ87wZS7Mh4dEIayDGKcFdaFgHtw74NtqY5Idi0=",
+ "h1:AMRlrrM3z1SmrslOtotqKq02zapxLKtXaSN9Jbs0Oho=",
+ "h1:OTjECFWTDxsjcUfOKCNBp75Z5lGrW/KplRDsjTZYT2g=",
+ "h1:b8LORLOKMOOl+nK1M2UhCjELSjjziClJuAv6hYuySHs=",
+ "h1:bUfTX1giRLOyfDbBvsDbwR3tJmsTFRWcOTQdj2npDWA=",
+ "h1:dzs4kwx+itVGAH7yEOyeoWcE3LNRMnWtlt4ROgyAa0M=",
+ "h1:lnjou+SiwpYJ+j9PXWozXPHSPlhxIZb0RqpsSEBzfGw=",
+ "h1:pqzUeHAQj9NctgkwaynaF2aB+3QiZXcoslzMGjT743w=",
+ "h1:qTXEWOWxA6sfUpC29UXrsbHnNzWH7+j1RTUVG4YCm+U=",
+ "h1:qdHKOKt/ISn9RLjUe22OZBpN3F7H2DFeHJL/CSc2x8E=",
+ "h1:tpNzIZBzzUW7/kLU3BhYf3jhdO5uNwYfNmgC9B8kvMM=",
+ "h1:uVVlFgjg6GyxJLbCsTO1+R5fTNbZ73mLpVpSd0mMrFk=",
+ "h1:xGJsV5IFf7c11cXzJrsY40hiJCghp4odT0eJyTyAUYY=",
+ "zh:039a03e920e55f14a691feb67216a2d142bfee603128e15f9c5138f9ecd85016",
+ "zh:14e060b7f46ca7b0fa009b91aef419c58cbdff854de96e9a1d853166f8d902fd",
+ "zh:18803e8fe2c291c8db5526c71b3287ff7c81453f10ca6d8e69cdf9c535b00783",
+ "zh:1b83fce6e31a6095e932d80a7c3f47ac04252653a2de2b98ec6204563310fcba",
+ "zh:2add7bc976ceebb1a94d84598762c9b9cf281ca52ec83deeb4e95e90aa200a12",
+ "zh:2f22cd5372408f11937fa5513a7b960d3cebc334c5ec65fc5322c3bac1c1f664",
+ "zh:41c5e857dacfd83b7ca12a435204957ff6ca8830b9efefd0d381ad4d63b19779",
+ "zh:4eace6246e46999782d219bc4f50f83d19ef9156bacf5ca1528da12da4918015",
+ "zh:5e1c1281c3f929399e2ed3dbdce03426fd57a9ec55cd36e04acf1712aa5954ba",
+ "zh:608272b1f5d75ead123c9d933aa1fed7dc832cedd1506019046b4c8fdcc91dce",
+ "zh:6b3680f8a2f7be2c171953aba89d639fb2624b9cf52ec304e16434874566601d",
+ "zh:99aa1006f2141f3341a02020e1c91abfb02280e57c77e0415c98b8d900353d88",
+ "zh:9ad235bef34a89a8dd9943f9fa9f05cc729bb52a4e0dc926a31bb13cb0ae2418",
+ "zh:e0e3ac361e04748a4ca0c1cdbb6abab2aa817f4ad67e1692817d16e370161d59",
+ "zh:f60962c982a41fde956e796425e7194b4311741c179c060c1c8b5e16a557d635",
+ ]
+}
+
+provider "registry.opentofu.org/hashicorp/null" {
+ version = "3.3.1"
+ constraints = "~> 3.0, ~> 3.2"
+ hashes = [
+ "h1:2wld81FnmHW0WVgy081sIfokCr2+NuatS8yjeLEet7Y=",
+ "h1:AClQjJ6X22V4qcRgcYSxiXCMmp2pz0G8WVQC7wAx66o=",
+ "h1:AY3XQbuviNd2X5VhHYEbhNta1m/CG3JD2BKFKhCt1Y4=",
+ "h1:CUOZUd7H11lsU+4tISlnYIiP5BqnX8IDwFCVqfLJyAg=",
+ "h1:JIfV0nA/pLWnIFGscvTfuavQCn2NeHxJBeb6UUg/joA=",
+ "h1:RejAh+nyCwqDGExGln2Kb4Ro5LyHak0eJe0P9g8CHPc=",
+ "h1:SHOuTZjYymsmy4asuRq6NC3yW+zdVZOOt4f5nrb+EPM=",
+ "h1:WwPat/gT4gO8GvvKNdSkkXWVD65JppLJfqKOt9HhOqQ=",
+ "h1:Z3hXVLrOyaRiiLmmL5UCOdcRMguwjN1x5TYNdmBDgls=",
+ "h1:dd78Ad5HdfPzPts7A9qIxfitXhAriV/qza38fr2ukjk=",
+ "h1:dyVb++KwDdybzLTE6bf7GZiVQ31iWsgKPWmhTQ8G42k=",
+ "h1:gD8ZH6WWe+5gg5+y8SpLWGPUDzSxcQ3HKP8IDM/wW3I=",
+ "h1:juXCww0zRQKFTDZoKqYR0+Sn1lu99oeL6pr0Jh6LWx0=",
+ "h1:kFAySmtsshyNV7IhIrEdASzVcvwy68eeZCVC66P7yNk=",
+ "h1:nS5azDopRisB2NInwDx3Hrfg2FdVt8Gw0gTQzC0rd70=",
+ "zh:164eb061d84e01759f391265865fb31828083d0a06b25f7af7e094cbdb18c799",
+ "zh:1bb9b669a82b52c0cba2860c71e9ee6699ef302f28cb8ed06f572d39bc6c7c4f",
+ "zh:1ea9b31a8f29302122c1e8d673693f3ac270336dae560af803cd1117265a469a",
+ "zh:238bd463cb0154fb935dc331da40c0a9cbe5db9cee615ae5f35ccad5eed7dc41",
+ "zh:30ef2b7384cf7e20f33fe75754b54cf669d59816f3ad4fc73bfb2b26fb6735e9",
+ "zh:35b5cded16e4b57c207d03ee0979b14baf486fa520e6edb7a2eecf18f1b85471",
+ "zh:3dc840d13a50cd215c7540573f27e2b61f739ba90aee5b7c3846079aa0ab5534",
+ "zh:3f9309a18db608f975d5691fcb47a6e14d77199156a52e9c39dcafe3737f2b07",
+ "zh:44263a219f7dbd1848b545d080110b4f7d0495e77b71cd3c7a0b5ec52a09accb",
+ "zh:4dec54aa5f445eeea035bbd4839bcded5e47ecd07cba0e70c5a09e9272cb592f",
+ "zh:5e8fb319d7c6d6c4566a18b9d0c91580b4901a96acd7fdc476bfc79f074368e2",
+ "zh:b0e8b6d41834b57fcfbb5ca00da52ccb757e1a95b6a2d546c0dae8bfbeca1cdf",
+ "zh:bbde4c3a1dcc1718027a61a4cdf661619d17af1b58df1038fe27bcf43c3dc29b",
+ "zh:c4140fff9f692baf29236557f706f9515f93229413438527d764023a82301da3",
+ "zh:f8e9d83184e4bbeb97c6f0d569833007c48ba5a7ff334def201df4991d03a962",
+ ]
+}
+
+provider "registry.opentofu.org/hashicorp/random" {
+ version = "3.9.0"
+ constraints = "~> 3.0"
+ hashes = [
+ "h1:8EQU5KSxezcjo/phRSe69rDOI0lk4pSaggj7FsskYp8=",
+ "h1:Lw9im2VBBJQ3RyAbHPQ0rcvcmmcZWm3x+kIOpN+Tv9s=",
+ "h1:U8KXqGCoNI9/guYbTvzgdtVk3fRthoG0UXwm1JoEpIs=",
+ "h1:YXaVd4p6qXPPVaxIBaIDNXmBwT02ZqDn0qD+tYpw8sA=",
+ "h1:cOpc03fphEt/G9Rfc4jLL/fW0D7tgvlXqiDKPF4vuww=",
+ "h1:g09RR7T1xWkeGrZwWvWMT9ncJrFGr1k3CBD585UmO7w=",
+ "h1:gGDdPPibmw2EWROx+sh1RGLjR5+nPwZyrf6/N9jXfeM=",
+ "h1:haE7/nXCOhXKP4oXeEnER3t5CaVQWqujz4nBnpeTUv4=",
+ "h1:ieSVpfZS2lKuMr05ph0QsOVpCzg7uk3cgKBaXR+Ikug=",
+ "h1:ig2s1IS9IzehorRjvVAnKIsUUj8fkgyxct1L/kswcc4=",
+ "h1:j3lS+ZEERFnoab8t1ppDrScGVP/cgWbzlCrEYKTCXYw=",
+ "h1:lxezrKmOiQIySHAM+os8qLVq7hqufDr8h3Hpzvsk+78=",
+ "h1:lzRqBJAG+NETxHbEZUJ/YP3RMEjZBinTX7VmgH3lw60=",
+ "h1:tdSNWK5ApqUsgbdYieyeYLTu6nIZUV3hR1oFqUfAuGo=",
+ "h1:xedet8yH/zI2CfdxsGlK0nlFWc/Bp61yrWsEa3fHB8g=",
+ "zh:03f1114cc20b8913523735ab76e0f0a2b16ce13c92923a53304bf85f07fc0dbc",
+ "zh:105b678ee72322a3067f105d7e05e940f6143238f377f6e87ff4ec909246ac2a",
+ "zh:55f3bbf13ea18cbace61a706566a80f25f33fe2b1780b6f3d7b582af2a05b6d2",
+ "zh:63adf996db48f082f7a6351eb485e219cd88795fc71e6ec60a837263ab0d2cb1",
+ "zh:7e99550738a4e3cc68b8a467714b0d69371025fe95e3326d5323d026d55653e9",
+ "zh:8342b54af3a18a37e075eeae61be57f4de2ba71b35d95c5075d402dd2c1f289d",
+ "zh:83ee18e32ac9dd5fc91298554b7c4cfa4c3a1db50f4c797945637cc93c0844ae",
+ "zh:993ecc0adbf6bd535a59fbc9b735d8c33950e6f6eb5e621d750da9b71d65d80a",
+ "zh:ad722bc59d4edbf1415e827fc007c0efe6e0e9462d5568bae20b34be1058a261",
+ "zh:ae9448e1f87b2f9a6c5197a0e9862162ec6b137cb3a3835e11522995d8939e7c",
+ "zh:bc9cdd3aac784f759125c6627f6f6416e8726a1c184eb9cf3e55b9edbc94c627",
+ "zh:c8e35b89572ba1c40a9b20022e033a3395fb8d42e7604d50c900f193ba10382e",
+ "zh:e2deaa8a9975ef81d9f62baed12c41286918b0a10908e0e031f13f69a3b730a1",
+ "zh:ee39707557210a0ab1098aa357d2cdfe502e5a312d0dbdffb09d08facc4d3fc5",
+ "zh:f81afe4eb63e8aa9e0ea71be6c990f0dc69cb360e7191c0742a991f4a5081b64",
+ ]
+}
diff --git a/examples/microvm/README.md b/examples/microvm/README.md
new file mode 100644
index 0000000000..72bcf69260
--- /dev/null
+++ b/examples/microvm/README.md
@@ -0,0 +1,107 @@
+# Lambda MicroVM runner example
+
+This example creates the VPC and GitHub Actions runner control plane for one
+Linux ARM64 Lambda MicroVM lane. The lane uses ephemeral runners and
+just-in-time configuration, which are required by the MicroVM provider.
+
+The regional MicroVM foundation is provisioned separately by the
+[`microvm-foundation`](../microvm-foundation) example. Apply that example
+first and provide its artifact bucket, build role, and egress Network Connector
+outputs to the image build script. The image ARN produced by that build is then
+supplied to this example.
+
+The GitHub App credentials must already exist in SSM Parameter Store. The
+example outputs the webhook endpoint; configure that endpoint on the GitHub
+App with the same secret stored in the referenced SSM parameter.
+
+## Usage
+
+Build or download the Lambda archives into an S3 bucket, then create a
+`terraform.tfvars` file. The parameter references below are examples only:
+
+```hcl
+aws_region = "eu-west-1"
+lambda_artifact_bucket = "my-runner-lambda-artifacts"
+microvm_image_arn = "arn:aws:lambda:eu-west-1:123456789012:microvm-image:github-runner-arm64"
+egress_network_connector_arn = "arn:aws:lambda:eu-west-1:123456789012:network-connector:example"
+
+github_app = {
+ key_base64_ssm = {
+ arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-key"
+ name = "/github-runner/app-key"
+ }
+ id_ssm = {
+ arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-id"
+ name = "/github-runner/app-id"
+ }
+ webhook_secret_ssm = {
+ arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/webhook-secret"
+ name = "/github-runner/webhook-secret"
+ }
+}
+```
+
+Run Terraform from this directory:
+
+```bash
+terraform init
+terraform apply
+terraform output -raw webhook_endpoint
+```
+
+The MicroVM image must be built for Linux ARM64 and should use a versioned image
+ARN in production. Network connector egress remains bounded by the VPC route
+tables and network ACLs configured by the helper module.
+
+
+## Requirements
+
+| Name | Version |
+|------|---------|
+| [terraform](#requirement\_terraform) | >= 1.3.0 |
+| [aws](#requirement\_aws) | >= 6.33 |
+| [random](#requirement\_random) | ~> 3.0 |
+
+## Providers
+
+| Name | Version |
+|------|---------|
+| [random](#provider\_random) | 3.9.0 |
+
+## Modules
+
+| Name | Source | Version |
+|------|--------|---------|
+| [base](#module\_base) | ../base | n/a |
+| [runners](#module\_runners) | ../../modules/multi-runner | n/a |
+
+## Resources
+
+| Name | Type |
+|------|------|
+| [random_id.random](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/id) | resource |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| [aws\_region](#input\_aws\_region) | AWS Region where the runner control plane and MicroVM resources are deployed. | `string` | `"eu-west-1"` | no |
+| [egress\_network\_connector\_arn](#input\_egress\_network\_connector\_arn) | Regional Lambda Network Connector ARN used by MicroVMs and the image build. | `string` | n/a | yes |
+| [environment](#input\_environment) | Name prefix for the example resources. | `string` | `null` | no |
+| [github\_app](#input\_github\_app) | GitHub for API usages. | object({
id = string
key_base64 = string
}) | n/a | yes |
+| [ingress\_network\_connector\_arns](#input\_ingress\_network\_connector\_arns) | Optional regional Lambda Network Connector ARNs exposed to MicroVMs. | `list(string)` | `[]` | no |
+| [lambda\_artifact\_bucket](#input\_lambda\_artifact\_bucket) | S3 bucket containing the runner-control Lambda artifacts. | `string` | n/a | yes |
+| [microvm\_image\_arn](#input\_microvm\_image\_arn) | Lambda MicroVM image ARN produced by the MicroVM image build. | `string` | n/a | yes |
+| [microvm\_image\_version](#input\_microvm\_image\_version) | Optional immutable version of the Lambda MicroVM image. | `string` | `null` | no |
+| [organization\_runners](#input\_organization\_runners) | Register the MicroVM runners at organization scope when true. | `bool` | `false` | no |
+| [runners\_lambda\_s3\_key](#input\_runners\_lambda\_s3\_key) | S3 key for the runners Lambda archive. | `string` | `"runners.zip"` | no |
+| [runners\_maximum\_count](#input\_runners\_maximum\_count) | Maximum number of concurrent MicroVM runners. | `number` | `10` | no |
+| [webhook\_lambda\_s3\_key](#input\_webhook\_lambda\_s3\_key) | S3 key for the webhook Lambda archive. | `string` | `"webhook.zip"` | no |
+
+## Outputs
+
+| Name | Description |
+|------|-------------|
+| [microvm\_image\_arn](#output\_microvm\_image\_arn) | The MicroVM image ARN consumed by this runner configuration. |
+| [webhook\_endpoint](#output\_webhook\_endpoint) | Webhook endpoint to configure on the GitHub App. |
+
diff --git a/examples/microvm/main.tf b/examples/microvm/main.tf
new file mode 100644
index 0000000000..4c51dd52e1
--- /dev/null
+++ b/examples/microvm/main.tf
@@ -0,0 +1,109 @@
+locals {
+ environment = coalesce(var.environment, "microvm")
+ aws_region = var.aws_region
+}
+
+module "base" {
+ source = "../base"
+
+ prefix = local.environment
+ aws_region = local.aws_region
+}
+
+resource "random_id" "random" {
+ byte_length = 20
+}
+
+module "runners" {
+ source = "../../modules/multi-runner"
+
+ aws_region = local.aws_region
+ prefix = local.environment
+
+ experimental_features = ["multi-runner-v2"]
+
+ global_config_github = {
+ app = {
+ key_base64 = var.github_app.key_base64
+ id = var.github_app.id
+ webhook_secret = random_id.random.hex
+ }
+ }
+
+ global_config_lambda = {
+ artifact = {
+ s3 = {
+ bucket = var.lambda_artifact_bucket
+ }
+ }
+ }
+
+ global_config_orchestration_provider = {
+ webhook = {
+ runner = {
+ ephemeral = true
+ jit_config_enabled = true
+ maximum_count = var.runners_maximum_count
+ boot_time_in_minutes = 5
+ }
+ github = {
+ organization_runners = var.organization_runners
+ }
+ lambda = {
+ artifact = {
+ s3 = {
+ key = var.runners_lambda_s3_key
+ }
+ }
+ webhook = {
+ artifact = {
+ s3 = {
+ key = var.webhook_lambda_s3_key
+ }
+ }
+ }
+ }
+ }
+ }
+
+ global_config_ssm = {
+ paths = {
+ root = "/github-action-runners/${local.environment}"
+ }
+ }
+
+ global_config_compute_provider = {
+ aws = {
+ microvm = {
+ image_arn = var.microvm_image_arn
+ image_version = var.microvm_image_version
+ ingress_network_connectors = var.ingress_network_connector_arns
+ egress_network_connectors = [var.egress_network_connector_arn]
+ }
+ }
+ }
+
+ multi_runner_config = {
+ microvm = {
+ runner = {
+ os = "linux"
+ architecture = "arm64"
+ name_prefix = "microvm-"
+ extra_labels = ["microvm"]
+ }
+ orchestration_provider = {
+ webhook = {
+ matcherConfig = {
+ labelMatchers = [["self-hosted", "linux", "arm64", "microvm"]]
+ bidirectionalLabelMatch = true
+ }
+ }
+ }
+ compute_provider = {
+ aws = {
+ microvm = {}
+ }
+ }
+ }
+ }
+}
diff --git a/examples/microvm/outputs.tf b/examples/microvm/outputs.tf
new file mode 100644
index 0000000000..87ad4c924c
--- /dev/null
+++ b/examples/microvm/outputs.tf
@@ -0,0 +1,9 @@
+output "webhook_endpoint" {
+ description = "Webhook endpoint to configure on the GitHub App."
+ value = module.runners.webhook.endpoint
+}
+
+output "microvm_image_arn" {
+ description = "The MicroVM image ARN consumed by this runner configuration."
+ value = var.microvm_image_arn
+}
diff --git a/examples/microvm/providers.tf b/examples/microvm/providers.tf
new file mode 100644
index 0000000000..eca2fe96a7
--- /dev/null
+++ b/examples/microvm/providers.tf
@@ -0,0 +1,9 @@
+provider "aws" {
+ region = local.aws_region
+
+ default_tags {
+ tags = {
+ Example = local.environment
+ }
+ }
+}
diff --git a/examples/microvm/variables.tf b/examples/microvm/variables.tf
new file mode 100644
index 0000000000..4f8eaa3bc9
--- /dev/null
+++ b/examples/microvm/variables.tf
@@ -0,0 +1,72 @@
+variable "aws_region" {
+ description = "AWS Region where the runner control plane and MicroVM resources are deployed."
+ type = string
+ default = "eu-west-1"
+}
+
+variable "environment" {
+ description = "Name prefix for the example resources."
+ type = string
+ default = null
+}
+
+variable "github_app" {
+ description = "GitHub for API usages."
+
+ type = object({
+ id = string
+ key_base64 = string
+ })
+}
+
+
+variable "lambda_artifact_bucket" {
+ description = "S3 bucket containing the runner-control Lambda artifacts."
+ type = string
+}
+
+variable "runners_lambda_s3_key" {
+ description = "S3 key for the runners Lambda archive."
+ type = string
+ default = "runners.zip"
+}
+
+variable "webhook_lambda_s3_key" {
+ description = "S3 key for the webhook Lambda archive."
+ type = string
+ default = "webhook.zip"
+}
+
+variable "microvm_image_arn" {
+ description = "Lambda MicroVM image ARN produced by the MicroVM image build."
+ type = string
+}
+
+variable "microvm_image_version" {
+ description = "Optional immutable version of the Lambda MicroVM image."
+ type = string
+ default = null
+}
+
+variable "egress_network_connector_arn" {
+ description = "Regional Lambda Network Connector ARN used by MicroVMs and the image build."
+ type = string
+}
+
+variable "ingress_network_connector_arns" {
+ description = "Optional regional Lambda Network Connector ARNs exposed to MicroVMs."
+ type = list(string)
+ default = []
+}
+
+variable "organization_runners" {
+ description = "Register the MicroVM runners at organization scope when true."
+ type = bool
+ default = false
+}
+
+variable "runners_maximum_count" {
+ description = "Maximum number of concurrent MicroVM runners."
+ type = number
+ default = 10
+}
diff --git a/examples/microvm/versions.tf b/examples/microvm/versions.tf
new file mode 100644
index 0000000000..8ace4cfd44
--- /dev/null
+++ b/examples/microvm/versions.tf
@@ -0,0 +1,13 @@
+terraform {
+ required_providers {
+ aws = {
+ source = "hashicorp/aws"
+ version = ">= 6.33"
+ }
+ random = {
+ source = "hashicorp/random"
+ version = "~> 3.0"
+ }
+ }
+ required_version = ">= 1.3.0"
+}
diff --git a/images/README.md b/images/README.md
index 689f3e2df5..c6722c2c2f 100644
--- a/images/README.md
+++ b/images/README.md
@@ -39,3 +39,16 @@ ami_owners = ["map(object({
name = string
vpc_id = string
subnet_ids = list(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..f1a2621728
--- /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_index, subnet_id in connector.subnet_ids :
+ "${connector_key}/${subnet_index}" => {
+ 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..5a444e3489
--- /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_index, subnet_id in each.value.subnet_ids :
+ data.aws_subnet.selected["${each.key}/${subnet_index}"].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..c21e6068c0
--- /dev/null
+++ b/modules/microvm-foundation/variables.tf
@@ -0,0 +1,154 @@
+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 = "Name for the regional MicroVM build-artifact bucket."
+}
+
+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 = list(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(distinct(connector.subnet_ids)) == length(connector.subnet_ids)
+ )
+ ])
+ error_message = "Each network connector must contain distinct subnet IDs."
+ }
+
+ 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..40dc4af0e8
--- /dev/null
+++ b/modules/microvm-foundation/versions.tf
@@ -0,0 +1,13 @@
+terraform {
+ required_version = ">= 1.5.6"
+ required_providers {
+ aws = {
+ source = "hashicorp/aws"
+ version = ">= 6.61"
+ }
+ time = {
+ source = "hashicorp/time"
+ version = ">= 0.13"
+ }
+ }
+}
diff --git a/tests/ministack/README.md b/tests/ministack/README.md
index 3e6cdc82fa..0f1a1965d7 100644
--- a/tests/ministack/README.md
+++ b/tests/ministack/README.md
@@ -5,7 +5,9 @@ The MiniStack workflow runs the `base`, `prebuilt`, `default`, `ephemeral`,
with Terraform 1.5.6 and the latest Terraform release, and with OpenTofu 1.11
and the latest OpenTofu 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
+in this directory. The `microvm-foundation` example uses the reusable `base`
+example module to create its VPC and private subnets, then wires those outputs
+into the MicroVM Network Connector. 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
creates and removes a temporary AMI override for `default` and
diff --git a/tests/ministack/microvm-foundation.tfvars b/tests/ministack/microvm-foundation.tfvars
new file mode 100644
index 0000000000..253a9ffaba
--- /dev/null
+++ b/tests/ministack/microvm-foundation.tfvars
@@ -0,0 +1,3 @@
+aws_region = "eu-west-1"
+
+artifact_bucket_name = "ministack-microvm-artifacts-eu-west-1"
diff --git a/tests/ministack/microvm.tfvars b/tests/ministack/microvm.tfvars
new file mode 100644
index 0000000000..c777b7228a
--- /dev/null
+++ b/tests/ministack/microvm.tfvars
@@ -0,0 +1,12 @@
+
+aws_region = "eu-west-1"
+environment = "microvm-ministack"
+
+github_app = {
+ id = "your-github-app-id"
+ key_base64 = "your-github-app-key-base64"
+}
+
+lambda_artifact_bucket = "github-actions-runner-microvm-ministack"
+microvm_image_arn = "arn:aws:lambda:eu-west-1:000000000000:microvm-image:ministack"
+egress_network_connector_arn = "arn:aws:lambda:eu-west-1:000000000000:network-connector:ministack"
diff --git a/tests/ministack/run-example.sh b/tests/ministack/run-example.sh
index 961b3a8cab..bb52c7689a 100755
--- a/tests/ministack/run-example.sh
+++ b/tests/ministack/run-example.sh
@@ -21,9 +21,8 @@ case "$iac_binary" in
exit 64
;;
esac
-
case "$example" in
- base | prebuilt | default | ephemeral | multi-runner | multi-runner-v2)
+ base | prebuilt | default | ephemeral | multi-runner | multi-runner-v2 | microvm-foundation | microvm)
use_tfvars=true
;;
migration-test)
@@ -33,7 +32,7 @@ case "$example" in
use_tfvars=false
;;
*)
- echo "Supported examples for the runner are: base, prebuilt, default, ephemeral, multi-runner, multi-runner-v2, migration-test, termination-watcher" >&2
+ echo "Supported examples for the runner are: base, prebuilt, default, ephemeral, multi-runner, multi-runner-v2, microvm-foundation, microvm, migration-test, termination-watcher" >&2
exit 64
;;
esac
@@ -41,7 +40,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|migration-test|termination-watcher} [TFVARS_FILE]" >&2
+ echo "Usage: $0 {init|plan|apply|destroy} {base|prebuilt|default|ephemeral|multi-runner|multi-runner-v2|microvm-foundation|microvm|migration-test|termination-watcher} [TFVARS_FILE]" >&2
exit 64
;;
esac
@@ -80,15 +79,17 @@ if [ "$use_tfvars" = true ]; then
tfvars_file="$script_dir/$example.tfvars"
fi
- case "$tfvars_file" in
- /*) ;;
- *) tfvars_file="$PWD/$tfvars_file" ;;
- esac
+ if [ -n "$tfvars_file" ]; then
+ case "$tfvars_file" in
+ /*) ;;
+ *) tfvars_file="$PWD/$tfvars_file" ;;
+ esac
- if [ ! -f "$tfvars_file" ]; then
- echo "Terraform variables file not found: $tfvars_file" >&2
- echo "Pass it as the third argument or set MINISTACK_TFVARS_FILE." >&2
- exit 66
+ if [ ! -f "$tfvars_file" ]; then
+ echo "Terraform variables file not found: $tfvars_file" >&2
+ echo "Pass it as the third argument or set MINISTACK_TFVARS_FILE." >&2
+ exit 66
+ fi
fi
fi
@@ -96,6 +97,7 @@ lambda_fixture_dir=""
lambda_created_paths=""
ami_created_ids=""
ssm_created_names=""
+s3_created_buckets=""
override_created_paths=""
lambda_zip_paths="
$source_root/lambdas/functions/ami-housekeeper/ami-housekeeper.zip
@@ -118,6 +120,12 @@ cleanup() {
ministack_aws ssm delete-parameter --name "$name" >/dev/null 2>&1 || true
done
+ for bucket in $s3_created_buckets; do
+ ministack_aws s3api delete-object --bucket "$bucket" --key runners.zip >/dev/null 2>&1 || true
+ ministack_aws s3api delete-object --bucket "$bucket" --key webhook.zip >/dev/null 2>&1 || true
+ ministack_aws s3api delete-bucket --bucket "$bucket" >/dev/null 2>&1 || true
+ done
+
for image_id in $ami_created_ids; do
ministack_aws ec2 deregister-image --image-id "$image_id" >/dev/null 2>&1 || true
done
@@ -225,6 +233,25 @@ create_ssm_fixture() {
$name"
}
+create_s3_fixture() {
+ bucket="$1"
+ key="$2"
+ file="$3"
+
+ if ! ministack_aws s3api head-bucket --bucket "$bucket" >/dev/null 2>&1; then
+ ministack_aws s3api create-bucket \
+ --bucket "$bucket" \
+ --create-bucket-configuration LocationConstraint="$AWS_DEFAULT_REGION" >/dev/null
+ s3_created_buckets="$s3_created_buckets
+$bucket"
+ fi
+
+ ministack_aws s3api put-object \
+ --bucket "$bucket" \
+ --key "$key" \
+ --body "$file" >/dev/null
+}
+
create_ami_override() {
override_file="$example_root/zz_ministack_ami_override.tf"
printf '%s\n' \
@@ -325,6 +352,25 @@ $lambda_zip"
create_ami_fixture "ministack-v2-linux-x64" x86_64 >/dev/null
create_ami_fixture "ministack-v2-windows-x64" x86_64 >/dev/null
;;
+ microvm)
+ create_ssm_fixture \
+ "/ministack/microvm/github-app-key" \
+ "test-only"
+ create_ssm_fixture \
+ "/ministack/microvm/github-app-id" \
+ "123456"
+ create_ssm_fixture \
+ "/ministack/microvm/webhook-secret" \
+ "test-only"
+ create_s3_fixture \
+ "github-actions-runner-microvm-ministack" \
+ "runners.zip" \
+ "$lambda_fixture_dir/ministack-lambda.zip"
+ create_s3_fixture \
+ "github-actions-runner-microvm-ministack" \
+ "webhook.zip" \
+ "$lambda_fixture_dir/ministack-lambda.zip"
+ ;;
esac
}