简介
虽然大部分Go应用程序可以编译为一个单一的二进制文件,但Web应用程序可能还有自己的模板和配置文件。如果一个项目中包含大量文件,可能会因为文件不同步而导致出错并造成更多严重问题。
您将通过本文了解如何使用Docker部署Go Web应用程序,以及Docker如何帮您改善开发工作流和部署过程。各种规模的团队都能从本文内容中获益。
目标
通过阅读本文,您将能:
- 对Docker有一些基本了解,
- 了解Docker如何帮您开发Go应用程序,
- 知道如何为Go应用程序创建生产用Docker容器,并
- 知道如何使用Semaphore将Docker容器持续部署到您的服务器。
前提要求
为了完成本文,您需要:
- 在您的计算机和服务器上安装Docker,并具备
- 一台可以使用SSH密钥对SSH请求进行身份验证的服务器。
理解Docker
Docker可以帮您为自己的应用程序创建一个单一的可部署“单位”。这样的单位也叫做容器,其中包含了应用程序需要的一切。例如代码(或二进制文件)、运行时、系统工具,以及系统库文件。将所有这些需要的内容打包为一个单一的单位,可确保无论将应用程序部署在何处,都能提供完全相同的环境。这种技术还可以帮您维持完全一致的开发和生产环境,通常这些环境是很难被追踪的。
一旦搭建完成,容器的创建和部署将可自动进行。这本身就可以避免一系列问题。这些问题中大部分都是因为文件不同步,或开发和生产环境之间的差异导致的。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
文件夹包含视图文件invalid-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>
配置文件的内容
Beego可以使用app.conf
文件配置应用程序,该文件的内容如下:
; app.conf
appname = MathApp
httpport = 8080
runmode = dev
在这个文件中,
appname
是运行该应用程序的进程对应的名称,httpport
是应用程序使用的端口,而runmode
决定了应用程序的运行模式。可用值包括对应开发环境的dev
,以及对应生产环境的prod
。
在开发过程中使用Docker
本节将介绍在开发过程中使用Docker所能获得的收益,并会指导您完成在开发过程中使用Docker的步骤。
针对开发工作配置Docker
我们会使用一个Dockerfile
配置开发工作所用的Docker。配置工作需要满足的开发环境要求如下:
- 我们会使用上文提到的应用程序,
- 文件必须能够从容器内部和外部访问,
- 我们将使用
beego
包含的bee
工具。该工具可用于在开发过程中实时重载(Live reload)应用(应用位于Docker容器内部), - Docker将通过
8080
端口暴露该应用程序, - 在我们的计算机上,该应用程序位于
/app/MathApp
, - 在Docker容器中,该应用程序位于
/go/src/MathApp
, - 我们为开发工作创建的Docker映像名为
ma-image
,并且 - 开发过程中所运行的Docker容器的名称为
ma-instance
。
第1步 - 创建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
命令开始对我们的应用程序进行实时重载。
第2步 - 构建映像
创建好Docker文件之后,可运行下列命令创建映像:
docker build -t ma-image .
执行上述命令可创建一个名为ma-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
请注意,实际的映像名称和数量可能各不相同,不过您至少应该可以在列表中看到golang
和ma-image
映像。
第3步 - 运行容器
准备好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
命令可用于通过映像运行容器,-it
标记可以用交互式模式启动该容器,--rm
标记可以在容器关闭后清理其中的内容,--name ma-instance
可以将容器命名为ma-instance
,-p 8080:8080
标记使得容器可以通过8080
端口访问,-v /app/MathApp:/go/src/MathApp
略微复杂,可以将/app/MathApp
从计算机映射至容器的/go/src/MathApp
目录。这样可以确保在容器内部和外部均可访问这些开发文件,并且ma-image
部分指定了容器内部使用的映像名称。
执行上述命令可启动Docker容器。这个容器可以将您的应用程序暴露到8080
端口,还可以在您修改代码后自动重新构建您的应用程序。您将在控制台中看到下列输出结果:
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
。您应该能看到类似下图的结果:
注意:上述步骤假设您在自己的本地计算机上执行操作。
第4步 - 开发应用程序
随后一起来看看这种做法能对开发工作起到什么帮助。执行下文操作的过程中请确保容器始终处于运行状态。在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
可将映像的入口点(Entry point)设置为应用程序的二进制文件。这样即可启动该二进制文件,并开始在8080
端口提供服务。
自动构建和测试
一旦将改动的代码推送至代码库,Semaphore就可以帮您轻松地自动构建并测试代码。这里介绍了如何添加您的GitHub或Bitbucket项目,以及在Semaphore上设置Golang项目的方法。
Go项目的默认配置可解决下列问题:
- 抓取依存项,
- 构建项目,并
- 运行测试。
完成上述过程后,便可通过Semaphore仪表板查看最新构建的状态并对其进行测试。如果构建或测试失败,整个过程将暂停,且不部署任何内容。
通过Semaphore为自动化开发创建初始环境
设置好构建过程后,下一步需要配置部署过程。若要部署应用程序,您需要:
- 构建Docker映像,
- 将Docker映像推送至Docker Hub,并
- 更新服务器以拉取新的映像,随后据此启动一个新的Docker容器。
首先我们需要在Semaphore上设置项目以进行持续部署。
前三个步骤相对较为简单:
- 选择部署方法,
- 选择部署策略,并
- 选择部署过程所使用的代码库分支。
第4步(设置部署命令)我们将使用下一节介绍的命令。目前可以留空并执行下一个步骤。
第5步,输入服务器上用户的SSH密钥。这样即可在无需密码的情况下,以安全的方式在服务器上执行某些部署命令。
第6步,可以为服务器命名。如果不指定名称,Semaphore会为服务器分配类似server-1234这样的随机名称。
在服务器上设置更新脚本
接下来需要设置部署过程,这样Semaphore即可构建新映像并将其上传至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
docker rmi $(docker images --filter "dangling=true" -q --no-trunc)
使用下列命令使得该文件可以执行:
chmod +x update.sh
随后来看看这个文件是如何生效的。这个脚本可以接受单一参数,并在自己的命令中使用这个参数。这个参数可以是您在Docker Hub上的用户名。例如可以通过下面这样的格式使用该命令:
./update.sh docker_hub_username
为了理解具体的用途,随后再来看看文件中的每条命令。
第一条命令,
docker pull $1/ma-prod:latest
将最新映像从Docker Hub拉取到服务器上。如果您在Docker Hub上的用户名是demo_user
,这条命令将从Docker Hub拉取名为demo_user/ma-prod
,且被标记为latest
的映像。
第二条命令,
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
使用能够反映最新构建中所有变动的最新映像启动一个新的容器(名为ma-app
)。
最后一条命令,
docker rmi $(docker images --filter "dangling=true" -q --no-trunc)
可从服务器上删除任何未使用的映像。这个清理工作可以确保服务器整洁,并能降低磁盘空间的使用。
注意:这个文件必须放置在上一步骤中所用SSH密钥对应用户的根目录下。如果文件位置有变化,下文使用的部署命令也需要酌情更新。
设置项目配合Docker工作
默认情况下,Semaphore上的新项目会使用Ubuntu 14.04 LTS v1603
平台。这个平台并非Docker自带的。因为我们感兴趣的是Docker的使用,因此需要更改Semaphore的平台设置以使用Ubuntu 14.04 LTS v1603(支持Docker的Beta测试版)
平台。
设置环境变量
为了在开发过程中以安全的方式使用Docker Hub,我们需要将自己的凭据存储在Semaphore自动初始化出来的环境变量内。
我们需要存储下列变量:
DH_USERNAME
- Docker Hub用户名DH_PASSWORD
- Docker Hub密码DH_EMAIL
- Docker Hub邮件地址
可以参考这里了解如何用安全的方式设置环境变量。
设置部署命令
虽然已经完成初始设置,但目前还无法进行部署。原因在于还没有配置需要运行的命令。
首先我们需要输入完成部署过程所需的命令,为此请在Semaphore上打开您的项目首页。
">
在这个页面上,点击Servers
选项下的服务器名称,随后可以看到下列界面:
点击页面右侧,页头下方的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进行身份验证,这样我们才可以推送最新映像。第四条命令,
docker build -t ma-prod .
根据最新代码基构建一个名为ma-prod
的Docker映像。第五条命令,
docker tag ma-prod:latest $DH_USERNAME/ma-prod:latest
为最新创建的映像添加your_docker_hub_username/ma-prod:latest
标签。这样我们就可以将该映像推送至Docker Hub上相应的代码库中。第六条命令,
docker push $DH_USERNAME/ma-prod:latest
可将该映像推送至Docker Hub。最后一条命令,
ssh -oStrictHostKeyChecking=no your_server_username@your_ip_address "~/update.sh $DH_USERNAME"
使用ssh
命令登录到您的服务器,执行我们在上文步骤中创建的update.sh脚本。这个脚本可以从Docker Hub获取最新映像,并据此启动一个新的容器。
部署应用程序
截至目前我们并未真正将应用程序部署到服务器,因此先来进行手工部署。请注意这一操作并非是必须的。当您下一次将改动的代码推送至代码库后,只要构建和测试都能成功完成,Semaphore将自动部署您的应用程序。我们这里进行手工部署只是为了测试一切都能正常工作。
您可以阅读 Semaphore文档了解如何在构建页面手工部署应用程序。
应用程序部署完毕后,可通过下列地址访问:
http://your_ip_address:8080/sum/4/5
访问结果应该类似下图所示:
结果与开发过程中看到的完全相同。唯一的差异在于此处访问时使用的URL并不是localhost
,而是服务器的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容器部署至服务器。
现在您已经可以使用Docker简化下一个Go应用程序的开发工作。如果您有任何问题,欢迎发布到下方的评论区。
作者
Kulshekhar Kabra是一位独立开发者,他的工作可以方便地接触到各种新技术,并将其运用到新项目中。只要工作之余有闲时间,他还很喜欢撰写技术文章并制作视频教程。
阅读英文原文:How To Deploy a Go Web Application with Docker