From 12f2b9506d1a38af966da98b35fbc86a58bf477f Mon Sep 17 00:00:00 2001 From: Will Thames Date: Thu, 26 Apr 2018 05:41:04 +1000 Subject: [PATCH] [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 --- .../testing_policies/ecs-policy.json | 12 +- .../modules/cloud/amazon/ecs_service.py | 122 +++++-- lib/ansible/modules/cloud/amazon/ecs_task.py | 80 +++-- .../cloud/amazon/ecs_taskdefinition.py | 10 +- .../ecs_cluster/playbooks/full_test.yml | 5 + .../ecs_cluster/playbooks/network_fail.yml | 146 +++++++++ .../roles/ecs_cluster}/defaults/main.yml | 2 +- .../ecs_cluster}/files/ec2-trust-policy.json | 0 .../ecs_cluster}/files/ecs-trust-policy.json | 0 .../roles/ecs_cluster}/meta/main.yml | 0 .../roles/ecs_cluster}/tasks/main.yml | 310 ++++++++++++++++-- test/integration/targets/ecs_cluster/runme.sh | 25 ++ 12 files changed, 639 insertions(+), 73 deletions(-) create mode 100644 test/integration/targets/ecs_cluster/playbooks/full_test.yml create mode 100644 test/integration/targets/ecs_cluster/playbooks/network_fail.yml rename test/integration/targets/ecs_cluster/{ => playbooks/roles/ecs_cluster}/defaults/main.yml (95%) rename test/integration/targets/ecs_cluster/{ => playbooks/roles/ecs_cluster}/files/ec2-trust-policy.json (100%) rename test/integration/targets/ecs_cluster/{ => playbooks/roles/ecs_cluster}/files/ecs-trust-policy.json (100%) rename test/integration/targets/ecs_cluster/{ => playbooks/roles/ecs_cluster}/meta/main.yml (100%) rename test/integration/targets/ecs_cluster/{ => playbooks/roles/ecs_cluster}/tasks/main.yml (53%) create mode 100755 test/integration/targets/ecs_cluster/runme.sh diff --git a/hacking/aws_config/testing_policies/ecs-policy.json b/hacking/aws_config/testing_policies/ecs-policy.json index 57412ab13f7..19db32c8aef 100644 --- a/hacking/aws_config/testing_policies/ecs-policy.json +++ b/hacking/aws_config/testing_policies/ecs-policy.json @@ -32,11 +32,15 @@ "application-autoscaling:RegisterScalableTarget", "cloudwatch:DescribeAlarms", "cloudwatch:PutMetricAlarm", - "ecs:List*", - "ecs:Describe*", "ecs:CreateCluster", - "ecs:DeleteCluster", "ecs:CreateService", + "ecs:DeleteCluster", + "ecs:DeleteService", + "ecs:DeregisterTaskDefinition", + "ecs:Describe*", + "ecs:List*", + "ecs:RegisterTaskDefinition", + "ecs:RunTask", "ecs:UpdateService", "elasticloadbalancing:Describe*", "iam:AttachRolePolicy", @@ -45,8 +49,8 @@ "iam:GetPolicyVersion", "iam:GetRole", "iam:ListAttachedRolePolicies", - "iam:ListRoles", "iam:ListGroups", + "iam:ListRoles", "iam:ListUsers" ], "Resource": [ diff --git a/lib/ansible/modules/cloud/amazon/ecs_service.py b/lib/ansible/modules/cloud/amazon/ecs_service.py index 4090a81495c..c1726be77a0 100644 --- a/lib/ansible/modules/cloud/amazon/ecs_service.py +++ b/lib/ansible/modules/cloud/amazon/ecs_service.py @@ -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 diff --git a/lib/ansible/modules/cloud/amazon/ecs_task.py b/lib/ansible/modules/cloud/amazon/ecs_task.py index 80c6b69c4e3..9446b81e23d 100644 --- a/lib/ansible/modules/cloud/amazon/ecs_task.py +++ b/lib/ansible/modules/cloud/amazon/ecs_task.py @@ -54,6 +54,12 @@ options: description: - A value showing who or what started the task (for informational purposes) required: False + 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 @@ -81,6 +87,12 @@ EXAMPLES = ''' container_instances: - arn:aws:ecs:us-west-2:172139249013:container-instance/79c23f22-876c-438a-bddf-55c98a3538a8 started_by: ansible_user + network_configuration: + subnets: + - subnet-abcd1234 + security_groups: + - sg-aaaa1111 + - my_security_group register: task_output - name: Stop a task @@ -150,14 +162,13 @@ task: type: string ''' +from ansible.module_utils.aws.core import AnsibleAWSModule +from ansible.module_utils.ec2 import ec2_argument_spec, 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 + pass # handled by AnsibleAWSModule class EcsExecManager: @@ -165,9 +176,25 @@ class EcsExecManager: 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 list_tasks(self, cluster_name, service_name, status): response = self.ecs.list_tasks( @@ -184,12 +211,14 @@ class EcsExecManager: def run_task(self, cluster, task_definition, overrides, count, startedBy): if overrides is None: overrides = dict() - response = self.ecs.run_task( - cluster=cluster, - taskDefinition=task_definition, - overrides=overrides, - count=count, - startedBy=startedBy) + params = dict(cluster=cluster, taskDefinition=task_definition, + overrides=overrides, count=count, startedBy=startedBy) + if self.module.params['network_configuration']: + params['networkConfiguration'] = self.format_network_configuration(self.module.params['network_configuration']) + try: + response = self.ecs.run_task(**params) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + self.module.fail_json_aws(e, msg="Couldn't run task") # include tasks and failures return response['tasks'] @@ -205,7 +234,12 @@ class EcsExecManager: args['containerInstances'] = container_instances if startedBy: args['startedBy'] = startedBy - response = self.ecs.start_task(**args) + if self.module.params['network_configuration']: + args['networkConfiguration'] = self.format_network_configuration(self.module.params['network_configuration']) + try: + response = self.ecs.start_task(**args) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + self.module.fail_json_aws(e, msg="Couldn't start task") # include tasks and failures return response['tasks'] @@ -213,6 +247,13 @@ class EcsExecManager: response = self.ecs.stop_task(cluster=cluster, task=task) return response['task'] + 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() @@ -224,14 +265,11 @@ def main(): count=dict(required=False, type='int'), # R task=dict(required=False, type='str'), # P* container_instances=dict(required=False, type='list'), # S* - started_by=dict(required=False, type='str') # R S + started_by=dict(required=False, type='str'), # R S + network_configuration=dict(required=False, type='dict') )) - module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - - # Validate Requirements - if not HAS_BOTO3: - module.fail_json(msg='boto3 is required.') + module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) # Validate Inputs if module.params['operation'] == 'run': @@ -257,6 +295,8 @@ def main(): status_type = "STOPPED" service_mgr = EcsExecManager(module) + if module.params['network_configuration'] and 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') existing = service_mgr.list_tasks(module.params['cluster'], task_to_list, status_type) results = dict(changed=False) diff --git a/lib/ansible/modules/cloud/amazon/ecs_taskdefinition.py b/lib/ansible/modules/cloud/amazon/ecs_taskdefinition.py index d9f4d9e6fce..e2653715cd9 100644 --- a/lib/ansible/modules/cloud/amazon/ecs_taskdefinition.py +++ b/lib/ansible/modules/cloud/amazon/ecs_taskdefinition.py @@ -58,9 +58,10 @@ options: network_mode: description: - The Docker networking mode to use for the containers in the task. + - C(awsvpc) mode was added in Ansible 2.5 required: false default: bridge - choices: [ 'bridge', 'host', 'none' ] + choices: [ 'bridge', 'host', 'none', 'awsvpc' ] version_added: 2.3 task_role_arn: description: @@ -166,6 +167,10 @@ class EcsTaskManager: for port in ('hostPort', 'containerPort'): if port in port_mapping: port_mapping[port] = int(port_mapping[port]) + if network_mode == 'awsvpc' and 'hostPort' in port_mapping: + if port_mapping['hostPort'] != port_mapping.get('containerPort'): + self.module.fail_json(msg="In awsvpc network mode, host port must be set to the same as " + "container port or not be set") validated_containers.append(container) @@ -227,7 +232,7 @@ def main(): revision=dict(required=False, type='int'), force_create=dict(required=False, default=False, type='bool'), containers=dict(required=False, type='list'), - network_mode=dict(required=False, default='bridge', choices=['bridge', 'host', 'none'], type='str'), + network_mode=dict(required=False, default='bridge', choices=['bridge', 'host', 'none', 'awsvpc'], type='str'), task_role_arn=dict(required=False, default='', type='str'), volumes=dict(required=False, type='list'))) @@ -390,5 +395,6 @@ def main(): module.exit_json(**results) + if __name__ == '__main__': main() diff --git a/test/integration/targets/ecs_cluster/playbooks/full_test.yml b/test/integration/targets/ecs_cluster/playbooks/full_test.yml new file mode 100644 index 00000000000..3219fa94720 --- /dev/null +++ b/test/integration/targets/ecs_cluster/playbooks/full_test.yml @@ -0,0 +1,5 @@ +- hosts: localhost + connection: local + + roles: + - ecs_cluster diff --git a/test/integration/targets/ecs_cluster/playbooks/network_fail.yml b/test/integration/targets/ecs_cluster/playbooks/network_fail.yml new file mode 100644 index 00000000000..5cdc7365c3e --- /dev/null +++ b/test/integration/targets/ecs_cluster/playbooks/network_fail.yml @@ -0,0 +1,146 @@ +- hosts: localhost + connection: local + + tasks: + - block: + - name: set up aws connection info + set_fact: + aws_connection_info: &aws_connection_info + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + security_token: "{{ security_token }}" + region: "{{ aws_region }}" + no_log: True + + - name: create ecs cluster + ecs_cluster: + name: "{{ resource_prefix }}" + state: present + <<: *aws_connection_info + + - name: create ecs_taskdefinition with bridged network + ecs_taskdefinition: + containers: + - name: my_container + image: ubuntu + memory: 128 + family: "{{ resource_prefix }}" + state: present + network_mode: bridge + <<: *aws_connection_info + register: ecs_taskdefinition_creation + + - name: create ecs_taskdefinition with awsvpc network + ecs_taskdefinition: + containers: + - name: my_container + image: ubuntu + memory: 128 + family: "{{ resource_prefix }}-vpc" + state: present + network_mode: awsvpc + <<: *aws_connection_info + register: ecs_taskdefinition_creation_vpc + + - name: ecs_taskdefinition works fine even when older botocore is used + assert: + that: + - ecs_taskdefinition_creation_vpc.changed + + - name: create ecs_service using bridged network + ecs_service: + name: "{{ resource_prefix }}" + cluster: "{{ resource_prefix }}" + task_definition: "{{ resource_prefix }}" + desired_count: 1 + state: present + <<: *aws_connection_info + register: ecs_service_creation + + - name: create ecs_service using awsvpc network_configuration + ecs_service: + name: "{{ resource_prefix }}-vpc" + cluster: "{{ resource_prefix }}" + task_definition: "{{ resource_prefix }}" + desired_count: 1 + network_configuration: + subnets: + - subnet-abcd1234 + groups: + - sg-abcd1234 + state: present + <<: *aws_connection_info + register: ecs_service_creation_vpc + ignore_errors: yes + + - name: check that graceful failure message is returned from ecs_service + assert: + that: + - ecs_service_creation_vpc.failed + - 'ecs_service_creation_vpc.msg == "botocore needs to be version 1.7.44 or higher to use network configuration"' + + - name: create ecs_task using awsvpc network_configuration + ecs_task: + cluster: "{{ resource_prefix }}-vpc" + task_definition: "{{ resource_prefix }}" + operation: run + count: 1 + started_by: me + network_configuration: + subnets: + - subnet-abcd1234 + groups: + - sg-abcd1234 + <<: *aws_connection_info + register: ecs_task_creation_vpc + ignore_errors: yes + + - name: check that graceful failure message is returned from ecs_task + assert: + that: + - ecs_task_creation_vpc.failed + - 'ecs_task_creation_vpc.msg == "botocore needs to be version 1.7.44 or higher to use network configuration"' + + always: + - name: scale down ecs service + ecs_service: + name: "{{ resource_prefix }}" + cluster: "{{ resource_prefix }}" + task_definition: "{{ resource_prefix }}" + desired_count: 0 + state: present + <<: *aws_connection_info + ignore_errors: yes + + - name: pause to wait for scale down + pause: + seconds: 30 + + - name: remove ecs service + ecs_service: + name: "{{ resource_prefix }}" + cluster: "{{ resource_prefix }}" + task_definition: "{{ resource_prefix }}" + desired_count: 1 + state: absent + <<: *aws_connection_info + ignore_errors: yes + + - name: remove ecs task definition + ecs_taskdefinition: + containers: + - name: my_container + image: ubuntu + memory: 128 + family: "{{ resource_prefix }}" + revision: "{{ ecs_taskdefinition_creation.taskdefinition.revision }}" + state: absent + <<: *aws_connection_info + ignore_errors: yes + + - name: remove ecs cluster + ecs_cluster: + name: "{{ resource_prefix }}" + state: absent + <<: *aws_connection_info + ignore_errors: yes diff --git a/test/integration/targets/ecs_cluster/defaults/main.yml b/test/integration/targets/ecs_cluster/playbooks/roles/ecs_cluster/defaults/main.yml similarity index 95% rename from test/integration/targets/ecs_cluster/defaults/main.yml rename to test/integration/targets/ecs_cluster/playbooks/roles/ecs_cluster/defaults/main.yml index 154ca7fc43c..0ccb08a2c8a 100644 --- a/test/integration/targets/ecs_cluster/defaults/main.yml +++ b/test/integration/targets/ecs_cluster/playbooks/roles/ecs_cluster/defaults/main.yml @@ -31,5 +31,5 @@ ecs_service_placement_strategy: - type: spread field: attribute:ecs.availability-zone ecs_task_container_port: 8080 -ecs_target_group_name: "{{ resource_prefix[:29] }}-tg" +ecs_target_group_name: "{{ resource_prefix[:28] }}-tg" ecs_load_balancer_name: "{{ resource_prefix[:29] }}-lb" diff --git a/test/integration/targets/ecs_cluster/files/ec2-trust-policy.json b/test/integration/targets/ecs_cluster/playbooks/roles/ecs_cluster/files/ec2-trust-policy.json similarity index 100% rename from test/integration/targets/ecs_cluster/files/ec2-trust-policy.json rename to test/integration/targets/ecs_cluster/playbooks/roles/ecs_cluster/files/ec2-trust-policy.json diff --git a/test/integration/targets/ecs_cluster/files/ecs-trust-policy.json b/test/integration/targets/ecs_cluster/playbooks/roles/ecs_cluster/files/ecs-trust-policy.json similarity index 100% rename from test/integration/targets/ecs_cluster/files/ecs-trust-policy.json rename to test/integration/targets/ecs_cluster/playbooks/roles/ecs_cluster/files/ecs-trust-policy.json diff --git a/test/integration/targets/ecs_cluster/meta/main.yml b/test/integration/targets/ecs_cluster/playbooks/roles/ecs_cluster/meta/main.yml similarity index 100% rename from test/integration/targets/ecs_cluster/meta/main.yml rename to test/integration/targets/ecs_cluster/playbooks/roles/ecs_cluster/meta/main.yml diff --git a/test/integration/targets/ecs_cluster/tasks/main.yml b/test/integration/targets/ecs_cluster/playbooks/roles/ecs_cluster/tasks/main.yml similarity index 53% rename from test/integration/targets/ecs_cluster/tasks/main.yml rename to test/integration/targets/ecs_cluster/playbooks/roles/ecs_cluster/tasks/main.yml index 185b0dc7c29..9fe63fccfaa 100644 --- a/test/integration/targets/ecs_cluster/tasks/main.yml +++ b/test/integration/targets/ecs_cluster/playbooks/roles/ecs_cluster/tasks/main.yml @@ -17,7 +17,7 @@ name: ecsInstanceRole assume_role_policy_document: "{{ lookup('file','ec2-trust-policy.json') }}" state: present - create_instance_profile: no + create_instance_profile: yes managed_policy: - AmazonEC2ContainerServiceforEC2Role <<: *aws_connection_info @@ -32,6 +32,20 @@ - AmazonEC2ContainerServiceRole <<: *aws_connection_info + - name: ensure AWSServiceRoleForECS role exists + iam_role_facts: + name: AWSServiceRoleForECS + <<: *aws_connection_info + register: iam_role_result + + # FIXME: come up with a way to automate this + - name: fail if AWSServiceRoleForECS role does not exist + fail: + msg: > + Run `aws iam create-service-linked-role --aws-service-name=ecs.amazonaws.com ` to create + a linked role for AWS VPC load balancer management + when: not iam_role_result.iam_roles + - name: create an ECS cluster ecs_cluster: name: "{{ ecs_cluster_name }}" @@ -122,20 +136,32 @@ Name: '{{ resource_prefix }}_ecs_agent' group_id: '{{ setup_sg.group_id }}' vpc_subnet_id: '{{ setup_subnet.results[0].subnet.id }}' - assign_public_ip: yes # public IP address assigned to avoid need for NAT GW. <<: *aws_connection_info register: setup_instance - name: create target group elb_target_group: - name: "{{ ecs_target_group_name }}" + name: "{{ ecs_target_group_name }}1" + state: present + protocol: HTTP + port: 8080 + modify_targets: no + vpc_id: '{{ setup_vpc.vpc.id }}' + target_type: instance + <<: *aws_connection_info + register: elb_target_group_instance + + - name: create second target group to use ip target_type + elb_target_group: + name: "{{ ecs_target_group_name }}2" state: present protocol: HTTP port: 8080 modify_targets: no vpc_id: '{{ setup_vpc.vpc.id }}' + target_type: ip <<: *aws_connection_info - register: elb_target_group + register: elb_target_group_ip - name: create load balancer elb_application_lb: @@ -149,7 +175,12 @@ Port: 80 DefaultActions: - Type: forward - TargetGroupName: "{{ ecs_target_group_name }}" + TargetGroupName: "{{ ecs_target_group_name }}1" + - Protocol: HTTP + Port: 81 + DefaultActions: + - Type: forward + TargetGroupName: "{{ ecs_target_group_name }}2" <<: *aws_connection_info - name: create task definition @@ -190,7 +221,7 @@ deployment_configuration: "{{ ecs_service_deployment_configuration }}" placement_strategy: "{{ ecs_service_placement_strategy }}" load_balancers: - - targetGroupArn: "{{ elb_target_group.target_group_arn }}" + - targetGroupArn: "{{ elb_target_group_instance.target_group_arn }}" containerName: "{{ ecs_task_name }}" containerPort: "{{ ecs_task_container_port }}" role: "ecsServiceRole" @@ -212,7 +243,7 @@ deployment_configuration: "{{ ecs_service_deployment_configuration }}" placement_strategy: "{{ ecs_service_placement_strategy }}" load_balancers: - - targetGroupArn: "{{ elb_target_group.target_group_arn }}" + - targetGroupArn: "{{ elb_target_group_instance.target_group_arn }}" containerName: "{{ ecs_task_name }}" containerPort: "{{ ecs_task_container_port }}" role: "ecsServiceRole" @@ -237,7 +268,7 @@ deployment_configuration: "{{ ecs_service_deployment_configuration }}" placement_strategy: "{{ ecs_service_placement_strategy }}" load_balancers: - - targetGroupArn: "{{ elb_target_group.target_group_arn }}" + - targetGroupArn: "{{ elb_target_group_instance.target_group_arn }}" containerName: "{{ ecs_task_name }}" containerPort: "{{ ecs_task_container_port|int + 1 }}" role: "ecsServiceRole" @@ -248,11 +279,176 @@ - name: assert that updating ECS load balancer failed with helpful message assert: that: - - update_ecs_service.failed + - update_ecs_service is failed + - "'error' not in update_ecs_service" - "'msg' in update_ecs_service" - # FIXME: fixed in #32876 + + + - name: attempt to use ECS network configuration on task definition without awsvpc network_mode + ecs_service: + state: present + name: "{{ ecs_service_name }}3" + cluster: "{{ ecs_cluster_name }}" + task_definition: "{{ ecs_task_name }}:{{ ecs_task_definition.taskdefinition.revision }}" + desired_count: 1 + deployment_configuration: "{{ ecs_service_deployment_configuration }}" + placement_strategy: "{{ ecs_service_placement_strategy }}" + load_balancers: + - targetGroupArn: "{{ elb_target_group_instance.target_group_arn }}" + containerName: "{{ ecs_task_name }}" + containerPort: "{{ ecs_task_container_port }}" + network_configuration: + subnets: "{{ setup_subnet.results | json_query('[].subnet.id') }}" + security_groups: + - '{{ setup_sg.group_id }}' + <<: *aws_connection_info + register: ecs_service_network_without_awsvpc_task ignore_errors: yes + - name: assert that using ECS network configuration with non AWSVPC task definition fails + assert: + that: + - ecs_service_network_without_awsvpc_task is failed + + - name: scale down ECS service + ecs_service: + state: present + name: "{{ ecs_service_name }}" + cluster: "{{ ecs_cluster_name }}" + task_definition: "{{ ecs_task_name }}:{{ ecs_task_definition.taskdefinition.revision }}" + desired_count: 0 + deployment_configuration: "{{ ecs_service_deployment_configuration }}" + placement_strategy: "{{ ecs_service_placement_strategy }}" + load_balancers: + - targetGroupArn: "{{ elb_target_group_instance.target_group_arn }}" + containerName: "{{ ecs_task_name }}" + containerPort: "{{ ecs_task_container_port }}" + role: "ecsServiceRole" + <<: *aws_connection_info + register: ecs_service_scale_down + + - name: pause to allow service to scale down + pause: + seconds: 60 + + - name: delete ECS service definition + ecs_service: + state: absent + name: "{{ ecs_service_name }}" + cluster: "{{ ecs_cluster_name }}" + <<: *aws_connection_info + register: delete_ecs_service + + - name: assert that deleting ECS service worked + assert: + that: + - delete_ecs_service.changed + + - name: create VPC-networked task definition with host port set to 0 (expected to fail) + ecs_taskdefinition: + containers: "{{ ecs_task_containers }}" + family: "{{ ecs_task_name }}-vpc" + state: present + network_mode: awsvpc + <<: *aws_connection_info + register: ecs_task_definition_vpc_no_host_port + ignore_errors: yes + + - name: check that awsvpc task definition with host port 0 fails gracefully + assert: + that: + - ecs_task_definition_vpc_no_host_port is failed + - "'error' not in ecs_task_definition_vpc_no_host_port" + + - name: create VPC-networked task definition with host port set to 8080 + ecs_taskdefinition: + containers: "{{ ecs_task_containers }}" + family: "{{ ecs_task_name }}-vpc" + network_mode: awsvpc + state: present + <<: *aws_connection_info + vars: + ecs_task_host_port: 8080 + register: ecs_task_definition_vpc_with_host_port + + - name: obtain ECS task definition facts + ecs_taskdefinition_facts: + task_definition: "{{ ecs_task_name }}-vpc:{{ ecs_task_definition_vpc_with_host_port.taskdefinition.revision }}" + <<: *aws_connection_info + register: ecs_taskdefinition_facts + + - name: assert that network mode is awsvpc + assert: + that: + - "ecs_taskdefinition_facts.network_mode == 'awsvpc'" + + + - name: create ECS service definition with network configuration + ecs_service: + state: present + name: "{{ ecs_service_name }}2" + cluster: "{{ ecs_cluster_name }}" + task_definition: "{{ ecs_task_name }}-vpc:{{ ecs_task_definition_vpc_with_host_port.taskdefinition.revision }}" + desired_count: 1 + deployment_configuration: "{{ ecs_service_deployment_configuration }}" + placement_strategy: "{{ ecs_service_placement_strategy }}" + load_balancers: + - targetGroupArn: "{{ elb_target_group_ip.target_group_arn }}" + containerName: "{{ ecs_task_name }}" + containerPort: "{{ ecs_task_container_port }}" + network_configuration: + subnets: "{{ setup_subnet.results | json_query('[].subnet.id') }}" + security_groups: + - '{{ setup_sg.group_id }}' + <<: *aws_connection_info + register: create_ecs_service_with_vpc + + - name: assert that network configuration is correct + assert: + that: + - "'networkConfiguration' in create_ecs_service_with_vpc.service" + - "'awsvpcConfiguration' in create_ecs_service_with_vpc.service.networkConfiguration" + - "create_ecs_service_with_vpc.service.networkConfiguration.awsvpcConfiguration.subnets|length == 2" + - "create_ecs_service_with_vpc.service.networkConfiguration.awsvpcConfiguration.securityGroups|length == 1" + + - name: create dummy group to update ECS service with + ec2_group: + name: "{{ resource_prefix }}-ecs-vpc-test-sg" + description: "Test security group for ECS with VPC" + vpc_id: '{{ setup_vpc.vpc.id }}' + state: present + <<: *aws_connection_info + + - name: update ECS service definition with new network configuration + ecs_service: + state: present + name: "{{ ecs_service_name }}2" + cluster: "{{ ecs_cluster_name }}" + task_definition: "{{ ecs_task_name }}-vpc:{{ ecs_task_definition_vpc_with_host_port.taskdefinition.revision }}" + desired_count: 1 + deployment_configuration: "{{ ecs_service_deployment_configuration }}" + placement_strategy: "{{ ecs_service_placement_strategy }}" + load_balancers: + - targetGroupArn: "{{ elb_target_group_ip.target_group_arn }}" + containerName: "{{ ecs_task_name }}" + containerPort: "{{ ecs_task_container_port }}" + network_configuration: + subnets: "{{ setup_subnet.results | json_query('[].subnet.id') }}" + security_groups: + - "{{ resource_prefix }}-ecs-vpc-test-sg" + <<: *aws_connection_info + register: update_ecs_service_with_vpc + + - name: check that ECS service changed + assert: + that: + - update_ecs_service_with_vpc.changed + - "'networkConfiguration' in update_ecs_service_with_vpc.service" + - "'awsvpcConfiguration' in update_ecs_service_with_vpc.service.networkConfiguration" + - "update_ecs_service_with_vpc.service.networkConfiguration.awsvpcConfiguration.subnets|length == 2" + - "update_ecs_service_with_vpc.service.networkConfiguration.awsvpcConfiguration.securityGroups|length == 1" + + - name: obtain facts for all ECS services in the cluster ecs_service_facts: cluster: "{{ ecs_cluster_name }}" @@ -297,38 +493,85 @@ that: - "ecs_service_facts.services_not_running[0].reason == 'MISSING'" + - name: obtain specific ECS service facts + ecs_service_facts: + service: "{{ ecs_service_name }}2" + cluster: "{{ ecs_cluster_name }}" + details: yes + <<: *aws_connection_info + register: ecs_service_facts + + - name: check that facts contain network configuration + assert: + that: + - "'networkConfiguration' in ecs_service_facts.ansible_facts.services[0]" + - name: attempt to get facts from missing task definition ecs_taskdefinition_facts: task_definition: "{{ ecs_task_name }}-vpc:{{ ecs_task_definition.taskdefinition.revision + 1}}" <<: *aws_connection_info always: - # TEAR DOWN: snapshot, ec2 instance, ec2 key pair, security group, vpc - name: Announce teardown start debug: msg: "***** TESTING COMPLETE. COMMENCE TEARDOWN *****" + - name: obtain ECS service facts + ecs_service_facts: + service: "{{ ecs_service_name }}" + cluster: "{{ ecs_cluster_name }}" + details: yes + <<: *aws_connection_info + register: ecs_service_facts + - name: scale down ECS service ecs_service: state: present name: "{{ ecs_service_name }}" cluster: "{{ ecs_cluster_name }}" - task_definition: "{{ ecs_task_name }}:{{ ecs_task_definition.taskdefinition.revision }}" + task_definition: "{{ ecs_service_facts.ansible_facts.services[0].taskDefinition }}" desired_count: 0 deployment_configuration: "{{ ecs_service_deployment_configuration }}" placement_strategy: "{{ ecs_service_placement_strategy }}" load_balancers: - - targetGroupArn: "{{ elb_target_group.target_group_arn }}" + - targetGroupArn: "{{ ecs_service_facts.ansible_facts.services[0].loadBalancers[0].targetGroupArn }}" containerName: "{{ ecs_task_name }}" containerPort: "{{ ecs_task_container_port }}" - role: "ecsServiceRole" <<: *aws_connection_info ignore_errors: yes + register: ecs_service_scale_down - - name: pause to allow service to scale down + - name: obtain second ECS service facts + ecs_service_facts: + service: "{{ ecs_service_name }}2" + cluster: "{{ ecs_cluster_name }}" + details: yes + <<: *aws_connection_info + ignore_errors: yes + register: ecs_service_facts + + - name: scale down second ECS service + ecs_service: + state: present + name: "{{ ecs_service_name }}2" + cluster: "{{ ecs_cluster_name }}" + task_definition: "{{ ecs_service_facts.ansible_facts.services[0].taskDefinition }}" + desired_count: 0 + deployment_configuration: "{{ ecs_service_deployment_configuration }}" + placement_strategy: "{{ ecs_service_placement_strategy }}" + load_balancers: + - targetGroupArn: "{{ ecs_service_facts.ansible_facts.services[0].loadBalancers[0].targetGroupArn }}" + containerName: "{{ ecs_task_name }}" + containerPort: "{{ ecs_task_container_port }}" + <<: *aws_connection_info + ignore_errors: yes + register: ecs_service_scale_down + + - name: pause to allow services to scale down pause: seconds: 60 + when: ecs_service_scale_down is not failed - name: remove ecs service ecs_service: @@ -338,6 +581,14 @@ <<: *aws_connection_info ignore_errors: yes + - name: remove second ecs service + ecs_service: + state: absent + cluster: "{{ ecs_cluster_name }}" + name: "{{ ecs_service_name }}2" + <<: *aws_connection_info + ignore_errors: yes + - name: remove ecs task definition ecs_taskdefinition: containers: "{{ ecs_task_containers }}" @@ -345,6 +596,19 @@ revision: "{{ ecs_task_definition.taskdefinition.revision }}" state: absent <<: *aws_connection_info + vars: + ecs_task_host_port: 8080 + ignore_errors: yes + + - name: remove second ecs task definition + ecs_taskdefinition: + containers: "{{ ecs_task_containers }}" + family: "{{ ecs_task_name }}-vpc" + revision: "{{ ecs_task_definition_vpc_with_host_port.taskdefinition.revision }}" + state: absent + <<: *aws_connection_info + vars: + ecs_task_host_port: 8080 ignore_errors: yes - name: remove load balancer @@ -354,16 +618,21 @@ wait: yes <<: *aws_connection_info ignore_errors: yes + register: elb_application_lb_remove - name: pause to allow target group to be disassociated pause: seconds: 30 + when: not elb_application_lb_remove is failed - - name: remove target group + - name: remove target groups elb_target_group: - name: "{{ ecs_target_group_name }}" + name: "{{ item }}" state: absent <<: *aws_connection_info + with_items: + - "{{ ecs_target_group_name }}1" + - "{{ ecs_target_group_name }}2" ignore_errors: yes - name: remove setup ec2 instance @@ -381,13 +650,16 @@ <<: *aws_connection_info ignore_errors: yes - - name: remove setup security group + - name: remove security groups ec2_group: - name: '{{ resource_prefix }}_ecs_cluster-sg' + name: '{{ item }}' description: 'created by Ansible integration tests' state: absent vpc_id: '{{ setup_vpc.vpc.id }}' <<: *aws_connection_info + with_items: + - "{{ resource_prefix }}-ecs-vpc-test-sg" + - '{{ resource_prefix }}_ecs_cluster-sg' ignore_errors: yes - name: remove IGW diff --git a/test/integration/targets/ecs_cluster/runme.sh b/test/integration/targets/ecs_cluster/runme.sh new file mode 100755 index 00000000000..000334cc716 --- /dev/null +++ b/test/integration/targets/ecs_cluster/runme.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +# We don't set -u here, due to pypa/virtualenv#150 +set -ex + +MYTMPDIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir') + +trap 'rm -rf "${MYTMPDIR}"' EXIT + +# This is needed for the ubuntu1604py3 tests +# Ubuntu patches virtualenv to make the default python2 +# but for the python3 tests we need virtualenv to use python3 +PYTHON=${ANSIBLE_TEST_PYTHON_INTERPRETER:-python} + +# Test graceful failure for older versions of botocore +virtualenv --system-site-packages --python "${PYTHON}" "${MYTMPDIR}/botocore-1.7.40" +source "${MYTMPDIR}/botocore-1.7.40/bin/activate" +$PYTHON -m pip install 'botocore<=1.7.40' boto3 +ansible-playbook -i ../../inventory -e @../../integration_config.yml -e @../../cloud-config-aws.yml -v playbooks/network_fail.yml "$@" + +# Run full test suite +virtualenv --system-site-packages --python "${PYTHON}" "${MYTMPDIR}/botocore-recent" +source "${MYTMPDIR}/botocore-recent/bin/activate" +$PYTHON -m pip install 'botocore>=1.8.0' boto3 +ansible-playbook -i ../../inventory -e @../../integration_config.yml -e @../../cloud-config-aws.yml -v playbooks/full_test.yml "$@"