Subroot-Lösung: Gleiche Website, mehrere Umgebungen

Unsere DevOps Engineers Markus und David besprechen etwas vor einem Computerbildschirm mit offenen Terminals.

Bisher verwendeten unsere Kunden für ihre Staging- und Prod-Umgebungen jeweils zwei verschiedene Websites. Vor Kurzem aber erhielten wir erstmals die Anfrage, ob und wie die Staging- und Prod-Umgebung auch auf der gleichen Website betrieben werden können.

Ja! Das ist möglich und benötigt abgesehen von etwas Apache Config Syntax auch keine weitere Magie. 🪄

In diesem Beispiel behandeln wir folgende zwei Use-Cases:

  • die gleiche Datenbank für zwei verschiedene Umgebungen benutzen
  • unterschiedliche Inhalte basierend auf verschiedenen Environment-Variablen anzeigen

Website erstellen

Zuerst muss dafür auf einem Managed Server eine neue Website erstellt werden, in diesem Fall vom Typ PHP.

Im Beispiel verwenden wir 3 Hostnames:

  • subroot-test.tetilla.dado.opsserver.ch ← Erste Website und Fallback
  • preview.subroot-test.tetilla.dado.opsserver.ch ← Zweite Website
  • not-assigned.subroot-test.tetilla.dado.opsserver.ch ← Domain zeigt auf den Server, aber es existiert kein entsprechender Subroot

Damit überprüft werden kann, ob die Environment-Variablen auch korrekt geteilt werden, aktivieren wir noch eine MySQL Datenbank.

Ordnerstruktur

Auf der neu erstellten Website müssen zwei Ordner innerhalb des ~/www-Ordner erstellt werden:

  • ~/www/subroot-test.tetilla.dado.opsserver.ch
  • ~/www/preview.subroot-test.tetilla.dado.opsserver.ch

Die Dateistruktur sollte aktuell also folgendermassen aussehen:

Dateien im Hauptordner

index.php

In der ~/www/index.php Datei legen wir nur einen kleinen Text an, um zu testen, ob jemand fälschlicherweise auf der Standardwebsite landet:

<?php
echo '<p>No one should see this</p>';
?>

.htaccess

Die Logik für die richtige Handhabung der Subroots wird in der ~/www/.htaccess Datei erfasst:

# Apache configuration
# feel free to adjust this file to your needs

##################
# Subsite Config #
##################

# Enable rewrite engine
RewriteEngine On

# Define the fallback site (name of the folder)
SetEnv FALLBACK_SITE subroot-test.tetilla.dado.opsserver.ch

###
# Rewrite request to appropriate subsite folder
# name within "~/www/". Must match HTTP host header.
# Only redirect if subroot directory exists.
###

# Prevent redirect loops
RewriteCond %{ENV:REDIRECT_STATUS} ^$
# Check if the requested directory exists
RewriteCond %{DOCUMENT_ROOT}/%{HTTP_HOST} -d
# Load content from the specified subdirectory
RewriteRule ^(.*)$ /%{HTTP_HOST}/$1 [L]

###
# Redirect to Fallback if subroot doesn't exits
###

# Prevent redirect loops
RewriteCond %{ENV:REDIRECT_STATUS} ^$
# Only apply redirect rule if the folder does not exist
RewriteCond %{DOCUMENT_ROOT}/%{HTTP_HOST} !-d
# Load content from the defined fallback site
RewriteRule ^(.*)$ /%{ENV:FALLBACK_SITE}/$1 [L]

#########
# Other #
#########

# recommended security headers
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Strict-Transport-Security "max-age=63072000"

Durch den in der FALLBACK_SITE-Environment-Variablen erfassten Ordner wird definiert, was mit den Requests von einer Domain passieren soll, für die kein Subroot existiert. In unserem Fall wird nun bei Requests an not-assigned.subroot-test.tetilla.dado.opsserver.ch der Inhalt von ~/www/subroot-test.tetilla.dado.opsserver.ch geladen.

Falls statt Anzeige des Inhalts eines Subroots ein Redirect zu einem anderen Subroot gewünscht ist, muss eine Zeile der Konfiguration angepasst werden:

-RewriteRule ^(.*)$ /%{ENV:FALLBACK_SITE}/$1 [L]
+RewriteRule ^(.*)$ http://%{ENV:FALLBACK_SITE}/$1 [R=302,L]

Erster Subroot: subroot-test.tetilla.dado.opsserver.ch

Im ersten Subroot kann nun das gewünschte Projekt angelegt werden und mit einer entsprechenden .htaccess-Datei eine Konfiguration angewendet werden.

Wir möchten folgendes Verhalten des Subroots erzeugen:

  • Die Environment-Variable APP_ENV kann von der PHP-Applikation ausgelesen werden und beinhaltet den Wert live
  • Die Route / gibt einen für die Live-Umgebung spezifischen Inhalt aus, die Environment-Variablen für die Datenbank-Konfiguration sind jedoch identisch mit dem zweiten Subroot
  • Die Route /test.php gibt einen für die Live-Umgebung spezifischen Inhalt aus
  • Die Route /preview führt einen Redirect zu der Preview-Umgebung (preview.subroot-test.tetilla.dado.opsserver.ch) aus
  • Die Route /preview/test.php führt einen Redirect zu der /test.php Route der Preview-Umgebung (preview.subroot-test.tetilla.dado.opsserver.ch) aus

Webserver Konfiguration

In der Datei ~/www/subroot-test.tetilla.dado.opsserver.ch/.htaccess konfigurieren wir die Subroot spezifische Environment-Variable und den /preview Redirect:

RewriteEngine On

SetEnv APP_ENV live

RewriteRule ^preview(.*)$ https://preview.subroot-test.tetilla.dado.opsserver.ch$1 [R=302,L]

Inhalt

Für die ~/www/subroot-test.tetilla.dado.opsserver.ch/index.php-Datei definieren wir einen Inhalt, der es uns ermöglicht, Environment-Variablen auf Website- und Subroot-Ebene zu vergleichen:

<?php
echo '<h1>LIVE</h1>';
echo '<p>APP_ENV: '.$_SERVER['APP_ENV'].'</p>';
echo '<p>DB_HOST: '.$_SERVER['DB_HOST'].'</p>';
echo '<p>DB_DATABASE: '.$_SERVER['DB_NAME'].'</p>';
echo '<p>DB_USERNAME: '.$_SERVER['DB_USERNAME'].'</p>';
?>

Ebenso muss die ~/www/subroot-test.tetilla.dado.opsserver.ch/test.php-Datei erstellt werden, hier wird ebenfalls ein für die Live-Umgebung spezifischer Inhalt erfasst:

<?php
echo 'Test on Live';
?>

Zweiter Subroot: preview.subroot-test.tetilla.dado.opsserver.ch

  • Die Environment-Variable APP_ENV kann von der PHP-Applikation ausgelesen werden und beinhaltet den Wert preview
  • Die Route / gibt einen für die Preview-Umgebung spezifischen Inhalt aus, die Environment-Variablen für die Datenbank-Konfiguration sind jedoch identisch mit dem ersten Subroot
  • Die Route /test.php gibt einen für die Preview-Umgebung spezifischen Inhalt aus
  • Alle Routes in der Preview-Umgebung sind durch Basic-Auth geschützt

Webserver Konfiguration

Zuerst muss eine .htpasswd-Datei mit entsprechendem Passwort generiert werden. Im Beispiel wird ein User test mit dem Passwort password erstellt.

# subroot-test@tetilla.dado.opsserver.ch in ~ on git:main o disk:80% [12:58:05] C:2
$ htpasswd -c ~/cnf/.htpasswd test
New password:
Re-type new password:
Adding password for user test

In der Datei ~/www/preview.subroot-test.tetilla.dado.opsserver.ch/.htaccess konfigurieren wir die Subroot-spezifische Environment-Variable und die Basic Authentication:

# Credentials:
# ==================
# Username: test
# Password: password

AuthType basic
AuthName "Preview Area"
AuthUserFile /home/subroot-test/cnf/.htpasswd
Require valid-user

SetEnv APP_ENV preview

Inhalt

Für die ~/www/preview.subroot-test.tetilla.dado.opsserver.ch/index.php-Datei definieren wir einen ähnlichen Inhalt wie im ersten Subroot, der es uns ermöglicht, Environment-Variablen auf Website- und Subroot-Ebene zu vergleichen:

<?php
echo '<h1>PREVIEW</h1>';
echo '<p>APP_ENV: '.$_SERVER['APP_ENV'].'</p>';
echo '<p>DB_HOST: '.$_SERVER['DB_HOST'].'</p>';
echo '<p>DB_DATABASE: '.$_SERVER['DB_NAME'].'</p>';
echo '<p>DB_USERNAME: '.$_SERVER['DB_USERNAME'].'</p>';
?>

Ebenso erstellen wir ein Äquivalent zur test.php-Datei im ersten Subroot, ~/www/preview.subroot-test.tetilla.dado.opsserver.ch/test.php:

?>
<?php
echo 'Test on preview';
?>

Finale Dateistruktur

Nun sollte unsere Dateistruktur folgendermassen aussehen:

Überprüfen

Jetzt können wir für die Tests die Password Protection der Website ausschalten:

Erster Subroot

Hostname: subroot-test.tetilla.dado.opsserver.ch

  1. Zugriff auf https://subroot-test.tetilla.dado.opsserver.ch verlangt keine auth
  2. Zugriff auf https://subroot-test.tetilla.dado.opsserver.ch zeigt Inhalt von ~/www/subroot-test.tetilla.dado.opsserver.ch/index.php an
  3. Zugriff auf https://subroot-test.tetilla.dado.opsserver.ch/test.php zeigt Inhalt von ~/www/subroot-test.tetilla.dado.opsserver.ch/test.php an
  4. APP_ENV aus ~/www/subroot-test.tetilla.dado.opsserver.ch/.htaccess wird in ~/www/subroot-test.tetilla.dado.opsserver.ch/index.php richtig geladen
  5. Redirect von https://subroot-test.tetilla.dado.opsserver.ch/preview auf https://preview.subroot-test.tetilla.dado.opsserver.ch/ funktioniert
  6. Redirect von https://subroot-test.tetilla.dado.opsserver.ch/preview/test.php auf https://preview.subroot-test.tetilla.dado.opsserver.ch/test.php funktioniert

Zweiter Subroot

Hostname: preview.subroot-test.tetilla.dado.opsserver.ch

  1. Zugriff auf https://preview.subroot-test.tetilla.dado.opsserver.ch verlangt basic auth
  2. Zugriff auf https://preview.subroot-test.tetilla.dado.opsserver.ch zeigt Inhalt von ~/www/preview.subroot-test.tetilla.dado.opsserver.ch/index.php an
  3. Zugriff auf https://preview.subroot-test.tetilla.dado.opsserver.ch/test.php zeigt Inhalt von ~/www/preview.subroot-test.tetilla.dado.opsserver.ch/test.php an
  4. APP_ENV aus ~/www/preview.subroot-test.tetilla.dado.opsserver.ch/.htaccess wird in ~/www/preview.subroot-test.tetilla.dado.opsserver.ch/index.php richtig geladen
  5. Redirect von https://preview.subroot-test.tetilla.dado.opsserver.ch/preview auf https://preview.subroot-test.tetilla.dado.opsserver.ch/ funktioniert nicht

Nicht existierende Subroots

Hier soll alles wie auf der konfigurierten FALLBACK_SITE funktionieren, es soll aber der originale Hostname not-assigned.subroot-test.tetilla.dado.opsserver.ch verwendet werden.

Falls ein Redirect zur FALLBACK_SITE konfiguriert wurde, soll dieser erfolgen.