AutoScalingで、EC2に別VPCのENIをアタッチ（ユーザーデータ編）

# はじめに
以前、スケールアウトするEC2に別のVPCのENIをアタッチする仕組みをStep Functionsで作りました。

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

これを、ユーザーデータでやってみました。

https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/user-data.html

# 概要

- AutoScalingでENIを追加でアタッチする機能はない
- ユーザーデータでインスタンス起動時に、ENIをアタッチするシェルを実行します
    - 事前に別VPCにENIを作っておきます
- ENIは同じAZに作っておく必要があります
- EC2にENI関連の権限を付与しておく必要があります

# 参考

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


# 構成図

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/ccf00643-9f41-fc93-7eee-518c5fb7abee.png)

以前の方法と比較してStep Functionsが無い分、シンプルになっています。


# CloudFormationテンプレート

CloudFormationは以下になります。
以前と同様、EniTarget01とEniTarget02という名前で、別VPC上に事前にENIを作成しています。

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

```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
      Policies:
        - PolicyName: writeEni
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - ec2:DescribeNetworkInterfaces
                  - ec2:AttachNetworkInterface
                Resource: '*'
  InstanceProfile:
    DependsOn: SessionManagerRole
    Type: AWS::IAM::InstanceProfile
    Properties:
      Path: '/'
      Roles:
        - !Ref SessionManagerRole

  ##################################################
  # 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

  ##################################################
  # AutoScaling
  ##################################################
  # LaunchTemplate
  myLaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateData:
        ImageId: !Ref ImageId
        InstanceType: !Ref InstanceTypeName
        IamInstanceProfile:
          Name: !Ref InstanceProfile
        SecurityGroupIds:
          - !GetAtt SecurityGroupForSource.GroupId
        UserData:
          Fn::Base64: !Sub
            - |
              #!/bin/bash

              function eni_attach() {
                INSTANCE_ID=`ec2-metadata -i | awk '{print $2}'`
                TARGET_AZ=`ec2-metadata -z | awk '{print $2}'`

                ALLOC_ID=$(/usr/bin/aws ec2 describe-network-interfaces \
                  --filters "Name=vpc-id, Values=${VPCTarget}" "Name=availability-zone, Values=$TARGET_AZ" "Name=status, Values=available" \
                  --query "sort_by(NetworkInterfaces, &PrivateIpAddress)[].NetworkInterfaceId | [0]" \
                  --output text)
                /usr/bin/aws ec2 attach-network-interface --instance-id=$INSTANCE_ID \
                  --device-index=1 --network-interface-id=$ALLOC_ID
              }
              eni_attach || sudo shutdown -h now
            - {
                VPCTarget: !Ref VPCTarget
              }

  myASG:
    Type: AWS::AutoScaling::AutoScalingGroup
    Properties:
      VPCZoneIdentifier:
      - !Ref PublicSubnetSource
      LaunchTemplate:
        LaunchTemplateId: !Ref myLaunchTemplate
        Version: !GetAtt myLaunchTemplate.LatestVersionNumber
      MaxSize: '1'
      MinSize: '0'
      DesiredCapacity: '1'
```
</div></details>

ユーザーデータは、以下のサイトを参考にしました。ほぼ同じなので詳細は以下をご覧ください。

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

インスタンスのVPCやAZの情報は、以下のサイトを参考に`ec2-metadata`で取得しています。

https://qiita.com/miyuki_samitani/items/be36df3ff41da4e3016a#%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B-ec2-metadata%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E7%B7%A8

# 実行結果
CFnテンプレートを実行すると、`10.80.0.0/16`にENIが作られます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/95fef45d-3565-dfd9-e35e-68369a1f4c63.png)

加えて、EC2インスタンスが１つ作成され、別VPCのENIがアタッチされています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/2cb80f7a-61a4-754c-25fe-09e877de38eb.png)

この１台を終了して、AutoScalingで立ち上げさせます。
別のマシンが立ち上がり、そちらでもENIがアタッチされました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/8be691ab-17d6-5378-2476-3d39586c62da.png)

AutoScalingの設定を変更して、２台動かすようにしてみます。
２台目にもアタッチされました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/bb0f434e-a96b-905f-66dc-896c9352d302.png)

ENIも２つ使われています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/91f284e6-84ea-0873-8b58-3662ee6bc30b.png)

また１台に戻してみます。
片方のENIが開放されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/4a7aa487-614c-5de7-9119-247efa6deea6.png)


# おわりに

今回はユーザーデータを使って、スケールアウト時にEC2に別VPCのENIをアタッチしました。
以前のStep Functionsで行う方法では、スケールアウト成功のイベント時をトリガーにしていましたが、今回はEC2起動時なのでENIがアタッチするタイミングは早くなったと思われます。
一方、EC2に余分な権限（ENIを検索・アタッチ）を付けていますので、その点許容する必要があります。

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