CloudFormationで高可用なWordpress環境構築〜スタックをネストする〜

# はじめに

下記のような定番構成をテンプレート化したい

![websever.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2097948/a2f2880f-dd64-dc20-bb71-2b1b63186c3d.png)

※**auto scailingは、テンプレートファイルに記述していません。**


テンプレートファイルは複数に分割して、可読性と変更のしやすさを向上させたいので、
cloudformationでスタックをネストした構成にする

# cloudformation ネストされたスタック とは
簡単に言うと
`親スタックが子のスタックを作成すること`

１つのテンプレートファイルに全てのリソースを記述には、行数が膨大になりリソースごとに分割する必要がある。しかし、分割するとデプロイ時に、テンプレートの数だけコマンドを叩く必要があるので、面倒。

そこでネストされたスタックを使用することで、親スタックをデプロイすれば依存関係のある子スタックもデプロイしてくれるので、とっても便利！
また、パラメータの変更など修正箇所を親テンプレートのみにできるので、エンハンスもしやすくなるなどのメリットもある。


#大まかな手順
####1.テンプレートファイルの作成
####2.子テンプレートをS3へアップロード
####3.子テンプレートのリンクを親テンプレートへ記述
####4.親テンプレートのスタックを作成


#テンプレート
```
.
├── root.yml
└── vpc.yml
└── ec2.yml
└── elb.yml
└── rds.yml
```

# AWS::CloudFormation::Stack

ネストを使用する場合は、Typeに、`AWS::CloudFormation::Stack`を指定する

```yaml:root.yml
Resources:
  # VPCの作成
  VPC:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Ref TemplateVPC
      Parameters:
        AZ1: !Select [0, !GetAZs ""]
        AZ2: !Select [1, !GetAZs ""]
        CIDRBlock: "10.0.0.0/16"
        PublicSubnet1CIDR: "10.0.1.0/24"
        PublicSubnet2CIDR: "10.0.2.0/24"
        PrivateSubnet1CIDR: "10.0.3.0/24"
        PrivateSubnet2CIDR: "10.0.4.0/24"
```


`Parameters:`に記載したパラメータは、子テンプレートに引き継いで使用できる

```yaml:vpc.yml
# パラメータ
Parameters:
  AZ1:
    Type: String
    Description: Use AZ name
  AZ2:
    Type: String
    Description: Use AZ name
  CIDRBlock:
    Type: String
    Description: CIDRBlock of VPC
  PublicSubnet1CIDR:
    Type: String
    Description: CIDRBlock of PublicSubnet1
  PublicSubnet2CIDR:
    Type: String
    Description: CIDRBlock of PublicSubnet2
  PrivateSubnet1CIDR:
    Type: String
    Description: CIDRBlock of PrivateSubnet1
  PrivateSubnet2CIDR:
    Type: String
    Description: CIDRBlock of PrivateSubnet2

Resources:
  # VPCの作成
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref CIDRBlock
      InstanceTenancy: default
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags: 
        - Key: !FindInMap [Common, ServiceTag, Key]
          Value: !FindInMap [Common, ServiceTag, Value]
```

`Parameters:`に指定したkeyを、`!Ref`で参照することができる

#子テンプレートをS3にアップロード

```
$ aws s3 mb yourbacket-name
$ aws s3 ls

#テンプレートファイルがあるディレクトリへ移動
$ cd currentDir

#カレントディレクトリにあるファイルを全てS3へアップロード
$ aws s3 cp . yourbacket-name --recursive

```

#子テンプレートのリンクをroot.ymlに記載

```yaml:roo.yml
Parameters:
  TemplateVPC:
    Description: VPC template Object URL
    Type: String
    Default: https://~~~~/vpc.yml
```

#テンプレートファイルの構文チェック

```shell
$ aws cloudformation \
  validate-template --template-body file://root.yml 
```
yamlの構文に問題があれば、エラーとして表示される

#root.ymlをデプロイ

```shell
$ aws cloudformation \
  create-stack --stack-name test-cfn \
  --template-body file://root.yml
```
作成されたstackのidが返ってくる

# スタックのアップデート
アップデート前に、changesetを作成すると、変更箇所が影響を及ぼすか確認することができる
アップデート時には、リソースが再起動する場合もあるので、注意が必要

```shell
$ aws cloudformation \
  create-change-set --stack-name test-cfn　\
  --change-set-name fixup \
  --template-body file://root.yml
```

```shell
$ aws cloudformation \
  update-stack --stack-name test-cfn \
  --template-body file://root.yml
```

#(参考)省略記法

```yaml
 #省略前
 VpcId: {Ref: VPC}
 #省略記法
 VpcId: !Ref VPC
```

#(参考)組込関数

##！Ref
```yaml
 VpcId: !Ref VPC
```

* パラメータを指定した場合、設定した値
* リソースを指定した場合、リソースのID

が返り値となる


##!GetAtt
```yaml
Outputs:
  VPCId:
    Value: !GetAtt VPC.Outputs.VPCId
    Description: VPC CIDR Block
```
リソースごとに戻り値が異なるため、確認が必要

##！Base64
```yaml
      UserData: !Base64 |
        #! /bin/bash
        yum update -y
        amazon-linux-extras install php7.2 -y
        yum -y install mysql httpd php-mbstring php-xml
        
        wget http://ja.wordpress.org/latest-ja.tar.gz -P /tmp/
        tar zxvf /tmp/latest-ja.tar.gz -C /tmp
        cp -r /tmp/wordpress/* /var/www/html/
        touch /var/www/html/.check_alive
        chown apache:apache -R /var/www/html
        
        systemctl enable httpd.service
        systemctl start httpd.service
```

入力文字列をBase64表現に変換する

#テンプレート全体
```yaml:root.yml
AWSTemplateFormatVersion: "2010-09-09"
Description: Manage myself verification environment

Parameters:
  TemplateVPC:
    Description: VPC template Object URL
    Type: String
    Default: https://~~~~/vpc.yml

  TemplateEC2:
    Description: EC2 template Object URL
    Type: String
    Default: https://~~~~/ec2.yml

  TemplateELB:
    Description: ELB template Object URL
    Type: String
    Default: https://~~~~/elb.yml

  TemplateRDS:
    Description: RDS template Object URL
    Type: String
    Default: https://~~~~/rds.yml

Resources:
  # VPCの作成
  VPC:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Ref TemplateVPC
      Parameters:
        AZ1: !Select [0, !GetAZs ""]
        AZ2: !Select [1, !GetAZs ""]
        CIDRBlock: "10.0.0.0/16"
        PublicSubnet1CIDR: "10.0.1.0/24"
        PublicSubnet2CIDR: "10.0.2.0/24"
        PrivateSubnet1CIDR: "10.0.3.0/24"
        PrivateSubnet2CIDR: "10.0.4.0/24"

  # EC2の作成
  EC2:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Ref TemplateEC2
      Parameters:
        VPCId: !GetAtt VPC.Outputs.VPCId
        PublicSubnet1Id: !GetAtt VPC.Outputs.PublicSubnet1Id
        PublicSubnet2Id: !GetAtt VPC.Outputs.PublicSubnet2Id
        EC2AMI: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2
      # VPC作成後に作成
    DependsOn: VPC

  # ELBの作成
  ELB:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Ref TemplateELB
      Parameters:
        VPCId: !GetAtt VPC.Outputs.VPCId
        PublicSubnet1Id: !GetAtt VPC.Outputs.PublicSubnet1Id
        PublicSubnet2Id: !GetAtt VPC.Outputs.PublicSubnet2Id
        EC2WebServer01: !GetAtt EC2.Outputs.EC2WebServer01
        EC2WebServer02: !GetAtt EC2.Outputs.EC2WebServer02
    DependsOn: EC2

  # RDSの作成
  RDS:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Ref TemplateRDS
      Parameters:
        VPCId: !GetAtt VPC.Outputs.VPCId
        PrivateSubnet1Id: !GetAtt VPC.Outputs.PrivateSubnet1Id
        PrivateSubnet2Id: !GetAtt VPC.Outputs.PrivateSubnet2Id
        DBUser: dbmaster
        DBPassword: H&ppyHands0n
    DependsOn: VPC

# !GetAttは、テンプレートのリソースから属性の値を返す
Outputs:
  VPCId:
    Value: !GetAtt VPC.Outputs.VPCId
    Description: VPC CIDR Block

  PublicSubnet1Id:
    Value: !GetAtt VPC.Outputs.PublicSubnet1Id
    Description: PublicSubnet1 CIDR Block

  PublicSubnet2Id:
    Value: !GetAtt VPC.Outputs.PublicSubnet2Id
    Description: PublicSubnet2 CIDR Block

  PrivateSubnet1Id:
    Value: !GetAtt VPC.Outputs.PrivateSubnet1Id
    Description: PrivateSubnet1 CIDR Block

  PrivateSubnet2Id:
    Value: !GetAtt VPC.Outputs.PrivateSubnet2Id
    Description: PrivateSubnet2 CIDR Block

  EC2WebServer01:
    Value: !GetAtt EC2.Outputs.EC2WebServer01
    Description: EC2WebServer01 ID

  EC2WebServer02:
    Value: !GetAtt EC2.Outputs.EC2WebServer02
    Description: EC2WebServer02 ID
  
  DBEndpoint:
    Value: !GetAtt RDS.Outputs.DBEndpoint
    Description: MySQL Endpoint 

  FrontLBEndpoint:
    Value: !GetAtt ELB.Outputs.FrontLBEndpoint
    Description: ELB Endpoint
```

```yaml:vpc.yml
AWSTemplateFormatVersion: "2010-09-09"
Description: Provision of VPC

# tag
Mappings:
  Common:
    ServiceTag: {Key: Name, Value: dev}

# パラメータ
Parameters:
  AZ1:
    Type: String
    Description: Use AZ name
  AZ2:
    Type: String
    Description: Use AZ name
  CIDRBlock:
    Type: String
    Description: CIDRBlock of VPC
  PublicSubnet1CIDR:
    Type: String
    Description: CIDRBlock of PublicSubnet1
  PublicSubnet2CIDR:
    Type: String
    Description: CIDRBlock of PublicSubnet2
  PrivateSubnet1CIDR:
    Type: String
    Description: CIDRBlock of PrivateSubnet1
  PrivateSubnet2CIDR:
    Type: String
    Description: CIDRBlock of PrivateSubnet2

Resources:
  # VPCの作成
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref CIDRBlock
      InstanceTenancy: default
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags: 
        - Key: !FindInMap [Common, ServiceTag, Key]
          Value: !FindInMap [Common, ServiceTag, Value]

  # IGWの作成
  IGW:
    Type: AWS::EC2::InternetGateway
    DependsOn: VPC
    Properties: 
      Tags: 
        - Key: !FindInMap [Common, ServiceTag, Key]
          Value: !FindInMap [Common, ServiceTag, Value]

  # IGWをVPCにアタッチ
  IGWAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref IGW
  
  # Public Subnetの作成
  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Ref AZ1
      CidrBlock: !Ref PublicSubnet1CIDR
      MapPublicIpOnLaunch: true
      Tags: 
        - Key: Name
          Value: Public1-a

  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Ref AZ2
      CidrBlock: !Ref PublicSubnet2CIDR
      MapPublicIpOnLaunch: true
      Tags: 
        - Key: Name
          Value: Public1-c

  # Private Subnetの作成
  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Ref AZ1
      CidrBlock: !Ref PrivateSubnet1CIDR
      Tags: 
        - Key: Name
          Value: Private1-a

  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Ref AZ2
      CidrBlock: !Ref PrivateSubnet2CIDR
      Tags: 
        - Key: Name
          Value: Private1-c

# Public Route Table　の作成と関連付け
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
  
  DefaultPublicRoute:
    Type: AWS::EC2::Route
    DependsOn: IGWAttachment
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref IGW
  
  PublicSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet1

  PublicSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet2

Outputs:
  VPCId:
    Value: !Ref VPC
    Description: VPC id

  PublicSubnet1Id:
    Value: !Ref PublicSubnet1
    Description: PublicSubnet1 id

  PublicSubnet2Id:
    Value: !Ref PublicSubnet2
    Description: PublicSubnet2 id

  PrivateSubnet1Id:
    Value: !Ref PrivateSubnet1
    Description: PrivateSubnet1 id

  PrivateSubnet2Id:
    Value: !Ref PrivateSubnet2
    Description: PrivateSubnet2 id
```

```yaml:ec2.yml
AWSTemplateFormatVersion: "2010-09-09"
Description: Provision of EC2

# tag
Mappings:
  Common:
    ServiceTag: {Key: Name, Value: dev}

# パラメータ
Parameters:
  VPCId:
    Type: String
    Description: ID for VPC.
  PublicSubnet1Id:
    Type: String
    Description: ID for PublicSubnet1
  PublicSubnet2Id:
    Type: String
    Description: ID for PublicSubnet2
  EC2AMI:
    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
    Description: AMI

# EC2インスタンスの作成
Resources:
  EC2WebServer01:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref EC2AMI
      InstanceType: t2.micro
      SubnetId: !Ref PublicSubnet1Id
      UserData: !Base64 |
        #! /bin/bash
        yum update -y
        amazon-linux-extras install php7.2 -y
        yum -y install mysql httpd php-mbstring php-xml
        
        wget http://ja.wordpress.org/latest-ja.tar.gz -P /tmp/
        tar zxvf /tmp/latest-ja.tar.gz -C /tmp
        cp -r /tmp/wordpress/* /var/www/html/
        touch /var/www/html/.check_alive
        chown apache:apache -R /var/www/html
        
        systemctl enable httpd.service
        systemctl start httpd.service
      SecurityGroupIds:
        - !Ref WEBSG
      Tags: 
        - Key: !FindInMap [Common, ServiceTag, Key]
          Value: !FindInMap [Common, ServiceTag, Value]

  EC2WebServer02:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref EC2AMI
      InstanceType: t2.micro
      SubnetId: !Ref PublicSubnet2Id
      UserData: !Base64 |
        #! /bin/bash
        yum update -y
        amazon-linux-extras install php7.2 -y
        yum -y install mysql httpd php-mbstring php-xml
        
        wget http://ja.wordpress.org/latest-ja.tar.gz -P /tmp/
        tar zxvf /tmp/latest-ja.tar.gz -C /tmp
        cp -r /tmp/wordpress/* /var/www/html/
        touch /var/www/html/.check_alive
        chown apache:apache -R /var/www/html
        
        systemctl enable httpd.service
        systemctl start httpd.service
      SecurityGroupIds:
        - !Ref WEBSG
      Tags: 
        - Key: !FindInMap [Common, ServiceTag, Key]
          Value: !FindInMap [Common, ServiceTag, Value]
        
  WEBSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: sg-web
      VpcId: !Ref VPCId
      SecurityGroupIngress:
        # http
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 10.0.0.0/16
        # ssh
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 10.0.0.0/16

Outputs:
  EC2WebServer01:
    Value: !Ref EC2WebServer01
    Description: EC2WebServer01 id

  EC2WebServer02:
    Value: !Ref EC2WebServer02
    Description: EC2WebServer02 id
```

```yaml:elb.yml
AWSTemplateFormatVersion: "2010-09-09"
Description: Provision of ELB

# tag
Mappings:
  Common:
    ServiceTag: {Key: Name, Value: dev}

# パラメータ
Parameters:
  VPCId:
    Type: String
    Description: ID for VPC.
  PublicSubnet1Id:
    Type: String
    Description: ID for PublicSubnet1
  PublicSubnet2Id:
    Type: String
    Description: ID for PublicSubnet2
  EC2WebServer01:
    Type: String
    Description: EC2WebServer01 ID
  EC2WebServer02:
    Type: String
    Description: EC2WebServer02 ID

# ロードバランサーの作成
Resources:
  SecurityGroupLB:
    Type: AWS::EC2::SecurityGroup
    Properties: 
      GroupDescription: ELB-Web-SG
      VpcId: !Ref VPCId
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0

  FrontLB:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: elb-webserver
      Subnets:
        - !Ref PublicSubnet1Id
        - !Ref PublicSubnet2Id
      SecurityGroups: 
        - !Ref SecurityGroupLB
      Type: application
      Tags: 
        - Key: !FindInMap [Common, ServiceTag, Key]
          Value: !FindInMap [Common, ServiceTag, Value]

  FrontLBTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: web-tg
      VpcId: !Ref VPCId
      # HealthCheck
      HealthCheckIntervalSeconds: 30
      # HealthCheckPath: /.check_alive
      HealthCheckPath: '/'
      HealthCheckPort: 80
      HealthCheckProtocol: HTTP
      HealthCheckTimeoutSeconds: 5
      HealthyThresholdCount: 5
      UnhealthyThresholdCount: 2
      Matcher:
        HttpCode: '200'
      # Routing
      # MEMO: トラフィックポートの場合は 'traffic-port'
      Port: 80
      Protocol: HTTP
      TargetType: 'instance'
      TargetGroupAttributes:
        # ターゲットの登録解除までの待機時間
        - Key: 'deregistration_delay.timeout_seconds'
          Value: 300
      Targets:
        - Id: !Ref EC2WebServer01
          Port: 80
        - Id: !Ref EC2WebServer02
          Port: 80

  FrontLBListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn: !Ref FrontLB
      Port: 80
      Protocol: HTTP 
      DefaultActions: 
        - Type: forward
          TargetGroupArn: !Ref FrontLBTargetGroup


Outputs:
  FrontLBEndpoint:
    Value: !GetAtt FrontLB.DNSName
    Description: ELB Endpoint
```

```yaml:rds.yml
AWSTemplateFormatVersion: "2010-09-09"
Description: Provision of RDS

# tag
Mappings:
  Common:
    ServiceTag: {Key: Name, Value: dev}

# パラメータ
Parameters:
  VPCId:
    Type: String
    Description: ID for VPC.
  PrivateSubnet1Id:
    Type: String
    Description: ID for PrivateSubnet1
  PrivateSubnet2Id:
    Type: String
    Description: ID for PrivateSubnet1
  DBUser:
    Type: String
    Description: DBUser ID
  DBPassword:
    Type: String
    Description: DBUser Password
    NoEcho: true

# RDSインスタンスの作成
Resources:
  DBInstance:
    Type: AWS::RDS::DBInstance
    DeletionPolicy: Delete
    Properties:
      DBInstanceClass: db.t2.micro
      AllocatedStorage: "10"
      StorageType: gp2
      Engine: MySQL
      MultiAZ: True
      MasterUsername: !Ref DBUser
      MasterUserPassword: !Ref DBPassword
      DBName: wordpress
      BackupRetentionPeriod: 0
      DBSubnetGroupName: !Ref DBSubnetGroup
      VPCSecurityGroups:
        - !Ref DBSecurityGroup

  DBSubnetGroup: 
    Type: AWS::RDS::DBSubnetGroup
    Properties: 
      DBSubnetGroupDescription: DB Subnet Group for Private Subnet
      SubnetIds: 
        - !Ref PrivateSubnet1Id
        - !Ref PrivateSubnet2Id

  DBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties: 
      GroupDescription: sg-MySQL
      VpcId: !Ref VPCId
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 3306
          ToPort: 3306
          CidrIp: 10.0.0.0/16


Outputs:
  DBEndpoint:
    Value: !GetAtt DBInstance.Endpoint.Address
    Description: MySQL Endpoint 
```

#最後に
上記、ファイルを使用すれば、エラーなくスタック作成できると思います。
細かいパラメータの設定などは、要件によって変更してください。
isuueなどあれば、お気軽にお願いします。