AWS Lambdaの耐障害性を考える

# はじめに
この記事はDevOps on AWS大全の一部です。
DevOps on AWS大全の一覧は[こちら](https://qiita.com/tech4anyone/items/b06f88035d27c6ef13b2)。

この記事ではAWS Lambdaを耐障害性の観点から超詳細解説しています。

具体的には以下流れで説明します。

- AWS Lambdaをリリースするときのダウンタイム
- AWS Lambdaのスケーラビリティ
- AWS Lambdaの永続ストレージ

AWSの区分でいう「Level 200：トピックの入門知識を持っていることを前提に、ベストプラクティス、サービス機能を解説するレベル」の内容です。

# この記事を読んでほしい人
- AWS Lambdaを採用するときのベストプラクティスを説明できるようになりたい人
- AWS Lambdaの耐障害性に不安を感じている人
- AWS Certified DevOps Engineer Professionalを目指している人

# AWS Lambdaをリリースするときのダウンタイム
AWS Lambdaをリリースする時のベストプラクティスは2つあります。
1つはエイリアス切り替えによるバージョンアップ、もう1つは環境変数を用いたソースコード修正なしのリリースです。
どちらも目的はダウンタイムの最小化にあります。

ちなみに、アンチパターンはベストプラクティスの裏返しでエイリアス切り替えなしでのソースコードの更新あるいはハードコーディングされたソースコードの更新です。

以下でそれぞれについて説明します。

## AWS Lambdaのバージョンとエイリアス
AWS Lambdaにはバージョンという概念があります。
これは文字通りLambda関数のバージョンで1つのLambda関数に対して複数のバージョンを発行することが可能です。

![](https://storage.googleapis.com/zenn-user-upload/a9467d7416ec-20231121.png)

そして、リリース時のダウンタイムを最小化するためにバージョンとセットで使うのがエイリアスです。
これはLambda関数のバージョンに対するポインタの役割を果たします。
エイリアスごとに異なるLambda関数のバージョンを指定できるため、開発環境と本番環境で異なるバージョンを選択することが可能です。

![](https://storage.googleapis.com/zenn-user-upload/0571c2f3a768-20231121.png)

上図では開発向けのdevエイリアスと本番向けのprodエイリアスを用意しています。
そして、開発向けのdevエイリアスは$LATESTを利用して、常に最新バージョンのLambdaが起動するように設定しています。

一方、本番向けはV1とV2でトラフィック分割をしています。
これは一気に新しいバージョンに切り替えることによる思わぬトラブルを避けるためです。

エイリアスを用いてトラフィック分割しながらバージョンを切り替えてのLambdaリリースは実プロジェクトでも非常に有用かつ試験でも聞かれるベストプラクティスナノでぜひ覚えておいてください。

## AWS Lambdaの環境変数
AWS Lambdaの環境変数はKey/Valueの形式で定義できる変数です。
環境ごとやリリースごとに変わる可能性のある変数を環境変数として定義しておくことで、ソースコードを変更することなくAWS Lambdaの挙動を変えることができます。

これも、エイリアス同様ダウンタイムの短いリリース方式としてベストプラクティスになります。
また、環境変数については管理しやすいソースコードという観点からもベストプラクティスです。

環境変数を使う場合のLambda関数を具体的に見てみましょう。

```python
def lambda_handler(event, context):
    # Lambda関数のメインハンドラー
    print("Hello World!")
    return {
        'statusCode': 200,
        'body': 'Hello World!'
    }
```

上記はHello World!を表示するためのLambda関数です。

例えば、Hello World!を定期的に変える可能性がある、という想定でここを環境変数からの読み出しに変えてみましょう。
前提として、Lambda環境変数にMESSAGEというKeyで環境変数を登録済み、という想定です。

```diff_python
+import os

def lambda_handler(event, context):
    # Lambda関数のメインハンドラー

+   # 環境変数からメッセージを取得
+   message = os.environ.get('MESSAGE')

-   print("Hello World!")
+   print(message)

    return {
        'statusCode': 200,
        'body': message
    }
```

このように書き換えると、例えばHello World!をHappy Holiday!に書き換えたいときソースコードの変更が不要になるので、Lambdaの更新時にダウンタイムが短くなります。

# AWS Lambdaのスケーラビリティ
AWS LambdaはサーバレスサービスだからかEC2のようなサービスと比べてスケーラビリティを意識する人が少ない印象です。

具体的にはAWS Lambdaのリージョン単位での同時実行数上限を意識している人が少なく、とりあえず負荷に応じてガンガンLambdaを起動する設定にすればよいと思っている人によく出会います。

しかし、デフォルトでLambdaは同時に1000までしか実行することができず、トラフィックがバーストすると容易に実行制限に突き当たります。

![](https://storage.googleapis.com/zenn-user-upload/235cae3b8ad6-20231121.png)

想定外にLambdaが起動できないという事態を避ける手段は2つ、1)予約済み同時実行、2)プロビジョニング済み同時実行、です。

なお、LambdaのスケーラビリティにかかわらずAWSではAPI CALLのスロットリング対策としてエクスポネンシャルバックオフの実装が推奨されているので併せて実装することをお勧めします。

## 予約済み同時実行
予約済み同時実行は1つのLambda関数が起動できるインスタンスを事前に決めておく方法です。
これは特定の関数が起動できるインスタンスを食い尽くさないようにかけるリミットになります。
そのため、特定の関数でさばくトラフィックがバーストしても、ほかの関数への影響は最小化されます。

![](https://storage.googleapis.com/zenn-user-upload/0972187305e5-20231121.png)
上図は[AWS公式](https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html#reserved-and-provisioned:~:text=%E5%BD%B9%E7%AB%8B%E3%81%A1%E3%81%BE%E3%81%99%E3%80%82-,%E4%BA%88%E7%B4%84%E3%81%95%E3%82%8C%E3%81%9F%E5%90%8C%E6%99%82%E5%AE%9F%E8%A1%8C%E6%80%A7,-%E9%96%A2%E6%95%B0%E3%81%A7%E3%81%84%E3%81%A4)のものですが、オレンジ関数が予約済み以上は起動できず制約されているのがわかると思います。

## プロビジョニング済み同時実行
プロビジョニング済み同時実行は予約済み同時実行と異なり、同時実行台数は制限されません。

プロビジョニング済み同時実行はLambdaインスタンスを事前に指定台数起動することでコールドスタート問題を解消し、より短い時間でLambda処理を完了させるために利用します。

![](https://storage.googleapis.com/zenn-user-upload/dcb999dc9082-20231121.png)
上図は[AWS公式](https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html#reserved-and-provisioned:~:text=%E3%81%A6%E3%81%8F%E3%81%A0%E3%81%95%E3%81%84%E3%80%82-,%E3%83%97%E3%83%AD%E3%83%93%E3%82%B8%E3%83%A7%E3%83%8B%E3%83%B3%E3%82%B0%E3%81%95%E3%82%8C%E3%81%9F%E5%90%8C%E6%99%82%E5%AE%9F%E8%A1%8C%E6%80%A7,-%E4%BA%88%E7%B4%84%E3%81%95%E3%82%8C)のものですが、予約済み同時実行と異なりオレンジ関数が予約済み以上に起動されているのがわかると思います。

そのため、実際にはプロビジョニング済み同時実行のみを同時実行数制限の対応で使うことはありません。

また、予約済み同時実行と異なりプロビジョニングを事前にしておく分の追加コストが発生するのでその点の留意を忘れないようにしましょう。

## 予約済み同時実行とプロビジョニング済み同時実行の併用
同時実行数の制限に可能な限り突き当たりたくない場合には予約済みとプロビジョニング済み同時実行を併用することがベストプラクティスです。

![](https://storage.googleapis.com/zenn-user-upload/ec702d5761e1-20231121.png)
上図は[AWS公式](https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html#reserved-and-provisioned:~:text=%E3%81%BF%E3%81%BE%E3%81%97%E3%82%87%E3%81%86%E3%80%82-,%E3%81%93%E3%81%AE%E5%9B%B3,-%E3%81%A7%E3%81%AF%E3%80%81%20%E3%81%AB%E5%AF%BE%E3%81%97%E3%81%A6%E3%83%97%E3%83%AD%E3%83%93%E3%82%B8%E3%83%A7%E3%83%8B%E3%83%B3%E3%82%B0)のものですが、プロビジョニング済みを使い切っても予約済み以上にはオレンジ関数が起動していない様子がわかると思います。

このようにプロビジョニング済みでコールドスタート問題を解決し可能な限り早く処理を進めつつ、他サービスへの影響を最小化するために予約済みを利用するというのがコスト度外視パターンのベストプラクティスです。

## AWS Lambdaの永続ストレージ
AWS Lambdaを利用する際にストレージを意識することはあまりないかもしれません。

しかし、大規模なライブラリのロードや大量データのロードで大きなサイズのファイルを読み込みたいので永続ストレージが欲しいという場面は意外と存在します。

また、処理結果を保持してほかの処理に渡す際にも永続ストレージが必要になります。

このように永続ストレージが欲しくなった場合の選択肢はS3かEFSです。
アクセス速度を求めるのであればEFSを選択するのがベストプラクティスです。

昔はS3が結果整合性だったのでEFS一択というのが常識でしたが、現在はS3も強い一貫性をサポートしているのであまり大きなファイルでなければS3でも問題ないでしょう。

ただし、ファイルのロックであったりアクセス速度であったりEFSのメリットもまだまだ多いのでプロジェクトの要件にあわせて選択しましょう。

# まとめ
この記事ではAWS Lambdaを耐障害性の観点から超詳細解説しました。

- AWS Lambdaをリリースするときのダウンタイム
- AWS Lambdaのスケーラビリティ
- AWS Lambdaの永続ストレージ

次回はAPI Gatewayの耐障害性を考えます。