如何自动将第二个弹性网络接口 (ENI) 附加到通过 Auto Scaling 启动的实例?

上次更新时间:2019 年 5 月 10 日

当 Auto Scaling 在横向扩展或运行状况检查更换期间建立新实例时,我想自动附加第二个弹性网络接口。

简短描述

Auto Scaling 不直接支持此使用案例,但通过结合使用 Auto Scaling 生命周期挂钩,Amazon CloudWatch Events 和 AWS Lambda 函数,您可以建立和使用类似的功能。

进行以下配置:

  • 创建生命周期挂钩,当 Auto Scaling 启动新实例时,发送事件“EC2 Instance-launch Lifecycle Action”。
  • 设置该事件以触发 Lambda 函数。
  • 将 Lambda 函数设置为:在实例处于等待状态时,自动将第二个网络接口附加到实例。

解决方法

Amazon Lambda 控制台创建当 Auto Scaling 启动实例时自动向该实例添加第二个网络接口的 Lambda 函数:

1.    选择创建函数

2.    将 Attach_Second_ENI_ASG 添加到名称字段,并为运行时选择 Python 2.7

3.    对于角色,请选择允许 Lambda 描述、创建和附加接口到实例的 IAM 角色,或选择创建自定义角色

如果选择创建自定义角色,则会打开 IAM 控制台窗口。对于 IAM 角色,选择创建新的 IAM 角色。然后展开策略文档下拉列表,并添加以下策略:

{
                "Statement": [
                    {
                        "Action": [
                            "logs:CreateLogGroup",
                            "logs:CreateLogStream",
                            "logs:PutLogEvents"
                        ],
                        "Effect": "Allow",
                        "Resource": "arn:aws:logs:*:*:*"
                    },
                    {
                        "Action": [
                            "ec2:CreateNetworkInterface",
                            "ec2:DescribeNetworkInterfaces",
                            "ec2:DetachNetworkInterface",
                            "ec2:DeleteNetworkInterface",
                            "ec2:AttachNetworkInterface",
                            "ec2:DescribeInstances",
                            "autoscaling:CompleteLifecycleAction"
                        ],
                        "Effect": "Allow",
                        "Resource": "*"
                    }
                ],
                "Version": "2012-10-17"
            }

然后选择允许。IAM 控制台窗口关闭。在 Lambda 控制台中,为现有角色选择新角色。

4.    选择创建函数

5.    将以下内容添加到函数代码字段,然后选择保存

import boto3
import botocore
from datetime import datetime

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


def lambda_handler(event, context):
    if event["detail-type"] == "EC2 Instance-launch Lifecycle Action":
        instance_id = event['detail']['EC2InstanceId']
        LifecycleHookName=event['detail']['LifecycleHookName']
        AutoScalingGroupName=event['detail']['AutoScalingGroupName']
        subnet_id = get_subnet_id(instance_id)
        interface_id = create_interface(subnet_id)
        attachment = attach_interface(interface_id, instance_id)
        if not interface_id:
            complete_lifecycle_action_failure(LifecycleHookName,AutoScalingGroupName,instance_id)
        elif not attachment:
            complete_lifecycle_action_failure(LifecycleHookName,AutoScalingGroupName,instance_id)
            delete_interface(interface_id)
        else:
            complete_lifecycle_action_success(LifecycleHookName,AutoScalingGroupName,instance_id)
       
        
def get_subnet_id(instance_id):
    try:
        result = ec2_client.describe_instances(InstanceIds=[instance_id])
        vpc_subnet_id = result['Reservations'][0]['Instances'][0]['SubnetId']
        log("Subnet id: {}".format(vpc_subnet_id))

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

    return vpc_subnet_id


def create_interface(subnet_id):
    network_interface_id = None

    if subnet_id:
        try:
            network_interface = ec2_client.create_network_interface(SubnetId=subnet_id)
            network_interface_id = network_interface['NetworkInterface']['NetworkInterfaceId']
            log("Created network interface: {}".format(network_interface_id))
        except botocore.exceptions.ClientError as e:
            log("Error creating network interface: {}".format(e.response['Error']))

    return network_interface_id


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: {}".format(attachment))
        except botocore.exceptions.ClientError as e:
            log("Error attaching network interface: {}".format(e.response['Error']))

    return attachment


def delete_interface(network_interface_id):
    try:
        ec2_client.delete_network_interface(
            NetworkInterfaceId=network_interface_id
        )
        log("Deleted network interface: {}".format(network_interface_id))
        return True

    except botocore.exceptions.ClientError as e:
        log("Error deleting interface {}: {}".format(network_interface_id, e.response['Error']))
        
        
def complete_lifecycle_action_success(hookname,groupname,instance_id):
    try:
        asg_client.complete_lifecycle_action(
                LifecycleHookName=hookname,
                AutoScalingGroupName=groupname,
                InstanceId=instance_id,
                LifecycleActionResult='CONTINUE'
            )
        log("Lifecycle hook CONTINUEd for: {}".format(instance_id))
    except botocore.exceptions.ClientError as e:
            log("Error completing life cycle hook for instance {}: {}".format(instance_id, e.response['Error']))
            log('{"Error": "1"}')    
            
def complete_lifecycle_action_failure(hookname,groupname,instance_id):
    try:
        asg_client.complete_lifecycle_action(
                LifecycleHookName=hookname,
                AutoScalingGroupName=groupname,
                InstanceId=instance_id,
                LifecycleActionResult='ABANDON'
            )
        log("Lifecycle hook ABANDONed for: {}".format(instance_id))
    except botocore.exceptions.ClientError as e:
            log("Error completing life cycle hook for instance {}: {}".format(instance_id, e.response['Error']))
            log('{"Error": "1"}')    
    

def log(error):
    print('{}Z {}'.format(datetime.utcnow().isoformat(), error))

使用以下 AWS 命令行界面 (AWS CLI) 命令创建生命周期挂钩以触发事件:

aws autoscaling put-lifecycle-hook \
    --lifecycle-hook-name my-lifecycle-launch-hook \
    --auto-scaling-group-name my-asg \
    --lifecycle-transition autoscaling:EC2_INSTANCE_LAUNCHING \
    --heartbeat-timeout 300 \
    --default-result ABANDON

打开 CloudWatch 控制台并创建 CloudWatch Events 规则来触发 Lambda 函数,操作步骤如下:

1.    从左侧导航窗格中,选择“事件”下面的规则

2.    选择创建规则

3.    对于服务名称,选择 Auto Scaling

4.    对于事件类型,选择实例启动和终止

5.    选择特定实例事件,然后选择 EC2 Instance-launch Lifecycle Action

6.    选择特定组名,然后选择 Auto Scaling 组或组以在启动时自动附加第二个网络接口。

7.    选择添加目标,然后选择新的 Lambda 函数。

8.    选择配置详细信息

9.    为规则添加名称,然后选择创建规则

现在,当 Auto Scaling 启动新实例时,您指定的第二个网络接口将附加到新实例。

注意:如果您未在启动配置中使用 Amazon Linux AMI,则可能需要在操作系统级别配置其他选项以启用额外接口。

为避免耗尽子网中的私有 IP 地址和达到账户的 ENI 限制,必须编写代码,以便在 Auto Scaling 终止实例时删除第二个网络接口:

  • 您可以使用终止生命周期挂钩,然后通过 CloudWatch Events 或 Amazon Simple Notification Service (Amazon SNS) 触发 Lambda 函数,以使用 DeviceIndex>0 分离和删除该接口。
  • 您还可以编写 boto 脚本,定期删除账户中未使用的接口,或者使用 modify-network-interface-attribute AWS CLI 命令修改网络接口的“delete on termination”(删除终止)属性。

您还可以使用 SNS 而不是 CloudWatch Events 来调用 Lambda 函数,方法是在创建挂钩时配置 --notification-target-arn

有关其他信息,请参阅将 AWS Lambda 与来自不同账户的 Amazon SNS 结合使用


这篇文章对您有帮助吗?

您觉得我们哪些地方需要改进?


需要更多帮助?