日本で 128 番目くらいに Ruby で AWS Lambda を試したメモ

# これは

初老丸アドベントカレンダー 2018 の 8 日目の記事になる予定です.

https://qiita.com/advent-calendar/2018/syoroumaru

# 加藤さん, 事件です.

ずっと待たれていたはず, もはや諦めかけていたかもしれない AWS Lambda のランタイムに Ruby が追加されたとの一報が我々の元に入りました.

https://aws.amazon.com/jp/blogs/compute/announcing-ruby-support-for-aws-lambda/

おお〜. なんちゃって Rubist の私もこれまで作ってきた Ruby スクリプトを Lambda で実行出来るかもしれないという興奮が今も続いております.

# ということで

早速, 上記の URL に掲載されているサンプルを参考に簡単な Ruby スクリプトを Lambda 上で動かしてみたいと思います.

## 検証環境

以下のような環境で動作確認を取ります.

```shell
$ ruby --version
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin17]
$ python --version
Python 3.7.0
$ sam --version
SAM CLI, version 0.8.0
$ aws --version
aws-cli/1.16.66 Python/3.7.0 Darwin/17.7.0 botocore/1.12.56
```

初めて [sam](https://github.com/awslabs/aws-sam-cli) を使ってみましたが, とても簡単に Lambda ファンクションのデプロイまで出来ました. Serverless framework と比較すると機能不足は否めませんが, AWS 純正なので新サービスへの対応も早かったりするのかなということで, もう少し使い込んでみようと思います.

## まずは, サンプルイベントを生成する

とりあえず, ローカルで Ruby で書いた関数を動かしてみたいので, Lambda をローカルで実行する際に利用するサンプルイベントを生成します.

```shell
$ sam local generate-event s3 put --bucket=foo --key=bar > event_file.json
```

これを実行すると, 以下のような JSON イベントが生成されます.

```json
$ cat event_file.json
{
  "Records": [
    {
      "eventVersion": "2.0",
      "eventSource": "aws:s3",
      "awsRegion": "us-east-1",
      "eventTime": "1970-01-01T00:00:00.000Z",
      "eventName": "ObjectCreated:Put",
      "userIdentity": {
        "principalId": "EXAMPLE"
      },
      "requestParameters": {
        "sourceIPAddress": "127.0.0.1"
      },
      "responseElements": {
        "x-amz-request-id": "EXAMPLE123456789",
        "x-amz-id-2": "EXAMPLE123/5678abcdefghijklambdaisawesome/mnopqrstuvwxyzABCDEFGH"
      },
      "s3": {
        "s3SchemaVersion": "1.0",
        "configurationId": "testConfigRule",
        "bucket": {
          "name": "foo",
          "ownerIdentity": {
            "principalId": "EXAMPLE"
          },
          "arn": "arn:aws:s3:::foo"
        },
        "object": {
          "key": "bar",
          "size": 1024,
          "eTag": "0123456789abcdef0123456789abcdef",
          "sequencer": "0A1B2C3D4E5F678901"
        }
      }
    }
  ]
}
```

今回は, この JSON イベントから S3 のオブジェクトキーを抜き出す関数を Ruby で書いてみます.

## 祝・Ruby で書く Lambda ファンクション

す, すいません. とりあえず, 雑に書きました.

```ruby
def hello(event:, context:)
  key = event["Records"][0]["s3"]["object"]["key"]
  key
end
```

なんのひねりも, エラー処理もない, 自分の Ruby スキルのレベルが伺えるスクリプトです.

## sam に必要な template.yaml

template.yaml (.yml ではない) を以下のように書きました.

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

Resources:
  ServerlessRubySample:
    Type: AWS::Serverless::Function
    Properties:
      Handler: sample.hello
      Runtime: ruby2.5

Outputs:
  ServerlessRubySample:
    Description: Serverless Ruby Sample Lambda Function ARN
    Value:
      Fn::GetAtt:
      - ServerlessRubySample
      - Arn
```

なんとなく CloudFormation のテンプレートっぽい佇まいです. `Runtime` の `ruby2.5` が眩しいです.

## まずはローカルでテスト

sam にもローカルで Lambda ファンクションを実行出来る `local invoke` というサブコマンドが用意されていますので, これを利用することで簡単にローカルで実行することが可能です.

```shell
$ sam local invoke ServerlessRubySample -e event_file.json
2018-11-30 08:33:13 Found credentials in shared credentials file: ~/.aws/credentials
2018-11-30 08:33:14 Invoking sample.hello (ruby2.5)

Fetching lambci/lambda:ruby2.5 Docker container image......
2018-11-30 08:33:17 Mounting /path/to/serverless-ruby-sample as /var/task:ro inside runtime container
START RequestId: 52fdfc07-2182-154f-163f-5f0f9a621d72 Version: $LATEST
END RequestId: 52fdfc07-2182-154f-163f-5f0f9a621d72
REPORT RequestId: 52fdfc07-2182-154f-163f-5f0f9a621d72  Duration: 5.64 ms       Billed Duration: 100 ms Memory Size: 128 MB     Max Memory Used: 18 MB
"bar"
```
`"bar"` が出力されることを確認出来ます. あっという間にローカル環境で Ruby で書いた Lambda ファンクションが動いてしまいました.

## あとはデプロイ

引き続き, デプロイです. sam では CloudFormation のテンプレートを書き出して, そのテンプレートを使って Create Stack するような感じでデプロイするようです. もしかしたら, Serverless Framework も同じような挙動なのかもしれません. 以下のように 2 ステップでデプロイしました.

```sh
# packaged-template.yaml を生成
sam package --template-file template.yaml \
  --output-template-file packaged-template.yaml \
  --s3-bucket oreno-sam-bucket

# packaged-template.yaml を利用してデプロイ
sam deploy --template-file packaged-template.yaml \
  --stack-name ServerlessRubySample \
  --capabilities CAPABILITY_IAM
```

以下のように出力されました.

```sh
# packaged-template.yaml を生成
$ sam package --template-file template.yaml \
> --output-template-file packaged-template.yaml \
> --s3-bucket oreno-sam-bucket
Uploading to b55e36e493168dc4d6627ee148c1ac97  1275 / 1275.0  (100.00%)
Successfully packaged artifacts and wrote output template to file packaged-template.yaml.
Execute the following command to deploy the packaged template
aws cloudformation deploy --template-file /Users/kawahara/sandboxies/sam/serverless-ruby-sample/packaged-template.yaml --stack-name <YOUR STACK NAME>

# packaged-template.yaml を使って Create Stack しているのかな...
$ sam deploy --template-file packaged-template.yaml \
> --stack-name ServerlessRubySample \
--capabilities CAPABILITY_IAM> --capabilities CAPABILITY_IAM

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

## 動作確認

動作確認も Ruby で書きましょう. 以下のように aws-sdk-lambda を使ったコードです. 事前に aws-sdk-lambda をインストールしておきましょう.

```ruby
require 'aws-sdk-lambda'
require 'json'

client = Aws::Lambda::Client.new(region: 'ap-northeast-1')

payload =<<"EOS"
{
  "Records": [
    {
      "s3": {
        "s3SchemaVersion": "1.0",
        "configurationId": "testConfigRule",
        "bucket": {
          "name": "foo",
          "ownerIdentity": {
            "principalId": "EXAMPLE"
          },
          "arn": "arn:aws:s3:::foo"
        },
        "object": {
          "key": "bar",
          "size": 1024,
          "eTag": "0123456789abcdef0123456789abcdef",
          "sequencer": "0A1B2C3D4E5F678901"
        }
      }
    }
  ]
}
EOS

resp = client.invoke({
                         function_name: 'ServerlessRubySample-ServerlessRubySample-xxxxxxxxxxx',
                         invocation_type: 'RequestResponse',
                         log_type: 'None',
                         payload: payload
                       })

res = JSON.parse(resp.payload.string)
puts res
```

`function_name` だけは, マネジメントコンソールで確認しましょう. このスクリプトを適当に invoke.rb とか名前をつけて保存しておきます.

```sh
$ bundle exec ruby invoke.rb
```

以下のように出力されました.

```sh
$ bundle exe ruby invoke.rb
bar
```

おお〜, いい感じです. CloudWatch Logs にもログが記録されていることを確認しています.

# 以上

Ruby で Lambda ファンクションが書けるなんて, 今年の頭に誰が予想していたでしょうか (結構, 予想していたりしたらすいません). 本当に嬉しい限りです.

レッツ Ruby on Lambda〜〜.