Categories
程式開發

如何將你的Python項目全面自動化?


每個項目——無論你是在從事Web應用程序、數據科學還是AI開發——都可以從配置良好的CI/CD、Docker鏡像或一些額外的代碼質量工具(如CodeClimate或SonarCloud)中獲益。所有這些都是本文要討論的內容,我們將看看如何將它們添加到Python項目中!

本文最初發佈於Martin Heinz的個人博客,由InfoQ中文站翻譯並分享。

開發環境中可調試的Docker容器

有些人不喜歡Docker,因為容器很難調試,或者構建鏡像需要花很長的時間。那麼,就讓我們從這裡開始,構建適合開發的鏡像——構建速度快且易於調試。

為了使鏡像易於調試,我們需要一個基礎鏡像,包括所有調試時可能用到的工具,像bashvimnetcatwgetcatfindgrep等。它默認包含很多工具,沒有的也很容易安裝。這個鏡像很笨重,但這不要緊,因為它只用於開發。你可能也注意到了,我選擇了非常具體的映像——鎖定了Python和Debian的版本——我是故意這麼做的,因為我們希望最小化Python或Debian版本更新(可能不兼容)導致“破壞”的可能性。

作為替代方案,你也可以使用基於Alpine的鏡像。然而,這可能會導致一些問題,因為它使用musl libc而不是Python所依賴的glibc。所以,如果決定選擇這條路線,請記住這一點。

至於構建速度,我們將利用多階段構建以便可以緩存盡可能多的層。通過這種方式,我們可以避免下載諸如gcc之類的依賴項和工具,以及應用程序所需的所有庫(來自requirements.txt)。

為了進一步提高速度,我們將從前面提到的python:3.8.1-buster創建自定義基礎鏡像,這將包括我們需要的所有工具,因為我們無法將下載和安裝這些工具所需的步驟緩存到最終的runner鏡像中。

說的夠多了,讓我們看看Dockerfile

# dev.Dockerfile
FROM python:3.8.1-buster AS builder
RUN apt-get update && apt-get install -y --no-install-recommends --yes python3-venv gcc libpython3-dev && 
    python3 -m venv /venv && 
    /venv/bin/pip install --upgrade pip
FROM builder AS builder-venv
COPY requirements.txt /requirements.txt
RUN /venv/bin/pip install -r /requirements.txt
FROM builder-venv AS tester
COPY . /app
WORKDIR /app
RUN /venv/bin/pytest
FROM martinheinz/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}
LABEL version={VERSION}

從上面可以看到,在創建最後的runner鏡像之前,我們要經歷3個中間鏡像。首先是名為builder的鏡像,它下載構建最終應用所需的所有必要的庫,其中包括gcc和Python虛擬環境。安裝完成後,它還創建了實際的虛擬環境,供接下來的鏡像使用。
接下來是build -venv鏡像,它將依賴項列表(requirements.txt)複製到鏡像中,然後安裝它。緩存會用到這個中間鏡像,因為我們只希望在requirement .txt更改時安裝庫,否則我們就使用緩存。

在創建最終鏡像之前,我們首先要針對應用程序運行測試。這發生在tester鏡像中。我們將源代碼複製到鏡像中並運行測試。如果測試通過,我們就繼續構建runner

對於runner鏡像,我們使用自定義鏡像,其中包括一些額外的工具,如vimnetcat,這些功能在正常的Debian鏡像中是不存在的。

你可以在Docker Hub:https://hub.docker.com/repository/docker/martinheinz/python-3.8.1-buster-tools 中找到這個鏡像;

你也可以在base.Dockerfilehttps://github.com/MartinHeinz/python-project-blueprint/blob/master/base.Dockerfile 中查看其非常簡單的Dockerfile

那麼,我們在這個最終鏡像中要做的是——首先我們從tester鏡像中復制虛擬環境,其中包含所有已安裝的依賴項,接下來我們複製經過測試的應用程序。現在,我們的鏡像中已經有了所有的資源,我們進入應用程序所在的目錄,然後設置ENTRYPOINT,以便它在啟動鏡像時運行我們的應用程序。出於安全原因,我們還將USER設置為1001,因為最佳實踐告訴我們,永遠不要在root用戶下運行容器。最後兩行設置鏡像標籤。它們將在使用make目標運行構建時被替換/填充,稍後我們將看到。

針對生產環境優化過的Docker容器

當涉及到生產級鏡像時,我們會希望確保它們小而安全且速度快。對於這個任務,我個人最喜歡的是來自Distroless項目的Python鏡像。可是,Distroless是什麼呢?

這麼說吧——在一個理想的世界裡,每個人都可以使用FROM scratch構建他們的鏡像,然後作為基礎鏡像(也就是空鏡像)。然而,大多數人不願意這樣做,因為那需要靜態鏈接二進製文件,等等。這就是Distroless的用途——它讓每個人都可以FROM scratch

好了,現在讓我們具體描述一下​​Distroless是什麼。它是由谷歌生成的一組鏡像,其中包含應用程序所需的最低條件,這意味著沒有shell、包管理器或任何其他工具,這些工具會使鏡像膨脹,干擾安全掃描器(如CVE),增加建立遵從性的難度。

現在,我們知道我們在幹什麼了,讓我們看看生產環境的Dockerfile……實際上,這裡我們不會做太大改變,它只有兩行:

# prod.Dockerfile
#  1. Line - Change builder image
FROM debian:buster-slim AS builder
#  ...
#  17. Line - Switch to Distroless image
FROM gcr.io/distroless/python3-debian10 AS runner
#  ... Rest of the Dockefile

我們需要更改的只是用於構建和運行應用程序的基礎鏡像!但區別相當大——我們的開發鏡像是1.03GB,而這個只有103MB,這就是區別!我知道,我已經能聽到你說:“但是Alpine可以更小!”是的,沒錯,但是大小沒那麼重要。你只會在下載/上傳時注意到鏡像的大小,這並不經常發生。當鏡像運行時,大小根本不重要。比大小更重要的是安全性,從這個意義上說,Distroless肯定更有優勢,因為Alpine(一個很好的替代選項)有很多額外的包,增加了攻擊面。

關於Distroless,最後值得一提的是鏡像調試。考慮到Distroless不包含任何shell(甚至不包含sh),當你需要調試和查找時,就變得非常棘手。為此,所有Distroless鏡像都有調試版本。因此,當遇到問題時,你可以使用debug標記構建生產鏡像,並將其與正常鏡像一起部署,通過exec命令進入鏡像並執行(比如說)線程轉儲。你可以像下面這樣使用調試版本的python3鏡像:

docker run --entrypoint=sh -ti gcr.io/distroless/python3-debian10:debug

所有操作都只需一條命令

所有的Dockerfiles都準備好了,讓我們用Makefile實現自動化!我們首先要做的是用Docker構建應用程序。為了構建dev映像,我們可以執行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 tags to set the version string
TAG := $(shell git describe --tags --always --dirty)
build-dev:
 @echo "n${BLUE}Building Development 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- .

這個目標會構建鏡像。它首先會用鏡像名和Tag(運行git describe創建)替換dev.Dockerfile底部的標籤,然後運行docker build

接下來,使用make build-prod VERSION=1.0.0構建生產鏡像:

build-prod:
 @echo "n${BLUE}Building Production image 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 shell in the containerized build environment...${NC}n"
  @docker run                                                     
   -ti                                                     
   --rm                                                    
   --entrypoint /bin/bash                                  
   -u $$(id -u):$$(id -g)                                  
   $(IMAGE):$(TAG)             
   $(CMD)

從上面我們可以看到,入口點被bash覆蓋,而容器命令被參數覆蓋。通過這種方式,我們可以直接進入容器瀏覽,或運行一次性命令,就像上面的例子一樣。

當我們完成了編碼並希望將鏡像推送到Docker註冊中心時,我們可以使用make push 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"
 @docker push $(IMAGE):$(VERSION)

它首先運行我們前面看到的目標build-prod,然後運行docker push。這裡假設你已經登錄到Docker註冊中心,因此在運行這個命令之前,你需要先運行docker login

最後一個目標是清理Docker工件。它使用被替換到Dockerfiles中性name標籤來過濾和查找需要刪除的工件:

docker-clean:
 @docker system prune -f --filter "label=name=$(MODULE)"

你可以在我的存儲庫中找到Makefile的完整代碼清單:https://github.com/MartinHeinz/python-project-blueprint/blob/master/Makefile。

借助GitHub Actions實現CI/CD

現在,讓我們使用所有這些方便的make目標來設置CI/CD。我們將使用GitHub Actions和GitHubPackage Registry來構建管道(作業)及存儲鏡像。那麼,它們又是什麼呢?

  • GitHub動作是幫助你自動化開發工作流的作業/管道。你可以使用它們創建單個的任務,然後將它們合併到自定義工作流中,然後在每次推送到存儲庫或創建發佈時執行這些任務。

  • GitHub軟件包註冊表是一個包託管服務,與GitHub完全集成。它允許你存儲各種類型的包,例如Ruby gems或npm包。我們將使用它來存儲Docker鏡像。如果你不熟悉GitHub Package Registry,那麼你可以查看我的博文,了解更多相關信息:https://martinheinz.dev/blog/6

現在,為了使用GitHubActions,我們需要創建將基於我們選擇的觸發器(例如push to repository)執行的工作流。這些工作流是存儲庫中.github/workflows目錄下的YAML文件:

.github
└── workflows
    ├── build-test.yml
    └── push.yml

在那裡,我們將創建兩個文件build-test.ymlpush.yml。前者包含2個作業,將在每次推送到存儲庫時被觸發,讓我們看下這兩個作業:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/[email protected]
    - name: Run Makefile build for Development
      run: make build-dev

第一個作業名為build,它驗證我們的應用程序可以通過運行make build-dev目標來構建。在運行之前,它首先通過執行發佈在GitHub上名為checkout的操作簽出我們的存儲庫。

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/[email protected]
    - uses: actions/[email protected]
      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(代碼質量檢查工具)。與上一個作業一樣,我們使用[email protected]操作來獲取源代碼。在此之後,我們運行另一個已發布的操作[email protected],設置python環境(要了解詳細信息,請點擊這裡:https://github.com/actions/setup-python )。

我們已經有了Python環境,我們還需要requirements.txt中的應用程序依賴關係,這是我們用pip安裝的。這時,我們可以著手運行make test目標,它將觸發我們的Pytest套件。如果我們的測試套件測試通過,我們繼續安裝前面提到的linter——pylint、flake8和bandit。最後,我們運行make lint目標,它將觸發每一個linter。
關於構建/測試作業的內容就這些,但push作業呢?讓我們也一起看下:

on:
  push:
    tags:
    - '*'
jobs:
  push:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/[email protected]
    - name: Set env
      run: echo ::set-env name=RELEASE_VERSION::$(echo ${GITHUB_REF:10})
    - name: Log into Registry
      run: echo "${​{ secrets.REGISTRY_TOKEN }}" | docker login docker.pkg.github.com -u ${​{ github.actor }} --password-stdin
    - name: Push to GitHub Package Registry
      run: make push VERSION=${​{ env.RELEASE_VERSION }}

前四行定義了何時觸發該作業。我們指定,只有當標籤被推送到存儲庫時,該作業才啟動(*指定標籤名稱的模式——在本例中是任何名稱)。這樣,我們就不會在每次推送到存儲庫的時候都把我們的Docker鏡像推送到GitHub Package Registry,而只是在我們推送指定應用程序新版本的標籤時才這樣做。

現在我們看下這個作業的主體——它首先簽出源代碼,並將環境變量RELEASE_VERSION設置為我們推送的git標籤。

這是通過GitHub Actions內置的::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)。最後,在最後一行,它運行目標push,構建生產鏡像並將其推送到註冊中心,以之前推送的git標籤作為鏡像標籤。

感興趣的讀者可以從這裡簽出完整的代碼清單:https://github.com/MartinHeinz/python-project-blueprint/tree/master/.github/workflows。

使用CodeClimate進行代碼質量檢查

最後但同樣重要的是,我們還將使用CodeClimate和SonarCloud添加代碼質量檢查。它們將與上文的測試作業一起觸發。所以,讓我們添加以下幾行:

# test, lint...
- name: Send report to CodeClimate
  run: |
    export GIT_BRANCH="${GITHUB_REF/refs/heads//}"
    curl -L https://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: SonarCloud scanner
  uses: sonarsource/[email protected]
  env:
    GITHUB_TOKEN: ${​{ secrets.GITHUB_TOKEN }}
    SONAR_TOKEN: ${​{ secrets.SONAR_TOKEN }}

我們從CodeClimate開始,首先輸出變量GIT_BRANCH,我們會用環境變量GITHUB_REF來檢索這個變量。接下來,我們下載CodeClimate test reporter並使其可執行。接下來,我們使用它來格式化由測試套件生成的覆蓋率報告,而且,在最後一行,我們將它與存儲在存儲庫秘密中的test reporter ID一起發送給CodeClimate。
至於SonarCloud,我們需要在存儲庫中創建sonar-project.properties文件,類似下面這樣(這個文件的值可以在SonarCloud儀表板的右下角找到):

sonar.organization=martinheinz-github
sonar.projectKey=MartinHeinz_python-project-blueprint
sonar.sources=blueprint

除此之外,我們可以使用現有的sonarcloud-github-action,它會為我們做所有的工作。我們所要做的就是提供2個令牌——GitHub令牌默認已在存儲庫中,SonarCloud令牌可以從SonarCloud網站獲得。

注意:關於如何獲取和設置前面提到的所有令牌和秘密的步驟都在存儲庫的自述文件中:https://github.com/MartinHeinz/python-project-blueprint/blob/master/README.md

小結

就是這樣!有了上面的工具、配置和代碼,你就可以構建和全方位自動化下一個Python項目了!如果關於本文討論的主題,你想了解更多信息,請查看存儲庫中的文檔和代碼:https://github.com/MartinHeinz/python-project-blueprint,如果你有什麼建議/問題,請在存儲庫中提交問題庫,或者如果你喜歡我的這個小項目,請為我點贊。

查看英文原文:

https://martinheinz.dev/blog/17