30分钟搭建无痛CI/CD环境

最近参与一个朋友的项目,项目使用SpringCloud Alibaba,共有6个module,部署方式还是本地打好jar包通过运行脚本上传到云服务器目录,然后kill掉原有进程再使用nohup java -jar 的方式运行,这种传统的部署方式有几个弊端

  1. 不同开发人员的环境配置或者代码版本不同,导致构建物有差异;
  2. 本地开发环境和服务器环境不同,本地代码可运行,部署后失败;
  3. 代码版本和构建物版本无法对应,出现问题不好追溯问题代码的版本,增加调试和沟通成本;
  4. 上传速度有限,北京联通300m宽带只有4m/s的上行速度,jar包动辄上百兆,在做大的修改无法及时更新程序;
  5. 需要专注于构建过程和运行结果,无法及时获取构建状态;
  6. 业务试运行只有一台服务器,如果需要扩容服务器,复杂度也是成倍的增长。

综上,需要选择一种简单的、占用系统资源小的持续集成环境,为什么需要简单的,因为工程越简单故障率越低;需要占用系统资源小是因为项目成本有限,不能为了一个集成环境再另开一台云服务器,几年前公司的持续集成环境是gitlab + jenkins + shell脚本,还需要安装Maven或者gradle环境,gitlab和jenkins都是内存大户,还要实时监控这两样服务的状态,定期做代码库的备份防止意外发生,如果没有运维介入,会分散开发很多的精力。

最后选择了阿里codeup + 自带的流水线(flow)来做CI/CD环境,项目打包成docker镜像,做到内置环境统一,方便以后横向扩展。本文的环境:java8/maven3/docker20.10/python3.9/云服务器(centos7.9)

0. 需要做的准备

  1. java、maven基础
  2. docker基础(非必要)
  3. python基础
  4. linux基础
  5. 一个阿里云账号,并注册了阿里云镜像仓库codeupflow流水线
  6. 30分钟

1. 搭建步骤:

1.1 项目代码中新增Dockerfile

# 使用openjdk作为基础镜像 访问官方的国外镜像构建比较慢,可以上传到私人仓库,加快下载速度
FROM  openjdk:8-jre
# 创建一个挂载点,这里选择日志目录
VOLUME /home/project/logs
# 在容器内创建目录/home/project 用于存放应用程序和相关文件
RUN mkdir -p /home/project
# 设置工作目录为/home/project,即后续的命令都在这个工作目录下执行。
WORKDIR /home/project
# 拷贝本地jar包到镜像内 health/target/health.jar是基于项目根目录的相对路径
COPY  health/target/health.jar /home/project/health.jar
# 容器在运行时将监听的端口号
EXPOSE 10001
# java启动命令
CMD ["java", "-Dfile.encoding=utf-8", "-Xms128m", "-Xmx256m", "-XX:MetaspaceSize=32m", "-XX:MaxMetaspaceSize=128m", "-jar", "/home/project/health.jar"]

例子中默认使用openjdk:8-jre,jar包路径是pom文件build后的路径,以上路径和端口号可以根据自己项目的实际情况做相应修改,本文的Dockerfile保存路径为 “项目根目录/docker/Dockerfile”。

2. 使用阿里云账号登录(注册),进入flow流水线

2.1 添加流水线 – 添加流水线源

这里可以是代码源、Jenkins源,或是现有流水线,代码源支持各大主流代码管理平台,这里的操作是添加代码源,推荐使用codeup作为代码管理,个人感觉比gitee要快,也满足小项目团队的使用要求,而且同一生态链产品支持度更好,比如flow的webhook可以在代码库自动添加。

codeup代码源添加示例:

这里选择的是代码仓库xx-project的dev分支,并开启了提交后自动触发构建的选项,如果是生产环境可以按需选择触发条件,比如”合并请求”后构建,注意,如果不是codeup平台,需要在代码管理平台(github/gitlab/gitee等)的webhook处填写flow的webhook地址才能触发构建。

记得打开“开启分支或标签过滤”,避免不同分支提交代码触发不必要的构建操作,这样当xx-project的dev分支有代码变更提交,就会触发流水线开关,开始下一步构建工作。

2.2 进行java构建操作

这个步骤就是执行mvn package操作,构建集群有北京和香港两个选择,如果在开发海外项目,可以选择香港区,下载流水线源一定要选上,这样才能在下一步获取到需要的jar包。jdk版本和maven版本,还有构建命令可以根据自己的需要修改,这里也支持gradle构建。

2.3 打包docker镜像

可以选择阿里云私有镜像仓库或其他镜像仓库,只要速度快就行。

这步是找到项目中的Dockerfile文件,构建docker镜像后上传到私有镜像仓库。如果不需要使用docker部署的可以忽略这一步,直接将构建物上传,通过oss下载到服务器上运行即可。

这里需要注意的是java构建和docker镜像打包需要在一个任务步骤下,不然无法获取上一步的jar包文件。

docker构建配置示例:

服务连接、仓库都可以直接代入,选择正确就可以,标签${BUILD_NUMBER}是构建的次数,官方的全局变量,比如:auth:17,相当于一个简易的版本号,方便回滚版本,还有更多参数,可以根据需要自行选择。

需要注意的是Dockerfile路径和ContextPath,官方说明比较含糊,这里的ContextPath如果填写 “./” 代表Dockerfile是相对于项目根目录的相对路径,比如项目的目录结构是:

projectA

    user

    address

    order

    others

        folder

            Dockerfile

这里的ContextPath填写”./”,Dockerfile路径就是projectA的相对路径: others/folder/Dockerfile

这里是单个Dockerfile打包,如果有多个module就复制多个步骤就好,目前没发现有更快捷的方式,如果有欢迎各位指点。

2.4 构建成功后的webhook通知

因为我用的不是阿里云的ECS,所以不能自动部署,需要将构建结果通知自有的云服务器,其实原理都是一样的。webhook有很多种方式:

除了邮件通知外,其他的通知都有详细的构建参数,可以用来解析,webhook传递的json示例:

data = {'event': 'task', 'action': 'status',
        'task': {'pipelineId': '25121', 'pipelineName': 'health-dev', 'stageName': '构建',
                 'taskName': 'maven打包、构建docker镜像', 'buildNumber': '21', 'statusCode': 'SUCCESS',
                 'statusName': '运行成功', 'pipelineUrl': 'https://flow.aliyun.com/pipelines/25121/builds/21',
                 'message': '流水线[health-dev] - 阶段[构建] - 任务[maven打包、构建docker镜像] - 运行成功',
                 'executorId': '64cb4792977bf7352bf5b20b', 'executorName': '**@aliyun.com', 'pipelineTags': 'dev',
                 'pipelineEnvironment': '日常环境', 'flowInstId': '96512586', 'pipelineInstId': '46588104',
                 'pipelineMark': '21'}, 'pipeline': None, 'artifacts': [], 'sources': [
        {'name': '**-project-dev', 'sign': '**-project_tktj', 'type': 'codeup',
         'data': {'repo': 'https://codeup.aliyun.com/64cb47b0a8c5569c80925b87/**/**-project.git', 'branch': 'dev',
                  'commitId': '3e6d7f09031ad82473377a4e126f8613b3a173a0', 'privousCommitId': None,
                  'commitMsg': '[{"commitTime": "Sun Aug 6 15:17:05 2023 +0800", "commitAuthor": "**", "commitMsg": "%E4%BF%AE%E6%94%B9%E6%9E%B%B6", "commitId": "3e6d7f09031ad84e126f8613b3a173a0"}]',
                  'args': []}}], 'globalParams': [{'id': None, 'key': 'datetime', 'value': '${DATETIME}'}]}

3. 服务器接收webhook,下载运行docker镜像文件

3.1 云服务器需要接收flow的构建通知,所以搭建一个简易的http服务即可,这里使用python的flask模块,示例代码:

import json
import logging
import re
import subprocess

from flask import Flask, request, jsonify

app = Flask(__name__)

# 设置日志记录器
logging.basicConfig(level=logging.INFO,
                    format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
                    datefmt='%Y-%m-%d %H:%M:%S',
                    filename='log-deploy.log',
                    filemode='a')


# 接收请求的地址,方式为POST
@app.route('/webhook/KNIEJGLAJDULDHH', methods=['POST'])
def handle_webhook():
    try:
        data = request.json
        logging.info(f"收到webhook data:{data}")
        # 判断是否状态为成功
        if data["event"] == "task" and data["action"] == "status" and data["task"]["statusCode"] == "SUCCESS":
            # 执行docker命令
            stop_and_remove_containers_images()
            pull_and_run_images(data["task"]["buildNumber"])

        return jsonify({"message": "Webhook handled successfully"}), 200

    except Exception as e:
        return jsonify({"error": str(e)}), 500


def stop_and_remove_containers_images():
    # 停止和删除包含 "auth", "gateway", "health", "system" 的旧镜像和旧容器
    containers_images = subprocess.check_output(
        "docker ps -a -q --filter 'name=auth' --filter 'name=gateway' --filter 'name=health' --filter 'name=system'",
        shell=True)
    logging.info(f"containers_images:{containers_images}")
    if containers_images:
        containers_images = containers_images.decode().strip().split('\n')
        for ci in containers_images:
            logging.info(f"docker stop {ci} && docker rm {ci}")
            subprocess.run(f"docker stop {ci} && docker rm {ci}", shell=True)

    # 定义要删除的镜像名称列表
    images_to_delete = ["auth", "system", "health", "gateway"]
    # 使用正则表达式匹配镜像名称和标签
    pattern = r"registry\.cn-beijing\.aliyuncs\.com/***/({})\s+(\d+)"
    regex = re.compile(pattern.format("|".join(images_to_delete)))

    # 获取docker images列表
    output = subprocess.check_output(["docker", "images"]).decode("utf-8")
    logging.info(f"output:{output}")

    # 提取匹配到的镜像名称和标签
    matches = regex.findall(output)

    # 执行删除操作
    for image_name, image_tag in matches:
        logging.info(f"docker rmi registry.cn-beijing.aliyuncs.com/***/{image_name}:{image_tag}")
        subprocess.run(["docker", "rmi", f"registry.cn-beijing.aliyuncs.com/***/{image_name}:{image_tag}"])


def pull_and_run_images(suffix):
    images = [{"name": "auth", "port": "9000"}, {"name": "gateway", "port": "8080"},
              {"name": "health", "port": "10001"},
              {"name": "system", "port": "9001"}]
    for image_info in images:
        name = image_info["name"]
        port = image_info["port"]
        logging.info(f"docker pull registry.cn-beijing.aliyuncs.com/***/{name}:{suffix}")
        subprocess.run(f"docker pull registry.cn-beijing.aliyuncs.com/***/{name}:{suffix}", shell=True)
        # 执行docker run命令
        cmd = f"docker run -d -p {port}:{port}--memory 384m --memory-swap 384m -v /usr/local/code/healthtrack/logs:/home/**/logs -e TZ=Asia/Shanghai --name {name} registry.cn-beijing.aliyuncs.com/***/{name}:{suffix}"
        process = subprocess.Popen(cmd, shell=True)
        process.wait()  # 等待容器执行完毕
        if process.returncode == 0:
            print("容器已成功启动!")
        else:
            print("启动容器时出现错误")


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=15000)

代码执行步骤:

  1. 接收一个方式为post的webhook请求,然后解析传递来的json;
  2. 如果判断flow构建成功就停止运行原容器,并删除原镜像和容器;
  3. 在docker私人镜像仓库获取构建版本号(也就是那个动态参数${BUILD_NUMBER})相对应的镜像再次运行。

代码需要修改自己docker镜像名、标签和各个端口号。

保存为deploy.py,运行命令:

nohup python deploy.py &

整体构建时间为2分钟左右,当然,速度会因为项目的大小、服务器的配置和带宽的不同而不同,pull私人镜像仓库很快,哪怕云服务器的带宽只有2m,下行速度也是非常快的,如果私人镜像仓库和云服务器是同一地区、或者使用阿里云服务器通过vps网络下载速度会更快。

4. 可以优化的选项

4.1 镜像默认地址

在Dockerfile里的代码里,FROM openjdk:8-jre 默认是从国外下载该镜像,会影响构建速度,建议将openjdk pull到本地,然后上传到私人仓库,这样可以节省一些构建时间。

# 登录私有仓库
docker login --username=****@aliyun.com registry.cn-beijing.aliyuncs.com
# pull openjdk镜像
docker pull openjdk:8-jre
# 提交到私有仓库
docker tag openjdk:8-jre registry.cn-beijing.aliyuncs.com/**/openjdk:8-jre
# push
docker push registry.cn-beijing.aliyuncs.com/**/openjdk:8-jre

这样Dockerfile的from就可以改写为:FROM registry.cn-beijing.aliyuncs.com/**/openjdk:8-jre

4.2 接收webhook的稳定性

python的flask并不推荐用于生产环境,接收webhook请求貌似不是十分可靠,好在webhook并不会有高并发的情况(多个构建请求会在flow端进行排队),可以根据自己的需求选择。

当然,最好添加另一种webhook方式,做到双保险。

4.3 Docker的运行方式

多个项目多个module可以使用Docker Compose进行编排批量运行。

4.4 流水线的其他步骤

flow里有很多可以添加的选项,比如代码安全检查、单元测试、代码合并、删除分支等,可以根据需要添加,注意上下文环境就可以。

5. 总结

通过flow流水线实现package,然后打包成docker文件上传到私人镜像仓库,云服务器下载并运行。不用单独维护代码库和Jenkins等构建工具,flow的执行效率也要比一般自建服务要快的多,目前是免费,对于规模不大的项目完全够用,也可以升级为企业版享受更多服务,这里的成本只有一台云服务器,如果你没注册过阿里云有很多免费资源可以试用:

链接:https://free.aliyun.com?userCode=s6xlr6dr

以上,对于小项目如果有更优雅的部署方式,欢迎各位指点补充,互相学习,有我没写清楚的也可以留言交流,感谢你的阅读。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇