Chapters: JavaScript HTML & CSS JSON XML YAML ← Full TOC

📜 Chapter 34 — YAML

Complete reference for YAML — syntax, Python, Kubernetes, Docker Compose, Ansible playbooks, GitHub Actions, and comparison with JSON and TOML.
Part of The Direct Path to Linux Ubuntu — free for all.

4Topics
91+Concepts
5Examples
yamlFormat
YAML 1.2Standard
FreeNo login
📝 YAML Syntax 🐍 YAML in Python ⚙️ YAML in DevOps ⚖️ YAML vs JSON vs TOML ⚡ Examples

📝 YAML Syntax

📝 YAML SyntaxScalars, sequences, mappings, anchors, and multiline strings

🐍 YAML in Python

🐍 YAML in PythonPyYAML and ruamel.yaml — load, dump, safe_load

⚙️ YAML in DevOps

⚙️ YAML in DevOpsKubernetes, Docker Compose, Ansible, GitHub Actions

⚖️ YAML vs JSON vs TOML

⚖️ YAML vs JSON vs TOMLCompare data formats — when to use which

⚡ Quick Examples

YAML files use .yaml or .yml extension. Run Python examples with python3 script.py.

YAML Syntax Cheatsheet

All YAML data types and structures in one file.

---
# YAML Cheatsheet — MyWebUniversity Chapter 34
# YAML is a superset of JSON — all JSON is valid YAML

# ── Scalars ──────────────────────────────────────────────────
string_bare:    Hello World
string_quoted:  "Hello, World!"
string_single:  'No interpolation: ${var}'
integer:        42
float:          3.14159
boolean_true:   true
boolean_false:  false
null_val:       null
null_tilde:     ~
date:           2026-06-25
datetime:       2026-06-25T08:00:00Z

# ── Multiline ─────────────────────────────────────────────────
literal_block: |
  Line 1 preserved
  Line 2 preserved
  Trailing newline kept

folded_block: >
  These lines are
  folded into one space-separated
  paragraph with a trailing newline.

no_newline: |-
  Last line, no trailing newline

# ── Collections ───────────────────────────────────────────────
sequence:
  - item one
  - item two
  - item three

inline_seq: [one, two, three]

mapping:
  key1: value1
  key2: value2
  key3: value3

inline_map: {a: 1, b: 2, c: 3}

# ── Nested ────────────────────────────────────────────────────
students:
  - name:  Alice
    score: 95
    tags:  [dean-list, scholarship]
    address:
      city:  LA
      state: CA
  - name:  Bob
    score: 87
    tags:  []
    address:
      city:  NYC
      state: NY

# ── Anchors & aliases ─────────────────────────────────────────
defaults: &defaults
  timeout:   30
  retries:   3
  log_level: info

development:
  <<: *defaults     # merge defaults
  log_level: debug  # override

production:
  <<: *defaults
  timeout:   60     # override
  replicas:  3      # add new key

Read & Write YAML (Python)

Full YAML file I/O with PyYAML.

import yaml
from pathlib import Path

# Write YAML config file
config = {
    'app': {
        'name': 'MyWebUniversity',
        'version': '2.0',
        'debug': False,
    },
    'server': {
        'host': '0.0.0.0',
        'port': 8080,
        'workers': 4,
    },
    'database': {
        'host': 'localhost',
        'port': 5432,
        'name': 'mywebuniversity',
        'pool': {'min': 2, 'max': 10},
    },
    'features': ['search', 'analytics', 'notifications'],
    'admins': [
        {'name': 'Alice', 'email': 'alice@mywu.com', 'role': 'superadmin'},
        {'name': 'Bob',   'email': 'bob@mywu.com',   'role': 'admin'},
    ],
}

yaml_str = yaml.dump(config, default_flow_style=False,
                     allow_unicode=True, sort_keys=False, indent=2)
Path('config.yaml').write_text(yaml_str)
print("=== Written config.yaml ===")
print(yaml_str)

# Read back
with open('config.yaml') as f:
    loaded = yaml.safe_load(f)

print(f"App:     {loaded['app']['name']} v{loaded['app']['version']}")
print(f"Server:  {loaded['server']['host']}:{loaded['server']['port']}")
print(f"DB pool: {loaded['database']['pool']['min']}-{loaded['database']['pool']['max']}")
print(f"Features: {', '.join(loaded['features'])}")

# Merge with overrides
overrides = yaml.safe_load("""
server:
  port: 9090
  workers: 8
app:
  debug: true
""")

def deep_merge(base, override):
    result = dict(base)
    for k, v in override.items():
        if k in result and isinstance(result[k], dict) and isinstance(v, dict):
            result[k] = deep_merge(result[k], v)
        else:
            result[k] = v
    return result

merged = deep_merge(loaded, overrides)
print(f"\nAfter merge — port: {merged['server']['port']}  debug: {merged['app']['debug']}")

Path('config.yaml').unlink()
# python3 yaml_io.py  (pip install pyyaml)

Kubernetes Manifest

Complete Kubernetes Deployment, Service, and ConfigMap.

---
# kubernetes/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name:      mywebuniversity
  namespace: default
  labels:
    app:     mywebuniversity
    version: "2.0"
spec:
  replicas: 2
  selector:
    matchLabels:
      app: mywebuniversity
  template:
    metadata:
      labels:
        app: mywebuniversity
    spec:
      containers:
        - name:  web
          image: mywebuniversity/web:2.0
          ports:
            - containerPort: 80
          envFrom:
            - configMapRef:
                name: app-config
          resources:
            requests: { cpu: "100m", memory: "128Mi" }
            limits:   { cpu: "500m", memory: "512Mi" }
          livenessProbe:
            httpGet:
              path: /health
              port: 80
            initialDelaySeconds: 15
            periodSeconds: 20
---
apiVersion: v1
kind: Service
metadata:
  name: mywebuniversity-svc
spec:
  selector:
    app: mywebuniversity
  ports:
    - protocol: TCP
      port:       80
      targetPort: 80
  type: ClusterIP
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  NODE_ENV:    production
  LOG_LEVEL:   info
  PORT:        "80"
  SITE_URL:    https://www.mywebuniversity.com

# Deploy:
# kubectl apply -f kubernetes/
# kubectl get pods
# kubectl rollout status deployment/mywebuniversity
# kubectl scale deployment mywebuniversity --replicas=5

Docker Compose Full Stack

Multi-service Docker Compose with health checks.

# docker-compose.yml — MyWebUniversity full stack
# Usage: docker compose up -d

services:

  # ── Web / CGI server ───────────────────────────────────────
  web:
    image: ubuntu/apache2:latest
    container_name: mywebuniversity_web
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./html:/var/www/React/prod:ro
      - ./cgi-bin:/usr/lib/cgi-bin:ro
      - ./ssl:/etc/ssl/mywebuniversity:ro
      - ./apache2.conf:/etc/apache2/sites-enabled/000-default.conf:ro
    environment:
      APACHE_RUN_USER:  www-data
      APACHE_RUN_GROUP: www-data
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped
    healthcheck:
      test:         ["CMD", "curl", "-f", "http://localhost/health"]
      interval:     30s
      timeout:      10s
      retries:      3
      start_period: 10s

  # ── Database ────────────────────────────────────────────────
  db:
    image: postgres:16-alpine
    container_name: mywebuniversity_db
    environment:
      POSTGRES_DB:       mywebuniversity
      POSTGRES_USER:     ${DB_USER:-admin}
      POSTGRES_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD required}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./sql/init.sql:/docker-entrypoint-initdb.d/01_init.sql:ro
    ports:
      - "127.0.0.1:5432:5432"   # localhost only — not public
    restart: unless-stopped
    healthcheck:
      test:     ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 10s
      timeout:  5s
      retries:  5

  # ── Cache ───────────────────────────────────────────────────
  cache:
    image: redis:7-alpine
    container_name: mywebuniversity_cache
    command: >
      redis-server
      --maxmemory      256mb
      --maxmemory-policy allkeys-lru
      --requirepass    ${REDIS_PASSWORD:-changeme}
    volumes:
      - redis_data:/data
    restart: unless-stopped

  # ── Database admin ──────────────────────────────────────────
  pgadmin:
    image: dpage/pgadmin4:latest
    environment:
      PGADMIN_DEFAULT_EMAIL:    admin@mywebuniversity.com
      PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD:-admin}
    ports:
      - "127.0.0.1:5050:80"
    depends_on:
      - db
    profiles:
      - tools    # only starts with: docker compose --profile tools up

volumes:
  postgres_data:
  redis_data:

networks:
  default:
    name: mywebuniversity_net

# .env file (never commit to git):
# DB_PASSWORD=supersecretpassword
# REDIS_PASSWORD=redispassword
# PGADMIN_PASSWORD=pgadminpassword

GitHub Actions CI/CD

Complete CI/CD workflow with tests and deployment.

# .github/workflows/deploy.yml
name: Test, Build & Deploy

on:
  push:
    branches: [main]
    paths:
      - 'src/**'
      - 'cgi-bin/**'
      - '.github/workflows/**'
  pull_request:
    branches: [main]
  workflow_dispatch:
    inputs:
      environment:
        description: 'Target environment'
        required: true
        default: 'staging'
        type: choice
        options: [staging, production]

env:
  PYTHON_VERSION: '3.12'
  NODE_VERSION:   '20'

jobs:
  # ── Lint & Test ─────────────────────────────────────────────
  test:
    name: Lint & Test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: ${{ env.PYTHON_VERSION }}
          cache: pip

      - name: Install dependencies
        run: |
          pip install flake8 pytest pytest-cov pyyaml lxml
          pip install -r requirements.txt 2>/dev/null || true

      - name: Lint CGI scripts
        run: |
          flake8 cgi-bin/**/*.cgi --max-line-length=120             --ignore=E501,W503 || true

      - name: Validate YAML files
        run: |
          python3 -c "
          import yaml, pathlib, sys
          errors = []
          for f in pathlib.Path('.').rglob('*.yaml'):
            if '.git' in str(f): continue
            try:
              yaml.safe_load(f.read_text())
              print(f'  ✅ {f}')
            except yaml.YAMLError as e:
              errors.append(f'  ❌ {f}: {e}')
          if errors:
            for e in errors: print(e)
            sys.exit(1)
          "

      - name: Run tests
        run: |
          python3 -m pytest tests/ -v             --cov=cgi-bin             --cov-report=xml             --cov-report=term-missing             --tb=short 2>/dev/null || echo "No tests found"

      - name: Upload coverage
        uses: codecov/codecov-action@v4
        if: always()
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  # ── Deploy ──────────────────────────────────────────────────
  deploy:
    name: Deploy to ${{ inputs.environment || 'production' }}
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    environment:
      name: production
      url: https://www.mywebuniversity.com
    steps:
      - uses: actions/checkout@v4

      - name: Deploy CGI scripts
        env:
          SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
          DEPLOY_HOST:     ${{ secrets.DEPLOY_HOST }}
          DEPLOY_USER:     ${{ secrets.DEPLOY_USER }}
        run: |
          echo "$SSH_PRIVATE_KEY" > /tmp/deploy_key
          chmod 600 /tmp/deploy_key
          rsync -avz --delete             -e "ssh -i /tmp/deploy_key -o StrictHostKeyChecking=no"             cgi-bin/ $DEPLOY_USER@$DEPLOY_HOST:/usr/lib/cgi-bin/
          rsync -avz --delete             -e "ssh -i /tmp/deploy_key -o StrictHostKeyChecking=no"             html/  $DEPLOY_USER@$DEPLOY_HOST:/var/www/React/prod/

      - name: Notify deployment
        run: |
          echo "✅ Deployed to production at $(date -u)"
          echo "   Commit: ${{ github.sha }}"
          echo "   Author: ${{ github.actor }}"

      - name: Smoke test
        run: |
          sleep 5
          curl -sf https://www.mywebuniversity.com/health || exit 1
          echo "✅ Health check passed"