Amazon Web Services ブログ

Amazon EC2 スポットインスタンスを活用したウェブアプリケーションの構築

本記事は、EC2スポットインスタンススペシャリスト シニアソリューションアーキテクトのIsaac Vallhonratによる寄稿です。

Amazon EC2 スポットインスタンスを使うと、AWS クラウド内の使用されていない EC2 キャパシティーを用いて、オンデマンド料金に比べ最大 90% の割引価格でご利用いただけます。スポットインスタンスは、バッチジョブ、ビルド等のCI/CDパイプライン、負荷テスト、コンテナ化されたワークロード、ウェブアプリケーション、ビッグデータの分析クラスター、ハイパフォーマンスコンピューティング(HPC)用計算クラスターなど、複数のインスタンスタイプで柔軟に実行できる、耐障害性を備えたワークロードに最適です。このブログ投稿では、スポットインスタンスでウェブアプリケーションを実行するための方法とベストプラクティスについて説明し、これによりもたらされるスケールと費用節減の両方のメリットを得られるようにします。

スポットインスタンスには中断という特徴があります。この特徴を踏まえて、これから構築するウェブアプリケーションはステートレスかつ耐障害性があり、また疎結合されていることが望ましいです。また永続データの保持には Amazon ElastiCache, AmazonRDS, Amazon DynamoDB などの外部データストアを使用する必要があります。

スポットインスタンスのおさらい

2009 年に提供開始されたスポットインスタンスは、ここ最近のアップデートや関連サービスとの統合によって、お使いのワークロードで格段に活用しやすくなっています。ウェブアプリケーションを構築する方法の詳細に入る前に、スポットインスタンスの動作の概要のおさらいにお付き合いください。

まず、スポットインスタンスは EC2 の購入オプション、買い方のひとつです。 他の購入オプションである、オンデマンドインスタンスリザーブドインスタンスSavings Plansで起動した場合と比べて、EC2インスタンスとして提供するハードウェアに違いはありません。スポットインスタンスと他の購入オプションの違いはただ一つ、EC2 サービスが容量を必要とする場合には、2 分前に通知したのち、EC2サービスがスポットインスタンスを中断する、という動作です。つまり、大幅な割引価格で提供する代わりに、オンデマンドインスタンスやリザーブドインスタンスからの起動需要が高まってきたとき、スポットインスタンスの使用していたキャパシティをEC2サービスに戻し、需要に応える、というのがスポットインスタンスサービスの動作原理です。

スポットインスタンスは、スポットキャパシティプールと呼ばれる、いわば空きキャパシティがある限り起動できます。スポットキャパシティプール(スポットプール)とは、とは、インスタンスタイプ (m5.large など), オペレーティングシステム種別(Linuxなど), アベイラビリティーゾーン (us-east-1a など) が同一である、Amazon EC2 サービスが使用していない(空の) EC2 インスタンスの集合を指します。属性の異なるプール同士はそれぞれ独立したプールとして区別されます。例えば、us-east-1aゾーンのLinux向けm5.largeのスポットプールと、us-east-1bゾーンのLinux向けm5.largeのスポットプールは、独立した別のプールです。このそれぞれに空きがあるとき、スポットインスタンスを起動し、使用できます。

スポットインスタンスの料金は Amazon EC2 サービスによって設定され、各プールの EC2 インスタンスの需要と供給の長期的な傾向に基づき、徐々に調整されます。スポット料金は急激に変化することはなく、突然のスパイクや変動がないことが期待できます。 EC2 マネジメントコンソールと API の両方から、最大過去 3 か月間の価格履歴データを表示できます。次の図は、バージニア北部 (us-east-1) リージョンにおける m5.xlarge インスタンスの料金履歴の例です。

図: us-east-1 の m5.xlarge Linux/UNIX インスタンスのスポットインスタンスの価格履歴。価格は、アベイラビリティーゾーンごとに独立して変化し、長期の需要と供給に基づいて時間の経過とともに緩やかに変動する

それぞれのスポットプールは互いに独立しています。このことから、スポットインスタンスを活用するには、稼働するインスタンス群を複数のアベイラビリティーゾーン (AZ) に分散し、複数のインスタンスタイプを使用できるよう、柔軟に設計することをお勧めします。これにより、スポットインスタンスを起動できるAWS内の未使用の容量(空き容量)を効果的に発見し、活用することができます。また仮にスポットインスタンスの中断が発生するような場面においても、他のプールからの起動を期待できます。

このことから、スポットインスタンスのベストプラクティスは、複数のインスタンスタイプを活用する、ということになります。これを「インスタンスタイプに柔軟性を持たせる」という表現で言い表します。あるいは「インスタンスタイプの柔軟性」という言い方をすることもあります。この柔軟性の原則に則ってお使いのインスタンスを管理する最善の方法のひとつに、 Amazon EC2 Auto Scaling グループの複数のインスタンスタイプと購入オプションを組み合わせる機能の使用が挙げられます。性能試験などのテストにより、ワークロードに適したインスタンスタイプをいくつか選定したのちに、各アベイラビリティーゾーンの最適なスポットインスタンスプールから、Auto Scaling 機能を使用してオンデマンドインスタンスとスポットインスタンスの組み合わせを起動できます。

一般に、ステートレスウェブアプリケーションは耐障害性が比較的高いとされるため、中断のあるスポットインスタンスを活用するのに向いたワークロードであると言えます。またステートレスウェブアプリケーションでは複数のインスタンスタイプを活用し、複数のアベイラビリティーゾーンで実行することもごく自然な選択です。したがって、スポットインスタンスの運用を安定させるために、複数のスポットプールを活用することへの親和性も自然に備わっていると言えます。

ウェブアプリケーションに適したインスタンスタイプを選択する

これまで述べてきたように、インスタンスタイプを柔軟に選択できるようにしておくことが、スポットインスタンスを成功させるための鍵となります。この記事の執筆時点で AWS は 270 を超えるインスタンスタイプを提供しており、ウェブアプリケーションを複数のインスタンスタイプで実行するための幅広い選択肢になっています。

たとえば、アプリケーションが m5.xlarge インスタンスタイプで実行されている場合を考えます。このとき、m5d.xlarge は m5.xlarge にインスタンスストア (ローカル SSD) が追加されたインスタンスタイプですので、このアプリケーションはm5d.xlarge インスタンスタイプでもそのまま実行することができるでしょう。これに加えて、r5.xlarge または r5d.xlargeでも実行可能である可能性が高いです。なぜならば、r5.xlarge または r5d.xlarge は m5.xlrage および m5d.xlarge と同じプロセッサファミリーを搭載するためです。さらに、Rファミリーという、Mファミリーよりも多くのメモリを持つインスタンスタイプで実行される場合、スポットインスタンスが提供する費用節減に加えて更なるメリットが得られると言うこともできます。

さらに、AMD プロセッサベースの同等バリアントである m5a.xlarge や r5a.xlarge, または高帯域幅バリアント m5n.xlarge や r5n.xlarge など、他のファミリーや世代から、同様のサイズのインスタンスタイプを追加できる場合があります。ただしこれらは、ハードウェアや仮想化システムの違いにより、他のタイプと比べてパフォーマンスに多少の差異が認められる場合があります。ここで、Application Load Balancer には、処理中の未処理のリクエスト数に応じてインスタンス全体に負荷を分散するメカニズムがあります。つまり、長いリクエストを処理するインスタンス、あるいは処理能力の低いインスタンスが過負荷にならないようにできるのです。これにより、ハードウェアの違いにかかわらず、より多くのインスタンスタイプを指定し、さらに多くのスポット容量プールから容量を取得できるようになることが期待できます。この負荷分散メカニズムについて、本記事の後半で詳しく説明します。

Auto Scaling グループを設定する

ウェブアプリケーションを実行するために複数のインスタンスタイプを選定したところで、EC2 Auto Scaling グループを作成しましょう。

最初のステップは起動テンプレートの作成です。起動テンプレートでは、AMI, セキュリティグループ、タグ、ユーザーデータスクリプトなどのインスタンスの設定を指定します。起動テンプレートにはインスタンスタイプを1種類だけ、このアプリケーションに対して最も汎用的なものを指定します。ここで一つ注意点があります。起動テンプレートの高度な詳細 (Advanced details) セクションでは、スポットインスタンスをリクエスト (Request Spot Instances) チェックボックスをオンにしないでください。複数のインスタンスタイプ、またスポットインスタンスの設定を指定するのは、ここではなく Auto Scaling グループを構成するときに行います。次の図はこのステップで作成した起動テンプレートをマネジメントコンソールで選択した様子です。

図: 作成した起動テンプレートを選択

続いて EC2 Auto Scaling コンソールに移動し、Auto Scaling グループを作成します。ウィザードで、作成した起動テンプレートを選択し、[次へ] をクリックします。次に、ウィザードの 2 番目のステップで、 購入オプションとインスタンスタイプを組み合わせる (Combine purchase options and instance types) を選択します。 この設定オプションは、次の図で確認できます。

図: 購入オプションとインスタンスタイプを組み合わせるための設定

設定の詳細を次に示します。

  • オプションのオンデマンドベース (Optional On-Demand base) : このパラメータでは、Auto Scaling グループ内でオンデマンドインスタンスとして起動してほしい、最小限の容量(ベースラインとなる台数)を指定します。すでにリザーブドインスタンス、あるいはSavings Plansが購入済みであれば、ここで起動されるオンデマンドインスタンスはリザーブドインスタンス、もしくはSavings Plansの対象になります。
  • ベースを超える場合のオンデマンドの割合 (On-Demand percentage above base) : 希望容量全体からオプションのオンデマンドベース数を差し引いた部分に対して、オンデマンドインスタンスとスポットインスタンスの割合を指定します。 デフォルト設定はオンデマンドが 70%、スポットインスタンスが30% の割合に設定されており、ワークロードに応じてこの割合を調整できます。
  • アベイラビリティーゾーンごとのスポット配分戦略 (Spot allocation strategy per Availability Zone)  : スポットインスタンスの起動時に、各アベイラビリティーゾーンで起動するインスタンスタイプを選択するために Auto Scaling が使用するロジックを定義します。選択可能なスポット配分戦略は次のとおりです。
    • capacity-optimized (推奨) : この戦略では、その時点で可用性が最も高いと判断した最適なスポットインスタンスプールを Amazon EC2 サービスが選択し、Auto Scalingサービスがそのプールからスポットインスタンスを起動します。このため、中断の可能性を低くできます。capacity-optimized がこの項目の推奨される選択で、Auto Scalingマネジメントコンソールのデフォルト値となっています。またスケールアウトのたびに最適なキャパシティプールが再評価されるため、Auto Scaling グループがスケールイン・スケールアウトするとき、インスタンスはリサイクルされ、また最適なプールからスポットインスタンスが起動されます。
    • lowest-price : この戦略では、起動時点で最低単価のスポットインスタンスプールから順にインスタンスを起動します。このとき、安い順に何番目までのスポットプールを使うかを、全体に分散する最低価格のスポットインスタンスプールの数 (Number of lowest priced Spot Instance pools to diversify across)(N個)に指定します。このあと、複数のインスタンスタイプの種類を指定するとき、思ったよりも多くのインスタンスタイプが使用されない、という状況を防ぐため、Nにはある程度大きな値を指定することを推奨します。

次に、ウィザードのインスタンスタイプセクションからインスタンスタイプを選択します。ここでは、起動テンプレートから継承されたプライマリインスタンスタイプが既に選択されています。ここから、ウェブアプリケーションの要件に適合する他のファミリーや、世代間で同じサイズのインスタンスタイプのリストが自動的に入力されます。インスタンスタイプは、必要に応じて追加または削除できます。

図: インスタンスタイプの選択

注意 : 異なるサイズのインスタンスタイプを組み合わせるオプションがあり、バッチワークロードで用いられるキューワーカーノードなどのワークロードに非常に役立つ場合があります。ウェブアプリケーションに関連しないため、このブログ投稿ではこれについては取り上げません。この機能の詳細については、こちらのドキュメントを参照してください。

インスタンスタイプを選択すると、Auto Scaling グループは設定されたスポットインスタンス配分戦略を使用して、各アベイラビリティーゾーンで起動するスポットインスタンスタイプを決定します。複数のインスタンスタイプを指定することで、インスタンスの一部が中断された場合に、EC2 Auto Scaling は設定された配分戦略を適用してスポットインスタンスを起動(補充)します。この動作により Auto Scaling グループのサイズが維持され、システムの可用性が担保されます。

Auto Scaling グループのオンデマンド部分では、prioritized と呼ばれる配分戦略が用いられ、これがオンデマンドインスタンスに指定できる唯一の配分戦略です。これは、上のリストで指定した一番先頭のインスタンスタイプから起動を試み、万一EC2 Auto Scaling がオンデマンドインスタンスを起動できない場合にはリストの次のインスタンスタイプから順に起動を試みる、という動作です。ワークロードに最低限必要なベースラインをカバーするために、既にリザーブドインスタンスや Savings Plans を購入している場合は、予約したインスタンスタイプを Auto Scaling グループのインスタンスタイプリストの上位に記述しておくことをお勧めします。これにより、購入済みのインスタンスタイプを優先的に起動させることができ、割引の適用を受けやすくなります。リザーブドインスタンスの適用方法の詳細については、 こちらのドキュメントを参照してください。

このステップを完了したら、Auto Scaling グループ作成ウィザードの残りのステップを完了させます。続いて、Application Load Balancer のセットアップに進みます。

Application Load Balancer を使用したロードバランシング

ウェブアプリケーションを実行しているインスタンス群間で負荷を分散するには、 Application Load Balancer (ALB) を使用します。 EC2 Auto Scaling グループは、ALB およびALB の後ろにある一連のインスタンス群を管理するターゲットグループと完全に統合されています。

ターゲットグループには、デフォルトで 300 秒の登録解除の遅延タイムアウト時間が設定されています。EC2 Auto Scaling が Auto Scaling グループからインスタンスを削除する必要がある場合、まずインスタンスを draining 状態にし、新しいリクエストの送信を停止するように ALB に通知し、処理中のリクエストが可能な限り完了できるよう、デフォルトで 300 秒待機します。そしてこの遅延時間が経過した後、インスタンスが最終的に登録解除されます。この仕組みにより、Auto Scaling グループからインスタンスが削除される場合のエンドユーザーへの影響を極小化することができます。

スポットインスタンスは 2 分の警告で中断されるため、 この設定値をやや短く調整しておく必要があります。推奨値は 90 秒、もしくはそれ以下です。これにより、スポットインスタンスが中断により削除される前に、実行中のリクエストを完了させ、既存の接続を正常に閉じられるようになります。

この記事の執筆時点では、EC2 Auto Scaling はスポットインスタンスの中断を自動的には認識せず、スポットインスタンスが中断されるときに draining に自動遷移することはありません。ただし、中断通知をキャッチし、これをトリガーに EC2 Auto Scaling API を呼び出すことで、インスタンスをドレイン状態にすることができます。これについては、次のセクションで詳しく説明します。

ターゲットグループでは、ルーティングアルゴリズム(デフォルトではラウンドロビン)を設定できます。バックエンドインスタンスに複数のインスタンスタイプを組み合わせて指定するときの考慮点として、異なるハードウェアが使用されている点や、場合によっては仮想化プラットフォームの差異(旧世代のインスタンスでは Xen、新しい世代のインスタンスでは AWS Nitro)により、インスタンスタイプ間でのパフォーマンスに多少の違いが生じることがあります。結果的に応答時間への影響はエンドユーザーにとって無視できるものかもしれませんが、各インスタンスタイプの処理能力の違いを考慮して、インスタンスが負荷を適切に処理できるかどうかを十分にテストしておく必要があります。

Least Outstanding Requests (LOR, 最小未処理リクエスト)負荷分散アルゴリズムでは、ロードバランサーはラウンドロビン方式でリクエストを分散するのではなく、未処理のリクエスト数が最も少ないターゲットにリクエストを送信します。このようにして、処理能力が低いバックエンドインスタンスが存在するパターンや、経過時間の長いリクエストを処理するような場面においても、ターゲット間で負荷を均等に分散することができます。これにより、過負荷のターゲットがある場合にも新しく追加されたターゲット(インスタンス)が負荷のバランスに効果的に貢献するようになります。

これらを設定するには、EC2 マネジメントコンソールの左メニューにあるターゲットグループに進み、ターゲットグループマネジメントコンソールからターゲットグループの属性を編集します。

図: ALB ターゲットグループ属性の設定

ここまでのアーキテクチャをまとめると次の図のようになります。

図: オンデマンドインスタンスとスポットインスタンスの組み合わせでホストされるステートレスウェブアプリケーション

スポットインスタンス中断通知の活用 – 安全なインスタンス終了のために

スポットインスタンスが中断されるとき、EC2 サービスによる2分前の中断通知が発行されます。中断通知は2通りの方法で受信することができます。一つ目の方法はインスタンスメタデータサービスによるものです。二つ目の方法はAWSイベントによるものです。EC2 サービスは 2 分前に中断イベントをトリガーし、このイベントを Amazon EventBridge ルールで検知することができます。

中断通知をハンドリングすることで、エンドユーザーに影響を与えることなく、中断対象のスポットインスタンスを Auto Scaling グループから安全に切り離すアクションを実行できます。たとえば、ロードバランサーからの新しいリクエストの受信を停止し、処理中のリクエストを終了して代わりの容量を起動できるようにします。

以下は、Amazon EventBridge がキャッチするスポットインスタンスの中断通知の例です。

{
  "version": "0",
  "id": "72d6566c-e6bd-117d-5bbc-2779c679abd5",
  "detail-type": "EC2 Spot Instance Interruption Warning",
  "source": "aws.ec2",
  "account": "12345678910",
  "time": "2020-03-06T08:25:40Z",
  "region": "eu-west-1",
  "resources": [
    "arn:aws:ec2:eu-west-1a:instance/i-0cefe48f36d7c281a"
  ],
  "detail": {
    "instance-id": "i-0cefe48f36d7c281a",
    "instance-action": "terminate"
  }
}

中断通知イベントを受信したのちに起動すべき処理として、EC2 Auto Scaling の DetachInstances API 呼び出しを活用できます。この API を呼び出すと、インスタンスがドレイン状態になり、関連づけられたロードバランサーはそれ以上の新しいリクエストをインスタンスに送信しなくなります。インスタンスは処理中のリクエストを可能な限り完了できるよう、ターゲットグループで設定された登録解除の遅延時間を待機します。

図: ドレイン状態のインスタンスを示すターゲットグループマネジメントコンソール

また、DetachInstances API 呼び出しに際し、ShouldDecrementDesiredCapacity パラメータを False に設定すると 、EC2 Auto Scaling は指定された配分戦略に従ってインスタンスタイプリストから代替となるスポットインスタンスを起動しようとします。以下は、Python Boto3 (Python用のAWS SDK) を使用して Auto Scaling API を呼び出すコードスニペットの例です。

 

def detach_instance_from_asg(instance_id,as_group_name):
   try:
       # detach instance from ASG and launch replacement instance
       response = asgclient.detach_instances(
           InstanceIds=[instance_id],
           AutoScalingGroupName=as_group_name,
           ShouldDecrementDesiredCapacity=False)
       logger.info(response['Activities'][0]['Cause'])
   except ClientError as e:
       error_message = "Unable to detach instance {id} from AutoScaling Group {asg_name}. ".format(
           id=instance_id,asg_name=as_group_name)
       logger.error( error_message + e.response['Error']['Message'])
       raise e

 

場合によっては、スポットインスタンスが中断されるときに、オペレーティングシステムレベルでアクションを実行することもできます。たとえば、アプリケーションを適切に停止して、データベースへのオープン接続をクローズしたり、インスタンスで実行されているエージェントの登録を解除したり、その他のクリーンアップアクティビティを実行したりできます。このようなインスタンス内部への操作を実現するには、AWS Systems Manager の Run Command 機能を活用し、これらのアクションのためのコマンドを対象のインスタンス上で呼び出すことができます。

また、中断までの 2 分間を最大限に活用するため、このコマンドの実行を遅らせることもできます。まず最初のステップとして、インスタンスメタデータなどから取得した中断通知 JSON データからインスタンスの中断予定時刻を取得します。そして例えば、中断の 30 秒前まで待機させます。次に、アプリケーションを適切に停止し、残りの終了コマンドを発行します。いくつか補足事項について述べます。 Run Command の使用には料金はかかりません。場合によって、サービスごとの制限(サービスクォータ)が適用される場合があります。Systems Manager への API 呼び出しは非同期であり、Lambda 関数の実行をブロックすることはありません。

注意 : Run Command を使用するには、インスタンスで AWS Systems Manager エージェントを実行し、IAM インスタンスプロファイルの設定など、一連の前提条件を満たす必要があります。詳細については、こちらのドキュメントを参照してください。

最後に、お使いの AWS アカウントに簡単にデプロイできる、ここで説明した中断処理のすべてのステップを含むサンプルソリューションをご紹介します。このアーキテクチャの概要を以下の図に示します。

図: スポットインスタンスの中断を処理するサーバーレスアーキテクチャ

このサンプルソリューションでは、スポットインスタンスの中断時に実行できる、さらに追加のコマンドを定義することもできます。このために、実行するコマンドを含む AWS Systems Manager パラメータストアをAuto Scaling グループごとに作成しておきます。これにより各アプリケーションのニーズに応じて、終了コマンドを簡単にカスタマイズ・管理できます。詳細な設定手順は README.md ファイルを確認してください。

この記事のまとめ

以上、スポットインスタンスでウェブアプリケーションを実行するための関連機能をすべて取り上げたので、これらを振り返ってみましょう。

  • EC2 Auto Scaling グループで複数のインスタンスタイプと購入オプションを組み合わせる機能を用いることで、オンデマンドインスタンスとスポットインスタンスの組み合わせを設定できます。この機能を利用して、複数のインスタンスタイプを設定し、スポットインスタンスでウェブアプリケーションを実行するときに、複数のスポットキャパシティープールから未使用のキャパシティーを活用し、中断されたインスタンスを置き換えることができます。
  • capacity-optimized スポットインスタンス配分戦略を使用すると、選択したインスタンスタイプの中で最も可用性の高いスポットインスタンスプールからスポットインスタンスを起動できます。これにより、中断の可能性が減少します。スポットインスタンスプール容量の変動に伴って起動済みスポットインスタンスの一部に中断が発生した場合、Auto Scaling は改めて各プールの可用性を評価し、その時点での最適なプールからスポットインスタンスの容量を補充します。
  • Application Load Balancer を使用すると、ターゲットグループの登録解除の遅延を、スポットインスタンスの終了通知時間(120 秒)よりわずかに短い時間(例 :90 秒)に調整できます。そのため、インスタンスが中断されるときに、そのインスタンスへの新しいリクエストの送信を停止し、通知時間を活用して未処理のリクエストを完了させ、エンドユーザーへの影響を回避できます。また、ALB ターゲットグループで Least Outstanding Requests (LOR)負荷分散アルゴリズムを設定し、異なるインスタンスファミリーの処理能力の違いを考慮して、負荷がターゲットグループ全体に均等に分散されるようにすることもできます。
  • Amazon EventBridge, AWS Lambda, AWS Systems Manager Run Command を活用して、インスタンスをドレイン状態にする、EC2 Auto Scaling に前もって代替インスタンスの起動をリクエストする、アプリケーションを適切にシャットダウンする、クリーンアップアクティビティをトリガーする、といった中断処理アクションを実行できます。また補足のアイデアとして、アラームやログ記録を検討することもできます。このために例えば Amazon Simple Notification Service (SNS) トピックをサブスクライブしておく、あるいはこのソリューションのように Lambda 関数をセットアップしておくこともできます。

このブログ投稿が役に立つことを願っています。もしステートレスなウェブアプリケーションがあり、まだスポットインスタンスをお試しでない場合は、費用節減とスケーリングを目的に、ぜひ試してみてください。

この記事はソリューションアーキテクト滝口が翻訳しました。原文はこちら