自动化 Python 项目的各个方面
注:本文最初发布于martinheinz.dev
每个项目——无论你开发的是 Web 应用、数据科学还是 AI——都可以从配置良好的 CI/CD、开发环境可调试且针对生产环境优化的 Docker 镜像,或者一些额外的代码质量工具(例如CodeClimate或SonarCloud)中受益。所有这些都是我们将在本文中讨论的内容,我们将了解如何将它们添加到你的Python项目中!
这是关于创建“终极” Python 项目设置的上一篇文章的后续,因此在阅读本文之前您可能需要先查看一下上一篇文章。
TL;DR:这是我的存储库,包含完整的源代码和文档:https://github.com/MartinHeinz/python-project-blueprint
用于开发的可调试 Docker 容器
有些人不喜欢Docker,因为容器难以调试,或者镜像构建时间过长。所以,让我们从这里开始,构建一些适合开发的镜像——快速构建且易于调试。
为了使镜像易于调试,我们需要一个包含调试时可能需要的所有工具的基础镜像,例如,,,,,,等等bash
。这似乎是这项任务的理想选择。它默认包含很多工具,我们可以很容易地安装所有缺少的工具。这个基础镜像相当厚,但这无关紧要,因为它只用于开发。另外,你可能已经注意到,我选择了非常具体的镜像——锁定了 Python 和 Debian 的两个版本vim
——netcat
这是有意为之,因为我们希望最大限度地减少由较新、可能不兼容的Python或Debian版本造成“崩溃”的可能性。wget
cat
find
grep
python:3.8.1-buster
您也可以使用基于Alpine的镜像。但是,这可能会导致一些问题,因为它使用的musl libc
是Pythonglibc
所依赖的镜像。因此,如果您决定选择这种方式,请记住这一点。
至于构建速度,我们将利用多阶段构建来缓存尽可能多的层。这样,我们就可以避免下载依赖项和工具,gcc
以及应用程序所需的所有库(来自requirements.txt
)。
为了进一步加快速度,我们将根据前面提到的创建自定义基础图像python:3.8.1-buster
,其中将包括我们需要的所有工具,因为我们无法将下载和安装这些工具所需的步骤缓存到最终运行器图像中。
说得够多了,让我们看看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}
上图显示,在创建最终的运行器镜像之前,我们将运行 3 个中间镜像。第一个名为builder
。它会下载构建最终应用程序所需的所有库,包括gcc
Python虚拟环境。安装完成后,它还会创建实际的虚拟环境,供后续镜像使用。
接下来是builder-venv
镜像,它会将依赖项列表(requirements.txt
)复制到镜像中,然后安装它。这个中间镜像用于缓存,因为我们只想在库requirements.txt
发生变化时才安装,否则我们就使用缓存。
在创建最终镜像之前,我们首先要对我们的应用程序运行测试。这就是tester
镜像中发生的事情。我们将源代码复制到镜像中并运行测试。如果测试通过,我们将继续进行runner
。
对于运行器镜像,我们使用自定义镜像,其中包括一些额外内容,例如vim
或 ,这些在普通Debian镜像netcat
中不存在。您可以在Docker Hub上找到此镜像,也可以在此处查看非常简单的镜像。那么,我们在此最终镜像中所做的是 - 首先我们从镜像中复制包含所有已安装依赖项的虚拟环境,接下来我们复制我们测试过的应用程序。现在我们拥有了镜像中的所有源,我们将其移动到应用程序所在的目录,然后进行设置,以便在启动镜像时运行我们的应用程序。出于安全原因,我们还设置为1001,因为最佳实践告诉我们永远不要在用户下运行容器。最后两行设置图像的标签。当使用目标运行构建时,这些将被替换/填充,我们稍后会看到。 Dockerfile
base.Dockerfile
tester
ENTRYPOINT
USER
root
make
针对生产环境优化的 Docker 容器
对于生产级镜像,我们希望确保它们体积小、安全且快速。我个人最喜欢的是Distroless项目的Python镜像。那么, Distroless到底是什么呢?
这么说吧——理想情况下,每个人都会使用FROM scratch
基础镜像(也就是空镜像)来构建自己的镜像。然而,大多数人并不想这样做,因为它需要你静态链接二进制文件等等。这就是Distroless 的用武之地——它FROM scratch
适合所有人。
好了,现在来正式描述一下Distroless是什么。它是Google制作的一套镜像,只包含应用所需的最低限度组件,这意味着它没有 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
),当你需要调试和探索时会变得相当棘手。为此,debug
所有Distroless镜像都有各自的版本。所以,当遇到问题时,你可以使用标签构建生产镜像,并将其与普通镜像一起部署,然后执行 exec 并执行诸如线程转储之类的操作。你可以像这样debug
使用镜像的调试版本:python3
docker run --entrypoint=sh -ti gcr.io/distroless/python3-debian10:debug
单一命令搞定一切
一切Dockerfiles
准备就绪后,让我们开始自动化吧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
,容器命令也会被参数覆盖。这样,我们就可以像上面的例子一样,直接进入容器进行查看,或者直接运行一个命令。
当我们完成编码并希望将镜像推送到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工件。它使用name
替换的标签来Dockerfiles
过滤并查找需要删除的工件:
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和GitHub Package Registry来构建我们的管道(作业)并存储我们的镜像。那么,它们到底是什么呢?
-
GitHub Actions是帮助您自动化开发工作流程的作业/管道。您可以使用它们创建单独的任务,然后将它们组合成自定义工作流程,这些工作流程随后会在每次推送到仓库或创建发布时执行。
-
GitHub Package Registry是一项软件包托管服务,与 GitHub 完全集成。它允许您存储各种类型的软件包,例如 Ruby gem或npm软件包。我们将使用它来存储Docker镜像。如果您不熟悉GitHub Package Registry并想了解更多信息,可以查看我的博客文章(此处)。
现在,要使用GitHub Actions,我们需要创建基于所选触发器(例如,推送到仓库)执行的工作流程。这些工作流程是YAML文件,位于.github/workflows
我们仓库的目录中:
.github
└── workflows
├── build-test.yml
└── push.yml
在其中,我们将创建两个文件build-test.yml
和push.yml
。第一个文件build-test.yml
包含两个作业,每次推送到存储库时都会触发,让我们看一下:
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
。不过,在运行目标之前,它首先会通过执行名为 的操作来检出我们的代码库,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 个 linters(代码质量检查器)。与上一项工作相同,我们使用checkout@v1
action 来获取源代码。之后,我们运行另一个已发布的操作,名为 ,setup-python@v1
它为我们设置了 python 环境(您可以在此处找到有关它的详细信息)。现在我们有了 python 环境,我们还需要requirements.txt
使用 来安装的应用程序依赖项pip
。此时,我们可以继续运行make test
target,这会触发我们的Pytest套件。如果我们的测试套件通过,我们将继续安装前面提到的 linters - pylint,flake8和bandit。最后,我们运行make lint
target,它会触发每个 linters。
这就是构建/测试任务的全部内容,但是推送任务呢?我们也来复习一下:
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,而是仅在推送指定应用程序新版本的标签时才将其推送到 GitHub Package Registry。
现在来看看这个作业的主体部分——它首先检出源代码,并设置环境变量,以RELEASE_VERSION
指示我们推送的标签。这是使用GitHub Actionsgit
的内置::setenv
功能完成的(更多信息请见此处)。接下来,它使用存储在仓库中的密钥和启动工作流程的用户登录信息( )登录到 Docker 镜像仓库。最后,在最后一行,它运行target 命令,构建 prod 镜像并将其推送到镜像仓库,并使用之前推送的标签作为镜像标签。REGISTRY_TOKEN
github.actor
push
git
您可以在此处的我的存储库中的文件中查看完整的代码列表。
使用 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测试报告器并使其可执行。接下来,我们使用它来格式化测试套件生成的覆盖率报告,并在最后一行将其与存储在存储库 secrets 中的测试报告器 ID 一起发送到CodeClimate 。
至于SonarCloud,我们需要sonar-project.properties
在我们的存储库中创建如下所示的文件(该文件的值可以在右下角的SonarCloud仪表板上找到):
sonar.organization=martinheinz-github
sonar.projectKey=MartinHeinz_python-project-blueprint
sonar.sources=blueprint
除此之外,我们可以使用现有的sonarcloud-github-action
,它会为我们完成所有工作。我们所要做的就是提供两个令牌 - GitHub令牌(默认在存储库中)和SonarCloud令牌(我们可以从SonarCloud网站获取)。
注意:有关如何获取和设置所有前面提到的令牌和秘密的步骤位于此处的存储库 README 中。
结论
就是这样!有了上面的工具、配置和代码,你就可以构建并自动化你的下一个Python项目的各个方面了!如果你需要更多关于本文中展示/讨论的主题的信息,可以查看我仓库中的文档和代码:https: //github.com/MartinHeinz/python-project-blueprint。如果你有任何建议/问题,请在仓库中提交问题,或者如果你喜欢我的这个小项目,可以点赞。🙂
资源
- 适用于 Python 应用程序的最佳 Docker 基础镜像
- Google Distroless
- 扫描 Docker 镜像中的漏洞
- 5 个用于容器安全的开源工具
- SonarCloud GitHub 操作