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 = [""] enable_userdata = false ``` + +## Lambda MicroVM images + +The `microvm-ubuntu` directory contains the Packer inputs for the Lambda MicroVM +image workflow. Unlike the AMI examples above, Lambda owns the image build. +The Packer template, Dockerfile, lifecycle-hook ZIP contract, and image +entrypoint are under `microvm-ubuntu/`; the compiled hook server is supplied +separately as a build artifact. + +Apply [`examples/microvm-foundation`](../examples/microvm-foundation) first, +then follow the [`microvm-ubuntu` build instructions](microvm-ubuntu/README.md) and run +Packer with its outputs. Use the resulting image ARN in the +[`examples/microvm`](../examples/microvm) runner example. diff --git a/images/microvm-ubuntu/README.md b/images/microvm-ubuntu/README.md new file mode 100644 index 0000000000..af1ce6b729 --- /dev/null +++ b/images/microvm-ubuntu/README.md @@ -0,0 +1,54 @@ +# Lambda MicroVM image build + +This directory contains the complete Lambda MicroVM image build inputs adapted +from the companion base-image repository: the Packer template, pinned ARM64 +Dockerfile, compiled lifecycle-hook ZIP contract, and image entrypoint. + +Before building the image: + +1. Apply `examples/microvm-foundation` in the target AWS Region. +2. Build and release the lifecycle-hook service from + `lambdas/services/microvm-lifecycle-hooks`, then set + `MICROVM_LIFECYCLE_HOOK_ZIP` to the released artifact. +3. Install Packer and set the required AWS, S3, IAM, connector, and image + variables. + +The image intentionally excludes the source repository's optional external +telemetry and Teleport services. It contains only the Actions runner, +CloudWatch Agent, and lifecycle-hook server; no credentials are stored in the +image source. + +The `github_agent.microvm.ubuntu.pkr.hcl` template packages a deterministic +artifact, resolves the Ubuntu ECR mirror to a digest, uploads the artifact to +the regional S3 bucket, and waits for the Lambda MicroVM image version to +become active. + +```bash +export AWS_REGION="" +export MICROVM_ARTIFACT_BUCKET="" +export MICROVM_BUILD_ROLE_ARN="" +export MICROVM_EGRESS_NETWORK_CONNECTOR_ARN="" +export MICROVM_IMAGE_NAME="" +export MICROVM_LIFECYCLE_HOOK_ZIP="" +export MICROVM_LOG_GROUP="" +export MICROVM_MEMORY_MIB=8192 +export MICROVM_UBUNTU_IMAGE="" +export MICROVM_IDEMPOTENCY_NONCE="$(date -u +%Y%m%dT%H%M%SZ)" + +packer init . +packer fmt -check=true github_agent.microvm.ubuntu.pkr.hcl +packer validate -evaluate-datasources github_agent.microvm.ubuntu.pkr.hcl +packer build -color=false github_agent.microvm.ubuntu.pkr.hcl +``` + +The build role, artifact bucket, and network connector are created by the +foundation module. The build role is used only while Packer creates and +publishes the image; it is not baked into the image and is not the role used +by runner jobs. The execution role is selected by the runner control plane and +passed to `RunMicrovm` at launch time, so it is not configured by this image +build. + +Keep the bucket private and versioned, use the module's least-privilege +policies, and do not put credentials in checked-in files. The lifecycle-hook +ZIP must contain the compiled `server.js` at its archive root; any bundled +dependencies must use safe relative paths. diff --git a/images/microvm-ubuntu/github_agent.microvm.ubuntu.pkr.hcl b/images/microvm-ubuntu/github_agent.microvm.ubuntu.pkr.hcl new file mode 100644 index 0000000000..7e4965f7d0 --- /dev/null +++ b/images/microvm-ubuntu/github_agent.microvm.ubuntu.pkr.hcl @@ -0,0 +1,110 @@ +# Lambda, rather than Packer, owns the MicroVM image build. This single +# pseudo-Packer target provides the same build interface as the AMI pipelines +# while delegating packaging, regional publication, and polling to boto3. +# The null builder and shell-local provisioner are Packer built-ins, so this +# template intentionally has no required_plugins entry for them. +variable "aws_region" { + description = "AWS Region for the S3 artifact, Ubuntu ECR mirror, and Lambda MicroVM image." + type = string + default = env("AWS_REGION") +} + +variable "artifact_bucket" { + description = "S3 artifact bucket. Lambda MicroVMs requires this bucket to be in aws_region." + type = string + default = env("MICROVM_ARTIFACT_BUCKET") +} + +variable "build_role_arn" { + description = "IAM role assumed by Lambda while it builds the MicroVM image." + type = string + default = env("MICROVM_BUILD_ROLE_ARN") +} + +variable "egress_network_connector_arn" { + description = "ARN of the regional Lambda Network Connector used for image-build egress." + type = string + default = env("MICROVM_EGRESS_NETWORK_CONNECTOR_ARN") +} + +variable "image_name" { + description = "Name of the customer Lambda MicroVM image." + type = string + default = env("MICROVM_IMAGE_NAME") +} + +variable "idempotency_nonce" { + description = "Per-attempt nonce that permits a workflow rerun to replace an asynchronously failed build." + type = string + default = env("MICROVM_IDEMPOTENCY_NONCE") +} + +variable "lifecycle_hook_zip" { + description = "ZIP containing the compiled lifecycle-hook server.js at the archive root." + type = string + default = env("MICROVM_LIFECYCLE_HOOK_ZIP") +} + +variable "log_group" { + description = "CloudWatch Logs group for the Lambda MicroVM image build." + type = string + default = env("MICROVM_LOG_GROUP") +} + +variable "memory_mib" { + description = "MicroVM memory tier in MiB. The complete runner image currently requires the 8192 MiB tier's 32 GiB disk." + type = string + default = env("MICROVM_MEMORY_MIB") +} + +variable "output_dir" { + description = "Directory for deterministic build artifacts and publication manifests." + type = string + default = env("MICROVM_OUTPUT_DIR") +} + +variable "release_version" { + description = "Stable or prerelease version recorded in MicroVM metadata." + type = string + default = env("MICROVM_RELEASE_VERSION") +} + +variable "ubuntu_image" { + description = "Regional private ECR mirror used for the Ubuntu 24.04 Dockerfile stages." + type = string + default = env("MICROVM_UBUNTU_IMAGE") +} + +source "null" "lambda_microvm" { + communicator = "none" +} + +build { + name = "lambda-microvm-image" + sources = [ + "source.null.lambda_microvm" + ] + + provisioner "shell-local" { + # MICROVM_ENVIRONMENT_VARIABLES is inherited from the build step. Do not + # add it here: shell-local renders environment_vars into the shell argv. + environment_vars = [ + "AWS_REGION=${var.aws_region}", + "MICROVM_ARTIFACT_BUCKET=${var.artifact_bucket}", + "MICROVM_BUILD_ROLE_ARN=${var.build_role_arn}", + "MICROVM_EGRESS_NETWORK_CONNECTOR_ARN=${var.egress_network_connector_arn}", + "MICROVM_IMAGE_NAME=${var.image_name}", + "MICROVM_IDEMPOTENCY_NONCE=${var.idempotency_nonce}", + "MICROVM_LIFECYCLE_HOOK_ZIP=${var.lifecycle_hook_zip}", + "MICROVM_LOG_GROUP=${var.log_group}", + "MICROVM_MEMORY_MIB=${var.memory_mib}", + "MICROVM_OUTPUT_DIR=${var.output_dir}", + "MICROVM_RELEASE_VERSION=${var.release_version}", + "MICROVM_UBUNTU_IMAGE=${var.ubuntu_image}", + "PYTHONDONTWRITEBYTECODE=1", + "PYTHONUNBUFFERED=1", + ] + script = "packer/scripts/microvm/build-microvm-image.py" + timeout = "90m" + } +} diff --git a/images/microvm-ubuntu/packer/scripts/microvm/build-microvm-image.py b/images/microvm-ubuntu/packer/scripts/microvm/build-microvm-image.py new file mode 100755 index 0000000000..c8fb9e5b6d --- /dev/null +++ b/images/microvm-ubuntu/packer/scripts/microvm/build-microvm-image.py @@ -0,0 +1,583 @@ +#!/usr/bin/env python3 +"""Package and publish the ARM64 Lambda MicroVM runner image.""" + +from __future__ import annotations + +import base64 +import datetime as dt +import hashlib +import json +import os +import re +import stat +import subprocess +import sys +import tempfile +import time +import zipfile +from dataclasses import dataclass +from decimal import Decimal +from pathlib import Path +from pathlib import PurePosixPath +from typing import Any, Iterable, Mapping + +REPOSITORY_ROOT = Path(__file__).resolve().parents[3] +IMAGE_ROOT = Path(__file__).resolve().parent / 'image' +OUTPUT_ROOT = REPOSITORY_ROOT / 'output' / 'microvm' +DOCKERFILE = 'ubuntu24.arm64.Dockerfile' +ZIP_TIMESTAMP = (1980, 1, 1, 0, 0, 0) +WAIT_TIMEOUT_SECONDS = 3000 +EXCLUDED_DIRECTORIES = { + '.cache', + '.git', + '.mypy_cache', + '.pytest_cache', + '.ruff_cache', + '__pycache__', + 'dist', + 'node_modules', +} +EXCLUDED_FILES = {'.DS_Store', '.git'} + + +class BuildError(RuntimeError): + """Expected publication failure.""" + + +@dataclass(frozen=True) +class Settings: + region: str + artifact_bucket: str + build_role_arn: str + egress_network_connector_arn: str + environment_variables: Mapping[str, str] + image_name: str + idempotency_nonce: str + lifecycle_hook_zip: Path + log_group: str + memory_mib: int + output_dir: Path + release_version: str + ubuntu_image: str + + +@dataclass(frozen=True) +class Artifact: + path: Path + sha256: str + + +def environment(name: str, default: str = '') -> str: + return os.environ.get(name, '').strip() or default + + +def load_settings() -> Settings: + return Settings( + region=environment('AWS_REGION'), + artifact_bucket=environment('MICROVM_ARTIFACT_BUCKET'), + build_role_arn=environment('MICROVM_BUILD_ROLE_ARN'), + egress_network_connector_arn=environment( + 'MICROVM_EGRESS_NETWORK_CONNECTOR_ARN' + ), + environment_variables=json.loads( + environment('MICROVM_ENVIRONMENT_VARIABLES', '{}') + ), + image_name=environment('MICROVM_IMAGE_NAME'), + idempotency_nonce=environment('MICROVM_IDEMPOTENCY_NONCE'), + lifecycle_hook_zip=Path( + environment('MICROVM_LIFECYCLE_HOOK_ZIP') + ).resolve(), + log_group=environment('MICROVM_LOG_GROUP'), + memory_mib=int(environment('MICROVM_MEMORY_MIB')), + output_dir=Path( + environment('MICROVM_OUTPUT_DIR', str(OUTPUT_ROOT)) + ).resolve(), + release_version=environment('MICROVM_RELEASE_VERSION'), + ubuntu_image=environment('MICROVM_UBUNTU_IMAGE'), + ) + + +def artifact_files(root: Path) -> Iterable[Path]: + for current_root, directories, files in os.walk(root): + directories[:] = sorted( + name for name in directories if name not in EXCLUDED_DIRECTORIES + ) + current = Path(current_root) + for name in sorted(files): + path = current / name + if all( + ( + name not in EXCLUDED_FILES, + path.suffix not in {'.pyc', '.pyo'}, + path.is_file(), + not path.is_symlink(), + ) + ): + yield path + + +def render_dockerfile(contents: bytes, ubuntu_image: str) -> bytes: + rendered = re.sub( + r'^ARG UBUNTU_IMAGE(?:=.*)?$', + f"ARG UBUNTU_IMAGE={json.dumps(ubuntu_image)}", + contents.decode(), + flags=re.MULTILINE, + ) + return rendered.encode() + + +def validate_lifecycle_hook_zip(path: Path) -> None: + if not path.is_file(): + raise BuildError( + f'MICROVM_LIFECYCLE_HOOK_ZIP must point to a file: {path}' + ) + + try: + with zipfile.ZipFile(path) as archive: + members = archive.infolist() + except (OSError, zipfile.BadZipFile) as error: + raise BuildError( + f'MICROVM_LIFECYCLE_HOOK_ZIP is not a valid ZIP archive: {path}' + ) from error + + files = set() + for member in members: + member_path = PurePosixPath(member.filename) + if member_path.is_absolute() or '..' in member_path.parts: + raise BuildError( + 'MICROVM_LIFECYCLE_HOOK_ZIP contains an unsafe archive path: ' + f'{member.filename}' + ) + if stat.S_IFMT(member.external_attr >> 16) == stat.S_IFLNK: + raise BuildError( + 'MICROVM_LIFECYCLE_HOOK_ZIP must not contain symbolic links: ' + f'{member.filename}' + ) + if not member.filename.endswith('/'): + files.add(member.filename) + + if 'server.js' not in files: + raise BuildError( + 'MICROVM_LIFECYCLE_HOOK_ZIP must contain a compiled server.js ' + 'at the archive root' + ) + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open('rb') as file_handle: + for chunk in iter(lambda: file_handle.read(1024 * 1024), b''): + digest.update(chunk) + return digest.hexdigest() + + +def create_artifact(settings: Settings, ubuntu_image: str) -> Artifact: + validate_lifecycle_hook_zip(settings.lifecycle_hook_zip) + files = [ + ( + 'Dockerfile' + if path == IMAGE_ROOT / DOCKERFILE + else path.relative_to(IMAGE_ROOT).as_posix(), + path, + ) + for path in artifact_files(IMAGE_ROOT) + ] + files.append(('lifecycle-hook.zip', settings.lifecycle_hook_zip)) + files.sort(key=lambda item: item[0]) + settings.output_dir.mkdir(parents=True, exist_ok=True) + + with tempfile.TemporaryDirectory( + prefix='microvm-package-', dir=settings.output_dir + ) as temporary: + temporary_zip = Path(temporary) / 'microvm-image.zip' + with zipfile.ZipFile( + temporary_zip, + mode='w', + compression=zipfile.ZIP_DEFLATED, + compresslevel=9, + ) as archive: + for archive_name, source in files: + contents = source.read_bytes() + if archive_name == 'Dockerfile': + contents = render_dockerfile(contents, ubuntu_image) + mode = 0o755 if source.stat().st_mode & 0o111 else 0o644 + info = zipfile.ZipInfo(archive_name, ZIP_TIMESTAMP) + info.create_system = 3 + info.compress_type = zipfile.ZIP_DEFLATED + info.external_attr = (stat.S_IFREG | mode) << 16 + archive.writestr( + info, + contents, + compress_type=zipfile.ZIP_DEFLATED, + compresslevel=9, + ) + + digest = sha256_file(temporary_zip) + artifact_path = settings.output_dir / ( + f"{settings.image_name}-{digest[:12]}.zip" + ) + os.replace(temporary_zip, artifact_path) + return Artifact(artifact_path, digest) + + +def source_revision() -> str: + revision = environment('GITHUB_SHA') or environment('SOURCE_REVISION') + if revision: + return revision[:12] + completed = subprocess.run( + [ + 'git', + '-C', + str(REPOSITORY_ROOT), + 'rev-parse', + '--short=12', + 'HEAD', + ], + check=True, + capture_output=True, + text=True, + ) + return completed.stdout.strip() + + +def aws_session(region: str) -> Any: + try: + import boto3 # type: ignore[import-not-found] + except ModuleNotFoundError as error: + raise BuildError( + 'boto3 is required to publish the MicroVM image' + ) from error + return boto3.Session(region_name=region) + + +def microvm_client(session: Any, region: str) -> Any: + try: + return session.client('lambda-microvms', region_name=region) + except Exception as error: + if type(error).__name__ == 'UnknownServiceError': + raise BuildError( + 'Version of boto3 must contain the Lambda MicroVM service model' + ) from error + raise + + +def resolve_ubuntu_image(ecr: Any, image: str) -> str: + if '@' in image: + return image + repository_uri, tag = image.rsplit(':', 1) + registry, repository = repository_uri.split('/', 1) + account = registry.split('.', 1)[0] + response = ecr.describe_images( + registryId=account, + repositoryName=repository, + imageIds=[{'imageTag': tag}], + ) + digest = response['imageDetails'][0]['imageDigest'] + return f"{repository_uri}@{digest}" + + +def upload_artifact( + s3: Any, settings: Settings, artifact: Artifact, revision: str +) -> str: + key = f"lambda-microvms/artifacts/{artifact.sha256}.zip" + checksum = base64.b64encode(bytes.fromhex(artifact.sha256)).decode() + with artifact.path.open('rb') as file_handle: + s3.put_object( + Bucket=settings.artifact_bucket, + Key=key, + Body=file_handle, + ChecksumSHA256=checksum, + ContentType='application/zip', + Metadata={ + 'sha256': artifact.sha256, + 'source-revision': revision, + }, + ) + return f"s3://{settings.artifact_bucket}/{key}" + + +def find_image(client: Any, name: str) -> str: + request: dict[str, Any] = {'maxResults': 50, 'nameFilter': name} + while True: + response = client.list_microvm_images(**request) + for image in response.get('items', []): + if image.get('name') == name: + return str(image['imageArn']) + token = response.get('nextToken') + if not token: + return '' + request['nextToken'] = token + + +def log_stream(settings: Settings) -> str: + if settings.idempotency_nonce: + return f"{settings.image_name}/{settings.idempotency_nonce}" + return settings.image_name + + +def build_request( + settings: Settings, + artifact_uri: str, + revision: str, + image_arn: str, +) -> dict[str, Any]: + operation = 'update' if image_arn else 'create' + description = f"Ephemeral GitHub Actions runner from {revision}" + if settings.release_version: + description = ( + f"Ephemeral GitHub Actions runner release " + f"{settings.release_version} from {revision}" + ) + request: dict[str, Any] = { + 'additionalOsCapabilities': ['ALL'], + 'baseImageArn': ( + f"arn:aws:lambda:{settings.region}:aws:microvm-image:al2023-1" + ), + 'buildRoleArn': settings.build_role_arn, + 'codeArtifact': {'uri': artifact_uri}, + 'cpuConfigurations': [{'architecture': 'ARM_64'}], + 'description': description, + 'egressNetworkConnectors': [settings.egress_network_connector_arn], + 'environmentVariables': dict(settings.environment_variables), + 'hooks': { + 'port': 8080, + 'microvmHooks': { + 'run': 'ENABLED', + 'runTimeoutInSeconds': 60, + 'terminate': 'ENABLED', + 'terminateTimeoutInSeconds': 60, + }, + 'microvmImageHooks': { + 'ready': 'ENABLED', + 'readyTimeoutInSeconds': 120, + 'validate': 'ENABLED', + 'validateTimeoutInSeconds': 120, + }, + }, + 'logging': { + 'cloudWatch': { + 'logGroup': settings.log_group, + 'logStream': log_stream(settings), + } + }, + 'resources': [{'minimumMemoryInMiB': settings.memory_mib}], + } + if operation == 'create': + request['name'] = settings.image_name + else: + request['imageIdentifier'] = image_arn + + canonical = json.dumps(request, sort_keys=True, separators=(',', ':')) + request['clientToken'] = hashlib.sha256( + ( + f"{settings.region}|{operation}|{settings.idempotency_nonce}|" + f"{canonical}" + ).encode() + ).hexdigest() + return request + + +def start_build(client: Any, request: Mapping[str, Any]) -> dict[str, Any]: + if 'imageIdentifier' in request: + print('Starting Lambda MicroVM image update') + return client.update_microvm_image(**request) + print('Starting Lambda MicroVM image create') + return client.create_microvm_image(**request) + + +def wait_for_image( + client: Any, image_arn: str, image_version: str +) -> tuple[dict[str, Any], dict[str, Any]]: + deadline = time.monotonic() + WAIT_TIMEOUT_SECONDS + last_state: tuple[str, str, str] | None = None + while time.monotonic() < deadline: + try: + version = client.get_microvm_image_version( + imageIdentifier=image_arn, + imageVersion=image_version, + ) + except Exception as error: + response = getattr(error, 'response', {}) + error_code = response.get('Error', {}).get('Code') + if error_code == 'ResourceNotFoundException': + time.sleep(10) + continue + raise + + state = str(version.get('state', 'UNKNOWN')) + status = str(version.get('status', 'UNKNOWN')) + image: dict[str, Any] = {} + image_state = 'UNKNOWN' + if state == 'SUCCESSFUL': + image = client.get_microvm_image(imageIdentifier=image_arn) + image_state = str(image.get('state', 'UNKNOWN')) + observed = (state, status, image_state) + if observed != last_state: + print( + f"MicroVM image version {image_version}: state={state} " + f"status={status} image_state={image_state}" + ) + last_state = observed + if state == 'FAILED': + raise BuildError( + 'MicroVM image build failed: ' + f"{version.get('stateReason', 'no reason returned')}" + ) + if state == 'SUCCESSFUL' and status == 'ACTIVE' and image_state in { + 'CREATED', + 'UPDATED', + }: + return image, version + time.sleep(10) + raise BuildError( + f"timed out waiting for MicroVM image after " + f"{WAIT_TIMEOUT_SECONDS} seconds" + ) + + +def print_build_logs( + logs: Any, settings: Settings, start_time_ms: int +) -> None: + request: dict[str, Any] = { + 'logGroupName': settings.log_group, + 'logStreamNames': [log_stream(settings)], + 'startTime': start_time_ms, + } + while True: + response = logs.filter_log_events(**request) + for event in response.get('events', []): + timestamp = ( + dt.datetime.fromtimestamp( + int(event['timestamp']) / 1000, + tz=dt.timezone.utc, + ) + .isoformat(timespec='milliseconds') + .replace('+00:00', 'Z') + ) + message = str(event.get('message', '')).rstrip() + print(f"[microvm-build {timestamp}] {message}") + token = response.get('nextToken') + if not token or token == request.get('nextToken'): + return + request['nextToken'] = token + + +def json_value(value: Any) -> Any: + if isinstance(value, (dt.date, dt.datetime)): + return value.isoformat() + if isinstance(value, Decimal): + return str(value) + raise TypeError(f"{type(value).__name__} is not JSON serializable") + + +def write_manifest(path: Path, value: Mapping[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with tempfile.NamedTemporaryFile( + mode='w', + encoding='utf-8', + dir=path.parent, + delete=False, + ) as temporary: + json.dump( + value, + temporary, + default=json_value, + indent=2, + sort_keys=True, + ) + temporary.write('\n') + temporary_path = Path(temporary.name) + os.replace(temporary_path, path) + + +def run() -> int: + settings = load_settings() + revision = source_revision() + session = aws_session(settings.region) + ecr = session.client('ecr', region_name=settings.region) + ubuntu_image = resolve_ubuntu_image(ecr, settings.ubuntu_image) + print(f"Using digest-pinned Ubuntu mirror: {ubuntu_image}") + + artifact = create_artifact(settings, ubuntu_image) + print(f"Packaged MicroVM artifact: {artifact.path}") + print(f"Artifact SHA-256: {artifact.sha256}") + + s3 = session.client('s3', region_name=settings.region) + artifact_uri = upload_artifact(s3, settings, artifact, revision) + print(f"Uploaded {artifact_uri}") + + client = microvm_client(session, settings.region) + existing_image_arn = find_image(client, settings.image_name) + request = build_request( + settings, + artifact_uri, + revision, + existing_image_arn, + ) + started_at = int(time.time() * 1000) - 5000 + response = start_build(client, request) + image_arn = str(response['imageArn']) + image_version = str(response['imageVersion']) + + manifest = { + 'artifactSha256': artifact.sha256, + 'artifactUri': artifact_uri, + 'egressNetworkConnectorArn': settings.egress_network_connector_arn, + 'imageArn': image_arn, + 'imageVersion': image_version, + 'logGroup': settings.log_group, + 'logStream': log_stream(settings), + 'name': settings.image_name, + 'operation': 'update' if existing_image_arn else 'create', + 'region': settings.region, + 'releaseVersion': settings.release_version, + 'sourceRevision': revision, + 'ubuntuBaseImage': ubuntu_image, + } + manifest_path = settings.output_dir / 'microvm-image.json' + write_manifest(manifest_path, manifest) + + try: + image, version = wait_for_image(client, image_arn, image_version) + finally: + try: + print_build_logs( + session.client('logs', region_name=settings.region), + settings, + started_at, + ) + except Exception as error: + print( + f"Warning: could not retrieve build logs: {error}", + file=sys.stderr, + ) + + manifest.update( + { + 'imageState': image.get('state'), + 'state': version.get('state'), + 'status': version.get('status'), + } + ) + write_manifest(manifest_path, manifest) + print( + f"Lambda MicroVM image is ready: " + f"{image_arn} version {image_version}" + ) + print(f"Manifest: {manifest_path}") + return 0 + + +def main() -> int: + try: + return run() + except KeyboardInterrupt: + print('Error: interrupted', file=sys.stderr) + return 130 + except Exception as error: + print(f"Error: {error}", file=sys.stderr) + return 1 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/images/microvm-ubuntu/packer/scripts/microvm/image/.dockerignore b/images/microvm-ubuntu/packer/scripts/microvm/image/.dockerignore new file mode 100644 index 0000000000..7a60b85e14 --- /dev/null +++ b/images/microvm-ubuntu/packer/scripts/microvm/image/.dockerignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/images/microvm-ubuntu/packer/scripts/microvm/image/image-entrypoint.sh b/images/microvm-ubuntu/packer/scripts/microvm/image/image-entrypoint.sh new file mode 100644 index 0000000000..5313aff275 --- /dev/null +++ b/images/microvm-ubuntu/packer/scripts/microvm/image/image-entrypoint.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# shellcheck shell=bash + +# Start the compiled lifecycle-hook server from the supplied ZIP artifact. + +set -euo pipefail + +readonly hook_node="${MICROVM_HOOK_NODE:-/opt/actions-runner/externals/node24/bin/node}" +readonly hook_server="${MICROVM_HOOK_SERVER:-/opt/microvm/server.js}" + +if [[ ! -x "$hook_node" ]]; then + printf '[microvm] Lifecycle hook Node executable is unavailable: %s\n' \ + "$hook_node" >&2 + exit 1 +fi +if [[ ! -r "$hook_server" ]]; then + printf '[microvm] Lifecycle hook server is unavailable: %s\n' \ + "$hook_server" >&2 + exit 1 +fi + +exec "$hook_node" "$hook_server" diff --git a/images/microvm-ubuntu/packer/scripts/microvm/image/services/cloudwatch-agent.sh b/images/microvm-ubuntu/packer/scripts/microvm/image/services/cloudwatch-agent.sh new file mode 100644 index 0000000000..1924c7fcd8 --- /dev/null +++ b/images/microvm-ubuntu/packer/scripts/microvm/image/services/cloudwatch-agent.sh @@ -0,0 +1,49 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -euo pipefail + +readonly agent_root=/opt/aws/amazon-cloudwatch-agent +readonly config_directory=/etc/cwagentconfig +readonly config_path="${config_directory}/config.json" +readonly microvm_id="${MICROVM_ID:?}" +readonly runner_config_ssm_path="${RUNNER_CONFIG_SSM_PATH:?}" + +read_parameter() { + AWS_PAGER='' /usr/local/bin/aws ssm get-parameter \ + --name "$1" \ + --query Parameter.Value \ + --output text \ + --no-cli-pager +} + +enabled="$(read_parameter "${runner_config_ssm_path}/enable_cloudwatch")" +if [[ "$enabled" == false ]]; then + printf '[cloudwatch-agent] disabled by runner configuration\n' >&2 + /command/s6-svc -d /run/service/cloudwatch-agent + exit 0 +fi +if [[ "$enabled" != true ]]; then + printf '[cloudwatch-agent] enable_cloudwatch must be true or false\n' >&2 + exit 1 +fi + +install -d -m 0700 -o root -g root "$config_directory" +umask 077 +read_parameter "${runner_config_ssm_path}/cloudwatch_agent_config_runner" | + MICROVM_ID="$microvm_id" jq --exit-status ' + select(type == "object") | + walk( + if type == "string" then + gsub("\\{microvm_id\\}"; env.MICROVM_ID) + else + . + end + ) +' >"$config_path" +chmod 0600 "$config_path" + +exec env \ + RUN_IN_AWS=True \ + RUN_IN_CONTAINER=True \ + "${agent_root}/bin/start-amazon-cloudwatch-agent" diff --git a/images/microvm-ubuntu/packer/scripts/microvm/image/start-services.sh b/images/microvm-ubuntu/packer/scripts/microvm/image/start-services.sh new file mode 100644 index 0000000000..5865ed1758 --- /dev/null +++ b/images/microvm-ubuntu/packer/scripts/microvm/image/start-services.sh @@ -0,0 +1,39 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -euo pipefail + +readonly internal_services_log=/var/log/microvm/internal-services.log +readonly microvm_id="${MICROVM_ID:?}" +readonly runner_config_ssm_path="${RUNNER_CONFIG_SSM_PATH:?}" +readonly s6_environment=/run/s6/container_environment + +exec > >(/usr/bin/tee --append -- "$internal_services_log") +exec 2> >(/usr/bin/tee --append -- "$internal_services_log" >&2) + +if [[ -z "${MICROVM_SERVICES:-}" ]]; then + exit 0 +fi + +IFS=',' read -r -a services <<<"${MICROVM_SERVICES}" +for service in "${services[@]}"; do + [[ -z "$service" ]] && continue + if [[ ! "$service" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$ ]]; then + printf '[microvm-services] invalid service name: %s\n' "$service" >&2 + exit 2 + fi + if [[ ! -d "/run/service/${service}" ]]; then + printf '[microvm-services] service is unavailable: %s\n' "$service" >&2 + exit 1 + fi +done + +printf '%s' "$microvm_id" >"${s6_environment}/MICROVM_ID" +chmod 0600 "${s6_environment}/MICROVM_ID" +printf '%s' "$runner_config_ssm_path" >"${s6_environment}/RUNNER_CONFIG_SSM_PATH" +chmod 0600 "${s6_environment}/RUNNER_CONFIG_SSM_PATH" + +for service in "${services[@]}"; do + [[ -z "$service" ]] && continue + /command/s6-svc -u "/run/service/${service}" +done diff --git a/images/microvm-ubuntu/packer/scripts/microvm/image/ubuntu24.arm64.Dockerfile b/images/microvm-ubuntu/packer/scripts/microvm/image/ubuntu24.arm64.Dockerfile new file mode 100644 index 0000000000..69d5f44552 --- /dev/null +++ b/images/microvm-ubuntu/packer/scripts/microvm/image/ubuntu24.arm64.Dockerfile @@ -0,0 +1,174 @@ +# syntax=docker/dockerfile:1 + +# Lambda MicroVMs currently run ARM64 images. The image contains the Actions +# runner, CloudWatch Agent, S6 overlay, and compiled lifecycle-hook server. +ARG UBUNTU_IMAGE=ubuntu:24.04 + +# hadolint ignore=DL3006 +FROM ${UBUNTU_IMAGE} AS tooling + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +ARG AWS_CLI_VERSION="2.36.24" +ARG AWS_CLI_SHA256=c024c45a9d22005f81c7c0fab9e23ee7118ffa210d812845b42e980cf93727a7 + +ARG RUNNER_VERSION="2.336.0" +ARG RUNNER_SHA256=58b758e420b87093fbd4bfddd368074960053e2f1388f01848c82624b90f27d1 + +ARG CLOUDWATCH_AGENT_VERSION=1.300071.0b1720 + +# S6 overlay is pinned and verified before it is copied into the runtime image. +ARG S6_OVERLAY_VERSION="3.2.3.2" +ARG S6_OVERLAY_NOARCH_SHA256=5379750ed30a84bbd2e2dd74847ba6b5bd29cd0b2e3ea2ec58049b57eb2eda12 +ARG S6_OVERLAY_AARCH64_SHA256=b17f17a82e7a515c682a91edaf2ffdabb73f891981b6c1fd712115693a2f8b4c + +# These packages are used only while assembling the runtime payload. +# hadolint ignore=DL3008,DL3015 +RUN apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + tar \ + unzip \ + xz-utils \ + && rm -rf /var/lib/apt/lists/* + +RUN install -d -m 0755 \ + /export/usr/local/aws-cli \ + /export/usr/local/bin \ + /export/opt/actions-runner \ + /export/opt/microvm \ + /export/run/amazon \ + /export/s6 \ + && curl --fail --location --show-error --silent \ + "https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz" \ + --output /tmp/actions-runner.tar.gz \ + && printf '%s %s\n' "${RUNNER_SHA256}" /tmp/actions-runner.tar.gz | sha256sum --check --strict \ + && tar --extract --gzip --no-same-owner --file /tmp/actions-runner.tar.gz \ + --directory /export/opt/actions-runner \ + && test -x /export/opt/actions-runner/externals/node24/bin/node \ + && rm -f /tmp/actions-runner.tar.gz + +RUN curl --fail --location --show-error --silent \ + "https://amazoncloudwatch-agent.s3.amazonaws.com/ubuntu/arm64/${CLOUDWATCH_AGENT_VERSION}/amazon-cloudwatch-agent.deb" \ + --output /tmp/amazon-cloudwatch-agent.deb \ + && install -d -m 0755 /tmp/cloudwatch-agent-root \ + && dpkg-deb --extract /tmp/amazon-cloudwatch-agent.deb /tmp/cloudwatch-agent-root \ + && test "$(cat /tmp/cloudwatch-agent-root/opt/aws/amazon-cloudwatch-agent/bin/CWAGENT_VERSION)" \ + = "${CLOUDWATCH_AGENT_VERSION}" \ + && install -d -m 0755 /tmp/cloudwatch-agent-root/run/amazon \ + && mv /tmp/cloudwatch-agent-root/var/run/amazon/amazon-cloudwatch-agent \ + /tmp/cloudwatch-agent-root/run/amazon/ \ + && rmdir /tmp/cloudwatch-agent-root/var/run/amazon /tmp/cloudwatch-agent-root/var/run \ + && cp -a /tmp/cloudwatch-agent-root/. /export/ \ + && rm -f /tmp/amazon-cloudwatch-agent.deb \ + && rm -rf /tmp/cloudwatch-agent-root /export/etc/init /export/etc/systemd + +RUN curl --fail --location --show-error --silent \ + "https://awscli.amazonaws.com/awscli-exe-linux-aarch64-${AWS_CLI_VERSION}.zip" \ + --output /tmp/awscliv2.zip \ + && printf '%s %s\n' "${AWS_CLI_SHA256}" /tmp/awscliv2.zip | sha256sum --check --strict \ + && unzip -q /tmp/awscliv2.zip -d /tmp \ + && /tmp/aws/install \ + --install-dir /export/usr/local/aws-cli \ + --bin-dir /export/usr/local/bin \ + && rm -f /export/usr/local/bin/aws /export/usr/local/bin/aws_completer \ + && ln -s ../aws-cli/v2/current/bin/aws /export/usr/local/bin/aws \ + && ln -s ../aws-cli/v2/current/bin/aws_completer /export/usr/local/bin/aws_completer \ + && rm -rf /tmp/aws /tmp/awscliv2.zip + +RUN curl --fail --location --show-error --silent \ + "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz" \ + --output /tmp/s6-overlay-noarch.tar.xz \ + && curl --fail --location --show-error --silent \ + "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-aarch64.tar.xz" \ + --output /tmp/s6-overlay-aarch64.tar.xz \ + && printf '%s %s\n' "${S6_OVERLAY_NOARCH_SHA256}" \ + /tmp/s6-overlay-noarch.tar.xz | sha256sum --check --strict \ + && printf '%s %s\n' "${S6_OVERLAY_AARCH64_SHA256}" \ + /tmp/s6-overlay-aarch64.tar.xz | sha256sum --check --strict \ + && tar --extract --xz --preserve-permissions --file /tmp/s6-overlay-noarch.tar.xz \ + --directory /export \ + && tar --extract --xz --preserve-permissions --file /tmp/s6-overlay-aarch64.tar.xz \ + --directory /export \ + && rm -f /tmp/s6-overlay-noarch.tar.xz /tmp/s6-overlay-aarch64.tar.xz + +COPY microvm-lifecycle-hooks.zip /tmp/microvm-lifecycle-hooks.zip +RUN unzip -q /tmp/microvm-lifecycle-hooks.zip -d /export/opt/microvm \ + && test -r /export/opt/microvm/server.js \ + && rm -f /tmp/microvm-lifecycle-hooks.zip + +FROM ${UBUNTU_IMAGE} + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +# These are the Actions runner runtime dependencies. Keep the list aligned +# with the runner's supported Ubuntu dependencies. +# hadolint ignore=DL3008,DL3015 +RUN apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + ca-certificates \ + git \ + jq \ + libicu74 \ + libkrb5-3 \ + liblttng-ust1t64 \ + libssl3t64 \ + zlib1g \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=tooling /export/ / + +RUN existing_group="$(getent group 1000 | cut -d: -f1)" \ + && if [ -n "${existing_group}" ]; then \ + groupmod --new-name runner "${existing_group}"; \ + else \ + groupadd --gid 1000 runner; \ + fi \ + && existing_user="$(getent passwd 1000 | cut -d: -f1)" \ + && if [ -n "${existing_user}" ]; then \ + usermod --login runner --home /home/runner --move-home \ + --shell /bin/bash "${existing_user}"; \ + else \ + useradd --create-home --home-dir /home/runner --shell /bin/bash \ + --uid 1000 --gid 1000 runner; \ + fi \ + && install -d -m 0755 /opt/hostedtoolcache /opt/microvm /etc/services.d/cloudwatch-agent /var/log/microvm \ + && install -m 0600 /dev/null /var/log/microvm/internal-services.log \ + && install -m 0600 /dev/null /var/log/microvm/run.log \ + && chown -R runner:runner /home/runner /opt/actions-runner /opt/hostedtoolcache + +COPY --chmod=0555 image-entrypoint.sh /opt/microvm/image-entrypoint.sh +COPY --chmod=0555 start-services.sh /opt/microvm/start-services.sh +COPY --chmod=0755 services/cloudwatch-agent.sh /etc/services.d/cloudwatch-agent/run +RUN touch /etc/services.d/cloudwatch-agent/down \ + && chmod 0644 /etc/services.d/cloudwatch-agent/down + +ENV ACTIONS_RUNNER_ROOT="/opt/actions-runner" \ + AGENT_TOOLSDIRECTORY="/opt/hostedtoolcache" \ + HOME="/home/runner" \ + HOOK_PORT="8080" \ + INTERNAL_SERVICES="/opt/microvm/start-services.sh" \ + MICROVM_HOOK_LOG_FILE="/var/log/microvm/run.log" \ + MICROVM_HOOK_NODE="/opt/actions-runner/externals/node24/bin/node" \ + MICROVM_HOOK_SERVER="/opt/microvm/server.js" \ + MICROVM_SERVICES="cloudwatch-agent" \ + RUN_HOOK_TIMEOUT_SECONDS="52" \ + RUNNER_CONFIG_POLL_SECONDS="2" \ + RUNNER_CONFIG_TIMEOUT_SECONDS="20" \ + RUNNER_GID="1000" \ + RUNNER_HOME="/home/runner" \ + RUNNER_LAUNCH_RESERVE_SECONDS="7" \ + RUNNER_ROOT="/opt/actions-runner" \ + RUNNER_UID="1000" \ + RUNNER_USER="runner" \ + RUNNER_TOOL_CACHE="/opt/hostedtoolcache" \ + RUNNER_TOOLSDIRECTORY="/opt/hostedtoolcache" + +# The lifecycle hook owns the MicroVM control socket and log file. +# hadolint ignore=DL3002 +USER 0 +WORKDIR /opt/actions-runner +EXPOSE 8080 +ENTRYPOINT ["/init"] +CMD ["/command/with-contenv", "/opt/microvm/image-entrypoint.sh"] diff --git a/mkdocs.yaml b/mkdocs.yaml index 4553b6f71f..b8158c5c5d 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -71,6 +71,7 @@ nav: - AMI Housekeeper: modules/public/ami-housekeeper.md - Lambda Downloader: modules/public/download-lambda.md - Setup IAM permissions: modules/public/setup-iam-permissions.md + - MicroVM foundation: modules/public/microvm-foundation.md - Submodules (internal): - Runners: modules/internal/runners.md - Syncer: modules/internal/runner-binaries-syncer.md diff --git a/modules/microvm-foundation/README.md b/modules/microvm-foundation/README.md new file mode 100644 index 0000000000..dde8e4a2a1 --- /dev/null +++ b/modules/microvm-foundation/README.md @@ -0,0 +1,153 @@ +# Lambda MicroVM Regional Foundation + +This module creates the regional AWS prerequisites for building and running +Lambda MicroVM GitHub Actions runners and is intended to be deployed once per +AWS Region. + +It manages: + +- A private, encrypted, versioned S3 bucket for content-addressed image build artifacts. +- A Lambda-trusted build role with scoped S3, CloudWatch Logs, and optional ECR pull access. +- Dedicated no-ingress security groups and native Lambda Network Connector resources for each configured VPC/subnet set. +- A Lambda-trusted Network Connector operator role and propagation barrier. +- An unattached runtime usage policy for the reserved image namespace and connector inventory. + +## Build role and execution role + +MicroVM deployments use two different IAM roles with different lifecycles: + +- The `build_role_arn` output is the **build role**. The image builder assumes + this role while it creates and publishes a MicroVM image. It grants the + image-build permissions for the foundation artifact bucket, logs, and any + configured ECR repositories. It is not the role used by a runner job. +- The **execution role** is attached to each MicroVM when the runner control + plane launches it. The control-plane TypeScript passes this role to + `RunMicrovm`; the Lambda that calls that API must be allowed to pass the + role. The MicroVM and the ephemeral runner use this role at runtime. + +The execution role is resolved by the runner configuration and is intentionally +not created by this foundation module. The foundation creates the regional +build resources and the reusable `usage_policy_arn`; the runner/control-plane +configuration owns the runtime role and its provider-specific permissions. + +The module does not create MicroVM images, runner execution roles, or the +runner control plane. Attach `usage_policy_arn` to the control-plane role that +owns the runtime launch operations. The caller must also grant the Terraform +identity `iam:PassRole` for the operator role with +`iam:PassedToService=lambda.amazonaws.com`. + +The module deliberately does not configure an AWS provider. Configure the +provider in the root module or example so credentials and account selection +remain caller-owned. + +```hcl +provider "aws" { + region = "eu-west-1" +} + +module "microvm_foundation" { + source = "../../modules/microvm-foundation" + + aws_region = "eu-west-1" + tags = { Environment = "example" } + build_policy_name_prefix = "github-actions-runner-microvm-build-policy-" + build_role_name_prefix = "github-actions-runner-microvm-build-" + network_connector_operator_role_name_prefix = "github-actions-runner-microvm-network-operator-" + usage_policy_name_prefix = "github-actions-runner-microvm-runtime-usage-policy-" + + image_name_prefix = "github-actions-runner-ubuntu-arm64" + + network_connectors = { + cicd = { + name = "github-actions-runner-egress" + vpc_id = "vpc-0123456789abcdef0" + subnet_ids = ["subnet-0123456789abcdef0"] + } + } +} +``` + +The companion `examples/microvm-foundation` directory is a complete setup +example. Apply it before following the direct Packer build instructions in +`images/microvm-ubuntu/README.md` or using the `examples/microvm` runner example. + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.5.6 | +| [aws](#requirement\_aws) | >= 6.61 | +| [time](#requirement\_time) | >= 0.13 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 6.61 | +| [time](#provider\_time) | >= 0.13 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [aws_iam_policy.build](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.usage](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_role.build](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role.operator](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role_policy_attachment.build](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.operator](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_lambdacore_network_connector.connector](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambdacore_network_connector) | resource | +| [aws_s3_bucket.artifacts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket) | resource | +| [aws_s3_bucket_lifecycle_configuration.artifacts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_lifecycle_configuration) | resource | +| [aws_s3_bucket_ownership_controls.artifacts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_ownership_controls) | resource | +| [aws_s3_bucket_policy.artifacts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_policy) | resource | +| [aws_s3_bucket_public_access_block.artifacts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource | +| [aws_s3_bucket_server_side_encryption_configuration.artifacts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_server_side_encryption_configuration) | resource | +| [aws_s3_bucket_versioning.artifacts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_versioning) | resource | +| [aws_security_group.connector](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | +| [aws_vpc_security_group_egress_rule.ipv4](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_egress_rule) | resource | +| [aws_vpc_security_group_egress_rule.ipv6](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_egress_rule) | resource | +| [time_sleep.operator_role_propagation](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/sleep) | resource | +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_iam_policy_document.artifact_bucket](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.build](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.lambda_service_assume_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.network_connector_assume_operator_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.usage](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | +| [aws_subnet.selected](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/subnet) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [artifact\_bucket\_name](#input\_artifact\_bucket\_name) | 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 MicroVM build artifacts. | `number` | `30` | no | +| [aws\_region](#input\_aws\_region) | AWS region in which to create the Lambda MicroVM prerequisites. | `string` | n/a | yes | +| [build\_policy\_name\_prefix](#input\_build\_policy\_name\_prefix) | Name prefix for the Lambda MicroVM build policy. | `string` | n/a | yes | +| [build\_role\_name\_prefix](#input\_build\_role\_name\_prefix) | Name prefix for the Lambda MicroVM build role. | `string` | n/a | yes | +| [ecr\_repository\_arns](#input\_ecr\_repository\_arns) | Optional regional ECR repository ARNs from which MicroVM image builds can pull runner base images. | `set(string)` | `[]` | no | +| [force\_destroy\_artifact\_bucket](#input\_force\_destroy\_artifact\_bucket) | Whether to force destroy the S3 bucket containing Lambda MicroVM image source artifacts. | `bool` | `false` | no | +| [image\_name\_prefix](#input\_image\_name\_prefix) | IAM namespace prefix reserved for externally published Lambda MicroVM image names. This module does not create or enumerate images. | `string` | n/a | yes | +| [network\_connector\_operator\_role\_name\_prefix](#input\_network\_connector\_operator\_role\_name\_prefix) | Name prefix for the Lambda Network Connector operator role. | `string` | n/a | yes | +| [network\_connectors](#input\_network\_connectors) | Regional Lambda MicroVM Network Connectors keyed by a stable consumer-defined identity. |
map(object({
name = string
vpc_id = string
subnet_ids = 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