diff --git a/lib/ansible/modules/cloud/amazon/elb_target_group.py b/lib/ansible/modules/cloud/amazon/elb_target_group.py index b9ddaf72baa..52b136a9402 100644 --- a/lib/ansible/modules/cloud/amazon/elb_target_group.py +++ b/lib/ansible/modules/cloud/amazon/elb_target_group.py @@ -110,14 +110,15 @@ options: target_type: description: - The type of target that you must specify when registering targets with this target group. The possible values are - C(instance) (targets are specified by instance ID) or C(ip) (targets are specified by IP address). - Note that you can't specify targets for a target group using both instance IDs and IP addresses. + C(instance) (targets are specified by instance ID), C(ip) (targets are specified by IP address) or C(lambda) (target is specified by ARN). + Note that you can't specify targets for a target group using more than one type. Target type lambda only accept one target. When more than + one target is specified, only the first one is used. All additional targets are ignored. If the target type is ip, specify IP addresses from the subnets of the virtual private cloud (VPC) for the target group, the RFC 1918 range (10.0.0.0/8, 172.16.0.0/12, and 192.168.0.0/16), and the RFC 6598 range (100.64.0.0/10). You can't specify publicly routable IP addresses. required: false default: instance - choices: ['instance', 'ip'] + choices: ['instance', 'ip', 'lambda'] version_added: 2.5 targets: description: @@ -212,6 +213,37 @@ EXAMPLES = ''' wait_timeout: 200 wait: True +# Using lambda as targets require that the target group +# itself is allow to invoke the lambda function. +# therefore you need first to create an empty target group +# to receive its arn, second, allow the target group +# to invoke the lamba function and third, add the target +# to the target group +- name: first, create empty target group + elb_target_group: + name: my-lambda-targetgroup + target_type: lambda + state: present + modify_targets: False + register: out + +- name: second, allow invoke of the lambda + lambda_policy: + state: "{{ state | default('present') }}" + function_name: my-lambda-function + statement_id: someID + action: lambda:InvokeFunction + principal: elasticloadbalancing.amazonaws.com + source_arn: "{{ out.target_group_arn }}" + +- name: third, add target + elb_target_group: + name: my-lambda-targetgroup + target_type: lambda + state: present + targets: + - Id: arn:aws:lambda:eu-central-1:123456789012:function:my-lambda-function + ''' RETURN = ''' @@ -389,9 +421,10 @@ def create_or_update_target_group(connection, module): new_target_group = False params = dict() params['Name'] = module.params.get("name") - params['Protocol'] = module.params.get("protocol").upper() - params['Port'] = module.params.get("port") - params['VpcId'] = module.params.get("vpc_id") + if module.params.get("target_type") != "lambda": + params['Protocol'] = module.params.get("protocol").upper() + params['Port'] = module.params.get("port") + params['VpcId'] = module.params.get("vpc_id") tags = module.params.get("tags") purge_tags = module.params.get("purge_tags") deregistration_delay_timeout = module.params.get("deregistration_delay_timeout") @@ -505,90 +538,128 @@ def create_or_update_target_group(connection, module): # Do we need to modify targets? if module.params.get("modify_targets"): + # get list of current target instances. I can't see anything like a describe targets in the doco so + # describe_target_health seems to be the only way to get them + try: + current_targets = connection.describe_target_health( + TargetGroupArn=tg['TargetGroupArn']) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't get target group health") + if module.params.get("targets"): - params['Targets'] = module.params.get("targets") - # Correct type of target ports - for target in params['Targets']: - target['Port'] = int(target.get('Port', module.params.get('port'))) + if module.params.get("target_type") != "lambda": + params['Targets'] = module.params.get("targets") - # get list of current target instances. I can't see anything like a describe targets in the doco so - # describe_target_health seems to be the only way to get them - - try: - current_targets = connection.describe_target_health(TargetGroupArn=tg['TargetGroupArn']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't get target group health") - - current_instance_ids = [] - - for instance in current_targets['TargetHealthDescriptions']: - current_instance_ids.append(instance['Target']['Id']) - - new_instance_ids = [] - for instance in params['Targets']: - new_instance_ids.append(instance['Id']) - - add_instances = set(new_instance_ids) - set(current_instance_ids) - - if add_instances: - instances_to_add = [] + # Correct type of target ports for target in params['Targets']: - if target['Id'] in add_instances: - instances_to_add.append({'Id': target['Id'], 'Port': target['Port']}) + target['Port'] = int(target.get('Port', module.params.get('port'))) - changed = True + current_instance_ids = [] + + for instance in current_targets['TargetHealthDescriptions']: + current_instance_ids.append(instance['Target']['Id']) + + new_instance_ids = [] + for instance in params['Targets']: + new_instance_ids.append(instance['Id']) + + add_instances = set(new_instance_ids) - set(current_instance_ids) + + if add_instances: + instances_to_add = [] + for target in params['Targets']: + if target['Id'] in add_instances: + instances_to_add.append({'Id': target['Id'], 'Port': target['Port']}) + + changed = True + try: + connection.register_targets(TargetGroupArn=tg['TargetGroupArn'], Targets=instances_to_add) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't register targets") + + if module.params.get("wait"): + status_achieved, registered_instances = wait_for_status(connection, module, tg['TargetGroupArn'], instances_to_add, 'healthy') + if not status_achieved: + module.fail_json(msg='Error waiting for target registration to be healthy - please check the AWS console') + + remove_instances = set(current_instance_ids) - set(new_instance_ids) + + if remove_instances: + instances_to_remove = [] + for target in current_targets['TargetHealthDescriptions']: + if target['Target']['Id'] in remove_instances: + instances_to_remove.append({'Id': target['Target']['Id'], 'Port': target['Target']['Port']}) + + changed = True + try: + connection.deregister_targets(TargetGroupArn=tg['TargetGroupArn'], Targets=instances_to_remove) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't remove targets") + + if module.params.get("wait"): + status_achieved, registered_instances = wait_for_status(connection, module, tg['TargetGroupArn'], instances_to_remove, 'unused') + if not status_achieved: + module.fail_json(msg='Error waiting for target deregistration - please check the AWS console') + + # register lambda target + else: try: - connection.register_targets(TargetGroupArn=tg['TargetGroupArn'], Targets=instances_to_add) + changed = False + target = module.params.get("targets")[0] + if len(current_targets["TargetHealthDescriptions"]) == 0: + changed = True + else: + for item in current_targets["TargetHealthDescriptions"]: + if target["Id"] != item["Target"]["Id"]: + changed = True + break # only one target is possible with lambda + + if changed: + if target.get("Id"): + response = connection.register_targets( + TargetGroupArn=tg['TargetGroupArn'], + Targets=[ + { + "Id": target['Id'] + } + ] + ) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't register targets") + module.fail_json_aws( + e, msg="Couldn't register targets") + else: + if module.params.get("target_type") != "lambda": - if module.params.get("wait"): - status_achieved, registered_instances = wait_for_status(connection, module, tg['TargetGroupArn'], instances_to_add, 'healthy') - if not status_achieved: - module.fail_json(msg='Error waiting for target registration to be healthy - please check the AWS console') + current_instances = current_targets['TargetHealthDescriptions'] - remove_instances = set(current_instance_ids) - set(new_instance_ids) - - if remove_instances: - instances_to_remove = [] - for target in current_targets['TargetHealthDescriptions']: - if target['Target']['Id'] in remove_instances: + if current_instances: + instances_to_remove = [] + for target in current_targets['TargetHealthDescriptions']: instances_to_remove.append({'Id': target['Target']['Id'], 'Port': target['Target']['Port']}) - changed = True - try: - connection.deregister_targets(TargetGroupArn=tg['TargetGroupArn'], Targets=instances_to_remove) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't remove targets") + changed = True + try: + connection.deregister_targets(TargetGroupArn=tg['TargetGroupArn'], Targets=instances_to_remove) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't remove targets") - if module.params.get("wait"): - status_achieved, registered_instances = wait_for_status(connection, module, tg['TargetGroupArn'], instances_to_remove, 'unused') - if not status_achieved: - module.fail_json(msg='Error waiting for target deregistration - please check the AWS console') - else: - try: - current_targets = connection.describe_target_health(TargetGroupArn=tg['TargetGroupArn']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't get target health") + if module.params.get("wait"): + status_achieved, registered_instances = wait_for_status(connection, module, tg['TargetGroupArn'], instances_to_remove, 'unused') + if not status_achieved: + module.fail_json(msg='Error waiting for target deregistration - please check the AWS console') - current_instances = current_targets['TargetHealthDescriptions'] - - if current_instances: - instances_to_remove = [] - for target in current_targets['TargetHealthDescriptions']: - instances_to_remove.append({'Id': target['Target']['Id'], 'Port': target['Target']['Port']}) - - changed = True - try: - connection.deregister_targets(TargetGroupArn=tg['TargetGroupArn'], Targets=instances_to_remove) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't remove targets") - - if module.params.get("wait"): - status_achieved, registered_instances = wait_for_status(connection, module, tg['TargetGroupArn'], instances_to_remove, 'unused') - if not status_achieved: - module.fail_json(msg='Error waiting for target deregistration - please check the AWS console') + # remove lambda targets + else: + changed = False + if current_targets["TargetHealthDescriptions"]: + changed = True + # only one target is possible with lambda + target_to_remove = current_targets["TargetHealthDescriptions"][0]["Target"]["Id"] + if changed: + connection.deregister_targets( + TargetGroupArn=tg['TargetGroupArn'], Targets=[{"Id": target_to_remove}]) else: try: connection.create_target_group(**params) @@ -600,16 +671,33 @@ def create_or_update_target_group(connection, module): tg = get_target_group(connection, module) if module.params.get("targets"): - params['Targets'] = module.params.get("targets") - try: - connection.register_targets(TargetGroupArn=tg['TargetGroupArn'], Targets=params['Targets']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't register targets") + if module.params.get("target_type") != "lambda": + params['Targets'] = module.params.get("targets") + try: + connection.register_targets(TargetGroupArn=tg['TargetGroupArn'], Targets=params['Targets']) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't register targets") - if module.params.get("wait"): - status_achieved, registered_instances = wait_for_status(connection, module, tg['TargetGroupArn'], params['Targets'], 'healthy') - if not status_achieved: - module.fail_json(msg='Error waiting for target registration to be healthy - please check the AWS console') + if module.params.get("wait"): + status_achieved, registered_instances = wait_for_status(connection, module, tg['TargetGroupArn'], params['Targets'], 'healthy') + if not status_achieved: + module.fail_json(msg='Error waiting for target registration to be healthy - please check the AWS console') + + else: + try: + target = module.params.get("targets")[0] + response = connection.register_targets( + TargetGroupArn=tg['TargetGroupArn'], + Targets=[ + { + "Id": target["Id"] + } + ] + ) + changed = True + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws( + e, msg="Couldn't register targets") # Now set target group attributes update_attributes = [] @@ -712,7 +800,7 @@ def main(): state=dict(required=True, choices=['present', 'absent']), successful_response_codes=dict(), tags=dict(default={}, type='dict'), - target_type=dict(default='instance', choices=['instance', 'ip']), + target_type=dict(default='instance', choices=['instance', 'ip', 'lambda']), targets=dict(type='list'), unhealthy_threshold_count=dict(type='int'), vpc_id=dict(), @@ -722,7 +810,11 @@ def main(): ) module = AnsibleAWSModule(argument_spec=argument_spec, - required_if=[['state', 'present', ['protocol', 'port', 'vpc_id']]]) + required_if=[ + ['target_type', 'instance', ['protocol', 'port', 'vpc_id']], + ['target_type', 'ip', ['protocol', 'port', 'vpc_id']], + ] + ) connection = module.client('elbv2') diff --git a/test/integration/targets/elb_target/playbooks/full_test.yml b/test/integration/targets/elb_target/playbooks/full_test.yml index 03b1c4de02a..18657f8f275 100644 --- a/test/integration/targets/elb_target/playbooks/full_test.yml +++ b/test/integration/targets/elb_target/playbooks/full_test.yml @@ -3,4 +3,5 @@ environment: "{{ ansible_test.environment }}" roles: + - elb_lambda_target - elb_target diff --git a/test/integration/targets/elb_target/playbooks/roles/elb_lambda_target/files/ansible_lambda_target.py b/test/integration/targets/elb_target/playbooks/roles/elb_lambda_target/files/ansible_lambda_target.py new file mode 100644 index 00000000000..0ba9e0d3009 --- /dev/null +++ b/test/integration/targets/elb_target/playbooks/roles/elb_lambda_target/files/ansible_lambda_target.py @@ -0,0 +1,8 @@ +import json + + +def lambda_handler(event, context): + return { + 'statusCode': 200, + 'body': json.dumps('Hello from Lambda!') + } diff --git a/test/integration/targets/elb_target/playbooks/roles/elb_lambda_target/files/assume-role.json b/test/integration/targets/elb_target/playbooks/roles/elb_lambda_target/files/assume-role.json new file mode 100644 index 00000000000..06456f7996d --- /dev/null +++ b/test/integration/targets/elb_target/playbooks/roles/elb_lambda_target/files/assume-role.json @@ -0,0 +1,8 @@ +{ + "Version": "2012-10-17", + "Statement": { + "Effect": "Allow", + "Principal": { "Service": "lambda.amazonaws.com" }, + "Action": "sts:AssumeRole" + } +} diff --git a/test/integration/targets/elb_target/playbooks/roles/elb_lambda_target/tasks/main.yml b/test/integration/targets/elb_target/playbooks/roles/elb_lambda_target/tasks/main.yml new file mode 100644 index 00000000000..54ab112e871 --- /dev/null +++ b/test/integration/targets/elb_target/playbooks/roles/elb_lambda_target/tasks/main.yml @@ -0,0 +1,135 @@ +--- +- 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: yes + +- name: set up lambda as elb_target + + block: + - name: create zip to deploy lambda code + archive: + path: "{{ role_path }}/files/ansible_lambda_target.py" + dest: /tmp/lambda.zip + format: zip + + - name: "create or update service-role for lambda" + iam_role: + <<: *aws_connection_info + name: ansible_lambda_execution + assume_role_policy_document: "{{ lookup('file', role_path + '/files/assume-role.json') }}" + managed_policy: + - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + register: ROLE_ARN + + - name: when it is to fast, the role is not usable. + pause: + minutes: 1 + + - name: deploy lambda.zip to ansible_lambda_target function + lambda: + <<: *aws_connection_info + name: "ansible_lambda_target" + state: present + zip_file: "/tmp/lambda.zip" + runtime: "python3.7" + role: "{{ ROLE_ARN.arn }}" + handler: "ansible_lambda_target.lambda_handler" + timeout: 30 + register: lambda_function + retries: 3 + delay: 15 + until: lambda_function.changed + + - name: create empty target group + elb_target_group: + <<: *aws_connection_info + name: ansible-lambda-targetgroup + target_type: lambda + state: present + modify_targets: False + register: elb_target_group + + - name: tg is created, state must be changed + assert: + that: + - elb_target_group.changed + + - name: allow elb to invoke the lambda function + lambda_policy: + <<: *aws_connection_info + state: present + function_name: ansible_lambda_target + version: "{{ lambda_function.configuration.version }}" + statement_id: elb1 + action: lambda:InvokeFunction + principal: elasticloadbalancing.amazonaws.com + source_arn: "{{ elb_target_group.target_group_arn }}" + + - name: add lambda to elb target + elb_target_group: + <<: *aws_connection_info + name: ansible-lambda-targetgroup + target_type: lambda + state: present + targets: + - Id: "{{ lambda_function.configuration.function_arn }}" + register: elb_target_group + + - name: target is updated, state must be changed + assert: + that: + - elb_target_group.changed + + - name: re-add lambda to elb target (idempotency) + elb_target_group: + <<: *aws_connection_info + name: ansible-lambda-targetgroup + target_type: lambda + state: present + targets: + - Id: "{{ lambda_function.configuration.function_arn }}" + register: elb_target_group + + - name: target is still the same, state must not be changed (idempotency) + assert: + that: + - not elb_target_group.changed + + - name: remove lambda target from target group + elb_target_group: + <<: *aws_connection_info + name: ansible-lambda-targetgroup + target_type: lambda + state: absent + targets: [] + register: elb_target_group + + - name: target is still the same, state must not be changed (idempotency) + assert: + that: + - elb_target_group.changed + + always: + - name: remove elb target group + elb_target_group: + <<: *aws_connection_info + name: ansible-lambda-targetgroup + target_type: lambda + state: absent + + - name: remove lambda function + lambda: + <<: *aws_connection_info + name: "ansible_lambda_target" + state: absent + + - name: remove iam role for lambda + iam_role: + <<: *aws_connection_info + name: ansible_lambda_execution + state: absent