This Tech Bite was brought to you by Mujo Hadžimehanović, DevOps Engineer at Atlantbh.

Tech Bites are tips, tricks, snippets or explanations about various programming technologies and paradigms, which can help engineers with their everyday job.

Deploying an application to Kubernetes with Helm

 

Introduction

This Tech Bite will show you how to deploy a simple frontend application to a Kubernetes cluster and the benefits of using Helm charts.

Plain k8s approach

In order to deploy our application and make it usable we have to generate 3 Kubernetes objects – Deployment, Service and Ingress. In the classical approach we need to create three files for each object.

deployment.yaml

apiVersion: apps/v1

kind: Deployment

metadata:

  name: nginx-test

  labels:

    app.kubernetes.io/name: nginx-test

    app.kubernetes.io/instance: ngx-test

spec:

  replicas: 1

  selector:

    matchLabels:

      app.kubernetes.io/name: nginx-test

      app.kubernetes.io/instance: ngx-test

  template:

    metadata:

      labels:

        app.kubernetes.io/name: nginx-test

        app.kubernetes.io/instance: ngx-test

    spec:

      serviceAccountName: default

      securityContext:

        {}

      containers:

        - name: nginx-test

          securityContext:

            {}

          image: "nginx"

          imagePullPolicy: Always

          ports:

            - name: http

              containerPort: 80

              protocol: TCP

service.yaml

apiVersion: v1

kind: Service

metadata:

  name: nginx-test

  labels:

    app.kubernetes.io/name: nginx-test

    app.kubernetes.io/instance: ngx-test

spec:

  type: ClusterIP

  ports:

    - port: 80

      targetPort: 80

      protocol: TCP

      name: http

  selector:

    app.kubernetes.io/name: nginx-test

    app.kubernetes.io/instance: ngx-test

ingress.yaml

apiVersion: networking.k8s.io/v1beta1

kind: Ingress

metadata:

  name: nginx-test

  labels:

    app.kubernetes.io/name: nginx-test

    app.kubernetes.io/instance: ngx-test

  annotations:

    cert-manager.io/cluster-issuer: letsencrypt-nginx

spec:

  tls:

    - hosts:

        - "nginx-test.example.com"

      secretName: tls-nginx

  rules:

    - host: "nginx-test.example.com"

      http:

        paths:

          - path: /

            backend:

              serviceName: nginx-test

              servicePort: 80

However, this problem can be solved by merging those three yaml files into a single one and executing kubectl apply -f frontend-app.yaml to spin up needed k8s objects.

But what if we have multiple environments? To which one should the application be deployed? For every environment we need to change the pod name, ingress host address etc… In order to replace those values we will need to create a yaml file for every environment with almost the same data.

The solution is simple – we are going to use Helm which is “The package manager for Kubernetes”.

Helm

As we have stated before, Helm is the package manager for Kubernetes applications which allows us to define blueprints for application deployment. All the necessary resources are going to be packed into Helm Charts and deployed to the cluster.

Let’s get to work and create a Helm chart for our application.

helm create test-appAfter helm create command is executed we will get a test-app folder and the structure is going to be similar to this:

We are going to put the Kubernetes objects that we need to generate in the templates/ folder (deployment.yaml, service.yamland ingress.yaml).  You will also notice the values.yaml file which in combination with Helm templates generates our k8s objects. Our files, ready for chart deployment, should look like this:

deployment.yaml

apiVersion: apps/v1

kind: Deployment

metadata:

  name: {{ include "test-app.fullname" . }}-ui

  labels:

{{ include "test-app.labels" . | indent 4 }}

spec:

  replicas: {{ .Values.replicaCount }}

  selector:

    matchLabels:

      app.kubernetes.io/name: {{ include "test-app.name" . }}-ui

      app.kubernetes.io/instance: {{ .Release.Name }}

  template:

    metadata:

      labels:

        app.kubernetes.io/name: {{ include "test-app.name" . }}-ui

        app.kubernetes.io/instance: {{ .Release.Name }}

    spec:

    {{- with .Values.imagePullSecrets }}

      imagePullSecrets:

        {{- toYaml . | nindent 8 }}

    {{- end }}

      serviceAccountName: {{ template "test-app.serviceAccountName" . }}

      securityContext:

        {{- toYaml .Values.podSecurityContext | nindent 8 }}

      containers:

        - name: {{ .Chart.Name }}-ui

          securityContext:

            {{- toYaml .Values.securityContext | nindent 12 }}

          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"

          imagePullPolicy: {{ .Values.image.pullPolicy }}

          ports:

            - name: http

              containerPort: 80

              protocol: TCP

service.yaml

apiVersion: v1

kind: Service

metadata:

  name: {{ include "test-app.fullname" . }}-ui

  labels:

{{ include "test-app.labels" . | indent 4 }}

spec:

  type: {{ .Values.service.ui.type }}

  ports:

    - port: {{ .Values.service.ui.port }}

      targetPort: 80

      protocol: TCP

      name: http

  selector:

    app.kubernetes.io/name: {{ include "test-app.name" . }}-ui

    app.kubernetes.io/instance: {{ .Release.Name }}

ingress.yaml

apiVersion: extensions/v1beta1

kind: Ingress

metadata:

  name: {{ $fullName }}-ui

  labels:

{{ include "test-app.labels" . | indent 4 }}

  {{- with .Values.ingress.ui.annotations }}

  annotations:

    {{- toYaml . | nindent 4 }}

  {{- end }}

spec:

{{- if .Values.ingress.ui.tls }}

  tls:

  {{- range .Values.ingress.ui.tls }}

    - hosts:

      {{- range .hosts }}

        - {{ . | quote }}

      {{- end }}

      secretName: {{ .secretName }}

  {{- end }}

{{- end }}

  rules:

  {{- range .Values.ingress.ui.hosts }}

    - host: {{ .host | quote }}

      http:

        paths:

        {{- range .paths }}

          - path: {{ . }}

            backend:

              serviceName: {{ $fullName }}-ui

              servicePort: {{ $svcPort }}

        {{- end }}

  {{- end }}

{{- end }}

In our deployment.ymltemplate file you can see variable definitions between two pairs of curly braces. These values are just references to the values.yaml file. For example, image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}” says that image name is going to take image.repository and image.tag values and generate full image name. A snippet of these values is shown below.

Once we have all the files set in the templates/ folder we can proceed with generating and deploying the chart to the k8s cluster. 

helm install test-app app/

If the command was successfully executed, after running kubectl get po,svc,ing we should see our objects up and running. In case we have dev and prod environments for the application, we can simply create values-dev.yml and values-prod.yml and reference them when creating charts.

helm install --namespace dev --create-namespace --values values-dev.yaml test-app app/ for DEV environment

helm install --namespace prod --create-namespace --values values-prod.yaml test-app app/ for PROD environment

--namespace – defines namespace of the environment to which we are deploying

--create-namespace – generates new namespace if it is not existing

--values – location to values file

You can find the full example code on github. Follow this link.

Leave a Reply