How To Create Your First Helm Chart

For a typical cloud-native application with a 3-tier architecture, the diagram below illustrates how it might be described in terms of Kubernetes objects. In this example, each tier consists of a Deployment and Service object, and may additionally define ConfigMap or Secret objects. Each of these objects are typically defined in separate YAML files, and are fed into the kubectl command line tool.

A Helm chart encapsulates each of these YAML definitions, provides a mechanism for configuration at deploy-time and allows you to define metadata and documentation that might be useful when sharing the package. Helm can be useful in different scenarios:

  1. Find and use popular software packaged as Kubernetes charts
  2. Share your own applications as Kubernetes charts
  3. Create reproducible builds of your Kubernetes applications
  4. Intelligently manage your Kubernetes object definitions
  5. Manage releases of Helm packages

Let’s explore the second and third scenarios by creating our first chart.

Generate Your First Chart

The best way to get started with a new chart is to use the helm create command to scaffold out an example we can build on. Use this command to create a new chart named admatic-chart in a new directory:

helm create admatic-chart
Creating admatic-chart

Helm will create a new directory in your project called admatic-chart with the structure shown below.

sudo apt update && sudo apt install tree -y

tree admatic-chart/
admatic-chart/
├── charts
├── Chart.yaml
├── templates
│   ├── deployment.yaml
│   ├── _helpers.tpl
│   ├── ingress.yaml
│   ├── NOTES.txt
│   ├── service.yaml
│   └── tests
│       └── test-connection.yaml
└── values.yaml

3 directories, 8 files

Templates

The most important piece of the puzzle is the templates/ directory. This is where Helm finds the YAML definitions for your Services, Deployments and other Kubernetes objects. If you already have definitions for your application, all you need to do is replace the generated YAML files for your own. What you end up with is a working chart that can be deployed using the helm install command.

It’s worth noting however, that the directory is named templates, and Helm runs each file in this directory through a Go template rendering engine. Helm extends the template language, adding a number of utility functions for writing charts. Open the service.yaml file to see what this looks like:

cat admatic-chart/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: { { include "admatic-chart.fullname" . } }
  labels:
    app.kubernetes.io/name: { { include "admatic-chart.name" . } }
    helm.sh/chart: { { include "admatic-chart.chart" . } }
    app.kubernetes.io/instance: { { .Release.Name } }
    app.kubernetes.io/managed-by: { { .Release.Service } }
spec:
  type: { { .Values.service.type } }
  ports:
    - port: { { .Values.service.port } }
      targetPort: http
      protocol: TCP
      name: http
  selector:
    app.kubernetes.io/name: { { include "admatic-chart.name" . } }
    app.kubernetes.io/instance: { { .Release.Name } }

This is a basic Service definition using templating. When deploying the chart, Helm will generate a definition that will look a lot more like a valid Service. We can do a dry-run of a helm install and enable debug to inspect the generated definitions:

helm install --dry-run --debug ./admatic-chart
[debug] Created tunnel using local port: '41373'

[debug] SERVER: "127.0.0.1:41373"

[debug] Original chart version: ""
[debug] CHART PATH: /home/adithya321/Documents/we-can-do-now/k8s/admatic-chart

NAME:   fuzzy-mite
REVISION: 1
RELEASED: Thu Jan  3 13:04:55 2019
CHART: admatic-chart-0.1.0
USER-SUPPLIED VALUES:
{}

COMPUTED VALUES:
affinity: {}
fullnameOverride: ""
image:
  pullPolicy: IfNotPresent
  repository: nginx
  tag: stable
ingress:
  annotations: {}
  enabled: false
  hosts:
  - chart-example.local
  paths: []
  tls: []
nameOverride: ""
nodeSelector: {}
replicaCount: 1
resources: {}
service:
  port: 80
  type: ClusterIP
tolerations: []


HOOKS:
---
# fuzzy-mite-admatic-chart-test-connection
apiVersion: v1
kind: Pod
metadata:
  name: "fuzzy-mite-admatic-chart-test-connection"
  labels:
    app.kubernetes.io/name: admatic-chart
    helm.sh/chart: admatic-chart-0.1.0
    app.kubernetes.io/instance: fuzzy-mite
    app.kubernetes.io/managed-by: Tiller
  annotations:
    "helm.sh/hook": test-success
spec:
  containers:
    - name: wget
      image: busybox
      command: ['wget']
      args:  ['fuzzy-mite-admatic-chart:80']
  restartPolicy: Never
MANIFEST:

---
# Source: admatic-chart/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: fuzzy-mite-admatic-chart
  labels:
    app.kubernetes.io/name: admatic-chart
    helm.sh/chart: admatic-chart-0.1.0
    app.kubernetes.io/instance: fuzzy-mite
    app.kubernetes.io/managed-by: Tiller
spec:
  type: ClusterIP
  ports:
    - port: 80
      targetPort: http
      protocol: TCP
      name: http
  selector:
    app.kubernetes.io/name: admatic-chart
    app.kubernetes.io/instance: fuzzy-mite
---
# Source: admatic-chart/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: fuzzy-mite-admatic-chart
  labels:
    app.kubernetes.io/name: admatic-chart
    helm.sh/chart: admatic-chart-0.1.0
    app.kubernetes.io/instance: fuzzy-mite
    app.kubernetes.io/managed-by: Tiller
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: admatic-chart
      app.kubernetes.io/instance: fuzzy-mite
  template:
    metadata:
      labels:
        app.kubernetes.io/name: admatic-chart
        app.kubernetes.io/instance: fuzzy-mite
    spec:
      containers:
        - name: admatic-chart
          image: "nginx:stable"
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 80
              protocol: TCP
          livenessProbe:
            httpGet:
              path: /
              port: http
          readinessProbe:
            httpGet:
              path: /
              port: http
          resources:
            {}

Values

The template in service.yaml makes use of the Helm-specific objects .Chart and .Values. The former provides metadata about the chart to your definitions such as the name, or version. The latter .Values object is a key element of Helm charts, used to expose configuration that can be set at the time of deployment. The defaults for this object are defined in the values.yaml file. Try changing the default value for service.internalPort and execute another dry-run, you should find that the targetPort in the Service and the containerPort in the Deployment changes. The service.internalPort value is used here to ensure that the Service and Deployment objects work together correctly. The use of templating can greatly reduce boilerplate and simplify your definitions.

If a user of your chart wanted to change the default configuration, they could provide overrides directly on the command-line:

helm install --dry-run --debug ./admatic-chart --set service.internalPort=8080
[debug] Created tunnel using local port: '38017'

[debug] SERVER: "127.0.0.1:38017"

[debug] Original chart version: ""
[debug] CHART PATH: /home/adithya321/Documents/we-can-do-now/k8s/admatic-chart

NAME:   lumpy-skunk
REVISION: 1
RELEASED: Thu Jan  3 13:08:02 2019
CHART: admatic-chart-0.1.0
USER-SUPPLIED VALUES:
service:
  internalPort: 8080

COMPUTED VALUES:
affinity: {}
fullnameOverride: ""
image:
  pullPolicy: IfNotPresent
  repository: nginx
  tag: stable
ingress:
  annotations: {}
  enabled: false
  hosts:
  - chart-example.local
  paths: []
  tls: []
nameOverride: ""
nodeSelector: {}
replicaCount: 1
resources: {}
service:
  internalPort: 8080
  port: 80
  type: ClusterIP
tolerations: []
...

For more advanced configuration, a user can specify a YAML file containing overrides with the --values option.

Helpers and other functions

The service.yaml template also makes use of partials defined in _helpers.tpl, as well as functions like replace.

Documentation

Another useful file in the templates/ directory is the NOTES.txt file. This is a templated, plaintext file that gets printed out after the chart is successfully deployed. Since NOTES.txt is run through the template engine, you can use templating to print out working commands for obtaining an IP address, or getting a password from a Secret object.

Metadata

As mentioned earlier, a Helm chart consists of metadata that is used to help describe what the application is, define constraints on the minimum required Kubernetes and/or Helm version and manage the version of your chart. All of this metadata lives in the Chart.yaml file. The Helm documentation describes the different fields for this file.

Deploying Your First Chart

The chart you generated in the previous step is setup to run an NGINX server exposed via a Kubernetes Service. By default, the chart will create a ClusterIP type Service, so NGINX will only be exposed internally in the cluster. To access it externally, we’ll use the NodePort type instead. We can also set the name of the Helm release so we can easily refer back to it. Let’s go ahead and deploy our NGINX chart using the helm install command:

helm install --name example ./admatic-chart --set service.type=NodePort
NAME:   example
LAST DEPLOYED: Thu Jan  3 13:13:28 2019
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1/Service
NAME                   TYPE      CLUSTER-IP    EXTERNAL-IP  PORT(S)       AGE
example-admatic-chart  NodePort  10.7.254.174  <none>       80:30764/TCP  1s

==> v1/Deployment
NAME                   DESIRED  CURRENT  UP-TO-DATE  AVAILABLE  AGE
example-admatic-chart  1        1        1           0          1s

==> v1/Pod(related)
NAME                                    READY  STATUS             RESTARTS  AGE
example-admatic-chart-56f4fc9bb4-jqzzd  0/1    ContainerCreating  0         1s


NOTES:
1. Get the application URL by running these commands:
  export NODE_PORT=$(kubectl get --namespace default -o jsonpath="{.spec.ports[0].nodePort}" services example-admatic-chart)
  export NODE_IP=$(kubectl get nodes --namespace default -o jsonpath="{.items[0].status.addresses[0].address}")
  echo http://$NODE_IP:$NODE_PORT

The output of helm install displays a handy summary of the state of the release, what objects were created, and the rendered NOTES.txt file to explain what to do next. Run the commands in the output to get a URL to access the NGINX service and pull it up in your browser.

export NODE_PORT=$(kubectl get --namespace default -o jsonpath="{.spec.ports[0].nodePort}" services example-admatic-chart)
echo $NODE_PORT
30764

Get external IP addresses of nodes

kubectl get nodes -o jsonpath='{ $.items[*].status.addresses[?(@.type=="ExternalIP")].address }'
35.232.43.157 35.226.116.253 104.154.32.120
curl 35.232.43.157:$NODE_PORT
curl 35.226.116.253:$NODE_PORT
curl 104.154.32.120:$NODE_PORT
<!DOCTYPE html>
<html>
  <head>
    <title>Welcome to nginx!</title>
    <style>
      body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
      }
    </style>
  </head>
  <body>
    <h1>Welcome to nginx!</h1>
    <p>
      If you see this page, the nginx web server is successfully installed and
      working. Further configuration is required.
    </p>

    <p>
      For online documentation and support please refer to
      <a href="http://nginx.org/">nginx.org</a>.<br />
      Commercial support is available at
      <a href="http://nginx.com/">nginx.com</a>.
    </p>

    <p><em>Thank you for using nginx.</em></p>
  </body>
</html>

Modify Chart To Deploy A Custom Service

The generated chart creates a Deployment object designed to run an image provided by the default values. This means all we need to do to run a different service is to change the referenced image in values.yaml.

We are going to update the chart to run a todo list application available on Docker Hub. In values.yaml, update the image keys to reference the todo list image:

vim admatic-chart/values.yaml
image:
  repository: prydonius/todo
  tag: 1.0.0
  pullPolicy: IfNotPresent

As you develop your chart, it’s a good idea to run it through the linter to ensure you’re following best practices and that your templates are well-formed. Run the helm lint command to see the linter in action:

helm lint ./admatic-chart
==> Linting ./admatic-chart
[INFO] Chart.yaml: icon is recommended

1 chart(s) linted, no failures

The linter didn’t complain about any major issues with the chart, so we’re good to go. However, as an example, here is what the linter might output if you managed to get something wrong:

echo "malformed" >> admatic-chart/values.yaml
helm lint ./admatic-chart
==> Linting ./admatic-chart
[INFO] Chart.yaml: icon is recommended
[ERROR] values.yaml: unable to parse YAML
        error converting YAML to JSON: yaml: line 49: could not find expected ':'

Error: 1 chart(s) linted, 1 chart(s) failed

This time, the linter tells us that it was unable to parse my values.yaml file correctly. With the line number hint, we can easily find the fix to the bug we introduced.

Delete line number 49

sed -i '49d' admatic-chart/values.yaml
helm lint ./admatic-chart
==> Linting ./admatic-chart
[INFO] Chart.yaml: icon is recommended

1 chart(s) linted, no failures

Now that the chart is once again valid, run helm install again to deploy the todo list application:

helm install --name todo ./admatic-chart --set service.type=NodePort
NAME:   todo
LAST DEPLOYED: Thu Jan  3 13:31:49 2019
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1/Deployment
NAME                DESIRED  CURRENT  UP-TO-DATE  AVAILABLE  AGE
todo-admatic-chart  1        1        1           0          1s

==> v1/Pod(related)
NAME                                READY  STATUS             RESTARTS  AGE
todo-admatic-chart-7f47b69b8-tlcqq  0/1    ContainerCreating  0         1s

==> v1/Service
NAME                TYPE      CLUSTER-IP   EXTERNAL-IP  PORT(S)       AGE
todo-admatic-chart  NodePort  10.7.253.76  <none>       80:32167/TCP  1s


NOTES:
1. Get the application URL by running these commands:
  export NODE_PORT=$(kubectl get --namespace default -o jsonpath="{.spec.ports[0].nodePort}" services todo-admatic-chart)
  export NODE_IP=$(kubectl get nodes --namespace default -o jsonpath="{.items[0].status.addresses[0].address}")
  echo http://$NODE_IP:$NODE_PORT

Once again, we can run the commands in the NOTES to get a URL to access our application.

export NODE_PORT=$(kubectl get --namespace default -o jsonpath="{.spec.ports[0].nodePort}" services todo-admatic-chart)
echo $NODE_PORT
30334

Get external IP addresses of nodes

kubectl get nodes -o jsonpath='{ $.items[*].status.addresses[?(@.type=="ExternalIP")].address }'
35.232.43.157 35.226.116.253 104.154.32.120
curl 35.232.43.157:$NODE_PORT
<!doctype html>
<html lang="en" data-framework="react">
        <head>
                <meta charset="utf-8">
                <title>React • TodoMVC</title>
                <link rel="stylesheet" href="node_modules/todomvc-common/base.css">
                <link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
        </head>
        <body>
                <section class="todoapp"></section>
                <footer class="info">
                        <p>Double-click to edit a todo</p>
                        <p>Created by <a href="http://github.com/petehunt/">petehunt</a></p>
                        <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
                </footer>

                <script src="node_modules/todomvc-common/base.js"></script>
                <script src="node_modules/react/dist/react-with-addons.js"></script>
                <script src="node_modules/classnames/index.js"></script>
                <script src="node_modules/react/dist/JSXTransformer.js"></script>
                <script src="node_modules/director/build/director.js"></script>

                <script src="js/utils.js"></script>
                <script src="js/todoModel.js"></script>
                <!-- jsx is an optional syntactic sugar that transforms methods in React's
                `render` into an HTML-looking format. Since the two models above are
                unrelated to React, we didn't need those transforms. -->
                <script type="text/jsx" src="js/todoItem.jsx"></script>
                <script type="text/jsx" src="js/footer.jsx"></script>
                <script type="text/jsx" src="js/app.jsx"></script>
        </body>
</html>

Packaging It All Up To Share

If you are looking to share your charts with your team or the community, your consumers will typically install the charts from a tar package. We can use helm package to create the tar package:

helm package ./admatic-chart

Helm will create a admatic-chart-0.1.0.tgz package in our working directory, using the name and version from the metadata defined in the Chart.yaml file.

Successfully packaged chart and saved it to: /home/adithya321/Documents/we-can-do-now/k8s/admatic-chart-0.1.0.tgz

A user can install from this package instead of a local directory by passing the package as the parameter to helm install.

helm install --name todo-package admatic-chart-0.1.0.tgz --set service.type=NodePort
NAME:   todo-package
LAST DEPLOYED: Thu Jan  3 13:44:54 2019
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1/Pod(related)
NAME                                        READY  STATUS             RESTARTS  AGE
todo-package-admatic-chart-f99d94b6c-zwnxf  0/1    ContainerCreating  0         1s

==> v1/Service
NAME                        TYPE      CLUSTER-IP   EXTERNAL-IP  PORT(S)       AGE
todo-package-admatic-chart  NodePort  10.7.243.22  <none>       80:32633/TCP  1s

==> v1/Deployment
NAME                        DESIRED  CURRENT  UP-TO-DATE  AVAILABLE  AGE
todo-package-admatic-chart  1        1        1           0          1s


NOTES:
1. Get the application URL by running these commands:
  export NODE_PORT=$(kubectl get --namespace default -o jsonpath="{.spec.ports[0].nodePort}" services todo-package-admatic-chart)
  export NODE_IP=$(kubectl get nodes --namespace default -o jsonpath="{.items[0].status.addresses[0].address}")
  echo http://$NODE_IP:$NODE_PORT

Repositories

In order to make it much easier to share packages, Helm has built-in support for installing packages from an HTTP server. Helm reads a repository index hosted on the server which describes what chart packages are available and where they are located. This is how the default stable repository works.

We can use the helm serve command to run a local repository to serve our chart.

helm serve
Regenerating index. This may take a moment.
Now serving you on 127.0.0.1:8879

Now, in a separate terminal window, you should be able to see your chart in the local repository and install it from there:

helm search local
NAME                    CHART VERSION   APP VERSION     DESCRIPTION
local/admatic-chart     0.1.0           1.0             A Helm chart for Kubernetes
stable/magic-ip-address 0.1.0           0.9.0           A Helm chart to assign static IP addresses for node-local...
helm install --name todo-repo local/admatic-chart --set service.type=NodePort
NAME:   todo-repo
LAST DEPLOYED: Thu Jan  3 13:47:37 2019
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1/Service
NAME                     TYPE      CLUSTER-IP    EXTERNAL-IP  PORT(S)       AGE
todo-repo-admatic-chart  NodePort  10.7.244.165  <none>       80:30302/TCP  0s

==> v1/Deployment
NAME                     DESIRED  CURRENT  UP-TO-DATE  AVAILABLE  AGE
todo-repo-admatic-chart  1        1        1           0          0s

==> v1/Pod(related)
NAME                                     READY  STATUS             RESTARTS  AGE
todo-repo-admatic-chart-5c9685665-587p5  0/1    ContainerCreating  0         0s


NOTES:
1. Get the application URL by running these commands:
  export NODE_PORT=$(kubectl get --namespace default -o jsonpath="{.spec.ports[0].nodePort}" services todo-repo-admatic-chart)
  export NODE_IP=$(kubectl get nodes --namespace default -o jsonpath="{.items[0].status.addresses[0].address}")
  echo http://$NODE_IP:$NODE_PORT

To setup a remote repository you can follow the guide in the Helm documentation.

Dependencies

As the applications your packaging as charts increase in complexity, you might find you need to pull in a dependency such as a database. Helm allows you to specify sub-charts that will be created as part of the same release. To define a dependency, create a requirements.yaml file in the chart root directory:

cat << EOF > ./admatic-chart/requirements.yaml
dependencies:
  - name: mariadb
    version: 0.6.0
    repository: https://kubernetes-charts.storage.googleapis.com
EOF

Much like a runtime language dependency file (such as Python’s requirements.txt), the requirements.yaml file allows you to manage your chart’s dependencies and their versions. When updating dependencies, a lockfile is generated so that subsequent fetching of dependencies use a known, working version. Run the following command to pull in the MariaDB dependency we defined:

helm dep update ./admatic-chart
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "local" chart repository
...Successfully got an update from the "stable" chart repository
Update Complete. ⎈Happy Helming!⎈
Saving 1 charts
Downloading mariadb from repo https://kubernetes-charts.storage.googleapis.com
Deleting outdated charts
ls admatic-chart/charts
mariadb-0.6.0.tgz

Helm has found a matching version in the stable repository and has fetched it into the chart's sub-chart directory. Now when we go and install the chart, we’ll see that MariaDB’s objects are created too:

helm install --name maria-dep ./admatic-chart --set service.type=NodePort
NAME:   maria-dep
LAST DEPLOYED: Thu Jan  3 13:53:44 2019
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1/Service
NAME                     TYPE       CLUSTER-IP   EXTERNAL-IP  PORT(S)       AGE
maria-dep-mariadb        ClusterIP  10.7.244.7   <none>       3306/TCP      1s
maria-dep-admatic-chart  NodePort   10.7.241.46  <none>       80:31116/TCP  1s

==> v1beta1/Deployment
NAME               DESIRED  CURRENT  UP-TO-DATE  AVAILABLE  AGE
maria-dep-mariadb  1        1        1           0          1s

==> v1/Deployment
NAME                     DESIRED  CURRENT  UP-TO-DATE  AVAILABLE  AGE
maria-dep-admatic-chart  1        1        1           0          1s

==> v1/Pod(related)
NAME                                      READY  STATUS             RESTARTS  AGE
maria-dep-mariadb-fdf4665cb-shw4p         0/1    Pending            0         1s
maria-dep-admatic-chart-57ff5654c4-rqnmt  0/1    ContainerCreating  0         1s

==> v1/Secret
NAME               TYPE    DATA  AGE
maria-dep-mariadb  Opaque  2     1s

==> v1/ConfigMap
NAME               DATA  AGE
maria-dep-mariadb  1     1s

==> v1/PersistentVolumeClaim
NAME               STATUS   VOLUME    CAPACITY  ACCESS MODES  STORAGECLASS  AGE
maria-dep-mariadb  Pending  standard  1s


NOTES:
1. Get the application URL by running these commands:
  export NODE_PORT=$(kubectl get --namespace default -o jsonpath="{.spec.ports[0].nodePort}" services maria-dep-admatic-chart)
  export NODE_IP=$(kubectl get nodes --namespace default -o jsonpath="{.items[0].status.addresses[0].address}")
  echo http://$NODE_IP:$NODE_PORT

Contribute To The Stable Repository

One of the advantages of Helm is its great set of community contributed charts that you can install with a single command. As a chart author, you can help to build out the stable repository by improving existing charts or submitting new ones. Checkout https://kubeapps.com to see what’s currently available.

results matching ""

    No results matching ""