+++ categories = ["software"] tags = ["automation","docker","git","devops","kubernetes"] date = 2025-05-08T12:50:00-05:00 description = "" draft = false slug = "gitea-actions-kubernetes" title = "▶️ Gitea Actions & Kubernetes" author = "nicholas" +++ I currently use **Jenkins** to automate the build and deployment of my [personal website](https://nicholas.uuard.com) and my [blog site](https://log.nicholas.uuard.com). I will experiment with a **Gitea Actions** workflow to see if I can accomplish the same thing in Gitea. This process should be familiar to me since the workflow I configure in this post will more or less mirror the behavior of my Jenkins setup: an independent service (e.g., Jenkins agent, Gitea runner) executes steps defined in a repo configuration file (e.g., `Jenkinsfile`, `demo.yml`). > I wrote a post for each of my other Jenkins pipelines. > - [🚀 Hugo Site Deployment]({{< relref "posts/jenkins-deploy-web-log">}}) > - [🤵🏻 Personal Website Deployment]({{< relref "posts/jenkins-deploy-website">}}) ## Generate Registration token for runner I will first need to generate a **authentication / identification** token to register with the Gitea server. This is very simple using the web app GUI. I click the button **Create new Runner** and it generates the registration token for me. I will use this token later. {{< image src="images/runnner-registration-token.png" caption="Generate registration token for gitea runner" >}} I *could* have generated this using `gitea actions generate-runner-token` command inside the container, which would give me more control over the scope of the runner (e.g. global, org, repo scope) but I do not care such minutiae right now. ## Create Gitea runner container Next I need to configure and run the **runner container**. To do this I will: - modify `docker-compose.yml` to include `gitea/act_runner` - add environment variables for `REGISTRATION_TOKEN`, etc. - restart the services. #### act_runner Gitea provides an official image `gitea/act_runner`. For testing purposes, I will not need anything more than this and the basic ubuntu-based image `ubuntu-latest`. This runner will be capable of running GitHub Actions-like workflows in Gitea -- it even uses the same [syntax](https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions) as GitHub Actions. Later, I will configure a workflow using this syntax that defines the "actions" the runner will perform on my repository. Next I add a `runner` service to my `docker-compose.yml` file. Before, there were only 2 containers in the application services: `db` and `gitea`. I have added a third: `gitea_runner`: ```yaml networks: gitea: external: false services: gitea: image: gitea/gitea:latest container_name: gitea environment: - USER_UID=1000 - USER_GID=1000 - GITEA__database__DB_TYPE=mysql - GITEA__database__HOST=db:3306 - GITEA__database__NAME=gitea - GITEA__database__USER=gitea - GITEA__database__PASSWD=${MYSQL_PASSWORD} restart: always networks: - gitea volumes: - ./data:/data #- ./logs:/var/lib/gitea/log - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro ports: - "${GITEA_PORT}:3000" depends_on: - db healthcheck: test: [ "CMD", "curl", "-f", "http://localhost:3000/api/healthz" ] interval: 10s timeout: 5s retries: 5 db: image: mysql:8 container_name: gitea_db restart: always environment: - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} - MYSQL_USER=gitea - MYSQL_PASSWORD=${MYSQL_PASSWORD} - MYSQL_DATABASE=gitea networks: - gitea volumes: - ./mysql:/var/lib/mysql runner: image: gitea/act_runner container_name: gitea_runner restart: always depends_on: gitea: condition: service_healthy restart: true volumes: - ./data/act_runner:/data - /var/run/docker.sock:/var/run/docker.sock environment: - GITEA_INSTANCE_URL=${GITEA_INSTANCE_URL} - GITEA_RUNNER_REGISTRATION_TOKEN=${GITEA_RUNNER_REGISTRATION_TOKEN} - GITEA_RUNNER_NAME=${GITEA_RUNNER_NAME} ``` ## Configure a demo workflow Now I need to configure a test workflow using the GitHub Actions syntax I mention above. Gitea provides a demo workflow that I will use to verify that I can successfully run a workflow in a runner container: ```yaml name: Gitea Actions Demo run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀 on: [push] jobs: Explore-Gitea-Actions: runs-on: ubuntu-latest steps: - run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event." - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!" - run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}." - name: Check out repository code uses: actions/checkout@v4 - run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner." - run: echo "🖥️ The workflow is now ready to test your code on the runner." - name: List files in the repository run: | ls ${{ gitea.workspace }} - run: echo "🍏 This job's status is ${{ job.status }}." ``` I place this `demo.yml` file in my `proving-ground` repository: ```plaintext proving-ground/ ├── 📂.gitea/ │ └── 📂workflows/ │ └── demo.yml ``` Before I push this workflow to the repository, I need to finish setting up the runner. Then, once I push the new code, Gitea Actions should trigger the workflow, according to the line `on: [push]` ## Runner test I bring down the current Gitea application by running `docker compose down` and start it up again (this time including the new runner) with `docker compose up -d`. I navigate to the **Gitea Runners** in the web interface to verify that the runner is registered and running correctly: {{< image src="images/gitea-runner.jpg" caption="Create runner" >}} **✅It works**. --- Now I can commit and push my new workflow `test.yml` and verify that the runner can perform Gitea Actions. After I push, I navigate to: **nicholas/proving-ground** → **Actions** to view the latest run: {{< image src="images/test-workflow-running.jpg" caption="Test workflow - running" >}} {{< image src="images/test-workflow-success.jpg" caption="Test workflow - success" >}} **✅ It works** --- The runner executed each step defined in the workflow configuration file. Next I will configure a new workflow to perform a more useful automation. ## Build, Deploy The next step is to **build** and **deploy** to a web server. There are many ways to accomplish this depending on how I choose to architect my infrastructure. Since I am only deploying a static website and all work is being performed on the same machine, it would be simple to transfer the site files locally between host and containers. However, I will make things more interesting by artificially introducing *arguably* unnecessary complexity to the solution. This will move me closer to DevOps pipeline that would be seen in production. ### Outline The pipeline will look something like this: - Push changes to site repo, `master` branch - Trigger Action - Pull `master` branch - **build** a Docker image with new content - **push** Docker image to container registry - `k3s` deploys containerized web site --- ###### k3s / Kubernetes container When my action runner connects to the Kubernetes API at `https://server:6443` (as per `kubeconfig.yml`), it verifies the server's TLS certificate. If the hostname in the `kubeconfig.yml` ("server") does not match any SAN in the cert, the connection will fail with a TLS verification error. I can explicitly add "server" as a valid domain in the TLS cert to prevent this error. `command: server --tls-san "server"` ```yaml k3s_server: image: "rancher/k3s:${K3S_VERSION:-latest}" container_name: k3s_server command: server --tls-san "server" tmpfs: - /run - /var/run ulimits: nproc: 65535 nofile: soft: 65535 hard: 65535 privileged: true restart: always environment: - K3S_TOKEN=${K3S_TOKEN:?err} #- K3S_KUBECONFIG_OUTPUT=/output/kubeconfig.yaml - K3S_KUBECONFIG_MODE=666 networks: - cicd volumes: - k3s-server:/var/lib/rancher/k3s - ./k3s:/output ports: - 6443:6443 # Kubernetes API Server - ${INGRESS_CONTROLLER_PORT_HTTP}:80 # Ingress controller port 80 - ${INGRESS_CONTROLLER_PORT_HTTPS}:443 # Ingress controller port 443 ``` --- ###### Create static web page I create a sample static web page `index.html` that I will serve in my nginx container. ```plaintext 📂site/ ├── index.html ``` ```html
The Cosmos is all that is or was or ever will be. Our feeblest contemplations of the Cosmos stir us — there is a tingling in the spine, a catch in the voice, a faint sensation, as if a distant memory, of falling from a height. We know we are approaching the greatest of mysteries.
--- ``` ###### Create Dockerfile Next I will create the Dockerfile. This file will define a container that will serve the static web page `index.html` using nginx. The Dockerfile will remove all of the default files that ship with `nginx:alpine` and replace them with the contents of my `site` directory, which contains only `index.html`. **`Dockerfile`** ```Dockerfile FROM nginx:alpine RUN rm -rf /usr/share/nginx/html/* COPY ./site/ /usr/share/nginx/html/ EXPOSE 80 ``` --- ###### Set up credentials for image registry (gitea) One step in the workflow involves pushing the newly-built application Docker image to my registry. To push, I will need to authenticate with the Gitea server. To do this, I will use a GitHub action called `login-action@v3` found in the [GitHub Actions marketplace](https://github.com/marketplace?type=actions). This action requires a few inputs to work: - Registry URL - Username - Password (access token) The registry URL and username inputs are straightforward and I can pass these variables along as strings. The access token will need to be passed as a secret. First I generate an access token in Gitea. I will name this `gitea-package-registry` and give it only the package write permission. {{< image src="images/gitea-access-token-generate.png" caption="Generate registry token" >}} Next I will store this as a secret so that I can securely pass it to the runner. I name the secret `REGISTRY_TOKEN` {{< image src="images/gitea-access-token-secret.png" caption="Store registry token secret" >}} {{< image src="images/registry-token-created.png" caption="Gitea registry token" >}} I can reference this secret in my workflow file `${{ secrets.REGISTRY_TOKEN }}`. --- ###### Store kubeconfig as Gitea secret My workflow file needs access to one more secret: my `kubeconfig.yml`. Storing this as a secret is not as straightforward as the registry access token, since I cannot store a file directly as a secret. I will need encode the file as a base64 string, store *that* string as a secret named `KUBECONFIG_DATA`, and decode the string back into the `kubeconfig.yml` file. First, my k3s container is configured to automatically produce a `kubeconfig.yml` and output it to the specified directory. I have included the relevant configuration from my `docker-compose.yml` file as context. **`docker-compose.yml` snippet** ```yaml environment: - K3S_KUBECONFIG_OUTPUT=/output/kubeconfig.yml volumes: - ./k3s:/output ``` On my host machine, I can locate the generated `kubeconfig.yml` file at `./k3s/kubeconfig.yaml`. Next, I run the following command to base64-encode it and write the result to `kubeconfig.b64`. ```shell base64 -w 0 kubeconfig.yaml > kubeconfig.b64 ``` The contents of `kubeconfig.b64` is a string that will be stored as a secret in Gitea: {{< image src="images/kubeconfig-data.png" caption="Base64 encoded secret" >}} I will reference this secret in my **Gitea Action workflow**, which is defined in yet another yaml file-- this one stored in the same `workflows` directory as the `demo.yml` created in earlier step, during initial stages of setting up the pipeline. ```plaintext 📂proving-ground/ └─📂.gitea/ └─📂workflows/ ├─demo.yml └─deploy.yml ``` --- ###### Action Workflow file - deploy static web page The workflow file defines several steps that will compose the deployment process: - **⬇️Checkout** app repository (`proving-ground`) - **🔐Login** to Gitea package registry - **🐋Build** Docker image from Dockerfile - **🚀Push** new image to package registry - **🛠️Install** `kubectl` - **🔑Decode** `KUBECONFIG_DATA` secret and write local **`kubeconfig.yml`** file; Use this file as `KUBECONFIG` env. - **🚀Test / Deploy** to Kubernetes cluster via API - **🧹Prune** images The `kubectl` CLI utility uses my `kubeconfig.yml` file to authenticate and connect to the Kubernetes cluster in the k3 container. It communicates with the Kubernetes API over HTTP/HTTPS (`https://server:6443`). **deploy.yml** ```yaml name: Deployment on: push: branches: - master workflow_dispatch: jobs: deploy-site: name: 'deploy' env: REGISTRY_USERNAME: nicholas IMAGE_REGISTRY: git.uuard.com IMAGE_NAME: nicholas/proving-ground IMAGE_TAG: latest runs-on: ubuntu-latest steps: - name: ⬇️ Checkout repo uses: actions/checkout@v4 - name: 🔐 Gitea Registry Login uses: docker/login-action@v3 with: registry: ${{ env.IMAGE_REGISTRY }} username: ${{ env.REGISTRY_USERNAME }} password: ${{ secrets.REGISTRY_TOKEN }} - name: 🐋 Build Docker image run: docker build -t $IMAGE_REGISTRY/$IMAGE_NAME:$IMAGE_TAG ./container - name: 🚀 Push Docker image run: docker push $IMAGE_REGISTRY/$IMAGE_NAME:$IMAGE_TAG - name: 🛠️ Setup kubectl uses: azure/setup-kubectl@v4 with: version: 'latest' - name: 🔑 Configure Kubeconfig run: | echo "${{ secrets.KUBECONFIG_DATA }}" | base64 -d > kubeconfig.yml - name: 🧪 Test access env: KUBECONFIG: kubeconfig.yml run: kubectl get nodes - name: 🚀 Deploy to k3s env: KUBECONFIG: kubeconfig.yml run: | kubectl apply -f k8s/deployment.yml kubectl apply -f k8s/service.yml kubectl apply -f k8s/ingress.yml kubectl rollout restart deployment proving-ground kubectl rollout status deployment/proving-ground - name: 🧹 Cleanup images if: always() run: docker image prune -f ``` --- ###### Diagram This diagram shows the relationship between the various containers. ```mermaid graph TD subgraph host[Host] subgraph docker[Docker] subgraph kubernetes[Kubernetes / k3s] subgraph pod[Pod] container["NGINX"] end end gitea-server[Gitea Server] gitea-runner[Gitea Action Runner] gitea-server <--> gitea-runner gitea-runner --Kubernetes API--> kubernetes end end ``` --- ###### Kubernetes deployment manifest My Kubernetes manifest defines a basic static website deployment using the NGINX container image `proving-ground:latest`. The manifest configuration includes three resource types: - **Deployment** (`deployment.yml`) - `kubectl apply -f k8s/deployment.yml` - Runs the NGINX container inside a pod - **Service** (`service.yml`) - `kubectl apply -f k8s/service.yml` - Exposes the pod containing the NGINX app, makes it accessible inside the cluster - **Ingress** (`ingress.yml`) - `kubectl apply -f k8s/ingress.yml` - Configures routing rules to expose NGINX app externally **deployment.yaml** ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: proving-ground labels: app: proving-ground-static-site spec: replicas: 1 selector: matchLabels: app: proving-ground-static-site template: metadata: labels: app: proving-ground-static-site spec: containers: - name: nginx image: git.uuard.com/nicholas/proving-ground:latest ports: - containerPort: 80 ``` **ingress.yml** ```yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: proving-ground-static-site-ingress spec: rules: - host: server http: paths: - path: / pathType: Prefix backend: service: name: proving-ground-static-site-service port: number: 80 ``` **service.yml** ```yaml apiVersion: v1 kind: Service metadata: name: proving-ground-static-site-service spec: selector: app: proving-ground-static-site ports: - protocol: TCP port: 80 targetPort: 80 type: LoadBalancer ``` --- ###### Site repo directory structure This is the complete repository directory structure. ``` 📁proving-ground ├─📁.gitea │ └─📁workflows │ ├─demo.yml │ └─deploy.yml ├─📁container │ ├─Dockerfile │ └─📁site │ └─index.html └─📁k8s ├─deployment.yml ├─ingress.yml └─service.yml ``` --- ###### Test Kubernetes API / kubectl Now I can test the Kubernetes connection by running `kubectl get nodes`. If this step succeeds, I have verified the connection between the runner container and Kubernetes. I can trigger this build by either pushing to `master` or manually triggering the build via Gitea UI. {{< image src="images/k3s-test-success.png" caption="Deployment test success" >}} **✅ It works** --- ###### Deploy Now I deploy the app. I simply push changes to the repository, and voilà, my app is live. {{< image src="images/k3s-deploy-success.png" caption="Deployment success" >}} {{< image src="images/view-site-deployment.png" caption="View deployment in browser" >}} **🎉 It works!** Done.