AWS CloudFormationのテンプレートをLayer別に作成してクロススタック参照を作ってみた。その3  [ Application-Layer編 ]

## 前回のまでのおさらい
前回のSeculity-Layer編では通信のポートを制御するセキュリティグループを作成した。最後となる今回はEC2など主要な構成をテンプレートで構築していく。
- [その1 [ Network-Layer編 ]](https://qiita.com/ezakiglico0215/items/262c93ef781ac98ca5b9)
- [その2 [ Security-Layer編 ]](https://qiita.com/ezakiglico0215/items/272cf7bae52775e6a9f9)



## 概要
テンプレートと言われる構築環境をコード化させたファイルに記載し、スタックと呼ばれる作業を行うことでGUI上で作成した状況と同じ環境が自動的に作成される。1つのテンプレートに全ての環境を記載することも可能だが、AWSからは各レイヤー別に分けることがベストプラクティスと言われている。

:::note 
今回は以下3つのLayerに分けて作成する。
- Network Layer　　
- Security Layer
- **Application Layer  　　←今回はここ!**
:::

## クロススタック参照
各レイヤー別にテンプレートを作成し1つの環境構築を行うためには、テンプレート間の互換性が必要になる。そのためには`クロススタック参照`を使用して他レイヤーの値を参照できるようにコードに'組込関数'と呼ばれる関数を活用していく作業がポイント。

### 構成図
ここまで進めて完成となる。
![Application.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2601549/7dfaa373-4183-53ca-7411-ed1ceb090fb7.png)



## Application-Layer
:::note 
このレイヤーでEC2、RDS、ELBのテンプレートを作成していく。
VPC関連を作成した時と同様に、Parametersで引用する数値を予め設定しておく
:::
:::note warn
2022年5月に新機能として、CloudFormationでkeypairを作成できるようになった。
今回は旧式のやり方で進めているが、自身でも準備が出来たら投稿予定！
参考記事 : [CloudFormationでキーペアの作成/削除が可能になった
](https://blog.serverworks.co.jp/update-ec2-keypair-20220429#CloudFormation%E3%81%A7%E3%82%AD%E3%83%BC%E3%83%9A%E3%82%A2%E3%81%AE%E4%BD%9C%E6%88%90%E5%89%8A%E9%99%A4%E3%81%8C%E5%8F%AF%E8%83%BD%E3%81%AB%E3%81%AA%E3%81%A3%E3%81%9F)
:::

### Parameters
```yaml:Application-Layer.yaml
AWSTemplateFormatVersion: 2010-09-09
Description: Creation of EC2 Related(for EC2,RDS,ALB)

  
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Live03 Prefix
        Parameters:
          - PJPrefix
      - Label:
          default: EC2 Configuration
        Parameters:
          - KeyName
          - EC2InstanceName
          - EC2InstanceAMI
          - EC2InstanceType
          - EC2InstanceVolumeType
          - EC2InstanceVolumeSize
      - Label:
          default: RDS Configuration
        Parameters:
          - DBInstanceName          #スタックの名前
          - MySQLMajorVersion       #MysqlのメジャーVer
          - MySQLMinorVersion       #MysqlのマイナーVer
          - DBInstanceClass         #インスタンスクラス
          - DBInstanceStorageSize   #ストレージサイズ
          - DBInstanceStorageType   #ストレージタイプ
          - DBName                  #データベースの名前
          - DBMasterUserName        #マスターユーザー名
          - DBPassword              #パスワード
      - Label:
          default: InternetALB Configuration
        Parameters:
          - InternetALBName

    ParameterLabels:
    # for EC2
      InternetALBName:
        default: InternetALBName
      KeyName:
        default: KeyPairName
      EC2InstanceName:
        default: EC2 Name
      EC2InstanceAMI:
        default: EC2 AMI
      EC2InstanceType:
        default: EC2 InstanceType
      EC2InstanceVolumeType:
        default: EC2 VolumeType
      EC2InstanceVolumeSize:
        default: EC2 VolumeSize

    # for RDS
      DBInstanceName:
        default: DBInstanceName
      MySQLMajorVersion:
        default: MySQLMajorVersion
      MySQLMinorVersion:
        default: MySQLMinorVersion
      DBInstanceClass:
        default: DBInstanceClass
      DBInstanceStorageSize:
        default: DBInstanceStorageSize
      DBInstanceStorageType:
        default: DBInstanceStorageType
      DBName:
        default: DBName
      DBMasterUserName:
        default: DBUserName
      DBPassword:
        default: DBPassword

#---------------------------------------------------
# input Parameters
#---------------------------------------------------   
Parameters:
  PJPrefix:
    Type: String
    Default: live03

# EC2
  KeyName:
    Description: input EC2 Keyname
    Type: AWS::EC2::KeyPair::KeyName
    Default: live03-keypair

  EC2InstanceName:
    Type: String
    Default: web

  EC2InstanceAMI:
    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
    Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2

  EC2InstanceType:
    Type: String
    Default: t2.micro

  EC2InstanceVolumeType:
    Type: String
    Default: gp2
  
  EC2InstanceVolumeSize:
    Type: String
    Default: "30"

# RDS
  DBInstanceName:
    Type: String
    Default: RDS

  MySQLMajorVersion:
    Type: String
    Default: "5.7"
    AllowedValues: [ "5.5", "5.6", "5.7" ]

  MySQLMinorVersion:
    Type: String
    Default: "22"

  DBInstanceClass:
    Type: String
    Default: db.t3.micro

  DBInstanceStorageSize:
    Type: String
    Default: "20"

  DBInstanceStorageType:
    Type: String
    Default: gp2

  DBName:
    Type: String
    Default: db

  DBMasterUserName:
    Type: String
    Default: dbuser  # 自身の設定した値を挿入
    NoEcho: true
    MinLength: 1
    MaxLength: 16
    AllowedPattern: "[a-zA-Z][a-zA-Z0-9]*"
    ConstraintDescription: must begin with a letter and contain only alphanumeric characters.

  DBPassword: 
    Default: dbpassword  # 自身の設定した値を挿入
    NoEcho: true
    Type: String
    MinLength: 8
    MaxLength: 41
    AllowedPattern: "[a-zA-Z0-9]*"
    ConstraintDescription: must contain only alphanumeric characters.

# ALB
  InternetALBName:
    Type: String
    Default: Cfn

# S3
  S3BucketName:
    Type: String
    Default: cfn202705

Resources:
# ------------------------------------------------------------#
#  IAM Role for EC2
# ------------------------------------------------------------# 
  EC2IAMRole: 
    Type: "AWS::IAM::Role"
    Properties: 
      RoleName: !Sub "${PJPrefix}-${EC2InstanceName}-role" 
      AssumeRolePolicyDocument: 
        Version: "2012-10-17"
        Statement: 
          - Effect: Allow
            Principal: 
              Service: 
                - "ec2.amazonaws.com"
            Action: 
              - "sts:AssumeRole"
      Path: "/"
      ManagedPolicyArns: 
        - "arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM"
        - "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess"

  EC2InstanceProfile: 
    Type: "AWS::IAM::InstanceProfile"
    Properties: 
      Path: "/"
      Roles: 
        - Ref: EC2IAMRole
      InstanceProfileName: !Sub "${PJPrefix}-${EC2InstanceName}-profile"


```

### EC2
:::note
ALBを使用しELB経由で負荷分散させる仕様にするためEC2は2台(1a /1c)作成する。
各IDはParametersから組込関数を用いて引用。
:::

```yaml:Application-Layer.yaml
Resources:
#---------------------------------------------------
# EC2の作成(public-subnet内)
#---------------------------------------------------
  CfnEC21a:
    Type: AWS::EC2::Instance
    Properties:
      SubnetId: !ImportValue nstack-PublicSubnet1a
      ImageId: !Ref EC2InstanceAMI
      InstanceType: !Ref EC2InstanceType
      KeyName: !Ref KeyName
      InstanceInitiatedShutdownBehavior: stop
      Tenancy: default
      BlockDeviceMappings: 
        - DeviceName: /dev/xvda
          Ebs:
            VolumeType: !Ref EC2InstanceVolumeType
            VolumeSize: !Ref EC2InstanceVolumeSize
      SecurityGroupIds:
        - !ImportValue sstack-Sg-WEB
      Tags:
        - Key: Name
          Value: !Sub "${PJPrefix}-${EC2InstanceName}-1a"
  
  CfnEC21c:
    Type: AWS::EC2::Instance
    Properties:
      SubnetId: !ImportValue nstack-PublicSubnet1c
      ImageId: !Ref EC2InstanceAMI
      InstanceType: !Ref EC2InstanceType
      KeyName: !Ref KeyName
      InstanceInitiatedShutdownBehavior: stop
      Tenancy: default
      BlockDeviceMappings: 
        - DeviceName: /dev/xvda
          Ebs:
            VolumeType: !Ref EC2InstanceVolumeType
            VolumeSize: !Ref EC2InstanceVolumeSize
      SecurityGroupIds:
        - !ImportValue sstack-Sg-WEB
      Tags:
        - Key: Name
          Value: !Sub "${PJPrefix}-${EC2InstanceName}-1c"
```

### RDS
AZ-1aとAZ-1cに設置しMulti-AZ構成とする。

```yaml:Application-Layer.yaml
  CfnRDS: 
    Type: "AWS::RDS::DBInstance"
    Properties:       
      Engine: MySQL
      EngineVersion: !Sub "${MySQLMajorVersion}.${MySQLMinorVersion}"
      Port: 3306
      DBInstanceClass: !Ref DBInstanceClass
      AllocatedStorage: !Ref DBInstanceStorageSize
      StorageType: !Ref DBInstanceStorageType
      DBName: !Ref DBName
      MasterUsername: !Ref DBMasterUserName
      MasterUserPassword: !Ref DBPassword
      DBSubnetGroupName: !Ref DBSubnetGroup
      PubliclyAccessible: false
      PreferredBackupWindow: "18:00-18:30"
      PreferredMaintenanceWindow: "sat:19:00-sat:19:30"
      AutoMinorVersionUpgrade: false
      VPCSecurityGroups:
        - !ImportValue (スタック名)-Sg-RDS
      CopyTagsToSnapshot: true
      BackupRetentionPeriod: 7
      Tags: 
        - Key: "Name"
          Value: !Ref DBInstanceName
    DeletionPolicy: "Delete"
    
  # RDS SubnetGroupの作成
  DBSubnetGroup: 
    Type: "AWS::RDS::DBSubnetGroup"
    Properties: 
      DBSubnetGroupName: DBSubnetGroupName
      DBSubnetGroupDescription: create for RDS
      SubnetIds: 
        - !ImportValue (スタック名）-PrivateSubnet1a
        - !ImportValue (スタック名）-PrivateSubnet1c
```

### ALB
SubnetはPublic1a/1cに設定し、最終的にNginx経由でロードバランサーに接続するので80番ポートを指定。

```yaml:Application-Layer.yaml
  CfnALB:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      IpAddressType: ipv4
      Name: CfnALB
      Scheme: internet-facing
      SecurityGroups:
        - !ImportValue (スタック名)-Sg-ALB
      Subnets:
        - !ImportValue (スタック名)-PublicSubnet1a
        - !ImportValue (スタック名)-PublicSubnet1c
      Tags:
        - Key: Name
          Value: CfnALB

  # ALBのターゲットグループの指定
  ALBTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: ALBTargetGroup
      Port: 80
      Protocol: HTTP
      Targets:
        - Id:
            Ref: CfnEC21a
          Port: 80
        - Id:
            Ref: CfnEC21c
          Port: 80
      VpcId: !ImportValue (スタック名)-VPCID

  # ALBのリスナーの指定
  ALBListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - Type: forward
          TargetGroupArn:
            !Ref ALBTargetGroup
      LoadBalancerArn:
        !Ref CfnALB
      Port: 80
      Protocol: HTTP
```
### S3
`PublicAccessBlockConfiguration`の設定で全てtrueにするとパブリックアクセスが全てブロックされる。

```yaml:Application-Layer.yaml
  SampleS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref S3BucketName
      AccessControl: Private
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True
      VersioningConfiguration:
        Status: Enabled
```
これで3つのレイヤーに分けて作成したテンプレートの作成が完了となる。

### 終わりに
:::note 
慣れるまでは、テンプレート同士の整合性の調整やエラー解消にかなり手間取ってしまった。
ここからAWS CLIを使用してターミナルから操作、完了できれば尚よしだが、今は一旦ここをゴールとする。
:::

## 参考にさせて頂いた記事
- [CloudFormationのテンプレートを分割して作成してみた。](https://qiita.com/Takao_/items/8384110f259b31abdc1b#ec2)
- [クロススタック参照でテンプレートを分割管理する](https://zenn.dev/minamiso/articles/7a71e625cdea90)
- [AWS テンプレートリファレンス](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/template-reference.html)
- [CloudFormationを使って最新のAmazonLinuxのAMIでEC2を構築する](https://qiita.com/okubot55/items/47bae61eacaf286f95b7)

## エラー事例
テンプレートを構築していく中で、多くのエラーに遭遇。
備忘録を兼ねて事象と解決方法をメモとして残しておく。

:::note alert
テンプレート作成を行う段階で遭遇したエラーと解決方法[主にcfn-lintで事前に検知]

[事象1] **cfn-lint  E1001: Top level template section VPCEndpoint is not valid**
[解決法] 作成したコードの深さの設定ミス：論理IDなどを設定するTOPの位置の深さに設定ミスがあり。

[事象2] **cfn-lint  E0000: Duplicate resource found "ParameterLabels"**
[解決法] コード内容が重複していると検出されるエラー。1つ削除して解消させる。

[事象3] **cfn-lint  E2015: Default should be allowed by AllowedPattern**
[解決法] インプットパラメーターで設定している値の表記ミス、正しく表記できていない。

[事象4] **cfn-lint  E1012: Ref CfnEC2 not found as a resource or parameter**
[解決法] インプットパラメーターで設定している値の表記ミス、正しく表記できていない。

[事象5] **Unresolved resource dependencies [ALBSecurityGroup] in the Outputs block of the template**
[解決法] !Ref関数で指定している引用元の論ID名に相違がある。

[事象6] **No export named sstack-PrivateSubnet1a found. Rollback requested by user**
[解決法] クロススタックで参照している値のtypoミス

[事象7] **Bucket name should not contain uppercase characters**
[解決法] Paraetersに記載しているS3のバケット名に大文字を使用していたため（大文字禁止）


:::