Skip to content
Install the CLI

Tutorial 10

Reuse packages across instances and environments

Once a package uses scope for resource naming and options for configuration, you can instantiate it as many times as you need: across multiple environment namespaces (app-v1 and app-v2, or staging and production) or multiple times inside the same namespace.

In this tutorial, you will inspect tutorials/10-reuse-packages/cluster.nix, which deploys two independent backend + gateway stacks (app-v1 and app-v2) from the exact same packages.echo-server and reverse-proxy-package.nix definitions.

Open tutorials/10-reuse-packages/cluster.nix. The cluster defines two namespaces (app-v1 and app-v2) and instantiates packages.echo-server and reverseProxyPackage in both instances.app-v1 and instances.app-v2:

tutorials/10-reuse-packages/cluster.nix
namespaces = {
app-v1 = { };
app-v2 = { };
};
# ── v1 stack ───────────────────────────────────────────────
# The two `package = ...` lines in v1 match the two
# `package = ...` lines in v2. Only `config` and namespace
# differ. That's the multi-instance pattern.
instances.app-v1 = {
backend = {
package = packages.echo-server; # ← same package as in v2
config.message = "Response from app-v1";
};
gateway = {
package = reverseProxyPackage; # ← same package as in v2
config = {
proxyPath = "/v1/";
maxBodySize = "10m";
};
deps.backend = ref.app-v1.backend;
};
};
# ── v2 stack ───────────────────────────────────────────────
instances.app-v2 = {
backend = {
package = packages.echo-server; # ← same package as in v1
config.message = "Response from app-v2";
};
gateway = {
package = reverseProxyPackage; # ← same package as in v1
config = {
proxyPath = "/v2/";
maxBodySize = "10m";
};
deps.backend = ref.app-v2.backend;
};
};

View source on GitHub ↗

Look at what stays identical and what changes between the two stacks:

  • Both stacks use the exact same packages.echo-server and reverseProxyPackage blueprints.
  • app-v1 configures message = "Response from app-v1" and proxyPath = "/v1/", and wires deps.backend = ref.app-v1.backend.
  • app-v2 configures message = "Response from app-v2" and proxyPath = "/v2/", and wires deps.backend = ref.app-v2.backend.

Inside reverseProxyPackage, the build function still reads backend.out.fqdn and backend.out.port. For app-v1/gateway, backend.out.fqdn resolves to backend.app-v1.svc.cluster.local; for app-v2/gateway, it resolves to backend.app-v2.svc.cluster.local. (Because each gateway and backend pair lives in the same namespace, you could even omit deps.backend here and let same-namespace resolution match backend automatically, though writing deps.backend = ref.app-v1.backend keeps the wiring explicit.)

Run kix check and kix graph on 10-reuse-packages:

kix-examples/
❱ kix check 10-reuse-packages
❱ kix graph 10-reuse-packages --format tree

In the dependency tree output, verify that the two proxy chains are isolated in their respective namespaces:

  • Under Namespace/app-v1, Service/backend@app-v1 feeds ConfigMap/gateway@app-v1 and Deployment/gateway@app-v1.
  • Under Namespace/app-v2, Service/backend@app-v2 feeds ConfigMap/gateway@app-v2 and Deployment/gateway@app-v2.

You will see this two-namespace layout again in tutorial 12 when we run cluster-wide scorecard rules across app-v1 and app-v2.

Why scope Also Allows Multiple Instances In One Namespace

Section titled “Why scope Also Allows Multiple Instances In One Namespace”

What if you want two instances of the same package inside a single namespace, such as an api-gateway and an admin-gateway in app-v1?

In tutorials/10-reuse-packages/reverse-proxy-package.nix, look at how the package names its ConfigMap, Deployment, and Service:

tutorials/10-reuse-packages/reverse-proxy-package.nix
let
name = scope.instanceName;
in
{
configmap = scope.mkResource {
apiVersion = "v1";
kind = "ConfigMap";
inherit name;
# ...
};

View source on GitHub ↗

Because the package derives name from scope.instanceName (instead of hardcoding "reverse-proxy" or "gateway"), every instance in a namespace gets its own distinct resource names and selector labels:

  • An instance key api-gateway receives scope.instanceName = "api-gateway" and scope.selectorLabels with app.kubernetes.io/instance = "api-gateway".
  • An instance key admin-gateway receives scope.instanceName = "admin-gateway" and scope.selectorLabels with app.kubernetes.io/instance = "admin-gateway".

Neither their resource names (ConfigMap/api-gateway vs. ConfigMap/admin-gateway) nor their pod label selectors collide, so both instances can run side by side in the same namespace and bind to different backends via deps.backend:

instances.app-v1 = {
api-backend = {
package = packages.echo-server;
config.message = "Response from API backend";
};
admin-backend = {
package = packages.echo-server;
config.message = "Response from Admin backend";
};
api-gateway = {
package = reverseProxyPackage;
deps.backend = ref.app-v1.api-backend;
config = {
proxyPath = "/api/";
maxBodySize = "10m";
};
};
admin-gateway = {
package = reverseProxyPackage;
deps.backend = ref.app-v1.admin-backend;
config = {
proxyPath = "/admin/";
maxBodySize = "50m";
};
};
};

You reused the same package definitions across multiple instances:

  • placing instances under instances.<namespace> (instances.app-v1 and instances.app-v2) deploys independent copies of a stack into separate namespaces while keeping out.fqdn scoped to each namespace
  • deriving resource names from scope.instanceName prevents collisions when a package is instantiated more than once in the same namespace
  • scope.mkDeployment includes app.kubernetes.io/instance = scope.instanceName in scope.selectorLabels, so each instance’s Service selects only its own pods
  • deps.<arg> = ref.<namespace>.<instance> lets each instance of a package bind to its intended provider

In the next tutorial, you will look at how clusters declare their underlying Kubernetes environment using flavors, roles, and kix.mkImport.