---
tags: flux, fluxcd.io, cd, continuous delivery, charm.sh, soft-serve, kubernetes, k8s, docker
lang: en
robots: index, follow
---
# Flux with Kind and Soft Serve
This article covers getting a local [Kind](kind.sigs.k8s.io "Kind Website") cluster
setup with [Flux](fluxcd.io "Flux Website") and the self hosted git server
[Soft Serve](github.com/charmbracelet/soft-serve "Soft Serve GitHub").
## Requirements
You will need to have the following installed:
- `docker` [installation](docs.docker.com/get-docker/)
- `kubectl` [installation](kubernetes.io/docs/tasks/tools/#kubectl)
- `kustomize` [installation](kubectl.docs.kubernetes.io/installation/kustomize/)
- `flux` [installation](fluxcd.io/docs/installation/ "Install Flux")
- `kind` [installation](kind.sigs.k8s.io/#installation-and-usage "Install Kind")
- `soft` [installation](github.com/charmbracelet/soft-serve#installation "Install Soft Serve")
### Assumptions
I am assuming the following things:
- Running on Linux
- Familiar with:
- Docker
- YAML
- Git
- Kind
- kubectl
## Let's get Started
We need to run two copies of Soft Serve in order to be able to access and configure
the software.
### Docker Run Soft Serve
We're going to start a local copy of Soft Serve in preparation for running a copy
within the Kind cluster. Let's assume that your Flux repository is located in `/home/nalum/code/flux`.
You will want to setup a new environment variable: `export PATH_TO_CODE="/home/nalum/code"`.
This will be used to mount your local code to both the local copy and the copy running
in the Kind cluster.
Let's get a running instance of Soft Serve going with Docker:
```bash
❯ docker run -d \
--name=soft-serve \
--volume=${PATH_TO_CODE}:/soft-serve \
--publish=23231:23231 \
--restart=unless-stopped \
--user=$(id -u):$(id -g) \
--add-host=host.docker.internal:host-gateway \
charmcli/soft-serve:latest
```
This will start the container and then create a few directories
that are used by Soft Serve as follows:
```bash
❯ cd ${PATH_TO_CODE}
❯ tree -d -L 1
.
├── flux
├── repos # Created by Soft Serve
└── ssh # Created by Soft Serve
```
You should also be able to ssh to the Docker Soft Serve:
```bash
❯ ssh localhost -p 23231
```
And you should be presented with a TUI looking something like this:
![Soft Serve SSH TUI showing Home and brief message to clone the configuration repository](https://i.imgur.com/sRzCm5x.png)
At this point we have the basics of what we need. You will want to clone the configuration
repository from Soft Serve:
```bash
❯ git clone ssh://localhost:23231/config
Cloning into 'config'...
remote: Enumerating objects: 38, done.
remote: Counting objects: 100% (38/38), done.
remote: Compressing objects: 100% (38/38), done.
remote: Total 38 (delta 11), reused 0 (delta 0), pack-reused 0
Receiving objects: 100% (38/38), 11.98 KiB | 5.99 MiB/s, done.
Resolving deltas: 100% (11/11), done.
```
You can now configure (we'll get to this later) Soft Serve to be ready for when it's
deployed on the Kind cluster. The configuration is done with YAML and the following
is the starting config created by running the software:
```yaml=
# The name of the server to show in the TUI.
name: Soft Serve
# The host and port to display in the TUI. You may want to change this if your
# server is accessible from a different host and/or port that what it's
# actually listening on (for example, if it's behind a reverse proxy).
host: localhost
port: 23231
# Access level for anonymous users. Options are: read-write, read-only and
# no-access.
anon-access: read-write
# You can grant read-only access to users without private keys. Any password
# will be accepted.
allow-keyless: false
# Customize repo display in the menu. Only repos in this list will appear in
# the TUI.
repos:
- name: Home
repo: config
private: true
note: "Configuration and content repo for this server"
# users:
# - name: Admin
# admin: true
# public-keys:
# - KEY TEXT
# - name: Example User
# collab-repos:
# - REPO
# public-keys:
# - KEY TEXT
```
### Setting Up Kind
For the Kind cluster we need to mount the `$PATH_TO_CODE` onto one or all of the
nodes running in the cluster. The following config will setup a cluster one node
and the path mounted for use by containers deployed on that node:
```yaml=
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: dev
nodes:
- role: control-plane
extraMounts:
- hostPath: /home/nalum/code # Same value that is in $PATH_TO_CODE
containerPath: /soft-serve
readOnly: false
selinuxRelabel: false
propagation: None
```
Get your cluster up by running the following (this will change your `kubectl` context):
```bash
❯ kind create cluster --config dev-config.yaml
Creating cluster "dev" ...
✓ Ensuring node image (kindest/node:v1.21.1) 🖼
✓ Preparing nodes 📦
✓ Writing configuration 📜
✓ Starting control-plane 🕹️
✓ Installing CNI 🔌
✓ Installing StorageClass 💾
Set kubectl context to "kind-dev"
You can now use your cluster with:
kubectl cluster-info --context kind-dev
Have a question, bug, or feature request? Let us know! https://kind.sigs.k8s.io/#community 🙂
```
### Kind Run Soft Serve
Now that we've got the a default setup of Soft Serve and the Kind cluster running
it's time to run Soft Serve on the Kind cluster. We will define a `Namespace`,
a `Service` and a `Deployment` to get this running on the Kind cluster.
The `Namespace`:
```yaml=
apiversion: v1
kind: namespace
metadata:
name: soft-serve
labels:
app: soft-serve
```
The `Service`:
```yaml=
apiversion: v1
kind: service
metadata:
name: git
namespace: soft-serve
labels:
app: soft-serve
spec:
type: clusterip
selector:
app: soft-serve
ports:
- name: ssh
port: 23231
targetPort: 23231
protocol: TCP
```
The `Deployment`:
```yaml=
apiVersion: apps/v1
kind: Deployment
metadata:
name: soft-serve
namespace: soft-serve
labels:
app: soft-serve
spec:
replicas: 2
revisionHistoryLimit: 4
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: soft-serve
template:
metadata:
labels:
app: soft-serve
spec:
containers:
- image: charmcli/soft-serve:latest
name: soft-serve
ports:
- containerPort: 23231
name: ssh
protocol: TCP
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /soft-serve
name: repos
readOnly: true
securityContext:
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
volumes:
- name: repos
hostPath:
path: /soft-serve
type: Directory
```
In the `Deployment` we are setting some specific security context information to
make it so the container is running as the user who owns the directory that has been
mounted into the Kind node and which we are mounting into the `Pod`.
When you install a Linux distribution and create the main user for that install it
will likely have the id `1000` for the User and the Group, you can double check this
by running the following commands in your terminal:
```bash
❯ id # Prints the User ID, Group ID and IDs of other groups the user is part of
uid=1000(nalum) gid=1000(nalum) groups=1000(nalum),3(sys),90(network),98(power),962(realtime),964(docker),991(lp),995(audio),998(wheel)
❯ id -u # Only the User ID
1000
❯ id -g # Only the Group ID (this is the main group for this user)
1000
```
This was used earlier when we started Soft Serve with the `docker` command to set
the `user:group` for the container to run as.
Let's apply these resources to the Kind cluster:
```bash
❯ kubectl apply -f soft-serve.yaml
namespace/soft-serve created
service/git created
deployment.apps/soft-serve created
```
Now if you run the command to get the `Pods` in the created `Namespace` you should
see something like the following:
```bash
❯ kubectl -n soft-serve get pods
NAME READY STATUS RESTARTS AGE
soft-serve-786fb9c9f8-2h4pl 1/1 Running 0 12s
soft-serve-786fb9c9f8-vzxgw 1/1 Running 0 12s
```
### Configure Soft Serve
Configuring Soft Serve is pretty simple, go into the config repo we cloned
earlier and edit the `config.yaml` file (cut down file for brevity):
```yaml=
...
host: git.soft-serve
...
repos:
...
- name: Flux
repo: flux
private: false
note: "Flux Kind Cluster Demo running Soft Serve local git server"
...
```
We've changed the host name and added a new repository to the config.
To have this repository appear correctly and be used by Flux within the
`Kind` cluster we need to create a symlink from `${PATH_TO_CODE}/flux` in
`${PATH_TO_CODE}/repos`:
```bash
❯ cd ${PATH_TO_CODE}/repos
❯ ls -lah
total 12K
drwxr-xr-x 3 nalum nalum 4.0K Feb 24 09:18 .
drwxr-xr-x 13 nalum nalum 4.0K Feb 8 13:05 ..
drwxr-xr-x 5 nalum nalum 4.0K Feb 8 13:12 config
❯ ln -s ../flux ./flux
❯ ls -lah
total 12K
drwxr-xr-x 3 nalum nalum 4.0K Feb 24 09:18 .
drwxr-xr-x 13 nalum nalum 4.0K Feb 8 13:05 ..
drwxr-xr-x 5 nalum nalum 4.0K Feb 8 13:12 config
lrwxrwxrwx 1 nalum nalum 7 Feb 24 09:18 flux -> ../flux
```
If you have the repo setup already with a README that should display when
you view it in the SSH TUI, if you don't have a repo setup let's do a quick
setup of git and add a README file to the flux repo, something simple e.g.:
```bash
❯ cd flux
❯ git init
Initialized empty Git repository in /home/nalum/code/flux/.git/
❯ tee README.md <<EOT
heredoc> # Flux Kind Cluster
heredoc>
heredoc> Flux GitRepository Source
heredoc> EOT
# Flux Kind Cluster
Flux GitRepository Source
❯ cat README.md
# Flux Kind Cluster
Flux GitRepository Source
❯ git add README.md
❯ git commit -m "Adding README"
[main (root-commit) b83b100] Adding README
1 file changed, 3 insertions(+)
create mode 100644 README.md
```
In order to see this change in the SSH TUI you will need to restart the docker
container e.g. `docker restart soft-serve`. Now when you ssh into Soft Serve
running on your local docker install you will see something like the following:
![SSH TUI View of the Flux Repo README we just added to the repo](https://i.imgur.com/bDIA7Cq.png)
The same can be done with the instance running on the `Kind` Cluster. Restart the
running `Pods` e.g. `kubectl --context kind-dev -n soft-serve delete pods -l app=soft-serve`.
Once the `Pods` have been recreated you can port forward to one of the running `Pods`
e.g. `kubectl --context kind-dev -n soft-serve port-forward soft-serve-786fb9c9f8-25wkc 23232:23231`
In the above command I've port forward the `Pods` port `23231` to the local port
`23232`. So now we can ssh into the TUI on the `Pod` and see the same view as above.
In the normal day to day use with Flux you do not need to stop and start the Soft
Serve instances unless you want to use the TUI and see the README file updates
as the TUI only appears to update when there is a push to the repo.
### Setting up Flux
In the `flux` repo setup a structure like the following:
```bash
❯ tree -a -I .git
.
├── clusters
│ └── dev
│ └── flux-system
└── README.md
```
We are going to export the Flux installation into the `flux-system` directory, you
can do this as follows: `flux install --export > clusters/dev/flux-system/gotk-components.yaml`
I won't include the content of that file as there is a lot in it, but feel free to
browse. Next we'll want to create a `gotk-sync.yaml` and a `kustomization.yaml`.
Which you can do as follows:
```bash
❯ flux create source git flux-system \
--git-implementation=libgit2 \
--url=ssh://git@git.soft-serve:23231/flux.git \
--branch=main \
--interval=1m \
--export > clusters/dev/flux-system/gotk-sync.yaml
❯ flux create kustomization flux-system \
--source=GitRepository/flux-system \
--path=./clusters/dev \
--prune=true \
--interval=1m \
--export >> clusters/dev/flux-system/gotk-sync.yaml
❯ cd clusters/dev/flux-system
❯ kustomize create --autodetect
❯ cd ../../..
❯ tree -a -I .git -I vendor
.
├── clusters
│ └── dev
│ └── flux-system
│ ├── gotk-components.yaml
│ ├── gotk-sync.yaml
│ └── kustomization.yaml
└── README.md
```
It is also worth adding the resource above that created the Soft Serve deployment
we are using and setting up the `kustomization.yaml` for that directory:
```bash
❯ tree -a -I .git
.
├── clusters
│ └── dev
│ ├── flux-system
│ │ ├── gotk-components.yaml
│ │ ├── gotk-sync.yaml
│ │ └── kustomization.yaml
│ └── soft-serve
│ ├── deployment.yaml
│ ├── kustomization.yaml
│ ├── namespace.yaml
│ └── service.yaml
└── README.md
```
Now let's apply the above `flux-system` resources to the Kind cluster:
```bash
❯ kubectl apply -f clusters/dev/flux-system/gotk-components.yaml
namespace/flux-system created
customresourcedefinition.apiextensions.k8s.io/alerts.notification.toolkit.fluxcd.io created
customresourcedefinition.apiextensions.k8s.io/buckets.source.toolkit.fluxcd.io created
customresourcedefinition.apiextensions.k8s.io/gitrepositories.source.toolkit.fluxcd.io created
customresourcedefinition.apiextensions.k8s.io/helmcharts.source.toolkit.fluxcd.io created
customresourcedefinition.apiextensions.k8s.io/helmreleases.helm.toolkit.fluxcd.io created
customresourcedefinition.apiextensions.k8s.io/helmrepositories.source.toolkit.fluxcd.io created
customresourcedefinition.apiextensions.k8s.io/kustomizations.kustomize.toolkit.fluxcd.io created
customresourcedefinition.apiextensions.k8s.io/providers.notification.toolkit.fluxcd.io created
customresourcedefinition.apiextensions.k8s.io/receivers.notification.toolkit.fluxcd.io created
serviceaccount/helm-controller created
serviceaccount/kustomize-controller created
serviceaccount/notification-controller created
serviceaccount/source-controller created
clusterrole.rbac.authorization.k8s.io/crd-controller-flux-system created
clusterrolebinding.rbac.authorization.k8s.io/cluster-reconciler-flux-system created
clusterrolebinding.rbac.authorization.k8s.io/crd-controller-flux-system created
service/notification-controller created
service/source-controller created
service/webhook-receiver created
deployment.apps/helm-controller created
deployment.apps/kustomize-controller created
deployment.apps/notification-controller created
deployment.apps/source-controller created
networkpolicy.networking.k8s.io/allow-egress created
networkpolicy.networking.k8s.io/allow-scraping created
networkpolicy.networking.k8s.io/allow-webhooks created
```
Before we apply the sync file we need to setup a `known_hosts` file and an SSH Key
that Flux can work with in order to pull from our Soft Serve repo. Let's start
with the SSH Key:
```bash
❯ ssh-keygen -t ed25519 -C "flux-system"
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/nalum/.ssh/id_ed25519): /home/nalum/code/ssh/identity
...
+----[SHA256]-----+
```
To create the `known_hosts` file we need to get the public key of the Soft Serve
server we're running, we can do that with:
```bash
❯ cat ${PATH_TO_CODE}/ssh/soft_serve_server_ed25519.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMDYacTjeLH4ha6BDgWN3rXeh70GDYmaSVBw5UQg1FQ3 root@2dcd99be9aef
```
Copy this public key into a new `${PATH_TO_CODE}/ssh/known_hosts` file and put
the domain name in front of it something like the following:
```=
git.soft-serve:23231 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMDYacTjeLH4ha6BDgWN3rXeh70GDYmaSVBw5UQg1FQ3 root@2dcd99be9aef
```
Now that we have the SSH Key and the `known_hosts` file we need to create a secret
that Flux will pick up, you can use `-o yaml` to see the output:
```bash
❯ kubectl -n flux-system create secret generic \
--from-file=identity=${PATH_TO_CODE}/ssh/identity \
--from-file=identity.pub=${PATH_TO_CODE}/ssh/identity.pub \
--from-file=known_hosts=${PATH_TO_CODE}/ssh/known_hosts \
flux-system
secret/flux-system created
```
With that put in place we can then apply the sync file:
```bash
❯ kubectl apply -f clusters/dev/flux-system/gotk-sync.yaml
gitrepository.source.toolkit.fluxcd.io/flux-system created
kustomization.kustomize.toolkit.fluxcd.io/flux-system created
```
If you now run `flux get all` you should see something like the following:
```bash
❯ flux get all
NAME READY MESSAGE REVISION SUSPENDED
gitrepository/flux-system True Fetched revision: main/7e12f0c main/7e12f0c False
NAME READY MESSAGE REVISION SUSPENDED
kustomization/flux-system False kustomization path not found: stat /tmp/flux-system2367946620/clusters/dev: no such file or directory False
```
The `Kustomization` is not working here because we have not committed any of the
files to the repo yet. If you run `watch flux get all --all-namespaces` and the add
and commit the `clusters` folder and it's contents, you will see the `Kustomization`
reconcile itself:
```bash
❯ flux get all --all-namespaces
NAMESPACE NAME READY MESSAGE REVISION SUSPENDED
flux-system gitrepository/flux-system True Fetched revision: main/b6e77db main/b6e77db False
NAMESPACE NAME READY MESSAGE REVISION SUSPENDED
flux-system kustomization/flux-system True Applied revision: main/b6e77db main/b6e77db False
```
If you describe the kustomization object with kubectl you'll see the resources that
have been applied to the system through it:
```bash
❯ kubectl -n flux-system describe kustomizations.kustomize.toolkit.fluxcd.io flux-system
Name: flux-system
Namespace: flux-system
Labels: kustomize.toolkit.fluxcd.io/name=flux-system
kustomize.toolkit.fluxcd.io/namespace=flux-system
Annotations: <none>
API Version: kustomize.toolkit.fluxcd.io/v1beta2
Kind: Kustomization
Metadata:
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning error 6m35s kustomize-controller kustomization path not found: stat /tmp/flux-system1699096295/clusters/dev: no such file or directory
...
Normal info 115s kustomize-controller CustomResourceDefinition/alerts.notification.toolkit.fluxcd.io configured
CustomResourceDefinition/buckets.source.toolkit.fluxcd.io configured
...
Namespace/flux-system configured
Namespace/soft-serve configured
ServiceAccount/flux-system/helm-controller configured
ServiceAccount/flux-system/kustomize-controller configured
...
Deployment/flux-system/source-controller configured
Deployment/soft-serve/soft-serve configured
...
Normal info 34s kustomize-controller Reconciliation finished in 680.646597ms, next run in 1m0s
```
## Final Thoughts
This is a lot of work in order to use Flux from a local repository. This was a fun
exercise to get going and document here. It could be automated and be a single command
to setup the whole system, which _maybe_ is worth it. I'll leave that for you to
decide.
Please leave a comment with your thoughts or if you have any issues following along
I'd be happy to try helping out.