My very first website was well in the spirit of the time – the early 2000s – and used framesets to navigate through a bunch of carefully crafted static html pages. I copied them via ftp to a free webspace with some megabytes of space as soon as they looked good on my computer, and so they were accessible by the entire world - at least its part which had internet access.
These times are now long over. Web sites are real applications, not a mere collection of some html files. And being applications, they need a deployment strategy, managing at least their lifecycle: They need to be started, updated and restarted.
The last years have seen kubernetes gaining a LOT of visibility and usage, and this has one very usefull advantage: Standardisation of the deployment. Kubernetes has the advantage of having a clear, declarative way of deploying an application, useful for deploying to every major and most of every other cloud provider and interchangeable in the greater parts of them. This is why I think kubernetes is a very good choice even in smaller scale, like for the small personal projects, and this is why this blog is deployed on kubernetes.
Installing kubernetes on my ionos vpc using k3s was a breeze, and after running the installation command and downloading the kubeconfig one can start exploring the cluster via kubectl or the very good k9s.
Install k3s on your vps:
curl -sfL https://get.k3s.io | sh -
This automatically installs the control plane on the server and additionaly sets it up as its first node.
I could then download the kubeconfig and connect to the control plane:
# Download configuration file (this will overwrite any existing local kubeconfig!)
scp user@myip:/etc/rancher/k3s/k3s.yaml ~/.kube/config
# List nodes available
kubectl get nodes
# NAME STATUS ROLES AGE VERSION
# main Ready control-plane,etcd,master 1d v1.28.8+k3s1
Any application is now just a kubectl apply
away!
By default, k3s installs traefik as an ingress controller, but this can be disabled and an alternative ingress controller can be installed. I decided to stick with traefik (just to change somewhat from nginx so I don’t get bored …)
FluxCD
After the second time I installed and configured a complete kubernetes cluster by hand for lotta because something went terribly wrong or we moved providers, I became somewhat more long-sighted the third time this was necessary and set the cluster up (at scaleway, which I am very happy with) using terraform and argocd.
Gitops immediatly clicked with me, and I now wonder how I could ever setup applications typing commands in my terminal, taking the risk to forget about what I did just seconds later, let alone recreating the same configs months or years later.
Essentially, gitops describes a method where you you have your infrastructure as code available in (one ore multiple) git repositories. Your infrastructure is constantly monitored and the checked against your repositories for discrepancies between what’s described in your repository and the current state of your infrastructure, and try to reconcile these so your cluster matches what’s described in your repositories.
While a tool like terraform is usually used to manage the infrastructure itself – servers, users, buckets, loadbalancers, databases and the likes – tools like argocd will install on your cluster and monitor your cluster’s state, making sure the pods and deployments you want to be running on your cluster do run and do so with the right configuration.
I will describe how I use flux, which positions itself as a tool with similar goals as argocd, to deploy this blog on k3s. Having worked with argocd, I wanted to try flux which I have heard good things about.
The whole purpose of flux - like argocd - is to define your kubernetes resources in a git repository. Flux will detect changes made to the repository - be it declarations added, changed or removed, and apply them to your cluster. This makes the repository the single source of truth for your cluster, and its state changed declarativly as well as easily recreated.
Getting started requires a bit of learning, especially if one has not worked with GitOps systems before, but flux has the advantage of bringing all important components you’ll need for most cases.
You’ll want to get the flux cli installed first. On a mac, run:
# Install flux cli
brew install fluxcd/tap/flux
# Get you a github PAT and set an env variable to it in your current shell session
export GITHUB_TOKEN=<your-token>
export GITHUB_USER=<your-username>
# Check your kubernetes cluster
flux check --pre
# Install flux
flux bootstrap github \
--components-extra=image-reflector-controller,image-automation-controller \
--owner=$GITHUB_USER \
--repository=my-infra \
--branch=main \
--path=./my-cluster \
--personal
This will create a (private) repository “my-infra” in your github account and set your flux to read flux custom resources from it.
Now pull the repository to your local computer.
You will see a directory “my-cluster/flux-system”, which contains the flux toolkit components definitions.
Adding an application
Now in order to to setup your application (I will use my blog’s repository as an example), you will have to create a kustomize application (alternatively, you could deploy Helm charts).
I created a blog
directory with the following files:
# my-cluster/blog/kustomization.yaml
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
metadata:
name: blog
namePrefix: blog-
commonLabels:
app: blog
resources:
- deployment.yaml
- service.yaml
- ingress.yaml
# my-cluster/blog/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment
labels:
deployment: blog
spec:
replicas: 1
selector:
matchLabels:
deployment: blog
template:
spec:
containers:
- name: blog
image: ghcr.io/ptitmouton/blog:latest
ports:
- name: http
containerPort: 80
# my-cluster/blog/service.yaml
apiVersion: v1
kind: Service
metadata:
name: service
spec:
selector:
deployment: blog
ports:
- protocol: TCP
port: 80
targetPort: http
# my-cluster/blog/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress
spec:
rules:
- host: example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: service
port:
number: 80
This is a very basic kustomize application, consisting of the basic resources start a container as a deployment and make it available to the internet via an ingress. Update the repository (and tag) you wish to use.
To make flux pick up the application, add the following flux custom resource:
# my-cluster/blog-kustomization.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: blog
namespace: flux-system
spec:
interval: 30m0s
path: ./my-cluster/blog
prune: true
retryInterval: 2m0s
sourceRef:
kind: GitRepository
name: flux-system
timeout: 3m0s
wait: true
You can look up all the options in the flux documentation,
but summarize: Flux will check the GitRepository named
flux-system
(which is by default the repository flux was bootstrapped to, the one you’re in that contains the flux-system
directory) and check the kustomize config located at ./my-cluster/blog
every 30m to see if updates must be applied.
Commit and push the change. After some time, flux will pick up the change and apply the configuration.
You can watch your deployed flux resources by running:
flux get kustomizations --watch
When the READY
state has changed to True
, you’ll be able to list your resources:
kubectl get all
Updating the image
In the example above, I used the latest
tag on the image name. You could of course do that, but
on one hand that wouldn’t give you very precise control over when your resource is updated (if at all),
and on the other hand it would violate the idea of gitops, which is to have the version defined in git.
You could of course make a github action or similar change the value and commit it to your configuration repo. But there’s a better way:
You can make flux check automatically for new updates and apply them to your configuration repository.
# my-cluster/blog-registry.yaml
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageRepository
metadata:
name: blog
namespace: flux-system
spec:
image: ghcr.io/ptitmouton/blog
interval: 5m
This describes the repository to pull from. You will need a registry secret
in the flux-system
namespace in order to pull from private registries.
In order to describe how to update (what is a newer version?), you’ll need a corresponding policy:
kind: ImagePolicy
spec:
imageRepositoryRef:
name: blog
policy:
alphabetical:
order: asc
This will always apply the latest available tag in alphabetical order.
Describe to flux where to apply the latest image. Update the deployment.yaml
# my-cluster/blog/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment
labels:
deployment: blog
spec:
replicas: 1
selector:
matchLabels:
deployment: blog
template:
spec:
containers:
- name: blog
image: ghcr.io/ptitmouton/blog:latest # {"$imagepolicy": "flux-system:blog"}
ports:
- name: http
containerPort: 80
At last, setup the ImageUpdateAutomation which will update the repository:
# my-cluster/blog/image-update-automation.yaml
apiVersion: image.toolkit.fluxcd.io/v1beta1
kind: ImageUpdateAutomation
metadata:
name: flux-system
namespace: flux-system
spec:
interval: 30m
sourceRef:
kind: GitRepository
name: flux-system
git:
checkout:
ref:
branch: main
commit:
author:
email: fluxcdbot@users.noreply.github.com
name: fluxcdbot
messageTemplate: "{{range .Updated.Images}}{{println .}}{{end}}"
push:
branch: main
update:
path: ./my-cluster
strategy: Setters
It is now time to commit and push the latest changes.
After some time, flux will have commited and updated image property of your deployment.yaml.
That’s it! Now, everytime a new image is available, flux will update your repository, which will then lead it to update your cluster’s state.