logo

Дополнительные типы блоков (EBT) — новый опыт конструктора страниц❗

Дополнительные типы блоков (EBT) — стилизованные, настраиваемые типы блоков: слайдшоу, вкладки, карточки, аккордеоны и многие другие. Встроенные настройки для фона, DOM Box, плагины Javascript.

Демо EBT модули Скачать EBT модули

❗Дополнительные типы параграфов (EPT) — новый опыт работы с параграфами

Дополнительные типы параграфов (EPT) — набор модулей, основанный на аналогичных параграфах.

Демо EPT модули Скачать EPT модули

GLightbox is a pure javascript lightbox (Colorbox alternative without jQuery)❗

It can display images, iframes, inline content and videos with optional autoplay for YouTube, Vimeo and even self-hosted videos.

Demo GLightbox Download GLightbox

Scroll

Управление конфигурацией Drupal на основе CI с использованием Jenkins и GitLab CI

16/04/2026, by Ivan

1. Почему управление конфигурацией на основе CI имеет значение

Система конфигурации Drupal — одна из самых сильных сторон платформы и одновременно один из самых надёжных источников боли. Возможность экспортировать и импортировать каждый элемент конфигурации сайта в виде YAML‑файлов невероятно мощна — но только если все согласны с тем, кто именно отвечает за перемещение этих файлов между окружениями. В большинстве команд такого согласия на самом деле не существует.

Классические проблемы хорошо знакомы каждому, кто запускал Drupal‑сайт:

  • Дрейф конфигурации — staging расходится с production, production — с локальным окружением, и никто не уверен, какое из них является эталонным.
  • «Работает на staging, но не на prod» — потому что кто‑то обновил представление или форматтер поля на staging и так и не экспортировал изменения.
  • Ручной drush cim, ломающий контент — поспешный импорт в 23:00, который удалил поле типа материала, всё ещё используемое живыми нодами.

Первопричина у всех этих сценариев одна и та же: человек решает, когда и нужно ли продвигать конфигурацию. Люди забывают. Пропускают шаги под давлением. Принимают решения, которые оказываются неверными.

CI не забывает. Пайплайн либо проходит, либо падает. У него нет митинга, на который нужно бежать. Он не знает, что релиз через двадцать минут. Именно такая детерминированность и нужна управлению конфигурацией.

Обещания, которые выполняет эта статья:

  • Каждое изменение конфигурации коммитится в Git до того, как попадает в любое общее окружение.
  • За валидацию и импорт конфигурации отвечает пайплайн, а не разработчик.
  • Продвижение между окружениями не требует ни одного ручного шага.
  • Дрейф конфигурации — это ошибка сборки, а не сообщение в Slack.
Предположения: вы используете Drupal 10 или 11, Git‑ориентированный workflow с feature‑ветками и деплоите как минимум в два общих окружения (например, staging и production). В команде используется Jenkins или GitLab CI — либо оба.

2. Базовые принципы, которые мы вынесли из реальных проектов

Конфигурация — это код

Если что‑то меняет поведение сайта, ему место в Git. Точка. Представление, тип материала, настройка производительности, стиль изображения — всё это код. Обращайтесь с конфигурационными файлами так же дисциплинированно, как с PHP‑файлами: проводите ревью, версионируйте, никогда не редактируйте их напрямую в общем окружении.

Никакого ручного drush cim в общих окружениях

Конфигурацию импортирует пайплайн. Не разработчики. Это правило звучит радикально, пока вы впервые не столкнётесь с тем, как кто‑то запускает drush cim на production, имея в рабочей директории незафиксированные локальные изменения.

Пайплайны должны падать при любом дрейфе конфигурации

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

Старший разработчик отлаживал медленную страницу поиска на production. Он подкрутил настройки индекса Search API через UI, убедился, что производительность улучшилась, и закрыл задачу. Через две недели релиз импортировал конфигурацию из Git, перезаписав его UI‑изменения, — и производительность поиска рухнула. Три дня никто не связывал эти события. После этого инцидента мы чётко зафиксировали правило: любое UI‑изменение, которое не было немедленно экспортировано и закоммичено, считается потерянным.
На сайте с большим объёмом медиа файл core.extension.yml был исключён из экспортов, потому что «модулями всё равно управляет Composer». Три месяца спустя хотфикс вновь включил модуль, который намеренно был отключён на production. Хотфикс был корректным — но последующий импорт конфигурации молча снова отключил модуль. Именно после этого случая мы стали называть core.extension.yml самым опасным файлом, который только можно игнорировать.

3. Архитектура высокого уровня

👨‍💻 Разработчик
drush cex
📁 Git-репозиторий
config/sync
⚙ CI-пайплайн
валидация + импорт
🌐 Dev
🌐 Stage
🌐 Prod

Поток конфигурации движется только в одном направлении: от локального экспорта разработчика через Git, через CI‑пайплайн и далее в каждое окружение.

Ключевая идея — разделение зон ответственности:

УчастникОтветственностьНикогда не отвечает за
РазработчикЭкспорт конфигурации, коммит в Git, создание MR/PRИмпорт конфигурации в любом общем окружении
CI‑пайплайнВалидация, импорт, проверка, продвижениеГенерация или редактирование конфигурационных файлов
ОкружениеЗапуск сайтаГенерация, хранение или экспорт конфигурации

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

4. Масштабируемая структура репозитория

Структура директорий корень проекта
project-root/
├── composer.json
├── composer.lock
├── Jenkinsfile
├── .gitlab-ci.yml
│
├── ci/
│   ├── drupal-config-check.sh   # Общий скрипт валидации
│   ├── drupal-deploy.sh
│   └── drupal-install.sh
│
├── config/
│   ├── sync/                        # Здесь хранится конфигурация — коммитится в Git
│   │   ├── core.extension.yml
│   │   ├── system.site.yml
│   │   └── ...
│   └── splits/                      # Переопределения config_split для каждого окружения
│       ├── development/
│       ├── staging/
│       └── production/
│
└── web/
    ├── sites/
    │   └── default/
    │       ├── settings.php          # Коммитится, без секретов
    │       ├── settings.local.php    # Игнорируется Git, локальные переопределения
    │       └── settings.env.php      # Загружается CI из env-переменных
    └── ...

Что находится в config/sync

Все YAML‑файлы конфигурации, сгенерированные командой drush cex. Эта директория является единственным источником истины для конфигурации сайта. Она коммитится, проходит ревью и деплоится так же, как и любой другой код.

Что никогда не должно попадать в конфигурацию

  • Имена хостов, API‑ключи или учётные данные, специфичные для окружения — используйте переменные окружения и settings.env.php.
  • Секреты любого вида — внедряйте через хранилище секретов CI (Jenkins Credentials или GitLab CI Variables).
  • Контент — не используйте модуль Default Content как костыль там, где должна быть конфигурация.
PHP web/sites/default/settings.php
// settings.php — коммитится в Git, независим от окружения
$settings['config_sync_directory'] = DRUPAL_ROOT . '/../config/sync';

// Загружаем значения, специфичные для окружения, внедрённые CI или хостингом.
if (file_exists($app_root . '/' . $site_path . '/settings.env.php')) {
  include $app_root . '/' . $site_path . '/settings.env.php';
}

// Загружаем опциональные локальные переопределения (игнорируются Git).
if (file_exists($app_root . '/' . $site_path . '/settings.local.php')) {
  include $app_root . '/' . $site_path . '/settings.local.php';
}
PHP web/sites/default/settings.env.php
// Генерируется CI из переменных окружения — никогда не коммитится.
$databases['default']['default'] = [
  'driver'   => 'mysql',
  'host'     => getenv('DB_HOST'),
  'database' => getenv('DB_NAME'),
  'username' => getenv('DB_USER'),
  'password' => getenv('DB_PASS'),
  'port'     => getenv('DB_PORT') ?: 3306,
  'prefix'   => '',
];

$settings['hash_salt'] = getenv('DRUPAL_HASH_SALT');

// Сообщаем config_split, в каком окружении мы находимся.
$config['config_split.config_split.production']['status'] =
  (getenv('APP_ENV') === 'production');

5. Рабочий процесс конфигурации Drupal

5.1 Локальная разработка

Локальный рабочий процесс — это единственное место, где цикл обратной связи между изменениями в UI и экспортом конфигурации должен быть быстрым и привычным. Каждый разработчик в проекте следует одним и тем же правилам:

  • Вносить изменения в UI или код локально.
  • Немедленно запускать drush cex — не в конце дня.
  • Просматривать diff с помощью git diff config/sync.
  • Либо закоммитить изменения, либо отбросить их с помощью git checkout -- config/sync.
  • Промежуточного состояния не существует.
Shell Локальный workflow разработки
# После внесения изменений конфигурации через Drupal UI или install hooks:
drush cex --yes

# Просматриваем изменения — относитесь к этому как к ревью любого кода:
git diff config/sync

# Добавляем в staging и коммитим вместе с кодом фичи:
git add config/sync
git commit -m "feat(search): add fulltext search API index config"

# Или отбрасываем, если изменение было экспериментальным и ещё не готово:
git checkout -- config/sync

Мы дополнительно обеспечиваем это с помощью лёгкого Git pre-commit hook, который предупреждает (но не блокирует) ситуацию, когда PHP‑ или шаблонные файлы добавлены в staging без соответствующих изменений в config/sync:

Shell .git/hooks/pre-commit
#!/bin/bash
# Предупреждать, если код модуля/темы изменился, а config/sync — нет.
CHANGED_CODE=$(git diff --cached --name-only | grep -E '\.(php|module|theme|install)$')
CHANGED_CONFIG=$(git diff --cached --name-only | grep '^config/sync')

if [[ -n "$CHANGED_CODE" ]] && [[ -z "$CHANGED_CONFIG" ]]; then
  echo "⚠  Warning: PHP/module files staged but no config/sync changes detected."
  echo "   Did you forget to run: drush cex ?"
  echo "   Proceeding anyway — but double-check."
fi
exit 0

5.2 Feature-ветки

Конфигурация живёт рядом с кодом, который от неё зависит — в одной и той же ветке и в том же pull/merge request. Фича, добавляющая новый тип материала, должна включать как PHP install hook (если он есть), так и YAML‑файлы этого типа материала в одном коммите.

Раннее обнаружение сломанных зависимостей: если ветка A добавляет новое поле, а ветка B изменяет отображение того же поля, то слияние B раньше A приведёт к ошибке импорта конфигурации. Это именно тот результат, который нужен — пусть пайплайн поймает это в ветке, а не на production.

6. Обязанности CI: что должен обеспечивать пайплайн

Каждый пайплайн, работающий с общим Drupal‑окружением, должен выполнять эти шаги по порядку:

  1. Установить или восстановить базу данных — чистая установка или обезличенный снапшот production.
  2. Выполнить обновления базы данныхdrush updb.
  3. Импортировать конфигурациюdrush cim --yes.
  4. Повторно экспортировать конфигурациюdrush cex --yes.
  5. Убедиться в отсутствии diff — если любой YAML‑файл изменился, сборка должна упасть.

Шаги 4 и 5 — самые важные. Импорт с последующим немедленным повторным экспортом не должен давать никаких отличий. Если отличия есть, верно одно из следующих:

  • Модуль генерирует конфигурацию во время импорта (часто это баг в модуле).
  • UUID конфигурационной сущности не совпадает с тем, что в базе данных.
  • Схема конфигурации неполна, из‑за чего Drupal нормализует значения иначе, чем при экспорте.
  • Разработчик вручную редактировал конфигурационные файлы и внёс несоответствия.
Shell ci/drupal-config-check.sh
#!/bin/bash
set -euo pipefail

echo "=== Running Drupal database updates ==="
drush updb --yes

echo "=== Importing configuration from config/sync ==="
drush cim --yes

echo "=== Re-exporting to verify no pending changes ==="
drush cex --yes

echo "=== Checking for config drift ==="
if ! git diff --exit-code config/sync; then
  echo ""
  echo "❌ FAIL: Configuration drift detected!"
  echo "   The config in Git does not match what Drupal produced after import+export."
  echo "   Diff shown above. Fix locally with 'drush cex' and commit."
  exit 1
fi

echo "✅ Configuration is clean — no drift detected."
Почему повторный экспорт после импорта обязателен: без шага повторного экспорта вы знаете только то, что импорт прошёл успешно. Вы не знаете, совпадает ли импортированная конфигурация с тем, что Drupal фактически сохранил внутри. Только повторный экспорт и сравнение diff закрывают этот разрыв.
 

7. Реализация в Jenkins (проверено в бою)

7.1 Структура Jenkinsfile

Мы используем исключительно declarative pipeline. Scripted pipeline предлагает большую гибкость, но declarative pipeline проще читать, проще проверять с помощью jenkins-cli declarative-linter и проще понимать новым членам команды.


pipeline {
  agent { label 'drupal-php82' }

  options {
    buildDiscarder(logRotator(numToKeepStr: '20'))
    timeout(time: 30, unit: 'MINUTES')
    disableConcurrentBuilds()
  }

  environment {
    DRUPAL_ROOT       = "${WORKSPACE}/web"
    COMPOSER_HOME     = "${WORKSPACE}/.composer"
    APP_ENV           = 'ci'
    DB_CREDS          = credentials('drupal-ci-db') // Jenkins secret
  }

  stages {

    stage('Checkout') {
      steps {
        checkout scm
        sh 'git log --oneline -5'
      }
    }

    stage('Composer Install') {
      steps {
        sh '''
          composer install \
            --no-interaction \
            --prefer-dist \
            --optimize-autoloader
        '''
      }
    }

    stage('Write Settings') {
      steps {
        // Генерация settings.env.php из Jenkins credentials/env переменных
        sh '''
          cat > web/sites/default/settings.env.php <<'EOF'
$databases['default']['default'] = [
  'driver'   => 'mysql',
  'host'     => '127.0.0.1',
  'database' => 'drupal_ci',
  'username' => '${DB_CREDS_USR}',
  'password' => '${DB_CREDS_PSW}',
  'port'     => 3306,
  'prefix'   => '',
];
\\$settings['hash_salt'] = '${DRUPAL_HASH_SALT}';
EOF
        '''
      }
    }

    stage('Site Install') {
      steps {
        sh '''
          drush site-install minimal \
            --yes \
            --existing-config \
            --account-name=admin \
            --account-pass="${DRUPAL_ADMIN_PASS}"
        '''
      }
    }

    stage('Validate Config') {
      steps {
        sh 'bash ci/drupal-config-check.sh'
      }
      post {
        failure {
          sh 'git diff config/sync || true'
          archiveArtifacts artifacts: 'config/sync/**/*.yml', allowEmptyArchive: true
        }
      }
    }

    stage('Deploy') {
      when {
        anyOf {
          branch 'main'
          branch 'release/*'
        }
      }
      steps {
        sh 'bash ci/drupal-deploy.sh'
      }
    }

  }

  post {
    always {
      sh 'drush cr || true'
      cleanWs()
    }
    failure {
      emailext(
        subject: "[FAIL] ${JOB_NAME} #${BUILD_NUMBER}",
        body: "Config validation failed. See: ${BUILD_URL}",
        recipientProviders: [[$class: 'DevelopersRecipientProvider']]
      )
    }
  }
}

7.2 Особенности Jenkins

Shared Libraries

При управлении более чем двумя‑тремя Drupal‑сайтами вынесите логику Drupal pipeline в Jenkins Shared Library. Тогда Jenkinsfile каждого проекта станет тонкой обёрткой:

Groovy Jenkinsfile (тонкая обёртка с использованием shared library)
@Library('drupal-pipeline-lib@v2') _

drupalPipeline(
  phpVersion:    '8.2',
  deployBranch:  'main',
  dbCredentials: 'drupal-ci-db',
  slackChannel:  '#deployments'
)

Очистка workspace

Всегда вызывайте cleanWs() в блоке post { always }. Jenkins‑агенты накапливают состояние от предыдущих сборок — оставшийся settings.env.php или директория vendor из предыдущего билда могут незаметно повлиять на текущую сборку. Действуйте жёстко: очищайте workspace.

Никогда не переиспользуйте базу данных между запусками пайплайна, если только вы специально не тестируете пути обновления. База данных, оставшаяся от предыдущего билда, может скрыть несоответствия UUID, отсутствующие миграции и проблемы с порядком установки модулей.

8. Реализация в GitLab CI

8.1 Структура .gitlab-ci.yml

YAML .gitlab-ci.yml
image: php:8.2-cli

stages:
  - build
  - test
  - validate-config
  - deploy

# ── Общие переменные ─────────────────────────────────────────────
variables:
  COMPOSER_CACHE_DIR: "$CI_PROJECT_DIR/.cache/composer"
  APP_ENV: "ci"
  MYSQL_DATABASE: "drupal_ci"
  MYSQL_ROOT_PASSWORD: "root"

# ── Повторно используемый кэш ────────────────────────────────────
.cache-composer: &cache-composer
  cache:
    key:
      files: [composer.lock]
    paths:
      - .cache/composer
      - vendor/
    policy: pull

# ── Стадия сборки ───────────────────────────────────────────────
composer:install:
  stage: build
  cache:
    key:
      files: [composer.lock]
    paths:
      - .cache/composer
      - vendor/
    policy: pull-push
  script:
    - composer install --no-interaction --prefer-dist --optimize-autoloader
  artifacts:
    paths:
      - vendor/
      - web/core/
      - web/modules/contrib/
    expire_in: 1 hour

# ── Стадия тестирования ─────────────────────────────────────────
phpunit:unit:
  stage: test
  <<: *cache-composer
  script:
    - ./vendor/bin/phpunit --testsuite=unit --log-junit=reports/phpunit.xml
  artifacts:
    reports:
      junit: reports/phpunit.xml

# ── Стадия валидации конфигурации ───────────────────────────────
drupal:validate-config:
  stage: validate-config
  <<: *cache-composer
  services:
    - name: mysql:8.0
      alias: mysql
  variables:
    DB_HOST: mysql
    DB_NAME: $MYSQL_DATABASE
    DB_USER: root
    DB_PASS: $MYSQL_ROOT_PASSWORD
  before_script:
    - bash ci/write-settings-env.sh
    - drush site-install minimal --yes --existing-config
  script:
    - bash ci/drupal-config-check.sh
  artifacts:
    when: on_failure
    paths:
      - config/sync/
    expire_in: 3 days

# ── Стадия деплоя ───────────────────────────────────────────────
deploy:staging:
  stage: deploy
  environment:
    name: staging
    url: https://staging.example.com
  rules:
    - if: '$CI_COMMIT_BRANCH == "develop"'
  script:
    - bash ci/drupal-deploy.sh staging

deploy:production:
  stage: deploy
  environment:
    name: production
    url: https://example.com
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: manual   # Продвижение в один клик с требованием ручного подтверждения
  script:
    - bash ci/drupal-deploy.sh production

8.2 Преимущества GitLab CI

GitLab CI обладает рядом функций, которые делают этот workflow особенно чистым:

  • Полноценные артефакты — неудачные экспорты конфигурации автоматически сохраняются и доступны в интерфейсе MR без дополнительной настройки.
  • Нативное кэширование по composer.lock — последующие установки Composer выполняются быстро.
  • Видимость пайплайна в Merge Request — разработчики видят статус валидации конфигурации прямо в MR до слияния.
  • Блок Services — MySQL запускается как sidecar без дополнительной инфраструктуры.
  • Environments + manual gates — правило when: manual при деплое в production даёт продвижение в один клик с человеческой проверкой без отдельного approval‑workflow.
Shell ci/drupal-deploy.sh
#!/bin/bash
set -euo pipefail

TARGET_ENV="${1:-staging}"
echo "=== Деплой в: ${TARGET_ENV} ==="

# Синхронизация собранного артефакта с целевым окружением (rsync, SSH, S3 — адаптируйте под ваш хостинг)
rsync -az --delete \
  --exclude='.git' \
  --exclude='web/sites/default/files' \
  --exclude='web/sites/default/settings.env.php' \
  ./ "deploy@${TARGET_ENV}.example.com:/var/www/drupal/"

echo "=== Выполнение post-deploy команд на ${TARGET_ENV} ==="
ssh "deploy@${TARGET_ENV}.example.com" bash -s <<'REMOTE'
  set -e
  cd /var/www/drupal
  drush updb --yes
  drush cim --yes
  drush cex --yes
  # Финальная sanity-проверка на самом целевом окружении
  git diff --exit-code config/sync || (echo "❌ Config drift on target!"; exit 1)
  drush cr
  echo "✅ Деплой завершён."
REMOTE

9. Конфигурация, специфичная для окружения — без читерства

Не каждое значение конфигурации должно быть одинаковым во всех окружениях. Поисковые бэкенды, уровень логирования, слои кэширования и сторонние API‑эндпоинты вполне обоснованно различаются. Вопрос в том, как учитывать эти различия, не нарушая целостность вашего конфигурационного пайплайна.

Правильные инструменты: config_split и config_ignore

config_split позволяет определять наборы конфигурации, активные только в определённых окружениях. Каждый split живёт в собственной директории и активируется через settings.php или settings.env.php на основе переменной окружения.

YAML config/sync/config_split.config_split.development.yml
langcode: en
status: true
id: development
label: Development
description: 'Конфигурация активна только в локальных/dev окружениях'
folder: '../config/splits/development'
module:
  devel: 0
  kint: 0
  dblog: 0
theme: {  }
blacklist: {  }
graylist: {  }
PHP web/sites/default/settings.env.php (активация окружения)
$app_env = getenv('APP_ENV') ?: 'production';

// Активируем нужный config_split в зависимости от окружения.
$config['config_split.config_split.development']['status']  = ($app_env === 'development');
$config['config_split.config_split.staging']['status']     = ($app_env === 'staging');
$config['config_split.config_split.production']['status']   = ($app_env === 'production');

Примеры split из реальных проектов

Элемент конфигурацииDev splitStage splitProd split
Бэкенд Search APIБэкенд базы данныхSolr (малый)Solr (prod‑кластер)
Логирование ошибокdblog, подробный режимsyslogsyslog + внешний APM
Модули производительностиОтключеныВключеныВключены + конфигурация CDN
Почтовый транспортMailhog / null mailerMailpitSMTP / SendGrid

Некорректные подходы — не делайте так:

  • Редактирование конфигурации напрямую в production‑базе данных.
  • Использование условий if ($settings['environment'] === 'prod') внутри settings.php для подмены значений конфигурации — это полностью обходят систему конфигурации и создаёт невидимые расхождения во время выполнения.
  • Хранение отдельных Git‑веток для каждого окружения с разными конфигурационными файлами.

10. Продвижение между окружениями

Продвижение — это не деплой. Деплой переносит код и конфигурацию в окружение. Продвижение переносит валидированный артефакт — то, что уже прошло CI — в следующее окружение по цепочке.

На практике это означает, что тот же Git SHA, который был проверен на dev, в итоге попадает в production. Никаких коммитов в последнюю минуту. Никаких хотфиксов, обходящих CI. Никаких cherry-pick, минующих проверку конфигурации.

✅ CI проверяет SHA
в feature ветке / main
🌐 Dev
автодеплой при merge
🌐 Stage
автодеплой из develop
🌐 Prod
manual gate на main

Один и тот же валидированный артефакт проходит через все окружения. CI запускается один раз; его результат распространяется дальше.

Философия неизменяемых сборок означает: то, что прошло проверку CI, и будет задеплоено. Если хотфикс действительно необходим, он проходит через отдельную fast‑track ветку со своим CI‑прогоном — он не должен обходить валидацию.

Предотвращение хотфиксов в последнюю минуту, обходящих проверку конфигурации: защитите ветку main обязательной проверкой статуса пайплайна. "Protected branches" в GitLab и плагин "GitHub Branch Source" в Jenkins поддерживают это. Если пайплайн не прошёл, деплой в production невозможен.

11. Обработка нестандартных случаев (с ними вы неизбежно столкнётесь)

Несоответствия UUID

Конфигурационные сущности содержат UUID. Если вы устанавливаете новый сайт, а затем пытаетесь импортировать конфигурацию из другой установки, Drupal откажется выполнять импорт из‑за ошибки несоответствия UUID. Решение — всегда выполнять установку с флагом --existing-config или установить UUID сайта после установки:

Shell
# Считываем UUID из закоммиченной конфигурации и применяем его к новой установке:
SITE_UUID=$(grep "^uuid:" config/sync/system.site.yml | awk '{print $2}')
drush config-set "system.site" uuid "$SITE_UUID" --yes
drush cim --yes

Порядок включения и отключения модулей

При включении нового модуля через конфигурацию Drupal автоматически соблюдает порядок зависимостей во время выполнения drush cim. Однако если install hook модуля генерирует конфигурацию по умолчанию, которая конфликтует с тем, что находится в config/sync, может потребоваться удалить эту конфигурацию по умолчанию и выполнить импорт повторно. Пайплайн обнаружит это как дрейф конфигурации.

Конфигурация, зависящая от контента

Некоторые элементы конфигурации ссылаются на контент — например, блок, указывающий на пункт меню по ID, или представление, фильтрующее данные по термину таксономии. Эти ссылки могут отличаться между окружениями, если контент был создан по‑разному. Решение — управлять таким контентом через миграции или модули default content, а не через конфигурацию.

Особенности мультсайтовой конфигурации

В мультисайтовой конфигурации каждый сайт имеет собственную директорию синхронизации конфигурации. CI‑пайплайн должен выполнять проверку конфигурации для каждого сайта, а не только для основного. Используйте цикл:

Shellci/multisite-config-check.sh
#!/bin/bash
set -euo pipefail

for SITE_DIR in web/sites/*/; do
  SITE=$(basename "$SITE_DIR")
  [[ "$SITE" == "default" ]] && continue
  [[ "$SITE" == "simpletest" ]] && continue

  echo "--- Checking config for site: $SITE ---"
  drush --uri="${SITE}" updb --yes
  drush --uri="${SITE}" cim --yes
  drush --uri="${SITE}" cex --yes

  CONFIG_DIR="config/${SITE}/sync"
  if ! git diff --exit-code "${CONFIG_DIR}"; then
    echo "❌ Дрейф конфигурации на сайте: $SITE"
    exit 1
  fi
done
echo "✅ Все сайты без расхождений."

Обновление ядра Drupal с изменениями конфигурации

Обновления ядра иногда включают изменения схемы, влияющие на существующую конфигурацию. Всегда выполняйте drush updb перед drush cim и запускайте drush cex после, чтобы зафиксировать любые изменения, нормализованные схемой. Коммитьте эти изменения в той же ветке, в которой происходит обновление ядра — не позволяйте пайплайну обнаруживать их неожиданно.

12. Операционные механизмы безопасности

Конфигурация «только для чтения» в production

Рассмотрите возможность включения модуля Config Readonly в production. Он предотвращает любые изменения конфигурации через административный UI — жёсткое ограничение, которое усиливает правило «никаких ручных изменений» на уровне приложения.

PHPweb/sites/default/settings.env.php
if (getenv('APP_ENV') === 'production') {
  $settings['config_readonly'] = TRUE;
}

Проверка после деплоя

ShellПроверки после деплоя
# Убедиться, что после деплоя не осталось ожидающих изменений конфигурации:
drush config-status 2>&1 | grep -v 'No differences' && { \
  echo "❌ Обнаружены неожиданные различия конфигурации после деплоя"; exit 1; \
} || echo "✅ Конфигурация без расхождений"

# Проверка прогрева кэша:
drush cr
drush php-eval "echo \Drupal::state()->get('system.cron_last');"

Стратегия отката

Откат — это операция Git, а не базы данных. Если деплой вызвал проблему:

  1. Определите последний стабильный commit SHA.
  2. Запустите пайплайн для этого SHA.
  3. Пайплайн повторно задеплоит ранее валидированный артефакт.

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

Аудит: кто изменил конфигурацию, когда и зачем

Поскольку каждое изменение конфигурации проходит через Git, вашим журналом аудита является история Git. Используйте осмысленные сообщения коммитов и обеспечьте их соблюдение через commit‑msg hook или шаг lint в CI:

ShellПример истории коммитов
$ git log --oneline config/sync/views.view.articles.yml

a3f8c12 feat(views): add taxonomy filter to articles view (PROJ-421)
9e1b307 fix(views): remove exposed sort causing query timeout (PROJ-389)
4d22a91 chore(config): export after Search API Solr 4.3.0 upgrade

13. Распространённые ошибки, которые мы больше не совершаем

ОшибкаПочему это плохоРешение
Ручной запуск drush cim на staging или prodИмпортирует неизвестное состояние — потенциально включая незафиксированные локальные измененияТолько пайплайн импортирует конфигурацию в общих окружениях
Разрешение изменений через UI на stagingStaging становится источником истины, создавая расхождения с GitВключить config_readonly во всех общих окружениях
Надежда на то, что разработчики «не забудут экспортировать»Под давлением они обязательно забудутPre-commit hooks предупреждают; сбой CI принуждает
Игнорирование дрейфа конфигурацииНезаметные расхождения накапливаются до критической поломкиТройка drush cim → cex → git diff --exit-code обязательна
Отдельные ветки для каждого окружения с разной конфигурациейСлияние превращается в кошмар конфликтов; отсутствует единый источник истиныОдна ветка конфигурации, разделяемая через config_split
Пропуск drush updb перед drush cimОбновления схемы могут вызвать ошибки импорта или тихую потерю данныхВсегда: updb → cim → cex → diff

14. Результаты после внедрения этого подхода

После внедрения этого подхода на портфеле Drupal‑сайтов — от одного небольшого сайта до агентства, управляющего десятками установок — результаты оказались стабильными:

~80%
Снижение числа инцидентов при деплое, связанных с конфигурацией
< 1 дня
Онбординг нового разработчика в конфигурационный workflow
0
Ручных запусков drush cim на production за 18 месяцев
100%
Изменений конфигурации, отслеживаемых до Git-коммита и MR

Помимо цифр, наиболее важным стало культурное изменение. Обсуждения конфигурации переместились из Slack в историю Git. Вместо «кто‑нибудь менял стили изображений на staging?» вопрос стал звучать так: «Есть ли коммит для этого?» — и он всегда есть, либо изменения не происходило.

Новым разработчикам больше не нужно разбираться в негласных правилах вроде «какое окружение сейчас каноническое». Ответ всегда один — Git. И пайплайн это обеспечивает.

15. Финальные рекомендации

  • Начинайте строго и ослабляйте только при необходимости. Ослабить ненужное правило проще, чем ужесточить то, которого изначально не было.
  • Рассматривайте любой diff конфигурации как сбой сборки. Без исключений и без «исправим после релиза».
  • CI — это источник истины для деплоя, а не ноутбук разработчика, staging‑окружение или решение в Slack.
  • Последовательность updb → cim → cex → diff неприкосновенна. Если пропустить хотя бы один шаг — вы действуете вслепую.
  • Используйте config_split для реальных различий между окружениями. Не применяйте условия в settings.php как обходной путь.
  • Включите config_readonly во всех общих окружениях. Сделайте случайные изменения через UI невозможными, а не просто нежелательными.
  • Сохраняйте неудачные экспорты конфигурации как CI‑артефакты. Разработчики должны видеть, что именно вызвало дрейф, а не просто факт его наличия.
  • Откат выполняется через Git, а не через «операции» с базой данных. Пайплайн — самый безопасный путь в обе стороны.
Этот подход масштабируется. Независимо от того, являетесь ли вы единственным разработчиком или агентством, управляющим пятьюдесятью Drupal‑установками, правила остаются прежними. Пайплайну всё равно на размер команды или давление сроков. Он либо проходит, либо падает. В этом и заключается смысл.

И Jenkins, и GitLab CI одинаково хорошо справляются с обеспечением этой дисциплины. GitLab CI требует меньше инфраструктуры и обладает лучшей нативной поддержкой артефактов; Jenkins предлагает большую гибкость для сложных корпоративных сред и отличную поддержку shared libraries для стандартизации пайплайнов между проектами. Выбирайте инструмент, подходящий вашей организации — принципы этой статьи применимы в любом случае.

Целью никогда не было усложнение управления конфигурацией. Цель — сделать его рутинным: предсказуемым шагом, который всегда работает, о котором никто не задумывается и который никогда не приводит к инцидентам в 23:00. При правильно реализованном CI‑пайплайне именно это вы и получаете.

Технические и архитектурные вопросы
Ivan Abramenko, Principal Drupal Architect
ivan.abramenko@drupalbook.org
Проектные вопросы
projects@drupalbook.org