「緊急開催!サーバーレス座談会 in JAWS-UG 大阪」にてLTさせて頂いた際の資料です
https://jawsugosaka.doorkeeper.jp/events/162714
AWS SDKのClientはFactory経由で作ろうクラスメソッド株式会社岩⽥ 智哉1
View Slide
2⾃⼰紹介lクラスメソッド株式会社 サーバーサイドエンジニアl2023 Japan AWS Top Engineerl2023 Japan AWS All Certifications Engineerl前⼗字靭帯再建⼿術リハビリ中岩⽥ 智哉
3⾔いたいことAWS SDKのClientはFactory経由で作ろう
4LambdaとEC2/ECSの違いリクエストとコンピューティング環境がN:1リクエストとコンピューティング環境が1:1
5リクエストとコンピューティング環境が1:1だと…ソケット ソケットソケットソケットDynamoDB等のAWSサービスソケットソケットLambda実⾏環境で⽣成するソケットは1つで⼗分(なことが多い)
6これらを意識するとAWS SDKのClientの扱いが最適化できる
7良くない例1import boto3class TableA:def __init__(self):self._client = boto3.client('dynamodb')def put_item(self, item):self._client.put_item(TableName='tableA', Item=item)class TableB:def __init__(self, client):self._client = boto3.client('dynamodb')def put_item(self, item):self._client.put_item(TableName='tableB', Item=item)def handler(event, context):table_a = TableA()table_a.put_item({'foo': 'bar’})table_b = TableB()table_b.put_item({'hoge': 'fuga'})
8何が良くないのか︖
9よくある解決策class TableA:def __init__(self, client):self._client = clientdef put_item(self, item):self._client.put_item(TableName='tableA', Item=item)class TableB:def __init__(self, client):self._client = clientdef put_item(self, item):self._client.put_item(TableName='tableB', Item=item)import boto3client = boto3.client('dynamodb’)def handler(event, context):table_a = TableA(client)table_a.put_item({'foo': 'bar'})table_b = TableB(client)table_b.put_item({'hoge': 'fuga'})
10そうはいっても• 現実世界のアプリはもっと複雑。呼び出し階層も深くなる• 呼び出し先の呼び出し先の呼び出し先…にclientを伝搬するのは⾯倒• clientの処理化処理はもっと⾊々やることがあるimport boto3client = boto3.client('dynamodb’)def handler(event, context):table_a = TableA(client)table_a.put_item({'foo': 'bar'})table_b = TableB(client)table_b.put_item({'hoge': 'fuga'})
11提案Factoryクラスを使おう︕
12実装例(簡易版)import boto3class Boto3ClientFactory:# ⽣成したclientクラスのインスタンスをクラス変数に保持しておく_clients = {}@classmethoddef get_singleton_client(cls, service_name, **kwargs):# 対象サービスのclientクラスを⽣成済みならクラス変数のキャッシュから返却# 複数リージョンを扱う場合はキャッシュキーにリージョンを含めるなど追加の考慮が必要if service_name in cls._clients:return cls._clients[service_name]client = boto3.client(service_name, **kwargs)cls._clients[service_name] = clientreturn client
13Factoryクラスの追加実装例• タイムアウト値の調整• デフォルト値はLambda実⾏環境の設定値としては不適切• connect_timeout:60, read_timeout:60• Event Systemを利⽤したフックの登録• API呼び出し前にパラメータをクラス変数に保存• 例外キャッチ時にクラス変数に保存したパラメータをログ出⼒• ⾮シングルトンなclientクラス⽣成処理• たまにはPromise.All的な実装が必要になることもある
14Factoryクラスの利⽤例client = Boto3ClientFactory.get_singleton_client('dynamodb')def handler(event, context):table_a = TableA(client)table_a.put_item({'foo': 'bar'})table_b = TableB(client)table_b.put_item({'hoge': 'fuga'})def foo():bar()def bar():baz()def baz():client = Boto3ClientFactory.get_singleton_client('dynamodb')table_a = TableA(client)table_a.put_item({'foo': 'bar'})def handler(event, context):foo()タイムアウト値など適切に設定されたclientクラスが1発で取得可能呼び出し階層の深いところまでclientクラスを引き回さなくて良くなる
15Provisioned Concurrency利⽤時の注意Boto3ClientFactory.get_singleton_client('dynamodb’)def handler(event, context):…Init処理の中でclientクラスの⽣成を「空打ち」しておく
16初回のClientクラス⽣成処理は重いhttps://github.com/boto/botocore/blob/40d6219947f4d047088cbeb80f8f222f599f9c7c/botocore/loaders.py• 初回のclientクラス⽣成時はJSONファイルを読み込んで動的にクラスを⽣成するので「重い」• 2回⽬以後はキャッシュを使う• Init処理の中でclientクラス向けのキャッシュを「暖気」することでProvisioned Concurrencyに最適化https://github.com/boto/botocore/blob/40d6219947f4d047088cbeb80f8f222f599f9c7c/botocore/data/dynamodb/2012-08-10/service-2.json
17以上ありがとうございました
18