AWS CloudWatch からの Zabbix 連携

# AWS CloudWatch からの Zabbix 連携
みなさんこんにちは。ふりっぱぁです。Zabbix、活用できていますか？
私はと言うと、新バージョンとなる3.0.xや3.2.xの流れについていくことがなかなか厳しく、未だに2.0.xを利用している状態だったりします(汗)

さて、個人でも手軽に始められるクラウドサービスの代表格となってしまったAWS、EC2やRDS等、さまざまなサービスが提供され、皆さん便利に活用されているかと思います。
今回は、その中でも、稼働監視やパフォーマンス監視に利用できるサービス、AWS CloudWatchが収集してくれている項目(メトリクス)のデータを、Zabbixと連携させるネタで行ってみようと思います。

既に多くの方々がブログ等で、CloudWatchのメトリクス値を取り込む工夫を公開されていますが、実際に使う過程において、少しでもスッキリさせられないかと考えてみました。

## 本記事の想定する環境
* Zabbix 3.2.2-1 (ZABBIX SIA rpm パッケージ配布版)
* CentOS 7.3.1611
* apache httpd / mariadb / php / ruby 等は CentOS7 標準構成
    * ruby gems (`gem install xxxx` でインストール)
        * zabbixapi(2.4.9)
        * open4(1.3.4)
        * right_aws(3.1.0)

## よく見かける構成と、その問題点
CloudWatchのメトリクス1項目に対し、UserParameterやexternalscriptsの1アイテムとして設定し、Zabbixで収集させる

1. アイテムの設定が煩雑<br>
CloudWatchでは1インスタンスあたりで10を超える数のメトリクスが提供されています。<br>
1項目を1アイテムとしてZabbixに設定するとなると、個々の項目に関する設定をWebUIから入れる必要があり、結構煩雑です。
1. 外部スクリプトの実行回数が多くなる<br>
CloudWatchからデータを取り込むためのスクリプトを、**インスタンス数ｘメトリクス数** の回数だけ実行させなければいけません。<br>
インスタンス数が少ないうちは良いのですが、ソコソコの数になってくると、zabbix_server 側の poller プロセスのビジー率が上がってしまい、データの取込がモタついたりすることもあります。
1. zabbix_agentd のタイムアウト<br>
zabbix_agentdから外部のスクリプトを実行させるような構成とした場合、zabbix_agentd.confの**Timeout**パラメータの設定値を超えるような時間が掛かってしまうと、タイムアウトとなってしまって、結果値の取込に失敗してしまいます。<br>
もちろん設定最大値となる30秒を超える時間が掛からなければ良いわけですが、ネットワーク等、周囲の環境によっては起こりうるわけで、対処できれば良いですよね。
1. 最新のデータしか取り込めない<br>
外部スクリプトとして実行した場合、その時点でCloudWatchから得た最新値を、その時点の値として登録することしかできません。<br>
各仮想ホスト内部のステータス情報はzabbix_agentdを用いて収集しているとしても、CloudWatchに蓄積された値を取り込むために、リアルタイムの値に束縛されるのは避けたいし、連携させようと思い立った時点で「CloudWatchの履歴値」として保存されている過去の値データも、可能ならばZabbix側へ取り込んでしまいたい

## 今回めざす構成 - CloudWatch からの取込スクリプト
1. 煩雑なアイテム設定等はしたくない<br>
可能な限り、各インスタンス単位等のアイテム設定は弄りたくない。
1. 1回の実行で、全部片付けてしまいたい<br>
EC2インスタンスが複数ある場合でも、1回の実行でとにかく全部済ませてしまいたい。
1. インスタンスタイプ毎に若干異なる構成にフィットさせたい<br>
インスタンスタイプ毎に異なることがあるメトリクス構成や、EBSのストレージに関する情報も、インスタンス情報の一部として取り込んでしまいたい
1. 複数のAWSアカウントをまとめて処理<br>
監視対象ホストごとに異なるAWSアカウントが混在しており、まとめて管理したい

そこで、**テンプレート**・**Zabbix API**・**ホストマクロ**・**zabbix_sender** を組み合わせ利用して、一回の実行だけで、一括してCloudWatchのデータをZabbixへ登録するようなrubyスクリプトを作成しました。

## 事前にZabbixサーバへ投入する設定
### 1. AWS のアカウント情報を登録するための、ダミーとなる**ホスト**
AWSの機能を呼び出すためには、**ACCESS_KEY**・**SECRET_KEY**と、2つのキー情報が必要となります。
これをスクリプト本体に直接記載してしまっても良いのですが、誰かが盗み見てしまうかもしれません。
そこで、スクリプトには直接記載せず、**Zabbixのホストマクロ**として登録することを考えてみます。

監視対象としたいホストに対するホストマクロとして、これらの値を設定する場合、WebUIで当該ホストに対する**読み書き権限**を持っているZabbixユーザ全員が、その内容を参照・編集できてしまいます。
また、監視対象としたいホストが多い場合、すべてのホストに対して、これらキー情報を登録しなければいけません。
そこで、**ZabbixのWebUIからダミーとなるホストを1つ登録し、そのホストに対するマクロとしてキー情報を登録**することにします。

この**ダミーのホスト**に対しては、スクリプト内部からZabbix API経由でアクセスしてキー情報を得るZabbixユーザのみが権限を持っていれば良いため、ある程度、情報を隠匿することが可能です。

新規ホストとして作成・登録し、AWSアカウントが複数ある場合には、それらが識別できるようなホスト名を付与します。
画像で示すように、2つのキー情報を、ホストに対するマクロ設定として設定します。

![ss022.png](https://qiita-image-store.s3.amazonaws.com/0/102016/54923e35-e6c4-cd5e-34cb-59a3e4bb9759.png)

### 2. CloudWatch からの情報連携を行うホストに適用する**テンプレート**
各インスタンスを示すホストに適用し、スクリプトから値を投げ込むターゲットを特定するための**テンプレート**を作成します。
このテンプレートを適用したホストが、CloudWatchからの値を連携したいホスト、ということになります。
ほとんどの設定はスクリプト内部からZabbix APIを利用して作成してしまうため、ここで行う設定は**ほんの少し**です。

1. 新規テンプレート「**T_AWS_EC2**」を作成・登録します
2. CloudWatchから取り込むデータを保管する**アイテム**を所属させるため、**アプリケーション**「**EC2**」を設定します。

![ss001.png](https://qiita-image-store.s3.amazonaws.com/0/102016/c721271b-4b55-9a8a-0de4-1e205ec0fe78.png) ![ss002.png](https://qiita-image-store.s3.amazonaws.com/0/102016/1eef67c6-10c9-b993-70e9-d80a42f96326.png)

### 3. 監視対象としたい **インスタンス** に対応する **ホスト**
インスタンス内部のゲストOSにzabbix_agentdをインストールして、通常のZabbix監視を実施しているならば、特に意識することはありません。
既に設定済の**ホスト**を、ほぼそのまま利用することが可能です。

1. 先の手順(2.)で作成した「**T_AWS_EC2**」テンプレートを適用します
1. ホストに対して、先の手順(1.)で作成した関連AWSアカウント情報を含むホストの名称を、マクロとして設定します
1. ホストに対して、AWS上でEC2インスタンスに関連付けられている「**Name**」タグの値を、マクロとして設定します

![ss031.png](https://qiita-image-store.s3.amazonaws.com/0/102016/7574090c-78ef-c6a1-e97e-d9e65e38e0e0.png)

## スクリプトの配置と実行
スクリプトは、cronでの実行を基本と考えて実装しています。
上で示したような設定を施した全てのホストに関するCloudWatchのデータを、たった1度の実行で一括で取り込んでしまうことから、Zabbixの特定ホストに紐付くものではなくなってしまったためです。
Zabbixサーバとなっているホストに導入してしまうのが簡単で良いかと思います。

CloudWatchから得た値の格納先となる**Zabbixのアイテム**を、実行時にスクリプト内部からZabbix APIで作成しています。
そのため、初回の実行時や、インスタンスの構成が変化した時には、若干、実行に時間が掛かります。
特に引数は必要ありません。```ruby``` コマンドにスクリプトファイル名を与えて実行するだけとなります。

```bash
user ~$ ruby aws_ec2info.rb
```

cronに設定する場合には、以下のような感じになるでしょうか。

```bash
15 * * * * zabbix ruby /opt/zabbix/scripts/aws_ec2info.rb </dev/null >/dev/null 2>&1
```

### 処理のおおまかな流れ
1. Zabbix APIを利用してZabbixへ接続
1. **T_AWS_EC2**テンプレートを探す
1. **T_AWS_EC2**テンプレートが適用された「CloudWatch連携対象ホスト」を探す
1. 「CloudWatch連携対象ホスト」に設定されたホストマクロの値から「Nameタグの値」を得る
1. 「CloudWatch連携対象ホスト」に設定されたホストマクロの値から「AWSアカウント情報ホスト」の名称を得る
1. 「AWSアカウント情報ホスト」を探す
1. 「AWSアカウント情報ホスト」に設定されたホストマクロの値から「ACCESS_KEY/SECRET_KEY」を得る
1. 「ACCESS_KEY/SECRET_KEY」と「Nameタグの値」からAWSへアクセスし、関連する期間のCloudWatchメトリクス値を得る
1. 「CloudWatch連携対象ホスト」に、メトリクス値を保存するためのアイテム設定が無い場合、APIを用いて作成する
1. アイテム設定を作成した場合、Serverの設定がリフレッシュされるまで、少しsleepする
1. 「CloudWatch連携対象ホスト」の収集値として、zabbix_senderでデータを投げ込む

## 実行すると‥
下図のように、EC2・EBSのメトリクス値が、対象ホストのアイテムとして登録されます。
EBSを利用したストレージをたくさんアタッチしているようなインスタンスでは、その個数だけの項目が生成されている‥はずです。

![ss011.png](https://qiita-image-store.s3.amazonaws.com/0/102016/8eae51af-c185-864d-eb9a-b2cea02a6ff0.png)

#### スクリプトのソースコード
以下、ソースコードです。

```ruby:aws_ec2info.rb
#!/bin/env ruby -Ku
# -*- encoding: utf-8 -*-

begin
  require 'rubygems'
  rescue LoadError
end
require 'logger'
require 'pp'
require 'zabbixapi'
require 'open4'
require 'right_aws'
require 'time'

SUCCESS = 0
FAILURE = 1

SELF_EXT  = File.extname(__FILE__)
SELF_BASE = File.basename(__FILE__, SELF_EXT)

LOGFILE = "/var/log/#{SELF_BASE}.log"

class AWSEC2Info
  attr_accessor(:logger)
  attr_accessor(:zbxapi)
  attr_accessor(:EC2Info)

  ZABBIX_API_URL = 'http://%s/zabbix/api_jsonrpc.php'
  ZABBIX_LOGINID = 'Admin'
  ZABBIX_PASSWORD = 'zabbix'
  ZABBIX_SENDER_PATH = '/usr/bin/zabbix_sender'
  ZABBIX_SERVER_CONF_PATH = '/etc/zabbix/zabbix_server.conf'
  ZABBIX_AGENTD_CONF_PATH = '/etc/zabbix/zabbix_agentd.conf'
  ZABBIX_ATTEMPTS_MAX = 3
  ZABBIX_ATTEMPTS_WAIT = 1
  ZABBIX_TEMPLATE_NAME = 'T_AWS_EC2'
  ZABBIX_ITEM_KEY_FORMAT_EC2 = 'user.ec2.info[%s]'
  ZABBIX_ITEM_KEY_FORMAT_EBS = 'user.ec2.info[%s,%s]'
  ZABBIX_ITEM_TYPE_TRAPPER = 2
  ZABBIX_ITEM_STATUS_ACTIVE = 0
  ZABBIX_ITEM_VALUE_TYPE_FLOAT = 0
  ZABBIX_ITEM_DELAY = 0
  ZABBIX_ITEM_HISTORY = 30
  ZABBIX_ITEM_TRENDS = 365
  ZABBIX_ITEM_APPNAME = 'EC2'
  ZABBIX_ITEM_CREATED = 0
  ZABBIX_ITEM_UPDATED = 1

  MACROKEY_AWS_ACCESS_KEY    = '{$ACCESS.KEY}'
  MACROKEY_AWS_SECRET_KEY    = '{$SECRET.KEY}'
  MACROKEY_AWS_ACCOUNT_HOST  = '{$ACCOUNT.HOST}'
  MACROKEY_EC2_INSTANCE_NAME = '{$INSTANCE.NAME}'

  # AWS EC2 GetData
  AWS_EC2_SERVER = 'ec2.ap-northeast-1.amazonaws.com'
  TAGKEY_NAME    = 'Name'

  # AWS CloudWatch GetData
  AWS_ACW_SERVER    = 'monitoring.ap-northeast-1.amazonaws.com'
  GETDATA_EC2_NAMESPACE = 'AWS/EC2'
  GETDATA_EBS_NAMESPACE = 'AWS/EBS'
  #  収集データサンプルの時間間隔 : 300 sec
  GETDATA_PERIOD    = 300
  #  収集データの時間範囲         : 2 時間分
  GETDATA_RANGE     = (60 * 60 * 2)
  #  収集データの時間基準点       : 現在時刻
  GETDATA_OFFSET    = (60 * 60 * 24 * 0)
  GETDATA_ENDTIME   = (Time.now.utc - GETDATA_OFFSET)
  GETDATA_STARTTIME = (GETDATA_ENDTIME - GETDATA_RANGE)

  # statistics : for use, xxx.downcase.to_sym
  GETDATA_STAT_AVG = 'Average' # :average
  GETDATA_STAT_MIN = 'Minimum' # :minimum
  GETDATA_STAT_MAX = 'Maximum' # :maximum
  GETDATA_STAT_SUM = 'Sum'     # :sum
  GETDATA_UNIT_MAP = {
    'Percent'      => '%',
    'Bytes'        => 'B',
    'Bytes/Second' => 'Bps',
    'Seconds'      => 's'
  }
  GETDATA_NAME_UNKNOWN_EC2 = 'CloudWatch EC2 項目 [%s]'
  GETDATA_NAME_UNKNOWN_EBS = 'CloudWatch EBS 項目 [%s,%s]'
  # name       => Monitor item display name
  # statistics => Value statistics
  #               default : GETDATA_STAT_AVG
  # unit       => Value custom unit
  #               default : '' <empty string>
  #               if exists AWS unit result in GETDATA_UNIT_MAP, then determine by map.
  #               if set this element, forcibly use it.
  GETDATA_MEASURES = {
    # EC2
    'CPUUtilization'             => { :name => 'CloudWatch CPU 使用率' },
    'CPUCreditBalance'           => { :name => 'CloudWatch CPU クレジット残高' },
    'CPUCreditUsage'             => { :name => 'CloudWatch CPU クレジット消費' },
    'DiskReadOps'                => { :name => 'CloudWatch Ephemeral ストレージ 読込 IO 件数', :statistics => GETDATA_STAT_SUM, :divide => GETDATA_PERIOD,       :unit => 'IOPS' },
    'DiskWriteOps'               => { :name => 'CloudWatch Ephemeral ストレージ 書込 IO 件数', :statistics => GETDATA_STAT_SUM, :divide => GETDATA_PERIOD,       :unit => 'IOPS' },
    'DiskReadBytes'              => { :name => 'CloudWatch Ephemeral ストレージ 読込バイト数', :statistics => GETDATA_STAT_SUM, :divide => GETDATA_PERIOD,       :unit => 'Bps' },
    'DiskWriteBytes'             => { :name => 'CloudWatch Ephemeral ストレージ 書込バイト数', :statistics => GETDATA_STAT_SUM, :divide => GETDATA_PERIOD,       :unit => 'Bps' },
    'NetworkIn'                  => { :name => 'CloudWatch ネットワーク データ受信量',         :statistics => GETDATA_STAT_SUM, :divide => GETDATA_PERIOD,       :unit => 'Bps' },
    'NetworkOut'                 => { :name => 'CloudWatch ネットワーク データ送信量',         :statistics => GETDATA_STAT_SUM, :divide => GETDATA_PERIOD,       :unit => 'Bps' },
    'NetworkPacketsIn'           => { :name => 'CloudWatch ネットワーク パケット受信量',       :statistics => GETDATA_STAT_SUM, :divide => GETDATA_PERIOD },
    'NetworkPacketsOut'          => { :name => 'CloudWatch ネットワーク パケット送信量',       :statistics => GETDATA_STAT_SUM, :divide => GETDATA_PERIOD },
    'StatusCheckFailed'          => { :name => 'CloudWatch EC2 ステータスチェック(総合)' },
    'StatusCheckFailed_Instance' => { :name => 'CloudWatch EC2 ステータスチェック(インスタンス)' },
    'StatusCheckFailed_System'   => { :name => 'CloudWatch EC2 ステータスチェック(システム)' },

    # EBS
    'VolumeReadBytes'            => { :name => 'CloudWatch EBS 読込バイト数 [%s]',             :statistics => GETDATA_STAT_SUM, :divide => GETDATA_PERIOD,       :unit => 'Bps' },
    'VolumeWriteBytes'           => { :name => 'CloudWatch EBS 書込バイト数 [%s]',             :statistics => GETDATA_STAT_SUM, :divide => GETDATA_PERIOD,       :unit => 'Bps' },
    'VolumeReadOps'              => { :name => 'CloudWatch EBS 読込 IO 件数 [%s]',             :statistics => GETDATA_STAT_SUM, :divide => GETDATA_PERIOD,       :unit => 'IOPS' },
    'VolumeWriteOps'             => { :name => 'CloudWatch EBS 書込 IO 件数 [%s]',             :statistics => GETDATA_STAT_SUM, :divide => GETDATA_PERIOD,       :unit => 'IOPS' },
    'VolumeTotalReadTime'        => { :name => 'CloudWatch EBS 消費時間率(読込) [%s]',                                          :divide => (GETDATA_PERIOD/100), :unit => '%' },
    'VolumeTotalWriteTime'       => { :name => 'CloudWatch EBS 消費時間率(書込) [%s]',                                          :divide => (GETDATA_PERIOD/100), :unit => '%' },
    'VolumeIdleTime'             => { :name => 'CloudWatch EBS 消費時間率(アイドル) [%s]',                                      :divide => (GETDATA_PERIOD/100), :unit => '%' },
    'VolumeQueueLength'          => { :name => 'CloudWatch EBS IO キュー件数 [%s]',            :statistics => GETDATA_STAT_MAX },
    'VolumeThroughputPercentage' => { :name => 'CloudWatch EBS 提供 IOPS 率 [%s]' },
    'VolumeConsumedReadWriteOps' => { :name => 'CloudWatch EBS 消費 IOPS 数 [%s]',             :statistics => GETDATA_STAT_SUM, :divide => GETDATA_PERIOD,       :unit => 'IOPS' }
  }

  def execcmd(_cmdline = '', _str_stdin = '')
    _str_stdout = ''
    _str_stderr = ''
    _status = Open4::popen4(_cmdline) {|_pid, _stdin, _stdout, _stderr|
      _str_stdin.each_line {|_s|
        _stdin.puts _s
      }
      _stdin.close_write
      _str_stdout = _stdout.read
      _str_stderr = _stderr.read
    }

    return _status, _str_stdout, _str_stderr
  end

  def get_server_conf_freq()
    File.open(ZABBIX_SERVER_CONF_PATH) {|_file|
      _file.each do |_s|
        if /^CacheUpdateFrequency=/ =~ _s
          return _s.split('=')[1].chomp.to_i
        end
      end
    }
    # if not defined, use default
    return 60
  end

  def get_server_ip()
    File.open(ZABBIX_AGENTD_CONF_PATH) {|_file|
      _file.each do |_s|
        if /^ServerActive=/ =~ _s
          _serverinfo = _s.split('=')[1].chomp
          _x = _serverinfo.split(':')[0]
          return '127.0.0.1' if _x.nil? || _x.empty?
          return _x
        end
      end
    }
    # if not defined, use default
    return '127.0.0.1'
  end

  def get_server_port()
    File.open(ZABBIX_AGENTD_CONF_PATH) {|_file|
      _file.each do |_s|
        if /^ServerActive=/ =~ _s
          _serverinfo = _s.split('=')[1].chomp
          _x = _serverinfo.split(':')[1]
          return '10051' if _x.nil? || _x.empty?
          return _x
        end
      end
    }
    # if not defined, use default
    return '10051'
  end

  def api_login()
    _attempts = 0
    begin
      _attempts += 1
      @zbxapi = ZabbixApi.connect(
        :url      => ZABBIX_API_URL%[get_server_ip()],
        :user     => ZABBIX_LOGINID,
        :password => ZABBIX_PASSWORD)
    rescue
      if _attempts <= ZABBIX_ATTEMPTS_MAX then
        @logger.warn('Zabbix Login Failure, so retry it.(fail count : %d)'%[_attempts])
        sleep(ZABBIX_ATTEMPTS_WAIT)
        retry
      else
        @logger.error('Zabbix Login Failure, retry failed.(fail count : %d)'%[_attempts])
        raise 'Zabbix Login Failure'
      end
    end
  end

  def api_call(_method = '', _params = Hash.new())
    return @zbxapi.query(
      :method => _method,
      :params => _params)
  end

  def api_hosts_get(_hostname = '', _templateid = '')
    _desc = Hash.new()
    _desc.store('filter',             {'host' => _hostname}) if !_hostname.empty?
    _desc.store('templated_hosts',    true)
    _desc.store('selectApplications', 'extend')
    _desc.store('selectMacros',       'extend')
    _desc.store('templateids',        _templateid)           if !_templateid.empty?
    _desc.store('output',             ['hostid', 'host'])
    return api_call('host.get', _desc)
  end

  def api_host_get(_hostname = '', _templateid = '')
    return api_hosts_get(_hostname, _templateid).first
  end

  def api_item_get(_hostid = 0, _itemkey = '')
    _data = api_call('item.get',
      {'hostids' => _hostid,
       'filter' => { 'type'   => ZABBIX_ITEM_TYPE_TRAPPER,
                     'status' => ZABBIX_ITEM_STATUS_ACTIVE,
                     'key_'   => _itemkey }})

    return (_data.nil? || _data.empty?) ? nil : _data.first
  end

  def api_item_create_update(_info = Hash.new())
    _command = (_info.has_key?(:itemid)   ? 'item.update'    : 'item.create')
    _units   = (_info.has_key?(:itemunit) ? _info[:itemunit] : '')
    _desc = Hash.new()
    _desc.store('itemid',       _info[:itemid])    if _info.has_key?(:itemid)
    _desc.store('hostid',       _info[:hostid])
    _desc.store('applications', [_info[:appid]])
    _desc.store('key_',         _info[:itemkey])
    _desc.store('name',         _info[:name])
    _desc.store('units',        _units)
    _desc.store('delay',        ZABBIX_ITEM_DELAY)
    _desc.store('type',         ZABBIX_ITEM_TYPE_TRAPPER)
    _desc.store('value_type',   ZABBIX_ITEM_VALUE_TYPE_FLOAT)
    _desc.store('history',      ZABBIX_ITEM_HISTORY)
    _desc.store('status',       ZABBIX_ITEM_STATUS_ACTIVE)
    _desc.store('trends',       ZABBIX_ITEM_TRENDS)
    api_call(_command, _desc)

    return !_info.has_key?(:itemid)
  end

  def get_local_time(_timestr = '')
    return Time.parse(_timestr).getlocal
  end

  def translate_local_unixtime(_timestr = '')
    return get_local_time(_timestr).to_i
  end

  def get_aws_hostinfo(_hostname = '')
    raise '[ERROR] can not determine hostname.' if _hostname.nil? || _hostname.empty?

    _ec2_host = api_host_get(_hostname)
    @EC2Info[_hostname].store(:hostid, _ec2_host['hostid'])
    _ec2_host['applications'].each {|_appinfo|
      @EC2Info[_hostname].store(:appid, _appinfo['applicationid']) if _appinfo['name'] == ZABBIX_ITEM_APPNAME
    }
    _ec2_host['macros'].each {|_macinfo|
      @EC2Info[_hostname].store(:aws_host, _macinfo['value']) if _macinfo['macro'] == MACROKEY_AWS_ACCOUNT_HOST
      @EC2Info[_hostname].store(:instname, _macinfo['value']) if _macinfo['macro'] == MACROKEY_EC2_INSTANCE_NAME
    }

    return SUCCESS
  end

  def get_aws_keyinfo(_hostname = '')
    raise '[ERROR] can not determine EC2 hostname.' if _hostname.nil? || _hostname.empty?
    raise '[ERROR] can not determine AWS hostname.' if ! @EC2Info[_hostname].has_key?(:aws_host)

    _aws_host = api_host_get(@EC2Info[_hostname][:aws_host])
    _aws_host['macros'].each {|_macinfo|
      @EC2Info[_hostname].store(:aws_akey, _macinfo['value']) if _macinfo['macro'] == MACROKEY_AWS_ACCESS_KEY
      @EC2Info[_hostname].store(:aws_skey, _macinfo['value']) if _macinfo['macro'] == MACROKEY_AWS_SECRET_KEY
    }

    return SUCCESS
  end

  def call_sender(_str_stdin = '')
    return if _str_stdin.nil? || _str_stdin.empty?

    _cmdline = ZABBIX_SENDER_PATH.dup
    _cmdline << ' --zabbix-server '
    _cmdline << get_server_ip()
    _cmdline << ' --port '
    _cmdline << get_server_port()
    _cmdline << ' --with-timestamps'
    _cmdline << ' --input-file -'
    _status, _str_stdout, _str_stderr = execcmd(_cmdline, _str_stdin)
    raise '[ERROR] Failed to execute Zabbix Sender.' if _status.to_i != 0
  end

  def initialize
    @EC2Info = Hash.new()
  end

  def run(_logger = nil, _args = Array.new())
    raise '[ERROR] can not initialize logger.' if _logger.nil?
    raise '[ERROR] can not determine host.'    if _args.nil?

    _args.uniq!

    _result = SUCCESS
    begin
      @logger = _logger
      _infos = Array.new()
      _created = false

      api_login()

      _template = api_host_get(ZABBIX_TEMPLATE_NAME)
      raise '[ERROR] can not determine template host.' if _template.nil? || _template.empty?
      @EC2Info.store(:templateid, _template['hostid'])

      _zbxhosts = api_hosts_get('', @EC2Info[:templateid])
      _zbxhosts.each {|_zbxhost|
        _host = _zbxhost['host']
        next if (!_args.empty?) && (!_args.include?(_host))
        @EC2Info.store(_host, Hash.new())
        get_aws_hostinfo(_host)
        next if ! @EC2Info[_host].has_key?(:hostid)
        next if ! @EC2Info[_host].has_key?(:appid)
        next if ! @EC2Info[_host].has_key?(:aws_host)
        next if ! @EC2Info[_host].has_key?(:instname)
        get_aws_keyinfo(_host)
        next if ! @EC2Info[_host].has_key?(:aws_akey)
        next if ! @EC2Info[_host].has_key?(:aws_skey)
        _ec2 = RightAws::Ec2.new(@EC2Info[_host][:aws_akey], @EC2Info[_host][:aws_skey], {:logger => @logger, :server => AWS_EC2_SERVER})
        _instances = _ec2.describe_instances
        _instances.each {|_instance|
          _equals = false
          _instance[:tags].each {|_tkey, _tval|
            _equals = true if ((_tkey == TAGKEY_NAME) && (_tval == @EC2Info[_host][:instname]))
          }
          next if ! _equals

          @EC2Info[_host].store(:instid,  _instance[:aws_instance_id])
          @EC2Info[_host].store(:devices, Hash.new())
          _instance[:block_device_mappings].each {|_device|
            @EC2Info[_host][:devices].store(_device[:ebs_volume_id], _device[:device_name].gsub(/[0-9]/, ''))
          }
        }

        _options = Hash.new()
        _options.store(:start_time, GETDATA_STARTTIME)
        _options.store(:end_time,   GETDATA_ENDTIME)
        _options.store(:period,     GETDATA_PERIOD)

        _acw = RightAws::AcwInterface.new(@EC2Info[_host][:aws_akey], @EC2Info[_host][:aws_skey], {:logger => @logger, :server => AWS_ACW_SERVER})
        _metrics = _acw.list_metrics
        next if _metrics.nil? || _metrics.empty?

        @EC2Info[_host].store(:metrics, Array.new())
        _metrics.each {|_metric|
          next if _metric[:namespace].nil?    || _metric[:namespace].empty?
          next if _metric[:dimentions].nil?   || _metric[:dimentions].empty?
          next if _metric[:measure_name].nil? || _metric[:measure_name].empty?

          case _metric[:namespace]
          when GETDATA_EC2_NAMESPACE then
            next if !_metric[:dimentions].has_key?('InstanceId')
            next if (_metric[:dimentions]['InstanceId'] != @EC2Info[_host][:instid])
            _options.store(:namespace,  GETDATA_EC2_NAMESPACE)
            _options.store(:dimentions, Hash.new())
            _options[:dimentions].store(:InstanceId, _metric[:dimentions]['InstanceId'])
          when GETDATA_EBS_NAMESPACE then
            next if !_metric[:dimentions].has_key?('VolumeId')
            next if ! @EC2Info[_host][:devices].has_key?(_metric[:dimentions]['VolumeId'])
            _options.store(:namespace,  GETDATA_EBS_NAMESPACE)
            _options.store(:dimentions, Hash.new())
            _options[:dimentions].store(:VolumeId, _metric[:dimentions]['VolumeId'])
          else
            next
          end

          _key_sym = GETDATA_STAT_AVG.downcase.to_sym
          _options.delete(:statistics) if _options.has_key?(:statistics)
          if GETDATA_MEASURES.has_key?(_metric[:measure_name]) then
            if GETDATA_MEASURES[_metric[:measure_name]].has_key?(:statistics) then
              _key_sym = GETDATA_MEASURES[_metric[:measure_name]][:statistics].downcase.to_sym
              _options.store(:statistics, GETDATA_MEASURES[_metric[:measure_name]][:statistics])
            end
          end
          _options.store(:measure_name, _metric[:measure_name])
          _metadata = _acw.get_metric_statistics(_options)
          next if _metadata[:datapoints].nil? || _metadata[:datapoints].empty?

          _info = Hash.new()
          _info.store(:label,    _metadata[:label])
          _info.store(:unit,     _metadata[:datapoints].first[:unit])
          _info.store(:host,     _host)
          _info.store(:instance, @EC2Info[_host][:instid])
          _info.store(:hostid,   @EC2Info[_host][:hostid])
          _info.store(:appid,    @EC2Info[_host][:appid])
          _info.store(:itemunit, GETDATA_UNIT_MAP[_info[:unit]]) if GETDATA_UNIT_MAP.has_key?(_info[:unit])
          case _metric[:namespace]
          when GETDATA_EC2_NAMESPACE then
            _info.store(:itemkey,  ZABBIX_ITEM_KEY_FORMAT_EC2%[_info[:label]])
            if GETDATA_MEASURES.has_key?(_info[:label]) then
              _info.store(:name,     GETDATA_MEASURES[_info[:label]][:name])
              _info.store(:itemunit, GETDATA_MEASURES[_info[:label]][:unit]) if GETDATA_MEASURES[_info[:label]].has_key?(:unit)
            else
              _info.store(:name,     GETDATA_NAME_UNKNOWN_EC2%[_info[:label]])
            end
          when GETDATA_EBS_NAMESPACE then
            _info.store(:volumeid, _metric[:dimentions]['VolumeId'])
            _info.store(:volpath,  @EC2Info[_host][:devices][_info[:volumeid]])
            _info.store(:itemkey,  ZABBIX_ITEM_KEY_FORMAT_EBS%[_info[:label], _info[:volpath]])
            if GETDATA_MEASURES.has_key?(_info[:label]) then
              _info.store(:name,     GETDATA_MEASURES[_info[:label]][:name]%[_info[:volpath]])
              _info.store(:itemunit, GETDATA_MEASURES[_info[:label]][:unit]) if GETDATA_MEASURES[_info[:label]].has_key?(:unit)
            else
              _info.store(:name,     GETDATA_NAME_UNKNOWN_EBS%[_info[:label], _info[:volpath]])
            end
          end
          _info.store(:values, Array.new())
          _metadata[:datapoints].each {|_point|
            _value = Hash.new()
            _value.store(:clock, translate_local_unixtime(_point[:timestamp]))
            if GETDATA_MEASURES.has_key?(_info[:label]) then
              if GETDATA_MEASURES[_info[:label]].has_key?(:divide) && (GETDATA_MEASURES[_info[:label]][:divide] != 0) then
                _value.store(:value, _point[_key_sym] / GETDATA_MEASURES[_info[:label]][:divide])
              else
                _value.store(:value, _point[_key_sym])
              end
            else
              _value.store(:value, _point[_key_sym])
            end
            _info[:values].push(_value)
          }
          _info[:values].sort! {|_a, _b| _a[:clock] <=> _b[:clock] }

          _item = api_item_get(_info[:hostid], _info[:itemkey])
          _info.store(:itemid,  _item['itemid']) if ! _item.nil?
          _info.store(:created, api_item_create_update(_info))
          _created = true if _info[:created]

          _infos.push(_info)
        }
      }
      sleep(get_server_conf_freq() * 2) if _created

      _sendstr = ''
      _infos.each {|_info|
        _info[:values].each {|_value|
          _sendstr << [_info[:host], _info[:itemkey], _value[:clock], sprintf('%.4f', _value[:value])].join(' ')
          _sendstr << "\n"
        }
      }
      call_sender(_sendstr) if !_sendstr.empty?
    rescue => err
      @logger.error(err.message)
      _result = FAILURE
      raise
    end

    return _result
  end
end

# main #################################################################
_result = SUCCESS
begin
  begin
    logger = Logger.new(LOGFILE)
    logger.level = Logger::WARN
  rescue
    begin
      Dir::mkdir(LOGDIR)
      retry
    rescue
      raise "[ERROR] Can not open '#{LOGFILE}'."
    end
  end

  # ARGV[0] : hostname
  #   empty value : all host mode
  #   'hostname'  : specified host only mode
  _obj = AWSEC2Info.new
  _result = _obj.run(logger, ARGV)
rescue => err
  warn "[ERROR] catch error in #{__FILE__}"
  warn err.message
  warn err.backtrace
  _result = FAILURE
end

puts _result
exit(_result)

# end of file
```

## 書いてみて‥ - 年の瀬所感
いやはや、書きかけてから、(ﾉ∀`)ｱﾁｬｰな感じでした(笑
気になった部分を直し直して書いているうちに、どんどんスクリプトが大掛かりなモノになっちまって‥。

スクリプト、結構ややこしいかもしれません。
不明な点や不審な点、ありましたらどんどん声を掛けていただければと思います。

クリスマスももうすぐとなって、Advent Calendarも、かなり大詰めです。
今年も１年間、Zabbix含め、たくさんのソフトに囲まれて過ごすことができました。

また来年も、楽しいモノを、楽しく使える１年になることを祈りつつ‥
明日のAdvent Calendarへバトンを繋ぎます。よろしくお願いいたします。