3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Azure function(python)でmattermost botを開発してみる

Last updated at Posted at 2019-12-20

はじめに

Azure functionが用意してくれているテンプレートを使えば何となく動かすことはできますが、実際に自分のやりたい処理を実現するにはどうするか少し分かりづらく、備忘録ながら公式ドキュメントを元にHttpTriggerテンプレートを改造し自作Functionを開発する方法をまとめます。

何となく動かすまでは以下のAzureLearnが参考になりますので必要に応じて参照ください。

目的

  • Azure functionの使い勝手を知る
  • 公式ドキュメントに従いつつ応用を効かせて実践的な関数を開発する
  • Azure functionのHttpTriggerテンプレートに機能追加し自作関数の作り方を学ぶ

環境を整備する

OSはubuntu18.04をベースとし、以下を参考にして開発環境(pythonランタイム)を整備します。

コマンドは以下の感じでpython3環境とazure cli、そしてazure function core toolsをインストールします。

  • python3のインストール
apt-get -y update
apt-get -y install python3-venv
  • azure cliのインストール
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
  • azure function coreのインストール
curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg
mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg
sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-ubuntu-$(lsb_release -cs)-prod $(lsb_release -cs) main" > /etc/apt/sources.list.d/dotnetdev.list'
apt-get -y update
apt-get -y install azure-functions-core-tools
  • venvの起動
python -m venv .venv
source .venv/bin/activate

FunctionAppを構築します

以下のARM templateを利用してfunctionをデプロイするリソースを作成します。
デプロイ方法は公式ドキュメントを参照しておこなってください。

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "suffix": {
            "type": "string",
            "defaultValue": "2020012603"
        },
        "storageAccountSKU": {
            "type": "string",
            "defaultValue": "Standard_LRS",
            "allowedValues": [
                "Standard_LRS",
                "Standard_GRS"
            ]
        },
        "storageAccountKind": {
            "type": "string",
            "defaultValue": "StorageV2"
        },
        "appServicePlanSku": {
            "type": "string",
            "defaultValue": "B1",
            "allowedValues": [
                "D1",
                "B1"
            ]
        }
    },
    "variables": {
        "storageAccountName": "[concat('stracc', parameters('suffix'))]",
        "appServicePlanName": "[concat('appsvr', parameters('suffix'))]",
        "functionAppName": "[concat('funcapp', parameters('suffix'))]"
    },
    "resources": [
        {
            "name": "[variables('storageAccountName')]",
            "type": "Microsoft.Storage/storageAccounts",
            "apiVersion": "2019-04-01",
            "location": "[resourceGroup().location]",
            "tags": {
                "displayName": "[variables('storageAccountName')]"
            },
            "sku": {
                "name": "[parameters('storageAccountSKU')]"
            },
            "kind": "[parameters('storageAccountKind')]"
        },
        {
            "name": "[variables('appServicePlanName')]",
            "type": "Microsoft.Web/serverfarms",
            "apiVersion": "2018-02-01",
            "location": "[resourceGroup().location]",
            "sku": {
                "name": "[parameters('appServicePlanSku')]",
                "capacity": 1
            },
            "kind": "linux",
            "properties": {
                "name": "[variables('appServicePlanName')]",
                "reserved": true
            }
        },
        {
            "apiVersion": "2019-08-01",
            "type": "Microsoft.Web/sites",
            "name": "[variables('functionAppName')]",
            "location": "[resourceGroup().location]",
            "kind": "functionapp",
            "dependsOn": [
                "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]",
                "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]"
            ],
            "properties": {
                "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]",
                "siteConfig": {
                    "appSettings": [
                        {
                            "name": "AzureWebJobsDashboard",
                            "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')),'2015-05-01-preview').key1)]"
                        },
                        {
                            "name": "AzureWebJobsStorage",
                            "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')),'2015-05-01-preview').key1)]"
                        },
                        {
                            "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING",
                            "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')),'2015-05-01-preview').key1)]"
                        },
                        {
                            "name": "WEBSITE_CONTENTSHARE",
                            "value": "[toLower(variables('functionAppName'))]"
                        },
                        {
                            "name": "FUNCTIONS_EXTENSION_VERSION",
                            "value": "~3"
                        },
                        {
                            "name": "FUNCTIONS_WORKER_RUNTIME",
                            "value": "python"
                        },
                        {
                            "name": "WEBSITE_RUN_FROM_PACKAGE",
                            "value": "1"
                        }
                    ]
                }
            }
        }
    ],
    "outputs": {
        "functionName": {
        "type": "string",
         "value": "[variables('functionAppName')]"
        }
    }
}

HttpTriggerテンプレートを生成する

テンプレートのfunctionは以下を参考にすれば簡単に作成できます。

以下コマンドでHTTP Triggerというサンプル関数を生成し、前手順で作成したFunctionApp上にデプロイできます。

func init MyFunctionProj --python
cd MyFunctionProj
func new --name HttpTrigger --template "HTTP trigger"
func azure functionapp publish <function_app_name>

Azureにpublishしたら以下のurlでアクセスができます。

<Azureポータル上で確認できるURL>/api/HttpTrigger?name=yourname

Azureであらかじめ準備されたHttpTrigger関数は動くようになりました。
しかし、自分のやりたい処理をするためにどう改造すれば良いか分からないため、さらに一歩進めてみます。

HttpTriggerテンプレートを改造してmattermost botにしてみる

azure functionを実装する上でどういう具合が良いかはAzure Functions の Python 開発者向けガイドを参照します。Mattermostに任意の文字を投稿するには以下のdriverを利用します。

matermostに任意のメッセージを投稿するコードは以下の感じです。optionsは上記driverの仕様を参照し適切なパラメータを渡します。WEBHOOK_IDはmattermostでincoming webhookを作成したときに取得できるIDを指定します。

import requests
from mattermostdriver import Driver


options = {}
WEBHOOK_ID=""
foo = Driver(options)
foo.login()
options["text"] = msg
response = foo.webhooks.call_webhook(WEBHOOK_ID, options)

上記optionsはMattermost連携時の認証情報を持つため、少し工夫をして外出ししたいと思います。12 factor appsに従い、外出しするパラメータは環境変数にします。Azure Functions での環境変数の切り替えも参照して実装を進めます。
環境変数から取得するコードはenviron.get(...)を使います。以下のような感じになります。

options = {
    'scheme': os.environ.get("MM_SCHEME", "https"),
    'url': os.environ.get("MM_HOST_OR_IP"),
    'port': int(os.environ.get("MM_PORT", "443")),
    'basepath': os.environ.get("MM_BASEPATH", "/api/v4"),
    'token': os.environ.get("MM_ACCESS_TOKEN"),
    'verify': _str2bool(os.environ.get("MM_SSL_VERIFY")),
    'channnel_id': os.environ.get("MM_CHANNEL_NAME", "test")
}
WEBHOOK_ID = os.environ.get("MM_WEBHOOK_ID")

改造するにあたってmattermost driverパッケージを追加する必要があるため以下を参考にrequirements.txtを編集します。

azure-functions
mattermostdriver
requests

サンプル関数HttpTriggerを改造した結果

前述を考慮して改造した結果、以下のコードとなります。

  • HttpTrigger/__init__.py
import azure.functions as func

import logging
import os

import requests
from mattermostdriver import Driver


def _str2bool(v):
    return v.lower() in ("true")

def _send_msg(msg):
    options = {
        'scheme': os.environ.get("MM_SCHEME", "https"),
        'url': os.environ.get("MM_HOST_OR_IP"),
        'port': int(os.environ.get("MM_PORT", "443")),
        'basepath': os.environ.get("MM_BASEPATH", "/api/v4"),
        'token': os.environ.get("MM_ACCESS_TOKEN"),
        'verify': _str2bool(os.environ.get("MM_SSL_VERIFY")),
        'channnel_id': os.environ.get("MM_CHANNEL_NAME", "test")
    }
    WEBHOOK_ID = os.environ.get("MM_WEBHOOK_ID")

    try:
        foo = Driver(options)
        foo.login()
        options["text"] = msg
        logging.info("mattemost call_webhook starting")
        response = foo.webhooks.call_webhook(WEBHOOK_ID, options)
        logging.info("mattemost call_webhook finished")
    except Exception as e:
        logging.error("Failed to call_webhook")
        raise(e)

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

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

    if msg:
        _send_msg(msg)
        return func.HttpResponse("OK", status_code=200)
    else:
        return func.HttpResponse(
             "Please pass a name on the query string or in the request body",
             status_code=400
        )

Azureにfunctionをデプロイする

function appを作成したら以下のコマンドでfunctionを発行します。
(すでに同じfunctionがあれば上書きしてくれます)。

func azure functionapp publish <function_app_name>

mattermost側の設定

mattermost側の設定は画面上から行います。
分かりやすいGUIのためメモレベルで設定方法を説明します。

ACCESS_TOKENを発行する

ACCESS_TOKENを発行するには管理者権限のユーザーでログインする必要があります。
Mattermostの左メニューから以下を押下します。

  • SystemConsole > Integration Management > Enable Personal Access Tokenstrue にします
  • SystemConsoleから離れ MainMenuのAccount Settings からSecurityを押下しPersonal Access Tokensを発行します
  • 発行したAccess Tokenを控えます

Incomming WEB_HOOKを設定する

  • MainMenuのIntegrations > Incomming Webhooks > Add Incoming Webhookでwebhookを作成します
  • webhookのURLが発行されるためもっとも末尾のランダムな文字列をWebhook IDとして控えます

bot用チャンネルtestを作成する

  • チャンネル名をtestとしてPUBLIC CHANNELSを追加します

環境変数を読み込むように改造したためfunction appに環境変数をCLIで設定する

az functionapp config appsettings set --name <APPSERVICE_NAME> -g <RG_NAME> --settings \
"MM_SCHEME=https" \
"MM_HOST_OR_IP=<mattermost host or ip ex) xx.xx.xx.xx>" \
"MM_PORT=443" \
"MM_BASEPATH=/api/v4" \
"MM_ACCESS_TOKEN=<ACCESS_TOKEN>" \
"MM_SSL_VERIFY=False" \
"MM_CHANNEL_NAME=test" \
"MM_WEBHOOK_ID=<WEBHOOK_ID>"

改造したHTTPTriggerを動作確認する

以下のURLでfunctionを実行しmattermostにメッセージをポストできます。
ただしHttpTrigger/function.jsonのauthLevelがfunctionのため、functionにアクセスするにはAPIリクエストにクエリパラメータcodeに承認キーを含める必要があります。
詳細は以下を参照します。

クエリパラメータも加味してこのURLで動作確認できます。

https://<function_app_name>.azurewebsites.net/api/HttpTrigger?msg=<mattermostにポストしたいメッセージ>&code=<host key>"

最後に

HttpTriggerをベースとしてmattermost botとして改造をしてみました。ドキュメントもCLIも充実しており慣れれば開発も進めやすいと思いました。
ただAzureポータルで参照できるfunctionの管理画面が非常にレスポンスが遅く1画面遷移するのに数秒かかるとか、pythonランタイムだとGUI上のエディタが制限であることなど色々と課題も見えましたので、そこはこれからのAzureのエンハンスに期待したいと思います。

以上

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?