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的更改应避免,应仅在主要版本中进行,并且在实际情况下应至少在完整的主要版本之前进行弃用周期。弃用可以回传到稳定分支。

例如:- 在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)的暂存位置,可能包含重大更改。

一般回传策略

  • 新功能、弃用和次要更改可以回传到最新的稳定版本。

  • 错误修复可以回传到最新的两个稳定版本。

  • 安全修复应至少回传到最新的两个稳定版本。

如有必要,可以将其他与CI相关的更改引入较旧的稳定分支,以确保CI继续运行。

回传PR最简单的机制是向PR添加backport-Y标签。PR合并后,patchback机器人将尝试自动创建回传PR。

创建新的AWS模块

编写新模块时,务必考虑模块的范围。一般来说,尝试做好一件事。

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

虽然可以编写一个s3模块来管理所有与S3相关的事务,但彻底测试和维护这样一个模块非常困难。类似地,虽然可以编写一个模块来管理基本的EC2安全组资源,以及另一个模块来管理安全组规则,但这与模块用户的预期相悖。

没有绝对正确的答案,但思考这个问题很重要,亚马逊在设计其API时通常已经为你完成了这项工作。

模块命名

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

除非某个名称是AWS主要组件的常用缩写(例如,VPC或ELB),否则避免进一步缩写名称,并且不要自行创建新的缩写。

如果AWS API主要管理单个资源,则管理此资源的模块可以仅命名为API的名称。但是,如果亚马逊使用这些名称来指代它们,请考虑使用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模块支持使用module_defaults用于常见的身份验证参数。要为你的新模块执行相同的操作,请在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 服务参考页的顶部都有一个指向分页器的链接,如果服务有任何分页器的话)。要使用分页器,请获取分页器对象,使用适当的参数调用 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 以驼峰式命名法返回大多数键。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_vol 使用 volume,对于 ec2_vol_info 使用 volumes

标签

标签应作为键值对的字典返回,其中每个键都是标签的键,值是标签的值。但是,需要注意的是,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)

信息模块

可以返回有关多个资源的信息的信息模块应返回字典列表,每个字典包含有关该特定资源的信息(即 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 年后,在一个月的第一天添加。

例如

# 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以字典的形式返回结果。字典的键采用驼峰式命名法。为了保持Ansible的格式,此函数会将键转换为蛇形命名法。

camel_dict_to_snake_dict带有一个可选参数ignore_list,它是一个不需要转换的键列表(这通常对tags字典很有用,其子键应保持大小写不变)。

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

snake_dict_to_camel_dict

snake_dict_to_camel_dict将蛇形命名法的键转换为驼峰式命名法。默认情况下,因为它最初是为ECS目的而引入的,所以它转换为驼峰式命名法(首字母小写)。一个名为capitalize_first的可选参数(默认为False)可用于转换为帕斯卡命名法(首字母大写)。

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”的键的字典列表返回。调用函数时可以覆盖这些键名。例如,如果您已经将标签列表转换为驼峰式命名法,则可能需要改为传递小写键名,即“key”和“value”。

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

ansible_dict_to_boto3_tag_list

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

get_ec2_security_group_ids_from_names

将安全组名称或安全组名称和ID的组合列表传递给此函数,此函数将返回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变量来安装。

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中定义了包含运行AWS集成测试所需权限的IAM策略。

如果您的模块与新的服务交互或需要新的权限,则在您提交拉取请求时测试将失败,并且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. 如果错误消息没有指示请求中使用的操作
      • 通常,操作是方法名称的驼峰式版本——例如,对于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 irc.libera.chat)。

不受支持的集成测试

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

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

如果存在这些原因之一,则应打开一个拉取请求,将所需的最小策略建议给不受支持的测试策略

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

代码格式

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

$ tox -m format
$ tox -m lint

有关我们使用的每个工具的更多信息,请访问其网站。

  • black - 代码格式化工具。

  • isort - 用于分组和排序导入的工具。

  • flynt - 鼓励使用f-string而不是其他方法,例如拼接,%str.format()string.Template

  • flake8 - 鼓励遵循PEP8规范。

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