LoginSignup
6
4

More than 1 year has passed since last update.

LambdaでFastAPI + NuxtJSなアプリ環境を構築してみる

Last updated at Posted at 2022-03-26

気軽にWebアプリが作れて、ローカルでの検証もしやすくて、本番デプロイも簡単。
そんな構成を目指して、FastAPIとNuxtJS(SPA)がLambda上で動作するアプリ環境をServerlessFrameworkで構築してみました。

structure.png

ソースコードはこちら

前提条件

ServerlessFrameworkを使います。
nodejsとpython3.8とdockerをインストールしておいてください。

ServerlessFrameworkインストール & プロジェクト作成

ServerlessFrameworkをグローバルにインストールして、プロジェクトを新規作成します。

# ServerlessFrameworkをグローバルにインストール
npm install -g serverless
sls --version

# プロジェクト作成
sls create --template aws-python3 --name sample-app --path sample-app
cd sample-app

# pythonのライブラリをlayerとしてまとめるためのプラグインをインストール
sls plugin install -n serverless-python-requirements
sample-app/
| .gitignore
| handler.py
| package-lock.json
| package.json
| serverless.yml

serverless.ymlにデプロイ先リージョンと、serverless-python-requirements プラグインを利用する設定を追記しておきましょう。

serverless.yml
provider:
  region: ap-northeast-1

plugins:
  - serverless-python-requirements

Lambda上でFastAPIを動作させる

APIGatewayの設定

APIGatewayでLambdaプロキシ統合を設定していきます。
ルーティングはLambda内のFastAPIが担当しますので、APIGatewayはすべてのリクエストを同じLambdaに渡せばよいです。

binaryMediaTypes はAPIGatewayがバイナリファイルを返却するために必要な設定です。
これがないとAPIGatewayは画像ファイルなどを返却できません。(この設定を知らずに4時間くらいハマりました、、、)

serverless.yml
provider:
  # ... 略 ...
  apiGateway:
    binaryMediaTypes:
      - '*/*'

functions:
  hello:
    handler: handler.hello
    events:
      - http:
          path: /
          method: ANY
      - http:
          path: /{path+}
          method: ANY

Layerの設定

Lambdaが巨大化するとデプロイに時間がかかるので、必要なライブラリはLayerにまとめていきます。
serverless-python-requirements プラグインを利用すると、 serverless.yml と同階層にある requirements.txt をLayerとしてデプロイできます。

今回必要なライブラリは fastapi と mangum の2つです。

requirements.txt
fastapi[all]
mangum

serverless.yml にLayerをビルドするための下記設定を追加します。
Ref: PythonRequirementsLambdaLayer は固定値です。serverless-python-requirements プラグインによって作成されたLayerを参照します。

serverless.yml
functions:
  hello:
    # ... 略 ...
    layers: # 作成したLayerをLambdaから利用するための設定
      - Ref: PythonRequirementsLambdaLayer
custom:
  pythonRequirements:
    dockerizePip: non-linux # deploy時にpythonライブラリのinstall & buildをdockerで行う設定
    layer: true # レイヤーを作成する

FastAPIアプリの実装

デフォルトの handler.py を削除して、FastAPIのアプリを実装しましょう。

rm handler.py
mkdir -p api
touch api/main.py api/__init__.py

Lambda上でFastAPIを動作させるには Mangum というライブラリを利用します。
MangumはAPIGatewayのイベントをLambda上のASGIアプリケーションが扱える形式に変換するアダプタとして機能します。
ちなみに、APIGatewayから渡されたイベントは Request.scope['aws.event'] から取得できます。

とりあえず今回は簡単に /api/hello/api/event というAPIを作りたいと思います。

api/main.py
import os
from fastapi import FastAPI, APIRouter
from mangum import Mangum
from starlette.requests import Request
from fastapi.middleware.cors import CORSMiddleware

router = APIRouter()
@router.get("/hello")
def hello():
    return {"Hello": "World"}

@router.get("/event")
def event(request: Request):
    return {
        "event": request.scope["aws.event"],
    }

app = FastAPI()
app.include_router(router, prefix="/api")

# CORS: https://fastapi.tiangolo.com/tutorial/cors/
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

handler = Mangum(app) # このhandlerをLambdaのハンドラとして指定します。

Lambdaのハンドラに app/main.pyhandler = Mangum(app) を指定し、デプロイパッケージには api/ のみを含めるように設定します。

serverless.yml
functions:
  hello:
    handler: api.main.handler
    # ... 略 ...

package:
  patterns:
    - '!**' # すべてのファイルをexclude
    - 'api/**' # fastapiのソースコードをinclude

一度デプロイ

これで一度デプロイしてみましょう

sls deploy --stage dev

こんな感じの構成で、/dev/api/hello /dev/api/event へのリクエストに成功すればOKです。

https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/api/hello
https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/api/event

スクリーンショット 2022-03-26 16.44.56 (1).png

NuxtJSの導入

NuxtJSはSPA方式を利用します。
npm run generate で静的ファイルを生成して、FastAPIの静的ファイルサーバー機能で提供します。

NuxtJSのプロジェクト作成

front ディレクトリに sample-app というプロジェクトを作成します。
ssr=false target='static' なSPA方式とします。

npm init nuxt-app front

create-nuxt-app v4.0.0
✨  Generating Nuxt.js project in front
? Project name: sample-app
? Programming language: JavaScript
? Package manager: Npm
? UI framework: Vuetify.js
? Nuxt.js modules: Axios - Promise based HTTP client
? Linting tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Testing framework: Jest
? Rendering mode: Single Page App
? Deployment target: Static (Static/Jamstack hosting)
? Development tools: jsconfig.json (Recommended for VS Code if you're not using typescript)
? Continuous integration: None
? Version control system: Git
# nuxtプロジェクトの.gitを削除します。
rm -rf front/.git 

NuxtJSプロジェクトの設定

ビルド結果の出力先を変更

パッケージのビルド先を front/dist から front_dist に変更します。

front/nuxt.config.js
import colors from 'vuetify/es5/util/colors'

export default {
  // ... 略 ...

  // npm run generate で生成されるビルド済みファイルの出力先ディレクトリの指定
  generate: {
    dir: "../front_dist"
  },
}

NuxtJSでビルドした静的ファイルをLambdaのパッケージに含めるため、patternsfront_dist ディレクトリを追加します。

serverless.yml
package:
  patterns:
    - '!**' # すべてのファイルをexclude
    - 'api/**' # fastapiのソースコードをinclude
    - 'front_dist/**' # nuxtのビルド結果をinclude

ベースURIの設定

APIGatewayを利用するとURIの先頭にステージ名がついてしまいます。
※ 例えば sls deploy --stage dev でデプロイすると /api/hello/dev/api/hello となります。
この仕様、本気でいらないと思うのですが、どうしようもないのでNuxtJS側にベースURIを設定します。

今回は ビルド時にAPI_BASE_PATH という環境変数を定義することで、ベースURIを設定できるようにします。

front/nuxt.config.js
import colors from 'vuetify/es5/util/colors'

export default {
  // ... 略 ...
  head: {
    // ... 略 ...
    link: [
      // ファビコンのパスの先頭にAPI_BASE_PATHを設定します。
      { rel: 'icon', type: 'image/x-icon', href: (process.env.API_BASE_PATH ?? "") + '/favicon.ico' }
    ]
  },

  // ... 略 ...

  axios: {
    // axiosによるリクエスト送信時にベースURIが付与されます。
    // API_BASE_PATHが存在しない場合はローカル開発サーバーとみなして、//127.0.0.1:8000/をベースURIとします。
    baseURL: process.env.API_BASE_PATH ?? "//127.0.0.1:8000",
  },

  // ... 略 ...

  router: {
    // 生成されるリンクの先頭にベースURIが付与され、HTMLにbaseタグ( `<base href="xxxx">` ) が追加されます。
    // https://nuxtjs.org/docs/configuration-glossary/configuration-router/#base
    base: process.env.API_BASE_PATH ?? ""
  },
}

baseタグは相対パスで記述されたURIの基準となるので、絶対パスを相対パスに書き換えていきます。

/v.png -> v.png

front/pages/inspire.vue
<template>
  <v-row>
    <v-col class="text-center">
      <img
        src="v.png"
        alt="Vuetify.js"
        class="mb-5"
      >
      <!-- 略 -->

/vuetify-logo.svg -> vuetify-logo.svg

front/components/VuetifyLogo.vue
<template>
  <img
    class="vuetify-logo"
    alt="Vuetify Logo"
    src="vuetify-logo.svg"
  >
  <!-- 略 -->

APIをコールするページの作成

ボタンをクリックすると /api/hello APIをコールする簡単なサンプルを実装してみます。

front/pages/hello.vue
<template>
  <div>
    <div>
      <v-btn @click="hello">HELLO</v-btn>
    </div>
    <h1>{{ message }}</h1>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: ""
    }
  },
  methods: {
    hello() {
      let data = this.$data;
      this.$axios.get("/api/hello")
        .then(res => {
          data.message = res.data["Hello"];
        })
        .catch((e) => {
          console.error(e);
        })
    }
  }
}
</script>

FastAPIでfront_dist配下を静的ファイルとして返却する

NuxtJSのビルド結果の静的ファイルをFastAPIの静的ファイルサーバー機能で提供できるようにします。

FastAPI側でもベースURIの設定を行うため、 API_BASE_PATH を環境変数としてLambdaに渡します。

serverless.yml
provider:
  # ... 略 ...
  environment:
    API_BASE_PATH: ${env:API_BASE_PATH, ""}

FastAPIの初期化時に引数 root_pathAPI_BASE_PATH を設定し、 app.mountfront_dist ディレクトリを / にマウントします。
こうすることで、ブラウザで / にアクセスしたときに front_dist/index.html が参照されるようになります。

api/main.py
# ... 略 ...

# 追加: StaticFilesインポート
from fastapi.staticfiles import StaticFiles

# ... 略 ...

# 追加: root_pathにAPI_BASE_PATHを設定する
app = FastAPI(root_path=os.getenv(f"API_BASE_PATH", ""))
app.include_router(router, prefix="/api")

# 追加: front_distディレクトリを "/" にマウント
# StaticFilesのオプションはこちらを参照:  https://www.starlette.io/staticfiles/
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
app.mount("/", StaticFiles(directory=f"{PROJECT_ROOT}/front_dist", html=True), name="front")

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

handler = Mangum(app)

デプロイ

export API_BASE_PATH="/dev"

# 環境変数でベースURIを定義してビルド
(cd front && npm run generate)

# デプロイ
sls deploy --stage dev

動作確認

https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/ にアクセスするとNuxtのWelcomeページが表示されます。

スクリーンショット 2022-03-26 19.08.35.png

https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello にアクセスして「HELLO」ボタンをクリックすると「World」が表示されます。

スクリーンショット 2022-03-26 22.16.52.png

おまけ: ローカルで開発サーバーを起動する

ローカルで開発サーバーを起動する場合は、FastAPIとNuxtJSの開発サーバーをそれぞれ起動します。

FastAPIの開発サーバー起動

export API_BASE_PATH=""

# pythonパッケージをローカルにインストール
python3.8 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt 

# FastAPI開発サーバー起動
uvicorn api.main:app --reload

http://127.0.0.1:8000/ にアクセス

NuxtJSの開発サーバー起動

export API_BASE_PATH=""
cd front

# パッケージをローカルにインストール
npm install

# NuxtJSの開発サーバー起動
npm run dev

http://127.0.0.1:3000/ にアクセス

参考

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4