AWS CloudFormationでCloudFormationエンドポイントを使用したEC2インスタンスを構築しよう

# はじめに
AWS CloudFormationを利用してVPC構築のテンプレートのサンプルです。

テンプレートの概要が分からない場合は、[はじめてのAWS CloudFormationテンプレートを理解する](https://qiita.com/miyabiz/items/eb7b0fa1675b5199b9b7)を参考にしてください。

コードは[GitHub](https://github.com/miyabiz-room/CloudFormation-Template/tree/vpc-endpoint-02)にもあります。

今回は、akane というシステムの all 環境を想定しています。
同じ構成で違う環境を作成する場合は、{環境名}-parameters.jsonを別途作成します。

```text:ディレクトリ構成
akane-cfn (システム)
  ├─ network (スタック)
  |   ├─ network.yml (CFnテンプレート)
  |   └─ all-parameters.json (all 環境のパラメータ)
  ├─ instance-cfn (スタック)
  |   ├─ instance-cfn.yml (CFnテンプレート)
  |   └─ all-parameters.json (all 環境のパラメータ)
  └─ endpoint-cfn (スタック)
      ├─ endpoint-cfn.yml (CFnテンプレート)
      └─ all-parameters.json (all 環境のパラメータ)
```

# AWS リソース構築内容
  1. networkスタック
      - VPC (10.0.0.0/16)
      - Publicサブネット (10.0.1.0/24)
      - インターネットゲートウェイ
      - Publicルートテーブル 
      - Elastic IP (NATゲートウェイ用)
 
  2. instance-cfnスタック
      - セキュリティグループ (インスタンス)
      - IAMロール (AdministratorAccess)
      - インスタンスプロファイル
      - EC2インスタンス (Amazon Linux 2)

  2. endpoint-cfnスタック
      - セキュリティグループ (エンドポイント:アウトバウンドにインスタンスのセキュリティグループを指定)
      - セキュリティグループのアウトバウンド (インスタンス:アウトバウンドにエンドポイントのセキュリティグループを指定)
      - CloudFormationエンドポイント (Interface)

# 実行環境の準備
[AWS CloudFormationを動かすためのAWS CLIの設定](https://qiita.com/miyabiz/items/fed11796f0ea2b7608f4)を参考にしてください。

# AWS リソース構築手順
1.  事前にEC2のキーペアを作成し、```akane-cfn/instance-cfn/all-parameters.json```の```KeyName```に記載します。

2. 下記を実行してスタックを作成

    ```
    ./create_stacks.sh
    ```

3. 下記を実行してスタックを削除

    ```
    ./delete_stacks.sh
    ```

# 構築テンプレート
**1. networkスタック**
 
```yaml:network.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Network For Akane

# Metadata:

Parameters:
  SystemName:
    Type: String
    AllowedPattern: '[a-zA-Z0-9-]*'
  EnvType:
    Description: Environment type.
    Type: String
    AllowedValues: [all, dev, stg, prod]
    ConstraintDescription: must specify all, dev, stg, or prod.
  VPCCidrBlock:
    Type: String
    AllowedPattern: (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/16
  PublicSubnet:
    Type: String
    AllowedPattern: (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/24

Mappings: 
  AzMap: 
    ap-northeast-1:
      1st: ap-northeast-1a
      2nd: ap-northeast-1c
      3rd: ap-northeast-1d

# Conditions

# Transform

Resources:
  # VPC作成
  akaneVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCidrBlock
      EnableDnsSupport: true
      EnableDnsHostnames: true
      InstanceTenancy: default
      Tags:
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-vpc
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType

###########################################################################
# Publicサブネット関連
###########################################################################
  # Publicサブネット作成
  akanePublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref akaneVPC
      CidrBlock: !Ref PublicSubnet
      AvailabilityZone: !FindInMap [AzMap, !Ref AWS::Region, 1st]
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-public-subnet
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
  # インターネットゲートウェイ作成
  akaneInternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-igw
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
  # インターネットゲートウェイをVPCにアタッチ
  akaneVPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref akaneVPC
      InternetGatewayId: !Ref akaneInternetGateway
  # Publicルートテーブル作成
  akanePublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref akaneVPC
      Tags:
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-public-route
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
  # Publicルートテーブル サブネット関連付け
  akanePublicSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref akanePublicSubnet
      RouteTableId: !Ref akanePublicRouteTable
  # Publicルートテーブル ルートにインターネットゲートウェイを関連付ける
  akanePublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref akanePublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref akaneInternetGateway

Outputs:
  akaneVPC:
    Value: !Ref akaneVPC
    Export:
      Name: !Sub
        - ${SystemName}-${EnvType}-vpc
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
  akanePublicSubnet:
    Value: !Ref akanePublicSubnet
    Export:
      Name: !Sub
        - ${SystemName}-${EnvType}-public-subnet
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
  akanePublicRouteTable:
    Value: !Ref akanePublicRouteTable
    Export:
      Name: !Sub
        - ${SystemName}-${EnvType}-public-route-table
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
```

```json:all-parameters.json
{
    "Parameters": [
        {
            "ParameterKey": "SystemName",
            "ParameterValue": "akane-cfn"
        },
        {
            "ParameterKey": "EnvType",
            "ParameterValue": "all"
        },
        {
            "ParameterKey": "VPCCidrBlock",
            "ParameterValue": "10.10.0.0/16"
        },
        {
            "ParameterKey": "PublicSubnet",
            "ParameterValue": "10.10.1.0/24"
        }
    ]
}
```

**２. EC2インスタススタック**

```yaml:instance-cfn.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Instance For Akane

# Metadata:

Parameters:
  SystemName:
    Type: String
    AllowedPattern: '[a-zA-Z0-9-]*'
  EnvType:
    Description: Environment type.
    Type: String
    AllowedValues: [all, dev, stg, prod]
    ConstraintDescription: must specify all, dev, stg, or prod.
  ImageId:
    Type: String
    AllowedPattern: 'ami-[a-zA-Z0-9-]*'
  InstanceType:
    Type: String
    AllowedPattern: '^[a-z][1-9][.][a-z0-9]+$'
  KeyName:
    Type: String

Mappings: 
  AzMap: 
    ap-northeast-1:
      1st: ap-northeast-1a
      2nd: ap-northeast-1c
      3rd: ap-northeast-1d

# Conditions

# Transform

Resources:
  # セキュリティグループ(EC2インスタンス)作成
  akaneSecurityGroupInstance:
    Type: AWS::EC2::SecurityGroup
    Properties: 
      GroupDescription: !Sub
        - ${SystemName}-${EnvType}-sg-instance
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      GroupName: !Sub
        - ${SystemName}-${EnvType}-sg-instance
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      SecurityGroupIngress: 
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 0.0.0.0/0
      SecurityGroupEgress: 
        - IpProtocol: tcp
          FromPort: 0
          ToPort: 65535
          CidrIp: 0.0.0.0/0
      Tags: 
        - Key: Name
          Value: !Sub
            - ${SystemName}-${EnvType}-sg-instance
            - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
      VpcId:
        Fn::ImportValue: !Sub
          - ${SystemName}-${EnvType}-vpc
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}

  # IAMロールを作成
  akaneRole:
    Type: AWS::IAM::Role
    Properties: 
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action:
              - sts:AssumeRole
      Description: !Sub
        - ${SystemName}-${EnvType}-role
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      ManagedPolicyArns: 
        - arn:aws:iam::aws:policy/AdministratorAccess
      RoleName: !Sub
        - ${SystemName}-${EnvType}-role
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      Tags: 
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-role
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType

  # インスタンスプロファイル作成
  akaneInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Path: "/"
      Roles:
      - !Ref akaneRole

  # EC2インスタンス作成
  akaneInstance:
    Type: AWS::EC2::Instance
    Properties: 
      AvailabilityZone: !FindInMap [AzMap, !Ref AWS::Region, 1st]
      DisableApiTermination: false
      IamInstanceProfile:
        !Ref akaneInstanceProfile      
      ImageId: !Ref ImageId
      InstanceInitiatedShutdownBehavior: stop
      InstanceType: !Ref InstanceType
      KeyName: !Ref KeyName
      Monitoring: false
      SecurityGroupIds: 
        - !Ref akaneSecurityGroupInstance
      SubnetId:
        Fn::ImportValue: !Sub
          - ${SystemName}-${EnvType}-public-subnet
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      Tags: 
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-instance
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
      Tenancy: default
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash
          yum update -y
          yum install git -y

          cd /home/ec2-user

          # ec2-user bashrc設定
          cat <<EOS | sudo tee -a /home/ec2-user/.bashrc
          SERVERNAME=${SystemName}-${EnvType}
          export PS1='\[\e[1;36m\][\e[1;33m\]\u@\[\e[1;32m\]\${!SERVERNAME} \w\[\e[1;36m\]]$\[\e[00m\] '
          export AWS_DEFAULT_REGION=${AWS::Region}
          EOS

          # root bashrc設定
          cat <<EOS | sudo tee /root/.bashrc
          export PS1="\[\e[1;36m\][\u@\h \W]\$\[\e[00m\] "
          EOS

  # EIP作成およびEC2インスタンスへのアタッチ
  akaneEIP:
    Type: AWS::EC2::EIP
    Properties: 
      Domain: vpc
      InstanceId: !Ref akaneInstance
      Tags: 
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-eip
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType

Outputs:
  akaneSecurityGroupInstance:
    Value: !Ref akaneSecurityGroupInstance
    Export:
      Name: !Sub
        - ${SystemName}-${EnvType}-sg-instance
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}

```

```json:all-parameters.json
{
    "Parameters": [
        {
            "ParameterKey": "SystemName",
            "ParameterValue": "akane-cfn"
        },
        {
            "ParameterKey": "EnvType",
            "ParameterValue": "all"
        },
        {
            "ParameterKey": "ImageId",
            "ParameterValue": "ami-0ce107ae7af2e92b5"
        },
        {
            "ParameterKey": "InstanceType",
            "ParameterValue": "t2.micro"
        },
        {
            "ParameterKey": "KeyName",
            "ParameterValue": "akane"
        }
    ],
    "Capabilities": [
        "CAPABILITY_NAMED_IAM"
    ]
}
```

**3. CloudFormationエンドポイントスタック**

```yaml:instance-cfn.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Endpoint For Akane

# Metadata:

Parameters:
  SystemName:
    Type: String
    AllowedPattern: '[a-zA-Z0-9-]*'
  EnvType:
    Description: Environment type.
    Type: String
    AllowedValues: [all, dev, stg, prod]
    ConstraintDescription: must specify all, dev, stg, or prod.

Mappings: 
  AzMap: 
    ap-northeast-1:
      1st: ap-northeast-1a
      2nd: ap-northeast-1c
      3rd: ap-northeast-1d

# Conditions

# Transform

Resources:
  # セキュリティグループ(エンドポイント)作成
  akaneSecurityGroupEndpoint:
    Type: AWS::EC2::SecurityGroup
    Properties: 
      GroupDescription: !Sub
        - ${SystemName}-${EnvType}-sg-endpoint-cfn
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      GroupName: !Sub
        - ${SystemName}-${EnvType}-sg-endpoint-cfn
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      SecurityGroupIngress: 
        - IpProtocol: tcp
          FromPort: 0
          ToPort: 65535
          SourceSecurityGroupId:
            Fn::ImportValue: !Sub
              - ${SystemName}-${EnvType}-sg-instance
              - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      Tags: 
        - Key: Name
          Value: !Sub
            - ${SystemName}-${EnvType}-sg-endpoint-cfn
            - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
      VpcId:
        Fn::ImportValue: !Sub
          - ${SystemName}-${EnvType}-vpc
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}

  #VPCエンドポイント作成
  akaneCloudFormationEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      PrivateDnsEnabled: true
      SecurityGroupIds:
        - !Ref akaneSecurityGroupEndpoint
      ServiceName: !Sub com.amazonaws.${AWS::Region}.cloudformation
      SubnetIds:
        - Fn::ImportValue: !Sub
          - ${SystemName}-${EnvType}-public-subnet
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      VpcEndpointType: Interface
      VpcId:
        Fn::ImportValue: !Sub
          - ${SystemName}-${EnvType}-vpc
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}

  # セキュリティグループ(EC2インスタンス)のアウトバウンドルール作成
  akaneSecurityGroupEgressInstance:
    Type: AWS::EC2::SecurityGroupEgress
    Properties: 
      Description:  !Sub
        - ${SystemName}-${EnvType}-sg-instance-cfn
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      DestinationSecurityGroupId: !Ref akaneSecurityGroupEndpoint
      IpProtocol: -1
      GroupId:
        Fn::ImportValue: !Sub
          - ${SystemName}-${EnvType}-sg-instance
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}

# Outputs:

```

```json:all-parameters.json
{
    "Parameters": [
        {
            "ParameterKey": "SystemName",
            "ParameterValue": "akane-cfn"
        },
        {
            "ParameterKey": "EnvType",
            "ParameterValue": "all"
        }
    ]
}
```

**4. 実行ファイル**

```sh:create_stacks.sh
#!/bin/sh
SYSTEM_NAME=akane-cfn
ENV_TYPE=all

create_stack () {
    aws cloudformation create-stack \
    --stack-name ${SYSTEM_NAME}-${ENV_TYPE}-${STACK_NAME} \
    --template-body file://./${SYSTEM_NAME}/${STACK_NAME}/${STACK_NAME}.yml \
    --cli-input-json file://./${SYSTEM_NAME}/${STACK_NAME}/${ENV_TYPE}-parameters.json \
    --capabilities CAPABILITY_NAMED_IAM

    aws cloudformation wait stack-create-complete \
    --stack-name ${SYSTEM_NAME}-${ENV_TYPE}-${STACK_NAME}
}

create_stack network
create_stack instance-cfn '--capabilities CAPABILITY_NAMED_IAM'
create_stack endpoint-cfn
```

```sh:delete_stacks.sh
#!/bin/sh
SYSTEM_NAME=akane-cfn
ENV_TYPE=all

delete_stack () {
    STACK_NAME=$1
    aws cloudformation delete-stack \
    --stack-name ${SYSTEM_NAME}-${ENV_TYPE}-${STACK_NAME}

    aws cloudformation wait stack-delete-complete \
    --stack-name ${SYSTEM_NAME}-${ENV_TYPE}-${STACK_NAME}
}

delete_stack endpoint-cfn
delete_stack instance-cfn
delete_stack network
```