How would I proceed with Kubernetes deployments?
Last time I’ve written a few words about the orchestration of Helm deployments. I’ve mentioned there that I’m not so big fan of Helm in terms of deployment of applications. That’s 100% true, but do I have any other alternatives?
Sure, be my guest. In this article I’m going to show you world of template-free customizations and single-purpose deployment tools!
Template-free customization with Kustomize
When I first saw Kustomize, I was using Helm everyday so any different approach seemed to be odd, but after thousands and thousands of written Helm templates I came back I could clearly see the benefits there.
I don’t want to bother you with the lesson of philosophy so let’s start with simple example. Let’s say that I want to deploy simple Kubernetes Deployment with some dummy image, here it is:
Basic project structure without Kustomize
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
labels:
app.kubernetes.io/name: backend
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: backend
template:
metadata:
labels:
app.kubernetes.io/name: backend
spec:
containers:
- name: backend
image: backend:version1234
ports:
- containerPort: 80
We also want to expose it to the internet so it would be great to have
some Service
and Ingress
:
apiVersion: v1
kind: Service
metadata:
name: backend
spec:
selector:
app.kubernetes.io/name: backend
ports:
- protocol: TCP
port: 80
targetPort: 80
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: backend
spec:
rules:
- host: backend.pipetail.io
http:
paths:
- path: /
backend:
serviceName: backend
servicePort: 80
So this is the structure of our application:
.
├── deployment.yaml
├── ingress.yaml
└── service.yaml
We can deploy it with kubectl apply
, we can adjust the image tag
if needed and I don’t hesitate to say that this solution will be
the best for such simple stack.
But it get’s a bit more complicated when we wan’t to deploy the same stack to more environments.
Adding kustomization manifest
So in our hypothetical case we’re gonna deploy the same app to two different environments,
development and production. The only difference is labeling: pipetail.io/environment: development
and pipetail.io/environment: production
. To use the same manifests,
we need to create a kustomization file kustomization.yaml
first:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
- ingress.yaml
The result is directory with following contents:
.
├── deployment.yaml
├── ingress.yaml
├── kustomization.yaml
└── service.yaml
We’ve basically just created something that is called base
. Now we can put
these files to some more complex structure and continue with overlays!
Production overlay
Regarding the more complex structure, here it is:
.
├── bases
│ └── backend
│ ├── deployment.yaml
│ ├── ingress.yaml
│ ├── kustomization.yaml
│ └── service.yaml
└── environments
├── development
│ └── backend
└── production
└── backend
All four YAML files were moved to bases/backend
directory. You can choose
arbitrary name, personally I always prefer something obvious. Moreover,
you’re gonna find the similar structure in the majority of tutorials.
Anyway, now let’s create a new kustomization file environments/production/backend/kustomization.yaml
and let’s try to use the base created before with slightly different labels.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../../bases/backend/
commonLabels:
pipetail.io/environment: production
That’s it. Now we can try to build the manifests for the production environment:
kustomize build environments/production/backend/
apiVersion: v1
kind: Service
metadata:
labels:
pipetail.io/environment: production
name: backend
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app.kubernetes.io/name: backend
pipetail.io/environment: production
...
And that’s pretty much it. You can check the official documentation and see the list of basic transformers. However, this is how the overlays work. You can go even further and consider this overlay as a base and build another overlay… but don’t try to be too clever. It can get messy even with template-free engine :D.
More complicated operations
Now we just need to clarify some more complicated matter. Do you remember that Ingress
object?
It has hard-coded host
property, But we certainly don’t want the same name for both environments.
How can we proceed here with template-free matter?
option 1: remove ingress from the base
.
├── bases
│ └── backend
│ ├── deployment.yaml
│ ├── kustomization.yaml
│ └── service.yaml
└── environments
├── development
│ └── backend
│ ├── ingress.yaml
│ └── kustomization.yaml
└── production
└── backend
├── ingress.yaml
└── kustomization.yaml
Kustomization in overlays will then contain reference to the local ingress.yaml
file:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../../bases/backend/
- ingress.yaml
commonLabels:
pipetail.io/environment: production
option 2: JSON patch
But if the amount of changes that we want to make in the whole resource is not so big, we can just simply replace one bit of information without the repetition.
This is the kustomization.yaml
in the overlay:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../../bases/backend/
commonLabels:
pipetail.io/environment: production
patches:
- path: patch-ingress.yaml
target:
group: networking.k8s.io
version: v1beta1
kind: Ingress
name: backend
and this is the patch refered in the kustomization file:
- op: replace
path: /spec/rules/0/host
value: production.backend.pipetail.io
Let’s just quickly check that everything works as expected.
kustomize build environments/production/backend/
...
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
labels:
pipetail.io/environment: production
name: backend
spec:
rules:
- host: production.backend.pipetail.io
http:
paths:
- backend:
serviceName: backend
servicePort: 80
path: /
My favorite parts of Kustomize
It’s pretty obvious that Kustomize can handle pretty much all the common use cases. I really don’t want to show all the capabilities here because it’s matter more for some book than for a single blog post. Here I just want to name a few features that I consider as really nice:
-
With images I don’t have to edit images in the Kubernetes manifests. I usually use some dummy (but descriptive) image names so I can easily change the it the kustomization file.
images: - name: backend newName: 123456789123.dkr.ecr.eu-central-1.amazonaws.com/deployment newTag: 322fe7d0
-
configMapGenerator really helps with creation of configmaps, I don’t have to struggle with indentation in the ConfigMap and I can refer files directly
-
Kustomize is also easily extensible with plugins. While native Go plugins are a bit hard to work with, simple exec plugin can be done in a matter of minutes. I use this for secrets management when I encrypt secrets with SOPS and simple wrapper written in Go is then decrypting them.
-
And last but not least, Kustomize is adding hash to ConfigMap names automatically. And last but not least, Kustomize is adding hash to ConfigMap names automatically. It means that edits in ConfigMaps trigger restart of pods (in Helm we usually use annotations with hashes for the same purpose).
Single-purpose tool for deployment
Templating is only one part of Helm, right? The other part is management of the releases. It’s particularly useful when we need to upsert resources in the given order or perform garbage collection.
All mentioned can be covered with Kapp from the K14s project. Let’s check how the most important features work in Kapp.
Apply ordering
Applying Kubernetes in the specific order is actually pretty common requirement.
It’s mainly used for the migrations together with Kubernetes Jobs
. In Kapp,
this behaviour can be controlled with kapp.k14s.io/change-group
and
kapp.k14s.io/change-rule
annotations. So let’s create a model scenario
for this specific requirement.
apiVersion: batch/v1
kind: Job
metadata:
name: backend-migrations
annotations:
kapp.k14s.io/change-group: "apps.pipetail.io/db-migrations"
spec:
template:
spec:
containers:
- name: backend-migrations
image: backend:version1234
command: ["app", "migrate", "--force"]
restartPolicy: Never
backoffLimit: 0
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
labels:
app.kubernetes.io/name: backend
annotations:
kapp.k14s.io/change-group: "apps.pipetail.io/deployment"
kapp.k14s.io/change-rule: "upsert after upserting apps.pipetail.io/db-migrations"
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: backend
template:
metadata:
labels:
app.kubernetes.io/name: backend
spec:
containers:
- name: backend
image: backend:version1234
ports:
- containerPort: 80
Note the change rule in the Deployment:
kapp.k14s.io/change-rule: "upsert after upserting apps.pipetail.io/db-migrations"
Here we are saying to Kapp that we want deploy a new version of the Deployment manifest after the migrations run successfully. Let’s test this scenario with previously created resources!
garbage collection
What if we delete ingress from our stack? Will Kapp delete them from cluster?
It’s a yes! Kapp can handle even this highly demanded requirement.
Wrap
Hey, I’m not telling you to stop using Helm. I’m just showing you that Internet is full of tools that can help you with the similar matter. So if you’re not so happy with Helm, you can take a look around and choose some different tool that does the job.
There are Jsonnet, Tanka and a lot of different tools. Some of them can handle multiple tasks, some of them are single-purpose as Kapp.
But don’t forget that one tool does not necessarily cover all the requirements. The key is always composition.