Python Monthly Topics

Cloudflare WorkersでサーバーレスPythonアプリを構築してみよう

筒井@ryu22eです。2024年7月の「Python Monthly Topics」は、Cloudflare WorkersのPythonサポートについて解説します。

前半ではCloudflare WorkersでPythonを使う方法について、後半ではCloudflare WorkersでPythonを動かす仕組みと技術的制限について解説します。

なお、Cloudflare WorkersのPythonサポートは本記事執筆時点(2024年7月24日)でオープンベータ版です。正式リリース時には仕様が変更される可能性があります。また、一部機能はローカル環境でしか利用できません。

Cloudflare Workersとは

Cloudflare Workersは、Cloudflareが提供するサーバーレスアプリケーションを構築・デプロイするためのプラットフォームです。主に以下のような特徴があります。

  • プログラマーはサーバーの管理やスケーリングを意識せずに、アプリケーションの開発に集中できる
  • エンドユーザーは物理的に最も近いサーバーにアクセスするため、高速なレスポンスを期待できる
  • コールドスタート(しばらく実行されていない関数を実行しなければならない状況)が排除されているため、高速な起動時間が期待できる
  • 無料枠があるため、気軽に試すことができる

類似のサービスとしてAWS Lambda@Edgeがありますが、Lambda@Edgeには無料枠がありません。また、Cloudflare WorkersにはJavaScript実行を高速化するための最適化が施されているため、Lambda@Edgeよりも高速なレスポンスが期待できます。

Cloudflare WorkersとLambda@Edgeの比較についての詳細は、以下の記事を参照してください。

Cloudflare Workersがサポートする言語

Cloudflare Workersがサポートする言語は以下の通りです。

  • JavaScript
  • TypeScript
  • WebAssembly(Wasm)のバイナリにビルドできる言語(Rust、C、C++、Kotlin、Goなど)
  • Python(オープンベータ版)

Cloudflare Workersの料金

Cloudflare Workersには無料のFreeプランと有料のStandardプラン、Enterpriseプランがあります。本記事ではFreeプランを使います。Freeプランでの制限は、以下の公式ドキュメントを参照してください。

また、リクエスト制限やアプリケーションのサイズ制限などについては、以下の公式ドキュメントを参照してください。

Cloudflare WorkersでPythonを使う方法

ここでは、Cloudflare WorkersでPythonを使う方法について解説します。

実際にローカル環境で本記事の内容を試すには、以下が必要です。

  • Node.js 16.17.0以上
  • npx

また、本番環境にコードをデプロイする際には、Cloudflareアカウントが必要です。

まずはサンプルコードを動かしてみる

公式ドキュメントGet startedにあるサンプルコードをローカル環境で動かしてみましょう。

以下の手順でサンプルコードをダウンロードし、Cloudflare Workersの開発者ツールWranglerを使ってローカルサーバーを起動します。最新のWranglerをインストールするかどうか確認するためOk to proceed? (y)と聞かれる場合がありますが、Enterキーを押して進めてください。

$ git clone https://github.com/cloudflare/python-workers-examples
$ cd python-workers-examples/01-hello
$ npx wrangler@latest dev
 ⛅️ wrangler 3.65.0
-------------------

▲ [WARNING] The entrypoint src/entry.py defines a Python worker, support for Python workers is currently experimental.


⎔ Starting local server...
[wrangler:inf] Ready on http://localhost:8787
╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ [b] open a browser, [d] open Devtools, [l] turn off local mode, [c] clear console, [x] to exit                                               │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

[b] open a browser, [d] open Devtools, ...と表示されたら、bキーを押してください。ブラウザが開いてアプリケーションの画面が表示されます。

bキーを押した後の画面
bキーを押した後の画面

xキーを押すとローカルサーバーが停止します。

サンプルコードの中身を見てみましょう。src/entry.pyの内容は以下の通りです。

src/entry.py
from js import Response

async def on_fetch(request, env):
    return Response.new("Hello world!")

リクエストはon_fetchという名前の関数で処理され、jsというモジュールを使ってレスポンスを返しています。このjsモジュールは、JavaScript APIを呼び出すためのものです。PythonなのになぜJavaScript?と思うかもしれませんが、jsモジュールが存在する理由は後述する「Cloudflare WorkersでPythonはどうやって動いているのか」で説明しますので、一旦置いておいてください。

次に、本番環境にデプロイしてみましょう。以下のコマンドを実行してください。Ok to proceed? (y)と聞かれる場合がありますが、Enterキーを押して進めてください。デプロイしたアプリケーションに割り当てられるサブドメインが未設定の場合は、サブドメインの入力も求められます。全Cloudflareユーザーで一意な名前を入力してください(後で変更可能です⁠⁠。

$ npx wrangler@latest deploy

 ⛅️ wrangler 3.65.0
-------------------

▲ [WARNING] The entrypoint src/entry.py defines a Python worker, support for Python workers is currently experimental.


Attempting to login via OAuth...
Opening a link in your default browser:〈省略〉

コマンド実行後、以下のようなアプリケーションのデプロイを許可する画面が表示されます(Cloudflareにログインしていない場合はログイン画面が表示されます⁠⁠。⁠Allow」をクリックしてください。

アプリケーションのデプロイを許可する画面
アプリケーションのデプロイを許可する画面

以下の画面が表示されたらデプロイ成功です。画面はこのまま閉じても問題ありません。

デプロイ成功画面
デプロイ成功画面

CloudflareダッシュボードのWorkers & Pagesには、今デプロイしたアプリケーション「hello-python」が表示されているはずです。

CloudflareダッシュボードのWorkers & Pages
CloudflareダッシュボードのWorkers & Pages

「hello-python」の詳細画面から、デプロイしたアプリケーションをブラウザで表示することができます。

デプロイしたアプリケーションをブラウザで表示させる
デプロイしたアプリケーションをブラウザで表示させる

または、npx wrangler@latest deployを実行した際に表示されるPublished hello-pythonの下にあるURLをブラウザで開いても、同じ画面が表示されます。

デプロイしたアプリケーションのURL
デプロイしたアプリケーションのURL

環境変数を使ってみる

APIの認証情報やデータベースの接続情報などは、本番環境、ステージング環境によって異なる場合があります。そのような情報は環境変数としてソースコードの外に定義しておくと、環境ごとに異なる情報を簡単に切り替えることができます。

Cloudflare Workersでも環境変数を扱うことができます。前述のサンプルコードを使って、環境変数の値を取得してみましょう。

src/entry.pyを以下のように変更してください。

src/entry.pyを変更
from js import Response

async def on_fetch(request, env):
    return Response.new(f"My name is {env.MY_NAME}.\nSECRET_KEY: {env.SECRET_KEY}")

上記のコードで参照している環境変数MY_NAMESECRET_KEYを定義します。SECRET_KEYは秘密の情報という前提です。wrangler.tomlに環境変数MY_NAMEを設定します。

wrangler.toml
name = "hello-python"
main = "src/entry.py"
compatibility_flags = ["python_workers"]
compatibility_date = "2024-03-29"

# ↓これを追加
[vars]
MY_NAME = "Ryuji Tsutsui"

秘密の情報をローカル環境で参照する場合は、プロジェクト直下の.dev.varsというファイルに環境変数と値を定義します(本番環境での定義方法は後述します⁠⁠。このファイルは.gitignoreに追加して、Gitリポジトリには含めないようにしてください。

.dev.vars
SECRET_KEY="local_value"

ローカル環境で動作確認を行います。npx wrangler@latest devを実行すると、以下のようにwrangler.tomlと.dev.varsに書かれた環境変数の値が表示されます。

ローカル環境で環境変数の値を表示
ローカル環境で環境変数の値を表示

次に、本番環境にデプロイするため、SECRET_KEYの値を環境変数として設定します。本番環境への環境変数の設定はnpx wrangler secret putコマンドを使います。****************の部分には「production_value」を入力してください。

$ npx wrangler secret put SECRET_KEY

 ⛅️ wrangler 3.65.0
-------------------

✔ Enter a secret value: … ****************
🌀 Creating the secret for the Worker "hello-python"
✨ Success! Uploaded secret SECRET_KEY

npx wrangler@latest deployを実行してからブラウザでアプリケーションを表示すると、以下のように環境変数の値が表示されます。

本番環境で環境変数の値を表示
本番環境で環境変数の値を表示

環境変数の内容は、CloudflareダッシュボードのWorkers & Pagesからも確認できます(ただし秘密の情報は変数名の表示のみで、値は表示されません⁠⁠。

Cloudflareダッシュボードで環境変数の値を表示
Cloudflareダッシュボードで環境変数の値を表示

簡単なAPIを作成する

書籍の情報を登録・取得するAPIを作成してみましょう。APIの要件は以下の通りです。

  • POSTメソッドの場合:書籍のtitleとdescriptionを受け取るとDBに登録する
    • titleとdescriptionのいずれかがない場合は400エラーを返す
  • GETメソッドの場合:DBに保存されている書籍情報(titleとdescription)を取得する
  • POST、GET以外のメソッドの場合:405エラーを返す

データベースはCloudflare D1を使います。Cloudflare D1とは、SQLiteで構築されたサーバーレスSQLデータベースです。

src/entry.pyを以下のように変更します。

src/entry.pyを変更
from js import Headers, Response
from pyodide.ffi import JsException

async def on_fetch(request, env):
    # JSON形式でレスポンスを返すためのヘッダーを設定
    headers = Headers.new({"content-type": "application/json; charset=utf-8"}.items())

    if "POST" in request.method:
        # POSTリクエストの場合
        try:
            data = await request.json()
        except JsException:
            # json()メソッドはリクエストボディがJSONでない場合に例外を発生させる仕様なので、ここで例外処理を行う
            return Response.new({"error": "Invalid JSON"}, status=400, headers=headers)
        # JSONリクエストボディからtitleとdescriptionを取得
        title = getattr(data, "title")
        description = getattr(data, "description")
        if not title or not description:
            # titleまたはdescriptionがない場合はエラーレスポンスを返す
            return Response.new(
                {"error": "Title and description are required"},
                status=400,
                headers=headers,
            )
        # データベースに書籍情報を登録
        await env.DB.prepare(
            "INSERT INTO books (title, description) VALUES (?, ?)"
        ).bind(title, description).run()
        return Response.new({"message": "ok"}, headers=headers)
    elif "GET" in request.method:
        # GETリクエストの場合
        # データベースから書籍情報を取得
        r = await env.DB.prepare("SELECT * from books").all()
        return Response.json(r.results, headers=headers)
    else:
        # POST, GET以外のリクエストの場合
        # POST, GET以外には非対応なので、405エラーを返す
        return Response.new({"error": "Method not allowed"}, status=405, headers=headers)

次に、以下のコマンドで本番環境にbookshelfデータベースを作成します。

$ npx wrangler d1 create bookshelf

 ⛅️ wrangler 3.65.0
-------------------

✅ Successfully created DB 'bookshelf' in region APAC
Created your new D1 database.

[[d1_databases]]
binding = "DB" # i.e. available in your Worker on env.DB
database_name = "bookshelf"
database_id = "***"

上記の出力内容から[[d1_databases]]以下をコピーして、wrangler.tomlに追加します。

wrangler.toml
name = "hello-python"
main = "src/entry.py"
compatibility_flags = ["python_workers"]
compatibility_date = "2024-03-29"

# ↓これを追加
[[d1_databases]]
binding = "DB" # i.e. available in your Worker on env.DB
database_name = "bookshelf"
database_id = "***"  # ここにはnpx wrangler d1 create bookshelf実行時に表示されたdatabase_idが入る

テーブルを作成するため、schema.sqlを作成して、以下の内容を記述します。

schema.sql
DROP TABLE IF EXISTS books;
CREATE TABLE IF NOT EXISTS books (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, description TEXT);

以下のコマンドで、schema.sqlの内容を元に、ローカルの.wrangler以下にテーブルを作成します。

$ npx wrangler d1 execute bookshelf --local --file=./schema.sql

 ⛅️ wrangler 3.65.0
-------------------

🌀 Executing on local database bookshelf (***) from .wrangler/state/v3/d1:
🌀 To execute on your remote database, add a --remote flag to your wrangler command.

これで完成です。npx wrangler@latest devを実行してローカルサーバーを起動し、以下のコマンドでPOSTリクエストを送信してください。

$ curl -X POST "http://localhost:8787/" -d '{"title": "Python実践レシピ", "description": "Pythonでプログラムを作成するときに役立つ機能とライブラリを網羅した、実践的なレシピ集です。"}'
{'message': 'ok'}
$ curl -X POST "http://localhost:8787/" -d '{"title": "最短距離でゼロからしっかり学ぶ Python入門 必修編", "description": "プログラミング環境の用意,基本的なプログラムの書き方に始まり,リスト,辞書,クラス,関数といった基礎的な知識からエラー処理,テストコードの書き方までを演習問題を交えながら,わかりやすく解説します。"}'
{'message': 'ok'}
$ curl -X POST "http://localhost:8787/" -d '{"title": "最短距離でゼロからしっかり学ぶ Python入門 実践編", "description": "「エイリアン侵略ゲーム」「データの可視化」「Webアプリケーション」という3つのプロジェクトにチャレンジします。"}'
{'message': 'ok'}

以下のコマンドでGETリクエストを送信すると、上記で登録した情報を確認できます。

$ curl "http://localhost:8787/"
[{"id":1,"title":"Python実践レシピ","description":"Pythonでプログラムを作成するときに役立つ機能とライブラリを網羅した、実践的なレシピ集です。"},{"id":2,"title":"最短距離でゼロからしっかり学ぶ Python入門 必修編","description":"プログラミング環境の用意,基本的なプログラムの書き方に始まり,リスト,辞書,クラス,関数といった基礎的な知識からエラー処理,テストコードの書き方までを演習問題を交えながら,わかりやすく解説します。"},{"id":3,"title":"最短距離でゼロからしっかり学ぶ Python入門 実践編","description":"「エイリアン侵略ゲーム」「データの可視化」「Webアプリケーション」という3つのプロジェクトにチャレンジします。"}]

本番環境にデプロイする場合は、次のコマンドを実行してテーブルを作成してください。This process may take some time, during which your D1 database will be unavailable to serve queries. Ok to proceed?と聞かれたらEnterキーを押して進めてください。

$ npx wrangler d1 execute bookshelf --remote --file=./schema.sql

 ⛅️ wrangler 3.65.0
-------------------

〈省略〉
┌────────────────────────┬───────────┬──────────────┬────────────────────┐
│ Total queries executed │ Rows read │ Rows written │ Database size (MB) │
├────────────────────────┼───────────┼──────────────┼────────────────────┤
│ 2                      │ 4         │ 2            │ 0.02               │
└────────────────────────┴───────────┴──────────────┴────────────────────┘

上記コマンドの実行後、npx wrangler@latest deployを実行してデプロイしてください。デプロイ後、ローカル環境と同じくデータの登録、取得ができます。デプロイしたアプリケーションのURLを確認する方法は、⁠まずはサンプルコードを動かしてみる」を参照してください。

$ export API_URL="https://****"  # デプロイしたアプリケーションのURL
$ curl -X POST $API_URL -d '{"title": "Python実践レシピ", "description": "Pythonでプログラムを作成するときに役立つ機能とライブラリを網羅した、実践的なレシピ集です。"}'
{'message': 'ok'}
$ curl -X POST $API_URL -d '{"title": "最短距離でゼロからしっかり学ぶ Python入門 必修編", "description": "プログラミング環境の用意,基本的なプログラムの書き方に始まり,リスト,辞書,クラス,関数といった基礎的な知識からエラー処理,テストコードの書き方までを演習問題を交えながら,わかりやすく解説します。"}'
{'message': 'ok'}
$ curl -X POST $API_URL -d '{"title": "最短距離でゼロからしっかり学ぶ Python入門 実践編", "description": "「エイリアン侵略ゲーム」「データの可視化」「Webアプリケーション」という3つのプロジェクトにチャレンジします。"}'
{'message': 'ok'}
$ curl $API_URL
[{"id":1,"title":"Python実践レシピ","description":"Pythonでプログラムを作成するときに役立つ機能とライブラリを網羅した、実践的なレシピ集です。"},{"id":2,"title":"最短距離でゼロからしっかり学ぶ Python入門 必修編","description":"プログラミング環境の用意,基本的なプログラムの書き方に始まり,リスト,辞書,クラス,関数といった基礎的な知識からエラー処理,テストコードの書き方までを演習問題を交えながら,わかりやすく解説します。"},{"id":3,"title":"最短距離でゼロからしっかり学ぶ Python入門 実践編","description":"「エイリアン侵略ゲーム」「データの可視化」「Webアプリケーション」という3つのプロジェクトにチャレンジします。"}]

built-in packagesを使ってみる

built-in packagesとは、Cloudflare Workersで提供されているPythonパッケージです。利用できるパッケージは以下の公式ドキュメントを参照してください。

利用するには、requirements.txtにパッケージ名を記述します。たとえば、FastAPIを使いたい場合は、以下のように記述します。

requirements.txt
fastapi

通常のPython開発では、requirements.txtに書くパッケージ名にはfastapi==0.68.0のようにバージョンを指定することができますが、Cloudflare Workersではこの書き方ができません。Cloudflare Workersでは、wrangler.tomlに書かれたCompatibility datesCompatibility flagsに基づいてパッケージのバージョンが決まります。Compatibility datesとCompatibility flagsの記述例は以下の通りです。

wrangler.toml
compatibility_date = "2023-12-18"  # Compatibility dates
compatibility_flags = ["python_workers"]  # Compatibility flags

実際に動かしてみる場合は、⁠まずはサンプルコードを動かしてみる」で使ったサンプルコードの03-fastapiを使うとすぐ試すことができます。なお、本記事執筆時点ではbuilt-in packagesは本番環境にデプロイすることができないため、ローカル環境でのみ利用可能です。

$ git clone https://github.com/cloudflare/python-workers-examples  # すでに実行済みなら省略
$ cd python-workers-examples/03-fastapi
$ npx wrangler@latest dev

その他のサンプルコード

以下公式ドキュメントにケース別のサンプルコードがあります。興味がある方は試してみてください。

Cloudflare WorkersでPythonはどうやって動いているのか

Cloudflare WorkersではWebAssemblyをサポートしているため、WebAssemblyのバイナリを生成できる言語であれば、Cloudflare Workersで動作させることができます。ところが、Pythonに関してはコードをWebAssemblyバイナリに変換しているわけではありません。

Cloudflare Workersのランタイムであるworkerdには、CPythonのWebAssembly実装であるPyodideが組み込まれています。PythonコードはPyodideが解釈することで実行されます。

なお、Pyodideについては以下の過去記事でも紹介しています。興味がある方は読んでみてください。

また、現在のCloudflare WorkersワーカーのほとんどはJavaScriptで書かれていますが、Pythonで同様の機能を持つワーカーを1から実装するのはかなりの労力がかかります。そこで、JavaScriptの外部インターフェイス、つまりFFI(Foreign Function Interface)を提供し、PythonからJavaScriptのAPIを呼び出すことができるようにしています。前述の「まずはサンプルコードを動かしてみる」でも紹介したとおり、Pythonコードからjsモジュールを通してJavaScriptのAPIを呼び出すことができます。

Cloudflare WorkersでPythonを動かす仕組みについては、以下のCloudflareのブログ記事でも詳しく解説しています。

Cloudflare WorkersでPythonを使う上での技術的制限

Cloudflare WorkersではPythonアプリケーションはPyodideで動作するため、いくつかの技術的制限があります[1]

利用制限がある標準ライブラリ

Cloudflare Workersでは、以下の標準ライブラリは利用制限があります。

ライブラリ名 制限の内容
hashlib OpenSSLに依存するハッシュアルゴリズムはデフォルトで利用不可
decimal C実装(_decimal⁠⁠、Python実装(_pydecimal)のうち、C実装のみ利用可能
pydoc 組み込みのヘルプメッセージは利用不可
webbrowser オリジナルのWebブラウザモジュールは利用不可

なお、hashlibの制限回避方法については、本記事執筆時点では公式ドキュメントに記載がありませんでした。

importできない標準ライブラリ

以下の標準ライブラリはランタイムから削除されているため、importできません。

curses、dbm、ensurepip、fcntl、grp、idlelib、lib2to3、msvcrt、pwd、resource、syslog、termios、tkinter、turtle.py、turtledemo、venv、winreg、winsound

また、以下標準ライブラリはランタイムに含まれていますが、削除されたtermiosに依存しているため、importできません。

  • pty
  • tty

importできるが利用できない標準ライブラリ

以下の標準ライブラリはimportできますが、利用できません。

  • multiprocessing
  • threading
  • sockets

最後に

Cloudflare WorkersでPythonを使う方法、内部の仕組み、技術的制限について解説しました。オープンベータ版ということもあり、まだ発展途上ではありますが、PythonプログラマーにとってCloudflare Workersが魅力的なプラットフォームになりつつあるのではないでしょうか。

筆者が使ってみた実感としては、JavaScriptのFFIがあることでWebアプリケーションに必要な機能は揃ってはいるものの、Pythonの流儀と少し異なるために戸惑う場面がいくつかありました。たとえば、リクエストボディのJSONを取得する際に書くコードrequest.json()は、期待する挙動と実際の挙動が以下のように異なっていました。

期待する挙動 実際の挙動
辞書型が返ってきてrequest.json()["foo"]request.json().get("foo")のように書ける request.json().foogetattr(request.json(), "foo")のように属性を呼び出す
リクエストボディがないと空の辞書を返す リクエストボディがないと例外pyodide.ffi.JsExceptionが発生する

この違和感は、built-in packagesが本番環境で使えるようになれば、ある程度解消されるのではないかと期待しています。

また、Pyodideを組み込んだ仕組みも面白いですね。これはあくまで筆者の想像ですが、Rubyのruby.wasm、PHPのphp-wasmなど、他の言語のWebAssembly実装インタプリタを組み込んで、Cloudflare Workersで動かせるようになる日が来るかもしれません。

おすすめ記事

記事・ニュース一覧