Skip to content

Commit

Permalink
Merge pull request awslabs#197 from snehrao/patch-4
Browse files Browse the repository at this point in the history
Patch 4
  • Loading branch information
harniva14 authored Apr 15, 2022
2 parents 7e5e2aa + 015b8a5 commit 2a9b970
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 124 deletions.
123 changes: 0 additions & 123 deletions EC2 Auto Scaling/Multiple ENI Auto Scaling group/MultipleENI.py

This file was deleted.

180 changes: 180 additions & 0 deletions EC2 Auto Scaling/Multiple ENI Auto Scaling group/MultipleENIforASG.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@


import boto3
import botocore
import sys
from datetime import datetime

ec2_client = boto3.client('ec2')
asg_client = boto3.client('autoscaling')


def lambda_handler(event, context):
global LifecycleHookName, AutoScalingGroupName, LifecycleActionToken, instance_id
instance_id = event["detail"]["EC2InstanceId"]
LifecycleHookName = event['detail']['LifecycleHookName']
AutoScalingGroupName = event['detail']['AutoScalingGroupName']
LifecycleActionToken = event['detail']['LifecycleActionToken']
instance_result = ec2_client.describe_instances(InstanceIds=[instance_id])
asg_response = asg_client.describe_auto_scaling_groups(
AutoScalingGroupNames=[event['detail']['AutoScalingGroupName']])

if event["detail"]["Destination"] == "AutoScalingGroup":

#This function will check if the instance already has multiple ENIs attached. If a warm pool is configured in the auto scaling group and --instance-reuse-policy ’{“ReuseOnScaleIn”: true} was configured while configuring the warm pool, a second ENI will already be attached to the instance that scaled in to the warm pool
if check_number_of_interfaces(instance_result) > 1:
log("The instance {} already has more than one interface attached.".format(instance_id))


# If the instance is launched for the first time into the auto scaling group or from the warm pool(not scaled into warm pool instances), the following step will be executed
else:

subnet_id = get_subnet_id(instance_result)
interface_id = create_interface(asg_response, instance_result, subnet_id)
attachment = attach_interface(interface_id, instance_id)


#Will check for error with network interface attachment. If there is any error, complete lifecycle call is run to terminate the instance
if interface_id and not attachment:
log("Removing network interface {} after attachment failed.".format(interface_id))
log('{"Error": "1"}')
delete_interface(interface_id)
abandon_lifecycle_action(LifecycleHookName, AutoScalingGroupName, instance_id, LifecycleActionToken)
return

#Modifying network interface attribute to set DeleteOnTermination as True. This ensures the interface is deleted when the instance is terminated, to prevent hitting EC2 quotas
set_delete_true = ec2_client.modify_network_interface_attribute(
Attachment={
'AttachmentId': attachment,
'DeleteOnTermination': True,
},
NetworkInterfaceId= interface_id,
)

log('{"Exit": "0"}')

#Attaching of second interface is skipped if the instance is launched into the warm pool to avoid hitting network interfaces limit while the instance is not being used and is in warm pool.
#This section is run when the instance is launched into a warm pool.
else:
log("Instance {} launching in warm pool. No action taken".format(instance_id))

continue_lifecycle_action(LifecycleHookName, AutoScalingGroupName, instance_id, LifecycleActionToken)



# check_number_of_interfaces function checks the number of interfaces attached to the instances
def check_number_of_interfaces(instance_result):
interfaces = instance_result['Reservations'][0]['Instances'][0]['NetworkInterfaces']
return len(interfaces)

# get_subnet_id function checks the subnet id of the instance's first ENI
def get_subnet_id(instance_result):
try:
vpc_subnet_id = instance_result['Reservations'][0]['Instances'][0]['SubnetId']
log("Subnet id: {} for Instance {}".format(vpc_subnet_id,instance_id))

except botocore.exceptions.ClientError as e:
log("Error describing the instance {}: {}".format(instance_id, e.response['Error']['Code']))
vpc_subnet_id = None
abandon_lifecycle_action(LifecycleHookName, AutoScalingGroupName, instance_id, LifecycleActionToken)

return vpc_subnet_id

# create_interface will create a new interface in the subnet that is not the subnet of the instance's first ENI
def create_interface(asg_response,instance_result,subnet_id):
network_interface_id = None
try:
string_response= asg_response["AutoScalingGroups"][0]["VPCZoneIdentifier"]
list_response = string_response.split(",");
instancezone= instance_result['Reservations'][0]['Instances'][0]['Placement']['AvailabilityZone']
AZsubnets= []
for item in list_response:
zones = ec2_client.describe_subnets(SubnetIds=[item])
azones= zones["Subnets"][0]["AvailabilityZone"]
if azones==instancezone:
AZsubnets.append(item)

#checks if the AZs that the auto scaling group is enabled in has 2 subnets in each AZ
if AZsubnets[0]== subnet_id:
if len(AZsubnets)==1:
log("The AZ {} has only one subnet in the auto scaling group".format(azones))
abandon_lifecycle_action(LifecycleHookName, AutoScalingGroupName, instance_id, LifecycleActionToken)

else:
subnet_id=AZsubnets[1]
else:
subnet_id=AZsubnets[0]
network_interface = ec2_client.create_network_interface(SubnetId=subnet_id)
network_interface_id = network_interface['NetworkInterface']['NetworkInterfaceId']
log("Created network interface: {} for Instance {}".format(network_interface_id,instance_id))

except botocore.exceptions.ClientError as e:
log("Error creating network interface: {} for Instance {}".format(e.response['Error']['Code'],instance_id))
abandon_lifecycle_action(LifecycleHookName, AutoScalingGroupName, instance_id, LifecycleActionToken)
return network_interface_id

#attach_interface will attach the newly created interface as the second interface
def attach_interface(network_interface_id, instance_id):
attachment = None

if network_interface_id and instance_id:
try:
attach_interface = ec2_client.attach_network_interface(
NetworkInterfaceId=network_interface_id,
InstanceId=instance_id,
DeviceIndex=1
)
attachment = attach_interface['AttachmentId']
log("Created network attachment: {} for Instance {}".format(attachment,instance_id))
except botocore.exceptions.ClientError as e:
log("Error attaching network interface: {} for Instance {}".format(e.response['Error']['Code'],instance_id))
abandon_lifecycle_action(LifecycleHookName, AutoScalingGroupName, instance_id, LifecycleActionToken)
return attachment

#delete_interface is run when there might be an error while attaching the secondary interface
def delete_interface(network_interface_id):
try:
ec2_client.delete_network_interface(
NetworkInterfaceId=network_interface_id
)
return True

except botocore.exceptions.ClientError as e:
log("Error deleting interface {}: {}".format(network_interface_id, e.response['Error']['Code']))


#Run complete-lifecycle-action call to continue bringing the instance InService
def continue_lifecycle_action(hookname, groupname, instance_id, tokenname):
try:
asg_client.complete_lifecycle_action(
LifecycleHookName=hookname,
AutoScalingGroupName=groupname,
InstanceId=instance_id,
LifecycleActionToken=tokenname,
LifecycleActionResult='CONTINUE'
)
log("Completing Lifecycle hook for instance {} with Result:CONTINUE".format(instance_id))

except botocore.exceptions.ClientError as e:
log("Error completing life cycle hook for instance {}: {}".format(instance_id, e.response['Error']['Code']))
log('{"Error": "1"}')
return

#Run complete-lifecycle-action call to terminate the instance due to an error
def abandon_lifecycle_action(hookname,groupname,instance_id,tokenname):
try:
asg_client.complete_lifecycle_action(
LifecycleHookName=hookname,
AutoScalingGroupName=groupname,
InstanceId=instance_id,
LifecycleActionToken=tokenname,
LifecycleActionResult='ABANDON'
)
log("Completing Lifecycle hook for instance {} with Result:ABANDON".format(instance_id))
except botocore.exceptions.ClientError as e:
log("Error completing life cycle hook for instance {}: {}".format(instance_id, e.response['Error']['Code']))
log('{"Error": "1"}')
return

def log(message):
print('{}Z {}'.format(datetime.utcnow().isoformat(), message))
4 changes: 3 additions & 1 deletion EC2 Auto Scaling/Multiple ENI Auto Scaling group/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
The script lets you automatically attach a second elastic network interface (ENI) in a difference subnet than an instance's primary ENI when Amazon EC2 Auto Scaling launches a new instance.
You can optionally have one of the ENI's in a public subnet and the other in a private subnet. The scripts also deletes ENIs attached to the instance when instances are being terminated to avoid exhausting private IP addresses in the subnet and reaching the ENI limit in your account.

Note: The following resolution is for an Auto Scaling Group enabled in single or multiple Availability Zones, with two or more subnets in each enabled Availability Zone. Amazon EC2 does not allow multiple ENIs in different Availability Zones to be attached to the same instance simultaneously
**This solution supports Warm pools and scale into Warm pools**.

**Note:** The following resolution is for an Auto Scaling Group enabled in single or multiple Availability Zones, with two or more subnets in each enabled Availability Zone. Amazon EC2 does not allow multiple ENIs in different Availability Zones to be attached to the same instance simultaneously

This script is for a lambda function which uses Python 3.8 for the Runtime. This script is triggered by a lifecycle hook in the auto scaling group

0 comments on commit 2a9b970

Please sign in to comment.