AutoScalingで、別VPCにENIを作ってEC2にアタッチ

# はじめに
以前、AutoScalingでスケールアウト時に、別VPCに作っておいたENIをアタッチする内容を記事にしました。

https://qiita.com/a_b_/items/136fb263664175d4ae1f

本方法だと、事前にENIを作成しておきます。

今回は、都度ENIを作ってアタッチするようにしてみました。


# 概要

- AutoScalingグループにライフサイクルフックを追加して、EventBridge -> StepFunctionsでアタッチします
- StepFunctionsで、ENIを作成しアタッチします
    - ENIはアタッチ後に、”EC2終了後に自動削除を有効”の設定にします

# 構成図

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/755d54f1-7486-f55e-dbd6-95d9e00e3158.png)



# CloudFormationテンプレート

長いですが、CloudFormationは以下になります。

<details><summary>クリックで表示</summary><div>

AZは一つだけです。

```yaml
AWSTemplateFormatVersion: '2010-09-09'

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Parameters
        Parameters:
          - VpcNameSource
          - VpcCIDRSource
          - VpcNameTarget
          - VpcCIDRTarget
          - ImageId
          - InstanceTypeName
          - AvailabilityZone1

Parameters:

  VpcNameSource:
    Type: String
    Default: sourceVpc
    Description: Name of the VPC
  VpcCIDRSource:
    Type: String
    Default: 10.79.0.0/16
    Description: CIDR block for the VPC

  VpcNameTarget:
    Type: String
    Default: targetVpc
    Description: Name of the VPC
  VpcCIDRTarget:
    Type: String
    Default: 10.80.0.0/16
    Description: CIDR block for the VPC

  ImageId:
    Type: String
    Default: ami-012261b9035f8f938
  InstanceTypeName:
    Type: String
    Default: t3.nano
  AvailabilityZone1:
    Type: AWS::EC2::AvailabilityZone::Name

Resources:
  ##################################################
  # Source VPC
  ##################################################
  VPCSource:
    Type: 'AWS::EC2::VPC'
    Properties:
      CidrBlock: !Ref VpcCIDRSource
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Ref VpcNameSource

  InternetGatewaySource:
    Type: 'AWS::EC2::InternetGateway'
    Properties:
      Tags:
        - Key: Name
          Value: !Sub ${VpcNameSource}-igw

  VPCGatewayAttachmentSource:
    Type: 'AWS::EC2::VPCGatewayAttachment'
    Properties:
      VpcId: !Ref VPCSource
      InternetGatewayId: !Ref InternetGatewaySource

  PublicSubnetSource1:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId: !Ref VPCSource
      AvailabilityZone: !Ref AvailabilityZone1
      CidrBlock: !Select [ 0, !Cidr [ !Ref VpcCIDRSource, 24, 8 ] ]
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${VpcNameSource}-subnet

  PublicRouteTableSource:
    Type: 'AWS::EC2::RouteTable'
    Properties:
      VpcId: !Ref VPCSource
      Tags:
        - Key: Name
          Value: !Sub ${VpcNameSource}-rtb-public
  PublicRouteSource:
    Type: 'AWS::EC2::Route'
    DependsOn: VPCGatewayAttachmentSource
    Properties:
      RouteTableId: !Ref PublicRouteTableSource
      DestinationCidrBlock: '0.0.0.0/0'
      GatewayId: !Ref InternetGatewaySource

  SubnetRouteTableAssociationSource:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    Properties:
      SubnetId: !Ref PublicSubnetSource1
      RouteTableId: !Ref PublicRouteTableSource

  SecurityGroupForSource:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId: !Ref VPCSource
      GroupDescription: "Source VPC SG"

  ##################################################
  # EC2
  ##################################################
  SessionManagerRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: 'Allow'
            Principal:
              Service:
                - 'ec2.amazonaws.com'
            Action:
              - 'sts:AssumeRole'
      Path: '/'
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
  InstanceProfile:
    DependsOn: SessionManagerRole
    Type: AWS::IAM::InstanceProfile
    Properties:
      Path: '/'
      Roles:
        - !Ref SessionManagerRole

  # LaunchTemplate
  myLaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateData:
        ImageId: !Ref ImageId
        InstanceType: !Ref InstanceTypeName
        IamInstanceProfile:
          Name: !Ref InstanceProfile
        SecurityGroupIds:
        - !GetAtt SecurityGroupForSource.GroupId

  myASG:
    Type: AWS::AutoScaling::AutoScalingGroup
    Properties:
      VPCZoneIdentifier:
      - !Ref PublicSubnetSource1
      LaunchTemplate:
        LaunchTemplateId: !Ref myLaunchTemplate
        Version: !GetAtt myLaunchTemplate.LatestVersionNumber
      MaxSize: '1'
      MinSize: '0'
      DesiredCapacity: '1'
      LifecycleHookSpecificationList:
        - DefaultResult: CONTINUE
          HeartbeatTimeout: 60
          LifecycleHookName: testLifeCycleHook
          LifecycleTransition: autoscaling:EC2_INSTANCE_LAUNCHING
          NotificationMetadata: message

  ##################################################
  # Target VPC
  ##################################################
  VPCTarget:
    Type: 'AWS::EC2::VPC'
    Properties:
      CidrBlock: !Ref VpcCIDRTarget
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Ref VpcNameTarget

  InternetGatewayTarget:
    Type: 'AWS::EC2::InternetGateway'
    Properties:
      Tags:
        - Key: Name
          Value: !Sub ${VpcNameTarget}-igw

  VPCGatewayAttachmentTarget:
    Type: 'AWS::EC2::VPCGatewayAttachment'
    Properties:
      VpcId: !Ref VPCTarget
      InternetGatewayId: !Ref InternetGatewayTarget

  PublicSubnetTarget1:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId: !Ref VPCTarget
      AvailabilityZone: !Ref AvailabilityZone1
      CidrBlock: !Select [ 0, !Cidr [ !Ref VpcCIDRTarget, 24, 8 ] ]
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${VpcNameTarget}-subnet

  PublicRouteTableTarget:
    Type: 'AWS::EC2::RouteTable'
    Properties:
      VpcId: !Ref VPCTarget
      Tags:
        - Key: Name
          Value: !Sub ${VpcNameTarget}-rtb-public
  PublicRouteTarget:
    Type: 'AWS::EC2::Route'
    DependsOn: VPCGatewayAttachmentTarget
    Properties:
      RouteTableId: !Ref PublicRouteTableTarget
      DestinationCidrBlock: '0.0.0.0/0'
      GatewayId: !Ref InternetGatewayTarget

  SubnetRouteTableAssociationTarget:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    Properties:
      SubnetId: !Ref PublicSubnetTarget1
      RouteTableId: !Ref PublicRouteTableTarget

  SecurityGroupForTarget:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId: !Ref VPCTarget
      GroupDescription: "Target VPC SG"

  ##################################################
  # Role 
  ##################################################
  # for StepFunctions
  StepFunctionsRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: 'Allow'
            Principal:
              Service:
                - 'states.amazonaws.com'
            Action:
              - 'sts:AssumeRole'
      Path: '/'
      Policies:
        - PolicyName: writeEni
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - ec2:CreateNetworkInterface
                  - ec2:AttachNetworkInterface
                  - ec2:ModifyNetworkInterfaceAttribute
                Resource: '*'

  # for EventBridge
  EventBridgeRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: 'Allow'
            Principal:
              Service:
                - 'events.amazonaws.com'
            Action:
              - 'sts:AssumeRole'
      Path: '/'
      Policies:
        - PolicyName: invokeEb
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - states:StartExecution
                Resource: '*'

  ##################################################
  # StateMachine
  ##################################################
  MyStateMachine:
    Type: AWS::StepFunctions::StateMachine
    Properties:
      StateMachineName: attachEniOnOtherVpc-StateMachine
      DefinitionString: !Sub |-
        {
          "Comment": "A description of my state machine",
          "StartAt": "AddSubnetMap",
          "States": {
            "AddSubnetMap": {
              "Type": "Pass",
              "Result": {
                "SecurityGroupId": "${SecurityGroupForTarget}",
                "TargetSubnets": [
                  {
                    "Availability Zone": "${AvailabilityZone1}",
                    "Subnet ID": "${PublicSubnetTarget1}"
                  }
                ]
              },
              "ResultPath": "$.SubnetMap",
              "Next": "MakeParameter"
            },
            "MakeParameter": {
              "Type": "Pass",
              "Next": "CreateNetworkInterface",
              "Parameters": {
                "TargetSubnet.$": "$.SubnetMap.TargetSubnets[?(@.['Availability Zone']==$['detail']['Details']['Availability Zone'])]",
                "Groups.$": "States.Array($.SubnetMap.SecurityGroupId)"
              },
              "ResultPath": "$.TargetParameter"
            },
            "CreateNetworkInterface": {
              "Type": "Task",
              "Parameters": {
                "SubnetId.$": "$.TargetSubnet[0]['Subnet ID']",
                "Groups.$": "$.Groups"
              },
              "Resource": "arn:aws:states:::aws-sdk:ec2:createNetworkInterface",
              "ResultPath": "$.CreatedNetworkInterface",
              "InputPath": "$.TargetParameter",
              "Next": "AttachNetworkInterface"
            },
            "AttachNetworkInterface": {
              "Type": "Task",
              "Parameters": {
                "DeviceIndex": 1,
                "InstanceId.$": "$.detail.EC2InstanceId",
                "NetworkInterfaceId.$": "$.CreatedNetworkInterface.NetworkInterface.NetworkInterfaceId"
              },
              "Resource": "arn:aws:states:::aws-sdk:ec2:attachNetworkInterface",
              "ResultPath": "$.AttachedNetworkInterface",
              "Next": "ModifyNetworkInterfaceAttribute"
            },
            "ModifyNetworkInterfaceAttribute": {
              "Type": "Task",
              "End": true,
              "Parameters": {
                "NetworkInterfaceId.$": "$.CreatedNetworkInterface.NetworkInterface.NetworkInterfaceId",
                "Attachment": {
                  "AttachmentId.$": "$.AttachedNetworkInterface.AttachmentId",
                  "DeleteOnTermination": "true"
                }
              },
              "Resource": "arn:aws:states:::aws-sdk:ec2:modifyNetworkInterfaceAttribute"
            }
          }
        }
      RoleArn: !GetAtt StepFunctionsRole.Arn

  ##################################################
  # Events::Rule
  ##################################################
  MyNewEventsRule:
    Type: 'AWS::Events::Rule'
    Properties:
      Description: Test Events Rule
      Name: catchScaleout
      EventPattern:
        source:
          - aws.autoscaling
        detail-type: 
          - "EC2 Instance Launch Successful"
      State: ENABLED
      Targets:
        - Arn: !GetAtt MyStateMachine.Arn
          Id: Id1234
          RoleArn: !GetAtt EventBridgeRole.Arn
```

</div></details>


## StepFunctionsの説明
フローは以下になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/b04467cb-3d90-8faf-979e-7e0a484d7e80.png)


以下のような動きをします。

1. AddSubnetMap
    - ENI作成時に指定する、サブネットIDとセキュリティグループIDの定義情報をセットします
        - AZと、対応する作成先のサブネットIDの対応情報として追加します
        - セキュリティグループはどのAZでも同じものを使うので、AZに依らず共通情報として追加します
    - AZが複数ある場合は、ここにAZの数だけ定義を書きます
1. MakeParameter
    - スケールアウトしたEC2のAZの情報を用いて、AddSubnetMapでセットした情報からパラメーターに使う情報をフィルターします
1. CreateNetworkInterface
    - フィルターした情報を使って、ENIを作ります
1. AttachNetworkInterface
    - 作ったENIを、スケールアウトしたEC2にアタッチします
1. ModifyNetworkInterfaceAttribute
    - EC2終了時にENIが自動で削除されるように設定変更します


# 実行結果
CFnテンプレートを実行すると、EC2インスタンスが１つ作成され、別VPCのENIがアタッチされています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/662d77bf-c2c2-2c48-9761-e3b2a8f7530a.png)
二つとも、終了時に削除する設定になっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/190c4e34-a328-01d9-a4c5-44961b724d88.png)
ENIも２つだけです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/28cc4558-1f45-92b4-a5cb-dd6da2649ba8.png)

ここで、動いているEC2を終了して、既存のENIが削除され、新しくENIが作られるか見ました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/d504d9af-1f33-0a1a-2329-79a08f32544e.png)
ENIも確認し、以前のENIは削除されていることを確認しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/b29cff27-4027-0e1b-756b-711b67ab1bf6.png)

AutoScalingの設定を変更して2台動かすと、ENIが4つ（同VPCのと、別VPCのの2対が、2台分）作成されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/449f3f3e-a2c3-969d-db67-caca58dc19c0.png)


また１台に戻してみます。
片方のENIが開放されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/2fad9f69-ab35-1038-6bbd-50363b3ccbb8.png)

# おわりに

今回はAutoScalingでスケールアウト時に、別VPCにENIを作成してアタッチするような仕組みを作ってみました。
あらかじめENIを作る方法と比較して、スケールアウト最大数に応じたENIを作っておく必要がない点がよいかと思います。
ただIPを固定したい場合は本方法ではなく、事前にENIを作っておく方法がよいかと思います。

この記事がどなたかのお役に立てれば幸いです。