CérénIT

Le blog tech de Nicolas Steinmetz (Time Series, IoT, Web, Ops, Data)

Grav, Docker-Compose et Traefik

docker docker-compose traefik grav

Pour un projet en cours de finalisation, j’ai utilisé le CMS Grav et j’ai décidé tant pour mon développement en local que pour l’environnement de production de déployer cela sous la forme de container docker et d’utiliser Traefik comme reverse-proxy et m’appuyer notamment sur son support natif et dynamique des containers docker.

Premiers pas…

J’avais initialement publié une image nsteinmetz/grav basée sur l’image officielle PHP:apache mais elle ne me convenait pas totalement :

  • Utilisation d’apache2 alors que j’ai basculé depuis longtemps sur nginx
  • L’image PHP:apache est basée sur Debian (taille plus importante qu’une image basée sur Alpine)
  • Pas d’utilisation des volumes (je n’ai pas pris le temps de les déclarer)

Pour mémoire et à toutes fins utiles, le Dockerfile - nsteinmetz/docker-grav :

FROM php:7-apache
ADD https://github.com/getgrav/grav/releases/download/1.1.8/grav-admin-v1.1.8.zip /tmp/grav-admin-v1.1.8.zip
RUN apt update && \
    apt upgrade -y && \
    apt install -y \
        unzip \
        libfreetype6-dev \
        libjpeg62-turbo-dev \
        libmcrypt-dev \
        libpng12-dev \
        pkg-config && \
    unzip /tmp/grav-admin-v1.1.8.zip -d /tmp/ && \
    mv /tmp/grav-admin/* /var/www/html/ && \
    mv /tmp/grav-admin/.htaccess /var/www/html/ && \
    chown www-data:www-data -R /var/www/html && \
    docker-php-ext-install -j$(nproc) mcrypt && \
    docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ && \
    docker-php-ext-install -j$(nproc) gd &&\
    docker-php-ext-install -j$(nproc) zip &&\
    a2enmod rewrite && \
    rm -rf /var/lib/apt/lists/*
COPY php.conf /etc/apache2/conf-enabled/ 
COPY server-signature.conf /etc/apache2/conf-enabled/

Avec php.conf :

php_admin_flag display_errors off
php_admin_flag expose_php off

php_admin_value post_max_size "40M"
php_admin_value upload_max_filesize "40M"

et server-signature.conf :

ServerSignature Off
ServerTokens Prod

Améliorations

Les améliorations apportées :

  • Passage à docker-compose pour avoir plus de flexibilité au moment de démarrer les containers en fonction des projets utilisant Grav,
  • Passage à des images basées sur alpine (plus légères)
  • Passage à une architecture PHP-FPM + Nginx

Cela donne :

├── app
│   ├── Dockerfile
│   ├── security.conf
│   └── uploads.conf
└── web
│   ├── Dockerfile
│   └── php-fpm.conf
├── docker-compose.yml

Petites précisions à ce niveau :

  • app correspond au container php-fpm
    • security.conf contient des options de sécurité PHP (ne pas exposer la version de PHP, ne pas afficher les erreurs, etc)
    • uploads.conf contient des options liés aux uploads PHP (taille max de fichiers, etc)
  • web est le container nginx
    • php-fpm.conf est la configuration de mon virtualhost nginx

Revue du container “app” (php-fpm)

On a app/Dockerfile :

FROM php:7-fpm-alpine
ADD https://github.com/getgrav/grav/releases/download/1.1.8/grav-admin-v1.1.8.zip /tmp/grav-admin-v1.1.8.zip
RUN apk update &&\
    apk upgrade &&\
    unzip /tmp/grav-admin-v1.1.8.zip -d /tmp/ && \
    mv /tmp/grav-admin/* /var/www/html/ && \
    mv /tmp/grav-admin/.htaccess /var/www/html/ && \
    chown www-data:www-data -R /var/www/html &&\
    apk add libjpeg libjpeg-turbo libjpeg-turbo-dev libpng libpng-dev freetype freetype-dev &&\
    docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ && \
    docker-php-ext-install mcrypt zip gd &&\
    rm -rf /var/cache/apk/* &&\
    rm -rf /tmp/grav-admin-v1.1.8.zip &&\
    cd /var/www/html && ./bin/gpm update --no-interaction
COPY uploads.conf /usr/local/etc/php-fpm.d/uploads.conf
COPY security.conf /usr/local/etc/php-fpm.d/security.conf
VOLUME ["/var/www/html", "/var/www/html/assets", "/var/www/html/backup", "/var/www/html/cache", "/var/www/html/images", "/var/www/html/logs", "/var/www/html/tmp"]

A noter, même s’il n’y a rien d’exceptionnel :

  • Initialisation du container en récupérerant la version de Grav, gestion des dépendances et des permissions
  • La dernière ligne de RUN va mettre à jour les plugins inclus nativement dans Grav
  • Les volumes déclarés permettent de sortir du container toutes les zones non stateless : logs, cache, contenus, etc sur la base de la documentation officielle de Grav ; j’ai ajouté néanmoins les répertoire backups et tmp.

Ensuite, app/security.conf:

[www]

php_admin_flag[display_errors] = off
php_admin_flag[expose_php] = off

et app/uploads.conf :

[www]

php_admin_value[post_max_size] = "40M"
php_admin_value[upload_max_filesize] = "40M"

J’ai fait le choix de surcharger l’instance php-fpm par défaut (ie: www) et j’utilise php_admin_* afin d’interdire toute surcharge de ces variables par l’application.

Revue du container “web” (nginx)

web/Dockerfile :

FROM nginx:stable-alpine
RUN rm /etc/nginx/conf.d/default.conf
ADD php-fpm.conf /etc/nginx/conf.d/php-fpm.conf

Je supprime le virtualhost fourni par défaut et je fournis le mien en lieu et place.

web/php-fpm.conf :

server {
    listen 80;
    server_name _;
    charset utf-8;
    
    root /var/www/html/;
    index index.html index.php;

    # Uploads to 100M
    client_max_body_size 100m;

    location / {
        try_files $uri $uri/ /index.php?_url=$uri;
    }

    ## Begin - Security

    # don't send the nginx version number in error pages and Server header
    server_tokens off;

    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Frame-Options SAMEORIGIN;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data: ; style-src 'self' 'unsafe-inline'; font-src 'self'; child-src; object-src 'none'";

    # deny all direct access for these folders
    location ~* /(.git|cache|bin|logs|backup|tests)/.*$ { return 403; }
    # deny running scripts inside core system folders
    location ~* /(system|vendor)/.*\.(txt|xml|md|html|yaml|php|pl|py|cgi|twig|sh|bat)$ { return 403; }
    # deny running scripts inside user folder
    location ~* /user/.*\.(txt|md|yaml|php|pl|py|cgi|twig|sh|bat)$ { return 403; }
    # deny access to specific files in the root folder
    location ~ /(LICENSE.txt|composer.lock|composer.json|nginx.conf|web.config|htaccess.txt|\.htaccess) { return 403; }
    ## End - Security

    ## Begin - PHP
    location ~ \.php$ {
        fastcgi_pass app:9000;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name;
    }
    ## End - PHP
    
    location ~* ^.+\.(ico|js|gif|jpg|jpeg|png|bmp)$ {
        expires 30d;
    }
}

A noter :

  • Le fichier est inspiré de la configuration officielle nginx donnée pour Grav
  • Ajout de directives de sécurité (X-Content-Type-Options, X-XSS-Protection, X-Frame-Options et Content-Security-Policy)
  • Interdiction d’accès à des répertoires
  • Déclaration de php-fpm où le point d’attention est juste d’indiquer app:9000, car app est le nom de mon service.
  • Mise en cache des images

Et un docker-compose.yml pour enrober le tout

version: '2'
services:
    web:
      build: ./web/
      depends_on:
        - app
      volumes_from:
        - app
      ports:
        - "80:80"
    app:
      build: ./app/
      volumes:
        - ./user:/var/www/html/user
        - ./assets:/var/www/html/assets
        - ./backup:/var/www/html/backup
        - ./cache:/var/www/html/cache
        - ./images:/var/www/html/images
        - ./logs:/var/www/html/logs
        - ./tmp:/var/www/html/tmp

Pour builder puis lancer vos containers en mode daemon :

docker-compse up -d --build

Traefik en reverse-proxy

Traefik est un reverse-proxy moderne et il a le bon goût de s’interfacer notamment avec l’API de Docker. On peut alors déclarer dynamiquement nos containers à traefik et celui-ci les prend en compte dynamiquement.

Démarrer Traefik

Pour mon poste en local, j’ai décidé de lancer Traefik de la façon suivante :

docker run -d -v /dev/null:/traefik.toml -v /var/run/docker.sock:/var/run/docker.sock -p 80:80 -p 443:443 -p 8080:8080 --name traefik traefik:camembert --web --docker --docker.domain=docker.localhost --logLevel=DEBUG

Pour les détails :

  • -v /dev/null:/traefik.toml permet de démarrer Traefik sans fichier de configuration
  • -v /var/run/docker.sock:/var/run/docker.sock permert au container Traefik de communiquer avec Docker et son API
  • J’expose les ports 80, 443 et 8080. Le dernier permettra d’accéder au dashboard fourni nativement par Traefik
  • --web : lance l’interface web (dashboard)
  • --docker --docker.domain=docker.localhost : active le support de docker et fourni un domaine par défaut à tous les containers docker.
  • --logLevel=DEBUG : permet d’avoir des logs verbeux si problème via docker logs -f traefik

Rendre son application “traefik-aware”

Il “suffit” d’indiquer des labels à vos services :

version: '2'
services:
    web:
      build: ./web/
      depends_on:
        - app
      volumes_from:
        - app
      labels:
        - "traefik.backend=grav-project"
        - "traefik.frontend.rule=Host:project.grav"
        - "traefik.port=80"
        - "traefik.protocol=http"
        - "traefik.frontend.entryPoints=http"
        - "traefik.docker.network=nom_du_reseau"
    app:
      build: ./app/
      volumes:
        - ./user:/var/www/html/user
        - ./assets:/var/www/html/assets
        - ./backup:/var/www/html/backup
        - ./cache:/var/www/html/cache
        - ./images:/var/www/html/images
        - ./logs:/var/www/html/logs
        - ./tmp:/var/www/html/tmp
      labels:
        - "traefik.enable=false" 

Explications pour le service web :

  • je lui indique un nom de backend (peu importe le nom) qui sert juste à Traefik pour “nommer” votre container
  • l’url (de frontend) sur laquelle Traefik devra réagir pour vous interfacer avec votre container (ici en tapant http://project.grav/ dans mon navigateur, je dois arriver sur mon container)
  • le protocol et le port indiquent que je me connecte sur le port 80 de mon container avec le protocole http
  • l’entryPoint indique que je vais me connecter à Traefik sur le port 80 pour me connecter ensuite à mon conteneur.
  • le nom du réseau que vous a créé docker lors de l’initialisation du projet. Pour le récupérer, faire un docker network ls.

En gros on a :

Vous <=> Votre navigateur <=> Traefik Frontend (Host:project.grav (frontend.rule) + protocole http (EntryPoint)) <=> Traefik Backend (grav-project + port 80 + protocole http) <=> Container Web

Explications pour le service app :

  • Je ne souhaite pas exposer ce service dans Traefik, je désactive alors ce service (par défault, il y a une auto-découverte de tous les services).

Pour finir

Il ne me reste plus qu’à :

  • Faire une entrée DNS ou éditer /etc/hosts pour project.grav
  • Ouvrir votre navigateur sur http://project.grav/

Sauf que cela ne marche pas à ce stade (si vous utilisez traefik dans un container docker). En effet, lors du docker-compose up -d, docker a créé un réseau pour votre application. Or par défaut, Traefik n’y a pas accès. Il vous faut faire:

docker network connect <nom_du_reseau> <container-traefik>

Et ce coup-ci, votre site propulsé par Grav s’affiche.

Vous pouvez retrouver les fichiers et remonter vos commentaires sur cerenit/docker-grav

Une fois prochaine; je vous parlerais d’Ansible, Docker et Traefik pour déployer vos projets aisément.

Web, Ops & Data - Semaine 48

traefik docker kubernetes grafana null aws alerte monitoring dashboard reverse-proxy

Container & orchrestration

  • Kompose: a tool to go from Docker-compose to Kubernetes : en cours d’incubation chez Kubernetes, ce projet permet de faciliter la transition de Docker à Kubernetes en transformant les fichier docker-compose.yml en fichiers Manifests Kubernetes. Un pivot intéressant si on considère que les développeurs vont utiliser Docker (et au mieux Swarm) pour leurs environnements de développement et en production éventuellement Swarm et ensuite vouloir migrer vers Kubernetes pour ses fonctionnalités plus évoluées.
  • Introducing Distributed Cheese: Traefik 1.1 Camembert Is Out! : Traefik, le reverse proxy moderne qui sait s’interfacer notamment avec Docker, Consul, Kubernetes et plein d’autres est sorti en version 1.1.x. Cette version apporte notamment le support de l’API Docker Swarm (Docker 1.12+), une meilleure gestion des contraintes, le support de Mesos, une meilleure gestion des affinités de sessions, un mode cluster pour Traefik (expérimental) et une image officielle docker basée sur alpine. J’ai profité de l’installation d’un nouveau serveur pour déployer Traefik et c’est vraiment agréable de pouvoir déclarer dynamiquement ses containers docker. Par ailleurs, je rappelle que je maintiens des images docker de Traefik pour sa version ARM
  • Docker for AWS Public Beta : l’offre Docker pour AWS passe d’une version alpha à une verion beta. On notera essentiellement qu’il s’agit d’une version plus intégrée avec l’offre AWS que si l’on déployait un docker soi-même. A suivre mais attention à vérifier qu’il n’y a pas un lock-in qui se crée à la fin, que l’intégration apporte un plus et ne nuit pas à la portabilité des containers.

Dashboard, Monitoring

  • What’s new in Grafana v4.0 : la fonctionnalité phare de cette version est la capacité de définir des alertes au niveau de chaque élément d’un dashboard. Pour se faire il faut définir les règles à appliquer et ensuite s’appuyer sur la partie notification. Pour ceux qui étaient tentés d’installer Chronograf 1.1 (béta) pour avoir cet alerting en plus des dashboards adhoc de Grafana, ils pourraient bien finalement rester dans Grafana (dont la maturité n’est plus à prouver) plutôt que d’attendre que Chronograf se stabilise… A moins que Chonograf n’apporte une valeur ajoutée de part son intégration native avec Telegraf.

Pépites

  • Si vous pensiez que toutes les villes commencent par une lettre, maintenant vous savez que c’est faux avec la ville de ’s_Herenelderen. Vous pouvez mettre vos regexp à jour !
  • These unlucky poeple have names that break computers : même si l’article concède que ce problème est de moins en moins vrai au fur et à mesure des progrès réalisés et de la prise de conscience par les développeurs, mais s’appeller “Null” ou avoir un nom de famille très long (36 caractères dans l’exemple donné ou même 8 au Japon quand l’habitude est de 4…), cela pose des tas de problèmes dans la vie du quotidien.

Web, Ops & Data - Semaine 22

kafka orientdb confluent ansible revue de code traefik

Traefik

OrientDB

Ansible

Revue de code

Kafka

2 3 4 5 6