【AWS】 FastAPI + Streamlit + DocumentDB構成で、簡易的な顧客情報入力フォームを作る(1)
はじめに
ご覧いただきありがとうございます。阿河です。
普段の業務でAWSを取り扱う中で、お客様から様々なサービス/構成の御相談を受けます。
私の担当はインフラ寄りではありますが、アプリケーション側の挙動が分からないと本質的な理解ができないのではないかと思っています。
そこでこの1週間ほど、FastAPIとStreamlitというフレームワークを学習しました。
「FastAPI + Streamlit + DocumentDB構成」で簡単な顧客情報入力フォームを作り、DocumentDBとのデータ連携を試していきたいと思います。
対象者
- AWSを運用中
- DynamoDBやDocumentDBなどデータベースの挙動を確かめてみたいが、コーディングスキルがなく検証できないとお悩みの方
- スキルの幅は広げたいが、フロントエンドまで手を出せない方
- MongoDB互換のDocumentDBを触ってみたい方
なお当方コーディング経験が浅いため、開発経験が長い方からすると効率の悪いコードの書き方をしている箇所があると思います。ご了承いただけると幸いです。
概要
(★)がついているセクションは、今回手を動かして頂く項目です。
- 今回のハンズオン構成
- EC2にFastAPI,Streamlitをインストール(★)
- FastAPIの簡易テスト: GETリクエストを投げる(★)
- Streamlitで入力フォーム作成(★)
- DocumentDBの構築~接続テスト~MongoDBの挙動確認(★)
- 入力フォーム ⇒ DocumentDB
- Streamlitで顧客情報ページを作成
- DocumentDB ⇒ 顧客情報ページ
- 挙動確認
各種公式ドキュメントを参照しながら進めていきます。
今回は(5)の工程まで進めて、土台の部分を作っていきたいと思います。
事前準備
- AWSアカウント作成
- AdministratorAccessを付与したIAMユーザーの作成
1.今回のハンズオン構成/使用データ
【構成】
- EC2
- DocumentDB
【下準備】
- VPC
- Public Subnet(Internet Gatewayへのルート設定あり)/ Private Subnet
2.EC2にFastAPI,Streamlitをインストール
EC2の作成
・AMI: Amazon Linux2
・インスタンスタイプ: t2.micro
・Public Subnetに配置
・パブリックIP自動割り当て有効化
・セキュリティグループ(SSH/カスタムTCP=8000番ポートからのインバウンド通信許可)
※情報表示の関係でソースを0.0.0.0にしていますが、ソースは絞ったほうが望ましいです。
上記設定でEC2インスタンスを作成します。
作成完了したら、SSH接続ができることを確認します。
FastAPIとStreamlitのインストール
-
EC2にSSHログイン
-
Pythonおよびパッケージ管理システムのインストール状況確認
$ sudo yum update -y
$ python3 --version
$ pip3 --version
- FastAPIおよびuvicornのインストール
$ pip3 install fastapi
$ pip3 install uvicorn
$ pip3 list
API構築のためのWebフレームワークである「FastAPI」と、Python用のASGIWebサーバであるuvicornをインストールします。
- Streamlitのインストール
$ pip3 install streamlit
$ pip3 list
Python でフロントエンド部分を構築出来るフレームワーク「Streamlit」をインストールします。
3. FastAPIの簡易テスト: GETリクエストを投げる
FastAPIを触ってみましょう。
- main.pyの作成
$ mkdir fast-app
$ cd fast-app
$ vi main.py
これからfast-appを作業フォルダとして、作業を行っていきます。
main.py
#必要な機能をインポートする
from fastapi import FastAPI
import uvicorn
import random
#FastAPIインスタンス作成
app = FastAPI()
#/testに対してGETメソッドが実行されたら、定義したtest関数を実行する
@app.get("/test")
async def test():
return {
"id": random.randint(1,10), #指定した範囲の中でランダムな整数を返す
"name": "name",
"address": "Tokyo",
"age": 30
}
#python main.pyでファイルが呼び出されたときに、Uvicornサーバーを起動する
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
ここでやりたいことは、「指定したURLに対してGETリクエストを投げると、関数が実行され、定義した4つの値が返ってくる」仕組みを作ることです。
具体的な記述方法は、公式を参考にしています。
・FastAPIはAPI機能を提供するPythonクラスであり、インポートが必要です。
・FastAPIクラスのインスタンスを作成する必要があります。後程uvicornが参照します。
・APIを構築する際は、HTTPメソッドを使用して特定のアクションを実行する。
POST: データの作成
GET: データの読み取り
PUT: データの更新
DELETE: データの削除
・@app.get("※パス")は直下の関数が下記のリクエストの処理を担当することをFastAPIに伝えます。/testにGETリクエストが行われると、関数が実行されます。
・関数の結果、定義した4つのデータを返します。
・コードから直接Uvicornサーバを実行するため、直接Pythonプログラム(FastAPIプログラム)を呼び出すことができます。
・uvicorn runの引数にFastAPIのインスタンスを指定します。またホストやポートの指定もできます。
ここまで書けたら、main.pyを保存します。
fast-appのフォルダで、下記コマンドを実行しましょう。
$ python3 main.py
Uvicornが起動します。
次にブラウザで検索をURLにアクセスしてみましょう。
http://[※EC2のIPv4アドレス]:8000
上記の[※EC2のIPv4アドレス]の部分は、自身のEC2のパブリックIPアドレスに置き換えてください。
結果はどうでしょうか。
URLにGETリクエストを行ったので、関数に定義した4つの値が返ってくるはずです。
返ってきてないようです。
それは/testにアクセスしてないからです。
http://[※EC2のIPv4アドレス]:8000/test
にアクセスします。
4つの値が返ってきました。
このように簡易的にAPIの挙動テストが可能です。
終わったら、SSHログインしているサーバ内に戻り、CTRL + CでUvicornを終了させます。
4. Streamlitで入力フォーム作成
Streamlitで入力フォームを作ります。
先に完成イメージを共有します。
Streamlitを使うと、シンプルな入力フォームをpythonコード数行で構成できます。
サイドバーでページを選択できるようになっています。
INPUT FORMを選択すると、インプットフォームが現れます。
入力フォームは「テキストインプット」「ラジオボタン」「セレクトボックス」で、Streamlitで標準に用意されているInput widgetsを利用しています。pythonコード1行で構成できます。
Sendボタンを押すと、風船が現れ、入力した情報が下に表示されます。
完成イメージが分かったところで、早速作成してみましょう!
まずfast-appフォルダ内に、新たにファイルを作成します。
$ vi app.py
app.py
#インポート
import streamlit as st
#サイドバーとセレクトボックス
page = st.sidebar.selectbox('Choose your page', ['INPUT FORM', 'RESULT'])
#情報入力後に発火させる関数
def update_page():
st.balloons()
st.markdown('# Thank you for information')
st.json(customer_information)
#もしセレクトボックスで選択したページがINPUT FORMなら
if page == 'INPUT FORM':
st.title('INPUT FORMATION')
#各種入力フォーム
with st.form(key='customer'):
customer_name: str = st.text_input('NAME', max_chars=15)
customer_age: int = st.text_input('AGE', max_chars=3)
customer_gender = st.radio("GENDER",('MEN', 'Women'))
customer_address = st.selectbox('COUNTRY',
('Hokkaido', 'Tohoku', 'Kanto', 'Chubu', 'Kinki', 'Kansai', 'Chugoku', 'Shikoku', 'Kyusyu', 'Okinawa'))
customer_mail: str = st.text_input('Mail Address', max_chars = 30)
#フォームへの入力結果をまとめる
customer_information = {
'customer_name': customer_name,
'customer_age' : customer_age,
'customer_gender': customer_gender,
'customer_address': customer_address,
'customer_mail': customer_mail
}
#フォームへの入力結果を送信する
submit_button = st.form_submit_button(label='Send')
#submit_buttonが送信されたら、関数を実行する
if submit_button:
update_page()
StreamlitのAPIリファレンスにコードの記述方法が載っています。
「自分がやりたいこと」を実現できそうな機能をリファレンス内で見つけて、そのコードを参照するのが良いと思います。
ざっくりコードの内容を説明します。
#インポート
import streamlit as st
#サイドバーとセレクトボックス
page = st.sidebar.selectbox('Choose your page', ['INPUT FORM', 'RESULT'])
・・・・・・
#もしセレクトボックスで選択したページがINPUT FORMなら
if page == 'INPUT FORM':
st.title('INPUT FORMATION')
selectboxは、ページを切り替えるために使います。
if文を用いて、選んだページごとに表示するページを分岐させます。
コードの記述方法はリファレンスに載っています。
sidebarを付加することで、セレクトボックスがサイドバーの位置に表示されるようになります。
#もしセレクトボックスで選択したページがINPUT FORMなら
if page == 'INPUT FORM':
st.title('INPUT FORMATION')
#各種入力フォーム
with st.form(key='customer'):
customer_name: str = st.text_input('NAME', max_chars=15)
customer_age: int = st.text_input('AGE', max_chars=3)
customer_gender = st.radio("GENDER",('MEN', 'Women'))
customer_address = st.selectbox('COUNTRY',
('Hokkaido', 'Tohoku', 'Kanto', 'Chubu', 'Kinki', 'Kansai', 'Chugoku', 'Shikoku', 'Kyusyu', 'Okinawa'))
customer_mail: str = st.text_input('Mail Address', max_chars = 30)
#フォームへの入力結果をまとめる
customer_information = {
'customer_name': customer_name,
'customer_age' : customer_age,
'customer_gender': customer_gender,
'customer_address': customer_address,
'customer_mail': customer_mail
}
#フォームへの入力結果を送信する
submit_button = st.form_submit_button(label='Send')
入力フォームを作成している部分です。
制御フローであるst.formは、送信ボタンで要素をまとめるフォームを作成します。
つまり様々なタイプの入力フォームのウィジェット値を統括して、サブミットボタンがクリックされると、情報をStreamlitに送信します。
テキストインプット、ラジオボタン、セレクトボックスといった各種ウィジェットの使い方はリファレンスを参照ください。
各入力フォームにインプットされた情報をcustomer_informationにまとめます。
#情報入力後に発火させる関数
def update_page():
st.balloons()
st.markdown('# Thank you for information')
st.json(customer_information)
・・・・・・・
#フォームへの入力結果を送信する
submit_button = st.form_submit_button(label='Send')
#submit_buttonが送信されたら、関数を実行する
if submit_button:
update_page()
サブミットボタンが送信されたら、定義したupdate_pate関数を実行します。
関数の内容は
・st.balloonsを使って、風船アニメーションを実行
・テキストの表示
・customer_informationにまとめた情報を、JSON形式で一括表示する
以上の流れで、入力フォームページを表示するコードを記述しました。
ではコードを実行します。
$ streamlit run app.py
ポート番号を確認して、EC2のセキュリティグループのインバウンドを許可します。
※Strealitを起動すると、ターミナルにURLが表示されます。そちらでポート番号を御確認ください。
その上でブラウザからURLにアクセスします。
http://[※EC2のパブリックIPv4アドレス]:[※ポート番号]
簡易的な入力フォームが生成されたはずです。
一旦フロントエンド側の作業を中断します。
今回入力されたデータをデータベース側に書き込む処理が必要になります。
5. DocumentDBの構築~接続テスト~MongoDBの挙動確認
AWS側の構築に戻ります。
今回使用するデータベースは、MongoDB互換のAWSサービスであるDocumentDBを使用します。
・Amazon Auroraに似たクラスタ構成
⇒コンピュート(クエリを実行するノード)と、ストレージ(データを入れる)を分離することで、個別に最適なスケールを行うことができる。
・コンピュート側
⇒コンピュート側では読み書き可能なプライマリインスタンスと、読み込みだけ可能なリードレプリカの構成となっている。
⇒リードレプリカは15台まで増やすことが可能で、読み込み性能を格段に向上させることができる。
⇒プライマリインスタンス障害時は、他のリードレプリカがマスタに昇格し、フェイルオーバーすることができる。フェイルオーバーが発生しても、常にマスタを参照するクラスタエンドポイントを用意している。
⇒クラスタエンドポイント/リーダーエンドポイントがあることで、アプリケーション側から見れば、宛先を変えずに読み書きを続けることができる。
・ストレージ側
⇒ストレージ側は3つのAZにまたがって6つコピーされる。データ障害に強い。
⇒ストレージの自動拡張にも対応。
では構築していきましょう。
DocumentDB用のセキュリティグループを作成
EC2からのインバウンド通信を許可します。
インバウンド通信許可(カスタムTCP、ポート:27017、ソース:EC2のセキュリティグループ)
サブネットグループ作成
- VPC: EC2と同じVPC
- サブネット: プライベートサブネットを複数指定
DocumentDBクラスターを作成
(AWSマネジメントコンソール上の操作) DocumentDB⇒クラスター⇒作成)
- クラスタ識別子: 任意
- インスタンスクラス: db.t3.medium
- インスタンス数: 1
- 認証用のマスターユーザー名/マスターパスワード: 任意
- VPC: EC2と同じVPC内に。
- サブネットグループ: 作成したサブネットグループを設定
- セキュリティグループ: 作成したセキュリティグループを設定
- ポート: 27017
- パラメータグループ: default
- その他 暗号化/バックアップなどの設定は任意でお願いします。
クラスター内に、プライマリインスタンスが1台起動しています。
今回はFastAPI~DocumentDB間の連携を検証するのが目的なので、インスタンスクラスやインスタンス数を最低限にしています。
読み込み性能の向上やフェイルオーバーを考える場合は、インスタンス数を増やすことをご検討ください。
EC2 ~ DocumentDB間の接続テスト
今回データベース操作はPyMongoを利用して行う予定ですが、一旦mongoコマンドでEC2~DocumentDB間の接続を試します。
接続方法については公式ドキュメントのステップ4以降で参照できます。
- mongoコマンドを利用できる状態にする
DocumentDBにmongoコマンドでアクセスするためには、mongoシェルをインストールする必要があります。
リポジトリファイルの作成
$ echo -e "[mongodb-org-4.0] \nname=MongoDB Repository\nbaseurl=https://repo.mongodb.org/yum/amazon/2013.03/mongodb-org/4.0/x86_64/\ngpgcheck=1 \nenabled=1 \ngpgkey=https://www.mongodb.org/static/pgp/server-4.0.asc" | sudo tee /etc/yum.repos.d/mongodb-org-4.0.repo
mongoシェルのインストール
$ sudo yum install -y mongodb-org-shell
・AmazonDocumentDBの公開鍵をダウンロード
DocumentDB⇒※作成したクラスターを指定⇒接続とセキュリティタブ
ここに接続の際に必要なコマンドが記載されています
$ cd fast-app
$ ※「認証に必要な Amazon DocumentDB 認証機関 (CA) 証明書を クラスター にダウンロードする」に記載のあるコマンドを実行ください
・mongoシェルでクラスターに接続する
接続とセキュリティタブを参照
$ ※「mongo シェルでこのクラスターに接続する」に記載のあるコマンドを実行ください。パスワードは、自身が設定したマスターパスワードにおきかえてください。
無事接続ができました。
mongoシェル実行
下記はデータベースの構造です。
DB
|-----------------
| - Collection
| - Document
| - Document
| - Document
| - Document
|
| - Collection
| - Document
| - Document
| - Document
| - Document
・DB(データベース)に、Collectionを作成。
・Collectionの中に、様々な型のDocument(オブジェクト)を挿入できます。
・オブジェクト挿入
object = { ID : 1, Name : "test-taro"};
ではDocumentDBに接続して、mongoシェルを実行してみましょう。
DB作成
#DB作成
> use dbtest
#DB一覧表示
> show dbs
「use DB名」 で、DB作成および移動を行う。
この時点でshow dbsを実行しても、DB一覧は表示されません。
コレクションが無いと、DB一覧に表示されません。
Collection作成
#コレクション作成
> db.createCollection('test1');
#DB一覧表示
> show dbs
dbtest 0.000GB
#コレクション一覧確認
> use dbtest
> show collections
test1
「db.createCollection(Collection名)」で、コレクション作成を行います。
「show collections」で、データベース内に作成されたコレクションを確認できます。
またこの時点で「show dbs」を実行すると、dbtestデータベースが表示されていることが分かります。
ドキュメント挿入
#test1コレクションに3つのドキュメントを挿入
> db.test1.insert( { ID:'1', Name:'test taro' });
> db.test1.insert( { ID:'1', Name:'test jiro' });
> db.test1.insert( { ID:'1', Name:'test saburo' });
#test1コレクション内のドキュメントを全件取得
> db.test1.find();
{ "_id" : ObjectId("627d1b3823a3bca6707fee93"), "ID" : "1", "Name" : "test taro" }
{ "_id" : ObjectId("627d1b4b23a3bca6707fee94"), "ID" : "1", "Name" : "test jiro" }
{ "_id" : ObjectId("627d1b5523a3bca6707fee95"), "ID" : "1", "Name" : "test saburo" }
#test1コレクション内の「Nameがtest taro」のドキュメントを取得
> db.test1.find({Name:'test taro'});
{ "_id" : ObjectId("627d1b3823a3bca6707fee93"), "ID" : "1", "Name" : "test taro" }
db.collection.insert()で、1つまたは複数のドキュメントをコレクションに挿入します。
挿入結果はfind()で確認ができます。コレクション内のドキュメントを全件取得したり、一部のドキュメントを取得できます。
間違えてIDをすべて1にしてしまいました。
IDを変更します。
ドキュメント更新
#ドキュメント更新
> db.test1.update({'Name': 'test jiro'},{$set: {ID:'2'}})`
> db.test1.update({'Name': 'test saburo'},{$set: {ID:'3', Name:'saburo'}})
#コレクション内のドキュメントを全件取得
> db.test1.find();
{ "_id" : ObjectId("627d1b3823a3bca6707fee93"), "ID" : "1", "Name" : "test taro" }
{ "_id" : ObjectId("627d1b4b23a3bca6707fee94"), "ID" : "2", "Name" : "test jiro" }
{ "_id" : ObjectId("627d1b5523a3bca6707fee95"), "ID" : "3", "Name" : "test saburo" }
db.collection.update()は、単一のドキュメントを更新します。
無事にIDの変更ができました。
では本セクションの最後にお片付けをしましょう。
コレクション削除とデータベース削除
#test1コレクションの削除
> db.test1.drop();
true
#データベースの削除
> use dbtest
> db.dropDatabase();
{ "ok" : 1, "operationTime" : Timestamp(1652368549, 1) }
ざっくりですが、MongoDBの操作を体験できました。
長くなってきたので、一旦このブログは終了とさせていただきます。
さいごに
次回はStreamlitで構築した入力フォームと、DocumentDB間を連携させていきます。
お疲れ様でした!!
Discussion