【AWS re:Invent2018】AWS Lambda で Rubyを使う(Hello worldからDynamoDBとの連携、速度計測まで)

## Ruby support for Lambda
LambdaでついにRuby Runtimeがサポートされました。

![UNADJUSTEDNONRAW_thumb_34b8.jpg](https://qiita-image-store.s3.amazonaws.com/0/317821/7b7c121d-013b-df61-cfd2-5a09951caeb2.jpeg)

現在対応しているバージョンは2.5です。
とにかく触ってみることが大事ということで、早速動かしてみました。

- Hello world
- DynamoDB連携
- pythonとnodeとの速度比較

### Lambda関数を作成
もうGAなので選択肢にruby2.5が表示されています。(嬉しい...!)
![screenshot1.png](https://qiita-image-store.s3.amazonaws.com/0/317821/2ce504e8-b959-dabc-3df6-f2dadc547cad.png)

作成。
<img width="1123" alt="スクリーンショット 2018-11-30 4.37.47.png" src="https://qiita-image-store.s3.amazonaws.com/0/317821/3c27043c-cb3a-5f16-7cd5-0a038082789c.png">

最初のコードはこんな感じですね。

```ruby:lambda_function

require 'json'
 
def lambda_handler(event:, context:)
    # TODO implement
    { statusCode: 200, body: JSON.generate('Hello from Lambda!') }
end
```

何はともあれとりあえずこのまま実行します。 **Hello from Lambda!**

![screenshot2.png](https://qiita-image-store.s3.amazonaws.com/0/317821/28cd6d39-c5bb-dc33-fcd4-0c31b071ed75.png)

## 色々遊んで見る

標準ライブラリ以外を利用したい場合には、NodeやPythonなどと同じように、ローカルでrubyアプリを作成してそれをまとめてsamでアップロードする形になります。 AWSブログの[チュートリアル](https://aws.amazon.com/jp/blogs/compute/announcing-ruby-support-for-aws-lambda/)を使って解説していきます。

### DynamoDBとの連携

##### 1. 下準備
まずはローカルにアプリディレクトリを作ります。
   
``` shell:bash
$ mkdir lambda_ruby
$ cd lambda_ruby
```

##### 2.　Gemfileを作成する
中身にaws用のライブラリを記述します。他のライブラリを利用したい場合は合わせて書くと良いでしょう。

``` ruby:Gemfile
source 'https://rubygems.org'
gem 'aws-record', '~> 2'
```
bundle installも忘れずに実行してください。ローカルのrubyにインストールしたい場合は`bundle install`だけでもいいのですが、今回はライブラリも含めてLambda側にdeployするので、`--deployment`オプションを付けて実行しておきましょう

```shell:bash
$ bundle install --deployment
```

##### 3. Lambdaの実行スクリプトを作成する
`hello_ruby_record.rb`を作成します。 `put_item` を今回のlambdaのhandlerメソッドとしています。(後述のtemplate.yamlの設定でhandlerの関数は自由に指定できます)

``` ruby:hello_ruby_record.rb
require 'aws-record'

class DemoTable
  include Aws::Record
  set_table_name ENV[‘DDB_TABLE’]
  string_attr :id, hash_key: true
  string_attr :body
end  

def put_item(event:,context:)
  body = event["body"]
  item = DemoTable.new(id: SecureRandom.uuid, body: body)
  item.save! # raise an exception if save fails
  item.to_h  
end
```

##### 4. template.yamlの作成
lambda側にアップロードするための設定ファイルを作成します。Lambda側へのアップロードは、[AWS SAM](https://aws.amazon.com/jp/serverless/sam/) を利用します。

```yaml:template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: 'sample ruby application'

Resources:
  HelloRubyRecordFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: hello_ruby_record.put_item
      Runtime: ruby2.5
      Policies:
      - DynamoDBCrudPolicy:
          TableName: !Ref RubyExampleDDBTable 
      Environment:
        Variables:
          DDB_TABLE: !Ref RubyExampleDDBTable

  RubyExampleDDBTable:
    Type: AWS::Serverless::SimpleTable
    Properties:
      PrimaryKey:
        Name: id
        Type: String

Outputs:
  HelloRubyRecordFunction:
    Description: Hello Ruby Record Lambda Function ARN
    Value:
      Fn::GetAtt:
      - HelloRubyRecordFunction
      - Arn
 ```
今回はLambda関数とDynamoDBテーブルを利用するので、Serverless::FunctionとServerless::SimpleTableをリソースとして定義します。
このテンプレートファイルの詳しい解説は[チュートリアル](https://aws.amazon.com/jp/blogs/compute/announcing-ruby-support-for-aws-lambda/)を見てください。

 
##### 5. 一旦フォルダ構成を確認
今までの作業でフォルダ内はこの様になっているはずです。

 ```shell
 $ tree -L 2 -a
 .
 ├── .bundle
 │   └── config
 ├── Gemfile
 ├── Gemfile.lock
 ├── hello_ruby_record.rb
 └── vendor
 └── bundle
 ```

 あとはSAMを使ってdeployを実施するだけです。

##### 6. SAMパッケージの作成

 SAMが入っていない場合はインストールしておきます。

 ```sh
 $ pip install aws-sam-cli
 ```

 先程作成したテンプレートファイルを使ってsamパッケージを作成します。
 ※S3バケットは予め作成しておいてください。

 ```shell
 sam package --template-file template.yaml \
 --output-template-file packaged-template.yaml \
 --s3-bucket <bucketname>
 ```

 フォルダに`packaged-template.yaml`が作成されたら成功です。


##### 7. パッケージのアップロード
 SAMのdeployコマンドを利用してパッケージをアップロードします。
 `stack-name`はlambdaの関数名になりますので、好きな名前を入力してください。

 ```sh
 $ sam deploy --template-file packaged-template.yaml \
 --stack-name helloRubyDynamo \
 --capabilities CAPABILITY_IAM

 Waiting for changeset to be created...
 Waiting for stack create/update to complete
 Successfully created/updated stack - helloRubyRecord
 ```

 **注意点** 
 CLIに紐付いているIAMにCloudFormationの権限を付けておかないとエラーになります。

 ```sh
 An error occurred (AccessDenied) when calling the CreateChangeSet operation: 
 User: arn:aws:iam::*** is not authorized to perform: 
 cloudformation:CreateChangeSet on resource:
 arn:aws:cloudformation:ap-northeast-1:***:stack/helloRubyDynamo/*
 ```

##### 8. テスト
 CloudFormationでデプロイしたので、DynamoDBのセットアップも含めてデプロイが完了しています。下記のようなテストイベントを送ってテストしてみましょう

 ```json:テストイベントの設定
 {"body": "hello lambda"}
 ```

成功しました。
![screenshot3.png](https://qiita-image-store.s3.amazonaws.com/0/317821/cba3539b-c001-8081-4130-dc9d8bec0cc3.png)

DynamoDBにも同じ値が入っていますね。
<img width="756" alt="スクリーンショット 2018-11-30 8.49.46.png" src="https://qiita-image-store.s3.amazonaws.com/0/317821/58687df7-e61a-147e-2813-1dd130e0a272.png">

こんな感じです。途中からSAMの解説になってしまいましたが、Rubyのライブラリを入れたいならSAMを導入しておくとその後の手間が大いに削減されるので、設定しておいて損はないと思います。

### 処理時間はどうなのか計測
nodeやpythonと違い、新しく発表されたCustom Runtimeをバックエンドに利用しているとのことなので、ruby Runtimeの読み込み時間なども含めて処理時間ってどうなんだろう、と気になったので、計測しました。

##### 初回起動 
|言語 | バージョン | 実行時間
|:------|:----|:-------|
|ruby   |2.5  |23.31 ms|
|node.js|8.10 |12.28 ms|
|python |3.7  |4.62 ms|

なるほど。。。Custom Runtimeの読み込み分が乗っかってる感じでしょうかね。。。


##### 2回目以降
それぞれ100回叩いて平均値を計測しました。公平を期すためにHello worldのみのコードで100回ループで起動時間を計測します。

```ruby:lambda_handler.rb
require 'json'

def lambda_handler(event:, context:)
    # TODO implement
    { statusCode: 200, body: JSON.generate('Hello from Lambda!') }
end
```
```js:lambda_handler.js
exports.handler = async (event) => {
    // TODO implement
    const response = {
        statusCode: 200,
        body: JSON.stringify('Hello from Lambda!'),
    };
    return response;
};
```
```python:lambda_handler.py
import json

def lambda_handler(event, context):
    # TODO implement
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }
```

では100回回します。下記のようなスクリプトを言語ごとに回しました。

```ruby:loop.rb
require 'json'
require 'base64'

duration_total_ruby = 0

100.times do |i| 

	result = `aws lambda invoke --invocation-type RequestResponse --function-name HelloworldRuby --log-type Tail --payload \'{}\' outputfile.txt`

	obj = JSON.parse(result)
	duration = Base64.decode64(obj["LogResult"])[/Duration: (.*) ms\tB/,1]
	p "#{i}, #{duration} ms."
	duration_total_ruby += duration.to_f

end

p "Invoke ruby duration total: #{duration_total_ruby} ms" 
p "Invoke ruby duration agverage: #{duration_total_ruby / 100} ms" 

```

```ruby:result
"Invoke ruby duration total: 704.3100000000002 ms"
"Invoke ruby duration agverage: 7.043100000000002 ms"

"Invoke node duration total: 137.77000000000004 ms"
"Invoke node duration agverage: 1.3777000000000004 ms"

"Invoke python duration total: 638.43 ms"
"Invoke python duration agverage: 6.3843 ms"
```
|言語    |バージョン | 総実行時間 |平均実行時間|
|:------|:-------|:---------|:---------|
|ruby   | 2.5    |704.31 ms |7.0431 ms |
|node.js| 8.10   |137.77 ms |1.3777 ms |
|python | 3.7    |638.43 ms |6.3843 ms |

なんとnode.jsが一番　早いという結果に！(当然rubyは一番遅い)


## 終わりに

個人的にも馴染みのあるRubyがサポートされたのは非常に嬉しかったですね。
これからどんどん使っていきたいと思います。