CloudFormationでDjango環境構築

#はじめに
以前投稿したAWSでのDjango環境構築をCloudFormationを活用して構築してみたいと思います。

https://qiita.com/zeems/items/7967f94e0049ed03eb40

#ポイント
* AWSマネジメントコンソールを使用せずに構築を実行
    * CloudFormation, AWS CLIを使用して構築します。
* Systems Managerのパラメータストアを使用
    * 一部パラメータをパラメータストアに格納して利用します。
* リソースの種類ごとにテンプレートを分割
    * 公式ドキュメントでベストプラクティスとして記載されているリソースのライフサイクルを考慮して、テンプレートを分割します。
    * クロススタック参照を使用します。
    * 分割したテンプレートを使用して、ネストされたスタックを作成します。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/best-practices.html#organizingstacks


#テンプレート構成
前述の通りテンプレートはリソースのライフサイクルを意識して分けてみました。
今回は以下のように分けています。

* root.yml：ルート(すべてのテンプレートのまとめ)
* network.yml：ネットワーク(VPC,サブネットなど)
* security.yml：セキュリティ(セキュリティグループ,IAMロールなど)
* bastion.yml：踏み台(踏み台用EC2インスタンス)
* database.yml：データベース(RDS,サブネットグループ)
* application.yml：アプリケーション(起動テンプレート,ロードバランサー,Auto Scaling Groupなど)

また、それぞれのテンプレートファイルは1つのS3バケットに配置し、スタック作成時にはroot.ymlのURIを指定して実行しています。
root.yml内でその他のテンプレートを参照し、ネストされたスタックとして作成します。

#テンプレート
実際のテンプレートの中身について以下に記載します。

#####ルート
```bash:root.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Root template for creating Django environment
Parameters: 
  TemplateNetwork:
    Description: Network layer template object URL
    Type: String
    Default: <network.ymlのURL>
  TemplateSecurity:
    Description: Security layer template object URL
    Type: String
    Default: <security.ymlのURL>
  TemplateBastion:
    Description: Bastion EC2 template object URL
    Type: String
    Default: <bastion.ymlのURL>
  TemplateDatabase:
    Description: RDS template object URL
    Type: String
    Default: <database.ymlのURL>
  TemplateApplication:
    Description: Application layer object URL
    Type: String
    Default: <application.ymlのURL>

Resources: 
  Network:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Ref TemplateNetwork
      Parameters:
        VPCCidrBlock: '10.0.0.0/21'
        VPCNameTag: 'django-vpc'
        IGWNameTag: 'django-igw'
        ELBSubnet1aCidr: '10.0.0.0/24'
        ELBSubnet1aNameTag: 'django-elb-subnet-1a'
        ELBSubnet1cCidr: '10.0.1.0/24'
        ELBSubnet1cNameTag: 'django-elb-subnet-1c'
        PublicRouteTableNameTag: 'django-public-rt'
        WebSubnet1aCidr: '10.0.2.0/24'
        WebSubnet1aNameTag: 'django-web-subnet-1a'
        WebSubnet1cCidr: '10.0.3.0/24'
        WebSubnet1cNameTag: 'django-web-subnet-1c'
        DBSubnet1aCidr: '10.0.4.0/24'
        DBSubnet1aNameTag: 'django-db-subnet-1a'
        DBSubnet1cCidr: '10.0.5.0/24'
        DBSubnet1cNameTag: 'django-db-subnet-1c'
        PrivateRouteTableNameTag: 'django-private-rt'
        NGWNameTag: 'django-ngw'
  Security:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Ref TemplateSecurity
      Parameters:
        ELBSGName: 'django-elb-sg'
        BastionSGName: 'django-bastion-sg'
        WebSGName: 'django-web-sg'
        RDSSGName: 'django-rds-sg'
        HTTPAccessIP: '0.0.0.0/0'
        SSHAccessIP: '0.0.0.0/0'
        IAMRoleName: 'django-s3-role'
    DependsOn: Network
  Bastion:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Ref TemplateBastion
      Parameters:
        EC2NameTag: 'django-bastion'
    DependsOn: Security
  Database:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Ref TemplateDatabase
      Parameters:
        RDSSubnetGroupName: 'django-subnetgroup'
        DBInstanceIdentifier: 'django-database'
        MultiAZ: 'true'
        AZ: 'ap-northeast-1a'
        FirstDBName: 'mysite'
        DBNameTag: 'django-db'
    DependsOn: Security
  Application:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Ref TemplateApplication
      Parameters:
        LaunchTemplateName: 'django-launch-template'
        TargetGroupName: 'django-tg'
        HealthCheckPath: '/polls/'
        HealthyThresholdCount: '2'
        HealthCheckInterval: '10'
        LoadBalancerName: 'django-lb'
        AutoScalingGroupName: 'django-asg'
        InstanceDesiredSize: '2'
        InstanceMinSize: '2'
        InstanceMaxSize: '4'
        CPUHighAlarmName: 'CPU_High'
        CPULowAlarmName: 'CPU_Low'
    DependsOn: Database
```

#####ネットワーク
```bash:network.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Network layer template
Parameters: 
  VPCCidrBlock:
    Type: String
    Description: CIDRBlock of VPC
  VPCNameTag:
    Type: String
    Description: Name tag of VPC
  IGWNameTag:
    Type: String
    Description: Name tag of IGW
  AZ1a:
    Type: AWS::EC2::AvailabilityZone::Name
    Description: AZ 1a of resources
    Default: 'ap-northeast-1a'
  AZ1c:
    Type: AWS::EC2::AvailabilityZone::Name
    Description: AZ 1c of resources
    Default: 'ap-northeast-1c'
  ELBSubnet1aCidr:
    Type: String
    Description: CIDR of ELB subnet 1a
  ELBSubnet1cCidr:
    Type: String
    Description: CIDR of ELB subnet 1c
  ELBSubnet1aNameTag:
    Type: String
    Description: Name tag of ELB subnet 1a
  ELBSubnet1cNameTag:
    Type: String
    Description: Name tag of ELB subnet 1c
  PublicRouteTableNameTag:
    Type: String
    Description: Name tag of Public route table
  WebSubnet1aCidr:
    Type: String
    Description: CIDR of Web subnet 1a
  WebSubnet1cCidr:
    Type: String
    Description: CIDR of Web subnet 1c
  WebSubnet1aNameTag:
    Type: String
    Description: Name tag of Web subnet 1a
  WebSubnet1cNameTag:
    Type: String
    Description: Name tag of Web subnet 1c
  DBSubnet1aCidr:
    Type: String
    Description: CIDR of DB subnet 1a
  DBSubnet1cCidr:
    Type: String
    Description: CIDR of DB subnet 1c
  DBSubnet1aNameTag:
    Type: String
    Description: Name tag of DB subnet 1a
  DBSubnet1cNameTag:
    Type: String
    Description: Name tag of DB subnet 1c
  PrivateRouteTableNameTag:
    Type: String
    Description: Name tag of Private route table
  NGWNameTag:
    Type: String
    Description: Name tag of Nat gateway

Resources: 
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCidrBlock
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: !Ref VPCNameTag
  IGW:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Ref IGWNameTag
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref IGW
  ELBSubnet1a:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AZ1a
      VpcId: !Ref VPC
      CidrBlock: !Ref ELBSubnet1aCidr
      Tags:
        - Key: Name
          Value: !Ref ELBSubnet1aNameTag
  ELBSubnet1c:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AZ1c
      VpcId: !Ref VPC
      CidrBlock: !Ref ELBSubnet1cCidr
      Tags:
        - Key: Name
          Value: !Ref ELBSubnet1cNameTag
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Ref PublicRouteTableNameTag
  DefaultPublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: '0.0.0.0/0'
      GatewayId: !Ref IGW
  ELBRouteTableAssoc1a:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref ELBSubnet1a
      RouteTableId: !Ref PublicRouteTable
  ELBRouteTableAssoc1c:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref ELBSubnet1c
      RouteTableId: !Ref PublicRouteTable
  WebSubnet1a:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AZ1a
      VpcId: !Ref VPC
      CidrBlock: !Ref WebSubnet1aCidr
      Tags:
        - Key: Name
          Value: !Ref WebSubnet1aNameTag
  WebSubnet1c:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AZ1c
      VpcId: !Ref VPC
      CidrBlock: !Ref WebSubnet1cCidr
      Tags:
        - Key: Name
          Value: !Ref WebSubnet1cNameTag
  DBSubnet1a:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AZ1a
      VpcId: !Ref VPC
      CidrBlock: !Ref DBSubnet1aCidr
      Tags:
        - Key: Name
          Value: !Ref DBSubnet1aNameTag
  DBSubnet1c:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AZ1c
      VpcId: !Ref VPC
      CidrBlock: !Ref DBSubnet1cCidr
      Tags:
        - Key: Name
          Value: !Ref DBSubnet1cNameTag
  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Ref PrivateRouteTableNameTag
  DefaultPrivateRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      DestinationCidrBlock: '0.0.0.0/0'
      NatGatewayId: !Ref NGW
  WebRouteTableAssoc1a:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref WebSubnet1a
      RouteTableId: !Ref PrivateRouteTable
  WebRouteTableAssoc1c:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref WebSubnet1c
      RouteTableId: !Ref PrivateRouteTable
  DBRouteTableAssoc1a:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref DBSubnet1a
      RouteTableId: !Ref PrivateRouteTable
  DBRouteTableAssoc1c:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref DBSubnet1c
      RouteTableId: !Ref PrivateRouteTable
  NGW:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NatGatewayEIP.AllocationId
      SubnetId: !Ref ELBSubnet1a
      Tags:
        - Key: Name
          Value: !Ref NGWNameTag
  NatGatewayEIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc

Outputs:
  VPCID:
    Value: !Ref VPC
    Description: VPC ID
    Export:
      Name: VPCID
  BastionSubnetID:
    Value: !Ref ELBSubnet1a
    Description: ID of ELB subnet 1a
    Export:
      Name: BastionSubnetID
  DBSubnet1aID:
    Value: !Ref DBSubnet1a
    Description: ID of DB subnet 1a
    Export:
      Name: DBSubnet1aID
  DBSubnet1cID:
    Value: !Ref DBSubnet1c
    Description: ID of DB subnet 1c
    Export:
      Name: DBSubnet1cID
  ELBSubnet1aID:
    Value: !Ref ELBSubnet1a
    Description: ID of ELB subnet 1a
    Export:
      Name: ELBSubnet1aID
  ELBSubnet1cID:
    Value: !Ref ELBSubnet1c
    Description: ID of ELB subnet 1c
    Export:
      Name: ELBSubnet1cID
  WebSubnet1aID:
    Value: !Ref WebSubnet1a
    Description: ID of Web subnet 1a
    Export:
      Name: WebSubnet1aID
  WebSubnet1cID:
    Value: !Ref WebSubnet1c
    Description: ID of Web subnet 1c
    Export:
      Name: WebSubnet1cID
```

#####セキュリティ
```bash:security.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Security layer template
Parameters: 
  ELBSGName:
    Type: String
    Description: Name of ELB security group
  BastionSGName:
    Type: String
    Description: Name of Bastion security group
  WebSGName:
    Type: String
    Description: Name of Web security group
  RDSSGName:
    Type: String
    Description: Name of RDS security group
  HTTPAccessIP:
    Type: String
    Description: CIDR block that is allowed HTTP access
  SSHAccessIP:
    Type: String
    Description: CIDR block that is allowed SSH access
  IAMRoleName:
    Type: String
    Description: Name of IAM role

Resources: 
  ELBSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Ref ELBSGName
      GroupDescription: !Ref ELBSGName
      VpcId: !ImportValue VPCID
      SecurityGroupIngress:
        -
          IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: !Ref HTTPAccessIP
      Tags:
        - Key: Name
          Value: !Ref ELBSGName
  BastionSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Ref BastionSGName
      GroupDescription: !Ref BastionSGName
      VpcId: !ImportValue VPCID
      SecurityGroupIngress:
        -
          IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: !Ref SSHAccessIP
      Tags:
        - Key: Name
          Value: !Ref BastionSGName
  WebSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Ref WebSGName
      GroupDescription: !Ref WebSGName
      VpcId: !ImportValue VPCID
      SecurityGroupIngress:
        -
          IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          SourceSecurityGroupId: !Ref ELBSG
        -
          IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          SourceSecurityGroupId: !Ref BastionSG
      Tags:
        - Key: Name
          Value: !Ref WebSGName
  RDSSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Ref RDSSGName
      GroupDescription: !Ref RDSSGName
      VpcId: !ImportValue VPCID
      SecurityGroupIngress:
        -
          IpProtocol: tcp
          FromPort: 3306
          ToPort: 3306
          SourceSecurityGroupId: !Ref WebSG
      Tags:
        - Key: Name
          Value: !Ref RDSSGName
  S3AccessRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: 'Allow'
            Principal:
              Service: 'ec2.amazonaws.com'
            Action: 'sts:AssumeRole'
      Description: IAM role for EC2 access to S3
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/AmazonS3FullAccess'
      RoleName: !Ref IAMRoleName
      Tags:
        - Key: Name
          Value: !Ref IAMRoleName
  S3AccessInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      InstanceProfileName: !Ref IAMRoleName
      Roles:
        - !Ref S3AccessRole

Outputs:
  BastionSecurityGroupID:
    Value: !Ref BastionSG
    Description: ID of Bastion security group
    Export:
      Name: BastionSGID
  RDSSecurityGroupID:
    Value: !Ref RDSSG
    Description: ID of RDS security group
    Export:
      Name: RDSSGID
  WebSecurityGroupID:
    Value: !Ref WebSG
    Description: ID of Web security group
    Export:
      Name: WebSGID
  S3AccessInstanceProfile:
    Value: !Ref S3AccessInstanceProfile
    Description: Instance profile of S3 access role
    Export:
      Name: S3AccessInstanceProfile
  ELBSecurityGroupID:
    Value: !Ref ELBSG
    Description: ID of ELB security group
    Export:
      Name: ELBSGID
```

#####踏み台
```bash:bastion.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Bastion EC2 template
Parameters: 
  ImageID:
    Type: String
    Description: Bastion EC2 image
    Default: 'ami-0404778e217f54308'
  InstanceType:
    Type: String
    Description: Bastion EC2 instance type
    Default: 't2.micro'
  EC2NameTag:
    Type: String
    Description: Bastion EC2 name tag
Resources: 
  BastionEC2:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref ImageID
      InstanceType: !Ref InstanceType
      NetworkInterfaces:
        - AssociatePublicIpAddress: 'True'
          DeviceIndex: '0'
          SubnetId: !ImportValue BastionSubnetID
          GroupSet:
            - !ImportValue BastionSGID
      KeyName: '{{resolve:ssm:ec2_keypair:1}}'
      Tags:
        - Key: Name
          Value: !Ref EC2NameTag
```

#####データベース
```bash:database.yml
AWSTemplateFormatVersion: 2010-09-09
Description: RDS template
Parameters: 
  RDSSubnetGroupName:
    Type: String
    Description: Name of RDS subnet group
  DBInstanceIdentifier:
    Type: String
    Description: DB instance identifier
  DBInstanceClass:
    Type: String
    Description: Instance class of DB
    Default: 'db.t2.micro'
  StorageType:
    Type: String
    Description: Storage type of DB
    Default: 'gp2'
  StorageSize:
    Type: String
    Description: Storage size of DB
    Default: '20'
  MaxStorageSize:
    Type: String
    Description: Max storage size of DB
    Default: '1000'
  MultiAZ:
    Type: String
    Description: Multi AZ
    Default: 'False'
  PublicAccess:
    Type: String
    Description: Public access
    Default: 'False'
  AZ:
    Type: String
    Description: Availability zone of DB
    Default: 'ap-northeast-1a'
  FirstDBName:
    Type: String
    Description: First DB name
  DBNameTag:
    Type: String
    Description: Name tag of DB

Resources: 
  RDSSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupName: !Ref RDSSubnetGroupName
      DBSubnetGroupDescription: !Ref RDSSubnetGroupName
      SubnetIds:
        - !ImportValue DBSubnet1aID
        - !ImportValue DBSubnet1cID
      Tags:
        - Key: Name
          Value: !Ref RDSSubnetGroupName
  MySQL:
    Type: AWS::RDS::DBInstance
    Properties:
      Engine: mysql
      EngineVersion: '8.0.23'
      DBInstanceIdentifier: !Ref DBInstanceIdentifier
      MasterUsername: '{{resolve:ssm:db_user:1}}'
      MasterUserPassword: '{{resolve:ssm-secure:db_password:1}}'
      DBInstanceClass: !Ref DBInstanceClass
      StorageType: !Ref StorageType
      AllocatedStorage: !Ref StorageSize
      MaxAllocatedStorage: !Ref MaxStorageSize
      MultiAZ: !Ref MultiAZ
      DBSubnetGroupName: !Ref RDSSubnetGroup
      PubliclyAccessible: !Ref PublicAccess
      VPCSecurityGroups:
        - !ImportValue RDSSGID
      # AvailabilityZone: !Ref AZ
      Port: '3306'
      DBName: !Ref FirstDBName
      Tags:
        - Key: Name
          Value: !Ref DBNameTag
```

#####アプリケーション
```bash:application.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Application layer template
Parameters: 
  LaunchTemplateName:
    Type: String
    Description: Name of Launch template
  ImageID:
    Type: String
    Description: Image ID of Launch template
    Default: 'ami-0404778e217f54308'
  InstanceType:
    Type: String
    Description: Instance type of Launch template
    Default: 't2.micro'
  TargetType:
    Type: String
    Description: Target type of Target group
    Default: 'instance'
  TargetGroupName:
    Type: String
    Description: Name of Target group
  Protocol:
    Type: String
    Description: Protocol of Target group
    Default: 'HTTP'
  Port:
    Type: String
    Description: Port of Target group
    Default: '80'
  ProtocolVersion:
    Type: String
    Description: Protocol version of Target group
    Default: 'HTTP1'
  HealthCheckProtocol:
    Type: String
    Description: Health check protocol of Target group
    Default: 'HTTP'
  HealthCheckPath:
    Type: String
    Description: Health check path of Target group
    Default: '/'
  HealthCheckPort:
    Type: String
    Description: Health check port of Target group
    Default: '80'
  HealthyThresholdCount:
    Type: String
    Description: Healthy threshold count of Target group
    Default: '5'
  UnhealthyThresholdCount:
    Type: String
    Description: Unhealthy threshold count of Target group
    Default: '2'
  HealthCheckTimeout:
    Type: String
    Description: Health check timeout of Target group
    Default: '5'
  HealthCheckInterval:
    Type: String
    Description: Health check interval of Target group
    Default: '30'
  HealthCheckSuccessCode:
    Type: String
    Description: Health check success code of Target group
    Default: '200'
  LoadBalancerType:
    Type: String
    Description: Type of Load balancer
    Default: 'application'
  LoadBalancerName:
    Type: String
    Description: Name of Load balancer
  LoadBalancerScheme:
    Type: String
    Description: Scheme of Load balancer
    Default: 'internet-facing'
  LoadBalancerIpAddressType:
    Type: String
    Description: IP address type of Load balancer
    Default: 'ipv4'
  AutoScalingGroupName:
    Type: String
    Description: Name of Auto scaling group
  HealthCheckType:
    Type: String
    Description: Health check type of Auto scaling group
    Default: 'ELB'
  HealthCheckGracePeriod:
    Type: String
    Description: Health check grace period of Auto scaling group
    Default: '300'
  InstanceDesiredSize:
    Type: String
    Description: Instance desired size of Auto scaling group
    Default: '1'
  InstanceMinSize:
    Type: String
    Description: Instance minimum size of Auto scaling group
    Default: '1'
  InstanceMaxSize:
    Type: String
    Description: Instance maximum size of Auto scaling group
    Default: '1'
  ListenerProtocol:
    Type: String
    Description: Protocol of Listener
    Default: 'HTTP'
  ListenerPort:
    Type: String
    Description: Port of Listener
    Default: '80'
  ListenerType:
    Type: String
    Description: Type of Listener
    Default: 'forward'
  CPUHighAlarmName:
    Type: String
    Description: Name of CPU high alarm
  CPULowAlarmName:
    Type: String
    Description: Name of CPU high alarm

Resources: 
  LaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateName: !Ref LaunchTemplateName
      LaunchTemplateData:
        ImageId: !Ref ImageID
        InstanceType: !Ref InstanceType
        KeyName: '{{resolve:ssm:ec2_keypair:1}}'
        NetworkInterfaces:
          - AssociatePublicIpAddress: 'False'
            DeviceIndex: '0'
            Groups:
              - !ImportValue WebSGID
        IamInstanceProfile:
          Name: !ImportValue S3AccessInstanceProfile
        UserData:
          Fn::Base64: |
            #!/bin/bash
            #必要なパッケージ・モジュールをインストール
            yum -y update
            yum -y install python3 git python3-devel mysql mysql-devel gcc
            pip3 install django
            pip3 install mysqlclient
            pip3 install gunicorn
            amazon-linux-extras install nginx1

            #AWS CLIプロファイルをダウンロード
            aws s3 cp s3://django-config/config /root/.aws/config
            aws s3 cp s3://django-config/credentials /root/.aws/credentials

            #Nginxサービスの自動起動設定・開始
            systemctl enable nginx
            systemctl start nginx

            #Staicファイル用ディレクトリの作成
            mkdir /usr/share/nginx/html/static

            #git clone用認証情報ファイルのダウンロード
            aws s3 cp s3://django-config/.netrc /root/.netrc

            #DjangoプロジェクトをGitHubから取得
            git clone https://github.com/zeems31/Django-Tutorial.git /home/ec2-user/mysite

            #Djangoプロジェクトの所有者をec2-userに変更
            chown -R ec2-user:ec2-user /home/ec2-user/mysite

            #settings.py内で使用するパスワード等を記述した外部ファイルをダウンロード・DB情報を置換
            aws s3 cp s3://django-config/settings_secret.py /home/ec2-user/mysite/mysite/settings_secret.py
            db_user=`aws ssm get-parameter --name db_user --query Parameter.Value`
            db_password=`aws ssm get-parameter --name db_password --query Parameter.Value --with-decryption`
            sed -i -e "s/ssm_DB_USER/$db_user/g" /home/ec2-user/mysite/mysite/settings_secret.py
            sed -i -e "s/ssm_DB_PASSWORD/$db_password/g" /home/ec2-user/mysite/mysite/settings_secret.py

            #上記ファイルの所有者をec2-userに変更
            chown ec2-user:ec2-user /home/ec2-user/mysite/mysite/settings_secret.py

            #Gunicornのサービスユニットファイルをダウンロード
            aws s3 cp s3://django-config/gunicorn.service /etc/systemd/system/gunicorn.service

            #Gunicornサービスユニットファイルの権限を変更
            chmod 644 /etc/systemd/system/gunicorn.service

            #Gunicornサービスを自動起動設定・開始
            systemctl enable gunicorn.service
            systemctl start gunicorn.service

            #Nginx設定ファイルをダウンロード・ローカルIPアドレスを置換
            aws s3 cp s3://django-config/mysite.conf /etc/nginx/conf.d/mysite.conf
            localip=`curl http://169.254.169.254/latest/meta-data/local-ipv4`
            sed -i -e "s/ec2-localip/$localip/g" /etc/nginx/conf.d/mysite.conf

            #Nginxサービスを再起動
            systemctl restart nginx

            #StaticファイルをSTATIC_ROOTに記述したパスに配置
            cd /home/ec2-user/mysite
            python3 manage.py collectstatic

            #Staticファイル用ディレクトリの所有者をec2-userに変更
            chown -R ec2-user:ec2-user /usr/share/nginx/html/static

            #migrateを実行
            python3 manage.py migrate

            #superuser作成用の環境変数を定義・superuserを作成
            django_superuser_username=`aws ssm get-parameter --name django_superuser_username --query Parameter.Value --with-decryption --output text`
            django_superuser_email=`aws ssm get-parameter --name django_superuser_email --query Parameter.Value --with-decryption --output text`
            django_superuser_password=`aws ssm get-parameter --name django_superuser_password --query Parameter.Value --with-decryption --output text`
            export DJANGO_SUPERUSER_USERNAME=$django_superuser_username
            export DJANGO_SUPERUSER_EMAIL=$django_superuser_email
            export DJANGO_SUPERUSER_PASSWORD=$django_superuser_password
            python3 manage.py createsuperuser --noinput
  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      TargetType: !Ref TargetType
      Name: !Ref TargetGroupName
      Protocol: !Ref Protocol
      Port: !Ref Port
      VpcId: !ImportValue VPCID
      ProtocolVersion: !Ref ProtocolVersion
      HealthCheckProtocol: !Ref HealthCheckProtocol
      HealthCheckPath: !Ref HealthCheckPath
      HealthCheckPort: !Ref HealthCheckPort
      HealthyThresholdCount: !Ref HealthyThresholdCount
      UnhealthyThresholdCount: !Ref UnhealthyThresholdCount
      HealthCheckTimeoutSeconds: !Ref HealthCheckTimeout
      HealthCheckIntervalSeconds: !Ref HealthCheckInterval
      Matcher:
        HttpCode: !Ref HealthCheckSuccessCode
      Tags:
        - Key: Name
          Value: !Ref TargetGroupName
  LoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Type: !Ref LoadBalancerType
      Name: !Ref LoadBalancerName
      Scheme: !Ref LoadBalancerScheme
      Subnets:
        - !ImportValue ELBSubnet1aID
        - !ImportValue ELBSubnet1cID
      SecurityGroups:
        - !ImportValue ELBSGID
      IpAddressType: !Ref LoadBalancerIpAddressType
      Tags:
        - Key: Name
          Value: !Ref LoadBalancerName
  Listner:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn: !Ref LoadBalancer
      Protocol: !Ref ListenerProtocol
      Port: !Ref ListenerPort
      DefaultActions:
        - Type: !Ref ListenerType
          TargetGroupArn: !Ref TargetGroup
  AutoScalingGroup:
    Type: AWS::AutoScaling::AutoScalingGroup
    Properties:
      AutoScalingGroupName: !Ref AutoScalingGroupName
      LaunchTemplate:
        LaunchTemplateId: !Ref LaunchTemplate
        Version: !GetAtt LaunchTemplate.LatestVersionNumber
      VPCZoneIdentifier:
        - !ImportValue WebSubnet1aID
        - !ImportValue WebSubnet1cID
      TargetGroupARNs:
        - !Ref TargetGroup
      HealthCheckType: !Ref HealthCheckType
      HealthCheckGracePeriod: !Ref HealthCheckGracePeriod
      DesiredCapacity: !Ref InstanceDesiredSize
      MinSize: !Ref InstanceMinSize
      MaxSize: !Ref InstanceMaxSize
      Tags:
        - Key: Name
          Value: !Ref AutoScalingGroupName
          PropagateAtLaunch: 'True'
  CPUHighAutoScalingPolicy:
    Type: AWS::AutoScaling::ScalingPolicy
    Properties:
      PolicyType: 'SimpleScaling'
      AutoScalingGroupName: !Ref AutoScalingGroup
      AdjustmentType: 'ChangeInCapacity'
      ScalingAdjustment: 1
      Cooldown: '30'
  CPULowAutoScalingPolicy:
    Type: AWS::AutoScaling::ScalingPolicy
    Properties:
      PolicyType: 'SimpleScaling'
      AutoScalingGroupName: !Ref AutoScalingGroup
      AdjustmentType: 'ChangeInCapacity'
      ScalingAdjustment: -1
      Cooldown: '30'
  CPUHighAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      Namespace: 'AWS/EC2'
      MetricName: 'CPUUtilization'
      Dimensions:
        - Name: 'AutoScalingGroupName'
          Value: !Ref AutoScalingGroup
      Statistic: 'Average'
      Period: 300
      ComparisonOperator: 'GreaterThanThreshold'
      Threshold: 70
      EvaluationPeriods: 1
      TreatMissingData: 'breaching'
      AlarmName: !Ref CPUHighAlarmName
      AlarmDescription: !Ref CPUHighAlarmName
      AlarmActions:
        - !Ref CPUHighAutoScalingPolicy
  CPULowAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      Namespace: 'AWS/EC2'
      MetricName: 'CPUUtilization'
      Dimensions:
        - Name: 'AutoScalingGroupName'
          Value: !Ref AutoScalingGroup
      Statistic: 'Average'
      Period: 300
      ComparisonOperator: 'LessThanThreshold'
      Threshold: 30
      EvaluationPeriods: 1
      TreatMissingData: 'missing'
      AlarmName: !Ref CPULowAlarmName
      AlarmDescription: !Ref CPULowAlarmName
      AlarmActions:
        - !Ref CPULowAutoScalingPolicy
```

#AWS CLIで構築
AWS CLIを使用して、事前準備とスタックの作成を実行します。
今回はWindowsにて以下のようなPowerShellスクリプトを作成して実行してみました。

```powershell:django-stack.ps1
$db_user = "<データベースのユーザー名>"
$db_password = "<データベースのユーザーのパスワード>"
$django_superuser_username = "<Djangoスーパーユーザーの名前>"
$django_superuser_password = "<Djangoスーパーユーザーのパスワード>"
$django_superuser_email = "<Djangoスーパーユーザーのメールアドレス>"
$ec2_keypair = "<EC2インスタンスで使用するキーペア名>"
$stack_name = "<作成するスタックの名前>"
$template_url = "<テンプレートのURL>"

aws ec2 create-key-pair --key-name $ec2_keypair --query 'KeyMaterial' --output text | out-file -encoding ascii -filepath ($ec2_keypair + ".pem")

aws ssm put-parameter --name "db_user" --value $db_user --type String --overwrite
aws ssm put-parameter --name "db_password" --value $db_password --type SecureString --overwrite
aws ssm put-parameter --name "django_superuser_username" --value $django_superuser_username --type SecureString --overwrite
aws ssm put-parameter --name "django_superuser_password" --value $django_superuser_password --type SecureString --overwrite
aws ssm put-parameter --name "django_superuser_email" --value $django_superuser_email --type SecureString --overwrite
aws ssm put-parameter --name "ec2_keypair" --value $ec2_keypair --type String --overwrite

aws cloudformation create-stack --stack-name $stack_name --template-url $template_url --capabilities CAPABILITY_NAMED_IAM
```

#トラブルシュート
#####各スタック作成の依存関係
root.ymlで各レイヤーのテンプレートから分割したスタックを作成するにあたって、まだネットワークレイヤーのリソースが作成されていないのに、同時にセキュリティレイヤーの作成が始まってしまいました。(まだVPCがないのでセキュリティグループの作成ができないなど...)
そのため、「DependsOn」を使用して明示的に依存関係を指定することで回避しています。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-attribute-dependson.html

#####EC2インスタンスへのインスタンスプロファイルの割り当て
AWS マネジメントコンソールからEC2インスタンスにIAMロールをアタッチする時には意識していませんでしたが、実際にはインスタンスプロファイルを介してIAMロールがアタッチされる動作のようです。
そのためCloudFormationでIAMロールを作成する際に併せてインスタンスプロファイルも作成する必要がありました。
ちなみにマネジメントコンソールでIAMロールを作成する時はインスタンスプロファイルが自動的に作成されます。
以下の記事を参考にしました。

https://dev.classmethod.jp/articles/do_you_know_iaminstanceprofile/

#####ユーザーデータ処理で Systems Manager のパラメータを取得できない
ユーザーデータ処理の途中でDB接続情報やDjangoのsuperuser情報を Systems Manager パラメータストアから取得するようにAWS CLIで実行していますが、AWS CLIの設定と認証情報がないことで実行に失敗しました。
そのため、事前にS3バケットにAWS CLIの設定ファイルと認証情報ファイルをアップロードしておき、ユーザーデータ処理の中でダウンロードするようにしました。

https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-configure-files.html

#####SecureStringタイプの Systems Manager パラメータの動的参照
root.yml内で Type が「AWS::CloudFormation::Stack」のリソースのパラメータとして SecureStringタイプの Systems Manager パラメータを指定しようとしましたが、エラーが発生しました。
CloudFormation テンプレート内で SecureString の動的参照をサポートするリソースの種類が決まっているようです。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/dynamic-references.html

#####マルチAZ
RDS作成時に以下のエラーにより作成に失敗しました。

```bash:error
Requesting a specific availability zone is not valid for Multi-AZ instances.
```

原因はマルチAZを有効にしたうえで、Availability Zoneを指定していたことでした。
以下の記事を参考にしました。

https://dev.classmethod.jp/articles/rds-has-two-configs-concerning-availability-zone/

#あとがき
CloudFormationでテンプレートを作成していると、GUIで何気なくデフォルトで設定していた項目がどういった設定値であるのかを調べる良いきっかけにもなりました。
まだ最適化できる部分が多々あると思うのでブラッシュアップしていければと思います。