【ナレッジ】AWS Parameters and Secrets Lambda Extension は、ハンドラー関数内から呼び出さないと400Errorになる

<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/341404/ba00643e-b43a-b452-6b28-505ad940d5e4.png">


# 0. 要約
タイトルの通りですが、  
**[AWS Parameters and Secrets Lambda Extension](https://docs.aws.amazon.com/ja_jp/secretsmanager/latest/userguide/retrieving-secrets_lambda.html) は、ハンドラー関数"外"から呼び出しをすると HTTP 400 Error が出てしまいます。**  
そのため、ハンドラー関数"**内**"から呼び出しましょう！

# 1. はじめに...
[2022年10月に、AWS Parameters and Secrets Lambda Extension が発表されました。](https://aws.amazon.com/jp/about-aws/whats-new/2022/10/aws-parameters-secrets-lambda-extension/)  
これまで、Lambda 関数内から[パラメーターストア](https://aws.amazon.com/jp/systems-manager/features/#Parameter_Store)や[シークレットマネージャー](https://aws.amazon.com/jp/secrets-manager/)に保存した値（APIキーなど）を取得するには、Lambda 関数内からSDK を使って取得するのが一般的でした。　　
しかし、**この Lambda Extension を活用することで、** パラメータ値を Lambda にキャッシュできるようになり、**レイテンシーとコスト効率の点でメリットがあります。**

https://docs.aws.amazon.com/ja_jp/secretsmanager/latest/userguide/retrieving-secrets_lambda.html

# 2. 使ってみる...
詳しい使い方は、[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/secretsmanager/latest/userguide/retrieving-secrets_lambda.html)や、他のブログ記事に譲ります。（例えば[こちら](https://dev.classmethod.jp/articles/aws-parameters-and-secrets-lambda-extension/)）
要点をまとめると、以下の作業をしています。
1. SecretsManager にAPIキーなどを保存
2. Lambda 関数に、今回の Lambda Layer を追加
3. Lambda の IAMロールに権限を追加

今回は、私がハマった「コードを書く場所」に焦点を当てて記事を書きます。  
※ この記事を読む方は、既に使っている方が過半数だと思いますし...

## 2-1. 【これはNG】Handler 関数"外"から呼び出す
以下は、ハンドラー関数"**外**"から、Lambda Extension 経由で、 Secrets Manager に保存した API キーを取得するコードです。　　
実行結果も貼り付けていますが、`HTTP Error 400: Bad Request` のエラーが起き、API キーを取得できていません。

**Lambda 関数のコード**
```python:lambda_function.py
import json
import os
import urllib.request

# Handler 関数"外"から、AWS Parameters and Secrets Lambda Extension 経由でシークレットの値を取得する
endpoint = 'http://localhost:2773/secretsmanager/get?secretId=SecretsManagerName'
request = urllib.request.Request(endpoint)
request.add_header('X-Aws-Parameters-Secrets-Token', os.environ['AWS_SESSION_TOKEN'])
with urllib.request.urlopen(request) as response:
    body = response.read()
secret_strings = json.loads(body.decode("utf-8"))["SecretString"]
APIKEY = json.loads(secret_strings)["APIKEY"]
print(f'GET APIKEY = {APIKEY}')

# Handler 関数
def lambda_handler(event, context):
    return {
        'statusCode': 200,
        'headers': {
            'Content-Type': 'text/plain'
        },
        'body': f'APIKEY is :{APIKEY}'
    }
```
**実行結果**
```
=== Function Logs [一部のみ] ===
[ERROR] HTTPError: HTTP Error 400: Bad Request

=== Response ===
{
  "errorMessage": "HTTP Error 400: Bad Request",
  "errorType": "HTTPError",
  "requestId": "",
  "stackTrace": [
    "  File \"/var/lang/lib/python3.10/importlib/__init__.py\", line 126, in import_module\n    return _bootstrap._gcd_import(name[level:], package, level)\n",
    "  File \"<frozen importlib._bootstrap>\", line 1050, in _gcd_import\n",
    "  File \"<frozen importlib._bootstrap>\", line 1027, in _find_and_load\n",
    "  File \"<frozen importlib._bootstrap>\", line 1006, in _find_and_load_unlocked\n",
    "  File \"<frozen importlib._bootstrap>\", line 688, in _load_unlocked\n",
    "  File \"<frozen importlib._bootstrap_external>\", line 883, in exec_module\n",
    "  File \"<frozen importlib._bootstrap>\", line 241, in _call_with_frames_removed\n",
    "  File \"/var/task/lambda_function.py\", line 9, in <module>\n    with urllib.request.urlopen(request) as response:\n",
    "  File \"/var/lang/lib/python3.10/urllib/request.py\", line 216, in urlopen\n    return opener.open(url, data, timeout)\n",
    "  File \"/var/lang/lib/python3.10/urllib/request.py\", line 525, in open\n    response = meth(req, response)\n",
    "  File \"/var/lang/lib/python3.10/urllib/request.py\", line 634, in http_response\n    response = self.parent.error(\n",
    "  File \"/var/lang/lib/python3.10/urllib/request.py\", line 563, in error\n    return self._call_chain(*args)\n",
    "  File \"/var/lang/lib/python3.10/urllib/request.py\", line 496, in _call_chain\n    result = func(*args)\n",
    "  File \"/var/lang/lib/python3.10/urllib/request.py\", line 643, in http_error_default\n    raise HTTPError(req.full_url, code, msg, hdrs, fp)\n"
  ]
}
```
↓ 実際のスクリーンショット
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/341404/78ee9e28-744d-48e7-534d-5b93ff82c84d.png)



## 2-2. 【こっちだとOK】Handler 関数"内"から呼び出す
Lambda のハンドラー関数"**内**"からであれば、Secrets Manager に保存したAPIキーを読み込めます。

**Lambda 関数のコード**
```python:lambda_function.py
import json
import os
import urllib.request

# Handler 関数
def lambda_handler(event, context):
    
    # Handler 関数"内"から、AWS Parameters and Secrets Lambda Extension 経由でシークレットの値を取得する
    endpoint = 'http://localhost:2773/secretsmanager/get?secretId=SecretsManagerName'
    request = urllib.request.Request(endpoint)
    request.add_header('X-Aws-Parameters-Secrets-Token', os.environ['AWS_SESSION_TOKEN'])
    with urllib.request.urlopen(request) as response:
	    body = response.read()
    secret_strings = json.loads(body.decode("utf-8"))["SecretString"]
    APIKEY = json.loads(secret_strings)["APIKEY"]
    print(APIKEY)
    
    return {
        'statusCode': 200,
        'headers': {
            'Content-Type': 'text/plain'
        },
        'body': f'APIKEY:{APIKEY}'
    }
```
**実行結果**
```
=== Function Logs [一部のみ] ===
GET APIKEY = hogehoge

=== Response ===
{
  "statusCode": 200,
  "headers": {
    "Content-Type": "text/plain"
  },
  "body": "APIKEY is :hogehoge"
}
```
↓ 実際のスクリーンショット
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/341404/bc8039fa-55e6-df71-e4fd-89c36a692e39.png)

# 3. あれ？ Handler 関数の "内" と "外" って何が違うの？
ここまで書くと、**「そもそも、なぜ Handler 関数"外"に処理を書いたのか？ 俺は常に Handler 関数"内"に処理を書いてるぞ！」という意見も出てくるかと思います。**

実は、Lambda の Handler 関数 "外" と "内" では、実行時の挙動が異なります。
|  | Handler 関数 "外" に書いたコード | Handler 関数 "内" に書いたコード |
| :--- | :--- | :--- |
| 動作 | コールドスタート時の１回だけ実行される。 | ウォームスタート時に毎回実行される。 |
| 用途 | 全ユーザー共通の処理や、時間のかかるライブラリの読み込み、初期化などを記載する | ユーザーごとに異なるデータがあるようなコードを記載する（DBアクセスなど） |


- 参考：[Lambdaのhandler外(メソッド以外)のコードは「コールドスタート時の1回だけ実行される」という話 | DevelopersIO](https://dev.classmethod.jp/articles/lambda-outside-handler-running-first/)  
- 詳細：[Lambda 実行環境 - AWS Lambda](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-runtime-environment.html)

### 3.1 自分の開発を振り返ってみると...
私は開発前半、APIキーを環境変数に設定し、Handler 関数 "外" から取得していました。  
これは、APIキーは長期間再利用できるため、Handler関数"**外**"に記載/定義するのが良いと考えたためです。（実際、Lambda関連のソースコードを見ると同じような実装をよく見かけます。）  
開発後半になってから、Lambda Extension を利用する構成に作り直した際、同じく Handler 関数 "**外**" に Secrets Manager へのアクセスコードを書いてしまったため、うまくいかないエラーに巻き込まれました。

### 3.2 未解決の疑問点
Handler 関数の "内" と "外" で違いがあることはご理解いただけたと思います。  
しかし、なぜ、「AWS Parameters and Secrets Lambda Extension を Handler 関数外からだと呼び出せないのか？」については、すみません。私には分かりませんでした。  
もし、ご存知の方がいらっしゃっいましたら、教えていただけますと幸いです。


# X. 終わりに
個人開発をしていた時に、このトラブルに遭遇しました。
私は 「[400 Error](https://developer.mozilla.org/ja/docs/Web/HTTP/Status/400)」 が権限不足に起因するもの（具体的には [401 Error](https://developer.mozilla.org/ja/docs/Web/HTTP/Status/401)）だと考え、間違った調査や修正対応をしてしまい、休日を丸１日溶かしました:scream:
こちらのナレッジが、悩める皆様の休日を救えることを祈っています。

### おまけ
このナレッジは、以下のアプリ（ゲーム）を開発していた際の副産物です。  
技術的な工夫点も多いアプリですので、ぜひこちらもご覧ください。

https://qiita.com/hayate_h/items/2e8ee634b456d751f744