ClowdwatchのメトリクスをBigQueryで永久保存する。

## まえがき
ClowdWatchはAmazonの各サービスの負荷状況（CPU使用率、IO使用量など…）などの情報を取得して表示するサービスです。
おおむね1分間隔ごとに取得できるので、急激な変化が生じて居ない時の負荷確認や記録には便利なのですが、ある欠点が一つあります。

欠点、それは… **過去の記録を鮮明に保存できないことです**
取得当時は1分間隔で保存されていた記録も、2週間経つと5分間隔でしか取得できなくなり、さらに2ヶ月経つと1時間ごと[^1] でしか取得できなくなります。負荷が高い状態の実績値や、万一の障害が発生した時の状況を思い出して振り返られるようにするためには、やはり1分間隔で永続化したいですね。

そこで、その保存用に今アツい（？）BigQueryを使ってみたいと思います。とりあえずBigQueryに入れておけば格安でデータの保存、クエリ等を使っての検索が出来ますし、データの送信もコマンドラインを使って自動化しやすいですね。
…ということで試してみました。

## バッチ処理でメトリクスを毎時取得し、BigQueryに送信してみる。

AWS CLIと bq コマンドラインを利用して、以下の手順で処理をしたいと思います
（今回はそれぞれのセットアップ方法は割愛させていただきます。）

1. AWS CLIを利用して各メトリクスの情報を取得する
2. jqを利用してjsonフォーマットに変換し、jsonファイルに追記・一時保存を行う
3. bq load コマンドを利用してjsonファイルからBigQueryにデータを転送する

各メトリクスを取得するスプリクト例

```shell:get_metrics.sh
#bin/bash
# 年月日時間、取得時間数を指定して実行する
# コマンド使用例： /bin/bash test.sh 2017 12 19 0 1

Y="$1"
M="$2"
D="$3"
H="${4#0}"
LEN=${5-1}

if [ $((H + $LEN)) -gt 24 ]; then
  echo 'Cannot determine date partition. UTC date must be the same.' >&2
  exit 1
fi

t_start=$(date +"%Y-%m-%dT%H:%M:%SZ" -d "$Y-$M-$D $H:00:00")
t_end=$(date +"%Y-%m-%dT%H:%M:%SZ" -d "$Y-$M-$D $H:00:00 $LEN hour")

bq_table_name="BigQueryのテーブル名を入れて下さい"
elb_names=("各LoadBalancerNameを入れて下さい")
ec2_autoscaling_group_names=("各AutoScalingGroupNameを入れて下さい")
rds_instance_ids=("各DBInstanceIdentifierを入れて下さい")
elasticache_cluster_ids=("各CacheClusterIdを入れて下さい")

# 例１：ELB metricsを取得する
out_name="$Y-$M-$D-$H-$LEN.json"
for elb_name in $elb_names; do
  for metric in RequestCount HTTPCode_Backend_5XX HTTPCode_ELB_5XX Latency HealthyHostCount UnHealthyHostCount; do
    stat=Sum
    [ $metric = "Latency" ] && stat=Average
    [ $metric = "HealthyHostCount" ] && stat=Minimum
    [ $metric = "UnHealthyHostCount" ] && stat=Maximum
    type=FLOAT

    aws cloudwatch get-metric-statistics --metric-name "$metric" \
      --start-time "$t_start" --end-time "$t_end" --period 60  \
      --namespace AWS/ELB --statistics $stat \
      --dimensions Name=LoadBalancerName,Value="$elb_name" | \
      jq -c '.Datapoints[] | {timestamp:.Timestamp, metric: "'$metric'", value:.'"$stat"', identifier_name: "'$elb_name'"}' >> $out_name
  done
done

# 例２：EC2のCPU使用率をAutoscalingグループ単位で取得する
for group_name in $ec2_autoscaling_group_names; do
  for metric in CPUUtilization; do
    stat=Average
    type=FLOAT

    aws cloudwatch get-metric-statistics --metric-name "$metric" \
      --start-time "$t_start" --end-time "$t_end" --period 60  \
      --namespace AWS/EC2 --statistics $stat \
      --dimensions Name=AutoScalingGroupName,Value="$group_name" | \
      jq -c '.Datapoints[] | {timestamp:.Timestamp, metric: "'$metric'", value:.'"$stat"', identifier_name: "'$group_name'"}' >> $out_name
  done
done

# 例３：RDSのメトリクスを取得する
for instance_id in $rds_instance_ids; do
  for metric in CPUUtilization WriteIOPS ReadIOPS WriteThroughput ReadThroughput DatabaseConnections WriteLatency ReadLatency DiskQueueDepth; do
    stat=Average
    type=FLOAT

    aws cloudwatch get-metric-statistics --metric-name "$metric" \
      --start-time "$t_start" --end-time "$t_end" --period 60  \
      --namespace AWS/RDS --statistics $stat \
      --dimensions Name=DBInstanceIdentifier,Value="$instance_id" | \
      jq -c '.Datapoints[] | {timestamp:.Timestamp, metric: "'$metric'", value:.'"$stat"', identifier_name: "'$instance_id'"}' >> $out_name
  done
done


# 例４：ElastiCacheのメトリクスを取得する
for cluster_id in $elasticache_cluster_ids; do
  for metric in CPUUtilization NetworkBytesIn NetworkBytesOut NewConnections CurrConnections; do
    stat=Average
    type=FLOAT

    aws cloudwatch get-metric-statistics --metric-name "$metric" \
      --start-time "$t_start" --end-time "$t_end" --period 60  \
      --namespace AWS/ElastiCache --statistics $stat \
      --dimensions Name=CacheClusterId,Value="$cluster_id" | \
      jq -c '.Datapoints[] | {timestamp:.Timestamp, metric: "'$metric'", value:.'"$stat"', identifier_name: "'$cluster_id'"}' >> $out_name
  done
done

bq load $args --source_format=NEWLINE_DELIMITED_JSON \
  "$bq_table_name\$$Y$M$D" $out_name \
  "timestamp:TIMESTAMP,identifier_name:STRING,metric:STRING,value:FLOAT"
rm -f $out_name
```

## 毎時自動実行する

上のスプリクトでは、時刻を指定してあげる必要があるので、毎時自動実行する場合は、現在時刻よりひとつ手前の時間帯（例：16:30なら15:00~16:00）の１時間のメトリクスを格納するように実行します。

```shell:date_exec.sh
DATE=${2-$(echo $(date "+%Y %m %d %H" -d "1 hour ago"))}
./get_metrics.sh $DATE
```

あとはこれの実行をcrontabに登録すればOKです。

## BigQuery側で保存用のテーブルを作成する。

DBのスキーマ構造は以下のようにしてみました。これならメトリクスやサーバーを増やしても構造を変更する必要が無いですし、特定のDB／メトリクスもselect文でよしなに取れて便利と思います。

| カラム名 | 型 | 内容 |
|:---|---|---|
| timestamp |TIMESTAMP | 時刻 |
| identifier_name | STRING | インスタンスIDなど識別子となる名前 |	
| metric | STRING | メトリクス名 |	
| value | FLOAT | 各メトリクスの値 |

## 気になる保存料金は…
メトリクスやインスタンスIDなどの識別子の長さにもよりますが、1レコードあたりのデータ量は約50バイトくらいに収まりそうです。
このとき、1ヶ月・1メトリクスあた保存量は 50×1440×30≒2.16MB になります。
ここでBigQueryの料金[^2] を見てみると…月10GBまで無料、それ以降は$0.02/GB/月のようです。
例えば各データを取りまくって合計1000メトリクス、1年分データを保存していたとしてもデータ量は25.9GBなので… **$0.318** という計算になるようです。
なので、お金のことはほぼ無視して大丈夫そうですねｗ

## おまけ：ついでにRedashで可視化してみる
社内では先輩の方々の尽力もありRedashによるクエリ実行結果の可視化もされていたので、それを利用してみました。

```SQL:クエリ
SELECT
  *
FROM
  !BigQuery_table_name! AS master
WHERE
  master._partitiontime BETWEEN TIMESTAMP('{{start_date}}')
  AND TIMESTAMP('{{end_date}}')
  AND metric = '{メトリクス名}'
```

これをVisualizationを使って以下のように設定することで、各識別子ごとにメトリクスがGroup Byされるので、一つのグラフに各サーバーのメトリクスをまとめて出せるようになりました！

- Generalタブ：
  - Chart Type: LINE
  - X Column: timestamp
  - Y Column: value
  - Group by: identifier_name
- X Axisタブ:
  - scale: Datetime
- Y Axisタブ:
  - scale: Linear


## 注記
[^1]: https://aws.amazon.com/jp/about-aws/whats-new/2016/11/cloudwatch-extends-metrics-retention-and-new-user-interface/
[^2]: https://cloud.google.com/bigquery/pricing?hl=ja#storage