AWS Lambda でRuby のREST APIをブラウザだけで作れるよって話

この記事は[Goodpatch Advent Calendar 2018](https://qiita.com/advent-calendar/2018/goodpatch)の17日目の記事です。
諸般の事情（家庭内パンデミック！）の影響で記事執筆が遅れたことをお詫びします:bow:

私は普段Ruby on RailsのREST APIのインフラ・バックエンド周りを担当しています。
先日のAWSのイベント [AWS re:Invent 2018 で発表された新サービスと機能 | AWS](https://aws.amazon.com/jp/new/reinvent/) の発表がどれも素晴らしいものばかりでした。
今回はお気軽に試せそうなAWS Lambdaの Ruby対応を試してみましたが、
今までRuby on Railsのみで乗り越えてきた方や、プログラミング初心者の方でも、REST APIやバックエンドで起こっていることについて、理解しやすい感じがしたので、初心者目線で紹介します。
（途中から若干その目線を忘れているかも:sweat:）

* この記事はWebブラウザだけで完結することを縛りにしました。
* AWSのアカウントは作成済み、Tokyoリージョンにあるものという前提。
* CloudWatchについては今回触れません。
* Rubyの `Lambda関数` ≠ `AWS Lambda` のRubyなので、以降 `Lambda` という場合は  `AWS Lambda` であると思ってください。

## 作ってみるもの
クリスマスっぽく、

*messageを送るとサンタさんからの簡単なreplyが返ってくる*
（ `POST /messages` でreplyを返却）

という単純なものにします。

## AWS Lambda でRubyを動かす
### Lambdaの関数を作成
AWSコンソールにログイン後、Lambda > 関数 > 関数の作成で、今回は1から作成してみます。
<img width="1091" alt="Lambda_1.png" src="https://qiita-image-store.s3.amazonaws.com/0/25926/9917411e-be46-124f-a934-4479ebbed108.png">

以下のように適当に入力します。
ロール名にはあまり意味がありませんが、santaにしておきますか。
<img width="1059" alt="Lambda_2.png" src="https://qiita-image-store.s3.amazonaws.com/0/25926/bc6ffde5-e929-3531-8f46-511bec9908af.png">

できました！早速「テスト」をしてみます

<img width="1087" alt="Lambda_3.png" src="https://qiita-image-store.s3.amazonaws.com/0/25926/3bc99c85-7707-e2d1-0ebe-2c941ee20843.png">

### テストイベントを作成
PostMessageなどのテストイベント名にして以下のようなお願いをしてみます。

<img width="838" alt="Lambda_4.png" src="https://qiita-image-store.s3.amazonaws.com/0/25926/3aadbd49-f15b-ae62-f67d-d2aa542fbcc0.png">

```
{
  "message": "Please give me something like special, Santa!"
}
```

成功！

<img width="1087" alt="Lambda_5.png" src="https://qiita-image-store.s3.amazonaws.com/0/25926/4f7149c6-3b6a-7a87-341d-c9be7d4a0920.png">

でも `Hello from Lambda!` とか言われても子供たちには意味がわかりません。
サンタは何を考えているんでしょうか :thinking: 
しかしこれは関数のデフォルトコードを全く変更してないからですねw

### Lambdaの関数をCloud9で編集

関数のコードの編集はCloud9というツールで行います。
ブラウザ上から変更可能で意外と便利だなと思いました。
まだRubyのFormatterが動作しないようなので、そこは今後に期待ですね。
<img width="1134" alt="Cloud9_1.png" src="https://qiita-image-store.s3.amazonaws.com/0/25926/64f98f92-f034-5125-5d76-f985a91f1d3f.png">

初期のファイルには以下のように書かれています。

```lambda_function.rb
require 'json'

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

まず、この関数の引数 `event:, context:` がどんな情報かを確かめるために一旦以下のようにを全て出力してみましょう。

```lambda_function.rb
def lambda_handler(event:, context:)
    res = {
        event: {
            class: event.class,
            inspect: event.inspect,
        },
        context: {
            class: context.class,
            inspect: context.inspect,
        },
    }
    { statusCode: 200, body: res.to_json }
end
```

ここで間違えやすいのが、ファイルの保存後、右上の「保存」もしないと保存されません。

再度「テスト」を走らせると、以下のようなレスポンスが返ってきていることがわかります。

```json
{
  "statusCode": 200,
  "body": "{\"event\":{\"class\":\"Hash\",\"inspect\":\"{\\\"message\\\"=>\\\"Please give me something like special, Santa!\\\"}\"},\"context\":{\"class\":\"LambdaContext\",\"inspect\":\"#<LambdaContext:0x0000555970e298d8 @clock_diff=xxxxxxxxxxx, @deadline_ms=xxxxxxxxxxxx, @aws_request_id=\\\"xxxxxxxxxxxxxxxx\\\", @invoked_function_arn=\\\"arn:aws:lambda:ap-northeast-1:XXXXXXXX:function:post_message\\\", @log_group_name=\\\"/aws/lambda/post_message\\\", @log_stream_name=\\\"yyyy/mm/dd/[$LATEST]XXXXXXXXXXXX\\\", @function_name=\\\"post_message\\\", @memory_limit_in_mb=\\\"128\\\", @function_version=\\\"$LATEST\\\">\"}}"
}

```

`event` はリクエストボディを `Hash` で
`context` はLambdaの実行環境の `LambdaContext` が返ってきていることがわかりました。

#### messageの取得
eventはHashなので、`event['message']` でメッセージが取得できそうです。
contextはこの APIでは利用することはないので扱いません。

#### replyの作成
一旦雑にgeneratorを定義して固定の文言を返します。
これもFile > NewでCloud9で新しいrbファイルを追加して `lambda_function.rb` と同じフォルダにおいておきます。

```reply_generator.rb
class ReplyGenerator
    def self.generate(message)
        'Ho! Ho! Ho!'
    end
end
```

そしてそれを `lambda_function.rb` から呼び出すようにしておきます。

```lambda_function.rb
require 'reply_generator'

def lambda_handler(event:, context:)
    message = event['message']
    return { statusCode: 400, body: {reply: 'error'}.to_json } if message.nil?
    reply = ReplyGenerator.generate(message)
    res = {
        reply: reply
    }
    { statusCode: 200, body: res.to_json }
end
```
入力チェックはひとまず簡単にnil判定だけにしちゃいます。（空文字でも通る）

これで「テスト」すると・・・！

```json
{
  "statusCode": 200,
  "body": "{\"reply\":\"Ho! Ho! Ho!\"}"
}
```

Ho!Ho!Ho!
これでサンタのメッセージが返ってきました。

もうここらで良いかなと思いましたがせっかくなのでインターネットから呼び出したいですよね。

## API GatewayでAWS Lambdaを動かす
API Gatewayを設定することでLambdaがインターネットから呼び出せるようになります。

* AWS LambdaのDesignerからAPIを追加できるのですが、そうするとちょっとややこしかったので、API Gatewayを直接追加していくことにします。


### APIの作成

APIゲートウェイへ移動し、APIの作成を行います。
<img width="901" alt="API_1.png" src="https://qiita-image-store.s3.amazonaws.com/0/25926/c540fdfc-a2a2-c712-7052-35f0e004ae3f.png">
名前は適当に `Santa-Message-API` とかにしました。

### リソースの作成
エンドポイントは `messages` になるように設定します。
<img width="951" alt="API_2.png" src="https://qiita-image-store.s3.amazonaws.com/0/25926/073dc3c6-1690-b418-951a-0eeb676c0089.png">


### メソッドの作成
POSTメソッドを追加します。
<img width="972" alt="API_3.png" src="https://qiita-image-store.s3.amazonaws.com/0/25926/b7de9407-8692-eccf-f951-5eaf5d023f2e.png">

### テスト

早速テストしてみましょう！
<img width="976" alt="API_4.png" src="https://qiita-image-store.s3.amazonaws.com/0/25926/9b5ce369-b7c9-8a61-cfd4-7ac6ab4ab1d5.png">

テストをクリックした後、 `リクエスト本文` には以下のような大人のお願いを入れておきます。

```json
{
    "message": "Oh, Santa! Give me more money!"
}
```

ちゃんと返ってきましたね！
<img width="528" alt="API_5.png" src="https://qiita-image-store.s3.amazonaws.com/0/25926/32ac5281-5bd7-7f56-550d-bf54585262ba.png">

### APIのデプロイとステージの作成

ここで「APIのデプロイ」すると、世界中のインターネットからアクセス可能になります。
セキュリティや利用制限など気になる方はAPIキーの設定、またはスロットリングの設定を適切に行なってください。

API>Santa-Message-API>ステージから「作成」します。
<img width="603" alt="API_stage_1.png" src="https://qiita-image-store.s3.amazonaws.com/0/25926/0d027ce0-7315-d4e5-f33e-44de40b6876c.png">

ステージの名前は普通は`beta` とか`production` とかですがノリで `christmas` にしておきますか。

<img width="975" alt="API_stage_2.png" src="https://qiita-image-store.s3.amazonaws.com/0/25926/81d47aad-3cab-b601-040f-f09bf31a0b39.png">
できました！

* 念の為スロットリング設定を10 request/秒で設定しました。それくらいで十分でしょ。

作成した`christmas` のステージからメソッドをたどって該当のAPIのURIまで辿り付けます。
こんなURIになります。
`https://XXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/christmas/messages`

### インターネットから試してみる
普段はcurlなどから叩きたいですが、ブラウザだけで完結したいので [API Tester](https://apitester.com/) という外部サービスを使ってみます。

メッセージには雑に心無いメッセージを入れておきます。
<img width="742" alt="Internet_1.png" src="https://qiita-image-store.s3.amazonaws.com/0/25926/942bc13a-8512-a72f-c4ff-cce7dc42a8c1.png">
できました！

しかしここらでいかなるお願いにも`Ho! Ho! Ho!` で返すサンタに苛立ちを隠しきれません。

## ちゃんとメッセージを返す
さて、ここらでさっき適当に作ってしまった、 `ReplyGenerator` をもう少しだけマシにしましょう。

### メッセージをランダムに返す
簡単ですね。

```reply_generator.rb
class ReplyGenerator
    REPLIES = [
        'Ho! Ho! Ho!',
        'Thanks for your message!',
        'Hope your enjoy with my present!',
        'Merry Chrinstmas!',
        'I wish Happy New Year!'
    ]
    def self.generate(message)
        REPLIES.sample
    end
end
```

### 良くないメッセージは適当に流す
適当に流す文章を作るのに一番時間がかかりました。
フィルタリングは雑にw

```reply_generator.rb
class ReplyGenerator
    module Replies
        TO_GOODIES = [
            'Ho! Ho! Ho!',
            'Thanks for your message!',
            'Hope your enjoy with my present!',
            'Merry Chrinstmas!',
            'I wish Happy New Year!'
        ]
        TO_BADDERS = [
            'Hey! Did you mean it? Merry Chrinstmas.',
            'Work hard and keep calm, and hope do your best!',
            'Take a better rest. and I wish your Happy New Year.'
        ]
    end

    CHECK_REGEX = /money|(go away)/

    class << self
        def generate(message)
            detect_reply(message).sample
        end
        
        private
        
        def detect_reply(message)
            CHECK_REGEX.match?(message&.downcase) ? Replies::TO_BADDERS : Replies::TO_GOODIES
        end
    end
end
```

これで振り分けもできましたw

### インターネットのエンドポイントからもテスト

以下のようにリクエストすると

```json
{"message": "Hey, Santa! Go away!"}
```

ちゃんと返ってきましたね。

```json
{"statusCode":200,"body":"{\"reply\":\"Work hard and keep calm, and hope do your best.\"}"}
```

これでサンタさんは心無い大人にも純粋な子供にもメッセージを返すことができるようになりました :relaxed:

:santa:めでたしめでたし:santa:

## まとめ
* AWS LambdaのRubyは簡単に実行だった
* Cloud9使えば全てブラウザから編集できる
* API Gatewayの設定も意外と簡単なので、ブラウザだけで APIが作れる

と、いう初心者向けの内容でした。

* Rubyはわかるけどインフラよくわからない
* AWS怖い・・・
* LambdaってNodeかPythonのものだったよね・・・

という方は是非AWS Lambdaで遊んでみてくださいね。

では Merry Christmas!
HO!HO!HO!