Loupe

Créer un serveur de log avec Docker, Elastic Search, Kibana et Nginx : Architecture et mise en place

Elastic search et Kibana sont des outils performants pour stocker et requêter les données en mode "full text search". Le moteur Lucêne offre de bonnes performances en lecture. Ce couple d’outils peut être une option intéressante à considérer pour la mise en place d’un serveur de log : Elastic Search expose des APIs pour envoyer des données de logs applicatifs et Kibana offre des outils (grilles et graphiques) de visualisation de données. De plus, certain logger comme Seri Log permettent d’envoyer les logs applicatifs directement vers une instance Elastic Search.

Dans cet article, nous allons découvrir comment créer un serveur de log en utilisant Docker pour "conteneriser" Elastic Search et Kibana. Dans un second temps nous verrons comment configurer Seri Log dans une application ASP net Core pour utiliser notre serveur de log. Dans un second article, nous verrons comment héberger ce serveur de log dans Azure. 

Installation classique vs Docker

Actuellement, il est parfaitement possible d’installer Elastic Search et Kibana sur une machine Windows ou Linux, cela fonctionne parfaitement. Cependant le setup d’une machine et l’installation des outils peut s’avérer longue et fastidieuse. L’idée de ce post est de proposer une solution simple et rapide pour mettre en place un serveur de log en quelques minutes. Docker nous permet de définir les différentes briques de notre serveur de log dans un seul fichier et de le rendre fonctionnel avec deux lignes de commande.

Architecture cible 

Nous allons comparer deux architectures, un première basique et une seconde plus robuste et sécurisée.

Architecture v1

Au minimum deux conteneurs suffisent pour mettre en place le serveur de log :

  • Kibana
    • Port interne : 5601
    • Port externe : 80
    • Image : docker.elastic.co/kibana/kibana:6.1.2
    • Configuration: Pas de configuration spécifique, ce conteneur a simplement besoin de communiquer avec l’instance Elastic Search (cette communication est obligatoire, car Kibana utilise les API de l’instance Elastic Search)
  • Elastic Search
    • Port interne : 9200
    • Port externe : 9200
    • Base Image : elastic.co/elasticsearch/elasticsearch:6.3.2
    • Configuration: Elastic Search utilise un fichier de configuration nommé elasticsearch.yml. Si nous voulons surcharger les valeurs par défaut de ce dernier, nous pouvons en créer un et le copier dans le conteneur lors de la phase de build de l’image Docker.

Ces deux conteneurs doivent être placés sur le même réseau Docker.

Shema

v1.PNG

Cette architecture fonctionne en l’état, cependant elle n’est pas sécurisée. Nous exposons publiquement les ports 80 et 9200 (ce qui est une mauvaise pratique !). Par défaut, l’accès à une instance Elastic Search n’est pas sécurisé, l’instance est exposée publiquement sur le port 9200. Pour sécuriser son accès nous avons deux possibilités :

  • Installer un plugin payant (ex : X-Pack)
  • Configurer un système d’authentification au niveau du host Docker

La seconde option peut être mise en place en utilisant un conteneur Nginx qui fera office de proxy entre le port public exposé depuis l’hôte Docker et les conteneurs Elastic Search et Kibana. Avec Nginx nous pouvons définir des routes et protéger leur accès avec une autorisation « basic ».

Architecture v2 (sécurisée)

Dans la seconde version de l’architecture, nous allons utiliser trois conteneurs :

  • Kibana
    • Port interne : 5601
    • External port : 80
    • Base Image : docker.elastic.co/kibana/kibana:6.1.2
  • Elastic Search
    • Port interne : 9200
    • External port : 80
    • Image : elastic.co/elasticsearch/elasticsearch:6.3.2
  • Nginx
    • Internal port : 80
    • External port : 8080 *
    • Image : nginx
    • Configuration : 2 fichiers sont requis, nginx.conf qui contient la configuration du reverse proxy et .htpasswd qui stocke les informations d’authentification “basic” (login / password)

* sur le schéma suivant, c'est le port 80 qui définit en tant que port externe du conteneur nginx. 

Schéma

v2.PNG

Persistance des données

Avant de rédiger notre Docker file, une problématique se pose, « Comment allons nous persister les données stockées par Elastic Search ? ». Par défaut, Elastic Search stocke ses données dans le répertoire  /var/lib/elasticsearch/data pour Ubuntu et Debian (ce dernier peut changer selon la distribution Linux). Ces données sont importantes car elles représenteront à terme nos logs de production, nous ne pouvons pas les perdre lorsque le conteneur s’arrête de fonctionner pour un raison X. Nous allons donc utiliser le système de Volume Docker pour persister ces données sur l’hôte Docker. Dans cet article nous allons utiliser le driver Docker local pour gérer les volumes. Nous pourrions également choisir de stocker les données dans un File Share Azure le driver suivant : https://github.com/Azure/azurefile-dockervolumedriver.

Travail local

Nous allons travailler dans un répertoire parent nommé « log server ». Dans ce répertoire parent, chaque conteneur possède son propre répertoire.

1.PNG

Conteneur Elastic Search

La configuration de ce conteneur est relativement simple, le dossier contient un simple Docker file qui récupère l’image officielle Elastic Search.

Contenu du dossier :

2.PNG

Contenu du Docker file:

FROM docker.elastic.co/elasticsearch/elasticsearch:6.3.2

Conteneur Kibana

Comme le conteneur Elastic Search, sa configuration est simple, le dossier contient également un seul fichier : le Docker file.

Contenu du dossier :

 3.2.PNG

Contenu du Docker file :

FROM docker.elastic.co/kibana/kibana:6.1.2

Conteneur nginx

La configuration du conteneur nginx est la plus intéressante car notre proxy a deux responsabilités :

  1. Router les requêtes provenant de l’extérieur vers les conteneurs Elastic Search et Kibana
  2. Appliquer une authentification “Basic” pour restreindre l’accès aux conteneurs Elastic Search et Kibana

Dans le dossier nginx, 3 fichiers sont présents :

  • Docker file
  • nginx.conf : Configuration des règles de reverse proxy
  • .htpasswd : Authentication Basic credentials (login & password)

Contenu du dossier:

3.1.PNG

Docker file

Le Docker file exécute les actions suivantes :

  1. Récupération de l’image nginx
  2. Copie le fichier conf dans le conteneur, dans le dossier /etc/nginx/nginx.conf
  3. Copie le fichier .htpasswd dans le conteneur, dans le dossier /etc/nginx/.htpasswd

Contenu du Docker file :

FROM nginx
COPY nginx.conf /etc/nginx/nginx.conf
COPY .htpasswd /etc/nginx/.htpasswd

Nginx

Le fichier nginx.conf est le cœur de notre architecture. C’est lui qui nous permet de router les requêtes vers nos conteneurs de façon sécurisé. 

Contenu du fichier nginx.conf :

events {
    worker_connections 2048;
}

http {

  upstream docker-kibana {
	server kibana:5601;
  }

  upstream docker-elasticsearch {
	server elasticsearch:9200;
  }
  
  server {		  	
    listen 80;
	
    location / {
      proxy_pass 		 http://docker-kibana;
	  auth_basic		 "Access limited";
	  auth_basic_user_file /etc/nginx/.htpasswd;
    }
	
	location /api/es {
      rewrite ^/api/es(.*) /$1 break;
	  proxy_pass 		 http://docker-elasticsearch;
	  auth_basic		 "Access limited";
	  auth_basic_user_file /etc/nginx/.htpasswd;
    }
	
	location = /favicon.ico {
	   log_not_found off;
	}	
  }
}

Un peu de details… ! Dans un premier temps nous avons besoin de définir deux “upstream”, c’est à dire les endpoints vers lesquels le reverse proxy dirigera les requêtes provenant de l’hôte Docker. Ces endpoints sont les conteneurs Elastic Search et Kibana. Ce routing est effectué au sein du réseau interne Docker, en effet c’est le conteneur nginx qui a besoin de communiquer avec les conteneurs Elastic Search et Kibana, c’est pourquoi nous devons spécifier les ports internes des conteneurs.

  • kibana:5601
  • elasticsearch:9200

Ensuite, il est nécessaire de définir la configuration du proxy nginx : le port qu’il doit écouter par défaut (80) ainsi que les routes qu’il expose (avec le mot clef location). Pour chaque route exposée, il est nécessaire de définir deux éléments :

  • Le endpoint (upstream) vers lequel la route redirige (avec le mot clef proxy_pass)
  • La configuration de l’authentification “Basic” en utilisant les mots clefs auth_basic_user_file et auth_basic.
    • auth_basic_user_file spécifie le chemin du fichier dans lequel le login et le mot de passe de connexion sont stockées
    • auth_basic définit le message affiché par la popup de connexion

Dans notre exemple, nous définissons deux routes :

  • / (racine) : Redirige vers le conteneur Kibana
  • /api/es : Redirige vers le conteneur Elastic Search

.htpasswd

Ce fichier contient les informations de connexion pour l’authentification « Basic ».

Contenu du fichier :

tranise:$apr1$SDzAe541$QOrVvoHMR0BLPGDDxiOSM0

Ce générateur en ligne peut être utilisé pour l’encodage du mot de passe. Dans l'exemple ci-dessus, le mot de passe encodé est test.

Docker compose

Maintenant que nous avons configuré l’ensemble de conteneurs, nous pouvons créer notre fichier Docker Compose. Ce dernier va définir comment les conteneurs seront « instanciés » sur l’hôte Docker.

Il définit les éléments suivants :

  • Un réseau nommé “net”, ce dernier est commun à tous les conteneurs
  • Un volume nommé “esdata” (géré par le driver local)
  • 3 services
    • Elasticsearch
      • Utilise le réseau “net”
      • Monte le volume “esdata” sur son prope système de fichier, dans le dossier /usr/share/elasticsearch/data
      • Utilise une variable d’environnement indiquant à Elastic Search de s’exécuter en mode « single node »
    • Kibana
      • Utilise le réseau “net”
      • Utilise deux variables d’environnement pour définir le nom du serveur et l’instance Elastic Search à utiliser
    • Proxy
      • Utilise le réseau “net”
      • Définit un mapping de port : les requêtes provenant de l’hôte de Docker sur le port 8080 seront routées vers le port 80 du conteneur. C’est le point d’entrée du serveur de log.

Contenu du fichier docker-compose.yml:

version: '3'

services:
 elasticsearch:
   build : ./elasticsearch
   container_name: elasticsearch
   environment:
     - "discovery.type=single-node"
   networks:
     - net
   volumes:
     - esdata:/usr/share/elasticsearch/data

 kibana:
   build : ./kibana
   container_name: kibana
   environment:
     SERVER_NAME : kibana
     ELASTICSEARCH_URL : http://elasticsearch:9200
   networks:
     - net

 proxy:
   build : ./nginx
   container_name : proxy
   ports:
     - "8080:80"
   networks:
     - net

volumes:
   esdata:
     driver: local

networks:
   net:

Déploiement local du serveur de log

Pour lancer et utiliser le serveur de log, il nous suffit d’utiliser le fichier docker-compose.yml. Les deux commandes suivantes doivent être exécutées :

  • docker-compose build
  • docker-compose up

Si on essaye maintenant d’accéder au conteneur nginx sur le port 8080 (http://localhost:8080) nous somme bloqué par un popup d’authentification : 3.png

Une fois les informations d’authentification renseignées, nous accédons au portail Kibana :

4.png

Pour accéder directement à l’instance Elastic Search, la route /api/es peut être utilisée :

5.png

Utilisation du serveur de log depuis une application ASP net Core

Seri Log permet de configurer un endpoint Elastic Search pour stocker les logs d’une application ASP net core.

Il suffit d’utiliser les deux packages Nuget suivants :

  • Extensions.Logging
  • Sinks.Elasticsearch

La configuration du logger est faite dans le fichier startup.cs, plus précisément dans la méthode Startup(IHostingEnvironment env). L’idée est de créer une instance du logger configurée pour envoyer les logs vers une instance Elastic Search :

            Log.Logger = new LoggerConfiguration()
            .MinimumLevel.Debug()
            .Enrich.FromLogContext()
            .WriteTo.RollingFile(Path.Combine(env.ContentRootPath, "Logs", "log-{Date}.txt"))
            .WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("http://localhost:8080/api/es"))
            {                
                AutoRegisterTemplate = true,    
                IndexFormat = "testlogs-{0:yyyy.MM.dd}",
                TypeName = "logs-api",
                ModifyConnectionSettings = x => x.BasicAuthentication("tranise", "*********")
            })
            .CreateLogger();

Bien évidemment, les informations d’authentification (login / password) doivent être stockées au minimum dans un fichier de configuration, aux mieux dans un Azure Key Vault.

 

Ensuite, il suffit d’ajouter le logger SeriLog fraîchement configuré dans la collection de logger Asp net via la méthode Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) :

loggerFactory.AddSerilog();

Nous pouvons maintenant utiliser notre serveur de log depuis l'application ASP Net core. Il ne reste plus qu'a configurer le format de l'index Elastic Search à utiliser : testlogs-*.

Dans cet article, nous avons vu comment mettre en place un serveur de log dans un environnement local. L’objectif est bien entendu d’héberger ce dernier sur un serveur distant. C’est ce que nous découvrirons dans le prochain article !

Happy coding :)

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus