基于Docker Compose的.NET Core微服务持续发布
是不是现在每个团队都需要上K8s才够潮流,不用K8s是不是就落伍了。今天,我就通过这篇文章来回答一下。
一、先给出我的看法和建议
我想说的是,对于很多的微小团队来说,可能都不是一定要上K8s,毕竟上K8s也是需要成本和人力的。对像我司一样的传统企业做数字化转型的信息团队来讲,人数不多,没有专门的Ops人员,领导又想要尽快迭代支持公司业务发展,而且关键还要节省成本(内心想法是:WTF)。
在此之下,信息团队需要综合引入先进技术带来的价值以及需要承担的成本和风险。任何架构的产生,都会解决一定的问题,但是同样也会引入新的复杂度,正如微服务架构风格,看着香实际吃着才知道需要承受很多的“苦”(比如数据一致性又比如服务的治理等等)。
因此,结合考虑下来,我的建议是开发测试环境使用Docker Compose进行容器编排即可,而UAT或生产环境则建议使用云厂商的K8s服务(比如阿里云ACK服务)而不选择自建K8s集群。
那么,今天就跟大家介绍一下如何使用Docker Compose这个轻量级的编排工具实现.NET Core微服务的持续发布。
二、Docker Compose
Docker主要用来运行单容器应用,而Docker Compose则是一个用来定义和应用多容器应用的工具,如下图所示:
使用Docker Compose,我们可以将多容器的定义和部署方式定义在一个yml文件中,这种方式特别是微服务这种架构风格,可以将多个微服务的定义及部署都规范在一个yml文件中,然后一键部署、启动或销毁整个微服务应用。所有的一切操作,只需要下面的一句话:
$docker-compose up
Compose 的安装请参考:https://docs.docker.com/compose/install/#install-compose,这里就不再赘述,它不是本文重点。
安装后验证:
$docker-compose --version
docker-compose version 1.25.1, build a82fef07
三、一个简单的发布流程示例
本文演示示例的流程大概会如下图所示:
阅读过我之前的一篇文章《基于Jenkins Pipeline的ASP.NET Core持续集成实践》的童鞋应该对这个流程比较熟悉了。这里,我仍然延续这个流程,作为一个平滑过渡。首先,我们在Jenkins上触发容器的发布流水线任务,此任务会从Git服务器上拉取指定分支(一般都是测试分支)的最新代码。
其次,在CI服务器上使用.NET Core SDK执行Build编译和发布Release文件,基于发布后的Release文件进行镜像的打包(确保你的项目里面都有Dockerfile且设置为“始终复制”)。然后,基于打包后的镜像,将其推送到企业的私有Registry服务器上(即本地镜像仓库,可以基于Harbor搭建一个,也可以直接用Docker Registry搭建一个,不建议使用docker hub的公有库,如何搭建私有镜像仓库可以参考我的这一篇文章:《Docker常用流行镜像仓库的搭建》)。
最后,在测试服务器或要运行容器的服务器上执行docker compose up完成容器的版本更新。当然,也可以直接在docker-compose.yml文件内设置编译路径完成编译和发布的操作(Dockerfile里面定义进行Build和Publish)。这里目的在于让实例更简单,且能让初学者更容易理解,于是我就分开了。
四、.NET Core微服务发布示例
微服务示例准备
假设我们有一堆使用ASP.NET Core开发的微服务,这些微服务主要是为了实现诸如API网关、Identity鉴权、Notification通知、Job中心等基础设施服务,因此我们将他们整合在一起进行持续集成和部署。
这里为了让示例尽可能简单,每个微服务的Dockerfile只有以下几句(这里以一个通知API服务为例):
FROM reg.xdp.xi-life.cn/xdp-service-runtime:2.2 WORKDIR /app COPY . /app EXPOSE 80 ENTRYPOINT ["dotnet", "XDP.Core.Notification.API.dll"]
其中这里的容器镜像来自于私有镜像仓库,是一个封装过的用于ASP.NET Core Runtime的容器镜像。当然,上面说过,也可以在Dockerfile里面进行服务的编译和发布。
流水线任务脚本
同样,为了在Jenkins上快速进行微服务的镜像构建和推送以及部署,我们也需要编写一个流水线构建任务。
下面是这个示例流水线任务的脚本:
pipeline{ agent any environment { API_CODE_BRANCH="*/master" SSH_SERVER_NAME_REGISTRY="XDP-REGISTRY-Server" SSH_SERVER_NAME_DEV="XDP-DEV-Server" SSH_SERVER_NAME_AT="XDP-AT-Server" SSH_SERVER_NAME_SIT="XDP-SIT-Server" } stages { stage('XDP Core APIs Checkout & Build') { steps{ checkout([$class: 'GitSCM', branches: [[name: env.API_CODE_BRANCH]], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: '35b9890b-2338-45e2-8a1a-78e9bbe1d3e2', url: 'http://192.168.18.150:3000/XDP.Core/XDP.Core.git']]]) echo 'Core APIs Dev Branch Checkout Done' bat ''' dotnet build XDP.Core-InfraServices.sln dotnet publish "%WORKSPACE%\\src\\services\\XDP.Core\\Components\\XDP.Core.ApiGateway\\XDP.Core.ApiGateway.csproj" -o "%WORKSPACE%\\XDP.Core.ApiGateway.API\\publish" --framework netcoreapp2.2 dotnet publish "%WORKSPACE%\\src\\services\\XDP.Core\\Components\\XDP.Core.ApiGateway.Internal\\XDP.Core.ApiGateway.Internal.csproj" -o "%WORKSPACE%\\XDP.Core.ApiGateway.Internal.API\\publish" --framework netcoreapp2.2 dotnet publish "%WORKSPACE%\\src\\services\\XDP.Core\\Services\\XDP.Core.Authorization.API\\XDP.Core.Authorization.API.csproj" -o "%WORKSPACE%\\XDP.Core.Authorization.API\\publish" --framework netcoreapp2.2 dotnet publish "%WORKSPACE%\\src\\services\\XDP.Core\\Services\\XDP.Core.Authorization.Job\\XDP.Core.Authorization.Job.csproj" -o "%WORKSPACE%\\XDP.Core.Authorization.Job\\publish" --framework netcoreapp2.2 dotnet publish "%WORKSPACE%\\src\\services\\XDP.Core\\Services\\XDP.Core.Identity.API\\XDP.Core.Identity.API.csproj" -o "%WORKSPACE%\\XDP.Core.Identity.API\\publish" --framework netcoreapp2.2 dotnet publish "%WORKSPACE%\\src\\services\\XDP.Core\\Services\\XDP.Core.Notification.API\\XDP.Core.Notification.API.csproj" -o "%WORKSPACE%\\XDP.Core.Notification.API\\publish" --framework netcoreapp2.2 dotnet publish "%WORKSPACE%\\src\\services\\XDP.Core\\Services\\XDP.Core.JobCenter\\XDP.Core.JobCenter.csproj" -o "%WORKSPACE%\\XDP.Core.JobCenter.API\\publish" --framework netcoreapp2.2 ''' echo 'Core APIs Build & Publish Done' } } stage('XDP API Gateway Docker Image') { steps{ bat ''' docker rmi reg.xdp.xi-life.cn/core-apigateway-portal:latest; cd XDP.Core.ApiGateway.API/publish; docker build -t reg.xdp.xi-life.cn/core-apigateway-portal:latest .; docker push reg.xdp.xi-life.cn/core-apigateway-portal:latest; ''' echo 'XDP Portal API Gateway Deploy Done' bat ''' docker rmi reg.xdp.xi-life.cn/core-apigateway-internal:latest; cd XDP.Core.ApiGateway.Internal.API/publish; docker build -t reg.xdp.xi-life.cn/core-apigateway-internal:latest .; docker push reg.xdp.xi-life.cn/core-apigateway-internal:latest; ''' echo 'XDP Internal API Gateway Deploy Done' } } stage('Core Identity API Docker Image') { steps{ ...... } } stage('Core Authorization Job Docker Image') { steps{ ...... } } stage('Core Notification API Docker Image') { steps{ ...... } } stage('Core JobCenter API Docker Image') { steps{ ...... } } stage('Deploy to Local SIT Server') { steps{ sshPublisher(publishers: [sshPublisherDesc(configName: env.SSH_SERVER_NAME_SIT, transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: ''' cd compose/xdp; IMAGE_TAG=latest docker-compose down; docker rmi $(docker images -q); IMAGE_TAG=latest docker-compose up -d; ''', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: 'compose/xdp/', remoteDirectorySDF: false, removePrefix: '', sourceFiles: '', excludeFiles: '')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)]) echo 'Deploy to XDP SIT Server Done' } } } }
这个脚本我省去了一些重复的内容,只需要了解它的职责即可。
需要注意的地方有几点:
(1)在进行dotnet build的时候,要明确SDK使用哪个版本,比如因为这里的示例代码是基于.NET Core 2.2开发的因此这里使用的是2.2。如果你使用的是2.1,则标注2.1,如果是3.1,则标注3.1。
(2)在进行docker build的时候,要明确镜像使用哪个Tag,这里因为是本地开发测试环境,所以直接简单暴力的直接使用了latest这个Tag。
(3)在进行sshPublish的时候,要提前将docker-compose.yml配置拷贝到对应的指定目录下。当然,这一块建议也将其纳入git仓库进行统一管理和统一发布到不同的环境的指定目录下。
(4)如果你的Jenkins是装在Windows Server上,要记住只有Windows Server 2016及以上版本才支持Docker,否则无法直接进行docker的命令行操作。如果低于2016,Windows 10专业版也可以,不过不建议。
扩展点:
是否可以一套docker-compose方案标准化部署到多个测试环境?是可以的,我们可以在Jenkins构建任务中配置Parameters,这样就可以一次性部署到多个环境。例如,下面的示例中我设置了一个每次发布可以选择到底要发布到哪个环境,这里是单选,你也可以设置为多选。
效果如下:
docker-compose.yml
终于来到了compose的重点内容:docker-compose.yml
这里我给出上面这个示例的yml示例内容(同样,也省略了重复性的内容):
version: '2' services: core_apigateway_portal: image: reg.xdp.xi-life.cn/core-apigateway-portal:${IMAGE_TAG} container_name: xdp_core_apigateway_portal restart: always privileged: true mem_limit: 1024m memswap_limit: 1024m env_file: - ../docker-variables.env ports: - 5000:80 volumes: - /etc/localtime:/etc/localtime core_apigateway_internal: image: reg.xdp.xi-life.cn/core-apigateway-internal:${IMAGE_TAG} container_name: xdp_core_apigateway_internal restart: always privileged: true mem_limit: 1024m memswap_limit: 1024m env_file: - ../docker-variables.env ports: - 5100:80 volumes: - /etc/localtime:/etc/localtime core_identity_api: image: reg.xdp.xi-life.cn/core-identity-api:${IMAGE_TAG} container_name: xdp_core_identity_api restart: always privileged: true mem_limit: 512m memswap_limit: 512m env_file: - ../docker-variables.env ports: - 6010:80 volumes: - /etc/localtime:/etc/localtime core_authorization_api: ...... core_authorization_job: ...... core_notification_api: ...... core_jobcenter_api: ...... bff_xams_api: ......
备注:这里使用的是version:2的语法,因为3开始不支持内存限制mem_limit等属性设置。当然,你可以使用3的语法,去掉mem_limit和memswap_limit属性即可。
这里的env环境变量配置是定义在另外一个单独的env文件里面的,建议每个环境建立一个单独的env文件供docker-compose.yml文件使用,比如下面是一个AT(自动化测试)环境的env文件内容示例:
# define xdp containers env ASPNETCORE_ENVIRONMENT=at ALIYUN_ACCESS_KEY=sxxdfdskjfkdsjkds ALIYUN_ACCESS_SECRET=xdfsfjiwerowuoi JWT_TOKEN=sdfsjkfjsdkfjlerwewe IDENTITY_DB_CONNSTR=Server=192.168.16.150;Port=3306;Database=identity_at;Uid=xdpat;Pwd=xdpdba;Charset=utf8mb4 APIGATEWAY_DB_CONNSTR=Server=192.168.16.150;Port=3306;Database=services_at;Uid=xdpat;Pwd=xdpdba;Charset=utf8mb4 ...... API_VERSION=AT-v1.0.0
这里,最主要的环境变量就是ASPNETCORE_ENVIRONMENT,你需要指定这些要编排的微服务容器使用哪个环境的appSettings。同样,这里也引申出另一个问题,那就是配置的集中管理,可能你会说出类似Apollo,Spring Cloud Config,K8s Configmap之类的解决方案。这里不是本文的重点,也就跳过。
快速实操体验
现在我们来通过在Jenkins中触发构建任务,可以看到如下图所示的流水线任务状态示意:
这样,一个简单的快速发布流水线就完成了,在单机多容器编排部署方面,Docker Compose是个不错的选择。
五、一些扩展
Consul服务发现容器编排
相比很多童鞋也都在使用Consul作为服务发现组件,我们也可以将Consul纳入到Compose中来统一编排。例如,我们可以这样来将其配置到docker-compose.yml中:
services: consul_agent_server: image: reg.xdp.xi-life.cn/xdp-consul-runtime:${IMAGE_TAG} container_name: xdp_consul_agent_server restart: always privileged: true mem_limit: 1024m memswap_limit: 1024m env_file: - ../docker-variables.env ports: - 8500:8500 command: agent -server -bootstrap-expect=1 -ui -node=xdp_local_server -client='0.0.0.0' -data-dir /consul/data -config-dir /consul/config -datacenter=xdp_local_dc volumes: - /etc/localtime:/etc/localtime - /docker/consul/data:/consul/data - /docker/consul/conf:/consul/config
这里只使用到了一个Consul Server Agent,你可以配置一个3个Server节点的Consul Server集群,请自行查阅相关资料。此外,基于Compose我们也可以为API网关设置links从而实现服务发现的效果,当然前提是你的服务数量不多的前提下。这种方式是通过网络层面帮你做了一层解析,从而实现多个容器之间的互连。这里也推荐一下俺们成都地区的小马甲老哥的一篇《docker-compose真香》的文章,他讲解了docker的网桥模式。
基于Compose的编译发布一体化
我们可以看到在很多开源项目中都是将编译发布一体化的,因此我们可以看到在这些项目的Dockerfile中是这样写的:
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build WORKDIR /app COPY ./*.sln ./NuGet.Config ./ COPY ./build/*.props ./build/ # Copy the main source project files COPY src/*/*.csproj ./ RUN for file in $(ls *.csproj); do mkdir -p src/${file%.*}/ && mv $file src/${file%.*}/; done RUN dotnet restore # Copy everything else and build app COPY . . RUN dotnet build -c Release # api-publish FROM build AS api-publish WORKDIR /app/src/Exceptionless.Web RUN dotnet publish -c Release -o out # api FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 AS api WORKDIR /app COPY --from=api-publish /app/src/Exceptionless.Web/out ./ ENTRYPOINT [ "dotnet", "Exceptionless.Web.dll" ] ......
在Dockerfile中我们看到的是拉取.NET Core SDK来进行Restore、Build和Publish,进一步地提高了标准化的迁移性,也尽可能发挥Docker的集装箱作用。
这时你可以在docker-compose.yml中定义Dockerfile告诉compose先帮我进行Build镜像(这里的build配置下就需要指定Dockerfile的位置):
services: api: build: context: . image: exceptionless/api:latest restart: always ......
六、小结
Docker是容器技术的核心、基础,Docker Compose是一个基于Docker的单主机容器编排工具,功能并不像Docker Swarm和Kubernetes是基于Docker的跨主机的容器管理平台那么丰富。
我想你看到这里也应该有了自己的答案,结合我在最开头给的建议,如果你处在一个小团队中,综合人员水平、技能储备、运维成本 及 真实业务量要求,可以在开发测试环境(一般都是单主机环境的话)中使用Docker Compose进行初步编排。而在生产环境,即使是小团队也建议上云主机,利用云的弹性为未来的业务发展做基础,然后可以考虑使用云上的K8s服务来进行生产级的容器编排。