diff --git a/.github/workflows/ministack.yml b/.github/workflows/ministack.yml index 32844c13e6..09aac4132a 100644 --- a/.github/workflows/ministack.yml +++ b/.github/workflows/ministack.yml @@ -51,14 +51,15 @@ jobs: - prebuilt - default - ephemeral + - microvm-foundation - multi-runner - - multi-runner-v2 - multi-runner-scale-set - migration-test + - multi-runner - termination-watcher services: ministack: - image: ghcr.io/ministackorg/ministack:1.5.13@sha256:ce3c906f2866ff953ce4c56f06b1fa3e453bc32e41c00de17b5f5a8672c5a42c + image: ghcr.io/ministackorg/ministack:1.5.16@sha256:9813da34285a0760477c761c1c03717e0290d259213c0ca97f551fefd87b292d ports: - 4566:4566 env: 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 e8747e10e2..9c16821875 100644 --- a/.github/workflows/terraform.yml +++ b/.github/workflows/terraform.yml @@ -51,8 +51,8 @@ env: prebuilt ephemeral termination-watcher + microvm-foundation multi-runner - multi-runner-v2 multi-runner-scale-set external-managed-ssm-secrets TEST_MODULES: | diff --git a/docs/examples/index.md b/docs/examples/index.md index 50aff55389..a4a3e7e1a7 100644 --- a/docs/examples/index.md +++ b/docs/examples/index.md @@ -5,10 +5,11 @@ Examples are located in the [examples](https://github.com/github-aws-runners/ter - _[Default](default.md)_: The default example of the module - _[Ephemeral](ephemeral.md)_: Example usages of ephemeral runners based on the default example. - _[Multi Runner](multi-runner.md)_ : Example usage of creating a multi runner which creates multiple runners/ configurations with a single deployment. The examples including: "arm64", "windows", and "ubuntu" runners. -- _[Multi Runner v2](multi-runner-v2.md)_ : Example usage of the experimental v2 multi-runner configuration interface with shared defaults and per-lane overrides. +- _[Multi Runner Webhook](multi-runner-webhook.md)_: Example usage of one webhook deployment serving EC2 and Lambda MicroVM runner lanes. - _[Multi Runner scale-set](multi-runner-scale-set.md)_ : Example usage of a v2 deployment combining webhook lanes with an experimental GitHub Actions scale-set lane. - _[Permissions boundary](permissions-boundary.md)_: Example usages of permissions boundaries. - _[Prebuilt Images](prebuilt.md)_: Example usages of deploying runners with a custom prebuilt image. - _[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. 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/multi-runner-v2.md b/docs/examples/multi-runner-v2.md deleted file mode 100644 index 565b601ecb..0000000000 --- a/docs/examples/multi-runner-v2.md +++ /dev/null @@ -1 +0,0 @@ ---8<-- "examples/multi-runner-v2/README.md" diff --git a/docs/examples/multi-runner-webhook.md b/docs/examples/multi-runner-webhook.md new file mode 100644 index 0000000000..19d24d5994 --- /dev/null +++ b/docs/examples/multi-runner-webhook.md @@ -0,0 +1 @@ +--8<-- "examples/multi-runner-webhook/README.md" diff --git a/docs/microvm-runners.md b/docs/microvm-runners.md new file mode 100644 index 0000000000..872dd52de0 --- /dev/null +++ b/docs/microvm-runners.md @@ -0,0 +1,129 @@ +# Lambda MicroVM Runners (Experimental) + +!!! warning + Lambda MicroVM runner support is experimental. The image build, lifecycle-hook server, control-plane integration, and AWS MicroVM APIs must be configured together. Validate the complete flow in a non-production environment before relying on it for workloads. + +## Overview + +Lambda MicroVM runners provide ephemeral GitHub Actions runners backed by +Lambda MicroVMs. The runner control plane receives demand, obtains the +one-time runner configuration, starts a MicroVM from a published image, and +passes the runtime execution role to the MicroVM. + +The repository includes a combined [multi-runner webhook example](examples/multi-runner-webhook.md) +that places EC2 and Lambda MicroVM lanes behind one webhook endpoint. The +provider-specific lifecycle checks are shared where possible, so the same +deployment can validate both providers. + +## Prerequisites + +Before deploying the MicroVM runner lane, prepare all of the following in the +target AWS Region: + +1. **MicroVM foundation.** Apply the + [MicroVM foundation example](examples/microvm-foundation.md). It creates the + regional artifact bucket, Lambda Network Connectors, the image-build role, + and the reusable MicroVM usage policy. +2. **Lifecycle-hook artifact.** Build and release + `lambdas/services/microvm-lifecycle-hooks` through the same workspace + artifact process used for the repository's Lambda services. The resulting + ZIP is embedded in the MicroVM image. +3. **Published MicroVM image.** Use the + [MicroVM Ubuntu image instructions](https://github.com/github-aws-runners/terraform-aws-github-runner/blob/main/images/microvm-ubuntu/README.md) + to build and publish an image with Packer. The image must contain the + compatible lifecycle-hook server and runner entrypoint. +4. **Runner execution role.** Configure the runner role through the runner + configuration. This is different from the foundation's build role. The + control-plane TypeScript passes the execution role to `RunMicrovm`, so the + Lambda that starts the MicroVM must have permission to pass it. +5. **Runner control plane and artifacts.** Deploy the runner control plane with + the published image ARN/version, Network Connector ARNs, GitHub App + configuration, and the runner-control and webhook Lambda ZIPs. + +The foundation does not create the image or the runner execution role. The +image build does not choose the runtime role. These are separate dependencies +owned by the image build and runner-control-plane stages respectively. + +## IAM roles + +MicroVM deployments use two roles for two different operations: + +| Role | Used by | Responsibility | +| --- | --- | --- | +| Build role (`build_role_arn`) | Packer/image publisher | Creates and publishes the MicroVM image and accesses the foundation build artifacts. | +| Execution role | Runner control plane and the MicroVM | Is passed to `RunMicrovm` and provides the permissions used by the ephemeral runner at runtime. | + +Do not use the build role as the runner execution role. The control-plane +Lambda needs `iam:PassRole` for the configured execution role, and the +execution role must contain the runtime permissions required by the selected +runner lane. + +## Deployment order + +The complete dependency chain is: + +```text +MicroVM foundation + | + v +Build/release lifecycle-hook server + | + v +Packer builds and publishes image + | + v +Runner control plane resolves execution role + | + v +RunMicrovm starts an ephemeral runner +``` + +The lifecycle-hook server is part of the image artifact. Updating the hook +server therefore requires building/releasing the artifact and publishing a +new compatible image before deploying that image version to the runner lane. + +## Combined EC2 and MicroVM deployment + +The [multi-runner webhook example](examples/multi-runner-webhook.md) accepts +explicit `runners_lambda_zip` and `webhook_lambda_zip` inputs and configures +both compute providers behind one webhook. Its MicroVM settings require a +published image: + +```hcl +compute_provider = { + aws = { + microvm = { + image_arn = "arn:aws:lambda:eu-west-1:123456789012:microvm-image:gha-ubuntu-arm64" + image_version = null + ingress_network_connectors = [] + egress_network_connectors = ["arn:aws:lambda:eu-west-1:123456789012:network-connector:example"] + } + } +} +``` + +Use the example's complete Terraform configuration as the source of truth for +the current input shape. The example deploys the control plane; it does not +build the foundation, lifecycle-hook artifact, or MicroVM image for you. + +## Known limitations + +- This integration is experimental and depends on AWS Lambda MicroVM APIs and + the lifecycle-hook protocol. +- A compatible lifecycle-hook server must be present in every image used by + the MicroVM provider. +- Image publication and activation are separate from Terraform deployment; + wait for the image version to become active before starting jobs. +- The build role and execution role are intentionally separate. Changes to + either role can affect a different stage of the lifecycle. +- The combined webhook example is useful for integration testing, but a real + deployment still needs a real MicroVM image and the network/runtime IAM + configuration described above. + +## Repository examples + +- [MicroVM foundation](examples/microvm-foundation.md) +- [MicroVM image build README](https://github.com/github-aws-runners/terraform-aws-github-runner/blob/main/images/microvm-ubuntu/README.md) +- [Lifecycle-hook service README](https://github.com/github-aws-runners/terraform-aws-github-runner/blob/main/lambdas/services/microvm-lifecycle-hooks/README.md) +- [Multi-runner webhook](examples/multi-runner-webhook.md) +- [MicroVM foundation module](modules/public/microvm-foundation.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..c52c872d47 --- /dev/null +++ b/examples/microvm-foundation/README.md @@ -0,0 +1,93 @@ +# 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 deployment order is: + +1. Apply this foundation to create the regional bucket, Network Connectors, + build role, and reusable runtime policy. +2. Build and release the lifecycle-hook service from + `lambdas/services/microvm-lifecycle-hooks` using the repository's normal + Lambda artifact process. +3. Build and publish the MicroVM image with Packer, passing the foundation + outputs and the released lifecycle-hook ZIP. The image builder uses the + **build role**. +4. Deploy the runner control plane, such as + `examples/multi-runner-webhook`, with the published image ARN/version. The + control plane resolves the **execution role** from the runner configuration + and passes it to `RunMicrovm` when it starts a job. + +The two roles must not be conflated: the build role creates the image, while +the execution role runs the ephemeral GitHub Actions runner inside that image. +The foundation module owns regional storage, build IAM, Network Connectors, +and the reusable runtime policy. It does not publish an image, create the +execution role, or create the runner control plane. + + +## 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..0f05980124
--- /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 = []
+}
\ No newline at end of file
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/multi-runner-v2/.terraform.lock.hcl b/examples/multi-runner-v2/.terraform.lock.hcl
deleted file mode 100644
index 62c535d49e..0000000000
--- a/examples/multi-runner-v2/.terraform.lock.hcl
+++ /dev/null
@@ -1,93 +0,0 @@
-# This file is maintained automatically by "terraform init".
-# Manual edits may be lost in future updates.
-
-provider "registry.terraform.io/hashicorp/aws" {
- version = "6.64.0"
- constraints = ">= 5.0.0, >= 6.21.0, >= 6.33.0"
- hashes = [
- "h1:2fTLxzUDmp/KVIHbIeLTB4bIzWHx8E6Dw+1ALLUi+Yw=",
- "h1:wXARLY+IeQ7ufYxCLTPCwToWGMRvOpiOTfJS97iwUzI=",
- "zh:07172315d67bc9781240272759cdfc7bd32b7e72384a56862c2c1da3cca99a81",
- "zh:154ce7d2659de9a59ddfe96d7cab41a9ddc2cb267a7d4bcdf4e737ff2ffdec06",
- "zh:17324d4335a7a7ac01cc23eded530775606680ff53b47cb74a3cb95d1121f836",
- "zh:307ab92324ec5a61b124881ab8cac1d9e316f4527dfd0e1b59794c229407eb4e",
- "zh:31e25f1903661332e36a95283042dd3ec50b47c186db00663fbd976a11e6a6b2",
- "zh:3311d9f3bd12a24886027dbe73859dcd1e67bd0e3046227a338cf2c7ca04d18e",
- "zh:37916156a3aac3b29be3acebd15d53145ea4ab5d4aaa825eaebe75481fa00500",
- "zh:4158cb8c38b3ac6aa98eb15935ec6bd7c30838d85d2b00acc9812df8382ae908",
- "zh:5bfb9499c66d9db5b34dc5c60f426a1ab1baa5457ce2aefebca826a9c3f92fb0",
- "zh:6eb29ead5a4aca3b1f35812e7e8c75419180e1928e479b458f206861277736db",
- "zh:7a82b6dd0c0cdef8045a4adfbddd36acb86b6b23fcbed8e189c2d71f7dc4a502",
- "zh:9556bd792032c3f7e73ea4dd08cec88dc1327f5a4a57d79c30ba844ae2b9a3c0",
- "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
- "zh:c5234180464cb800c83a41f57462742b802c150ad7d4417626fcd9cb511c01d2",
- "zh:cd776b83b1f7b36635957350afe7ce28ba4e4ea3a5e2deb00d13dbd3b35d9d40",
- "zh:fb583a7b791c6f915b86573d04f05ddbf7f1a5e4120c5d8a7450a3086c1225c4",
- ]
-}
-
-provider "registry.terraform.io/hashicorp/local" {
- version = "2.9.0"
- constraints = "~> 2.0"
- hashes = [
- "h1:9rBZCMNpxKwMlRbWH2QpwD3kqUCAejdOZQ/aiiDObXQ=",
- "h1:m24fjcInWvTVZ1XSo2MaNuKPe+X/gfG8SIi09rA7a7M=",
- "zh:0baa4566cf77f1ff52f4293d1c8536202dd23edc197c3196413a28343c3ac3a0",
- "zh:16b5559c3c07088ddad11a9bb9e9c0799999363c2958e9a5be2bcbbf2cd9ca64",
- "zh:197c79015a10d1cce904a8ea722cbc750c42aeae2da53f44a6a0751d9fd1aa90",
- "zh:29d0b03e5343a80677ebfeb2e2c31cbe4b1f65e736e53417454a4277fec2544c",
- "zh:4896bfa6cf1d2fd562b47ef2e87f47862ae92a04f8ad5d764380f0c6653473b8",
- "zh:531f8529cbca49f681883e57761a05a8398afaef6d1ab0d205d26bf12f4428e8",
- "zh:6aaf5011d83161c86d2bfb80c0923ec934e578288758da2f37acb7aec129004b",
- "zh:7430275253d3d3c40aa6179e0ec0d63212874dbbc06c5a51b9d07ec590f9756c",
- "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
- "zh:be17dc611e95e26cdf6cad79dfccf1064f0e32032a2efeb939a9bbe7fb1cbfe9",
- "zh:f0e3b0aa644202e1d79d2000dca91f6019425da71e9800fa23f27e51c034f195",
- "zh:f62bae4519e4ead49182ddc8afe8cf61e2a4c3ba3973b0fbba967736a2696aa3",
- "zh:fcafa360a5b0b96244f26f4e3a6d642b716a376557142c2442ff2fb12d11da18",
- ]
-}
-
-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/multi-runner-v2/.terraform.lock.hcl.tofu b/examples/multi-runner-v2/.terraform.lock.hcl.tofu
deleted file mode 100644
index e8fb9a5cf8..0000000000
--- a/examples/multi-runner-v2/.terraform.lock.hcl.tofu
+++ /dev/null
@@ -1,150 +0,0 @@
-# This file is maintained automatically by "tofu init".
-# Manual edits may be lost in future updates.
-
-provider "registry.opentofu.org/hashicorp/aws" {
- version = "6.64.0"
- constraints = ">= 5.0.0, >= 6.21.0, >= 6.33.0"
- hashes = [
- "h1:/G38+XhC1mBVkmeWdtk/wk7lX2BxviJ2XZ70dpoaKKQ=",
- "h1:7BzHdGCBG5usqOIhfBq89dkdUopnSo+qe9qRCKDSRHc=",
- "h1:8AgY9Hc5/R5j97WgCcDSlbuKk0pjk3vkp7oz4mtGVY8=",
- "h1:DKdOy/0RfYLxpzAXBPWTO5Eusvqx5UoGxiq2J0E6DY4=",
- "h1:KSwetpR4S2eUsKmHftt73Cbx72lPWYET4V+Ej05rnkI=",
- "h1:MEi5Ecge1Uwx/DRGfdVDzV5Q/soRxKh6dBHxUjGdaDQ=",
- "h1:VqjWicgPZW32+YnSe0Lo78qq8/24I8XNV+E9d/lBz/4=",
- "h1:WBgbFHdg/3ekWoAH6UeKiwfk6iqLr1f7TX9R/mJUK8M=",
- "h1:YisB3zMV5Kh6p5/eVuPAAPEmudD/UqGN4C/V3zRtAq4=",
- "h1:bG5dXqR4mSlcebUG+anerOWYDyeaScZJeLSJk0cYBfE=",
- "h1:iosW/imG2pc4La7qdeM/rK6ldMXhcU6YVW7tjqwNXtI=",
- "h1:nKE1gnLZxIoqukQ1YI9EUdmrQIUeAN4PWb5ecN8U9K8=",
- "h1:x0hJO5+On8FaKExr4p2cNJhWsNWFZq1EiDD6CfVwy2E=",
- "h1:yPH75sRH+f3aJlJAloOL/BikeZV6/0GP8VQvnJoMRKM=",
- "h1:zCWB5ZD98/ZC0a50HTGoC/fTAseh189xxCFEL5Mt7r4=",
- "zh:06e09ced9480ae12578122f7a25758a15d8fe684da0f6a0a61b9bc2f4a4918ad",
- "zh:2035805f0ed8bf81d493e7a52f22965b3d5d402687a95d1caa8c4b1b348c1264",
- "zh:25fe72a3d6a330eab6c8957f9e6bdf297ffdce95fa059fef30b80da764bee6b2",
- "zh:49df644d19e39b9947e84609260028687057191ddd941783c0211386ade53040",
- "zh:4d8438a5d25f18eb376c8375c70f81afb79d0fc1e63ebb6df1d0e02287964dde",
- "zh:5cd9717e819506132126a896e959cd4cf1bb213c033c37777c9d01a593937e2c",
- "zh:6955caa4f435373ae870de31bdda85e51c60b68c51a4206df5a21b853bcefe21",
- "zh:82a413500c35241745e097797610d2bff57c26e29f34ca711182fdde5c265d13",
- "zh:831f78acce42a759a977769b0409387ec13ff64b4f46c24eb7e7662e0f352525",
- "zh:88648a159119a0435bf86c6cd1f7482dc43dfa2eb742f9b29053da1ee9fdabd8",
- "zh:9fc745d71a2e36dbdae0ee69be70675509e5a9dec1a3c5a9be6007e568d78c07",
- "zh:bf6d11d6ed1655f61f70eb2906e5d1f7ff5e78b6539dcfb3116ed6f8960c9e20",
- "zh:ca17a6ca363afe930ad3474966d39cb549b7f1e5efdca909972dd26f90eefc89",
- "zh:d7e9cc87ada1314e6d8ecc5849385a8f8f45757bd2c145b8657f015c65e5078d",
- "zh:eddb4d6d86700788d132ba2a83d306646ccb2a3a0cec0a0ab3e215307c61d8f2",
- ]
-}
-
-provider "registry.opentofu.org/hashicorp/local" {
- version = "2.9.0"
- constraints = "~> 2.0"
- hashes = [
- "h1:1dtKYW/5a1qob3yneL6WzOlnSGfYtJ6a2XeejCk9yb4=",
- "h1:5NseXq5wU8O20ersTtV4ocrLYFFtgFr7n0pRLO1W2Rw=",
- "h1:5d22ZPPK4iiygPbwRz/PJF5Es/0axVpMlPRpCR0Padw=",
- "h1:AnwyolirmIlBMjH6+tV8bKkvT+5axJNYxi2y2IguiX4=",
- "h1:PBp+HeseY021Fw3sLznCG27idgwPoff4cBuNmKgPL2w=",
- "h1:VDxIhe4GbzdOCdmt7mQaqdwERQW6GSI7Roonts42Gr0=",
- "h1:ZO6eWWnf8LjjV1q/JNeL9WLtZ6fwIttOnyN5LjCNSEo=",
- "h1:dPIAf8oUAz+vW2E0iZunMvpuPddRZIztRsPSY1u+VnY=",
- "h1:fwTDVG9AhFVKQZIb1EXkHv4FqzsZNlLWgkyPGDmZZEE=",
- "h1:kDc465XPC7/6XFCjrMC4mTqhA9ef0FHKuJ3ZgfGNfeg=",
- "h1:kGbjxrI2P8MHeyVtE1U3Q1TbyF71ExnHxtkrE+Aj6UU=",
- "h1:kcoK6Afbsj54u9zaEqpecWAFKytqjBijtguCNwV3d4M=",
- "h1:rxomJjDwOo+YZ+WIPc25FqEgsz9orh/2MCyUcZmFjvw=",
- "h1:t0CMn/Rkwquw8l2yQ+O4ApzbMZfY2UazbsDnZygzACA=",
- "h1:tJwgm2BS4xCGlElCDQEFXQoefY9Y4t0JdSKTtsPBbBo=",
- "zh:13ef7ecd1e397ec5b20ea588508dd3e3b8d6c50d809ae76b079abf9dd8d02e4b",
- "zh:2190c9325980076489ce02b0f5dd2c0b91fc8711cefa99e714d8619a32827ad1",
- "zh:2a0cfc5600730093705071707e4a4e4e953e7d9091859e0f66b46daa1060dd5d",
- "zh:2ff53eac1af43ab9a2248a0e53c963d46e19cf04bc4c3f323591cfcebb218252",
- "zh:4ebc3dee700f60af9da29970052fd02fa947813162b224716862dc9d7f1f7542",
- "zh:5fe6dab84ceeaa8eb3f1567c5f05578333370c472240ca5c5bfc25e92d4d5586",
- "zh:66bbec16367bbf440045502c9779b11f4ac5b022c8d8d17afe12d431950838b5",
- "zh:7641e5c2e4b529e869cde29ab5b1de2fd1091489eb745b19ac2709bd7f4dfd84",
- "zh:855bfba0756d17ce07595ff57d7cf664443d1495127cb88fb063362734b8b22a",
- "zh:aaec10f237921d60c581d1b7a66f0a8a8019d9802dc04af11b5b981f6682e01d",
- "zh:e460835a38ffa1e74f6929904bfd14ef473d217fd537b7ce834abe5ce5e2ce07",
- "zh:ecc4295215db0e4aea3c9329611c31e09a853e1ae207d56742403bd4f5516703",
- "zh:ee6d9fae63a612072e00402894e14826af7a3351c235b9c5b423b7629a77ca29",
- "zh:f2b5c8db74aa7ebcf7cd423672358437d42401675069ef67b01ff910054e49d5",
- "zh:f5aff74d3eb96d4592c7bca5cd3ea89b469e84efbf382944bd0f844a57059c09",
- ]
-}
-
-provider "registry.opentofu.org/hashicorp/null" {
- version = "3.3.2"
- constraints = "~> 3.0, ~> 3.2"
- hashes = [
- "h1:1T+00cjQNmRAHAz9xjEBFpf5wRRb0IBuXS/W8ke5BWs=",
- "h1:46gmIYe+klib6TlHKSqEkMLjvnzVWiCB2NYA2zR8MX8=",
- "h1:7WQ3wjfaeqnXxq+a8cYiYeWUnMTgY1JcuX+z7sZd72s=",
- "h1:MVM+vkVtW/YyKfn111pyho0y87I4TekaNMbBLkn0/C8=",
- "h1:QBcIbI2Dp4v6Iui37pn4qmw8YeiFLbSWcJuzZVl/65Y=",
- "h1:SsVKTUR+vgLaC1YnoDa2fnYpzREcgNgWRcu5x+vwjHA=",
- "h1:WUaeuTNn9w6UXZ9cMq4+qZy4ZAr71B9NcUDEXjqfdKs=",
- "h1:WtEaA7alasNwEQ4L3+KyQtbkSOPsexzJ4LUZ7PKaycI=",
- "h1:WxS7rjYIZ1WQc4GkICch8XrbxoSY8TjUfLbPDo6oEcQ=",
- "h1:Ysvc/FPvcwk+iMg7IcLkqZhT/KhtZTYji+UBqMlcTs4=",
- "h1:ZLjbXnfVcRvS/DAN3BcNebOsnOcs3Nx6mJpFCj4dZ2c=",
- "h1:fAmvQjIGyqdGMc+v/fUEINCyuU4iaKSzsV8PWsOnmAc=",
- "h1:jwkbEtf3S7W+Bl4soynNkUHFfK/4I/H74urHY758XVw=",
- "h1:qY1sKzlxNTp/dqZR23bM4egmVMRlaQudLlBYraMt1pw=",
- "h1:t8H1KNwJQwKE/GqpHeRxOWgMk0Yv35qbBTBzqB/rhr0=",
- "zh:09e94b0b7dfc0c6450c247517b5410546039c758e513d89b588af6df70c3d57d",
- "zh:0b72497b6fd79a2b04785b64890a565a8cc7b06ded95da05e6dab2f3b8a02d58",
- "zh:1c0ee6f81f7bcdec8d568a145a450eb57a6f1cfe5e48943375d1af22ed54151e",
- "zh:1c6899b475f035d352af1e7f33dc30beab8b8e3784f8cb55a2cc4a11997fbd66",
- "zh:43e57a2a56e9874604501bdebe431bb573fb77d2c5f4d7598ab30727dc0e90ea",
- "zh:49cf2f36298a5ac3ac8d80ceb87466e6a99d2c021005bcd9e3a79f2314fd0a13",
- "zh:665be40d2c7f3d768b8f39a041371526d4b7b396b4c11e162a1886212da176c9",
- "zh:6bb1583d88ddb38b1c6b4624e25ad414ddd8bf65b0dd9c074580847311f83924",
- "zh:71d64453bdc795667e9841d7c90e3fef6ff157e0598d51dac4bb1e4583b85407",
- "zh:73ac02bc3b680e1ea75aab24ec2a359c8f0a021f73d43b2feae0a899e75a93ad",
- "zh:b2b777ee07b910e7345df85321fab6c9a25c33ecb56129758dda8fadaba09fe4",
- "zh:bb984d52880749a49e509e3b243804869e1af40ee2d34322a30f5f340f8d8dbd",
- "zh:e0724bc083527343b4a4099fd4f95511e49a0e113416cdba58a64446742b68b1",
- "zh:e391c14e367cd64d986ddc8d81f2db76d49600ce0521720618ff1ecc0decde7f",
- "zh:e95c1af8e8e9967d678cfa7c77ca229c863a68f7c9a85318bf08f26628afe338",
- ]
-}
-
-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/multi-runner-v2/README.md b/examples/multi-runner-v2/README.md
deleted file mode 100644
index 2755c8fcf5..0000000000
--- a/examples/multi-runner-v2/README.md
+++ /dev/null
@@ -1,77 +0,0 @@
-# Multi-runner v2 example
-
-This example demonstrates the experimental multi-runner v2 interface. Shared
-defaults are configured with `global_config*` variables, while
-each runner lane uses `multi_runner_config` for its matcher,
-runner lifecycle, and compute-provider settings.
-
-The example creates three lanes from one deployment:
-
-- Linux ARM64 Amazon Linux runners.
-- Ephemeral Linux x64 Amazon Linux runners with job retry enabled.
-- Windows x64 Server Core 2022 runners.
-
-The v2 interface keeps provider-owned settings inside the selected provider
-configuration. For example, VPC and subnet settings are under
-`global_config_compute_provider.aws.ec2`, while the per-lane
-instance types and AMI configuration are under each lane's compute provider
-block. The optional `ami` variable can provide per-lane AMI filters and owners,
-which is useful for test environments with locally registered images.
-
-Configure the GitHub App variables before applying:
-
-```bash
-terraform init
-terraform apply \
- -var='github_app={id="123456",key_base64="..."}'
-```
-
-The `github_app` value is sensitive and should be supplied through a secure
-variable source in real deployments rather than committed to configuration.
-
-
-## Requirements
-
-| Name | Version |
-|------|---------|
-| [terraform](#requirement\_terraform) | >= 1.5.6 |
-| [aws](#requirement\_aws) | >= 6.33 |
-| [local](#requirement\_local) | ~> 2.0 |
-| [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 |
-| [webhook\_github\_app](#module\_webhook\_github\_app) | ../../modules/webhook-github-app | 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 |
-|------|-------------|------|---------|:--------:|
-| [ami](#input\_ami) | Optional AMI configuration keyed by runner lane. | map(object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
id_ssm_parameter = optional(object({
arn = string
}), null)
kms_key = optional(object({
arn = string
}), null)
})) | `{}` | no |
-| [aws\_region](#input\_aws\_region) | AWS region to deploy to. | `string` | `"eu-west-1"` | no |
-| [environment](#input\_environment) | Environment name, used as prefix. | `string` | `null` | no |
-| [github\_app](#input\_github\_app) | GitHub App ID and base64-encoded private key. | object({
id = string
key_base64 = string
}) | n/a | yes |
-
-## Outputs
-
-| Name | Description |
-|------|-------------|
-| [webhook\_endpoint](#output\_webhook\_endpoint) | n/a |
-| [webhook\_secret](#output\_webhook\_secret) | n/a |
-
diff --git a/examples/multi-runner-v2/main.tf b/examples/multi-runner-v2/main.tf
deleted file mode 100644
index 4f63974f11..0000000000
--- a/examples/multi-runner-v2/main.tf
+++ /dev/null
@@ -1,180 +0,0 @@
-locals {
- environment = var.environment != null ? var.environment : "multi-runner-v2"
- aws_region = var.aws_region
-}
-
-resource "random_id" "random" {
- byte_length = 20
-}
-
-module "base" {
- source = "../base"
-
- prefix = local.environment
- aws_region = local.aws_region
-}
-
-module "runners" {
- source = "../../modules/multi-runner"
-
- prefix = local.environment
- aws_region = local.aws_region
-
- experimental_features = ["multi-runner-v2"]
-
- global_config = {
- tags = {
- Example = local.environment
- Project = "ProjectX"
- }
- runner = {
- os = "linux"
- architecture = "x64"
- extra_labels = ["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 = {
- architecture = "arm64"
- }
-
- global_config_orchestration_provider = {
- webhook = {
- eventbridge = {
- enabled = true
- accept_events = ["workflow_job"]
- }
- }
- }
-
- global_config_compute_provider = {
- aws = {
- ec2 = {
- vpc_id = module.base.vpc.vpc_id
- subnet_ids = module.base.vpc.private_subnets
- ssm_enabled = true
- runner_binaries = {
- enabled = true
- }
- }
- }
- }
-
- multi_runner_config = {
- linux-arm64 = {
- runner = {
- architecture = "arm64"
- name_prefix = "amazon-arm64-"
- extra_labels = ["amazon"]
- }
- orchestration_provider = {
- webhook = {
- runner = {
- maximum_count = 1
- }
- matcherConfig = {
- exactMatch = true
- labelMatchers = [["self-hosted", "linux", "arm64", "amazon"]]
- }
- }
- }
- compute_provider = {
- aws = {
- ec2 = {
- instance_types = ["t4g.large", "c6g.large"]
- ami = lookup(var.ami, "linux-arm64", null)
- }
- }
- }
- }
-
- linux-x64 = {
- runner = {
- name_prefix = "amazon-x64-"
- extra_labels = ["amazon"]
- }
- orchestration_provider = {
- webhook = {
- runner = {
- ephemeral = true
- maximum_count = 1
- }
- matcherConfig = {
- labelMatchers = [["self-hosted", "linux", "x64", "amazon"]]
- exactMatch = false
- priority = 1
- }
- queue = {
- delay_webhook_event = 0
- }
- job_retry = {
- enabled = true
- }
- }
- }
- compute_provider = {
- aws = {
- ec2 = {
- instance_types = ["m5a.large", "m5ad.large"]
- ami = lookup(var.ami, "linux-x64", null)
- }
- }
- }
- }
-
- windows-x64 = {
- runner = {
- os = "windows"
- name_prefix = "windows-x64-"
- }
- orchestration_provider = {
- webhook = {
- runner = {
- boot_time_in_minutes = 20
- maximum_count = 1
- }
- matcherConfig = {
- exactMatch = true
- labelMatchers = [["self-hosted", "windows", "x64", "servercore-2022"]]
- }
- }
- }
- compute_provider = {
- aws = {
- ec2 = {
- instance_types = ["m5.large", "c5.large"]
- ami = lookup(var.ami, "windows-x64", {
- filter = {
- name = ["Windows_Server-2022-English-Full-ECS_Optimized-*"]
- state = ["available"]
- }
- owners = ["amazon"]
- id_ssm_parameter = null
- kms_key = null
- })
- }
- }
- }
- }
- }
-}
-
-module "webhook_github_app" {
- source = "../../modules/webhook-github-app"
- depends_on = [module.runners]
-
- github_app = {
- key_base64 = var.github_app.key_base64
- id = var.github_app.id
- webhook_secret = random_id.random.hex
- }
- webhook_endpoint = module.runners.webhook.endpoint
-}
diff --git a/examples/multi-runner-v2/providers.tf b/examples/multi-runner-v2/providers.tf
deleted file mode 100644
index eca2fe96a7..0000000000
--- a/examples/multi-runner-v2/providers.tf
+++ /dev/null
@@ -1,9 +0,0 @@
-provider "aws" {
- region = local.aws_region
-
- default_tags {
- tags = {
- Example = local.environment
- }
- }
-}
diff --git a/examples/multi-runner-v2/variables.tf b/examples/multi-runner-v2/variables.tf
deleted file mode 100644
index fe104758b9..0000000000
--- a/examples/multi-runner-v2/variables.tf
+++ /dev/null
@@ -1,39 +0,0 @@
-variable "github_app" {
- description = "GitHub App ID and base64-encoded private key."
-
- type = object({
- id = string
- key_base64 = string
- })
- sensitive = true
-}
-
-variable "environment" {
- description = "Environment name, used as prefix."
-
- type = string
- default = null
-}
-
-variable "aws_region" {
- description = "AWS region to deploy to."
-
- type = string
- default = "eu-west-1"
-}
-
-variable "ami" {
- description = "Optional AMI configuration keyed by runner lane."
-
- type = map(object({
- filter = optional(map(list(string)), { state = ["available"] })
- owners = optional(list(string), ["amazon"])
- id_ssm_parameter = optional(object({
- arn = string
- }), null)
- kms_key = optional(object({
- arn = string
- }), null)
- }))
- default = {}
-}
diff --git a/examples/multi-runner-webhook/.terraform.lock.hcl b/examples/multi-runner-webhook/.terraform.lock.hcl
new file mode 100644
index 0000000000..c1f4433ff3
--- /dev/null
+++ b/examples/multi-runner-webhook/.terraform.lock.hcl
@@ -0,0 +1,93 @@
+# This file is maintained automatically by "terraform init".
+# Manual edits may be lost in future updates.
+
+provider "registry.terraform.io/hashicorp/aws" {
+ version = "6.66.0"
+ constraints = ">= 5.0.0, >= 6.21.0, >= 6.33.0, >= 6.61.0"
+ hashes = [
+ "h1:OnLj4nhqJnEcUzyyRKUjp1FgWG00Y8maikJEYSf9Zjw=",
+ "h1:hBEaeBm9nm7A/u1nnD0nfolTPP55/BoKRFWk8zG8/fk=",
+ "zh:156fe7164a3d26ef6b35734c43e99fb198df90575ed897d1182b8e930b8cd523",
+ "zh:1af52b22b35be00f8d16e3ebebff9fa699ec4db2ef69e6032ba5c536f80c03d9",
+ "zh:2545a8478bd551fdc9694f6cc1a1ad24617f6736f8bde0ad6cae90987c65380f",
+ "zh:4070db1ee369ccb41cb610bfd887386bc0a9b9ecad60aeb4dbce58443d2519dd",
+ "zh:53da7d3c1840ef875c7d34e967732502a64fe677af0e78824773d4c15a8fe740",
+ "zh:576a93a28bf611a4de2a2e6ced697a41d5126b8fd31d30782b16797e410a9706",
+ "zh:58fed5fa9a033355b9d4f3092c817b70d934100e0d8678d6e4c93f3c9493d4e4",
+ "zh:6a9ca2f24e2ee9156dd785d159a850b35d190e9cf7eca21cb9582970c2db80cd",
+ "zh:729edd30f99cc16009deba5c013265b0c81eda261a3d0821cbd011d3287fd230",
+ "zh:7ae460049b75bd4aefee465ef7c53a01ac2df46d4d3e3ac00824afa8b5cb83fb",
+ "zh:9051fa85c8034ade8a57a5c6f232fd33da28f3800bb5aa40bc8625dbc5e27632",
+ "zh:906547e4319805e7acf7fbdf2bac28a4b1a7370790a2a430c7adb1b29bb934eb",
+ "zh:998f27410a66158a35ee5ed142c27e5b21fe8601941da55da2157f8042d6dcca",
+ "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
+ "zh:9c1804eff1dda0446dc2d215231015bb65a2fc6c3b7ba24584fe45f1ddd3fa9f",
+ "zh:b03ff5efdee310502aaaeb460144dc059bce72a0d8217e6b989099ef8aef9283",
+ ]
+}
+
+provider "registry.terraform.io/hashicorp/null" {
+ version = "3.3.2"
+ constraints = "~> 3.0, ~> 3.2"
+ hashes = [
+ "h1:/Wbz7DHFO92KAqyDBMxGA8nwOJYECM8iLGhR7+wVWhQ=",
+ "h1:IQ1qrkht1sC1nibUR+AJ3ulryyhVDHfCHZhoJi0sg2Y=",
+ "zh:10ec43b8b7b18d5639238c7fb9e111f6a4b038523dd66c7a426bf27b25fa4c08",
+ "zh:60beb9cc2ad5b871c710860cee75b42850cc6acd43db0d77cb5e00fda7288b55",
+ "zh:62538582d0a4a2f10ad8a8d9a6c3cd3f05af6c6d91c6641ffc78d4f0e8e69b27",
+ "zh:64a8f9ce7852d9efc5b464c12306c946366d59f5e2757def97969c9fd64bd1d6",
+ "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
+ "zh:92a374fb736a52f465283326d0a5bf4f495132eb99be209dfb4c75ec803fe8db",
+ "zh:98da9c42785d27a50f0604758bcb61a30f6278b9f2acd92bb3b2046e0e71916c",
+ "zh:b0f7896fae554729cdf4a24ac06359a050cff5817e6cd8597cba8a4ae01a7409",
+ "zh:bc8179ee35d67c72fb03012e7023b9f9816f033a7ec4109c001dd6d29752e812",
+ "zh:d23a598f713bfb6098bc003571d7de90b5a33b78f9be240488252fe5f3c2a60d",
+ "zh:d2855b922ea345dbd89ea287e4c6c4757e38bc0aaffeb2b79aa0b8004f9c53ff",
+ "zh:d3a60422bc6a2f9244d076c5222c07060c826ef91bdbaf4634cb752b86057473",
+ "zh:faa01928c25d2a6ecd9c7eb8b88134cb08de55a6b11ca6c703ac0092845344ba",
+ ]
+}
+
+provider "registry.terraform.io/hashicorp/random" {
+ version = "3.9.1"
+ constraints = "~> 3.0"
+ hashes = [
+ "h1:PlW+UZ4EElQF3NQwf41KQwavFujab3Czc51zu9dyVM8=",
+ "h1:g40qr7yDmIpaur4SsK5BcOda3HSo1RJ6zHVMqN4EJ+0=",
+ "zh:05f4734c1f0be840b711b3eff259ebc5fca436784c728955b1678078466f48d7",
+ "zh:0b91bf19371d012434eba1deeb6aab77158def9b39601dcbd94450b3974a2a26",
+ "zh:0ee6eacd47ec00183d55d726a4b6c4ce951a199f944bf22f1aa58392ebdfa7a2",
+ "zh:19388a4074b76a89a43a6c8328d7ae8ee2e7de3d346af51e80d3e6d3d12925f1",
+ "zh:23e74d48c5e2ac2e823fd527f49fee9db37d32a1990c9e3bf126ead697b843eb",
+ "zh:3cabf7fbd096c520064aae3aba61aba670af83ab91291a71fa1b1332929c2b7f",
+ "zh:5c0a3b8af0be60be4eca12ddee385cfa8babc1ec8e98cdf9de2f2274c73eabfa",
+ "zh:60b4f8a8ef18f52bf8e19215229dae408bee732825964092db7c989fd2de4097",
+ "zh:7359015acfedcbd6366f2329c854cf8d3c8ca5cd0faa89d2d37db358d6eba6c5",
+ "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
+ "zh:7b38758402f0e13a1071162da28994023cd2ac676e54af350c9ffd8dfa73fa7b",
+ "zh:7c7fbb8895eb75bb4de1f933e98553bd99c8d048c89a925ddba490aa5a67f7dc",
+ "zh:8c2b8c6a7ccdec16b73e2fb9f3700ea097f58c592571e4c5de60c93d2301732c",
+ ]
+}
+
+provider "registry.terraform.io/hashicorp/time" {
+ version = "0.14.2"
+ constraints = ">= 0.13.0"
+ hashes = [
+ "h1:eQRXh8mZFlUJfzYXKdaYRHRMhiS2cFyCfgP1mjkrtuI=",
+ "h1:gnP2hptiFIHSHUFBvAFKhE/Yh5u5yVEx+P7XSB58A/E=",
+ "zh:0aa1028d91041f4dceba193e3707dac57358d0063d97e20700e554758b67baca",
+ "zh:32bee9f2b2678e2a0789ad86e716d09ca1d5450180b3cd8033ee7a251bfd352e",
+ "zh:3aded9ef4dc6f4aec202a50c68a08b40013d325f9947f10168ebc8bee54109fc",
+ "zh:4d924637f3115ffa4ffc7f16d3f366bc472594f7447d400adb9def7ac92e3fc8",
+ "zh:5c35008e1363deafaa440ab43519409866dd7ec72aabeb2317bc16cd82756784",
+ "zh:6b30d97c9827501d7010fe49c8889d7a6cc8b45b77cdb1668c65af7f28a27d73",
+ "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
+ "zh:851e0f0e9c4de395e42220de51e7fb20d0e845e629f3cf37056da19e597304e7",
+ "zh:85622b4779b3ba7424780f7f8efcfa585227e245201cdb68303f2118b388e971",
+ "zh:9039153e3d45147183804188a1ce36c3811db9a4dca80f36fa382408d5f50b72",
+ "zh:a3d385413dc258a53fe8f4ded5d1fcd1eba715c65a015d74e70911bab2919207",
+ "zh:b9310a2327f7c8ad2aa3266f89c4d026b4bf18b09f7c4257c0ecd71a32f32db3",
+ "zh:cee7f2143da0c494115da94984bf3630e3e5855e27dae720a7bc29bb6acd9be2",
+ ]
+}
diff --git a/examples/multi-runner-webhook/.terraform.lock.hcl.tofu b/examples/multi-runner-webhook/.terraform.lock.hcl.tofu
new file mode 100644
index 0000000000..8737446421
--- /dev/null
+++ b/examples/multi-runner-webhook/.terraform.lock.hcl.tofu
@@ -0,0 +1,150 @@
+# This file is maintained automatically by "tofu init".
+# Manual edits may be lost in future updates.
+
+provider "registry.opentofu.org/hashicorp/aws" {
+ version = "6.65.0"
+ constraints = ">= 5.0.0, >= 6.21.0, >= 6.33.0, >= 6.61.0"
+ hashes = [
+ "h1:/D/KChJMHHi6N2Ae8pDT2CoZ1ZVPgmO4DvT3TM1uYdA=",
+ "h1:0KBMNN4G86DISLGy8e7PdtgcjgLWmFM0Tu/+PlcI6Xc=",
+ "h1:0xke8tvUJrFES4mVvTaPvBQAad6XfV1kOE7p8i+xN+4=",
+ "h1:1Ra6ZrgNEnjkReeeNpOEml91kbhCHwoSzV/ZI3yVeNw=",
+ "h1:1jrxTTKLGfTEPR9BZ6hkGMYcK0ph5bx1dzhSSYtGgsQ=",
+ "h1:AYFFxtquEyqmRHuAEPx1mOnNzIEGPh5WvKIY+u4HSoA=",
+ "h1:CXFxyNowi2BGGcMxHPk6+X5wuo7ZAwuK2/OGX9wRve8=",
+ "h1:UTMijAbC6R9HYu8x6QoQjOKP5EhDB2885v+GjVbOfuk=",
+ "h1:UUQbMjGJufv6KVfAN1uMVoDkJJV7gL5+qXt8pA82W2w=",
+ "h1:XOuZUW+/aP5FeEMd25136uGu4Yd3scANUQ3khIePxpw=",
+ "h1:XkTODDfiyRIzHYCSQX/TxT5A28OJpimgbJvddYSVl0M=",
+ "h1:bJ3Hx/OsgpaMGLAyu7U3x+IJOOEpynsrp5SAa68/xdQ=",
+ "h1:e8xYPOcVYj/ke4NwRyOTdy2AH/g1n6I6STENon69LvM=",
+ "h1:pYM2NuBZ9Yml+TdLZfuTDSjpRR2ZDW2lgljMrSYhbS4=",
+ "h1:q041n/UFMg+1rQ8ydIoVaHSOYE7qzV/dnTii/eQl2HA=",
+ "zh:079c8d8adae825fcd81180979a7c87f66eb4c18825dde91790eac178ffa5b506",
+ "zh:30912d1497c8a5dfe2d26eeab68b1ceb05621a0cc5205a9623fc048c2a34987d",
+ "zh:33cbbc1fa2df3c80ca026b4073cff1013fd5e648b32ae40d2e2dd7a8892cd6f4",
+ "zh:6ba73e9aff1762c2ec9f3d4184b49ae7c219bc302fe38aa9c47a3267617c7c7d",
+ "zh:8581d0ae4ab4bb14b24fe3d4145900cbcbb373271fa327da10f107bff947ee62",
+ "zh:8d4528d906ebba03c857d2a30cfd75d3f847ff6c1781a57b535fd0edd659f7db",
+ "zh:9617db8d86e5ec3c4be76c163d3fd0de013cbd804b337368e21d7916aac686bf",
+ "zh:bbd497e2859a5a09962b1209529a07c8d921493758cd6c8d01200b2ec1f6eba7",
+ "zh:c446d97456cc9e8adf2a409e0f4ad8ab5c4a54d6b36e3d985a52f6ea7c2603e7",
+ "zh:c8b005e981e6e8fbb01e9ea2281df977401c236da24e676d98e8c46c2042463b",
+ "zh:d3c2ecb7647f865b17e93ae6f44fedd50e3d3aa1e7d67336731dc38d6b46a9de",
+ "zh:d4b0a5ab8625c787e9e706eaa3843a104ac4edaadd4cc46c3f13490c2003fd69",
+ "zh:da81af555be23b26b6f82352b29bdb904456429b3752b5a574fb7e636a62784b",
+ "zh:e4d7efff69897583bcf7451f631ae027da528f9355ff72339a5460837856e41b",
+ "zh:e76114d3e0b20893ce22bd50d7c813842f0747570101e4389b8c7e57cf2a1019",
+ ]
+}
+
+provider "registry.opentofu.org/hashicorp/null" {
+ version = "3.3.2"
+ constraints = "~> 3.0, ~> 3.2"
+ hashes = [
+ "h1:1T+00cjQNmRAHAz9xjEBFpf5wRRb0IBuXS/W8ke5BWs=",
+ "h1:46gmIYe+klib6TlHKSqEkMLjvnzVWiCB2NYA2zR8MX8=",
+ "h1:7WQ3wjfaeqnXxq+a8cYiYeWUnMTgY1JcuX+z7sZd72s=",
+ "h1:MVM+vkVtW/YyKfn111pyho0y87I4TekaNMbBLkn0/C8=",
+ "h1:QBcIbI2Dp4v6Iui37pn4qmw8YeiFLbSWcJuzZVl/65Y=",
+ "h1:SsVKTUR+vgLaC1YnoDa2fnYpzREcgNgWRcu5x+vwjHA=",
+ "h1:WUaeuTNn9w6UXZ9cMq4+qZy4ZAr71B9NcUDEXjqfdKs=",
+ "h1:WtEaA7alasNwEQ4L3+KyQtbkSOPsexzJ4LUZ7PKaycI=",
+ "h1:WxS7rjYIZ1WQc4GkICch8XrbxoSY8TjUfLbPDo6oEcQ=",
+ "h1:Ysvc/FPvcwk+iMg7IcLkqZhT/KhtZTYji+UBqMlcTs4=",
+ "h1:ZLjbXnfVcRvS/DAN3BcNebOsnOcs3Nx6mJpFCj4dZ2c=",
+ "h1:fAmvQjIGyqdGMc+v/fUEINCyuU4iaKSzsV8PWsOnmAc=",
+ "h1:jwkbEtf3S7W+Bl4soynNkUHFfK/4I/H74urHY758XVw=",
+ "h1:qY1sKzlxNTp/dqZR23bM4egmVMRlaQudLlBYraMt1pw=",
+ "h1:t8H1KNwJQwKE/GqpHeRxOWgMk0Yv35qbBTBzqB/rhr0=",
+ "zh:09e94b0b7dfc0c6450c247517b5410546039c758e513d89b588af6df70c3d57d",
+ "zh:0b72497b6fd79a2b04785b64890a565a8cc7b06ded95da05e6dab2f3b8a02d58",
+ "zh:1c0ee6f81f7bcdec8d568a145a450eb57a6f1cfe5e48943375d1af22ed54151e",
+ "zh:1c6899b475f035d352af1e7f33dc30beab8b8e3784f8cb55a2cc4a11997fbd66",
+ "zh:43e57a2a56e9874604501bdebe431bb573fb77d2c5f4d7598ab30727dc0e90ea",
+ "zh:49cf2f36298a5ac3ac8d80ceb87466e6a99d2c021005bcd9e3a79f2314fd0a13",
+ "zh:665be40d2c7f3d768b8f39a041371526d4b7b396b4c11e162a1886212da176c9",
+ "zh:6bb1583d88ddb38b1c6b4624e25ad414ddd8bf65b0dd9c074580847311f83924",
+ "zh:71d64453bdc795667e9841d7c90e3fef6ff157e0598d51dac4bb1e4583b85407",
+ "zh:73ac02bc3b680e1ea75aab24ec2a359c8f0a021f73d43b2feae0a899e75a93ad",
+ "zh:b2b777ee07b910e7345df85321fab6c9a25c33ecb56129758dda8fadaba09fe4",
+ "zh:bb984d52880749a49e509e3b243804869e1af40ee2d34322a30f5f340f8d8dbd",
+ "zh:e0724bc083527343b4a4099fd4f95511e49a0e113416cdba58a64446742b68b1",
+ "zh:e391c14e367cd64d986ddc8d81f2db76d49600ce0521720618ff1ecc0decde7f",
+ "zh:e95c1af8e8e9967d678cfa7c77ca229c863a68f7c9a85318bf08f26628afe338",
+ ]
+}
+
+provider "registry.opentofu.org/hashicorp/random" {
+ version = "3.9.1"
+ constraints = "~> 3.0"
+ hashes = [
+ "h1:38E2VQmQDhws/3AL3D/EzBGuCseepyZRIswAOx8CqoQ=",
+ "h1:7+qv9kpOpBC9EUPCubnPxh603tu3l9EIMMBkpbt1H1Y=",
+ "h1:CEQeHfnUDB3uqAkKoEWfWgbj+kpoQHgcuPbAjPzbh+U=",
+ "h1:HPYO9tf8KUSHqSdz1uOL97MLeaVHPaWPY1JW6tKU19E=",
+ "h1:KYXiC06Pr3WJcIUbDq9MgdAbInO5zcRyHFqV1x5UcJg=",
+ "h1:MygjbYH8CrPv8RUe75tZAFmrFNIzQLT45fiyYx7u2tI=",
+ "h1:RMSARNOw4qZx+VmHYnVMGljsFVfCV1P+nJjKrN2XIOI=",
+ "h1:U/71jbSbfsfVLxWpSlhyVHh/DnQXQhjoJCGkyongkBA=",
+ "h1:WwLLvRE1q95CTGxyTjKpctXJ0ooVs9d22JAQS9fC3uo=",
+ "h1:ZtRBSqoyfQAhngjUjM0NRPtj6NdSJ/JBENFwT8D276s=",
+ "h1:cfxedZLduhHD1UtqQDjAQZNAEhr/bWDAZ4nU9rSdySg=",
+ "h1:i45mo4de0QKOreStMqUQ7qyZL3ucFq42l178Fm5/hMU=",
+ "h1:v3SAJKN4D3dOM95xKwgKGIWELe5nUBbyXdb5HNz7icw=",
+ "h1:v3vTk/STekrzNc6NG3jL9/05zhvF1QVCgyRRbvSHPcI=",
+ "h1:zHgFWtRBgOycqhw8HdLSvMVNWGJtIjOAPYOcAdE7cL0=",
+ "zh:09aaf19b0d22726d2378e0e89fbbefc183494d7bd585759d6c4e69ba50951a2f",
+ "zh:31575ca9bc0db20337096d178ea73bce3ebca343ed071c67f78cf39f800c9ec6",
+ "zh:624fb6ed552abc34a5aaac41e76a373da65ac08e524b09b672f29c60e6ac896a",
+ "zh:6a4760d55132b9750ac1a04f6fc32e247034daa999f71452dba9cbca225a529a",
+ "zh:768a6047cfb8958e7b0b120c580aa3de6624a7fbb2c56ad6df85cd559ed26ec7",
+ "zh:8983c788ba660bcb587e64ff9c3e4323515caf78facbe0abe6432e7aff8df893",
+ "zh:8d570eb026a4f00b58a1d36be0ce3c13adf4d973efcd4162b05cb295bbc14257",
+ "zh:a2259540854d5f699c36b89244fb202ebb2c219b64669a51072687d04fb47152",
+ "zh:aaa51d905b0e80a28e02f9bee2cf6c91ffade7389d77ab9198aa12809ed04955",
+ "zh:afb60995e98573facddfb47baedf7e288408680eb00b5d3df570611758947c72",
+ "zh:b9a46d852ce53fa037f47537a7de53f37b759ccf211600b7ba44c66ba4b616b7",
+ "zh:bafcfeeefcd0dfefeff120b655b45edb0497c4717534ffe5201b3cb556d1ffe6",
+ "zh:c3ac24d397eae054aca2290e20943e0c767592cc661c890850c25ac01829308d",
+ "zh:eafba4127ebadcc5ed0e427935c66fb5e2da7cfdaae39a66d52f4a50d51faf1e",
+ "zh:f39d4bce213ed9bba3474bad468136af08ff6c4c33adaafcc10c1f78067adfe3",
+ ]
+}
+
+provider "registry.opentofu.org/hashicorp/time" {
+ version = "0.14.2"
+ constraints = ">= 0.13.0"
+ hashes = [
+ "h1:0lkmuDlyUBEK2vAxb9r8jY8kMpqYbMoSb4GWnSbA9iY=",
+ "h1:4ccOXW03+ENKJieXGwTfMvRlkpT9o+ra6dw240C1UFE=",
+ "h1:8+b7rm7aVI0cNDoegUPuEKNAQeEC9jYHaaqgVWGvGag=",
+ "h1:BzLQYmKbF3aM81kS9GZQ0mnJPU/bVFa3Jgk9ZIFJP80=",
+ "h1:EHyMvebIwqieUVzq6WbbheNWUHRgy8B5hhRv++V10qA=",
+ "h1:ICdTbU+IeBH2xihtoYENxmLIGfObCbcb8l2XK2abJGw=",
+ "h1:JKIAzWzVxRY6Q+ybCjmZ6DnMfkyp/zdZmWkTIB1JzOo=",
+ "h1:JkTAWz5bbrSgrnkkF5XhoMbSLSihtDDb5V+SYAZYOes=",
+ "h1:TNzAoSy5lcv/8pjzlb4nz+m92N3G/osbU+FV+uh0SQs=",
+ "h1:XJLK9UX0/LxUM7Z4pxB1tf6TP8iCz0bxM87ot4wq1ns=",
+ "h1:bkdzFk//GNj0iHkXgupa0XqwNjYx3O6+czgIi+IkXjA=",
+ "h1:cbpg0fadPwbhtL0EMnPj+AkwpPUvflGGeYB31SKTHdw=",
+ "h1:hv6Fp4zk1JQ7Dj+cmUJZmuI3aLEGjw4VSFyqgUvisDg=",
+ "h1:oMHgtUYEDs1DJrTXNPgSNoQ0f+0FZ+nAYJWJYpHBtAI=",
+ "h1:qdSn+kIg2bcZkgGLJ5zQ1K03qoRWYYBxGv6J7SWlShY=",
+ "zh:0c5caf61f978612c78eead85fac256ce42a62e5a04616afedf41065a98639c9f",
+ "zh:1356743176e6522d6f0a998cf04edc9c5b3e3a99893562fc3a9bc7c4f7ac738e",
+ "zh:1aae0a419092d5e20d6c38199b6c406a73abe12f7f665fa1bd9fed07f454d427",
+ "zh:4fc26faa672af3806d5760c88c7ad223ae8a04002ae77372d7744fa44e56b3f6",
+ "zh:503189a08d6468a1c24099f3aa9b22aa9f6fd82bdab118ee6a2af665441fc8a7",
+ "zh:53f1ca144377f8c42a1fc71cff22e6c3ad5e2ae9d9eaaf0a95cbe70c896b30cb",
+ "zh:64ac1844933a18767bb1b9ea19b5432f83acf682fcff1e49680e50d341420e78",
+ "zh:6819c08b9228375f56fd2d33881fb7fbb0a59581acabfcd662b4eb002aae3917",
+ "zh:c7e0df3698ad0f75114ce72049d8d8be242e6510806e13bdf8ca8f416e3d9f1c",
+ "zh:c8a7d8601dcc700efdfc65194bb8450ca28f04c0e42aac8a355a7b758c82e252",
+ "zh:d311cd2ef10b5b9762f248e54c9d9211c8f6c24a5efc80fcddeba95d0347713d",
+ "zh:d34865e994a91b764bd1283dfa25ee3d84571d65959987d4a7f17405dcf27e43",
+ "zh:ee5ff12288f160969dc6a2764b634295cfe73f5705172f0d2f5d32a358cf29f7",
+ "zh:eec9f3327f2cd180af6fa5e16370c7d5cf0c79874f03d2c1a48e22c7c1af4951",
+ "zh:f0fc458693aec12dcf84bb7d7b2f49283a818282072729a26765d679fb2984ed",
+ ]
+}
diff --git a/examples/multi-runner-webhook/README.md b/examples/multi-runner-webhook/README.md
new file mode 100644
index 0000000000..1d63205c7c
--- /dev/null
+++ b/examples/multi-runner-webhook/README.md
@@ -0,0 +1,100 @@
+# Multi-runner webhook example
+
+This example exercises the shared experimental multi-runner v2 webhook path
+with EC2 and Lambda MicroVM compute. The runner lanes, webhook orchestration,
+Lambda artifacts, and GitHub configuration are common; provider-owned inputs
+are grouped under `compute_provider`.
+
+The example creates both an EC2 lane and a Lambda MicroVM lane behind the same
+webhook endpoint. The MiniStack smoke test sends matching jobs to each lane in
+sequence, so adding another provider means adding another lane and provider
+specific lifecycle assertions to the same deployment.
+
+The runner-control and webhook Lambda archives are explicit inputs:
+
+```sh
+terraform apply \
+ -var='runners_lambda_zip=/path/to/runners.zip' \
+ -var='webhook_lambda_zip=/path/to/webhook.zip'
+```
+
+## MicroVM prerequisites
+
+The MicroVM lane expects an image that has already been built and published in
+the target Region. The image is not created by this example. Prepare it in
+this order:
+
+1. Apply `examples/microvm-foundation`.
+2. Build and release the lifecycle-hook service from
+ `lambdas/services/microvm-lifecycle-hooks` using the same artifact process
+ used for the repository's Lambda services.
+3. Build the image with Packer from `images/microvm-ubuntu`, passing the
+ foundation's bucket, connector, build-role, and lifecycle-hook artifact.
+4. Set `compute_provider.aws.microvm.image_arn` (and, when applicable,
+ `image_version`) to the published image.
+
+The foundation's build role is used to create the image. It is different from
+the execution role used by the runner job. The runner configuration owns that
+execution role; the control-plane TypeScript passes it to `RunMicrovm` when it
+starts an ephemeral runner. The control-plane Lambda therefore needs
+permission to pass the configured execution role, and the role needs the
+runtime permissions required by the selected runner lane.
+
+This example deploys both EC2 and MicroVM lanes behind one webhook endpoint,
+but it does not replace the foundation, image build, lifecycle-hook release,
+or execution-role setup steps.
+
+
+## Requirements
+
+| Name | Version |
+|------|---------|
+| [terraform](#requirement\_terraform) | >= 1.5.6 |
+| [aws](#requirement\_aws) | >= 6.33 |
+| [null](#requirement\_null) | ~> 3.0 |
+| [random](#requirement\_random) | ~> 3.0 |
+
+## Providers
+
+| Name | Version |
+|------|---------|
+| [aws](#provider\_aws) | 6.66.0 |
+
+## Modules
+
+| Name | Source | Version |
+|------|--------|---------|
+| [base](#module\_base) | ../base | n/a |
+| [microvm\_foundation](#module\_microvm\_foundation) | ../../modules/microvm-foundation | n/a |
+| [runners](#module\_runners) | ../../modules/multi-runner | n/a |
+
+## Resources
+
+| Name | Type |
+|------|------|
+| [aws_cloudwatch_log_group.aws_cloudwatch_log_group_microvm](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource |
+| [aws_ecr_repository.base_ubuntu24](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_repository) | resource |
+| [aws_ecr_repository_policy.repository_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_repository_policy) | resource |
+| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source |
+| [aws_iam_policy_document.ecr_repository_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| [aws\_region](#input\_aws\_region) | AWS Region where the runner control plane and compute provider resources are deployed. | `string` | `"eu-west-1"` | no |
+| [compute\_provider](#input\_compute\_provider) | Provider-specific settings for the EC2 and MicroVM runner lanes. | object({
aws = object({
ec2 = object({
instance_types = list(string)
ami = object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
id_ssm_parameter = optional(object({
arn = string
}), null)
kms_key = optional(object({
arn = string
}), null)
})
})
microvm = object({
image_arn = string
image_version = optional(string, null)
ingress_network_connectors = optional(list(string), [])
egress_network_connectors = list(string)
})
})
}) | n/a | yes |
+| [environment](#input\_environment) | Name prefix for the example resources. | `string` | n/a | yes |
+| [github\_app](#input\_github\_app) | GitHub App credentials used by the webhook orchestration provider. | object({
id = string
key_base64 = string
webhook_secret = string
}) | n/a | yes |
+| [github\_enterprise\_server](#input\_github\_enterprise\_server) | Optional GitHub Enterprise Server endpoint used by the smoke-test API mock. | object({
url = string
ssl_verify = bool
}) | `null` | no |
+| [runners\_lambda\_zip](#input\_runners\_lambda\_zip) | Local ZIP file for the runner-control Lambda. | `string` | n/a | yes |
+| [webhook\_lambda\_zip](#input\_webhook\_lambda\_zip) | Local ZIP file for the webhook Lambda. | `string` | n/a | yes |
+
+## Outputs
+
+| Name | Description |
+|------|-------------|
+| [microvm](#output\_microvm) | n/a |
+| [webhook\_endpoint](#output\_webhook\_endpoint) | n/a |
+| [webhook\_secret](#output\_webhook\_secret) | n/a |
+
diff --git a/examples/multi-runner-webhook/main.tf b/examples/multi-runner-webhook/main.tf
new file mode 100644
index 0000000000..5b1b8b94c6
--- /dev/null
+++ b/examples/multi-runner-webhook/main.tf
@@ -0,0 +1,183 @@
+module "base" {
+ source = "../base"
+
+ prefix = var.environment
+ aws_region = var.aws_region
+}
+
+module "runners" {
+ source = "../../modules/multi-runner"
+
+ prefix = var.environment
+ aws_region = var.aws_region
+
+ experimental_features = ["multi-runner-v2"]
+
+ global_config = {
+ tags = {
+ Example = var.environment
+ Project = "MiniStack"
+ }
+ runner = {
+ os = "linux"
+ architecture = "x64"
+ }
+ }
+
+ global_config_github = {
+ app = {
+ key_base64 = var.github_app.key_base64
+ id = var.github_app.id
+ webhook_secret = var.github_app.webhook_secret
+ }
+ enterprise_server = var.github_enterprise_server
+ }
+
+ global_config_lambda = {
+ architecture = "x86_64"
+ }
+
+ global_config_observability = {
+ logs = {
+ level = "debug"
+ }
+ }
+
+ global_config_orchestration_provider = {
+ webhook = {
+ runner = {
+ ephemeral = true
+ jit_config_enabled = true
+ # The smoke test keeps the ephemeral resources alive until each provider's
+ # scale-down phase, so it needs capacity for standard, dynamic, and pool runners.
+ maximum_count = 3
+ boot_time_in_minutes = 0
+ }
+ lambda = {
+ artifact = {
+ zip = var.runners_lambda_zip
+ }
+ scale = {
+ up = {
+ job_queued_check_enabled = true
+ }
+ down = {
+ # Smoke scenarios create and remove runners immediately; do not
+ # wait for the Linux five-minute minimum runtime before checking
+ # the GitHub runner state.
+ minimum_running_time_in_minutes = 0
+ }
+ }
+ pool = {
+ config = [{
+ schedule_expression = "cron(0 0 1 1 ? 2099)"
+ schedule_expression_timezone = "UTC"
+ size = 1
+ }]
+ runner_owner = "test-owner"
+ }
+ webhook = {
+ artifact = {
+ zip = var.webhook_lambda_zip
+ }
+ }
+ }
+ }
+ }
+
+ global_config_storage_provider = {
+ aws = {
+ ssm = {
+ paths = {
+ root = "/github-action-runners/${var.environment}"
+ }
+ }
+ }
+ }
+
+ global_config_compute_provider = {
+ aws = {
+ ec2 = {
+ vpc_id = module.base.vpc.vpc_id
+ subnet_ids = module.base.vpc.private_subnets
+ ssm_enabled = true
+ binaries_syncer = {
+ enabled = false
+ }
+ }
+ microvm = {
+ image_arn = var.compute_provider.aws.microvm.image_arn
+ image_version = var.compute_provider.aws.microvm.image_version
+ ingress_network_connectors = var.compute_provider.aws.microvm.ingress_network_connectors
+ egress_network_connectors = var.compute_provider.aws.microvm.egress_network_connectors
+ }
+ }
+ }
+
+ multi_runner_config = {
+ ec2 = {
+ runner = {
+ os = "linux"
+ architecture = "x64"
+ name_prefix = "ec2-"
+ extra_labels = ["ec2"]
+ }
+ orchestration_provider = {
+ webhook = {
+ github = {
+ organization_runners = true
+ }
+ matcherConfig = {
+ labelMatchers = [["self-hosted", "linux", "x64", "ec2"]]
+ bidirectionalLabelMatch = true
+ dynamic_labels_enabled = true
+ awsDynamicLabelsPolicy = {
+ restricted_keys = {
+ "instance-type" = { allowed = ["m5.*"] }
+ }
+ }
+ }
+ }
+ }
+ compute_provider = {
+ aws = {
+ ec2 = {
+ instance_types = var.compute_provider.aws.ec2.instance_types
+ ami = var.compute_provider.aws.ec2.ami
+ }
+ }
+ }
+ }
+
+ microvm = {
+ runner = {
+ os = "linux"
+ architecture = "arm64"
+ name_prefix = "microvm-"
+ extra_labels = ["microvm"]
+ }
+ orchestration_provider = {
+ webhook = {
+ github = {
+ organization_runners = true
+ }
+ matcherConfig = {
+ labelMatchers = [["self-hosted", "linux", "arm64", "microvm"]]
+ bidirectionalLabelMatch = true
+ dynamic_labels_enabled = true
+ awsDynamicLabelsPolicy = {
+ restricted_keys = {
+ "image-version" = { allowed = ["3.0"] }
+ }
+ }
+ }
+ }
+ }
+ compute_provider = {
+ aws = {
+ microvm = {}
+ }
+ }
+ }
+ }
+}
diff --git a/examples/multi-runner-webhook/microvm.tf b/examples/multi-runner-webhook/microvm.tf
new file mode 100644
index 0000000000..fa2a39e758
--- /dev/null
+++ b/examples/multi-runner-webhook/microvm.tf
@@ -0,0 +1,74 @@
+resource "aws_cloudwatch_log_group" "aws_cloudwatch_log_group_microvm" {
+ name = "/aws/lambda/microvms/ubuntu24"
+}
+
+resource "aws_ecr_repository" "base_ubuntu24" {
+ name = "base-ubuntu24"
+ force_delete = true
+}
+
+
+data "aws_iam_policy_document" "ecr_repository_policy" {
+
+ statement {
+ effect = "Allow"
+ actions = [
+ "ecr:GetDownloadUrlForLayer",
+ "ecr:BatchCheckLayerAvailability",
+ "ecr:BatchGetImage",
+ "ecr:DescribeImages",
+ "ecr:GetAuthorizationToken",
+ "ecr:ListImages"
+ ]
+
+ principals {
+ type = "AWS"
+ identifiers = [data.aws_caller_identity.current.account_id]
+ }
+ }
+}
+
+resource "aws_ecr_repository_policy" "repository_policy" {
+ repository = "base-ubuntu24"
+ policy = data.aws_iam_policy_document.ecr_repository_policy.json
+}
+
+
+locals {
+ network_connectors = {
+ ministack = {
+ name = "ministack"
+ vpc_id = module.base.vpc.vpc_id
+ subnet_ids = module.base.vpc.private_subnets
+ }
+ }
+}
+
+data "aws_caller_identity" "current" {}
+
+module "microvm_foundation" {
+ source = "../../modules/microvm-foundation"
+
+ aws_region = var.aws_region
+ tags = {
+ Component = "microvm-foundation"
+ }
+ build_policy_name_prefix = "gha-microvm-build-policy-"
+ build_role_name_prefix = "gha-microvm-build-"
+ network_connector_operator_role_name_prefix = "gha-microvm-network-operator-"
+ usage_policy_name_prefix = "gha-microvm-runtime-usage-policy-"
+ artifact_bucket_name = "ministack-microvm-artifacts-${var.aws_region}"
+ artifact_retention_days = 30
+ image_name_prefix = "gha-ubuntu-arm64"
+ ecr_repository_arns = ["arn:aws:ecr:${var.aws_region}:${data.aws_caller_identity.current.account_id}:repository/base-ubuntu24"]
+ network_connectors = local.network_connectors
+ force_destroy_artifact_bucket = true
+}
+
+locals {
+ microvm = {
+ ecr_repo = aws_ecr_repository.base_ubuntu24.repository_url
+ log_group = aws_cloudwatch_log_group.aws_cloudwatch_log_group_microvm.name
+ microvm_foundation = module.microvm_foundation
+ }
+}
\ No newline at end of file
diff --git a/examples/multi-runner-v2/outputs.tf b/examples/multi-runner-webhook/outputs.tf
similarity index 57%
rename from examples/multi-runner-v2/outputs.tf
rename to examples/multi-runner-webhook/outputs.tf
index 1feaf2e671..333ef57ea6 100644
--- a/examples/multi-runner-v2/outputs.tf
+++ b/examples/multi-runner-webhook/outputs.tf
@@ -1,8 +1,11 @@
output "webhook_endpoint" {
value = module.runners.webhook.endpoint
}
-
output "webhook_secret" {
sensitive = true
- value = random_id.random.hex
+ value = var.github_app.webhook_secret
}
+
+output "microvm" {
+ value = local.microvm
+}
\ No newline at end of file
diff --git a/examples/multi-runner-webhook/providers.tf b/examples/multi-runner-webhook/providers.tf
new file mode 100644
index 0000000000..f24f950b27
--- /dev/null
+++ b/examples/multi-runner-webhook/providers.tf
@@ -0,0 +1,9 @@
+provider "aws" {
+ region = var.aws_region
+
+ default_tags {
+ tags = {
+ Example = var.environment
+ }
+ }
+}
diff --git a/examples/multi-runner-webhook/variables.tf b/examples/multi-runner-webhook/variables.tf
new file mode 100644
index 0000000000..7e19d64c1a
--- /dev/null
+++ b/examples/multi-runner-webhook/variables.tf
@@ -0,0 +1,68 @@
+variable "aws_region" {
+ description = "AWS Region where the runner control plane and compute provider resources are deployed."
+ type = string
+ default = "eu-west-1"
+}
+
+variable "environment" {
+ description = "Name prefix for the example resources."
+ type = string
+}
+
+variable "github_app" {
+ description = "GitHub App credentials used by the webhook orchestration provider."
+ sensitive = true
+
+ type = object({
+ id = string
+ key_base64 = string
+ webhook_secret = string
+ })
+}
+
+variable "github_enterprise_server" {
+ description = "Optional GitHub Enterprise Server endpoint used by the smoke-test API mock."
+ type = object({
+ url = string
+ ssl_verify = bool
+ })
+ default = null
+}
+
+variable "runners_lambda_zip" {
+ description = "Local ZIP file for the runner-control Lambda."
+ type = string
+}
+
+variable "webhook_lambda_zip" {
+ description = "Local ZIP file for the webhook Lambda."
+ type = string
+}
+
+variable "compute_provider" {
+ description = "Provider-specific settings for the EC2 and MicroVM runner lanes."
+
+ type = object({
+ aws = object({
+ ec2 = object({
+ instance_types = list(string)
+ ami = object({
+ filter = optional(map(list(string)), { state = ["available"] })
+ owners = optional(list(string), ["amazon"])
+ id_ssm_parameter = optional(object({
+ arn = string
+ }), null)
+ kms_key = optional(object({
+ arn = string
+ }), null)
+ })
+ })
+ microvm = object({
+ image_arn = string
+ image_version = optional(string, null)
+ ingress_network_connectors = optional(list(string), [])
+ egress_network_connectors = list(string)
+ })
+ })
+ })
+}
diff --git a/examples/multi-runner-v2/versions.tf b/examples/multi-runner-webhook/versions.tf
similarity index 76%
rename from examples/multi-runner-v2/versions.tf
rename to examples/multi-runner-webhook/versions.tf
index 6af69ab915..6883c62423 100644
--- a/examples/multi-runner-v2/versions.tf
+++ b/examples/multi-runner-webhook/versions.tf
@@ -4,14 +4,15 @@ terraform {
source = "hashicorp/aws"
version = ">= 6.33"
}
- local = {
- source = "hashicorp/local"
- version = "~> 2.0"
+ null = {
+ source = "hashicorp/null"
+ version = "~> 3.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.0"
}
}
+
required_version = ">= 1.5.6"
}
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..fb717f0697
--- /dev/null
+++ b/modules/microvm-foundation/storage.tf
@@ -0,0 +1,105 @@
+# 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
+
+ force_destroy = var.force_destroy_artifact_bucket
+}
+
+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..e2d5ae2fa5
--- /dev/null
+++ b/modules/microvm-foundation/variables.tf
@@ -0,0 +1,160 @@
+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) <= 38 && can(regex("^[a-zA-Z0-9-_]+-$", var.build_policy_name_prefix))
+ error_message = "build_policy_name_prefix must be 1 to 38 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) <= 38 && can(regex("^[a-zA-Z0-9-_]+-$", var.usage_policy_name_prefix))
+ error_message = "usage_policy_name_prefix must be 1 to 38 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) <= 38 && can(regex("^[a-zA-Z0-9-_]+-$", var.build_role_name_prefix))
+ error_message = "build_role_name_prefix must be 1 to 38 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) <= 38 && can(regex("^[a-zA-Z0-9-_]+-$", var.network_connector_operator_role_name_prefix))
+ error_message = "network_connector_operator_role_name_prefix must be 1 to 38 characters, contain only letters, numbers, hyphens, or underscores, and end with a hyphen."
+ }
+}
+
+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) <= 38
+ && 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 38 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."
+ }
+}
+
+variable "force_destroy_artifact_bucket" {
+ type = bool
+ description = "Whether to force destroy the S3 bucket containing Lambda MicroVM image source artifacts."
+ default = false
+}
\ No newline at end of file
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 1a00a7e32c..fe0ad72b9d 100644
--- a/tests/ministack/README.md
+++ b/tests/ministack/README.md
@@ -6,7 +6,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/multi-runner-v2.tfvars b/tests/ministack/multi-runner-v2.tfvars
deleted file mode 100644
index de54e291cf..0000000000
--- a/tests/ministack/multi-runner-v2.tfvars
+++ /dev/null
@@ -1,33 +0,0 @@
-environment = "ministack-v2"
-aws_region = "eu-west-1"
-
-github_app = {
- id = "0"
- key_base64 = "ministack-invalid-key"
-}
-
-runner_binaries_enabled = false
-
-ami = {
- "linux-arm64" = {
- filter = {
- name = ["ministack-v2-linux-arm64"]
- state = ["available"]
- }
- owners = ["self"]
- }
- "linux-x64" = {
- filter = {
- name = ["ministack-v2-linux-x64"]
- state = ["available"]
- }
- owners = ["self"]
- }
- "windows-x64" = {
- filter = {
- name = ["ministack-v2-windows-x64"]
- state = ["available"]
- }
- owners = ["self"]
- }
-}
diff --git a/tests/ministack/multi-runner-webhook.tfvars b/tests/ministack/multi-runner-webhook.tfvars
new file mode 100644
index 0000000000..aaab6d1da1
--- /dev/null
+++ b/tests/ministack/multi-runner-webhook.tfvars
@@ -0,0 +1,32 @@
+aws_region = "eu-west-1"
+environment = "multi-runner-webhook"
+
+runners_lambda_zip = "../../lambda_output/runners.zip"
+webhook_lambda_zip = "../../lambda_output/webhook.zip"
+
+github_app = {
+ id = "123"
+ key_base64 = "ministack-invalid-key"
+ webhook_secret = "ministack-webhook-secret"
+}
+
+compute_provider = {
+ aws = {
+ ec2 = {
+ instance_types = ["m7a.large", "m5.large"]
+ ami = {
+ filter = {
+ name = ["ministack-webhook-linux-x64"]
+ state = ["available"]
+ }
+ owners = ["self"]
+ }
+ }
+ microvm = {
+ image_arn = "arn:aws:lambda:eu-west-1:000000000000:microvm-image:ministack"
+ image_version = "3.0"
+ egress_network_connectors = ["arn:aws:lambda:eu-west-1:000000000000:network-connector:ministack"]
+ ingress_network_connectors = []
+ }
+ }
+}
diff --git a/tests/ministack/run-example.sh b/tests/ministack/run-example.sh
index 17b516961b..a7707aeec8 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 | multi-runner-scale-set)
+ base | prebuilt | default | ephemeral | multi-runner | multi-runner-webhook | microvm-foundation | multi-runner-scale-set)
use_tfvars=true
;;
migration-test)
@@ -33,15 +32,15 @@ case "$example" in
use_tfvars=false
;;
*)
- echo "Supported examples for the runner are: base, prebuilt, default, ephemeral, multi-runner, multi-runner-v2, multi-runner-scale-set, migration-test, termination-watcher" >&2
+ echo "Supported examples for the runner are: base, prebuilt, default, ephemeral, multi-runner, multi-runner-webhook, microvm-foundation, multi-runner-scale-set, migration-test, termination-watcher" >&2
exit 64
;;
esac
case "$action" in
- init | plan | apply | destroy) ;;
+ init | plan | apply | destroy | output) ;;
*)
- echo "Usage: $0 {init|plan|apply|destroy} {base|prebuilt|default|ephemeral|multi-runner|multi-runner-v2|multi-runner-scale-set|migration-test|termination-watcher} [TFVARS_FILE]" >&2
+ echo "Usage: $0 {init|plan|apply|destroy|output} {base|prebuilt|default|ephemeral|multi-runner|multi-runner-webhook|microvm-foundation|multi-runner-scale-set|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
@@ -188,11 +196,11 @@ create_ami_fixture() {
architecture="$2"
ami_id=$(ministack_aws ec2 describe-images \
--owners self \
- --filters "Name=name,Values=$ami_name" "Name=state,Values=available" \
+ --filters "Name=name,Values=$ami_name" \
--query 'Images[0].ImageId' \
--output text)
- if [ "$ami_id" = "None" ]; then
+ if [ "$ami_id" = "None" ] || [ -z "$ami_id" ]; then
ami_id=$(ministack_aws ec2 register-image \
--name "$ami_name" \
--description "MiniStack test-only AMI" \
@@ -206,6 +214,25 @@ create_ami_fixture() {
$ami_id"
fi
+ attempts=60
+ while [ "$attempts" -gt 0 ]; do
+ ami_state=$(ministack_aws ec2 describe-images \
+ --owners self \
+ --image-ids "$ami_id" \
+ --query 'Images[0].State' \
+ --output text)
+ if [ "$ami_state" = "available" ]; then
+ return
+ fi
+
+ attempts=$((attempts - 1))
+ if [ "$attempts" -eq 0 ]; then
+ echo "AMI $ami_id ($ami_name) did not become available; last state: $ami_state" >&2
+ exit 70
+ fi
+ sleep 1
+ done
+
}
create_ssm_fixture() {
@@ -225,6 +252,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' \
@@ -320,10 +366,8 @@ $lambda_zip"
"/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-arm64" \
"ami-0abcdef1234567890"
;;
- multi-runner-v2)
- create_ami_fixture "ministack-v2-linux-arm64" arm64 >/dev/null
- create_ami_fixture "ministack-v2-linux-x64" x86_64 >/dev/null
- create_ami_fixture "ministack-v2-windows-x64" x86_64 >/dev/null
+ multi-runner-webhook)
+ create_ami_fixture "ministack-webhook-linux-x64" x86_64 >/dev/null
;;
multi-runner-scale-set)
create_ami_fixture "ministack-scale-set-linux-x64" x86_64 >/dev/null
@@ -367,4 +411,8 @@ case "$action" in
iac_init
iac_example destroy -auto-approve -input=false -parallelism=1 -compact-warnings
;;
+ output)
+ iac_init
+ iac_example output
+ ;;
esac