一、DevOps

DevOps(Development和Operations的组合词)是一种重视“软件开发人员(Dev)”和“IT运维技术人员(Ops)”之间沟通合作的文化、运动或惯例。透过自动化“软件交付”和“架构变更”的流程,来使得构建、测试、发布软件能够更加地快捷、频繁和可靠。

通常一个采用 Node 作为服务端的前后端项目技术架构图如下图所示:
WX20200531-075018.png
之前我们搭建了一个Jenkins自动化平台,这个自动化平台最终目的是要将我们的项目发布出去,这个时候就需借用 Docker 容器技术。

为什么使用 Docker 容器技术?**
最简单解释是 Docker 容器技术可以统一我们的发布环境,而且结合 Dockerfile 这个配置文件,可以对容器进行高度的配置。所以需了解 Dockerfile 这个配置文件,以便产生对应镜像。

二、配置前后端项目的 Dockerfile

2.1 配置前端项目

2.1.1 编写前端项目的Dockerfile 文件

  1. # 第一部分:构建(build)
  2. # FROM 代表有一个基础镜像
  3. FROM node:10 as build-stage
  4. # 用来标识谁在维护这个项目
  5. LABEL maintainer=bgl_cumt_zju@163.com
  6. # 创建一个工作目录
  7. WORKDIR /app
  8. # 将当前目录下的所有文件或资源复制到镜像中。
  9. # 注:针对项目中的 node_modules 或 dist 目录等不需要拷贝到镜像的文件,可以借助.dockerignore
  10. COPY . .
  11. # 使用淘宝的镜像源进行加速
  12. RUN yarn install --registry=https://registry.npm.taobao.org
  13. RUN yarn build
  14. # 第二部分:生产发布(production)
  15. FROM nginx:stable-alpine as production-stage
  16. # 将构建后的内容复制到 web 容器中的 html 目录,这里采用的 web 容器是 Nginx
  17. COPY --from=build-stage /app/dist /usr/share/nginx/html
  18. # EXPOSE 相当于暴露镜像的某个端口。
  19. # 这里暴露一些服务端口,以便在宿主机去运行这个镜像的时候可以将其映射到宿主机上的端口
  20. EXPOSE 80
  21. # 运行 nginx。
  22. # CMD 用于执行脚本。相当于镜像运行起来之后,它就会执行CMD后的命令。命令通过数组的方式拼接起来。
  23. CMD ["nginx", "-g", "daemon off;"]

Tips:

  • 在 Visual Code 中要想让 Dockerfile文件语法高亮,可以安装下 Docker 插件;
  • nginx 命令后面为什么要加上 daemon off 参数呢?在 nginx Docker 的官方网页上有一句话:

    If you add a custom CMD in the Dockerfile, be sure to include -g daemon off; in the CMD in order for nginx to stay in the foreground, so that Docker can track the process properly (otherwise your container will stop immediately after starting)!

加上了 daemon off ,nginx 才能一直在后台持续运行,否则就会被docker进程终止,因为docker默认会终止pid为1的进程。

Docker 容器启动时,默认会把容器内部第一个进程,也就是 pid=1 的程序,作为 Docker 容器是否正在运行的依据,如果 Docker 容器 pid=1 的进程挂了,那么 Docker 容器便会直接退出。 Docker 未执行自定义的 CMD 之前,nginx 的 pid 是1,执行到CMD之后,nginx就在后台运行,bash或sh脚本的pid变成了1。 所以一旦执行完自定义CMD,Nginx容器也就退出了。

2.1.2 编写 .dockerignore 文件

针对项目中的 node_modulesdist 目录等不需要拷贝到镜像的文件,就可以借助 .dockerignore 文件。常规配置如下:

  1. # Dependency directory
  2. node_modules
  3. .DS_Store
  4. dist
  5. # node-waf configuration
  6. .lock-wscript
  7. # Compiled binary addons
  8. build/Release
  9. .dockerignore
  10. Dockerfile
  11. *docker-compose*
  12. # logs
  13. logs
  14. *.log
  15. # Runtime data
  16. .idea
  17. .vscode
  18. *.suo
  19. pids
  20. *.pid
  21. .git
  22. .hg
  23. .svn

2.2 配置 Node 服务端

2.2.1 编写服务端的 Dockerfile 文件

  1. # FROM 代表有一个基础镜像
  2. FROM node:10
  3. # 用来标识谁在维护这个项目
  4. LABEL maintainer=bgl_cumt_zju@163.com
  5. # 创建一个工作目录
  6. WORKDIR /app
  7. # 将当前目录下的所有文件或资源复制到镜像中。
  8. # 注:针对项目中的 node_modules 或 dist 目录等不需要拷贝到镜像的文件,可以借助.dockerignore
  9. COPY . .
  10. # 使用淘宝的镜像源进行加速
  11. RUN yarn install --registry=https://registry.npm.taobao.org
  12. # 产生 dist 目录,以及 server.bundle.js
  13. # RUN yarn build
  14. # EXPOSE 相当于暴露镜像的某个端口。
  15. # 这里暴露一些服务端口,以便在宿主机去运行这个镜像的时候可以将其映射到宿主机上的端口
  16. EXPOSE 3000
  17. # 将 app 目录下的 public 目录挂载出来
  18. VOLUME [ "/app/public" ]
  19. CMD ["node", "src/index.js"]

2.2.2 编写.dockerignore 文件

服务端的 .dockerignore 文件参照 2.1.2

三、本地测试

编写完项目的 Dockfile 文件后,那么就带来了这样一个问题:即如何确保编写的 Dockerfile 文件能在线上被打包成一个镜像,并且里面的服务也能正常运行?针对这一问题,我们可以进行本地测试

3.1 使用 Dockerfile 创建镜像

拿前端项目为例,运行如下命令:

docker build -t web:1.0 .

其中:

  • -t:tag 缩写,相当于打了一个标签;
  • web:1.0 :表示镜像名称;
  • . :表示选择当前工程目录下的 Dockerfile 文件;

注意:
如果在执行上述命令的过程中,Docker 下载镜像比较慢,则需添加镜像加速地址

  1. 要想使用阿里云的镜像加速器进行快速拉取镜像,则需注册阿里云账号,具体过程参照文章《加速拉取镜像到本地——用阿里云》;
  2. 获得镜像加速器地址后,针对安装了Docker for Mac的用户,可参考以下配置步骤:在任务栏点击 Docker Desktop 应用图标 -> Perferences,在顶部导航栏中选择 Daemon,将镜像加速地址添加到”registry-mirrors”的数组里,点击 Apply & Restart按钮,等待Docker重启并应用配置的镜像加速器。

image.png

3.2 运行镜像

先使用 docker images 命令查看web镜像是否成功生成。

  1. log-admin git:(master) docker images
  2. REPOSITORY TAG IMAGE ID CREATED SIZE
  3. web 1.0 0560da9f3ed6 46 seconds ago 21.6MB
  4. <none> <none> 5b19fbaa2681 About a minute ago 1.75GB
  5. node 10 c5f1efe092a0 2 weeks ago 912MB
  6. nginx stable-alpine ab94f84cc474 5 weeks ago 21.3MB
  7. html latest 5e98cd063517 7 months ago 126MB
  8. my-app latest 2c1ab9afc31e 7 months ago 913MB

可以看出web镜像大小是21.6M,是非常小的,而且这里面还包含了 Nginx 服务。接下来通过 docker run 命令将刚才创建的 web 1.0 镜像运行起来,具体命令如下:

docker run -itd —name web -p 11000:80 web:1.0

  • 命令 --name 用于指定名称;
  • 命令 -p 将容器80端口映射到宿主机的11000端口;

在浏览器窗口,输入 localhost:11000 ,不出意外的话,就可以看到我们的前端项目。若想查看web容器的日志信息,可以通过 docker logs 命令:

docker logs -f web

四、配置 Jenkins 的自动化任务

4.1 配置 GitLab

参照《搭建 Jenkins 与 GitLab 的持续集成环境》,配置前后端两个项目代码的 Deploy Keys,如下图所示。
image.png
接下来配置前后端两个项目的 Webhooks,这个时候需要在 Jenkins 那边新建两个任务,详细过程继续参照《搭建 Jenkins 与 GitLab 的持续集成环境》。注意: 一般来说,不同的任务不同的仓库密钥不一样。

4.2 配置自动化部署

4.2.1 配置构建参数

配置前端管理项目在构建过程中所需的参数,具体操作如下图所示:
企业微信截图_bb659f9d-2d73-44ab-a3c0-dfdf399fd4f2.png
参照上图操作,依次完成参数 container_namecontainer_portimage_nametag 等参数配置,默认值如下图所示。
WX20200601-195053.png

4.2.2 配置构建操作

配置前端管理项目的构建操作,选择 执行 Shell ,输入如下脚本:

  1. #!/bin/bash
  2. # 定义变量
  3. CONTAINER=${container_name}
  4. PORT=${container_port}
  5. # 完成镜像构建
  6. # --no-cache表示构建过程中不需要缓存构建的文件,以保障每次构建都是最新的文件
  7. docker build --no-cache -t ${image_name}:${tag} .
  8. # docker inspect 用于查看docker对象的底层基础信息,这里面包含了容器的运行状态。
  9. # --format 表示传入go template,格式化输出
  10. # 系统变量{{.}}:其中点号表示当前对象及上下文。可以通过{{.}}获取当前对象
  11. RUNNING=${docker inspect --format='{{ .State.Running}}' $CONTAINER 2> /dev/null}
  12. # 条件判断:判断镜像是否存在
  13. # 其中: -n 用来判定字符串非空
  14. if [ ! -n $RUNNING ]; then
  15. echo "$CONTAINER does not exit"
  16. return 1
  17. fi
  18. # 进行资源回收
  19. if [ "$RUNNING" == "false" ]; then
  20. echo "$CONTAINER is not running."
  21. return 2
  22. else
  23. echo "$CONTAINER is running"
  24. # 停止相同名称的容器
  25. matchingStarted=$(docker ps --filter="name=$CONTAINER" -q | xargs)
  26. if [ -n $matchingStarted ]; then
  27. docker stop $matchingStarted
  28. fi
  29. # 删除相同名称的容器
  30. matching=$(docker ps -a --filter="name=$CONTAINER" -q | xargs)
  31. if [ -n $matching ]; then
  32. docker rm $matching
  33. fi
  34. fi
  35. echo "RUNNING is ${RUNNING}"
  36. #运行我们的镜像
  37. docker run -itd --name $CONTAINER -p $PORT:80 ${image_name}:${tag}

其中:

  • 2> /dev/null 代表忽略掉错误提示信息,其中 /dev/null 是一个特殊的设备文件,这个文件接收到的任何数据都会被丢弃。 2 是标准错误(stderr);
  • docker ps -q 命令,其中 -q 表示只展示容器ID;
  • xargs 命令的作用,是将标准输入转为命令行参数;
    1. echo "hello world" | xargs echo
    2. hello world
    上面的代码将管道左侧的标准输入,转为命令行参数 hello world ,传给第二个 echo 命令。

配置 Node 服务端的Shell脚本也是同样操作,只需修改下相应端口号,即将 $PORT:80 改成 $PORT: 端口号 即可。

注:
在配置过程中报如下错误

Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get http://%2Fvar%2Frun%2Fdocker.sock/v1.40/containers/json?filters=%7B%22name%22%3A%7B%22log_admin%22%3Atrue%7D%7D: dial unix /var/run/docker.sock: connect: permission denied

造成这一问题的主要原因是之前配置 Jenkins 服务的 docker-compose.yml 文件,在指定 user 时需指定自己的用户名,由于是 root 全局的,那么 Jenkins 中的 user 必须是 user:root。具体修改操作如下所示:
原先内容:

  1. user: jenkins:994

修改内容:

  1. user: root:994