AWS ECS Managed Instance の実戦的な基礎知識

ECS on EC2 と ECS Fargate に続いて、ECS Managed Instance という新しいキャパシティープロバイダーが出現しました。

実際に検討/採用するために必要そうな知識について、いつもの感じで整理していきますが、モノがモノだけにややとっ散らかるかもしれません。



基本知識

そもそもなんなの ~ 使ってみた、あたりまでは他所様の記事に任せたいと思います。


要約すると、インスタンスを自身で管理するけどコスパ最強の on EC2 と、ややコスパが劣るが運用が楽な Fargate、そこにコスパ最強かつ運用が楽な Managed Instance が割って入ってきた、という感じです。

まっさらならすぐに飛びついてもイィくらいなんですが、既存の ECS がある場合は移行する価値があるのかどうか、を考えなくちゃってのもあるし、使うにしても基礎知識が成ってないとコスパ最強に届かないかもなので、ちゃんと把握しましょうってところです。

前に X でつらつら書いておいたのはこの辺。



コスパの差

まず Managed Instance の価値を感じるためにはコスパの差を知っておく必要があります。

on EC2 だと自由にインスタンスタイプやスポットを選択できるため、サーバーリソースのコストに対するパフォーマンスは最高です。しかし、EC2 インスタンスの管理を自身で行うため、やや運用コストがかかるのと、適切な設計をしないと真に最適化されない状態での運用となることで、その分もややコスト損になる場合があります。

Fargate はインスタンス不要でタスクのみを管理すればよいので運用コストが低めなのと、メモリ容量を細かく調整できるのが良いですが、CPU の選択ができず、最新と比べれば1~2世代は古いモノとなるためコストに対するパフォーマンスがやや低いです。関連記事としてはこの辺です。
ただ、去年末から今年2月頃にかけて異変があり、これもつらつらと書いておきました。


先進リージョンは 2024/12 頃から、東京はおそらく 2025/02 頃から、Fargate ARM64 では Graviton3 が 8割 Graviton2 が 2割 の割合で動いていただろうことが発覚しました。


これらに対して Managed Instance では、インスタンスや Autoscaling の管理が不要になったことで運用コストが抑えられ、自由にインスタンスタイプを選択できるので最新の CPU で稼働させることができて嬉しいでしょ!?ってところなのですが、今のところスポットインスタンスは利用できないので、SavingsPlans のみに頼ることになります。

CPU性能の恩恵

ちなみに CPU の性能が良いと何が嬉しいかというと、Graviton3 → Graviton4 になると性能が 約20% 向上するので、同じタスク数・同じトラフィックなら平均CPU使用率が元の数値の 2割 ほど減ることになります(※元のCPU使用率が 50% なら 40% になるのを期待できるということ)。

元々 Autoscaling 条件を 50% を基準としていたなら、変更後に平均CPU使用率が減る分はタスク数も減るので、単純なコスト削減になります。

また、CPU 処理が速いということは、平均レスポンスタイムの短縮や、ジョブなどの所要時間の短縮を期待できます。そのため、仮に新旧で同じコスパだとしても、同じなら処理時間が短くなる分は新しい方に分があると言えます。


費用比較

流れで先に実際の費用比較をみて、だいたいの価値を把握することで検討するかどうか考えるとよいでしょう。

この表は、Managed Instance と Fargate それぞれで、1 vCPU 2 GB で 10 タスク起動した時の $/h を算出して割合比較したものです。



現実的なところで考えると、Managed Instance を全て SavingsPlans にした条件と、Fargate を SavingsPlans 7割 Spot 3割 にした条件とで、およそ 2割 ほどのコスト差があります。

面倒なので Fargate ARM64 が全て Graviton3 で動くとして考えて、EC2 c8g Graviton4 と性能差が 20% あると考えると、これでトントンくらいの条件となります。

SavingsPlans の条件や Spot 割合次第では、コスパは Fargate の方が安くなるかもしれませんが、処理速度の恩恵があるので多少高くとも Graviton4 にする価値はあるかもしれません。これらは Spot 対応の有無でまた変わってくるところでしょう。


Terraform

使ってみるか検討するなら、ここから先は構築についてです。Terraform コードを抜粋して説明していきます。

見たらわかりますが、Managed Instance の設定以外は一部は変更が必要なものの、ほとんどそのまま使えるので Fargate からの変更もそんなに苦ではありません。

IAM Role

詳しくは上に載せたブログを参考にしてほしいのですが、まずは ECS が EC2 などを管理する Role が必要になります。

管理画面からも作れますが、ちゃんと内容を見ておいたほうが理解しやすいでしょう。また、iam:PassRole がないとインスタンスが起動しないので、注意してください。


次に、EC2 インスタンスに割り当てる profile が必要です。


ECS Cluster

aws_ecs_cluster は FARGATE の時と変わりません。
また、aws_ecs_cluster_capacity_providers は不要です。

最近、クラスタのログを残しやすくなったのですが、どうも Terraform でもろもろ用意しても有効化にならないので、対応待ちっぽいです。構築中はあると役に立つことがあるので、必要なら管理画面でポチってください。

Namespace

Service間で名前解決して通信するための Service Discovery は利用可能なので、aws_service_discovery_private_dns_namespace と aws_service_discovery_service はそのまま使えます。

Managed Instance で使えないのは Service Connect なので最初は勘違いしてどうしようか考えてしまいました。

Managed Instance の Capacity Provider

これを追加すると、クラスタ>インフラストラクチャ>キャパシティプロバイダーに追加されます。コードで見るより管理画面で一通り何があるのかをチェックした方が理解が早いと思います。

コードは手元からコピペ+やや編集してますが、ほぼドキュメント通りです。


Task Definition

タスク定義はほぼ Fargate のまま流用できます。唯一ここだけ変更します。

ちなみに、仮に FARGATE のままでも Managed Instance で動きます。動きますが、メモリの指定は区切りに従わないといけないとか、不自由なので正しく指定したほうがよいです。


Service

サービスも一部は変更が必要です。


あとはサービスに関係する、aws_appautoscaling_target , aws_appautoscaling_policy , aws_appautoscaling_scheduled_action やら CodeDeploy , CodePipeline はそのまま流用できます。

動作確認

適当な service でタスク数を 1 にすると、それに必要なリソース量のインスタンスが起動し、その後にタスクが起動完了までいきます。

成功しない場合は、Role がおかしい、Subnet がおかしい、NAT G/W がない、などが考えられ、そのエラーログが残らない場合があるので丁寧に見返していきましょう。


実効リソース量

インスタンスの選択

タスク起動時に必要なリソース量を見てインスタンスが起動しますが、このインスタンスタイプの選択には2種類あります。1つは ECS デフォルト、もう1つはカスタムです。

デフォルトは試していないのでアレですが、よしなに選んでくれるとはいえ、c8g Graviton4 を目的にしているのに c7g Graviton3 が起動されても困るので、私は最初からカスタムしか目に入っていません。

私の場合は、必須条件である CPU を 1 以上、メモリを 1024 MB 以上としつつ、素直にインスタンスタイプを [“c8g.medium”, “c8g.large”] と直指定することにしました。色々指定条件があるので、一応見てみて、人によっては便利に使えるかもしれません。

実際に使えるメモリ容量

昔から仮想環境とか on EC2 で扱ってる人には既知の内容ですが、親OS の中で子OS が動くには、親自身が動くためのメモリを確保しつつ、子に分け与えなくてはいけません。

on EC2 だと ECS_RESERVED_MEMORY を /etc/ecs/ecs.config に書いておくことで任意の量にできましたが、Managed ではインスタンスの管理ができないので自動的に決定されるようで、こんな感じでした。

タイプリソース量実効量予約量
c8g.medium 1 vCPU, 2048 MiB 1 vCPU, 1548 MiB 500 MiB
c8g.large 2 vCPU, 4096 MiB 2 vCPU, 3525 MiB 571 MiB
c8g.xlarge 4 vCPU, 8192 MiB 4 vCPU, 7420 MiB 772 MiB
c8g.2xlarge 8 vCPU, 16384 MiB 8 vCPU, 15020 MiB1364 MiB
c8g.4xlarge16 vCPU, 32768 MiB16 vCPU, 30529 MiB2239 MiB

まず CPU は親OS ではあまり使われないのと、普通は常時余裕があるから予約まではしないという感じでしょう。メモリも常にパンパンに使われるわけではないものの、不足した時に致命傷になりやすいので、こちらは厳密に管理することになります。

予約量を見ると、インスタンスのサイズが大きくなるほど増えはするものの、そこまで親OS で必要とするわけではないので徐々に割合が減っているのがわかります。なので、大きいサイズを扱うほどタスクに割り当てられる量も増えることになります。medium だと 25% も削られるので、結構な考えどころとなるでしょう。


タスク起動とインスタンス起動の関係

上記の表を見ればどういう挙動になるかは想像できるでしょうが、一応いくつか例を示しておきます。

以下、インスタンスタイプの指定を [“c8g.medium”, “c8g.large”] とします。

0.5vCPU 512MB を 4task

最小のサイズが選択され、複数タスクは可能な限り詰め込まれます。
通常の区切り容量だと、予約分が足りないので 512 / 2048 = 4 task ではなく 3 task までとなります。

  • c8g.medium が 1a に起動し、1task 目が起動
  • 同インスタンスに 2, 3task 目が起動
  • c8g.medium が 1c に起動し、4task 目が起動

1vCPU 2048MB を 3task

medium では足りないので large で起動します。
次のインスタンスが必要な時は AZ バランスを考慮して起動されます。

  • cg8.large が 1a に起動し、1task 目が起動
  • c8g.large が 1c に起動し、2task 目が起動
  • c8g.large が 1d に起動し、3task 目が起動

2vCPU 4096 MiB を 1task

起動可能なサイズがないとエラーになります。

  • エラーが発生。SERVICE_TASK_CONFIGURATION_FAILURE : ResourceInitializationError: No available instance types were able to satisfy the task and placement constraints.

Service(1) 0.5vCPU 1548MB, Service(2) 0.5vCPU 1549MB

実効メモリ容量に忠実に、足りれば最小サイズを、1 でも足りなければ次の大きさのサイズを選択します。AZ バランスは Service 毎になるようです。

  • c8g.medium が 1a に起動し、Service(1) の 1task が起動
  • c8g.large が 1a に起動し、Service(2) の 1task が起動


リソース設計

ここまでの例を見たうえで、ではどうしたら最適なリソースの使用になるのかという私見です。

ここでは、リソースの大半はアプリケーション・サーバーのような大きめのタスクかつ多い台数で、それ以外には小さなタスクがいくつか存在する、ようなイメージで考えています。

1インスタンス1タスク

Fargate で 8vCPU 16GB で動かしていたとして、それを Managed で動かすには c8g.2xlarge ではなく c8g.4xlarge が必要になり、かつリソースの半分弱が空いた状態になります。この空きに細かいタスクが入るとしても、そんなに数が多くなければ全てが埋まり切ることはないでしょう。

なので c8g.2xlarge で動かし、かつ空きがでないようにするには、8vCPU 16384 MB ではなく、8vCPU 15020 MB でタスクを起動することになります。こうすることで、リソースを無駄なく利用し、複数タスクで確実に AZ がバラけます。

1インスタンス複数タスク(考察)

さきほどの例の続きとして、8vCPU のタスクを c8g.4xlarge に 2 task 載せるということも考えることはできます。その場合、8vCPU 15264 MB を 2 task ということになります。

これのメリットは、ややメモリ容量を大きく取れることは確定として、それ以外に何かあるのだろうか、と考えてみるべきです。

もしそこに 1 task しか必要ない状態だと、半分が遊びリソースとなってしまいコスパに影響しますし、1インスタンスに対する障害の耐久としては 2 task とも運命をともにすることになるでしょう。あまりシンプルなメリットはありそうにないですが、どうでしょうか。

あるとしたら、ミドルウェアがシングルスレッドでしか動けない場合、1vCPU 1548 MB を c8g.medium で動かすのは効率が悪そうです。もっと大きなサイズの中で、複数タスクとして動かしたほうが予約メモリの分はコスパが良いかもしれません。ただ、あまり大きなサイズに詰め込みすぎると、16 task あるのに 2 インスタンスだと、1 インスタンス落ちたら半分になってしまう、とかありえるので耐久性についてはサイズと台数との要相談になるでしょう。

小さなタスク

基本的にはどの役割の service でも、冗長性の観点から複数のタスクで稼働できるようにするべきです。もしそれができなければ、ミドルウェアの選定ミスを含んだ設計ミスと言えます。

もし 1vCPU 1024 MB で十分な小さなタスクがあったとして、それを xlarge 以上に詰め込む形になっていたとしたらどうでしょうか。起動タイミングによっては同じインスタンスに入る可能性を排除できず、タスクは複数でも実態は Single-AZ や Single-Instance になってしまうのではないでしょうか。

そうしないためには、最小リソースを medium 用とし、2 task で確実に 2 インスタンスにする必要があります。サイズの指定を [“c8g.medium”, “c8g.2xlarge”] としておけば、大きなタスクは 2xlarge を、小さなタスクは medium を起動して使ってくれます。

小さいタスクは開発用途では 0.25 vCPU とか 512 MB など小さくして詰め込むと、安上がりにしやすい面はありますが、本番では 1 vCPU 未満にすることは処理速度の観点から推奨しませんし、Single-AZ リスクを考慮すると、詰め込むメリットはなかなか見出せそうにはありません。

開発用途

インスタンスタイプにはメモリ容量だけバカでかいのがあったりします。そして、開発用のタスクに必須なのはメモリ容量であって、vCPU はごく小さくても十分だったりします。

このことを利用して、1つの大容量メモリのインスタンスに vCPU を小さく・メモリを適正に取ったタスクを大量に詰め込むことで安上がりにすることは可能です。ただ、これは ECS Managed Instance でやるようなことではない気がします。


監視

ここではメトリクスについて整理しておきます。

標準

クラスタに標準でついているメトリクスは2種類あります。

  • 実際に今使われている、CPU使用率・メモリ使用率
  • インスタンスに対してタスクが確保している、CPU予約率・メモリ予約率

メモリ予約率は、親OS用の予約容量を引いた実効容量を分母とした数値になっています。

一通り必要なタスクを起動した状態で、予約率がやけに低いとしたら、インスタンス実効容量に対するタスクのリソース値がいまいちで、十分に活用できず空きがあることになるので、序盤ではチェックしてみるとよいでしょう。

Container Insights

ECS を本番運用するには Container Insights を有効にするのは、ほぼ必須であると考えてよいです。本番以外はお金が勿体ないので OFF にしていいです。

有効にすると追加で得られるメトリクスは、「Amazon ECS Container Insights メトリクス」 – Amazon CloudWatch に書いてありザッとまとめると、

  • CPU, Memory. Network RX/TX, Storage Read/Write Bytes
  • インスタンス OS ファイルシステムの使用率, インスタンスデータファイルシステムの使用率

コンテナ運用の基本として、コンテナ内にデータを蓄積せず、外に吐き出すというものがあります。あるとはいえ、それぞれ事情があったり、気づかず蓄積することもあるので、インスタンスのストレージ容量に余裕があるのか、時間経過で危うくなる可能性はあるのか、を可視化・アラート化するのは大事です。


インスタンスのライフサイクル

Managed Instance は 14 日間隔で入れ替わることが確定しています。この意味でも、Service は複数のタスクで稼働できる構成である必要があります。

何が起きるかというと「ドレイン」が開始されます。ドレインについてはこちら。

旧インスタンスに新タスクを起動できなくなり、新インスタンスが起動して代わりのタスクが先に起動します。新タスクが起動したら旧タスク・旧インスタンスが削除されて入れ替わりが完了します。

Managed Instance の管理画面では手動でドレインすることが可能なので、実際に実行してみてサービスの稼働率やデータに問題がないかを試すことができます。


その他

最後に、細かいメモを残しておきます。

EC2 上のデータ

Managed Instance を構成/起動すると、LaunchTemplate が作成され、インスタンスのリストにも出現します。

タグとか色々ついてるので見てみると へー ってなりますが、直接触ることはありません。

タスク内CPU

Fargate では CPU Model name を確認できましたが、Managed Instance 内のタスクには何も表示されません。これは c8g.large 内のタスク。


起動時間

c8g しか使っていませんが、タスクやインスタンスにかかる所要時間は、

  • インスタンス起動:30秒前後
  • タスク起動:10~20秒程度(軽量かつヘルスチェック込み)
  • インスタンス削除:不要になってから1分程度

十分に速いので Autoscaling として問題ないし、危ない時間帯は Schedule とで二段構えにすればよいので心配はないですね。

スポット非対応

公式説明にはスポットインスタンスについて言及されていないと思いますが、
terraform で max_spot_price_as_percentage_of_optimal_on_demand_price = 50 をつけたら、
インスタンスの属性に maxSpotPriceAsPercentageOfOptimalOnDemandPrice は付きました。

付きましたが、スポットインスタンスとして起動はしませんでした。

AWS のこういう仕組みでは、あとからスポット対応されることはよくあれど、今回の Managed Instance に限っては、空いているリソースがある場合は詰め込む設計になっていることから、スポットには対応しないのではないかと考えています。

上述した通り、Multi-AZ を考慮すれば1インスタンス1タスクが最もわかりやすく効率も良い場合が多いので、その場合はスポット対応されれば嬉しいだけのはずです。

しかし、Service や Role のようなものを気にして設計するかどうかはユーザーに委ねられているので、何も考えず詰め込まれる可能性を考慮すると、スポット強制Terminate はクリティカルになりかねないので、このままの仕様でスポットを使えるようにしてくることはない気がします。


おわりに

いくつか考察点があるため、どの選択肢が絶対優位という感じではなさそうです。

とはいえ、仮に Graviton 5 が EC2 に登場してきて、Fargate が Graviton3:2=8:2 のままだとしたら、2世代以上の差が開くことになるので、そこまでいくと Fargate より Managed Instance が優勢といってよさげです。

AWSユーザーとしては、柔軟な SavingsPlans を適用しつつ、柔軟に最もコスパが良い構成に切り替えられるようにしておくのが最良と思われるので、今回で準備としては整いましたいつでも来やがれという感じでしょうか。


個人的には、Fargate でメモリ容量を細かく調整できるのは結構気に入っていて、スポットも使えるので、1世代差くらいなら Fargate でイィと思うので、サクッと Graviton3 のみで動くようになってくれれば言う事なしです。

1~2年スパンの中で、時期によってどれにすべきかってのはブレるとはいえ、このくらいのスパンならどれかに固定して最適化して運用する方が良い場合も多いと思うので、落ち着いて判断していきたいところです:-)