Factory

Présentation

Factory est une plateforme de développement ouverte permettant la création et le suivi d’applications de bout en bout : de l’écriture du code à la livraison en production, puis à ses évolutions. Factory est assemblé à partir de briques opensources reconnues et largement utilisées : Gitlab et Docker.

Devops

Factory utilise une approche devops et suit les recommandations classiques, comme The twelve factor app, Infrastructure as Code ou CAMS : Culture, Automatisation, Mesures, et Partage (Sharing en VO).

Factory propose un workflow, regroupant un ensemble d’outils open source cohérent, correspondant à l’état de l’art du moment.

Gitlab

Factory est centré sur Gitlab, qui permet à des utilisateurs de créer des projets dans des groupes. Ces projets vont utiliser l'intégration continue avec des images Docker, pour construire, tester, empaqueter, tester fonctionnellement les applications pour ensuite les déployer.

Git, et Gitlab, sont utilisés pour versionner le code, mais aussi la description de l’architecture des services format l’application, s’inscrivant ainsi dans l’approche "infrastructure as code".

Gitlab centralise la gestion des utilisateurs, et les services complémentaires utilisent l’OAuth2 fourni par Gitlab pour l’authentification.

Gitlab CI permet de déclencher une suite d’actions à chaque fois que du code est poussé. Ces actions sont séquentielles et peuvent être parallélisées. Le flot d’actions est décrit dans un fichier YAML, .gitlab-ci.yml, qui doit être versionné à la racine du projet.

Il est possible de restreindre certaines actions à des branches, à des étiquettes, ou à une action manuelle.

La séquence classique d’intégration continue est d’enchaîner construction (images, assets), tests et déploiement. Le déploiement doit être systématique sur un serveur d’intégration et manuel sur un environnement de préproduction, puis de production.

C’est ce que propose notre workflow:

deploiement

Prérequis

Avant de lire cette documentation, vous devez être familiarisé avec les technologies suivantes:

  • Le format YAML

  • La conteneurisation Docker et plus particulièrement la création de conteneurs décrit avec Dockerfile

  • La composition de services avec Docker-compose et sa configuration. Nous utilisons le format de configuration de docker-compose dans sa version 3.x, ce qui requiert un docker 1.13.0 minimum, la cible étant la version courante 17.09.

  • L’utilisation de Gitlab CI

Prise en main

Architecture d’un projet

Le projet est hébergé dans un Gitlab, son architecture est décrite dans un Docker-Compose, et les services métiers sont déployés dans des conteneurs. Un proxy HTTP, Træfik, expose publiquement l’application, en suivant des règles définies dans les labels des conteneurs.

Arborescence de fichiers

Voici la liste minimale de fichiers requis pour un projet Factory:

$ ls -a
Dockerfile
.env
docker-compose.yml
.gitlab-ci.yml

Docker

Vous devrez construire l’image de votre application à l’aide d’un Dockerfile:

# we use a bearstech image!
FROM bearstech/python:3

# we create a user for our app
ARG uid=1001
RUN useradd factory_hello --uid ${uid} --shell /bin/bash --home /home/factory_hello
WORKDIR /home/factory_hello

# copy our project
COPY . /home/factory_hello

# use our user
USER factory_hello

# run our application

CMD ["python3", "-m", "http.server", "--bind=0.0.0.0", "9000"]

Docker Compose

Il faut ensuite utiliser l’image construite dans un projet docker-compose

Le fichier docker-compose doit être versionné à la racine, dans les sources de l’application

Compose utilise un fichier .env pour initialiser des variables d’environnement. Nous en créons un pour reproduire le comportement de Gitlab CI :

CI_REGISTRY_IMAGE=factory_hello
CI_COMMIT_SHA=dev
CI_ENVIRONMENT_NAME=dev

Notre docker-compose.yml va pouvoir utiliser ces variables et se comporter de façon identique en local et sur la CI. Ici nous lançons notre service web :

# this file is good to go for production
version: "3"

services:

    # our webapp.
    app:
        # use our image
        image: ${CI_REGISTRY_IMAGE}/app:${CI_COMMIT_SHA}
        # expose our server port
        expose: [9000]
        # define some env vars available to our container
        environment:
            ENV: ${CI_ENVIRONMENT_NAME}
            CI_ENVIRONMENT_DOMAIN: ${CI_ENVIRONMENT_DOMAIN}
        labels:
            # traefik configuration
            traefik.frontend.rule: Host:${CI_ENVIRONMENT_DOMAIN}
            traefik.enable: 'true'
            traefik.tags: web

Makefile

Pour faciliter la reproductibilité en local, un Makefile est utilisé :

# The Makefile defines all builds/tests steps

# current user id is usefull to be able to write in volumes
export UID:=$(shell id -u)

# include .factory.make file; set random Traefik ports, etc
include .factory.make

# compose command to merge production file and and dev/tools overrides
COMPOSE?=docker-compose -f docker-compose.yml -f tools.yml -f dev.yml

# docker run command used to build dependencies before images build
DOCKER?=docker run --rm -u $(UID) -e "HOME=/home/factory_hello" -w "/home/factory_hello" \
	-v "$(PWD):/home/factory_hello" -v "$(HOME)/.cache:/.cache" \
	bearstech/python-dev:3

export COMPOSE DOCKER

pull:
	# pull latest images required for our build
	docker pull bearstech/python:3
	docker pull bearstech/python-dev:3

build:
	# build the docker images with the commit as tag
	docker build -t $(CI_REGISTRY_IMAGE)/app:$(CI_COMMIT_SHA) \
		-f Dockerfile.app --build-arg=uid=$(UID) .

push:
	# push images to our private registry
	docker push $(CI_REGISTRY_IMAGE)/app:$(CI_COMMIT_SHA)

volumes:

up: volumes
	# run compose in background
	$(COMPOSE) up -d
	# wait for services using the bearstech/traefik-dev container utils
	$(COMPOSE) exec -T traefik wait_for_services --timeout 20
	# show apps logs
	$(COMPOSE) logs --tail="all" app

dev: volumes
	# run compose in foreground
	$(COMPOSE) up app

ps:
	$(COMPOSE) ps

down:
	# stop compose
	$(COMPOSE) down --remove-orphans

test:

clean:
	# clean local folders
	rm -Rf data
	# remove docker stuff
	$(COMPOSE) rm --stop --force
	# clean docker volumes
	docker volume prune -f || true
	# clean docker networks
	docker network prune -f || true

Puis utiliser ces commandes make durant l’integration continue.

Gitlab

Intégration continue

Votre .gitlab-ci.yml doit reproduire un schéma comme celui-ci :

---

# Full documentation to configure you CI can be found here:
# https://docs.gitlab.com/ee/ci/yaml/

# stages to run in the ci (ordered)
stages:
    - pull       # pull latest images required for our build
    - build      # build the image
    - push       # push the image to the local registry
    - staging    # deploy the compose project on a staging server
    - production # deploy the compose project on a production server

# name of the step
pull:
    # stage for this step
    stage: pull
    # command(s) to run
    script:
        # pull images (see Makefile)
        - make pull

build:
    stage: build
    script:
        # build images (see Makefile)
        - make build

push:
    stage: push
    script:
        # login to the local registry
        - echo $CI_BUILD_TOKEN | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
        # push images (see Makefile)
        - make push

staging:
    stage: staging
    environment:
        name: staging
        url: http://factory-hello.staging-server.factory.sh
    script:
        # use the deploy-master script to deploy the compose project
        # this will (re)start docker-compose on the target after pulling the new image
        - deploy-master staging-server.factory.sh

production:
    stage: production
    environment:
        name: production
        url: http://yourwebsite.com
    script:
        - deploy-master production-server.factory.sh
    # we trigger deployement on the production serveur manualy
    when: manual
    # only for branch master
    only:
        - master
    # and we force this step to fail on failure
    allow_failure: false

Dans notre fichier compose, nous avons indiqué le label traefik.frontend.rule: Host: ${CI_ENVIRONMENT_DOMAIN}. L’url de votre application dépend donc de l’environnement Gitlab où vous la déployez.

Déploiement

Pour déployer votre projet, poussez (git push) votre code sur Gitlab.

Vous pouvez ensuite vous rendre à l’url Gitlab de votre projet et consulter le rapport de build de la CI (Onglet Pipelines dans l’interface).

Si le build s’est déroulé sans accroc, vous pouvez lancer l’étape de déploiement manuellement.

Nous l’avons taggué manual dans notre .gitlab-ci.yml mais vous pouvez bien évidemment rendre cette étape automatique.

Patienter le temps que le déploiement se déroule et voilà !

Documentation

Intégration continue

Le déploiement de projets Factory se fait par le serveur d’intégration continue (abrégé en CI). Le serveur d’intégration continue ne contient pas d’outils de développement, toutes les étapes de l’intégration seront faites par des conteneurs Docker.

Le fichier .gitlab-ci.yml décrit le flot d’intégration continue du projet. Gitlab utilise le terme technique de Pipelines pour désigner l’intégration continue.

Les étapes seront déclenchées par la gitlab-ci, mais il est fortement recommandé de n’utiliser que de simple commande make dans le fichier .gitlab-ci.yml, pour ne pas avoir à le deboguer, ce qui nécessite beaucoup de commit laborieux.

Préparation

En utilisant les outils propres aux langages du projets, la CI va télécharger les bibliothèques nécessaires. Il peut être judicieux d’utiliser les volumes et le système de cache proposé par la CI pour diminuer le temps requis par certaines étapes du pipeline.

Compilation diverse et préparation des assets. Le travail se fait dans un conteneur, mais les sources et les destinations sont des volumes montés, et ainsi le résultat se trouvera sur place, disponible pour les étapes suivantes.

Tests unitaires

La CI va tester unitairement l’application, toujours avec un conteneur, qui partage le volume contenant les sources. Il est possible de paramétrer Gitlab pour qu’il puisse lire le taux de couverture des tests unitaires.

Analyse statique

Gitlab met en avant codeclimate qui regroupe des outils d’analyses classiques, avec une configuration centralisé dans un fichier .codeclimate.yml

Construction de l’image

La CI va construire la ou les images contenant l’application, et va la pousser dans le registre, dument étiqueté pour pouvoir conserver un historique des versions sans risque de confusion.

Tests fonctionnels

Il est sage de vérifier qu’a minima : l’image démarre et répond en HTTP. Il est conseillé d’aller plus loin en validant que les branchements de services décrit dans le docker-compose.yml permettent à l’image de fonctionner.

Déploiement

La CI va déclencher le déploiement, qui utilisera la ou les images Docker disponibles dans le registre, tel que décrit dans le fichier docker-compose.yml.

Limiter les actions à des branches

Pour ce cas, vous pouvez utiliser la clé only documentée ici

Utilisation de la registry locale

Pour utiliser la registry docker locale et privée associée à votre gitlab, vous devez vous authentifier à l’aide de la commande docker login.

Pour pouvoir pousser une image dans la registry depuis la CI, l’utilisateur qui lance le job doit avoir le droit de commiter dans le projet.

Pour pouvoir puller une image depuis la registry depuis la CI, l’utilisateur qui lance le job doit avoir, à minima, le droit de consulter le projet.

Ceci reste vrai pour utiliser une image associée à un autre projet. Le token attribué par la CI prenant en compte les droits sur tous les projets de l’utilisateur qui lance le job.

Optimisation des phases

Les phases de build peuvent prendre du temps. Il est possible d’en gagner en utilisant un cache entre les stages. Par exemple pour ne pas avoir à reconstruire des assets à chaque stage.

L’utilisation du cache est documenté ici

Docker

Factory utilise Docker lors de l’étape de l'intégration continue, et pour déployer le projet, qui sera composé de services.

Construire l’image de son service

L’image de l’application va être utilisée dans différents environnements, et sera utilisée pour les déploiements. Il faut faire attention à la taille de l’image, même si le système de fichiers en couches de Docker permet une mutualisation des couches basses.

Les images produites doivent être neutre et pouvoir être utilisées sans modification quel que soit leur environnement. Une fois instanciées, la résolution des noms d’hôtes et des variables d’environnement seront mises à disposition, et elles dépendront de l’environnement. Pour maîtriser la taille de l’image de son service, il ne faut pas embarquer les outils de développement. Pour ça, il faut créer deux images et utiliser les déploiements "sur place" de son langage de développement (venv, npm, bundler, composer…).

La première image va contenir les outils de développements, et les variantes de développement des bibliothèques utilisés, et permettra de travailler avec le code monté dans un volume local. Une fois prête, l’application sera recopiée dans l’image de l’application.

Pour des raisons de sécurité, les images utilisant l’user root sont bannies de Factory. Assurez vous d’utiliser un user non root à l’aide de RUN adduser et USER.

Images de bases

Factory fournit un ensemble d’images de bases, avec leurs variantes de développement. Les images sont disponibles sur le hub de Docker et les sources sont sur github :

Des exemples de projets sont fournis dans votre Gitlab.

Exemple d’image Docker: un CDN

Pour servir vos fichiers statiques, nous recommandons d’utiliser un conteneur spécifique.

Pour cela, la CI doit construire vos fichiers statiques (compilation des assets) puis créer une image qui les contient. Voici un exemple de Dockerfile qui copie le répertoire static/ afin de les faire servir par le serveur web nginx:

FROM bearstech/nginx

# this will copy statics/ to /var/www/html because we use
# statics/ as context to build the image
COPY . /var/www/html

La CI devra construire cette image (puis la pousser) en utilisant une commande comme celle-ci :

docker build -t $(CI_REGISTRY_IMAGE)/cdn:$(CI_COMMIT_SHA) \
	-f Dockerfile.cdn --build-arg=uid=$(UID) statics/

Vous pouvez ensuite l’utiliser dans votre fichier compose :

    cdn:
        # we use our cdn image to serve static content
        image: ${CI_REGISTRY_IMAGE}/cdn:${CI_COMMIT_SHA}
        expose: [8000]
        labels:
            # notice the domaine name starts with cdn
            traefik.frontend.rule: Host:cdn.${CI_ENVIRONMENT_DOMAIN}
            traefik.enable: 'true'
            traefik.tags: web

Grâce à un Traefik local correctement configuré, vous pourrez accéder à ces deux domaines depuis un navigateur.

L’utilisation du mot clé VOLUMES est à proscrire dans les Dockerfile. Les volumes ainsi créés ne sont pas sauvegardés. Si vous avez besoin d’utiliser des volumes, définissez les dans votre docker-compose.yml
Si votre application utilise des volumes, celle-ci doit tourner avec un utilisateur unix ayant l’uid 1001. En effet, les points de montages sont créé avec cet uid. Si votre application utilise un autre utilisateur elle ne pourra pas écrire dans vos volumes.

Docker-Compose

Docker-compose permet de décrire l’ensemble des services formant une application.

Cette description sera utilisée à différentes étapes du projet : développement (en local), intégration continue (tests fonctionnels) puis finalement en production. Le fichier docker-compose.yml sera utilisé tel quel en développement et en intégration continue, mais il ne servira que de base pour la description de l’environnement de production.

Les Services infogérés seront déclarés dans le fichier en utilisant des images de base, mais ce service sera rendu différemment en production, avec un service natif, et potentiellement répliqué.

Variables d’environnement

Docker-compose permet de gérer ces variables via un fichier .env, une fois déployé, nous assurons la mise à disposition des mots de passe et la disponibilité des services, spécifiques à l’environnement. Les variables de la CI sont utilisable dans votre fichier docker-compose.yml.

Vous pouvez aussi ajouter d’autres variables pour dynamiser la configuration de votre application.

Pour pouvoir déployer en production, votre service doit consommer une liste d’environnement et non un fichier d’environnement. (le mot clé env_file n’est pas autorisé)

Migration de base de donnée

Pour des migrations de BDD systématiques, vous pouvez utiliser un conteneur dédié qui effectue la commande de migration. Exemple, le service migrate_db lance un script ./migrate.sh et ne redémarre pas:

# this file is good to go for production
version: "3"

services:

    # our webapp. we define a &web_service yaml alias to "duplicate" this service later
    app: &web_service
        # use our image
        image: ${CI_REGISTRY_IMAGE}/app:${CI_COMMIT_SHA}
        # expose our server port
        expose: [9000]
        # define some env vars available to our container
        environment:
            ENV: ${CI_ENVIRONMENT_NAME}
            CI_ENVIRONMENT_DOMAIN: ${CI_ENVIRONMENT_DOMAIN}
        labels:
            # traefik configuration
            traefik.frontend.rule: Host:${CI_ENVIRONMENT_DOMAIN}
            traefik.enable: 'true'
            traefik.tags: web

    # we define a service to start the migration script
    migrate_db:
        <<: *web_service
        # migrate script
        command: ./migrate.sh
        # do not restart on failure
        restart: "no"
        # overrides labels. we do not want to route this service
        labels: {}

On notera l’utilisation de l’option command pour lancer la commande de migration. Ainsi que l’option restart qui indique à compose de ne pas redémarrer le conteneur quand le processus se termine.

Limitations

Factory n’autorise pas toutes les options de compose. Ci-dessous, la liste des options autorisées:

Top-Level keys
  • services

  • version

Service keys
  • command

  • entrypoint

  • environment

  • expose

  • extra_hosts

  • healthcheck

  • image

  • labels

  • links

  • logging

  • ports

  • read_only

  • restart

  • volumes

Example
version: '3'

services:
    myservice:
        command: /command
        entrypoint: /command
        environment:
            MY_SERVICE: http://...
        expose:
        - '8080'
        extra_hosts: my_external_server
        healthcheck: bash cmd
        image: ${CI_REGISTRY_IMAGE}
        labels:
            traefik.frontend.rule: Host:${CI_ENVIRONMENT_DOMAIN}
        links:
        - other_service
        logging: {}
        ports: DEPRECATED. Use "expose" instead
        read_only: false
        restart: always
        volumes:
        - ./data:/data

Tester votre application

Tests unitaire

Vous pouvez faire lancer vos tests unitaire par la CI. Il est important de faire tourner vos tests dans un conteneur.

Couverture de code

Vous pouvez également utiliser la CI pour parser et afficher la taux de couverture de vos tests. La configuration s’effectue via une regex dans l’onglet Settings → CI/CD → Test coverage parsing.

Par exemple, pour un projet Golang :

(\d+.\d+)% of statements$

Celui-ci sera ensuite affiché dans les détails du job dans lequel vous effectuez vos tests.

Tests fonctionnels

Pour toute application web, il est fortement recommandé de vérifier qu’elle répond bien en 200 sur sa home avec curl. Pour ceci, vous pouvez utiliser docker compose.

Un des problèmes à résoudre est que Docker lance un service, mais il ne sait pas à quel moment il est disponible. Les images de base de données ont souvent un premier démarrage lent, le temps de créer les bases et utilisateurs, voir même de charger des données.

Voici un exemple simple, avec un service web et sa base de données db qui vérifie le bon fonctionnement du service web avant de lancer un appel à l’application:

# compose command to merge production file and and dev/tools overrides
COMPOSE?=docker-compose -f docker-compose.yml -f tools.yml -f dev.yml
# run compose in background
$(COMPOSE) up -d
# wait for services using the bearstech/traefik-dev container utils
$(COMPOSE) exec -T traefik wait_for_services --timeout 20
# ensure selenium container is aware of traefik hosts
$(COMPOSE) exec -T traefik traefik_hosts > traefik_hosts
# show apps logs
$(COMPOSE) logs --tail="all" app
# test our app!
curl --verbose --fail --retry-delay 3 --retry 3 \
	 --header 'Host: $(CI_ENVIRONMENT_DOMAIN)' \
	 'http://127.0.0.1:$(TRAEFIK_HTTP_PORT)/'
# stop compose
$(COMPOSE) down --remove-orphans
  • Les services sont lancés puis détachés

  • Attente que le service db, un mysql, réponde

  • Appel du service web ou de tests fonctionnels

  • Les services sont arretés, les containers sont détruits

Déploiement

Environnements

Votre projet va pouvoir être déployé sur plusieurs environnements, une préprod et une prod pour commencer.

L’environnement de déploiement désigne la cible sur laquelle le projet var être déployé ainsi qu’un ensemble de variables relatives à celui-ci.

Gitlab

Gitlab supporte nativement la notion d’environnements. L’onglet correspondant est visible dans la section Pipelines → Environments de votre projet Gitlab. Cette fonctionnalité permet de suivre les déploiements effectués sur vos différents Environnements.

Pour définir et créer un nouvel environnement, la CI met à disposition le mot clé environment. Celui-ci s’accompagne de deux champs supplémentaires name (nom de l’environnement) et url (addresse de l’application à déployer).

    environment:
        name: <prod|preprod>
        url: https://<votre-nom-de-domaine>

S’ajoutera à l’étape de déploiement, exemple

staging:
    stage: staging
    environment:
        name: staging
        url: http://factory-hello.staging-server.factory.sh
    script:
        # use the deploy-master script to deploy the compose project
        # this will (re)start docker-compose on the target after pulling the new image
        - deploy-master staging-server.factory.sh

production:
    stage: production
    environment:
        name: production
        url: http://yourwebsite.com
    script:
        - deploy-master production-server.factory.sh
    # we trigger deployement on the production serveur manualy
    when: manual
    # only for branch master
    only:
        - master
    # and we force this step to fail on failure
    allow_failure: false
Ajouter des variables d’environnement supplémentaires

Vous pouvez ajouter des variables supplémentaires dans l’environnement de la CI pour les utiliser dans le contexte de la CI et dans le contexte de l’environnement déployé. Ces variables peuvent être globales à la CI ou spécifiques à chaque job:

variables:
    X_CONFIG_FILE=ci.yml

production:
    stage: deploy
    script:
        - deploy-master production.example.factory.sh
    environment:
        name: production
        url: https://production.example.factory.sh
    variables:
        X_CONFIG_FILE=prod.yml
    when: manual

Pour les retrouver dans le contexte de déploiement, ces variables doivent être préfixées par X_.

Ajouter des variables secrètes

De la même manière, vous pouvez ajouter des variables secrètes (clé d’api externe, etc.) spécifique à un environnement. Pour cela, le nom de la variable doit respecter la convention suivante:

SV_${CI_ENVIRONMENT_NAME}_${VAR_NAME}

Le préfix SV assure qu’il s’agit d’une variable secrète. ${CI_ENVIRONMENT_NAME} est le nom de votre environnement, en majuscule. ${VAR_NAME} est le nom réel de la variable qui sera mise à disposition dans l’application déployée.

Par exemple, pour mettre à disposition une variable nommée API_KEY différentes dans les environnements Preprod et Prod vous devrez définir des variables de la manière suivante:

SV_PROD_API_KEY=your_api_key_for_prod
SV_PREPROD_API_KEY=your_api_key_for_preprod

Routage HTTP

Une application est composée d’un ensemble de services. Les services, des containers Docker, vont réclamer des règles de routage, décrites dans le chapitre sur le Routage.

Les règles de routage utilisent les étiquettes Docker (labels), et permettent de sélectionner des headers, ou des patterns d’url.

traefik.frontend.rule: Host: ${CI_ENVIRONMENT_DOMAIN}
traefik.enable: 'true'

TLS est déployé de manière systématique, c’est important pour la sécurité, et pour le gain en indexation (SEO), et ce, quelque soit l’environnement, dev ou prod.

Configuration de la CI

Pour déployer votre application, il est nécessaire d’avoir certaines étapes dans la configuration de votre .gitlab-ci.yml. Ici nous ajoutons deux étapes. Une pour pousser une image docker sur la registry locale. L’autre pour déployer l’application sur un serveur distant.

stages:
    - 
    - push
    - deploy

Pousser votre image docker sur la registry locale:

push:
    stage: push
    script:
        - docker image tag $CI_REGISTRY_IMAGE:latest $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
        - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY
        - docker push $CI_REGISTRY_IMAGE:latest
        - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

Vous pouvez ensuite lancer le script deploy-master pour déployer cette image sur un serveur:

deploy:
    stage: deploy
    script:
        - deploy-master <nom-de-votre-serveur>
    environment:
        name: <prod|preprod>
        url: https://<votre-nom-de-domaine>
    when: manual

Par défaut, le fichier docker-compose.yml est utilisé mais vous pouvez utiliser un autre fichier à l’aide de l’option -f de deploy-master

script:
    - deploy-master <nom-de-votre-serveur> -f dev-compose.yml

Gestion des noms de domaine

Par défaut, la solution fournit pour chaque serveur de préproduction ou staging un catch all sur une entrée DNS du type : *.example.factory.sh.

Côté applicatif, la gestion du nom de domaine passe par les labels du conteneur qui seront lus par le reverse proxy HTTP Traefik.

Pour différencier les domaines de préproduction et production, les outils de déploiement intégrés à factory ajoutent une variable dans le contexte de l’application déployée.

Cette variable nommée CI_ENVIRONMENT_DOMAIN est basée sur l’url déclarée dans l’environnement de déploiement Gitlab (section Environnements). En précisant cette variable dans le fichier docker-compose.yml la configuration du nom de domaine est généralisée et contrôlée par les variables d’environnements, depuis la CI.

Pour illustrer, vous trouverez ci-dessous un exemple illustrant une application constituée d’un service web et d’un CDN.

deploy:
    stage: deploy
    script:
        - deploy-master example.com
    environment:
        name: preprod
        url: https://www.example.com
    when: manual
# this file is good to go for production
version: "3"

services:

    # our webapp.
    app:
        # use our image
        image: ${CI_REGISTRY_IMAGE}/app:${CI_COMMIT_SHA}
        # expose our server port
        expose: [9000]
        # define some env vars available to our container
        environment:
            ENV: ${CI_ENVIRONMENT_NAME}
            CI_ENVIRONMENT_DOMAIN: ${CI_ENVIRONMENT_DOMAIN}
            # we want to know the cdn url
            CDN: cdn.${CI_ENVIRONMENT_DOMAIN}
        labels:
            # traefik configuration
            traefik.frontend.rule: Host:${CI_ENVIRONMENT_DOMAIN}
            traefik.enable: 'true'
            traefik.tags: web

    cdn:
        # we use our cdn image to serve static content
        image: ${CI_REGISTRY_IMAGE}/cdn:${CI_COMMIT_SHA}
        expose: [8000]
        labels:
            # notice the domaine name starts with cdn
            traefik.frontend.rule: Host:cdn.${CI_ENVIRONMENT_DOMAIN}
            traefik.enable: 'true'
            traefik.tags: web

Routage

Le routage des différents services est assuré par Træfik, tout comme la terminaison TLS.

Træfik va se charger du routage HTTP, ainsi que de la répartition de charge, quand il y a plusieurs instances d’un même service avec les mêmes règles.

Les routes sont déclarées dans des labels des images docker avec la clef traefik.frontend.rule

Træfik utilise la notion de matcher pour définir ses routes.

Les clefs les plus courantes sont :

  • Host : le nom de domaine du site web

  • PathPrefix : le début de l’url

Træfik met à disposition une douzaine de règles que l’on peut combiner avec des virgules pour définir des OU logiques et des points virgules pour des ET logique. La documentation complète des règles de routage est disponible sur le site web de Træefik.

Par exemple, pour matcher un Host ET un PathPrefix, vous pouvez combiner ces clés dans votre docker-compose.yml en les séparant pas un ;:

labels:
    traefik.frontend.rule: Host:${CI_ENVIRONMENT_DOMAIN};PathPrefix:/ws/

Si plusieurs règles pointent sur le même domaine, vous devez ajuster la priorité de ces règles. Ci-dessous, un exemple, pour faire servir une websocket par un container et le reste du site par un serveur web classique:

services:
    web:
        image: bearstech/nginx
        labels:
            traefik.frontend.rule: Host:${CI_ENVIRONMENT_DOMAIN}
            traefik.frontend.priority: '2'

    ws:
        image: bearstech/node
        labels:
            traefik.frontend.rule: Host:${CI_ENVIRONMENT_DOMAIN};PathPrefix:/ws/
            traefik.frontend.priority: '1'

Notre service ws sera ainsi prioritaire, uniquement si le path de la requête commence par /ws/.

Authentification basique

Il est possible avec les étiquettes de Docker de réclamer à Træfik une authentification basique (basic auth dans le vocabulaire HTTP).

La clef traefik.frontend.auth.basic permet de fournir les utilisateurs et mots de passe.

La valeur pourra être vide (pas d’authentification) ou une liste utilisateur/hachage au format CSV : User:Hash,User:Hash

Le hash utilisé est apr1, et nécessite de doubler les $. La commande shell utilisant openssl est la suivante :

echo "password" | openssl passwd -apr1 -stdin | sed -e s/\\$/\\$\\$/g

Ce qui va donner comme exemple de configuration :

services:
    web:
        image: bearstech/nginx
        labels:
            traefik.frontend.rule: Host:${CI_ENVIRONMENT_DOMAIN}
            traefik.frontend.auth.basic: >
                bob:$$apr1$$FNdSDLrp$$TAXy/4ZjKQtNCupEyQ4tl1
Le > utilisé dans le YAML n’est pas obligatoire, c’est juste un outil de la syntaxe YAML pour améliorer la visibilité en n’ayant pas une ligne trop longue.

Courriel

Les conteneurs ne peuvent pas envoyer directement des mails, les ports 25, 465 et 587 sont bloqués.

Un service d’envoi de mail authentifié est mis à disposition, et les paramètres sont fournis aux conteneurs via les classiques variables d’environnements.

  • MAILS_TOKEN

  • MAILS_USER

  • MAILS_DOMAIN

  • MAILS_PORT

Les images PHP sont fournis avec un msmtp qui se configure au démarrage, pour pouvoir envoyer des mails avec la très classique commande sendmail.

Si vous désirez utiliser votre propre serveur, vous pouvez définir des variables dans votre docker-compose.yml et utiliser des variables secrètes pour les données sensibles.

Tester les envois de courriels avec MailHog

MailHog propose un débogueur d’envoi de mails.

MailHog est une image Docker proposant un serveur SMTP et une interface web pour consulter les messages qui ont transité par ce serveur SMTP.

Services infogérés

Il est pratique de développer (et de tester) avec des services directement fournis par les images officielles.

Pour la production, les contraintes sont différentes. En conséquence, il peut être intéressant de profiter de réglages fins du service, du backup, d’une potentielle réplication, mais surtout de métrologie et d’une astreinte.

Les services managés sont les services infogérés et hébergés (mise à jour, maintenance, astreinte) par Bearstech.

En activant cette option pour un ou plusieurs services, vous ordonnez à l’infrastructure et à nos outils de créer les ressources nécessaires pour permettre à votre application de consommer le service (routage, authentification…)

Les services managés restent accessibles directement, via un tunnel SSH, permettant de les interroger à distance, avec un compte en lecture seule. Les modifications de modèle, ou le chargement massif de données doivent se faire par du code, dans le cycle de déploiement de l’application.

Il ne vous est pas possible de modifier la configuration d’un service infogéré. Celle-ci étant initialisée et optimisée par Bearstech.

Consommer un service managé

Exemple : MySQL/MariaDB
Réclamer un service ayant pour nom 'mysql'
Attention à ne pas confondre nom du service / nom de l’image

En déclarant le service MySQL dans votre fichier docker-compose vous déclarez explicitement une dépendance de votre application à la BDD MySQL. Durant les phases de développement, votre application utilisera une BDD conteneurisée (jetable) vous permettant de développer et tester rapidement votre application. Côté production, nos outils analyseront la description de votre application pour router le service MySQL vers notre infrastructure.

Ci-dessous, l’exemple d’un fichier compose qui réclamera une BDD. Votre premier déploiement va créer une base vide. Les déploiements suivants n’affecteront pas le contenu de la base et vous pourrez réutiliser les données.

    # mysql database service
    mysql:
        image: mariadb:10.1
        environment:
            MYSQL_ROOT_PASSWORD: ${MYSQL_PASSWORD}
            MYSQL_DATABASE: ${MYSQL_DATABASE}
            MYSQL_USER: ${MYSQL_USER}
            MYSQL_PASSWORD: ${MYSQL_PASSWORD}
Utiliser les variables d’env

L’accès au service s’effectue via des informations passées via l’environnement. Pour MySQL :

  • MYSQL_DATABASE

  • MYSQL_PASSWORD

  • MYSQL_USER

  • MYSQL_SSL

Les variables _SSL contiennent true quand l’utilisation du service requiert ssl.

Se connecter à la BDD, depuis un conteneur

La connections depuis un autre conteneur s’effectue via les variables d’environnement et plus particulièrement un fichier .env, pour les services liés à une BDD (MySQL), il faudra réclamer les variables. :

    # our webapp.
    app:
        # use our image
        image: ${CI_REGISTRY_IMAGE}/app:${CI_COMMIT_SHA}
        # expose our server port
        expose: [9000]
        links:
            # link this container to the mysql service
            - mysql
        # define some env vars available to our container
        environment:
            ENV: ${CI_ENVIRONMENT_NAME}
            CI_ENVIRONMENT_DOMAIN: ${CI_ENVIRONMENT_DOMAIN}
            # mysql database connection
            MYSQL_DATABASE: ${MYSQL_DATABASE}
            MYSQL_USER: ${MYSQL_USER}
            MYSQL_PASSWORD: ${MYSQL_PASSWORD}
        labels:
            # traefik configuration
            traefik.frontend.rule: Host:${CI_ENVIRONMENT_DOMAIN}
            traefik.enable: 'true'
            traefik.tags: web

De cette façon, vous récupèrerez les variables fournies par le fichier .env généré par l’infrastructure.

Résolution de nom

En créant un lien (clé links) mysql dans votre fichier docker-compose le nom mysql sera résolu par votre conteneur.

Services supportés

MySQL/MariaDB

Mariadb est la variante libre de Mysql retenue par Debian. Mariadb est le M de LAMP.

Variables à utiliser:

MYSQL_DATABASE
MYSQL_USER
MYSQL_PASSWORD
MYSQL_SSL

Nom du service: mysql

Image recommandée en local: mariadb:10.1

service:
    mysql:
        image: mariadb:10.1
PostgreSQL

Variables à utiliser:

POSTGRES_DB
POSTGRES_USER
POSTGRES_PASSWORD
POSTGRES_SSL

Nom du service: postgresql

Image recommandée en local: postgres:9.6

services:
    postgresql:
        image: postgres:9.6
MongoDB

Variables à utiliser:

MONGODB_DB
MONGODB_USER
MONGODB_PASSWORD
MONGODB_SSL

Nom du service: mongodb

Image recommandée en local: mongo:3.2

services:
    mongodb:
        image: mongo:3.2
Redis

Redis (de l’anglais REmote DIctionary Server qui peut être traduit par « serveur de dictionnaire distant » et jeu de mot avec Redistribute1) est un système de gestion de base de données clef-valeur scalable, très hautes performances, écrit en C ANSI et distribué sous licence BSD. Il fait partie de la mouvance NoSQL et vise à fournir les performances les plus élevées possibles.

Variables à utiliser: Aucunes

Nom du service: redis

services:
    redis:
        image: bearstech/redis

Cronjob

Si vous avez besoin d’éxécuter des tâches à interval régulier, vous pouvez utiliser la fonctionalité cronjob.

Pour cela il vous faut un service qui:

  • échoue rapidement: command: /bin/false

  • ne redémarre pas après cette échec: restart: no

  • deux labels définissant la commande à lancer et la périodicité de la tâche

Il n’y a pas d’autre contrainte. N’importe quel service peut devenir un cronjob.

Dans l’exemple ci-dessous, nous utilisons l’image de notre application pour créer un deuxième service qui sera traité comme cronjob. Ce service sera lancé toutes les heures.

# this file is good to go for production
version: "3"

services:

    # our webapp. we define a &web_service yaml alias to "duplicate" this service later
    app: &web_service
        # use our image
        image: ${CI_REGISTRY_IMAGE}/app:${CI_COMMIT_SHA}
        # expose our server port
        expose: [9000]
        # define some env vars available to our container
        environment:
            ENV: ${CI_ENVIRONMENT_NAME}
            CI_ENVIRONMENT_DOMAIN: ${CI_ENVIRONMENT_DOMAIN}
        labels:
            # traefik configuration
            traefik.frontend.rule: Host:${CI_ENVIRONMENT_DOMAIN}
            traefik.enable: 'true'
            traefik.tags: web

    # we define a service as a cron job.
    run_hourly:
        <<: *web_service
        # fail fast
        command: /bin/false
        # do not restart on failure
        restart: "no"
        labels:
            # delay. run each hour
            sh.factory.cronjob.schedule: "0 * * * *"
            # command to launch
            sh.factory.cronjob.command: "echo time to run"

Le format du label sh.factory.cronjob.schedule suis la syntaxe standard de cron

Le label sh.factory.cronjob.command doit être une commande executable depuis votre conteneur avec bash -c. Par exemple bash -c "echo time to run"

Ces deux labels sont requis pour que votre service soit traité comme cronjob lors du déploiement.

Si vos images utilise des entrypoints, ceux ci doivent pouvoir lancer la commande bash. Si ce n’est pas le cas, vous pouvez surcharger l’entrypoint du service avec l’option entrypoint: /bin/bash

Consultation des journaux systèmes

Factory regroupe et homogénéise diverses sources de logs pour des services locaux ou distants.

Journaleux

Journaleux est un outil en ligne de commande permettant d’accéder et d’interroger le flot de logs de ses applications. Il est possible d’accéder aux événements passés, mais aussi de suivre le flot d’événements courants, en direct.

Journaleux est une application compilée, sans dépendance, disponible pour différents OS.

Authentification

Journaleux utilise Gitlab pour authentifier ses utilisateurs et ne donne accès qu’aux projets accessibles à l’utilisateur.

Lors de la première utilisation, puis quand le jeton sera périmé, Journaleux ouvre le navigateur web sur le site Gitlab et demande une autorisation OAuth2.

journaleux -d projet.example.com user

Journaleux ne trouvant aucune session associée à votre compte vous verrez apparaître le message suivant. Journaleux va ouvrir automatiquement un nouvel onglet dans votre navigateur web à l’adresse générée.

Invalid session:
	In order to generate a new session, please authenticate at https://mongitlab.bearstech.com/oauth/authorize?client_id=xxxxxxxxxx&redirect_uri=https%3A%2F%2Fprojet.example.com%3A50051%2Foauth%2Fcallback&response_type=code&scope=api&state=yyyyyyyyyyyyyyyyyyyyy

A la première connexion, vous devez autoriser Journaleux à accéder à votre compte gitlab. Ensuite, un token sera généré. Si Journaleux a besoin d’un nouveau token, vous serez, de la même façon, automatiquement redirigé sur l’url de génération de celui-ci.

Lire les logs

Pour accéder au logs, les commandes disponibles sont les suivantes (l’exemple utilise le serveur de préprod) :

Obtenir de l’aide

$ journaleux
NAME:
  Journaleux - Client CLI application for journaleuxd

USAGE:
  journaleux [global options] command [command options] [arguments...]

VERSION:
  ac6c950a45601d56500fe1f00a70650fe8e4ace8

COMMANDS:
    user, u      Get yourself
    projects, p  Get your projects
    journal, j   Read journal `NAME`
    help, h      Shows a list of commands or help for one command

GLOBAL OPTIONS:
  --domain value, -d value  Target RPC server (default: "rpc.example.com")
  --help, -h                show help
  --version, -v             print the version

ou pour une aide spécifique

journaleux <cmd> --help

Afficher les 10 dernières lignes de log

journaleux -d projet.example.com journal

Afficher les 30 dernières lignes de log

journaleux -d projet.example.com journal --lines -30

Afficher les 10 dernières lignes et suivre le flow

journaleux -d projet.example.com journal --lines -10 --follow

Appliquer une regex sur les logs

journaleux -d projet.example.com journal --lines -10 --follow --regexp "Handshake"

Journaleux supporte également un fichier de configuration, .journaleux.toml que vous pouvez versionner et ajouter à la racine de votre projet :

[Project]
Name = "namespace/name"

[servers]
  [servers.staging]
    address = "staging.example.com"
  [servers.prod]
    address = "prod.example.com"

Et utiliser -s pour distinguer la cible (preprod ci-dessous)

journaleux -s staging journal --lines -10 --follow --regexp "Handshake"

Logstash

Développement

Traefik, Mailhog et InfluxDB pour le développement

Vous pouvez utiliser Traefik, Mailhog et InfluxDB en local pendant vos développements pour reproduire au plus près un environnement Factory.

Pour cela il vous suffit de créer un fichier tools.yml contenant :

# this define some services usefull for dev/testing
version: "3"

services:
    # we can test mails with mailhog. send a mail from your app and check
    # http://mails.factory
    mails:
        image: mailhog/mailhog:latest
        labels:
            traefik.frontend.rule: Host:mails.factory
            traefik.enable: 'true'
            traefik.port: "8025"
            traefik.tags: web

    # allow to post data to influxdb
    influxdb:
        image: influxdb
        environment:
            INFLUXDB_DB: ${INFLUXDB_DATABASE}
            INFLUXDB_ADMIN_USER: ${INFLUXDB_USERNAME}
            INFLUXDB_ADMIN_PASSWORD: ${INFLUXDB_PASSWORD}
        labels:
            traefik.frontend.rule: Host:influxdb.factory
            traefik.port: "8080"
            traefik.enable: 'true'
            traefik.tags: web

    # we can use selenium remote webdriver to test our app
    # see https://github.com/SeleniumHQ/docker-selenium
    firefox:
        image: selenium/standalone-firefox
        volumes:
            - ./traefik_hosts:/etc/hosts
        expose: ["4444"]
        labels:
              traefik.frontend.rule: Host:selenium.factory
              traefik.enable: 'true'
              traefik.tags: web
        links:
            - traefik

    # enable Traefik for dev at http://traefik.factory
    traefik:
        image: bearstech/traefik-dev:latest
        volumes:
            - /var/run/docker.sock:/var/run/docker.sock
        ports:
            - "${TRAEFIK_UI_PORT:-8080}:8080"
            - "${TRAEFIK_HTTP_PORT:-80}:80"
        environment:
            MYSQL_ROOT_PASSWORD: ${MYSQL_PASSWORD:-null}
            POSTGRES_USER: ${POSTGRES_USER:-null}
        labels:
            traefik.frontend.rule: Host:traefik.factory
            traefik.port: "8080"
            traefik.enable: 'true'
            traefik.tags: web

Il vous faut aussi modifier votre fichier hosts pour que votre ordinateur puisse résoudre les noms de domaine en .factory et les diriger vers Traefik.

Ajouter une ligne comme celle ci au fichier hosts:

127.0.2.1 traefik.factory mails.factory dev.factory cdn.dev.factory # etc..

Cette ligne doit contenir tous les noms de domaine qui doivent être exposés par Traefik.

Pour Linux, ce fichier est /etc/hosts. Pour Windows, ce fichier est %SystemRoot%\System32\drivers\etc\hosts

Pour utiliser Mailhog comme serveur de mails, ajoutez ces variables à votre .env:

MAILS_DOMAIN=app.local
MAILS_PORT=1025
MAILS_USER=user
MAILS_TOKEN=token

Lancez ensuite compose en associant vos fichiers de configuration :

$ docker-compose -f docker-compose.yml -f tools.yml up

Puis rendez-vous sur l’interface de Traefik pour contrôler votre configuration à l’url http://traefik.factory

Projets examples

Nous fournissons à la demande des projets example pour certains langages et CMS:

  • python

  • ruby

  • node

  • php

  • drupal

  • wordpress

Toutes les branches de ces projets ne sont pas toutes fonctionnelles. Par contre, un diff entre la branche master et une autre branche vous permet de voir ce qui est nécessaire pour ajouter une fonctionalité.

$ git checkout mails
$ git diff master

Vous pouvez aussi voir ce diff dans l’interface Gitlab en clickant sur l’unique commit de la branche.

Les branches *_starter sont pleinement fonctionnelles.

Vous pouvez utiliser ces branches pour bootstraper votre projet:

$ git checkout mysql_starter
$ rm -Rf .git
$ git init
$ git add -A
$ git commit -m "Initial commit from mysql_starter branch"
$ git push <git url de votre projet> master
Les projets examples sont suceptible d’être écraser à tout moment. N’y faites pas de modifications, vous les perdriez.