Python
AWS
EC2
boto3

辞書型とリスト型を理解しながらEC2のdescribe結果を扱う

qiitaデビューです。
これまでシェルスクリプトぐらいしかコードを書くことはなかったんですが、AWSを扱うことになって、これからはインフラ屋さんも言語ぐらい使えねばと自らに言い聞かせて、Pythonの学習を始めました。

初学者なので、間違いがあったら指摘してもらえると嬉しいです。


きっかけ

EC2のインスタンスとかボリュームについて必要な情報だけ拾って一覧を作りたいなあ
→「そうだスクリプトを書こう」

AWS CLI とシェル でもよかったのですが、今後Lambdaに持っていって色々やってみたいので、SDKが提供されてる言語で書いてみようと。

それに、Python流行ってるし...

目標

ひとまず以下を目標にしました。

  • AWS SDK for Python (Boto3)を使ってEC2インスタンスの情報を取得する
  • 今後、応用が利くように、コードの意味を理解する

環境

CentOS 6.9
Python 3.6.5 (Python3.xの環境を準備するのはこちらの記事を参考にしました)
AWS側でIAMのアクセスキー作成(ユーザーガイド
Boto3の初期設定(Boto 3 Documentation - Quickstart

とりあえず出力

1回目

ググって出てきた情報を参考に、「describe_instances メソッドを使えばいいんだな」と以下のコードを実行。

import boto3

def test():
    client = boto3.client('ec2')
    res = client.describe_instances()
    print(res)
    return

if __name__ == '__main__':
    test()

なんかそれっぽい情報が出力されてきましたが、

{'Reservations': [{'Groups': [],
                   'Instances': [{'AmiLaunchIndex': 0,
                                  'Architecture': 'x86_64',
                                  'BlockDeviceMappings': [{'DeviceName': '/dev/xvda',
                                                           'Ebs': {'AttachTime': datetime.datetime(2018, 4, 23, 3, 25, 17, tzinfo=tzutc()),
                                                                   'DeleteOnTermination': True,
                                                                   'Status': 'attached',
                                                                   'VolumeId': 'vol-xxxxxxxxxxxxxxxxx'}}],
                                  'ClientToken': '',
                                  'EbsOptimized': False,
                                  'EnaSupport': True,
                                  'Hypervisor': 'xen',
                                  'ImageId': 'ami-xxxxxxxx',
                                  'InstanceId': 'i-xxxxxxxxxxxxxxxxx',
                                  'InstanceType': 't2.micro',
                                  'KeyName': 'xxxxxxxx',
                                  'LaunchTime': datetime.datetime(2018, 5, 15, 5, 30, 45, tzinfo=tzutc()),
                                  'Monitoring': {'State': 'disabled'},
                                  'NetworkInterfaces': [{'Attachment': {'AttachTime': datetime.datetime(2018, 4, 23, 3, 25, 16, tzinfo=tzutc()),
                                                                        'AttachmentId': 'eni-attach-xxxxxxxx',
                                                                        'DeleteOnTermination': True,
                                                                        'DeviceIndex': 0,
                                                                        'Status': 'attached'},
 #長いので以降省略

※出力結果は、pprint(参考記事)で見やすくしています

インスタンスのメタデータなんだろうなあ、ということは辛うじて分かるものの、これじゃ全く応用ができないことに気づく。(体系的に勉強してないし残念ながら当然)

2回目

次に、こちらの記事とかを参考にしつつ、「これでインスタンスIDだけを抜き出せる」と思って実行。

import boto3

def test():
    client = boto3.client('ec2')
    res = client.describe_instances()["Reservations"][0]["Instances"][0]["InstanceId"]
    print(res)
    return

if __name__ == '__main__':
    test()

出力結果

i-xxxxxxxxxxxxxxxxx

あれれー、おかしいぞー?
インスタンスIDは取れたけど、インスタンスが1個しか表示されない。
やっぱり、ちゃんと理解しないとダメですね...

...というわけで、Pythonで扱う辞書型とリスト型を調べました。

dictionary (辞書)型

波括弧{}で囲われているデータを見て(良く分かってなかったので)漠然とjsonぽいと思っていたのですが、調べたらdictionaryと呼ばれるPythonに組み込みのデータ型だと分かりました。

特徴

ググると色々な解説があるので、簡単に特徴をまとめます。

  • キー(Key) と 値(Value) のペアが集まったデータ
    データを取り出すときは、キーを用いて紐づく値を得る
    このようなデータ構造は、連想配列やマップ型と呼ばれる

  • 要素を並び順で管理するわけじゃない
    なので、よくある配列みたいに番号から値を得ることはできない

記述

各要素はカンマ(,)区切りで分けられていて、要素内のキーと値はコロン(:)で分けられています。

{ Key_A: Value_A, Key_B: Value_B }

使い方

ポイントは「キー」を用いて「値」を得るという特徴です。
たとえば、以下のようにな、hogehogeという辞書型オブジェクトから値(Value_A)を取り出すには、

hogehoge = {
    Key_A: Value_A,
    Key_B: Value_B
}

以下のように、オブジェクト(hogehoge)に対して、キー(Key_A)を添え字として付けます。この時、キーを角括弧で囲います。

hogehoge[Key_A]

もし、キー(Key_A)に紐づく値を変更したい場合は、以下のように変更したい値(Value_X)代入してあげればよいです。

hogehoge[Key_A] = Value_X

辞書型オブジェクトのメソッドを使って他にもいろいろな操作ができますが、とりあえずキーを使って値を取り出せれば、EC2のdescribeした情報を拾うことができそうです。

list(リスト)型

出力結果をよく見ると、波括弧のほかに、角括弧[]で囲われている部分があることに気づきます。
「なんじゃこれ。括弧のネスト複雑すぎ...」と思いつつ調べると、Python組み込みのlist型というデータの記述であるとわかりました。

特徴

こちらも簡単に特徴をまとめます。

  • データが順序をもって並ぶシーケンス型のデータ
  • いわゆる、配列みたいな感じ

記述

角括弧の中で、各要素(オブジェクト)はカンマ(,)で区切られています。

 [ object1, object2, object3, object4 ] 

使い方

辞書型と違ってキーで値を求めることは出来ませんが、要素が順番に並んでいるので、番号を指定して要素を取り出すことができます。
たとえば、以下のhogehogeリストの値を取り出す場合、

hogehoge = [
    object1,
    object2,
    object3,
    object4,
    object5
]

以下のように番号を添え字として指定します。
0から数えるので、以下のように「2」を指定すると、object3 が取り出されます。

hogehoge[2]

さらに、要素が順番に並んでいるという特徴から、X番目~Y番目というように部分的なリストを取り出すこともできます。

hogehoge[1:3]  # [object2, object3, object4]
hogehoge[:3]   # [object1, object2, object3, object4]
hogehoge[1:]   # [object2, object3, object4, object5]
hogehoge[:]    # 全部

このような角括弧[]とコロン(:)を使ったリストの範囲指定を、スライスと言います。
マイナスの値も使えるらしいです。

リストの操作も他に色々な方法がありますが、ひとまずデータを取り出す方法が分かったので、describeの結果から望みの要素を取り出せそうです。

データを取り出す前に

辞書型とリスト型について、大事なことに触れていませんでした。
それぞれのデータ型に含まれる要素は、他の様々な種類のオブジェクトを含むことができます。

文字列や数値といった基本的なオブジェクトを指定することができますし、辞書型やリスト型をネストさせることもできるようです。
(たとえば、辞書オブジェクトの値にリストオブジェクトを入れ、そのリストオブジェクトの中でさらに辞書オブジェクトを並べる...といったことも)

また、文字列型を扱う場合は、ダブルクォーテーション("~")、あるいはシングルクォーテーション('~')で囲う必要があります。
個人的には、キーの値を文字列で指定する場合、クォーテーションで囲うのを忘れがちでエラーを吐いて怒られることが多かったです。(慣れかもしれませんが

目的のデータを取り出してみる

さきほど、インスタンスIDを1個だけ取得した時の以下の記載を理解してみます。

res = client.describe_instances()["Reservations"][0]["Instances"][0]["InstanceId"]

client.describe_instances()

res = client.describe_instances()

EC2インスタンスのメタデータを辞書型で返すメソッドです。詳しい説明は省きますが、以下のような形式でデータが渡されます。(理解しやすいように、色々省略してます)

{'Reservations': [{'Groups': [],
                   'Instances': [{'AmiLaunchIndex': 0,
                                  # ~省略~
                                  'InstanceId': 'i-xxxxxxxxxxxxxxxxx',
                                  # ~省略~
                                  'VpcId': 'vpc-xxxxxxxx'}],
                   'OwnerId': '000000000000',
                   'ReservationId': 'r-xxxxxxxxxxxxxxxxx'},
                  {'Groups': [],
                   'Instances': [{'AmiLaunchIndex': 0,
                                  # ~省略~
                                  'InstanceId': 'i-yyyyyyyyyyyyyyyyy',
                                  # ~省略~
                                  'VpcId': 'vpc-xxxxxxxx'}],
                   'OwnerId': '000000000000',
                   'ReservationId': 'r-xxxxxxxxxxxxxxxxx'},
                  {'Groups': [],
                   'Instances': [{'AmiLaunchIndex': 0,
                                  # ~省略~
                                  'InstanceId': 'i-zzzzzzzzzzzzzzzzz',
                                  # ~省略~
                                  'VpcId': 'vpc-xxxxxxxx'}],
                   'OwnerId': '000000000000',
                   'ReservationId': 'r-xxxxxxxxxxxxxxxxx'},
# 以降省略

ここからどのようにインスタンスIDを取り出しているのか見ていきます。

["Reservations"] の部分

res = client.describe_instances()["Reservations"]

辞書型のメタデータ全体から"Reservations"キー(文字列)に紐づく値を取り出します。メタデータを見ると、'Reservations': に続けて、角括弧[] があるので、紐づく値がリスト型であると分かります。
この時点で、以下のようなオブジェクトが取り出せました。

[{'Groups': [],
  'Instances': [{'AmiLaunchIndex': 0,
                 # ~省略~
                 'InstanceId': 'i-xxxxxxxxxxxxxxxxx',
                 # ~省略~
                 'VpcId': 'vpc-xxxxxxxx'}],
  'OwnerId': '000000000000',
  'ReservationId': 'r-xxxxxxxxxxxxxxxxx'},
 {'Groups': [],
  'Instances': [{'AmiLaunchIndex': 0,
                 # ~省略~
                 'InstanceId': 'i-yyyyyyyyyyyyyyyyy',
                 # ~省略~
                 'VpcId': 'vpc-xxxxxxxx'}],
  'OwnerId': '000000000000',
  'ReservationId': 'r-xxxxxxxxxxxxxxxxx'},
 {'Groups': [],
  'Instances': [{'AmiLaunchIndex': 0,
                 # ~省略~
                 'InstanceId': 'i-zzzzzzzzzzzzzzzzz',
                 # ~省略~
                 'VpcId': 'vpc-xxxxxxxx'}],
  'OwnerId': '000000000000',
  'ReservationId': 'r-xxxxxxxxxxxxxxxxx'},
# 以降省略

1個目の [0] の部分

res = client.describe_instances()["Reservations"][0]

"Reservations"キーで取り出したリストオブジェクトから0番目の値を取り出します。
角括弧の開始 [ に続けて、 波括弧の開始 { がきているので、リストの0番目に格納されているのは、辞書オブジェクトだと分かります。
この時点で、以下のようなオブジェクトが取得できました。

{'Groups': [],
 'Instances': [{'AmiLaunchIndex': 0,
                # ~省略~
                'InstanceId': 'i-xxxxxxxxxxxxxxxxx',
                # ~省略~
                'VpcId': 'vpc-xxxxxxxx'}],
 'OwnerId': '000000000000',
 'ReservationId': 'r-xxxxxxxxxxxxxxxxx'}

このときに、0番目のデータだけを対象にしているので、インスタンスを1個分の情報しか得られない結果となったのでした。

["Instances"] の部分

res = client.describe_instances()["Reservations"][0]["Instances"]

"Instances"キー(文字列)に対応する値(リスト型)を取得します。

[{'AmiLaunchIndex': 0,
  # ~省略~
  'InstanceId': 'i-xxxxxxxxxxxxxxxxx',
  # ~省略~
  'VpcId': 'vpc-xxxxxxxx'}]

2個目の [0] の部分

res = client.describe_instances()["Reservations"][0]["Instances"][0]

"Instances"キーに紐づくリストオブジェクトから、0番目のオブジェクト(辞書型)を
取得します。

{'AmiLaunchIndex': 0,
 # ~省略~
 'InstanceId': 'i-xxxxxxxxxxxxxxxxx',
 # ~省略~
 'VpcId': 'vpc-xxxxxxxx'}

["InstanceId"] の部分

res = client.describe_instances()["Reservations"][0]["Instances"][0]["InstanceId"]

最後に、"InstanceId"キー(文字列)で、値(文字列)を取得しています。
ようやく、インスタンスIDを取得するところまでの部分が理解できました。

最後に

リスト型はそのままfor文で使えるので、以下のようにループと組み合わせれば、

import boto3

def test():
    client = boto3.client('ec2')
    res = client.describe_instances()["Reservations"]

    for i in res:
        print(i["Instances"][0]["InstanceId"])
    return

if __name__ == '__main__':
    test()

インスタンスIDを列挙するようなこともできました。

i-xxxxxxxxxxxxxxxxx
i-yyyyyyyyyyyyyyyyy
i-zzzzzzzzzzzzzzzzz

boto3でAWSの操作をすると、辞書型とリスト型の組み合わせで返されるケースが多く、ちゃんと理解さえすれば、ほかにも応用がしやすいと思います。

主に参考にさせていただいたサイト