(image by Nina Dzyvulska)
Start your EC2 self-hosted runner right before you need it. Run the job on it. Finally, stop it when you finish. And all this automatically as a part of your GitHub Actions workflow.
See below the YAML code of the depicted workflow.
Table of Contents
The action can start the EC2 runner in any subnet of your VPC that you need - public or private. In this way, you can easily access any private resources in your VPC from your GitHub Actions workflow.
For example, you can access your database in the private subnet to run the database migration.
GitHub provides one fixed hardware configuration for their Linux virtual machines: 2-core CPU, 7 GB of RAM, 14 GB of SSD disk space.
Some of your CI workloads may require more powerful hardware than GitHub-hosted runners provide. In the action, you can configure any EC2 instance type for your runner that AWS provides.
For example, you may run a c5.4xlarge EC2 runner for some of your compute-intensive w 8000 orkloads. Or r5.xlarge EC2 runner for workloads that process large data sets in memory.
If your CI workloads don't need the power of the GitHub-hosted runners and the execution takes more than a couple of minutes, you can consider running it on a cheaper and less powerful instance from AWS.
According to GitHub's documentation, you don't need to pay for the jobs handled by the self-hosted runners:
Self-hosted runners are free to use with GitHub Actions, but you are responsible for the cost of maintaining your runner machines.
So you will be charged by GitHub only for the time the self-hosted runner start and stop. EC2 self-hosted runner will handle everything else so that you will pay for it to AWS, which can be less expensive than the price for the GitHub-hosted runner.
Use the following steps to prepare your workflow for running on your EC2 self-hosted runner:
1. Prepare GitHub OpenID role with least-privilege
-
If you haven't already done so, add the GitHub OIDC provider to IAM using the following procedure: Configuring OpenID Connect in Amazon Web Services. The GitHub OIDC provider allows you to directly authorize a GitHub repository to perform actions on an AWS account without storing IAM user identifiers (more secure). However, you can use any other means of authentication offered by aws-actions/configure-aws-credentials.
-
Create an IAM role adding at least your repository under the conditions of the trust-relationship (see the documentation proposed above to know the possible filtering), and add the following policies (following least-privilege minimum required permissions):
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ec2:RunInstances", "ec2:TerminateInstances", "ec2:DescribeInstances", "ec2:DescribeInstanceStatus" ], "Resource": "*" } ] }
If you plan to attach an IAM role to the EC2 runner with the
iam-role-name
parameter, you will need to allow additional permissions:{ "Version": "2012-10-17", "Statement": [ { // [ Others statements... ] }, { "Effect": "Allow", "Action": [ "ec2:ReplaceIamInstanceProfileAssociation", "ec2:AssociateIamInstanceProfile" ], "Resource": "*" }, { "Effect": "Allow", "Action": "iam:PassRole", "Resource": "*" } ] }
If you use the
aws-resource-tags
parameter, you will also need to allow the permissions to create tags:{ "Version": "2012-10-17", "Statement": [ { // [ Others statements... ] }, { "Effect": "Allow", "Action": [ "ec2:CreateTags" ], "Resource": "*", "Condition": { "StringEquals": { "ec2:CreateAction": "RunInstances" } } } ] }
Model of trust-relationship policy for GitHub OIDC provider to allow access from all branches of repository (replace variables values with your own values):
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" }, "StringLike": { "token.actions.githubusercontent.com:sub": "repo:${GITHUB_ORG}/${GITHUB_REPO_NAME}:*" } } } ] }
These example policies above are provided as a guide. They can and most likely should be limited even more by specifying the resources you use.
-
If your repository is public, it is recommended to store the role ARN in GitHub secrets to hide your AWS account information..
-
Use the aws-actions/configure-aws-credentials action to set up the connection to your AWS account.
If you use OIDC authentication, your workflow yaml file must contain at least the following permissions :
permissions:
id-token: write
contents: read
For authentication, the aws-actions/configure-aws-credentials action must contain at least the following options:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_DEPLOYMENT }}
aws-region: ${{ vars.AWS_REGION }}
2. Prepare GitHub personal access token
- Create a new GitHub personal access token with the
repo
scope. The action will use the token for self-hosted runners management in the GitHub account on the repository level. - Add the token to GitHub secrets.
3. Prepare EC2 image (optional)
For your runners, you can use directly an official AMI from a supported OS, and perform initialization operations and set up the dependencies via pre-runner-script
in the workflow YAML file. See example in the pre-runner-script
documentation below.
Alternatively, you can make your own AMI containing all pre-requisites and dependencies :
-
Create a new EC2 instance based on any Linux distribution you need.
-
Connect to the instance using SSH, install
docker
andgit
, then enabledocker
service.For Amazon Linux 2, it looks like the following:
sudo yum update -y && \ sudo yum install docker -y && \ sudo yum install git -y && \ sudo yum install libicu -y && \ sudo systemctl enable docker
-
Install any other tools required for your workflow.
-
Create a new EC2 image (AMI) from the instance.
-
Remove the instance if not required anymore after the image is created.
4. Prepare VPC with subnet and security group
- If you do not have a VPC for launch your runners in the desired AWS region, create it with at least one subnet with Internet access (either a private subnet with a NAT gateway, or a public subnet with Internet Gateway with automatic allocation of public IPs enabled).
- Create a new security group for the runners in the VPC. Only the outbound traffic on port 443 should be allowed for pulling jobs from GitHub. No inbound traffic is required.
5. Configure the GitHub workflow
- Create a new GitHub Actions workflow or edit the existing one.
- Use the documentation and example below to configure your workflow.
- Please don't forget to set up a job for removing the EC2 instance at the end of the workflow execution. Otherwise, the EC2 instance won't be removed and continue to run even after the workflow execution is finished.
Now you're ready to go!
Name | Required | Description |
---|---|---|
mode |
Always required. | Specify here which mode you want to use: - start - to start a new runner; - stop - to stop the previously created runner. |
github-token |
Always required. | GitHub Personal Access Token with the repo scope assigned. |
ec2-image-id |
Required if you use the start mode. |
EC2 Image Id (AMI): The new runner will be launched from this image. The action is compatible with Amazon Linux 2 images. |
ec2-instance-type |
Required if you use the start mode. |
EC2 Instance Type. |
subnet-id |
Required if you use the start mode. |
VPC Subnet Id: The subnet should belong to the same VPC as the specified security group. |
security-group-id |
Required if you use the start mode. |
EC2 Security Group Id: The security group should belong to the same VPC as the specified subnet. Only the outbound traffic for port 443 should be allowed. No inbound traffic is required. |
label |
Required if you use the stop mode. |
Name of the unique label assigned to the runner (provided by the output of the action in the start mode). The label is used to remove the runner from GitHub when the runner is not needed anymore. |
ec2-instance-id |
Required if you use the stop mode. |
EC2 Instance Id of the created runner (provided by the output of the action in the start mode). The id is used to terminate the EC2 instance when the runner is not needed anymore. |
iam-role-name |
Optional. Used only with the start mode. |
IAM role name to attach to the created EC2 runner. This allows the runner to have permissions to run additional actions within the AWS account, without having to manage additional GitHub secrets and AWS users. Setting this requires additional AWS permissions for the role launching the instance (see above). |
aws-resource-tags |
Optional. Used only with the start mode. |
Specifies tags to add to the EC2 instance and any attached storage. This field is a stringified JSON array of tag objects, each containing a Key and Value field (see example below). Setting this requires additional AWS permissions for the role launching the instance (see above). |
block-device-mapping |
Optional. Used only with the start mode. |
JSON string specifying the BlockDeviceMapping. For example:block-device-mapping: | |
key-name |
Optional. Used only with the start mode. |
Specifies SSH key-pair name to assign to an instance. This is useful for SSHing into an instance for debugging. |
runner-home-dir |
Optional. Used only with the start mode. |
Specifies a directory where pre-installed actions-runner software and scripts are located. |
pre-runner-script |
Optional. Used only with the start mode. |
Specifies bash commands to run before the runner starts. It's useful for installing dependencies with apt-get, yum, dnf, etc. For example:- name: Start EC2 runner |
runner-version |
Optional. Used only with the start mode without runner-home-dir . |
Specifies the version of the runner to install on the instance (https://github.com/actions/runner/releases). |
Name | Description |
---|---|
label |
Name of the unique label assigned to the runner. The label is used in two cases: - to use as the input of runs-on property for the following jobs; - to remove the runner from GitHub when it is not needed anymore. |
ec2-instance-id |
EC2 Instance Id of the created runner. The id is used to terminate the EC2 instance when the runner is not needed anymore. |
The workflow showed in the picture above and declared in do-the-job.yml
looks like this:
name: do-the-job
on: pull_request
permissions:
id-token: write
contents: read
jobs:
start-runner:
name: Start self-hosted EC2 runner
runs-on: ubuntu-latest
outputs:
label: ${{ steps.start-ec2-runner.outputs.label }}
ec2-instance-id: ${{ steps.start-ec2-runner.outputs.ec2-instance-id }}
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_DEPLOYMENT }}
aws-region: ${{ vars.AWS_REGION }}
- name: Start EC2 runner
id: start-ec2-runner
uses: jul-m/ec2-github-runner@v2.fork1
with:
mode: start
github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
ec2-image-id: ami-123
ec2-instance-type: t3.nano
subnet-id: subnet-123
security-group-id: sg-123
iam-role-name: my-role-name # optional, requires additional permissions
aws-resource-tags: | # optional, requires additional permissions
[
{"Key": "Name", "Value": "ec2-github-runner"},
{"Key": "GitHubRepository", "Value": "${{ github.repository }}"}
]
key-name: my-ssh-key # optional
block-device-mapping: | # optional
[
{"DeviceName" : "/dev/sda1", "Ebs" : { "VolumeType": "gp2", "VolumeSize": 34 }},
{"DeviceName" : "/dev/sdb", "VirtualName": "ephemeral0" }
]
do-the-job:
name: Do the job on the runner
needs: start-runner # required to start the main job when the runner is ready
runs-on: ${{ needs.start-runner.outputs.label }} # run the job on the newly created runner
steps:
- name: Hello World
run: echo 'Hello World!'
stop-runner:
name: Stop self-hosted EC2 runner
needs:
- start-runner # required to get output from the start-runner job
- do-the-job # required to wait when the main job is done
runs-on: ubuntu-latest
if: ${{ always() }} # required to stop the runner even if the error happened in the previous jobs
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_DEPLOYMENT }}
aws-region: ${{ vars.AWS_REGION }}
- name: Stop EC2 runner
uses: jul-m/ec2-github-runner@v2.fork1
with:
mode: stop
github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
label: ${{ needs.start-runner.outputs.label }}
ec2-instance-id: ${{ needs.start-runner.outputs.ec2-instance-id }}
We recommend using aws-actions/configure-aws-credentials action right before running the step for creating a self-hosted runner.
In this discussion, 5A2D you can find feedback and examples from the users of the action.
If you use this action in your workflow, feel free to add your story there as well 🙌
We recommend that you do not use self-hosted runners with public repositories.
Forks of your public repository can potentially run dangerous code on your self-hosted runner machine by creating a pull request that executes the code in a workflow.
Please find more details about this security note on GitHub documentation.
This code is made available under the MIT license.