1台のEC2を低コストで冗長化するシンプルな仕組み。AutoScalingのLifecycle hookでENIをLambdaに移動させます。

![SecondENI.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276897/a63aa4b4-26c1-e649-87e8-98773cffe237.gif)


##EC2がダウンした場合、同じIPをアサインして通信を継続できるようにするにはどうしたらいい？


- [x] **テーマ**

EC2に予期せぬ障害が発生した際、自動復旧して同じプライベートIPアドレスで処理を継続する方法を解説します。

使用サービス
 - Lambda
 - CloudWatch Event
 - AutoScaling

- [x] **結論**
・EC2には予め2つ目のENIをアタッチし、これを移動することで実現可能
・AutoScaling発動時に任意のイベントを指定できる「Lifecyclehook」機能を使用する
・「Lifecyclehook」のイベントにLambdaの起動を指定する
・LambdaにはENI (Elastic Network Interface)を新EC2にアタッチするようプログラミングすることで可能

- [x] **信頼性**
2019年5月10日にAWS公式サイト「knowledge-center」に再掲された内容に基づくので、信頼性は高いと思います。
記事のリンクを張っておきます。「[How do I automatically attach a second elastic network interface (ENI) to an instance launched through Auto Scaling?](https://aws.amazon.com/jp/premiumsupport/knowledge-center/attach-second-eni-auto-scaling/)」

この記事では「AutoScaling機能で自動起動したEC2に2つ目のENIをアタッチする」というもので、プライベートIPを指定できずランダムにアサインされます。

そこで、少しコードを改変し、「ENIのIDを明記し、元々EC2にアタッチされていたENIを新EC2の2つ目のENIとしてアタッチする」ようにしました。

**他の環境での再現性も高いです。**

ただし、設定にもよりますが通信が4分ほど遮断されるのでオンラインの処理にはLoad Blancerの使用を推奨します。

それでは、以下に設定手順を記載します。

##事前準備
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276897/dd333c28-38a9-babf-f9ac-c4973399e412.png)


AutoScaling Groupを作成します。
公式ドキュメント「[Amazon EC2 Auto Scaling の使用開始](https://docs.aws.amazon.com/ja_jp/autoscaling/ec2/userguide/GettingStartedTutorial.html)」

ENIを作成します。
公式ドキュメント「[ネットワークインターフェイスを作成する](https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/using-eni.html#create_eni)」


稼働中EC2にENIを追加でアタッチします。
公式ドキュメント「[インスタンスの起動時にネットワークインターフェイスをアタッチする](https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/using-eni.html#attach_eni_launch)」


##Lambdaの設定
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276897/cdbf6b05-ec26-bd74-6dbe-b292927de42c.png)



Python 2.7でLambdaを作成します。
LambdaのIAMロールに以下を指定します。

IAMについてはclassmethodさんの「[AWS IAMポリシーを理解する](https://dev.classmethod.jp/cloud/aws-iam-policy/)」の記事が分かりやすかったので、参考にしてみて下さい。

```
{
                "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"
            }
```


Lambdaの関数に以下をペーストします。

「def create_interface(subnet_id):」の**eni-0123456789abcdefg**をアタッチしたいENIのIDに変更して下さい。（前後のダブルコーテーションは削除しないで下さい）


```python

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 = "eni-0123456789abcdefg"
            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 CLIコマンドを実行し、Lifecycle-hookイベントを作成します。
任意のEC2インスタンスにログインし、実行してください。

```
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 eventの設定
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276897/8ef005c1-3f57-eccb-3ab2-7575b4eb23f4.png)




CloudWatch consoleでEventを設定します。

公式ドキュメントにも設定方法が載っているので一応リンクを貼っておきます。「[イベントでトリガーする CloudWatch イベント ルールの作成](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/events/Create-CloudWatch-Events-Rule.html)」

Service Nameを Auto Scalingにする

Event Typeを Instance Launch and Terminateにする

Specific instance event(s),を選択し、 EC2 Instance-launch Lifecycle Actionとする

Specific group name(s),を選択し、設定したAutoScaling名を選択する（my-asg）

右側のペインの設定↓

Add targetを選択し、作成したLabmdaを指定します。


##では、EC2をターミネートして試してみましょう


IPは「172.24.0.150」と「**172.24.0.136**」　2つあります。 末尾が「22a」のEC2です。
![15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276897/861f9d0e-e2c8-f0b4-1fbd-e2a391f41984.png)




「22a」さん、さようなら
![6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276897/3ec67673-3dc7-a704-4b05-d8dfff79cdfe.png)





新しいものが起動してきました。末尾「c27」です。
![10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276897/48ebc4d0-a06d-572c-4f2d-6be7c346943c.png)






「**172.24.0.136**」のIPが引き継がれましたね。 (今回、偶然150のIPも一緒でした。)
![20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276897/54f154e3-3bb4-d67b-4cb0-be75a679ad44.png)





##発展


今回のように、ENIを使えば「シングルインスタンスでバッチ処理をコスト低で冗長運用できる」のでおすすめです。

このようにENIは様々な使い方ができて大変便利です。

・例えば**2つのSSL証明書をプライベートIPアドレスに1つずつ割り当てることで1つのインスタンスで2つのサイトを運営するテクニック**

・AWS公式サイトで紹介されている**「Management Network」テクニック**で、一つのWEBサーバが、インターネットに対面しているパブリックなENIとSSHアクセス向けのプライベートなENIを持ち、オンプレはWEBサーバーにSSHアクセスし、インターネット通信を使用して安全にfacebookを見たりできたり

このあたりのテクニックをまとめたVPCについての記事リンクを貼っておきます。
「[VPCとは何か？それはAWSに張り巡らされた1つの血管であった。　〜AWS Virtual Private Cloud 〜](https://www.awsskill.com/121/vpc/
)」

今回はAutoScalingのイベントを契機にしましたが、おそらく「任意の監視で引っかかった場合に発動」といった方が便利そうです。

このあたりの監視と絡めたテクニックもまとめてQiitaに投稿したいと思います。

ありがとうございました。