Getting started with ksonnet

Introduction

This tutorial will show you how to create a simple application and also how to deploy it to kubernetes using ksonnet, in the examples I will be using minikube or you can check out this repo that has a good overview of minikube, once installed and started (minikube start) that command will download and configure the local environment, if you have been following the previous posts you already have minikube installed and working, before we dive into an example let’s review some terminology from ksonnet (extracted from the official documentation):

Application

A ksonnet application represents a well-structured directory of Kubernetes manifests (this is generated using the ks init).

Environment

An environment consists of four elements, some of which can be pulled from your current kubeconfig context: Name, Server, Namespace, API version. The environment determines to which cluster you’re going to deploy the application.

Component

A component can be as simple as a Kubernetes resource (a Pod, Deployment, etc), or a fully working stack for example EFK/ELK, you can generate components using ks generate.

Prototype

Prototype + Parameters = Component. Think of a prototype as a base template before you apply the parameters, to set a name, replicas, etc for the resource, you can explore some system prototypes with ks prototype.

Parameter

It gives live to a component with dynamic values, you can use ks param to view or modify params, there are App params (global), Component params, and Environment params (overrides app params).

Module

Modules provide a way for you to share components across environments. More concisely, a module refers to a subdirectory in components/ containing its own params.libsonnet. To create a module ks module create <module name>.

Part

It provides a way to organize and re-use code.

Package

A package is a set of related prototypes and associates helper libraries, it allows you to create and share packages between applications.

Registry

It’s essentially a repository for packages, it supports the incubator registry, github, filesystem, and Helm.

Manifest

The same old YAML or JSON manifest but this time written in Jsonnet, basically Jsonnet is a simple extension of JSON.

Phew, that’s a lot of names and terminology at once, let’s get started with the terminal already.

Let’s get started

This command will generate the following folder structure ks init wordpress:

INFO Using context "minikube" from kubeconfig file "~/.kube/config"
INFO Creating environment "default" with namespace "default", pointing to "version:v1.12.4" cluster at address "https://192.168.99.100:8443"
INFO Generating ksonnet-lib data at path '~/k8s-examples/wordpress/lib/ksonnet-lib/v1.12.4'

$ ls -l |  awk '{ print $9 }'
app.yaml        <--- Defines versions, namespace, cluster address, app name, registry.
components      <--- Components by default it's empty and has a params file.
environments    <--- By default there is only one environment called default.
lib             <--- Here we can find the ksonnet helpers that match the Kubernetes API with the common resources (Pods, Deployments, etc).
vendor          <--- Here is where the installed packages/apps go, it can be seen as a dependencies folder.

Let’s generate a deployed-service and inspect it’s context:

$ ks generate deployed-service wordpress \
  --image bitnami/wordpress:5.0.2 \
  --type ClusterIP

INFO Writing component at '~/k8s-examples/wordpress/components/wordpress.jsonnet'
At the moment of this writing the latest version of Wordpress is 5.0.2, it’s always recommended to use static version numbers instead of tags like latest (because latest can not be latest).

Let’s see how our component looks like:

local env = std.extVar("__ksonnet/environments");
local params = std.extVar("__ksonnet/params").components.wordpress;
[
  {
    "apiVersion": "v1",
    "kind": "Service",
    "metadata": {
      "name": params.name
    },
    "spec": {
      "ports": [
        {
          "port": params.servicePort,
          "targetPort": params.containerPort
        }
      ],
      "selector": {
        "app": params.name
      },
      "type": params.type
    }
  },
  {
    "apiVersion": "apps/v1beta2",
    "kind": "Deployment",
    "metadata": {
      "name": params.name
    },
    "spec": {
      "replicas": params.replicas,
      "selector": {
        "matchLabels": {
          "app": params.name
        },
      },
      "template": {
        "metadata": {
          "labels": {
            "app": params.name
          }
        },
        "spec": {
          "containers": [
            {
              "image": params.image,
              "name": params.name,
              "ports": [
                {
                  "containerPort": params.containerPort
                }
              ]
            }
          ]
        }
      }
    }
  }
]
It’s just another template for some known resources, a service and a deployment that’s where the name came from: deployed-service, but where are those params coming from?

If we run ks show default:

---
apiVersion: v1
kind: Service
metadata:
  labels:
    ksonnet.io/component: wordpress
  name: wordpress
spec:
  ports:
  - port: 80
    targetPort: 80
  selector:
    app: wordpress
  type: ClusterIP
---
apiVersion: apps/v1beta2
kind: Deployment
metadata:
  labels:
    ksonnet.io/component: wordpress
  name: wordpress
spec:
  replicas: 1
  selector:
    matchLabels:
      app: wordpress
  template:
    metadata:
      labels:
        app: wordpress
    spec:
      containers:
      - image: bitnami/wordpress:5.0.2
        name: wordpress
        ports:
        - containerPort: 80
We will see what our package will generate in YAML with some good defaults. And by default if you remember from the definitions a component needs a params file to fill the blanks in this case it is components/params.libsonnet:
{
  global: {
    // User-defined global parameters; accessible to all component and environments, Ex:
    // replicas: 4,
  },
  components: {
    // Component-level parameters, defined initially from 'ks prototype use ...'
    // Each object below should correspond to a component in the components/ directory
    wordpress: {
      containerPort: 80,
      image: "bitnami/wordpress:5.0.2",
      name: "wordpress",
      replicas: 1,
      servicePort: 80,
      type: "ClusterIP",
    },
  },
}
But that’s not enough to run wordpress is it?, No is not, we need a database with persistent storage for it to work properly, so we will need to generate and extend another deployed-service.

The next step would be to create another component:

$ ks generate deployed-service mariadb \
  --image bitnami/mariadb:10.1.37 \
  --type ClusterIP

INFO Writing component at '/home/kainlite/Webs/k8s-examples/wordpress/components/mariadb.jsonnet'
The latest stable version of MariaDB 10.1 GA at the moment of this writting is 10.1.37.

Then we will need to add a persistent volume and also tell Wordpress to use this MariaDB instance. How do we do that, we will need to modify a few files, like this (in order to re-use things I placed the mysql variables in the global section, for this example that will simplify things, but it might not be the best approach for a production environment): The resulting components/params.json will be:

{
  global: {
    // User-defined global parameters; accessible to all component and environments, Ex:
    // replicas: 4,
    mariadbEmptyPassword: "no",
    mariadbUser: "mywordpressuser",
    mariadbPassword: "mywordpresspassword",
    mariadbDatabase: "bitnami_wordpress",
  },
  components: {
    // Component-level parameters, defined initially from 'ks prototype use ...'
    // Each object below should correspond to a component in the components/ directory
    wordpress: {
      containerPort: 80,
      image: "bitnami/wordpress:5.0.2",
      name: "wordpress",
      replicas: 1,
      servicePort: 80,
      type: "ClusterIP",
    },
    mariadb: {
      containerPort: 3306,
      image: "bitnami/mariadb:10.1.37",
      name: "mariadb",
      replicas: 1,
      servicePort: 3306,
      type: "ClusterIP",
    },
  },
}

The resulting components/wordpress.jsonnet will be:

local env = std.extVar("__ksonnet/environments");
local params = std.extVar("__ksonnet/params").components.wordpress;
[
  {
    "apiVersion": "v1",
    "kind": "Service",
    "metadata": {
      "name": params.name
    },
    "spec": {
      "ports": [
        {
          "port": params.servicePort,
          "targetPort": params.containerPort
        }
      ],
      "selector": {
        "app": params.name
      },
      "type": params.type
    }
  },
  {
    "apiVersion": "apps/v1beta2",
    "kind": "Deployment",
    "metadata": {
      "name": params.name
    },
    "spec": {
      "replicas": params.replicas,
      "selector": {
        "matchLabels": {
          "app": params.name
        },
      },
      "template": {
        "metadata": {
          "labels": {
            "app": params.name
          }
        },
        "spec": {
          "containers": [
            {
              "image": params.image,
              "name": params.name,
              "ports": [
                {
                  "containerPort": params.containerPort
                }
              ],
              "env": [
                {
                    "name": "WORDPRESS_DATABASE_USER",
                    "value": params.mariadbUser,
                },
                {
                    "name": "WORDPRESS_DATABASE_PASSWORD",
                    "value": params.mariadbPassword,
                },
                {
                    "name": "WORDPRESS_DATABASE_NAME",
                    "value": params.mariadbDatabase,
                },
                {
                    "name": "WORDPRESS_HOST",
                    "value": "mariadb",
                }
              ]
            }
          ]
        }
      }
    }
  }
]
The only thing that changed here is spec.containers.env which wasn’t present before.

The resulting components/mariadb.jsonnet will be:

local env = std.extVar("__ksonnet/environments");
local params = std.extVar("__ksonnet/params").components.mariadb;
[
{
    "apiVersion": "v1",
        "kind": "Service",
        "metadata": {
            "name": params.name
        },
        "spec": {
            "ports": [
            {
                "port": params.servicePort,
                "targetPort": params.containerPort
            }
            ],
            "selector": {
                "app": params.name
            },
            "type": params.type
        }
},
{
    "apiVersion": "apps/v1beta2",
    "kind": "Deployment",
    "metadata": {
        "name": params.name
    },
    "spec": {
        "replicas": params.replicas,
        "selector": {
            "matchLabels": {
                "app": params.name
            },
        },
        "template": {
            "metadata": {
                "labels": {
                    "app": params.name
                }
            },
            "spec": {
                "containers": [
                {
                    "image": params.image,
                    "name": params.name,
                    "ports": [
                    {
                        "containerPort": params.containerPort
                    },
                    ],
                    "env": [
                    {
                        "name": "ALLOW_EMPTY_PASSWORD",
                        "value": params.mariadbEmptyPassword,
                    },
                    {
                        "name": "MARIADB_USER",
                        "value": params.mariadbUser,
                    },
                    {
                        "name": "MARIADB_PASSWORD",
                        "value": params.mariadbPassword,
                    },
                    {
                        "name": "MARIADB_ROOT_PASSWORD",
                        "value": params.mariadbPassword,
                    },
                    {
                        "name": "MARIADB_DATABASE",
                        "value": params.mariadbDatabase,
                    },
                    ],
                    "volumeMounts": [
                    {
                        "mountPath": "/var/lib/mysql",
                        "name": "mariadb"
                    }
                    ]
                }
                ],
                "volumes": [
                {
                    "name": "mariadb",
                    "hostPath": {
                        "path": "/home/docker/mariadb-data"
                    }
                }
                ]
            }
        }
    }
}
]
I know, I know, that is a lot of JSON, I trust you have a decent scroll :).

The only things that changed here are spec.containers.env, spec.containers.volumeMount and spec.volumes which weren’t present before, that’s all you need to make wordpress work with mariadb.

This post only scratched the surface of what Ksonnet and Jsonnet can do, in another post I will describe more advances features with less JSON / YAML. There are a lot of things that can be improved and we will cover those things in the next post, if you want to see all the source code for this post go here.

Let’s clean up ks delete default:

INFO Deleting services mariadb
INFO Deleting deployments mariadb
INFO Deleting services wordpress
INFO Deleting deployments wordpress

Notes

If you want to check the wordpress installation via browser you can do minikube proxy and then look up the following URL: Wordpress (I’m using the default namespace here and the service name is wordpress, if you use ingress you don’t need to do this step)

I’m not aware if Ksonnet supports releases and rollbacks like Helm, but it seems it could be emulated using git tags and just some git hooks.

If everything goes well, you should see something like this in the logs:

$ kubectl logs -f wordpress-5b4d6bd47c-bdtmw

Welcome to the Bitnami wordpress container
Subscribe to project updates by watching https://github.com/bitnami/bitnami-docker-wordpress
Submit issues and feature requests at https://github.com/bitnami/bitnami-docker-wordpress/issues

nami    INFO  Initializing apache
apache  INFO  ==> Patching httpoxy...
apache  INFO  ==> Configuring dummy certificates...
nami    INFO  apache successfully initialized
nami    INFO  Initializing php
nami    INFO  php successfully initialized
nami    INFO  Initializing mysql-client
nami    INFO  mysql-client successfully initialized
nami    INFO  Initializing libphp
nami    INFO  libphp successfully initialized
nami    INFO  Initializing wordpress
mysql-c INFO  Trying to connect to MySQL server
mysql-c INFO  Found MySQL server listening at mariadb:3306
mysql-c INFO  MySQL server listening and working at mariadb:3306
wordpre INFO
wordpre INFO  ########################################################################
wordpre INFO   Installation parameters for wordpress:
wordpre INFO     First Name: FirstName
wordpre INFO     Last Name: LastName
wordpre INFO     Username: user
wordpre INFO     Password: **********
wordpre INFO     Email: user@example.com
wordpre INFO     Blog Name: User's Blog!
wordpre INFO     Table Prefix: wp_
wordpre INFO   (Passwords are not shown for security reasons)
wordpre INFO  ########################################################################
wordpre INFO
nami    INFO  wordpress successfully initialized
INFO  ==> Starting wordpress...
[Thu Dec 27 04:30:59.684053 2018] [ssl:warn] [pid 116] AH01909: localhost:443:0 server certificate does NOT include an ID which matches the server name
[Thu Dec 27 04:30:59.684690 2018] [ssl:warn] [pid 116] AH01909: localhost:443:0 server certificate does NOT include an ID which matches the server name
[Thu Dec 27 04:30:59.738783 2018] [ssl:warn] [pid 116] AH01909: localhost:443:0 server certificate does NOT include an ID which matches the server name
[Thu Dec 27 04:30:59.739701 2018] [ssl:warn] [pid 116] AH01909: localhost:443:0 server certificate does NOT include an ID which matches the server name
[Thu Dec 27 04:30:59.765798 2018] [mpm_prefork:notice] [pid 116] AH00163: Apache/2.4.37 (Unix) OpenSSL/1.1.0j PHP/7.2.13 configured -- resuming normal operations
[Thu Dec 27 04:30:59.765874 2018] [core:notice] [pid 116] AH00094: Command line: 'httpd -f /bitnami/apache/conf/httpd.conf -D FOREGROUND'
172.17.0.1 - - [27/Dec/2018:04:31:00 +0000] "GET / HTTP/1.1" 200 3718
172.17.0.1 - - [27/Dec/2018:04:31:01 +0000] "GET /wp-includes/js/wp-embed.min.js?ver=5.0.2 HTTP/1.1" 200 753
172.17.0.1 - - [27/Dec/2018:04:31:01 +0000] "GET /wp-includes/css/dist/block-library/theme.min.css?ver=5.0.2 HTTP/1.1" 200 452
172.17.0.1 - - [27/Dec/2018:04:31:01 +0000] "GET /wp-includes/css/dist/block-library/style.min.css?ver=5.0.2 HTTP/1.1" 200 4281
172.17.0.1 - - [27/Dec/2018:04:31:01 +0000] "GET /wp-content/themes/twentynineteen/style.css?ver=1.1 HTTP/1.1" 200 19371
172.17.0.1 - - [27/Dec/2018:04:31:01 +0000] "GET /wp-content/themes/twentynineteen/print.css?ver=1.1 HTTP/1.1" 200 1230

And that folks is all I have for now, be sure to check out the Ksonnet official documentation and ks help to know more about what ksonnet can do to help you deploy your applications to any kubernetes cluster.

Errata

If you spot any error or have any suggestion, please send me a message so it gets fixed.

Also you can check the source code and changes in the generated code and the sources here



comments powered by Disqus