【AWS】用語を整理しながら学ぶAWS AWS SDK for Python (Boto3)

## はじめに

この記事では AWSが提供する`AWS SDK for Python (Boto3)`（以下、Boto3）を学習していく記事です。主な内容としては実践したときのメモを中心に書きます。（忘れやすいことなど）
誤りなどがあれば書き直していく予定です。

## AWS SDKとは

AWSが提供するAWSでリソースを操作し、構築に利用するツールです。
AWSの公式サイトには下記のように表現されています。

> AWS での構築ツール
> AWS でアプリケーションを開発および管理するためのツール
> 選択したプログラミング言語を使用して、AWS でアプリケーションを簡単に開発する
> Python 固有の API と有益なライブラリを使って、アプリケーションを開発する

[参考](https://aws.amazon.com/jp/developer/tools/)

サービスではなく、ツールの一つです。そもそもSDKとはなんでしょうか。

### 補足：SDKとは（SDKの定義）

SDKとは何だったかここではCircleCIさんのサイトから引用して見ていきましょう。

> SDKとは「Software Development Kit」の略称で、Webサイトやアプリケーションの開発に必要なツール一式が含まれています。
> SDK ツールには、開発者が使用して独自のアプリに統合できるライブラリ、ドキュメント、コード サンプル、プロセス、ガイドなど、さまざまなものが含まれます。

[SDK (ソフトウェア開発キット）とは？ CircleCI](https://circleci.com/ja/blog/what-is-sdk/)

つまりはSDKを使うとアプケーションをゼロから構築するのではなく、用意されたプログラムを呼び出し、まとめあげて一つのアプリケーションを構築できます。

ここでSDKを使う際に呼び出すプログラムのことを
ライブラリもしくはソフトウェアパッケージなどと呼んだりします。言語によって呼び方は異なるので各言語のお作法に従っていただければと思います。

今回はPythonを利用するのでモジュール、パッケージ、ライブラリを主に利用します。

## 話を戻してAWS SDKとは

つまりはAWS SDKはプログラムからAWSのサービスにアクセスするためのSDKです。

たとえば、以下のようなアクションがプログラミングでできます。

- EC2を起動、停止、終了
- VPCの作成
- セキュリティグループの作成
- アカウントIDの取得

他のサービスも同様にさまざまな操作ができます。お気づきかと思いますが、これらの動作はAWSマネジメントコンソールでも可能です。

AWSマネジメントコンソールによるアクセスでもAWS SDKによるアクセスでも同じ操作ができます。
つまりはマネジメントコンソールの操作はプログラミングで実現できます。

### AWS SDKとマネジメントコンソールの違いや関係

同じことができる両者の違いは何でしょうか。
マネジメントコンソールでもプログラミングでも同じ操作ができるのはなぜでしょうか。

結論を述べると、両者もAWSが提供するAPIにアクセスしています。
この記事では解説していないですが、AWS CloudFormationでもAWSが提供するAPIを利用しています。

つまり、両者のやっていることは画面から操作するかプログラミングで操作するかの違いしかありません。

しかし、AWS SDKにはプログラミング毎にさまざまなタイプがあります。
今回はBoto3を例に挙げていきたいと思います。

## AWS SDK for Python(Boto3)とは

Boto3はAWS SDK提供するPython用のAWS SDKです。いくつか具体例を挙げていきます。

## ソースコードの例

AWSを操作するツールなので具体例がないとわかりにくいと思います。ここではいくつかサンプルコードを見ていきましょう。

### アカウントIDを取得する

まずはAWSアカウントIDです。12桁のアカウントIDを取得する関数を作成すると下記の通りになります。

```python
import boto3


def get_aws_account_id():
    """
    AWSのアカウントIDを返します。
    :param なし
    :return: AWSのアカウントを示す12桁のID
    """
    sts = boto3.client('sts')
    id_info = sts.get_caller_identity()
    return id_info['Account']
```

AWS STSサービスを呼び出してアカウントIDを取得します。

### Glueのデータベースを操作する

AWS Glueのメタデータを取得します。

```python
import boto3


def get_glue_databases(session):
    """
    Glueのデータベース一覧を取得します。
    Args:
        :param session: セッション
    Returns:
        :return: glue 上で保存されているデータベース情報を保存したリスト
    """
    database_list = session.client("glue").get_databases()
    if "NextToken" in database_list:
        next_token = database_list["NextToken"]
        has_next_token = True

        while has_next_token:
            next_data = session.client("glue").get_databases(NextToken=next_token)
            database_list["DatabaseList"].extend(next_data["DatabaseList"])

            if "NextToken" in next_data:
                next_token = next_data["NextToken"]
            else:
                has_next_token = False

    return database_list


def get_glue_tables(session, database_name):
    """
    Glueのテーブル一覧を取得します。
    Args:
        :param session: セッション
        :param database_name: データベース名
    Returns:
        :return: glue 上で保存されているテーブル情報を保存したリスト
    """
    table_list = session.client("glue").get_tables(DatabaseName=database_name)
    if "NextToken" in table_list:
        next_token = table_list["NextToken"]
        has_next_token = True

        while has_next_token:
            next_data = session.client("glue").get_tables(NextToken=next_token)
            table_list["DatabaseList"].extend(next_data["DatabaseList"])

            if "NextToken" in next_data:
                next_token = next_data["NextToken"]
            else:
                has_next_token = False

    return table_list


if __name__ == "__main__":
    session = boto3.Session(
        profile_name='data',
        region_name='us-east-1'
    )
    database_list = get_glue_databases(session=session)
    databases = database_list.get('DatabaseList')
    table_metadata = []

    for database in databases:
        tables = get_glue_tables(
            session=session,
            database_name=database.get('Name')
        )
        table_list = tables.get('TableList')
        for table_info in table_list:
            # print(table_info.get('StorageDescriptor'))
            table_metadata.append({
                'DatabaseName': table_info.get('DatabaseName'),
                'TableName': table_info.get('Name'),
                'Columns': table_info.get('StorageDescriptor').get('Columns')
            })

    print(table_metadata)
```

### Amazon Bedrockを操作する

最後にAmazon Bedrockを操作するコードです。LINE botのコードからの抜粋ですが、`bedrock`を呼び出しているのがわかると思います。

```python
def lambda_handler(event, context):

    print(f"boto3 version: {boto3.__version__}")
    body = json.loads(event['body'])

    # Webhookの接続確認用
    if len(body['events']) == 0:
        return {
            'statusCode': 200,
            'body': ''
        }

    # bodyからevent を取得する
    body_event = body['events'][0]
    reply_token = body_event['replyToken']

    message_text = body_event["message"].get("text")

    message_type = body_event['message']['type']
    event_type = body_event['type']

    if event_type == 'message':
        if message_type == 'text':
            session = boto3.Session(region_name='us-east-1')
            bedrock = session.client('bedrock')

            body = json.dumps({
                "prompt": "\n\nHuman:"+message_text+"\n\nAssistant:",
                "max_tokens_to_sample": 300,
                "temperature": 0.1,
                "top_p": 0.9,
            })

            modelId = 'anthropic.claude-v2'
            accept = 'application/json'
            contentType = 'application/json'

            response = bedrock.invoke_model(body=body, modelId=modelId, accept=accept, contentType=contentType)
            response_body = json.loads(response.get('body').read())

            # text
            reply_message_text = response_body.get('completion')
```

## いくつかコードを見ていきたけど。。。

書き方や読み方のコツを知りたい！！

### 書き方（基本）

まずは、`import boto3`から書き始めます。次に`Session`を作成します。
`Session`作成時ですが、ご自分の端末で実行している場合、`profile_name`を指定するようにしてください。

さまざまな記事を読むとシークレットアクセスキーとアクセスキーを指定した書き方が見られますが、GitHub等でソースコードを管理している場合はソースコードと一緒にリポジトリにプッシュしてしまう可能性があるので機密情報はソースコードに書かないようにしましょう。

次に`region_name`ですが、ここには取得先のリージョン名を書いてください。取得したいリソース（サービス）情報が`us-east-1`なら`region_name`は`us-east-1`と書いてください。

```python
import boto3


if __name__ == "__main__":
    session = boto3.Session(
            profile_name='data',
            region_name='us-east-1'
        )
```

### clientかresourceか

boto3には主にclient インターフェイスとresource インターフェイスの２つがあります。
client インターフェイスが使えれば、だいたいのことはできます。

両者の違いは実装されているメソッドの違いです。clientはアクセス元がどんな操作をするかを決めますが、resourceは指定されたリソースにアクセスできるようにします。

clientの場合

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

resourceの場合

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

前述のコードからclientインターフェイスを使用する例を示します。

```python
import boto3

session = boto3.Session(region_name='us-east-1')
bedrock = session.client('bedrock')

body = json.dumps({
    "prompt": "\n\nHuman:"+message_text+"\n\nAssistant:",
    "max_tokens_to_sample": 300,
    "temperature": 0.1,
    "top_p": 0.9,
})

modelId = 'anthropic.claude-v2'
accept = 'application/json'
contentType = 'application/json'

response = bedrock.invoke_model(body=body, modelId=modelId, accept=accept, contentType=contentType)
```

### バージョンを限定する

boto3ではバージョンによってできることとできないことがあります。
具体的な例ではバージョンによって操作できるサービスの数やメソッドが異なるケースがあります。

以下の例ではboto3のバージョンを表示しつつ、特定のバージョンでのみスクリプトが動くようになっています。

```python
import boto3


# entry point
if __name__ == '__main__':
    # バージョン表示
    print(f'boto3 version: {boto3.__version__}')
    # print(f'botocore version: {botocore.__version__}')

    boto3_ver = f'boto3 version: {boto3.__version__}'

    if boto3_ver == "boto3 version: 1.17.32":
        print("session start")
```

## 読み方

ここまでで書き方がよくわかったと思います。boto3にはサービスそれぞれに対してモジュールが作成されており、そのモジュールを操作するように複数のメソッドが伸びています。

サービスの数だけモジュールが存在しますのでもちろんメソッドもたくさんあります。
ではどのようにしてboto3 を読み進めていけば良いのでしょうか。

実はコツがあります。一緒に見ていきましょう。

### ココがポイント！英語で読んでいく

各サービスのメソッドには命名ルールがあります。それさえ覚えることができれば、あとは慣れの問題です。

具体的にはどんなメソッドも以下のような単語から始まり、だいたいは単語のイメージ通りの動作をします。

- create
    - 作成
- delete
    - 削除
- flush
    - 消去
- update
    - 更新
- describe
    - 表示
- get
    - 抽出
- attach
    - 取り付け
- associate
    - 関連付け

他にもさまざまですが、どのようなメソッドか想像がつかない場合は英単語から想像するとよいでしょう。また、メソッドによってはドライランの機能もあります。

## ページングとNextToken

さまざまなメソッドを持つboto3ですが、実は万能ではなく一度のリクエストですべてのデータを取れないケースがあります。
たとえば、リソースが多い場合においては分割して取得する必要があります。

分割して取得する場合は取得件数の最大値を超えた時にNextTokenが発行されますのでNextTokenを使って再度、リクエストを送信します。

塊毎にちょっとずつデータを取っていく、この操作をページングと言います。150件あって100件までは一度に取得できる上限であれば、2回のリクエストが必要です。

ちなみに前述のコードでもページングにまつわる実装をしています。

```python
    if "NextToken" in table_list:
        next_token = table_list["NextToken"]
        has_next_token = True

        while has_next_token:
            next_data = session.client("glue").get_tables(NextToken=next_token)
            table_list["DatabaseList"].extend(next_data["DatabaseList"])

            if "NextToken" in next_data:
                next_token = next_data["NextToken"]
            else:
                has_next_token = False
```

## boto3のドキュメントを読むには

他にもいくつか勘所がありますが、ドキュメントを読めるようになった方がいささか早いかもしれません。

boto3ドキュメントを読むコツとしては個人的に以下のやり方が良いと思います。

- 使いたいサービスを探す
    - たとえば、VPCならEC2のページを開く
- 英単語からやりたいことを想像する
    - ネットワークACLを作りたいなら`create`
- 単語を検索する
    - `create`から始まるものを文書内から検索する
- メソッドの詳細を追う
    - Request Syntaxを見る
    - parametersを見て、requiredになっているものメモする
    - Response SyntaxとResponse Structureをチェックする
    - 実際に動かしてみる

ページによってはサンプルプログラムもあるので参考にしましょう。

## まとめ

AWS SDK for Python、いわゆるboto3を使ってAWSを操作できることがわかりました。

AWS SDKでできることはAWSマネジメントコンソールでもできますが、オペレーションの自動化やパラメータの取得などはマネジメントコンソールではできませんので積極的に活用しましょう。