摆脱开发5分钟部署半小时的尴尬

前端自动化部署

本文参考自: 作者:yeyan1996 链接:https://juejin.cn/post/6845166890420011021 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

docker简介

开发5分钟,打包半小时, 早已是前端的痛点, 更着, 开发者自身环境的差异会导致最终的产物也有不同

docker 可以灵活的创建/销毁/管理多个“服务器”,这些“服务器”被称为 容器 (container)

在容器中你可以做任何服务器可以做的事,例如在有 node 环境的容器中运行 npm run build 打包项目,在有 nginx 环境的容器中部署项目,在有 mysql 环境的容器中做数据存储等等

一旦服务器安装了 docker ,就可以自由创建任意多的容器,上图中 docker 的 logo 形象的展示了它们之间的关系,🐳就是 docker,上面的一个个集装箱就是容器

本机安装docker

官方下载地址: Get Started with Docker | Docker

下载时最好顺手也注册一个dockerHub

我的电脑是windows, 安装完成后点击docker图标启动docker, 在终端输入docker, 看到如下输出则代表docker正常运行

image-20220127112024967

docker概念:

容器可以类比一个服务器, 镜像则是创建容器的模板, 一个docker镜像可以创建多个容器

有两种获取镜像的方式:

创建docker镜像

首先创建一个hello-docker目录, 在目录中创建index.htmlDockerfile文件

<!--index.html-->
<h1>Hello docker</h1>
# Dockerfile
FROM nginx
COPY index.html /usr/share/nginx/html/index.html
EXPOSE 80

在当前目录(项目目录)运行以下命令创建doker镜像

docker build . -t test-image:latest

创建完成后, 可以通过docker images命令查看所有镜像

image-20220127122327921

创建docker容器

镜像成功创建后, 运行以下命令可以创建一个docker容器

docker run -d -p 8081:80 –name test-container test-image:latest

通过docker ps -a命令查看所有容器

image-20220127123344952

现在在本地浏览器输入: localhost:8081即可访问服务内容(即项目中的index.html)

image-20220127123445069

dockerHub

dockerhub是存储镜像的仓库, 开发者可以将 Dockerfile 生成的镜像上传到 dockerhub 来存储自定义镜像,也可以直接使用官方提供的镜像

image-20220127124048928

docker的好处

docker将环境统一起来, 保证生成环境和开发环境项目均能正常运行

开发者将开发环境用docker镜像上传到docker仓库, 在生成环境拉取并运行相同环境即可保持环境一直

docker也有版本控制, 在创建镜像时可以使用tag标记版本, 如果某个版本的环境有问题, 可以快速回滚到之前的版本

docker将项目构建需要的环境放在容器中, 与服务器隔离

容器创建和销毁都十分高效

高效的前端自动化部署

没有自动化的部署, 我们需要 npm run build生成构建产物(dist), 将dist文件上传到服务器, 同时还需要将代码提交到仓库(团队合作总要提交的吧).

实现自动化部署后, 我们要做的仅仅是git push提交代码到仓库, 其余均由脚本自动执行.

登录Linux云服务器

参考各大云服务器厂商官方文档, 附上腾讯云CentOS登录指南文档: 轻量应用服务器 使用远程登录软件登录 Linux 实例 - 操作指南 - 文档中心 - 腾讯云 (tencent.com)

我的是腾讯云CentOS 7.6 64位的操作系统, 学生购买云服务器有优惠, 应该是99/年

Linux服务器安装必要的系统工具

安装必要工具

sudo yum install -y yum-utils

添加软件软件源, 使用阿里云镜像

sudo yum-config-manager –add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

Linux安装docker

安装docker

sudo yum install docker-ce docker-ce-cli containerd.io

开启docker服务

sudo systemctl start docker

运行hello-world项目

sudo docker run hello-world

如果能够看到输出Hello from Docker!, 证明Docker安装成功

img

Linux安装git

用于从代码仓库拉取代码

yum install git

Linux安装nvm

前端自动化部署, 那当然处理逻辑是用js来写, node可以让js在服务端运行

nvm: 管理node版本

官方地址: nvm-sh/nvm:节点版本管理器 - 符合 POSIX 标准的 bash 脚本,用于管理多个主动节点.js版本 (github.com)

首先运行安装命令:

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash

安装时输出示例如下:

[root@VM-12-9-centos /]# curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 13226 100 13226 0 0 7281 0 0:00:01 0:00:01 –:–:– 7283 => Downloading nvm from git to ‘/root/.nvm’ => Cloning into ‘/root/.nvm’… remote: Enumerating objects: 278, done. remote: Counting objects: 100% (278/278), done. remote: Compressing objects: 100% (245/245), done. remote: Total 278 (delta 31), reused 101 (delta 20), pack-reused 0 Receiving objects: 100% (278/278), 142.25 KiB | 54.00 KiB/s, done. Resolving deltas: 100% (31/31), done. => Compressing and cleaning up git repository

=> nvm source string already in /root/.bashrc => Appending bash_completion source string to /root/.bashrc => Close and reopen your terminal to start using nvm or run the following to use it now:

export NVM_DIR=”$HOME/.nvm” [ -s “$NVM_DIR/nvm.sh” ] && . “$NVM_DIR/nvm.sh” # This loads nvm [ -s “$NVM_DIR/bash_completion” ] && . “$NVM_DIR/bash_completion” # This loads nvm bash_completion

程序自动地尝试将环境变量添加到正确的位置, 在此之后, 我们需要手动运行使能命令,让环境变量生效

source ~/.bashrc

验证是否安装成功

command -v nvm

正常输出示例:

[root@VM-12-9-centos /]# command -v nvm nvm

Linux安装node

下载、编译、安装最新版本的node: (本次操作执行此命令)

nvm install node # “node” is an alias for the latest version

如果需要安装特定版本的node, 请运行:

nvm install 14.7.0 # or 16.3.0, 12.22.1, etc

安装的第一个版本将成为默认版本。新 shell 将从节点的默认版本(例如) 开始。

其他命令:

Linux安装pm2

pm2可以让我们的js脚本在服务器后台运行

npm i pm2 -g

创建前端项目

本地创建一个简单的前端基础项目

vue create docker-test

创建完成后将demo上传到github (建议创建public共有仓库, 这样可以免去鉴权直接clone, 如果创建了private私有仓库, 在运行时需要输入密码, 代码运行时当然不希望这样, 解决办法是使用token, 参见: (37条消息) 【突发】解决remote: Support for password authentication was removed on August 13, 2021. Please use a perso_日积月累,天道酬勤-CSDN博客) , 接下来配置webhook

webhook

github仓库有一个hook(钩子), 它会在当前仓库触发某些事件时, 发送一个post形式的http请求

当仓库有提交代码时,通过将 webhook 请求地址指向云服务器 IP 地址,云服务器就能知道项目有更新,之后运行相关代码实现自动化部署

​ 参数主要涉及当前仓库和本地提交的信息,这里我们只用 repository.name 获取更新的仓库名即可

请求如何处理?

当我们的服务器收到项目更新后发送的post请求后, 需要创建/更新镜像来实现自动化部署

创建Dockerfile

在本地项目里新建一个Dockerfile文件, 用于之后创建镜像

# dockerfile
# build stage
FROM registry.cn-hangzhou.aliyuncs.com/dyjutil/node:v14.8.0 as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# production stage
FROM nginx:stable-alpine as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

通过scp命令, 将Dockerfile文件复制到云服务器上

scp ./Dockerfile root@121.5.110.8:/root

image-20220127131456618

创建.dockerignore

.dockerignore 可以在创建镜像复制文件时忽略复制某些文件

在本地项目里新建.dockerignore

# .dockerignore
node_modules

接着将.dockerignore文件也复制到云服务器上

scp ./.dockerignore root@121.5.110.8:/root

image-20220127132047601

创建http服务器并编写自动部署脚本

使用node来开启简单的http服务器处理webhook发送的post请求,脚本需要包含创建http服务器、拉取仓库代码、创建镜像和容器。

在本地项目新建index.js

const http = require("http");
const { execSync } = require("child_process");
const path = require("path");
const fs = require("fs");

// 递归删除目录
function deleteFolderRecursive(path) {
  if (fs.existsSync(path)) {
    fs.readdirSync(path).forEach(function (file) {
      const curPath = path + "/" + file;
      if (fs.statSync(curPath).isDirectory()) {
        // recurse
        deleteFolderRecursive(curPath);
      } else {
        // delete file
        fs.unlinkSync(curPath);
      }
    });
    fs.rmdirSync(path);
  }
}

const resolvePost = (req) =>
  new Promise((resolve) => {
    let chunk = "";
    req.on("data", (data) => {
      chunk += data;
    });
    req.on("end", () => {
      resolve(JSON.parse(chunk));
    });
  });

http
  .createServer(async (req, res) => {
    console.log("receive request");
    console.log(req.url);
    if (req.method === "POST" && req.url === "/") {
      const data = await resolvePost(req);
      const projectDir = path.resolve(`./${data.repository.name}`);
      deleteFolderRecursive(projectDir);

      // 拉取仓库最新代码  data.repository.name 即 webhook 中记录仓库名的属性
      execSync(
        `git clone https://github.com/wzc520pyfm/${data.repository.name}.git ${projectDir}`,
        {
          stdio: "inherit",
        }
      );
      // 复制 Dockerfile 到项目目录
      fs.copyFileSync(
        path.resolve(`./Dockerfile`),
        path.resolve(projectDir, "./Dockerfile")
      );

      // 复制 .dockerignore 到项目目录
      fs.copyFileSync(
        path.resolve(__dirname, `./.dockerignore`),
        path.resolve(projectDir, "./.dockerignore")
      );

      // 创建 docker 镜像
      execSync(`docker build . -t ${data.repository.name}-image:latest `, {
        stdio: "inherit",
        cwd: projectDir,
      });

      // 销毁 docker 容器
      execSync(
        `docker ps -a -f "name=^${data.repository.name}-container" --format="" | xargs -r docker stop | xargs -r docker rm`,
        {
          stdio: "inherit",
        }
      );

      // 创建 docker 容器  -- 这里使用了服务器的8888端口
      execSync(
        `docker run -d -p 8888:80 --name ${data.repository.name}-container  ${data.repository.name}-image:latest`,
        {
          stdio: "inherit",
        }
      );

      console.log("deploy success");
    }
    res.end("ok");
  })
  .listen(3000, () => {
    console.log("server is ready");
  });

在销毁 docker 容器部分用到了 linux 的管道运算符和 xargs 命令,过滤出以 docker-test 开头容器(用 docker-test 仓库的代码制作的镜像创建的容器),停止,删除并重新创建它们

最后, 通过scp将index上传到云服务器上

scp ./index.js root@121.5.110.8:/root

image-20220127154500978

现在, 我们的项目结构为:

image-20220127204833035

云服务器上使用pm2运行index.js

pm2 start index.js

image-20220127160209979

小插曲

因为我们需要通过服务器的8888端口访问部署的项目, 仓库需要通过服务器3000端口通知服务器代码更新, 所以服务器需要放通8888和3000端口, 下面介绍腾讯云服务器放通端口的步骤:

  1. image-20220127204350525
  2. image-20220127204509146

接着就可以访问 http://服务器ip:8888 看到页面, 如果无响应, 尝试向仓库push一次代码, 查看pm2日志是否成功拉取代码并更新镜像, 如果出现网络问题无法拉取代码, 最好是改用gitee仓库, gitee的webhook配置访问与github一致.

image-20220127202846153

image-20220127202906803

接下来对项目中的App.vue稍作更改,并提交仓库, 测试自动化部署

image-20220127203640027

重新打开 http://服务器ip:8888

image-20220127203724224

可以看到内容已经更新

示例代码

wzc520pyfm/docker-test - 码云 - 开源中国 (gitee.com)

关注 Dockerfile ,.dockerignore, index.js 文件

距离真实环境仍有一定差距

上述 demo 只创建了单个 docker 容器,当项目更新时,由于容器需要经过销毁和创建的过程,会存在一段时间页面无法访问情况

而实际投入生产时一般会创建多个容器,并逐步更新每个容器,配合负载均衡将用户的请求映射到不同端口的容器上,确保线上的服务不会因为容器的更新而宕机

image-20200701210630305

另外基于 github 平台也有非常成熟的 CI/CD 工具,例如

通过 yml 配置文件,简化上文中注册 webhook 和编写更新容器的 index.js 脚本的步骤

# .travis.yml
language: node_js
node_js:
  - 8
branchs:
  only:
    - master
cache:
  directories:
    - node_modules
install:
  - yarn install
scripts:
  - yarn test
  - yarn build

另外随着环境的增多,容器也会逐渐增加,docker 也推出了更好管理多个容器的方式 docker-compose

img