Automatizando o trabalho com um projeto Python



Hoje estamos compartilhando com você a tradução de um artigo de um engenheiro do IBM DevOps sobre a automação da construção de imagens Docker rapidamente montadas e facilmente depuradas para projetos Python usando um Makefile. Este projeto não apenas torna a depuração no Docker mais fácil, mas também cuida da qualidade do código do seu projeto. Detalhes, como sempre, sob o corte.






Cada projeto - esteja você trabalhando em um aplicativo da web, com Data Science ou IA, pode se beneficiar de CI / CDs bem ajustados, imagens Docker que são depuradas simultaneamente durante o desenvolvimento e otimizadas para um ambiente de produção, ou ferramentas de garantia de qualidade código como CodeClimate ou SonarCloud . Todas essas coisas são abordadas neste artigo e mostradas como são adicionadas a um projeto Python.



Contêineres depuráveis ​​para desenvolvimento



Algumas pessoas não gostam do Docker porque os contêineres podem ser difíceis de depurar ou porque as imagens demoram muito para construir. Portanto, vamos começar criando imagens ideais para desenvolvimento - rápidas de construir e fáceis de depurar. Para tornar a imagem fácil de depurar, você precisa de uma imagem de base que inclua todas as ferramentas que você possa precisar para depurar. Estes são bash, vim, netcat, wget, cat, find, grep e outros.



Imagem Python: 3.8.1-busterparece um candidato perfeito para essa tarefa. Inclui muitas ferramentas prontas para usar, é fácil instalar ferramentas que faltam. A imagem é grande, mas não importa aqui: só será usada no desenvolvimento. Como você provavelmente notou, as imagens são muito específicas. O bloqueio de versões do Python e do Debian é intencional: você deseja minimizar o risco de quebra causado por novas versões possivelmente incompatíveis do Python ou do Debian . Uma imagem baseada em Alpine é possível como alternativa , mas pode causar alguns problemas: dentro dela usa musl lib em vez de glibcno qual o Python depende. Lembre-se disso se decidir escolher um Alpine. Em termos de velocidade, usaremos compilações de vários estágios para armazenar em cache o máximo de camadas possível. Portanto, dependências e ferramentas como o gcc , bem como todas as dependências necessárias para o aplicativo, não são carregadas de requirements.txt todas as vezes. Para acelerar ainda mais as coisas , uma imagem base personalizada é criada a partir do python mencionado anteriormente : 3.8.1-buster , que tem tudo de que precisamos, já que não podemos armazenar em cache as etapas necessárias para baixar e instalar essas ferramentas na imagem final runner. Mas pare de falar, vamos dar uma olhada no 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}


Acima você pode ver que o código runnerpassará por 3 imagens intermediárias antes de criar a imagem final . O primeiro é o construtor . Ele baixa todas as bibliotecas necessárias para construir o aplicativo, incluindo gcc e o ambiente virtual Python. Após a instalação, um ambiente virtual real é criado e usado pelas seguintes imagens. Em seguida, vem o builder-vv , que copia a lista de dependências (requirements.txt) na imagem e as instala. Esta imagem intermediária é necessária para o armazenamento em cache: você só deseja instalar as bibliotecas se o arquivo requirements.txt mudar, caso contrário, apenas usaremos o cache. Vamos testar o aplicativo antes de criar a imagem final.



Antes de criar nossa imagem final, vamos primeiro executar os testes de nosso aplicativo. Copie o código-fonte e execute os testes. Quando os testes forem aprovados, vá para a imagem do runner . Ele usa uma imagem personalizada com algumas ferramentas adicionais não encontradas na imagem regular do Debian: vim e netcat. Esta imagem está no Docker Hub , e você também pode ver um Dockerfile muito simples em base.Dockerfile . Então, o que fazemos nesta imagem final: primeiro, copiamos o ambiente virtual onde todas as dependências que instalamos da imagem do testador são armazenadase copie o aplicativo testado. Agora que todas as fontes estão na imagem, vá para o diretório onde o aplicativo está localizado e instale ENTRYPOINT para que, quando a imagem for iniciada, o aplicativo seja iniciado. Por motivos de segurança, USER é definido como 1001 : a melhor prática recomenda nunca executar containers como root. As 2 linhas finais definem os rótulos da imagem. Eles serão substituídos ao construir através do alvo make, o que veremos um pouco mais tarde.



Recipientes otimizados para o ambiente de produção



Quando se trata de looks de nível de produção, você quer ter certeza de que eles são pequenos, seguros e rápidos. Meu favorito pessoal neste sentido é a imagem Python do projeto Distroless . Mas o que é "Distroless"? Vamos colocar desta forma: em um mundo ideal, todos construiriam suas próprias imagens usando DO zero como base (ou seja, uma imagem vazia). Mas não é isso que a maioria de nós deseja, pois exige vinculação estática de binários etc. É aí que o Distroless entra em jogo : é um DO zero para todos. E agora vou realmente dizer o que é "Distroless". Este é um conjunto criado pelo Googleimagens contendo o mínimo absoluto exigido pelo aplicativo. Isso significa que não há wrappers, gerenciadores de pacotes ou outras ferramentas que incham a imagem e geram ruído de sinal para scanners de segurança (como CVE ), dificultando o estabelecimento de conformidade. Agora que sabemos com o que estamos lidando, vamos dar uma olhada no Dockerfile de produção. Na verdade, você não precisa mudar muito o código, você só precisa mudar 2 linhas:




# 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


Tudo o que precisamos mudar foram nossas imagens de base para construir e executar o aplicativo! Mas a diferença é muito grande - a imagem de desenvolvimento pesava 1,03 GB e esta tinha apenas 103 MB, o que é uma grande diferença! E já te ouço: "Alpina pode pesar ainda menos!" ... Sim, é, mas o tamanho não importa muito. Você só vai notar o tamanho da imagem ao carregar / descarregar, isso não acontece com muita frequência. Quando a imagem funciona, o tamanho não importa. O que é mais importante do que o tamanho é a segurança e, a esse respeito, Distroless é claramente superior ao Alpine: o Alpine tem muitos pacotes complementares para aumentar a superfície de ataque. A última coisa que vale a pena mencionar ao falar sobre Distroless é a depuração de imagens. Considerando queDistroless não contém nenhum invólucro (nem mesmo "sh"), depurar e explorar torna-se bastante difícil. Para isso, existem versões de "depuração" de todas as imagens Distroless . Dessa forma, quando ocorrer um problema, é possível construir sua imagem de trabalho utilizando uma tag debuge implantá-la junto com sua imagem usual, realizar o necessário na imagem de depuração e fazer, por exemplo, um stream dump. É possível usar a versão de depuração da imagem python3 desta forma:



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


Um time para tudo



Com todos os Dockerfiles prontos, você pode automatizar todo esse pesadelo com um Makefile! A primeira coisa que queremos fazer é construir o aplicativo usando Docker. Portanto, para construir uma imagem de desenvolvimento, escreveremos make build-devque execute o seguinte 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 destino constrói a imagem substituindo primeiro os rótulos na parte inferior pelo dev.Dockerfilenome da imagem e a tag que é criada pelo lançamento e git describe, em seguida, é lançado docker build. Em seguida, crie para o ambiente de produção usando 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 destino é muito semelhante ao anterior, mas em vez de usar a tag git como a versão, a versão passada como um argumento é usada, no exemplo acima é 1.0.0. Quando tudo está sendo executado no Docker , em algum momento você também precisa depurar tudo no Docker . Existe um objetivo para isso:




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


No código acima, você pode ver que o ponto de entrada é substituído por bash e o comando do contêiner é substituído por um argumento no CMD. Assim, podemos apenas entrar no contêiner e vasculhar ou executar algum tipo de comando, como no exemplo acima. Assim que terminarmos de programar e enviar a imagem para o registro do Docker, podemos usar make push VERSION=0.0.2. Vamos ver o que essa meta faz:




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)


Ele primeiro inicia o destino discutido anteriormente build-prode, em seguida, simplesmente docker push. Isso pressupõe que você esteja conectado ao registro do Docker, portanto, esse destino precisa ser feito antes da execução docker login. O objetivo final é limpar os artefatos do Docker. Isso usa a tag de nome, que foi substituída nos arquivos de compilação de imagem do Docker, para filtrar e encontrar artefatos que precisam ser removidos:




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


Todo o código Makefile está no repositório .



CI / CD com ações GitHub



O projeto usa make, Github Actions e o registro de pacote Github para construir pipelines (tarefas) e armazenar nossas imagens para configurar CI / CD. Mas o que é isso?



  • Ações do GitHub são tarefas / pipelines que ajudam a automatizar os fluxos de trabalho de desenvolvimento. É possível usá-los para criar tarefas separadas e, em seguida, combiná-los em fluxos de trabalho personalizados que são executados, por exemplo, toda vez que você envia dados para o repositório ou ao criar uma versão.
  • O Github Package Registry é um serviço de hospedagem de pacotes totalmente integrado ao GitHub. Ele permite que você armazene vários tipos de pacotes, como gemas Ruby ou pacotes npm . O projeto o usa para armazenar imagens Docker. Saiba mais sobre registro pacote Github pode ser aqui .


Para usar ações do GitHub , fluxos de trabalho são criados no projeto com base nos gatilhos selecionados (um exemplo de um gatilho é o envio para o repositório). Esses fluxos de trabalho são arquivos YAML no diretório .github/workflows:




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


O arquivo build-test.yml contém 2 trabalhos que são executados cada vez que o código é enviado ao repositório, eles são mostrados abaixo:




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


A primeira tarefa, chamada build, verifica se o aplicativo pode ser construído executando o destino make build-dev. No entanto, antes de iniciar, ele verifica o repositório executando checkout-o publicado no 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


A segunda tarefa é um pouco mais difícil. Ele executa testes junto ao aplicativo, bem como 3 linters de controle de qualidade de código (controladores de qualidade de código). Como na tarefa anterior, uma ação é usada para obter o código-fonte checkout@v1. Depois disso, outra ação publicada chamada setup-python@v1, que configura o ambiente python, é iniciada (mais sobre isso aqui ). Agora que temos um ambiente Python, precisamos de dependências de aplicativo a partir das requirements.txtquais são instaladas usando pip. Neste ponto make test, vamos começar a executar o destino , ele executa o conjunto de testes Pytest . Se os testes do kit passarem, prossiga com a instalação dos linters mencionados anteriormente - pylint , flake8 e bandit . Finalmente, lançamos o alvomake lintque por sua vez inicia cada um desses linters. É tudo sobre o trabalho de construção / teste, mas que tal enviar o código? Vamos falar sobre ela:




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


As primeiras 4 linhas definem quando o trabalho começa. Indicamos que esta tarefa só deve ser acionada quando as tags são movidas para o repositório (* indica um padrão de nome, aqui são todas as tags ). Isso é feito para que não enviemos a imagem Docker para o registro de pacote do GitHub sempre que enviarmos dados para o repositório, mas apenas quando uma tag indicando a nova versão de nosso aplicativo for carregada. Agora, para o corpo desta tarefa - começa inspecionando o código-fonte e definindo o valor da variável de ambiente RELEASE_VERSION para a tag carregada por git. Isso é feito usando a função interna de Ações do GitHub :: setenv (mais detalhes aqui) Em seguida, a tarefa entra no registro Docker com o segredo REGISTRY_TOKEN armazenado no repositório e o login do usuário que iniciou o fluxo de trabalho (github.actor). Por fim, a última linha executa o push target, que cria a imagem de produção e a coloca no registro com a tag git postada anteriormente como a tag da imagem. Verifique todo o código em meus arquivos de repositório .



Verificação da qualidade do código com CodeClimate



Por último, mas não menos importante, vamos adicionar verificação de qualidade de código usando CodeClimate e SonarCloud . Eles trabalharão em conjunto com a tarefa de teste mostrada acima. Adicione algumas linhas 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 }}


Começando com CodeClimate : exportando uma variável GIT_BRANCHrecuperada usando uma variável de ambiente GITHUB_REF. Em seguida, baixamos a ferramenta de relatório de teste CodeClimate e a tornamos executável. Em seguida, vamos usá-lo para formatar o relatório de cobertura do conjunto de testes. Na última linha, enviamos para a CodeClimate com o ID da ferramenta para o relatório de teste, que fica armazenado nos segredos do repositório. Para SonarCloud , você precisa criar um sonar-project.properties. Os valores para este arquivo podem ser encontrados no painel SonarCloud no canto inferior direito, e este arquivo tem a seguinte aparência:




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

sonar.sources=blueprint


Além disso, é possível simplesmente usar aquele que faz o trabalho para nós sonarcloud-github-action. Tudo o que precisamos fazer é fornecer dois tokens: para GitHub, aquele no repositório padrão, e para SonarCloud , aquele que obtivemos no site do SonarCloud . Nota: As etapas para obter e instalar todos os tokens e segredos mencionados são descritas no README do repositório .



Conclusão



Isso é tudo! Com ferramentas, configurações e código, você está pronto para personalizar e automatizar todos os aspectos do seu próximo projeto Python! Se você precisar de mais informações sobre os tópicos mostrados ou discutidos neste artigo, verifique a documentação e o código em meu repositório e se você tiver alguma sugestão ou problema, envie uma solicitação ao repositório ou apenas marque este pequeno projeto se precisar gostar.



imagem


E com o código promocional do HABR , você ganha 10% a mais no desconto indicado no banner.







Artigos recomendados






All Articles