Editing YAML Configuration Files with yq
Read and patch Kubernetes and CI YAML in place using yq while preserving structure and comments.
Editing YAML Configuration Files with yq is a free DevOps Bootcamp lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the DevOps Bootcamp learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
What Is yq and Why Use It for YAML?
yq is a portable command-line YAML processor, similar to how jq handles JSON. It lets you read, filter, and edit YAML files without writing a script in Python or Ruby.
There are two popular tools named yq:
- mikefarah/yq (Go) — actively maintained, supports YAML, JSON, XML, TOML. This lesson uses this version.
- kislyuk/yq (Python) — a
jqwrapper for YAML; syntax differs.
Install the Go version:
brew install yqon macOSsnap install yqon Linux- Or download the binary:
wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/local/bin/yq && chmod +x /usr/local/bin/yq
Verify: yq --version should print v4.x.x. Version 4 uses a different expression syntax than v3, so the version matters.
# Install yq (Go version) on Linux
wget -q https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 \
-O /usr/local/bin/yq
chmod +x /usr/local/bin/yq
# Confirm version
yq --versionReading Values from a Kubernetes Deployment YAML
Before editing anything, learn to read YAML fields. Given a Kubernetes Deployment, you can extract any nested value with dot-notation paths.
Example deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: production
spec:
replicas: 3
template:
spec:
containers:
- name: app
image: my-app:1.0.0Key read commands:
yq '.metadata.name' deployment.yaml— printsmy-appyq '.spec.replicas' deployment.yaml— prints3yq '.spec.template.spec.containers[0].image' deployment.yaml— printsmy-app:1.0.0
Output is plain text by default (no quotes). Add -r flag or use | yq -r if you need raw strings in scripts.
# Create a sample deployment YAML
cat > /tmp/deployment.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: production
spec:
replicas: 3
template:
spec:
containers:
- name: app
image: my-app:1.0.0
EOF
# Read individual fields
echo "App name: $(yq '.metadata.name' /tmp/deployment.yaml)"
echo "Replicas: $(yq '.spec.replicas' /tmp/deployment.yaml)"
echo "Image: $(yq '.spec.template.spec.containers[0].image' /tmp/deployment.yaml)"In-Place Editing with the -i Flag
The most important flag for real-world use is -i (in-place). Without it, yq prints results to stdout and leaves the file unchanged.
Syntax:
- Read only (stdout):
yq '.spec.replicas' file.yaml - In-place edit:
yq -i '.spec.replicas = 5' file.yaml
The assignment operator = sets a value. The expression is a full yq filter, so you can combine reading and writing in one pass.
Important: yq -i rewrites the file completely. Comments placed on the same line as a field are generally preserved, but standalone comment blocks may move. Always commit your YAML to version control before running bulk in-place edits.
Test without -i first, then add it once you are happy with the output.
# Start with the deployment from the previous scene
echo 'Before:' && yq '.spec.replicas' /tmp/deployment.yaml
# Edit in place: scale to 5 replicas
yq -i '.spec.replicas = 5' /tmp/deployment.yaml
echo 'After:' && yq '.spec.replicas' /tmp/deployment.yamlUpdating the Container Image Tag
A very common CI task is bumping the Docker image tag in a Kubernetes manifest after a new image is built. With yq this becomes a one-liner.
The pattern is:
- Target the container by name using
select()to avoid hard-coding array index 0. - Use
|=(update operator) or=to set the new value.
Using array index (fragile if containers list changes):
yq -i '.spec.template.spec.containers[0].image = "my-app:2.1.0"' deployment.yaml
Using select() (robust):
yq -i '(.spec.template.spec.containers[] | select(.name == "app")).image = "my-app:2.1.0"' deployment.yaml
In a CI pipeline you would pass the tag as a shell variable:
NEW_TAG="my-app:2.1.0"
CONTAINER_NAME="app"
# Robust update: target by container name, not index
yq -i \
"(.spec.template.spec.containers[] | select(.name == \"${CONTAINER_NAME}\")).image = \"${NEW_TAG}\"" \
/tmp/deployment.yaml
# Verify
yq '.spec.template.spec.containers[0].image' /tmp/deployment.yamlAdding and Removing Fields
Beyond updating existing fields, yq can add new keys or delete existing ones.
Adding a field:
- Simply assign to a path that does not exist:
yq -i '.metadata.labels.version = "v2"' file.yaml - If the parent key (
labels) is missing, yq creates it automatically.
Deleting a field:
- Use the
del()function:yq -i 'del(.metadata.annotations)' file.yaml - Delete an array element by index:
yq -i 'del(.spec.template.spec.containers[1])' file.yaml
Adding an element to an array:
yq -i '.spec.template.spec.containers += [{"name": "sidecar", "image": "envoy:latest"}]' file.yaml
# Add a label to the deployment
yq -i '.metadata.labels.version = "v2"' /tmp/deployment.yaml
yq -i '.metadata.labels.managed-by = "ci-pipeline"' /tmp/deployment.yaml
echo '--- Labels after adding ---'
yq '.metadata.labels' /tmp/deployment.yaml
# Delete one label
yq -i 'del(.metadata.labels.managed-by)' /tmp/deployment.yaml
echo '--- Labels after delete ---'
yq '.metadata.labels' /tmp/deployment.yamlWorking with Multi-Document YAML Files
Kubernetes manifests often bundle multiple resources in a single file separated by ---. By default yq processes all documents in such a file.
Key techniques:
- List all document kinds:
yq '.[].kind' multi.yaml— note the leading.[]to iterate documents. - Target a specific document by kind:
yq 'select(.kind == "Service")' multi.yaml - Edit only matching documents in place:
yq -i 'select(.kind == "Deployment").spec.replicas = 2' multi.yaml
Documents that do not match the select() predicate are passed through unchanged, so your Service, ConfigMap, and other resources stay intact.
To split a multi-doc file into individual files you can loop over yq output or use:
yq -s '.kind' multi.yaml— writes one file per document named after its.kindvalue.
cat > /tmp/multi.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 1
---
apiVersion: v1
kind: Service
metadata:
name: web-svc
spec:
port: 80
EOF
# Scale ONLY the Deployment, leave Service untouched
yq -i 'select(.kind == "Deployment").spec.replicas = 4' /tmp/multi.yaml
echo '--- Deployment replicas ---'
yq 'select(.kind == "Deployment").spec.replicas' /tmp/multi.yaml
echo '--- Service port (unchanged) ---'
yq 'select(.kind == "Service").spec.port' /tmp/multi.yamlPatching a GitHub Actions CI YAML
CI configuration files (.github/workflows/*.yml, .gitlab-ci.yml) are also YAML. The same yq commands work, though the paths can be deeply nested.
Common CI patching tasks:
- Pin a runner version: update
runs-onacross all jobs. - Update an action version: find steps that use a particular action and bump its
usesfield. - Toggle a flag: enable or disable a workflow-level setting.
Example: update all steps that use actions/checkout to v4:
yq -i '(.jobs[].steps[] | select(.uses == "actions/checkout@v3")).uses = "actions/checkout@v4"' .github/workflows/ci.ymlThis idiom — iterate with [], narrow with select(), assign with = — is the core pattern for any structured YAML edit.
cat > /tmp/ci.yml << 'EOF'
name: CI
on: [push]
jobs:
build:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- run: npm test
lint:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- run: npm run lint
EOF
# Bump all checkout steps from v3 → v4
yq -i '(.jobs[].steps[] | select(.uses == "actions/checkout@v3")).uses = "actions/checkout@v4"' \
/tmp/ci.yml
# Verify both jobs were updated
yq '.jobs[].steps[] | select(.uses | test("checkout")).uses' /tmp/ci.ymlUsing Environment Variables in yq Expressions
Hardcoding values in yq expressions makes scripts brittle. yq supports injecting shell variables using the env() function or the strenv() shorthand.
env(VAR_NAME)— reads the environment variable and converts it to the appropriate YAML type (number stays a number, string stays a string).strenv(VAR_NAME)— always returns a string, useful for image tags.
This avoids the quoting nightmare of interpolating variables inside double-quoted shell strings with embedded YAML paths.
Pattern:
export IMAGE_TAG="my-app:3.0.0"
yq -i '.spec.template.spec.containers[0].image = strenv(IMAGE_TAG)' deployment.yamlUse env() when setting numeric fields like replicas so the YAML type is preserved (integer, not a quoted string).
export APP_IMAGE="my-app:3.0.0"
export REPLICA_COUNT=6
# Set image using strenv() — result is a YAML string
yq -i '.spec.template.spec.containers[0].image = strenv(APP_IMAGE)' \
/tmp/deployment.yaml
# Set replicas using env() — result is a YAML integer
yq -i '.spec.replicas = env(REPLICA_COUNT)' \
/tmp/deployment.yaml
# Confirm types are correct in the output
yq '.spec.replicas, .spec.template.spec.containers[0].image' /tmp/deployment.yamlMerging Two YAML Files
Sometimes you need to apply a patch file (a small override YAML) onto a base config — for example, environment-specific overrides in Kustomize-style workflows.
yq can merge two files using the * merge operator:
yq '. *= load("patch.yaml")' base.yaml— deep-merges patch into base, writing to stdout.- Add
-ito update base in place:yq -i '. *= load("patch.yaml")' base.yaml
Merge behaviour:
- Scalar values in the patch overwrite the base.
- Mappings are deep-merged (keys not in patch are preserved).
- Sequences (arrays) are replaced by default, not appended. Use
*+to append instead.
This pattern replaces brittle sed scripts that break on whitespace changes.
cat > /tmp/base.yaml << 'EOF'
app:
name: my-service
port: 8080
debug: false
database:
host: localhost
port: 5432
EOF
cat > /tmp/patch.yaml << 'EOF'
app:
port: 9090
debug: true
database:
host: db.production.svc
EOF
# Deep-merge patch into base (stdout preview first)
yq '. *= load("/tmp/patch.yaml")' /tmp/base.yaml
# Apply in place
yq -i '. *= load("/tmp/patch.yaml")' /tmp/base.yamlValidating YAML and Converting to JSON
Before applying a patched YAML to a cluster, it is good practice to validate it and optionally convert it to JSON for other tools.
Validate syntax:
yq '.' file.yaml && echo "Valid"— yq exits with code 1 on parse errors, so this works in CI gates.
Convert YAML to JSON:
yq -o=json '.' file.yaml— outputs pretty JSON.- Pipe to
jqfor further JSON processing:yq -o=json '.' file.yaml | jq '.metadata.name'
Convert JSON to YAML:
yq -P '.' file.json— the-Pflag forces YAML (prettyprint) output when the input is JSON.
These conversions make yq a bridge between YAML-native tools (Helm, kubectl) and JSON-native tools (Terraform, AWS CLI, jq).
# Validate YAML (exits 0 on success, 1 on parse error)
if yq '.' /tmp/deployment.yaml > /dev/null 2>&1; then
echo "YAML is valid"
else
echo "YAML parse error!" >&2
exit 1
fi
# Convert to JSON and query with jq
yq -o=json '.' /tmp/deployment.yaml \
| jq '{name: .metadata.name, image: .spec.template.spec.containers[0].image}'
# Round-trip: JSON snippet back to YAML
echo '{"replicas": 7, "strategy": "RollingUpdate"}' \
| yq -P '.'A Complete CI Deployment Patch Script
Putting all the techniques together: a real CI script that patches a Kubernetes Deployment manifest as part of a GitOps pipeline.
The script:
- Validates the input YAML before touching it.
- Uses
env()/strenv()for all variable substitutions. - Updates the container image tag using a name-based
select(). - Bumps the replica count.
- Stamps a
deploy-timeannotation with the current timestamp. - Validates the output again before committing.
This pattern ensures that even if the pipeline runs concurrently, each step is atomic and auditable.
#!/usr/bin/env bash
set -euo pipefail
MANIFEST="/tmp/deployment.yaml"
export NEW_IMAGE="my-app:$(date +%Y%m%d)-abc1234"
export NEW_REPLICAS=3
export DEPLOY_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
export CONTAINER="app"
# 1. Validate before patching
yq '.' "$MANIFEST" > /dev/null
# 2. Update image (by container name)
yq -i \
'(.spec.template.spec.containers[] | select(.name == strenv(CONTAINER))).image = strenv(NEW_IMAGE)' \
"$MANIFEST"
# 3. Set replicas
yq -i '.spec.replicas = env(NEW_REPLICAS)' "$MANIFEST"
# 4. Stamp annotation
yq -i '.metadata.annotations."deploy-time" = strenv(DEPLOY_TIME)' "$MANIFEST"
# 5. Validate result
yq '.' "$MANIFEST" > /dev/null && echo "Patch applied successfully"
# 6. Show diff summary
yq '{image: .spec.template.spec.containers[0].image, replicas: .spec.replicas}' "$MANIFEST"Knowledge Check: Safe Multi-Document Editing
Test your understanding of editing multi-document Kubernetes YAML files with yq.
Lesson Recap: Editing YAML with yq
You have completed the lesson on editing YAML configuration files with yq. Here is a concise summary of everything covered:
- Installation: Use the mikefarah/yq Go binary (v4). Verify with
yq --version. - Reading: Dot-notation paths like
.spec.replicas; array access with[0]or[]iteration. - In-place editing: The
-iflag rewrites the file. Always preview without-ifirst. - Robust targeting: Prefer
select(.name == "app")over hard-coded array indices. - Adding / deleting: Assign to a new path to create it; use
del()to remove fields. - Multi-document files: Use
select(.kind == "...")to target one resource and leave others untouched. - CI variables: Use
strenv(VAR)for strings andenv(VAR)for typed values — avoids shell quoting bugs. - Merging:
. *= load("patch.yaml")deep-merges an override file without losing unpatched keys. - Validation & conversion:
yq '.'as a lint gate;-o=jsonand-Pfor format conversion.
The core pattern for any YAML patch in CI is: validate → select → assign → validate. Combine this with strenv() and select() and you will never need to reach for fragile sed one-liners again.
Frequently asked questions
Is the “Editing YAML Configuration Files with yq” lesson free?
Yes — the full text of “Editing YAML Configuration Files with yq” is free to read here on the web, and the DevOps Bootcamp course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the DevOps Bootcamp course, upgrade to CoddyKit PRO.
What will I learn in “Editing YAML Configuration Files with yq”?
Read and patch Kubernetes and CI YAML in place using yq while preserving structure and comments. You practise DevOps Bootcamp with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start DevOps Bootcamp?
No prior experience is required. DevOps Bootcamp on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Editing YAML Configuration Files with yq” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this DevOps Bootcamp lesson?
Yes. Every DevOps Bootcamp lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Filtering and Selecting JSON with jq Pipelines
- Transforming and Building JSON Objects with jq
- Consuming REST APIs with curl and jq Together
- Editing YAML Configuration Files with yq