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的名称。但是,如果亚马逊使用这些名称来指代它们,请考虑使用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模块支持使用module_defaults用于常见的身份验证参数。要为你的新模块执行相同的操作,请在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 服务参考页的顶部都有一个指向分页器的链接,如果服务有任何分页器的话)。要使用分页器,请获取分页器对象,使用适当的参数调用 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 以驼峰式命名法返回大多数键。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
。
信息模块
可以返回有关多个资源的信息的信息模块应返回字典列表,每个字典包含有关该特定资源的信息(即 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
辅助函数
除了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=True
将HTTPEndpoint
转换为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返回的异常,此函数将始终从异常中获取消息。
已弃用:改用AnsibleAWSModule
的fail_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。此函数递归地对字典进行排序并使其在比较前可哈希。
比较策略时应始终使用此方法,以便顺序的更改不会导致不必要的更改。
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_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中定义了包含运行AWS集成测试所需权限的IAM策略。
如果您的模块与新的服务交互或需要新的权限,则在您提交拉取请求时测试将失败,并且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权限。
- 检查测试失败时的错误。
如果错误消息指示请求中使用的操作,请将该操作添加到您的策略中。
- 如果错误消息没有指示请求中使用的操作
通常,操作是方法名称的驼峰式版本——例如,对于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 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
有关我们使用的每个工具的更多信息,请访问其网站。