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.