MackerelでEC2スポットインスタンスの価格変動を監視する

まえがき

Mackerel Advent Calendar 2015 12月13日の記事です。

qiita.com

mackerel.io

昨日は la_luna_azul さんでした。Ore no homepageはよく参考にさせてもらってます!

go-check-pluginsを勝手に解説 – Mackerelアドベントカレンダー12日目 | Ore no homepage

今日のカレンダーがたまたま空いていたので、ここまで続いたのが途切れるのもなんかもったいないなと思って参加してみました。

この記事では、AWSのAPIからEC2スポットインスタンスの価格を取得し、Mackerelにサービスメトリックとして送信する方法を紹介します。

使用するライブラリーについて

Python3を使いますので、AWS APIとの連携にはboto3、Mackerelとの連携にはmackerel.clientを使用します

環境構築について、詳しい説明は割愛しますが、boto3, mackerel.clientともにpipでインストールできます。

RubyでrbenvやRVMを使用するように、PythonではVirtualenvを使用するのが良いでしょう。

Virtualenvを使う前提にはなりますが、以下のコマンドでこの記事で使用するコードを動かす環境を作れると思います。

$ virtualenv -p python3 .venv
$ source .venv/bin/activate
$ pip install boto3 mackerel.client

スポットインスタンスの価格を取得する

スポットインスタンスの価格はEC2 APIのDescribeSpotPriceHistoryを使うと取得できます。

http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeSpotPriceHistory.html

boto3を使用してAWS東京リージョンのスポットインスタンス価格を取得するには、以下のようなコードになります

import boto3

access_key_id = 'AWSのアクセスキー'
secret_access_key = 'AWSのシークレットキー'
region_name = 'ap-northeast-1' # AWSのリージョン名

boto3_session = boto3.session.Session(aws_access_key_id=access_key_id,
                                      aws_secret_access_key=secret_access_key,
                                      region_name=region_name)

ec2_client = boto3_session.client('ec2')

spot_price_history = ec2_client.describe_spot_price_history()

for history in spot_price_history.get('SpotPriceHistory'):
    print(history)

このコードを実行すると以下のような結果が表示されます。

$ python boto3_example.py
{'SpotPrice': '0.087700', 'Timestamp': datetime.datetime(2015, 12, 13, 8, 21, 25, tzinfo=tzutc()), 'AvailabilityZone': 'ap-northeast-1b', 'InstanceType': 'm2.4xlarge', 'ProductDescription': 'Linux/UNIX'}
{'SpotPrice': '0.126400', 'Timestamp': datetime.datetime(2015, 12, 13, 8, 21, 13, tzinfo=tzutc()), 'AvailabilityZone': 'ap-northeast-1c', 'InstanceType': 'c3.2xlarge', 'ProductDescription': 'Linux/UNIX'}
{'SpotPrice': '0.419200', 'Timestamp': datetime.datetime(2015, 12, 13, 8, 21, 11, tzinfo=tzutc()), 'AvailabilityZone': 'ap-northeast-1c', 'InstanceType': 'c3.2xlarge', 'ProductDescription': 'Windows'}
{'SpotPrice': '0.104200', 'Timestamp': datetime.datetime(2015, 12, 13, 8, 21, 8, tzinfo=tzutc()), 'AvailabilityZone': 'ap-northeast-1c', 'InstanceType': 'd2.xlarge', 'ProductDescription': 'Linux/UNIX'}
{'SpotPrice': '0.091900', 'Timestamp': datetime.datetime(2015, 12, 13, 8, 21, 8, tzinfo=tzutc()), 'AvailabilityZone': 'ap-northeast-1b', 'InstanceType': 'd2.xlarge', 'ProductDescription': 'Linux/UNIX'}
...

このままではAvailability Zoneやインスタンスタイプなどでの絞り込みができていませんが、そのような条件でフィルタリングすることもできるので後述します。

Mackerelと連携する

オーガニゼーションにサービスを追加する

スポットインスタンスの価格をサービスメトリックとして記録するために、オーガニゼーションにサービスを追加します。

サービスの登録方法については11日目の記事、Mackerelを使ってTwitterを監視してみようでも手順が紹介されていますので、この記事では割愛します。

stefafafan.hatenablog.com

mackerel.clientを使う

mackerel.clientについても、11日目の記事で紹介されているのですが、せっかくなのでmackerel.clientのサンプルを紹介します。

以下のコードは、mackerel.clientを使用して、公式ヘルプのホストのカスタムメトリックを投稿すると同じように6面ダイス、20面ダイスの結果をサービスメトリックとして送信します。

help-ja.mackerel.io

import random
import time

import mackerel.client

api_key = 'MackerelのAPIキー'
service_name = 'Mackerelのオーガニゼーションに追加したサービス名'
timestamp = int(time.time())

metrics = [
    {'name': 'test.mackerel_client.dice6', 'value': random.randint(1, 6), 'time': timestamp},
    {'name': 'test.mackerel_client.dice20', 'value': random.randint(1, 20), 'time': timestamp},
]

mackerel_client = mackerel.client.Client(mackerel_api_key=api_key)
mackerel_client.post_service_metrics(service_name, metrics)

上記のコードを定期的に実行すると、以下のようにMackerel上でサービスメトリックのグラフを見ることができます。

f:id:ariarijp:20151213202356p:plain

boto3とmackerel.clientを組み合わせる

さて、boto3とmackerel.clientの使い方がわかったので、組み合わせてみましょう。

import time
from datetime import datetime, timedelta

import boto3
import mackerel.client


def metric_exists(instance_type, availability_zone, metrics):
    for metric in metrics:
        if metric.get('name') == 'ec2_spot_price_history.{0}.{1}'.format(availability_zone, instance_type):
            return True

    return False


def build_metric_name(availability_zone, instance_type):
    return 'ec2_spot_price_history.{0}.{1}'.format(availability_zone, instance_type)


def build_metric(instance_type, availability_zone, spot_price, timestamp):
    return {
        'name': build_metric_name(availability_zone, instance_type),
        'time': timestamp,
        'value': spot_price,
    }


aws_access_key_id = 'AWSのアクセスキー'
aws_secret_access_key = 'AWSのシークレットキー'
region_name = 'ap-northeast-1' # AWSのリージョン名
aws_availability_zone = 'ap-northeast-1c' # AWSのAZ名

mackerel_api_key = 'MackerelのAPIキー'
mackerel_service_name = 'Mackerelのオーガニゼーションに追加したサービス名'

boto3_session = boto3.session.Session(aws_access_key_id=aws_access_key_id,
                                      aws_secret_access_key=aws_secret_access_key,
                                      region_name=aws_region_name)

boto3_client = boto3_session.client('ec2')

ec2_instance_types = [
    'c4.large', 'c4.xlarge', 'c4.2xlarge', 'c4.4xlarge',
]

# スポットインスタンスの価格履歴をAZ, インスタンスタイプ, OSタイプで絞り込む
filters = [
    {'Name': 'availability-zone', 'Values': [aws_availability_zone]},
    {'Name': 'instance-type', 'Values': ec2_instance_types},
    {'Name': 'product-description', 'Values': ['Linux/UNIX']},
]

timestamp = int(time.time())
one_min_ago = datetime.now() - timedelta(minutes=1)

# スポットインスタンスの履歴を取得
# 直近の値を取得するため、開始日時を1分前の日時とする
spot_price_history = boto3_client \
    .describe_spot_price_history(Filters=filters, StartTime=one_min_ago) \
    .get('SpotPriceHistory')

mackerel_client = mackerel.client.Client(mackerel_api_key=mackerel_api_key)
metrics = []

for history in spot_price_history:
    # 価格履歴からインスタンスタイプなどの情報を取得
    # ハイフンはMackerelのメトリクス名として使用できないので、アンダースコアに置換する
    instance_type = history.get('InstanceType')
    aws_availability_zone = history.get('AvailabilityZone').replace('-', '_')
    spot_price = float(history.get('SpotPrice'))

    # すでにメトリックがリストに存在する場合は読み飛ばす
    if metric_exists(instance_type, aws_availability_zone, metrics):
        continue

    # メトリックを構築してリストに追加
    metric = build_metric(instance_type, aws_availability_zone, spot_price, timestamp)
    metrics.append(metric)

# 指定したサービス名のメトリックとして価格履歴を送信
mackerel_client.post_service_metrics(mackerel_service_name, metrics)

上記のコードを定期的に実行すると、Mackerel上で以下の様なグラフを見ることができます。

f:id:ariarijp:20151213202325p:plain

サービスメトリックとしてMackerelに送信してしまえば、価格の変更を監視することも簡単にできます。

Slackなどのサービスと連携することが簡単なので、使い慣れたツールで通知を受け取ることもできて便利そうですね。

まとめ

気軽にメトリクスを送りつけるだけで可視化も監視もできるので、Mackerel便利ですね!

明日はpyama86さんです。