直播: 近二十载从业老兵谈金融科技赋能的探索与实践
本文转载自公众号 "读芯术"(ID:AI_Discovery)
无论你的项目是用于开发 web 应用, 处理数据科学问题还是 AI, 使用配置良好的 CI / CD, 可在开发中调试且针对生产环境进行了优化的 Docker 镜像, 或一些其它的代码质量工具, 都能让你受益.
本文将告诉你该如何把它们加入 Python 项目中!
这是我的仓库, 其中包含完整的源代码和文档:
用于开发的可调试 Docker 容器
有些人不喜欢 Docker, 因为容器可能很难调试, 或者因为它们的镜像需要很长时间才能构建. 因此, 让我们从构建用于开发的理想镜像开始, 它能够快速构建且易于调试.
为了使镜像易于调试, 需要基础镜像, 其中包括调试时可能需要的所有工具, 例如 bash,VIM,netcat,wget,cat,find,grep 等.
python:3.8.1-buster 似乎是这一任务的理想选择. 它在默认情况下包含许多工具, 我们可以很容易地安装所有缺少的东西. 这个基本镜像非常厚重, 但这并不重要, 因为此时它将仅用于开发.
你可能已经注意到, 我选择了非常具体的镜像: 锁定了 Python 版本和 Debian 版本. 这是故意的, 因为我们希望最大程度地减少由更新的, 可能不兼容的 Python 或 Debian 版本引起 "损坏" 的可能性.
图源: techcrunch
可以使用基于 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 镜像.
- 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
- 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 }}
- # 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 }}
- .organization=martinheinz-GitHub
- sonar.projectKey=MartinHeinz_python-project-blueprint
- sonar.sources=blueprint
来源: http://developer.51cto.com/art/202004/615033.htm