開發環境中可除錯的 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.DockerfileFROM python:3.8.1-buster AS builderRUN 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 pipFROM builder AS builder-venvCOPY requirements.txt /requirements.txtRUN /venv/bin/pip install -r /requirements.txtFROM builder-venv AS testerCOPY . /appWORKDIR /appRUN /venv/bin/pytestFROM martinheinz/python-3.8.1-buster-tools:latest AS runnerCOPY --from=tester /venv /venvCOPY --from=tester /app /appWORKDIR /appENTRYPOINT ["/venv/bin/python3", "-m", "blueprint"]USER 1001LABEL 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/repository/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 imageFROM debian:buster-slim AS builder# ...# 17. Line - Switch to Distroless imageFROM 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-blueprintIMAGE := (REGISTRY)/(MODULE)# This version-strategy uses git tags to set the version stringTAG := (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-blueprintpush: 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-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└── 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 )。
接下來,它使用儲存在儲存庫中的 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-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
原文連結:https://martinheinz.dev/blog/17
作者 | Martin Heinz
譯者 | 平川
文章來自:InfoQ
本文連結:http://www.yunweipai.com/39190.html