Datadog で CloudWatch メトリクスストリームを利用してみた

CloudWatch メトリクスストリームにより、Datadog で素早くメトリクスを取得できるようになりました。
従来、Datadog から API 経由でポーリングしていましたが、AWS からストリーミングできるようになります。 

ちなみに、Datadog が API ポーリングする間隔はデフォルトで 10 分となっており、サポートに依頼することで最短 2 分まで短縮可能です。（AWS 側のコストは 5 倍になる）

![202104-breaking-cloudwatch-metric-stream_1-fs8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/619762/8cca02b3-e444-ee6b-865d-d14cb145fd0d.png)
画像出典： [[速報] CloudWatchの監視メトリクスをKinesis経由でストリーム送信可能な新機能「CloudWatch Metric Streams」が登場しました！](https://dev.classmethod.jp/articles/202104-breaking-cloudwatch-metric-stream/)

今回は Datadog 提供の CFn テンプレートを利用してメトリクスストリームを設定してみました。
エントリがあまり出ていなかったので、同じ方法を検討している方の参考になればと思います。

# 効果
先に導入効果をお伝えします。

|   評価項目  | 設定前 (GetMetricData API) | 設定後 (CloudWatch Metric Streams) | 
| -------- | ------ | ------ | 
| 遅延時間 |  最大12分  |  3分 (稀に4分)    | 
| コスト   | 1,000メトリクスごとに 0.01 USD |  1,000メトリクスごとに 0.003 USD + Data Firehose | 

設定前は最大12分程度あった遅延が、3分に短縮されました。 (目視なのでざっくりですが)
遅延と呼んでいるのは、リアルタイム時刻からメトリクスが表示されるまでの時間です。
コストの最新情報は、[AWS公式サイト](https://aws.amazon.com/jp/cloudwatch/pricing/?nc1=h_ls) よりご確認を。






# 設定方法
設定方法はとても簡単です。 [Datadog公式サイト](https://docs.datadoghq.com/integrations/guide/aws-cloudwatch-metric-streams-with-kinesis-data-firehose/?tab=cloudformation#setup) よりご確認ください。
CFn で作成するパターン、AWS コンソール から手作業で作成するパターンがあります。

### (CFn利用の場合) テンプレートを微修正
※ こちらは任意です。テンプレートを一つにまとめたい、自身で管理したいという方はご参照ください。

手順中で ``Automatically Using CloudFormation``を押下すると AWS の CFn スタック作成画面に飛びます。このテンプレートはDatadog管理のテンプレートであり、かつ、ネストになっているため、状態をコードで追うのがやや面倒です。

私の場合、メトリクスを取得したいリージョンが東京のみで、テンプレートを一つにまとめたかったので下記のように修正しました。逆に複数のリージョンから取得したい人はそのまま利用するのがよいと思います。

テンプレートは長いので [一番下](#cfnテンプレート) に貼り付けます。

### 設定確認

リソースを作成して数分後にこのような画面になっていれば正常に設定されています。![image2021-5-12_10-24-27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/619762/d8c5dacf-b410-9b2e-df4a-084c03373352.png)

＜設定前＞
10分くらい遅延してメトリクスが取得されています。（見に行くタイミングによる）
![image2021-5-12_9-47-51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/619762/8232095c-5f2d-7371-f45a-fcd16062452b.png)

＜設定後＞
コンスタントに3分遅延でメトリクスが取得されるようになりました！
![image2021-5-12_10-54-8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/619762/56b0cc7c-01ec-b607-8dbf-0fb18d18b68c.png)

# 注意事項

### 設定直後グラフが変な感じになる
設定後のスクショを見てもらうと 10:20 で値が上昇していますが、Cloudwatch 側ではこの変化は見られませんでした。おそらくAPIとストリームの重複取得が生じているのだと思いますが、十数分待つと正常値に戻りました。
Datadogページ内には次のように記載されています。
> Metric collection via API is automatically disabled for any namespace sending metrics via streaming.

### Delay evaluation を調整
Monitor でアラームを仕掛けている場合は、``Delay evaluation`` の調整が必要です。この値は、モニターの評価を何秒遅らせるかを指定するものです。メトリクスを素早く取得できても、``Delay evaluation`` を変更しなければアラーム発報までの時間を短縮できないのでご注意ください。

Datadogページ内には次のように記載されていますが、これはAPI経由でメトリクスを取得することを前提としていると思われます。
> We highly recommend a delay of at least 900s for AWS metrics.

### デフォルトゼロ関数を追加 （2021/5/29 追記）
ALB 5XX などの正常時にデータが存在しないメトリクスには、以下に記載する設定をオススメします。
この設定をしないと、現時点では、アラームにおける Alert → OK への Resolve が自動で成されません。

#### なぜ自動で Resolve されないのか？
下のスクショは、CloudWatch メトリクスストリームを有効化する前の EVALUATION GRAPH の状態です。
デフォルトで0埋めがされていることが分かります。
![before_chaged.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/619762/b541251b-c9cc-d3ad-e3ca-e24b035dfaaa.png)

一方で、有効化後になると、メトリクスが存在しないときにはNodataとなっていることが分かります。
![after_changed.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/619762/837a1302-18c6-c97f-21d1-eb60b60c83c1.png)

有効化前は、データが存在しない部分を気を利かせて0埋めをしてくれていましたが、
有効化後は、この機能が失われてしまうようです。（なんでやし）

これにより、メトリクスが閾値を超えて Alert 状態に遷移した後、たとえ実際には正常状態に回復していたとしても、**それを示すメトリクスが存在しない**ので、ずっと Alert 状態のままになってしまう、というわけです。

この仕様についてDatadog のサポートに問い合わせた結果、次のような回答でした。

> Cloudwatch APIから取得する場合、弊社側のCrawlerでAPIをCallし、メトリクスを取得しております。
このとき、aws.applicationelb.httpcode_elb_5xxメトリクスの値がNullであった場合、0埋めをするという挙動になっております。
それに対して、Metrics streamはKinesisから弊社のAPIにデータを送信するということになりますので、上記の仕様にはなっていないということかと存じます。

#### 対応策は2パターン
オススメは「デフォルト0埋め」です。

|  方法  |  説明  |  ドキュメント
| ---- | ---- | ---- |
|  デフォルト0埋め  |  メトリクスが存在しない箇所を自動的に0で埋めてくれる。Alert → OK の遷移が実際の状態に近い遅延で行われる。  |https://docs.datadoghq.com/ja/dashboards/functions/interpolation/|
|  Resolveタイマー  |  Alert になってから特定時間後に自動的に OK に遷移する。（実際のリソースが異常のままであれば、また Alert に戻る。） |https://docs.datadoghq.com/ja/monitors/monitor_status/|





# まとめ
CloudWatch メトリクスストリームを用いた Datadog メトリクス取得事例をご紹介しました。
アラーム発報までの時間が短縮されることで異常に素早く対応できるのでよかったです！！

# CFnテンプレート

```datadog-metric-streams.yml
AWSTemplateFormatVersion: "2010-09-09"
Parameters:
  GlobalPrefix:
    Type: 'String'
    Default: 'datadog-metric-streams'
  GlobalEnvironment:
    Type: 'String'
    Default: 'stg'
  ApiKey:
    Description: >-
      Your Datadog API Key
    Type: String
    AllowedPattern: .+
    ConstraintDescription: ApiKey is required
    NoEcho: true
  # [NOTE]
  # ストリームするメトリクスをフィルタしたい場合 -> Include/Exclude を指定の上、Namespaceに必要/不要な名前空間を入力
  # 全メトリクスを取得したい場合 -> Default:Includeのまま、FirstNamespace以降の入力は不要
  FilterMethod:
    Description: >-
      "Include" for an inclusion filter or "Exclude" for an exclusion filter for the following namespaces.
    Type: String
    Default: 'Include'
  # [NOTE]
  # Namespaceパラメータ不足の際には適宜追加すること
  # その際、ConditionsとResourcesの該当箇所への追記も忘れずに
  FirstNamespace:
    Description: >-
      A namespace to use for filtering. Leave blank if you do not need to filter by namespace.
    Type: String
    Default: ''
  SecondNamespace:
    Description: >-
      A namespace to use for filtering. Leave blank if you do not need to filter by namespace.
    Type: String
    Default: ''
  ThirdNamespace:
    Description: >-
      A namespace to use for filtering. Leave blank if you do not need to filter by namespace.
      If you need additional namespaces follow this link for additional templates.
    Type: String
    Default: ''
  DdSite:
    Type: String
    Default: datadoghq.com
    Description: Define your Datadog Site to send data to. For the Datadog EU site, set to datadoghq.eu
    AllowedPattern: .+
    ConstraintDescription: DdSite is required

Conditions:
  HasIncludeNamespace1: !And [ !Not [ !Equals [ !Ref FirstNamespace, '' ] ], !Equals [ !Ref FilterMethod, 'Include' ]]
  HasIncludeNamespace2: !And [ !Not [ !Equals [ !Ref SecondNamespace, '' ] ], !Equals [ !Ref FilterMethod, 'Include' ]]
  HasIncludeNamespace3: !And [ !Not [ !Equals [ !Ref ThirdNamespace, '' ] ], !Equals [ !Ref FilterMethod, 'Include' ]]
  HasExcludeNamespace1: !And [ !Not [ !Equals [ !Ref FirstNamespace, '' ] ], !Not [ !Equals [ !Ref FilterMethod, 'Include' ]]]
  HasExcludeNamespace2: !And [ !Not [ !Equals [ !Ref SecondNamespace, '' ] ], !Not [ !Equals [ !Ref FilterMethod, 'Include' ]]]
  HasExcludeNamespace3: !And [ !Not [ !Equals [ !Ref ThirdNamespace, '' ] ], !Not [ !Equals [ !Ref FilterMethod, 'Include' ]]]
  USDatacenter: !Equals [ !Ref DdSite, 'datadoghq.com' ]
  EUDatacenter: !Equals [ !Ref DdSite, 'datadoghq.eu' ]
  Staging: !Equals [ !Ref DdSite, 'datad0g.com' ]

Resources:
  # For Firehose
  ServiceRole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: !Sub "${GlobalPrefix}-${GlobalEnvironment}-datadog-service-role"
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: "Allow"
            Principal:
              Service:
                - "firehose.amazonaws.com"
            Action:
              - 'sts:AssumeRole'
      Path: /
      Policies:
        - PolicyName: "datadog_stream_s3_policy"
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: "Allow"
                Action:
                  - "s3:AbortMultipartUpload"
                  - "s3:GetBucketLocation"
                  - "s3:GetObject"
                  - "s3:ListBucket"
                  - "s3:ListBucketMultipartUploads"
                  - "s3:PutObject"
                Resource:
                  - !Sub "arn:aws:s3:::${GlobalPrefix}-${GlobalEnvironment}-backup"
                  - !Sub "arn:aws:s3:::${GlobalPrefix}-${GlobalEnvironment}-backup/*"

  # For CloudWatch Metric Streams
  DatadogMetricStreamRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${GlobalPrefix}-${GlobalEnvironment}-datadog-streams-role"
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - streams.metrics.cloudwatch.amazonaws.com
            Action:
              - "sts:AssumeRole"
      Path: /
      Policies:
        - PolicyName: "datadog_stream_firehose_policy"
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - "firehose:PutRecord"
                  - "firehose:PutRecordBatch"
                Resource:
                  - !Sub "arn:aws:firehose:*:${AWS::AccountId}:deliverystream/${GlobalPrefix}-${GlobalEnvironment}"
      Description: A metric stream role

  DatadogStreamLogs:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "${GlobalPrefix}-${GlobalEnvironment}"
      RetentionInDays: 14

  HTTPLogStream:
    Type: AWS::Logs::LogStream
    Properties:
      LogGroupName: !Ref DatadogStreamLogs
      LogStreamName: "http_endpoint_delivery"

  S3Backup:
    Type: AWS::Logs::LogStream
    Properties:
      LogGroupName: !Ref DatadogStreamLogs
      LogStreamName: "s3_backup"

  # 配信エラー時のバックアップ用
  DatadogStreamBackupBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub "${GlobalPrefix}-${GlobalEnvironment}-backup"
      AccessControl: 'Private'
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: 'AES256'
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True

  DatadogMetricKinesisFirehose:
    Type: AWS::KinesisFirehose::DeliveryStream
    Properties:
      DeliveryStreamName: !Sub "${GlobalPrefix}-${GlobalEnvironment}"
      DeliveryStreamType: "DirectPut"
      HttpEndpointDestinationConfiguration:
        BufferingHints:
          # [NOTE]
          # Datadog推奨値に設定
          # ref) https://www.datadoghq.com/ja/blog/amazon-cloudwatch-metric-streams-datadog/
          SizeInMBs: 4
          IntervalInSeconds: 60
        EndpointConfiguration:
          Url:
            !If
            - Staging
            - "https://awsmetrics-http-intake.datad0g.com/v1/input"
            - !If
              - EUDatacenter
              - "https://awsmetrics-intake.datadoghq.eu/v1/input"
              - "https://awsmetrics-intake.datadoghq.com/v1/input"
          Name: "Event intake"
          AccessKey: !Ref ApiKey
        CloudWatchLoggingOptions:
          Enabled: True
          LogGroupName: !Ref DatadogStreamLogs
          LogStreamName: "http_endpoint_delivery"
        RoleARN: !GetAtt ServiceRole.Arn
        RetryOptions:
          DurationInSeconds: 60
        S3BackupMode: "FailedDataOnly"
        S3Configuration:
          RoleARN: !GetAtt ServiceRole.Arn
          BucketARN: !GetAtt DatadogStreamBackupBucket.Arn
          ErrorOutputPrefix: "datadog_stream"
          BufferingHints:
            SizeInMBs: 4
            IntervalInSeconds: 60
          CompressionFormat: "GZIP"
          CloudWatchLoggingOptions:
            Enabled: True
            LogGroupName: !Ref DatadogStreamLogs
            LogStreamName: "s3_backup"
      Tags:
        - Key: "Team"
          Value: "aws-integration"
        - Key: "StreamAccountID"
          Value: !Ref "AWS::AccountId"
          
  DatadogMetricStreamAllNamespaces:
    Type: AWS::CloudWatch::MetricStream
    Properties:
      Name: !Sub "${GlobalPrefix}-${GlobalEnvironment}"
      FirehoseArn: !GetAtt DatadogMetricKinesisFirehose.Arn
      RoleArn: !GetAtt DatadogMetricStreamRole.Arn
      OutputFormat: "opentelemetry0.7"
      # IncludeFilters と ExcludeFilters は一方のみ指定（両方は不可）
      # Include も Exclude も指定しない場合は全てのメトリクスがストリームされる
      IncludeFilters:
        - !If
          - HasIncludeNamespace1
          - Namespace:
              !Ref FirstNamespace
          - !Ref 'AWS::NoValue'
        - !If
          - HasIncludeNamespace2
          - Namespace:
              !Ref SecondNamespace
          - !Ref 'AWS::NoValue'
        - !If
          - HasIncludeNamespace3
          - Namespace:
              !Ref ThirdNamespace
          - !Ref 'AWS::NoValue'
      ExcludeFilters:
        - !If
          - HasExcludeNamespace1
          - Namespace:
              !Ref FirstNamespace
          - !Ref 'AWS::NoValue'
        - !If
          - HasExcludeNamespace2
          - Namespace:
              !Ref SecondNamespace
          - !Ref 'AWS::NoValue'
        - !If
          - HasExcludeNamespace3
          - Namespace:
              !Ref ThirdNamespace
          - Ref: 'AWS::NoValue'

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
    - Label:
        default: Required
      Parameters:
        - GlobalPrefix
        - GlobalEnvironment
        - ApiKey
        - DdSite
    - Label:
        default: Optional
      Parameters:
        - FilterMethod
        - FirstNamespace
        - SecondNamespace
        - ThirdNamespace
```