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

# 概要
最近流行りのsam-cliを使って、Kotlinでlambdaを書いてみました。
今回書いたのは簡単なGETとPOSTの処理になります。
完成物は[こちら](https://github.com/curry-like/kotlin-sam-cli)
## 環境
- 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に変更する。

完成品は[こちら](https://github.com/KotaroNishiyamaBiz/kotlin-sam-cli/blob/master/pom.xml)

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

## 最初の動作確認
```
$ mvn package
$ sam local start-api
```
これでlocal環境でlambdaが立ち上がり、localhostでアクセスできるようになるのでまずは動くことを確認します。
最初は `localhost:3000/hello` で `Hello World` になるはずです。

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

### 受け取るRequestのclassの作成
今回はbodyに以下のようなJsonを渡します。

```json

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

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

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

```json

{
  "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に対応するクラスを作っていきます。

```kotlin
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のものと同様ですが、最初にリクエストボディをパースします。
実際のコードは以下の通りです。

```kotlin

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を書いた話は終わりになります。
ありがとうございました。