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
, andwith
to 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.v2
is 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 ismyrelease
and 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>.fullname
helper 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>.fullname
for naming consistency. - Labels: Adds labels using
<application_name>.labels
for versioning and management. - Configuration Data: Moved to
values.yaml
for 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>.fullname
for consistent naming. - Labels: Added using
<application_name>.labels
. - Secret Data: Moved to
values.yaml
for 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>.labels
for management. - Ports and Type: Defined in
values.yaml
for 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.yaml
for 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.yaml
for 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.yaml
for 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.