Skip to content

Creating a new package

Pål Karlsrud edited this page May 8, 2018 · 39 revisions

Before reading this, you should know ...

What is a package?

A package is a collection of files describing how to construct an application. These files describe how the different parts of the application should be created (for instance which webserver to use) and how it is allowed to interact with other applications (ex. if the application should be available to the outside world, which ports it should use etc.).

Technically packages uses an extension of the Helm chart packaging format, which provides a generic way of describing Kubernetes objects.

Creating a new package

The different elements of a package is best shown through an example. The following section will describe how to create a package capable of creating a Jupyter notebook.

Usually packages have the structure seen below

File structure

jupyter/
    Chart.yaml              # metadata describing the package/chart
    README.md               # (Optional) providees general information about the chart
    resources.yaml          # (Optional) describes which third-party resources this package supports
    values.yaml             # default configuration values
    templates/              # (will be described below)
    templates/_helpers.tpl

If you are following this example from home, you are should create a similar file tree now.

Defining package metadata (Chart.yaml)

Some metadata is required for a package to function correctly. A file called Chart.yaml is used to store this metadata. TheChart.yaml that will be used in the Jupyter package is shown below.

# filename: Chart.yaml

apiVersion: v1                     # chart API version, always "v1"
name: jupyter                      # package name
description: Jupyter               # a short sentence describing the package.
version: 1.0.0                     # package version (in SemVer 2 format)
maintainers:
  - name: My Name
    email: me@domain.mail
home: https://package.com          # package homepage
icon: https://package.com/pic.png  # package icon

User-friendly package information (README.md)

In order to make the package more user-friendly, a README.md is recommended. This file provides general information about the package, such as what application the package creates and how to use it.

The README.md for our Jupyter notebook may look like the following file:

# Jupyter notebook

[Jupyter Notebook](http://jupyter.org/) is an open-source web application that
allows you to create and share documents that contain live code, equations,
visualizations and narrative text. Uses include: data cleaning and transformation,
numerical simulation, statistical modeling, data visualization, machine learning, and much more.

Providing default user input (values.yaml)

We want the user to be able to specify certain parts of the application configuration. A package contains a values.yaml file that contains default values that can be overridden by the user.

When creating the Jupyter notebook, we want the user to be able to allocate a custom amount of resources, as well as specifiying which host the application will be using.

The following values.yaml file contains values that does this

# filename: values.yaml

ingress:
  host: "local-chart.example.com"
resources:
  requests:
    cpu: 100m
    memory: 512Mi
  limits:
    cpu: 300m
    memory: 1Gi
dockerImage: quay.io/uninett/jupyterlab:20180501-6469a2f

So, if specifies that the host should be foo.bar.interwebz.cat when installing the application, the value of ingress.host: "local-chart.example.com" will be replaced with the user input. So, all the values in this file is optional, and a user does not specify a value, the value present in values.yaml will be used as defaults.

when later creating the Kubernetes object templates, these values can be referenced like this {{ .Values.ingress.host }}.

Creating Kubernetes objects (templates/)

As we want make it possible for the user to specify how certain aspects of a application should be created, we need generic /templates/ that can be used to create Kubernetes objects. These templates are stored in the templates/ directory.

Templates are written in Go template syntax with some additional functions added by Helm. See the Helm template guide for more information.

A template is mostly just a definition of a Kubernetes object, but with the ability to insert variables into parts of the template. Below an example of a template is shown

apiVersion: v1
kind: Secret
metadata:
  name: secret
type: Opaque
data:
  psst: {{ .Values.very_secret | quote }} # <-- this uses the Go templating engine to insert some secret data

Given the preceeding values.yaml file

very_secret: "Pink is pretty cool"

the Kubernetes secret above will be rendered as

apiVersion: v1
kind: Secret
metadata:
  name: secret
type: Opaque
data:
  psst: "Pink is pretty cool"

A quick detour to user defined functions (templates/_helpers.tpl)

In order to make the package easier to maintain, it is useful to define functions or commonly used variables.

Below the _helpers.tpl file we will use for the Jupyter notebook is shown.

{{/* filename: templates/_helpers.tpl */}}

{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes
name fields are limited to this (by the DNS naming spec).
*/}}
{{- define "fullname" -}}
{{- $name := default .Chart.Name -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}

The template above defines a variable called fullname which ensures that the application name is valid in Kubernetes. Another variable called "oidcconfig" is also defined, which contains the JSON config required to use a component we will create soon. To use this variable, the following syntax is used {{ template "fullname" . }}.


Knowing that we can use functions and variables from templates/_helpers.tpl, we can continue creating our regular templates. Our Jupyter notebook will consist of the following components:

  1. The Jupyter notebook
  2. An ingress which exposes the application to the outside world
  3. A proxy which provides Dataporten authentication

We will begin by creating the Jupyter notebook. In order to make the application easy to manage and scale, we will use a Kubernetes Deployment object as shown below.

# filename: templates/deployment.yaml

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: {{ template "fullname" . }}
  labels:
    chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: {{ template "fullname" . }}
    spec:
      containers:
      - name: jupyter
        image: {{ .Values.dockerImage }}
        resources:
{{ toYaml .Values.resources | indent 10 }}
        ports:
        - containerPort: 8888

this file creates a Kubernetes deployment using an image provided by the user (through the dockerImage value in values.yaml) and exposes it on port 8888.

Next, we want to create the ingress exposing the application to the outside world. To do so, we first need a service which exposes the deployment to the cluster

# filename: templates/service.yaml

apiVersion: v1
kind: Service
metadata:
  name: {{ template "fullname" . }}
  labels:
    chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
spec:
  ports:
  - port: 8888
    targetPort: 8888
    protocol: TCP
    name: {{ template "fullname" . }}-service
  selector:
    app: {{ template "fullname" . }}

then, we can create the ingress as follows

# filename: templates/ingress.yaml

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: {{ template "fullname" . }}
  labels:
    app: {{ template "fullname" . }}
    chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
    release: "{{ .Release.Name }}"
    heritage: "{{ .Release.Service }}"
  annotations:
    kubernetes.io/ingress.class: nginx
    kubernetes.io/tls-acme-staging: "true"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
spec:
  tls:
    - secretName: {{ template "fullname" . }}-tls
      hosts:
         - {{ .Values.ingress.host }}
  rules:
    - host: {{ .Values.ingress.host }}
      http:
        paths:
          - path: /
            backend:
              serviceName: {{ template "fullname" . }}
              servicePort: 8888

As the application by default is isolated from all the others in the clusters, we now need to create a NetworkPolicy allowing traffic between the notebook and the ingress.

# filename: templates/networkpolicy.yaml

apiVersion: extensions/v1beta1
kind: NetworkPolicy
metadata:
  name: {{ template "fullname" . }}
spec:
  podSelector:
    matchLabels:
      app: {{ template "fullname" . }}
  ingress:
    - from:
      - namespaceSelector:
          matchLabels:
            name: kube-ingress
      ports:
        - protocol: TCP
          port: 8888

Specifying supported third-party resources (resources.yaml)

To make it easier for third-parties to provide custom resources, it is possible to specify which third-party resources a package supports. A third-party plugin can for instance be used to get information from a API and then insert the value into the templates.

Testing

Debugging

Checking packages for errors

In order to see whether the packages is valid, you can use

helm lint --strict <chart directory>

which will notify you of any errors.

This repository (that is, uninett/helm-charts) also contains a script called lint-chart.sh which uses kubeval and kubetest to determine whether the package is valid. This script can be run in the following way: ./lint-chart.sh <chart directory>.

Viewing generated Kubernetes objects

In order to see what the Kubernetes objects will look like after having been passed through the template engine, you can use

helm template <chart directory>

which will output all the generated files to stdout.

Clone this wiki locally