Github Actions and Karpenter to dynamically provision your runners on spot instances
In this tutorial, I will show how to:
- 
    
Run self-hosted runners with Github Actions on AWS spot instances.
 - 
    
Dynamically add/remove resources to your k8s cluster by Karpenter.
 
This guide is continuation of previous tutorial, please make sure you read it and have cluster with installed ARC.
Github Actions published guides:
- 
    
GitOps way with Github Actions and self-hosted runner on Kubernetes
 - 
    
Automatic scaling with Github Actions and self-hosted runners
 - 
    
Github Actions with k8s and Karpenter to dynamically provision your runners on spot instances
 - 
    
Use OpenID Connect with Github Actions to authenticate with Amazon Web Services
 
What is Karpenter?
https://karpenter.sh/
Karpenter automatically launches just the right compute resources to handle your cluster’s applications. It is designed to let you take full advantage of the cloud with fast and simple compute provisioning for Kubernetes clusters.
Let’s do it
I will use EKS cluster for demo
Prerequisites
- AWS account with all needed permissions to create EKS cluster
 - Installed kubectl and eksctl
 - Installed AWS CLI
 
Provision of EKS cluster
You can do it by Terraform or using eksctl, to make it simple I will use eksctl
Let’s provision the EKS cluster
https://docs.aws.amazon.com/eks/latest/userguide/create-cluster.html
Provision of EKS cluster to the ‘eu-west-1’ region, cluster name: eks-gh-runners
$ eksctl create cluster \
 --name eks-gh-runners \
 --region eu-west-1Install Cert Manager and ARC to EKS
Read my previous post for detailed explanation
- 
    
You need to Generate PAT
 - 
    
Install Cert Manager
 
helm repo add jetstack https://charts.jetstack.io
helm repo update
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.11.0/cert-manager.crds.yaml
helm install \
  cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --version v1.11.0 - Create k8s secret for ARC from Github’s Personal access token
 
kubectl create ns actions-runner-system
kubectl create secret generic controller-manager \
    -n actions-runner-system \
    --from-literal=github_token=${GITHUB_TOKEN} - Install ARC
 
helm repo add actions-runner-controller https://actions-runner-controller.github.io/actions-runner-controller
helm upgrade --install --namespace actions-runner-system --create-namespace \
             --wait actions-runner-controller actions-runner-controller/actions-runner-controllerInstall Karpenter to EKS
https://archive.eksworkshop.com/beginner/085_scaling_karpenter/setup_the_environment/
Set the following environment variable to the Karpenter version you would like to install.
export CLUSTER_NAME=eks-gh-runners
export KARPENTER_VERSION=v0.20.0
export CLUSTER_ENDPOINT="$(aws eks describe-cluster --name ${CLUSTER_NAME} --query "cluster.endpoint" --output text)"
export ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)
export TEMPOUT=$(mktemp)Create the ‘KarpenterNode’ IAM Role
curl -fsSL https://karpenter.sh/"${KARPENTER_VERSION}"/getting-started/getting-started-with-eksctl/cloudformation.yaml  > $TEMPOUT \
&& aws cloudformation deploy \
  --stack-name "Karpenter-${CLUSTER_NAME}" \
  --template-file "${TEMPOUT}" \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameter-overrides "ClusterName=${CLUSTER_NAME}"
Grant access to instances using the profile to connect to the cluster
eksctl create iamidentitymapping \
  --username system:node: \
  --cluster  ${CLUSTER_NAME} \
  --arn "arn:aws:iam::${ACCOUNT_ID}:role/KarpenterNodeRole-${CLUSTER_NAME}" \
  --group system:bootstrappers \
  --group system:nodes
Before adding the IAM Role for the service account we need to create the IAM OIDC Identity Provider for the cluster.
eksctl utils associate-iam-oidc-provider --cluster ${CLUSTER_NAME} --approveKarpenter requires permissions like launching instances. This will create an AWS IAM Role, Kubernetes service account, and associate them using IAM Roles for Service Accounts (IRSA)
eksctl create iamserviceaccount --override-existing-serviceaccounts \
  --cluster "${CLUSTER_NAME}" --name karpenter --namespace karpenter \
  --role-name "${CLUSTER_NAME}-karpenter" \
  --attach-policy-arn "arn:aws:iam::${ACCOUNT_ID}:policy/KarpenterControllerPolicy-${CLUSTER_NAME}" \
  --role-only \
  --approveCreate the EC2 Spot Linked Role
aws iam create-service-linked-role --aws-service-name spot.amazonaws.com 2> /dev/null || echo 'Already exist'Install Karpenter Helm Chart
export CLUSTER_ENDPOINT="$(aws eks describe-cluster --name ${CLUSTER_NAME} --query "cluster.endpoint" --output text)"
helm upgrade --install --namespace karpenter --create-namespace \
  karpenter oci://public.ecr.aws/karpenter/karpenter \
  --version ${KARPENTER_VERSION} \
  --set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"=${KARPENTER_IAM_ROLE_ARN} \
  --set settings.aws.clusterName=${CLUSTER_NAME} \
  --set settings.aws.clusterEndpoint=${CLUSTER_ENDPOINT} \
  --set defaultProvisioner.create=false \
  --set settings.aws.defaultInstanceProfile=KarpenterNodeInstanceProfile-${CLUSTER_NAME} \
  --set settings.aws.interruptionQueueName=${CLUSTER_NAME} \
  --wait
As you see everything deployed successfully! :-)
Create and deploy Karpenter’s provisioner for AWS Spot Instances
Provisioner Overview:
- Provider will spin up nodes from security group and subnet with ‘alpha.eksctl.io/cluster-name: eks-gh-runners’ tag, check you have those tags.
 

- 
    
Spots will be provisioned from ‘medium’ type and ‘amd64’ architecture.
 - 
    
Node will have ‘instance-type: spot-ci’ label, which I am using with ‘nodeSelector’ for runners, in order to deploy them only to spots provisioned with Karpenter.
 
apiVersion: karpenter.sh/v1alpha5
kind: Provisioner
metadata:
  name: default
spec:
  labels:
    instance-type: spot-ci
  limits:
    resources:
      cpu: 1k
  provider:
    securityGroupSelector:
      alpha.eksctl.io/cluster-name: eks-gh-runners
    subnetSelector:
      alpha.eksctl.io/cluster-name: eks-gh-runners
    tags:
      alpha.eksctl.io/cluster-name: eks-gh-runners
  requirements:
  - key: node.kubernetes.io/instance-type
    operator: In
    values: ["t2.medium", "t3.medium", "t3a.medium"]
  - key: karpenter.sh/capacity-type
    operator: In
    values:
    - spot
  - key: kubernetes.io/arch
    operator: In
    values:
    - amd64
  ttlSecondsAfterEmpty: 30git clone [email protected]:warolv/github-actions-series.git
cd gh-karpenter
kubectl apply -f hra.yamlDeploying self-hosted runners with HRA
Read my previous post for detailed explanation
In order for scaling test to work, need to tune runner’s requests first.
I want to run one self-hosted runner per worker node.
Karpenter will provision spots of medium type with 2 CPU and 4Gb of memory only, meaning I need to set CPU requests of runner’s pod more than 1 and less than 2 CPU, let’s pick 1.5 CPU to run a runner per worker node.
I have only 2 worker nodes, this way Karpenter need to provision additional capacity for runners.
As you see I changes requests to 1.5 CPU and 3Gb.
hra.yaml
apiVersion: actions.summerwind.dev/v1alpha1
kind: RunnerDeployment
metadata:
  name: sh-runner-deployment
spec:
  template:
    spec:
      repository: warolv/github-actions-series
      labels:
      - sh-k8s-runner
      resources:
        requests:
          cpu: 1.5
          memory: 3Gb
      nodeSelector: 
        instance-type: spot-ci
---
apiVersion: actions.summerwind.dev/v1alpha1
kind: HorizontalRunnerAutoscaler
metadata:
  name: sh-runner-deployment-autoscaler
spec:
  scaleTargetRef:
    kind: RunnerDeployment
    name: sh-runner-deployment
  minReplicas: 1
  maxReplicas: 4
  metrics:
  - type: TotalNumberOfQueuedAndInProgressWorkflowRuns
    repositoryNames:
    - warolv/github-actions-seriesgit clone [email protected]:warolv/github-actions-series.git
cd gh-karpenter
kubectl apply -f hra.yaml
Runner will be deployed on spot provisioned with Karpenter only, if configuration is ok, you will see in logs:
kubectl logs karpenter-bd845d99-lr7n4 -n karpenter

Testing dynamic provisioning of new resources with scale test
For testing purposes will be used scale test from previous guide:
https://raw.githubusercontent.com/warolv/github-actions-series/master/.github/workflows/scale_test.yaml
name: Scale test
on:
  workflow_dispatch:
jobs:
  scale-test1:
    runs-on: sh-k8s-runner
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '14'
      - run: npm install -g bats
      - run: bats -v
      - run: sudo apt update -y
      - name: Run a one-line script
        run: echo scale test 1 finished!
  scale-test2:
    runs-on: sh-k8s-runner
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '14'
      - run: npm install -g bats
      - run: bats -v
      - run: sudo apt update -y
      - name: Run a one-line script
        run: echo scale test 2 finished!
  scale-test3:
    runs-on: sh-k8s-runner
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '14'
      - run: npm install -g bats
      - run: bats -v
      - run: sudo apt update -y
      - name: Run a one-line script
        run: echo scale test 3 finished!
  scale-test4:
    runs-on: sh-k8s-runner
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '14'
      - run: npm install -g bats
      - run: bats -v
      - run: sudo apt update -y
      - name: Run a one-line script
        run: echo scale test 4 finished!git clone [email protected]:warolv/github-actions-series.git
cd .github/workflows
kubectl apply -f hra.yamlYou must push this workflow to your repo, to ‘.github/workflows’ folder.
It’s time to run Scale Test
Before you executed ‘scale test’, you have 3 nodes, 2 provisioned on EKS with eksctl, when the cluster is created.
3rd node provisioned with Karpenter to run first runner.




After some time, around 1 min you can see additional capacity is provisioned by Karpenter :-)
After workflow is finished, worker nodes under Karpenter’s management will be removed.
Works!
Thank you for reading, I hope you enjoyed it, see you in the next post.
I also will create Youtube video for this tutorial on my YT channel, please subscribe!
If you want to be notified when the next post of this tutorial is published, please follow me on Twitter @warolv.
You can get all tutorials of Gihub Action from my github repo by cloning it: ‘git clone https://github.com/warolv/github-actions-series.git‘