Bonnes pratiques pour conteneuriser des applications Go avec Docker
Jekayin-Oluwa Olabemiwo
23 mars 2022
0 minutes de lectureLes applications Go et les conteneurs sont faits l’un pour l’autre. La faible taille des binaires des applications Go convient parfaitement aux stratégies de déploiement de microservices que Docker et Kubernetes permettent de mettre en œuvre. Cette synergie ne va toutefois pas sans difficultés. Il est donc important de comprendre les bonnes pratiques et les concepts clés liés aux conteneurs afin d’éviter les failles de sécurité qui peuvent facilement se glisser dans vos images de conteneur.
Dans cet article, vous allez coder un exemple d’application Go et apprendre à la conteneuriser et à l’exécuter de manière sécurisée.
Prérequis
Pour suivre ce tutoriel, vous devez avoir installé Docker et Go sur votre machine et connaître les bases de ces deux outils. Vous trouverez des instructions de prise en main et des liens de téléchargement de Docker sur la page orientation et configuration de la documentation Docker. Vous trouverez de bons tutoriels Go dans la documentation officielle de Go, ou pouvez accéder directement à la page Installation de Go pour Linux.
Créer un exemple d’application Go
Pour commencer, créons notre API Go. Accédez d’abord au répertoire dans lequel vous souhaitez placer votre application Go. Créez ensuite un répertoire nommé « godocker ». Dans ce répertoire godocker/, exécutez la commande suivante pour définir votre module Go :
Ensuite, créez un fichier nommé « main.go » dans le répertoire « godocker ». Ce fichier contiendra le code de votre API. Vous pouvez maintenant ajouter la logique permettant de fournir et de gérer l’heure actuelle via l’API.
Saisissez le code suivant dans godocker/main.go :
Ce code fournit l’heure actuelle via l’API en utilisant la variable TimeFormat du package http. Nous définissons ensuite la bibliothèque NewServeMux comme mux afin de configurer le serveur HTTP. Enfin, nous enregistrons le point de terminaison /time pour renvoyer l’heure actuelle via l’API et encoder la réponse au point de terminaison /time au format JSON.
Faisons un test rapide. Exécutez l’application dans le terminal avec la commande suivante :
Vous devriez obtenir un résultat semblable à celui-ci :
Testons maintenant l’API dans une autre fenêtre de terminal à l’aide de cURL. Saisissez la commande suivante dans le terminal :
Le résultat devrait ressembler à ceci :
Préparer le Dockerfile
Un Dockerfile contient une série d’instructions pour empaqueter et déployer une application sous forme de conteneur. Dans cette section, nous allons créer un Dockerfile et passer en revue certaines des instructions permettant d’empaqueter notre exemple d’application sous forme de conteneur.
Indiquer la version de la syntaxe Docker
Commençons par ajouter la directive de syntaxe. Dans le répertoire godocker/, créez un nouveau fichier nommé « Dockerfile » et saisissez le code suivant sur la première ligne :
La directive de syntaxe indique l’emplacement de la syntaxe Dockerfile que nous utiliserons pour créer notre Dockerfile. Cette ligne de code définit cet emplacement sur docker/dockerfile:1, qui correspond à la dernière version de la syntaxe. Docker vérifie la version de la syntaxe avant d’utiliser le moteur Buildkit pour créer le Dockerfile.
Veillez à commenter cette ligne au début du fichier. Après la directive de syntaxe, la convention consiste à laisser une ligne vide.
Hériter d’une image de base peu volumineuse
Ensuite, nous indiquons l’image Docker de base dont nous voulons hériter à l’aide de l’instruction FROM.
Ajoutez le code suivant à godocker/Dockerfil` :
Cette instruction nous évite d’avoir à créer notre propre image Docker de base. Nous héritons plutôt de l’image officielle Docker destinée aux applications Go, dans sa variante Alpine Linux. La version de Go de l’image de base est la 1.17. L’image alpine est très légère par rapport à une variante comme l’image ubuntu.
Complétons maintenant le reste du Dockerfile, en ajoutant des commentaires au code pour expliquer brièvement le rôle de chaque ligne.
Mettez à jour godocker/Dockerfile pour y inclure le code suivant :
Créer l’image
Notre Dockerfile est prêt. Créons maintenant notre image Docker à partir du Dockerfile avec la commande docker build. Docker utilise le daemon Docker pour créer les images. Avec la commande docker build, nous utilisons l’option --tag — que nous pouvons abréger en -t — pour attribuer un nom personnalisé à notre image Docker.
Saisissez la commande suivante dans le terminal :
Nous ajoutons . à la commande docker build pour créer l’image dans le répertoire actuel, qui sert de contexte de build. Évitez d’utiliser le chemin / comme contexte de build, car cela peut transférer tout le code source au daemon Docker.
La sortie du build doit contenir la ligne FINISHED et ressembler à ceci :
Cela signifie que l’image Docker a été entièrement créée avec le tag godocker.
Pour afficher la liste des images locales, saisissez la commande suivante dans le terminal :
Votre résultat devrait ressembler à ceci :
Dans notre résultat, la nouvelle image godocker créée fait 319 Mo, ce qui est volumineux pour une simple application API. Nous devons donc optimiser le build pour créer une image plus légère. Dans la section suivante, nous allons mettre en œuvre le concept de builds multi-étapes pour obtenir une image plus légère.
Utiliser des builds multi-étapes
Les builds multi-étapes permettent de créer des images bien plus petites que celles obtenues avec l’approche à une seule étape présentée dans la section précédente. Un build multi-étapes utilise une image pour créer des éléments, qui sont ensuite empaquetés dans une image plus légère contenant uniquement les éléments indispensables à leur exécution. En réduisant nos images au strict nécessaire pour exécuter l’application, nous pouvons limiter le risque de vulnérabilités. Pour ce faire, nous devons utiliser plusieurs instructions FROM dans notre Dockerfile.
Utiliser l’image officielle scratch
Nous pouvons commencer notre build avec une image vide en héritant de l’image officielle Docker scratch. Dans cette section, nous allons montrer comment utiliser l’image scratch pour les builds multi-étapes.
Accédez d’abord au répertoire racine de votre application. Créez ensuite un fichier nommé « Dockerfile.multistage » et saisissez le code suivant :
Ce code indique que l’image de base à utiliser est l’image officielle golang:1.17-alpine, à laquelle on attribue le nom d’étape build. Nous utilisons ensuite une autre instruction FROM pour mettre en œuvre le principe du build multi-étapes : le binaire créé à la première étape est copié dans l’image vide de la deuxième étape.
Nous devons ensuite créer une nouvelle image à l’aide du nouveau fichier Dockerfile.multistage. Nous devons également attribuer à cette nouvelle image le tag « multistage », afin de la distinguer de celle créée précédemment.
Saisissez la commande suivante dans le terminal :
Une fois le build terminé, vérifiez la liste des images en saisissant la commande suivante dans le terminal :
Votre résultat devrait ressembler à ceci :
Ce résultat montre la grande différence de taille entre les images godocker:multistage et godocker:latest. La réduction est considérable : l’image à une seule étape passe de 319 Mo à seulement 6,1 Mo pour l’image multi-étapes. Comme nous comptons sur les conteneurs pour démarrer rapidement, ce type d’optimisation est essentiel lors de la conteneurisation d’applications Go.
Déployer le conteneur
Outre l’optimisation des performances et de l’efficacité, nous devons également réfléchir à la meilleure façon de déployer nos conteneurs pour les exécuter en toute sécurité. Au cours de cette étape du tutoriel, nous allons appliquer quelques bonnes pratiques.
Exécuter en tant qu’utilisateur non root
Le principe du moindre privilège exige de limiter l’accès aux ressources système. Nos conteneurs Docker Go sont des conteneurs d’application et n’ont pas besoin de s’exécuter avec les privilèges root. Pour renforcer la sécurité, nous devrions donc créer dans notre Dockerfile un nouvel utilisateur et un nouveau groupe disposant d’un accès limité.
Pour créer un utilisateur non root, ajoutez les lignes suivantes immédiatement après la première instruction FROM dans godocker/Dockerfile.multistage :
Cette instruction définit les arguments USERNAME et PASSWORD à l’aide du mot-clé ARG, puis crée un utilisateur avec l’instruction RUN adduser.
Ajoutons maintenant les instructions permettant de copier les informations de l’utilisateur depuis la première étape et de les appliquer à la deuxième.
Ajoutez le code en gras de l’exemple suivant à la deuxième étape de godocker/Dockerfile.multistage. Le fichier devrait alors se terminer par le code suivant :
Dans Kubernetes, vous pouvez spécifier runAsuser: UID dans le champ securityContext. Consultez la documentation Kubernetes pour découvrir comment définir le contexte de sécurité d’un Pod.
Exécuter avec un système de fichiers racine en lecture seule
Une autre façon de renforcer la sécurité de notre application consiste à exécuter le conteneur avec un système de fichiers en lecture seule. Pour imposer l’accès en lecture seule au système de fichiers de notre conteneur, nous transmettons l’option correspondante à la commande docker run.
Saisissez la commande suivante dans le terminal :
Supprimer ou refuser les capacités Linux
Les capacités Linux sont des privilèges que nous pouvons activer ou désactiver sous Linux. Comme notre conteneur repose sur une variante de Linux, nous pouvons également utiliser ces capacités pour renforcer la sécurité de notre application. La suppression de certaines capacités réduit les risques pour notre conteneur.
Pour ce tutoriel, supprimons toutes les capacités à l’exception de setuid.
Saisissez la commande suivante dans le terminal :
Limiter l’utilisation du processeur et de la mémoire
La commande docker run permet de définir des limites d’utilisation des ressources de la machine hôte pour notre conteneur Docker. Docker utilise l’option --cpus.
Par exemple, pour empêcher le conteneur d’utiliser plus de 50 % d’un seul processeur, saisissez la commande suivante dans l’interface de ligne de commande Docker :
Si vous devez utiliser 2 processeurs, vous pouvez en limiter l’utilisation avec la commande suivante dans l’interface de ligne de commande Docker :
Pour limiter à 1 024 Mo l’utilisation de la mémoire d’un conteneur Docker, vous pouvez utiliser la commande docker run, comme ceci :
Cette commande définit également une limite de mémoire de 256 Mo, appliquée lorsque Docker détecte que la mémoire de l’hôte est insuffisante.
Conclusion
Dans cet article, nous avons parcouru les étapes de configuration d’une application Go et de sa conteneurisation avec Docker. Nous avons également mis en œuvre le principe des builds multi-étapes pour optimiser les performances, créé une image à partir de l’image officielle Docker vide, et présenté quelques bonnes pratiques utiles lors de la création et du déploiement de conteneurs. Ces recommandations constituent un point de départ pour mettre en place une approche efficace et robuste de la sécurité et de la gestion de la mémoire lors de la conteneurisation d’applications web Go avec Docker.
Pour en savoir plus sur les bonnes pratiques de sécurité, consultez le centre de ressources Snyk Learn.
