When we hired engineer #2 to our team, it was time to set up some CI jobs to check our pull requests. At Traduality, we use GitHub, so GitHub Actions was a natural choice for our CI system. GitHub provides cloud-hosted job runners, but I wanted to host our own for fun. This feature is called self-hosted runners in GitHub Actions.
At Traduality, we manage our servers using Ansible. My goal was to automate the configuration of a self-hosted runner for GitHub Actions using Ansible.
GitHub’s documentation for running a self-hosted runner was ClickOps:
- Visit your GitHub repository’s settings
- Click on the “New self-hosted runner” button
- Copy the shell command
- Paste & run the shell command on your server 😬
- Repeat this process if the server ever reboots ☹️
Let’s automate these steps using Ansible!
This tutorial assumes basic knowledge of Ansible.
Setting Up the Server
Before we set up the software, it’s important to think about security for a second. We want to have some isolation and restrictions for our runner so that jobs can’t (accidentally or intentionally) shut down the machine, for example.
We will accomplish this by creating a dedicated user. Let’s call it github-actions-runner. Here is the start of our role’s tasks file:
# roles/github-actions-runner/tasks/main.yml
- name: Ensure github-actions-runner group exists
ansible.builtin.group:
name: github-actions-runner
state: present
become: true
- name: Ensure github-actions-runner user exists
ansible.builtin.user:
name: github-actions-runner
group: github-actions-runner
home: /home/github-actions-runner
state: present
become: trueInstalling GitHub Actions Runner
The first thing we need to do is install the GitHub Actions Runner software on our server. This is simple enough. GitHub releases versioned tarballs for different operating systems. All we have to do is download the tarball and extract it:
# continuation of roles/github-actions-runner/tasks/main.yml
- name: Download GitHub Actions software
ansible.builtin.get_url:
url: "https://github.com/actions/runner/releases/download/v2.323.0/actions-runner-linux-x64-2.323.0.tar.gz"
dest: "/home/github-actions-runner/actions-runner-linux-x64-2.323.0.tar.gz"
mode: "0600"
owner: github-actions-runner
group: github-actions-runner
checksum: sha256:0dbc9bf5a58620fc52cb6cc0448abcca964a8d74b5f39773b7afcad9ab691e19
become: true
become_user: github-actions-runner
- name: Create directory for extracted GitHub Actions software
ansible.builtin.file:
dest: "/home/github-actions-runner/actions-runner"
owner: github-actions-runner
group: github-actions-runner
state: directory
become: true
become_user: github-actions-runner
- name: Extract GitHub Actions software
ansible.builtin.unarchive:
src: "/home/github-actions-runner/actions-runner-linux-x64-2.323.0.tar.gz"
remote_src: true
dest: "/home/github-actions-runner/actions-runner"
# NOTE(strager): It's a tar bomb, so --strip-components=1 is implied.
owner: github-actions-runner
group: github-actions-runner
creates: "/home/github-actions-runner/actions-runner/bin/Run.Listener"
become: true
become_user: github-actions-runnerNote that we are putting all of the files inside of the home directory of the github-actions-runner user we created in the previous section. We are also using become_user so that everything is done by our user and not root (for example).
For simplicity, the above tasks install only the 64-bit x86 version for Linux. If you’re using a different architecture (such as AArch64), adjust the commands accordingly.
Configuring the Runner
The GitHub Actions Runner software needs to be configured before use. This configuration tells the software which account to associate with and tags that jobs can use to refer to the runner.
Unfortunately, configuration is done with a normal human-readable configuration file. Instead, we need to call a configuration script.
At first glance, the configuration script looks like it requires manual intervention. It requires a token, and the only way to get a token is to use the GitHub website. But after some digging, I found that you can use a GitHub Personal Access Token instead.
Here’s how to create a GitHub Personal Access Token that we can use to configure our GitHub Actions Runner:
- Visit the Fine-grain Personal Access Token page in your GitHub account’s settings.
- Click “Generate new token”.
- Select your GitHub organization as the “Resource owner”.
- Select the repositories that the runner should have access to.
- Under “Organization permissions”, set “Self-hosted runners” to “Access: Read and write”.
- Under “Repository permissions”, set the following:
- “Actions” to “Access: Read and write”
- “Administration” to “Access: Read and write”
- “Commit statuses” to “Access: Read and write”
- “Contents” to “Access: Read and write”
- “Deployments” to “Access: Read and write”
- “Metadata” to “Access: Read-only”
- “Pull requests” to “Access: Read and write”
- “Secrets” to “Access: Read-only”
- Click “Generate token”.
Once you have generated your GitHub Personal Access Token, you need to store it somewhere. At Traduality, we use Azure Key Vaults, but you can use whatever mechanism you’re comfortable with such as Ansible Vaults.
Now let’s run the GitHub Actions Runner configuration tool. We only want to run this once during initial setup, so let’s use Ansible’s when directive to only execute it if the generated configuration file doesn’t already exist:
# continuation of roles/github-actions-runner/tasks/main.yml
- name: Check if GitHub Actions software is already configured
stat:
path: /home/github-actions-runner/actions-runner/.credentials
register: traduality_github_actions_runner_credentials_file
become: true
become_user: github-actions-runner
# TODO(strager): Rerun config.sh and restart the service if the configuration
# (labels, name, etc.) changes.
- name: Configure GitHub Actions self-hosted runner
ansible.builtin.shell:
chdir: /home/github-actions-runner/actions-runner/
cmd: |
./config.sh \
--url https://github.com/traduality/Traduality \
--pat '{{ YOUR_GITHUB_PERSONAL_ACCESS_TOKEN }}' \
--name daedalus-il \
--labels 'daedalus-il linux-amd64 testlabel' \
--unattended \
--replace
when: traduality_github_actions_runner_credentials_file.stat.exists == False
no_log: true # Do not leak secrets by printing the command.
become: true
become_user: github-actions-runnerThere are some knobs here that you probably want to tweak, such as the runner’s name (daedalus-il above) and its labels (daedalus-il, linux-amd64, and testlabel above). Adapt the task to your needs.
Note that if you run the task then later want to change the runner’s name or labels or GitHub Personal Access Token, re-running the Ansible task won’t apply any updates. Making this work automatically is left as an exercise for the reader. 🙂
Running the Runner
We’ve set up the files needed to run the GitHub Actions Runner, but how do we actually run it? GitHub Actions’s user interface says you should execute the run.sh script, but that script blocks forever processing jobs. We shouldn’t run run.sh from our Ansible script!
At Traduality, we use systemd for running our services on our Linux machines. Let’s configure a systemd service to run GitHub Actions Runner:
# roles/github-actions-runner/files/traduality-github-actions-runner.service
[Unit]
Description = Traduality: GitHub Actions Runner (self-hosted)
After = network.target
[Service]
Type = exec
ExecStart = /home/github-actions-runner/actions-runner/bin/runsvc.sh
WorkingDirectory = /home/github-actions-runner/actions-runner/
User = github-actions-runner
KillMode = process
KillSignal = SIGTERM
TimeoutStopSec = 5min
[Install]
WantedBy = multi-user.targetThe WantedBy= line ensures that the GitHub Actions Runner starts itself automatically on reboot.
Note that we use the runsvc.sh script instead of the run.sh script. I don’t remember the difference between the two, but runsvc.sh sounds more appropriate for running GitHub Actions Runner software a as a service. 🙂
Let’s deploy this systemd service file to the server using an Ansible task, and let’s also use Ansible to start the service:
# continuation of roles/github-actions-runner/tasks/main.yml
- name: Install GitHub Actions self-hosted runner service
ansible.builtin.copy:
src: traduality-github-actions-runner.service
dest: /etc/systemd/system/traduality-github-actions-runner.service
owner: root
group: root
mode: "0644"
become: true
- name: Start GitHub Actions self-hosted runner
ansible.builtin.service:
name: traduality-github-actions-runner.service
state: started
enabled: yes
daemon_reload: true
become: trueUsing the Runner
Now the GitHub Actions Runner is running on our server, but right now it’s idling waiting for a job. Let’s give it some work to do.
Create a GitHub Actions workflow file in the /.github/workflows/ directory of your Git repository. In the job specification, set runs-on to one of the labels you specified during the configuration of the runner. For example, here’s one of Traduality’s workflow files that runs the golangci-lint linter on our codebase:
# .github/workflows/golangci-lint.yml
name: golangci-lint
on:
push:
branches: [master]
pull_request:
types: [opened, synchronize]
permissions:
contents: read
pull-requests: read
jobs:
golangci-lint:
name: lint
runs-on: daedalus-il
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
with:
go-version: 1.24.1
- name: golangci-lint
uses: golangci/golangci-lint-action@1481404843c368bc19ca9406f87d6e0fc97bdcfd # v7.0.0
with:
version: v2.0.1
env:
GOEXPERIMENT: synctestCreate a commit with this workflow, push it to a branch on the GitHub server, and maybe create a pull request to share it with your friends.
Wrapping Up
It all went by so fast. A few Ansible tasks to move things around, and a tricky configuration step (which honestly took a few hours to figure out because GitHub’s permissions are annoying). That’s really all it takes to set up a self-hosted GitHub Actions runner using Ansible.
For completeness, here is the entire Ansible task that we use in production at Traduality:
---
- name: Ensure github-actions-runner group exists
ansible.builtin.group:
name: github-actions-runner
state: present
become: true
- name: Ensure github-actions-runner user exists
ansible.builtin.user:
name: github-actions-runner
group: github-actions-runner
home: /home/github-actions-runner
state: present
become: true
- name: Download GitHub Actions software
ansible.builtin.get_url:
url: "https://github.com/actions/runner/releases/download/v2.323.0/actions-runner-linux-x64-2.323.0.tar.gz"
dest: "/home/github-actions-runner/actions-runner-linux-x64-2.323.0.tar.gz"
mode: "0600"
owner: github-actions-runner
group: github-actions-runner
checksum: sha256:0dbc9bf5a58620fc52cb6cc0448abcca964a8d74b5f39773b7afcad9ab691e19
become: true
become_user: github-actions-runner
- name: Create directory for extracted GitHub Actions software
ansible.builtin.file:
dest: "/home/github-actions-runner/actions-runner"
owner: github-actions-runner
group: github-actions-runner
state: directory
become: true
become_user: github-actions-runner
- name: Extract GitHub Actions software
ansible.builtin.unarchive:
src: "/home/github-actions-runner/actions-runner-linux-x64-2.323.0.tar.gz"
remote_src: true
dest: "/home/github-actions-runner/actions-runner"
# NOTE(strager): It's a tar bomb, so --strip-components=1 is implied.
owner: github-actions-runner
group: github-actions-runner
creates: "/home/github-actions-runner/actions-runner/bin/Run.Listener"
become: true
become_user: github-actions-runner
- name: Check if GitHub Actions software is already configured
stat:
path: /home/github-actions-runner/actions-runner/.credentials
register: traduality_github_actions_runner_credentials_file
become: true
become_user: github-actions-runner
- name: Get GitHub Personal Access Token from Azure Vault
traduality_azure_secret_info:
vault: rnd-vault-il
name: github-actions-runner-personal-access-token
register: traduality_rnd_secrets
when: traduality_github_actions_runner_credentials_file.stat.exists == False
# TODO(strager): Rerun config.sh and restart the service if the configuration
# (labels, name, etc.) changes.
- name: Configure GitHub Actions self-hosted runner
ansible.builtin.shell:
chdir: /home/github-actions-runner/actions-runner/
cmd: |
./config.sh \
--url https://github.com/traduality/Traduality \
--pat '{{ traduality_rnd_secrets['github-actions-runner-personal-access-token'] }}' \
--name daedalus-il \
--labels daedalus-il \
--unattended \
--replace
when: traduality_github_actions_runner_credentials_file.stat.exists == False
no_log: true # Do not leak secrets by printing the command.
become: true
become_user: github-actions-runner
- name: Install GitHub Actions self-hosted runner service
ansible.builtin.copy:
src: traduality-github-actions-runner.service
dest: /etc/systemd/system/traduality-github-actions-runner.service
owner: root
group: root
mode: "0644"
become: true
- name: Start GitHub Actions self-hosted runner
ansible.builtin.service:
name: traduality-github-actions-runner.service
state: started
enabled: yes
daemon_reload: true
become: true




0 Comments