原文:
www.kdnuggets.com/2020/09/automating-every-aspect-python-project.html
评论
作者为马丁·海因兹,IBM 的 DevOps 工程师
每个项目 - 无论您是在开发 Web 应用程序、某些数据科学或人工智能 - 都可以从良好配置的 CI/CD、在开发过程中既可调试又针对生产环境优化的 Docker 镜像或一些额外的代码质量工具(如CodeClimate或SonarCloud)中受益。本文中将介绍所有这些内容,并将看到如何将这些内容添加到您的Python项目中!
本文是前一篇关于创建“终极”Python 项目设置的文章的续篇,所以在阅读本文之前您可能想查看那篇.
太长不看:这是我的包含完整源代码和文档的存储库:https://github.com/MartinHeinz/python-project-blueprint
有些人不喜欢Docker,因为容器很难调试,或者因为它们的镜像构建时间很长。因此,让我们从这里开始,构建适合开发的镜像 - 构建快速且易于调试。
为了使镜像易于调试,我们需要一个包含我们在调试时可能需要的所有工具的基础镜像 - 诸如bash
、vim
、netcat
、wget
、cat
、find
、grep
等工具。python:3.8.1-buster
似乎是此任务的理想选择。它默认包含许多工具,我们可以很容易地安装所有缺少的内容。这个基础镜像相当庞大,但在这里这并不重要,因为它仅用于开发。另外,您可能已经注意到,我选择了非常具体的镜像 - 锁定了Python和Debian的版本 - 这是有意为之的,因为我们要尽量减少因较新且可能不兼容的Python或Debian版本引起的*“破坏”*的机会。
作为替代,您可以使用基于Alpine的镜像。不过,这可能会导致一些问题,因为它使用musl libc
而不是glibc
,而Python中依赖于glibc
。因此,如果决定选择这条路,请记住这一点。
至于构建速度,我们将利用多阶段构建来允许我们尽可能缓存尽可能多的层。这样我们就可以避免下载依赖项和工具,比如gcc
,以及我们的应用程序所需的所有库(来自requirements.txt
)。
为了进一步加快速度,我们将从之前提到的python:3.8.1-buster
创建自定义基础镜像,其中包含我们需要的所有工具,因为我们无法将下载和安装这些工具到最终运行镜像的步骤缓存。
说够了,让我们看看Dockerfile
:
上面你可以看到,在创建最终的runner镜像之前,我们将经过三个中间镜像。第一个镜像被命名为builder
。它下载构建最终应用程序所需的所有库,包括gcc
和Python虚拟环境。安装完成后,它还会创建实际的虚拟环境,接下来镜像将使用这个虚拟环境。
接下来是builder-venv
镜像,它将我们的依赖列表(requirements.txt
)复制到镜像中,然后进行安装。这个中间镜像用于缓存,因为我们只希望在requirements.txt
更改时安装库,否则我们只使用缓存。
在我们创建最终镜像之前,首先要对应用程序进行测试。这就是在tester
镜像中发生的事情。我们将源代码复制到镜像中并运行测试。如果测试通过,我们会继续到runner
镜像。
对于 runner 镜像,我们使用了一个包含一些额外工具的自定义镜像,例如vim
或netcat
,这些工具在普通的Debian 镜像中并不存在。你可以在Docker Hub 这里找到这个镜像,还可以查看非常简单的Dockerfile
,地址在base.Dockerfile
这里。在这个最终镜像中,我们首先从tester
镜像中复制所有安装的依赖项的虚拟环境,然后复制经过测试的应用程序。现在我们在镜像中有了所有源文件,我们转到应用程序所在的目录,然后设置ENTRYPOINT
,以便在镜像启动时运行应用程序。出于安全原因,我们还将USER
设置为1001,因为最佳实践告诉我们不应以root
用户身份运行容器。最后的两行设置了镜像的标签。这些标签将在使用make
目标构建时被替换/填充,我们稍后将看到这一点。
当涉及到生产级镜像时,我们希望确保它们小巧、安全和快速。我个人最喜欢的这个任务的选择是来自Distroless项目的Python镜像。那Distroless是什么呢?
我这样说吧——在一个理想的世界里,每个人都应该使用FROM scratch
作为基础镜像(即空镜像)来构建他们的镜像。然而,这不是我们大多数人想做的,因为这要求你静态链接二进制文件等。这就是Distroless发挥作用的地方——它是为每个人提供的FROM scratch
。
好的,现在来实际描述一下Distroless是什么。它是由Google 制作的一组镜像,包含了应用程序所需的最基本内容,意味着没有 shell、包管理器或任何其他会使镜像膨胀并给安全扫描器(如CVE)带来信号噪声的工具,从而使建立合规性变得更困难。
现在我们知道我们要处理什么了,让我们看看production Dockerfile
... 实际上,我们不会在这里做太多更改,仅仅是 2 行:
我们只需更改用于构建和运行应用程序的基础镜像!但差异很大——我们的开发镜像是 1.03GB,而这个镜像只有 103MB,这个差别相当大!我知道,我已经听到你了——“但是 Alpine 可以更小!”——是的,没错,但大小并不是那么重要。你只会在下载/上传镜像时注意到镜像大小,这并不常见。当镜像运行时,大小根本不重要。比大小更重要的是安全性,在这方面Distroless无疑更优,因为Alpine(一个很好的替代品)有很多额外的包,增加了攻击面。
讨论Distroless时最后值得一提的是debug镜像。考虑到Distroless不包含任何 shell(甚至没有sh
),当你需要调试和探查时会变得非常棘手。为此,所有Distroless镜像都有debug
版本。因此,当出现问题时,你可以使用debug
标签构建生产镜像,并将其与正常镜像一起部署,进入它并执行,例如线程转储。你可以这样使用python3
镜像的调试版本:
准备好所有的Dockerfiles
后,让我们用Makefile
来彻底自动化吧!我们要做的第一件事是用Docker构建我们的应用程序。因此,我们可以运行make build-dev
来构建开发镜像,该命令运行以下目标:
这个目标通过首先将dev.Dockerfile
底部的标签替换为由运行git describe
生成的镜像名称和标签,然后运行docker build
来构建镜像。
接下来——使用make build-prod VERSION=1.0.0
构建生产环境:
这与之前的目标非常相似,但我们将使用作为参数传递的版本,而不是使用git
标签作为版本,在上面的例子中是1.0.0
。
当你在Docker中运行所有操作时,你还需要在Docker中进行调试,为此,有以下目标:
从上述内容我们可以看到,entrypoint 被bash
覆盖,容器命令被参数覆盖。这样我们可以进入容器进行探查,或者像上面的例子一样运行一次性命令。
当我们完成编码并想要将镜像推送到Docker注册表时,我们可以使用make push VERSION=0.0.2
。让我们看看这个目标做了什么:
它首先运行我们之前查看的build-prod
目标,然后只是运行docker push
。这假设你已经登录到Docker注册表,因此在运行之前需要先执行docker login
。
最后的目标是清理Docker工件。它使用被替换到Dockerfiles
中的name
标签来筛选和查找需要删除的工件:
你可以在我的仓库中找到这个Makefile
的完整代码列表:github.com/MartinHeinz/python-project-blueprint/blob/master/Makefile
现在,让我们利用所有这些方便的make
目标来设置我们的 CI/CD。我们将使用GitHub Actions和GitHub Package Registry来构建我们的管道(作业)和存储我们的镜像。那么,这些究竟是什么呢?
-
GitHub Actions是jobs/pipelines,它们帮助你自动化开发工作流程。你可以用它们来创建单独的任务,然后将这些任务组合成自定义工作流程,然后在每次推送到仓库或创建发布时执行这些工作流程。
-
GitHub Package Registry是一个与 GitHub 完全集成的包托管服务。它允许你存储各种类型的包,例如 Ruby 的gems或npm包。我们将用它来存储我们的Docker镜像。如果你对GitHub Package Registry不太熟悉并想要了解更多信息,你可以查看我的博客文章这里。
现在,为了使用GitHub Actions,我们需要创建workflows,这些workflows将根据我们选择的触发器(例如推送到仓库)执行。这些workflows是位于我们仓库中的.github/workflows
目录中的YAML文件:
在这里,我们将创建 2 个文件build-test.yml
和push.yml
。第一个文件build-test.yml
将包含 2 个作业,这些作业将在每次推送到仓库时触发,让我们来看看这些作业:
第一个作业叫做build
,它通过运行我们的make build-dev
目标来验证我们的应用程序是否可以构建。然而,在运行之前,它会首先通过执行名为checkout
的操作来检出我们的仓库,这个操作在GitHub上发布。
第二个任务稍微复杂一些。它会对我们的应用程序以及 3 个代码质量检查工具(linters)运行测试。与之前的任务一样,我们使用checkout@v1
操作来获取我们的源代码。之后,我们运行另一个名为setup-python@v1
的已发布操作,它为我们设置 Python 环境(你可以在这里找到详细信息)。现在我们已经有了 Python 环境,我们还需要从requirements.txt
中安装应用程序的依赖项,这些依赖项通过pip
进行安装。此时,我们可以继续运行make test
目标,这将触发我们的Pytest测试套件。如果我们的测试套件通过,我们会继续安装前面提到的代码检查工具——pylint、flake8和bandit。最后,我们运行make lint
目标,这将触发每一个代码检查工具。
这就是构建/测试作业的全部内容,但推送作业呢?让我们也来看看:
前四行定义了我们希望何时触发此任务。我们指定这个任务仅在标签被推送到仓库时开始(*
指定了标签名称的模式 - 在这种情况下是任何)。这样我们就不会在每次推送到仓库时都将 Docker 镜像推送到GitHub Package Registry,而是仅在推送指定应用程序新版本的标签时才这样做。
现在进入这项工作的主体 — 首先检出源代码,并将RELEASE_VERSION
环境变量设置为我们推送的git
标签。这个操作是使用GitHub Actions的内置::setenv
功能完成的(更多信息见这里)。接下来,使用存储在仓库中的REGISTRY_TOKEN
密钥和发起工作流的用户(github.actor
)登录 Docker 注册表。最后一行执行push
目标,这会构建生产镜像并将其推送到注册表中,标签为之前推送的git
标签。
你可以在我的仓库中的文件里查看完整的代码列表这里。
最后但同样重要的是,我们还将使用CodeClimate和SonarCloud添加代码质量检查。这些检查将与上述test任务一起触发。所以,让我们在其中添加几行:
我们从CodeClimate开始,首先导出GIT_BRANCH
变量,这个变量是通过GITHUB_REF
环境变量检索到的。接下来,我们下载CodeClimate测试报告器并使其可执行。然后,我们用它来格式化测试套件生成的覆盖报告,最后一行将其通过测试报告器 ID 发送到CodeClimate,该 ID 存储在仓库的密钥中。
至于SonarCloud,我们需要在仓库中创建一个sonar-project.properties
文件,其内容如下(该文件的值可以在SonarCloud仪表板的右下角找到):
除此之外,我们只需使用现有的sonarcloud-github-action
,它会为我们完成所有工作。我们所需要做的就是提供两个令牌 - GitHub的令牌(默认在仓库中)和SonarCloud的令牌(可以从SonarCloud网站获取)。
注意:获取和设置之前提到的所有令牌和密钥的步骤在仓库的 README 文件中这里。
就这些!通过上述工具、配置和代码,你已准备好构建和自动化你下一个Python项目的所有方面!如果你需要更多关于本文中展示/讨论的主题的信息,可以查看我这里的文档和代码:github.com/MartinHeinz/python-project-blueprint
,如果你有任何建议或问题,请在仓库中提交问题,或者如果你喜欢这个小项目,请给它加星。 ????
资源
个人简介: 马丁·海因茨 是 IBM 的一名 DevOps 工程师。作为软件开发者,马丁对计算机安全、隐私和加密学充满热情,专注于云计算和无服务器计算,并且总是准备迎接新的挑战。
原文。已获许可转载。
相关:
-
MIT 免费课程: 计算机科学与 Python 编程入门
-
数据科学遇见 Devops: 使用 Jupyter、Git 和 Kubernetes 的 MLOps
-
在 AWS Fargate 上部署机器学习管道
1. Google 网络安全证书 - 快速进入网络安全领域。
2. Google 数据分析专业证书 - 提升你的数据分析技能
3. Google IT 支持专业证书 - 支持你所在组织的 IT 需求