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, and with 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 from values.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.
./charts-local/Chart.yaml
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:

./charts-local/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 is myrelease and the chart name is <application_name>, this helper will return myrelease-<application_name>.
  • <application_name>.chart: Returns a string with the chart name and version.
    For a chart named <application_name> with version 1.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.
./charts-local/templates/_helpers.tpl
{{- 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:

./Manifests_local/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: <application_name>-ns

Updated Namespace Helm Charts Template :

./charts-local/templates/namespace.yaml
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:

./Manifests_local/config-cm.yaml
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:

./charts-local/config-cm.yaml
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:

./charts-local/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:

./Manifests_local/secret.yaml
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:

./charts-local/secret.yaml
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:

./charts-local/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:

./Manifests_local/service.yaml
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:

./charts-local/service.yaml
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:

./charts-local/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:

./Manifests_local/deployment.yaml
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:

./charts-local/deployment.yaml
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:

./charts-local/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:

./Manifests_local/tls-secret.yaml
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

./charts-local/tls-secret.yaml
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:

./charts-local/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:

./Manifests_local/ingressroute.yaml
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:

./charts-local/ingressroute.yaml
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:

./charts-local/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: 

./charts-local/values.yaml
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 

Request
 > curl -k  -X 'GET' 'https://local_application.com/health' | jq .
Response
 {
  "status": "OK"
}

2.2 Test /config_values endpoint

Request
 > curl -k  -X 'GET' 'https://local_application.com/config_values' | jq .
Response
 {
  "SERVER_URL": "https://jsonplaceholder.typicode.com/users",
  "CONFIG_OPTION": "deployed config value"
}

2.3  Test /environment_variables endpoint with secrets

Request
 > curl -k  -X 'GET' 'https://local_application.com/environment_variables' | jq . | grep -E 'SECRET_BASE_64_ENCODED|SECRET_PASSWORD|SECRET_USERNAME'
Response
 
  "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.