简介
虽然大部分 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
感谢陈兴璐对本文的策划和审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论