自动化部署(Jenkins, Ansible,Bash)

使用 Jenkins,Ansible,Bash 自动化部署的流程。

pineline

前言

前端项目中有这么几个不直接服务于 UI 的文件、目录,主要就是用于自动部署的:

  • /ansible
  • /scripts
  • build.sh
  • Dockerfile
  • entrypoint.sh
  • nginx.conf
  • .gitlab-ci.yml

示例中对脚本进行了优化,所以会跟源码稍有不同。

PS: 代码相关的内容会分为代码详细注释和总结执行结果。

入口

开发环境一般用的是 gitlab ci/cd,查看 .gitlab-ci.yml 中 deploy 阶段可以看到其获取代码后执行的是 ./scripts/deploy.sh 脚本。
./scripts/deploy.sh 脚本内容和 Jenkins 部署流程中的脚本类似,所以下面主要展开看 Jenkins 的部署流程。

有权限的话可以查看 Jenkins 的配置,下面仅截取关键步骤。

第一步是先将指定分支的代码克隆到指定位置,然后执行以下脚本:

sh "sed -i  's/10.12.78.47/hostName.com/g' ./nginx.conf"
sh "./build.sh && cd ansible && ansible-playbook -i '10.xx.xx.xx,10.xx.xx.xx' display.yml -e role=prado_fe --extra-vars \"ListenPort=9000\""

这两条命令就是部署的全过程,下面会详细展开说这两条命令的执行。

第一行命令

sh "sed -i 's/10.12.78.47/hostName.com/g' ./nginx.conf"

涉及的关键字:
sed -> stream editor
-i -> 直接修改读取的文件内容
语法:sed -i ‘s/原字符串/新字符串/g’ file (g 在正则中为全局替换)

所以第一行的命令翻译过来就是 nginx.conf 文件中的 10.12.78.47 全部替换为 hostName.com

查看项目中的 nginx.config 可以知道修改的是 proxy_pass 的内容:

server {
  location /api {
    proxy_pass http://10.12.78.47; // 改为 http://hostName.com
  }
}

总结

修改 nginx.conf 中 proxy_pass 字段的内容。

第二行命令

sh "./build.sh && cd ansible && ansible-playbook ..."
  • 执行 build.sh 脚本
  • 进入到 ansible 目录执行 ansible-playbook

下面分别展开看这两步的执行流程

build.sh

#!/bin/bash
# 定义变量 IMAGES_NAME
IMAGES_NAME="prado_ui:latest"
# 打印环境变量 ENV, 可以在 Jenkins 的构建参数中看到 ENV 为 live
echo $ENV
# 使用当前目录下的 Dockerfile 构建定制的镜像
docker build --build-arg ENV -t $IMAGES_NAME .
# $? 是 shell 变量,表示最后一次执行命令的状态,0为成功,非0为失败
# -ne -> 不等于
if [ $? -ne 0 ]; then
        echo "build dot ui images failed"
        exit 1
# fi 为 if 语句的结尾
fi
# 构建完镜像后,将镜像保存到 ./ansible/role/prado_fe/files 目录下
cd ./ansible/role/prado_fe/files && docker save $IMAGES_NAME > prado_ui.tar && cd ..

总结

执行 docker build 构建镜像,生成镜像后将镜像保存在 ansible/role/prado_fe/files 目录下,文件名为 prado_ui.tar。

执行 docker build 默认会读取目录下的 DOCKERFILE 进行构建,所以接着查看 Dockerfile 了解构建的镜像内容。

Dockerfile

有两个 From,多阶段编译,只保留最后的一个阶段的结果

# 阶段一
# 基础镜像包含node。后续阶段可以通过 builder 获取该阶段的结果
FROM xxx.com/prado-user-images/node:latest AS builder
# 指定后续 COPY 和 RUN 的工作路径
WORKDIR /usr/src/app
# 将项目的依赖文件复制到包中
COPY package.json yarn.lock ./
# 安装依赖
RUN yarn
# 将跟 Dockerfile 同目录的所有内容(即项目源码)复制到包中
COPY . ./
# ARG ENV="dev" 可以被命令中的 --build-arg ENV 覆盖为 live,仅在DOCKERFILE中生效
ARG ENV="dev"
# 构建目标文件
RUN yarn build:$ENV
# 清除超大的依赖,这步是必须的。虽然阶段1并不会保留到最终镜像内,但是docker会创建这一步build的缓存,硬盘空间很快就会不够用
RUN rm -rf ./node_modules/

# 阶段二
# 基础镜像包含nginx
FROM hostName.com/prado-user-images/nginx:alpine
# 将阶段一中 /usr/src/app/dist 目录下的文件(也就是构建产物)复制到当前镜像的 /usr/share/nginx/html 路径下
COPY --from=builder /usr/src/app/dist /usr/share/nginx/html
# 将 nginx.conf 和 entrypoint.sh 复制到镜像中
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY entrypoint.sh ./
# 定义环境变量 DOMAIN 和 PORT,容器运行时生效
ENV DOMAIN "test-hostName.com"
ENV PORT 80
# 容器启动时执行 sh ./entrypoint.sh
CMD ["sh","./entrypoint.sh"]

总结

最终的镜像主要就是包含 ngnix 和构建后的网站的静态文件。

build 命令执行完后,就是 ansible 命令相关的内容了。

ansible

ansible 自动化运维的框架,简单理解就是远程连接需要部署的主机并发送相关的命令,仅介绍他的执行和执行结果。

ansible-playbook -i '10.xx.xx.xx,10.xx.xx.xx' display.yml -e role=prado_fe --extra-vars \"ListenPort=9000\"

执行 display.yml 所定义的流程

-i hosts

-i 指定需要部署的主机 ip,默认会读取 ansible/hosts 文件,此处是直接指定两台主机的 ip

-e –extra-vars

都是对 yml 文件和模板文件传参,分别传了 role=prado_fe 和 ListenPort=9000

ansible/display.yml

---
- name: Installing prado_agent
  gather_facts: no # 是否收集各机器的信息
  hosts: all
  become: yes # 需要root权限
  become_user: root # 需要的特权用户
  roles:
    - "{{ role }}" # role 被替换为 prado_fe
    #- { role: prado_agent }
    #- { role: change_config }

display.yml 总结

最后的执行的 roles 为 prado_fe,查看对应文件夹 ansible/role/prado_fe 下的文件执行

roles/prado_fe 目录解释

  • files 存放需要 copy 的文件,经过上面的 build.sh,会在该目录下生成 prado_ui.tar
  • tasks 用户存放一系列任务
  • handlers 空 无视
  • template 存放此 Role 需要使用的 jinjis2 模板文件

查看任务的的入口 tasks/main.yml

ansible/role/prado_fe/tasks/main.yml

---
- name: get tar
  # 将 prado_ui.tar 复制到远程主机的 /tmp/ 目录下
  copy:
    src: "{{ role_path }}/files/prado_ui.tar"
    dest: "/tmp/"

- name: get deploy_sh
  # 获取模板 deploy.j2,替换文件中的 {{ ListenPort }} 变量为9000后,保存到远程主机 /tmp/ 目录下的deploy_ui.sh文件中
  template:
    src: deploy.j2
    dest: /tmp/deploy_ui.sh

- name: start ui
  shell:
    # 在 tmp目录下执行命令
    chdir: "/tmp/"
    # 给予所有用户执行 deploy_ui.sh 的权限,并执行 deploy_ui.sh
    cmd: chmod a+x deploy_ui.sh && ./deploy_ui.sh
    # 保存执行结果到 checkStarted 中,暂无用
    register: checkStarted

main.yml 总结

将 prado_ui.tar 和 deploy_ui.sh(deploy.j2)复制到远程主机的 tmp 目录下并执行 deploy_ui.sh

所以接下来就是 deploy_ui.sh 的执行

ansible/role/prado_fe/templates/deploy.j2

deploy.j2 -> deploy.sh

#!/bin/bash
NAME="prado_ui"
BACK_NAME="prado_ui_backup"
IMAGES_NAME="prado_ui:latest"
IMAGES_NAME_BACKUP="prado_ui:backup"
# 先做一通操作,清除和备份上一次的运行容器、镜像,可用作回滚
# 停止运行中的容器 prado_ui
docker stop  $NAME
# 删除容器 prado_ui
docker rm    $NAME
# 删除镜像 prado_ui:backup
docker image rm $IMAGES_NAME_BACKUP
# 给 prado_ui:latest 打上 backup 标签
docker tag $IMAGES_NAME $IMAGES_NAME_BACKUP
# 删除镜像 prado_ui:latest
docker image rm $IMAGES_NAME
# 加载镜像 prado_ui.tar,也就是我们构建的那个镜像
docker load < prado_ui.tar
# 运行镜像 prado_ui:latest
# -itd  -it 提供可交互的伪终端,容器启动时需要执行shell脚本(entrypoint.sh),-d 保持在后台执行
# --name 指定容器名字为 prado_ui
# -p 将本地主机的 ListenPort 映射到容器的 80 端口,ListenPort在上一步知道被替换为9000
docker run -itd --name $NAME -p {{ ListenPort }}:80 $IMAGES_NAME

在 DOCKERFILE 一节可以知道,容器启动后会执行脚本 entrypoint.sh

entrypoint.sh

#!/usr/bin/env bash
# 侯勋所有bash命令返回的code如果不是0,脚本立即退出
set -e
# 定义DOMAIN和PORT,:=指定默认值,DOCKERFILE中已经定义为了 ENV DOMAIN "test-hostName.com" 和 ENV PORT 80
DOMAIN="${DOMAIN:-test-hostName.com}"
PORT="${PORT:-80}"

# 将nginx配置中的 port 改成 80,domain 改成 test-hostName.com
sed -i "s#{{port}}#${PORT}#g" /etc/nginx/conf.d/default.conf
sed -i "s#{{domain}}#${DOMAIN}#g" /etc/nginx/conf.d/default.conf
# 启动ngnix服务,ngnix默认为后台模式启动,使用daemon off改为前台进程,避免跟随脚本执行进程退出,导致容器退出
exec nginx -g "daemon off;"

最终启动的 ngnix 的配置为

server {
    listen       80;
    server_name  test-hostName.com;
    access_log   /dev/null;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        try_files $uri /index.html;
    }

    location /api {
         proxy_pass http://hostName.com;
    }
}

根据前面的配置,访问主机的 9000 端口,就能访问到这个容器中的 ngnix 在 80 端口的服务了

PS: 从域名到服务整个代理过程暂时黑盒,没搜到文档