Python boto3 でAWSを自在に操ろう ~入門編~

## Boto3 とは :dolphin: 
AWS (Amazon Web Services) を Python から操作するためのライブラリの名称です。
S3 などのサービス操作から EC2 や VPC といったインフラの設定まで幅広く扱うことが出来ます。
Boto3 は AWS が公式で提供しているライブラリのため、APIとして提供している機能をほぼ Python から使えるようになっています。

今回はこの Boto3 の使い方と活用例を紹介したいと思います。
ちなみに Botoという名称は ボート :sailboat: ではなくイルカ :dolphin: の名前から来ているそうです。
https://reboooot.net/post/why-boto/
 
## セットアップ

### Boto3 のインストール

```
pip install boto3
```
pip からインストールすることができます。
Python の 2 系は 2.6.5 から、3 系は 3.3 からサポートされてます。

### AWS の API キーの用意

Boto3 から AWS を操作するためには API キーも用意する必要があります。
https://aws.amazon.com/jp/developers/access-keys/
などを参考に

  * **アクセスキー ID** (access_key_id) 
  * **シークレットアクセスキー** (secret_access_key) 

の2つの情報を取得してください。
API キーがあれば誰でも許可した操作を出来てしまうので、鍵の管理や権限設定は厳重に行いましょう。
EC2 のインスタンスを生成できるキーが流出して、何百万も請求が来てしまったみたいなケースもままあるのでご注意を…。

* [初心者がAWSでミスって不正利用されて$6,000請求、泣きそうになったお話。](https://qiita.com/mochizukikotaro/items/a0e98ff0063a77e7b694)
* [AWS で不正アクセスされて凄い額の請求が来ていた件](http://d.hatena.ne.jp/yoya/20150404/aws)

あとで紹介しますが、AWS 上のリソースからだと API キーがなくても IAM Role の権限を利用してアクセスできるので、より安全に利用することが出来ます。


## 初期化 

準備ができたら早速 Python から使ってみましょう。

```python3
import boto3

s3 = boto3.client('s3',
        aws_access_key_id=[アクセスキー ID],
        aws_secret_access_key=[シークレットアクセスキー] ,
        region_name='ap-northeast-1'
)

```
このようにすることで S3 へアクセスするオブジェクトを取得できます。
`boto3.client` の最初の引数には、使いたいサービスの名前を文字列で渡してあげています。
DynamoDB なら `dynamodb`、EC2なら `ec2`  みたいな感じですね。
使えるサービスや対応表はドキュメントを参照してください。

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/index.html

オプションとしてキーワード引数で AWS の API キーなどを渡すこともできます。
ただこうやって API キーを渡すのは、機密情報をソースコードに埋め込むことになるので好ましくありません。
間違って公開レポジトリに置いてしまったという自体を避けるためにも、別の方法でキーを渡すことを考えましょう。

### 環境変数を使う
boto3 は環境変数を読み取ることが可能です。

`AWS_ACCESS_KEY_ID`
`AWS_SECRET_ACCESS_KEY`

という決められた環境変数に APIキーを入れておけば

```python3
s3 = boto3.client('s3', region_name='ap-northeast-1')
```
のように呼び出すことができるようになります。
コードと設定の分離もできるので便利ですね。

ちなみに リージョンも `AWS_DEFAULT_REGION`  という名前で環境変数にセットすることが可能です。

```python3
s3 = boto3.client('s3')
```
かなりシンプルになりました。

### AWS のリソース上から使う
ここまでは API キーを直接扱っていましたが、AWS のリソース上からであれば API キーを意識せずに使うことができます。
つまり EC2 や Lambda から boto3 を呼び出す場合には環境変数を使った場合と同じように

```python3
s3 = boto3.client('s3')
```
として利用することが出来ます。
この場合、boto3 がアクセスできる範囲は EC2 や Lambda に Role として設定した権限内だけになります。
API キーを扱わなくてすむので安心して使えますね。

なので、Boto3 を呼び出すときは、上記のような形で書いておき、**開発環境から呼び出す場合は環境変数で、AWS 上で動かす場合は Role を割り当てて使う**というようにするのが良さそうです。

## Boto3 の基本
### client と resource
S3 を利用する場合、

```python3
s3 = boto3.resource('s3')
```
のように初期化することも可能です。
client と resource どう違うのかというと、使い方が違います。

```python3
s3 = boto3.client('s3')
# バケット一覧を取得
s3.list_buckets()
obj = client.get_object(Bucket='test_bucket', Key='test.text')
print(obj['body'].read())
```

client の方は AWS API をそのままラップしたような形で呼び出せるようになってます。


```python3
s3 = boto3.resource('s3')
bucket = s3.Bucket('test_bucket')
obj = bucket.Object('test.text').get()
print(obj['body'].read())
```

一方 resource はよりオブジェクト指向っぽく書けるよう用意された高レベルなライブラリです。
resource は必ずしもすべてのサービスで用意されているわけではなくて、S3 などの頻出サービスでのみ利用できます。
目的に応じて resource が使えないか検討してみるとよいでしょう。

```python3
s3_resource = boto3.resource('s3')
s3_client = s3_resource.meta.client
```
ちなみに resource で初期化した場合、このように meta オブジェクトの中から client オブジェクトを取得できます。
初期化複数回したくないときなどに便利です。

### Waiter 
Boto3 には Waiter と呼ばれるリソースが整うまで待ってくれる機能もあるので紹介します。
S3 とかだとすぐ作成されるのであまり使うタイミングはないかもしれませんが、EC2 のインスタンス立ち上げなど時間のかかる操作をする場合役に立ちます。

```python
s3 = boto3.client('s3')

# object ができるまで待機する waiter を作成
waiter = s3.get_waiter('object_exists')

# NewObject.txt が作成されるまで sleep する
waiter.wait(Bucket='test_bucket', Key='NewObject.txt')
```

## 実際に使ってみる
boto3 の基本的な使い方は紹介できたので、実践的に試してみましょう。

### EC2 のインスタンスを起動して終了する
練習として 「**EC2 のインスタンスを起動、起動後ローカルIPを取得して、最後に終了する**」みたいなことを考えてみます。
これが出来れば任意のタイミングでプログラムからインスタンスを起動して処理をさせるということも可能になってきます。


まずは Boto3 で利用可能なサービスを見てみましょう。

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/index.html

こちらに利用可能なものがリストアップされています。今回は EC2 を起動したいので、「EC2」の項目を探してみます。

|![image.png](https://qiita-image-store.s3.amazonaws.com/0/25168/dc55d851-c26c-2cea-02f6-7cf1c7d912ff.png)|
|:-:|

A-Z順なのですぐ見つかりますね。
EC2 の操作では Client だけでなく Service Resource(=先程紹介した resource) も利用できることが分かります。

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#service-resource
 Service Resource のページに飛ぶと、利用可能なアクションが列挙されてます。

|![image.png](https://qiita-image-store.s3.amazonaws.com/0/25168/74b777f5-8bd9-9759-7d86-9710445b17ad.png)|
|:-:|

Instance を建てる際には `create_instances()` というのが利用できそうです。ドキュメント見てみましょう。

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#EC2.ServiceResource.create_instances





|![image.png](https://qiita-image-store.s3.amazonaws.com/0/25168/5300e284-5dee-a8bb-e81e-18a4d013e24f.png)|
|:-:|

アクションのドキュメントには、そのアクションの概要とともに呼び出し方が書かれています。
引数の名前と、その取れる値( or 型)が書いてあるんだなというのがなんとなく分かると思います。
ただ、これをみて「よしわかった、実装しよう！」となるにはハードルが高いです。
単純に引数多すぎですね。EC2 に精通していないとすぐにはパラメータ埋められなそうです。

そこでちょっとお行儀が悪いですが、パラメータなしで呼び出してみることにします。

> :warning: デフォルト値でコマンドを試す場合は費用やセキュリティ的に問題がないかを確認の上、実行してください。
今回のケースで言うと、`m1.small` タイプのインスタンスが立ち上がるので起動しっぱなしで放置しておくと費用が高くつきます。

```python3
>>> ec2_resouce = boto3.resouce('ec2')
>>> ec2_resouce.create_instances()

Traceback (most recent call last):
  File "<input>", line 1, in <module>
    ec2.create_instances()
  File "/usr/local/lib/python3.6/site-packages/boto3/resources/factory.py", line 520, in do_action
    response = action(self, *args, **kwargs)
  File "/usr/local/lib/python3.6/site-packages/boto3/resources/action.py", line 83, in __call__
    response = getattr(parent.meta.client, operation_name)(**params)
  File "/usr/local/lib/python3.6/site-packages/botocore/client.py", line 312, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/usr/local/lib/python3.6/site-packages/botocore/client.py", line 579, in _make_api_call
    api_params, operation_model, context=request_context)
  File "/usr/local/lib/python3.6/site-packages/botocore/client.py", line 634, in _convert_to_request_dict
    api_params, operation_model)
  File "/usr/local/lib/python3.6/site-packages/botocore/validate.py", line 291, in serialize_to_request
    raise ParamValidationError(report=report.generate_report())
botocore.exceptions.ParamValidationError: Parameter validation failed:
Missing required parameter in input: "ImageId"
Missing required parameter in input: "MaxCount"
Missing required parameter in input: "MinCount"
```

当然引数が足りないので怒られてしまいますが、エラーを見てみると `ImageId` `MaxCount` `MinCount` が足りないと言われてますね。この 3 つを埋めるだけならすぐ調べられそうです。

* ImageId (string): The ID of the AMI, which you can get by calling DescribeImages .
* MinCount  (integer): The minimum number of instances to launch.
* MaxCount  (integer): The maximum number of instances to launch. 

とのことなので、適当な ImageID をみつけて、数は 1 で起動してみましょう。

ec2.describe_images() でも ImageId 見つけられますが、検索条件を指定しないとレスポンスがとてつもなく大きいので AWS コンソールから探してくるほうが無難です。今回は Amazon Linux の `ami-0a2de1c3b415889d2` を指定します。

```python3
# Amazon Linux 2
>>> instances = ec2.create_instances(ImageId='ami-0a2de1c3b415889d2', MaxCount=1, MinCount=1)
>>> print(instances)

[ec2.Instance(id='i-XXXXXXXXXX')]
```

エラーなく実行できました。起動したインスタンスが配列で返ってきてるみたいですね。

```python3
>>> instance = instance[0]

>>> dir(instances[0])
[ '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', 
 '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__',
 '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_id', 'ami_launch_index', 'architecture', 
 'attach_classic_link_vpc', 'attach_volume', 'block_device_mappings', 'capacity_reservation_id', 
 'capacity_reservation_specification', 'classic_address', 'client_token', 'console_output', 'cpu_options', 'create_image',
 'create_tags', 'delete_tags', 'describe_attribute', 'detach_classic_link_vpc', 'detach_volume', 'ebs_optimized', 
 'elastic_gpu_associations', 'elastic_inference_accelerator_associations', 'ena_support', 'get_available_subresources', 
 'hibernation_options', 'hypervisor', 'iam_instance_profile', 'id', 'image', 'image_id', 'instance_id', 'instance_lifecycle', 
 'instance_type', 'kernel_id', 'key_name', 'key_pair', 'launch_time', 'licenses', 'load', 'meta', 'modify_attribute', 'monitor', 
 'monitoring', 'network_interfaces', 'network_interfaces_attribute', 'password_data', 'placement', 'placement_group', 
 'platform', 'private_dns_name', 'private_ip_address', 'product_codes', 'public_dns_name', 'public_ip_address',
 'ramdisk_id', 'reboot', 'reload', 'report_status', 'reset_attribute', 'reset_kernel', 'reset_ramdisk', 'reset_source_dest_check', 
 'root_device_name', 'root_device_type', 'security_groups', 'source_dest_check', 'spot_instance_request_id', 
 'sriov_net_support', 'start', 'state', 'state_reason', 'state_transition_reason', 'stop', 'subnet', 'subnet_id', 'tags', 'terminate',
 'unmonitor', 'virtualization_type', 'volumes', 'vpc', 'vpc_addresses', 'vpc_id', 'wait_until_exists', 'wait_until_running',
 'wait_until_stopped', 'wait_until_terminated']
```
1 つめのインスタンスを取り出し、呼び出せるメソッドを dir 関数で見てみましょう。
インスタンスの情報を取得するには `describe_attribute`, `network_interfaces_attribute`, `wait_until_running` このあたりが利用できそうですね。



```python3
# 起動まで待つ
>>> instance.wait_until_running()  # 〜30秒くらい


# インスタンスの属性を見る
>>> instance.instance.describe_attribute()

  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.6/site-packages/boto3/resources/factory.py", line 520, in do_action
    response = action(self, *args, **kwargs)
  File "/usr/local/lib/python3.6/site-packages/boto3/resources/action.py", line 83, in __call__
    response = getattr(parent.meta.client, operation_name)(**params)
  File "/usr/local/lib/python3.6/site-packages/botocore/client.py", line 320, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/usr/local/lib/python3.6/site-packages/botocore/client.py", line 597, in _make_api_call
    api_params, operation_model, context=request_context)
  File "/usr/local/lib/python3.6/site-packages/botocore/client.py", line 633, in _convert_to_request_dict
    api_params, operation_model)
  File "/usr/local/lib/python3.6/site-packages/botocore/validate.py", line 297, in serialize_to_request
    raise ParamValidationError(report=report.generate_report())
botocore.exceptions.ParamValidationError: Parameter validation failed:
Missing required parameter in input: "Attribute"


# Attribute をドキュメントみて指定してみる
>>> instance.describe_attribute(Attribute='instanceType')

{'InstanceId': 'i-XXXXXXXXXX', 'InstanceType': {'Value': 'm1.small'}, 'ResponseMetadata': {'RequestId': 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXXXXX', 'HTTPStatusCode': 200, 'HTTPHeaders': {'content-type': 'text/xml;charset=UTF-8', 'content-length': '344', 'date': 'Mon, 10 Dec 2018 04:57:05 GMT', 'server': 'AmazonEC2'}, 'RetryAttempts': 0}}


# 今度は IP などを取得してみる
>>> instance.network_interfaces_attribute()

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'list' object is not callable


# メソッドではなかったので、カッコを取ってもう一度
>>> instance.network_interfaces_attribute

[{'Association': {'IpOwnerId': 'amazon', 'PublicDnsName': 'ec2-XXX-XXX-XXX-XXX.ap-northeast-1.compute.amazonaws.com', 'PublicIp': 'XXX.XXX.XXX.XXX'}, 'Attachment': {'AttachTime': datetime.datetime(2018, 12, 10, 4, 51, 41, tzinfo=tzutc()), 'AttachmentId': 'eni-attach-XXXXXXXXXXXX', 'DeleteOnTermination': True, 'DeviceIndex': 0, 'Status': 'attached'}, 'Description': '', 'Groups': [{'GroupName': 'default', 'GroupId': 'XXXXXXXXXXX'}], 'Ipv6Addresses': [], 'MacAddress': 'XX:XX:XX:XX:XX:XX', 'NetworkInterfaceId': 'eni-XXXXXXXXXXXXX', 'OwnerId': 'XXXXXXXXXXXX', 'PrivateDnsName': 'ip-XXX-XXX-XXX-XXX.ap-northeast-1.compute.internal', 'PrivateIpAddress': 'XXX.XXX.XXX.XXX', 'PrivateIpAddresses': [{'Association': {'IpOwnerId': 'amazon', 'PublicDnsName': 'ec2-XXX-XXX-XXX-XXX.ap-northeast-1.compute.amazonaws.com', 'PublicIp': 'XXX-XXX-XXX-XXX'}, 'Primary': True, 'PrivateDnsName': 'ip-XXX-XXX-XXX-XXX.ap-northeast-1.compute.internal', 'PrivateIpAddress': 'XXX.XXX.XXX.XXX'}], 'SourceDestCheck': True, 'Status': 'in-use', 'SubnetId': 'subnet-XXXXXXXX', 'VpcId': 'vpc-XXXXXXXX'}]
```

場当たり的なやり方ですが、無事目的の IP アドレスがちゃんと取れましたね。
最後にお片付けしましょう。
先程のリストに `termiate` `wait_until_terminated` があったのでそちらが使えそうです。

```python3
# インスタンスを終了させる
>>> instance.terminate()

{'StoppingInstances': [{'CurrentState': {'Code': 64, 'Name': 'shutting-down'}, 'InstanceId': 'i-XXXXXXXXXX', 'PreviousState': {'Code': 16, 'Name': 'running'}}], 'ResponseMetadata': {'RequestId': 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXXXXX', 'HTTPStatusCode': 200, 'HTTPHeaders': {'content-type': 'text/xml;charset=UTF-8', 'content-length': '579', 'date': 'Mon, 10 Dec 2018 00:58:41 GMT', 'server': 'AmazonEC2'}, 'RetryAttempts': 0}}


# 終了まで待つ
>>> instance.wait_until_terminated()
```
終了出来たみたいです。
無駄なインスタンスが残っていると怖いので、念の為 AWS コンソールで確認しておきましょう。

|![image.png](https://qiita-image-store.s3.amazonaws.com/0/25168/10f132de-5e74-9a9b-765d-8c971c782c90.png)|
|:-:|

ちゃんと削除されてますね。
「EC2 のインスタンスを起動、起動後ローカルIPを取得して、最後に終了する」までを Boto3 で自動化することができました。
他の AWS サービスを利用するときもこのようにトライアンドエラーで進めてく事ができると思います。(作られるリソースが問題ないかは別途確認しましょう。)

## おわりに
Boto3 から AWS のリソースが触れるようになると今まで手動で行っていた作業を Python で自動化していくことが可能です。
Python で動くということは AWS Lambda にこのコードを載せて動かすことも出来ます。
また Lambda をスケジュールに従って動かすための CloudWatch Event というのも用意されています。
つまり今回紹介した内容だけでも EC2 のスケジュール起動・終了はできちゃうわけですね。
Python を接着剤代わりに他のサービスを組み合わせていくことでより高度なことができるようになります。
身の回りのインフラタスクを自動化して快適な AWS エンジニアリングをしていきましょう！