Jeśli kiedykolwiek kopiowałeś blok YAML z jednego miejsca w pliku do innego, już natrafiłeś na problem, który rozwiązują kotwice i aliasy. Duże konfiguracje Kubernetes, wielośrodowiskowe pliki Docker Compose i złożone potoki CI mają tendencję do powtarzania tych samych bloków — limitów zasobów, zmiennych środowiskowych, montowań woluminów — w kółko. YAML ma wbudowaną odpowiedź na to, o której większość deweloperów nie wie, że istnieje.

Kotwice (&) i aliasy (*) to mechanizm YAML do definiowania wartości raz i odwoływania się do niej w dowolnym innym miejscu tego samego pliku. Traktuj kotwicę jak deklarację zmiennej, a alias jak jej użycie. Klucz scalania (<<) rozszerza to o scalanie obiektów — podobne do operatora spread w JavaScript. Obie funkcje są zdefiniowane w sekcji specyfikacji YAML 1.2 o kotwicach węzłów.

Podstawowa składnia kotwic i aliasów

Składnia jest minimalna. Poprzedź wartość przez &name, aby utworzyć kotwicę, a następnie użyj *name w dowolnym miejscu dokumentu, aby się do niej odwołać:

yaml
# Define the anchor once
default_timeout: &timeout 30

# Reference it multiple times
services:
  api:
    timeout: *timeout      # → 30
  worker:
    timeout: *timeout      # → 30
  scheduler:
    timeout: *timeout      # → 30

Gdy parser YAML odczyta to, każdy alias rozwiązuje się do dokładnie tej samej wartości co kotwica. Zmień kotwicę, a wszystkie aliasy zostaną zaktualizowane. Nie potrzeba ręcznej zamiany.

Scalanie obiektów kluczem <<

Ponowne użycie skalara jest przydatne, ale prawdziwa moc pochodzi ze scalania całych bloków odwzorowań. Klucz scalania << wchłania wszystkie pary klucz-wartość z kotwicy do bieżącego odwzorowania:

yaml
# Define a base configuration block
base_service: &base
  restart: unless-stopped
  logging:
    driver: json-file
    options:
      max-size: "10m"
      max-file: "3"
  networks:
    - app-network

# Merge it into specific services
services:
  api:
    <<: *base              # merge all keys from base_service
    image: my-api:latest
    ports:
      - "8080:8080"

  worker:
    <<: *base              # same base config
    image: my-worker:latest
    command: ["node", "worker.js"]

  scheduler:
    <<: *base              # and again
    image: my-scheduler:latest
    command: ["node", "cron.js"]

Klucze zdefiniowane bezpośrednio w odwzorowaniu nadpisują klucze ze scalonej kotwicy. Jeśli więc base ustawia restart: unless-stopped, a serwis ustawia też restart: no, wygrywa wartość na poziomie serwisu.

Przed i po: Docker Compose w praktyce

Oto jak wygląda typowy wielousługowy plik Docker Compose bez kotwic — pełen powtórzeń:

yaml
# BEFORE — repetitive and hard to maintain
version: "3.9"
services:
  api:
    restart: unless-stopped
    env_file: .env
    networks: [app-network]
    logging:
      driver: json-file
      options:
        max-size: "10m"
    depends_on:
      - postgres

  worker:
    restart: unless-stopped
    env_file: .env
    networks: [app-network]
    logging:
      driver: json-file
      options:
        max-size: "10m"
    depends_on:
      - postgres

  mailer:
    restart: unless-stopped
    env_file: .env
    networks: [app-network]
    logging:
      driver: json-file
      options:
        max-size: "10m"
    depends_on:
      - postgres

Teraz z kotwicami i kluczem scalania — każdy powtarzający się blok jest zdefiniowany dokładnie raz:

yaml
# AFTER — DRY and easy to maintain
version: "3.9"

x-common: &common
  restart: unless-stopped
  env_file: .env
  networks: [app-network]
  logging:
    driver: json-file
    options:
      max-size: "10m"
  depends_on:
    - postgres

services:
  api:
    <<: *common
    image: my-api:2.4.1
    ports:
      - "8080:8080"

  worker:
    <<: *common
    image: my-worker:2.4.1
    command: ["node", "worker.js"]

  mailer:
    <<: *common
    image: my-mailer:2.4.1
    command: ["node", "mailer.js"]
Wskazówka pro: Prefiks x- na polach rozszerzeń (jak x-common) to konwencja Docker Compose, która uniemożliwia Compose próbę parsowania bloku jako definicji serwisu. Jest ignorowany przez silnik Compose, ale w pełni użyteczny jako kotwica YAML.

GitHub Actions: Strategia matrix z kotwicami

Konfiguracja CI to kolejne miejsce, gdzie kotwice się opłacają. Oto przepływ pracy GitHub Actions, który ponownie używa wspólnej konfiguracji kroków w wielu zadaniach:

yaml
name: Build and Test

on: [push, pull_request]

# Shared setup steps
x-setup-steps: &setup-steps
  - uses: actions/checkout@v4
  - uses: actions/setup-node@v4
    with:
      node-version: "20"
      cache: npm
  - run: npm ci

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - *setup-steps
      - run: npm run lint

  test:
    runs-on: ubuntu-latest
    steps:
      - *setup-steps
      - run: npm test -- --coverage

  build:
    runs-on: ubuntu-latest
    steps:
      - *setup-steps
      - run: npm run build

Uwaga: Parser YAML GitHub Actions obsługuje kotwice i aliasy. Jednak niektóre systemy CI (zwłaszcza CircleCI's config.yml przed wersją 2.1) miały własną składnię podobną do kotwic. Zawsze sprawdzaj dokumentację swojego konkretnego narzędzia.

Kubernetes: Ponowne użycie limitów zasobów

W manifestach Kubernetes, limity zasobów i zmienne środowiskowe to główni kandydaci do kotwic. Oto wzorzec, którego regularnie używam:

yaml
# Anchors at the top of the file (or in a shared ConfigMap generator)
x-resources:
  small: &resources-small
    requests:
      memory: "64Mi"
      cpu: "50m"
    limits:
      memory: "256Mi"
      cpu: "200m"
  large: &resources-large
    requests:
      memory: "256Mi"
      cpu: "200m"
    limits:
      memory: "1Gi"
      cpu: "1000m"

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
spec:
  template:
    spec:
      containers:
        - name: frontend
          image: frontend:latest
          resources: *resources-small    # reuse small profile

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  template:
    spec:
      containers:
        - name: api
          image: api:latest
          resources: *resources-large    # reuse large profile

Wielokrotne scalania i priorytety nadpisywania

Możesz scalać z wielu kotwic w jednym odwzorowaniu. Podaj je jako listę pod <<. Wcześniejsze wpisy na liście mają priorytet nad późniejszymi dla zduplikowanych kluczy:

yaml
defaults: &defaults
  color: blue
  size: medium
  weight: light

overrides: &overrides
  color: red          # will override defaults.color
  weight: heavy       # will override defaults.weight

result:
  <<: [*overrides, *defaults]    # overrides takes priority
  name: widget
# Result: color=red, size=medium, weight=heavy, name=widget

Ograniczenia, o których warto wiedzieć

  • Tylko ten sam plik. Kotwice i aliasy działają tylko w obrębie pojedynczego strumienia dokumentu YAML. Nie można odwoływać się do kotwicy z innego pliku. Do ponownego użycia między plikami potrzebujesz własnego mechanizmu swojego narzędzia (np. szablonów Helm, Kustomize lub procesorów YAML jak ytt).
  • Brak obliczanych wartości. Aliasy rozwiązują się do dokładnych kopii wartości kotwicy. Nie ma interpolacji ani logiki szablonu — to zadanie dla narzędzi takich jak Helm lub Jinja2.
  • Klucz scalania to rozszerzenie YAML. Klucz scalania << jest zdefiniowany w rozszerzeniu YAML 1.1, a nie w głównej specyfikacji. Większość parserów go obsługuje, ale ściśle zgodne parsery YAML 1.2 mogą nie.
  • Odwołania cykliczne psują parsery. Nie próbuj kotwicować węzła i odwoływać się do niego z jego wnętrza. Jest to technicznie nieprawidłowe.
  • Aliasy kopiują, nie referencjonują. Modyfikowanie wartości rozwiązanego aliasu w kodzie nie zmienia kotwicy ani innych aliasów. Po parsowaniu są to niezależne wartości.

Podsumowanie

Kotwice i aliasy YAML to jedna z najbardziej niedocenianych funkcji tego formatu. W każdym pliku YAML dłuższym niż około 50 linii, gdzie ten sam blok konfiguracyjny pojawia się więcej niż dwa razy, kotwice prawie na pewno powinny tam być. Różnica w możliwościach utrzymania przed/po jest dramatyczna — zmiana jednego profilu limitu zasobów lub jednej współdzielonej zmiennej środowiskowej zamiast przeszukiwania całego pliku. Użyj Formatera YAML, aby pliki z kotwicami były schludne, i Walidatora YAML, aby potwierdzić, że odwołania do kotwic rozwiązują się poprawnie.