Kahramana Sıfır: Şişe Üretim Tarifleri

Yayınlanan: 2022-03-11

Bir makine öğrenimi mühendisi ve bilgisayarla görme uzmanı olarak, kendimi Flask ile şaşırtıcı sıklıkta API'ler ve hatta web uygulamaları oluştururken buluyorum. Bu yazıda, eksiksiz bir üretime hazır Flask uygulaması oluşturmak için bazı ipuçlarını ve faydalı tarifleri paylaşmak istiyorum.

Aşağıdaki konuları ele alacağız:

  1. Konfigürasyon yönetimi. Herhangi bir gerçek yaşam uygulamasının belirli aşamaları olan bir yaşam döngüsü vardır - en azından geliştirme, test etme ve dağıtım olacaktır. Her aşamada, uygulama kodu, veritabanı bağlantı dizeleri, harici API anahtarları ve URL'ler gibi farklı bir dizi ayara sahip olmayı gerektiren biraz farklı bir ortamda çalışmalıdır.
  2. Gunicorn ile kendi kendini barındıran Flask uygulaması. Flask'ın yerleşik bir web sunucusu olmasına rağmen, hepimizin bildiği gibi, üretim için uygun değildir ve bir WSGI protokolü aracılığıyla Flask ile iletişim kurabilen gerçek bir web sunucusunun arkasına konulması gerekir. Bunun için yaygın bir seçim, bir Python WSGI HTTP sunucusu olan Gunicorn'dur.
  3. Nginx ile statik dosyalar ve proxy isteği sunma. Gunicorn, bir HTTP web sunucusu olmakla birlikte, web ile yüzleşmeye uygun olmayan bir uygulama sunucusudur. Bu yüzden Nginx'e bir ters proxy olarak ve statik dosyalar sunmak için ihtiyacımız var. Uygulamamızı birden fazla sunucuya ölçeklendirmemiz gerekirse, Nginx yük dengelemeyle de ilgilenecektir.
  4. Özel bir Linux sunucusunda Docker kapsayıcılarının içinde bir uygulama dağıtma. Kapsayıcılı dağıtım, oldukça uzun bir süredir yazılım tasarımının önemli bir parçası olmuştur. Bizim uygulamamız da farklı değil ve kendi konteynerinde (aslında birden fazla konteynerde) düzgün bir şekilde paketlenecektir.
  5. Uygulama için bir PostgreSQL veritabanını yapılandırma ve dağıtma. Veritabanı yapısı ve geçişler, nesne-ilişkisel eşleme sağlayan SQLAlchemy ile Alembic tarafından yönetilecektir.
  6. Uzun süren görevleri işlemek için Kereviz görev kuyruğu ayarlama. Her uygulama, harici çalışanlar üzerindeki web sunucusu iş parçacıklarından posta gönderme, otomatik veritabanı temizliği veya yüklenen görüntülerin işlenmesi gibi zaman veya hesaplama yoğun süreçleri boşaltmak için eninde sonunda buna ihtiyaç duyacaktır.

Flask Uygulamasını Oluşturma

Bir uygulama kodu ve varlıkları oluşturarak başlayalım. Lütfen bu yazıda uygun Flask uygulama yapısını ele almayacağımı unutmayın. Demo uygulaması, kısalık ve netlik adına minimum sayıda modül ve paketten oluşur.

İlk önce bir dizin yapısı oluşturun ve boş bir Git deposunu başlatın.

 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

Ardından kodu ekleyeceğiz.

config/__init__.py

Yapılandırma modülünde, küçük yapılandırma yönetimi çerçevemizi tanımlayacağız. Buradaki fikir, uygulamanın APP_ENV ortam değişkeni tarafından seçilen yapılandırma ön ayarına göre davranmasını sağlamak ve ayrıca gerekirse belirli bir ortam değişkeni ile herhangi bir yapılandırma ayarını geçersiz kılmak için bir seçenek eklemektir.

 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

Bu, biri APP_ENV değişkeni tarafından seçilen bir dizi yapılandırma sınıfıdır. Uygulama çalıştığında, __init__.py içindeki kod, varsa, belirli ortam değişkenleriyle alan değerlerini geçersiz kılarak bu sınıflardan birini başlatır. Daha sonra Flask ve Kereviz konfigürasyonunu başlatırken bir son konfigürasyon nesnesi kullanacağız.

 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

görevler/__init__.py

Görev paketi Kereviz başlatma kodunu içerir. Başlatma sırasında tüm ayarları modül düzeyinde kopyalamış olacak olan yapılandırma paketi, gelecekte Kereviz'e özgü bazı ayarlarımız olması durumunda, örneğin zamanlanmış görevler ve çalışan zaman aşımları olması durumunda Kereviz yapılandırma nesnesini güncellemek için kullanılır.

 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()

görevler/celery_worker.py

Bu modül, ayrı bir Docker kapsayıcısında çalışacak bir Kereviz çalışanını başlatmak ve başlatmak için gereklidir. Uygulamayla aynı ortama erişebilmek için Flask uygulama bağlamını başlatır. Bu gerekli değilse, bu çizgiler güvenle kaldırılabilir.

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

API/__init__.py

Ardından, Flask-Restful paketini kullanarak REST API'yi tanımlayan API paketi gelir. Uygulamamız yalnızca bir demodur ve yalnızca iki uç noktası olacaktır:

  • /process_data – Kereviz çalışanında yapay bir uzun işlem başlatır ve yeni bir görevin kimliğini döndürür.
  • /tasks/<task_id> – Görev kimliğine göre bir görevin durumunu döndürür.
 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>')

modeller/__init__.py

Şimdi User nesnesi için bir SQLAlchemy modeli ve bir veritabanı motoru başlatma kodu ekleyeceğiz. User nesnesi, demo uygulamamız tarafından anlamlı bir şekilde kullanılmayacaktır, ancak veritabanı geçişlerinin çalıştığından ve SQLAlchemy-Flask entegrasyonunun doğru şekilde kurulduğundan emin olmak için buna ihtiyacımız olacak.

 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)

Varsayılan ifadeyle bir nesne kimliği olarak UUID'nin nasıl otomatik olarak oluşturulduğuna dikkat edin.

app.py

Son olarak bir ana Flask uygulama dosyası oluşturalım.

 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>

Buradayız:

  • Temel günlük kaydının zaman, seviye ve süreç kimliği ile uygun bir biçimde yapılandırılması
  • API başlatma ve “Merhaba dünya!” ile Flask uygulaması oluşturma işlevini tanımlama sayfa
  • Geliştirme süresi boyunca uygulamayı çalıştırmak için bir giriş noktası tanımlama

wsgi.py

Ayrıca Flask uygulamasını Gunicorn ile çalıştırmak için ayrı bir modüle ihtiyacımız olacak. Sadece iki satırı olacak:

 from app import create_app app = create_app()

Uygulama kodu hazır. Bir sonraki adımımız bir Docker konfigürasyonu oluşturmak.

Docker Konteynerleri Oluşturma

Uygulamamızın çalışması için birden çok Docker konteyneri gerekecek:

  1. Şablonlu sayfaları sunmak ve API uç noktalarını ortaya çıkarmak için uygulama kapsayıcısı. Bu iki işlevi üretimde bölmek iyi bir fikirdir, ancak demo uygulamamızda şablonlu sayfalarımız yok. Konteyner, WSGI protokolü aracılığıyla Flask ile iletişim kuracak olan Gunicorn web sunucusunu çalıştıracaktır.
  2. Uzun görevleri yürütmek için kereviz işçisi konteyneri. Bu aynı uygulama kabıdır, ancak Gunicorn yerine Kereviz'i başlatmak için özel çalıştırma komutuna sahiptir.
  3. Kereviz kapsayıcıyı yendi - yukarıdakine benzer, ancak e-postalarını asla onaylamayan kullanıcıların hesaplarını kaldırmak gibi düzenli bir programa göre çağrılan görevler için.
  4. RabbitMQ kapsayıcısı. Kereviz, çalışanlar ve uygulama arasında iletişim kurmak ve görev sonuçlarını depolamak için bir mesaj komisyoncusu gerektirir. RabbitMQ yaygın bir seçimdir, ancak Redis veya Kafka'yı da kullanabilirsiniz.
  5. PostgreSQL ile veritabanı kapsayıcısı.

Birden çok kapsayıcıyı kolayca yönetmenin doğal bir yolu, Docker Compose kullanmaktır. Ama önce, uygulamamız için bir kapsayıcı görüntüsü oluşturmak için bir Dockerfile oluşturmamız gerekecek. Proje dizinine koyalım.

 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

Bu dosya Docker'a şunları yapmasını söyler:

  • Pipenv kullanarak tüm bağımlılıkları yükleyin
  • Kapsayıcıya bir uygulama klasörü ekleyin
  • TCP bağlantı noktası 5000'i ana bilgisayara gösterin
  • Kapsayıcının varsayılan başlatma komutunu bir Gunicorn çağrısına ayarlayın

Son satırda ne olduğunu daha fazla tartışalım. İşçi sınıfını gevent olarak belirterek Gunicorn'u çalıştırır. Gevent, işbirlikçi çoklu görev için hafif bir eşzamanlılık kitaplığıdır. İş parçacıkları için işletim sistemi önleyici çoklu göreve kıyasla daha iyi CPU kullanımı sağlayarak, G/Ç'ye bağlı yüklerde önemli performans kazanımları sağlar. --workers parametresi, çalışan işlemlerin sayısıdır. Sunucudaki çekirdeğe eşit olarak ayarlamak iyi bir fikirdir.

Uygulama kapsayıcı için bir Dockerfile'ımız olduğunda, uygulamanın çalıştırması gereken tüm kapsayıcıları tanımlayacak bir docker-compose.yml dosyası oluşturabiliriz.

 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

Aşağıdaki hizmetleri tanımladık:

  • broker-rabbitmq – Bir RabbitMQ mesaj komisyoncusu kapsayıcısı. Bağlantı kimlik bilgileri ortam değişkenleri tarafından tanımlanır
  • db-postgres – Bir PostgreSQL kapsayıcısı ve kimlik bilgileri
  • migration – Flask-Migrate ile veritabanı geçişini gerçekleştirecek ve çıkacak bir uygulama kapsayıcısı. API kapsayıcıları buna bağlıdır ve daha sonra çalışır.
  • api – Ana uygulama kabı
  • api-worker ve api-beat – API'den alınan görevler ve zamanlanmış görevler için Kereviz çalışanlarını çalıştıran kapsayıcılar

Her uygulama kapsayıcısı, docker-compose up komutundan APP_ENV değişkenini de alır.

Tüm uygulama varlıklarını hazır hale getirdikten sonra, kodu sunucuda dağıtmamıza yardımcı olacak GitHub'a koyalım.

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

Sunucuyu Yapılandırma

Kodumuz artık bir GitHub'da ve geriye yalnızca ilk sunucu yapılandırmasını gerçekleştirmek ve uygulamayı dağıtmak kalıyor. Benim durumumda sunucu, AMI Linux çalıştıran bir AWS örneğidir. Diğer Linux çeşitleri için talimatlar biraz farklılık gösterebilir. Ayrıca sunucunun zaten harici bir IP adresine sahip olduğunu, DNS'nin bu IP'ye işaret eden A kaydı ile yapılandırıldığını ve etki alanı için SSL sertifikalarının verildiğini varsayıyorum.

Güvenlik ipucu: HTTP(S) trafiği için 80 ve 443 numaralı bağlantı noktalarına, barındırma konsolunuzda (veya iptables kullanarak) SSH için 22 numaralı bağlantı noktasına izin vermeyi ve diğer tüm bağlantı noktalarına harici erişimi kapatmayı unutmayın! IPv6 protokolü için de aynısını yaptığınızdan emin olun!

Bağımlılıkları Yükleme

İlk olarak, sunucuda çalışan Nginx ve Docker'a ve ayrıca kodu çekmek için Git'e ihtiyacımız olacak. SSH ile giriş yapalım ve bunları yüklemek için bir paket yöneticisi kullanalım.

 sudo yum install -y docker docker-compose nginx git

Nginx'i Yapılandırma

Sonraki adım Nginx'i yapılandırmaktır. Ana nginx.conf yapılandırma dosyası genellikle olduğu gibi iyidir. Yine de, ihtiyaçlarınıza uygun olup olmadığını kontrol ettiğinizden emin olun. Uygulamamız için bir conf.d klasöründe yeni bir yapılandırma dosyası oluşturacağız. Üst düzey yapılandırma, içindeki tüm .conf dosyalarını dahil etmek için bir yönergeye sahiptir.

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

İşte Nginx için bir Flask site yapılandırma dosyası, piller dahil. Aşağıdaki özelliklere sahiptir:

  1. SSL yapılandırıldı. Etki alanınız için geçerli sertifikalarınız olmalıdır, örneğin ücretsiz Let's Encrypt sertifikası.
  2. www.your-site.com istekleri your-site.com yönlendirilir
  3. HTTP istekleri, güvenli HTTPS bağlantı noktasına yönlendirilir.
  4. Ters proxy, istekleri yerel bağlantı noktası 5000'e iletecek şekilde yapılandırılmıştır.
  5. Statik dosyalar, yerel bir klasörden Nginx tarafından sunulur.
 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; } }

Dosyayı düzenledikten sonra sudo nginx -s reload çalıştırın ve herhangi bir hata olup olmadığına bakın.

GitHub Kimlik Bilgilerini Ayarlama

Projeyi ve CI/CD sistemini dağıtmak için ayrı bir "dağıtım" VCS hesabına sahip olmak iyi bir uygulamadır. Bu şekilde kendi hesabınızın kimlik bilgilerini ifşa ederek risk almazsınız. Proje havuzunu daha fazla korumak için, bu tür bir hesabın izinlerini salt okunur erişimle de sınırlayabilirsiniz. GitHub deposu için bunu yapmak için bir kuruluş hesabına ihtiyacınız olacak. Demo uygulamamızı dağıtmak için sunucuda bir ortak anahtar oluşturacağız ve her seferinde kimlik bilgilerini girmeden projemize erişmek için GitHub'a kaydedeceğiz.

Yeni bir SSH anahtarı oluşturmak için şunu çalıştırın:

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

Ardından GitHub'da oturum açın ve hesap ayarlarında ~/.ssh/id_rsa.pub genel anahtarınızı ekleyin.

Uygulama Dağıtma

Son adımlar oldukça basittir; GitHub'dan uygulama kodunu almamız ve tüm kapsayıcıları Docker Compose ile başlatmamız gerekiyor.

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

Her bir kapsayıcının çıktısını doğrudan terminalde görmek ve olası sorunları kontrol etmek için ilk çalıştırmada -d (kapsayıcıyı ayrılmış modda başlatır) atlamak iyi bir fikir olabilir. Diğer bir seçenek de, her bir konteyneri daha sonra docker logs incelemektir. Bakalım tüm container'larımız docker ps.

image_alt_text

Harika. Beş konteynerin tamamı çalışıyor ve çalışıyor. Docker Atanan kapsayıcı adlarını, docker-compose.yml içinde belirtilen hizmete göre otomatik olarak oluşturun. Şimdi nihayet tüm konfigürasyonun nasıl çalıştığını test etme zamanı! Sunucunun doğru ağ ayarlarına sahip olduğundan emin olmak için testleri harici bir makineden çalıştırmak en iyisidir.

 # 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

Bu kadar. Bir AWS örneğinde çalışan uygulamamızın minimalist, ancak üretime tamamen hazır bir yapılandırmasına sahibiz. Umarım gerçek hayattaki bir uygulamayı hızlı bir şekilde oluşturmaya başlamanıza ve bazı yaygın hatalardan kaçınmanıza yardımcı olur! Kodun tamamı bir GitHub deposunda mevcuttur.

Çözüm

Bu makalede, bir Flask uygulamasını üretime yapılandırma, yapılandırma, paketleme ve dağıtmanın en iyi uygulamalarından bazılarını tartıştık. Bu çok büyük bir konu, tek bir blog yazısında tamamen ele alınması imkansız. İşte ele almadığımız önemli soruların bir listesi:

Bu makale şunları kapsamaz:

  • Sürekli entegrasyon ve sürekli dağıtım
  • Otomatik test
  • günlük nakliye
  • API izleme
  • Bir uygulamayı birden çok sunucuya ölçeklendirme
  • Kaynak koddaki kimlik bilgilerinin korunması

Ancak, bu blogdaki diğer harika kaynaklardan bazılarını kullanarak bunu nasıl yapacağınızı öğrenebilirsiniz. Örneğin, günlüğe kaydetmeyi keşfetmek için Python Günlüğü: Derinlemesine Bir Eğitim bölümüne veya CI/CD ve otomatik testlere genel bir bakış için bkz. Etkili Bir İlk Dağıtım İşlem Hattı Nasıl Oluşturulur. Bunların uygulanmasını bir alıştırma olarak siz okuyuculara bırakıyorum.

Okuduğunuz için teşekkürler!