Upgrade to Pro — share decks privately, control downloads, hide ads and more …

KotlessとDynamoDBで自分のツイートを収集するサーバーレスアプリケーションを作る

 KotlessとDynamoDBで自分のツイートを収集するサーバーレスアプリケーションを作る

2021年12月21日(火) 「集まれKotlin好き!Kotlin愛好会 vol.33 @オンライン」の談義資料です。

Takehata Naoto

December 21, 2021
Tweet

More Decks by Takehata Naoto

Other Decks in Programming

Transcript

  1. 概要 竹端 尚人 フリーランスエンジニア Twitter: @n_takehata 職種: バックエンドエンジニア 好きな言語:Kotlin Server-Side

    Kotlin 、Java 、Go 、etc… ( 少し前まで) スマートフォンゲーム開発 昨年12 月からフリーランスに
  2. 登壇、執筆など CEDEC2018 、2019 登壇 Software Design 2019 年2 〜4 月号で短期連

    載 Swift/Kotlin 愛好会で技術書典執筆 https://booth.pm/ja/items/1315478 書籍「Kotlin サーバーサイドプログラミング 実践開発」を2021 年4 月に発売 https://gihyo.jp/book/2021/978-4-297- 11859-4
  3. Kotlin で実装できるサーバーレスフレームワーク ざっくり言うと、Kotlin でアプリケーションを実装してAWS Lambda などにデプロイしたりできるもの コードはKotlin で実装し、Kotless のGradle タスクを実行すると裏側でLambda

    を構成するためのTerraform のファイルなどが作られ、AWS のResource 作成からアプリケーションのデプロイまでやってくれる Node.js やPython を使わず、Lambda のコードを「Kotlin で書きたい!」という願いを叶えてくれる夢のある フレームワーク
  4. Gradle にplugin とDSL を追加 plugins { kotlin("jvm") version "1.4.32" apply

    true id("io.kotless") version "0.1.7-beta-5" apply true } dependencies { // ・・・ 省略 implementation("io.kotless", "kotless-lang", "0.1.7-beta-5") } Kotlin のバージョンは1.5 以上だとエラー出てたので、一旦1.4.32 で検証中。
  5. Gradle にAWS の構成を追加 kotless { config { // ①S3 のバケットの名前を指定

    bucket = "kotless-example-takehata" // ②$HOME/.aws/credentials の情報を指定 terraform { profile = "default" region = "us-west-2" } } webapp { lambda { memoryMb = 1024 timeoutSec = 120 } } }
  6. 関数を一つ実装 import io.kotless.dsl.lang.http.Get @Get("/") fun main() = "Hello world!" Kotless

    DSL の @Get アノテーションなどを使ってルーティングできる ` `
  7. Terraform のplan の結果が出力される # aws_api_gateway_deployment.root will be created + resource

    "aws_api_gateway_deployment" "root" { // ・・・ } # aws_api_gateway_integration.get will be created + resource "aws_api_gateway_integration" "get" { // ・・・ } // ・・・ Plan: 14 to add, 0 to change, 0 to destroy. ------------------------------------------------------------------------ Note: You didn't specify an "-out" parameter to save this plan, so Terraform can't guarantee that exactly these actions will be performed if "terraform apply" is subsequently run. BUILD SUCCESSFUL in 53s
  8. Terraform のapply と、AWS Lambda へのアプリケーションのデ プロイが実行される data.aws_s3_bucket.kotless_bucket: Refreshing state... data.aws_caller_identity.current:

    Refreshing state... data.aws_region.current: Refreshing state... data.aws_iam_policy_document.kotless_static_assume: Refreshing state... data.aws_iam_policy_document.get_assume: Refreshing state... data.aws_iam_policy_document.get: Refreshing state... // ・・・ Apply complete! Resources: 14 added, 0 changed, 0 destroyed. Outputs: application_url = https://8443r74qk8.execute-api.us-west-2.amazonaws.com/1 BUILD SUCCESSFUL in 1m 16s
  9. Ktor DSL // dependencies にKtor DSL を追加 implementation("io.kotless", "ktor-lang", "0.1.7-beta-5")

    class Server : Kotless() { override fun prepare(app: Application) { app.routing { get("/") { call.respondText { "Hello World!" } } } } } Ktor と同様の書き方でAPI を実装できる
  10. Spring Boot DSL // dependencies にSpring Boot DSL を追加 implementation("io.kotless",

    "spring-boot-lang", "0.1.7-beta-5") @SpringBootApplication open class Application : Kotless() { override val bootKlass: KClass<*> = this::class } @RestController object Main { @GetMapping("/") fun main() = "Hello World!" } Spring Boot と同様の書き方でAPI を実装できる
  11. 定期的にTwitter API を叩いて保存しておく方法を取る Daily でTwitter API を叩いて、DynamoDB に保存しておく 過去のツイートはDynamoDB から取る形で実現する

    過去分に関しては、自分のアカウントのものならTwitter から全データJSON でダウンロードすることができ るようなので、それを登録する
  12. DynamoDB のテーブル テーブル名: Tweet 物理名 意味 型 id ツイートのID( パーティションキー)

    数値 tweet_date ツイートした年月日 文字列 tweet_time ツイートした時分秒 文字列 tweet_text ツイートした文章 文字列 グローバルセカンダリインデックス パーティションキー ソートキー tweet_date tweet_time
  13. DynamoDB のテーブルに関連付けるobject を作る @DynamoDBTable("Tweet", PermissionLevel.ReadWrite) object TweetTable { // ・・・

    } @DynamoDBTable アノテーションを付与することで、Lambda の関数のIAM ロールにDynamoDB へのアク セスのポリシーを追加してくれる この設定では AmazonDynamoDBFullAccess のポリシーが追加される このobject の中にDynamoDB へアクセスする関数を作っていく ` ` ` `
  14. Twitter API 関連の設定 private val twitterClient = TwitterFactory( ConfigurationBuilder().setDebugEnabled(true) .setOAuthConsumerKey("XXXXXXXXXXXXXXXXXXXX")

    .setOAuthConsumerSecret("XXXXXXXXXXXXXXXXXXXX") .setOAuthAccessToken("XXXXXXXXXXXXXXXXXXXX") .setOAuthAccessTokenSecret("XXXXXXXXXXXXXXXXXXXX") .build() ).instance Twitter4J を使い、自分のTwitter API の認証情報を設定する( 事前にTwitter API の登録が必要) この後のコードではこの twitterClient を使用します ` `
  15. 取得したツイート情報を保持するデータクラス data class Tweet(val id: Long, val time: LocalDateTime, val

    text: String) Twitter API のレスポンスからこのクラスに変換して保持する ツイートのID 、ツイートした時間、ツイートの本文を保持
  16. fun putTweetList(accountName: String, since: String, until: String) { val query

    = Query("from:$accountName since:$since until:$until") val queryResults = twitterClient.search(query).tweets val list = queryResults.map { Tweet(it.id, LocalDateTime.ofInstant(it.createdAt.toInstant(), ZoneId.systemDefault()), it.text) } val client = AmazonDynamoDBClientBuilder.defaultClient() list.forEach { val time = it.time val values = mapOf( "id" to AttributeValue().withN(it.id.toString()), "tweet_date" to AttributeValue().withS( TABLE_DATE_FORMAT.format( time.year, time.month.value, time.dayOfMonth ) ), "tweet_time" to AttributeValue().withS(TABLE_TIME_FORMAT.format(time.hour, time.minute, time.second)), "tweet_text" to AttributeValue().withS(it.text) ) val request = PutItemRequest().withItem(values).withTableName("Tweet") client.putItem(request) } }
  17. 引数とTwitter API での結果取得 fun putTweetList(accountName: String, since: String, until: String)

    { val query = Query("from:$accountName since:$since until:$until") val queryResults = twitterClient.search(query).tweets val tweetList = queryResults.map { Tweet(it.id, LocalDateTime.ofInstant(it.createdAt.toInstant(), ZoneId.systemDefault()), it.text) } アカウント名、検索対象の開始と終了の日時( yyyy-MM-dd_HH:mm:ss_JST の形式の文字列) が引数 Query に検索条件を設定し、 search 関数で実行(Query の引数の文字列はTwitter の検索窓にそのまま入 れて使えます) 結果を Tweet クラスのList に変換 ` ` ` ` ` ` ` `
  18. DynamoDB への登録 val client = AmazonDynamoDBClientBuilder.defaultClient() tweetList.forEach { val time

    = it.time val values = mapOf( "id" to AttributeValue().withN(it.id.toString()), "tweet_date" to AttributeValue().withS( "%d-%02d-%02d".format( time.year, time.month.value, time.dayOfMonth ) ), "tweet_time" to AttributeValue().withS("%02d:%02d:%02d".format(time.hour, time.minute, time.second)), "tweet_text" to AttributeValue().withS(it.text) ) val request = PutItemRequest().withItem(values).withTableName("Tweet") client.putItem(request) } } AmazonDynamoDBClientBuilder でdefault のCredentials を使用したDynamoDB のclient を生成 登録する値はKey が文字列、Value が AttributeValue 型のMap を使用する PutItemRequest クラスで値のMap とテーブル名を指定し、 putItem で実行 ` ` ` ` ` ` ` `
  19. @Scheduled("0 0 1/1 * ? *") private fun putTweetList() {

    val TIME_FORMAT = "\"%d-%02d-%02d_%02d:%02d:%02d_JST\"" val accountName = "account_name" val lastDate = LocalDateTime.now(ZoneId.of("Asia/Tokyo")).minusDays(1) val year = lastDate.year val month = lastDate.month.value val day = lastDate.dayOfMonth val since = TIME_FORMAT.format(year, month, day, 0, 0, 0) val until = TIME_FORMAT.format(year, month, day, 23, 59, 59) putTweetList(accountName, since, until) } @Scheduled アノテーションで、AWS のcron 形式で実行条件を指定できる( これは1 日1 回実行される設定) 現在日時の1 日前の年月日を取得( 毎日前日のツイートを収集する想定) Twitter API で必要な文字列形式に変換して、前述の登録処理の関数を実行 ` `
  20. 本当はcron の指定は定数が用意されているが・・・ こういう感じで指定できるはずだが @Scheduled(Scheduled.everyHour) @Scheduled(Scheduled.everyDay) everyDay の定数の指定方法が間違ってて動かない(day のフィールドに0 を指定しているため) @Target(AnnotationTarget.FUNCTION)

    annotation class Scheduled(val cron: String, val id: String = "") { @Suppress("unused") companion object { const val everyMinute = "0/1 * * * ? *" const val every5Minutes = "0/5 * * * ? *" const val every10Minutes = "0/10 * * * ? *" const val everyHour = "0 0/1 * * ? *" const val every3Hours = "0 0/3 * * ? *" const val everyDay = "0 0 0/1 * ? *" } } ( とりあえずPR 投げて待ってます) ` `
  21. レスポンスとして使うデータクラス data class GetTweetListResponse(val id: Long, val time: String, val

    text: String) 表示で使用するデータのため、time を文字列にしている
  22. fun getTweetListByMonthDay(month: Int, day: Int): Map<Int, List<GetTweetListResponse>> { val twitterUser

    = twitterClient.showUser("account_name") val startYear = LocalDateTime.ofInstant(twitterUser.createdAt.toInstant(), ZoneId.systemDefault()).year val currentYear = LocalDateTime.now().year val client = AmazonDynamoDBClientBuilder.defaultClient() val index = DynamoDB(client).getTable("Tweet").getIndex("datetime-index") val tweetMap = mutableMapOf<Int, List<GetTweetListResponse>>() for (year in startYear..currentYear) { val date = "$year-$month-$day" val query = QuerySpec() .withProjectionExpression("id, tweet_date, tweet_time, tweet_text") .withKeyConditionExpression("tweet_date = :v_date") .withValueMap(ValueMap().withString(":v_date", date)) val queryResults = index.query(query) tweetMap[year] = queryResults.map { GetTweetListResponse( it.getLong("id"), "${it.getString("tweet_date")} ${it.getString("tweet_time")}", it.getString("tweet_text") ) } } return tweetMap }
  23. 引数とTwitter を開始した年、現在の年の取得 fun getTweetListByMonthDay(month: Int, day: Int): Map<Int, List<GetTweetListResponse>> {

    val twitterUser = twitterClient.showUser("account_name") val startYear = LocalDateTime.ofInstant(twitterUser.createdAt.toInstant(), ZoneId.systemDefault()).year val currentYear = LocalDateTime.now().year Twitter API から showUser 関数でアカウントのの情報を取得 アカウントの作成日時から開始した年を、現在日時から現在の年を取得 開始した年から現在の年までのツイートを取得するのに使用する ` `
  24. DynamoDB の検索のためのclient を生成 val client = AmazonDynamoDBClientBuilder.defaultClient() val index =

    DynamoDB(client).getTable("Tweet").getIndex("datetime-index") 登録の時と同じく、default のCredentials を使用したDynamoDB のclient を生成 グローバルセカンダリインデックスを使用して検索するので、Index のオブジェクトを取得
  25. DynamoDB からデータを取得し、年ごとのMap に設定して返却 val tweetMap = mutableMapOf<Int, List<GetTweetListResponse>>() for (year

    in startYear..currentYear) { val date = "$year-$month-$day" val query = QuerySpec() .withProjectionExpression("id, tweet_date, tweet_time, tweet_text") .withKeyConditionExpression("tweet_date = :v_date") .withValueMap(ValueMap().withString(":v_date", date)) val queryResults = index.query(query) tweetMap[year] = queryResults.map { GetTweetListResponse( it.getLong("id"), "${it.getString("tweet_date")} ${it.getString("tweet_time")}", it.getString("tweet_text") ) } } return tweetMap } 開始の年から現在の年までループを回す QuerySpec で取得するカラム、検索条件を指定してクエリを組み立てる クエリを実行し、結果をKey が年、Value がその年のツイートのList のMap に追加 ` `
  26. API で年ごとのデータを取得 $ curl "https://mdibxq3aai.execute-api.us-west-2.amazonaws.com/1/find?month=12&day=17" {2011=[], 2012=[], 2013=[], 2014=[], 2015=[],

    2016=[], 2017=[], 2018=[], 2019=[], 2020=[], 2021=[GetTweetListResponse(id=1471816801135001604, time=2021-12-17 12:17:25, text= 満を持して強力なMacBook Pro を注文した。メモリ64GB の世界がどんなもんなのか楽しみ。 ただ納品が遠い https://t.co/Q4UeiMPJI2)]}