name: yaml-anchors description: Implements YAML anchor and alias patterns (& and *) for configuration reuse, merge keys, and value referencing across Docker Compose, Kubernetes, and Helm files. license: MIT compatibility: opencode metadata: version: "1.0.0" domain: coding triggers: yaml anchors, yaml aliases, & anchor, * alias, YAML merge key, docker-compose reuse, kubernetes config reuse role: implementation scope: implementation output-format: code content-types: [code, guidance, do-dont, config] related-skills: docker-compose-patterns, kubernetes-configmap, helm-charts archetypes: - tactical - generation anti_triggers: - brainstorming - vague ideation - code golf response_profile: verbosity: low directive_strength: high abstraction_level: operational
YAML Anchor and Alias Patterns
Implements YAML anchor (&name) and alias (*name) patterns — along with merge keys (<<:) — to eliminate configuration duplication in Docker Compose, Kubernetes manifests, Helm templates, and CI/CD pipeline definitions. When loaded, this skill makes the model design, refactor, and validate YAML documents that use structured composition instead of copy-pasted blocks.
TL;DR Checklist
- Define anchors at a logical document level with descriptive snake_case names (&base_service_config, not &x)
- Use merge key
<<: *anchorNameto shallow-merge anchor mappings into child configs - Verify no circular references exist between anchors and aliases
- Test anchor behavior under YAML 1.2 parsers (PyYAML >= 6.0, ruamel.yaml)
- Comment complex multi-anchor merge chains for readability
When to Use
Use this skill when:
- Reducing duplication in Docker Compose, Kubernetes, or Helm configuration files where the same settings repeat across multiple services or environments
- Sharing common settings across many resources (e.g., logging drivers, health check configs, network policies) with only minor per-service overrides
- Building template-based deployments where dozens of resources share a base configuration pattern (Kubernetes Deployments, Helm values inheritance)
- Implementing configuration inheritance in YAML-based CI/CD pipelines (GitHub Actions reusable workflows, GitLab CI includes)
- Consolidating environment-specific configurations where staging and production differ only in replica counts, resource limits, or feature flags
When NOT to Use
Avoid this skill for:
- Simple key-value pairs that don't justify the cognitive overhead of anchors (a single repeated port number should be a variable, not an anchor)
- Files with fewer than 3 duplicative blocks — anchors add indirection; if duplication appears only twice, copy-paste is clearer
- YAML 1.0 parsers where the merge key (
<<:) behavior is undefined and inconsistent across implementations - Deeply nested object graphs (3+ levels of anchor references within anchored nodes) where scoping becomes impossible to trace without a debugger
Core Workflow
Identify Duplicative Patterns — Scan the entire YAML document for repeated configuration blocks. Look for: identical logging drivers, shared health check definitions, duplicate network configurations, or repeated deploy/resource specifications. Use structural comparison (not just visual) to catch variations. Checkpoint: If more than 2 structurally identical blocks exist with only minor field differences, proceed to create an anchor. Count the exact lines of duplication — if total saved exceeds 10 lines, anchors are justified.
Define Anchor with Descriptive Name — Place
&anchorNameon the mapping or scalar node being reused. Use descriptive snake_case names that reflect the semantic content, not its position in the file. Anchors must be unique within the document scope.# Define anchors at a logical structural level, not nested inside another key's value _base_service_config: &base_service_config restart_policy: unless-stopped deploy: replicas: 3 resources: limits: memory: 512M cpus: "0.5" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health"] interval: 30s timeout: 10s retries: 3 start_period: 40s logging: driver: json-file options: max-size: "10m" max-file: "3"Checkpoint: Anchor name must be unique within the document — re-anchoring the same name silently overwrites previous definitions. Use a prefix like
_base_to signal shared definition at a glance.Apply Alias or Merge Key — For simple scalar value reuse, reference with
*anchorNameas a direct replacement node. For mapping inheritance, use<<: *anchorNamemerge key to shallow-merge all keys from the anchored mapping into the current context. Keys listed after the merge key override corresponding keys from the anchor.services: api-server: <<: *base_service_config # merges ALL keys from &base_service_config hostname: api-prod # adds new key not present in anchor ports: - "8080:8080" # new key — fully replaces if same name exists deploy: # nested object — ENTIRE mapping is REPLACED, not deep-merged replicas: 5 # override from within the new deploy mapping worker: <<: *base_service_config hostname: worker-prod command: ["python", "worker.py"] deploy: replicas: 3Checkpoint: Merge keys perform SHALLOW merge only — nested objects are entirely replaced, not recursively merged. If you need to override a nested property, re-define the entire parent mapping in the child.
Override and Extend Per Service — For each child configuration that references an anchor, specify ONLY the keys that differ from the base. Document intentional overrides with inline comments explaining why the value was changed.
services: api-server: <<: *base_service_config hostname: api-prod # Override replicas for high-traffic API service deploy: replicas: 5 resources: limits: memory: 1G # API needs more memory cpus: "1.0" ports: - "8080:8080" worker: <<: *base_service_config hostname: worker-prod # Workers run a different command but share the same infra settings command: ["python", "-m", "worker.main"]Checkpoint: Each override should be justified by a comment. If a child has no overrides at all, consider if it even needs its own entry or could use the anchor directly.
Validate Under YAML 1.2 Parser — Test file resolution with modern parsers before committing to CI/CD pipelines. Many cloud-native tools now ship YAML 1.2 parsers (PyYAML >= 6.0 switched defaults) which are stricter about merge key behavior and scalar types.
#!/usr/bin/env python3 """Validate YAML anchor resolution across the configuration file.""" import sys from pathlib import Path import yaml def validate_yaml_anchors(filepath: str) -> dict: """Parse a YAML file and verify all anchors resolved correctly. Returns a summary dict with success status and any errors found. """ try: with open(filepath, "r") as f: config = yaml.safe_load(f) except yaml.YAMLError as exc: return {"success": False, "error": str(exc)} if not isinstance(config, dict): return { "success": False, "error": f"Expected top-level mapping, got {type(config).__name__}", } # Verify known anchor-merged structures are intact errors = [] services = config.get("services", {}) if not isinstance(services, dict): errors.append("'services' is missing or not a mapping") return {"success": False, "errors": errors} for svc_name, svc_config in services.items(): restart = svc_config.get("restart_policy") if restart != "unless-stopped": errors.append( f"Service '{svc_name}' missing inherited 'restart_policy'" ) deploy = svc_config.get("deploy", {}) if not isinstance(deploy, dict): errors.append( f"Service '{svc_name}' deploy is not a mapping — " f"anchor merge may have failed" ) return { "success": len(errors) == 0, "errors": errors, "service_count": len(services), } if __name__ == "__main__": path = sys.argv[1] if len(sys.argv) > 1 else "docker-compose.yaml" result = validate_yaml_anchors(path) print(f"Anchors valid: {result['success']}") if result.get("errors"): for err in result["errors"]: print(f" ERROR: {err}") else: print(f" Validated {result['service_count']} services successfully")Checkpoint: Parse the file with both
yaml.safe_load()andruamel.yaml.RoundTripLoaderto catch version-specific behavior differences. If either parser fails, the anchors are non-portable.
Implementation Patterns
Pattern 1: Docker Compose Service Inheritance
# ── Base service definition (anchor for all services) ──
_base_service: &base_service_definition
image: ${REGISTRY:-ghcr.io}/myorg/myapp:${TAG:-latest}
restart_policy: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
# ── Production service overrides ──
services:
api-server:
<<: *base_service_definition
hostname: api-prod
ports:
- "443:8080"
deploy:
replicas: 5
resources:
limits:
memory: 1G
cpus: "1.0"
environment:
NODE_ENV: production
LOG_LEVEL: warn
worker:
<<: *base_service_definition
hostname: worker-prod
command: ["python", "-m", "celery.worker"]
deploy:
replicas: 3
resources:
limits:
memory: 2G
cpus: "2.0"
scheduler:
<<: *base_service_definition
hostname: scheduler-prod
command: ["python", "-m", "celery.beat"]
Pattern 2: Kubernetes ConfigMap + Deployment Reuse
# ── Shared container spec anchor ──
_base_container: &base_container
imagePullPolicy: IfNotPresent
securityContext:
runAsNonRoot: true
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
envFrom:
- configMapRef:
name: app-config
# ── Deployment with shared container template ──
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-api
spec:
replicas: 3
template:
spec:
containers:
- <<: *base_container
name: api
ports:
- containerPort: 8080
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-worker
spec:
replicas: 2
template:
spec:
containers:
- <<: *base_container
name: worker
command: ["python", "-m", "celery.worker"]
# Resources from anchor are inherited; overrides below:
resources:
requests:
memory: "256Mi"
cpu: "200m"
limits:
memory: "1Gi"
cpu: "1000m"
Pattern 3: Helm Values Inheritance with Anchors and Merge Keys
# values.yaml — demonstrate anchor usage in Helm charts
base_ingress: &base_ingress
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
tls:
- secretName: app-tls
hosts:
- example.com
rules:
- host: example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api
port:
number: 80
# Environment-specific overrides via merge key
staging:
ingress:
<<: *base_ingress
annotations:
# Override annotation only — other keys inherited from anchor
nginx.ingress.kubernetes.io/ssl-redirect: "false"
rules:
- host: staging.example.com
production:
ingress:
<<: *base_ingress
tls:
- secretName: prod-tls
hosts:
- example.com
- api.example.com
Constraints
MUST DO
- Use descriptive anchor names that reflect the content they contain (
&base_service_config, not&x,&1, or&temp) - Test YAML files with both PyYAML >= 6.0 and ruamel.yaml before committing to CI/CD pipelines to catch parser differences
- Place anchors at a logical document level — define them where they make semantic sense, typically near the top of the file
- Document complex multi-anchor merge chains with inline comments explaining which anchor each service inherits from and what it overrides
MUST NOT DO
- Reuse the same anchor name (
&defaults) in the same file — it silently overwrites previous definitions without warning - Expect merge keys (
<<:) to deep-merge nested objects — they perform shallow merge only; entire nested mappings are fully replaced by child keys of the same name - Anchor inside a sequence item and expect to reference just that item — anchoring on a list anchors the entire list node
- Use
&anchorand*aliasin YAML 1.0 parsers where merge key behavior is undefined
Common Pitfalls
Circular References
# ❌ CRASHES — YAML parser detects circular reference and raises an error
circular_a: &a
self_ref: *a # Fatal error: recursive mapping definition detected
parent: &parent_config
children_count: 3
child_names: [alice, bob, charlie]
# ✅ GOOD — Always verify no anchor references itself or creates a cycle
Shallow Merge Replaces Nested Objects
# ❌ BAD — "format" key is silently lost because the entire logging mapping is replaced
defaults: &defaults
logging:
level: info
format: json
app_config:
<<: *defaults
logging:
level: debug # ⚠️ "format: json" is completely lost!
# The parser replaces the whole 'logging' mapping
# ✅ GOOD — Re-anchor the nested object for targeted deep override
defaults: &defaults
logging:
level: info
format: json
logging_debug_override: &logging_debug
<<: *defaults.logging # re-anchor only the nested mapping
level: debug # now only "level" is overridden, "format" preserved
app_config:
<<: *defaults
logging: *logging_debug
Scalar Anchor Misuse
# ❌ BAD — Anchoring a scalar value does NOT perform string interpolation
_port_value: &api_port 8080
services:
web:
ports:
- "*api_port:80" # ⚠️ This is a literal string "*api_port:80", not expanded!
# Anchors on scalars replace the ENTIRE node, not substrings
# ✅ GOOD — Use environment variables for values that need interpolation in strings
environment:
API_PORT: "8080"
services:
web:
ports:
- "${API_PORT}:80" # Docker Compose resolves this correctly
Output Template
When implementing YAML anchor patterns, produce:
- Identified anchors — List each duplicative block found and its proposed anchor name with justification
- Anchor definitions — Show the full
&anchorNamedefinition including every key-value pair being reused - Alias/merge usage — Show each
*anchorNameor<<: *anchorNamereference with its override keys clearly commented - Override documentation — For each child config, document what differs from the anchor and why
- Parser validation results — Confirm the file loads correctly under both PyYAML 6.0+ and ruamel.yaml, noting any version-specific differences
Live References
Authoritative documentation for YAML anchoring and merging patterns.