/ docker

Go + Docker API服务开发和部署 - 部署篇

本文是 Go + Docker API服务开发和部署 文章系列的“部署篇”,主要讲解如何使用Docker来搭建开发和测试环境,以及部署应用到生产环境。

  1. Docker简介

Docker简介

相信很多开发和测试人员都遇到过搭建环境的问题。新的开发人员加入团队需要花费一两天甚至更长时间来搭建一套完整的开发环境。测试人员搭建测试环境同样如此,并且如果想再多搭一套来做压力测试,又得重复一次,非常的枯燥和无趣。有没有可能使用一个标准的环境搭建脚本就能把应用及其依赖服务都Run起来?如果使用传统的脚本编写方式,很困难。每个人的工作环境千差万别,比如有的用Windows有的用Mac,脚本很难做到一次编写到处运行。并且编写脚本的人需要知晓所有应用和服务的部署细节,工作量很大。如果只有一台服务器,还要想法应对多套环境的端口、路径等系统资源冲突的问题。

Docker通过将应用及其依赖的系统环境一起打包,使得部署应用就跟执行一条命令一样简单,Docker里这叫启动一个container(容器)。结合Docker Compose提供的容器编排功能,能够一次将应用及其依赖服务全都Run起来,并且相互之间能够通过私有网络互相访问。由于容器具有资源隔离性,所以可以在一台主机(Docker Host,容器运行所在的服务器)上启动多个应用(image)的运行实例(container)。从此,搭建环境不再是问题。

关于Docker的安装,之前在Mac和Windows下只能通过在本地启动一个Linux虚拟机,然后在该虚拟机里安装Docker,运行容器。虚拟机会额外消耗资源不说,也有一些使用限制,比如挂载挂载本地目录到容器。现在Docker已经推出了原生的Mac for Mac和Docker for Windows,目前处于Release Candidate阶段,已经比较稳定。详细安装可以参考官方文档,Docke for MacDocke for Windows

使用Docker来打包应用镜像

docker build 命令依照Dockerfile里的指示来构建镜像,因此我们首先需要编写Dockerfile。

Dockerfile:

# 从我们自己的Go语言镜像开始,也可选择官方镜像仓库里的Go语言镜像
FROM daocloud.io/jaggerwang/go

# 添加当前目录(应用根目录)内容,并设置工作目录
ADD . /go/src/zaiqiuchang.com/server
WORKDIR /go/src/zaiqiuchang.com/server

# 编译应用,内容详见后面
RUN ./build.sh

# 创建一个存放运行时数据的磁盘卷
VOLUME /data/zaiqiuchang/server

# 对外暴露应用服务监听端口
EXPOSE 1323

# 通过Supervisor来启动应用服务
CMD supervisord

build.sh:

#!/usr/bin/env bash

source ./common.sh

log INFO "build begin ..."
go install -v . && mv $GOPATH/bin/server $GOPATH/bin/zqc
if [[ $? != 0 ]]; then
  log ERROR "build failed"
  exit 1
fi
log INFO "build ok"

exit 0

打包出来的镜像中有两个重要目录,一个是存放代码的目录,另一个是存放运行时数据的目录。代码目录在部署开发环境时会挂载本机目录去覆盖容器中的目录,这样在本机上编写的代码在容器里实时可见,而不用每次修改后去重新打包部署。再结合“开发篇”里讲到的 使用Fswatch来自动部署代码更新 ,使得调试效率跟在本机开发一样高效。

有了Dockerfile,就可以构建镜像了。

docker-build.sh

#!/usr/bin/env bash

source ./common.sh

log INFO "docker build begin ..."
docker build -t daocloud.io/jaggerwang/zqc-server .
if [[ $? != 0 ]]; then
  log ERROR "docker build failed"
  exit 1
fi
log INFO "docker build ok"

exit 0

其中 -t 参数表示镜像访问路径,Docker中的镜像采用类似Git那样的版本管理方式。最前面的“daocloud.io”表示镜像仓库服务地址,开发者可以自由选择访问速度最快的服务商,甚至可以自己搭建这个服务。如果是Docker官方的服务则没有这部分路径。我们选择的是国内的一家 DaoCloud ,为避免广告嫌疑这里就不多做介绍,国内笔者知道的还有 灵雀云 ,也提供容器云服务。

镜像生成后,使用 docker push daocloud.io/jaggerwang/zqc-server 来推送镜像到远程仓库,需要的时候使用 docker pull daocloud.io/jaggerwang/zqc-server 来下载镜像。

对于生产环境镜像打包,可以使用一个可选的build脚本。除了增加push镜像的操作,如果生产环境镜像跟开发环境镜像有差别,可以通过传递不同参数给 docker build 命令来控制。

docker-build-prod.sh

#!/usr/bin/env bash

source ./common.sh

log INFO "docker build begin ..."
docker build -t daocloud.io/jaggerwang/zqc-server .
if [[ $? != 0 ]]; then
  log ERROR "docker build failed"
  exit 1
fi
log INFO "docker build ok"

log INFO "push image begin ..."
docker push daocloud.io/jaggerwang/zqc-server
log INFO "push image end"

exit 0

使用Docker Compose来部署应用及其依赖服务

要使得应用服务正常运行起来,除了需要部署应用服务本身,还需要部署应用服务依赖的其它服务,比如数据库服务、缓存服务、消息队列服务等,并且还要保障应用服务能够私密的访问这些服务。当然可以采用手动的方式执行 docker run 命令来一个一个启动这些服务,并创建一个私有的网络来使得相互之间连通。但这样未免太过繁琐,Docker Compose 就是为了解决这个问题而出现。类似Dockerfile文件,我们也需要编写“docker-compose”文件来告诉Docker Compose如何部署所有服务。

docker-compose.yml:

version: "2"
services:
  server:
    image: daocloud.io/jaggerwang/zqc-server
    environment:
      ZQC_SERVER_DEBUG: "false"
      ZQC_LOG_LEVEL: info
    volumes:
      - server:/data/zaiqiuchang/server
    depends_on:
      - mongodb
  mongodb:
    image: daocloud.io/jaggerwang/mongodb
    volumes:
      - mongodb:/data/zaiqiuchang/mongodb

volumes:
  server:
    driver: local
  mongodb:
    driver: local

我们启动了两个服务,一个是应用服务“server”,另一个是数据库服务“mongodb”。给每个服务都创建了一个本地磁盘卷,用来存放运行时数据,包括日志、数据文件等。

依据要部署到环境(开发、测试或生产)不同,在部署细节上会有一些区别。前面的“docker-compose.yml”里只包含了公共相同的部分,不同的部分需要使用独立的文件来保存。docker-compose 命令允许通过 -f 参数指定多个配置文件,默认使用当前目录下的“docker-compose.yml”和“docker-compose.override.yml”(如果存在)。“docker-compose.override.yml”我们用于开发环境部署,另外还为测试和生产环境部署分别准备了“docker-compose.test.yml”和“docker-compose.prod.yml”文件。

docker-compose.override.yml:

version: "2"
services:
  server:
    ports:
      - 1323:1323
    environment:
      ZQC_SERVER_DEBUG: "true"
      ZQC_LOG_LEVEL: debug
    volumes:
      - ./:/go/src/zaiqiuchang.com/server
      - ~/data/projects/zaiqiuchang/server:/data/zaiqiuchang/server
  mongodb:
    ports:
      - 27018:27017

docker-compose.test.yml:

version: "2"
services:
  server:
    ports:
      - 1324:1323
    environment:
      ZQC_SERVER_DEBUG: "true"
      ZQC_LOG_LEVEL: debug
    volumes:
      - ./:/go/src/zaiqiuchang.com/server
      - ~/data/projects/zaiqiuchang/server:/data/zaiqiuchang/server
  mongodb:
    ports:
      - 27019:27017

docker-compose.prod.yml:

version: "2"
services:
  server:
    ports:
      - 1323:1323
    volumes:
      - /data/zaiqiuchang/server:/data/zaiqiuchang/server
  mongodb:
    volumes:
      - /data/zaiqiuchang/mongodb:/data/zaiqiuchang/mongodb

可以看到各个环境在监听端口、是否开启Debug模式、日志打印级别、运行时数据存放磁盘卷创建方式上,因需求和限制不同而有区别。由于开发和测试环境都在本地运行,因此要避免开放端口映射冲突。

现在执行 docker-compose -p zqc up -d 命令就可以把整套开发环境启动起来了,-p 参数表示这套环境的命名空间,以区分在一台主机上启动的多套环境。如果要启动测试和生产环境,分别使用 docker-compose -p zqctest -f docker-compose.yml -f docker-compose.test.yml up -ddocker-compose -p zqc -f docker-compose.yml -f docker-compose.prod.yml up -d 命令。

最后可以把代码编译、打包镜像、部署等一系列操作通过Shell脚本串连起来,为每种环境的部署编写一个部署脚本。

deploy.sh

#!/usr/bin/env bash

source ./common.sh

./build.sh
if [[ $? != 0 ]]; then
  exit 1
fi

./docker-build.sh
if [[ $? != 0 ]]; then
  exit 1
fi

log INFO "docker compose begin ..."
docker-compose -p zqc up -d
log INFO "docker compose end"

log INFO "create db indexes begin ..."
docker-compose -p zqc exec server zqc db createindexes
log INFO "create db indexes end"

exit 0

deploy-test.sh

#!/usr/bin/env bash

source ./common.sh

./build.sh
if [[ $? != 0 ]]; then
  exit 1
fi

./docker-build.sh
if [[ $? != 0 ]]; then
  exit 1
fi

log INFO "docker compose begin ..."
docker-compose -p zqctest -f docker-compose.yml -f docker-compose.test.yml up -d
log INFO "docker compose end"

log INFO "create db indexes begin ..."
docker-compose -p zqctest exec server zqc db createindexes
log INFO "create db indexes end"

exit 0

deploy-prod.sh

#!/usr/bin/env bash

source ./common.sh

log INFO "pull image begin ..."
docker pull daocloud.io/jaggerwang/zqc-server
log INFO "pull image end"

log INFO "docker compose begin ..."
docker-compose -p zqc -f docker-compose.yml -f docker-compose.prod.yml up -d
log INFO "docker compose end"

log INFO "create db indexes begin ..."
docker-compose -p zqc exec server zqc db createindexes
log INFO "create db indexes end"

exit 0

使用Docker Machine来管理容器主机

默认docker命令操作的是本地Docker Engine(Docker容器引擎),启动的容器都是在本机上运行。如果我们要部署到生产环境主机,需要登录到远程主机,安装Docker Engine,以及Docker Compose等其它Docker Tools,然后在远程主机上执行docker命令。也可以只在远程主机上安装Docker Engine,设置本地Docker环境变量,包括DOCKER_HOST、DOCKER_MACHINE_NAME、DOCKER_CERT_PATH、DOCKER_TLS_VERIFY,之后再执行docker命令,这个时候操作的就是远程主机。后面一种方式比前面一种更方便,但如果主机数较多,需要在每一台上面安装Docker Engine,并且每次手动设置环境变量也很不方便。Docker Machine 此时能帮你分忧解难,使用它可以在本地管理一批远程主机,包括在远程主机上安装Docker Engine,执行docker命令。

添加远程主机:
docker-machine create --driver generic --generic-ip-address=zqc-app1 --generic-ssh-key ~/.ssh/id_rsa --generic-ssh-user worker zqc-app1
address为远程主机地址,可以是IP或域名,key为登录远程主机的SSH密钥,user为登录远程主机的用户身份,最后一个参数为在Docker Machine里显示的主机名。
添加主机时如果发现远程主机上没有安装Docker Engine,将自动安装。

查看已添加的主机:

➜  ~ docker-machine ls
NAME       ACTIVE   DRIVER    STATE     URL                   SWARM   DOCKER        ERRORS
zqc-app1   -        generic   Running   tcp://zqc-app1:2376           v1.12.0-rc3

查看和设置操作远程主机的环境变量:

➜  ~ docker-machine env zqc-app1
export DOCKER_TLS_VERIFY="1"
export DOCKER_HOST="tcp://zqc-app1:2376"
export DOCKER_CERT_PATH="/Users/jagger/.docker/machine/machines/zqc-app1"
export DOCKER_MACHINE_NAME="zqc-app1"
# Run this command to configure your shell:
# eval $(docker-machine env zqc-app1)
➜  ~ eval $(docker-machine env zqc-app1)
➜  ~ env | grep DOCKER
DOCKER_TLS_VERIFY=1
DOCKER_HOST=tcp://zqc-app1:2376
DOCKER_CERT_PATH=/Users/jagger/.docker/machine/machines/zqc-app1
DOCKER_MACHINE_NAME=zqc-app1

现在再执行docker命令,操作的就是远程主机上的Docker Engine了。如果要操作本地,删除前面设置的环境变量即可。建议单独新开一个终端窗口来操作远程主机,以免跟本地环境混淆。

使用Docker来管理测试环境

本系列的 测试篇 中我们提到了使用Docker来准备“Model测试”和“API测试”需要的环境。当时着重讲解了测试用例编写,现在来看看如何在Test Main里启动和销毁测试环境。

使用Docker API来创建和销毁提供MongoDB服务的容器:

// test/common.go

package test

import (
  "errors"
  "fmt"
  "time"

  "github.com/fsouza/go-dockerclient"
)

// 启动MongoDB服务,尝试 tries 次,每次间隔 delay,成功返回服务地址
func StartMongodb(tries int, delay time.Duration) (addrs string, ctn *docker.Container, err error) {
  // 创建Docker client对象
  client, err := docker.NewClient("unix:///var/run/docker.sock")
  if err != nil {
    return addrs, ctn, err
  }

  // 使用mongodb镜像来创建一个容器
  ctn, err = client.CreateContainer(docker.CreateContainerOptions{
    Config: &docker.Config{
      Image: "daocloud.io/jaggerwang/mongodb",
    },
    HostConfig: &docker.HostConfig{
      PublishAllPorts: true,
    },
  })
  if err != nil {
    return addrs, ctn, err
  }
  // 启动前面创建的容器,注意新创建的容器默认不会启动
  err = client.StartContainer(ctn.ID, &docker.HostConfig{})
  if err != nil {
    return addrs, ctn, err
  }

  // 轮询尝试获取MongoDB服务在本地的随机映射端口,如果获取到则表明容器已处于正常运行状态
  for i := 0; i < tries; i++ {
    ctn, err = client.InspectContainer(ctn.ID)
    if err != nil {
      return addrs, ctn, err
    }
    portBinding, ok := ctn.NetworkSettings.Ports["27017/tcp"]
    if !ok {
      time.Sleep(delay)
      continue
    }
    addrs = fmt.Sprintf("%v:%v", portBinding[0].HostIP, portBinding[0].HostPort)
    return addrs, ctn, nil
  }
  return addrs, ctn, errors.New("start mongodb failed")
}

// 销毁容器
func RemoveMongodb(ctn *docker.Container) (err error) {
  client, err := docker.NewClient("unix:///var/run/docker.sock")
  if err != nil {
    return err
  }
  return client.RemoveContainer(docker.RemoveContainerOptions{
    ID:            ctn.ID,
    RemoveVolumes: true,
    Force:         true,
  })
}

至于API测试依赖的集成测试环境,出于效率考虑,不是每次运行测试都重新创建,而是预先手动创建好,运行测试时只需清空上次测试遗留下来的测试数据即可。创建集成测试环境可参考 使用Docker Compose来部署应用及其依赖服务