docker-composeで、Nginx+Flask+MySQLの構成でREST APIを実装していきます。
今回のゴールはヘルスチェック用のサンプルAPIがresponseを返すまでです。DB接続やマイグレーションについては、触れていません。
なぜFlaskなのかなどの技術選定についてはこちらです。
ディレクトリ構成(ファイル構成)
サンプルAPIが動くまでなので、以下の様なシンプル構成にします。(作り込んでいくときにアーキテクチャーは考えることにします。)
/
|---app/
| |---controller/
| | |---health.py
| |---app.ini
| |---app.py
| |---uwsg.log
| |---wsgi.py
|---docker/
|---api/
| |---Dockerfile
| |---requirement.txt
|---db/
| |----my.cnf
|---web/
| |----nginx.conf
|---docker-compose.yml
Docker周りの基本設定
まずは、Flask, Nginx, MySQLが動くコンテナを用意するために、docker-compose.ymlを作成します。
docker-compose
version: '3.8'
services:
api:
build:
context: .
dockerfile: ./api/Dockerfile
container_name: sample_api
ports:
- "5000:5000"
volumes:
- ../app:/usr/src/app
- socket:/tmp
networks:
- web-net
- db-net
# command: flask run --host=0.0.0.0
command: uwsgi --ini /usr/src/app/app.ini
db:
restart: always
image: mysql:8.0.28
container_name: sample_db
environment:
MYSQL_DATABASE: api
MYSQL_USER: api
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: password
cap_add:
- SYS_NICE
ports:
- '3306:3306'
volumes:
- ./db/my.cnf:/etc/mysql/conf.d/my.cnf
networks:
- db-net
web:
image: nginx:latest
container_name: sample_web
ports:
- "80:80"
volumes:
- ./web/nginx.conf:/etc/nginx/conf.d/default.conf
- socket:/tmp
networks:
- web-net
volumes:
socket:
networks:
db-net:
name: sample-db-net
web-net:
name: sample-web-net
docker-compseのサンプルより少し盛りすぎに思われるかもしれませんが、これぐらいは最低限かと思います。
Flask実行コマンドについて
Flask内蔵の簡易Webサーバを起動する場合は、
flask run --host=0.0.0.0
のコマンドになります。
ただ今回はNginxを使うので、このコマンドはコメントアウトしuWSGIを起動する様に以下のコマンドにしています。
uwsgi --ini /usr/src/app/app.ini
netの設定
netは設定しなければ、デフォルトで全てのコンテナがbridgeというネットワーク名に入ります。今回はwebサーバとdbサーバ間のアクセスは分離したかったので、webサーバ用のネットワークとdb用のネットワークを作成して、所属を分けました。
DBの設定
DBはまだ使わないので、my.cnfは空ファイルですが、後々タイムゾーンの設定などで使うことになるので、ファイルは作成しておいてマッピングだけ済ませておきます。
DBデータのマッピングは、まだしていません。この辺りはDBの設定を詰めるときに、やることにします。
Nginxの設定
Nginxの設定は、ネットから適当に拾ってきました。uwsgiと接続するだけで、特殊なことはしません。
nginx.conf
server {
listen 80;
listen [::]:80;
server_name localhost;
location / {
include uwsgi_params;
uwsgi_pass unix:/tmp/uwsgi.sock;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
Flaskの設定
メインのFlask部分は、Pythonのベースイメージに必要ライブラリをインストールしていくことにします。
Dockerfileを作成して、これをdocker-composeで指定しておきます。
Dockerfile
FROM python:3.10.4-slim-buster
WORKDIR /usr/src/app
ENV FLASK_APP=app
COPY ./api/requirements.txt ./
RUN apt update
RUN apt-get install -y build-essential python
RUN pip install --upgrade pip
RUN pip install --upgrade wheel
RUN pip install --upgrade setuptools
RUN pip install -r requirements.txt
後々でDB周りの拡張ライブラリなどが必要になりますが、現時点で必要なライブラリはflaskとuwagiだけなので、requirements.txtは以下の通りです。現時点(2022年5月現在)での最新のバージョンを指定しておきます。
requirements.txt
flask==2.1.1
uWSGI==2.0.20
アプリ側の実装
uwsgiの設定
まずはuwagiの起動時に読まれる設定ファイルを作成します。
app.ini
[uwsgi]
module = wsgi:app
master = true
socket = /tmp/uwsgi.sock
chmod-socket = 666
wsgi-file = /usr/src/app/wsgi.py
logto = /usr/src/app/uwsgi.log
ログは、とりあえずapp直下に出力するようにします。これはみっともないので、DB周りの設定が済んだ後、ログ周りはまとめて設定しなおすことにします。
Controller
これで動作環境周りが一応はできたので、いよいよアプリの実装に入っていきます。
まずはControllerから。Flaskのサンプルは、main(app.py)に直接書いてあることが多いのですが、これは個人的にどうかと思うのでControllerフォルダを作成して、その中に書いていくことにします。
とりあえずヘルスチェック用のGETとPOSTを用意します。
controller/health.py
from flask import Blueprint
bp = Blueprint("health", __name__, url_prefix="/health")
@bp.route("/", methods=["GET"])
def get():
return "OK"
@bp.route("/hello/<string:name>", methods=["POST"])
def hello(name):
return f"hello {name}"
この辺はFlask固有の書き方になります。といっても、Webフレームワークによくある書き方だと思います。
Blueprintで起点となるエンドポイントを定義し、デコレータで後続のエンドポイントとメソッドをを定義しています。
app.py
app.pyは、Flask起動を扱うファイルです。いわゆるmainです。
appの起動を設定しつつ、作成したControllerをappに紐付けます。
app.py
from flask import Flask
from controller import health
app = Flask(__name__)
app.register_blueprint(health.bp)
if __name__ == "__main__":
app.run(debug=True, host='0.0.0.0')
uwagi起動ファイル
最後にuwagiからappをコールするファイルを作成します。このファイルは、app.iniで設定しているwsgi-fileです。
wsgi.py
from app import app
if __name__ == '__main__':
app.run()
起動
以上で設定は完了です。
問題なければ、
docker-compose up
で、web, db, apiの3つのコンテナが起動します。
http://localhost/health/ にGETでアクセスしてステータス200が返ってくれば動作OKです。
余談
余談1
今回はappのディレクトリ名をappとしましたが、これは配下にapp.pyと名前がかぶるのでよくありません。
他からappを呼ぶとき、モジュールを呼びたいのにパッケージ呼んでしまうといったことが起こります。別名にした方が良いです。
余談2
今回はヘルスチェックで”OK”の文字だけで返していますが、ちゃんと作る場合は、ヘルスチェック用のAPIは、APIのリリースバージョンやコミットハッシュなどのメタ情報を返した方が良いです。
Software Designの2020年12月号の特集に「Dockerアプリケーション開発実践ガイド」が掲載されており、そこにこのようなことが書いていました。
