LCL Engineers' Blog

バス比較なび・格安移動・バスとりっぷを運営する LCLの開発者ブログ

えっ、CloudWatchにRDSリードレプリカ数のメトリクスがない!?→自作しました

こんにちは。SREエンジニアのこばやし(id: kasei_san)です。

弊社サービス バス比較なび では、RDS Aurora にオートスケーリングを設定しています。

最近、この構成のメトリクスを見直していたのですが、その時点のリードレプリカ数が把握できず、ちょっと不便に感じました。

(例えば、リクエスト数が多いタイミングで、ちゃんとスケーリングが効いてレスポンスタイムが下がっているか? などを確認したいときなど)

そこで CloudWatch ダッシュボードにリードレプリカ数のグラフを出そうと、対応するメトリクスを探してみたのですが……見つからないんですね。

リードレプリカ数なんて、多くの人が気にしそうな項目ですし、「さすがにあるだろう」と思ってサポートに問い合わせてみたところ……

恐れ入りますが、AutoScaling で追加されたリーダーインスタンスの台数を報告するメトリクスはございません。

まさかの未サポート……!!!

というわけで、自分でメトリクスを取得する仕組みを作ることにしました。

やったこと

  • RDSのオートスケール時にEventBridgeでlambdaを実行
  • lambdaで、リードレプリカ数をCloudWatchMetricsに投げる(ついでにslackに通知)
  • CloudWatchMetricsの値をダッシュボードで表示

Terraformのコード

# autoscalingの生成/削除イベントを拾う
resource "aws_cloudwatch_event_rule" "rds_autoscale" {
  name = "rds-autoscale"
  event_pattern = jsonencode(
    {
      source = ["aws.rds"]
      detail = {
        EventCategories = ["creation", "deletion"]
        SourceIdentifier = [{
          prefix = "application-autoscaling-"
        }]
      }
    }
  )
}

# イベントとlambdaを紐づける
resource "aws_cloudwatch_event_target" "rds_autoscale" {
  rule = aws_cloudwatch_event_rule.rds_autoscale.name
  arn  = aws_lambda_function.rds_autoscale.arn
}

# 実行するlambda
resource "aws_lambda_function" "rds_autoscale" {
  function_name = "rds-autoscale"
  role          = aws_iam_role.rds_autoscale_lambda.arn
  handler       = "rds-autoscale.lambda_handler"
  runtime       = "ruby3.3"
  timeout       = 300

  filename         = data.archive_file.rds_autoscale_lambda.output_path
  source_code_hash = data.archive_file.rds_autoscale_lambda.output_base64sha256

  depends_on = [
    aws_cloudwatch_log_group.rds_autoscale_lambda
  ]

  tracing_config {
    mode = "Active"
  }

  environment {
    variables = {
      SLACK_WEBHOOK_URL = "https://hooks.slack.com/triggers/********"
    }
  }
}

# 一部割愛します

# lambdaにわたす権限
data "aws_iam_policy_document" "rds_autoscale_lambda" {

  # ログ
  statement {
    sid       = "CreateCloudWatchLogs"
    resources = ["arn:aws:sqs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:*"]

    actions = [
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:PutLogEvents"
    ]
  }

 # DBクラスタの情報を取得する権限
  statement {
    sid = "RdsDescribeDBClusters"
    # tfsec:ignore:AVD-AWS-0057
    resources = ["*"]

    actions = [
      "rds:DescribeDBClusters"
    ]
  }

 # CloudWatchMetricsに値を送る権限
  statement {
    sid = "PutCloudWatchMetrics"
    resources = ["*"]
    actions = [
      "cloudwatch:PutMetricData"
    ]
  }
}

lambdaのコード

require 'json'
require 'net/http'
require 'aws-sdk-rds'
require 'aws-sdk-cloudwatch'

def lambda_handler(event:, context:)
  puts "event: #{event.inspect}}"
  puts "context: #{context.inspect}"

  message = event.dig('detail', 'Message')
  autoscaling_resourceId = event.dig('detail', 'Tags', 'application-autoscaling:resourceId')

  # 現状のインスタンス数を確認して、chatbot経由で通知
  cluster_name = autoscaling_resourceId.split(':').last
  puts "cluster_name: #{cluster_name}"

  rds_client = Aws::RDS::Client.new
  response = rds_client.describe_db_clusters({ db_cluster_identifier: cluster_name })
  puts "response: #{response.inspect}"

  db_cluster = response.db_clusters.first
  db_cluster_members_count = 0
  if db_cluster
    db_cluster_members_count = db_cluster.db_cluster_members.count - 1
    puts "db_cluster_members_count: #{db_cluster_members_count}"

    # CloudWatchメトリクスに送信
    cloudwatch_client = Aws::CloudWatch::Client.new
    cloudwatch_client.put_metric_data({
      namespace: 'AWS/RDS',
      metric_data: [
        {
          metric_name: 'ClusterMembersCount',
          value: db_cluster_members_count,
          unit: 'Count',
          dimensions: [
            {
              name: 'DBClusterIdentifier',
              value: cluster_name
            }
          ]
        }
      ]
    })
  end

  response = slack_publish(cluster_name: cluster_name, message: message,
                           cluster_members_count: db_cluster_members_count)
  puts "slack_publish response: #{response.inspect}"

  { event: JSON.generate(event), context: JSON.generate(context.inspect) }
end

def slack_publish(cluster_name:, message:, cluster_members_count:)
  uri = URI(ENV['SLACK_WEBHOOK_URL'])
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true

  request = Net::HTTP::Post.new(uri.path, { 'Content-Type' => 'application/json' })
  request.body = {
    cluster_name: cluster_name,
    message: message,
    cluster_members_count: cluster_members_count.to_s
  }.to_json

  http.request(request)
end

以上で、RDSのリードレプリカ数がCloudWatchMetricsに格納されるようになります!

結果

CloudWatchのダッシュボードでも、グラフが確認できるようになりました!

レプリケーション発生時に値を送っているだけなので、ちょっと見づらいですが、今は実用上十分なのでこれでOKです。

これだと見づらい! という人は、cronでもレプリカの数を CloudWatchMetrics に送ると良いと思います。

まとめ

このように、「CloudWatchMetricsに値がない! 」という時にも諦めずに、メトリクスを自作すれば、グラフ化による可視化、その値をトリガーとしたイベントの発火など、自由に活用することが可能です。

弊社でも、ECS タスク内の Nginx リクエスト数を CloudWatch Metrics に渡し、一定のリクエスト数を超えたら自動でスケールするような構成を採用しています。

本記事が皆様の監視ライフのQOL向上の一助になりましたら幸いです。