每个项目——无论您从事的是Web应用程序、数据科学还是AI开发——都可以从配置良好的CI/CD、Docker映像或一些额外的代码质量工具(如CodeClimate或SonarCloud)开始。本文涵盖了所有这些内容,我们将看看如何将它们添加到Python项目中!在写这篇文章之前,我还写了一篇文章《PythonProjectUltimateSettings》。如果读者有兴趣,可以先阅读那篇文章:https://martinheinz.dev/blog/14。GitHub存储库中提供了完整的源代码和文档:https://github.com/MartinHeinz/python-project-blueprint。开发中的可调试Docker容器有些人不喜欢Docker,因为容器很难调试,或者构建映像花费的时间太长。所以,让我们从这里开始构建适合开发的镜像——快速构建且易于调试。为了方便镜像的调试,我们需要一个基础镜像,包括所有调试可能用到的工具,如bash、vim、netcat、wget、cat、find、grep等,默认包含了很多工具,没有它也很容易安装。此图像很笨重,但没关系,因为它仅用于开发。您可能还注意到我选择了非常具体的图像-Python和Debian的锁定版本-我是故意这样做的,因为我们希望尽量减少由于Python或Debian更新(可能不兼容)版本的可能性而导致的“损坏”。作为替代方案,您也可以使用基于Alpine的镜像。但是,这可能会导致一些问题,因为它使用musllibc而不是Python所依赖的glibc。因此,如果您决定走这条路,请记住这一点。至于构建速度,我们将利用多阶段构建,以便可以缓存尽可能多的层。通过这种方式,我们避免下载依赖项和工具,如gcc,以及应用程序所需的所有库(来自requirements.txt)。为了进一步加快速度,我们将从前面提到的python:3.8.1-buster创建一个自定义基础映像,它将包含我们需要的所有工具,因为我们无法缓存下载和安装这些工具所需的步骤,直到跑步者形象的尽头。说得够多了,让我们看看Dockerfile:#dev.DockerfileFROMpython:3.8.1-busterASbuilderRUNapt-getupdate&&apt-getinstall-y--no-install-recommends--yespython3-venvgcclibpython3-dev&&\python3-mvenv/venv&&\/venv/bin/pipinstall--upgradepipFROMbuilderASbuilder-venvCOPYrequirements.txt/requirements.txtRUN/venv/bin/pipinstall-r/requirements.txtFROMbuilder-venvAStesterCOPY./appWORKDIR/appRUN/venv/bin/pytestFROMmartinheinz/python-3.8.1-总线工具:latestASrunnerCOPY--from=tester/venv/venvCOPY--from=tester/app/appWORKDIR/appENTRYPOINT["/venv/bin/python3","-m","blueprint"]USER1001LABELname={NAME}LABELversion={VERSION}作为从上面可以看出,在创建最终的跑步者图像之前,我们必须经过3个中间图像。第一个是名为builder的镜像,它下载构建最终应用程序所需的所有必要库,包括gcc和Python虚拟环境。安装后,它还会创建将用于后续映像的实际虚拟环境。接下来是build-venvmirror,它将依赖项列表(requirements.txt)复制到镜像中,然后安装它。缓存将使用这个中间图像,因为我们只希望在requirement.txt更改时安装库,否则我们使用缓存。在创建最终图像之前,我们首先针对应用程序运行测试。这发生在测试图像中。我们将源代码复制到图像中并运行测试。如果测试通过,我们将继续构建运行器。对于runner镜像,我们使用自定义镜像,其中包含一些额外的工具,例如普通Debian镜像中不存在的vim或netcat。您可以在DockerHub中找到此图像:https://hub.docker.com/repository/docker/martinheinz/python-3.8.1-buster-tools您还可以在base.Dockerfile中看到它非常简单的`Dockerfile`:https://github.com/MartinHeinz/python-project-blueprint/blob/master/base.Dockerfile所以,我们在这个最终图像中所做的是-首先我们从测试图像复制虚拟环境,其中包括所有已安装的依赖项,接下来我们复制测试过的应用程序。现在我们已经拥有映像中的所有资源,我们进入应用程序所在的目录并设置ENTRYPOINT,以便它在映像启动时运行我们的应用程序。出于安全原因,我们还将USER设置为1001,因为最佳实践告诉我们永远不要在root用户下运行容器。最后两行设置镜像标签。当使用make目标运行构建时,它们将被替换/填充,我们稍后会看到。为生产优化的Docker容器当谈到生产级图像时,我们希望它们保持小巧、安全和快速。对于此任务,我个人最喜欢的是来自Distroless项目的Python镜像。但什么是Distroless?让我们这样说吧——在一个理想的世界里,每个人都可以使用FROMscratch来构建他们的镜像,然后将它用作基础镜像(又名空镜像)。然而,大多数人不愿意这样做,因为这需要静态链接二进制文件等。这就是Distroless的用途-它使每个人都可以从头开始。好了,现在让我们详细介绍一下什么是Distroless。它是由谷歌生成的一组图像,包含应用程序所需的最低条件,这意味着没有外壳、包管理器或任何其他会使图像膨胀并干扰安全扫描器(如CVE)的工具,增加建立合规性的难度。现在我们知道我们在做什么,让我们看看生产Dockerfile......实际上,我们不会在这里改变太多,它只是两行:#prod.Dockerfile#1.Line-ChangebuilderimageFROMdebian:buster-slimASbuilder#...#17.Line-SwitchtoDistrolessimageFROMgcr.io/distroless/python3-debian10ASrunner#...RestoftheDockefile我们需要更改的只是用于构建和运行应用程序的基础映像!但是差别很大——我们的开发映像是1.03GB而这个只有103MB,这就是差别!我知道,我已经能听到你说,“但Alpine可以更小!”是的,的确如此,但大小并不重要。您只会在下载/上传时注意到图像大小,这种情况并不经常发生。当镜像运行时,大小根本无关紧要。比大小更重要的是安全性,从这个意义上说,Distroless绝对有优势,因为Alpine(一个很好的替代品)有很多额外的包,增加了攻击面。关于Distroless最后值得一提的是图像调试。考虑到Distroless不包含任何shell(甚至不包括sh),当您需要调试和查找时它变得棘手。为此,所有Distroless图像都具有调试版本。因此,当您遇到问题时,您可以使用调试标志构建生产映像,将其与正常映像一起部署,执行到映像中并进行(比如说)线程转储。你可以像这样使用调试版本的python3镜像:dockerrun--entrypoint=sh-tigcr.io/distroless/python3-debian10:debug所有的操作都只是一个命令所有的Dockerfiles都准备好了,让我们用Makefile自动化来实现吧!我们要做的第一件事是使用Docker构建应用程序。要构建开发镜像,我们可以执行makebuild-dev,它运行以下目标:#Thebinarytobuild(justthebasename).MODULE:=blueprint#Wheretopushthedockerimage.REGISTRY?=docker.pkg.github.com/martinheinz/python-project-blueprintIMAGE:=$(REGISTRY)/$(MODULE)#Thisversion-strategyusesgittagstosettheversionstringTAG:=$(shellgitdescribe--tags--always--dirty)build-dev:@echo"\n${BLUE}BuildingDevelopmentimagewithlabels:\n"@echo"name:$(MODULE)"@echo"version:$(TAG)${NC}\n"@sed\-e's|{NAME}|$(MODULE)|g'\-e's|{VERSION}|$(TAG)|g'\dev.Dockerfile|dockerbuild-t$(IMAGE):$(TAG)-f-.该目标将构建图像。它首先用图像名称和标签(通过运行gitdescribe创建)替换dev.Dockerfile底部的标签,然后运行??dockerbuild。接下来,使用makebuild-prodVERSION=1.0.0构建生产镜像:build-prod:@echo"\n${BLUE}BuildingProductionimagewithlabels:\n"@echo"name:$(MODULE)"@echo"version:$(VERSION)${NC}\n"@sed\-e's|{NAME}|$(MODULE)|g'\-e's|{VERSION}|$(VERSION)|g'\prod.Dockerfile|dockerbuild-t$(图像):$(版本)-f-。这个目标与之前的目标非常相似,但在上面的示例1.0.0中,我们使用作为参数传递的版本而不是版本的git标记。当你在Docker中运行某些东西时,有时你还需要在Docker中调试它,为此,有以下目标:#Example:makeshellCMD="-c'date>datefile'"shell:build-dev@echo"\n${BLUE}在容器化构建环境中启动shell...${NC}\n"@dockerrun\-ti\--rm\--entrypoint/bin/bash\-u$$(id-u):$$(id-g)\$(IMAGE):$(TAG)\$(CMD)从上面我们可以看出入口点被bash覆盖,容器命令被参数覆盖。这样,我们就可以直接进入到容器中进行浏览,或者像上面的例子一样运行一次性的命令。当我们完成编码并想要将图像推送到Docker注册表时,我们可以使用makepushVERSION=0.0.2。让我们看看目标做了什么:REGISTRY?=docker.pkg.github.com/martinheinz/python-project-blueprintpush:build-prod@echo"\n${BLUE}PushingimagetoGitHubDockerRegistry...${NC}\n"@dockerpush$(IMAGE):$(VERSION)首先运行我们之前看到的目标构建产品,然后运行??dockerpush。这假设您已经登录到Docker注册表,因此您需要在运行此命令之前运行dockerlogin。最终目标是清理Docker工件。它使用替换到Dockerfile中的名称标签来过滤和查找需要删除的工件:docker-clean:@dockersystemprune-f--filter"label=name=$(MODULE)"您可以在我的存储库中找到MakefileFull代码清单:https://github.com/MartinHeinz/python-project-blueprint/blob/master/MakefileCI/CDwithGitHubActions现在让我们使用所有这些方便的make目标设置CI/CD。我们将使用GitHubActions和GitHubPackageRegistry来构建管道(作业)和存储图像。那么它们是什么?GitHubActions是帮助您自动化开发工作流程的作业/管道。您可以使用它们来创建单独的任务,然后将它们组合成自定义工作流,并在每次推送到存储库或创建发布时执行它们。GitHubPackageRegistry是一个与GitHub完全集成的包托管服务。它允许您存储各种类型的包,例如Rubygems或npm包。我们将使用它来存储Docker镜像。如果您不熟悉GitHubPackageRegistry,可以查看我的博客文章了解更多信息:https://martinheinz.dev/blog/6。现在,为了使用GitHubActions,我们需要创建将根据我们选择的触发器(例如推送到存储库)执行的工作流。这些工作流是存储库.github/workflows目录中的YAML文件:.github└──workflows├──build-test.yml└──push.yml在那里我们将创建两个文件build-test。yml和push.yml。前者包含2个作业,每次推送到存储库时都会触发,让我们看看这两个作业:jobs:build:runs-on:ubuntu-lateststeps:-uses:actions/checkout@v1-name:RunMakefilebuildforDevelopmentrun:makebuild-dev第一项工作称为构建,它验证我们的应用程序是否可以通过运行makebuild-dev目标来构建。在运行之前,它首先通过执行在GitHub上发布的名为checkout的操作来检查我们的存储库。工作:测试:运行:ubuntu-lateststeps:-uses:actions/checkout@v1-uses:actions/setup-python@v1with:python-version:'3.8'-name:InstallDependenciesrun:|python-mpipinstall--upgradepippipinstall-rrequirements.txt-name:RunMakefiletestrun:maketest-name:InstallLintersrun:|pipinstallpylintpipinstallflake8pipinstallbandit-name:RunLintersrun:makelint第二项工作稍微复杂一些。它测试我们的应用程序并运行3个linters(代码质量检查工具)。与之前的作业一样,我们使用checkout@v1操作来获取源代码。在此之后,我们运行另一个已发布的操作setup-python@v1来设置python环境。要了解更多信息,请查看此处:https://github.com/actions/setup-python我们已经有了Python环境,我们还需要requirements.txt中的应用程序依赖项,我们使用pip安装了这些依赖项。此时,我们可以继续运行maketest目标,这将触发我们的pytest套件。如果我们的测试套件通过,我们将继续安装上述linters——pylint、flake8和bandit。最后,我们运行makelint目标,这将触发每个linter。这就是构建/测试作业的全部内容,但是推送作业呢?让我们也看看它:on:push:tags:-'*'jobs:push:runs-on:ubuntu-lateststeps:-uses:actions/checkout@v1-name:Setenvrun:echo::set-envname=RELEASE_VERSION::$(echo${GITHUB_REF:10})-name:LogintoRegistryrun:echo"${{secrets.REGISTRY_TOKEN}}"|dockerlogindocker.pkg.github.com-u${{github.actor}}--密码-标准输入-name:PushtoGitHubPackageRegistryrun:makepushVERSION=${{env.RELEASE_VERSION}}前四行定义何时触发作业。我们指定作业仅在将标签推送到存储库时才启动(*模式指定标签名称——在本例中为任何名称)。这样,我们不会在每次推送到存储库时都将我们的Docker映像推送到GitHubPackageRegistry,而是仅在推送指定应用程序新版本的标签时才推送。现在让我们看看作业的主体——它首先检查源代码并将环境变量RELEASE_VERSION设置为我们推送的git标签。这是通过GitHubActions中内置的::setenv功能完成的(更多信息在这里:https://help.github.com/en/actions/automating-your-workflow-with-github-actions/development-tools-for-github-actions#set-an-environment-variable-set-env)。接下来,它使用存储在存储库中的secretREGISTRY_TOKEN登录到Docker注册表,并由启动工作流的用户(github.actor)登录。最后,在最后一行,它运行目标推送,构建生产镜像并将其推送到注册表,之前推送的git标签作为镜像标签。感兴趣的读者可以从这里查看完整的代码清单:https://github.com/MartinHeinz/python-project-blueprint/tree/master/.github/workflows使用CodeClimate进行代码质量检查最后但同样重要的是,我们还将添加使用CodeClimate和SonarCloud进行代码质量检查。它们将与上面的测试作业一起触发。因此,让我们添加以下行:#test,lint...-name:SendreporttoCodeClimaterun:|exportGIT_BRANCH="${GITHUB_REF/refs\/heads\//}"curl-Lhttps://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64>./cc-test-reporterchmod+x./cc-test-reporter./cc-test-reporterformat-coverage-tcoverage.pycoverage.xml./cc-test-reporterupload-coverage-r"${{secrets.CC_TEST_REPORTER_ID}}"-name:SonarCloudscanneruses:sonarsource/sonarcloud-github-action@masterenv:GITHUB_TOKEN:${{secrets.GITHUB_TOKEN}}SONAR_TOKEN:${{secrets.SONAR_TOKEN}我们从CodeClimate开始,它首先输出变量GIT_BRANCH,我们将使用环境变量GITHUB_REF检索它。接下来,我们下载CodeClimate测试报告程序并使其可执行。接下来,我们使用它来格式化测试套件生成的覆盖率报告,并且在最后一行,我们将它与存储在存储库秘密中的测试报告者ID一起发送到CodeClimate。至于SonarCloud,我们需要在repository中创建一个sonar-project.properties文件,大概是这样的(这个文件的值可以在SonarClouddashboard的右下角找到):sonar.organization=martinheinz-githubsonar。projectKey=MartinHeinz_python-project-blueprintsonar.sources=blueprint否则我们可以使用现有的sonarcloud-github-action,它将为我们完成所有工作。我们所要做的就是提供2个令牌——GitHub令牌默认已经在存储库中,而SonarCloud令牌可以从SonarCloud网站获取。注意:关于如何获取和设置前面提到的所有令牌和秘密的步骤在存储库的README中:https://github.com/MartinHeinz/python-project-blueprint/blob/master/README.md就是这样总之!有了上面的工具、配置和代码,您就可以构建并完全自动化您的下一个Python项目了!如果您想了解有关本文所讨论主题的更多信息,请查看存储库中的文档和代码:https://github.com/MartinHeinz/python-project-blueprint,如果您有任何建议/问题,请请随时通过在存储库中提交问题与我们联系,或者如果您喜欢我的小项目,请给我竖起大拇指。
