logo

パレット - カラフルに🎨

Palette — ビジュアルページビルダー、デザインの専門知識は不要です。

ライブデモ パレットをダウンロード

Scroll

Jenkins と GitLab CI を用いた CI 主導の Drupal 設定管理

16/04/2026, by Ivan

1. なぜ CI 主導の設定管理が重要なのか

Drupal の設定システムは、このプラットフォームの最大の強みの1つであり、同時に最も確実な痛みの源の1つです。サイト設定のあらゆる要素を YAML ファイルとしてエクスポート・インポートできる能力は強力です。しかしそれは、それらのファイルを環境間で移動させる責任者が誰なのかについて全員が合意している場合に限ります。ほとんどのチームでは、その合意は決してきちんと存在しません。

古典的な問題は、Drupal サイトをリリースしたことのある人なら誰でもよく知っています。

  • 設定のドリフト — ステージングが本番と乖離し、本番がローカルと乖離し、どの環境が正典なのか誰も確信が持てなくなる。
  • 「ステージングでは動くのに本番では動かない」 — 誰かがステージングでビューやフィールドフォーマッターを更新し、それをエクスポートしなかったから。
  • 手動の drush cim がコンテンツを壊す — 夜23時の慌ただしいインポートが、稼働中のノードから今なお参照されているコンテンツタイプのフィールドを削除してしまう。

これらのシナリオすべての根本原因は同じです。人間が、設定をいつ、そして反映するかどうかを決めているのです。人間は忘れます。プレッシャーの下で手順を飛ばします。結果的に誤りだった判断を下します。

CI は忘れません。 パイプラインは通るか、落ちるかのどちらかです。出席すべきスタンドアップもありません。リリースが20分後だということも知りません。その決定性こそ、設定管理が必要とするものです。

この記事が果たす約束はこうです。

  • あらゆる設定変更は、共有環境に触れる前に Git にコミットされる。
  • 設定の検証とインポートの責任は、開発者ではなくパイプラインにある。
  • 環境間の反映に、手動の手順は一切不要。
  • 設定のドリフトは、Slack のメッセージではなくビルドの失敗である。
前提: あなたは Drupal 10 または 11 を使い、フィーチャーブランチによる Git ベースのワークフローを用い、少なくとも2つの共有環境(例:ステージングと本番)にデプロイしています。チームは Jenkins か GitLab CI、あるいはその両方を使っています。

2. 実際のプロジェクトから学んだ基本原則

設定はコードである

サイトの挙動を変えるものなら、それは Git に属します。以上。ビュー、コンテンツタイプ、パフォーマンス設定、画像スタイル — すべてコードです。設定ファイルを PHP ファイルと同じ規律で扱ってください。レビューし、バージョン管理し、共有環境で直接編集してはいけません。

共有環境で手動の drush cim をしない

設定をインポートするのはパイプラインです。開発者ではありません。このルールは、作業ディレクトリに未コミットのローカル変更を残したまま誰かが本番で drush cim を実行する瞬間を初めて経験するまでは、極端に聞こえます。

パイプラインは設定ドリフトで即座に失敗しなければならない

設定をインポートし、その直後にエクスポートしても、差分が生じてはなりません。差分が生じたら、ビルドは失敗します。この1つのルールは、私たちが追加した他のどのチェックよりも多くのバグを捕まえます。

あるシニア開発者が、本番の遅い検索ページをデバッグしていました。彼は UI から Search API のインデックス設定を微調整し、パフォーマンスが改善したことを確認して、チケットを閉じました。2週間後、あるリリースが Git から設定をインポートし、彼の UI での変更を上書きしたため、検索のパフォーマンスが崩壊しました。3日間、誰も点と点を結びつけませんでした。そのインシデントの後、私たちはルールを明示化しました。すぐにエクスポートしてコミットされなかった UI 変更は、失われたものとみなす、と。
メディアの多いサイトで、core.extension.yml がエクスポートから除外されていました。「どうせモジュールは Composer で管理されているから」という理由です。3か月後、あるホットフィックスが、本番で意図的に無効化されていたモジュールを再び有効化しました。ホットフィックスは正しかったのですが、その後の設定インポートが、静かにそのモジュールを再び無効化しました。このインシデントこそ、私たちが今 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 が環境変数から読み込む
    └── ...

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): add fulltext search API index config"

# または、変更が試験的でまだ準備できていない場合は破棄する:
git checkout -- config/sync

私たちはこれを、PHP やテンプレートファイルが config/sync の対応する変更なしにステージングされたときに警告する(ブロックはしない)軽量な Git pre-commit フックで補強しています。

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 フィーチャーブランチ

設定は、それを必要とするコードと同じブランチ、同じ 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が合わせて最も重要です。インポートし、その直後に再エクスポートしても、空の差分でなければなりません。そうでなければ、次のいずれかが真です。

  • モジュールがインポート時に設定を生成している(しばしばモジュールのバグ)。
  • 設定エンティティの 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 が内部に実際に保存したものと一致するかは分かりません。再エクスポートして差分を取ることだけが、そのギャップを埋めます。
 

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 の秘密情報
  }

  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 の credentials/環境変数から settings.env.php を生成
        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

2〜3 以上の Drupal サイトを管理する場合は、Drupal パイプラインのロジックを 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'
	)

ワークスペースのクリーンアップ

post { always } ブロックで必ず cleanWs() を呼び出してください。Jenkins のエージェントは以前のビルドの状態を蓄積します。前のビルドの 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"

# ── 再利用可能なキャッシュ ────────────────────────────────
.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 には、このワークフローを特にクリーンにする機能がいくつかあります。

  • 一級のアーティファクト — 失敗した設定エクスポートが自動的に保存され、追加設定なしで MR の UI から閲覧できます。
  • composer.lock をキーにしたネイティブキャッシュ — 初回以降の Composer インストールが安価になります。
  • Merge Request でのパイプラインの可視化 — 開発者は、マージ前に MR で直接、設定検証のステータスを確認できます。
  • Services ブロック — MySQL が、インフラのオーバーヘッドなしにサイドカーとして起動します。
  • Environments + 手動ゲート — 本番デプロイの when: manual ルールが、別個の承認ワークフローを必要とせずに、人によるチェックポイント付きのワンクリック反映を提供します。
Shell ci/drupal-deploy.sh
#!/bin/bash
set -euo pipefail

TARGET_ENV="${1:-staging}"
echo "=== Deploying to: ${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 "=== Running post-deploy commands on ${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 "❌ Config drift on target!"; exit 1)
  drush cr
  echo "✅ Deploy complete."
REMOTE

9. ごまかさない、環境固有の設定

すべての設定値が全環境で同じであるべきではありません。検索バックエンド、ログの詳細度、キャッシュ層、サードパーティの API エンドポイントは、いずれも正当に異なります。問題は、設定パイプラインの整合性を損なわずに、その違いをどう扱うかです。

正しいツール:config_splitconfig_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(本番クラスター)
エラーログdblog、詳細モードsyslogsyslog + 外部 APM
パフォーマンスモジュール無効有効有効 + CDN 設定
メール転送Mailhog / null mailerMailpitSMTP / SendGrid

不適切なアプローチ — これらをしてはいけません:

  • 本番データベースで設定を直接編集する。
  • settings.php 内で if ($settings['environment'] === 'prod') の条件分岐を使って設定値を差し替える — これは設定システムを完全に回避し、目に見えない実行時の乖離を生みます。
  • 環境ごとに異なる設定ファイルを持つ、別々の Git ブランチを維持する。

10. 環境間の反映(プロモーション)

プロモーションはデプロイではありません。 デプロイはコードと設定を環境へ移します。プロモーションは、検証済みのアーティファクト(すでに CI を通過したもの)を、連鎖の次の環境へ移します。

実際には、これは、dev で検証されたのと同じ Git SHA が本番に到達することを意味します。土壇場のコミットはなし。CI を飛ばすホットフィックスはなし。設定チェックを回避する cherry-pick はなし。

✅ CI が SHA を検証
フィーチャーブランチ / main で
🌐 Dev
マージ時に自動デプロイ
🌐 Stage
develop から自動デプロイ
🌐 Prod
main で手動ゲート

同じ検証済みアーティファクトが各環境を渡っていきます。CI は一度だけ実行され、その結果が伝播します。

不変ビルドの哲学は、こう意味します。CI が検証したものが、デプロイされるものである。ホットフィックスが本当に必要なら、それは独自の CI 実行を伴うファストトラックのブランチを通ります。検証を回避することはありません。

設定チェックを飛ばす土壇場のホットフィックスを防ぐ: main ブランチを、必須のパイプラインステータスチェックで保護してください。GitLab の「protected branches」と 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 で参照するブロックや、タクソノミーの用語でフィルタリングするビューです。これらの参照は、コンテンツが異なる形で作成された場合、環境間で異なり得ます。解決策は、そのコンテンツを設定ではなく、マイグレーションや 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 "❌ Config drift in site: $SITE"
    exit 1
  fi
done
echo "✅ All sites clean."

設定変更を伴う Drupal コアのアップグレード

コアの更新は、既存の設定に影響するスキーマ変更を伴うことがあります。常に drush cim の前に drush updb を実行し、その後 drush cex を実行して、スキーマによって正規化された変更を取り込みます。それらの変更を、コアアップグレードのブランチの一部としてコミットしてください。パイプラインで不意打ちを食らわないように。

12. 運用上のセーフティネット

本番での読み取り専用の設定

本番で Config Readonly モジュールを有効化することを検討してください。管理 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 "❌ Unexpected config differences after deploy"; exit 1; \
} || echo "✅ Config status clean"

# キャッシュがウォームか検証する:
drush cr
drush php-eval "echo \Drupal::state()->get('system.cron_last');"

ロールバック戦略

ロールバックは Git の操作であって、データベースの操作ではありません。デプロイが問題を引き起こした場合は:

  1. 最後に正常だったコミット SHA を特定する。
  2. その SHA でパイプラインの実行をトリガーする。
  3. パイプラインが、以前に検証済みのアーティファクトを再デプロイする。

設定インポートを取り消すためにデータベースを手作業でいじることは、決して答えではありません。それはすべての安全チェックを回避し、たいてい直す以上のドリフトを生みます。

監査:誰が、いつ、なぜ設定を変えたか

あらゆる設定変更が Git を通るため、あなたの監査証跡は Git の履歴です。意味のあるコミットメッセージを使い、commit-msg フックや CI の lint ステップでそれを強制してください。

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 を実行する未知の状態をインポートする — 未コミットのローカル変更を含む可能性共有環境で設定をインポートするのはパイプラインのみ
ステージングでの UI 変更を許すステージングが信頼できる情報源になり、Git との乖離を生むすべての共有環境で config_readonly を有効化する
開発者が「エクスポートを忘れない」と信じるプレッシャーの下で、彼らは忘れるpre-commit フックが警告し、CI の失敗が強制する
設定ドリフトで失敗させない静かな乖離が、派手に壊れるまで蓄積するdrush cim → cex → git diff --exit-code の三点セットは必須
環境ごとに異なる設定を持つ別々のブランチマージがコンフリクトの悪夢になり、単一の信頼できる情報源がなくなる1つの設定ブランチを config_split で分割する
drush cim の前に drush updb を飛ばすスキーマ更新がインポート失敗や静かなデータ損失を引き起こし得る常に:updb → cim → cex → diff

14. このパターンを適用した後の結果

このアプローチを、1つの小さなサイトから数十のインストールを管理するエージェンシーまで、Drupal サイトのポートフォリオ全体に展開した結果、成果は一貫していました。

約 80%
設定に関連するデプロイインシデントの削減
1 日未満
新しい開発者の設定ワークフローへのオンボーディング
0
18 か月間の本番での手動 drush cim 実行回数
100%
Git コミットと MR まで追跡可能な設定変更

数字を超えて、文化の変化が最も重要です。設定の議論が、Slack から Git の履歴へ移ります。「誰かステージングで画像スタイルを変えた?」の代わりに、問いは「それのコミットはある?」になります。そして、必ずあるか、変更が起きていないかのどちらかです。

新しい開発者は、「今どの環境が正典なのか」という暗黙のルールを学ぶ必要がもはやありません。答えは常に Git です。パイプラインがそれを強制します。

15. 最終的な推奨事項

  • 厳格に始め、理由があるときにだけ緩める。 不要だと分かったルールを緩めるほうが、そもそも無かったルールを締めるより簡単です。
  • あらゆる設定差分をビルドの失敗として扱う。 例外なし、「リリース後に直す」もなし。
  • CI が、デプロイされるものの信頼できる情報源である — 開発者のノートパソコンでも、ステージング環境でも、Slack の決定でもありません。
  • updb → cim → cex → diff の順序は不可侵である。 どれか1つの手順でも飛ばせば、あなたは盲目で飛んでいます。
  • 真の環境差には config_split を使う。 settings.php の条件分岐を近道として使わないでください。
  • すべての共有環境で config_readonly を有効化する。 偶発的な UI 変更を、単に非推奨にするのではなく、不可能にしてください。
  • 失敗した設定エクスポートを CI アーティファクトとして保存する。 開発者は、差分があったという事実だけでなく、何が正確にドリフトしたのかを見る必要があります。
  • ロールバックは Git 経由で、決してデータベースの手術で行わない。 パイプラインが、両方向で最も安全な道です。
このアプローチはスケールします。 あなたが1つのサイトの単独開発者であろうと、50 の Drupal インストールを管理するエージェンシーであろうと、ルールは同じです。パイプラインは、チームの規模やリリースのプレッシャーを気にしません。落ちるか、通るかです。その一貫性こそが、すべての要点です。

Jenkins と GitLab CI は、この規律を強制するうえで同等に有能です。GitLab CI は必要なインフラが少なく、アーティファクトのネイティブサポートが優れています。Jenkins は複雑なエンタープライズ環境向けの柔軟性が高く、多くのプロジェクトでパイプラインを標準化するための shared library のサポートが優れています。あなたの組織に合ったツールを選んでください。この記事の原則は、いずれにせよ当てはまります。

目標は、設定管理を複雑にすることでは決してありませんでした。目標は、それを退屈にすることです。常に機能し、誰も考える必要がなく、夜23時のインシデントを決して引き起こさない、日常的な手順にすることです。正しく実装された CI パイプラインがあれば、退屈こそ、あなたが手にするものです。

技術・アーキテクチャに関するお問い合わせ
Ivan Abramenko, Principal Drupal Architect
ivan.abramenko@drupalbook.org
プロジェクトに関するお問い合わせ
projects@drupalbook.org