Docker – deploiement avec gitlab-runner

On a réussi à mettre en place des constructions conteneurisées avec docker in docker dans l’article précédent. Le fait d’utiliser docker in docker permet de rendre la construction repétable puisque même l’outil de construction (ici docker) est sous forme se conteneur. On ne depend pas d’une installation préalable sur le système qui exécute la construction. Nous définissons dans le mécanisme de construction le systeme de construction lui-même.

L’autre avantage est d’isoler chaque construction. Ainsi on peut avoir plusieurs constructions d’un même projet en parallèle sans avoir a ce soucier d’un même port exposé sur l’hôte. Pas besoin d’avoit un port par construction: si cela reste possible par branche, 2 commit successifs sur une même branche vont utiliser le même port donc il faut séquencer les construction etc. Dind permet d’avoir l’esprit tranquille.

Déploiement continu

Dans mon environnement d’exécution, une application disons le blog de cerf-volant est distanciée une seule fois. Je pourrais être tenté de n’avoir qu’une application instanciée autant de fois qu’il y a de blog par exemple. Mais chaque blog wordpress est légèrement différents avec des plugins différents et cette personnalisation est souvent longue donc il est plus judicieux d’avoir un service déjà personnalisé avec des images déjà personnalisées. Finalement je préfère avoir autant d’application (docker-compose.yml) qu’autrefois j’avais de VM.

La conséquence est que le déploiement est plus simple. Un application ne se deploie qu’une fois donc son port exposé sur l’hôte (la VM de production) lui est dédié. Dans ce cas docker in docker ne sert plus à rien. On gagne à passer par le démon docker de l’hôte pour lancer les services.

C’est pourquoi selon l’étape (stage) de la construction j’utilise dind ou pas. J’identifie 4 étapes essentielles:

  1. build
  2. test
  3. staging
  4. production

Les 2 premiers doivent se faire en environnent isolé c’est-à-dire avec docker in docker tandis que le 3e consiste en un déploiement automatisé en environnement de preprod et le dernier est un déploiement manuel en production.

Outils de déploiement

Le deploiement consite à lancer un service docker sur la VM de prepod ou de prod. Est-ce que les outils déjà rencontré jusque jusque-là peuvent répondre à ce besoin ? docker CLI, docker-compose et gitlab-runner sont-ils suffisants ?

Le lancement du service consiste en:

  1. rapatrier la définition du service sur le serveur de prod ou de preprod
  2. lancer le service avec docker-compose

L’étape 1 revient à rapatrier une copie des sources et a seconde étape revient utiliser docker-compose sur le demon docker de la VM de prod ou preprod.

Or ces fonctions sont remplies par gitlab-runner. Il suffit simplement de l’utiliser avec un job (construction) qui n’utilise pas dind mais le démon docker de l’hôte. On va utiliser la même configuration qu’en Integration: un executor de type docker. Il suffit pour cela de configurer le runner pour passer par le partage de socket (monter la socket de l’hôte comme socket docker du conteneur de construction (docker-compose).

Finalement la VM de prod ou de preprod n’aura comme prérequis que l’installation d’un docker engine et gitlab-runner dockerisé dans lequel on va créer le runner de déploiement.

Les aspects de sécurités devront être investigués: authentification du serveur gitlab voire du client gitlab-runner

Installation du serveur de préprOD

Il faut créer une VM avec un partitionnement LVM2 afi de pouvoir étendre l’espace disque de la VM si besoin. Ma configuration de départ est une VM à 4 Go de RAM et 20 Go de disque avec comme OS une débian 9.9.0.

  1. Installer docker selon la documentation officielle
  2. Installer docker-compose pour pouvoir installer le gitlab-runner de déploiement (même si on pourrait s’en passer et utiliser une version conteneurisé de docker-compose !)
  3. Télécharger la configuration du service du gitlab-runner sur la machine de préprod. Dans mon cas comme elle se trouve sur mon dépot git, je fais un clone….
version: "3.7"
services:
    docker:
        image: 'gitlab/gitlab-runner:alpine'
        restart: always
        volumes:
            - '/srv/gitlab-runner/config:/etc/gitlab-runner'
            - '/var/run/docker.sock:/var/run/docker.sock'

Configuration du runner de déploiement

Lancer le service gitlab-runner par la commande suivante dans le répertoire où se trouve le fichier docker-compose.yml, disons que ce répertoire s’appelle runner

docker-compose up -d

Voilà le serveur de préprod est presque prêt. Il ne reste qu’a instancier le runner dédié au déploiement dans cet environnement de préprod. Le conteneir gitlab-runner est un service qui prend en charge la création des runner.

Si le répertoire du fichier docker-compose.yml s’appelle runner alors le conteneur s’appelle runner_docker_1. On va s’y connecter par

docker exec -it runner_docker_1 bash

Puis entrer la commande:

gitlab-runner register

Les différents paramètres ont été vus dans le billet précédent et ne changent pas. En revanche le tag devra être renseigné à une valeur qui permettra de spécifier que le runner que l’on est en train de créer est dédié à la preprod. Utilisons par exemple le tag staging.

Quitter le shell bash pour revenir sur le shell initial de notre VM de préprod. Comme on persiste la configuration du gitlab-runner sur l’hôte, on va pouvoir aller la modifier pour relier la socket de docker du conteneur créé par le runner à la socket l’hôte. C’est par ce biais que le docker CLI (de l’image docker ou docker-compose) va communiquer avec le docker engine de l’hôte.

nano /srv/gitlab-runner/config/config.toml

Dans la section volumes apporter la modification suivante:

volumes = ["/var/run/docker.sock:/var/run/docker.sock","/cache"]

INSTLLATION DU SERVEUR DE PRODUCTION

Les actions sont identiques à l’exception qu’il faut utiliser un autre tag disons production à la place de staging.

Configuration du build

Le fichier .gitlab-ci.yml de mon projet blog prend finalement alors la forme ci-après. J’ai utilisé une combinaison de tag plus précise afin de spécifier les runner adaptés à chaque étape. Les tag peuvent être modifiés dans l’interface administrateur de gitlab.

 image:
    name: docker/compose:1.24.0
    entrypoint: [""]



 before_script:
   - docker info
   - docker-compose version

 stages:
   - build
   - test
   - staging
   - production
   
 

 run_blog:
   stage: test
   tags:
     - docker
     - dind
   variables:
    # For non-Kubernetes executors, we use tcp://docker:2375/
    DOCKER_HOST: tcp://docker:2375/
    # When using dind, it's wise to use the overlayfs driver for
    # improved performance.
    DOCKER_DRIVER: overlay2

   services:
     - docker:dind     
   script:
     - echo "run blog for testing"
     - docker-compose up -d
     - docker-compose ps

 #verify_running:
 #  stage: test
 #  tags: 
 #  image: golang # because I know it has curl installed
 #  script:
 #     - sleep 120
 #     - curl -v http://docker:8180
                         
    
 deploy_to_staging:
    stage: staging
    tags:
        - staging
        - docker
    script:
        - echo "deploy to staging"
        - docker-compose down
        - docker-compose up -d
    only:
    - master        
 
 deploy_to_production:
    stage: production
    tags:
        - production
        - docker
    script:
        - echo "deploy to production"
        - docker-compose down
        - docker-compose up -d
    only:
    - master
    when: manual

La prochaine étape sera de mettre sous déploiement continue le conteneur initial gitlab-runner qui a dû être installé manuellement. Ensuite je m’attacherai à la personnalisation de l’image wordpress et ce sera l’occasion de creuser les mise en place du proxy de registry et de relier les dockers engines (de ma machine hôte et ceux des VM de préprod et de prod) à mon propre registry.

Docker – gitlab-runner conteneurisé et dind

J’ai choisi de mettre en place l’intégration continue avec un gitlab-runner conteneurisé et Docker in docker (Dind) pour lancer les commandes docker.

L’avantage du gitlab-runner conteneurisé est de ne pas se soucier de l’installation sur le système hôte qui effectue la construction. Gitlab-runner est un processus client qui communique avec le serveur gitlab afin de récupérer les constructions à faire.

Docker in docker permet de démarrer un démon docker (docker engine) vierge auquel va se connecter le docker CLI (ou docker-compose) pour lancer les conteneurs. Les 2 grosses flèches dans le schémas suivant signifient « lance un processus »

Gitlab-runner en service

Le démon gitlab-runner est lancé en service docker via docker-compose. Dans un repertoire disons runner créer le fichier docker-compose.yml suivant:

version: "3.7"
services:
    docker:
        image: 'gitlab/gitlab-runner:alpine'
        restart: always
        volumes:
            - '/srv/gitlab-runner/config:/etc/gitlab-runner'
            - '/var/run/docker.sock:/var/run/docker.sock'

Le premier volume permet de rendre persistant la configuration tandis que le second permet de se connecter au démon docker de l’hôte. Ainsi le gitlab-runner va pouvoir lancer des conteneurs frères via docker CLI.

Lancer le service par

docker-compose up -d

Il faut ensuite connecter le runner avec gitlab. Pour cela la documentation du gitlab-runner conteneurisé propose de lancer un autre conteneur temporaire qui partage le même répertoire de config persistant sur l’hôte. A par de montrer que les conteneurs peuvent partager un même repertoire, je trouve que cela complique la compréhension. Il est plus simple de se connecter au conteneur que l’on vient de lancer et d’y effectuer la tâche d’administration consistant à enregistrer un nouveau runner.

le démon gitlab-runner peut gérer plusieurs runner.

Determiner le nom du conteneur en faisant un

docker ps

La sortie doit contenir une ligne de la forme

746a1ebc7908 gitlab/gitlab-runner:alpine "/usr/bin/dumb-init …" 3 days ago Up 3 days runner_docker_1

Disposant de cette information, on peut lancer un shell dans le conteneur par

docker exec -it runner_docker_1 bash

On dispose alors d’un shell en tant que root. On doit y entrer la commande:

gitlab-runner register -n --url \n

https://gitlab.bressure.net \n

--registration-token xxxxxxx-yyyyyyy \n

--executor docker \n

--description 'docker runner' \n

--docker-image 'docker:18.09.06' \n

--docker-privileged

et renseigner les informations obligatoires suivantes

  • serveur gitlab: https://gitlab.bressure.net
  • token de connexion: voir dans l’admin du serveur gitlab pour obtenir un token pour un runner partagé
  • type de d’executor: docker
  • image par defaut: docker:18.09.06
  • utiliser le mode privilégié

Construction docker-compose

L’application est un blog worpdress composée d’un service wordpress et d’un service mysql. Pour mémoire voici son fichier docker-compose.yml

version: '3.3'

services:
   db:
     image: mysql:5.7
     volumes:
       - db_data:/var/lib/mysql
     restart: always
     environment:
       MYSQL_ROOT_PASSWORD: somewordpress
       MYSQL_DATABASE: wordpress
       MYSQL_USER: wordpress
       MYSQL_PASSWORD: wordpress

   wordpress:
     depends_on:
       - db
     image: wordpress:latest
     volumes:
       - www_data:/var/www/html
     ports:
       - "8180:80"
     restart: always
     environment:
       WORDPRESS_DB_HOST: db:3306
       WORDPRESS_DB_USER: wordpress
       WORDPRESS_DB_PASSWORD: wordpress
       WORDPRESS_DB_NAME: wordpress
volumes:
    db_data: {}
    www_data: {}

Comme il n’y a pas d’utilisation d’image docker personnalisées, la construction se limite la phase de test pour vérifier que l’application démarre. On utilise donc l’image docker/compose pour lancer principalement la commande docker-compose up

Voici le fichier CI de gitlab pour le projet blog

 image:
    name: docker/compose:1.24.0
    entrypoint: [""]



 before_script:
   - docker info
   - docker-compose version

 

 run_blog:
   stage: test
   tags:
     - docker
   variables:
    # For non-Kubernetes executors, we use tcp://docker:2375/
    DOCKER_HOST: tcp://docker:2375/
    # When using dind, it's wise to use the overlayfs driver for
    # improved performance.
    DOCKER_DRIVER: overlay2

   services:
     - docker:dind     
   script:
     - echo "run blog for testing"
     - docker-compose up -d
     - docker-compose ps

                         
    

Performances

Ce premier essai avec Dind révèle une première caractéristique. Chaque lancement d’une construction semble utiliser un nouveau conteneur docker. En tout cas il n’y a aucune persistance des images téléchargées. Ainsi à chaque exécution déclenchée par une modification de source (ici le fichier docker-compose.yml) les images de wordpress et mysql sont téléchargées. Avec ma connexion cela prend 5 minutes à chaque fois !

L’utilisation d’un proxy par le démon docker (dockerd) pourra être une solution que je mettrai en œuvre dans le prochain billet qui sera aussi l’occasion de faire du déploiement automatisé en environnement de préprod.

 

Docker – CI/CD avec GitLab

Nous avons un Gitlab offrant les services de dépôts de sources et d’images docker mis en place dans le billet Docker – Registry avec GitLab. L’étape suivante sera de mettre en oeuvre l’integration continue et par la suite le deploiement continu.

Répétablité de la construction

La construction doit être un processus répétable. Même en conservant les sources et les dependances, le système exécutant la construction n’est pas à l’abri de changement. Ainsi le maven doit être figé, ses outils et paramétrages également. Cela permet de reproduire la construction ailleur. C’est là que docker rentre sur la scène : on va dockeriser la construction.

Docker pour construire

Docker permet de mettre en boîte une application (code, dependance et paramétrage). Cela assure une exécution identique où que le code s’executera ensuite.

Comme notre but est de lancer des commandes docker dans la phase de construction, docker doit lancer du docker. D’un point de vue technique l’integration continue doit lancer un conteneur docker qui est lui-même docker.

Une double couche

C’est là où on entrevoit le changement de paradigme dans l’approche de l’exécution d’un programme. Le principe n’est pas nouveau et on peut faire une analogie avec Java.

Analogie avec Java

Java offre une promesse d’exécution identique quelque soit la platforme. Pourvu que l’on ait une JVM. Si le but de mon application est d’executer du code Java en entrée, il est sensé d’écrire une JVM en Java. Ainsi l’exécution d’un programme devient indépendante de la JVM hôte puisque celle-ce exécute une JVM. Pourquoi est-ce mieux ? pourquoi ne pas simplement exécuter le code java en entrée sur la JVM hôte ? Parce que le code java en entrée va avoit les effets de bord de la JVM hôte comme par exemple les librairies endorsed…

Performances

L’idée n’est pas nouvelle mais là où l’émulation d’une JVM par une JVM va entrainer des lenteurs car la seconde JVM sera exécutée par un code Java contrairement à la JVM hôte qui est en code natif du système et qui exécute la JVM émulée, avec Docker on a beaucoup moins ce problème.

En effet l’application dans le conteneur s’exécute à vitesse réelle sur le noyau de l’hôte. L’isolation se fait par le noyau de l’hôte via des techniques d’isolation du système de fichiers (chroot), de la memoire et des processus (cgroup) etc… docker c’est de l’isolation de processus et non pas de la virtualisation de materiel.

Docker in docker

Il existe une image docker de docker appelée Docker in docker (dind). L’objectif sera donc de faire en sorte que GitLab lance les constructions via ce docker conteneurisé.

Gitlab et le CI décentralisé

L’implémentation de l’intégration continue dans GitLab utilise un mecanisme client-serveur. Des processus clients appelés Gitlab-runner s’executent sur une machine de construction: le serveur hébergeant GitLab ou une tout autre machine.

Le client gitlab-runner peut au choix et entre autre d’executer

  1. une commande shell quelconque
  2. ou bien une commande docker via Dind

Le second exemple est un cas particulier où le gitlab-runner exécute une commande de construction via docker. Au lieu d’utiliser docker pour lancer un maven conteneurisé, par exemple, on va utiliser docker pour lancer un docker conteneurisé.

Il permet également de factoriser l’installation de docker utilisé. Toutes les constructions, quel que soit leur lieu, se feront de la même manière puisque conteneurisées !

L’installation du gitlab-runner sur le système qui va exécuter la construction peut elle-même être conteneurisée. Cette solution me paraît interessante dans l’optique de migrer à terme tous les composants de l’atelier logiciel (Gitlab, registry, gitlab-runner) sur une VM à conteneurs dédiée indépendante de ma machine hôte afin de réserver à cette dernière l’unique fonction de station de travail.

Au prochain billet, la mise en œuvre !

Docker – Registry avec GitLab

La registry est un dépôt d’images docker. Son utilisation est centrale dans l’architecture que je veux metttre en place comme indiqué dans Docker – un pas vers la registry. Il restait le choix d’utiliser Gitlab en tant que Registry ou bien l’implementation conteneur registry de docker même.

Sauvegarde et sécurité

Le registry sera un élément de l’infrastructure et en tant que tel il devra être sauvegardé. Or le service Gitlab est déjà defini avec un mapping vers le systèmes de fichier hôte. Sauvegarder ce montage permet de sauvegarder toutes les données de Gitlab: sources et registry !

De plus l’utilisation du registery impose de s’y connecter en HTTPS. Le client docker (docker CLI) est configuré ainsi par défaut. Je vais garder ce paramétrage qui sera fort utile dans le cas où je voudrais déplacer mon registry ailleur que sur localhost.

Il faut donc mettre en place un frontal pour faire le TLS, or mon service gitlab est déjà dernier un proxy SSL.

Enfin mon besoin de limiter les droits des accès de la production et de la préprod en lecture au registry et dépôt de source est déjà pris en charge par des mécanismes de clé et de jeton de déploiement.

Toutes ces considérations me font choisir l’utilisation du registry intégré à Gitlab.

Mise en oeuvre

Sa mise en oeuvre est très simple. Le fichier docker-compose.yml devient:

version: "3.7"
services:

 web:
   image: 'gitlab/gitlab-ce:latest'
   restart: always
   hostname: 'gitlab.bressure.net'
   environment:
     GITLAB_OMNIBUS_CONFIG: |
       external_url 'https://gitlab.bressure.net'
       nginx['listen_port'] = 80
       nginx['listen_https'] = false

       registry_external_url 'https://registry.bressure.net'
       registry_nginx['listen_portex'] = 80
       registry_nginx['listen_https'] = false
       # Add any other gitlab.rb configuration here, each on its own line

       gitlab_rails['gitlab_shell_ssh_port'] = 2222
   ports:
      - '8080:80'    
      - '2222:22'
      
   volumes:
     - '/srv/gitlab/config:/etc/gitlab'
     - '/srv/gitlab/logs:/var/log/gitlab'
     - '/srv/gitlab/data:/var/opt/gitlab'

Il suffit déclarer l’url externe du registry pour que gitlab affiche le lien dans l’interface du projet. Voici le schéma d’architecture :

Il permet l’utilisation suivante:

  1. sur la machine hôte on développe avec un éditeur
  2. COMMIT en local
  3. exécute en local et dans le cas particulier qui me concerne on lance un conteneur dit de DEV en local
  4. on verifie le fonctionnement attendu (test)
  5. on pousse les source sur GitLab par un PUSH
  6. on pousse eventulement l’image dans le registry

On remarque la mise à disposition de la preprod et à la prod des livrables se fait par 2 biais selon le type de livrable:

  • repository Git (Gitlab) pour les applications afin d’obtenir leur composition en terme de services ie en terme d’images. Il s’agit concrètement récupérer les fichiers docker-compose.yml
  • registry docker (Gitlab) pour les images. Je fais finir par y arriver quand les images actuelles ne me suffiront plus…

CI /CD

On remarque que les étapes 3,4 et 6 pourraient être effectuées par l’Integration Continue (IC). En effet une fois poussée sur Gitlab, le code doit passer des tests. Si ces derniers sont concluants, un livrable doit être produit (jar, ear, image docker….) et dans la cas d’application on peut même déployer directement en preprod.

Cela ouvre la voie vers l’exploration de la construction.

A suivre dans le prochain billet

Docker – Un pas vers la registry

Dans l’article précédent Docker – le réseau je suis arrivé à la conclusion de mettre en place 2 VM hébergeant chacune une infrastructure de service complète. L’une devra être la copie de l’autre et servira d’étape de validation préproduction avant d’installer réellement en production.

Afin d’assurer la prédictibilité de le l’exécution, ces 2 VM ne devront faire aucune construction d’image mais utiliser des images pré-construites mises à disposition dans ce que Docker appelle une registry dont Docker Hub est un exemple.

Registry autohebergée

Dans le schéma ci-dessus Gitlab sert de dépôt Git mais les VM de prod et preprod ne devront pas s’en servir que pour rapatrier les définitions des applications (docker-compose.yml) et ces dernières ne devront pas comporter des instructions de construction d’images mais uniquement des references vers les images récupérées dans le registry privé.

Je ne sais pas encore comment interdire cela. Peut-être en mettant des controle lors du push des fichiers de docker-compose.yml et en interdisant aux machines de prod et preprod d’accéder aux répertoires autres que ceux des applications au sein du dépôt Git.

Automatisation des constructions

Gitlab est également une plateforme d’intégration continue et de déploiement continu. Je vais pouvoir m’en servir pour construire mes images docker et déployer les applications en preprod. En investiguant le sujet j’ai decouvert que Gitlab est aussi une registry… à voir si je passe tout sur Gitlab ou bien je garde le registry de docker.

La suite au prochain billet…

Docker – le réseau

J’ai mis en oeuvre mon premier conteneur hier, une instance de Gitlab que j’ai exposé sur internet. La question de la configuration du réseau et la façon dont docker s’intègre se pose. L’objectif est de limiter les configurations et les compétences requises afin de montrer que c’est accessible.

Rappel sur un reseau domestique

La configuration du schéma précédent est commune dans les accès internet grand public. Comme évoqué dans le billet De la virtualisation à la conteneurisation ce reseau domestique peut peut-être utiliser pour accueillir des services autohébergés.

Avec la mutualisation comme les services sont sur la même IP, les conflits de port surviennent. Même si on pourrait definir plusieurs interfacex reseaux ou attribuer plusieurs IP à la même interface, cela complexifie la configuration de l’hôte et le routage au niveau de la box qui possède l’ip publique. Au final pour rester simple avec une seul IP sur l’hôte, on expose autant de ports qu’il y a de services.

Avec certains protocoles comme HTTP il est possible de virtualiser les serveurs basés sur le nom de domaine. Dans le DNS il suffit d’associer plusieurs noms de domaine à la même IP publique. On diminue ainsi le nombre de ports exposés sur l’IP publique.

Le paramétrage des services pour changer leur port d’écoute n’est pas le plus compliqué mais les conflits de librairies et de paramétrage des applications sur le même hôte fini par limiter l’agilité de la solution mutualisé. La virtualisation apporte des solutions.

La configuration de chaque service est autonome ainsi les virtualappliance permettent de dupliquer les services et la couche de virtualisation quemu/kvm crée automatiquement un réseau pour les VM. Ainsi on a une IP par service. Cette IP est attribuée par adresse MAC et même si elles ne sont pas visibles du réseau local, elles sont fixes et sont accessibles par l’hôte.

Reste le problème des ports de l’hôte. On peut là aussi mutualiser certains ports pour les services basés sur le nom de domaine. Dans le cas contraire il faut ouvrir un nouveau port sur la box. Ce port devra être redirigé vers l’IP de la VM. Par défaut cette IP est interne à l’hôte et non visible du reseau local (depuis le routeur de la box) donc il faudrait faire une redirection des paquets depuis l’hôte ou bien configurer la VM en mode bridge pour qu’elle ait une IP dans le réseau local et permettre de faire une redirection depuis le routeur vers l’IP de la VM.

Dans tous les cas il faudra ouvrir un nouveau port sur la box: un part instance de service. Avec le reroutage local à l’hôte la VM reste cachée derrière l’hôte et dans l’autre la VM sera visible du reseau local.

Avec les conteneurs docker

Le dernier schema vu plus haut reste vrai avec la configuration par défaut quand on utilise docker directement sur la machine hôte. On remplace les VM par des conteneurs.

On peut toujours mutualiser les ports 80 et 443 en déployant autant de services HTTP que voulu derrière le reverse proxy. On remarque déjà que les IP1 et IP2 associées à chacun des conteneurs ne sont pas utilisérs par le reverse proxy même si l’hôte les voit. Ces IP sont ephémères et changent d’un redémarrage à l’autre du service.

Docker expose chaque services sur un port de l’hôte.

On revient à l’organisation d’un serveur mutualisé… c’est d’ailleurs ce que l’on voulait en passant de la virtualisation à l’isolation autre terme pour parler de conteneurisation.

Conteneur virtualisé

On va avoir une concurrence entre les applications docker pour l’attribution des ports de l’hôte. Or la machine hôte est également une machine de développement. Nous sommes en informatique résidentielle. Il faudra séparer les réseaux en gérant soi-même 2 réseaux sur l’hôte avec 2 IP l’une pour le dev l’autre pour la production. Outre la compétence supplémentaire, la configuration de dev sera differente de celle de production. Ce qui n’est jamais bon et qu’on cherche à éviter. Cela est encore plus vrai si on veut avoir un environnent de préprod iso-prod. Une autre machine identique en guise d’hôte…. la virtualisation répond à ce besoin.

Faire tourner docker en machine virtuelle

L’utilisation de docker en VM est d’ailleurs un moyen de permettre la scalabilité. Au lieu d’augmenter la mémoire et le nombre de process sur même hôtes, on finit par trouver plus simple d’augmenter le nombre d’hôtes et distribuer les traitements à travers ceux-ci.

On voit que d’un point de vue de l’hôte (rectangle arrondi), les services conteneurisés dans une VM sont accessible par une IP spécifique car exposés par docker sur un port de l’hôte sur lequel il tourne, c’est-à-dire avec une IP fournie par la couche de virtualisation quem/kvm.

L’étape ultime sera pour moi d’avoir sur une VM tous les services du domaine bressure.net, ainsi j’aurais totalement séparé  par virtualisation  et conteneurisation les applications qui n’ont rien à faire sur un poste de travail. A partir de là, tous ces services pourront être hébergé ailleurs ou bénéficier d’une ré-allocation sur des ressources physiques plus grande sans être limité à l’hôte.

La suite au prochain billet.

 

Docker – sautons le pas

Docker est sans doute la plus populaire des solutions de conteneurisation du moment. Elle a l’avantage d’être assez mature puisque elle existe depuis 2013. Par ailleurs de nombreux outils et une documentation suffisante existe c’est un projet très actif pour l’outillage complémentaire contrairement à Lxc.

Si dans l’article précédent https://blog.bressure.net/index.php/2019/05/24/de-la-virtualisation-kvm-a-la-conteneurisation-docker/ je présentais des idées générales, dans le présent article je vais illustrer par un cas pratique de mise en œuvre étant moi même le cobaye de cette expérience de migration de VM au conteneurs avec docker.

Pas convaincu

Pour l’histoire je viens de finir d’être convaincu que la conteneurisation est supérieur à la virtualisation en terme de prédictibilité d’exécution. Comme je suis débutant avec Docker, je vais sans doute faire des erreurs et un environnement de développement avec au minimum Git est nécessaire. Pour cela, je me suis tourné vers une Virtual Appliance turnkeylinux Gitlab. Je pensais comme cela avoir une solution sur étagère pour m’offrir le service de gestion de source moderne avec éditeurs etc etc.

Malheureusement, avec une VM de 2 Go de mémoire adressable, mon Gitlab plante sur erreur 500. En passant à 3 Go même erreur. Comme je n’ai pas le temps d’investiguer, alors je me suis installé une gitlab dans docker. Et là ça a marché du premier coup !!!

Docker pour peu que les images prises soient récentes honore bien le principe de prédictibilité de l’exécution.

Revenons à mon cas

L’objectif est de migrer une quinzaine de VM constituant mes serveurs tous hébergés sur mon gros PC Linux/Debian qui fait également office de PC de bureau. Certes je m’en sers très peu, c’est donc plus un serveur mais par période il m’arrive de l’utiliser et….

Utilisation mémoire de mon serveur

… l’image ci-dessus montre de l’overcommit de mémoire et du swap malgré mon passage de 16 à 32 Mo de RAM. Entre-temps, le nombre de VM avait bien augmenté également.

VM et mémoire hôte uilisée

Récemment j’ai installé ElasticSearch et Owncloud, la mémoire de mon PC est mise à rude épreuve. Et finalement le seuil d’overcommit que je me suis fixé a été atteint aujourd’hui.

Overcommit et swap, mon serveur est à la peine

Feuille de route

La migration a pour objectif de:

  • Réduire le nombre de VM (moins d’administration etc)
  • Réduire l’empreinte mémoire des services (blog divers, owncloud etc)
  • Répondre au besoin de sauvegarde comme à l’heure actuel
  • Répondre au besoin de monitoring comme à l’heure actuel
  • Ajouter la possibilité d’avoir une préproduction complète

En option:

  • Permettre la scalabilité des services
  • L’exécution des services dans le cloud

Préliminaires

Après avoir parcouru la documentation de docker, voici les étapes à faire:

  1. Installer les dépôt docker
  2. puis docker
  3. enfin docker-compose

Toutes ces actions sont décrites dans la documentation et ce n’est pas mon propos de la dupliquer ici. Je précise en revanche leur intérêts:

Le dépôt docker plutôt que ceux de Débian afin d’avoir la dernière version de docker. Docker lui-même permettra d’exécuter des images chacune offrant un service tandis que docker-compose permettra d’agréger des services entre eux afin de constituer une application.

VOCABULAIRE

On voit que j’utilise les VM comme réceptacle pour accueillir un service ou une application : une application wordpress pour offrir un service de blog par exemple. Turnkeylinux est ainsi fait: une VM qui contient toute l’application.

Avec Docker, il faut oublier la notion de VM et surtout la relation 1-1 VM-application

Avec docker, les services sont des éléments unitaires logiciels qui s’exécutent dans un conteneur. La granularité est importante car la bonne pratique dans docker est de conserver au service la granularité la plus fine possible. Par exemple un service sera dans mon cas la couche web wordpress. Je vais donc avoir besoin d’un service de base de données en plus. Ces 2 services devront être agrégés pour constituer mon application wordpress fonctionnelle.

Heureusement que les services de bases sont déjà disponibles et c’est ce qui permet à tout un chacun de créer des applications en agrégeant des services pris sur l’étagère. Ces services sont distribués sous forme d’image docker.

Premier conteneur GItlab

Comme je l’indiquais plus haut le premier conteneur qui sera installé sur mon serveur debian, sera Gitlab pour avoir l’outillage de développement.

Faire un premier test en lançant en tant que root dans un terminal

# docker run gitlab/gitlab-ce

La première fois c’est long car il faut que docker télécharge l’image qui fait plus de 600 Mo. Quand c’est finit gitlab va envoyer une sortie de ce genre

GItlab en fonctionnement

On vient de lancer le conteneur avec aucune option. C’est juste pour la premier essai afin de prendre confiance dans en docker. On va se connecter à l’application par l’adresse ip du conteneur même. Prendre un autre terminal lancer la commande suivantes afin de voir le conteneur gitlab:

# docker container ps

Ensuite déterminer l’identifiant du réseau bridge qui est celui auquel est connecté le conteneur lancé sans aucune option.

# docker network ls

Puis regarder le réseau bridge

# docker network inspect 4f1346b59d99

Le conteneur possède l’ip 172.17.0.2 et quand on ouvre un navigateur sur cette ip depuis la machine qui a lancé la commande docker run gitlab/gitlab:ce alors on arrive sur notre instance gitlab.

GITLAB PERSISTENT

Le conteneur gitlab que l’on a lancé précédemment ne conserve pas les informations en cas d’arrêt/redémarrage. Une notion importante à garder à l’esprit, un conteneur n’est pas une VM c’est seulement l’exécution d’un service (d’un programme) dans un espace isolé. Tant que le conteneur s’exécute il écrit sur le disque dans on espace mais quand il s’éteint, tout disparaît. C’est ce que docker appelle l’éphémérité.

La plupart des services doivent être persistent. Heureusement permet de relier des répertoire du système de fichier du conteneur à un point sur la machine hôte.

Commençons par arrêter le conteneurs éphémère

# docker container stop 1b166f7989b1

Préparer un fichier de définition du service gitlab docker-compose.yml

version: "3.7"
services:

 web:
   image: 'gitlab/gitlab-ce:latest'
   restart: always
   hostname: 'gitlab.bressure.net'
   environment:
     GITLAB_OMNIBUS_CONFIG: |
       # external_url 'https://gitlab.bressure.net'
       # Add any other gitlab.rb configuration here, each on its own line
   ports:
      - '8080:80'
 #    - '443:443'
      - '2222:22'
   volumes:
     - '/srv/gitlab/config:/etc/gitlab'
     - '/srv/gitlab/logs:/var/log/gitlab'
     - '/srv/gitlab/data:/var/opt/gitlab'

Comme l’ip du serveur gitlab change au grès des redémarrages du conteneur, il faut passer par l’ip de l’hôte qui est bien plus stable… Je vais par la suite utiliser une redirection depuis mon frontal apache et j’aurais alors besoin d’un ip fixe vers laquelle rediriger les flux. Comme sur l’ip de l’hôte le port 80 es déjà pris par mon apache, j’indique le port 8080 comme celui sur lequel sera exposé le conteneur gitlab sur l’hôte. De même pour le port ssh pour les commit git, le port 22 étant déjà occupé sur l’hôte, le port 2222 sera utilisé pour accéder à mon serveur git dans gitlab.

En tant que root il suffit de lancer

# docker-compose up -d

La suite

Je viens de monter mon premier service docker. La prochaine étape va consister à mettre en place la redirection web et la couche TLS dans le frontal apache ainsi que la redirection pour SSH afin de pouvoir utiliser mon serveur gitlab depuis internet avant de replonger dans le conteneurisation de mes VM.

De la virtualisation kvm à la conteneurisation docker

Le cloud est à la mode et son adoptions par les géants du web semble prouver qu’il est bien plus qu’une passade. Au sens strict une plateforme ou un service dans le cloud se definissent comme des ressources dont la localisation est secondaire. La conteneurisation prend ici tout son intérêt et pas seulement pour le GAFA. Le particulier qui héberge lui-même ses services trouve aussi un avantage à passer aux conteneurs.

Historique

Internet n’était pas à l’origine un concept centralisé. Loin s’en faut. Tout noeud du réseau, toute machine peut-être un serveur, tout devrait être au même niveau, tout devrait être en pair-à-pair. Grâce au logiciel libre, cela est possible depuis toujours.

Limite de l’infrastructure personnelle

Un particulier qui héberge son propre serveur arrivera néanmoins assez immédiatement à la limite de ressource physique: l’espace qui va limiter le nombre de machines physiques. Le coût d’acquisition ainsi que la mise en place de l’infrastructure réseau sont également des facteurs limitants.

Mutualisation

Le particulier va donc mutualiser des fonctions sur une même machine qui fera office de serveur web, de serveur de messagerie ou encore de serveur ftp à la fois par exemple. Mais:

La mutalisation engendre des conflits de version de librairies

Les logiciels s’appuyant sur des bibliothèques du système, partagent ces ressources de l’OS et des incompatibilités peuvent empêcher les services d’évoluer indépendamment les uns des autres.

La virtualisation

Heureusement que la virtualisation permet de s’affranchir des contraintes physiques et la prise en charge de la virtualisation par le processeur et l’OS (kvm, intel VT-x) permet d’avoir des performances proches d’une exécution non virtualisée.

La virtualisation au niveau hardware permet d’avoir des environnements d’exécution cloisonnés

Ainsi chaque application peut êtres dans la version de son choix avec la version d’OS de son choix et même avec le type de ressources de son choix. La virtualisation telle que kvm par exemple le permet, est une abstraction de la machine physique.

Le prix à payer est celui d’un nombre d’installations d’OS qui croit avec le nombre d’application. Dans ce modèle une application c’est une VM au moins. Une VM c’est un OS qu’il faut installer et administrer.

Limites de la virtualisation

Outres la charge d’administration système qui ne change pas par rapport une plateforme physique, la virtualisation est coûteuse en ressources physique de la machine hôte.

En effet une VM c’est une allocation d’office d’un espace disque et mémoire sur l’hôte. Ainsi une VM qui est taillée avec 1 Go de mémoire consomme 1 Go de mémoire sur l’hôte même si dans la VM son OS ne prend que 600 Mo. La technique de balloon que j’ai testé ne permet que de réduire la mémoire de l’invité à chaud. L’augmentation de la mémoire ne se fait pas à chaud et n’est pas automatisée. Une intervention humaine est nécessaire.

Il en est de même pour l’espace disque. La virtualisation définit des disques virtuels. Grâce à LVM on peut augmenter la tailles des partitions dans l’invité à chaud mais c’est une intervention volontaire comme on ne ferait sur une machine physique. De plus quand on alloue un disque de 10 Go à une VM, on crée un fichier de 10 Go sur l’hôte même si dans l’invité on utilise que 2 Go.

La virtualisation c’est un découpage d’une machine physique. Sur chaque partie, on installe un OS et les applications.

En terme d’isolation la virtualisation est championne

Simplifier la virtualisation

L’administration de multiples VM est fastidieuse. Il faut mettre à jours autant d’OS que de VM.

La création d’une VM fonctionnelle l’est tout autant. Il faut d’abord installer l’OS puis l’application et son parametrage. Heureusement grâce aux virtual appliance comme celles fournies par turnkeylinux, l’ajout d’une nouvelle application virtualisée, qui se materialise par une VM, est rapide.

Vers la conteneurisation

On a évoqué le manque de souplesse dans l’allocation des ressources. Ce problème ne se rencontrerait pas si les applications étaient toutes installées sur l’hôte en partageant les bibliothèques du système et les ressources de l’OS que sont la mémoire et le disque.

Cette idée qu’un service comme un moteur de blog par exemple devrait être ce qu’il a toujours été depuis l’origine: une application et non pas une VM, est fondamentale à mon sens pout appréhender la conteneurisation avec la bonne grille de lecture. Comme une application ne doit pas être associé à un OS dédié depuis l’avènement du multitâche…les applications doivent se partager less ressources de la machine qui les heberge via un l’OS.

Pour autant on ne veut pas retomber dans les écueils de la mutualisation et garder un cloisonnement des applications afin que chacune puisse évoluer avec des versions de librairies différentes.

C’est là que les concepts d’isolations via des techniques qui ont évoluées du chroot (1982), en passant pas lxc (2008) puis docker (2013) au sein même de l’OS rentrent en jeu. Ainsi les applications sont isolées tout en étant à un niveau de performances quasi identiques que si elles non conteneurisées. En effet tous les appels systèmes se font directement sur l’OS hôtes sans passer par un OS invité qui va ensuite appeler via la couche de virtualisation l’OS hôte. C’est plus long même si et le noyaux linux avec kvm et les cpu (ex: Intek VT-x) offrent un support de la virtualisation.

Un conteneur n’est pas une VM

Je souligne encore cette difference car il me semble assez commun de faire l’erreur. Voici les differences à bien avoir en tête.

Un conteneur execute une application qui utilise l’OS de l’hôte ou plus exactement les appels systèmes (noyaux linux) de l’hôte. De la on comprend qu’il ne sera pas possible d’executer une applications compilée pour un CPU différent ou un OS différent. On pourra exécuter une distribution différente (simple arborescence de fichiers spécifique à la distribution) mais il n’est pas possible d’executer une application Windows sur un hôte linux.

Un conteneur c’est une application. On ne définit plus des ressources physiques dans ce paradigme. L’application va utiliser à la demande la mémoire et le disque disponible sur l’hôte.

Quand passer à Docker ?

Hier.

Dans le cas où toutes VM sont des linux que l’on souhaite garder à jour c’est-à-dire dans la plus récente version disponible alors la conteneurisation est la solution pour mieux utiliser les ressources en quantité et en performance.

La paradigme des VM mono-application n’est pas optimum et represente une solution surdimensionnée au besoin d’isolations réel: a-t-on vraiment besoin d’un OS par application ?

A suivre sur ce blog, mes prochaines aventures dans le monde de docker avec la conteneurisations de mes VM.

Shell distant pour connexion instable

Pour administrer mes serveurs en déplacement rien de plus facile avec SSH et les réseaux mobiles. J’utilisais ainsi SSH via l’application ConnectBot sur Android. Si cette solution permet de dépanner elle présente quelques défauts.

Limites du SSH

Tout d’abord elle est très sensible à la qualité de la connexion. En effet le terminal est rapidement bloqué si le retour (echo) du serveur tarde à venir. Cela peut même provoquer un plantage de ConnectBot.

Ensuite quand la commande est longue comme par exemple un download ou une commande de compilation ou bien une commande de transcodage vidéo ou même une installation de paquet, une interruption de la connexion lui est fatal. On peut y remédier par l’utilisation d’un nohup mais ce dernier fait perdre la main sur la commande. De plus la commande qui part en nohup n’est plus accrochée à la connexion ssh quand le shell se termine.

La perte de connexion réseau est un réel problème pour les longues sessions de shell distant. C’est encore plus vrai même pour les courtes sessions quane la connectivité est mauvaise ou par intermittence comme dans le métro parisien !

Mosh le shell déconnecté

J’ai alors découvert Mosh. C’est un shell qui au lieu d’être connecté à un serveur ssh en TCP, envoie et reçoit ses paquets en UDP. La phase de connexion initiale se fait en SSH classique pour démarrer ensuite une session en UDP . Cette session en UDP survit aux interruptions réseaux !

Pour l’utilisateur client, mosh se presente comme un shell ssh. Quand il y a une perte de connexion, mosh indique par un message que la connexion réseaux est perdue, le terminal est alors bloqué puis quand le réseau revient, le terminal retrouve sont fonctionnement normal. Les commandes lancées sur le serveur continuent pendant la perte de connexion.

Installation

Mosh est un programme client/serveur. Il faut donc l’installer sur le serveur que l’on veut pouvoir atteindre vie un réseau intermittant. En root sur ma Debian

# apt-get install mosh

Termux le client Android le plus simple

Sur le client, il faut également installer mosh. Avec Android le plus simple est d’installer Termux. Cet emulateur de terminal fournit le paquet mosh.

pkg install mosh

L’utilisation de mosh est comme ssh

mosh user@server.net

On pourra avantageusement passer unr authentification par clé via une commande du type

mosh –ssh= »ssh -i /data/data/com.termux/files/usr/etc/ssh/ssh_host_ed25519_key » user@server.net

UserLand pour un client Linux sur Android

Si on veut rester dans un environnement familier en l’occurence Debian pour moi, le logiciel UserLand pour Android permet d’avoir une distribution Linux complète. A partir de là on installe mosh de manière classique. Pour Debian

sudo apt-get install mosh

UserLand permettra également de partager l’installation de mosh avec une session UserLand graphique. Mais le grand intérêt pour moi est de bénéficier de l’émulateur de terminal de UserLand que je trouve plus abouti que celui de Termux.

Paramétrage avec UserLand

Néanmoins les sessions Debian de UserLand sont un peu brutes. Il faut les personnaliser un peu. En effet mosh nécessite une locales UTF-8.

sudo apt-get install locale

Choisir alors une locales UTF-8, pour moi fr_FR.UTF-8

sudo dpkg-reconfigure locales

Mettre à la fin de .bashrc

LANG= »fr_FR.UTF-8″
export LANG

Sourcer le fichier

source .bashrc

Mosh est désormais utilisable.