每一個項目——不管你是在從事 Web 應用程序、數據科學仍是 AI 開發——均可以從配置良好的 CI/CD、Docker 鏡像或一些額外的代碼質量工具(如 CodeClimate 或 SonarCloud)中獲益。全部這些都是本文要討論的內容,咱們將看看如何將它們添加到 Python 項目中!php
在寫這篇文章以前,我還寫了一篇「Python 項目終極設置」,讀者感興趣的話,能夠先讀下那一篇:https://martinheinz.dev/blog/14。css
有些人不喜歡 Docker,由於容器很難調試,或者構建鏡像須要花很長的時間。那麼,就讓咱們從這裏開始,構建適合開發的鏡像——構建速度快且易於調試。python
爲了使鏡像易於調試,咱們須要一個基礎鏡像,包括全部調試時可能用到的工具,像bash
、vim
、netcat
、wget
、cat
、find
、grep
等。它默認包含不少工具,沒有的也很容易安裝。這個鏡像很笨重,但這沒關係,由於它只用於開發。你可能也注意到了,我選擇了很是具體的映像——鎖定了 Python 和 Debian 的版本——我是故意這麼作的,由於咱們但願最小化 Python 或 Debian 版本更新(可能不兼容)致使「破壞」的可能性。linux
做爲替代方案,你也可使用基於 Alpine 的鏡像。然而,這可能會致使一些問題,由於它使用musl libc
而不是 Python 所依賴的glibc
。因此,若是決定選擇這條路線,請記住這一點。至於構建速度,咱們將利用多階段構建以即可以緩存儘量多的層。經過這種方式,咱們能夠避免下載諸如gcc
之類的依賴項和工具,以及應用程序所需的全部庫(來自requirements.txt
)。nginx
爲了進一步提升速度,咱們將從前面提到的python:3.8.1-buster
建立自定義基礎鏡像,這將包括咱們須要的全部工具,由於咱們沒法將下載和安裝這些工具所需的步驟緩存到最終的runner
鏡像中。說的夠多了,讓咱們看看Dockerfile
:git
# 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
更改時安裝庫,不然咱們就使用緩存。github
在建立最終鏡像以前,咱們首先要針對應用程序運行測試。這發生在tester
鏡像中。咱們將源代碼複製到鏡像中並運行測試。若是測試經過,咱們就繼續構建runner
。sql
對於runner
鏡像,咱們使用自定義鏡像,其中包括一些額外的工具,如vim
或netcat
,這些功能在正常的 Debian 鏡像中是不存在的。docker
你能夠在 Docker Hub 中找到這個鏡像:
https://hub.docker.com/repository/docker/martinheinz/python-3.8.1-buster-tools typescript
base.Dockerfile 中查看其很是簡單的`Dockerfile`
:那麼,咱們在這個最終鏡像中要作的是——首先咱們從tester
鏡像中複製虛擬環境,其中包含全部已安裝的依賴項,接下來咱們複製通過測試的應用程序。如今,咱們的鏡像中已經有了全部的資源,咱們進入應用程序所在的目錄,而後設置ENTRYPOINT
,以便它在啓動鏡像時運行咱們的應用程序。出於安全緣由,咱們還將USER
設置爲1001
,由於最佳實踐告訴咱們,永遠不要在root
用戶下運行容器。最後兩行設置鏡像標籤。它們將在使用make
目標運行構建時被替換 / 填充,稍後咱們將看到。
當涉及到生產級鏡像時,咱們會但願確保它們小而安全且速度快。對於這個任務,我我的最喜歡的是來自 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
如今,讓咱們使用全部這些方便的make
目標來設置 CI/CD。咱們將使用 GitHub Actions 和 GitHubPackage Registry 來構建管道(做業)及存儲鏡像。那麼,它們又是什麼呢?
GitHub Actions 是幫助你自動化開發工做流的做業 / 管道。你可使用它們建立單個的任務,而後將它們合併到自定義工做流中,而後在每次推送到存儲庫或建立發佈時執行這些任務。
如今,爲了使用 GitHubActions,咱們須要建立將基於咱們選擇的觸發器(例如 push to repository)執行的工做流。這些工做流是存儲庫中.github/workflows
目錄下的 YAML 文件:
.github └── workflows ├── build-test.yml └── push.yml
在那裏,咱們將建立兩個文件build-test.yml
和push.yml
。前者包含 2 個做業,將在每次推送到存儲庫時被觸發,讓咱們看下這兩個做業:
jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - 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/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 環境。要了解詳細信息,請查看這裏: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/checkout@v1 - 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 和 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/sonarcloud-github-action@master 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做者 | Martin Heinz譯者 | 平川文章來自:InfoQ