logo

Paleta - Deixe colorido🎨

Palette — Construtor visual de páginas. Não precisa ser designer.

Demonstração ao vivo Baixar Palette

Scroll

Gestão de configuração do Drupal baseada em CI com Jenkins e GitLab CI

16/04/2026, by Ivan

1. Por que a gestão de configuração baseada em CI importa

O sistema de configuração do Drupal é um dos pontos mais fortes da plataforma e, ao mesmo tempo, uma das fontes mais confiáveis de dor de cabeça. A capacidade de exportar e importar cada item de configuração do site na forma de arquivos YAML é incrivelmente poderosa — mas só se todos concordarem sobre quem exatamente é responsável por mover esses arquivos entre os ambientes. Na maioria das equipes, esse acordo simplesmente não existe.

Os problemas clássicos são bem conhecidos de qualquer um que já tenha operado um site Drupal:

  • Deriva de configuração (config drift) — o staging diverge do production, o production diverge do ambiente local, e ninguém tem certeza de qual deles é a referência.
  • «Funciona no staging, mas não no prod» — porque alguém atualizou uma view ou um formatador de campo no staging e nunca exportou as mudanças.
  • O drush cim manual que quebra o conteúdo — uma importação apressada às 23h que removeu um campo de tipo de conteúdo ainda usado por nós ao vivo.

A causa raiz de todos esses cenários é a mesma: um humano decide quando, e se, promover a configuração. As pessoas esquecem. Pulam etapas sob pressão. Tomam decisões que se mostram erradas.

O CI não esquece. O pipeline ou passa ou falha. Ele não tem uma reunião para correr. Não sabe que há um release em vinte minutos. É exatamente esse determinismo que a gestão de configuração precisa.

As promessas que este artigo cumpre:

  • Toda mudança de configuração é commitada no Git antes de chegar a qualquer ambiente compartilhado.
  • A validação e a importação da configuração são responsabilidade do pipeline, não do desenvolvedor.
  • A promoção entre ambientes não exige nenhuma etapa manual.
  • A deriva de configuração é um erro de build, e não uma mensagem no Slack.
Premissas: você usa Drupal 10 ou 11, um workflow orientado a Git com feature branches e faz deploy em pelo menos dois ambientes compartilhados (por exemplo, staging e production). A equipe usa Jenkins ou GitLab CI — ou ambos.

2. Princípios básicos que aprendemos em projetos reais

Configuração é código

Se algo muda o comportamento do site, seu lugar é no Git. Ponto final. Uma view, um tipo de conteúdo, um ajuste de desempenho, um estilo de imagem — tudo isso é código. Trate os arquivos de configuração com a mesma disciplina dos arquivos PHP: faça revisão, versione, nunca os edite diretamente em um ambiente compartilhado.

Nenhum drush cim manual em ambientes compartilhados

Quem importa a configuração é o pipeline. Não os desenvolvedores. Essa regra soa radical até você presenciar, pela primeira vez, alguém executar drush cim em produção com mudanças locais não commitadas no diretório de trabalho.

Os pipelines devem falhar diante de qualquer deriva de configuração

Importar a configuração e, em seguida, exportá-la imediatamente não deve produzir nenhuma diferença. Se houver diferença — o build falha. Essa única regra pega mais bugs do que todas as outras verificações que adicionamos.

Um desenvolvedor sênior estava depurando uma página de busca lenta em produção. Ele ajustou as configurações de índice do Search API pela UI, confirmou que o desempenho melhorou e fechou a tarefa. Duas semanas depois, um release importou a configuração do Git, sobrescrevendo suas mudanças de UI — e o desempenho da busca despencou. Por três dias ninguém ligou os eventos. Depois desse incidente, fixamos claramente a regra: qualquer mudança de UI que não tenha sido imediatamente exportada e commitada é considerada perdida.
Em um site com grande volume de mídia, o arquivo core.extension.yml foi excluído das exportações porque «os módulos são gerenciados pelo Composer mesmo». Três meses depois, um hotfix reativou um módulo que havia sido intencionalmente desativado em produção. O hotfix estava correto — mas a importação de configuração seguinte silenciosamente desativou o módulo de novo. Foi exatamente após esse caso que passamos a chamar o core.extension.yml de o arquivo mais perigoso que se pode ignorar.

3. Arquitetura de alto nível

👨‍💻 Desenvolvedor
drush cex
📁 Repositório Git
config/sync
⚙ Pipeline de CI
validação + importação
🌐 Dev
🌐 Stage
🌐 Prod

O fluxo de configuração se move em uma única direção: da exportação local do desenvolvedor, passando pelo Git, pelo pipeline de CI e, então, para cada ambiente.

A ideia central é a separação de responsabilidades:

ParticipanteResponsabilidadeNunca é responsável por
DesenvolvedorExportar a configuração, commitar no Git, criar MR/PRImportar a configuração em qualquer ambiente compartilhado
Pipeline de CIValidar, importar, verificar, promoverGerar ou editar arquivos de configuração
AmbienteExecutar o siteGerar, armazenar ou exportar configuração

Os ambientes são consumidores de configuração, e não suas fontes. No momento em que um ambiente se torna a fonte da verdade para a configuração, a deriva se torna inevitável.

4. Estrutura de repositório escalável

Estrutura de diretórios raiz do projeto
project-root/
├── composer.json
├── composer.lock
├── Jenkinsfile
├── .gitlab-ci.yml
│
├── ci/
│   ├── drupal-config-check.sh   # Script de validação compartilhado
│   ├── drupal-deploy.sh
│   └── drupal-install.sh
│
├── config/
│   ├── sync/                        # A configuração fica aqui — commitada no Git
│   │   ├── core.extension.yml
│   │   ├── system.site.yml
│   │   └── ...
│   └── splits/                      # Sobrescritas de config_split por ambiente
│       ├── development/
│       ├── staging/
│       └── production/
│
└── web/
    ├── sites/
    │   └── default/
    │       ├── settings.php          # Commitado, sem segredos
    │       ├── settings.local.php    # Ignorado pelo Git, sobrescritas locais
    │       └── settings.env.php      # Carregado pelo CI a partir de variáveis de ambiente
    └── ...

O que fica em config/sync

Todos os arquivos YAML de configuração gerados pelo comando drush cex. Esse diretório é a única fonte da verdade para a configuração do site. Ele é commitado, revisado e implantado como qualquer outro código.

O que nunca deve entrar na configuração

  • Nomes de host, chaves de API ou credenciais específicas de ambiente — use variáveis de ambiente e settings.env.php.
  • Segredos de qualquer tipo — injete-os por meio do cofre de segredos do CI (Jenkins Credentials ou GitLab CI Variables).
  • Conteúdo — não use o módulo Default Content como gambiarra onde deveria haver configuração.
PHP web/sites/default/settings.php
// settings.php — commitado no Git, independente de ambiente
$settings['config_sync_directory'] = DRUPAL_ROOT . '/../config/sync';

// Carrega valores específicos do ambiente, injetados pelo CI ou pela hospedagem.
if (file_exists($app_root . '/' . $site_path . '/settings.env.php')) {
  include $app_root . '/' . $site_path . '/settings.env.php';
}

// Carrega sobrescritas locais opcionais (ignoradas pelo 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
// Gerado pelo CI a partir de variáveis de ambiente — nunca é commitado.
$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');

// Informa ao config_split em qual ambiente estamos.
$config['config_split.config_split.production']['status'] =
  (getenv('APP_ENV') === 'production');

5. Fluxo de trabalho de configuração do Drupal

5.1 Desenvolvimento local

O fluxo local é o único lugar onde o ciclo de feedback entre as mudanças na UI e a exportação da configuração deve ser rápido e habitual. Cada desenvolvedor do projeto segue as mesmas regras:

  • Fazer mudanças na UI ou no código localmente.
  • Executar drush cex imediatamente — não no fim do dia.
  • Revisar o diff com git diff config/sync.
  • Commitar as mudanças ou descartá-las com git checkout -- config/sync.
  • Não existe um estado intermediário.
Shell Workflow de desenvolvimento local
# Depois de fazer mudanças de configuração pela UI do Drupal ou por install hooks:
drush cex --yes

# Revise as mudanças — trate isso como a revisão de qualquer código:
git diff config/sync

# Adicione ao staging e commite junto com o código da feature:
git add config/sync
git commit -m "feat(search): add fulltext search API index config"

# Ou descarte, se a mudança foi experimental e ainda não está pronta:
git checkout -- config/sync

Reforçamos isso ainda com um Git pre-commit hook leve que avisa (mas não bloqueia) quando arquivos PHP ou de template são adicionados ao staging sem as mudanças correspondentes em config/sync:

Shell .git/hooks/pre-commit
#!/bin/bash
# Avisar se o código de módulo/tema mudou, mas config/sync não.
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 Feature branches

A configuração vive ao lado do código que depende dela — na mesma branch e no mesmo pull/merge request. Uma feature que adiciona um novo tipo de conteúdo deve incluir tanto o PHP install hook (se houver) quanto os arquivos YAML desse tipo de conteúdo no mesmo commit.

Detecção precoce de dependências quebradas: se a branch A adiciona um novo campo e a branch B altera a exibição do mesmo campo, então mesclar B antes de A causará um erro de importação de configuração. Esse é exatamente o resultado desejado — deixe o pipeline pegar isso na branch, e não em produção.

6. Responsabilidades do CI: o que o pipeline deve garantir

Todo pipeline que trabalha com um ambiente Drupal compartilhado deve executar estas etapas na ordem:

  1. Instalar ou restaurar o banco de dados — instalação limpa ou snapshot anonimizado de produção.
  2. Executar as atualizações do banco de dadosdrush updb.
  3. Importar a configuraçãodrush cim --yes.
  4. Reexportar a configuraçãodrush cex --yes.
  5. Garantir a ausência de diff — se qualquer arquivo YAML mudou, o build deve falhar.

As etapas 4 e 5 são as mais importantes. Importar e, em seguida, reexportar imediatamente não deve produzir nenhuma diferença. Se houver diferenças, uma das seguintes coisas é verdadeira:

  • Um módulo gera configuração durante a importação (muitas vezes é um bug no módulo).
  • O UUID da entidade de configuração não coincide com o que está no banco de dados.
  • O schema da configuração está incompleto, fazendo o Drupal normalizar os valores de forma diferente da exportação.
  • Um desenvolvedor editou manualmente os arquivos de configuração e introduziu inconsistências.
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."
Por que reexportar após a importação é obrigatório: sem a etapa de reexportação, você só sabe que a importação foi bem-sucedida. Você não sabe se a configuração importada coincide com o que o Drupal realmente salvou internamente. Só a reexportação e a comparação do diff fecham essa lacuna.
 

7. Implementação no Jenkins (testada em batalha)

7.1 Estrutura do Jenkinsfile

Usamos exclusivamente o declarative pipeline. O scripted pipeline oferece mais flexibilidade, mas o declarative pipeline é mais fácil de ler, mais fácil de validar com jenkins-cli declarative-linter e mais fácil de entender para novos membros da equipe.


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') // segredo do 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 {
        // Geração do settings.env.php a partir das credentials/variáveis de ambiente do 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: "Config validation failed. See: ${BUILD_URL}",
        recipientProviders: [[$class: 'DevelopersRecipientProvider']]
      )
    }
  }
}

7.2 Particularidades do Jenkins

Shared Libraries

Ao gerenciar mais de dois ou três sites Drupal, extraia a lógica do pipeline Drupal para uma Jenkins Shared Library. Assim, o Jenkinsfile de cada projeto se torna um invólucro fino:

Groovy Jenkinsfile (invólucro fino usando shared library)
@Library('drupal-pipeline-lib@v2') _

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

Limpeza do workspace

Sempre chame cleanWs() no bloco post { always }. Os agentes do Jenkins acumulam estado de builds anteriores — um settings.env.php remanescente ou um diretório vendor de um build anterior podem afetar silenciosamente o build atual. Seja rigoroso: limpe o workspace.

Nunca reutilize o banco de dados entre execuções do pipeline, a menos que você esteja testando especificamente os caminhos de atualização. Um banco de dados remanescente de um build anterior pode ocultar incompatibilidades de UUID, migrações ausentes e problemas na ordem de instalação dos módulos.

8. Implementação no GitLab CI

8.1 Estrutura do .gitlab-ci.yml

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

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

# ── Variáveis compartilhadas ─────────────────────────────────────
variables:
  COMPOSER_CACHE_DIR: "$CI_PROJECT_DIR/.cache/composer"
  APP_ENV: "ci"
  MYSQL_DATABASE: "drupal_ci"
  MYSQL_ROOT_PASSWORD: "root"

# ── Cache reutilizável ───────────────────────────────────────────
.cache-composer: &cache-composer
  cache:
    key:
      files: [composer.lock]
    paths:
      - .cache/composer
      - vendor/
    policy: pull

# ── Estágio de build ────────────────────────────────────────────
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

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

# ── Estágio de validação da configuração ────────────────────────
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

# ── Estágio de deploy ───────────────────────────────────────────
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   # Promoção em um clique, exigindo confirmação manual
  script:
    - bash ci/drupal-deploy.sh production

8.2 Vantagens do GitLab CI

O GitLab CI tem uma série de recursos que deixam esse workflow especialmente limpo:

  • Artefatos completos — exportações de configuração que falham são salvas automaticamente e ficam disponíveis na interface do MR sem configuração adicional.
  • Cache nativo por composer.lock — as instalações subsequentes do Composer ficam rápidas.
  • Visibilidade do pipeline no Merge Request — os desenvolvedores veem o status da validação da configuração direto no MR, antes do merge.
  • Bloco Services — o MySQL é executado como sidecar, sem infraestrutura adicional.
  • Environments + manual gates — a regra when: manual no deploy em produção proporciona promoção em um clique, com verificação humana, sem um approval workflow à parte.
Shell ci/drupal-deploy.sh
#!/bin/bash
set -euo pipefail

TARGET_ENV="${1:-staging}"
echo "=== Deploy para: ${TARGET_ENV} ==="

# Sincronização do artefato construído com o ambiente de destino (rsync, SSH, S3 — adapte à sua hospedagem)
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 "=== Executando comandos pós-deploy em ${TARGET_ENV} ==="
ssh "deploy@${TARGET_ENV}.example.com" bash -s <<'REMOTE'
  set -e
  cd /var/www/drupal
  drush updb --yes
  drush cim --yes
  drush cex --yes
  # Sanity-check final no próprio ambiente de destino
  git diff --exit-code config/sync || (echo "❌ Config drift on target!"; exit 1)
  drush cr
  echo "✅ Deploy concluído."
REMOTE

9. Configuração específica de ambiente — sem trapaças

Nem todo valor de configuração precisa ser igual em todos os ambientes. Backends de busca, nível de logging, camadas de cache e endpoints de APIs de terceiros diferem por bons motivos. A questão é como contemplar essas diferenças sem comprometer a integridade do seu pipeline de configuração.

As ferramentas certas: config_split e config_ignore

O config_split permite definir conjuntos de configuração ativos apenas em determinados ambientes. Cada split vive em seu próprio diretório e é ativado via settings.php ou settings.env.php com base em uma variável de ambiente.

YAML config/sync/config_split.config_split.development.yml
langcode: en
status: true
id: development
label: Development
description: 'Configuração ativa apenas em ambientes locais/dev'
folder: '../config/splits/development'
module:
  devel: 0
  kint: 0
  dblog: 0
theme: {  }
blacklist: {  }
graylist: {  }
PHP web/sites/default/settings.env.php (ativação do ambiente)
$app_env = getenv('APP_ENV') ?: 'production';

// Ativa o config_split correspondente conforme o ambiente.
$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');

Exemplos de split de projetos reais

Item de configuraçãoDev splitStage splitProd split
Backend do Search APIBackend de banco de dadosSolr (pequeno)Solr (cluster de prod)
Log de errosdblog, modo detalhadosyslogsyslog + APM externo
Módulos de desempenhoDesativadosAtivadosAtivados + configuração de CDN
Transporte de e-mailMailhog / null mailerMailpitSMTP / SendGrid

Abordagens incorretas — não faça assim:

  • Editar a configuração diretamente no banco de dados de produção.
  • Usar condições if ($settings['environment'] === 'prod') dentro do settings.php para substituir valores de configuração — isso ignora totalmente o sistema de configuração e cria divergências invisíveis em tempo de execução.
  • Manter branches Git separadas para cada ambiente com arquivos de configuração diferentes.

10. Promoção entre ambientes

Promoção não é deploy. O deploy leva código e configuração para um ambiente. A promoção leva um artefato validado — algo que já passou pelo CI — para o próximo ambiente da cadeia.

Na prática, isso significa que o mesmo Git SHA que foi verificado no dev acaba chegando à produção. Nenhum commit de última hora. Nenhum hotfix que contorna o CI. Nenhum cherry-pick que pula a verificação de configuração.

✅ O CI verifica o SHA
na feature branch / main
🌐 Dev
deploy automático no merge
🌐 Stage
deploy automático a partir de develop
🌐 Prod
manual gate na main

O mesmo artefato validado passa por todos os ambientes. O CI roda uma vez; seu resultado se propaga adiante.

A filosofia de builds imutáveis significa: o que passou na verificação do CI é o que será implantado. Se um hotfix for realmente necessário, ele passa por uma branch fast-track própria, com sua própria execução de CI — ele não deve contornar a validação.

Prevenção de hotfixes de última hora que contornam a verificação de configuração: proteja a branch main com uma verificação obrigatória de status do pipeline. Os "Protected branches" do GitLab e o plugin "GitHub Branch Source" do Jenkins dão suporte a isso. Se o pipeline não passar, o deploy em produção é impossível.

11. Tratamento de casos fora do padrão (com os quais você inevitavelmente vai se deparar)

Incompatibilidades de UUID

As entidades de configuração contêm UUIDs. Se você instala um site novo e depois tenta importar a configuração de outra instalação, o Drupal se recusa a fazer a importação por causa de um erro de incompatibilidade de UUID. A solução é sempre instalar com a flag --existing-config ou definir o UUID do site após a instalação:

Shell
# Lê o UUID da configuração commitada e o aplica à nova instalação:
SITE_UUID=$(grep "^uuid:" config/sync/system.site.yml | awk '{print $2}')
drush config-set "system.site" uuid "$SITE_UUID" --yes
drush cim --yes

Ordem de ativação e desativação de módulos

Ao ativar um novo módulo via configuração, o Drupal respeita automaticamente a ordem das dependências durante a execução do drush cim. Porém, se o install hook do módulo gera uma configuração padrão que conflita com o que está em config/sync, pode ser necessário remover essa configuração padrão e refazer a importação. O pipeline detectará isso como deriva de configuração.

Configuração que depende de conteúdo

Alguns itens de configuração fazem referência a conteúdo — por exemplo, um bloco que aponta para um item de menu por ID, ou uma view que filtra dados por um termo de taxonomia. Essas referências podem diferir entre ambientes se o conteúdo foi criado de forma distinta. A solução é gerenciar esse conteúdo por meio de migrações ou de módulos de default content, e não por configuração.

Particularidades da configuração multisite

Em uma configuração multisite, cada site tem seu próprio diretório de sincronização de configuração. O pipeline de CI deve executar a verificação de configuração para cada site, e não apenas para o principal. Use um loop:

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 "❌ Deriva de configuração no site: $SITE"
    exit 1
  fi
done
echo "✅ Todos os sites sem divergências."

Atualização do núcleo do Drupal com mudanças de configuração

As atualizações do núcleo às vezes incluem mudanças de schema que afetam a configuração existente. Sempre execute drush updb antes do drush cim e rode drush cex depois, para capturar quaisquer mudanças normalizadas pelo schema. Commite essas mudanças na mesma branch em que ocorre a atualização do núcleo — não deixe o pipeline detectá-las de surpresa.

12. Mecanismos operacionais de segurança

Configuração «somente leitura» em produção

Considere ativar o módulo Config Readonly em produção. Ele impede qualquer mudança de configuração pela UI administrativa — uma restrição rígida que reforça a regra de «nenhuma mudança manual» no nível da aplicação.

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

Verificação pós-deploy

ShellVerificações pós-deploy
# Garantir que não sobraram mudanças de configuração pendentes após o deploy:
drush config-status 2>&1 | grep -v 'No differences' && { \
  echo "❌ Diferenças de configuração inesperadas detectadas após o deploy"; exit 1; \
} || echo "✅ Configuração sem divergências"

# Verificação de aquecimento do cache:
drush cr
drush php-eval "echo \Drupal::state()->get('system.cron_last');"

Estratégia de rollback

O rollback é uma operação de Git, e não de banco de dados. Se um deploy causou um problema:

  1. Identifique o último commit SHA estável.
  2. Execute o pipeline para esse SHA.
  3. O pipeline reimplanta o artefato previamente validado.

Editar manualmente o banco de dados para desfazer uma importação de configuração nunca é a solução. Isso contorna todos os mecanismos de segurança e normalmente cria mais deriva do que corrige.

Auditoria: quem mudou a configuração, quando e por quê

Como toda mudança de configuração passa pelo Git, o seu log de auditoria é o histórico do Git. Use mensagens de commit significativas e imponha-as por meio de um commit-msg hook ou de uma etapa de lint no CI:

ShellExemplo de histórico de commits
$ 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. Erros comuns que não cometemos mais

ErroPor que é ruimSolução
Executar drush cim manualmente no staging ou prodImporta um estado desconhecido — potencialmente incluindo mudanças locais não commitadasApenas o pipeline importa a configuração em ambientes compartilhados
Permitir mudanças pela UI no stagingO staging vira fonte da verdade, criando divergências com o GitAtivar o config_readonly em todos os ambientes compartilhados
Esperar que os desenvolvedores «não esqueçam de exportar»Sob pressão, eles vão esquecer com certezaPre-commit hooks avisam; a falha do CI obriga
Ignorar a deriva de configuraçãoDivergências sutis se acumulam até uma quebra críticaO trio drush cim → cex → git diff --exit-code é obrigatório
Branches separadas por ambiente com configuração diferenteO merge vira um pesadelo de conflitos; falta uma única fonte da verdadeUma única branch de configuração, dividida por config_split
Pular o drush updb antes do drush cimAtualizações de schema podem causar erros de importação ou perda silenciosa de dadosSempre: updb → cim → cex → diff

14. Resultados após adotar essa abordagem

Após adotar essa abordagem em um portfólio de sites Drupal — de um único site pequeno a uma agência que gerencia dezenas de instalações — os resultados foram consistentes:

~80%
Redução no número de incidentes de deploy relacionados à configuração
< 1 dia
Onboarding de um novo desenvolvedor no workflow de configuração
0
Execuções manuais de drush cim em produção em 18 meses
100%
Das mudanças de configuração rastreáveis até um commit e MR no Git

Além dos números, o mais importante foi a mudança cultural. As discussões sobre configuração migraram do Slack para o histórico do Git. Em vez de «alguém mudou os estilos de imagem no staging?», a pergunta passou a ser: «Existe um commit para isso?» — e sempre existe, ou a mudança não aconteceu.

Os novos desenvolvedores não precisam mais decifrar regras tácitas do tipo «qual ambiente é o canônico agora». A resposta é sempre uma só — o Git. E o pipeline garante isso.

15. Recomendações finais

  • Comece rigoroso e relaxe apenas quando necessário. É mais fácil afrouxar uma regra desnecessária do que endurecer uma que nunca existiu.
  • Trate qualquer diff de configuração como falha de build. Sem exceções e sem «a gente corrige depois do release».
  • O CI é a fonte da verdade para o deploy, e não o notebook do desenvolvedor, o ambiente de staging ou uma decisão no Slack.
  • A sequência updb → cim → cex → diff é intocável. Se você pular até mesmo uma etapa — está agindo às cegas.
  • Use o config_split para diferenças reais entre ambientes. Não use condições no settings.php como atalho.
  • Ative o config_readonly em todos os ambientes compartilhados. Torne as mudanças acidentais pela UI impossíveis, e não apenas indesejadas.
  • Salve as exportações de configuração que falham como artefatos de CI. Os desenvolvedores precisam ver o que exatamente causou a deriva, e não apenas o fato de ela existir.
  • O rollback é feito via Git, e não por «operações» no banco de dados. O pipeline é o caminho mais seguro nos dois sentidos.
Essa abordagem escala. Independentemente de você ser o único desenvolvedor ou uma agência que gerencia cinquenta instalações Drupal, as regras permanecem as mesmas. O pipeline não se importa com o tamanho da equipe nem com a pressão dos prazos. Ele ou passa ou falha. É esse o sentido.

Tanto o Jenkins quanto o GitLab CI dão conta igualmente bem de impor essa disciplina. O GitLab CI exige menos infraestrutura e tem melhor suporte nativo a artefatos; o Jenkins oferece mais flexibilidade para ambientes corporativos complexos e excelente suporte a shared libraries para padronizar pipelines entre projetos. Escolha a ferramenta adequada à sua organização — os princípios deste artigo se aplicam em qualquer caso.

O objetivo nunca foi tornar a gestão de configuração mais complicada. O objetivo é torná-la rotineira: uma etapa previsível que sempre funciona, em que ninguém pensa e que nunca leva a incidentes às 23h. Com um pipeline de CI corretamente implementado, é exatamente isso que você obtém.

Questões técnicas e de arquitetura
Ivan Abramenko, Principal Drupal Architect
ivan.abramenko@drupalbook.org
Questões sobre projetos
projects@drupalbook.org