AutoScalingで、EC2に別VPCのENIをアタッチ

# はじめに

先日、EC2のアップデートで、別のVPCのENIをアタッチできるようになり、それを記事にしました。

https://qiita.com/a_b_/items/b28a90cca7bf79ad3f82

これを、AutoScalingでもやってみました。

# 概要

- AutoScalingでENIを追加でアタッチする機能はない
- AutoScalingグループにライフサイクルフックを追加して、EventBridge -> StepFunctionsでアタッチします
- 事前に別VPCにENIを作っておきます
    - 同じAZに作っておく必要があります

# 参考

https://repost.aws/ja/knowledge-center/ec2-auto-scaling-multiple-network-interfaces

https://qiita.com/mittsukan/items/83b55ac65eaf23309f15

# 構成図


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/a5d9a7a1-0bff-3c9a-8f82-61d0d2f573cb.png)


# CloudFormationテンプレート

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

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



EniTarget01とEniTarget02で、別VPC上に事前にENIを作成しています。
AMIのImageIdのデフォルトは東京リージョンのものです。

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

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

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
  AvailabilityZone:
    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

  PublicSubnetSource:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId: !Ref VPCSource
      AvailabilityZone: !Ref AvailabilityZone
      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 PublicSubnetSource
      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 PublicSubnetSource
      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

  PublicSubnetTarget:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId: !Ref VPCTarget
      AvailabilityZone: !Ref AvailabilityZone
      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 PublicSubnetTarget
      RouteTableId: !Ref PublicRouteTableTarget

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

  EniTarget01:
    Type: AWS::EC2::NetworkInterface
    Properties:
      Tags:
        - Key: Name
          Value: TargetEni01
      GroupSet:
        - !GetAtt SecurityGroupForTarget.GroupId
      SubnetId: !Ref PublicSubnetTarget

  EniTarget02:
    Type: AWS::EC2::NetworkInterface
    Properties:
      Tags:
        - Key: Name
          Value: TargetEni02
      GroupSet:
        - !GetAtt SecurityGroupForTarget.GroupId
      SubnetId: !Ref PublicSubnetTarget

  ##################################################
  # 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:DescribeNetworkInterfaces
                  - ec2:AttachNetworkInterface
                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": "DescribeNetworkInterfaces",
          "States": {
            "DescribeNetworkInterfaces": {
              "Type": "Task",
              "Parameters": {
                "Filters": [
                  {
                    "Name": "vpc-id",
                    "Values": [
                      "${VPCTarget}"
                    ]
                  },
                  {
                    "Name": "availability-zone",
                    "Values.$": "States.Array($['detail']['Details']['Availability Zone'])"
                  },
                  {
                    "Name": "status",
                    "Values": [
                      "available"
                    ]
                  }
                ]
              },
              "Resource": "arn:aws:states:::aws-sdk:ec2:describeNetworkInterfaces",
              "Next": "Count",
              "ResultPath": "$.filtered"
            },
            "Count": {
              "Type": "Pass",
              "Parameters": {
                "length.$": "States.ArrayLength($.filtered.NetworkInterfaces)"
              },
              "ResultPath": "$.count",
              "Next": "Choice"
            },
            "Choice": {
              "Type": "Choice",
              "Choices": [
                {
                  "Variable": "$.count.length",
                  "NumericGreaterThan": 0,
                  "Next": "AttachNetworkInterface"
                }
              ],
              "Default": "Fail"
            },
            "AttachNetworkInterface": {
              "Type": "Task",
              "Next": "Success",
              "Parameters": {
                "DeviceIndex": 1,
                "InstanceId.$": "$.detail.EC2InstanceId",
                "NetworkInterfaceId.$": "$.filtered.NetworkInterfaces[0].NetworkInterfaceId"
              },
              "Resource": "arn:aws:states:::aws-sdk:ec2:attachNetworkInterface"
            },
            "Success": {
              "Type": "Succeed"
            },
            "Fail": {
              "Type": "Fail"
            }
          }
        }
      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/e59f4c54-d14a-37b6-51c4-39b549f4cc10.png)


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

1. DescribeNetworkInterfaces
    - 以下の条件に一致するENIを抽出します
        - 紐づけるENIを持つVPC上にある
        - 使用可能
        - スケールアウトして作成されたEC2と同じAZ上にある
            - インスタンスの実行AZがわかるAutoScalingのイベントが、スケールアウト成功なのでそれを使っています

1. Count
    - 抽出したENIの個数をカウント
1. Choice
    - Countの結果が0より多いかどうか判定
1. AttachNetworkInterface
    - 抽出したENIの中で1つ目のENIを、スケールアウトして作成されたEC2にアタッチ


# 実行結果
CFnテンプレートを実行すると、EC2インスタンスが１つ作成され、別VPCのENIがアタッチされています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/fa2d6586-8bf4-cd47-b1f5-0a7b6d7a1c16.png)

StepFunctionsも実行されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/882bd275-a1d9-43d2-bf1c-f0e34b3a78ff.png)


この１台を終了して、AutoScalingで立ち上げさせます。
別のマシンが立ち上がり、そちらでもENIがアタッチされました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/ca5d5b14-4bbd-2f82-2dbd-37e274b59970.png)

StepFunctionsも実行されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/f71d3c3d-3e01-9d6d-1f23-2ffe0e1c7c0f.png)

AutoScalingの設定を変更して、２台動かすようにしてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/faa5bfdf-85a3-b5bc-691b-e882028bd22a.png)

２台目にもアタッチされました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/2b3844f8-7e50-6f7d-04ab-c852323e24a2.png)

StepFunctionsも実行されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/519e1676-f92b-7ab5-db0e-0e5445257b1b.png)

ENIも２つ使われています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/b182967d-277a-e20c-bf71-bcac437bc045.png)

また１台に戻してみます。
片方のENIが開放されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/371eb45c-9cdb-c33d-899e-4834404b81d0.png)


# おわりに
今回はAutoScalingでスケールアウト時に、別VPCのENIをアタッチするような仕組みを作ってみました。
スケールアウト完了後に本処理が動くので、ヘルスチェックの猶予期間を設ければ使えるかと思います。

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