Skip to content
Install the CLI

Tutorial 07

Package anatomy and safe resource composition

In tutorial 06, you ran a local reverse-proxy package (tutorials/06-service-dep/reverse-proxy-package.nix) and saw how it reads backend.out.fqdn and backend.out.port.

This tutorial opens that package file and walks through how a Kix package is put together: how it declares metadata and options, how it constructs resources inside an instance scope, how it mounts a ConfigMap into a Deployment without string drift, and how it exposes a Service as its public contract.

Open tutorials/06-service-dep/reverse-proxy-package.nix. A Kix package is a Nix attribute set (or a function returning an attribute set) built around three main sections:

tutorials/06-service-dep/reverse-proxy-package.nix
{
meta = { ... };
# options = { ... }; # added in tutorial 09 when a package takes config
build = { self, scope, kix, backend, ... }: { ... };
}

Each section has a distinct job:

SectionPurpose
metaStatic facts about the package (version, description, roles, defaultAliases, provides, and compatibility bounds) that kix list packages and scorecards can inspect without building the package
optionsOptional typed configuration options (introduced in tutorial 09) that a cluster can set in config = { ... }
buildA function (or list of functions) that takes resolved dependencies, config, scope, and kix, and returns the Kubernetes resources and root entry for one instance

At the top of tutorials/06-service-dep/reverse-proxy-package.nix, the meta block records the package version and description:

tutorials/06-service-dep/reverse-proxy-package.nix
meta = {
version = "0.0.1";
description = "nginx reverse-proxy in front of a backend Service";
};

View source on GitHub ↗

Later tutorials add provides = [ "reverse-proxy" ]; to meta so scorecard rules can target every reverse proxy in a cluster, regardless of what the instance is named.

Next, look at the argument list of build:

tutorials/06-service-dep/reverse-proxy-package.nix
build =
{
self,
scope,
kix,
backend,
...
}:

View source on GitHub ↗

Kix calls build once for each instance of the package in the cluster. The arguments fall into two groups:

  1. Framework arguments (self, config, clusterConfig, scope, lib, kix, deps, and availability): always available to any build function.
  2. Dependency arguments (backend in this package): any argument name that is not one of those eight built-in framework arguments tells Kix that this package requires another instance.

Because backend does not have a ? null default in the function header, Kix treats it as a required dependency. If a cluster installs this package without a matching backend instance or explicit deps.backend binding, evaluation fails before any YAML is rendered.

Inside build, the package creates its nginx configuration ConfigMap using scope.mkResource:

tutorials/06-service-dep/reverse-proxy-package.nix
configmap = scope.mkResource {
apiVersion = "v1";
kind = "ConfigMap";
inherit name;
data."default.conf" = ''
server {
listen 80;
location /healthz {
default_type text/plain;
return 200 'ok\n';
}
location / {
proxy_pass http://${backend.out.fqdn}:${backend.out.port}/;
}
}
'';
};

View source on GitHub ↗

Why call scope.mkResource instead of writing a plain Nix attribute set?

  • scope.mkResource stamps the resource with the instance’s metadata.namespace and standard app.kubernetes.io/* labels automatically.
  • It validates the manifest shape and records an identity hash for the resource.
  • It attaches a .out record (self.configmap.out.name, self.configmap.out.fqdn, and self.configmap.out.identityHash) so other resources in the same package can reference it safely.

Notice that data."default.conf" interpolates ${backend.out.fqdn} and ${backend.out.port}. Because those strings come from backend.out, Kix automatically records that self.configmap depends on backend’s Service.

Mount The ConfigMap And Build The Deployment

Section titled “Mount The ConfigMap And Build The Deployment”

Next, the package builds a Deployment that runs nginx and mounts self.configmap at /etc/nginx/conf.d:

tutorials/06-service-dep/reverse-proxy-package.nix
deployment =
{
inherit name;
spec = {
replicas = 1;
selector.matchLabels = scope.selectorLabels;
template.spec = {
containers = [
{
name = "nginx";
image = "nginx:1.27-alpine";
imagePullPolicy = "IfNotPresent";
# `kix.port "http" 80` is shorthand for
# `{ name = "http"; containerPort = 80; }`.
ports = [ (kix.port "http" 80) ];
readinessProbe = {
httpGet = {
path = "/healthz";
port = 80;
};
periodSeconds = 5;
};
volumeMounts = [
{
name = "config";
mountPath = "/etc/nginx/conf.d";
readOnly = true;
}
];
}
];
volumes = [
{
name = "config";
configMap.name = self.configmap.out.name;
}
];
};
};
}
|> scope.mkDeployment;

View source on GitHub ↗

Three pieces work together here:

  1. self.configmap.out.name via the self fixpoint: self refers to the attribute set returned by build. Referencing self.configmap.out.name in volumes ensures the volume reference and the ConfigMap resource name cannot drift apart, and records a dependency from the Deployment to the ConfigMap so ConfigMap changes trigger a Deployment rollout. (When a workload mounts several volumes, you can also use kix.withMounts [ (kix.mount.configMap self.configmap { mountPath = "/etc/nginx/conf.d"; readOnly = true; }) ] around the workload attribute set to populate both volumes and volumeMounts from one declaration.)
  2. scope.selectorLabels and scope.mkDeployment: scope.mkDeployment wraps the workload spec into an apps/v1 Deployment. It requires spec.selector.matchLabels (throwing an error if omitted, so you pass scope.selectorLabels), copies those labels onto spec.template.metadata.labels when pod template labels are not set explicitly, and exposes self.deployment.out.selector for downstream Services.
  3. The |> pipe operator: expr |> scope.mkDeployment is left-to-right function application, equivalent to scope.mkDeployment expr.

To expose the Deployment inside the cluster, the package creates a Service using kix.service.fromWorkload:

tutorials/06-service-dep/reverse-proxy-package.nix
service =
self.deployment
|> kix.service.fromWorkload {
inherit name;
ports = [
{
name = "http";
port = 80;
targetPort = "http";
protocol = "TCP";
}
];
}
|> scope.mkResource;

View source on GitHub ↗

In raw Kubernetes YAML, a Service can silently fail to route traffic if its spec.selector labels or targetPort names do not match the Deployment’s pod template labels and containerPort declarations.

kix.service.fromWorkload cfg workload takes a Service configuration attribute set and the built Deployment:

  • spec.selector is copied from self.deployment.out.selector;
  • spec.ports uses the ports list in cfg (checking that any named targetPort such as "http" exists on self.deployment’s containers and resolving it to the container port number), or extracts ports directly from the workload’s container specs when ports is omitted;
  • because spec.selector reads self.deployment.out.selector, the Service records a dependency on self.deployment in kix.run/depends-on, so kix deploy applies and waits for the Deployment before applying the Service.

At the bottom of build, a single line chooses the package’s root resource:

tutorials/06-service-dep/reverse-proxy-package.nix
root = self.service;

View source on GitHub ↗

Setting root = self.service does two things:

  1. When another package depends on gateway, gateway.out forwards the .out fields of self.service (gateway.out.name, gateway.out.fqdn, gateway.out.port, gateway.out.ports, gateway.out.url, and so on). If a package needs to expose additional custom .out fields beyond what the root resource provides, it can pass an attribute set instead: root = { resource = self.service; out = { ... }; };. Catalog packages that provide shared backing services (such as PostgreSQL databases, Redis caches, or OpenTelemetry receivers) validate root.out against built-in dependency contracts (kix.contracts.*), which you will meet in tutorial 11.
  2. During deployment, the instance’s PackageInstance resource waits on root, and any resource in build that is reachable from root is included in the package’s built resources.

Run kix build 06-service-dep and look at the rendered gateway ConfigMap, Deployment, and Service:

kix-examples/ offline capture
❱ kix build 06-service-dep
Show outputHide output · 429 lines
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    kix.run/identity-hash: gvrlspcfgh9a2qnsx5kqxw9f4l3x52jj
    kix.run/package: _cluster
    kix.run/package-namespace: _cluster
  labels:
    app.kubernetes.io/managed-by: kix
  name: activations.kix.run
spec:
  group: kix.run
  names:
    kind: Activation
    listKind: ActivationList
    plural: activations
    singular: activation
  scope: Cluster
  versions:
  - name: v1alpha1
    schema:
      openAPIV3Schema:
        properties:
          apiVersion:
            type: string
          kind:
            type: string
          metadata:
            type: object
          spec:
            type: object
            x-kubernetes-preserve-unknown-fields: true
          status:
            type: object
            x-kubernetes-preserve-unknown-fields: true
        type: object
    served: true
    storage: true
    subresources:
      status: {}
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    kix.run/identity-hash: pk4hih6jm4yj336rlaqk2qycz2k46xvs
    kix.run/package: _cluster
    kix.run/package-namespace: _cluster
  labels:
    app.kubernetes.io/managed-by: kix
  name: packageinstances.kix.run
spec:
  group: kix.run
  names:
    kind: PackageInstance
    listKind: PackageInstanceList
    plural: packageinstances
    singular: packageinstance
  scope: Namespaced
  versions:
  - name: v1alpha1
    schema:
      openAPIV3Schema:
        properties:
          apiVersion:
            type: string
          kind:
            type: string
          metadata:
            type: object
          spec:
            type: object
            x-kubernetes-preserve-unknown-fields: true
          status:
            type: object
            x-kubernetes-preserve-unknown-fields: true
        type: object
    served: true
    storage: true
    subresources:
      status: {}
---
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    kix.run/depends-on: 1wz2372bgznyyc56m28alsibkgfcq4dc,wlzmynapx327962105rsvvnmaqyw2vyn
    kix.run/identity-hash: j8cml44phbwvsadkgynhg26k4cjh6z41
    kix.run/package: backend
    kix.run/package-namespace: tutorial-06
  labels:
    app.kubernetes.io/instance: backend
    app.kubernetes.io/managed-by: kix
    app.kubernetes.io/name: backend
  name: backend
  namespace: tutorial-06
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/instance: backend
      app.kubernetes.io/name: backend
  template:
    metadata:
      labels:
        app.kubernetes.io/instance: backend
        app.kubernetes.io/name: backend
    spec:
      containers:
      - image: docker.io/nginxinc/nginx-unprivileged:1.30.5-alpine@sha256:ed04ec1ff34502c339ee5c3ae3f855442398edc1d05591e2b98981dcbbd20b1e
        imagePullPolicy: IfNotPresent
        livenessProbe:
          failureThreshold: 3
          httpGet:
            path: /healthz
            port: 8080
          periodSeconds: 10
        name: nginx
        ports:
        - containerPort: 8080
          name: http
          protocol: TCP
        readinessProbe:
          failureThreshold: 1
          httpGet:
            path: /healthz
            port: 8080
          periodSeconds: 5
        resources:
          limits:
            cpu: 100m
            memory: 64Mi
          requests:
            cpu: 50m
            memory: 32Mi
        securityContext:
          readOnlyRootFilesystem: true
          runAsNonRoot: true
        volumeMounts:
        - mountPath: /etc/nginx/conf.d
          name: config
          readOnly: true
        - mountPath: /tmp
          name: tmp
      terminationGracePeriodSeconds: 30
      volumes:
      - configMap:
          name: backend
        name: config
      - emptyDir: {}
        name: tmp
---
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    kix.run/depends-on: 1wz2372bgznyyc56m28alsibkgfcq4dc,45s9v6ay46lbfgdqay8db07z3i35p7dm,requires:jh345cnnffy1bnj4cz5x0hwq6ibxvkkr
    kix.run/identity-hash: ny8470iq2gln3qlfk4sqi7vi9959mw1h
    kix.run/package: gateway
    kix.run/package-namespace: tutorial-06
  labels:
    app.kubernetes.io/instance: gateway
    app.kubernetes.io/managed-by: kix
    app.kubernetes.io/name: gateway
  name: gateway
  namespace: tutorial-06
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/instance: gateway
      app.kubernetes.io/name: gateway
  template:
    metadata:
      labels:
        app.kubernetes.io/instance: gateway
        app.kubernetes.io/name: gateway
    spec:
      containers:
      - image: nginx:1.27-alpine
        imagePullPolicy: IfNotPresent
        name: nginx
        ports:
        - containerPort: 80
          name: http
          protocol: TCP
        readinessProbe:
          httpGet:
            path: /healthz
            port: 80
          periodSeconds: 5
        volumeMounts:
        - mountPath: /etc/nginx/conf.d
          name: config
          readOnly: true
      volumes:
      - configMap:
          name: gateway
        name: config
---
apiVersion: kix.run/v1alpha1
kind: Activation
metadata:
  annotations:
    kix.run/built-via: /nix/store/iaj67mpx1n0hjc3xwf2s670lrhnjyy4r-kix-activation-06-service-dep.json
    kix.run/depends-on: jh345cnnffy1bnj4cz5x0hwq6ibxvkkr,qi1g33g63yg34a53ihk2p71azvjk5vrw,r5sgwjk8w3m41l0xaj0s16xxsacd0nk6,wqbjg92xkv0b7xr9bv0zwd0vqpm1wkxc,requires:gvrlspcfgh9a2qnsx5kqxw9f4l3x52jj
    kix.run/identity-hash: d04a15rmk2v6a4124n5m2m0k48irmdj4
    kix.run/package: _cluster
    kix.run/package-namespace: _cluster
  labels:
    app.kubernetes.io/managed-by: kix
    kix.run/cluster: 06-service-dep
  name: 06-service-dep-d04a15rmk2v6
spec:
  instances:
    kube-system:
      platform-dns: platform-dns
      platform-storage: platform-storage
    tutorial-06:
      backend: backend
      gateway: gateway
---
apiVersion: kix.run/v1alpha1
kind: PackageInstance
metadata:
  annotations:
    kix.run/depends-on: sj0fv5bs2hnh9zcqdh62ph34l2wm0b1m,requires:pk4hih6jm4yj336rlaqk2qycz2k46xvs
    kix.run/identity-hash: jh345cnnffy1bnj4cz5x0hwq6ibxvkkr
    kix.run/import-apiversion: apps/v1
    kix.run/import-fqdn: kube-dns.kube-system.svc.cluster.local
    kix.run/import-kind: Deployment
    kix.run/import-name: coredns
    kix.run/mode: import
    kix.run/package: _cluster
    kix.run/package-namespace: _cluster
  labels:
    app.kubernetes.io/instance: platform-dns
    app.kubernetes.io/managed-by: kix
  name: platform-dns
  namespace: kube-system
spec:
  instanceName: platform-dns
  mode: import
  namespaceName: kube-system
---
apiVersion: kix.run/v1alpha1
kind: PackageInstance
metadata:
  annotations:
    kix.run/depends-on: sj0fv5bs2hnh9zcqdh62ph34l2wm0b1m,requires:pk4hih6jm4yj336rlaqk2qycz2k46xvs
    kix.run/identity-hash: r5sgwjk8w3m41l0xaj0s16xxsacd0nk6
    kix.run/import-apiversion: storage.k8s.io/v1
    kix.run/import-kind: StorageClass
    kix.run/import-name: standard
    kix.run/mode: import
    kix.run/package: _cluster
    kix.run/package-namespace: _cluster
  labels:
    app.kubernetes.io/instance: platform-storage
    app.kubernetes.io/managed-by: kix
  name: platform-storage
  namespace: kube-system
spec:
  instanceName: platform-storage
  mode: import
  namespaceName: kube-system
---
apiVersion: kix.run/v1alpha1
kind: PackageInstance
metadata:
  annotations:
    kix.run/depends-on: 1wz2372bgznyyc56m28alsibkgfcq4dc,pldbj73ylma3pwnh69c1i02dkbmhqrwl,requires:pk4hih6jm4yj336rlaqk2qycz2k46xvs
    kix.run/identity-hash: qi1g33g63yg34a53ihk2p71azvjk5vrw
    kix.run/package: _cluster
    kix.run/package-namespace: _cluster
    kix.run/root: backend
  labels:
    app.kubernetes.io/instance: backend
    app.kubernetes.io/managed-by: kix
  name: backend
  namespace: tutorial-06
spec:
  instanceName: backend
  namespaceName: tutorial-06
  version: "1.30.5"
---
apiVersion: kix.run/v1alpha1
kind: PackageInstance
metadata:
  annotations:
    kix.run/depends-on: 1wz2372bgznyyc56m28alsibkgfcq4dc,y5zxg5jkkwxrl1jrln37hqnfywd4s9q1,requires:pk4hih6jm4yj336rlaqk2qycz2k46xvs
    kix.run/identity-hash: wqbjg92xkv0b7xr9bv0zwd0vqpm1wkxc
    kix.run/package: _cluster
    kix.run/package-namespace: _cluster
    kix.run/root: gateway
  labels:
    app.kubernetes.io/instance: gateway
    app.kubernetes.io/managed-by: kix
  name: gateway
  namespace: tutorial-06
spec:
  instanceName: gateway
  namespaceName: tutorial-06
  version: "0.0.1"
---
apiVersion: v1
data:
  default.conf: |
    server {
        listen 8080;
        location / {
            default_type text/plain;
            return 200 'Hello from the backend!\n';
        }
        location /healthz {
            default_type text/plain;
            return 200 'ok\n';
        }
    }
kind: ConfigMap
metadata:
  annotations:
    kix.run/depends-on: 1wz2372bgznyyc56m28alsibkgfcq4dc
    kix.run/identity-hash: wlzmynapx327962105rsvvnmaqyw2vyn
    kix.run/package: backend
    kix.run/package-namespace: tutorial-06
  labels:
    app.kubernetes.io/instance: backend
    app.kubernetes.io/managed-by: kix
  name: backend
  namespace: tutorial-06
---
apiVersion: v1
data:
  default.conf: |
    server {
        listen 80;
        location /healthz {
            default_type text/plain;
            return 200 'ok\n';
        }
        location / {
            proxy_pass http://backend.tutorial-06.svc.cluster.local:80/;
        }
    }
kind: ConfigMap
metadata:
  annotations:
    kix.run/depends-on: 1wz2372bgznyyc56m28alsibkgfcq4dc,pldbj73ylma3pwnh69c1i02dkbmhqrwl
    kix.run/identity-hash: 45s9v6ay46lbfgdqay8db07z3i35p7dm
    kix.run/package: gateway
    kix.run/package-namespace: tutorial-06
  labels:
    app.kubernetes.io/instance: gateway
    app.kubernetes.io/managed-by: kix
  name: gateway
  namespace: tutorial-06
---
apiVersion: v1
kind: Namespace
metadata:
  annotations:
    kix.run/identity-hash: sj0fv5bs2hnh9zcqdh62ph34l2wm0b1m
    kix.run/package: _cluster
    kix.run/package-namespace: _cluster
  labels:
    app.kubernetes.io/managed-by: kix
    kubernetes.io/metadata.name: kube-system
  name: kube-system
---
apiVersion: v1
kind: Namespace
metadata:
  annotations:
    kix.run/identity-hash: 1wz2372bgznyyc56m28alsibkgfcq4dc
    kix.run/package: _cluster
    kix.run/package-namespace: _cluster
  labels:
    app.kubernetes.io/managed-by: kix
    kubernetes.io/metadata.name: tutorial-06
  name: tutorial-06
---
apiVersion: v1
kind: Service
metadata:
  annotations:
    kix.run/depends-on: 1wz2372bgznyyc56m28alsibkgfcq4dc,j8cml44phbwvsadkgynhg26k4cjh6z41
    kix.run/identity-hash: pldbj73ylma3pwnh69c1i02dkbmhqrwl
    kix.run/package: backend
    kix.run/package-namespace: tutorial-06
  labels:
    app.kubernetes.io/instance: backend
    app.kubernetes.io/managed-by: kix
  name: backend
  namespace: tutorial-06
spec:
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: 8080
  selector:
    app.kubernetes.io/instance: backend
    app.kubernetes.io/name: backend
  type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
  annotations:
    kix.run/depends-on: 1wz2372bgznyyc56m28alsibkgfcq4dc,ny8470iq2gln3qlfk4sqi7vi9959mw1h
    kix.run/identity-hash: y5zxg5jkkwxrl1jrln37hqnfywd4s9q1
    kix.run/package: gateway
    kix.run/package-namespace: tutorial-06
  labels:
    app.kubernetes.io/instance: gateway
    app.kubernetes.io/managed-by: kix
  name: gateway
  namespace: tutorial-06
spec:
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app.kubernetes.io/instance: gateway
    app.kubernetes.io/name: gateway
  type: ClusterIP

Check three details in the rendered YAML for gateway:

  1. Matching selector labels: Both Deployment/gateway and Service/gateway carry app.kubernetes.io/instance: gateway and app.kubernetes.io/name: gateway, set via scope.selectorLabels on the Deployment and copied by kix.service.fromWorkload.
  2. Matching volume and mount: Deployment/gateway has both volumes (configMap.name: gateway, from self.configmap.out.name) and volumeMounts (mountPath: /etc/nginx/conf.d).
  3. Automatic kix.run/depends-on annotations: Deployment/gateway lists the identity hash of ConfigMap/gateway in its kix.run/depends-on annotation, and Service/gateway lists the identity hash of Deployment/gateway. You did not write those hashes; Kix tracked them from self.configmap.out.name and self.deployment.out.selector.

You inspected every part of a local Kix package:

  • a package is a Nix attribute set with meta, optional options, and build
  • build receives framework helpers (self, config, clusterConfig, scope, lib, kix, deps, and availability) plus any dependencies declared by parameter name
  • scope.mkResource and scope.mkDeployment stamp namespace and instance labels and attach .out records
  • self.configmap.out.name (or kix.withMounts with kix.mount.configMap) wires ConfigMap volumes to workloads and records the ConfigMap dependency
  • kix.service.fromWorkload derives Service selectors and verifies or extracts container ports directly from a workload
  • root = self.service (or root = { resource = self.service; out = { ... }; }) defines the .out contract that downstream packages see

In the next tutorial, you will move backend and gateway into separate namespaces and learn how Kix handles dependency resolution across namespace boundaries.