Twitterのフォロワー数の推移を記録&取得するAPIをサーバーレスで作った
作ったもの
Twitterのフォロワー数の遷移を記録、取得するAPIをサーバーレス構成で作ってみました🐤
以下のページでAPIを叩いて、chart.jsで表示してみています(表示の雑さはご愛嬌)
要件は以下です^^
- 対象のユーザーを登録
- 登録したユーザのプロフィール情報を一定間隔で保存する
- ユーザーごとのプロフィール情報を時系列順で取得する
ソースコードのリポジトリ(API部分のみ)
モチベーション
機能面
好きな芸能人アカウントのフォロワーの伸びを見届けたいというのが作りたいと思った理由です。要は推しを成長を見守りたいってことです^^。他人のアカウントのフォロワーの推移を確認する無料サービスは、ツイプロやSOCIAL BLADEなどがあるようですが、データの保存期間に制限があったため自作に至りました。
一応サイトを公開してはいますが、自分以外が使う想定もしていないためデータ量が膨れ上がるという心配も特にしていないです。
技術面
個人的な趣味開発のため、なるべくお金は払いたくありません。したがってクラウドサービスの無料枠を組み合わせながら構築することになります。
上記の記事を参考にした上で、以下の点を考慮しました。
- 自分しか使わないので、常時サーバーが立ち上がってるのはコストが高い
- 定期実行処理があるためサーバーレスの実行環境で手軽に実装できそう
- 使い慣れてるAWSであること
結果、Lambda + DynamoDB + API Gatewayを使うことに決めました。
さらに以下の理由からServerless Frameworkを使っています
- サーバーレス環境を簡単に構築できる(特にLambdaのLayersとAPI Gatewayの設定を楽したい)
- インフラ設定をコードに落としこむことで、作業間隔が空いた時にインフラの設定を把握し直す面倒さを解消したい
ちなみにサーバーレスアーキテクチャやServerless Frameworkの簡単な内容をこちらの書きましたので、読んでみてください👽
技術的なポイント
構成
DynamoDBにTwitterAPIから取得したデータを詰めてく形です。Lambdaにはフォロワー数などのデータの保存や、ユーザープロフィール更新といった定期実行の関数と、データを呼び出すAPI用の関数があります。
今回Serverless Frameworkで構成管理しているのはAPI部分のみです。フロント部分は別リポジトリに置いてあり、Netfilyにホスティングしてます(そっちのほうが自分は早くデプロイできるため)。
関数の定義
例として、フォロワー数などのデータを保存する関数をみていきます。functions/storeCounts.js
のhandle
の中身を毎日実行する設定です。
functions:
storeCounts:
handler: functions/storeCounts.handle
timeout: 15
events:
- schedule: cron(0 15 * * ? *)
storeCounts.js
の中身では登録されているアカウント一覧を呼び出して、1件ずつデータを取得・保存します。
'use strict'
module.exports.handle = async (event) => {
const utils = require('./modules/utils')
const DbManager = require('./modules/dbManager')
const DB = new DbManager()
try {
//ユーザーリストを取得
let userList = await DB.getUserList()
//並列で登録処理を実行する
await Promise.all(userList.Items.map(async user => {
return DB.storeCountData(user.id)
}))
return utils.getResponseData("done")
}
catch (e) {
return utils.getResponseData({error:e})
}
}
DynamoDBに登録されたアカウントを取得してます。type
というパーティションキーがuser
のものを取得しています。
/**
* DynamoDBからuserのリストを取得する
*/
getUserList(){
let queryParams = {
TableName: process.env['TABLE_NAME'],
KeyConditionExpression: "#type = :type",
ExpressionAttributeNames:{
"#type": "type",
},
ExpressionAttributeValues: {
":type": 'user',
}
}
return this.dynamoClient.query(queryParams).promise()
}
上記の関数の中で呼ばれている、Twitter APIを叩いてDynamoDBへデータを保存する処理です。もちろん現在の時間もまとめてputします。
/**
* twitterのカウントデータをDynamoDBに保存する
*
* @param string id
*/
async storeCountData(id){
let apiResponse = await this.getUserDataFromTwitter(id)
let twitterUserData = apiResponse.data.data
//put用のデータを作成する
let dt = new Date(Date.now() + ((new Date().getTimezoneOffset() + (9 * 60)) * 60 * 1000));
let now = dt.toFormat("YYYY-MM-DD-HH24-MI-SS")
let putItem = {
type:'count',
id_date: twitterUserData.id + '_' + now,
id: twitterUserData.id,
date: now,
username:twitterUserData.username,
name:twitterUserData.name,
public_metrics:twitterUserData.public_metrics,
//オリジナルサイズの画像パスに変換する
profile_image_url:twitterUserData.profile_image_url.replace("_normal.", "."),
entities:twitterUserData.entities,
url:twitterUserData.url,
description:twitterUserData.description,
created_at:twitterUserData.created_at,
}
let putParams = {
TableName: process.env['TABLE_NAME'],
Item: putItem
}
return this.dynamoClient.put(putParams).promise()
}
また、HTTP経由で呼び出す場合は、以下のように定義するとまとめてApi Gatewayの構築もやってくれます。
getCountData:
handler: functions/getCountData.handle
events:
- http:
path: /count/{id}
method: get
request:
parameters:
paths:
id: true
cors: true
自分と同じようにNode.js + AWS Lambda + API Gateway + DynamoDBでAPIをつくる場合はこの記事がかなり参考になると思います。
DynamoDBの設計
RDBにおける正規化の思想とは違って、取得効率を高めるためにデータの構造が冗長的になっていても問題ないという考えです。このアプローチを理解するのにとても苦戦しました><
AWSのドキュメントに詳しく書いてあります。
- DynamoDB の場合は答えが必要な質問が分かるまで、スキーマの設計を開始すべきではありません。ビジネス上の問題とアプリケーションのユースケースを理解することが不可欠です。
- DynamoDB アプリケーションではできるだけ少ないテーブルを維持する必要があります。
今回においては、以下の2つの要件を満たす必要があります。
- フォロワー数データをアカウントID別に取得し、日付順にソートする
- 登録したアカウントを一覧で取得する
それをふまえて、以下のように設計しました
- パーティションキーはデータの種類を表す
type
。アカウントに関する項目なのかフォロワー数に関する項目なのかを表す - ソートキーは「アカウントID + _ + 日付」からなる
id_date
1.により異なるコンテキストのデータを1つのテーブルにまとめています。また2.のように設計することで、「フォロワー数データをアカウントID別に取得し、日付順にソートする」という要件を効率的に満たします。id_date
を対象にbegins_with構文を用いて、指定したIDから始まるid_date
の項目を取得するというクエリ投げることをことで、絞り込みと並び替えを実現しています。
DynamooDBについてのserverless.ymlの記述は以下です。
resources:
Resources:
UserDynamoDbTable:
Type: 'AWS::DynamoDB::Table'
Properties:
TableName: ${env:TABLE_NAME}
# キーの型を指定
AttributeDefinitions:
-
AttributeName: type
AttributeType: S
-
AttributeName: id_date
AttributeType: S
# キーの種類を指定(ハッシュorレンジキー)
KeySchema:
-
AttributeName: type # Partition key
KeyType: HASH
-
AttributeName: id_date # Sort key
KeyType: RANGE
# プロビジョニングするキャパシティーユニットの設定
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
Layersのデプロイ
serverless-layers
というプラグインを使っています。package.json
の情報を元にして
、Layersのデプロイを簡単にやってくれます。
※Layersデプロイ用のS3のバケットを用意する必要があります
custom:
serverless-layers:
layersDeploymentBucket: ${env:LAYER_BUCKET}
Twitter API
Twitterのユーザーアカウントを取得するにはUserオブジェクトを取得するAPI群を使用します。
Userオブジェクトにはidが付いていてこれによって一位に識別しています。ちなみに@yousuck2020などの@
から始まる文字列はusernameという名前がついています。
またAPIを叩くにはbearerToken
をheaderに持たせます。bearerToken
の取得のために申請が必要なのです。
終わりに
Serverless Frameworkを触ってみましたが、確かにコンソールポチポチより楽ですし、構成がコードで管理されてるというのはいい体験だなと感じました。ただsls deploy
コマンドでデプロイするときに時間がかかってしまっていて、細かい修正を繰り返した際はコンソール上からいじりたいなと思うときもありました。
あとはDynamoDBの設計の概念の理解が大変かつ面白かったです。今回は要件がシンプルでしたが複雑な場合はもっと難しいんだろうな〜と思います。
インスタやTiktokでも似たようなことをやりたいのですが、APIが充実していなく残念です😅
運用して半年が立ちますが、今のところ無料できているので満足です^^
参考
- https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Introduction.html
- https://docs.aws.amazon.com/ja_jp/ja_jp/apigateway/latest/developerguide/getting-started.html
- https://www.serverless.com/blog/node-rest-api-with-serverless-lambda-and-dynamodb
- https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/GettingStarted.NodeJs.04.html
- https://www.serverless.com/blog/cors-api-gateway-survival-guide
- https://www.serverless.com/framework/docs/
- https://serverless.co.jp/blog/25/
- https://service.plan-b.co.jp/blog/tech/30863/
- https://qiita.com/yuno_miyako/items/fad33456d9c32d8f4483
記事のフォーマットは以下の記事を参考にさせていただいていますmm
Discussion