diff --git a/lib/ansible/modules/cloud/amazon/ec2_instance.py b/lib/ansible/modules/cloud/amazon/ec2_instance.py index 9437afee4a..86c2e7cb1e 100644 --- a/lib/ansible/modules/cloud/amazon/ec2_instance.py +++ b/lib/ansible/modules/cloud/amazon/ec2_instance.py @@ -737,6 +737,47 @@ def build_volume_spec(params): return [ec2_utils.snake_dict_to_camel_dict(v, capitalize_first=True) for v in volumes] +def add_or_update_instance_profile(instance, desired_profile_name): + instance_profile_setting = instance.get('IamInstanceProfile') + if instance_profile_setting and desired_profile_name: + if desired_profile_name in (instance_profile_setting.get('Name'), instance_profile_setting.get('Arn')): + # great, the profile we asked for is what's there + return False + else: + desired_arn = determine_iam_role(desired_profile_name) + if instance_profile_setting.get('Arn') == desired_arn: + return False + # update association + ec2 = module.client('ec2') + try: + association = ec2.describe_iam_instance_profile_associations(Filters=[{'Name': 'instance-id', 'Values': [instance['InstanceId']]}]) + except botocore.exceptions.ClientError as e: + # check for InvalidAssociationID.NotFound + module.fail_json_aws(e, "Could not find instance profile association") + try: + resp = ec2.replace_iam_instance_profile_association( + AssociationId=association['IamInstanceProfileAssociations'][0]['AssociationId'], + IamInstanceProfile={'Arn': determine_iam_role(desired_profile_name)} + ) + return True + except botocore.exceptions.ClientError as e: + module.fail_json_aws(e, "Could not associate instance profile") + + if not instance_profile_setting and desired_profile_name: + # create association + ec2 = module.client('ec2') + try: + resp = ec2.associate_iam_instance_profile( + IamInstanceProfile={'Arn': determine_iam_role(desired_profile_name)}, + InstanceId=instance['InstanceId'] + ) + return True + except botocore.exceptions.ClientError as e: + module.fail_json_aws(e, "Could not associate new instance profile") + + return False + + def build_network_spec(params, ec2=None): """ Returns list of interfaces [complex] @@ -1292,9 +1333,9 @@ def pretty_instance(i): def determine_iam_role(name_or_arn): if re.match(r'^arn:aws:iam::\d+:instance-profile/[\w+=/,.@-]+$', name_or_arn): return name_or_arn - iam = module.client('iam') + iam = module.client('iam', retry_decorator=AWSRetry.jittered_backoff()) try: - role = iam.get_instance_profile(InstanceProfileName=name_or_arn) + role = iam.get_instance_profile(InstanceProfileName=name_or_arn, aws_retry=True) return role['InstanceProfile']['Arn'] except botocore.exceptions.ClientError as e: if e.response['Error']['Code'] == 'NoSuchEntity': @@ -1313,6 +1354,8 @@ def handle_existing(existing_matches, changed, ec2, state): changes = diff_instance_and_params(existing_matches[0], module.params) for c in changes: ec2.modify_instance_attribute(**c) + changed |= bool(changes) + changed |= add_or_update_instance_profile(existing_matches[0], module.params.get('instance_role')) changed |= change_network_attachments(existing_matches[0], module.params, ec2) altered = find_instances(ec2, ids=[i['InstanceId'] for i in existing_matches]) module.exit_json( diff --git a/test/integration/targets/ec2_instance/tasks/iam_instance_role.yml b/test/integration/targets/ec2_instance/tasks/iam_instance_role.yml index a20a9e0dc7..b8654ed286 100644 --- a/test/integration/targets/ec2_instance/tasks/iam_instance_role.yml +++ b/test/integration/targets/ec2_instance/tasks/iam_instance_role.yml @@ -19,6 +19,17 @@ <<: *aws_connection_info register: iam_role + - name: Create second IAM role for test + iam_role: + name: "{{ resource_prefix }}-test-policy-2" + assume_role_policy_document: "{{ lookup('file','assume-role-policy.json') }}" + state: present + create_instance_profile: yes + managed_policy: + - AmazonEC2ContainerServiceRole + <<: *aws_connection_info + register: iam_role_2 + - name: Wait for IAM role to be available, otherwise the next step will fail (Invalid IAM Instance Profile name) command: sleep 10 @@ -36,6 +47,21 @@ that: - 'instance_with_role.instances[0].iam_instance_profile.arn == iam_role.arn.replace(":role/", ":instance-profile/")' + - name: Update instance with new instance_role + ec2_instance: + name: "{{ resource_prefix }}-test-default-vpc" + image_id: "{{ ec2_ami_image[aws_region] }}" + security_groups: "{{ sg.group_id }}" + instance_type: t2.micro + instance_role: "{{ resource_prefix }}-test-policy-2" + <<: *aws_connection_info + register: instance_with_updated_role + + - assert: + that: + - 'instance_with_updated_role.instances[0].iam_instance_profile.arn == iam_role_2.arn.replace(":role/", ":instance-profile/")' + - 'instance_with_updated_role.instances[0].instance_id == instance_with_role.instances[0].instance_id' + always: - name: Terminate instance ec2: @@ -49,13 +75,16 @@ - name: Delete IAM role for test iam_role: - name: "{{ resource_prefix }}-test-policy" + name: "{{ item }}" assume_role_policy_document: "{{ lookup('file','assume-role-policy.json') }}" state: absent create_instance_profile: yes managed_policy: - AmazonEC2ContainerServiceRole <<: *aws_connection_info + loop: + - "{{ resource_prefix }}-test-policy" + - "{{ resource_prefix }}-test-policy-2" register: removed until: removed is not failed ignore_errors: yes