Skip to content
Install the CLI

Tutorial 11

Flavors, roles, and imports

Back in tutorial 02, when you ran kix list packages --cluster 02-hello-world, the output included two rows you did not write in cluster.nix: platform-dns and platform-storage, both marked import [kube-system].

This tutorial explains where those import rows come from and how Kix models pre-existing cluster infrastructure. You will learn how flavors (kix.flavors.kind) declare what your Kubernetes distribution already provides, how roles (dns, storageClasses) wire those platform capabilities to workloads automatically, and how kix.mkImport lets your own packages depend on existing services through the standard .out contract.

Look again at the kix list packages output for 02-hello-world:

kix-examples/
❱ kix list packages --cluster 02-hello-world
 NAME              VERSION  OWNER     STATUS                  
 hello-world       1.30.5   app-team  installed [tutorial-02] 
 platform-dns      -        -         import [kube-system]    
 platform-storage  -        -         import [kube-system]

In tutorials/02-hello-world/cluster.nix, you only declared one instance (hello-world), yet you also included kix.flavors.kind in the cluster’s modules list:

tutorials/02-hello-world/cluster.nix
{ kix, packages }:
# `kix.buildCluster` builds a cluster spec from an attrset. The body
# below is one big attrset literal.
kix.buildCluster {
name = "02-hello-world";
# `modules` is a list (whitespace-separated, no commas).
modules = [
# Sets the Kix flavor. These examples use kind (Kubernetes in Docker).
# Setting the flavor allows Kix to assume sensible defaults.
kix.flavors.kind
# A module — an attrset describing part of the cluster.
{
# Every namespace the cluster manages. `{ }` is an empty attrset
# — we don't need any per-namespace config here.
namespaces = {
tutorial-02 = { };
};
# `instances.<namespace> =` is the dotted-path shorthand for
# creating a nested attrset.
# Here we have an instance named `hello-world` in namespace
# `tutorial-02`.
instances.tutorial-02 = {
# ...
};
}
];
}

View source on GitHub ↗

That single entry (kix.flavors.kind in modules) added platform-dns and platform-storage in kube-system.

Every Kubernetes distribution starts with a different set of pre-installed services:

  • kind ships with CoreDNS (Deployment/coredns and Service/kube-dns in kube-system) and Rancher local-path provisioner (StorageClass/standard).
  • EKS, GKE, AKS, and k3s ship with their own DNS services, storage classes, CNI capabilities, and cloud load balancer controllers.
  • Self-managed (kubeadm, talos, rke2) clusters may require you to install your own storage provisioner, ingress controller, and CNI.

Instead of forcing packages to guess what cluster they are running on, Kix packages declare what cluster capabilities they need, and a cluster flavor module (kix.flavors.kind, kix.flavors.eks, kix.flavors.gke, kix.flavors.k3s, kix.flavors.kubeadm, kix.flavors.talos, and others) declares what the platform already provides.

Specifically, kix.flavors.kind does two things:

  1. It registers two imported instances in kube-system (platform-dns and platform-storage) using kix.mkImport.
  2. It binds those imported instances to the cluster’s infrastructure roles (roles = [ "dns" ] and roles = [ "storageClasses" ], alongside cluster.roles.dns and cluster.roles.storageClasses).

How Infrastructure Roles Connect Workloads To The Platform

Section titled “How Infrastructure Roles Connect Workloads To The Platform”

Why does kix.flavors.kind register platform-dns and platform-storage as instances in the dependency graph?

  1. Readiness barriers (requires:): Every Pod that uses cluster DNS (dnsPolicy = "ClusterFirst", the Kubernetes default) needs CoreDNS to be running before it starts making upstream requests. And every PersistentVolumeClaim needs its StorageClass to exist before it can bind. Because platform-dns provides the dns role (Deployment/coredns in kube-system), Kix automatically adds a requires: readiness dependency from your gateway Deployment to platform-dns. During kix deploy, Kix verifies that CoreDNS is live and ready in kube-system before rolling out workloads that depend on DNS.
  2. StorageClass resolution and validation: When a stateful package asks for storageClasses in build (or sets spec.storageClassName), Kix resolves the storageClasses role to platform-storage (storageClasses.out.storageClassName = "standard" on kind) and records a dependency on platform-storage. If a PersistentVolumeClaim or StatefulSet volumeClaimTemplate requests a StorageClass that is neither created in the cluster nor imported via kix.mkImport, evaluation fails before deploy telling you that nothing in the cluster creates or imports that StorageClass.
  3. DNS egress network policy: When a cluster enables NetworkPolicy synthesis (networkPolicy.enable = true), Kix uses the dns role provider and the flavor’s platformIntents so default-deny egress policies still allow UDP/TCP port 53 traffic to CoreDNS.

You can see that platform-dns dependency in the kix graph output you ran in tutorial 08:

PackageInstance/platform-dns@kube-system (import)
├── Activation/08-namespace-deps-y50i580sy41b
└── Deployment/gateway@app
└── Service/gateway@app

Declare Your Own Imported Resource With kix.mkImport

Section titled “Declare Your Own Imported Resource With kix.mkImport”

Flavors use kix.mkImport for cluster-level DNS and storage, and you can use the exact same kix.mkImport helper in cluster.nix whenever a Kix package needs to talk to an existing service that Kix does not manage (for example, a legacy Service in another namespace or a Helm release you have not migrated yet).

In how-to/adoption/cluster.nix, the cluster declares an imported Service named legacy-api in the legacy namespace:

how-to/adoption/cluster.nix
package = kix.mkImport {
kind = "Service";
apiVersion = "v1";
out = {
name = "legacy-api";
fqdn = "legacy-api.legacy.svc.${config.clusterDomain}";
port = 8080;
};
};
aliases = [ "legacyApi" ];

View source on GitHub ↗

Look at what kix.mkImport produces:

  • No YAML manifest is applied to Kubernetes. kix deploy will never create, modify, or prune Service/legacy-api. Instead, kix deploy verifies that Service/legacy-api exists in the legacy namespace before rolling out dependents.
  • It exposes the standard .out interface. Any Kix package can depend on legacyApi and read legacyApi.out.fqdn and legacyApi.out.port, just like it read backend.out.fqdn in tutorial 06:
how-to/adoption/client-package.nix
clientConfig = scope.mkResource {
apiVersion = "v1";
kind = "ConfigMap";
name = "legacy-api-client";
data.endpoint = "http://${legacyApi.out.fqdn}:${toString legacyApi.out.port}";
};
root = self.clientConfig;

View source on GitHub ↗

Later, if you migrate legacy-api into a native Kix package, you only replace package = kix.mkImport { ... } in cluster.nix with the real package. The consumer package (client-package.nix) does not change at all.

Shared Dependency Contracts For Backing Services

Section titled “Shared Dependency Contracts For Backing Services”

In the legacy-api example above, client-package.nix only read two fields (fqdn and port). Catalog applications such as grafana, harbor, immich, metabase, n8n, plausible, and vaultwarden go further: when they ask for a backing service (database, valkey, smtp, oidc, gateway, or otlp), they validate the provider’s out attributes against a shared dependency contract in kix.contracts.*.

Whether a database dependency is satisfied by an in-cluster operator (cnpg-database or stackgres-database) or by an external cloud database declared with kix.mkImport, Kix checks that out exposes host, port, database, and a credentials secret declaring the keys username and password:

clusters/test-doc-how-tos.nix
instances.contracts-rds = {
rds-login = {
package = packages.secret-ref;
config.keys = [
"username"
"password"
];
};
database.package = kix.mkImport {
scope = "namespace";
out = {
host = "app.abc123.eu-west-1.rds.amazonaws.com";
port = "5432";
database = "metabase";
tls = true;
credentials = ref.contracts-rds.rds-login;
};
};
encryption-key = {
package = packages.secret-ref;
aliases = [ "encryptionKey" ];
config.keys = [ "key" ];
};
metabase.package = packages.metabase;
};

View source on GitHub ↗

Because a pod can only read Secret objects in its own namespace, an import that carries out.credentials (credentials = ref.<namespace>.<secret>) sits in the consuming application’s namespace with scope = "namespace". If a required contract field or secret key is missing on out, kix check catches it before deployment.

You saw how Kix connects packages to the surrounding Kubernetes cluster:

  • including kix.flavors.kind in modules declares the capabilities and pre-installed services of a kind cluster
  • infrastructure roles (dns and storageClasses) connect workloads and PVCs to the cluster’s DNS provider and storage classes
  • kix.mkImport registers an existing Kubernetes resource or external service in the dependency graph without managing its lifecycle
  • packages consume imported services through .out (.out.fqdn, .out.port, .out.credentials) using the exact same syntax and kix.contracts.* checks as Kix-managed instances

In the final tutorial (12. Scorecards and compile-time policy), you will write custom scorecard rules that inspect package instances and enforce routing and configuration policies across the entire cluster at build time.