冗長構成WebサーバーのCloudFormationテンプレートを作成、デプロイ

## はじめに
以前ハンズオンで学習したCFnを使用してWordPress環境を構築した際の応用として、マルチAZで分散する冗長構成のテンプレートをCloudFormationで書いてみました。
動作検証、比較も兼ねてAmazon QとLinterを拡張機能を導入したVScodeとChatGPTを使用。


## やること
- 冗長構成のVPC作成
- EC2インスタンス（Pub）とRDSインスタンス（Pri）をマルチAZ構成で作成
- RDSはパブリックサブネットのEC2からのみのアクセス許可
- ELBでEC2の冗長化
- LBのトラフィック制御

## 構成図
![名称未設定ファイル.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3812406/108ef713-7cd1-a7a7-eff1-3c1f204a0e12.png)


## 使用ツール
- VScode(Amazon Q, cfn-linter)
- CloudFormation
- ChatGPT

### 以下が作成したテンプレートです

## VPC
```
AWSTemplateFormatVersion: 2010-09-09
Description: VPC with public and private subnets

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: xxxxxxx

  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.0.0/24
      AvailabilityZone: !Select
        - 0
        - Fn::GetAZs: !Ref AWS::Region
      MapPublicIpOnLaunch: true

  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.1.0/24
      AvailabilityZone: !Select
        - 1
        - Fn::GetAZs: !Ref AWS::Region
      MapPublicIpOnLaunch: true

  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.10.0/24
      AvailabilityZone: !Select
        - 0
        - Fn::GetAZs: !Ref AWS::Region       

  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.11.0/24
      AvailabilityZone: !Select
        - 1
        - Fn::GetAZs: !Ref AWS::Region

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: test-igw

  VPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: test-public-crt

  PublicSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet1
      RouteTableId: !Ref PublicRouteTable

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

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

Outputs:
  VPCID:
    Description: VPC ID
    Value: !Ref VPC
    Export:
      Name: !Sub ${AWS::StackName}-VPCID

  PublicSubnet1ID:
    Description: Public Subnet1 ID
    Value: !Ref PublicSubnet1
    Export:
      Name: !Sub ${AWS::StackName}-public-subnet1-id

  PublicSubnet2ID:
    Description: Public Subnet2 ID
    Value: !Ref PublicSubnet2
    Export:
      Name: !Sub ${AWS::StackName}-public-subnet2-id

  PrivateSubnet1ID:
    Description: Private Subnet1 ID
    Value: !Ref PrivateSubnet1
    Export:
      Name: !Sub ${AWS::StackName}-private-subnet1-id

  PrivateSubnet2ID:
    Description: Private Subnet2 ID
    Value: !Ref PrivateSubnet2
    Export:
      Name: !Sub ${AWS::StackName}-private-subnet2-id      
```      
## EC2
```
AWSTemplateFormatVersion: 2010-09-09
Description: template to launch an EC2 instance

Parameters:
  VPCStack:
    Type: String
    Description: xxxxx
  EC2AMI:
    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
    Default: "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2"

Resources:
  EC2Instance1A:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref EC2AMI
      InstanceType: t2.micro
      SubnetId: xxxxxxxxxxxxxxxxx
      UserData: 
        Fn::Base64: |
          #! /bin/bash
          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

          sudo systemctl start httpd.service
          sudo systemctl enable httpd.service
      SecurityGroupIds:
        - !Ref EC2SG

  EC2Instance1C:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref EC2AMI
      InstanceType: t2.micro
      SubnetId: xxxxxxxxxxxxxxxxx
      UserData: 
        Fn::Base64: |
          #! /bin/bash
          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

          sudo systemctl start httpd.service
          sudo systemctl enable httpd.service
      SecurityGroupIds:
        - !Ref EC2SG

  EC2SG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allow HTTP access
      VpcId: xxxxxxxxxxxxxxxxxxxxxxxxx
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 10.0.0.0/16

Outputs:
  EC2Instance1A:
    Value: !Ref EC2Instance1A
    Export:
      Name: !Sub ${AWS::StackName}-EC2Instance1A

  EC2Instance1C:
    Value: !Ref EC2Instance1C
    Export:
      Name: !Sub ${AWS::StackName}-EC2Instance1C
```



## RDS
```
AWSTemplateFormatVersion: 2010-09-09
Description: CloudFormation template to create an RDS instance

Parameters:
  VPCStackName:
    Type: String
    Default: xxxxxxx
  DBUsername:
    Type: String
    Default: admin
    Description: The name of the database.
  DBPassword:
    Type: String
    Default: xxxxxxxx
    NoEcho: true
    Description: The password for the database user. 

Resources:
  MyDBInstance:
    Type: AWS::RDS::DBInstance
    DeletionPolicy: Delete
    Properties:
      DBInstanceClass: db.t3.micro
      AllocatedStorage: 10
      StorageType: gp2
      Engine: MySQL
      MasterUsername: !Ref DBUsername
      MasterUserPassword: !Ref DBPassword
      DBName: wordpress
      BackupRetentionPeriod: 0
      DBSubnetGroupName: !Ref MyDBSubnetGroup
      VPCSecurityGroups:
        - !Ref MyDBSecurityGroup

  MyDBSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties: 
      DBSubnetGroupDescription: Subnet group for RDS
      SubnetIds: 
        - subnet-xxxxxxxxxxxxxxxxx
        - subnet-xxxxxxxxxxxxxxxxx
         
  MyDBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties: 
      GroupDescription: Security group for RDS
      VpcId: vpc-xxxxxxxxxxxxxxxxx
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 3306
          ToPort: 3306
          CidrIp: 10.0.0.0/16

Outputs:
  DBEndpoint:
    Value: !GetAtt MyDBInstance.Endpoint.Address
    Export:
      Name: !Sub ${AWS::StackName}-DBEndpoint
```      
      


## ELB
ターゲットグループで複数のEC2インスタンスの指定はできなかったので書き加えました。
```
AWSTemplateFormatVersion: '2010-09-09'
Description: CloudFormation template to create EC2 instances with Multi-AZ and ELB for load balancing (without Auto Scaling)

Parameters:
  VPCStack:
    Type: String
    Default: xxxxxx
  EC2Stack:
    Type: String
    Default: xxxxxx

Resources:
  LoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: "MyLoadBalancer"
      Subnets:
        - subnet-xxxxxxxxxxxxxxxxx
        - subnet-xxxxxxxxxxxxxxxxx
      SecurityGroups:
        - !Ref LoadBalancerSecurityGroup
      Scheme: internet-facing

  LoadBalancerListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn: !Ref LoadBalancer
      Protocol: HTTP
      Port: 80
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref LoadBalancerTargetGroup

  LoadBalancerTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: "MyTargetGroup"
      VpcId: vpc-xxxxxxxxxxxxxxxx
      Port: 80
      Protocol: HTTP
      Targets:
        - Id: i-xxxxxxxxxxxxxxxxx    
        - Id: i-xxxxxxxxxxxxxxxxx
      HealthCheckPath: /.check_alive

  LoadBalancerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for the Load Balancer
      VpcId: vpc-xxxxxxxxxxxxxxxxx
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0

Outputs:
  LoadBalancerEndpoint:
    Value: !GetAtt LoadBalancer.DNSName
    Export:
      Name: !Sub ${AWS::StackName}-Endpoint
```      



## 検証
- ALBのトラフィック検証としてap-northeast-1aのEC2インスタンスを停止した状態でトラフィックが1cのみに制限されたことを確認
![スクリーンショット 2024-11-16 171214.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3812406/8d0d056f-0401-e002-065f-74f295424668.png)
----
![スクリーンショット 2024-11-16 171702.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3812406/1cfe41d1-f849-6a18-ecbc-b40bb3440622.png)


## 資料
[AWS リソースおよびプロパティタイプのリファレンス](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html)

## 最後に
- Amazon Qは自分で構文の前後を読み取って自動で組込関数を指定して参照したり、マルチAZの指定をしてくれたりと便利だった
- どちらも出力をそのまま使おうとすると、Paramatersに余計な構文を入れていたり逆になかったりと、結局は見直しで時間がかかったため公式のテンプレートリファレンスを参照しながら進めた
- ただアシストツールとしては優れているので、ある程度の知識があればIaC実践のハードルを下げてくれると思う