From 7f8adf4df3cf6ba241658007300b8c0ec36ce5f1 Mon Sep 17 00:00:00 2001 From: Mike Borozdin Date: Fri, 4 Feb 2022 11:45:49 -0800 Subject: [PATCH] effective customizations chapter (#2659) merging this as an MVP and will iterate based on additional requests and customer feedback. --- site/book/06-deploying-packages/00.md | 2 +- site/book/07-effective-customizations/00.md | 19 +++ .../01-single-value-replacement.md | 137 ++++++++++++++++++ .../02-limiting-package-changes.md | 113 +++++++++++++++ .../03-generation.md | 29 ++++ site/book/README.md | 2 + site/sidebar.md | 4 + 7 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 site/book/07-effective-customizations/00.md create mode 100644 site/book/07-effective-customizations/01-single-value-replacement.md create mode 100644 site/book/07-effective-customizations/02-limiting-package-changes.md create mode 100644 site/book/07-effective-customizations/03-generation.md diff --git a/site/book/06-deploying-packages/00.md b/site/book/06-deploying-packages/00.md index 81a6ad94f2..2727abf73f 100644 --- a/site/book/06-deploying-packages/00.md +++ b/site/book/06-deploying-packages/00.md @@ -1,4 +1,4 @@ -In the last chapter of this book, we are going to cover how you deploy a kpt +In this chapter of this book, we are going to cover how you deploy a kpt package to a Kubernetes cluster and how the cluster state is managed as the package evolves over time. diff --git a/site/book/07-effective-customizations/00.md b/site/book/07-effective-customizations/00.md new file mode 100644 index 0000000000..3c699224db --- /dev/null +++ b/site/book/07-effective-customizations/00.md @@ -0,0 +1,19 @@ +Kubernetes configuration packages and customizations go hand in hand, all the +packaging tools enable package customization, since every package needs to be adapted to each specific use. In this chapter we cover effective +customizations techniques that kpt rendering and packaging enables. We show how +providing customization through parameters has some [pitfalls] and recommend +alternatives where the contents of the package are not hidden behind a facade. +Some of these alternatives are only possible because kpt has made an investment +into bulk editing with [KRM functions] and upstream merging. + +### Prerequisites + +Before reading this chapter you should familiarize yourself with [chapter 4] +which talks about using functions as well as [updating a package page] in +[chapter 3]. + +[chapter 4]: /book/04-using-functions/ +[chapter 3]: /book/03-packages/ +[pitfalls]: https://github.com/kubernetes/design-proposals-archive/blob/main/architecture/declarative-application-management.md#parameterization-pitfalls +[KRM functions]: https://github.com/kubernetes-sigs/kustomize/blob/master/cmd/config/docs/api-conventions/functions-spec.md +[updating a package page]: /book/03-packages/05-updating-a-package.md \ No newline at end of file diff --git a/site/book/07-effective-customizations/01-single-value-replacement.md b/site/book/07-effective-customizations/01-single-value-replacement.md new file mode 100644 index 0000000000..1239d5697a --- /dev/null +++ b/site/book/07-effective-customizations/01-single-value-replacement.md @@ -0,0 +1,137 @@ +## Scenario + +I have a single value replacement in my package. I don’t want package consumers +to look through all the yaml files to find the value I want them to set. It +seems easier to just create a parameter for this value and have the user look +at Kptfile for inputs. + +Example storage bucket: + +```yaml +apiVersion: storage.cnrm.cloud.google.com/v1beta1 +kind: StorageBucket +metadata: + name: my-bucket # kpt-set: ${project-id}-${name} + namespace: ns-test # kpt-set: ${namespace} + annotations: + cnrm.cloud.google.com/force-destroy: "false" + cnrm.cloud.google.com/project-id: my-project # kpt-set: ${project-id} +spec: + storageClass: standard # kpt-set: ${storage-class} + uniformBucketLevelAccess: true + versioning: + enabled: false +``` + +The corresponding Kptfile: + +```yaml +apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: bucket +info: + description: A Google Cloud Storage bucket +pipeline: + mutators: + - image: gcr.io/kpt-fn/apply-setters:v0.2 + configMap: + name: todo-bucket-name + namespace: todo-namespace + project-id: todo-project-id + storage-class: standard +``` + + +## Problems + +1. With package popularity the single values inevitably expand to provide a +facade to a large portion of the data. That defeats the purpose of minimizing +the cognitive load. With this small example almost half of the StorageBucket configuration is now covered with parameters. +1. Some values like resource names are used as references so setting them in +one place needs to trigger updates in all the places where they are referenced. +1. If additional resources that have similar values are added to the package +new string replacements need to be added. In this case everything will need +to also be marked up with project ID and namespace. +1. If a package is used as a sub-package the string replacement parameters need +to be surfaced to the parent package and if the parent package already expects +some values to be set and the parameters do not exist, the sub-package needs to +be updated. + +## Solutions: + +1. kpt allows the user to edit a particular value directly in the configuration +data and will handle upstream merge. When [editing the yaml] directly the +consumers are not confined to the parameters that the package author has +provided. [kpt pkg update] merges the local edits made by consumer with the +changes in the upstream package made by publisher. In this case `storageClass` +can be set directly by the user. +1. Attributes like resource names which are often updated by consumers to add +prefix or suffix (e.g. *-dev, *-stage, *-prod, na1-*, eu1-*) are best handled +by the [ensure-name-substring] function that will handle dependency updates as +well as capture all the resources in the package. +1. Instead of setting a particular value on a resource a bulk operation can be +applied to all the resources that fit a particular interface. This can be done +by a custom function or by [set-namespace], [search-and-replace] , [set-labels] +and [set-annotations] functions. + +New bucket configuration: + +```yaml +apiVersion: storage.cnrm.cloud.google.com/v1beta1 +kind: StorageBucket +metadata: + name: bucket + annotations: + cnrm.cloud.google.com/force-destroy: "false" +spec: + storageClass: standard + uniformBucketLevelAccess: true + versioning: + enabled: false +``` + +The suggested customizations are now in the Kptfile: + +```yaml +apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: bucket +info: + description: A Google Cloud Storage bucket +pipeline: + mutators: + - image: gcr.io/kpt-fn/set-namespace:v0.2.0 + configMap: + namespace: example-ns + - image: gcr.io/kpt-fn/ensure-name-substring:v0.1.1 + configMap: + prepend: project111- + - image: gcr.io/kpt-fn/set-annotations:v0.1.4 + configMap: + cnrm.cloud.google.com/project-id: project111 +``` + +The resource configuration YAML doesn't need to be marked up with where the +namespace value needs to go. The [set-namespace] function is smart enough to +find all the appropriate resources that need the namespace. + +We have put in the starter name `bucket` and have an [ensure-name-substring] +that shows the package consumer that the project ID prefix is what we suggest. +However if they have a different naming convention they can alter the name +prefix or suffix on all the resources in the pacakge. + +Since we are trying to set the annotation to the project ID we can use the +[set-annotations] function one time and the annotation are going to be set on +all the resources in the package. If we add additional resources or whole +sub packages we will get the consistent annotations across all resources +without having to find all the places where annotations can go. + +[editing the yaml]: /book/03-packages/03-editing-a-package +[kpt pkg update]: /book/03-packages/05-updating-a-package +[ensure-name-substring]: https://catalog.kpt.dev/ensure-name-substring/v0.1/ +[search-and-replace]: https://catalog.kpt.dev/search-replace/v0.2/ +[set-labels]: https://catalog.kpt.dev/set-labels/v0.1/ +[set-annotations]: https://catalog.kpt.dev/set-annotations/v0.1/ +[set-namespace]: https://catalog.kpt.dev/set-namespace/v0.2/ \ No newline at end of file diff --git a/site/book/07-effective-customizations/02-limiting-package-changes.md b/site/book/07-effective-customizations/02-limiting-package-changes.md new file mode 100644 index 0000000000..c7aa4bb0e3 --- /dev/null +++ b/site/book/07-effective-customizations/02-limiting-package-changes.md @@ -0,0 +1,113 @@ +## Scenario: + +I’d like to limit what my package consumers can do with my package and it feels +safer to just provide a string replacement in one place so they know not to +alter the configuration outside of the few places that I designated as OK +places to change. + +Example deployment: +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: # kpt-merge: /nginx-deploy + name: nginx-deploy +spec: + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: backend + image: nginx:1.16.1 # kpt-set: nginx:${tag} +``` + +kpt configuration that uses a setter: +```yaml +apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: dont-change-much +pipeline: + mutators: + - image: gcr.io/kpt-fn/apply-setters:v0.2.0 + configMap: + tag: 1.21 +``` + +## Problems: + +1. The limitation by parameters does not guarantee that consumers are in fact +going to limit their changes to the parameters. A popular pattern is using +kustomize to change output of other tools no matter what parameters had. In +this particular case I am able to fork or patch this package and add: + +```yaml +securityContext: + runAsNonRoot: false +``` + +2. String replacements rarely describe the intent of the package author. +When additional resources are added I need additional places where parameters +need to be applied. I can easily add other containers to this deployment and +the package author's rules are not clear and not easily validated. + +## Solutions: + +1. General ways to describe policy already exist. kpt has a [gatekeeper] +function that allows the author to describe intended limitations for a class +of resources or the entire package giving the consumer the freedom to customize +and get an error or a warning when the policy is violated. + +In the sample provided by the function we see how to provide a policy that will +clearly describe the intent using rego: + +```yaml +apiVersion: templates.gatekeeper.sh/v1 +kind: ConstraintTemplate +metadata: # kpt-merge: /disallowroot + name: disallowroot +spec: + crd: + spec: + names: + kind: DisallowRoot + targets: + - target: admission.k8s.gatekeeper.sh + rego: |- + package disallowroot + violation[{"msg": msg}] { + not input.review.object.spec.template.spec.securityContext.runAsNonRoot + msg := "Containers must not run as root" + } +--- +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: DisallowRoot +metadata: # kpt-merge: /disallowroot + name: disallowroot +spec: + match: + kinds: + - apiGroups: + - 'apps' + kinds: + - Deployment +``` + +The Kptfile can enforce that resources comply with this policy every time +`kpt fn render` is used: + +```yaml +apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: gatekeeper-disallow-root-user +pipeline: + validators: + - image: gcr.io/kpt-fn/gatekeeper:v0.2 +``` + +[gatekeeper]: https://catalog.kpt.dev/gatekeeper/v0.2/ diff --git a/site/book/07-effective-customizations/03-generation.md b/site/book/07-effective-customizations/03-generation.md new file mode 100644 index 0000000000..2e56329942 --- /dev/null +++ b/site/book/07-effective-customizations/03-generation.md @@ -0,0 +1,29 @@ +## Scenario: + +When using template languages I am able to provide conditional statements based +on parameter values. This allows me to ask the user for a little bit of +information and generate a lot of boilerplate configuration. Some template +languages like [Jinja] are very robust and feature rich. + +## Problems: + +1. Increased usage and additional edge cases make a template a piece of code +that required tets and debugging. +1. The interplay between different conditionals and loops is interleaved in the template making it hard to understand what exactly is configuration and what is +the logic that alters the configuration. The consumer is left with one choice +supply different parameter values, execute the template rendering code and see +what happens. +1. Templates are generally monolithic, when a change is introduced the package consumers need to either pay the cost of updating or the new consumers pay the +cost of having to decipher more optional parameters. + +## Solutions: + +1. When the generated configuration is simple consider just using a sub-package +and running customizations using [single value replacement] techniques. +1. When a complex configuration needs to be generated the package author can +create a generator function using turing complete languages and debugging tools. Example of such a function is [folder generation]. The output of the function +is plain old KRM. + +[folder generation]: https://catalog.kpt.dev/generate-folders/v0.1/ +[Jinja]: https://palletsprojects.com/p/jinja/ +[single value replacement]: /book/07-effective-customizations/01-single-value-replacement.md \ No newline at end of file diff --git a/site/book/README.md b/site/book/README.md index a77e3880d3..005dd90c80 100644 --- a/site/book/README.md +++ b/site/book/README.md @@ -13,6 +13,7 @@ This book is organized as follows: - [Chapter 4] covers how to use kpt functions to automate configuration changes. - [Chapter 5] guides you through developing custom functions. - [Chapter 6] covers how to deploy a package to a Kubernetes cluster. +- [Chapter 7] covers effective customizations techniques. Let's get started! @@ -22,3 +23,4 @@ Let's get started! [chapter 4]: /book/04-using-functions/ [chapter 5]: /book/05-developing-functions/ [chapter 6]: /book/06-deploying-packages/ +[chapter 7]: /book/07-effective-customizations/ diff --git a/site/sidebar.md b/site/sidebar.md index 5c709c19b0..2c1e8afb9d 100644 --- a/site/sidebar.md +++ b/site/sidebar.md @@ -30,6 +30,10 @@ - [6.1 Initializing a Package for Apply](book/06-deploying-packages/01-initializing-a-package-for-apply.md) - [6.2 Applying a Package](book/06-deploying-packages/02-applying-a-package.md) - [6.3 Handling Dependencies](book/06-deploying-packages/03-handling-dependencies.md) + - [7 Effective Customizations](book/07-effective-customizations/) + - [7.1 Single Value Replacement](book/07-effective-customizations/01-single-value-replacement.md) + - [7.2 Limiting Package Changes](book/07-effective-customizations/02-limiting-package-changes.md) + - [7.3 Generation](book/07-effective-customizations/03-generation.md) - [Reference](reference/) - [Annotations](reference/annotations/) - [apply-time mutation](reference/annotations/apply-time-mutation/)