熟悉Docker如何提升你在构建、测试并部署Go Web应用程序的方式,并且理解如何使用Semaphore来持续部署。
简介
大多数情况下Go应用程序被编译成单个二进制文件,web应用程序则会包括模版和配置文件。而当一个项目中有很多文件的时候,由于很多文件没有同步就会导致错误的发生并且产生很多的问题。
在本教程中,你将学习如何使用Docker部署一个Go web应用程序,并且认识到Docker将如何改进你的开发工作流及部署流程。各种规模的团队都会从这里所介绍的设置中受益。
目标
在本文结束后,你将:
。对Docker有基本的了解,
。发现在Docker将如何帮助你开发Go应用程序
。学习如何为一个生产环境中的Go应用程序创建Docker容器
。知道如何使用Semaphore持续地在你的服务器上部署Docker容器
先决条件
为了学习本教程,你讲需要:
。在你的主机或者服务器上安装Docker
。具有一台能够使用SSH密钥对SSH请求进行认证的服务器
理解Docker
Docker帮助你为应用程序创建一个单独的可部署单元。这个单元,也被称为容器,包含该应用程序需要的所有东西。它包括代码(或者二进制文件)、runtime(运行环境)、系统工具盒系统库。将所有必需的资源打包成一个单元将确保无论应用程序部署到哪里都有完全相同的环境。这也有助于维护一个完全相同的开发和生产配置,这在以前是很难追踪的。
一旦开始,容器的创建和部署将自动完成。它消除了一大类问题。这些问题主要是由于文件没有同步或者开发和生产环境之间的差异导致的。Docker帮助解决了这些问题。
相比于虚拟机的优势
容器提供了与虚拟机相似的资源分配和隔离优势。然而,相同之处仅此而已。
一个虚拟机需要它自己的客户操作系统而容器共享主机操作系统的内核。这意味着容器更加轻量而且需要更少的资源。从本质上讲,一个虚拟机是操作系统中的一个操作系统。而另一方面的容器则更像是操作系统中的其它应用程序。基本上,容器需要的资源(内存、磁盘空间等等)比虚拟机少很多,并且具有比虚拟机快很多的启动时间。
Docker在开发阶段的优势
在开发中使用Docker的优势包括:
。一个用于所有团队成员的标准开发环境
。更新的依赖性集中化以及在任何地方都能使用相同的容器
。在开发和生产中完全相同的环境
。修复了可能只会出现在生产环境中的潜在问题
为什么使用Docker运行一个Go Web应用程序?
多数Go应用程序时简单的二进制文件。这就引发一个问题 - 为什么使用Docker运行一个Go应用程序?一些使用Docker运行Go的理由包括:
。Web应用程序通常都有模版和配置文件。Docker有助于保持这些文件与二进制文件的同步
。Docker确保了在开发或生产中完全相同的配置。很多时候当应用程序可以在开发环境中正常工作时,在生产环境去无法正常工作。使用DOcker则把你从对这些问题的担心中解放了出来。
。在一个大型的团队中主机、操作系统及所安装的软件可能存在很大的不同。Docker提供了一种机制来确保一致的开发环境配置。这将提升团队的生产力并且在开发阶段减少冲突和可避免问题的发生。
创建一个简单的Go Web应用程序
在本文中味了演示,我们会用Go创建一个简单的Web应用程序。这个我们称之为MathApp的应用程序将:
。探索不同数学运算的路径
。在视图中使用HTML模版
。使用一个可配置的文件来定制化该应用程序
。包含所选功能的测试
访问 /sum/3/6 将显示一个包含3与6相加后结果的页面。同样的,访问 /product/3/6 将显示一个3和6乘积的页面。
在本文中我们使用 Beego 框架。请注意你可以为你的应用亨旭使用任何框架(或者什么也不用)。
最终的目录结构
完成之后,MathApp的目录结构应该看起来如下:
MathApp
├── conf
│ └── app.conf
├── main.go
├── main_test.go
└── views
├── invalid-route.html
└── result.html
我们假设 MathApp 目录位于 /app 目录之中。
应用程序的主文件时 main.go ,为主应用程序的根目录中。这个文件包含该应用的所有功能。一些 main.go 中的功能是使用 main_test.go 来测试的。
views文件夹中包含视图文件 invald-route.html 和 result.html 。配置文件 app.conf 位于 conf 文件夹中。 Beego 使用该文件来定制化应用程序。
应用程序文件的内容
应用程序主文件(main.go)包含所有的应用程序逻辑。该文件的内容如下:
*// main.go*
**package** main
**import** (
"strconv"
"github.com/astaxie/beego"
)
*// The main function defines a single route, its handler*
*// and starts listening on port 8080 (default port for Beego)*
**func** main() {
*/* This would match routes like the following:*
*/sum/3/5*
*/product/6/23*
*...*
**/*
beego.Router("/:operation/:num1:int/:num2:int", &mainController{})
beego.Run()
}
*// This is the controller that this application uses*
**type** mainController **struct** {
beego.Controller
}
*// Get() handles all requests to the route defined above*
**func** (c *mainController) Get() {
*//Obtain the values of the route parameters defined in the route above*
operation := c.Ctx.Input.Param(":operation")
num1, _ := strconv.Atoi(c.Ctx.Input.Param(":num1"))
num2, _ := strconv.Atoi(c.Ctx.Input.Param(":num2"))
*//Set the values for use in the template*
c.Data["operation"] = operation
c.Data["num1"] = num1
c.Data["num2"] = num2
c.TplName = "result.html"
*// Perform the calculation depending on the 'operation' route parameter*
**switch** operation {
**case** "sum":
c.Data["result"] = add(num1, num2)
**case** "product":
c.Data["result"] = multiply(num1, num2)
**default**:
c.TplName = "invalid-route.html"
}
}
**func** add(n1, n2 int) int {
**return** n1 + n2
}
**func** multiply(n1, n2 int) int {
**return** n1 * n2
}
在你的应用程序中,它可能被分割到多个文件中。然而,针对本教程的目的,我们希望事情简单化。
测试文件的内容
main.go文件有一些需要测试的功能。对于这些功能的测试可以在main_test.go中找到。该文件的内容如下:
// main_test.go
package main
import "testing"
func TestSum(t *testing.T) {
if add(2, 5) != 7 {
t.Fail()
}
if add(2, 100) != 102 {
t.Fail()
}
if add(222, 100) != 322 {
t.Fail()
}
}
func TestProduct(t *testing.T) {
if multiply(2, 5) != 10 {
t.Fail()
}
if multiply(2, 100) != 200 {
t.Fail()
}
if multiply(222, 3) != 666 {
t.Fail()
}
}
如果你想进行持续的部署,那么对你的应用程序进行测试是特别有用的。如果你有了足够的测试,那么你可以持续地部署而不必担心在你的应用程序中出现错误。
视图文件内容
视图文件时HTML模版。应用程序使用它们来显示对请求的应答。result.html的内容如下:
<!-- result.html --><!-- This file is used to display the result of calculations --><!doctype html><html><head><title>MathApp - {{.operation}}</title></head><body>
The {{.operation}} of {{.num1}} and {{.num2}} is {{.result}}
</body></html>
invalid-route.html的内容如下:
<!-- invalid-route.html --><!-- This file is used when an invalid operation is specified in the route --><!doctype html><html><head><title>MathApp</title><meta name="viewport" content="width=device-width, initial-scale=1"><meta charset="UTF-8"></head><body>
Invalid operation
</body></html>
配置文件的内容
app.conf是Beego用于配置应用程序的文件。它的内容如下:
; app.conf
appname = MathApp
httpport = 8080
runmode = dev
在这个文件中:
。appname是应用程序将要运行的进程的名字
。httpport是应用程序将要监听的的端口
。runmode声明了应用程序将要运行的模式。有效的指包括dev用于开发而prod用于生产。
在开发中使用Docker
本节将介绍在开发过程中使用Docker的好处,并且向你展示在开发中使用Docker的必须步骤。
配置Docker用于开发
我们将使用dockerfile来配置Docker以便用于开发。针对开发环境,对其的配置应该满足以下的要求:
。我们将使用上一节所提及的应用程序
。这些文件无论从容器的内部还是外部都可以访问
。我们将使用beego自带的bee工具。它用于在开发过程中在线地重新加载应用程序(在Docker容器的内部)
。Docker将为应用程序开放8080端口
。在我们的主机上,应用程序保存在/app/MathApp中
。在Docker容器中,应用程序保存在/go/src/MathApp中
。我们将为开发所创建的Docker image的名字是ma-image
。我们将要运行的Docker容器的名字是ma-instance
步骤一 - 创建Dockerfile
如下的Dockerfile可以满足以上的要求:
**FROM** golang:1.6
*# Install beego and the bee dev tool*
**RUN** go get github.com/astaxie/beego && go get github.com/beego/bee
*# Expose the application on port 8080*
**EXPOSE** 8080
*# Set the entry point of the container to the bee command that runs the*
*# application and watches for changes*
**CMD** ["bee", "run"]
第一行,
FROM golang:1.6
将Go的官方映像文件作为基础映像。该映像文件预安装了 Go 1.6 . 该映像已经把 $GOPATH 的值设置到了 /go 。所有安装在 /go/src 中的包将能够被go命令访问。
第二行,
RUN go get github.com/astaxie/beego && go get github.com/beego/bee
安装 beego 包和 bee 工具。 beego 包将在应用程序中使用。 bee 工具用语在开发中再现地重新加载我们的代码。
第三行,
EXPOSE 8080
在开发主机上利用容器为应用程序开放8080端口。
最后一行,
CMD ["bee", "run"]
使用bee命令启动应用程序的在线重新加载。
步骤二 - 构建image
一旦创建了Docker file,运行如下的命令来创建image:
docker build -t ma-image .
执行以上的命令将创建名为ma-image的image。该image现在可以用于使用该应用程序的任何人。这将确保这个团队能够使用一个统一的开发环境。
为了查看你的系统上的image列表,运行如下的命令:
docker images
这行该命令将输出与以下类似的内容:
REPOSITORY TAG IMAGE ID CREATED SIZE
ma-image latest 8d53aa0dd0cb 31 seconds ago 784.7 MB
golang 1.6 22a6ecf1f7cc 5 days ago 743.9 MB
注意image的确切名字和编号可能不同,但是,你应该至少看到列表中有 golang 和 ma-image image。
步骤三 - 运行容器
一旦 ma-image 已经完成,你可以使用以下的命令启动一个容器:
docker run -it --rm --name ma-instance -p 8080:8080 \
-v /app/MathApp:/go/src/MathApp -w /go/src/MathApp ma-image
让我们分析一下上面的命令来看看它做了什么。
。docker run命令用于从一个image上启动一个容器
。-it 标签以交互的方式启动容器
。--rm 标签在容器关闭后将会将其清除
。--name ma-instance 将容器命名为ma-instance
。-p 8080:8080 标签允许通过8080端口访问该容器
。-v /app/MathApp:/go/src/MathApp更复杂一些。它将主机的/app/MathApp映射到容器中的/go/src/MathApp。这将使得开发文件在容器的内部和外部都可以访问。
。ma-image 部分声明了用于容器的image。
执行以上的命令将启动Docker容器。该容器为你的应用程序开发了8080端口。无论何时你做了变更,它都将自动地重构你的应用程序。你将在console(控制台)上看到以下的输出:
bee :1.4.1
beego :1.6.1
Go :go version go1.6 linux/amd64
2016/04/10 13:04:15 [INFO] Uses 'MathApp' as 'appname'
2016/04/10 13:04:15 [INFO] Initializing watcher...
2016/04/10 13:04:15 [TRAC] Directory(/go/src/MathApp)
2016/04/10 13:04:15 [INFO] Start building...
2016/04/10 13:04:18 [SUCC] Build was successful
2016/04/10 13:04:18 [INFO] Restarting MathApp ...
2016/04/10 13:04:18 [INFO] ./MathApp is running...
2016/04/10 13:04:18 [asm_amd64.s:1998][I] http server Running on :8080
为了检查相关的设置,可以在浏览器中访问 http://localhost:8080/sum/4/5 。你讲看到与下面类似的东西:
注意:这里假定你是在使用本地主机
步骤四 - 开发应用程序
现在,让我们看看这将如何在开发阶段提供帮助。在完成以下的操作时请确保容器在运行。在## main.go ##文件中,将第34行:
c.Data["operation"] = operation
改成:
c.Data["operation"] = "real " + operation
在你保存修改的一刻,你讲看到类似以下的输出:
2016/04/10 13:17:51 [EVEN] "/go/src/MathApp/main.go": MODIFY
2016/04/10 13:17:51 [SKIP] "/go/src/MathApp/main.go": MODIFY
2016/04/10 13:17:52 [INFO] Start building...
2016/04/10 13:17:56 [SUCC] Build was successful
2016/04/10 13:17:56 [INFO] Restarting MathApp ...
2016/04/10 13:17:56 [INFO] ./MathApp is running...
2016/04/10 13:17:56 [asm_amd64.s:1998][I] http server Running on :8080
为了检查该变更,在你的浏览器中访问 http://localhost:8080/sum/4/5 。你将看到类似下面的输出:
如你所见,你的应用程序在保存了修改之后自动地编译并提供了服务。
在生产中使用Docker
本节将讲解如何在一个Docker容器中部署Go应用程序。我们将使用Semaphore来完成以下的工作:
。当一个变更被推送到git资料库后自动地进行编译
。自动地运行测试
。如果编译成功并且通过测试就创建一个Docker映像
。将Docker映像文件推送入Docker Hub
。更新服务器以便使用最新的Docker映像
创建一个生产用的Dockerfile
在开发过程中,我们的目录有如下的结构:
MathApp
├── conf
│ └── app.conf
├── main.go
├── main_test.go
└── views
├── invalid-route.html
└── result.html
由于我们想要从项目中构建Docker映像,我们需要创建一个将用于生产环境的Dockerfile。在项目的根目录中创建一个Dockerfile。新的目录结构如下所示:
MathApp
├── conf
│ └── app.conf
├── Dockerfile
├── main.go
├── main_test.go
└── views
├── invalid-route.html
└── result.html
在Dockerfile文件中输入以下的内容:
FROM golang:1.6
Create the directory where the application will reside
RUN mkdir /app
Copy the application files (needed for production)
ADD MathApp /app/MathApp
ADD views /app/views
ADD conf /app/conf
Set the working directory to the app directory
WORKDIR /app
Expose the application on port 8080.
This should be the same as in the app.conf file
EXPOSE 8080
Set the entry point of the container to the application executable
ENTRYPOINT /app/MathApp
让我们具体看一下这些命令都做了什么。第一个命令,
FROM golang:1.6
表明将基于我们在开发中使用的golang:1.6映像构建新的映像文件。第二个命令:
RUN mkdir /app
在容器的根里创建一个名为app的目录,我们用其来保存项目文件。第三个命令集:
ADD MathApp /app/MathApp
ADD views /app/views
ADD conf /app/conf
从主机中拷贝二进制、视图文件夹及配置文件夹到映像文件中的应用程序目录。第四个命令:
WORKDIR /app
在映像文件中把/app设置为工作目录。第五个命令:
EXPOSE 8080
在容器中开放8080端口。该端口应该与应用程序的 app.conf 文件中声明的端口一致。最后的命令:
ENTRYPOINT /app/MathApp
将映像文件的入口设置为应用程序的二进制文件。这将启动二进制文件的执行并监听8080端口。
自动地编译及测试
一旦你把代码上传到你的资料库中Semaphore将自动地对代码进行编译和测试,一切都变得简单了。点击这里了解如何添加你的 Github 或 Bitbucket 项目并且在Semaphore上设置Golang项目。
一个Go项目的缺省配置文件关注以下几点:
。获取相关文件
。编译项目
。运行测试
一旦你完成这个过程,就可以在Semaphore仪表盘上看到最近的编译和测试状态。如果编译或测试失败,该过程会终止而且也不会部署任何内容。
在Semaphore上创建Initial Setup来实现自动部署
一旦你配置好了编译过程,下一步就是配置部署过程。为了部署应用程序,你需要:
1. 创建Docker image
2. 将Docker image推送入Docker Hub
3. 拉取新的image来更新服务器并基于该image启动一个新的Docker容器
作为开始,我们需要在semaphore上配置项目实现持续部署。
前三个步骤相对简单:
。选择部署模式
。选择部署策略
。选择在部署过程中使用的资料库分支
第四步(设置部署命令),我们将使用下一节中的命令。当前暂且空着并转到下一步。
在第五步中,输入你的服务器中用户的SSH私钥。这将使得一些部署命令可以在你的服务器上安全执行而不需要输入口令。
在第六部中,你可以命名你的服务器。如果你做的话,Semaphore会给该服务器指定一个类似server-1234这样的随机名字。
在服务器上设置更新脚本
之后,我们将配置部署过程,Semaphore将创建新的image冰将其上传到Docker Hub中。一旦完成,一个Semaphore的命令将执行你的服务器上的脚本来初始化更新过程。
为了完成这个工作,我们需要将名为 update.sh 的文件放置到你的服务器中。
#!/bin/bash
docker pull $1/ma-prod:latest
if docker stop ma-app; then docker rm ma-app; fi
docker run -d -p 8080:8080 --name ma-app $1/ma-prod
if docker rmi $(docker images --filter "dangling=true" -q --no-trunc); then :; fi
使用如下的命令给该文件赋予执行权限:
chmod +x update.sh
让我们来看一下该文件是如何使用的。这个脚本接收一个参数并且在命令中使用该参数。这个参数应该是你在Docker Hub上的用户名。下面是使用该命令的例子:
./update.sh docker_hub_username
现在让我们看一下这个文件中的每一个命令来理解他们要做什么。
第一个命令,
docker pull $1/ma-prod:latest
从Docker Hub上拉取最新的image到服务器中。如果你在Docker Hub上的用户名是 demo_user ,该命令将拉取Docker Hub上标记为 latest 、名为 demo_user/ma-prod 的image。
第二个命令:
if docker stop ma-app; then docker rm ma-app; fi
停止并删除之前任何以 ma-app 为名字而启动的容器。
第三个命令:
docker run -d -p 8080:8080 --name ma-app $1/ma-prod
使用在最近一次编译中包涵变更的最新image来启动一个新的容器。
最后的命令:
docker rmi $(docker images --filter "dangling=true" -q --no-trunc)
从服务器上删除任何没有用的image。这种清理将保持服务器整洁并降低磁盘空间的占用。
注意:这个文件必须存放在用户主目录中,而该用户就是之前的步骤中所用到的SSH密钥的所有者。如果文件的位置发生了变化,则需要在后面的章节中相应地更新部署命令。
配置项目使其能够支持Docker
缺省情况下,Semaphore上的新项目使用 Ubuntu 14.04 LTS v1603 平台。该平台并不支持Docker。由于我们希望使用Docker,我们需要修改Semaphore的配置来使用 Ubuntu 14.04 LTS v1603(beta with Docker support) 平台。
设置环境变量
为了在部署过程中安全使用Docker Hub,我们需要把我们的证书保存在Semaphore自动初始化的环境变量中。
我们将保存以下的变量:
。DH_USERNAME - Docker Hub用户名
。DH_PASSWORD - Docker Hub口令
。DH_EMAIL - Docker Hub email地址
这里是如何以安全的方式设置环境变量。
设置部署命令
虽然我们完成了初始配置,但是实际上什么也不会部署。原因是我们在命令环节中都还是空白。
在第一步,我们将输入用于完成部署过程的命令。为了完成这一步,请进入Semaphore中你的项目主页。
在这一页上,点击 Server 栏中服务器的名字。这将带你进入:
点击位于页头下方页面右侧的 Edit server 按钮。
在随后的一页中,我们需要关注标题为 Deploy commands 的最后一栏。点击 Change deploy commands 链接来开启命令编辑。
在编辑框中,输入如下的命令并点击 Save Deploy Commands 按钮:
go get -v -d ./
go build -v -o MathApp
docker login -u $DH_USERNAME -p $DH_PASSWORD -e $DH_EMAIL
docker build -t ma-prod .
docker tag ma-prod:latest $DH_USERNAME/ma-prod:latest
docker push $DH_USERNAME/ma-prod:latest
ssh -oStrictHostKeyChecking=no your_server_username@your_ip_address "~/update.sh $DH_USERNAME"
注意:请确定用正确的值替换 your_server_username@your_ip_address 。
让我们具体看一下每个命令。
前两个命令 go get和go build是Go的标准命令用于获取相关文件并相应地编译项目。注意go build命令说声明的二进制文件名应该是MathApp。这个名字应该与Dockerfile中使用的名字相同。
第三个命令,
docker login -u $DH_USERNAME -p $DH_PASSWORD -e $DH_EMAIL
使用环境变量实现在Docker Hub上的认证,从而使得我们能够推送最新的Image。第四个命令,
docker build -t ma-prod .
基于最新的代码库莱创建Docker image。第五个命令,
docker tag ma-prod:latest $DH_USERNAME/ma-prod:latest
将新生成的image标记为 your_docker_hub_username/ma-prod:latest 。完成这一步后,我们就可以把image推送到Docker Hub上相应的资料库中。第六个命令,
docker push $DH_USERNAME/ma-prod:latest
将该image推送到Docker Hub中。最后一个命令,
ssh -oStrictHostKeyChecking=no your_server_username@your_ip_address "~/update.sh $DH_USERNAME"
使用ssh命令登陆到你的服务器上并执行我们之前创建的 update.sh 脚本。这个脚本从Docker Hub上获取最新的image并在其上启动一个新的容器。
部署应用程序
由于我们目前还没有真正地在我们的服务器上部署应用程序,那么我们就手工操作一遍。注意你大可不必这么做。以后你提交任何变更到你的资料库后,如果编译和测试都成功的话Semaphore会自动部署你的应用程序。我们现在手工部署它知识测试是否一切都能工作正常。
你可以在semaphore 文档的编译页中找到如何手工地部署一个应用程序。
一旦你已经部署了应用程序,就可以访问:
http://your_ip_address:8080/sum/4/5
这将显示与以下类似的结果:
这与我们在开发过程中看到的应该是一致的。唯一的不同在于localhost被替换掉,在URL中你应使用服务器的IP地址。
对配置进行测试
现在我们已经拥有了自动编译和部署路程配置,我们将看到它是如何简化工作流的。让我们做一个小修改然后看看服务器上的应用程序如何自动地响应。
让我们尝试吧文本的颜色从黑色改为红色。为了这个结果,在 views/result.html 文件中把第8行从
<body>
改成
<body style="color: red">
现在,保存文件。在你的应用程序目录中,使用如下的命令提交变更:
git add views/result.html
git commit -m 'Change the color of text from black (default) to red'
使用如下的命令将变更推送到你的资料库中:
git push origin master
一旦git push命令完成,Semaphore将监测到你的资料库中的变化并且自动地启动编译过程。只要编译过程(包括测试)成功完成,Semaphore将启动部署过程。Semaphore的仪表盘上会实时地显示编译和部署的过程。
一旦Semaphore的仪表盘上显示编译和部署过程都以完成,刷新页面:
http://your_ip_address:8080/sum/4/5
你现在应该看到类似以下的内容:
总结
在本教程中,我们学习了如何为一个Go应用程序创建Docker容器并且使用Semaphore将容器部署到服务器上。
你现在应该能够使用Docker来简化以后的Go应用程序的部署任务。如果你有任何问题,请随时在下面的备注中发帖。