Aujourd'hui, nous partageons avec vous la traduction d'un article d'un ingénieur IBM DevOps sur l'automatisation de la construction d'images Docker rapidement assemblées et facilement déboguées pour les projets Python à l'aide d'un Makefile. Ce projet facilite non seulement le débogage dans Docker, mais prend également en charge la qualité du code de votre projet. Détails, comme toujours, sous la coupe.
Chaque projet - que vous travailliez sur une application Web, la science des données ou l'intelligence artificielle peut bénéficier de CI / CD bien réglés, d'images Docker simultanément déboguées pendant le développement et optimisées pour un environnement de production, ou d'outils d'assurance qualité code tel que CodeClimate ou SonarCloud . Toutes ces choses sont couvertes dans cet article et montrent comment elles sont ajoutées à un projet Python.
Conteneurs déboguables pour le développement
Certaines personnes n'aiment pas Docker parce que les conteneurs peuvent être difficiles à déboguer ou parce que les images prennent du temps à se créer. Commençons donc par créer des images idéales pour le développement - rapides à créer et faciles à déboguer. Pour rendre une image facile à déboguer, vous avez besoin d'une image de base qui inclut tous les outils dont vous pourriez avoir besoin pour déboguer. Ce sont bash, vim, netcat, wget, cat, find, grep et autres.
Image Python: 3.8.1-bustersemble être un candidat parfait pour cette tâche. Il comprend de nombreux outils prêts à l'emploi, il est facile d'installer les outils manquants. L'image est grande, mais peu importe ici: elle ne sera utilisée qu'en développement. Comme vous l'avez probablement remarqué, les images sont très spécifiques. Le verrouillage des versions Python et Debian est intentionnel: vous voulez minimiser le risque de rupture causé par de nouvelles versions éventuellement incompatibles de Python ou Debian . Une image basée sur Alpine est possible comme alternative , mais elle peut poser quelques problèmes: à l'intérieur, elle utilise musl lib au lieu de glibcsur lequel Python s'appuie. Gardez cela à l'esprit si vous décidez de choisir une Alpine. En termes de vitesse, nous utiliserons des builds en plusieurs étapes pour mettre en cache autant de couches que possible. Ainsi, les dépendances et les outils tels que gcc , ainsi que toutes les dépendances nécessaires à l'application, ne sont pas chargés à chaque fois depuis requirements.txt . Pour accélérer encore les choses , une image de base personnalisée est créée à partir du python mentionné précédemment : 3.8.1-buster , qui contient tout ce dont nous avons besoin, car nous ne pouvons pas mettre en cache les étapes nécessaires pour télécharger et installer ces outils dans l'image finale
runner
. Mais arrĂŞtez de parler, jetons un coup d'Ĺ“il au Dockerfile:
# dev.Dockerfile
FROM python:3.8.1-buster AS builder
RUN apt-get update && apt-get install -y --no-install-recommends --yes python3-venv gcc libpython3-dev && \
python3 -m venv /venv && \
/venv/bin/pip install --upgrade pip
FROM builder AS builder-venv
COPY requirements.txt /requirements.txt
RUN /venv/bin/pip install -r /requirements.txt
FROM builder-venv AS tester
COPY . /app
WORKDIR /app
RUN /venv/bin/pytest
FROM martinheinz/python-3.8.1-buster-tools:latest AS runner
COPY --from=tester /venv /venv
COPY --from=tester /app /app
WORKDIR /app
ENTRYPOINT ["/venv/bin/python3", "-m", "blueprint"]
USER 1001
LABEL name={NAME}
LABEL version={VERSION}
Ci-dessus, vous pouvez voir que le code
runner
passera par 3 images intermédiaires avant de créer l' image finale . Le premier est le constructeur . Il télécharge toutes les bibliothèques nécessaires à la construction de l'application, y compris gcc et l'environnement virtuel Python. Après l'installation, un véritable environnement virtuel est créé et utilisé par les images suivantes. Vient ensuite builder-vv , qui copie la liste des dépendances (requirements.txt) dans l'image, puis les installe. Cette image intermédiaire est nécessaire pour la mise en cache: vous ne souhaitez installer les bibliothèques que si requirements.txt change, sinon nous n'utilisons que le cache. Testons l'application avant de créer l'image finale.
Avant de créer notre image finale, commençons par lancer les tests de notre application. Copiez le code source et exécutez les tests. Une fois les tests réussis, accédez à l'image du coureur . Cela utilise une image personnalisée avec des outils supplémentaires introuvables dans l'image Debian standard: vim et netcat. Cette image est sur Docker Hub , et vous pouvez également consulter un Dockerfile très simple dans base.Dockerfile . Donc ce que nous faisons dans cette image finale: d'abord nous copions l'environnement virtuel où toutes les dépendances que nous avons installées à partir de l'image du testeur sont stockées, puis copiez l'application testée. Maintenant que toutes les sources sont dans l'image, déplacez-vous vers le répertoire où se trouve l'application et installez ENTRYPOINT afin que lorsque l'image est lancée, l'application soit lancée. Pour des raisons de sécurité, USER est défini sur 1001 : la meilleure pratique recommande de ne jamais exécuter de conteneurs en tant que root. Les 2 dernières lignes définissent les étiquettes de l'image. Ils seront remplacés lors de la construction à travers la cible
make
, ce que nous verrons un peu plus tard.
Conteneurs optimisés pour l'environnement de production
En ce qui concerne les looks de production, vous voulez vous assurer qu'ils sont petits, sûrs et rapides. Mon préféré dans ce sens est l'image Python du projet Distroless . Mais qu'est-ce que "Distroless"? Disons-le de cette façon: dans un monde idéal, chacun construirait sa propre image en utilisant FROM scratch comme base (c'est-à -dire une image vide). Mais ce n'est pas ce que la plupart d'entre nous veulent, car cela nécessite une liaison statique des binaires, etc. C'est là que Distroless entre en jeu : c'est un FROM scratch pour tout le monde. Et maintenant, je vais vraiment vous dire ce qu'est "Distroless". Ceci est un ensemble créé par Googleimages contenant le minimum absolu requis par l'application. Cela signifie qu'il n'y a pas de wrappers, de gestionnaires de packages ou d'autres outils qui gonflent l'image et génèrent un bruit de signal pour les scanners de sécurité (tels que CVE ), ce qui rend difficile l'établissement de la conformité. Maintenant que nous savons à quoi nous avons affaire, jetons un coup d'œil au Dockerfile de production. En fait, vous n'avez pas besoin de beaucoup changer le code, il vous suffit de changer 2 lignes:
# prod.Dockerfile
# 1. Line - Change builder image
FROM debian:buster-slim AS builder
# ...
# 17. Line - Switch to Distroless image
FROM gcr.io/distroless/python3-debian10 AS runner
# ... Rest of the Dockefile
Tout ce dont nous avions besoin pour changer était nos images de base pour créer et exécuter l'application! Mais la différence est assez grande - l'image de développement pesait 1,03 Go, et celle-ci ne faisait que 103 Mo, et c'est une grande différence! Et je peux déjà vous entendre: "Alpina peut peser encore moins!" ... Oui, mais la taille importe peu. Vous ne remarquerez que la taille de l'image lors du chargement / déchargement, cela n'arrive pas très souvent. Lorsque l'image fonctionne, la taille n'a pas d'importance. Ce qui est plus important que la taille, c'est la sécurité, et à cet égard Distroless est clairement supérieur à Alpine: Alpine a de nombreux packages supplémentaires pour augmenter la surface d'attaque. La dernière chose à mentionner quand on parle de Distroless est le débogage d'image. Étant donné queDistroless ne contient aucun wrapper (pas même "sh"), le débogage et l'exploration deviennent assez difficiles. Pour cela, il existe des versions "debug" de toutes les images Distroless . De cette façon, lorsqu'un problème survient, il est possible de créer votre image de travail à l'aide d'une balise
debug
et de la déployer avec votre image habituelle, d'effectuer le nécessaire dans l'image de débogage et de faire, par exemple, un vidage de flux. Il est possible d'utiliser la version de débogage de l'image python3 comme ceci:
docker run --entrypoint=sh -ti gcr.io/distroless/python3-debian10:debug
Une Ă©quipe pour tout
Avec tous les Dockerfiles prêts, vous pouvez automatiser tout ce cauchemar avec un Makefile! La première chose que nous voulons faire est de créer l'application à l'aide de Docker. Par conséquent, pour créer une image de développement, nous écrirons
make build-dev
qui exécute le code suivant:
# The binary to build (just the basename).
MODULE := blueprint
# Where to push the docker image.
REGISTRY ?= docker.pkg.github.com/martinheinz/python-project-blueprint
IMAGE := $(REGISTRY)/$(MODULE)
# This version-strategy uses git tags to set the version string
TAG := $(shell git describe --tags --always --dirty)
build-dev:
@echo "\n${BLUE}Building Development image with labels:\n"
@echo "name: $(MODULE)"
@echo "version: $(TAG)${NC}\n"
@sed \
-e 's|{NAME}|$(MODULE)|g' \
-e 's|{VERSION}|$(TAG)|g' \
dev.Dockerfile | docker build -t $(IMAGE):$(TAG) -f- .
Cette cible construit une image en remplaçant d'abord les étiquettes en bas par le
dev.Dockerfile
nom de l'image et la balise créée au lancement git describe
, puis elle est lancée docker build
. Vient ensuite la construction de l'environnement de production avec make build-prod VERSION=1.0.0
:
build-prod:
@echo "\n${BLUE}Building Production image with labels:\n"
@echo "name: $(MODULE)"
@echo "version: $(VERSION)${NC}\n"
@sed \
-e 's|{NAME}|$(MODULE)|g' \
-e 's|{VERSION}|$(VERSION)|g' \
prod.Dockerfile | docker build -t $(IMAGE):$(VERSION) -f- .
Cette cible est très similaire à la précédente, mais au lieu d'utiliser la balise git comme version, la version passée en argument est utilisée, dans l'exemple ci-dessus, elle est 1.0.0. Lorsque tout démarre dans Docker , à un moment donné, vous devez également tout déboguer dans Docker . Il y a un objectif pour cela:
# Example: make shell CMD="-c 'date > datefile'"
shell: build-dev
@echo "\n${BLUE}Launching a shell in the containerized build environment...${NC}\n"
@docker run \
-ti \
--rm \
--entrypoint /bin/bash \
-u $$(id -u):$$(id -g) \
$(IMAGE):$(TAG) \
$(CMD)
Dans le code ci-dessus, vous pouvez voir que le point d'entrée est remplacé par bash et que la commande container est remplacée par un argument dans le CMD. Ainsi, nous pouvons soit simplement entrer dans le conteneur et fouiller, soit exécuter une sorte de commande, comme dans l'exemple ci-dessus. Une fois que nous avons terminé la programmation et que nous avons poussé l'image dans le registre Docker, nous pouvons utiliser
make push VERSION=0.0.2
. Voyons ce que fait cet objectif:
REGISTRY ?= docker.pkg.github.com/martinheinz/python-project-blueprint
push: build-prod
@echo "\n${BLUE}Pushing image to GitHub Docker Registry...${NC}\n"
@docker push $(IMAGE):$(VERSION)
Il lance d'abord la cible discutée précédemment
build-prod
, puis simplement docker push
. Cela suppose que vous êtes connecté au registre Docker, donc cette cible doit être effectuée avant l'exécution docker login
. Le but final est de nettoyer les artefacts Docker. Cela utilise la balise de nom, qui a été remplacée dans les fichiers de construction de l'image Docker, pour filtrer et trouver les artefacts qui doivent être supprimés:
docker-clean:
@docker system prune -f --filter "label=name=$(MODULE)"
Tout le code Makefile est dans le référentiel .
CI / CD avec actions GitHub
Le projet utilise make, Github Actions et le registre de packages Github pour créer des pipelines (tâches) et stocker nos images pour configurer CI / CD. Mais qu'est-ce que c'est?
- Les actions GitHub sont des tâches / pipelines qui aident à automatiser les flux de travail de développement. Il est possible de les utiliser pour créer des tâches distinctes, puis de les combiner dans des flux de travail personnalisés qui sont exécutés, par exemple, chaque fois que des données sont soumises au référentiel ou lorsqu'une version est créée.
- Le registre de packages Github est un service d'hébergement de packages entièrement intégré à GitHub. Il vous permet de stocker différents types de packages, tels que des gemmes Ruby ou des packages npm . Le projet l'utilise pour stocker des images Docker. En savoir plus sur Registre de package Github peut être ici .
Pour utiliser les actions GitHub , des flux de travail sont créés dans le projet en fonction des déclencheurs sélectionnés (un exemple de déclencheur est soumis au référentiel). Ces workflows sont des fichiers YAML dans le répertoire
.github/workflows
:
.github
└── workflows
├── build-test.yml
└── push.yml
Le fichier build-test.yml contient 2 jobs qui sont exécutés à chaque fois que le code est soumis au référentiel, ils sont affichés ci-dessous:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Run Makefile build for Development
run: make build-dev
La première tâche, appelée build, vérifie que l'application peut être créée en exécutant la cible
make build-dev
. Cependant, avant de démarrer, il vérifie le référentiel en l'exécutant checkout
publié sur GitHub.
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-python@v1
with:
python-version: '3.8'
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run Makefile test
run: make test
- name: Install Linters
run: |
pip install pylint
pip install flake8
pip install bandit
- name: Run Linters
run: make lint
La deuxième tâche est un peu plus difficile. Il exécute des tests à côté de l'application, ainsi que 3 linters de contrôle de qualité de code (contrôleurs de qualité de code). Comme dans la tâche précédente, une action est utilisée pour obtenir le code source
checkout@v1
. Après cela, une autre action publiée appelée setup-python@v1
, qui configure l'environnement python, est lancée (plus à ce sujet ici ). Maintenant que nous avons un environnement Python, nous avons besoin de dépendances d'application à partir requirements.txt
desquelles sont installées à l'aide de pip. À ce stade make test
, commençons à exécuter la cible , elle exécute la suite de tests Pytest . Si les tests du kit réussissent, procédez à l'installation des linters mentionnés précédemment - pylint , flake8 et bandit . Enfin, nous lançons la ciblemake lint
qui Ă son tour lance chacun de ces linters. Tout tourne autour du travail de build / test, mais qu'en est-il de la soumission du code? Parlons d'elle:
on:
push:
tags:
- '*'
jobs:
push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set env
run: echo ::set-env name=RELEASE_VERSION::$(echo ${GITHUB_REF:10})
- name: Log into Registry
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login docker.pkg.github.com -u ${{ github.actor }} --password-stdin
- name: Push to GitHub Package Registry
run: make push VERSION=${{ env.RELEASE_VERSION }}
Les 4 premières lignes définissent le début du travail. Nous indiquons que ce job ne doit être déclenché que lorsque les balises sont déplacées vers le référentiel (* indique un modèle de nom, ici ce sont toutes des balises ). Ceci est fait pour que nous ne poussions pas l'image Docker dans le registre du package GitHub chaque fois que nous envoyons des données vers le référentiel, mais uniquement lorsque la balise indiquant la nouvelle version de notre application est téléchargée. Maintenant, pour le corps de cette tâche - elle commence par vérifier le code source et définir la valeur de la variable d'environnement RELEASE_VERSION égale à la balise téléchargée git. Cela se fait en utilisant la fonction intégrée Actions GitHub :: setenv (plus de détails ici). Ensuite, la tâche entre dans le registre Docker avec le secret REGISTRY_TOKEN stocké dans le référentiel et la connexion de l'utilisateur qui a lancé le workflow (github.actor). Enfin, la dernière ligne exécute la cible push, qui construit l'image de production et la pousse dans le registre avec la balise git précédemment publiée comme balise d'image. Découvrez tout le code dans mes fichiers de référentiel .
Contrôle de la qualité du code avec CodeClimate
Enfin, ajoutons la vérification de la qualité du code à l'aide de CodeClimate et SonarCloud . Ils travailleront avec la tâche de test indiquée ci-dessus. Ajoutez quelques lignes de code:
# test, lint...
- name: Send report to CodeClimate
run: |
export GIT_BRANCH="${GITHUB_REF/refs\/heads\//}"
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
chmod +x ./cc-test-reporter
./cc-test-reporter format-coverage -t coverage.py coverage.xml
./cc-test-reporter upload-coverage -r "${{ secrets.CC_TEST_REPORTER_ID }}"
- name: SonarCloud scanner
uses: sonarsource/sonarcloud-github-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
Ă€ partir de CodeClimate : exportation d'une variable
GIT_BRANCH
récupérée à l'aide d'une variable d'environnement GITHUB_REF
. Ensuite, nous téléchargeons l' outil de rapport de test CodeClimate et le rendons exécutable. Ensuite, nous l'utiliserons pour formater le rapport de couverture de la suite de tests. Dans la dernière ligne, nous l'envoyons à CodeClimate avec l'ID de l'outil pour le rapport de test, qui est stocké dans les secrets du référentiel. Pour SonarCloud , vous devez créer un fichiersonar-project.properties
. Les valeurs de ce fichier se trouvent sur le tableau de bord SonarCloud dans le coin inférieur droit, et ce fichier ressemble à ceci:
sonar.organization=martinheinz-github
sonar.projectKey=MartinHeinz_python-project-blueprint
sonar.sources=blueprint
De plus, il est possible d'utiliser simplement celui qui fait le travail pour nous
sonarcloud-github-action
. Tout ce que nous avons à faire est de fournir deux jetons: pour GitHub, celui du référentiel par défaut, et pour SonarCloud , celui que nous avons obtenu sur le site Web SonarCloud . Remarque: les étapes pour obtenir et installer tous les jetons et secrets mentionnés sont décrites dans le README du référentiel .
Conclusion
C'est tout! Avec des outils, des configurations et du code, vous êtes prêt à personnaliser et à automatiser tous les aspects de votre prochain projet Python! Si vous avez besoin de plus d'informations sur les sujets présentés ou abordés dans cet article, consultez la documentation et le code de mon référentiel , et si vous avez des suggestions ou des problèmes, veuillez soumettre une demande au référentiel, ou simplement mettre en vedette ce petit projet si c'est pour vous. comme.
Et avec le code promo HABR , vous pouvez obtenir un supplément de 10% sur la réduction indiquée sur le bandeau.
- Enseigner le métier de Data Science à partir de zéro
- Bootcamp en ligne pour la science des données
- Former le métier d'analyste de données à partir de zéro
- Bootcamp en ligne sur l'analyse des données
- Cours Python pour le développement Web
Plus de cours