AWS CodeBuild-hosted GitHub Actions runner with CDK in TypeScript
In this tutorial, I am going to show you how to create a CodeBuild-hosted GitHub Actions runner. With AWS CDK as a IaC tool, you can define CodeBuild project in TypeScript, JavaScript, Python, Java, C#/.Net, or Go. This tutorial uses TypeScript as an example. Whatever programming language you use, the properties you pass to the constructs are consistent. So even if you use other than TypeScript, you can understand how you define the construct of CodeBuild-hosted GitHub Actions runner. Nevertheless, you can make AI translate the TypeScript code into other language, just like that.
Prerequisites
In order to follow this tutorial, you must have:
- Created a GitHub repository. (I use github-actions-cdk-codebuild-runner for this tutorial.)
- Installed AWS CLI.
- Installed AWS CDK CLI.
- Set up IAM Authentication for CLI.
- Set up GitHub App connection.
When you set up GitHub App connection, the console says your can install the GitHub App during CodeBuild creation but you can't since we are creating the CodeBuild project via CDK. So, please install the app upon connection creation. Also, do not forget to add permission to the app to read your repository while installing the GitHub App.
Getting started
Before jump on coding a construct, let’s create a github-actions-cdk-codebuild-runner project.
mkdir github-actions-cdk-codebuild-runner
cd github-actions-cdk-codebuild-runner
Next, initialize CDK project:
cdk init app --language typescript
Commit and push your code to your repository.
Construct
Create a file `lib/github-actions-codebuild-runner.ts`. The code of this file is as follows:
import { EventAction, FilterGroup, Project, Source } from 'aws-cdk-lib/aws-codebuild';
import { Effect, Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';
import type { CfnResource } from 'aws-cdk-lib';
import type { BuildEnvironment, IProject } from 'aws-cdk-lib/aws-codebuild';
export interface GithubActionsCodeBuildRunnerProps {
projectName?: string;
repositoryOwner: string;
repositoryName: string;
connectionArn: string;
environment: BuildEnvironment;
}
export class GithubActionsCodeBuildRunner extends Construct {
private readonly project: IProject;
constructor(scope: Construct, id: string, props: GithubActionsCodeBuildRunnerProps) {
super(scope, id);
this.project = new Project(this, 'Project', {
projectName: props.projectName ?? 'github-actions-codebuild-runner-project',
environment: props.environment,
source: Source.gitHub({
owner: props.repositoryOwner,
repo: props.repositoryName,
cloneDepth: 1,
webhook: true,
webhookFilters: [FilterGroup.inEventOf(EventAction.WORKFLOW_JOB_QUEUED)],
}),
});
const policyStatement = new PolicyStatement({
actions: [
'codeconnections:GetConnection',
'codeconnections:GetConnectionToken'
],
resources: [props.connectionArn],
effect: Effect.ALLOW,
});
const policy = new Policy(this, 'Policy', {
statements: [policyStatement],
});
this.project.role!.attachInlinePolicy(policy);
(this.project.node.defaultChild as CfnResource).addDependency(policy.node.defaultChild as CfnResource);
(this.project.node.defaultChild as CfnResource).addPropertyOverride('Source.Auth', {
Type: 'CODECONNECTIONS',
Resource: props.connectionArn,
});
}
public get projectArn(): string {
return this.project.projectArn;
}
public get projectName(): string {
return this.project.projectName;
}
}
You might wonder what the last two statements in constructor are. See GitHub Issue why the simple construct constructor of CodeBuild Project is not enough as a GitHub Actions self-hosted runner.
About the connectionArn. You can define GitHub App connection with CfnConnection. But you still need authentication on console after your CDK stack is deployed. To avoid CDK-console-CDK hassle, I decided to create GitHub App connection on console beforehand. To my excuse, GitHub App connection is more likely to be shared by multiple GitHub repositories. So it is not recommended to be defined in one CDK project.
Stack
Now update github-actions-cdk-codebuild-runner-stack.ts:
import { Stack } from 'aws-cdk-lib';
import { ComputeType, LinuxBuildImage } from 'aws-cdk-lib/aws-codebuild';
import { Construct } from 'constructs';
import { GithubActionsCodeBuildRunner } from './github-actions-codebuild-runner';
import type { StackProps } from 'aws-cdk-lib';
export class GithubActionsCdkCodebuildRunnerStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const [repositoryOwner, repositoryName] = (() => {
const { GITHUB_REPOSITORY } = process.env;
const repository = GITHUB_REPOSITORY?.split('/');
if (repository?.length === 2) return repository;
throw new Error();
})();
const connectionArn = (() => {
const { CONNECTION_ARN } = process.env;
if (CONNECTION_ARN) return CONNECTION_ARN;
throw new Error();
})();
new GithubActionsCodeBuildRunner(this, 'GithubActionsCodeBuildRunner', {
projectName: 'GithubActionsCodeBuildRunner',
connectionArn,
repositoryOwner,
repositoryName,
environment: {
buildImage: LinuxBuildImage.STANDARD_7_0,
computeType: ComputeType.SMALL,
}
});
}
}
This tutorial assumes CDK stacks are deployed from your local computer. You might, however, want to deploy from CI/CD pipeline like GitHub Actions. In GitHub Actions workflow, your GitHub account repository names are attainable from the process environment GITHUB_REPOSITORY. The format of this variable is account_name/repository_name. The code above is an implementation based on this fact.
Just like repositoryOwner and repositoryName, I decided to get connectionArn from the process as well.
Hello world Workflow
Create a GitHub Actions workflow .github/workflows/hello-world.yml to test your CodBuild-hosted runner:
name: HelloWorld
on: [workflow_dispatch]
jobs:
hello-world:
runs-on:
- codebuild-GithubActionsCodeBuildRunner-${{ github.run_id }}-${{ github.run_attempt }}
steps:
- run: echo "Hello, world."
If you change the CodeBuild project name above, the the project name part of runner name GithubActionsCodeBuildRunner must also be changed.
Deploy/Test
Commit/push your updates and run the following commands:
export GITHUB_REPOSITORY=account_name/repository_name
export CONNECTION_ARN=your_github_app_connection_arn
npx cdk bootstrap # if you ever deploy cdk for the first time in your AWS account
npx cdk deploy
Go to your GitHub repository, Actions, then HelloWorld. Dispatch workflow from main branch. After 10-15 seconds, you see the workflow success echoing "Hello, world." in the log. You can also see the log from CodeBuild console.
Bonus: Workflow to Deploy CodeBuild
It’s common that you want to deploy your CodeBuild from CI. Here is a GitHub Actions workflow to do that:
name: Deploy GitHub Actions CodeBuild Runner
on:
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- uses: aws-actions/configure-aws-credentials@v4.0.2
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN_TO_ASSUME }}
aws-region: ${{ vars.AWS_REGION }}
- run: echo "CONNECTION_ARN=${{ vars.CONNECTION_ARN }}" >> $GITHUB_ENV
- run: npm install
- run: npx cdk bootstrap -y
- run: npx cdk deploy --require-approval never -y
Note that you need to store CONNECTION_ARN variable in your repository. Also, to authorize your GitHub account to access your AWS account, you need to set up IAM OIDC identity providers. You can refer to my other blog on how to add an identity provider. The identity information role ARN and region must also be stored in your repository.