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.
Prerequisites
Section titled “Prerequisites”- Finish the reusing packages tutorial.
- Keep the same
kix-examplescheckout.
Inspect The import Rows From Tutorial 02
Section titled “Inspect The import Rows From Tutorial 02”Look again at the kix list packages output for 02-hello-world:
❱ 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:
{ 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 = { # ... }; } ];}That single entry (kix.flavors.kind in modules) added platform-dns and platform-storage in kube-system.
What A Cluster Flavor Does
Section titled “What A Cluster Flavor Does”Every Kubernetes distribution starts with a different set of pre-installed services:
- kind ships with CoreDNS (
Deployment/corednsandService/kube-dnsinkube-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:
- It registers two imported instances in
kube-system(platform-dnsandplatform-storage) usingkix.mkImport. - It binds those imported instances to the cluster’s infrastructure roles (
roles = [ "dns" ]androles = [ "storageClasses" ], alongsidecluster.roles.dnsandcluster.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?
- 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 everyPersistentVolumeClaimneeds itsStorageClassto exist before it can bind. Becauseplatform-dnsprovides thednsrole (Deployment/corednsinkube-system), Kix automatically adds arequires:readiness dependency from yourgatewayDeployment toplatform-dns. Duringkix deploy, Kix verifies that CoreDNS is live and ready inkube-systembefore rolling out workloads that depend on DNS. StorageClassresolution and validation: When a stateful package asks forstorageClassesinbuild(or setsspec.storageClassName), Kix resolves thestorageClassesrole toplatform-storage(storageClasses.out.storageClassName = "standard"on kind) and records a dependency onplatform-storage. If aPersistentVolumeClaimorStatefulSetvolumeClaimTemplaterequests aStorageClassthat is neither created in the cluster nor imported viakix.mkImport, evaluation fails before deploy telling you that nothing in the cluster creates or imports thatStorageClass.- DNS egress network policy: When a cluster enables NetworkPolicy synthesis (
networkPolicy.enable = true), Kix uses thednsrole provider and the flavor’splatformIntentsso 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:
1PackageInstance/platform-dns@kube-system (import)2├── Activation/08-namespace-deps-y50i580sy41b3└── Deployment/gateway@app4 └── Service/gateway@appDeclare 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:
27 package = kix.mkImport {28 kind = "Service";29 apiVersion = "v1";30 out = {31 name = "legacy-api";32 fqdn = "legacy-api.legacy.svc.${config.clusterDomain}";33 port = 8080;34 };35 };36 aliases = [ "legacyApi" ];Look at what kix.mkImport produces:
- No YAML manifest is applied to Kubernetes.
kix deploywill never create, modify, or pruneService/legacy-api. Instead,kix deployverifies thatService/legacy-apiexists in thelegacynamespace before rolling out dependents. - It exposes the standard
.outinterface. Any Kix package can depend onlegacyApiand readlegacyApi.out.fqdnandlegacyApi.out.port, just like it readbackend.out.fqdnin tutorial 06:
19 clientConfig = scope.mkResource {20 apiVersion = "v1";21 kind = "ConfigMap";22 name = "legacy-api-client";23 data.endpoint = "http://${legacyApi.out.fqdn}:${toString legacyApi.out.port}";24 };25
26 root = self.clientConfig;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:
217 instances.contracts-rds = {218 rds-login = {219 package = packages.secret-ref;220 config.keys = [221 "username"222 "password"223 ];224 };225
226 database.package = kix.mkImport {227 scope = "namespace";228 out = {229 host = "app.abc123.eu-west-1.rds.amazonaws.com";230 port = "5432";231 database = "metabase";232 tls = true;233 credentials = ref.contracts-rds.rds-login;234 };235 };236
237 encryption-key = {238 package = packages.secret-ref;239 aliases = [ "encryptionKey" ];240 config.keys = [ "key" ];241 };242
243 metabase.package = packages.metabase;244 };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.
What You Learned
Section titled “What You Learned”You saw how Kix connects packages to the surrounding Kubernetes cluster:
- including
kix.flavors.kindinmodulesdeclares the capabilities and pre-installed services of a kind cluster - infrastructure roles (
dnsandstorageClasses) connect workloads and PVCs to the cluster’s DNS provider and storage classes -
kix.mkImportregisters 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 andkix.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.