はじめに
六木本未来ラボとして技術書典6に初出典するのですが、
当日はメンバーがローテーションを組んで販売ブースに立つことになりました。
この六木本未来ラボ、初出典にもかかわらず300冊も刷ってしまいドキドキなため販売ブースにいない間も、
メンバー間で本の売れ具合を共有したいということで
先日、メンバーの一人@wpuzzle_minegishiさんが「売れたら押してね」とIoTボタンを押したらSlackに通知するシステムを作りました。
その後、「どうせなら自分たちの爆死具合を公表したいよね」からの、「誰でも使えるようにしたほうが楽しいよね?」
ということで、突貫作業でWEBサービスを作っちゃいました。
サービス名は「URETAYO(仮)」で、現在アルファ版として公開中していますので
ご自由にお使いください!(技術書典終了後、どこかのタイミングで閉じると思います)
サービスURL
https://dm1ba20lvc4ut.cloudfront.net/
このサービスのソースコードは以下のリポジトリで公開しています。
https://github.com/takaaki-s/uretayo
また、お試し用アカウントも作成しましたので、
こちらもご自由にお試しください。
ID: guest
パスワード: guestguest
利用イメージ
動作イメージ
利用技術
AWSフルマネージドのサーバーレス構成にしています。
また、実運用でも耐えうるようなセキュリティレベルを目指してみました。
AWSリソース
ServerlessFrameworkでプロビジョニングします。便利。
- Cognito
- SQS
- DynamoDB
- Lambda
- IoT Core
- S3
- KinesisFirehose
- Athena
- CloudFront
フロントエンド側
- AWS Amplify Framework
- React
- Typescript
- Semantic UI React
構成図
処理フロー
ユーザー認証
はじめに閲覧者がURETAYOサイトにアクセスすると、
Cognito IdPoolから未認証ユーザー用のIAMロール(一時トークン)が払い出されます。
また、Cognito UserPoolへログインすると、IdPoolから今度は認証ユーザー用のIAMロールが付与されます。
AmplifyFrameworkではCognitoの認証状態に応じて閲覧できるページを制御しています。
セキュリティポイント
AmplifyFrameworkはフロント側(ブラウザ)で実行されるJavascriptなので、簡単に改変することが可能です。
閲覧ページの制限ももちろん解除可能ですが、AWSのIAMロールでAWSリソースの権限管理をしているので
Javascript側を改変してもAWSのリソースを変更することはできないようになっています。
また、一時トークンを利用されたとしても影響範囲はユーザーの権限範囲にのみ限定されます。
ユーザーの権限
上記の一時トークンの不正利用時の影響範囲は、ユーザーの権限範囲に限定されるのですが
逆に言えば、ユーザーに大きな権限を与えているとそれだけ影響範囲が広くなるので、なるべく権限を絞ります。
このサービスでは認証ユーザーに以下のような権限を付与しました。
- S3への書き込み権限(ユーザーIDプレフィックス付きのディレクトリ以下のみ)
- SQSヘのメッセージ送信権限
- IoTポリシーのアタッチ
Cognitoについて
Cognitoには2つの役割があり、
一つは、ユーザーを識別して一意なIDの付与と、認証状態に応じて一時的なIAMロールを付与するIDプール、
もう一つはユーザーの認証を行うユーザープールです。
ユーザープールは認証プロバイダの一つでもあります。
IDプールは認証プロバイダと連携して認証状態を判断します。
認証プロバイダはGoogleやFacebook等の外部の認証プロバイダを利用することも可能です。
売れた通知の仕組み
SQSへメッセージを送信すると裏でLambdaが動き以下の処理が行われます。
- Jwtの検証
- DynamoDBのカウントアップ
- Firehoseへのログ送信
- MQTTへのパブリッシュ
認証ユーザーにはSQSへのメッセージ送信権限が付与されているので、
売れたボタンをトリガーにして、SQSへ下記のJSON形式でメッセージを送信します。
{
"jwt": "{CognitoのアクセスIDトークン}",
"book_id": "本のID"
}
送信されたメッセージはSQSの裏でLambdaが受信して、DynamoDBテーブルの該当の本の売れた数をカウントアップし
ログをFirehoseに送信して、MQTTのTopicに下記のJSON形式でPublishします。
{
"count": "{売れた数}",
"book_id": "本のID",
"sub": "ユーザーの一意ID"
}
セキュリティポイント
認証ユーザーであればSQSへ自由にメッセージが送信可能で、一見セキュリティリスクになりそうですが、
メッセージに含まれたjwtの検証を行うことで、送信したユーザーの特定を行い
その特定されたユーザーが持つ本IDのカウントアップを行うようにしています。
例え第三者がjwtを手に入れ、でたらめなメッセージを送信したとしても、最悪で影響範囲はそのユーザーのみに限定されます。
売れた通知の受信と画面への反映
サークル一覧画面や頒布物画面では、
AmplifyFrameworkのPubSubでMQTTのトピックをサブスクライブしていて、
受信したデータを元にリアルタイムに画面に反映しています。
はまりどころ
AmplifyのPubSubですがAmplifyのドキュメント通りにしても
下記のエラーが出てうまくサブスクライブできませんでした。
{invocationContext: undefined, errorCode: 8, errorMessage: "AMQJS0008I Socket closed."}
これは、Amplifyのドキュメントで指定しているIoTポリシーでは権限が足りないのと、
そのIoTポリシーにユーザーをアタッチしていないためでした。
ですので、IoTポリシーを下記のように定義して
IoTPolicy:
Type: AWS::IoT::Policy
Properties:
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- iot:Receive
- iot:Connect
- iot:Subscribe
Resource:
- "arn:aws:iot:${AWS::Region}:${AWS::AccountId}:topic/{トピック名}/*"
- "arn:aws:iot:${AWS::Region}:${AWS::AccountId}:topicfilter/{トピック名}/*"
- "arn:aws:iot:${AWS::Region}:${AWS::AccountId}:client/*"
IoTポリシーへのアタッチと、トピックのサブスクライブは
こんな感じで処理してうまく動くようになりました。
import { Iot } from "aws-sdk";
import { Auth, PubSub } from "aws-amplify";
import Config from "../config";
const iotSubscribe = async (topic: string, cb: any) => {
const credentials = await Auth.currentCredentials();
const iot = new Iot({
region: Config.region,
credentials: Auth.essentialCredentials(credentials)
});
const policyName = Config.iotPolicyName;
const target = credentials.identityId;
const { policies } = await iot.listAttachedPolicies({ target }).promise();
if (policies && !policies.find(policy => policy.policyName === policyName)) {
await iot.attachPolicy({ policyName, target }).promise();
}
return PubSub.subscribe(topic, {}).subscribe({
next: cb,
error: error => console.error(error),
complete: () => console.log("Done")
});
};
export default iotSubscribe;
さいごに
これを作ろうとなったのが1週間前で、大丈夫だろうとタカをくくっていたのですが
完全に見積もりが甘すぎて、もうこんなギリギリに。。。
Firehoseのログをサークル詳細画面にグラフ表示をしようとか、
いろいろ考えたりしたのですが、全然間に合いませんでした!
ぱっと見わかりにくさ満点のサービスですが、実際に触って確かめてみていただけると嬉しいです。
それから、バグを発見したらそっと教えてください。
われわれ六木本未来ラボは「え28」で技術書典初参加させていただきます。
はじめての技術書典でドキドキしていますが、
参加サークルのみなさま、よろしくお願いします。