Tutorial 12
Scorecards and compile-time policy
In tutorial 09, you used lib.mkOption and lib.types to validate each option on a single package instance. Some policies, however, belong to the platform team rather than to an individual package author:
- “Every reverse-proxy gateway in our production cluster must set
maxBodySizeexplicitly rather than relying on the1mdefault.” - “No two reverse-proxy gateways across the entire cluster may claim the same
proxyPath.”
In Kix, you enforce these rules at build time using scorecards. Because scorecards evaluate inside Nix against the typed package options, metadata, and full cluster graph, they can check cross-instance invariants that single-manifest YAML linters cannot see.
Prerequisites
Section titled “Prerequisites”- Finish the flavors, roles, and imports tutorial.
- Keep the same
kix-examplescheckout.
Inspect 19-scorecards
Section titled “Inspect 19-scorecards”Open:
1tutorials/19-scorecards/cluster.nix2tutorials/19-scorecards/scorecard.nixIn tutorials/19-scorecards/cluster.nix, the cluster defines two application namespaces (app-v1 and app-v2), each with a backend and a gateway instance:
app-v1.gatewaysetsproxyPath = "/v1/"andmaxBodySize = "10m".app-v2.gatewaysetsproxyPath = "/v2/"andmaxBodySize = "10m".
At the top of cluster.nix, the cluster module inside modules sets scorecard by importing ./scorecard.nix:
kix.buildCluster { name = "19-scorecards";
modules = [ kix.flavors.kind
# `lib` is injected by the NixOS module system — we pull it in # here so we can hand it to ./scorecard.nix on import. ( { ref, lib, ... }: { # ── Scorecard ──────────────────────────────────────────────── # The whole rules block lives in ./scorecard.nix. cluster.nix # stays focused on what the cluster *contains*; the rules file # owns the checks we want to run against it. scorecard = import ./scorecard.nix { inherit lib kix; };
# ... } ) ];}Keeping scorecard policy in a separate file (./scorecard.nix) lets platform teams share a single policy definition across multiple clusters while keeping cluster.nix focused on topology and instances.
Enable Built-In Rules And Severity Controls
Section titled “Enable Built-In Rules And Severity Controls”Open tutorials/19-scorecards/scorecard.nix. At the top of the file, enable, ruleOverrides, and rules configure what the scorecard checks:
{ enable = true;
# Per-rule severity override. Key is `<category>.<rule>`. # Raises a built-in from "warning" to "error" so the build fails # if a probe is missing. ruleOverrides.byRule."reliability.hasProbes".severity = "error";
rules = { # 1. Whole built-in category. Bundles hasProbes, # boundedResources, hasResourceLimits, hasResourceRequests, # gracefulShutdown, hasPDB. reliability = kix.rules.reliability;
# custom rules ... };}Here is what each field controls:
enable = true: Turns on scorecard evaluation duringkix checkandkix build.ruleOverrides.byRule."reliability.hasProbes".severity = "error": Overrides the severity of a single rule (<category>.<rule>) from"warning"to"error".rules = { reliability = kix.rules.reliability; ... }: Selects Kix’s built-inreliabilityrule category (kix.rules.reliability, alongsidekix.rules.security,kix.rules.governance, andkix.rules.architecture) and adds custom rules for this cluster.maxSeverity(optional, defaults to"warning"): Caps the effective severity of every finding in the cluster to"info","warning", or"error". Because the default is"warning", findings from error-severity rules are capped to warnings unless the cluster also setsmaxSeverity = "error".
Write A Package-Level Rule
Section titled “Write A Package-Level Rule”Next, look at the first custom rule in tutorials/19-scorecards/scorecard.nix:
52 custom.proxyMaxBodySizeMustBeExplicit = {53 description = "Reverse-proxy gateways must set maxBodySize explicitly (not the 1m default).";54 level = "package";55 severity = "error";56 tags = [ "reliability" "kix-examples" ];57
58 # Only run on reverse-proxy packages. `appliesTo` receives the59 # package metadata directly, so this checks `meta.provides`.60 appliesTo =61 { meta, ... }:62 builtins.elem "reverse-proxy" (meta.provides or [ ]);63
64 check =65 { config, instanceName, ... }:66 if (config.maxBodySize or "") == "1m" then67 [68 {69 message =70 "instance '${instanceName}' uses the default maxBodySize '1m'. "71 + "nginx rejects larger request bodies — set this explicitly.";72 }73 ]74 else75 [ ];76 };A level = "package" rule runs once per package instance and inspects the package’s meta, config, and built resources:
appliesTo = { meta, ... }: ...: Filters which instances this rule checks. Becausereverse-proxy-package.nixdeclaresmeta.provides = [ "reverse-proxy" ],builtins.elem "reverse-proxy" (meta.provides or [ ])runs this rule only on reverse-proxy instances and skipsecho-serverbackends.check = { config, instanceName, ... }: ...: Inspects the instance’s mergedconfigdirectly. You do not have to parse nginx’sdefault.conftext out of a rendered ConfigMap; you readconfig.maxBodySizeas a structured Nix value. If it equals"1m",checkreturns a finding with a descriptive message.
Write A Cluster-Level Rule
Section titled “Write A Cluster-Level Rule”Some mistakes only appear when you compare multiple instances across the cluster. If app-v1/gateway and app-v2/gateway both configure proxyPath = "/v1/", each instance looks completely valid in isolation, yet their routes collide across the cluster.
To catch that, tutorials/19-scorecards/scorecard.nix defines a helper predicate isProxy and a level = "cluster" rule named custom.noProxyPathCollision:
26 isProxy =27 inst:28 # Cluster-level rules inspect built instances, so the package29 # metadata is under `inst._meta.packageMeta`.30 builtins.elem "reverse-proxy" (inst._meta.packageMeta.provides or [ ]);82 custom.noProxyPathCollision = {83 description = "No two reverse-proxy gateways may share the same proxyPath.";84 level = "cluster";85 severity = "error";86 tags = [87 "routing"88 "kix-examples"89 ];90
91 check =92 { allInstances, ... }:93 let94 # `allInstances` is `{ <ns> = { <name> = instance; }; }`.95 # Flatten into a list of { ns, name, inst } records.96 #97 # `lib.concatLists` is the nixpkgs equivalent of JS's98 # `arr.flat()` — flatten a list of lists into one list.99 # `builtins.attrNames` returns the keys of an attrset.100 flat = lib.concatLists (101 map (102 ns:103 map (name: {104 inherit ns name;105 inst = allInstances.${ns}.${name};106 }) (builtins.attrNames allInstances.${ns})107 ) (builtins.attrNames allInstances)108 );109
110 # Keep only reverse-proxy instances.111 proxies = builtins.filter (e: isProxy e.inst) flat;112
113 # Group by proxyPath → { "/v1/" = [...]; "/v2/" = [...]; }.114 byPath = builtins.foldl' (115 acc: e:116 let117 p = e.inst._meta.config.proxyPath or "?";118 in119 acc // { ${p} = (acc.${p} or [ ]) ++ [ e ]; }120 ) { } proxies;121
122 # Any path with more than one owner is a collision.123 collisions = lib.filterAttrs (_: members: builtins.length members > 1) byPath;124 in125 map (path: {126 message =127 "${toString (builtins.length collisions.${path})} reverse-proxy gateways share proxyPath '${path}': "128 + builtins.concatStringsSep ", " (129 map (e: "${e.ns}/${e.name}") collisions.${path}130 );131 }) (builtins.attrNames collisions);132 };Look at how custom.noProxyPathCollision works:
level = "cluster": Runs once for the entire cluster and receivesallInstances({ <namespace> = { <instance> = inst; }; }).- Filter to reverse proxies: Flattens
allInstancesand usesisProxyto keep only instances whosemeta.providesincludes"reverse-proxy". - Group by
config.proxyPath: Readse.inst._meta.config.proxyPathon every reverse-proxy instance and groups instances by path. - Report collisions: Any
proxyPathclaimed by more than one gateway produces an error finding that lists every colliding<namespace>/<instance>pair.
Run kix check And kix build
Section titled “Run kix check And kix build”Run kix check 19-scorecards:
❱ kix check 19-scorecards
TOOL RESULT DETAILS
eval pass 24 manifests evaluated
scorecard pass 0 errors, 6 warnings, 2 info Both custom rules pass (0 errors), while the built-in kix.rules.reliability rules report 6 warnings and 2 informational findings across the 24 evaluated manifests. kix build 19-scorecards succeeds and renders all 24 resources:
❱ kix build 19-scorecards Show outputHide output · 717 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: 5dj5rd7lw2dk1pv9pcnf6dpi46i93z47,95gmkzkbdf1k30pp3k54hp8vc8qd54p8
kix.run/identity-hash: 9ph1i0ympz81gpl8xxvdriykp6q5c6lq
kix.run/package: backend
kix.run/package-namespace: app-v1
labels:
app.kubernetes.io/instance: backend
app.kubernetes.io/managed-by: kix
app.kubernetes.io/name: backend
name: backend
namespace: app-v1
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: 95gmkzkbdf1k30pp3k54hp8vc8qd54p8,p1v3s0pi2ksrbbpw8mmdxnb57clwmzl4,requires:jh345cnnffy1bnj4cz5x0hwq6ibxvkkr
kix.run/identity-hash: yc2asr6hm9fawjk7f5kcmc1r7sq83wrx
kix.run/package: gateway
kix.run/package-namespace: app-v1
labels:
app.kubernetes.io/instance: gateway
app.kubernetes.io/managed-by: kix
app.kubernetes.io/name: gateway
name: gateway
namespace: app-v1
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
livenessProbe:
failureThreshold: 3
httpGet:
path: /healthz
port: 80
periodSeconds: 10
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: apps/v1
kind: Deployment
metadata:
annotations:
kix.run/depends-on: 3az3y9dajri5bycnprn4kc14y884dypp,lxi0gciwfx6mgp92319mjxhjwvgkdqhs
kix.run/identity-hash: 7mlywnw2vzddhhag58w05y8sbk9p6pms
kix.run/package: backend
kix.run/package-namespace: app-v2
labels:
app.kubernetes.io/instance: backend
app.kubernetes.io/managed-by: kix
app.kubernetes.io/name: backend
name: backend
namespace: app-v2
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: 1bw942wwr3a5jy0h5mgv7zrmi97lhpm6,lxi0gciwfx6mgp92319mjxhjwvgkdqhs,requires:jh345cnnffy1bnj4cz5x0hwq6ibxvkkr
kix.run/identity-hash: 84760c477szfqhg7ihq3ni5a1x1a7z9r
kix.run/package: gateway
kix.run/package-namespace: app-v2
labels:
app.kubernetes.io/instance: gateway
app.kubernetes.io/managed-by: kix
app.kubernetes.io/name: gateway
name: gateway
namespace: app-v2
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
livenessProbe:
failureThreshold: 3
httpGet:
path: /healthz
port: 80
periodSeconds: 10
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/hbvagl9f7fi6d8z50yp5v29jkimcl0xy-kix-activation-19-scorecards.json
kix.run/depends-on: 35cl4xdxp4ivq0y758j4d7ks65i08dm5,h87xkj4fjz6ngyi6vq4qpdqwdf5rcxgk,jh345cnnffy1bnj4cz5x0hwq6ibxvkkr,ksz9y8ygfpc5bzbpxq1kz9pn4ahqjgxb,pq911a8cdinlzlhd4aaw2fnqz0brr1hr,r5sgwjk8w3m41l0xaj0s16xxsacd0nk6,requires:gvrlspcfgh9a2qnsx5kqxw9f4l3x52jj
kix.run/identity-hash: v5f4pbxglm62hzcpz74p49vsbcp0dxdm
kix.run/package: _cluster
kix.run/package-namespace: _cluster
labels:
app.kubernetes.io/managed-by: kix
kix.run/cluster: 19-scorecards
name: 19-scorecards-v5f4pbxglm62
spec:
instances:
app-v1:
backend: backend
gateway: gateway
app-v2:
backend: backend
gateway: gateway
kube-system:
platform-dns: platform-dns
platform-storage: platform-storage
---
apiVersion: kix.run/v1alpha1
kind: PackageInstance
metadata:
annotations:
kix.run/depends-on: 192aaf58bs5chnywmnlgix3s6qg0qi1z,95gmkzkbdf1k30pp3k54hp8vc8qd54p8,requires:pk4hih6jm4yj336rlaqk2qycz2k46xvs
kix.run/identity-hash: ksz9y8ygfpc5bzbpxq1kz9pn4ahqjgxb
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: app-v1
spec:
instanceName: backend
namespaceName: app-v1
version: "1.30.5"
---
apiVersion: kix.run/v1alpha1
kind: PackageInstance
metadata:
annotations:
kix.run/depends-on: 95gmkzkbdf1k30pp3k54hp8vc8qd54p8,ysmf49biwrwmi63smk3f39iapybg43jb,requires:pk4hih6jm4yj336rlaqk2qycz2k46xvs
kix.run/identity-hash: 35cl4xdxp4ivq0y758j4d7ks65i08dm5
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: app-v1
spec:
instanceName: gateway
namespaceName: app-v1
version: "0.0.2"
---
apiVersion: kix.run/v1alpha1
kind: PackageInstance
metadata:
annotations:
kix.run/depends-on: 42s2njbqlf3cxh44ny28ajxn44z4ycdc,lxi0gciwfx6mgp92319mjxhjwvgkdqhs,requires:pk4hih6jm4yj336rlaqk2qycz2k46xvs
kix.run/identity-hash: h87xkj4fjz6ngyi6vq4qpdqwdf5rcxgk
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: app-v2
spec:
instanceName: backend
namespaceName: app-v2
version: "1.30.5"
---
apiVersion: kix.run/v1alpha1
kind: PackageInstance
metadata:
annotations:
kix.run/depends-on: lxi0gciwfx6mgp92319mjxhjwvgkdqhs,qwaclll2gzrb91xf2frqzk9yrk0fhg60,requires:pk4hih6jm4yj336rlaqk2qycz2k46xvs
kix.run/identity-hash: pq911a8cdinlzlhd4aaw2fnqz0brr1hr
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: app-v2
spec:
instanceName: gateway
namespaceName: app-v2
version: "0.0.2"
---
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: v1
data:
default.conf: |
server {
listen 8080;
location / {
default_type text/plain;
return 200 'Response from app-v1\n';
}
location /healthz {
default_type text/plain;
return 200 'ok\n';
}
}
kind: ConfigMap
metadata:
annotations:
kix.run/depends-on: 95gmkzkbdf1k30pp3k54hp8vc8qd54p8
kix.run/identity-hash: 5dj5rd7lw2dk1pv9pcnf6dpi46i93z47
kix.run/package: backend
kix.run/package-namespace: app-v1
labels:
app.kubernetes.io/instance: backend
app.kubernetes.io/managed-by: kix
name: backend
namespace: app-v1
---
apiVersion: v1
data:
default.conf: |
server {
listen 80;
client_max_body_size 10m;
location /healthz {
default_type text/plain;
return 200 'ok\n';
}
location /v1/ {
proxy_pass http://backend.app-v1.svc.cluster.local:80/;
}
}
kind: ConfigMap
metadata:
annotations:
kix.run/depends-on: 192aaf58bs5chnywmnlgix3s6qg0qi1z,95gmkzkbdf1k30pp3k54hp8vc8qd54p8
kix.run/identity-hash: p1v3s0pi2ksrbbpw8mmdxnb57clwmzl4
kix.run/package: gateway
kix.run/package-namespace: app-v1
labels:
app.kubernetes.io/instance: gateway
app.kubernetes.io/managed-by: kix
name: gateway
namespace: app-v1
---
apiVersion: v1
data:
default.conf: |
server {
listen 8080;
location / {
default_type text/plain;
return 200 'Response from app-v2\n';
}
location /healthz {
default_type text/plain;
return 200 'ok\n';
}
}
kind: ConfigMap
metadata:
annotations:
kix.run/depends-on: lxi0gciwfx6mgp92319mjxhjwvgkdqhs
kix.run/identity-hash: 3az3y9dajri5bycnprn4kc14y884dypp
kix.run/package: backend
kix.run/package-namespace: app-v2
labels:
app.kubernetes.io/instance: backend
app.kubernetes.io/managed-by: kix
name: backend
namespace: app-v2
---
apiVersion: v1
data:
default.conf: |
server {
listen 80;
client_max_body_size 10m;
location /healthz {
default_type text/plain;
return 200 'ok\n';
}
location /v2/ {
proxy_pass http://backend.app-v2.svc.cluster.local:80/;
}
}
kind: ConfigMap
metadata:
annotations:
kix.run/depends-on: 42s2njbqlf3cxh44ny28ajxn44z4ycdc,lxi0gciwfx6mgp92319mjxhjwvgkdqhs
kix.run/identity-hash: 1bw942wwr3a5jy0h5mgv7zrmi97lhpm6
kix.run/package: gateway
kix.run/package-namespace: app-v2
labels:
app.kubernetes.io/instance: gateway
app.kubernetes.io/managed-by: kix
name: gateway
namespace: app-v2
---
apiVersion: v1
kind: Namespace
metadata:
annotations:
kix.run/identity-hash: 95gmkzkbdf1k30pp3k54hp8vc8qd54p8
kix.run/package: _cluster
kix.run/package-namespace: _cluster
labels:
app.kubernetes.io/managed-by: kix
kubernetes.io/metadata.name: app-v1
name: app-v1
---
apiVersion: v1
kind: Namespace
metadata:
annotations:
kix.run/identity-hash: lxi0gciwfx6mgp92319mjxhjwvgkdqhs
kix.run/package: _cluster
kix.run/package-namespace: _cluster
labels:
app.kubernetes.io/managed-by: kix
kubernetes.io/metadata.name: app-v2
name: app-v2
---
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: Service
metadata:
annotations:
kix.run/depends-on: 95gmkzkbdf1k30pp3k54hp8vc8qd54p8,9ph1i0ympz81gpl8xxvdriykp6q5c6lq
kix.run/identity-hash: 192aaf58bs5chnywmnlgix3s6qg0qi1z
kix.run/package: backend
kix.run/package-namespace: app-v1
labels:
app.kubernetes.io/instance: backend
app.kubernetes.io/managed-by: kix
name: backend
namespace: app-v1
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: 95gmkzkbdf1k30pp3k54hp8vc8qd54p8,yc2asr6hm9fawjk7f5kcmc1r7sq83wrx
kix.run/identity-hash: ysmf49biwrwmi63smk3f39iapybg43jb
kix.run/package: gateway
kix.run/package-namespace: app-v1
labels:
app.kubernetes.io/instance: gateway
app.kubernetes.io/managed-by: kix
name: gateway
namespace: app-v1
spec:
ports:
- name: http
port: 80
protocol: TCP
targetPort: 80
selector:
app.kubernetes.io/instance: gateway
app.kubernetes.io/name: gateway
type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
annotations:
kix.run/depends-on: 7mlywnw2vzddhhag58w05y8sbk9p6pms,lxi0gciwfx6mgp92319mjxhjwvgkdqhs
kix.run/identity-hash: 42s2njbqlf3cxh44ny28ajxn44z4ycdc
kix.run/package: backend
kix.run/package-namespace: app-v2
labels:
app.kubernetes.io/instance: backend
app.kubernetes.io/managed-by: kix
name: backend
namespace: app-v2
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: 84760c477szfqhg7ihq3ni5a1x1a7z9r,lxi0gciwfx6mgp92319mjxhjwvgkdqhs
kix.run/identity-hash: qwaclll2gzrb91xf2frqzk9yrk0fhg60
kix.run/package: gateway
kix.run/package-namespace: app-v2
labels:
app.kubernetes.io/instance: gateway
app.kubernetes.io/managed-by: kix
name: gateway
namespace: app-v2
spec:
ports:
- name: http
port: 80
protocol: TCP
targetPort: 80
selector:
app.kubernetes.io/instance: gateway
app.kubernetes.io/name: gateway
type: ClusterIP Trigger The Package-Level Rule
Section titled “Trigger The Package-Level Rule”To make error-severity scorecard findings fail the build instead of being capped at "warning", first add maxSeverity = "error"; to tutorials/19-scorecards/scorecard.nix:
1{2 enable = true;3 maxSeverity = "error";4 # ...5}Next, open tutorials/19-scorecards/cluster.nix and comment out maxBodySize = "10m"; on the gateway in instances.app-v1 so it falls back to the "1m" default:
1gateway = {2 package = reverseProxyPackage;3 config = {4 proxyPath = "/v1/";5 # maxBodySize = "10m";6 };7 deps.backend = ref.app-v1.backend;8};Now run kix check 19-scorecards:
❱ kix check 19-scorecards Because custom.proxyMaxBodySizeMustBeExplicit has severity = "error" and scorecard.maxSeverity = "error" is set, evaluation fails and prints:
1[scorecard:custom.proxyMaxBodySizeMustBeExplicit] app-v1/gateway: instance 'gateway' uses the default maxBodySize '1m'. nginx rejects larger request bodies - set this explicitly.Uncomment maxBodySize = "10m"; in instances.app-v1.gateway.
Trigger The Cluster-Level Collision Rule
Section titled “Trigger The Cluster-Level Collision Rule”Next, in tutorials/19-scorecards/cluster.nix, change proxyPath in instances.app-v2.gateway from "/v2/" to "/v1/" so both namespaces claim "/v1/":
1instances.app-v2.gateway = {2 package = reverseProxyPackage;3 config = {4 proxyPath = "/v1/";5 maxBodySize = "10m";6 };7 deps.backend = ref.app-v2.backend;8};Run kix check 19-scorecards again:
❱ kix check 19-scorecards This time, custom.noProxyPathCollision catches the cross-namespace conflict at build time:
1[scorecard:custom.noProxyPathCollision] : 2 reverse-proxy gateways share proxyPath '/v1/': app-v1/gateway, app-v2/gatewayChange app-v2’s proxyPath back to "/v2/" and remove maxSeverity = "error"; from tutorials/19-scorecards/scorecard.nix so the example returns to its checked-in state, then run kix check 19-scorecards to verify the cluster passes again.
What You Learned
Section titled “What You Learned”You completed the 12-part Kix tutorial path:
-
scorecard.enable = trueruns compile-time policy checks across your cluster -
scorecard.maxSeverity("info","warning", or"error", defaulting to"warning") caps finding severity across the cluster, and settingmaxSeverity = "error"makes error-severity rules fail evaluation -
level = "package"rules inspect an individual instance’smeta,config, and resources without parsing rendered YAML strings -
level = "cluster"rules inspectallInstancesacross all namespaces to catch cross-instance collisions before deployment
Next Steps
Section titled “Next Steps”- Browse the How-to guides for step-by-step recipes on secrets, CRDs, Helm chart adoption, NetworkPolicies, and scorecard customization.
- Read the Explanation pages for deeper dives into the dependency graph, availability synthesis, NetworkPolicy derivation, and Server-Side Apply field ownership.
- Keep the Reference handy while authoring your own clusters and packages.