【AWS】EC2(wordpress)+RDSの基本構成をCloudFormationで構築

#■はじめに
EC2(wordpress)・RDSのシングル構成(※)を今度はCloudFormationで構築してみました。
※[AWSで基本的なブログサービスを構築する（シングル構成）](https://qiita.com/CycleSHD/items/0a02aae2e2321adf5496)

#■構成図
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1864204/2218ac46-df39-bc6d-debf-50161c303091.png)

#■テンプレート概要

https://d1.awsstatic.com/webinars/jp/pdf/services/20200826_AWS-BlackBelt_AWS-CloudFormation.pdf

上記 85ページを参考に、
ネットワーク、セキュリティ、アプリケーションの3つに分ける。
加え、一部設定値を記述した外部ファイルの計4つを作成する。

 - ネットワーク ⇒ cf_network.yml
 - セキュリティ ⇒ cf_security.yml
 - アプリケーション ⇒ cf_application.yml
 - 設定ファイル ⇒ dev.cfg

#■テンプレート詳細

<details><summary>【ネットワークテンプレート】</summary><div>

VPC, サブネット, ルートテーブルについて記述する。

**VPC**
CidrBlockの指定。

**サブネット**
パブリックサブネット1,2、プライベートサブネット1,2のAZ,VPC,CidrBlockの指定。

**DBサブネットグループ**
SubnetIdsでプライベートサブネット1,2 であることを指定。

**インターネットゲートウェイ**
VPC1にアタッチすることを指定。

**ルートテーブル**
パブリックサブネット用のルートテーブルには localとインターネットゲートウェイ向けの設定。
プライベートサブネット用のルートテーブルには 宛先指定なし(localのみになる)の設定。

**output**
VPCID,サブネットID,サブネットグループ名、インターネットゲートウェイ名、ルートテーブルIDをアウトプット指定。

```ruby:cf_network.yml
AWSTemplateFormatVersion: 2010-09-09
Resources: 
#-----------------------
#VPC Create
#-----------------------
  MyVPC1Cf:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.8.0/21
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: MyVPC1-Cf
#-----------------------
#Subnet Create
#-----------------------
#PublicSubnet1 create
  PublicSubnet1Cf:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: "ap-northeast-1a"
      VpcId: !Ref MyVPC1Cf
      CidrBlock: 10.0.10.0/24
      Tags:
        - Key: Name
          Value: PublicSubnet1-Cf
#PublicSubnet2 create
  PublicSubnet2Cf:    
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: "ap-northeast-1c"
      VpcId: !Ref MyVPC1Cf
      CidrBlock: 10.0.11.0/24
      Tags:
        - Key: Name
          Value: PublicSubnet2-Cf
#PrivateSubnet1 create
  PrivateSubnet1Cf:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: "ap-northeast-1a"
      VpcId: !Ref MyVPC1Cf
      CidrBlock: 10.0.12.0/24
      Tags:
        - Key: Name
          Value: PrivateSubnet1-Cf
#PrivateSubnet2 create
  PrivateSubnet2Cf:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: "ap-northeast-1c"
      VpcId: !Ref MyVPC1Cf
      CidrBlock: 10.0.13.0/24
      Tags:
        - Key: Name
          Value: PrivateSubnet2-Cf
#-----------------------
#RDS SubnetGroup Create
#-----------------------
  RdsSubnetGroupCf:
    Type: AWS::RDS::DBSubnetGroup
    Properties: 
      DBSubnetGroupName: RdsSubnetGroupCf
      DBSubnetGroupDescription: RdsSubnetGroupCf
      SubnetIds: 
        - !Ref PrivateSubnet1Cf
        - !Ref PrivateSubnet2Cf
      Tags: 
        - Key: Name
          Value: RdsSubnetGroup-Cf
#-----------------------
#InternetGateway Create
#-----------------------
  InternetGW1Cf:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: InternetGW1-Cf
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref MyVPC1Cf
      InternetGatewayId: !Ref InternetGW1Cf
#-----------------------
#Routetable Create
#-----------------------
#PublicRoutetable create
  PublicRoutetableCf:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref MyVPC1Cf
      Tags:
        - Key: Name
          Value: PublicRoutetable-Cf
  PublicRoutetableAssociationCf:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet1Cf
      RouteTableId: !Ref PublicRoutetableCf
  PublicRouteCf:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PublicRoutetableCf
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGW1Cf
#PrivateRoutetable create
  PrivateRoutetableCf:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref MyVPC1Cf
      Tags:
        - Key: Name
          Value: PrivateRoutetable-Cf
  PrivateRoutetableAssociationCf:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet1Cf
      RouteTableId: !Ref PrivateRoutetableCf
Outputs:
#-----------------------
#VPC output
#-----------------------
    MyVPC1CfOutput:
      Value: !Ref MyVPC1Cf
      Export:
        Name: MyVPC1Cf-Id
#-----------------------
#Subnet output
#-----------------------
#PublicSubnet1Cf
    PublicSubnet1CfOutput:
      Value: !Ref PublicSubnet1Cf
      Export:
        Name: PublicSubnet1Cf-Id
#PublicSubnet2Cf
    PublicSubnet2CfOutput:
      Value: !Ref PublicSubnet2Cf
      Export:
        Name: PublicSubnet2Cf-Id
#PrivateSubnet1Cf
    PrivateSubnet1CfOutput:
      Value: !Ref PrivateSubnet1Cf
      Export:
        Name: PrivateSubnet1Cf-Id
#PrivateSubnet1Cf
    PrivateSubnet2CfOutput:
      Value: !Ref PrivateSubnet2Cf
      Export:
        Name: PrivateSubnet2Cf-Id
#-----------------------
#RDS SubnetGroup output
#-----------------------
    RdsSubnetGroupCfOutput:
      Value: !Ref RdsSubnetGroupCf
      Export:
        Name: RdsSubnetGroupCf-GroupName
#-----------------------
#InternetGateway output
#-----------------------
    InternetGW1CfOutput:
      Value: !Ref InternetGW1Cf
      Export:
        Name: InternetGW1Cf-Name
#-----------------------
#Routetable output
#-----------------------
#PublicRoutetableCf
    PublicRoutetableCfOutput:
      Value: !Ref PublicRoutetableCf
      Export:
        Name: PublicRoutetableCf-Id
#PrivateRoutetableCf
    PrivateRoutetableCfOutput:
      Value: !Ref PrivateRoutetableCf
      Export:
        Name: PrivateRoutetableCf-Id

```

</div></details>

<details><summary>【セキュリティテンプレート】</summary><div>

セキュリティグループについて記述する。

**セキュリティグループ**
EC2セキュリティグループとRDSセキュリティグループの作成。
EC2はsshとhttpのインバウンド、RDSはmysqlのインバウンド指定。

**アウトプット**
アプリケーションテンプレートから セキュリティグループのIDを参照するためGetAttで指定。

```ruby:cf_security.yml
AWSTemplateFormatVersion: 2010-09-09
Resources: 
#-----------------------
#SecurityGroup Create
#-----------------------
#EC2 SecurityGroup create
  WebSg1Cf:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: WebSg1Cf
      GroupDescription: WebSg1Cf
      VpcId: !ImportValue MyVPC1Cf-Id
      SecurityGroupIngress:
#       ssh
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 0.0.0.0/0
#       http
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: WebSg1-Cf
#RDS SecurityGroup create
  RdsSg1Cf:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: RdsSg1Cf
      GroupDescription: RdsSg1cf
      VpcId: !ImportValue MyVPC1Cf-Id
      SecurityGroupIngress:
        - SourceSecurityGroupId: !Ref WebSg1Cf
          IpProtocol: tcp
          FromPort: 3306
          ToPort: 3306
      Tags:
        - Key: Name
          Value: RdsSg1-Cf
Outputs:
#-----------------------
#SecurityGroup output
#-----------------------
#WebSg1Cf
    WebSg1CfOutput:
      Value: !GetAtt WebSg1Cf.GroupId
      Export:
        Name: WebSg1Cf-Id
#RdsSg1Cf
    RdsSg1CfOutput:
      Value: !GetAtt RdsSg1Cf.GroupId
      Export:
        Name: RdsSg1Cf-Id
```

</div></details>

<details><summary>【アプリケーションテンプレート】</summary><div>

EC2, RDSについて記述する。

**パラメータ**
EC2インスタンスのイメージIDとインスタンスタイプの指定、キーペア名の指定、データベースパスワードの型定義。
イメージIDとは、マネジメントコンソールでいうと以下の部分に該当する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1864204/8a0a46e9-4358-0787-3b63-b573ae31f766.png)

CLIで以下を実行するとインスタンスイメージIDが取得できる。

```
$ aws ssm get-parameter --name /aws/service/ami-amazon-linux-latest/amzn2-ami-kernel-5.10-hvm-x86_64-gp2 --query "Parameter.Value" --region ap-northeast-1

ami-0404778e217f54308
```

CloudFormationで自動で取得するにはParameterで以下を指定する。

```
  EC2ImageIdCf:
    Type: AWS::SSM::Parameter::Value<String>
    Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-kernel-5.10-hvm-x86_64-gp2
```

キーペアはあらかじめ作成しておく前提。
以下記述をしておくことで、マネジメントコンソールから選択できるようになる。
また、CLIからデプロイ実行時は"Default"のキーペア名が使用される

```
  KeyNameCf:
    Description: The EC2 Key Pair to allow SSH access to the instance
    Type: "AWS::EC2::KeyPair::KeyName"
    Default: "Mykeypair"
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1864204/ba3570a0-d587-5d64-beaf-e650d9a80893.png)


**EC2**
パブリックIPの自動割り当て有効化のため、"NetworkInterface:" を記述。
ここでインデックスを指定しないと自動割り当てされない。(もしくはVPC側の設定で自動割り当てされるようにする必要がある)
wordpressのインストールコマンドは「UserData」に記述。

NetworkInterfaceとは、マネジメントコンソールでいうと以下の部分に該当する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1864204/70153d2f-dd31-114e-ea87-4f58a206cc80.png)

テンプレートでは以下のように記述する。セキュリティグループID(GroupSet)も記述しなければならない。

```
      NetworkInterfaces:
        - AssociatePublicIpAddress: "true"
          DeviceIndex: "0"
          SubnetId: !ImportValue PublicSubnet1Cf-Id
          GroupSet:
            - !ImportValue WebSg1Cf-Id

```

**RDS**
マスターユーザーパスワードは、コマンド実行時に外部ファイルをオーバーライド指定するので !Refで参照する


```ruby:cf_application.yml
AWSTemplateFormatVersion: 2010-09-09
Parameters:
#-----------------------
#EC2 Instance Parameter
#-----------------------
  EC2ImageIdCf:
    Type: AWS::SSM::Parameter::Value<String>
    Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-kernel-5.10-hvm-x86_64-gp2
  EC2InstanceTypeCf:
    Type: String
    Default: t2.micro
#-----------------------
#EC2 Keypair Parameter
#-----------------------
  KeyNameCf:
    Description: The EC2 Key Pair to allow SSH access to the instance
    Type: "AWS::EC2::KeyPair::KeyName"
    Default: "Mykeypair"
#-----------------------
#RDS DatabasePassword
#-----------------------
  DatabasePassword:
    Type: String
    Description: Database password
    NoEcho: "true"
Resources:
#-----------------------
#EC2 Create
#-----------------------
  WebServer1Cf:
    Type: AWS::EC2::Instance
    Properties:
      KeyName: !Ref KeyNameCf
      ImageId: !Ref EC2ImageIdCf
      InstanceType: !Ref EC2InstanceTypeCf
      Monitoring: false
      NetworkInterfaces:
        - AssociatePublicIpAddress: "true"
          DeviceIndex: "0"
          SubnetId: !ImportValue PublicSubnet1Cf-Id
          GroupSet:
            - !ImportValue WebSg1Cf-Id
#wordpress install
      UserData:
        Fn::Base64: |
          #!/bin/bash
          yum -y update
          amazon-linux-extras install php7.2 -y
          yum -y install mysql httpd php-mbstring php-xml gd php-gd
          systemctl enable httpd.service
          systemctl start httpd.service
          cd /var/tmp
          wget http://ja.wordpress.org/latest-ja.tar.gz ./
          tar zxvf ./latest-ja.tar.gz
          cp -r ./wordpress/* /var/www/html/
          chown apache:apache -R /var/www/html
      Tags:
        - Key: Name
          Value: WebServer1-Cf
#-----------------------
#RDS Create
#-----------------------
  Database1Cf:
    Type: AWS::RDS::DBInstance
    Properties:
      Engine: mysql
      EngineVersion: 5.7
      DBInstanceClass: db.t2.micro
      AllocatedStorage: 10
      StorageType: gp2
      MasterUsername: wordpress
      MasterUserPassword: !Ref DatabasePassword
      DBName: wordpress
      VPCSecurityGroups:
          - !ImportValue RdsSg1Cf-Id
      DBSubnetGroupName: !ImportValue RdsSubnetGroupCf-GroupName
      AvailabilityZone: "ap-northeast-1a"
      DBInstanceIdentifier: database-1-cf
      MultiAZ: false
      BackupRetentionPeriod: 0
      Tags:
        - Key: Name
          Value: Database1-Cf

```

</div></details>

<details><summary>【設定ファイル】</summary><div>

データベースパスワードのみ記述。

```ruby:dev.cfg
DatabasePassword=xxxxxx
```

</div></details>

#■作成時に詰まった点

<details><summary>【ec2 「NetworkInterfaces」のエラー】</summary><div>

NetworkInterfacesの中にGroupSetを記述する必要があるが、
以下のようにNetworkInterfacesとGroupSetを同列に記述してエラーが発生していた。

```
      NetworkInterfaces:
        - AssociatePublicIpAddress: "true"
          DeviceIndex: "0"
          SubnetId: !ImportValue PublicSubnet1Cf-Id
      GroupSet:
        - !ImportValue WebSg1Cf-Id
```

また、GroupSetではなく、SecurityGroupIds にIDを書くこともできるようだが、
そちらは私のテンプレートではエラーが出てしまっていた。List of Stringでないとか。
対処できていないがGroupSetで構築できてしまったので一旦パスした。

</div></details>

<details><summary>【外部ファイルからのデータベースパスワード読み込み不可】</summary><div>

エラー文字列をメモるのを忘れてしまったが、ファイル読み込み時に「パスワードに使用不可の文字が設定されている」といったようなエラー。
こちらも原因不明だが、外部ファイルを作り直したら読み込みに成功した(変な改行、ブランクが入っていた？ 文字コードがおかしかった？)

</div></details>

#■deploy、deleteコマンド

作成時は依存性の低い順、削除時は逆順で以下実行。

【作成】

```
$ aws cloudformation deploy --template-file cf_network.yml --stack-name CFNetwork
$ aws cloudformation deploy --template-file cf_security.yml --stack-name CFSecurity
$ aws cloudformation deploy --template-file cf_application.yml --stack-name CFApplication --parameter-overrides $(cat dev.cfg)
```

【削除】

```
$ aws cloudformation delete-stack --stack-name CFApplication
$ aws cloudformation delete-stack --stack-name CFSecurity
$ aws cloudformation delete-stack --stack-name CFNetwork
```