logo

Extra Block Types (EBT) - Nieuwe Layout Builder ervaring❗

Extra Block Types (EBT) - gestileerde, aanpasbare bloktypes: Slideshows, Tabs, Cards, Accordions en vele andere. Ingebouwde instellingen voor achtergrond, DOM Box, javascript-plugins. Ervaar vandaag al de toekomst van layout building.

Demo EBT-modules Download EBT-modules

❗Extra Paragraph Types (EPT) - Nieuwe Paragraphs ervaring

Extra Paragraph Types (EPT) - analoge op paragrafen gebaseerde set modules.

Demo EPT-modules Download EPT-modules

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-gestuurd configuratiebeheer met Jenkins en GitLab CI

16/04/2026, by Ivan

1. Waarom CI-gestuurd configuratiebeheer belangrijk is

Het configuratiesysteem van Drupal is een van de grootste sterke punten van het platform β€” en een van de meest betrouwbare bronnen van problemen. De mogelijkheid om elk stukje siteconfiguratie te exporteren en te importeren als YAML-bestanden is krachtig, maar alleen als iedereen het eens is over wie verantwoordelijk is voor het verplaatsen van die bestanden tussen omgevingen. In de meeste teams bestaat die overeenstemming nooit echt.

De klassieke problemen zijn goed bekend bij iedereen die een Drupal-site heeft opgeleverd:

  • Config drift β€” staging wijkt af van productie, productie wijkt af van lokaal, en niemand weet zeker welke omgeving canoniek is.
  • "Werkt op staging maar niet op productie" β€” omdat iemand een view of een field formatter op staging heeft bijgewerkt en dit nooit heeft geΓ«xporteerd.
  • Handmatige drush cim breekt content β€” een gehaaste import om 23:00 uur die een content type-veld verwijderde dat nog steeds werd gebruikt door live nodes.

De hoofdoorzaak van elk van deze scenario's is hetzelfde: een mens bepaalt wanneer en of configuratie wordt gepromoveerd. Mensen vergeten dingen. Ze slaan stappen over onder druk. Ze maken beoordelingskeuzes die achteraf verkeerd blijken te zijn.

CI vergeet niet. Een pipeline slaagt of faalt. Hij hoeft niet naar een stand-up. Hij weet niet dat de release over twintig minuten plaatsvindt. Die deterministische aanpak is precies wat configuratiebeheer nodig heeft.

De belofte die dit artikel waarmaakt:

  • Elke configuratiewijziging wordt vastgelegd in Git voordat deze een gedeelde omgeving bereikt.
  • De pipeline, niet een ontwikkelaar, is verantwoordelijk voor het valideren en importeren van configuratie.
  • Promotie tussen omgevingen vereist nul handmatige stappen.
  • Config drift is een buildfout, geen Slack-bericht.
Aannames: Je draait Drupal 10 of 11, gebruikt een Git-gebaseerde workflow met feature branches en deployt naar minstens twee gedeelde omgevingen (bijv. staging en productie). Je team gebruikt Jenkins of GitLab CI β€” of beide.

2. Kernprincipes die we hebben geleerd uit echte projecten

Configuratie is code

Als het het gedrag van de site verandert, hoort het in Git. Punt. Een view, een content type, een performance-instelling, een image style β€” alles is code. Behandel configuratiebestanden met dezelfde discipline als PHP-bestanden: review ze, versieer ze, bewerk ze nooit direct in een gedeelde omgeving.

Geen handmatige drush cim in gedeelde omgevingen

De pipeline importeert configuratie. Ontwikkelaars niet. Deze regel klinkt extreem totdat je meemaakt dat iemand drush cim op productie uitvoert terwijl er nog niet-gecommitteerde lokale wijzigingen in hun werkdirectory staan.

Pipelines moeten snel falen bij config drift

Configuratie importeren en vervolgens onmiddellijk exporteren zou geen diff mogen opleveren. Als dat wel gebeurt, faalt de build. Deze ene regel vangt meer bugs af dan elke andere check die we hebben toegevoegd.

Een senior ontwikkelaar was een trage zoekpagina op productie aan het debuggen. Hij paste de Search API-indexinstellingen via de UI aan, bevestigde dat de performance verbeterde en sloot het ticket. Twee weken later importeerde een release configuratie uit Git β€” waardoor zijn UI-wijzigingen werden overschreven β€” en de zoekprestaties instortten. Drie dagen lang legde niemand het verband. Na dat incident hebben we de regel expliciet gemaakt: elke UI-wijziging die niet onmiddellijk wordt geΓ«xporteerd en gecommit, wordt als verloren beschouwd.
Op een mediasite werd core.extension.yml uitgesloten van exports omdat "modules toch al door Composer worden beheerd." Drie maanden later schakelde een hotfix een module opnieuw in die bewust in productie was uitgeschakeld. De hotfix was correct β€” maar een daaropvolgende config-import schakelde de module stilletjes weer uit. Dat incident is de reden waarom we core.extension.yml nu het gevaarlijkste bestand noemen dat je kunt negeren.

3. Architectuur op hoog niveau

πŸ‘¨β€πŸ’» Ontwikkelaar
drush cex
β†’
πŸ“ Git-repository
config/sync
β†’
βš™ CI-pipeline
valideren + importeren
β†’
🌐 Dev
β†’
🌐 Stage
β†’
🌐 Prod

Configuratie stroomt slechts in één richting: van de lokale export van de ontwikkelaar, via Git, door de CI-pipeline en naar elke omgeving.

Het belangrijkste inzicht is de scheiding van verantwoordelijkheden:

ActorVerantwoordelijkheidNooit verantwoordelijk voor
OntwikkelaarConfig exporteren, committen naar Git, MR/PR openenConfig importeren op een gedeelde omgeving
CI-pipelineValideren, importeren, verifiΓ«ren, promotenConfiguratiebestanden genereren of bewerken
OmgevingDe site draaienConfig genereren, opslaan of exporteren

Omgevingen zijn consumenten van configuratie, nooit producenten. Op het moment dat een omgeving een bron van waarheid voor configuratie wordt, is drift onvermijdelijk.

4. Repository-structuur die schaalt

Directory Tree project root
project-root/
β”œβ”€β”€ composer.json
β”œβ”€β”€ composer.lock
β”œβ”€β”€ Jenkinsfile
β”œβ”€β”€ .gitlab-ci.yml
β”‚
β”œβ”€β”€ ci/
β”‚   β”œβ”€β”€ drupal-config-check.sh   # Herbruikbaar validatiescript
β”‚   β”œβ”€β”€ drupal-deploy.sh
β”‚   └── drupal-install.sh
β”‚
β”œβ”€β”€ config/
β”‚   β”œβ”€β”€ sync/                        # Config staat hier β€” gecommit naar Git
β”‚   β”‚   β”œβ”€β”€ core.extension.yml
β”‚   β”‚   β”œβ”€β”€ system.site.yml
β”‚   β”‚   └── ...
β”‚   └── splits/                      # config_split overrides per omgeving
β”‚       β”œβ”€β”€ development/
β”‚       β”œβ”€β”€ staging/
β”‚       └── production/
β”‚
└── web/
    β”œβ”€β”€ sites/
    β”‚   └── default/
    β”‚       β”œβ”€β”€ settings.php          # Gecommit, geen secrets
    β”‚       β”œβ”€β”€ settings.local.php    # Gitignored, lokale overrides
    β”‚       └── settings.env.php      # Geladen door CI vanuit env-vars
    └── ...

Wat hoort in config/sync

Alle configuratie-YAML-bestanden die zijn gegenereerd door drush cex. Deze directory is de enige bron van waarheid voor siteconfiguratie. Deze wordt gecommit, gereviewd en gedeployed zoals elke andere code.

Wat nooit in config thuishoort

  • Omgevingsspecifieke hostnames, API-sleutels of credentials β€” gebruik omgevingsvariabelen en settings.env.php.
  • Secrets van welke aard dan ook β€” injecteer via je CI secret store (Jenkins Credentials of GitLab CI-variabelen).
  • Content β€” gebruik de Default Content-module niet als hulpmiddel voor wat configuratie zou moeten zijn.
PHP web/sites/default/settings.php
// settings.php β€” gecommit naar Git, omgevingsonafhankelijk
$settings['config_sync_directory'] = DRUPAL_ROOT . '/../config/sync';

// Laad omgevingsspecifieke waarden die door CI of de host zijn geΓ―njecteerd.
if (file_exists($app_root . '/' . $site_path . '/settings.env.php')) {
  include $app_root . '/' . $site_path . '/settings.env.php';
}

// Laad optionele lokale overrides (gitignored).
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
// Gegenereerd door CI vanuit omgevingsvariabelen β€” nooit committen.
$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');

// Laat config_split weten in welke omgeving we zitten.
$config['config_split.config_split.production']['status'] =
  (getenv('APP_ENV') === 'production');

5. Drupal-configuratieworkflow

5.1 Lokale ontwikkeling

De lokale workflow is de enige plek waar de feedbacklus tussen UI-wijzigingen en config-export snel en routinematig moet zijn. Elke ontwikkelaar op het project volgt dezelfde regels:

  • Maak UI- of codewijzigingen lokaal.
  • Voer onmiddellijk drush cex uit β€” niet aan het einde van de dag.
  • Bekijk de diff met git diff config/sync.
  • Commit de wijzigingen of verwerp ze met git checkout -- config/sync.
  • Er bestaat geen tussenstatus.
Shell Lokale ontwikkelingsworkflow
# Na het aanbrengen van configuratiewijzigingen via de Drupal UI of install hooks:
drush cex --yes

# Bekijk wat er veranderd is β€” behandel dit zoals elke codewijziging:
git diff config/sync

# Stage en commit samen met de featurecode:
git add config/sync
git commit -m "feat(search): add fulltext search API index config"

# Of verwerp als de wijziging verkennend was en nog niet klaar:
git checkout -- config/sync

We handhaven dit met een lichte Git pre-commit hook die waarschuwt (maar niet blokkeert) wanneer PHP- of templatebestanden worden gestaged zonder een overeenkomstige wijziging in config/sync:

Shell .git/hooks/pre-commit
#!/bin/bash
# Waarschuw als module/theme-code gewijzigd is maar config/sync niet.
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 "⚠  Waarschuwing: PHP/module-bestanden gestaged maar geen config/sync-wijzigingen gedetecteerd."
  echo "   Ben je vergeten: drush cex uit te voeren?"
  echo "   Ga toch door β€” maar controleer dit nog eens."
fi
exit 0

5.2 Feature branches

Configuratie bevindt zich naast de code die deze vereist in dezelfde branch en dezelfde pull/merge request. Een feature die een nieuw content type toevoegt, moet zowel de PHP install hook (indien aanwezig) als de YAML-bestanden voor dat content type in dezelfde commit bevatten.

Vroegtijdig kapotte afhankelijkheden opsporen: Als branch A een nieuw veld toevoegt en branch B de weergave van datzelfde veld wijzigt, zal het mergen van B vΓ³Γ³r A een config-importfout veroorzaken. Dit is precies het juiste resultaat β€” je wilt dat de pipeline dit op de branch detecteert, niet op productie.

6. CI-verantwoordelijkheden: Wat de pipeline moet afdwingen

Elke pipeline die een gedeelde Drupal-omgeving aanraakt, moet deze stappen in deze volgorde uitvoeren:

  1. Installeer of herstel de database β€” een schone installatie of een gesanitiseerde productiesnapshot.
  2. Voer database-updates uit β€” drush updb.
  3. Importeer configuratie β€” drush cim --yes.
  4. Exporteer configuratie opnieuw β€” drush cex --yes.
  5. Controleer op diff β€” als een YAML-bestand is gewijzigd, faalt de build.

Stap 4 en 5 samen zijn het belangrijkst. Configuratie importeren en vervolgens onmiddellijk opnieuw exporteren moet een lege diff opleveren. Als dat niet zo is, is één van deze dingen waar:

  • Een module genereert config bij import (vaak een bug in de module).
  • De UUID van een config-entiteit komt niet overeen met wat er in de database staat.
  • Een config-schema is onvolledig, waardoor Drupal waarden anders normaliseert dan hoe ze zijn geΓ«xporteerd.
  • Een ontwikkelaar heeft configbestanden handmatig bewerkt en inconsistenties geΓ―ntroduceerd.
Shell ci/drupal-config-check.sh
#!/bin/bash
set -euo pipefail

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

echo "=== Configuratie importeren vanuit config/sync ==="
drush cim --yes

echo "=== Opnieuw exporteren om te verifiΓ«ren dat er geen openstaande wijzigingen zijn ==="
drush cex --yes

echo "=== Controleren op config drift ==="
if ! git diff --exit-code config/sync; then
  echo ""
  echo "❌ FOUT: Config drift gedetecteerd!"
  echo "   De config in Git komt niet overeen met wat Drupal produceerde na import+export."
  echo "   Diff hierboven getoond. Herstel dit lokaal met 'drush cex' en commit."
  exit 1
fi

echo "βœ… Configuratie is schoon β€” geen drift gedetecteerd."
Waarom opnieuw exporteren na import niet onderhandelbaar is: Zonder de re-exportstap weet je alleen dat de import is geslaagd. Je weet niet of de geΓ―mporteerde config overeenkomt met wat Drupal intern daadwerkelijk heeft opgeslagen. Alleen opnieuw exporteren en een diff uitvoeren sluit dat gat.
 

7. Jenkins-implementatie (Bewezen in de praktijk)

7.1 Jenkinsfile-structuur

We gebruiken uitsluitend declaratieve pipelines. Scripted pipelines bieden meer flexibiliteit, maar declaratieve pipelines zijn gemakkelijker te lezen, eenvoudiger te controleren met jenkins-cli declarative-linter en beter te begrijpen voor nieuwe teamleden.


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 {
        // Genereer settings.env.php vanuit Jenkins credentials/env-vars
        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: "Configvalidatie mislukt. Zie: ${BUILD_URL}",
        recipientProviders: [[$class: 'DevelopersRecipientProvider']]
      )
    }
  }
}

7.2 Jenkins-specifieke overwegingen

Shared Libraries

Wanneer je meer dan twee of drie Drupal-sites beheert, haal dan de Drupal pipeline-logica uit naar een Jenkins Shared Library. De Jenkinsfile van elk project wordt dan een dunne wrapper:

Groovy Jenkinsfile (dunne wrapper met shared library)
@Library('drupal-pipeline-lib@v2') _

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

Workspace-opruiming

Roep altijd cleanWs() aan in het post { always }-blok. Jenkins-agents verzamelen status van eerdere builds β€” een settings.env.php of vendor-directory van een vorige build kan de huidige build stilzwijgend beΓ―nvloeden. Wees rigoureus: maak de workspace schoon.

Herbruik nooit een database tussen pipeline-runs tenzij je expliciet upgradepaden test. Een overgebleven database van een vorige build kan UUID-mismatches, ontbrekende migraties en problemen met de installvolgorde van modules maskeren.

8. GitLab CI-implementatie

8.1 .gitlab-ci.yml-structuur

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

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

# ── Gedeelde variabelen ─────────────────────────────────────────────
variables:
  COMPOSER_CACHE_DIR: "$CI_PROJECT_DIR/.cache/composer"
  APP_ENV: "ci"
  MYSQL_DATABASE: "drupal_ci"
  MYSQL_ROOT_PASSWORD: "root"

# ── Herbruikbare cache ───────────────────────────────────────────────
.cache-composer: &cache-composer
  cache:
    key:
      files: [composer.lock]
    paths:
      - .cache/composer
      - vendor/
    policy: pull

# ── Build-fase ──────────────────────────────────────────────────
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

# ── Testfase ───────────────────────────────────────────────────
phpunit:unit:
  stage: test
  <<: *cache-composer
  script:
    - ./vendor/bin/phpunit --testsuite=unit --log-junit=reports/phpunit.xml
  artifacts:
    reports:
      junit: reports/phpunit.xml

# ── Configvalidatiefase ──────────────────────────────────────
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

# ── Deployfase ─────────────────────────────────────────────────
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   # Promotie met één klik, menselijke goedkeuring vereist
  script:
    - bash ci/drupal-deploy.sh production

8.2 GitLab CI-voordelen

GitLab CI heeft verschillende functies die deze workflow bijzonder overzichtelijk maken:

  • First-class artifacts β€” Mislukte config-exports worden automatisch opgeslagen en kunnen in de MR UI worden bekeken zonder extra configuratie.
  • Native caching op basis van composer.lock β€” Composer-installaties zijn na de eerste run goedkoop.
  • Zichtbaarheid van Merge Request pipelines β€” Ontwikkelaars zien de configvalidatiestatus direct op de MR vΓ³Γ³r het mergen.
  • Services-blok β€” MySQL wordt als sidecar opgestart zonder infrastructuuroverhead.
  • Omgevingen + handmatige gates β€” De when: manual-regel op de productiedeploy geeft je promotie met één klik en een menselijke controle, zonder een aparte goedkeuringsworkflow.
Shell ci/drupal-deploy.sh
#!/bin/bash
set -euo pipefail

TARGET_ENV="${1:-staging}"
echo "=== Deployen naar: ${TARGET_ENV} ==="

# Synchroniseer build-artifact naar doel (rsync, SSH, S3 β€” pas aan naar je host)
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-deploycommando's uitvoeren op ${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
  # Laatste sanity check op de doelomgeving zelf
  git diff --exit-code config/sync || (echo "❌ Config drift op doelomgeving!"; exit 1)
  drush cr
  echo "βœ… Deploy voltooid."
REMOTE

9. Omgevingsspecifieke configuratie zonder valsspelen

Niet elke configuratiewaarde moet in alle omgevingen hetzelfde zijn. Zoekbackends, logging-niveaus, cachinglagen en API-endpoints van derden verschillen terecht. De vraag is hoe je dat verschil behandelt zonder de integriteit van je config-pipeline in gevaar te brengen.

De juiste tools: config_split en config_ignore

config_split stelt je in staat om configuratiesets te definiΓ«ren die enkel actief zijn in specifieke omgevingen. Elke split bevindt zich in een eigen directory en wordt geactiveerd via settings.php of settings.env.php op basis van een omgevingsvariabele.

YAML config/sync/config_split.config_split.development.yml
langcode: en
status: true
id: development
label: Development
description: 'Config alleen actief in lokale/dev-omgevingen'
folder: '../config/splits/development'
module:
  devel: 0
  kint: 0
  dblog: 0
theme: {  }
blacklist: {  }
graylist: {  }
PHP web/sites/default/settings.env.php (omgeving activeren)
$app_env = getenv('APP_ENV') ?: 'production';

// Activeer de juiste config_split op basis van de omgeving.
$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');

Praktische voorbeelden van splits

Config-itemDev splitStage splitProd split
Search API-backendDatabase-backendSolr (klein)Solr (prod-cluster)
Foutloggingdblog, uitgebreidsyslogsyslog + externe APM
Performance-modulesUitgeschakeldIngeschakeldIngeschakeld + CDN-config
MailtransportMailhog / null mailerMailpitSMTP / SendGrid

Onjuiste benaderingen β€” doe dit niet:

  • Configuratie rechtstreeks bewerken in de productiedatabase.
  • Gebruik maken van if ($settings['environment'] === 'prod')-voorwaarden in settings.php om config-waarden te vervangen β€” dit omzeilt het configsysteem volledig en creΓ«ert onzichtbare runtime-afwijkingen.
  • Aparte Git-branches per omgeving bijhouden met verschillende configbestanden.

10. Promotie tussen omgevingen

Promotie is geen deployment. Deployment verplaatst code en config naar een omgeving. Promotie verplaatst een gevalideerd artefact β€” iets dat CI al heeft doorstaan β€” naar de volgende omgeving in de keten.

In de praktijk betekent dit dat dezelfde Git SHA die op dev is gevalideerd, ook de productieomgeving bereikt. Geen last-minute commits. Geen hotfixes die CI overslaan. Geen cherry-picks die de configcontrole omzeilen.

βœ… CI valideert SHA
op feature branch / main
β†’
🌐 Dev
auto-deploy bij merge
β†’
🌐 Stage
auto-deploy vanuit develop
β†’
🌐 Prod
handmatige gate op main

Hetzelfde gevalideerde artefact gaat door de omgevingen. CI draait één keer; het resultaat verspreidt zich.

De filosofie van immutabele builds betekent: wat CI valideert, is wat wordt gedeployed. Als een hotfix echt noodzakelijk is, gaat deze via een fast-track branch met een eigen CI-run β€” validatie wordt niet omzeild.

Voorkomen dat last-minute hotfixes configchecks overslaan: Bescherm je main-branch met een verplichte pipeline-statuscheck. GitLab's "protected branches" en Jenkins' "GitHub Branch Source"-plugin ondersteunen dit beide. Als de pipeline niet is geslaagd, kan de branch niet naar productie worden gedeployed.

11. Omgaan met randgevallen (je gaat deze tegenkomen)

UUID-mismatches

Config-entiteiten bevatten UUID’s. Als je een nieuwe site installeert en vervolgens configuratie probeert te importeren vanuit een andere installatie, zal Drupal weigeren met een UUID-mismatchfout. De oplossing is om altijd te installeren met --existing-config of om na installatie de site-UUID in te stellen:

Shell
# Lees de UUID uit de gecommitte configuratie en pas deze toe op de nieuwe installatie:
SITE_UUID=$(grep "^uuid:" config/sync/system.site.yml | awk '{print $2}')
drush config-set "system.site" uuid "$SITE_UUID" --yes
drush cim --yes

Module in-/uitschakelvolgorde

Bij het inschakelen van een nieuwe module via configuratie respecteert Drupal automatisch de afhankelijkheidsvolgorde tijdens drush cim. Als de install hook van een module echter standaardconfiguratie genereert die conflicteert met wat zich in config/sync bevindt, moet je mogelijk die standaardconfiguratie verwijderen en opnieuw importeren. De pipeline zal dit als drift detecteren.

Contentafhankelijke configuratie

Sommige configuratie verwijst naar content β€” bijvoorbeeld een block dat verwijst naar een menu-item op basis van ID, of een view die filtert op een taxonomieterm. Deze verwijzingen kunnen per omgeving verschillen als content anders is aangemaakt. De oplossing is om die content te beheren via migraties of default content-modules, niet via config.

Multisite-eigenaardigheden

In een multisite-opzet heeft elke site zijn eigen config sync-directory. De CI-pipeline moet de configcheck uitvoeren voor elke site, niet alleen voor de primaire. Gebruik een lus:

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 "--- Config controleren voor 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 "βœ… Alle sites zijn schoon."

Drupal Core upgraden met configuratiewijzigingen

Core-updates bevatten soms schemawijzigingen die bestaande configuratie beΓ―nvloeden. Voer altijd drush updb uit vΓ³Γ³r drush cim, en daarna drush cex om eventuele door schema genormaliseerde wijzigingen vast te leggen. Commit deze wijzigingen als onderdeel van de core-upgrade-branch β€” laat je er niet door verrassen in de pipeline.

12. Operationele vangnetten

Read-only config in productie

Overweeg om de https://www.drupal.org/project/config_readonly Config Readonly-module in productie in te schakelen. Dit voorkomt configuratiewijzigingen via de admin UI β€” een harde blokkade die de regel "geen handmatige wijzigingen" op applicatieniveau afdwingt.

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

Verificatie na deployment

ShellControles na deployment
# Controleer of er geen openstaande config-wijzigingen zijn na deployment:
drush config-status 2>&1 | grep -v 'No differences' && { \
  echo "❌ Onverwachte configverschillen na deployment"; exit 1; \
} || echo "βœ… Configstatus schoon"

# Controleer of caches opgewarmd zijn:
drush cr
drush php-eval "echo \Drupal::state()->get('system.cron_last');"

Rollback-strategie

Rollback is een Git-operatie, geen database-operatie. Als een deployment een probleem veroorzaakt:

  1. Identificeer de laatste goede commit SHA.
  2. Start een pipeline-run op die SHA.
  3. De pipeline deployt opnieuw het eerder gevalideerde artefact.

Handmatige database-ingrepen om een config-import ongedaan te maken zijn nooit de oplossing β€” het omzeilt alle veiligheidscontroles en creΓ«ert doorgaans meer drift dan het oplost.

Auditing: Wie heeft config gewijzigd, wanneer en waarom

Omdat elke configwijziging via Git gaat, is je audittrail je Git-geschiedenis. Gebruik betekenisvolle commitberichten en dwing dit af met een commit-msg hook of een CI-lintstap:

ShellVoorbeeld commitgeschiedenis
$ 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. Veelgemaakte fouten die we niet meer maken

FoutWaarom dit problematisch isDe oplossing
Handmatig drush cim uitvoeren op staging of productieImporteert een onbekende status β€” mogelijk inclusief niet-gecommitteerde lokale wijzigingenAlleen de pipeline importeert configuratie op gedeelde omgevingen
UI-wijzigingen toestaan op stagingStaging wordt een bron van waarheid en veroorzaakt afwijkingen ten opzichte van GitSchakel config_readonly in op alle gedeelde omgevingen
Vertrouwen dat ontwikkelaars "zullen onthouden om te exporteren"Dat doen ze niet, onder drukPre-commit hooks waarschuwen; CI-fouten dwingen naleving af
Niet falen bij config driftStilzwijgende afwijkingen stapelen zich op tot iets spectaculair faaltDe drush cim β†’ cex β†’ git diff --exit-code-stappenreeks is verplicht
Aparte branches per omgeving met verschillende configuratieMergen wordt een conflictΒ­nachtmerrie; geen enkele bron van waarheidEΓ©n configuratieΒ­branch, gesplitst via config_split
drush updb overslaan vΓ³Γ³r drush cimSchema-updates kunnen importfouten of stil dataverlies veroorzakenAltijd: updb β†’ cim β†’ cex β†’ diff

14. Resultaten na toepassing van deze aanpak

Na het uitrollen van deze aanpak over een portfolio van Drupal-sites β€” variΓ«rend van één kleine website tot een bureau dat tientallen installaties beheert β€” bleken de resultaten consistent:

~80%
Afname van configuratiegerelateerde deployment-incidenten
< 1 dag
Onboarding van nieuwe ontwikkelaars in de configuratieworkflow
0
Handmatige drush cim-runs op productie in 18 maanden
100%
ConfiguratieΒ­wijzigingen herleidbaar tot een Git-commit en MR

Naast de cijfers is vooral de culturele verandering van belang. Config-discussies verleggen zich van Slack naar Git-geschiedenis. In plaats van "heeft iemand de image styles op staging aangepast?", wordt de vraag "is daar een commit voor?" β€” en die is er altijd, anders heeft de wijziging niet plaatsgevonden.

Nieuwe ontwikkelaars hoeven niet langer de ongeschreven regels te leren over "welke omgeving op dit moment leidend is." Het antwoord is altijd Git. De pipeline handhaaft dit.

15. Eindaanbevelingen

  • Begin strikt en versoepel alleen met goede redenen. Het is eenvoudiger om een regel te versoepelen die overbodig blijkt dan om er een in te voeren die er nooit was.
  • Behandel elke config-diff als een buildfout. Geen uitzonderingen, geen "we lossen het na de release op."
  • CI is de bron van waarheid voor wat wordt gedeployed β€” niet de laptop van een ontwikkelaar, niet een staging-omgeving, niet een Slack-beslissing.
  • De updb β†’ cim β†’ cex β†’ diff-stappenreeks is onaantastbaar. Als je een stap overslaat, werk je blind.
  • Gebruik config_split voor echte omgevingsverschillen. Gebruik settings.php-voorwaarden niet als snelkoppeling.
  • Schakel config_readonly in op alle gedeelde omgevingen. Maak onbedoelde UI-wijzigingen onmogelijk, niet alleen ongewenst.
  • Sla mislukte config-exports op als CI-artifacts. Ontwikkelaars moeten exact kunnen zien wat is afgeweken, niet alleen dat er een diff bestond.
  • Rollback via Git, nooit via database-ingrepen. De pipeline is in beide richtingen de veiligste route.
Deze aanpak schaalt. Of je nu een solo-ontwikkelaar bent op één site of een bureau dat vijftig Drupal-installaties beheert, de regels blijven hetzelfde. De pipeline geeft niets om teamgrootte of releasedruk. Hij faalt of slaagt. Die consistentie is precies het doel.

Zowel Jenkins als GitLab CI zijn uitstekend geschikt om deze discipline af te dwingen. GitLab CI vereist minder infrastructuur en biedt betere native ondersteuning voor artifacts; Jenkins biedt meer flexibiliteit voor complexe enterprise-omgevingen en uitstekende shared library-ondersteuning om pipelines te standaardiseren over meerdere projecten heen. Kies de tool die het beste bij je organisatie past β€” de principes in dit artikel blijven hetzelfde.

Het doel was nooit om configuratiebeheer complex te maken. Het doel is om het saai te maken: een routinematige stap die altijd werkt, waar niemand over hoeft na te denken en die nooit een incident om 23:00 uur veroorzaakt. Met een correct ingerichte CI-pipeline is saai precies wat je krijgt.

Technische en architecturale vragen
Ivan Abramenko, Principal Drupal Architect
ivan.abramenko@drupalbook.org
Projectaanvragen
projects@drupalbook.org