"
+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 fadbedc0f7..2c56e7fa8f 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/modules/multi-runner/README.md b/modules/multi-runner/README.md
index b632ee52aa..635e078570 100644
--- a/modules/multi-runner/README.md
+++ b/modules/multi-runner/README.md
@@ -102,7 +102,7 @@ module "multi-runner" {
## Requirements
| Name | Version |
-| ---- | ------- |
+|------|---------|
| [terraform](#requirement\_terraform) | >= 1.4 |
| [aws](#requirement\_aws) | >= 6.33 |
| [random](#requirement\_random) | ~> 3.0 |
@@ -110,7 +110,7 @@ module "multi-runner" {
## Providers
| Name | Version |
-| ---- | ------- |
+|------|---------|
| [aws](#provider\_aws) | 6.63.0 |
| [random](#provider\_random) | 3.9.0 |
| [terraform](#provider\_terraform) | n/a |
@@ -118,7 +118,7 @@ module "multi-runner" {
## Modules
| Name | Source | Version |
-| ---- | ------ | ------- |
+|------|--------|---------|
| [ami\_housekeeper](#module\_ami\_housekeeper) | ../ami-housekeeper | n/a |
| [instance\_termination\_watcher](#module\_instance\_termination\_watcher) | ../termination-watcher | n/a |
| [runner\_binaries](#module\_runner\_binaries) | ../runner-binaries-syncer | n/a |
@@ -130,7 +130,7 @@ module "multi-runner" {
## Resources
| Name | Type |
-| ---- | ---- |
+|------|------|
| [aws_sqs_queue.queued_builds](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sqs_queue) | resource |
| [aws_sqs_queue.queued_builds_dlq](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sqs_queue) | resource |
| [aws_sqs_queue_policy.build_queue_dlq_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sqs_queue_policy) | resource |
@@ -143,7 +143,7 @@ module "multi-runner" {
## Inputs
| Name | Description | Type | Default | Required |
-| ---- | ----------- | ---- | ------- | :------: |
+|------|-------------|------|---------|:--------:|
| [additional\_github\_apps](#input\_additional\_github\_apps) | Additional GitHub Apps for random API rate limit distribution.
The primary app (var.github\_app) is always included and is the one whose
webhook secret is used for incoming webhook signature validation. Only the
primary app needs a webhook configured in GitHub.
Additional apps listed here are used exclusively by the control-plane
lambdas (scale-up, scale-down, pool, job-retry) which randomly select an
app for each GitHub API call. Each additional app must be installed on the
same repositories/organizations as the primary app. | list(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 }))
installation_id = optional(string)
installation_id_ssm = optional(object({ arn = string, name = string }))
})) | `[]` | no |
| [ami\_housekeeper\_cleanup\_config](#input\_ami\_housekeeper\_cleanup\_config) | Configuration for AMI cleanup. | object({
maxItems = optional(number)
minimumDaysOld = optional(number)
amiFilters = optional(list(object({
Name = string
Values = list(string)
})))
launchTemplateNames = optional(list(string))
ssmParameterNames = optional(list(string))
dryRun = optional(bool)
}) | `{}` | no |
| [ami\_housekeeper\_lambda\_memory\_size](#input\_ami\_housekeeper\_lambda\_memory\_size) | Memory size limit in MB of the lambda. | `number` | `256` | no |
@@ -235,7 +235,7 @@ module "multi-runner" {
## Outputs
| Name | Description |
-| ---- | ----------- |
+|------|-------------|
| [binaries\_syncer\_map](#output\_binaries\_syncer\_map) | n/a |
| [instance\_termination\_handler](#output\_instance\_termination\_handler) | n/a |
| [instance\_termination\_watcher](#output\_instance\_termination\_watcher) | n/a |
diff --git a/modules/runner-config/README.md b/modules/runner-config/README.md
index e7dfd50d63..e7ed1fb14a 100644
--- a/modules/runner-config/README.md
+++ b/modules/runner-config/README.md
@@ -119,7 +119,7 @@ yarn run dist
|------|-------------|------|---------|:--------:|
| [aws\_partition](#input\_aws\_partition) | AWS partition used to construct ARNs. | `string` | `"aws"` | no |
| [aws\_region](#input\_aws\_region) | AWS region. | `string` | n/a | yes |
-| [compute\_provider](#input\_compute\_provider) | Typed compute-provider configuration. Provider-owned settings remain inside the selected compute-provider block.
Exactly one compute-provider block must be non-null. The populated block selects the provider, and its presence must be known during planning. Values inside the selected block may remain unknown until apply.
- `aws`: AWS compute-provider configurations.
- `aws.ec2`: EC2 compute-provider configuration.
- `aws.ec2.ami`: Optional AMI discovery or external AMI-parameter configuration. Null uses the operating-system and architecture defaults.
- `aws.ec2.ami.filter`: EC2 AMI filters combined with the provider's default AMI-name filter.
- `aws.ec2.ami.owners`: AWS account IDs or aliases allowed to own the selected AMI.
- `aws.ec2.ami.id_ssm_parameter`: Optional externally managed SSM parameter containing the AMI ID. Null creates a provider-managed AMI-ID parameter. The wrapper's presence is the plan-time ownership discriminator, so keep the object literal even when its ARN comes from another resource.
- `aws.ec2.ami.id_ssm_parameter.arn`: ARN of the externally managed SSM parameter. The ARN may be unknown until apply.
- `aws.ec2.ami.kms_key`: Optional KMS key required to launch encrypted AMIs or snapshots. The wrapper's presence is the plan-time policy discriminator.
- `aws.ec2.ami.kms_key.arn`: ARN of the KMS key. The ARN may be unknown until apply.
- `aws.ec2.vpc_id`: VPC in which runner networking resources are created.
- `aws.ec2.subnet_ids`: Subnets from which scale-up may launch runner instances.
- `aws.ec2.overrides`: Optional resource-name overrides.
- `aws.ec2.overrides.name_runner`: Name tag used for runner compute resources. An empty value uses the generated provider name.
- `aws.ec2.overrides.name_sg`: Name tag used for the managed runner security group. An empty value uses the generated provider name.
- `aws.ec2.instance_profile`: Optional externally managed instance profile used by the launch template.
- `aws.ec2.instance_profile.name`: Name of the externally managed instance profile.
- `aws.ec2.instance_profile_path`: IAM path for the provider-managed instance profile. Null uses a path derived from the runner-configuration prefix.
- `aws.ec2.binaries_syncer`: Runner-distribution synchronization configuration.
- `aws.ec2.binaries_syncer.enabled`: Enables use of a synchronized runner distribution from S3.
- `aws.ec2.binaries_syncer.s3`: S3 object containing the synchronized runner distribution. Required when synchronization is enabled.
- `aws.ec2.binaries_syncer.s3.arn`: ARN of the runner-distribution bucket, used by IAM policies.
- `aws.ec2.binaries_syncer.s3.id`: Bucket name used to construct the runner-distribution S3 URI.
- `aws.ec2.binaries_syncer.s3.key`: Object key of the runner distribution.
- `aws.ec2.block_device_mappings`: EBS mappings added to the runner launch template.
- `aws.ec2.block_device_mappings[].delete_on_termination`: Deletes the volume when its runner instance terminates.
- `aws.ec2.block_device_mappings[].device_name`: Device name exposed to the runner instance.
- `aws.ec2.block_device_mappings[].encrypted`: Enables EBS encryption.
- `aws.ec2.block_device_mappings[].iops`: Provisioned IOPS for volume types that support it.
- `aws.ec2.block_device_mappings[].kms_key_id`: KMS key ID or ARN used to encrypt the volume.
- `aws.ec2.block_device_mappings[].snapshot_id`: Snapshot used to initialize the volume.
- `aws.ec2.block_device_mappings[].throughput`: Provisioned throughput for volume types that support it.
- `aws.ec2.block_device_mappings[].volume_initialization_rate`: Fixed initialization rate in MiB/s for supported snapshot-backed volumes.
- `aws.ec2.block_device_mappings[].volume_size`: Volume size in GiB.
- `aws.ec2.block_device_mappings[].volume_type`: EBS volume type.
- `aws.ec2.ebs_optimized`: Requests EBS-optimized runner instances.
- `aws.ec2.instance_target_capacity_type`: Primary capacity type, either `spot` or `on-demand`.
- `aws.ec2.instance_allocation_strategy`: EC2 Fleet allocation strategy used to select instance capacity.
- `aws.ec2.instance_type_priorities`: Optional numeric priorities keyed by instance type.
- `aws.ec2.instance_max_spot_price`: Optional maximum hourly Spot price.
- `aws.ec2.instance_types`: EC2 instance types available to the scale-up and pool functions.
- `aws.ec2.user_data`: Runner bootstrap user-data configuration.
- `aws.ec2.user_data.enabled`: Enables launch-template user data.
- `aws.ec2.user_data.template`: Optional path to a custom user-data template.
- `aws.ec2.user_data.content`: Optional complete user-data content. When set, it is used instead of rendering a template.
- `aws.ec2.user_data.pre_install`: Script content inserted before runner installation in the default template.
- `aws.ec2.user_data.post_install`: Script content inserted after runner installation in the default template.
- `aws.ec2.user_data.debug_logging_enabled`: Enables verbose user-data tracing, which can expose secrets in logs.
- `aws.ec2.ssm_enabled`: Attaches runner permissions and policies required for AWS Systems Manager access.
- `aws.ec2.create_service_linked_role_spot`: Allows scale-up to create the EC2 Spot service-linked role.
- `aws.ec2.cloudwatch_agent`: CloudWatch agent configuration for runner instances.
- `aws.ec2.cloudwatch_agent.enabled`: Installs and configures the CloudWatch agent through the default bootstrap flow.
- `aws.ec2.cloudwatch_agent.config`: Optional complete CloudWatch agent configuration. Null renders the provider default from `log_files`.
- `aws.ec2.managed_security_group_enabled`: Creates and attaches the provider-managed runner security group.
- `aws.ec2.log_files`: Optional log files collected by the CloudWatch agent. Null uses the provider defaults.
- `aws.ec2.log_files[].log_group_name`: CloudWatch log-group name, before optional prefixing.
- `aws.ec2.log_files[].prefix_log_group`: Prefixes the log-group name with the runner configuration path when true.
- `aws.ec2.log_files[].file_path`: File or glob read by the CloudWatch agent.
- `aws.ec2.log_files[].log_stream_name`: CloudWatch log-stream name template.
- `aws.ec2.log_files[].log_class`: CloudWatch log-group class for the collected file.
- `aws.ec2.key_name`: Optional EC2 key-pair name added to the launch template.
- `aws.ec2.additional_security_group_ids`: Existing security groups attached in addition to the managed security group.
- `aws.ec2.detailed_monitoring_enabled`: Enables detailed EC2 monitoring for runner instances.
- `aws.ec2.egress_rules`: Egress rules created on the managed runner security group.
- `aws.ec2.egress_rules[].cidr_blocks`: IPv4 CIDR destinations.
- `aws.ec2.egress_rules[].ipv6_cidr_blocks`: IPv6 CIDR destinations.
- `aws.ec2.egress_rules[].prefix_list_ids`: AWS prefix-list destinations.
- `aws.ec2.egress_rules[].from_port`: First destination port in the permitted range.
- `aws.ec2.egress_rules[].protocol`: IP protocol name or number. Use `-1` for all protocols.
- `aws.ec2.egress_rules[].security_groups`: Destination security-group IDs.
- `aws.ec2.egress_rules[].self`: Allows traffic to the managed security group itself when true.
- `aws.ec2.egress_rules[].to_port`: Last destination port in the permitted range.
- `aws.ec2.egress_rules[].description`: Optional rule description.
- `aws.ec2.tags`: Additional tags for runner instances, EBS volumes, network interfaces, and eligible Spot instance requests created from the launch template. They override module-level tags and the generated runner `Name`; the provider-managed `ghr:environment`, `ghr:ssm_config_path`, and `ghr:runner_name_prefix` bootstrap tags take final precedence. These tags do not apply to static provider resources such as the launch template, security group, IAM resources, SSM parameters, or log groups.
- `aws.ec2.metadata_options`: Instance Metadata Service configuration in the launch template.
- `aws.ec2.metadata_options.instance_metadata_tags`: Exposes instance tags through Instance Metadata Service when `enabled`.
- `aws.ec2.metadata_options.http_endpoint`: Enables or disables the Instance Metadata Service endpoint.
- `aws.ec2.metadata_options.http_tokens`: Controls whether IMDSv2 session tokens are optional or required.
- `aws.ec2.metadata_options.http_put_response_hop_limit`: Network hop limit for Instance Metadata Service token responses.
- `aws.ec2.credit_specification`: CPU credit mode for burstable instance types, either `standard` or `unlimited`.
- `aws.ec2.cpu_options`: CPU topology and processor-feature configuration.
- `aws.ec2.cpu_options.core_count`: Number of CPU cores exposed to the runner instance.
- `aws.ec2.cpu_options.threads_per_core`: Number of hardware threads exposed per CPU core.
- `aws.ec2.cpu_options.amd_sev_snp`: Enables or disables AMD SEV-SNP on supported instance types.
- `aws.ec2.cpu_options.nested_virtualization`: Enables or disables nested virtualization on supported instance types.
- `aws.ec2.placement`: EC2 placement configuration for runner instances.
- `aws.ec2.placement.affinity`: Host affinity setting.
- `aws.ec2.placement.availability_zone`: Availability Zone in which the instance is placed.
- `aws.ec2.placement.group_id`: Placement-group ID.
- `aws.ec2.placement.group_name`: Placement-group name.
- `aws.ec2.placement.host_id`: Dedicated Host ID.
- `aws.ec2.placement.host_resource_group_arn`: ARN of the host resource group used for placement.
- `aws.ec2.placement.spread_domain`: Spread-domain placement value.
- `aws.ec2.placement.tenancy`: Instance tenancy, such as `default`, `dedicated`, or `host`.
- `aws.ec2.placement.partition_number`: Placement-group partition number.
- `aws.ec2.license_specifications`: License Manager configurations added to the launch template.
- `aws.ec2.license_specifications[].license_configuration_arn`: ARN of a License Manager license configuration.
- `aws.ec2.associate_public_ipv4_address`: Associates a public IPv4 address with runner network interfaces.
- `aws.ec2.on_demand_failover_for_errors`: EC2 error codes that trigger an on-demand fallback after a Spot launch failure.
- `aws.ec2.scale_errors`: EC2 error codes treated as retryable scale-up failures.
- `aws.ec2.use_dedicated_host`: Enables the dedicated-host launch path, required for macOS runners. | object({
aws = optional(object({
ec2 = optional(object({
ami = optional(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)
}), null)
vpc_id = string
subnet_ids = list(string)
overrides = optional(object({
name_runner = optional(string, "")
name_sg = optional(string, "")
}), {})
instance_profile = optional(object({
name = string
}), null)
instance_profile_path = optional(string, null)
binaries_syncer = optional(object({
enabled = optional(bool, true)
s3 = optional(object({
arn = string
id = string
key = string
}), null)
}), {})
block_device_mappings = optional(list(object({
delete_on_termination = optional(bool, true)
device_name = optional(string, "/dev/xvda")
encrypted = optional(bool, true)
iops = optional(number)
kms_key_id = optional(string)
snapshot_id = optional(string)
throughput = optional(number)
volume_initialization_rate = optional(number)
volume_size = number
volume_type = optional(string, "gp3")
})), [{ volume_size = 30 }])
ebs_optimized = optional(bool, false)
instance_target_capacity_type = optional(string, "spot")
instance_allocation_strategy = optional(string, "lowest-price")
instance_type_priorities = optional(map(number), null)
instance_max_spot_price = optional(string, null)
instance_types = list(string)
user_data = optional(object({
enabled = optional(bool, true)
template = optional(string, null)
content = optional(string, null)
pre_install = optional(string, "")
post_install = optional(string, "")
debug_logging_enabled = optional(bool, false)
}), {})
ssm_enabled = optional(bool, false)
create_service_linked_role_spot = optional(bool, false)
cloudwatch_agent = optional(object({
enabled = optional(bool, true)
config = optional(string, null)
}), {})
managed_security_group_enabled = optional(bool, true)
log_files = optional(list(object({
log_group_name = string
prefix_log_group = bool
file_path = string
log_stream_name = string
log_class = optional(string, "STANDARD")
})), null)
key_name = optional(string, null)
additional_security_group_ids = optional(list(string), [])
detailed_monitoring_enabled = optional(bool, false)
egress_rules = optional(list(object({
cidr_blocks = list(string)
ipv6_cidr_blocks = list(string)
prefix_list_ids = list(string)
from_port = number
protocol = string
security_groups = list(string)
self = bool
to_port = number
description = string
})), [{
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
prefix_list_ids = null
from_port = 0
protocol = "-1"
security_groups = null
self = null
to_port = 0
description = null
}])
tags = optional(map(string), {})
metadata_options = optional(object({
instance_metadata_tags = optional(string, "enabled")
http_endpoint = optional(string, "enabled")
http_tokens = optional(string, "required")
http_put_response_hop_limit = optional(number, 1)
}), {})
credit_specification = optional(string, null)
cpu_options = optional(object({
core_count = optional(number)
threads_per_core = optional(number)
amd_sev_snp = optional(string)
nested_virtualization = optional(string)
}), null)
placement = optional(object({
affinity = optional(string)
availability_zone = optional(string)
group_id = optional(string)
group_name = optional(string)
host_id = optional(string)
host_resource_group_arn = optional(string)
spread_domain = optional(string)
tenancy = optional(string)
partition_number = optional(number)
}), null)
license_specifications = optional(list(object({
license_configuration_arn = string
})), [])
associate_public_ipv4_address = optional(bool, false)
on_demand_failover_for_errors = optional(list(string), [])
scale_errors = optional(list(string), [
"UnfulfillableCapacity",
"MaxSpotInstanceCountExceeded",
"TargetCapacityLimitExceededException",
"RequestLimitExceeded",
"ResourceLimitExceeded",
"MaxSpotInstanceCountExceeded",
"MaxSpotFleetRequestCountExceeded",
"InsufficientInstanceCapacity",
"InsufficientCapacityOnHost",
])
use_dedicated_host = optional(bool, false)
}), null)
}), {})
}) | n/a | yes |
+| [compute\_provider](#input\_compute\_provider) | Typed compute-provider configuration. Provider-owned settings remain inside the selected compute-provider block.
Exactly one compute-provider block must be non-null. The populated block selects the provider, and its presence must be known during planning. Values inside the selected block may remain unknown until apply.
- `aws`: AWS compute-provider configurations.
- `aws.ec2`: EC2 compute-provider configuration.
- `aws.ec2.ami`: Optional AMI discovery or external AMI-parameter configuration. Null uses the operating-system and architecture defaults.
- `aws.ec2.ami.filter`: EC2 AMI filters combined with the provider's default AMI-name filter.
- `aws.ec2.ami.owners`: AWS account IDs or aliases allowed to own the selected AMI.
- `aws.ec2.ami.id_ssm_parameter`: Optional externally managed SSM parameter containing the AMI ID. Null creates a provider-managed AMI-ID parameter. The wrapper's presence is the plan-time ownership discriminator, so keep the object literal even when its ARN comes from another resource.
- `aws.ec2.ami.id_ssm_parameter.arn`: ARN of the externally managed SSM parameter. The ARN may be unknown until apply.
- `aws.ec2.ami.kms_key`: Optional KMS key required to launch encrypted AMIs or snapshots. The wrapper's presence is the plan-time policy discriminator.
- `aws.ec2.ami.kms_key.arn`: ARN of the KMS key. The ARN may be unknown until apply.
- `aws.ec2.vpc_id`: VPC in which runner networking resources are created.
- `aws.ec2.subnet_ids`: Subnets from which scale-up may launch runner instances.
- `aws.ec2.overrides`: Optional resource-name overrides.
- `aws.ec2.overrides.name_runner`: Name tag used for runner compute resources. An empty value uses the generated provider name.
- `aws.ec2.overrides.name_sg`: Name tag used for the managed runner security group. An empty value uses the generated provider name.
- `aws.ec2.instance_profile`: Optional externally managed instance profile used by the launch template.
- `aws.ec2.instance_profile.name`: Name of the externally managed instance profile.
- `aws.ec2.instance_profile_path`: IAM path for the provider-managed instance profile. Null uses a path derived from the runner-configuration prefix.
- `aws.ec2.binaries_syncer`: Runner-distribution synchronization configuration.
- `aws.ec2.binaries_syncer.enabled`: Enables use of a synchronized runner distribution from S3.
- `aws.ec2.binaries_syncer.s3`: S3 object containing the synchronized runner distribution. Required when synchronization is enabled.
- `aws.ec2.binaries_syncer.s3.arn`: ARN of the runner-distribution bucket, used by IAM policies.
- `aws.ec2.binaries_syncer.s3.id`: Bucket name used to construct the runner-distribution S3 URI.
- `aws.ec2.binaries_syncer.s3.key`: Object key of the runner distribution.
- `aws.ec2.block_device_mappings`: EBS mappings added to the runner launch template.
- `aws.ec2.block_device_mappings[].delete_on_termination`: Deletes the volume when its runner instance terminates.
- `aws.ec2.block_device_mappings[].device_name`: Device name exposed to the runner instance.
- `aws.ec2.block_device_mappings[].encrypted`: Enables EBS encryption.
- `aws.ec2.block_device_mappings[].iops`: Provisioned IOPS for volume types that support it.
- `aws.ec2.block_device_mappings[].kms_key_id`: KMS key ID or ARN used to encrypt the volume.
- `aws.ec2.block_device_mappings[].snapshot_id`: Snapshot used to initialize the volume.
- `aws.ec2.block_device_mappings[].throughput`: Provisioned throughput for volume types that support it.
- `aws.ec2.block_device_mappings[].volume_initialization_rate`: Fixed initialization rate in MiB/s for supported snapshot-backed volumes.
- `aws.ec2.block_device_mappings[].volume_size`: Volume size in GiB.
- `aws.ec2.block_device_mappings[].volume_type`: EBS volume type.
- `aws.ec2.ebs_optimized`: Requests EBS-optimized runner instances.
- `aws.ec2.instance_target_capacity_type`: Primary capacity type, either `spot` or `on-demand`.
- `aws.ec2.instance_allocation_strategy`: EC2 Fleet allocation strategy used to select instance capacity.
- `aws.ec2.instance_type_priorities`: Optional numeric priorities keyed by instance type.
- `aws.ec2.instance_max_spot_price`: Optional maximum hourly Spot price.
- `aws.ec2.instance_types`: EC2 instance types available to the scale-up and pool functions.
- `aws.ec2.user_data`: Runner bootstrap user-data configuration.
- `aws.ec2.user_data.enabled`: Enables launch-template user data.
- `aws.ec2.user_data.template`: Optional path to a custom user-data template.
- `aws.ec2.user_data.content`: Optional complete user-data content. When set, it is used instead of rendering a template.
- `aws.ec2.user_data.pre_install`: Script content inserted before runner installation in the default template.
- `aws.ec2.user_data.post_install`: Script content inserted after runner installation in the default template.
- `aws.ec2.user_data.debug_logging_enabled`: Enables verbose user-data tracing, which can expose secrets in logs.
- `aws.ec2.ssm_enabled`: Attaches runner permissions and policies required for AWS Systems Manager access.
- `aws.ec2.create_service_linked_role_spot`: Allows scale-up to create the EC2 Spot service-linked role.
- `aws.ec2.cloudwatch_agent`: CloudWatch agent configuration for runner instances.
- `aws.ec2.cloudwatch_agent.enabled`: Installs and configures the CloudWatch agent through the default bootstrap flow.
- `aws.ec2.cloudwatch_agent.config`: Optional complete CloudWatch agent configuration. Null renders the provider default from `log_files`.
- `aws.ec2.managed_security_group_enabled`: Creates and attaches the provider-managed runner security group.
- `aws.ec2.log_files`: Optional log files collected by the CloudWatch agent. Null uses the provider defaults.
- `aws.ec2.log_files[].log_group_name`: CloudWatch log-group name, before optional prefixing.
- `aws.ec2.log_files[].prefix_log_group`: Prefixes the log-group name with the runner configuration path when true.
- `aws.ec2.log_files[].file_path`: File or glob read by the CloudWatch agent.
- `aws.ec2.log_files[].log_stream_name`: CloudWatch log-stream name template.
- `aws.ec2.log_files[].log_class`: CloudWatch log-group class for the collected file.
- `aws.ec2.key_name`: Optional EC2 key-pair name added to the launch template.
- `aws.ec2.additional_security_group_ids`: Existing security groups attached in addition to the managed security group.
- `aws.ec2.detailed_monitoring_enabled`: Enables detailed EC2 monitoring for runner instances.
- `aws.ec2.egress_rules`: Egress rules created on the managed runner security group.
- `aws.ec2.egress_rules[].cidr_blocks`: IPv4 CIDR destinations.
- `aws.ec2.egress_rules[].ipv6_cidr_blocks`: IPv6 CIDR destinations.
- `aws.ec2.egress_rules[].prefix_list_ids`: AWS prefix-list destinations.
- `aws.ec2.egress_rules[].from_port`: First destination port in the permitted range.
- `aws.ec2.egress_rules[].protocol`: IP protocol name or number. Use `-1` for all protocols.
- `aws.ec2.egress_rules[].security_groups`: Destination security-group IDs.
- `aws.ec2.egress_rules[].self`: Allows traffic to the managed security group itself when true.
- `aws.ec2.egress_rules[].to_port`: Last destination port in the permitted range.
- `aws.ec2.egress_rules[].description`: Optional rule description.
- `aws.ec2.tags`: Additional tags for runner instances, EBS volumes, network interfaces, and eligible Spot instance requests created from the launch template. They override module-level tags and the generated runner `Name`; the provider-managed `ghr:environment`, `ghr:ssm_config_path`, and `ghr:runner_name_prefix` bootstrap tags take final precedence. These tags do not apply to static provider resources such as the launch template, security group, IAM resources, SSM parameters, or log groups.
- `aws.ec2.metadata_options`: Instance Metadata Service configuration in the launch template.
- `aws.ec2.metadata_options.instance_metadata_tags`: Exposes instance tags through Instance Metadata Service when `enabled`.
- `aws.ec2.metadata_options.http_endpoint`: Enables or disables the Instance Metadata Service endpoint.
- `aws.ec2.metadata_options.http_tokens`: Controls whether IMDSv2 session tokens are optional or required.
- `aws.ec2.metadata_options.http_put_response_hop_limit`: Network hop limit for Instance Metadata Service token responses.
- `aws.ec2.credit_specification`: CPU credit mode for burstable instance types, either `standard` or `unlimited`.
- `aws.ec2.cpu_options`: CPU topology and processor-feature configuration.
- `aws.ec2.cpu_options.core_count`: Number of CPU cores exposed to the runner instance.
- `aws.ec2.cpu_options.threads_per_core`: Number of hardware threads exposed per CPU core.
- `aws.ec2.cpu_options.amd_sev_snp`: Enables or disables AMD SEV-SNP on supported instance types.
- `aws.ec2.cpu_options.nested_virtualization`: Enables or disables nested virtualization on supported instance types.
- `aws.ec2.placement`: EC2 placement configuration for runner instances.
- `aws.ec2.placement.affinity`: Host affinity setting.
- `aws.ec2.placement.availability_zone`: Availability Zone in which the instance is placed.
- `aws.ec2.placement.group_id`: Placement-group ID.
- `aws.ec2.placement.group_name`: Placement-group name.
- `aws.ec2.placement.host_id`: Dedicated Host ID.
- `aws.ec2.placement.host_resource_group_arn`: ARN of the host resource group used for placement.
- `aws.ec2.placement.spread_domain`: Spread-domain placement value.
- `aws.ec2.placement.tenancy`: Instance tenancy, such as `default`, `dedicated`, or `host`.
- `aws.ec2.placement.partition_number`: Placement-group partition number.
- `aws.ec2.license_specifications`: License Manager configurations added to the launch template.
- `aws.ec2.license_specifications[].license_configuration_arn`: ARN of a License Manager license configuration.
- `aws.ec2.associate_public_ipv4_address`: Associates a public IPv4 address with runner network interfaces.
- `aws.ec2.on_demand_failover_for_errors`: EC2 error codes that trigger an on-demand fallback after a Spot launch failure.
- `aws.ec2.scale_errors`: EC2 error codes treated as retryable scale-up failures.
- `aws.ec2.use_dedicated_host`: Enables the dedicated-host launch path, required for macOS runners. | object({
aws = optional(object({
ec2 = optional(object({
ami = optional(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)
}), null)
vpc_id = string
subnet_ids = list(string)
overrides = optional(object({
name_runner = optional(string, "")
name_sg = optional(string, "")
}), {})
instance_profile = optional(object({
name = string
}), null)
instance_profile_path = optional(string, null)
binaries_syncer = optional(object({
enabled = optional(bool, true)
s3 = optional(object({
arn = string
id = string
key = string
}), null)
}), {})
block_device_mappings = optional(list(object({
delete_on_termination = optional(bool, true)
device_name = optional(string, "/dev/xvda")
encrypted = optional(bool, true)
iops = optional(number)
kms_key_id = optional(string)
snapshot_id = optional(string)
throughput = optional(number)
volume_initialization_rate = optional(number)
volume_size = number
volume_type = optional(string, "gp3")
})), [{ volume_size = 30 }])
ebs_optimized = optional(bool, false)
instance_target_capacity_type = optional(string, "spot")
instance_allocation_strategy = optional(string, "lowest-price")
instance_type_priorities = optional(map(number), null)
instance_max_spot_price = optional(string, null)
instance_types = list(string)
user_data = optional(object({
enabled = optional(bool, true)
template = optional(string, null)
content = optional(string, null)
pre_install = optional(string, "")
post_install = optional(string, "")
debug_logging_enabled = optional(bool, false)
}), {})
ssm_enabled = optional(bool, false)
create_service_linked_role_spot = optional(bool, false)
cloudwatch_agent = optional(object({
enabled = optional(bool, true)
config = optional(string, null)
}), {})
managed_security_group_enabled = optional(bool, true)
log_files = optional(list(object({
log_group_name = string
prefix_log_group = bool
file_path = string
log_stream_name = string
log_class = optional(string, "STANDARD")
})), null)
key_name = optional(string, null)
additional_security_group_ids = optional(list(string), [])
detailed_monitoring_enabled = optional(bool, false)
egress_rules = optional(list(object({
cidr_blocks = list(string)
ipv6_cidr_blocks = list(string)
prefix_list_ids = list(string)
from_port = number
protocol = string
security_groups = list(string)
self = bool
to_port = number
description = string
})), [{
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
prefix_list_ids = null
from_port = 0
protocol = "-1"
security_groups = null
self = null
to_port = 0
description = null
}])
tags = optional(map(string), {})
metadata_options = optional(object({
instance_metadata_tags = optional(string, "enabled")
http_endpoint = optional(string, "enabled")
http_tokens = optional(string, "required")
http_put_response_hop_limit = optional(number, 1)
}), {})
credit_specification = optional(string, null)
cpu_options = optional(object({
core_count = optional(number)
threads_per_core = optional(number)
amd_sev_snp = optional(string)
nested_virtualization = optional(string)
}), null)
placement = optional(object({
affinity = optional(string)
availability_zone = optional(string)
group_id = optional(string)
group_name = optional(string)
host_id = optional(string)
host_resource_group_arn = optional(string)
spread_domain = optional(string)
tenancy = optional(string)
partition_number = optional(number)
}), null)
license_specifications = optional(list(object({
license_configuration_arn = string
})), [])
associate_public_ipv4_address = optional(bool, false)
on_demand_failover_for_errors = optional(list(string), [])
scale_errors = optional(list(string), [
"UnfulfillableCapacity",
"MaxSpotInstanceCountExceeded",
"TargetCapacityLimitExceededException",
"RequestLimitExceeded",
"ResourceLimitExceeded",
"MaxSpotInstanceCountExceeded",
"MaxSpotFleetRequestCountExceeded",
"InsufficientInstanceCapacity",
"InsufficientCapacityOnHost",
])
use_dedicated_host = optional(bool, false)
}), null)
microvm = optional(object({
image_arn = string
image_version = optional(string, null)
ingress_network_connectors = optional(list(string), [])
egress_network_connectors = optional(list(string), [])
cloudwatch_agent = optional(object({
enabled = optional(bool, true)
config = optional(string, null)
}), {})
log_files = optional(list(object({
log_group_name = string
prefix_log_group = bool
file_path = string
log_stream_name = string
log_class = optional(string, "STANDARD")
})), null)
environment_variables = optional(map(string), {})
iam = optional(object({
resource_arns = optional(object({
images = optional(list(string), null)
}), {})
additional_policy_json = optional(object({
scale_up = optional(string, null)
}), {})
managed_policies = optional(object({
scale_up = optional(object({
arn = string
}), null)
pool = optional(object({
arn = string
}), null)
}), {})
}), {})
}), null)
}), {})
}) | n/a | yes |
| [compute\_provider\_key](#input\_compute\_provider\_key) | Optional plan-known compute-provider dispatch key. Null discovers the key from the exactly one populated compute\_provider block. | `string` | `null` | no |
| [github](#input\_github) | GitHub API and runner-registration configuration.
- `app_parameters.key_base64`: Ordered Parameter Store references for GitHub App private keys.
- `app_parameters.id`: Ordered Parameter Store references for GitHub App IDs.
- `app_parameters.installation_id`: Ordered optional Parameter Store references for GitHub App installation IDs.
- `enterprise_server.url`: Optional GitHub Enterprise Server base URL. Null selects GitHub.com.
- `enterprise_server.ssl_verify`: Enables TLS certificate verification for GitHub Enterprise Server requests.
- `user_agent`: Optional User-Agent value added to GitHub API requests. | object({
app_parameters = object({
key_base64 = list(map(string))
id = list(map(string))
installation_id = list(object({ name = string, arn = string }))
})
enterprise_server = optional(object({
url = optional(string, null)
ssl_verify = optional(bool, true)
}), {})
user_agent = optional(string, null)
}) | n/a | yes |
| [lambda](#input\_lambda) | Common Lambda substrate independent of the selected runner orchestration provider.
- `artifact.s3.bucket`: Optional shared S3 bucket containing component-owned Lambda artifacts. An orchestration provider selects its own object key and version; the bucket alone selects no artifact.
- `runtime`: Runtime used by the control-plane Lambda functions.
- `architecture`: Instruction-set architecture used by the control-plane Lambda functions. Supported values are `arm64` and `x86_64`.
- `subnet_ids`: Subnets used for Lambda VPC configuration.
- `security_group_ids`: Security groups used for Lambda VPC configuration.
- `tags`: Shared tags applied to Lambda function resources only. These override module-level `tags`; component `tags` override this map when keys conflict.
- `principals`: Additional principals allowed to assume the control-plane Lambda roles.
- `role.path`: IAM path for module-managed Lambda execution roles. Defaults to a path derived from `prefix`.
- `role.permissions_boundary`: Permissions-boundary ARN applied to module-managed Lambda execution roles. | object({
artifact = optional(object({
s3 = optional(object({
bucket = optional(string, null)
}), {})
}), {})
runtime = optional(string, "nodejs24.x")
architecture = optional(string, "arm64")
subnet_ids = optional(list(string), [])
security_group_ids = optional(list(string), [])
tags = optional(map(string), {})
principals = optional(list(object({
type = string
identifiers = list(string)
})), [])
role = optional(object({
path = optional(string, null)
permissions_boundary = optional(string, null)
}), {})
}) | `{}` | no |