That's my logoGrafana TankaFlexible, reusable and concise configuration for Kubernetes
Edit page
IntroductionInstallation
Tutorial
OverviewRefresher on deployingUsing JsonnetCreating a new projectEnvironmentsDefining ResourcesTaking a look at the generated resourcesConnecting to the clusterVerifying the changesApplying to the clusterChecking it workedParameterizingAbstractionKubernetes libraryEnvironments
Writing Jsonnet
Libraries
Advanced features
References
Frequently asked questionsKnown issues

Using Jsonnet

The most powerful piece of Tanka is the Jsonnet data templating language. Jsonnet is a superset of JSON, adding variables, functions, patching (deep merging), arithmetic, conditionals and many more to it.

It has a lot in common with more real programming languages such as JavaScript than with markup languages, still it is tailored specifically to representing data and configuration. As opposed to JSON (and YAML) it is a language meant for humans, not for computers.

Creating a new project

To get started with Tanka and Jsonnet, let's initiate a new project, in which we will install both Prometheus and Grafana into our Kubernetes cluster:

$ mkdir prom-grafana && cd prom-grafana # create a new folder for the project and change to it
$ tk init # initiate a new project

This gives us the following directory structure:

├── environments
│   └── default # default environment
│       ├── main.jsonnet # main file (important!)
│       └── spec.json # environment's config
├── jsonnetfile.json
├── lib # libraries
└── vendor # external libraries

For the moment, we only really care about the environments/default folder. The purpose of the other directories will be explained later in this guide (mostly related to libraries).

Environments

When using Tanka, you apply configuration for an Environment to a Kubernetes cluster. An Environment is some logical group of pieces that form an application stack.

Grafana Labs for example runs Loki, Cortex and of course Grafana for our Grafana Cloud hosted offering. For each of these, we have a separate environment. Furthermore, we like to see changes to our code in separate dev setups to make sure they are all good for production usage – so we have dev and prod environments for each app as well, as prod environments usually require other configuration (secrets, scale, etc) than dev. This roughly leaves us with the following:

LokiCortexGrafana
prodName: /environments/loki/prod
Namespace: loki-prod
Name: /environments/cortex/prod
Namespace: cortex-prod
Name: /environments/grafana/prod
Namespace: grafana-prod
devName: /environments/loki/dev
Namespace: loki-dev
Name: /environments/cortex/dev
Namespace: cortex-dev
Name: /environments/grafana/dev
Namespace: grafana-dev

There is no limit in Environment complexity, create as many as you need to model your own requirements. Grafana Labs for example also has all of these multiplied per high-availability region.

To get started, a single environment is enough. Lets use the automatically created environnments/default for that.

Defining Resources

While kubectl loads all .yaml files in a certain folder, Tanka has a single file that serves as the canonical source for all contents of an environment, called main.jsonnet. This is just like Go has the main.go or C++ the main.cpp.

Similar to JSON, each .jsonnet file holds a single object. The one returned by main.jsonnet will hold all of your Kubernetes resources:

// main.jsonnet
{
    "some_deployment": { /* ... */ },
    "some_service": { /* ... */ }
}

They may be deeply nested, Tanka extracts everything that looks like a Kubernetes resource automatically.

So let's rewrite the previous .yaml to very basic .jsonnet:

environments/default/main.jsonnet:
{
  // Grafana
  grafana: {
    deployment: {
      apiVersion: 'apps/v1',
      kind: 'Deployment',
      metadata: {
        name: 'grafana',
      },
      spec: {
        selector: {
          matchLabels: {
            name: 'grafana',
          },
        },
        template: {
          metadata: {
            labels: {
              name: 'grafana',
            },

At the moment, this is even more verbose because we have effectively converted YAML to JSON, which requires more characters by design.

But Jsonnet opens up enough possibilities to improve this a lot, which will be covered in the next sections.

Taking a look at the generated resources

So far so good, but can we make sure Tanka correctly finds our resources? We can! By running tk show you can see the good old yaml, just as kubectl receives it:

# run from the project root:
/prom-grafana$ tk show environments/default
apiVersion: apps/v1
kind: Deployment
metadata:
  name: grafana
spec:
  selector:
# ...

Spend some time here and try to identify resources from the output in the .jsonnet source.

Bonus: There is also tk eval, which displays the raw JSON object main.jsonnet evaluates to. Tanka won't extract resources or mutate the structure here, so you can verify how your Jsonnet works.

Connecting to the cluster

The YAML looks as expected? Let's apply it to the cluster. To do so, Tanka needs some additional configuration.

While kubectl uses a $KUBECONFIG environment variable and a file in the home directory to store the currently selected cluster, Tanka takes a more explicit approach:

Each environment has a file called spec.json, which includes the information to select a cluster:

{
  "apiVersion": "tanka.dev/v1alpha1",
  "kind": "Environment",
  "metadata": {
    "name": "default"
  },
  "spec": {
    "apiServer": "https://127.0.0.1:6443", // cluster to use
    "namespace": "monitoring" // default namespace for all created resources
  }
}

You still have to setup a cluster in $KUBECONFIG that matches this IP – Tanka will automatically find and use it. This also means that all of your kubectl clusters just work.

This allows us to make sure that you will never accidentally apply to the wrong cluster.

Note: Tanka won't create the namespace for you -- you need to include it in Jsonnet by adding it to environments/default/main.jsonnet:

{
  my_namespace: {
    apiVersion: "v1",
    kind: "Namespace",
    metadata: {
      name: "monitoring"
    }
  }
}

Alternatively, you can create the namespace manually:

$ kubectl create ns monitoring

This, however, will create an object that is not tracked by Tanka and thus needs to be taken care of via other means.

Verifying the changes

Before applying to the cluster, Tanka gives you a chance to check that your changes actually behave as expected: tk diff works just like git diff – you see what will be changed.

/prom-grafana$ tk diff environments/default
--- /tmp/LIVE-610130621/apps.v1.Deployment.monitoring.grafana        2019-12-17 20:14:45.213363586 +0100
+++ /tmp/MERGED-517481208/apps.v1.Deployment.monitoring.grafana      2019-12-17 20:14:45.213363586 +0100
@@ -0,0 +1,45 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: grafana
+  namespace: monitoring
+  # ...
+spec:
+  selector:
+    matchLabels:
+      name: grafana
+  strategy:
+    rollingUpdate:
+      maxSurge: 25%
+      maxUnavailable: 25%
+    type: RollingUpdate
+  template:

As you can see, it shows everything as to-be created .. just as we'd expect, since we are using a blank namespace.

Note: Diff may fail before the first apply when the namespace does not yet exist. This is a limitation of kubectl which is used for computing the differences.

Applying to the cluster

Once it's all looking good, tk apply serves the exact same purpose as kubectl apply:

/prom-grafana$ tk apply environments/default
Applying to namespace 'monitoring' of cluster 'default' at 'https://127.0.0.1:6443' using context 'default'.
Please type 'yes' to confirm: yes
deployment.apps/grafana created
deployment.apps/prometheus created
service/grafana created
service/prometheus created

It shows you the diff first and the chosen cluster once more and requires interactive approval (type yes).

After that, kubectl is used to apply to the cluster. By piping to kubectl Tanka makes sure it behaves exactly as you would expect it. No edge-cases of differing Kubernetes client implementations should ever occur.

Checking it worked

Again, let's connect to Grafana:

$ kubectl port-forward --namespace=monitoring deployments/grafana 8080:3000

And go to http://localhost:8080 for Grafana's UI.


PreviousRefresher on deployingNextParameterizing