docker部署angular和asp.net core组成的前后端分离项目
最近使用docker对项目进行了改进,把步骤记录一下,顺便说明一下项目的结构。
项目是前后端分离的项目,后端使用asp.net core 2.2,采用ddd+cqrs架构的分层思想,前端使用的是angular,数据库采用了sqlserver。所有的部件都是由docker部署到服务器上。
后端
后端的整体结构如下:
Application层是应用层,主要解耦api层(展现层)和领域层,提供dto和应用服务接口等内容,它主要用来描述客户用例。
Core层是领域核心层,这里定义了实体、Command、Event的基类,并且定义了处理Command和Event的处理器(bus)基类;仓储的接口以及一些核心概念
Domain层是Core层的扩展,这里定义了具体的实体、Command、Event以及Command和Event的处理器(bus),以及各个实体对应的仓储(repository)接口,我把关于认证(Authentication)的相关接口也放到了这里,把它放到这里的主要原因是在这层我定义了用户的实体。但是怀疑这样做不对,以后可能会将认证的相应接口放到基础设施层。
Infrastructure层是基础设施层,这里存放了大量的接口的实现,把接口的实现放到这里是因为这一层是一个热插拔层,倘若以后有接口的不同实现,那么我可以新增一个基础设施层来实现接口,而不用大动干戈的修改代码,符合开闭原则。
Web层提供客户端需要的api,输出dto并接收viewmodel,dto和viewmodel都在Application层定义。Web层就是api提供者,目前Web层实现了RESTFUl接口定义,我故意将查询(Get)的api端口和命令(POST,PUT,PATCH,DELETE等)的api端口做了分离,这是为了响应CQRS的架构风格,便于将来性能上的升级。
Application
Application层的结构如下:
Automapper里面主要包含了领域实体和dto之间的转换设置
Dto里面主要包含了定义的各个展现层需要用到的数据(dto主要定义了输出的数据接口)。
ServiceInterface里面包含了定义的各个实体对应的客户用例接口
ServiceImplement实现了ServiceInterface里面的接口
ViewModels和Dto的作用正好相反,它定义了从客户端输入的数据接口,ViewModels会由Automapper转换成command,进入领域实体处理具体的事务。
IApplicationService接口定义了应用服务的公共接口,目前是一个空接口。
Core
Core层的结构如下:
Bus定义了命令和事件处理器的基类
Commands定义了命令的基类
Events定义了事件的基类
Models定义了实体的基类
Notifications定义了系统级通知的类
Repositories定义了仓储的基础接口,仓储分为了两种,一种是实体存储仓储,一种是事件存储仓储
SharedKernel定义了一些公共的、核心的基类,包括值对象、UnitOfWork等
Domain
Domain层的结构如下:
Authentication包含了认证的接口和实现,目前考虑把它放到这一层并不合适,需要在以后的工作中优化一下。Authentication中定义的认证是采用jwt bearer的方式,Jwt可以在多端之间进行传输,因为我们采用的是前后端分离的方式,采用jwt bearer的方式就很有必要了。关于jwt的内容有很多,这里就不展开了。
Commands里面包含了各种实体相关的命令,如修改密码,重置密码等,它由Application层的ViewModel转换而来。Command会在进入CommandHandler之前进行验证,如果里面的参数没有通过校验,那么会直接发出一个系统通知(Notification),Web层会检查这一状况,如果发现了会返回一个错误给客户端。
CommandHandlers里面包含了命令处理器。
Events里面包含了各种事件的定义,如客户密码已修改事件、客户密码重置事件等,事件会通过仓储记录到一个StoredEvent的类型中,包含了事件发生的主体,事件的内容等。
EventsHandler中定义了各种事件处理器。
Models里面定义了各种实体,如机构,资产等,它是业务最核心的表述。
Repositories里面定义了各种实体相关的仓储。
Services里面定义了领域服务,当一些方法(method)放在类中不太合适的话,就要建立一个领域服务来处理这些逻辑了,比如资产类会产生一些资产转移记录,那么描述由资产到资产转移记录的这个过程就不太适合放到资产类中来做,而是建立一个资产的领域服务,来根据一项资产创建一条资产转移记录。领域服务过度使用会造成实体的贫血,产生一种“贫血”的实体模型。不要过度使用领域服务。
ValueObjects里面定义了各种值对象,值对象是作为实体的一个属性而存在的,值对象和实体的主要区别是它没有Id,录入Person类中有一个Address属性,Address属性本身是一个类,里面包含了省、市、街道等信息。
Infrastruecture
基础设施层中包含了各种接口的实现,我这里图省事儿吧所有的接口实现都放到了一层中,实际上这样做是不对的,应该根据领域层的定义将Infrastructure分为多个子层,这样才能更好的实现开闭原则。
Bus定义了命令和事件的处理总线。
DataBase定义了仓储的上下文,我采用了ORM(EntityFramework core)来实现对实体和数据库表之间的映射管理,这样在项目建立初期为我省去了大量的时间来把注意力集中到了业务开发上,同时,目前的EntityFramework core的性能还算不错,基本和原生sql的性能持平。
DbConfigurations配置了约束,实体之间的联系等,像主外键约束,一对一和一对多的关系等
Identity主要是实现了用户标识的获取接口。
Migrations由entityframework core创建,记录实体的迁移(实体和数据库表之间的迁移)历史
Repository定义了各个实体相关接口的实现,这里都是有entityframeworkcore来实现的。
UnitOfWork定义了工作单元,定义了事务处理的关键类。
Web
Auth主要包含了授权的相关逻辑,目前项目中使用的授权是基于策略(policy)授权
Controllers定义了控制器(controller),也就是api的具体位置。Controller分为两种接口,一种是查询(Get请求),另外一种是命令(Post、put、patch、delete等请求),查询接口采用OData的标准进行开发,由于OData已经是一个RESTFul的标准,所以采用OData可以让我们的生活更轻松。命令接口同样采用标准的RESTFul接口形式进行开发,并会返回统一的消息格式(一个自定义的ActionResult类)。
Extensions里面包含了依赖注入(DI)的全部逻辑。
Appsettings.json中包含了环境变量的配置和一写内存中的对象的配置。如jwt认证的选项、数据库连接的选项、跨域访问的一些选项等。
Program.cs是Web项目的启动类
StartUp.cs是配置中间件和注入服务的类。
以上就是后端项目的一个总体介绍。
前端
前端的整体结构如下:
前端采用angular进行开发,angular的好处自不必说,采用TypeScript而不是javascript来开发可以避免很多类型上的问题,在项目的编译阶段就避免了大量的问题调试等。
dist文件夹存放项目发布后的文件
node_modules存放node npm安装的依赖包
e2e存放测试用例
src是项目的源文件开发的大部分代码都是放到这个文件夹中的
.eidtorconfig存放ide的一些设置
.gitignore存放git命令提交时应该忽略的一些文件,主要是机密文件和配置文件避免上传到仓库中造成机密泄露
Angular.json存放angular开发的配置
Dockerfile是docker构建工具的配置文件,用来构建镜像
Package.json描述项目依赖的包以及所用到的脚本命令等信息
tsconfig.json配置typescript使用规则
其他不重要,不介绍了。
下面是src的结构:
Src存放项目的开发代码,core中保存的是各种模型和service以及公共基础类
Home里面保存的是主页的内容
此外还有user和manage,顾名思义就是普通用户和管理者的相关页面
前端总体就是这个样子了。
Angular的相关概念很多,学习曲线陡峭(主要是还要学习tpescript),但是回报也很高,用angular开发的项目可以用很少的代码量完成很多强大的功能。
Docker项目部署
先说明:所有的服务器都是centos7.
Docker私有仓库
Docker有一个registry的概念,就是镜像仓库的意思,docker官方公布了一个官方的registry,就是docker hub,我们默认拉取和推送的最终目的地都是这个docker hub。但是我们自己的代码需要放到一个相对安全的地方,不能让别人看到,docker也提供了一个工具,可以让我们自己搭建一个局域网内的仓库,这个工具的名字叫registry,拉取这个镜像到我们本地:
docker pull registry:latest
然后搭建我们的私有仓库容器:
docker run –d\ -p 5000:5000\ --restart=always\ --name=registry\ -v /home/wallee/dockerRegistry/config.yml:/etc/docker/registry/config.yml\ -v /home/wallee/dockerRegistry:/var/lib/registry\ registry:latest
说明:
-d:后台daemon方式运行
-p 5000:5000:容器内部5000端口映射到外部的5000端口
--restart=always:docker重启时自动重启这个容器
--name=registry:容器名称
-v /home/wallee/dockerRegistry/config.yml:/etc/docker/registry/config.yml:将外部一个编辑好的配置文件挂载到容器中
-v /home/wallee/dockerRegistry:/var/lib/registry:给容器内部存储的数据挂载一个外部的volume,备份好数据。
Ok,docker的私有仓库搞定,下面就可以给这个仓库放镜像了。
另外别忘记开通相应端口:firewall-cmd --zone=public --add-port=5000/tcp --permanent
数据库
我使用的是mcr.microsoft.com/mssql/server,下载该镜像时要注意,有一个mssql-server的官方镜像已被标注为deprecated,前面提到的这个镜像才是官方支持的镜像,直接拉取最新的版本就好:
docker pull mcr.microsoft.com/mssql/server:latest
关于这个镜像的连接:https://hub.docker.com/_/microsoft-mssql-server
下载好镜像之后就可以启动这个镜像了:
Docker run –d \ –p 1433:1433 \ -e ‘ACCEPT_EULA=Y’ \ –e ‘SA_PASSWORD=************’ \ -v /home/wallee/data: /var/opt/mssql \ --name=sqlserver \ mcr.microsoft.com/mssql/server:latest
说明:
-p 1433:1433将容器内部的1433端口和外部的1433端口关联
-e ‘ACCEPT_EULA=Y’ 定义环境变量,该环境变量表示接收最终用户许可协议
–e ‘SA_PASSWORD=************’ 定义SA系统用户的密码,密码必须大于8位,有数字、字母和特殊符号组成
-v /home/wallee/data: /var/opt/mssql 将容器内部的卷挂载到外面,这样当容器瘫痪时数据可以完整的保存下来
--name=sqlserver容器名称
mcr.microsoft.com/mssql/server:latest镜像名称
这样就ok了
还需要注意的是要打开相应的端口,否则无法访问:
Firewall-cmd --zone=public --add-port=1433/tcp --permanent
然后重启防火墙生效。
后端
后端要写一个dockerfile来构建一个镜像,dockerfile的位置如下图:
项目的目录是Boc.Assets,dokcerfile放到了项目的统计目录上。
dockerfile内容如下:
#用microsoft/dotnet:2.2-sdk-alpine作为基础镜像并给一个别名 FROM microsoft/dotnet:2.2-sdk-alpine AS dotnetcore-sdk #定义工作目录,直到下一个FROM子句之前的指令都是基于这个目录来执行的 WORKDIR /source #复制工程文件 COPY Boc.Assets/Boc.Assets.Application/Boc.Assets.Application.csproj /source/Boc.Assets.Application/ COPY Boc.Assets/Boc.Assets.Domain/Boc.Assets.Domain.csproj ./Boc.Assets.Domain/ COPY Boc.Assets/Boc.Assets.Domain.Core/Boc.Assets.Domain.Core.csproj /source/Boc.Assets.Domain.Core/ COPY Boc.Assets/Boc.Assets.Infrastructure/Boc.Assets.Infrastructure.csproj /source/Boc.Assets.Infrastructure/ COPY Boc.Assets/Boc.Assets.Web/Boc.Assets.Web.csproj /source/Boc.Assets.Web/ #Restore RUN dotnet restore /source/Boc.Assets.Web/Boc.Assets.Web.csproj #然后将所有文件复制到WORKDIR下面 COPY Boc.Assets /source #构建和发布 FROM dotnetcore-sdk as dotnetcore-publish RUN dotnet publish /source/Boc.Assets.Web/Boc.Assets.Web.csproj -c Release -o /publish #ASP.NET CORE RUNTIME FROM microsoft/dotnet:2.2-aspnetcore-runtime-alpine AS aspnetcore-runtime WORKDIR /app COPY --from=dotnetcore-publish /publish /app EXPOSE 5003 ENTRYPOINT [ "dotnet","Boc.Assets.Web.dll" ]
关于说明已经在上面有写。该Dockerfile采用了分阶段编译,这样使得我们编译出来的镜像可以达到最小化
然后在当前目录执行docker build –t wallee/assetmanagementserver:1.0 .
–t wallee/assetmanagementserver:1.0是给要生成的镜像设置一个名称和tag。
注意后面还有一个‘.’,表示上下文为当前目录。
生成的这个镜像在本地,我们要把它推送到服务器那边,利用我们刚才搭建好的私有仓库:
1、 首先给这个镜像重新生成一个标签,写上私有仓库的地址:docker tag wallee/assetmanagementserver:1.0 21.33.129.180:5000/wallee/assetmanagementserver:1.0
2、 然后就可以把这个标记有新标签的镜像发到私有仓库了:docker push 21.33.129.180:5000/wallee/assetmanagementserver:1.0
3、 过程中如果有http和https的问题的话通过如下方式处理:
windows:
双击docker的图标,在弹出框左侧选择daemon,然后在右侧的insecure registries里面填写好私有仓库的地址。
Linux:
在/etc/docker下面新建一个daemon.josn,写入下面的内容:
{
"insecure-registries":["21.33.129.180:5000"]
}
里面的ip就是私有仓库的地址
然后就差不多ok了
接下来我们连接到服务器上,先从私有仓库吧这个镜像下载下来:
docker pull 21.33.129.180:5000/wallee/assetmanagementserver:1.0
生成好镜像后,我们把它跑起来:
Docker run -d \ -p 5003:5003\ --name assetsApi\ --network=bocassets\ 21.33.129.180:5000/wallee/assetmanagementserver:1.0
说明:
--network=bocassets:给容器指定一个二层网络(网桥),因为之后要部署的前端需要和后端进行通信,所以在这里把后端和前端放到一个局域网中,便于通信。
上面的bocassets是docker中网桥的名称,我们可以通过以下命令创建:
docker network create bocassets
前端
我的前端是angular编写的,angular项目构建后生成如下的画面:
Angular使用webkpack构建工具,生成的内容可以通过angular.json进行配置。
具体的信息查看https://angular.cn/cli/build
前端项目使用nginx作为反向代理进行访问,关于nginx的基础知识你需要知道的首先是它的配置文件的结构,像上下文,各个指令,模块的含义等,关于它的配置文件,需要知道的是:Nginx的配置文件分为主配置文件和子配置文件,主配置文件一般是/etc/nginx/nginx.conf,子配置文件一般是以代理的server情况来定,一般情况下在主配置文件中有这么一句:include /etc/nginx/conf.d/*.conf;
这句指令在http模块下面,意思就是在nginx的主配置文件中引用了子配置文件。子配置文件定义了各种server模块,我一般都是有几个server定义几个子配置文件。
Nginx使用docker来运行,首先从docker hub下载nginx,最新版本:
docker pull nginx
下载好后,在angular的项目中建立一个dockerfile,放到项目的根目录中:
Dockerfile中就有两句:
FROM nginx:latest
COPY dist/Client /usr/share/nginx/html
然后在项目的根目录下(dockerfile所处目录)执行
docker build –t 21.33.129.180:5000/wallee/assetmanagementclient:1.0 .
运行完成后push到registry。不赘述了。
解释一下上面的dockerfile:首先将nginx镜像作为该镜像的基础镜像,(指定了基础镜像后,直到下一个FROM子句前都是以当前这个基础镜像作为执行环境)然后将构建好的前端项目复制到nginx相应的目录下面,这个目录需要在nginx的配置文件中定义好。我运行nginx镜像时都是先将nginx的主配置文件做好,然后运行docker时挂载到相应的容器目录上,这样我就可以保存好配置文件,修改也方便,直接修改宿主上的配置文件重启docker就生效。下面看一下nginx的主配置文件:
user nginx; worker_processes 1; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; #tcp_nopush on; keepalive_timeout 65; #gzip on; include /etc/nginx/conf.d/*.conf; }
主配置文件定义了一些必要的参数信息,然后定义了一个http块,http块中包含了若干的server块,server块描述的就是你nginx代理的服务。比如angular、asp.netcore等项目。
http块的最下方有这么一句:
include /etc/nginx/conf.d/*.conf;
这句话的含义就是将/etc/nginx/conf.d中所有的配置文件引入进来,相当于/etc/nginx/conf.d这个目录下放的就是子配置文件。
我们在/etc/nginx/conf.d这个文件夹中放了两个配置文件,一个是前端项目的,一个是后端项目的,前端项目的子配置文件如下:
server { listen 80; server_name bocAssets.client; location / { try_files $uri $uri/ /index.html; root /usr/share/nginx/html; index index.html index.htm; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } }
可以看到监听的是默认的80端口,在location块中我们定义了前端项目的访问路径,这和前面我们编写的dockerfile中copy指令所指定的路径是一致的。
后端项目的子配置文件如下:
server{ listen 5003; location /{ proxy_pass http://assetsApi:5003; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }
监听的是5003端口,这个端口不用暴露到外面,只会在二层网络上访问。注意在location中有这么一句:proxy_pass http://assetsApi:5003;这个指令就将对代理的5003端口的访问转移到了后端项目的地址上了,其中assetsApi这个是后端容器的名称,要拿名称访问的话,我们就需要将后端项目和前端项目放到一个网桥上,还记得我们前面运行的后端项目的命令吗?我们用--network=bocassets这个指令指定了docker运行的网络,接下来,就是运行前端项目了:
docker run \ --name assetmanagementclient \ -d -p 80:80\ --network=bocassets \ -v /home/wallee/nginx/nginx.conf:/etc/nginx/nginx.conf\ -v /home/wallee/nginx/conf.d:/etc/nginx/conf.d\ -v /home/wallee/nginx/log:/var/log/nginx \ wallee/assetmanagementclient:1.0
说明:首先当然是从搭建的私有仓库中吧这个镜像拿下来,使用docker pull命令,这里不赘述,拿到后我把镜像的tag改成了上面代码中最后那行的tag
--network=bocassets :将docker的网络设置成和后端的一样的网络,这样我们在nginx配置文件中已容器名访问的方式就能生效了。
-v /home/wallee/nginx/nginx.conf:/etc/nginx/nginx.conf:将宿主上面的nginx主配置文件目录挂载到容器上nginx的主配置文件目录上,在容器中这个主配置文件的目录是固定的,一定不能写错。
-v /home/wallee/nginx/conf.d:/etc/nginx/conf.d:将宿主上的nginx的子配置文件的目录挂载到容器上的子配置文件目录,这个目录实际上是由主配置文件中http模块下的include指定的。
-v /home/wallee/nginx/log:/var/log/nginx :将宿主上的日志目录挂载到nginx容器上的日志目录上,这个目录也是固定的,不要写错。
写到这里所有的工作差不多就完成了。