Skip to content

03. Project files and flake entry point

This content is for the v0.1 version. Switch to the latest version for up-to-date documentation.

You will not create a new project yet. The goal is to recognize the files Kix looks for and what each one contributes. The next tutorial can then start from an empty directory without making the Nix pieces feel like magic.

A small Kix repo usually starts like this:

  • flake.nix
  • cluster.nix
  • Directorypackages/
    • my-package.nix

The first example repo is slightly more structured because it contains many numbered examples:

  • flake.nix
  • kind-config.yaml
  • Directorytutorials/
    • Directory02-hello-world/
      • README.md
      • cluster.nix

Both layouts use the same Kix idea:

  • flake.nix is the project entry point;
  • each cluster.nix file says what one cluster contains;
  • package files under packages/ hold reusable package definitions.

A flake.nix file is a Nix project definition. In a Kix repo, it answers two basic questions:

  • What does this project depend on?
  • Which named clusters does this project expose?

The Kix CLI reads that file so commands like kix list clusters and kix deploy 02-hello-world know what clusters exist.

You do not need to understand every Nix construct yet. Read this example for three landmarks: inputs, mkFlake, and clusters.

flake.nix (L2–L63)
{
description = "Kix example clusters — a tour of Kix patterns runnable on kind.";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
# Fetched via the GitHub fetcher. While the repo is private, every
# consumer (local dev, kixpkgs CI, the captures pipeline) must
# provide a token: `gh auth token` plugged into nix.conf
# `access-tokens = github.com=...`, or the workflow's auto-issued
# GITHUB_TOKEN. The kixpkgs captures pipeline rewrites this URL
# to `path:<store>` and drops flake.lock before invoking kix, so
# its sandboxed eval never hits the network. (kix's clap CLI
# doesn't forward `--override-input`, so the rewrite is the only
# in-band knob.)
kixpkgs.url = "github:adamcharnock/kixpkgs";
kixpkgs.inputs.nixpkgs.follows = "nixpkgs";
};
# Each example folder has its own cluster.nix. Add a new example by
# appending a line here and creating examples/NN-name/cluster.nix.
outputs =
inputs:
inputs.kixpkgs.lib.mkFlake {
inherit inputs;
clusters = {
"02-hello-world" = ./tutorials/02-hello-world/cluster.nix;
"06-service-dep" = ./tutorials/06-service-dep/cluster.nix;
"08-namespace-deps" = ./tutorials/08-namespace-deps/cluster.nix;
"09-typed-options" = ./tutorials/09-typed-options/cluster.nix;
"10-reuse-packages" = ./tutorials/10-reuse-packages/cluster.nix;
"19-scorecards" = ./tutorials/19-scorecards/cluster.nix;
"how-to-application" = ./how-to/application/cluster.nix;
"how-to-adoption" = ./how-to/adoption/cluster.nix;
"how-to-stateful-migration" = ./how-to/adoption/stateful-before-cluster.nix;
"how-to-adoption-takeover" = ./how-to/adoption/takeover-cluster.nix;
"how-to-helm-bridge" = ./how-to/adoption/helm-bridge-cluster.nix;
"how-to-auto-instantiation" = ./how-to/auto-instantiation/cluster.nix;
"how-to-composition" = ./how-to/composition/cluster.nix;
"how-to-fragments" = ./how-to/composition/fragments-cluster.nix;
"how-to-multi-env" = ./how-to/composition/multi-env-cluster.nix;
"how-to-package-storage" = ./how-to/package-stacks/storage-cluster.nix;
"how-to-package-python" = ./how-to/package-stacks/python-cluster.nix;
"how-to-package-stackgres" = ./how-to/package-stacks/stackgres-cluster.nix;
"how-to-package-monitoring" = ./how-to/package-stacks/monitoring-cluster.nix;
"how-to-package-cilium" = ./how-to/package-stacks/cilium-cluster.nix;
"how-to-package-valkey" = ./how-to/package-stacks/valkey-cluster.nix;
"how-to-package-velero" = ./how-to/package-stacks/velero-cluster.nix;
"how-to-package-victoria-metrics" = ./how-to/package-stacks/victoria-metrics-cluster.nix;
"how-to-platform-gateway" = ./how-to/platform/gateway-cluster.nix;
"how-to-platform-hostpath" = ./how-to/platform/hostpath-cluster.nix;
"how-to-platform-ingress" = ./how-to/platform/ingress-cluster.nix;
"how-to-platform-monitoring" = ./how-to/platform/monitoring-cluster.nix;
"how-to-platform-network-policy" = ./how-to/platform/network-policy-cluster.nix;
"how-to-platform-secrets" = ./how-to/platform/secrets-cluster.nix;
"how-to-platform-sops" = ./how-to/platform/sops-secrets-cluster.nix;
"how-to-platform-cloudflare-tunnel" = ./how-to/platform/cloudflare-tunnel-cluster.nix;
};
};
}

View source on GitHub ↗

The first block, inputs, names the external Nix dependencies:

  • nixpkgs is the normal Nix package collection;
  • kixpkgs provides Kix itself and the package catalog used by these examples;
  • kixpkgs.inputs.nixpkgs.follows = "nixpkgs"; keeps both inputs using the same nixpkgs.

The second block, outputs, says what this repo provides. Kix projects usually delegate that output wiring to inputs.kixpkgs.lib.mkFlake.

mkFlake is the Kix helper that turns the clusters map, such as "02-hello-world" = ./tutorials/02-hello-world/cluster.nix, into the flake outputs the CLI knows how to read.

A few Nix syntax notes, kept deliberately small:

  • { ... } is an attrset, like a map or object.
  • Each assignment ends with ;.
  • inherit inputs; means inputs = inputs;.
  • outputs = inputs: ... is a function. It receives the flake inputs and returns the project outputs.

The attr name, 02-hello-world, is the cluster name you pass to the CLI:

Run in kix-examples/
❱ kix deploy 02-hello-world -y Show output
Building cluster '02-hello-world'...
Cluster 02-hello-world: 10 manifests
Connecting to cluster...
No previous activation on cluster — first deploy.
Reading live cluster state...

  _cluster
    ~ cluster-level resources (4 added)
  kube-system
    + platform-dns (0 resources)
  tutorial-02
    + hello-world 1.27 (3 resources)

  Plan: cluster-level changes, 2 added
  Resources: 4 real content, 0 dep-affected
plan: 10 nodes
  ~ Namespace/kube-system configured
  ✔ Namespace/kube-system ready
  + Namespace/tutorial-02 created
  ✔ Namespace/tutorial-02 ready
  + CustomResourceDefinition/packageinstances.kix.run created
  + CustomResourceDefinition/activations.kix.run created
  + ConfigMap/hello-world@tutorial-02 created
  ✔ ConfigMap/hello-world@tutorial-02 ready
  + Deployment/hello-world@tutorial-02 created
  ✔ CustomResourceDefinition/packageinstances.kix.run ready
  ✔ CustomResourceDefinition/activations.kix.run ready
  + PackageInstance/platform-dns@kube-system created
  ✔ PackageInstance/platform-dns@kube-system ready
  ✔ Deployment/hello-world@tutorial-02 ready
  + Service/hello-world@tutorial-02 created
  ✔ Service/hello-world@tutorial-02 ready
  + PackageInstance/hello-world@tutorial-02 created
  ✔ PackageInstance/hello-world@tutorial-02 ready
  + Activation/02-hello-world-rh3pdaspsdxk created
  ✔ Activation/02-hello-world-rh3pdaspsdxk ready
  • activation '02-hello-world-rh3pdaspsdxk' → Active

Deploy complete: 9 created, 1 configured, 0 unchanged, 0 failed
Run in kix-examples/
❱ kix pf 02-hello-world hello-world 8080:80

The value points at the file that builds that cluster.

clusters = {
dev = ./clusters/dev.nix;
staging = ./clusters/staging.nix;
prod = ./clusters/prod.nix;
};

The names are yours. The CLI discovers them by evaluating the flake.

The flake points at ./tutorials/02-hello-world/cluster.nix. That file is the recipe for one cluster: it names the cluster, chooses the local kind flavor, and declares what should exist inside the cluster.

At the top level, the file has this structure:

tutorials/02-hello-world/cluster.nix (L21–L64)
{ 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 = {
# package instances go here
};
}
];
}

View source on GitHub ↗

mkFlake passes two useful arguments into this function:

  • kix, the framework helpers;
  • packages, the package catalog from kixpkgs.

That first line, { kix, packages }:, is Nix function syntax. For now, read it as “Kix gives this file the helpers and package catalog it needs.”

The cluster then calls kix.buildCluster. Inside modules, the first entry, kix.flavors.kind, says this cluster targets kind. The second entry is a small cluster module that creates one namespace and leaves room for package instances.

The package instance lives under instances.tutorial-02:

tutorials/02-hello-world/cluster.nix (L48–L59)
hello-world = {
# `packages.echo-server` ships in the public kixpkgs
# repository, along with many others.
# It serves a simple fixed message over HTTP.
package = packages.echo-server;
# `config` is whatever the package's options accept.
# echo-server's `message` becomes the body of every response.
config = {
message = "Hello from Kix!";
};
};

View source on GitHub ↗

That instance defines four practical facts:

  • namespace: tutorial-02;
  • instance: hello-world;
  • package: packages.echo-server;
  • config: the message returned by the echo server.

That is enough for Kix to build Kubernetes manifests and enough for the CLI to know there is a package instance named hello-world.

There are two package sources to keep separate.

Catalog packages come from kixpkgs. In the first example, packages.echo-server is a catalog package. You use it directly from the packages argument.

Local packages live in your repo. The second example imports one:

tutorials/06-service-dep/cluster.nix (L32–L34)
let
reverseProxyPackage = import ./reverse-proxy-package.nix;
in

View source on GitHub ↗

You will build a local package later in the tutorial path. For now, the rule of thumb is:

  • put cluster composition in cluster.nix;
  • put reusable package code in a separate package file;
  • keep shared cluster modules in their own files once cluster.nix gets too large.

That keeps “what is installed here?” separate from “how does this package build Kubernetes resources?”

When you run a command from the repo root:

Run in kix-examples/
❱ kix list clusters
 NAME                              
 02-hello-world                    
 06-service-dep                    
 08-namespace-deps                 
 09-typed-options                  
 10-reuse-packages                 
 19-scorecards                     
 how-to-adoption                   
 how-to-adoption-takeover          
 how-to-application                
 how-to-auto-instantiation         
 how-to-composition                
 how-to-fragments                  
 how-to-helm-bridge                
 how-to-multi-env                  
 how-to-package-cilium             
 how-to-package-monitoring         
 how-to-package-python             
 how-to-package-stackgres          
 how-to-package-storage            
 how-to-package-valkey             
 how-to-package-velero             
 how-to-package-victoria-metrics   
 how-to-platform-cloudflare-tunnel 
 how-to-platform-gateway           
 how-to-platform-hostpath          
 how-to-platform-ingress           
 how-to-platform-monitoring        
 how-to-platform-network-policy    
 how-to-platform-secrets           
 how-to-platform-sops              
 how-to-stateful-migration

Kix evaluates the current directory’s flake. If you pass --flake, it evaluates that flake instead:

❱ kix --flake /path/to/kix-examples list clusters

From the flake outputs, the CLI can discover:

  • cluster names;
  • package catalog metadata;
  • rendered manifests for a cluster;
  • installed package instances inside a cluster.

That is why the command names in the previous tutorial were short. Once Kix has the flake and cluster name, it can find the rest.

You now know what flake.nix contributes and what cluster.nix contributes: the flake exposes clusters, and each cluster file says what to build.

The next tutorial uses these files to create the first cluster from scratch.