2

背景

近期团队其他成员们在开发一个新项目,项目仓库初始化时还缺少 CD(持续部署)的内容,于是老师将这个 issue 分配给了我。

简述 CD

CD 全称为 Continuous Deployment ,翻译成中文是持续部署。说得再通俗一些就是自动打包发布。

我们的项目从开发环境到服务器上的生产环境之间,还有一个打包部署的步骤。 所以 gitlab 给我们提供了 CD 功能,方便我们自定义执行我们的自动打包和部署。

image.png

通过查阅官方文档可以得知,要配置 CD ,我们需要在项目根目录下创建 .gitlab-ci.yml 文件,并按它的语法编写脚本。

在脚本中我们可以设置执行时机、开放端口,甚至还可以通过捕获标签加以判断后再执行不同的操作等等。

更多相关内容请见官方文档

CD 的思想

在自己上手之前先是参考了其他项目的 CD ,进行了一些总结。

首先 CD 分为两个部分:build 和 deploy。

然后,配置自动打包的时机:当我们在 gitlab 网站上新建 Tag 时。所以,在要执行的部分( build 和 deploy )就应该有

rules:
    - if: $CI_COMMIT_TAG != null

新建 Tag 之后,服务器就会执行我们的脚本。见下图。

image.png

image.png

build配置

angular-build

angular-build:
  tags:
    - docker
  stage: build
  artifacts:
    paths:
      - dist
  cache:
    - key:
       files:
          - web/package-lock.json
      paths:
        - web/node_modules
  image: registry.cn-beijing.aliyuncs.com/mengyunzhi/node-chrome:18.16.0
  before_script:
    - cd web
  script:
    - env
    - npm install -dd
    - npm run build
    - mv dist/* ../dist
  rules:
    - if: $CI_COMMIT_TAG != null

spring-build

spring-build:
  tags:
    - docker
  stage: build
  artifacts:
    paths:
      - app.jar
  cache:
    - key:
       files:
          - api/pom.xml
      paths:
        - /root/.m2/repository
  image: registry.cn-beijing.aliyuncs.com/mengyunzhi/maven:3.6-openjdk-8-node
  before_script:
    - cd api
  script:
    - env
    - node -v
    - npm -v
    - mvn -v
    - mvn package -Dmaven.test.skip=true
    - mv target/*.jar ../app.jar
  rules:
    - if: $CI_COMMIT_TAG != null

deploy配置

variables

variables:
  # 工程名称唯一
  PROJECT_NAME: "${CI_PROJECT_TITLE}"
  DB_NAME: "xxx"
  DB_USER: "xxx"
  DB_PASSWORD: "xxx"
  DB_ROOT_PASSWORD: "xxx"
  HTTP_EXPORT_PORT: xxx
  DB_EXPORT_PORT: xxx

deploy

deploy:
  tags:
    - debian-cd-pro
  stage: deploy-cd
  dependencies:
    - spring-build
    - angular-build
  script:
    - ls -a -l
    - mkdir -p /home/app/${PROJECT_NAME}
    - mkdir -p /home/app/${PROJECT_NAME}/db
    - mkdir -p /home/app/${PROJECT_NAME}/backups
    - rm -f /home/app/${PROJECT_NAME}/nginx.conf
    - rm -f /home/app/${PROJECT_NAME}/app.jar
    - rm -f /home/app/${PROJECT_NAME}/application.yml
    - rm -rf /home/app/${PROJECT_NAME}/web
    - cp app.jar /home/app/${PROJECT_NAME}/app.jar
    - cp -r dist /home/app/${PROJECT_NAME}/web
    - sed -i "s/api-server/${PROJECT_NAME}-api/g" cd/nginx.conf
    - |
      sed "s/\$DB_PASSWORD/$DB_PASSWORD/g" cd/application.yml | sed "s/\$PROJECT_NAME/$PROJECT_NAME/g" | \
      sed "s/\$DB_NAME/$DB_NAME/g" | sed "s/\$DB_USER/$DB_USER/g" > /home/app/${PROJECT_NAME}/application.yml
    - cat cd/nginx.conf
    - cp cd/nginx.conf /home/app/${PROJECT_NAME}/nginx.conf
    # - ls
    # - cd /home/app/${PROJECT_NAME}
    # - ls
    - |
      if [ "$(docker ps -q -f name=${PROJECT_NAME}-api)" ]; then
            docker stop ${PROJECT_NAME}-api
      fi
      if [ "$(docker ps -q -f name=${PROJECT_NAME}-nginx)" ]; then
            docker stop ${PROJECT_NAME}-nginx
      fi
      if [ "$(docker ps -q -f name=${PROJECT_NAME}-db)" ]; then
            docker stop ${PROJECT_NAME}-db
      fi
    - |
      if [[ $CI_COMMIT_TAG == *"#resetapi"* ]]; then
        if [ "$(docker ps -a -q -f name=${PROJECT_NAME}-api)" ]; then
          echo 'remove api container' && docker rm ${PROJECT_NAME}-api
        fi
      fi
    - |
      if [[ $CI_COMMIT_TAG == *"#backdb"* ]]; then
        echo 'backup db'
        NOW=$(date +"%Y-%m-%d-%H-%M-%S")
        tar czf /home/app/${PROJECT_NAME}/backups/dbbackup-$NOW.tar.gz /home/app/${PROJECT_NAME}/db
      fi
    - |
      if [[ $CI_COMMIT_TAG == *"#resetdb"* ]]; then
        if [[ $CI_COMMIT_TAG != *"#backdb"* ]]; then
          echo 'backup db'
          NOW=$(date +"%Y-%m-%d-%H-%M-%S")
          tar czf /home/app/${PROJECT_NAME}/backups/dbbackup-$NOW.tar.gz /home/app/${PROJECT_NAME}/db
        fi
        if [ "$(docker ps -a -q -f name=${PROJECT_NAME}-db)" ]; then
          echo 'remove db container' && docker rm ${PROJECT_NAME}-db && rm -rf /home/app/${PROJECT_NAME}/db
        fi
      fi
    - |
      if [[ $CI_COMMIT_TAG == *"#resetnginx"* || $CI_COMMIT_TAG == *"#resetweb"* ]]; then
        if [ "$(docker ps -a -q -f name=${PROJECT_NAME}-nginx)" ]; then
          echo 'remove nginx container' && docker rm ${PROJECT_NAME}-nginx
        fi
      fi
    - |
      docker network inspect ${PROJECT_NAME}-network >/dev/null 2>&1 || \
      docker network create --driver bridge ${PROJECT_NAME}-network
    - |
      if [ "$(docker ps -a -q -f name=${PROJECT_NAME}-nginx)" ]; 
      then
        echo "${PROJECT_NAME}-nginx exist"
      else
        echo "create ${PROJECT_NAME}-nginx"
        docker create -p ${HTTP_EXPORT_PORT}:80 --cpus=1 --memory=1G --name=${PROJECT_NAME}-nginx \
        --network=${PROJECT_NAME}-network \
        -v /home/app/${PROJECT_NAME}/web:/usr/share/nginx/html:ro \
        -v /home/app/${PROJECT_NAME}/nginx.conf:/etc/nginx/conf.d/default.conf:ro \
        nginx
      fi
    - |
      if [ "$(docker ps -a -q -f name=${PROJECT_NAME}-db)" ]; 
      then
        echo "${PROJECT_NAME}-db exist"
      else
        echo "create ${PROJECT_NAME}-db"
        docker create --cpus=1 --memory=2G --name=${PROJECT_NAME}-db \
        -p ${DB_EXPORT_PORT}:3306 \
        --network=${PROJECT_NAME}-network \
        -e MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD} -e MYSQL_DATABASE=${DB_NAME} \
        -e MYSQL_USER=${DB_USER} -e MYSQL_PASSWORD=${DB_PASSWORD} \
        -v /home/app/${PROJECT_NAME}/db:/var/lib/mysql \
        mysql:5.7 \
        --character-set-server=utf8mb4 \
        --collation-server=utf8mb4_unicode_ci
      fi
    - |
      if [ "$(docker ps -a -q -f name=${PROJECT_NAME}-api)" ]; 
      then
        echo "${PROJECT_NAME}-api exist"
      else
        echo "create ${PROJECT_NAME}-api"
        docker create --cpus=2 --memory=4G --name=${PROJECT_NAME}-api \
        --network=${PROJECT_NAME}-network \
        --workdir=/opt/app \
        -v /home/app/${PROJECT_NAME}:/opt/app \
        adoptopenjdk/openjdk8:jre8u282-b08-debian \
        java -jar app.jar \
        --spring.config.location=/opt/app/
      fi
    - docker start ${PROJECT_NAME}-db
    - sleep 1m
    - docker start ${PROJECT_NAME}-api
    - docker start ${PROJECT_NAME}-nginx
    - sleep 1m
    - docker ps
    # - docker network inspect ${PROJECT_NAME}-network
    # - docker logs ${PROJECT_NAME}-api
    # - docker logs ${PROJECT_NAME}-nginx
  rules:
    - if: $CI_COMMIT_TAG != null

实现过程中遇到的问题

打包后机器人提示打包成功,这意味着服务器在跑脚本时并没有出错。但是访问对应网址却无法访问到资源。

查看日志发现,缺少 api 容器(项目名称为 dentistry)

image.png

但是日志显示 nginx 容器是启动的,这样的话就算缺少api也不应该访问不到资源。

当时在这个节点上停留了很久,最后找学长帮忙。

学长用老师的权限登录服务器后再次查看容器,发现只有一个 db 容器, 而 nginx 容器并没有被成功启动。

image.png

之后我又加入一些语句用于查看日志,得到如下结果。

image.png

先前打印的日志是在,容器启动后立刻打印的。而这次等待一分钟后再打印的结果可以看到,nignx 容器确实没有启动成功。

猜测或许是执行了几秒钟后发现存在问题,然后自动关闭了。

打印 nginx 容器的日志。

image.png

在 nignx 配置文件里找不到 dentistry-api。这是当然的,从上述截图可以看出 api 容器也没有启动成功。

打印 api 容器的日志。

image.png

no main manifest attribute, in app.jar 直接翻译为中文意思是 app.jar 中没有主要清单属性。

经过谷歌之后,才知道这个报错实际上说的是找不到主类入口(java 从主类开始启动)。

于是我就去谷歌 spring boot 在打包时如何配置主类入口。

最后发现在 spring boot 项目的 pom.xml 中是有配置主类入口的,只是被忽略了。(下图中88行 <skip></skip> 配置项表示是否跳过)

image.png

将88行注释后,再推送,新建 Tag,这次打包后所有容器都启动成功了。

image.png

访问对应地址自然也能够访问到我们的项目了。

参考资料

gitlab CI/CD 官方文档


HHepan
164 声望13 粉丝

河北工业大学