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 使用这些名称来引用它们,请考虑使用 instance
或 cluster
来提高清晰度。
示例
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
模块行为
为了减少添加新功能时发生破坏性更改的可能性,当任务中未显式设置参数时,该模块应避免修改资源属性。
按照惯例,当在任务中显式设置参数时,该模块应将资源属性设置为与任务中设置的值匹配。在某些情况下,例如标签或关联,添加一个额外的参数(可以设置为将行为从替换更改为添加)可能会有所帮助。但是,默认行为仍然应该是替换而不是添加。
有关 tags
和 purge_tags
的示例,请参见“处理标签<ansible_collections.amazon.aws.docsite.dev_tags>”部分。
连接到 AWS
AnsibleAWSModule 提供了 resource
和 client
辅助方法来获取 boto3 连接。这些方法处理一些比较深奥的连接选项,例如安全令牌和 boto 配置文件。
如果使用基本的 AnsibleModule,则应使用 get_aws_connection_info
,然后使用 boto3_conn
连接到 AWS,因为它们处理相同范围的连接选项。
这些辅助方法还会检查是否缺少配置文件或未设置区域(当需要时),因此您无需执行此操作。
下面显示了连接到 EC2 的示例。请注意,与 boto 不同,这里没有像 boto 中那样的 NoAuthHandlerFound
异常处理。相反,当您使用连接时,会引发 AuthFailure
异常。为了确保捕获授权、参数验证和权限错误,您应该使用每个 boto3 连接调用捕获 ClientError
和 BotoCoreError
异常。请参见异常处理。
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 通常提供分页器。 如果您调用的方法具有 NextToken
或 Marker
参数,您可能应该检查是否存在分页器(每个 boto3 服务参考页面的顶部都有一个指向“Paginators”的链接,如果该服务有的话)。要使用分页器,请获取一个分页器对象,使用适当的参数调用 paginator.paginate
,然后调用 build_full_result
。
任何时候您大量调用 AWS API 时,都可能会遇到 API 限制,并且有一个 AWSRetry
装饰器可以用来确保退避。由于异常处理可能会干扰重试的正常工作(因为 AWSRetry 需要捕获限制异常才能正常工作),您需要提供一个退避函数,然后在退避函数周围进行异常处理。
您可以使用 exponential_backoff
或 jittered_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 使用的 retries
、delay
和 max_delay
参数。您可以查看 cloudformation <cloudformation_module>
模块作为示例。
- 为了使所有 Amazon 模块统一,请在模块参数前添加
backoff_
前缀,因此retries
变为backoff_retries
同样,
backoff_delay
和backoff_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_vol
的 volume
,ec2_vol_info
的 volumes
。
信息模块
可以返回多个资源信息的 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
辅助函数
除了 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 返回的异常,此函数将始终从异常中获取消息。
已弃用:请改用 AnsibleAWSModule
的 fail_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。这会在比较之前递归排序字典并使其可哈希。
任何比较策略的时候都应使用此方法,这样,顺序的改变不会导致不必要的更改。
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_prefix
和 tiny_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 策略开始
创建一个 IAM 策略,该策略允许所有操作(将
Action
和Resource
设置为*
)。使用此策略在本地运行测试。在基于 AnsibleAWSModule 的模块上,
debug_botocore_endpoint_logs
选项会自动设置为yes
,因此您应该在 PLAY RECAP 后看到一个 AWS ACTIONS 列表,其中显示了所有使用的权限。如果您的测试使用 boto/AnsibleModule 模块,则必须从最严格的策略开始(请参见下文)。修改您的策略以仅允许您的测试使用的操作。尽可能限制帐户、区域和前缀。等待几分钟以使您的策略更新。
再次使用仅允许新策略的用户或角色运行测试。
如果测试失败,请进行故障排除(请参阅下面的提示),修改策略,再次运行测试,并重复此过程,直到测试在限制性策略下通过。
打开一个拉取请求,建议将所需的最低策略提交到 CI 策略。
要从最严格的 IAM 策略开始
在本地运行没有 IAM 权限的集成测试。
- 检查测试失败时的错误。
如果错误消息指示请求中使用的操作,请将该操作添加到您的策略中。
- 如果错误消息未指示请求中使用的操作
通常,该操作是方法名称的 CamelCase 版本 - 例如,对于 ec2 客户端,方法
describe_security_groups
对应于操作ec2:DescribeSecurityGroups
。参考文档以识别该操作。
如果错误消息指示请求中使用的资源 ARN,请将该操作限制为该资源。
- 如果错误消息未指示使用的资源 ARN
通过检查文档来确定是否可以将该操作限制为资源。
如果可以限制该操作,请使用文档构造 ARN 并将其添加到策略中。
将导致失败的操作或资源添加到 IAM 策略。等待几分钟以使您的策略更新。
再次运行测试,并将此策略附加到您的用户或角色。
如果测试仍然在同一位置失败,并出现相同的错误,则您需要进行故障排除(请参阅下面的提示)。如果第一个测试通过,请针对下一个错误重复步骤 2 和 3。重复此过程,直到测试在限制性策略下通过。
打开一个拉取请求,建议将所需的最低策略提交到 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
有关我们使用的每个工具的更多信息,可以在它们的网站上找到