راه اندازی یک پروژه نمونه از میکروسرویسها در Asp.net Core و Docker - قسمت اول

در این مقاله سعی داریم یک نرم افزار بسیار ساده بر پایه معماری میکروسرویس با Asp.net Core و روی بستر docker آماده و راه اندازی کنیم. سطح مطالب این مقاله مناسب توسعه دهندگان بوده و هدف این است که محصول تولیدی آنها برای deploy در محیطهای container ای مناسب باشد.مقدمهگاهی با پروژه هایی سروکار داریم که از قسمت هایی با مرزهای حدودا مشخص تشکیل شده است. این مرزبندی ها را اغلب اوقات میتوان در فرایندهای سازمان مشتری مشاهده کرد. به بخش های مختلفی که توسط این مرزبندی ها تشکیل میشوند اصطلاحا Bounded Context یا BC میگویند. در معماری میکروسرویس معمولا به ازای یک یا چند BC (بر اساس اندازه یا اهمیت) یک میکروسرویس ایجاد میشود.در این مقاله میخواهیم سیستم یک آموزشگاه را تعریف کرده و راه حلی (Solution) برای آن ایجاد کنیم. BC اصلی در این سیستم آموزشگاه (School) و BC دیگر مربوط به مالی یا درآمد (Income) میباشد. برای هرکدام از BC ها میکروسرویسی به همان نام وجود داشته و هر دو میکروسرویس به اطلاعات یکدیگر وابستگی دارند. در ابتدا این دو میکروسرویس را بصورت مستقل راه اندازی میکنیم و در مرحله بعدی میکروسرویسها را پشت یک دروازه (gateway) قرار میدهیم که توسط تنها یک آدرس قابل دسترس شوند.پیشنیاز هابرای شروع فرض بر این است که شما با برنامه نویسی در Asp.net Core، زبان C# و مفاهیم اولیه داکر آشنایی داشته و داکر را روی سیستم عامل خود یا یک ماشین دیگر نصب کرده باشید. درصورتی که با موارد یاد شده آشنایی ندارید این مقاله مناسب شما نخواهد بود و درصورتی که با مراحل نصب داکر آشنایی ندارید میتوانید ابتدا با جستجو در اینترنت نحوه نصب آن را فراگرفته و نصب کنید.کدهای مقالهتمامی کدهای نمونه این مقاله در آدرس زیر قرار دارد:https://github.com/MostafaTech/MicroservicesOnDocker/tree/part1_exposed-servicesبرنچ part1_exposed-services شامل کدهای قسمت اول مقاله و برنچ master شامل آخرین نسخه و کامل شده کدها در انتهای مقاله میباشد.مخزن کد در گیتهاب شامل یک پروژه UI با VueJS نیز هست که درخواست ها به سرویسها در درون اون ارسال میشن و نیازی به ارسال دستی درخواستها نیست.کدها و تنظیمات بصورتی نوشته شدن که بدون نیاز به هیچگونه تغییر، در مدهای development و production کار کنند.تعریف ساختار پروژه در ویژوال استودیوسیستم ما Solution ای با نام MicroservicesOnDocker و حاوی پروژه های زیر است:پروژه MicroservicesOnDocker از نوع Class Library برای تعریف domainپروژه MicroservicesOnDocker.Infrastructure از نوع Class Library برای لایه Infrastructure جهت تعریف ابزارهایی که بصورت مشترک در همه پروژه ها استفاده میکنیمپروژه MicroservicesOnDocker.School.Api از نوع Asp.net Core - Api برای میکروسرویس School تنظیم شده روی پورت 57801پروژه MicroservicesOnDocker.Income.Api از نوع Asp.net Core - Api برای میکروسرویس Income تنظیم شده روی پورت 57802در همین ابتدا CORS را در سرویسهای خود فعال کنید که درصورتی که از ajax برای ارسال درخواستها استفاده میکنید دچار مشکل نشوید. در کلاس Startup هر دو پروژه api:public void ConfigureServices(IServiceCollection services) { ... services.AddCors(); ... }public void Configure(IApplicationBuilder app, IHostingEnvironment env) { ... app.UseCors(c => c.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin().Build()); ... }در پروژه MicroservicesOnDocker.School.Api یک api برای خواندن لیست دانشجویان قرار دارد که بصورت زیر عمل میکند:Request => [GET] http://localhost:57801/api/students Response => [{&quotid&quot:1,&quotname&quot:&quotJohnny Depp&quot},{&quotid&quot:2,&quotname&quot:&quotBrad Pitt&quot},{&quotid&quot:3,&quotname&quot:&quotLeonardo DiCaprio&quot},{&quotid&quot:4,&quotname&quot:&quotMichael Jackson&quot},{&quotid&quot:5,&quotname&quot:&quotFreddy Mercury&quot}]و در پروژه MicroservicesOnDocker.Income.Api یک api برای خواندن لیست پرداختهای دانشجویان قرار دارد که بصورت زیر عمل میکند:Request: [GET] http://localhost:57802/api/payments Response => [{&quotstudentId&quot:1,&quotstudentName&quot:&quotJohnny Depp&quot,&quotcourseId&quot:1,&quotcourseName&quot:&quotActing Course&quot,&quotpayAmount&quot:2000.0},{&quotstudentId&quot:2,&quotstudentName&quot:&quotBrad Pitt&quot,&quotcourseId&quot:1,&quotcourseName&quot:&quotActing Course&quot,&quotpayAmount&quot:2000.0},{&quotstudentId&quot:4,&quotstudentName&quot:&quotMichael Jackson&quot,&quotcourseId&quot:2,&quotcourseName&quot:&quotMusic Course&quot,&quotpayAmount&quot:3000.0}]تا اینجا میکروسرویسهای ما بصورت مستقل و کاملا جداگانه عمل میکنند. برای اینکه بتونیم این سرویسهارو روی داکر به اجرا در بیاریم از docker-compose استفاده میکنیم. به این صورت که در شاخه اصلی پروژه یک فایل با نام docker-compose.yml با محتویات زیر اضافه میکنیم:version: '3' services: school_service: image: mcr.microsoft.com/dotnet/core/aspnet:2.2 volumes: - ./school_service:/app working_dir: /app command: dotnet &quotMicroservicesOnDocker.School.Api.dll&quot ports: - 5001:80 income_service: image: mcr.microsoft.com/dotnet/core/aspnet:2.2 volumes: - ./income_service:/app working_dir: /app command: dotnet &quotMicroservicesOnDocker.Income.Api.dll&quot ports: - 5001:80نکاتی درباره محتویات فایل بالا:دو سرویس school_service و income_service تعریف شده که در هردو، فایلهای پابلیش شده پروژه ها در پوشه app کانتینر قرار میگیرد که فایل dll اصلی پروژه با دستور dotnet اجرا میشود.ایمیج mcr.microsoft.com/dotnet/core/aspnet:2.2 برای اجرای هر دو سرویس استفاده شده که شامل یک ماشین لینوکس که روی آن dotnet core runtime نصب شده است.برای سرویس school_service پورت 5001 و برای income_Service پورت 5002 درنظر گرفته شده. به این صورت که داکر پورت 80 که asp.net برای گوش دادن به درخواستهای سرویس روی container باز کرده رو به 5001 و 5002 در ماشینی که داکر روی اون اجرا هست تبدیل میکنه و در واقع شما با ارسال درخواست به ماشین حاوی داکر روی پورت 5001 دارید به پورت 80 container ای که حاوی سرویس school_service هست درخواست ارسال میکنید. امیدوارم که شفاف گفته باشم.دستور working_dir باعث میشه که نیازی نباشه در آدرس دهی ها، آدرس پوشه حاوی فایلهای پروژه رو همیشه وارد کنید که در اینجا قبل از نام dll سرویس هست ولی در بخش بعدی استفاده بهتری هم داره که بعدا اشاره میکنم.یک پوشه با نام publish_ ایجاد کنید و از دو پروژه api خود پابلیش بگیرید و در مسیرهای زیر فایلهای پابلیش شده رو قرار بدید:project MicroservicesOnDocker.School.Api => ..\_publish\school_service project MicroservicesOnDocker.Income.Api => ..\_publish\income_serviceمیتونید ویژوال استودیو رو طوری تنظیم کنید که موقع پابلیش در مسیرهای بالا فایلهارو پابلیش کنه. ضمنا تنظیمات پابلیش رو هم بصورت پیشفرض نگه دارید که بصورت زیر هستن:Deployment Mode: Framework-Dependent Target Runtime: Portableفایل docker-compose.yml را در شاخه اصلی پوشه publish_ کپی کنید و یک terminal یا cmd در پوشه مذکور باز کرده و با دستور زیر پروژه ها را راه اندازی کنید:~ docker-compose upنکته: اگر سیستم عامل شما ویندوز هست همین دستور اکتفا میکنه ولی اگر در لینوکس هستید کافیه قبل از دستور فوق sudo را نیز وارد کنید. اگر شما هم مثل من سیستم عامل ویندوز دارید ولی داکر رو روی ماشین مجازی لینوکسی نصب کردید میتونید با استفاده از پروتکل samba یک پوشه share ویندوزی روی سیستم عامل لینوکس خود راه بندازید که پابلیش هایی که میگیرید رو روی ماشین مجازی خود کپی کنید و در اون ماشین دستورات داکر رو اجرا کنید. فقط قبلش یادتون باشه ip ماشین رو بدونید که در browser بتونید سرویسهارو باز کنید.هر دو سرویس با موفقیت اجرا شدندحالا میتونید یک browser باز کرده و درخواست های زیر رو ارسال کنید:[GET] http://localhost:5001/api/students [GET] http://localhost:5002/api/paymentsو پروژه شما روی داکر جواب مناسب رو ارسال خواهد کرد. برای خاموش کردن سرویسها میتونید از Ctrl+C استفاده کنید.ارتباط سرویسها با یکدیگریکی از مزیت های داکر این است که میتواند بین چند container یک شبکه مجازی ایجاد کرده که مجهز به DNS اختصاصی میباشد. در docker-compose تنظیمات این شبکه مجازی بطور اتوماتیک انجام میشود و در حالات ساده نیاز به هیچ تنظیم اضافه ای نداریم. روش کار به این صورت است که تمام سرویسهایی که در docker-compose تعریف میشوند مشابه ماشینهایی درون یک شبکه ایزوله قرار میگیرند که توسط نام هر سرویس توسط سرویسهای دیگر قابل دسترس میباشند. ارتباطات درونی کاربردهای زیادی از جمله ارتباط با سرویس database، سرویس cache، سرویس messaging و ... دارند. با استفاده از این امکان میخواهیم بین دو میکروسرویس خودمان ارتباطی ایجاد نماییم.ابتدا به پروژه MicroservicesOnDocker.Infrastructure پکیج نوگت RestSharp را اضافه میکنیم. پکیج RestSharp این امکان را به ما میدهد که برای یک آدرس خاص درخواست http ارسال کنیم و جواب آن را بصورت serialize شده تحویل بگیریم. سپس کلاس زیر را به آن پروژه اضافه میکنیم:public class ServiceHelpers { #if DEBUG public const string SchoolService = &quotlocalhost:57801&quot public const string IncomeService = &quotlocalhost:57802&quot #else public const string SchoolService = &quotschool_service&quot public const string IncomeService = &quotincome_service&quot #endif public static async Task<T> GetServiceData<T>(string service, string action) { var restClient = new RestSharp.RestClient($&quothttp://{service}/api/&quot); var restRequest = new RestSharp.RestRequest(action, RestSharp.Method.GET, RestSharp.DataFormat.Json); var restResponse = await restClient.ExecuteAsync<T>(restRequest); if (restResponse.IsSuccessful) { return restResponse.Data; } return default(T); } }در ابتدا دو constant برای هر سرویس تعریف کردیم که در حالت های Debug (یعنی موقعی که در ویژوال استودیو کلید F5 را میزنیم) و Release (یعنی وقتی پابلیش میگیریم) مقادیر متفاوت خواهند داشت. در حالت دیباگ پورت هایی که برای سرویسهایمان در زمان توسعه اختصاص داده ایم در ثابتها قرار خواهند گرفت و در حالت release به نامی که برای هر سرویس در docker-compose تعیین کرده ایم و پورت 80 آنها در ثابتها قرار میگیرند. اگر به متنی که در docker-compose نوشته بودیم توجه کنید میبینید که هر سرویس درواقع روی یک کانتینر با نام آن سرویس و پورت 80 بالا می آید و ما برای دسترسی بیرونی پورت 80 هر کانتینر را به پورت های 5001 و 5002 در ماشینی که داکر روی آن اجرا شده تبدیل کرده ایم. به این عمل expose کردن سرویس میگویند. ولی وقتی که ما از درون سرویسهای داکر قصد ارتباط با دیگر سرویسها را داشته باشیم به پورتهای بیرونی نیازی نداریم و میتوانیم با کانتینری که سرویس مورد نظر را اجرا کرده بصورت مستقیم ارتباط برقرار کنیم. حتی اگر آن سرویس expose نشده باشد.حال میخواهیم سرویس school را به سرویس income متصل کنیم. برای همین api دریافت اطلاعات دانشجویان را به شکل زیر تغییر میدهیم:[HttpGet] public asyncTask<ActionResult<IEnumerable<Dtos.StudentDto>>> Get() { var students = _ds.GetStudents(); var payments = awaitInfrastructure.ServiceHelpers.GetServiceData<List<Dtos.StudentPaymentDto>>(Infrastructure.ServiceHelpers.IncomeService, &quotpayments&quot); return students.Select(x => new Dtos.StudentDto { StudentId = x.Id, StudentName = x.Name, hasPaid = payments.Any(y => y.StudentId == x.Id) }).ToList(); }در خط زیر ما از طریق http به یک سرویس داخلی داکر متصل شدیم:var payments = awaitInfrastructure.ServiceHelpers.GetServiceData<List<Dtos.StudentPaymentDto>>(Infrastructure.ServiceHelpers.IncomeService, &quotpayments&quot);نکته: روشی که در این مثال برای پر کردن لیست دانشجویان استفاده شده از نظر پرفورمنس کاملا اشتباهه و در برنامه واقعی از این روش استفاده نکنید!الان میتونیم دوباره از سرویسها پابلیش بگیریم و پروژه رو روی داکر اجرا کنیم. برای اجرا و ساختن مجدد image ها از دستور زیر استفاده کنید:~ docker-compose up --buildنکته: درصورتی که میخاهید هنگام اجرای پروژه از ترمینال همچنان استفاده کنید در جلوی دستور بالا از سوییچ d- و برای خاموش کردن پروژه از docker-compose stop استفاده کنید.تست api دانشجویان:Request => [GET] http://localhost:5001/api/students Response => [{&quotid&quot:1,&quotname&quot:&quotJohnny Depp&quot,&quothasPaid&quot:true},{&quotid&quot:2,&quotname&quot:&quotBrad Pitt&quot,&quothasPaid&quot:true},{&quotid&quot:3,&quotname&quot:&quotLeonardo DiCaprio&quot,&quothasPaid&quot:false},{&quotid&quot:4,&quotname&quot:&quotMichael Jackson&quot,&quothasPaid&quot:true},{&quotid&quot:5,&quotname&quot:&quotFreddy Mercury&quot,&quothasPaid&quot:false}]خسته نباشید!در این قسمت از مقاله یاد گرفتیم که مجموعه ای از سرویسها که با asp.net core در محیط ویژوال استودیو را روی داکر اجرا کنیم و ارتباطی بین آنها برقرار کنیم. در قسمت بعدی میخواهیم تمام میکروسرویسها رو پشت یک gateway قرار دهیم و از expose کردن تک تک سرویسها جلوگیری کنیم.