【ServerlessFramework】LambdaがCode storage limit exceededになってしまった時の対処法

ServerlessFrameworkでデプロイしようとしたら、Lambdaの上限に達してしまい、さらにはCloudformationのスタックのロールバックに失敗してどうにもならなくなってしまいました。
この問題を解決するために手こずってしまったので、覚書です。

# 初めに
この記事は己への戒めです

# 事の経緯

ある日突然、ServerlessFrameworkでデプロイしようとしたら、

```
An error occurred: 関数名 – Code storage limit exceeded. (Service: AWSLambda; Status Code: 400; Error Code: CodeStorageExceededException; Request ID: hogehoge).
```
というメッセージが返ってきて、deployをお断りされてしまいました。
さらに、

```
Stack:arn:aws:cloudformation:ap-northeast-1:8515373650097:stack/ReceptionROBO-receptionDev/e71e9510-475a-11e8-b902-503a6feb6ef is in UPDATE_ROLLBACK_FAILED state and can not be updated.
```
なんてことまで言われてしまいました。

この時点で発生している問題は、
**・アカウント単位で設定されているLambdaのコードストレージ上限（75GB）に達してしまった。**
**・CloudFormationのスタックのロールバックに失敗してしまった。**
の２点です。

**Lambdaの問題**
Lambdaには様々な[制限](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/limits.html)があり、Lambdaのコードストレージの上限は75GBとなっています。
コードストレージにはLambda関数のバージョンが含まれており、Serverlessframeworkで特に何も指定していない場合、現在の一番新しいバージョン（$LATEST）以外に、歴代の古いバージョンが蓄積されていきます。これがコードストレージを圧迫していき、ついに75GBに達してしまったようです。

**CloudFormationの問題**
CloudFormationのステータスですが、[公式](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/cfn-console-view-stack-data-resources.html)によると、UPDATE_ROLLBACK_FAILEDは「スタックの更新の失敗後、1 つ以上のスタックを前の動作ステートに戻すことに失敗」した状態です。
Lambdaのコードストレージが上限に達し、デプロイに失敗したことでスタックの更新にも失敗してしまった。しかし、Lambdaがどうにもならない状態になっているので、ロールバックもできない、ということでしょうか・・・（間違っていたら教えてください）

とりあえず、
CloudFormationのスタックを正常にロールバックし、ステータスをUPDATE_ROLLBACK_COMPLETEにして、かつLambdaのコードストレージから不要なバージョンを削除すれば良さそうです。


# 対処法
結論を先に言ってしまうと、この問題に対する対処法は
・CloudFormationのスタックを正常にロールバックし、ステータスをUPDATE_ROLLBACK_FAILED→UPDATE_ROLLBACK_COMPLETEにする。
・Lambdaの不要なバージョンを消す。
・再発を防ぐため、Lambdaのバージョン管理するプラグインを入れる。
です。

# やったこと
## 1.CloudFormationのスタックをロールバックする。
ロールバックが既に失敗している時点で、一筋縄ではいかないことは容易に想像がつきます・・・
[公式のトラブルシューティング](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/troubleshooting.html#troubleshooting-errors-update-rollback-failed)を確認すると、更新のロールバックを強制的に続行することができるようです（失敗することもあります。というか、失敗することの方が多いようですが・・・）。
それでダメなら失敗の原因になっているリソースをスキップしてロールバックするように書いてありました。
とりあえずやってみます。

CloudFormationのコンソール画面に行き、左側のスタック一覧から、問題を起こしているスタックを選択します（スタックの名前はエラーに書いてあります）。
この時、状況の理由欄に、なぜロールバックが失敗してしまったのかが書いてあります。ロールバックがうまくいかない原因になっているリソース名が書いてあるので、確認しておきます。

<img width="266" alt="スクリーンショット 2019-08-30 20.14.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325419/6e98169d-c62f-c270-a159-0c4bb3aca3a7.png">
上の方にあるスタックアクションをクリックし、更新のロールバックを続行を選択します（画像はクリックできないようにグレーアウトになってしますが、ステータスがUPDATE_ROLLBACK_FAILEDの時はクリックできます）。
<img width="868" alt="スクリーンショット 2019-08-29 10.36.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325419/ffe6c514-569d-2fb8-6373-7c454908614d.png">
ロールバック時にスキップするリソースの一覧が出てきます。
スキップするリソースにチェックを入れて、左下の更新ロールバックを続けるをクリックします。

<img width="266" alt="スクリーンショット 2019-08-29 10.40.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325419/b3868c83-4ef8-9ba7-c970-4e026f1f4038.png">

しばらく待って成功すれば、ステータスがUPDATE_ROLLBACK_COMPLETEに変わります。
UPDATE_ROLLBACK_COMPLETEは「スタックの更新の失敗後、1 つ以上のスタックを前の動作状態に正常に戻した状態」です。
これでCloudFormationの問題の方は解決・・・？


## 2.Lambdaの不要なバージョンを消す。
CloudFormationのステータスがUPDATE_ROLLBACK_COMPLETEになったからといって、この段階でデプロイできるようになるわけではありません。
根本的な原因＝Lambdaのコードストレージに空きがない問題を解決していないからです。

試しにLambdaのコンソール画面から様子を伺ってみると、
<img width="1130" alt="スクリーンショット 2019-08-28 19.34.29.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325419/a053b3a8-bbba-f756-82b1-c01e159c3f4e.png">
コードストレージがバッチリ100%になっちゃってます。
ちなみにバージョンの方も確認してみると、
<img width="318" alt="スクリーンショット 2019-08-29 10.35.56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325419/6bb29fcc-101b-dfa2-eab3-48a8474aed0a.png">
うわっ・・・（引）
こんな状態の関数がいっぱいあるわけですから、75GBあっても足りなくなってしまうわけです・・・
いくら一つ一つの関数のサイズが小さくても、大量のバージョンをぶら下げていたら意味ないですね・・・

単純にこれらのバージョンを消していけばいいだけなのですが、数が膨大すぎるので手作業ではやりたくありません。
どうしようかと思って調べていたところ、[こちら](https://epsagon.com/blog/free-lambda-code-storage-exceeded/#)にいい解決策がありました。
この記事で紹介している[clear-lambda-storage](https://github.com/epsagon/clear-lambda-storage)を使うと、溜まりに溜まったLambda関数の古いバージョンを一気に消してくれます。やってみましょう。

やるといっても、本当にシンプルで、

```
git clone https://github.com/epsagon/clear-lambda-storage
cd clear-lambda-storage/
pip install -r requirements.txt
python clear_lambda_storage.py --token-key-id <access_key_id> --token-secret <secret_access_key>
```
これだけなのですが、

```
An error occurred (UnrecognizedClientException) when calling the ListFunctions operation: The security token included in the request is invalid.
```
エラーです。

```
Scanning ap-east-1 region
```
の出力があった後に失敗していたので、リージョンと認証情報が噛み合ってない・・・？
tokenの有効期限も確認しましたが、問題なし。
Lambdaは東京リージョンを使っていたので、東京だけみてくれたらいいのになーと思い、ソースコードをみてみることにしました。

```python:clear_lambda_storage.py
def list_available_lambda_regions():
    """
    Enumerates list of all Lambda regions
    :return: list of regions
    """
    session = Session()
    return session.get_available_regions('lambda')

```
ここでLambdaが使用可能なリージョン一覧をリストで取ってきて、

```python:clear_lambda_storage.py
def remove_old_lambda_versions(args):
    """
    Removes old versions of Lambda functions
    :param args: arguments
    :return: None
    """
    regions = list_available_lambda_regions()
    total_deleted_code_size = 0
    total_deleted_functions = {}
    num_to_keep = 2

    if args.num_to_keep:
        num_to_keep = args.num_to_keep

    for region in regions:
        print('Scanning {} region'.format(region))
        ##以下省略

```
regionsの中にあるリージョン分削除を繰り返す、というような動きのようです。
東京だけやれたらいいので、

```python:clear_lambda_storage.py
    #regions = list_available_lambda_regions()
    regions = ['ap-northeast-1']
```
こうしました（最低）。

もう一度やってみます。

```
$ clear-lambda-storage ootaizumi$ python clear_lambda_storage.py --token-key-id <access_key_id> --token-secret <secret_access_key>
Scanning ap-northeast-1 region
Detected ReceptionROBO-receptionDev-Get_ReservationInfo_Date with an old version 1
Detected ReceptionROBO-receptionDev-Get_ReservationInfo_Date with an old version 2
Detected ReceptionROBO-receptionDev-Get_ReservationInfo_Date with an old version 3
Detected ReceptionROBO-receptionDev-Get_ReservationInfo_Date with an old version 4
......長すぎるので省略
```
古いバージョンの検出が始まりました。
あまりにも多すぎて、手に汗を握りながら眺めていたのですが、多い関数で400くらいありました。ひええ・・・

```
----------
Deleted 6424 versions from 34 functions
Freed 83916 MBs
```
終わったようです。
コンソール画面を確認してみると、
<img width="835" alt="スクリーンショット 2019-08-29 20.37.52.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325419/f5da92ea-5303-a2de-7d0b-76e743ffcf9d.png">
すごい減ってる・・・！

ここまでやって、ようやくdeployできるようになりました。長かった・・・

## 3.Lambdaのバージョン管理を行うプラグインを導入する。
何もしなければ、いつか同じ問題が再発してしまうので、このタイミングで対策しておきます。

Lambda関数のバージョンは、Serverlessframeworkでデプロイした時に溜まっていきます。
必要位以上に古いバージョンを溜め込まないように、[Serverless Prune Plugin](https://github.com/claygregory/serverless-prune-plugin)を使います。
Serverless Prune Pluginは、デプロイする度に溜まっていく古いバージョンを適宜削除してくれるプラグインです。
早速使ってみます。
Serverlessと同じ階層で

```
$ sls plugin install -n serverless-prune-plugin
```
これだけです。
Serverless Prune Pluginがインストールされ、serverless.ymlに

```yml:serverless.yml
plugins:
  - serverless-prune-plugin
custom:
  prune:
    automatic: true
    number: 3
```
この記述が追加されます。
**automatic**をtrueにしておくと、Serverlessでデプロイした時にこのプラグインが自動的に実行されます。
**number**は残しておくバージョンの数です。デフォルトは３になっているようです。

この状態でデプロイしていると、
<img width="310" alt="スクリーンショット 2019-09-04 10.21.52.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325419/35964b6a-ddd1-4038-0bf0-d5115b1a7dfa.png">
こんな感じで、最新のバージョン（$LATEST）と3つ前のバージョンまで残るようになります。
関数の数にもよりますが、この程度ならコードストレージを圧迫してしまうこともないでしょう。きっと。

# 終わりに
今回のことは、事前にきちんと対策をしていれば防げたことだと思います。
サーバーレスってとても便利で、サクサクできるので開発していてすごく楽しいのですが、やることはきちんとやっておかないといけないなと思いました。反省・・・
もっと勉強しよう・・・

# 参考
・[How to Free AWS Lambda Code Storage when Exceeded](https://epsagon.com/blog/free-lambda-code-storage-exceeded/#)
・[Serverless Frameworkを本番運用する際にやっておいたほうが良い事](https://qiita.com/keitakn/items/22cbc3e00acb79560b87)