AWS LambdaがContainer Image Support 言うから 疑似Cloud Run(Rails on Lambda)

# はじめに

メリークリスマス！！ :christmas_tree: :christmas_tree: :santa: :christmas_tree: :christmas_tree:

[Ateam Lifestyle Advent Calendar 2020](https://qiita.com/advent-calendar/2020/ateam-lifestyle)の25日目は、
株式会社エイチームライフスタイル 執行役員CTO @tsutorm が担当します！


# Lambdaがコンテナイメージサポートと聞いて

今年のアドカレどうしよっかなー :thinking: 

ポエム系になるけど 「ビジネスが間違えやすいエンジニアの"開発生産性"」 みたいなのを書こうかなー

せっかくのクリスマス :christmas_tree: だしなー。プレゼントになるようなコードがあるほうがいいよなー

と思っていた所...

---
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76630/25f26eea-359d-5ec6-8190-d5d1020c49b7.png)

[【速報】Lambdaのパッケージフォーマットとしてコンテナイメージがサポートされるようになりました！！ #reinvent
](https://dev.classmethod.jp/articles/lambda-support-oci-container-image/#toc-3)

---

それなんて[Cloud Ru...](https://cloud.google.com/run) とおもったけど、ちょっと違うか？

- [AWS SAM CLI で Lambda Container Support をお試し](https://qiita.com/hayao_k/items/be0b9f984dd9ebf31696)
- [爆速？！コンテナイメージからデプロイしたLambdaのコールドスタートについて検証してみた #reinvent](https://dev.classmethod.jp/articles/measure-container-image-lambda-coldstart/)

既にいくつかの触った系記事が出てますが(みなさん早いですねー)、思ったほどCloud Runじゃない模様。

- Lambdaで API Gatewayとガッチャンコして Cloud Run的なやつだと思ったのになー
- [BaseImageはAmazonLinux2っぽい](https://docs.aws.amazon.com/lambda/latest/dg/ruby-image.html)ので、Dockerfileさえちゃんと作れば原理的にはRailsとかも動くよなぁー
- Aurora ServerlessとかRDS Proxyとかあるし、WebAPIくらい作れそうなのになぁー


...


いっちょやってみるか！

# Rails API on AWS Lambda Run(仮) 動くぞ

```shell
$ # データぶっこんだりなんかはRailsなんでRESTとかseedsとかでやってくれい
$ curl https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/posts  # GET /posts
[{"id":1,"title":"test","body":"hoge","published":null,"created_at":"2020-12-23T11:31:39.626Z","updated_at":"2020-12-23T11:31:39.626Z"},{"id":2,"title":"test2","body":"hoge","published":null,"created_at":"2020-12-23T12:11:04.785Z","updated_at":"2020-12-23T12:11:04.785Z"},{"id":3,"title":"test3","body":"hoge","published":null,"created_at":"2020-12-23T13:29:04.307Z","updated_at":"2020-12-23T13:29:04.307Z"}]
```

```
START RequestId: 87bfb3ea-568d-477a-aa52-809fc14c3d40 Version: $LATEST
I, [2020-12-24T06:35:23.744874 #8]  INFO -- : [de7866af-0a31-4bd7-a7f1-9b15aeb864dc] Started GET "/posts" for xx.xx.xx.xx at 2020-12-24 06:35:23 +0000
I, [2020-12-24T06:35:23.746365 #8]  INFO -- : [de7866af-0a31-4bd7-a7f1-9b15aeb864dc] Processing by PostsController#index as */*
I, [2020-12-24T06:35:23.960053 #8]  INFO -- : [de7866af-0a31-4bd7-a7f1-9b15aeb864dc] Completed 200 OK in 214ms (Views: 212.8ms | ActiveRecord: 27.8ms | Allocations: 6188)
END RequestId: 87bfb3ea-568d-477a-aa52-809fc14c3d40
REPORT RequestId: 87bfb3ea-568d-477a-aa52-809fc14c3d40	Duration: 220.86 ms	Billed Duration: 3193 ms	Memory Size: 1024 MB	Max Memory Used: 158 MB	Init Duration: 2971.41 ms
```

## 全体感

### 構成全景

言うほど複雑じゃないけど、図にするとこんな感じ 
![undefined.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76630/ad823eeb-99c5-ec3d-feb7-80c62e64b409.png)

そんだけ

### コード

[githubに上げました。](https://github.com/tsutorm/lambda-run-sample)

アプリケーションとしては、`rails new --api` なものに、有る有るの`post`モデルを追加しただけの大変シンプルなやつなんで、既存のガッツリ作ったRailsアプリケーションをFitされるとどうなるか。というのは又別の問題になるかと思います。。

deploy周りはserverlessに全部投げました。大変に便利。

docker buildは一部シェルにしてありますが、公式のドキュメントみてればそれほど難しい感じではなかったです。

Aurora Serverlessの構築だけ手動でやりましたが、そんな難しい話じゃないのでここでの手順説明は割愛します。

### build & deploy

完全にスクリプト化できてなくてごめんけど大体以下

- buildしてイメージ作る `./build.sh`
- image hashを含む、deployに必要な情報を `dev.yml` に記載する
- `serverless deploy`
- 初回だけ `rails db:migrate` 相当のLambda functionをCLI経由でinvokeしておく

```shell
$ aws lambda invoke --function-name lambda-run-dev-migrate --payload '{}' response.json # migration
```

## ハマった所、躓いた所

### Dockerfile.lambda

#### AWS提供のBaseImage or [Ruby runtime interface clients](https://github.com/aws/aws-lambda-ruby-runtime-interface-client)を入れた自分のImageの二択 がわからん

- [公式ドキュメント](https://docs.aws.amazon.com/lambda/latest/dg/ruby-image.html) ちゃんと読めって話である
- 最初AlpineベースでDockerfileを書いたけど、aws_lambda_ric入れなきゃいかんのがちょっとなんかGemfileに侵入されて嫌。公式BaseImageで作り直した
- AWS提供のBaseImageはRuby 2.5/2.7しか無い

```dockerfile
FROM public.ecr.aws/lambda/ruby:2.7.2020.12.18.21
```

#### BaseImageは`entrypoint`に予め`aws_lambda_ric`が指定されてるので `docker run -it public.ecr.aws/lambda/ruby:2.7 /bin/sh`すると入力受け付けなくて :thinking: 

**before**

```shell
$ docker run --rm -it public.ecr.aws/lambda/ruby:2.7 /bin/sh
INFO[0000] exec '/var/runtime/bootstrap' (cwd=/var/task, handler=)
# このまま 
```

**after**

```shell
$ docker run --rm --entrypoint="" -it public.ecr.aws/lambda/ruby:2.7 /bin/sh
sh-4.2#
```

私はこれで `yum install` したりアレヤコレヤ弄りながら `Dockerfile` 書いていくスタイル

### serverless

基本は[すごくわかりやすいエントリがあったのでそれを参考にした](https://www.serverless.com/blog/container-support-for-lambda)だけ

#### `CMD`や`ENTRYPOINT`の上書きにまだ対応してない

`rails db:migrate`する別のfunctionを別imageで用意して回避

```dockerfile
# Dockerfile.lambda
# 略
FROM app as migrate

WORKDIR /var/task

#中略

CMD ["lambda.App::Handler.migrate"]
```

```ruby
# lambda.rb
# 略
    def self.migrate(event:, context:)
      p `bin/rails db:create`
      p `bin/rails db:migrate`
      response = {
        'statusCode' => 204
      }
    end
end
```


#### Auroraと繋ぐのであればVPC Lambdaにしないといけない

すっかり忘れてて普通に作ってしまったので作り直した。
serverlessだったので `serverless remove && serverless deploy`で簡単

```yaml
# serverless.yml
  vpc:
    securityGroupIds:
      - ${self:custom.environment.${self:provider.stage}.MY_AWS_SECURITY_GROUP}
    subnetIds:
      - ${self:custom.environment.${self:provider.stage}.MY_AWS_SUBNET_ID_0}
      - ${self:custom.environment.${self:provider.stage}.MY_AWS_SUBNET_ID_1}
      - ${self:custom.environment.${self:provider.stage}.MY_AWS_SUBNET_ID_2}
  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - "ec2:CreateNetworkInterface"
        - "ec2:DescribeNetworkInterfaces"
        - "ec2:DeleteNetworkInterface"
      Resource:
        - "*"
```

### Rails on Lambda

#### そもそも LambdaからRailsをどう動かすの？？

[Ruby on Rails on AWS Lambda and API Gateway by Serverless Frameworkを試してみた](https://nihemak.hatenablog.com/entry/2019/01/15/015802) で先人が居たので助かった。
もうほぼこれ。大感謝。

`aws_lambda_ric` → `lambda.rb` → `config.ru` で動く。 

ハンドラから`rack`に渡すところのコードは以下をベースに`config.ru`の参照パスだけ変えた。

https://github.com/aws-samples/serverless-sinatra-sample/blob/master/lambda.rb

そのため、Railsではアプリケーションサーバとして一般的な`puma`は起動しない :exclamation: :exclamation: 

結果、invoke時に発生すると想定していた puma起動オーバーヘッドが削減されるという恩恵が有るような気がする。

目からウロコ。棚からぼたもち。

#### WORKDIRの`/var/task/` は書き込み禁止なので `Rails.root.join('tmp')` に書き込む処理が全部エラー

主に今回ハマったのは `bootsnap` でのキャッシュファイルと `tmp/log/*.log`

##### bootsnapのキャッシュが `tmp/cache` に書けない

環境変数を使ってbootsnapキャッシュの出力先を `/tmp` に変更して回避 [bootsnapドキュメントの通り](https://github.com/Shopify/bootsnap)

```dockerfile
# Dockerfile.lambda
ENV BOOTSNAP_CACHE_DIR=/tmp/cache
```

##### `tmp/log/development.log` にログが書けないので `STDOUT` にログを出力するように変更

普通にcloudwatch logsに乗らなくて困るのでこれはやるだけだし、その時は`RAILS_ENV=development`だったという話もある。`config/environments/development.rb` にも書いておくとよい

```ruby
# config/environments/development.rb
  if ENV["RAILS_LOG_TO_STDOUT"].present?
    logger           = ActiveSupport::Logger.new(STDOUT)
    logger.formatter = config.log_formatter
    config.logger    = ActiveSupport::TaggedLogging.new(logger)
  end
```

```dockerfile
# Dockerfile.lambda
ENV RAILS_LOG_TO_STDOUT=1
```

## 負荷掛けるとどんな感じか

### 一発だけリクエスト投げた時のリソース状況

#### コールドスタート

```shell
$ hay -c 1 -n 1 https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/posts

Summary:
  Total:        3.7599 secs
  Slowest:      3.7597 secs
  Fastest:      3.7597 secs
  Average:      3.7597 secs
  Requests/sec: 0.2660

  Total data:   408 bytes
  Size/request: 408 bytes

Response time histogram:
  3.760 [1]     |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  3.760 [0]     |
  3.760 [0]     |
  3.760 [0]     |
  3.760 [0]     |
  3.760 [0]     |
  3.760 [0]     |
  3.760 [0]     |
  3.760 [0]     |
  3.760 [0]     |
  3.760 [0]     |


Latency distribution:
  0% in 0.0000 secs
  0% in 0.0000 secs
  0% in 0.0000 secs
  0% in 0.0000 secs
  0% in 0.0000 secs
  0% in 0.0000 secs
  0% in 0.0000 secs

Details (average, fastest, slowest):
  DNS+dialup:   0.1575 secs, 3.7597 secs, 3.7597 secs
  DNS-lookup:   0.1406 secs, 0.1406 secs, 0.1406 secs
  req write:    0.0001 secs, 0.0001 secs, 0.0001 secs
  resp wait:    3.6014 secs, 3.6014 secs, 3.6014 secs
  resp read:    0.0006 secs, 0.0006 secs, 0.0006 secs

Status code distribution:
  [200] 1 responses
```

#### ウォームスタート

```shell
$ hay -c 1 -n 1 https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/posts

Summary:
  Total:        0.0556 secs
  Slowest:      0.0556 secs
  Fastest:      0.0556 secs
  Average:      0.0556 secs
  Requests/sec: 17.9856

  Total data:   408 bytes
  Size/request: 408 bytes

Response time histogram:
  0.056 [1]     |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  0.056 [0]     |
  0.056 [0]     |
  0.056 [0]     |
  0.056 [0]     |
  0.056 [0]     |
  0.056 [0]     |
  0.056 [0]     |
  0.056 [0]     |
  0.056 [0]     |
  0.056 [0]     |


Latency distribution:
  0% in 0.0000 secs
  0% in 0.0000 secs
  0% in 0.0000 secs
  0% in 0.0000 secs
  0% in 0.0000 secs
  0% in 0.0000 secs
  0% in 0.0000 secs

Details (average, fastest, slowest):
  DNS+dialup:   0.0159 secs, 0.0556 secs, 0.0556 secs
  DNS-lookup:   0.0052 secs, 0.0052 secs, 0.0052 secs
  req write:    0.0003 secs, 0.0003 secs, 0.0003 secs
  resp wait:    0.0386 secs, 0.0386 secs, 0.0386 secs
  resp read:    0.0005 secs, 0.0005 secs, 0.0005 secs

Status code distribution:
  [200] 1 responses
```

Auroraはキャパ1で動作する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76630/a536f15d-9d50-93ce-141a-3613c637466f.png)

### 負荷掛けた場合 (300並列 / 3000リクエスト)

```shell
$ hay -c 300 -n 3000 https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/posts

Summary:
  Total:        7.3692 secs
  Slowest:      5.1843 secs
  Fastest:      0.0223 secs
  Average:      0.4378 secs
  Requests/sec: 407.1016

  Total data:   370236 bytes
  Size/request: 123 bytes

Response time histogram:
  0.022 [1]     |
  0.538 [2645]  |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  1.055 [59]    |■
  1.571 [3]     |
  2.087 [0]     |
  2.603 [2]     |
  3.120 [5]     |
  3.636 [20]    |
  4.152 [255]   |■■■■
  4.668 [9]     |
  5.184 [1]     |


Latency distribution:
  10% in 0.0351 secs
  25% in 0.0404 secs
  50% in 0.0490 secs
  75% in 0.0728 secs
  90% in 0.8516 secs
  95% in 3.8275 secs
  99% in 4.0212 secs

Details (average, fastest, slowest):
  DNS+dialup:   0.0331 secs, 0.0223 secs, 5.1843 secs
  DNS-lookup:   0.0079 secs, 0.0000 secs, 0.1565 secs
  req write:    0.0009 secs, 0.0000 secs, 0.2680 secs
  resp wait:    0.3979 secs, 0.0222 secs, 4.7219 secs
  resp read:    0.0004 secs, 0.0000 secs, 0.1758 secs

Status code distribution:
  [200] 907 responses
  [500] 2093 responses
```

さばけてなーい。原因はRDS側のキャパが足りずなので、一定時間立つと...

```shell
$ hay -c 300 -n 3000 https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/posts

Summary:
  Total:        1.3416 secs
  Slowest:      0.7870 secs
  Fastest:      0.0215 secs
  Average:      0.1167 secs
  Requests/sec: 2236.2066

  Total data:   844968 bytes
  Size/request: 281 bytes

(略)

Status code distribution:
  [200] 2071 responses
  [500] 929 responses
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76630/064073f4-9808-5d3f-4007-28a1b5f4cd24.png)

Auroraもサーバレスなんで、勝手にキャパが増えて勝手にだいぶさばける！！！

### 一定時間経つと...

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76630/1827a588-060d-2ac1-90cf-bb525b17a2e6.png)

ゼロに向かってスケールインするよ

## 手がつけられなかった所

### ActiveJob(sidekiq)もLambdaにしちゃう

[aws-sdk-rails gemを使ってハンドラを Aws::Rails::SqsActiveJob.lambda_job_handler にするだけでできそう](https://aws.amazon.com/jp/blogs/developer/introducing-new-features-for-the-aws-sdk-rails-gem/)

### ECS Fargate/EKSとのコスト比較

どう考えても有る一定以上のワークロードが有るような商用利用のケースではLambdaの分が悪いはず。とは思う。

# まとめ

- 面白かったのでよかった。
- [Cloud Run](https://cloud.google.com/run)で良い気はする。[Jetsとかも有るよね](https://rubyonjets.com/)という話は十分承知している。悔いはない。
- 巨大なRailsだとどうかはわからん。検証不足。

# おわりに

[Ateam Lifestyle Advent Calendar 2020](https://qiita.com/advent-calendar/2020/ateam-lifestyle)もこれですべて終了です。いかがだったでしょうか？

運営に動いてくれた @engabesi, @asasigure ありがとうございました！

2020年は主にCovid-19もあり、激動の一年でしたね。

来年2021年はどんな年になるんでしょうか。楽しみです！

それではみなさん Happy Holidays!!! :tada: