從零開始寫 Docker(十五)---實現 mydocker run -e 支援環境變數傳遞

探索云原生發表於2024-05-24

mydocker-run-e.png

本文為從零開始寫 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=17xname=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

相關文章