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 Linux Command Line & Bash Scripting Mastery lesson on CoddyKit. This is 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 Linux Command Line & Bash Scripting Mastery learning path, and your progress syncs across the web and the CoddyKit app. The Linux Command Line & Bash Scripting Mastery course includes 4 lessons in total.
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.
Learn Bash with an AI tutor — free
Write and run real code in your browser, get instant help from a 24/7 AI tutor, and pick up where you left off on the web or in the app.
- Courses
- 22
- Lessons
- 88
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. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Linux Command Line & Bash Scripting Mastery course, upgrade to CoddyKit PRO. The Linux Command Line & Bash Scripting Mastery course includes 4 lessons in total.
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 Linux Command Line & Bash Scripting Mastery 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 Linux Command Line & Bash Scripting Mastery?
No prior experience is required. Linux Command Line & Bash Scripting Mastery on CoddyKit is structured for beginners through advanced learners, so you can start here or from the beginning and move at your own pace. This is lesson 4 of 4.
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 Linux Command Line & Bash Scripting Mastery lesson?
Yes. Every Linux Command Line & Bash Scripting Mastery 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