Création d'un conteneur Docker minimal pour les applications Go

Bonjour, Habr! J'attire votre attention sur une traduction de l'article du fondateur du service Meetspaceapp Nick Gauthier "Construire des conteneurs Docker minimaux pour les applications Go" .

10 minutes pour lire

Il existe de nombreux conteneurs officiels et pris en charge par la communauté pour différents langages de programmation (y compris Go). Mais ces conteneurs peuvent être assez volumineux. Comparons d'abord les méthodes de construction de conteneurs standard pour les applications Go, puis je vais vous montrer comment créer des applications Go conteneurisées statiques extrêmement petites

Partie 1: Notre «application»


Pour les tests, nous avons besoin d'une petite application. Forkons google.com et affichons la taille HTML.

package main import ( "fmt" "io/ioutil" "net/http" "os" ) func main() { resp, err := http.Get("https://google.com") check(err) body, err := ioutil.ReadAll(resp.Body) check(err) fmt.Println(len(body)) } func check(err error) { if err != nil { fmt.Println(err) os.Exit(1) } } 

Si nous commençons, nous obtenons seulement un certain nombre. J'ai eu environ 17K. J'ai délibérément décidé d'utiliser SSL, mais j'expliquerai la raison plus tard.

Partie 2: Dockérisation


En utilisant l'image officielle Go, nous écrivons Dockerfile «onbuild»:

 FROM golang:onbuild 

L'image «Onbuild» suppose que votre projet a une structure standard et créera une application Go standard. Si vous avez besoin de plus de flexibilité, vous pouvez utiliser l'image standard Go et la compiler vous-même:

 FROM golang:latest RUN mkdir /app ADD . /app/ WORKDIR /app RUN go build -o main . CMD ["/app/main"] 

Ce serait bien ici de créer un Makefile ou quelque chose comme ça que vous utilisez pour la construction de l'application. Nous pourrions charger des ressources à partir du CDN ou les importer à partir d'un autre projet, ou peut-être nous voulons exécuter des tests dans le conteneur ...
Comme vous pouvez le constater, la dockérisation Go est assez simple, surtout si vous considérez que nous n'utilisons pas les services et les ports auxquels nous devons nous connecter. Mais il y a un sérieux défaut dans les images officielles - elles sont vraiment grandes. Voyons voir:

 REPOSITORY SIZE TAG IMAGE ID CREATED VIRTUAL SIZE example-onbuild latest 9dfb1bbac2b8 19 minutes ago 520.7MB example-golang latest 02e19291523e 19 minutes ago 520.7MB golang onbuild 3be7ee2ec1ae 9 days ago 514.9MB golang 1.4.2 121a93c90463 9 days ago 514.9MB golang latest 121a93c90463 9 days ago 514.9MB 

L'image de base occupe 514,9 Mo et notre application ajoute 5,8 Mo supplémentaires. Comment se fait-il que notre application compilée nécessite 515 Mo de dépendances?
Le fait est que notre application a été compilée à l'intérieur du conteneur. Cela signifie que le conteneur doit installer Go. Par conséquent, il a besoin des dépendances Go, ainsi que d'un gestionnaire de packages et d'un système d'exploitation vraiment complet. En fait, si vous regardez le Dockerfile pour golang: 1.4, il est livré avec Debian Jessie, installe le compilateur GCC et les outils de construction, télécharge Go et l'installe. Ainsi, nous obtenons l'intégralité du serveur Debian et la boîte à outils Go pour lancer notre petite application. Que peut-on faire à ce sujet?

Partie 3: Compilez!


Vous pouvez améliorer la situation en s'écartant légèrement de l'approche habituelle. Pour ce faire, nous allons compiler Go dans notre répertoire de travail, puis ajouter le binaire au conteneur. Cela signifie qu'une construction de docker simple ne fonctionnera pas. Nous avons besoin d'un assemblage de conteneurs en plusieurs étapes:

 go build -o main . docker build -t example-scratch -f Dockerfile.scratch . 

Et un simple Dockerfile.scratch:

 FROM scratch ADD main / CMD ["/main"] 

Qu'est-ce que le scratch? Scratch est une image vierge spéciale dans Docker. Sa taille est 0B:

 REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE example-scratch latest ca1ad50c9256 About a minute ago 5.60MB scratch latest 511136ea3c5a 22 months ago 0B 

En conséquence, notre conteneur ne prend que 5,6 Mo. Super! Mais il y a un problème:

 $ docker run -it example-scratch no such file or directory 

Qu'est-ce que cela signifie? Il m'a fallu un certain temps pour réaliser que notre binaire Go recherchait des bibliothèques sur le système d'exploitation sur lequel il fonctionnait. Nous avons compilé notre application, mais elle est toujours liée dynamiquement aux bibliothèques qui doivent être lancées (c'est-à-dire à toutes les bibliothèques C). Malheureusement, scratch est vide, il n'y a donc pas de bibliothèques ou de chemins de chargement. Nous devons modifier le script de construction pour compiler statiquement notre application avec toutes les bibliothèques intégrées:

 CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . 

Nous désactivons cgo, ce qui nous donne un binaire statique. Nous spécifions également Linux comme OS (au cas où quelqu'un le construirait sur Mac ou Windows). Le drapeau -a signifie reconstruire tous les packages que nous utilisons, ce qui reconstruira toutes les importations avec cgo désactivé. Nous avons maintenant un binaire statique. Courons:

 $ docker run -it example-scratch Get https://google.com: x509: failed to load system roots and no roots provided 

Qu'est-ce que c'est? C'est pourquoi j'ai décidé d'utiliser SSL dans notre exemple. Il s'agit d'un «dévers» vraiment courant pour de tels scénarios: pour terminer les demandes SSL, nous avons besoin de certificats SSL racine. Alors, comment pouvons-nous les ajouter à notre conteneur?
Selon le système d'exploitation, les certificats peuvent se trouver à différents endroits. Pour de nombreuses distributions Linux, il s'agit de /etc/ssl/certs/ca-certificates.crt . Donc, premièrement, nous allons copier ca-certificats.crt depuis notre ordinateur (ou une machine virtuelle Linux ou un fournisseur de certificats en ligne) vers notre référentiel. Ensuite, nous ajoutons ADD à notre Dockerfile pour déplacer ce fichier là où Go l'attend:

 FROM scratch ADD ca-certificates.crt /etc/ssl/certs/ ADD main / CMD ["/main"] 

Il suffit maintenant de recréer notre image et de la lancer. Ça marche! Voyons maintenant la taille de notre application:

 REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE example-scratch latest ca1ad50c9256 About a minute ago 6.12MB example-onbuild latest 9dfb1bbac2b8 19 minutes ago 520.7MB example-golang latest 02e19291523e 19 minutes ago 520.7MB golang onbuild 3be7ee2ec1ae 9 days ago 514.9MB golang 1.4.2 121a93c90463 9 days ago 514.9MB golang latest 121a93c90463 9 days ago 514.9MB scratch latest 511136ea3c5a 22 months ago 0B 

Nous avons ajouté un peu plus d'un demi-mégaoctet (et la plupart proviennent d'un fichier statique et non de certificats racine). Nous avons obtenu un très petit conteneur - il sera très pratique de le déplacer entre les registres.

Conclusion


Notre objectif était de réduire la taille des conteneurs pour l'application Go. La particularité de Go est qu'il peut créer un fichier binaire lié statiquement qui contient complètement l'application. D'autres langues peuvent le faire aussi, mais pas toutes. L'application d'une technique similaire pour réduire la taille des conteneurs dans d'autres langues dépendra de leurs exigences minimales. Par exemple, une application Java ou JVM peut être compilée en dehors du conteneur, puis incorporée dans un conteneur qui contient uniquement la JVM (et ses dépendances). Mais même ainsi, ce sera moins qu'un conteneur avec JDK.

Source: https://habr.com/ru/post/fr460535/


All Articles