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 "$@"