This post is a continuation of our journey with self-hosted CI/CD agents. I encourage you to check part 1 and part 2 if you want to see a different approach to that topic. In this post, we will focus on GitHub Actions.
GitHub-hosted vs self-hosted runners
The documentation says that GitHub-hosted runners offer a simple way to run workflows, while self-hosted runners are a great solution if you want to create a more configurable set up in your own environment.
GitHub-hosted runners:
- Receive automatic updates for the operating system and tools.
- Are managed and maintained by GitHub.
- Provide a clean instance for every job execution.
- Use free minutes on your GitHub plan, with per-minute rates applied after surpassing the free minutes.
Self-hosted runners:
- Receive automatic updates for the self-hosted runner application only.
- You are responsible for updating the operating system and all other software.
- Can use cloud services or local machines that you already pay for.
- Are customizable to your hardware, operating system, software, and security requirements.
- Don’t need to have a clean instance for every job execution.
- Are free to use with GitHub Actions, but you are responsible for the cost of maintaining your runner machines.
Overview
We will create and configure the following resources:
- AKS cluster with workload identity and kubelet identity.
- Azure Container Registry.
- Role assignment for kubelet identity and workload identity.
- Kubernetes objects for workload identity and GitHub runner.
We will also check how to se up a runner with actions runner controller.
Here is a link to GitHub repo with all files for reference.
Video walkthrough
Create AKS cluster and ACR
- Run script.
1 2 3
# After running above script, if there were no errors, variables should be available in terminal. chmod +x aks.sh ./aks.sh 'add user id, for me, it is my email of AAD user'
- Get credentials to AKS, oidcUrl and test connection.
1 2 3 4 5
az aks get-credentials --resource-group $resourceGroup --name $aksName export oidcUrl="$(az aks show --name $aksName \ --resource-group $resourceGroup \ --query "oidcIssuerProfile.issuerUrl" -o tsv)" kubectl get nodes
Set up workload identity
- Create workload identity.
1 2 3 4 5
workloadIdentity="workload-identity" workloadClientId=$(az identity create --name $workloadIdentity \ --resource-group $resourceGroup --query clientId -o tsv) workloadPrincipalId=$(az identity show --name $workloadIdentity \ --resource-group $resourceGroup --query principalId -o tsv)
- Create Kubernetes Service Account in GitHub namespace.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
# github-runner-sa.yaml apiVersion: v1 kind: Namespace metadata: name: github labels: name: github --- apiVersion: v1 kind: ServiceAccount metadata: annotations: azure.workload.identity/client-id: <insert workloadClientId> # echo $workloadClientId labels: azure.workload.identity/use: "true" name: workload-sa namespace: github
1
kubectl apply -f github-runner-sa.yaml
- Create ClusterRole and ClusterRoleBinding for Service Account. Update workloadPrincipalId in file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
# github-roles.yaml --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: annotations: rbac.authorization.kubernetes.io/autoupdate: "true" name: github rules: - apiGroups: - '*' resources: - statefulsets - services - replicationcontrollers - replicasets - podtemplates - podsecuritypolicies - pods - pods/log - pods/exec - podpreset - poddisruptionbudget - persistentvolumes - persistentvolumeclaims - endpoints - deployments - deployments/scale - daemonsets - configmaps - events - secrets verbs: - create - get - watch - delete - list - patch - update --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: annotations: rbac.authorization.kubernetes.io/autoupdate: "true" name: github roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: github subjects: - apiGroup: rbac.authorization.k8s.io kind: Group name: system:serviceaccounts:github - apiGroup: rbac.authorization.k8s.io kind: User name: # echo $workloadPrincipalId
1
kubectl apply -f github-roles.yaml
- Create federated identity credentials.
1 2 3 4 5 6
az identity federated-credential create \ --name "aks-federated-credential" \ --identity-name $workloadIdentity \ --resource-group $resourceGroup \ --issuer "${oidcUrl}" \ --subject "system:serviceaccount:github:workload-sa"
- Create custom role for workload identity. Create acrbuild.json file with following definition. Replace
{YOUR SUBSCRIPTION}
with your subscription id.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
{ "Name": "AcrBuild", "IsCustom": true, "Description": "Can read, push, pull and list builds.", "Actions": [ "Microsoft.ContainerRegistry/registries/read", "Microsoft.ContainerRegistry/registries/pull/read", "Microsoft.ContainerRegistry/registries/push/write", "Microsoft.ContainerRegistry/registries/scheduleRun/action", "Microsoft.ContainerRegistry/registries/runs/*", "Microsoft.ContainerRegistry/registries/listBuildSourceUploadUrl/action" ], "AssignableScopes": [ "/subscriptions/{YOUR SUBSCRIPTION}" ] }
1
az role definition create --role-definition acrbuild.json
- Assign AcrBuild role to workload identity.
1 2
az role assignment create --assignee $workloadClientId \ --role 'AcrBuild' --scope $acrId
- Assign Azure Kubernetes Service Cluster User Role and Azure Kubernetes Service RBAC Writer to workload identity.
1 2 3 4
az role assignment create \ --role "Azure Kubernetes Service Cluster User Role" \ --assignee $workloadPrincipalId \ --scope $aksId
1 2 3 4
az role assignment create \ --role "Azure Kubernetes Service RBAC Writer" \ --assignee $workloadPrincipalId \ --scope "$aksId/namespaces/github"
Install GitHub Actions self-hosted runner
Create docker image for GitHub runner.
We will use information from above instructions. We will add az cli, kubectl. Size of image is far from perfect. Build and push it to our ACR.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
FROM ubuntu:22.04
USER root
RUN apt-get -y update && apt-get install -y curl && \
curl -sL https://aka.ms/InstallAzureCLIDeb | bash && az aks install-cli && \
curl -fsSL https://get.docker.com -o get-docker.sh && sh ./get-docker.sh && \
mkdir actions-runner && cd actions-runner && \
curl -o actions-runner-linux-x64-2.301.1.tar.gz -L https://github.com/actions/runner/releases/download/v2.301.1/actions-runner-linux-x64-2.301.1.tar.gz && \
tar xzf ./actions-runner-linux-x64-2.301.1.tar.gz && ./bin/installdependencies.sh && \
apt-get clean
RUN addgroup --gid 106 github && adduser github --uid 105 --system && adduser github github && \
chown -R github:github actions-runner
USER github
EXPOSE 8080
1
az acr build -f Dockerfile.runner -t github-runner:v1.0.0 -r $acrName -g $resourceGroup .
Create Storage Class and Persistent Volume Claim
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# github-storageclass.yaml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: github-azurefile
provisioner: file.csi.azure.com
mountOptions:
- uid=105
- gid=106
allowVolumeExpansion: true
volumeBindingMode: Immediate
reclaimPolicy: Delete
parameters:
skuName: Standard_LRS
1
2
3
4
5
6
7
8
9
10
11
12
13
# github-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: github-pvc
namespace: github
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 50Gi
storageClassName: github-azurefile
1
2
kubectl apply -f github-storageclass.yaml
kubectl apply -f github-pvc.yaml
Create StatefulSet and Service for runner
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
apiVersion: v1
kind: Service
metadata:
name: github-runner
namespace: github
labels:
app: github-runner
spec:
ports:
- port: 8080
name: github-runner-port
clusterIP: None
selector:
app: github-runner
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: github-runner
namespace: github
spec:
replicas: 1
minReadySeconds: 10
serviceName: github-runner
selector:
matchLabels:
app: github-runner
azure.workload.identity/use: "true"
persistentVolumeClaimRetentionPolicy:
whenDeleted: Retain
whenScaled: Delete
template:
metadata:
labels:
app: github-runner
azure.workload.identity/use: "true"
spec:
terminationGracePeriodSeconds: 10
serviceAccountName: workload-sa
containers:
- image: githubacr14149.azurecr.io/github-runner:v1.0.0 # Add your ACR respository
name: github-runner
imagePullPolicy: Always
command:
- sleep
args:
- 99d
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2000m"
memory: "2Gi"
volumeMounts:
- mountPath: "/home/github"
name: runner-data
volumes:
- name: runner-data
persistentVolumeClaim:
claimName: github-pvc
1
kubectl apply -f statefulset.yaml
Set up runner inside pod
- Check if pod is running and connect to container.
1
2
kubectl -n github get pods
kubectl -n github exec -it github-runner-0 -- sh
- Configure runner
1
./actions-runner/config.sh --url https://github.com/adamkielar/github-runner --token <YOU TOKEN>
- Start runner
1
./actions-runner/run.sh
Create GitHub workflow
- In
.github/workflows
createdeploy-app.yaml
.
Update name of ACR registry and optionally AKS name and resource group.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# deploy-app.yaml
name: Build image, push to ACR, deploy to AKS
concurrency:
group: deployment
on:
push:
workflow_dispatch:
jobs:
deploy:
name: Deploy application
runs-on: self-hosted
steps:
- name: Checkout repo
uses: actions/checkout@v2
- name: Login with workload identity
run: az login --service-principal -u $AZURE_CLIENT_ID -t $AZURE_TENANT_ID --federated-token $(cat $AZURE_FEDERATED_TOKEN_FILE)
- name: Build image with AZ ACR
run: |
az acr build -f /actions-runner/_work/github-runner/github-runner/Dockerfile -t githubacr14149.azurecr.io/github-demo:$ -r githubacr14149 .
- name: Get AKS credentials
run: |
az aks get-credentials --resource-group github-runner-rg --name aks-github-runner --overwrite-existing
kubelogin convert-kubeconfig -l workloadidentity
- name: Deploy application
run: |
sed 's|IMAGE|githubacr14149.azurecr.io/github-demo|g; s/TAG/$/g' /actions-runner/_work/github-runner/github-runner/pod.yaml | kubectl apply -f -
- Push changes to GitHub and check status of each resource.
Set up self-hoster runner with Actions Runner Controller
Let’s follow the documentation and see if the steps are straightforward.
- Install cert-manager.
1
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.8.2/cert-manager.yaml
- Generate a Personal Access Token in GitHub for your repo.
- repo (all)
- admin:public_key - read:public_key
- admin:repo_hook - read:repo_hook
- admin:org_hook
- notifications
- workflow
- Deploy controller using helm.
1
helm repo add actions-runner-controller https://actions-runner-controller.github.io/actions-runner-controller
1 2 3 4
helm upgrade --install --namespace actions-runner-system --create-namespace\ --set=authSecret.create=true\ --set=authSecret.github_token="REPLACE_YOUR_TOKEN_HERE"\ --wait actions-runner-controller actions-runner-controller/actions-runner-controller
- Create runnerdeployment.yaml.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
# runnerdeployment.yaml apiVersion: actions.summerwind.dev/v1alpha1 kind: RunnerDeployment metadata: name: github-runnerdeploy namespace: github spec: replicas: 1 selector: matchLabels: app: github-runner-v2 template: metadata: labels: app: github-runner-v2 azure.workload.identity/use: "true" spec: repository: adamkielar/github-runner # change to your repository labels: - github-runner ephemeral: true serviceAccountName: workload-sa
1
kubectl apply -f runnerdeployment.yaml
- Confirm pods are running.
- Create
.github/workflow/deploy-app-v2.yaml
file. We will modify our pipeline a bit. We will add install_libs script.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
name: Build image, push to ACR, deploy to AKS
concurrency:
group: deployment
on:
push:
workflow_dispatch:
jobs:
deploy:
name: Deploy application
runs-on: github-runner
steps:
- name: Checkout repo
uses: actions/checkout@v2
- name: Install Azure CLI, kubectl
run: chmod +x install_libs.sh && ./install_libs.sh
- name: Install kubelogin
run: sudo az aks install-cli
- name: Login with workload identity
run: az login --service-principal -u $AZURE_CLIENT_ID -t $AZURE_TENANT_ID --federated-token $(cat $AZURE_FEDERATED_TOKEN_FILE)
- name: Build image with AZ ACR
run: |
az acr build -f /runner/_work/github-runner/github-runner/Dockerfile -t githubacr14149.azurecr.io/github-demo:$ -r githubacr14149 .
- name: Get AKS credentials
run: |
az aks get-credentials --resource-group github-runner-rg --name aks-github-runner --overwrite-existing
export KUBECONFIG=/home/runner/.kube/config
kubelogin convert-kubeconfig -l workloadidentity
- name: Deploy application
run: |
sed 's|IMAGE|githubacr14149.azurecr.io/github-demo|g; s/TAG/$/g' /runner/_work/github-runner/github-runner/pod.yaml | kubectl apply -f -
- Delete
github-demo-pod
.1
kubectl -n github delete pod github-demo-pod
- Push changes to GitHub and check result.
We successfully managed to deploy our application.