Gestión de la configuración de Drupal impulsada por CI utilizando Jenkins y GitLab CI
1. Por qué importa la gestión de configuración impulsada por CI
El sistema de configuración de Drupal es una de las mayores fortalezas de la plataforma — y también una de sus fuentes más confiables de dolor. La capacidad de exportar e importar cada pieza de la configuración del sitio como archivos YAML es poderosa, pero solo si todos están de acuerdo sobre quién es responsable de mover esos archivos entre entornos. En la mayoría de los equipos, ese acuerdo nunca termina de existir.
Los problemas clásicos son bien conocidos por cualquiera que haya puesto un sitio Drupal en producción:
- Deriva de configuración — staging diverge de producción, producción diverge del entorno local, y nadie está seguro de qué entorno es el canónico.
- «Funciona en staging pero no en producción» — porque alguien actualizó una vista o un formateador de campos en staging y nunca lo exportó.
- Un
drush cimmanual que rompe contenido — una importación apresurada a las 11 de la noche que eliminó un campo de tipo de contenido que todavía era referenciado por nodos activos.
La causa raíz de todos estos escenarios es la misma: un ser humano decide cuándo y si la configuración se promueve. Los humanos olvidan. Se saltan pasos bajo presión. Toman decisiones que luego resultan erróneas.
CI no olvida. Un pipeline pasa o falla. No tiene una reunión diaria a la que llegar. No sabe que el lanzamiento es dentro de veinte minutos. Ese determinismo es exactamente lo que necesita la gestión de configuración.
La promesa que cumple este artículo:
- Cada cambio de configuración se comitea en Git antes de tocar cualquier entorno compartido.
- El pipeline, y no un desarrollador, es responsable de validar e importar la configuración.
- La promoción entre entornos no requiere pasos manuales.
- La deriva de configuración es un fallo de build, no un mensaje en Slack.
2. Principios fundamentales que aprendimos de proyectos reales
La configuración es código
Si cambia el comportamiento del sitio, pertenece a Git. Punto. Una vista, un tipo de contenido, una configuración de rendimiento, un estilo de imagen — todo es código. Trata los archivos de configuración con la misma disciplina que los archivos PHP: revísalos, versionálos, nunca los edites directamente en un entorno compartido.
Nada de drush cim manual en entornos compartidos
El pipeline importa la configuración. Los desarrolladores no. Esta regla suena extrema hasta que experimentas la primera vez que alguien ejecuta drush cim en producción con cambios locales sin commitear aún en su directorio de trabajo.
Los pipelines deben fallar rápido ante la deriva de configuración
Importar la configuración y luego exportarla inmediatamente no debería producir ninguna diferencia. Si la produce, el build falla. Esta única regla detecta más errores que cualquier otra verificación que hayamos añadido.
core.extension.yml fue excluido de las exportaciones porque «los módulos se gestionan con Composer de todos modos». Tres meses después, un hotfix volvió a habilitar un módulo que había sido deshabilitado deliberadamente en producción. El hotfix era correcto — pero una posterior importación de configuración volvió a deshabilitar el módulo silenciosamente. Ese incidente es la razón por la que ahora llamamos a core.extension.yml el archivo más peligroso que puedes ignorar.3. Arquitectura de alto nivel
drush cex
config/sync
validar + importar
La configuración fluye en una sola dirección: desde la exportación local del desarrollador, a través de Git, a través del pipeline de CI y hacia cada entorno.
La idea clave es la separación de responsabilidades:
| Actor | Responsabilidad | Nunca responsable de |
|---|---|---|
| Desarrollador | Exportar configuración, hacer commit en Git, abrir MR/PR | Importar configuración en cualquier entorno compartido |
| Pipeline de CI | Validar, importar, verificar, promover | Generar o editar archivos de configuración |
| Entorno | Ejecutar el sitio | Generar, almacenar o exportar configuración |
Los entornos son consumidores de configuración, nunca productores. En el momento en que un entorno se convierte en una fuente de verdad para la configuración, la deriva se vuelve inevitable.
4. Estructura de repositorio que escala
project-root/
├── composer.json
├── composer.lock
├── Jenkinsfile
├── .gitlab-ci.yml
│
├── ci/
│ ├── drupal-config-check.sh # Script de validación reutilizable
│ ├── drupal-deploy.sh
│ └── drupal-install.sh
│
├── config/
│ ├── sync/ # La configuración vive aquí — almacenada en Git
│ │ ├── core.extension.yml
│ │ ├── system.site.yml
│ │ └── ...
│ └── splits/ # Sobrescrituras de config_split por entorno
│ ├── development/
│ ├── staging/
│ └── production/
│
└── web/
├── sites/
│ └── default/
│ ├── settings.php # Versionado, sin secretos
│ ├── settings.local.php # Ignorado por Git, sobrescrituras locales
│ └── settings.env.php # Cargado por CI desde variables de entorno
└── ...
Qué vive en config/sync
Todos los archivos YAML de configuración generados por drush cex. Este directorio es la única fuente de verdad para la configuración del sitio. Se versiona, se revisa y se despliega como cualquier otro código.
Qué nunca debe incluirse en la configuración
- Hostnames específicos del entorno, claves API o credenciales — utiliza variables de entorno y
settings.env.php. - Secretos de cualquier tipo — inyéctalos mediante el almacén de secretos de tu CI (Jenkins Credentials o GitLab CI Variables).
- Contenido — no utilices el módulo Default Content como sustituto de lo que debería ser configuración.
// settings.php — almacenado en Git, independiente del entorno
$settings['config_sync_directory'] = DRUPAL_ROOT . '/../config/sync';
// Cargar valores específicos del entorno inyectados por CI o el host.
if (file_exists($app_root . '/' . $site_path . '/settings.env.php')) {
include $app_root . '/' . $site_path . '/settings.env.php';
}
// Cargar sobrescrituras locales opcionales (ignoradas por Git).
if (file_exists($app_root . '/' . $site_path . '/settings.local.php')) {
include $app_root . '/' . $site_path . '/settings.local.php';
}// Generado por CI a partir de variables de entorno — nunca versionado.
$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');
// Indica a config_split en qué entorno estamos.
$config['config_split.config_split.production']['status'] =
(getenv('APP_ENV') === 'production');5. Flujo de trabajo de configuración en Drupal
5.1 Desarrollo local
El flujo de trabajo local es el único lugar donde el ciclo de retroalimentación entre los cambios en la interfaz de usuario y la exportación de configuración debe ser rápido y habitual. Todos los desarrolladores del proyecto siguen las mismas reglas:
- Realizar cambios en la interfaz de usuario o en el código de forma local.
- Ejecutar
drush cexinmediatamente — no al final del día. - Revisar las diferencias con
git diff config/sync. - Confirmar los cambios o descartarlos con
git checkout -- config/sync. - No existe un estado intermedio.
# Después de realizar cambios de configuración mediante la interfaz de Drupal o hooks de instalación:
drush cex --yes
# Revisar qué cambió — trátalo como cualquier cambio de código:
git diff config/sync
# Añadir al staging y hacer commit junto con el código de la funcionalidad:
git add config/sync
git commit -m "feat(search): add fulltext search API index config"
# O descartar si el cambio fue exploratorio y aún no está listo:
git checkout -- config/syncReforzamos esto con un hook ligero de Git previo al commit que advierte (pero no bloquea) cuando se añaden archivos PHP o plantillas sin un cambio correspondiente en config/sync:
#!/bin/bash
# Advertir si el código de módulo/tema cambió pero config/sync no.
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 "⚠ Advertencia: se agregaron archivos PHP/módulo pero no se detectaron cambios en config/sync."
echo " ¿Olvidaste ejecutar: drush cex?"
echo " Continuando de todos modos — pero verifica."
fi
exit 05.2 Ramas de funcionalidad
La configuración vive junto al código que la requiere en la misma rama y en la misma pull/merge request. Una funcionalidad que añade un nuevo tipo de contenido debe incluir tanto el hook de instalación en PHP (si aplica) como los archivos YAML para ese tipo de contenido en el mismo commit.
6. Responsabilidades de CI: Lo que el pipeline debe imponer
Todo pipeline que interactúe con un entorno compartido de Drupal debe ejecutar estos pasos en el siguiente orden:
- Instalar o restaurar la base de datos — una instalación limpia o un snapshot de producción saneado.
- Ejecutar actualizaciones de la base de datos —
drush updb. - Importar la configuración —
drush cim --yes. - Reexportar la configuración —
drush cex --yes. - Verificar que no haya diferencias — si algún archivo YAML cambia, el build debe fallar.
Los pasos 4 y 5 juntos son los más importantes. Importar la configuración y luego reexportarla inmediatamente debe producir un diff vacío. Si no es así, significa que ocurre alguna de las siguientes situaciones:
- Un módulo está generando configuración durante la importación (a menudo un bug en el módulo).
- El UUID de una entidad de configuración no coincide con lo que hay en la base de datos.
- Un esquema de configuración está incompleto, lo que provoca que Drupal normalice los valores de forma distinta a como fueron exportados.
- Un desarrollador editó archivos de configuración manualmente e introdujo inconsistencias.
#!/bin/bash
set -euo pipefail
echo "=== Ejecutando actualizaciones de la base de datos de Drupal ==="
drush updb --yes
echo "=== Importando configuración desde config/sync ==="
drush cim --yes
echo "=== Reexportando para verificar que no haya cambios pendientes ==="
drush cex --yes
echo "=== Verificando deriva de configuración ==="
if ! git diff --exit-code config/sync; then
echo ""
echo "❌ ERROR: ¡Deriva de configuración detectada!"
echo " La configuración en Git no coincide con lo que Drupal generó tras la importación + exportación."
echo " El diff se muestra arriba. Corrige localmente con 'drush cex' y haz commit."
exit 1
fi
echo "✅ La configuración está limpia — no se detectó deriva."7. Implementación con Jenkins (Probada en entornos reales)
7.1 Estructura del Jenkinsfile
Utilizamos exclusivamente pipelines declarativos. Los pipelines basados en scripts ofrecen mayor flexibilidad, pero los declarativos son más fáciles de leer, más fáciles de validar con jenkins-cli declarative-linter y más fáciles de entender para los nuevos miembros del equipo.
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') // Secreto de 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 {
// Generar settings.env.php a partir de credenciales/variables de entorno de Jenkins
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: "La validación de configuración ha fallado. Ver: ${BUILD_URL}",
recipientProviders: [[$class: 'DevelopersRecipientProvider']]
)
}
}
}
7.2 Consideraciones específicas de Jenkins
Bibliotecas compartidas
Al gestionar más de dos o tres sitios Drupal, extrae la lógica del pipeline de Drupal a una Biblioteca Compartida de Jenkins. Entonces, el Jenkinsfile de cada proyecto se convierte en un envoltorio ligero:
@Library('drupal-pipeline-lib@v2') _
drupalPipeline(
phpVersion: '8.2',
deployBranch: 'main',
dbCredentials: 'drupal-ci-db',
slackChannel: '#deployments'
)Limpieza del espacio de trabajo
Llama siempre a cleanWs() en el bloque post { always }. Los agentes de Jenkins acumulan estado de builds anteriores — el settings.env.php o el directorio vendor de una build previa pueden influir silenciosamente en la build actual. Sé contundente: limpia el workspace.
8. Implementación de GitLab CI
8.1 Estructura de .gitlab-ci.yml
image: php:8.2-cli
stages:
- build
- test
- validate-config
- deploy
# ── Variables compartidas ─────────────────────────────────────────
variables:
COMPOSER_CACHE_DIR: "$CI_PROJECT_DIR/.cache/composer"
APP_ENV: "ci"
MYSQL_DATABASE: "drupal_ci"
MYSQL_ROOT_PASSWORD: "root"
# ── Caché reutilizable ─────────────────────────────────────────────
.cache-composer: &cache-composer
cache:
key:
files: [composer.lock]
paths:
- .cache/composer
- vendor/
policy: pull
# ── Fase de construcción ───────────────────────────────────────────
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
# ── Fase de pruebas ────────────────────────────────────────────────
phpunit:unit:
stage: test
<<: *cache-composer
script:
- ./vendor/bin/phpunit --testsuite=unit --log-junit=reports/phpunit.xml
artifacts:
reports:
junit: reports/phpunit.xml
# ── Fase de validación de configuración ───────────────────────────
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
# ── Fase de despliegue ─────────────────────────────────────────────
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 # Promoción con un clic (requiere aprobación humana)
script:
- bash ci/drupal-deploy.sh production8.2 Ventajas de GitLab CI
GitLab CI dispone de varias características que hacen que este flujo de trabajo sea especialmente limpio:
- Artefactos de primera categoría — Las exportaciones de configuración fallidas se almacenan automáticamente y pueden consultarse desde la interfaz de la Merge Request sin necesidad de configuración adicional.
- Caché nativa basada en
composer.lock— Las instalaciones de Composer son rápidas después de la primera ejecución. - Visibilidad del pipeline en las Merge Requests — Los desarrolladores pueden ver el estado de validación de la configuración directamente en la MR antes del merge.
- Bloque de servicios — MySQL se ejecuta como contenedor auxiliar sin sobrecarga de infraestructura.
- Entornos y aprobaciones manuales — La regla
when: manualen el despliegue a producción permite la promoción con un solo clic y un punto de control humano, sin necesidad de un flujo de aprobación independiente.
#!/bin/bash
set -euo pipefail
TARGET_ENV="${1:-staging}"
echo "=== Desplegando en: ${TARGET_ENV} ==="
# Sincronizar el artefacto generado con el entorno de destino (rsync, SSH, S3 — adaptar según el hosting)
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 "=== Ejecutando comandos posteriores al despliegue en ${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
# Verificación final directamente en el entorno de destino
git diff --exit-code config/sync || (echo "❌ Deriva de configuración en el entorno de destino"; exit 1)
drush cr
echo "✅ Despliegue completado."
REMOTE9. Configuración específica por entorno sin hacer trampas
No todos los valores de configuración deben ser iguales en todos los entornos. Los backends de búsqueda, la verbosidad del logging, las capas de caché y los endpoints de APIs de terceros difieren legítimamente. La cuestión es cómo manejar esas diferencias sin comprometer la integridad de tu pipeline de configuración.
Las herramientas correctas: config_split y config_ignore
config_split te permite definir conjuntos de configuración que están activos únicamente en entornos específicos. Cada split vive en su propio directorio y se activa mediante settings.php o settings.env.php según una variable de entorno.
langcode: en
status: true
id: development
label: Development
description: 'Configuración activa solo en entornos locales/dev'
folder: '../config/splits/development'
module:
devel: 0
kint: 0
dblog: 0
theme: { }
blacklist: { }
graylist: { }$app_env = getenv('APP_ENV') ?: 'production';
// Activar el config_split correcto según el entorno.
$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');Ejemplos reales de splits
| Elemento de configuración | Split Dev | Split Stage | Split Prod |
|---|---|---|---|
| Backend de Search API | Backend de base de datos | Solr (pequeño) | Solr (clúster de producción) |
| Registro de errores | dblog, verboso | syslog | syslog + APM externo |
| Módulos de rendimiento | Deshabilitados | Habilitados | Habilitados + configuración de CDN |
| Transporte de correo | Mailhog / null mailer | Mailpit | SMTP / SendGrid |
Enfoques incorrectos — no hagas esto:
- Editar la configuración directamente en la base de datos de producción.
- Usar condicionales como
if ($settings['environment'] === 'prod')dentro desettings.phppara cambiar valores de configuración — esto evita completamente el sistema de configuración y crea divergencias invisibles en tiempo de ejecución. - Mantener ramas Git separadas por entorno con distintos archivos de configuración.
10. Promoción entre entornos
La promoción no es despliegue. El despliegue mueve el código y la configuración a un entorno. La promoción mueve un artefacto validado — algo que ya ha pasado por CI — al siguiente entorno de la cadena.
En la práctica, esto significa que el mismo SHA de Git que fue validado en dev es el que llega a producción. Sin commits de último minuto. Sin hotfixes que omitan CI. Sin cherry-picks que eludan la verificación de configuración.
en rama de funcionalidad / main
auto-despliegue tras el merge
auto-despliegue desde develop
aprobación manual en main
El mismo artefacto validado viaja a través de los entornos. CI se ejecuta una sola vez; el resultado se propaga.
La filosofía de builds inmutables implica: lo que valida CI es lo que se despliega. Si un hotfix es realmente necesario, pasa por una rama de vía rápida con su propia ejecución de CI — no se omite la validación.
main con una comprobación obligatoria del estado del pipeline. Las "protected branches" de GitLab y el plugin "GitHub Branch Source" de Jenkins permiten hacerlo. Si el pipeline no ha pasado, la rama no puede desplegarse en producción.11. Manejo de casos límite (los encontrarás)
Desajustes de UUID
Las entidades de configuración tienen UUID. Si instalas un sitio limpio e intentas importar la configuración desde otra instalación, Drupal rechazará la operación con un error de desajuste de UUID. La solución es instalar siempre con --existing-config o establecer el UUID del sitio tras la instalación:
# Leer el UUID desde la configuración versionada y aplicarlo a la nueva instalación:
SITE_UUID=$(grep "^uuid:" config/sync/system.site.yml | awk '{print $2}')
drush config-set "system.site" uuid "$SITE_UUID" --yes
drush cim --yesOrden de habilitación/deshabilitación de módulos
Al habilitar un nuevo módulo mediante configuración, Drupal respeta automáticamente el orden de dependencias durante drush cim. Sin embargo, si el hook de instalación de un módulo genera configuración por defecto que entra en conflicto con lo que está en config/sync, puede que debas eliminar esa configuración por defecto e importarla nuevamente. El pipeline detectará esto como una deriva.
Configuración dependiente del contenido
Algunas configuraciones hacen referencia al contenido — por ejemplo, un bloque que referencia un elemento de menú por ID, o una vista que filtra por un término de taxonomía. Estas referencias pueden diferir entre entornos si el contenido se ha creado de manera distinta. La solución es gestionar ese contenido mediante migraciones o módulos de contenido por defecto, no mediante la configuración.
Particularidades de Multisite
En una configuración multisite, cada sitio tiene su propio directorio de sincronización de configuración. El pipeline de CI debe ejecutar la verificación de configuración para cada sitio, no solo para el principal. Usa un bucle:
#!/bin/bash
set -euo pipefail
for SITE_DIR in web/sites/*/; do
SITE=$(basename "$SITE_DIR")
[[ "$SITE" == "default" ]] && continue
[[ "$SITE" == "simpletest" ]] && continue
echo "--- Verificando configuración para el sitio: $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 "❌ Deriva de configuración en el sitio: $SITE"
exit 1
fi
done
echo "✅ Todos los sitios están limpios."Actualización de Drupal Core con cambios de configuración
Las actualizaciones de Core a veces incluyen cambios de esquema que afectan la configuración existente. Ejecuta siempre drush updb antes de drush cim y luego drush cex para capturar los cambios normalizados por el esquema. Realiza el commit de esos cambios como parte de la rama de actualización de Core — no esperes a que el pipeline los detecte por sorpresa.
12. Redes de seguridad operacionales
Configuración de solo lectura en producción
Considera habilitar el módulo Config Readonly en producción. Evita cualquier cambio de configuración desde la interfaz de administración — un bloqueo estricto que refuerza la regla de "sin cambios manuales" a nivel de aplicación.
if (getenv('APP_ENV') === 'production') {
$settings['config_readonly'] = TRUE;
}Verificación posterior al despliegue
# Verificar que no queden cambios de configuración pendientes tras el despliegue:
drush config-status 2>&1 | grep -v 'No differences' && { \
echo "❌ Diferencias de configuración inesperadas tras el despliegue"; exit 1; \
} || echo "✅ Estado de configuración limpio"
# Verificar que las cachés estén calientes:
drush cr
drush php-eval "echo \Drupal::state()->get('system.cron_last');"Estrategia de rollback
El rollback es una operación de Git, no de base de datos. Si un despliegue causa un problema:
- Identifica el último commit SHA correcto.
- Lanza una ejecución del pipeline sobre ese SHA.
- El pipeline vuelve a desplegar el artefacto previamente validado.
La manipulación manual de la base de datos para deshacer una importación de configuración nunca es la solución — evita todos los controles de seguridad y normalmente genera más deriva de la que corrige.
Auditoría: Quién cambió la configuración, cuándo y por qué
Como todo cambio de configuración pasa por Git, tu rastro de auditoría es el historial de Git. Utiliza mensajes de commit claros y aplícalos con un hook commit-msg o un paso de lint en 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. Errores comunes que ya no cometemos
| Error | Por qué es un problema | La solución |
|---|---|---|
Ejecutar drush cim manualmente en staging o producción | Importa un estado desconocido — posiblemente incluyendo cambios locales no commiteados | Solo el pipeline debe importar la configuración en entornos compartidos |
| Permitir cambios en la interfaz (UI) en staging | Staging se convierte en una fuente de verdad, creando divergencias respecto a Git | Habilitar config_readonly en todos los entornos compartidos |
| Confiar en que los desarrolladores "recordarán exportar" | No lo harán bajo presión | Los hooks pre-commit advierten; el fallo en CI lo impone |
| No fallar ante la deriva de configuración | La divergencia silenciosa se acumula hasta que algo falla de forma espectacular | La secuencia drush cim → cex → git diff --exit-code es obligatoria |
| Ramas separadas por entorno con configuración distinta | El merge se convierte en una pesadilla de conflictos; no existe una única fuente de verdad | Una sola rama de configuración, separada con config_split |
Omitir drush updb antes de drush cim | Las actualizaciones de esquema pueden causar fallos de importación o pérdida silenciosa de datos | Siempre: updb → cim → cex → diff |
14. Resultados tras aplicar este patrón
Tras aplicar este enfoque en una cartera de sitios Drupal — desde un único sitio pequeño hasta una agencia que gestiona decenas de instalaciones — los resultados han sido consistentes:
drush cim en producción en 18 mesesMás allá de los números, el cambio cultural es lo más importante. Las discusiones sobre configuración pasan de Slack al historial de Git. En lugar de "¿alguien cambió los estilos de imagen en staging?", la pregunta pasa a ser "¿existe un commit para eso?" — y siempre lo hay, o el cambio no ocurrió.
Los nuevos desarrolladores ya no necesitan aprender las reglas no escritas sobre "qué entorno es el canónico ahora mismo". La respuesta siempre es Git. El pipeline lo impone.
15. Recomendaciones finales
- Empieza siendo estricto y relaja solo con motivos. Es más fácil flexibilizar una regla innecesaria que endurecer una que nunca existió.
- Trata cada diff de configuración como un fallo de build. Sin excepciones, sin "lo arreglaremos después del release".
- CI es la fuente de verdad de lo que se despliega — no el portátil de un desarrollador, ni un entorno de staging, ni una decisión en Slack.
- La secuencia
updb → cim → cex → diffes inviolable. Si omites cualquier paso, estás trabajando a ciegas. - Usa
config_splitpara diferencias reales entre entornos. No utilices condicionales ensettings.phpcomo atajo. - Habilita config_readonly en todos los entornos compartidos. Haz que los cambios accidentales en la UI sean imposibles, no solo desaconsejados.
- Archiva las exportaciones de configuración fallidas como artefactos de CI. Los desarrolladores necesitan ver exactamente qué ha divergido, no solo que existe una diferencia.
- Haz rollback mediante Git, nunca mediante cirugía en la base de datos. El pipeline es la vía más segura en ambas direcciones.
Tanto Jenkins como GitLab CI son igualmente capaces de imponer esta disciplina. GitLab CI requiere menos infraestructura y dispone de mejor soporte nativo para artefactos; Jenkins ofrece mayor flexibilidad para entornos empresariales complejos y un excelente soporte de bibliotecas compartidas para estandarizar pipelines entre múltiples proyectos. Elige la herramienta que mejor se adapte a tu organización — los principios de este artículo se aplican independientemente de ello.
El objetivo nunca fue hacer que la gestión de configuración fuera complicada. El objetivo es hacerla aburrida: un paso rutinario que siempre funciona, en el que nadie tiene que pensar y que nunca provoca un incidente a las 11 de la noche. Con un pipeline de CI correctamente implementado, eso es exactamente lo que obtendrás.
Ivan Abramenko, Arquitecto Principal de Drupal
ivan.abramenko@drupalbook.org
projects@drupalbook.org