Clean history, git LFS

This commit is contained in:
2025-07-05 15:36:34 +00:00
commit 70423ce723
141 changed files with 8484 additions and 0 deletions
@@ -0,0 +1,558 @@
+++
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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Hello, World!</title>
<style>
/*...*/
</style>
</head>
<body>
<h1>Behold</h1>
<p>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. </p>
</body>
</html>
---
```
###### 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.