当前位置: 首页 > 科技观察

精益求精!如何让你的Python项目受益于自动化

时间:2023-03-18 15:41:39 科技观察

本文转载自公众号《读书芯》(ID:AI_Discovery)无论你的项目是用来开发web应用,处理数据科学问题还是AI,用一个好的配置您可以受益于可在开发中调试并针对生产进行优化的CI/CD,或其他一些代码质量工具。本文将向您展示如何将它们添加到您的Python项目中!这是我的完整源代码和文档的repo: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版本引起“损坏”的可能性。资料来源:techcrunch可以使用基于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-buster-toollatestASrunnerCOPY--from=tester/venv/venvCOPY--from=tester/app/appWORKDIR/appENTRYPOINT["/venv/bin/python3","-m","blueprint"]USER1001LABELname={NAME}LABELversion={VERSION}从上面的文档可以看出,我们会创建3个中间镜像,然后创建最终的运行镜像。第一个图像称为构建器,它下载构建最终应用程序所需的所有必需库,其中包括gcc和Python虚拟环境。安装后,它还会创建下一个映像要使用的实际虚拟环境。接下来是builder-venv映像,它将依赖项列表(requirements.txt)复制到映像中,然后安装它。这个中间镜像是缓存需要的,因为只有requirements.txt发生变化才会安装库,否则只使用缓存。在创建最终图像之前,首先对应用程序运行测试。这就是测试图像所做的。我们将源代码复制到图像中并运行测试。如果通过,程序将运行到runner。对于runner镜像,我们使用自定义镜像,其中包含一些普通Debian镜像中不存在的额外功能,例如vim或netcat。您可以在此处的DockerHub上找到此图像,也可以在此处的base.Dockerfile中查看这个非常简单的Dockerfile。所以这是在最终镜像中要做的事情:首先复制虚拟环境,它保留测试器镜像中所有已安装的依赖项,然后复制测试的应用程序。现在图像已经有了所有源,移动到应用程序所在的目录并设置ENTRYPOINT以在图像启动时运行应用程序。出于安全原因,将USER设置为1001,因为最佳实践告诉我们永远不要在root用户下运行容器。最后两行设置图像的标签。当使用make命令运行构建时,这些将被替换或填充,我们将在后面看到。为生产优化的Docker容器在生产级镜像方面,我们希望确保它们体积小、安全且速度快。我个人最喜欢的是来自Distroless项目的Python镜像。那么什么是Distroless?可以这样描述:在一个理想的世界中,每个人都会使用FROMscratch作为他们的基础镜像(即空镜像)来构建他们的镜像。但这不是大多数人想要做的,因为它需要二进制文件等的静态链接。这就是Distroless发挥作用的地方,它是从头开始为每个人设计的。Distroless是Google制作的一组图像,其中包含应用程序所需的最低要求,这意味着没有shell、包管理器或任何其他工具会膨胀图像和信号安全扫描器(例如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一个命令就可以解决所有问题一旦你准备好了所有的Dockerfile,让我们用一个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-.这个目标文件首先通过在dev.Dockerfile的底部用标签替换图像的名称和标签来构建图像,标签是通过运行gitdescribe然后运行??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$(IMAGE):$(VERSION)-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)它首先运行它之前看到的build-prod文件,然后运行dockerpush。这假定您已登录到Docker注册表,因此您需要在运行此注册表之前运行dockerlogin。最后一个目标文件用于清理Docker工件。它使用用Dockerfiles替换的名称标签来过滤和查找需要删除的工件:docker-clean:@dockersystemprune-f--filter"label=name=$(MODULE)"CI/CDwithGitHubActions现在开始使用所有这些Handymaketarget命令来设置CI/CD。我们将使用GitHubActions和GitHubPackageRegistry来构建管道(作业)和存储图像。那么这两件事到底是什么呢?GithubActions是可以帮助自动化开发工作流程的作业/管道。这些可用于创建单独的任务,然后可以将这些任务组合到自定义工作流中,然后执行,例如,每次推送到存储库或创建发布时。GitHubPackageRegistry是一个与GitHub完全集成的包托管服务。它可以存储各种类型的包,例如:Rubygems或npm包。我们将使用它来存储Docker镜像。如果您不熟悉GitHubPackageRegistry并想了解更多信息,可以查看我的博文:https://martinheinz.dev/blog/6来源:unsplash现在,为了使用GitHubActions,您需要要创建工作流,这些工作流将根据选定的触发器执行(例如推送到存储库)。这些工作流是YAML文件,位于repo的.github/workflows目录中: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环境(您可以在此处找到有关它的详细信息)。现在你已经有了python环境,你还需要使用pip在requirements.txt中安装应用依赖。此时您可以继续运行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}}--password-stdin-name:PushtoGitHubPackageRegistryrun:makepushVERSION=${{env.RELEASE_VERSION}}前4行定义何时触发此作业。我们指定作业仅在将标签推送到存储库时启动(*在这种情况下指定标签名称可以是任何模式),因此不是在每次推送到存储库时将Docker镜像推送到GitHubPackageRegistry,而是,仅当推送指定应用程序新版本的标签时才推送到GitHubPackageRegistry。现在是作业的主体,它首先检索源代码并将RELEASE_VERSION环境变量设置为推送的git标记。这是通过GitHubActions的内置::setenv功能完成的。接下来,它使用存储在存储库机密中的REGISTRY_TOKEN登录到Docker注册表和启动工作流的用户(github.actor)。最后,在最后一行,它运行push命令,该命令构建生产镜像并将其推送到注册表,使用之前推送的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的右下角找到):.organization=martinheinz-githubsonar。projectKey=MartinHeinz_python-project-blueprintsonar。sources=blueprint除此之外,只需使用现有的sonarcloud-github-action即可为我们完成所有工作。所要做的就是提供2个令牌:GitHub令牌(默认情况下位于存储库中)和SonarCloud令牌(可从SonarCloud网站获得)。注意:有关如何获取和设置上述所有令牌和密钥的步骤,请参阅自述文件:https://github.com/MartinHeinz/python-project-blueprint/blob/master/README.md来源:unsplashArmed使用上述工具、配置和代码,您可以构建和自动化下一个Python项目的各个方面。去试试吧!