Ajouter de la valeur avec des images Docker personnalisées

Beginner

This tutorial is from open-source community. Access the source code

Introduction

Dans ce laboratoire, nous nous appuyons sur les connaissances acquises dans le laboratoire 1 où nous avons utilisé des commandes Docker pour exécuter des conteneurs. Nous allons créer une image Docker personnalisée à partir d'un Dockerfile. Une fois que nous aurons construit l'image, nous la pousserons vers un registre central où elle pourra être extraite pour être déployée dans d'autres environnements. Nous décrirons également brièvement les couches d'image, et la manière dont Docker incorpore le "copy-on-write" et le système de fichiers union pour stocker efficacement les images et exécuter les conteneurs.

Nous utiliserons quelques commandes Docker dans ce laboratoire. Pour la documentation complète des commandes disponibles, consultez la documentation officielle.

Ceci est un Guided Lab, qui fournit des instructions étape par étape pour vous aider à apprendre et à pratiquer. Suivez attentivement les instructions pour compléter chaque étape et acquérir une expérience pratique. Les données historiques montrent que c'est un laboratoire de niveau débutant avec un taux de réussite de 83%. Il a reçu un taux d'avis positifs de 100% de la part des apprenants.

Créer une application Python (sans utiliser Docker)

Exécutez la commande suivante pour créer un fichier nommé app.py avec un programme Python simple. (Copiez/collez le bloc de code entier)

cd ~/project
echo 'from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return "hello world!"

if __name__ == "__main__":
    app.run(host="0.0.0.0")' > app.py

Il s'agit d'une simple application Python qui utilise Flask pour exposer un serveur web HTTP sur le port 5000 (5000 est le port par défaut pour Flask). Ne vous inquiétez pas si vous n'êtes pas très familier avec Python ou Flask, ces concepts peuvent s'appliquer à une application écrite dans n'importe quelle langue.

Facultatif : Si vous avez Python et pip installés, vous pouvez exécuter cette application localement. Sinon, passez à l'étape suivante.

$ python3 --version
$ pip3 --version
$ pip3 install flask

$ python3 app.py
 * Serving Flask app "app" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

Ouvrez l'application dans un nouvel onglet de navigateur en utilisant http://0.0.0.0:5000/.

Flask app browser output

Créer et construire l'image Docker

Maintenant, et si vous n'avez pas Python installé localement? Ne vous inquiétez pas! Parce que vous n'en avez pas besoin. L'un des avantages d'utiliser des conteneurs est que vous pouvez installer Python dans vos conteneurs, sans avoir besoin d'installer Python sur votre machine hôte.

Créez un Dockerfile en exécutant la commande suivante. (Copiez/collez le bloc de code entier)

echo 'FROM python:3.8-alpine
RUN pip install flask
CMD ["python","app.py"]
COPY app.py /app.py' > Dockerfile

Un Dockerfile liste les instructions nécessaires pour construire une image Docker. Passons en revue le fichier ci-dessus ligne par ligne.

FROM python:3.8-alpine C'est le point de départ de votre Dockerfile. Tout Dockerfile doit commencer par une ligne FROM qui est l'image de départ sur laquelle vous construirez vos couches.

Dans ce cas, nous sélectionnons la couche de base python:3.8-alpine (voir Dockerfile pour python3.8/alpine3.12) car elle a déjà la version de Python et de pip dont nous avons besoin pour exécuter notre application.

La version alpine signifie qu'elle utilise la distribution Alpine Linux, qui est considérablement plus petite que de nombreuses autres variétés de Linux, environ 8 Mo de taille, tandis qu'une installation minimale sur disque peut être d'environ 130 Mo. Une image plus petite signifie qu'elle téléchargera (déployera) beaucoup plus rapidement, et elle présente également des avantages en termes de sécurité car elle a une surface d'attaque plus réduite. Alpine Linux est une distribution Linux basée sur musl et BusyBox.

Ici, nous utilisons l'étiquette "3.8-alpine" pour l'image Python. Consultez les étiquettes disponibles pour l'image Python officielle sur le Docker Hub. Il est recommandé de choisir une étiquette spécifique lors de l'héritage d'une image parent afin de contrôler les modifications de la dépendance parentale. Si aucune étiquette n'est spécifiée, l'étiquette "latest" prend effet, qui est un pointeur dynamique qui pointe vers la dernière version d'une image.

Pour des raisons de sécurité, il est très important de comprendre les couches sur lesquelles vous construisez votre image Docker. Pour cette raison, il est fortement recommandé d'utiliser uniquement des images "officielles" trouvées sur le docker hub, ou des images non communautaires trouvées dans le docker-store. Ces images sont vérifiées pour répondre à certaines exigences de sécurité et ont également une excellente documentation pour les utilisateurs. Vous pouvez trouver plus d'informations sur cette image de base Python, ainsi que sur toutes les autres images que vous pouvez utiliser, sur le docker hub.

Pour une application plus complexe, vous pouvez avoir besoin d'utiliser une image FROM plus haut dans la chaîne. Par exemple, le Dockerfile parent de notre application Python commence par FROM alpine, puis spécifie une série de commandes CMD et RUN pour l'image. Si vous aviez besoin d'un contrôle plus fin, vous pourriez commencer par FROM alpine (ou une autre distribution) et exécuter ces étapes vous-même. Pour commencer, je recommande d'utiliser une image officielle qui répondra le plus précisément possible à vos besoins.

RUN pip install flask La commande RUN exécute les commandes nécessaires pour configurer votre image pour votre application, telles que l'installation de packages, l'édition de fichiers ou la modification des permissions de fichiers. Dans ce cas, nous installons flask. Les commandes RUN sont exécutées lors de la construction et sont ajoutées aux couches de votre image.

CMD ["python","app.py"] CMD est la commande qui est exécutée lorsque vous lancez un conteneur. Ici, nous utilisons CMD pour exécuter notre application Python.

Il ne peut y avoir qu'une seule commande CMD par Dockerfile. Si vous spécifiez plusieurs commandes CMD, alors la dernière commande CMD entrera en vigueur. Le parent python:3.8-alpine spécifie également une commande CMD (CMD python3). Vous pouvez trouver le Dockerfile pour l'image officielle python:alpine ici.

Vous pouvez utiliser directement l'image Python officielle pour exécuter des scripts Python sans installer Python sur votre hôte. Mais aujourd'hui, nous créons une image personnalisée pour inclure notre source, afin que nous puissions construire une image avec notre application et la distribuer dans d'autres environnements.

COPY app.py /app.py Ceci copie le fichier app.py dans le répertoire local (où vous exécuterez docker image build) dans une nouvelle couche de l'image. Cette instruction est la dernière ligne du Dockerfile. Les couches qui changent fréquemment, telles que la copie du code source dans l'image, devraient être placées près du bas du fichier pour profiter pleinement du cache de couches Docker. Cela nous permet d'éviter de reconstruire les couches qui pourraient autrement être mises en cache. Par exemple, si il y avait un changement dans l'instruction FROM, cela invaliderait le cache pour toutes les couches suivantes de cette image. Nous le démontrerons un peu plus tard dans ce laboratoire.

Il peut sembler contre-intuitif de le placer après la ligne CMD ["python","app.py"]. Rappelez-vous, la ligne CMD est exécutée seulement lorsque le conteneur est démarré, donc nous n'aurons pas d'erreur fichier non trouvé ici.

Et voilà : un Dockerfile très simple. Vous trouverez une liste complète des commandes que vous pouvez insérer dans un Dockerfile ici. Maintenant que nous avons défini notre Dockerfile, utilisons-le pour construire notre image Docker personnalisée.

Construit l'image Docker.

Ajoutez -t pour nommer votre image python-hello-world.

docker image build -t python-hello-world.

Vérifiez que votre image apparaît dans votre liste d'images.

docker image ls

Remarque que votre image de base python:3.8-alpine est également dans votre liste.

Vous pouvez exécuter une commande d'historique pour afficher l'historique d'une image et de ses couches,

docker history python-hello-world
docker history python:3.8-alpine

Exécuter l'image Docker

Maintenant que vous avez construit l'image, vous pouvez l'exécuter pour voir si elle fonctionne.

Exécutez l'image Docker

docker run -p 5001:5000 -d python-hello-world

Le drapeau -p mappe un port exécutant à l'intérieur du conteneur vers votre hôte. Dans ce cas, nous sommes en train de mapper l'application Python exécutant sur le port 5000 à l'intérieur du conteneur, vers le port 5001 sur votre hôte. Notez que si le port 5001 est déjà utilisé par une autre application sur votre hôte, vous devrez peut-être remplacer 5001 par une autre valeur, telle que 5002.

Accédez à l'onglet PORTS dans la fenêtre du terminal et cliquez sur le lien pour ouvrir l'application dans un nouvel onglet de navigateur.

Terminal ports tab link

Dans un terminal, exécutez curl localhost:5001, qui renvoie hello world!.

Vérifiez la sortie des journaux du conteneur.

Si vous voulez voir les journaux de votre application, vous pouvez utiliser la commande docker container logs. Par défaut, docker container logs imprime ce qui est envoyé à la sortie standard par votre application. Utilisez docker container ls pour trouver l'ID de votre conteneur en cours d'exécution.

labex:project/ $ docker container ls
CONTAINER ID   IMAGE                COMMAND           CREATED         STATUS         PORTS                                       NAMES
52df977e5541   python-hello-world   "python app.py"   2 minutes ago   Up 2 minutes   0.0.0.0:5001->5000/tcp, :::5001->5000/tcp   heuristic_lamport
labex:project/ $ docker container logs 52df977e5541
 * Serving Flask app 'app'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:5000
 * Running on http://172.17.0.2:5000
Press CTRL+C to quit
172.17.0.1 - - [23/Jan/2024 02:43:10] "GET / HTTP/1.1" 200 -
172.17.0.1 - - [23/Jan/2024 02:43:10] "GET /favicon.ico HTTP/1.1" 404 -

Le Dockerfile est le moyen de créer des builds reproductibles pour votre application. Un workflow courant consiste à faire exécuter docker image build par votre automatisation CI/CD en tant que partie de son processus de build. Une fois les images construites, elles seront envoyées vers un registre central, où elles peuvent être accessibles par tous les environnements (tels qu'un environnement de test) qui ont besoin d'exécuter des instances de cette application. Dans l'étape suivante, nous pousserons notre image personnalisée vers le registre Docker public : le Docker Hub, où elle peut être consommée par d'autres développeurs et opérateurs.

Pousser vers un registre central

Accédez à Docker Hub et créez un compte si vous n'en avez pas déjà. Alternativement, vous pouvez également utiliser https://quay.io par exemple.

Pour ce laboratoire, nous utiliserons Docker Hub comme notre registre central. Docker Hub est un service gratuit pour stocker des images disponibles publiquement, ou vous pouvez payer pour stocker des images privées. Accédez au site web Docker Hub et créez un compte gratuit.

La plupart des organisations qui utilisent intensément Docker installeront leur propre registre en interne. Pour simplifier les choses, nous utiliserons Docker Hub, mais les concepts suivants s'appliquent à tout registre.

Connexion

Vous pouvez vous connecter au compte du registre d'images en tapant docker login sur votre terminal, ou si vous utilisez podman, tapez podman login.

labex:project/ $ export DOCKERHUB_USERNAME=<votre_nom_docker>
labex:project/ $ docker login docker.io -u $DOCKERHUB_USERNAME
Mot de passe :
AVERTISSEMENT! Votre mot de passe sera stocké en clair dans /home/labex/.docker/config.json.
Configurez un assistant de credentials pour supprimer cet avertissement. Consultez
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Connexion réussie

Taguez votre image avec votre nom d'utilisateur

La convention de nommage de Docker Hub est de taguer votre image avec [nom d'utilisateur dockerhub]/[nom d'image]. Pour ce faire, nous allons taguer notre image python-hello-world précédemment créée pour qu'elle corresponde à ce format.

docker tag python-hello-world $DOCKERHUB_USERNAME/python-hello-world

Poussez votre image vers le registre

Une fois que nous avons une image correctement taguée, nous pouvons utiliser la commande docker push pour pousser notre image vers le registre Docker Hub.

docker push $DOCKERHUB_USERNAME/python-hello-world

Consultez votre image sur Docker Hub dans votre navigateur

Accédez à Docker Hub et allez dans votre profil pour voir votre image nouvellement téléchargée à https://hub.docker.com/repository/docker/<nom_dockerhub>/python-hello-world.

Maintenant que votre image est sur Docker Hub, d'autres développeurs et opérations peuvent utiliser la commande docker pull pour déployer votre image dans d'autres environnements.

Remarque : Les images Docker contiennent toutes les dépendances dont elles ont besoin pour exécuter une application à l'intérieur de l'image. Cela est pratique car nous n'avons plus à gérer la dérive d'environnement (différences de versions) lorsque nous dépendons de dépendances installées sur chaque environnement dans lequel nous déployons. Nous n'avons pas non plus à passer par des étapes supplémentaires pour provisionner ces environnements. Juste une étape : installer Docker, et vous êtes prêt.

Déploiement d'un changement

L'application "hello world!" est surestimée, mettons à jour l'application pour qu'elle dise "Hello Beautiful World!" à la place.

Mettre à jour app.py

Remplacez la chaîne "Hello World" par "Hello Beautiful World!" dans app.py. Vous pouvez mettre à jour le fichier avec la commande suivante. (Copiez/collez le bloc de code entier)

echo 'from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return "hello beautiful world!"

if __name__ == "__main__":
    app.run(host="0.0.0.0")' > app.py

Rebuild and Push Your Image

Maintenant que votre application est mise à jour, vous devez répéter les étapes ci-dessus pour reconstruire votre application et la pousser vers le registre Docker Hub.

Tout d'abord, reconstruisez, cette fois-ci utilisez votre nom d'utilisateur Docker Hub dans la commande de construction :

docker image build -t $DOCKERHUB_USERNAME/python-hello-world.

Remarquez "Using cache" pour les étapes 1-3. Ces couches de l'image Docker ont déjà été construites et docker image build utilisera ces couches à partir du cache au lieu de les reconstruire.

docker push $DOCKERHUB_USERNAME/python-hello-world

Il existe également un mécanisme de mise en cache pour pousser les couches. Docker Hub a déjà toutes les couches sauf une d'un push antérieur, donc il ne pousse que la couche qui a changé.

Lorsque vous modifiez une couche, toutes les couches construites au-dessus de celle-ci devront être reconstruites. Chaque ligne dans un Dockerfile construit une nouvelle couche qui est construite sur la couche créée à partir des lignes précédentes. C'est pourquoi l'ordre des lignes dans notre Dockerfile est important. Nous avons optimisé notre Dockerfile de sorte que la couche la plus susceptible de changer (COPY app.py /app.py) soit la dernière ligne du Dockerfile. Généralement pour une application, vos modifications de code sont les plus fréquentes. Cette optimisation est particulièrement importante pour les processus CI/CD, où vous voulez que votre automatisation s'exécute le plus rapidement possible.

Comprendre les couches d'image

L'une des principales propriétés de conception de Docker est son utilisation du système de fichiers union.

Considérez le Dockerfile que nous avons créé précédemment :

FROM python:3.8-alpine
RUN pip install flask
CMD ["python","app.py"]
COPY app.py /app.py

Chacune de ces lignes est une couche. Chaque couche contient uniquement le delta, la différence ou les modifications par rapport aux couches précédentes. Pour assembler ces couches en un seul conteneur en cours d'exécution, Docker utilise le système de fichiers union pour superposer les couches de manière transparente en une seule vue.

Chaque couche de l'image est en lecture seule, sauf la couche la plus haute qui est créée pour le conteneur en cours d'exécution. La couche de conteneur en lecture/écriture implémente le "copier lors de l'écriture", ce qui signifie que les fichiers stockés dans les couches d'image inférieures sont extraits jusqu'à la couche de conteneur en lecture/écriture seulement lorsqu'il y a des modifications apportées à ces fichiers. Ces modifications sont ensuite stockées dans la couche de conteneur en cours d'exécution. La fonction "copier lors de l'écriture" est très rapide et, dans presque tous les cas, n'a pas d'effet notable sur les performances. Vous pouvez examiner quels fichiers ont été extraits jusqu'au niveau du conteneur avec la commande docker diff. Plus d'informations sur la façon d'utiliser docker diff peuvent être trouvées ici.

understanding image layers

Depuis les couches d'image sont en lecture seule, elles peuvent être partagées par des images et par des conteneurs en cours d'exécution. Par exemple, créer une nouvelle application Python avec son propre Dockerfile avec des couches de base similaires, partagera toutes les couches qu'elle avait en commun avec la première application Python.

FROM python:3.8-alpine
RUN pip install flask
CMD ["python","app2.py"]
COPY app2.py /app2.py

understanding image layers

Vous pouvez également ressentir le partage des couches lorsque vous lancez plusieurs conteneurs à partir de la même image. Étant donné que les conteneurs utilisent les mêmes couches en lecture seule, vous pouvez imaginer que le lancement de conteneurs est très rapide et a une empreinte très faible sur l'hôte.

Vous pouvez remarquer qu'il y a des lignes dupliquées dans ce Dockerfile et dans le Dockerfile que vous avez créé plus tôt dans ce laboratoire. Bien que ce soit un exemple très simple, vous pouvez extraire les lignes communes de ces deux Dockerfiles dans un Dockerfile "de base", que vous pouvez ensuite pointer avec chacun de vos Dockerfiles enfants en utilisant la commande FROM.

La stratification d'image active le mécanisme de mise en cache Docker pour les builds et les poussées. Par exemple, la sortie de votre dernière commande docker push montre que certaines des couches de votre image existent déjà sur Docker Hub.

$ docker push $DOCKERHUB_USERNAME/python-hello-world

Pour examiner plus attentivement les couches, vous pouvez utiliser la commande docker image history de l'image Python que nous avons créée.

$ docker image history python-hello-world

Chaque ligne représente une couche de l'image. Vous remarquerez que les lignes supérieures correspondent à votre Dockerfile que vous avez créé, et les lignes suivantes sont extraites de l'image Python parent. Ne vous inquiétez pas des étiquettes "<missing>". Ce sont toujours des couches normales ; elles n'ont simplement pas été attribuées un ID par le système Docker.

Nettoyer

La fin de ce laboratoire résulte d'un certain nombre de conteneurs en cours d'exécution sur votre hôte. Nettoyons-les.

Exécutez docker container stop [id du conteneur] pour chaque conteneur en cours d'exécution

Commencez par obtenir une liste des conteneurs en cours d'exécution en utilisant docker container ls.

$ docker container ls

Ensuite, exécutez la commande pour chaque conteneur de la liste.

$ docker container stop <id_du_conteneur>

Supprimez les conteneurs arrêtés

docker system prune est une commande très pratique pour nettoyer votre système. Elle supprimera tous les conteneurs arrêtés, les volumes et les réseaux inutilisés, et les images orphelines.

$ docker system prune
AVERTISSEMENT! Cela supprimera :
- tous les conteneurs arrêtés
- tous les volumes non utilisés par au moins un conteneur
- tous les réseaux non utilisés par au moins un conteneur
- toutes les images orphelines
Êtes-vous sûr de vouloir continuer? [y/N] y
Conteneurs supprimés :
0b2ba61df37fb4038d9ae5d145740c63c2c211ae2729fc27dc01b82b5aaafa26

Espace total récupéré : 300,3 kB

Résumé

Dans ce laboratoire, vous avez commencé à ajouter de la valeur en créant vos propres conteneurs Docker personnalisés.

Idées clés :

  • Le Dockerfile est la manière dont vous créez des builds reproductibles pour votre application et la manière dont vous intégrez votre application avec Docker dans le pipeline CI/CD.
  • Les images Docker peuvent être rendues disponibles pour tous vos environnements grâce à un registre central. Docker Hub est un exemple de registre, mais vous pouvez déployer votre propre registre sur des serveurs que vous contrôlez.
  • Les images Docker contiennent toutes les dépendances dont elles ont besoin pour exécuter une application à l'intérieur de l'image. Cela est pratique car nous n'avons plus à gérer la dérive d'environnement (différences de versions) lorsque nous dépendons de dépendances installées sur chaque environnement dans lequel nous déployons.
  • Docker utilise le système de fichiers union et le "copier lors de l'écriture" pour réutiliser les couches d'images. Cela réduit l'occupation mémoire de stockage des images et augmente considérablement les performances du lancement de conteneurs.
  • Les couches d'image sont mises en cache par le système de build et de poussée Docker. Il n'est pas nécessaire de reconstruire ou de repousser les couches d'image déjà présentes sur le système souhaité.
  • Chaque ligne dans un Dockerfile crée une nouvelle couche, et en raison du cache de couche, les lignes qui changent plus fréquemment (par exemple, ajout de code source à une image) devraient être listées près du bas du fichier.