基於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服務來進行生產級的容器編排。
作者:周旭龍
出處:https://edisonchou.cnblogs.com
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連結。