I tried running GitHub Actions self-hosted runners on Lambda MicroVMs

I tried running GitHub Actions self-hosted runners on Lambda MicroVMs

I tried building GitHub Actions self-hosted runners on Lambda MicroVMs using cdk-github-microvm-runners published on Construct Hub. Since they start up quickly and scale out easily, it seems like quite a good choice as an execution platform for self-hosted runners. If you are struggling with GitHub Actions processing time or costs, please use this as a reference.
2026.08.27

This page has been translated by machine translation. View original

This is Iwata from the Retail App Co-Creation Department @ Osaka.

I read the following article that was published the other day and wanted to try building a GitHub Actions self-hosted runner on Lambda MicroVMs.

https://dev.classmethod.jp/articles/self-hosted-runner-with-mbp/

My thinking was that using Lambda MicroVMs might resolve the issues with startup time and scale-out speed that are cited as disadvantages of the architecture that launches Runners on ECS each time.

So I was about to set up a verification environment right away, when...

I found that a prior contribution was already published on Construct Hub. This time, I'll use this Construct to build a self-hosted runner on Lambda MicroVMs and verify it.

Environment

The environment used this time is as follows.

  • cdk-github-microvm-runners: 0.1.7
  • aws-cdk-lib: 2.266.0

Architecture Overview

The details are explained in the cdk-github-microvm-runners GitHub repository.

https://github.com/schuettc/cdk-github-microvm-runners/blob/main/docs/architecture.md

https://github.com/schuettc/cdk-github-microvm-runners/blob/2aa10fffea58b20c88166f2afefb032cdbef39c6/docs/architecture.md?plain=1#L17-L28

The rough flow is as follows.

  • When a GitHub Actions workflow is triggered, it calls Lambda via WebHook
  • Lambda validates the caller, and if there are no issues, puts a message into SQS
  • Lambda is triggered from SQS, Lambda starts a MicroVM and registers a self-hosted runner with GitHub as a Just-in-time runner
  • The self-hosted runner executes the workflow

Information about the started MicroVM is managed in DynamoDB, which is said to control against duplicate job executions.

Let's Try It

Let's go ahead and build the self-hosted runner on Lambda MicroVMs environment.

Deploy all necessary resources with CDK

First, run npx cdk init --language typescript to prepare the CDK code, then install cdk-github-microvm-runners with npm install cdk-github-microvm-runners.

Once ready, write the CDK code following the Getting started guide.

import { App, CfnOutput, Stack } from 'aws-cdk-lib';
import { Secret } from 'aws-cdk-lib/aws-secretsmanager';
import {
  GithubAppId,
  GithubAppKey,
  GithubAuth,
  GithubMicrovmRunners,
  MicrovmSize,
  RunnerScope,
} from 'cdk-github-microvm-runners';

const app = new App();
const stack = new Stack(app, 'Runners', { env: { region: 'us-east-1' } });

const appId = Secret.fromSecretNameV2(
  stack,
  'AppId',
  'microvm-runner/dev/app-id',
);
const privateKey = Secret.fromSecretNameV2(
  stack,
  'AppKey',
  'microvm-runner/dev/app-private-key',
);
const webhookSecret = Secret.fromSecretNameV2(
  stack,
  'WebhookSecret',
  'microvm-runner/dev/webhook-secret',
);

const runners = new GithubMicrovmRunners(stack, 'Runners', {
  github: GithubAuth.app({
    appId: GithubAppId.fromSecret(appId),
    privateKey: GithubAppKey.fromSecret(privateKey),
    webhookSecret,
  }),
  scope: RunnerScope.org('<Set the name of the Org where you want to use the self-hosted runner here>'),
});

// A runner class: the `microvm` label, on 4 GB VMs.
runners.addRunnerClass('microvm', { size: MicrovmSize.GB4 });

new CfnOutput(stack, 'WebhookUrl', { value: runners.webhookUrl });
new CfnOutput(stack, 'SetupCommand', { value: runners.setupCommand });

The key point is to set the GitHub Org name where you want to register the self-hosted runner in RunnerScope.org.

Once the code is ready, deploy the stack with npm cdk deploy. After waiting a while, the various resources will be created.

Created CloudFormation stack

The MicroVM image is also registered.

Registered MicroVM image

Incidentally, when I downloaded the Dockerfile used to build the above MicroVM image from S3, the contents were as follows.

FROM public.ecr.aws/lambda/microvms:al2023-minimal
RUN dnf install -y libicu bash git docker jq tar zip unzip nodejs22 sudo shadow-utils && dnf clean all
RUN dnf install -y gh --repofrompath gh-cli,https://cli.github.com/packages/rpm || true
RUN curl -fsSL https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip -o /tmp/awscliv2.zip && unzip -q /tmp/awscliv2.zip -d /tmp && /tmp/aws/install && rm -rf /tmp/aws /tmp/awscliv2.zip
RUN useradd -m runner && usermod -aG docker runner || groupadd docker && usermod -aG docker runner
RUN mkdir -p /opt/runner && cd /opt/runner && curl -fsSLo r.tgz https://github.com/actions/runner/releases/download/v2.335.1/actions-runner-linux-arm64-2.335.1.tar.gz && tar xzf r.tgz && rm r.tgz && chown -R runner:runner /opt/runner
COPY microvm-runner/agent.mjs /opt/microvm-runner/agent.mjs
COPY microvm-runner/entrypoint.sh /opt/microvm-runner/entrypoint.sh
RUN chmod +x /opt/microvm-runner/entrypoint.sh
ENTRYPOINT ["/opt/microvm-runner/entrypoint.sh"]

If you want to customize the image, you can also adjust it in the CDK code. For example, a way of writing using RunnerImage.fromInline like the following is available.

runners.addRunnerClass('custom', {
  size: MicrovmSize.GB4,
  image: RunnerImage.fromInline(`
FROM public.ecr.aws/lambda/microvms:al2023-minimal
...
(omitted)
`),
});

For details, please refer to the following link.

https://github.com/schuettc/cdk-github-microvm-runners/blob/main/docs/images.md

GitHub App Setup

Now that the stack is deployed, let's create a GitHub App. The setup command is also output in the CFn stack Output that was deployed earlier, and it looks like the following command.

npx cdk-github-microvm-runners@0.1 setup --org <target GitHub Org name> --stack Runners --region us-east-1

Please set the --stack option to the CFn stack name and --region to the region where the stack was deployed.

When you run the above command, a screen to create a GitHub App opens, so specify a unique name.

GitHub App creation screen

You are asked to confirm whether to install the created GitHub App to the Org, so click Install.GitHub App permissions confirmation screen

Once the installation is complete, close the browser.GitHub App creation completion screen

This completes all the necessary preparation. Behind the scenes of this series of operations, the secret values such as microvm-runner/dev/app-id specified in the CDK code are also registered.

Automatically registered secret values

Running a Workflow That Uses a Self-Hosted Runner

Now that we're ready to use the self-hosted runner, let's create a repository and trigger a GitHub Actions workflow. I first tried the following simple workflow.

on:
  workflow_dispatch
jobs:
  test:
    runs-on: [self-hosted, microvm]
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: test
        run: |
          echo test
          sleep 120

After triggering the workflow, checking Runners from the Org settings shows that the self-hosted runner has been registered.

Screen showing the self-hosted runner was automatically registered

After waiting a few more seconds, the workflow starts executing on the registered self-hosted runner.The workflow being executed on the self-hosted runner

Here is the log. You can see it executed properly.

Log of the job executed on the self-hosted runner

Looking at the log details, it seems to take about 20 seconds until the runner picks up the job.

2026-08-27T02:56:56.9760000Z Waiting for a runner to pick up this job...
2026-08-27T02:57:15.4380000Z Job is about to start running on the runner: microvm-runner-ba2f8119-bc1605de

According to the cdk-github-microvm-runners documentation, the time required for each phase is as follows. From when the workflow is triggered to when actual processing begins, the total wait time is about 25 seconds.

Segment Time What happens
Queued → the VM is running 6.8 s webhook delivery, the queue, the launcher, VM boot
VM running → the runner's first log line 8.2 s run.sh, the .NET host starting, assemblies loading
Runner starting → connected to GitHub 9.0 s reading its configuration, registering
Connected → the job begins 1.0 s GitHub assigns the queued job to it
Total 25 s

※ Table quoted from https://github.com/schuettc/cdk-github-microvm-runners/blob/main/docs/architecture.md

While we're at it, let's also check the contents of the DynamoDB table. Scanning the table showed that the following items were registered.

Result of scanning the management DynamoDB table

Looking at the actual data makes it much easier to visualize what's happening behind the scenes.

Let's also create another workflow to confirm that Docker can be used. The workflow definition is as follows.

on:
  workflow_dispatch
jobs:
  test:
    runs-on: [self-hosted, microvm]
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: test
        run: |
          docker run --rm  hello-world
    services:
      postgres:
        image: postgres
        env:
          POSTGRES_PASSWORD: postgres

We are running a docker command inside run, and also starting a postgres container with the services specification.

Here is the log when this workflow was executed.

Log of the job executed on the self-hosted runner, part 2

Log of the job executed on the self-hosted runner, part 3

Docker can indeed be used on Lambda MicroVMs. When you select Lambda as the execution environment for CodeBuild, Docker cannot be used, but with a self-hosted runner on Lambda MicroVMs, Docker works without any issues.

Analysis

We managed to run GitHub Actions workflows on a self-hosted runner on Lambda MicroVMs, but let's consider whether this architecture is actually effective.

Comparing costs assuming the same processing time

First, let's do a simple cost comparison under the assumption that the workflow execution time doesn't change even when using a self-hosted runner.

For GitHub-hosted runners, the price for the SKU linux_2_core_arm is $0.005/minute. Starting an instance equivalent to linux_2_core_arm on Lambda MicroVMs, even in the Virginia region, gives the following estimate.

  • vCPU price per second: 2vCPU × $0.0000276944
  • Memory price per second (per GB): 8GB × $0.0000036667

The cost per minute is (2×0.0000276944 + 8 * 0.0000036667) × 60, which comes to $0.00508334. Although omitted here for simplicity, in practice additional storage costs for storing snapshots will also be incurred. Therefore, in a simple calculation, GitHub-hosted runners come out cheaper, but there is a possibility of achieving cost benefits by leveraging Saving Plans and similar options.

Another advantage of Lambda MicroVMs is that billing is per-second rather than per-minute. Since GitHub-hosted runners round billing up to the nearest minute, there could be cases where Lambda MicroVMs would bill for 1 minute and 31 seconds while GitHub-hosted runners would bill for 2 minutes.

Will the processing time actually be the same?

The previous estimate was based on the assumption that workflow execution time would not change, but what is the reality?

The cdk-github-microvm-runners used this time is designed to register self-hosted runners as Just-in-time runners and terminate MicroVMs each time, but Lambda MicroVMs support pausing and resuming. If reimplemented with an architecture where the MicroVM is paused after workflow completion and the paused MicroVM is resumed the next time the same workflow is triggered, it seems possible to make the most of Lambda MicroVMs' characteristic of being able to retain state.

For example, suppose a workflow contains a step that runs apt install. The first time the MicroVM starts, the package installation process runs in full, resulting in longer processing time, but from the second workflow execution onward, if the MicroVM with the packages already installed is resumed, the installation process would be skipped and command execution should complete quickly.

Going further, another approach would be to build a MicroVM image optimized for your own workload. Rather than running apt install inside the workflow, running apt install at MicroVM image build time would further reduce workflow processing time.

Whether you want to use a self-hosted runner for a specific project only, or provide self-hosted runners to multiple teams as an organizational foundation — depending on the requirements, the thinking around how much you can modify the MicroVM image will change, but reducing workflow processing time by leveraging the characteristics of a self-hosted runner seems worth considering.

Summary

I built and tested a GitHub Actions self-hosted runner on Lambda MicroVMs. With some additional work, this seems like it could enable some pretty interesting things.

If you're struggling with GitHub Actions processing time or costs, why not consider the self-hosted runner on Lambda MicroVMs architecture?

References

Share this article