Amazon AppStream 2.0のスケーリング設定をおさらいする

2022.02.10

しばたです。

突然ですがAppStream 2.0のスケーリング設定をおさらいしたいと思います。

「おさらい」と言いましたが、私自身がこれまでずっとこのスケーリング設定を根本的に勘違いしたままでおりつい先日そのことに気が付きました...
この記事で恥を晒すとともに皆さんが私と同じ轍を踏まない様にしてほしくてこの記事を書きました。

AppStream 2.0 フリートインスタンスの状態

スケーリングの話に入る前に、はじめに、AppStream 2.0フリートの各インスタンスが取りうる状態について解説します。

AppStream 2.0は公開アプリケーションのユーザー(セッション単位)が接続先のインスタンスを占有して利用し、セッションが終了するとそのインスタンスを破棄する挙動をします。

要はインスタンスを都度使い捨てています。

そしてフリートインスタンスは通常のEC2インスタンスとは異なりAppStream 2.0向けの初期化処理があるため、インスタンスが作成され始めてからユーザーが利用可能になるまで大体10分程度の時間を必要とします。

ここまでの内容を踏まえてフリートインスタンスは以下の状態を取り得ます。

  • インスタンス準備中
  • インスタンスの起動が完了し、ユーザーの接続待ち
  • インスタンスの起動が完了し、ユーザー接続中

これらの状態に対してREST API(ComputeCapacityStatus)およびCloudWatch Metricsではそれぞれ以下の状態で表現されます。

状態 REST API
(ComputeCapacityStatus)
CloudWatch Metrics
(capacity値)
備考
インスタンス準備中 - Pending capacity
インスタンスの起動が完了し、ユーザーの接続待ち Available Available capacity
インスタンスの起動が完了し、ユーザー接続中 InUse InUse capacity
ユーザーの利用状態を問わず起動中 Running Actual capacity Available capacity + InUse capacity

両者だいたい同じ表現をしていますが、準備中インスタンスの数はCloudWatch Mectricsでのみ取得可能なのと、起動中の状態がRunningActual capacityと若干異なる表現になっています。

Desired capacity

次にAppStream 2.0のスケーリングにおいて一番重要なパラメーターであるDesired capacityについて触れます。

Desired capacityは名前の通り「希望インスタンス数」であり、起動中インスタンス(RunningおよびActual capacity)の数をこの値に保とうとする値です。

たとえばDesired capacity = 5にしてフリートを開始した場合、最初に5台のインスタンスを起動します。
10分程度待てばインスタンスの準備が完了しAvailable capacity = 5, InUse capacity = 0となりユーザーの接続を待ち受けます。

ここで一人のユーザーがフリートを利用するとAvailable capacity = 4, InUse capacity = 1になります。
まだ起動中インスタンスの総数は変わらないので変化はありません。

次にこのユーザーがセッションを終了した場合、InUseのインスタンスは破棄されAvailable capacity = 4, InUse capacity = 0になります。
ここで起動中インスタンス総数が5から4に減ったためDesired capacity = 5を維持しようと1台のインスタンスを新たに起動します。

これがDesired capcityの役割になります。

ちなみにREST APIおよびCloudWatch Metricsでは以下の状態で表現されます。

状態 REST API
(ComputeCapacityStatus)
CloudWatch Metrics
(capacity値)
備考
希望インスタンス数 Desired Desired capacity

AppStream 2.0 フリートのスケーリング設定

ここからやっと本題に入ります。

AppStream 2.0 フリート *1はオートスケーリングにより起動インスタンスの数を動的に変更することができます。

このスケーリング設定はApplication Auto Scalingにより実装されています。
AppStream 2.0のスケーリングを考える場合、AppStream 2.0とApplication Auto Scalingの2つのサービスを意識する必要があることをまず覚えておいてください。

そして、ここで一番重要なのが「AppStream 2.0ではDesired capacityしかスケーリングの指標を持っていない」という点です。

マネジメントコンソールからフリートの設定をしたことがある方ならMinimum capacityMaximum capacityといった値を見たことがあるかと思いますが、これらの値はApplication Auto Scaling側のパラメーターであり、単にDesired capacityの値が上がり過ぎない(下がり過ぎない)様にするためのストッパーにすぎません。

実装が何であれAppStream 2.0のスケーリングは「フリートをスケールアップする場合はDesired capacityの値を増やす」、「フリートをスケールインする場合はDesired capacityの値を減らす」この2つの操作が全てです。

私はずっとこの仕組みを誤解してDesired capacityの値はオートスケーリングに関与せずにスケーリングの最後に行きつく値だとずっと思い込んでいました...
AppStream 2.0のオートスケーリングは「Application Auto Scalingを使ってDesired capacityの値を良い感じにスライドさせる」が正解です。

結果論ですがApplication Auto Scalingに慣れ親しんでいればこの様な勘違いはせずに済んだ気がします。

AppStream 2.0 でサポートされるスケーリングポリシー

Application Auto ScalingのスケーリングポリシーにおけるAppStream 2.0のサポート状況は以下となります。

ポリシー種別 AppStream 2.0サポート マネジメントコンソール CLI 備考
ステップスケーリング
スケジュールされたスケーリング 昔はマネジメントコンソールから設定できなかった記憶がある...
ターゲット追跡スケーリング - 現状CLIからのみ設定可能

それぞれのスケーリングポリシーについて軽く触れていきます。

1. ステップスケーリング

まず最初に「ステップスケーリング」について説明します。
このポリシーが基本であり、インスタンス使用率などのメトリクス値を元にスケールアウト・スケールインを行います。

AppStream 2.0でサポートされているメトリクスは以下の通りです。

メトリクス 単位 備考
Capacity Utilization 使用率(%) インスタンス使用率。In use capacity / Actual capacity * 100 の値
Available capacity インスタンス数
Insufficient Capacity Error エラー回数 ユーザーが利用可能なインスタンスを見つけることができなかった際に発生するエラー

スケールする単位は「インスタンス数」および「使用率(%)」となり内容に応じてDesired capacityの値が変動します。

具体的にスケールアウト・スケールインの設定をどうすべきかはシステム次第です。
とりあえずはマネジメントコンソールで表示されるデフォルト値から試して適宜調整していくのが良いでしょう。

  • スケールアウト : Capacity Utilization > 75% になると 2インスタンス 増やす (Desired capacityを2増やす)
  • スケールイン : Capacity Utilization < 25% になると 1インスタンス 減らす (Desired capacityを1減らす)

ステップスケーリングポリシーを設定するとCloudWatch Alarmが同時に作られ、スケールアウト・スケールインのトリガーとなります。

CLIからはaws application-autoscaling describe-scaling-policiesコマンドで詳細を確認できます。

# コマンド実行例
C:\> aws application-autoscaling describe-scaling-policies --service-namespace appstream --resource-id "fleet/my-test-fleet"
{
    "ScalingPolicies": [
        {
            "PolicyARN": "arn:aws:autoscaling:ap-northeast-1:xxxxxxxxxxxx:scalingPolicy:1edc4f22-f987-4e60-9d19-abdff44f8cd1:resource/appstream/fleet/my-test-fleet:policyName/default-scale-in-1",
            "PolicyName": "default-scale-in-1",
            "ServiceNamespace": "appstream",
            "ResourceId": "fleet/my-test-fleet",
            "ScalableDimension": "appstream:fleet:DesiredCapacity",
            "PolicyType": "StepScaling",
            "StepScalingPolicyConfiguration": {
                "AdjustmentType": "ChangeInCapacity",
                "StepAdjustments": [
                    {
                        "MetricIntervalUpperBound": 0.0,
                        "ScalingAdjustment": -1
                    }
                ],
                "Cooldown": 360,
                "MetricAggregationType": "Average"
            },
            "Alarms": [
                {
                    "AlarmName": "Appstream2-my-test-fleet-default-scale-in-1-Alarm",
                    "AlarmARN": "arn:aws:cloudwatch:ap-northeast-1:xxxxxxxxxxxx:alarm:Appstream2-my-test-fleet-default-scale-in-1-Alarm"
                }
            ],
            "CreationTime": "2022-02-07T12:58:34.198000+09:00"
        },
        {
            "PolicyARN": "arn:aws:autoscaling:ap-northeast-1:xxxxxxxxxxxx:scalingPolicy:1edc4f22-f987-4e60-9d19-abdff44f8cd1:resource/appstream/fleet/my-test-fleet:policyName/default-scale-out-1",
            "PolicyName": "default-scale-out-1",
            "ServiceNamespace": "appstream",
            "ResourceId": "fleet/my-test-fleet",
            "ScalableDimension": "appstream:fleet:DesiredCapacity",
            "PolicyType": "StepScaling",
            "StepScalingPolicyConfiguration": {
                "AdjustmentType": "ChangeInCapacity",
                "StepAdjustments": [
                    {
                        "MetricIntervalLowerBound": 0.0,
                        "ScalingAdjustment": 2
                    }
                ],
                "Cooldown": 120,
                "MetricAggregationType": "Average"
            },
            "Alarms": [
                {
                    "AlarmName": "Appstream2-my-test-fleet-default-scale-out-1-Alarm",
                    "AlarmARN": "arn:aws:cloudwatch:ap-northeast-1:xxxxxxxxxxxx:alarm:Appstream2-my-test-fleet-default-scale-out-1-Alarm"
                }
            ],
            "CreationTime": "2022-02-07T12:58:34.741000+09:00"
        }
    ]
}

2. スケジュールされたスケーリング

次に「スケジュールされたスケーリング」について説明します。

こちらはCRON式などでスケーリングの実行をスケジュールすることができ、Minimum capacityMaximum capacityの値を変更することができます。

Desired capacityの値を直接変更することは出来ませんのでご注意ください。
このため大抵の場合において他のスケーリングポリシーと併用することになるでしょう。

例えば下図の様に毎朝(JST)にインスタンスの最小・最大を引き上げ、夜(JST)になったら引き下げを行うといった設定が可能となります。

ただし、スケーリング設定変更時にDesired capacity < Minimum capacityになる場合はDesired capacity = Minimum capacityまでDesired capacityの値が引き上げられます。
逆にDesired capacity > Maximum capacityになる場合はDesired capacity = Maximum capacityまでDesired capacityの値が引き下げられます。
この特性を活かすことで間接的にある程度Desired capacityの値を操作することは可能です。

CLIからはaws application-autoscaling describe-scheduled-actionsコマンドで詳細を確認できます。

# コマンド実行例
C:\> aws application-autoscaling describe-scheduled-actions --service-namespace appstream --resource-id "fleet/my-test-fleet"
{
    "ScheduledActions": [
        {
            "ScheduledActionName": "daily-morning",
            "ScheduledActionARN": "arn:aws:autoscaling:ap-northeast-1:xxxxxxxxxxxx:scheduledAction:1edc4f22-f987-4e60-9d19-abdff44f8cd1:resource/appstream/fleet/my-test-fleet:scheduledActionName/daily-morning",
            "ServiceNamespace": "appstream",
            "Schedule": "cron(0 22 * * ? *)",
            "ResourceId": "fleet/my-test-fleet",
            "ScalableDimension": "appstream:fleet:DesiredCapacity",
            "StartTime": "2022-02-01T09:00:00+09:00",
            "ScalableTargetAction": {
                "MinCapacity": 5,
                "MaxCapacity": 10
            },
            "CreationTime": "2022-02-07T17:48:33.910000+09:00"
        },
        {
            "ScheduledActionName": "daily-evening",
            "ScheduledActionARN": "arn:aws:autoscaling:ap-northeast-1:xxxxxxxxxxxx:scheduledAction:1edc4f22-f987-4e60-9d19-abdff44f8cd1:resource/appstream/fleet/my-test-fleet:scheduledActionName/daily-evening",
            "ServiceNamespace": "appstream",
            "Schedule": "cron(0 13 * * ? *)",
            "ResourceId": "fleet/my-test-fleet",
            "ScalableDimension": "appstream:fleet:DesiredCapacity",
            "StartTime": "2022-02-01T09:00:00+09:00",
            "ScalableTargetAction": {
                "MinCapacity": 1,
                "MaxCapacity": 3
            },
            "CreationTime": "2022-02-07T17:48:34.194000+09:00"
        }
    ]
}

3. ターゲット追跡スケーリング

最後に「ターゲット追跡スケーリング」について説明します。

ターゲット追跡スケーリングは所定のメトリクス値を維持しようとするオートスケールを行うポリシーとなります。
AppStream 2.0ではAppStreamAverageCapacityUtilizationという名前でインスタンス使用率(%)の値を維持しようします。
(現状サポートされているメトリクスはこれだけであり他にはありません)

ターゲット追跡スケーリングはCLIからのみ設定可能で、例えばインスタンス使用率を80%維持にしたい場合は以下の様なコマンドとなります。

# AWS CLI on PowerShell での実行例
$resourceId = "fleet/my-test-fleet"
$json = @"
{
  "PolicyName":"target-tracking-scaling-policy",
  "ServiceNamespace":"appstream",
  "ResourceId":"$resourceId",
  "ScalableDimension":"appstream:fleet:DesiredCapacity",
  "PolicyType":"TargetTrackingScaling",
  "TargetTrackingScalingPolicyConfiguration":{
    "TargetValue":80.0,
    "PredefinedMetricSpecification":{
      "PredefinedMetricType":"AppStreamAverageCapacityUtilization"
    },
    "ScaleOutCooldown":300,
    "ScaleInCooldown":300
  }
}
"@ -replace '"','\"'
aws application-autoscaling put-scaling-policy --cli-input-json $json

この実行結果は以下の様になり、スケールアウト・スケールインのトリガーとなる2つのCloudWatch Alarmが同時に生成されます。

# コマンド実行結果
C:\> aws application-autoscaling put-scaling-policy --cli-input-json $json
{
    "PolicyARN": "arn:aws:autoscaling:ap-northeast-1:xxxxxxxxxxxx:scalingPolicy:1edc4f22-f987-4e60-9d19-abdff44f8cd1:resource/appstream/fleet/my-test-fleet:policyName/target-tracking-scaling-policy",
    "Alarms": [
        {
            "AlarmName": "TargetTracking-fleet/my-test-fleet-AlarmHigh-404bd8fc-16ae-4fe0-abf1-fa044883bc26",
            "AlarmARN": "arn:aws:cloudwatch:ap-northeast-1:xxxxxxxxxxxx:alarm:TargetTracking-fleet/my-test-fleet-AlarmHigh-404bd8fc-16ae-4fe0-abf1-fa044883bc26"
        },
        {
            "AlarmName": "TargetTracking-fleet/my-test-fleet-AlarmLow-5cfe33eb-ddb1-4b96-b98d-acadc164066c",
            "AlarmARN": "arn:aws:cloudwatch:ap-northeast-1:xxxxxxxxxxxx:alarm:TargetTracking-fleet/my-test-fleet-AlarmLow-5cfe33eb-ddb1-4b96-b98d-acadc164066c"
        }
    ]
}

AlarmHighの方のアラームはインスタンス使用率(3分)が80%を超えると発動し、使用率を下げるためにスケールアウトします。
逆にAlarmLowの方のアラームはインスタンス使用率(15分)が72%未満になると発動し、使用率を上げるためにスケールインします。
ドキュメントにある様に若干緩やかにスケールインする設定となっています。

ちなみにマネジメントコンソール上では何故かスケールアウトポリシーの形で表示されるのですが、内容も微妙におかしいのでGUI上では触れない方が良いと思います。

ちなみにポリシーを削除したい場合はaws application-autoscaling delete-scaling-policyコマンドを使います。

# ポリシー削除例 on PowerShell
aws application-autoscaling delete-scaling-policy --service-namespace appstream `
    --resource-id "fleet/my-test-fleet" `
    --policy-name "target-tracking-scaling-policy" `
    --scalable-dimension "appstream:fleet:DesiredCapacity"

補足 : ターゲット追跡スケーリング例

ターゲット追跡スケーリングはインスタンス使用率の変動を追跡するため、規模の小さい(インスタンス数の少ない)環境では使用率の変動が激しすぎてあまり期待した挙動にならないと思います。
このポリシーはある程度の規模がある環境向きです。

一応、手元の環境でMinimum capacity = 1, Maximum capacity = 10初期 Desired capacity = 1で前述の例(インスタンス使用率80%維持)を試した結果を記載しておきます。

No. 状態 Desired capacity Available InUse 使用率(%) 備考
1 利用開始 1 1 0 0%
2 スケールイン(不発) 1 1 0 0% CloudWacth Alarmが発火するが、これ以上下げようがないので何もしない
2 ユーザーがインスタンス利用 1 1 1 100% 使用率100%に更新
3 スケールアウト発動 2 1 1 50% 使用率50%までダウン
4 ユーザーがインスタンス利用 2 0 2 100% 使用率100%に更新
5 スケールアウト発動 3 1 2 66% 使用率66%までダウン
6 スケールイン(不発) 3 1 2 66% CloudWacth Alarmは発火するがスケールインせず
7 ユーザーがログアウト 3 1 1 50%
8 インスタンス補充 3 2 1 33% Desired capacityが変動してないので補充される
9 スケールイン 2 1 1 50% ここでスケールイン発動
10 ユーザーがログアウト 2 1 0 0% 全ユーザーログアウト
11 インスタンス補充 2 2 0 0% Desired capacityが変動してないので補充される
12 スケールイン 1 1 0 0% 最後のスケールイン発動

規模が小さすぎて使用率の変動がめちゃくちゃなのが見て取れますね。
それでもそれなりに動作しますので、細かいことを考えたくない場合はこちらのポリシーを使っても良いかもしれません。

最後に

以上となります。

これまで間違って覚えていたAppStream 2.0のオートスケーリングをおさらいしてみました。
AppStream 2.0ではなくApplication Auto Scalingを中心にして考えると勘違いすることも無かったのかなという感じです。

本記事が皆さんの役に立てば嬉しいです。

脚注

  1. 厳密にはElastic Fleets以外のフリート