YAML Reference
YAML Reference
Syntax, types, multi-line strings, anchors, and the gotchas that break your Kubernetes manifests, GitHub Actions, and Ansible playbooks.
Basic syntax — scalars, mappings, sequences
# YAML document starts with --- (optional but good practice)
# Comments use #
---
# Mapping (key: value pairs — equivalent to a dict/object)
name: Alice
age: 30
active: true
score: 9.5
nothing: null # or ~
# Sequence (list — equivalent to an array)
fruits:
- apple
- banana
- cherry
# Nested mapping
address:
street: 123 Main St
city: London
country: UK
# Mapping with sequence values
servers:
- name: web1
ip: 10.0.1.1
tags:
- web
- prod
- name: db1
ip: 10.0.1.2
# Inline (flow) style — compact, single line
colors: [red, green, blue]
person: {name: Bob, age: 25}
# Quoted strings (when you need to preserve special chars)
message: "Hello: World" # colon in value needs quotes
path: "C:\\Users\\alice" # backslash
version: "1.0" # prevent "1.0" from becoming float 1.0
boolean_str: "true" # prevent true from becoming boolean
Scalar types — strings, numbers, booleans, null
# Strings — unquoted (most of the time)
name: Alice Smith
url: https://example.com # colons in URLs are fine without quotes
# Strings that need quoting
tricky: "yes" # without quotes: parsed as boolean true
port: "8080" # without quotes: parsed as integer 8080
colon: "key: val" # contains colon+space
hash: "a#b" # contains hash
newline: "line1\nline2"
# Numbers
integer: 42
negative: -7
float: 3.14
scientific: 1.5e3 # 1500.0
hex: 0xFF # 255
octal: 0o77 # 63 (YAML 1.2) — be careful: 077 parsed as octal in YAML 1.1
# Booleans — YAML 1.1 is broad, YAML 1.2 is strict
# YAML 1.2 booleans (JSON-compatible): true / false / True / False
# YAML 1.1 also treated as bool: yes/no/on/off (and capitalised variants)
# Safe practice: always quote yes/no/on/off in config files
enabled: true
debug: false
# Risky (YAML 1.1 parsers):
# Sweden: SE # fine
# Norway: no # parsed as false in YAML 1.1 — the "Norway problem"
# Null
nothing: null
also_null: ~
empty_key:
The Norway problem: Country codes NO, YES, ON, OFF are parsed as booleans by many YAML 1.1 parsers (Python’s PyYAML, Ruby’s Psych). Always quote them: "NO", "yes".
Multi-line strings — literal and folded blocks
# Literal block scalar: | — preserves newlines exactly
script: |
#!/bin/bash
set -euo pipefail
echo "Starting deploy..."
./deploy.sh --env prod
# Each line becomes a real newline in the parsed value.
# Trailing newline is preserved. Leading indentation is stripped.
# Folded block scalar: > — folds newlines into spaces
description: >
This is a long description that
wraps across multiple lines but
will be joined into one paragraph.
# Result: "This is a long description that wraps across multiple lines but will be joined into one paragraph.\n"
# Block chomping:
# | / > — keep final newline (default: clip)
# |- / >- — strip final newline
# |+ / >+ — keep ALL trailing newlines (keep)
script_no_newline: |-
echo "no trailing newline"
# Multi-line strings inline (not recommended — hard to read)
message: "line one\nline two\nline three"
# Indented literal block (works fine, just needs consistent indentation)
config: |
[server]
host = 0.0.0.0
port = 8080
[database]
url = postgres://localhost/mydb
Rule: Use | for shell scripts, config files, SQL, code — anything where newlines matter. Use > for long prose descriptions where line wrapping is cosmetic.
Anchors and aliases — reuse without repeating
# Define an anchor with &name
# Reference it with *name
# Merge a mapping with <<: *name
# Basic anchor
defaults: &defaults
timeout: 30
retries: 3
log_level: info
production:
<<: *defaults # merge all keys from defaults
log_level: warn # override one key
endpoint: https://prod.example.com
staging:
<<: *defaults
endpoint: https://staging.example.com
# Sequence anchor
common_tags: &tags
- team:engineering
- managed-by:ansible
service_a:
tags: *tags
service_b:
tags:
- *tags # merges individual items differently
- extra:tag
# Practical: Docker Compose
x-common-env: &common-env
NODE_ENV: production
LOG_LEVEL: info
DB_HOST: postgres
services:
web:
environment:
<<: *common-env
PORT: "3000"
worker:
environment:
<<: *common-env
CONCURRENCY: "4"
Note: Anchors and aliases are defined in a single YAML document. They do not work across multiple files. The <<: merge key only works with mappings, not sequences.
Kubernetes YAML patterns
# Multiple documents in one file (separated by ---)
---
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
LOG_LEVEL: info
PORT: "8080" # quoted — otherwise parsed as integer
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
labels:
app: my-app
spec:
replicas: 3
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: app
image: my-app:1.4.2
env:
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: app-config
key: LOG_LEVEL
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: password
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
ports:
- containerPort: 8080
# Common gotchas in K8s YAML:
# - Values that look like numbers must be quoted: port: "8080"
# - Boolean strings must be quoted: value: "true"
# - Indentation must be spaces, never tabs
# - After changing a label selector, you must delete+recreate the resource
GitHub Actions YAML patterns
on:
push:
branches: [main, "release/*"] # glob patterns need quoting
pull_request:
branches: [main]
schedule:
- cron: "0 9 * * 1" # always quote cron expressions
jobs:
build:
runs-on: ubuntu-latest
env:
NODE_ENV: production
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20" # quoted — prevent 20 vs 20.0 ambiguity
- name: Install and test
run: | # literal block — preserves newlines
npm ci
npm test
- name: Build
run: npm run build
if: github.ref == 'refs/heads/main'
- name: Set output
id: version
run: echo "tag=$(git describe --tags)" >> $GITHUB_OUTPUT
- name: Use output
run: echo "Version is ${{ steps.version.outputs.tag }}"
# Matrix strategy
strategy:
matrix:
node: [18, 20, 22]
os: [ubuntu-latest, windows-latest]
fail-fast: false
# Gotcha: YAML special chars in expressions
# Wrap ${{ }} expressions in double quotes when they contain : or #
run: echo "${{ toJSON(github.event) }}"
Common gotchas and parser differences
| Value | YAML 1.1 (PyYAML default) | YAML 1.2 (strict/JSON-compat) | Safe fix |
|---|---|---|---|
yes / no |
true / false | string | "yes" |
on / off |
true / false | string | "on" |
077 |
63 (octal) | string "077" | "077" |
1.0 |
float 1.0 | float 1.0 | "1.0" if string needed |
1e3 |
float 1000.0 | float 1000.0 | "1e3" if string needed |
null |
None | None | fine as-is |
~ |
None | string "~" in some parsers | use null |
2024-01-15 |
datetime object | string | "2024-01-15" |
Tab characters are not valid YAML indentation. Every YAML parser will reject tabs. Configure your editor to use spaces in YAML files.
# Other common mistakes
# BAD: missing space after colon
name:Alice # parse error
# GOOD
name: Alice
# BAD: inconsistent indentation
items:
- one
- two # different indent = parse error
# BAD: special char in unquoted string
message: Hello: World # colon+space splits into key:value
message: "Hello: World" # GOOD
# BAD: version number becoming float
version: 1.10 # parsed as 1.1 (float)
version: "1.10" # GOOD — preserves string "1.10"
# BAD: empty value vs null
key: # null (None)
key: "" # empty string — different thing!
# GOOD: validate YAML before applying
cat manifest.yml | python3 -c "import sys,yaml; yaml.safe_load(sys.stdin); print('OK')"
yamllint manifest.yml
kubectl apply --dry-run=client -f manifest.yml
YAML in Python — PyYAML and ruamel.yaml
import yaml
# Load YAML (safe_load prevents arbitrary object creation)
with open("config.yml") as f:
config = yaml.safe_load(f) # always use safe_load, not load()
# Load multiple documents
with open("k8s.yml") as f:
docs = list(yaml.safe_load_all(f))
# Parse from string
data = yaml.safe_load("""
name: Alice
tags:
- admin
- user
""")
# Dump Python object to YAML
output = yaml.dump(data, default_flow_style=False, sort_keys=False)
# Dump to file
with open("output.yml", "w") as f:
yaml.dump(data, f, default_flow_style=False)
# ruamel.yaml — preserves comments and formatting
from ruamel.yaml import YAML
yaml_parser = YAML()
yaml_parser.preserve_quotes = True
with open("config.yml") as f:
config = yaml_parser.load(f)
config["new_key"] = "value"
with open("config.yml", "w") as f:
yaml_parser.dump(config, f) # preserves existing comments
PyYAML gotcha: yaml.load() without a Loader is deprecated and unsafe — it can execute arbitrary Python. Always use yaml.safe_load() for untrusted input.
Validation and tooling
# yamllint — lint YAML files (install: pip install yamllint)
yamllint config.yml
yamllint -d relaxed site.yml # less strict rules
yamllint -d "{extends: default, rules: {line-length: {max: 120}}}" *.yml
# .yamllint config
---
extends: default
rules:
line-length:
max: 120
truthy:
allowed-values: [true, false] # ban yes/no/on/off
comments:
min-spaces-from-content: 1
# Python quick validation
python3 -c "import yaml,sys; yaml.safe_load(sys.stdin)" < config.yml
# yq — jq for YAML (install: brew install yq / snap install yq)
yq '.metadata.name' deployment.yml
yq '.spec.replicas = 5' deployment.yml # update in place with -i
yq -o=json deployment.yml # convert to JSON
yq eval-all 'select(.kind == "Deployment")' *.yml # filter docs
# kubectl dry run (best K8s YAML validation)
kubectl apply --dry-run=client -f manifest.yml
kubectl apply --dry-run=server -f manifest.yml # server-side: validates against live API
# Diff two YAML files (semantic diff)
diff <(yq -o=json a.yml) <(yq -o=json b.yml)
Founded
2023 in London, UK
Contact
hello@releaserun.com