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

# はじめに

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

# 事象

このたび、/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`の内容が再利用されるという事象が発生する。

# 事象再現検証

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

```py
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（説明）」などの設定を変更する処理入れる、という別案も考えられる。ファイル名を固定値にしかできない場合は有用だが、大変そうである。

以上