← Blog

Drupal

Drupal 7-redding: toen staging stiekem productie was

Een Utrechtse wooncoöperatie wilde voor EOL van Drupal 7 af. Twee uur in de migratie ontdekten we dat staging en productie dezelfde machine waren.

Jacob Molkenboer· Oprichter · A Brand New Company· 29 mrt 2024· 9 min
Open leren logboek met messing label, groene indexkaart, ijzeren sleutel op papieren hoes, ivoren bureau, zijlicht.

Het was maandag, 23:14. We waren twee uur bezig met wat een smoke test had moeten zijn op de stagingomgeving van de Drupal 7-site van een Utrechtse wooncoöperatie. De officiële end-of-life van Drupal 7 was op 5 januari 2025 verlopen, en de coöperatie had eindelijk de migratie naar Drupal 10 getekend. De staging-URL zag er normaal uit. We klikten door naar de accountpagina van een huurder om een custom veld te checken. De pagina laadde een echt IBAN, een echt adres en een onderhoudsmelding die diezelfde middag was ingediend.

Staging was productie. Productie was staging. Het team zat al zes jaar live te editen.

Hoe we erachter kwamen

De aanwijzing was klein en we hadden 'm bijna gemist. De zogenaamde staging-URL was zoiets als nieuw.cooperatief.nl. We logden in met de testcredentials die de interne ontwikkelaar (al lang vertrokken) in een Google Doc uit 2019 had gedocumenteerd. Een notitie die we om 23:11 op staging bij een huurder zetten, dook de volgende ochtend op in een CSV-export die de office assistant draaide vanuit wat zij "de echte site" noemde. Zij vond het grappig. Wij niet.

De architectuur, toen we 'm uit elkaar trokken, was deze:

  • Eén LAMP-server bij een Nederlandse hoster (een van de goedkope).
  • Twee vhosts, cooperatief.nl en nieuw.cooperatief.nl, beide wijzend naar dezelfde docroot.
  • Eén settings.php. Eén database. Eén sites/default/files.
  • Eén $base_url-override op basis van $_SERVER['HTTP_HOST'].

Elke "test" die sinds 2019 was uitgevoerd, had naar de echte database geschreven. Elke pdf die op staging was gerenderd, was in dezelfde files-map gezet waaruit productie serveerde. Huurders van de coöperatie hadden zes jaar lang op testpdfs zitten klikken zonder het door te hebben.

Waarschuwing

Als je staging-URL echte klantdata serveert zodra je een record verandert, heb je geen stagingomgeving. Je hebt een productieomgeving met een tweede domeinnaam.

Wat we de eerste 24 uur deden

De migratie was niet meer de prioriteit. De prioriteit was data-corruptie stoppen terwijl we uitzochten wat er stond. Die nacht deden we drie dingen.

Eerst zetten we de staging-vhost in Apache achter een deny-from-all, zodat geen interne tester er per ongeluk naar kon schrijven:

<VirtualHost *:443>
    ServerName nieuw.cooperatief.nl
    DocumentRoot /var/www/cooperatief
    <Location />
        Require ip 10.0.0.0/8
        Require all denied
    </Location>
</VirtualHost>

Daarna pakten we een schone drush sql-dump en een rsync van sites/default/files naar een locatie buiten de server. Het nachtelijke back-upvenster van de hoster begon om 03:00 en we wilden niet om 03:15 ontdekken dat "nachtelijk" eigenlijk "wekelijks, met geluk" betekende.

Tot slot schreven we een mail van één alinea aan de directeur van de coöperatie die begon met het woord "dringend" en eindigde met "laat vannacht niemand inloggen". Ze was niet blij. Ze belde wel binnen tien minuten terug.

De audit

De volgende ochtend brachten we in kaart wat er daadwerkelijk draaide. Zes jaar van "dat fixen we volgend kwartaal" had zich opgestapeld.

De site draaide op Drupal 7.78. De laatste D7-securityrelease was op dat moment 7.103. In de vijfentwintig tussenliggende releases zaten drie SA-CORE-adviezen met de status Highly Critical, waaronder Drupalgeddon 2 (laat gepatcht) en SA-CORE-2018-004 (niet gepatcht). De site draaide op PHP 7.2, dat zelf in november 2020 al end-of-life ging.

De modules vertelden hetzelfde verhaal. Eenenveertig contrib-modules geïnstalleerd. Bij elf stond een securityadvies open. Drie waren unmaintained. De custom module die de facturatie van huurders regelde, was een file van 2.400 regels genaamd cooperatief_custom.module, zonder tests, met twee TODO's uit 2017 en de naam van één ontwikkelaar in elke comment.

De files waren erger. sites/default/files was 38GB. Daarvan was 31GB een map genaamd old_backups met SQL-dumps uit 2018, 2019 en 2020. Ze waren via het web world-readable. We hebben het gecheckt. Ja, je kon ze downloaden. Nee, we hebben niemand verteld hoe we dat wisten.

Waarschuwing. Als je Drupal files-map een map bevat genaamd old_backups, archive of private_temp, open dan example.com/sites/default/files/old_backups/ nu in een nieuwe browser. Zie je een directory listing? Dan heb je een lopende datalek.

Het migratieplan

De officiële end-of-life van Drupal 7 was op 5 januari 2025. Na die datum publiceert het Drupal Security Team geen adviezen meer voor Drupal 7 core of contrib. Vendor extended support is beschikbaar bij een handvol Drupal Association-partners en is niet goedkoop. De coöperatie wilde niet eindeloos voor extended support betalen. Ze wilden voor het einde van het boekjaar op een supported versie van Drupal zitten.

We hebben geen lift-and-shift gedaan. Drupal 7 naar Drupal 10 is geen upgrade. Het is een rebuild met een migratiescript erbij. De architectuur is in Drupal 8 veranderd (Symfony, Twig, configuratiebeheer als YAML, dependencies via Composer), en de enige zinnige route is een schone Drupal 10-installatie opzetten en de Migrate Drupal-module gebruiken om content over te halen.

Het plan had vier fases:

  1. Bevriezen. Alle writes naar de oude database stoppen, behalve de writes die huurders nodig hadden (onderhoudsmeldingen, accountupdates). Wekelijks snapshotten.
  2. Scaffolden. Nieuwe Drupal 10-codebase, beheerd via Composer, op een aparte server, met een echte stagingomgeving (aparte database, aparte files, alles apart).
  3. Migreren. Elk Drupal 7-content type mappen naar een Drupal 10-equivalent. De custom facturatiemodule herschrijven als kleine Symfony-service. Files met checksums naar de nieuwe server overzetten.
  4. Cutover. DNS-flip in een onderhoudsvenster, met de oude site daarna twee weken bevroren en read-only.

De migrate-config die het meeste werk deed

Het leeuwendeel van de contentmigratie waren zevenendertig YAML-files die migrationplugins definiëren. Drupal's migrate_plus en migrate_drupal handelen de meeste standaard entities (users, taxonomy, nodes, files) out of the box af. Het patroon ziet er zo uit:

id: cooperatief_tenant_node
label: 'Tenants from Drupal 7'
migration_group: cooperatief
source:
  plugin: d7_node
  node_type: tenant
process:
  nid: tid
  title: title
  uid:
    plugin: migration_lookup
    migration: d7_user
    source: node_uid
  field_iban:
    plugin: get
    source: field_iban/0/value
  field_address:
    plugin: sub_process
    source: field_address
    process:
      country_code: country
      locality: city
      postal_code: postal_code
      address_line1: thoroughfare
destination:
  plugin: 'entity:node'
  default_bundle: tenant
migration_dependencies:
  required:
    - d7_user
    - d7_file

Je schrijft er één per content type. Je draait drush migrate:import cooperatief_tenant_node tegen een kopie van de Drupal 7-database. Je leest de error log, fixt de source field mapping, draait 'm opnieuw. De derde keer werkt het. De vierde keer werkt het en is het snel genoeg om op echte data te draaien.

De cutover

We kozen een zondag om 06:00. Huurders dienen geen onderhoudsmeldingen in om 06:00 op een zondag. De office assistant stond vanaf 05:45 op Slack voor het geval iemand belde.

De checklist telde 41 items. Het eerste item was "verifieer dat de staging-URL niet productie is". We moesten erom lachen. We hebben 't toch twee keer gecheckt. Item zeven was "final migration draaien met --update tegen snapshot van 05:00". Item achtendertig was "DNS TTL minimaal 24 uur van tevoren op 60 seconden zetten". Dat hadden we vrijdag al gedaan.

Om 06:47 was de nieuwe site live. Twee huurders logden voor 09:00 in, geen van beiden merkte iets anders dan dat het zoeken sneller was. Om 11:00 mailde een bestuurslid waarom het contactformulier er "anders" uitzag. Het zag er niet anders uit. We antwoordden netjes en vroegen om een screenshot. Die is er nooit gekomen.

Wat we aan onze eigen werkwijze veranderd hebben

Wat bleef hangen was niet de migratie. Het was de oorspronkelijke fout. Een goedkope hoster, een junior ontwikkelaar, zes jaar van "dat fixen we volgend kwartaal", en een directeur die het woord "staging" vertrouwde omdat ze geen reden had dat niet te doen.

We hebben één item toegevoegd aan elke audit die we nu draaien: open de staging-URL, verander iets triviaals, en check of productie de verandering ziet. Het kost negentig seconden. Het had deze coöperatie zes jaar latente risico bespaard.

Er loopt een verwante draad door ons werk: het gat tussen wat een klein team denkt dat hun infrastructuur is en wat 't daadwerkelijk is. Datzelfde gat zie je overal waar iemand een systeem vertrouwt dat ze niet kunnen inspecteren, of dat nou een CMS is, een CI-pipeline of een AI coding assistant. De fix is altijd hetzelfde. Maak 'm open. Verander iets triviaals. Kijk wat er gebeurt.

Als je niet in twee zinnen kunt uitleggen hoe een write op je stagingomgeving wordt tegengehouden voordat 'ie productie raakt, heb je geen stagingomgeving.

Audit van vijf minuten die je vandaag kunt doen

Hiervoor heb je ons niet nodig. Als je een Drupal-site, een WordPress-site of welk CMS dan ook met een staging-subdomein draait, doe vanmiddag deze vijf dingen:

  1. SSH naar je stagingserver. Draai grep -i 'database' sites/default/settings.php (Drupal) of grep -i DB_NAME wp-config.php (WordPress). Vergelijk met productie. Als de database-naam hetzelfde is, heb je geen staging.
  2. Vergelijk het document root-pad tussen de vhosts. Hetzelfde pad betekent dezelfde files.
  3. Pas op staging één node-titel aan. Opslaan. Productie herladen. Is de titel mee veranderd? Stop met lezen en bel iemand.
  4. Open example.com/sites/default/files/ in een incognito-venster. Zie je een directory listing, voeg dan vanavond Options -Indexes toe aan je Apache-config.
  5. Check je Drupal core-versie tegen de release list. Zit je op Drupal 7 en betaal je geen vendor extended support? Dan draai je ongepatchte code.

Toen we de rebuild voor de coöperatie deden, schreven we uiteindelijk een korte legacy migratie-playbook die precies dit patroon in het eerste uur van elke audit oppikt. De aanwijzing zit bijna altijd in de Apache-config, en gaat bijna altijd terug op een hoster die "staging" heeft opgezet door een tweede ServerName aan de bestaande vhost toe te voegen.

Open je apache2.conf vandaag. Lees 'm. Zie je twee ServerName-directives in één VirtualHost-blok? Dan weet je al wat je te doen staat.

Kern

Als je niet in twee zinnen kunt uitleggen hoe een write op staging wordt tegengehouden voordat 'ie productie raakt, heb je geen stagingomgeving.

FAQ

Wanneer is Drupal 7 end-of-life gegaan?

Drupal 7 is officieel end-of-life gegaan op 5 januari 2025. Sindsdien publiceert het Drupal Security Team geen core- of contrib-adviezen meer. Betaalde vendor extended support is beschikbaar bij een handvol Drupal Association-partners.

Kan ik Drupal 7 in place upgraden naar Drupal 10?

Nee. Drupal 8 heeft de architectuur compleet veranderd. Je zet een schone Drupal 10-installatie op en gebruikt de Migrate Drupal-module om content uit de oude database over te halen. De codebase, het thema en custom modules bouw je opnieuw.

Hoe lang duurt een migratie van Drupal 7 naar Drupal 10?

Voor een middelgrote site (10 tot 30 content types, 5k tot 50k nodes) reken op zes tot twaalf weken. De migratie zelf gaat snel. Het mappen van content types, het herschrijven van custom modules en de themabouw kosten de tijd.

Wat is de snelste check om te zien of staging echt los staat van productie?

Verander één node-titel op staging en herlaad productie. Is de titel mee veranderd? Dan staan je omgevingen niet los van elkaar. De check kost minder dan twee minuten en vangt de ergste klasse aan misconfig.

drupallegacy sitesmigrationsecurityphpcase study

Iets bouwen?

Start een project