基于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., 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
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.
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.
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.
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.
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.
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.
dotnet publish "%WORKSPACE%\\src\\services\\XDP.Core\\Services\\XDP.Core.JobCenter\\XDP.Core.JobCenter.csproj" -o "%WORKSPACE%\\XDP.Core.JobCenter.API\\publish" --framework netcoreapp2.
'''
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: , 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: '' 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:
- :
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:
- :
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:
- :
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=;Database=identity_at;Uid=xdpat;Pwd=xdpdba;Charset=utf8mb4
APIGATEWAY_DB_CONNSTR=Server=192.168.16.150;Port=;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:
- :
command:
agent -server -bootstrap-expect= -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服务来进行生产级的容器编排。

基于Docker Compose的.NET Core微服务持续发布的更多相关文章
- 使用Docker Compose编排Spring Cloud微服务
文章目录 微服务构建实例 简化Compose的编写 编排高可用的Eureka Server 编排高可用Spring Cloud微服务集群及动态伸缩 微服务项目名称 项目微服务中的角色 microser ...
- .Net Core微服务入门全纪录(八)——Docker Compose与容器网络
Tips:本篇已加入系列文章阅读目录,可点击查看更多相关文章. 前言 上一篇[.Net Core微服务入门全纪录(七)--IdentityServer4-授权认证]中使用IdentityServer4 ...
- .NET Core微服务之基于Jenkins+Docker实现持续部署(Part 1)
Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.CI, CD 与Jenkins 互联网软件的开发和发布,已经形成了一套标准流程,最重要的组成部分就是持续集成(Continuous i ...
- 基于.net core微服务(Consul、Ocelot、Docker、App.Metrics+InfluxDB+Grafana、Exceptionless、数据一致性、Jenkins)
1.微服务简介 一种架构模式,提倡将单一应用程序划分成一组小的服务,服务之间互相协调.互相配合,为用户提供最终价值.每个服务运行在其独立的进程中,服务与服务间采用轻量级的通信机制互相沟通(RESTfu ...
- .NET Core微服务之基于Consul实现服务治理
Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.Consul基础介绍 Consul是HashiCorp公司推出的开源工具,用于实现分布式系统的服务发现与配置.与其他分布式服务注册与发 ...
- .NET Core微服务之基于Exceptionless实现分布式日志记录
Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.Exceptionless极简介绍 Exceptionless 是一个开源的实时的日志收集框架,它可以应用在基于 ASP.NET,AS ...
- .NET Core微服务之基于Apollo实现统一配置中心
Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.关于统一配置中心与Apollo 在微服务架构环境中,项目中配置文件比较繁杂,而且不同环境的不同配置修改相对频繁,每次发布都需要对应修改 ...
- .NET Core微服务之ASP.NET Core on Docker
Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.Docker极简介绍 1.1 总体介绍 Docker 是一个开源的应用容器引擎,基于 Go 语言 并遵从Apache2.0协议开源.D ...
- .NET Core微服务之基于Steeltoe使用Hystrix熔断保护与监控
Tip: 此篇已加入.NET Core微服务基础系列文章索引 => Steeltoe目录快速导航: 1. 基于Steeltoe使用Spring Cloud Eureka 2. 基于Steelt ...
随机推荐
- jchdl - RTL Module
https://mp.weixin.qq.com/s/Sr4ffU4TPPoUJpdInwWd6w jchdl Module类在概念上对应Verilog的module,作为所有用户自定义模块的父 ...
- SpringBoot 集成 Mybatis(三)
个人博客网:https://wushaopei.github.io/ (你想要这里多有) 1.增加持久化层 <dependency> <groupId>mysql< ...
- Java实现 LeetCode 814 二叉树剪枝 (遍历树)
814. 二叉树剪枝 给定二叉树根结点 root ,此外树的每个结点的值要么是 0,要么是 1. 返回移除了所有不包含 1 的子树的原二叉树. ( 节点 X 的子树为 X 本身,以及所有 X 的后代. ...
- Java实现 计蒜客 拯救行动
拯救行动 公主被恶人抓走,被关押在牢房的某个地方.牢房用 N \times M (N, M \le 200)N×M(N,M≤200) 的矩阵来表示.矩阵中的每项可以代表道路(@).墙壁(#).和守卫( ...
- Java实现 LeetCode 1227 飞机座位分配概率
1227. 飞机座位分配概率 有 n 位乘客即将登机,飞机正好有 n 个座位.第一位乘客的票丢了,他随便选了一个座位坐下. 剩下的乘客将会: 如果他们自己的座位还空着,就坐到自己的座位上, 当他们自己 ...
- java实现拉丁方块填数字
"数独"是当下炙手可热的智力游戏.一般认为它的起源是"拉丁方块",是大数学家欧拉于1783年发明的. 如图[1.jpg]所示:6x6的小格被分为6个部分(图中用 ...
- 第03组 Alpha(2/4)
队名:不等式方程组 组长博客 作业博客 团队项目进度 组员一:张逸杰(组长) 过去两天完成的任务: 文字/口头描述: 制定了初步的项目计划,并开始学习一些推荐.搜索类算法 GitHub签入纪录: 暂无 ...
- arduino 的analogRead() 和analogWrite()
模拟输入analogRead()函数的返回值范围是0 到1023; 而模拟输出analogWrite()函数的输出值范围是0 到255; 所以: val = analogRead(potpin); / ...
- Hive中row_number()、dense_rank()、rank()的区别
摘要 本文对Hive中常用的三个排序函数row_number().dense_rank().rank()的特性进行类比和总结,并通过笔者亲自动手写的一个小实验,直观展现这三个函数的特点. 三个排序函数 ...
- 【LGR-072】回首过去
题目 点这里看题目. 分析 可以发现,符合条件的分数约分后,其分母必须为\(2^m5^k\).因此,原分数一定可以表示为: \[\frac{XY}{2^m5^kX} \] 其中\((10, ...