$30 off During Our Annual Pro Sale. View Details »

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. Kotless
    とDynamoDB
    で自分のツイートを収集するサーバーレス
    アプリケーションを作る
    2021
    年12
    月21
    日 Kotlin
    愛好会 vol.33

    竹端 尚人

    View Slide

  2. 自己紹介

    View Slide

  3. 概要
    竹端 尚人

    フリーランスエンジニア
    Twitter: @n_takehata

    職種:
    バックエンドエンジニア

    好きな言語:Kotlin
    Server-Side Kotlin
    、Java
    、Go
    、etc…
    (
    少し前まで)
    スマートフォンゲーム開発
    昨年12
    月からフリーランスに

    View Slide

  4. 登壇、執筆など
    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

    View Slide

  5. 今回のテーマ

    View Slide

  6. Kotless
    というサーバーレスフレームワークを
    使ったアプリケーション

    View Slide

  7. この内容はKotlin Advent Calendar 2021 24
    日目の記事として
    ブログでも公開する予定です

    View Slide

  8. アジェンダ
    1. Kotless
    とは?
    2.
    作ろうとしているアプリケーション
    3. Kotless
    でツイートを収集してDynamoDB
    へ登録
    4. Kotless
    でDynamoDB
    からデータの取得
    5.
    まとめ

    View Slide

  9. 1.Kotless
    とは?

    View Slide

  10. Kotlin
    で実装できるサーバーレスフレームワーク
    ざっくり言うと、Kotlin
    でアプリケーションを実装してAWS Lambda
    などにデプロイしたりできるもの
    コードはKotlin
    で実装し、Kotless
    のGradle
    タスクを実行すると裏側でLambda
    を構成するためのTerraform
    のファイルなどが作られ、AWS
    のResource
    作成からアプリケーションのデプロイまでやってくれる
    Node.js
    やPython
    を使わず、Lambda
    のコードを「Kotlin
    で書きたい!」という願いを叶えてくれる夢のある
    フレームワーク

    View Slide

  11. 導入方法はブログでも紹介しています
    Kotless
    でKotlin
    のアプリケーションをAWS Lambda
    にデプロイする

    https://blog.takehata-engineer.com/entry/deploy-kotlin-applications-to-aws-lambda-using-kotless

    View Slide

  12. 簡単なサンプル

    View Slide

  13. 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
    で検証中。

    View Slide

  14. 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
    }
    }
    }

    View Slide

  15. 関数を一つ実装
    import io.kotless.dsl.lang.http.Get
    @Get("/")
    fun main() = "Hello world!"
    Kotless DSL
    の @Get
    アノテーションなどを使ってルーティングできる
    ` `

    View Slide

  16. Gradle
    でKotless
    のタスクをいくつか実行していく

    View Slide

  17. plan
    タスクを実行
    $ ./gradlew plan

    View Slide

  18. 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

    View Slide

  19. この時に裏ではTerraform
    のファイルを作って実行されている

    View Slide

  20. deploy
    タスクを実行


    $ ./gradlew deploy

    View Slide

  21. 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

    View Slide

  22. deploy
    の結果に出ていたURL
    でアクセスできる


    $ curl https://8443r74qk8.execute-api.us-west-2.amazonaws.com/1
    Hello World!

    View Slide

  23. これでLambda
    周りの構成とアプリケーションのデプロイが完了

    View Slide

  24. AWS
    上のリソースはどうなっているか?

    View Slide

  25. S3
    にtfstate
    ファイルと、アプリケーションのjar
    ファイルが
    アップロードされる

    View Slide

  26. Lambda
    には関数が作られる

    View Slide

  27. Lambda
    の関数に紐付いたAPI Gateway
    も作られる

    View Slide

  28. Kotlin
    を書いてポチポチするだけで
    サーバーレスアプリケーションができました

    View Slide

  29. ちょっとデモ

    View Slide

  30. Ktor
    とSpring Boot
    のDSL
    もある

    View Slide

  31. 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
    を実装できる

    View Slide

  32. 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
    を実装できる

    View Slide

  33. チームや要件に合わせてやりやすい書き方を選べる
    (
    複数のDSL
    をdependencies
    に追加するとエラーになります)

    View Slide

  34. Kotless
    の基本まとめ


    Kotlin
    を書いてポチポチやるとLambda
    のリソースとアプリケーションのデプロイをしてくれる
    Ktor
    やSpring Boot like
    な書き方もできる

    View Slide

  35. 2.
    作ろうとしているアプリケーション

    View Slide

  36. カコミエールというアプリの模倣

    View Slide

  37. 愛用していたが結構前に使えなくなり・・・


    2018
    年からアップデートがなく、Twitter API
    の仕様変更とかの影響か過去が表示されなくなった
    作ろうと思ったが、Twitter API
    は通常だと1
    週間前までのデータしか取得できないことが判明
    課金すればもっと過去も取れるが、リクエスト数の制限もあり一般公開のアプリには使えない
    search/universal
    という本来は公開されていないAPI
    を使えば取れるらしいが怪しい

    View Slide

  38. 別の方法で自分用に作ることを考える

    View Slide

  39. 定期的にTwitter API
    を叩いて保存しておく方法を取る
    Daily
    でTwitter API
    を叩いて、DynamoDB
    に保存しておく
    過去のツイートはDynamoDB
    から取る形で実現する
    過去分に関しては、自分のアカウントのものならTwitter
    から全データJSON
    でダウンロードすることができ
    るようなので、それを登録する

    View Slide

  40. 自分が使うだけだしLambda
    とかでいいのでは?

    View Slide

  41. せっかくだからKotless
    使ってみよう!

    View Slide

  42. この後紹介していく実装


    Twitter API
    をDaily
    で叩いてDynamoDB
    に保存するバッチ
    DynamoDB
    から特定の月日のツイートを取ってくるAPI
    API
    はKotless DSL
    で実装

    View Slide

  43. せっかくSwift/Kotlin
    の合同会なので
    クライアントアプリもSwift UI
    で作ってくる予定だったけど
    間に合わなかったのでまた今度・・・

    View Slide

  44. 3.Kotless
    でツイートを収集してDynamoDB
    へ登録

    View Slide

  45. 必要なライブラリの追加
    dependencies {
    implementation("io.kotless", "kotless-lang", "0.1.7-beta-5")
    implementation("com.amazonaws:aws-java-sdk-dynamodb:1.12.126")
    implementation("org.twitter4j:twitter4j-core:4.0.7")
    }
    Kotless DSL
    DynamoDB
    のJava SDK
    Twitter4J(Java
    製のTwitter
    のサードパーティライブラリ)

    View Slide

  46. サードパーティーライブラリも普通にGradle
    に追加するだけで
    使えます

    View Slide

  47. DynamoDB
    のテーブル
    テーブル名: Tweet
    物理名 意味 型
    id
    ツイートのID(
    パーティションキー)
    数値
    tweet_date
    ツイートした年月日 文字列
    tweet_time
    ツイートした時分秒 文字列
    tweet_text
    ツイートした文章 文字列
    グローバルセカンダリインデックス
    パーティションキー ソートキー
    tweet_date tweet_time

    View Slide

  48. 共通で使う部分

    View Slide

  49. DynamoDB
    のテーブルに関連付けるobject
    を作る
    @DynamoDBTable("Tweet", PermissionLevel.ReadWrite)
    object TweetTable {
    //
    ・・・
    }
    @DynamoDBTable
    アノテーションを付与することで、Lambda
    の関数のIAM
    ロールにDynamoDB
    へのアク
    セスのポリシーを追加してくれる
    この設定では AmazonDynamoDBFullAccess
    のポリシーが追加される
    このobject
    の中にDynamoDB
    へアクセスする関数を作っていく
    ` `
    ` `

    View Slide

  50. 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
    を使用します
    ` `

    View Slide

  51. 取得したツイート情報を保持するデータクラス
    data class Tweet(val id: Long, val time: LocalDateTime, val text: String)
    Twitter API
    のレスポンスからこのクラスに変換して保持する
    ツイートのID
    、ツイートした時間、ツイートの本文を保持

    View Slide

  52. 登録処理の関数

    View Slide

  53. 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)
    }
    }

    View Slide

  54. 引数と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
    に変換
    ` `
    ` ` ` `
    ` `

    View Slide

  55. 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
    で実行
    ` `
    ` `
    ` ` ` `

    View Slide

  56. 登録処理をDaily
    で実行する関数

    View Slide

  57. @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
    で必要な文字列形式に変換して、前述の登録処理の関数を実行
    ` `

    View Slide

  58. 本当は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
    投げて待ってます)
    ` `

    View Slide

  59. 登録処理は完成

    View Slide

  60. 4.
    検索処理の関数Kotless
    でDynamoDB
    からデータの取得

    View Slide

  61. レスポンスとして使うデータクラス


    data class GetTweetListResponse(val id: Long, val time: String, val text: String)

    表示で使用するデータのため、time
    を文字列にしている

    View Slide

  62. fun getTweetListByMonthDay(month: Int, day: Int): Map> {
    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>()
    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
    }

    View Slide

  63. 引数とTwitter
    を開始した年、現在の年の取得


    fun getTweetListByMonthDay(month: Int, day: Int): Map> {
    val twitterUser = twitterClient.showUser("account_name")
    val startYear = LocalDateTime.ofInstant(twitterUser.createdAt.toInstant(), ZoneId.systemDefault()).year
    val currentYear = LocalDateTime.now().year


    Twitter API
    から showUser
    関数でアカウントのの情報を取得
    アカウントの作成日時から開始した年を、現在日時から現在の年を取得
    開始した年から現在の年までのツイートを取得するのに使用する
    ` `

    View Slide

  64. DynamoDB
    の検索のためのclient
    を生成


    val client = AmazonDynamoDBClientBuilder.defaultClient()
    val index = DynamoDB(client).getTable("Tweet").getIndex("datetime-index")

    登録の時と同じく、default
    のCredentials
    を使用したDynamoDB
    のclient
    を生成
    グローバルセカンダリインデックスを使用して検索するので、Index
    のオブジェクトを取得

    View Slide

  65. DynamoDB
    からデータを取得し、年ごとのMap
    に設定して返却


    val tweetMap = mutableMapOf>()
    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
    に追加
    ` `

    View Slide

  66. 取得処理を呼び出すAPI
    のルーティング

    View Slide

  67. GET
    でルーティングする関数を作り、取得処理を呼び出す


    @Get("/find")
    fun findTweet(month: Int, day: Int) = getTweetListByMonthDay(month, day)

    View Slide

  68. ここまでできたらplan
    →deploy
    を実行する

    View Slide

  69. DyanamoDB
    に日々データが積まれていく

    View Slide

  70. 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)]}

    View Slide

  71. 完成!

    View Slide

  72. 改善しようと思っている点


    年と月日を別のカラムにして、月日だけで検索できるようにする
    ツイートID
    指定で検索するAPI
    の追加

    View Slide

  73. 5.
    まとめ

    View Slide

  74. Kotless
    はKotlin
    で書いてポチポチやったらAWS Lambda
    にアプリケーションが作れる
    サードパーティライブラリなども使って、通常のKotlin
    のアプリケーションと同じ感覚で作れる
    これからはサーバーレスもKotlin
    に!

    View Slide

  75. View Slide