آموزش میکروسرویس Microservice - پیاده سازی یک میکروسرویس برای نمایش کالاها (بخش پنجم)

بخش اول : آشنایی با مفهوم میکروسرویسبخش دوم : ویژگی های اصلی یک میکروسرویسبخش سوم : تحلیل یک پروژه کوچک بر اساس میکروسرویس هابخش چهارم : شروع پیاده سازی یک پروژه فروشگاهی پیشنیاز 1 بخش پنجم : آموزش Node و Typescript برای تولید apiپیشنیاز 2 بخش پنجم : آموزش داکر و مفاهیم اولیهبخش پنجم : پیاده سازی یک میکروسرویس برای نمایش کالاها (همین مقاله)پیشنیاز 1 بخش ششم : آشنایی با Asp.net core 6 بخش ششم : پیاده سازی یک میکروسرویس برای کار با سبد خریدبخش هفتم : پیاده سازی یک میکروسرویس برای محاسبه قیمت و تخفیفاتبخش هشتم: پیاده سازی یک میکروسرویس برای ثبت سفارشبرای دیدن ویدیوهای من در مورد برنامه نویسی عضو این کانال شوید : https://t.me/mediapub_channelبخش چهارم را به عنوان مقدمه برای این بخش بخوانید.میخواهیم به کمک یک پروژه Node و با استفاده از ابزار Typescript و دیتابیس Mongo یک پروژه ساده برای دسترسی و تغییر کالاها ایجاد کنیم و در نهایت به کمک داکر و docker-compose نحوه داکرایز کردن این مجموعه را بررسی کنیم. اگر برنامه نویس جاوااسکریپت نیستید اصلا نگران نباشید، ذهن خودتونو آزاد کنید و با من همراه باشید تا به کمک Node (ابزار ران تایم جاوااسکریپت) یک پروژه کوچک طراحی کنیم.دقت کنید که تخصص در زبان برنامه نویسی جاوااسکریپت برای خواندن این مقاله لازم نیست.با مطالعه این مقاله، روش داکرایز کردن را برای پایتون و دات نت و php و ... هم یاد میگیرید ثانیا در مقالات بعدی با فریم ورک ها و زبان های برنامه نویسیِ مختلف میکروسرویس مینویسیم و داکرایز کردن آن ها را نیز بررسی میکنیم.انتظار من این است که خواننده بعد از مطالعه این نوشته بتواند موارد زیر را به راحتی تجزیه و تحلیل کنید :ایجاد یک پروژه Node js به کمک express و typescript به منظور تولید apiایجاد یک CRUD ساده به کمک node/express/typescript/mongooseساخت فایل dockerدرک و ایجاد docker imageدرک و ایجاد docker containerدرک و ایجاد docker-composeآشنایی با مفهوم volume و Bind mount در داکرآشنایی با مفهوم شبکه در داکرآشنایی با دستورات ایجاد و حذف image و ایجاد و حذف containerآشنایی با سکشن های docker-composeاگر با Node js آشنایی ندارید و نمیدانید چطور یک پروژه Node/Typescript بسازید این مقاله را بخوانید و سپس ادامه نوشته را پیگیری کنید. من این پروژه را به شکل آماده در Github قرار داده ایم (این لینک). برای دسترسی به تمپلیت اولیه که در مقاله فوق آن را پیاده کرده ایم کامیت اول ریپازیتوری موجود در github را ببینید.برای دسترسی به دیتابیس در پروژه Node از Mongoos استفاده میکنیم که بهتر است در محل کار آن را مانگوووس (با تکید روی س) صدا کنید. (برای خودتون میگم).بدون فوت وقت به ایجاد یک CRUD ساده برای کار با کالا می پردازیم.(Create/Read/Update/Delete) قبل از هر چیز روش ارتباط با Mongo را در یک پروژه Node/Typescript/Express بررسی میکنیم.1. مطمئن شوید که mongo را نصب کرده اید. (برای انجام جزئیات این مرحله، این مقاله را بخوانید، در انتهای مقاله به روش نصب mongo از طریق داکر اشاره شده است)2. روی پروژه ای که به شکل تمپلیت از Node/Typescript/Express تهیه کرده ایم کتابخانه Mongoos را نصب کنید.npm i mongooseعبارت فوق را در ترمینال Visual code مینویسیم (فرض من این است که شما مقاله مربوط به ساخت تمپلیت Node با Typescript را مطالعه کرده اید. در حقیقت ما در حال تکمیل آن پروژه هستیم)نصب mongoose3. برای اینکه تایپ اسکریپت یک دسترسی خوش ساخت به mongoos داشته باشد و بتواند با interface ها به ما کمک کند (متدهای دسترسی را با hint و intellisense ببینیم)، defenition type مربوط به mongoos را به کمک دستور زیر نصب میکنیم.npm i --save-dev @types/mongooseاز save-dev برای این استفاده میکنیم که ابزار توسعه را از کتابخانه های کاربردی جدا کنیم. این اتفاق در package.json می افتد. دو بخش جدا برای dependencies و devdependencies درست میشود. ابزار توسعه نیازی به انتقال حین build شدن ندارند. یعنی بعد از build، وجود این type ها ارزشی ندارد.4.کنترلر و روت جدیدی در پروژه تمپلیت میسازیم تا لیست کالا ها را دریافت کنیم. https://gist.github.com/3126b1d8c3f15dbb343f3daeaa18ee48 در فایل product.ts موجود در فولدر controllers متدی که لیست کالاها را برگرداند میسازیم. فعلا آن را پیاده سازی نمیکنیم. https://gist.github.com/2c4bc57e3eb754c54aaaf3c598e91ea6 روت getAllProducts را در فایل product.ts موجود در فولدر routes تعریف میکنیم که به controller.getAllProducts متصل است.در فایل server.ts هم یک روت جدید برای ارتباط با فایل route بالا تعریف میکنیم. روت های جدید را فقط در فایل routes/product.ts اضافه میکنیم و همه به نوعی Sub-Route آنچیزی هستند که در server.ts تعریف کرده ایم.5. به کانفیگ پروژه اطلاعات اتصال به مانگو را اضافه میکنیم. MONGO_OPTIONS و MONGO_HOST و MONGO ثوابتی هستند که ایجاد کردیم و در نهایت ثابت MONGO را به ثابت CONFIG در پایین فایل config.ts اضافه میکنیم. https://gist.github.com/bd85a29f94291e53d2bc184e77d5961e 6. در ادامه میخواهیم در فایل Server.ts به Mongo بوسیله Mongoose متصل شویم و کانکشن برقرار کنیم. https://gist.github.com/058a0004bb25a0a2dd12f84c6d57439b 7. فولدری به نام interfaces میسازیم. میخواهیم مدلی را بسازیم که به کمک آن مانگوس از ساختار product مطلع شود. https://gist.github.com/fabfdf61540d45a1f08f2cb334efc1ee 8. حالا اسکیمای product را در فولدر models میسازیم. https://gist.github.com/4feaa83d6472b1aa3e51a9b302998d89 timestamps : به طور خودکار تاریخ را در داکیومنت مونگو ثبت میکند9. حالا کنترلر product را تکمیل میکنیم. https://gist.github.com/6b2fa04d21ba91349a4bce7fbd315cb9 10. متد list را تست میکنیم. قبل از تست لازم است پروژه را با دستور زیر اجرا کنیم :nodemon source/server.tsهنوز دیتایی نداریم.11. با فشردن ctrl+c در ترمینال visual code میتوانیم اجرا را متوقف کنیم. متد createPost را برای ایجاد آیتم در دیتابیس درست کرده ایم. همچنین متدهای لازم برای یک crud را مینویسیم. برای این متدها روت های مناسب را در فایل routes/product.ts تعیین میکنیم. https://gist.github.com/e846a29f97ff5414fb9d80304f8e4ab9 12. پروژه را با دستور شماره 10 دوباره اجرا کنیم.13.برای تست متد add چون POST است از ابزاری مثل Postman استفاده میکنیم.بخش هایی که علامت گذاشتیم را به همین شکل در پستمن تکمیل میکنیم و ورودی را به شکل فوق مینویسیم و خروجی نشان میدهد که در دیتابیس یک رکورد (داکیومنت) ثبت شده است.میتوانید به کمک ابزار دیگری مثل compass یا Studio 3T محتوای دیتابیس را چک کنید14. با تست put خواهیم دید.متد ویرایشمتد حذفبه اهدافی که تعیین کرده بودیم در پروژه رسیدیم. CRUD با موفقیت اجرا میشود.ساخت فایل داکرقبل از هرکاری مطمئن باشید vpn شما متصل است. داکر برای گرفتن کتابخانه های مورد نیاز گاهی بدون وی پی ان به مشکل میخورد.ابتدا فایل داکر را میسازیم. https://gist.github.com/6c8b984d2e9bd44df2468dbf53593016 به محل قرار گیری dockerfile دقت کنید.برای ساخت فایل ایمیج دستور زیر را در محلی که فایل package.json قرار دارد مینویسیم:docker build . -t catalogapiو برای ساختن کانتینر از این ایمیج از دستور زیر استفاده میکنیم و پورت 4300 داخلی را به 4500 خارجی مپ میکنیم (catalogapi اولی اسم کانتینر و دومی اسم imageی است که کانتینر از روی آن ساخته میشود):docker run -d -p 4500:4300 --name catalogapi catalogapiو میبینیم که پروژه اجرا میشود.برای دیدن لاگ ِ کانتینر دستور زیر را مینویسیم:docker logs -f a18b3a98e3faمقدار a18b3a98e3fan شناسه کانتینر ماست که به کمک دستور docker ps قابل دستیابی است.docker psخروجی لاگ یک خطا نشان میدهد.خطایی که به دلیل عدم دسترسی به دیتابیس میبینیمخطای فوق یعنی سرویس ما نمیتواند به monogo متصل شود و طبیعیست. چون ما پروژه را داکرایز کردیم و در کانتینر ما، فولدر build داخل یک سیستم لینوکسی قرار دارد و در داخل یک سیستم لینوکسی پروژه اجرا شده است. پورت 4300 که پورت اجرا شده داخلی است را به 4500 خارجی مپ کردیم که از بیرون به api ها دسترسی داشته باشیم. با این اوصاف واضح است این کانیتنر (یعنی catalog) به mongo که خودش یک کانتینر دیگر است دسترسی ندارد و همدیگر را نمیبینند. ( در مرحله 1 در این مقاله ما mongo را داکرایز کردیم).یک راه حل برای این مشکل مشخص کردن نام یک container برای container دیگر است. در تصویر فوق صراحتا نوشته شده که ارتباط با 127.0.0.1:27017 ممکن نیست ولی حتما باید ارتباط با mongo:27017 ممکن باشد (فرض کنید mongo اسم کانتینر مربوط به مانگو است) پس در کد به جای 127.0.0.1 اسم کانتینر را مینویسیم. اما بهتر است اینکار را با تعریف environment انجام دهیم. در کد نیز این قابلیت (استفاده از environment ها ) را به نام MONGO_URL لحاظ کرده ایم. پس کافیست موقع اجرای ایمیج مربوط به catalog.api این کلید از environment را تعریف میکنیم.docker run -d --network node-webapp-network --name mongodb mongo docker run -d -p 4500:4300 --env 'MONGO_URL=mongodb:27017' --network node-webapp-network01 --name catalogapi catalogapiدستور اول، داکرِ مربوط به ایمیجِ mongo را به نام mongodb اجرا میکند.دستور دوم داکر مربوط به ایمیج ِcatalogapi را به نام catalogapi اجرا میکند و یک آیتم environment vatraible هم به نام MONGO_URL تعریف میکند و مقدار میدهد.نکته مبهم ولی ساده در این دو دستور آپشن network است که باید در هر دو یکسان باشد، یعنی هر دو در یک شبکه باشند که بتوانند همدیگر را ببینند. این شبکه را باید قبل از اجرای دو دستور فوق با دستور زیر بسازیم.docker network create node-webapp-networkدر تصویر اول دو کانتینر در دو نتورک جدا هستند و ارتباط ممکن نیست. در صویر دوم یک شبکه برای هر دو کانتینر تعریف کرده ایم و امکان برقراری ارتباط بین دو کانتینر توسط نام کانتینرها برقرار است.اما راه حل ساده تر برای یکسان سازی شبکه و همینطور مدیریت ارتباط بین میکروسرویس ها استفاده از docker-compose است.ساخت فایل docker-composeداکر کامپوز مسئول ایجاد، اجرا و مدیریت ارتباط بین میکروسرویس هاست. چون برای همه است، پس محل قرار گیری آن نباید داخل فولدر این میکروسرویس باشد بلکه یک سطح عقب تر باید قرار داشته باشد.(که برای دسترسی به میکروسرویس های دیگر به زحمت نیفتیم و به میکروسرویس های دیگر هم دسترسی داشته باشد)محل درست قرارگیری فایل docker-composeیک فایل docker-compose.yaml با بخش های اصلی میسازیم و برای محیط های مختلف مثل development یا production این فایل را override میکنیم. چون فعلا هدف ما development است یک فایل docker-compose.override.yaml هم میسازیم.محتوای یک فایل کامپوز به شکل زیر است. https://gist.github.com/b0f2bc71b8e4ed999627643959bf6b37 https://gist.github.com/31088ec45ff14cee3cb3db896c2f8c7a همانطور که میبینید ما به ازای هر سرویس یک سکشن داریم. فعلا دو سرویس یکی مخصوص دیتابیس مونگو و دیگری مخصوص به catalog داریم که نامگذاری دلخواه میکنیم و در docker-compose و docker-compose.override آنها را به شکل فوق تکمیل میکنیم.نکات :ورژن داکر کامپوزی که از آن استفاده میکنیم در بالای فایل نوشته شده است و 3 است.اجرای داکر کامپوز در محلی که فایل کامپوز قرار دارد انجام میشود، اگر ایمیج ها وجود نداشته باشند ابتدا pull میشوند(مثل mongo) و چنانچه برای آنها محلی به عنوان context (بخش context را در فایل فوق ببینید) معرفی شده باشد با پیدا کردن dockerfile ، ایمیج آنها ساخته میشود. اگرایمیج آنها قبلا ساخته شده باشد دوباره ساخته نخواهند شد. این اطلاعات به همراه نام image در فایل docker-compose.yml قرار دارد چنانچه این اطلاعات در development شما با production شما متفاوت است نباید آنها را در فایل docker-compose.yml بنویسید و باید آن را در override ها ذکر کنید.در فایل docker-compose-orverride.yaml همان سکشن های مربوط به سرویس ها تکرار شده اند اما محتوای آنها متفاوت است و نیاز به تکرار آنچه در docker-compose اصلی نوشته ایم نیست.در داکر کامپوز باید مشخص شود چه پورت داخلی از کانتینر به بیرون مپ میشود. مثلا چون پورت دیفالت داخلی مانگو 27017 است آن را به یک پورت بیرونی مپ کرده ایم (مثلا در اینجا همان شماره را برای پورت بیرونی درنظر گرفته ایم) یا در سرویس catalog.api چون node/express ما روی پورت 4300 اجرا میشود آن را به یک پورت در خارج مپ کرده ایم. 4000 پورت انتخابی ماست و بوسیله پستمن بعد از ایجاد کانتینر به این پورت دسترسی داریم.سکشن مهم depends_on مشخص میکند که اجرای این سرویس به کدام سرویس وابستگی دارد. یک نوع dependency order را حین اجرای ایمیج ها رعایت میکند. در اینجا یعنی catalog.api بعد از اجرای catalog.db اجرا میشوند.سکشن environment برای سرویس هایی که از environment چیزی میخوانند لازم است. مثلا ما در سرویس catalog.api بخشی از اطلاعات اتصال دیتابیس را از environment میخوانیم.بخش volumes برای سکشن هایی که نیاز به ذخیره اطلاعات روی دیسک دارند لازم است. درادامه این بخش را توضیح خواهم داد.انواع روش های ذخیره سازی در داکردر مورد volume در این مقاله به اختصار صحبت کردیم. گفتیم چون کانتینری که از ایمیج ساخته میشود به لحاظ پایداری مطمئن نیست و ممکن است Stop یا Remove شود (توسط ما یا به دلیل مشکلات نرم افزاری) بهتر است دیتاها را در جای دیگری نگه داریم. مثلا اگر سرویس آپلود عکس داریم دلیلی ندارد در خود کانتینر عکس ها را نگه داریم، چون با حذف کانتینر، تصاویر نیز از بین میروند. مثلا فرض کنید داخل کانتینر یک فولدر images داریم که عکس ها در آن قرار میگیرند. کار درست این است که این فولدر را به یک فولدر خارجی یا یک docker volume خارجی مپ کنیم که هر اتفاقی در فولدر images افتاد در آن فولدر هم رخ دهد و ما دیتا را در یک جای مطمئن داشته باشیم. در مورد دیتابیس ها نیز به همین شکل عمل میکنیم و فایل های db را به کمک volume یا bind mount به یک محیط خارجی مپ میکنیم. در تصویر بالا volume و bind mount را می‌بینید، یک حالتِ tmpfs هم داریم که اطلاعات را در memory نگه میدارد و دسترسی به آن سریع است، اما به لحاظ پایداری و حجم دیتا باید ملاحظاتی را در نظر بگیریم که در این مقاله فرصت پرداختن به آن نیست.در فایل docker compose اگر قرار باشد از حالت docker volume استفاده کنیم یک سکشن به نام Volume در سطح اول آن تعریف میکنیم و اسم ولیوم هایی که میخواهیم بسازیم را مشخص میکنیم. حالا در سکشن مربوط به دیتابیس این docker volume ها را به آدرس های داخل کانتینر مپ میکنیم. در mongo محل ذخیره سازی اطلاعات دیتابیس و کانفیگ ها فولدرهایی هستند که میبینید:یک نمونه از فایل docker-composeبرنامه داکر دسکتاپ هم volume ها را نشان میدهدبا دستور زیر هم میتوانیم لیست volume ها را ببنیمdocker volume lsاگر هم بخواهید به شکل bind mount عمل کنید یعنی یک فولدر روی دیسک را به فولدری داخل کانتینر مپ کنید از روش زیر عمل کنید.در این حالت دو فولدر در مسیرهای داده شده روی سیستم عاملِ Host تعریف میکنیم(چون من روی ویندوز کدنویسی میکنم برای توسعه فعلا دو فولدر در درایو c معرفی کرده ام که در تصویر میبینید). به محض اجرای داکر کامپوز، این دو فولدر ساخته میشوند و محتوای فولدرهای متناظر داخل کانتینر درون این فولدرها ساخته میشود. دقت کنید که دیگر داکر، volume مشخص برای اینکار نمی سازد.(در داکر دسکتاپ بخش Volume اطلاعاتی از این فولدرها نمایش نمیدهد)اگر نوع ذخیره سازی شما از نوع docker volume است (و Bind mount نیست)، اگر میخواهید محتوای volume های درست شده روی ویندوز را ببینید از آدرس زیر در پنجره explorer استفاده کنید:\\wsl$\docker-desktop-data\version-pack-data\community\docker\volumes\بطور کل در هر دو حالت شما این مزیت را بدست آوردید که فایل های دیتابیس شما در محلی فیزیکی روی سیستم ذخیره شده که حالا یا به شکل docker volume است و یا به شکل یک فولدر قابل دسترسی(bind mount) است. هر اتفاق بدی برای کانتینر شما بیفتد دیتای شما از بین نخواهد رفت. هر بار کانتینر را از بین ببرید و دوباره بسازید میتوانید به این دیتا دسترسی داشته باشید به شرط آنکه در فایل docker-compose مسیر ها را درست تعریف کنید.برای اجرای docker-compose ابتدا مطمئن میشویم کانتینر برای تست از روی ایمیج ها نداشته باشیم. بهتر است قبلیها را پاک کنیم.ابتدا لیست کانتینرها را چک کردیم. بعد با دستور force که با -f مشخص است کانتینرهای موجود را پاک کردیم. سپس ایمیج ها را لیست گرفتیم و در نهایت فقط یکی از ایمیج ها را پاک کردیم و به ایمیج mongo دست نزدیم.(نیازی به پاک کردن ایمیج چیزهایی که pull کردیم نیست چون با اجرای دوباره docker-compose اگر وجود نداشته باشند دوباره pull میشوند) حالا در فولدری که docker-compose وجود دارد دستور زیر را مینویسیمdocker-compose -f .\docker-compose.yml -f .\docker-compose.override.yml up -dهمانطور که میبینید بر اساس اطلاعات موجود در docker-compose ایمیج ها به container تبدیل میشوند و همه چیز اجرا میشود.همیطور با دستور زیر میتوانید سرویس ها را down کنید و کانتینرها ابتدا stop و سپسremove میشوند.در این بخش نه تنها توانستیم اولین میکروسرویس خود را بسازیم بلکه با ایجاد docker-compose هم آشنا شدیم. پیش از این گفتیم docker-compose مختص این میکروسرویس نیست، به همین دلیل آن را خارج از فولدر این پروژه قرار دادیم. در بخش های بعد ما میکروسرویس های دیگری خواهیم نوشت که وظایف مختلفی دارند اما همه آنها با تکمیل همین docker-compose به مرحله عملیاتی و اجرا خواهند رسید.کدهای مربوط به این بخش را در این ریپازیتوری از گیتهاب ببنید (کامیت پایانی تا اینجا "add and modify docker compose")