Roadworker v0.5.6.beta2

Roadworker v0.5.6.beta2をリリースしました。

github.com

Calculated Health Checks/Latency Checksのサポート

Calculated Health ChecksとLatency Checksをサポートしました。

Calculated Health Checksはこんな感じ。

rrset "zzz.info.winebarrel.jp", "A" do
  set_identifier "Secondary"
  failover "SECONDARY"
  health_check :calculated => ["07c03a45-5b69-4044-9ec3-016cd8e5f74b", "bba4d1ea-27c2-4d0c-a249-c857a3e46d88"], :health_threshold => 1, :inverted => false
  ttl 456
  resource_records(
    "127.0.0.3",
    "127.0.0.4"
  )
end

Latency Checksはこんな感じです。

rrset "zzz.info.winebarrel.jp", "A" do
    set_identifier "Primary"
    failover "PRIMARY"
    health_check "http://example.com:80/path", :search_string => "ANY_RESPONSE_STRING", :request_interval => 30, :failure_threshold => 3, :measure_latency => true
    ttl 456
    resource_records(
      "127.0.0.1",
      "127.0.0.2"
    )
  end

また、APIの変更に伴って、ヘルスチェック周りで意図しない変更が検出されるのは諸々直しました。

それから、ふる〜いI/Fとの互換性を保つため、以下のような書き方を許可していたのですが、やめました。

health_check "http://192.168.0.1:80/path", "example.com"

Health Check GCをデフォルトで無効に

Health Check GCという使われていないHealth Checkを自動的に削除する機能があり、デフォルトで有効になっていたのですが、無効にしました。 --health-check-gcオプションを渡すと、有効になります。Calculated Health Checksで参照されているHealth Checkは削除対象にはなりません。*1

テンプレートのサポート

以下のように、テンプレートを使えるようにしました。

template "default_rrset" do
  rrset context.name + "." + context.hosted_zone_name, "A" do
    ttl context.ttl
    resource_records(
      "127.0.0.1"
    )
  end
end

hosted_zone "winebarrel.jp." do
  context.ttl = 100
  include_template "default_rrset", :name => "www"
  include_template "default_rrset", :name => "www2"
end

テンプレートに渡す変数はcontextという特殊変数で受け渡せます。 また、include_templateの第二引数にHashを渡すことで、テンプレート内のcontextにHashの値をセットすることができます。 context変数のクラスはHashie::Mashなので、普通のハッシュのように値をセットすることもできます。

  context[:ttl] = 100

あと、include_templateはどのブロックでも使えるので

template "default_hz" do
  hosted_zone context.hz_name do
    rrset "www." + context.hosted_zone_name, "A" do
      ttl context.ttl
      resource_records(
        "127.0.0.1"
      )
    end
  end
end

include_template "default_hz", hz_name: "winebarrel.jp."
include_template "default_hz", hz_name: "winebarrel2.jp."

とかもできます。

その他

rrsetからHealth Checkを名前/IDで参照する機能は、今のところ実装してないんですが、そういうユースケースはあるんですかね…

*1:が、再帰的参照までは調べてません。できるのかな…

fluent-plugin-munin-nodeを作った

先日に引き続いて、fluent-plugin-munin-nodeを作りました。

github.com

これは何?

munin-nodeからmetricsを取得するためのfluentdのプラグインです。

実はすでに同様のプラグインあったりするんですが、いくつか不満があって改修量が多そうだったので、車輪を再実装しました。

fluent-plugin-muninとの主な違いは以下の通りです。

  • 1フィールド、1レコード(bulkモードもあります)
  • munin-nodeとのコネクションは都度接続
  • ホスト名は取得しない

使い方

<source>
  type munin_node
  extra {"hostname", "my-host"}
</source>
2015-91-02 12:30:09 +0000 munin.cpu.user: {"service":"cpu","field":"user","value":"4192","hostname":"my-host"}
2015-91-02 12:30:09 +0000 munin.cpu.nice: {"service":"cpu","field":"nice","value":"0","hostname":"my-host"}
2015-91-02 12:30:09 +0000 munin.cpu.system: {"service":"cpu","field":"system","value":"1935","hostname":"my-host"}

四方山

本当は、どこかに投入しようと考えていたのですが、残念ながら別のメトリクス収集方法を使うことになりそうな感じです。 ただ、シンプルなプラグインでテストも書いているので、production readyのつもりではあります。

いまさらmunin…という感じですが、munin-nodeはfluentd用のメトリクス収集ツールとしては優れていると思うので、使う機会はそれなりにありそうです。

どうぞご利用ください。

それはさておき

設定ファイルのテストだけのプラグイン、結構多くないですかね…

fluent-plugin-zabbix-agentを作った

fluent-plugin-zabbix-agentというfluentdのプラグインを作りました。

github.com

zabbixのpassive checkを使って、zabbix-agentから、各種メトリクスをfluentdに流すInputプラグインです。

使い方

以下のような設定ファイルを書くと、指定したitemをzabbix-agentから取得します。

<source>
  type zabbix_agent
  tag zabbix.item
  interval 60
  extra {"hostname", "my-host"}
  items {
    "system.cpu.load[all,avg1]": "load_avg1",
    "system.cpu.load[all,avg5]": null
  }
</source>
2015-01-02 12:30:40 +0000 zabbix.item: {"key":"load_avg1","value":0.0,"hostname":"my-host"}
2015-01-02 12:30:40 +0000 zabbix.item: {"key":"system.cpu.load[all,avg5]","value":0.01,"hostname":"my-host"}

itamsの指定はhashで、キーがzabbixのitem名、値がfluentdのレコードに書くときのキーです(nullの場合、item名をそのまま使う)。 itemsを外部のJSONファイルから読み込むとか、複数itemを一つのレコードにまとめるとかもできます。

ユースケース

ひとつはzabbixのitemをzabbixサーバだけではなくて、DataDogとかGrowthForecastにも流したい場合などです。 それらを併用している環境がどのくらいあるか分からないんですが…。

もうひとつは、zabbix-agentは使いたいけどzabbix-serverは使いたくないような場合です。

とある環境でメトリクス収集をfluentdでやろうとしてまして、fluent-plugin-dstatを導入しようとしてみたのですが、メトリクスの「収集」部分に関してはかなり手間な感じでした。fluentdのcron的pluginとかcronそのものでがんばるとかも考えたのですが、zabbix-agentに収集させるのが結構楽そうだったので、今回作った次第です。

TIME_WAIT問題

今のところ、ひとつ問題がありまして、1メトリクスの収集ごとにTCPの接続をzabbix-agentに対して行っているので、あんまりintervalを短くするとTIME_WAITが一杯になります。

zabbixのドキュメントやソースコードで、1セッションで複数のメトリクスを取得する方法や、つなぎっぱなしのコネクションを使い回す方法を探したのですが、現状では対応していないように見えました(active check使えってことですかね)。 個人的には60sぐらいならそれほど問題にならないかと考えています。


というわけで、どうぞご利用ください。

続Libratoコード化: Space・Alertでテンプレートを使う

先日の記事で紹介したLibrato管理ツールlbrt」にテンプレート機能を付けました。

github.com

基本的な使い方は以下の通り。

template "dstat" do
  chart "load_avg" do
    type "line"
    stream do
      metric "dstat.#{context.space_name}.load_avg"
      type "gauge"
      source "*"
      group_function "breakout"
      summary_function "average"
    end
  end
end

space "my-host-001" do
  include_template "dstat"
end

Alert・Serviceでも同様に使うことができます。

以下のような書き方もできます。

template "dstat" do
  space context.hostname + context.my_id do
    chart "load_avg" do
      type "line"
      stream do
        metric "dstat.#{context.space_name}.load_avg"
        type "gauge"
        source "*"
        group_function "breakout"
        summary_function "average"
      end
    end
  end
end

contest[:my_id] = "100"
include_template "dstat", hostname: "my-host"

templateメソッドの呼び出しはトップレベルのみですが、include_templateはどこでも使えます。

template "my stream" do
  stream do
    metric "dstat.#{context.space_name}.load_avg"
    type "gauge"
    source "*"
    group_function "breakout"
    summary_function "average"
  end
end

space "my-host-001" do
  chart "load_avg" do
    type "line"
    include_template "my stream"
  end
end

利用例①: テンプレートをホストごとSpace(ダッシュボード)に適用する

# requireでテンプレートを他のファイルに分けることもできます
template "dstat" do
  chart "load_avg" do
    type "line"
    stream do
      metric "dstat.#{context.space_name}.load_avg"
      type "gauge"
      source "*"
      group_function "breakout"
      summary_function "average"
    end
  end
end

space "my-host-001" do
  include_template "dstat"
end

space "my-host-002" do
  include_template "dstat"
end

space "my-host-003" do
  include_template "dstat"

  # このホストはMySQLのチャートも表示する
  chart "mysql qps" do
    type "line"
    stream do
      metric "mysql.qps"
      type "gauge"
      source "*"
      group_function "breakout"
      summary_function "average"
    end
  end
end

利用例②: テンプレートをホストごとのアラート適用する

# requireでテンプレートを他のファイルに分けることもできます
template "alert set1" do
  alert "#{context.hostname}/login-delay" do
    description "desc"
    attributes "runbook_url"=>"http://example.com"
    active true
    rearm_seconds 600
    rearm_per_signal false

    condition do
      type "below"
      metric_name 'login-delay'
      source nil
      threshold 4.0
      summary_function "sum"
    end

    service "mail", "my email"
  end

  alert "#{context.hostname}/login-delay2" do
    description "desc"
    attributes "runbook_url"=>"http://example.com"
    active true
    rearm_seconds 600
    rearm_per_signal false

    condition do
      type "below"
      metric_name 'login-delay2'
      source nil
      threshold 4.0
      summary_function "sum"
    end

    service "mail", "my email"
  end
end

include_template "alert set1", hostname: 'host1'

"host2".tap do |host|
  include_template "alert set1", hostname: host
  # host2はMySQLも監視する
  alert "#{host}/mysql.qps" do
    description "desc"
    attributes "runbook_url"=>"http://example.com"
    active true
    rearm_seconds 600
    rearm_per_signal false

    condition do
      type "below"
      metric_name 'mysql.qps'
      source nil
      threshold 4.0
      summary_function "sum"
    end

    service "mail", "my email"
    service "hipchat", "my hipchat"
  end
end

これでまた、Zabbixが不要な世界に一歩近づき、人類の進歩と平和に貢献することができました。

どうぞご利用ください。

Libratoをコード化しました

最近、業務案件でLibratoを使っています。

f:id:winebarrel:20150817001920p:plain

Libratoはメトリクス収集のSaaSで、メトリクスの収集とアラートの設定・通知ができます。 たぶんDatadogとかが競合になるんじゃないでしょうか。

Datadogほど多機能ではないですが、「グラフを書く」「グラフを並べてダッシュボード(Space)」を作る」「閾値を超える・下がる等でアラートを投げる(emal・hipchat・slack・SNS…etc)」「AWS・HerokuなどとのIntegration」と基本的な機能はそろっています。

有名な事例だとTreasure DataがLibratoを使っていたみたいです(いまはDatadogなのかな・・?)

なんでDatadogじゃないの?

ずばりお値段です。

Datadogは$15/ホストの課金ですが、Libratoはメトリクスごとの課金になります。解像度を下げるとさらに安く。

dstatの主要メトリクスだけを取るとすると、60〜300sの解像度でホストあたり$1〜$2とかなりお安くなります。

参考: fluentd+dstat+libratoでサーバのメトリクスを可視化する - Qiita

あとホストごとじゃなくて、単体でサービスメトリクスを取りたいことが結構あるので、その辺もLibratoの課金体系にマッチするかなと。

コード化

AWSのインテグレーションなどを使うと適当にダッシュボード作ってくれるのですが、自前のメトリクスは自分でダッシュボードを作る必要があります。

ホストごとに「CPU使用率」「メモリ使用率」「ディスク使用率」…etcとちまちまダッシュボードを作っていたのですが、いい加減辛くなって、ダッシュボードまわり・アラートまわりをコード化しました。

github.com

使い方

サブコマンドがalert service(通知先) space(ダッシュボード)とあって、以下のような感じで使います。

$ lbrt space export space.rb
$ lbrt space apply space.rb --dry-run
$ lbrt space apply space.rb

他のCodenize.toolsのようなMakefile的なファイルは特にありません。

ダッシュボード

ダッシュボードのDSLは以下のようになります。

space "My Space1" do
  chart "chart1" do
    type "stacked"
    stream do
      metric "login-delay"
      type "gauge"
      source "*"
      group_function "average"
      summary_function "average"
    end
  end
end

chartを増やしていけば、ダッシュボードにグラフが追加されます。

アラート

alert "alert1" do
  description "My Alert1"
  attributes "runbook_url"=>"http://example.com"
  active true
  rearm_seconds 600
  rearm_per_signal false

  condition do
    type "below"
    metric_name "login-delay"
    source "foo.bar.com"
    threshold 1.0
    summary_function "sum"
  end

  service "mail", "my email"
end

service "mail", "my email" do
  settings "addresses"=>"sugawara@example.com"
end

service "slack", "my slack" do
  settings "url"=>"https://hooks.slack.com/services/..."
end

serviceはアラートの通知先です。HipChat・Slack・Amazon SNS・Zapierなどのメジャーどころはそろっています。

個人的にTwillioがなかったのが残念だったのですが、Amazon SNSさえ使えればLambda経由でどうとでもできますね。

所感

シンプルさ故なのでしょうが、ダッシュボードをコード化できたのは大変うれしいです。 Zabbixもいつかコード化してやろうとはもくろんでいたのですが、なんか夢で終わりそうな気がしてます。

まだすごく使い込んでいるわけではないのですが、今のところ大きく困るところはないのでしばらく使い続けてみようと思ってます。

どうぞご利用ください。

ところでDatadogのアラートもコード化していました

barkdogというツールを作ってDatadogのアラート部分も一応コード化してました。

github.com

monitor "Check load avg", :type=>"metric alert" do
  query "avg(last_5m):avg:ddstat.load_avg.1m{host:i-XXXXXXXX} > 1"
  message "@winebarrel@example.net"
  options do
    notify_no_data true
    no_data_timeframe 2
    notify_audit true
    silenced({})
  end
end

ちょうがんばればDatadogの各種ダッシュボードもコード化できるのでしょうが、やるモチベーションは今のところないですね…

GitHub+Amazon SNS+Lambda+CloudFormationで簡易CI

先ほどの記事に引き続き。

GitHubからAmazon SNSでLambdaにイベント飛ばせば簡易CIができそうだだったので作ってみた。

処理の概要

  1. GitHubにpush→Amazon SNS→Lambdaにイベント
  2. Lambdaがイベントをフック→cfnスタックを作成
  3. cfnスタックがEC2インスタンスを起動→git clone
  4. bundle exec rspec
  5. cfnスタックは処理完了後に自動的に削除

Lambda Function

こんな感じ。

var Promise = require('bluebird');

var AWS = require("aws-sdk");
AWS.config.update({region: 'ap-northeast-1'});

var cloudformation = Promise.promisifyAll(new AWS.CloudFormation());

var fs = Promise.promisifyAll(require('fs'));

exports.handler = function(event, context) {
  var message = JSON.parse(event.Records[0].Sns.Message);
  var repo_url = message.repository.url;
  var stackName = 'my-stack-' + new Date().getTime();

  fs.readFileAsync('template.json', 'utf8').then(function(data) {
    return cloudformation.createStackAsync({
      StackName: stackName,
      Parameters: [
        {
          ParameterKey: 'Repository',
          ParameterValue: repo_url
        }
      ],
      TemplateBody: data
    });
  }).then(function(data) {
    console.log(data);
  }).then(function() {
    context.succeed('OK');
  }).catch(function(err) {
    context.fail(err);
  });
};

roleにはcfnスタックを作成できる適切な権限を付与しておく。

cfn template

こんな感じ。

{
  "Parameters": {
    "Repository": {
      "Type": "String"
    }
  },
  "Resources": {
    "MyInstance": {
      "Type": "AWS::EC2::Instance",
      "Properties": {
        "ImageId": "ami-cbf90ecb",
        "InstanceType": "t2.micro",
        "SubnetId": "subnet-XXXXXXXX",
        "IamInstanceProfile": "my_role",
        "Tags": [
          {
            "Key": "Name",
            "Value": {"Ref": "AWS::StackId"}
          }
        ],
        "UserData": {
          "Fn::Base64": {
            "Fn::Join": [
              "",
              [
                "#!/bin/bash\n",
                "export PATH=/usr/local/bin:$PATH\n",
                "yum install -y git\n",
                "yum install -y rubygem-io-console\n",
                "gem install bundler\n",
                "git clone ", {"Ref": "Repository"}, " myrepo\n",
                "cd myrepo\n",
                "bundle install\n",
                "bundle exec rspec\n",
                "aws cloudformation delete-stack --stack-name ", {"Ref": "AWS::StackName"}, " --region ", {"Ref": "AWS::Region"}, "\n"
              ]
            ]
          }
        }
      }
    }
  }
}

やってることは、見たとおり。

/usr/local/binにパスを通す必要があった。

Amazon SNS

Lambdaにイベントを飛ばすトピックを作成。

f:id:winebarrel:20150808173745p:plain

GitHubリポジトリ

https://github.com/winebarrel/gh-lambda

f:id:winebarrel:20150808173331p:plain

SNSとの連携を設定しておく。

f:id:winebarrel:20150808174401p:plain

使ってみる

上記のリポジトリで適当にコミットすると

$ git commit --allow-empty -m 'XXX'; git push
[master 1d03048] XXX
Counting objects: 1, done.
Writing objects: 100% (1/1), 189 bytes | 0 bytes/s, done.
Total 1 (delta 0), reused 0 (delta 0)
To git@github.com:winebarrel/gh-lambda.git
   765ff58..1d03048  master -> master

Lambdaにイベントが飛んで

START RequestId: d7f6bdab-3da8-11e5-82cf-81454d6df691
END RequestId: d7f6bdab-3da8-11e5-82cf-81454d6df691
2015-08-08T08:38:30.060Z    d7f6bdab-3da8-11e5-82cf-81454d6df691    { ResponseMetadata: { RequestId: 'd8206580-3da8-11e5-84e4-e722a24172c9' },
  StackId: 'arn:aws:cloudformation:ap-northeast-1:123456789012:stack/my-stack-1439023109307/d8343b90-3da8-11e5-8e97-5001aba754a8' }
REPORT RequestId: d7f6bdab-3da8-11e5-82cf-81454d6df691  Duration: 768.96 ms Billed Duration: 800 ms     Memory Size: 128 MB Max Memory Used: 16 MB

インスタンスが起動して

f:id:winebarrel:20150808174022p:plain

rspecが実行されて

f:id:winebarrel:20150808174134p:plain

インスタンスは削除される。

f:id:winebarrel:20150808174200p:plain

S3+Lambda+CloudFormationでサーバレスyumリポジトリ

先日の記事で、大きめの処理をLambda+CloudFormationで実行するめどがついたので、S3+Lambda+CloudFormationでサーバレスyumリポジトリを作ってみた。

処理の概要

  1. S3にrpmを追加・更新・削除
  2. Lambdaがイベントをフック→cfnスタックを作成
  3. cfnスタックがEC2インスタンスを起動→S3リポジトリをダウンロード
  4. createrepoを実行して、インデックスをS3にアップロード
  5. cfnスタックは処理完了後に自動的に削除

Lambda Function

こんな感じ。

var Promise = require('bluebird');

var AWS = require("aws-sdk");
AWS.config.update({region: 'ap-northeast-1'});

var cloudformation = Promise.promisifyAll(new AWS.CloudFormation());

var fs = Promise.promisifyAll(require('fs'));

exports.handler = function(event, context) {
  var stackName = 'my-stack-' + new Date().getTime();

  fs.readFileAsync('template.json', 'utf8').then(function(data) {
    return cloudformation.createStackAsync({
      StackName: stackName,
      Parameters: [
        {
          ParameterKey: 'S3Bucket',
          ParameterValue: 'my-yum-repo'
        }
      ],
      TemplateBody: data
    });
  }).then(function(data) {
    console.log(data);
  }).then(function() {
    context.succeed('OK');
  }).catch(function(err) {
    context.fail(err);
  });
};

roleにはcfnスタックを作成できる適切な権限を付与しておく。

また、S3のイベントソースをPost/Put/Deleteで追加する。

f:id:winebarrel:20150808160127p:plain

なんか、イベントソースの情報取得まわりがぶっ壊れているような気が… 一応、動作はしました。

cfn template

こんな感じ。

{
  "Parameters": {
    "S3Bucket": {
      "Type": "String"
    }
  },
  "Resources": {
    "MyInstance": {
      "Type": "AWS::EC2::Instance",
      "Properties": {
        "ImageId": "ami-cbf90ecb",
        "InstanceType": "t2.micro",
        "SubnetId": "subnet-XXXXXXXX",
        "IamInstanceProfile": "my_role",
        "Tags": [
          {
            "Key": "Name",
            "Value": {"Ref": "AWS::StackId"}
          }
        ],
        "UserData": {
          "Fn::Base64": {
            "Fn::Join": [
              "",
              [
                "#!/bin/bash\n",
                "yum install -y createrepo\n",
                "aws s3 sync s3://", {"Ref": "S3Bucket"}, " myrepo --delete\n",
                "createrepo myrepo\n",
                "aws s3 sync myrepo s3://", {"Ref": "S3Bucket"}, " --delete\n",
                "aws cloudformation delete-stack --stack-name ", {"Ref": "AWS::StackName"}, " --region ", {"Ref": "AWS::Region"}, "\n"
              ]
            ]
          }
        }
      }
    }
  }
}

やってることは、まあ見たとおりです。

S3のバケットの構成

s3://my-yum-repo/
├── noarch
│   └── hello-0.1.0-1.noarch.rpm
└── repodata
    ├── ...
    └── repomd.xml

yumの設定ファイル

/etc/yum.repos.d/my.repo

[my]
name=my
baseurl=http://my-yum-repo.s3-website-ap-northeast-1.amazonaws.com/
gpgcheck=0
enabled=1

使ってみる

初期状態でhello2というrpmはなし。

$ sudo yum clean all; sudo yum search hello2
...
警告: 一致するものが見つかりません: hello2
見つかりませんでした

rpmファイルをS3においてみる。

$ aws s3 cp hello2-0.1.0-1.noarch.rpm s3://my-yum-repo/noarch/
upload: rpmbuild/RPMS/noarch/hello2-0.1.0-1.noarch.rpm to s3://my-yum-repo/noarch/hello2-0.1.0-1.noarch.rpm

すると、Lambdaが起動。

START RequestId: 3043f34d-3d9c-11e5-8a8b-ebe5957bb289
2015-08-08T07:07:55.968Z    3043f34d-3d9c-11e5-8a8b-ebe5957bb289    { ResponseMetadata: { RequestId: '30fc94cc-3d9c-11e5-8ca5-3128034c20e8' },
  StackId: 'arn:aws:cloudformation:ap-northeast-1:123456789012:stack/my-stack-1439017674468/31109130-3d9c-11e5-8e97-5001aba754a8' }
END RequestId: 3043f34d-3d9c-11e5-8a8b-ebe5957bb289
REPORT RequestId: 3043f34d-3d9c-11e5-8a8b-ebe5957bb289  Duration: 1558.67 ms    Billed Duration: 1600 ms    Memory Size: 128 MB Max Memory Used: 16 MB

Lambdaがcfnスタックを作成→EC2インスタンス起動。

f:id:winebarrel:20150808160949p:plain

EC2インスタンスリポジトリのインデックスを更新→S3にアップロード。

f:id:winebarrel:20150808161221p:plain

EC2インスタンスは自動的に削除。

f:id:winebarrel:20150808161303p:plain

yumリポジトリにhello2が追加される。

$ sudo yum clean all; sudo yum search hello2読み込んだプラグイン:fastestmirror
...
======================================= N/S Matched: hello2 ========================================
hello2.noarch : hello2