当サイトは、アフィリエイト広告を利用しています
Pythonの軽量フレームワークであるflaskの開発環境を
dockerを使ってコンテナで構築し、VScodeからコンテナ内で
flask製の基本的なREST APIを実装、動作させる方法をまとめる
この記事を読めばflaskでREST API開発をする際、基本となる
が理解できると思う。
今回はコンテナはflask製のREST APIの開発環境として使うので、
コンテナデプロイは行わない。
※そのうちコンテナデプロイについても試してみる
作成するflask製のREST APIはどんなものかを
先にまとめておく。
flask製のREST APIのルートの実装方法には
下記の二つがある。
今回は両方のパターンを実装する
またFlaskのREST APIを効率的に実装するためのライブラリである
「Flask RESTful」を使って実装することもできる
「Flask RESTful」での実装については下記記事でまとめています
関数ベースのルート実装は下記のようになる。
from flask import Flask# flaskアプリケーションインスタンス作成flask_api = Flask(__name__)@flask_api.route('/')def hello():return "Hello from Flask!!!"
関数ベースのルート実装は直観的で見やすい
from flask import Flaskfrom flask.views import MethodView# flaskアプリケーションインスタンス作成flask_api = Flask(__name__)class HelloAPI(MethodView):def get(self):return "Hello from Flask!!!"flask_api.add_url_rule('/', view_func=HelloAPI.as_view('hello_api'))
クラスベースのルーティングはより複雑なロジックや複数のHTTPメソッド(GET、POST、PUT、DELETEなど)を
同じクラス内で処理する場合に便利に使える。
大規模なアプリケーションや複雑なルーティングロジックを扱う場合、
クラスベースのルートはコードの再利用しやすいと思う。
作成するREST APIはHTTPメソッドの
を受け付けるように作る
※REST APIなので
また
が含まれたHTTP通信が来た場合の送受信も実装する
パスパラメータやクエリパラメータについては下記記事で書いている。
REST APIの基本的なことについてもまとめているのでご参照ください。
flask製のREST APIを動かすためのサーバーだが
flaskには簡易的な内部サーバーが付属しているので
それで動かすことにする。
外部サーバーであるGunicornを使う方法もある。
Gunicornで動かす場合の方法は下記記事でまとめています
VSCodeとDockerを使ったflaskの開発環境構築時の
Dockerコンテナの作成は
の2パターンで行うことができる。
また開発環境内のflask製のREST APIを
dockerのどこで永続化するかについても
の2パターンがある
今回は、上記の方法のうち
で行っていく。
またdockerコンテナの永続化を行うvolumeの設定について詳しくは下記記事参照
docker-composeコマンドで作ったコンテナに VScodeからアタッチする形になる。
その際にVSCodeの拡張機能であるDevcontainerを使う。
使い方については下記記事で紹介しています
下記の環境で行う
Docker for Windowsのインストール方法については下記記事で 紹介しています
全体的な構成は下記のようになる
|-- .env|-- .vscode| |-- launch.json| `-- tasks.json|-- Dockerfile|-- api| |-- __init__.py| |-- api.py| `-- wsgi.py|-- docker-compose.yml|-- requirements.txt`-- vscode_ex_install.sh
FlaskのREST APIを開発するためのコンテナ環境を作るためのファイル
詳細は後述する
下記がVSCodeの機能を使うための設定ファイルになる
詳細は後述する
下記がFlaskのREST APIの実装ファイルになる
詳細は後述する
dockerを使ってFlaskのREST APIを実装し、実行するための
コンテナを作成する。
上記でも記載したが、コンテナの作成自体はdocker composeコマンドで
行い、VScodeの拡張機能を使って作成したコンテナにアタッチして実装する
docker composeを使ってコンテナ作成するので
コンテナ作成するためのファイルについて解説する
FLASK_APP=api.wsgi:api # Flaskがどのアプリケーションを実行するか指定(パッケージ.モジュール:flaskインスタンス)FLASK_ENV=development # Flaskがどの環境で動作するか指定FLASK_DEBUG=true #デバッグモードの指定# PYTHONPATH=/workspace/api #Pythonがモジュールを検索するためのパスを指定
Flaskアプリケーションの設定や動作を制御するための環境変数を設定する
PYTHONPATHにPythonがモジュールを検索するためのパスを指定する。
指定するとPythonがモジュールを探す際に、この環境変数で指定されたパスを優先的に検索するようになる
PYTHONPATHに「/workspace/api」を追加しておかないと
flask run実行時にflaskインスタンスを見つけられずエラーになる。
「FLASK_APP=wsgi:api」についてはwsgi.pyはapiディレクトリ配下にあるが
PYTHONPATHに「/workspace/api」を設定しているため、apiディレクトリの記載は不要になる
__init__.pyを作成すればPYTHONPATHの設定は不要でした。
Pythonの環境をセットアップし、必要な依存関係をインストールするDockerイメージを作成する
FROM python:3.12# workspaceディレクトリ作成、移動WORKDIR /workspace# プロジェクトディレクトリにコピーCOPY requirements.txt /workspace# 必要モジュールのインストールRUN pip install --upgrade pipRUN pip install -r requirements.txt
requirements.txtを読み込み必要なパッケージをインストールする
version: "3"services:sample-api:container_name: "sample-api"build:context: .dockerfile: Dockerfileports:- "5000:5000"volumes:# バインドマウント- .:/workspace# 環境変数読み込みenv_file:- .envtty: true
インストールするパッケージを設定
Flask==3.0.2
必須なのはFlaskのみ
実際にコンテナを作っていく。
「Docker for Windows」を起動しておく。
※起動しておかないとできないので
$ docker compose up -d$ docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESc9050742de6a flask-sample-restapi-sample-api "python3" About a minute ago Up About a minute 0.0.0.0:5000->5000/tcp sample-api
「docker ps」コマンドで作成できていることも確認しておく。
永続化をバインドマウントにしているので
「docker compose」を実行したディクショナリ配下が
コンテナにコピーされている
またコンテナ内での変更はホスト側にも反映される。
「docker compose」を使ってコンテナを作成した場合
拡張機能はインストールされないので、スクリプトを使って
インストールする
#!/bin/bash# 拡張機能のIDリストextensions=("ms-python.python""ms-python.vscode-pylance""ms-python.debugpy""mhutchie.git-graph")# 各拡張機能をインストールfor extension in "${extensions[@]}"; docode --install-extension $extensiondone
このスクリプトを実行することで
拡張機能がインストールされる
詳しくは下記記事でまとめています
開発環境が構築できたのでREST APIを実行していく。
中身は空白でOK
apiをパッケージとして認識させるために作成
flaskのREST APIを起動するためのファイル
# flaskインスタンスをimport# apiパッケージ.apiモジュール.apiインスタンスfrom api.api import apiif __name__ == "__main__":# flaskサーバーを起動api.run()
wsgi.pyは本番環境でのデプロイメントにおいて、WSGI互換のサーバー(例えばGunicornやuWSGIなど)と
アプリケーションを接続するために使用される
※wsgi.pyはWSGIサーバーがアプリケーションオブジェクトを認識するためのエントリポイントとして機能し、
多くのサーバーとフレームワークがこのファイルをデフォルトで探しにくる。
開発中やテスト段階ではwsgi.pyを使わずにFlaskの内部サーバーを直接起動することもできる(むしろそっちが主流)が
本番環境にデプロイ前には作る必要はあるので、Flaskの内部サーバーもwsgi.py経由で起動させておく
flaskのREST APIのルーティングと処理を実装している
一旦、全体を載せる。
from flask import Flask, request, jsonifyfrom flask.views import MethodViewimport copy# flaskインスタンス作成api = Flask(__name__)# ディクショナリusers = [{"user_id": "1", "name": "tujimura", "age": 11},{"user_id": "2", "name": "mori", "age": 20},{"user_id": "3", "name": "shimada", "age": 50},{"user_id": "4", "name": "kyogoku", "age": 70}]# 関数ベースのルーティング# 取得# HTTPリクエストにクエリパラメータあり# 全件取得 or ageフィルタリング@api.route('/functionalBase', methods=['GET'])def get_users():# GETリクエストを処理age = request.args.get('age')if age:return jsonify(list(filter(lambda user: user['age'] == int(age), users)))return jsonify(users)# 登録# HTTPリクエストからJSONを受け取る# JSONデータを新規登録@api.route('/functionalBase', methods=['POST'])def post_user():# POSTリクエストを処理data = request.get_json()res_users = copy.deepcopy(users)res_users.append(data)return jsonify(res_users), 201# 更新# HTTPリクエストにパスパラメータあり# パスパラメータをkeyにしてJSONデータで更新@api.route('/functionalBase/<user_id>', methods=['PUT'])def put_user(user_id):# PUTリクエストを処理data = request.get_json()res_users = copy.deepcopy(users)res_users = list(map(lambda user: user.update(data) or user if user['user_id'] == user_id else user, res_users))return jsonify(res_users)# 削除# HTTPリクエストにパスパラメータあり# パスパラメータをkeyにして削除@api.route('/functionalBase/<user_id>', methods=['DELETE'])def delete_user(user_id):# DELETEリクエストを処理res_users = list(filter(lambda user: user['user_id'] != user_id, users))return jsonify(res_users)# クラスベースのルーティングclass UserAPI(MethodView):# 取得# HTTPリクエストにクエリパラメータあり# 全件取得 or ageフィルタリングdef get(self):age = request.args.get('age')if age:return jsonify(list(filter(lambda user: user['age'] == int(age), users)))return jsonify(users)# 登録# HTTPリクエストからJSONを受け取る# JSONデータを新規登録def post(self):data = request.get_json()res_users = copy.deepcopy(users)res_users.append(data)return jsonify(res_users), 201# 更新# HTTPリクエストにパスパラメータあり# パスパラメータをkeyにしてJSONデータで更新def put(self, user_id):data = request.get_json()res_users = copy.deepcopy(users)res_users = list(map(lambda user: user.update(data) or user if user['user_id'] == user_id else user, res_users))return jsonify(res_users)# 削除# HTTPリクエストにパスパラメータあり# パスパラメータをkeyにして削除def delete(self, user_id):res_users = list(filter(lambda user: user['user_id'] != user_id, users))return jsonify(res_users)# クラスベースのビューを関数ベースのビューに変換user_view = UserAPI.as_view('user_api')# エンドポイントを設定api.add_url_rule('/classBase', view_func=user_view, methods=['GET',])api.add_url_rule('/classBase', view_func=user_view, methods=['POST',])api.add_url_rule('/classBase/<string:user_id>', view_func=user_view, methods=['PUT', 'DELETE'])
ディクショナリに対して、参照、登録、更新、削除を行うAPIを実装した
動きを見やすいように、それぞれのhttpメソッドの関数は編集後ディクショナリをjsonで返却するようにした。
※敢えて元のディクショナリは変更しないようにしている。
実際にすることはないと思うが、動作を見るため
の両方の書き方でルートを書いている。
関数ベースルートの実装部分について解説する
# 関数ベースのルーティング# 取得# HTTPリクエストにクエリパラメータあり# 全件取得 or ageフィルタリング@api.route('/functionalBase', methods=['GET'])def get_users():# GETリクエストを処理age = request.args.get('age')if age:return jsonify(list(filter(lambda user: user['age'] == int(age), users)))return jsonify(users)# 登録# HTTPリクエストからJSONを受け取る# JSONデータを新規登録@api.route('/functionalBase', methods=['POST'])def post_user():# POSTリクエストを処理data = request.get_json()res_users = copy.deepcopy(users)res_users.append(data)return jsonify(res_users), 201# 更新# HTTPリクエストにパスパラメータあり# パスパラメータをkeyにしてJSONデータで更新@api.route('/functionalBase/<user_id>', methods=['PUT'])def put_user(user_id):# PUTリクエストを処理data = request.get_json()res_users = copy.deepcopy(users)res_users = list(map(lambda user: user.update(data) or user if user['user_id'] == user_id else user, res_users))return jsonify(res_users)# 削除# HTTPリクエストにパスパラメータあり# パスパラメータをkeyにして削除@api.route('/functionalBase/<user_id>', methods=['DELETE'])def delete_user(user_id):# DELETEリクエストを処理res_users = list(filter(lambda user: user['user_id'] != user_id, users))return jsonify(res_users)
関数ベースルートは@api.routeでエンドポイントとHTTPメソッドを指定する
直観的にわりやすい。
クラスベースルートの実装部分について解説する
# クラスベースのルーティングclass UserAPI(MethodView):# 取得# HTTPリクエストにクエリパラメータあり# 全件取得 or ageフィルタリングdef get(self):age = request.args.get('age')if age:return jsonify(list(filter(lambda user: user['age'] == int(age), users)))return jsonify(users)# 登録# HTTPリクエストからJSONを受け取る# JSONデータを新規登録def post(self):data = request.get_json()res_users = copy.deepcopy(users)res_users.append(data)return jsonify(res_users), 201# 更新# HTTPリクエストにパスパラメータあり# パスパラメータをkeyにしてJSONデータで更新def put(self, user_id):data = request.get_json()res_users = copy.deepcopy(users)res_users = list(map(lambda user: user.update(data) or user if user['user_id'] == user_id else user, res_users))return jsonify(res_users)# 削除# HTTPリクエストにパスパラメータあり# パスパラメータをkeyにして削除def delete(self, user_id):res_users = list(filter(lambda user: user['user_id'] != user_id, users))return jsonify(res_users)# クラスベースのビューを関数ベースのビューに変換user_view = UserAPI.as_view('user_api')# エンドポイントを設定api.add_url_rule('/classBase', view_func=user_view, methods=['GET',])api.add_url_rule('/classBase', view_func=user_view, methods=['POST',])api.add_url_rule('/classBase/<string:user_id>', view_func=user_view, methods=['PUT', 'DELETE'])
クラスベースのルートは
を行う必要がある。
クラスでエンドポイントのメソッド名については
とHTTPメソッドと合わせる。
HTTPメソッドは
があり、それに対して
が組み合わされてでリクエストされていくるので
それぞれサンプルとして実装した
関数ベースルートのコードを元に解説する
HTTPメソッドがGETでクエリパラメータがある場合の実装
# 取得# HTTPリクエストにクエリパラメータあり# 全件取得 or ageフィルタリング@api.route('/functionalBase', methods=['GET'])def get_users():# GETリクエストを処理age = request.args.get('age')if age:return jsonify(list(filter(lambda user: user['age'] == int(age), users)))return jsonify(users)
クエリパラメータ(age)がある場合は、usersディクショナリをageでフィルタリングして
json形式でレスポンスを返却する
HTTPメソッドがPOSTでリクエストボディにJSONがある場合の実装
# 登録# HTTPリクエストからJSONを受け取る# JSONデータを新規登録@api.route('/functionalBase', methods=['POST'])def post_user():# POSTリクエストを処理data = request.get_json()users.append(data)return jsonify(users), 201
リクエストからJSONを取得してusersディクショナリに
新規登録する
HTTPステータスコードは201(Created)で返却する
※他メソッドは成功の場合はデフォルトの200で返却している
HTTPメソッドがPUTでパスパラメータあり、リクエストボディにJSONがありの場合の実装
# 更新# HTTPリクエストにパスパラメータあり# パスパラメータをkeyにしてJSONデータで更新@api.route('/functionalBase/<user_id>', methods=['PUT'])def put_user(user_id):# PUTリクエストを処理data = request.get_json()res_users = list(map(lambda user: user.update(data) or user if user['user_id'] == user_id else user, users))return jsonify(res_users)
リクエストからのパスパラメータをメソッドの引数として使うことができる。
JSONデータを取得し、usersディクショナリの中からパスパラメータと一致する
レコードを探して、更新している。
HTTPメソッドがDELETEでパスパラメータありの場合の実装
# 削除# HTTPリクエストにパスパラメータあり# パスパラメータをkeyにして削除@api.route('/functionalBase/<user_id>', methods=['DELETE'])def delete_user(user_id):# DELETEリクエストを処理res_users = list(filter(lambda user: user['user_id'] != user_id, users))return jsonify(res_users)
リクエストからのパスパラメータをメソッドの引数として使うことができる。
usersディクショナリの中からパスパラメータと一致するレコードを探して、削除している。
実装したコードをFlaskサーバーで起動する。
Flaskサーバーを「flask run」コマンドで起動する際には
が設定されている必要がある。
コンテナを作成する際に.envファイルをdocker-compose.ymlで
読み込んで設定しているので確認する。
コンテナにアタッチしているVScodeのターミナルで確認しておく
$ echo $FLASK_APPapi.wsgi:api$ echo $FLASK_ENVdevelopment$ echo $FLASK_DEBUGtrue
コンテナにアタッチしているVScodeのターミナルで起動する
$ flask run --host=0.0.0.0* Tip: There are .env or .flaskenv files present. Do "pip install python-dotenv" to use them.* Serving Flask app 'api.wsgi.py'* Debug mode: onWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.* Running on all addresses (0.0.0.0)* Running on http://127.0.0.1:5000* Running on http://172.23.0.2:5000Press CTRL+C to quit* Restarting with stat* Tip: There are .env or .flaskenv files present. Do "pip install python-dotenv" to use them.* Debugger is active!* Debugger PIN: 123-556-262
flask runでは環境変数FLASK_APPに指定されているflaskインスタンスを実行する。
下記の「api.run()」が実行されるイメージ。
# flaskインスタンスをimportfrom api.api import apiif __name__ == "__main__":# flaskサーバーを起動api.run()
Dockerのコンテナは、コンテナがそれぞれ独立した環境を持っているため
デフォルトではホストOSからアクセスできない状態になっている
そしてflaskサーバーがデフォルトで待ち受けるIPアドレスが127.0.0.1(localhost)
のためIP、ポート番号なしで起動してもホストからはアクセスできない。
そのため起動時に
を指定して起動することでホストからアクセス可能になる。
いちいちコマンドで「flask run --host=0.0.0.0」を入力して
実行するのは面倒なので、VScodeのタスクに登録しておく。
実行すると.vscode/task.jsonがプロジェクトルートに作成される
下記のように書き換える
{"version": "2.0.0","tasks": [{"label": "Flask Run","type": "shell","command": "flask run --host=0.0.0.0","group": {"kind": "build","isDefault": true},"presentation": {"reveal": "always"},"problemMatcher": []}]}
これでコマンドを打たずともサーバーを起動できる
このタスクはこのプロジェクトでしか使わないため
カスタムタスクとして登録したが
全体で使うコマンド(docker compose系)などはユーザータスクとして
登録しておくと便利だと思う
詳しくは下記記事参照
flaskサーバーを起動して
作成したREST APIの動作を確認する
ブラウザのアドレスバーから直接送信できるのはGETリクエストのみになる
GET,POST,PUT,DELETEの全て動作を確認するためには
ブラウザからはできないのでツールやプログラムを使う必要がある
今回はPostmanを使う
Postmanはpostmanから入手できる
関数ベースのルートで確認してみる。
クラスベースのルートでも同様のやり方で確認できる
GETリクエストでusersディクショナリの全件が取得できる
GETリクエストにクエリパラメータをつけて送ると
usersディクショナリのageでフィルタリングしたでーたが取得できる
POSTリクエストをボディにJSONデータをつけて送信すると
JSONデータのuserがusersディクショナリに追加された結果が取得できる
またコード201が帰ってくる
PUTリクエストをパスパラメータつけ、ボディにJSONデータをつけて送信すると
パスパラメータのuser_idと一致したuserディクショナリのレコードが
送付したJSONデータで更新される
DELETEリクエストをパスパラメータつけて送信すると
パスパラメータのuser_idと一致したuserディクショナリのレコードが削除される
最後にVScodeでデバッグする方法を記載する
VScodeではデバッグする際はデバッグ設定用のファイルである
を作る必要がある。
flaskデバッグ用のlaunch.jsonを作成する
{"version": "0.2.0","configurations": [{"name": "Python デバッガー: Flask","type": "debugpy","request": "launch","module": "flask","env": {"FLASK_APP": "api.wsgi:api","FLASK_DEBUG": "1"},"args": ["run","--debugger","--reload","--host=0.0.0.0"],"jinja": true}]}
実際にデバッグを実行してみる
作成したlaunch.jsonのnameに設定した値が表示されるので
選択して実行する
dockerコンテナでflaskの開発環境を作成し、そのコンテナ内で
flaskのREST APIを実装し、動作させるまでの手順をまとめてみた。
動作を見るために作成したapi.pyなどは実際使用する時に書き換える
必要がある。※関数ベースとクラスベースの併用とかは普通はしないと思うので
またAPIとしてほぼルーティング機能しか持たせていないので
そのほかのビジネスロジックやデータベースを使う場合はプロジェクト構成を
考える必要がありそう。
今回はREST APIの作り方と動かし方について重きをおいて作ったので
次は応用偏としてもう少し凝ったREST APIを作ってみる。