Boto3(AWS SDK for Python)をコードリーディングして仕組みを理解する
はじめに
Boto3を利用する機会があったので、どのように動いているか気になったのでコードを読んでみた。その中で自分のための情報の整理や、実装が面白いな・なるほどなと感じた部分をまとめる。
Boto3を使ってAWS上での開発や、AWSのREST APIの理解などの役に立てば嬉しい。
Boto3とは
Boto3とは AWS SDK for Python のことで、内部的にはAWS CLI
でも利用されているBotocore
を利用している。
The SDK is composed of two key Python packages: Botocore (the library providing the low-level functionality shared between the Python SDK and the AWS CLI) and Boto3 (the package implementing the Python SDK itself).
そのため今回はBoto3とBotocoreのコードを調査対象とする。
調査対象
今回利用するバージョンは調査を開始した2021/04/25時点での最新版のこちら。この後出てくるコードの行数の指定などは全てこのバージョンのものが適用される。
Package | Version |
---|---|
boto3 | 1.17.57 |
botocore | 1.20.57 |
Boto3の2つのAPI、クライアントAPIとリソースAPIについて
Boto3を利用してAWSの操作をする場合、リソースAPIとクライアント(「低レベル」)APIが存在するため2通りの使い方がある。
それぞれでS3のListBucketsを呼び出して結果を表示するコードを比較すると以下のような特徴がある。
s3_client = boto3.client('s3')
response = s3_client.list_buckets()
for b in response['Buckets']:
print('bucket name:', b['Name'], ', created at:', b['CreationDate'])
s3_resource = boto3.resource('s3')
buckets = s3_resource.buckets
for b in buckets.all():
print('bucket name:', b.name, ', created at:', b.creation_date)
- | クライアントAPI | リソースAPI |
---|---|---|
特徴 |
boto3.client('<AWS_SERVICE_NAME>') を利用。全てのAWSのREST APIと1:1で対応し、API名とスネークケースにしたPythonのメソッド名に対応している(詳細) |
boto3.resource('<AWS_SERVICE_NAME>') を利用。AWSリソースをオブジェクト指向で表現している(詳細) |
戻り値の型 | dict型 | 対象のAWSリソースを抽象化したオブジェクト型 |
今回はそれぞれのAPIを利用する時に生成するオブジェクトを以下のように呼ぶこととして進める。
-
boto3.client('<AWS_SERVICE_NAME>')
で取得できる値をClient
オブジェクト -
boto3.resource('<AWS_SERVICE_NAME>')
で取得できる値をResource
オブジェクト
ちなみにドキュメントを読むとresource
はclient
を内包していると書かれており、以下のようにResource
オブジェクトからClient
オブジェクトを取り出せる。
It is also possible to access the low-level client from an existing resource:
# Create the resource sqs_resource = boto3.resource('sqs') # Get the client from the resource sqs = sqs_resource.meta.client
なのでまずはクライアントAPIの中身を読んでみて、その後にリソースAPI読むことでクライアントAPIとどう関係しているのか、どんな実装の違いがあるのかを調べていく╭(・ㅂ・)و
この記事を読んでわかること
- クライアントAPIとリソースAPIの実装の違い
- boto3の役割とbotocoreの役割の違い
- jsonファイルから動的にPythonのクラスやメソッドを生成している方法
- botocoreでAWSのREST APIに対応するメソッド以外に拡張されているメソッドの定義場所
- boto3が拡張しているメソッドの定義場所
- SDK内でAWSのREST APIのリトライ設定
- クレデンシャル情報を取得し、リクエスト署名を作成してAuthorizationヘッダに設定している実装
- AWSのREST APIのレスポンスをPythonのオブジェクトに変換する実装
など
クライアントAPIのコードリーディング
今回はS3で利用できるAWSのREST APIのListBuckets
を実行する場合で、大まかな流れと要点をまとめた。
boto3.client('s3')
によるClient
オブジェクトの生成
クライアントAPIを実行するため、対象サービス(今回はS3)のClient
オブジェクトを生成する部分。この内部で行われている処理の中で重要な役割を果たすオブジェクトを紹介しながら解説する。
boto3.session.Session
オブジェクト
└Client
オブジェクトを生成するために必要なclient
メソッドを持っているため、まず最初に生成されるオブジェクト。
def client(*args, **kwargs):
# Clientオブジェクトを生成する前に_get_default_session()でSessionオブジェクトを生成している
return _get_default_session().client(*args, **kwargs)
-
client
メソッドは、内包しているbotocore.session.Session
オブジェクトのcreate_client
メソッドを呼び出すだけであり、create_client
が最終的にClient
オブジェクト生成の処理をしている -
boto3
側のSession
でやっていることは少ないが_register_default_handlers
でboto3の拡張メソッドの定義情報を読み込んでいて、Client
(やResource
)オブジェクトの生成時に拡張メソッドを追加されるようにしている - メソッドを追加している部分は
Client
オブジェクトの説明パートに記載
botocore.session.Session
オブジェクト
└Client
(やResource
)オブジェクトを生成するメソッドを持ったオブジェクト。boto3を利用する場合は自分で直接生成はしないで良い。
# **boto3**のSessionのクラス定義
class Session(object):
def __init__(self, aws_access_key_id=None, aws_secret_access_key=None,
aws_session_token=None, region_name=None,
botocore_session=None, profile_name=None):
if botocore_session is not None:
self._session = botocore_session
else:
# boto3の通常利用だとここでbotocoreのSessionが生成され、boto3.Sessionオブジェクトに保持される
self._session = botocore.session.get_session()
-
__init__
処理の中で、AWSのREST APIには無いがbotocoreで処理を拡張するための定義が書かれているBUILTIN_HANDLERS
という定数を_register_builtin_handlers
で読み込んでいる-
Client
オブジェクト生成時には'creating-client-class'
に対応するadd_generate_presigned_url
が呼び出されることでgenerate_presigned_url
が追加されるようになっており、さらにサービスごとに固有の拡張の定義がある - 例えばS3の
Client
オブジェクト生成時であれば'creating-client-class.s3'
に対応するadd_generate_presigned_post
が呼び出され、generate_presigned_post
が追加されるようになっている - こちらもメソッドを追加している部分は
Client
オブジェクトの説明パートに記載
-
botocore.config.Config
オブジェクト
└リージョン名などのAWSリソースに対する設定や、リトライやプロキシなどSDKに対する設定が可能なオブジェクト。詳細は公式ドキュメントを。
import boto3
from botocore.config import Config
my_config = Config(
region_name = 'us-west-2',
signature_version = 'v4',
retries = {
'max_attempts': 5,
'mode': 'standard'
}
)
client = boto3.client('kinesis', config=my_config)
-
Config
オブジェクトはbotocoreパッケージなので、boto3のみ利用している場合は明示的にimport
を増やす必要がある - リージョンなどは
Config
オブジェクトに設定があればその値が最優先される - Boto3はAWSのRES APIのリトライの挙動はデフォルトでは
'legacy'
モード(v1)が利用され、他の言語のAWS SDKのリトライロジック (v2)と異なる- 他のAWS SDKと同じv2にしたい場合は
Config
のコンストラクタで'mode': 'standard'
を設定する必要がある - 実験的ではあるが、
'standard'
の機能に追加してクライアント側でレートリミットもできる'adaptive'
モードもある - v1とv2の違いの詳細はドキュメントを見て欲しいが、デフォルトのリトライ回数(v1:5回 <=> V2:3回)やリトライ対象のHTTPステータスコードや例外の種類が異なる
- 他のAWS SDKと同じv2にしたい場合は
'legacy'(v1)
と'standard'(v2)
の違いの記載について公式ドキュメントを一部引用
Legacy mode is the default mode used by any Boto3 client you create. As its name implies, legacy mode uses an older (v1) retry handler that has limited functionality.
Standard mode is a retry mode that was introduced with the updated retry handler (v2). This mode is a standardization of retry logic and behavior that is consistent with other AWS SDKs.
botocore.credentials.Credentials
オブジェクト
└環境変数や~/.aws/credentials
、IAMロールなどから得られたクレデンシャル情報を保持しているオブジェクト。
def load_credentials(self):
### 省略 ###
# 複数のProviderが優先順位を持ちリストになっている
for provider in self.providers:
# 提供されているProviderからCredentialsが取得できるかを上から順に確認
creds = provider.load()
if creds is not None:
# 一番最初に取得できたCredentialsを利用する
return creds
-
Client
オブジェクト生成時の引数に認証情報のパラメータが渡されてない場合(つまりデフォルトでは)create_client
内でget_credentials
を呼び出すことでCredentials
を取得する -
get_credentials
の処理を深追いすると-
ComponentLocator#get_component('credential_provider')
がcreate_credential_resolver
メソッドを呼ぶことで、複数のbotocore.credentials.XXXProvider
というクラスのオブジェクトのリストを保持したCredentialResolver
オブジェクトを取得する -
CredentialResolver#load_credentials
メソッドで、保持しているXXXProvider
オブジェクトのリストを上から順にloadしていき、最初に取得できたCredetial
が利用される
-
このXXXProvider
のリストの順序はこちら↓↓↓
#index | botocore.credentials.XXXProvider |
取得場所 |
---|---|---|
0 | EnvProvider | 環境変数 |
1 | AssumeRoleProvider | 指定したprofileが~/.aws/ の中でrole_arn が設定されていればそのロール |
2 | AssumeRoleWithWebIdentityProvider | |
3 | SSOProvider | |
4 | SharedCredentialProvider | 指定した~/.aws/ のprofile情報 |
5 | ProcessProvider | |
6 | ConfigProvider | |
7 | OriginalEC2Provider | |
8 | BotoProvider | |
9 | ContainerProvider | |
10 | InstanceMetadataProvider |
SDKドキュメントの順序は以下の通りで、1.
, 2.
はオブジェクト生成時に明示的に指定した場合に優先されるので3.
以降が↑のリストの順番通りになる。
それぞれのProviderの処理を全部見れてはいないのでもう少し調べたい🔎
- Passing credentials as parameters in the boto.client() method
- Passing credentials as parameters when creating a Session object
- Environment variables
- Shared credential file (~/.aws/credentials)
- AWS config file (~/.aws/config)
- Assume Role provider
- Boto2 config file (/etc/boto.cfg and ~/.boto)
- Instance metadata service on an Amazon EC2 instance that has an IAM role configured.
botocore.model.ServiceModel
オブジェクト
└クライアントAPI作成対象サービス(今回の例であればS3)のAWSのREST APIの一覧情報を保持するオブジェクトで、後述するがClient
オブジェクトのメソッド定義はこの情報から作られる。
def _load_service_model(self, service_name, api_version=None):
# 対象サービスのAPIが定義されたjsonファイルをdictに変換
json_model = self._loader.load_service_model(service_name, 'service-2', api_version=api_version)
# サービス名やAPI定義のdictなどを保持した`ServiceModel`クラスのオブジェクトにする
service_model = ServiceModel(json_model, service_name=service_name)
- 一覧情報は
ClientCreator#_load_service_model
で対象サービスのAWSのREST APIが定義されたjsonファイルを読み込み、dictに変換することで実現している- 今回ローカルのMacで動かしていた場合のS3のクライアントAPIの場合、
botocore/data/s3/2006-03-01/service-2.json
に定義されているAWSのREST APIの情報が読み込まれている
- 今回ローカルのMacで動かしていた場合のS3のクライアントAPIの場合、
読み込んでdictにしているjsonファイルの大まかな構成要素はこちら。(要素名のリンクや値の例はS3のservice-2.json
)
要素名 | 役割 | 値の例 |
---|---|---|
metadata |
サービスのAPIエンドポイント、プロトコルなどの子要素を持つ | "metadata":{ "globalEndpoint":"s3.amazonaws.com", "protocol":"rest-xml", ...} |
operations |
AWSのREST API名一覧と、それぞれのHTTPメソッド・パス・リクエスト/レスポンスボディの構成定義を示すキー(shape )などの子要素を持つ |
"operations":{ "AbortMultipartUpload":{ "name":"AbortMultipartUpload", "http":{ "method":"DELETE", "requestUri":"/{Bucket}/{Key+}", "responseCode":204 }, "input":{"shape":"AbortMultipartUploadRequest"}, "output":{"shape":"AbortMultipartUploadOutput"} }, "CompleteMultipartUpload":{...}, ...} |
shape |
リクエスト/レスポンスボディの構成要素の定義(XXXRequest , XXXOutput )と、その要素(member )の型定義が羅列されていて、例えばXXXOutput->members[]->${MEMBER_NAME}->shape->${MEMBER_DEFINITION} のような階層になっている |
"shapes":{ "AbortMultipartUploadRequest":{ "type":"structure", "required":[ "Bucket", "Key", "UploadId" ], "members":{ "Bucket":{ "shape":"BucketName", "documentation":"<p>The bucket nam....</p>", "location":"uri", "locationName":"Bucket" }, "Key":{ "shape":"ObjectKey", ...}, ...}, ...}, "AbortRuleId":{"type":"string"}, ...} |
Client
オブジェクト
└前述したがあらためて、boto3.client('s3')
で生成される、AWSのREST APIを呼び出せるメソッドなどを持っているオブジェクト。
-
botocore.Session#create_client
内で、Built-in Functionsのtype()を利用して対象サービスごとに動的に定義されたクラスからオブジェクト生成される - クラス定義は
type(str(class_name), tuple(bases), class_attributes)
の部分
引数 | 説明 |
---|---|
str(class_name) |
動的に定義されるクラス名、S3の場合は'S3' が入る |
tuple(bases) |
動的に定義されるクラスの親クラスで、ここではbotocore.client.BaseClient が設定される |
class_attributes |
動的に定義されるクラスのメソッド名やインスタンス変数名をkey, 実体をvalueとしたdictで、ServiceModel の情報を元にスネークケースのメソッド名をAWSのREST API呼び出しができるメソッドに対応させた形式になっている |
より細かい話をすると、class_attributes
は、_create_methods
でAWSのREST APIに対応するメソッド群、hooks.EventAliaser#emit
はbotocore, boto3で拡張するメソッド群がkey, valueで設定される。
_create_methods
対象サービスのREST API名をスネークケースにした文字列をkey、_create_api_method
内で動的に作成する_api_call
メソッドオブジェクトをvalueにすることで、Client
オブジェクトでAWSのREST APIに対応するメソッド呼び出しするとAPIが実行できるようにしている。
hooks.EventAliaser#emit
大きく分けると拡張メソッドの追加は2パターン
- botocoreの
Session
オブジェクトで定義した拡張メソッドを追加する処理の呼び出し - boto3の
Session
オブジェクトで定義されたパスを指定してimport_moduleすることで拡張メソッドを追加- S3の
Client
オブジェクト生成時であれば、'creating-client-class.s3'
に対するboto3.utils.lazy_call('boto3.s3.inject.inject_s3_transfer_methods')
がコールバックされ、'boto3.s3.inject.inject_s3_transfer_methods'の.
の前半部分である"boto3.s3.inject"
のパスに対応するモジュールであるinject.py
をimportし、.
の後半の'inject_s3_transfer_methods'
によってinject.py
のinject_s3_transfer_methods
に記載されているメソッドだけがオブジェクトに拡張される
- S3の
メタプログラミングのテクニックをうまく使っていて、なるほどなと感じた👏
ここで今までの情報を整理し、Client
オブジェクトが生成されているbotocore.client.pyの流れを説明すると↓↓↓
# ServiceModelオブジェクトを生成(今回はS3のAPI一覧などの情報を持っている)
service_model = self._load_service_model(service_name, api_version)
# この中でtype()を利用して動的にクラス定義(clsにClientクラスのメタ情報が入る)
cls = self._create_client_class(service_name, service_model)
### 3行省略 ###
# コンストラクタに渡すためのCredentialやConfigなどをまとめる
client_args = self._get_client_args(
service_model, region_name, is_secure, endpoint_url,
verify, credentials, scoped_config, client_config, endpoint_bridge)
# 動的クラスからオブジェクト生成 == 今回であればS3のClientオブジェクト
service_client = cls(**client_args)
ここまでで、Client
オブジェクトの生成処理が完了!!
s3_client.list_buckets()
でHTTPリクエストを送るまで
先ほど生成したClient
オブジェクトのメソッドを実行すると、どのように対応するAWSのREST API呼び出しにバインドしているかについての解説。
# operation_name='ListBuckets', py_operation_name='list_buckets'のような値が入る
# _create_api_method自体はclass_attributesを作るときにすでに呼ばれた部分
def _create_api_method(self, py_operation_name, operation_name,
service_model):
# ★呼び出されるのはここで、kwargsはクライアントAPI呼び出し時の引数のdict
def _api_call(self, *args, **kwargs):
if args:
raise TypeError(
"%s() only accepts keyword arguments." % py_operation_name)
# 処理の実装はさらにこの中
return self._make_api_call(operation_name, kwargs)
-
list_bucket
すると、動的なクラス定義で利用したclass_attributes
のdictの情報を利用することでoperation_name='ListBuckets'
の状態で_api_call
が呼ばれる -
kwargs
は引数をdictにしたものが入る-
list_buckets()
の場合は引数が空なので、空のdict - 引数があるAPI、例えばS3の
GetObject
であれば、呼び出し方はs3_client.get_object(Bucket='examplebucket', Key='HappyFace.jpg')
であるためkwargs
は{'Bucket': 'sugikei-sandbox', 'Key': 'HappyFace.jpg'}
が入ってくる
-
-
_make_api_call
では変数のoperation_name
に設定されているAPI名で、API定義のjsonであるoperations
からHTTPリクエストに必要なパスやリクエストメソッドなどの情報(例えばListBucketsであればこの部分)を取り出してリクエストに必要なオブジェクトを生成し、_make_request
でAWSのREST APIを呼ぶ
AWSのREST API呼び出しの実行ライブラリ・認証設定・レスポンスのパース
SDKがAWSの低レベルAPIと呼ばれるHTTPリクエストを、どのように肩代わりしてくれているのかを説明していく。
リクエスト送信処理と利用しているライブラリなどについて
- リクエスト処理まわりを担うオブジェクトは
botocore.endpoint.Endpoint
で、Client
オブジェクト生成の時のcreate_endpoint
で設定される-
ここで指定されている
botocore.httpsession.URLLib3Session
が実際にHTTP通信をするオブジェクトや、エンドポイントのURL、レスポンスをパースするオブジェクトなどを保持している - コネクションプールの数やタイムアウトの設定もこの時点で設定される
-
ここで指定されている
-
_make_api_call
から呼び出す_make_request
でEndpoint
オブジェクトを利用して、リクエストヘッダやボディの生成・HTTPリクエストの送信・HTTPレスポンスボディのXMLをパースしてdict形式にする処理を行なっている-
ここで
URLLib3Session#send
が呼ばれ、urllib3
パッケージを利用してHTTP通信をしている
-
ここで
urllib3
を利用する直前の処理がここ↓↓↓
def send(self, request):
try:
# proxyやコネクションの設定
proxy_url = self._proxy_config.proxy_url_for(request.url)
manager = self._get_connection_manager(request.url, proxy_url)
conn = manager.connection_from_url(request.url)
self._setup_ssl_cert(conn, request.url, self._verify)
request_target = self._get_request_target(request.url, proxy_url)
# ここからurllib3でHTTPリクエスト
urllib_response = conn.urlopen(method=request.method, url=request_target, body=request.body, headers=request.headers, retries=Retry(False), assert_same_host=False, preload_content=False, decode_content=False, chunked=self._chunked(request.headers))
Authorizationヘッダの生成
まずは署名バージョンを確認し、署名情報を計算するためのXXXAuth
オブジェクトを取得する。
def sign(self, operation_name, request, region_name=None,
signing_type='standard', expires_in=None, signing_name=None):
# 署名バージョンの取得 ex) 'v4', 's3v4', ...
signature_version = self._choose_signer(
operation_name, signing_type, request.context)
if signature_version != botocore.UNSIGNED:
kwargs = {
'signing_name': signing_name,
'region_name': region_name,
'signature_version': signature_version
}
try:
# SigV4Auth, S3SigV4AuthなどのAuthオブジェクト取得
auth = self.get_auth_instance(**kwargs)
except UnknownSignatureVersionError as e:
### 省略 ###
# 署名情報を計算して'Authorization'ヘッダへ設定
auth.add_auth(request)
-
hooks.EventAliaser#emit
からメタ呼び出しを経由してbotocore.signers.RequestSigner#sign
へ -
auth = self.get_auth_instance(**kwargs)
で、Authオブジェクト取得- 署名バージョン4であれば
botocore.auth.SigV4Auth
だが、S3の場合はSigV4Auth
を継承したbotocore.auth.S3SigV4Auth
が使われる
- 署名バージョン4であれば
Auth
オブジェクトとCredential
オブジェクトを利用して署名情報を計算し、Authorizationヘッダに設定する。
def add_auth(self, request):
datetime_now = datetime.datetime.utcnow()
request.context['timestamp'] = datetime_now.strftime(SIGV4_TIMESTAMP)
# STSのトークンがあればここで'X-Amz-Security-Token'ヘッダ設定
self._modify_request_before_signing(request)
# リクエスト情報を文字列化
canonical_request = self.canonical_request(request)
# タイムスタンプとリクエスト情報のハッシュ値計算
string_to_sign = self.string_to_sign(request, canonical_request)
# ここまでの情報を合わせて署名情報の計算
signature = self.signature(string_to_sign, request)
# 'Authorization'ヘッダを設定
self._inject_signature_to_request(request, signature)
- 取得した
Auth
オブジェクトのadd_auth
メソッド呼び出しでtoken、署名情報を生成 -
STSの一時的な認証情報を使う場合はSigV4の場合、
_modify_request_before_signing
で['X-Amz-Security-Token'
ヘッダを設定]する(https://github.com/boto/botocore/blob/1.20.57/botocore/auth.py#L401-L404) -
SigV4Auth#signature
でCredential
オブジェクトを利用して署名情報を生成して、_inject_signature_to_request
で'Authorization'
ヘッダに設定 - add_authで作成する署名情報は、厳密には
string_to_sign
でタイムスタンプや、ここまでのリクエスト情報文字列をsha256してDIGESTした文字列も利用しており、公式ドキュメントに書かれているプロセスにに対応して実装されてそうだなということがわかる
レスポンスボディのXMLのパース
ここ
でAWSのREST APIの結果を取得するが、レスポンスボディはXMLである。SDKとしてPythonで利用しやすいようにdict形式にパース処理をしている部分を解説する。
def _do_get_response(self, request, operation_model):
### 省略 ###
http_response = self._send(request)
# header, bodyの親要素をdictにしただけで、まだここではレスポンスボディはXMLのまま
response_dict = convert_to_response_dict(http_response, operation_model)
# protocol='rest-xml'
protocol = operation_model.metadata['protocol']
# XML用のParserオブジェクト取得
parser = self._response_parser_factory.create_parser(protocol)
# ここでレスポンスボディのXMLをdictに変換
parsed_response = parser.parse(
response_dict, operation_model.output_shape)
- 今回XMLのパースなのでParserは
botocore.parser.BaseXMLResponseParser
が利用される - XML以外でもパースできるようになっており、プロトコルによりどのParserが利用されるかは
botocore.parser.py
のpydocでわかりやすいAAが書かれている - 変数の
output_shape
はjsonで定義されているshape
のことで、対象のAWSのREST APIのレスポンスの要素が定義情報が入っている - さらに追っていくと
_handle_structure
の中でXMLをdictにし、shape
に入っている要素と照らし合わせて最終的なレスポンスのdictに必要な要素だけが入るようにしている
クライアントAPIまとめ
クライアントAPIはboto3
を使っているがほぼbotocore
内で完結していた。(厳密にはboto3.Session
はbotocore.Session
のuser-agentなどの設定を少しだけいじってるようではある)
なので極論、AWSのREST API(S3であればここに定義されているAPI)を利用するだけならboto3
を使わずにbotocore
だけでも↓のようなコードを書けば可能。しかしマルチパートアップロードを自動で実施してくれるupload_file
のような複数のAWSのREST APIを組み合わせて処理してくれる便利メソッドはboto3
に用意されている。
import botocore.session
# boto3#client('s3')した時にClientオブジェクト取得に必要な部分だけを抜粋
s3_botocore_client = botocore.session.get_session().create_client('s3')
botocore_response = s3_botocore_client.list_buckets()
for b in botocore_response['Buckets']:
print('bucket name:', b['Name'], ', created at:', b['CreationDate'])
クライアントAPIの動きはだいたい把握できたので、リソースAPIは何をしているのかを見ていく─=≡Σ((( つ•̀ω•́)つ
リソースAPIのコードリーディング
こちらもS3で利用できるAWSのREST APIのListBuckets
を実行する場合で、大まかな流れと要点をまとめた。
boto3.resource('s3')
によるResource
オブジェクトの生成
リソースAPIを実行するために(今回はS3)のResource
オブジェクトを生成する部分。ポイントとなるオブジェクトを紹介しながら解説する。
boto3.sessin.Session
オブジェクト
└Resource
オブジェクトはSession
のresource
メソッドにより生成する。Client
オブジェクト生成もSession
を利用していたが、今回はboto3
側でリソースAPI特有の処理を行なっている。
def resource(*args, **kwargs):
# Resourceオブジェクトを生成する前に_get_default_session()でSessionオブジェクトを生成している
return _get_default_session().resource(*args, **kwargs)
resources-1.json
ファイル
└リソースAPIを定義するためのjsonファイルで、botocore.loader.Loader#load_service_model
でjsonファイルを探して読み込みしている
try:
# ここで'resources-1'を指定
resource_model = self._loader.load_service_model(
service_name, 'resources-1', api_version)
except UnknownServiceError:
# サービスが存在しない場合はエラー
raise ResourceNotExistsError(...
except DataNotFoundError:
# 利用可能なAPIバージョンがない場合はエラー
raise UnknownAPIVersionError(...
- クライアントAPIと異なり、
resources-1.json
を指定して読み込んでいる - 指定したサービスの
resources-1.json
がロードできない場合、リソースAPIが使用できないと判定されてエラーになる - このjsonファイルが動的なクラス定義で利用される
読み込んでdictにしているjsonファイルの大まかな構成要素はこちら。(要素名のリンクや値の例はS3のresources-1.json
)
要素名 | 役割 | 値の例 |
---|---|---|
service |
Resource オブジェクトが利用できるメソッドやプロパティの親要素 |
- |
└action
|
Resource オブジェクトが利用できるメソッド |
CreateBucket を定義したjson |
└has
|
このjsonのresources 要素と合わせて利用される(詳しい役割は読み解けなかった) |
Bucket を定義したjson |
└hasMany
|
Resource オブジェクトが利用できるプロパティ |
Buckets を定義したjson |
resources |
Resource オブジェクトで生成できるオブジェクト |
Bucket , BucketPolicy , Object , ObjectVersion などを定義したjson |
Client
オブジェクト
└クライアントAPIの時に利用したClient
オブジェクトを生成して、リソースAPI内部で利用できるようにしている。生成方法はすでに説明したので省略。
boto3.resources.factory.ResourceFactory
オブジェクト
└load_from_definition
で動的にResource
オブジェクトのクラス定義をする役割を持つ。
def load_from_definition(self, resource_name,
single_resource_json_definition, service_context):
# 'action'をクラス定義に組み込むための処理
self._load_actions(attrs=attrs, ...
# 'hasMany'をクラス定義に組み込むための処理
self._load_collections(attrs=attrs, ...
# 'resources'をクラス定義に組み込むための処理
self._load_has_relations(attrs=attrs, ...
# 親クラス
base_classes = [ServiceResource]
# 動的にクラスを定義
return type(str(cls_name), tuple(base_classes), attrs)
-
_load_actions
でresources-1.json
のaction
要素をそれぞれ呼び出せるよう、do_action
メソッドにバインド- S3の場合
create_bucket
メソッドがここで作られる - 対応するクライアントAPIのメソッドに紐づけられている
- S3の場合
-
_load_collections
でresources-1.json
のhasMany
の要素をそれぞれPythonのproperty
として呼び出せるようにしたget_collection
メソッドをバインド-
get_collection
はboto3.resources.CollectionManager
クラスを親にして動的に定義したクラスのオブジェクトを返すメソッド - S3の場合
buckets
プロパティがここで作られる
-
-
_load_has_relations
でresources-1.json
のresources
要素をそれぞれcreate_resource
メソッドにバインド- S3の場合
Bucket
,BucketPolicy
,Object
,ObjectVersion
などなど
- S3の場合
- 最後にBuilt-in Functionsのtype()を利用して、対象サービスの
Resource
オブジェクトのクラスを定義
Resource
オブジェクト
└ResourceFactory
によって定義されたクラスから生成される
引数 | 説明 |
---|---|
str(class_name) |
動的に定義されるクラス名、S3の場合は's3.ServiceResource' が入る |
tuple(bases) |
動的に定義されるクラスの親クラスで、ここではboto3.resources.base.ServiceResource が設定される |
class_attributes |
動的に定義されるクラスのメソッド名やインスタンス変数名をkey, 実体をvalueとしたdictで、ResourceFactory で作成した情報を元にリソースAPIのメソッドやプロパティ呼び出しができるようになっている |
ここで今までの情報を整理し、Resource
オブジェクトが生成されているboto3.session.Session#resource
の流れを説明すると↓↓↓
# jsonからリソースAPIの定義を読み込み
resource_model = self._loader.load_service_model(service_name, 'resources-1', api_version)
# Clientオブジェクト生成
client = self.client(...
# 動的なクラス定義
cls = self.resource_factory.load_from_definition(...
# Clientオブジェクトを引数にResourceオブジェクト生成
return cls(client=client)
API実行時のバインド
ResourceFactory
オブジェクトの部分で解説したが、action
, collection
(hasMany
から定義されたproperty), resource
それぞれでバインドされたメソッドが呼ばれるが、少しだけ補足する。
do_action
)
actionの呼び出し(ServiceAction#__call__
の中でgetattr
を利用することで、Client
オブジェクトの対応するクライアントAPIを実行し、dictのレスポンスをリソースオブジェクトに変換する。
get_collection
)
collectionの呼び出し(親クラスをCollectionManager
にしたクラスを動的に定義し、そこからXXXCollectionManager
オブジェクトを生成する。(S3のbuckets
であればs3.bucketsCollectionManager
というクラス名)
XXXCollectionManager
でall
を実行した場合はResourceCollection#__iter__
が呼ばれる。そのpage
の中でgetattr
を利用することで、Client
オブジェクトの対応するクライアントAPI(buckets
であればlist_buckets
)を実行し、dictのレスポンスをリソースオブジェクトに変換する。
create_resource
)
resourceの呼び出し(load_from_definition
を利用してBucket
などのサブリソースのクラスを動的に定義してオブジェクト生成している。これはResource
オブジェクト自体のクラス定義もしているメソッドである。
リソースAPIまとめ
resources-1.json
からリソースAPIの定義を読み取っていて、オブジェクトの定義やAWSのREST APIの呼び出し方が記載されている。そしてAWSのREST APIを呼んでいるのはクライアントAPIで利用されていたClient
オブジェクトである。
なので僅かではあるが、レスポンスの成型時にオブジェクト生成などやっていない分リソースAPIの方が少しだけパフォーマンスが良いのかな?と想像(単純に比較しようとしてもAWSのREST APIの結果取得した後の部分のみを抽出してテストしないといけないので今回は断念)。
ただしパフォーマンスにこだわるならそもそもPython以外のSDK使う方が良いと思うので、Boto3を使う場合は可読性や使いやすさを考慮してリソースAPI使えるサービスなら使う方針が良いと思う。
感想
- クラスなどのオブジェクト生成はjsonファイルなどからメタプログラミングを利用している
- botocoreがCLIでも使われているので、できるだけサービスに対する処理実行時に読み込むファイル量を減らせるようにしてるのかも(想像)
- なのでIDEで補完が効かないのが個人的にはちょっとつらい…
- クライアントAPIにはWaitersという、特定のリソースがある状態(S3のバケットやオブジェクトが存在するorしないなど)になるまでポーリンングして状態を待ち続ける機能があることを知った
- 並列処理での待ち合わせとかに使うのかな?🤔
- ずっとコードを読んでると
('s3')
が顔文字に見えてくる - コードリーディングして理解した内容を文章で伝えるのめちゃむずい
おわりに
過去にはQiitaにも記事を書いてましたので、ぜひこちらも見てください。
Discussion