Zero to Hero: Resep Produksi Labu

Diterbitkan: 2022-03-11

Sebagai seorang insinyur pembelajaran mesin dan ahli visi komputer, saya mendapati diri saya membuat API dan bahkan aplikasi web dengan Flask secara mengejutkan. Pada postingan kali ini saya ingin membagikan beberapa tips dan resep berguna untuk membangun aplikasi Flask siap produksi lengkap.

Kami akan membahas topik-topik berikut:

  1. Manajemen konfigurasi. Setiap aplikasi kehidupan nyata memiliki siklus hidup dengan tahapan tertentu—paling tidak, itu adalah pengembangan, pengujian, dan penerapan. Pada setiap tahap, kode aplikasi harus bekerja di lingkungan yang sedikit berbeda, yang memerlukan pengaturan yang berbeda, seperti string koneksi database, kunci API eksternal, dan URL.
  2. Aplikasi Flask hosting sendiri dengan Gunicorn. Meskipun Flask memiliki server web built-in, seperti yang kita semua tahu, itu tidak cocok untuk produksi dan perlu diletakkan di belakang server web nyata yang dapat berkomunikasi dengan Flask melalui protokol WSGI. Pilihan umum untuk itu adalah Gunicorn—server HTTP WSGI Python.
  3. Melayani file statis dan permintaan proxy dengan Nginx. Saat menjadi server web HTTP, Gunicorn, pada gilirannya, adalah server aplikasi yang tidak cocok untuk menghadapi web. Itu sebabnya kami membutuhkan Nginx sebagai proxy terbalik dan untuk melayani file statis. Jika kita perlu meningkatkan aplikasi kita ke beberapa server, Nginx akan menangani load balancing juga.
  4. Menyebarkan aplikasi di dalam wadah Docker di server Linux khusus. Penyebaran kemas telah menjadi bagian penting dari desain perangkat lunak untuk waktu yang cukup lama sekarang. Aplikasi kami tidak berbeda dan akan dikemas dengan rapi dalam wadahnya sendiri (beberapa wadah, sebenarnya).
  5. Mengonfigurasi dan menerapkan database PostgreSQL untuk aplikasi. Struktur basis data dan migrasi akan dikelola oleh Alembic dengan SQLAlchemy yang menyediakan pemetaan relasional objek.
  6. Menyiapkan antrian tugas Seledri untuk menangani tugas yang berjalan lama. Setiap aplikasi pada akhirnya akan memerlukan ini untuk membuang waktu atau proses komputasi yang intensif—baik itu pengiriman email, pemeliharaan database otomatis, atau pemrosesan gambar yang diunggah—dari utas server web pada pekerja eksternal.

Membuat Aplikasi Flask

Mari kita mulai dengan membuat kode aplikasi dan aset. Harap dicatat bahwa saya tidak akan membahas struktur aplikasi Flask yang tepat dalam posting ini. Aplikasi demo terdiri dari jumlah minimal modul dan paket demi singkatnya dan kejelasan.

Pertama, buat struktur direktori dan inisialisasi repositori Git yang kosong.

 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

Selanjutnya, kita akan menambahkan kode.

config/__init__.py

Dalam modul config, kita akan mendefinisikan kerangka kerja manajemen konfigurasi kecil kita. Idenya adalah untuk membuat aplikasi berperilaku sesuai dengan prasetel konfigurasi yang dipilih oleh variabel lingkungan APP_ENV , plus, tambahkan opsi untuk menimpa pengaturan konfigurasi apa pun dengan variabel lingkungan tertentu jika diperlukan.

 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

Ini adalah satu set kelas konfigurasi, salah satunya dipilih oleh variabel APP_ENV . Saat aplikasi berjalan, kode di __init__.py akan menginstansiasi salah satu kelas ini yang menggantikan nilai bidang dengan variabel lingkungan tertentu, jika ada. Kita akan menggunakan objek konfigurasi akhir saat menginisialisasi konfigurasi Flask dan Seledri nanti.

 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

tugas/__init__.py

Paket tugas berisi kode inisialisasi Seledri. Paket konfigurasi, yang sudah memiliki semua pengaturan yang disalin pada tingkat modul saat inisialisasi, digunakan untuk memperbarui objek konfigurasi Celery jika kita akan memiliki beberapa pengaturan khusus Seledri di masa mendatang—misalnya, tugas terjadwal dan batas waktu pekerja.

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

tugas/seledri_worker.py

Modul ini diperlukan untuk memulai dan menginisialisasi pekerja Seledri, yang akan berjalan di wadah Docker terpisah. Ini menginisialisasi konteks aplikasi Flask untuk memiliki akses ke lingkungan yang sama dengan aplikasi. Jika itu tidak diperlukan, garis-garis ini dapat dihapus dengan aman.

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

api/__init__.py

Selanjutnya adalah paket API, yang mendefinisikan REST API menggunakan paket Flask-Restful. Aplikasi kami hanyalah demo dan hanya akan memiliki dua titik akhir:

  • /process_data – Memulai operasi lama dummy pada pekerja Seledri dan mengembalikan ID tugas baru.
  • /tasks/<task_id> – Mengembalikan status tugas menurut ID tugas.
 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>')

model/__init__.py

Sekarang kita akan menambahkan model SQLAlchemy untuk objek User , dan kode inisialisasi mesin database. Objek User tidak akan digunakan oleh aplikasi demo kami dengan cara apa pun yang berarti, tetapi kami memerlukannya untuk memastikan migrasi basis data berfungsi dan integrasi SQLAlchemy-Flask diatur dengan benar.

 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)

Perhatikan bagaimana UUID dihasilkan secara otomatis sebagai ID objek dengan ekspresi default.

app.py

Terakhir, mari kita buat file aplikasi Flask utama.

 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>

Inilah kami:

  • Mengonfigurasi logging dasar dalam format yang tepat dengan waktu, level, dan ID proses
  • Mendefinisikan fungsi pembuatan aplikasi Flask dengan inisialisasi API dan “Hello, world!” halaman
  • Menentukan titik masuk untuk menjalankan aplikasi selama waktu pengembangan

wsgi.py

Juga, kita akan membutuhkan modul terpisah untuk menjalankan aplikasi Flask dengan Gunicorn. Ini hanya akan memiliki dua baris:

 from app import create_app app = create_app()

Kode aplikasi sudah siap. Langkah kita selanjutnya adalah membuat konfigurasi Docker.

Membangun Kontainer Docker

Aplikasi kami akan membutuhkan beberapa wadah Docker untuk dijalankan:

  1. Wadah aplikasi untuk menyajikan halaman template dan mengekspos titik akhir API. Merupakan ide bagus untuk membagi dua fungsi ini pada produksi, tetapi kami tidak memiliki halaman template di aplikasi demo kami. Container tersebut akan menjalankan web server Gunicorn yang akan berkomunikasi dengan Flask melalui protokol WSGI.
  2. Wadah pekerja seledri untuk menjalankan tugas yang panjang. Ini adalah wadah aplikasi yang sama, tetapi dengan perintah run kustom untuk meluncurkan Celery, bukan Gunicorn.
  3. Wadah pengocok seledri—mirip dengan di atas, tetapi untuk tugas yang dilakukan pada jadwal reguler, seperti menghapus akun pengguna yang tidak pernah mengonfirmasi email mereka.
  4. wadah RabbitMQ. Seledri membutuhkan perantara pesan untuk berkomunikasi antara pekerja dan aplikasi, dan menyimpan hasil tugas. RabbitMQ adalah pilihan umum, tetapi Anda juga dapat menggunakan Redis atau Kafka.
  5. Wadah basis data dengan PostgreSQL.

Cara alami untuk mengelola banyak container dengan mudah adalah dengan menggunakan Docker Compose. Tapi pertama-tama, kita perlu membuat Dockerfile untuk membangun image container untuk aplikasi kita. Mari kita masukkan ke direktori proyek.

 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

File ini menginstruksikan Docker untuk:

  • Instal semua dependensi menggunakan Pipenv
  • Tambahkan folder aplikasi ke wadah
  • Buka port TCP 5000 ke host
  • Setel perintah startup default container ke panggilan Gunicorn

Mari kita bahas lebih lanjut apa yang terjadi di baris terakhir. Ini menjalankan Gunicorn yang menentukan kelas pekerja sebagai gevent. Gevent adalah lib konkurensi ringan untuk multitasking kooperatif. Ini memberikan peningkatan kinerja yang cukup besar pada beban terikat I/O, memberikan pemanfaatan CPU yang lebih baik dibandingkan dengan multitasking preemptive OS untuk utas. Parameter --workers adalah jumlah proses pekerja. Sebaiknya setel sama dengan jumlah inti di server.

Setelah kami memiliki Dockerfile untuk wadah aplikasi, kami dapat membuat file docker-compose.yml , yang akan mendefinisikan semua wadah yang diperlukan aplikasi untuk dijalankan.

 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

Kami mendefinisikan layanan berikut:

  • broker-rabbitmq – Wadah perantara pesan RabbitMQ. Kredensial koneksi ditentukan oleh variabel lingkungan
  • db-postgres – Wadah PostgreSQL dan kredensialnya
  • migration – Wadah aplikasi yang akan melakukan migrasi database dengan Flask-Migrate dan keluar. Wadah API bergantung padanya dan akan berjalan setelahnya.
  • api – Wadah aplikasi utama
  • api-worker dan api-beat – Kontainer yang menjalankan pekerja Seledri untuk tugas yang diterima dari API dan tugas terjadwal

Setiap wadah aplikasi juga akan menerima variabel APP_ENV dari perintah docker-compose up .

Setelah semua aset aplikasi siap, mari kita letakkan di GitHub, yang akan membantu kita menerapkan kode di server.

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

Mengkonfigurasi Server

Kode kami ada di GitHub sekarang, dan yang tersisa hanyalah melakukan konfigurasi server awal dan menerapkan aplikasi. Dalam kasus saya, server adalah instance AWS yang menjalankan AMI Linux. Untuk rasa Linux lainnya, instruksi mungkin sedikit berbeda. Saya juga berasumsi bahwa server sudah memiliki alamat IP eksternal, DNS dikonfigurasi dengan catatan A yang menunjuk ke IP ini, dan sertifikat SSL dikeluarkan untuk domain tersebut.

Tip keamanan: Jangan lupa untuk mengizinkan port 80 dan 443 untuk lalu lintas HTTP(S), port 22 untuk SSH di konsol hosting Anda (atau menggunakan iptables ) dan tutup akses eksternal ke semua port lain! Pastikan untuk melakukan hal yang sama untuk protokol IPv6 !

Menginstal Dependensi

Pertama, kita perlu Nginx dan Docker berjalan di server, ditambah Git untuk menarik kodenya. Mari login melalui SSH dan gunakan manajer paket untuk menginstalnya.

 sudo yum install -y docker docker-compose nginx git

Mengonfigurasi Nginx

Langkah selanjutnya adalah mengkonfigurasi Nginx. File konfigurasi nginx.conf utama seringkali baik apa adanya. Namun, pastikan untuk memeriksa apakah itu sesuai dengan kebutuhan Anda. Untuk aplikasi kami, kami akan membuat file konfigurasi baru di folder conf.d Konfigurasi tingkat atas memiliki arahan untuk memasukkan semua file .conf darinya.

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

Berikut adalah file konfigurasi situs Flask untuk Nginx, termasuk baterai. Ini memiliki fitur berikut:

  1. SSL dikonfigurasi. Anda harus memiliki sertifikat yang valid untuk domain Anda, misalnya, sertifikat Let's Encrypt gratis.
  2. www.your-site.com permintaan dialihkan ke your-site.com
  3. Permintaan HTTP dialihkan ke port HTTPS yang aman.
  4. Proksi terbalik dikonfigurasi untuk meneruskan permintaan ke port lokal 5000.
  5. File statis dilayani oleh Nginx dari folder lokal.
 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; } }

Setelah mengedit file, jalankan sudo nginx -s reload dan lihat apakah ada kesalahan.

Menyiapkan Kredensial GitHub

Merupakan praktik yang baik untuk memiliki akun VCS “penyebaran” terpisah untuk menerapkan proyek dan sistem CI/CD. Dengan cara ini Anda tidak mengambil risiko dengan mengekspos kredensial akun Anda sendiri. Untuk lebih melindungi repositori proyek, Anda juga dapat membatasi izin akun tersebut untuk akses hanya-baca. Untuk repositori GitHub, Anda memerlukan akun organisasi untuk melakukannya. Untuk menyebarkan aplikasi demo kami, kami hanya akan membuat kunci publik di server dan mendaftarkannya di GitHub untuk mendapatkan akses ke proyek kami tanpa memasukkan kredensial setiap saat.

Untuk membuat kunci SSH baru, jalankan:

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

Kemudian masuk ke GitHub dan tambahkan kunci publik Anda dari ~/.ssh/id_rsa.pub di pengaturan akun.

Menyebarkan Aplikasi

Langkah terakhir cukup mudah—kita perlu mendapatkan kode aplikasi dari GitHub dan memulai semua container dengan Docker Compose.

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

Mungkin ide yang baik untuk menghilangkan -d (yang memulai wadah dalam mode terpisah) untuk menjalankan pertama untuk melihat output dari setiap wadah tepat di terminal dan memeriksa kemungkinan masalah. Pilihan lainnya adalah memeriksa setiap kontainer individu dengan docker logs sesudahnya. Mari kita lihat apakah semua container kita berjalan dengan docker ps.

gambar_alt_teks

Besar. Semua lima kontainer berdiri dan berjalan. Docker Compose menetapkan nama container secara otomatis berdasarkan layanan yang ditentukan di docker-compose.yml. Sekarang saatnya untuk menguji bagaimana seluruh konfigurasi bekerja! Yang terbaik adalah menjalankan tes dari mesin eksternal untuk memastikan server memiliki pengaturan jaringan yang benar.

 # 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

Itu dia. Kami memiliki konfigurasi aplikasi kami yang minimalis, namun sepenuhnya siap produksi, yang berjalan pada instans AWS. Semoga ini akan membantu Anda untuk mulai membangun aplikasi kehidupan nyata dengan cepat dan menghindari beberapa kesalahan umum! Kode lengkapnya tersedia di repositori GitHub.

Kesimpulan

Dalam artikel ini, kami membahas beberapa praktik terbaik dalam menyusun, mengonfigurasi, mengemas, dan menerapkan aplikasi Flask ke produksi. Ini adalah topik yang sangat besar, tidak mungkin untuk sepenuhnya dibahas dalam satu posting blog. Berikut adalah daftar pertanyaan penting yang tidak kami jawab:

Artikel ini tidak mencakup:

  • Integrasi berkelanjutan dan penerapan berkelanjutan
  • Pengujian otomatis
  • Log pengiriman
  • pemantauan API
  • Meningkatkan skala aplikasi ke beberapa server
  • Perlindungan kredensial dalam kode sumber

Namun, Anda dapat mempelajari cara melakukannya menggunakan beberapa sumber daya hebat lainnya di blog ini. Misalnya, untuk menjelajahi logging, lihat Python Logging: Tutorial Mendalam, atau untuk ikhtisar umum tentang CI/CD dan pengujian otomatis, lihat Cara Membuat Pipeline Penerapan Awal yang Efektif. Saya serahkan implementasi ini sebagai latihan kepada Anda, para pembaca.

Terima kasih sudah membaca!