每個專案——無論你是在從事 Web 應用程式、
資料科學
還是 AI 開發——都可以從配置良好的 CI/CD、Docker 映象或一些額外的
程式碼質量
工具(如 CodeClimate 或 SonarCloud)中獲益。所有這些都是本文要討論的內容,我們將看看如何將它們新增到 Python 專案中!
在寫這篇文章之前,我還寫了一篇“Python 專案終極設定”,讀者感興趣的話,可以先讀下那一篇:
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 的映象。然而,這可能會導致一些問題,因為它使用
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
映象,我們使用自定義映象,其中包括一些額外的工具,如
vim
或
netcat
,這些功能在正常的 Debian 映象中是不存在的。
你可以在 Docker Hub 中找到這個映象:
https://
hub。docker。com/reposito
ry/docker/martinheinz/python-3。8。1-buster-tools
你也可以在
base。Dockerfile 中檢視其非常簡單的`Dockerfile`
:
https://
github。com/MartinHeinz/
python-project-blueprint/blob/master/base。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 Actions
是幫助你自動化開發工作流的作業 / 管道。你可以使用它們建立單個的任務,然後將它們合併到自定義工作流中,然後在每次推送到儲存庫或建立釋出時執行這些任務。
GitHub Package Registry
是一個包託管服務,與 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。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/setu
p-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/acti
ons/automating-your-workflow-with-github-actions/development-tools-for-github-actions#set-an-environment-variable-set-env
)。
接下來,它使用儲存在儲存庫中的 secret
REGISTRY_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/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