Laravel webapplicatie met Varnish in een Docker omgeving

Varnish cache

Stein Smilder, oktober 2021.


In deze blogpost onderzoek ik of en hoe het mogelijk is een Laravel webapplicatie binnen een Docker omgeving te draaien. De reden om voor *Laravel* te kiezen is omdat ik in mijn eigen projecten als ZZP'er veelal gebruik maak van dit PHP-framework. De reden om voor *Varnish caching* te kiezen is omdat dit binnen de hostingbranche een bekende tool is om een website sneller te maken, door middel van caching.

Al langere tijd ben ik aan het kijken naar op welke wijze ik Docker in mijn eigen projecten kan toepassen. Gezien de voordelen die Docker biedt wat betreft het opzetten van een applicatieomgeving inclusief eventueel te installeren dependencies.

De vraag die ik in deze blogpost beantwoord is: Hoe kan een Laravel webapplicatie in combinatie met een Varnish caching service geautomatiseerd in een Docker omgeving worden geplaatst?

Mogelijkheden van Laravel in een Microservice architectuur

Op dit moment gebruik ik binnen mijn eigen projecten Laravel veelal als monoliet.

"A monolithic app has all or most of its functionality within a single process or container and it's componentized in internal layers or libraries. The downside to this approach comes if or when the application grows, requiring it to scale. If the entire application scaled, it's not really a problem. However, in most cases, a few parts of the application are the choke points that require scaling, whereas other components are used less." (Microsoft, 2021)

Een aantal applicaties waar ik aan werk worden steeds groter in omvang, terwijl de hoeveelheid gebruikers toeneemt. Omdat microservices individueel kunnen schalen, verbetert dit in veel gevallen de performance van de software aanzienlijk. Uitbreiden van RAM en CPU is ook een optie, maar dit brengt meer kosten met zich mee.

"Laravel is an open-source PHP framework designed to make developing web apps easier and faster through built-in features. These features are part of what makes Laravel so widely used by web developers." (Tim, 2019)

De features die hier bedoeld worden zijn onder andere:

  • Een volledig authenticatie systeem.
  • Een ORM-systeem om data uit een database om te zetten naar PHP-objecten en vice versa.
  • Een command-line interface om unit tests uit te voeren, handmatig een stuk PHP-code aan te roepen en database migraties uit te voeren.
  • Een virtuele development omgeving. Om tijdens het ontwikkelen de Laravel-applicatie doormiddel van een commando de applicatie te starten.

Doordat Laravel voorziet in een groot aantal features is het framework te zwaar om in meerdere microservices op te nemen. Om die reden hebben de ontwikkelaars van het Laravel framework een apart microframework ontwikkeld genaamd Lumen.

"Lumen microframework is a lightweight version of Laravel full-stack framework. Lumen use the Laravel syntax and components, and can be 'upgrade' easily to Laravel.

Lumen is a more specialized (and stripped-down) framework designed for Microservices and API development. So, some of the features in Laravel such as HTTP sessions, cookies, and templating are not needed and Lumen takes them away, keeping what's essential - routing, logging, caching, queues, validation, error handling and a couple of others." (Garbar, 2021)

Als voorbeeld voor hoe je het Lumen-framework kan integreren in microservices, heb ik het Microservices with Lumen project onderzocht. In dit project werken meerdere Lumen-microservices met elkaar samen.

De architectuur van dit bestaande project is als volgt (afbeelding overgenomen uit genoemde repository): Architectuur voorbeeldproject met Lumen-microsevices

Dit project bestaat uit drie Lumen-microservices:

  • LumenApiGateway
    • Verantwoordelijk voor authenticatie en het doorsturen van requests naar onderliggende microservices.
    • Dit is de microservice waar de (externe) gebruiker mee communiceert.
  • LumenAuthorApi
    • Een microservice waarin CRUD-operaties (cread, read, update, delete) toegepast worden op een Auteur-object.
  • LumenBookApi
    • Een microservice waarin CRUD-operaties toegepast worden op een Boek-object.

Communicatie tussen de microservices gebeurt in dit voorbeeld via HTTP. In plaats daarvan kan ook AMQP (via bijvoorbeeld RabbitMQ) gebruikt kunnen worden. Het voordeel aan AMQP is dat het betrouwbaar en asynchroon is, en je je dus geen zorgen hoeft te maken over of je bericht wel aankomt (Sörenson, 2017).

Om die reden heb ik verder gekeken naar de mogelijkheden van RabbitMQ in combinatie met het Lumen-framework. Hierop vond ik een tutorial met een combinatie van het Lumen-framework en RabbitMQ.

Het project, besproken in deze tutorial, bestaat uit twee microservices:

  • Microservice 1
    • Stuurt een message 'OrderCreated' naar Microservice 2.
    • Code zichtbaar op GitHub
  • Microservice 2
    • Luistert naar messages van het type 'OrderCreated'.
    • Code zichtbaar op GitHub

Dit project laat zien dat het mogelijk is om Lumen microservices met elkaar te laten communiceren via een message queue. Dit heeft onder andere het voordeel van 'gegarandeerde levering' tussen de microservices. Nadeel is echter dat bij gebruik van RabbitMQ als communicatiesysteem een groot deel van het Lumen-framework niet niet nodig is. Communicatie binnen zowel Laravel en Lumen verloopt via HTTP. Vandaar dat er een HTTP-routing systeem zit ingebouwd in dit framework. Door RabbitMQ toe te passen worden de onderdelen van Laravel die dit verzorgen echter overbodig.

Voor dit onderzoek houd ik de onderlinge communicatie tussen de microservices daarom bij HTTP. Bij (verplicht) gebruik van RabbitMQ zou ik niet kiezen omdat veel code uit het Lumen-framework hierdoor overbodig is.

Extra services (containers) om Laravel in een Docker container te draaien

Over het algemeen zijn er binnen Laravel een aantal services nodig. Waaronder:

  • PHP-FPM, om live PHP te compileren tot HTML
  • Relationele database, vanzelfsprekend om data in op te slaan
  • RabbitMQ, indien communicatie tussen microservices via AMQP verloopt

PHP-FPM beschikt over een ingebouwde webserver. Deze server verzorgt het compileren van PHP-code tot bijvoorbeeld tekst of HTML en het versturen daarvan naar de (in het geval van dit onderzoek) de Varnish cache.

Mogelijkheden van Varnish in een Microservice architectuur

Varnish Cache is een reverse proxy die op basis van request headers zoals URL en Request Method een kopie van de pagina in het geheugen houdt om deze bij een vervolgbezoek direct uit het geheugen uit te kunnen serveren, ook wel Full Page Cache genoemd (Hipex, 2019).

Voor Varnish is er op Docker Hub een image beschikbaar. Om deze image te gebruiken is het enkel benodigd een zogenaamd .VCL configuratiebestand mee sturen, waarin de instellingen voor de Varnish service worden gedefinieerd.

VCL is de 'configuratietaal' van Varnish en staat voor Varnish Configuration Language.

"Varnish has a great configuration system. Most other systems use configuration directives, where you basically turn on and off lots of switches. We have instead chosen to use a domain specific language called VCL for this.

Every inbound request flows through Varnish and you can influence how the request is being handled by altering the VCL code. You can direct certain requests to particular backends, you can alter the requests and the responses or have Varnish take various actions depending on arbitrary properties of the request or the response. This makes Varnish an extremely powerful HTTP processor, not just for caching.

VCL has inherited a lot from C and it reads much like simple C or Perl. Blocks are delimited by curly brackets, statements end with semicolons, and comments may be written as in C, C++ or Perl according to your own preferences." (Varnish Software AS, z.d.)

De VCL-syntax kent een aantal onderdelen:

  • Strings
  • Access control lists (ACLs)
  • Operators
  • Built in subroutines
  • Custom subroutines

Via een custom subroutine is het bijvoorbeeld mogelijk om bepaalde webpagina's uit te sluiten van de Varnish cache. Dit kan handig zijn wanneer je een API hebt die altijd de meest recente data terug moet geven. Met behulp van een custom subroutine kan worden ingesteld dat deze API niet gecached moet worden.

In dit geval kan zo'n subroutine er als volgt uit zien:

sub vcl_recv {
  if (req.url == "/api") {
      return (pass);
  }
}

Varnish combineren met Laravel in Docker

Om Laravel in combinatie met Varnish te gebruiken heb ik allereerst een docker-compose.yaml aangemaakt met daarin een aantal services (containers):

  • Database (MariaDB)
  • Microservice 1, die data wegschrijft naar de database
  • Microservice 2, die data doorstuurt naar de eerste microservice
  • Varnish cache

Uiteindelijk ziet deze configuratie er als volgt uit:


version: '3'
services:

  # Service 1 - Slaat inkomend bericht op in Database
  service1:
    build: service1/
    restart: unless-stopped
    command:  bash -c "php artisan migrate && php -S service1:80 -t public"
    working_dir: /var/www
    volumes:
      - ./.env:/var/www/.env
    depends_on:
      - mariadb

  # Service - Verstuurt willekeurig bericht naar service 1 en haalt alle berichten op uit service 1
  service2:
    build: service2/
    restart: unless-stopped
    command:  bash -c "php -S service2:80 -t public"
    working_dir: /var/www
    volumes:
      - ./.env:/var/www/.env
    depends_on:
      - service1

  # Varnish Service
  varnish:
    image: varnish:6.0.8
    restart: unless-stopped
    ports:
      - "80:80"
    volumes:
      - ./varnish/default.vcl:/etc/varnish/default.vcl:ro

  # MySQL Service
  mariadb:
    image: mariadb:latest
    restart: unless-stopped
    environment:
      MARIADB_DATABASE: ${DB_DATABASE}
      MARIADB_USER: ${DB_USERNAME}
      MARIADB_PASSWORD: ${DB_PASSWORD}
      MARIADB_RANDOM_ROOT_PASSWORD: "yes"

Hierbij heb ik poort 80 van de Varnish service publiek toegankelijk gemaakt. De Varnish service stuurt vervolgens (gecached) door middel van een reverse proxy microservice 2 door aan de browser van de gebruiker. Microservice 2 roept om zijn beurt microservice 1 aan om een bericht op te slaan.

Binnen het prototype is het mogelijk berichten te plaatsen en een overzicht van alle berichten op te halen. Er was echter een probleem dat na het toevoegen van een bericht deze niet in het overzicht van berichten kwam te staan. Dit kwam simpelweg doordat het overzicht van berichten in de Varnish cache stond. Om dit probleem op te lossen heb ik een regel toegevoegd, die bij het plaatsen van bericht, de Varnish cache leegt.

Dit maakt dat de Varnish VCL-configuratie er als volgt uit ziet:

vcl 4.0;

backend default {
  .host = "service2";
  .port = "80";
  .connect_timeout = 500s;
  .first_byte_timeout = 500s;
}

sub vcl_recv {
  if (req.url == "/create") {
      ban("req.http.host ~ .*");
      return (pass);
  }
}

Je kunt hier nog een Docker-volume toevoegen om de database gegevens in op te slaan. Aangezien dit een prototype is heb ik dit voor nu weggelaten.

Het volledige prototype heb ik geplaatst op GitHub: https://github.com/stein155/laravel-varnish-docker.

Laravel webapplicatie in combinatie met een Varnish cache geautomatiseerd uitbrengen

Het proces om een Laravel webapplicatie op een externe server te deployen is mogelijk door middel van continous integration (CI). Onder andere via GitHub is er de mogelijkheid om CI op te zetten. Dit kan gedaan worden met GitHub Actions. In dit geval heb ik de keuze gemaakt om enkel naar GitHub Actions te kijken, aangezien ik daar in een eerder project mee heb gewerkt en daardoor al enige ervaring met dit systeem heb.

Voordat er een configuratie opgesteld kan worden voor GitHub Actions moet er eerst gekeken worden welke stappen er doorlopen moeten worden om de webapplicatie in combinatie met Varnish op een externe server te plaatsen. Deze stappen zijn als volgt, vanuitgaand dat er een bestaande server is waarop ingelogd kan worden door middel van SSH, en Docker (Compose) is geïnstalleerd:

  • Inloggen via SSH op de server
  • Content van repository kopiëren naar server
    • Lumen microservices
    • Varnish configuratie
    • docker-compose.yaml en Dockerfile
  • Uitvoeren commando docker compose up, waarna de Docker-images opgebouwd worden en de applicatie start.

Binnen GitHub Actions is het mogelijk op bestaande Actions opgesteld door andere GitHub-gebruikers te gebruiken. Uiteindelijk is mijn keuze gevallen op de GitHub check github-action-ssh-docker-compose. Aangezien deze precies de stappen doorliep welke bovenstaand worden genoemd, namelijk om de applicatie op een server te plaatsen en met docker-compose op te starten.

Uiteindelijk heeft dit tot de volgende GitHub Actions configuratie geleid:

name: Deploy

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2

    - uses: alex-ac/github-action-ssh-docker-compose@master
      name: Docker-Compose Remote Deployment
      with:
        ssh_host: ${{ secrets.SSH_HOST }}
        ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }}
        ssh_user: ${{ secrets.SSH_USER }}
        docker_compose_prefix: prototype

Te zien is dat in bovenstaande configuratie de term secrets voorbijkomt. Dit zijn geheime waarden die per GitHub repository ingesteld kunnen worden. Dit is dus de plek om in dit geval een private key op te slaan. Secrets zijn niet in te zien nadat ze bij de opgegeven repository zijn opgeslagen.

Conclusie

Op basis van dit onderzoek kan ik zeggen dat het mogelijk is het Laravel-framework te gebruiken in een Docker-omgeving, samen met een Varnish caching laag. Net als het automatisch deployen daarvan door middel van een CI-omgeving als GitHub Actions.

Wat wel een punt is bij het gebruik van Varnish, is dat er rekening gehouden moet met dat er situaties zijn waarin caching niet gewenst is. Dit bijvoorbeeld wanneer er een endpoint is om een wijziging te maken, die vervolgens niet zichtbaar is omdat de oude waarde in de cache staat. Binnen Varnish is hier een oplossing voor. Door middel van regels is het mogelijk op bepaalde momenten de cache te legen. Bijvoorbeeld bij het plaatsen van een nieuw bericht (prototype).

Kennis opgedaan uit dit onderzoek heb ik gebruikt in een prototype. Deze is zichtbaar op GitHub: https://github.com/stein155/laravel-varnish-docker.

Bronnen

Last change: 2025-01-13