In our last post we covered using a k3s on a Raspberry Pi4 to serve up build agents for Azure DevOps. But what about those who use Github? Github has a robust yaml based build system with actions and runners. Is it possible to leverage our setup to serve build foo for a GH repo?
Create a GH Project
Let's start with the basics. Make a local repo:
$ mkdir helloworld-gh-docker-action
$ cd helloworld-gh-docker-action/
$ git init
Initialized empty Git repository in /Users/johnsi10/Workspaces/helloworld-gh-docker-action/.git/
$ git checkout -b master
Switched to a new branch 'master'
Create Dockerfile and Action.yaml (from this guide)
$ cat Dockerfile
# Container image that runs your code
FROM alpine:3.10
# Copies your code file from your action repository to the filesystem path `/` of the container
COPY entrypoint.sh /entrypoint.sh
# Code file to execute when the docker container starts up (`entrypoint.sh`)
ENTRYPOINT ["/entrypoint.sh"]
$ cat action.yml
# action.yml
name: 'Hello World'
description: 'Greet someone and record the time'
inputs:
who-to-greet: # id of input
description: 'Who to greet'
required: true
default: 'World'
outputs:
time: # id of output
description: 'The time we greeted you'
runs:
using: 'docker'
image: 'Dockerfile'
args:
- ${{ inputs.who-to-greet }}
Create entrypoint.sh and set perms (and test)
$ cat entrypoint.sh
#!/bin/sh -l
echo "Hello $1"
time=$(date)
echo ::set-output name=time::$time
$ chmod 755 entrypoint.sh
$ ./entrypoint.sh
Hello
::set-output name=time::Wed Feb 19 08:37:29 CST 2020
You can create a readme as well:
$ cat README.md
# Hello world docker action
This action prints "Hello World" or "Hello" + the name of a person to greet to the log.
## Inputs
### `who-to-greet`
**Required** The name of the person to greet. Default `"World"`.
## Outputs
### `time`
The time we greeted you.
## Example usage
uses: actions/hello-world-docker-action@v1
with:
who-to-greet: 'Mona the Octocat'
Next, we add an origin and push (assumes you created a repo in your namespace):
$ git remote add origin https://github.com/idjohnson/helloworld-gh-docker-action.git
$ git add -A
$ git commit -m "Initial Repo"
[master (root-commit) d34233c] Initial Repo
4 files changed, 50 insertions(+)
create mode 100644 Dockerfile
create mode 100644 README.md
create mode 100644 action.yml
create mode 100755 entrypoint.sh
$ git push -u origin master
Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
Delta compression using up to 12 threads
Compressing objects: 100% (6/6), done.
Writing objects: 100% (6/6), 1.02 KiB | 1.02 MiB/s, done.
Total 6 (delta 0), reused 0 (delta 0)
To https://github.com/idjohnson/helloworld-gh-docker-action.git
* [new branch] master -> master
Branch 'master' set up to track remote branch 'master' from 'origin'.
Testing
Let’s test with their public hello world action first
$ mkdir -p .github/workflows
$ vi .github/workflows/main.yaml
$ cat .github/workflows/main.yaml
on: [push]
jobs:
hello_world_job:
runs-on: ubuntu-latest
name: A job to say hello
steps:
- name: Hello world action step
id: hello
uses: actions/hello-world-docker-action@v1
with:
who-to-greet: 'Mona the Octocat'
# Use the output from the `hello` step
- name: Get the output time
run: echo "The time was ${{ steps.hello.outputs.time }}"
Add, commit and push
$ git add .github/
$ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: .github/workflows/main.yaml
$ git commit -m "set github action"
[master 1bc143f] set github action
1 file changed, 15 insertions(+)
create mode 100644 .github/workflows/main.yaml
$ git push
Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
Delta compression using up to 12 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (5/5), 596 bytes | 596.00 KiB/s, done.
Total 5 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To https://github.com/idjohnson/helloworld-gh-docker-action.git
d34233c..1bc143f master -> master
We can see it now reflected:
Add a Self-Hosted Runner
Go to actions (e.g. https://github.com/idjohnson/helloworld-gh-docker-action/settings/actions) and click "Add Runner" to follow the steps (it will show you a token you'll need)
pi@raspberrypi:~ $ mkdir actions-runner && cd actions-runner
pi@raspberrypi:~/actions-runner $ curl -O -L https://github.com/actions/runner/releases/download/v2.165.2/actions-runner-linux-arm-2.165.2.tar.gz
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 630 100 630 0 0 2290 0 --:--:-- --:--:-- --:--:-- 2282
100 53.4M 100 53.4M 0 0 23.5M 0 0:00:02 0:00:02 --:--:-- 37.0M
pi@raspberrypi:~/actions-runner $ tar xzf ./actions-runner-linux-arm-2.165.2.tar.gz
pi@raspberrypi:~/actions-runner $
pi@raspberrypi:~/actions-runner $ ./config.sh --url https://github.com/idjohnson/helloworld-gh-docker-action --token ABTJDHFJIKJJDJDLKKDJDQ4
Then use run.sh to start it up
pi@raspberrypi:~/actions-runner $ ./run.sh
√ Connected to GitHub
2020-02-25 11:59:28Z: Listening for Jobs
Testing
Now lets test.. We change our yaml file to:
# Use this yaml in your workflow file for each job
runs-on: self-hosted
On the Pi we immediately see:
pi@raspberrypi:~/actions-runner $ ./run.sh
√ Connected to GitHub
2020-02-25 11:59:28Z: Listening for Jobs
2020-02-25 12:02:13Z: Running job: A job to say hello
2020-02-25 12:02:27Z: Job A job to say hello completed with result: Succeeded
And we see the same reflected in the GH UI:
I was able to background the run.sh and continue to run pipelines without a connected shell (for a short amount of time)...
However, after disconnecting, eventually you'll go offline. That is, Unless you did this from a kvm attached to the device, it will time out and go offline:
ideally we would containerize this…
Containerizing the Pi based GH Runner
Using: https://github.com/myoung34/docker-github-actions-runner
apiVersion: apps/v1
kind: Deployment
metadata:
name: actions-runner
namespace: runners
spec:
replicas: 2
selector:
matchLabels:
app: actions-runner
template:
metadata:
labels:
app: actions-runner
spec:
volumes:
- name: dockersock
hostPath:
path: /var/run/docker.sock
- name: workdir
hostPath:
path: /tmp/github-runner
containers:
containers:
- name: runner
image: myoung34/github-runner:latest
env:
- name: RUNNER_TOKEN
value: ABTHDHDHHDHDHHDHDHDHDDVS
- name: REPO_URL
value: https://github.com/idjohnson/helloworld-gh-docker-action
- name: RUNNER_WORKDIR
value: /tmp/github-runner
volumeMounts:
- name: dockersock
mountPath: /var/run/docker.sock
- name: workdir
mountPath: /tmp/github-runner
Note: that RUNNER_TOKEN does expire. So you may need to do "Add runner" as at the start of this guide to get a fresh token, lest you get a 401 error as i did below:
Normal Pulled 37m (x5 over 39m) kubelet, raspberrypi Successfully pulled image "myoung34/github-runner:latest"
Warning BackOff 87s (x172 over 39m) kubelet, raspberrypi Back-off restarting failed container
$ kubectl logs actions-runner-5594bdf89c-j7f4j -n runners
ldd: ./bin/libcoreclr.so: No such file or directory
ldd: ./bin/System.Security.Cryptography.Native.OpenSsl.so: No such file or directory
ldd: ./bin/System.IO.Compression.Native.so: No such file or directory
--------------------------------------------------------------------------------
| ____ _ _ _ _ _ _ _ _ |
| / ___(_) |_| | | |_ _| |__ / \ ___| |_(_) ___ _ __ ___ |
| | | _| | __| |_| | | | | '_ \ / _ \ / __| __| |/ _ \| '_ \/ __| |
| | |_| | | |_| _ | |_| | |_) | / ___ \ (__| |_| | (_) | | | \__ \ |
| \____|_|\__|_| |_|\__,_|_.__/ /_/ \_\___|\__|_|\___/|_| |_|___/ |
| |
| Self-hosted runner registration |
| |
--------------------------------------------------------------------------------
# Authentication
Http response code: Unauthorized from 'POST https://api.github.com/repos/idjohnson/helloworld-gh-docker-action/actions-runners/registration'
{"message":"Token expired.","documentation_url":"https://developer.github.com/v3"}
Response status code does not indicate success: 401 (Unauthorized).
An error occurred: Not configured
After fixing the token and applying:
$ kubectl apply -f ./test.yaml
deployment.apps/actions-runner created
$ kubectl get pods -n runners
NAME READY STATUS RESTARTS AGE
actions-runner-7f866cff78-hjkj9 1/1 Running 0 7s
And updating the RS to add a runner:
$ kubectl get pods -n runners
NAME READY STATUS RESTARTS AGE
actions-runner-7f866cff78-hjkj9 1/1 Running 0 3m16s
$ git diff
diff --git a/test.yaml b/test.yaml
index d742dca..6895f66 100644
--- a/test.yaml
+++ b/test.yaml
@@ -4,7 +4,7 @@ metadata:
name: actions-runner
namespace: runners
spec:
- replicas: 1
+ replicas: 2
selector:
matchLabels:
app: actions-runner
$ kubectl apply -f ./test.yaml
deployment.apps/actions-runner configured
$ kubectl get pods -n runners
NAME READY STATUS RESTARTS AGE
actions-runner-7f866cff78-hjkj9 1/1 Running 0 3m29s
actions-runner-7f866cff78-pvmmp 1/1 Running 0 6s
Did that work? Did we get 2 runners?
Summary
Github is clearly the largest most widely adopted source code hosting site for the open-source community (especially after the demise of google code in 2016). With Microsoft buying Github, many feared it would mean a push into Azure DevOps, but it has not come to pass. Github is as strong as ever and continues to roll out new features and support (in fact, right after buying, Microsoft made private repos free which is what kept me personally out of Github and in Azure DevOps).
Github Actions are yaml based. If you can stomach that, then they are great. I think at this point I'm just old and curmudgeonly on yaml. That's a me thing i need to get over. The fact is, GH Actions are very similar to Azure DevOps yaml pipelines and I would expect convergence in due time.
Using a Pi not only allows for ARM based builds, but allows one to create CI/CD pipelines to build and launch locally. It also puts the control in one's hands. I think they are free, but in reading the pricing guides and notes on self hosted, it's still not clear if you are constrained to 20 concurrent / 2000 hours. Notes in forum suggest the data-transfer is free but the "minutes" might count against private repos even on self-hosted.
Overall I'm pretty impressed with the offering, but not enough to jump ship from my Azure DevOps.