Overview:
This section provides a comprehensive guide to preparing Helm charts for deploying an application in a Kubernetes environment.
The manifests we created in the previous step are powerful tools to instruct k8s cluster how to deploy the application. However they are static by its nature and lack flexibility and conditional logic that we might need for different deployments. This can be resolved by transforming those manifests into Helm chart. Helm is also a powerful package manager for Kubernetes, which enables you to define, install, and upgrade complex Kubernetes applications using simple commands.
Understanding Helm Chart Structure:
Before diving into creating Helm charts, it's essential to understand the basic structure of a Helm chart:
Chart.yaml: Contains metadata about the chart, such as its name, version, and description.values.yaml: Contains default configuration values for the chart templates.templates/: A directory that contains the Kubernetes resource files (manifests) that Helm uses to deploy the application. These files can use Go templating to create dynamic configurations._helpers.tpl: A template file used to define reusable template helpers.
Artifacts :
For the purpose of this tutorial we will be using this artifacts repository: https://gitlab.portaone.com:8949/read-only/tutorial-for-simple-application
Documentation Variables:
The documentation contains custom variables:
- <application_name>
- <application_version>
- <personal dockerhub repository>
For details related to variables meaning, please refer => Guide to deploy application in Add-On Mart for third-party developers
Syntax Used in Helm Charts in scope of documentation:
Before diving into the steps, let's briefly go over the syntax used in Helm templates.
Template Syntax
Helm uses Go templates to dynamically generate YAML files. Here are some common constructs you'll encounter:
Variables: Declared using
{{- $variableName := value }}.Functions: Built-in and custom functions are called using
{{ printf "Hello %s" .Values.name }}.Conditionals: You can use
if,else, andwithto control logic.{{- if .Values.enabled }} # do something {{- end }}- Loops: Iterate over a list using
range.{{- range $key, $value := .Values.list }} # do something {{- end }} - Comments: Use
{{- /* This is a comment */ -}}for comments in templates.
Important Helm Template Constructs:
.Chart: Refers to the current chart's metadata, such as name and version..Values: Accesses values fromvalues.yaml..Release: Provides information about the release, such as name and namespace.
Steps to Create Helm Charts:
1. Create Chart.yaml
Purpose:
The Chart.yaml file is the main descriptor for the Helm chart.
It contains metadata that describes the chart's details, including its name, version, and the application it represents.
Explanation:
apiVersion: Defines the chart API version.v2is the latest version.name: Specifies the name of the chart.description: Provides a brief description of the chart.version: Specifies the version of the chart itself. This should be incremented with changes to the chart. (For simplicity we will use same version number for chart as for application)appVersion: Specifies the version of the application being deployed. This corresponds to the application version.
apiVersion: v2 name: <application_name> description: A Helm chart for deploying <application_name> on Kubernetes version: <application_version> appVersion: <application_version>
2. Create values.yaml
Purpose: This file holds default values for your templates. You can override these values when deploying the chart.
Initial values.yaml:
# This file is initially empty and will be filled with configuration values.
We'll fill in this file as we progress through the templates.
For more details related to the final version of values.yaml , please refer → Complete values.yaml (Helm Charts)
3. Create the templates Directory
Purpose: This directory contains all the template files used by Helm to generate Kubernetes manifests.
4. Create templates/_helpers.tpl
Purpose:
The _helpers.tpl file is used to define reusable template helpers for generating resource names and labels dynamically.
This reduces redundancy in template files and ensures consistency across resources.
Explanation:
<application_name>.name: Returns the chart's name, ensuring it doesn't exceed 63 characters.<application_name>.fullname: Generates a full application name, combining the release and chart names.
If the release name ismyreleaseand the chart name is<application_name>, this helper will returnmyrelease-<application_name>.<application_name>.chart: Returns a string with the chart name and version.
For a chart named<application_name>with version1.0.0, this helper outputs<application_name>-1.0.0.<application_name>.selectorLabels: Provides labels for selectors, including app name and instance.<application_name>.labels: Creates standard labels for resources, using previous helpers for consistency.
{{- define "<application_name>.name" -}}
{{- default .Chart.Name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "<application_name>.fullname" -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "<application_name>.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "<application_name>.selectorLabels" -}}
app.kubernetes.io/name: {{ include "<application_name>.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{- define "<application_name>.labels" -}}
helm.sh/chart: {{ include "<application_name>.chart" . }}
{{ include "<application_name>.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
5. Create Namespace Template
Purpose: Define the namespace for isolating resources in Kubernetes.
Explanation:
name:Uses the<application_name>.fullnamehelper for consistent naming.
Original Namespace YAML Manifest:
apiVersion: v1 kind: Namespace metadata: name: <application_name>-ns
Updated Namespace Helm Charts Template :
apiVersion: v1
kind: Namespace
metadata:
name: {{ include "<application_name>.fullname" . }}-ns
6. Create ConfigMap Template
Purpose: Store configuration data for your application.
Explanation:
- Resource Name: Utilizes
<application_name>.fullnamefor naming consistency. - Labels: Adds labels using
<application_name>.labelsfor versioning and management. - Configuration Data: Moved to
values.yamlfor dynamic updates.
Original ConfigMap YAML Manifest:
apiVersion: v1
kind: ConfigMap
metadata:
name: <application_name>-config
namespace: <application_name>-ns
data:
config.yaml: |
SERVER_URL: "https://jsonplaceholder.typicode.com/users"
CONFIG_OPTION: "deployed config value"
Updated ConfigMap Helm Charts Template:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "<application_name>.fullname" . }}-config
namespace: {{ include "<application_name>.fullname" . }}-ns
labels:
{{ include "<application_name>.labels" . | indent 2 }}
data:
config.yaml: |
SERVER_URL: {{ .Values.appConfig.serverUrl | quote }}
CONFIG_OPTION: {{ .Values.appConfig.configOption | quote }}
Update values.yaml:
appConfig: serverUrl: "https://jsonplaceholder.typicode.com/users" configOption: "deployed config value"
For more details related to the final version of values.yaml , please refer → Complete values.yaml (Helm Charts)
7. Create a Secret Template
Purpose: Securely store sensitive data, such as passwords and API keys.
Explanation:
- Resource Name: Uses
<application_name>.fullnamefor consistent naming. - Labels: Added using
<application_name>.labels. - Secret Data: Moved to
values.yamlfor flexibility and security.
Original Secret YAML Manifest:
apiVersion: v1 kind: Secret metadata: name: <application_name>-secret namespace: <application_name>-ns type: Opaque stringData: SECRET_USERNAME: "username" SECRET_PASSWORD: "s_password" data: SECRET_BASE_64_ENCODED: VGhpcyBzdHJpbmcgd2FzIGVuY29kZWQgdXNpbmcgYmFzZTY0
Updated Secret Helm Charts Template:
apiVersion: v1
kind: Secret
metadata:
name: {{ include "<application_name>.fullname" . }}-secret
namespace: {{ include "<application_name>.fullname" . }}-ns
labels:
{{ include "<application_name>.labels" . | indent 2 }}
type: Opaque
stringData:
SECRET_USERNAME: {{ .Values.appSecret.secretUsername | quote }}
SECRET_PASSWORD: {{ .Values.appSecret.secretPassword | quote }}
data:
SECRET_BASE_64_ENCODED: {{ .Values.appSecret.secretBase64Encoded | quote }}
Update values.yaml:
appSecret: secretUsername: "username" secretPassword: "s_password" secretBase64Encoded: "VGhpcyBzdHJpbmcgd2FzIGVuY29kZWQgdXNpbmcgYmFzZTY0"
For more details related to the final version of values.yaml , please refer → Complete values.yaml (Helm Charts)
8. Create Service Template
Purpose: Define how to access your application from within the cluster.
Explanation:
- Resource Name: Utilizes
<application_name>.fullname. - Labels: Includes
<application_name>.labelsfor management. - Ports and Type: Defined in
values.yamlfor flexibility.
Original Service YAML Helm Charts Manifest:
apiVersion: v1
kind: Service
metadata:
namespace: <application_name>-ns
name: <application_name>
spec:
type: ClusterIP
selector:
app: <application_name>
ports:
- name: http
protocol: TCP
port: 8888
targetPort: 8000
Updated Service Template:
apiVersion: v1
kind: Service
metadata:
namespace: {{ include "<application_name>.fullname" . }}-ns
name: {{ include "<application_name>.fullname" . }}
labels:
{{- include "<application_name>.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
selector:
{{- include "<application_name>.selectorLabels" . | nindent 4 }}
ports:
- name: http
protocol: TCP
port: {{ .Values.service.port }}
targetPort: {{ .Values.service.targetPort }}
Update values.yaml:
service: type: ClusterIP targetPort: 8000 port: 8888
For more details related to the final version of values.yaml , please refer → Complete values.yaml (Helm Charts)
9. Create Deployment Template
Purpose: Define the deployment strategy for your application, including replicas, environment variables, and probes.
Explanation:
- Resource Names and Labels: Use helpers for consistent naming and labeling.
- Replicas: Configurable through
values.yaml. - Image: Defined in
values.yamlfor easy updates. - Environment Variables: Use values from
values.yaml. - Probes: Configured using values for health checks.
- Volume Mounts: ConfigMap usage is updated for flexibility.
Original Deployment YAML Manifest:
apiVersion: apps/v1
kind: Deployment
metadata:
name: <application_name>
namespace: <application_name>-ns
spec:
replicas: 1
selector:
matchLabels:
app: <application_name>
template:
metadata:
labels:
app: <application_name>
spec:
containers:
- name: <application_name>
image: <personal dockerhub repository>/<application_name>:<application_version>
ports:
- name: http
containerPort: 8000
protocol: TCP
env:
- name: CONFIG_PATH
value: /app/config.yaml
- name: BASE_PATH
value: /
- name: CUSTOM_ENV_VARIABLE
value: "deployed_env_value"
envFrom:
- secretRef:
name: <application_name>-secret
startupProbe:
httpGet:
path: "/health"
port: http
periodSeconds: 5
initialDelaySeconds: 5
livenessProbe:
httpGet:
path: "/health"
port: http
initialDelaySeconds: 5
periodSeconds: 60
volumeMounts:
- name: config-vol
mountPath: /app/config.yaml
subPath: config.yaml
volumes:
- name: config-vol
configMap:
name: <application_name>-config
Updated Deployment Helm Charts Template:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "<application_name>.fullname" . }}
namespace: {{ include "<application_name>.fullname" . }}-ns
labels:
{{- include "<application_name>.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "<application_name>.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "<application_name>.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.targetPort }}
protocol: TCP
env:
- name: CONFIG_PATH
value: {{ .Values.appEnv.configPath | quote }}
- name: BASE_PATH
value: {{ .Values.ingress.path | quote }}
- name: CUSTOM_ENV_VARIABLE
value: {{ .Values.appEnv.customEnvVariable | quote }}
envFrom:
- secretRef:
name: {{ include "<application_name>.fullname" . }}-secret
startupProbe:
httpGet:
path: "{{ .Values.ingress.path }}health"
port: http
periodSeconds: 5
initialDelaySeconds: 5
livenessProbe:
httpGet:
path: "{{ .Values.ingress.path }}health"
port: http
initialDelaySeconds: 5
periodSeconds: 60
volumeMounts:
- name: config-vol
mountPath: {{ .Values.appEnv.configPath | quote }}
subPath: config.yaml
volumes:
- name: config-vol
configMap:
name: {{ include "<application_name>.fullname" . }}-config
Updated values.yaml:
replicaCount: 1 image: pullPolicy: IfNotPresent repository: <personal dockerhub repository>/<application_name> tag: "<application_version>" service: type: ClusterIP targetPort: 8000 port: 8888 ingress: path: / appConfig: serverUrl: "https://jsonplaceholder.typicode.com/users" configOption: "deployed config value" appSecret: secretUsername: "username" secretPassword: "s_password" secretBase64Encoded: "VGhpcyBzdHJpbmcgd2FzIGVuY29kZWQgdXNpbmcgYmFzZTY0" appEnv: configPath: /app/config.yaml customEnvVariable: deployed_via_helm_env_value
For more details related to the final version of values.yaml , please refer → Complete values.yaml (Helm Charts)
10. Create TLS Secret Template
Purpose: Secure communication using TLS.
Explanation:
- Resource Name and Namespace: Uses helpers for naming consistency.
- TLS Data: Moved to
values.yamlfor security and flexibility.
Original TLS Secret YAML Manifest:
apiVersion: v1 kind: Secret metadata: name: <application_name>-secrets-tls namespace: <application_name>-ns type: kubernetes.io/tls data: tls.crt: <Base64_certificate> tls.key: <Base64_key>
Updated TLS Secret Helm Charts Template
apiVersion: v1
kind: Secret
metadata:
name: {{ include "<application_name>.fullname" . }}-secrets-tls
namespace: {{ include "<application_name>.fullname" . }}-ns
type: kubernetes.io/tls
data:
tls.crt: {{ .Values.ingress.tls.crt }}
tls.key: {{ .Values.ingress.tls.key }}
Update values.yaml:
ingress:
path: /
tls:
crt: "<Base64_certificate>"
key: "<Base64_key>"
For more details related to the final version of values.yaml , please refer → Complete values.yaml (Helm Charts)
11. Create Ingress Route Template
Purpose: Define routes and middlewares for your application using Traefik.
Explanation:
- Resource Names and Namespace: Utilizes helpers for consistency.
- Route Matching: Configured using
values.yamlfor host and path. - Services and Middlewares: Linked to services and middleware using helpers.
- TLS Configuration: Uses values for certificates.
Original Ingress Route YAMLManifest:
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: <application_name>-redirect-scheme
namespace: <application_name>-ns
spec:
redirectScheme:
scheme: https
permanent: true
port: "443"
---
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: <application_name>-strip-prefix
namespace: <application_name>-ns
spec:
stripPrefix:
prefixes:
- /
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: <application_name>-http
namespace: <application_name>-ns
spec:
entryPoints:
- web
routes:
- match: Host(`local_application.com`) && PathPrefix(`/`)
kind: Rule
services:
- name: <application_name>
namespace: <application_name>-ns
port: 8888
middlewares:
- name: <application_name>-redirect-scheme
- name: <application_name>-strip-prefix
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: <application_name>-https
namespace: <application_name>-ns
spec:
entryPoints:
- websecure
routes:
- match: Host(`local_application.com`) && PathPrefix(`/`)
kind: Rule
services:
- name: <application_name>
namespace: <application_name>-ns
port: 8888
middlewares:
- name: <application_name>-strip-prefix
tls:
secretName: <application_name>-secrets-tls
domains:
- main: local_application.com
Updated Ingress Route Helm Charts Template:
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: {{ include "<application_name>.fullname" . }}-redirect-scheme
namespace: {{ include "<application_name>.fullname" . }}-ns
spec:
redirectScheme:
scheme: https
permanent: true
port: "443"
---
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: {{ include "<application_name>.fullname" . }}-strip-prefix
namespace: {{ include "<application_name>.fullname" . }}-ns
spec:
stripPrefix:
prefixes:
- {{ .Values.ingress.path }}
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: {{ include "<application_name>.fullname" . }}-http
namespace: {{ include "<application_name>.fullname" . }}-ns
labels:
{{- include "<application_name>.labels" . | nindent 4 }}
spec:
entryPoints:
- web
routes:
- match: Host(`{{ .Values.ingress.host }}`) && PathPrefix(`{{ .Values.ingress.path }}`)
kind: Rule
services:
- name: {{ include "<application_name>.fullname" . }}
namespace: {{ include "<application_name>.fullname" . }}-ns
port: {{ .Values.service.port }}
middlewares:
- name: {{ include "<application_name>.fullname" . }}-redirect-scheme
- name: {{ include "<application_name>.fullname" . }}-strip-prefix
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: {{ include "<application_name>.fullname" . }}-https
namespace: {{ include "<application_name>.fullname" . }}-ns
labels:
{{- include "<application_name>.labels" . | nindent 4 }}
spec:
entryPoints:
- websecure
routes:
- match: Host(`{{ .Values.ingress.host }}`) && PathPrefix(`{{ .Values.ingress.path }}`)
kind: Rule
services:
- name: {{ include "<application_name>.fullname"
Update values.yaml:
ingress:
host: "local_application.com"
path: /
tls:
crt: "<Base64_certificate>"
key: "<Base64_key>"
For more details related to the final version of values.yaml , please refer → Complete values.yaml (Helm Charts)
12. complete values.yaml Helm Charts template:
replicaCount: 1
image:
pullPolicy: IfNotPresent
repository: <personal dockerhub repository>/<application_name>
tag: "<application_version>"
service:
type: ClusterIP
targetPort: 8000
port: 8888
ingress:
host: "local_application.com"
path: /
tls:
crt: "<Base64_certificate>"
key: "<Base64_key>"
appConfig:
serverUrl: "https://jsonplaceholder.typicode.com/users"
configOption: "deployed config value"
appSecret:
secretUsername: "username"
secretPassword: "s_password"
secretBase64Encoded: "VGhpcyBzdHJpbmcgd2FzIGVuY29kZWQgdXNpbmcgYmFzZTY0"
appEnv:
configPath: /app/config.yaml
customEnvVariable: deployed_via_helm_env_value
Verification: Ensure Everything Works as Expected
After creating all the necessary templates and configuration files, it's crucial to verify that everything is working correctly.
To ensure the correctness of the Helm charts, "helm lint" command can be used.
This command performs basic syntax validation and checks for common issues within the Helm chart files.
helm lint <path/to/directory/with/charts>
Review the output for any errors or warnings.
Helm lint will provide feedback on potential issues such as missing or incorrectly formatted values, syntax errors in templates, or other problems that could prevent chart from deploying correctly.
1. Deploy Application with Helm Charts
Deploy your application using the prepared Helm charts:
From the namespace.yaml template, we can see that the namespace will be created in the following format: <release-name>-<chart-name>-ns.
In this setup, we use a duplicated app name for the namespace to simplify local development. This works because, in our example, <release-name> = <chart-name> = <application_name>.
In the Add-On Mart infrastructure, both the release name and app name will be enhanced for better distinction, but this approach is used for simplicity in the current setup.
helm install <application_name> ./path/to/directory/with/charts --namespace <application_name>-<application_name>-ns --create-namespace
2. Verify Application Functionality
2.1 Test /health endpoint
> curl -k -X 'GET' 'https://local_application.com/health' | jq .
{
"status": "OK"
}
2.2 Test /config_values endpoint
> curl -k -X 'GET' 'https://local_application.com/config_values' | jq .
{
"SERVER_URL": "https://jsonplaceholder.typicode.com/users",
"CONFIG_OPTION": "deployed config value"
}
2.3 Test /environment_variables endpoint with secrets
> curl -k -X 'GET' 'https://local_application.com/environment_variables' | jq . | grep -E 'SECRET_BASE_64_ENCODED|SECRET_PASSWORD|SECRET_USERNAME'
"SECRET_BASE_64_ENCODED": "This string was encoded using base64", "SECRET_PASSWORD": "s_password", "SECRET_USERNAME": "username"
Summary:
This guide explained how to convert Kubernetes manifests into Helm chart templates, allowing for a more flexible and reusable deployment process.
By parameterizing values such as image tags, service ports, and environment variables, we transformed static manifests into dynamic templates that adapt to different environments.
This approach enables easier management, version control, and scalability of Kubernetes applications using Helm.