Sansan Builders Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

【Techの道も一歩から】第39回「Google App Engine で Python による WebAPI を動かす」

f:id:kanjirz50:20190104142720j:plain
ヘッダ画像

こんにちは。 DSOC R&D グループの高橋寛治です。

最近は Google App Engine(App Engine) を用いて WebAPI としてアルゴリズムを提供できるよう開発中です。

App Engine を触ったのは初めてだったため、備忘録として App Engine の概要や環境構築からデプロイまで書きたいと思います。 App Engine の説明や設定の考え方が主となります。

App Engine とは

Webアプリケーションの提供のためのフルマネージド型のサーバーレスプラットフォームです。

コードを用意して App Engine 上に反映すれば、GCP 上で Webアプリケーションが稼働し利用可能になっているというイメージです。

動作環境には、スタンダード環境とフレキシブル環境があります。 それぞれ利点が異なるため、公式の比較を見ながら検討すると良いと思います。

様々な言語やフレームワークが利用可能です。 スタンダード環境の Python だと、2021年5月現在は Python 3.7, 3.8, 3.9 がサポートされています。

今回作るもの

App Engine 上で Python 3.8 ランタイムで動作する WebAPI としてアルゴリズムを提供することを目指します。 また、Linux マシン上で開発できる体制を整えます。

App Engineの環境は、スタンダード環境を用います。 スタンダード環境では、独自の Docker コンテナを利用できないという制約はありますが、その反面スケーリングやコスト面で利点が多いです*1。 コスト面の考慮が気軽になることとスタンダード環境で作れる範囲の内容であることから、スタンダード環境を選んでいます。

開発時のローカル環境としては、App Engine と同様の環境をコンテナで作成し、Python パッケージは Poetry で管理します。 App Engine へのデプロイ時には、App Engineのデプロイツールが要求する requirements.txt を出力しデプロイ作業を行います。

開発環境を準備

ディレクトリ構成です。 説明に必要なもののみ示します。

package_namexxx といった文字列は実際動かす際には、適宜設定の必要があります。

.
├── Dockerfile
├── app.xxx.yaml
├── deploy.sh
├── pyproject.toml
├── src/package_name
└── tests

開発時における各ファイルの狙いを紹介します。

Dockerfile

動作確認を行うためのコンテナの定義です。 Python 3.8 をベースイメージとし、Poetry を用いて Python 環境を構築します。 Poetry のレポジトリのディスカッションで書き込まれているものを利用します。 こちらはPoetry の設定および、マルチステージビルドによるローカル開発環境とローカルテスト環境をコンテナで構築します。

app.xxx.yaml

ステージングやプロダクションのように環境ごとに App Engine の設定ファイルを用意します(xxxは環境名を想定)。 設定ファイルでは、Webアプリケーションの起動コマンドやサービス名、環境変数などを記述します。

Python の WebAPIフレームワークは FastAPI を利用しているため、ASGI サーバーを指定しています。

インスタンスクラスや gunicorn のワーカ数は目的に合わせて調整します。

runtime: python38
instance_class: F2
service: service-name
entrypoint: gunicorn -w 1 -k uvicorn.workers.UvicornWorker package_name.module_name:app

env_variables:
  ENV_VAR_1: "XXXX"

handlers:
  - url: /.*
    script: auto

deploy.sh

App Engineへのデプロイスクリプトです。

今回はデプロイスクリプトを用いて、デプロイを行います。 Poetryのパッケージ情報から、App Engineのデプロイに必要な requirements.txt を作成します。 その後、App Engine設定ファイルを元にデプロイを行います。 なお、デプロイ時は、app_engine ディレクトリ(デプロイ用ディレクトリ)を作成し、その中でデプロイパッケージを作ります*2

#!/bin/bash
set -eu

DEPLOY_ENV="xxx"

SCRIPT_DIR=$(cd $(dirname $0); pwd)
APP_ENGINE_DIR="app_engine"

if [ -e $APP_ENGINE_DIR ]; then
    rm -r $APP_ENGINE_DIR
fi
mkdir -p $APP_ENGINE_DIR

# gcloud deploy ではハッシュがあるとうまくいかないので、オプションで指定
poetry export -f requirements.txt --without-hashes --output $APP_ENGINE_DIR/requirements.txt

APP_YAML="app.$DEPLOY_ENV.yaml"
ln -s $SCRIPT_DIR/$APP_YAML $APP_ENGINE_DIR/$APP_YAML
ln -s $SCRIPT_DIR/src/package_name$APP_ENGINE_DIR/

# デプロイ
cd $SCRIPT_DIR/$APP_ENGINE_DIR
gcloud app deploy $APP_YAML --project project-name

手動で運用するgitignore に一時ディレクトリ名を追加しておくと、操作ミスが減らせるかと思います。

また、デプロイ先の環境を引数により指定できると便利になります。

pyproject.toml

Python パッケージビルドのためのファイルです。 ここでは、Poetry が出力します。

src/package_name と tests

パッケージおよびテストです。ここに具体的なコードを記述します。

少し詳細になりますが、Cloud Logging で構造化ログを扱えるようにするため、こちらの記事を参考に ログのJSON化や日付フォーマットの整形を行います。

WebAPI 部分のコードについて一部抜粋します。 上記の構造化ログを出力するロガーと FastAPI による WebAPI を定義します。

src/package_name/custom_logging.py

...

def getLogger(name=None):
    formatter = CloudLoggingFormatter()

    stream = logging.StreamHandler(stream=sys.stderr)
    stream.setFormatter(formatter)

    logger = logging.getLogger(name)
    logger.setLevel(logging.INFO)
    logger.addHandler(stream)

    return logger

class CloudLoggingFormatter:
    ...

src/package_name/api.py

import logging
from fastapi import FastAPI
from .custom_logging import CloudLoggingFormatter, getLogger


app = FastAPI()
logger = getLogger(__name__)

@app.get("/")
def read_root():
    return {"message": "Hello world."}

Uvicorn や FastAPI、 Gunicorn がログを出力するのが困る場合は、 @app.on_event("startup") と FastAPI の起動時に StreamHandler を上記の getLogger 関数でやっているように上書きするといいと思います。

開発環境での動作

開発環境(developmentコンテナ)で動かします。

development は次の CMD で起動します。

WORKDIR /app

CMD ["uvicorn", "package_name.module_name:app", "--reload", "--host", "0.0.0.0", "--port", "8000"]

コンテナビルドのターゲットを development としてビルドし、コンテナを起動します。

docker build --target development -t container_name:development .
docker run -it --rm -p8000:8000 -v $(pwd)/src:/app -v $(pwd)/tests:/app/tests container_name:development

これで、8000番ポートで Web アプリケーションが起動します。

開発環境で、本番と近い環境で動作確認するには次のコードでコンテナを起動します。

docker build -t container_name .
docker run -it --rm -p8000:8000 container_name

開発環境でのテストは、次のコードで行います。

docker run -it --rm -p8000:8000 -v $(pwd)/src:/app -v $(pwd)/tests:/app/tests container_name:development pytest

デプロイ

用意したシェルスクリプトでデプロイします。 対話ダイアログで Google SDK からデプロイについて確認があり、Yes で進めると、デプロイが進みます。

bash deploy.sh

デプロイ後は、Google Cloud Platform の App Engine のサービスからデプロイ情報やWebアプリケーションにアクセスできます。

おわりに

App Engine で Python による WebAPI を動かす際の、設定や開発環境構築の大枠を紹介しました。 App Engine を使うと、かなり手軽に API 開発ができるため、中身の開発に集中しやすいと思います。

運用が始まると、いろいろと不整合も出てくると思います。 そのときには、また記事を書きたいと思います。

執筆者プロフィール

高橋寛治 Sansan株式会社 DSOC (Data Strategy & Operation Center) 研究開発部 研究員

阿南工業高等専門学校卒業後に、長岡技術科学大学に編入学。同大学大学院電気電子情報工学専攻修了。在学中は、自然言語処理の研究に取り組み、解析ツールの開発や機械翻訳に関連する研究を行う。大学院を卒業後、2017年にSansan株式会社に入社。キーワード抽出など自然言語処理を生かした研究開発に取り組む。

▼本連載のほかの記事はこちら

buildersbox.corp-sansan.com

*1:スタンダード環境は、インスタンスの起動時間が数秒であり、アクセスがない時間が続くと自動で停止するため、低コストを目指すことができます。

*2:今後はクリーンデプロイできるように git clone から行うか、CI/CD で回せるといいと思います。

© Sansan, Inc.