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

# はじめに
前回、AutoScalingでスケールアウト時に、別VPCに作っておいたENIをアタッチする仕組みを、ユーザーデータで実装した内容を記事にしました。

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

今回は、都度ENIを作ってアタッチする仕組みをユーザーデータで実装しました。

# 概要
ユーザーデータでインスタンス起動時に、以下を行います。
1. ENIを作成
    - インスタンスとは別VPCに作ります
1. インスタンスにアタッチ
1. ”EC2終了後に自動削除を有効”の設定に変更
 
そのため、上記を行う権限をインスタンスに付与しています。

# 参考

以前、Step Functionsを使って同様のことをやっていますので、そちらも参考にしてください。

https://qiita.com/a_b_/items/76d707635bfc1f1d3337

# CloudFormationテンプレート

CloudFormationは以下になります。単純になるよう、AZは一つです。

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

```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
      Policies:
        - PolicyName: writeEni
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - ec2:CreateNetworkInterface
                  - ec2:AttachNetworkInterface
                  - ec2:ModifyNetworkInterfaceAttribute
                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

  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"

  ##################################################
  # 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}'`

                declare -A TargetSubnets=(
                  ["${AvailabilityZone1}"]="${PublicSubnetTarget1}"
                )

                NETWORKINTERFACEID=`aws ec2 create-network-interface \
                    --subnet-id "${!TargetSubnets[$TARGET_AZ]}" \
                    --groups "${SecurityGroupId}" \
                    --output text --query 'NetworkInterface.NetworkInterfaceId'`

                ATTACHEMENTID=`aws ec2 attach-network-interface \
                    --network-interface-id $NETWORKINTERFACEID \
                    --instance-id $INSTANCE_ID \
                    --device-index 1 \
                    --output text --query 'AttachmentId'`

                aws ec2 modify-network-interface-attribute \
                  --network-interface-id $NETWORKINTERFACEID \
                  --attachment AttachmentId=$ATTACHEMENTID,DeleteOnTermination=true

              }
              eni_attach || sudo shutdown -h now
            - {
                PublicSubnetTarget1: !Ref PublicSubnetTarget1 ,
                AvailabilityZone1: !Ref AvailabilityZone1 ,
                SecurityGroupId: !Ref SecurityGroupForTarget
              }

  myASG:
    Type: AWS::AutoScaling::AutoScalingGroup
    Properties:
      VPCZoneIdentifier:
      - !Ref PublicSubnetSource1
      LaunchTemplate:
        LaunchTemplateId: !Ref myLaunchTemplate
        Version: !GetAtt myLaunchTemplate.LatestVersionNumber
      MaxSize: '1'
      MinSize: '0'
      DesiredCapacity: '1'

```
</div></details>

## シェルの解説
以下の部分は、`"対象のアベイラビリティゾーン"=>"作成する（別VPCの）サブネット"`の情報を格納する連想配列です。
```bash
declare -A TargetSubnets=(
  ["${AvailabilityZone1}"]="${PublicSubnetTarget1}"
)
```
今回はAZが1つなので1要素ですが、複数のAZに作る場合はその分の情報を要素として追加します。

連想配列は以下を参考にしました。

https://qiita.com/YutaSaito1991/items/c7c6bacb916f41ef1471

ユーザーデータの中でシェル変数を使う場合は、`${!HOGEHOGE}`と、`!`を入れて使います。

```bash
NETWORKINTERFACEID=`aws ec2 create-network-interface \
    --subnet-id "${!TargetSubnets[$TARGET_AZ]}" \
    --groups "${SecurityGroupId}" \
    --output text --query 'NetworkInterface.NetworkInterfaceId'`
```
以下を参考にしました。

https://dev.classmethod.jp/articles/using-variables-in-ec2-user-data-in-cloudformation/


# 実行結果
CFnテンプレートを実行すると、EC2インスタンスが１つ作成され、別VPCのENIがアタッチされています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/a70522b9-aa21-b12f-b0ee-811a9d9a3aed.png)

二つとも、終了時に削除する設定になっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/002aa693-fae8-11a1-6cdb-97d2cfebcfce.png)

AutoScalingの設定を変更して2台にすると、2台目も同様に2つのENIがアタッチされます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/56da1edd-5cb0-cfab-ff42-873094a658a1.png)

ENIも、1台目と合わせて4つ作られています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/b0b9584e-407a-f58c-d25d-73ddb1d6d360.png)



# おわりに
今回はユーザーデータを使って、別VPCにENIを作ってEC2にアタッチさせてみました。
前回と同様、アタッチのタイミングがStep Functionsより早いはずです。

一方、EC2に余分な権限（ENIの作成・アタッチ・変更）を付けていますので、その点許容する必要があります。

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