19

如何将你的 Python 项目全面自动化?

 3 years ago
source link: https://www.infoq.cn/article/AqR3bA109c6p576AC6hw
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

每个项目——无论你是在从事 Web 应用程序、数据科学还是 AI 开发——都可以从配置良好的 CI/CD、Docker 镜像或一些额外的代码质量工具(如 CodeClimate 或 SonarCloud)中获益。所有这些都是本文要讨论的内容,我们将看看如何将它们添加到 Python 项目中!

本文最初发布于 Martin Heinz 的个人博客,由 InfoQ 中文站翻译并分享。

开发环境中可调试的 Docker 容器

有些人不喜欢 Docker,因为容器很难调试,或者构建镜像需要花很长的时间。那么,就让我们从这里开始,构建适合开发的镜像——构建速度快且易于调试。

为了使镜像易于调试,我们需要一个基础镜像,包括所有调试时可能用到的工具,像 bashvimnetcatwgetcatfindgrep 等。它默认包含很多工具,没有的也很容易安装。这个镜像很笨重,但这不要紧,因为它只用于开发。你可能也注意到了,我选择了非常具体的映像——锁定了 Python 和 Debian 的版本——我是故意这么做的,因为我们希望最小化 Python 或 Debian 版本更新(可能不兼容)导致“破坏”的可能性。

作为替代方案,你也可以使用基于 Alpine 的镜像。然而,这可能会导致一些问题,因为它使用 musl libc 而不是 Python 所依赖的 glibc 。所以,如果决定选择这条路线,请记住这一点。

至于构建速度,我们将利用多阶段构建以便可以缓存尽可能多的层。通过这种方式,我们可以避免下载诸如 gcc 之类的依赖项和工具,以及应用程序所需的所有库(来自 requirements.txt )。

为了进一步提高速度,我们将从前面提到的 python:3.8.1-buster 创建自定义基础镜像,这将包括我们需要的所有工具,因为我们无法将下载和安装这些工具所需的步骤缓存到最终的 runner 镜像中。

说的够多了,让我们看看 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}

从上面可以看到,在创建最后的 runner 镜像之前,我们要经历 3 个中间镜像。首先是名为 builder 的镜像,它下载构建最终应用所需的所有必要的库,其中包括 gcc 和 Python 虚拟环境。安装完成后,它还创建了实际的虚拟环境,供接下来的镜像使用。

接下来是 build -venv 镜像,它将依赖项列表( requirements.txt )复制到镜像中,然后安装它。缓存会用到这个中间镜像,因为我们只希望在 requirement .txt 更改时安装库,否则我们就使用缓存。

在创建最终镜像之前,我们首先要针对应用程序运行测试。这发生在 tester 镜像中。我们将源代码复制到镜像中并运行测试。如果测试通过,我们就继续构建 runner

对于 runner 镜像,我们使用自定义镜像,其中包括一些额外的工具,如 vimnetcat ,这些功能在正常的 Debian 镜像中是不存在的。

你可以在 Docker Hub: https://hub.docker.com/repository/docker/martinheinz/python-3.8.1-buster-tools 中找到这个镜像;

你也可以在 base.Dockerfilehttps://github.com/MartinHeinz/python-project-blueprint/blob/master/base.Dockerfile 中查看其非常简单的 Dockerfile

那么,我们在这个最终镜像中要做的是——首先我们从 tester 镜像中复制虚拟环境,其中包含所有已安装的依赖项,接下来我们复制经过测试的应用程序。现在,我们的镜像中已经有了所有的资源,我们进入应用程序所在的目录,然后设置 ENTRYPOINT ,以便它在启动镜像时运行我们的应用程序。出于安全原因,我们还将 USER 设置为 1001 ,因为最佳实践告诉我们,永远不要在 root 用户下运行容器。最后两行设置镜像标签。它们将在使用 make 目标运行构建时被替换 / 填充,稍后我们将看到。

针对生产环境优化过的 Docker 容器

当涉及到生产级镜像时,我们会希望确保它们小而安全且速度快。对于这个任务,我个人最喜欢的是来自 Distroless 项目的 Python 镜像。可是,Distroless 是什么呢?

这么说吧——在一个理想的世界里,每个人都可以使用 FROM scratch 构建他们的镜像,然后作为基础镜像(也就是空镜像)。然而,大多数人不愿意这样做,因为那需要静态链接二进制文件,等等。这就是 Distroless 的用途——它让每个人都可以 FROM scratch

好了,现在让我们具体描述一下 Distroless 是什么。它是由谷歌生成的一组镜像,其中包含应用程序所需的最低条件,这意味着没有 shell、包管理器或任何其他工具,这些工具会使镜像膨胀,干扰安全扫描器(如 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.03GB,而这个只有 103MB,这就是区别!我知道,我已经能听到你说:“但是 Alpine 可以更小!”是的,没错,但是大小没那么重要。你只会在下载 / 上传时注意到镜像的大小,这并不经常发生。当镜像运行时,大小根本不重要。比大小更重要的是安全性,从这个意义上说,Distroless 肯定更有优势,因为 Alpine(一个很好的替代选项)有很多额外的包,增加了攻击面。

关于 Distroless,最后值得一提的是镜像调试。考虑到 Distroless 不包含任何 shell(甚至不包含 sh ),当你需要调试和查找时,就变得非常棘手。为此,所有 Distroless 镜像都有调试版本。因此,当遇到问题时,你可以使用 debug 标记构建生产镜像,并将其与正常镜像一起部署,通过 exec 命令进入镜像并执行(比如说)线程转储。你可以像下面这样使用调试版本的 python3 镜像:

复制代码

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

所有操作都只需一条命令

所有的 Dockerfiles 都准备好了,让我们用 Makefile 实现自动化!我们首先要做的是用 Docker 构建应用程序。为了构建 dev 映像,我们可以执行 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- .

这个目标会构建镜像。它首先会用镜像名和 Tag(运行 git describe 创建)替换 dev.Dockerfile 底部的标签,然后运行 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- .

这个目标与之前的目标非常相似,但是在上面的示例 1.0.0 中,我们使用作为参数传递的版本而不是 git 标签作为版本 。

当你运行 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 覆盖,而容器命令被参数覆盖。通过这种方式,我们可以直接进入容器浏览,或运行一次性命令,就像上面的例子一样。

当我们完成了编码并希望将镜像推送到 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 工件。它使用被替换到 Dockerfiles 中的 name 标签来过滤和查找需要删除的工件:

复制代码

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

你可以在我的存储库中找到 Makefile 的完整代码清单: https://github.com/MartinHeinz/python-project-blueprint/blob/master/Makefile。

借助 GitHub Actions 实现 CI/CD

现在,让我们使用所有这些方便的 make 目标来设置 CI/CD。我们将使用 GitHub Actions 和 GitHubPackage Registry 来构建管道(作业)及存储镜像。那么,它们又是什么呢?

  • GitHub Actions是帮助你自动化开发工作流的作业 / 管道。你可以使用它们创建单个的任务,然后将它们合并到自定义工作流中,然后在每次推送到存储库或创建发布时执行这些任务。

  • GitHub Package Registry是一个包托管服务,与 GitHub 完全集成。它允许你存储各种类型的包,例如 Ruby gems 或 npm 包。我们将使用它来存储 Docker 镜像。如果你不熟悉 GitHub Package Registry,那么你可以查看我的博文,了解更多相关信息: https://martinheinz.dev/blog/6

现在,为了使用 GitHubActions,我们需要创建将基于我们选择的触发器(例如 push to repository)执行的工作流。这些工作流是存储库中 .github/workflows 目录下的 YAML 文件:

复制代码

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

在那里,我们将创建两个文件 build-test.ymlpush.yml 。前者包含 2 个作业,将在每次推送到存储库时被触发,让我们看下这两个作业:

复制代码

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

第一个作业名为 build ,它验证我们的应用程序可以通过运行 make build-dev 目标来构建。在运行之前,它首先通过执行发布在 GitHub 上名为 checkout 的操作签出我们的存储库。

复制代码

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 个 linter(代码质量检查工具)。与上一个作业一样,我们使用 checkout@v1 操作来获取源代码。在此之后,我们运行另一个已发布的操作 setup-python@v1 ,设置 python 环境(要了解详细信息,请点击这里: https://github.com/actions/setup-python )。

我们已经有了 Python 环境,我们还需要 requirements.txt 中的应用程序依赖关系,这是我们用 pip 安装的。这时,我们可以着手运行 make test 目标,它将触发我们的 Pytest 套件。如果我们的测试套件测试通过,我们继续安装前面提到的 linter——pylint、flake8 和 bandit。最后,我们运行 make lint 目标,它将触发每一个 linter。

关于构建 / 测试作业的内容就这些,但 push 作业呢?让我们也一起看下:

复制代码

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

前四行定义了何时触发该作业。我们指定,只有当标签被推送到存储库时,该作业才启动( * 指定标签名称的模式——在本例中是任何名称)。这样,我们就不会在每次推送到存储库的时候都把我们的 Docker 镜像推送到 GitHub Package Registry,而只是在我们推送指定应用程序新版本的标签时才这样做。

现在我们看下这个作业的主体——它首先签出源代码,并将环境变量 RELEASE_VERSION 设置为我们推送的 git 标签。

这是通过 GitHub Actions 内置的 ::setenv 特性完成的(更多信息请点击这里: https://help.github.com/en/actions/automating-your-workflow-with-github-actions/development-tools-for-github-actions#set-an-environment-variable-set-env )。

接下来,它使用存储在存储库中的 secret REGISTRY_TOKEN 登录到 Docker 注册中心,并由发起工作流的用户登录( github.actor )。最后,在最后一行,它运行目标 push ,构建生产镜像并将其推送到注册中心,以之前推送的 git 标签作为镜像标签。

感兴趣的读者可以从这里签出完整的代码清单: https://github.com/MartinHeinz/python-project-blueprint/tree/master/.github/workflows。

使用 CodeClimate 进行代码质量检查

最后但同样重要的是,我们还将使用 CodeClimate 和 SonarCloud 添加代码质量检查。它们将与上文的测试作业一起触发。所以,让我们添加以下几行:

复制代码

# 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 test reporter 并使其可执行。接下来,我们使用它来格式化由测试套件生成的覆盖率报告,而且,在最后一行,我们将它与存储在存储库秘密中的 test reporter ID 一起发送给 CodeClimate。

至于 SonarCloud,我们需要在存储库中创建 sonar-project.properties 文件,类似下面这样(这个文件的值可以在 SonarCloud 仪表板的右下角找到):

复制代码

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

除此之外,我们可以使用现有的 sonarcloud-github-action ,它会为我们做所有的工作。我们所要做的就是提供 2 个令牌——GitHub 令牌默认已在存储库中,SonarCloud 令牌可以从 SonarCloud 网站获得。

注意:关于如何获取和设置前面提到的所有令牌和秘密的步骤都在存储库的自述文件中: https://github.com/MartinHeinz/python-project-blueprint/blob/master/README.md


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK