精益求精!如何讓你的Python項目從自動化中受益
本文轉載自公眾號“讀芯術”(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項目的方各個方面了。快去試試吧!