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のスタックのロールバックに失敗してしまった。
の2点です。
Lambdaの問題
Lambdaには様々な制限があり、Lambdaのコードストレージの上限は75GBとなっています。
コードストレージにはLambda関数のバージョンが含まれており、Serverlessframeworkで特に何も指定していない場合、現在の一番新しいバージョン($LATEST)以外に、歴代の古いバージョンが蓄積されていきます。これがコードストレージを圧迫していき、ついに75GBに達してしまったようです。
CloudFormationの問題
CloudFormationのステータスですが、公式によると、UPDATE_ROLLBACK_FAILEDは「スタックの更新の失敗後、1 つ以上のスタックを前の動作ステートに戻すことに失敗」した状態です。
Lambdaのコードストレージが上限に達し、デプロイに失敗したことでスタックの更新にも失敗してしまった。しかし、Lambdaがどうにもならない状態になっているので、ロールバックもできない、ということでしょうか・・・(間違っていたら教えてください)
とりあえず、
CloudFormationのスタックを正常にロールバックし、ステータスをUPDATE_ROLLBACK_COMPLETEにして、かつLambdaのコードストレージから不要なバージョンを削除すれば良さそうです。
対処法
結論を先に言ってしまうと、この問題に対する対処法は
・CloudFormationのスタックを正常にロールバックし、ステータスをUPDATE_ROLLBACK_FAILED→UPDATE_ROLLBACK_COMPLETEにする。
・Lambdaの不要なバージョンを消す。
・再発を防ぐため、Lambdaのバージョン管理するプラグインを入れる。
です。
やったこと
1.CloudFormationのスタックをロールバックする。
ロールバックが既に失敗している時点で、一筋縄ではいかないことは容易に想像がつきます・・・
公式のトラブルシューティングを確認すると、更新のロールバックを強制的に続行することができるようです(失敗することもあります。というか、失敗することの方が多いようですが・・・)。
それでダメなら失敗の原因になっているリソースをスキップしてロールバックするように書いてありました。
とりあえずやってみます。
CloudFormationのコンソール画面に行き、左側のスタック一覧から、問題を起こしているスタックを選択します(スタックの名前はエラーに書いてあります)。
この時、状況の理由欄に、なぜロールバックが失敗してしまったのかが書いてあります。ロールバックがうまくいかない原因になっているリソース名が書いてあるので、確認しておきます。
しばらく待って成功すれば、ステータスがUPDATE_ROLLBACK_COMPLETEに変わります。
UPDATE_ROLLBACK_COMPLETEは「スタックの更新の失敗後、1 つ以上のスタックを前の動作状態に正常に戻した状態」です。
これでCloudFormationの問題の方は解決・・・?
2.Lambdaの不要なバージョンを消す。
CloudFormationのステータスがUPDATE_ROLLBACK_COMPLETEになったからといって、この段階でデプロイできるようになるわけではありません。
根本的な原因=Lambdaのコードストレージに空きがない問題を解決していないからです。
試しにLambdaのコンソール画面から様子を伺ってみると、
コードストレージがバッチリ100%になっちゃってます。
ちなみにバージョンの方も確認してみると、
うわっ・・・(引)
こんな状態の関数がいっぱいあるわけですから、75GBあっても足りなくなってしまうわけです・・・
いくら一つ一つの関数のサイズが小さくても、大量のバージョンをぶら下げていたら意味ないですね・・・
単純にこれらのバージョンを消していけばいいだけなのですが、数が膨大すぎるので手作業ではやりたくありません。
どうしようかと思って調べていたところ、こちらにいい解決策がありました。
この記事で紹介している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は東京リージョンを使っていたので、東京だけみてくれたらいいのになーと思い、ソースコードをみてみることにしました。
def list_available_lambda_regions():
"""
Enumerates list of all Lambda regions
:return: list of regions
"""
session = Session()
return session.get_available_regions('lambda')
ここでLambdaが使用可能なリージョン一覧をリストで取ってきて、
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の中にあるリージョン分削除を繰り返す、というような動きのようです。
東京だけやれたらいいので、
#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
終わったようです。
コンソール画面を確認してみると、
すごい減ってる・・・!
ここまでやって、ようやくdeployできるようになりました。長かった・・・
3.Lambdaのバージョン管理を行うプラグインを導入する。
何もしなければ、いつか同じ問題が再発してしまうので、このタイミングで対策しておきます。
Lambda関数のバージョンは、Serverlessframeworkでデプロイした時に溜まっていきます。
必要位以上に古いバージョンを溜め込まないように、Serverless Prune Pluginを使います。
Serverless Prune Pluginは、デプロイする度に溜まっていく古いバージョンを適宜削除してくれるプラグインです。
早速使ってみます。
Serverlessと同じ階層で
$ sls plugin install -n serverless-prune-plugin
これだけです。
Serverless Prune Pluginがインストールされ、serverless.ymlに
plugins:
- serverless-prune-plugin
custom:
prune:
automatic: true
number: 3
この記述が追加されます。
automaticをtrueにしておくと、Serverlessでデプロイした時にこのプラグインが自動的に実行されます。
numberは残しておくバージョンの数です。デフォルトは3になっているようです。
この状態でデプロイしていると、
こんな感じで、最新のバージョン($LATEST)と3つ前のバージョンまで残るようになります。
関数の数にもよりますが、この程度ならコードストレージを圧迫してしまうこともないでしょう。きっと。
終わりに
今回のことは、事前にきちんと対策をしていれば防げたことだと思います。
サーバーレスってとても便利で、サクサクできるので開発していてすごく楽しいのですが、やることはきちんとやっておかないといけないなと思いました。反省・・・
もっと勉強しよう・・・
参考
・How to Free AWS Lambda Code Storage when Exceeded
・Serverless Frameworkを本番運用する際にやっておいたほうが良い事