blob: 19c86df3f08edd3a3d0765fed58ba4fc3f39741b [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```
Marcel van Lohuizen275105a2019-12-06 21:32:41 +010037$ tree ./original | head
Marcel van Lohuizen02173f82018-12-20 13:27:07 +010038.
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```
Marcel van Lohuizenf8afaed2020-02-07 15:07:14 +0100112$ cue import ./... -p kube -l 'strings.ToCamel(kind)' -l metadata.name -f
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100113```
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
Marcel van Lohuizen39604732019-12-01 17:26:43 +0100165<-- TODO: update import label format -->
166
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100167```
Marcel van Lohuizenf8afaed2020-02-07 15:07:14 +0100168$ cue import ./... -p kube -l 'strings.ToCamel(kind)' -l metadata.name -f -R
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100169```
170
171Now the file looks like:
172
173```
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200174$ cat mon/prometheus/configmap.cue
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100175package kube
176
177import "encoding/yaml"
178
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200179configMap: prometheus: {
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100180 apiVersion: "v1"
181 kind: "ConfigMap"
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200182 metadata: name: "prometheus"
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100183 data: {
184 "alert.rules": yaml.Marshal(_cue_alert_rules)
185 _cue_alert_rules: {
186 groups: [{
187...
188```
189
190That looks better!
191The resulting configuration file replaces the original embedded string
Marko Mikulicicc3d0b482019-07-09 13:58:21 +0200192with a call to `yaml.Marshal` converting a structured CUE source to
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100193a string with an equivalent YAML file.
194Fields starting with an underscore (`_`) are not included when emitting
195a configuration file (they are when enclosed in double quotes).
196
197```
198$ cue eval ./mon/prometheus -e configMap.prometheus
199apiVersion: "v1"
200kind: "ConfigMap"
201metadata: {
202 name: "prometheus"
203}
204data: {
205 "alert.rules": """
206 groups:
207 - name: rules.yaml
208...
209```
210
211Yay!
212
213
214## Quick 'n Dirty Conversion
215
216In this tutorial we show how to quickly eliminate boilerplate from a set
217of configurations.
218Manual tailoring will usually give better results, but takes considerably
219more thought, while taking the quick and dirty approach gets you mostly there.
Marcel van Lohuizen50a01f32018-12-20 21:47:59 +0100220The result of such a quick conversion also forms a good basis for
221a more thoughtful manual optimization.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100222
223### Create top-level template
224
225Now we have imported the YAML files we can start the simplification process.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100226
227Before we start the restructuring, lets save a full evaluation so that we
228can verify that simplifications yield the same results.
229
230```
Marcel van Lohuizendb4e4d22019-04-18 08:43:57 +0200231$ cue eval -c ./... > snapshot
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100232```
233
Marcel van Lohuizendb4e4d22019-04-18 08:43:57 +0200234The `-c` option tells `cue` that only concrete values, that is valid JSON,
235are allowed.
Marcel van Lohuizen50a01f32018-12-20 21:47:59 +0100236We focus on the objects defined in the various `kube.cue` files.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100237A quick inspection reveals that many of the Deployments and Services share
238common structure.
239
240We copy one of the files containing both as a basis for creating our template
241to the root of the directory tree.
242
243```
244$ cp frontend/breaddispatcher/kube.cue .
245```
246
247Modify this file as below.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100248
249```
250$ cat <<EOF > kube.cue
251package kube
252
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200253service: [ID=_]: {
Marko Mikulicicc3d0b482019-07-09 13:58:21 +0200254 apiVersion: "v1"
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100255 kind: "Service"
256 metadata: {
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200257 name: ID
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100258 labels: {
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200259 app: ID // by convention
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100260 domain: "prod" // always the same in the given files
261 component: string // varies per directory
262 }
263 }
264 spec: {
265 // Any port has the following properties.
266 ports: [...{
267 port: int
Marcel van Lohuizene5d8d092019-01-30 15:58:07 +0100268 protocol: *"TCP" | "UDP" // from the Kubernetes definition
269 name: string | *"client"
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100270 }]
271 selector: metadata.labels // we want those to be the same
272 }
273}
274
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200275deployment: [ID=_]: {
Marcel van Lohuizen8f9ef312019-12-01 17:07:59 +0100276 apiVersion: "apps/v1"
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100277 kind: "Deployment"
Marcel van Lohuizen39604732019-12-01 17:26:43 +0100278 metadata: name: ID
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100279 spec: {
280 // 1 is the default, but we allow any number
Marcel van Lohuizendb4e4d22019-04-18 08:43:57 +0200281 replicas: *1 | int
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100282 template: {
Marcel van Lohuizen39604732019-12-01 17:26:43 +0100283 metadata: labels: {
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200284 app: ID
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100285 domain: "prod"
286 component: string
287 }
288 // we always have one namesake container
Marcel van Lohuizen39604732019-12-01 17:26:43 +0100289 spec: containers: [{ name: ID }]
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100290 }
291 }
292}
293EOF
294```
295
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200296By replacing the service and deployment name with `[ID=_]` we have changed the
297definition into a template matching any field.
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200298CUE bind the field name to `ID` as a result.
Tarun Gupta Akiralabb2b6512019-06-03 13:24:12 +0000299During importing we used `metadata.name` as a key for the object names,
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200300so we can now set this field to `ID`.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100301
Marcel van Lohuizen50a01f32018-12-20 21:47:59 +0100302Templates are applied to (are unified with) all entries in the struct in which
303they are defined,
304so we need to either strip fields specific to the `breaddispatcher` definition,
305generalize them, or remove them.
306
307One of the labels defined in the Kubernetes metadata seems to be always set
308to parent directory name.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100309We enforce this by defining `component: string`, meaning that a field
Marcel van Lohuizen50a01f32018-12-20 21:47:59 +0100310of name `component` must be set to some string value, and then define this
311later on.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100312Any underspecified field results in an error when converting to, for instance,
313JSON.
314So a deployment or service will only be valid if this label is defined.
315
316<!-- TODO: once cycles in disjunctions are implemented
317 port: targetPort | int // by default the same as targetPort
318 targetPort: port | int // by default the same as port
319
320Note that ports definition for service contains a cycle.
321Specifying one of the ports will break the cycle.
322The meaning of cycles are well-defined in CUE.
323In practice this means that a template writer does not have to make any
324assumptions about which of the fields that can be mutually derived from each
325other a user of the template will want to specify.
326-->
327
328Let's compare the result of merging our new template to our original snapshot.
329
330```
Marcel van Lohuizendb4e4d22019-04-18 08:43:57 +0200331$ cue eval ./... -c > snapshot2
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100332--- ./mon/alertmanager
Marcel van Lohuizen2437f9d2019-10-29 12:22:38 +0100333service.alertmanager.metadata.labels.component: incomplete value (string):
334 ./kube.cue:11:24
335service.alertmanager.spec.selector.component: incomplete value (string):
336 ./kube.cue:11:24
337deployment.alertmanager.spec.template.metadata.labels.component: incomplete value (string):
338 ./kube.cue:36:28
339service."node-exporter".metadata.labels.component: incomplete value (string):
340 ./kube.cue:11:24
341...
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100342```
343
344<!-- TODO: better error messages -->
345
346Oops.
347The alert manager does not specify the `component` label.
348This demonstrates how constraints can be used to catch inconsistencies
349in your configurations.
350
351As there are very few objects that do not specify this label, we will modify
352the configurations to include them everywhere.
353We do this by setting a newly defined top-level field in each directory
354to the directory name and modify our master template file to use it.
355
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200356<!--
357```
358$ cue add */kube.cue -p kube --list <<EOF
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200359Component :: "{{.DisplayPath}}"
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200360EOF
361```
362-->
363
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100364```
365# set the component label to our new top-level field
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200366$ sed -i.bak 's/component:.*string/component: Component/' kube.cue && rm kube.cue.bak
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100367
368# add the new top-level field to our previous template definitions
369$ cat <<EOF >> kube.cue
370
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200371Component :: string
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100372EOF
373
374# add a file with the component label to each directory
375$ ls -d */ | sed 's/.$//' | xargs -I DIR sh -c 'cd DIR; echo "package kube
376
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200377Component :: \"DIR\"
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100378" > kube.cue; cd ..'
379
380# format the files
381$ cue fmt kube.cue */kube.cue
382```
383
384Let's try again to see if it is fixed:
385
386```
Marcel van Lohuizendb4e4d22019-04-18 08:43:57 +0200387$ cue eval -c ./... > snapshot2
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100388$ diff snapshot snapshot2
389...
390```
391
392Except for having more consistent labels and some reordering, nothing changed.
393We are happy and save the result as the new baseline.
394
395```
396$ cp snapshot2 snapshot
397```
398
399The corresponding boilerplate can now be removed with `cue trim`.
400
401```
402$ find . | grep kube.cue | xargs wc | tail -1
403 1792 3616 34815 total
404$ cue trim ./...
405$ find . | grep kube.cue | xargs wc | tail -1
406 1223 2374 22903 total
407```
408
409`cue trim` removes configuration from files that is already generated
410by templates or comprehensions.
411In doing so it removed over 500 lines of configuration, or over 30%!
412
413The following is proof that nothing changed semantically:
414
415```
Marcel van Lohuizen7e3bdda2020-03-06 18:28:55 +0100416$ cue eval -c ./... > snapshot2
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100417$ diff snapshot snapshot2 | wc
418 0 0 0
419```
420
421We can do better, though.
422A first thing to note is that DaemonSets and StatefulSets share a similar
423structure to Deployments.
424We generalize the top-level template as follows:
425
426```
427$ cat <<EOF >> kube.cue
428
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200429daemonSet: [ID=_]: _spec & {
Marcel van Lohuizen8f9ef312019-12-01 17:07:59 +0100430 apiVersion: "apps/v1"
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100431 kind: "DaemonSet"
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200432 Name :: ID
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100433}
434
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200435statefulSet: [ID=_]: _spec & {
Marcel van Lohuizen8f9ef312019-12-01 17:07:59 +0100436 apiVersion: "apps/v1"
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100437 kind: "StatefulSet"
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200438 Name :: ID
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100439}
440
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200441deployment: [ID=_]: _spec & {
Marcel van Lohuizen8f9ef312019-12-01 17:07:59 +0100442 apiVersion: "apps/v1"
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100443 kind: "Deployment"
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200444 Name :: ID
Marcel van Lohuizen39604732019-12-01 17:26:43 +0100445 spec: replicas: *1 | int
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100446}
447
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200448configMap: [ID=_]: {
449 metadata: name: ID
450 metadata: labels: component: Component
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100451}
452
453_spec: {
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200454 Name :: string
455
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200456 metadata: name: Name
457 metadata: labels: component: Component
Marcel van Lohuizen275105a2019-12-06 21:32:41 +0100458 spec: selector: {}
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200459 spec: template: {
460 metadata: labels: {
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200461 app: Name
462 component: Component
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100463 domain: "prod"
464 }
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200465 spec: containers: [{name: Name}]
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100466 }
467}
468EOF
469$ cue fmt
470```
471
472The common configuration has been factored out into `_spec`.
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200473We introduced `Name` to aid both specifying and referring
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100474to the name of an object.
475For completeness, we added `configMap` as a top-level entry.
476
477Note that we have not yet removed the old definition of deployment.
478This is fine.
479As it is equivalent to the new one, unifying them will have no effect.
Emil Hessman66ec9592019-07-14 17:58:27 +0200480We leave its removal as an exercise to the reader.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100481
482Next we observe that all deployments, stateful sets and daemon sets have
483an accompanying service which shares many of the same fields.
484We add:
485
486```
487$ cat <<EOF >> kube.cue
488
489// Define the _export option and set the default to true
490// for all ports defined in all containers.
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200491_spec: spec: template: spec: containers: [...{
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100492 ports: [...{
Marcel van Lohuizene5d8d092019-01-30 15:58:07 +0100493 _export: *true | false // include the port in the service
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100494 }]
495}]
496
Marcel van Lohuizen9af9a902019-09-07 20:30:10 +0200497for x in [deployment, daemonSet, statefulSet] for k, v in x {
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200498 service: "\(k)": {
499 spec: selector: v.spec.template.metadata.labels
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100500
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200501 spec: ports: [ {
Marcel van Lohuizen9af9a902019-09-07 20:30:10 +0200502 Port = p.containerPort // Port is an alias
503 port: *Port | int
504 targetPort: *Port | int
505 } for c in v.spec.template.spec.containers
506 for p in c.ports
507 if p._export ]
Marcel van Lohuizen9af9a902019-09-07 20:30:10 +0200508 }
509}
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100510EOF
511$ cue fmt
512```
513
514This example introduces a few new concepts.
Emil Hessman66ec9592019-07-14 17:58:27 +0200515Open-ended lists are indicated with an ellipsis (`...`).
516The value following an ellipsis is unified with any subsequent elements and
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100517defines the "type", or template, for additional list elements.
518
519The `Port` declaration is an alias.
520Aliases are only visible in their lexical scope and are not part of the model.
521They can be used to make shadowed fields visible within nested scopes or,
522in this case, to reduce boilerplate without introducing new fields.
523
524Finally, this example introduces list and field comprehensions.
Emil Hessman66ec9592019-07-14 17:58:27 +0200525List comprehensions are analogous to list comprehensions found in other
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100526languages.
527Field comprehensions allow inserting fields in structs.
528In this case, the field comprehension adds a namesake service for any
529deployment, daemonSet, and statefulSet.
530Field comprehensions can also be used to add a field conditionally.
531
532
533Specifying the `targetPort` is not necessary, but since many files define it,
Emil Hessman66ec9592019-07-14 17:58:27 +0200534defining it here will allow those definitions to be removed
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100535using `cue trim`.
536We add an option `_export` for ports defined in containers to specify whether
537to include them in the service and explicitly set this to false
538for the respective ports in `infra/events`, `infra/tasks`, and `infra/watcher`.
539
540For the purpose of this tutorial, here are some quick patches:
541```
542$ cat <<EOF >> infra/events/kube.cue
543
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200544deployment: events: spec: template: spec: containers: [{ ports: [{_export: false}, _] }]
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100545EOF
546
547$ cat <<EOF >> infra/tasks/kube.cue
548
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200549deployment: tasks: spec: template: spec: containers: [{ ports: [{_export: false}, _] }]
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100550EOF
551
552$ cat <<EOF >> infra/watcher/kube.cue
553
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200554deployment: watcher: spec: template: spec: containers: [{ ports: [{_export: false}, _] }]
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100555EOF
556```
557In practice it would be more proper form to add this field in the original
558port declaration.
559
560We verify that all changes are acceptable and store another snapshot.
561Then we run trim to further reduce our configuration:
562
563```
564$ cue trim ./...
565$ find . | grep kube.cue | xargs wc | tail -1
566 1129 2270 22073 total
567```
Emil Hessman66ec9592019-07-14 17:58:27 +0200568This is after removing the rewritten and now redundant deployment definition.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100569
570We shaved off almost another 100 lines, even after adding the template.
571You can verify that the service definitions are now gone in most of the files.
572What remains is either some additional configuration, or inconsistencies that
573should probably be cleaned up.
574
Emil Hessman66ec9592019-07-14 17:58:27 +0200575But we have another trick up our sleeve.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100576With the `-s` or `--simplify` option we can tell `trim` or `fmt` to collapse
577structs with a single element onto a single line. For instance:
578
579```
580$ head frontend/breaddispatcher/kube.cue
581package kube
582
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200583deployment: breaddispatcher: {
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100584 spec: {
585 template: {
586 metadata: {
587 annotations: {
588 "prometheus.io.scrape": "true"
589 "prometheus.io.port": "7080"
590 }
591$ cue trim ./... -s
592$ head -7 frontend/breaddispatcher/kube.cue
593package kube
594
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200595deployment: breaddispatcher: spec: template: {
596 metadata: annotations: {
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100597 "prometheus.io.scrape": "true"
598 "prometheus.io.port": "7080"
599 }
600$ find . | grep kube.cue | xargs wc | tail -1
601 975 2116 20264 total
602```
603
604Another 150 lines lost!
605Collapsing lines like this can improve the readability of a configuration
606by removing considerable amounts of punctuation.
607
608
609### Repeat for several subdirectories
610
611In the previous section we defined templates for services and deployments
612in the root of our directory structure to capture the common traits of all
613services and deployments.
614In addition, we defined a directory-specific label.
615In this section we will look into generalizing the objects per directory.
616
617
Marcel van Lohuizen1e0fe9c2018-12-21 00:17:06 +0100618#### Directory `frontend`
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100619
620We observe that all deployments in subdirectories of `frontend`
621have a single container with one port,
622which is usually `7080`, but sometimes `8080`.
623Also, most have two prometheus-related annotations, while some have one.
624We leave the inconsistencies in ports, but add both annotations
625unconditionally.
626
627```
628$ cat <<EOF >> frontend/kube.cue
629
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200630deployment: [string]: spec: template: {
631 metadata: annotations: {
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100632 "prometheus.io.scrape": "true"
633 "prometheus.io.port": "\(spec.containers[0].ports[0].containerPort)"
634 }
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200635 spec: containers: [{
Marcel van Lohuizene5d8d092019-01-30 15:58:07 +0100636 ports: [{containerPort: *7080 | int}] // 7080 is the default
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100637 }]
638}
639EOF
640$ cue fmt ./frontend
641
642# check differences
Marcel van Lohuizen7e3bdda2020-03-06 18:28:55 +0100643$ cue eval -c ./... > snapshot2
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100644$ diff snapshot snapshot2
645368a369
646> prometheus.io.port: "7080"
647577a579
648> prometheus.io.port: "8080"
649$ cp snapshot2 snapshot
650```
651
652Two lines with annotations added, improving consistency.
653
654```
655$ cue trim -s ./frontend/...
656$ find . | grep kube.cue | xargs wc | tail -1
657 931 2052 19624 total
658```
659
Emil Hessman66ec9592019-07-14 17:58:27 +0200660Another 40 lines removed.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100661We may have gotten used to larger reductions, but at this point there is just
662not much left to remove: in some of the frontend files there are only 4 lines
Emil Hessman66ec9592019-07-14 17:58:27 +0200663of configuration left.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100664
665
666#### Directory `kitchen`
667
668In this directory we observe that all deployments have without exception
669one container with port `8080`, all have the same liveness probe,
670a single line of prometheus annotation, and most have
671two or three disks with similar patterns.
672
673Let's add everything but the disks for now:
674
675```
676$ cat <<EOF >> kitchen/kube.cue
677
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200678deployment: [string]: spec: template: {
679 metadata: annotations: "prometheus.io.scrape": "true"
680 spec: containers: [{
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100681 ports: [{
682 containerPort: 8080
683 }]
684 livenessProbe: {
685 httpGet: {
686 path: "/debug/health"
687 port: 8080
688 }
689 initialDelaySeconds: 40
690 periodSeconds: 3
691 }
692 }]
693}
694EOF
695$ cue fmt ./kitchen
696```
697
698A diff reveals that one prometheus annotation was added to a service.
699We assume this to be an accidental omission and accept the differences
700
701Disks need to be defined in both the template spec section as well as in
702the container where they are used.
703We prefer to keep these two definitions together.
704We take the volumes definition from `expiditer` (the first config in that
705directory with two disks), and generalize it:
706
707```
708$ cat <<EOF >> kitchen/kube.cue
709
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200710deployment: [ID=_]: spec: template: spec: {
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200711 hasDisks :: *true | bool
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100712
Marcel van Lohuizen9af9a902019-09-07 20:30:10 +0200713 // field comprehension using just "if"
714 if hasDisks {
715 volumes: [{
716 name: *"\(ID)-disk" | string
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200717 gcePersistentDisk: pdName: *"\(ID)-disk" | string
718 gcePersistentDisk: fsType: "ext4"
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100719 }, {
Marcel van Lohuizen9af9a902019-09-07 20:30:10 +0200720 name: *"secret-\(ID)" | string
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200721 secret: secretName: *"\(ID)-secrets" | string
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100722 }, ...]
Marcel van Lohuizen9af9a902019-09-07 20:30:10 +0200723
724 containers: [{
725 volumeMounts: [{
726 name: *"\(ID)-disk" | string
727 mountPath: *"/logs" | string
728 }, {
729 mountPath: *"/etc/certs" | string
730 name: *"secret-\(ID)" | string
731 readOnly: true
732 }, ...]
733 }]
734 }
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100735}
736EOF
737
738$ cat <<EOF >> kitchen/souschef/kube.cue
739
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200740deployment: souschef: spec: template: spec: {
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200741 hasDisks :: false
742}
743
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100744EOF
745$ cue fmt ./kitchen/...
746```
747
748This template definition is not ideal: the definitions are positional, so if
749configurations were to define the disks in a different order, there would be
750no reuse or even conflicts.
751Also note that in order to deal with this restriction, almost all field values
Emil Hessman66ec9592019-07-14 17:58:27 +0200752are just default values and can be overridden by instances.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100753A better way would be define a map of volumes,
754similarly to how we organized the top-level Kubernetes objects,
755and then generate these two sections from this map.
756This requires some design, though, and does not belong in a
757"quick-and-dirty" tutorial.
758Later in this document we introduce a manually optimized configuration.
759
760We add the two disk by default and define a `_hasDisks` option to opt out.
761The `souschef` configuration is the only one that defines no disks.
762
763```
764$ cue trim -s ./kitchen/...
765
766# check differences
767$ cue eval ./... > snapshot2
768$ diff snapshot snapshot2
769...
770$ cp snapshot2 snapshot
771$ find . | grep kube.cue | xargs wc | tail -1
772 807 1862 17190 total
773```
774
775The diff shows that we added the `_hadDisks` option, but otherwise reveals no
776differences.
777We also reduced the configuration by a sizeable amount once more.
778
779However, on closer inspection of the remaining files we see a lot of remaining
780fields in the disk specifications as a result of inconsistent naming.
Emil Hessman66ec9592019-07-14 17:58:27 +0200781Reducing configurations like we did in this exercise exposes inconsistencies.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100782The inconsistencies can be removed by simply deleting the overrides in the
783specific configuration.
784Leaving them as is gives a clear signal that a configuration is inconsistent.
785
786
787### Conclusion of Quick 'n Dirty tutorial
788
789There is still some gain to be made with the other directories.
Emil Hessman66ec9592019-07-14 17:58:27 +0200790At nearly a 1000-line, or 55%, reduction, we leave the rest as an exercise to
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100791the reader.
792
793We have shown how CUE can be used to reduce boilerplate, enforce consistencies,
794and detect inconsistencies.
795Being able to deal with consistencies and inconsistencies is a consequence of
796the constraint-based model and harder to do with inheritance-based languages.
797
798We have indirectly also shown how CUE is well-suited for machine manipulation.
799This is a factor of syntax and the order independence that follows from its
800semantics.
801The `trim` command is one of many possible automated refactor tools made
802possible by this property.
803Also this would be harder to do with inheritance-based configuration languages.
804
805
806## Define commands
807
808The `cue export` command can be used to convert the created configuration back
809to JSON.
810In our case, this requires a top-level "emit value"
811to convert our mapped Kubernetes objects back to a list.
812Typically, this output is piped to tools like `kubectl` or `etcdctl`.
813
814In practice this means typing the same commands ad nauseam.
815The next step is often to write wrapper tools.
816But as there is often no one-size-fits-all solution, this lead to the
817proliferation of marginally useful tools.
818The `cue` tool provides an alternative by allowing the declaration of
819frequently used commands in CUE itself.
820Advantages:
821
822- added domain knowledge that CUE may use for improved analysis,
823- only one language to learn,
824- easy discovery of commands,
825- no further configuration required,
826- enforce uniform CLI standards across commands,
827- standardized commands across an organization.
828
829Commands are defined in files ending with `_tool.cue` in the same package as
830where the configuration files are defined on which the commands should operate.
831Top-level values in the configuration are visible by the tool files
832as long as they are not shadowed by top-level fields in the tool files.
833Top-level fields in the tool files are not visible in the configuration files
834and are not part of any model.
835
836The tool definitions also have access to additional builtin packages.
837A CUE configuration is fully hermetic, disallowing any outside influence.
838This property enables automated analysis and manipulation
839such as the `trim` command.
840The tool definitions, however, have access to such things as command line flags
841and environment variables, random generators, file listings, and so on.
842
843We define the following tools for our example:
844
845- ls: list the Kubernetes objects defined in our configuration
846- dump: dump all selected objects as a YAML stream
847- create: send all selected objects to `kubectl` for creation
848
849### Preparations
850
851To work with Kubernetes we need to convert our map of Kubernetes objects
852back to a simple list.
853We create the tool file to do just that.
854
855```
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200856$ cat <<EOF > kube_tool.cue
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100857package kube
858
859objects: [ x for v in objectSets for x in v ]
860
861objectSets: [
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200862 service,
863 deployment,
864 statefulSet,
865 daemonSet,
866 configMap,
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100867]
868EOF
869```
870
871### Listing objects
872
873Commands are defined in the `command` section at the top-level of a tool file.
874A `cue` command defines command line flags, environment variables, as well as
875a set of tasks.
876Examples tasks are load or write a file, dump something to the console,
877download a web page, or execute a command.
878
879We start by defining the `ls` command which dumps all our objects
880
881```
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200882$ cat <<EOF > ls_tool.cue
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100883package kube
884
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200885import (
886 "text/tabwriter"
887 "tool/cli"
888 "tool/file"
889)
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100890
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200891command: ls: {
892 task: print: cli.Print & {
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200893 text: tabwriter.Write([
894 "\(x.kind) \t\(x.metadata.labels.component) \t\(x.metadata.name)"
895 for x in objects
896 ])
897 }
898
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200899 task: write: file.Create & {
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200900 filename: "foo.txt"
901 contents: task.print.text
902 }
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100903}
904EOF
905```
906<!-- TODO: use "let" once implemented-->
907
908NOTE: THE API OF THE TASK DEFINITIONS WILL CHANGE.
909Although we may keep supporting this form if needed.
910
911The command is now available in the `cue` tool:
912
913```
914$ cue cmd ls ./frontend/maitred
915Service frontend maitred
916Deployment frontend maitred
917```
918
919As long as the name does not conflict with an existing command it can be
920used as a top-level command as well:
921```
922$ cue ls ./frontend/maitred
923...
924```
925
926If more than one instance is selected the `cue` tool may either operate
927on them one by one or merge them.
928The default is to merge them.
Marcel van Lohuizen09491352018-12-20 20:20:54 +0100929Different instances of a package are typically not compatible:
930different subdirectories may have different specializations.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100931A merge pre-expands templates of each instance and then merges their root
932values.
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200933The result may contain conflicts, such as our top-level `Component` field,
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100934but our per-type maps of Kubernetes objects should be free of conflict
935(if there is, we have a problem with Kubernetes down the line).
Emil Hessman66ec9592019-07-14 17:58:27 +0200936A merge thus gives us a unified view of all objects.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100937
938```
939$ cue ls ./...
Marcel van Lohuizen2437f9d2019-10-29 12:22:38 +0100940Service infra tasks
941Service frontend bartender
942Service frontend breaddispatcher
943Service frontend host
944Service frontend maitred
945Service frontend valeter
946Service frontend waiter
947Service frontend waterdispatcher
948Service infra download
949Service infra etcd
950Service infra events
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100951
952...
953
Marcel van Lohuizen2437f9d2019-10-29 12:22:38 +0100954Deployment proxy nginx
955StatefulSet infra etcd
956DaemonSet mon node-exporter
957ConfigMap mon alertmanager
958ConfigMap mon prometheus
959ConfigMap proxy authproxy
960ConfigMap proxy nginx
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100961```
962
963### Dumping a YAML Stream
964
965The following adds a command to dump the selected objects as a YAML stream.
966
967<!--
968TODO: add command line flags to filter object types.
969-->
970```
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200971$ cat <<EOF > dump_tool.cue
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100972package kube
973
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200974import (
975 "encoding/yaml"
976 "tool/cli"
977)
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100978
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200979command: dump: {
980 task: print: cli.Print & {
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200981 text: yaml.MarshalStream(objects)
982 }
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100983}
984EOF
985```
986
987<!--
988TODO: with new API as well as conversions implemented
989command dump task print: cli.Print(text: yaml.MarshalStream(objects))
990
991or without conversions:
992command dump task print: cli.Print & {text: yaml.MarshalStream(objects)}
993-->
994
995The `MarshalStream` command converts the list of objects to a '`---`'-separated
996stream of YAML values.
997
998
999### Creating Objects
1000
1001The `create` command sends a list of objects to `kubectl create`.
1002
1003```
1004$ cat <<EOF > create_tool.cue
1005package kube
1006
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +02001007import (
1008 "encoding/yaml"
1009 "tool/exec"
1010 "tool/cli"
1011)
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001012
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001013command: create: {
1014 task: kube: exec.Run & {
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +02001015 cmd: "kubectl create --dry-run -f -"
1016 stdin: yaml.MarshalStream(objects)
1017 stdout: string
1018 }
1019
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001020 task: display: cli.Print & {
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +02001021 text: task.kube.stdout
1022 }
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001023}
1024EOF
1025```
1026
1027This command has two tasks, named `kube` and `display`.
1028The `display` task depends on the output of the `kube` task.
1029The `cue` tool does a static analysis of the dependencies and runs all
Emil Hessman66ec9592019-07-14 17:58:27 +02001030tasks which dependencies are satisfied in parallel while blocking tasks
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001031for which an input is missing.
1032
1033```
1034$ cue create ./frontend/...
1035service "bartender" created (dry run)
1036service "breaddispatcher" created (dry run)
1037service "host" created (dry run)
1038service "maitred" created (dry run)
1039service "valeter" created (dry run)
1040service "waiter" created (dry run)
1041service "waterdispatcher" created (dry run)
Marcel van Lohuizen8f9ef312019-12-01 17:07:59 +01001042deployment.apps "bartender" created (dry run)
1043deployment.apps "breaddispatcher" created (dry run)
1044deployment.apps "host" created (dry run)
1045deployment.apps "maitred" created (dry run)
1046deployment.apps "valeter" created (dry run)
1047deployment.apps "waiter" created (dry run)
1048deployment.apps "waterdispatcher" created (dry run)
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001049```
1050
1051A production real-life version of this could should omit the `--dry-run` flag
1052of course.
1053
Joel Longtineb6544662019-06-11 16:59:12 +00001054### Extract CUE templates directly from Kubernetes Go source
1055
Florian Kleind7d28372019-11-19 05:14:54 -08001056In order for `cue get go` to generate the CUE templates from Go sources, you first need to have the sources locally:
1057
1058```
1059$ go get k8s.io/api/apps/v1
Florian Kleind7d28372019-11-19 05:14:54 -08001060```
1061
Joel Longtineb6544662019-06-11 16:59:12 +00001062```
1063$ cue get go k8s.io/api/core/v1
Marcel van Lohuizen8f9ef312019-12-01 17:07:59 +01001064$ cue get go k8s.io/api/apps/v1
Joel Longtineb6544662019-06-11 16:59:12 +00001065
1066```
1067
Marcel van Lohuizenb13155b2019-10-25 16:52:30 +02001068Now that we have the Kubernetes definitions in our module, we can import and use them:
Joel Longtineb6544662019-06-11 16:59:12 +00001069
1070```
1071$ cat <<EOF > k8s_defs.cue
1072package kube
1073
1074import (
1075 "k8s.io/api/core/v1"
Marcel van Lohuizen8f9ef312019-12-01 17:07:59 +01001076 apps_v1 "k8s.io/api/apps/v1"
Joel Longtineb6544662019-06-11 16:59:12 +00001077)
1078
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001079service: [string]: v1.Service
Marcel van Lohuizen8f9ef312019-12-01 17:07:59 +01001080deployment: [string]: apps_v1.Deployment
1081daemonSet: [string]: apps_v1.DaemonSet
1082statefulSet: [string]: apps_v1.StatefulSet
Joel Longtineb6544662019-06-11 16:59:12 +00001083EOF
1084```
1085
1086And, finally, we'll format again:
1087
1088```
1089cue fmt
1090```
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001091
1092## Manually tailored configuration
1093
1094In Section "Quick 'n Dirty" we showed how to quickly get going with CUE.
1095With a bit more deliberation, one can reduce configurations even further.
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001096Also, we would like to define a configuration that is more generic and less tied
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001097to Kubernetes.
1098
1099We will rely heavily on CUEs order independence, which makes it easy to
1100combine two configurations of the same object in a well-defined way.
1101This makes it easy, for instance, to put frequently used fields in one file
1102and more esoteric one in another and then combine them without fear that one
1103will override the other.
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001104We will take this approach in this section.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001105
1106The end result of this tutorial is in the `manual` directory.
1107In the next sections we will show how to get there.
1108
1109
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001110### Outline
1111
Emil Hessman66ec9592019-07-14 17:58:27 +02001112The basic premise of our configuration is to maintain two configurations,
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001113a simple and abstract one, and one compatible with Kubernetes.
1114The Kubernetes version is automatically generated from the simple configuration.
1115Each simplified object has a `kubernetes` section that get gets merged into
1116the Kubernetes object upon conversion.
1117
1118We define one top-level file with our generic definitions.
1119
1120```
1121// file cloud.cue
1122package cloud
1123
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001124service: [Name=_]: {
Roger Peppe1ce0c512019-09-24 15:29:39 +01001125 name: *Name | string // the name of the service
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001126
1127 ...
1128
1129 // Kubernetes-specific options that get mixed in when converting
1130 // to Kubernetes.
1131 kubernetes: {
1132 }
1133}
1134
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001135deployment: [Name=_]: {
Roger Peppe1ce0c512019-09-24 15:29:39 +01001136 name: *Name | string
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001137 ...
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001138}
1139```
1140
1141A Kubernetes-specific file then contains the definitions to
1142convert the generic objects to Kubernetes.
1143
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001144Overall, the code modeling our services and the code generating the kubernetes
1145code is separated, while still allowing to inject Kubernetes-specific
1146data into our general model.
1147At the same time, we can add additional information to our model without
Emil Hessman66ec9592019-07-14 17:58:27 +02001148it ending up in the Kubernetes definitions causing it to barf.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001149
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001150
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001151### Deployment Definition
1152
1153For our design we assume that all Kubernetes Pod derivatives only define one
1154container.
1155This is clearly not the case in general, but often it does and it is good
1156practice.
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001157Conveniently, it simplifies our model as well.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001158
1159We base the model loosely on the master templates we derived in
1160Section "Quick 'n Dirty".
1161The first step we took is to eliminate `statefulSet` and `daemonSet` and
1162rather just have a `deployment` allowing different kinds.
1163
1164```
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001165deployment: [Name=_]: _base & {
Roger Peppe1ce0c512019-09-24 15:29:39 +01001166 name: *Name | string
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001167 ...
1168```
1169
1170The kind only needs to be specified if the deployment is a stateful set or
1171daemonset.
1172This also eliminates the need for `_spec`.
1173
1174The next step is to pull common fields, such as `image` to the top level.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001175
Emil Hessman66ec9592019-07-14 17:58:27 +02001176Arguments can be specified as a map.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001177```
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001178 arg: [string]: string
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001179 args: [ "-\(k)=\(v)" for k, v in arg ] | [...string]
1180```
1181
1182If order matters, users could explicitly specify the list as well.
1183
1184For ports we define two simple maps from name to port number:
1185
1186```
Marcel van Lohuizen1e0fe9c2018-12-21 00:17:06 +01001187 // expose port defines named ports that is exposed in the service
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001188 expose: port: [string]: int
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001189
Marcel van Lohuizen1e0fe9c2018-12-21 00:17:06 +01001190 // port defines a named port that is not exposed in the service.
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001191 port: [string]: int
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001192```
1193Both maps get defined in the container definition, but only `port` gets
1194included in the service definition.
1195This may not be the best model, and does not support all features,
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001196but it shows how one can chose a different representation.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001197
1198A similar story holds for environment variables.
1199In most cases mapping strings to string suffices.
1200The testdata uses other options though.
1201We define a simple `env` map and an `envSpec` for more elaborate cases:
1202
1203```
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001204 env: [string]: string
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001205
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001206 envSpec: [string]: {}
Marcel van Lohuizen2437f9d2019-10-29 12:22:38 +01001207 envSpec: {
1208 for k, v in env {
1209 "\(k)" value: v
1210 }
1211 }
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001212```
1213The simple map automatically gets mapped into the more elaborate map
1214which then presents the full picture.
1215
1216Finally, our assumption that there is one container per deployment allows us
1217to create a single definition for volumes, combining the information for
1218volume spec and volume mount.
1219
1220```
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001221 volume: [Name=_]: {
Marcel van Lohuizen2437f9d2019-10-29 12:22:38 +01001222 name: *Name | string
1223 mountPath: string
1224 subPath: null | string
1225 readOnly: bool
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001226 kubernetes: {}
1227 }
1228```
1229
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001230All other fields that we way want to define can go into a generic kubernetes
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001231struct that gets merged in with all other generated kubernetes data.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001232This even allows us to augment generated data, such as adding additional
1233fields to the container.
1234
1235
1236### Service Definition
1237
1238The service definition is straightforward.
1239As we eliminated stateful and daemon sets, the field comprehension to
1240automatically derive a service is now a bit simpler:
1241
1242```
1243// define services implied by deployments
Marcel van Lohuizen2437f9d2019-10-29 12:22:38 +01001244service: {
1245 for k, spec in deployment {
1246 "\(k)": {
1247 // Copy over all ports exposed from containers.
1248 for Name, Port in spec.expose.port {
1249 port: "\(Name)": {
1250 port: *Port | int
1251 targetPort: *Port | int
1252 }
1253 }
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001254
Marcel van Lohuizen2437f9d2019-10-29 12:22:38 +01001255 // Copy over the labels
1256 label: spec.label
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001257 }
1258 }
Marcel van Lohuizen2437f9d2019-10-29 12:22:38 +01001259}
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001260```
1261
Marcel van Lohuizen50a01f32018-12-20 21:47:59 +01001262The complete top-level model definitions can be found at
1263[doc/tutorial/kubernetes/manual/services/cloud.cue](https://cue.googlesource.com/cue/+/master/doc/tutorial/kubernetes/manual/services/cloud.cue).
1264
1265The tailorings for this specific project (the labels) are defined
1266[here](https://cue.googlesource.com/cue/+/master/doc/tutorial/kubernetes/manual/services/kube.cue).
1267
1268
1269### Converting to Kubernetes
1270
1271Converting services is fairly straightforward.
1272
1273```
Marcel van Lohuizen2437f9d2019-10-29 12:22:38 +01001274kubernetes: services: {
Marcel van Lohuizen9af9a902019-09-07 20:30:10 +02001275 for k, x in service {
1276 "\(k)": x.kubernetes & {
1277 apiVersion: "v1"
1278 kind: "Service"
Marcel van Lohuizen50a01f32018-12-20 21:47:59 +01001279
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001280 metadata: name: x.name
1281 metadata: labels: x.label
1282 spec: selector: x.label
Marcel van Lohuizen50a01f32018-12-20 21:47:59 +01001283
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001284 spec: ports: [ p for p in x.port ]
Marcel van Lohuizen9af9a902019-09-07 20:30:10 +02001285 }
1286 }
Marcel van Lohuizen50a01f32018-12-20 21:47:59 +01001287}
1288```
1289
1290We add the Kubernetes boilerplate, map the top-level fields and mix in
1291the raw `kubernetes` fields for each service.
1292
1293Mapping deployments is a bit more involved, though analogous.
1294The complete definitions for Kubernetes conversions can be found at
1295[doc/tutorial/kubernetes/manual/services/k8s.cue](https://cue.googlesource.com/cue/+/master/doc/tutorial/kubernetes/manual/services/k8s.cue).
1296
1297Converting the top-level definitions to concrete Kubernetes code is the hardest
1298part of this exercise.
1299That said, most CUE users will never have to resort to this level of CUE
1300to write configurations.
1301For instance, none of the files in the subdirectories contain comprehensions,
Emil Hessman66ec9592019-07-14 17:58:27 +02001302not even the template files in these directories (such as `kitchen/kube.cue`).
Marcel van Lohuizen1e0fe9c2018-12-21 00:17:06 +01001303Furthermore, none of the configuration files in any of the
1304leaf directories contain string interpolations.
Marcel van Lohuizen50a01f32018-12-20 21:47:59 +01001305
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001306
1307### Metrics
1308
1309The fully written out manual configuration can be found in the `manual`
1310subdirectory.
1311Running our usual count yields
1312```
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +02001313$ find . | grep kube.cue | xargs wc | tail -1
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001314 542 1190 11520 total
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001315```
1316This does not count our conversion templates.
1317Assuming that the top-level templates are reusable, and if we don't count them
Emil Hessman66ec9592019-07-14 17:58:27 +02001318for both approaches, the manual approach shaves off about another 150 lines.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001319If we count the templates as well, the two approaches are roughly equal.
1320
1321
1322### Conclusions Manual Configuration
1323
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001324We have shown that we can further compact a configuration by manually
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001325optimizing template files.
Emil Hessman66ec9592019-07-14 17:58:27 +02001326However, we have also shown that the manual optimization only gives
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001327a marginal benefit with respect to the quick-and-dirty semi-automatic reduction.
Emil Hessman66ec9592019-07-14 17:58:27 +02001328The benefits for the manual definition largely lies in the organizational
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001329flexibility one gets.
1330
1331Manually tailoring your configurations allows creating an abstraction layer
1332between logical definitions and Kubernetes-specific definitions.
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001333At the same time, CUE's order independence
Emil Hessman66ec9592019-07-14 17:58:27 +02001334makes it easy to mix in low-level Kubernetes configuration wherever it is
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001335convenient and applicable.
1336
1337Manual tailoring also allows us to add our own definitions without breaking
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001338Kubernetes.
1339This is crucial in defining information relevant to definitions,
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001340but unrelated to Kubernetes, where they belong.
1341
1342Separating abstract from concrete configuration also allows us to create
1343difference adaptors for the same configuration.
1344
1345
1346<!-- TODO:
1347## Conversion to `docker-compose`
Tarun Gupta Akiralabb2b6512019-06-03 13:24:12 +00001348-->