複数のAWS APIを組み合わせてこういうのが欲しいんだよ集

# どういうことか

1つのAPIでは欲しい情報として足りない場合に、複数のAPIを実行して結果をマージさせたデータが欲しいことがあります。
そういった関数をいくつか作ったので、事例としてまとめました。
**AWS API単体で取得できてほしいよ** という強い想いで、このようなタイトルの付け方になってます。

# 組み合わせ実例集

個人的によく使っているものです。
いくつか紹介します。言語はPython(boto3)です。
ReadOnlyAccess の IAMロールがアタッチされている AWS Lambda から呼び出すことが可能です。
ローカルで実行する場合は、boto3 API の事前設定（IAMキー設定、リージョン指定、プロファイル指定、等）などは実施済みとします。


## EC2ごとのEBSの容量

これ、標準APIで欲しいと思いませんかぁぁぁ！
残念ながら無いので組み合わせます。

- ```ec2``` : ```describe_instances```  1回
- ```ec2``` : ```describe_volumes```  1回

```python
import boto3

ec2 = boto3.client('ec2')

instances = []
for reserv in ec2.describe_instances()['Reservations']:
  instances.extend(reserv['Instances'])

volumes = ec2.describe_volumes()['Volumes']

for instance in instances:
  instance['EbsSizeList'] = []
  for blockdevice in instance.get('BlockDeviceMappings', [{}]):
    if blockdevice.get('Ebs', {}).get('VolumeId'):
      filtered = list(filter(lambda x: x['VolumeId']==blockdevice['Ebs']['VolumeId'], volumes))
      blockdevice['Ebs'] = filtered[0]
      instance['EbsSizeList'].append(filtered[0]['Size'])
```

まず、EC2インスタンスの一覧は ```describe_instances``` API で取得しますが、結果が ```['Reservations'][*]['Instances'][*]``` の形式となっているものをフラット化しています。
EC2インスタンスごとに ```['BlockDeviceMappings'][*]['Ebs']``` があり、この配下にあるデータを、より情報量の多い ```describe_volumes``` APIで取得した結果で置き換えています。

また、元のデータに ```'EbsSizeList'``` キーを追加して、EBSの数だけ容量(GiB)の数値がリストに含まれるようにしていて、簡単に合計容量が出せるようにしています。

EC2ごとのEBSの合計容量と内訳のGiBを知りたいだけの場合は、

```python
for instance in instances:
    print(instance['InstanceId'], sum(instance['EbsSizeList']), instance['EbsSizeList'])
```

を実行することで、

```text
i-0xxxxxxxxxxxxxxxx 20 [20]
i-0xxxxxxxxxxxxxxxx 225 [25, 100, 100]
i-0xxxxxxxxxxxxxxxx 80 [50, 30]
```

のように取得できます。

この調子で紹介が続きます。

## ELB(ALB・NLB)のタグ

EC2の ```describe_instances``` や RDSの ```describe_db_instances``` は、一覧の中にタグ情報が含まれていますが、他のサービスでは取れないものが多いです。ELBもそうです。
**標準APIに入ってて欲しいです、タグ情報**。

- ```elbv2``` : ```describe_load_balancers```  1回
- ```elbv2``` : ```describe_tags```  N回(ELBの数)

```python
import boto3

elbv2 = boto3.client('elbv2')

elbs = []
for pg in elbv2.get_paginator('describe_load_balancers').paginate():
    elbs.extend(pg['LoadBalancers'])

for elb in elbs:
    key = 'Tags'
    elb.update({
        key : elbv2.describe_tags(ResourceArns=[elb['LoadBalancerArn']])['TagDescriptions'][0][key]
    })
```

ELBのリストに ```'Tags'``` としてタグ情報がマージされます。

対象の数が多い場合を考慮して、ページネーションでデータが欠落しないよう対応します。

参考　[boto3 で pagination が発生する API を良い感じに処理する](https://qiita.com/noid11/items/c2d9f9213e92a0f5ee4b)


## ELB(ALB・NLB)ごとのリスナー

複数のポート番号でリッスンしているELBが混在している場合は、あると便利な組み合わせです。

- ```elbv2``` : ```describe_load_balancers```  1回
- ```elbv2``` : ```describe_listeners```  N回(ELBの数)

```python
import boto3

elbv2 = boto3.client('elbv2')

elbs = []
for pg in elbv2.get_paginator('describe_load_balancers').paginate():
    elbs.extend(pg['LoadBalancers'])

for elb in elbs:
    key = 'Listeners'
    elb.update({
        key : elbv2.describe_listeners(LoadBalancerArn=elb['LoadBalancerArn'])[key]
    })
```

ELBのリストに ```'Listeners'``` としてリスナー情報がマージされます。

さらに ```acm:list_certificates``` と組み合わせることで、ELBと証明書のデータの組み合わせデータも生成することが可能です。

## 専有ホストごとのEC2の割り当て量

EC2で専有ホスト(Dedicated Host)はあまり使う機会が無いかもしれませんが、専有ホストを使っていると取りたい情報です。
AWS API単体では、**稼働している** EC2の割り当て量しか取れないのです。

- ```ec2``` : ```describe_instances```  1回
- ```ec2``` : ```describe_hosts```  1回
- ```ec2``` : ```describe_instance_types```  1回

```python
import boto3

ec2 = boto3.client('ec2')

hosts = ec2.describe_hosts()['Hosts']
instance_reservs = ec2.describe_instances(Filters=[{"Name":"tenancy","Values":["host"]}])['Reservations']
instances = []
for reserv in instance_reservs:
    instances.extend(reserv['Instances'])

instance_types = []
for x in ec2.get_paginator('describe_instance_types').paginate():
    instance_types.extend(x['InstanceTypes'])

for host in hosts:
    sumcpu=0
    instances_on_host = list(filter(lambda x: x['Placement']['HostId']==host['HostId'], instances))
    host['AssignedInstances'] = []
    for instance in instances_on_host:
        filtered = filter(lambda x: x['InstanceType']==instance['InstanceType'], instance_types)
        sumcpu += list(filtered)[0]['VCpuInfo']['DefaultVCpus']
        host['AssignedInstances'].append(instance)

    host['AssignedVCpus'] = sumcpu
```

```'AssignedInstances'``` キーに対象インスタンスのリストがマージされます。
```'AssignedVCpus'``` には、対象インスタンスのvCPU数の合計値が入るようにしています。この数値と```'AvailableCapacity' の 'AvailableVCpus'``` の比率を取ることで、後どれだけ余裕があるかもわかります。


## IAMユーザーごとのIAMグループ

IAMユーザーは複数のIAMグループに含むことができます。よって、IAMユーザーごとにどのIAMグループに入っているかを出力します。

- ```iam``` : ```list_users```  1回
- ```iam``` : ```list_groups_for_user```  N回

```python
import boto3

iam = boto3.client('iam')

users = []
for pg in iam.get_paginator('list_users').paginate():
    users.extend(pg['Users'])

for u in users:
    key = 'Groups'
    u.update({
        key : iam.list_groups_for_user(UserName=u['UserName'])[key]
    })
```

これによって、result にはIAMユーザーのリストが ```'Groups'``` 付きで入ります。

通常の ```list_users``` APIよりも階層構造を減らしてシンプルにしています。
各ユーザーには ```list_groups_for_user``` API で得られた結果の ```'Groups'``` を、そのキー名のままマージしています。

## IAMユーザーごとのIAMポリシー

- ```iam``` : ```list_users```  1回
- ```iam``` : ```list_attached_user_policies```  N回

```python
import boto3

iam = boto3.client('iam')

# IAMユーザー
users = []
for pg in iam.get_paginator('list_users').paginate():
    users.extend(pg['Users'])

for u in users:
    key = 'AttachedPolicies'
    u.update({
        key : iam.list_attached_user_policies(UserName=u['UserName'])[key]
    })
```

IAMユーザーごとのIAMグループとほぼ似たようなコードになります。
ユーザーのリストにマージする際のIAMポリシーのキー名は ```'AttachedPolicies'``` にしています。

## IAMグループごとのIAMポリシー

- ```iam``` : ```list_groups```  1回
- ```iam``` : ```list_attached_group_policies```  N回

```python
import boto3

iam = boto3.client('iam')

groups = []
for pg in iam.get_paginator('list_groups').paginate():
    groups.extend(pg['Groups'])

for g in groups:
    key = 'AttachedPolicies'
    g.update({
        key : iam.list_attached_group_policies(GroupName=g['GroupName'])[key]
    })
```

IAMユーザーグループのリストにポリシーである ```'AttachedPolicies'``` をマージします。

事例集はここまでです。


# マルチスレッドによる高速化

APIの実行回数が多くなる場合、以下のようにマルチスレッドで実行すると高速化する可能性があります。(必ずしも高速化するとは限りません）
上記に示した **IAMユーザーとIAMグループ** のケースで ThreadPoolExecutorを使用した例です。

```python
import boto3
from concurrent.futures import ThreadPoolExecutor

users = []
for pg in iam.get_paginator('list_users').paginate():
    users.extend(pg['Users'])

def task(x):
    session = boto3.Session()
    return session.client('iam').list_groups_for_user(UserName=x['UserName'])

g_user = ThreadPoolExecutor(max_workers=12).map(task, users)

for x,y in zip(users, list(g_user)):
    x.update({'Groups':y['Groups']})
```

```Session()``` を```task()```内で呼んでいる理由は、Sessionはスレッドセーフではないためです。
参考: https://dev.classmethod.jp/articles/python-boto3-session-thread-unsafe/

元々sessionを使っていなくても、```boto3.client()``` が内部的に session() を呼んでいるようでもあります。


# Lambda関数で呼び出すときの注意点

単一APIであればデータ量も実行時間も、特に気にしなくて良かったものが、マージするとどうしてもデータ量が多くなり、時間もかかります。

Lambda関数で実装する場合は、Lambda関数の実行時間を十分延ばしましょう。

Lambda関数でAPI Gatewayなどを経由してWebブラウザーで戻り値を受け取る場合は、REST API の 29秒の制限や、Lambda関数のレスポンスサイズ 6MBの制限があるため、成り立たない組み合わせもあるかもしれません。
実装には注意が必要です。