1. 記事一覧 >
  2. ブログ記事
Azure
category logo

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
にデプロイしましたので、その全手順を紹介していきます。

on Azure の図


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 で起動して、動作確認済みとします。


なお、ローカル環境のデータは、引き継がないものとします。


fastapi-starter 構成図


Azure Web Apps

Azure Web Apps 図

Web App 作成

バックエンドからデプロイします。
VS Code で、backend ディレクトリを開きます。

VS Code で、backend ディレクトリを開く


拡張機能の Azure App Service をインストールします。 Azure Tools の方は、全部入りで、そちらでも良いですが、今回、必要なものだけインストールしていきます。

拡張機能の Azure App Service をインストール


左端の Azure アイコンをクリックして、 Sign in to Azure... をクリックします。
Web ブラウザでサインインします。

Azure アイコンをクリックして、 Sign in to Azure


鍵アイコンのサブスクリプションを開いて、
App Services のところで、右クリック → Create New Web App... をクリックします。

App Services のところで、右クリック → Create New Web App


ユニークなアプリ名を決めます。
ここでは、
otamesi-backend
とし、エンターキーを押します。

otamesi-backendは、この記事公開前に消します。

アプリ名 otamesi-backend


元々の Docker アプリの方が Python 3.8 だったため、Python 3.8 を選択します。

Python 3.8 を選択


Free(F1)を選択します。

Free(F1)を選択

Web App : F1 プラン(無料)

項目制限
アプリ数10
ストレージ1GB
CPU 時間(5 分)3 分
CPU 時間約 60 分
帯域幅165 MB


今回用途では、十分なスペックですが、デプロイを繰り返すと、CPU 時間を超過して、QuotaExceeded になって、何もできなくなり、手探り段階の場合、きついです。(5分くらいすると回復します。)

QuotaExceeded

この後、Basic(B1)に変更する手順が出てきます。(無料プランでは、この記事の内容は実現しませんでした。)


構築中です。

Web App 構築中


こうなったら、成功です。

Web App 構築成功


startup 設定

Web App 起動時のスタートアップスクリプトを作成し、設定します。


backend/startup.sh
を以下の内容で作成します。

python -m uvicorn --host 0.0.0.0 main:app

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...
を選択します。

Deploy to Web App


監視 - ログ ストリーム で見ると、KeyError: 'DATABASE_URL' が出力されています。 これは、環境変数で、PostgreSQL の データベース接続先を指定してないからで、後で対処します。 とりあえず、ここまで行い、次の作業に入ります。

KeyError DATABASE_URL


何回も接続を試みるため、停止しておきます。

Web App 停止


PostgreSQL 作成

Azure Database for PostgreSQL 図

Azure Database for PostgreSQL にて、PostgreSQL サービスを作成します。

無料プランが無く、従量課金の有料プランになります。この手順程度では、請求は微々たるものですので、続けます。


カテゴリ - データベース から Azure Database for PostgreSQL を選択して、作成 をクリックします。

Azure Database for PostgreSQL を選択


フレキシブル サーバー を 作成します。

フレキシブル サーバー を 作成


リソース グループは、otamesi-backend のリソースグループを選択します。
リソースグループは、Web App - otamesi-backend の概要のところに表示されています。

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 でおなじみの設定は、サーバー パラメーター で変更できます。 postgresql.conf でおなじみの設定


接続文字列 のところで各場面での接続方法が確認できます。 各場面での接続方法


VNET 統合

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 統合 をクリックします...が、有効になっていません。
有料プランに変更が必要です。(無料でもできるという情報がありましたが、どういうことでしょうか...。できませんでした。)

VNET 統合 無効


App Service プランの変更 →  価格レベル Free(F1) のところをクリックします。
B1 を選択して適用します。

App Service プランの変更

表示される見積もり総額は、あくまで月単位の見積もりなので、試してすぐに削除すれば少額になります。

この手順に従ったら、高額の請求が来たとかになっても、一切責任を負いません。


ネットワーク → 送信トラフィック の VNET 統合 をクリックします。

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 をクリックします。

VNet の追加


ルートすべて

ルートすべて について、
[ルートすべて] が無効:アプリはプライベート トラフィックのみを仮想ネットワークにルーティングします。(デフォルトゲートウェイが通常の NIC)
[ルートすべて] が有効:すべての送信アプリ トラフィックを仮想ネットワークにルーティングします。(デフォルトゲートウェイが VNet 統合側の NIC)
ということですが、今回の場合、どちらでも良いため、このまま進めます。


環境変数設定

次に、
構成 → アプリケーション設定 → 新しいアプリケーション設定
で環境変数を設定しておきます。


ここで設定する値は、ローカル環境の時に .env に設定していた PostgreSQL 接続先等の設定値です。ローカル環境の時に .env は、docker-compose に読み取られていましたが、今回の Web App は、新しいアプリケーション設定に設定します。

ローカル環境の.env
# 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 を立てて psqlcreate database します。(Web App の方には、アプリの起動に失敗しているため、SSH に接続できません。


VMを立ててVNetにアクセス 図


仮想ネットワーク 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 は、先ほどダウンロードした秘密鍵です。

ssh
Git Bash を使っています。


ログイン出来たら、以下のコマンドで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 を削除します。

createdb したらすぐに VM を削除


startup.sh に alembic upgrade head を追加して、

alembic upgrade head
python -m uvicorn --host 0.0.0.0 main:app

とし、再び、デプロイします。

再び、デプロイ


再び、デプロイ URL


再び、デプロイ 表示


変な表示が出ますが...起動しているから、ヨシ!


Azure Static Web Apps

Azure Static Web Apps 図

frontend を Azure Static Web Apps にデプロイします。


前回記事「Python FastAPI React PostgreSQL のプロジェクトを動かして紐解いてみた」で触れたように、API が https://[自ホスト名] になるため、可変になるようにソースコードを書き換えておきます。

src/providers/env.ts


src/providers/env.ts
    //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 です。

.env.production


他に、ビルドしたときに
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 を直しておきます。

src/pages/ProfileEdit.tsx
    },
    [notify, refreshProfile]
  );

src/pages/ProfileEdit.tsx
    },
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [notify, refreshProfile]
  );

GitHub をデプロイに使います。
git、GitHub の説明になってしまうため、詳細は、省略します。ここでは、otamesi-frontend という Private リポジトリに frontend のソースコードを GitHub に push 済みとします。


静的 Web アプリ を作成します。

静的 Web アプリ を作成

名前 の部分は、URL に反映されません。URL は、自動的に決まります。


ホスティング プラン は、無料の Free:趣味または個人的なプロジェクト用 を選択します。


Azure Functions とステージングの詳細 は、おそらく、今回の構成の場合、何でも良く、Central US とします。

GitHub アカウントでサインイン をクリックします。 → GitHub サインイン画面が現れて、サインインします。

GitHub アカウントでサインイン


リポジトリを選択できるようになるため、frontend のソースコードがあるリポジトリを選択します。
ビルドのプリセット は、frontend が React で作成されているため、React とします。
確認および作成 をクリックします。

確認および作成


作成 をクリックします。

作成


静的 Web アプリ 構築完了直後、GitHub Actions が動き出して、コンテンツを作成します。(作成 をクリック直後では、まだアクセスしても何もない状態です。)

GitHub Actions が動き出して、コンテンツを作成


GitHub Actions 処理中


ビルドが終わったら、アクセスしてみます。

ビルドが終わったら、アクセス


アクセス結果


できました!

CORS

ただ、この段階では、backend, frontend で別の URL のため、CORS エラーになります。

CORS エラー


前回記事「Python FastAPI React PostgreSQL のプロジェクトを動かして紐解いてみた」のときは、ソースコード側の .env 書き換えで対処しましたが、Azure Web Apps(API側)の設定で対処します。


CORS をクリックします。

許可される元のドメインhttps://orange-coast-083482f10.1.azurestaticapps.net
として、保存 をクリックします。

CORS をクリック


ユーザー登録できて、ログイン

ユーザー登録できて、ログインできるようになりました!


ただ、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 を設定して、乗り切りました。

backend/app/factory.py
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"],
        )

BACKEND_CORS_ORIGINS を設定
値の書き方に注意


BACKEND_CORS_ORIGINS を設定 保存 再起動


Items登録成功


OK!!

loading...