Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ec2_instance: Add count parameter to ec2_instance #539

Merged
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelogs/fragments/539-ec2_instance_add_count_param.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
minor_changes:
- ec2_instance - add count parameter support
(https://github.com/ansible-collections/amazon.aws/pull/539).
108 changes: 106 additions & 2 deletions plugins/modules/ec2_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
instance_ids:
description:
- If you specify one or more instance IDs, only instances that have the specified IDs are returned.
- Mutually exclusive with I(exact_count).
type: list
elements: str
state:
Expand All @@ -36,6 +37,7 @@
- "I(state=restarted): convenience alias for I(state=stopped) immediately followed by I(state=started)"
- "I(state=terminated): ensures an existing instance is terminated."
- "I(state=absent): alias for I(state=terminated)"
- Mutually exclusive with I(exact_count).
choices: [present, terminated, running, started, stopped, restarted, rebooted, absent]
default: present
type: str
Expand All @@ -55,6 +57,17 @@
Only required when instance is not already present.
default: t2.micro
type: str
count:
description:
- Number of instances to launch.
mandar242 marked this conversation as resolved.
Show resolved Hide resolved
- Mutually exclusive with I(exact_count).
type: int
exact_count:
description:
- An integer value which indicates how many instances that match the I(filters) parameter should be running.
- Instances are either created or terminated based on this value.
- Mutually exclusive with I(count), I(state), and I(instance_ids).
type: int
user_data:
description:
- Opaque blob of data which is made available to the ec2 instance
Expand Down Expand Up @@ -415,6 +428,7 @@
tags:
Env: "eni_on"
instance_type: t2.micro

- name: start an instance with metadata options
amazon.aws.ec2_instance:
name: "public-metadataoptions-instance"
Expand All @@ -426,6 +440,35 @@
metadata_options:
http_endpoint: enabled
http_tokens: optional

# ensure number of instances running with a tag matches exact_count
- name: start multiple instances
amazon.aws.ec2_instance:
instance_type: t3.small
image_id: ami-123456
exact_count: 5
region: us-east-2
network:
assign_public_ip: yes
security_group: default
vpc_subnet_id: subnet-0123456
tags:
foo: bar

# launches multiple instances - specific number of instances
- name: start specific number of multiple instances
amazon.aws.ec2_instance:
instance_type: t3.small
image_id: ami-123456
count: 3
region: us-east-2
network:
assign_public_ip: yes
security_group: default
vpc_subnet_id: subnet-0123456
state: present
tags:
foo: bar
'''

RETURN = '''
Expand Down Expand Up @@ -848,6 +891,7 @@
except ImportError:
pass # caught by AnsibleAWSModule


from ansible.module_utils._text import to_native
from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict
from ansible.module_utils.common.dict_transformations import snake_dict_to_camel_dict
Expand Down Expand Up @@ -1250,6 +1294,14 @@ def build_run_instance_spec(params):
if params.get('instance_role'):
spec['IamInstanceProfile'] = dict(Arn=determine_iam_role(params.get('instance_role')))

if module.params.get('exact_count'):
spec['MaxCount'] = module.params.get('to_launch')
spec['MinCount'] = module.params.get('to_launch')

if module.params.get('count'):
spec['MaxCount'] = module.params.get('count')
spec['MinCount'] = module.params.get('count')

spec['InstanceType'] = params['instance_type']
return spec

Expand Down Expand Up @@ -1699,6 +1751,51 @@ def handle_existing(existing_matches, state):
return result


def enforce_count(existing_matches, module):
exact_count = module.params.get('exact_count')
changed = False

try:
instances = existing_matches
mandar242 marked this conversation as resolved.
Show resolved Hide resolved
if len(instances) == exact_count:
module.exit_json(
changed=False,
msg='{0} instances already running, nothing to do.'.format(exact_count)
)

elif len(instances) < exact_count:
changed = True
to_launch = exact_count - len(instances)
module.params['to_launch'] = to_launch
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could overwrite module.params['count'] here to avoid the extra code path in build_run_instance_spec

@jillr do you have any opinions on this?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I generally don't like overwriting module.params, I worry that it's asking for issues down the road when we make some other change that uses count and expects it to be whatever the user originally specified. I think I'd rather have the extra conditional?

# launch instances
try:
ensure_present(existing_matches=instances, desired_module_state='present')
except botocore.exceptions.ClientError as e:
tremble marked this conversation as resolved.
Show resolved Hide resolved
module.fail_json(e, msg='Unable to launch instances')
elif len(instances) > exact_count:
to_terminate = len(instances) - exact_count
# get the instance ids of instances with the count tag on them
all_instance_ids = sorted([x['InstanceId'] for x in instances])
terminate_ids = all_instance_ids[0:to_terminate]
instances = [x for x in instances if x['InstanceId'] not in terminate_ids]
mandar242 marked this conversation as resolved.
Show resolved Hide resolved
if module.check_mode:
module.exit_json(changed=True, msg='Would have terminated following instances if not in check mode {0}'.format(terminate_ids))
# terminate instances
try:
result = client.terminate_instances(InstanceIds=terminate_ids)
await_instances(terminate_ids, desired_module_state='terminated', force_wait=True)
except botocore.exceptions.ClientError as e:
module.fail_json(e, msg='Unable to terminate instances')
module.exit_json(
changed=True,
msg='Successfully terminated instances.',
terminated_ids=terminate_ids,
)

except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
module.fail_json_aws(e, msg="Failed to enforce instance count")


def ensure_present(existing_matches, desired_module_state):
tags = dict(module.params.get('tags') or {})
name = module.params.get('name')
Expand All @@ -1712,6 +1809,7 @@ def ensure_present(existing_matches, desired_module_state):
module.exit_json(
changed=True,
spec=instance_spec,
msg='Would have launched instances if not in check_mode.',
)
instance_response = run_instances(**instance_spec)
instances = instance_response['Instances']
Expand Down Expand Up @@ -1817,7 +1915,8 @@ def main():
state=dict(default='present', choices=['present', 'started', 'running', 'stopped', 'restarted', 'rebooted', 'terminated', 'absent']),
wait=dict(default=True, type='bool'),
wait_timeout=dict(default=600, type='int'),
# count=dict(default=1, type='int'),
count=dict(type='int'),
exact_count=dict(type='int'),
image=dict(type='dict'),
image_id=dict(type='str'),
instance_type=dict(default='t2.micro', type='str'),
Expand Down Expand Up @@ -1861,6 +1960,9 @@ def main():
['availability_zone', 'vpc_subnet_id'],
['tower_callback', 'user_data'],
['image_id', 'image'],
['exact_count', 'count'],
['exact_count', 'state'],
['exact_count', 'instance_ids'],
mandar242 marked this conversation as resolved.
Show resolved Hide resolved
],
supports_check_mode=True
)
Expand Down Expand Up @@ -1895,7 +1997,9 @@ def main():
msg='No matching instances found',
changed=False,
)
elif existing_matches:
elif module.params.get('exact_count'):
enforce_count(existing_matches, module)
elif existing_matches and not module.params.get('count'):
for match in existing_matches:
warn_if_public_ip_assignment_changed(match)
warn_if_cpu_options_changed(match)
Expand Down
1 change: 1 addition & 0 deletions tests/integration/targets/ec2_instance/inventory
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[tests]
instance_minimal
instance_multiple
checkmode_tests
termination_protection
ebs_optimized
Expand Down
Loading