Управление конфигурацией Drupal на основе CI с использованием Jenkins и GitLab CI
1. Почему управление конфигурацией на основе CI имеет значение
Система конфигурации Drupal — одна из самых сильных сторон платформы и одновременно один из самых надёжных источников боли. Возможность экспортировать и импортировать каждый элемент конфигурации сайта в виде YAML‑файлов невероятно мощна — но только если все согласны с тем, кто именно отвечает за перемещение этих файлов между окружениями. В большинстве команд такого согласия на самом деле не существует.
Классические проблемы хорошо знакомы каждому, кто запускал Drupal‑сайт:
- Дрейф конфигурации — staging расходится с production, production — с локальным окружением, и никто не уверен, какое из них является эталонным.
- «Работает на staging, но не на prod» — потому что кто‑то обновил представление или форматтер поля на staging и так и не экспортировал изменения.
- Ручной
drush cim, ломающий контент — поспешный импорт в 23:00, который удалил поле типа материала, всё ещё используемое живыми нодами.
Первопричина у всех этих сценариев одна и та же: человек решает, когда и нужно ли продвигать конфигурацию. Люди забывают. Пропускают шаги под давлением. Принимают решения, которые оказываются неверными.
CI не забывает. Пайплайн либо проходит, либо падает. У него нет митинга, на который нужно бежать. Он не знает, что релиз через двадцать минут. Именно такая детерминированность и нужна управлению конфигурацией.
Обещания, которые выполняет эта статья:
- Каждое изменение конфигурации коммитится в Git до того, как попадает в любое общее окружение.
- За валидацию и импорт конфигурации отвечает пайплайн, а не разработчик.
- Продвижение между окружениями не требует ни одного ручного шага.
- Дрейф конфигурации — это ошибка сборки, а не сообщение в Slack.
2. Базовые принципы, которые мы вынесли из реальных проектов
Конфигурация — это код
Если что‑то меняет поведение сайта, ему место в Git. Точка. Представление, тип материала, настройка производительности, стиль изображения — всё это код. Обращайтесь с конфигурационными файлами так же дисциплинированно, как с PHP‑файлами: проводите ревью, версионируйте, никогда не редактируйте их напрямую в общем окружении.
Никакого ручного drush cim в общих окружениях
Конфигурацию импортирует пайплайн. Не разработчики. Это правило звучит радикально, пока вы впервые не столкнётесь с тем, как кто‑то запускает drush cim на production, имея в рабочей директории незафиксированные локальные изменения.
Пайплайны должны падать при любом дрейфе конфигурации
Импорт конфигурации с последующим немедленным экспортом не должен давать никакой разницы. Если разница есть — сборка падает. Это единственное правило ловит больше багов, чем все остальные проверки, которые мы добавляли.
core.extension.yml был исключён из экспортов, потому что «модулями всё равно управляет Composer». Три месяца спустя хотфикс вновь включил модуль, который намеренно был отключён на production. Хотфикс был корректным — но последующий импорт конфигурации молча снова отключил модуль. Именно после этого случая мы стали называть core.extension.yml самым опасным файлом, который только можно игнорировать.3. Архитектура высокого уровня
drush cex
config/sync
валидация + импорт
Поток конфигурации движется только в одном направлении: от локального экспорта разработчика через 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 как костыль там, где должна быть конфигурация.
// 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';
}
// Генерируется 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. - Промежуточного состояния не существует.
# После внесения изменений конфигурации через 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:
#!/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 05.2 Feature-ветки
Конфигурация живёт рядом с кодом, который от неё зависит — в одной и той же ветке и в том же pull/merge request. Фича, добавляющая новый тип материала, должна включать как PHP install hook (если он есть), так и YAML‑файлы этого типа материала в одном коммите.
6. Обязанности CI: что должен обеспечивать пайплайн
Каждый пайплайн, работающий с общим Drupal‑окружением, должен выполнять эти шаги по порядку:
- Установить или восстановить базу данных — чистая установка или обезличенный снапшот production.
- Выполнить обновления базы данных —
drush updb. - Импортировать конфигурацию —
drush cim --yes. - Повторно экспортировать конфигурацию —
drush cex --yes. - Убедиться в отсутствии diff — если любой YAML‑файл изменился, сборка должна упасть.
Шаги 4 и 5 — самые важные. Импорт с последующим немедленным повторным экспортом не должен давать никаких отличий. Если отличия есть, верно одно из следующих:
- Модуль генерирует конфигурацию во время импорта (часто это баг в модуле).
- UUID конфигурационной сущности не совпадает с тем, что в базе данных.
- Схема конфигурации неполна, из‑за чего Drupal нормализует значения иначе, чем при экспорте.
- Разработчик вручную редактировал конфигурационные файлы и внёс несоответствия.
#!/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."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 каждого проекта станет тонкой обёрткой:
@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.
8. Реализация в GitLab CI
8.1 Структура .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 production8.2 Преимущества GitLab CI
GitLab CI обладает рядом функций, которые делают этот workflow особенно чистым:
- Полноценные артефакты — неудачные экспорты конфигурации автоматически сохраняются и доступны в интерфейсе MR без дополнительной настройки.
- Нативное кэширование по
composer.lock— последующие установки Composer выполняются быстро. - Видимость пайплайна в Merge Request — разработчики видят статус валидации конфигурации прямо в MR до слияния.
- Блок Services — MySQL запускается как sidecar без дополнительной инфраструктуры.
- Environments + manual gates — правило
when: manualпри деплое в production даёт продвижение в один клик с человеческой проверкой без отдельного approval‑workflow.
#!/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 "✅ Деплой завершён."
REMOTE9. Конфигурация, специфичная для окружения — без читерства
Не каждое значение конфигурации должно быть одинаковым во всех окружениях. Поисковые бэкенды, уровень логирования, слои кэширования и сторонние API‑эндпоинты вполне обоснованно различаются. Вопрос в том, как учитывать эти различия, не нарушая целостность вашего конфигурационного пайплайна.
Правильные инструменты: config_split и config_ignore
config_split позволяет определять наборы конфигурации, активные только в определённых окружениях. Каждый split живёт в собственной директории и активируется через settings.php или settings.env.php на основе переменной окружения.
langcode: en
status: true
id: development
label: Development
description: 'Конфигурация активна только в локальных/dev окружениях'
folder: '../config/splits/development'
module:
devel: 0
kint: 0
dblog: 0
theme: { }
blacklist: { }
graylist: { }$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 split | Stage split | Prod split |
|---|---|---|---|
| Бэкенд Search API | Бэкенд базы данных | Solr (малый) | Solr (prod‑кластер) |
| Логирование ошибок | dblog, подробный режим | syslog | syslog + внешний APM |
| Модули производительности | Отключены | Включены | Включены + конфигурация CDN |
| Почтовый транспорт | Mailhog / null mailer | Mailpit | SMTP / SendGrid |
Некорректные подходы — не делайте так:
- Редактирование конфигурации напрямую в production‑базе данных.
- Использование условий
if ($settings['environment'] === 'prod')внутриsettings.phpдля подмены значений конфигурации — это полностью обходят систему конфигурации и создаёт невидимые расхождения во время выполнения. - Хранение отдельных Git‑веток для каждого окружения с разными конфигурационными файлами.
10. Продвижение между окружениями
Продвижение — это не деплой. Деплой переносит код и конфигурацию в окружение. Продвижение переносит валидированный артефакт — то, что уже прошло CI — в следующее окружение по цепочке.
На практике это означает, что тот же Git SHA, который был проверен на dev, в итоге попадает в production. Никаких коммитов в последнюю минуту. Никаких хотфиксов, обходящих CI. Никаких cherry-pick, минующих проверку конфигурации.
в feature ветке / main
автодеплой при merge
автодеплой из develop
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 сайта после установки:
# Считываем 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‑пайплайн должен выполнять проверку конфигурации для каждого сайта, а не только для основного. Используйте цикл:
#!/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 — жёсткое ограничение, которое усиливает правило «никаких ручных изменений» на уровне приложения.
if (getenv('APP_ENV') === 'production') {
$settings['config_readonly'] = TRUE;
}Проверка после деплоя
# Убедиться, что после деплоя не осталось ожидающих изменений конфигурации:
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, а не базы данных. Если деплой вызвал проблему:
- Определите последний стабильный commit SHA.
- Запустите пайплайн для этого SHA.
- Пайплайн повторно задеплоит ранее валидированный артефакт.
Ручное редактирование базы данных для отмены импорта конфигурации — никогда не является решением. Это обход всех механизмов безопасности и обычно создаёт больше дрейфа, чем исправляет.
Аудит: кто изменил конфигурацию, когда и зачем
Поскольку каждое изменение конфигурации проходит через Git, вашим журналом аудита является история Git. Используйте осмысленные сообщения коммитов и обеспечьте их соблюдение через commit‑msg hook или шаг lint в CI:
$ 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 upgrade13. Распространённые ошибки, которые мы больше не совершаем
| Ошибка | Почему это плохо | Решение |
|---|---|---|
Ручной запуск drush cim на staging или prod | Импортирует неизвестное состояние — потенциально включая незафиксированные локальные изменения | Только пайплайн импортирует конфигурацию в общих окружениях |
| Разрешение изменений через UI на staging | Staging становится источником истины, создавая расхождения с 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‑сайтов — от одного небольшого сайта до агентства, управляющего десятками установок — результаты оказались стабильными:
drush cim на production за 18 месяцевПомимо цифр, наиболее важным стало культурное изменение. Обсуждения конфигурации переместились из Slack в историю Git. Вместо «кто‑нибудь менял стили изображений на staging?» вопрос стал звучать так: «Есть ли коммит для этого?» — и он всегда есть, либо изменения не происходило.
Новым разработчикам больше не нужно разбираться в негласных правилах вроде «какое окружение сейчас каноническое». Ответ всегда один — Git. И пайплайн это обеспечивает.
15. Финальные рекомендации
- Начинайте строго и ослабляйте только при необходимости. Ослабить ненужное правило проще, чем ужесточить то, которого изначально не было.
- Рассматривайте любой diff конфигурации как сбой сборки. Без исключений и без «исправим после релиза».
- CI — это источник истины для деплоя, а не ноутбук разработчика, staging‑окружение или решение в Slack.
- Последовательность
updb → cim → cex → diffнеприкосновенна. Если пропустить хотя бы один шаг — вы действуете вслепую. - Используйте
config_splitдля реальных различий между окружениями. Не применяйте условия вsettings.phpкак обходной путь. - Включите config_readonly во всех общих окружениях. Сделайте случайные изменения через UI невозможными, а не просто нежелательными.
- Сохраняйте неудачные экспорты конфигурации как CI‑артефакты. Разработчики должны видеть, что именно вызвало дрейф, а не просто факт его наличия.
- Откат выполняется через Git, а не через «операции» с базой данных. Пайплайн — самый безопасный путь в обе стороны.
И Jenkins, и GitLab CI одинаково хорошо справляются с обеспечением этой дисциплины. GitLab CI требует меньше инфраструктуры и обладает лучшей нативной поддержкой артефактов; Jenkins предлагает большую гибкость для сложных корпоративных сред и отличную поддержку shared libraries для стандартизации пайплайнов между проектами. Выбирайте инструмент, подходящий вашей организации — принципы этой статьи применимы в любом случае.
Целью никогда не было усложнение управления конфигурацией. Цель — сделать его рутинным: предсказуемым шагом, который всегда работает, о котором никто не задумывается и который никогда не приводит к инцидентам в 23:00. При правильно реализованном CI‑пайплайне именно это вы и получаете.
Ivan Abramenko, Principal Drupal Architect
ivan.abramenko@drupalbook.org
projects@drupalbook.org