Azure Functionsを使ってSlackチャットサービス自動応答ツールを作ってみた

2023年1月31日掲載

キービジュアル

Azure Functionsを使ってSlackチャットサービス自動応答ツールを作ってみました。

目次

  • Azureのサーバレスサービス Azure Functions を使って、Slack チャットサービス自動応答ツールを作る方法を解説します
  • ステップバイステップの解説記事です。
  • はじめてFunctionsを触る人も分かるように出来るだけ丁寧に解説しました

はじめに

サーバレスサービスは非常に便利です。
仮想コンピューティングを準備しなくても、開発者は好きな言語による関数コードを綴るだけで、サーバレスがHTTP Requestやタイマートリガー、APIアクセス等に応じて自動処理し、その結果を返答するため、仮想コンピューティングのランニングコストを削減しつつ、さまざまな組み合わせやシナリオを実現することができます。

今回はそのシナリオの一つとして、Azure のサーバレスプロダクトサービス Azure Functionsがタイマートリガーをベースに外部からJSONデータを取得、その結果をSlackへ通知する仕組みを作ってみます。

注意:本記事はコンテナを使ったデプロイとなります。

1. 全体構成図

全体構成図は次の通りになります。ゴールは一定時刻になると天気予報を通知するものです。

操作画面

Azureの場合、ローカルにてAzure Functions Appを作成したら、それをコンテナイメージとしてAzure Functionsへデプロイする流れになります。

2. 参考:Slack - サーバレスでできることの例

上記は非常に簡単な例ですが、この流れから基本を抑えておけば、次のようなシナリオを実現することができます。

・Stable Diffusionを使って、チャットでBotが画像を生成して返却するサービス

・家計簿DWHへSQLクエリで取得、およびGoogle spreadsheetsと連携してChart自動作成・表示

・レシートをGoogle spreadsheetsの家計簿へ自動反映の最中に異常検知アラート発信

・クラウドサービス等の料金通知

Slackは対話型チャットベースでさまざまなサービスと連携できるため、上記を含め、さまざまなシナリオを簡単に実現することができます。コツは、Slack入力を検知するPythonスクリプト、およびSlackへ通知するPythonスクリプトを上手く生かすことです。今回はその一歩として、サーバレスからPythonスクリプトによって通知する簡単な仕組みを作ってみます。

3. Slackの設定

今回はSlack上にBotを作成しますので、Slackアカウント、利用したいサービスのワークスペース、およびslack連携には Slack AppとIncoming webhook URL の発行が必要です。
もし、Slackアカウント、利用したいサービスのワークスペース、Slack AppとIncoming webhook URLを持っている場合は、「4. Docker Desktopの準備」へお進みください。

3-1. Slackアカウント&ワークスペース作成

Slackアカウントを準備します。
https://slack.com/

Slackアカウントの準備が出来たら、以下のリンクを開き、ワークスペースを作成します。
https://slack.com/get-started#/createnew

今回はワークスペース名を「ServerlessToSlack」という名前にします。

表示された指示に従い、ワークスペースを作成し、起動します。

チャンネルを作成します。チャンネル名は何でもよいので、今回は「weatherchannel」にします。

3-2. Slackアプリ設定

ワークスペースが完成したら、今度はそのワークスペースとFunctionComputeを連携するための、Slackアプリ設定をします。

https://api.slack.com/apps にアクセスし、「Create an APP」をクリックしてSlackアプリを作成します。

「From scratch」をクリックします。

AppNameを設定し、ワークスペースを選択します。

「Incoming Webhooks」を選択します。

「Incoming Webhooks」をオンにします。

「Incoming Webhooks」をオンにすると、メニューの表示内容が変わります。

Webhooksをワークスペースに追加します。

チャンネルを選択し、「許可する」ボタンをクリックして許可します。

Incoming Webhooksを保存します。

これでSlack App設定は完了です。

最後に、Slack App設定にてWebhook URLがありますが、こちらはサーバレス側から通知するために必要なURLなので手元に置くようにします。

4. Docker desktopの準備

ソースコードをDockerコンテナイメージでAzure Functionsへデプロイするためのコンテナイメージを構築するために、Dockerの作業環境を準備します。

今回コンテナイメージでデプロイする際、Docker Hub上のリポジトリとコンテナイメージを利用するためにDocker IDを持つことが前提となるため、もしDocker IDを持っていない場合は、Docker Hubから新規登録する必要があります。

関連記事リンク

Dockerは複数のプラットフォームをサポートしており、公式サイトよりインストールガイドラインに沿ってインストール、Docker作業環境を構築することができます。

著者はWindows PCを使用しているので、Docker Desktop for Windowsを使用します。

Dockerの作業環境が構築できたら、ターミナルにてDockerの操作ができるようになります。
ここで、手持ちのDocker IDアカウント情報を使って`docker login` でDockerにログインします。

5. Azure CLIとAzure Functions Core Toolsのインストールと設定

Azure CLIコマンドツールとAzure Functions Core Toolsを導入します。
Azure CLIコマンドツールおよびAzure Functions Core Toolsを使用するには、それぞれのガイドラインに従ってインストールし、設定します。著者はWindowsPCを使用しているので、Windowsを例に説明します。

インストールが完了したら、`az --version``function --version`を実行してインストール状況と関連バージョンを確認します。

コマンド`func`で直接Azure Functions Core Toolsのヘルプ情報を確認することもできます。

操作画面

`az login`コマンドでアカウント資格情報を設定します。デフォルトのWebブラウザが開き、Azureサインインプロセスに入ります。作業環境にWebブラウザがない場合は、代わりに`az login --use-device-code`を使用することもできます。

6. Slackと連携するPythonコードの準備

Slack、Azure CLI、Azure Functions Core Tools側の準備が完了したので、今度はPython関数コードの準備をします。Python仮想環境を使用している場合は、最初に仮想環境を作成してアクティブ化することができます。

azure-docker-slackという名前のプロジェクトフォルダを作成します。

次のコマンドを実行して、.view仮想環境を準備します。

py -m venv .venv

次のコマンドを実行して、.view仮想環境をアクティブにします。

.venv\scripts\activate

ローカルプロジェクトを初期化します。

func init --worker-runtime python --docker
操作画面

Azure Functions Appとして実行するために、 `ohara-serverless-slack` という名前の関数フォルダを作成し、その中にpythonスクリプトファイル`__init__.py`と設定ファイル`function.json`を作成します。

ローカルプロジェクトの現在の構造は以下のようになります。

.
├── host.json
├── local.settings.json
├── Dockerfile
├── requirements.txt
└── ohara-serverless-slack
├── __init__.py
└── function.json

pythonスクリプトファイル`__init__.py`のソースコードは次の通りです。

import datetime
import logging
 
import azure.functions as func
 
import urllib3
import json
 
http = urllib3.PoolManager()
 
slack_endpoint = "<Slack App設定から入手したWebhook URL>"
weather_endpoint = "https://www.jma.go.jp/bosai/forecast/data/overview_forecast/130000.json"
 
def weather_handler():
    # get weather result
    weather_result = get_weather()
    if weather_result["success"]:
        # compose weather info to markdown message
        weather_msg = compose_weather_message(weather_result["data"])
        forward_slack_message(weather_msg)
    else:
        weather_msg = weather_result["error_message"]
    forward_slack_message(weather_msg)
 
 
def forward_slack_message(message_content):
    msg = {
        "channel": "#weatherchannel",
        "username": "nancy",
        "text": message_content,
        "icon_emoji": ""
    }
    encoded_msg = json.dumps(msg).encode('utf-8')
    resp = http.request('POST', slack_endpoint, body=encoded_msg)
    # print({
    #     "message": "test message: {0}".format(message_content),
    #     "status_code": resp.status,
    #     "response": resp.data
    # })
 
 
def compose_weather_message(weather):
    return "*{0}  _{1}_  {2} 地域の天気情報です*\n{3}\n{4}".format(
        weather['publishingOffice'],
        weather['reportDatetime'],
        weather['targetArea'],
        weather['headlineText'],
        weather['text']
    )
 
 
def get_weather():
    result = {"success": True, "error_message": "OK"}
    try:
        resp = http.request(method="GET", url=weather_endpoint, headers={"Content-Type": "application/json"})
        if resp.status == 200 and resp.data:
            weather = json.loads(resp.data)
            # print(weather)
            result["data"] = weather
        else:
            msg = "Failed get weather, response code: {0}, response: {1}".format(resp.status, resp)
            print(msg)
            result["success"] = False
            result["error_message"] = msg
        return result
    except Exception as e:
        result["success"] = False
        result["error_message"] = str(e)
        print("Failed getting weather information.")
    return result
 
 
def main(mytimer: func.TimerRequest) -> None:
    utc_timestamp = datetime.datetime.utcnow().replace(
        tzinfo=datetime.timezone.utc).isoformat()
 
    if mytimer.past_due:
        logging.info('The timer is past due!')
 
    logging.info('Python timer trigger function ran at %s', utc_timestamp)
    weather_handler()

function.jsonは、トリガ定義を含む関数の設定です。今回はタイマートリガーなので、毎日日本時間で朝9時に実行するように設定します。AzureのタイマートリガーのCronはUTC 指定となりますので、JST で指定する場合には 9 時間を差し引いた時刻を指定する必要があります。今回は毎日 9:00:00 JST に実行するとして、以下のように設定しています。

{
    "scriptFile": "__init__.py",
    "bindings": [
    {
      "name": "mytimer",
      "type": "timerTrigger",
      "direction": "in",
      "schedule": "0 0 0 * * *"
    }
    ]
}

今回の関数コードにはHTTP通信用ライブラリのrequestsと、Slack APIを介してSlackにメッセージを送信するためのslackwebという名前のサードパーティパッケージが必要なので、生成されたばかりのrequirements.txt に依存関係の情報を更新します。

azure-functions
slackweb==1.0.5
requests==2.22.0

7. カスタムコンテナイメージの作成

Azure Functions appの準備ができたら、ローカルプロジェクト配下にあるDockerfileを確認します。

下記コマンドでカスタムコンテナイメージをビルドします。

docker build --tag <ユーザ名>/azure_functions_slack:1.0 .

Docker DesktopのLocalにてコンテナイメージのビルド結果としてカスタムコンテナイメージがあることを確認します。

今度はこのカスタムコンテナイメージを以下のコマンドでAzure Functionsへプッシュします。

docker push <ユーザ名>/azure_functions_slack:1.0

Azure Functionsへのプッシュが完了したら、Docker DesktopのLocalにあるRemote Repositoriesでイメージを確認します。

8. カスタムコンテナイメージでAzure Functionsの作成

Docker Desktopでカスタムコンテナイメージの準備ができたら、今度はコマンドを使ってAzure Functionsへデプロイの作業に入ります。まずは以下のコマンドでリソースグループを作成します。今回は「oharanancy-rg」という名前で作成します。

az group create --name oharanancy-rg --location japaneast

リソースグループが出来たら、リソースグループ配下に以下のコマンドでストレージアカウントを作成します。ストレージアカウントは、Azure Storageデータオブジェクトをグループ化するためのコンテナです。


<参考>
ストレージアカウントの概要
ストレージ アカウントとは

az storage account create --name oharanancysac --location japaneast --resource-group oharanancy-rg --sku Standard_LRS

ストレージアカウントの作成が完了したら、今度は以下のコマンドでplanを作成します。

az functionapp plan create --resource-group  oharanancy-rg --name oharanancyplan --location japaneast --number-of-workers 1 --sku EP1 --is-linux

planが作成完了したら、Function Appを作成します。

az functionapp create --name ohara-nancy-slack --storage-account oharanancysac --resource-group oharanancy-rg --plan oharanancyplan  --functions-version 4 --deployment-container-image-name nancyli2017/azure_functions_slack:1.0

ここまでのコマンドが問題なく順調であれば、Azureコンソール側でFunction Appを確認します。Function App画面からアプリ名などを確認することができます。

9. Azureコンソール上でAzure Functions Appを確認

Azure Functions側にアプリが無事生成できたので、コードを手動で実行しながらテストします。
対象のアプリを選定しながら、Code + Test 画面へ遷移します。

Code + Test 画面にて、上記手順通りにLocalで開発したコード内容を確認します。

コード内容に問題がなければ、Code + Test画面のメニューバーにある、Test/Runボタンをクリックします。

その結果、手動実行によるDeploy結果として、Slack側で天気予報が通知されるようになります。

10. Azure Functionsスケジュール設定

Azure Functions Appを手動テストしたところ、無事slack通知ができるようになったので、今度はAzure FunctionsのTimer Trigger機能のスケジュール設定で、自動的にSlack通知するようにします。これはもともと先述通り Dockerコンテナイメージを作成する過程にて、function.jsonでタイムスケジュールを設定しているので、Azure Functionsコンソール側での追加設定は不要となります。

なお、Azure Functionsコンソールの Code + Test画面にて、function.jsonの設定内容を確認することはできます。

この設定が問題なければ、Slackで定期的に天気予報通知が届くようになります。

11. さいごに

SlackとAzure Functionsの組み合わせにより、天気予報情報を自動で通知する方法をご紹介しました。Azure Functionsの場合、ローカルでDockerコンテナイメージを併用してAzure Functionsアプリらコードを作成するため、初めての方には不慣れなところもありますが、実はVisual Stdio Codeと併用してAzure Functionsアプリを開発したほうがスムーズです。

本記事は非常に簡単かつ10分もしないで作り上げることができるので、サーバレスをはじめる方にはちょうどよいハンズオントレーニングかもしれません。また上記関数コードは天気予報情報を取得して通知する処理ですが、このコードをアレンジして、ニュースを通知したり、近所スーパーのセール情報を通知したり、クラウドの障害を通知したり、などなど、さまざまなシナリオを満たすことができますので、ご参考になれば幸いです。

関連サービス

Microsoft Azure

Microsoft Azureは、Microsoftが提供するパブリッククラウドプラットフォームです。コンピューティングからデータ保存、アプリケーションなどのリソースを、必要な時に必要な量だけ従量課金で利用することができます。

MSPサービス

MSP(Managed Service Provider)サービスは、お客さまのパブリッククラウドの導入から運用までをトータルでご提供するマネージドサービスです。

おすすめの記事

条件に該当するページがございません