LoginSignup
3
3

More than 5 years have passed since last update.

Azure FunctionsとGrapheneでCosmosDBデータ操作を行う

Last updated at Posted at 2019-05-02

1. はじめに

今回はAzure FunctionsでGraphQL APIを実装し始めたので、備忘録としてまとめていきたいと思います。
前回はAzure Functionsの下準備として、PythonからAzure CosmosDBのデータ操作を用意しました。次はAzure FunctionsからCosmosDBのデータ操作を用意し、合わせてAzure FunctionsのHTTPリクエストとしてGraphQLクエリを受け付けられるようにしたいと思います。

この記事ではAzureFunctionsローカル関数までの確認をしています。(実行時にモジュールのインポートエラーとなったので)AzureFunctionsのデプロイと確認は別途する予定です。

Azure Functionsに関しては公式ドキュメントに概要・詳細ともに充実しているようです。
また「Azure で初めての Python 関数を作成する」と「Azure Functions の Python 開発者向けガイド」という公式ドキュメントには設定・実装方法・チュートリアルが用意されているので、こちらを参照しながら理解を深めていくと良いかなと思います。

GraphQLの実装部分については、GraphQLのドキュメントでも紹介されていたGrapheneというライブラリを利用しています。GraphQLに関しては用語や仕様などは勉強していきたいなと思います。

2.事前準備

この章はAzure Functionsをローカルで開発するためのツール群のインストールと、関数プロジェクト雛形の作成に関して記載しています。公式ドキュメントにも記載されているので、実施済みの場合は読み飛ばしてください。

2.1. ツールのインストール

開発環境はmacOSを前提とします。
Azure Functionsをローカルで開発するための.Net Core SDKとAzure Functions Core Toolsをインストールします。

.NET Core SDKは以下からパッケージをダウンロードしインストールします。
.NET Core 2.2 Build apps - SDK

Auzre Functions Core Toolsは公式ドキュメントに記載のあるとおり、brewコマンドにてインストールします。

brew tap azure/functions
brew install azure-functions-core-tools

Azure CLIも同様に公式ドキュメントに記載のあるとおり、brewコマンドにてインストールします。

brew install azure-cli

2.2. 関数の初期設定

プロジェクト名はMyFunctionProjに、関数名はMyFunctionとして作成していきます。
名称については適宜利用環境に合わせて決めて作成します。

1. 仮想環境を作成してアクティブ

python3.6 -m venv .env
source .env/bin/activate

2. ローカル関数プロジェクトを作成

func init MyFunctionProj
Select a worker runtime: 
1. dotnet
2. node
3. python (preview)
4. java
5. powershell
Choose option: 3

3. 関数を作成

cd MyFunctionProj
func new
Select a template:
1. Blob trigger
2. Cosmos DB trigger
3. Event Grid trigger
4. Event Hub trigger
5. HTTP trigger
6. Queue trigger
7. Service Bus Queue trigger
8. Service Bus Topic trigger
9. Timer trigger

Choose option: 5
Function name: MyFunction

4. ローカル関数の実行

pip install -r requirements.txt
func host start 

5.Azure Functionsの作成

リソースグループとストレージアカウントは作成済みとします。
Azure FunctionsのFunction Appを作成します。
(以下に記載されているコマンドではApplication Insightsを展開していないので、ログ確認が必要の場合は別途展開します。)

az login
az functionapp create --resource-group <resource_group_name> --os-type Linux \
--consumption-plan-location westeurope  --runtime python \
--name <azurefunctions_app_name> --storage-account  <storage_name>

6.Azure Functionsへのコードデプロイ

az login
cd MyFunctionProj
func azure functionapp publish <azurefunctions_app_name> --build-native-deps

以下が表示されればデプロイ完了です。

Upload completed successfully.
Deployment completed successfully.

またデプロイコマンドを実行する際、パッケージによっては--build-native-depsオプションを付与して実行が必要です。(以下のエラーが表示されます。関連情報)
--build-native-depsオプションを付与すると、PythonコードのビルドをDockerコンテナ(mcr.microsoft.com/azure-functions/pythonイメージ)内部でビルドするようなので、Docker環境もインストールが必要になると思います。

binary dependencies without wheels are not supported.  
Use the --build-native-deps option to automatically build and configure the dependencies using a Docker container. 
More information at https://aka.ms/func-python-publish

3. 関数の実装

3.1. 実装概要

前回作成したCosmosDB関連のコードとGraphQLのコードを追加し、HTTP POSTでGraphQLクエリが来たらレスポンスを返すサンプルを実装していきたいと思います。

Azure Functions の Python 開発者向けガイドに記載のあるとおり、共有コードは別のフォルダーに保存のうえ、モジュールは相対インポートにてインポートするように変更します。

フォルダ構造は以下になります。

MyFunctionProj
 | - cosmosdb
 | | - __init__.py
 | | - config.py
 | | - cosmosdb.py
 | - graphqllib
 | | - __init__.py
 | | - graphql.py
 | - MyFunction
 | | - __init__.py
 | | - function.json
 | - .funcignore
 | - .gitignore
 | - host.json
 | - requirements.txt

requirements.txtには以下2つを追加します。

azure-cosmos
graphene>=2.0

cosmosdbフォルダには、前回作成したCosmosDBデータ操作用のコードを配置しています。
graphqlフォルダには、GraphQLのスキーマ・リゾルバー定義と実際の処理を実行しているコードを配置しています。GraphQLの実装にはGrapheneを利用しました。こちらの利用は別途整理していきたいと思います。

3.2. GraphQLクエリの実行

上述のとおり、PythonでのGraphQLの実装にGrapheneを利用しています。GrapheneはPython/JavaScript向けのGraphQLライブラリです。
実装していくにあたり、公式ドキュメントGitHubも参考になると思います。

GraphQLの実装を進めていくにあたり、まずスキーマ定義ファイル(SDL)を作成し、次にリゾルバーを定義し、最後にリゾルバー内部にデータソースに対する処理を実装するという流れで実装していきます。

Grapheneではスキーマ・リゾルバーの定義を含め、Pythonコードで実装していくことになります。GraphQLスキーマをスキーマ定義言語ではなくコードで記述する点が他言語GraphQLライブラリ(express-graphqlapollo-serverPrismaなど)との違いでしょうか。

オブジェクト型の定義

GraphQLのオブジェクト型を定義するにはObjectTypeを継承したクラスを用意します。
オブジェクト内部のフィールドは、フィールド名とGraphQLの型を式として宣言します。

#idとmessageというフィールとを持つItem型オブジェクトの定義
class Item(graphene.ObjectType): #GraphQLオブジェクトの定義
    id = graphene.String(required=True) #オブジェクト内部のフィールドの名称と型の宣言
    message = graphene.String() 

クエリ型の定義

GraphQLのQuery型も同様にObjectTypeを継承したクラスを用意します。
Query内部のフィールド(主にリゾルバー)は、フィールド名と引数・返り値をGraphQLの型を式として宣言した上で、リゾルバー関数(resolve_メソッド名のメソッド)を実装します。
リゾルバーとは、データソースに対する呼び出しを行う関数、またはある値 (個々のレコード、レコードのリストなど) を返すための関数で、GraphQLクエリに記述されたフィールド名のリゾルバーが呼び出されるという流れになります。
(CosmosDBのデータ操作はリゾルバー内部で実行しています。)

#getItemというリゾルバー関数を持つQuery型の定義
class Query(graphene.ObjectType): #クエリ型オブジェクトの用意
    getItem = graphene.Field(Item) #リゾルバーの関数名、引数・返り値の型の宣言

    #リゾルバー関数getItemと対応づけのうえ、内部処理を実装する
    def resolve_getItem(self, info): 
        return Item(id="1", message="SampleMessage")

ルートクエリ宣言とクエリの実行

GraphQLのクエリを実行するにあたり、SchemaクラスでqueryとQuery型を対応づけ(ルートクエリ型の宣言)しておくことで、実際にクエリを実行(executeメソッドの呼び出し)する際に対応するQueryのリゾルバーの関数が呼び出されて処理が実行されるようになります。

schema = graphene.Schema(query=Query)
schema.execute("GraphQLのクエリ")

GraphQL関連のコード

今回用意したGraphQL関連のコードは以下になります。
#コードはGitHubにはPUSHしていますが整理中です。

graphql/graphql.py
import graphene
import json
from datetime import datetime, timezone
from ..cosmosdb.cosmosdb import DatabaseConnection
from ..cosmosdb.cosmosdb import getItem, getReplacedItem
from logging import getLogger
logger = getLogger(__name__)

class DbItem(graphene.ObjectType):
    id = graphene.String(required=True)
    partitionKey = graphene.ID(required=True)
    message = graphene.String()
    addition = graphene.String()
    rid = graphene.String() # comosdb column name _rid
    link = graphene.String() # comosdb column name _self
    etag = graphene.String() # comosdb column name _etag
    attachments = graphene.String() # comosdb column name _attachments
    ts = graphene.Int() # comosdb column name _ts
    datetime = graphene.types.datetime.DateTime()  

    def resolve_rid(self, info):
        return self._rid
    def resolve_link(self, info):
        return self._self
    def resolve_etag(self, info):
        return self._etag
    def resolve_attachments(self, info):
        return self._attachments
    def resolve_ts(self, info):
        return self._ts
    def resolve_datetime(self, info):
        return datetime.fromtimestamp(self._ts, timezone.utc)

class Query(graphene.ObjectType):
    getSampleItem = graphene.Field(DbItem)
    readItem = graphene.Field(DbItem, argument=graphene.String())
    readItems = graphene.List(DbItem)

    def resolve_getSampleItem(self, info):
        item = DbItem(id="1", partitionKey=1,
            message="SampleMessage", addition="SampleAddtionMessage")
        return item

    def resolve_readItem(self, info, argument):
        results = DatabaseConnection().read_item(argument)
        if results.__len__() > 0:
            item = DbItem.__new__(DbItem)
            item.__dict__.update(results[0])
            return item
        else:
            return {}

    def resolve_readItems(self, info):
        results = []
        for item in DatabaseConnection().read_items():
            i = DbItem.__new__(DbItem)
            i.__dict__.update(item)
            results.append(i)
        return results

class GraphQL:
    def __init__(self):
        self.schema = graphene.Schema(
            query=Query,
        )

    def query(self, query):
        logger.info(query)
        results = self.schema.execute(query)
        return json.dumps(results.data)

上記のスキーマをGraphQLスキーマ定義言語で記述すると以下のような感じになります。

type DbItem {
  id: String!
  partitionKey: ID!
  message: String
  addition: String
  rid: String
  link: String
  etag: String
  attachments: String
  ts: Int
  datetime: DateTime
}

type Query {
  getSampleItem: DbItem
  readItem(argument:String): DbItem
  readItems: [DbItem]     
}

schema {
  query: Query
}

CosmosDBのデータ取得結果からはdict型が返されるのですが、Grapheneに渡す際には以下のようにややトリッキーなことをしています。

item = DbItem.__new__(DbItem)
item.__dict__.update(CosmosDBデータ取得結果[dict型])

またフィールド名とリゾルバーに(というか記号?)を使えないようなので、取得する元データのフィールド名にを含む場合はresolve_XXX関数を用意して対応する方法をとりました。

3.3. HTTP POSTリクエストの処理

Azure Functionsのリクエストを受けて処理する部分はシンプルな構成になります。
リクエストからJSONデータを取得してGraphqlクエリ文字列を受け取って、GraphQLのクエリ実行部に処理を投げて結果を得るという、シンプルな構成にしています。

MyFunction/__init__.py
import azure.functions as func
from ..graphqllib.graphql import GraphQL, Query
from logging import getLogger
logger = getLogger(__name__)

def main(req: func.HttpRequest) -> func.HttpResponse:
    logger.info('Python HTTP trigger function processed a request.')

    query = req.params.get('query')
    if not query:
        try:
            req_body = req.get_json()
            # query = str(req.get_body().decode(encoding='utf-8'))
            logger.info(req_body)
        except ValueError:
            pass
        else:
            query = req_body.get('query')

    try:
        results = GraphQL().query(query)
    except Exception as e:
        logger.error(e)
        return func.HttpResponse(
             "Internal Server Error",
             status_code=500
        )

    if results:
        return func.HttpResponse(f"{results.data}")
    else:
        return func.HttpResponse(
             "Please pass a name on the query string or in the request body",
             status_code=400
        )

コードが準備できたら再度デプロイします。

az login
cd MyFunctionProj
func azure functionapp publish <app_name> --build-native-deps

4. GraphQLクエリの実行確認

実装した関数にGraphQLのクエリを発行し、レスポンスを見たいと思います。
クエリの発行はcurlコマンドを使うほか、GraphiQLInsomniaを使う手があると思います。
 
curlコマンドの場合には以下でクエリを発行できます。

curl -X POST -H "Content-Type: application/json" -d '{"query": "query { readItems { id message etag ts} }"}' http://localhost:7071/api/MyFunction

今回発行したGraphQLクエリは以下になります。Insomniaの場合はURLを指定し以下のGraphQLクエリを発行するとレスポンスが得られます。

query { 
  readItems { 
    id
    message
    etag
    ts
  } 
}

で、レスポンスは以下になりました。
GrapheneによるGraphQLクエリの実行結果はOrderedDict型で返されるようですが、このままだと見づらいのと外部のAPIからコールした時に色々と変換しないとなので、要修正ですね。
→レスポンスデータはOrderedDict型からJSONに変換すれば問題なく対応できそうです。

OrderedDict([('readItems', [OrderedDict([('id', 'id1'), ('message', 'Hello World CosmosDB!'), ('etag', '"00001946-0000-2300-0000-5cc96e910000"'), ('ts', 1556704913)]), OrderedDict([('id', 'id3'), ('message', 'Hello World CosmosDB!'), ('etag', '"00008a47-0000-2300-0000-5cca65bc0000"'), ('ts', 1556768188)]), OrderedDict([('id', 'id4'), ('message', 'Hello World CosmosDB!'), ('etag', '"00008b47-0000-2300-0000-5cca65bc0000"'), ('ts', 1556768188)])])])

Insomniaで確認すると以下のような結果になりました。(レスポンスデータ対応後)
graphene-test01.png
graphene-test02.png

AzureFunctionsデプロイ済みの場合は以下のURLになります。
https://<azurefunctions_app_name>.azurewebsites.net/api/myfunction

(この記事の手順を踏むと500エラーとなります。~ApplicationInsightsでログを見るとどうやらインポートエラーが起きている模様。ここは別途確認したいと思います。~)
→こちらはどうやらGrapheneとモジュールの衝突が発生していたようなので、graphql関連のコードのフォルダ名を修正しました。

5. まとめ

今回は、Azure FunctionsでCosmosDBデータ操作およびGraphQL APIの実装を行いました。
Azure FunctionsのPythonは記事記載時点でプレビュー段階です、利用しているツールや仕様が変更される可能性があったり、ドキュメントを見ながら試行錯誤が必要になるかなと思います。(ローカル関数だとうまくいったけど、デプロイするとうまくいかないというのもあり・・・)

まずはAzureFunctionsデプロイ後のエラーを確認し、レスポンスデータの変換、Mutationを実装する、認証を追加する、コードをCI/CDでデプロイできるようにする、Azure Functions関連の環境をARMテンプレートからデプロイできるようにするのが今後の作業でしょうか。

参考情報

Azure Functions のドキュメント
Azure で初めての Python 関数を作成する (プレビュー)
Azure Functions の Python 開発者向けガイド
Azure Functions Core Tools の操作
GraphQL
Graphene

3
3
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
3
3