cdk-github-microvm-runners
A CDK construct library that runs GitHub Actions jobs on AWS Lambda MicroVMs. Each job runs on its own MicroVM in your AWS account, created when the job starts and removed once it finishes.
Why this exists
AWS Lambda MicroVMs provides the ability to launch a MicroVM, but nothing that
connects one to GitHub Actions: there is no runner registration, no path from
an incoming job to a running VM, and no cleanup when a VM or a runner is left
behind. AWS CDK provides the individual building blocks — Lambda functions, a
queue, a table, IAM roles — but not an assembled system. This construct is that
system. It receives GitHub's workflow_job events, launches a MicroVM for each
job, registers a single-use runner, and reconciles the result afterward, so
that adopting per-job MicroVM runners is a matter of configuring a construct
rather than building and operating the surrounding machinery yourself.
Two design decisions shape the rest of the library. The runner VMs hold no AWS
credentials by default, because a MicroVM's instance metadata service would
otherwise return any attached role's credentials to the code running in the
job; a job that needs AWS obtains its own short-lived credentials through GitHub
OIDC instead. Routing is per job: a workflow job opts in by naming the runner's
label in its runs-on, and every other job continues to run on GitHub-hosted
runners, so a repository can be moved across one job at a time.
How it works
A single runner set — one GithubMicrovmRunners in a stack — serves a GitHub
organization or a set of repositories. It is configured with two required
properties: how it authenticates to GitHub, and which organization or
repositories it serves.
A runner class pairs a runs-on label with the MicroVM size and image that
jobs carrying that label run on, and a runner set can define more than one.
Each class builds its own image and runs at its own size, so a workflow can
send small jobs to one class and memory-heavy jobs to a larger one by choosing
the matching label. The runner VMs are ARM64, so the tools and container images
a job pulls need arm64 builds.
flowchart TD
GH["GitHub Actions — workflow_job event"] --> WH["Webhook Lambda — verifies the signature"]
WH --> Q["SQS job queue — launch and terminate intents"]
Q --> LN["Launcher Lambda — claims the job, obtains a MicroVM"]
LN --> VM["MicroVM — runs one job"]
LN -.->|registers a single-use runner| GH
LN <--> DB[("DynamoDB runner table")]
JAN["Janitor Lambda — scheduled sweep"] -.->|reaps stranded VMs, reconciles state| VM
JAN <--> DB
When GitHub sends a workflow_job event, the webhook handler verifies its
signature and places the job on an SQS queue. The launcher reads the queue,
records a claim so that a given job is only ever launched once, starts a
MicroVM (or resumes one from a warm pool, if the runner class keeps one), and
registers a single-use runner with GitHub. The VM runs that one job and is then
removed. A janitor runs on a schedule to reconcile the runner set against the
running VMs and GitHub's view of its runners, terminating anything that was
stranded and cleaning up records that are no longer needed. The
architecture guide describes the lifecycle, the
idempotency model, and the checks that keep the janitor from terminating a
runner that is still working.
Install
One source, compiled by jsii into two packages. The API is the same in each, with names rendered in the target language's conventions.
npm install cdk-github-microvm-runners
pip install cdk-github-microvm-runners
Getting started
To deploy, you need an AWS account, a GitHub App connected to the runner set, and the App's private key and webhook secret stored in AWS Secrets Manager. The getting-started guide walks through all three, and includes a helper that performs the GitHub App setup for you.
The example below is TypeScript; API.md carries the same surface for every language.
from aws_cdk import Environment
from aws_cdk import App, CfnOutput, Stack
from aws_cdk.aws_secretsmanager import Secret
from cdk_github_microvm_runners import GithubMicrovmRunners, GithubAppId, GithubAppKey, GithubAuth, RunnerScope, MicrovmSize
app = App()
stack = Stack(app, "Runners", env=Environment(region="us-east-1"))
# The GitHub App's ID, private key, and webhook secret are read from Secrets
# Manager at run time, so this stack deploys before the App is created.
app_id = Secret.from_secret_name_v2(stack, "AppId", "microvm-runner/dev/app-id")
private_key = Secret.from_secret_name_v2(stack, "AppKey", "microvm-runner/dev/app-private-key")
webhook_secret = Secret.from_secret_name_v2(stack, "WebhookSecret", "microvm-runner/dev/webhook-secret")
runners = GithubMicrovmRunners(stack, "Runners",
github=GithubAuth.app(
app_id=GithubAppId.from_secret(app_id),
private_key=GithubAppKey.from_secret(private_key),
webhook_secret=webhook_secret
),
scope=RunnerScope.org("my-org")
)
# A runner class pairs a label with the MicroVM size it runs on. Workflows
# reach it through `runs-on: [self-hosted, microvm]`.
runners.add_runner_class("microvm", size=MicrovmSize.GB4)
CfnOutput(stack, "WebhookUrl", value=runners.webhook_url)
Once the stack is deployed, point the GitHub App's webhook at the stack's
WebhookUrl output — the setup helper reads it from the stack and wires it for
you, or you can set it by hand — and change a workflow job's runs-on to
[self-hosted, microvm].
Examples
The samples below extend the runner set above, and each shows only the part it adds.
A runner class with its own image
A runner class that names no image builds from the base image, which carries
the operating system, the Actions runner, Docker, and the AWS CLI.
RunnerImage.fromOptions describes what to add to that base: system packages,
setup commands, environment variables, and files.
runners.add_runner_class("build",
size=MicrovmSize.GB8,
image=RunnerImage.from_options(
system_packages=["jq", "ripgrep"],
setup_commands=["npm install -g pnpm@10"]
)
)
Images are built when the stack deploys, so a job that arrives later boots an image that already exists. Runner images covers the three ways to describe one, including supplying a Dockerfile of your own.
Language versions baked into the image
actions/setup-python and actions/setup-node resolve the version a job asks
for from the image's tool cache. A toolchain is a language runtime placed in
that cache when the image is built.
runners.add_runner_class("test",
size=MicrovmSize.GB4,
image=RunnerImage.from_options(
toolchains=[
RunnerToolchain.python("3.12.7"),
RunnerToolchain.node("22.11.0")
]
)
)
Each toolchain pins a full three-part version, because that version names the
cache directory it is baked into. A workflow targeting the class then writes
actions/setup-python as it would on a GitHub-hosted runner, and
Toolchains covers the rest.
Runners in your own VPC
Network egress belongs to the runner set rather than to a class, and every
runner class shares it. RunnerNetwork.vpc has the construct build a Lambda
runtime connector from a VPC you pass, so the VMs follow that VPC's subnets,
security groups, and route tables.
GithubMicrovmRunners(stack, "Runners",
github=github,
scope=RunnerScope.org("my-org"),
network=RunnerNetwork.vpc(vpc,
subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS)
)
)
The connector's network interfaces land in the VPC's private-with-egress subnets and get a new security group unless you pass your own. The security guide covers attaching a connector you already manage, and what the VPC's routing then decides for a job.
Security
The runner VMs carry no AWS identity by default, so a compromised or malicious workflow step has no credentials to read from the instance metadata service. A job that needs AWS assumes a role scoped to that job through GitHub OIDC. The security guide covers the whole model, including the OIDC trust-policy setup and the details that are easy to get wrong.
The webhook is served from a public Lambda Function URL, because GitHub cannot sign its webhook deliveries with SigV4. The handler verifies GitHub's HMAC-SHA256 signature on every request.
A single property attaches an execution role to the VMs — vmExecutionRole —
for a runner set whose jobs need a standing AWS identity. Because that role's
credentials become readable from inside the job, it is off by default, and the
construct never creates a VM identity on your behalf. Runtime console capture
(consoleLogs: ConsoleLogs.enabled()) writes its logs through that same role,
so it too requires vmExecutionRole, and you grant the role the log-write
actions yourself.
Documentation
| Guide | What it covers |
|---|---|
| Getting started | Prerequisites, GitHub App setup, deploying, and running a first job |
| Onboarding a repo | Moving an existing repository's CI onto the runners |
| Runner images | What an image contains, when it is built, how it changes, and the disk each size allows |
| Toolchains | Baking the Python and Node versions your workflows use into the image |
| Security | The security model, per-job AWS access through GitHub OIDC, and the control plane |
| Service quotas | Sizing for concurrency, and what to do when a quota cannot be raised |
| Monitoring | Opt-in CloudWatch metrics and the ready-made alarms |
| Logging | Image build logs and runtime VM console output, and the tradeoff console capture involves |
| Architecture | The lifecycle, the idempotency model, and the checks that prevent terminating an active runner |
| API reference | Every construct, property, and default, generated from the source |
| Security policy · Contributing · Changelog | Reporting and project information |
Configuration
Beyond the two required properties, every option is optional and has a
conservative default, so the example above deploys as written. The available
options include a customer-managed KMS key applied to the
DynamoDB table, the SQS queues, and the log groups (encryptionKey); a
permissions boundary applied to every role the construct creates
(permissionsBoundary); the removal policy, DynamoDB point-in-time recovery,
log retention, dead-letter retention, dead-letter redrive count, and Lambda
memory; and control over the VMs' network egress through RunnerNetwork.
Development
Quality checks run through just and are
enforced by lefthook git hooks; CI
runs the same just verify.
brew install just lefthook
pnpm install
lefthook install
just verify
Feature pull requests target dev, which is promoted to main by pull
request.
See CONTRIBUTING.md for local setup and the full workflow.
License
Released under the Apache-2.0 license.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cdk_github_microvm_runners-0.1.6.tar.gz.
File metadata
- Download URL: cdk_github_microvm_runners-0.1.6.tar.gz
- Upload date:
- Size: 1.5 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9c883f9a64501ce21a580a2a40deef4a223ff63d58b77b53d3d652bb6be1d9b7
|
|
| MD5 |
bf325550d3ed50c7337695421b13692d
|
|
| BLAKE2b-256 |
d60198d0fc03a2e8d76e111b65b0217c3c5d3d28d08269ccb7924c08a26150aa
|
Provenance
The following attestation bundles were made for cdk_github_microvm_runners-0.1.6.tar.gz:
Publisher:
release.yml on schuettc/cdk-github-microvm-runners
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cdk_github_microvm_runners-0.1.6.tar.gz -
Subject digest:
9c883f9a64501ce21a580a2a40deef4a223ff63d58b77b53d3d652bb6be1d9b7 - Sigstore transparency entry: 2342184327
- Sigstore integration time:
-
Permalink:
schuettc/cdk-github-microvm-runners@48e4c4b743f9a64afc84805fa1df84be64481e01 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/schuettc
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@48e4c4b743f9a64afc84805fa1df84be64481e01 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file cdk_github_microvm_runners-0.1.6-py3-none-any.whl.
File metadata
- Download URL: cdk_github_microvm_runners-0.1.6-py3-none-any.whl
- Upload date:
- Size: 1.5 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
660f590ded686040ec67665cbfe049e99d9786acec678f842d5814ec7ef8dbd6
|
|
| MD5 |
241f5a830196669bebe40cc500aa0e6d
|
|
| BLAKE2b-256 |
f2a47dba7eea8a1cd1ecf34b1ffa337c01fd00ccb9715f5d66d90471f6601085
|
Provenance
The following attestation bundles were made for cdk_github_microvm_runners-0.1.6-py3-none-any.whl:
Publisher:
release.yml on schuettc/cdk-github-microvm-runners
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cdk_github_microvm_runners-0.1.6-py3-none-any.whl -
Subject digest:
660f590ded686040ec67665cbfe049e99d9786acec678f842d5814ec7ef8dbd6 - Sigstore transparency entry: 2342184318
- Sigstore integration time:
-
Permalink:
schuettc/cdk-github-microvm-runners@48e4c4b743f9a64afc84805fa1df84be64481e01 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/schuettc
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@48e4c4b743f9a64afc84805fa1df84be64481e01 -
Trigger Event:
workflow_dispatch
-
Statement type: