559 lines
18 KiB
Markdown
559 lines
18 KiB
Markdown
+++
|
|
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.
|