Amazon ECS Workshop #3 ~コンテナのモニタリング~

# 概要
今回の投稿における達成目標と構成図を記載します。
##達成目標
本投稿は[#2 ~コンテナのデプロイ~](https://qiita.com/itoumke/items/1755e6bcef0d76dff65b)編の続きになります。
デプロイしたECSのタスクにおけるコンテナのリソース状況のモニタリングについて、設定してみます。
大きく以下のことができることを確認します。

- コンテナのリソース状況（CPU使用率など）を確認できること
- リソースの高騰を検知し、イベント（メール通知）を発出できること

## 環境構成図
本投稿では下記構成図の赤字のリソースを新規に作成します。
![環境構成図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640480/b1d746dd-3188-3aa2-d8e4-23f05e34c294.png)


# コンテナリソースモニタリング
## Cloudwatch Container Insights の有効化
タスク実行されているコンテナのリソース状況を取得するマネージドサービスとして、Cloudwatch Container Insightsが提供されています。
コンテナの起動状況（クラスターやサービスで起動している数など）から、CPU/MEMORY/NW関連情報などの情報が取得可能です。
詳細は[Amazon ECS Container Insights Metrics](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Container-Insights-metrics-ECS.html)を参照ください。

Cloudwatch Container Insightsはアカウント、クラスターの単位で有効/無効を設定可能です。
クラスター単位で設定しなければ、アカウントの設定が採用され、クラスター単位で設定するとオーバーライドされるといった仕様です。

では、さっそく[#2 ~コンテナのデプロイ~](https://qiita.com/itoumke/items/1755e6bcef0d76dff65b)でデプロイしたコンテナのCloudwatch Container Insightsを有効化し、リソース状況を確認してみます。

クラスターに関するCFnのテンプレートに下記追記をし、スタック更新をします。

```
      ClusterSettings: 
        - Name: containerInsights
          Value: enabled
```

<details><summary>Cloudwatch Container Insights有効化 更新用テンプレート</summary><div>

```ecs-cfn-template-v2.yml
AWSTemplateFormatVersion: "2010-09-09"
Description:
   Create Container with ECS on Fargate
  
Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "Project Name Prefix"
        Parameters:
          - PJPrefix
  
      - Label:
          default: "ECS Configuration"
        Parameters:
          - ECSTaskCPUUnit
          - ECSTaskMemory
          - HTTPLocation
          - IAMRoleParameter
 
    ParameterLabels:
      ECSTaskCPUUnit:
        default: "ECSTaskCPUUnit"
      ECSTaskMemory:
        default: "ECSTaskMemory"
      HTTPLocation:
        default: "Permit HTTP Src IP Address"
      IAMRoleParameter:
        default: "IAM Role"
  
# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------#
Parameters:
  PJPrefix:
    Type: String

  ECSTaskCPUUnit:
    AllowedValues: [ 256, 512, 1024, 2048, 4096  ]
    Type: String
    Default: "256"

  ECSTaskMemory:
    AllowedValues: [ 512, 1024, 2048, 4096  ]
    Type: String
    Default: "512"

  HTTPLocation:
    Default: 127.0.0.0/32
    Type: String
    MinLength: 9
    MaxLength: 18
    AllowedPattern: (\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})
  
  IAMRoleParameter:
    Type: String
     
# ------------------------------------------------------------#
#  ECS Create
# ------------------------------------------------------------#
Resources:
## ECS Cluster
  ECSCluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: !Sub ${PJPrefix}-Cluster
      ### Enable CloudWatch Container Insights
      ClusterSettings: 
        - Name: containerInsights
          Value: enabled
## ECS LogGroup
  ECSLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub /ecs/logs/${PJPrefix}
## ECS TaskDefinition
  ECSTaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Cpu: !Ref ECSTaskCPUUnit
      ExecutionRoleArn: !Ref IAMRoleParameter
      Family: !Sub ${PJPrefix}-task
      Memory: !Ref ECSTaskMemory
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE
      ContainerDefinitions:
        - Name: !Sub ${PJPrefix}-ecscontainer
          Image: !Sub ${AWS::AccountId}.dkr.ecr.ap-northeast-1.amazonaws.com/myrepository
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref ECSLogGroup
              awslogs-region: !Ref "AWS::Region"
              awslogs-stream-prefix: !Ref PJPrefix
          MemoryReservation: 128
          PortMappings:
            - HostPort: 80
              Protocol: tcp
              ContainerPort: 80
## ECS Service
  ECSService:
    Type: AWS::ECS::Service
    Properties:
      Cluster: !Ref ECSCluster
      DesiredCount: 1
      LaunchType: FARGATE
      NetworkConfiguration:
       AwsvpcConfiguration:
           AssignPublicIp: ENABLED
           SecurityGroups:
             - !Ref ECSSG
           Subnets:
             - { "Fn::ImportValue": !Sub "${PJPrefix}-public-subnet-a" }
      ServiceName: !Sub ${PJPrefix}-ecsservice
      TaskDefinition: !Ref ECSTaskDefinition

## ECS SecurityGroup Create
  ECSSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: ecs-http-permit
      GroupDescription: Allow HTTP access only MyIP
      VpcId:  { "Fn::ImportValue": !Sub "${PJPrefix}-vpc" }
      SecurityGroupIngress:
        # 
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: !Ref HTTPLocation

```

</div></details>

コンソール上でCloudwatchのContainer Insightsを選択すると、クラスター/サービス/タスクの単位で各リソース状況がモニタリングできました。

![コンテナinsight有効化１.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640480/98b63d49-5c47-816e-1b50-bb3613423b2f.png)

## CloudwatchAlarmの設定
少しおまけになりますが、CloudwatchAlarmを設定し、コンテナに対するアクセスが閾値を超えたことをイベント検知し、メール通知まで設定してみます。

### メール通知用SNS作成
Cloudwatchで異常状態を検知した際にSNSに連携することでメール通知する仕組みにします。
下記テンプレートを実行することでメール通知用のSNS設定を作成できます。
SNSはSubscriptionにメールアドレスを設定した場合、該当アドレス宛に承認メールが届くので、Confirmを忘れないようにしましょう。

<details><summary>メール通知用SNS作成テンプレート</summary><div>

```sns-cfn-template.yml
AWSTemplateFormatVersion: "2010-09-09"
Description:
   Create sns for E-mail notification
  
Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "Project Name Prefix"
        Parameters:
          - PJPrefix
  
      - Label:
          default: "Notification Configuration"
        Parameters:
          - EmailAddress

# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------#
Parameters:
  PJPrefix:
    Type: String

  EmailAddress:
   Type: String
# ------------------------------------------------------------#
#  SNS Create
# ------------------------------------------------------------#
Resources:
## topic
  Topic:
    Type: AWS::SNS::Topic
    Properties:
      DisplayName: !Sub ${PJPrefix}-topic
      TopicName: !Sub ${PJPrefix}-topic
## subscription
  Subscription:
    Type: AWS::SNS::Subscription
    Properties:
      Endpoint: !Sub ${EmailAddress}
      Protocol: email
      Region: ap-northeast-1
      TopicArn: !Ref Topic
# ------------------------------------------------------------#
# Output Parameters
# ------------------------------------------------------------#               
Outputs:
# Topic
  Topic:
    Value: !Ref Topic
    Export:
      Name: !Sub "${PJPrefix}-topic"
```

</div></details>

### Alarm設定
どのメトリクスがどのような状態となるときを異常とするかはCloudwatchAlarmで設定します。
今回は確認試験がしやすいようCPU使用率を対象に閾値をかなり低くして設定します。

<details><summary>CloudwatchAlarm設定用テンプレート</summary><div>

```resourcealarm-cfn-template.yml
AWSTemplateFormatVersion: "2010-09-09"
Description:
   Create CloudwatchAlarm of ecs-resource
  
Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "Project Name Prefix"
        Parameters:
          - PJPrefix
      - Label:
          default: "Alarm Configuration"
        Parameters:
          - Threshold
# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------#
Parameters:
  PJPrefix:
    Type: String
  Threshold:
    Type: String
    Default: "0.1"
# ------------------------------------------------------------#
#  CloudWatchAlarm of ECS-Resource
# ------------------------------------------------------------#
Resources:
## CloudwatchAlarm of ECS Resource
  CloudWatchAlarmECSResource:
    Type: 'AWS::CloudWatch::Alarm'
    Properties:
      AlarmActions:
        - Fn::ImportValue: 
            !Sub "${PJPrefix}-topic"
      AlarmDescription: !Sub ${PJPrefix}-ecsresource-alarm
      AlarmName: !Sub ${PJPrefix}-ecsresource-alarm
      ComparisonOperator: GreaterThanOrEqualToThreshold
      EvaluationPeriods: 1
      Dimensions:
        - Name:  ServiceName
          Value: !Sub "${PJPrefix}-ecsservice"
        - Name:  ClusterName
          Value: !Sub "${PJPrefix}-Cluster"
      MetricName: "CpuUtilized"
      Namespace: "ECS/ContainerInsights"
      Period: 300
      Statistic: Sum
      Threshold: 1
      TreatMissingData: ignore
```

</div></details>

### LoadTest確認
siegeコマンドを利用して一定量のアクセスを掛けてみます。
[#2 ~コンテナのデプロイ~](https://qiita.com/itoumke/items/1755e6bcef0d76dff65b)編で構築したEC2のユーザデータに下記を追加して更新し、再作成します。

```
          #Siege Install
          sudo yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
          sudo yum install -y siege

```

<details><summary>siegeインストール追加踏み台EC2構築用テンプレート</summary><div>

```bastion-cfn-template.yml
AWSTemplateFormatVersion: "2010-09-09"
Description:
   Create Bastion-EC2
  
Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "Project Name Prefix"
        Parameters:
          - PJPrefix
  
      - Label:
          default: "EC2 Configuration"
        Parameters:
          - ImageId
          - InstanceTypeParameter
          - DiskSize
          - SSHLocation
          - KeyName
          - IAMRoleParameter
 
    ParameterLabels:
      ImageId:
        default: "AMI ID"
      InstanceTypeParameter:
        default: "Instance Type"
      DiskSize:
        default: "Disk Size(GiB)"
      SSHLocation:
        default: "Permit SSH Src IP Address"
      KeyName:
        default: "SSH Key Name"
      IAMRoleParameter:
        default: "IAM Role"
  
# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------#
Parameters:
  PJPrefix:
    Type: String
  
  ImageId:
    Type: AWS::EC2::Image::Id
    Default: ami-0f310fced6141e627

  
  InstanceTypeParameter:
    Default: t2.small
    Type: String
  
  DiskSize:
    Default: 8
    Type: String
  
  SSHLocation:
    Default: 127.0.0.0/32
    Type: String
    MinLength: 9
    MaxLength: 18
    AllowedPattern: (\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})
 
  KeyName:
    Type: AWS::EC2::KeyPair::KeyName
 
  IAMRoleParameter:
    Type: String
     
# ------------------------------------------------------------#
#  EC2 Create
# ------------------------------------------------------------#
Resources:
  EC2:
    Type: AWS::EC2::Instance
    Properties:
      DisableApiTermination: "true"
      InstanceInitiatedShutdownBehavior: stop
      ImageId: !Ref ImageId
      KeyName: !Ref KeyName
      InstanceType: !Ref InstanceTypeParameter
       
      BlockDeviceMappings:
      - DeviceName: "/dev/xvda"
        Ebs:
          DeleteOnTermination: "false"
          VolumeSize: !Ref DiskSize
          VolumeType: "gp2"
 
      NetworkInterfaces:
        - AssociatePublicIpAddress: "true"
          DeleteOnTermination: "true"
          DeviceIndex: "0"
          SubnetId:  { "Fn::ImportValue": !Sub "${PJPrefix}-public-subnet-a" }
          GroupSet:
            - !Ref EC2SG
 
      IamInstanceProfile: 
        !Ref IAMRoleParameter
 
      UserData:
        Fn::Base64: |
          #!/bin/sh
          #TimeZone Setting JST
          sudo timedatectl set-timezone Asia/Tokyo
          #Docker Install and Boot
          sudo yum update -y
          sudo yum -y install docker
          sudo systemctl start docker
          sudo systemctl enable docker
          #Siege Install
          sudo yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
          sudo yum -y install siege

      Tags:
          - Key: Name
            Value: !Sub ${PJPrefix}-BastionEC2
             
## EIP Create
  ElasticIp:
    Type: AWS::EC2::EIP
    Properties:
      InstanceId: !Ref EC2
      Domain: vpc
 
 
## EC2 SecurityGroup Create
  EC2SG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: ec2-ssh-permit
      GroupDescription: Allow SSH and HTTP access only MyIP
      VpcId:  { "Fn::ImportValue": !Sub "${PJPrefix}-vpc" }
      SecurityGroupIngress:
        # ssh
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: !Ref SSHLocation
 
 
# ------------------------------------------------------------#
#  EC2 Output
# ------------------------------------------------------------#
 
Outputs:
  ElasticIp:
    Value: !GetAtt EC2.PublicIp
    Description: Public IP of EC2 instance

```

</div></details>

下記コマンドでパフォーマンステストをしてみます。

```
siege -c 50 -i {{デプロイされているコンテナのIP}}
```

しばらく待機しているとメールを受信し、CPUの高騰を検知しました。
念のためコンソールで確認すると下記のようにCPU使用率が上昇して、異常状態となっていることが分かります。

![LoadTest確認.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640480/bd113a76-1507-f8cf-1739-6ce7cb684263.png)


# まとめ
今回はCloudwatch→SNSを設定し、起動しているコンテナのCPU高騰を検知し、メール発報する部分を構築してみました。
今回はSNSでメール通知のサブスクリプションのみを連携させましたが、
実運用を考えると、Lambdaをキックさせ、Serviceの必要台数を変化させることで自動拡張させるといったことが考えられます。
次回はこちらにチャレンジしたいと思います。