容器中的容器——利用Dind實現開箱即用的K3s

DrunkCat90發表於2021-12-07

我在學習 Rancher 和 Minikube 的時候,發現它們都可以在自己的容器環境中提供一個 K3s 或 K8s 叢集。尤其是 Minikube ,使用者可以在它的容器環境中執行 docker ps 等命令,這種套娃一般的 docker in docker 體驗有點兒意思。經過自己的調研和上手實踐,我成功利用 Dind 結合輕量化的 K3s 實現了開箱即用的 dind-k3s

用途場景

在研究這件事情的時候,我就在想:容器的最佳使用實踐,一直是推崇在一個容器中儘量只啟動一個程式。這麼做的好處是避免了多個程式之間相互影響,通過一個程式的好壞,就可以判定容器執行的狀態。這一最佳實踐在微服務領域體現的很好,一個容器執行一個微服務程式,程式掛掉,容器也會立即有執行失敗的反饋,配合 K8s 等排程編排系統的存活探針(Liveness Probe)與失敗重啟策略,可以實現微服務快速恢復。

那麼研究容器中執行容器這種看似更為複雜的使用方法,甚至還要在其中執行 K8s 等更復雜的系統,是否真的只是為了炫技呢?我在認真思考過後,認為起碼在以下場景中,這種使用方法是很有價值的。

  • 開發測試環境:當開發者的專案依託於 K8s 體系進行開發時,這種使用方法很有價值。對於一個普通開發者而言,如何快速部署一套 K8s 環境還是比較考驗技術的。畢竟術業有專攻,部署環境這種事情,還是專業的運維人員做起來更加順手一些。但是使用 dind-k3s 就會好很多,通過一條 docker run 命令,就可以快速啟動一個執行在容器中的 K3s 環境。所有的開發測試工作都在容器中進行,保障了環境的高度一致性。
  • CI/CD流水線:在容器中執行一系列的任務,並在最終生成構建產物並打包為映象之後,將映象推送到映象倉庫中儲存起來。這個過程對於在容器中可以使用 docker build 等命令的能力是一種剛需。
  • 邊緣裝置場景:當我們需要在一個邊緣裝置上,部署比較複雜的終端業務系統時,交付的技術成本是很高的。工程師需要逐臺裝置一一配置,低效得很。如果在目前已實現的 dind-k3s 的基礎上稍加改造,在 K3s 中自動化的初始一些資源,那麼就很方便的完成了業務系統的部署。這種部署方式還會使業務系統享受到 K3s 提供的自動化運維能力,降低部署成本的同時,保障了業務的存活能力。

Dind 簡述

Dind (docker in docker)並不是一件新鮮的事務,docker 官方很早就支援這麼做。而我是這幾天才瞭解到這一技術,這讓工作於雲原生領域,天天和容器打交道的我非常汗顏?‍♂️。這一技術的實現,要滿足兩個要點:

  • 在映象中安裝 docker,這裡的 docker 不是指日常使用的客戶端二進位制命令,更重要的是安裝 dockerd。
  • 容器需要以特權模式啟動,特權模式為容器提供訪問宿主機環境的所有許可權,所以要注意這個行為的風險性。

除了上述的必要條件之外,還需要注重 Dind 的資料持久化。Dind 容器的持久化目錄是需要被儲存的,和在VM上直接啟動的 docker 服務一樣,這個目錄就是 docker 的 data-root 目錄,預設是 /var/lib/docker

在有些環境中,這個目錄必須被掛載到宿主機上去,比如我在 Intel 晶片聯想老爺機上啟動的 ubuntu1804 虛擬機器,就遭遇了 overlay2 driver not supported ;然而在 MacOS 中啟動時,卻不可以隨便掛載到宿主機上去,會遭遇許可權問題,我依然不知道為什麼會出現這個問題,歡迎解決這個問題的小夥伴在評論裡指出。

K3s 簡述

K3s 是 Rancher 推出的輕量化 k8s 環境。安裝部署非常簡單,而且使用起來和 k8s 沒有區別。我最終選擇使用它的原因就是看中了它的輕量化特徵。畢竟我的最終目標不是單純的在容器中搭建 k8s 環境,而是部署自己的業務進去。

K3s 預設使用的容器執行時同時支援 containerd 與 docker。我由於後續要部署的業務對 docker 有較強的依賴,故而需要將 K3s 和 Dind 結合起來。K3s 同時支援部署 traefik 作為 Ingress Controller ,可以非常方便的利用 Ingress Rules 暴露服務。

最終形態

Dind-K3s.drawio

具體實現

我已經將相關程式碼上傳至程式碼倉庫中,供感興趣的人檢視:

程式碼的結構如下:

.
├── Dockerfile              # 構建 dind-k3s          
├── README.en.md
├── README.md
├── docker-entrypoint.sh    # 容器啟動指令碼
└── utils
    ├── daemon.json         # dockerd 配置檔案
    ├── dind.conf           # dockerd k3s 的程式管理配置檔案
    ├── k3s-conf.yaml       # k3s 配置檔案
    └── supervisord.conf    # supervisord 配置檔案

專案中的關鍵檔案是 Dockerfile,全文如下:

FROM ubuntu:20.10
LABEL auther="guox@goodrain.com"
WORKDIR /app

# 安裝必要的依賴
RUN sed -i -e 's/ports.ubuntu.com/mirrors.aliyun.com/g' \
    -e 's/archive.ubuntu.com/mirrors.aliyun.com/g' \
    -e 's/security.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list \
    && apt update \
    && apt install -y supervisor iptables wget vim \
    && rm -rf /var/lib/apt/lists/*
# 安裝 docker k3s kubectl
# 根據構建環境的 CPU 架構區分下載地址
RUN Arch="$(arch)"; \
	case "$Arch" in \
		'x86_64') \
			docker_url='https://download.docker.com/linux/static/stable/x86_64/docker-20.10.11.tgz'; \
            k3s_url="https://github.com/rancher/k3s/releases/download/v1.22.3+k3s1/k3s" \
            kubectl_url="https://storage.googleapis.com/kubernetes-release/release/v1.22.3/bin/linux/amd64/kubectl" \
			;; \
		'aarch64') \
			docker_url='https://download.docker.com/linux/static/stable/aarch64/docker-20.10.11.tgz'; \
            k3s_url="https://github.com/rancher/k3s/releases/download/v1.22.3+k3s1/k3s-arm64" \
            kubectl_url="https://storage.googleapis.com/kubernetes-release/release/v1.22.3/bin/linux/arm64/kubectl" \
			;; \
	esac \
    && wget -O docker.tgz "$docker_url" \
	&& tar xzf docker.tgz --strip-components 1 --directory /usr/local/bin/ \
	&& rm docker.tgz \
    && mkdir -p /etc/docker \
    && wget -O /usr/local/bin/k3s "$k3s_url" \
    && wget -O /usr/local/bin/kubectl "$kubectl_url" 
# 檔案的變更是最頻繁的變更,把拷貝檔案的過程放在安裝軟體包、下載大體積資源的後面,可以更合理的利用映象構建快取,極大的節約構建時間
ADD . .
# 配置檔案的處理
RUN chmod +x /usr/local/bin/k3s /usr/local/bin/kubectl /app/docker-entrypoint.sh \
    && mkdir -p /app/logs/ /app/k3s \
    && cp utils/dind.conf /etc/supervisor/conf.d/dind.conf \
    && cp utils/daemon.json /etc/docker/ \
    && cp utils/k3s-conf.yaml /app/k3s/config.yaml \
    && cp utils/supervisord.conf /etc/supervisor/supervisord.conf
   
VOLUME [ "/var/lib/docker", "/app/k3s" ]
# 啟動指令碼
ENTRYPOINT [ "/app/docker-entrypoint.sh" ]
CMD ["/usr/bin/supervisord"]

安裝相關資源

最開始,我們需要在 dind-k3s 映象中安裝 docker 、k3s、kubectl 以及相關的軟體包。這一操作被定義在了 Dockerfile。在安裝過程中,可以根據執行構建操作的宿主機的 CPU 架構,選擇對應的包進行安裝。目前支援 x86_64 以及 arm64 兩種架構。除此之外,dockerd 想要正常執行,還需要安裝 iptables 軟體包。同時安裝的 supervisor 用於容器內部程式管理,放在後文中詳細說明。所使用的版本列表如下:

  • Docker :20.10.11
  • K3s:v1.22.3+k3s1
  • Kubectl:v1.22.3

配置檔案的處理

下一個重要步驟,是將 utils 下的配置檔案,和啟動指令碼拷貝到映象中,並且將配置檔案分發到正確的位置中去。

實用 Tip:

檔案的變更是最頻繁的變更,把拷貝檔案的過程放在安裝軟體包、下載大體積資源的後面,可以更合理的利用映象構建快取,極大的節約構建時間。

構建快取的機制是,每執行一條指令,將會形成一個映象層作為構建快取。下一次構建時,有變化的指令之前的構建操作會引用上次構建已經形成的構建快取。所以,應該儘量將耗時時間長的安裝包、下載包的操作,放在靠前的 RUN 指令中執行。

啟動指令碼與CMD命令

最後,指定啟動指令碼和 CMD 命令。我在啟動指令碼中新增了一些試驗性的邏輯操作,如變更 dockerd 啟動引數之類的,大家可以自行參考。

在這裡,我想重點解釋的是啟動指令碼和 CMD 之間的關係。在 ENTRYPOINT 和 CMD 同時出現時,CMD 會作為引數傳遞給 ENTRYPOINT。當前的配置等效於 /app/docker-entrypoint.sh /usr/bin/supervisord 。所以,在 /app/docker-entrypoint.sh 指令碼的最後,新增了 exec $@ 來承接後續的引數。

ENTRYPOINT [ "/app/docker-entrypoint.sh" ]
CMD ["/usr/bin/supervisord"]

等效於

bash -c /app/docker-entrypoint.sh /usr/bin/supervisord

Supervisor 程式管理工具

鑑於 dind-k3s 容器中,註定有至少兩個程式 (docker、 k3s)要啟動,那麼就有必要引入一個程式管理工具來託管它們。在容器中使用 systemd 管理程式需要做很多特殊的操作,我轉而選擇了 supervisor 進行程式管理。

專案中的 utils/supervisord.conf 是 supervisord 本身所使用的配置檔案,主要負責配置其日誌路徑,以及 被託管的程式配置檔案的位置 。我將其定義為 /etc/supervisor/conf.d/*.conf ,符合要求的配置檔案,其內部定義的程式都會受到 supervisord 的監管。換句話說,我需要將 docker 、 k3s 的啟動方式定義到配置檔案,放到上面說的路徑中,容器啟動時,supervisord 會負責帶起它們的程式。

定義好的配置如下:

[program:dind]
priority=20
command=/usr/local/bin/dockerd
user=root
autostart=true
autorestart=true
restartpause=10
stdout_logfile=/app/logs/dind.log
stdout_logfile_maxbytes=10mb
stdout_logfile_backups=3
redirect_stderr=true

[program:k3s]
depends_on=dind
priority=20
command=/usr/local/bin/k3s server --config /app/k3s/config.yaml
user=root
autostart=true
autorestart=true
restartpause=10
stdout_logfile=/app/logs/k3s.log
stdout_logfile_maxbytes=10mb
stdout_logfile_backups=3
redirect_stderr=true

dockerd 的配置檔案為 /etc/docker/daemon.json,自定義的引數追加,參考 /app/docker-entrypoint.sh

k3s 的配置檔案為 /app/k3s/config.yaml,該檔案位於持久化目錄中,修改其配置,只需要在宿主機直接修改該檔案後重啟 dind-k3s 容器。

supervisord 提供很多高階功能,在這裡被用到的包括:

  • 依賴關係,這個是最重要的,k3s 開始啟動需要在 dockerd 啟動完成之後。

  • 自動重啟策略

  • 日誌輸出路徑、分割、尺寸限定

構建映象

一切準備就緒,可以開始構建映象了,構建命令需要在專案根目錄下執行。

docker build -t dind-k3s .

經過檢測,映象可以在 x86_64 以及 arm64 環境下構建成功並使用。

啟動容器

映象構建完成後,需要以特權模式啟動容器例項。

sudo docker run -d \
--name=my-dind-k3s \
--privileged \
-v ~/data/docker:/var/lib/docker \
-v ~/data/k3s:/app/k3s \
dind-k3s

如果需要容器內執行的容器,可以向外部暴露服務,則需要在啟動命令中使用 -p 引數指定埠對映關係。比如使用 Traefik 時,需要對映 80、 443 兩個埠;而使用 NodePort 時,則應對映 30000 + 的埠。

效果驗證

進入容器,可以執行 docker 相關的命令,也可以使用 kubectl 和 k3s 進行互動。

image-20211205142621154

完結,撒花?


Rainbond是一個開源的雲原生應用管理平臺,使用簡單,不需要懂容器和Kubernetes,支援管理多個Kubernetes叢集,提供企業級應用的全生命週期管理,功能包括應用開發環境、應用市場、微服務架構、應用持續交付、應用運維、應用級多雲管理等。

圖片

相關文章