Using AWS CodeBuild as a Self-Hosted Runner for GitHub Actions

| 11 min read
Author: noboru-kudo noboru-kudoの画像
Information

To reach a broader audience, this article has been translated from Japanese.
You can find the original version here.

GitHub Actions has become a representative CI/CD service, and last year its workflow syntax became available for use with AWS CodeBuild. However, the pipeline itself operates as a job in CodeBuild, which means that the github context is not available, and there are some restrictions on using the GitHub Actions Marketplace.

Recently, on April 24, 2024, AWS announced the following release:

Now, CodeBuild itself can be used as a self-hosted runner for GitHub Actions. Here, CodeBuild is used as the job execution environment (ephemeral self-hosted runners).

This allows the use of ARM architectures, GPU workloads, Lambda execution environments, etc., supported by CodeBuild in GitHub Actions workflows. Moreover, since the job itself operates on CodeBuild, it is easy to use various AWS services such as IAM and VPC resources (OIDC provider settings and access keys are not needed).

Of course, there are limitations as a self-hosted runner, but since the self-hosted runner itself is provided by GitHub Actions, you can use all the features of GitHub Actions.

I tried out this feature, so I will introduce how to use it.

Preliminary Preparation: GitHub Connection Settings

#

You need to set up the connection from CodeBuild to GitHub in advance. As stated in the official documentation below, connect using a Personal Access Token (PAT) or OAuth App.

Below is an example of registering a PAT using AWS CLI.

# Set the access token to GITHUB_TOKEN
aws codebuild import-source-credentials --token ${GITHUB_TOKEN} \
  --server-type GITHUB --auth-type PERSONAL_ACCESS_TOKEN
# Check settings
aws codebuild list-source-credentials

CodeBuild uses this setting to create WebHooks. Do not forget to set the permissions described in the above document when creating the PAT (otherwise, the creation of the CodeBuild project will fail).

For private repositories, the following permissions allowed me to create a self-hosted runner.

Creating a CodeBuild Project

#

Let's create a self-hosted runner in CodeBuild.

Here, we will create it from the management console. Create a new build project from the CodeBuild menu.

Enter an arbitrary project name, select "GitHub" as the source provider, and enter the URL of the GitHub repository (if you have already connected to GitHub, you should be able to select from the list of repositories).

In "Primary source webhook events", check "Rebuild every time code is pushed to this repository" and select WORKFLOW_JOB_QUEUED in "Filter Groups".
This notifies CodeBuild via WebHook when a job is queued from a GitHub Actions workflow.

Next, select an arbitrary execution environment. Here, it is left as default.

Next, for BuildSpec, select "Use a buildspec file".
Incidentally, any buildspec you prepare here will be ignored. At job execution time, it seems to be replaced with the setup process for the GitHub Actions runner.
The following is an excerpt from the official document.

Note that your Buildspec will be ignored. Instead, CodeBuild will override it to use commands that will setup the self-hosted runner. This project’s primary responsibility is to set up a self-hosted runner in CodeBuild to run GitHub Actions workflow jobs.

This creates the project. If successful, the CodeBuild build project is created as shown below.

Information

The BuildSpec of the created project was set as follows:

version: 0.2
phases:
  build:
    commands:
      - echo "BuildSpec will be overloaded for GHA self-hosted runner builds."

Meanwhile, let's check the WebHook settings on the GitHub side of the repository.

GitHub WebHooks

A new WebHook has been created. The process on the CodeBuild side is executed via this CodeBuild endpoint.

Operating the CodeBuild Self-Hosted Runner

#

Let's run a job with the CodeBuild self-hosted runner.
Create the following workflow file in the GitHub repository.

name: test-codebuild-runner
on:
  push:
jobs:
  build:
    runs-on: codebuild-GitHubActionsExampleRunner-${{ github.run_id }}-${{ github.run_attempt }}
    steps:
      - run: |
          echo "running on CodeBuild..."
          echo "OS: $(uname -a)"
          aws sts get-caller-identity

The point is the label specified in runs-on. In the case of CodeBuild's self-hosted runner, you need to use one of the following:

  • codebuild-<CodeBuild Project name>-${{ github.run_id }}-${{ github.run_attempt }}
  • codebuild-<CodeBuild Project name>-${{ github.run_id }}-${{ github.run_attempt }}-<image>-<image-version>-<instance-size>

Here, we are using the former specification. The latter specification is used if you want to overwrite the execution environment of CodeBuild (see the column "Using GitHub Actions Matrix").

Place this file under the .github/workflows directory and push it. The GitHub Actions workflow is executed.
CodeBuild checks the label name (project name) of the queued job and creates and registers a self-hosted runner on the GitHub side.

During the execution of GitHub Actions, if you look at the Runner menu from the Settings of the relevant GitHub repository, you will see that the self-hosted runner is registered as shown below.

This self-hosted runner is temporary (ephemeral self-hosted runners) and is deleted after the job ends.

The log of GitHub Actions after the job ends is as follows.

GitHub Actions Logs

The job has completed successfully, and the results of the script execution can be confirmed.
On the other hand, let's also check the logs on the CodeBuild side.

CodeBuild Logs

These logs show the initialization and cleanup processes of the self-hosted runner.

Watching this behavior, the lifecycle of the CodeBuild self-hosted runner seems to follow the flow below (some imagination is included).

sequenceDiagram
    actor User as User
    participant GH as GitHub
    participant CB as CodeBuild
    participant Runner as Self-Hosted<br>Runner
    User->>GH: commit/push
    GH->>CB: workflow_job.queued event
    CB->>Runner: Create execution environment
    Runner->>Runner: Download/Install
    Runner->>GH: Register self-hosted runner
    GH->>Runner: Job execution instruction
    Runner->>Runner: Execute job
    GH->>CB: workflow_job.completed event
    CB->>Runner: Cleanup process
    Runner->>GH: Unregister self-hosted runner
Using GitHub Actions Matrix

The matrix of GitHub Actions can also be used with CodeBuild's self-hosted runner.
For example, if you want to run the same job in three different environments: Amazon Linux2023 x86/ARM, and Ubuntu, you would specify as follows:

name: test-codebuild-runner
on:
  push:
jobs:
  build:
    runs-on: codebuild-GitHubActionsExampleRunner-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.os }}
    strategy:
      matrix:
        os: ["al2-5.0-small", "arm-3.0-small", "ubuntu-7.0-small"]
    steps:
      - run: |
          echo "running on CodeBuild..."
          echo "OS: $(uname -a)"
          aws sts get-caller-identity

The label at the end of runs-on specifies matrix.os, and strategy.matrix.os specifies the variations.
This way, the same job is executed in each environment.
Note that the execution environment specified when creating the CodeBuild project is overwritten by the values in the matrix.

(Bonus) Creating with CloudFormation

#

Instead of the management console, I also created a self-hosted runner CodeBuild project using CloudFormation.
The template file is as follows.

Parameters:
  GitHubRepository:
    Type: String
    Description: The GitHub repository URL

Resources:
  CodeBuildProject:
    Type: AWS::CodeBuild::Project
    Properties:
      Name: GitHubActionsExampleRunner
      ServiceRole: !GetAtt CodeBuildRole.Arn
      Artifacts:
        Type: NO_ARTIFACTS
      Environment:
        Type: LINUX_CONTAINER
        ComputeType: BUILD_GENERAL1_SMALL
        Image: aws/codebuild/amazonlinux2-x86_64-standard:5.0
      Source:
        Type: GITHUB
        Location: !Ref GitHubRepository
      # WebHook(Self-hosted runner) settings
      Triggers:
        Webhook: true
        FilterGroups:
          - - Type: EVENT
              Pattern: WORKFLOW_JOB_QUEUED

  CodeBuildRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - codebuild.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: GitHubActionsExampleRunnerPolicy
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: "*"

The content is exactly the same as what is done in the management console, so I will omit it.
In actual operation, like other resources, it will be managed using some IaC tool (even though it doesn't look like a template for a self-hosted runner at first glance).

Summary

#

I introduced how to use CodeBuild as a self-hosted runner for GitHub Actions.
It is an optimal method for jobs that are natively AWS or where the runners provided by GitHub Actions are insufficient in specs.

According to the official documentation of GitHub Actions, self-hosted runners are recommended to be implemented as follows:

GitHub recommends implementing autoscaling with ephemeral self-hosted runners; autoscaling with persistent self-hosted runners is not recommended. In certain cases, GitHub cannot guarantee that jobs are not assigned to persistent runners while they are shut down. With ephemeral runners, this can be guaranteed because GitHub only assigns one job to a runner.

If you implement this yourself, it can be quite a hassle, and you may need to consider operating it using the Action Runner Controller (ARC) with Kubernetes prepared.
Using CodeBuild's self-hosted runner allows you to handle runner instance autoscaling as a managed service, greatly lowering the barrier to entry.

It might become a strong option for projects combining GitHub and AWS.

豆蔵では共に高め合う仲間を募集しています!

recruit

具体的な採用情報はこちらからご覧いただけます。