Edited at

AWS Lambdaで/tmpディレクトリ上のデータが不本意に再利用されてしまう事象に対処した話


はじめに

AWS Lambdaには関数実行時に実行環境として起動したコンテナをある程度の期間再利用する動作仕様(ウォームスタート)があり、それに伴い/tmpディレクトリ上のデータも次回の処理で再利用される動作となる。


事象

このたび、/tmpディレクトリ上にfunc.pyというモジュールを作成し、モジュール内の関数をimport funcでインポートして利用する以下のようなコード(事象発生コード.py)から成るLambda関数を作成した。

func.pyの内容はf.write(event['body'])でほかのLambda関数から渡されたデータを書き込んでいる。

Lambdaのウォームスタートの動作は認識していたため、/tmp/func.pyにはmode='w'により関数実行毎に上書きを行い、func.py利用後はcall('rm -rf /tmp/*', shell=True)によりファイルごと削除する処理を入れるという対策をした。


事象発生コード.py

import json,sys

from subprocess import call
#(略)

def lambda_handler(event, context):

global driver
#(中略)
driver = #(略)

def return200(res_body):
return{
'statusCode': 200,
'body': res_body
}

with open('/tmp/func.py', mode='w') as f:
f.write(event['body'])

sys.path.insert(0, '/tmp/')
import func
call('rm -rf /tmp/*', shell=True)

return(return200(func.scrape_process(driver)))


しかし、デバッグのために何回か関数を実行していると、対策したつもりにも関わらず、どうも前回のfunc.pyの内容が再利用されるという事象が発生する。


事象再現検証

そこで検証として以下のような2つのファイルから成るLambda関数を別途作成した。先程の事象が発生したLambda関数に対して、実行毎に1から10のインクリメントされる数字を30秒ごとにリクエストして、同じ値がレスポンスされるようにした。

import boto3,json,time

def lambda_handler(event, context):

for i in range(1,10):

input_event = {
"body": open('func.py').read().replace('__value__',str(i))
}
response = boto3.client('lambda').invoke(
FunctionName = '事象発生Lambda関数',
InvocationType = 'RequestResponse',
Payload = json.dumps(input_event)
)
print('リクエスト値:' + str(i) + ' レスポンス値:' + json.loads(response['Payload'].read().decode())['body'])
time.sleep(30)


func.py

def scrape_process(driver):

return('__value__')

結果は下記の通り、一回目の実行時にリクエストされた値が二回目移行もレスポンスされ、実行されたLambda側でfunc.pyの内容が更新されず再利用される動作となった。

Function Logs:

START RequestId: ebfb51dc-52c4-4862-8f87-79e60fdf9b44 Version: $LATEST
リクエスト値:1 レスポンス値:1
リクエスト値:2 レスポンス値:1
リクエスト値:3 レスポンス値:1
リクエスト値:4 レスポンス値:1
リクエスト値:5 レスポンス値:1
リクエスト値:6 レスポンス値:1
リクエスト値:7 レスポンス値:1
リクエスト値:8 レスポンス値:1
リクエスト値:9 レスポンス値:1
END RequestId: ebfb51dc-52c4-4862-8f87-79e60fdf9b44
REPORT RequestId: ebfb51dc-52c4-4862-8f87-79e60fdf9b44 Duration: 303557.55 ms Billed Duration: 303600 ms Memory Size: 128 MB Max Memory Used: 76 MB

/tmpディレクトリ上のデータは同名のファイルだと実行毎に読み込みに行かず、オンメモリに保持した内容を毎回利用しているのだろうか。


解決

モジュール名をfunc.pyという固定値ではなく実行毎に異なるUUIDとした。


事象発生コード(修正版).py

import json,sys

from subprocess import call
import uuid,importlib
#(略)

def lambda_handler(event, context):

global driver
#(中略)
driver = #(略)

def return200(res_body):
return{
'statusCode': 200,
'body': res_body
}

moduleName = str(uuid.uuid4())

with open('/tmp/%s.py' % moduleName, mode='w') as f:
f.write(event['body'])

sys.path.insert(0, '/tmp/')
module = importlib.import_module(moduleName)

return(return200(module.scrape_process(driver)))


先ほど別途作成したLambda関数を再度実行してみたら、当然ファイル名の異なる別ファイルなので再利用はされず(/tmpディレクトリ内に前回のファイルは残っているはず)、期待通りの実行結果を得られた。

Function Logs:

START RequestId: 9ba0dd32-2824-4d45-a818-67419751dd9b Version: $LATEST
リクエスト値:1 レスポンス値:1
リクエスト値:2 レスポンス値:2
リクエスト値:3 レスポンス値:3
リクエスト値:4 レスポンス値:4
リクエスト値:5 レスポンス値:5
リクエスト値:6 レスポンス値:6
リクエスト値:7 レスポンス値:7
リクエスト値:8 レスポンス値:8
リクエスト値:9 レスポンス値:9
END RequestId: 9ba0dd32-2824-4d45-a818-67419751dd9b
REPORT RequestId: 9ba0dd32-2824-4d45-a818-67419751dd9b Duration: 303805.91 ms Billed Duration: 303900 ms Memory Size: 128 MB Max Memory Used: 76 MB


別案

ウォームスタートのために保持されたコンテナはそのLambda関数の何らかの設定を変更することにより破棄されるため、/tmpディレクトリを実際に利用する本体のLambda関数の手前にもう一つプロキシとなるLambda関数を配置し、データをプロキシする前に本体のLambdaの「description(説明)」などの設定を変更する処理入れる、という別案も考えられる。ファイル名を固定値にしかできない場合は有用だが、大変そうである。

以上