Publié par

Il y a 3 ans -

Temps de lecture 5 minutes

Docker pattern : conteneur de build

Docker patternLorsqu’on développe une application, la mise en place de l’environnement de build et la gestion des dépendances est un moment frustrant. Découvrons un cas d’utilisation de Docker permettant de résoudre cette problématique en laissant votre poste de travail propre.

Le contexte

Me voici nouvel équipier sur un projet et ma première tâche consiste classiquement à installer l’environnement sur mon poste de travail.

Le projet est une application dont le but est de fournir l’heure. Afin d’être multiplateforme, l’application est écrite en go. Les logs de l’application sont gérés par une bibliothèque tierce.

Enfin, pour simplifier le déploiement, l’application sera livrée en tant qu’image Docker.

Vous l’aurez compris, l’idée est de fournir les outils pour construire l’application ainsi que l’image Docker finale

Le code

Le code de l’application est ici très simple.

package main

import (
    log "github.com/Sirupsen/logrus" // Use some external dep
    "time"
)

func main() {
    log.Println(time.Now())
}

Le seul point d’intérêt réside dans l’utilisation de logrus pour la gestion des logs.

Image de build

Dans le projet créons un fichier Dockerfile dédié au conteneur de build. Sans grande originalité je vous propose de le nommer build.Dockerfile, permettant ainsi la reconnaissance du format de fichier par un éditeur de texte.

FROM golang:1.5

RUN go get github.com/Sirupsen/logrus

VOLUME $GOPATH/src/github.com/tauffredou/clock
VOLUME /output

WORKDIR $GOPATH/src/github.com/tauffredou/clock
CMD GOOS=linux GOARCH=amd64 go build -o /output/clock

Quelques explications:

FROM golang:1.5

L’image de base utilisée est directement l’image « officielle » golang en précisant la version souhaitée. Nous évitons ainsi de gérer l’installation du SDK go.

RUN go get github.com/Sirupsen/logrus

go get permet de télécharger la dépendance logrus dans le GOPATH. D’autres solutions telles que godep existent pour gérer les dépendances plus finement mais nous ne les utiliserons pas ici.

VOLUME $GOPATH/src/github.com/tauffredou/clock

Ce volume contiendra les sources du projet. Pour être précis, il n’est pas obligatoire de le déclarer dans le Dockerfile mais cela fournit une information sur l’emplacement attendu directement.

VOLUME /output
Ce volume va recevoir les binaires compilés.

WORKDIR $GOPATH/src/github.com/tauffredou/clock

Par commodité le répertoire d’exécution est celui des sources du projet

CMD GOOS=linux GOARCH=amd64 go build -o /output/clock

La commande par défaut du conteneur compile le projet dans le répertoire /output

Image finale

FROM busybox
COPY bin/clock /
CMD /clock

FROM busybox

Le binaire étant statiquement compilé, il serait possible d’utiliser une image « scratch », l’image finale ne contenant alors que l’application. Cependant, pour disposer d’un minimum d’outils, nous utiliserons une image de base « busybox »
COPY bin/clock /

Le binaire est copié depuis le répertoire bin.
CMD /clock

Sans surprise, la commande par défaut exécute le programme

Un peu d’outillage

La démarche étant de limiter le plus possible les prérequis, il n’est pas judicieux de se reposer sur un autre outil de build, sauf si vous avez la certitude que celui-ci est présent sur les postes cibles. make, par exemple sera probablement présent sur un environnement linux mais moins généralement sur un poste windows. L’utilisation de Docker sous Windows impose l’utilisation d’un terminal cygwin, nous sommes donc certain d’avoir un shell sur tous les postes.

#!/bin/sh

CWD=$(cd $(dirname $0);pwd)
MAKE=$0
IMAGE=tauffredou/clock

usage(){
cat<<-EOUSAGE
make.sh [Action]
Actions:
  builder       create builder image
  build         create binary using builder image
  image         create final image
  run           run the final image
  build-chain   builder,build,image

EOUSAGE
}

case $1 in
  builder)
    docker build -f build.Dockerfile -t $IMAGE-builder .
  ;;
  build)
    docker run -v gopath:/go/src -v $CWD:/go/src/github.com/tauffredou/clock -v $CWD/bin:/output $IMAGE-builder #note 1
    echo "Built in $CWD/bin"
  ;;
  image)
    docker build -t $IMAGE .
    echo run clock with: docker run $IMAGE
  ;;
  run)
    docker run $IMAGE
  ;;
  build-chain)
    $MAKE builder && $MAKE build && $MAKE image
  ;;
  *)
    usage
  ;;
esac

note 1 : J’utilise lors du build deux volumes : le premier est un volume nommé gopath (disponible depuis Docker 1.9), le second monte le répertoire de travail dans le conteneur en respectant la nomenclature go.

docker volume ls

DRIVER              VOLUME NAME
local               gopath

Un volume gopath est automatiquement créé si celui-ci n’existe pas. Il permet d’enregistrer les dépendances en dehors du conteneur; plusieurs projets peuvent ainsi partager ce même volume et éviter de télécharger les dépendances communes.

L’essayer c’est l’adopter

S’il est besoin de vous convaincre, vous pouvez essayer avec le projet présenté. Les sources sont disponibles sur github.

git clone https://github.com/tauffredou/clock.git
cd clock
./make.sh build-chain
./make.sh run

Plusieurs images ont été créées:

docker images

REPOSITORY                                        TAG                 IMAGE ID            CREATED             SIZE
tauffredou/clock                                  latest              d41a6cf3695b        4 days ago          4.148 MB
tauffredou/clock-builder                          latest              6b56026a925b        4 days ago          725.9 MB
golang                                            1.5                 2e1c8fd5ddc3        3 weeks ago         725.2 MB

Nous constatons que l’écart de taille entre le builder et l’image finale est important. En effet le builder contient le SDK et les dépendances ; de plus nous utilisons une image de base officielle et avons donc peu de possibilités d’optimisation.

Pour conclure cet article, je vous propose de faire le ménage

# Suppression des conteneurs 
docker rm $(docker ps -a | grep " tauffredou/clock" | awk '{print $1}' )
# Suppression des images
docker rmi tauffredou/clock-builder tauffredou/clock

L’image golang et le volume gopath sont toujours présents, si vous ne les utilisez pas pour un autre projet, vous pouvez les supprimer. J’ai choisi de les garder.

Et notre poste de travail retrouve l’odeur du neuf !

Publié par

Commentaire

2 réponses pour " Docker pattern : conteneur de build "

  1. Publié par , Il y a 3 ans

    Salut @Thomas,

    Tout d’abord, merci !

    Comme vu ce soir, as-tu un exemple de cette philosophie pour un projet NodeJS ? PHP ? Ruby ? ou Python ?

    L’idée étant bien-sur de voir comment faire avec des langages non compilés.

    Encore merci :)

  2. Publié par , Il y a 3 ans

    Salut Julien,

    Pour les langages non compilés, la solution est généralement plus simple : il suffit de copier les sources du projet.
    Tu peux utiliser les images « onbuild » qui en règle générale copie les sources et résout les dépendances. ( ex pour node: https://github.com/nodejs/docker-node/blob/master/7.1/onbuild/Dockerfile )

    Je te conseille cependant de tester le code séparément de la création de l’image finale. Tu éviteras ainsi d’embarquer des dépendances de test inutiles au runtime.
    Le pattern de conteneur de build s’applique ici, il a pour responsabilité de résoudre les dépendances, tester ton code et packager l’application ( supprimer les fichiers de test, les configurations qui trainent) pour ne garder que ce qui est nécessaire dans un répertoire « dist », par exemple.
    La création de l’image finale utilise alors uniquement ce répertoire; tu gagneras sur la taille de l’image.

    Thomas

  3. Publié par , Il y a 3 ans

    Thomas,

    D’accord, tu prêches un convertis :)

    Merci, je pense que ça aidera d’autres !

    A bientôt.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Xebian, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.