前言

最近在研究阿里云的容器镜像服务ACR,花了一天的时间也是终于把微服务项目成功在本地打包成Docker镜像,上传到阿里云的私有镜像仓库。过程还是稍微有点复杂,笔者这里也只能说个大概,提供下关键脚本和步骤,给大家做个参考。


一、开通阿里云容器镜像服务ACR

这个是前提了,自行去阿里云的产品中心去购买即可,如果你是个人学习使用,那就买个人版,入果是企业的话那有好几种选择,经济版、基础版、标准版、高级版,按需购买即可,下面贴张图供参考。

在这里插入图片描述
笔者购买的是企业实例中的经济版。


二、相关服务Dockerfile文件

这一步是核心了,笔者要处理的项目是微服务架构,就是有多个子服务,需要做的就是把这些子服务分别构建成镜像,这中间会涉及到各个子服务的Dockerfile编写。

先展示下笔者的微服务项目目录结构
在这里插入图片描述
截至到上图展示的阶段为止,一共有六个子服务,分别是usteu-gateway、usteu-auth、usteu-monitor、usteu-system、usteu-etl、usteu-snailjob,具体不管这些服务是干啥的,首先要做的就是在这些子服务的模块根目录下创建各自的Dockerfile文件,好用来分别构建各自的镜像。
在这里插入图片描述
给一个gateway模块的Dockerfile示例

# 基础镜像
FROM openjdk:17-jdk

# 维护者信息
MAINTAINER usteu

# 添加jar包
ADD target/usteu-gateway.jar app.jar

# 暴露端口
EXPOSE 8050

# 启动命令
ENTRYPOINT ["java", "-jar", "/app.jar"]

这个 Dockerfile 的作用如下:

  1. 基础镜像:使用 openjdk:17-jdk 作为基础镜像,这是官方提供的 OpenJDK 17 镜像,包含了运行 Java 应用所需的环境。
  2. 维护者信息:指定镜像的维护者为 “usteu”。
  3. 添加应用文件:将构建好的 JAR 文件 target/usteu-gateway.jar 添加到镜像中,并重命名为 app.jar。这个文件是在 Maven 构建阶段生成的。
  4. 暴露端口:声明容器将监听 8050 端口,这是网关服务的默认端口,就是服务端口了。
  5. 启动命令:设置容器启动时执行的命令,即使用 java -jar 运行 app.jar 文件。

三、JenkinsFile文件

笔者是借助使用Jenkins工具使用流水线的方式来完成构建、打包、上传阿里云的。在笔者这个项目的根目录下有一个JenkinsFile文件:

在这里插入图片描述

脚本内容如下

pipeline {
    agent any

    // 配置Maven(与Jenkins全局工具配置的Name「maven」一致)
    tools {
        maven 'maven'
    }

    environment {
        REGISTRY = 'kdnt-docker-images-registry.cn-hangzhou.cr.aliyuncs.com'
        NAMESPACE = 'usteu-ai-cloud'
        SERVICES_STR = 'usteu-auth,usteu-gateway,usteu-visual/usteu-visual-monitor,usteu-modules/usteu-modules-system,usteu-modules/usteu-modules-etl,usteu-modules/usteu-modules-snailjob'
        // 阿里云Docker凭据(用户名密码类型)
        ALIYUN_CREDENTIALS = credentials('aliyun-docker-credentials')
        // 使用简单的构建号作为版本标签
        VERSION_TAG = "${env.BUILD_NUMBER}"
    }

    stages {
        // 顶级Stage:拉取代码
        stage('拉取代码') {
            steps {
                checkout scm
                // 打印目录结构,确认服务目录存在
                sh 'ls -l'
            }
        }

        // 构建所有模块
        stage('构建所有模块') {
            steps {
                script {
                    echo "=== 开始构建所有模块 ==="
                    // 在根目录下构建所有模块
                    sh "mvn clean package -DskipTests"
                    echo "=== 所有模块构建完成 ==="
                }
            }
        }

        // 顶级Stage:构建与推送镜像
        stage('构建与推送镜像') {
            steps {
                script {
                    def SERVICES = env.SERVICES_STR.split(',')

                    // 串行处理每个服务,避免并发推送问题
                    SERVICES.each { service ->
                        stage("构建 ${service}") {
                            // 检查服务目录是否存在
                            script {
                                if (!fileExists(service)) {
                                    error "服务目录 ${service} 不存在"
                                }

                                dir(service) {
                                    // 检查关键文件是否存在
                                    def hasPom = fileExists('pom.xml')
                                    def hasDockerfile = fileExists('Dockerfile')

                                    if (!hasPom || !hasDockerfile) {
                                        echo "⚠️ 服务 ${service} 缺少必要文件,跳过构建"
                                        echo "pom.xml存在: ${hasPom}, Dockerfile存在: ${hasDockerfile}"
                                        return
                                    }

                                    echo "=== 开始构建服务:${service},当前目录:${pwd()} ==="
                                    sh 'ls -l | grep -E "pom.xml|Dockerfile"'

                                    // 直接进行Docker构建(标签含构建号和latest)
                                    // 提取服务名称,去掉重复的模块前缀
                                    def serviceName = service.split('/').last()
                                    if (serviceName.startsWith('usteu-modules-')) {
                                        serviceName = serviceName.replace('usteu-modules-', 'usteu-')
                                    }
                                    if (serviceName.startsWith('usteu-visual-')) {
                                        serviceName = serviceName.replace('usteu-visual-', 'usteu-')
                                    }
                                    sh """
                                        set -x
                                        docker build -t ${REGISTRY}/${NAMESPACE}/${serviceName}:latest .
                                        docker tag ${REGISTRY}/${NAMESPACE}/${serviceName}:latest ${REGISTRY}/${NAMESPACE}/${serviceName}:${env.VERSION_TAG}
                                    """

                                    // Docker登录并推送(凭据自动注入)
                                    sh """
                                        set -x
                                        echo "密码内容: ${ALIYUN_CREDENTIALS_PSW}"
                                        echo "用户名: ${ALIYUN_CREDENTIALS_USR}"
                                        echo "仓库地址: ${REGISTRY}"
                                        echo "目标镜像: ${REGISTRY}/${NAMESPACE}/${serviceName}"

                                        # 登录Docker仓库
                                        echo ${ALIYUN_CREDENTIALS_PSW} | docker login -u ${ALIYUN_CREDENTIALS_USR} --password-stdin ${REGISTRY}

                                        # 检查登录状态
                                        if [ \$? -eq 0 ]; then
                                            echo "Docker登录成功"
                                        else
                                            echo "Docker登录失败"
                                            exit 1
                                        fi

                                        # 推送镜像(带重试机制)
                                        echo "开始推送版本标签: ${REGISTRY}/${NAMESPACE}/${serviceName}:${env.VERSION_TAG}"
                                        for i in {1..3}; do
                                            if docker push ${REGISTRY}/${NAMESPACE}/${serviceName}:${env.VERSION_TAG}; then
                                                echo "推送 ${serviceName}:${env.VERSION_TAG} 成功"
                                                break
                                            else
                                                echo "推送 ${serviceName}:${env.VERSION_TAG} 失败,重试第 \$i"
                                                if [ \$i -eq 3 ]; then
                                                    echo "⚠️ 版本标签推送失败,但latest标签仍会尝试推送"
                                                fi
                                                sleep 5
                                            fi
                                        done

                                        echo "开始推送latest标签: ${REGISTRY}/${NAMESPACE}/${serviceName}:latest"
                                        for i in {1..3}; do
                                            if docker push ${REGISTRY}/${NAMESPACE}/${serviceName}:latest; then
                                                echo "推送 ${serviceName}:latest 成功"
                                                break
                                            else
                                                echo "推送 ${serviceName}:latest 失败,重试第 \$i"
                                                sleep 5
                                            fi
                                        done

                                        docker logout ${REGISTRY}
                                    """
                                }

                                // 清理镜像(在Scripted模式中直接执行)
                                script {
                                    def serviceName = service.split('/').last()
                                    if (serviceName.startsWith('usteu-modules-')) {
                                        serviceName = serviceName.replace('usteu-modules-', 'usteu-')
                                    }
                                    if (serviceName.startsWith('usteu-visual-')) {
                                        serviceName = serviceName.replace('usteu-visual-', 'usteu-')
                                    }
                                    echo "=== 清理服务 ${service} 的本地镜像 ==="
                                    sh "set -x; docker rmi ${REGISTRY}/${NAMESPACE}/${serviceName}:latest ${REGISTRY}/${NAMESPACE}/${serviceName}:${env.VERSION_TAG} || true"
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

各部分详细解释

  1. 环境配置部分
agent any

tools {
    maven 'maven'
}

environment {
    REGISTRY = 'kdnt-docker-images-registry.cn-hangzhou.cr.aliyuncs.com'
    NAMESPACE = 'usteu-ai-cloud'
    SERVICES_STR = 'usteu-auth,usteu-gateway,usteu-visual/usteu-visual-monitor,usteu-modules/usteu-modules-system,usteu-modules/usteu-modules-etl,usteu-modules/usteu-modules-snailjob'
    ALIYUN_CREDENTIALS = credentials('aliyun-docker-credentials')
    VERSION_TAG = "${env.BUILD_NUMBER}"
}

  • agent any: 表示该流水线可以在任何可用的代理节点上运行
  • tools: 指定使用 Jenkins 中配置的 Maven 工具
  • environment: 定义环境变量
    • REGISTRY: 阿里云容器镜像仓库地址
    • NAMESPACE: 镜像仓库中的命名空间
    • SERVICES_STR: 需要构建和部署的服务列表
    • ALIYUN_CREDENTIALS: 从 Jenkins 凭据中获取的阿里云 Docker 仓库登录信息
    • VERSION_TAG: 使用 Jenkins 构建号作为版本标签
  1. 拉取代码阶段
stage('拉取代码') {
    steps {
        checkout scm
        sh 'ls -l'
    }
}

  • 从 SCM(源代码管理)中检出代码
  • 列出目录内容以确认代码已正确拉取
  1. 构建所有模块阶段
stage('构建所有模块') {
    steps {
        script {
            echo "=== 开始构建所有模块 ==="
            sh "mvn clean package -DskipTests"
            echo "=== 所有模块构建完成 ==="
        }
    }
}

  • 使用 Maven 清理并打包所有模块,跳过测试
  • 这会生成所有服务的 JAR 文件
  1. 构建与推送镜像阶段
    这是最复杂的部分,处理每个服务的 Docker 镜像构建和推送:
  • 将服务列表拆分为数组
  • 逐个处理每个服务
  • 对每个服务:
    • 检查服务目录是否存在
    • 检查 pom.xml 和 Dockerfile 是否存在
    • 构建 Docker 镜像
    • 登录到阿里云容器镜像仓库
    • 推送镜像(带重试机制)
    • 清理本地镜像

四、jenkins任务构建和相关设置

这里默认是已经搭建好jenkins工具的,这是最基本的条件,不会搭建的自行搜索相关教程(笔者也写过特别详细的教程,在博客主页搜索即可),并且jenkins所在服务器必须要安装好docker服务,因为jenkins要打包成镜像上传。

  1. JenkinsFile中maven工具配置

上面的JenkinsFile中有这么一段脚本

    tools {
        maven 'maven'
    }

这个其实就是你的jenkins中配置的maven名称,查看路径在系统管理——>全局工具配置下的Maven安装里,取Name名称即可。

在这里插入图片描述

  1. 阿里云docker容器镜像服务连接凭证配置
    还是看JenkinsFile,里面有这么一个配置脚本
  // 阿里云Docker凭据(用户名密码类型)
  ALIYUN_CREDENTIALS = credentials('aliyun-docker-credentials')

这个是干嘛的呢?是配置获取阿里云容器镜像ACR实例的凭证入口,登录成功后才可以上传镜像,aliyun-docker-credentials是我们在jenkins里配置时设置的凭证Id,下面展示下在哪里配置。

在这里插入图片描述
是在系统管理,凭据管理里面设置凭据,有两个凭据,第一个是拉取代码的凭证,笔者配置的是gitee账号的,因为代码放在gitee上面,第二个就是阿里云容器镜像服务ACR实例凭证,可以看到唯一标识就是aliyun-docker-credentials
在这里插入图片描述
那这个的用户名和密码是什么呢?

用户名是你的阿里云账号名字,密码却不是,是在如下图位置设置的单独固定密码

在这里插入图片描述
命名空间的设置,这个在上面的JenkinsFile脚本中也有对应的配置,二者也要对应上。

 NAMESPACE = 'usteu-ai-cloud'

在这里插入图片描述

每个子服务的镜像仓库也要提前在阿里云容器镜像服务实例中的镜像仓库中一个个的设置好,这些仓库都是要设置在usteu-ai-cloud的命名空间下

在这里插入图片描述
在这里插入图片描述
这些设置好以后,再去jenkin中新建流水线任务

在这里插入图片描述
任务新建好后,点击进去进行流水线配置,这里要配置上我们的gitee代码拉取凭证

在这里插入图片描述
代码拉取分支是master,脚本路径由于JenkinsFile是在项目根路径下,所以直接写文件名就行了

在这里插入图片描述
保存后就可以尝试开始构建了


五、镜像上传测试

在这里插入图片描述
在这里插入图片描述
ok,可以看到容器镜像全部都上传成功了,我们去阿里云上看下,随便找一个gateway的仓库,可以看到上传的相关镜像了

在这里插入图片描述
可以看到一直有一个latest版本,这个就是最新的镜像了,但是为了让历史版本镜像能够有一定的保留版本,我们每次构建上传每个子服务的镜像实际是两个,这俩除了版本标签名字不一样,其他都是一样的,一个是版本号标签,一个latest标签,其中latest对应的镜像可以被覆盖掉,其他的不可覆盖,这样的话下次一上传时,版本号标签按顺序递增一个上传进来,latest则会被最新的覆盖掉。这样就达到了保留历史记录又能提供最新镜像服务的效果。

那又有同学问了,随着构建次数的增多,镜像会越来越多,该怎么样去清理呢?阿里云提供了非常灵活的解决方案,可以配置不同维度的清理范围,定时清理的周期等。
在这里插入图片描述
点击下一步后,可以设置保留镜像的策略,比如保留最近构建的三个,或者有特殊版本的镜像也可以设置单独保留。

在这里插入图片描述


六、总结

到这里,有关本地服务打包成容器镜像上传到阿里云容器服务ACR的教程就结束了,这是服务上云,容器化的第一大步,后面笔者还会研究使用阿里云的容器服务 Kubernetes 版 ACK,如何编排启动这些服务,让其提供一个完整的服务能力,并且可以自动扩缩容,到时再为大家带来详细教程步骤,本文也仅提供一个参考,旨在说明大概步骤有哪些,实际操作起来可能会遇到各种问题,需要读者朋友们自行解决。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐