sam-cliを使ってKotlinでlambdaを書いた話


概要

最近流行りのsam-cliを使って、Kotlinでlambdaを書いてみました。
今回書いたのは簡単なGETとPOSTの処理になります。
完成物はこちら

環境

  • Kotlin1.3.20
  • Docker for Mac
  • SAM-CLI

開発

開発環境構築

sam-cliのインストール〜パッケージ作成

  1. brew tap aws/tap
  2. brew install aws-sam-cli
  3. sam init --runtime java8

mavenにKotlinの依存関係等の追記

  1. dependenciesに以下のライブラリを追加します。
    • kotlin-stdlib-jdk8
    • kotlin-test-junit
  2. pluginに以下のプラグインを追記します。
    • maven-shade-plugin
    • kotlin-maven-plugin
    • maven-compiler-plugin
  3. sourceDirectory, testSourceDirectoryのpathをsrc/main/kotlin, src/test/kotlinに変更する。

完成品はこちら

javaのソースをKotlinに変換

こちらはintellijを使っていれば、自動でjavaからKotlinに変更することができます。

最初の動作確認

$ mvn package
$ sam local start-api

これでlocal環境でlambdaが立ち上がり、localhostでアクセスできるようになるのでまずは動くことを確認します。
最初は localhost:3000/helloHello World になるはずです。

POST用のlambdaの作成

今回は新たにリクエストボディに人の名前を詰めると、レスポンスにそれを含めてHello Worldを返すAPIを作成します。

受け取るRequestのclassの作成

今回はbodyに以下のようなJsonを渡します。


{
    "firstName": "太郎",
    "lastName": "山田"
}

次にリクエストを受け取るためのクラスを作成します。
ターミナルにて、以下のコマンドを実行するとlambdaが受け取るeventのjsonが取得できるので、それに沿ってクラスを作ります。

$ sam local generate-event apigateway aws-proxy --method POST

{
  "body": "eyJ0ZXN0IjoiYm9keSJ9",
  "resource": "/{proxy+}",
  "path": "/path/to/resource",
  "httpMethod": "POST",
  "isBase64Encoded": true,
  "queryStringParameters": {
    "foo": "bar"
  },
  "pathParameters": {
    "proxy": "/path/to/resource"
  },
  "stageVariables": {
    "baz": "qux"
  },
  "headers": {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
    "Accept-Encoding": "gzip, deflate, sdch",
    "Accept-Language": "en-US,en;q=0.8",
    "Cache-Control": "max-age=0",
    "CloudFront-Forwarded-Proto": "https",
    "CloudFront-Is-Desktop-Viewer": "true",
    "CloudFront-Is-Mobile-Viewer": "false",
    "CloudFront-Is-SmartTV-Viewer": "false",
    "CloudFront-Is-Tablet-Viewer": "false",
    "CloudFront-Viewer-Country": "US",
    "Host": "1234567890.execute-api.us-east-1.amazonaws.com",
    "Upgrade-Insecure-Requests": "1",
    "User-Agent": "Custom User Agent String",
    "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
    "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
    "X-Forwarded-For": "127.0.0.1, 127.0.0.2",
    "X-Forwarded-Port": "443",
    "X-Forwarded-Proto": "https"
  },
  "requestContext": {
    "accountId": "123456789012",
    "resourceId": "123456",
    "stage": "prod",
    "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
    "requestTime": "09/Apr/2015:12:34:56 +0000",
    "requestTimeEpoch": 1428582896000,
    "identity": {
      "cognitoIdentityPoolId": null,
      "accountId": null,
      "cognitoIdentityId": null,
      "caller": null,
      "accessKey": null,
      "sourceIp": "127.0.0.1",
      "cognitoAuthenticationType": null,
      "cognitoAuthenticationProvider": null,
      "userArn": null,
      "userAgent": "Custom User Agent String",
      "user": null
    },
    "path": "/prod/path/to/resource",
    "resourcePath": "/{proxy+}",
    "httpMethod": "POST",
    "apiId": "1234567890",
    "protocol": "HTTP/1.1"
  }
}

続いてこのJsonに対応するクラスを作っていきます。

class Request {
    lateinit var body: String |
}

data class Person(val firstName: String, val lastName: String)

Requestクラスの方は、 RequestHandler を継承したクラスでlambdaの関数を作成する場合、空のコンストラクタがないとパースできないので、今回は通常のclassにしています。
また、リクエストボディに関しては、Jsonではなく文字列で受け取るので、RequestクラスではString型で定義します。
今回はbody以外使わないので、classにはbody以外持たせていません。

lambdaの関数の実装

まずはJsonをパースするために jackson-module-kotlin をpom.xmlのdependencyに追記します。
続いていよいよ関数を実装していきます。
基本的な流れは自動生成されるgetのものと同様ですが、最初にリクエストボディをパースします。
実際のコードは以下の通りです。


class PostApp : RequestHandler<Request, GatewayResponse> {

    override fun handleRequest(input: Request, context: Context?): GatewayResponse {
        val headers = HashMap<String, String>()
        headers["Content-Type"] = "application/json"
        headers["X-Custom-Header"] = "application/json"

        val mapper = jacksonObjectMapper()
        val person = mapper.readValue<Person>(input.body)
        return try {
            val pageContents = this.getPageContents("https://checkip.amazonaws.com")
            val output = String.format("{ \"message\": \"hello %s\", \"location\": \"%s\" }", person.lastName, pageContents)
            GatewayResponse(output, headers, 200)
        } catch (e: IOException) {
            GatewayResponse("{}", headers, 500)
        }
    }

    @Throws(IOException::class)
    private fun getPageContents(address: String): String {
        val url = URL(address)
        BufferedReader(InputStreamReader(url.openStream())).use { br -> return br.lines().collect(Collectors.joining(System.lineSeparator())) }
    }
}

動作確認

$ mvn package
$ sam local start-api

これらを実行し、今回自分で作成したlambdaのエンドポイントを叩いてみます。

$ curl -X POST -d '{"firstName": "山田", lastName: "太郎"}' https://127.0.0.1:3000

これで期待したレスポンスが返ってくれば成功です!

最後に

「javaでかけるんだからKotlinでもかけるっしょ」の精神で書いてみました。
ただ書いてみて思うのは、あえてKotlinで書く必要性がそこまでなかったのではないだろうか。。。というものでした。
個人の趣味とか、会社で使うlambdaの一つをお試しで、とかなら良いかもしれません。

以上でsam-cliを使ってKotlinでlambdaを書いた話は終わりになります。
ありがとうございました。