VPCネットワーク内での350秒のタイムアウトを確認してみた

# はじめに
この記事は[Optimind Advent Calendar 2023](https://qiita.com/advent-calendar/2023/optimind)の22日目の記事となります。

# 概要
最近NATゲートウェイなどを経由した（AWS Lambdaなどの）同期通信が350秒程度で通信が切断されてしまうということを知りました（詳しくは以下の記事を参考にしてください）。なので、AWS LambdaやEC2を建ててみて実際にタイムアウトするかを簡単に確認してみたいと思います。

https://aws.amazon.com/jp/blogs/news/jp-implementing-long-running-tcp-connections-within-vpc-networking/

https://repost.aws/ja/knowledge-center/lambda-vpc-timeout

## 実験
まずAWS LambdaからAWS Lambdaとの同期通信を試してみます。次に、Private Subnetの中に建てたEC2からAWS Lambdaとの同期通信をおこなってみます。

実験のための簡単なプログラムをGPT-4-sanに伺いながら作成しました。lambda1はlambda2との同期通信用のプログラムで、lambda2はsleepするだけの処理をしています。

```python:lambda1
import json
import boto3
from botocore.config import Config

def lambda_handler(event, context):
    
    config = Config(region_name="ap-northeast-1", read_timeout=900, connect_timeout=900)
    client = boto3.client('lambda', config=config)
    function_name = 'lambda2'
    
    # Lambda関数の実行
    try:
        response = client.invoke(
            FunctionName=function_name,
            InvocationType='RequestResponse', 
            Payload=json.dumps({})
        )
        ret = response['Payload'].read()
        print (ret)
    except Exception as e:
        print (e)
        
    
    return {
        'statusCode': 200,
        'body': ret
    }

```

```python:lambda2
import json
import time

def lambda_handler(event, context):
    time.sleep(360)
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }
```

AWS Lambda(lambda1)からAWS Lambda(lambda2）を呼び出したときは以下のような結果を得ることができました。360秒のsleepでも問題なく接続できています。
```
b'{"statusCode": 200, "body": "\\"Hello from Lambda!\\""}'
```

### Private Subnet内のEC2からLambdaを呼び出してみる
EC2からlambda2を呼び出すためのプログラムを作成します。

```python:invoke_lambda.py
import json
import boto3
import time
from botocore.config import Config

conf = Config(region_name="ap-northeast-1", read_timeout=400, connect_timeout=400, retries={"mode": "standard", "total_max_attempts": 1})
client = boto3.client('lambda', config=conf)
function_name = 'lambda2'

# Lambda関数の実行
start = time.time()
try:
    response = client.invoke(
        FunctionName=function_name,
        InvocationType='RequestResponse', 
        Payload=json.dumps({})
    )
    print(response['Payload'].read())
except Exception as e:
    print (e)

print ('elapsed time: ', time.time() - start)
```

sleep時間が10秒のときの実行は問題なく終了しました。意図通りに実行していそうです。
```
[ec2-user@ip-xx~]$ python3 invoke_lambda.py 
b'{"statusCode": 200, "body": "\\"Hello from Lambda!\\""}'
elapsed time:  10.290184497833252
```

早速sleep時間が360秒のときを見てみます。対応するlambda2のログのREPORTを見ると360秒で終了しています。readTimeoutの設定時間は400秒にしているため、タイムアウト設定値内に終わっていそうです。
```
REPORT RequestId: xxx	Duration: 360002.70 ms	Billed Duration: 360003 ms	Memory Size: 128 MB	Max Memory Used: 41 MB	
```

しかし、EC2の方を見るとReadTimeoutという結果になりました。elapsedTimeを見てみると400秒と設定されたタイムアウトの時間まで結果を待ち続けているようで、接続の応答が来ていなさそうに見えます。
```
[ec2-user@ip-xx~]$ python3 invoke_lambda.py 
Read timeout on endpoint URL: "https://lambda.ap-northeast-1.amazonaws.com/xxx"
elapsed time:  400.1450889110565
```

一応どのタイミングでReadTimeoutになるのかを確認してみました。

- 345秒のsleep
```
[ec2-user@ip-xx~]$ python3 invoke_lambda.py 
b'{"statusCode": 200, "body": "\\"Hello from Lambda!\\""}'
elapsed time:  345.31132411956787
```

- 349秒のsleep
```
[ec2-user@ip-xx~]$python3 invoke_lambda.py 
b'{"statusCode": 200, "body": "\\"Hello from Lambda!\\""}'
elapsed time:  349.288724899292
```

- 350秒のsleep
```
[ec2-user@ip-xx~]$ python3 invoke_lambda.py 
Read timeout on endpoint URL: "https://lambda.ap-northeast-1.amazonaws.com/xxx"
elapsed time:  400.09561228752136
```

349秒で成功しますが350秒だとだめみたいでタイムアウトの設定値まで待っており、挙動が一致していそうです。

### 紹介されている方法を試してみる

紹介されているとおりにTCPキープアライブの動作を設定するためのパラメータの変更と、アプリケーション側でのTCPキープアライブを有効化してみます。詳しい説明は以下の文献を参考にしてください。

https://aws.amazon.com/jp/blogs/news/jp-implementing-long-running-tcp-connections-within-vpc-networking/

初期値は以下の通りでした。
```
[ec2-user@ip-xx~]$ cat /proc/sys/net/ipv4/tcp_keepalive_time
7200
[ec2-user@ip-xx~]$ cat /proc/sys/net/ipv4/tcp_keepalive_intvl
75
[ec2-user@ip-xx~]$ cat /proc/sys/net/ipv4/tcp_keepalive_probes
9
```

350秒で通信が切断しないように設定値を以下のように変更します。

```bash
[ec2-user@ip-xx~]$ sudo sysctl -w net.ipv4.tcp_keepalive_time=45
net.ipv4.tcp_keepalive_time = 45
[ec2-user@ip-xx~]$ sudo sysctl -w net.ipv4.tcp_keepalive_intvl=45
net.ipv4.tcp_keepalive_intvl = 45
[ec2-user@ip-xx~]$ sudo sysctl -w net.ipv4.tcp_keepalive_probes=9
net.ipv4.tcp_keepalive_probes = 9
```

次に、~/.aws/configに以下の設定を書き込みます。

```bash
[default]
tcp_keepalive=true
```

無事通信が成功しました！

```
[ec2-user@ip-xx~]$ python3 invoke_lambda.py 
b'{"statusCode": 200, "body": "\\"Hello from Lambda!\\""}'
elapsed time:  360.0784122943878
```

# おわりに

最近の（？）AWSのアップデートでECS on Fargateでカーネルパラメータを調整できるようになったみたいなのでこちらもまた試してみたいと思います。

https://dev.classmethod.jp/articles/ecs-on-fargate-support-systemcontrols/

# 参考

https://aws.amazon.com/jp/blogs/news/jp-implementing-long-running-tcp-connections-within-vpc-networking/

https://repost.aws/ja/knowledge-center/lambda-vpc-timeout

https://qiita.com/orfx/items/a9c86dcb1ce91d1a6f4a

https://dev.classmethod.jp/articles/set-tcp-keepalive-to-boto3-on-lambda/

https://dev.classmethod.jp/articles/ecs-on-fargate-support-systemcontrols/