This repository is a Terraform module. A single terraform apply:
- Zips the MicroVM code artifact (
microvm/Dockerfile+microvm/entrypoint.py) to S3 and builds the MicroVM image (create-or-update, then polls to ACTIVE). - Creates the IAM roles (build, exec, dispatcher), the SSM Parameter Store SecureString (webhook secret + GitHub credential - free vs a Secrets Manager secret), and CloudWatch log groups.
- Builds and deploys the dispatcher Lambda (arm64, with
pyjwt/cryptographywheels) behind a public Lambda Function URL. - Optionally wires the GitHub
workflow_jobwebhook(s) via the github provider.
On the machine that runs terraform apply (the MicroVM image is a native Cloud Control resource; only the dispatcher Lambda zip is built with a local-exec step):
| Tool | Why |
|---|---|
| Terraform ≥ 1.9 | Cross-variable validation (installation_id required when manage_webhooks = true). |
| AWS credentials for the target account | Point both the aws and awscc providers at a region where Lambda MicroVMs are available. |
python3 + pip ≥ 22, zip, bash |
Building the dispatcher Lambda zip (arm64 wheels); it fails fast if a tool is missing. |
| A GitHub App | Created once - Terraform cannot create a GitHub App (GitHub has no API for it; only a browser manifest flow). |
The dispatcher wheels are cross-installed for python3.13/manylinux2014_aarch64, so the host's own Python version doesn't matter.
The dispatcher's only credential is a GitHub App: a machine identity (not a
user, not a PAT) you create once and install on your org/repos. The module needs
three values from it - app_id, private_key, installation_id - so make the App
before you write any Terraform.
Create it:
- GitHub → your org (or account) → Settings → Developer settings → GitHub Apps → New GitHub App.
- Name it (e.g.
microvm-runners). Homepage URL can be anything. - Webhook → uncheck "Active". The App's own webhook is not used - the runner
webhook is wired separately (see Ingress /
manage_webhooks). - Permissions:
- For per-repo runners → Repository → Administration: Read & write (this registers runners and lets Terraform create the repo webhook).
- For org-wide runners → Organization → Self-hosted runners: Read & write
(add Organization → Webhooks: Read & write if
manage_webhooks = true). - Metadata: Read is selected automatically.
- Create the App. Note the App ID on its page, then click
Generate a private key → save the downloaded
.pem. - Install App (left sidebar) onto your org / the target repos. The URL after
install ends in
…/installations/<number>- that number is the installation ID.
Which of the three values you actually need:
| Mode | Needs |
|---|---|
manage_webhooks = false (you wire the webhook by hand) |
app_id + private_key (installation_id optional - the dispatcher derives it per repo) |
manage_webhooks = true (Terraform wires the webhook) |
app_id + private_key + installation_id (the github provider's app_auth needs it) |
Keep the .pem out of git - pass it via TF_VAR_app_private_key or
private_key = file("app.pem").
Plug the three values from the App above into
app_id/installation_id/private_keybelow.
terraform {
required_version = ">= 1.9.0"
required_providers {
aws = { source = "hashicorp/aws", version = ">= 6.0, < 7.0" }
awscc = { source = "hashicorp/awscc", version = ">= 1.0" }
github = { source = "integrations/github", version = ">= 6.2" }
}
}
provider "aws" {
region = "eu-west-1"
}
provider "awscc" {
region = "eu-west-1" # same region as aws; the module builds the image via awscc
}
# Required only because manage_webhooks = true (same App identity as the module).
provider "github" {
owner = "my-org"
app_auth {
id = "123456"
installation_id = "12345678"
pem_file = file("app.private-key.pem")
}
}
module "gha_runner" {
source = "mkdev-me/github-runner-lambda-microvms/aws" # or a local path
name_prefix = "gha-microvm"
github_organization = "my-org"
github_repositories = ["my-org/api", "my-org/web"] # or [] for an org-level webhook
github_app = {
app_id = "123456"
installation_id = "12345678"
private_key = file("app.private-key.pem")
}
manage_webhooks = true
}
output "webhook_payload_url" { value = module.gha_runner.webhook_payload_url }terraform init
terraform apply # builds the image (a few minutes on first apply), deploys, wires webhooksThen run a workflow with runs-on: [self-hosted, linux, arm64, microvm]. A runnable copy lives in examples/github-app.
github_app = { app_id, installation_id, private_key } is required - see
You need a GitHub App above for how to
obtain the three values. It's the dispatcher's only credential (a machine identity
with fine-grained, ~1h auto-rotating tokens); when manage_webhooks = true the
github provider reuses the same App identity to create the webhook.
GitHub reaches the dispatcher through a public Lambda Function URL (authorization_type = NONE). The aws provider auto-adds the public invoke permission, so there's nothing else to configure. The endpoint is internet-facing; request authenticity relies entirely on the X-Hub-Signature-256 HMAC check the dispatcher runs against the webhook secret.
github_repositories = ["owner/name", ...]→ oneworkflow_jobwebhook per repo. All entries must belong to the github provider'sowner(validated).github_repositories = []+github_organization = "my-org"→ a single org-level webhook covering every repo.
If you'd rather not give Terraform GitHub access, set manage_webhooks = false. Then omit the github provider entirely and wire the webhook by hand:
terraform output webhook_payload_url
terraform output -raw webhook_secret # -raw: the value is marked sensitiveIn GitHub (repo or org Settings → Webhooks → Add webhook):
- Payload URL: the
webhook_payload_urloutput - Content type:
application/json(required - the HMAC is computed over the raw JSON bytes) - Secret: the
webhook_secretoutput - Events: Let me select individual events → Workflow jobs only
| Variable | Default | Purpose |
|---|---|---|
name_prefix |
"gha-microvm" |
Prefix for all resource names. |
github_app |
(required) | { app_id, installation_id?, private_key } (sensitive). |
github_organization |
null |
Owner of repos/webhooks; with empty github_repositories, selects an org webhook. |
github_repositories |
[] |
["owner/name", ...] for per-repo webhooks. |
manage_webhooks |
true |
Create the webhooks via the github provider. |
runner_memory_mib |
8192 |
MicroVM memory. 8192 suits Docker builds; lower to cut cost. |
max_duration_seconds |
1200 |
Hard per-job cap (auto-terminate backstop). |
additional_os_capabilities |
["ALL"] |
["ALL"] enables nested Docker; [] to tighten. |
required_labels |
["self-hosted","microvm"] |
Labels a job must carry to trigger a MicroVM. |
runner_labels |
["self-hosted","linux","arm64","microvm"] |
Labels the runner registers with. |
log_retention_days |
14 |
CloudWatch retention. |
github_api_url |
https://api.github.com |
Set for GitHub Enterprise Server. |
See variables.tf for the full set (validations, dispatcher sizing, base image name, bucket override).
| Output | Purpose |
|---|---|
webhook_payload_url |
The payload URL (already wired when manage_webhooks = true). |
webhook_secret |
HMAC secret (sensitive - use terraform output -raw). |
image_arn / image_version |
The MicroVM image identifier + active version. |
exec_role_arn |
MicroVM execution role. |
dispatcher_function_name |
Dispatcher Lambda name. |
secret_param_name / secret_param_arn |
SSM SecureString parameter (name / ARN). |
artifacts_bucket |
S3 artifacts bucket name. |
- First apply takes a few minutes - it waits for the MicroVM image build to reach ACTIVE before the dispatcher is configured with the version.
- Updating runner tooling (the
microvm/Dockerfileorentrypoint.py) changes the artifact hash, which triggers a new image-version build on the next apply; the dispatcher picks up the new version automatically. - The Function URL endpoint is public; its only protection is the
X-Hub-Signature-256HMAC check against the webhook secret.