AWS SDK for Python (Boto3) の “Client API” と “Resource API” の違いについて調べてみた

2020.03.17

みなさん、こんにちは! AWS事業本部の青柳@福岡オフィスです。

AWS SDK for Python (Boto3) の特徴の一つとして挙げられることに、AWSのリソースを操作するAPIとして "Client API""Resource API" の2種類が用意されているという点があります。

(AWS SDK for Python (Boto3) トップページ の冒頭に書かれているほどです)

これらの違いについてあまり分かっていなかったので、今回調べてみました。

なお、本ブログエントリは、以下の記事を読んでAWS SDK for Python (Boto3) に入門した方、入門しようとしている方のレベルを想定しています。

コードでAWSをさわってみよう!プログラミング初心者向けAWS SDK(Boto3)の使い方 | Developers.IO

"Client API" と "Resource API"

"Client API" と "Resource API" の違いをザックリ説明すると、次のようになります。

Client API (低レベルAPI)
AWSのREST APIと1対1で対応した作りになっている。
Resource API (高レベルAPI)
AWSリソースをオブジェクト指向で取り扱えるようになっている。

これだけだと何だかよく分からないので、実際にコードを書いて違いを見て行きます。

その1: EC2インスタンスの情報を取得する

Client APIを使った場合

コード例は以下のようになります。

import boto3

client = boto3.client('ec2')
response = client.describe_instances(InstanceIds=['i-025e8bfafc8937f02'])

print(response)

Client APIを使用する場合、最初に boto3.client('サービス名') を呼び出します。 続けて、対象のインスタンスIDを引数に指定して describe_instances メソッドを呼び出します。(インスタンスの情報を取得するメソッド)

実行結果は以下のようになります。

{'Reservations': [{'Groups': [], 'Instances': [{'AmiLaunchIndex': 0, 'ImageId': 'ami-052652af12b58691f', 'InstanceId': 'i-025e8bfafc8937f02', 'InstanceType': 't3.micro', 'KeyName': 'example-key', 'LaunchTime': datetime.datetime(2020, 3, 15, 17, 28, 43, tzinfo=tzutc()), ・・・(以下省略)

戻り値 response を出力すると一見JSONのようにも見えますが、JSONとは異なる「辞書型」という型で格納されています。 辞書型からJSON形式に変換すると以下のようになります。(一部のみ抜粋しています)

{
    "Reservations": [
        {
            "Groups": [],
            "Instances": [
                {
                    "AmiLaunchIndex": 0,
                    "ImageId": "ami-052652af12b58691f",
                    "InstanceId": "i-025e8bfafc8937f02",
                    "InstanceType": "t3.micro",
                    "KeyName": "example-key",
                    "LaunchTime": "2020-03-15T17:28:43+00:00",
                    --------<< 中略 >>--------
                }
            ],
            "OwnerId": "123456789012",
            "ReservationId": "r-04213284b39c734a0"
        }
    ],
    "ResponseMetadata": {
        "RequestId": "8a054f17-d7fe-43f5-8f30-a2fec9928b2e",
        "HTTPStatusCode": 200,
        "HTTPHeaders": {
            "content-type": "text/xml;charset=UTF-8",
            "content-length": "7291",
            "vary": "accept-encoding",
            "date": "Sun, 15 Mar 2020 18:23:13 GMT",
            "server": "AmazonEC2"
        },
        "RetryAttempts": 0
    }
}

Reservations で始まる階層は、describe_instances の結果、つまり「取得したインスタンスの情報」が格納されています。

ResponseMetadata で始まる階層は、APIの実行結果が格納されています。

Reservations の階層構造については後ほど説明しますが、インスタンスの具体的な情報を取り出すには以下のようにします。 (get は辞書型から要素を取り出す関数です)

import boto3
 
client = boto3.client('ec2')
response = client.describe_instances(InstanceIds=['i-025e8bfafc8937f02'])

instance = response['Reservations'][0]['Instances'][0]
instance_type = instance.get('InstanceType')
print(instance_type)

実行結果:

t3.micro

このように、Client APIでは、情報を得たい対象のリソースIDを引数に与えてメソッドを実行します。 実行結果は「辞書型」で得られるため、そこから階層を辿って必要な情報を取り出すことになります。

Resource APIを使った場合

コード例は以下のようになります。

import boto3

ec2 = boto3.resource('ec2')
instance = ec2.Instance('i-025e8bfafc8937f02')

instance_type = instance.instance_type
print(instance_type)

実行結果:

t3.micro

Resource APIを使用する場合、最初に boto3.resource('サービス名') を呼び出します。 続けて、対象のインスタンスIDを引数に指定して Instance オブジェクトを生成します。

得られた Instance オブジェクトは、インスタンスの各種情報を「アトリビュート (属性)」として持っています。 ここでは instance_type という属性を参照して、インスタンスのタイプを取得しています。

(属性の一覧は Boto3リファレンス で確認できます)

このように、Resource APIでは、まず対象のリソースを「オブジェクト」として取得してから、オブジェクトの持つ「属性」を参照して情報を取り出すという手順になります。

その2: EC2インスタンスを起動する

Client APIを使った場合

コード例は以下のようになります。

import boto3

client = boto3.client('ec2')
response = client.start_instances(InstanceIds=['i-025e8bfafc8937f02'])

print(response)

「その1: EC2インスタンスの情報を取得する」の時と同様に boto3.client('ec2') を呼び出します。 次に、情報取得の describe_instances メソッドの代わりに、インスタンスを起動する start_instances メソッドを呼び出します。

実行結果は以下のようになります。(JSON形式に変換しています)

{
    "StartingInstances": [
        {
            "CurrentState": {
                "Code": 0,
                "Name": "pending"
            },
            "InstanceId": "i-025e8bfafc8937f02",
            "PreviousState": {
                "Code": 80,
                "Name": "stopped"
            }
        }
    ],
    "ResponseMetadata": {
        "RequestId": "942b4308-9665-4292-a7e4-bbb1c9431f72",
        "HTTPStatusCode": 200,
        "HTTPHeaders": {
            "content-type": "text/xml;charset=UTF-8",
            "content-length": "579",
            "date": "Sun, 15 Mar 2020 19:21:11 GMT",
            "server": "AmazonEC2"
        },
        "RetryAttempts": 0
    }
}

StartingInstances で始まる階層に、「インスタンス起動」の処理結果が格納されています。

PreviousState は処理実行前のインスタンスの状態で、stopped (停止) となっています。 CurrentState は処理実行後のインスタンスの状態で、pending (処理待ち) となっています。

処理実行後の状態が running ではなく pending となっているのは、start_instances メソッドは起動を指示したら結果を待たずに次の処理に進んでしまうためです。 (結果を待ち合わせるための仕組みも用意されていますが、今回は割愛します)

Client APIでは、リソースを操作する場合も参照系と同様に、対象のリソースIDを引数に与えてメソッドを実行する方法を取ります。

Resource APIを使った場合

コード例は以下のようになります。

import boto3

ec2 = boto3.resource('ec2')
instance = ec2.Instance('i-025e8bfafc8937f02')

response = instance.start()
print(response)

「その1: EC2インスタンスの情報を取得する」の時と同様に boto3.resource('ec2') を呼び出した後、Instance オブジェクトを生成します。 そして、Instance オブジェクトに用意された start メソッドを呼び出すことでインスタンスの起動を指示します。

実行結果はClient APIを使った場合と同じフォーマットになっています。

{
    "StartingInstances": [
        {
            "CurrentState": {
                "Code": 16,
                "Name": "pending"
            },
            "InstanceId": "i-025e8bfafc8937f02",
            "PreviousState": {
                "Code": 16,
                "Name": "stopped"
            }
        }
    ],
    "ResponseMetadata": {
        "RequestId": "9ea709fa-cd54-41e6-a3d6-175c6108c7a8",
        "HTTPStatusCode": 200,
        "HTTPHeaders": {
            "content-type": "text/xml;charset=UTF-8",
            "content-length": "580",
            "date": "Sun, 15 Mar 2020 19:39:19 GMT",
            "server": "AmazonEC2"
        },
        "RetryAttempts": 0
    }
}

Resource APIでは、リソースを操作する場合には対象のリソースを「オブジェクト」として取得してから、オブジェクトの持つ「メソッド」を呼び出して操作を行います。

その3: EC2インスタンスの一覧を取得する

Client APIを使った場合

コード例は以下のようになります。

import boto3

client = boto3.client('ec2')
response = client.describe_instances()

print(response)

Client APIを使用する場合、インスタンスの情報を取得する describe_instances メソッドを引数を付けずに呼び出すことで、リージョン内の全てのインスタンスの情報を取得することができます。

実行結果には全インスタンス分の情報が含まれるため膨大なものとなります。 全体の構造が分かるように整理すると、以下のようになります。

{
    "Reservations": [
        {
            "Groups": [],
            "Instances": [
                {
                    << インスタンスの情報 >>
                },
                {
                    << インスタンスの情報 >>
                },
                ・・・
            ],
            << Reservationに関する情報 >>
        },
        {
            "Groups": [],
            "Instances": [
                {
                    << インスタンスの情報 >>
                },
                {
                    << インスタンスの情報 >>
                }.
                ・・・
            ],
            << Reservationに関する情報 >>
        },
        ・・・
    ],
    "ResponseMetadata": {
        << APIの実行結果 >>
    }
}

最上位に Reservations[] という要素の配列があります。 その配下に Groups[] および Instances[] という要素の配列が含まれています。

Reservations とは、EC2インスタンスの起動要求の単位を表す概念です。

マネジメントコンソール、AWS CLI、AWS SDKの手段を問わず、EC2インスタンスの起動要求を行う毎に「Reservation ID」が割り当てられます。 1度の要求で複数のEC2インスタンスを起動する場合は、1つのReservation IDに複数のインスタンスが含まれます。 別々に要求した場合は、別々のReservation IDにそれぞれインスタンスが含まれます。

※ Reservation (ID) についての詳しい解説は、こちらの記事を参照してください。 【小ネタ】Amazon EC2のReservation IDって何なんだろう? | Developers.IO

Groups は「EC2-Classic」の場合にのみ関係する項目ですので、「EC2-VPC」の場合には無視して構いません。

Instances には、配列の形で各インスタンスの情報が格納されています。

インスタンスの情報を取り出す方法

「1. EC2インスタンスの情報を取得する」では、結果の中に1つのインスタンスの情報しか含まれていませんでしたので、決め打ちで以下のようにしていました。

instance = response['Reservations'][0]['Instances'][0]

これは「Reservations 配列の最初の要素を取り出し、更にその中から Instances 配列の最初の要素を取り出す」という意味を示します。

しかし今回は、Reservations 配列に1つ以上 (複数かもしれない) の要素が含まれており、かつ、その各要素に含まれる Instances 配列に1つ以上 (複数かもしれない) の要素が含まれています。

その場合には for ~ in ~ 構文を使います。

import boto3

client = boto3.client('ec2')
response = client.describe_instances()

for reservation in response['Reservations']:
    for instance in reservation['Instances']:
        instance_id   = instance.get('InstanceId')
        instance_type = instance.get('InstanceType')
        print(instance_id , instance_type)

Reservations 配列から要素を1つずつ取り出す for 文の中に、Instances 配列から要素を1つずつ取り出す for 文が「入れ子 (ネスト)」になっています。 これで、全てのインスタンスの情報を網羅的に取り出すことができます。

実行結果:

i-087ac4608d9c5ed4b t3.micro
i-025e8bfafc8937f02 t3.micro
i-039dfe0fdc00ad0e7 t3.micro
i-0873d1fa32a24c43b t3.micro

Resource APIを使った場合

コード例は以下のようになります。

ec2 = boto3.resource('ec2')
instance_iterator = ec2.instances.all()

for instance in instance_iterator:
    instance_id   = instance.instance_id
    instance_type = instance.instance_type
    print(instance_id, instance_type)

Resource APIでは、単一のインスタンスの情報を取得する場合とは全く異なる構文を用います。

まず、ec2.instances によって「EC2インスタンスの集まり」を示す「コレクション」を取得します。

更に、instances コレクションの all() メソッドを呼び出すことで、コレクション内の全てのインスタンスを列挙したものである「イテレータ」を取得します。

インスタンスのイテレータから各要素 (つまり個々のインスタンスオブジェクト) を取り出すために for ~ in ~ 構文を使います。 Client APIのように Reservation の概念はありませんので、シンプルに一重の for 文となっています。

(コレクションとイテレータって何が違うの? という疑問があるかもしれませんが・・・ここでは「for 文で要素を順に取り出すには『イテレータ』の形式でなければならない」とだけ覚えておいてください)

実行結果:

i-087ac4608d9c5ed4b t3.micro
i-025e8bfafc8937f02 t3.micro
i-039dfe0fdc00ad0e7 t3.micro
i-0873d1fa32a24c43b t3.micro

応用例: 起動中のインスタンスをチェックして強制停止するスクリプト

ここまで紹介してきたコードを組み合わせて、少しだけ複雑なスクリプトを作ってみましょう。

Client APIを使ったサンプル

import boto3

# 状態が「running」であるインスタンスの一覧を取得する
client = boto3.client('ec2')
response = client.describe_instances(
    Filters=[
        {
            'Name': 'instance-state-name',
            'Values': [ 'running' ]
        }
    ]
)

# 各インスタンスに対して処理を行う
for reservation in response['Reservations']:
    for instance in reservation['Instances']:
        # インスタンスIDを取得する
        instance_id = instance.get('InstanceId')
        print(instance_id)
        # インスタンスIDを指定してインスタンスを強制停止する
        response = client.stop_instances(
            InstanceIds=[ instance_id ],
            Force=True
        )
        print(response['StoppingInstances'][0]['CurrentState']['Name'])

これまでに紹介した describe_instances メソッドの使い方は、以下の2通りでした。

  • 引数にインスタンスIDを指定する: 指定したインスタンスの情報を取得
  • 引数なし: 全てのインスタンスの情報を取得

もう一つの使い方は、引数 Filter を指定することにより特定条件に合致するインスタンスを抽出することです。

ここでは、条件となる項目に instance-state-name を、値に running を指定することで「実行中のインスタンス」を抽出しています。(値は複数指定することもできるため配列型となってます)

抽出した「起動中のインスタンス」の一覧から、for 文を使って各インスタンスを取り出します。

インスタンスIDを取得して変数 instance_id に代入した後、instance_id を引数に指定してメソッド stop_instances を呼び出しています。

stop_instances メソッドの使い方は start_instances メソッドとほぼ同じです。 ここでは強制的に停止を行うために引数 Force を指定しています。

実行結果:

i-087ac4608d9c5ed4b
stopping
i-025e8bfafc8937f02
stopping

「その2: EC2インスタンスを起動する」で説明した通り、start_instances メソッドや stop_instances メソッドはインスタンスの起動/停止の処理結果を待ち合わせません。 今回は、確実にインスタンスが停止したかどうかを確認する処理については割愛します。

Resource APIを使った場合

コード例は以下のようになります。

import boto3

# 状態が「running」であるインスタンスの一覧を取得する
ec2 = boto3.resource('ec2')
instance_iterator = ec2.instances.filter(
    Filters=[
        {
            'Name': 'instance-state-name',
            'Values': [ 'running' ]
        }
    ]
)

# 各インスタンスに対して処理を行う
for instance in instance_iterator:
    # インスタンスIDを取得する
    instance_id = instance.instance_id
    print(instance_id)
    # インスタンスオブジェクトに対して強制停止のメソッドを呼び出す
    response = instance.stop(Force=True)
    print(response['StoppingInstances'][0]['CurrentState']['Name'])

全インスタンスを取得するには instances コレクションの all メソッドを使いましたが、条件を指定して抽出するには filter メソッドを使います。 filter メソッドで条件を指定する引数の書き方は、Client APIの場合とほぼ同様です。

抽出した「起動中のインスタンス」のイテレータから、for 文を使って各インスタンスを取り出します。

インスタンスIDを取得していますが、この後の処理で使われることはなく、print 文で画面に出力するためだけに取得しています。(この点がClient APIとの違いです)

インスタンスのメソッド stop に引数 Force を指定して呼び出すことで、インスタンスを強制停止しています。

おわりに

"Client API" と "Resource API" の基本的な使い方と、簡単な応用サンプルを試してみました。

この規模のコードですと処理の流れは似たようなものになりますので、あまり違いが分からなかったかもしれません。 しかし、細部の記述方法には明らかな違いがあることがお分かり頂けたのではないかと思います。

2種類のAPIについて少し調べて・使ってみただけですが、以下のような特徴があるのではないかと思いました。

Client API
REST APIと1対1で対応しているため、理論上は「できないことはない。」
結果が基本的に辞書型で得られるので、必要な情報を取り出すのが若干めんどう。
Resource API
オブジェクト指向で処理を記述できるので、コードの見通しが良くなる。
サービスやリソースの種類によってはResource APIが用意されていないことがある。(その場合はClient APIを使う必要がある)

複雑な処理を行うコードを書く際には、Client APIとResource APIの違いを踏まえて使い分ける必要がでてくるかもしれません。

その際に、本ブログエントリの内容が少しでも参考になれば幸いです。