# YAML

## Що це таке

YAML — це текстовий формат даних, у якому структуру задають відступи, двокрапки
й дефіси замість дужок і лапок; саме тому його зазвичай обирають для
конфігурації, яку люди пишуть і переглядають вручну. Назва — рекурсивний акронім
«YAML Ain't Markup Language». Файли мають розширення `.yaml` або `.yml`, а RFC
9512, опублікований у лютому 2024 року, зареєстрував тип медіа
`application/yaml` і назвав `.yaml` бажаним розширенням.

Під скупою пунктуацією лежить невелика модель даних. Є відображення (mappings),
що пов'язують ключі зі значеннями, послідовності, тобто впорядковані списки, і
скаляри: рядки, числа, булеві значення та null. Навколо цієї моделі мова додає
те, що потрібно людині з файлом у редакторі: коментарі, які починаються з `#`,
блокові скаляри, що зберігають розриви рядків через `|` або згортають їх через
`>`, кілька документів в одному файлі, розділених `---`, якорі й псевдоніми
(`&name` і `*name`), завдяки яким одне значення можна використати ще раз деінде,
і теги, що прямо називають тип.

Перший фреймворк для YAML написали на Perl у 2001 році. Версія 1.1 специфікації
вийшла 2005-го, версія 1.2 — 2009-го; головною її метою було зробити YAML
строгою надмножиною JSON, і з неї прибрали чимало старих правил неявної
типізації. Редакція 1.2.2, оприлюднена в жовтні 2021 року, виправила помилки й
уточнила формулювання, не змінивши саму мову. Утім, багато програм досі читає
файли за старими правилами: документація Ansible зазначає, що PyYAML і libyaml
реалізують YAML 1.1.

Отже, YAML стоїть між двома сусідами. JSON — його строга підмножина, лаконічна й
однозначна, створена для обміну даними між програмами. TOML — пласкіший формат
конфігурації, де кожен рядок береться в лапки, тож тип значення ніколи не
доводиться вгадувати. YAML уміє більше за обидва, і саме ця додаткова свобода
породжує його проблеми.

## Чому це важливо

Справжній вибір стосується формату для файлу, який люди підтримуватимуть роками:
описів розгортань, визначень конвеєрів, конфігурації серверів. Плейбуки Ansible
пишуть на YAML, як і конфігураційні файли багатьох інших інфраструктурних
інструментів, тож у такій роботі рішення часто ухвалено заздалегідь. Залишається
вирішити, наскільки суворо писати ці файли.

Головний аргумент на користь YAML — читабельність. Рецензент читає diff, не
рахуючи дужок, коментар пояснює, чому значення саме таке, а багаторядковий
скрипт чи сертифікат уміщується в блоковий скаляр без екранування. З іншого
боку, пробіли мають значення: ключ, зсунутий на два пробіли далі, може опинитися
в іншому відображенні, а файл і далі розбирається. Табуляцію для відступів
заборонено зовсім. Значення без лапок отримують тип за написанням, і правила
написання в 1.1 та 1.2 різняться. Шаблони додають згори ще один синтаксис: в
Ansible значення, що починається з `{{`, треба брати в лапки, інакше парсер
сприйме його як відображення.

Вирішальне питання — як перевірятимуть файл. YAML з лінтером і звичкою брати
рядки в лапки — зручний формат для конфігурації, яку команда редагує спільно.
YAML, який ніхто не перевіряє, — формат, у якому коректний файл може казати щось
інше, ніж мав на увазі автор. Для даних, що передаються між програмами,
надійніший вибір — JSON, а для короткого файлу налаштувань із кількома рівнями
вкладеності простіший TOML.

## Як це виглядає на практиці

Компанія підтримує власне господарство — вебприсутність, git-форджу, базу даних,
резервні копії, поштовий транспорт і DNS, — і
[усе воно описане в 19 плейбуках Ansible і 34 ролях на 12 065 рядках YAML](/uk/portfolio/built-the-companys-infrastructure-as-code-108/).
Композиція тут є даними, а не прапорцями: які ролі виконує хост, записано
YAML-змінними групи його рівня, і розгортання, запущене без аргументів, читає
саме їх, щоб звести кожен хост до оголошеного стану. YAML тут слугує мовою
оголошеного стану, і результат перевіряють, а не припускають: зведений хост має
звітувати про нуль змін. Сторінка чесно називає й ціну. Кожна зміна займає
більше часу, ніж редагування файлу просто на сервері, а оскільки зведення,
застосоване наполовину, гірше за те, що відмовляється, перед ним тепер працює
попередня перевірочна п'єса.

Дванадцять тисяч рядків YAML мусить прочитати машина, перш ніж їх побачить
людина.
[YAML — одна з мов, які перевіряють ворота коміту з 22 лінтерів](/uk/portfolio/built-a-22-linter-commit-gate-127/),
разом із HTML, CSS, JavaScript, Python, Markdown і shell. Кожна діагностика там
зупиняє коміт, бо рівня попереджень у цих воротах немає, а вбудовані придушення
заборонено цілком: коментар ігнорування чи директива вимкнення у файлі перевірку
не обходять. Для YAML це важливо, бо більшість пасток нижче — цілком коректний
синтаксис, і зловити їх до того, як вони потраплять на хост, може лише правило,
яке їх відхиляє.

## Де виникають проблеми

Пастки нижче мають спільну рису: файл розбирається без помилок, але означає щось
інше.

**Код країни перетворюється на false.** За правилами YAML 1.1 значення `yes`,
`no`, `on`, `off`, `y` і `n` без лапок є булевими, причому в кількох варіантах
регістру. Базова схема (core schema) YAML 1.2 залишає тільки `true` і `false` у
трьох написаннях, а RFC 9512 попереджає, що парсер за старим синтаксисом може
прочитати `{insecure: n}` як false, а не як літеру n. Список північних країн
показує проблему:

```yaml
країни:
    - DK
    - SE
    - NO # YAML 1.1 читає це як false
```

Беріть у лапки кожен рядок, який можна прочитати як щось інше, записуйте булеві
значення лише як `true` і `false` і доручіть лінтеру стежити за обома правилами.

**Номер версії втрачає цифру.** Числа без лапок перетворюються, тож
`version: 1.10` надходить як число з рухомою комою 1.1, а за правилами 1.1
початковий нуль позначає вісімкове число, і код `010` стає вісімкою.
Документація Ansible саме тому радить брати версію на кшталт `"1.0"` у лапки.
Беріть у лапки ідентифікатори, коди й версії: це рядки, у яких просто
трапляються цифри.

**Ключ опиняється під чужим батьком.** Відступ визначає, якому відображенню
належить ключ, тож рядок, зсунутий на два пробіли, переносить налаштування в
інше місце без жодного повідомлення про помилку. Специфікація також вимагає, щоб
кожен ключ у відображенні був унікальним, а повторений ключ глибоко в довгому
блоці легко проґавити під час рецензії, тому yamllint має окреме правило для
кожної з цих двох помилок. Запускайте його до того, як файл піде в роботу.

**Завантаження файлу запускає код.** Теги можуть називати типи конкретної мови
програмування, і завантажувач, який їх виконує, збудує будь-який об'єкт, що його
замовить файл. Документація PyYAML пише, що повний `yaml.load` такий самий
потужний, як `pickle.load`, і може викликати будь-яку функцію Python, та радить
натомість `yaml.safe_load`, що розпізнає лише стандартні теги. RFC 9512 просить,
щоб виконання коду під час десеріалізації було типово вимкнене. Використовуйте
безпечний завантажувач для кожного файлу, що прийшов ззовні.

**Короткий файл заповнює пам'ять.** Якорі й псевдоніми роблять документ YAML
графом, а не деревом. Коли кожен якір двічі повторює попередній, кожен новий
рядок подвоює розмір, і тридцять коротких рядків описують понад мільярд
текстових значень, щойно програма обходить результат як звичайне дерево. RFC
9512 називає це експоненційним розростанням даних, атакою billion laughs, і
відповідає на неї процесорами, що обмежують глибину рекурсії якорів. Приймайте
YAML ззовні лише через такий процесор і перевіряйте це обмеження на тому
парсері, який справді використовуєте.

## Що почитати далі

- [YAML 1.2.2 specification](https://yaml.org/spec/1.2.2/) — сама мова,
  зокрема базова схема, яка визначає типи значень без лапок.
- [RFC 9512: YAML Media Type](https://www.rfc-editor.org/rfc/rfc9512) —
  реєстрація `application/yaml` із нотатками з безпеки про булеві значення,
  виконання коду й розгортання псевдонімів.
- [YAML specification changes](https://yaml.org/spec/1.2.2/ext/changes/) —
  що змінила кожна редакція, від 1.2.1 до 1.2.2.
- [Ansible: YAML Syntax](https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html)
  — підмножина, якою користується Ansible, і її пастки з лапками.
- [yamllint rules](https://yamllint.readthedocs.io/en/stable/rules.html) —
  правила для булевих значень, дубльованих ключів, вісімкових чисел і відступів.
- [PyYAML documentation](https://pyyaml.org/wiki/PyYAMLDocumentation) —
  різниця між `load` і `safe_load` словами самої бібліотеки.

<https://engineer.company/uk/glossary/yaml/>
