This recipe will guide you on how to deploy the same deployment multiple times to the same (via namespaces) or different clusters.

Use specific targets

The easiest way to achieve this is to define targets in your .kluctl.yaml. Each target should then use args to define a small set configuration values for the specific target.

Each target should relate to the target environment and/or cluster that it needs to be deployed to. For example, one could be named prod while another is named test, meaning that you can either deploy to the prod or to the test environment. It’s also useful to set the context field on each target, so that you can’t accidentally deploy the prod target to the test cluster.

args should be minimalistic to avoid bloating up the .kluctl.yaml. It should be used as the “entrypoint” into the actual configuration, which is then loaded from inside the root deployment.yaml via vars. See advanced configuration for details on this.

Example targets definition:

  targets:
  - name: prod
    context: prod.example.com
    args:
      environment_name: prod
  - name: test
    context: test.example.com
    args:
      environment_name: test

# Warning, this discriminator is only ok if targets are only deployed once per cluster. See next chapter for details.
discriminator: "my-project-{{ target.name }}"

args:
  - name: environment_name
  

Example CLI invocations:

  $ kluctl deploy -t prod
$ kluctl deploy -t test
  

Use more dynamic targets

As an alternative to very specific targets, you could also define targets that are more dynamic so that each target can be deployed multiple times, but to different Kubernetes contexts or even namespaces. You can also mix such targets, for example have one prod target that is just like described in the previous chapter, and one non-prod target that can be used to deploy to multiple non-production clusters.

The dynamic targets then need a way so that they can be differentiated. The easiest way is to use different contexts, which means you deploy it to different clusters. Another way is to introduce args that serve to differentiate, e.g. an arg names environment_name which can then be used to deploy the same workloads to different namespaces, add prefixes to global resources, create unique ingresses, and so on.

If such an argument is introduced, you would then invoke the CLI with the argument being set.

Another thing to take into account is the required uniqueness of discriminators to make delete and prune work properly. If you miss this crucial part or make a mistake, you might end up deleting resources that were not meant to be deleted. The uniqueness must be ensured inside the boundaries of individual clusters.

Example targets definition:

  targets:
  - name: prod
    context: prod.example.com
    args:
      environment_type: prod
      environment_name: prod
  - name: non-prod
    args:
      environment_type: non-prod
      # environment_name must be passed via CLI

# This will ensure that the discriminator is unique, even if the same target is deployed multiple times
discriminator: my-project-{{ target.name }}-{{ args.environment_type }}-{{ args.environment_name }}

# This is a bad example of a discriminator. It will lead to the discriminator being equal for every environment deployed to the same cluster.
# discriminator: "my-project-{{ target.name }}"

args:
  - name: environment_type
  - name: environment_name
  

Example CLI invocations:

  $ kluctl deploy -t prod # deploys to prod context
$ kluctl deploy -t non-prod -a environment_name=test-env1 # deploys to currently active context 
$ kluctl deploy -t non-prod -a environment_name=test-env2 # deploys to currently active context
$ kluctl deploy -t non-prod -a environment_name=test-env3 --context test2.exmaple.com
  

Too long discriminators

Right now, Kluctl is internally using a single label to store discriminators in Kubernetes. This has some serious limitations in regard to the length of the discriminators, which is 63 characters. This means, that the discriminator template shown in the above example can easily lead to errors. This issue will be fixed when https://github.com/kluctl/kluctl/issues/468 is implemented.

Until then, you might need to use some form of shortening, e.g. by using a shortened hash of some string. Example for this:

  discriminator: my-project-{{ target.name }}-{{ args.environment_type }}-{{ (args.environment_name | sha256)[:8] }}
  

Using namespaces and more

So far, we have only shown how to define and use the targets feature to implement multiple target environments. This works out-of-the-box when you target different clusters per target, but will need some additional work when deploying to the same cluster. In that case, you are required to use different namespaces for each environment.

This can be easily achieved by using the mentioned environment_name inside resources. Combined with templating, it can be used to create dynamic namespaces, prefix resource names and create unique ingresses.

Example project:

  my-project/
├── .kluctl.yaml
├── deployment.yaml
├── namespaces/
│   └── namespace.yaml
└── apps
    ├── deployment.yaml
    ├── app1/
    │   ├── resource1.yaml
    │   └── resource2.yaml
    └── app2/
        ├── resource1.yaml
        └── resource2.yaml
  
.kluctl.yaml

See above.

deployment.yaml
  deployments:
  - path: namespaces
  - barrier: true # ensure namespaces are applied before we continue
  - include: apps
  
namespaces/namespace.yaml
  apiVersion: v1
kind: Namespace
metadata:
  name: {{ args.environment_name }}
  
apps/deployment.yaml
  deployments:
  - path: app1
  - path: app2

# This instructs Kluctl to set the specified namespace on all resources, including resources from `app1` and `app2`,
# that do not have a namespace set explicitly.
overrideNamespace: {{ args.environment_name }}
  
apps/app1/resource1.yaml
  apiVersion: v1
kind: ConfigMap
metadata:
  name: my-cm
  # no namespace needed here, as it is set via the `overrideNamespace` from `apps/deployment.yaml`
data:
  # just an example to show that you can also use the `args` here.
  environment_name: {{ args.environment_name }}