|

Deploying Applications to Kubernetes with Helm

Kubernetes supports declarative approach to define its resources. This means you can add configuration on Kubernetes resources in yaml files in which basically you define the state of that particular resource and then deploy those files.

Imperative vs Declarative

Imperative means you use cli commands to create resources. For example,

kubectl run nginx --image=nginx:1.14.2 --port=80

The declarative approach is to create a yaml file let’s say nginx-pod.yaml with following contents

apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - name: nginx
    image: nginx:1.14.2
    ports:
    - containerPort: 80

and then apply the yaml file. Such yaml files are called Kubernetes manifests.

kubectl apply -f nginx-pod.yaml

As you can see that obvious benefit of declarative approach is that you can put yaml files in version control tool like github and it will be a lot easier to do future updates as configuration is saved. It’s hard to do the same with imperative approach.

What is Helm and how its helps?

Helm is the package manager for Kubernetes just like yum or apt. Helm deploys charts which are packaged Kubernetes software or microservices.

While declarative approach is great it can become cumbersome to deal with yamls if you have hundreds or thousands of microservices as per microservice at least you will need 3 yaml manifest files (For Dev, Non-Production and Production environments) and that too with duplicated code and hardcoded values . Hence making changes and maintaining can be issues prone and nightmare for DevOps.

Helm provides a solution to this using templating and hence producing re-usable code with no hardcoding.

Helm is not just a package manager, it is also have other features like configuration management, hooks, rollbacks.

In addition to this Helm has huge user base and hence strong community. You can get pre-built helm charts for popular tools from repositories. You can find such repositories at https://artifacthub.io/

Please note helm itself uses commands and hence is imperative.

Set up lab for helm

Let’s first quickly set up a lab to learn helm

In order to learn helm, we need kubernetes cluster and helm.

We will first install kind. Please follow this link -> Install Kind

Now create kubernetes cluster. If you followed above link and already created, then skip this step.

kind create cluster -n test-helm --image kindest/node:v1.24.7

Now let’s use docker to have isolated environment for learning.

docker run -it --rm -v ${HOME}:/root/ -v ${PWD}:/work -w /work --net host alpine sh

Now lets install kubectl as well. We won’t use kubectl for deployment but we will use it to verify our deployments.

apk add --no-cache curl
curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl
chmod +x ./kubectl
mv ./kubectl /usr/local/bin/kubectl

Verify our cluster

/work # kubectl get nodes
NAME                      STATUS   ROLES           AGE   VERSION
test-helm-control-plane   Ready    control-plane   11m   v1.24.7

Now install helm

curl -LO https://get.helm.sh/helm-v3.10.3-linux-amd64.tar.gz
tar -C /tmp/ -zxvf helm-v3.10.3-linux-amd64.tar.gz
rm helm-v3.10.3-linux-amd64.tar.gz
mv /tmp/linux-amd64/helm /usr/local/bin/helm
chmod +x /usr/local/bin/helm

Verify installation

work # helm version
version.BuildInfo{Version:"v3.10.3", GitCommit:"835b7334cfe2e5e27870ab3ed4135f136eecc704", GitTreeState:"clean", GoVersion:"go1.18.9"}

This finish all installations we need to start creating and deploying helm charts.

Deploy first helm chart

Before we create our own custom chart, first we will deploy a pre-built helm charts to our cluster.

You can search pre-built helm repositories from https://artifacthub.io/ which are available for public use.

We will install nginx from bitnami repository. In order to do this, we need to add the repository and then install the chart.

/work # helm repo add my-bitnami-repo https://charts.bitnami.com/bitnami
"my-bitnami-repo" has been added to your repositories

The above will add bitnami repo to my local. You can name it anything. I have named it my-bitnami-repo.

You can perform a search to see all charts available for use. Bitnami has really good charts available.

helm search repo my-bitnami-repo

Now we will install our nginx chart. I am calling it my-nginx-release and you can choose a name of your choice or use same. It can be anything.

/work # helm install my-nginx-release my-bitnami-repo/nginx
NAME: my-nginx-release
LAST DEPLOYED: Fri Dec 23 02:23:16 2022
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
CHART NAME: nginx
CHART VERSION: 13.2.20
APP VERSION: 1.23.3

** Please be patient while the chart is being deployed **
NGINX can be accessed through the following DNS name from within your cluster:

    my-nginx-release.default.svc.cluster.local (port 80)

To access NGINX from outside the cluster, follow the steps below:

1. Get the NGINX URL by running these commands:

  NOTE: It may take a few minutes for the LoadBalancer IP to be available.
        Watch the status with: 'kubectl get svc --namespace default -w my-nginx-release'

    export SERVICE_PORT=$(kubectl get --namespace default -o jsonpath="{.spec.ports[0].port}" services my-nginx-release)
    export SERVICE_IP=$(kubectl get svc --namespace default my-nginx-release -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
    echo "http://${SERVICE_IP}:${SERVICE_PORT}"

Let’s check with kubectl

/work # kubectl get deployment
NAME               READY   UP-TO-DATE   AVAILABLE   AGE
my-nginx-release   1/1     1            1           116s

/work # kubectl get pods
NAME                                READY   STATUS    RESTARTS   AGE
my-nginx-release-5f66c8c65b-nfl78   1/1     Running   0          56s

/work # kubectl get svc
NAME               TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
kubernetes         ClusterIP      10.96.0.1       <none>        443/TCP        41m
my-nginx-release   LoadBalancer   10.96.139.231   <pending>     80:31031/TCP   87s

As you see above it tries to create a Load Balancer above but since we have local cluster, it is in pending state.

Let’s open another terminal and expose the pod as we do not have load balancer and hence cannot use service as mentioned in helm install description logs above. (NOTE: Do not run inside the docker but on local OS/HOST)

$ kubectl port-forward pod/my-nginx-release-5f66c8c65b-nfl78 9080:8080 

Let’s check http://localhost:9080

So we have successfully deployed a helm chart into our cluster. Instead of kubectl, we used helm cli.

We can also modify some values like change container port from 8080 to 8090. (Note: We won’t be able to change to 80 as it is a privileged port and nginx chart we are deploying has security context enabled. I am not explaining security context as it is out of scope of this blog.)

We need to run following command

helm show values my-bitnami-repo/nginx

You will see a huge list of values but what we are interested in is as follows:

## Configures the ports NGINX listens on
## @param containerPorts.http Sets http port inside NGINX container
## @param containerPorts.https Sets https port inside NGINX container
##       
containerPorts:
  http: 8080
  https: ""

Or better we can check here: https://artifacthub.io/packages/helm/bitnami/nginx

We can now change port as follows

We will delete and install again.

helm uninstall my-nginx-release
helm install --set containerPorts.http=8090 my-nginx-release my-bitnami-repo/nginx

Verify pod

/work # kubectl get pods
NAME                               READY   STATUS    RESTARTS   AGE
my-nginx-release-557bd4598-h29ds   1/1     Running   0          3m24s

Now we will expose the pod on local OS/HOST in another terminal

kubectl port-forward pod/my-nginx-release-557bd4598-h29ds 3080:8090

Verify in browser http://localhost:3080

So that’s how you can install a predefined helm chart.

Creating Custom chart with Helm

Now we will head into creating our own custom helm chart. We will create our resources as Helm templates to understand its benefits

We will create a simple chart myfirstchart

/work # helm create myfirstchart
Creating myfirstchart

It will generate a boiler plate code as files as follows:

/work/myfirstchart # ls -lrt
total 8
-rw-r--r--    1 root     root          1879 Dec 23 03:14 values.yaml
drwxr-xr-x   10 root     root           320 Dec 23 03:14 templates
drwxr-xr-x    2 root     root            64 Dec 23 03:14 charts
-rw-r--r--    1 root     root          1148 Dec 23 03:14 Chart.yaml

Let’s delete everything under tempaltes

/work/myfirstchart # rm -rf templates/*

Now let’s create our first template by creating a file myfirstchart/templates/configmap.yaml with below contents

apiVersion: v1
kind: ConfigMap
metadata:
  name: myfirstchart-configmap
data:
  myvalue: "Hello World"

And now we will install the chart

/work # helm install my-cutom-chart ./myfirstchart
NAME: my-cutom-chart
LAST DEPLOYED: Fri Dec 23 03:21:36 2022
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None

We can see configmap is created in our cluster

/work # kubectl get configmap
NAME                     DATA   AGE
kube-root-ca.crt         1      99m
myfirstchart-configmap   1      90s

helm let us retrieve the release and see the actual template that was loaded as follows

/work # helm get manifest my-cutom-chart
---
# Source: myfirstchart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: myfirstchart-configmap
data:
  myvalue: "Hello World"

Now we will uninstall and try to remove some hardcoding

helm uninstall my-cutom-chart

Modify the myfirstchart/templates/configmap.yaml file to below

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"

We have used in built helm variable which will be set to whatever release name we set. Now lets deploy again with release name my-template-demo

/work # helm install my-template-demo ./myfirstchart
NAME: my-template-demo
LAST DEPLOYED: Fri Dec 23 03:29:26 2022
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None

Now we will check what was deployed

/work # helm get manifest my-template-demo
---
# Source: myfirstchart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: my-template-demo-configmap
data:
  myvalue: "Hello World"

As you can see name metadata is set according to release name we choose.

We can also define our own Variables in  myfirstchart/values.yaml which was autogenerated. Delete everything inside this file and just add

myMessage: "Helm is awesome"

Now modify the myfirstchart/templates/configmap.yaml as follows

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: {{ .Values.myMessage }}

As you can see we changed myValue to pick value from values.yaml file.

Let’s install a new helm release now.

work # helm install my-template-demo-2 ./myfirstchart
NAME: my-template-demo-2
LAST DEPLOYED: Fri Dec 23 03:37:32 2022
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None

Lets verify changes

/work # helm get manifest my-template-demo-2
---
# Source: myfirstchart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: my-template-demo-2-configmap
data:
  myvalue: Helm is awesome

As you can see value got picked up from values.yaml file

DevOps Tip: You can just store these files and folders in a github repository and then in your CICD pipeline you can run helm commands. You won’t have to publish these charts like predefined charts.

Multiple environments with Helm

Till now you have learned how to use Helm to deploy your kubernetes resources. Now before we end this article let’s talk about how to use Helm with multiple environments. There can be many approaches but I am going to explain a simple one.

In real projects, you will have to deploy your resources to different environments and hence there will be some environment specific values as well.

We will add one more file myfirstchart/values-dev.yaml and add below:

env: development

Now we will add this variable to myfirstchart/values.yaml as well

myMessage: "Helm is awesome"
env: localhost

As you can see we added same in values.yaml to have default value.

In the end, we also need to modify our myfirstchart/templates/configmap.yaml file to use new variable

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-{{ .Values.env }}-configmap
data:
  myvalue: {{ .Values.myMessage }}

We modified name metadata to add env.

Now we will deploy new release as below. The values.yaml is read first and the values-dev.yaml we use in below command will overwrite any matching values.

/work # helm install my-multienv-demo -f myfirstchart/values-dev.yaml ./myfirstchart
NAME: my-multienv-demo
LAST DEPLOYED: Fri Dec 23 04:03:31 2022
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None

Let’s check what we deployed

/work # helm get manifest my-multienv-demo
---
# Source: myfirstchart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: my-multienv-demo-development-configmap
data:
  myvalue: Helm is awesome

As you can see, it picked up value from values-dev.yaml file for env.

So this way you add all common variables in values.yaml and environment specific variables in files like values-<env>.yaml and use them in your CICD pipeline to set environment specific values.

Conclusion

That’s all!! We finished learning basics of helm. I hope you understand the benefits of helm. For DevOps it is a must learn tool as it is used by many companies. Do try to practice all above. Best of luck!

Similar Posts