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, les services métiers sont déployés dans des conteneurs, et leur architecture est décrite dans un Docker-Compose. 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 as root
COPY . /home/factory_hello
# replace the previous line if you want to copy files as factory_hello user
# COPY --chown=factory_hello:factory_hello . /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.yml doit être versionné à la racine, dans les sources de l’application

Compose utilise un fichier .env pour initialiser des variables d’environnement. Les projets déployés auront un .env généré spécifiquement pour chaque environnement de déploiement (préprod/prod/…). 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]
        # always restart on failure
        restart: "always"
        # define some env vars available to our container
        environment:
            ENV: ${CI_ENVIRONMENT_NAME}
            CI_ENVIRONMENT_DOMAIN: ${CI_ENVIRONMENT_DOMAIN}
        labels:
            # traefik v1 configuration
            traefik.frontend.rule: Host:${CI_ENVIRONMENT_DOMAIN}
            traefik.enable: 'true'
            # headers STS
            traefik.frontend.headers.STSPreload: 'true'
            traefik.frontend.headers.STSSeconds: '63072000'

Makefile

Pour construire et tester l’application, nous allons utiliser un Makefile pour pouvoir itérer tranquilement en local, sans dépendre de la CI. CI qui utilisera ce Makefile.

# The Makefile defines all builds/tests steps

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

# factory CLI version (used to fetch the correct url)
FACTORY_CLI_VERSION=v0.2.0

# 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

# fetch factory CLI and make if executable
# $(FACTORY_CLI_URL) is defined in .factory.make
factory:
	curl -L $(FACTORY_CLI_URL) | gunzip - > factory
	chmod +x factory

cli: factory

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 -v --timeout 60
	# 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:
	# rm CLI
	rm -f factory
	# 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 factory-deploy script to deploy the compose project
        # this will (re)start docker-compose on the target after pulling the new image
        - factory-deploy

production:
    stage: production
    environment:
        name: production
        url: http://yourwebsite.com
    script:
        - factory-deploy
    # 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 disponibles sur le hub de Docker et leur sources sont sur Github.

Nous fournissons des images pour différents langages et leurs variantes de développement :

Nous fournissons aussi quelques services :

Des exemples de projets sont fournis dans votre Gitlab.

Image, user et UID

Vos images seront lancé en prod avec un utilisateur non root, et si vous avez des volumes, cet utilisateur devra avoir un UID de 1001.

Pour construire des images qui utilisent l’utilisateur courant, et ainsi avec des volumes avec le bon utilisateur, le plus simple est de le faire en dynamique : la CI a le même UID que les cibles de déploiement.

Dans le Makefile on récupère l’UID de l’utilisateur.

UID:=$(shell id -u)

Quand on consrtuit l’image, on passe l’UID en argument

docker build --build-arg=UID=$(UID)

Dans le Dockerfile, on crée un utilisateur avec le bon UID

ARG UID=1001
RUN useradd bob --uid ${UID}
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

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:

Dockerfile.cdn

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 :

Makefile
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 :

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

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

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é)
Votre fichier docker-compose.yml peut utiliser les variables d’environnements fournit la CI, les variables du projet Gitlab ou les variables spécifique aux environnements de déploiement avec les variables de substitution. Par contre, pour qu’une image Docker utilise une variable d’environnement, il faut qu’elle soit explicitement recopiée dans le fichier docker-compose.yml dans l' environment du service.

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]
        # always restart on failure
        restart: "always"
        # define some env vars available to our container
        environment:
            ENV: ${CI_ENVIRONMENT_NAME}
            CI_ENVIRONMENT_DOMAIN: ${CI_ENVIRONMENT_DOMAIN}
        labels:
            # traefik v1 configuration
            traefik.frontend.rule: Host:${CI_ENVIRONMENT_DOMAIN}
            traefik.enable: 'true'
            # headers STS
            traefik.frontend.headers.STSPreload: 'true'
            traefik.frontend.headers.STSSeconds: '63072000'

    # 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

  • depends_on

  • entrypoint

  • environment

  • expose

  • extra_hosts

  • healthcheck

  • image

  • labels

  • links

  • logging

  • ports

  • read_only

  • restart

  • tty

  • volumes

Example
version: '3'

services:
    myservice:
        command: /command
        depends_on:
        - other_service
        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:
        - DEPRECATED. Use "depends_on" instead
        logging: {}
        ports: DEPRECATED. Use "expose" instead
        read_only: false
        restart: always
        tty: false
        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 -v --timeout 60
# 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.

Une limitation liée à la taille des comptes UNIX impose que le nom de votre groupe/projet ne dépasse pas 32 caractères pour pouvoir être déployé.
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 factory-deploy script to deploy the compose project
        # this will (re)start docker-compose on the target after pulling the new image
        - factory-deploy

production:
    stage: production
    environment:
        name: production
        url: http://yourwebsite.com
    script:
        - factory-deploy
    # 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:
        - factory-deploy
    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_.

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'
traefik.frontend.headers.STSPreload: 'true'
traefik.frontend.headers.STSSeconds: '63072000'

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 factory-deploy pour déployer cette image sur un serveur:

deploy:
    stage: deploy
    script:
        - factory-deploy
    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 factory-deploy

script:
    - factory-deploy -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:
        - factory-deploy
    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]
        # always restart on failure
        restart: "always"
        # 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 v1 configuration
            traefik.frontend.rule: Host:${CI_ENVIRONMENT_DOMAIN}
            traefik.enable: 'true'
            # headers STS
            traefik.frontend.headers.STSPreload: 'true'
            traefik.frontend.headers.STSSeconds: '63072000'

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

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 sans suivi réputationnel est mis à disposition.

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. 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]
        # always restart on failure
        restart: "always"
        depends_on:
            # 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 v1 configuration
            traefik.frontend.rule: Host:${CI_ENVIRONMENT_DOMAIN}
            traefik.enable: 'true'
            # headers STS
            traefik.frontend.headers.STSPreload: 'true'
            traefik.frontend.headers.STSSeconds: '63072000'

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é depends_on) mysql dans votre fichier docker-compose le nom mysql sera résolu par votre conteneur.

Vous pouvez donc utiliser une chaine de connexion de se type:

mysql://$MYSQL_USER:$MYSQL_PASSWORD@mysql/$MYSQL_DATABASE

Ou @mysql est équal à @nom_de_votre_service.

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

Vous devez spécifier l'`authSource` dans votre chaine de connexion:

mongodb://${MONGODB_USER}:${MONGODB_PASSWORD}@mongodb/${MONGODB_DB}?authSource=${MONGODB_DB}
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]
        # always restart on failure
        restart: "always"
        # define some env vars available to our container
        environment: &web_service_env
            ENV: ${CI_ENVIRONMENT_NAME}
            CI_ENVIRONMENT_DOMAIN: ${CI_ENVIRONMENT_DOMAIN}
        labels:
            # traefik v1 configuration
            traefik.frontend.rule: Host:${CI_ENVIRONMENT_DOMAIN}
            traefik.enable: 'true'
            # headers STS
            traefik.frontend.headers.STSPreload: 'true'
            traefik.frontend.headers.STSSeconds: '63072000'

    # we define a service as a cron job.
    run_hourly:
        <<: *web_service
        # fail fast
        command: /bin/false
        # do not restart on failure
        restart: "no"
        environment:
            <<: *web_service_env
            X_CRONJOB: "1"
        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

Nos démos contienne un script factory-runjob qui vous permet de simuler le lancement d’un job:

$ ./factory-runjob <nom_du_service>

NB: factory empèche d’avoir deux processus d’un même job en simultané. Veillez à avoir une fréquence d’exécution supérieur à la durée d’éxécution du job.

Metrics

Nous fournissons un service InfluxDB et un service Grafana

Une organisation Grafana est créé par groupe Gitlab. Vous pouvez accéder à cette organisation en cliquant sur le badge grafana affiché sur chaque projet et y ajouter des dashboards pour répondre à vos besoins.

Une base de donnée InfluxDB est créée par projet et environnement. Ces bases de données sont accessible en tant que datasource depuis Grafana.

Vous pouvez poster des données dans ces bases de données (par exemple en utilisant statsd), depuis votre application en utilisant les variables suivantes fournies dans l’environnement de votre conteneur:

INFLUXDB_DOMAIN: ${INFLUXDB_DOMAIN}
INFLUXDB_DATABASE: ${INFLUXDB_DATABASE}
INFLUXDB_USERNAME: ${INFLUXDB_USERNAME}
INFLUXDB_PASSWORD: ${INFLUXDB_PASSWORD}

Il est aussi possible d’ajouter des labels docker-compose pour ajouter des metrics automatiquement.

Avec une url de status, qui doit retourner un code HTTP 200 et qui sera pingué à interval régulier. Ajoutez simplement ce label à votre application:

services:
    app:
        labels:
            sh.factory.probe.health.path: /status

Avec une url prometheus:

services:
    app:
        labels:
            sh.factory.probe.prometheus.path: /metrics

Référez-vous à la documentation des clients pour intégrer prometheus à votre application

Pour accéder à l’organisation Grafana, vous devez avoir à minima le rôle Developer dans le groupe Gitlab. Si vous n’arrivez pas à accéder à l’organisation en cliquant sur le badge, assurez vous d’avoir les droits nécessaire.

Utilisation du client en ligne de commande

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

Ce client permet d’intérragir avec les projets déployés sur votre usine logiciel factory.

Pré requis

L’utilisation du client requiert:

  • un agent ssh (reportez vous à la documentation de ssh-agent) gérant une clé ssh dont la clé publique a été uploader sur votre compte utilisateur gitlab. Voir la documentation gitlab pour plus d’information sur ce point.

  • un token gitlab avec, à minima, les scopes api et read_registry.

  • de lancer la commande depuis un projet git hébergé sur gitlab. Ainsi certain paramètre sont déduis de votre .git (url du gitlab et non du projet)

Options communes

Obtenir de l’aide, utilisez l’option -h

$ factory -h

Vous pouvez obtenir l’aide d’une commande de la même manière:

$ factory journal -h

L’option -t vous permet de passer votre token gitlab:

$ factory -t votre_token ...

Vous pouvez aussi le définir dans l’environnement de votre shell pour ne pas avoir à le repasser à chaque commande:

$ export PRIVATE_TOKEN=votre_token
$ factory ...

Enfin, vous pouvez le stocker dans un fichier de configuration:

$ echo "token: votre_token" > $HOME/.factory-cli.yaml

La plupart des commandes sont liées à un environnement (staging/production/etc.) L’option -e staging indique que la commande agira sur l’environnement de staging.

$ factory journal -e staging

Afficher les informations du projet

Afficher les informations du projet courant:

$ factory infos

Afficher aussi la liste des environnements du projet courant:

$ factory infos --with-environments
$ factory infos -e

Consultation des journaux systèmes

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

La commande factory journal permets 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.

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

Afficher les 10 dernières lignes de log

$ factory journal -e staging

Afficher les 30 dernières lignes de log

$ factory journal -e staging --lines -30

Afficher les 10 dernières lignes et suivre le flow

$ factory journal -e staging --lines -10 --follow

Appliquer une regex sur les logs

$ factory journal -e staging --lines -10 --follow --regexp "Handshake"

Copier des fichiers

Un serveur sftp est à votre disposition.

Vous pouvez l’utiliser en mode interractif:

$ factory volume -e staging sftp

Ou en mode non interractif, en passant vos commandes via l’entrée standard (STDIN).

Pousser un fichier:

$ echo "put test ./data/volume/test" | factory volume -e staging sftp

Récupérer un fichier:

$ echo "get ./data/volume/test test" | factory volume -e staging sftp

Executer une commande au sein d’un conteneur

factory exec vous permets de lancer une commande au sein des conteneurs de vôtre projet.

Bash est la commande par défaut:

$ factory container exec -e staging web

Mais vous pouvez en spécifier une:

$ factory container exec -e staging web -- ls -l

-- est utilisé pour passer les arguments au shell et non à factory exec.

Récupérer un dump de base de donnée

La commande factory dump vous permets de récupérer un dump de base de données:

$ factory container dump -e staging mysql

Le fichier est récupéré en local sous forme d’archive.

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: "8086"
            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
        depends_on:
            - 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.