你好,我是轩脉刃。
在前面的课程中,我们基本上已经完成了一个能同时生成前端和后端的框架hade,也能很方便对框架进行管理控制。下面两节课,我们来考虑框架的一些周边功能,比如部署自动化。
部署自动化其实不是一个框架的刚需,有很多方式可以将一个服务进行自动化部署,比如现在比较流行的Docker化或者CI/CD流程。
但是一些比较个人比较小的项目,比如一个博客、一个官网网站,这些部署流程往往都太庞大了,更需要一个服务,能快速将在开发机器上写好、调试好的程序上传到目标服务器,并且更新应用程序。这就是我们今天要实现的框架发布自动化。
所有的部署自动化工具,基本都依赖本地与远端服务器的连接,这个连接可以是FTP,可以是HTTP,但是更经常的连接是SSH连接。因为一旦我们购买了一个Web服务器,服务器提供商就会提供一个有SSH登录账号的服务器,我们可以通过这个账号登录到服务器上,来进行各种软件的安装,比如FTP、HTTP服务等。
基本上,SSH账号是我们拿到Web服务器的首要凭证,所以要设计的自动化发布系统也是依赖SSH的。
那么在Golang中如何SSH连接远端的服务器呢?有一个ssh库能完成SSH的远端连接。
这里介绍一个小知识,你可以看下这个ssh库的git:golang.org/x/crypto/ssh。它是在官网golang.org 下的,但是又不是官方的标准库,因为子目录是x。
这种库其实也是经过官方认证的,属于实验性的库,我们可以这么理解:以golang.org/x/ 开头的库,都是官方认为这些库后续有可能成为标准库的一部份,但是由于种种原因,现在还没有计划放进标准库中,需要更多时间打磨。但是这种库的维护者和开发者一般已经是Golang官方组的人员了。比如现在今年讨论热度很大的Golang泛型,据说也会先以实验库的形式出现。
不管怎么样,这种以golang.org/x/开头的库,成熟度已经非常高了,我们是可以放心使用的。来了解一下这个ssh库:
package main
import (
"bytes"
"fmt"
"log"
"golang.org/x/crypto/ssh"
)
func main() {
var hostKey ssh.PublicKey
// ssh相关配置
config := &ssh.ClientConfig{
User: "username",
Auth: []ssh.AuthMethod{
ssh.Password("yourpassword"),
},
HostKeyCallback: ssh.FixedHostKey(hostKey),
}
// 创建client
client, err := ssh.Dial("tcp", "yourserver.com:22", config)
if err != nil {
log.Fatal("Failed to dial: ", err)
}
defer client.Close()
// 使用client做各种操作
session, err := client.NewSession()
if err != nil {
log.Fatal("Failed to create session: ", err)
}
defer session.Close()
var b bytes.Buffer
session.Stdout = &b
if err := session.Run("/usr/bin/whoami"); err != nil {
log.Fatal("Failed to run: " + err.Error())
}
fmt.Println(b.String())
}
在这个官方示例中,我们可以看到ssh库作为客户端连接,最重要的是创建ssh.Client这个数据结构,而这个数据结构使用ssh.Dail能进行创建,创建的时候依赖ssh.ClientConfig这么一个配置结构。
是不是非常熟悉?和前面的Gorm、Redis一样,将SSH的连接部分封装成为hade框架的SSH服务,这样我们就能很方便地初始化一个ssh.Client了。
经过前面几节课,相信你已经非常熟悉这种套路了,我们就简要说明下ssh service的封装和实现思路。这节课的重点在后面对自动化发布系统的实现上。
ssh service的封装一样有三个部分,服务协议、服务提供者、服务实现。
服务协议我们提供GetClient方法:
// SSHService 表示一个ssh服务
type SSHService interface {
// GetClient 获取ssh连接实例
GetClient(option ...SSHOption) (*ssh.Client, error)
}
而其中的SSHOption作为更新SSHConfig的函数:
// SSHOption 代表初始化的时候的选项
type SSHOption func(container framework.Container, config *SSHConfig) error
我们封装配置结构为 SSHConfig:
// SSHConfig 为hade定义的SSH配置结构
type SSHConfig struct {
NetWork string
Host string
Port string
*ssh.ClientConfig
}
对应的配置文件如下 config/testing/ssh.yaml,你可以看看每个配置的说明:
timeout: 1s
network: tcp
web-01:
host: 118.190.3.55 # ip地址
port: 22 # 端口
username: yejianfeng # 用户名
password: "123456" # 密码
web-02:
network: tcp
host: localhost # ip地址
port: 3306 # 端口
username: jianfengye # 用户名
rsa_key: "/Users/user/.ssh/id_rsa"
known_hosts: "/Users/user/.ssh/known_hosts"
这里注意下,SSH的连接方式有两种,一种是直接使用用户名密码来连接远程服务器,还有一种是使用rsa key文件来连接远端服务器,所以这里的配置需要同时支持两种配置。对于使用rsa key文件的方式,需要设置rsk_key的私钥地址和负责安全验证的known_hosts。
定义好了SSH的服务协议,服务提供者和服务实现并没有什么特别,就不展示具体代码了,在GitHub上的provider/ssh/provider.go 和provider/ssh/service.go中。我们简单说一下思路。
对于服务提供者,我们实现基本的五个函数Register/Boot/IsDefer/Param/Name。另外这个ssh服务并不是框架启动时候必要加载的,所以设置IsDefer为true,而Param我们就照例把服务容器container作为参数,传递给Register设定的实例化方法。
而SSH服务的具体实现,同样类似Redis,先配置更新,再查询是否已经实例化,若已经实例化,返回实例化对象;若没有实例化,实例化client,并且存在map中。
完成了SSH的服务协议、服务提供者、服务实例,我们就重点讨论下如何使用SSH的服务协议来实现自动化部署。
首先还是思考清楚自动化部署的命令设计。我们的hade框架是同时支持前后端的开发框架,所以自动化部署是需要同时支持前后端部署的,也就是说它的命令也需要支持前后端的部署,这里我们设计一个显示帮助信息的一级命令./hade deploy
和四个二级命令:
./hade deploy frontend
,部署前端./hade deploy backend
,部署后端./hade deploy all
,同时部署前后端./hade deploy rollback
,部署回滚同时也设计一下部署配置文件。
首先,我们是需要知道部署在哪个或者哪几个服务器上的,所以需要有一个数组配置项connections来定义部署服务器。而部署服务器的具体用户名密码配置,在前面SSH的配置里是存在的,所以这里直接把SSH的配置路径放在我们的connections中就可以了。
其次,还要知道我们要部署的远端服务器的目标文件夹是什么?所以这里需要有一个remote_folder配置项来配置远端文件夹。
然后就是前端部署的配置frontend了。我们知道,在本地编译之后,会直接编译成了dist目录下的HTML/JS/CSS文件,这些文件直接上传到远端文件夹就是可以使用的了。
但是,在上传前端编译文件之前和在远端服务器执行一些命令之后,是有可能要做一些操作的。比如上传前先清空远端文件夹、上传后更新nginx等。所以这里,我们设计两个数组结构pre_action和post_action来分别存放部署的前置命令和部署的后置命令。
最后就是后端部署的配置backend。同前端部署一样,我们也有部署的前置命令和后置命令。但是后端编译还有一个不同点。
因为后端是Golang编译的,而它的编译其实是分平台的,加上Go支持“交叉编译”。就是说,比如我的工作机器是Mac操作系统,Web服务器是Linux操作系统,那么我需要编译Linux操作系统的后端程序,但是我可以直接在Mac操作系统上使用GOOS 和 GOARCH 来编译Linux操作系统的程序:
GOOS=linux GOARCH=amd64 go build ./
这样编译出来的文件就是可以在Linux运行的后端进程了。所以在后端部署的配置项里面,我们增加GOOS 和 GOARCH分别表示后端的交叉编译参数。
完整的配置文件在config/development/deploy.yaml中:
connections: # 要自动化部署的连接
- ssh.web-01
remote_folder: "/home/yejianfeng/coredemo/" # 远端的部署文件夹
frontend: # 前端部署配置
pre_action: # 部署前置命令
- "pwd"
post_action: # 部署后置命令
- "pwd"
backend: # 后端部署配置
goos: linux # 部署目标操作系统
goarch: amd64 # 部署目标cpu架构
pre_action: # 部署前置命令
- "pwd"
post_action: # 部署后置命令
- "chmod 777 /home/yejianfeng/coredemo/hade"
- "/home/yejianfeng/coredemo/hade app restart"
好,配置文件设计好了,下面我们开始实现对应的命令。
其实估计你对如何实现,已经大致心中有数了。一级命令 ./hade deploy
还是并没有什么内容,只是将帮助信息打印出来,之前也做过很多次,就不描述了。二级命令按之前的套路,一般是先编译,再部署,最后上传到目标服务器。
看二级命令 ./hade deploy frontend
。对于部署前端,我们分为三个步骤:
在framework/command/deploy.go中:
// deployFrontendCommand 部署前端
var deployFrontendCommand = &cobra.Command{
Use: "frontend",
Short: "部署前端",
RunE: func(c *cobra.Command, args []string) error {
container := c.GetContainer()
// 创建部署文件夹
deployFolder, err := createDeployFolder(container)
if err != nil {
return err
}
// 编译前端到部署文件夹
if err := deployBuildFrontend(c, deployFolder); err != nil {
return err
}
// 上传部署文件夹并执行对应的shell
return deployUploadAction(deployFolder, container, "frontend")
},
}
这里可能你会有个疑惑,为什么要创建一个部署文件夹?我们直接将前端编译的dist目录上传到目标服务器不就行了么?来为你解答下。
部署服务是一个很小心的过程,因为它会影响现在的线上服务,而每次部署都是有可能失败的,也就很有可能需要进行回滚操作,就是我们前面定义的部署回滚操作命令 ./hade deploy rollback
。而回滚的时候,需要能找到某个特定版本的编译内容,这里就需要部署文件夹。
这个部署文件夹我们定义为目录 deploy/xxxxxx,其中的xxxx直接设置为细化到秒的时间。对应的创建部署文件夹的函数如下:
// 创建部署的folder
func createDeployFolder(c framework.Container) (string, error) {
appService := c.MustMake(contract.AppKey).(contract.App)
deployFolder := appService.DeployFolder()
// 部署文件夹的名称
deployVersion := time.Now().Format("20060102150405")
versionFolder := filepath.Join(deployFolder, deployVersion)
if !util.Exists(versionFolder) {
return versionFolder, os.Mkdir(versionFolder, os.ModePerm)
}
return versionFolder, nil
}
这里的appService.DeployFolder() 是我们在appService下创建的一个新的目录deploy,在framework/contract/app.go中:
// App 定义接口
type App interface {
...
// DeployFolder 存放部署的时候创建的文件夹
DeployFolder() string
...
}
有了这个部署文件夹,每次的发布都有“档案”存储了,这就为回滚命令提供了可能性。我们每次编译的文件,也都会先经过这个部署文件夹,再中转上传到目标服务器。
第一步创建部署文件夹实现了,我们再回头看下部署前端的第二个步骤,编译前端文件到部署文件夹。可以直接使用 buildFrontendCommand的RunE方法,它会将前端编译到dist目录下,然后我们再将dist目录文件拷贝到部署文件夹中:
func deployBuildFrontend(c *cobra.Command, deployFolder string) error {
container := c.GetContainer()
appService := container.MustMake(contract.AppKey).(contract.App)
// 编译前端
if err := buildFrontendCommand.RunE(c, []string{}); err != nil {
return err
}
// 复制前端文件到deploy文件夹
frontendFolder := filepath.Join(deployFolder, "dist")
if err := os.Mkdir(frontendFolder, os.ModePerm); err != nil {
return err
}
buildFolder := filepath.Join(appService.BaseFolder(), "dist")
if err := util.CopyFolder(buildFolder, frontendFolder); err != nil {
return err
}
return nil
}
第三步,上传部署文件夹,并且执行对应的前置和后置的shell。
这个步骤的实现是今天这节课的重点了。首先遍历配置文件中的deploy.connections,明确我们要在哪几个远端节点中进行部署;然后对每个远端服务创建一个ssh.Client,由于前面已经写好了SSH服务,所以直接使用GetClient方法就能为每个节点创建一个sshClient了:
for _, node := range deployNodes {
sshClient, err := sshService.GetClient(ssh.WithConfigPath(node))
if err != nil {
return err
}
...
}
接下来就要执行命令了,那怎么执行前置或者后置命令呢?
我们需要为每个命令创建一个session,然后使用session.CombinedOut来输出这个命令的结果,把每个命令的结果都输出在控制台中。相关代码如下:
for _, action := range preActions {
// 创建session
session, err := sshClient.NewSession()
if err != nil {
return err
}
// 执行命令,并且等待返回
bts, err := session.CombinedOutput(action)
if err != nil {
session.Close()
return err
}
session.Close()
// 执行前置命令成功
logger.Info(context.Background(), "execute pre action", map[string]interface{}{
"cmd": action,
"connection": node,
"out": strings.ReplaceAll(string(bts), "\n", ""),
})
}
执行了前置命令之后,下面就是要把部署文件夹中的文件上传到目标服务器了。如何通过SSH服务将文件上传到目标服务器呢?
这里需要使用到一个成熟的第三方库 sftp 了,目前已经有1.1k star,采用BSD-2的开源协议,允许修改商用,但是要保留申明。这个库就是封装SSH的,将SFTP文件传输协议封装了一下。SFTP是什么?它是基于SSH协议来进行文件传输的一个协议,功能与FTP相似,区别就是它的连接通道使用SSH。
SFTP的底层连接实际上就是SSH,只是把传输的文件内容进行了一下加密等工作,增加了传输的安全性。所以SFTP本质就是“使用SSH连接来完成文件传输功能”。这点可以从它的实例化看出,sftp.Client的唯一参数就是ssh.Client。
client, err := sftp.NewClient(sshClient)
if err != nil {
return err
}
SFTP这个库,在初始化sftp.Client之后,会将这个client封装地和官方的本地操作文件OS库一样,你在使用sftp.Client的时候完全没有障碍。
比如,OS库创建一个文件是os.Create,在SFTP中就是使用client.Create;OS库获取一个文件信息的函数是os.Stat,在SFTP中就是client.Stat。但是注意下,这里完全是SFTP刻意将这个库函数设计的和OS库一样的,它们之间并没有什么嵌套关系。
我们使用ssh.Client初始化一个sftp.Client之后,写一个uploadFolderToSFTP的函数来实现将本地文件夹同步到远端文件夹:
// 上传部署文件夹
func uploadFolderToSFTP(container framework.Container, localFolder, remoteFolder string, client *sftp.Client) error {
logger := container.MustMake(contract.LogKey).(contract.Log)
// 遍历本地文件
return filepath.Walk(localFolder, func(path string, info os.FileInfo, err error) error {
// 获取除了folder前缀的后续文件名称
relPath := strings.Replace(path, localFolder, "", 1)
if relPath == "" {
return nil
}
// 如果是遍历到了一个目录
if info.IsDir() {
logger.Info(context.Background(), "mkdir: "+filepath.Join(remoteFolder, relPath), nil)
// 创建这个目录
return client.MkdirAll(filepath.Join(remoteFolder, relPath))
}
// 打开本地的文件
rf, err := os.Open(filepath.Join(localFolder, relPath))
if err != nil {
return errors.New("read file " + filepath.Join(localFolder, relPath) + " error:" + err.Error())
}
// 检查文件大小
rfStat, err := rf.Stat()
if err != nil {
return err
}
// 打开/创建远端文件
f, err := client.Create(filepath.Join(remoteFolder, relPath))
if err != nil {
return errors.New("create file " + filepath.Join(remoteFolder, relPath) + " error:" + err.Error())
}
// 大于2M的文件显示进度
if rfStat.Size() > 2*1024*1024 {
logger.Info(context.Background(), "upload local file: "+filepath.Join(localFolder, relPath)+
" to remote file: "+filepath.Join(remoteFolder, relPath)+" start", nil)
// 开启一个goroutine来不断计算进度
go func(localFile, remoteFile string) {
// 每10s计算一次
ticker := time.NewTicker(2 * time.Second)
for range ticker.C {
// 获取远端文件信息
remoteFileInfo, err := client.Stat(remoteFile)
if err != nil {
logger.Error(context.Background(), "stat error", map[string]interface{}{
"err": err,
"remote_file": remoteFile,
})
continue
}
// 如果远端文件大小等于本地文件大小,说明已经结束了
size := remoteFileInfo.Size()
if size >= rfStat.Size() {
break
}
// 计算进度并且打印进度
percent := int(size * 100 / rfStat.Size())
logger.Info(context.Background(), "upload local file: "+filepath.Join(localFolder, relPath)+
" to remote file: "+filepath.Join(remoteFolder, relPath)+fmt.Sprintf(" %v%% %v/%v", percent, size, rfStat.Size()), nil)
}
}(filepath.Join(localFolder, relPath), filepath.Join(remoteFolder, relPath))
}
// 将本地文件并发读取到远端文件
if _, err := f.ReadFromWithConcurrency(rf, 10); err != nil {
return errors.New("Write file " + filepath.Join(remoteFolder, relPath) + " error:" + err.Error())
}
// 记录成功信息
logger.Info(context.Background(), "upload local file: "+filepath.Join(localFolder, relPath)+
" to remote file: "+filepath.Join(remoteFolder, relPath)+" finish", nil)
return nil
})
}
这段代码长一点。首先我们使用功能filePath.Walk来遍历本地文件夹中的所有文件,如果遍历到的是子文件夹,就创建子文件夹,否则的话,我们就将本地文件上传到远端。而上传远端的操作大致就是三步:打开本地文件、打开远端文件、将本地文件传输到远端文件。
在上述函数中大致是这几句代码:
// 打开本地的文件
rf, err := os.Open(filepath.Join(localFolder, relPath))
// 打开/创建远端文件
f, err := client.Create(filepath.Join(remoteFolder, relPath))
// 将本地文件并发读取到远端文件
if _, err := f.ReadFromWithConcurrency(rf, 10); err != nil
SFTP提供了并发读取到远端文件ReadFromWithConcurrency的方法,我们可以使用这个并发读的方法提高上传效率。
但是即使是并发读,对于比较大的文件,还是需要等候比较长的时间。而这个等待时长,对于在控制台敲下部署命令的使用者来说是非常不友好的。我们希望能每隔一段时间显示一下当前的部署进度,这个怎么做呢?
这里我们设计大于2M的文件,执行这个操作。2M是我自己实验出来体验比较差的一个阈值。然后每2s就打印一下当前进度,所以使用了一个ticker,来计算时间。每次这个ticker结束的时候,计算一下远端文件的大小,再计算一下本地文件的大小。两者相除就是这个文件的上传进度,再使用日志打印就能打印出具体的进度了。
最后的效果如下:
到这里部署前端的代码就开发完成了。
理解了如何部署前端,部署后端的对应方法基本如出一辙。唯一不同的地方就是编译。
编译Golang的后端需要指定对应的编译平台和编译CPU架构,就是前面说的GOOS和GOARCH。所以我们就不能直接使用build命令来编译后端了。改成定位go程序,来执行go build,并且需要修改输出文件路径,输出到部署文件夹中。
当然这个部署文件夹还是按照我们之前的设计为 deploy/xxxxxx,其中的xxxx直接设置为细化到秒的时间,继续在framework/command/deploy.go中写入:
// 编译后端
path, err := exec.LookPath("go")
if err != nil {
log.Fatalln("hade go: 请在Path路径中先安装go")
}
// 组装命令
deployBinFile := filepath.Join(deployFolder, binFile)
cmd := exec.Command(path, "build", "-o", deployBinFile, "./")
cmd.Env = os.Environ()
// 设置GOOS和GOARCH
if configService.GetString("deploy.backend.goos") != "" {
cmd.Env = append(cmd.Env, "GOOS="+configService.GetString("deploy.backend.goos"))
}
if configService.GetString("deploy.backend.goarch") != "" {
cmd.Env = append(cmd.Env, "GOARCH="+configService.GetString("deploy.backend.goarch"))
}
// 执行命令
ctx := context.Background()
out, err := cmd.CombinedOutput()
if err != nil {
logger.Error(ctx, "go build err", map[string]interface{}{
"err": err,
"out": string(out),
})
return err
}
logger.Info(ctx, "编译成功", nil)
同时除了生成二进制文件,还要记得把.env文件(如果有的话)、config目标文件传递到本地的部署目录:
// 复制.env
if util.Exists(filepath.Join(appService.BaseFolder(), ".env")) {
if err := util.CopyFile(filepath.Join(appService.BaseFolder(), ".env"), filepath.Join(deployFolder, ".env")); err != nil {
return err
}
}
// 复制config文件
deployConfigFolder := filepath.Join(deployFolder, "config", env)
if !util.Exists(deployConfigFolder) {
if err := os.MkdirAll(deployConfigFolder, os.ModePerm); err != nil {
return err
}
}
if err := util.CopyFolder(filepath.Join(appService.ConfigFolder(), env), deployConfigFolder); err != nil {
return err
}
自动化部署后端的命令,除了以上的编译文件到部署目录之外,其他部分都和自动化部署前端的命令一致:
// deployBackendCommand 部署后端
var deployBackendCommand = &cobra.Command{
Use: "backend",
Short: "部署后端",
RunE: func(c *cobra.Command, args []string) error {
container := c.GetContainer()
// 创建部署文件夹
deployFolder, err := createDeployFolder(container)
if err != nil {
return err
}
// 编译后端到部署文件夹
if err := deployBuildBackend(c, deployFolder); err != nil {
return err
}
// 上传部署文件夹并执行对应的shell
return deployUploadAction(deployFolder, container, "backend")
},
}
而对于同时部署前后端命令,其实就是在编译阶段,把前端和后端同时进行编译,并且最终上传部署文件夹。同样放在framework/command/deploy.go:
var deployAllCommand = &cobra.Command{
Use: "all",
Short: "全部部署",
RunE: func(c *cobra.Command, args []string) error {
container := c.GetContainer()
deployFolder, err := createDeployFolder(container)
if err != nil {
return err
}
// 编译前端
if err := deployBuildFrontend(c, deployFolder); err != nil {
return err
}
// 编译后端
if err := deployBuildBackend(c, deployFolder); err != nil {
return err
}
// 上传前端+后端,并执行对应的shell
return deployUploadAction(deployFolder, container, "all")
},
}
最后就是部署回滚操作,主要明确一下需要传递的参数:
一个是回滚版本号。这个版本号就是我们的部署目录的名称,前面说过部署目录为deploy/xxxxxx,xxxx设置为细化到秒的时间。比如20211110233354,表示是我们2021年11月10日23点33分54秒创建的版本。
另外一个就是标记希望回滚前端,还是后端,还是全部回滚。这里主要涉及执行前端的回滚命令,还是执行后端的回滚命令。
这两个参数我们直接以参数形式,跟在deploy rollback命令之后,如下:
./hade deploy rollback 20211110233354 backend
明确了参数,它的具体实现就很简单了,因为它没有任何的编译过程,我们只需要把回滚版本所在目录的编译结果,上传到目标服务器就可以了,同样,我们把这个命令放在framework/command/deploy.go中:
// deployRollbackCommand 部署回滚
var deployRollbackCommand = &cobra.Command{
Use: "rollback",
Short: "部署回滚",
RunE: func(c *cobra.Command, args []string) error {
container := c.GetContainer()
if len(args) != 2 {
return errors.New("参数错误,请按照参数进行回滚 ./hade deploy rollback [version] [frontend/backend/all]")
}
version := args[0]
end := args[1]
// 获取版本信息
appService := container.MustMake(contract.AppKey).(contract.App)
deployFolder := filepath.Join(appService.DeployFolder(), version)
// 上传部署文件夹并执行对应的shell
return deployUploadAction(deployFolder, container, end)
},
}
到这里四个自动化部署命令就都开发完成。我们来验证一下。
要验证部署命令,我们当然需要有一个目标部署服务器,这是我设置的web-01服务器配置,在config/development/ssh.yaml中:
timeout: 3s
network: tcp
web-01:
host: 111.222.333.444 # ip地址
port: 22 # 端口
username: yejianfeng # 用户名
password: "123456" # 密码
而在config/development/deploy.yaml中我的配置如下:
connections: # 要自动化部署的连接
- ssh.web-01
remote_folder: "/home/yejianfeng/coredemo/" # 远端的部署文件夹
frontend: # 前端部署配置
pre_action: # 部署前置命令
- "pwd"
post_action: # 部署后置命令
- "pwd"
backend: # 后端部署配置
goos: linux # 部署目标操作系统
goarch: amd64 # 部署目标cpu架构
pre_action: # 部署前置命令
- "rm /home/yejianfeng/coredemo/hade"
post_action: # 部署后置命令
- "chmod 777 /home/yejianfeng/coredemo/hade"
- "/home/yejianfeng/coredemo/hade app restart"
重点看后端部署配置。在部署后端之前,我们先运行一个rm 命令来将旧的hade二进制进程删除,然后部署后端文件,其中包括这个二进制进程。最后执行了两个命令,一个是chmod命令,保证上传上去的二进制进程命令可以执行;第二个就是./hade app restart命令,能将远端的命令启动。
这里就演示下部署后端服务 ./hade deploy backend
,输出结果如下:
我们看到,它成功地编译后端服务,到目标文件夹deploy/20211110233533, 并且上传了编译的hade命令,在远端启动了进程。
接着验证下回滚命令。在之前已经发布过版本 20211110233354 了。所以这里直接运行命令 ./hade deploy rollback 20211110233354 backend
将版本回滚到 20211110233354。
验证成功!
本节课我们对framework下的provider、contract、command目录都有修改。目录截图如下,供你对比查看,所有代码都已经上传到geekbang/28分支了。
今天我们实现了将代码自动化部署到Web服务器的机制。为了实现这个自动化部署,先实现了一个SSH服务,然后定制了一套自动化部署命令,包括部署前端、部署后端、部署全部和部署回滚。
虽然说这个由框架负责的自动化部署机制在大项目中可能用不上,毕竟现在大项目都采用Docker化和k8s部署了。不过对于小型项目,这种部署机制还是有其便利性的。所以我们的hade框架还是决定提供这个机制。
在实现这个机制的过程中,要做到熟练掌握Golang对于SSH、SFTP等库的操作。基本上这两个库的操作你熟悉了,就能在一个程序中同时自动化操作多个服务器了。在实际工作中,如果遇到类似的需求,可以按照这节课所展示的技术来自动化你的需求。
其实今天的内容涉及自动化运维的范畴了,我们就布置一个课外研究吧。自动化运维范畴中有一个很出名的自动化运维配置框架ansible,你可以去浏览下Ansible中文权威指南网站,学习一下ansible有哪些功能,分享一下你的学习心得。
欢迎在留言区分享你的思考。感谢你的收听,如果你觉得今天的内容对你有所帮助,也欢迎分享给你身边的朋友,邀请他一起学习。我们下节课见。