公司的任务调度平台Cloud Task去年已经支持以镜像方式部署和运行定时任务,降低了定时任务对于环境的依赖性。但是由于种种原因,我们项目组还一直在使用传统的方式开发和部署定时任务。最近终于是时间,来研究如何以镜像方式部署定时任务,并且将其纳入我们持续集成流程中。
目标
目前我们依据领域驱动的理念将项目拆分为一个个微服务,一个微服务对应一个Solution。而各个领域的定时任务,作为ConsoleApp类型的Project存在于Solution中的Tasks目录下。
整个Solution是通过公司的持续集成平台进行自动构建和部署的:
那我们现在要做的,就是在持续集成流程中,增加一个节点,该节点负责将Tasks目录下的各个Project编译、构建镜像并发布镜像到公司仓库。此后我们手动在Cloud Task平台上通过镜像创建定时任务即可。
分析
这里的主要难点在于,在一个节点里需要构建多个镜像。
- 每个节点只能编写Shell脚本,要编写复杂逻辑的脚本较为困难,且不好维护。
- 每个Project都要构建镜像,那么理论上每个都应该有自己的Dockerfile。但是单独为每个Project创建一个Dockerfile明显做了重复工作,且一旦Dockerfile有修改,极难维护。
针对第1个问题,我们可以通过其他语言来书写构建脚本(比如Python),然后在shell中调用即可。Python编写复杂逻辑比Shell更简单易读易维护。
针对第2个问题,我们可以构建一个公共的基础镜像(po-task-runtime),然后每个task的Dockerfile中只需要有一句话即可:
FROM docker.neg/po/po-task-runtime:x.x.x
这样,如果需要修改镜像,我们只需要修改基础镜像即可,不用每个Project都去修改。更进一步,这个Dockerfile我们可以直接利用Python脚本在构建时创建,这样就不用每个项目都去创建了。
基础镜像的Dockerfile
FROM docker.neg/genesis/aspnetcore:2.1.6-runtimeLABEL maintainer="jake.c.xiao@newegg.com" donet_core_version="2.1.6"ENV SERVICE_IDENTITY $PROJECT_NAMEENV ASPNETCORE_URLS http://0.0.0.0:8080ENV ASPNETCORE_ENVIRONMENT DevelopmentONBUILD ARG TASK_NAMEONBUILD ENV TASK_NAME ${TASK_NAME}ONBUILD COPY ./bin/Release/netcoreapp2.1/publish /usr/taskWORKDIR /usr/taskCMD ["sh","-c","dotnet ${TASK_NAME}.dll"]
有以下几点值得注意:
ONBUILD命令的使用。ONBUILD命令的意思是,当当前镜像作为子镜像的基础镜像时,在子镜像的构建阶段执行指定的命令。注意其中的ONBUILD COPY命令,由于只有在子镜像构建时,才能获取到编译后的项目代码,因此需要使用ONBUILD。ARG命令的使用。ARG命令的含义是“构建参数”,是只在镜像构建阶段才存在的参数(相对的,ENV的值是在构建阶段和容器运行阶段都存在的)。在镜像构建时,通过-docker build -build-arg key=value形式进行传值。(Docker这么设计的原因,是有些参数只在构建阶段有用,不如HTTP代理。)这里使用ARG的原因是,各个项目的名字不一样,调用的dll命令也不一样,因此需要在构建时传递这个值。ARG ENV ONBUILD CMD的联合使用。注意以下代码: ```dockerfile ONBUILD ARG TASK_NAME ONBUILD ENV TASK_NAME ${TASK_NAME}
CMD [“sh”,”-c”,”dotnet ${TASK_NAME}.dll”]
首先,ARG用于在构建阶段从外部获取参数。但这个参数只能存在于构建阶段。**CMD命令的值则是在运行阶段进行解析,因此无法获取构建阶段的参数值**。解决方案是,将ARG的值赋值给一个ENV,而ENV是可以存活到运行阶段的。<br />其次,ARG参数是无法被“继承的”,即如果采用如下方式书写:```dockerfileARG TASK_NAMEENV TASK_NAME ${TASK_NAME}
则这个TASK_NAME实际上只有在基础镜像构建时会生效,且设置ENV TASK_NAME。而在子镜像构建时,根本没有TASK_NAME这个ARG,传参也没有用!
Also, note that ARGs from Dockerfile 1 will not known/transferred to Dockerfile 2. 来源:https://github.com/moby/moby/issues/26533,
因此,这里需要在ARG和ENV前加上ONBUILD,即将构建参数定义和环境变量设置都延迟到子镜像的构建时。这样,才能最终将TASK_NAME作为环境变量保存到容器运行时,供CMD命令使用。
然而,如果直接按如下方式书写:
CMD ["dotnet","${TASK_NAME}.dll"]
启动容器时会报错。原因是,实际上docker本身并没有值替换的功能,这实际上是shell本身的功能。Docker只是忠实地执行CMD指定的命令,因此上面指定的命令在运行时会执行: dotnet ${TASK_NAME}.dll ,而这会报错。为了替换TASK_NAME,我们必须通过shell去调用 dotnet ,因此写成如下形式:
CMD ["sh","-c","dotnet ${TASK_NAME}.dll"]
构建脚本
#!/usr/bin/python# coding:utf-8import xmlimport xml.dom.minidomfrom os import listdir, getcwd, chdir, system, path# 获取Task目录tasksDirs = path.join(getcwd(), "Tasks")# 获取Task目录下每个Task Project的目录taskProjectsDirs = [path.join(tasksDirs, f) for f in listdir(tasksDirs) ifpath.isdir(path.join(tasksDirs, f))]print(taskProjectsDirs)# 通过读取.csproj文件,获取版本def get_task_version(task_name):DOMTree = xml.dom.minidom.parse(task_name + '.csproj')collection = DOMTree.documentElementversion_node = collection.getElementsByTagName('Version')version = version_node[0].childNodes[0].data if len(version_node) > 0 is not None else '0.0.1'return version# 为每个Task创建Dockerfiledef create_dockerfile():with open('Dockerfile', 'w') as f:f.write('FROM docker.neg/po/po-task-runtime:0.0.7')if len(taskProjectsDirs) > 0:for taskDir in taskProjectsDirs:chdir(taskDir) # 切换到当前Project目录create_dockerfile() # 创建Dockerfilesystem('dotnet publish -c Release') # 编译项目task_name = path.basename(taskDir) # 获取Task名称print('task name: ' + task_name)task_version = get_task_version(task_name) # 获取Task版本print('task version: ' + task_version)image_tag = 'docker.neg/po/%s:%s' % (task_name.lower(), task_version) # 构建Image Tagprint('building image: ' + image_tag)# 构建命令,注意这里需要传递TASK_NAME作为参数cmd = 'docker build --build-arg TASK_NAME作为参数=%s -t %s .' % (task_name, image_tag)print('build Command: %s' % cmd)system(cmd)system('docker push %s' % image_tag) # 推送镜像print('build %s succeed.' % image_tag)
