本文為從零開始寫 Docker 系列第十五篇,實現 mydocker run -e
, 支援在啟動容器時指定環境變數,讓容器內執行的程式可以使用外部傳遞的環境變數。
完整程式碼見:https://github.com/lixd/mydocker
歡迎 Star
推薦閱讀以下文章對 docker 基本實現有一個大致認識:
- 核心原理:深入理解 Docker 核心原理:Namespace、Cgroups 和 Rootfs
- 基於 namespace 的檢視隔離:探索 Linux Namespace:Docker 隔離的神奇背後
- 基於 cgroups 的資源限制
- 初探 Linux Cgroups:資源控制的奇妙世界
- 深入剖析 Linux Cgroups 子系統:資源精細管理
- Docker 與 Linux Cgroups:資源隔離的魔法之旅
- 基於 overlayfs 的檔案系統:Docker 魔法解密:探索 UnionFS 與 OverlayFS
- 基於 veth pair、bridge、iptables 等等技術的 Docker 網路:揭秘 Docker 網路:手動實現 Docker 橋接網路
開發環境如下:
root@mydocker:~# lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 20.04.2 LTS
Release: 20.04
Codename: focal
root@mydocker:~# uname -r
5.4.0-74-generic
注意:需要使用 root 使用者
1. 概述
本節實現mydocker run -e
flag,支援在啟動容器時指定環境變數,讓容器內執行的程式可以使用外部傳遞的環境變數。
2. 實現
實現也比較簡單,就是在構建 cmd 的時候指定 Env 引數。
- 1)run 命令增加 -e 引數
- 2)cmd 中指定 Env 引數
run 命令增加 -e flag
在原來的基礎上,增加 -e 選項指定環境變數,由於可能存在多個環境變數,因此允許使用者透過多次使用 -e 選項來傳遞多個環境變數。
var runCommand = cli.Command{
Name: "run",
Usage: `Create a container with namespace and cgroups limit
mydocker run -it [command]
mydocker run -d -name [containerName] [imageName] [command]`,
Flags: []cli.Flag{
// 省略其他內容
cli.StringSliceFlag{ // 增加 -e flag
Name: "e",
Usage: "set environment,e.g. -e name=mydocker",
},
},
Action: func(context *cli.Context) error {
if len(context.Args()) < 1 {
return fmt.Errorf("missing container command")
}
envSlice := context.StringSlice("e") // 獲取 env 並傳遞
Run(tty, cmdArray, envSlice, resConf, volume, containerName, imageName)
return nil
},
}
注意到這裡的型別是cli. StringSliceFlag
,即字串陣列引數,因為這是針對傳入多個環境變數的情況。
然後增加對環境變數的解析,並且傳遞給 Run 函式。
cmd 物件指定 Env 引數
由於原來的 command 實際就是容器啟動的程序,所以只需要在原來的基礎上,增加一下環境變數的配置即可。
預設情況下,新啟動程序的環境變數都是繼承於原來父程序的環境變數,但是如果手動指定了環境變數,那麼這裡就會覆蓋掉原來繼承自父程序的變數。
由於在容器的程序中,有時候還需要使用原來父程序的環境變數,比如 PATH 等,因此這裡會使用 os.Environ() 來獲取宿主機的環境變數,然後把自定義的變數加進去。
func NewParentProcess(tty bool, volume, containerId, imageName string, envSlice []string) (*exec.Cmd, *os.File) {
// 省略其他內容
cmd.Env = append(os.Environ(), envSlice...)
return cmd, writePipe
}
到此,環境變數的實現就完成了。
3. 測試
透過 -e 注入兩個環境變數測試一下
$ go build .
./mydocker run -it -name c1 -e user=17x -e name=mydocker busybox sh
然後在容器中檢視環境變數
/ # env|grep user
user=17x
/ # env|grep name
name=mydocker
這裡可以看到,手動指定的環境變數 user=17x
和name=mydocker
都已經可以在容器內可見了。
說明,-e flag 基本 ok。
下面建立一個後臺執行的容器,檢視一下是否可以。
root@mydocker:~/feat-run-e/mydocker# ./mydocker run -d -name c2 -e user=17x -e name=mydocker busybox top
檢視 ID
root@mydocker:~/feat-run-e/mydocker# ./mydocker ps
ID NAME PID STATUS COMMAND CREATED
9250006592 c2 228185 running top 2024-02-27 10:37:09
然後透過 exec 命令進入容器,檢視環境變數
root@mydocker:~/feat-run-e/mydocker# ./mydocker exec 9250006592 sh
# 容器內
/ # env|grep user
/ #
可以發現,並沒有看到建立時指定的環境變數。
這裡看不到環境變數的原因是:exec 命令其實是 mydocker 建立
的另外一個程序,這個程序的父程序其實是宿主機的的程序,並不是容器程序的。
因為在 Cgo 裡面使用了 setns 系統呼叫,才使得這個程序進入到了容器內的名稱空間
由於環境變數是繼承自父程序的,因此這個 exec 程序的環境變數其實是繼承自宿主機的,所以在 exec 程序內看到的環境變數其實是宿主機的環境變數。
因此需要修改一下 exec 命令實現,使其能夠看到容器中的環境變數。
4. 修改 mydocker exec 命令
首先提供了一個函式,可以根據指定的 PID 來獲取對應程序的環境變數。
// getEnvsByPid 讀取指定PID程序的環境變數
func getEnvsByPid(pid string) []string {
path := fmt.Sprintf("/proc/%s/environ", pid)
contentBytes, err := os.ReadFile(path)
if err != nil {
log.Errorf("Read file %s error %v", path, err)
return nil
}
// env split by \u0000
envs := strings.Split(string(contentBytes), "\u0000")
return envs
}
由於程序存放環境變數的位置是/proc/<PID>/environ
,因此根據給定的 PID 去讀取這個檔案,便可以獲取環境變數。
在檔案的內容中,每個環境變數之間是透過\u0000
分割的,因此以此為標記來獲取環境變數陣列。
然後再啟動 exec 程序時把容器中的環境變數也一併帶上:
func ExecContainer(containerName string, comArray []string) {
// 省略其他內容
// 把指定PID程序的環境變數傳遞給新啟動的程序,實現透過exec命令也能查詢到容器的環境變數
containerEnvs := getEnvsByPid(pid)
cmd.Env = append(os.Environ(), containerEnvs...)
}
這樣,exec 到容器內之後就可以看到所有的環境變數了。
再次測試一下,使用通用的 exec 命令進入容器,檢視能否看到環境變數
root@mydocker:~/feat-run-e/mydocker# ./mydocker exec 9250006592 sh
/ # env|grep user
user=17x
/ # env|grep name
name=mydocker
ok,mydocker exec 已經可以獲取到容器中的環境變數了。
5. 小結
本章實現了mydocker run -e
flag 的新增,支援啟動容器時傳遞環境變數到容器中。
核心實現就是啟動 cmd 時指定 Env 引數,具體如下:
cmd := exec.Command("/proc/self/exe", "init")
cmd.Env = append(os.Environ(), envSlice...)
同時修改了 exec 命令,將容器中的環境變數append 到 exec 程序,便於檢視。
【從零開始寫 Docker 系列】持續更新中,搜尋公眾號【探索雲原生】訂閱,閱讀更多文章。
完整程式碼見:https://github.com/lixd/mydocker
歡迎關注~
相關程式碼見 refactor-isolate-rootfs
分支,測試指令碼如下:
需要提前在 /var/lib/mydocker/image 目錄準備好 busybox.tar 檔案,具體見第四篇第二節。
# 克隆程式碼
git clone -b feat-run-e https://github.com/lixd/mydocker.git
cd mydocker
# 拉取依賴並編譯
go mod tidy
go build .
# 測試
./mydocker run -d -name c1 -e user=17x -e name=mydocker busybox top
# 檢視容器 Id
./mydocker ps
# stop 停止指定容器
./mydocker exec ${containerId} sh