~/guides/yaml-anchors-docker-compose

YAML

YAML anchors и merge keys: один docker compose на три окружения без копипасты

Как раздать конфиг на dev, staging и prod через якоря и ссылки YAML, не размножая одни и те же настройки по трем файлам.

Классическая ситуация: три docker-compose файла, dev, staging и prod, и в каждом на девяносто процентов одинаковый набор настроек. Restart policy одинаковый, logging одинаковый, network одинаковая, различается по сути пара переменных окружения и лимит памяти. А потом кто-то в пятницу вечером правит restart policy в dev файле, забывает про два остальных, и в понедельник продакшн падает при первом же чихе контейнера, потому что там осталась настройка по умолчанию.

YAML anchors решают именно эту проблему. Идея простая: один раз описываешь блок настроек, ставишь на него якорь через амперсанд, а дальше ссылаешься на этот якорь звездочкой в любом другом месте документа.

Базовый синтаксис anchor и alias

x-common-settings: &common
  restart: unless-stopped
  logging:
    driver: json-file
    options:
      max-size: "10m"

services:
  web:
    <<: *common
    image: nginx:alpine

Амперсанд перед common это объявление якоря, звездочка в <<: *common это ссылка на него, а сам двойной символ меньше это специальный merge key, который говорит YAML “подмешай сюда все поля из указанного якоря”. Сервис web в итоге получит и restart, и logging, как будто вы прописали их вручную.

Раздача по трем окружениям через переопределение

Вот тут начинается настоящая магия. Merge key не просто копирует блок, он позволяет локально переопределить отдельные поля поверх унаследованных.

x-common-settings: &common
  restart: unless-stopped
  logging:
    driver: json-file
    options:
      max-size: "10m"

services:
  web:
    <<: *common
    image: myapp:latest
    deploy:
      resources:
        limits:
          memory: 512m

Здесь web берет restart и logging из якоря, но добавляет свои собственные поля image и deploy, которых в якоре не было. Если то же самое поле есть и в якоре, и в самом сервисе, побеждает то значение, которое написано в сервисе, то есть локальное переопределение имеет приоритет.

Практический пример: dev, staging, prod в одном файле

Окружение memory limit replicas log level
dev без ограничения 1 debug
staging 512m 2 info
prod 1024m 4 warn

Вместо трех файлов можно держать один compose файл с профилями, где базовые настройки идут через якорь, а специфика окружения переопределяется через merge key на уровне каждого сервиса. Комбинируя это с docker compose --profile prod up, получаем один файл, три поведения, ноль копипасты.

Когда это не работает

Merge key переопределяет поля только на первом уровне вложенности объекта, а не рекурсивно вглубь. Если внутри logging.options в сервисе вы хотите поменять только max-size, но оставить остальные опции logging из якоря, этого не произойдет автоматически: весь блок logging в сервисе просто заменит весь блок logging из якоря целиком.

Это не баг YAML, а особенность именно merge key: он работает на уровне ключей объекта верхнего уровня, а не сливает вложенные объекты рекурсивно.

Если нужно менять именно вложенный параметр, проще завести отдельный более специфичный якорь только для этого случая, чем полагаться на автоматическое глубокое слияние, которого в спецификации YAML попросту нет.

Итоговый чеклист

  • Выносите в якорь только то, что реально повторяется в трех и более местах, ради одного повторения городить якорь не стоит
  • Помните, что переопределение работает только на первом уровне вложенности, не рекурсивно
  • Проверяйте итоговую конфигурацию через docker compose config, эта команда покажет полностью развернутый YAML без якорей, ровно то, что реально увидит docker
  • Если конфиг стал сложным и трудночитаемым из-за большого числа якорей, это сигнал вынести общую часть в отдельный docker-compose.base.yml и подключать через extends, а не наращивать вложенность якорей до бесконечности

Инструменты по теме

Если якоря в вашем файле уже разрослись так, что сложно на глаз понять итоговый результат после всех подстановок, разверните файл полностью через YAML Anchors Resolver, он покажет документ без единой ссылки, только с реальными значениями.

Если наоборот, нужно объединить базовый compose файл с override без якорей вообще, для этого есть отдельный Docker Compose Merge, который сливает два файла по тем же правилам, что использует сам docker compose.