boto3でList系APIを使用するときの典型コードについて
こんにちは。サービス開発室の武田です。
PythonでAWSのリソースを操作する際にはboto3というライブラリを使うのが基本です。さてboto3を使用する上で覚えておかなければいけない注意点として、List系APIがあります。「IAMロールの一覧を取得する」「S3バケットのオブジェクト一覧を取得する」など、List操作をする機会は多くあります。注意点とは何かというと、 一度のList操作ですべての結果が返ってくるわけではない ということです。
たとえばiam.list_roles()
では次のように書かれています。
Note that IAM might return fewer results, even when there are more results available. In that case, the IsTruncated response element returns true, and Marker contains a value to include in the subsequent call that tells the service where to continue from.
引用元:list_roles
同じようにs3.list_objects_v2()
でも次のように書かれています。
By default, the action returns up to 1,000 key names. The response might contain fewer keys but will never contain more.
引用元:list_objects_v2
そんなわけで、一覧取得するプログラムではこれらをケアしてやる必要があります。
いつもどう書いてたっけな?
さてAPIによってパラメーター名が多少変わることもありますが、基本的な使い方は同じです。ところがいざ書こうとすると、いつもどう書いてたっけな?と少し考えてしまうことがままあります。いくつかのパターンを考え、「マイベスト」な書き方を残しておきます。
今回は例題としてiam.list_roles()
を使っていきます。
愚直な書き方
まず大前提として、一覧を取得するときに、一度で取りきれず後続の一覧が欲しい場合にはMarker
というパラメーターを指定します。これによってAPIはどの地点以降を返せばいいかを識別します。APIによってはNextToken
という名前の場合もあるでしょう。もちろん1回目の問い合わせではそんな値ありません。そこでNone
を指定したりするとエラーになるので、呼び分けが必要です。
import boto3 iam = boto3.client("iam") marker = None roles = [] while True: if marker: res = iam.list_roles(Marker=marker) else: res = iam.list_roles() roles.extend(res["Roles"]) if res["IsTruncated"]: marker = res["Marker"] else: break
Maker
の有無で分岐し、取得したロール一覧はroles
リストに溜めていきます。IsTruncated
というパラメーターが存在する場合、後続の値があるということですので、ループを継続するかのチェックをしています。このコードは問題なく動きます。
1回目の呼び出しはループの外へ
Marker
なしの呼び出しは最初の1回だけです。これを無理にループに入れる必要はなさそうです。というわけでそれを外に出し、合わせてMarker
ありの呼び出しをIsTruncated
のブロックに移してしまいます。
res = iam.list_roles() roles = [] while True: roles.extend(res["Roles"]) if res["IsTruncated"]: res = iam.list_roles(Marker=res["Marker"]) else: break
1回目の呼び出し結果を先にリストに突っ込む
roles
を空リストとして宣言していますが、1回目の呼び出し結果を先に突っ込んでも問題なさそうです。roles
は最初の結果を使って初期化するようにします。
res = iam.list_roles() roles = res["Roles"] while True: if res["IsTruncated"]: res = iam.list_roles(Marker=res["Marker"]) roles.extend(res["Roles"]) else: break
ループの条件にIsTruncatedを使用する
先ほどのコード修正をすると、そもそもwhile True:
が無駄に見えます。ループの中でIsTruncated
を使ってループの終了制御をしていますが、ループの条件に書いた方が早いでしょう。
res = iam.list_roles() roles = res["Roles"] while res["IsTruncated"]: res = iam.list_roles(Marker=res["Marker"]) roles.extend(res["Roles"])
IsTruncated vs Marker
ここは好みの問題で賛否ありそうですが、仕様として IsTruncatedがTrueの場合、Markerが存在 します。そのため次のようにループ条件を書き換えても問題ありません。
res = iam.list_roles() roles = res["Roles"] while "Marker" in res: res = iam.list_roles(Marker=res["Marker"]) roles.extend(res["Roles"])
extend vs +=
list#exnted()
は元のリストに、引数のコレクションの要素を追加するメソッドです。これは+=
でも同じ操作が提供されています。
res = iam.list_roles() roles = res["Roles"] while "Marker" in res: res = iam.list_roles(Marker=res["Marker"]) roles += res["Roles"]
だいぶシンプルになりましたね!
Paginatorを使用する
※2024-06-02 追記。
同僚からPaginatorも便利だよ!と教えてもらったので追記します。
MarkerやTokenを使用しての取得はいかにもプリミティブな書き方ですね。Paginatorを使用することでそれらを隠してスマートに取得できます。典型的なコードは次のようになります。
roles = [] paginator = iam.get_paginator("list_roles") for page in paginator.paginate(): roles += page["Roles"]
なかなかいい感じですね!
さらにこのコードを一歩推し進めると次のように書けます。なんと1行になりました!
roles = sum((page["Roles"] for page in iam.get_paginator("list_roles").paginate()), [])
最後に
Markerを使った書き方も悪くないと思っていますが、Paginatorを使うとスマートですね。皆さんは普段どのように書かれているでしょうか。