LoginSignup
3
1

More than 3 years have passed since last update.

AWS Lambda + DynamoDB + CloudWatchでQiitaにストックした記事をSlackに定期通知する

Last updated at Posted at 2020-06-07

概要

  • AWSサービスを触って学びたい
  • 毎週土曜日にメールで通知されるQiitaの「今週ストックした記事」をSlackに通知したい

使用するもの

サービス 用途
AWS Lambda Qiita APIからストックした記事を取得してSlackに通知する用
Amazon DynamoDB Qiita APIから取得した記事には「いつストックしたか」の情報がないため通知情報が重複してしまう。これを防ぐために前回通知した情報を保持しておく
Amazon CloudWatch Lambdaの定期実行用
AWS SAM(サーバレスアプリケーションモデル) ローカルで構築したアプリケーションを各サービスにデプロイする用
Qiita API ストックした情報を取得する用

全体図

↑をまとめると以下のようになります。
Slack App (1).png

アジェンダ

0. 事前準備
1. テンプレートを作成
2. SAMを用いてデプロイ
3. Slackに通知
4. Qiita APIを使用してストック情報をSlackに通知
5. DynamoDBを使用して通知履歴の管理
6. CloudWatchで任意の時間にLambdaを定期実行させるようにする

0. 事前準備

AWS

以下が未作成、未インストールの場合は適宜行います。

Slack

通知させたいチャンネルにwebhookアプリをインストールして動作確認しておきます。
参考: SlackのWebhook URL取得手順 - Qiita

実装

1. テンプレートを作成

samコマンドでテンプレートを作成します。

$ sam init -n qiita-stocks-notifier -r ruby2.5
オプション 説明
-n, --name TEXT initコマンドで作成されるプロジェクトの名前。
-r, --runtime ランタイムの指定。今回はrubyにしました。

参考: sam init - AWS サーバーレスアプリケーションモデル

コンソール内で聞かれる質問はデフォルトを選択しました。
コマンドが完了すると以下の構造のテンプレートがディレクトリ配下に作成されています。

$ tree
.
├── Gemfile
├── README.md
├── events
│   └── event.json
├── hello_world
│   ├── Gemfile
│   └── app.rb
├── template.yaml
└── tests
    └── unit
        └── test_handler.rb

hello_world/app.rb の中身はメッセージを出力するだけになっています。

2. SAMを用いてデプロイ

テンプレートが作成出来たのでLambda上にデプロイしてみます。
参考: チュートリアル: Hello World アプリケーションのデプロイ - AWS サーバーレスアプリケーションモデル

$ sam build
$ sam deploy --guided
# 質問に回答していきます。

デプロイが正常に完了するとコンソールに文字列が出力され流ことが確認出来ます。

 {"message": "hello world"}

Lambdaの管理画面にデプロイしたアプリケーションが反映されていることを確認します。
ダウンロード (2).png

3. Slackに通知

hello_world/Gemfileslack-notifierを追加してbundle install します。
stevenosloan/slack-notifier: A simple wrapper for posting to slack channels

gem "slack-notifier"
$ bundle

app.rbを以下のようにslack-notifierにHooksURLを渡して初期化し、テスト文言を送信するようにしてみます。

app.rb
require "json"
require "slack-notifier"

def lambda_handler(event:, context:)
  notifier = Slack::Notifier.new "https://hooks.slack.com/services/**/**" do
    defaults channel: "#qiita-slacks-notification"
  end
  notifier.ping "yeah"
end

3.1 デプロイして動作確認

修正が完了したらビルド&デプロイして動作確認します。

$ sam build
$ sam deploy

Lambdaの管理画面にて「テストイベントの選択」 -> 「テストイベントの設定」を選択します。
スクリーンショット 2020-06-07 5.37.06.png

作成したアプリケーションへのパラメータを選択する画面になりますが、今回はパラメータによって挙動を変えるものではないので
デフォルトのままにしておきます。
スクリーンショット 2020-06-07 5.38.52.png

「テストイベントの設定」が完了したら「テスト」を選択し、Slackにメッセージが送信されることを確認します。
スクリーンショット 2020-06-07 1.02.08.png

これでLambdaからSlackへの疎通確認は出来ました。

4. Qiita APIを使用してストック情報をSlackに通知

続いてQiita APIからユーザーのストックを取得して通知するようにします。

4.1 APIドキュメントの確認

ユーザーのストックした記事を取得するAPIはこちらにありました。
Qiita API v2ドキュメント - Qiita:Developer

外部APIとのやり取りを楽に行うために Faraday gemを入れておきます。
参考: lostisland/faraday: Simple, but flexible HTTP client library, with support for multiple backends.

gem "faraday"
$ bundle

4.2 通知部分の実装

実装部分の概要は以下になります。

  • Faraday を使用してAPIからストック情報を取得
  • レスポンスを タイトルURL 毎に配列にまとめてSlackに通知
app.rb
require 'json'
require 'faraday'
require 'slack-notifier'

USER_ID = "{自分のユーザーID}".freeze

def lambda_handler(event:, context:)
  # APIからストック情報を取得
  response = Faraday.get("https://qiita/api/v2/user/#{USER_ID}/stocks", page: 1, per_page: 10)

  # タイトルとURLを配列にまとめる
  messages = JSON.parse(response.body).each_with_object([]) do |res, ary|
    ary << [res["title"], res["url"]]
  end

  notifier = Slack::Notifier.new "https://hooks.slack.com/services/**/**" do
    defaults channel: "#qiita-slacks-notification"
  end

  notifier.ping "先週Qiitaでストックした記事を流すよ"
  messages.each do |message|
    notifier.ping message[0]
    notifier.ping message[1]
    notifier.ping "- - - - - - - - - - - - - - - - - - - - "
  end
end

修正が完了したら、ローカルでもアプリのテストが出来るsam local invokeを使用して動作確認してみます。

sam local invoke --no-event
オプション 説明
--no-event 空のイベントを使用して関数を呼び出します。パラメータを渡して確認する場合は別のオプションを使用します。

参考: sam local invoke - AWS サーバーレスアプリケーションモデル

コマンドを実行するとSlackにストック情報が送信されていることを確認出来ます。

スクリーンショット 2020-06-07 2.08.04.png

5. DynamoDBを使用して通知履歴の管理

通知部分は出来ましたがこのままですと毎回同じ情報が流れてくる可能性があるので、一度通知した記事は通知させないようにしたいです。
DynamoDB を用いて通知情報の登録と、データベース内にある情報は通知させないようにします。

DynamoDBとのデータのやり取りは aws-record gemを使用します。
aws/aws-sdk-ruby-record: Official repository for the aws-record gem, an abstraction for Amazon DynamoDB.

5.1 実装

template.yml

AWSTemplateFormatVersion: '2010-09-09'
...
Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: ruby2.5
      Policies:
+     # DynamoDBへCreate/Read/Update/Deleteする権限を付与
+     - DynamoDBCrudPolicy:
+         TableName: !Ref NotifiedMessageDDBTable
+     # 環境変数
+     Environment:
+       Variables:
+         DDB_TABLE: !Ref NotifiedMessageDDBTable
      Events:
        HelloWorld:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /hello
            Method: get
+ NotifiedMessageDDBTable:
+   Type: AWS::Serverless::SimpleTable
...
app.rb
require 'aws-record'
require 'json'
require 'faraday'
require 'slack-notifier'

USER_ID = "{自分のユーザーID}".freeze

class NotifiedMessage
  include Aws::Record
  set_table_name ENV['DDB_TABLE']
  string_attr :id, hash_key: true
  string_attr :title
  string_attr :url
end

def lambda_handler(event:, context:)
  # 通知済みの情報を取得
  already_notified_messages = []
  NotifiedMessage.scan.each do |notified_message|
    already_notified_messages << notified_message.url
  end

  # APIからストックした記事を取得し、未通知のもののみを配列にまとめる
  response = Faraday.get("https://qiita.com/api/v2/users/#{USER_ID}/stocks", page: 1, per_page: 10)
  messages = JSON.parse(response.body).each_with_object([]) do |res, ary|
    ary << [res["title"], res["url"]] unless already_notified_messages.include? res["url"]
  end

  notifier = Slack::Notifier.new "https://hooks.slack.com/services/xx/xx" do
    defaults channel: "#qiita-stocks-notification"
  end
  notifier.ping "今週ストックした記事はなかったよ" and return if messages.size.zero?

  # Slackに通知とデータベースに保存
  notifier.ping "先週Qiitaでストックした記事を流すよ"
  messages.each do |message|
    notifier.ping message[0]
    notifier.ping message[1]
    notifier.ping "- - - - - - - - - - - - - - - - - - - -"

    notified_message = NotifiedMessage.new(id: SecureRandom.uuid, title: message["title"], url: message["url"])
    notified_message.save!
  end
end

これをデプロイしてテストすると、DynamoDBの管理画面にて追加したテーブルの確認、また最新のストック情報が10件挿入されていることが出来ます。

スクリーンショット 2020-06-07 3.05.25.png
ダウンロード (3).png

6. CloudWatchで任意の時間にLambdaを定期実行させるようにする

最後に、毎週いい感じの時間にSlackへの通知が来るようします。

6.1 スケジュールの設定

トリガーを追加 を選択し、検索から「CloudWatch Events/EventBridge」を選択すると実行に関するルールを設定出来るので以下のように設定しました。
スクリーンショット 2020-06-07 4.29.19.png

スクショが漏れていましたが、スケジュール式の箇所は正しくはcron(0 12 ? * SAT *)(毎週土曜12時)で設定しました。
参考: Rate または Cron を使用したスケジュール式 - AWS Lambda

CloudWatchからschedulerを確認すると、以下のように毎週土曜日12時に実行されることが確認できます。
スクリーンショット 2020-06-07 4.34.54.png

まとめ

これで毎週Qiitaからのストック通知を受け取ることが出来ました。
しかしながら、このままですとDynamoDBの読み込み数が膨れ上がっていきお財布がピンチになるのでscanqueryを渡して取得する情報を絞り込む必要があります。
AWSのサービスは触ってみて始めて分かることも多いので、とても勉強になりました。

間違い等ありましたらご指摘頂けると幸いです。

参考記事

公式ドキュメント

参考にさせて頂いたブログ

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