本文转载自公众号“读芯术”(ID:AI_Discovery)
无论你的项目是用于开发Web应用,处理数据科学问题还是AI,使用配置良好的CI / CD,可在开发中调试且针对生产环境进行了优化的Docker镜像,或一些其它的代码质量工具,都能让你受益。
本文将告诉你该如何把它们加入Python项目中!
这是我的仓库,其中包含完整的源代码和文档:https://github.com/MartinHeinz/python-project-blueprint
用于开发的可调试Docker容器
有些人不喜欢Docker,因为容器可能很难调试,或者因为它们的镜像需要很长时间才能构建。因此,让我们从构建用于开发的理想镜像开始,它能够快速构建且易于调试。
为了使镜像易于调试,需要基础镜像,其中包括调试时可能需要的所有工具,例如bash,vim,netcat,wget,cat,find,grep等。
python:3.8.1-buster似乎是这一任务的理想选择。它在默认情况下包含许多工具,我们可以很容易地安装所有缺少的东西。这个基本镜像非常厚重,但这并不重要,因为此时它将仅用于开发。
你可能已经注意到,我选择了非常具体的镜像:锁定了Python版本和Debian版本。这是故意的,因为我们希望最大程度地减少由更新的,可能不兼容的Python或Debian版本引起“损坏”的可能性。
可以使用基于Alpine的镜像作为替代。但是,这可能会引起一些问题,因为它使用musllibc而不是Python依赖的glibc。因此,如果决定选择此配置,请记住这一点。
至于构建的速度,我们将利用多阶段构建以便缓存尽可能多的层。这样,就可以避免下载例如gcc的依赖项和工具以及(requirements.txt中的)应用程序所需的所有库。
因为无法将下载和安装这些工具所需的步骤缓存到最终的运行程序镜像中,我们将使用前面提到的python:3.8.1-buster创建自定义基本镜像,该镜像将包含需要的所有工具,从而进一步提升处理速度。
说了这么多,来看看Dockerfile:
- # dev.Dockerfile
- FROMpython:3.8.1-buster AS builder
- RUN apt-get update&& apt-get install -y --no-install-recommends --yes python3-venv gcclibpython3-dev && \
- python3 -m venv /venv && \
- /venv/bin/pip install --upgradepip
- FROM builder ASbuilder-venv
- COPYrequirements.txt /requirements.txt
- RUN /venv/bin/pipinstall -r /requirements.txt
- FROM builder-venv AStester
- COPY . /app
- WORKDIR /app
- RUN/venv/bin/pytest
- FROMmartinheinz/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}
- LABELversion={VERSION}
从上面的文档可以看到我们将创建3个中间镜像,然后创建最终的运行镜像。第一个镜像被称为builder,它会下载构建最终应用程序所需的所有必需库,其中包括gcc和Python虚拟环境。安装完成后,它还会创建实际的虚拟环境以供下一个镜像使用。
接着是builder-venv镜像,该镜像将依赖项列表(requirements.txt)复制到镜像中,然后进行安装。缓存需要此中间镜像,因为仅在requirements.txt更改时才会安装库,否则仅使用缓存。
在创建最终镜像之前,首先要针对应用程序运行测试。这就是tester镜像做的工作。我们将源代码复制到镜像中并运行测试。如果通过了,程序就会运行至runner。
对于runner镜像,我们使用的是自定义镜像,其中包括普通Debian镜像中不存在的一些额外功能,例如vim或netcat。你可以在Docker Hub上的这里找到此镜像,还可以通过这里在base.Dockerfile中检验这个非常简单的Dockerfile。
因此,在最终镜像中的工作有这些:首先复制虚拟环境,该环境保留了tester镜像中所有已安装的依赖项,接下来复制经过测试的应用程序。
现在,镜像已经拥有了所有源,移至应用程序所在的目录设置ENTRYPOINT,以便在镜像启动时运行应用程序。出于安全原因将USER设置为1001,因为最佳实践告诉我们,永远不要在root用户下运行容器。
最后2行设置镜像的标签。当使用make 命令指向构建并运行时,这些将被替换或填充,这一点稍后我们会看到。
为产品优化的Docker容器
谈及产品级镜像时,我们想确保它们小巧,安全且快速。我个人最喜欢的是Distroless项目中的Python镜像。那么什么是Distroless?
可以这样形容它:在理想的世界中,每个人都将使用FROM scratch作为其基本镜像(即空镜像)来构建其镜像。
但这不是大多数人想要做的,因为它要求静态连接二进制文件等。这就是Distroless发挥作用的地方了,它是为每个人设计的FROM scratch。
Distroless是由Google制作的一组镜像,包含应用所需的最低要求,这意味着没有壳(shell),程序包管理器或任何其他工具会使镜像膨胀并给安全扫描器(例如CVE)造成信号噪声,从而使其变得更难建立规则。
知道了要解决的问题,让我们看一下生产型Dockerfile ...... 实际上,在这里不需要做太多更改,只有两行:
- # prod.Dockerfile
- # 1. Line - Change builder image
- FROMdebian:buster-slim AS builder
- # ...
- # 17. Line - Switch to Distroless image
- FROMgcr.io/distroless/python3-debian10 AS runner
- # ... Rest of the Dockefile
需要更改的只是用于构建和运行应用程序的基本镜像!
但是差别是巨大的:我们的开发镜像为1.03GB,而这个镜像仅为103MB,这是完全不一样的!
我知道你会说“但是Alpine可以变得更小”是的,没错,但是大小的差距并不那么重要。你只会在下载/上传镜像时注意镜像的大小,这种情况并不常见。当镜像运行时,大小完全不重要。比大小更重要的是安全性,就这一点而言,Distroless肯定具有优势,因为Alpine(这是一个很好的替代)具有许多额外的程序包,可以增加攻击面。
关于Distroless值得一提的最后一件事是调试镜像。考虑到Distroless不包含任何壳(甚至不包括sh),这使得需要调试和检查时非常棘手。为此,所有Distroless镜像都有调试版本。
因此,当遇到麻烦时,可以使用debug标签构建生产镜像,并将其部署到常规镜像旁边,在其中执行并且进行如线程转储的操作。可以像这样使用python3镜像的调试版本:
- docker run --entrypoint=sh -tigcr.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 tagsto set the version string
- TAG := $(shell git describe --tags--always --dirty)
- build-dev:
- @echo "\n${BLUE}BuildingDevelopment 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 Productionimage 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 shellin the containerized build environment...${NC}\n"
- @docker run \
- -ti \
- --rm \
- --entrypoint /bin/bash \
- -u $$(id -u):$$(id -g) \
- $(IMAGE):$(TAG) \
- $(CMD)
从上面可以看出,bash覆盖了入口点,而参数则覆盖了容器命令。这样,我们可以像上面的示例那样直接进入容器并进行调试或运行一个关闭命令。
当完成编码并想将镜像推送到Docker注册表时,可以使用makepush 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"
- @dockerpush $(IMAGE):$(VERSION)
它首先运行之前看过的build-prod文件,然后运行docker push。这里假设已登录Docker注册表,因此在运行此注册表之前,需要运行docker login。
最后一个目标文件用来清理Docker工件。它使用替换为Dockerfiles的name标签来过滤和查找需要删除的工件:
- docker-clean:
- @docker system prune -f --filter "label=name=$(MODULE)"
使用GitHub Actions的CI / CD
现在开始使用所有这些方便的make目标命令来设置CI / CD。我们将使用GitHub Actions和GitHub Package Registry来构建管道(工作)并存储镜像。那么这两个东西到底是什么呢?
- Github Actions是可以帮助自动化开发工作流程的作业/管道。可以使用它们来创建单个任务,然后将它们组合到自定义的工作流程中,然后在诸如每次推送到仓库或创建发行版的时候执行这些工作流程。
- GitHub Package Registry是与GitHub完全集成的软件包托管服务。它可以存储各种类型的软件包,例如:Ruby gems或npm软件包。我们将使用它来存储Docker镜像。
- 如果你不熟悉GitHub Package Registry,并且想要了解更多相关信息,可以查看我的博客文章:https://martinheinz.dev/blog/6
现在,为了使用GitHub Action,需要创建工作流,这些工作流将根据选择的触发器(例如,推送到仓库)执行。这些工作流是YAML文件,位于仓库中的.github / workflows目录中:
- .github
- └── workflows
- ├── build-test.yml
- └── push.yml
第一项工作名为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环境(你可以在此处找到有关它的详细信息)。
现在有了python环境,还需要使用pip安装的requirements.txt中的应用程序依赖项。此时可以继续运行make test命令,这将触发Pytest套件。如果测试套件通过,将继续安装前面提到的linters,即pylint,flake8和bandit。最后运行make lint命令,这将触发每个linter。
这就是构建/测试工作的全部流程,但是应该如何推送呢?来看一下:
- on:
- push:
- tags:
- - '*'
- jobs:
- push:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v1
- - name: Set env
- run: echo ::set-envname=RELEASE_VERSION::$(echo ${GITHUB_REF:10})
- - name: Log intoRegistry
- run: echo "${{secrets.REGISTRY_TOKEN }}" |
- docker login docker.pkg.github.com -u ${{github.actor }} --password-stdin
- - name: Push to GitHubPackage Registry
- run: make pushVERSION=${{ env.RELEASE_VERSION }}
前4行定义了何时触发这项工作。我们指定仅当将标签推送到仓库时才开始该工作(*在这种情况下指定标签名称可以是任何模式),因此不会在每次推送到仓库时都将Docker镜像推送到GitHub Package Registry,而仅在推送指定应用程序新版本的标签时才推送到GitHubPackage Registry。
现在到了工作的主体,它是从检索源代码并将RELEASE_VERSION的环境变量设置为推送的git标签开始的。这是通过GitHub Actions的内置:: setenv功能完成的。
接下来,它使用存储在repository secrets中的REGISTRY_TOKEN登录到Docker注册表,并登录启动工作流的用户(github.actor)。最后,在最后一行中,它运行push命令,该命令构建生产镜像并将其推送到注册表,并以先前推送的git标签作为镜像标签。
此处可以检索出完整的代码清单:
https://github.com/MartinHeinz/python-project-blueprint/tree/master/.github/workflows
使用CodeClimate进行代码质量检查
最后但并非最不重要的一点是,还要使用CodeClimate和SonarCloud来添加代码质量检查。这些将与上面展示的test工作一起触发。因此,我们在其中添加几行:
- # test, lint...
- - name: Send report toCodeClimate
- run: |
- export GIT_BRANCH="${GITHUB_REF/refs\/heads\//}"
- curl -Lhttps://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: SonarCloudscanner
- 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存储在repository secrets中。
至于SonarCloud,我们需要在仓库中创建如下所示的sonar-project.properties文件(文件中的值可以在SonarCloud仪表板的右下角找到):
- .organization=martinheinz-github
- sonar.projectKey=MartinHeinz_python-project-blueprint
- sonar.sources=blueprint
除此之外,只需使用已有的sonarcloud-github-action就可以帮我们完成所有工作。要做的就是提供2个令牌:GitHub令牌(默认情况下位于仓库中)和SonarCloud令牌(可从SonarCloud网站获得)。
注意:有关如何获取和设置所有上述令牌和密钥的步骤,请参见README文件:
https://github.com/MartinHeinz/python-project-blueprint/blob/master/README.md
有了上述的工具、配置和代码,你就可以构建和自动化下一个Python项目的方各个方面了。快去试试吧!