Helm: reusable chart — named templates, and a generic chart for multiple applications
Tags: kubernetes, devops, todayilearned, tutorial
Published: 2020-10-20 12:54:52

---
title: Helm: reusable chart — named templates, and a generic chart for multiple applications
published: true
date: 2020-10-20 07:43:24 UTC
tags: kubernetes,devops,todayilearned,tutorial
canonical_url: https://rtfm.co.ua/en/helm-reusable-chart-named-templates-and-a-generic-chart-for-multiple-applications/
---

### Helm: reusable chart — named templates, and a generic chart for multiple applications

![](https://cdn-images-1.medium.com/max/795/1*Oy91xGZZ-BTU1WcRfpg_SA.png)

Our project is growing, and more and more applications are started on the AWS Elastic Kubernetes Service.

Finally, we’ve faced with the question already mentioned in the [Helm: пошаговое создание чарта и деплоймента из Jenkins](https://rtfm.co.ua/helm-poshagovoe-sozdanie-charta-i-deplojmenta-iz-jenkins/) (_Rus_) — what to do with Kubernetes manifests and Helm templates when using a lot of similar applications?

Especially now, when we have the same code deployed from the same repository but three similar applications.

At first, we had an only one, then was added another one, and now we are preparing to release the third one.

As result, their chart now looks like the next:

```
$ tree -d k8s/
k8s/
├── app1-chart
│ ├── charts
│ ├── env
│ │ ├── dev
│ │ ├── dev-2
│ │ └── prod
│ └── templates
└── app2-chart
├── charts
├── env
│ ├── dev
│ ├── prod
│ └── stage
└── templates
```

So, now I need to copy the same chart again?

Don’t like that idea at all, so let’s take a deeper view of the Helm templates and will try to create one chart which can be used to deploy three similar applications.

### Contents

- [Deployment — the file’s structure](https://rtfm.co.ua/en/helm-reusable-chart-named-templates-and-a-generic-chart-for-multiple-applications/#Deployment_%E2%80%93_the_file%E2%80%99s_structure)
- [Helm Named templates](https://rtfm.co.ua/en/helm-reusable-chart-named-templates-and-a-generic-chart-for-multiple-applications/#Helm_Named_templates)
- [Common labels](https://rtfm.co.ua/en/helm-reusable-chart-named-templates-and-a-generic-chart-for-multiple-applications/#Common_labels)
- [define](https://rtfm.co.ua/en/helm-reusable-chart-named-templates-and-a-generic-chart-for-multiple-applications/#define)
- [include](https://rtfm.co.ua/en/helm-reusable-chart-named-templates-and-a-generic-chart-for-multiple-applications/#include)
- [indent vs nindent](https://rtfm.co.ua/en/helm-reusable-chart-named-templates-and-a-generic-chart-for-multiple-applications/#indent_vs_nindent)
- [Templates inside of a template](https://rtfm.co.ua/en/helm-reusable-chart-named-templates-and-a-generic-chart-for-multiple-applications/#Templates_inside_of_a_template)
- [The env block from a project’s values.yaml file](https://rtfm.co.ua/en/helm-reusable-chart-named-templates-and-a-generic-chart-for-multiple-applications/#The_env_block_from_a_project%E2%80%99s_values_yaml_file)
- [The rangeloop](https://rtfm.co.ua/en/helm-reusable-chart-named-templates-and-a-generic-chart-for-multiple-applications/#The_rangeloop)
- [The range loop with variables](https://rtfm.co.ua/en/helm-reusable-chart-named-templates-and-a-generic-chart-for-multiple-applications/#The_range_loop_with_variables)
- [toYaml](https://rtfm.co.ua/en/helm-reusable-chart-named-templates-and-a-generic-chart-for-multiple-applications/#toYaml)
- [Whitespaces](https://rtfm.co.ua/en/helm-reusable-chart-named-templates-and-a-generic-chart-for-multiple-applications/#Whitespaces)
- [Helm tpl](https://rtfm.co.ua/en/helm-reusable-chart-named-templates-and-a-generic-chart-for-multiple-applications/#Helm_tpl)
- [The env block from the _helpers.tpl](https://rtfm.co.ua/en/helm-reusable-chart-named-templates-and-a-generic-chart-for-multiple-applications/#The_env_block_from_the_helpers_tpl)
- [if/else — flow control](https://rtfm.co.ua/en/helm-reusable-chart-named-templates-and-a-generic-chart-for-multiple-applications/#if/else_%E2%80%94_flow_control)
- [The env block from an external file](https://rtfm.co.ua/en/helm-reusable-chart-named-templates-and-a-generic-chart-for-multiple-applications/#The_env_block_from_an_external_file)
- [Excluding a template from the chart](https://rtfm.co.ua/en/helm-reusable-chart-named-templates-and-a-generic-chart-for-multiple-applications/#Excluding_a_template_from_the_chart)

### Deployment — the file’s structure

To think about future generic template’s structure let’s check which blocks will be common and shared among all applications and which will be different and will be included to the template.

Here, we will speak mostly about the `deployment.yaml` file and others will be done similar to it.

So:

- the `deployment.yaml` file:
- `metadata`:
- `name`: common, a value will be taken from the values.yaml of a specific project
- `annotations`: common and we have the only `reloader.stakater.com/auto` here and it always will be set to _true_ (still, check the [Kubernetes: ConfigMap and Secrets – data auto-reload in pods](https://rtfm.co.ua/en/kubernetes-configmap-and-secrets-data-auto-reload-in-pods/) post about other available options)
- `labels`: common and will be used in different places, for example in the same file below in the `spec.template.metadata.labels` and in the template with the Kubernetes Cronjobs, so it will be a good idea to move labels into the `_helpers.yaml` file
- `spec`:
- `replicas`: common, a value will be taken from the `values.yaml` of a specific project
- `strategy`:
- `type`: common, a value will be taken from the `values.yaml` of a specific project
- `selector`:
- `matchLabels`: also common and also will be used in different places, let's move it into the `_helpers.yaml`
- `template`:
- `metadata`:
- `labels`: will be taken from the `_helpers.yaml`
- `spec`:
- `containers`:
- `name`: common, a value will be taken from the `values.yaml` of a specific project
- `image`: common, a value will be taken from the `values.yaml` of a specific project
- `env`: the most interesting part of this post:

1. we can move it into the `values.yaml` of each project  
- pros: no need to specify values as they can be set directly here in the `value` fields of the variables  
- cons:   
- our variables are almost identical among all the projects — so they will be duplicated  
- the `values.yaml` will be bloated as we have a lot of variables to be added  
- not all variables are in the _KEY:VALUE_ view — some will take their values from a `valueFrom.secretKeyRef.<KUBE_SECRET_NAME>`, thus will be unable to set its values directly in the values.yaml
2. another way is to use the `_helpers.yaml` file where we will have a set of common variables, each with its own `if/then` - if the `values.yaml` of a project will not contain a value for a variable then such a variable will not be added into the generic template and thus will not break a project's deploy by adding a new variable
3. and for a case when a project will have to have its own set of variables — we can add a condition before the env block by checking something like if `{{ .Values.chartSettings.customEnvs == true }}` and will skip adding the block from the `_helpers.yaml` or `values.yaml` and will use a dedicated file instead with something like `tpl .File.Get project1/envs.yaml`
- `volumeMounts`: also can differ, will think about it later
- `ports`:
- `containerPort`: will be always, will take its value from the `values.yaml` (btw, it's not necessary to add them here at all, see the [Should I configure the ports in the Kubernetes deployment?](https://medium.com/faun/should-i-configure-the-ports-in-kubernetes-deployment-c6b3817e495) post)
- `livenessProbe`, `readinessProbe`: will be common with the `httpGet.path` and `httpGet.port` - so can be moved into the `_helpers.yaml` but also add an option to include it via an external file with the `.File.Get`
- `resources`:
- `requests.cpu`, `requests.memory` - well... I guess will be added for all, but still, it's a question - need to think about it later check the [Kubernetes: Evicted pods and Pods Quality of Service](https://rtfm.co.ua/en/kubernetes-evicted-pods-and-pods-quality-of-service/) post for more details
- `limits.cpu`, `limits.memory` - the memory will be limited for all... or not... will think later
- `volumes`: also can differ, leave it for later
- `imagePullSecrets`: similar for all
- `hpa.yaml` - for the HPA
- `network.yaml` - Service, Ingress
- `secrets.yaml` - Kubernetes Secrets
- `rbac.yaml` - attaching user groups into a c namespace created by Helm during deployment
- `cronjobs.yaml` - Kubernetes Cronjobs
- `_helpers.tpl` - and our "helper" for the [Helm Named Templates](https://helm.sh/docs/chart_template_guide/named_templates/)

So, basically, the main idea is the following:

- a general chart with templates in the templates directory
- in the templates will keep the `deployment.yaml`, `hpa.yaml`, `_helpers.tpl`, etc
- near the templates will create an additional directory called `projects`
- and inside it — catalogs by projects manes
- _project1_
- _project2_
- _project3_
- and inside each of them will have an `env` directory
- with `dev`, `stage`, `prod` directories
- with dedicated `values.yaml` and `secrets.yaml` files

### Helm Named templates

Good post about named templates — [How To Reduce Helm Chart Boilerplate With Named Templates](https://austindewey.com/2020/08/09/how-to-reduce-helm-chart-boilerplate-with-named-templates/) (more links at the end of this post).

The official documentation is [here>>>](https://helm.sh/docs/chart_template_guide/named_templates/).

The very first block in our template which we’d like to move into the `_helpers.yaml` is `labels`.

The general idea behind the Named Templates in Helm is to write some code only once and then include it in every placed where we need for this code. Also, this helps to reduce a template’s content and make it more readable.

The named templates file name is stared with the underline with the `tpl` extension.

The most known file is the `_helpers.tpl` which we will use in this case.

Each template definition in this file is started with the `define` keyword and ended with the `end`.

A name for a named template usually includes a chart name and a block which is described the template, but you can use any other. For example, in our file, we will use the _helpers.<BLOCK_NAME>_ name.

The one inconvenience here is the fact that you can’t use values like `.Chart.Name`, but you can create a Helm's variable inside. Not sure about this, will see later, for now, will just hardcode the _helpers_.

#### Common labels

Well, which labels will be good to have at all?

- _environment_ — _Dev_, _Stage_, _Prod_ — will be set from the values.yaml
- _appversion_ — will be set during a build in Jenkins via the `--set` option

For more good ideas we can ask Google — “_kubernetes recommended labels_”, and the very first link is the [Recommended Labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/).

Also, check the [Labels and Annotations](https://helm.sh/docs/chart_best_practices/labels/) in the Helm’s documentation

Plus, it may be a good idea to take a look at some existing charts, for example, the [sonarqube/templates/deployment.yaml](https://github.com/helm/charts/blob/0445fad8d8308f089f5fb6756a5570bc6d6f0bf5/stable/sonarqube/templates/deployment.yaml#L5).

So, for now, we can add four custom labels:

- _application_: `{{ .Values.appConfig.appName }}`, will be set in the values.yaml for each application
- _version_: `{{ .Chart.Version }}`-`{{ .Chart.AppVersion }}`, will be set from Jenkins
- _environment_: `{{ .Values.appConfig.appEnv }}`, will be set in the `values.yaml` for each application
- _managed-by_: `{{ .Release.Service }}` by Helm

And if we will want to add another one — we can do it in one place instead of updating all the places where labels are used.

#### `define`

Go back to our `_helpers.tpl` and describe our labels:

```
{{- define "helpers.labels" -}}
application: {{ .Values.appConfig.appName }}
version: {{ .Chart.Version }}-{{ .Chart.AppVersion }}
environment: {{ .Values.appConfig.appEnv }}
managed-by: {{ .Release.Service }}
{{- end }}
```

#### `include`

Then, using the include, specify where it must be included in the general deployment's template:

```
apiVersion: apps/v1
kind: Deployment
metadata:
 name: {{ .Values.appConfig.appName }}-deployment
 labels: {{- include "helpers.labels" . | nindent 4 }}
...
```

#### `indent` vs `nindent`

- `indent` - just will add spaces
- `nindent` - will add spaces plus a new line symbol

I.e. instead of writing something like this:

```
...
 labels: 
 {{- include "helpers.labels" . | indent 4 }}
...
```

We can set it in one line and avoid typing whitespaces in the template:

```
...
labels: {{- include "helpers.labels" . | nindent 4 }}
...
```

Also, this plays a role in adding a YAML block into the template, will see it in action below.

Going back to the named templates — let’s do the same for the `spec.selector.matchLabels` - move it to a dedicated template so we can re-use it later, for example in Services.

In the `_helpers.tpl` add a new block:

```
...
{{- define "helpers.selectorLabels" -}}
application: {{ .Values.appConfig.appName }}
{{- end }}
```

For now, we do a selection by an application name only, and later we can update it and include any other selectors.

Add it to the deployment:

```
...
 selector:
 matchLabels:
 {{- include "helpers.selectorLabels" . | nindent 6 }}
 template:
...
```

### Templates inside of a template

And we came to the most interesting part of the task — adding whole blocks into the template’s file.

So, in the currently used template, we have the `env` block:

```
...
 env:
 - name: APP_SECRET
 valueFrom:
 secretKeyRef:
 name: app-backend-secrets
 key: backend-app-secret
 - name: AUTH_SECRET
 valueFrom:
 secretKeyRef:
 name: app-backend-secrets
 key: backend-auth-secret
 - name: CLIENT_HOST
 value: {{ .Values.appConfig.clientHost }}
 - name: DATABASE_HOST
 value: {{ .Values.appConfig.db.host }}
 - name: DATABASE_SLAVE_HOST
 value: {{ .Values.appConfig.db.slave }}
 - name: DB_USERNAME
 value: {{ .Values.appConfig.db.user }}
 - name: INSTANA_AGENT_HOST
 valueFrom:
 fieldRef:
 fieldPath: status.hostIP
...
```

Which we’d like to move to a block in another file and include it in the new template which we are writing now.

#### The `env` block from a project's `values.yaml` file

Let’s see how we can implement this.

At first, need to add environment variables to the `values.yaml`.

Create the directories tree:

```
$ mkdir -p projects/newapp/env/dev
```

And files inside — `values.yaml` and `secrets.yaml`:

```
$ touch projects/newapp/env/dev/{values.yaml,secrets.yaml}
```

Then, in the `projects/newapp/env/dev/values.yaml` create a list called _environments_ with the only one variable for now:

```
environments: 
 - name: 'DATABASE_HOST'
 value: 'dev.aurora.web.example.com'
```

To be able to test the template with the `helm --debug --dry-run` - add all other data like `{{ .Values.deployment.replicaCount }}`.

#### The `range` loop

Documentation — [Flow Control](https://helm.sh/docs/chart_template_guide/control_structures/).

Good examples — [Helm Template range](https://kb.novaordis.com/index.php/Helm_Template_range).

So, we have an environment variable `DATABASE_HOST` with the _dev.aurora.web.example.com_ value which is described in the `values.yaml` which is set as an element of the list with two _key:value_ pairs:

```
...
 <LIST_NAME>:
 - <VAR_NAME>: <VAR_VALUE>
   <VAR_NAME>: <VAR_VALUE>
...
```

Which we can then include in the general template with the following code:

```
...
 spec:
 containers:
 - name: {{ .Values.appConfig.appName }}-pod
 image: {{ .Values.deployment.image.repository }}/{{ .Values.deployment.image.name }}:{{ .Values.deployment.image.tag }}
 env:
 {{- range .Values.deployment.environments }}
 - name: {{ .name }} 
 value: {{ .value }}
 {{- end }}
...
```

Here:

- iterate the `environments` list with the key:value lines
- iterate each of the element of the list looking for the VAR_NAME `.name` - this will be set in the name: `{{ .name }}`
- iterate each of the element of the list looking for the VAR_NAME `.value` - this will be set in the value: `{{ .value }}`

Check it:

```
helm upgrade — install eks-dev-1-newapp-backend — namespace eks-dev-1-newapp-backend-ns — create-namespace -f projects/newapp/env/dev/values.yaml — debug — dry-run .
…
spec:
replicas: 2
strategy:
type:
selector:
matchLabels:
application: newapp
template:
metadata:
labels:
application: newapp
version: 0.1.0–1.16.0
environment: dev
managed-by: Helm
spec:
containers:
- name: newapp-pod
image: projectname/projectname:latest
env:
- name: DATABASE_HOST
value: dev.aurora.web.example.com
ports:
- containerPort: 3001
…
```

By the way — we are seeing our labels here.

And here is our environment variables:

```
…
env:
- name: DATABASE_HOST
value: dev.aurora.web.example.com
…
```

#### The `range` loop with variables

Another solution is to use the same loop but now using `$key` and `$value` variables - in this case, we are not tied to names of the variables in the `values.yaml` file, instead just iterate over each of them:

```
...
 {{- range $key, $value := .Values.deployment.environments }}
 env:
 - name: {{ $key }}
 value: {{ $value }}
 {{- end }}
...
```

Update the `values.yaml` - now our environments are set as a dictionary containing our variables just as a _key:value_ pairs, here is added another one to make the example more illustrative, the `DB_USERNAME` one:

```
...
 environments: 
 DATABASE_HOST: 'dev.aurora.web.example.com'
 DB_USERNAME: 'dbuser'
...
```

Check it:

```
$ helm upgrade — install eks-dev-1-newapp-backend — namespace eks-dev-1-newapp-backend-ns — create-namespace -f projects/newapp/env/dev/values.yaml — debug — dry-run .
…
spec:
containers:
- name: newapp-pod
image: projectname/projectname:latest
env:
- name: DATABASE_HOST
value: dev.aurora.web.example.com
- name: DB_USERNAME
value: dbuser
ports:
- containerPort: 3001
…
```

#### `toYaml`

But let’s recall our initial template:

```
...
 - name: DATABASE_HOST
 value: {{ .Values.backendConfig.db.host }}
 - name: DB_USERNAME
 value: {{ .Values.backendConfig.db.user }}
 - name: DB_PASSWORD
 valueFrom:
 secretKeyRef:
 name: projectname-backend-secrets
 key: backend-db-password
...
```

We can’t use a simple loop here, so the third solution will be to describe the whole env block in the `values.yaml`, and then include it in the general template by using the `toYaml`.

Update the `values.yaml` and set the block here:

```
environments:
 - name: DATABASE_HOST
 value: 'dev.aurora.web.example.com'
 - name: DB_USERNAME
 value: 'dbuser'
 - name: DB_PASSWORD
 valueFrom:
 secretKeyRef:
 name: projectname-backend-secrets
 key: backend-db-password
```

And then include it to the general template to the `env` block:

```
...
 containers:
 - name: {{ .Values.appConfig.appName }}-pod
 image: {{ .Values.deployment.image.repository }}/{{ .Values.deployment.image.name }}:{{ .Values.deployment.image.tag }}
 env: 
 {{- toYaml .Values.deployment.environments | nindent 8 }}
...
```

#### Whitespaces

Pay your attention that before the toYaml and after the `{{` we've set the dash symbol "`-`":

```
{{- toYaml .Values.deployment.environments | nindent 8 }}
```

It is used to remove whitespaces before the block to be included, also keep in mind that new lines also are threaded as whitespaces.

If remove the “`-`" from here we will get an extra newline in the resulted manifest:

```
…
env:

- name: DATABASE_HOST
value: dev.aurora.web.example.com
- name: DB_USERNAME
…
```

See the _Controlling Whitespace_ on the [Flow Control](https://helm.sh/docs/chart_template_guide/control_structures/) and more example on the [Directives and Whitespace Handling](https://kb.novaordis.com/index.php/Helm_Templates#Directives_and_Whitespace_Handling) pages.

And here we are again using the `nindent` to add newlines after each line from the `.Values.deployment.environments`.

#### Helm `tpl`

And one more solution — use the `tpl` function.

In contrast to the previous one — this time we can use directives like`{{ .Release.Name }}` in our `values.yaml` as the code from it will be treated as a part of the template itself.

Update the `values.yaml`:

```
environments: |-
 - name: RELEASE_NAME
 value: {{ .Release.Name }}
 - name: DATABASE_HOST
 value: 'dev.aurora.web.example.com'
 - name: DB_USERNAME
 value: 'dbuser'
 - name: DB_PASSWORD
 valueFrom:
 secretKeyRef:
 name: projectname-backend-secrets
 key: backend-db-password
```

Pay attention to the “`|-`" - we are describing the block with variables as a simple string and removing newlines.

Add it to the deployment’s template:

```
...
 containers:
 - name: {{ .Values.appConfig.appName }}-pod
 image: {{ .Values.deployment.image.repository }}/{{ .Values.deployment.image.name }}:{{ .Values.deployment.image.tag }}
 env: 
 {{- tpl .Values.deployment.environments . | nindent 8 }}
...
```

Here we are passing content of the `.Values.deployment.environments` to the `tml` function to include it to the template.

Check it:

```
$ helm upgrade — install eks-dev-1-newapp-backend — namespace eks-dev-1-newapp-backend-ns — create-namespace -f projects/newapp/env/dev/values.yaml — debug — dry-run .
…
spec:
containers:
- name: newapp-pod
image: projectname/projectname:latest
env:
- name: RELEASE_NAME
value: eks-dev-1-newapp-backend
- name: DATABASE_HOST
value: ‘dev.aurora.web.example.com’
- name: DB_USERNAME
value: ‘dbuser’
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: projectname-backend-secrets
key: backend-db-password
ports:
- containerPort: 3001
…
```

#### The env block from the `_helpers.tpl`

We already used the file, but this time let’s add a condition to check if we have some parameter set in the `values.yaml`.

#### `if/else `— flow control

In the `_helpers.yaml` define the `env` block's template:

```
{{- define "helpers.environments" -}}
- name: RELEASE_NAME
 value: {{ .Release.Name }}
{{- if .Values.appConfig.db.host }}
- name: DATABASE_HOST
 value: 'dev.aurora.web.example.com'
{{- end }}
{{- if .Values.appConfig.db.user }}
- name: DB_USERNAME
 value: 'dbuser'
{{- end }}
{{- if .Values.appConfig.db.password }}
- name: DB_PASSWORD
 valueFrom: 
 secretKeyRef:
 name: projectname-backend-secrets
 key: backend-db-password
{{- end }}
{{- end }}
```

Here we are using the `if/else` condition for each environment variable to check if we have a value specified for this variable and it will be found - then the variable will be added to the generic template.

Using this we will able to expand the environment variables list in the `helpers.environments` without worrying that some projet's `values.yaml` will have no value for it and this will break its deployment.

Now, include it to the template:

```
...
 containers:
 - name: {{ .Values.appConfig.appName }}-pod
 image: {{ .Values.deployment.image.repository }}/{{ .Values.deployment.image.name }}:{{ .Values.deployment.image.tag }}
 env: {{- include "helpers.environments" . | nindent 8 }}
...
```

Check it:

```
$ helm upgrade — install eks-dev-1-newapp-backend — namespace eks-dev-1-newapp-backend-ns — create-namespace -f projects/newapp/env/dev/values.yaml — debug — dry-run .
…
spec:
containers:
- name: newapp-pod
image: projectname/projectname:latest
env:
- name: RELEASE_NAME
value: eks-dev-1-newapp-backend
- name: DATABASE_HOST
value: ‘dev.aurora.web.example.com’
- name: DB_USERNAME
value: ‘dbuser’
ports:
- containerPort: 3001
…
```

And pay attention here — the `DB_PASSWORD` was not set, because we have the `{{ .Release.Name }}` by default, we have the `.Values.appConfig.db.host` and `.Values.appConfig.db.user` in the `values.yaml`, but the `.Values.appConfig.db.password` is set with the [Helm Secrets](https://rtfm.co.ua/en/helm-helm-secrets-sensitive-data-encryption-with-aws-kms-and-use-it-from-jenkins/) and is stored in the `secrets.yaml` file which we are not using now at all.

So, the `if/else` condition playing here, and Helm didn't create the `DB_PASSWORD` variable.

#### The `env` block from an external file

And the latest solution which I was able to find is to include the env from a file in a project's directory.

Let’s add a new option to the `values.yaml` to disable include from the `_helpers.tpl`, call it `customEnvs`, and add another one to specify a path to a file with environment variables - `customEnvsFile`:

```
...
deployment:

 customEnvs: true
 customEnvsFile: 'projects/newapp/templates/environments.yaml'
....
```

Now, into the deployment template add a condition to check the `customEnvs` is will be set to _false_ - then via `{{ else }}` we will include our previous template from the `_helpers.yaml`:

```
...
 env:
 {{- if .Values.deployment.customEnvs }}
 {{- .Files.Get .Values.deployment.customEnvsFile | nindent 8 }}
 {{- else }}
 {{- include "helpers.environments" . | nindent 8 }}
 {{ end -}}
 ports:
...
```

And this will work but only because I’ve removed the `.Release.Name` from the `projects/newapp/templates/environments.yaml`:

```
- name: DATABASE_HOST
 value: 'dev.aurora.web.example.com'
- name: DB_USERNAME
 value: 'dbuser'
- name: DB_PASSWORD
 valueFrom:
 secretKeyRef:
 name: projectname-backend-secrets
 key: backend-db-password
```

This can be solved with already familiar `tpl` function.

Go back to the `projects/newapp/templates/environments.yaml` - set the `.Release.Name` back:

```
- name: RELEASE_NAME
 value: {{ .Release.Name }}
- name: DATABASE_HOST
 value: 'dev.aurora.web.example.com'
...
```

In the deployment template, add the `tpl`, and `.Files.Get` and its argument wrap into parentheses:

```
...
 {{- if .Values.deployment.customEnvs }}
 {{- tpl ( .Files.Get .Values.deployment.customEnvsFile ) . | nindent 8 }}
 {{- else }} 
 {{- include "helpers.environments" . | nindent 8 }}
 {{ end -}}
...
```

Check it:

```
$ helm upgrade — install eks-dev-1-newapp-backend — namespace eks-dev-1-newapp-backend-ns — create-namespace -f projects/newapp/env/dev/values.yaml — debug — dry-run .
…
spec:
containers:
- name: newapp-pod
image: projectname/projectname:latest
env:
- name: RELEASE_NAME
value: eks-dev-1-newapp-backend
- name: DATABASE_HOST
value: ‘dev.aurora.web.example.com’
- name: DB_USERNAME
value: ‘dbuser’
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: projectname-backend-secrets
key: backend-db-password
ports:
- containerPort: 3001
…
```

With `volumes`, `volumesMounts` we can use the same approach.

In general, the solution with the `_helpers.yaml` looks usable - will test it on the new project and will see how it's going.

The whole Deployment template now:

```
apiVersion: apps/v1
kind: Deployment
metadata:
 name: {{ .Values.appConfig.appName }}-deployment
 labels: {{- include "helpers.labels" . | nindent 4 }}
 annotations:
 {{- if .Values.deployment.deploymentAnnotations }}
 {{- toYaml .Values.deployment.deploymentAnnotations | nindent 6 }}
 {{- end }}
 reloader.stakater.com/auto: "true"
spec:
 replicas: {{ .Values.deployment.replicaCount }}
 strategy:
 type: {{ .Values.deployment.delpoyStrategy }}
 selector:
 matchLabels:
 {{- include "helpers.selectorLabels" . | nindent 6 }}
 template:
 metadata:
 labels: {{- include "helpers.labels" . | nindent 8 }}
 spec:
 containers:
 - name: {{ .Values.appConfig.appName }}-pod
 image: {{ .Values.deployment.image.repository }}/{{ .Values.deployment.image.name }}:{{ .Values.deployment.image.tag }}
 env: 
 {{- if .Values.deployment.customEnvs }}
 {{- tpl ( .Files.Get .Values.deployment.customEnvsFile ) . | nindent 8 }}
 {{- else }}
 {{- include "helpers.environments" . | nindent 8 }}
 {{ end -}}
 ports:
 - containerPort: {{ .Values.appConfig.port }}
 {{- with .Values.deployment.livenessProbe }}
 livenessProbe:
 httpGet:
 path: {{ .path }}
 port: {{ .port }}
 initialDelaySeconds: {{ .initDelay }}
 {{- end }}
 {{- with .Values.deployment.readinessProbe }}
 readinessProbe:
 httpGet:
 path: {{ .path }}
 port: {{ .port }}
 initialDelaySeconds: {{ .initDelay }}
 {{- end }}
 resources:
 requests:
 cpu: {{ .Values.deployment.resources.requests.cpu | quote }}
 imagePullSecrets:
 - name: bttrm-docker-secret
```

### Excluding a template from the chart

And the final thought — how can we exclude a template file from the chart at all.

For example, let’s check the `cronjobs.yaml` file:

```
apiVersion: batch/v1beta1
kind: CronJob
metadata:
 name: {{ .Values.appConfig.appName }}-cron
 labels: {{- include "helpers.labels" . | nindent 4 }}
spec: 
 schedule: {{ .Values.cronjobs.schedule | quote }}
 startingDeadlineSeconds: {{ .Values.cronjobs.startingDeadline }}
 concurrencyPolicy: {{ .Values.cronjobs.concurrencyPolicy }}
 jobTemplate:
 spec:
 template:
 metadata:
 labels: {{- include "helpers.labels" . | nindent 12 }}
 spec:
 containers:
 - name: {{ .Values.appConfig.appName }}-cron
 image: {{ .Values.deployment.image.repository }}/{{ .Values.deployment.image.name }}:{{ .Values.deployment.image.tag }}
 env:
 {{- if .Values.deployment.customEnvs }}
 {{- tpl ( .Files.Get .Values.deployment.customEnvsFile ) . | nindent 14 }}
 {{- else }}
 {{- include "helpers.environments" . | nindent 14 }}
 {{ end -}}
 command: ["npm"]
 args: ["run", "cron:app"]
 restartPolicy: Never
 imagePullSecrets:
 - name: bttrm-docker-secret
```

By the way — here are our labels and the `env` block used from the `_helpers.yaml`.

But not every project have cronjobs.

So, we can add a new parameter to the `values.yaml` - `cronjobs.enabled`:

```
...
################
### Cronjobs ###
################ 

cronjobs: 

 enabled: false
 schedule: '*/15 * * * *'
 startingDeadline: 10
 concurrencyPolicy: 'Forbid'
...
```

And then wrap the whole template’s content in one `if/else` condition check:

```
{{- if .Values.cronjobs.enabled }}
apiVersion: batch/v1beta1
kind: CronJob
metadata:
 name: {{ .Values.appConfig.appName }}-cron
 labels: {{- include "helpers.labels" . | nindent 4 }}
...
 imagePullSecrets:
 - name: bttrm-docker-secret
{{- end }}
```

Check it:

```
$ helm upgrade — install eks-dev-1-newapp-backend — namespace eks-dev-1-newapp-backend-ns — create-namespace -f projects/newapp/env/dev/values.yaml — debug — dry-run .
…
 — -
Source: project-backend/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: newapp-deployment
…
imagePullSecrets:
- name: bttrm-docker-secret
```

No cronjobs were added.

Add `--set cronjobs.enabled=true` - and they will be added to the release:

```
$ helm upgrade — install eks-dev-1-newapp-backend — namespace eks-dev-1-newapp-backend-ns — create-namespace -f projects/newapp/env/dev/values.yaml — debug — dry-run . — set cronjobs.enabled=true
…
imagePullSecrets:
- name: bttrm-docker-secret
 — -
Source: project-backend/templates/cronjobs.yaml
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: newapp-cron
…
imagePullSecrets:
```

- name: bttrm-docker-secret

_“That’s all folks!_” ©

### Useful links

- [The Art of the Helm Chart: Patterns from the Official Kubernetes Charts](https://hackernoon.com/the-art-of-the-helm-chart-patterns-from-the-official-kubernetes-charts-8a7cafa86d12)
- [Writing Reusable Helm Charts](https://www.arthurkoziel.com/writing-reusable-helm-charts/)
- [How To Reduce Helm Chart Boilerplate With Named Templates](https://austindewey.com/2020/08/09/how-to-reduce-helm-chart-boilerplate-with-named-templates/)
- [Helm from basics to advanced](https://banzaicloud.com/blog/creating-helm-charts/)
- [Создание пакетов для Kubernetes с Helm: структура чарта и шаблонизация](https://habr.com/ru/company/flant/blog/423239/)
- [Helm Templates Cheat Sheet](https://lzone.de/cheat-sheet/Helm+Templates)
- [Introduction to Helm — Templates](https://cloudacademy.com/course/introduction-to-helm-1034/helm-templates/)
- [Helm Templates](https://kb.novaordis.com/index.php/Helm_Templates#Directives_and_Whitespace_Handling)
- [Helm Tricks: Input Validation With ‘Required’ And ‘Fail’](https://austindewey.com/2018/12/28/helm-tricks-input-validation-with-required-and-fail/)
- [Helm Chart and Template Basics — Part 1](https://www.alibabacloud.com/blog/helm-chart-and-template-basics---part-1_595489?spm=a2c65.11461447.0.0.51291b6aFbpqk1)

_Originally published at_ [_RTFM: Linux, DevOps и системное администрирование_](https://rtfm.co.ua/en/helm-reusable-chart-named-templates-and-a-generic-chart-for-multiple-applications/)_._

* * *