ansibleのaws_rds inventory pluginが大量のClusterを取得する場合バグりそうなので、カスタムプラグインを作ってみた

# 困ったこと

- pagenationがないので、100以上クラスターがあると取得できなくなる場合が生まれる
  - https://github.com/ansible-collections/amazon.aws/blob/9.3.0/plugins/inventory/aws_rds.py#L161-L163
- describe_db_instance / describe_db_clusterのフィルターが完全一致しか対応していないので、部分一致の場合毎回全件取得しないといけない。そのためインベントリ取得のオーバヘッドが大きい
  - タグでのフィルターもできない

https://qiita.com/pitan/items/00539606f7733db1602c

https://github.com/ansible-collections/amazon.aws/blob/9.3.0/plugins/inventory/aws_rds.py

これを参考に↓を使ってリソースを絞り込んでからdescribe_db_instance / describe_db_clusterを実行する

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/resourcegroupstaggingapi/client/get_resources.html

## はまったところ

- 追加の引数を入れた場合にDOCUMENTATIONに書かないと読み込みできない
  - https://docs.ansible.com/ansible/latest/dev_guide/developing_plugins.html#developing-plugins
- 設定ファイル名をコード内で制御しているので変えないといけない
　- https://github.com/ansible-collections/amazon.aws/blob/9.3.0/plugins/inventory/aws_rds.py#L167-L168
- ansible側から渡したタグフィルター情報をboto3側に渡す形式にするための変換ロジックの実装が必要
  - もともと、そういった用途にansible_dict_to_boto3_filter_listのようなの関数が用意されているが、resourcegroupstraggingapiでのタグフィルターで渡すデータ形式が異なるため使用できない

## code

- ansible: 2.18.4
- collection amazon.aws: 9.3.0

aws_rds_from_resource_tag.py  

```py
# -*- coding: utf-8 -*-

# Copyright (c) 2018 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# original file is https://github.com/ansible-collections/amazon.aws/blob/9.3.0/plugins/inventory/aws_rds.py

DOCUMENTATION = r"""
name: aws_rds
short_description: RDS instance inventory source
description:
  - Get instances and clusters from Amazon Web Services RDS.
  - Uses a YAML configuration file that ends with aws_rds.(yml|yaml).
options:
  resource_tag_filters:
    description: A dictionary of filter value pairs. Available filters are listed here
        U(https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/resourcegroupstaggingapi/client/get_resources.html)
    default: {}
  regions:
    description:
      - A list of regions in which to describe RDS instances and clusters. Available regions are listed here
        U(https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html).
    default: []
  filters:
    description:
      - A dictionary of filter value pairs. Available filters are listed here
        U(https://docs.aws.amazon.com/cli/latest/reference/rds/describe-db-instances.html#options). If you filter by
        db-cluster-id and I(include_clusters) is True it will apply to clusters as well.
    default: {}
  strict_permissions:
    description:
      - By default if an AccessDenied exception is encountered this plugin will fail. You can set strict_permissions to
        False in the inventory config file which will allow the restrictions to be gracefully skipped.
    type: bool
    default: true
  include_clusters:
    description: Whether or not to query for Aurora clusters as well as instances.
    type: bool
    default: false
  statuses:
    description: A list of desired states for instances/clusters to be added to inventory. Set to ['all'] as a shorthand to find everything.
    type: list
    elements: str
    default:
      - creating
      - available
  hostvars_prefix:
    description:
      - The prefix for host variables names coming from AWS.
    type: str
    version_added: 3.1.0
  hostvars_suffix:
    description:
      - The suffix for host variables names coming from AWS.
    type: str
    version_added: 3.1.0
notes:
  - Ansible versions prior to 2.10 should use the fully qualified plugin name 'amazon.aws.aws_rds'.
extends_documentation_fragment:
  - inventory_cache
  - constructed
  - amazon.aws.boto3
  - amazon.aws.common.plugins
  - amazon.aws.region.plugins
  - amazon.aws.assume_role.plugins
author:
  - Sloane Hertel (@s-hertel)
"""

EXAMPLES = r"""
plugin: aws_rds
regions:
  - us-east-1
  - ca-central-1
resource_tag_filters:
  EnvironmentName: dummy-develop
keyed_groups:
  - key: 'db_parameter_groups|json_query("[].db_parameter_group_name")'
    prefix: rds_parameter_group
  - key: engine
    prefix: rds
  - key: tags
  - key: region
hostvars_prefix: aws_
hostvars_suffix: _rds
"""

try:
    import botocore
except ImportError:
    pass  # will be captured by imported HAS_BOTO3

from ansible.errors import AnsibleError
from ansible.module_utils._text import to_native
from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict

from ansible.module_utils.six import integer_types
from ansible.module_utils.six import string_types

from ansible_collections.amazon.aws.plugins.module_utils.botocore import is_boto3_error_code
from ansible_collections.amazon.aws.plugins.module_utils.tagging import boto3_tag_list_to_ansible_dict
from ansible_collections.amazon.aws.plugins.module_utils.transformation import ansible_dict_to_boto3_filter_list
from ansible_collections.amazon.aws.plugins.plugin_utils.inventory import AWSInventoryBase


def _find_hosts_with_valid_statuses(hosts, statuses):
    if "all" in statuses:
        return hosts
    valid_hosts = []
    for host in hosts:
        if host.get("DBInstanceStatus") in statuses:
            valid_hosts.append(host)
        elif host.get("Status") in statuses:
            valid_hosts.append(host)
    return valid_hosts


def _get_rds_hostname(host):
    if host.get("DBInstanceIdentifier"):
        return host["DBInstanceIdentifier"]
    else:
        return host["DBClusterIdentifier"]


def _add_tags_for_rds_hosts(connection, hosts, strict):
    for host in hosts:
        if "DBInstanceArn" in host:
            resource_arn = host["DBInstanceArn"]
        else:
            resource_arn = host["DBClusterArn"]

        try:
            tags = connection.list_tags_for_resource(ResourceName=resource_arn)["TagList"]
        except is_boto3_error_code("AccessDenied") as e:
            if not strict:
                tags = []
            else:
                raise e
        host["Tags"] = tags


def describe_resource_with_tags(func):
    def describe_wrapper(connection, filters, strict=False):
        try:
            results = func(connection=connection, filters=filters)
            if "DBInstances" in results:
                results = results["DBInstances"]
            else:
                results = results["DBClusters"]
            _add_tags_for_rds_hosts(connection, results, strict)
        except is_boto3_error_code("AccessDenied") as e:  # pylint: disable=duplicate-except
            if not strict:
                return []
            raise AnsibleError(f"Failed to query RDS: {to_native(e)}")
        except (
            botocore.exceptions.BotoCoreError,
            botocore.exceptions.ClientError,
        ) as e:  # pylint: disable=duplicate-except
            raise AnsibleError(f"Failed to query RDS: {to_native(e)}")

        return results

    return describe_wrapper


@describe_resource_with_tags
def _describe_db_instances(connection, filters):
    paginator = connection.get_paginator("describe_db_instances")
    return paginator.paginate(Filters=filters).build_full_result()


@describe_resource_with_tags
def _describe_db_clusters(connection, filters):
    return connection.describe_db_clusters(Filters=filters)


class InventoryModule(AWSInventoryBase):
    NAME = "custom.aws_rds_from_resource_tag"  # forked from amazon.aws.aws_rds
    INVENTORY_FILE_SUFFIXES = (
        "aws_rds_from_resource_tag.yml",
        "aws_rds_from_resource_tag.yaml",
    )

    def __init__(self):
        super().__init__()
        self.credentials = {}

    def _populate(self, hosts):
        group = "aws_rds"
        self.inventory.add_group(group)
        if hosts:
            self._add_hosts(hosts=hosts, group=group)
            self.inventory.add_child("all", group)

    def _populate_from_source(self, source_data):
        hostvars = source_data.pop("_meta", {}).get("hostvars", {})
        for group in source_data:
            if group == "all":
                continue
            self.inventory.add_group(group)
            hosts = source_data[group].get("hosts", [])
            for host in hosts:
                self._populate_host_vars([host], hostvars.get(host, {}), group)
            self.inventory.add_child("all", group)

    def _format_inventory(self, hosts):
        results = {"_meta": {"hostvars": {}}}
        group = "aws_rds"
        results[group] = {"hosts": []}
        for host in hosts:
            hostname = _get_rds_hostname(host)
            results[group]["hosts"].append(hostname)
            h = self.inventory.get_host(hostname)
            results["_meta"]["hostvars"][h.name] = h.vars
        return results

    def _update_filters(self, filters, filter_name, value):
        if [d for d in filters if d['Name'] == filter_name]:
            filters = [
                {"Name": d["Name"], "Values": list(set(d["Values"] + value))} if d['Name'] == filter_name else d for d in filters
            ]
        else:
            filters += [{"Name": filter_name, "Values": value}]
        return filters

    def _ansible_dict_to_boto3_resourcegroupstaggingapi_filter_list(self, filters_dict):
        """Convert an Ansible dict of filters to list of dicts that boto3 resourcegroupstaggingapi.get_resources can use
        https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/resourcegroupstaggingapi/client/get_resources.html
        Args:
            filters_dict (dict): Dict of AWS filters.
        Basic Usage:
            >>> filters = {'some-aws-id': 'i-01234567'}
            >>> ansible_dict_to_boto3_filter_list(filters)
            {
                'some-aws-id': 'i-01234567'
            }
        Returns:
            List: List of AWS filters and their values
            [
                {
                    'Key': 'some-aws-id',
                    'Values': [
                        'i-01234567',
                    ]
                }
            ]
        """

        filters_list = []
        for k, v in filters_dict.items():
            filter_dict = {"Key": k}
            if isinstance(v, bool):
                filter_dict["Values"] = [str(v).lower()]
            elif isinstance(v, integer_types):
                filter_dict["Values"] = [str(v)]
            elif isinstance(v, string_types):
                filter_dict["Values"] = [v]
            else:
                filter_dict["Values"] = v

            filters_list.append(filter_dict)

        return filters_list

    def _add_hosts(self, hosts, group):
        """
        :param hosts: a list of hosts to be added to a group
        :param group: the name of the group to which the hosts belong
        """
        for host in hosts:
            hostname = _get_rds_hostname(host)
            host = camel_dict_to_snake_dict(host, ignore_list=["Tags"])
            host["tags"] = boto3_tag_list_to_ansible_dict(host.get("tags", []))

            # Allow easier grouping by region
            if "availability_zone" in host:
                host["region"] = host["availability_zone"][:-1]
            elif "availability_zones" in host:
                host["region"] = host["availability_zones"][0][:-1]

            self.inventory.add_host(hostname, group=group)
            hostvars_prefix = self.get_option("hostvars_prefix")
            hostvars_suffix = self.get_option("hostvars_suffix")
            new_vars = dict()
            for hostvar, hostval in host.items():
                if hostvars_prefix:
                    hostvar = hostvars_prefix + hostvar
                if hostvars_suffix:
                    hostvar = hostvar + hostvars_suffix
                new_vars[hostvar] = hostval
                self.inventory.set_variable(hostname, hostvar, hostval)
            host.update(new_vars)

            # Use constructed if applicable
            strict = self.get_option("strict")
            # Composed variables
            self._set_composite_vars(self.get_option("compose"), host, hostname, strict=strict)
            # Complex groups based on jinja2 conditionals, hosts that meet the conditional are added to group
            self._add_host_to_composed_groups(self.get_option("groups"), host, hostname, strict=strict)
            # Create groups based on variable values and add the corresponding hosts to it
            self._add_host_to_keyed_groups(self.get_option("keyed_groups"), host, hostname, strict=strict)

    def _get_all_db_hosts(self, regions, tag_filters, instance_filters, cluster_filters, strict, statuses, gather_clusters=False):
        """
        :param regions: a list of regions in which to describe hosts
        :param tag_filters: a list of boto3 filter dictionaries
        :param instance_filters: a list of boto3 filter dictionaries
        :param cluster_filters: a list of boto3 filter dictionaries
        :param strict: a boolean determining whether to fail or ignore 403 error codes
        :param statuses: a list of statuses that the returned hosts should match
        :return A list of host dictionaries
        """
        all_instances = []
        all_clusters = []

        for connection, _region in self.all_clients("rds"):
            resourcegroupstaggingapi = self.client(
                'resourcegroupstaggingapi', region=_region
            )
            resource_tag_mapping_list_instances = (
                resourcegroupstaggingapi.get_resources(
                    TagFilters=tag_filters, ResourceTypeFilters=["rds:db"]
                )["ResourceTagMappingList"]
            )
            # Put an empty list in db-instance-id in filters to get InvalidParameterCombination. So, put the dummy value by default.
            instances = ["DummyForAvoidInvalidParameterCombination"] + [
                i["ResourceARN"].split(":")[-1]
                for i in resource_tag_mapping_list_instances
            ]
            instance_filters = self._update_filters(
                instance_filters, "db-instance-id", instances
            )
            all_instances += _describe_db_instances(connection, instance_filters, strict=strict)
            if gather_clusters:
                resource_tag_mapping_list_clusters = (
                    resourcegroupstaggingapi.get_resources(
                        TagFilters=tag_filters, ResourceTypeFilters=["rds:cluster"]
                    )["ResourceTagMappingList"]
                )
                # Put an empty list in db-cluster-id in filters to get InvalidParameterCombination. So, put the dummy value by default.
                clusters = ["DummyForAvoidInvalidParameterCombination"] + [
                    i["ResourceARN"].split(":")[-1]
                    for i in resource_tag_mapping_list_clusters
                ]
                cluster_filters = self._update_filters(
                    cluster_filters, "db-cluster-id", clusters
                )
                all_clusters += _describe_db_clusters(connection, cluster_filters, strict=strict)
        sorted_hosts = list(
            sorted(all_instances, key=lambda x: x["DBInstanceIdentifier"])
            + sorted(all_clusters, key=lambda x: x["DBClusterIdentifier"])
        )
        return _find_hosts_with_valid_statuses(sorted_hosts, statuses)

    def parse(self, inventory, loader, path, cache=True):
        super().parse(inventory, loader, path, cache=cache)

        # get user specifications
        regions = self.get_option("regions")
        filters = self.get_option("filters")
        resource_tag_filters = self.get_option("resource_tag_filters")
        strict_permissions = self.get_option("strict_permissions")
        statuses = self.get_option("statuses")
        include_clusters = self.get_option("include_clusters")
        instance_filters = ansible_dict_to_boto3_filter_list(filters)
        tag_filters = self._ansible_dict_to_boto3_resourcegroupstaggingapi_filter_list(
            resource_tag_filters
        )
        cluster_filters = []
        if "db-cluster-id" in filters and include_clusters:
            cluster_filters = ansible_dict_to_boto3_filter_list({"db-cluster-id": filters["db-cluster-id"]})

        result_was_cached, cached_result = self.get_cached_result(path, cache)
        if result_was_cached:
            self._populate_from_source(cached_result)
            return

        results = self._get_all_db_hosts(
            regions,
            tag_filters,
            instance_filters,
            cluster_filters,
            strict_permissions,
            statuses,
            include_clusters,
        )
        self._populate(results)

        # Update the cache once we're done
        formatted_inventory = self._format_inventory(results)
        self.update_cached_result(path, cache, formatted_inventory)
```

## 元のコードとの差分

- モジュールに引き渡す引数にタグフィルター用のresource_tag_filtersを追加
- ansible側から渡したタグフィルター情報をboto3側に渡す形式にするための変換ロジックである  `_ansible_dict_to_boto3_resourcegroupstaggingapi_filter_list` の実装
  - https://github.com/ansible-collections/amazon.aws/blob/71c20cd0d644546451f8122f2be029aa1659d6fd/plugins/module_utils/transformation.py#L54 をベースにKey/Valueを変更
- resourcegroupstaggingapiからリソースを検索し、結果をfilterに追加するロジックの実装

`diff aws_rds.py aws_rds_from_resource_tag.py` の結果

```diff
4a5
> # original file is https://github.com/ansible-collections/amazon.aws/blob/9.3.0/plugins/inventory/aws_rds.py
12a14,17
>   resource_tag_filters:
>     description: A dictionary of filter value pairs. Available filters are listed here
>         U(https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/resourcegroupstaggingapi/client/get_resources.html)
>     default: {}
68a74,75
> resource_tag_filters:
>   EnvironmentName: dummy-develop
89,100c96,102
< from ansible_collections.amazon.aws.plugins.module_utils.botocore import (
<     is_boto3_error_code,
< )
< from ansible_collections.amazon.aws.plugins.module_utils.tagging import (
<     boto3_tag_list_to_ansible_dict,
< )
< from ansible_collections.amazon.aws.plugins.module_utils.transformation import (
<     ansible_dict_to_boto3_filter_list,
< )
< from ansible_collections.amazon.aws.plugins.plugin_utils.inventory import (
<     AWSInventoryBase,
< )
---
> from ansible.module_utils.six import integer_types
> from ansible.module_utils.six import string_types
> 
> from ansible_collections.amazon.aws.plugins.module_utils.botocore import is_boto3_error_code
> from ansible_collections.amazon.aws.plugins.module_utils.tagging import boto3_tag_list_to_ansible_dict
> from ansible_collections.amazon.aws.plugins.module_utils.transformation import ansible_dict_to_boto3_filter_list
> from ansible_collections.amazon.aws.plugins.plugin_utils.inventory import AWSInventoryBase
130,132c132
<             tags = connection.list_tags_for_resource(ResourceName=resource_arn)[
<                 "TagList"
<             ]
---
>             tags = connection.list_tags_for_resource(ResourceName=resource_arn)["TagList"]
150,152c150
<         except is_boto3_error_code(
<             "AccessDenied"
<         ) as e:  # pylint: disable=duplicate-except
---
>         except is_boto3_error_code("AccessDenied") as e:  # pylint: disable=duplicate-except
179,180c177,181
<     NAME = "amazon.aws.aws_rds"
<     INVENTORY_FILE_SUFFIXES = ("aws_rds.yml", "aws_rds.yaml")
---
>     NAME = "custom.aws_rds_from_resource_tag"  # forked from amazon.aws.aws_rds
>     INVENTORY_FILE_SUFFIXES = (
>         "aws_rds_from_resource_tag.yml",
>         "aws_rds_from_resource_tag.yaml",
>     )
214a216,263
>     def _update_filters(self, filters, filter_name, value):
>         if [d for d in filters if d['Name'] == filter_name]:
>             filters = [
>                 {"Name": d["Name"], "Values": list(set(d["Values"] + value))} if d['Name'] == filter_name else d for d in filters
>             ]
>         else:
>             filters += [{"Name": filter_name, "Values": value}]
>         return filters
> 
>     def _ansible_dict_to_boto3_resourcegroupstaggingapi_filter_list(self, filters_dict):
>         """Convert an Ansible dict of filters to list of dicts that boto3 resourcegroupstaggingapi.get_resources can use
>         https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/resourcegroupstaggingapi/client/get_resources.html
>         Args:
>             filters_dict (dict): Dict of AWS filters.
>         Basic Usage:
>             >>> filters = {'some-aws-id': 'i-01234567'}
>             >>> ansible_dict_to_boto3_filter_list(filters)
>             {
>                 'some-aws-id': 'i-01234567'
>             }
>         Returns:
>             List: List of AWS filters and their values
>             [
>                 {
>                     'Key': 'some-aws-id',
>                     'Values': [
>                         'i-01234567',
>                     ]
>                 }
>             ]
>         """
> 
>         filters_list = []
>         for k, v in filters_dict.items():
>             filter_dict = {"Key": k}
>             if isinstance(v, bool):
>                 filter_dict["Values"] = [str(v).lower()]
>             elif isinstance(v, integer_types):
>                 filter_dict["Values"] = [str(v)]
>             elif isinstance(v, string_types):
>                 filter_dict["Values"] = [v]
>             else:
>                 filter_dict["Values"] = v
> 
>             filters_list.append(filter_dict)
> 
>         return filters_list
> 
247,249c296
<             self._set_composite_vars(
<                 self.get_option("compose"), host, hostname, strict=strict
<             )
---
>             self._set_composite_vars(self.get_option("compose"), host, hostname, strict=strict)
251,253c298
<             self._add_host_to_composed_groups(
<                 self.get_option("groups"), host, hostname, strict=strict
<             )
---
>             self._add_host_to_composed_groups(self.get_option("groups"), host, hostname, strict=strict)
255,257c300
<             self._add_host_to_keyed_groups(
<                 self.get_option("keyed_groups"), host, hostname, strict=strict
<             )
---
>             self._add_host_to_keyed_groups(self.get_option("keyed_groups"), host, hostname, strict=strict)
259,267c302
<     def _get_all_db_hosts(
<         self,
<         regions,
<         instance_filters,
<         cluster_filters,
<         strict,
<         statuses,
<         gather_clusters=False,
<     ):
---
>     def _get_all_db_hosts(self, regions, tag_filters, instance_filters, cluster_filters, strict, statuses, gather_clusters=False):
269a305
>         :param tag_filters: a list of boto3 filter dictionaries
280,281c316,322
<             all_instances += _describe_db_instances(
<                 connection, instance_filters, strict=strict
---
>             resourcegroupstaggingapi = self.client(
>                 'resourcegroupstaggingapi', region=_region
>             )
>             resource_tag_mapping_list_instances = (
>                 resourcegroupstaggingapi.get_resources(
>                     TagFilters=tag_filters, ResourceTypeFilters=["rds:db"]
>                 )["ResourceTagMappingList"]
282a324,332
>             # Put an empty list in db-instance-id in filters to get InvalidParameterCombination. So, put the dummy value by default.
>             instances = ["DummyForAvoidInvalidParameterCombination"] + [
>                 i["ResourceARN"].split(":")[-1]
>                 for i in resource_tag_mapping_list_instances
>             ]
>             instance_filters = self._update_filters(
>                 instance_filters, "db-instance-id", instances
>             )
>             all_instances += _describe_db_instances(connection, instance_filters, strict=strict)
284,285c334,345
<                 all_clusters += _describe_db_clusters(
<                     connection, cluster_filters, strict=strict
---
>                 resource_tag_mapping_list_clusters = (
>                     resourcegroupstaggingapi.get_resources(
>                         TagFilters=tag_filters, ResourceTypeFilters=["rds:cluster"]
>                     )["ResourceTagMappingList"]
>                 )
>                 # Put an empty list in db-cluster-id in filters to get InvalidParameterCombination. So, put the dummy value by default.
>                 clusters = ["DummyForAvoidInvalidParameterCombination"] + [
>                     i["ResourceARN"].split(":")[-1]
>                     for i in resource_tag_mapping_list_clusters
>                 ]
>                 cluster_filters = self._update_filters(
>                     cluster_filters, "db-cluster-id", clusters
286a347
>                 all_clusters += _describe_db_clusters(connection, cluster_filters, strict=strict)
298a360
>         resource_tag_filters = self.get_option("resource_tag_filters")
302a365,367
>         tag_filters = self._ansible_dict_to_boto3_resourcegroupstaggingapi_filter_list(
>             resource_tag_filters
>         )
305,307c370
<             cluster_filters = ansible_dict_to_boto3_filter_list(
<                 {"db-cluster-id": filters["db-cluster-id"]}
<             )
---
>             cluster_filters = ansible_dict_to_boto3_filter_list({"db-cluster-id": filters["db-cluster-id"]})
315a379
>             tag_filters,
```

## 実際の使用例

aws_rds_from_resource_tag.ymlを以下のように設定

```yml
plugin: aws_rds_from_resource_tag
regions:
  - ap-northeast-1
  - ap-northeast-3
include_clusters: yes
resource_tag_filters:
  EnvironmentName: hoge-fuga
statuses:
  - available
  - creating

groups:
  AuroraCluster: "'ap-northeast-1' in db_cluster_arn"
  AuroraBackupCluster: "'ap-northeast-3' in db_cluster_arn"
  AuroraInstance: "'ap-northeast-1' in db_instance_arn"
```

コマンドを実行

```
$ ansible-inventory -i inventory/aws_rds_from_resource_tag.yml --graph
@all:
  |--@ungrouped:
  |--@aws_rds:
  |  |--hoge-fuga-aurora-2-instance-az-a
  |  |--hoge-fuga-aurora-2-822366f0
  |--@AuroraInstance:
  |  |--hoge-fuga-aurora-2-instance-az-a
  |--@AuroraCluster:
  |  |--hoge-fuga-aurora-2-822366f0
```

## 開発時のテスト・デバッグ

通常のansibleと同様に、-v でデバッグログを表示できるので、 `-vvvvv`で最大のデバッグ出力量にして実行すると、Python側のスタックトレースまで見れるので原因調査がしやすい

```python
$ ansible-inventory -i inventory/aws_rds_from_resource_tag.yml --graph -vvvvv
ansible-inventory [core 2.18.4]
~~~~
ubuntu@ip-10-0-13-174:~/$  ansible-inventory -i inventory/aws_rds_from_resource_tag.yml --graph -vvvvv
ansible-inventory [core 2.18.4]
  config file = /home/ubuntu//ansible.cfg
  configured module search path = ['/home/ubuntu//library']
  ansible python module location = /usr/local/lib/python3.12/dist-packages/ansible
  ansible collection location = /home/ubuntu/.ansible/collections:/usr/share/ansible/collections
  executable location = /usr/local/bin/ansible-inventory
  python version = 3.12.3 (main, Feb  4 2025, 14:48:35) [GCC 13.3.0] (/usr/bin/python3)
  jinja version = 3.1.6
  libyaml = True
Using /home/ubuntu//ansible.cfg as config file
setting up inventory plugins
Loading collection ansible.builtin from
host_list declined parsing /home/ubuntu/inventory/aws_rds_from_resource_tag.yml as it did not pass its verify_file() method
script declined parsing /home/ubuntu/inventory/aws_rds_from_resource_tag.yml as it did not pass its verify_file() method
Loading collection amazon.aws from /usr/local/lib/python3.12/dist-packages/ansible_collections/amazon/aws
Using inventory plugin 'aws_rds_from_resource_tag' to process inventory source '/home/ubuntu/inventory/aws_rds_from_resource_tag.yml'
toml declined parsing /home/ubuntu/inventory/aws_rds_from_resource_tag.yml as it did not pass its verify_file() method
[WARNING]:  * Failed to parse /home/ubuntu/inventory/aws_rds_from_resource_tag.yml with auto plugin:
Parameter validation failed: Unknown parameter in TagFilters[0]: "Name", must be one of:
Key, Values
  File "/usr/local/lib/python3.12/dist-packages/ansible/inventory/manager.py", line 292, in parse_source
    plugin.parse(self._inventory, self._loader, source, cache=cache)
  File "/usr/local/lib/python3.12/dist-packages/ansible/plugins/inventory/auto.py", line 58, in parse
    plugin.parse(inventory, loader, path, cache=cache)
  File "/home/ubuntu/inventory/aws_rds_from_resource_tag.py", line 376, in parse
    results = self._get_all_db_hosts(
              ^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ubuntu/inventory/aws_rds_from_resource_tag.py", line 320, in _get_all_db_hosts
    resourcegroupstaggingapi.get_resources(
  File "/usr/local/lib/python3.12/dist-packages/botocore/client.py", line 570, in _api_call
    return self._make_api_call(operation_name, kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/dist-packages/botocore/context.py", line 124, in wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/dist-packages/botocore/client.py", line 988, in _make_api_call
    request_dict = self._convert_to_request_dict(
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/dist-packages/botocore/client.py", line 1055, in _convert_to_request_dict
    request_dict = self._serializer.serialize_to_request(
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/dist-packages/botocore/validate.py", line 381, in serialize_to_request
    raise ParamValidationError(report=report.generate_report())
[WARNING]:  * Failed to parse /home/ubuntu/inventory/aws_rds_from_resource_tag.yml with yaml plugin:
Plugin configuration YAML file, not YAML inventory
  File "/usr/local/lib/python3.12/dist-packages/ansible/inventory/manager.py", line 292, in parse_source
    plugin.parse(self._inventory, self._loader, source, cache=cache)
  File "/usr/local/lib/python3.12/dist-packages/ansible/plugins/inventory/yaml.py", line 113, in parse
    raise AnsibleParserError('Plugin configuration YAML file, not YAML inventory')
[WARNING]:  * Failed to parse /home/ubuntu/inventory/aws_rds_from_resource_tag.yml with ini plugin:
Invalid host pattern 'plugin:' supplied, ending in ':' is not allowed, this character is
reserved to provide a port.
  File "/usr/local/lib/python3.12/dist-packages/ansible/inventory/manager.py", line 292, in parse_source
    plugin.parse(self._inventory, self._loader, source, cache=cache)
  File "/usr/local/lib/python3.12/dist-packages/ansible/plugins/inventory/ini.py", line 137, in parse
    raise AnsibleParserError(e)
[WARNING]: Unable to parse /home/ubuntu/inventory/aws_rds_from_resource_tag.yml as an inventory source
ERROR! No inventory was parsed, please check your configuration and options.
```

## 開発してみた感想

- はまりポイントもいくつかあるが、そこを把握すれば割とすんなり実装できた
- amazon.aws コレクション自体が結構作りこまれているので、その資産を利用していくことが大事
- まだゴリ押し実装の部分があるので、より洗練された実装にして、ansible側にコントリビューションしたい