من الصفر إلى البطل: وصفات إنتاج القارورة

نشرت: 2022-03-11

بصفتي مهندسًا للتعلم الآلي وخبيرًا في رؤية الكمبيوتر ، أجد نفسي أقوم بإنشاء واجهات برمجة التطبيقات وحتى تطبيقات الويب باستخدام Flask بشكل مفاجئ في كثير من الأحيان. في هذا المنشور ، أرغب في مشاركة بعض النصائح والوصفات المفيدة لبناء تطبيق Flask كامل جاهز للإنتاج.

سنتناول الموضوعات التالية:

  1. إدارة التكوين. أي تطبيق واقعي له دورة حياة بمراحل محددة - على أقل تقدير ، سيكون التطوير والاختبار والنشر. في كل مرحلة ، يجب أن يعمل كود التطبيق في بيئة مختلفة قليلاً ، الأمر الذي يتطلب وجود مجموعة مختلفة من الإعدادات ، مثل سلاسل اتصال قاعدة البيانات ، ومفاتيح واجهات برمجة التطبيقات الخارجية ، وعناوين URL.
  2. تطبيق Flask للاستضافة الذاتية مع Gunicorn. على الرغم من أن Flask يحتوي على خادم ويب مدمج ، كما نعلم جميعًا ، فهو غير مناسب للإنتاج ويجب وضعه خلف خادم ويب حقيقي قادر على الاتصال بـ Flask من خلال بروتوكول WSGI. الخيار الشائع لذلك هو Gunicorn - خادم Python WSGI HTTP.
  3. تقديم ملفات ثابتة وطلب وكيل باستخدام Nginx. أثناء كونه خادم ويب HTTP ، يعد Gunicorn بدوره خادم تطبيق غير مناسب لمواجهة الويب. لهذا السبب نحتاج إلى Nginx كوكيل عكسي ولخدمة الملفات الثابتة. في حال احتجنا إلى توسيع نطاق تطبيقنا ليشمل خوادم متعددة ، فإن Nginx سيهتم أيضًا بموازنة الحمل.
  4. نشر تطبيق داخل حاويات Docker على خادم Linux مخصص. لقد كان النشر في الحاويات جزءًا أساسيًا من تصميم البرنامج لفترة طويلة جدًا الآن. لا يختلف تطبيقنا وسيتم تعبئته بدقة في حاوياته الخاصة (حاويات متعددة ، في الواقع).
  5. تكوين ونشر قاعدة بيانات PostgreSQL للتطبيق. ستتم إدارة بنية قاعدة البيانات وعمليات الترحيل بواسطة Alembic مع توفير SQLAlchemy تخطيطًا للعلائقية للكائنات.
  6. إعداد قائمة انتظار مهام الكرفس للتعامل مع المهام طويلة المدى. سيتطلب كل تطبيق هذا في النهاية لإلغاء تحميل الوقت أو العمليات الحسابية المكثفة - سواء كان ذلك بإرسال البريد أو التدبير الآلي لقاعدة البيانات أو معالجة الصور التي تم تحميلها - من سلاسل خوادم الويب على العاملين الخارجيين.

إنشاء تطبيق Flask

لنبدأ بإنشاء كود تطبيق وأصول. يرجى ملاحظة أنني لن أتناول هيكل تطبيق Flask المناسب في هذا المنشور. يتكون التطبيق التجريبي من عدد قليل من الوحدات والحزم من أجل الإيجاز والوضوح.

أولاً ، قم بإنشاء بنية دليل وتهيئة مستودع Git فارغ.

 mkdir flask-deploy cd flask-deploy # init GIT repo git init # create folder structure mkdir static tasks models config # install required packages with pipenv, this will create a Pipfile pipenv install flask flask-restful flask-sqlalchemy flask-migrate celery # create test static asset echo "Hello World!" > static/hello-world.txt

بعد ذلك ، سنضيف الرمز.

config / __ init__.py

في وحدة التكوين ، سنحدد إطار عمل إدارة التكوين الصغير الخاص بنا. تكمن الفكرة في جعل التطبيق يتصرف وفقًا للإعداد المسبق للتكوين المحدد بواسطة متغير البيئة APP_ENV ، بالإضافة إلى إضافة خيار لتجاوز أي إعداد تكوين بمتغير بيئة معين إذا لزم الأمر.

 import os import sys import config.settings # create settings object corresponding to specified env APP_ENV = os.environ.get('APP_ENV', 'Dev') _current = getattr(sys.modules['config.settings'], '{0}Config'.format(APP_ENV))() # copy attributes to the module for convenience for atr in [f for f in dir(_current) if not '__' in f]: # environment can override anything val = os.environ.get(atr, getattr(_current, atr)) setattr(sys.modules[__name__], atr, val) def as_dict(): res = {} for atr in [f for f in dir(config) if not '__' in f]: val = getattr(config, atr) res[atr] = val return res

config / settings.py

هذه مجموعة من فئات التكوين ، يتم تحديد أحدها بواسطة متغير APP_ENV . عند تشغيل التطبيق ، ستقوم الشفرة الموجودة في __init__.py بإنشاء مثيل لإحدى هذه الفئات التي تتجاوز قيم الحقل بمتغيرات بيئة معينة ، إذا كانت موجودة. سنستخدم كائن التكوين النهائي عند تهيئة تكوين Flask و Celery لاحقًا.

 class BaseConfig(): API_PREFIX = '/api' TESTING = False DEBUG = False class DevConfig(BaseConfig): FLASK_ENV = 'development' DEBUG = True SQLALCHEMY_DATABASE_URI = 'postgresql://db_user:db_password@db-postgres:5432/flask-deploy' CELERY_BROKER = 'pyamqp://rabbit_user:rabbit_password@broker-rabbitmq//' CELERY_RESULT_BACKEND = 'rpc://rabbit_user:rabbit_password@broker-rabbitmq//' class ProductionConfig(BaseConfig): FLASK_ENV = 'production' SQLALCHEMY_DATABASE_URI = 'postgresql://db_user:db_password@db-postgres:5432/flask-deploy' CELERY_BROKER = 'pyamqp://rabbit_user:rabbit_password@broker-rabbitmq//' CELERY_RESULT_BACKEND = 'rpc://rabbit_user:rabbit_password@broker-rabbitmq//' class TestConfig(BaseConfig): FLASK_ENV = 'development' TESTING = True DEBUG = True # make celery execute tasks synchronously in the same process CELERY_ALWAYS_EAGER = True

المهام / __ init__.py

تحتوي حزمة المهام على كود تهيئة الكرفس. يتم استخدام حزمة التكوين ، التي سيتم نسخ جميع الإعدادات عليها بالفعل على مستوى الوحدة النمطية عند التهيئة ، لتحديث كائن تكوين الكرفس في حالة وجود بعض الإعدادات الخاصة بالكرفس في المستقبل - على سبيل المثال ، المهام المجدولة ومهلة العمال.

 from celery import Celery import config def make_celery(): celery = Celery(__name__, broker=config.CELERY_BROKER) celery.conf.update(config.as_dict()) return celery celery = make_celery()

المهام / celery_worker.py

هذه الوحدة مطلوبة لبدء تشغيل عامل الكرفس وتهيئته ، والذي سيتم تشغيله في حاوية Docker منفصلة. يقوم بتهيئة سياق تطبيق Flask للوصول إلى نفس بيئة التطبيق. إذا لم يكن ذلك مطلوبًا ، فيمكن إزالة هذه الخطوط بأمان.

 from app import create_app app = create_app() app.app_context().push() from tasks import celery

api / __ init__.py

بعد ذلك ينتقل إلى حزمة API ، التي تحدد واجهة برمجة تطبيقات REST باستخدام حزمة Flask-Restful. تطبيقنا مجرد عرض توضيحي وسيحتوي على نقطتي نهاية فقط:

  • /process_data - تبدأ عملية وهمية طويلة على عامل الكرفس وتعيد معرف مهمة جديدة.
  • /tasks/<task_id> - إرجاع حالة المهمة حسب معرف المهمة.
 import time from flask import jsonify from flask_restful import Api, Resource from tasks import celery import config api = Api(prefix=config.API_PREFIX) class TaskStatusAPI(Resource): def get(self, task_id): task = celery.AsyncResult(task_id) return jsonify(task.result) class DataProcessingAPI(Resource): def post(self): task = process_data.delay() return {'task_id': task.id}, 200 @celery.task() def process_data(): time.sleep(60) # data processing endpoint api.add_resource(DataProcessingAPI, '/process_data') # task status endpoint api.add_resource(TaskStatusAPI, '/tasks/<string:task_id>')

النماذج / __ init__.py

سنقوم الآن بإضافة نموذج SQLAlchemy لكائن User ، وكود تهيئة محرك قاعدة البيانات. لن يتم استخدام كائن User بواسطة تطبيقنا التجريبي بأي طريقة ذات معنى ، ولكننا سنحتاجه للتأكد من عمل عمليات ترحيل قاعدة البيانات وإعداد تكامل SQLAlchemy-Flask بشكل صحيح.

 import uuid from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() class User(db.Model): id = db.Column(db.String(), primary_key=True, default=lambda: str(uuid.uuid4())) username = db.Column(db.String()) email = db.Column(db.String(), unique=True)

لاحظ كيف يتم إنشاء UUID تلقائيًا كمعرف كائن بالتعبير الافتراضي.

app.py

أخيرًا ، لنقم بإنشاء ملف تطبيق Flask رئيسي.

 from flask import Flask logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s]: {} %(levelname)s %(message)s'.format(os.getpid()), datefmt='%Y-%m-%d %H:%M:%S', handlers=[logging.StreamHandler()]) logger = logging.getLogger() def create_app(): logger.info(f'Starting app in {config.APP_ENV} environment') app = Flask(__name__) app.config.from_object('config') api.init_app(app) # initialize SQLAlchemy db.init_app(app) # define hello world page @app.route('/') def hello_world(): return 'Hello, World!' return app if __name__ == "__main__": app = create_app() app.run(host='0.0.0.0', debug=True)</td> </tr> <tr> <td>

نحن هنا:

  • تكوين التسجيل الأساسي بتنسيق مناسب مع الوقت والمستوى ومعرف العملية
  • تحديد وظيفة إنشاء تطبيق Flask من خلال تهيئة واجهة برمجة التطبيقات و "مرحبًا ، أيها العالم!" صفحة
  • تحديد نقطة دخول لتشغيل التطبيق أثناء وقت التطوير

wsgi.py

سنحتاج أيضًا إلى وحدة منفصلة لتشغيل تطبيق Flask مع Gunicorn. سيتكون من سطرين فقط:

 from app import create_app app = create_app()

كود التطبيق جاهز. خطوتنا التالية هي إنشاء تكوين Docker.

بناء حاويات Docker

سيتطلب تطبيقنا تشغيل عدة حاويات Docker:

  1. حاوية التطبيق لخدمة الصفحات النموذجية وكشف نقاط نهاية API. من الجيد تقسيم هاتين الوظيفتين على الإنتاج ، لكن ليس لدينا أي صفحات نموذجية في تطبيقنا التجريبي. ستقوم الحاوية بتشغيل خادم الويب Gunicorn الذي سيتواصل مع Flask من خلال بروتوكول WSGI.
  2. حاوية عامل الكرفس لتنفيذ المهام الطويلة. هذه هي نفس حاوية التطبيق ، ولكن مع أمر تشغيل مخصص لتشغيل Celery ، بدلاً من Gunicorn.
  3. حاوية خفق الكرفس — مشابهة لما ورد أعلاه ، ولكن للمهام التي يتم استدعاؤها وفقًا لجدول زمني منتظم ، مثل إزالة حسابات المستخدمين الذين لم يؤكدوا بريدهم الإلكتروني مطلقًا.
  4. حاوية RabbitMQ. يتطلب الكرفس وسيط رسائل للتواصل بين العاملين والتطبيق ، وتخزين نتائج المهام. يعتبر RabbitMQ خيارًا شائعًا ، ولكن يمكنك أيضًا استخدام Redis أو Kafka.
  5. حاوية قاعدة البيانات مع PostgreSQL.

من الطرق الطبيعية لإدارة الحاويات المتعددة بسهولة استخدام Docker Compose. لكن أولاً ، سنحتاج إلى إنشاء Dockerfile لإنشاء صورة حاوية لتطبيقنا. دعنا نضعها في دليل المشروع.

 FROM python:3.7.2 RUN pip install pipenv ADD . /flask-deploy WORKDIR /flask-deploy RUN pipenv install --system --skip-lock RUN pip install gunicorn[gevent] EXPOSE 5000 CMD gunicorn --worker-class gevent --workers 8 --bind 0.0.0.0:5000 wsgi:app --max-requests 10000 --timeout 5 --keep-alive 5 --log-level info

يوجه هذا الملف Docker إلى:

  • قم بتثبيت جميع التبعيات باستخدام Pipenv
  • أضف مجلد تطبيق إلى الحاوية
  • قم بتعريض منفذ TCP 5000 للمضيف
  • قم بتعيين أمر بدء التشغيل الافتراضي للحاوية على مكالمة Gunicorn

دعنا نناقش أكثر ما يحدث في السطر الأخير. يتم تشغيل Gunicorn مع تحديد فئة العمال على أنها gevent. Gevent هو lib التزامن خفيف الوزن لتعدد المهام التعاوني. إنه يوفر مكاسب كبيرة في الأداء على أحمال الإدخال / الإخراج المقيدة ، مما يوفر استخدامًا أفضل لوحدة المعالجة المركزية مقارنةً بتعدد المهام الوقائي لنظام التشغيل للخيوط. المعلمة --workers هي عدد عمليات العامل. إنها لفكرة جيدة أن تجعلها مساوية لعدد النوى على الخادم.

بمجرد أن يكون لدينا Dockerfile لحاوية التطبيق ، يمكننا إنشاء ملف docker-compose.yml ، والذي سيحدد جميع الحاويات التي سيحتاجها التطبيق للتشغيل.

 version: '3' services: broker-rabbitmq: image: "rabbitmq:3.7.14-management" environment: - RABBITMQ_DEFAULT_USER=rabbit_user - RABBITMQ_DEFAULT_PASS=rabbit_password db-postgres: image: "postgres:11.2" environment: - POSTGRES_USER=db_user - POSTGRES_PASSWORD=db_password migration: build: . environment: - APP_ENV=${APP_ENV} command: flask db upgrade depends_on: - db-postgres api: build: . ports: - "5000:5000" environment: - APP_ENV=${APP_ENV} depends_on: - broker-rabbitmq - db-postgres - migration api-worker: build: . command: celery worker --workdir=. -A tasks.celery --loglevel=info environment: - APP_ENV=${APP_ENV} depends_on: - broker-rabbitmq - db-postgres - migration api-beat: build: . command: celery beat -A tasks.celery --loglevel=info environment: - APP_ENV=${APP_ENV} depends_on: - broker-rabbitmq - db-postgres - migration

حددنا الخدمات التالية:

  • broker-rabbitmq - حاوية وسيط رسائل RabbitMQ. يتم تحديد بيانات اعتماد الاتصال بواسطة متغيرات البيئة
  • db-postgres - حاوية PostgreSQL وبيانات اعتمادها
  • migration - حاوية التطبيق التي ستقوم بترحيل قاعدة البيانات باستخدام Flask-Migrate والخروج. تعتمد حاويات API عليها وستعمل بعد ذلك.
  • api - حاوية التطبيق الرئيسية
  • api-worker و api-beat - الحاويات التي تشغل عمال الكرفس للمهام الواردة من API والمهام المجدولة

ستتلقى كل حاوية تطبيق أيضًا المتغير APP_ENV من أمر docker docker-compose up .

بمجرد أن تصبح جميع أصول التطبيق جاهزة ، فلنضعها على GitHub ، مما سيساعدنا في نشر الكود على الخادم.

 git add * git commit -a -m 'Initial commit' git remote add origin [email protected]:your-name/flask-deploy.git git push -u origin master

تكوين الخادم

الكود الخاص بنا موجود على GitHub الآن ، وكل ما تبقى هو إجراء التكوين الأولي للخادم ونشر التطبيق. في حالتي ، الخادم هو مثيل AWS يقوم بتشغيل AMI Linux. بالنسبة إلى نكهات Linux الأخرى ، قد تختلف التعليمات قليلاً. أفترض أيضًا أن الخادم يحتوي بالفعل على عنوان IP خارجي ، وقد تم تكوين DNS بسجل A يشير إلى عنوان IP هذا ، ويتم إصدار شهادات SSL للمجال.

نصيحة أمنية: لا تنس السماح للمنفذين 80 و 443 لحركة مرور HTTP (S) ، والمنفذ 22 لـ SSH في وحدة تحكم الاستضافة (أو باستخدام iptables ) وإغلاق الوصول الخارجي إلى جميع المنافذ الأخرى! تأكد من أن تفعل الشيء نفسه بالنسبة لبروتوكول IPv6 !

تثبيت التبعيات

أولاً ، سنحتاج إلى تشغيل Nginx و Docker على الخادم ، بالإضافة إلى Git لسحب الكود. لنقم بتسجيل الدخول عبر SSH ونستخدم مدير الحزم لتثبيتها.

 sudo yum install -y docker docker-compose nginx git

تكوين Nginx

الخطوة التالية هي ضبط Nginx. غالبًا ما يكون ملف nginx.conf الرئيسي كما هو. ومع ذلك ، تأكد من التحقق مما إذا كان يناسب احتياجاتك. بالنسبة لتطبيقنا ، سننشئ ملف تكوين جديد في مجلد conf.d يحتوي تكوين المستوى الأعلى على توجيه لتضمين كافة ملفات .conf منه.

 cd /etc/nginx/conf.d sudo vim flask-deploy.conf

هنا ملف تكوين موقع Flask لـ Nginx ، البطاريات مضمنة. لديه الميزات التالية:

  1. تم تكوين SSL. يجب أن يكون لديك شهادات صالحة للمجال الخاص بك ، على سبيل المثال ، شهادة Let's Encrypt المجانية.
  2. تتم إعادة توجيه طلبات www.your-site.com إلى your-site.com
  3. تتم إعادة توجيه طلبات HTTP لتأمين منفذ HTTPS.
  4. تم تكوين الوكيل العكسي لتمرير الطلبات إلى المنفذ المحلي 5000.
  5. يتم تقديم الملفات الثابتة بواسطة Nginx من مجلد محلي.
 server { listen 80; listen 443; server_name www.your-site.com; # check your certificate path! ssl_certificate /etc/nginx/ssl/your-site.com/fullchain.crt; ssl_certificate_key /etc/nginx/ssl/your-site.com/server.key; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers HIGH:!aNULL:!MD5; # redirect to non-www domain return 301 https://your-site.com$request_uri; } # HTTP to HTTPS redirection server { listen 80; server_name your-site.com; return 301 https://your-site.com$request_uri; } server { listen 443 ssl; # check your certificate path! ssl_certificate /etc/nginx/ssl/your-site.com/fullchain.crt; ssl_certificate_key /etc/nginx/ssl/your-site.com/server.key; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers HIGH:!aNULL:!MD5; # affects the size of files user can upload with HTTP POST client_max_body_size 10M; server_name your-site.com; location / { include /etc/nginx/mime.types; root /home/ec2-user/flask-deploy/static; # if static file not found - pass request to Flask try_files $uri @flask; } location @flask { add_header 'Access-Control-Allow-Origin' '*' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization'; add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range'; proxy_read_timeout 10; proxy_send_timeout 10; send_timeout 60; resolver_timeout 120; client_body_timeout 120; # set headers to pass request info to Flask proxy_set_header Host $http_host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $remote_addr; proxy_redirect off; proxy_pass http://127.0.0.1:5000$uri; } }

بعد تحرير الملف ، قم بتشغيل sudo nginx -s reload ومعرفة ما إذا كانت هناك أية أخطاء.

إعداد بيانات اعتماد GitHub

إنها ممارسة جيدة أن يكون لديك حساب VCS منفصل "للنشر" لنشر المشروع ونظام CI / CD. بهذه الطريقة لا تخاطر بكشف بيانات اعتماد حسابك. لمزيد من الحماية لمستودع المشروع ، يمكنك أيضًا تقييد أذونات هذا الحساب للوصول للقراءة فقط. بالنسبة لمستودع GitHub ، ستحتاج إلى حساب مؤسسة للقيام بذلك. لنشر تطبيقنا التجريبي ، سنقوم فقط بإنشاء مفتاح عام على الخادم وتسجيله على GitHub للوصول إلى مشروعنا دون إدخال بيانات الاعتماد في كل مرة.

لإنشاء مفتاح SSH جديد ، قم بتشغيل:

 cd ~/.ssh ssh-keygen -b 2048 -t rsa -f id_rsa.pub -q -N "" -C "deploy"

ثم قم بتسجيل الدخول على GitHub وأضف مفتاحك العام من ~/.ssh/id_rsa.pub في إعدادات الحساب.

نشر التطبيق

الخطوات النهائية واضحة ومباشرة - نحتاج إلى الحصول على رمز التطبيق من GitHub وبدء جميع الحاويات باستخدام Docker Compose.

 cd ~ git clone https://github.com/your-name/flask-deploy.git git checkout master APP_ENV=Production docker-compose up -d

قد يكون من الجيد حذف -d (الذي يبدأ الحاوية في الوضع المنفصل) للتشغيل الأول لرؤية إخراج كل حاوية مباشرة في الجهاز والتحقق من المشكلات المحتملة. خيار آخر هو فحص كل حاوية فردية docker logs بعد ذلك. دعونا نرى ما إذا كانت جميع حاوياتنا تعمل باستخدام docker ps.

image_alt_text

رائعة. جميع الحاويات الخمس جاهزة للعمل. Docker يؤلف أسماء الحاويات المعينة تلقائيًا بناءً على الخدمة المحددة في docker-compose.yml. حان الوقت الآن لاختبار كيفية عمل التكوين بالكامل أخيرًا! من الأفضل إجراء الاختبارات من جهاز خارجي للتأكد من أن الخادم لديه إعدادات الشبكة الصحيحة.

 # test HTTP protocol, you should get a 301 response curl your-site.com # HTTPS request should return our Hello World message curl https://your-site.com # and nginx should correctly send test static file: curl https://your-site.com/hello-world.txt

هذا هو. لدينا تكوين بسيط ، ولكنه جاهز تمامًا للإنتاج لتطبيقنا الذي يعمل على مثيل AWS. آمل أن يساعدك ذلك على البدء في إنشاء تطبيق واقعي بسرعة وتجنب بعض الأخطاء الشائعة! الكود الكامل متاح في مستودع جيثب.

خاتمة

في هذه المقالة ، ناقشنا بعضًا من أفضل الممارسات في هيكلة تطبيق Flask وتكوينه وتعبئته ونشره في الإنتاج. هذا موضوع كبير للغاية ، من المستحيل تغطيته بالكامل في منشور مدونة واحد. فيما يلي قائمة بالأسئلة المهمة التي لم نعالجها:

هذا المقال لا يغطي:

  • التكامل المستمر والنشر المستمر
  • الاختبار التلقائي
  • سجل الشحن
  • مراقبة API
  • توسيع نطاق التطبيق إلى خوادم متعددة
  • حماية أوراق الاعتماد في التعليمات البرمجية المصدر

ومع ذلك ، يمكنك تعلم كيفية القيام بذلك باستخدام بعض الموارد الرائعة الأخرى في هذه المدونة. على سبيل المثال ، لاستكشاف التسجيل ، راجع Python Logging: An-Depth Tutorial ، أو للحصول على نظرة عامة عامة على CI / CD والاختبار الآلي ، راجع كيفية إنشاء خط أنابيب نشر أولي فعال. أترك تطبيق هذه كتمرين لك ، أيها القارئ.

شكرا للقراءة!