- 記事一覧 >
- ブログ記事
FastAPIのアプリをAzure Web AppsとAzure Static Web Appsにデプロイ
はじめに
前回記事「Python FastAPI React PostgreSQL のプロジェクトを動かして紐解いてみた」のアプリを Microsoft Azure にデプロイしてみました。
前回記事は、
バックエンド:Python FastAPI (on Docker)
フロントエンド:TypeScript React (on Docker)
DB:PostgreSQL (on Docker)
という構成です。
これを
バックエンド:Azure Web Apps
フロントエンド:Azure Static Web Apps
DB:Azure Database for PostgreSQL
にデプロイしましたので、その全手順を紹介していきます。
Dockerで動くシステムなので、コンテナデプロイや、Kubernetes も視野に入りますが、あえて、ソースコードをデプロイする手順です。
この方が料金が安くなるとか、実行速度が速いとかベストプラクティス的なことは一切考慮していません。
CI/CD、負荷分散や強固なセキュリティなど考慮していません。アプリが動けばゴールです。
本記事情報により何らかの問題が生じても、一切責任を負いません。
無料で押し通したかったですが...本記事の手順は、有料プランを利用します。
前々回記事「Alpine LinuxをインストールしてVS Code Remote SSHしてみた」
前回記事「Python FastAPI React PostgreSQLのプロジェクトを動かして紐解いてみた」の続きとします。
VS Code(SSH Remote)を利用していることが前提となります。
【ローカル環境】
Windows10 PRO x64
Visual Studio Code(VS Code) 1.68.1
VMware Workstation 16 Pro
alpine-virt-3.16.0-x86_64
Docker 20.10.17
【ローカル環境でのデプロイ先イメージ】
python:3.8
node:16 as build -> nginx:latest
postgres:12
ローカルの実装内容
詳しくは、前回記事「Python FastAPI React PostgreSQL のプロジェクトを動かして紐解いてみた」
をご確認ください。この状態の続きとします。
frontend、backend、PostgreSQL がおのおの Docker で起動して、動作確認済みとします。
なお、ローカル環境のデータは、引き継がないものとします。
Azure Web Apps
Web App 作成
バックエンドからデプロイします。
VS Code で、backend ディレクトリを開きます。
拡張機能の Azure App Service をインストールします。 Azure Tools の方は、全部入りで、そちらでも良いですが、今回、必要なものだけインストールしていきます。
左端の Azure アイコンをクリックして、 Sign in to Azure... をクリックします。
Web ブラウザでサインインします。
鍵アイコンのサブスクリプションを開いて、
App Services のところで、右クリック → Create New Web App... をクリックします。
ユニークなアプリ名を決めます。
ここでは、
otamesi-backend
とし、エンターキーを押します。
otamesi-backendは、この記事公開前に消します。
元々の Docker アプリの方が Python 3.8 だったため、Python 3.8 を選択します。
Free(F1)を選択します。
Web App : F1 プラン(無料)
項目 | 制限 |
---|---|
アプリ数 | 10 |
ストレージ | 1GB |
CPU 時間(5 分) | 3 分 |
CPU 時間 | 約 60 分 |
帯域幅 | 165 MB |
↑
今回用途では、十分なスペックですが、デプロイを繰り返すと、CPU 時間を超過して、QuotaExceeded
になって、何もできなくなり、手探り段階の場合、きついです。(5分くらいすると回復します。)
この後、Basic(B1)に変更する手順が出てきます。(無料プランでは、この記事の内容は実現しませんでした。)
構築中です。
こうなったら、成功です。
startup 設定
Web App 起動時のスタートアップスクリプトを作成し、設定します。
backend/startup.sh
を以下の内容で作成します。
python -m uvicorn --host 0.0.0.0 main:app
https://portal.azure.com/
へアクセスして、App Service アプリ名 otamesi-backend をクリック → 構成 → 全般設定
スタートアップ コマンド:startup.sh
とし、
保存 をクリックします。
スタートアップ コマンドのところに、startup.shの内容を直接書いても良いですが、推奨されていないようです。
Gunicorn 以外の Web サーバーを起動するには、サーバーを直接呼び出す代わりに python -m コマンドを使用します。
参考:https://docs.microsoft.com/ja-jp/azure/developer/python/tutorial-deploy-app-service-on-linux-04
Deploy
RESOURCES ツリーから otamesi-backend を選択します。
右クリックして、メニューから
Deploy to Web App...
を選択します。
監視 - ログ ストリーム
で見ると、KeyError: 'DATABASE_URL'
が出力されています。
これは、環境変数で、PostgreSQL の データベース接続先を指定してないからで、後で対処します。
とりあえず、ここまで行い、次の作業に入ります。
何回も接続を試みるため、停止しておきます。
PostgreSQL 作成
Azure Database for PostgreSQL にて、PostgreSQL サービスを作成します。
無料プランが無く、従量課金の有料プランになります。この手順程度では、請求は微々たるものですので、続けます。
カテゴリ - データベース から Azure Database for PostgreSQL を選択して、作成 をクリックします。
フレキシブル サーバー を 作成します。
リソース グループは、otamesi-backend のリソースグループを選択します。
リソースグループは、Web App - otamesi-backend の概要のところに表示されています。
サーバー名 を入力します。何でも良いと思いますが、 サーバー名は利用可能である必要があります。
の部分が × になっていると、変更が必要です。
ここでは、 postgresx
とします。
リージョン は、otamesi-backend と同じリージョンを選択します。
ここで、Web App と別のリージョンを指定すると、この後の接続手順のところで先に進めなくなります。
PostgreSQL バージョン は、12
とします。(ローカル環境のときは、v12 で動作確認したため。)
ワークロードの種類 は、開発
を選択します。
可用性ゾーン は、優先設定なし
とします。
高可用性 は、チェック無しとします。
管理者アカウントは、任意ですが、ローカル環境の時に合わせて、
管理者ユーザー名:postgres
パスワード:WhhKGoAfTQpIAFbULLQIEwHqdkDAdrlG-
とします。
パスワードは、要件を満たす必要が有り、ローカル環境の時から変更が必要でした。
右側(または最下部)に表示される見積もり総額は、あくまで月単位の見積もりなので、試してすぐに削除すれば少額になります。
この手順に従ったら、高額の請求が来たとかになっても、一切責任を負いません。
次: ネットワーク >
ボタンをクリックします。
ネットワーク接続:プライベート アクセス (VNET 統合)
を選択します。(これにより、PostgreSQL サーバーをインターネットに晒さず、内側に閉じ込めます。)
Virtual network は、(New) のものを選択し、新しく作成するものとします。(今回の場合、appsvc_linux_centralusvnet592
が自動作成されます。)
プライベート DNS ゾーン は、(New) のものを選択し、新しく作成するものとします。(今回の場合、postgresx.private.postgres.database.azure.com
が自動作成されます。)
確認および作成
ボタンをクリックします。
作成
ボタンをクリックします。
... 数分待ちます。 ...
postgresql.conf でおなじみの設定は、サーバー パラメーター
で変更できます。
VNET 統合
リージョン仮想ネットワーク統合により、Web App と PostgreSQL の通信ができるようにします。(PostgreSQL をプライベートネットワークに閉じ込める方法を採ったため、この作業が必要です。)
ホーム → リソースグループ → appsvc_linux_centralusvnet592 仮想ネットワーク(PostgreSQL サービス作成のときに新規作成した仮想ネットワーク)を選択します。
アドレス空間 → その他のアドレス範囲の追加
のところに、現在設定されている範囲以外を追加します。(今回の例では、10.0.5.0/24
とします。)
この作業の意味は、この直後のサブネット追加のために追加できるアドレス範囲を広げています。
次にサブネットを追加します。
サブネット → +サブネット をクリックします。
名前:webappsubnet
(任意です。)
サブネット アドレス範囲:10.0.5.0/24
(任意です。)
とし、他はそのままで、保存
をクリックします。
サブネットを作成したら、App Service の otamesi-backend へ移動します。
ネットワーク → 送信トラフィック の VNET 統合
をクリックします...が、有効になっていません。
有料プランに変更が必要です。(無料でもできるという情報がありましたが、どういうことでしょうか...。できませんでした。)
App Service プランの変更
→ 価格レベル Free(F1)
のところをクリックします。
B1 を選択して適用します。
表示される見積もり総額は、あくまで月単位の見積もりなので、試してすぐに削除すれば少額になります。
この手順に従ったら、高額の請求が来たとかになっても、一切責任を負いません。
ネットワーク → 送信トラフィック の VNET 統合
をクリックします。
VNet の追加
をクリックします。
仮想ネットワーク:appsvc_linux_centralusvnet592 仮想ネットワーク(PostgreSQL サービス作成のときに新規作成した仮想ネットワーク)を選択します。
サブネット:既存のものを選択 とし、先ほど作成した webappsubnet
を選択します。
以下のような謎のエラーが出ましたが、もう一度やったら、うまくいきました。
アプリに VNet を構成
An operation on the Virtual Network has failed. Details: {
"error": {
"code": "ReferencedResourceNotProvisioned",
"message": "Cannot proceed with operation because resource /subscriptions/*****/resourceGroups/appsvc_linux_centralus/providers/Microsoft.Network/virtualNetworks/appsvc_linux_centralusvnet592/subnets/webappsubnet used by resource webappsubnet is not in Succeeded state. Resource is in Updating state and the last operation that updated/is updating the resource is PutSubnetOperation.",
"details": []
OK
をクリックします。
ルートすべて について、
[ルートすべて] が無効:アプリはプライベート トラフィックのみを仮想ネットワークにルーティングします。(デフォルトゲートウェイが通常の NIC)
[ルートすべて] が有効:すべての送信アプリ トラフィックを仮想ネットワークにルーティングします。(デフォルトゲートウェイが VNet 統合側の NIC)
ということですが、今回の場合、どちらでも良いため、このまま進めます。
環境変数設定
次に、
構成 → アプリケーション設定 → 新しいアプリケーション設定
で環境変数を設定しておきます。
ここで設定する値は、ローカル環境の時に .env
に設定していた PostgreSQL 接続先等の設定値です。ローカル環境の時に .env
は、docker-compose に読み取られていましたが、今回の Web App は、新しいアプリケーション設定に設定します。
# POSTGRES_DB and POSTGRES_PASSWORD are used by the postgres docker image to initialse the db
POSTGRES_PASSWORD=WhhKGoAfTQpIAFbULLQIEwHqdkDAdrlG
POSTGRES_DB=app
DATABASE_URL=postgresql://postgres:WhhKGoAfTQpIAFbULLQIEwHqdkDAdrlG@postgres/app
TEST_DATABASE_URL=postgresql://postgres:WhhKGoAfTQpIAFbULLQIEwHqdkDAdrlG@postgres/apptest
SECRET_KEY=KhZFiewUoaBtMsfOyhdPGxfcjjPjkFOq
#BACKEND_CORS_ORIGINS='["http://localhost:3000","http://127.0.0.1:3000"]'
BACKEND_CORS_ORIGINS='["http://192.168.11.9:3000","http://127.0.0.1:3000"]'
Web App では、以下の設定をします。
POSTGRES_PASSWORD=WhhKGoAfTQpIAFbULLQIEwHqdkDAdrlG-
POSTGRES_DB=app
DATABASE_URL=postgresql://postgres:WhhKGoAfTQpIAFbULLQIEwHqdkDAdrlG-@postgresx.postgres.database.azure.com/app
TEST_DATABASE_URL=postgresql://postgres:WhhKGoAfTQpIAFbULLQIEwHqdkDAdrlG-@postgresx.postgres.database.azure.com/apptest
SECRET_KEY=KhZFiewUoaBtMsfOyhdPGxfcjjPjkFOq
BACKEND_CORS_ORIGINS は、CORS 対処の設定ですが、一旦設定しません。(この後、別に対処します。)
postgres://
ではなく、postgresql://
にしないと、sqlalchemyでエラー出力されます。postgres://
でもpostgresql://
でもどちらでも良かったのが SQLAlchemy 1.4 からpostgresql://
だけしか許されなくなりました。
なお、ついでに、以下のタイムゾーンの設定もしておきます。
WEBSITE_TIME_ZONE=Asia/Tokyo
保存
をクリックします。
DB 作成 migrate
今の状況で、起動すると、DB の app が無いため、エラーになります。
DB 作成 と マイグレーション(テーブル作成)が必要です。
PostgreSQL がプライベートネットワーク内にあるため、プライベートネットワークから createdb と マイグレーション が必要です。
プライベートネットワーク接続可能な踏み台サーバー的な VM を立てて psql
で create database
します。(Web App の方には、アプリの起動に失敗しているため、SSH に接続できません。)
仮想ネットワーク appsvc_linux_centralusvnet592
に
名前:vmnet
(任意です。)
サブネット アドレス範囲:10.0.7.0/24
(任意です。)
を事前作成します。(上記の webappsubnet
と同じ手順ですので、説明は省略します。)
仮想マシン → 作成 をクリックします。
地域 は、Web App と同じリージョンに、サイズ は、一番安いものを選択します。
認証の種類 は SSH 公開キー
とし、
ユーザー名:azureuser
SSH 公開キーのソース:新しいキーの組の生成
キーの組名:temp_key
パブリック受信ポート:選択したポートを許可する
受信ポートを選択:SSH (22)
とします。
秘密鍵が無いと接続できませんが、SSH (22) ポートがインターネットに公開されますので、推奨されません。このVMは作業後削除します。
次: ディスク >
をクリックします。
OS ディスクの種類:Standard HDD とし、他はそのままにします。(何でも良いので、最遅ディスク選択)
次: ネットワーク >
をクリックします。
仮想ネットワークとサブネットは、事前作成しておいたものを選択します。
仮想ネットワーク:appsvc_linux_centralusvnet592
サブネット:vmnet (10.0.7.0/24)
確認および作成
をクリックします。
作成
をクリックします。
秘密キーのダウンロードとリソースの作成
をクリックして、秘密鍵をダウンロードします。
作成できたら、
接続
で SSH コマンドを確認します。
ここでは、ssh -i <秘密キーのパス> azureuser@20.9.xx.xx
とします。
ネット接続できる端末から ssh -i temp_key.pem azureuser@20.9.xx.xx
として、接続します。temp_key.pem
は、先ほどダウンロードした秘密鍵です。
ログイン出来たら、以下のコマンドでDBを作成します。
$ sudo apt install postgresql-client
$ psql "host=postgresx.postgres.database.azure.com port=5432 dbname=postgres user=postgres password=WhhKGoAfTQpIAFbULLQIEwHqdkDAdrlG-"
postgres=> CREATE DATABASE app;
CREATE DATABASE
postgres=> CREATE DATABASE apptest;
CREATE DATABASE
postgres=> \q
$ exit
createdb したらすぐに VM を削除します。
startup.sh に alembic upgrade head
を追加して、
alembic upgrade head
python -m uvicorn --host 0.0.0.0 main:app
とし、再び、デプロイします。
変な表示が出ますが...起動しているから、ヨシ!
Azure Static Web Apps
frontend を Azure Static Web Apps にデプロイします。
前回記事「Python FastAPI React PostgreSQL のプロジェクトを動かして紐解いてみた」で触れたように、API が https://[自ホスト名]
になるため、可変になるようにソースコードを書き換えておきます。
//return `https://${document.location.host}`;
return process.env.REACT_APP_API_BASE || `https://${document.location.host}`;
.env.production
を作成して、
REACT_APP_API_BASE=https://otamesi-backend.azurewebsites.net
を設定しておきます。 URL は、先ほど準備した、バックエンド側 API の URL です。
他に、ビルドしたときに
src/pages/ProfileEdit.tsx
Line 84:5: React Hook useCallback has a missing dependency: 'redirect'. Either include it or remove the dependency array react-hooks/exhaustive-deps
とエラーになるため、src/pages/ProfileEdit.tsx
を直しておきます。
},
[notify, refreshProfile]
);
↓
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[notify, refreshProfile]
);
GitHub をデプロイに使います。
git、GitHub の説明になってしまうため、詳細は、省略します。ここでは、otamesi-frontend
という Private リポジトリに frontend のソースコードを GitHub に push 済みとします。
静的 Web アプリ を作成します。
名前 の部分は、URL に反映されません。URL は、自動的に決まります。
ホスティング プラン は、無料の Free:趣味または個人的なプロジェクト用
を選択します。
Azure Functions とステージングの詳細 は、おそらく、今回の構成の場合、何でも良く、Central US
とします。
GitHub アカウントでサインイン
をクリックします。 → GitHub サインイン画面が現れて、サインインします。
リポジトリを選択できるようになるため、frontend のソースコードがあるリポジトリを選択します。
ビルドのプリセット は、frontend が React で作成されているため、React
とします。確認および作成
をクリックします。
作成
をクリックします。
静的 Web アプリ 構築完了直後、GitHub Actions が動き出して、コンテンツを作成します。(作成
をクリック直後では、まだアクセスしても何もない状態です。)
ビルドが終わったら、アクセスしてみます。
できました!
CORS
ただ、この段階では、backend, frontend で別の URL のため、CORS エラーになります。
前回記事「Python FastAPI React PostgreSQL のプロジェクトを動かして紐解いてみた」のときは、ソースコード側の .env
書き換えで対処しましたが、Azure Web Apps(API側)の設定で対処します。
CORS をクリックします。
許可される元のドメイン:https://orange-coast-083482f10.1.azurestaticapps.net
として、保存
をクリックします。
ユーザー登録できて、ログインできるようになりました!
ただ、Items をクリックすると、
The Content-Range header is missing in the HTTP Response. The simple REST data provider expects responses for lists of resources to contain this header with the total number of results to build the pagination. If you are using CORS, did you declare Content-Range in the Access-Control-Expose-Headers header?
のエラー表示になりました。
これは、CORS で Content-Range
ヘッダの存在を許していないからです。
Access-Control-Expose-Headers
ヘッダに Content-Range が有れば許されるのですが、そうなっていません。
結局、プログラムで対処しないといけないです。(上記で設定した CORS は削除。)
以下のように、BACKEND_CORS_ORIGINS
が設定されている場合、Access-Control-Expose-Headers ヘッダに Content-Range が有る状態になるため、BACKEND_CORS_ORIGINS
を設定して、乗り切りました。
def setup_cors_middleware(app):
if settings.BACKEND_CORS_ORIGINS:
app.add_middleware(
CORSMiddleware,
allow_origins=[str(origin) for origin in settings.BACKEND_CORS_ORIGINS],
allow_credentials=True,
allow_methods=["*"],
expose_headers=["Content-Range", "Range"],
allow_headers=["Authorization", "Range", "Content-Range"],
)
OK!!
その他、宣伝、誹謗中傷等、当方が不適切と判断した書き込みは、理由の如何を問わず、投稿者に断りなく削除します。
書き込み内容について、一切の責任を負いません。
このコメント機能は、予告無く廃止する可能性があります。ご了承ください。
コメントの削除をご依頼の場合はTwitterのDM等でご連絡ください。