Slide 1

Slide 1 text

Kotless とDynamoDB で自分のツイートを収集するサーバーレス アプリケーションを作る 2021 年12 月21 日 Kotlin 愛好会 vol.33 竹端 尚人

Slide 2

Slide 2 text

自己紹介

Slide 3

Slide 3 text

概要 竹端 尚人 フリーランスエンジニア Twitter: @n_takehata 職種: バックエンドエンジニア 好きな言語:Kotlin Server-Side Kotlin 、Java 、Go 、etc… ( 少し前まで) スマートフォンゲーム開発 昨年12 月からフリーランスに

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

今回のテーマ

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

1.Kotless とは?

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

導入方法はブログでも紹介しています Kotless でKotlin のアプリケーションをAWS Lambda にデプロイする https://blog.takehata-engineer.com/entry/deploy-kotlin-applications-to-aws-lambda-using-kotless

Slide 12

Slide 12 text

簡単なサンプル

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

deploy タスクを実行 $ ./gradlew deploy

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

deploy の結果に出ていたURL でアクセスできる $ curl https://8443r74qk8.execute-api.us-west-2.amazonaws.com/1 Hello World!

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

Lambda には関数が作られる

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

ちょっとデモ

Slide 30

Slide 30 text

Ktor とSpring Boot のDSL もある

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

必要なライブラリの追加 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 のサードパーティライブラリ)

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

共通で使う部分

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

登録処理の関数

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

登録処理は完成

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

レスポンスとして使うデータクラス data class GetTweetListResponse(val id: Long, val time: String, val text: String) 表示で使用するデータのため、time を文字列にしている

Slide 62

Slide 62 text

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 }

Slide 63

Slide 63 text

引数と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 関数でアカウントのの情報を取得 アカウントの作成日時から開始した年を、現在日時から現在の年を取得 開始した年から現在の年までのツイートを取得するのに使用する ` `

Slide 64

Slide 64 text

DynamoDB の検索のためのclient を生成 val client = AmazonDynamoDBClientBuilder.defaultClient() val index = DynamoDB(client).getTable("Tweet").getIndex("datetime-index") 登録の時と同じく、default のCredentials を使用したDynamoDB のclient を生成 グローバルセカンダリインデックスを使用して検索するので、Index のオブジェクトを取得

Slide 65

Slide 65 text

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 に追加 ` `

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

GET でルーティングする関数を作り、取得処理を呼び出す @Get("/find") fun findTweet(month: Int, day: Int) = getTweetListByMonthDay(month, day)

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

完成!

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

5. まとめ

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

No content