logo

额外区块类型 (EBT) - 全新的布局构建器体验❗

额外区块类型 (EBT) - 样式化、可定制的区块类型:幻灯片、标签页、卡片、手风琴等更多类型。内置背景、DOM Box、JavaScript 插件的设置。立即体验布局构建的未来。

演示 EBT 模块 下载 EBT 模块

❗额外段落类型 (EPT) - 全新的 Paragraphs 体验

额外段落类型 (EPT) - 类似的基于 Paragraph 的模块集合。

演示 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

滚动

基于 Jenkins 和 GitLab CI 的 Drupal CI 驱动配置管理

16/04/2026, by Ivan

1. 为什么由 CI 驱动的配置管理至关重要

Drupal 的配置系统是该平台最大的优势之一 —— 同时也是最可靠的痛点来源之一。能够将站点中的每一项配置以 YAML 文件的形式进行导出和导入,这一能力非常强大,但前提是团队成员对谁负责在各个环境之间移动这些文件达成一致。在大多数团队中,这种共识从未真正存在。

任何发布过 Drupal 站点的人都熟悉以下经典问题:

  • 配置漂移(Config drift) —— 预发布环境与生产环境产生偏差,生产环境又与本地环境不一致,且没有人确定哪个环境才是标准版本。
  • “在预发布环境能运行,但在生产环境不行” —— 因为有人在预发布环境更新了一个视图或字段格式化器,却从未进行导出。
  • 手动 drush cim 导致内容损坏 —— 晚上 11 点匆忙进行了一次导入,删除了仍被线上节点引用的内容类型字段。

所有这些场景的根本原因是相同的:由人为决定何时以及是否推进配置。人会遗忘,会在压力下跳过步骤,也可能做出错误的判断。

CI 不会遗忘。一个流水线要么通过,要么失败。它不需要赶着去参加站会,也不知道发布还剩二十分钟。这种确定性正是配置管理所需要的。

本文将实现如下承诺:

  • 每一个配置更改在触及任何共享环境之前,都会先提交到 Git。
  • 由流水线而非开发者负责配置的验证与导入。
  • 环境之间的推进无需任何手动步骤。
  • 配置漂移将成为构建失败,而不是 Slack 消息提醒。
前提假设:你正在运行 Drupal 10 或 11,使用基于 Git 的工作流和功能分支,并部署到至少两个共享环境(例如预发布环境和生产环境)。你的团队使用 Jenkins、GitLab CI,或两者兼用。

2. 我们从真实项目中总结出的核心原则

配置即代码(Configuration is code)

只要它会改变站点行为,就必须纳入 Git。就是这么简单。视图、内容类型、性能设置、图片样式 —— 全部都是代码。像对待 PHP 文件一样对待配置文件:进行评审、版本控制,绝不要在共享环境中直接编辑。

在共享环境中禁止手动执行 drush cim

配置应由流水线导入,而不是开发者来做。这个规则听起来可能有点极端,直到你第一次经历有人在生产环境执行 drush cim 时,其工作目录中仍然存在未提交的本地更改。

当出现配置漂移时,流水线必须快速失败

在导入配置后立即再次导出,不应该产生任何 diff。如果产生差异,则构建失败。这一条规则比我们添加过的任何其他检查都能捕获更多的错误。

一位高级开发者在生产环境中调试一个缓慢的搜索页面。他通过 UI 调整了 Search API 索引设置,确认性能得到提升后关闭了工单。两周后,一次发布从 Git 导入配置 —— 覆盖了他的 UI 更改 —— 搜索性能随即崩溃。整整三天都没有人将问题关联起来。在那次事件之后,我们明确了一条规则:任何未立即导出并提交的 UI 更改都被视为已丢失。
在一个以媒体为主的网站中,core.extension.yml 被排除在导出之外,因为“模块反正是由 Composer 管理的”。三个月后,一个热修复重新启用了一个在生产环境中被刻意禁用的模块。该热修复本身是正确的 —— 但随后的配置导入又悄无声息地再次禁用了该模块。正因如此,我们现在称 core.extension.yml 为你最不应该忽视的危险文件。

3. 高层架构

👨‍💻 开发者
drush cex
📁 Git 仓库
config/sync
⚙ CI 流水线
验证 + 导入
🌐 开发环境
🌐 预发布环境
🌐 生产环境

配置仅以单向流动:从开发者的本地导出,经由 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 通过环境变量加载
    └── ...

config/sync 中存放的内容

所有由 drush cex 生成的配置 YAML 文件。该目录是站点配置的唯一事实来源。它会像其他代码一样被提交、审核和部署。

绝不能进入配置的内容

  • 环境特定的主机名、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 —— 而不是等到一天结束时。
  • 使用 git diff config/sync 审查差异。
  • 提交更改,或使用 git checkout -- config/sync 丢弃更改。
  • 不存在中间状态。
Shell 本地开发工作流程
# 在通过 Drupal UI 或 install hook 进行配置更改之后:
drush cex --yes

# 审查更改内容 —— 像审查任何代码变更一样对待:
git diff config/sync

# 与功能代码一起暂存并提交:
git add config/sync
git commit -m "feat(search): 添加全文搜索 API 索引配置"

# 或如果该更改只是探索性尝试,尚未准备好:
git checkout -- config/sync

我们通过一个轻量级的 Git pre-commit hook 来执行此规则(仅警告而不阻止):当 PHP 或模板文件被暂存,但 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 "⚠  警告:已暂存 PHP/模块文件,但未检测到 config/sync 更改。"
  echo "   你是否忘记运行:drush cex?"
  echo "   仍将继续 —— 但请再次确认。"
fi
exit 0

5.2 功能分支

配置必须与其所依赖的代码一同存在于同一分支及同一个 Pull/Merge Request 中。一个新增内容类型的功能,应在同一次提交中同时包含 PHP install hook(如有)以及该内容类型对应的 YAML 文件。

尽早发现依赖破损:如果分支 A 新增了一个字段,而分支 B 修改了该字段的显示设置,在 A 合并之前合并 B 将会导致配置导入错误。这正是期望的结果 —— 你希望流水线在分支阶段捕获这个问题,而不是在生产环境中。

6. CI 职责:流水线必须执行的内容

每一个涉及共享 Drupal 环境的流水线都必须按顺序执行以下步骤:

  1. 安装或恢复数据库 —— 执行全新安装或使用已脱敏的生产快照。
  2. 运行数据库更新 —— drush updb
  3. 导入配置 —— drush cim --yes
  4. 重新导出配置 —— drush cex --yes
  5. 确认无差异 —— 如果任何 YAML 文件发生变化,则构建失败。

第 4 步和第 5 步是最关键的。在导入后立即重新导出,必须产生空的 diff。如果没有,说明以下情况之一成立:

  • 某个模块在导入时生成了配置(通常是模块中的缺陷)。
  • 配置实体的 UUID 与数据库中的不匹配。
  • 配置 Schema 不完整,导致 Drupal 规范化数值的方式与导出时不同。
  • 开发者手动编辑了配置文件,从而引入不一致。
Shell ci/drupal-config-check.sh
#!/bin/bash
set -euo pipefail

echo "=== 正在运行 Drupal 数据库更新 ==="
drush updb --yes

echo "=== 正在从 config/sync 导入配置 ==="
drush cim --yes

echo "=== 重新导出以验证是否存在待处理更改 ==="
drush cex --yes

echo "=== 正在检查配置漂移 ==="
if ! git diff --exit-code config/sync; then
  echo ""
  echo "❌ 失败:检测到配置漂移!"
  echo "   Git 中的配置与 Drupal 在导入+导出后生成的不一致。"
  echo "   差异已显示于上方。请在本地使用 'drush cex' 修复并提交。"
  exit 1
fi

echo "✅ 配置干净 —— 未检测到漂移。"
为何导入后必须重新导出:如果没有重新导出步骤,你只能确认导入成功,却无法确认导入的配置是否与 Drupal 实际内部存储的内容一致。只有重新导出并进行 diff,才能弥补这一差距。
 

7. Jenkins 实现(经过实战验证)

7.1 Jenkinsfile 结构

我们专门使用声明式流水线(Declarative Pipelines)。脚本式流水线提供了更高的灵活性,但声明式流水线更易阅读,更易通过 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 密钥
  }

  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 {
        // 根据 Jenkins 凭据/环境变量生成 settings.env.php
        sh '''
          cat > web/sites/default/settings.env.php <<EOF
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: "配置校验失败。详情请查看: ${BUILD_URL}",
        recipientProviders: [[$class: 'DevelopersRecipientProvider']]
      )
    }
  }
}

7.2 Jenkins 特殊注意事项

共享库(Shared Libraries)

当你需要管理超过两三个 Drupal 站点时,应将 Drupal 流水线逻辑提取至 Jenkins 共享库。此时,每个项目的 Jenkinsfile 仅需作为一个轻量封装:

Groovy Jenkinsfile(使用共享库的轻量包装)
@Library('drupal-pipeline-lib@v2') _

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

工作空间清理(Workspace Cleanup)

始终在 post { always } 代码块中调用 cleanWs()。Jenkins Agent 会累积先前构建的状态 —— 上一次构建遗留的 settings.env.php 或 vendor 目录可能会悄然影响当前构建。务必果断处理:清理工作空间。

除非你是在专门测试升级路径,否则绝不要在流水线运行之间复用数据库。先前构建残留的数据库可能会掩盖 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"

composer:install:
  stage: build
  script:
    - composer install --no-interaction --prefer-dist --optimize-autoloader

drupal:validate-config:
  stage: validate-config
  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

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 拥有若干特性,使其特别适合此类工作流程:

  • 一流的构建产物管理(Artifacts) —— 失败的配置导出会自动存储,并可在 MR 界面中浏览,无需任何额外配置。
  • 基于 composer.lock 的原生缓存机制 —— 首次运行后,Composer 安装成本大幅降低。
  • Merge Request 流水线可视性 —— 开发者可在合并前直接查看 MR 中的配置校验状态。
  • Services 模块 —— MySQL 可作为 Sidecar 服务运行,无需额外基础设施开销。
  • 环境 + 手动关卡 —— 在生产部署阶段使用 when: manual 规则,可实现一键推进并保留人工审核环节,无需额外审批流程。
Shell ci/drupal-deploy.sh
#!/bin/bash
set -euo pipefail

TARGET_ENV="${1:-staging}"
echo "=== 正在部署至: ${TARGET_ENV} ==="

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 "=== 正在 ${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
  git diff --exit-code config/sync || (echo "❌ 目标环境存在配置漂移!"; exit 1)
  drush cr
  echo "✅ 部署完成。"
REMOTE

9. 在不“作弊”的情况下实现环境特定配置

并非所有配置值都应在所有环境中保持一致。搜索后端、日志详细程度、缓存层以及第三方 API 端点等配置在不同环境中合理地存在差异。问题在于应当如何处理这些差异,而不损害配置流水线的完整性。

正确的工具:config_splitconfig_ignore

config_split 允许你定义仅在特定环境中激活的一组配置。每个 split 存放在独立目录中,并通过环境变量在 settings.phpsettings.env.php 中启用。

YAML config/sync/config_split.config_split.development.yml
langcode: en
status: true
id: development
label: Development
description: '仅在本地/开发环境中启用的配置'
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 示例

配置项开发环境 split预发布环境 split生产环境 split
Search API 后端数据库后端Solr(小规模)Solr(生产集群)
错误日志记录dblog,详细模式syslogsyslog + 外部 APM
性能模块禁用启用启用 + CDN 配置
邮件传输方式Mailhog / 空邮件发送器MailpitSMTP / SendGrid

不正确的方法——切勿这样做:

  • 直接在生产数据库中编辑配置。
  • settings.php 中使用 if ($settings['environment'] === 'prod') 条件语句来替换配置值 —— 这会完全绕过配置系统并导致不可见的运行时分歧。
  • 为每个环境维护不同配置文件的独立 Git 分支。

10. 环境之间的推进(Promotion)

推进(Promotion)并非部署(Deployment)。部署是将代码和配置移动到某个环境中,而推进则是将一个经过验证的构建产物(即已通过 CI 的内容)移动到链路中的下一个环境。

在实践中,这意味着在开发环境中验证过的同一个 Git SHA 将被部署到生产环境。不允许临时提交,不允许绕过 CI 的热修复,也不允许跳过配置检查的 cherry-pick 操作。

✅ CI 验证 SHA
在功能分支 / main 上
🌐 开发环境
合并后自动部署
🌐 预发布环境
从 develop 自动部署
🌐 生产环境
在 main 上设置人工关卡

相同的已验证构建产物在各个环境之间传递。CI 只运行一次;结果逐级传播。

不可变构建(Immutable Builds)理念意味着:CI 验证的内容就是最终被部署的内容。如果确实需要紧急修复,应通过快速通道分支执行其专属 CI 流程 —— 而不是绕过验证。

防止临时热修复绕过配置检查:请为你的 main 分支设置必要的流水线状态检查保护。GitLab 的“受保护分支”以及 Jenkins 的“GitHub Branch Source”插件均支持此功能。若流水线未通过,则该分支不可部署到生产环境。

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 的区块,或根据某个分类术语进行过滤的视图。在不同环境中若内容创建方式不同,这些引用可能产生差异。正确的处理方式是通过迁移(Migrations)或默认内容模块管理这些内容,而不是通过配置同步解决。

多站点(Multisite)注意事项

在多站点架构下,每个站点都拥有独立的配置同步目录。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 "--- 正在检查站点配置: $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 Core 并伴随配置变更

Core 更新有时会附带影响现有配置的 Schema 变更。务必在执行 drush cim 前先运行 drush updb,然后在导入配置后再运行 drush cex,以捕获经 Schema 规范化后的配置变更。这些变更应作为 Core 升级分支的一部分提交,而不要等到流水线中才被发现。

12. 运维安全防护措施

生产环境启用只读配置

建议在生产环境中启用 Config Readonly 模块。它可阻止通过管理界面进行任何配置更改 —— 在应用层面强制执行“禁止手动修改配置”的规则。

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. 定位上一个可用的提交 SHA。
  2. 针对该 SHA 重新触发流水线。
  3. 流水线将重新部署此前已验证的构建产物。

通过手动修改数据库来撤销配置导入绝不可取 —— 这会绕过所有安全检查,并通常带来更多配置漂移问题。

审计:谁在何时、为何修改了配置

由于所有配置更改均通过 Git 提交,你的审计记录即为 Git 历史。请使用清晰、有意义的提交信息,并通过 commit-msg hook 或 CI lint 流程加以约束:

Shell提交记录示例
$ git log --oneline config/sync/views.view.articles.yml

a3f8c12 feat(views): 为文章视图添加分类过滤器 (PROJ-421)
9e1b307 fix(views): 移除导致查询超时的 exposed 排序 (PROJ-389)
4d22a91 chore(config): 升级 Search API Solr 4.3.0 后导出配置

13. 我们曾经犯过的常见错误

错误影响解决方案
在 staging 或生产环境手动运行 drush cim导入未知状态(可能包含未提交的本地更改)共享环境中的配置导入仅允许由流水线执行
允许在 staging 通过 UI 修改配置使 staging 成为事实来源,导致与 Git 偏离在所有共享环境中启用 config_readonly
依赖开发者“记得导出配置”在发布压力下极易被忽略使用 pre-commit 提醒 + CI 强制校验
配置漂移时未使构建失败隐性偏差不断累积直至系统性故障强制执行:drush cim → cex → git diff --exit-code
为每个环境维护不同配置分支合并冲突频发,缺乏单一事实来源统一配置分支,通过 config_split 进行环境区分
在运行 drush cim 前跳过 drush updbSchema 更新可能导致导入失败或数据丢失务必遵循:updb → cim → cex → diff

14. 采用该模式后的成效

在将这一方法推广至多个 Drupal 站点组合之后 —— 从单个小型站点到管理数十个安装实例的代理机构 —— 所取得的成果始终一致:

~80%
与配置相关的部署事故减少幅度
< 1 天
新开发人员掌握配置工作流程所需时间
0
18 个月内生产环境手动运行 drush cim 的次数
100%
所有配置变更均可追溯至 Git 提交及 MR

比数据本身更重要的是团队文化的转变。配置相关讨论从 Slack 迁移到了 Git 历史记录之中。不再是“是否有人在 staging 修改了图片样式?”,而是“是否存在对应的提交?” —— 如果没有提交,那么该更改就从未发生。

新加入的开发人员也无需再去了解那些“当前哪个环境才是权威来源”的隐性规则。答案始终是 Git,而流水线会对此进行强制保障。

15. 最终建议

  • 从严格策略开始,仅在合理情况下适度放宽。放宽一项被证明不必要的规则,远比事后收紧一项从未存在的规则要容易。
  • 将每一次配置差异视为构建失败处理。不设例外,也不要抱有“发布后再修复”的侥幸心理。
  • CI 才是部署内容的唯一事实来源 —— 而不是开发者本地环境、staging 环境或 Slack 中的临时决策。
  • updb → cim → cex → diff 执行顺序不可破坏。跳过任意步骤都意味着在缺乏可见性的情况下部署风险变更。
  • 针对真实的环境差异使用 config_split切勿以 settings.php 条件逻辑作为替代方案。
  • 在所有共享环境中启用 config_readonly。使误操作导致的 UI 配置更改变得不可能发生,而不仅仅是被警示。
  • 将失败的配置导出归档为 CI 构建产物。开发人员需要明确看到配置漂移的具体内容,而不仅仅是知道存在差异。
  • 通过 Git 执行回滚,绝不可通过数据库手动操作。流水线在正向部署与回滚场景下都是最安全的路径。
该方法具备良好的扩展能力。无论是单人维护一个站点,还是管理五十个 Drupal 安装实例的代理机构,适用规则完全一致。流水线不会因团队规模或发布时间压力而改变行为。它要么通过,要么失败,而这种一致性正是核心价值所在。

Jenkins 与 GitLab CI 均可有效执行此类流程约束。GitLab CI 所需基础设施更少,并具备更完善的原生构建产物支持;Jenkins 则在复杂企业环境中提供更高灵活性,并可通过共享库实现多项目流水线的标准化。请选择最适合贵组织的工具 —— 本文提出的原则在任何平台上均适用。

这一方案的目标从来不是让配置管理变得更加复杂,而是让其变得枯燥且可预测:始终按既定流程执行,无需人为干预,不再在深夜引发线上事故。通过合理实现的 CI 流水线,这种“可预期性”正是最终成果。

技术与架构咨询
Ivan Abramenko,Principal Drupal Architect
ivan.abramenko@drupalbook.org