Azure FunctionsをPythonで開発する際に意識していること

2024/12/04に公開

はじめに

こちらの記事は Azure PoC 部 Advent Calendar 2024 の 4 日目の記事です。

本記事では、Python で Azure Functions を使った開発する時に意識していることを紹介します。
なお本記事のサンプルコードやディレクトリ構成などは、Azure Functions の「Python v2 プログラミングモデル」に沿ったものになります。

Python での Azure Functions のディレクトリ構成の把握

Azure Functions ではデプロイ時に関数アプリを正しくロードするために推奨されるディレクトリ構成で開発を進めることが重要です。
Python での Azure Functions のプロジェクトにおいて推奨されるディレクトリ構成は以下のような形です。

<project_root>/
 ├──.venv/
 ├── function_app.py
 ├── .funcignore
 ├── host.json
 ├── local.settings.json
 ├── requirements.txt
 └── Dockerfile

エントリーポイントはfunction_app.pyになります。
またパッケージの依存関係はrequirements.txtになります。
poetryなどのpip以外のパッケージ管理ツールを利用している場合は、デプロイ時にrequirements.txtに書き出すなどの対応が必要です。

最初にデプロイしておく

まず関数アプリのプロジェクトを作成したら、最初にデプロイをしてしまいましょう。
Azure Functions のデプロイは割とハマりポイントが多いです。
最初にデプロイを済ませておくことで、プロジェクト構成の誤りなどを早期に発見し、スムーズに開発を進めることができます。

プランとデプロイ可能な選択肢の把握

Azure Functions はデプロイするプランによってデプロイ可能な選択肢に制限があります。
Azure Functions でのデプロイのプランの選択肢には以下の五つがあります。

  • 従量課金プラン
  • Flex 従量課金プラン
  • Premium プラン
  • 専用 (App Service) プラン
  • Azure Container Apps

それぞれのプランの特徴は以下の通りです。

従量課金プラン

従量課金プランは Azure Functions を実行しているコンピューティングリソースの利用時間に応じて課金が発生するプランです。
従量課金プランは関数アプリで実行可能な処理時間が最大で 10 分などの制限があります。
また仮想ネットワーク統合などの機能はサポートされていません。

Flex 従量課金プラン

Flex 従量課金プランは先日の Ignite 2024 で GA となったプランです。
Flex 従量課金プランも Azure Functions を実行しているコンピューティングリソースの利用時間に応じて課金が発生するプランです。
従量課金プランと比較すると高機能であり、仮想ネットワーク統合やコールドスタートを防ぐために常時稼働インスタンスを設けるといったことがサポートされています。

なお Flex 従量課金プランではコンテナでのデプロイはサポートされていません。

Premium プラン

Premium プランは、動的スケールに対応したプランです。
従量課金プラン/Flex 従量課金プランと異なる点としては、課金が実行しているコンピューティングリソースの利用時間ではなく、確保しているインスタンス単位で課金が発生することです。
Premium プランでは少なくとも 1 台のインスタンスを常時稼働させる必要があります。
この課金形態は関数アプリをホスティングするための仮想マシンを借りているというイメージを持つとわかりやすいかもしれません。
Premium プランは仮想ネットワーク統合に加えて、コンテナデプロイに対応しています。

専用(App Service)プラン

専用(App Service)プランは App Service プラン上で関数アプリを実行するプランです。
課金形態は App Service プランと同様です。
動的スケールには対応していませんが、App Service プランのインスタンス数をあげることでスケーリングすることはできます。
同一の App Service プランで関数アプリの他に Web アプリを実行できるのが特徴です。
専用(App Service)プランでも仮想ネットワーク統合に加えて、コンテナデプロイに対応しています。

Azure Container Apps

Azure Container Apps でコンテナ化された関数アプリを実行するプランです。
課金形態は Azure Container Apps と同様です。
GPU 対応ワークロード プロファイルを使用した Azure Container Apps 上で関数アプリを実行させることで、GPU を必要とする処理を実行させることもできます。

Zip デプロイとリモートビルド

Zip デプロイは従量課金、Premium プラン、専用 (App Service) プランのデフォルトのデプロイ方式です。
Python では、この Zip デプロイではリモートビルドを使うのが基本とされています。
これはローカルなどでインストールした依存パッケージが Azure Functions 上で動作しないといった可能性を減らすためです。
リモートビルドは Zip を展開後に Azure Functions のホスト上でrequirements.txtの情報をもとにパッケージをインストールする形になります。

基本的には Python で Zip デプロイをする場合はリモートビルドが有効になっていますが、リモートビルドが適用されずに依存パッケージが見つからずに関数アプリが立ち上がらないパターンがあります。
その場合は以下の環境変数を設定し、明示的にリモートビルドを有効にする必要があります。

環境変数名
ENABLE_ORYX_BUILD true
SCM_DO_BUILD_DURING_DEPLOYMENT 1

コンテナデプロイする場合の Docker イメージの作成方法

Python ではパッケージによっては OS レベルでのミドルウェアなどのインストールが要求される場合もあるため、コンテナデプロイの選択肢に持っておくと良いです。
既存のプロジェクトをコンテナデプロイをする場合は、以下のコマンドでプロジェクトルートにDockerfileを生成できます。

func init --docker-only

Dockerfileを編集することで必要なミドルウェアのセットアップなどができますが、ベースイメージは公式で用意されている Azure Functions のベースイメージを利用するのが良いです。イメージサイズなどが気になる場合はslimなどの選択肢もあるので確認しておきましょう。

https://mcr.microsoft.com/en-us/artifact/mar/azure-functions/python/tags

Blueprints の使い方を把握しておく

Python での Azure Functions プロジェクトでは Blueprints を利用して、関数アプリをモジュール化できます。
これによりfunctions_app.pyの肥大化を防ぎ、関数アプリを分割したコンポーネント単位で開発できます。

Blueprints の基本的な使い方

Blueprints の基本的な使い方は分割したい関数ごとに Blueprint とオブジェクトを作成し、functions_app.pyでインポートし、関数アプリに登録します。

例えばユーザーとグループに関する関数アプリを実装する際に、ユーザーの操作の関数とグループの操作の関数を分割することを考えてみましょう。

blueprints配下にuser_routes.pygroup_routes.pyを用意し、それぞれで Blueprint を定義し、functions_app.pyでインポートします。

├── blueprints
│   ├── group_routes.py
│   └── user_routes.py
└── function_app.py

user_routes.pygroup_routes.pyにはそれぞれに HTTP トリガーの関数が存在します。

  • user_routes.py
from azure.functions import Blueprint, HttpRequest, HttpResponse

user_bp = Blueprint()


@user_bp.route(route="users", methods=["GET"])
def get_users(req: HttpRequest) -> HttpResponse:
    # ユーザー一覧を返すダミーデータ
    users = [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
    return HttpResponse(body=str(users), status_code=200, mimetype="application/json")


@user_bp.route(route="users/{user_id}", methods=["GET"])
def get_user(req: HttpRequest) -> HttpResponse:
    # 特定のユーザー詳細を返すダミーデータ
    user_id = req.route_params.get("user_id")
    user = {"id": user_id, "name": f"User {user_id}"}
    return HttpResponse(body=str(user), status_code=200, mimetype="application/json")


@user_bp.route(route="users/{user_id}/groups", methods=["GET"])
def get_user_groups(req: HttpRequest) -> HttpResponse:
    # 特定のユーザーが所属するグループのダミーデータ
    groups = [{"id": 101, "name": "Admins"}, {"id": 102, "name": "Developers"}]
    return HttpResponse(body=str(groups), status_code=200, mimetype="application/json")
  • group_routes.py
from azure.functions import Blueprint, HttpRequest, HttpResponse

group_bp = Blueprint()


@group_bp.route("groups", methods=["GET"])
def get_groups(req: HttpRequest) -> HttpResponse:
    # グループ一覧を返すダミーデータ
    groups = [{"id": 101, "name": "Admins"}, {"id": 102, "name": "Developers"}]
    return HttpResponse(body=str(groups), status_code=200, mimetype="application/json")


@group_bp.route("groups/{group_id}", methods=["GET"])
def get_group(req: HttpRequest) -> HttpResponse:
    # 特定のグループ詳細を返すダミーデータ
    group_id = req.route_params.get("user_id")
    group = {"id": group_id, "name": f"Group {group_id}"}
    return HttpResponse(body=str(group), status_code=200, mimetype="application/json")


@group_bp.route("groups/{group_id}/users", methods=["GET"])
def get_group_users(req: HttpRequest) -> HttpResponse:
    # 特定のグループに所属するユーザーのダミーデータ
    users = [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
    return HttpResponse(body=str(users), status_code=200, mimetype="application/json")

function_app.pyでは、それぞれをインポートすることで関数アプリを定義できます。

import azure.functions as func
from blueprints.user_routes import user_bp
from blueprints.group_routes import group_bp

app = func.FunctionApp()

# Blueprintsの登録
app.register_blueprint(user_bp)
app.register_blueprint(group_bp)

このように Blueprints を活用することで、関数アプリが増えても見通しよくコードを管理できます。

関数アプリとロジックにレイヤーを設ける

関数アプリのプロジェクトではエントリーポイントとなるfunctions_app.pyや Blueprints で定義する各関数アプリのルートにロジックを書かずに、サービスレイヤーやユースケースレイヤーのような抽象レイヤーを作成しましょう。
関数アプリのエントリーポイントとなる部分に直接ロジックを書いてしまうと、コードが肥大化しメンテナンスが難しくなります。

関数アプリを構築する際はエントリーポイント部分と分離した抽象化レイヤーを設けると良いでしょう。

例えばビジネスロジックを記述するサービスレイヤー、データベースなどと接続するリポジトリレイヤーを設ける場合は以下のような構成になります。

project/
│
├── functions_app.py  # エントリーポイント
├── blueprints/
│   ├── user_routes.py  # ユーザー関連のBlueprint
│
├── services/
│   └── user_service.py  # ユーザー関連のビジネスロジック
│
└── repositories/
    └── user_repository.py  # データベース操作や外部API呼び出し

リポジトリレイヤーではデータベースなどにアクセスし情報の取得・永続化を実行します。

class UserRepository:
    def get_users(self) -> list:
        # ダミーデータの取得 (通常はデータベースやAPIから取得)
        return [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]

サービスレイヤーはビジネスロジックを実行する部分でリポジトリの呼び出しなどを実行します。

from repositories.user_repository import UserRepository

class UserService:
    def __init__(self, repository: UserRepository) -> None:
        self.repository = repository

    def fetch_users(self) -> list:
        # ビジネスロジックを実行し、データを取得
        users = self.repository.get_users()
				# データの加工
        return [{"id": u["id"], "name": u["name"].upper()} for u in users]

こうしたレイヤーを設けることで、ロジックを関数アプリと分離し、レイヤーごとにテストを記述できます。
また関数アプリ特有の実装から分離されているため HTTP トリガー、Blob トリガー、キュートリガーなど関数アプリのトリガーの違いに依存しない形でコードを記述できるため、テスタビリティを向上させることができます。

まとめ

本記事では、Python を使った Azure Functions 開発で意識すべきポイントを紹介しました。
最後に重要な点をまとめておきます。

  • 最初にデプロイをする
    初期段階でデプロイを行うことで、ディレクトリ構成の誤りや利用するプランやデプロイ方式の選択ミスといった重要な点に早期に気が付くことができます。
    これらの部分は開発の基盤となるため、開発のスムーズな進行のために早めの確認が重要です。

  • 関数アプリ特有の実装箇所の極小化
    無計画に実装を進めているとfunctions_app.pyの肥大化が起こり、メンテナンス性の低下に繋がります。
    Blueprints を活用した関数アプリのモジュール化や抽象レイヤーの導入によってfunctions_app.pyの肥大化防止や関数アプリ特有の実装から分離した形でのテスタビリティの確保などメンテナンス性を高めることができます。

Azure Functions は、適切な設計と運用方法を意識することで、様々なユースケースに対応したアプリケーションの開発を実現できます。
本記事で紹介した内容がより良い Azure Functions 開発への一助となれば幸いです。

参考

https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-deployment-technologies

https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-reference-python

https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-best-practice

Discussion