AWS CloudFormation 複数スタック使用によるEC2作成

# はじめに

AWS CloudFormationで複数のスタックを使用してEC2を構築したので、その検証結果を記事に残したいと思います。なお、別記事を作成予定となりますが、最終的にはCloudFormationで作成したEC2にAnsibleを導入し、OS設定まで自動化を目標としています。

# 構成図
今回の検証では以下のような構成でEC2を作成しました。
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/816973/5d4a2708-a3a5-32fe-94f0-8c320f0ec4d8.png)

VPCは東京リージョンに作成し、パブリックサブネットはap-northeast-1aに作成しています。
EC2へのアクセスは指定した固定IPからのSSH, HTTPアクセスのみ許可しています。


# テンプレートファイルの構成

ファイル構成は以下のように行います。テンプレートファイルをVPC、EC2等、サービスごとに分けることで、今後のメンテナンス性も上がると思います。

├── main.yaml
│
└── vpc
│   └── vpc.yaml
│
├── subnet
│   └── public
│       └── subnet.yaml
│
├── ec2
│   ├── ec2.yaml
│   └── sg.yaml

# 親テンプレート(main.yaml)の作成

```ruby:main.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: testCFn stack 

Resources:

  VPC:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: vpc/vpc.yaml
      Parameters:
        TagName : "VPCTEST"
   
  PublicSubnet:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: subnet/public/subnet.yaml
      Parameters:
        VPC: !GetAtt VPC.Outputs.VPCID

  EC2Instance:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: ec2/ec2.yaml
      Parameters:
        VPC: !GetAtt VPC.Outputs.VPCID
        KeyName: test CFn
        Subnet: !GetAtt PublicSubnet.Outputs.SubnetID
```


* VPCテンプレート(vpc.yaml)の作成

```ruby:vpc.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'VPC for testCFn '
Parameters:
  VPCCIDR:
    Type: String
    Default: "10.0.0.0/16"
  TagName :
    Type: String
    Default: "testCFn"
  
Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCIDR
      Tags:
        - Key: Name
          Value: !Ref TagName

Outputs:
  VPCID: 
    Value: !Ref VPC
```

* サブネットテンプレート(subnet.yaml)の作成

```ruby:subnet.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'public subnet for testCFn'

Parameters:
  AZ:
    Type: AWS::EC2::AvailabilityZone::Name
    Default: ap-northeast-1a
  TagName:
    Type: String
    Default: "subnettestCFn"
  VPC:
    Type: AWS::EC2::VPC::Id

Resources:
  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties: 
      AvailabilityZone: !Ref AZ
      VpcId: !Ref VPC
      CidrBlock: 10.1.2.0/24
      Tags:
        - Key: Name
          Value: !Ref TagName
  
  PublicRouteTable: 
    Type: "AWS::EC2::RouteTable"
    Properties: 
      VpcId: !Ref VPC 
      Tags: 
        - Key: Name
          Value: !Ref TagName
  
  InternetGateway: 
    Type: "AWS::EC2::InternetGateway"
    Properties: 
      Tags: 
        - Key: Name
          Value: !Ref TagName
  
  InternetGatewayAttachment: 
    Type: "AWS::EC2::VPCGatewayAttachment"
    Properties: 
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC 

  PublicRoute: 
    Type: "AWS::EC2::Route"
    Properties: 
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref InternetGateway 

  PublicSubnetTableAssociation: 
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties: 
      SubnetId: !Ref PublicSubnet
      RouteTableId: !Ref PublicRouteTable

Outputs:
  SubnetID: 
    Value: !Ref PublicSubnet

```
* EC2テンプレートの作成(ec2.yaml)の作成

```ruby:ec2.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'VPC for testCFn'
Parameters:
  ImageId:
    Type: String
    Default: "ami-0701e21c502689c31"
  KeyName:
    Type: String
  Subnet:
    Type: AWS::EC2::Subnet::Id
  VPC:
    Type: AWS::EC2::VPC::Id
  TagName :
    Type: String
    Default: "testCFn"

Resources:
  EC2SG:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: sg.yaml
      Parameters:
        VPC: !Ref VPC
  
  EC2Instance:
    Type: AWS::EC2::Instance
    Properties: 
      ImageId: !Ref ImageId
      KeyName: !Ref KeyName
      InstanceType: t2.micro
      NetworkInterfaces: 
        - AssociatePublicIpAddress: "true"
          DeviceIndex: "0"
          SubnetId: !Ref Subnet
          GroupSet:
            - !GetAtt EC2SG.Outputs.SGID
             
      Tags:
          - Key: Name
            Value: !Ref TagName

```
* EC2テンプレートの作成(sg.yaml)の作成


```ruby:sg.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'SG for testCFn'
Parameters:
  MyIP:
    Type: String
    Default: "0.0.0.0/0"
  VPC:
    Type: AWS::EC2::VPC::Id
  TagName :
    Type: String
    Default: "testCFn"

Resources:
  EC2SG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: "test demo"
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: !Ref MyIP
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: !Ref MyIP
      Tags:
        - Key: Name
          Value: !Ref TagName

Outputs:
  SGID: 
    Value: !Ref EC2SG

```
# テンプレートファイルをCloudShell→S3へアップロードする

CloudShellとは？
AWS関連のツールがあらかじめインストールされたAmazon Linux 2の環境をAWS管理コンソールからボタン一つで作成でき、スクリプトの実行やAPIの呼び出しなどが簡単に行えます。また、ファイルのアップロード、ダウンロードも可能です。

AWS CLIの実行権限はAWS管理コンソールにログインしているIAMユーザの権限をそのまま使用できますので、非常に便利です。また、AWS CloudShellは追加費用なしで利用でき、リージョンごとに1GBの無料ストレージが含まれます。このストレージをホームディレクトリとして利用でき、スクリプトファイルや構成ファイル、追加のコマンドファイルなどを保存できます。

https://docs.aws.amazon.com/ja_jp/cloudshell/latest/userguide/welcome.html

* CloudShellを起動して以下のディレクトリ、ファイルを配置します。

/home/cloudshell-user/

├── main.yaml
│
└── vpc
│   └── vpc.yaml
│
├── subnet
│   └── public
│       └── subnet.yaml
│
├── ec2
│   ├── ec2.yaml
│   └── sg.yaml


* S3バケットの作成
以下のコマンドでtest-CFnというバケットを作成します。

```ruby:
aws s3 mb s3://test-CFn
```

* S3へアップロード
以下のコマンドでS3へアップロードします。

```ruby:
aws cloudformation package --template-file main.yaml \
    --s3-bucket test-CFn \
    --output-template-file test-CFn.yml
```

作成されたtest-CFn.ymlは以下となっています。
ファイル参照先がs3となっています。

```ruby:
AWSTemplateFormatVersion: '2010-09-09'
Description: test-CFn stack
Resources:
  VPC:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: https://s3.ap-northeast-1.amazonaws.com/test-CFn/hogehoge.template
      Parameters:
        TagName: VPCTEST
  PrivateSubnet:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: https://s3.ap-northeast-1.amazonaws.com/test-CFn/hogehoge.template
      Parameters:
        VPC:
          Fn::GetAtt:
          - VPC
          - Outputs.VPCID
  PublicSubnet:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: https://s3.ap-northeast-1.amazonaws.com/test-CFn/hogehoge.template
      Parameters:
        VPC:
          Fn::GetAtt:
          - VPC
          - Outputs.VPCID
  EC2Instance:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: https://s3.ap-northeast-1.amazonaws.com/test-CFn/hogehoge.template
      Parameters:
        VPC:
          Fn::GetAtt:
          - VPC
          - Outputs.VPCID
        KeyName: hogehoge
        Subnet:
          Fn::GetAtt:
          - PublicSubnet
          - Outputs.SubnetID

```

# Cloudformationリソース作成
* test-CFn.ymlの実行
以下のコマンドを実行します。

```ruby:
aws cloudformation deploy --template-file test-CFn.yml --stack-name test-CFn
```
コンソールを確認すると、各リソースが作成できた(CREATE_COMPLETE)ことが確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/816973/236edf0e-e770-5aa5-9f1c-835fece0c062.png)


# おわりに

サービスごとにスタックを分けて作成すると今後のメンテナンスが楽になると思いました。今後はCloudFormationの追加/変更時、運用面で考慮する部分を検討できたらと思います。本記事の続編としてCloudFormationで作成したEC2にAnsibleを導入し、OS設定するまでの記事を次回、投稿したいと思います。

本記事が何かのお役に立てれば幸いです。
ありがとうございました。