Drupal 8 Development Workflow

Präsentation bei der Drupal User Group München am 15.02.2017

Für einen Entwicklungsworkflow bauen wir 3 Drupal Instanzen auf.

  • Entwicklung/Development
  • Staging/Test
  • Produktiv/Live

Beispielswiese kann die Entwicklungsinstanz lokal sein und die Staging-/Testinstanz und die produktive- oder Liveinstanz auf einem öffentlich zugänglichen Webserver.

Staging-/Testinstanz

In der Entwicklungsinstanz/Development finden die Entwicklungen statt und werden dann zuerst in die Staging Instanz deployed. Wenn die Änderungen dort erfolgreich getestet wurden, kann der gleiche Prozess an der produktiven Instanz vollzogen werden. Wichtig ist, dass die Staging Instanz der produktiven Instanz so ähnlich wie möglich ist. Dann können wir ziemlich sicher sein, dass das Einspielen der Entwicklungen in die produktive Instanz fehlerfrei läuft.

 

Content Image
Workflow mit 3 Drupal Instanzen

Betrachten wir dies genauer im Drupal Kontext. In der Staging Instanz wird die produktive Datenbank kopiert. Zudem holen wir uns die zum Content dazugehörigen Dateien wie z.B. Bilder. Diese werden in Drupal bekanntlich im sites/default/files Verzeichnis abgelegt.

Von der Entwicklungsinstanz kommen die Einstellungen aus dessen Datenbank. Diese werden mit dem Configuration Management Tool von Drupal 8, mit dem Drushbefehl "drush config-export" exportert. Diese landen dann in yaml Dateien in dem /config Verzeichnis. Außerdem holen wir natürlich alle Codeänderungen in das Staging System. Die Yaml Datein mit allen Einstellungen und die Codeänderungen werden über das Versionierungstool Git deployed.

Wichtiger Hinweis zum Configuration Management

Das Drupal 8 Configuration Management versetzt Entities mit einem UUID. Dies ist ein Hashwert der für jedes Element einzigartig ist. Die Übertragung von einer Instanz zu einer anderen funktioniert nur, wenn die UUID's übereinstimmen. Daher ist es nötig die Instanzen mit der gleichen Datenbank aufzubauen. Ich gibt öfter Probleme, dass das Importieren der Konfigurationen abgelehnt wird, weil die UUID's nicht übereinstimmen. Seit ich bei der Neuinstallation eines Projekts immer die nagelneue Datenbank sofort kopiere und unverändert in die anderen Instanzen importiere, hatte ich keine Probleme mehr.

Content Image
Workflow mit 3 Drupal Instanzen

Composer Workflow

Betrachten wir nun den Composer Workflow isoliert. Zuerst ist es wichtig, den Unterschied zwischen den Dateien composer.json und composer.lock, sowie den Befehlen "composer update" und "composer install" zu verstehen.

composer.json

In der composer.json Datei wird der Versionsbereich angegeben. Beispielsweise "^1.0" schließt alle Versionen eine, die kleiner als 2.0 sind.

    "require": {
        "drupal/paragraphs": "^1.0",
    },

Beim ausführen von "composer update" wird die aktuell neuste Version zwischen 1.0 und alles was noch kleiner als 2.0 ist, heruntergeladen.

Hier findet man die Versionsbefehle: https://getcomposer.org/doc/articles/versions.md

composer.lock

In der composer.lock Datei wird der aktuelle Stand der exakten Versionen gespeichert. Betrachten wir das Paragraphs Modul in der Datei unterhalb, dann sehen wir, dass sich aktuell die Version 1.0.0 im System befindet.

Mit dem Befehl "composer install" wird das System mit der composer.lock Datei syncronisiert. Es sind dann also exakt die Versionen, die in der composer.lock Datei aufgeführt sind im System.

        {
            "name": "drupal/paragraphs",
            "version": "1.0.0",
            "source": {
                "type": "git",
                "url": "https://git.drupal.org/project/paragraphs",
                "reference": "8.x-1.0"
            },
            "dist": {
                "type": "zip",
                "url": "https://ftp.drupal.org/files/projects/paragraphs-8.x-1.0.zip",
                "reference": "8.x-1.0",
                "shasum": "4fcbd6a50f5b725a9ba66ee105d6f8a1385ff2c8"
            }

Composer Workflow in den 3 Instanzen

Neue Module hinzufügen

Neue Module werden beispielsweise mit dem Befehl "composer require drupal/projectname" eingetragen. Dies entspricht dem manuellen Eintrag in die compser.json Datei und dem anschließenden Ausführen von "composer update". Hiermit wird die aktuellste Version in dem angegebenen Versionsbereich heruntergeladen.

Update von bestehenden Modulen und dem Drupal core

Mit dem Befehl "composer update" werden alle Module, Libraries und der Drupal Core auf den neusten Stand gebracht. Dies kann natürlich unvorhergesehene Probleme bereiten, da dies viele Codeänderungen mit sich bringen kann. Daher ist es sehr wichtig den Befehlt "composer update" nie auf dem produktiven System auszuführen.

Deployment der composer.lock Datei

Wir übertragen auf das Testsystem und das produktive System die composer.lock Datei. Hier sind exakt die Versionen dokumentiert, die wir in der Entwicklungsinstanz getestet haben. Dann führen in der produktiven Instanz nur "composer install" aus. Hiermit befinden sich das produktive System exakt auf dem gleichen Stand wie das Entwicklungssystem.

# Add new modules on development
$ composer require drupal/projectname
# Update on development
$ composer update

# Production
$ composer install

Composer Template Installation

Ich verwende für die Drupal 8 Installation das Composer Template.

https://github.com/drupal-composer/drupal-project

Bei Verwendung des Composer Templates ergibt sich folgende Verzeichnisstruktur im Projektverzeichnis.

/drush = drush Verzeichnis

/web = Drupal Root Verzeichnis

/vendor = Bibliotheken

composer.json

composer.lock

.gitignore

Verlegen des Konfigurationsverzeichnisses außerhalb der Drupal Root

Außerdem konfiguriere ich das /config/sync Verzeichnis des Drupal 8 Configuration Managements außer halb der Dokumentroot parallel zum Drupal Rootverzeichnis. Dies kann durch in der Datei settings.php durch folgende Direktive erreicht werden.

+/config/sync

$config_directories['sync'] = '../config/sync';

Git deployment Workflow

Zur Vorbereitung auf den Workflow mit Git, ist zu überlegen was wir in das produktive System übertragen wollen. Dies entscheiden wir durch die Konfiguration der Datei .gitignore.

Ich schlage folgendes Konzept vor.

  1. Der Drupal Core, die Bibliotheken im vendor Verzeichnis und alle contributed Module kommen nicht unter die Versionskontrolle, sondern werden auf dem produktiven Server mit "compsoer install" heruntergeladen.
  2. Die custom Themes und custom Module werden zu Git versioniert und deployed
  3. Die Konfigurationen im /config Verzeichnis werdem zu Git versioniert und deployed

Ich verwende hier die Verzeichnisstruktur die durch die Installation mit Composer Template entsteht. Die .gitignore Datei liegt im Projektverzeichnis, indem auch git initialisiert wird und damit das ".git" Verzeichnis liegen wird.

Eine Beispiel .gitignore könnte daher so aussehen.

# Ignore directories generated by Composer
drush/contrib
vendor
web/core
web/modules/contrib
web/themes/contrib
web/profiles/contrib

# Ignore Drupal's file directory
web/sites/*/files

# Ignore Drupal settings files
web/sites/*/settings.*
web/sites/*/services.yml

# Ignore SimpleTest multi-site environment.
web/sites/simpletest

# Ignore files generated by PhpStorm
.idea

# Ignore dump files
dumps/*

# Ignore deployment scripts
deployment

Zusammenführen des Workflows mit dem Configuration Management, Git und Composer

In der Entwicklungsinstanz/Development

Entwicklungen bestehen meist aus Codeänderungen in custom Modulen und Themes, sowie Einstellungen die in der Datenbank gespeichert werden.

Nach einen Entwicklungszyklus führen wir folgende Schritte durch.

 

# Hinzufügen von Modulen während der Entwicklung
$ composer require drupal/module_name

# Vollständiges updaten des Entwicklungssystems
$ composer update
# Update eines Moduls
$ composer update drupal/module_name
# Update eines Moduls mit dessen Abhängigkeiten
$ composer update drupal/module_name --with-dependencies

# Exportieren der Einstellungen der Datenbank in Yaml Dateien
$ drush config-export (cex)

# Committen aller Änderungen 
$ git add *
$ git commit -m "something"
$ git push origin master

In der produktiven Instanz

Hier holen wir uns die Codeänderungen von Git und bringen das System auf den gleichen Stand wie die Entwicklungsinstanz mit "composer install". Daraufhin spielen die Datenbankupdates ein und importieren anschließend die Konfigurationen aus den Yaml-Dateien.

# Holen der Codeänderungen
$ git pull origin master

# Aktualisieren von Core/Libraries/contrib Projects
$ composer install

# Datenbankupdate
$ drush updatedb (updb)

# Einspielen der Konfigurationen
$ drush config-import (cim)

Scriptgesteuerter Rebuild der Staging- und produktiven Instanzen

Ich verwende z.B. Shellscripts um die Änderungen im Stagingsystem und im produktiven System einzuspielen. Dies hat den Vorteil, dass man nichts vergessen kann, sowie dass man ein automatisches Backup und einige Sicherheitstest einfach integrieren kann. Alternativ können auch Tools wie Jenkins verwendet werden.

Ein Scriptbeispiel findet man auf meinem github.com Account. https://github.com/thom44/deployment

Vorbereiten der Verzeichnisstruktur

Ich plaziere die Rebuildscripts in ein Verzeichnis namens /deployment parallel zum Drupal Rootverzeichnis. Dies befindet sich dann außerhalb der Apache DokumentRoot. Das Script legt logdateien unter /deployment/logs ab.

Die Code- und Datenbanksicherungen lege ich in einem Verzeichnis namens /dumps parallel zum Drupal Rootverzeichnis ab.

Das Projektverzeichnis sieht dann wie folgt aus.

Projektverzeichnisstruktur

 

 

 

 

 

 

 

 

 

 

 

 

Die Verzeichnisrechte sollte man wie folgt einstellen. Dies repräsentiert eine Serverkonfiguration, wo der Webserver nicht Owner, sondern Grupperechte besitzt.

Beispielsweise: root:www-data

  • /deployment (chmod 700)
  • /dumps (chmod 700)
  • /config (chmod 770)

Das stage-rebuild.sh Script landet in /deployment/stage-rebuild.sh und kann wie folgt unter Linux ausgeführt werden.

$ cd deployment
$ sudo ./stage-rebuild.sh

Prozesse des Rebuilds der Staging Instanz

  1. drush set maintance-mode TRUE
  2. Backup der Stage Datenbank (optional)
  3. Löschen der Stage Datenbank und ersetzen mit der produktiven Datenbank
  4. Löschen des Drupal /files Verzeichnisses und ersetzen mit dem des produktiven Systems
  5. Pullen des Git Repositories
  6. System aktualisieren $ composer install
  7. Eventuelle Datanbankupdates einspielen $ drush updatedb
  8. Konfigurationen importieren $ drush config-import
  9. Eventuelle Sicherheitstests
  10. drush set maintance-mode FALSE
  11. Schreiben eines Logfiles in deployment/logs/*

Prozesse des Rebuilds der produktiven Instanz

Das prod-rebuild.sh Script landet in /deployment/prod-rebuild.sh und kann wie folgt unter Linux ausgeführt werden.

$ sudo ./prod-rebuild.sh
$ cd deployment
  • drush set maintance-mode TRUE
  • Backup des /web Verzeichnisses und der bestehenden composer.lock Datei
  • Backup der producton Datenbank
  • Pullen des Git Repositories
  • System aktualisieren $ composer install
  • Eventuelle Datanbankupdates einspielen $ drush updatedb
  • Konfigurationen importieren $ drush config-import
  • Sicherheitstests
  • drush set maintance-mode FALSE
  • Schreiben eines Logfiles in deployment/logs/*

Sicherheitstests

Unter Sicherheitstests stelle ich z.B. die Verzeichnisrechte der Konfigurationsdateien settings.php und services.yml ein. Zudem überprüfe ich ob das .git Verzeichnis mit einer .htaccess Datei geschützt ist. Falls nicht kann dies automatisch nachgeholt werden. Auch die Backups werden mit "chmod 400" abgesichert, sollten sie auf dem produktiven Server verbleiben.

echo "Make some security check" 2>&1 | tee -a $logfile

# Make shure the config files are protected
chmod 440 $DRUPAL_ROOT/$SITE_DIR/settings.php 2>&1 | tee -a $logfile
if [ -f $DRUPAL_ROOT/$SITE_DIR/services.yml ]; then
    chmod 440 $DRUPAL_ROOT/$SITE_DIR/services.yml 2>&1 | tee -a $logfile
fi
if [ -f $DRUPAL_ROOT/$SITE_DIR/settings.local.php ]; then
    chmod 440 $DRUPAL_ROOT/$SITE_DIR/settings.local.php 2>&1 | tee -a $logfile
fi

# Check of .git directory contains a .htaccess file for security
if [ ! -f $PROJECT_PATH/.git/.htaccess ]; then
     cp $PROJECT_PATH/deployment/.htaccess $PROJECT_PATH/.git/.htaccess 2>&1 | tee -a $logfile
     echo ".git/.htaccess created for security" 2>&1 | tee -a $logfile
else
     echo ".git/.htaccess file exists already" 2>&1 | tee -a $logfile
fi

Die kompletten Rebuildscripts findet man auf meinem Github Account unter:

https://github.com/thom44/deployment

Dies kann gerne auf eigene Gefahr verwendet und abgeändert werden.

Probleme und Lösungen

Überschreiben einzelner Werte in lokaler Datei (settings.php oder settings.local.php)

http://docs.drush.org/en/master/config-exporting/ (Simple Value Changes)

Ausklammern bestimmter Module

http://docs.drush.org/en/master/config-exporting/ (Ignoring Development Modules)

Durch einfügen der folgenden Direktiven In die Datei drushrc.php wird die Einstellung, ob die Module installiert sind, nicht mehr exportiert und importiert. In diesem Beispiel die Module devel und devel_generate.

$command_specific['config-export']['skip-modules'] = array('devel','devel_generate');
$command_specific['config-import']['skip-modules'] = array('devel','devel_generate');

drush cex --skip-modules=devel Im Moment werden leider die Konfigurationsdateien der Moduleinstallungen noch exportiert, was den Import verhindert.

ISSUE: https://www.drupal.org/node/2663558

patch: https://github.com/drush-ops/drush/issues/1820

Workaround

Entfernen der entsprechenden Konfigurationsdateien über .gitignore.

# Workaround: remove devel module settings files
config/sync/devel.settings.yml
config/sync/devel.toolbar.settings.yml
config/sync/system.menu.devel.yml

Überprüfen auf zwischenzeitliche Konfigurationsänderungen in der produktiven Seite

Ein "drush config-import" birgt die Gefahr, dass auf der produktiven Seite veränderte Einstellungen überschrieben werden. Die kann man mit Git leicht überprüfen.

Auf der produktiven Seite führen wir folgende Befehle aus:

# Wechselt zu einem neuen branch
$ git checkout -b check
# export der produktiven Konfiguration
$ drush config-export
# Vergleichen der Konfigurationen
$ git diff
Beispiel bei geänderten Seitentitel
thom@gold:/var/www/test/deploy/stage_d8/web$ git diff
diff --git a/config/sync/system.site.yml b/config/sync/system.site.yml
index 381a7b1..aec5905 100644
--- a/config/sync/system.site.yml
+++ b/config/sync/system.site.yml
@@ -1,5 +1,5 @@
 uuid: 5fe78c13-d8f6-4ce7-97c2-e20df050d5a1
-name: 'Neuer Seitentitel'
+name: 'Neuer Seitentitel gehackt'
 mail: thom@gold.local
 slogan: ''
 page:

Filtering Drupal Configuration

http://docs.drush.org/en/master/config-filter/

Ergänzende Module

Diese folgenden 2 Module bieten die Möglichkeit, die Konfigurationsdateien in verschiedene Verzeichnisse zu exportieren.

Projekt Config Split

Projekt Nimbus

 

Das folgende Modul erlaubt das sperren von Konfigurationsänderungen auf der produktiven Seite.

Configuration Read-only mode

Weitere hilfreiche Module

Configuration development

Config Importer and Tools

Configuration Update Manager

Weiterführende Artikel

Guter Artikel über einige Probleme und Lösungen