AWS Lambdaのループ実行による課金上昇を減らすためのアプローチを考えてみた

記事タイトルとURLをコピーする

こんにちは。 CI2 部 1 課の水本です。

極々希ではあるのですが、弊社では時々、AWS Lambda のループ実行による課金上昇が発生することがあります。

当社は「間違いについて叱責しない、ちゃんと防止策を真面目に考えていく」最高な会社だと思いつつも、「人間はいつかミスするのだから、人依存ではいけないな」と思い、自社でできそうなアプローチをいくつか考えてみることにしました。

思考のついでにブログを書いているという感じですので、気軽な気持ちで読んでいただけますと幸いです。

直近で発生した事象

直近で発生した事象は以下のようなものでした。

  • 検証のための AWS アカウントで、S3 イベントをトリガーに Lambda が起動する実装を行った
  • 短時間の動作確認はしたが、ループが発生していることに気がつかなかった
  • 社内の利用料監視システムが異常を検知し、ループ実行が発覚した

考え得る統制について挙げてみる

では早速「どのような統制があり、それらについて当社はどうしているのか」を整理してみます。

発見的統制

発見的統制とは、文字通り事象を見つける仕組みによって、対処開始までの時間を最小限にすることで「被害の軽減」を期待するものです。

当ブログ以外でも沢山挙がる仕組みとしては「利用料の監視」です。

AWS では、 AWS Cost Anomary Detection という仕組みあり、大きな変動があったときに知らせる為の仕組みとして利用が可能です。 aws.amazon.com

当社では AWS Cost Anomary Detection は勿論のこと、独自の Slack アプリを開発し、月の目安金額を超えないか・大きく利用料が変動ていないかを監視し、異常があった場合は Slack 上にメンションが飛ぶようになっています。

発見的統制については、「実施できることは実施済」かもしれませんね。

予防的統制

次に、予防的統制です。リスクをあらかじめ予見して防止策を導入することで、「被害ゼロまたは軽減」を期待するものです。

この実装は Lambda のループ実行については難しいところで、基本的には「ループしないようにコードやトリガーについて確認する」が中心でしょうか?

たとえば Amazon S3 の PUT/GET リクエストをトリガーとする場合は、イベントソースとなるバケットは分けておきましょう、などがあります。

しかしながら、この実装はあくまで設計者とレビュアーの意識にその品質が委ねられるため、これは万全とは言えなさそうです。

自動化できる予防的統制の検討

とはいえ、自動化できる予防的統制は難しそうです。

ネット上の対策は基本的に利用料監視が中心であり、既に当社としては実施済みのものでしたので、自動でできることを検討した結果、「AWS Lambda の同時実行数をタイマーで管理すれば良いのでは?」という発想が浮かびました。

今回は検証の AWS アカウントですから、時間外に起動することは基本的に求めておらず、むしろ起動してほしくなかったので、「なら時間外は起動禁止でもいいのでは?」という判断です。

今回は 2 パターンを考えてみました。

パターン 1:コードを書く

今回は Python(boto3)を使った AWS Lambda でコードを書いてみます。

仕組みとしては、自分自身は止まってしまうとまずいので、自分の名前を取得したうえで、それ以外を同時実行数 0 に変えてしまう、というものです。

環境変数CONCURRENCY_COUNTを定義しておくことで 0 以外にもすることが可能です。

これを Amazon EventBridge に cron ライクでセットしておけば、夜中に実行数が増大して翌朝に青ざめる、ということは無くなるのではないでしょうか。

ただしこれは注意点もあり、自分以外は軒並み止めてしまいますので、必要なものはちゃんと除外リストをメンテしたほうが良い気がします。

会社全体で使うなら、除外用の prefix を取り決めして、その関数は除外したほうがいいかもしれないですね。

import os
import boto3

# boto3
session = boto3.Session()
aws_lambda = session.client('lambda')

# variables
ignore_list = []
concurrency_count = int(os.getenv('CONCURRENCY_COUNT',0))

# main function
def lambda_handler(event, context):
    try:
        # get function name from context
        function_name = context.function_name

        target_list = create_list(function_name)
        change_concurrency(target_list,concurrency_count)
        print(event)
    except Exception as e:
        print(e)

# functions
## create list
def create_list(function_name):
    # add function name to ignore_list
    ignore_list.append(function_name)

    # get lambda function name list with multiple lines
    lambda_list = aws_lambda.list_functions()['Functions']

    # create function name list
    function_name_list = []
    for function in lambda_list:
        function_name_list.append(function['FunctionName'])

    # remove function names in ignore_list
    for ignore_function in ignore_list:
        function_name_list.remove(ignore_function)

    return function_name_list

## change concurrency
def change_concurrency(list,count):
    # set concurrency limit
    for lambda_function in list:
        print(f"{lambda_function}: change concurrency value to {count}")
        try:
            aws_lambda.put_function_concurrency(
                FunctionName=lambda_function,
                ReservedConcurrentExecutions=count
            )
        except Exception as e:
            print(e)

パターン 2:ローコードでの実装

ここでは「ローコードの観点だとどうするか?」を考えてみます。

少し話が変わりますが、弊社には Cloud Automator という AWS 運用支援の Web サービスがあります。

このサービスではカレンダーをトリガーに EC2 に AWS Systems Manager 経由でコマンドを発行できる機能があります。

これを利用して、たとえばコマンド実行用の軽量な EC2 を配置、この EC2 に対して定期でコマンドを実行させるのが良いのではないでしょうか。

CloudAutomatorの実装例

検証はしていませんが、たとえば以下の通りのコマンドで良いと思います。

#! /bin/bash

concurrency=0

# get lambda function name list with multiple lines
function_list=$(aws lambda list-functions --query 'Functions[*].FunctionName' --output text|tr $'\t' '\n')

# set concurrency limit with xargs
echo "$function_list" | xargs -I {} aws lambda put-function-concurrency --function-name {} --reserved-concurrent-executions $concurrency

このパターンであれば、コードらしいコードはこのシェルスクリプトだけなので、実装が容易かもしれませんね。

EC2もジョブ起動の30分前に別のジョブで起動しておけば、より料金も抑えられますね。

最後に

今回は AWS Lambda の実行数を制限して、予期せぬ過剰実行を抑える方法を紹介しました。

実施には以下のような権限が必要ですのでご注意ください。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PutFunctionConcurrency",
            "Effect": "Allow",
            "Action": "lambda:PutFunctionConcurrency",
            "Resource": "arn:aws:lambda:*:123412341234:function:*"
        },
        {
            "Sid": "ListFunctions",
            "Effect": "Allow",
            "Action": "lambda:ListFunctions",
            "Resource": "*"
        }
    ]
}

皆さんの参考になれば幸いです。

2024/10/15 追記

なんと、最新のSDKでループ検出に対応しました。

同じトリガーイベントによって関数が約16回呼び出されると、中断してくれるそうです。

水本 正敏(執筆記事の一覧)

エンタープライズクラウド部 ソリューションアーキテクト1課

国内ITベンダーのカスタマーエンジニアからAWSに魅了されサーバーワークスへ。