ytt: YAML Templating with Starlark
ytt Deep Dive
Section titled “ytt Deep Dive”ytt (pronounced “why-tee-tee”) is a YAML-aware templating tool from the Carvel project (VMware Tanzu). Unlike text-based templaters, ytt understands YAML structure, enabling safer and more powerful configuration management.
The Problem with Text Templating
Section titled “The Problem with Text Templating”Traditional templating (like Helm’s Go templates) treats YAML as plain text:
# Helm-style text templating - fragile!args:- "-listen=:{{ .Values.port }}" # Manual quoting- {{ if .Values.debug }}-debug{{ end }} # Indentation issuesenv:{{- range $k, $v := .Values.env }} # Complex syntax - name: {{ $k }} value: {{ $v | quote }}{{- end }}Common issues:
- Manual quoting and escaping
- Indentation errors break YAML structure
- Template errors caught only at render time
- Hard to compose and test
ytt’s Structural Approach
Section titled “ytt’s Structural Approach”ytt understands YAML structure natively:
#@ load("@ytt:data", "data")
args:- #@ "-listen=:" + str(data.values.port)- #@ "-debug" if data.values.debug else Noneenv:#@ for k, v in data.values.env.items():- name: #@ k value: #@ v#@ endBenefits:
- YAML structure preserved automatically
- No quoting/escaping needed
- Type-safe values
- Errors caught at template evaluation time
Core Concepts
Section titled “Core Concepts”Annotations (#@ Directives)
Section titled “Annotations (#@ Directives)”ytt uses comment annotations to embed Starlark:
| Annotation | Purpose |
|---|---|
#@ expr | Inline Starlark expression |
#@ load(...) | Import modules |
#@ def fn(): ... #@ end | Define functions |
#@ if cond: ... #@ end | Conditionals |
#@ for x in items: ... #@ end | Loops |
#@data/values-schema | Mark as schema file |
#@overlay/... | Overlay operations |
#! comment | Preserved comment |
The @ytt: Module Namespace
Section titled “The @ytt: Module Namespace”ytt provides domain-specific modules:
#@ load("@ytt:data", "data") # Data values#@ load("@ytt:overlay", "overlay") # Patching#@ load("@ytt:template", "template") # Template utils#@ load("@ytt:library", "library") # Library loading#@ load("@ytt:assert", "assert") # Validation#@ load("@ytt:struct", "struct") # Typed structs#@ load("@ytt:yaml", "yaml") # YAML encoding#@ load("@ytt:json", "json") # JSON encoding#@ load("@ytt:base64", "base64") # Base64 encoding#@ load("@ytt:md5", "md5") # MD5 hashing#@ load("@ytt:sha256", "sha256") # SHA256 hashing#@ load("@ytt:url", "url") # URL handling#@ load("@ytt:regexp", "regexp") # Regular expressions#@ load("@ytt:ip", "ip") # IP address parsing#@ load("@ytt:math", "math") # Math functionsData Values System
Section titled “Data Values System”Schema Definition
Section titled “Schema Definition”Define your configuration schema:
#@data/values-schema---#@schema/title "Application Configuration"app: name: "" #@schema/desc "Number of replicas" replicas: 1 #@schema/nullable image: ""
resources: cpu: "100m" memory: "128Mi"
#@schema/type any=True extra_config: {}
debug: falseUsing Data Values
Section titled “Using Data Values”#@ load("@ytt:data", "data")
apiVersion: apps/v1kind: Deploymentmetadata: name: #@ data.values.app.namespec: replicas: #@ data.values.app.replicas template: spec: containers: - name: app image: #@ data.values.app.image resources: requests: cpu: #@ data.values.app.resources.cpu memory: #@ data.values.app.resources.memoryOverriding Values
Section titled “Overriding Values”# Via CLI flagsytt -f config/ -v app.name=myapp -v app.replicas=3
# Via values fileytt -f config/ -f values-prod.yml
# Via environmentytt -f config/ --data-values-env APP_
# Multiple sources (later overrides earlier)ytt -f config/ -f base.yml -f prod.yml -v debug=trueOverlays: Surgical YAML Patching
Section titled “Overlays: Surgical YAML Patching”Overlays modify existing YAML without changing the original files.
Basic Overlay Pattern
Section titled “Basic Overlay Pattern”#@ load("@ytt:overlay", "overlay")
#! Match all documents#@overlay/match by=overlay.all---metadata: #@overlay/match missing_ok=True annotations: managed-by: yttOverlay Annotations
Section titled “Overlay Annotations”| Annotation | Purpose |
|---|---|
#@overlay/match | Select nodes to modify |
#@overlay/replace | Replace node value |
#@overlay/insert | Insert new items |
#@overlay/remove | Delete nodes |
#@overlay/assert | Validate matches |
#@overlay/match-child-defaults | Set defaults for children |
Matching Strategies
Section titled “Matching Strategies”#@ load("@ytt:overlay", "overlay")
#! Match by document subset#@overlay/match by=overlay.subset({"kind": "Deployment"})---spec: template: spec: #@overlay/match missing_ok=True nodeSelector: env: production
#! Match by custom function#@overlay/match by=lambda i,l,r: l["metadata"]["name"].endswith("-api")---metadata: labels: tier: api
#! Match with expectations#@overlay/match by=overlay.subset({"kind": "Service"}), expects="1+"---spec: type: LoadBalancerArray Operations
Section titled “Array Operations”#@ load("@ytt:overlay", "overlay")
#@overlay/match by=overlay.subset({"kind": "Deployment"})---spec: template: spec: containers: #! Match container by name #@overlay/match by="name" - name: app #@overlay/match missing_ok=True env: #@overlay/append - name: NEW_VAR value: "added"
#! Insert new container #@overlay/append - name: sidecar image: sidecar:latestRemove Resources
Section titled “Remove Resources”#@ load("@ytt:overlay", "overlay")
#! Remove all ServiceAccounts#@overlay/match by=overlay.subset({"kind": "ServiceAccount"}), expects="0+"#@overlay/remove---Functions and Modules
Section titled “Functions and Modules”Defining Functions
Section titled “Defining Functions”#@ def labels(name, component="app"):app.kubernetes.io/name: #@ nameapp.kubernetes.io/component: #@ componentapp.kubernetes.io/managed-by: ytt#@ end
---apiVersion: v1kind: Servicemetadata: name: api labels: #@ labels("api", "backend")Multi-Document Functions
Section titled “Multi-Document Functions”#@ def namespace(name):apiVersion: v1kind: Namespacemetadata: name: #@ name---apiVersion: v1kind: ResourceQuotametadata: name: default namespace: #@ namespec: hard: pods: "10"#@ end
--- #@ template.replace(namespace("prod"))--- #@ template.replace(namespace("staging"))Library Modules
Section titled “Library Modules”Create reusable libraries in _ytt_lib/:
config/├── config.yml└── _ytt_lib/ └── @k8s/ ├── deployment.lib.yml └── service.lib.yml#! _ytt_lib/@k8s/deployment.lib.yml#@ load("@ytt:struct", "struct")
#@ def deployment(name, image, replicas=1):apiVersion: apps/v1kind: Deploymentmetadata: name: #@ namespec: replicas: #@ replicas selector: matchLabels: app: #@ name template: metadata: labels: app: #@ name spec: containers: - name: main image: #@ image#@ end
#@ k8s = struct.make(deployment=deployment)#! config.yml#@ load("@k8s:deployment.lib.yml", "k8s")
--- #@ k8s.deployment("api", "myapp/api:v1", replicas=3)--- #@ k8s.deployment("worker", "myapp/worker:v1")Assertions and Validation
Section titled “Assertions and Validation”Basic Assertions
Section titled “Basic Assertions”#@ load("@ytt:assert", "assert")#@ load("@ytt:data", "data")
#! Validate data values#@ assert.equals(data.values.env, "prod", "staging", "dev")#@ assert.min(data.values.replicas, 1)#@ assert.max(data.values.replicas, 100)
#! Inline assertionsreplicas: #@ assert.min(data.values.replicas, 1)name: #@ data.values.name or assert.fail("name is required")Schema Validation
Section titled “Schema Validation”#@data/values-schema---#@schema/validation min=1, max=100replicas: 1
#@schema/validation one_of=["debug", "info", "warn", "error"]log_level: "info"
#@schema/validation format="ip"bind_address: "0.0.0.0"Custom Validation Functions
Section titled “Custom Validation Functions”#@ load("@ytt:assert", "assert")
#@ def validate_port(val):#@ if val < 1 or val > 65535:#@ assert.fail("port must be 1-65535")#@ end#@ return val#@ end
port: #@ validate_port(data.values.port)Real-World Examples
Section titled “Real-World Examples”Multi-Environment Kubernetes
Section titled “Multi-Environment Kubernetes”config/├── schema.yml├── base/│ ├── deployment.yml│ └── service.yml├── overlays/│ ├── prod.yml│ └── dev.yml└── values/ ├── prod.yml └── dev.yml#! schema.yml#@data/values-schema---env: ""app: name: myapp replicas: 1 image: "" resources: cpu: "100m" memory: "128Mi"#! values/prod.yml#@data/values---env: prodapp: replicas: 3 image: registry.prod/myapp:v1.2.3 resources: cpu: "500m" memory: "512Mi"#! overlays/prod.yml#@ load("@ytt:overlay", "overlay")
#@overlay/match by=overlay.subset({"kind": "Deployment"})---spec: template: spec: #@overlay/match missing_ok=True nodeSelector: env: production #@overlay/match missing_ok=True tolerations: - key: "production" operator: "Exists"# Deploy to productionytt -f config/schema.yml \ -f config/base/ \ -f config/values/prod.yml \ -f config/overlays/prod.yml | kubectl apply -f -Helm Post-Rendering
Section titled “Helm Post-Rendering”ytt can post-process Helm output:
#!/bin/bash# ytt-post-rendererytt -f - -f overlays/helm template myrelease mychart/ --post-renderer ./ytt-post-rendererConfigMap from Files
Section titled “ConfigMap from Files”#@ load("@ytt:data", "data")
apiVersion: v1kind: ConfigMapmetadata: name: app-configdata: #@ for path in data.list("configs/"): #@ filename = path.split("/")[-1] #@ content = data.read(path) _: #@ { filename: content } #@ endSecret Generation
Section titled “Secret Generation”#@ load("@ytt:data", "data")#@ load("@ytt:base64", "base64")#@ load("@ytt:sha256", "sha256")
apiVersion: v1kind: Secretmetadata: name: app-secrets annotations: #! Hash for change detection checksum: #@ sha256.sum(data.read("secrets.json"))type: Opaquedata: config.json: #@ base64.encode(data.read("secrets.json"))Starlark Fork: Why and How
Section titled “Starlark Fork: Why and How”ytt uses a customized fork of starlark-go (carvel-dev/starlark-go) rather than upstream. This wasn’t a casual decision - it solves a fundamental incompatibility between Starlark and YAML templating.
The Problem: Whitespace Conflicts
Section titled “The Problem: Whitespace Conflicts”Standard Starlark uses Python-style significant whitespace:
# Standard Starlark - whitespace determines block structuredef greet(name): if name: return "Hello, " + name # indentation required return "Hello" # dedent closes blockBut YAML also uses significant whitespace:
# YAML - indentation determines structurespec: containers: # child of spec - name: app # list itemThe conflict: If you embed Starlark in YAML templates, which whitespace rules win? This was raised in ytt Issue #8 - developers couldn’t indent YAML inside Starlark functions because the indentation would be interpreted as Starlark block structure.
The Solution: Block Scanner
Section titled “The Solution: Block Scanner”In July 2019, ytt’s creator Dmitriy Kalinin added a blockScanner to their vendored starlark-go. This scanner:
- Ignores whitespace for block detection - Doesn’t use INDENT/OUTDENT tokens based on spaces
- Uses explicit
endkeyword - Blocks close withend, not dedentation - Handles
else/elifspecially - These implicitly close prior blocks - Forbids
pass- Onlyendis allowed to close blocks
// From syntax/block_scanner.go// blockScanner changes INDENT/OUTDENT to be// based on nesting depth (start->end) instead of whitespacetype blockScanner struct { scanner *scanner indentStack []blockScannerToken // ...}How It Works
Section titled “How It Works”The parser can be invoked with a BlockScanner mode flag:
// In parse.goconst ( RetainComments Mode = 1 << iota BlockScanner Mode = 1 << iota // use if/end syntax instead of indent)
func Parse(filename string, src interface{}, mode Mode) (*File, error) { in, _ := newScanner(filename, src, mode&RetainComments != 0) var inScanner scannerInterface = in if (mode & BlockScanner) == BlockScanner { inScanner = newBlockScanner(in) // Use block-based parsing } // ...}Block-Based Syntax in Practice
Section titled “Block-Based Syntax in Practice”#@ if condition:some_yaml: here nested: data: works#@ end
#@ for item in items:- name: #@ item value: #@ item.upper()#@ end
#@ def my_function():key: valuelist:- one- two#@ endNotice how YAML indentation inside blocks is now purely YAML - not Starlark block structure.
Other Fork Changes
Section titled “Other Fork Changes”Beyond the block scanner, the fork:
- Removed unused modules - Stripped
lib/proto/,lib/json/,lib/math/,lib/time/(~2000+ lines) since ytt provides its own@ytt:*modules - Simplified Thread model - Removed step counting, cancellation APIs (ytt doesn’t need execution limits)
- Added Module type -
starlarkstruct/module.gofor creating named module collections - Enhanced Struct - Binary operations support for struct merging
Why Not Upstream?
Section titled “Why Not Upstream?”The block scanner is ytt-specific. Standard Starlark needs whitespace sensitivity for:
- Bazel BUILD files (pure Starlark, no YAML embedding)
- General Python-like behavior expectations
The fork is kept minimal and synced with upstream where possible. The key commits:
fd88429 - moved over from ytt repo (add blockScanner, improve reserved keyword errors)3e0f0a9 - Make ytt flavoured starlark embeddable into starlark applications8a7b203 - fix handling of unbalanced end and if/for/...Comparison with Alternatives
Section titled “Comparison with Alternatives”vs. Helm
Section titled “vs. Helm”| Aspect | ytt | Helm |
|---|---|---|
| Templating | YAML-aware (structural) | Text-based (Go templates) |
| Package Management | None (use with imgpkg) | Built-in charts |
| Composition | Overlays + libraries | Values + subcharts |
| Type Safety | Schema validation | Limited |
| Learning Curve | Medium (Starlark) | Low-Medium |
When to use ytt over Helm:
- Need structural YAML manipulation
- Want type-safe configuration
- Complex multi-environment setups
- Post-processing Helm output
vs. Kustomize
Section titled “vs. Kustomize”| Aspect | ytt | Kustomize |
|---|---|---|
| Language | Starlark (full language) | YAML patches only |
| Complexity | Can be complex | Simpler, limited |
| Flexibility | Very high | Medium |
| Kubectl Integration | Separate | Built-in |
When to use ytt over Kustomize:
- Need conditional logic
- Complex transformations
- Reusable functions
- Cross-cutting concerns
vs. Jsonnet
Section titled “vs. Jsonnet”| Aspect | ytt | Jsonnet |
|---|---|---|
| Target Format | YAML-first | JSON-first |
| Syntax | Python-like | Unique JSON-like |
| Learning Curve | Lower | Higher |
| Kubernetes Focus | Primary | General |
Best Practices
Section titled “Best Practices”1. Use Schemas
Section titled “1. Use Schemas”Always define data value schemas:
#@data/values-schema---#@schema/desc "Application name (required)"name: ""
#@schema/desc "Deployment replicas"#@schema/validation min=1replicas: 12. Separate Concerns
Section titled “2. Separate Concerns”config/├── schema.yml # Data value schema├── base/ # Base configurations├── overlays/ # Environment-specific patches├── values/ # Environment values└── _ytt_lib/ # Reusable libraries3. Validate Early
Section titled “3. Validate Early”#@ load("@ytt:assert", "assert")#@ load("@ytt:data", "data")
#@ if not data.values.name:#@ assert.fail("name is required")#@ end4. Use Libraries for Reuse
Section titled “4. Use Libraries for Reuse”#@ load("@lib:k8s.lib.yml", "k8s")
--- #@ k8s.deployment(data.values)--- #@ k8s.service(data.values)5. Document with Comments
Section titled “5. Document with Comments”#! This overlay adds production-specific settings#@ load("@ytt:overlay", "overlay")
#@overlay/match by=overlay.all, expects="1+"---#! Add resource limits for productionspec: #@overlay/match missing_ok=True resources: limits: memory: "1Gi"