A Forgejo Runner plugin that spawn jobs as Kubernetes pods
kubernetes ci forgejo runner
kuranto docs manual_deployment.md
10 kB · 301 lines
Markdown
at main

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.