ちょっと話題の記事

【新機能】AWS CLIがアップデート!新機能「EC2 Spot Fleet API」が超便利!

2015.05.19

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、せーのです。今日はアップデートした[AWS CLI 1.7.28]の中から新機能「EC2 Spot Fleet API」をご紹介します。 このAPIを使うと数千とあるEC2のスポットインスタンスの中から今使いたいEC2を見つけ出してくれます。

スポットインスタンスの隠れた苦労

みなさんはスポットインスタンスを使用する時にどのような操作を行うでしょうか。マネージメントコンソールからの操作は便利ですね。特に最近は画面が日本語化されましたので説明通りに上から埋めていけばほぼ躓くことなくスポットインスタンスが購入できるかと思います。

spotfleetapi1

一方コードから使う場合にはどうでしょうか。スポットインスタンスは特に分散処理やバッチ処理などで沢山のEC2を立ちあげなければいけない時にコストをかけないように使用する場合が多いですが(このように分散処理で使う複数のEC2インスタンスの事を「フリート(艦隊)」といいます)、今コードにてこれらのEC2を管理するにはキャパシティの検索、価格の変動を監視する等、複数のAPIを組み合わせてゴリゴリ書き込む必要がありました。

EC2 Spot Fleet API

今回発表された「EC2 Spot Fleet API」を使うとマネージメントコンソールにあるような条件をオプションとして付け足すだけでAWS側でスポットインスタンスの価格を監視し、自分の使いたい価格以下になったEC2を見つけ出して起動、逆に使いたい価格より高くなったEC2は自動的に廃棄する、といったローテーションをAPI一発で行ってくれます。これは便利ですね。しかも使えるキャパシティは1台から数千台まで自由に指定できます。

API内容

Spot Fleet APIはこのようなAPI群から構成されます。

request-spot-fleet

スポットインスタンスを購入するためのリクエストを送り、自動的にスポットインスタンスを管理します。今回のメインのAPIです。

  • --dry-run | --no-dry-run (boolean) : 実際に実行するかリハーサルとして動かすか
  • --spot-fleet-request-config : 必須。fleetをリクエストする際に必要な情報を書いたJSON形式のファイルを指定します
  • --cli-input-json : cliのパラメータをJSONファイルで記述します
  • --generate-cli-skeleton : パラメータを書くサンプルのJSONを標準出力に吐きます。
  • --terminate-instances : 以前のspot-fleet-requestで設定されているインスタンスを破棄する

--spot-fleet-request-config以外はCLI標準のどのAPIにも大抵ついてくるオプションなので飛ばします。--spot-fleet-request-configに書くJSONファイルですが全体像を書くとこんな感じになります。

         {
            "ClientToken": "string",
            "SpotPrice": "string",
            "TargetCapacity": integer,
            "ValidFrom": timestamp,
            "ValidUntil": timestamp,
            "TerminateInstancesWithExpiration": true|false,
            "IamFleetRole": "string",
            "LaunchSpecifications": [
              {
                "ImageId": "string",
                "KeyName": "string",
                "SecurityGroups": [
                  {
                    "GroupName": "string",
                    "GroupId": "string"
                  }
                  ...
                ],
                "UserData": "string",
                "AddressingType": "string",
                "InstanceType": "t1.micro"|"m1.small"|"m1.medium"|"m1.large"|"m1.xlarge"|"m3.medium"|"m3.large"|"m3.xlarge"|"m3.2xlarge"|"t2.micro"|"t2.small"|"t2.medium"|"m2.xlarge"|"m2.2xlarge"|"m2.4xlarge"|"cr1.8xlarge"|"i2.xlarge"|"i2.2xlarge"|"i2.4xlarge"|"i2.8xlarge"|"hi1.4xlarge"|"hs1.8xlarge"|"c1.medium"|"c1.xlarge"|"c3.large"|"c3.xlarge"|"c3.2xlarge"|"c3.4xlarge"|"c3.8xlarge"|"c4.large"|"c4.xlarge"|"c4.2xlarge"|"c4.4xlarge"|"c4.8xlarge"|"cc1.4xlarge"|"cc2.8xlarge"|"g2.2xlarge"|"cg1.4xlarge"|"r3.large"|"r3.xlarge"|"r3.2xlarge"|"r3.4xlarge"|"r3.8xlarge"|"d2.xlarge"|"d2.2xlarge"|"d2.4xlarge"|"d2.8xlarge",
                "Placement": {
                  "AvailabilityZone": "string",
                  "GroupName": "string"
                },
                "KernelId": "string",
                "RamdiskId": "string",
                "BlockDeviceMappings": [
                  {
                    "VirtualName": "string",
                    "DeviceName": "string",
                    "Ebs": {
                      "SnapshotId": "string",
                      "VolumeSize": integer,
                      "DeleteOnTermination": true|false,
                      "VolumeType": "standard"|"io1"|"gp2",
                      "Iops": integer,
                      "Encrypted": true|false
                    },
                    "NoDevice": "string"
                  }
                  ...
                ],
                "SubnetId": "string",
                "NetworkInterfaces": [
                  {
                    "NetworkInterfaceId": "string",
                    "DeviceIndex": integer,
                    "SubnetId": "string",
                    "Description": "string",
                    "PrivateIpAddress": "string",
                    "Groups": ["string", ...],
                    "DeleteOnTermination": true|false,
                    "PrivateIpAddresses": [
                      {
                        "PrivateIpAddress": "string",
                        "Primary": true|false
                      }
                     ...
                    ],
                    "SecondaryPrivateIpAddressCount": integer,
                    "AssociatePublicIpAddress": true|false
                  }
                  ...
                ],
                "IamInstanceProfile": {
                  "Arn": "string",
                  "Name": "string"
                },
                "EbsOptimized": true|false,
                "Monitoring": {
                  "Enabled": true|false
                }
              }
              ...
            ]
          }

ここから必要に応じて埋めていけば良いわけです。ちなみに私が今回試したJSONファイルは

{
    "TargetCapacity": 3,
    "SpotPrice": "0.50",
    "IamFleetRole": "arn:aws:iam::123456789012:role/fleettest",
    "LaunchSpecifications": [
        {
            "ImageId": "ami-cbf90ecb",
            "InstanceType": "m3.medium"
        },
        {
            "ImageId": "ami-cbf90ecb",
            "InstanceType": "m3.large"
        }
    ]
}

こう書くとシンプルだな、と思いませんか? 戻り値はSpotFleetRequestIdというリクエスト受理のIDが返ってきます。このIDはこの後ご案内するモニタリング系APIでモニタリングするリクエストを特定するために使います。

describe-spot-fleet-requests

スポットリクエストされている一覧を取得します

describe-spot-fleet-instances

特定のリクエストIDにて起動されたスポットインスタンスの一覧を取得します。

  • --spot-fleet-request-id : 対象となるリクエストIDを指定します

describe-spot-fleet-request-history

特定のリクエストIDにて指定された期間のスポットインスタンスの履歴を取得します。

  • --spot-fleet-request-id : 対象となるリクエストIDを指定します
  • --start-time : 履歴を参照する日時をISO 8601形式(2015-05-18T00:00:00Zのような形式)で書く

cancel-spot-fleet-request

指定したリクエストIDをキャンセルします。

  • --spot-fleet-request-ids : 対象となるリクエストIDを指定します。複数ある場合はスペース区切りで追記していきます
  • --terminate-instances | --no-terminate-instances : キャンセル時に起動しているインスタンスを破棄するかどうか

やってみた

それでは早速やってみましょう。まずはAWS CLIをアップデートします。

ip-172-16-0-104:~ seinotsuyoshi$ sudo pip install -U awscli
Password:
The directory '/Users/seinotsuyoshi/Library/Logs/pip' or its parent directory is not owned by the current user and the debug log has been disabled. Please check the permissions and owner of that directory. If executing pip with sudo, you may want sudo's -H flag.
The directory '/Users/seinotsuyoshi/Library/Caches/pip/http' or its parent directory is not owned by the current user and the cache has been disabled. Please check the permissions and owner of that directory. If executing pip with sudo, you may want sudo's -H flag.
The directory '/Users/seinotsuyoshi/Library/Caches/pip/http' or its parent directory is not owned by the current user and the cache has been disabled. Please check the permissions and owner of that directory. If executing pip with sudo, you may want sudo's -H flag.
/Library/Python/2.7/site-packages/pip-6.1.1-py2.7.egg/pip/_vendor/requests/packages/urllib3/util/ssl_.py:79: InsecurePlatformWarning: A true SSLContext object is not available. This prevents urllib3 from configuring SSL appropriately and may cause certain SSL connections to fail. For more information, see https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning.
  InsecurePlatformWarning
Collecting awscli
  Downloading awscli-1.7.28.tar.gz (336kB)
    100% |████████████████████████████████| 339kB 820kB/s
Collecting botocore<0.110.0,>=0.109.0 (from awscli)
  Downloading botocore-0.109.0.tar.gz (1.6MB)
    100% |████████████████████████████████| 1.6MB 235kB/s
Collecting bcdoc<0.15.0,>=0.14.0 (from awscli)
  Downloading bcdoc-0.14.0-py2.py3-none-any.whl
Requirement already up-to-date: colorama<=0.3.3,>=0.2.5 in /Library/Python/2.7/site-packages (from awscli)
Requirement already up-to-date: docutils>=0.10 in /Library/Python/2.7/site-packages (from awscli)
Requirement already up-to-date: rsa<=3.1.4,>=3.1.2 in /Library/Python/2.7/site-packages (from awscli)
Collecting jmespath==0.7.1 (from botocore<0.110.0,>=0.109.0->awscli)
  Downloading jmespath-0.7.1-py2.py3-none-any.whl
Collecting python-dateutil<3.0.0,>=2.1 (from botocore<0.110.0,>=0.109.0->awscli)
  Downloading python_dateutil-2.4.2-py2.py3-none-any.whl (188kB)
    100% |████████████████████████████████| 192kB 1.5MB/s
Collecting six<2.0.0,>=1.8.0 (from bcdoc<0.15.0,>=0.14.0->awscli)
  Downloading six-1.9.0-py2.py3-none-any.whl
Requirement already up-to-date: pyasn1>=0.1.3 in /Library/Python/2.7/site-packages (from rsa<=3.1.4,>=3.1.2->awscli)
Installing collected packages: jmespath, six, python-dateutil, botocore, bcdoc, awscli
  Found existing installation: jmespath 0.6.1
    Uninstalling jmespath-0.6.1:
      Successfully uninstalled jmespath-0.6.1
  Found existing installation: six 1.8.0
    Uninstalling six-1.8.0:
      Successfully uninstalled six-1.8.0
  Found existing installation: python-dateutil 2.2
    Uninstalling python-dateutil-2.2:
      Successfully uninstalled python-dateutil-2.2
  Found existing installation: botocore 0.103.0
    Uninstalling botocore-0.103.0:
      Successfully uninstalled botocore-0.103.0
  Running setup.py install for botocore
  Found existing installation: bcdoc 0.13.0
    Uninstalling bcdoc-0.13.0:
      Successfully uninstalled bcdoc-0.13.0
  Found existing installation: awscli 1.7.22
    Uninstalling awscli-1.7.22:
      Successfully uninstalled awscli-1.7.22
  Running setup.py install for awscli
Successfully installed awscli-1.7.28 bcdoc-0.14.0 botocore-0.109.0 jmespath-0.7.1 python-dateutil-2.4.2 six-1.9.0
ip-172-16-0-104:~ seinotsuyoshi$ aws --version
aws-cli/1.7.28 Python/2.7.6 Darwin/14.3.0
ip-172-16-0-104:~ seinotsuyoshi$

IAMロールの作成

Spot Fleet APIは価格によってEC2を立ち上げたり廃棄したりしなくてはいけないので、その権限をIAMロールでつけます。IAMロールの中にSpot Fleet用のIAMロールがあるので選択します。

spotfleetapi2

Spot Fleet用のIAMロールを選択すると対象となるポリシーが一つしか出てきませんのでそちらを選択します。

spotfleetapi3

確認します。このARNを後で使うのでコピーしておきましょう。

spotfleetapi4

configファイルを作る

APIを叩くマシンにどのようなスポットインスタンスをリクエストするのかのJSONファイルを指定します。ファイル名はなんでも良いのですが、config.jsonとしておきます。

{
    "TargetCapacity": 3,
    "SpotPrice": "0.50",
    "IamFleetRole": "arn:aws:iam::123456789012:role/fleettest",
    "LaunchSpecifications": [
        {
            "ImageId": "ami-cbf90ecb",
            "InstanceType": "m3.medium"
        },
        {
            "ImageId": "ami-cbf90ecb",
            "InstanceType": "m3.large"
        }
    ]
}

コマンド送信

ここまで準備したら後はコマンドを叩くだけです。まずリクエストしてみましょう。

ip-172-16-0-104:dev seinotsuyoshi$ aws ec2 request-spot-fleet --spot-fleet-request-config file://config.json --region ap-northeast-1
{
    "SpotFleetRequestId": "sfr-a37733f5-8221-406f-8c27-d92dd98c0164"
}
ip-172-16-0-104:dev seinotsuyoshi$

リクエストIDが返ってきました。どうなってるか見てみましょう。EC2の[スポットリクエスト]のナビを開きます。

spotfleetapi5

設定通り3つのスポットインスタンスのリクエストが飛んでいるのが確認できます。価格が下のインスタンスが見つかると実際にEC2が立ち上がり始めます。

spotfleetapi6

次にモニタリング系のAPIも確認してみましょう。

ip-172-16-0-104:dev seinotsuyoshi$ aws ec2 describe-spot-fleet-requests
{
    "SpotFleetRequestConfigs": [
        {
            "SpotFleetRequestId": "sfr-a37733f5-8221-406f-8c27-d92dd98c0164",
            "SpotFleetRequestConfig": {
                "TargetCapacity": 3,
                "LaunchSpecifications": [
                    {
                        "EbsOptimized": false,
                        "InstanceType": "m3.medium",
                        "ImageId": "ami-cbf90ecb"
                    },
                    {
                        "EbsOptimized": false,
                        "InstanceType": "m3.large",
                        "ImageId": "ami-cbf90ecb"
                    }
                ],
                "SpotPrice": "0.5",
                "IamFleetRole": "arn:aws:iam::123456789012:role/fleettest"
            },
            "SpotFleetRequestState": "active"
        }
    ]
}
ip-172-16-0-104:dev seinotsuyoshi$ aws ec2 describe-spot-fleet-instances --spot-fleet-request-id sfr-a37733f5-8221-406f-8c27-d92dd98c0164
{
    "ActiveInstances": [
        {
            "InstanceId": "i-72fdf881",
            "InstanceType": "m3.medium",
            "SpotInstanceRequestId": "sir-031pbt32"
        },
        {
            "InstanceId": "i-32fcf9c1",
            "InstanceType": "m3.medium",
            "SpotInstanceRequestId": "sir-031qencw"
        },
        {
            "InstanceId": "i-e2fcf911",
            "InstanceType": "m3.medium",
            "SpotInstanceRequestId": "sir-031mr5p8"
        }
    ],
    "SpotFleetRequestId": "sfr-a37733f5-8221-406f-8c27-d92dd98c0164"
}
ip-172-16-0-104:dev seinotsuyoshi$ aws ec2 describe-spot-fleet-request-history --spot-fleet-request-id sfr-a37733f5-8221-406f-8c27-d92dd98c0164 --start-time 2015-05-18T00:00:00
{
    "HistoryRecords": [
        {
            "Timestamp": "2015-05-19T06:05:27.858Z",
            "EventInformation": {
                "EventSubType": "submitted"
            },
            "EventType": "fleetRequestChange"
        },
        {
            "Timestamp": "2015-05-19T06:05:28.044Z",
            "EventInformation": {
                "EventSubType": "active"
            },
            "EventType": "fleetRequestChange"
        },
        {
            "Timestamp": "2015-05-19T06:09:30.311Z",
            "EventInformation": {
                "InstanceId": "i-e2fcf911",
                "EventSubType": "launched"
            },
            "EventType": "instanceChange"
        },
        {
            "Timestamp": "2015-05-19T06:09:30.359Z",
            "EventInformation": {
                "InstanceId": "i-72fdf881",
                "EventSubType": "launched"
            },
            "EventType": "instanceChange"
        },
        {
            "Timestamp": "2015-05-19T06:09:30.445Z",
            "EventInformation": {
                "InstanceId": "i-32fcf9c1",
                "EventSubType": "launched"
            },
            "EventType": "instanceChange"
        }
    ],
    "SpotFleetRequestId": "sfr-a37733f5-8221-406f-8c27-d92dd98c0164",
    "LastEvaluatedTime": "2015-05-19T06:19:09+0000",
    "StartTime": "2015-05-18T00:00:00Z"
}
ip-172-16-0-104:dev seinotsuyoshi$

どんなコマンドを叩いたらどういう値が返ってくるのか、大体お分かりになったかと思います。それでは最後にスポットリクエストをキャンセルして破棄してみましょう。

ip-172-16-0-104:dev seinotsuyoshi$ aws ec2 cancel-spot-fleet-requests --spot-fleet-request-ids sfr-a37733f5-8221-406f-8c27-d92dd98c0164 --terminate-instances
{
    "SuccessfulFleetRequests": [
        {
            "SpotFleetRequestId": "sfr-a37733f5-8221-406f-8c27-d92dd98c0164",
            "CurrentSpotFleetRequestState": "cancelled_terminating",
            "PreviousSpotFleetRequestState": "active"
        }
    ],
    "UnsuccessfulFleetRequests": []
}
ip-172-16-0-104:dev seinotsuyoshi$

これでキャンセルされました。--terminate-instancesオプションを付けたので現在起動しているインスタンスも全て破棄されます。

spotfleetapi7

まとめ

いかがでしょうか。このAPIは相当使えます。皆さんも是非お試しになって下さい。

参考サイト