design/defunct/design-doc-template-stacks-experience.md
Managing configuration of bespoke Kubernetes applications has been a topic of much interest and discussion in the Kubernetes community. A design document written by Brian Grant gives a nice overview of the space, and proposes some of the properties and techniques that a unified solution would have.
A summary of the properties would be:
Users could choose an overlay-oriented approach or a template-style approach. In general, overlay-oriented configuration is considered better when the user has a large and complex configuration codebase, or when extending a third-party configuration is needed. Template-oriented configuration is considered easier to use for simple cases, especially for people who are not accustomed to overlay-oriented configuration.
The most prominent overlay-oriented configuration tool is kustomize, which recently became part of the mainline kubectl tool. The most prominent template-oriented configuration tool is helm, which is the de-facto standard tool for managing resource configuration bundles in Kubernetes.
On the Crossplane side, the extensibility model is still relatively new. In the most recent release, the concept of Stacks was introduced, and the first version of some tooling to help write Stacks was also introduced. The Crossplane project has been working toward making Stacks easier to write, ideally to the point where an author doesn't need to write a full controller. The easier version of Stacks is being called "Template Stacks". There is now a repository with some examples of what writing and interacting with a Template Stack may look like for different use-cases.
The goal of this document is to propose and discuss what the developer and user experience would look like for Template Stacks. This encompasses the complete lifecycle of a Template Stack: project setup, development, testing, publishing, and consuming.
Some of the design goals include:
We plan to support overlays in the long run, but plan to start with templates, because they are easier for users to get into when they're not familiar with overlays.
Because this is the first iteration of the Template-style Stacks Experience, there are many things which are out of scope. These include:
Also out of scope is the internal representation of configuration, and the implementation of the stack manager. See the complementary internals-oriented Template Stack design doc for those details.
This document is intended to be read alongside the quick start example in the template stacks experience repository, which contains an example of a complete user scenario of installing an application and its infrastructure using Crossplane and template stacks.
Additionally, for more detail about the internals of template stacks on the stack manager and Crossplane side, see the design doc focused on this
The overall flow of authoring and consuming a template stack will be very similar for all scenarios. It is expected that template stacks will support a wide variety of scenarios, so this section will show some examples to help explain them.
Creating a template stack from scratch involves only a couple steps. The project must first be initialized, and then the yamls must be put in a particular directory. The rbac requirements for the stack must also be configured, though some of that is done by the stack manager at runtime. At a high level, the steps would be as follows:
kubectl crossplane stack init --template myorg/mystack to create the boilerplate of the stack layout.config/stack/manifests/resources, or a different
directory if desired, so long as the directory is in the right
location in the stack artifact.config/stack/manifests/resources or in
config/crd/bases. We plan to make this simpler for the user by
adding a crossplane-cli command for it.stack.yaml to
specify how configurations are rendered.stack.yaml to specify that the stack will be working with all
of the kinds that it will be working with (using the dependsOn
field).stack.yaml as appropriate.Note that we plan to merge app.yaml and stack.yaml into a single
document (stack.yaml) in the future, so this set of steps is written
as though that has already happened.
Because we plan to support multiple engines, this document won't get
into the specifics of what templates look like. That said, if a helm
chart were being used (for example), then the helm chart could be placed
in a folder (such as wordpress) in the resources directory, and the
stack would be configured to use that directory. Using the folder
structure described above, that would mean that root of the helm chart
would exist at configu/stack/manifests/resources/wordpress.
The tooling will make it simpler to add a CRD from scratch. For example, the following would create a basic CRD in the appropriate folder, so that it becomes part of the stack:
kubectl crossplane stack crd init WordpressInstance wordpress.samples.stacks.crossplane.io
The command will generate a reasonable version (such as v1alpha1), and
sensible list, plural, and singular names from the input; the generated
values can be adjusted by the user in the CRD file.
There will also be a convenience flag in the stack init command so
that people starting from scratch can initialize the stack and the first
CRD with a single command:
$ kubectl crossplane stack init --template mygroup/mystackname --init-crd
> CRD name: WordpressInstance
> CRD api group: wordpress.samples.stacks.crossplane.io
In the future, we will likely do more work in the are of making CRDs and their schemas simpler to write.
To build and publish a stack, the standard steps for building and publishing a stack are used.
Building:
kubectl crossplane stack build
Publishing:
kubectl crossplane stack publish
Consuming has two steps: installation and creation. Installation is installing the stack into the Crossplane control cluster. Creation is creating a Kubernetes resource which the stack recognizes, so that the stack will do something in response to it. In the case of template stacks, that usually will look something like rendering a set of yamls and applying the rendered output to the cluster.
Installation is much the same as installing any stack:
kubectl crossplane stack install myorg/mystack mystack
This installs the stack from the myorg/mystack image, using the name mystack.
Instantiation is also very similar to creating any object. Here is some sample yaml:
apiVersion: thing.samples.stacks.crossplane.io/v1alpha1
kind: ThingInstance
metadata:
name: thinginstance-sample
spec:
myfield: myvalue
The difference is that underneath, the ThingInstance will be used as the input for rendering a template.
When the ThingInstance is deleted, the corresponding resources will also be deleted. This is the same behavior as in any stack.
The instance is also deleted if the stack is uninstalled.
Uninstall looks the same as any other stack:
kubectl crossplane stack uninstall mystack
Upgrading a stack version, and updating an instance of an object managed by the stack, are out of scope of this document. However, one could imagine that the process of upgrading a stack version would change a version number on object instances which go with the stack, and that the version number change would underneath cause the template to be rendered again and applied to the cluster.
Configuration for the template stack will be in a stack.yaml file at
the build root of the stack in the repository. For most cases, this will
mean the stack.yaml lives at the root of the repository.
A Stack author must create a stack.yaml in order to configure how
templates are rendered in response to a render request. Here's an
example, with comments explaining the directives:
# This field configures which templates are rendered in response
# to a given object type. An instance of the object type is
# considered to be a "render request".
behaviors:
# An engine configuration here will apply to the rest of the
# configuration, but it could also be specified at a per-crd
# level, or as low as a per-hook level. Engine configurations
# nested deeper in the configuration hierarchy will override
# ones which are higher up in the hierarchy, so a hook-level
# configuration would override this one.
engine:
type: kustomize
crds:
# This is a particular CRD which is being configured. When the
# controller sees an object of this type, it will do something.
wordpressinstance.wordpress.samples.stacks.crossplane.io:
# This is a top-level object to group hook configurations.
# There can be hooks for multiple different types of events.
hooks:
# Post create is triggered after an instance is created
postCreate:
# These are the templates which should be rendered when an object
# of the type above is seen.
#
# Defaults for the variables can be defined in the default values
# for the CRD fields, using the standard Kubernetes mechanism for
# specifying CRD field default values.
# Note that this is a list of objects, so multiple can be
# specified.
- directory: wordpress
# Post update is triggered after an instance is changed
postUpdate:
- directory: wordpress
The stack.yaml should be in the build root of the repository. For most
single-stack repositories, this means the root of the repository.
For a realistic sample for a complete user scenario, see the quick start example in the template stack experience repository.
Default values will be specified in the CRD itself, using the standard
mechanism for specifying default values for
CRD fields. Here is an example excerpt from a realistic CRD, where the
CRD's spec.image field is configured with a default:
kind: CustomResourceDefinition
metadata:
creationTimestamp: null
name: wordpressinstances.wordpress.samples.stacks.crossplane.io
spec:
group: wordpress.samples.stacks.crossplane.io
names:
kind: WordpressInstance
plural: wordpressinstances
scope: ""
validation:
openAPIV3Schema:
description: WordpressInstance is the Schema for the wordpressinstances API
properties:
...
spec:
type: object
properties:
...
image:
type: string
description: A custom wordpress container image id to use
# Defaults are specified like this, using the schema validation for CRD fields.
# For more about how this works with CRDs, see:
# https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/#defaulting
default: "wordpress:4.6.1-apache"
For the full example, see the sample in the template stack experience repository.
The imagined implementation for how objects sent to the stack for rendering become resources is that the object's fields become the input for the template rendered by the controller.
For example, given this object as input:
apiVersion: wordpress.samples.stacks.crossplane.io/v1alpha1
kind: WordpressInstance
metadata:
name: "my-wordpress-app-from-helm2"
namespace: dev
spec:
engineVersion: "8.0"
The engineVersion field would be available to the templating engine
which was being used, and would have a value of 8.0. This means that
if the engine being used were helm, the behavior would be equivalent to
if there were a values.yaml passed in which looked like this:
engineVersion: "8.0"
We expect the configuration to support the same things as the underlying
engine. So, for example, nested configuration values would be supported
just as well as they would be with a regular values.yaml for a helm
chart.
For the complete example, see the quick start example in the template stack experience repository. The snippet was taken from the part of the example where the app stack is used.
For more details, see the design document about the internals.
Template stacks will not be opinionated about which templating engine is
used. We plan to support multiple configuration engines. The engine will
be configurable by setting values in the stack.yaml. Here's an example
of a snippet from a stack.yaml which specifies a particular engine:
behaviors:
engine:
type: kustomize
In some cases, the engine configuration line may be optional; the system
may be able to infer the engine based on the structure of the stack. For
example, if no engine is specified, and a kustomization.yaml is found,
the engine could be inferred to be kustomize.
The engine can be specified at multiple levels; setting it under
behaviors will set a default, but setting it lower down will override
a value set at a higher level. It can be configured per hook.
For a more complete example of a user scenario, including usage of multiple different engines, see the quick start example. There are also some more details in the stack yaml section of this document, and in the helm charts section.
We expect to eventually support lifecycle hooks. See the speculative design in the template stacks experience repo for more details about what that could look like. Lifecycle hooks are out of the scope of this document, and will be revisited in the future.
See the design doc on the internals of the Template Stack implementation.
See the design doc on the internals of the Template Stack implementation.
For a coherent user scenario, see the quick start example.
The scenario shows what it might look like for a user to set up an application and its infrastructure from scratch using Crossplane and Template Stacks. Multiple configuration engines are shown.
To author a template stack from a simple helm chart, these steps could be followed:
kubectl crossplane stack init --template.kubectl crossplane stack crd init WordpressInstance wordpress.samples.stacks.crossplane.iostack.yaml in the root of the repostory, and configure it
to use the templates for the created CRD.values.yaml into the CRD's
default field value definitions.When consuming the stack, the default template values can be overridden by specifying fields on the render request object.
This use-case will probably need additional thought if we want to support all permutations of helm chart. For more realistic and complete examples, see the helm variation of the app stack in the quick start example in the template stack experience repository.