Ansible Amazon AWS 模块开发指南

Ansible AWS 集合(在 Galaxy 上,源代码 存储库)由 Ansible AWS 工作组维护。有关更多信息,请参阅 AWS 工作组社区页面。如果您计划向 Ansible 贡献 AWS 模块,那么与工作组联系是一个很好的开始方式,特别是因为可能已经有一个类似的模块正在开发中。

要求

Python 兼容性

Ansible 2.9 和 1.x 集合版本中的 AWS 内容支持 Python 2.7 和更高版本。

从两个集合的 2.0 版本开始,将按照 AWS 的 Python 2.7 支持终止结束对 Python 2.7 的支持。针对 2.0 或更高集合版本的两个集合的贡献可以编写为支持 Python 3.6+ 语法。

SDK 版本支持

从两个集合的 2.0 版本开始,通常的政策是支持在最近的主要集合版本发布前 12 个月发布的 botocore 和 boto3 版本,遵循语义版本控制(例如,2.0.0、3.0.0)。

可以贡献需要较新版本 SDK 的功能,前提是在模块文档中注明

DOCUMENTATION = '''
---
module: ec2_vol
options:
  throughput:
    description:
      - Volume throughput in MB/s.
      - This parameter is only valid for gp3 volumes.
      - Valid range is from 125 to 1000.
      - Requires at least botocore version 1.19.27.
    type: int
    version_added: 1.4.0

并使用 botocore_at_least 助手方法处理

if module.params.get('throughput'):
    if not module.botocore_at_least("1.19.27"):
        module.fail_json(msg="botocore >= 1.19.27 is required to set the throughput for a volume")

从两个集合的 4.0 版本开始,所有对原始 boto SDK 的支持都已删除。AWS 模块必须使用 botocore 和 boto3 SDK 编写。

维护现有模块

更新日志

任何更改功能或修复错误的 PR 都必须添加更新日志片段。有关更新日志片段的更多信息,请参阅 Ansible 开发周期文档的“使你的 PR 值得合并”部分<community_changelogs>

重大变更

应避免可能破坏使用 AWS 集合的现有 playbook 的更改,仅应在主要版本中进行更改,并且在实际可行的情况下,应在至少 1 个完整的主要版本弃用周期之前进行。弃用可能会反向移植到稳定分支。

例如:- 在 3.0.0 版本中添加的弃用可能会在 4.0.0 版本中删除。- 在 1.2.0 版本中添加的弃用可能会在 3.0.0 版本中删除。

重大变更包括:- 删除参数。- 使参数 required。- 更新参数的默认值。- 更改或删除现有返回值。

添加新功能

尽量保持与至少一年前的 boto3/botocore 版本的向后兼容性。这意味着,如果要实现使用 boto3/botocore 新功能的功能,则只有在显式使用该功能时才应失败,并显示一条消息,指出缺少的功能和 botocore 的最低要求版本。(功能支持通常在 botocore 中定义,然后由 boto3 使用)

module = AnsibleAWSModule(
    argument_spec=argument_spec,
    ...
)

if module.params.get('scope') == 'managed':
    module.require_botocore_at_least('1.23.23', reason='to list managed rules')

发布策略和反向移植合并的 PR

所有 amazon.aws 和 community.aws 的 PR 都必须先合并到 main 分支。在 PR 被接受并合并到 main 分支后,才可以将其反向移植到稳定分支。

main 分支是集合的下一个主要版本(X+1)的暂存位置,可能包含破坏性更改。

常规反向移植策略

  • 新功能、弃用和次要更改可以反向移植到最新的稳定版本。

  • 错误修复可以反向移植到最新的 2 个稳定版本。

  • 安全修复应至少反向移植到最新的 2 个稳定版本。

在必要时,可以将额外的 CI 相关更改引入到较旧的稳定分支,以确保 CI 继续运行。

反向移植 PR 最简单的方法是在 PR 上添加 backport-Y 标签。一旦 PR 被合并,patchback 机器人将尝试自动创建一个反向移植 PR。

创建新的 AWS 模块

在编写新模块时,重要的是要考虑模块的作用范围。通常,尽量只做一件事,并且把它做好。

在 Amazon API 提供依赖资源(如 S3 存储桶和 S3 对象)之间的区分时,这通常是模块之间很好的分隔。此外,与另一个资源具有多对多关系(如 IAM 管理策略和 IAM 角色)的资源,通常最好由两个单独的模块管理。

虽然可以编写一个 s3 模块来管理所有与 S3 相关的内容,但彻底测试和维护这样一个模块是很困难的。同样,虽然可以编写一个模块来管理基本的 EC2 安全组资源,并编写第二个模块来管理安全组上的规则,但这将与模块用户可能期望的相反。

没有绝对正确的答案,但重要的是要考虑它,并且 Amazon 在设计其 API 时经常为您完成这项工作。

命名您的模块

模块名称应包含正在管理的资源的名称,并以模块所基于的 AWS API 的名称作为前缀。如果不存在前缀示例,一个好的经验法则是使用您在 boto3 中使用的客户端名称作为起点。

除非某个名称是 AWS 主要组件(例如,VPC 或 ELB)的众所周知的缩写,否则请避免进一步缩写名称,并且不要独立创建新的缩写。

如果 AWS API 主要管理单个资源,则管理此资源的模块可以仅命名为 API 的名称。但是,如果 Amazon 使用这些名称来引用它们,请考虑使用 instancecluster 来提高清晰度。

示例

  • ec2_instance

  • s3_object(之前命名为 aws_s3,但主要用于操作 S3 对象)

  • elb_classic_lb(之前是 ec2_elb_lb,但属于 ELB API,而不是 EC2)

  • networkfirewall_rule_group

  • networkfirewall(虽然这可以称为 networkfirewall_firewall,但第二个 firewall 是多余的,并且该 API 专注于创建这些 firewall 资源)

注意:在集合从 Ansible Core 分离之前,通常使用 aws_ 作为前缀来消除具有通用名称的服务的歧义,例如 aws_secret。现在不再需要这样做,并且 aws_ 前缀保留用于具有非常广泛影响的服务,在这些服务中,引用 AWS API 可能会导致混淆。例如,aws_region_info,它连接到 EC2,但提供有关帐户中为所有服务启用的区域的全局信息。

使用 boto3 和 AnsibleAWSModule

所有新的 AWS 模块都必须使用 boto3/botocore 和 AnsibleAWSModule

AnsibleAWSModule 大大简化了异常处理和库管理,减少了样板代码的数量。如果您不能使用 AnsibleAWSModule 作为基础,则必须记录原因并请求对此规则的例外。

导入 botocore 和 boto3

ansible_collections.amazon.aws.plugins.module_utils.botocore 模块会自动导入 boto3 和 botocore。如果系统中缺少 boto3,则变量 HAS_BOTO3 将设置为 False。通常,这意味着模块不需要直接导入 boto3。当使用 AnsibleAWSModule 时,无需检查 HAS_BOTO3,因为该模块会执行该检查。

from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule
try:
    import botocore
except ImportError:
    pass  # handled by AnsibleAWSModule

或者

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.basic import missing_required_lib
from ansible_collections.amazon.aws.plugins.module_utils.botocore import HAS_BOTO3
try:
    import botocore
except ImportError:
    pass  # handled by imported HAS_BOTO3

def main():
    if not HAS_BOTO3:
        module.fail_json(missing_required_lib('botocore and boto3'))

支持模块默认值

现有的 AWS 模块支持使用 模块默认值 来设置常用的身份验证参数。要为新模块执行相同的操作,请在 meta/runtime.yml 中为其添加一个条目。这些条目的形式为

action_groups:
  aws:
     ...
     example_module

模块行为

为了减少添加新功能时发生破坏性更改的可能性,当任务中未显式设置参数时,该模块应避免修改资源属性。

按照惯例,当在任务中显式设置参数时,该模块应将资源属性设置为与任务中设置的值匹配。在某些情况下,例如标签或关联,添加一个额外的参数(可以设置为将行为从替换更改为添加)可能会有所帮助。但是,默认行为仍然应该是替换而不是添加。

有关 tagspurge_tags 的示例,请参见“处理标签<ansible_collections.amazon.aws.docsite.dev_tags>”部分。

连接到 AWS

AnsibleAWSModule 提供了 resourceclient 辅助方法来获取 boto3 连接。这些方法处理一些比较深奥的连接选项,例如安全令牌和 boto 配置文件。

如果使用基本的 AnsibleModule,则应使用 get_aws_connection_info,然后使用 boto3_conn 连接到 AWS,因为它们处理相同范围的连接选项。

这些辅助方法还会检查是否缺少配置文件或未设置区域(当需要时),因此您无需执行此操作。

下面显示了连接到 EC2 的示例。请注意,与 boto 不同,这里没有像 boto 中那样的 NoAuthHandlerFound 异常处理。相反,当您使用连接时,会引发 AuthFailure 异常。为了确保捕获授权、参数验证和权限错误,您应该使用每个 boto3 连接调用捕获 ClientErrorBotoCoreError 异常。请参见异常处理。

module.client('ec2')

或者对于更高级别的 EC2 资源

module.resource('ec2')

基于 AnsibleModule 而不是 AnsibleAWSModule 的模块使用的较旧样式的连接示例

region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True)
connection = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_params)
region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True)
connection = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_params)

连接参数的通用文档片段

有四个 通用文档片段 应该包含在几乎所有 AWS 模块中。

  • boto3 - 包含集合的最低要求。

  • common.modules - 包含常见的 boto3 连接参数。

  • region.modules - 包含许多 AWS API 所需的常见区域参数。

  • tags - 包含常见的标记参数。

应使用这些片段,而不是重新记录这些属性,以确保一致性并记录更深奥的连接选项。例如

DOCUMENTATION = '''
module: my_module
# some lines omitted here
extends_documentation_fragment:
    - amazon.aws.boto3
    - amazon.aws.common.modules
    - amazon.aws.region.modules
'''

其他插件类型具有略有不同的文档片段格式,应使用以下片段。

  • boto3 - 包含集合的最低要求。

  • common.plugins - 包含常见的 boto3 连接参数。

  • region.plugins - 包含许多 AWS API 所需的常见区域参数。

  • tags - 包含常见的标记参数。

应使用这些片段,而不是重新记录这些属性,以确保一致性并记录更深奥的连接选项。例如

DOCUMENTATION = '''
module: my_plugin
# some lines omitted here
extends_documentation_fragment:
    - amazon.aws.boto3
    - amazon.aws.common.plugins
    - amazon.aws.region.plugins
'''

处理异常

您应该将任何 boto3 或 botocore 调用包装在 try 块中。如果引发异常,则有多种处理它的可能性。

  • 捕获常规的 ClientError 或使用查找特定的错误代码

    is_boto3_error_code.

  • 使用 aws_module.fail_json_aws() 以标准方式报告模块失败。

  • 使用 AWSRetry 进行重试。

  • 使用 fail_json() 报告失败,而不使用 AnsibleAWSModule

  • 在知道如何处理异常的情况下,执行一些自定义操作。

有关 botocore 异常处理的更多信息,请参见 botocore 错误文档

使用 is_boto3_error_code

要使用 ansible_collections.amazon.aws.plugins.module_utils.botocore.is_boto3_error_code 来捕获单个 AWS 错误代码,请在 except 子句中调用它,而不是 ClientError。在此示例中,只有 InvalidGroup.NotFound 错误代码会在此处被捕获,并且任何其他错误都将被引发,以便在程序的其他位置进行处理。

try:
    info = connection.describe_security_groups(**kwargs)
except is_boto3_error_code('InvalidGroup.NotFound'):
    pass
do_something(info)  # do something with the info that was successfully returned

使用 fail_json_aws()

在 AnsibleAWSModule 中,有一个特殊的方法 module.fail_json_aws(),用于很好地报告异常。在异常上调用此方法,它将报告错误以及用于 Ansible 详细模式的追溯信息。

除非不可能,否则您应该对所有新模块使用 AnsibleAWSModule。

from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule

# Set up module parameters
# module params code here

# Connect to AWS
# connection code here

# Make a call to AWS
name = module.params.get('name')
try:
    result = connection.describe_frooble(FroobleName=name)
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
    module.fail_json_aws(e, msg="Couldn't obtain frooble %s" % name)

请注意,通常情况下捕获所有正常异常是可以接受的,但是如果您期望出现 botocore 异常之外的任何其他异常,则应测试所有内容是否按预期工作。

如果您需要根据 boto3 返回的错误执行操作,请使用错误代码和 is_boto3_error_code() 辅助函数。

# Make a call to AWS
name = module.params.get('name')
try:
    result = connection.describe_frooble(FroobleName=name)
except is_boto3_error_code('FroobleNotFound'):
    workaround_failure()  # This is an error that we can work around
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:  # pylint: disable=duplicate-except
    module.fail_json_aws(e, msg="Couldn't obtain frooble %s" % name)

使用 fail_json() 并避免 AnsibleAWSModule

当抛出异常时,Boto3 提供了许多有用的信息,因此请将此信息以及消息传递给用户。

from ansible_collections.amazon.aws.plugins.module_utils.botocore import HAS_BOTO3
try:
    import botocore
except ImportError:
    pass  # caught by imported HAS_BOTO3

# Connect to AWS
# connection code here

# Make a call to AWS
name = module.params.get('name')
try:
    result = connection.describe_frooble(FroobleName=name)
except botocore.exceptions.ClientError as e:
    module.fail_json(msg="Couldn't obtain frooble %s: %s" % (name, str(e)),
                     exception=traceback.format_exc(),
                     **camel_dict_to_snake_dict(e.response))

注意:我们使用 str(e) 而不是 e.message,因为后者不适用于 python3

如果您需要根据 boto3 返回的错误执行操作,请使用错误代码。

# Make a call to AWS
name = module.params.get('name')
try:
    result = connection.describe_frooble(FroobleName=name)
except botocore.exceptions.ClientError as e:
    if e.response['Error']['Code'] == 'FroobleNotFound':
        workaround_failure()  # This is an error that we can work around
    else:
        module.fail_json(msg="Couldn't obtain frooble %s: %s" % (name, str(e)),
                         exception=traceback.format_exc(),
                         **camel_dict_to_snake_dict(e.response))
except botocore.exceptions.BotoCoreError as e:
    module.fail_json_aws(e, msg="Couldn't obtain frooble %s" % name)

API 限制(速率限制)和分页

对于返回大量结果的方法,boto3 通常提供分页器。 如果您调用的方法具有 NextTokenMarker 参数,您可能应该检查是否存在分页器(每个 boto3 服务参考页面的顶部都有一个指向“Paginators”的链接,如果该服务有的话)。要使用分页器,请获取一个分页器对象,使用适当的参数调用 paginator.paginate,然后调用 build_full_result

任何时候您大量调用 AWS API 时,都可能会遇到 API 限制,并且有一个 AWSRetry 装饰器可以用来确保退避。由于异常处理可能会干扰重试的正常工作(因为 AWSRetry 需要捕获限制异常才能正常工作),您需要提供一个退避函数,然后在退避函数周围进行异常处理。

您可以使用 exponential_backoffjittered_backoff 策略 - 有关更多详细信息,请参阅云 module_utils ()/lib/ansible/module_utils/cloud.py) 和 AWS 架构博客

这两种方法的组合如下:

@AWSRetry.jittered_backoff(retries=5, delay=5)
def describe_some_resource_with_backoff(client, **kwargs):
     paginator = client.get_paginator('describe_some_resource')
     return paginator.paginate(**kwargs).build_full_result()['SomeResource']

def describe_some_resource(client, module):
    filters = ansible_dict_to_boto3_filter_list(module.params['filters'])
    try:
        return describe_some_resource_with_backoff(client, Filters=filters)
    except botocore.exceptions.ClientError as e:
        module.fail_json_aws(e, msg="Could not describe some resource")

在 Ansible 2.10 之前,如果底层 describe_some_resources API 调用抛出 ResourceNotFound 异常,AWSRetry 会将其视为重试的提示,直到不再抛出此异常(这样在创建资源时,我们可以一直重试,直到它存在)。此默认值已更改,现在需要明确请求此行为。可以通过在装饰器上使用 catch_extra_error_codes 参数来完成。

@AWSRetry.jittered_backoff(retries=5, delay=5, catch_extra_error_codes=['ResourceNotFound'])
def describe_some_resource_retry_missing(client, **kwargs):
     return client.describe_some_resource(ResourceName=kwargs['name'])['Resources']

def describe_some_resource(client, module):
    name = module.params.get['name']
    try:
        return describe_some_resource_with_backoff(client, name=name)
    except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
        module.fail_json_aws(e, msg="Could not describe resource %s" % name)

为了更容易地使用 AWSRetry,现在可以将它包装在 AnsibleAWSModule 返回的客户端周围。从客户端进行的任何调用。要向客户端添加重试,请创建一个客户端

module.client('ec2', retry_decorator=AWSRetry.jittered_backoff(retries=10))

可以使用在调用时传递的装饰器来使用该客户端的任何调用,使用 aws_retry 参数。默认情况下,不使用重试。

ec2 = module.client('ec2', retry_decorator=AWSRetry.jittered_backoff(retries=10))
ec2.describe_instances(InstanceIds=['i-123456789'], aws_retry=True)

# equivalent with normal AWSRetry
@AWSRetry.jittered_backoff(retries=10)
def describe_instances(client, **kwargs):
    return ec2.describe_instances(**kwargs)

describe_instances(module.client('ec2'), InstanceIds=['i-123456789'])

该调用将重试指定的次数,因此调用函数无需包装在退避装饰器中。

您还可以使用模块参数自定义 AWSRetry.jittered_backoff API 使用的 retriesdelaymax_delay 参数。您可以查看 cloudformation <cloudformation_module> 模块作为示例。

为了使所有 Amazon 模块统一,请在模块参数前添加 backoff_ 前缀,因此 retries 变为 backoff_retries

同样,backoff_delaybackoff_max_delay 也是如此。

返回值

当您使用 boto3 进行调用时,您可能会获得一些有用的信息,您应该在模块中返回这些信息。除了与调用本身相关的信息外,您还将获得一些响应元数据。可以将其返回给用户,他们可能会觉得有用。

Boto3 以 CamelCase 形式返回大多数键。Ansible 采用 python 标准来命名变量和用法。有一个有用的辅助函数叫做 camel_dict_to_snake_dict,可以方便地将 boto3 响应转换为 snake_case。它位于 module_utils/common/dict_transformations 中。

您应该使用此辅助函数,并避免更改 Boto3 返回的值的名称。例如,如果 boto3 返回一个名为“SecretAccessKey”的值,请不要将其更改为“AccessKey”。

有一个可选参数 ignore_list,用于避免转换字典的子树。这对于标签特别有用,标签的键是区分大小写的。

# Make a call to AWS
resource = connection.aws_call()

# Convert resource response to snake_case
snaked_resource = camel_dict_to_snake_dict(resource, ignore_list=['Tags'])

# Return the resource details to the user without modifying tags
module.exit_json(changed=True, some_resource=snaked_resource)

注意:表示特定资源详细信息的返回键(上面的 some_resource)应该是资源名称的合理近似值。例如,ec2_volvolumeec2_vol_infovolumes

标签

标签应作为键值对的字典返回,其中每个键都是标签的键,值是标签的值。但是,应该注意的是,boto3 通常将标签作为字典列表返回。

module_utils/ec2.py 中有一个辅助函数叫做 boto3_tag_list_to_ansible_dict(在下面的“辅助函数”部分中详细讨论),它可以方便地将 boto3 返回的标签列表转换为模块要返回的所需标签字典。

下面是一个完整的示例,说明如何获取 AWS 调用的结果并返回预期值

# Make a call to AWS
result = connection.aws_call()

# Make result snake_case without modifying tags
snaked_result = camel_dict_to_snake_dict(result, ignore_list=['Tags'])

# Convert boto3 list of dict tags to just a dict of tags
snaked_result['tags'] = boto3_tag_list_to_ansible_dict(result.get('tags', []))

# Return the result to the user
module.exit_json(changed=True, **snaked_result)

信息模块

可以返回多个资源信息的 Info 模块应返回字典列表,其中每个字典都包含有关该特定资源的信息(例如,ec2_group_info 中的 security_groups)。

如果 _info 模块仅返回有关单个资源的信息(例如,ec2_tag_info),则应返回单个字典,而不是字典列表。

如果 _info 模块未返回任何实例,则应返回一个空列表“[]”。

返回的字典中的键应遵循上述指南并使用 snake_case。如果返回值可以用作其对应主模块的参数,则键应与参数名称本身或该参数的别名匹配。

以下是一个示例,说明了如何不正确地使用带有其各自主模块的示例信息模块

"security_groups": {
    {
        "description": "Created by ansible integration tests",
        "group_id": "sg-050dba5c3520cba71",
        "group_name": "ansible-test-87988625-unknown5c5f67f3ad09-icmp-1",
        "ip_permissions": [],
        "ip_permissions_egress": [],
        "owner_id": "721066863947",
        "tags": [
            {
                "Key": "Tag_One"
                "Value": "Tag_One_Value"
            },
        ],
        "vpc_id": "vpc-0cbc2380a326b8a0d"
    }
}

上面的示例输出显示了示例安全组信息模块中的一些错误:* security_groups 是一个字典的字典,而不是字典的列表。 * tags 似乎是直接从 boto3 返回的,因为它们是字典的列表。

以下是更正错误后的示例输出的外观。

"security_groups": [
    {
        "description": "Created by ansible integration tests",
        "group_id": "sg-050dba5c3520cba71",
        "group_name": "ansible-test-87988625-unknown5c5f67f3ad09-icmp-1",
        "ip_permissions": [],
        "ip_permissions_egress": [],
        "owner_id": "721066863947",
        "tags": {
            "Tag_One": "Tag_One_Value",
        },
        "vpc_id": "vpc-0cbc2380a326b8a0d"
    }
]

弃用返回值

如果需要更改当前返回值,则应**除了**现有键之外还返回新的/“正确”键,以保持与现有剧本的兼容性。应将弃用添加到被替换的返回值中,最初至少应在 2 年后,在某个月的 1 号放置。

例如

# Deprecate old `iam_user` return key to be replaced by `user` introduced on 2022-04-10
module.deprecate("The 'iam_user' return key is deprecated and will be replaced by 'user'. Both values are returned for now.",
                 date='2024-05-01', collection_name='community.aws')

处理 IAM JSON 策略

如果您的模块接受 IAM JSON 策略,请在模块规范中将类型设置为“json”。例如

argument_spec.update(
    dict(
        policy=dict(required=False, default=None, type='json'),
    )
)

请注意,AWS 不太可能以提交时的相同顺序返回策略。因此,请使用 compare_policies 辅助函数来处理此差异。

compare_policies 接受两个字典,递归排序并使它们可哈希以进行比较,如果它们不同,则返回 True。

from ansible_collections.amazon.aws.plugins.module_utils.iam import compare_policies

import json

# some lines skipped here

# Get the policy from AWS
current_policy = json.loads(aws_object.get_policy())
user_policy = json.loads(module.params.get('policy'))

# Compare the user submitted policy to the current policy ignoring order
if compare_policies(user_policy, current_policy):
    # Update the policy
    aws_object.set_policy(user_policy)
else:
    # Nothing to do
    pass

处理标签

AWS 有资源标签的概念。通常,boto3 API 有单独的调用来标记和取消标记资源。例如,EC2 API 具有 create_tagsdelete_tags 调用。

在添加标签支持时,Ansible AWS 模块应添加一个默认为 Nonetags 参数和一个默认为 Truepurge_tags 参数。

argument_spec.update(
    dict(
        tags=dict(type='dict', required=False, default=None),
        purge_tags=dict(type='bool', required=False, default=True),
    )
)

purge_tags 参数设置为 True **并且**在任务中显式设置了 tags 参数时,应删除 tags 中未显式设置的任何标签。

如果未设置 tags 参数,则不应修改标签,即使将 purge_tags 设置为 True 也是如此。这意味着删除所有标签需要在 Ansible 任务中将 tags 显式设置为一个空字典 {}

有一个辅助函数 compare_aws_tags,可以简化标签的处理。它比较两个字典,即当前的标签和期望的标签,并返回要设置的标签和要删除的标签。详情请参阅下面的“辅助函数”部分。

还有一个文档片段 amazon.aws.tags,在添加标签支持时应包含该片段。

辅助函数

除了 Ansible ec2.py module_utils 中的连接函数外,下面还详细介绍了一些其他有用的函数。

camel_dict_to_snake_dict

boto3 返回的结果是一个字典。字典的键采用 CamelCase 格式。为了与 Ansible 格式保持一致,此函数会将键转换为 snake_case 格式。

camel_dict_to_snake_dict 接受一个名为 ignore_list 的可选参数,该参数是一个不进行转换的键的列表(这通常对于 tags 字典很有用,该字典的子键应保持大小写不变)。

另一个可选参数是 reversible。默认情况下,HTTPEndpoint 会转换为 http_endpoint,然后 snake_dict_to_camel_dict 会将其转换为 HttpEndpoint。传递 reversible=True 会将 HTTPEndpoint 转换为 h_t_t_p_endpoint,然后转换回 HTTPEndpoint

snake_dict_to_camel_dict

snake_dict_to_camel_dict 将 snake_case 格式的键转换为 camel case 格式。默认情况下,由于最初是为 ECS 目的引入的,因此会转换为 dromedaryCase 格式。一个名为 capitalize_first 的可选参数(默认为 False)可用于转换为 CamelCase 格式。

ansible_dict_to_boto3_filter_list

将 Ansible 过滤器列表转换为 boto3 友好的字典列表。这对于任何 boto3 _facts 模块都很有用。

boto_exception

传递从 boto 或 boto3 返回的异常,此函数将始终从异常中获取消息。

已弃用:请改用 AnsibleAWSModulefail_json_aws

boto3_tag_list_to_ansible_dict

将 boto3 标签列表转换为 Ansible 字典。默认情况下,Boto3 将标签作为包含名为“Key”和“Value”的键的字典列表返回。调用函数时可以覆盖此键名。例如,如果您已将标签列表转换为 camel_case 格式,您可能希望传递小写键名,即“key”和“value”。

此函数将列表转换为单个字典,其中字典键是标签键,字典值是标签值。

ansible_dict_to_boto3_tag_list

与上述相反。将 Ansible 字典转换为 boto3 字典标签列表。如果“Key”和“Value”不合适,您可以再次覆盖使用的键名。

get_ec2_security_group_ids_from_names

将安全组名称列表或安全组名称和 ID 的组合传递给此函数,此函数将返回 ID 列表。如果已知 VPC ID,您还应传递 VPC ID,因为安全组名称在不同的 VPC 中不一定是唯一的。

compare_policies

传递两个策略字典以检查是否存在任何有意义的差异,如果有,则返回 true。这会在比较之前递归排序字典并使其可哈希。

任何比较策略的时候都应使用此方法,这样,顺序的改变不会导致不必要的更改。

compare_aws_tags

传递两个标签字典和一个可选的 purge 参数,此函数将返回一个包含您需要修改的键值对的字典和一个您需要删除的标签键名列表。默认情况下,Purge 为 True。如果 purge 为 False,则不会修改任何现有标签。

在使用 boto3 add_tagsremove_tags 函数时,此函数很有用。请务必在使用其他辅助函数 boto3_tag_list_to_ansible_dict 之前获取适当的标签字典。由于 AWS API 并非统一(例如,EC2 与 Lambda 不同),因此此函数适用于某些(Lambda),而其他一些则可能需要在使用这些值之前进行修改(例如,EC2 需要取消设置的标签采用 [{'Key': key1}, {'Key': key2}] 的形式)。

AWS 模块的集成测试

所有新的 AWS 模块都应包含集成测试,以确保检测到 AWS API 中影响模块的任何更改。至少,这应涵盖关键的 API 调用,并检查模块结果中是否存在文档化的返回值。

有关运行集成测试的常规信息,请参阅模块开发指南的集成测试页面,特别是关于云测试配置的部分。

应将您的模块的集成测试添加到 test/integration/targets/MODULE_NAME 中。

您还必须在 test/integration/targets/MODULE_NAME/aliases 中有一个别名文件。此文件有两个用途。首先,它表示它在 AWS 测试中,导致测试框架在测试运行期间提供 AWS 凭据。其次,将测试放入测试组中,导致它在持续集成构建中运行。

新模块的测试应添加到 cloud/aws 组。通常,只需复制一个现有的别名文件,例如aws_s3 测试别名文件

集成测试的自定义 SDK 版本

默认情况下,集成测试将针对最早支持的 AWS SDK 版本运行。当前支持的版本可以在 tests/integration/constraints.txt 中找到,并且不应更新。如果模块需要访问更高版本的 SDK,可以通过依赖 setup_botocore_pip 角色并在您的测试的 meta/main.yml 文件中设置 botocore_version 变量来安装此 SDK。

dependencies:
  - role: setup_botocore_pip
    vars:
      botocore_version: "1.20.24"

在集成测试中创建 EC2 实例

启动时,集成测试将以额外的变量的形式传递 aws_region。创建的任何资源都应在此区域中创建,包括 EC2 实例。由于 AMI 是特定于区域的,因此可以包含一个角色来查询 API 以获取要使用的 AMI 并设置 ec2_ami_id 事实。可以通过在您的测试的 meta/main.yml 文件中添加 setup_ec2_facts 角色作为依赖项来包含此角色。

dependencies:
  - role: setup_ec2_facts

然后可以在测试中使用 ec2_ami_id 事实。

- name: Create launch configuration 1
  community.aws.ec2_lc:
    name: '{{ resource_prefix }}-lc1'
    image_id: '{{ ec2_ami_id }}'
    assign_public_ip: yes
    instance_type: '{{ ec2_instance_type }}'
    security_groups: '{{ sg.group_id }}'
    volumes:
      - device_name: /dev/xvda
        volume_size: 10
        volume_type: gp2
        delete_on_termination: true

为了提高跨区域的测试结果可重复性,测试应使用此角色及其提供的事实来选择要使用的 AMI。

集成测试中的资源命名

AWS 对资源名称有一系列限制。在可能的情况下,资源名称应包含一个字符串,使资源名称对于测试是唯一的。

用于运行集成测试的 ansible-test 工具提供了两个有用的额外变量:resource_prefixtiny_prefix,它们对于测试集是唯一的,并且通常应作为名称的一部分使用。resource_prefix 将根据运行测试的主机生成前缀。有时,这可能会导致资源名称超出 AWS 允许的字符限制。在这些情况下,tiny_prefix 将提供一个 12 个字符的随机生成的前缀。

集成测试的 AWS 凭据

测试框架处理使用适当的 AWS 凭据运行测试,这些凭据在以下变量中提供给您的测试

  • aws_region

  • aws_access_key

  • aws_secret_key

  • security_token

因此,测试中 AWS 模块的所有调用都应设置这些参数。为了避免为每个调用重复这些参数,最好使用module_defaults。例如

- name: set connection information for aws modules and run tasks
  module_defaults:
    group/aws:
      aws_access_key: "{{ aws_access_key }}"
      aws_secret_key: "{{ aws_secret_key }}"
      security_token: "{{ security_token | default(omit) }}"
      region: "{{ aws_region }}"

  block:

  - name: Do Something
    ec2_instance:
      ... params ...

  - name: Do Something Else
    ec2_instance:
      ... params ...

集成测试的 AWS 权限

正如集成测试指南中所述,mattclay/aws-terminator 中定义了 IAM 策略,其中包含运行 AWS 集成测试所需的必要权限。

如果您的模块与新服务交互或需要新的权限,当您提交拉取请求时,测试将失败,并且 Ansibullbot 会将您的 PR 标记为需要修订。我们不会自动向持续集成构建使用的角色授予额外的权限。您需要针对 mattclay/aws-terminator 提交拉取请求以添加这些权限。

如果您的 PR 有测试失败,请仔细检查以确保失败仅是由于缺少权限导致的。如果您排除了其他失败原因,请添加带有 ready_for_review 标签的评论,并说明这是由于缺少权限导致的。

在测试通过之前,您的拉取请求无法合并。如果您的拉取请求由于缺少权限而失败,您必须收集运行测试所需的最低 IAM 权限。

有两种方法可以确定您的 PR 需要哪些 IAM 权限才能通过

  • 从最宽松的 IAM 策略开始,运行测试以收集有关您的测试实际使用的资源的信息,然后根据该输出来构建策略。此方法仅适用于使用 AnsibleAWSModule 的模块。

  • 从最严格的 IAM 策略开始,运行测试以发现失败,添加解决该失败的资源的权限,然后重复。如果您的模块使用 AnsibleModule 而不是 AnsibleAWSModule,则必须使用此方法。

要从最宽松的 IAM 策略开始

  1. 创建一个 IAM 策略,该策略允许所有操作(将 ActionResource 设置为 *)。

  2. 使用此策略在本地运行测试。在基于 AnsibleAWSModule 的模块上,debug_botocore_endpoint_logs 选项会自动设置为 yes,因此您应该在 PLAY RECAP 后看到一个 AWS ACTIONS 列表,其中显示了所有使用的权限。如果您的测试使用 boto/AnsibleModule 模块,则必须从最严格的策略开始(请参见下文)。

  3. 修改您的策略以仅允许您的测试使用的操作。尽可能限制帐户、区域和前缀。等待几分钟以使您的策略更新。

  4. 再次使用仅允许新策略的用户或角色运行测试。

  5. 如果测试失败,请进行故障排除(请参阅下面的提示),修改策略,再次运行测试,并重复此过程,直到测试在限制性策略下通过。

  6. 打开一个拉取请求,建议将所需的最低策略提交到 CI 策略

要从最严格的 IAM 策略开始

  1. 在本地运行没有 IAM 权限的集成测试。

  2. 检查测试失败时的错误。
    1. 如果错误消息指示请求中使用的操作,请将该操作添加到您的策略中。

    2. 如果错误消息未指示请求中使用的操作
      • 通常,该操作是方法名称的 CamelCase 版本 - 例如,对于 ec2 客户端,方法 describe_security_groups 对应于操作 ec2:DescribeSecurityGroups

      • 参考文档以识别该操作。

    3. 如果错误消息指示请求中使用的资源 ARN,请将该操作限制为该资源。

    4. 如果错误消息未指示使用的资源 ARN
      • 通过检查文档来确定是否可以将该操作限制为资源。

      • 如果可以限制该操作,请使用文档构造 ARN 并将其添加到策略中。

  3. 将导致失败的操作或资源添加到 IAM 策略。等待几分钟以使您的策略更新。

  4. 再次运行测试,并将此策略附加到您的用户或角色。

  5. 如果测试仍然在同一位置失败,并出现相同的错误,则您需要进行故障排除(请参阅下面的提示)。如果第一个测试通过,请针对下一个错误重复步骤 2 和 3。重复此过程,直到测试在限制性策略下通过。

  6. 打开一个拉取请求,建议将所需的最低策略提交到 CI 策略

IAM 策略的故障排除

  • 当您对策略进行更改时,请等待几分钟以使策略更新,然后再重新运行测试。

  • 使用策略模拟器来验证您的策略中的每个操作(在适用时受资源限制)是否被允许。

  • 如果您将操作限制为某些资源,请暂时将资源替换为 *。如果测试在使用通配符资源的情况下通过,则说明您的策略中的资源定义存在问题。

  • 如果上面的初始故障排除没有提供更多见解,则 AWS 可能正在使用其他未公开的资源和操作。

  • 检查该服务的 AWS FullAccess 策略以寻找线索。

  • 重新阅读 AWS 文档,尤其是各种 AWS 服务的操作、资源和条件键列表。

  • 查看 cloudonaut 文档作为故障排除的交叉参考。

  • 使用搜索引擎。

  • 在 #ansible-aws 聊天频道中提问(在 ansible.im 上使用 Matrix 或在 irc.libera.chat 上使用 IRC)。

不支持的集成测试

在 CI 中为模块运行集成测试可能不切实际的原因有很多。如果这些原因适用,您应该将关键字 unsupported 添加到 test/integration/targets/MODULE_NAME/aliases 中的别名文件中。

应将测试标记为不支持的一些情况:1) 测试完成时间超过 10 或 15 分钟 2) 测试创建昂贵的资源 3) 测试创建内联策略 4) 测试需要存在外部资源 5) 测试管理帐户级别的安全策略,例如密码策略或 AWS Organizations。

如果这些原因之一适用,您应该打开一个拉取请求,建议将所需的最低策略提交到 不支持的测试策略

CI 不会自动运行不支持的集成测试。但是,应该提供必要的策略,以便执行 PR 审核或编写补丁的人员可以手动运行测试。

AWS 插件的单元测试

当有功能测试时,为什么我们需要单元测试

单元测试更快,更适合测试极端情况。它们也不依赖于第三方服务,因此失败不太可能是误报。

如何保持我的代码简单?

理想情况下,您应该将代码分解为小的函数。每个函数应该具有有限数量的参数,并且与代码的其余部分(低耦合)的交叉依赖性较低。

  • 如果函数只使用一个字段,请不要将大型数据结构传递给该函数。这可以阐明函数的输入(约定),还可以降低函数内部意外转换数据结构的风险。

  • boto 客户端对象很复杂,并且可能是意外副作用的来源。最好将调用隔离在专用函数中。这些函数将有自己的单元测试。

  • 当您只需要从 module.params 读取几个参数时,请不要传递 module 对象。将参数直接传递给您的函数。通过这样做,您可以明确函数的输入(约定),并减少潜在的副作用。

单元测试准则

理想情况下,所有 module_utils 都应该被单元测试覆盖。但是,我们承认编写单元测试可能具有挑战性,我们也接受没有单元测试的贡献。一般来说,建议进行单元测试,并且很可能会加快 PR 审核的速度。

  • 我们的测试使用 pytest 运行,我们使用它提供的功能,例如 Fixture、参数化。

  • 为了保持一致性和简洁性,不鼓励使用 unittest.TestCase

  • 单元测试应该在没有任何网络连接的情况下正常运行。

  • 没有必要模拟所有的 boto3/botocore 调用(get_paginator()paginate() 等)。通常最好设置一个包装这些调用的函数并模拟结果。

  • 简洁至上。测试应该简短,并覆盖有限的功能集。

Pytest 有很好的文档,你可以在它的使用指南中找到一些示例。

如何运行我的单元测试

在我们的 CI 中,测试是通过 ansible-test 完成的。您可以使用以下命令在本地运行测试

$ ansible-test units --docker

我们还提供了一个 tox 配置,它允许您更快地运行一个特定的测试。在此示例中,我们专注于 s3_object 模块的测试

$ tox -e py3 -- tests/unit/plugins/modules/test_s3_object.py

代码格式化

为了提高代码的一致性,我们使用了一些格式化工具和 linter。 这些工具可以使用 tox 在本地运行

$ tox -m format
$ tox -m lint

有关我们使用的每个工具的更多信息,可以在它们的网站上找到

  • black - 有主见的(opinionated)代码格式化工具。

  • isort - 对导入进行分组和排序。

  • flynt - 鼓励使用 f-strings 而不是诸如拼接,%str.format()string.Template 之类的替代方法。

  • flake8 - 鼓励遵循 PEP8 建议。

  • pylint - 一个静态代码分析工具。