Serverless FrameworkとRubyでLambda Layers

# はじめに
この記事は、[UUUM Advent Calendar 2018](https://qiita.com/advent-calendar/2018/uuum) 21日目の記事です。

すでに出遅れている感がありますが、今年のre:Inventで発表された**Lambdaのruby対応**(Custome runtimes)と**Lambda Layers**を**Serverless Framework**で試してみました！

## AWS Lambda Layers

https://aws.amazon.com/jp/blogs/news/new-for-aws-lambda-use-any-programming-language-and-share-common-components/

今までは各Lambda関数が共通で使用するライブラリであっても、各関数のパッケージに同梱する必要があったためライブラリの管理が手間だったりパッケージのサイズが肥大化したりなどといった問題がありました。
AWS Lambda Layersが登場したことで共通的な処理をLayerとしてまとめて切り出すことができ、管理がしやすくなり、各パッケージに同じ処理を毎回同梱させる必要がなくなりました。

## Layerの用意
まず、Layer側から用意していきます。
今回はDB（DynamoDB）処理周りをlayerとして切り出してみました。

### serverless.yaml
Layerに関する定義と、併せてDynamoDBの定義も記載しておきます。
当然のことですが、LayerとFunctionでregionは同じにする必要があるので明示的にap-northeast-1を指定しておきます。

```yaml
service: aws-ruby-layers

provider:
  name: aws
  region: ap-northeast-1

layers:
  dbLayer:
    path: layers
    compatibleRuntimes:
      - ruby2.5

resources:
  Resources:
    FoodsDynamoDbTable:
      Type: 'AWS::DynamoDB::Table'
      Properties:
        AttributeDefinitions:
          -
            AttributeName: name
            AttributeType: S
        KeySchema:
          -
            AttributeName: name
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
        TableName: "foods"
```

### layerのソースコード
DynamoDB上のfoodテーブルからnameを指定してevaluationを取得するコードです。

```ruby
require 'aws-sdk'

class Food
  def initialize
    @dynamoDB = Aws::DynamoDB::Resource.new(region: 'ap-northeast-1')
  end

  def evaluation(food_name)
    table = @dynamoDB.table('foods')
    resp = table.get_item({
      key: { 'name' => food_name }
    })

    resp.item['evaluation']
  end
end
```

### deploy
これでLayer側は完成なので、deployします。
`sls deploy` と入力するだけで諸々のリソースが構築されます。
また、最後に出てくるarnは後から使うのでメモしておきます。

```shell
$ sls deploy
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
.....
Serverless: Stack create finished...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service .zip file to S3 (376 B)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
.........
Serverless: Stack update finished...
Service Information
service: aws-ruby-layers
stage: dev
region: ap-northeast-1
stack: aws-ruby-layers-dev
api keys:
  None
endpoints:
  None
functions:
  None
layers:
  dbLayer: arn:aws:lambda:ap-northeast-1:xxxxxxxxxxx:layer:dbLayer:1
```

## Functionの用意
続いてLayerの処理を呼び出すFunctionを構築していきます。
まず `sls create` で雛形を作成します。

```shell

$ sls create --template aws-ruby --name lambda-ruby-func
Serverless: Generating boilerplate...
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.35.1
 -------'

Serverless: Successfully generated boilerplate for template: "aws-ruby"
```

これで、以下のようなファイルが生成されます。

```sh
.
├── handler.rb
└── serverless.yml
```

### serverless.yaml
生成されたserverless.yamlを編集していきます。
DynamoDBを利用するためのIAMRoleの設定は、layer側ではなくfunction側で定義します。
また、functionにlayerを利用する旨定義し、layerのdeploy時に記載されていたarnを定義しておきます。

```yml
service: lambda-ruby-func

provider:
  name: aws
  runtime: ruby2.5
  region: ap-northeast-1
  iamRoleStatements:
    - Effect: "Allow"
      Resource: "arn:aws:dynamodb:ap-northeast-1:*:*"
      Action:
        - "dynamodb:*"

functions:
  hello:
    handler: handler.hello
    layers:
      - arn:aws:lambda:ap-northeast-1:xxxxxxxxx:layer:dbLayer:1
```

### Functionのソースコード
Layerを呼び出す際のコードです。
ここで少しハマったのですが、Layerのコードは/opt配下に格納されます。
Pythonなどではlayerの/opt配下は自動的にライブラリ検索パスに追加されるとのことだったのですが、
rubyの$LOAD_PATHには追加されていませんでした。。
そのためとりあえず絶対パスでrequireして読み込んでいます。
（もっといいやり方があったらどなたか教えてください！）


```ruby
require 'json'
require '/opt/dynamodb_client'

def hello(event:, context:)
  food = Food.new
  name = 'カレー'
  evaluation = "#{name}は#{food.evaluation(name)}"
  { statusCode: 200, body: JSON.generate(evaluation) }
end
```

### deploy & invoke
今回はlambdaのイベントソースを特に指定していないので、ローカルから直接invokeします。
DynamoDBにはあらかじめ適当なデータを登録してあります。

```sh
$ sls deploy
 〜省略〜
$ sls invoke --function hello
{
    "statusCode": 200,
    "body": "\"カレーは最高に美味い\""
}
```

## まとめ
共通処理をLayerに切り分けることができるようになり、サーバーレス関連の設計がより柔軟にできるようになったと感じました。
今回は雑な切り分けでLayerを分けましたが、今後はより良いLayerの切り分け方を模索していけたらと思います。

## 宣伝
UUUMではエンジニアを募集しています！！
詳しくは下記のリンクをご参照ください。

https://www.wantedly.com/projects/9783
https://www.wantedly.com/projects/25995