[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
pull/39335/head
Will Thames 7 years ago committed by Ryan Brown
parent 58bf4ae611
commit 12f2b9506d

@ -32,11 +32,15 @@
"application-autoscaling:RegisterScalableTarget", "application-autoscaling:RegisterScalableTarget",
"cloudwatch:DescribeAlarms", "cloudwatch:DescribeAlarms",
"cloudwatch:PutMetricAlarm", "cloudwatch:PutMetricAlarm",
"ecs:List*",
"ecs:Describe*",
"ecs:CreateCluster", "ecs:CreateCluster",
"ecs:DeleteCluster",
"ecs:CreateService", "ecs:CreateService",
"ecs:DeleteCluster",
"ecs:DeleteService",
"ecs:DeregisterTaskDefinition",
"ecs:Describe*",
"ecs:List*",
"ecs:RegisterTaskDefinition",
"ecs:RunTask",
"ecs:UpdateService", "ecs:UpdateService",
"elasticloadbalancing:Describe*", "elasticloadbalancing:Describe*",
"iam:AttachRolePolicy", "iam:AttachRolePolicy",
@ -45,8 +49,8 @@
"iam:GetPolicyVersion", "iam:GetPolicyVersion",
"iam:GetRole", "iam:GetRole",
"iam:ListAttachedRolePolicies", "iam:ListAttachedRolePolicies",
"iam:ListRoles",
"iam:ListGroups", "iam:ListGroups",
"iam:ListRoles",
"iam:ListUsers" "iam:ListUsers"
], ],
"Resource": [ "Resource": [

@ -70,7 +70,7 @@ options:
role: role:
description: 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 - 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 required: false
delay: delay:
description: 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 - The placement strategy objects to use for tasks in your service. You can specify a maximum of 5 strategy rules per service
required: false required: false
version_added: 2.4 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: extends_documentation_fragment:
- aws - aws
- ec2 - ec2
@ -117,6 +123,20 @@ EXAMPLES = '''
state: present state: present
cluster: new_cluster 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 # Simple example to delete
- ecs_service: - ecs_service:
name: default name: default
@ -265,15 +285,14 @@ DEPLOYMENT_CONFIGURATION_TYPE_MAP = {
'minimum_healthy_percent': 'int' '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: try:
import botocore import botocore
HAS_BOTO3 = True
except ImportError: except ImportError:
HAS_BOTO3 = False pass # handled by AnsibleAWSModule
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
class EcsServiceManager: class EcsServiceManager:
@ -281,9 +300,25 @@ class EcsServiceManager:
def __init__(self, module): def __init__(self, module):
self.module = 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) def format_network_configuration(self, network_config):
self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) 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'): def find_in_array(self, array_of_services, service_name, field_name='serviceArn'):
for c in array_of_services: for c in array_of_services:
@ -322,8 +357,8 @@ class EcsServiceManager:
def create_service(self, service_name, cluster_name, task_definition, load_balancers, def create_service(self, service_name, cluster_name, task_definition, load_balancers,
desired_count, client_token, role, deployment_configuration, desired_count, client_token, role, deployment_configuration,
placement_constraints, placement_strategy): placement_constraints, placement_strategy, network_configuration):
response = self.ecs.create_service( params = dict(
cluster=cluster_name, cluster=cluster_name,
serviceName=service_name, serviceName=service_name,
taskDefinition=task_definition, taskDefinition=task_definition,
@ -334,21 +369,51 @@ class EcsServiceManager:
deploymentConfiguration=deployment_configuration, deploymentConfiguration=deployment_configuration,
placementConstraints=placement_constraints, placementConstraints=placement_constraints,
placementStrategy=placement_strategy) 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, def update_service(self, service_name, cluster_name, task_definition,
desired_count, deployment_configuration): desired_count, deployment_configuration, network_configuration):
response = self.ecs.update_service( params = dict(
cluster=cluster_name, cluster=cluster_name,
service=service_name, service=service_name,
taskDefinition=task_definition, taskDefinition=task_definition,
desiredCount=desired_count, desiredCount=desired_count,
deploymentConfiguration=deployment_configuration) 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): def delete_service(self, service, cluster=None):
return self.ecs.delete_service(cluster=cluster, service=service) 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(): def main():
argument_spec = ec2_argument_spec() argument_spec = ec2_argument_spec()
@ -365,21 +430,22 @@ def main():
repeat=dict(required=False, type='int', default=10), repeat=dict(required=False, type='int', default=10),
deployment_configuration=dict(required=False, default={}, type='dict'), deployment_configuration=dict(required=False, default={}, type='dict'),
placement_constraints=dict(required=False, default=[], type='list'), 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, module = AnsibleAWSModule(argument_spec=argument_spec,
supports_check_mode=True, supports_check_mode=True,
required_if=[ required_if=[('state', 'present', ['task_definition', 'desired_count'])],
('state', 'present', ['task_definition', 'desired_count']) required_together=[['load_balancers', 'role']])
],
required_together=[['load_balancers', 'role']]
)
if not HAS_BOTO3:
module.fail_json(msg='boto3 is required.')
service_mgr = EcsServiceManager(module) 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 = map_complex_type(module.params['deployment_configuration'],
DEPLOYMENT_CONFIGURATION_TYPE_MAP) DEPLOYMENT_CONFIGURATION_TYPE_MAP)
@ -418,7 +484,8 @@ def main():
module.params['cluster'], module.params['cluster'],
module.params['task_definition'], module.params['task_definition'],
module.params['desired_count'], module.params['desired_count'],
deploymentConfiguration) deploymentConfiguration,
network_configuration)
else: else:
for loadBalancer in loadBalancers: for loadBalancer in loadBalancers:
if 'containerPort' in loadBalancer: if 'containerPort' in loadBalancer:
@ -433,7 +500,8 @@ def main():
role, role,
deploymentConfiguration, deploymentConfiguration,
module.params['placement_constraints'], module.params['placement_constraints'],
module.params['placement_strategy']) module.params['placement_strategy'],
network_configuration)
results['service'] = response results['service'] = response

@ -54,6 +54,12 @@ options:
description: description:
- A value showing who or what started the task (for informational purposes) - A value showing who or what started the task (for informational purposes)
required: False 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: extends_documentation_fragment:
- aws - aws
- ec2 - ec2
@ -81,6 +87,12 @@ EXAMPLES = '''
container_instances: container_instances:
- arn:aws:ecs:us-west-2:172139249013:container-instance/79c23f22-876c-438a-bddf-55c98a3538a8 - arn:aws:ecs:us-west-2:172139249013:container-instance/79c23f22-876c-438a-bddf-55c98a3538a8
started_by: ansible_user started_by: ansible_user
network_configuration:
subnets:
- subnet-abcd1234
security_groups:
- sg-aaaa1111
- my_security_group
register: task_output register: task_output
- name: Stop a task - name: Stop a task
@ -150,14 +162,13 @@ task:
type: string 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: try:
import botocore import botocore
HAS_BOTO3 = True
except ImportError: except ImportError:
HAS_BOTO3 = False pass # handled by AnsibleAWSModule
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.ec2 import boto3_conn, ec2_argument_spec, get_aws_connection_info
class EcsExecManager: class EcsExecManager:
@ -165,9 +176,25 @@ class EcsExecManager:
def __init__(self, module): def __init__(self, module):
self.module = 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) def format_network_configuration(self, network_config):
self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) 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): def list_tasks(self, cluster_name, service_name, status):
response = self.ecs.list_tasks( response = self.ecs.list_tasks(
@ -184,12 +211,14 @@ class EcsExecManager:
def run_task(self, cluster, task_definition, overrides, count, startedBy): def run_task(self, cluster, task_definition, overrides, count, startedBy):
if overrides is None: if overrides is None:
overrides = dict() overrides = dict()
response = self.ecs.run_task( params = dict(cluster=cluster, taskDefinition=task_definition,
cluster=cluster, overrides=overrides, count=count, startedBy=startedBy)
taskDefinition=task_definition, if self.module.params['network_configuration']:
overrides=overrides, params['networkConfiguration'] = self.format_network_configuration(self.module.params['network_configuration'])
count=count, try:
startedBy=startedBy) 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 # include tasks and failures
return response['tasks'] return response['tasks']
@ -205,7 +234,12 @@ class EcsExecManager:
args['containerInstances'] = container_instances args['containerInstances'] = container_instances
if startedBy: if startedBy:
args['startedBy'] = 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 # include tasks and failures
return response['tasks'] return response['tasks']
@ -213,6 +247,13 @@ class EcsExecManager:
response = self.ecs.stop_task(cluster=cluster, task=task) response = self.ecs.stop_task(cluster=cluster, task=task)
return response['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(): def main():
argument_spec = ec2_argument_spec() argument_spec = ec2_argument_spec()
@ -224,14 +265,11 @@ def main():
count=dict(required=False, type='int'), # R count=dict(required=False, type='int'), # R
task=dict(required=False, type='str'), # P* task=dict(required=False, type='str'), # P*
container_instances=dict(required=False, type='list'), # S* 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) module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True)
# Validate Requirements
if not HAS_BOTO3:
module.fail_json(msg='boto3 is required.')
# Validate Inputs # Validate Inputs
if module.params['operation'] == 'run': if module.params['operation'] == 'run':
@ -257,6 +295,8 @@ def main():
status_type = "STOPPED" status_type = "STOPPED"
service_mgr = EcsExecManager(module) 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) existing = service_mgr.list_tasks(module.params['cluster'], task_to_list, status_type)
results = dict(changed=False) results = dict(changed=False)

@ -58,9 +58,10 @@ options:
network_mode: network_mode:
description: description:
- The Docker networking mode to use for the containers in the task. - The Docker networking mode to use for the containers in the task.
- C(awsvpc) mode was added in Ansible 2.5
required: false required: false
default: bridge default: bridge
choices: [ 'bridge', 'host', 'none' ] choices: [ 'bridge', 'host', 'none', 'awsvpc' ]
version_added: 2.3 version_added: 2.3
task_role_arn: task_role_arn:
description: description:
@ -166,6 +167,10 @@ class EcsTaskManager:
for port in ('hostPort', 'containerPort'): for port in ('hostPort', 'containerPort'):
if port in port_mapping: if port in port_mapping:
port_mapping[port] = int(port_mapping[port]) 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) validated_containers.append(container)
@ -227,7 +232,7 @@ def main():
revision=dict(required=False, type='int'), revision=dict(required=False, type='int'),
force_create=dict(required=False, default=False, type='bool'), force_create=dict(required=False, default=False, type='bool'),
containers=dict(required=False, type='list'), 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'), task_role_arn=dict(required=False, default='', type='str'),
volumes=dict(required=False, type='list'))) volumes=dict(required=False, type='list')))
@ -390,5 +395,6 @@ def main():
module.exit_json(**results) module.exit_json(**results)
if __name__ == '__main__': if __name__ == '__main__':
main() main()

@ -0,0 +1,5 @@
- hosts: localhost
connection: local
roles:
- ecs_cluster

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

@ -31,5 +31,5 @@ ecs_service_placement_strategy:
- type: spread - type: spread
field: attribute:ecs.availability-zone field: attribute:ecs.availability-zone
ecs_task_container_port: 8080 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" ecs_load_balancer_name: "{{ resource_prefix[:29] }}-lb"

@ -17,7 +17,7 @@
name: ecsInstanceRole name: ecsInstanceRole
assume_role_policy_document: "{{ lookup('file','ec2-trust-policy.json') }}" assume_role_policy_document: "{{ lookup('file','ec2-trust-policy.json') }}"
state: present state: present
create_instance_profile: no create_instance_profile: yes
managed_policy: managed_policy:
- AmazonEC2ContainerServiceforEC2Role - AmazonEC2ContainerServiceforEC2Role
<<: *aws_connection_info <<: *aws_connection_info
@ -32,6 +32,20 @@
- AmazonEC2ContainerServiceRole - AmazonEC2ContainerServiceRole
<<: *aws_connection_info <<: *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 - name: create an ECS cluster
ecs_cluster: ecs_cluster:
name: "{{ ecs_cluster_name }}" name: "{{ ecs_cluster_name }}"
@ -122,20 +136,32 @@
Name: '{{ resource_prefix }}_ecs_agent' Name: '{{ resource_prefix }}_ecs_agent'
group_id: '{{ setup_sg.group_id }}' group_id: '{{ setup_sg.group_id }}'
vpc_subnet_id: '{{ setup_subnet.results[0].subnet.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 <<: *aws_connection_info
register: setup_instance register: setup_instance
- name: create target group - name: create target group
elb_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 state: present
protocol: HTTP protocol: HTTP
port: 8080 port: 8080
modify_targets: no modify_targets: no
vpc_id: '{{ setup_vpc.vpc.id }}' vpc_id: '{{ setup_vpc.vpc.id }}'
target_type: ip
<<: *aws_connection_info <<: *aws_connection_info
register: elb_target_group register: elb_target_group_ip
- name: create load balancer - name: create load balancer
elb_application_lb: elb_application_lb:
@ -149,7 +175,12 @@
Port: 80 Port: 80
DefaultActions: DefaultActions:
- Type: forward - 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 <<: *aws_connection_info
- name: create task definition - name: create task definition
@ -190,7 +221,7 @@
deployment_configuration: "{{ ecs_service_deployment_configuration }}" deployment_configuration: "{{ ecs_service_deployment_configuration }}"
placement_strategy: "{{ ecs_service_placement_strategy }}" placement_strategy: "{{ ecs_service_placement_strategy }}"
load_balancers: load_balancers:
- targetGroupArn: "{{ elb_target_group.target_group_arn }}" - targetGroupArn: "{{ elb_target_group_instance.target_group_arn }}"
containerName: "{{ ecs_task_name }}" containerName: "{{ ecs_task_name }}"
containerPort: "{{ ecs_task_container_port }}" containerPort: "{{ ecs_task_container_port }}"
role: "ecsServiceRole" role: "ecsServiceRole"
@ -212,7 +243,7 @@
deployment_configuration: "{{ ecs_service_deployment_configuration }}" deployment_configuration: "{{ ecs_service_deployment_configuration }}"
placement_strategy: "{{ ecs_service_placement_strategy }}" placement_strategy: "{{ ecs_service_placement_strategy }}"
load_balancers: load_balancers:
- targetGroupArn: "{{ elb_target_group.target_group_arn }}" - targetGroupArn: "{{ elb_target_group_instance.target_group_arn }}"
containerName: "{{ ecs_task_name }}" containerName: "{{ ecs_task_name }}"
containerPort: "{{ ecs_task_container_port }}" containerPort: "{{ ecs_task_container_port }}"
role: "ecsServiceRole" role: "ecsServiceRole"
@ -237,7 +268,7 @@
deployment_configuration: "{{ ecs_service_deployment_configuration }}" deployment_configuration: "{{ ecs_service_deployment_configuration }}"
placement_strategy: "{{ ecs_service_placement_strategy }}" placement_strategy: "{{ ecs_service_placement_strategy }}"
load_balancers: load_balancers:
- targetGroupArn: "{{ elb_target_group.target_group_arn }}" - targetGroupArn: "{{ elb_target_group_instance.target_group_arn }}"
containerName: "{{ ecs_task_name }}" containerName: "{{ ecs_task_name }}"
containerPort: "{{ ecs_task_container_port|int + 1 }}" containerPort: "{{ ecs_task_container_port|int + 1 }}"
role: "ecsServiceRole" role: "ecsServiceRole"
@ -248,11 +279,176 @@
- name: assert that updating ECS load balancer failed with helpful message - name: assert that updating ECS load balancer failed with helpful message
assert: assert:
that: that:
- update_ecs_service.failed - update_ecs_service is failed
- "'error' not in update_ecs_service"
- "'msg' 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 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 - name: obtain facts for all ECS services in the cluster
ecs_service_facts: ecs_service_facts:
cluster: "{{ ecs_cluster_name }}" cluster: "{{ ecs_cluster_name }}"
@ -297,38 +493,85 @@
that: that:
- "ecs_service_facts.services_not_running[0].reason == 'MISSING'" - "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 - name: attempt to get facts from missing task definition
ecs_taskdefinition_facts: ecs_taskdefinition_facts:
task_definition: "{{ ecs_task_name }}-vpc:{{ ecs_task_definition.taskdefinition.revision + 1}}" task_definition: "{{ ecs_task_name }}-vpc:{{ ecs_task_definition.taskdefinition.revision + 1}}"
<<: *aws_connection_info <<: *aws_connection_info
always: always:
# TEAR DOWN: snapshot, ec2 instance, ec2 key pair, security group, vpc # TEAR DOWN: snapshot, ec2 instance, ec2 key pair, security group, vpc
- name: Announce teardown start - name: Announce teardown start
debug: debug:
msg: "***** TESTING COMPLETE. COMMENCE TEARDOWN *****" 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 - name: scale down ECS service
ecs_service: ecs_service:
state: present state: present
name: "{{ ecs_service_name }}" name: "{{ ecs_service_name }}"
cluster: "{{ ecs_cluster_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 desired_count: 0
deployment_configuration: "{{ ecs_service_deployment_configuration }}" deployment_configuration: "{{ ecs_service_deployment_configuration }}"
placement_strategy: "{{ ecs_service_placement_strategy }}" placement_strategy: "{{ ecs_service_placement_strategy }}"
load_balancers: load_balancers:
- targetGroupArn: "{{ elb_target_group.target_group_arn }}" - targetGroupArn: "{{ ecs_service_facts.ansible_facts.services[0].loadBalancers[0].targetGroupArn }}"
containerName: "{{ ecs_task_name }}" containerName: "{{ ecs_task_name }}"
containerPort: "{{ ecs_task_container_port }}" containerPort: "{{ ecs_task_container_port }}"
role: "ecsServiceRole"
<<: *aws_connection_info <<: *aws_connection_info
ignore_errors: yes 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: pause:
seconds: 60 seconds: 60
when: ecs_service_scale_down is not failed
- name: remove ecs service - name: remove ecs service
ecs_service: ecs_service:
@ -338,6 +581,14 @@
<<: *aws_connection_info <<: *aws_connection_info
ignore_errors: yes 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 - name: remove ecs task definition
ecs_taskdefinition: ecs_taskdefinition:
containers: "{{ ecs_task_containers }}" containers: "{{ ecs_task_containers }}"
@ -345,6 +596,19 @@
revision: "{{ ecs_task_definition.taskdefinition.revision }}" revision: "{{ ecs_task_definition.taskdefinition.revision }}"
state: absent state: absent
<<: *aws_connection_info <<: *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 ignore_errors: yes
- name: remove load balancer - name: remove load balancer
@ -354,16 +618,21 @@
wait: yes wait: yes
<<: *aws_connection_info <<: *aws_connection_info
ignore_errors: yes ignore_errors: yes
register: elb_application_lb_remove
- name: pause to allow target group to be disassociated - name: pause to allow target group to be disassociated
pause: pause:
seconds: 30 seconds: 30
when: not elb_application_lb_remove is failed
- name: remove target group - name: remove target groups
elb_target_group: elb_target_group:
name: "{{ ecs_target_group_name }}" name: "{{ item }}"
state: absent state: absent
<<: *aws_connection_info <<: *aws_connection_info
with_items:
- "{{ ecs_target_group_name }}1"
- "{{ ecs_target_group_name }}2"
ignore_errors: yes ignore_errors: yes
- name: remove setup ec2 instance - name: remove setup ec2 instance
@ -381,13 +650,16 @@
<<: *aws_connection_info <<: *aws_connection_info
ignore_errors: yes ignore_errors: yes
- name: remove setup security group - name: remove security groups
ec2_group: ec2_group:
name: '{{ resource_prefix }}_ecs_cluster-sg' name: '{{ item }}'
description: 'created by Ansible integration tests' description: 'created by Ansible integration tests'
state: absent state: absent
vpc_id: '{{ setup_vpc.vpc.id }}' vpc_id: '{{ setup_vpc.vpc.id }}'
<<: *aws_connection_info <<: *aws_connection_info
with_items:
- "{{ resource_prefix }}-ecs-vpc-test-sg"
- '{{ resource_prefix }}_ecs_cluster-sg'
ignore_errors: yes ignore_errors: yes
- name: remove IGW - name: remove IGW

@ -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 "$@"
Loading…
Cancel
Save