前言:本文中介绍的自动化部署,需要用到服务器以及docker容器的相关知识。因为用到的相关技术在之前的博客中已经介绍过了,在本文里就直接使用不再赘述,感兴趣的可以在阅读本文前看下我之前写的文章:
如何将项目部署到服务器:https://blog.csdn.net/yangxbai/article/details/125439694?spm=1001.2014.3001.5501
什么是docker以及docker的简单应用:https://blog.csdn.net/yangxbai/article/details/126557983?spm=1001.2014.3001.5501
前端的项目部署,是将本地项目打包上线发布到服务器的行为,部署上线后可以通过公网ip或者域名进行访问。
自动化部署是前端工程化的一部分,它包含持续集成,自动化构建,自动部署。
如果不使用自动化部署的话,前端部署上线的流程是:
本地打包 => 通过ftp工具链接到线上服务器 => 将打包后的文件拖动到服务器的项目目录下。
这个流程看似简单,其实还是比较的麻烦的,而且在移动项目文件的时候还容易放错文件目录,造成一系列问题,那么有没有什么办法能够规避掉这些问题呢?那就是自动化部署,只需要配置一次,在之后项目上线的时候,我们只用将代码合并到master分支,然后推送master分支到github仓库,服务器就自动构建打包,然后发布到线上。这样的话我们做的事情从头到尾就只有 “git push”,不仅简单方便避免了复杂的重复操作,还规避了一系列问题。
本文的自动化部署方案是用docker+webHook实现的,因此在具体开始之前先介绍一下webHook。
webHook是一个接收HTTP POST (GET,PUT,DELETE)的URL,一个实现了webHook的API提供商就是在事件发生的时候给配置的URL发送一条信息,信息里面可以包含具体的数据。
举个例子来说就是我们可以配置一个URL,当用户的某一个事件触发的时候,就会给设定的某个URL发送一条信息。
github的代码存储仓库就提供了这样的设置,它可以监听到我们推送了代码,然后给指定的URL发送一条信息。
既然这样的话,我们可以在服务器上启动一个简单的node服务器,这个node服务监听某个端口,然后github发送信息的URL地址就填写这个服务器的公网ip+这个端口。这样一来的话,我们推送了代码,webHook监听到了这一事件,就给我们服务器上的node服务发送信息,node服务监听到了信息来了,就自动进行打包构建发布的流程。那么具体怎么实现呢?
这里选择用vue脚手架直接创建一个项目:
vue create autopulish
创建好了后,我们去github创建一个仓库,然后将项目代码推送到github仓库里面去:
现在回到项目里面,我们创建一个auto.js文件,这个文件主要就是用来在服务器端启动一个node服务,可以在服务端拿到webHook发送的信息。
接着我们创建一个HTTP服务用来监听请求:
const http = require("http"); //http模块用来创建http服务
const createHandler = require("github-webhook-handler");//中间件,用来监听github的webHook的事件
const handler = createHandler({
path: "/webhook",
secret: "mySecretKiKoHashKey",
});//创建实例,path和secret是自定义的,后面在github上配置时需要保持一致
http.createServer(function (req, res) {
handler(req, res, function () {
res.statusCode = 200;
});
}).listen(8082);//创建http服务并且监听8082端口
handler.on("push", function (event) {
console.log(event)
})//监听github仓库的push事件
创建好了auto.js脚本后我们需要将这个脚本放到服务器上面去,在此之前的文章里已经教过大家如何申请一个云服务器,此处就不多说了。由于我之前买的阿里云的服务器过期了,因此现在我自己又申请了一个腾讯云的服务器。大概是这个样子:
申请好了云服务器后登录上远程的控制台(如何登录前面的文章讲过了),在服务器的根目录下创建一个目录,名字就叫auto吧:
cd /
mkdir auto
可以看到文件已经创建好了,之后这个文件夹就将作为我们服务器上存放项目以及执行脚本存放的文件夹
利用scp命令将文件auto.js上传到服务器的auto文件夹里面
scp ./auto.js root@150.158.12.5:/auto
要运行js脚本需要用到node,但是服务器上一开始是没有node的,因此我们要先在服务器上安装node,在服务器上执行以下命令:
cd /usr/local/
//下载14.13.1版本的node
wget https://npm.taobao.org/mirrors/node/v14.13.1/node-v14.13.1-linux-x64.tar.xz
//解压node
tar -xvf node-v14.13.1-linux-x64.tar.xz
瓦特?说好的node -v会显示node的版本号呢?为什么还是不行呢。先别急,大家想一下在自己的windows电脑上安装了node后是不是要配置环境变量呢,目的是在终端直接输入node的时候,系统知道该去哪里找node。服务器本质上就是一台电脑,因此要能够在服务器上直接使用node需要建立软连接,使其变为全局,也就是类似于windows操作系统的环境变量配置。那么如何为node建立软连接呢?执行以下命令:
ln -s /usr/local/node-v14.13.1-linux-x64/bin/node /usr/local/bin/
ln -s /usr/local/node-v14.13.1-linux-x64/bin/npm /usr/local/bin/
成功!
可以看到报错了,因为没有下载相应的npm包,所以我们在文件夹里面安装一下需要的两个依赖包:
npm i http
npm i github-webhook-handler
可以看到报错已经不见了
由于我们的脚本监听了8082端口,因此我们的服务器就需要开启8082端口,不然可能造成访问端口被防火墙阻止的问题,我们来到服务器的控制台,点击管理规则。
点击添加规则:
脚本运行起来端口也开放后,我们需要去项目的github仓库配置webHook,点击setting
点击Add webHook
我们在脚本里写了监听push事件,并且打印传递来的信息
现在我们向github仓库推送一下代码,看一下能不能打印出东西,如果能够打印出东西的话说明到目前为止,在服务器端监听push事件的操作已经成功了(注意auto.js不要推送到仓库去,他只是我们放到服务器的脚本,和项目本身没有关系):
可以看到服务器的控制台打印了一大堆的东西,里面有这次事件的信息,包括触发方式(event),仓库的名字等。
前面的文章已经讲过怎么创建一个最简单的docker容器了,那么为什么自动化部署的时候要用到docker呢,有人会说,我可以将环境装在服务器上,为什么还要多此一举来创建镜像,然后放在一个个容器里面呢?我在这里列举使用docker的几个优点:
docker的出现解决了一个世纪难题,那就是 “在我的电脑上明明是好的” ,docker可以将本地开发的环境和项目一起打包成一个镜像,然后用镜像创建容器,那么这个容器里就是我本地开发时的环境,能够保证容器内部的环境和本地开发的环境保持完全的一致,从而避免环境不同造成的问题,真正的做到 build once run anywhere。
环境隔离,docker容器采用沙箱机制,相互之间完全隔离并且不会存在任何接口,也就是说容器和容器之间是完全相互独立的。
让你的服务器环境更干净,假如项目1需要node环境,那么我只需要在这个容器内部拉取node的镜像。项目2需要另外一个环境,也只需要拉取对应的镜像到容器内部就好了。不需要把什么乱七八糟的东西和环境全部往服务器上面放。
之前的文章介绍了在本地安装docker,现在到了服务器上依旧要装docekr,但是步骤有点不一样了,因此我还是讲解一下,首先要安装docker运行所需要的环境以及工具:
//安装必要的一些系统工具
sudo yum install -y yum-utils
//使用阿里云镜像
sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
//安装 docker-ce
sudo yum install docker-ce docker-ce-cli containerd.io
//开启 docker服务
sudo systemctl start docker
运行完后使用命令查看docker是否安装成功:
docker -v
# dockerfile
# build stage
FROM node:14.13.1 as build-step
WORKDIR /auto/app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# production stage
FROM nginx:stable-alpine as production-step
COPY --from=build-step /auto/app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
这里是用了多阶段构建的技巧,将构建分为两个阶段,第一阶段基于node镜像,第二部分基于nginx镜像。
基于node的14.13.1版本镜像,并通过构建阶段命名,将此阶段命名为build-step,这里为什么要用14.13.1版本是因为之前讲过,docker可以将本地环境打包成镜像,我的本地项目开发时运行的环境的node版本是14.13.1,因此我拉取的node镜像的版本也是14.13.1,保证开发的环境和上线时的环境保持一致。
FROM node:14.13.1 as build-step
将工作区设置为/auto/app,如果没有的话会自动创建
WORKDIR /auto/app
将package.json以及package-lock.json复制到容器的工作区内
COPY package*.json ./
执行npm install命令安装依赖
RUN npm install
拷贝其他的目录文件到工作区内
COPY . .
执行命令打包项目
RUN npm run build
拉取stable-alpine版本的nginx镜像,并将此构建阶段命名为production-step
FROM nginx:stable-alpine as production-step
将build-step阶段的产物复制到/usr/share/nginx/html目录下,为什么要复制到这个目录下,是因为容器内部的nginx的默认指向地址就是/usr/share/nginx/html
COPY --from=build-step /auto/app/dist /usr/share/nginx/html
声明容器暴露出80端口,但是这个地方只是做声明让人来看的,真正映射的端口需要在创建容器的时候进行发布。
EXPOSE 80
容器创建时运行nginx -g daemon off 命令,通过 daemon off 让 nginx 一直在前台运行
CMD ["nginx", "-g", "daemon off;"]
scp ./Dockerfile root@150.158.12.5:/auto
类似于.gitignore文件,代表忽略什么文件,在Dockerfile进行操作创建镜像的时候会忽略这个文件。此处我们要忽略什么文件呢?我们看到在Dockerfile里面有一步操作是拷贝其他的目录文件到工作区内,其实我们在拷贝的时候是不需要管node_modules的,为了保证每次构建时依赖都是最新的,所以每次都是把package.json复制到工作区重新去下载依赖,因此在执行 copy . .的时候,要将node_modules忽略掉
然后使用命令将.dockerignore文件上传到服务器
scp ./.dockerignore root@150.158.12.5:/auto
目前我们完成的事情有:
1.服务端的脚本可以监听到push事件了
2.创建好了Dockerfile
接下来需要完成的就是自动化构建了,此方案的自动化构建的流程大致如下:
1.我们在本地推代码
2.服务端的脚本监听到了推送事件
3.拉取仓库的项目到服务器
4.将Dockerfile和.dockerignore文件复制到拉取的项目里,运行docker。Dockerfile将项目里的除node_modules的文件复制到工作区下载依赖,创建镜像(这个镜像包含了项目以及本地的node环境)
5.通过创建的镜像来创建容器并运行
接下来就一步步的来完成自动化构建的剩余步骤,第一二步我们目前已经完成了,我们来完成第三步,首先完善auto.js脚本
需要注意的是,这里我拉取代码使用的是SSH Key的方式,这种方式需要在服务器上配置SSH Key,这个东西只需要配置一次,网络上教程很多,也不复杂,我就不赘述了,大家不会的可以自行百度一下。
const http = require("http"); //http模块用来创建http服务
const createHandler = require("github-webhook-handler"); //中间件,用来监听github的webHook的事件
const path = require("path");
const fs = require("fs");
const { execSync } = require("child_process");
const handler = createHandler({
path: "/webhook",
secret: "mySecretKiKoHashKey",
}); //创建实例,path和secret是自定义的,后面在github上配置时需要保持一致
http.createServer(function (req, res) {
handler(req, res, function () {
res.statusCode = 200;
});
}).listen(8082); //创建http服务并且监听8082端口
handler.on("push", function (event) {
const projectDir = path.resolve(`./${event.payload.repository.name}`);
//拉取项目前先删除一次项目
if (fs.existsSync(projectDir)) {
execSync(`rm -rf ${event.payload.repository.name}`, {
stdio: "inherit",
});
}
// 拉取仓库最新代码
execSync(
`git clone [email protected]:YoungDan-hero/autopulish.git ${projectDir}`,
{
stdio: "inherit",
}
);
}); //监听github仓库的push事件
上传auto.js到服务器,然后下载相应的依赖
npm i path
npm i fs
npm i child_process
然我我们推一次代码看一下能不能拉取仓库里的项目到服务器(记住Dockerfile,auto.js,.dockerignore都是不需要随项目推到仓库的)
可以看到是可以拉取项目到本地的,这一步到这里就结束了。
// 复制 Dockerfile 到项目目录
fs.copyFileSync(
path.resolve(`./Dockerfile`),
path.resolve(projectDir, "./Dockerfile")
);
// 复制 .dockerignore 到项目目录
fs.copyFileSync(
path.resolve(`./.dockerignore`),
path.resolve(projectDir, "./.dockerignore")
);
// 创建docker镜像 并将这个镜像标记为最新版本
execSync(
`docker build . -t ${event.payload.repository.name}-image:latest `,
{
stdio: "inherit",
cwd: projectDir,
}
);
// 销毁 docker 容器 在创建容器前先销毁上一次创建的容器
execSync(
`docker ps -a -f "name=^${event.payload.repository.name}-container" --format="{{.Names}}" | xargs -r docker stop | xargs -r docker rm`,
{
stdio: "inherit",
}
);
// 创建docker容器 映射服务器的9527端口到 docker容器的80端口
execSync(
`docker run -d -p 9527:80 --name ${event.payload.repository.name}-container ${event.payload.repository.name}-image:latest`,
{
stdio: "inherit",
}
);
const http = require("http"); //http模块用来创建http服务
const createHandler = require("github-webhook-handler"); //中间件,用来监听github的webHook的事件
const path = require("path");
const fs = require("fs");
const { execSync } = require("child_process");
const handler = createHandler({
path: "/webhook",
secret: "mySecretKiKoHashKey",
}); //创建实例,path和secret是自定义的,后面在github上配置时需要保持一致
http.createServer(function (req, res) {
handler(req, res, function () {
res.statusCode = 200;
});
}).listen(8082); //创建http服务并且监听8082端口
handler.on("push", function (event) {
const projectDir = path.resolve(`./${event.payload.repository.name}`);
//拉取项目前先删除一次项目
if (fs.existsSync(projectDir)) {
execSync(`rm -rf ${event.payload.repository.name}`, {
stdio: "inherit",
});
}
// 拉取仓库最新代码
execSync(
`git clone [email protected]:YoungDan-hero/autopulish.git ${projectDir}`,
{
stdio: "inherit",
}
);
// 复制 Dockerfile 到项目目录
fs.copyFileSync(
path.resolve(`./Dockerfile`),
path.resolve(projectDir, "./Dockerfile")
);
// 复制 .dockerignore 到项目目录
fs.copyFileSync(
path.resolve(`./.dockerignore`),
path.resolve(projectDir, "./.dockerignore")
);
// 创建docker镜像 并将这个镜像标记为最新版本
execSync(
`docker build . -t ${event.payload.repository.name}-image:latest `,
{
stdio: "inherit",
cwd: projectDir,
}
);
// 销毁 docker 容器 在创建容器前先销毁上一次创建的容器
execSync(
`docker ps -a -f "name=^${event.payload.repository.name}-container" --format="{{.Names}}" | xargs -r docker stop | xargs -r docker rm`,
{
stdio: "inherit",
}
);
// 创建 docker 容器 映射服务器的9527端口到 docker容器的80端口
execSync(
`docker run -d -p 9527:80 --name ${event.payload.repository.name}-container ${event.payload.repository.name}-image:latest`,
{
stdio: "inherit",
}
);
console.log("success");
}); //监听github仓库的push事件
上传脚本到服务器并再次推送代码测试
可以看到容器已经创建运行成功了,可以通过以下的命令来查看运行中的容器
docker container ls
那么容器创建成功后怎么在浏览器上访问项目呢,我们可以看到在创建容器的时候做了端口的映射,访问服务器的9527端口就可以映射到容器的80端口,就可以访问容器了,我们首先根据之前的步骤开放服务器的9527端口:
然后我们在浏览器上通过公网ip以及端口号查看一下项目能正常访问吗:
可以看到项目已经可以访问了。然后我们在本地改一下代码推送一下,看能正常构建吗:
到目前为止我们已经实现了推送代码自动构建打包,但是有两点需要注意:
1.服务器退出终端时会自动终止脚本服务,我们不可能每次都一直把服务器的终端开着。
2.在推送任何分支的情况下都会进行打包,但是我们只需要在推送主分支的情况下打包。
安装pm2
npm install pm2 -g
安装完后运行报错
-bash: pm2: command not found
这和node报错一样,也需要配置软连接,首先找到pm2的安装目录
find / -name pm2
选择第一个作为软连接路径:
ln -s /usr/local/node-v14.13.1-linux-x64/lib/node_modules/pm2 /usr/local/bin
然后我们找到我们的脚本执行:
pm2 start auto.js
可以看到当前脚本已经启动,并且在关掉服务器终端的时候也会一直运行在后台。当监听到我们的push事件的时候,就会自动帮我们构建打包了。是不是很方便?
前面说过,监听事件的时候可以拿到webHook发的信息,其中里面包含了分支的信息,我们只需要判断当前是否是主分支,只在主分支的情况下执行打包构建的操作,修改一下auto.js脚本的代码:
handler.on("push", function (event) {
if(event.payload.ref==='refs/heads/main'){
}
})
然后我们再在主分支上改一下代码推送:
至此,webHook+docker的前端自动化部署的流程就结束了,当然这个方案只是自动化部署的其中一个方案,比较常见的还有,利用Jenkins进行自动化构建,后面可以再写一篇文章来介绍一下。当前的这个方案还有比较多的可以优化的地方,最典型的就是当项目更新时,由于容器需要经过销毁和创建的过程,这个过程需要一定的时间,在这段时间里会存在页面无法访问的情况,而在实际的生产环境中一般会创建多个容器,并逐步更新每个容器,配合负载均衡将用户的请求映射到不同的端口上,确保线上的服务不会因为容器的更新造成对用户的影响。