blob: bdd25b155e2746c4d75bba47088d9a0aac515002 [file] [log] [blame] [view]
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001# Kubernetes tutorial
2
3In this tutorial we show how to convert Kubernetes configuration files
4for a collection of microservices.
5
6The configuration files are scrubbed and renamed versions of
7real-life configuration files.
8The files are organized in a directory hierarchy grouping related services
9in subdirectories.
10This is a common pattern.
11The `cue` tooling has been optimized for this use case.
12
Emil Hessman66ec9592019-07-14 17:58:27 +020013In this tutorial we will address the following topics:
Marcel van Lohuizen02173f82018-12-20 13:27:07 +010014
151. convert the given YAML files to CUE
161. hoist common patterns to parent directories
171. use the tooling to rewrite CUE files to drop unnecessary fields
181. repeat from step 2 for different subdirectories
191. define commands to operate on the configuration
Joel Longtineb6544662019-06-11 16:59:12 +0000201. extract CUE templates directly from Kubernetes Go source
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100211. manually tailor the configuration
221. map a Kubernetes configuration to `docker-compose` (TODO)
23
24
25## The given data set
26
27The data set is based on a real-life case, using different names for the
28services.
29All the inconsistencies of the real setup are replicated in the files
30to get a realistic impression of how a conversion to CUE would behave
31in practice.
32
33The given YAML files are ordered in following directory
34(you can use `find` if you don't have tree):
35
36```
37$ tree ./testdata | head
38.
39└── services
40 ├── frontend
41 │ ├── bartender
42 │ │ └── kube.yaml
43 │ ├── breaddispatcher
44 │ │ └── kube.yaml
45 │ ├── host
46 │ │ └── kube.yaml
47 │ ├── maitred
48...
49```
50
51Each subdirectory contains related microservices that often share similar
52characteristics and configurations.
53The configurations include a large variety of Kubernetes objects, including
54services, deployments, config maps,
55a daemon set, a stateful set, and a cron job.
56
57The result of the first tutorial is in the `quick`, for "quick and dirty"
58directory.
59A manually optimized configuration can be found int `manual`
60directory.
61
62
63## Importing existing configuration
64
65We first make a copy of the data directory.
66
67```
Marcel van Lohuizenac39cd72019-06-11 22:27:54 +020068$ cp -a original tmp
Marcel van Lohuizenb13155b2019-10-25 16:52:30 +020069$ cd tmp
Marcel van Lohuizen02173f82018-12-20 13:27:07 +010070```
71
Marcel van Lohuizenb13155b2019-10-25 16:52:30 +020072We initialize a module so that we can treat all our configuration files
73in the subdirectories as part of one package.
74We do that later by giving all the same package name.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +010075
76```
Marcel van Lohuizenb13155b2019-10-25 16:52:30 +020077$ cue mod init
Marcel van Lohuizen02173f82018-12-20 13:27:07 +010078```
Marcel van Lohuizenb13155b2019-10-25 16:52:30 +020079
80Creating a module also allows our packages import external packages.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +010081
82Let's try to use the `cue import` command to convert the given YAML files
83into CUE.
84
85```
Marcel van Lohuizenb13155b2019-10-25 16:52:30 +020086$ cd services
Marcel van Lohuizen02173f82018-12-20 13:27:07 +010087$ cue import ./...
Marcel van Lohuizen2437f9d2019-10-29 12:22:38 +010088must specify package name with the -p flag
Marcel van Lohuizen02173f82018-12-20 13:27:07 +010089```
90
91Since we have multiple packages and files, we need to specify the package to
92which they should belong.
93
94```
95$ cue import ./... -p kube
Marcel van Lohuizen2437f9d2019-10-29 12:22:38 +010096list, flag, or files flag needed to handle multiple objects in file "./frontend/bartender/kube.yaml"
Marcel van Lohuizen02173f82018-12-20 13:27:07 +010097```
98
99Many of the files contain more than one Kubernetes object.
100Moreover, we are creating a single configuration that contains all objects
101of all files.
102We need to organize all Kubernetes objects such that each is individually
Emil Hessman66ec9592019-07-14 17:58:27 +0200103identifiable within a single configuration.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100104We do so by defining a different struct for each type putting each object
105in this respective struct keyed by its name.
106This allows objects of different types to share the same name,
107just as is allowed by Kubernetes.
108To accomplish this, we tell `cue` to put each object in the configuration
109tree at the path with the "kind" as first element and "name" as second.
110
111```
112$ cue import ./... -p kube -l '"\(strings.ToCamel(kind))" "\(metadata.name)"' -f
113```
114
115The added `-l` flag defines the labels for each object, based on values from
116each object, using the usual CUE syntax for field labels.
117In this case, we use a camelcase variant of the `kind` field of each object and
118use the `name` field of the `metadata` section as the name for each object.
119We also added the `-f` flag to overwrite the few files that succeeded before.
120
121Let's see what happened:
122
123```
124$ tree . | head
125.
126└── services
127 ├── frontend
128 │ ├── bartender
129 │ │ ├── kube.cue
130 │ │ └── kube.yaml
131 │ ├── breaddispatcher
132 │ │ ├── kube.cue
133 │ │ └── kube.yaml
134...
135```
136
137Each of the YAML files is converted to corresponding CUE files.
138Comments of the YAML files are preserved.
139
140The result is not fully pleasing, though.
141Take a look at `mon/prometheus/configmap.cue`.
142
143```
144$ cat mon/prometheus/configmap.cue
145package kube
146
147apiVersion: "v1"
148kind: "ConfigMap"
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200149metadata: name: "prometheus"
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100150data: {
151 "alert.rules": """
152 groups:
153 - name: rules.yaml
154...
155```
156
157The configuration file still contains YAML embedded in a string value of one
158of the fields.
159The original YAML file might have looked like it was all structured data, but
160the majority of it was a string containing, hopefully, valid YAML.
161
162The `-R` option attempts to detect structured YAML or JSON strings embedded
163in the configuration files and then converts these recursively.
164
165```
166$ cue import ./... -p kube -l '"\(strings.ToCamel(kind))" "\(metadata.name)"' -f -R
167```
168
169Now the file looks like:
170
171```
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200172$ cat mon/prometheus/configmap.cue
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100173package kube
174
175import "encoding/yaml"
176
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200177configMap: prometheus: {
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100178 apiVersion: "v1"
179 kind: "ConfigMap"
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200180 metadata: name: "prometheus"
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100181 data: {
182 "alert.rules": yaml.Marshal(_cue_alert_rules)
183 _cue_alert_rules: {
184 groups: [{
185...
186```
187
188That looks better!
189The resulting configuration file replaces the original embedded string
Marko Mikulicicc3d0b482019-07-09 13:58:21 +0200190with a call to `yaml.Marshal` converting a structured CUE source to
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100191a string with an equivalent YAML file.
192Fields starting with an underscore (`_`) are not included when emitting
193a configuration file (they are when enclosed in double quotes).
194
195```
196$ cue eval ./mon/prometheus -e configMap.prometheus
197apiVersion: "v1"
198kind: "ConfigMap"
199metadata: {
200 name: "prometheus"
201}
202data: {
203 "alert.rules": """
204 groups:
205 - name: rules.yaml
206...
207```
208
209Yay!
210
211
212## Quick 'n Dirty Conversion
213
214In this tutorial we show how to quickly eliminate boilerplate from a set
215of configurations.
216Manual tailoring will usually give better results, but takes considerably
217more thought, while taking the quick and dirty approach gets you mostly there.
Marcel van Lohuizen50a01f32018-12-20 21:47:59 +0100218The result of such a quick conversion also forms a good basis for
219a more thoughtful manual optimization.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100220
221### Create top-level template
222
223Now we have imported the YAML files we can start the simplification process.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100224
225Before we start the restructuring, lets save a full evaluation so that we
226can verify that simplifications yield the same results.
227
228```
Marcel van Lohuizendb4e4d22019-04-18 08:43:57 +0200229$ cue eval -c ./... > snapshot
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100230```
231
Marcel van Lohuizendb4e4d22019-04-18 08:43:57 +0200232The `-c` option tells `cue` that only concrete values, that is valid JSON,
233are allowed.
Marcel van Lohuizen50a01f32018-12-20 21:47:59 +0100234We focus on the objects defined in the various `kube.cue` files.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100235A quick inspection reveals that many of the Deployments and Services share
236common structure.
237
238We copy one of the files containing both as a basis for creating our template
239to the root of the directory tree.
240
241```
242$ cp frontend/breaddispatcher/kube.cue .
243```
244
245Modify this file as below.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100246
247```
248$ cat <<EOF > kube.cue
249package kube
250
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200251service: [ID=_]: {
Marko Mikulicicc3d0b482019-07-09 13:58:21 +0200252 apiVersion: "v1"
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100253 kind: "Service"
254 metadata: {
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200255 name: ID
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100256 labels: {
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200257 app: ID // by convention
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100258 domain: "prod" // always the same in the given files
259 component: string // varies per directory
260 }
261 }
262 spec: {
263 // Any port has the following properties.
264 ports: [...{
265 port: int
Marcel van Lohuizene5d8d092019-01-30 15:58:07 +0100266 protocol: *"TCP" | "UDP" // from the Kubernetes definition
267 name: string | *"client"
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100268 }]
269 selector: metadata.labels // we want those to be the same
270 }
271}
272
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200273deployment: [ID=_]: {
Marcel van Lohuizen8f9ef312019-12-01 17:07:59 +0100274 apiVersion: "apps/v1"
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100275 kind: "Deployment"
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200276 metadata name: ID
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100277 spec: {
278 // 1 is the default, but we allow any number
Marcel van Lohuizendb4e4d22019-04-18 08:43:57 +0200279 replicas: *1 | int
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100280 template: {
281 metadata labels: {
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200282 app: ID
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100283 domain: "prod"
284 component: string
285 }
286 // we always have one namesake container
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200287 spec containers: [{ name: ID }]
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100288 }
289 }
290}
291EOF
292```
293
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200294By replacing the service and deployment name with `[ID=_]` we have changed the
295definition into a template matching any field.
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200296CUE bind the field name to `ID` as a result.
Tarun Gupta Akiralabb2b6512019-06-03 13:24:12 +0000297During importing we used `metadata.name` as a key for the object names,
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200298so we can now set this field to `ID`.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100299
Marcel van Lohuizen50a01f32018-12-20 21:47:59 +0100300Templates are applied to (are unified with) all entries in the struct in which
301they are defined,
302so we need to either strip fields specific to the `breaddispatcher` definition,
303generalize them, or remove them.
304
305One of the labels defined in the Kubernetes metadata seems to be always set
306to parent directory name.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100307We enforce this by defining `component: string`, meaning that a field
Marcel van Lohuizen50a01f32018-12-20 21:47:59 +0100308of name `component` must be set to some string value, and then define this
309later on.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100310Any underspecified field results in an error when converting to, for instance,
311JSON.
312So a deployment or service will only be valid if this label is defined.
313
314<!-- TODO: once cycles in disjunctions are implemented
315 port: targetPort | int // by default the same as targetPort
316 targetPort: port | int // by default the same as port
317
318Note that ports definition for service contains a cycle.
319Specifying one of the ports will break the cycle.
320The meaning of cycles are well-defined in CUE.
321In practice this means that a template writer does not have to make any
322assumptions about which of the fields that can be mutually derived from each
323other a user of the template will want to specify.
324-->
325
326Let's compare the result of merging our new template to our original snapshot.
327
328```
Marcel van Lohuizendb4e4d22019-04-18 08:43:57 +0200329$ cue eval ./... -c > snapshot2
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100330--- ./mon/alertmanager
Marcel van Lohuizen2437f9d2019-10-29 12:22:38 +0100331service.alertmanager.metadata.labels.component: incomplete value (string):
332 ./kube.cue:11:24
333service.alertmanager.spec.selector.component: incomplete value (string):
334 ./kube.cue:11:24
335deployment.alertmanager.spec.template.metadata.labels.component: incomplete value (string):
336 ./kube.cue:36:28
337service."node-exporter".metadata.labels.component: incomplete value (string):
338 ./kube.cue:11:24
339...
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100340```
341
342<!-- TODO: better error messages -->
343
344Oops.
345The alert manager does not specify the `component` label.
346This demonstrates how constraints can be used to catch inconsistencies
347in your configurations.
348
349As there are very few objects that do not specify this label, we will modify
350the configurations to include them everywhere.
351We do this by setting a newly defined top-level field in each directory
352to the directory name and modify our master template file to use it.
353
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200354<!--
355```
356$ cue add */kube.cue -p kube --list <<EOF
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200357Component :: "{{.DisplayPath}}"
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200358EOF
359```
360-->
361
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100362```
363# set the component label to our new top-level field
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200364$ sed -i.bak 's/component:.*string/component: Component/' kube.cue && rm kube.cue.bak
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100365
366# add the new top-level field to our previous template definitions
367$ cat <<EOF >> kube.cue
368
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200369Component :: string
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100370EOF
371
372# add a file with the component label to each directory
373$ ls -d */ | sed 's/.$//' | xargs -I DIR sh -c 'cd DIR; echo "package kube
374
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200375Component :: \"DIR\"
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100376" > kube.cue; cd ..'
377
378# format the files
379$ cue fmt kube.cue */kube.cue
380```
381
382Let's try again to see if it is fixed:
383
384```
Marcel van Lohuizendb4e4d22019-04-18 08:43:57 +0200385$ cue eval -c ./... > snapshot2
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100386$ diff snapshot snapshot2
387...
388```
389
390Except for having more consistent labels and some reordering, nothing changed.
391We are happy and save the result as the new baseline.
392
393```
394$ cp snapshot2 snapshot
395```
396
397The corresponding boilerplate can now be removed with `cue trim`.
398
399```
400$ find . | grep kube.cue | xargs wc | tail -1
401 1792 3616 34815 total
402$ cue trim ./...
403$ find . | grep kube.cue | xargs wc | tail -1
404 1223 2374 22903 total
405```
406
407`cue trim` removes configuration from files that is already generated
408by templates or comprehensions.
409In doing so it removed over 500 lines of configuration, or over 30%!
410
411The following is proof that nothing changed semantically:
412
413```
414$ cue eval ./... > snapshot2
415$ diff snapshot snapshot2 | wc
416 0 0 0
417```
418
419We can do better, though.
420A first thing to note is that DaemonSets and StatefulSets share a similar
421structure to Deployments.
422We generalize the top-level template as follows:
423
424```
425$ cat <<EOF >> kube.cue
426
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200427daemonSet: [ID=_]: _spec & {
Marcel van Lohuizen8f9ef312019-12-01 17:07:59 +0100428 apiVersion: "apps/v1"
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100429 kind: "DaemonSet"
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200430 Name :: ID
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100431}
432
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200433statefulSet: [ID=_]: _spec & {
Marcel van Lohuizen8f9ef312019-12-01 17:07:59 +0100434 apiVersion: "apps/v1"
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100435 kind: "StatefulSet"
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200436 Name :: ID
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100437}
438
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200439deployment: [ID=_]: _spec & {
Marcel van Lohuizen8f9ef312019-12-01 17:07:59 +0100440 apiVersion: "apps/v1"
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100441 kind: "Deployment"
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200442 Name :: ID
Marcel van Lohuizene5d8d092019-01-30 15:58:07 +0100443 spec replicas: *1 | int
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100444}
445
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200446configMap: [ID=_]: {
447 metadata: name: ID
448 metadata: labels: component: Component
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100449}
450
451_spec: {
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200452 Name :: string
453
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200454 metadata: name: Name
455 metadata: labels: component: Component
456 spec: template: {
457 metadata: labels: {
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200458 app: Name
459 component: Component
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100460 domain: "prod"
461 }
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200462 spec: containers: [{name: Name}]
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100463 }
464}
465EOF
466$ cue fmt
467```
468
469The common configuration has been factored out into `_spec`.
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200470We introduced `Name` to aid both specifying and referring
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100471to the name of an object.
472For completeness, we added `configMap` as a top-level entry.
473
474Note that we have not yet removed the old definition of deployment.
475This is fine.
476As it is equivalent to the new one, unifying them will have no effect.
Emil Hessman66ec9592019-07-14 17:58:27 +0200477We leave its removal as an exercise to the reader.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100478
479Next we observe that all deployments, stateful sets and daemon sets have
480an accompanying service which shares many of the same fields.
481We add:
482
483```
484$ cat <<EOF >> kube.cue
485
486// Define the _export option and set the default to true
487// for all ports defined in all containers.
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200488_spec: spec: template: spec: containers: [...{
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100489 ports: [...{
Marcel van Lohuizene5d8d092019-01-30 15:58:07 +0100490 _export: *true | false // include the port in the service
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100491 }]
492}]
493
Marcel van Lohuizen9af9a902019-09-07 20:30:10 +0200494for x in [deployment, daemonSet, statefulSet] for k, v in x {
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200495 service: "\(k)": {
496 spec: selector: v.spec.template.metadata.labels
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100497
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200498 spec: ports: [ {
Marcel van Lohuizen9af9a902019-09-07 20:30:10 +0200499 Port = p.containerPort // Port is an alias
500 port: *Port | int
501 targetPort: *Port | int
502 } for c in v.spec.template.spec.containers
503 for p in c.ports
504 if p._export ]
Marcel van Lohuizen9af9a902019-09-07 20:30:10 +0200505 }
506}
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100507EOF
508$ cue fmt
509```
510
511This example introduces a few new concepts.
Emil Hessman66ec9592019-07-14 17:58:27 +0200512Open-ended lists are indicated with an ellipsis (`...`).
513The value following an ellipsis is unified with any subsequent elements and
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100514defines the "type", or template, for additional list elements.
515
516The `Port` declaration is an alias.
517Aliases are only visible in their lexical scope and are not part of the model.
518They can be used to make shadowed fields visible within nested scopes or,
519in this case, to reduce boilerplate without introducing new fields.
520
521Finally, this example introduces list and field comprehensions.
Emil Hessman66ec9592019-07-14 17:58:27 +0200522List comprehensions are analogous to list comprehensions found in other
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100523languages.
524Field comprehensions allow inserting fields in structs.
525In this case, the field comprehension adds a namesake service for any
526deployment, daemonSet, and statefulSet.
527Field comprehensions can also be used to add a field conditionally.
528
529
530Specifying the `targetPort` is not necessary, but since many files define it,
Emil Hessman66ec9592019-07-14 17:58:27 +0200531defining it here will allow those definitions to be removed
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100532using `cue trim`.
533We add an option `_export` for ports defined in containers to specify whether
534to include them in the service and explicitly set this to false
535for the respective ports in `infra/events`, `infra/tasks`, and `infra/watcher`.
536
537For the purpose of this tutorial, here are some quick patches:
538```
539$ cat <<EOF >> infra/events/kube.cue
540
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200541deployment: events: spec: template: spec: containers: [{ ports: [{_export: false}, _] }]
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100542EOF
543
544$ cat <<EOF >> infra/tasks/kube.cue
545
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200546deployment: tasks: spec: template: spec: containers: [{ ports: [{_export: false}, _] }]
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100547EOF
548
549$ cat <<EOF >> infra/watcher/kube.cue
550
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200551deployment: watcher: spec: template: spec: containers: [{ ports: [{_export: false}, _] }]
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100552EOF
553```
554In practice it would be more proper form to add this field in the original
555port declaration.
556
557We verify that all changes are acceptable and store another snapshot.
558Then we run trim to further reduce our configuration:
559
560```
561$ cue trim ./...
562$ find . | grep kube.cue | xargs wc | tail -1
563 1129 2270 22073 total
564```
Emil Hessman66ec9592019-07-14 17:58:27 +0200565This is after removing the rewritten and now redundant deployment definition.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100566
567We shaved off almost another 100 lines, even after adding the template.
568You can verify that the service definitions are now gone in most of the files.
569What remains is either some additional configuration, or inconsistencies that
570should probably be cleaned up.
571
Emil Hessman66ec9592019-07-14 17:58:27 +0200572But we have another trick up our sleeve.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100573With the `-s` or `--simplify` option we can tell `trim` or `fmt` to collapse
574structs with a single element onto a single line. For instance:
575
576```
577$ head frontend/breaddispatcher/kube.cue
578package kube
579
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200580deployment: breaddispatcher: {
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100581 spec: {
582 template: {
583 metadata: {
584 annotations: {
585 "prometheus.io.scrape": "true"
586 "prometheus.io.port": "7080"
587 }
588$ cue trim ./... -s
589$ head -7 frontend/breaddispatcher/kube.cue
590package kube
591
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200592deployment: breaddispatcher: spec: template: {
593 metadata: annotations: {
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100594 "prometheus.io.scrape": "true"
595 "prometheus.io.port": "7080"
596 }
597$ find . | grep kube.cue | xargs wc | tail -1
598 975 2116 20264 total
599```
600
601Another 150 lines lost!
602Collapsing lines like this can improve the readability of a configuration
603by removing considerable amounts of punctuation.
604
605
606### Repeat for several subdirectories
607
608In the previous section we defined templates for services and deployments
609in the root of our directory structure to capture the common traits of all
610services and deployments.
611In addition, we defined a directory-specific label.
612In this section we will look into generalizing the objects per directory.
613
614
Marcel van Lohuizen1e0fe9c2018-12-21 00:17:06 +0100615#### Directory `frontend`
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100616
617We observe that all deployments in subdirectories of `frontend`
618have a single container with one port,
619which is usually `7080`, but sometimes `8080`.
620Also, most have two prometheus-related annotations, while some have one.
621We leave the inconsistencies in ports, but add both annotations
622unconditionally.
623
624```
625$ cat <<EOF >> frontend/kube.cue
626
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200627deployment: [string]: spec: template: {
628 metadata: annotations: {
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100629 "prometheus.io.scrape": "true"
630 "prometheus.io.port": "\(spec.containers[0].ports[0].containerPort)"
631 }
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200632 spec: containers: [{
Marcel van Lohuizene5d8d092019-01-30 15:58:07 +0100633 ports: [{containerPort: *7080 | int}] // 7080 is the default
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100634 }]
635}
636EOF
637$ cue fmt ./frontend
638
639# check differences
640$ cue eval ./... > snapshot2
641$ diff snapshot snapshot2
642368a369
643> prometheus.io.port: "7080"
644577a579
645> prometheus.io.port: "8080"
646$ cp snapshot2 snapshot
647```
648
649Two lines with annotations added, improving consistency.
650
651```
652$ cue trim -s ./frontend/...
653$ find . | grep kube.cue | xargs wc | tail -1
654 931 2052 19624 total
655```
656
Emil Hessman66ec9592019-07-14 17:58:27 +0200657Another 40 lines removed.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100658We may have gotten used to larger reductions, but at this point there is just
659not much left to remove: in some of the frontend files there are only 4 lines
Emil Hessman66ec9592019-07-14 17:58:27 +0200660of configuration left.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100661
662
663#### Directory `kitchen`
664
665In this directory we observe that all deployments have without exception
666one container with port `8080`, all have the same liveness probe,
667a single line of prometheus annotation, and most have
668two or three disks with similar patterns.
669
670Let's add everything but the disks for now:
671
672```
673$ cat <<EOF >> kitchen/kube.cue
674
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200675deployment: [string]: spec: template: {
676 metadata: annotations: "prometheus.io.scrape": "true"
677 spec: containers: [{
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100678 ports: [{
679 containerPort: 8080
680 }]
681 livenessProbe: {
682 httpGet: {
683 path: "/debug/health"
684 port: 8080
685 }
686 initialDelaySeconds: 40
687 periodSeconds: 3
688 }
689 }]
690}
691EOF
692$ cue fmt ./kitchen
693```
694
695A diff reveals that one prometheus annotation was added to a service.
696We assume this to be an accidental omission and accept the differences
697
698Disks need to be defined in both the template spec section as well as in
699the container where they are used.
700We prefer to keep these two definitions together.
701We take the volumes definition from `expiditer` (the first config in that
702directory with two disks), and generalize it:
703
704```
705$ cat <<EOF >> kitchen/kube.cue
706
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200707deployment: [ID=_]: spec: template: spec: {
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200708 hasDisks :: *true | bool
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100709
Marcel van Lohuizen9af9a902019-09-07 20:30:10 +0200710 // field comprehension using just "if"
711 if hasDisks {
712 volumes: [{
713 name: *"\(ID)-disk" | string
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200714 gcePersistentDisk: pdName: *"\(ID)-disk" | string
715 gcePersistentDisk: fsType: "ext4"
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100716 }, {
Marcel van Lohuizen9af9a902019-09-07 20:30:10 +0200717 name: *"secret-\(ID)" | string
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200718 secret: secretName: *"\(ID)-secrets" | string
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100719 }, ...]
Marcel van Lohuizen9af9a902019-09-07 20:30:10 +0200720
721 containers: [{
722 volumeMounts: [{
723 name: *"\(ID)-disk" | string
724 mountPath: *"/logs" | string
725 }, {
726 mountPath: *"/etc/certs" | string
727 name: *"secret-\(ID)" | string
728 readOnly: true
729 }, ...]
730 }]
731 }
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100732}
733EOF
734
735$ cat <<EOF >> kitchen/souschef/kube.cue
736
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200737deployment: souschef: spec: template: spec: {
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200738 hasDisks :: false
739}
740
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100741EOF
742$ cue fmt ./kitchen/...
743```
744
745This template definition is not ideal: the definitions are positional, so if
746configurations were to define the disks in a different order, there would be
747no reuse or even conflicts.
748Also note that in order to deal with this restriction, almost all field values
Emil Hessman66ec9592019-07-14 17:58:27 +0200749are just default values and can be overridden by instances.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100750A better way would be define a map of volumes,
751similarly to how we organized the top-level Kubernetes objects,
752and then generate these two sections from this map.
753This requires some design, though, and does not belong in a
754"quick-and-dirty" tutorial.
755Later in this document we introduce a manually optimized configuration.
756
757We add the two disk by default and define a `_hasDisks` option to opt out.
758The `souschef` configuration is the only one that defines no disks.
759
760```
761$ cue trim -s ./kitchen/...
762
763# check differences
764$ cue eval ./... > snapshot2
765$ diff snapshot snapshot2
766...
767$ cp snapshot2 snapshot
768$ find . | grep kube.cue | xargs wc | tail -1
769 807 1862 17190 total
770```
771
772The diff shows that we added the `_hadDisks` option, but otherwise reveals no
773differences.
774We also reduced the configuration by a sizeable amount once more.
775
776However, on closer inspection of the remaining files we see a lot of remaining
777fields in the disk specifications as a result of inconsistent naming.
Emil Hessman66ec9592019-07-14 17:58:27 +0200778Reducing configurations like we did in this exercise exposes inconsistencies.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100779The inconsistencies can be removed by simply deleting the overrides in the
780specific configuration.
781Leaving them as is gives a clear signal that a configuration is inconsistent.
782
783
784### Conclusion of Quick 'n Dirty tutorial
785
786There is still some gain to be made with the other directories.
Emil Hessman66ec9592019-07-14 17:58:27 +0200787At nearly a 1000-line, or 55%, reduction, we leave the rest as an exercise to
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100788the reader.
789
790We have shown how CUE can be used to reduce boilerplate, enforce consistencies,
791and detect inconsistencies.
792Being able to deal with consistencies and inconsistencies is a consequence of
793the constraint-based model and harder to do with inheritance-based languages.
794
795We have indirectly also shown how CUE is well-suited for machine manipulation.
796This is a factor of syntax and the order independence that follows from its
797semantics.
798The `trim` command is one of many possible automated refactor tools made
799possible by this property.
800Also this would be harder to do with inheritance-based configuration languages.
801
802
803## Define commands
804
805The `cue export` command can be used to convert the created configuration back
806to JSON.
807In our case, this requires a top-level "emit value"
808to convert our mapped Kubernetes objects back to a list.
809Typically, this output is piped to tools like `kubectl` or `etcdctl`.
810
811In practice this means typing the same commands ad nauseam.
812The next step is often to write wrapper tools.
813But as there is often no one-size-fits-all solution, this lead to the
814proliferation of marginally useful tools.
815The `cue` tool provides an alternative by allowing the declaration of
816frequently used commands in CUE itself.
817Advantages:
818
819- added domain knowledge that CUE may use for improved analysis,
820- only one language to learn,
821- easy discovery of commands,
822- no further configuration required,
823- enforce uniform CLI standards across commands,
824- standardized commands across an organization.
825
826Commands are defined in files ending with `_tool.cue` in the same package as
827where the configuration files are defined on which the commands should operate.
828Top-level values in the configuration are visible by the tool files
829as long as they are not shadowed by top-level fields in the tool files.
830Top-level fields in the tool files are not visible in the configuration files
831and are not part of any model.
832
833The tool definitions also have access to additional builtin packages.
834A CUE configuration is fully hermetic, disallowing any outside influence.
835This property enables automated analysis and manipulation
836such as the `trim` command.
837The tool definitions, however, have access to such things as command line flags
838and environment variables, random generators, file listings, and so on.
839
840We define the following tools for our example:
841
842- ls: list the Kubernetes objects defined in our configuration
843- dump: dump all selected objects as a YAML stream
844- create: send all selected objects to `kubectl` for creation
845
846### Preparations
847
848To work with Kubernetes we need to convert our map of Kubernetes objects
849back to a simple list.
850We create the tool file to do just that.
851
852```
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200853$ cat <<EOF > kube_tool.cue
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100854package kube
855
856objects: [ x for v in objectSets for x in v ]
857
858objectSets: [
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200859 service,
860 deployment,
861 statefulSet,
862 daemonSet,
863 configMap,
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100864]
865EOF
866```
867
868### Listing objects
869
870Commands are defined in the `command` section at the top-level of a tool file.
871A `cue` command defines command line flags, environment variables, as well as
872a set of tasks.
873Examples tasks are load or write a file, dump something to the console,
874download a web page, or execute a command.
875
876We start by defining the `ls` command which dumps all our objects
877
878```
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200879$ cat <<EOF > ls_tool.cue
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100880package kube
881
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200882import (
883 "text/tabwriter"
884 "tool/cli"
885 "tool/file"
886)
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100887
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200888command: ls: {
889 task: print: cli.Print & {
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200890 text: tabwriter.Write([
891 "\(x.kind) \t\(x.metadata.labels.component) \t\(x.metadata.name)"
892 for x in objects
893 ])
894 }
895
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200896 task: write: file.Create & {
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200897 filename: "foo.txt"
898 contents: task.print.text
899 }
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100900}
901EOF
902```
903<!-- TODO: use "let" once implemented-->
904
905NOTE: THE API OF THE TASK DEFINITIONS WILL CHANGE.
906Although we may keep supporting this form if needed.
907
908The command is now available in the `cue` tool:
909
910```
911$ cue cmd ls ./frontend/maitred
912Service frontend maitred
913Deployment frontend maitred
914```
915
916As long as the name does not conflict with an existing command it can be
917used as a top-level command as well:
918```
919$ cue ls ./frontend/maitred
920...
921```
922
923If more than one instance is selected the `cue` tool may either operate
924on them one by one or merge them.
925The default is to merge them.
Marcel van Lohuizen09491352018-12-20 20:20:54 +0100926Different instances of a package are typically not compatible:
927different subdirectories may have different specializations.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100928A merge pre-expands templates of each instance and then merges their root
929values.
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200930The result may contain conflicts, such as our top-level `Component` field,
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100931but our per-type maps of Kubernetes objects should be free of conflict
932(if there is, we have a problem with Kubernetes down the line).
Emil Hessman66ec9592019-07-14 17:58:27 +0200933A merge thus gives us a unified view of all objects.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100934
935```
936$ cue ls ./...
Marcel van Lohuizen2437f9d2019-10-29 12:22:38 +0100937Service infra tasks
938Service frontend bartender
939Service frontend breaddispatcher
940Service frontend host
941Service frontend maitred
942Service frontend valeter
943Service frontend waiter
944Service frontend waterdispatcher
945Service infra download
946Service infra etcd
947Service infra events
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100948
949...
950
Marcel van Lohuizen2437f9d2019-10-29 12:22:38 +0100951Deployment proxy nginx
952StatefulSet infra etcd
953DaemonSet mon node-exporter
954ConfigMap mon alertmanager
955ConfigMap mon prometheus
956ConfigMap proxy authproxy
957ConfigMap proxy nginx
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100958```
959
960### Dumping a YAML Stream
961
962The following adds a command to dump the selected objects as a YAML stream.
963
964<!--
965TODO: add command line flags to filter object types.
966-->
967```
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200968$ cat <<EOF > dump_tool.cue
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100969package kube
970
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200971import (
972 "encoding/yaml"
973 "tool/cli"
974)
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100975
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200976command: dump: {
977 task: print: cli.Print & {
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200978 text: yaml.MarshalStream(objects)
979 }
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100980}
981EOF
982```
983
984<!--
985TODO: with new API as well as conversions implemented
986command dump task print: cli.Print(text: yaml.MarshalStream(objects))
987
988or without conversions:
989command dump task print: cli.Print & {text: yaml.MarshalStream(objects)}
990-->
991
992The `MarshalStream` command converts the list of objects to a '`---`'-separated
993stream of YAML values.
994
995
996### Creating Objects
997
998The `create` command sends a list of objects to `kubectl create`.
999
1000```
1001$ cat <<EOF > create_tool.cue
1002package kube
1003
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +02001004import (
1005 "encoding/yaml"
1006 "tool/exec"
1007 "tool/cli"
1008)
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001009
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001010command: create: {
1011 task: kube: exec.Run & {
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +02001012 cmd: "kubectl create --dry-run -f -"
1013 stdin: yaml.MarshalStream(objects)
1014 stdout: string
1015 }
1016
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001017 task: display: cli.Print & {
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +02001018 text: task.kube.stdout
1019 }
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001020}
1021EOF
1022```
1023
1024This command has two tasks, named `kube` and `display`.
1025The `display` task depends on the output of the `kube` task.
1026The `cue` tool does a static analysis of the dependencies and runs all
Emil Hessman66ec9592019-07-14 17:58:27 +02001027tasks which dependencies are satisfied in parallel while blocking tasks
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001028for which an input is missing.
1029
1030```
1031$ cue create ./frontend/...
1032service "bartender" created (dry run)
1033service "breaddispatcher" created (dry run)
1034service "host" created (dry run)
1035service "maitred" created (dry run)
1036service "valeter" created (dry run)
1037service "waiter" created (dry run)
1038service "waterdispatcher" created (dry run)
Marcel van Lohuizen8f9ef312019-12-01 17:07:59 +01001039deployment.apps "bartender" created (dry run)
1040deployment.apps "breaddispatcher" created (dry run)
1041deployment.apps "host" created (dry run)
1042deployment.apps "maitred" created (dry run)
1043deployment.apps "valeter" created (dry run)
1044deployment.apps "waiter" created (dry run)
1045deployment.apps "waterdispatcher" created (dry run)
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001046```
1047
1048A production real-life version of this could should omit the `--dry-run` flag
1049of course.
1050
Joel Longtineb6544662019-06-11 16:59:12 +00001051### Extract CUE templates directly from Kubernetes Go source
1052
Florian Kleind7d28372019-11-19 05:14:54 -08001053In order for `cue get go` to generate the CUE templates from Go sources, you first need to have the sources locally:
1054
1055```
1056$ go get k8s.io/api/apps/v1
Florian Kleind7d28372019-11-19 05:14:54 -08001057```
1058
Joel Longtineb6544662019-06-11 16:59:12 +00001059```
1060$ cue get go k8s.io/api/core/v1
Marcel van Lohuizen8f9ef312019-12-01 17:07:59 +01001061$ cue get go k8s.io/api/apps/v1
Joel Longtineb6544662019-06-11 16:59:12 +00001062
1063```
1064
Marcel van Lohuizenb13155b2019-10-25 16:52:30 +02001065Now that we have the Kubernetes definitions in our module, we can import and use them:
Joel Longtineb6544662019-06-11 16:59:12 +00001066
1067```
1068$ cat <<EOF > k8s_defs.cue
1069package kube
1070
1071import (
1072 "k8s.io/api/core/v1"
Marcel van Lohuizen8f9ef312019-12-01 17:07:59 +01001073 apps_v1 "k8s.io/api/apps/v1"
Joel Longtineb6544662019-06-11 16:59:12 +00001074)
1075
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001076service: [string]: v1.Service
Marcel van Lohuizen8f9ef312019-12-01 17:07:59 +01001077deployment: [string]: apps_v1.Deployment
1078daemonSet: [string]: apps_v1.DaemonSet
1079statefulSet: [string]: apps_v1.StatefulSet
Joel Longtineb6544662019-06-11 16:59:12 +00001080EOF
1081```
1082
1083And, finally, we'll format again:
1084
1085```
1086cue fmt
1087```
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001088
1089## Manually tailored configuration
1090
1091In Section "Quick 'n Dirty" we showed how to quickly get going with CUE.
1092With a bit more deliberation, one can reduce configurations even further.
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001093Also, we would like to define a configuration that is more generic and less tied
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001094to Kubernetes.
1095
1096We will rely heavily on CUEs order independence, which makes it easy to
1097combine two configurations of the same object in a well-defined way.
1098This makes it easy, for instance, to put frequently used fields in one file
1099and more esoteric one in another and then combine them without fear that one
1100will override the other.
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001101We will take this approach in this section.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001102
1103The end result of this tutorial is in the `manual` directory.
1104In the next sections we will show how to get there.
1105
1106
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001107### Outline
1108
Emil Hessman66ec9592019-07-14 17:58:27 +02001109The basic premise of our configuration is to maintain two configurations,
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001110a simple and abstract one, and one compatible with Kubernetes.
1111The Kubernetes version is automatically generated from the simple configuration.
1112Each simplified object has a `kubernetes` section that get gets merged into
1113the Kubernetes object upon conversion.
1114
1115We define one top-level file with our generic definitions.
1116
1117```
1118// file cloud.cue
1119package cloud
1120
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001121service: [Name=_]: {
Roger Peppe1ce0c512019-09-24 15:29:39 +01001122 name: *Name | string // the name of the service
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001123
1124 ...
1125
1126 // Kubernetes-specific options that get mixed in when converting
1127 // to Kubernetes.
1128 kubernetes: {
1129 }
1130}
1131
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001132deployment: [Name=_]: {
Roger Peppe1ce0c512019-09-24 15:29:39 +01001133 name: *Name | string
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001134 ...
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001135}
1136```
1137
1138A Kubernetes-specific file then contains the definitions to
1139convert the generic objects to Kubernetes.
1140
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001141Overall, the code modeling our services and the code generating the kubernetes
1142code is separated, while still allowing to inject Kubernetes-specific
1143data into our general model.
1144At the same time, we can add additional information to our model without
Emil Hessman66ec9592019-07-14 17:58:27 +02001145it ending up in the Kubernetes definitions causing it to barf.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001146
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001147
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001148### Deployment Definition
1149
1150For our design we assume that all Kubernetes Pod derivatives only define one
1151container.
1152This is clearly not the case in general, but often it does and it is good
1153practice.
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001154Conveniently, it simplifies our model as well.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001155
1156We base the model loosely on the master templates we derived in
1157Section "Quick 'n Dirty".
1158The first step we took is to eliminate `statefulSet` and `daemonSet` and
1159rather just have a `deployment` allowing different kinds.
1160
1161```
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001162deployment: [Name=_]: _base & {
Roger Peppe1ce0c512019-09-24 15:29:39 +01001163 name: *Name | string
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001164 ...
1165```
1166
1167The kind only needs to be specified if the deployment is a stateful set or
1168daemonset.
1169This also eliminates the need for `_spec`.
1170
1171The next step is to pull common fields, such as `image` to the top level.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001172
Emil Hessman66ec9592019-07-14 17:58:27 +02001173Arguments can be specified as a map.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001174```
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001175 arg: [string]: string
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001176 args: [ "-\(k)=\(v)" for k, v in arg ] | [...string]
1177```
1178
1179If order matters, users could explicitly specify the list as well.
1180
1181For ports we define two simple maps from name to port number:
1182
1183```
Marcel van Lohuizen1e0fe9c2018-12-21 00:17:06 +01001184 // expose port defines named ports that is exposed in the service
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001185 expose: port: [string]: int
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001186
Marcel van Lohuizen1e0fe9c2018-12-21 00:17:06 +01001187 // port defines a named port that is not exposed in the service.
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001188 port: [string]: int
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001189```
1190Both maps get defined in the container definition, but only `port` gets
1191included in the service definition.
1192This may not be the best model, and does not support all features,
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001193but it shows how one can chose a different representation.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001194
1195A similar story holds for environment variables.
1196In most cases mapping strings to string suffices.
1197The testdata uses other options though.
1198We define a simple `env` map and an `envSpec` for more elaborate cases:
1199
1200```
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001201 env: [string]: string
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001202
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001203 envSpec: [string]: {}
Marcel van Lohuizen2437f9d2019-10-29 12:22:38 +01001204 envSpec: {
1205 for k, v in env {
1206 "\(k)" value: v
1207 }
1208 }
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001209```
1210The simple map automatically gets mapped into the more elaborate map
1211which then presents the full picture.
1212
1213Finally, our assumption that there is one container per deployment allows us
1214to create a single definition for volumes, combining the information for
1215volume spec and volume mount.
1216
1217```
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001218 volume: [Name=_]: {
Marcel van Lohuizen2437f9d2019-10-29 12:22:38 +01001219 name: *Name | string
1220 mountPath: string
1221 subPath: null | string
1222 readOnly: bool
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001223 kubernetes: {}
1224 }
1225```
1226
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001227All other fields that we way want to define can go into a generic kubernetes
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001228struct that gets merged in with all other generated kubernetes data.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001229This even allows us to augment generated data, such as adding additional
1230fields to the container.
1231
1232
1233### Service Definition
1234
1235The service definition is straightforward.
1236As we eliminated stateful and daemon sets, the field comprehension to
1237automatically derive a service is now a bit simpler:
1238
1239```
1240// define services implied by deployments
Marcel van Lohuizen2437f9d2019-10-29 12:22:38 +01001241service: {
1242 for k, spec in deployment {
1243 "\(k)": {
1244 // Copy over all ports exposed from containers.
1245 for Name, Port in spec.expose.port {
1246 port: "\(Name)": {
1247 port: *Port | int
1248 targetPort: *Port | int
1249 }
1250 }
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001251
Marcel van Lohuizen2437f9d2019-10-29 12:22:38 +01001252 // Copy over the labels
1253 label: spec.label
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001254 }
1255 }
Marcel van Lohuizen2437f9d2019-10-29 12:22:38 +01001256}
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001257```
1258
Marcel van Lohuizen50a01f32018-12-20 21:47:59 +01001259The complete top-level model definitions can be found at
1260[doc/tutorial/kubernetes/manual/services/cloud.cue](https://cue.googlesource.com/cue/+/master/doc/tutorial/kubernetes/manual/services/cloud.cue).
1261
1262The tailorings for this specific project (the labels) are defined
1263[here](https://cue.googlesource.com/cue/+/master/doc/tutorial/kubernetes/manual/services/kube.cue).
1264
1265
1266### Converting to Kubernetes
1267
1268Converting services is fairly straightforward.
1269
1270```
Marcel van Lohuizen2437f9d2019-10-29 12:22:38 +01001271kubernetes: services: {
Marcel van Lohuizen9af9a902019-09-07 20:30:10 +02001272 for k, x in service {
1273 "\(k)": x.kubernetes & {
1274 apiVersion: "v1"
1275 kind: "Service"
Marcel van Lohuizen50a01f32018-12-20 21:47:59 +01001276
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001277 metadata: name: x.name
1278 metadata: labels: x.label
1279 spec: selector: x.label
Marcel van Lohuizen50a01f32018-12-20 21:47:59 +01001280
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001281 spec: ports: [ p for p in x.port ]
Marcel van Lohuizen9af9a902019-09-07 20:30:10 +02001282 }
1283 }
Marcel van Lohuizen50a01f32018-12-20 21:47:59 +01001284}
1285```
1286
1287We add the Kubernetes boilerplate, map the top-level fields and mix in
1288the raw `kubernetes` fields for each service.
1289
1290Mapping deployments is a bit more involved, though analogous.
1291The complete definitions for Kubernetes conversions can be found at
1292[doc/tutorial/kubernetes/manual/services/k8s.cue](https://cue.googlesource.com/cue/+/master/doc/tutorial/kubernetes/manual/services/k8s.cue).
1293
1294Converting the top-level definitions to concrete Kubernetes code is the hardest
1295part of this exercise.
1296That said, most CUE users will never have to resort to this level of CUE
1297to write configurations.
1298For instance, none of the files in the subdirectories contain comprehensions,
Emil Hessman66ec9592019-07-14 17:58:27 +02001299not even the template files in these directories (such as `kitchen/kube.cue`).
Marcel van Lohuizen1e0fe9c2018-12-21 00:17:06 +01001300Furthermore, none of the configuration files in any of the
1301leaf directories contain string interpolations.
Marcel van Lohuizen50a01f32018-12-20 21:47:59 +01001302
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001303
1304### Metrics
1305
1306The fully written out manual configuration can be found in the `manual`
1307subdirectory.
1308Running our usual count yields
1309```
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +02001310$ find . | grep kube.cue | xargs wc | tail -1
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001311 542 1190 11520 total
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001312```
1313This does not count our conversion templates.
1314Assuming that the top-level templates are reusable, and if we don't count them
Emil Hessman66ec9592019-07-14 17:58:27 +02001315for both approaches, the manual approach shaves off about another 150 lines.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001316If we count the templates as well, the two approaches are roughly equal.
1317
1318
1319### Conclusions Manual Configuration
1320
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001321We have shown that we can further compact a configuration by manually
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001322optimizing template files.
Emil Hessman66ec9592019-07-14 17:58:27 +02001323However, we have also shown that the manual optimization only gives
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001324a marginal benefit with respect to the quick-and-dirty semi-automatic reduction.
Emil Hessman66ec9592019-07-14 17:58:27 +02001325The benefits for the manual definition largely lies in the organizational
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001326flexibility one gets.
1327
1328Manually tailoring your configurations allows creating an abstraction layer
1329between logical definitions and Kubernetes-specific definitions.
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001330At the same time, CUE's order independence
Emil Hessman66ec9592019-07-14 17:58:27 +02001331makes it easy to mix in low-level Kubernetes configuration wherever it is
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001332convenient and applicable.
1333
1334Manual tailoring also allows us to add our own definitions without breaking
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001335Kubernetes.
1336This is crucial in defining information relevant to definitions,
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001337but unrelated to Kubernetes, where they belong.
1338
1339Separating abstract from concrete configuration also allows us to create
1340difference adaptors for the same configuration.
1341
1342
1343<!-- TODO:
1344## Conversion to `docker-compose`
Tarun Gupta Akiralabb2b6512019-06-03 13:24:12 +00001345-->