Serverless Framework + Kotlin + Gradle で Lambda をデプロイする

業務でLambdaを使用してたんですが、最初は盲目的にPython使ってました。しかし自分以外が使う場合の学習コストとメンテナンス性を考慮すると普段使っていないPythonはどうもよろしくない。というわけで普段使っているKotlinに移行する + ServerlessFrameworkを使うことにしました。その時の備忘録。

# 下準備
## Kotlin projectを作成する
端折ります。この辺を見るといいかと。
[Getting Started with IntelliJ IDEA - Kotlin Programming Language](https://kotlinlang.org/docs/tutorials/getting-started.html)


## Serverless Framework をインストール
- npmでインストールするので先にインストールしてください
	- 自分はnodebrewを使いました。

```
brew install nodebrew
```

- インストール

```
cd hogeProject
npm install -g serverless
```

# Gradle で shadowJar ビルドの設定
## shadowJarとはなんぞや？
[Gradle Shadow](https://github.com/johnrengelman/shadow)のことです(説明になってない)。ざっくりいうとShadowというpluginを使ってFatJarを作れます。

Lambdaにデプロイする際に、普通に gradle build してデプロイすると依存関係に追加したライブラリが含まれません。含まれない状態でもデプロイできますが、ライブラリがないので実行時エラーになります。
それを防ぐためにライブラリも含めて jar を作っちゃおうということです。
これで出来上がった jar を FatJar といいます。

## 実装
```build.gradle
buildscript {
  ext {
    kotlinVersion = '1.3.50'
  }
  repositories {
    mavenCentral()
/* == 追記 == */
    maven { url "https://plugins.gradle.org/m2/" }
/* ========== */
  }
  dependencies {
    classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
    classpath("org.jetbrains.kotlin:kotlin-allopen:$kotlinVersion")

/* == 追記 == */
    // shadow plugin
    classpath "com.github.jengelman.gradle.plugins:shadow:2.0.1"
/* ========== */
  }
}

apply plugin: 'kotlin'

/* == 追記 == */
apply plugin: 'com.github.johnrengelman.shadow'
/* ========== */

repositories {
  mavenCentral()
/* == 追記 == */
  maven { url "https://plugins.gradle.org/m2/" }
/* ========== */
}

dependencies {
  implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
  implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
/* == 追記 == */
  implementation 'com.amazonaws:aws-lambda-java-core:1.1.0'
  implementation 'com.amazonaws:aws-lambda-java-events:2.0.2'
/* ========== */
}

compileKotlin {
  kotlinOptions {
    freeCompilerArgs = ['-Xjsr305=strict']
    jvmTarget = '1.8'
  }
}

/* == 追記 == */
build.dependsOn shadowJar
/* ========== */
```

- shadow plugin を依存関係に追加してます。
- `build.dependsOn shadowJar` でビルド時に shadowJar を実行するようにしてます。こうしないと build → shadowJar実行を毎回やらないといけないので、少し楽になります(気がする)

# Lambda関数の実装
```HogeHandler.kt
package jp.co.valus.sample

class HogeHandler : RequestHandler<Map<String, Any>, Unit> {

  override fun handleRequest(input: Map<String, Any>, context: Context) {
    LOG.info(input.toString())
    // do something
  }
}
```

- RequestHandler を implements する。
- Lambdaが実行されるときに handleRequest がコールされるので、ココを起点に実装していく

# serverless.yml の実装
AWSにDeployするための設定を書いていきます。わかりやすいところは説明を省いています。内容が違っていたら指摘してください。
公式はこのへん → [Serverless Variables](https://serverless.com/framework/docs/providers/aws/guide/variables/)

```serverless.yml
service: hoge

provider:
  name: aws
  runtime: java8
  stage: ${opt:stage, self:custom.defaultStage}
  region: ap-northeast-1
  memorySize: 512
  iamRoleStatements:
    - Effect: Allow
      Action:
        - lambda:InvokeFunction
      Resource: arn:aws:lambda:*:*:function:${self:custom.process}

custom:
  defaultStage: dev

  settings: ${file(./variables-${self:custom.stage}.yml), file(./variables-dev.yml)}

  region: ${self:provider.region}
  stage: ${opt:stage, self:provider.stage}
  prefix: ${self:custom.stage}-${self:service}

package:
  artifact: ./build/libs/hoge-1.0.jar

functions:
 worker:
  timeout: 30
  handler: jp.co.valus.sample.HogeHandler
  name: ${self:custom.prefix}-worker
  vpc:
    securityGroupIds:
      - ${self:custom.settings.securitygroup}
    subnetIds:
      - ${self:custom.settings.subnet}
  environment:
    url: ${self:custom.settings.db.url}
    username: ${self:custom.settings.db.username}
    password: ${self:custom.settings.db.password}
  events:
    - schedule: rate(1 minute)
```

##  `service: hoge`
自分の作る Service Name をここに記述します

##  `provider:`
後述する `function:` で継承できる設定を記述。 Lambda function を複数書く場合、共通する設定をここに記述できる。
今回は aws の ap-northeast-1 リージョンに  java8 ランタイム環境で メモリ512MB を指定して、 iamRoleStatements で設定した権限をもたせてデプロイするといった形で書いてある。

- `stage: ${opt:stage, self:custom.defaultStage}`

環境(staging, productionなど) の指定をしている。${} 内は左辺がコマンド引数、左辺がなければ右辺の値が代入される。
環境を指定しているのは各環境で外出しした設定ファイルを読み替えるため。

## `custom:`
カスタム値を記載。ほとんどは環境(stagingとか)毎に名前を変えるために使っているが、settings の項目だけ、外部に置いてある設定ファイルの内容を import するようになっている。

- `settings: ${file(./variables-${self:custom.stage}.yml), file(./variables-dev.yml)}`

アクセスは settings.db.url といった形で行う。
以下のような設定ファイルを参照している。

```variables-dev.yml
db:
  url: jdbc:mysql://localhost:3308/hoge?useSSL=false&autoReconnect=true
  username: hoge
  password: 12345678

securitygroup: sg-0xxxxxxxx
subnet: subnet-0xxxxxxxx
```

## `package:`
デプロイする FatJar を指定。

## `functions:`
デプロイする Lambda 関数をここに記載していく。

- `worker:`
Function のタグのようなもの

- `handler:`
Lambda 関数が呼び出す対象をここで指定する。先に書いた RequestHandler を継承したクラスである必要がある。

- `vpc:`
AWS 上で設定したVPCのIDを指定。ここで指定した securityGroup と subnet でLambda が動くので適当に指定したのが private subnet だったりすると外のネットワークに出れなくて、おや？となる。

- `environment:`
環境変数を設定できる。DBへの接続設定を settings から代入している。

- `events:`
起動イベントを指定できる。 `- schedule: rate(1 minute)` で一分毎にLambda関数が起動される。

# デプロイする
以下のコマンドを打って、クリーンビルドして FatJar 作成 → デプロイ する

```
./gradlew clean build && sls deploy —stage [環境を指定] —aws-profile [※awsのprofileを指定]
```

※ aws-cli がインストールされていて、ログインできる前提です。
※ 特に profile 設定してない場合は最後の値は不要です。