AWS SDK for Java
開発者ガイド

チュートリアル: Amazon EC2 スポットインスタンス

概要

スポットインスタンスとは、Amazon Elastic Compute Cloud (Amazon EC2) の未使用キャパシティに対してお客様から価格を提示していただき、入札価格がその時点のスポット料金を上回っている限り、お客様がそのインスタンスを取得し、実行できるというシステムです。Amazon EC2 のスポット料金は、需要と供給に基づいて定期的に変動しますが、お客様の入札価格がその価格以上ならば、空いているスポットインスタンスにアクセスできます。オンデマンドインスタンスやリザーブドインスタンスと同様に、スポットインスタンスは計算キャパシティを増やしたいときの選択肢の 1 つとなります。

スポットインスタンスを利用すると、Amazon EC2 によるバッチ処理、科学研究、画像処理、動画エンコーディング、データと Web のクローリング、財務分析、テストなどのコストの大幅削減を期待できます。加えて、スポットインスタンスは、大量の追加計算キャパシティが必要であるけれどもその緊急性が低いという場合にも適しています。

スポットインスタンスを使用するには、スポットインスタンスリクエストを提出し、このときにインスタンス時間当たりいくらまで支払えるかを指定します。これが入札価格です。入札価格がその時点のスポット価格を超えている場合は、リクエストが受理されてインスタンスを実行できるようになります。このインスタンスの実行は、お客様がインスタンスを終了した時点と、スポット価格が入札価格を上回った時点のいずれか早い方までとなります。

次のことに注意することが重要です。

  • 時間当たりの支払い金額が入札価格を下回ることもよくあります。Amazon EC2 のスポット料金は、提出されるリクエストや空きインスタンスの変動に応じて、定期的に変更されます。お客様それぞれの入札価格の方が上かどうかにかかわらず、どのお客様もその期間の同一のスポット料金をお支払いいただきます。したがって、お客様が支払う金額は入札価格を下回ることもありますが、入札価格を超えることはありません。

  • スポットインスタンスを実行しているときに、お客様の入札価格がその時点のスポット料金以上ではなくなった場合は、そのインスタンスは終了となります。つまり、この変動性の高いキャパシティを活用できる、柔軟性の高いワークロードとアプリケーションに限ってスポットインスタンスを利用することをお勧めします。

スポットインスタンスは稼働中、他の Amazon EC2 インスタンスとまったく同じように動作します。そして他の Amazon EC2 インスタンスと同様に、スポットインスタンスは必要がなくなった場合に終了することができます。お客様がインスタンスを終了した場合は、使用時間の端数分についても料金をいただきます (オンデマンドやリザーブドのインスタンスと同様です)。ただし、スポット価格がお客様の入札価格を超えたためにインスタンスが Amazon EC2 によって終了させられた場合は、使用時間の端数分の料金は発生しません。

このチュートリアルでは、AWS SDK for Java を使用して以下を行う方法について説明します。

  • スポットリクエストを提出する

  • スポットリクエストが受理されたかどうかを判断する

  • スポットリクエストをキャンセルする

  • 関連するインスタンスを終了させる

前提条件

このチュートリアルを使用するには、AWS SDK for Java がインストールされており、基本インストール前提条件を満たしている必要があります。詳細については、「AWS SDK for Java のセットアップ」を参照してください。

ステップ 1: 認証情報のセットアップ

このサンプルコードの使用を開始するには、AWS 認証情報を設定する必要があります。その方法については、「開発用の AWS 認証情報とリージョンのセットアップ」を参照してください。

注記

IAM ユーザーの認証情報を使用してこれらの値を指定することをお勧めします。詳細については、「AWS にサインアップし、IAM ユーザーを作成する」を参照してください。

これで設定が完了したので、例に示すコードを使用できるようになります。

ステップ 2: セキュリティグループのセットアップ

セキュリティグループとは、ファイアウォールとしての役割を果たすものであり、インスタンスのグループに対してどのトラフィックの送受信を許可するかを制御します。デフォルトでは、インスタンスの起動時にセキュリティグループは何も設定されていません。つまり、着信 IP トラフィックは、どの TCP ポートであってもすべて拒否されます。したがって、ここでは、スポットリクエストを提出する前に、必要なネットワークトラフィックを許可するセキュリティグループをセットアップすることにします。このチュートリアルの目的に合わせて、ここでは新しいセキュリティグループを「GettingStarted」という名前で作成します。このグループでは、自分のアプリケーションを実行する IP アドレスからの Secure Shell (SSH) トラフィックを許可します。新しいセキュリティグループをセットアップするには、次に示すコードサンプルをインクルードするか実行する必要があります。このコードは、セキュリティグループをプログラムからセットアップするためのものです。

AmazonEC2 クライアントオブジェクトを作成した後で、CreateSecurityGroupRequest オブジェクトを作成し、「GettingStarted」という名前と、セキュリティグループの説明を指定します。その後で、ec2.createSecurityGroup API を呼び出してグループを作成します。

このグループにアクセスできるようにするために、ipPermission オブジェクトを作成します。IP アドレス範囲は、ローカルコンピュータのサブネット (CIDR 表現) で設定します。IP アドレスの「/10」というサフィックスが、指定した IP アドレスのサブネットを示します。また、ipPermission オブジェクトを設定して TCP プロトコルとポート 22 (SSH) を指定します。最後のステップは、ec2.authorizeSecurityGroupIngress を呼び出すことです。このときに、作成したセキュリティグループの名前と ipPermission オブジェクトを指定します。

// Create the AmazonEC2 client so we can call various APIs. AmazonEC2 ec2 = AmazonEC2ClientBuilder.defaultClient(); // Create a new security group. try { CreateSecurityGroupRequest securityGroupRequest = new CreateSecurityGroupRequest("GettingStartedGroup", "Getting Started Security Group"); ec2.createSecurityGroup(securityGroupRequest); } catch (AmazonServiceException ase) { // Likely this means that the group is already created, so ignore. System.out.println(ase.getMessage()); } String ipAddr = "0.0.0.0/0"; // Get the IP of the current host, so that we can limit the Security // Group by default to the ip range associated with your subnet. try { InetAddress addr = InetAddress.getLocalHost(); // Get IP Address ipAddr = addr.getHostAddress()+"/10"; } catch (UnknownHostException e) { } // Create a range that you would like to populate. ArrayList<String> ipRanges = new ArrayList<String>(); ipRanges.add(ipAddr); // Open up port 22 for TCP traffic to the associated IP // from above (e.g. ssh traffic). ArrayList<IpPermission> ipPermissions = new ArrayList<IpPermission> (); IpPermission ipPermission = new IpPermission(); ipPermission.setIpProtocol("tcp"); ipPermission.setFromPort(new Integer(22)); ipPermission.setToPort(new Integer(22)); ipPermission.setIpRanges(ipRanges); ipPermissions.add(ipPermission); try { // Authorize the ports to the used. AuthorizeSecurityGroupIngressRequest ingressRequest = new AuthorizeSecurityGroupIngressRequest("GettingStartedGroup",ipPermissions); ec2.authorizeSecurityGroupIngress(ingressRequest); } catch (AmazonServiceException ase) { // Ignore because this likely means the zone has // already been authorized. System.out.println(ase.getMessage()); }

このアプリケーションを実行して新しいセキュリティグループを作成する必要があるのは 1 回のみです。

また、AWS Toolkit for Eclipse を使用してセキュリティグループを作成することもできます。詳細については、「AWS Explorer からセキュリティグループを管理する」参照してください。

ステップ 3: スポットリクエストを提出する

スポットリクエストを提出するには、最初に、使用するインスタンスタイプ、Amazon マシンイメージ (AMI)、最高入札価格を決定する必要があります。前のステップで設定したセキュリティグループも指定する必要があります。これは、必要に応じてインスタンスにログインできるようにするためです。

選択できるインスタンスタイプにはさまざまなものがあります。すべての一覧については、Amazon EC2 インスタンスタイプのページを参照してください。このチュートリアルでは、最も低価格のインスタンスタイプである t1.micro を使用します。次に、使用する AMI のタイプを決定します。ここでは、ami-a9d09ed1 を使用します。これは、このチュートリアルの執筆時点で最新の Amazon Linux AMI です。最新の AMI は時間の経過と共に変化する可能性がありますが、次のステップを実行することで最新バージョンの AMI であることを常に判断できます。

  1. Amazon EC2 コンソールを開きます。

  2. [Launch Instance (インスタンスの起動)] ボタンを選択します。

  3. 最初のウィンドウには、利用可能な AMI が表示されます。各 AMI のタイトルの横には、AMI の ID が表示されます。DescribeImages API を使用することもできますが、このコマンドの利用方法は、このチュートリアルでは取り上げません。

スポットインスタンス入札のアプローチは多数あります。さまざまなアプローチの概要については、スポットインスタンスの入札の動画をご覧ください。ただし、ここでは初めての方のために、3 つの一般的な戦略について説明します。その 3 つとは、「コストがオンデマンド価格より低くなるように入札する」、「計算処理の結果の価値に基づいて入札する」、「できるだけ早く計算キャパシティーを獲得できるように入札する」です。

  • コストをオンデマンドよりも低くする 実行完了までに何時間も、あるいは何日間もかかるバッチ処理ジョブがあるとします。ただし、いつ開始していつ完了するかについては、特に決められていないものとします。このジョブを完了するためのコストを、オンデマンドインスタンスを使用する場合よりも低くできるかどうかを考えます。インスタンスタイプ別のスポット料金の履歴を、AWS マネジメントコンソールまたは Amazon EC2 API を使用して調べます。詳細については、「スポット価格の履歴の表示」を参照してください。使用したいインスタンスタイプの、特定のアベイラビリティーゾーンでの価格履歴を分析した後は、入札のアプローチとして次の 2 つも考えられます。

    • スポット料金の範囲の上限(ただしオンデマンド価格よりは下)で入札します。このようにすれば、この 1 回限りのスポットリクエストが受理される可能性が高くなり、ジョブが完了するまで連続して実行できるからです。

    • または、価格範囲の下限で入札し、1 つの永続リクエストで次々とインスタンスを起動するよう計画を立てます。これらのインスタンスの実行時間を合計すると、ジョブを完了するのに十分な長さとなり、合計コストも低くなります。(この作業を自動化する方法については、このチュートリアルで後ほど説明します)

  • 結果の価値以上は支払わない データ処理ジョブを実行するとします。このジョブの結果の価値は判明しており、計算コストに換算してどれくらいになるかもわかっています。使用するインスタンスタイプのスポット料金履歴の分析が完了した後で、入札価格を選択します。計算時間のコストがこのジョブの結果の価値を上回ることがないように、価格を決定します。永続リクエストを作成し、スポット料金が入札価格以下となったときに断続的に実行するよう設定します。

  • 計算キャパシティをすぐに獲得する 追加のキャパシティが突然、短期間だけ必要になることがあり、オンデマンドインスタンスではそのキャパシティを獲得できないとします。使用するインスタンスタイプのスポット価格履歴の分析が完了した後で、履歴の価格の最大値を超える価格で入札します。このようにすれば、リクエストがすぐに受理される可能性が高まり、計算が完了するまで連続して計算できるようになります。

入札価格を選択すると、スポットインスタンスをリクエストできる状態になります。ここでは、このチュートリアルの目的に合わせて、オンデマンド価格 (0.03 USD) で入札します。これは、受理される可能性を最大にするためです。利用できるインスタンスのタイプと、インスタンスのオンデマンド価格を調べるには、Amazon EC2 の価格表ページを参照してください。スポットインスタンスをリクエストするには、これまでに選択したパラメータを使用してリクエストを作成します。初めに、RequestSpotInstanceRequest オブジェクトを作成します。このリクエストオブジェクトには、起動したいインスタンスの数と入札価格が必要です。さらに、リクエストの LaunchSpecification を設定する必要があります。この内容は、インスタンスタイプ、AMI ID、および使用するセキュリティグループです。リクエストの内容が入力されたら、requestSpotInstances オブジェクトの AmazonEC2Client メソッドを呼び出します。次の例で、スポットインスタンスをリクエストする方法を示します。

// Create the AmazonEC2 client so we can call various APIs. AmazonEC2 ec2 = AmazonEC2ClientBuilder.defaultClient(); // Initializes a Spot Instance Request RequestSpotInstancesRequest requestRequest = new RequestSpotInstancesRequest(); // Request 1 x t1.micro instance with a bid price of $0.03. requestRequest.setSpotPrice("0.03"); requestRequest.setInstanceCount(Integer.valueOf(1)); // Setup the specifications of the launch. This includes the // instance type (e.g. t1.micro) and the latest Amazon Linux // AMI id available. Note, you should always use the latest // Amazon Linux AMI id or another of your choosing. LaunchSpecification launchSpecification = new LaunchSpecification(); launchSpecification.setImageId("ami-a9d09ed1"); launchSpecification.setInstanceType(InstanceType.T1Micro); // Add the security group to the request. ArrayList<String> securityGroups = new ArrayList<String>(); securityGroups.add("GettingStartedGroup"); launchSpecification.setSecurityGroups(securityGroups); // Add the launch specifications to the request. requestRequest.setLaunchSpecification(launchSpecification); // Call the RequestSpotInstance API. RequestSpotInstancesResult requestResult = ec2.requestSpotInstances(requestRequest);

このコードを実行すると、新しいスポットインスタンスリクエストが発行されます。他にも、スポットリクエストの設定に使用できるオプションがあります。詳細については、「チュートリアル: Amazon EC2 スポットリクエストの高度な管理」または『AWS SDK for Java API Reference』の「RequestSpotInstances」クラスを参照してください。

注記

スポットインスタンスが実際に起動されるとお客様への課金が発生するので、料金を抑えるために、リクエストを作成した場合はキャンセルし、インスタンスを起動した場合は終了してください。

ステップ 4: スポットリクエストの状態を特定する

次に、最後のステップに進む前にスポットリクエストの状態が「アクティブ」になるのを待つようにするコードを作成する必要があります。スポットリクエストの状態を特定するには、describeSpotInstanceRequests メソッドをポーリングすることによって、モニタリング対象のスポットリクエスト ID の状態を調べます。

ステップ 2 で作成したリクエスト ID は、requestSpotInstances リクエストへのレスポンスに埋め込まれています。次に示すコード例では、リクエスト ID を requestSpotInstances レスポンスから取り出して ArrayList への入力に使用する方法を示します。

// Call the RequestSpotInstance API. RequestSpotInstancesResult requestResult = ec2.requestSpotInstances(requestRequest); List<SpotInstanceRequest> requestResponses = requestResult.getSpotInstanceRequests(); // Setup an arraylist to collect all of the request ids we want to // watch hit the running state. ArrayList<String> spotInstanceRequestIds = new ArrayList<String>(); // Add all of the request ids to the hashset, so we can determine when they hit the // active state. for (SpotInstanceRequest requestResponse : requestResponses) { System.out.println("Created Spot Request: "+requestResponse.getSpotInstanceRequestId()); spotInstanceRequestIds.add(requestResponse.getSpotInstanceRequestId()); }

リクエスト ID をモニタリングするには、describeSpotInstanceRequests メソッドを呼び出してリクエストの状態を特定します。その後で、リクエストが「オープン」状態でなくなるまでループを繰り返します。状態が、例えば「アクティブ」ではなく、「オープン」以外かどうかをモニタリングするのは、リクエストが直接「クローズ済み」に遷移することもあるからです (リクエストの引数に問題がある場合)。次に示すコード例では、このことを実現する具体的な方法を示します。

// Create a variable that will track whether there are any // requests still in the open state. boolean anyOpen; do { // Create the describeRequest object with all of the request ids // to monitor (e.g. that we started). DescribeSpotInstanceRequestsRequest describeRequest = new DescribeSpotInstanceRequestsRequest(); describeRequest.setSpotInstanceRequestIds(spotInstanceRequestIds); // Initialize the anyOpen variable to false - which assumes there // are no requests open unless we find one that is still open. anyOpen=false; try { // Retrieve all of the requests we want to monitor. DescribeSpotInstanceRequestsResult describeResult = ec2.describeSpotInstanceRequests(describeRequest); List<SpotInstanceRequest> describeResponses = describeResult.getSpotInstanceRequests(); // Look through each request and determine if they are all in // the active state. for (SpotInstanceRequest describeResponse : describeResponses) { // If the state is open, it hasn't changed since we attempted // to request it. There is the potential for it to transition // almost immediately to closed or cancelled so we compare // against open instead of active. if (describeResponse.getState().equals("open")) { anyOpen = true; break; } } } catch (AmazonServiceException e) { // If we have an exception, ensure we don't break out of // the loop. This prevents the scenario where there was // blip on the wire. anyOpen = true; } try { // Sleep for 60 seconds. Thread.sleep(60*1000); } catch (Exception e) { // Do nothing because it woke up early. } } while (anyOpen);

このコードを実行すると、スポットインスタンスリクエストは完了するか、エラーありで失敗し、そのエラーが画面に出力されます。どちらの場合も、次のステップに進んで、アクティブなリクエストがある場合はクリーンアップし、実行中のインスタンスがある場合は終了させてください。

ステップ 5: スポットリクエストとインスタンスをクリーンアップする

最後に、リクエストとインスタンスをクリーンアップする必要があります。未完了リクエストのキャンセルと、インスタンスの削除の両方を行うことが重要です。リクエストをキャンセルするだけではインスタンスは終了しないので、引き続きお客様への課金が発生することになります。インスタンスを削除すると、スポットリクエストがキャンセルされることもありますが、場合によっては (持続的入札を使用した場合など)、インスタンスを終了しただけでは、リクエストが再度受理されるのを停止できないことがあります。したがって、アクティブな入札のキャンセルと実行中インスタンスの削除の両方を行うことをお勧めします。

次のコードでは、リクエストをキャンセルする方法を示します。

try { // Cancel requests. CancelSpotInstanceRequestsRequest cancelRequest = new CancelSpotInstanceRequestsRequest(spotInstanceRequestIds); ec2.cancelSpotInstanceRequests(cancelRequest); } catch (AmazonServiceException e) { // Write out any exceptions that may have occurred. System.out.println("Error cancelling instances"); System.out.println("Caught Exception: " + e.getMessage()); System.out.println("Reponse Status Code: " + e.getStatusCode()); System.out.println("Error Code: " + e.getErrorCode()); System.out.println("Request ID: " + e.getRequestId()); }

稼働中のインスタンスを終了させるには、そのインスタンスを起動したリクエストに関連付けられているインスタンス ID が必要です。次のコード例は、前に示したインスタンスをモニタリングするためのコードに ArrayList を追加したものです。この中に、describeInstance レスポンスに関連付けられているインスタンス ID を格納します。

// Create a variable that will track whether there are any requests // still in the open state. boolean anyOpen; // Initialize variables. ArrayList<String> instanceIds = new ArrayList<String>(); do { // Create the describeRequest with all of the request ids to // monitor (e.g. that we started). DescribeSpotInstanceRequestsRequest describeRequest = new DescribeSpotInstanceRequestsRequest(); describeRequest.setSpotInstanceRequestIds(spotInstanceRequestIds); // Initialize the anyOpen variable to false, which assumes there // are no requests open unless we find one that is still open. anyOpen = false; try { // Retrieve all of the requests we want to monitor. DescribeSpotInstanceRequestsResult describeResult = ec2.describeSpotInstanceRequests(describeRequest); List<SpotInstanceRequest> describeResponses = describeResult.getSpotInstanceRequests(); // Look through each request and determine if they are all // in the active state. for (SpotInstanceRequest describeResponse : describeResponses) { // If the state is open, it hasn't changed since we // attempted to request it. There is the potential for // it to transition almost immediately to closed or // cancelled so we compare against open instead of active. if (describeResponse.getState().equals("open")) { anyOpen = true; break; } // Add the instance id to the list we will // eventually terminate. instanceIds.add(describeResponse.getInstanceId()); } } catch (AmazonServiceException e) { // If we have an exception, ensure we don't break out // of the loop. This prevents the scenario where there // was blip on the wire. anyOpen = true; } try { // Sleep for 60 seconds. Thread.sleep(60*1000); } catch (Exception e) { // Do nothing because it woke up early. } } while (anyOpen);

この ArrayList に格納されているインスタンス ID を使用して、稼働中のインスタンスを終了させます。コードは次のとおりです。

try { // Terminate instances. TerminateInstancesRequest terminateRequest = new TerminateInstancesRequest(instanceIds); ec2.terminateInstances(terminateRequest); } catch (AmazonServiceException e) { // Write out any exceptions that may have occurred. System.out.println("Error terminating instances"); System.out.println("Caught Exception: " + e.getMessage()); System.out.println("Reponse Status Code: " + e.getStatusCode()); System.out.println("Error Code: " + e.getErrorCode()); System.out.println("Request ID: " + e.getRequestId()); }

ステップの集約

これまでに説明したステップは、よりオブジェクト指向的なアプローチをとって 1 つに集約することができます。このステップとは、EC2 クライアントの初期化、スポットリクエストの提出、スポットリクエストがオープン状態でなくなったかどうかの特定、および未完了のスポットリクエストや関連するインスタンスのクリーンアップです。これらのすべてを実行する、Requests というクラスを作成します。

さらに、GettingStartedApp というクラスも作成します。ここにメインメソッドがあり、ここで高レベルの関数呼び出しを実行します。具体的には、既に説明した Requests オブジェクトを初期化します。スポットインスタンスリクエストを提出します。その後は、スポットリクエストが「アクティブ」状態になるまで待ちます。最後に、リクエストとインスタンスをクリーンアップします。

この例の完全なソースコードは、GitHub で確認またはダウンロードできます。

おめでとうございます。 これで、AWS SDK for Java を使用したスポットインスタンスソフトウェア開発の入門チュートリアルは終了です。

次のステップ

チュートリアル: Amazon EC2 スポットリクエストの高度な管理」に進みます。