Laravel, Glide & S3: Dynamisch skalierte Bilder aus der Cloud

Für das Hosting einer großen Plattform, spielen die Bilddaten die größte Rolle. Bilder benötigen viel Speicherplatz, Rechenpower um skaliert zu werden und eine schnelle Leitung. Mit Glide lassen sich Bilder direkt aus der Cloud ausliefern.

Die aktuelle Version von workeer ist mit WordPress gebaut und eins kann WordPress definitiv nicht: Bilder. Zwar ist die Mediathek für frische Nutzer intuitiv, aber für Entwickler ein Horror. Bilder und verschiedene Größen der gleichen Bilddaten werden gemeinsam in einem Ordner abgeladen, Bildformate müssen definiert werden, bevor die Bilder hochgeladen werden und es gibt nur eingeschränkte Möglichkeiten Bilder weiterzuverarbeiten.

Für den Relaunch haben wir ein paar Eckdaten festgesetzt:

  1. Bilddaten sollen von der eigentlichen Anwendung entkoppelt werden. So können wir Server schneller wechseln, die Anwendung leichter auf mehrere Server verteilen und sind flexibler im Speicherplatz.
  2. Aus den Originaldaten sollen dynamisch verschiedenste Bildgrößen erzeugt werden. Auch nach dem Upload soll es einfach sein, neue Ausgabeformate hinzuzufügen. Für die Originaldaten sollen einfache Bearbeitungsoptionen zur Verfügung stehen: Skalieren, Beschneiden, Weichzeichnen, Drehen.
  3. Die generierten Bilder sollen zwischengespeichert und von CDNs ausgeliefert werden. Gehen die zwischengespeicherten, dynamisch erzeugten Bilder verloren (z. B. durch einen Serverwechsel), sollen sich die Daten automatisch neu generieren, damit im Betrieb der Seite keine Einschränkung zu spüren ist.

Wir haben uns dazu folgendes Setup ausgedacht: Hochgeladene Bilder landen in einem S3 (Simple Storage Service) Bucket bei Amazon Web Services, natürlich in Frankfurt, möglichst nah am Server. In den Templates haben wir für die Ausgabe der Bild-URLs einen Helper, dem wir gewünschte Bildgrößen und andere Einstellungen übergeben können. Die generierte URL wird von Laravel verarbeitet: Das gewünschte Bild wird von S3 abgerufen, skaliert, das Ergebnis lokal zwischengespeichert und an den Client ausgeliefert. Zwischen Server und Client ist Cloudflare als CDN-Proxy geschaltet, dadurch wird die generierte Bildgröße auf Server in der ganzen Welt verteilt und zwischengespeichert. Damit wird spätestens der zweite Requests wahnsinnig schnell und der Anwendungsserver kaum belastet.

Es gibt zwar ein paar fertige Laravel Integrationen für Glide, aber wir haben uns entschieden, die Bibliothek selber einzubinden. Der Code für das beschrieben Setup sieht also in etwa so aus:

// routes/web.php

Route::get('/images/{file}', 'ImageController@render')->where('file', '.+');

Wer die Bilder auch bei S3 ablegen will, muss den Storage dafür konfigurieren. In der Laravel Dokumentation steht, wie das geht. Der passende Controller dazu, sieht so aus:

// app/Http/Controllers/ImageController.php

namespace App\Http\Controllers;

use Storage;
use App\Http\Requests;
use Illuminate\Http\Request;
use League\Glide\ServerFactory;
use League\Glide\Signatures\SignatureFactory;
use League\Glide\Signatures\SignatureException;
use League\Glide\Filesystem\FileNotFoundException;
use League\Glide\Responses\LaravelResponseFactory;

class ImageController extends Controller
{
    function render($file, Request $request) {
        try {
            // Validate HTTP signature
            SignatureFactory::create(env('GLIDE_SIGNATURE'))
                ->validateRequest('images/' . $file, $request->all());

            $server = ServerFactory::create([
                // Cache filesystem
                'cache' => Storage::disk('local')->getDriver(),
                // Cache filesystem path prefix
                'cache_path_prefix' => 'cache',
                // Source filesystem
                'source' => Storage::disk('s3')->getDriver(),
                // Source filesystem path prefix
                'source_path_prefix' => 'images',
                // Response
                'response' => new LaravelResponseFactory(),
                // Default image manipulations
                'defaults' => [
                    'q' => 80,
                ]
            ]);

            return $server->getImageResponse($file, $request->all());
        } catch (SignatureException $e) {
            // Forbidden
            abort(403);
        } catch (FileNotFoundException $e) {
            // Not Found
            abort(404);
        }
    }
}

Die verschiedenen Bildgrößen werden über URL-Parameter generiert. Das eignet sich natürlich perfekt für DDoS-Attacken oder zumindest um schnell eine große Auslastung des Servers zu erzeugen. Glide bringt zum Glück ein Sicherheitsmechanismus mit um dem vorzubeugen. Aus URL-Parametern wird ein Hash erzeugt und mitgegeben, der vor dem Erzeugen der Bilder gegengeprüft werden kann. Dafür kann man einen Signatur-Token in der Umgebungskonfigurationsdatei .env ablegen.

// z. B. app/Support/helpers.php

function image(string $file = '', array $options = []) {
    $urlBuilder = UrlBuilderFactory::create('/images/', env('GLIDE_SIGNATURE'));
    return $urlBuilder->getUrl($file, $options);
}

Und natürlich nicht vergessen, die Composer-Abhängigkeiten zu installieren:

composer require league/flysystem-aws-s3-v3
composer require league/glide-laravel

Dieser Artikel ist der zweite Teil einer Serie, die den Relaunch der Plattform workeer, der ersten Jobbörse für Geflüchtete und Arbeitgeber, umfasst.

  1. Der phänomenale Start von workeer
  2. Laravel, Glide & S3: Dynamisch skalierte Bilder aus der Cloud

Hans Pagel

Hans, gelernter Designer, berät, konzipiert und programmiert. Ob individuelle, experimentelle oder pragmatische Lösungen, er findet eine, die zu dir passt.