Optimize EC2 Costs for Self-Hosted GitHub Action Runners: A Smart Solution for Small Development Teams

A cost-effective solution that leverages AWS EC2 to optimize your self-hosted GitHub Action runners.

Martin Kjellstrand
Cloud Native Daily
5 min readMay 9, 2023

--

Using GitHub’s own runners for building the front- and back-end, running unit tests, and executing end-to-end tests can take considerable time.

This is especially true for small development teams working in mono-repos, where build times can reach around 20 minutes.

To address this issue, you can use self-hosted runners with more cores and RAM.

However, the challenge lies in avoiding high monthly costs for a machine that idles most of the time.

In this post, we’ll discuss a cost-effective solution that leverages AWS EC2 to optimize your self-hosted GitHub Action runners.

Overview

The solution involves creating an IAM role for starting and stopping EC2 instances, using a GitHub-hosted runner to start the dedicated build host only when necessary, and automating the shutdown of idle runners with a cron job.

This approach ensures that you only pay for the resources you actually use, saving you money without sacrificing performance.

This strategy can be particularly beneficial for small development teams, with a single dedicated build host being sufficient for most use cases.

Simplified savings calculation

At the time of writing, the hourly cost for a c6i.2xlarge instance is $0.3880, which amounts to approximately $300 per month.

However, with the solution outlined in this article and taking into account the following assumptions:

  • 10 commits per day on average (*)
  • A 20-day work month
  • Each commit initiating a full start/stop cycle
  • The build server shutting down after 30 minutes of starting

The estimated monthly cost is reduced to under $40.

This equates to a significant savings of around 87% compared to the expense of running the instance continuously.

In addition to this, the “commits per day on average” may be skewed too high, and not every commit will lead to a full start/stop cycle, so the actual savings will likely be even greater.

(*) To calculate the average number of commits per day in your repository, use the following one-liner: git log --format="%ad" --date=short | sort | uniq -c | awk '{s += $1; n++;} END {print s/n}' (Note: This does not take into account days for where there are no commits at all.)

Setting Up a GitHub Action Runner Instance on EC2

Before proceeding with the IAM setup, you’ll need to set up a GitHub Action runner instance on EC2. While a detailed explanation of this process is outside the scope of this article, you can follow the official GitHub documentation for installing a self-hosted runner on your EC2 instance:

GitHub: Adding self-hosted runners

Once you’ve completed the installation and the runner is registered as “idle” in your GitHub Actions settings, you can continue with the remaining steps in this article.

Set Up an IAM Role

First, create a dedicated IAM role for this operation, which we’ll call builder-bob. Attach the following JSON permission policy to grant the necessary permissions for starting and stopping EC2 instances, replacing 123456789abcdef01 with your build host instance-id, and 098765432109 with your AWS account-id.

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:StartInstances",
"ec2:StopInstances"
],
"Resource": "arn:aws:ec2:eu-central-1:098765432109:instance/i-123456789abcdef01"
}
]
}

Configure Your GitHub Actions Workflow

Next, set up your GitHub Actions workflow file to use a GitHub-hosted runner for starting the dedicated build host only when needed. This prevents the build host from running continuously and accumulating unnecessary costs.

name: example
on: push

jobs:
# This will use a Github hosted runner to start our dedicated build host
start_builder:
runs-on: ubuntu-20.04
steps:
- name: Set up AWS CLI
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Start self-hosted builder
run: aws ec2 start-instances --instance-ids i-123456789abcdef01 || true
integration_tests:
runs-on: self-hosted
needs: startbuilder
steps:
- name: Your build / test commands here
run: make -j

Lastly, implement a cron job to automate the shutdown of idle runners. This will check the server’s uptime and the age of the most recent job folder. If the server has been up for more than 20 minutes and no recent jobs are found, it will shut down automatically.

Here’s the cron job script:

* * * * * root find /proc -maxdepth 1 -name uptime -mmin +20 -exec sh -c 'find /opt/github-runner/_work -maxdepth 0 -type d ! -mmin -20 -execdir /sbin/poweroff \;' \;

The find command checks if /proc/uptime was modified more than 20 minutes ago (its mtime is set to when the server booted). If true, another find invocation checks for new runner jobs in the last 20 minutes. If no recent jobs are found, it issues a /sbin/poweroff to shut down the server.

Conclusion

By implementing these strategies, you can improve your build times and save on EC2 costs, making your development process more efficient and cost-effective.

Learn More:

--

--

Martin Kjellstrand
Cloud Native Daily

20+ years in software dev & ops, specializing in DevOps, high-availability, backend systems, Unix/Linux. Join me exploring the tech world together.