[aws]Add VPC configuration to ECS modules (#34381)

Enable awsvpc network mode for ECS services and tasks and
their underlying task definitions

Improve test suite to thoroughly test the changes

Use runme.sh technique to run old and new versions of botocore to
ensure that the modules work with older botocore and older network modes
and fail gracefully if awsvpc network mode is used with older botocore
This commit is contained in:
Will Thames 2018-04-26 05:41:04 +10:00 committed by Ryan Brown
parent 58bf4ae611
commit 12f2b9506d
12 changed files with 639 additions and 73 deletions

View file

@ -70,7 +70,7 @@ options:
role:
description:
- The name or full Amazon Resource Name (ARN) of the IAM role that allows your Amazon ECS container agent to make calls to your load balancer
on your behalf. This parameter is only required if you are using a load balancer with your service.
on your behalf. This parameter is only required if you are using a load balancer with your service, in a network mode other than `awsvpc`.
required: false
delay:
description:
@ -97,6 +97,12 @@ options:
- The placement strategy objects to use for tasks in your service. You can specify a maximum of 5 strategy rules per service
required: false
version_added: 2.4
network_configuration:
description:
- network configuration of the service. Only applicable for task definitions created with C(awsvpc) I(network_mode).
- I(network_configuration) has two keys, I(subnets), a list of subnet IDs to which the task is attached and I(security_groups),
a list of group names or group IDs for the task
version_added: 2.6
extends_documentation_fragment:
- aws
- ec2
@ -117,6 +123,20 @@ EXAMPLES = '''
state: present
cluster: new_cluster
- name: create ECS service on VPC network
ecs_service:
state: present
name: console-test-service
cluster: new_cluster
task_definition: 'new_cluster-task:1'
desired_count: 0
network_configuration:
subnets:
- subnet-abcd1234
security_groups:
- sg-aaaa1111
- my_security_group
# Simple example to delete
- ecs_service:
name: default
@ -265,15 +285,14 @@ DEPLOYMENT_CONFIGURATION_TYPE_MAP = {
'minimum_healthy_percent': 'int'
}
from ansible.module_utils.aws.core import AnsibleAWSModule
from ansible.module_utils.ec2 import ec2_argument_spec
from ansible.module_utils.ec2 import snake_dict_to_camel_dict, map_complex_type, get_ec2_security_group_ids_from_names
try:
import botocore
HAS_BOTO3 = True
except ImportError:
HAS_BOTO3 = False
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.ec2 import boto3_conn, ec2_argument_spec, get_aws_connection_info, snake_dict_to_camel_dict, map_complex_type
pass # handled by AnsibleAWSModule
class EcsServiceManager:
@ -281,9 +300,25 @@ class EcsServiceManager:
def __init__(self, module):
self.module = module
self.ecs = module.client('ecs')
self.ec2 = module.client('ec2')
region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True)
self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs)
def format_network_configuration(self, network_config):
result = dict()
if 'subnets' in network_config:
result['subnets'] = network_config['subnets']
else:
self.module.fail_json(msg="Network configuration must include subnets")
if 'security_groups' in network_config:
groups = network_config['security_groups']
if any(not sg.startswith('sg-') for sg in groups):
try:
vpc_id = self.ec2.describe_subnets(SubnetIds=[result['subnets'][0]])['Subnets'][0]['VpcId']
groups = get_ec2_security_group_ids_from_names(groups, self.ec2, vpc_id)
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
self.module.fail_json_aws(e, msg="Couldn't look up security groups")
result['securityGroups'] = groups
return dict(awsvpcConfiguration=result)
def find_in_array(self, array_of_services, service_name, field_name='serviceArn'):
for c in array_of_services:
@ -322,8 +357,8 @@ class EcsServiceManager:
def create_service(self, service_name, cluster_name, task_definition, load_balancers,
desired_count, client_token, role, deployment_configuration,
placement_constraints, placement_strategy):
response = self.ecs.create_service(
placement_constraints, placement_strategy, network_configuration):
params = dict(
cluster=cluster_name,
serviceName=service_name,
taskDefinition=task_definition,
@ -334,21 +369,51 @@ class EcsServiceManager:
deploymentConfiguration=deployment_configuration,
placementConstraints=placement_constraints,
placementStrategy=placement_strategy)
return response['service']
if network_configuration:
params['networkConfiguration'] = network_configuration
response = self.ecs.create_service(**params)
return self.jsonize(response['service'])
def update_service(self, service_name, cluster_name, task_definition,
desired_count, deployment_configuration):
response = self.ecs.update_service(
desired_count, deployment_configuration, network_configuration):
params = dict(
cluster=cluster_name,
service=service_name,
taskDefinition=task_definition,
desiredCount=desired_count,
deploymentConfiguration=deployment_configuration)
return response['service']
if network_configuration:
params['networkConfiguration'] = network_configuration
response = self.ecs.update_service(**params)
return self.jsonize(response['service'])
def jsonize(self, service):
# some fields are datetime which is not JSON serializable
# make them strings
if 'createdAt' in service:
service['createdAt'] = str(service['createdAt'])
if 'deployments' in service:
for d in service['deployments']:
if 'createdAt' in d:
d['createdAt'] = str(d['createdAt'])
if 'updatedAt' in d:
d['updatedAt'] = str(d['updatedAt'])
if 'events' in service:
for e in service['events']:
if 'createdAt' in e:
e['createdAt'] = str(e['createdAt'])
return service
def delete_service(self, service, cluster=None):
return self.ecs.delete_service(cluster=cluster, service=service)
def ecs_api_handles_network_configuration(self):
from distutils.version import LooseVersion
# There doesn't seem to be a nice way to inspect botocore to look
# for attributes (and networkConfiguration is not an explicit argument
# to e.g. ecs.run_task, it's just passed as a keyword argument)
return LooseVersion(botocore.__version__) >= LooseVersion('1.7.44')
def main():
argument_spec = ec2_argument_spec()
@ -365,21 +430,22 @@ def main():
repeat=dict(required=False, type='int', default=10),
deployment_configuration=dict(required=False, default={}, type='dict'),
placement_constraints=dict(required=False, default=[], type='list'),
placement_strategy=dict(required=False, default=[], type='list')
placement_strategy=dict(required=False, default=[], type='list'),
network_configuration=dict(required=False, type='dict')
))
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True,
required_if=[
('state', 'present', ['task_definition', 'desired_count'])
],
required_together=[['load_balancers', 'role']]
)
if not HAS_BOTO3:
module.fail_json(msg='boto3 is required.')
module = AnsibleAWSModule(argument_spec=argument_spec,
supports_check_mode=True,
required_if=[('state', 'present', ['task_definition', 'desired_count'])],
required_together=[['load_balancers', 'role']])
service_mgr = EcsServiceManager(module)
if module.params['network_configuration']:
if not service_mgr.ecs_api_handles_network_configuration():
module.fail_json(msg='botocore needs to be version 1.7.44 or higher to use network configuration')
network_configuration = service_mgr.format_network_configuration(module.params['network_configuration'])
else:
network_configuration = None
deployment_configuration = map_complex_type(module.params['deployment_configuration'],
DEPLOYMENT_CONFIGURATION_TYPE_MAP)
@ -418,7 +484,8 @@ def main():
module.params['cluster'],
module.params['task_definition'],
module.params['desired_count'],
deploymentConfiguration)
deploymentConfiguration,
network_configuration)
else:
for loadBalancer in loadBalancers:
if 'containerPort' in loadBalancer:
@ -433,7 +500,8 @@ def main():
role,
deploymentConfiguration,
module.params['placement_constraints'],
module.params['placement_strategy'])
module.params['placement_strategy'],
network_configuration)
results['service'] = response