yu nkt’s blog

nkty blog

I'm an enterprise software and system architecture. This site dedicates sharing knowledge and know-how about system architecture with me and readers.

SlackのSlash commandとAWSで作る猫監視アプリ

背景

AWS API Gateway, AWS SQS, Slack (file.uploadとスラッシュコマンド)で、自宅の猫を遠隔から監視するアプリを作りました。

AWSもSlack Appもほぼ初心者の自分が、数週間、隙間時間で調べながら作ってみました。 一度作れば簡単なのですが、初めてだと結構はまりどころが多かったので、今回は、作成過程とはまりどころを、丁寧に説明します。

アプリの概要

Raspberry PiにつながったUSBのWebカメラで、定期的に(1時間間隔くらい)、部屋の写真を撮ります。 撮影された写真は、Slackにアップロードされます。

さらに、Slackのスラッシュコマンドで、今の部屋の状況を撮影してアップロードすることもできます。

構成

f:id:yunkt:20200103131931p:plain
全体構成

矢印がループして見にくいですが、それぞれの矢印を解説していきます。

Java appからSlackへの矢印は、SlackにWebカメラで撮った画像をアップロードする矢印です。 アップロードされたら、Slackの指定のチャンネルに、画像が表示されます。

SlackからAWS API Gatewayの矢印は、Slackのユーザーが自作のスラッシュコマンドを入力した後にSlackからAWS API Gatewayに送信されるHTTPリクエストです。 主に、Slackユーザーが、今の猫の様子を見たい時に、このスラッシュコマンドを入力します。

AWS API GatewayからAWS SQSへの矢印は、AWS API Gatewayが、受け取ったHTTPリクエストに応じてAWS SQSに送るメッセージの流れです。

AWS SQSからJava appの矢印は、SQSのキューからJava appに送られるメッセージの流れです。 ただ、実際にはJava appからAWS SQSにロングポーリングしてメッセージを取得しにいき、キューにメッセージがあれば取り出します(取得したのち削除)。 メッセージが取り出せたら、Java appはWebカメラで写真を撮り、Slackに画像をアップロードします。

開発過程

上の構成に従って、以下の手順で開発していきました。

  • 画像を撮影する機能を作成
  • Java appからSlackに画像を送信する機能を作成
  • Slackのスラッシュコマンドを作成
  • SQSのキューを用意
  • API Gatewayを用意とSQSの連携
  • SQSのキューからメッセージを取得する機能を作成

この手順に従って、解説します。

(Java app) 画像を撮影する

まず、Webカメラで画像を撮影する処理を実装しました。 これが、意外にもしょっぱなから詰まったポイントでした。

JavaWebカメラを操作するライブラリは、Webcam Captureが有名ですが、私の開発環境のmacOS Catalinaでは、このライブラリがうまくWebカメラを認識しませんでした(参照)。 開発者の方が開発中のようですが、別の方法を検討を模索し、Webカメラでの撮影を行うコマンドをJavaから叩くことにしました。

Linuxでは、fswebcamコマンドが有名です。コマンド一発で、簡単にWebカメラで写真が撮影できます。 しかし、Macでそれに相当するのは、imagesnapコマンドです。 私の環境は、開発はMac、実稼動はRaspberry Piなので、環境によって画像の撮影の処理が異なる事態になりました。 仕方ないので、System.getPropertyメソッドで、Java app稼動中のOS名を取得して、撮影の機能を持つWebカメラの適切なインスタンスを生成するような設計になりました。

以下のように、インターフェースにファクトリメソッドを持たせて、OSに応じて適切なインスタンスを返します。

public interface Camera {

    public static Camera createCamera(){
        final String OS_NAME = System.getProperty("os.name").toLowerCase();
        switch (OS_NAME) {
            case "linux":
                return new CameraOnLinux();
            case "mac os x":
                return new CameraOnMac();
            case "windows 10":
                return new CameraOnWindows();
            default:
                throw new RuntimeException("Unsupported OS: " + OS_NAME);
        }
    }

    Photograph takePhoto() throws IOException;
}

(Slack) ファイルをSlackに送信

SlackのAPIを使って、Slackにファイルを送信します。

まずは、Slackのアプリ、というものを理解する必要があります。 Slackの一つのワークスペースには、無料だと最大10個まで、アプリがつけられます。 アプリというのは、ボットや、投票機能など、Slackの機能拡張だと思えばいいと思います。 メッセージやファイルの操作など、あらゆる機能を自分で用意したい場合は、このアプリをまずは作成し、そのアプリに機能を設定して、ワークスペースに機能追加するのが基本方針です。

Slackアプリの作成や、ワークスペースへのインストールは、こちらのページから行えます。

api.slack.com

また、公式の説明はここにまとまっています。

api.slack.com

自分はファイル(画像)のアップロードをしたいので、上のドキュメントの"Working with files"を見ていきます。 要約すると、このような手順です。

  1. Slack appを作り、ファイルの送受信に関するPermissionの設定をして、自分のworkspaceに作ったSlack appをつける
  2. files.upload APIを叩く

files.upload APIの詳しい説明は、こちらに書いてあります(Slackのドキュメントは、情報が散らばっていて、やや調べるのに苦戦しました)。

api.slack.com

まずは、上のページの"Tester"タブで、APIを試すことができるので使って見ます。 おそらく、唯一困るのは、tokenとは?だろうと思います。 これは、作成した自分のSlack appのページに行き、"Features"->"OAuth & Permissions"のページにある、"Tokens for Your Workspace"のことです。 "xoxp-"で始まる文字列です。

f:id:yunkt:20200103115622p:plain

"Tester"でうまくいったら、Postmanなどでも試し、問題なさそうであれば、Java appに実装を加えていきましょう。 自分は最初、Unirest-javaを使っていたのですが、 Slackからinvalid_form_dataが返ってきて、なぜかうまくいきませんでした。

unirest.io

Java appからcURLを叩く実装にしたところ、問題なく行けました。が、環境にcURLが存在することを前提とする構成になってしまいました…(これはリファクタリング予定)。

(Slack) Slash commandの設定

スラッシュコマンドとは、Slackで"/"から始まるコマンドを入力して、Slackに搭載された何らかの機能を呼び出せられる機能です。 例えば、"/remind"は、指定した時刻に何かをやることをリマインドしてくれる通知機能です。

まずは、Slash commandの仕組みの理解が必要です。 Slash commandを自作する場合、以下の構成になります。

f:id:yunkt:20200103130640p:plain

Slack自体は、ユーザーからスラッシュコマンドのメッセージを受け取ると、Slackとは関係ないWebアプリにHTTPのPOSTリクエストを送るだけです。 POSTリクエストのレスポンスが届いたら、Slackはその内容をスラッシュコマンドが送られたチャンネルに表示します。

開発者の作業が必要なのは、以下の2点です。

  • 自分が用意したSlack appに、Slash commandを追加し、後段のWebアプリのURLなどを設定
  • 後段のWebアプリを用意

ここまで理解できたら、作るの自体は簡単です。 さきほど作ったSlack appのページから、"Slash command"というリンクを辿り、"Create New Command"ボタンを押して、必要事項を書くだけです。

ただ、まだ後段のWebアプリができていないので、Request URLの項目は記載できません。 あとでこの設定に戻ってくることにしましょう。

ちなみに、私は以下のように、新しく"/tousatu"というスラッシュコマンドを作りました。 Slack app名の"Wakame"は、私の家の猫の名前です。

f:id:yunkt:20200103121354p:plain

AWS側の構成

後段のWebアプリは、AWS API GatewayAWS SQSで、プログラミングレスに構築しました。

AWS API Gatewayは、HTTPリクエストとWebsocketのメッセージを受け付けて、後段のAWSコンポーネントにメッセージを受け渡す、ゲートウェイです。 AWSでWebアプリケーションを作成する場合、必ず必要になります。 通常は、AWS WAFというファイアーウォールとセットで利用し、リクエストのフィルタやトラフィックの可視化を行い、セキュアに運用しますが、今回はまだ使用していません(追って使用する予定です)。

AWS SQSは、簡易的なメッセージングキューです。処理のリクエストを一時的に貯めるためのバッファと思えば良いと思います。 システムのコンポーネント間を、非同期に連携させるときに使います。

今回は、Java appは自宅のラズパイであり、グローバルIPをそのラズパイにつけたくないので、ラズパイがAWSにアクセスしてスラッシュコマンドのメッセージを取得しにいく形式にする必要がありました。 そのため、AWS SQSにいったんスラッシュコマンドのメッセージを置き、Java appはロングポーリングでそのキューを監視する形式になりました。

(AWS SQS) キューの用意

AWS SQSでキューを用意します。

AWSの構築をする際には、まず初めに、AWS IAMでユーザーを作ります。

作成するユーザ(ユーザーが所属するグループ)につける権限は、以下の通りです。

  • AmazonSQSFullAccess
  • CloudWatchEventsReadOnlyAccess

一つ目は、AWS SQSの構築をするために必要です。 二つ目は、AWS SQSのコンソール画面から、届いたメッセージ量のモニタリングを閲覧するために必要です。

では次に、AWS SQSのコンソール画面から、新しいキューを作ります(別にCLIからでもいいです)。 キューの種類には、標準キューとFIFOキューがあり、順序保証をするならFIFOにする必要がありますが、今は全くそんな要件はないので、標準キューにしました。

キューの設定で重要なのは、「メッセージ受信待機時間」を20sにしておくことです。この設定は、ロングポーリングの設定です。 キューに何もメッセージが入っていない場合、20秒間はクライアントからのリクエストに対し、レスポンスをすぐに返さず、クライアントを待たせます。 20秒の間にキューにメッセージが届いたら、即座にレスポンスを返します。

この設定をすることで、Java app側では、AWS SQSへのメッセージを無限ループのポーリングで問い合わせるだけで、キューのメッセージをリアルタイムに確認できます。 また、AWS SQSの課金形態は、この問い合わせに対して課金されるので、リアルタイムかつコストを抑える、という意味でも必須の設定です。

設定が終わったら、キューの名前(<ユーザー番号>/<キュー名>)を控えておきましょう。

(AWS IAM) API Gatewayの構築のためにIAMユーザー権限の追加

AWS API GatewayAPIエンドポイントを作り、AWS SQSのキューにメッセージを転送する設定をします。

そのために、先ほど用意したIAMユーザーに新たに以下の権限を付けます。

  • AmazonAPIGatewayInvokeFullAccess
  • AmazonAPIGatewayPushToCloudWatchLogs
  • AmazonAPIGatewayAdministrator
  • (独自ポリシー) PassRole

上三つは、API Gatewayを操作するための権限です。 四つ目は、AWS サービスにロールを渡す設定をするためのPassRoleという権限です。

PassRoleとは何か、ですが、AWS IAMには、AWSのサービスにも権限を設定でき、その権限のことをロールと言います。 AWS API GatewayはSQSのキューにメッセージを投入するためのロールを付けなければいけません。 ただ、このロールをつけるためには、その操作をするユーザーにAWS IAMの操作権限が必要です。 いちユーザーに、AWS IAMの操作権限を与えてしまったら、何でも出来ることになってしまうので危険です。 そこで、AWSのサービスにロールをつけることだけ可能にした権限が必要であり、それをPassRoleと言います。

以下のAWS IAMのコンソール画面から、ポリシーを開き、ポリシーの作成、をクリックします。

f:id:yunkt:20200103201418p:plain
AWS IAMのコンソール画面

そして、JSONというタブを開き、以下を記述してポリシーの確認をクリックし、作成します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "iam:Get*",
                "iam:List*",
                "iam:PassRole"
            ],
            "Resource": "*"
        }
    ]
}

上のポリシーでは、AWS IAMのユーザーやロールなどの情報を取得すること、その一覧を閲覧すること、PassRole、の三つを許可しています。

ユーザーに(ユーザーが所属するグループに)このポリシーを追加します。

最後に、AWS API Gatewayに付ける、AWS SQSのキューにメッセージを投入するためのロールを用意します。

AWS IAMのロールの画面から、「ロールの作成」をクリックし、このロールを使うサービスにAPI Gatewayを選択。 アタッチするポリシーにAmazonSQSFullAccessを設定します。 ロールが作成できたら、ロールの識別子である「ロール ARN」を控えておきましょう。

(AWS API Gateway) HTTPリクエストを受けてSQSにエンキュー

AWS API GatewayAPIエンドポイントを作成する手順は以下のとおりです。

  • リソースの作成
  • メソッドの作成
  • ステージの作成
  • APIのデプロイ

API Gatewayのコンソール画面を開き、リソースとPOSTメソッドを用意します。

通常、APIエンドポイントを用意したら、「メソッドリクエスト」の項目で、受け付けるリクエストのパラメータやボディのモデルなどを設定します。 しかし、今回は、「スラッシュコマンドのリクエストが来た」ことのみが重要で、リクエストの内容は全くどうでもいいので、そう言った設定は何もする必要がありません。

ポイントは、「統合リクエスト」の方です。 こちらは、AWS SQSにメッセージを送信する設定です。

まず以下のように設定します。

f:id:yunkt:20200103203640p:plain

実行ロールの項目は、上で控えておいた、ロールのARNです。

次に、URLクエリ文字列パラメータを次のように設定します。

f:id:yunkt:20200103203801p:plain

URLクエリ文字列パラメータの項目は、AWS API Gatewayが受け取ったリクエストの内容を使って(または任意の文字列を設定して)、AWS SQSへのリクエストを作成するための項目です。 AWS SQSのAPIでは、Actionパラメータで何をするかを指定します。 今回は、メッセージ送信なので、'SendMessage'とします。 'SendMessage'の場合は、メッセージのボディを設定しないといけません。 MessageBodyパラメータに、何でもいいので文字列を設定します。

AWS SQSのAPIは、こちらを確認してください。

https://docs.aws.amazon.com/ja_jp/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-making-api-requests.html

連携はこれで完了です。試しにAWS API Gatewayのコンソール画面から、テストを行えます。

あとは、ステージの作成とAPIのデプロイです。 今のままでは、AWSの外部から実際にHTTPリクエストを送信しても、AWS API Gatewayは受信できません。 それをするためには、ステージを作り(ステージとは、開発途中版なのか、リリース版なのかを表すもの)、APIがどれかのステージに属させます。

AWS API Gatewayのコンソール画面で、ステージという項目があるので、そこでステージを作成します。 自分は、適当にdevというステージを作りました。 そうしたら、リソースの画面に戻り、「アクション」のボタンから、APIのデプロイを選択して、devステージにデプロイします。

これができたら、Postmanなどで、HTTPリクエストを送り、AWS SQSのキューにメッセージが届くか確認してみましょう。 問題なさそうであれば、Slackのスラッシュコマンドの設定に戻り、APIのURLをスラッシュコマンドの設定画面に記載します。

ちなみに、APIのURLは、AWS API Gatewayのステージのページに記載されています。 ステージのURLの末尾に、リソースのパスを追加すればOKです。

余談ですが、自分のIAMの設定では、AWS API Gatewayのリソースの設定時に、以下のようなエラーメッセージが出ます。 ただ、特に問題なく操作ができます。 フル権限のAdministratorでは、このメッセージは出ないので、何かIAMの権限が足りていないと思いますが、謎です。

f:id:yunkt:20200103202129p:plain

(Java app) AWS SQSへのポーリング

あとは、コーディングだけです。 AWS SDKは、バージョン1と2がありますが、せっかくなので2を使いましょう。

AWS SQSへリクエストをする処理は、こちらが参考になります。

Gradleを使っているなら、build.gradleの設定はここに書かれています。

https://docs.aws.amazon.com/ja_jp/sdk-for-java/v2/developer-guide/setup-project-gradle.html

また、AWS SQSの利用方法は、コード例の章に書かれています。

https://docs.aws.amazon.com/ja_jp/sdk-for-java/v2/developer-guide/sqs-examples.html

あとは、このドキュメントに基づいて、粛々とコードを書くのみです。

一応、AWS SDK全般の話として、APIを呼ぶためには、アプリを動かす環境に、AWSの認情報を設定する必要があるので、それをまずは行いましょう。 方法は5種類あり、こちらのドキュメントにまとまっています。 もしラズパイで、AWS SDKを使うアプリを複数同時に動かす予定がなければ、環境変数に設定する方法か、aws configureコマンドでデフォルトを設定してしまうのが手っ取り早いかなと思います。

https://docs.aws.amazon.com/ja_jp/sdk-for-java/v2/developer-guide/credentials.html

あと、私が少しハマったポイントですが、AWS SDKのバージョン2は、Gradleのバージョンが5以上でしか使えません。 このネコ監視アプリ特有の話ではありませんが、Intellijで開発している場合、利用するGradleを適切に設定しないと、Gradleのbuild時に、以下のエラーが出ます。

> Could not find method platform() for arguments [software.amazon.awssdk:bom:2.5.29] on object of type org.gradle.api.internal.artifacts.dsl.dependencies.DefaultDependencyHandler.

おそらくですが、Intellijの設定画面にある、"Use Gradle from"の項目に、"'gradle-wrapper.properties' file"という選択肢がありますが、これがGradleの5未満のバージョンを利用する設定になっており、そうすると上のエラーが出るようです。 自分は、Gradle 6.0をインストールし、上記の項目を"Specified Location"という選択肢にして、Gradleの場所(/usr/local/Cellar/gradle/6.0/libexec)を設定して対処しました。

f:id:yunkt:20200105190623p:plain
Intellijのgradle設定画面

おわりに

この記事を書くのも疲れるほど、細かい作業、ハマりポイントが多くありましたが、一度分かれば簡単です。 Slackを使ったアプリや、ポーリングで処理リクエスを取得しにいって処理するシステム構成は、汎用性が高いと思いますので、是非覚えたいところです。

あと、今回はAWS SQSを利用しましたが、もっと高機能なメッセージングキューに、AWS MQというサービスがあります。 ただ、このサービスはEC2のように、あらかじめ用意したキューのリソース量と、そのキューを確保している時間で課金されるので、今回のような自分しか使わないお手軽アプリには、コストがかかりすぎます。 そのため、今回はAWS SQSを利用しました。

もう一つ余談ですが、Slackのスラッシュコマンドを入力すると、このようなメッセージが表示されます。

f:id:yunkt:20200103212240p:plain

これは、AWS API GatewayからAWS SQSへのPOSTリクエストのレスポンスが、そのまま返されているのだと思います。 機能的には別に問題ではありませんが、やや無骨なので、レスポンスに手を加える方法を模索中です。

あと、まだ手順に書き漏れがある気がしますが、何か疑問があればコメントください。