【AWS】CloudFormationでWebサービスを構築②（EC2編）

#1. はじめに
本投稿は以下の記事の続編となります。
[【AWS】CloudFormationでWebサービスを構築①（NW編）](https://qiita.com/chibiharu/items/ade872ce161f3577aea5)
本投稿を初めてご覧の方はまずはこちらの記事からお読みになって下さい。

#2. EC2編概要
今回の『【AWS】CloudFormationでWebサービスを構築②（EC2編）』では、Web環境の核となるサーバ郡、及びその周辺サービスのテンプレートを作成します。
具体的なサービスとしては以下の通りとなります。

##2.1 「server.yml」で登場するサービス
- IAMロール
    - MySQLとの連携にSSMを使用しますので、SSMへの権限をEC2に付与します
- SSM
    - EC2接続用
- EC2（Web/APサーバ）
    - HTTPサーバにはApacheを使用します
- Route53
    - 事前にドメインを準備しておいてください
   （投稿主は「Route53」で取得したドメインを使用します）
- Elastic Load Balancer（ALB）
    - 負荷分散方式ですが、Web環境を構築する為、一番シナジーの強いALBを採用します
- ACM
    - 証明書はACMにて新規発行します

##2.2 EC2編構成図
EC2編で作成するテンプレート環境の構成図を下図の通りとします。
![chibiharu-cfn-qiita-web-env-target-ec2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570694/d84835f5-a1f2-30aa-f20d-6815a5e74c03.png)

#3. EC2テンプレートの作成
以下はEC2部分のテンプレートになります。
命名規則等、必要であればご自身の環境に合わせて修正してください。
修正が完了したら、作業用ディレクトリに「server.yml」というファイル名で保存してください。

```:server.yml
AWSTemplateFormatVersion: "2010-09-09"
Description: Web2tier-Server-Template


# ------------------------------------------------------------
# Input Parameters
# ------------------------------------------------------------
Parameters:
### Project Prefix ###
  PJPrefix:
    Type: String
### Key pair ###
  KeyName:
    Description: input EC2 Keyname
    Type: 'AWS::EC2::KeyPair::KeyName'
### Hosted Zone ###
  HostedZoneName:
    Type: String
    Description: DNS Name to create
    Default: '<<ドメイン(example.com)>>'
    AllowedPattern: (?!-)[a-zA-Z0-9-.]{1,63}(?<!-)
    ConstraintDescription: must be a valid DNS zone name
  SubDomain:
    Description: FQDN of the certificate
    Type: String
    Default: 'www.<<ドメイン(example.com)>>'
    
    
### Resources ###
Resources: 
# ------------------------------------------------------------
# IAM Role
# ------------------------------------------------------------
  SSMRole:
    Type: AWS::IAM::Role
    Properties:
      Path: /
      RoleName: !Sub "${PJPrefix}-SSMRole"
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action:
              - sts:AssumeRole
      MaxSessionDuration: 3600
      ManagedPolicyArns: 
        - arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM

  SSMRoleProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Path: /
      Roles:
        - !Ref SSMRole
        
        
# ------------------------------------------------------------
# EC2
# ------------------------------------------------------------
### AvailabilityZone-A ###
  EC2WebA: 
    Type: AWS::EC2::Instance
    Properties: 
      ImageId: ami-032d6db78f84e8bf5
      InstanceType: t2.micro
      IamInstanceProfile: !Ref SSMRoleProfile
      KeyName: !Ref KeyName
      NetworkInterfaces: 
        - AssociatePublicIpAddress: "true"
          DeviceIndex: "0"
          SubnetId: { "Fn::ImportValue": !Sub "${PJPrefix}-private-subnet-a" }
          GroupSet:
            - { "Fn::ImportValue": !Sub "${PJPrefix}-web-sg" }
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash
          sudo yum -y update
          sudo yum -y install httpd
          sudo systemctl start httpd
          sudo systemctl enable httpd
          sudo echo "chibiharu's Qiita Apache Test Page For Success AZ-a" > /var/www/html/index.html
          sudo systemctl start amazon-ssm-agent.service
          sudo systemctl enable amazon-ssm-agent.service
      Tags:
          - Key: Name
            Value: !Sub "${PJPrefix}-web-server-1a"
            
### AvailabilityZone-C ###
  EC2WebC: 
    Type: AWS::EC2::Instance
    Properties: 
      ImageId: ami-032d6db78f84e8bf5
      InstanceType: t2.micro
      IamInstanceProfile: !Ref SSMRoleProfile
      KeyName: !Ref KeyName
      NetworkInterfaces: 
        - AssociatePublicIpAddress: "true"
          DeviceIndex: "0"
          SubnetId: { "Fn::ImportValue": !Sub "${PJPrefix}-private-subnet-c" }
          GroupSet:
            - { "Fn::ImportValue": !Sub "${PJPrefix}-web-sg" }
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash
          sudo yum -y update
          sudo yum -y install httpd
          sudo systemctl start httpd
          sudo systemctl enable httpd
          sudo echo "chibiharu's Qiita Apache Test Page For Success AZ-c" > /var/www/html/index.html
          sudo systemctl start amazon-ssm-agent.service
          sudo systemctl enable amazon-ssm-agent.service
      Tags:
          - Key: Name
            Value: !Sub "${PJPrefix}-web-server-1c"
            
            
# ------------------------------------------------------------
# Target Group
# ------------------------------------------------------------
  TargetGroup:
    Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
    Properties: 
      VpcId: { "Fn::ImportValue": !Sub "${PJPrefix}-vpc" } 
      Name: !Sub "${PJPrefix}-tg"
      Protocol: HTTP
      Port: 80
      HealthCheckProtocol: HTTP
      HealthCheckPath: "/"
      HealthCheckPort: "traffic-port"
      HealthyThresholdCount: 2
      UnhealthyThresholdCount: 2
      HealthCheckTimeoutSeconds: 5
      HealthCheckIntervalSeconds: 10
      Matcher: 
        HttpCode: 200
      Tags: 
        - Key: Name
          Value: !Sub "${PJPrefix}-${ALB}-tg"
      TargetGroupAttributes: 
        - Key: "deregistration_delay.timeout_seconds"
          Value: 300
        - Key: "stickiness.enabled"
          Value: false
        - Key: "stickiness.type"
          Value: lb_cookie
        - Key: "stickiness.lb_cookie.duration_seconds"
          Value: 86400
      Targets: 
        - Id: !Ref EC2WebA
        - Id: !Ref EC2WebC
          Port: 80
          
          
# ------------------------------------------------------------
# Application Load balancer
# ------------------------------------------------------------
### ALB ###
  ALB: 
    Type: "AWS::ElasticLoadBalancingV2::LoadBalancer"
    Properties: 
      Name: !Sub "${PJPrefix}-alb"
      Scheme: "internet-facing"
      LoadBalancerAttributes: 
        - Key: "deletion_protection.enabled"
          Value: false
        - Key: "idle_timeout.timeout_seconds"
          Value: 4000
      SecurityGroups:
        - { "Fn::ImportValue": !Sub "${PJPrefix}-alb-sg" } 
      Subnets: 
        - { "Fn::ImportValue": !Sub "${PJPrefix}-public-subnet-a" } 
        - { "Fn::ImportValue": !Sub "${PJPrefix}-public-subnet-c" }
        
### Listener HTTP ###
  ALBListenerHTTP: 
    Type: "AWS::ElasticLoadBalancingV2::Listener"
    Properties: 
      Port: 80
      Protocol: HTTP
      DefaultActions: 
        - Type: redirect
          RedirectConfig: 
            Host: '#{host}'
            Path: '/#{path}'
            Port: 443
            Protocol: HTTPS
            Query: '#{query}'
            StatusCode: HTTP_301
      LoadBalancerArn: !Ref ALB
      
### Listener HTTPS ###
  ALBListenerHTTPS:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      Port: 443
      Protocol: HTTPS
      Certificates:
        - CertificateArn: !Ref ACM
      DefaultActions:
        - TargetGroupArn: !Ref TargetGroup
          Type: forward
      LoadBalancerArn: !Ref ALB
      
      
# ------------------------------------------------------------
# Route53
# ------------------------------------------------------------
  HostedZone:
    Type: AWS::Route53::HostedZone
    Properties:
      Name: !Ref HostedZoneName
      HostedZoneTags:
      - Key: Name
        Value: !Sub "${PJPrefix}-cfn-hostedzone"
        
  DNSRecord:
    Type: AWS::Route53::RecordSet
    Properties:
      HostedZoneId: !Sub '${HostedZone}'
      Comment: "alias to alb"
      Name: !Sub '${SubDomain}'
      Type: A
      AliasTarget:
        HostedZoneId: !GetAtt 'ALB.CanonicalHostedZoneID'
        DNSName: !GetAtt 'ALB.DNSName'
        
        
# ------------------------------------------------------------
# ACM
# ------------------------------------------------------------
  ACM:
    Type: AWS::CertificateManager::Certificate
    Properties:
      DomainName: !Sub '${SubDomain}'
      DomainValidationOptions:
        - DomainName: !Sub '${SubDomain}'
          HostedZoneId: !Sub '${HostedZone}'
      ValidationMethod: DNS
      
      
# ------------------------------------------------------------
# Output Parameter
# ------------------------------------------------------------
Outputs:
### EC2 ###
  EC2WebA:
    Value: !Ref EC2WebA
    Export:
      Name: !Sub "${PJPrefix}-web-server-1a"
  EC2WebC:
    Value: !Ref EC2WebC
    Export:
      Name: !Sub "${PJPrefix}-web-server-1c"
```

##3.1 EC2テンプレートのビルド
作成したCFnテンプレートのビルドを行います。

以下コマンドを実行してください。
※ スタック名やプレフィックス、また追加で指定したいパラメータ等があればご自身の環境に合わせ修正して下さい。

```:サーバテンプレートのビルドコマンド
$ aws cloudformation create-stack \
--stack-name web2tier-Server-stack \
--template-body file://./server.yml \
--parameters ParameterKey=KeyName,ParameterValue=<<キーペア名>> \ 
ParameterKey=PJPrefix,ParameterValue=web2tier
```

↓ 構文エラーが無ければ、以下のような結果が出力されます。

```:実行結果
{
    "StackId": "arn:aws:cloudformation:ap-northeast-1:xxxxxxx:stack/<スタックネーム>/<メタ番号>"
}
```

##3.2 ドメインプロパイダーへのNSの登録
Route53にてホストゾーンを作成しただけでは、名前解決はできません。
ドメインを発行しているプロパイダーにて、Route53にて発行されたNSレコードを登録する必要があります。

この手順に関しては、流石にCFnでは実行できず、手動にて行う必要があります。

申し訳ありませんが、本記事ではIaCにおける環境構築に焦点を当てているので、プロパイダーへのNSレコードの登録手順は省かさせていただきます。
しかし、Googleで「Route53 <ドメインプロパイダー名> NSレコード登録」等で検索をかけると同手順が腐るほどでてきますので、そちらをご覧になってくださればと思います。

##3.3 ACMでの証明書のDNS検証
CFnビルド後、DNSプロパイダーへNSレコードの追加。
ここまで行ってもまだ、作業は完了しません。

ACMにて発行した証明書のDNS検証が終わらないとHTTPS通信ができない為、検証が終わるのを待つという作業があります。
CFnビルド時点で、ACMでのDNS検証は自動で始まるので、本当にただひたすら待つだけです（笑）
大体15~30分程で検証は終わりますので、気長に待ちましょう。

#4. 動作確認 - Webサーバのテストページの閲覧
ここで一度動作確認を行います。
完了要件の一つである以下を確認します。
・Webサーバのテストページの閲覧

具体的には、ホストゾーンへ登録したドメインをブラウザへ打ち込み、Webサーバにて設定したテストページが閲覧します。
また、リロードを繰り返し、ALBに負荷分散が行われているかも確認します。

早速確認してみましょう。
以下ページが表示され、リロードを繰り返し、"a"と"c"の表記が変動すること、またWebページが暗号化されていることが確認できれば、動作確認はクリアとなります！
　
● AZ-a
![chibi-dousakakunin-1a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570694/f9240dbf-c79c-4909-e2fd-968589a506b2.png)
● AZ-c
![chibi-dousakakunin-1c.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570694/dc4cb225-cc61-2550-26a5-95004e88aaa4.png)

#5. まとめ
今回の記事にて、Web環境におけるサーバ群のデプロイが完了しました。
たったこれだけの作業で、サーバ構築が行えてしまうのですから、やはりIaCは偉大としか言いようがありません。

個人的な見解ですが、将来的にはインフラ構築も全てコード管理されていくのではないかと予想しています。
(CFnが主流になるとは限りませんが..)

次回の記事は、データベース（RDS）編となります。
お楽しみにしていてください。

--- 記事一覧 ---
[①【AWS】CloudFormationでWebサービスを構築①（NW編)](https://qiita.com/chibiharu/items/ade872ce161f3577aea5)
[②【AWS】CloudFormationでWebサービスを構築②（EC2編)](https://qiita.com/chibiharu/items/5d5fbc97f55ea6fa1e31)
[③【AWS】CloudFormationでWebサービスを構築③（RDS編)](https://qiita.com/chibiharu/items/dc42c6a7ac9cb6740bce)