Azure Functions App × Python で Azure CosmosDB にアクセスしてみる

Serverless で何か処理を行いたいときに便利な Azure Function App。
Web API から Azure CosmosDB をアップデートするという操作を行いたく、新しく対応した Python で実装してみましたので、手順を紹介します。

# 苦労したところ

- Azure Functions で Python を利用できるのは Linux サーバー、Function App の開発をするのは Windows ということで
    - Visual Studio Code + Azure Functions Extention を利用して開発を進めていたのですが、仮想 Web サーバー(venv) が自動起動せず、結局手動で起動して進めました
    - Azure へデプロイする際に ローカルの Docker (Linux Container) が必要でした

# 実行環境
- Windows 10
- Python 3.6.x
- [Azure CLI v2.x](https://docs.microsoft.com/ja-jp/cli/azure/install-azure-cli?view=azure-cli-latest)
- [Azure Function Core Tools v2.x](https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-run-local#v2)
- Visual Studio Code
    - [Python Extension for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=ms-python.python)
    - [Azure CLI Tools for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=ms-vscode.azurecli)
    - [Azure Functions for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurefunctions) ※ CLI のみで作成するなら不要
- Docker for Windows (Linux containers)
- Azure サブスクリプション

# 手順

[1. Azure Function Python App を Azure 上に作成](#1-azure-function-app-%E3%82%92-azure-%E4%B8%8A%E3%81%AB%E4%BD%9C%E6%88%90)
[2. Azure Function Python App をローカルで作成](#2-azure-function-app-%E3%82%92%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E3%81%A7%E4%BD%9C%E6%88%90)
- [2-1. Function Python App の新規作成](#2-1-function-python-app-%E3%81%AE%E6%96%B0%E8%A6%8F%E4%BD%9C%E6%88%90)
- [2-2. Function Python App のテスト](#2-2-function-python-app-%E3%81%AE%E3%83%86%E3%82%B9%E3%83%88)
- [2-3. Function App で CosmosDB にアクセスする](#2-3-function-app-%E3%81%A7-cosmosdb-%E3%81%AB%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%81%99%E3%82%8B)
[3. ローカルで作成した Azure Function App を Azure にデプロイ](#3-%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E3%81%A7%E4%BD%9C%E6%88%90%E3%81%97%E3%81%9F-azure-function-app-%E3%82%92-azure-%E3%81%AB%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4)

# 1. Azure Function App を Azure 上に作成

Azure Portal から Azure サブスクリプション が 紐づいたアカウントでサインインします。 
[+新規作成] をクリック、**Function App** と検索して Function App を新規作成します。 
Python を選択するには、Linux サーバーを選択する必要があります。
今回は PyCosmosFunc という名前で作成しています。
![20181217_01.PNG](https://qiita-image-store.s3.amazonaws.com/0/117583/e6f9f799-340e-e3c6-b743-1209a703c1a9.png)


# 2. Azure Function App をローカルで作成

## 2-1. Function Python App の新規作成

[Azure Function Core Tools (ターミナル) を使う方法](#2-1-a-azure-function-core-tools-%E3%82%92%E4%BD%BF%E3%81%86%E5%A0%B4%E5%90%88)と、[Azure Function ツール (GUI) を使う方法](#2-1-b-azure-function-%E3%83%84%E3%83%BC%E3%83%AB%E3%82%92%E4%BD%BF%E3%81%86%E5%A0%B4%E5%90%88)、いずれかで Function の作成を行ってください。

### 2-1-a. Azure Function Core Tools を使う場合

ターミナルから作成を行います。
Visual Studio Code を開き、Open Folder をクリック、プロジェクトを配置する任意のフォルダーを指定して開きます。
![20181217_02.PNG](https://qiita-image-store.s3.amazonaws.com/0/117583/d519fcce-0cce-a727-c831-f2ba37cc3093.png)

Azure Function をローカルで作成、起動するには venv の環境が必要です。
ツールバーの **[Terminal]** > **[New Terminal]** でターミナルを開き、venv をインストールして起動します。

```
> python -m venv venv
> venv\scripts\activate
```

venv を起動した状態で、Azure Function App を作成します。

```
(venv)> func init
```

runtime は python を選択します。

```
Select a worker runtime: python
```

Azure Function のスターターテンプレートから App (Project) が作成されます。
![20181217_03.PNG](https://qiita-image-store.s3.amazonaws.com/0/117583/634096b2-ab6a-242c-0cba-bfc9253575a7.png)


Azure Function App に Function を作成します。

```
(venv)> func new
```

Template は HTTPTrigger, name は デフォルトのまま (HTTPTrigger) にします。

```
Select a template: HTTP trigger
Function name: [HttpTrigger]
```

Azure Function App 内に HttpRequest テンプレートから Function が作成されます。
![20181217_04.PNG](https://qiita-image-store.s3.amazonaws.com/0/117583/64f8b04f-1b8c-6711-60a3-5355197aa91b.png)


### 2-1-b. Azure Function ツールを使う場合

Visual Studio Code に統合されたツールを利用します。
Visual Studio Code を開き、Azure Tools のタブを開きます。
Azure Function の **Create New Project** をクリックして Function App を作成します。
![20181217_a02.PNG](https://qiita-image-store.s3.amazonaws.com/0/117583/e39e98b5-067c-5b0a-b75e-50219a592900.png)

Runtime は **Python** を選択します。
![20181217_a03.PNG](https://qiita-image-store.s3.amazonaws.com/0/117583/37145e7b-87f9-b58a-cc4d-aef72eeeb3bf.png)

今の Windows で開くには **Open in current window** を選択します。
![20181217_a04.PNG](https://qiita-image-store.s3.amazonaws.com/0/117583/78f1b183-e8d1-5990-39b9-169e4e5cf590.png)

Azure Function をローカルで作成、起動するには venv の環境が必要です。
.env フォルダに venv がインストールされているので、Terminal を開いてコマンドプロンプトで起動します。

```
> .env\scripts\activate
```

再び Azure Tools のタブにある Azure Function の **Create New Function** をクリックして Function を作成します。
![20181217_a05.PNG](https://qiita-image-store.s3.amazonaws.com/0/117583/0a647631-f5fb-3133-321f-11387433f93d.png)

作成した App のフォルダを選択します。
![20181217_a06.PNG](https://qiita-image-store.s3.amazonaws.com/0/117583/7f33f6b0-f094-b813-68fb-a110f5fadbbc.png)

Function の種類は **HTTPTrigger** を選択します。
![20181217_a07.PNG](https://qiita-image-store.s3.amazonaws.com/0/117583/1d2d3545-2429-7653-0241-083b692edc92.png)

Function Name はデフォルト (**HTTPTrigger**) のままにします。
![20181217_a08.PNG](https://qiita-image-store.s3.amazonaws.com/0/117583/ce80632d-cb6c-cdd2-06c3-898d18c72f4a.png)

Authorization Level は **Functions** を選択します。
![20181217_a09.PNG](https://qiita-image-store.s3.amazonaws.com/0/117583/2ef19391-0842-e380-35ea-4a5d4fa5c6c6.png)

Azure Function App 内に HttpRequest テンプレートから Function が作成されます。
![20181217_a10.PNG](https://qiita-image-store.s3.amazonaws.com/0/117583/fb466616-dc3c-9579-07d6-f1d83a7e7369.png)


## 2-2. Function Python App のテスト

Function App が起動すると、HttpTrigger > `__init__.py` が実行されます。
HTTP GET または POST を行うと、送信されたパラメータ(name)を取得して Hello {name}! と表示するという簡単なものです。

```__init__.py
import logging
import azure.functions as func

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

    name = req.params.get('name')
    if not name:
        try:
            req_body = req.get_json()
        except ValueError:
            pass
        else:
            name = req_body.get('name')

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

ターミナルから起動して動作を確認します。

```
(venv)> func start
```

以下のようなメッセージが表示されたら起動完了です。ブラウザーを起動して HttpTrigger の URLにアクセスします。

```
Now listening on: http://0.0.0.0:7071
Application started. Press Ctrl+C to shut down.

Http Functions:

        HttpTrigger: [GET,POST] http://localhost:7071/api/HttpTrigger
```

まず、なにもパラメータがない場合はメッセージが表示されます。
![20181217_06.PNG](https://qiita-image-store.s3.amazonaws.com/0/117583/71fcadab-ffae-0bc9-12ad-c392dd17c11a.png)

このようにパラメーターを追加すると名前が表示されるようになります。

```
http://localhost:7071/api/HttpTrigger?user=annie
```

![20181217_07.PNG](https://qiita-image-store.s3.amazonaws.com/0/117583/31d707e7-e262-8c61-3df4-7733d42c7a0f.png)

ターミナルから Ctrl+C で一旦アプリを停止します。


## 2-3. Function App で CosmosDB にアクセスする

Azure Cosmos DB を操作する API を作成していきます。
パラメーターが取得できた場合、Azure Cosmos DB のデータベース、コレクション(コンテナー)を参照して、ドキュメント数を表示するようにします。
パラメーターは database, container とします。


### Azure CosmosDB 用パッケージの追加

CosmosDB を操作するには、Python 用のモジュール(azure-cosmos)が公開されていますので、それを利用します。コマンドプロンプトから pip でインストールします。

```
(venv)> pip install azure-cosmos
```

このとき表示される azure-cosmos のバージョン(2018年12月現在、v3.0.2)を確認します。

```
(venv)> pip install azure-cosmos
Collecting azure-cosmos
　:
Installing collected packages: idna, certifi, chardet, urllib3, requests, azure-cosmos
Successfully installed azure-cosmos-3.0.2 certifi-2018.11.29 chardet-3.0.4 idna-2.8 requests-2.21.0 urllib3-1.24.1
```

この情報を requirements.txt に追加します。

```requirements.txt
azure-cosmos==3.0.2
```


### 外部スクリプトの作成

HTTPTrigger と同じ階層に SharedScripts という名前でフォルダを追加、その中に Cosmos.py という名前でファイルを作成します。

Cosmos.py を記述します。ポイントは

- import azure.cosmos.cosmos_client でモジュールをインポートしています
- CosmosDBの接続情報は冒頭、config で設定しています。(※本来であれば、別途 config ファイル等に記載すべきですが、動作確認のため直接記載しています)
    - ENDPOINT および PRIMARYKEY にお持ちの Azure CosmosDB の設定を入力してください
- CountDocuments で database および container を引数として、document の数を返します

```cosmos.py
import azure.cosmos.cosmos_client as cosmos_client

def CountDocuments(database,container):
    config = {
        'ENDPOINT': 'YOUR_COSMOSDB_ENDPOINT',
        'PRIMARYKEY': 'YOUR_COSMOSDB_KEY',
        'DATABASE': database,
        'CONTAINER': container
    }

    # Initialize the Cosmos client
    client = cosmos_client.CosmosClient(url_connection=config['ENDPOINT'], auth={
                                        'masterKey': config['PRIMARYKEY']})

    # Connect to a database
    database_link = 'dbs/' + config['DATABASE']
    db = client.ReadDatabase(database_link)

    # Connect to a container
    container_definition = {
        'id': config['CONTAINER']
    }
    container_link = database_link + '/colls/{0}'.format(config['CONTAINER'])
    container = client.ReadContainer(container_link)

    # Query these items in SQL
    query = {'query': 'SELECT * FROM server s'}

    options = {}
    options['enableCrossPartitionQuery'] = True
    options['maxItemCount'] = 2

    result_iterable = client.QueryItems(container['_self'], query, options)
    count = 0
    for item in iter(result_iterable):
        count = count +1

    #return count
    msg = str(count) + ' document(s) found in DB'
    return msg
```

### init.py の修整

database および container というパラメーターを取得して処理を行うように `__init.py__` を修正します。ポイントは

- 作成した SharedScripts > Cosmos.py から CountDocuments を import します
- database および container のパラメーターが取得できた場合は CountDocuments を呼び出します
- どちらかのパラメーターが取得できない場合は、パラメーターが必要な旨のメッセージを表示します


```__init__.py
import logging
import azure.functions as func
from ..SharedScripts import cosmos

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

    database = req.params.get('database')
    container = req.params.get('container')
    if not database:
        try:
            req_body = req.get_json()
        except ValueError:
            pass
        else:
            database = req_body.get('database')
    if not container:
        try:
            req_body = req.get_json()
        except ValueError:
            pass
        else:
            container = req_body.get('container')

    if database is not None and container is not None:
        msg = cosmos.CountDocuments(database,container)
        return func.HttpResponse(
            msg,
            status_code=200
        )
    else:
        return func.HttpResponse(
             "Please pass database and container name on the query string or in the request body",
             status_code=400
        )
```

### 再びテスト実行

ここで再度実行します。

```
(venv)> func start
```

HTTPTrigger の URL をブラウザーで開きます。

```
http://localhost:7071/api/HttpTrigger
```

パラメーターがないときは、パラメーターが必要な旨のメッセージが表示されます。
![20181217_09.PNG](https://qiita-image-store.s3.amazonaws.com/0/117583/5bcee77d-018b-caba-fb99-8ce2318a367f.png)

パラメーターとしてお持ちの Azure CosmosDB の Database 名と Container 名をセットします。

```
http://localhost:7071/api/HttpTrigger?database=YOUR_COSMOSDB&container=YOUR_CONTAINER
```

CosmosDB 内のドキュメント数が返されれば OK です。
![20181217_10.PNG](https://qiita-image-store.s3.amazonaws.com/0/117583/a2d82345-0d5f-4da3-ce5e-48af3fa39204.png)


# 3. ローカルで作成した Azure Function App を Azure にデプロイ

Linux 環境が必要となるため、**Docker for Windows (Linux Container) を起動しておきます。**

Visual Studio Code のターミナルから Azure CLI で Azure にサインインします。

```
> az login
```

ブラウザーが開くので、1. と同じアカウントでサインインします。
*Login Success* と表示されたら Visual Studio Code に戻ります。

複数サブスクリプションを持っている場合は、使用するサブスクリプション(YOUR_SUBSCRIPTION)をセットします。

```
> az account set --subscription YOUR_SUBSCRIPTION
```

Visual Studio Code のターミナルから Azure Function App (今回は PyCosmosFunc) へ Azure Function Core Tools を使ってデプロイします。
ポイントは、`--nozip` オプション および `--build-native-deps` オプションで、(Docker を利用して)ビルド後にデプロイすることです。

```
> func azure functionapp publish PyCosmosFunc --nozip --build-native-deps
```

以下のようなメッセージが表示されたらデプロイ完了です。

```
Upload completed successfully.
Functions in PyCosmosFunc:
    HttpTrigger - [httpTrigger]
        Invoke url: http://YOUR_FUNCTION_NAME.azurewebsites.net/api/httptrigger?code=YOUR_FUNCTION_ACCESSCODE
```

表示されている URL にブラウザーからアクセスします。code というパラメーターでアクセスコードが付与されています。

```
http://YOUR_FUNCTION_NAME.azurewebsites.net/api/httptrigger?code=YOUR_FUNCTION_ACCESSCODE
```

database と container のパラメーターを入力していないので、パラメーターが必要な旨のメッセージが表示されればOKです。
![20181217_11.PNG](https://qiita-image-store.s3.amazonaws.com/0/117583/9034e2da-0650-f3e8-9689-a9f3ad32005c.png)

URL の後ろに `&database=YOUR_DATABASE&container=YOUR_CONTAINER` の形式でパラメーターを入力して再度アクセスします。

```
http://YOUR_FUNCTION_NAME.azurewebsites.net/api/httptrigger?code=YOUR_FUNCTION_ACCESSCODE&database=YOUR_DATABASE&container=YOUR_CONTAINER
```

Document の数が表示されれば完了です。
![20181217_12.PNG](https://qiita-image-store.s3.amazonaws.com/0/117583/72c5c614-7799-a3aa-d091-518574fea009.png)