使用Python项目自动化工作



今天,我们与您分享IBM DevOps工程师的一篇文章的翻译,该文章涉及使用Makefile为Python项目自动构建快速组装且易于调试的Docker映像。该项目不仅使在Docker中进行调试更加容易,而且还照顾了项目代码的质量。细节一如既往地削减。






每个项目-无论您是在Web应用程序上工作,还是在数据科学或人工智能上,都可以受益于精心调整的CI / CD,可在开发过程中同时调试并针对生产环境进行优化的Docker映像,或质量保证工具代码,例如CodeClimateSonarCloud本文介绍了所有这些内容,并说明了如何将它们添加到Python项目中。



用于开发的可调试容器



有些人不喜欢Docker,因为容器可能难以调试,或者因为图像构建需要很长时间。因此,让我们从构建最适合开发的图像开始-快速构建和易于调试。为了使映像易于调试,您需要一个基础映像,其中包含您可能需要调试的所有工具。这些是bash,vim,netcat,wget,cat,find,grep等。Python



图像:3.8.1-破坏者似乎是完成此任务的理想人选。它包括许多现成的工具,很容易安装缺少的工具。图像很大,但是在这里并不重要:它将仅在开发中使用。您可能已经注意到,图像非常具体。锁定PythonDebian版本是有意的:您希望最大程度地减少由新的或不兼容的PythonDebian版本引起的损坏风险。也可以使用基于Alpine的映像,但它可能会引起一些问题:在其内部使用musl lib而不是glibcPython所依赖的。如果您决定选择阿尔卑斯山,请记住这一点。在速度方面,我们将使用多阶段构建来缓存尽可能多的层。因此,并非每次都requirements.txt加载gcc之类的依赖项和工具以及应用程序所需的所有依赖项。为了进一步加快处理速度,将使用前面提到的python:3.8.1-buster创建一个自定义基础映像,该映像具有我们所需的一切,因为我们无法缓存将这些工具下载并将其安装到最终映像中所需的步骤。但是,别再说了,让我们看一下Dockerfile:runner



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


在上方可以看到,代码runner在创建最终图像之前经过3个中间图像。首先是建设者。它下载了构建应用程序所需的所有库,包括gcc和Python虚拟环境。安装后,以下图像将创建并使用真实的虚拟环境。接下来是builder-vv,它将依赖项列表(requirements.txt)复制到映像中,然后安装它们。此中间映像对于缓存是必需的:您只想在requirements.txt更改时安装库,否则我们仅使用缓存。在创建最终图像之前,让我们测试应用程序。



在创建最终映像之前,让我们首先运行应用程序的测试。复制源代码并运行测试。测试通过后,转到跑步者图像。这将使用自定义映像以及在常规Debian映像中找不到的一些其他工具:vim和netcat。该映像位于Docker Hub上,您还可以在base.Dockerfile中查看一个非常简单的Dockerfile。因此,我们在此最终映像中的操作是:首先,我们复制虚拟环境,在该环境中,我们从测试程序映像安装的所有依赖项都存储在其中,然后复制经过测试的应用程序。现在,所有源都在映像中,我们将移至应用程序所在的目录并安装ENTRYPOINT,以便在启动映像时启动应用程序。出于安全原因,USER设置为1001:最佳实践建议不要以root用户身份运行容器。最后两行设置图像标签。通过目标进行构建时,它们将被替换make,稍后我们将看到。



针对生产环境优化的容器



当涉及到生产级外观时,您要确保它们小巧,安全且快速。在这种意义上,我个人最喜欢的是Distroless项目中Python图像。但是什么是“ Distroless”?让我们这样说:在理想的世界中,每个人都将使用FROM scratch作为基础来构建自己的图像(即空图像)。但这不是我们大多数人想要的,因为它需要静态链接二进制文件等。那就是Distroless发挥作用的地方对每个人来说都是从零开始。现在,我将真正告诉您什么是“ Distroless”。这是由Google创建的集合包含应用程序所需的绝对最小值的图像。这意味着没有包装程序,程序包管理器或其他工具会使图像膨胀,并为安全扫描程序(例如CVE生成信号噪声,从而难以建立合规性。现在我们知道了要处理的内容,让我们看一下生产Dockerfile。实际上,您不需要太多更改代码,只需更改两行即可:




# 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


我们需要更改的只是构建和运行应用程序的基本图像!但是差异非常大-开发映像的重量为1.03 GB,而这个映像只有103 MB,这是一个很大的差异!我已经听到您的声音:“ Alpina的重量更轻!” ...是的,但是大小并不重要。您只会在加载/卸载时注意到图像的大小,这种情况很少发生。当图像起作用时,大小无关紧要。比大小更重要的是安全性,在这方面,Distroless绝对优于Alpine:Alpine具有许多其他软件包来增加攻击面。关于Distroless值得一提的最后一件事是图像调试。考虑到Distroless不包含任何外壳程序(甚至不包含“ sh”),调试和研究变得相当困难。为此,所有Distroless映像都有“调试”版本这样,当出现问题时,可以使用标签来构建您的工作映像,debug并将其与您的常规映像一起部署,在调试映像中执行必要的操作,并进行流转储。可以使用python3映像的调试版本,如下所示:



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


一站式服务



准备好所有Dockerfile之后,您可以使用Makefile自动化整个噩梦!我们要做的第一件事是使用Docker构建应用程序。因此,为了构建开发映像,我们将编写make build-dev执行以下代码的代码:




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




该目标通过首先用图像的dev.Dockerfile名称和通过启动创建的标签替换底部的标签来构建图像git describe,然后启动docker build接下来,使用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- .


这个目标与前一个目标非常相似,但是没有使用git标签作为版本,而是使用了作为参数传递的版本,在上面的示例中是1.0.0。当所有内容都在Docker中运行时,有时还需要在Docker中调试所有内容为此有一个目标:




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


在上面的代码中,您可以看到bash覆盖了入口点,而CMD中的参数覆盖了container命令。因此,我们可以只是进入容器并四处逛逛,或者执行某种命令,如上例所示。完成编程并将映像推送到Docker注册表后,我们可以使用make push VERSION=0.0.2让我们看看这个目标的作用:




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)


它首先启动前面讨论的目标build-prod,然后简单地启动docker push假设您已登录Docker注册表,因此需要在运行之前完成此目标docker login最终目标是清理Docker工件。这将使用已在Docker映像构建文件中替换的名称标签来过滤和查找需要删除的工件:




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


所有Makefile代码都在存储库中



CI / CD和GitHub Actions



该项目使用make,Github Actions和Github软件包注册表来构建管道(任务)并存储我们的映像以配置CI / CD。那是什么



  • GitHub Actions是有助于自动化开发工作流程的任务/管道。可以使用它们创建单独的任务,然后将它们组合到自定义工作流中,例如,每次将数据提交到存储库时或创建发行版时,这些工作流就会执行。
  • Github软件包注册表是与GitHub完全集成的软件包托管服务。它允许您存储各种类型的软件包,例如Ruby gems或npm软件包该项目使用它来存储Docker映像。了解更多关于Github的包注册表可以在这里


要使用GitHub Actions,将基于选定的触发器在项目中创建工作流(触发器示例正在提交到存储库)。这些工作流是目录中的YAML文件.github/workflows




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


build-test.yml文件包含2个作业,每次将代码提交到存储库时都会运行这些作业,如下所示:




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


第一个任务称为build,通过运行target来验证可以构建应用程序make build-dev。但是,在开始之前,它将通过执行checkout发布到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


第二项任务比较困难。它在应用程序旁边以及3个代码质量控制lint(代码质量控制器)旁边运行测试。与上一个任务一样,使用操作来获取源代码checkout@v1。之后,又启动了另一个已发布的动作setup-python@v1,它设置了python环境(有关更多信息,请参见此处)。现在我们有了Python环境,我们需要requirements.txt使用pip从中安装应用程序依赖项。至此make test让我们开始运行目标,它运行Pytest测试套件。如果套件测试通过,则继续安装前面提到的linters- pylintflake8bandit。最后,我们启动目标make lint反过来又发射了这些短绒。一切都与构建/测试工作有关,但是提交代码又如何呢?让我们谈谈她:




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


前4行定义了作业何时开始。我们指示仅当将标签移至存储库时才应触发此作业(*表示名称模式,此处均为标签)。这样做是为了避免在每次将数据推送到存储库时都不会将Docker映像推送到GitHub软件包注册表中,而只是在上载了指示应用程序新版本的标记时才进行。现在,该任务的主体-首先检查源代码并将RELEASE_VERSION环境变量的值设置为git upload标签。这是使用内置的GitHub Actions函数:: setenv完成的(此处有更多详细信息))。然后,该任务进入Docker注册表,其中存储了存储库中的秘密REGISTRY_TOKEN和启动工作流程的用户的登录名(github.actor)。最后,最后一行运行push目标,该目标将构建生产映像并将其以先前发布的git标记作为图像标记推入注册表。检出存储库文件中的所有代码



使用CodeClimate进行代码质量检查



最后但并非最不重要的一点,让我们使用CodeClimateSonarCloud添加代码质量检查他们将与上面显示的测试任务一起工作。添加几行代码:




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


CodeClimate开始:导出GIT_BRANCH使用环境变量检索的变量GITHUB_REF。然后,我们下载CodeClimate测试报告工具并使其可执行。然后,我们将使用它来格式化测试套件覆盖率报告。在最后一行中,我们将其与测试报告工具的ID一起发送到CodeClimate,该ID存储在存储库的秘密中。对于SonarCloud,您需要创建一个sonar-project.properties。该文件的值可以在右下角SonarCloud仪表板上找到,该文件如下所示:




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

sonar.sources=blueprint


而且,可以简单地使用一个为我们做的工作sonarcloud-github-action我们要做的就是提供两个令牌:对于GitHub,一个默认存储库中的令牌;对于SonarCloud,一个从SonarCloud网站获得的令牌注意:在资源库的自述文件中描述了获取和安装所有提到的令牌和机密的步骤



结论



就这样!有了工具,配置和代码,您就可以自定义和自动化下一个Python项目的各个方面!如果您需要有关本文显示或讨论的主题的更多信息,请查看我的存储库中的文档和代码,如果有任何建议或问题,请向存储库提交请求,或者仅在需要时给这个小项目加注星标喜欢。



图片


通过HABR促销代码,您可以在横幅上显示的折扣中获得10%的额外折扣。







推荐文章






All Articles