Boto3に再入門してみた

[Python Advent Calendar](https://qiita.com/advent-calendar/2017/python) の 15 日目のエントリです。

## Boto3

AWS SDK for Pythonなライブラリです。AWSをインフラ基盤としたシステムの運用においては、お世話になる機会が多いと思います。
本エントリでは、Boto3の主要機能の一部である`Low-Level Client`、`Resource`、`Waiter`についてまとめてみました。

## 環境

サンプルコードは以下の環境で実行しています。

```
$ python3 --version
Python 3.6.3

$ pip show boto3 | grep Version
Version: 1.4.8
```

## Low-Level Client

Boto3では、AWSリソースを操作するインターフェースとして低レベルインターフェース、高レベルインターフェースの2種類があります。
Low-Level Clientは名前のとおり低レベルインターフェースにあたるものです。

例として、Low-Level Cientを利用して`<インスタンスID>`のインスタンスに対して、次の操作を行うプログラムを書いてみます。

- インスタンス停止
- インスタンス起動
- インスタンスにアタッチされたPublicDNS Nameを表示

各操作は順番に処理される必要がありますが、メソッドは非同期で処理されるので、前処理の完了を待機するためのスリープ処理をはさんであります。

```low_level_client.py
#!/usr/bin/env python3

from time import sleep

import boto3


client = boto3.client('ec2')
instance_id = '<インスタンスID>'

# EC2インスタンスを停止
client.stop_instances(
    InstanceIds=[instance_id]
)

# インスタンス停止操作の完了を待機
sleep(60)

# EC2インスタンスを起動
client.start_instances(
    InstanceIds=[instance_id]
)

# インスタンス起動操作の完了を待機
sleep(60)

response = client.describe_instances(InstanceIds=[instance_id])
public_dns_name = response['Reservations'][0]['Instances'][0]['PublicDnsName']
print(public_dns_name)
```

Low-Level Clientの各種メソッドを組み合わせることで実装することができましたが、いくつか不便な点もあります。

- メソッド実行時に都度対象リソースのIDを指定する必要がある
- レスポンスがJSON形式なので、必要な情報を取得するのが少々手間

Low-Level Clientで提供されるメソッドはHTTP API と1対1でマッピングするものであるため、このような少し扱いづらい部分もあります。

## Resource

Rerourceは高レベルなインターフェースとしてAWSリソースを抽象化したクラスを提供します。Resourceを利用することでオブジェクト指向なプログラミングで同じような処理を実装することができます。

同じ処理をResourceを使って書き直してみます。

```resource.py
#!/usr/bin/env python3

from time import sleep

import boto3


ec2 = boto3.resource('ec2')
instance = ec2.Instance('i-03322a04df4f4a385')

# EC2インスタンスを停止
instance.stop()

# インスタンス停止操作の完了を待機
sleep(60)

# EC2インスタンスを起動
instance.start()

# インスタンス起動操作の完了を待機
sleep(60)

public_dns_name = instance.public_dns_name
print(public_dns_name)
```

`instance`がEC2インスタンスを抽象化したオブジェクトです。Low-Level Clientを使った場合より直感的にインスタンスの操作や属性を記述できていると思います。

なお、ResouceはLow-Level Clientを完全に置き換えたり、すべてのAWSリソースに用意されているわけではありませんので、注意してください。

## Waiter

AWSリソースの操作を非同期に行うAPIを利用した場合、その処理が完了するまで次の処理が正常に行えないことがあります。
例として作成しているプログラムの場合では、インスタンス起動処理はインスタンスの停止が完了してからでないと行なえません。

先ほどまではインスタンスのステータスが変わるのを`time.sleep`を利用して待機していましたが、`time.slee`の場合は実際のインスタンスのステータス推移を見ているわけではなく指定した秒数を待機しているだけですので、問題が出るケースもあります。
このようなAWSリソースのステータス推移を待機する用途としては、Waiterを利用するのがスマートです。WaiterはAWSリソースが指定したステータスになるまでポーリングしてくれる機能です。

`sleep`の部分をWaiterに置き換えると次のようになります。

```waiter.py
#!/usr/bin/env python3

import boto3


ec2 = boto3.resource('ec2')
instance = ec2.Instance('<インスタンスID>')

# EC2インスタンスを停止
instance.stop()

# インスタンス停止操作の完了を待機
instance.wait_until_stopped()

# EC2インスタンスを起動
instance.start()

# インスタンス起動操作の完了を待機
instance.wait_until_running()

public_dns_name = instance.public_dns_name
print(public_dns_name)
```

`EC2.Instance`クラスにはWaiterを利用できるメソッドが用意されていたのでそれを使いました。
Waiterを利用できるメソッドがResourceに用意されていない場合もありますので、その場合はLow-Level ClientでWaiterを定義してから使います。以下はEBSの例です。

```ebs_waiter.py
waiter = client.get_waiter('volume_in_use')
waiter.wait('<ボリュームID>')
```

## まとめ

Boto3の主要機能の一部である`Low-Level Client`、`Resource`、`Waiter`について紹介しました。
本エントリがプログラムによるインフラ運用の改善の参考になれば幸いです。

## 参考文献

- https://aws.amazon.com/jp/sdk-for-python/
- http://boto3.readthedocs.io/en/latest/index.html
- https://github.com/boto/boto3