boto3でList系APIを使用するときの典型コードについて

boto3のList APIを使用する際の典型的なコードについての備忘録です。
2024.05.29

こんにちは。サービス開発室の武田です。

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を使うとスマートですね。皆さんは普段どのように書かれているでしょうか。