Bob's Journey Continues: Build a Secure Chart
Join Bob and Josie as they build a secure Helm chart from the ground up, exploring Kubernetes best practices and mastering advanced Helm configurations
Bob, eager to build his Helm chart, turns to Josie. "Alright, Josie, I'm ready to build that chart for my To-Do List application. Where do we start?"
Josie smiles. "Great! Let's create a secure Helm chart for your To-Do List application from scratch. I'll guide you through each step."
📂 Code Repository: Explore the complete code and configurations for this article on GitHub.
1. Setting Up the Chart Structure
"First, we need to create a directory for our chart and initialise it," Josie explains.
$ helm create my-todo-list
Creating my-todo-list
$ cd my-todo-list
$ code .
"This creates the basic Helm chart structure" she adds, launching VSCode "we'll now populate it with our application's specifics." A quick inspection shows the following structure:
$ tree .
.
├── charts
├── Chart.yaml
├── dryrun.yaml
├── summary.txt
├── templates
│ ├── deployment.yaml
│ ├── _helpers.tpl
│ ├── _hpa.yaml
│ ├── ingress.yaml
│ ├── NOTES.txt
│ ├── serviceaccount.yaml
│ └── service.yaml
└── values.yaml
3 directories, 11 files
To keep helm charts DRY we can leverage the _helpers.tpl
file to keep a track of vaiables and use those throughout the templates.
2. Cleaning Up
"Let's delete the charts
folder as we have no chart dependencies" Josie says as they flick through the templates folder. "We only have a single host, so let's simplify things."
3. Update Chart.yaml
Bob opens the Chart.yaml
file and fills in the necessary information:
apiVersion: v2
name: my-todo-list
description: A Helm chart for Kubernetes
type: application
version: 0.1.0
appVersion: "1.0.0"
maintainers:
- name: Bob
email: bobs_email@example.com
4. Craft the Ingress
Bob opens the templates/ingress.yaml
file and modifies it as follows to expose his application:
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "my-todo-list.fullname" . }}
labels:
{{- include "my-todo-list.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
ingressClassName: {{ .Values.ingress.className | default "" }}
tls:
- hosts:
- {{ .Values.ingress.host }}
secretName: {{ include "my-todo-list.fullname" . }}-tls
rules:
- host: {{ .Values.ingress.host }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ include "my-todo-list.fullname" . }}
port:
number: {{ .Values.service.port }}
{{- end }}
"This Ingress definition configures Traefik to route traffic to your To-Do List application," Josie explains. "The annotations ensure that HTTPS is enabled and that certificates are automatically provisioned using Let's Encrypt."
In the values.yaml
file, Josie helps Bob to modify the ingress as follows:
ingress:
enabled: true
className: traefik
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
cert-manager.io/cluster-issuer: letsencrypt-issuer
host: my-todo-list.example.com
5. Expose the Service
Bob opens the templates/service.yaml
file and modifies it as follows:
apiVersion: v1
kind: Service
metadata:
name: {{ include "my-todo-list.fullname" . }}
labels:
{{- include "my-todo-list.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: {{ .Values.service.targetPort }}
protocol: TCP
name: http
selector:
{{- include "my-todo-list.selectorLabels" . | nindent 4 }}
He then modifies the values.yaml file as follows:
service:
type: ClusterIP
port: 80
targetPort: 3000
6. Define the Service Account
"Now, let's define the service account that your application will eventually use," Josie says.
Bob creates the templates/serviceaccount.yaml
file:
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "my-todo-list.serviceAccountName" . }}
labels:
{{- include "my-todo-list.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
automountServiceAccountToken: false
{{- end }}
"This service account will be used by your application to interact with the Kubernetes API and other pods" Josie explains. "We've set automountServiceAccountToken
to false
for enhanced security, as your application doesn't currently require access to the API or any other pods at the moment."
7. Remove the HPA
"We'll cover this later as we grow your app" Josie says, and they delete the templates/hpa.yaml
file.
8. Craft the Deployment
"Now for the fun bit, let's define your application's deployment in the templates/deployment.yaml
file" Josie says.
Together, they craft the deployment definition, ensuring it aligns with security best practices:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "my-todo-list.fullname" . }}
labels:
{{- include "my-todo-list.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "my-todo-list.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.pods.annotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "my-todo-list.labels" . | nindent 8 }}
{{- with .Values.pods.labels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "my-todo-list.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.pods.securityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.targetPort }}
protocol: TCP
{{- range .Values.probes }}
{{- if .enabled }}
{{ .name }}:
{{- toYaml .probe | nindent 12 }}
{{- end }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
"We've added security contexts at both the pod and container levels" Josie points out. "This enforces running the application as a non-root user and restricts privilege escalation, enhancing the security of your deployment."
9. Set Default Values
"Now, let's set some default configuration values in the values.yaml
file" Josie says.
replicaCount: 1
image:
repository: my.registry.example.com/bob/my-todo-list
pullPolicy: IfNotPresent
tag: 1.0.0
imagePullSecrets:
- name: imagecredentials-secret
# This is to override the chart name.
nameOverride: ""
fullnameOverride: ""
serviceAccount:
create: true
pods:
annotations:
owner: bobs_email@example.com
labels: {}
securityContext:
runAsUser: 1001
runAsGroup: 1001
fsGroup: 1001
securityContext:
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1001
runAsGroup: 1001
service:
type: ClusterIP
port: 80
targetPort: 3000
ingress:
enabled: true
className: traefik
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
cert-manager.io/cluster-issuer: letsencrypt-issuer
host: my-todo-list.example.com
resources: {}
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
probes:
- name: startupProbe
enabled: true
probe:
httpGet:
path: /
port: http
- name: livenessProbe
enabled: true
probe:
httpGet:
path: /
port: http
- name: readinessProbe
enabled: true
probe:
httpGet:
path: /
port: http
10. Lint, Dry-run and Scan
Bob remembers the earlier discussion and jumps into his terminal and runs the following ... grinning at the output 😁
$ helm lint
==> Linting .
[INFO] Chart.yaml: icon is recommended
1 chart(s) linted, 0 chart(s) failed
Josie spots his grin and say "Well done ... there's usually a lot more issues than this, now let's dry run it and scan it using Trivy". Bob taps out the following:
$ helm install todo-app . -f values.yaml --dry-run > dryrun.yaml
$ trivy config dryrun.yaml > summary.txt
2024-11-26T10:06:15Z INFO [misconfig] Misconfiguration scanning is enabled
2024-11-26T10:06:17Z INFO Detected config files num=1
They both look through the output of the summary and spot a couple of required changes to their chart:
- MEDIUM: Container 'my-todo-list' of Deployment 'todo-app-my-todo-list' should set 'securityContext.allowPrivilegeEscalation' to false
- LOW: Container 'my-todo-list' of Deployment 'todo-app-my-todo-list' should set 'resources.limits|requests.memory|cpu'
- LOW: Container 'my-todo-list' of Deployment 'todo-app-my-todo-list' should set 'securityContext.runAsUser' > 10000
- LOW: Either Pod or Container should set 'securityContext.seccompProfile.type' to 'RuntimeDefault'
Josie remarks "This is looking pretty good for a first time run but there are a couple of tweaks we can make based on this information. First let's modify out values.yaml file and include the allowPrivilegeEscalation
and the seccompProfile.type
setting to the container securityContext settings. Also, given the app runs as user/group 1001 for now we'll leave the runAs settings as they are but in the future we can modify our Dockerfile to take this into account". Together they modify the values.yaml file as follows:
securityContext:
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1001
runAsGroup: 1001
allowPrilegeEscalation: false
seccompProfile:
type: RuntimeDefault
Bob then regenerates the dryrun and rescans the chart while commenting "we don't know what the resource requirements are just yet so it should be okay to accept that one for now" as he makes a note to follow up on this later.
They double check the output and note the only remaining advisors are around the resources and the user/group being greater than 10000.
It's worth noting that you don't have to resovle everything providing you understand why
6. Deploy and Verify the app
"Finally, we're ready to deploy our app into the cluster" Josie concludes. "Want to do the honours?" she asks Bob, he eagerly nods and types out:
$ helm install --namespace=mytodoapp todo-app . -f values.yaml
NAME: todo-app
LAST DEPLOYED: Tue Nov 26 10:22:28 2024
NAMESPACE: bobsjourney
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
1. Get the application URL by running these commands:
export POD_NAME=$(kubectl get pods --namespace mytodoapp -l "app.kubernetes.io/name=my-todo-list,app.kubernetes.io/instance=todo-app" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace mytodoapp $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit https://my-todo-list.example.com to use your application"
kubectl --namespace mytodoapp port-forward $POD_NAME 3000:$CONTAINER_PORT
Bob then checks the status of the pods, svc and ingress to make sure everything is present and expected:
$ kubectl get pods,svc,ingress
NAME READY STATUS RESTARTS AGE
pod/todo-my-todo-list-79cc46998d-7tj7w 1/1 Running 0 75s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/todo-my-todo-list ClusterIP 10.101.125.144 <none> 80/TCP 2m7s
NAME CLASS HOSTS ADDRESS PORTS AGE
ingress.networking.k8s.io/todo-my-todo-list traefik my-todo-list.example.com 192.168.0.180 80, 443 39s
Seeing everything looks to be fine, he fires up a browser and goes to https://my-todo-list.example.com
and jumps in excitement as he sees his example app loading.
Success!
Bob's To-Do List application is now deployed using his secure custom Helm chart! He's thrilled with the ease and security of the process.
"This is fantastic!" he exclaims. "Deploying my application securely was a breeze with Helm."
Josie smiles. "You've done a great job, Bob! You've built a secure Helm chart from scratch."
Next Steps
Bob, eager to learn more, asks, "What else can I do with Helm charts?"
"The possibilities are endless," Josie replies. "You can explore Helm's templating language to customise deployments further, use Helm hooks to execute jobs before or after deployment, and even create your own Helm repository to share your charts with others."
Bob's first custom Helm chart is a success! Not only has he securely deployed his To-Do List application, but he's also gained valuable insights into Helm's templating, security best practices, and deployment workflows.
As he starts thinking about future enhancements, such as integrating Redis for data persistence or automating more aspects of his deployments, Bob feels confident that Helm is the right tool to help him navigate the complexities of Kubernetes.
Stay tuned as Bob and Josie continue their journey, diving into advanced Helm features and tackling the challenges of scaling and optimisation!