diff --git a/.github/workflows/ministack.yml b/.github/workflows/ministack.yml
index 1bfd47c7e5..1352a212b6 100644
--- a/.github/workflows/ministack.yml
+++ b/.github/workflows/ministack.yml
@@ -53,6 +53,7 @@ jobs:
- ephemeral
- multi-runner
- multi-runner-v2
+ - microvm
- termination-watcher
terraform:
- "1.4.0"
diff --git a/.github/workflows/packer-build.yml b/.github/workflows/packer-build.yml
index 8dcff4efb6..726d70e9d3 100644
--- a/.github/workflows/packer-build.yml
+++ b/.github/workflows/packer-build.yml
@@ -28,7 +28,7 @@ jobs:
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"]
+ image: ["linux-al2023", "windows-core-2019", "windows-core-2022", "ubuntu-focal", "ubuntu-jammy", "ubuntu-jammy-arm64", "microvm-ubuntu"]
defaults:
run:
working-directory: images/${{ matrix.image }}
diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml
index f89bdfe742..e046edb9d7 100644
--- a/.github/workflows/terraform.yml
+++ b/.github/workflows/terraform.yml
@@ -160,7 +160,8 @@ jobs:
"multi-runner",
"multi-runner-v2",
"external-managed-ssm-secrets",
- "microvm-foundation"
+ "microvm-foundation",
+ "microvm"
]
defaults:
run:
diff --git a/docs/adr/002-runner-orchestration-provider-boundary.md b/docs/adr/002-runner-orchestration-provider-boundary.md
index 06dc16a875..3d77e2f3cd 100644
--- a/docs/adr/002-runner-orchestration-provider-boundary.md
+++ b/docs/adr/002-runner-orchestration-provider-boundary.md
@@ -229,36 +229,6 @@ not below `modules/runner-config`. This keeps the common composition module
small and prevents provider-owned resources from becoming part of the common
contract.
-### `runner-config` is the provider-neutral composition boundary
-
-`modules/runner-config` is an internal composition module selected by
-`multi-runner`; it is not a standalone public entry point. It receives one
-resolved runner configuration and owns the common runner identity, IAM role,
-runner bootstrap parameters, SSM housekeeper composition, and the capability
-connections between the selected providers.
-
-`runner-config` dispatches exactly one typed orchestration provider and one
-typed compute provider. Provider selection is made from the plan-known typed
-wrappers, not from a string discriminator or runtime fallback. The selected
-provider receives the resolved common runner settings and returns only the
-provider-specific resources, environment variables, IAM fragments, and
-outputs required by the orchestration provider.
-
-Webhook queues, Lambda functions, schedules, and retry behavior remain owned
-by the webhook orchestration provider. EC2 instances, Lambda MicroVM capacity,
-image publication, and provider-specific bootstrap behavior remain owned by
-their compute providers. `runner-config` connects these capabilities but does
-not absorb either provider's implementation.
-
-For Lambda MicroVM runners, the image is an immutable runtime artifact. The
-runner configuration and its sensitive, short-lived bootstrap value are
-published through the runner-config SSM contract and retrieved when the
-MicroVM starts. Tenant-specific runner configuration, registration tokens, and
-JIT payloads must not be baked into the image or its Terraform configuration.
-The image therefore supplies the runner and lifecycle-hook runtime, while the
-selected compute provider supplies the lane-specific SSM path and execution
-permissions.
-
```mermaid
flowchart TD
Multi["multi-runner: translate and resolve"] --> Config["runner-config: compose one runner config"]
diff --git a/docs/examples/microvm.md b/docs/examples/microvm.md
new file mode 100644
index 0000000000..4014781114
--- /dev/null
+++ b/docs/examples/microvm.md
@@ -0,0 +1,3 @@
+# Lambda MicroVM
+
+--8<-- "examples/microvm/README.md"
diff --git a/examples/microvm-foundation/README.md b/examples/microvm-foundation/README.md
index bdc531bc12..4ceb112d29 100644
--- a/examples/microvm-foundation/README.md
+++ b/examples/microvm-foundation/README.md
@@ -14,7 +14,7 @@ terraform output
```
Apply this foundation before building an image with the direct Packer commands
-documented in `../../images/microvm/README.md`. Use the outputs as the build inputs:
+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`
diff --git a/examples/microvm/.terraform.lock.hcl b/examples/microvm/.terraform.lock.hcl
new file mode 100644
index 0000000000..e46d40b514
--- /dev/null
+++ b/examples/microvm/.terraform.lock.hcl
@@ -0,0 +1,71 @@
+# This file is maintained automatically by "terraform init".
+# Manual edits may be lost in future updates.
+
+provider "registry.terraform.io/hashicorp/aws" {
+ version = "6.63.0"
+ constraints = ">= 5.0.0, >= 6.21.0, >= 6.33.0"
+ hashes = [
+ "h1:9cre7jh1lSs/9igpgAcENMUAUlYW3HCtkav3up4oit0=",
+ "h1:dRlYHkc+r6fgzF57WC7Zjcmb6sF/6TTGDEgwGK+LAZY=",
+ "zh:005d56736afd17d963998c405cee6f434dbc23a415109f9435ff1542879ae611",
+ "zh:026ef126321a86ad7080b5d858e2527f96f5289678cbcd8856296e229c43339d",
+ "zh:06e0b58b2d1eddb5137fc86bee7ad2d07953c0bc3f57cccfc5ae0d2456068a3a",
+ "zh:07221735d61ababed84734e5ffcfc5bd59d01f29f029166ba5f2175895dceed1",
+ "zh:1a72db00583112bdb8c19b213a78a3f5de754fffc08f07e061f4e326289fab7d",
+ "zh:32968e74a53b03e97a084dc7050c22ef661fb5b3ea8a44f5a63e47bc45ad0e7c",
+ "zh:4b357dfe4b820e3e4acd2881cff8288b2186491e63416751f0d12692ba478ceb",
+ "zh:81e30884d7de686265e7d87bb92527e802878c65a378470ede2a1e9f4e40ccc9",
+ "zh:82e137297f6a5a08b9ce2138f7aabea245ad99495d9d9eff502f752d6ca90dbd",
+ "zh:8eb83b67099f0ea9df238a979dff933ff50ce06a2e3ff05a48556a10f10dd204",
+ "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
+ "zh:d0ba30886cbe41850fee689f51ef9088578f323cfd21817bb409951d43c465eb",
+ "zh:dd48e7089784454bc03d713e9057f5ca0ea1613bd402125054a51894957b7925",
+ "zh:f250fa81e54cf60fcb0e9c0fc4ac043f1ecc2ac24967f628b3609364fcab3d04",
+ "zh:f38fc09fc25a8d2cf89a4d4cd6a5ef7cb1aad72798dbdcad58b8876b6a551a54",
+ "zh:f7c7380fdf126e1901f2084588dbfd724c76cb131ccfa795a541219111103c06",
+ ]
+}
+
+provider "registry.terraform.io/hashicorp/null" {
+ version = "3.3.1"
+ constraints = "~> 3.0, ~> 3.2"
+ hashes = [
+ "h1:TuxJq10DVnRP7c5HBZPyyvQGcckNVfijyU1eXEu5e4M=",
+ "h1:m5FqidbIgh+E9OigiZh8/xbkvpUQFSj3hZo/jqNLCLQ=",
+ "zh:08c59776542ea16e5a8545752787b17ff412922182b4cfabe16139197be8ac44",
+ "zh:123109cc7e5ed6d515787fbc212f2a3fd5e75647bb24ab7c801ccd4d4ed42451",
+ "zh:14b3fa4372754b54844b41d5dbd4671a292d8d6828b90169061feb4d7b15dd05",
+ "zh:56a4daaa3212f57b764bf3d1f333141c6610c5f21abb240e0111221f7c7fa4d4",
+ "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
+ "zh:7e888a026dbacd2474a42264227ae35f639780f0f0c613529d10a95cd61988b3",
+ "zh:85a53646267e87d600df7124e4767ffde9bba3b6356d45d961618bdd68131cc7",
+ "zh:8ffa0e9c7c39b2ab0905b472465d6e35ef0b776b3f6273bb34c150340b61bff1",
+ "zh:9846510a1841530d4403f4818e233f91e3b3bade7441047599fbf800742f65be",
+ "zh:afa98d44860875f037c6def0a7e6ff208e042712ba771f620482b143cd336891",
+ "zh:bdca130d9ef27488ae0b13bc8fd8019e8bbdd4f2ceff29da066bd333165d68c5",
+ "zh:cb3b94cbca88210dd0d1f11e2b8a89333f48c3857faf8f70f589072ce7c28610",
+ "zh:f0c0ba87925fe32f84b80f7513b1efb1b0866f51f899ba825e95ad59ff09b018",
+ ]
+}
+
+provider "registry.terraform.io/hashicorp/random" {
+ version = "3.9.0"
+ constraints = "~> 3.0"
+ hashes = [
+ "h1:OO+IuvQJSPmWdN8AyyIEvPJbLvDQpgX/zbktoa9KsJE=",
+ "h1:UlBuNVuCGJ39tTv2c5gz2NRZnQbXfbIWbTzWcth5o74=",
+ "zh:161ad0bd9a75768c82f53fb6e7172a9d8be2d4889b012645a34795031aaf1bf1",
+ "zh:19dc9a5b17729725ccfc4f45b0500af0ee5bc6b6b160c7adb8f2bf617d2c80ea",
+ "zh:269eda8fe42daa7974d5a34d166c3ba9defe80cde86c01e4dadcfdf2e1f05e5f",
+ "zh:373f7c65566f8f2cc7f45d698654feb9d988996957e1266a69ca00c52d6d16d0",
+ "zh:5599d16804c41c83009ec621b6d6b6f74e102f5827678a4750f8809055546b61",
+ "zh:583be0440469a22bff70dcfa56593b01566860b29607437264adb51060cf46fc",
+ "zh:5f211d8ec3f2e1f414870d9584bfe26e6995560ef81c748f8447a48164767398",
+ "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
+ "zh:7b547fd16216761ef86efc3ed516ac5ac0c5c42b7c7eb24a08cef2d93f69ed5e",
+ "zh:7e7c0679daf2a382151d05068c8c3f0dae6b7b7dccf818827b73dd08638df2ef",
+ "zh:8089dec888a8038b9b4fb23b3df7e1057293dbc5b60b42cc47ff690d69d4b61b",
+ "zh:c51f15a031edfd6f23ce8ced3446ca7f8d8d647e2499890d7d5d10d5016d7257",
+ "zh:c94784f005708890dc6895afd53636ec00ec1e430b15d41e5aebfb1d4b39bd04",
+ ]
+}
diff --git a/examples/microvm/README.md b/examples/microvm/README.md
new file mode 100644
index 0000000000..514c790110
--- /dev/null
+++ b/examples/microvm/README.md
@@ -0,0 +1,102 @@
+# Lambda MicroVM runner example
+
+This example creates the VPC and GitHub Actions runner control plane for one
+Linux ARM64 Lambda MicroVM lane. The lane uses ephemeral runners and
+just-in-time configuration, which are required by the MicroVM provider.
+
+The regional MicroVM foundation is provisioned separately by the
+[`microvm-foundation`](../microvm-foundation) example. Apply that example
+first and provide its artifact bucket, build role, and egress Network Connector
+outputs to the image build script. The image ARN produced by that build is then
+supplied to this example.
+
+The GitHub App credentials must already exist in SSM Parameter Store. The
+example outputs the webhook endpoint; configure that endpoint on the GitHub
+App with the same secret stored in the referenced SSM parameter.
+
+## Usage
+
+Build or download the Lambda archives into an S3 bucket, then create a
+`terraform.tfvars` file. The parameter references below are examples only:
+
+```hcl
+aws_region = "eu-west-1"
+lambda_artifact_bucket = "my-runner-lambda-artifacts"
+microvm_image_arn = "arn:aws:lambda:eu-west-1:123456789012:microvm-image:github-runner-arm64"
+egress_network_connector_arn = "arn:aws:lambda:eu-west-1:123456789012:network-connector:example"
+
+github_app = {
+ key_base64_ssm = {
+ arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-key"
+ name = "/github-runner/app-key"
+ }
+ id_ssm = {
+ arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-id"
+ name = "/github-runner/app-id"
+ }
+ webhook_secret_ssm = {
+ arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/webhook-secret"
+ name = "/github-runner/webhook-secret"
+ }
+}
+```
+
+Run Terraform from this directory:
+
+```bash
+terraform init
+terraform apply
+terraform output -raw webhook_endpoint
+```
+
+The MicroVM image must be built for Linux ARM64 and should use a versioned image
+ARN in production. Network connector egress remains bounded by the VPC route
+tables and network ACLs configured by the helper module.
+
+
+## Requirements
+
+| Name | Version |
+|------|---------|
+| [terraform](#requirement\_terraform) | >= 1.3.0 |
+| [aws](#requirement\_aws) | >= 6.33 |
+
+## Providers
+
+No providers.
+
+## Modules
+
+| Name | Source | Version |
+|------|--------|---------|
+| [base](#module\_base) | ../base | n/a |
+| [runners](#module\_runners) | ../../modules/multi-runner | n/a |
+
+## Resources
+
+No resources.
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| [aws\_region](#input\_aws\_region) | AWS Region where the runner control plane and MicroVM resources are deployed. | `string` | `"eu-west-1"` | no |
+| [egress\_network\_connector\_arn](#input\_egress\_network\_connector\_arn) | Regional Lambda Network Connector ARN used by MicroVMs and the image build. | `string` | n/a | yes |
+| [environment](#input\_environment) | Name prefix for the example resources. | `string` | `null` | no |
+| [github\_app](#input\_github\_app) | Pre-created SSM parameter references for the GitHub App credentials. |
object({
key_base64 = optional(string)
key_base64_ssm = optional(object({
arn = string
name = string
}))
id = optional(string)
id_ssm = optional(object({
arn = string
name = string
}))
webhook_secret = optional(string)
webhook_secret_ssm = optional(object({
arn = string
name = string
}))
}) | n/a | yes |
+| [ingress\_network\_connector\_arns](#input\_ingress\_network\_connector\_arns) | Optional regional Lambda Network Connector ARNs exposed to MicroVMs. | `list(string)` | `[]` | no |
+| [lambda\_artifact\_bucket](#input\_lambda\_artifact\_bucket) | S3 bucket containing the runner-control Lambda artifacts. | `string` | n/a | yes |
+| [microvm\_image\_arn](#input\_microvm\_image\_arn) | Lambda MicroVM image ARN produced by the MicroVM image build. | `string` | n/a | yes |
+| [microvm\_image\_version](#input\_microvm\_image\_version) | Optional immutable version of the Lambda MicroVM image. | `string` | `null` | no |
+| [organization\_runners](#input\_organization\_runners) | Register the MicroVM runners at organization scope when true. | `bool` | `false` | no |
+| [runners\_lambda\_s3\_key](#input\_runners\_lambda\_s3\_key) | S3 key for the runners Lambda archive. | `string` | `"runners.zip"` | no |
+| [runners\_maximum\_count](#input\_runners\_maximum\_count) | Maximum number of concurrent MicroVM runners. | `number` | `10` | no |
+| [webhook\_lambda\_s3\_key](#input\_webhook\_lambda\_s3\_key) | S3 key for the webhook Lambda archive. | `string` | `"webhook.zip"` | no |
+
+## Outputs
+
+| Name | Description |
+|------|-------------|
+| [microvm\_image\_arn](#output\_microvm\_image\_arn) | The MicroVM image ARN consumed by this runner configuration. |
+| [webhook\_endpoint](#output\_webhook\_endpoint) | Webhook endpoint to configure on the GitHub App. |
+
diff --git a/examples/microvm/main.tf b/examples/microvm/main.tf
new file mode 100644
index 0000000000..105c278b96
--- /dev/null
+++ b/examples/microvm/main.tf
@@ -0,0 +1,104 @@
+locals {
+ environment = coalesce(var.environment, "microvm")
+ aws_region = var.aws_region
+}
+
+module "base" {
+ source = "../base"
+
+ prefix = local.environment
+ aws_region = local.aws_region
+}
+
+module "runners" {
+ source = "../../modules/multi-runner"
+
+ aws_region = local.aws_region
+ vpc_id = module.base.vpc.vpc_id
+ subnet_ids = module.base.vpc.private_subnets
+ prefix = local.environment
+
+ # Keep GitHub App credentials in pre-created SSM parameters. This example
+ # therefore does not place the private key or webhook secret in Terraform
+ # configuration or state.
+ global_config_github = {
+ app = var.github_app
+ }
+
+ global_config_lambda = {
+ artifact = {
+ s3 = {
+ bucket = var.lambda_artifact_bucket
+ }
+ }
+ }
+
+ global_config_orchestration_provider = {
+ webhook = {
+ runner = {
+ ephemeral = true
+ jit_config_enabled = true
+ maximum_count = var.runners_maximum_count
+ boot_time_in_minutes = 5
+ }
+ github = {
+ organization_runners = var.organization_runners
+ }
+ lambda = {
+ artifact = {
+ s3 = {
+ key = var.runners_lambda_s3_key
+ }
+ }
+ webhook = {
+ artifact = {
+ s3 = {
+ key = var.webhook_lambda_s3_key
+ }
+ }
+ }
+ }
+ }
+ }
+
+ global_config_ssm = {
+ paths = {
+ root = "/github-action-runners/${local.environment}"
+ }
+ }
+
+ global_config_compute_provider = {
+ aws = {
+ microvm = {
+ image_arn = var.microvm_image_arn
+ image_version = var.microvm_image_version
+ ingress_network_connectors = var.ingress_network_connector_arns
+ egress_network_connectors = [var.egress_network_connector_arn]
+ }
+ }
+ }
+
+ multi_runner_config = {
+ microvm = {
+ runner = {
+ os = "linux"
+ architecture = "arm64"
+ name_prefix = "microvm-"
+ extra_labels = ["microvm"]
+ }
+ orchestration_provider = {
+ webhook = {
+ matcherConfig = {
+ labelMatchers = [["self-hosted", "linux", "arm64", "microvm"]]
+ bidirectionalLabelMatch = true
+ }
+ }
+ }
+ compute_provider = {
+ aws = {
+ microvm = {}
+ }
+ }
+ }
+ }
+}
diff --git a/examples/microvm/outputs.tf b/examples/microvm/outputs.tf
new file mode 100644
index 0000000000..87ad4c924c
--- /dev/null
+++ b/examples/microvm/outputs.tf
@@ -0,0 +1,9 @@
+output "webhook_endpoint" {
+ description = "Webhook endpoint to configure on the GitHub App."
+ value = module.runners.webhook.endpoint
+}
+
+output "microvm_image_arn" {
+ description = "The MicroVM image ARN consumed by this runner configuration."
+ value = var.microvm_image_arn
+}
diff --git a/examples/microvm/providers.tf b/examples/microvm/providers.tf
new file mode 100644
index 0000000000..eca2fe96a7
--- /dev/null
+++ b/examples/microvm/providers.tf
@@ -0,0 +1,9 @@
+provider "aws" {
+ region = local.aws_region
+
+ default_tags {
+ tags = {
+ Example = local.environment
+ }
+ }
+}
diff --git a/examples/microvm/variables.tf b/examples/microvm/variables.tf
new file mode 100644
index 0000000000..7a6f1abd61
--- /dev/null
+++ b/examples/microvm/variables.tf
@@ -0,0 +1,95 @@
+variable "aws_region" {
+ description = "AWS Region where the runner control plane and MicroVM resources are deployed."
+ type = string
+ default = "eu-west-1"
+}
+
+variable "environment" {
+ description = "Name prefix for the example resources."
+ type = string
+ default = null
+}
+
+variable "github_app" {
+ description = "Pre-created SSM parameter references for the GitHub App credentials."
+ type = object({
+ key_base64 = optional(string)
+ key_base64_ssm = optional(object({
+ arn = string
+ name = string
+ }))
+ id = optional(string)
+ id_ssm = optional(object({
+ arn = string
+ name = string
+ }))
+ webhook_secret = optional(string)
+ webhook_secret_ssm = optional(object({
+ arn = string
+ name = string
+ }))
+ })
+
+ validation {
+ condition = (
+ var.github_app.key_base64 == null &&
+ var.github_app.id == null &&
+ var.github_app.webhook_secret == null &&
+ var.github_app.key_base64_ssm != null &&
+ var.github_app.id_ssm != null &&
+ var.github_app.webhook_secret_ssm != null
+ )
+ error_message = "github_app must use pre-created SSM parameters for the key, app ID, and webhook secret."
+ }
+}
+
+variable "lambda_artifact_bucket" {
+ description = "S3 bucket containing the runner-control Lambda artifacts."
+ type = string
+}
+
+variable "runners_lambda_s3_key" {
+ description = "S3 key for the runners Lambda archive."
+ type = string
+ default = "runners.zip"
+}
+
+variable "webhook_lambda_s3_key" {
+ description = "S3 key for the webhook Lambda archive."
+ type = string
+ default = "webhook.zip"
+}
+
+variable "microvm_image_arn" {
+ description = "Lambda MicroVM image ARN produced by the MicroVM image build."
+ type = string
+}
+
+variable "microvm_image_version" {
+ description = "Optional immutable version of the Lambda MicroVM image."
+ type = string
+ default = null
+}
+
+variable "egress_network_connector_arn" {
+ description = "Regional Lambda Network Connector ARN used by MicroVMs and the image build."
+ type = string
+}
+
+variable "ingress_network_connector_arns" {
+ description = "Optional regional Lambda Network Connector ARNs exposed to MicroVMs."
+ type = list(string)
+ default = []
+}
+
+variable "organization_runners" {
+ description = "Register the MicroVM runners at organization scope when true."
+ type = bool
+ default = false
+}
+
+variable "runners_maximum_count" {
+ description = "Maximum number of concurrent MicroVM runners."
+ type = number
+ default = 10
+}
diff --git a/examples/microvm/versions.tf b/examples/microvm/versions.tf
new file mode 100644
index 0000000000..e4d4e1e015
--- /dev/null
+++ b/examples/microvm/versions.tf
@@ -0,0 +1,9 @@
+terraform {
+ required_providers {
+ aws = {
+ source = "hashicorp/aws"
+ version = ">= 6.33"
+ }
+ }
+ required_version = ">= 1.3.0"
+}
diff --git a/images/README.md b/images/README.md
index 689f3e2df5..c6722c2c2f 100644
--- a/images/README.md
+++ b/images/README.md
@@ -39,3 +39,16 @@ ami_owners = [""]
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..84feeb24b4
--- /dev/null
+++ b/images/microvm-ubuntu/README.md
@@ -0,0 +1,46 @@
+# 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. Install Packer and set the required AWS, S3, IAM, connector, and
+ lifecycle-hook 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 AWS_DATA_PATH=""
+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. 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..f8d0638d94
--- /dev/null
+++ b/images/microvm-ubuntu/github_agent.microvm.ubuntu.pkr.hcl
@@ -0,0 +1,118 @@
+# 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_data_path" {
+ description = "Botocore data path containing the Lambda MicroVM service model."
+ type = string
+ default = env("AWS_DATA_PATH")
+}
+
+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_DATA_PATH=${var.aws_data_path}",
+ "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 100644
index 0000000000..81c13beb2e
--- /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(
+ 'AWS_DATA_PATH 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..c60fc1316e
--- /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
+
+# 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 lifecycle-hook.zip /tmp/lifecycle-hook.zip
+RUN unzip -q /tmp/lifecycle-hook.zip -d /export/opt/microvm \
+ && test -r /export/opt/microvm/server.js \
+ && rm -f /tmp/lifecycle-hook.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/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
+
+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/modules/microvm-foundation/README.md b/modules/microvm-foundation/README.md
index 77b620c107..15747640f8 100644
--- a/modules/microvm-foundation/README.md
+++ b/modules/microvm-foundation/README.md
@@ -51,7 +51,7 @@ module "microvm_foundation" {
The companion `examples/microvm-foundation` directory is a complete setup
example. Apply it before following the direct Packer build instructions in
-`images/microvm/README.md` or using the `examples/microvm` runner example.
+`images/microvm-ubuntu/README.md` or using the `examples/microvm` runner example.
## Requirements
diff --git a/tests/ministack/README.md b/tests/ministack/README.md
index eb35b954a7..455f85bab1 100644
--- a/tests/ministack/README.md
+++ b/tests/ministack/README.md
@@ -1,7 +1,8 @@
# MiniStack example tests
The MiniStack workflow runs the `base`, `prebuilt`, `default`, `ephemeral`,
-`multi-runner`, `multi-runner-v2`, `microvm-foundation`, and `termination-watcher` examples directly
+`multi-runner`, `multi-runner-v2`, `microvm-foundation`, `microvm`, and
+`termination-watcher` examples directly
with Terraform 1.4.0 and the latest Terraform release.
The examples with input variables get their inputs from their own tfvars files
in this directory. The `termination-watcher` example has no input variables
@@ -12,6 +13,9 @@ Connector API. No override files or setup module are checked in. The helper
creates and removes a temporary AMI override for `default` and
`ephemeral`, temporary SSM parameters for `multi-runner`, and temporary AMI
fixtures plus an override for `multi-runner-v2`.
+The `microvm` lane seeds test-only GitHub App SSM parameters and Lambda ZIP
+objects in a temporary S3 bucket, and uses synthetic MicroVM image and network
+connector ARNs. It does not create a real MicroVM image or network connector.
Start MiniStack, set the AWS endpoint and test credentials, then run:
@@ -30,6 +34,8 @@ tests/ministack/run-example.sh apply multi-runner-v2
# or
tests/ministack/run-example.sh apply microvm-foundation
# or
+tests/ministack/run-example.sh apply microvm
+# or
tests/ministack/run-example.sh apply termination-watcher
```
diff --git a/tests/ministack/microvm.tfvars b/tests/ministack/microvm.tfvars
new file mode 100644
index 0000000000..b83956b548
--- /dev/null
+++ b/tests/ministack/microvm.tfvars
@@ -0,0 +1,21 @@
+aws_region = "eu-west-1"
+environment = "microvm-ministack"
+
+github_app = {
+ key_base64_ssm = {
+ name = "/ministack/microvm/github-app-key"
+ arn = "arn:aws:ssm:eu-west-1:000000000000:parameter/ministack/microvm/github-app-key"
+ }
+ id_ssm = {
+ name = "/ministack/microvm/github-app-id"
+ arn = "arn:aws:ssm:eu-west-1:000000000000:parameter/ministack/microvm/github-app-id"
+ }
+ webhook_secret_ssm = {
+ name = "/ministack/microvm/webhook-secret"
+ arn = "arn:aws:ssm:eu-west-1:000000000000:parameter/ministack/microvm/webhook-secret"
+ }
+}
+
+lambda_artifact_bucket = "github-actions-runner-microvm-ministack"
+microvm_image_arn = "arn:aws:lambda:eu-west-1:000000000000:microvm-image:ministack"
+egress_network_connector_arn = "arn:aws:lambda:eu-west-1:000000000000:network-connector:ministack"
diff --git a/tests/ministack/run-example.sh b/tests/ministack/run-example.sh
index 8961f71db9..1e0d0ac605 100755
--- a/tests/ministack/run-example.sh
+++ b/tests/ministack/run-example.sh
@@ -15,14 +15,14 @@ tfvars_file="${3:-${MINISTACK_TFVARS_FILE:-}}"
microvm_foundation_default_tfvars=false
case "$example" in
- base | prebuilt | default | ephemeral | multi-runner | multi-runner-v2 | microvm-foundation)
+ base | prebuilt | default | ephemeral | multi-runner | multi-runner-v2 | microvm-foundation | microvm)
use_tfvars=true
;;
termination-watcher)
use_tfvars=false
;;
*)
- echo "Supported examples for the runner are: base, prebuilt, default, ephemeral, multi-runner, multi-runner-v2, microvm-foundation, termination-watcher" >&2
+ echo "Supported examples for the runner are: base, prebuilt, default, ephemeral, multi-runner, multi-runner-v2, microvm-foundation, microvm, termination-watcher" >&2
exit 64
;;
esac
@@ -30,7 +30,7 @@ esac
case "$action" in
init | plan | apply | destroy) ;;
*)
- echo "Usage: $0 {init|plan|apply|destroy} {base|prebuilt|default|ephemeral|multi-runner|multi-runner-v2|microvm-foundation|termination-watcher} [TFVARS_FILE]" >&2
+ echo "Usage: $0 {init|plan|apply|destroy} {base|prebuilt|default|ephemeral|multi-runner|multi-runner-v2|microvm-foundation|microvm|termination-watcher} [TFVARS_FILE]" >&2
exit 64
;;
esac
@@ -64,6 +64,7 @@ lambda_fixture_dir=""
lambda_created_paths=""
ami_created_ids=""
ssm_created_names=""
+s3_created_buckets=""
override_created_paths=""
tfvars_created_paths=""
lambda_zip_paths="
@@ -87,6 +88,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
@@ -202,6 +209,25 @@ $fixture_tfvars"
tfvars_file="$fixture_tfvars"
}
+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' \
@@ -412,6 +438,25 @@ $lambda_zip"
create_ami_fixture "ministack-v2-linux-x64" x86_64 >/dev/null
create_ami_fixture "ministack-v2-windows-x64" x86_64 >/dev/null
;;
+ microvm)
+ create_ssm_fixture \
+ "/ministack/microvm/github-app-key" \
+ "test-only"
+ create_ssm_fixture \
+ "/ministack/microvm/github-app-id" \
+ "123456"
+ create_ssm_fixture \
+ "/ministack/microvm/webhook-secret" \
+ "test-only"
+ create_s3_fixture \
+ "github-actions-runner-microvm-ministack" \
+ "runners.zip" \
+ "$lambda_fixture_dir/ministack-lambda.zip"
+ create_s3_fixture \
+ "github-actions-runner-microvm-ministack" \
+ "webhook.zip" \
+ "$lambda_fixture_dir/ministack-lambda.zip"
+ ;;
esac
}