Sensuを使ってCloudWatchのメトリクスを取得する方法

 2014/04/02
このエントリーをはてなブックマークに追加

全国200人くらいのSensuユーザーのみなさんこんにちは。 Sensuを使うと自分で自由に監視したりメトリクスデータの取得が可能ですし、他のシステムと組み合わせて監視を一元化することも可能です。 例えばAWSであれば標準でCloudWatchによる監視が行われますが、SensuとCloudWatchを別々に眺めるのは面倒なので、これを一元化する方法を紹介します。

おおまかな流れ

  1. Sensuでは監視結果やメトリクスの取得自体はSensu Client側で行われます。従ってチェック甩のスクリプトはクライアント側に配置します
  2. Sensu Server側では、あらかじめ監視やメトリクス取得の内容を定義します。その中では、監視の名前の定義、どのグループに属しているクライアントで実行するか(Subscriber)、どんなコマンドを実行するか、データを受け取った際にどのように処理するのか(Handler)を定義します
  3. 一方で、Sensu Client側では自分がどのようなチェックを実行するかについて、Subscription(購読)の設定を行い、Sensu Serverで定義されているチェックのうちそのSubscriptionに合致する処理を実行します

チェック用スクリプトを作成する

では早速チェック用のスクリプトを作成します。今回は例として、CloudWatchから指定したElastic Load Balancingのメトリクスデータを取得し、Sensu Serverに送信するようにしてみます。繰り返しになりますが、このスクリプト自体はSensu Client側に配置します。配置するパスは/etc/sensu/plugins/ 以下となります。ファイル名は基本的になんでも構いませんが、rubyスクリプトですので、拡張子は.rbにします。また単体で実行可能になるようにパーミッションを付与してください。

では、早速コードを見てみましょう(こちらにも置いてあります)。

#!/usr/bin/env ruby
#
# Retrieve All ELB metrics from CloudWatch
# ===
#
# Copyright 2014 Ryutaro YOSHIBA http://www.ryuzee.com/
#
# Released under the same terms as Sensu (the MIT license); see LICENSE
# for details.
#

require 'rubygems' if RUBY_VERSION < '1.9.0'
require 'sensu-plugin/metric/cli'
require 'aws-sdk'

# AllELBMetrics
class AllELBMetrics < Sensu::Plugin::Metric::CLI::Graphite
  option :scheme,
         description: 'Metric naming scheme, text to prepend to metric',
         short: '-s SCHEME',
         long: '--scheme SCHEME',
         default: 'ELB'

  option :region,
         short: '-r REGION',
         long: '--region REGION',
         description: 'AWS Region (such as ap-northeast-1).',
         default: 'ap-northeast-1'

  option :elb_name,
         description: 'ELB name to retrieve metrics from CloudWatch',
         short: '-n ELB_NAME',
         long: '--name ELB_NAME'

  option :fetch_age,
         description: 'How long ago to fetch metrics for',
         short: '-f AGE',
         long: '--fetch_age',
         default: 60,
         proc: proc { |a| a.to_i }

  option :duration,
         description: 'Duration to collect metrics data',
         short: '-d DURATION',
         long: '--duration',
         default: 60,
         proc: proc { |a| a.to_i }

  def run
    if config[:scheme] == ''
      graphite_root = "#{config[:elb_name]}"
    else
      graphite_root = config[:scheme]
    end

    # please confirm the link
    # http://docs.aws.amazon.com/ElasticLoadBalancing/latest/
    # DeveloperGuide/US_MonitoringLoadBalancerWithCW.html
    statistic_type = {
      'HealthyHostCount' => 'Average',
      'UnHealthyHostCount' => 'Average',
      'RequestCount' => 'Sum',
      'Latency' => 'Average',
      'HTTPCode_ELB_4XX' => 'Sum',
      'HTTPCode_ELB_5XX' => 'Sum',
      'HTTPCode_Backend_2XX' => 'Sum',
      'HTTPCode_Backend_3XX' => 'Sum',
      'HTTPCode_Backend_4XX' => 'Sum',
      'HTTPCode_Backend_5XX' => 'Sum',
      'BackendConnectionErrors' => 'Sum',
      'SurgeQueueLength' => 'Maximum',
      'SpilloverCount' => 'Sum'
    }

    end_time = Time.now - config[:fetch_age]
    start_time = end_time - config[:duration]

    begin
      AWS.config(
        cloud_watch_endpoint: "monitoring.#{config[:region]}.amazonaws.com"
      )

      statistic_type.each do |metric_name, statistics_type|
        metric = AWS::CloudWatch::Metric.new(
          'AWS/ELB',
          metric_name,
          dimensions: [{
            name: 'LoadBalancerName',
            value: config[:elb_name]
          }]
        )
        stats = metric.statistics(
          start_time: start_time,
          end_time: end_time,
          statistics: [statistics_type]
        )
        last_stats = stats.sort_by { |stat| stat[:timestamp] }.last
        unless last_stats.nil?
          output graphite_root + ".#{config[:elb_name]}.#{metric_name}",
                 last_stats[statistics_type.downcase.to_sym].to_f,
                 last_stats[:timestamp].to_i
        end
      end
    rescue Exception => e
      puts "Error: exception: #{e}"
      critical
    end
    ok
  end
end

順番に中身を説明していきます。

require 'rubygems' if RUBY_VERSION < '1.9.0'
require 'sensu-plugin/metric/cli'
require 'aws-sdk'

では必要なライブラリを読み込んでいます。上2行はRubyを使ってチェックスクリプトを作成する場合は必須のおまじないと思ってください。また今回はCloudWatchからデータを取得しますので、AWS SDK for Rubyを使用しますのでインストールが必要です(もちろんChefとか使うと良いです)。

なお、Sensu Client側でこのチェックスクリプトが実行される際は、Sensu Client側に組み込まれたRubyが利用されます。実体は/opt/sensu/embedded/bin/ruby となっています。 従って、AWS SDK for Rubyを利用する際は、単純にgemコマンドでインストールするのではなく、以下のようにしてください。またこのgemをインストールする際には、Native Extensionのビルドが走りますので、gccなど必要なツールをインストールしておいてください。Chefを使っている場合はコミュニティクックブックであるbuild-essentialを適用すればOKです。

/opt/sensu/embedded/bin/gem install aws-sdk

また、自分の端末で開発を行う場合は、SensuのRubyは使われないので、普通にaws-sdkをインストールするとともに、あわせて、sensu-pluginのgemもインストールしておいてください。

# AllELBMetrics
class AllELBMetrics < Sensu::Plugin::Metric::CLI::Graphite
  # … 略 …
  option :elb_name,
         description: 'ELB name to retrieve metrics from CloudWatch',
         short: '-n ELB_NAME',
         long: '--name ELB_NAME'
  # … 略 …

の箇所では、クラスの定義を行っています。今回はメトリクスデータの取得なので、Sensu::Plugin::Metric::CLI::Graphiteを継承したクラスを作成してください。クラス名は何でも構いません。またその後の箇所で、引数の定義が行われています。今回の例ではElastic Load Balancingの名前などが引数になります。もちろんデフォルト値の設定も可能です。

    statistic_type = {
      'HealthyHostCount' => 'Average',
      'UnHealthyHostCount' => 'Average',
      'RequestCount' => 'Sum',
      'Latency' => 'Average',
      'HTTPCode_ELB_4XX' => 'Sum',
      'HTTPCode_ELB_5XX' => 'Sum',
      'HTTPCode_Backend_2XX' => 'Sum',
      'HTTPCode_Backend_3XX' => 'Sum',
      'HTTPCode_Backend_4XX' => 'Sum',
      'HTTPCode_Backend_5XX' => 'Sum',
      'BackendConnectionErrors' => 'Sum',
      'SurgeQueueLength' => 'Maximum',
      'SpilloverCount' => 'Sum'
    }

の箇所では、CloudWatchからどのようなメトリクスを、どのような値の形式で取得するかを決めています。例えば、HealthyHostCountについては、正常に動作しているロードバランサー配下のAmazon EC2インスタンスの数を示していますが、これについては、平均値(Average)を取得しており、HTTPCode_Backend_2XXについては、EC2インスタンス側でHTTPステータスコード200を返した数ということで、合計値(Sum)を取得しています。値の形式については、SampleCount、Average、Sum、Minimum、Maximumの5種類が指定可能です。 それぞれのメトリクスを、どのような形で取得すべきか、またそれぞれのメトリクスが何を意味しているかについては、こちらを参照すると良いでしょう。

    end_time = Time.now - config[:fetch_age]
    start_time = end_time - config[:duration]

の箇所は、CloudWatchでデータを取得する際の対象時間(いつからいつまでのデータを取得するか)になります。

    begin
      AWS.config(
        cloud_watch_endpoint: "monitoring.#{config[:region]}.amazonaws.com"
      )

      statistic_type.each do |metric_name, statistics_type|
        metric = AWS::CloudWatch::Metric.new(
          'AWS/ELB',
          metric_name,
          dimensions: [{
            name: 'LoadBalancerName',
            value: config[:elb_name]
          }]
        )
        stats = metric.statistics(
          start_time: start_time,
          end_time: end_time,
          statistics: [statistics_type]
        )
        last_stats = stats.sort_by { |stat| stat[:timestamp] }.last
        unless last_stats.nil?
          output graphite_root + ".#{config[:elb_name]}.#{metric_name}",
                 last_stats[statistics_type.downcase.to_sym].to_f,
                 last_stats[:timestamp].to_i
        end
      end
    rescue Exception => e
      puts "Error: exception: #{e}"
      critical
    end
    ok
  end

そして最後の箇所ですが、aws-sdkの機能を使って、CloudWatchのメトリクスデータをループで回して全種類取得しています。 今回はaws-sdkを使っているため、このスクリプトを動かすSensu ClientのサーバがAmazon EC2であり、なおかつ起動時にIAM Roleを割り当ててAPIコールを可能にしていれば、アクセスキーやシークレットキーをスクリプト内に埋め込んだり引数として渡す必要はありません。これが出来ない場合は環境変数AWS_ACCESS_KEY_IDおよびAWS_SECRET_ACCESS_KEYを設定してあげる形になります。コード内にこれら秘密情報を記述するのは絶対に避けてください。 メトリクスには時系列で複数のデータが含まれることがあるため、最後のものを利用するようにしており、なおかつ、CloudWatchでは全てのデータが常に取得可能とは限らないため、データがない場合には処理をスキップするようになっています。 なお、Sensuでメトリクスデータを取得する場合は、所定の書式に従ってデータを出力する必要がありますが、その箇所が、outputの箇所になります。最後に特に問題がなければ、okを返します。

このスクリプトを単体で実行すると、以下のように出力されます。

$ ruby elb-cf-metrics.rb --name elb1 --region ap-northeast-1

ELB.elb1.HealthyHostCount     1.0     1396387500
ELB.elb1.UnHealthyHostCount     0.0     1396387500
ELB.elb1.RequestCount     13.0     1396387500
ELB.elb1.Latency     0.0023789772620567908     1396387500
ELB.elb1.HTTPCode_Backend_2XX     13.0     1396387500

Sensu Server側の設定

次にSensu Serverでチェックの定義を行います。 Sensu Server側の/etc/sensu/conf.d 以下に次の内容で、jsonファイルを作成してください。 commandの箇所の引数は、チェックする自分のELBに応じて名前を変更してください。 また複数のELBをチェックしたい場合は、これと同様のファイルを複数作成してください。

{
  "checks": {
    "elb-full-metrics-ryuzee": {
      "handlers": ["graphite"],
      "type": "metric",
      "command": "/etc/sensu/plugins/elb-cf-metrics --name elb1",
      "subscribers": ["aws"],
      "interval": 60
    }
  }
}

作成が終わったらSensu ServerとSensu APIを再起動してください。

Sensu Client側での設定

最後にSensu Client側の設定を行います。今回は単純に、awsというsubscriberのものをクライアント側で実行するようにします。通常のサーバ監視と違って、このチェックは沢山のサーバで行う必要はなく、1台のサーバで実行されれば十分なので、専用のサーバを作って、そこで外部連携系の監視を全部動かしてしまうのが分かりやすいかもしれません。

{
  "client": {
    "name": "sensu",
    "address": "10.0.90.254",
    "subscriptions": [
      "aws"
    ]
  }
}

設定が終わったらSensu Clientを再起動してください。 うまくいけば、/var/log/sensu/sensu-client.logに以下のように出力されるはずです(整形していますが実際は改行はありません)。

{
  "timestamp":"2014-04-01T21:49:10.490563+0000",
  "level":"info",
  "message":"publishing check result",
  "payload":{
    "client":"Sandbox",
    "check":{
      "name":"elb-cf-metrics",
      "issued":1396389008,
      "command":"/etc/sensu/plugins/elb-cf-metrics.rb --name elb1",
      "executed":1396388949,
      "output":"ELB.elb1.UnHealthyHostCount\t0.0\t1396388820\nELB.elb1.HealthyHostCount\t1.0\t1396388820\n\n","status":0,"duration":1.257
    }
  }
}

最後に

これと同じ考え方で、CloudWatchの様々なデータを持ってきて自前でグラフ化したり、もしくは通常の監視(OK/NG/WARNING)も行うことが可能になります。色々試してみると良いかもしれません。

 2014/04/02
このエントリーをはてなブックマークに追加

サイト内検索


著作

寄稿

Latest post: