最近使用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容器上的日志目錄上,這個目錄也是固定的,不要寫錯。
寫到這里所有的工作差不多就完成了。