Terraform で CloudWatch の監視設定をコード化する
はじめに
Terraform はコードを書くだけであれば結構簡単です。ただし、コードを適切に管理するためには検討すべきことがたくさんあります。いきおいで IaC を導入するとコードのメンテナンスが疎かになり、管理不全に陥ってしまうかもしれません。
というわけで IaC を導入する場合はシステム全体を一度にコード化するよりも、スモールスタートする方が安全です。まずは、万が一しくじってもサービス影響が出にくい運用機能で PoC してみるのがよいと思います。
今回は CloudWatch のさまざまな監視設定について、パターン毎に Terraform のサンプルコードを紹介します。
これから IaC に取り組むみなさまの参考になれば幸いです👍
Terraform 実装例
こんな感じの構成になります。
メトリクス監視
あるメトリクスが閾値を上回った or 下回った場合にアラートを生成します。CPU やメモリのように上限のあるリソースの使用率を監視する場合などに使用されます。Warning と Error で 2 段階程度の閾値を設けることが一般的です。
CloudWatch Alarm はひとつの閾値しか設定できませんが、今回は Module を使用することで対応しています。また、必要な設定値を Map 型変数のcloudwatch_metric_alarms
にまとめました。main.tf
ではfor_each
を使用することで要素の数だけ処理を繰り返します。こういう作りにしておくと、監視設定の追加が必要になったときはvariables.tfvars
に要素を追加するだけで対応できるので便利です。
Sample code
cloudwatch_metric_alarms = {
ecs_service_xxxx_cpu_usage = {
warning_name = "CPU usage of ECS Service - xxxx has exceeded the warning threshold."
warning_threshold = "80"
error_name = "CPU usage of ECS Service - xxxx has exceeded the error threshold."
error_threshold = "90"
comparison_operator = "GreaterThanThreshold"
evaluation_period = "5"
period = "60"
datapoint = "5"
metric = {
name = "CPUUtilization"
namespace = "AWS/ECS"
statistic = "Average"
}
dimensions = {
ServiceName = "xxxx",
ClusterName = "xxxx"
}
action_isenabled = true
}
ecs_service_xxxx_memory_usage = {
warning_name = "Memory usage of ECS Service - xxxx has exceeded the warning threshold."
warning_threshold = "80"
error_name = "Memory usage of ECS Service - xxxx has exceeded the error threshold."
error_threshold = "90"
comparison_operator = "GreaterThanThreshold"
evaluation_period = "5"
period = "60"
datapoint = "5"
metric = {
name = "MemoryUtilization"
namespace = "AWS/ECS"
statistic = "Average"
}
dimensions = {
ServiceName = "xxxx",
ClusterName = "xxxx"
}
action_isenabled = true
}
}
module "cloudwatch_metric_alarms" {
source = "modules/cloudwatch_metric_alarms"
for_each = var.cloudwatch_metric_alarms
warning_name = each.value["warning_name"]
warning_threshold = each.value["warning_threshold"]
error_name = each.value["error_name"]
error_threshold = each.value["error_threshold"]
comparison_operator = each.value["comparison_operator"]
evaluation_period = each.value["evaluation_period"]
period = each.value["period"]
datapoint = each.value["datapoint"]
metric = each.value["metric"]
dimensions = each.value["dimensions"]
action_isenabled = each.value["action_isenabled"]
sns_topic_arn = "arn:aws:sns:ap-northeast-1:123456789012:MyTopic"
}
resource "aws_cloudwatch_metric_alarm" "warning" {
alarm_name = var.warning_name
threshold = var.warning_threshold
comparison_operator = var.comparison_operator
evaluation_periods = var.evaluation_period
metric_name = var.metric["name"]
namespace = var.metric["namespace"]
statistic = var.metric["statistic"]
period = var.period
dimensions = var.dimensions
datapoints_to_alarm = var.datapoint
actions_enabled = var.action_isenabled
alarm_actions = [var.sns_topic_arn]
ok_actions = [var.sns_topic_arn]
}
resource "aws_cloudwatch_metric_alarm" "error" {
alarm_name = var.error_name
threshold = var.error_threshold
comparison_operator = var.comparison_operator
evaluation_periods = var.evaluation_period
metric_name = var.metric["name"]
namespace = var.metric["namespace"]
statistic = var.metric["statistic"]
period = var.period
datapoints_to_alarm = var.datapoint
dimensions = var.dimensions
actions_enabled = var.action_isenabled
alarm_actions = [var.sns_topic_arn]
ok_actions = [var.sns_topic_arn]
}
計算式を使用したメトリクス監視
基本的にはメトリクス監視と同じですが、メトリクスをそのまま監視対象とするのではなく、数式を使用して計算した値を評価します。ユースケースは RDS のメモリ使用率の監視です。RDS は標準メトリクスとしてメモリ使用率(%)を報告をしてくれないので、実装するには工夫が必要です。具体的には以下の手順を踏みます。
- RDS の拡張モニタリングを有効化し、メトリクスフィルターを使用してログから総メモリ容量を取得する。
- 総メモリ容量と標準メトリクスとして報告される空きメモリ容量(
FreeableMemory
)からメモリ使用率(%)を算出する。
詳細は以下のブログがとても丁寧に解説されてますのでリンクさせていただきます🙇♂️
メトリクス監視と同様に 2 段階閾値の対応で Module を使っているため、この Module にメトリクスフィルター設定を追加すると使い回しが難しくなってしまいます。Module の汎用性を考慮して、今回はメトリクスフィルターを Module の外側で設定しました。
Sample code
cloudwatch_metric_math_alarms = {
rds_xxxx_disk_usage = {
warning_name = "Memory usage of RDS - xxxx has exceeded the warning threshold."
warning_threshold = "80"
error_name = "Memory usage of RDS - xxxx has exceeded the error threshold."
error_threshold = "90"
comparison_operator = "GreaterThanThreshold"
evaluation_period = "5"
period = "60"
datapoint = "5"
metrics = {
m1 = {
id = "m1"
name = "TotalMemory"
namespace = "Custom/RDS"
statistic = "Average"
dimensions = {
DBInstanceIdentifier = "xxxx"
}
},
m2 = {
id = "m2"
name = "FreeableMemory"
namespace = "AWS/RDS"
statistic = "Average"
dimensions = {
DBInstanceIdentifier = "xxxx"
}
}
}
expression = "(m1 - m2 / 1024) / m1 * 100"
expression_label = "Memory Usage"
action_isenabled = true
}
}
resource "aws_cloudwatch_log_metric_filter" "rds_xxxx_total_memory" {
name = "rds_xxxx_total_memory"
log_group_name = "RDSOSMetrics"
pattern = <<-EOT
{ ($.instanceID="xxxx") && ($.memory.total = *) }
EOT
metric_transformation {
name = "TotalMemory"
namespace = "Custom/RDS"
value = "$.memory.total"
unit = "Kilobytes"
dimensions = {
DBInstanceIdentifier = "$.instanceID"
}
}
}
module "cloudwatch_metric_math_alarms" {
source = "modules/cloudwatch_metric_math_alarms"
for_each = var.cloudwatch_metric_math_alarms
warning_name = each.value["warning_name"]
warning_threshold = each.value["warning_threshold"]
error_name = each.value["error_name"]
error_threshold = each.value["error_threshold"]
comparison_operator = each.value["comparison_operator"]
evaluation_period = each.value["evaluation_period"]
period = each.value["period"]
datapoint = each.value["datapoint"]
metrics = each.value["metrics"]
expression = each.value["expression"]
expression_label = each.value["expression_label"]
action_isenabled = each.value["action_isenabled"]
sns_topic_arn = "arn:aws:sns:ap-northeast-1:123456789012:MyTopic"
}
resource "aws_cloudwatch_metric_alarm" "warning" {
alarm_name = var.warning_name
threshold = var.warning_threshold
comparison_operator = var.comparison_operator
evaluation_periods = var.evaluation_period
datapoints_to_alarm = var.datapoint
actions_enabled = var.action_isenabled
alarm_actions = [var.sns_topic_arn]
ok_actions = [var.sns_topic_arn]
metric_query {
id = "e1"
expression = var.expression
label = var.expression_label
return_data = "true"
}
dynamic "metric_query" {
for_each = var.metrics
content {
id = metric_query.value["id"]
metric {
metric_name = metric_query.value["name"]
namespace = metric_query.value["namespace"]
period = var.period
stat = metric_query.value["statistic"]
dimensions = metric_query.value["dimensions"]
}
}
}
}
resource "aws_cloudwatch_metric_alarm" "error" {
alarm_name = var.error_name
threshold = var.error_threshold
comparison_operator = var.comparison_operator
evaluation_periods = var.evaluation_period
datapoints_to_alarm = var.datapoint
actions_enabled = var.action_isenabled
alarm_actions = [var.sns_topic_arn]
ok_actions = [var.sns_topic_arn]
metric_query {
id = "e1"
expression = var.expression
label = var.expression_label
return_data = "true"
}
dynamic "metric_query" {
for_each = var.metrics
content {
id = metric_query.value["id"]
metric {
metric_name = metric_query.value["name"]
namespace = metric_query.value["namespace"]
period = var.period
stat = metric_query.value["statistic"]
dimensions = metric_query.value["dimensions"]
}
}
}
}
ログ監視
ログに含まれる特定の文字列を検知してアラートを生成します。
CloudWatch Alarm でログ監視を行うにはメトリクスフィルターを使用するため、aws_cloudwatch_log_metric_filter
とaws_cloudwatch_metric_alarm
の 2 つの Resource をひとつの Module にまとめました。
メトリクスフィルターのパターンは書き方が結構特殊です。いつも忘れてしまうのでマニュアルのリンクを貼っておきます。
ログ監視も最終的にはメトリクスを評価することになりますが、リソースの使用量を監視するのとは少し性質が違うのでその辺りを調整します。
Sample code
cloudwatch_log_alarms = {
ec2_xxxx_var_log_messages = {
name = "Error detected in /var/log/messages on EC2 - xxxx."
threshold = "0"
comparison_operator = "GreaterThanThreshold"
evaluation_period = "1"
period = "60"
datapoint = "1"
metric = {
name = "error"
namespace = "Custom/Logs"
statistic = "Sum"
}
filter_name = "ec2_xxxx_var_log_messages"
filter_pattern = "error"
log_group_name = "ec2_xxxx_var_log_messages"
action_isenabled = true
}
}
module "cloudwatch_log_alarms" {
source = "modules/cloudwatch_log_alarms"
for_each = var.cloudwatch_log_alarms
name = each.value["name"]
threshold = each.value["threshold"]
comparison_operator = each.value["comparison_operator"]
evaluation_period = each.value["evaluation_period"]
period = each.value["period"]
datapoint = each.value["datapoint"]
metric = each.value["metric"]
filter_name = each.value["filter_name"]
filter_pattern = each.value["filter_pattern"]
log_group_name = each.value["log_group_name"]
action_isenabled = each.value["action_isenabled"]
sns_topic_arn = "arn:aws:sns:ap-northeast-1:123456789012:MyTopic"
}
resource "aws_cloudwatch_log_metric_filter" "this" {
name = var.filter_name
log_group_name = var.log_group_name
pattern = var.filter_pattern
metric_transformation {
name = var.metric["name"]
namespace = var.metric["namespace"]
value = "1"
unit = "Count"
}
}
resource "aws_cloudwatch_metric_alarm" "this" {
alarm_name = var.name
threshold = var.threshold
comparison_operator = var.comparison_operator
evaluation_periods = var.evaluation_period
metric_name = var.metric["name"]
namespace = var.metric["namespace"]
statistic = var.metric["statistic"]
period = var.period
datapoints_to_alarm = var.datapoint
treat_missing_data = "notBreaching"
actions_enabled = var.action_isenabled
alarm_actions = [var.sns_topic_arn]
}
イベント監視
システムから報告される特定のイベントを検知してアラートを生成します。
aws_cloudwatch_event_rule
とaws_cloudwatch_event_target
の 2 つの Resource を使用するのでこちらも Module でまとめました。
pattern
やinput_paths
、input_template
の書き方は以下を参考にしてください。
通知されるイベントの形式がさまざまなので、variables.tfvars
がかなり見づらくなってしまいました😥
正直この辺りは Management Console から設定する方が圧倒的にわかりやすいです。Management Console ではサンプルイベントと照らし合わせて動作確認しながら設定できますが、Terramform にはそういった機能はありません。目当てのイベントが条件にマッチするか確認するために、結局 Management Console から確認したりすることが多いです。思い切ってイベント監視はコード化しないというのもアリかもしれません。
Sample code
cloudwatch_event_alarms = {
aws_health_event = {
name = "aws_health_event"
pattern = <<-EOT
{
"detail-type": ["AWS Health Event"],
"source": ["aws.health"]
}
EOT
input_paths = {
"detail":"$.detail-type",
"eventTypeCode": "$.detail.eventTypeCode",
"time":"$.time",
"region":"$.region"
}
input_template = <<-EOT
"A monitored event has occurred."
"---"
"Event Type: <detail>"
"Event Type Code: <eventTypeCode>"
"Event Time: <time>"
"Event Region: <region>"
EOT
action_isenabled = true
}
rds_instance_event = {
name = "rds_instance_event"
pattern = <<-EOT
{
"detail-type": ["RDS DB Instance Event"],
"source": ["aws.rds"],
"detail": {
"EventCategories": [
"availability",
"failover",
"deletion",
"failure",
"notification",
"recovery"
],
"SourceType": ["DB_INSTANCE"]
}
}
EOT
input_paths = {
detail = "$.detail-type",
resource = "$.resources",
message = "$.detail.Message"
}
input_template = <<-EOT
"A monitored event has occurred."
"---"
"Event Type: <detail>"
"Event Resource: <resource>"
"Event Message: <message>"
EOT
action_isenabled = true
}
}
module "cloudwatch_event_alarms" {
source = "../../modules/cloudwatch_event_alarms"
for_each = var.cloudwatch_event_alarms
name = each.value["name"]
pattern = each.value["pattern"]
input_paths = each.value["input_paths"]
input_template = each.value["input_template"]
action_isenabled = each.value["action_isenabled"]
sns_topic_arn = "arn:aws:sns:ap-northeast-1:123456789012:MyTopic"
}
resource "aws_cloudwatch_event_rule" "this" {
name = var.name
event_pattern = var.pattern
is_enabled = var.action_isenabled
}
resource "aws_cloudwatch_event_target" "this" {
rule = aws_cloudwatch_event_rule.this.name
arn = var.sns_topic_arn
input_transformer {
input_paths = var.input_paths
input_template = var.input_template
}
}
おわりに
何年か前に IaC が流行し出した頃から Terraform は触っていますが、当時よりベストプラクティスもある程度整理されてきて取っ付きやすくなってきたように思います。
特に以下のドキュメントは GCP を使用していなくても一読する価値があります。オススメです。
もちろんプロジェクトによっては IaC を採用しないケースもありますが、選択肢として持っておくためにも情報のキャッチアップは続けていきたいと思っています。自分の中で整理できた内容をこんな形で少しずつアウトプットしていきたいです👍
Discussion