AWS Parameters and Secrets Lambda Extensionの実装について色々確認してみた

Goで実装されてるみたいです
2022.10.23

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

CX事業本部@大阪の岩田です。 AWS Parameters and Secrets Lambda Extensionの実装が気になったので色々試してみました。

レスポンスヘッダー

AWS Parameters and Secrets Lambda ExtensionはLambda実行環境内でHTTPサーバーを起動します。このHTTPサーバーがパラメータやSecretの取得リクエストを処理してくれるわけですが、レスポンスヘッダに何か特徴は無いのでしょうか?試しに以下のコードを実行してレスポンスヘッダを確認してみましょう。

import json
from urllib import request
import os
from pprint import pprint


def lambda_handler(event, context):
    
    headers = {
        'X-Aws-Parameters-Secrets-Token': os.environ['AWS_SESSION_TOKEN']
        
    }
    req = request.Request('http://localhost:2773/secretsmanager/get?secretId=lambda/extension/test', headers=headers)
    
    with request.urlopen(req) as res:
      pprint(res.getheaders())
    
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

結果は以下のようになりました。

[('Content-Type', 'text/plain'),
('Date', 'Fri, 21 Oct 2022 15:48:11 GMT'),
('Content-Length', '327'),
('Connection', 'close')]

特にこれといった特徴も無い最小限のレスポンスヘッダという感じですね。

ユーザーエージェント

キャッシュミスが発生した場合は、AWS Parameters and Secrets Lambda ExtensionからSecrets ManagerやSSMパラメータストアにアクセスすることになりますが、ユーザーエージェントはどうなっているのでしょうか?先程Invokeした際Cloud Trailに記録されたログを確認してみました。

{
    "eventVersion": "1.08",
    "userIdentity": {
        "type": "AssumedRole",
        ...略
    "userAgent": "aws-sdk-go-v2/1.16.8 os/linux lang/go/1.19.1 md/GOOS/linux md/GOARCH/amd64 api/secretsmanager/1.15.14 secretsmanager.ExtensionUserAgent/SecretsManagerLambdaExtension/1.0.94",
    "requestParameters": {
        "secretId": "lambda/extension/test"
    },
    "responseElements": null,
    "requestID": "fb147514-1ab3-49c2-ad68-3d40af905a88",
    "eventID": "1d575083-44dc-466f-8951-d2e51d6524a9",
    "readOnly": true,
    "eventType": "AwsApiCall",
    "managementEvent": true,
    "recipientAccountId": "AWSアカウントID",
    "eventCategory": "Management",
    "tlsDetails": {
        "tlsVersion": "TLSv1.2",
        "cipherSuite": "ECDHE-RSA-AES128-GCM-SHA256",
        "clientProvidedHostHeader": "secretsmanager.ap-northeast-1.amazonaws.com"
    }
}

ユーザーエージェントは aws-sdk-go-v2/1.16.8 os/linux lang/go/1.19.1 md/GOOS/linux md/GOARCH/amd64 api/secretsmanager/1.15.14 secretsmanager.ExtensionUserAgent/SecretsManagerLambdaExtension/1.0.94だそうです。また、TLS1.2でアクセスしていることや暗号スイートに ECDHE-RSA-AES128-GCM-SHA256を利用していることが分かります。

デバッグログ

環境変数PARAMETERS_SECRETS_EXTENSION_LOG_LEVELを指定するとAWS Parameters and Secrets Lambda Extensionのログレベルが変更できまます。上記環境変数にDEBUGを設定して再度Lambdaを実行してみました。

出力されたログからタイムスタンプ等を削って分かりやすく加工したのが以下のログです。

PARAMETERS_SECRETS_EXTENSION_LOG_LEVEL is DEBUG. Log level set to debug.
DEBUG PARAMETERS_SECRETS_EXTENSION_CACHE_ENABLED is not present. Cache is enabled by default.
DEBUG PARAMETERS_SECRETS_EXTENSION_CACHE_SIZE is not present. Using default cache size: 1000 objects.
DEBUG SECRETS_MANAGER_TTL is not present. Setting default time-to-live: 5m0s.
DEBUG SSM_PARAMETER_STORE_TTL is not present. Setting default time-to-live: 5m0s.
DEBUG SECRETS_MANAGER_TIMEOUT_MILLIS is not present. Setting default timeout: 0s.
DEBUG SSM_PARAMETER_STORE_TIMEOUT_MILLIS is not present. Setting default timeout: 0s.
DEBUG PARAMETERS_SECRETS_EXTENSION_MAX_CONNECTIONS is not present. Setting default value: 3.
DEBUG PARAMETERS_SECRETS_EXTENSION_HTTP_PORT is not present. Setting default port: 2773.
INFO Systems Manager Parameter Store and Secrets Manager Lambda Extension 1.0.94
DEBUG Creating a new cache with size 1000
INFO Serving on port 2773
EXTENSION       Name: AWSParametersAndSecretsLambdaExtension    State: Ready    Events: [INVOKE,SHUTDOWN]
INFO ready to serve traffic
DEBUG Caching secret with key /secretsmanager/get?secretId=lambda/extension/test

設定値に関するログが色々と出力されていますが、あまり内部の挙動が分かるようなログは出力されていなさそうです。

実行ファイルを確認してみる

今度はAWS Parameters and Secrets Lambda Extensionの実行ファイルについて見ていきましょう。以下のコードを実行し、Extensionの本体である実行ファイルを適当なS3バケットにアップロードします。アップロードが完了したローカルにダウンロードして色々確認してみましょう。

import json
import boto3

def lambda_handler(event, context):

    s3 = boto3.resource('s3')
    bucket = s3.Bucket('適当なバケット名')
    bucket.upload_file('/opt/extensions/AWSParametersAndSecretsLambdaExtension', 'AWSParametersAndSecretsLambdaExtension')
      
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

まずファイルサイズでも確認してみましょう。

$ du -h AWSParametersAndSecretsLambdaExtension
6.8M	AWSParametersAndSecretsLambdaExtension

サイズは6.8Mでした。

続いてfileコマンドを実行してみます。

$ file AWSParametersAndSecretsLambdaExtension
AWSParametersAndSecretsLambdaExtension: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, Go BuildID=6GMSLUap4TAIQDIwkZFk/Lht7YB4ZxIArtVKhpq6A/LaUOXo0ANxGRFaIsZg6T/bO1xuDx-0bH4VqWlg212, stripped

ユーザーエージェントを確認した段階で分かっていましたが、Goで開発されているようですね。

stringsコマンドの実行結果から色々確認してみましょう

$ strings AWSParametersAndSecretsLambdaExtension | grep ^github.com | cut -d / -f1,2,3 | sort | uniq
github.com/aws/aws-sdk-go-v2
github.com/aws/smithy-go
github.com/aws/smithy-go.(*CanceledError).CanceledError
github.com/aws/smithy-go.(*CanceledError).Error
github.com/aws/smithy-go.(*CanceledError).Unwrap
github.com/aws/smithy-go.(*DeserializationError).Error
github.com/aws/smithy-go.(*DeserializationError).Unwrap
github.com/aws/smithy-go.(*ErrorFault).String
github.com/aws/smithy-go.(*GenericAPIError).Error
github.com/aws/smithy-go.(*GenericAPIError).ErrorCode
github.com/aws/smithy-go.(*InvalidParamsError).Add
github.com/aws/smithy-go.(*InvalidParamsError).AddNested
github.com/aws/smithy-go.(*InvalidParamsError).Error
github.com/aws/smithy-go.(*InvalidParamsError).Len
github.com/aws/smithy-go.(*OperationError).Error
github.com/aws/smithy-go.(*OperationError).Unwrap
github.com/aws/smithy-go.(*ParamRequiredError).AddNestedContext
github.com/aws/smithy-go.(*ParamRequiredError).Error
github.com/aws/smithy-go.(*ParamRequiredError).SetContext
github.com/aws/smithy-go.(*SerializationError).Error
github.com/aws/smithy-go.(*SerializationError).Unwrap
github.com/aws/smithy-go.(*invalidParamError).AddNestedContext
github.com/aws/smithy-go.(*invalidParamError).Error
github.com/aws/smithy-go.(*invalidParamError).SetContext
github.com/aws/smithy-go.ErrorFault.String
github.com/aws/smithy-go.InvalidParamsError.Error
github.com/aws/smithy-go.NewErrParamRequired
github.com/aws/smithy-go.ParamRequiredError.Error
github.com/aws/smithy-go.invalidParamError.Error
github.com/aws/smithy-go.invalidParamError.Field
github.com/jmespath/go-jmespath
github.com/jmespath/go-jmespath.init

どういうライブラリを使っているかなんとなく分かりました。

色々なリクエストを発行してみる

AWS Parameters and Secrets Lambda Extensionが起動するHTTPサーバーは/systemsmanager/parameters/getもしくは/secretsmanager/getというパスに対するGETリクエストを処理します。GET以外のメソッドを利用したり、別のパスにリクエストを発行するとどうなるのでしょうか?

以下のコードを試します。

req = request.Request('http://localhost:2773/secretsmanager/get?secretId=lambda/extension/test', headers=headers, method="POST")
try:
    with request.urlopen(req) as res:
      pprint(res.getheaders())
except urllib.error.HTTPError as ex:
    print(ex.code, ex.reason)

ログには400 Bad Requestが出力されました。

続いて先程のコードのmethod="POST"を他のメソッドにして試していきます。

メソッド ステータスコード レスポンスボディ
POST 400 Bad Request
PUT 400 Bad Request
PATCH 400 Bad Request
DELETE 400 Bad Request
HEAD 400 Bad Request
TRACE 400 Bad Request
OPTIONS 400 Bad Request

全部400 Bad Requestでした。

続いてクエリストリングを指定せずにGETするとどうなるか確認します。リクエスト発行部を以下のように変更して実行してみます。

req = request.Request('http://localhost:2773/secretsmanager/get', headers=headers, method="GET")

こちらもログは400 Bad Requestでした。クエリストリングが不足しているとか、もう少し情報が欲しいところではあります。

続いてコードを以下のように修正し、存在しないパスへのリクエストを試します。

req = request.Request('http://localhost:2773/secretsmanager/', headers=headers, method="GET")

こちらもログは400 Bad Requestでした。404ではないんですね。

まとめ

AWS Parameters and Secrets Lambda Extensionの仕様について色々と調査してみました。そのうちOSSでコードが公開されると面白そうですね。