Explanation
The dependency graph
In most Kubernetes setups, YAML files know nothing about each other. If a Deployment reads a ConfigMap or dials a database Service, you have to remember to configure deploy ordering, readiness waits, and network policies in separate places. In Kix, referencing another resource’s output automatically records that relationship in a cluster-wide dependency graph, and Kix uses that single graph to order deployments and generate firewall rules.
How output references become dependency edges
Section titled “How output references become dependency edges”Kix does not ask you to maintain a separate dependsOn list for ordinary resource references. Instead, it uses Nix string context to discover dependencies from the values you actually use.
When a package builds a resource with scope.mkResource or scope.mkDeployment, Kix serializes the manifest to JSON and writes it to a content-addressed file in Kix’s local store. The file path includes a hash of the manifest’s contents. Kix then builds the resource’s public out attributes (out.name, out.fqdn, out.selector, out.url, and any custom outputs) and attaches the resource’s store path to each string’s hidden context.
When another resource reads one of those out values, the rendered string looks like an ordinary Kubernetes value, such as "backend.app.svc.cluster.local", while carrying the hidden store-path reference alongside it:
1# Inside the gateway package:2proxy_pass = "http://${backend.out.fqdn}:${toString backend.out.port}/";When Kix seals the consuming resource, scope.mkResource inspects the string context of the serialized JSON (builtins.getContext), extracts every referenced resource hash, and stamps those hashes into the kix.run/depends-on annotation. Because the value and the dependency travel together on the same string, you cannot read backend.out.fqdn and forget to declare the dependency. Conversely, typing a literal string like "backend.app.svc.cluster.local" carries no context and records no dependency, which is why Kix provides scorecard rules and cross-resource checks to catch untracked literals.
String context carries two distinct kinds of tokens:
- Identity paths identify the upstream resource in Kix’s local store. Every
outattribute carries an identity path, which tells Kix that the consumer was computed from the provider and must be deployed after it. - Endpoint facts attach to address-shaped attributes such as
out.fqdnandout.url. An endpoint fact records the target Service’s namespace, pod selector, and ports. Non-workload resources such as ConfigMaps conduct endpoint facts onward when mounted, and workloads terminate them. This lets Kix distinguish between a workload that merely names a resource and a workload that dials a Service over the network.
Sometimes one resource must be applied before another even though no string value flows between their manifests. A CustomResourceDefinition must be established before any custom resource of its kind is applied, and a network policy must be in place before the pod it governs starts. For those cases, builders accept requires (hard ordering dependencies that must be ready first) and extraDeps (additional ordering hints). Kix folds both into kix.run/depends-on before sealing the resource.
Dependency resolution and explicit deps
Section titled “Dependency resolution and explicit deps”Within a single package, resources reference siblings through the self fixpoint (self.configmap.out.name). Across packages, a package declares what it needs by naming arguments on its build function:
1build = { self, config, database, redis ? null, ... }: {2 # database is required; redis is optional3};Kix inspects the build function arguments before building any resources and resolves each non-system argument against the cluster’s instances. The resolver acts like a linker with a strict priority ladder:
- Explicit wiring (
deps.<arg>): If the consuming instance setsdeps.database = ref.data.postgres, Kix binds the argument directly to that target instance. - Same-namespace match: Otherwise, Kix looks in the consumer’s own namespace for an instance whose name,
aliases, or packagemeta.defaultAliases(or claimedmeta.roles) matches the argument name. If two candidates in the same namespace match, evaluation fails with an ambiguity error. - Cluster-wide match: If nothing matches in the same namespace, Kix looks across all other namespaces for candidates whose package serves the whole cluster: it sets
meta.scope = "cluster", claims a role inmeta.roles, or declaresmeta.conflictsWithon acluster:key. Stand-ins made withkix.mkImportserve the whole cluster unless they setscope = "namespace". An instance that Kix created fromavailablePackagesor enabled viaoptional = trueto satisfy a dependency also counts, because the cluster definition chose where it lives. If exactly one such candidate matches, Kix binds it. If several match, evaluation fails and lists adepsline for each. An instance whose package does none of these is used from another namespace only through step 1; when it is the only match for a required argument, evaluation fails and the error prints thedepsline to add.meta.scope = "namespace"(orscope = "namespace"on an import) goes further: the instance is never used from another namespace, not even withref. - Auto-instantiation (
availablePackages): When a required dependency has no matching instance, Kix checksavailablePackages. If a single shelf package answers the requested name, Kix instantiates it automatically in the requester’s namespace (orautoNamespace). The new instance binds to every consumer of that name across all namespaces. The same applies to anoptional = trueinstance that Kix enables for a dependency. - Default or error: If no candidate is found and the argument has a Nix default (
? null), Kix leaves it unbound so the package receivesnull. When an instance in another namespace answers the name but does not serve the whole cluster, Kix also prints a warning with thedepsline that would bind it. If the argument is required, evaluation fails with a missing-dependency diagnostic.
At every step of the ladder, managed instances take precedence over external imports (kix.mkImport). This allows cluster flavors to provide fallback imports for platform services like DNS or storage classes that are automatically shadowed as soon as the cluster installs a managed package for the same role.
Imports and managed packages differ in their default scope. A managed package with meta.scope unset is used from another namespace only through ref. An import is used from every namespace unless it sets scope = "namespace", because an environment-provided service is normally shared. This includes imported Secrets and the service grants created by kix.interface. When several imports answer the same name from different namespaces, name the one you mean with ref.
Explicit deps wiring with ref.<namespace>.<instance> is the right tool whenever a cluster runs multiple providers of the same kind (such as two database clusters or public and internal ingress controllers) or when you want cross-namespace dependencies to stay visible at the call site in cluster.nix. A ref pointer goes through the same validation as automatic resolution: it activates optional instances, enforces meta.scope and meta.compatibleWith version bounds, and forwards any dependency config injections declared by the consumer.
DAG-driven deployment scheduler
Section titled “DAG-driven deployment scheduler”At the top of a compiled cluster sits a single Activation custom resource. The Activation references every PackageInstance marker, and each PackageInstance references its package’s root resource and transitively reachable manifests. Together with the kix.run/depends-on annotations on each manifest, this forms a complete directed acyclic graph (DAG) of the cluster.
When you run kix deploy (or when kix-operator reconciles an Activation in the cluster), Kix does not apply files in alphabetical order or sleep between numbered waves. Instead, it runs a graph-streaming scheduler over the DAG:
- Predecessor gating: A resource becomes eligible for server-side apply at the exact moment every resource in its
kix.run/depends-onset reachesReady. Independent branches of the graph deploy in parallel without waiting for unrelated slow resources in the same tier. - Structured readiness gates: Applying an object to the API server is separate from waiting for it to work. For Deployments, StatefulSets, DaemonSets, and Jobs, the scheduler watches pod rollout and completion conditions. For custom resources whose operators report health on custom status fields, packages attach a
kix.run/readinessrule on the CRD handle so the scheduler waits for the operator’s actual readiness condition before unblocking downstream dependents. - CRD and import barriers: Custom resources automatically depend on their CRD (or a
scope.mkCRDRefmarker). During deployment, Kix waits until the CRD reportsEstablished=Trueand serves the required API versions before applying any custom resource of that kind. For external resources declared viakix.mkImportorscope.mkSecretRef, Kix polls until the target object (and any required Secret keys) exists in the cluster before unblocking consumers. - Post-deploy health checks: Packages can attach
postHooks(such as akix.healthCheckprobe Job or database migration Job) that depend on the package’s primary workload. These jobs run after the workload is ready and gate completion of thePackageInstancenode.
Because the deployment scheduler follows the exact same dependencies that resource references created during evaluation, any dependency expressed through out is respected during rollout.