Automatizar el trabajo con un proyecto de Python



Hoy compartimos con usted una traducción de un artículo de un ingeniero de IBM DevOps sobre la automatización de la construcción de imágenes de Docker ensambladas rápidamente y fácilmente depuradas para proyectos de Python utilizando un Makefile. Este proyecto no solo facilita la depuración en Docker, sino que también se ocupa de la calidad del código de su proyecto. Detalles, como siempre, bajo el corte.






Cada proyecto, ya sea que esté trabajando en una aplicación web, ciencia de datos o inteligencia artificial puede beneficiarse de CI / CD bien ajustados, imágenes de Docker que se depuran simultáneamente durante el desarrollo y se optimizan para un entorno de producción, o herramientas de control de calidad. código como CodeClimate o SonarCloud . Todas estas cosas se tratan en este artículo y se muestran cómo se agregan a un proyecto de Python.



Contenedores depurables para desarrollo



A algunas personas no les gusta Docker porque los contenedores pueden ser difíciles de depurar o porque las imágenes tardan mucho en compilarse. Así que comencemos por crear imágenes que sean ideales para el desarrollo: rápidas de crear y fáciles de depurar. Para que una imagen sea fácil de depurar, necesita una imagen base que incluya todas las herramientas que pueda necesitar para depurar. Estos son bash, vim, netcat, wget, cat, find, grep y otros.



Imagen de Python: 3.8.1-busterparece un candidato perfecto para esta tarea. Incluye muchas herramientas listas para usar, es fácil instalar las herramientas faltantes. La imagen es grande, pero aquí no importa: solo se usará en desarrollo. Como probablemente hayas notado, las imágenes son muy específicas. El bloqueo de las versiones de Python y Debian es intencional: desea minimizar el riesgo de rotura causado por versiones nuevas, posiblemente incompatibles de Python o Debian . Una imagen basada en Alpine es posible como alternativa , pero puede causar algunos problemas: en su interior usa musl lib en lugar de glibcen el que se basa Python. Tenga esto en cuenta si decide elegir un Alpine. En términos de velocidad, usaremos compilaciones de múltiples etapas para almacenar en caché tantas capas como sea posible. Por lo tanto, las dependencias y herramientas como gcc , así como todas las dependencias que necesita la aplicación, no se cargan desde requirements.txt cada vez. Para acelerar aún más las cosas , se crea una imagen base personalizada a partir del python mencionado anteriormente : 3.8.1-buster , que tiene todo lo que necesitamos, ya que no podemos almacenar en caché los pasos necesarios para descargar e instalar estas herramientas en la imagen final runner. Pero deja de hablar, echemos un vistazo al 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}


Arriba puede ver que el código runnerpasará por 3 imágenes intermedias antes de crear la imagen final . El primero es constructor . Descarga todas las bibliotecas necesarias para construir la aplicación, incluyendo gcc y el entorno virtual de Python. Después de la instalación, las siguientes imágenes crean y utilizan un entorno virtual real. Luego viene builder-vv , que copia la lista de dependencias (requirements.txt) en la imagen y luego las instala. Esta imagen intermedia es necesaria para el almacenamiento en caché: solo desea instalar bibliotecas si requirements.txt cambia, de lo contrario, solo usamos el caché. Probemos la aplicación antes de crear la imagen final.



Antes de crear nuestra imagen final, primero ejecutemos las pruebas de nuestra aplicación. Copie el código fuente y ejecute las pruebas. Cuando pasen las pruebas, ve a la imagen del corredor . Esto usa una imagen personalizada con algunas herramientas adicionales que no se encuentran en la imagen regular de Debian: vim y netcat. Esta imagen está en Docker Hub , y también puede ver un Dockerfile muy simple en base.Dockerfile . Entonces, qué hacemos en esta imagen final: primero copiamos el entorno virtual donde se almacenan todas las dependencias que instalamos desde la imagen del tester, luego copie la aplicación probada. Ahora que todas las fuentes están en la imagen, muévase al directorio donde se encuentra la aplicación e instale ENTRYPOINT para que cuando se lance la imagen, se inicie la aplicación. Por razones de seguridad, USER se establece en 1001 : las mejores prácticas recomiendan nunca ejecutar contenedores como root. Las últimas 2 líneas establecen las etiquetas de la imagen. Serán reemplazados cuando se construya a través del objetivo make, que veremos un poco más adelante.



Contenedores optimizados para el entorno de producción



Cuando se trata de estilos de producción, debes asegurarte de que sean pequeños, seguros y rápidos. Mi favorito personal en este sentido es la imagen de Python del proyecto Distroless . Pero, ¿qué es "Distroless"? Digámoslo de esta manera: en un mundo ideal, todos construirían su propia imagen usando FROM scratch como base (es decir, una imagen vacía). Pero eso no es lo que la mayoría de nosotros queremos, ya que requiere enlaces binarios estáticamente, etc. Ahí es donde entra en juego Distroless : es un DESDE cero para todos. Y ahora te diré realmente qué es "Distroless". Este es un conjunto creado por Googleimágenes que contengan el mínimo absoluto requerido por la aplicación. Esto significa que no hay envoltorios, administradores de paquetes u otras herramientas que inflen la imagen y generen ruido de señal para los escáneres de seguridad (como CVE ), lo que dificulta establecer el cumplimiento. Ahora que sabemos a qué nos enfrentamos, echemos un vistazo al Dockerfile de producción. De hecho, no necesita cambiar mucho el código, solo necesita cambiar 2 líneas:




# 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


¡Todo lo que necesitábamos cambiar eran nuestras imágenes base para crear y ejecutar la aplicación! Pero la diferencia es bastante grande: la imagen de desarrollo pesaba 1.03 GB y esta solo tenía 103 MB, ¡lo cual es una gran diferencia! Y ya te escucho: "¡Alpina puede pesar menos!" ... Sí lo es, pero el tamaño no importa mucho. Solo notarás el tamaño de la imagen al cargar / descargar, no sucede muy a menudo. Cuando la imagen funciona, el tamaño no importa. Lo que es más importante que el tamaño es la seguridad, y en este sentido, Distroless es definitivamente superior a Alpine: Alpine tiene muchos paquetes adicionales para aumentar la superficie de ataque. Lo último que vale la pena mencionar cuando se habla de Distroless es la depuración de imágenes. Teniendo en cuenta queDistroless no contiene ningún contenedor (ni siquiera "sh"), depurar y explorar se vuelve bastante difícil. Para ello, existen versiones " depuradas " de todas las imágenes de Distroless . De esa manera, cuando surgen problemas, es posible construir su imagen de trabajo usando una etiqueta debuge implementarla junto con su imagen habitual, realizar lo necesario en la imagen de depuración y hacer, por ejemplo, un volcado de flujo. Es posible usar la versión de depuración de la imagen de python3 de esta manera:



docker run --entrypoint=sh -ti gcr.io/distroless/python3-debian10:debug


Un equipo para todo



Con todos los Dockerfiles listos, ¡puedes automatizar toda esta pesadilla con un Makefile! Lo primero que queremos hacer es compilar la aplicación usando Docker. Por tanto, para construir una imagen de desarrollo, escribiremos make build-devque ejecute el siguiente código:




# 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- .




Este objetivo crea una imagen reemplazando primero las etiquetas en la parte inferior con el dev.Dockerfilenombre de la imagen y la etiqueta que se crea al iniciar git describe, luego se lanza docker build. A continuación, compile para el entorno de producción utilizando 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- .


Este objetivo es muy similar al anterior, pero en lugar de usar la etiqueta git como versión, se usa la versión pasada como argumento, en el ejemplo anterior es 1.0.0. Cuando todo se está ejecutando en Docker , en algún momento también necesitará depurar todo en Docker . Hay un objetivo para esto:




# 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)


En el código anterior, puede ver que bash anula el punto de entrada y un argumento en la CMD anula el comando del contenedor. Por lo tanto, podemos simplemente ir al contenedor y hurgar, o ejecutar algún tipo de comando, como en el ejemplo anterior. Una vez que terminemos de programar y empujemos la imagen al registro de Docker, podemos usar make push VERSION=0.0.2. Veamos qué hace este objetivo:




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)


Primero lanza el objetivo discutido anteriormente build-prody luego simplemente docker push. Esto supone que ha iniciado sesión en el registro de Docker, por lo que este objetivo debe ejecutarse antes de ejecutarse docker login. El objetivo final es limpiar los artefactos de Docker. Esto usa la etiqueta de nombre, que se reemplazó dentro de los archivos de compilación de imágenes de Docker, para filtrar y encontrar artefactos que deben eliminarse:




docker-clean:
 @docker system prune -f --filter "label=name=$(MODULE)"


Todo el código Makefile está en el repositorio .



CI / CD con acciones de GitHub



El proyecto usa make, Github Actions y el registro de paquetes de Github para construir pipelines (tareas) y almacenar nuestras imágenes para configurar CI / CD. ¿Pero, qué es esto?



  • Las acciones de GitHub son tareas / canalizaciones que ayudan a automatizar los flujos de trabajo de desarrollo. Es posible utilizarlos para crear tareas independientes y luego combinarlas en flujos de trabajo personalizados que se ejecutan, por ejemplo, cada vez que envía datos al repositorio o al crear una versión.
  • El Registro de paquetes de Github es un servicio de alojamiento de paquetes totalmente integrado con GitHub. Le permite almacenar diferentes tipos de paquetes, como Ruby gems o paquetes npm . El proyecto lo usa para almacenar imágenes de Docker. Más información sobre Github registro de paquete puede ser aquí .


Para usar las acciones de GitHub , los flujos de trabajo se crean en el proyecto en función de los desencadenantes seleccionados (el ejemplo de un desencadenador se envía al repositorio). Estos flujos de trabajo son archivos YAML en el directorio .github/workflows:




.github
└── workflows
    ├── build-test.yml
    └── push.yml


El archivo build-test.yml contiene 2 trabajos que se ejecutan cada vez que se envía el código al repositorio, se muestran a continuación:




jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v1
    - name: Run Makefile build for Development
      run: make build-dev


La primera tarea, llamada compilación, verifica que la aplicación se pueda compilar ejecutando el destino make build-dev. Sin embargo, antes de comenzar, verifica el repositorio ejecutándolo checkoutpublicado en 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 segunda tarea es un poco más difícil. Ejecuta pruebas junto a la aplicación, así como 3 linters de control de calidad de código (controladores de calidad de código). Como en la tarea anterior, se usa una acción para obtener el código fuente checkout@v1. Después de eso, se lanza otra acción publicada, llamada setup-python@v1configuración del entorno de Python (más sobre eso aquí ). Ahora que tenemos un entorno Python, necesitamos dependencias de aplicaciones desde las requirements.txtque se instalan usando pip. En este punto make test, comencemos a ejecutar el objetivo , ejecuta el conjunto de pruebas Pytest . Si las pruebas del kit pasan, proceda a instalar los linters mencionados anteriormente: pylint , flake8 y bandit . Finalmente, lanzamos el objetivomake lintque a su vez lanza cada uno de estos linters. Se trata del trabajo de compilación / prueba, pero ¿qué hay de enviar el código? Hablemos de ella:




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 }}


Las primeras 4 líneas definen cuándo comienza el trabajo. Indicamos que este trabajo solo debe activarse cuando las etiquetas se mueven al repositorio (* indica un patrón de nombre, aquí son todas las etiquetas ). Esto se hace para que no insertemos la imagen de Docker en el registro del paquete de GitHub cada vez que enviamos datos al repositorio, sino solo cuando se carga una etiqueta que indica la nueva versión de nuestra aplicación. Ahora, para el cuerpo de esta tarea, comienza verificando el código fuente y estableciendo el valor de la variable de entorno RELEASE_VERSION igual a la etiqueta cargada de git. Esto se hace usando la función de Acciones de GitHub incorporada :: setenv (más detalles aquí). Luego, la tarea ingresa al registro de Docker con el REGISTRY_TOKEN secreto almacenado en el repositorio y el inicio de sesión del usuario que inició el flujo de trabajo (github.actor). Finalmente, la última línea ejecuta el objetivo de inserción que crea la imagen de producción y la inserta en el registro con la etiqueta git publicada anteriormente como etiqueta de imagen. Mira todo el código en mis archivos de repositorio .



Control de calidad del código con CodeClimate



Por último, pero no menos importante, agreguemos la verificación de la calidad del código usando CodeClimate y SonarCloud . Trabajarán junto con la tarea de prueba que se muestra arriba. Agregue algunas líneas de código:




# 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 }}


Comenzando con CodeClimate : exportando una variable GIT_BRANCHrecuperada usando una variable de entorno GITHUB_REF. Luego, descargamos la herramienta de informe de prueba CodeClimate y la hacemos ejecutable. Luego lo usaremos para formatear el informe de cobertura del conjunto de pruebas. En la última línea, lo enviamos a CodeClimate con el ID de la herramienta para el informe de prueba, que se almacena en los secretos del repositorio. Para SonarCloud , debe crear un archivosonar-project.properties . Los valores para este archivo se pueden encontrar en el panel de SonarCloud en la esquina inferior derecha, y este archivo se ve así:




sonar.organization=martinheinz-github
sonar.projectKey=MartinHeinz_python-project-blueprint

sonar.sources=blueprint


Además, es posible simplemente usar el que hace el trabajo por nosotros sonarcloud-github-action. Todo lo que tenemos que hacer es proporcionar dos tokens: para GitHub, el que está en el repositorio predeterminado, y para SonarCloud , el que obtuvimos del sitio web de SonarCloud . Nota: Los pasos para obtener e instalar todos los tokens y secretos mencionados se describen en el archivo README del repositorio .



Conclusión



¡Eso es todo! Con herramientas, configuraciones y código, ¡está listo para personalizar y automatizar todos los aspectos de su próximo proyecto de Python! Si necesita más información sobre los temas mostrados o discutidos en este artículo, consulte la documentación y el código en mi repositorio , y si tiene alguna sugerencia o problema, envíe una solicitud al repositorio, o simplemente inicie este pequeño proyecto si es para usted. me gusta.



imagen


Y con el código de promoción HABR , puede obtener un 10% adicional al descuento indicado en el banner.







Artículos recomendados






All Articles