سرویس پایتون Flask در محیط عملیاتی (Production)

پیاده‌سازی سرویس پایتون Flask در عملیات؛ Productionفلسک / Flask یکی از کتابخانه‌های پایتون برای پیاده‌سازی وب‌سرویس و API است و یک میکروفریمورک ساده اما سبک محسوب می‌شود.در این نوشتار به چگونگی استفاده از Flask در محیط Production (محیط عملیاتی یا محصول) بصورت یک سرویس لینوکسی پرداخته می‌شود. هرچند استفاده از داکر راه بسیار مناسبی برای استقرار در Production است اما برخی از مواقع و بنا به ملاحظاتی چندان بهینه نیست. بعنوان یکی از طرفداران سرسخت داکر باید به این نکته اشاره کنم: استفاده از داکر در مواقعی که منابع پایینی داریم (مثلاً ۵۱۲ مگ رم) یا هنگامی که در حال کار با یک رزبری‌پای هستیم، چندان منطقی بنظر نمی‌رسد. البته سعی خواهم کرد همین شیوه را با استفاده از داکر در نوشته دیگری بیاورم.برای هر بخش سعی کردم توضیحاتی داشته باشد و فقط Command و Code نباشد تا خواننده نسبت به آن دید پیدا کند، که منجر به طولانی‌شدن مطلب شده است. محیط Production چیست؟مهم نیست چه چیزی را توسعه می‌دهیم، زمانی که کد یا محصولی توسعه داده می‌شود، در محیط توسعه یا Development قرار داریم. در Development طبیعی است که خطاها مشاهده شوند و کسی در آنجا محصول را Attack نمی‌زند و از همه مهم‌تر کاربر / مشتری با آن کار نمی‌کند. در Production نه تنها کاربر / مشتری با محصول کار می‌کند بلکه معمولا انواع حملات روی آن صورت می‌گیرد.زمانی که صحبت از کاربر / مشتری می‌شود، در واقع صحبت از یک کسب‌وکار است؛ محصول، وب‌سایت، سخت‌افزار و ... تفاوتی ندارند؛ در Production حق اشتباه نداریم. منظورم فقط باگ نیست؛ وقتی یک خطای برنامه به کاربر نمایش داده شد، یا اینکه بخاطر سهل‌انگاری در پیکربندی یک سرویس، آسیب‌پذیر شویم و بعدا Attack (رَکَب) بخوریم، مساله باگ نیست، این یعنی اهمیت نداشتن کسب‌وکار و در نهایت رسیدن به نقطه شکست.همه تلاش ما در Production باید این مساله باشد که حداکثر پایداری، صحت و امنیت را تامین کنیم.۱. معماریمعماری پیشنهادی برای یک وب‌سرویس‌ اینگونه است:معماری پیشنهادیفلسک / Flask ، وب‌سرورِ Production نیست!زمانی که وب‌سرویسی توسعه داده می‌شود (در محیط Development) وب‌سرور داخلی Flask اجرا شده و درخواست‌ها را مستقیما پردازش می‌کند. اما در محیط‌ Production ارسال درخواست به وب‌سرور داخلی Flask اشتباه است و باید از WSGI استفاده کنیم.استفاده از Gunicornبرای WSGI از Gunicorn استفاده می‌کنیم (اطلاعات بیشتر در این مقاله آمده است) که بصورت سرویس لینوکسی با systemd پیاده‌سازی خواهیم‌کرد.پروکسی معکوس / Reverse Proxyدر لبه سرور، Reverse Proxy قرار دارد؛ درخواست ابتدا به پروکسی می‌رسد و پروکسی آن‌ را به WSGI ارسال می‌کند. استفاده از Reverse Proxy اختیاری است اما شدیداً توصیه‌ می‌شود و همچنین امکانات زیر را در اختیار ما می‌گذارد:امکان استفاده از HTTPs بدون نیاز به تغییر در WSGI برای پیاده‌سازی TLS/SSLامکان پخش‌بار و Load Balancingلاگ‌گیری ساده‌تربررسی درخواست‌ها از نظر امنیتی و Reject درخواست‌های مشکوک سرویس‌های متعددی برای Reverse Proxy وجود دارند: Apache ، Nginx ، HA-Proxy و … . هرچند Apache و Nginx وب‌سرور هستند اما در حالت Reverse Proxy نیز کار می‌کنند. معمولاً برای پروکسی از Nginx استفاده می‌شود.پروکسی اختیاری است، اما بشدت، بشدت و بشدت استفاده از آن را توصیه می‌کنم، مگر در مواردی که واقعا با کمبود منابع مواجه هستید و درخواست‌های کاملاً کنترل‌شده‌ای به WSGI ارسال خواهند شد.۲. سورس‌کدکتابخانه Flask با دستور زیر نصب می‌شود:pip3 install flaskبرای شروع، کد زیر را با Flask پیاده‌سازی و در فایلی با نام webapp.py ذخیره می‌کنیم:from flask import Flask from os import urandom app = Flask(__name__) app.config.update(SECRET_KEY = urandom(24)) @app.route('/') def hello_world(): return 'Hello, World!' if __name__ == &quot__main__&quot: app.run()این کد ساده زمانی که اجرا شود، روی پورت ۵۰۰۰ (پیش‌فرض) یک وب‌سرور را راه‌اندازی می‌کند:وب‌سرور داخلی Flaskبه هشدار قرمز رنگ دقت کنید :)This is a development server. Do not use it in a production deployment.اگر در مرورگر آدرس را وارد کنیم طبیعتا نتیجه زیر مشاهده می‌شود:ریسپانس وب‌سرور مقداردهی SECRET_KEYبرای رمزنگاری اطلاعات session که در cookie ذخیره می‌شود، حتماً باید SECRET_KEY تعریف شود. مقدار SECRET_KEY را نباید هاردکد شده یا در فایل نگه‌داری کرد؛ در اینصورت اگر سورس‌کد ما در محیط‌های مختلف استفاده شود، همگی SECRET_KEY های یکسانی را خواهند داشت و در صورت لو رفتنِ مقدار آن، برنامه‌ی ما در برابر حملات Session Hijacking آسیب‌پذیر می‌شود. برای حل این مشکل از تابع urandom استفاده می‌کنیم که برای هربار اجرا یک مقدار تصادفی برای SECRET_KEY مقداردهی می‌شود. دقت کنید که SECRET_KEY برای هربار اجرای Flask مقداردهی می‌شود نه هر درخواست!app.config.update(SECRET_KEY= urandom(24))تا اینجا وب‌اپلیکیشن راه‌اندازی شده است.۳- پل ارتباطی وب‌ سرور‌ با وب‌اپلیکیشن (WSGI)برای Web Server Gateway Interface یا WSGI معادل فارسی مناسبی پیدا نکردم؛ در ویکی‌پدیا واسط دروازه کارساز وب نام‌گذاری شده اما بنظرم پل ارتباطی وب‌سرور به ذهن نزدیک‌تر است.همانطور که گفته شد برای WSGI از gunicorn استفاده می‌کنیم. نصب آن نیز به کار خاصی نیاز ندارد:pip3 install gunicorn برای تست WSGI و کارکرد آن با وب‌اپلیکیشن بصورت زیر اجرا می‌کنیم:‫‪gunicorn‬‬ ‫‪‫‪-w 4 --bind‬‬ ‫‪127.0.0.1:5000‬‬ --pid /tmp/webapp.pid ‫‪webapp:app‬‬نتیجه اجرای gunicornپارامتر bind تعیین می‌کند که WSGI روی کدام آدرس و پورت گوش کند. اگر قرار است از پروکسی استفاده شود حتماً روی 127.0.0.1 یا localhost تنظیم شود.پارامتر pid هم مسیر Process ID (همان 734) را تعیین می‌کند که برای سرویس استفاده خواهدشد.پارامتر w تعداد worker یا همان پروسس‌ها را مشخص می‌کند. دقت کنید این‌ها Thread نیستند، پروسس‌های فرزند از یک پروسس اصلی هستند (در اینجا 734)؛ یعنی یک gunicorn اصلی داریم که چهار پروسس موازی را بعنوان فرزند از آبجکت app تشکیل داده است.همچنین اگر برنامه‌ی شما Multi-thread باشد thread ها بعنوان فرزند برای هر worker ایجاد می‌شوند.ورکرهای gunicorn۴. سرویس gunicornبرای مانیتور و مدیریت راحت‌تر gunicorn با استفاده از systemd سرویسی را برای آن پیاده‌سازی می‌کنیم. با توجه به اینکه در Production هستیم از نظر امنیتی توصیه می‌شود user و group مربوط به سرویس ایجاد شوند. sudo adduser --shell=/bin/false --disabled-password --home=/usr/share/webapp --system --group webapp سورس‌کد را در دایرکتوری home مربوط به user که ساخته شد قرار می‌دهیم. دقت داشته باشید که دایرکتوری home هنگام تعریف user مشخص شد./usr/share/webappهمچنین لازم است permission و owner فایل‌های داخل دایرکتوری اصلاح شوند:chown -R webapp:webapp /usr/share/webapp chmod 644 /usr/share/webapp/*توجهمواردی که برای اضافه‌کردن user و تنظیم permission ها گفته شد، اجباری نیستند اما بنا به دلایل امنیتی و با توجه به اینکه در محیط Production قرار داریم ضروری است. برای ایجاد سرویس در systemd فایل سرویس را در مسیر زیر ایجاد می‌کنیم:/usr/lib/systemd/system/webapp.serviceکه محتویات آن بصورت زیر است:[Unit] Description=Web-Application Service after=network.target Conflicts=getty@tty1.service [Service] Type=simple User=webapp Group=webapp WorkingDirectory=/usr/share/wepapp PIDFile=/var/run/webapp.pid ExecStart=/usr/local/bin/gunicorn -w 4 -b 127.0.0.1:5000 --pid /var/run/webapp.pid server:app Restart=always RuntimeDirectoryMode=755 [Install] WantedBy = multi-user.targetتا اینجا webapp به لیست سرویس‌ها اضافه شده اما برای اینکه systemctl تغییرات (افزودن، ویرایش و حذف فایل سرویس) را اعمال کند ضروری است دستور زیر اجرا شود:systemctl daemon-reloadبرای استارت سرویس این دستورِ آشنا را اجرا می‌کنیم:service webapp startحالا اگر به پورت ۵۰۰۰ لوکال‌هاست درخواستی را بفرستیم خروجی زیر را دریافت خواهیم کرد:خروجی سرویس webapp۵. پروکسیتا اینجا وب‌سرویس ما اجرا شد و روی پورت ۵۰۰۰ لوکال‌هاست درخواست‌ها را پاسخ می‌دهد. مرحله بعدی باید Reverse Proxy کانفیگ شود تا از لبه‌ی سرور درخواست‌های ورودی را به وب‌سرویس ارسال کند.کانفیگ Nginxserver { listen 80; location / { proxy_pass http://127.0.0.1:5000; } }کانفیگ Apache<VirtualHost *:80> ProxyPreserveHost On ProxyPass / http://127.0.0.1:5000/ ProxyPassReverse / http://127.0.0.1:5000/ </VirtualHost>پس از ریستارت سرویس apache اگر درخواستی ارسال شود، apache آن را به وب‌سرویس ارسال و پاسخ را برمی‌گرداند.برای WSGI وب‌سرور آپاچی ماژول mod_wsgi دارد اما شخصا استفاده از آن را توصیه نمی‌کنم: اول آنکه تجربه‌ خیلی بسیار سخت و ناخوشایندی را با این ماژول دارم. دوم اینکه این ماژول برای هر درخواست کد را اجرا می‌کند.سوم هم امکان Scale-Up وجود ندارد.درنظر داشته باشید کانفیگ‌های Nginx و Apache را می‌توان روی دامنه (و همچنین زیردامنه) و پورت خاصی نیز تعریف کرد؛ مثلا api.domain.com و پورت ۸۰۸۰.۶. چند نکتهدر سورس‌کد خود از ساختارهای مدیریت خطا (Try/Exception) استفاده کنید.خودِ سورس‌کد برای خطایابی و بررسی مشکلات حتما Log داشته باشد.مطالبی که گفته شد برای Django هم امکان استفاده دارد.برای پروکسی حتما Log را فعال کنید.در پروکسی‌ ماژول ModSecurity را درصورت امکان فعال و استفاده کنید تا درخواست‌ها از نظر امنیتی بررسی و موارد مشکوک Reject شوند.ممکن است نیاز باشد پورت پروکسی روی سرویس فایروال (firewalld ، iptables ، ufw) نیز باز شود.پیشنهاد می‌کنم برای امن‌سازی، دستورالعمل‌های Hardening برای وب‌اپلیکشین، وب‌سرورها، لینوکس و هرآنچه که نیاز دارید را مطالعه و اجرا کنید.