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.
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.
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.
- 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.
- Die custom Themes und custom Module werden zu Git versioniert und deployed
- 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.
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
- drush set maintance-mode TRUE
- Backup der Stage Datenbank (optional)
- Löschen der Stage Datenbank und ersetzen mit der produktiven Datenbank
- Löschen des Drupal /files Verzeichnisses und ersetzen mit dem des produktiven Systems
- Pullen des Git Repositories
- System aktualisieren $ composer install
- Eventuelle Datanbankupdates einspielen $ drush updatedb
- Konfigurationen importieren $ drush config-import
- Eventuelle Sicherheitstests
- drush set maintance-mode FALSE
- 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.
Das folgende Modul erlaubt das sperren von Konfigurationsänderungen auf der produktiven Seite.