github.com/grafana/tanka@v0.26.1-0.20240506093700-c22cfc35c21a/docs/design/tanka.md (about) 1 # tanka, advanced Kubernetes configuration 2 **Author**: @sh0rez 3 **Date**: 26.07.2019 4 5 ## Motivation 6 `json` and it's superset `yaml` are great configuration languages for 7 machines, but they are [not really good for humans](https://youtu.be/FjdS21McgpE). 8 9 But the Kubernetes ecosystem seems to have settled to configuring all kinds of 10 workloads using exactly these. While they provide a low entry barrier, it is 11 challenging to model advanced requirements, for example deploying the same 12 application to multiple clusters or to different environments. 13 14 This usually leads to duplication on two levels: The obvious one is the 15 application level. Once an app needs to redeployed to another environment 16 (`dev`, `prod`, etc.), it usually shares most of the configuration, except for 17 some edge-cases (secrets, etc). `kubectl` does not really provide a utility to 18 fully address this. 19 20 Even more duplication happens on the systems level. Today, multiple applications 21 are usually composed into a larger picture. 22 But deploying common building blocks as `postgresql` or `nginx` requires the 23 same code (`Deployment`, `Service`, etc) that nearly everyone attempting to use 24 these will write. 25 26 While maintaining the same code for `dev` and `prod` might go well, it becomes 27 unconquerable once it comes to multi-region (5+) deployments or even more 28 versatile use-cases. 29 30 #### Existing solutions 31 Historically, this problem has been approached using string-templating (`helm`). 32 While `helm` allows code-reuse using `values.yml`, a chart is only able to provide what 33 has been thought of during writing. Even worse, charts are maintained inside of 34 the `helm/charts` repository, which makes quick or even domain-specific edits 35 hard. It is impossible to easily address edge-cases. 36 37 A solution to this problem is called [jsonnet](https://jsonnet.org): It is 38 basically `json` but with variables, conditionals, arithmetic, functions, 39 **imports**, and error propagation and especially very clever [**deep-merging**](#edge-cases). 40 41 #### Prior art 42 Especially [`ksonnet`](https://ksonnet.io) had a big impact on this idea. 43 While `ksonnet` really proved the idea to be working, is was based on the 44 concept of components, building blocks consisting of prototypes and parameters, 45 which may be composed into applications or modules which in turn may be applied 46 to multiple environments. 47 48 We believe such a concept overcomplicates the immediate goal of reducing 49 duplication while allowing edge-cases, because it handles these on a higher 50 conceptual level, instead of reusing the native capabilities of jsonnet. 51 52 Code-reuse and composability is 53 adequately provided by the native `import` feature of `jsonnet`. Sharing code 54 beyond application boundaries is already enabled by 55 [`jsonnet-bundler`](https://github.com/jsonnet-bundler/jsonnet-bundler). 56 57 While it is possible to mimic environments with native `jsonnet`, it falls short 58 when it comes to actually applying it to the correct cluster. The raw `json` 59 being returned by the compiler needs reconciling and it must be made sure it is 60 applied to the correct cluster. 61 62 This leaves us effectively with **`jsonnet`** and **[Environments](#environments)** 63 64 ## Code Reuse 65 By using [`jsonnet`](https://jsonnet.org) as the underlying data templating 66 language, tanka supports dynamic reusing of code, just like real programming languages do: 67 68 ### Imports 69 `jsonnet` is able to 70 [import](https://jsonnet.org/learning/tutorial.html#imports) other jsonnet 71 snippets into the current scope, which in turn allows to refactor commonly used code 72 into shared libraries. 73 74 ### Bundle 75 Once a library becomes general enough to be used beyond project or 76 even domain boundaries, it is a common practice in programming languages to have 77 shared dependencies. 78 79 In the `jsonnet` world, this is provided by 80 [`jsonnet-bundler`](https://github.com/jsonnet-bundler/jsonnet-bundler), which maintains a 81 `vendor` folder (the bundle) with all required libraries ready to be imported, 82 much like older versions of `go` used to (<1.11) do. 83 This procedure integrates smoothly with tanka, because `vendor` is on the [`JPATH`](#jpath). 84 85 ### `JPATH` 86 To enable a predictable developer experience, tanka uses clear rules to define 87 how importing works. 88 89 Imports are relative to the `JPATH`. The earlier a directory appears in the 90 `JPATH`, the higher its precedence is. 91 92 To set it up, tanka makes use of the following directories: 93 94 | Name | Identifier | Description | 95 |------------------|--------------------|---------------------------------------------------------------------------------------------------------------------------------------| 96 | `rootDir` | `jsonnetfile.json` | Every file in the tree of this folder is considered part of the project. Much like `git` has the one directory with the `.git` folder | 97 | `rootDir/vendor` | | Populated with shared dependencies by `jsonnet-bundler` | 98 | `baseDir/lib` | | Code that is only re-used in-tree may be put here | 99 | `baseDir` | `main.jsonnet` | Environment specific code can be put here. The special `main.jsonnet` is the entry point for the evaluation | 100 101 To resolve the `JPATH`, tanka first traverses the directory tree *upwards*, to 102 find a `jsonnetfile.json`, which marks the `rootDir`. Reaching `/` without a 103 match will result in an error. 104 This is required to be able to resolve the `JPATH` regardless of how deep one is 105 inside of the directory tree. Think of it as a root marker, like git has its `.git` folder. 106 Even if `jb` is not used, it barely harms to have an unused file with `{}` in it around. 107 108 Same applies for the `baseDir`, the tree is traversed *upwards* for a 109 `main.jsonnet`. 110 111 The final `JPATH` looks like the following: 112 ``` 113 <baseDir> 114 <rootDir>/lib 115 <rootDir>/vendor 116 ``` 117 118 119 ### Directory structure 120 In a simple setup, it is fair to have the same `rootDir` and `baseDir`: 121 ``` 122 . 123 ├── jsonnetfile.json 124 ├── lib/ 125 ├── main.jsonnet 126 └── vendor/ 127 ``` 128 129 However, to use [Environments](#environments), the `baseDir` could be moved 130 into subdirectories: 131 ``` 132 . 133 ├── environments 134 │ ├── dev 135 │ │ └── main.jsonnet 136 │ └── prod 137 │ └── main.jsonnet 138 ├── jsonnetfile.json 139 ├── lib/ 140 └── vendor/ 141 ``` 142 143 While the latter structure is the one suggested by `tk init`, it is perfectly 144 fine to use another if it fits the use-case better. The folder does not need to 145 be named `environments`, either. 146 147 ## Edge Cases 148 During development of `jsonnet` libraries, e.g. for applications like `mysql`, 149 it is impossible to think of every edge-case in advance. 150 151 But thanks to the power of `jsonnet`, this is not a problem. Imagine the output 152 of the library being the following: 153 ```jsonnet 154 local out = { 155 apiVersion: "v1", 156 kind: "namespace", 157 metadata: { 158 name: "production" 159 } 160 }; 161 ``` 162 163 When you wanted to label the namespace, but the library did not provide 164 functions for it, you could use deep-merging: 165 ```jsonnet 166 out + { 167 metadata+: { 168 labels: { 169 foo: "bar" 170 } 171 } 172 } 173 ``` 174 175 Note the special `+:` to enable deep merging. 176 177 This would result in the second dict being recursively merged on top of the first one: 178 ```jsonnet 179 { 180 apiVersion: "v1", 181 kind: "namespace", 182 metadata: { 183 name: "production", 184 labels: { 185 foo: "bar" 186 } 187 } 188 } 189 ``` 190 191 ## Environments 192 The only core concept of tanka is an `Environment`. It describes a single 193 context that can be configured. Such a context might be `dev` and `prod` 194 environments, Blue/Green deployments or the same application available 195 in multiple regional zones / datacenters. 196 197 An environment does not need to be created, it rather just exists as soon as a 198 `main.jsonnet` is found somewhere in the tree of a `rootDir`. 199 200 However, an environment may receive additional configuration, by adding a file 201 called `spec.json` alongside the `main.jsonnet`: 202 ```json 203 { 204 "apiVersion": "tanka.dev/v1alpha1", 205 "kind": "Environment", 206 "metadata": { 207 "name": "auto", 208 "labels": {} 209 }, 210 "spec": { 211 "apiServer": "https://localhost:6443", 212 "namespace": "default" 213 } 214 } 215 ``` 216 217 | Field | Description | 218 |-------------------|---------------------------------------------------------------------| 219 | `apiVersion` | marks the version of the API, to allow schema changes once required | 220 | `kind` | not used yet, added for completeness | 221 | `metadata.name` | automatically set to the directory name | 222 | `metadata.labels` | descriptive `key:value` pairs | 223 | `spec.apiServer` | The Kubernetes endpoint to use | 224 | `spec.namespace` | Default namespace used if not set in jsonnet | 225 226 The environment object is accessible from within `jsonnet`. 227 228 ## Workflow 229 The anticipated workflow is a three step process: 230 231 1. **Iterating:** During the actual development phase, one quickly switches between the editor with the jsonnet inside and `tk show`, which renders the Kubernetes manifests in `yaml` form on screen, to verify it all looks as expected. 232 2. **Verification**: `tk diff` allows to quickly check whether the changes to be done are correct. 233 3. **Applying**: Finally, `tk apply` invokes `kubectl` to apply the changes to the cluster.