介紹
docker的sdk的官方介紹的樣例有g(shù)o和Python的如筛,并包含了如下對(duì)docker二次開發(fā)的幾種簡(jiǎn)單的實(shí)現(xiàn)
- Run a container
- Run a container in the background
- List and manage containers
- Stop all running containers
- Print the logs of a specific container
- List all images
- Pull an image
- Pull an image with authentication
- Commit a container
具體代碼請(qǐng)移步上述鏈接攒读。
這篇主要講講怎樣用go對(duì)docker進(jìn)行簡(jiǎn)單的二次開發(fā):一個(gè)docker容器的守護(hù)程序
kubernetes就是利用go對(duì)docker進(jìn)行二次開發(fā)以管理成千上萬(wàn)的docker容器的成功案例
kubernetes部署需要踩很多坑,但是有時(shí)我們只需要對(duì)docker進(jìn)行一次簡(jiǎn)單的二次開發(fā)以滿足業(yè)務(wù)的需求鹏秋,如新上線一個(gè)版本,我們需要在docker容器中部署燕雁,此時(shí)就可以對(duì)docker進(jìn)行二次開發(fā)以滿足我們的需求窒所。
需求一
- 從git或svn上拉取最新的代碼楣嘁,并將其編譯成go的二進(jìn)制可運(yùn)行文件
- 從docker倉(cāng)庫(kù)中拉需要的鏡像
- 在鏡像的基礎(chǔ)上創(chuàng)建容器磅轻,包括配置容器的一些參數(shù)等等
- 啟動(dòng)容器
這樣當(dāng)我們需要發(fā)布一個(gè)項(xiàng)目的新版本時(shí)直接運(yùn)行這個(gè)程序就能做到一鍵發(fā)布珍逸。一個(gè)容器運(yùn)行時(shí),就像一個(gè)操作系統(tǒng)運(yùn)行一樣聋溜,也有崩潰的時(shí)候谆膳,此時(shí)我們需要一個(gè)監(jiān)聽docker容器的健康狀況來(lái)以防一些意外
需求二
- 監(jiān)聽docker容器運(yùn)行時(shí)的相關(guān)參數(shù)
- 針對(duì)獲取到的參數(shù)做出相應(yīng)的處理,如mem使用打到80%時(shí)發(fā)送郵件通知小組的開發(fā)人員
- 在docker容器崩潰時(shí)能重新啟動(dòng)該容器
假設(shè)需求
現(xiàn)在我就上面介紹的兩個(gè)需求簡(jiǎn)單綜合一下勤婚,以完成一個(gè)自己的需求
- 假設(shè)本地已有我們需要的docker image
- 檢查docker container中是否已存在目標(biāo)容器
- 若有摹量,則跳轉(zhuǎn)到第5步
- 若沒(méi)有,創(chuàng)建一個(gè)從container
- 啟動(dòng)該容器并按時(shí)檢查該container的狀態(tài)
- 若該container已崩潰馒胆,那么該程序能自動(dòng)重啟container
附:我們所期望的container內(nèi)部還掛在了一個(gè)宿主機(jī)的目錄
以上就是本篇文章將要實(shí)現(xiàn)的功能
正篇
SDK的安裝
go get github.com/docker/docker/client
安裝成功之后將$GOPATH/src/github.com/docker/docker下的vendor中的文件拷貝到$GOPATH/src下,然后刪除vendor文件
注:如果不進(jìn)行上述操作凝果,會(huì)有包沖突問(wèn)題祝迂,比如import包github.com/docker/go-connections/nat時(shí),程序優(yōu)先找到的是github.com/docker/docker/vendor/下的github.com/docker/go-connections/nat包器净,而不是$GOPATH/src/github.com/docker/go-connections/nat包型雳,所以會(huì)有包沖突
實(shí)現(xiàn)
注:最終目的是啟動(dòng)docker容器之后還要運(yùn)行其中的ginDocker服務(wù),本篇程序?qū)崿F(xiàn)的 部分功能 和如下的docker命令的效果一樣
docker run -it --name mygin-latest -p 7070:7070 -v /home/youngblood/Go/src/ginDocker:/go/src/ginDocker -w /go/src/ginDocker my-gin
1.檢查本地是否有我們需求的image
這里有很多方法可以實(shí)現(xiàn)這個(gè)山害,就像運(yùn)行docker pull命令時(shí)一樣纠俭,docker首先會(huì)檢查本地是否有該image,如果沒(méi)有才去docker hub 拉取這個(gè)image浪慌,所以這里我們直接使用代碼拉取鏡像即可冤荆,類似于這樣(但是該篇示例程序中并沒(méi)有寫拉取鏡像的代碼,因?yàn)樵撶R像是本地自己創(chuàng)建的一個(gè)鏡像权纤,和Docker中g(shù)o web項(xiàng)目部署中的鏡像是一樣的)
rc, err := cli.ImagePull(ctx, "busybox", types.ImagePullOptions{})
if err != nil {
panic(err)
}
defer rc.Close()
2.檢查docker container中是否已存在目標(biāo)容器
當(dāng)創(chuàng)建一個(gè)container時(shí)钓简,顯示的給函數(shù)傳遞一個(gè)container name,那么之后我們?cè)俅芜\(yùn)行這個(gè)程序時(shí)同樣會(huì)創(chuàng)建同名的container汹想。但是外邓,docker中不允許存在同名的container,所以會(huì)創(chuàng)建失敗古掏,這樣就可以在創(chuàng)建container時(shí)確認(rèn)該container是否存在损话,代碼如下
imageName := "my-gin:latest"
cont, err := cli.ContainerCreate(ctx, &container.Config{
Image: imageName, //Docker基于該鏡像創(chuàng)建容器
Tty: true, //docker run 命令的-t
OpenStdin: true, //docker run命令的-i
Cmd: []string{"./ginDocker2"},//docker容器中執(zhí)行的命令
WorkingDir: "/go/src/ginDocker2", //docker容器工作目錄
ExposedPorts: nat.PortSet{ //docker容器對(duì)外開放的端口
"7070": struct{}{},
},
}, &container.HostConfig{
PortBindings: nat.PortMap{
"7070": []nat.PortBinding{nat.PortBinding{//docker容器映射到宿主機(jī)的端口
HostIP: "0.0.0.0",
HostPort: "7070",
}},
},
Mounts: []mount.Mount{//docker容器卷掛載
mount.Mount{
Type: mount.TypeBind,
Source: "/home/youngblood/Go/src/ginDocker2",
Target: "/go/src/ginDocker2",
},
},
}, nil, "mygin-latest")
關(guān)于上述代碼作如下簡(jiǎn)述:
- &container.Config中的Tty和OpenStdin是-it標(biāo)識(shí),WorkingDir是-w標(biāo)識(shí)槽唾,ExposePorts是容器對(duì)外開放的端口丧枪。
- &container.HostConfig中PortMap表示端口映射,是-p標(biāo)識(shí)夏漱,注意這里必須和ExposedPorts配對(duì)使用豪诲,也就是說(shuō)容器開放了哪個(gè)端口,哪個(gè)端口才能映射到宿主機(jī)上挂绰,否則即使能映射成功屎篱,由于該端口容器未開放服赎,也不能訪問(wèn)服務(wù);Mounts是-v標(biāo)識(shí)交播,其中的Type有4種重虑,分別是TypeBind="bind",TypeVolume="volume"秦士,TypeTmpfs="tmpfs"缺厉,TypeNamedPipe="npipe",其中bind表示掛在到host dir隧土,所以這里選擇使用TypeBind提针。
- nil表示的是*net.NetWorkingConfig,由于此處沒(méi)有配置曹傀,所以使用nil
- "mygin-latest"表示容器的name
3. 啟動(dòng)該容器并按時(shí)檢查該container的狀態(tài)
啟動(dòng)容器
//啟動(dòng)容器
if err = cli.ContainerStart(ctx, containerID, types.ContainerStartOptions{}); err != nil {
panic(err)
}
獲取容器內(nèi)部的運(yùn)行狀態(tài)
status, err = cli.ContainerStats(ctx, id, true)
if err != nil {
panic(err)
}
io.Copy(os.Stdout, status.Body)
將status.Body輸出到標(biāo)準(zhǔn)輸出中辐脖,你會(huì)看到控制臺(tái)不斷的輸出容器的狀態(tài)參數(shù)等,你可以根據(jù)status.Body獲取你關(guān)心的一些參數(shù)
4.若該container已崩潰皆愉,那么該程序能自動(dòng)重啟container
下列代碼能獲取到正在運(yùn)行的container嗜价。利用container的name屬性來(lái)判斷該container是否是在運(yùn)行。
//獲取正在運(yùn)行的container list
containerList, err := cli.ContainerList(ctx, types.ContainerListOptions{})
if err != nil {
panic(err)
}
var contTemp types.Container
//找出名為“mygin-latest”的container并將其存入contTemp中
for _, v1 := range containerList {
log.Println("name=", v1.ID)
for _, v2 := range v1.Names {
if v2 == "/mygin-latest" {
contTemp = v1//若contTemp為空幕庐,則該容器未運(yùn)行久锥;反之,正在運(yùn)行
break
}
}
}
綜合
目前异剥,每一步最基本的做法我們已經(jīng)實(shí)現(xiàn)并貼出了代碼瑟由,接下來(lái)的工作就是將這個(gè)工作整合到一起,做一個(gè)簡(jiǎn)單的封裝并做好流程調(diào)度即可届吁。
ginDocker2
是我們要在docker容器中發(fā)布的一個(gè)Go項(xiàng)目
代碼如下
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/hello/:name", func(c *gin.Context) {
name := c.Param("name")
c.String(http.StatusOK, "hello %s", name)
})
router.Run(":7070")
}
注:由于在下面的這個(gè)程序中創(chuàng)建容器時(shí)沒(méi)辦法一次執(zhí)行多個(gè)cmd命令错妖,所以這里的ginDocker2是先在外面的終端執(zhí)行g(shù)o build ginDocker2,在目錄ginDocker2下生成一個(gè)可執(zhí)行的二進(jìn)制文件ginDocker2
守護(hù)程序containerDeamon
聲明
- 該程序相當(dāng)于執(zhí)行命令:docker run -it --name mygin-latest -p 7070:7070 -v /home/youngblood/Go/src/ginDocker:/go/src/ginDocker -w /go/src/ginDocker my-gin
- 該程序會(huì)檢測(cè)名為mygin-latest的容器是否存在疚沐,并檢查該容器是否在運(yùn)行暂氯,若沒(méi)有,則啟動(dòng)容器并運(yùn)行其中的程序
代碼
package main
import (
"io"
"log"
"os"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/client"
"github.com/docker/go-connections/nat"
"golang.org/x/net/context"
)
const (
imageName string = "my-gin:latest" //鏡像名稱
containerName string = "mygin-latest" //容器名稱
indexName string = "/" + containerName //容器索引名稱亮蛔,用于檢查該容器是否存在是使用
cmd string = "./ginDocker2" //運(yùn)行的cmd命令痴施,用于啟動(dòng)container中的程序
workDir string = "/go/src/ginDocker2" //container工作目錄
openPort nat.Port = "7070" //container開放端口
hostPort string = "7070" //container映射到宿主機(jī)的端口
containerDir string = "/go/src/ginDocker2" //容器掛在目錄
hostDir string = "/home/youngblood/Go/src/ginDocker2" //容器掛在到宿主機(jī)的目錄
n int = 5 //每5s檢查一個(gè)容器是否在運(yùn)行
)
func main() {
ctx := context.Background()
cli, err := client.NewEnvClient()
defer cli.Close()
if err != nil {
panic(err)
}
checkAndStartContainer(ctx, cli)
}
//創(chuàng)建容器
func createContainer(ctx context.Context, cli *client.Client) {
//創(chuàng)建容器
cont, err := cli.ContainerCreate(ctx, &container.Config{
Image: imageName, //鏡像名稱
Tty: true, //docker run命令中的-t選項(xiàng)
OpenStdin: true, //docker run命令中的-i選項(xiàng)
Cmd: []string{cmd}, //docker 容器中執(zhí)行的命令
WorkingDir: workDir, //docker容器中的工作目錄
ExposedPorts: nat.PortSet{
openPort: struct{}{}, //docker容器對(duì)外開放的端口
},
}, &container.HostConfig{
PortBindings: nat.PortMap{
openPort: []nat.PortBinding{nat.PortBinding{
HostIP: "0.0.0.0", //docker容器映射的宿主機(jī)的ip
HostPort: hostPort, //docker 容器映射到宿主機(jī)的端口
}},
},
Mounts: []mount.Mount{ //docker 容器目錄掛在到宿主機(jī)目錄
mount.Mount{
Type: mount.TypeBind,
Source: hostDir,
Target: containerDir,
},
},
}, nil, containerName)
if err == nil {
log.Printf("success create container:%s\n", cont.ID)
} else {
log.Println("failed to create container!!!!!!!!!!!!!")
}
}
//啟動(dòng)容器
func startContainer(ctx context.Context, containerID string, cli *client.Client) error {
err := cli.ContainerStart(ctx, containerID, types.ContainerStartOptions{})
if err == nil {
log.Printf("success start container:%s\n", containerID)
} else {
log.Printf("failed to start container:%s!!!!!!!!!!!!!\n", containerID)
}
return err
}
//將容器的標(biāo)準(zhǔn)輸出輸出到控制臺(tái)中
func printConsole(ctx context.Context, cli *client.Client, id string) {
//將容器的標(biāo)準(zhǔn)輸出顯示出來(lái)
out, err := cli.ContainerLogs(ctx, id, types.ContainerLogsOptions{ShowStdout: true})
if err != nil {
panic(err)
}
io.Copy(os.Stdout, out)
//容器內(nèi)部的運(yùn)行狀態(tài)
status, err := cli.ContainerStats(ctx, id, true)
if err != nil {
panic(err)
}
io.Copy(os.Stdout, status.Body)
}
//檢查容器是否存在并啟動(dòng)容器
func checkAndStartContainer(ctx context.Context, cli *client.Client) {
for {
select {
case <-isRuning(ctx, cli):
//該container沒(méi)有在運(yùn)行
//獲取所有的container查看該container是否存在
contTemp := getContainer(ctx, cli, true)
if contTemp.ID == "" {
//該容器不存在,創(chuàng)建該容器
log.Printf("the container name[%s] is not exists!!!!!!!!!!!!!\n", containerName)
createContainer(ctx, cli)
} else {
//該容器存在究流,啟動(dòng)該容器
log.Printf("the container name[%s] is exists\n", containerName)
startContainer(ctx, contTemp.ID, cli)
}
}
}
}
//獲取container
func getContainer(ctx context.Context, cli *client.Client, all bool) types.Container {
containerList, err := cli.ContainerList(ctx, types.ContainerListOptions{All: all})
if err != nil {
panic(err)
}
var contTemp types.Container
//找出名為“mygin-latest”的container并將其存入contTemp中
for _, v1 := range containerList {
for _, v2 := range v1.Names {
if v2 == indexName {
contTemp = v1
break
}
}
}
return contTemp
}
//容器是否正在運(yùn)行
func isRuning(ctx context.Context, cli *client.Client) <-chan bool {
isRun := make(chan bool)
var timer *time.Ticker
go func(ctx context.Context, cli *client.Client) {
for {
//每n s檢查一次容器是否運(yùn)行
timer = time.NewTicker(time.Duration(n) * time.Second)
select {
case <-timer.C:
//獲取正在運(yùn)行的container list
log.Printf("%s is checking the container[%s]is Runing??", os.Args[0], containerName)
contTemp := getContainer(ctx, cli, false)
if contTemp.ID == "" {
log.Print(":NO")
//說(shuō)明container沒(méi)有運(yùn)行
isRun <- true
} else {
log.Print(":YES")
//說(shuō)明該container正在運(yùn)行
go printConsole(ctx, cli, contTemp.ID)
}
}
}
}(ctx, cli)
return isRun
}
說(shuō)明:
- const中的變量按自己的需求定制
- 該程序名稱叫做containerDeamon辣吃,運(yùn)行時(shí)前面加上sudo,否則會(huì)提示權(quán)限不夠
- 運(yùn)行成功之后控制臺(tái)會(huì)打印很多日志芬探,此時(shí)可以注釋掉isRuning函數(shù)中的 go printConsole()函數(shù)重新編譯運(yùn)行神得,此時(shí)的日志更方便于閱讀
現(xiàn)在沒(méi)有名為mygin-latest的docker容器,啟動(dòng)containerDeamon守護(hù)進(jìn)程之后看看控制臺(tái)打印了什么
2017/11/18 00:41:16 ./containerDeamon is checking the container[mygin-latest]is Runing??
2017/11/18 00:41:16 :NO
2017/11/18 00:41:16 the container name[mygin-latest] is not exists!!!!!!!!!!!!!
2017/11/18 00:41:16 success create container:e1d91ca1cc2adf84675c9bb90854e2ce709617088a6f7090127090ad4230fcf8
2017/11/18 00:41:21 ./containerDeamon is checking the container[mygin-latest]is Runing??
2017/11/18 00:41:21 :NO
2017/11/18 00:41:21 ./containerDeamon is checking the container[mygin-latest]is Runing??
2017/11/18 00:41:21 :NO
2017/11/18 00:41:21 the container name[mygin-latest] is exists
2017/11/18 00:41:22 success start container:e1d91ca1cc2adf84675c9bb90854e2ce709617088a6f7090127090ad4230fcf8
2017/11/18 00:41:26 ./containerDeamon is checking the container[mygin-latest]is Runing??
2017/11/18 00:41:26 :YES
2017/11/18 00:41:27 ./containerDeamon is checking the container[mygin-latest]is Runing??
2017/11/18 00:41:27 :YES
此時(shí)用命令sudo docker ps查看正在運(yùn)行的docker容器
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e1d91ca1cc2a my-gin:latest "./ginDocker2" 18 seconds ago Up 12 seconds 0.0.0.0:7070->7070/tcp mygin-latest
用docker stop e1d91ca1cc2a停掉該容器之后會(huì)在containerDeamon的控制臺(tái)看到如下輸出
2017/11/18 00:41:41 ./containerDeamon is checking the container[mygin-latest]is Runing??
2017/11/18 00:41:41 :YES
2017/11/18 00:41:42 ./containerDeamon is checking the container[mygin-latest]is Runing??
2017/11/18 00:41:42 :NO
2017/11/18 00:41:42 the container name[mygin-latest] is exists
2017/11/18 00:41:43 success start container:e1d91ca1cc2adf84675c9bb90854e2ce709617088a6f7090127090ad4230fcf8
2017/11/18 00:41:46 ./containerDeamon is checking the container[mygin-latest]is Runing??
2017/11/18 00:41:46 :YES
2017/11/18 00:41:47 ./containerDeamon is checking the container[mygin-latest]is Runing??
2017/11/18 00:41:47 :YES
2017/11/18 00:41:48 ./containerDeamon is checking the container[mygin-latest]is Runing??
2017/11/18 00:41:48 :YES
此時(shí)再用命令sudo docker ps查看正在運(yùn)行的docker容器偷仿,發(fā)現(xiàn)該容器已被啟動(dòng)哩簿。
最后宵蕉,讓我們?cè)跒g覽器訪問(wèn)localhost:7070/hello/初級(jí)賽亞人看看容器的程序啟用的端口是否成功映射到了宿主機(jī)的7070端口
截圖
至此,一個(gè)docker container的守護(hù)程序就完成了节榜,如果你對(duì)上面的代碼有任何疑問(wèn)歡迎提問(wèn)羡玛,覺得不好的地方請(qǐng)斧正。
關(guān)注喜歡隨便點(diǎn)宗苍,看看也行——支持是對(duì)我的最大鼓勵(lì)(初級(jí)賽亞人)稼稿。
——待更——
最近對(duì)這個(gè)守護(hù)程序,忽然發(fā)現(xiàn)有一種情況被忽略了讳窟,就是在檢查容器是否在運(yùn)行時(shí)让歼,如果容器已在運(yùn)行,而容器中的go程序并沒(méi)有在運(yùn)行挪钓,所以會(huì)導(dǎo)致雖然容器在運(yùn)行是越,但是我們的服務(wù)并沒(méi)有發(fā)發(fā)布,之后會(huì)抽個(gè)時(shí)間將這種情況補(bǔ)上碌上。