Clean history, git LFS
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user