Manual deployment of Kuranto #
Kuranto works as a plugin to the official Forgejo Runner, so it runs alongside it.
This guide will walk through a manual deployment of the runner and the plugin.
Prerequisites #
- A Forgejo instance with Actions enabled
- A Kubernetes cluster
Setup #
Registering the runner #
To begin with, you'll need to register a new runner in Forgejo, you can follow the instructions available in the official docs for this.
Note the UUID and token you receive from Forgejo somewhere, we'll need them later.
Creating a runner namespace #
Ideally you should use a dedicated namespace for the runner, this can easily be done with:
kubectl create namespace <NAME>
The rest of this guide will assume the namespace is named forgejo-actions,
adapt the commands with your own namespace name accordingly.
Creating the Forgejo Runner config #
Create a file named config.yml somewhere on your computer, with the following
contents:
log:
level: info
job_level: info
runner:
capacity: 1
labels:
- "kubernetes:kubernetes://data.forgejo.org/oci/node:lts"
plugins:
kubernetes:
address: "127.0.0.1:50051"
options:
namespace: "forgejo-actions"
server:
connections:
primary:
url: "https://forgejo.example.invalid/"
uuid: "REPLACE_WITH_REGISTERED_RUNNER_UUID"
token: "REPLACE_WITH_REGISTERED_RUNNER_TOKEN"
In this file, adapt the namespace in plugin.kubernetes.options.namespace to the
one you've created. You will also need to replace the uuid, token and url
values in server.connections.primary with the UUID and token you got during the
runner registration step earlier, and the URL of your Forgejo instance, respectively.
Note: this configuration file is intentionally very minimal for the purposes of this guide. You can adapt it to your own liking.
The label deserves a little bit of a mention here: it is named kubernetes, and
then specifies that it will use the kubernetes plugin with the data.forgejo.org/oci/node:lts
image. You can change the label name and the image, but the plugin name needs to
match the one set in the plugins section below it.
Once you have this file stored on your computer, you can create a secret in your Kubernetes cluster to store this, so the runner can then retrieve it and use it. We're using a secret instead of a ConfigMap since this configuration contains a token that can access your Forgejo instance.
To create the secret, use the following command:
kubectl -n forgejo-actions create secret generic forgejo-runner-config \
--from-file=config.yml
RBAC #
We're going to give permissions to Kuranto to do what it needs to in the Kubernetes cluster, and also make sure that the pods we start for jobs have no permissions in the cluster.
We'll start by creating two service accounts:
kubectl -n forgejo-actions create serviceaccount kuranto
kubectl -n forgejo-actions create serviceaccount forgejo-actions-job
The first one, we'll give some permissions to, the second one we'll keep as-is without any role or permissions.
Now we can create the role for the kuranto service account. This is best done
using a YAML file we can apply with kubectl, so let's create this role.yml file:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: kuranto
namespace: forgejo-actions
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["create", "get", "list", "watch", "delete"]
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["get", "create"]
Then apply it to your cluster:
kubectl -n forgejo-actions apply -f role.yml
And finally, we can bind this role to the service account we created previously:
kubectl -n forgejo-actions create rolebinding kuranto \
--serviceaccount=forgejo-actions:kuranto \
--role=kuranto
With this done, we're now done setting up the permissions for our runner, let's get into actually deploying it.
Deploying Kuranto #
We're going to create a Deployment in Kubernetes that contains both the Forgejo Runner, as well as Kuranto.
Let's start by creating a file named deployment.yml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: forgejo-runner
namespace: forgejo-actions
labels:
app.kubernetes.io/name: forgejo-runner
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: forgejo-runner
template:
metadata:
labels:
app.kubernetes.io/name: forgejo-runner
spec:
# This is using our own service account, where we set the exact permissions
# needed by Kuranto to run correctly.
automountServiceAccountToken: false
serviceAccountName: kuranto
containers:
- name: runner
image: code.forgejo.org/forgejo/runner:13.2.0
command: ["forgejo-runner"]
args: ["daemon", "--config", "/etc/forgejo-runner/config.yml"]
volumeMounts:
- name: runner-config
mountPath: /etc/forgejo-runner
readOnly: true
- name: plugin
# In a real deployment, please use tagged versions instead of `:latest`
image: blurrycat/kuranto:latest
env:
# This must be the same value that is configured in the runner
# configuration file we created at the beginning.
- name: KURANTO_KUBERNETES_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
# Set to debug for detailed Kuranto logs; defaults to info.
- name: KURANTO_LOG_LEVEL
value: info
# This is the service account we created with no permissions.
- name: KURANTO_JOB_SERVICE_ACCOUNT
value: forgejo-actions-job
# This is not very important here, it is only used if the label and
# the job themselves do not specify an image to use. In this case the
# label we set in the config does.
- name: KURANTO_DEFAULT_JOB_IMAGE
value: data.forgejo.org/oci/node:lts
# Set requests and limits for every generated job container. Choose
# values appropriate for the cluster's nodes and expected workloads.
- name: KURANTO_JOB_CPU_REQUEST
value: 250m
- name: KURANTO_JOB_MEMORY_REQUEST
value: 512Mi
- name: KURANTO_JOB_CPU_LIMIT
value: "2"
- name: KURANTO_JOB_MEMORY_LIMIT
value: 1Gi
# Optional resource requests and limits for each generated service
# container; leave any value unset to omit it.
# - name: KURANTO_SERVICE_CPU_REQUEST
# value: 100m
# - name: KURANTO_SERVICE_MEMORY_REQUEST
# value: 128Mi
# - name: KURANTO_SERVICE_CPU_LIMIT
# value: 500m
# - name: KURANTO_SERVICE_MEMORY_LIMIT
# value: 512Mi
# Optional Pod-level resources. These require Kubernetes 1.34+.
# - name: KURANTO_POD_CPU_REQUEST
# value: "1"
# - name: KURANTO_POD_MEMORY_REQUEST
# value: 2Gi
# - name: KURANTO_POD_CPU_LIMIT
# value: "2"
# - name: KURANTO_POD_MEMORY_LIMIT
# value: 4Gi
ports:
- name: grpc
containerPort: 50051
readinessProbe:
grpc:
port: 50051
initialDelaySeconds: 2
livenessProbe:
grpc:
port: 50051
initialDelaySeconds: 5
volumeMounts:
- name: kubernetes-api
mountPath: /var/run/secrets/kubernetes.io/serviceaccount
readOnly: true
volumes:
# This is the config we created earlier for the Forgejo Runner
- name: runner-config
secret:
secretName: forgejo-runner-config
- name: kubernetes-api
projected:
sources:
- serviceAccountToken:
path: token
expirationSeconds: 3600
- configMap:
name: kube-root-ca.crt
items:
- key: ca.crt
path: ca.crt
- downwardAPI:
items:
- path: namespace
fieldRef:
fieldPath: metadata.namespace
Read the comments in the file to understand what everything does, and change what you need.
With this file in hand, we can go ahead and create our Deployment in our cluster:
kubectl -n forgejo-actions apply -f deployment.yml
And with that done, we're done with the setup! The only thing left to do is to trigger a workflow in your Forgejo, and watch it execute in your Kubernetes cluster.
Since the label we set on the runner here is kubernetes, your jobs will need
to be configured with a runs-on: kubernetes to be picked up by this runner.
Job Pod scheduling #
To configure placement for all generated job Pods, add JSON environment variables
to the plugin container's env list in deployment.yml. For example:
- name: KURANTO_JOB_NODE_SELECTOR
value: '{"example.com/workload":"ci"}'
- name: KURANTO_JOB_AFFINITY
value: >-
{"nodeAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"preference":{"matchExpressions":[{"key":"topology.kubernetes.io/zone","operator":"In","values":["zone-a"]}]}}]}}
- name: KURANTO_JOB_TOLERATIONS
value: '[{"key":"example.com/workload","operator":"Equal","value":"ci","effect":"NoSchedule"}]'
This example requires the node label example.com/workload=ci, prefers nodes in
zone-a, and tolerates the taint example.com/workload=ci:NoSchedule. Replace these
labels and values with those used by your cluster. Tolerations permit use of tainted
nodes, but do not attract Pods to them or reserve them for jobs. Affinity uses the
native Kubernetes structure, supporting node affinity and Pod affinity/anti-affinity.
If both nodeSelector and required nodeAffinity are set, both must match.