DevOps? 由于花旗杯的契机,YDJSIR 开始认真研究 Jenkins。虽然说也不能研究得多么深入,但总归是有些尝试。下面的脚本实际上还不算完整的 CICD 流程,只是 YDJSIR 在南京大学软件学院《软件工程与计算 Ⅲ》中使用的配置,仅供参考。
环境说明 该容器运行在一台腾讯轻量云服务器(上海,4C4G8M)的 Docker 容器中,通过 Agent 的方式,控制另一台腾讯轻量服务器(上海,4C4G8M)进行代码拉取、自动构建打包测试与发布等工作。
Jenkins 通过南大 Git 上的镜像仓库获得 WebHook 推送 ++ 代码,相关操作均按照文档进行。前 / 后端和 Python 服务在生产环境中均运行在容器内。目前设置上以 master
分支为生产环境,仅该分支的推送会触发构建。构建状态会用 GitLab Connection 推回给南大 Git。日常开发主分支是 develop
。
部署图如下。
仓库列表和说明如下。
搭建过程 此部分从略。总的来说,步骤如下:
1、安装基础环境、拉取镜像并配置镜像仓库;
2、启动 Jenkins Docker,并确保它可以 SSH 走 RSA 密钥登录业务逻辑服务器;
3、配置业务逻辑服务器使其可以用 SSH 从南大 Git 拉取镜像;
4、编写流水线脚本、配置 WebHook、GitLab 访问令牌和 GitLab Connection 等,逐步调试使得整套 CICD 流程完善,并在日常开发中使用。
部署说明 触发方式 通过 WebHook 触发 通过主动发送或在 master 分支有新的提交以发起 push event 类型的 WebHook 以触发构建。
主动在 Jenkins 上点击触发构建 登录到 Jenkins 后台后,进入 Jenkins 的流水线页面,点击 Build Now
手动触发构建。
目前在 Java 后端仓库已经能实现 JUnit 测试结果和基于 JaCoCo 的测试覆盖率报告收集并能够在 Jenkins 网页上展示。
此外,构建的结果已实测可以推回南大 Git。
注意事项 前端 前端构建完基本是马上就可以用了,但强烈建议用隐私模式或者是 Ctrl+F5 强制刷新查看网页。虽然已改用 cnpm,但是打包速度仍不是十分稳定,可能存在一定波动。
后端 后端打包不是在容器里做的,只是部署在容器里,因而可以有效地利用 cache,速度不错,基本都能在 1 分钟内完成(服务器升级 4C4G 后)
Python 由于需要加载预训练模型,网络不一定稳定(要么快得秒过要么会莫名其妙卡死,大概 20% 概率?),所以构建成功后启动(docker 后台启动不影响执行流)需要的时间波动大。直到四个模型文件加载完后 Python 服务才能正常启动 。已设置如果十分钟加载不完或者失败,gunicorn 会重启 worker 来加载这四个文件。 如有需要,可以多次尝试。
pip 源方面,docker 构建时已换源。
1 2 3 4 5 6 7 8 9 10 11 [2022-03-30 11:28:53 +0000] [7] [INFO] Starting gunicorn 20.1.0 [2022-03-30 11:28:53 +0000] [7] [DEBUG] Arbiter booted [2022-03-30 11:28:53 +0000] [7] [INFO] Listening at: http://0.0.0.0:5000 (7) [2022-03-30 11:28:53 +0000] [7] [INFO] Using worker: sync [2022-03-30 11:28:53 +0000] [8] [INFO] Booting worker with pid: 8 [2022-03-30 11:28:53 +0000] [7] [DEBUG] 1 workers Downloading: 100%|██████████| 319/319 [00:00<00:00, 383kB/s] Downloading: 100%|██████████| 107k/107k [00:01<00:00, 73.7kB/s] Downloading: 100%|██████████| 112/112 [00:00<00:00, 133kB/s] Downloading: 100%|██████████| 856/856 [00:00<00:00, 1.04MB/s] Downloading: 100%|██████████| 390M/390M [02:51<00:00, 2.38MB/s]
部署脚本 目前 Jenkinsfile
改成放代码仓库里面了。还是代码即流水线好。
后端(Java Maven 项目)流水线脚本 改版前 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 pipeline { agent any stages { stage('SCM from Mirror' ) { steps { git url: 'git@git.nju.edu.cn:YDJSIR/191250186_mxyzyyds_backend_mxyzyyds.git' sh "ls -al" } } stage('Build' ) { steps { sh "chmod +x mvnw" sh "./mvnw install" junit 'target/surefire-reports/*.xml' step( [ $class: 'JacocoPublisher' ] ) } post { failure { updateGitlabCommitStatus name: 'build' , state: 'failed' } success { updateGitlabCommitStatus name: 'build' , state: 'success' } } } stage('Launch' ) { steps { sh "/usr/local/shell/start_collect_backend.sh" } } } }
脚本中引用的 /usr/local/shell/start_collect_backend.sh
内容如下。
1 2 3 4 5 6 7 8 9 cd /home/webroot/workspace/COLLECT-Backendls -aldocker stop collect-backend docker rm collect-backend docker system prune -f docker build . -t collect-backend --no-cache docker run -d --name collect-backend -p 8888:8888 --restart unless-stopped collect-backend:latest /bin/bash -c "java -jar target/collect*.jar; tail -f /dev/null" exit exit
Dockerfile 如下:
1 2 FROM openjdk:8 as production-stageCOPY ./target/ /target/
改版后 只有 Jenkinsfile
改变,Dockerfile
不变。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 pipeline { agent { label 'XY' } stages { stage('SCM from Mirror' ) { steps { git url: 'git@git.nju.edu.cn:YDJSIR/191250186_mxyzyyds_backend_mxyzyyds.git' sh "ls -al" sh "pwd" } } stage('Compile' ) { steps { sh "chmod +x mvnw" sh "./mvnw install -Dmaven.test.skip=true" } post { failure { updateGitlabCommitStatus name: 'compile' , state: 'failed' } success { updateGitlabCommitStatus name: 'compile' , state: 'success' } } } stage('Test' ) { steps { sh "./mvnw test" junit 'target/surefire-reports/*.xml' step( [ $class: 'JacocoPublisher' ] ) } post { failure { updateGitlabCommitStatus name: 'test' , state: 'failed' } success { updateGitlabCommitStatus name: 'test' , state: 'success' } } } stage('Build' ) { steps { sh "docker stop collect-backend | true" sh "docker rm collect-backend | true" sh "docker build . -t collect-backend --no-cache" } post { failure { updateGitlabCommitStatus name: 'build' , state: 'failed' } success { updateGitlabCommitStatus name: 'build' , state: 'success' } } } stage('Start' ) { steps { sh 'docker run -d --name collect-backend -p 8888:8888 --restart unless-stopped collect-backend:latest /bin/bash -c "java -jar target/collect*.jar; tail -f /dev/null"' sh "docker ps -a | grep collect-backend" } post { failure { updateGitlabCommitStatus name: 'start' , state: 'failed' } success { updateGitlabCommitStatus name: 'start' , state: 'success' } } } } }
前端(基于 Node.js 的 Vue 项目)流水线脚本 改版前 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 pipeline { agent any stages { stage('SCM from Mirror' ) { steps { git url: 'git@git.nju.edu.cn:YDJSIR/191250186_mxyzyyds_frontend_mxyzyyds.git' sh "ls -al" sh "pwd" } } stage('Build' ) { steps { sh "/usr/local/shell/start_collect_frontend.sh" } post { failure { updateGitlabCommitStatus name: 'build' , state: 'failed' } success { updateGitlabCommitStatus name: 'build' , state: 'success' } } } stage('Post' ) { steps { sh "docker ps -a | grep collect-frontend" } } } }
脚本中引用的 /usr/local/shell/start_collect_frontend.sh
内容如下。
1 2 3 4 5 6 7 8 9 10 cd /home/webroot/workspace/COLLECT-Frontendls -alpwd docker stop collect-frontend docker rm collect-frontend docker build --no-cache . -t collect-frontend docker run -d --name collect-frontend --restart unless-stopped -p 80:80 -p 443:443 collect-frontend:latest /bin/bash -c "nginx; tail -f /dev/null" exit exit
使用的 Dockerfile 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 FROM node:14.18 -stretch as build-stageWORKDIR /app COPY package*.json ./ RUN npm install -g cnpm --registry=https://registry.npm.taobao.org RUN cnpm install COPY ./ . RUN cnpm run build FROM nginx as production-stageRUN mkdir /app COPY --from=build-stage /app/dist /app COPY nginx.conf /etc/nginx/nginx.conf COPY cert/se3.ydjsir.com.cn.pem /etc/nginx/se3.ydjsir.com.cn.pem COPY cert/se3.ydjsir.com.cn.key /etc/nginx/se3.ydjsir.com.cn.key
使用的 nginx.conf 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 user nginx; worker_processes 1; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"' ; access_log /var/log/nginx/access.log main; sendfile on; keepalive_timeout 65; client_max_body_size 100m; server { listen 80; listen 443 ssl; server_name se3.ydjsir.com.cn; ssl_certificate /etc/nginx/se3.ydjsir.com.cn.pem; ssl_certificate_key /etc/nginx/se3.ydjsir.com.cn.key; if ($server_port !~ 443){ rewrite ^(/.*)$ https://$host$1 permanent; } location / { root /app; index index.html; try_files $uri $uri / /index.html; } location ^~ /api/ { proxy_pass http://172.17.0.1:8888/; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } } }
改版后 只有 Jenkinsfile 变了,Dockerfile
和 nginx.conf
不变。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 pipeline { agent { label 'XY' } stages { stage('SCM from Mirror' ) { steps { git url: 'git@git.nju.edu.cn:YDJSIR/191250186_mxyzyyds_frontend_mxyzyyds.git' sh "ls -al" sh "pwd" } } stage('Build' ) { steps { sh "docker stop collect-frontend | true" sh "docker rm collect-frontend | true" sh "docker build --no-cache . -t collect-frontend" } post { failure { updateGitlabCommitStatus name: 'build' , state: 'failed' } success { updateGitlabCommitStatus name: 'build' , state: 'success' } } } stage('Start' ) { steps { sh 'docker run -d --name collect-frontend --restart unless-stopped -p 80:80 -p 443:443 collect-frontend:latest /bin/bash -c "nginx; tail -f /dev/null"' sh "docker ps -a | grep collect-frontend" } post { failure { updateGitlabCommitStatus name: 'start' , state: 'failed' } success { updateGitlabCommitStatus name: 'start' , state: 'success' } } } } }
Python 服务(flask+gunicorn 项目)流水线脚本 改版前 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 pipeline { agent any stages { stage('SCM from Mirror' ) { steps { git url: 'git@git.nju.edu.cn:YDJSIR/191250186_mxyzyyds_python_mxyzyyds.git' sh "ls -al" } } stage('Build' ) { steps { sh "docker stop sim" sh "docker build -t se3python ." } post { failure { updateGitlabCommitStatus name: 'build' , state: 'failed' } success { updateGitlabCommitStatus name: 'build' , state: 'success' } } } stage('Launch' ) { steps { sh "docker run --rm -d -p 5000:5000 --name sim se3python" sh "docker system prune -f" } } } }
使用的 Dockerfile 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 FROM python:3.9 WORKDIR /usr/src/app COPY requirements.txt ./ RUN pip install -i https://mirrors.cloud.tencent.com/pypi/simple --no-cache-dir -r requirements.txt RUN pip install -i https://mirrors.cloud.tencent.com/pypi/simple torch COPY . . RUN python3 setup.py install CMD gunicorn -b 0.0.0.0:5000 app:app --timeout 600 --log-level debug
改版后 只有 Jenkinsfile
变了,Dockerfile
不变。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 pipeline { agent { label 'XY' } stages { stage('SCM from Mirror' ) { steps { git url: 'git@git.nju.edu.cn:YDJSIR/191250186_mxyzyyds_python_mxyzyyds.git' sh "ls -al" } } stage('Build' ) { steps { sh "docker stop sim | true" sh "docker build -t se3python ." } post { failure { updateGitlabCommitStatus name: 'build' , state: 'failed' } success { updateGitlabCommitStatus name: 'build' , state: 'success' } } } stage('Start' ) { steps { sh "docker run --rm -d -p 5000:5000 --name sim se3python" sh "docker system prune -f" } post { failure { updateGitlabCommitStatus name: 'start' , state: 'failed' } success { updateGitlabCommitStatus name: 'start' , state: 'success' } } } } }