GameWith Engineering Blog

GameWith のエンジニア、デザイナーが技術について日々発信していきます。

DroidKaigi 2018 にスポンサーとして協賛します

f:id:serimaryo:20171206151812p:plain

GameWith は DroidKaigi 2018 への「サポーターズ」スポンサーとして協賛を行います。

GameWith はウェブサイトだけでなく、より良いユーザ体験を実現するためにネイティブアプリの開発も行っています。

今回は、Android の技術コミュニティの発展のお手伝いを始めていきたいという思いから、スポンサーとして協賛させていただきました。

当日は弊社のエンジニアもイベントに参加する予定ですので、もし見かけましたらお気軽にお声がけください!

イベント概要については、以下をご覧ください。

イベント概要

イベント名 : DroidKaigi 2018

概要 : DroidKaigiはエンジニアが主役のAndroidカンファレンスです。 Android技術情報の共有とコミュニケーションを目的に、2018年2月8日(木)、9日(金)の2日間開催します。 今回は「ニッチな技術とコミュニケーション」を重視する予定です。

日時 : 2018 年 2 月 8 日(木), 9 日(金)

会場 : ベルサール新宿グランド コンファレンスセンター

主催 : DroidKaigi実行委員会

公式 HP : https://droidkaigi.jp/2018/

droidkaigi.jp

Spot Instance の価格を CloudWatch で記録する

はじめまして。GameWith のエンジニアの @serima です。 普段は GameWith というゲームメディアの機能開発やインフラに目を向けるお仕事をしています。

GameWith では EC2 インスタンスのスケールアウトを Auto Scaling を使用せずに独自のスクリプトでインスタンス数をコントロールしています。*1

それなりの数のインスタンス数を起動しようとしたときに、その Availability Zone におけるそのインスタンスタイプが枯渇しているという状態が一時的に存在し、目的のインスタンスが起動できなかった事がありました。*2

具体的には、以下のようなエラーメッセージが返されました。

InsufficientInstanceCapacity: We currently do not have sufficient c4.xlarge capacity in the Availability Zone you requested (ap-northeast-1a). Our system will be working on provisioning additional capacity. You can currently get c4.xlarge capacity by not specifying an Availability Zone in your request or choosing ap-northeast-1c. status code: 500, request id:

高負荷が予測されるタイミングでインスタンスを想定通り起動できていない状態になってしまうと、サービス運営に影響があるため、回避したい問題となります。

その Availability Zone における残りのインスタンス数などを取得する API などは用意されていないため、いかにその状態を予見するかという解決策を探りました。 何か手がかりになるものはないかとメトリクスを見ていると、枯渇していたタイミングの前には Spot Instance の価格が高騰しているという状態が観測できました。

EC2 のコンソールから、スポットリクエスト -> 価格設定履歴から以下のようなグラフを参照することができます。 f:id:serimaryo:20171124183654p:plain

これを監視し、高騰したタイミングでアラート通知できれば、インスタンス起動のタイミングを早くするなど別のオプションを選ぶことができるようになります。 GameWith では CloudWatch のアラームを PagerDuty に飛ばすような運用をしていますので、そのフローにのせられるとスムースです。

しかし、SpotInstance の価格はそのままでは CloudWatch のメトリクスとして設定できません。 そこで、AWS Lambda を利用して、CloudWatch にカスタムメトリクスとして 1 分おきに値を送信するようにしました。

具体的なスクリプトは以下です。(なお、ランタイムは Python 2.7 を使用しています。)

import boto3
import time
import os
from datetime import datetime, timedelta

region = os.getenv('AWS_REGION', 'ap-northeast-1')

ec2 = boto3.client('ec2', region_name=region)
cloudwatch = boto3.client('cloudwatch', region_name=region)

aws_availability_zone = 'ap-northeast-1a'

def put_cloudwatch(namespace, metric_name, instance_type, timestamp, value):
    metric_data = {
        'MetricName': metric_name,
        'Timestamp': timestamp,
        'Value': value,
        'Unit': 'None',
        'Dimensions': [
            {
                'Name': 'InstanceType',
                'Value': instance_type
            }
        ]
    }
    r = cloudwatch.put_metric_data(
        Namespace = namespace,
        MetricData = [metric_data]
    )
    return r

def lambda_handler(event, context):
    ec2_instance_types = [
        'c4.large',
        'c4.xlarge'
    ]

    metric_name = 'SpotInstancePrice'

    filters = [
        {'Name': 'availability-zone', 'Values': [aws_availability_zone]},
        {'Name': 'instance-type', 'Values': ec2_instance_types},
        {'Name': 'product-description', 'Values': ['Linux/UNIX']},
    ]

    now = datetime.now()
    one_min_ago = now - timedelta(minutes=1)

    spot_price_history = ec2.describe_spot_price_history(Filters=filters, StartTime=one_min_ago).get('SpotPriceHistory')

    for history in spot_price_history:
        instance_type = history.get('InstanceType')
        spot_price = float(history.get('SpotPrice'))
        put_cloudwatch('EC2', metric_name, instance_type, now, spot_price)

    return str(spot_price_history)

Lambda は、現在では定期実行できるようになっており、指定した時間間隔ごとにイベントを発火させることができます。 少し分かりづらいのですが、定期実行を設定する場合は、Lambda のコンソールではなく、CloudWatch のコンソールに移動し、サイドバーの「ルール」から設定することになります。

ちなみに、このスクリプトを処理するロールを新規で作成し、以下のようなポリシーをアタッチしました。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "cloudwatch:PutMetricData"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeSpotPriceHistory"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}

カスタムメトリクスとして送信された値を元に、無事に以下のようなグラフを作成することができました。 今回は、On-Demand Price を一定時間超えていたらアラーム通知を行うように設定しました。

f:id:serimaryo:20171124180842p:plain

このような小ネタも引き続き、こちらのブログで公開していきたいと思います。

参考

*1:語ると長くなってしまうので、こちらについては後日別記事で書きたいと思います

*2:レイテンシの関係上、Single AZ で運用しています