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.
Prerequisites
Section titled “Prerequisites”- Finish the local package and service dependency tutorial.
- Keep the
kix-examplescheckout open in your editor.
The Parts Of A Package
Section titled “The Parts Of A Package”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:
1{2 meta = { ... };3 # options = { ... }; # added in tutorial 09 when a package takes config4 build = { self, scope, kix, backend, ... }: { ... };5}Each section has a distinct job:
| Section | Purpose |
|---|---|
meta | Static facts about the package (version, description, roles, defaultAliases, provides, and compatibility bounds) that kix list packages and scorecards can inspect without building the package |
options | Optional typed configuration options (introduced in tutorial 09) that a cluster can set in config = { ... } |
build | A function (or list of functions) that takes resolved dependencies, config, scope, and kix, and returns the Kubernetes resources and root entry for one instance |
Inspect meta
Section titled “Inspect meta”At the top of tutorials/06-service-dep/reverse-proxy-package.nix, the meta block records the package version and description:
16 meta = {17 version = "0.0.1";18 description = "nginx reverse-proxy in front of a backend Service";19 };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.
Inspect The build Arguments
Section titled “Inspect The build Arguments”Next, look at the argument list of build:
41 build =42 {43 self,44 scope,45 kix,46 backend,47 ...48 }:Kix calls build once for each instance of the package in the cluster. The arguments fall into two groups:
- Framework arguments (
self,config,clusterConfig,scope,lib,kix,deps, andavailability): always available to anybuildfunction. - Dependency arguments (
backendin 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.
Build The ConfigMap With scope.mkResource
Section titled “Build The ConfigMap With scope.mkResource”Inside build, the package creates its nginx configuration ConfigMap using scope.mkResource:
72 configmap = scope.mkResource {73 apiVersion = "v1";74 kind = "ConfigMap";75 inherit name;76 data."default.conf" = ''77 server {78 listen 80;79 location /healthz {80 default_type text/plain;81 return 200 'ok\n';82 }83 location / {84 proxy_pass http://${backend.out.fqdn}:${backend.out.port}/;85 }86 }87 '';88 };Why call scope.mkResource instead of writing a plain Nix attribute set?
scope.mkResourcestamps the resource with the instance’smetadata.namespaceand standardapp.kubernetes.io/*labels automatically.- It validates the manifest shape and records an identity hash for the resource.
- It attaches a
.outrecord (self.configmap.out.name,self.configmap.out.fqdn, andself.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:
109 deployment =110 {111 inherit name;112 spec = {113 replicas = 1;114 selector.matchLabels = scope.selectorLabels;115 template.spec = {116 containers = [117 {118 name = "nginx";119 image = "nginx:1.27-alpine";120 imagePullPolicy = "IfNotPresent";121 # `kix.port "http" 80` is shorthand for122 # `{ name = "http"; containerPort = 80; }`.123 ports = [ (kix.port "http" 80) ];124 readinessProbe = {125 httpGet = {126 path = "/healthz";127 port = 80;128 };129 periodSeconds = 5;130 };131 volumeMounts = [132 {133 name = "config";134 mountPath = "/etc/nginx/conf.d";135 readOnly = true;136 }137 ];138 }139 ];140 volumes = [141 {142 name = "config";143 configMap.name = self.configmap.out.name;144 }145 ];146 };147 };148 }149 |> scope.mkDeployment;Three pieces work together here:
self.configmap.out.namevia theselffixpoint:selfrefers to the attribute set returned bybuild. Referencingself.configmap.out.nameinvolumesensures 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 usekix.withMounts [ (kix.mount.configMap self.configmap { mountPath = "/etc/nginx/conf.d"; readOnly = true; }) ]around the workload attribute set to populate bothvolumesandvolumeMountsfrom one declaration.)scope.selectorLabelsandscope.mkDeployment:scope.mkDeploymentwraps the workload spec into anapps/v1Deployment. It requiresspec.selector.matchLabels(throwing an error if omitted, so you passscope.selectorLabels), copies those labels ontospec.template.metadata.labelswhen pod template labels are not set explicitly, and exposesself.deployment.out.selectorfor downstream Services.- The
|>pipe operator:expr |> scope.mkDeploymentis left-to-right function application, equivalent toscope.mkDeployment expr.
Derive The Service From The Workload
Section titled “Derive The Service From The Workload”To expose the Deployment inside the cluster, the package creates a Service using kix.service.fromWorkload:
159 service =160 self.deployment161 |> kix.service.fromWorkload {162 inherit name;163 ports = [164 {165 name = "http";166 port = 80;167 targetPort = "http";168 protocol = "TCP";169 }170 ];171 }172 |> scope.mkResource;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.selectoris copied fromself.deployment.out.selector;spec.portsuses theportslist incfg(checking that any namedtargetPortsuch as"http"exists onself.deployment’s containers and resolving it to the container port number), or extracts ports directly from the workload’s container specs whenportsis omitted;- because
spec.selectorreadsself.deployment.out.selector, the Service records a dependency onself.deploymentinkix.run/depends-on, sokix deployapplies and waits for the Deployment before applying the Service.
Expose The Public Contract With root
Section titled “Expose The Public Contract With root”At the bottom of build, a single line chooses the package’s root resource:
183 root = self.service;Setting root = self.service does two things:
- When another package depends on
gateway,gateway.outforwards the.outfields ofself.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.outfields 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) validateroot.outagainst built-in dependency contracts (kix.contracts.*), which you will meet in tutorial 11. - During deployment, the instance’s
PackageInstanceresource waits onroot, and any resource inbuildthat is reachable fromrootis included in the package’s built resources.
Verify The Rendered Resources
Section titled “Verify The Rendered Resources”Run kix build 06-service-dep and look at the rendered gateway ConfigMap, Deployment, and Service:
❱ 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:
- Matching selector labels: Both
Deployment/gatewayandService/gatewaycarryapp.kubernetes.io/instance: gatewayandapp.kubernetes.io/name: gateway, set viascope.selectorLabelson the Deployment and copied bykix.service.fromWorkload. - Matching volume and mount:
Deployment/gatewayhas bothvolumes(configMap.name: gateway, fromself.configmap.out.name) andvolumeMounts(mountPath: /etc/nginx/conf.d). - Automatic
kix.run/depends-onannotations:Deployment/gatewaylists the identity hash ofConfigMap/gatewayin itskix.run/depends-onannotation, andService/gatewaylists the identity hash ofDeployment/gateway. You did not write those hashes; Kix tracked them fromself.configmap.out.nameandself.deployment.out.selector.
What You Learned
Section titled “What You Learned”You inspected every part of a local Kix package:
- a package is a Nix attribute set with
meta, optionaloptions, andbuild -
buildreceives framework helpers (self,config,clusterConfig,scope,lib,kix,deps, andavailability) plus any dependencies declared by parameter name -
scope.mkResourceandscope.mkDeploymentstamp namespace and instance labels and attach.outrecords -
self.configmap.out.name(orkix.withMountswithkix.mount.configMap) wires ConfigMap volumes to workloads and records the ConfigMap dependency -
kix.service.fromWorkloadderives Service selectors and verifies or extracts container ports directly from a workload -
root = self.service(orroot = { resource = self.service; out = { ... }; }) defines the.outcontract 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.