Webアプリ公開のためのAWS環境構築・CloudFormation編(備忘録ベース)

# はじめに

[RaiseTech](https://raise-tech.net/)の[AWSフルコース](https://raise-tech.net/courses/aws-full-course)にて学習した結果、ひとしきり課題どおりの成果物ができあがったこともあり、備忘録ベースで記事として残しておこうと思います。

こちらの記事では、Webアプリケーションを動作させるサーバ構成を、AWS上に構築するところまでを行っていきます。
あらかじめ作成したCloudFormationテンプレートを元に、AWS-CLIのコマンドでスタック作成させるところまでが一旦のゴールですね。
（Ansibleを用いたサーバ上の環境構築・デプロイは ~~別途記事を作成します~~ 文章を書くのがド下手クソで嫌いなのに、「エンジニア志望でアウトプットしないなんてカスだ」って風潮に流されて嫌々記事を書いた結果、精神的に痛手を負っただけで終わったので、続きは断固書きません。
[リポジトリ](https://github.com/artail2020/ansible)は一応公開してるので、「どうせ環境違えば動かないヘボPlayBookだろうから読んであざ笑ってやろう」って方はどうぞ）

__※ 一旦は目標通りに作ることが出来た……という段階であり、ベストプラクティスと呼ぶにはいろいろ内容が足りてないと思います。ご留意。__

## どんな環境が出来上がるのか

[今回のテンプレート（GitHubのリポジトリ）](https://github.com/artail2020/cloudformation-template)で作成する環境は、下の画像のとおりとなります。
Webアプリケーションが動作するEC2（__Amazon Linux2__）がアベイラビリティゾーン（A・C）ごとに1台ずつ・RDS（データベース・__MySQL__）・S3（静的ファイル保管）・外部ネットワークからのアクセスはELBを経由、と最小限の構成を組んでいきます。

![drawing_cf.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1353676/29e7eb5e-8315-a195-b661-1152869c16f1.png)

※ [draw.ioでAWSのインフラ構成図を書く - Qiita](https://qiita.com/nave-m/items/68425f476b254a1a47b0)を参考に作成しました。

## 構築した環境で動作するアプリ

実際に構築したアプリは、[GitHubのリポジトリ](https://github.com/artail2020/association_tutorial)に作成してあります。
以下の記事を参考に作りました。__アプリが動く環境構築の方がメイン__なので、元記事の内容から手を加えた部分はさほど多くありません。
S3の活用＝静的ファイル(画像)をアップロードする機能を追加した程度、でしょうか。

- [【初心者向け】丁寧すぎるRails『アソシエーション』チュートリアル【幾ら何でも】【完璧にわかる】🎸 - Qiita](https://qiita.com/kazukimatsumoto/items/14bdff681ec5ddac26d1)
- [railsで画像アップロードの方法 - Qiita](https://qiita.com/uchida0331/items/f99aba46fd1a6df9e753)

## 作業環境・バージョン

- macOS Big Sur 11.3
- AWS-CLI 2.1.38

# CloudFormationテンプレートの内容

## 入力パラメータの設定

```yaml:new.template
AWSTemplateFormatVersion: 2010-09-09

### … 略
Parameters:
  StackEnv:
    Description: Stack Environment
    Type: String
  CidrBlock:
    Description: IP range
    Type: String
    Default: 10.0.0.0/16
  EC2ImageId:
    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
    Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2
  KeyName:
    Description: The EC2 Key Pair to allow SSH access to the instance
    Type: AWS::EC2::KeyPair::KeyName
  MyIP:
    Description: IP address allowed to access EC2
    Type: String
  InstanceType:
    Description: EC2 Instance Type
    Type: String
    Default: t2.micro
  DBUser:
    Description: RDS master user name
    Type: String
  DBPassword:
    Description: RDS master user password
    Type: String
    NoEcho: true
  DBInstanceClass:
    Description: DB Instance Class
    Type: String
    Default: db.t2.micro
  DBCharSetCode:
    Description: DB Character Set Code
    Type: String
    Default: utf8mb4
```

ポイントとしては、

- リソース名にスタック名：` AWS::StackName `とともに作成した環境を明記するため、` StackEnv `で環境（本番：prod、開発：devなど）をパラメータとして保持
- 複数のリソースに割り当てることになる` CidrBlock `や` InstanceType `、` DBCharSetCode `はDefaultで値を設定して各所で使う。念のため、参照が一回のみだが` DBInstanceClass `も。
- テンプレートにべた書きはよろしくない認証情報（キーペア名：` KeyName `、DBユーザー名・パスワード：` DBUser `,` DBPassword `）や、ローカル環境のグローバルIP：` MyIP `はパラメータでのみ指定。
    - パスワードは` NoEcho: true `で表示をマスクしておく。パラメータストアなりSecrets Managerなりを使う方がより適切ですが、一旦動かせることの確認を優先で。
- EC2のイメージID：` EC2ImageId `は、特にこだわりがなければ[AWS公式ブログ](https://aws.amazon.com/jp/blogs/news/query-for-the-latest-amazon-linux-ami-ids-using-aws-systems-manager-parameter-store/)の例どおりに、パラメータストアを使用して最新のAMIを指定する。
- AWS固有のパラメータタイプがある場合はなるべく使う。エディタにLinterが入ってると、Stringから置き換えられる箇所にWarningを出してくれるので、活用したいところですね。

これに加えて、[俺的CloudFormation テンプレートの便利な定義 3選 - サーバーワークスエンジニアブログ](https://blog.serverworks.co.jp/tech/2019/02/28/cfn-3tips/)を元に、Metadataに` AWS::CloudFormation::Interface `の` ParameterGroups `を定義して、ラベル付きのグループ分けをしておくと分かりやすいでしょう。

```yaml:new.template
AWSTemplateFormatVersion: 2010-09-09
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      -
        Label:
          Default: VPC config
        Parameters:
          - StackEnv
          - CidrBlock
      -
        Label:
          Default: EC2 config
        Parameters:
          - EC2ImageId
          - KeyName
          - MyIP
          - InstanceType
      -
        Label:
          Default: RDS Config
        Parameters:
          - DBUser
          - DBPassword
          - DBInstanceClass
          - DBCharSetCode
Parameters:
### … 略
```

といっても、このグループ分けが影響するのはWebコンソール画面上のグルーピングや順序付けとなります。
コマンドラインでスタック作成する分には、Metadataより都度コメントを差し込む方が手間ではないかもしれません。

## リソース定義

### VPC・サブネット

```yaml:new.template
AWSTemplateFormatVersion: 2010-09-09
Metadata:
### … 略
Parameters:
### … 略
Resources:
  # VPC
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref CidrBlock
      EnableDnsHostnames: true
      EnableDnsSupport: true
      InstanceTenancy: default
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-${StackEnv}-vpc
        - Key: Env
          Value: !Ref StackEnv
  # InternetGateway
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-${StackEnv}-igw
        - Key: Env
          Value: !Ref StackEnv
  # RouteTable
  PublicRouteTableA:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-${StackEnv}-public-rtb00
        - Key: Env
          Value: !Ref StackEnv
  PublicRouteTableC:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-${StackEnv}-public-rtb01
        - Key: Env
          Value: !Ref StackEnv
  PrivateRouteTableA:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-${StackEnv}-private-rtb00
        - Key: Env
          Value: !Ref StackEnv
      VpcId: !Ref VPC
  PrivateRouteTableC:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-${StackEnv}-private-rtb01
        - Key: Env
          Value: !Ref StackEnv
      VpcId: !Ref VPC
  # GatewayAttachment
  GatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway
  # Route
  RouteA:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      RouteTableId: !Ref PublicRouteTableA
      GatewayId: !Ref InternetGateway
    DependsOn: GatewayAttachment
  RouteC:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      RouteTableId: !Ref PublicRouteTableC
      GatewayId: !Ref InternetGateway
    DependsOn: GatewayAttachment
  # Subnet
  PublicSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select
        - 0
        - Fn::GetAZs: !Ref AWS::Region
      CidrBlock: !Select [ 0, !Cidr [ !GetAtt VPC.CidrBlock, 1, 8 ]]
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-${StackEnv}-public-subnet00
        - Key: Env
          Value: !Ref StackEnv
  PublicSubnetC:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select
        - 1
        - Fn::GetAZs: !Ref AWS::Region
      CidrBlock: !Select [ 1, !Cidr [ !GetAtt VPC.CidrBlock, 2, 8 ]]
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-${StackEnv}-public-subnet01
        - Key: Env
          Value: !Ref StackEnv
  PrivateSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Select
        - 0
        - Fn::GetAZs: !Ref AWS::Region
      CidrBlock: !Select [ 2, !Cidr [ !GetAtt VPC.CidrBlock, 3, 8 ]]
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-${StackEnv}-private-subnet00
        - Key: Env
          Value: !Ref StackEnv
      VpcId: !Ref VPC
  PrivateSubnetC:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Select
        - 1
        - Fn::GetAZs: !Ref AWS::Region
      CidrBlock: !Select [ 3, !Cidr [ !GetAtt VPC.CidrBlock, 4, 8 ]]
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-${StackEnv}-private-subnet01
        - Key: Env
          Value: !Ref StackEnv
      VpcId: !Ref VPC
  # SubnetRouteTableAssociation
  SubnetRouteTableAssociation1a:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnetA
      RouteTableId: !Ref PublicRouteTableA
  SubnetRouteTableAssociation1c:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnetC
      RouteTableId: !Ref PublicRouteTableC
  SubnetRouteTableAssociation1aPrivate:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTableA
      SubnetId: !Ref PrivateSubnetA
  SubnetRouteTableAssociation1cPrivate:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTableC
      SubnetId: !Ref PrivateSubnetC
```

以降のリソースも含めて、私の場合は動作確認のためにWebコンソール上で作成した環境と[AWS resource and property types reference - AWS CloudFormation](https://docs.aws.amazon.com/en_us/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html)のリソースタイプごとの記載を照らし合わせて、自作環境の再現を目標にして記述していきました。

- VPC：` AWS::EC2::VPC `
- 付随するインターネットゲートウェイ：` AWS::EC2::InternetGateway `・ルートテーブル：` AWS::EC2::RouteTable `
- AZごとにサブネット` AWS::EC2::Subnet `をパブリック・プライベートで一種ずつ
    - アベイラビリティゾーンは、` Fn::GetAZs `・` AWS::Region `で[利用できるAZを取得](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getavailabilityzones.html)して指定する
- リソース間の繋ぎこみを行う
    - VPC・インターネットゲートウェイを` AWS::EC2::VPCGatewayAttachment `
    - インターネットゲートウェイ・ルートテーブル（パブリックサブネット用）を` AWS::EC2::Route `
        - ` DependsOn: GatewayAttachment `で、` VPCGatewayAttachment `およびその前に作られるインターネットゲートウェイよりも、` Route `の作成を後にすることが可能。一度作成したスタックに内容を修正したテンプレートを適用する際に、変更前のリソースを参照してしまうことによるエラーを防ぐことができます
    - サブネット・ルートテーブルを` AWS::EC2::SubnetRouteTableAssociation `

VPC以外も含めて、` Tags `が設定できるリソースについては、`Name`タグに` AWS::StackName `と` StackEnv `、リソースの性質やタイプを` ！Sub `で連結して指定します。`Env`タグはまんま` StackEnv `ですね。
このあたりは、[弊社で使っているAWSリソースの命名規則を紹介します | DevelopersIO](https://dev.classmethod.jp/articles/aws-name-rule/)の規則をそのまま使わせていただいた感じになります。

各種プロパティに関しては、前述の通り作成済みの環境に基づいて設定していきます。
その中でも` CidrBlock `は、VPCではパラメータのCidrBlockをそのまま指定し、各サブネットには[[小ネタ]「!Cidr」というチョット便利なCloudFormationの組み込み関数 | DevelopersIO](https://dev.classmethod.jp/articles/cidr_cloudformation/)の要領で自動生成してしまうのが良いかと。


### EC2・起動テンプレート

```yaml:new.template
  # EC2
  EC2A:
    Type: AWS::EC2::Instance
    Properties:
      AvailabilityZone: !Select
        - 0
        - Fn::GetAZs: !Ref AWS::Region
      ImageId: !Ref EC2ImageId
      KeyName: !Ref KeyName
      InstanceType: !Ref InstanceType
      NetworkInterfaces:
        - AssociatePublicIpAddress: true
          DeviceIndex: "0"
          SubnetId: !Ref PublicSubnetA
          GroupSet:
            - !Ref SecurityGroupEC2
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-${StackEnv}-app00
        - Key: Env
          Value: !Ref StackEnv
      IamInstanceProfile: !Ref InstanceProfileEC2
  EC2C:
    Type: AWS::EC2::Instance
    Properties:
      AvailabilityZone: !Select
        - 1
        - Fn::GetAZs: !Ref AWS::Region
      ImageId: !Ref EC2ImageId
      KeyName: !Ref KeyName
      InstanceType: !Ref InstanceType
      NetworkInterfaces:
        - AssociatePublicIpAddress: true
          DeviceIndex: "0"
          SubnetId: !Ref PublicSubnetC
          GroupSet:
            - !Ref SecurityGroupEC2
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-${StackEnv}-app01
        - Key: Env
          Value: !Ref StackEnv
      IamInstanceProfile: !Ref InstanceProfileEC2
```

EC2に関しても、原則作成・動作確認済みの環境に基づいた内容を設定します。

` ImageId `、` KeyName `、` InstanceType `はパラメータを参照。今回は2台ともに同じ値を設定するので、まとめてパラメータで管理しております。
` NetworkInterfaces - GroupSet `はセキュリティグループ、` IamInstanceProfile `はS3アクセス用のIAMロールを当てたインスタンスプロファイルを設定するので、別途設定したリソースの論理IDを` !Ref `で渡しておきましょう。

必要であれば、` AWS::EC2::Instance `の` UserData `プロパティや、` AWS::EC2::LaunchTemplate `で作成した起動テンプレートに、EC2起動時に環境構築するコマンドなどを登録・実行させることもできます。
（[CloudFormationの中のEC2のユーザーデータでシェル変数を使用する | DevelopersIO](https://dev.classmethod.jp/articles/using-variables-in-ec2-user-data-in-cloudformation/)などを参考にするとよいと思います）

ユーザーデータに関しては、このテンプレートでも当初は使っていましたが、

- ` yum update `を実行していたが、Ansibleでyum-cronによる自動更新を設定したので必要なくなった
- Python3をインストールしてAnsibleを実行させようとしたものの、Amazon Linux2・yumの環境ではPython2でないと動かしづらいため断念

……と、前もって振っておいた作業の必要性がなくなり、さらに環境構築を全面的にAnsibleに任せたこともあって削除しました。


### セキュリティグループ

```yaml:new.template
  # SecurityGroup
  SecurityGroupEC2:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: EC2 Security Group
      SecurityGroupIngress:
        - SourceSecurityGroupId: !Ref SecurityGroupELB
          FromPort: 80
          ToPort: 80
          IpProtocol: tcp
        - CidrIp: !Ref MyIP
          FromPort: 22
          ToPort: 22
          IpProtocol: tcp
      SecurityGroupEgress:
        - CidrIp: 0.0.0.0/0
          IpProtocol: "-1"
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-${StackEnv}-app-sg
        - Key: Env
          Value: !Ref StackEnv
      VpcId: !Ref VPC
  SecurityGroupELB:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: ELB Security Group
      SecurityGroupIngress:
        - CidrIp: 0.0.0.0/0
          FromPort: 80
          ToPort: 80
          IpProtocol: tcp
        - CidrIp: 0.0.0.0/0
          FromPort: 443
          ToPort: 443
          IpProtocol: tcp
      SecurityGroupEgress:
        - CidrIp: 0.0.0.0/0
          IpProtocol: "-1"
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-${StackEnv}-elb-sg
        - Key: Env
          Value: !Ref StackEnv
      VpcId: !Ref VPC
  SecurityGroupRDS:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: RDS Security Group
      SecurityGroupIngress:
        - SourceSecurityGroupId: !Ref SecurityGroupEC2
          FromPort: 3306
          ToPort: 3306
          IpProtocol: tcp
      SecurityGroupEgress:
        - DestinationSecurityGroupId: !Ref SecurityGroupEC2
          FromPort: 3306
          ToPort: 3306
          IpProtocol: tcp
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-${StackEnv}-rds-sg
        - Key: Env
          Value: !Ref StackEnv
      VpcId: !Ref VPC
```

EC2・ELB・RDSにそれぞれセキュリティグループを設定、` SecurityGroupIngress `でインバウンドルールを、` SecurityGroupEgress `でアウトバウンドルールを指定していきます。
インバウンド元/アウトバウンド先には、` CidrIp `でIPアドレスを指定するだけではなく、` SourceSecurityGroupId / DestinationSecurityGroupId `で別のセキュリティグループを指定するとかなり楽に書けるでしょう。

EC2のインバウンドで使用するポート番号については、ELBのリソースの` Port `で指定している"80"（HTTP）と、パラメータの` MyIP `で設定しているローカル環境から接続する際の"22"（SSH）となります。
（この後でAnsibleを動作させる際に、別途AWS環境内にAnsible稼働用のサーバーを作った方がよいところを、ローカルPCでAnsibleを走らせてSSH接続したEC2の環境構築を行うようにしているので、SSHも必須としてます）

また、ELBのインバウンドにポート番号443（HTTPS）も指定していますが、別途SSL接続の設定をできているわけではないので、今のところ役立てられてはいません。
いずれ活用したいところです。

### ELB・ターゲットグループ・リスナー

```yaml:new.template
  # LoadBalancer
  ElasticLoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      IpAddressType: ipv4
      LoadBalancerAttributes:
        - Key: access_logs.s3.enabled
          Value: "false"
        - Key: deletion_protection.enabled
          Value: "false"
        - Key: idle_timeout.timeout_seconds
          Value: "60"
        - Key: routing.http.desync_mitigation_mode
          Value: "defensive"
        - Key: routing.http.drop_invalid_header_fields.enabled
          Value: "false"
        - Key: routing.http2.enabled
          Value: "true"
      Name: !Sub ${AWS::StackName}-${StackEnv}-elb
      Scheme: internet-facing
      SecurityGroups:
        - !Ref SecurityGroupELB
      Subnets:
        - !Ref PublicSubnetA
        - !Ref PublicSubnetC
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-${StackEnv}-elb
        - Key: Env
          Value: !Ref StackEnv
      Type: application
  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckIntervalSeconds: 30
      HealthCheckPath: /
      HealthCheckPort: traffic-port
      HealthCheckProtocol: HTTP
      HealthCheckTimeoutSeconds: 5
      HealthyThresholdCount: 5
      Matcher:
        HttpCode: "200"
      Name: !Sub ${AWS::StackName}-${StackEnv}-tg
      Port: 80
      Protocol: HTTP
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-${StackEnv}-tg
        - Key: Env
          Value: !Ref StackEnv
      TargetGroupAttributes:
        - Key: deregistration_delay.timeout_seconds
          Value: "300"
        - Key: stickiness.enabled
          Value: "false"
        - Key: load_balancing.algorithm.type
          Value: "round_robin"
        - Key: slow_start.duration_seconds
          Value: "0"
      Targets:
        - Id: !Ref EC2A
          Port: 80
        - Id: !Ref EC2C
          Port: 80
      TargetType: instance
      UnhealthyThresholdCount: 2
      VpcId: !Ref VPC
  Listener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - TargetGroupArn: !Ref TargetGroup
          Type: forward
      LoadBalancerArn: !Ref ElasticLoadBalancer
      Port: 80
      Protocol: HTTP
```

今回作成するのはALBなので、` AWS::ElasticLoadBalancingV2::LoadBalancer `、` AWS::ElasticLoadBalancingV2::TargetGroup `、` AWS::ElasticLoadBalancingV2::Listener `を使って正常なプロパティを指定していきます。
ロードバランサーに限りませんが、事前に手作業で動作する環境を作ってイメージを持っておくのが近道でしょうか。いきなりテンプレートからリソースを作成、というのは初心者には厳しいかと。

前述のとおりSSLの設定はしていないので、あくまでHTTPのみでの接続となります。

### RDB・パラメータグループ・サブネットグループ

```yaml:new.template
  # RDS
  RdsMysql:
    Type: AWS::RDS::DBInstance
    Properties:
      AllocatedStorage: "20"
      AllowMajorVersionUpgrade: true
      AutoMinorVersionUpgrade: true
      AvailabilityZone: !Select
        - 0
        - Fn::GetAZs: !Ref AWS::Region
      BackupRetentionPeriod: 7
      CopyTagsToSnapshot: true
      DBInstanceClass: !Ref DBInstanceClass
      DBInstanceIdentifier: !Sub ${AWS::StackName}-${StackEnv}-rds
      DBParameterGroupName: !Ref DBParameterGroup
      DBSubnetGroupName: !Ref DBSubnetGroup
      DeletionProtection: false
      EnablePerformanceInsights: false
      Engine: mysql
      EngineVersion: 8.0.20
      LicenseModel: general-public-license
      MasterUserPassword: !Ref DBPassword
      MasterUsername: !Ref DBUser
      MaxAllocatedStorage: 1000
      MonitoringInterval: 0
      MultiAZ: false
      Port: "3306"
      PreferredBackupWindow: 18:00-18:30
      PreferredMaintenanceWindow: mon:03:00-mon:03:30
      PubliclyAccessible: false
      StorageEncrypted: false
      StorageType: gp2
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-${StackEnv}-rds
        - Key: Env
          Value: !Ref StackEnv
      VPCSecurityGroups:
        - !Ref SecurityGroupRDS
  # DBParameterGroup
  DBParameterGroup:
    Type: AWS::RDS::DBParameterGroup
    Properties:
      Description: sample parameter cloudformation
      Family: mysql8.0
      Parameters:
        character_set_client: !Ref DBCharSetCode
        character_set_connection: !Ref DBCharSetCode
        character_set_database: !Ref DBCharSetCode
        character_set_results: !Ref DBCharSetCode
        character_set_server: !Ref DBCharSetCode
        collation_server: utf8mb4_general_ci
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-${StackEnv}-rds-pg
        - Key: Env
          Value: !Ref StackEnv
  # DBSubnetGroup
  DBSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupDescription: subnetgroup created by cloudformation
      DBSubnetGroupName: !Sub ${AWS::StackName}-${StackEnv}-rds-subnetg
      SubnetIds:
        - !Ref PrivateSubnetA
        - !Ref PrivateSubnetC
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-${StackEnv}-rds-subnetg
        - Key: Env
          Value: !Ref StackEnv
```

かなり長い内容ではありますが、各プロパティが必須であるかないかをあまり重要視せず、作成済みの環境の設定内容をそのままテンプレートに適用した形となっております。
利用額は低く収めておきたいので、マルチAZなどは設定せず。

今回のエンジンはMySQL、バージョンは8.0.20を使用しました。
` AWS::RDS::DBInstance - EngineVersion `はもちろん、` AWS::RDS::DBParameterGroup - Family `にも同じバージョンを指定しておきましょう。
初期パラメータについては、character関連にパラメータ` DBCharSetCode `を参照したのと、` collation_server `に[ひらがなカタカナを判別する日本語用Collation](https://mysqlserverteam.com/mysql-8-0-kana-sensitive-collation-for-japanese-ja_jp/)である` utf8mb4_ja_0900_as_cs_ks `を設定……したものの、AWSのパラメータグループはバージョン8.0止まりのためか、8.0.1で導入されたCollationはサポートされておらず。
やむなく` utf8mb4_general_ci `に変更しました。

` AWS::RDS::DBSubnetGroup `でRDSのサブネットグループを作成するのですが、グループには必ず2つ以上のAZが含まれていなければいけません。
ここで、あらかじめ作成したAZ-AとAZ-Cのプライベートサブネットを参照しておきます。

### S3・アクセス用IAMロール

```yaml:new.template
  # S3 Bucket
  S3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      AccelerateConfiguration:
        AccelerationStatus: Suspended
      AccessControl: Private
      BucketName: !Sub ${AWS::StackName}-${StackEnv}-contents-${AWS::AccountId}
      ObjectLockEnabled: false
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-${StackEnv}-contents-${AWS::AccountId}
        - Key: Env
          Value: !Ref StackEnv
      VersioningConfiguration:
        Status: Suspended
```

今回作成するS3については、原則EC2上のアプリケーションで取り扱う静的ファイルを保存するのみなので、基本プライベートでパブリックアクセスもブロックする設定にしておきます。

気をつけるところがあるとすれば、` BucketName `プロパティの付け方次第ではバケット名の制約に引っかかってしまう点でしょうか。
今回は` AWS::AccountId `でアカウントIDを追記しておき、他ユーザーのバケット名とバッティングする可能性を低くしておきました。
一応必須のプロパティではないので、テンプレート上で命名せずに` Outputs `でバケット名を出力して確認できるようにしておくのもよいかもしれません。

そして、EC2からS3へのアクセスが行えるよう、IAMロールとインスタンスプロファイルも以下で作成しておきます。

```yaml:new.template
  # IAM Role
  RoleEC2:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action:
              - sts:AssumeRole
      Description: Allows EC2 instances to call AWS services on your behalf.
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonS3FullAccess
      MaxSessionDuration: 3600
      Path: /
      RoleName: !Sub ${AWS::StackName}-${StackEnv}-app-role
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-${StackEnv}-app-role
        - Key: Env
          Value: !Ref StackEnv
  # IAM InstanceProfile
  InstanceProfileEC2:
    Type: AWS::IAM::InstanceProfile
    Properties:
      InstanceProfileName: !Sub ${AWS::StackName}-${StackEnv}-app-ip
      Path: /
      Roles:
        - !Ref RoleEC2
```

公式ドキュメントで[CloudFormation で IAM マネージドポリシーを IAM ロールにアタッチする方法](https://aws.amazon.com/jp/premiumsupport/knowledge-center/cloudformation-attach-managed-policy/)が解説されているので、同様に` AWS::IAM::Role `でロールを作成。
セキュリティ上のベストは、テンプレート中で作成したS3バケットのみへのアクセスを許可したポリシーを` AWS::IAM::ManagedPolicy `で設定するべきですが、今回は手っ取り早くAWS管理ポリシー（` AmazonS3FullAccess `）を使用。

作成したロールは` AWS::IAM::InstanceProfile `で作成したインスタンスプロファイルへ割り当てて、そのプロファイルをEC2の` IamInstanceProfile `プロパティで参照すれば、アクセス許可の付与は完了します。

```yaml:new.template
  # EC2
  EC2A:
    Type: AWS::EC2::Instance
    Properties:
### … 略
      IamInstanceProfile: !Ref InstanceProfileEC2
  EC2C:
    Type: AWS::EC2::Instance
    Properties:
### … 略
      IamInstanceProfile: !Ref InstanceProfileEC2
```

※ この他にも` Outputs `セクションで、作成したリソースの値などを出力してコンソールやコマンドラインで受け取れるように設定できますが、今回は使用していません。

# コマンドラインでスタック作成

[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/install-cliv2.html)の使っているOSのページを元に、AWS CLIバージョン2をインストール。
` aws cloudformation create-stack `を使ってもいいのですが、` aws cloudformation deploy `であれば、スタックの新規作成も既存スタック更新もできるので、そちらで問題ないでしょう。
Jenkinsなどで継続的に実行するつもりなら、最初から` aws cloudformation deploy `でジョブを登録した方が楽かと。

```shell:
aws cloudformation deploy --template-file テンプレート名 \
--stack-name スタック名 --capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides KeyName=キーペア名 MyIP=ローカル環境のパブリックIP \
DBUser=DBユーザー名 DBPassword=DBパスワード StackEnv=環境(prod/stg/devなど)
```

今回のテンプレートではIAMロールを作成するので、` --capabilities `オプションで` CAPABILITY_NAMED_IAM `を指定しないとエラーとなります。

# 最後に

RaiseTechのメンターの方からのアドバイスもあって、やってきたことを記事として残してみました。
結果、改めて自分の起こしたコードを念入りに読み返すことで、「もっといいやり方があるのではないか？」といった気づきができたのはプラスではあるな、と思いました。
（第三者が読んでも、読みやすい文章になっている保証は全然できませんが……）

~~そして、この記事ではWebアプリケーションを動かすサーバー構成の構築にとどまっているので、以降のAnsibleでのデプロイについては別途記事を書こうかと思います。~~
書きません。