介紹
docker的sdk的官方介紹的樣例有go和Python的,并包含了如下對docker二次開發的幾種簡單的實現
- 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
具體代碼請移步上述鏈接。
這篇主要講講怎樣用go對docker進行簡單的二次開發:一個docker容器的守護程序
kubernetes就是利用go對docker進行二次開發以管理成千上萬的docker容器的成功案例
kubernetes部署需要踩很多坑,但是有時我們只需要對docker進行一次簡單的二次開發以滿足業務的需求,如新上線一個版本,我們需要在docker容器中部署,此時就可以對docker進行二次開發以滿足我們的需求。
需求一
- 從git或svn上拉取最新的代碼,并將其編譯成go的二進制可運行文件
- 從docker倉庫中拉需要的鏡像
- 在鏡像的基礎上創建容器,包括配置容器的一些參數等等
- 啟動容器
這樣當我們需要發布一個項目的新版本時直接運行這個程序就能做到一鍵發布。一個容器運行時,就像一個操作系統運行一樣,也有崩潰的時候,此時我們需要一個監聽docker容器的健康狀況來以防一些意外
需求二
- 監聽docker容器運行時的相關參數
- 針對獲取到的參數做出相應的處理,如mem使用打到80%時發送郵件通知小組的開發人員
- 在docker容器崩潰時能重新啟動該容器
假設需求
現在我就上面介紹的兩個需求簡單綜合一下,以完成一個自己的需求
- 假設本地已有我們需要的docker image
- 檢查docker container中是否已存在目標容器
- 若有,則跳轉到第5步
- 若沒有,創建一個從container
- 啟動該容器并按時檢查該container的狀態
- 若該container已崩潰,那么該程序能自動重啟container
附:我們所期望的container內部還掛在了一個宿主機的目錄
以上就是本篇文章將要實現的功能
正篇
SDK的安裝
go get github.com/docker/docker/client
安裝成功之后將$GOPATH/src/github.com/docker/docker下的vendor中的文件拷貝到$GOPATH/src下,然后刪除vendor文件
注:如果不進行上述操作,會有包沖突問題,比如import包github.com/docker/go-connections/nat時,程序優先找到的是github.com/docker/docker/vendor/下的github.com/docker/go-connections/nat包,而不是$GOPATH/src/github.com/docker/go-connections/nat包,所以會有包沖突
實現
注:最終目的是啟動docker容器之后還要運行其中的ginDocker服務,本篇程序實現的 部分功能 和如下的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
這里有很多方法可以實現這個,就像運行docker pull命令時一樣,docker首先會檢查本地是否有該image,如果沒有才去docker hub 拉取這個image,所以這里我們直接使用代碼拉取鏡像即可,類似于這樣(但是該篇示例程序中并沒有寫拉取鏡像的代碼,因為該鏡像是本地自己創建的一個鏡像,和Docker中go web項目部署中的鏡像是一樣的)
rc, err := cli.ImagePull(ctx, "busybox", types.ImagePullOptions{})
if err != nil {
panic(err)
}
defer rc.Close()
2.檢查docker container中是否已存在目標容器
當創建一個container時,顯示的給函數傳遞一個container name,那么之后我們再次運行這個程序時同樣會創建同名的container。但是,docker中不允許存在同名的container,所以會創建失敗,這樣就可以在創建container時確認該container是否存在,代碼如下
imageName := "my-gin:latest"
cont, err := cli.ContainerCreate(ctx, &container.Config{
Image: imageName, //Docker基于該鏡像創建容器
Tty: true, //docker run 命令的-t
OpenStdin: true, //docker run命令的-i
Cmd: []string{"./ginDocker2"},//docker容器中執行的命令
WorkingDir: "/go/src/ginDocker2", //docker容器工作目錄
ExposedPorts: nat.PortSet{ //docker容器對外開放的端口
"7070": struct{}{},
},
}, &container.HostConfig{
PortBindings: nat.PortMap{
"7070": []nat.PortBinding{nat.PortBinding{//docker容器映射到宿主機的端口
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")
關于上述代碼作如下簡述:
- &container.Config中的Tty和OpenStdin是-it標識,WorkingDir是-w標識,ExposePorts是容器對外開放的端口。
- &container.HostConfig中PortMap表示端口映射,是-p標識,注意這里必須和ExposedPorts配對使用,也就是說容器開放了哪個端口,哪個端口才能映射到宿主機上,否則即使能映射成功,由于該端口容器未開放,也不能訪問服務;Mounts是-v標識,其中的Type有4種,分別是TypeBind="bind",TypeVolume="volume",TypeTmpfs="tmpfs",TypeNamedPipe="npipe",其中bind表示掛在到host dir,所以這里選擇使用TypeBind。
- nil表示的是*net.NetWorkingConfig,由于此處沒有配置,所以使用nil
- "mygin-latest"表示容器的name
3. 啟動該容器并按時檢查該container的狀態
啟動容器
//啟動容器
if err = cli.ContainerStart(ctx, containerID, types.ContainerStartOptions{}); err != nil {
panic(err)
}
獲取容器內部的運行狀態
status, err = cli.ContainerStats(ctx, id, true)
if err != nil {
panic(err)
}
io.Copy(os.Stdout, status.Body)
將status.Body輸出到標準輸出中,你會看到控制臺不斷的輸出容器的狀態參數等,你可以根據status.Body獲取你關心的一些參數
4.若該container已崩潰,那么該程序能自動重啟container
下列代碼能獲取到正在運行的container。利用container的name屬性來判斷該container是否是在運行。
//獲取正在運行的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為空,則該容器未運行;反之,正在運行
break
}
}
}
綜合
目前,每一步最基本的做法我們已經實現并貼出了代碼,接下來的工作就是將這個工作整合到一起,做一個簡單的封裝并做好流程調度即可。
ginDocker2
是我們要在docker容器中發布的一個Go項目
代碼如下
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")
}
注:由于在下面的這個程序中創建容器時沒辦法一次執行多個cmd命令,所以這里的ginDocker2是先在外面的終端執行go build ginDocker2,在目錄ginDocker2下生成一個可執行的二進制文件ginDocker2
守護程序containerDeamon
聲明
- 該程序相當于執行命令:docker run -it --name mygin-latest -p 7070:7070 -v /home/youngblood/Go/src/ginDocker:/go/src/ginDocker -w /go/src/ginDocker my-gin
- 該程序會檢測名為mygin-latest的容器是否存在,并檢查該容器是否在運行,若沒有,則啟動容器并運行其中的程序
代碼
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" //運行的cmd命令,用于啟動container中的程序
workDir string = "/go/src/ginDocker2" //container工作目錄
openPort nat.Port = "7070" //container開放端口
hostPort string = "7070" //container映射到宿主機的端口
containerDir string = "/go/src/ginDocker2" //容器掛在目錄
hostDir string = "/home/youngblood/Go/src/ginDocker2" //容器掛在到宿主機的目錄
n int = 5 //每5s檢查一個容器是否在運行
)
func main() {
ctx := context.Background()
cli, err := client.NewEnvClient()
defer cli.Close()
if err != nil {
panic(err)
}
checkAndStartContainer(ctx, cli)
}
//創建容器
func createContainer(ctx context.Context, cli *client.Client) {
//創建容器
cont, err := cli.ContainerCreate(ctx, &container.Config{
Image: imageName, //鏡像名稱
Tty: true, //docker run命令中的-t選項
OpenStdin: true, //docker run命令中的-i選項
Cmd: []string{cmd}, //docker 容器中執行的命令
WorkingDir: workDir, //docker容器中的工作目錄
ExposedPorts: nat.PortSet{
openPort: struct{}{}, //docker容器對外開放的端口
},
}, &container.HostConfig{
PortBindings: nat.PortMap{
openPort: []nat.PortBinding{nat.PortBinding{
HostIP: "0.0.0.0", //docker容器映射的宿主機的ip
HostPort: hostPort, //docker 容器映射到宿主機的端口
}},
},
Mounts: []mount.Mount{ //docker 容器目錄掛在到宿主機目錄
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!!!!!!!!!!!!!")
}
}
//啟動容器
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
}
//將容器的標準輸出輸出到控制臺中
func printConsole(ctx context.Context, cli *client.Client, id string) {
//將容器的標準輸出顯示出來
out, err := cli.ContainerLogs(ctx, id, types.ContainerLogsOptions{ShowStdout: true})
if err != nil {
panic(err)
}
io.Copy(os.Stdout, out)
//容器內部的運行狀態
status, err := cli.ContainerStats(ctx, id, true)
if err != nil {
panic(err)
}
io.Copy(os.Stdout, status.Body)
}
//檢查容器是否存在并啟動容器
func checkAndStartContainer(ctx context.Context, cli *client.Client) {
for {
select {
case <-isRuning(ctx, cli):
//該container沒有在運行
//獲取所有的container查看該container是否存在
contTemp := getContainer(ctx, cli, true)
if contTemp.ID == "" {
//該容器不存在,創建該容器
log.Printf("the container name[%s] is not exists!!!!!!!!!!!!!\n", containerName)
createContainer(ctx, cli)
} else {
//該容器存在,啟動該容器
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
}
//容器是否正在運行
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檢查一次容器是否運行
timer = time.NewTicker(time.Duration(n) * time.Second)
select {
case <-timer.C:
//獲取正在運行的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")
//說明container沒有運行
isRun <- true
} else {
log.Print(":YES")
//說明該container正在運行
go printConsole(ctx, cli, contTemp.ID)
}
}
}
}(ctx, cli)
return isRun
}
說明:
- const中的變量按自己的需求定制
- 該程序名稱叫做containerDeamon,運行時前面加上sudo,否則會提示權限不夠
- 運行成功之后控制臺會打印很多日志,此時可以注釋掉isRuning函數中的 go printConsole()函數重新編譯運行,此時的日志更方便于閱讀
現在沒有名為mygin-latest的docker容器,啟動containerDeamon守護進程之后看看控制臺打印了什么
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
此時用命令sudo docker ps查看正在運行的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停掉該容器之后會在containerDeamon的控制臺看到如下輸出
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
此時再用命令sudo docker ps查看正在運行的docker容器,發現該容器已被啟動。
最后,讓我們在瀏覽器訪問localhost:7070/hello/初級賽亞人看看容器的程序啟用的端口是否成功映射到了宿主機的7070端口
截圖
至此,一個docker container的守護程序就完成了,如果你對上面的代碼有任何疑問歡迎提問,覺得不好的地方請斧正。
關注喜歡隨便點,看看也行——支持是對我的最大鼓勵(初級賽亞人)。
——待更——
最近對這個守護程序,忽然發現有一種情況被忽略了,就是在檢查容器是否在運行時,如果容器已在運行,而容器中的go程序并沒有在運行,所以會導致雖然容器在運行,但是我們的服務并沒有發發布,之后會抽個時間將這種情況補上。