✏️ 👤

Amazon ECS に途中で挫折しないために

=== 2021/01/13 更新 ===
本記事に挙げているペインポイントは Amazon ECS 自体の機能追加や改善、また AWS Fargate の登場により現在では解消しています.
2015年の冒頭に Amazon ECS が GA した当時はこんな感じだったんだなぁへ〜という気持ちで読んでいただけると良いかもしれません.
=====================

この記事は AWS Advent Calendar 2015 の 8 日目です.

昨日は @dkfj さんの SWF x Lambda でした. SWF は AWS に触れて以来ずっと食わず嫌いしているので、個人的に実装の紹介がすごく楽しみです :)

さて、タイトルの通りこの記事は Amazon ECS を使い始める前に知っておく/想定しておくと良さげなことを紹介し、みんなで楽しくコンテナ運用ができるようになることを目標としています. 事前に知識として持っておくだけでムダな時間をかけずに運用にトライできるはずです.

Amazon ECS の構成要素についての知識があったほうが読みやすいので、事前に What is Amazon EC2 Container Service? - Amazon EC2 Container Service ページに並んでいる用語だけでも押さえておくことをオススメします.

あくまでも僕の経験に基づくお話なので、コンテナが3度の飯より美味しい方にはつまんないかもしれません.

そもそも Amazon ECS って?

本家サイトの日本語版「Amazon EC2 Container Service (高いスケーラビリティとパフォーマンスを備えたコンテナ管理サービス) | アマゾン ウェブ サービス(AWS 日本語)」によれば、高性能なコンテナ管理サービスだそうです.

コンペティターとしては、マネージドなコンテナ管理サービスでは GCP の Container Engine、自前でサーバーを管理するタイプのものだと Docker Swarm, Kubernetes などなどが挙げられると思います.

要は複数の EC2 インスタンスを取りまとめて管理した上で、あらかじめ定めた通りにコンテナをグリグリ動かすためのマネージドサービスです. たくさんのサーバーを徘徊して docker run コマンドを実行する手間を省いてくれます.

使い始めに知っておきたい

「まずはお試しで Amazon ECS!」というタイミングの話から. 1 台の EC2 インスタンス (Amazon ECS の世界では ECS インスタンスと呼びます) だけがクラスタに存在しているときに起きやすい問題です.

ポートだってリソースなんです

先述した通り、Amazon ECS の管理下で新たにコンテナを走らせようとすると、まずは Amazon ECS は取りまとめて管理している ECS インスタンスの中で空いている場所を探してそこでコンテナを走らせようとします.

マシンリソースというとCPU やメモリが思い浮かびますが、意外と忘れがちなのが、ポートです.

具体的にはどんな問題なの

yum や apt-get などで Apache と Nginx をインストールして、初期設定のまま動かそうとするとポート 80 が被って起動しないアレです.

ただし、実際に Amazon ECS でこんな事情でポート被りが起きるわけではなく、後述のような理由で発生することが多いと思います.

Service を更新しようとすると…

Amazon ECS の Service としてすでに走っている Docker コンテナの更新版をデプロイしようとする場合、手動ワークフローでは

  1. Task Definition の image を更新
  2. Service が利用する Task Definition を更新

という流れになります.

Service が更新されると、Amazon ECS は

  1. 空いているリソースを見つけて新しいバージョンの Docker イメージからコンテナを走らせる
  2. 新バージョンのコンテナが動いていることを確認できたら古いコンテナを停止する

という処理を行います.

するとどうでしょう. 新コンテナは ポート 80 を必要としているけど、古いコンテナが使っているから起動できない!という悲しい事態がひっそりと起こります.

この際、Amazon ECS は特に大きなアラートなど出さずにただただ健気にリトライを繰り返すため、より悲しい気持ちになります. (Service のログのような立ち位置である Events とかを見ていればすぐに気づけます)

どう解決するのか

現実的な解決策

上の例は基本的にはお試し利用や開発環境など、インスタンス 1 台でクラスタを構成した場合に起こります. t2.small などの安めのインスタンスをもう1台追加してあげましょう.

新しく追加したインスタンスがクラスタにジョインすると、今までのスタッキングが嘘のようにサクッと新バージョンのコンテナが立ち上がります. (当然ですが、インスタンスを足しても CPU/メモリ が足りてない場合は状況に変化はありません)

無料の範囲での解決策

「お試しで使ってるだけだから無料の方が…」という場合は、

  1. Service の Desired Count を 0 にする
  2. コンテナが停止するのを待つ (待つのが苦手な人は 画面/CLI から強制停止しちゃいましょう)
  3. Service が利用する Task Definition を新しいものに更新して、Desired Count を 1 に戻す

というオペレーションを行いましょう. ただ、めんどくさいのであんまりおすすめしません.

(話はずれるけど Amazon ECS 自体の利用料は無料です. すごい.)

理想的な解決策

Elastic Load Balancing (ELB) が Dynamic Port Mapping に対応してくれたら全て解決です.

Amazon ECS, Task Definition のレイヤーでは Dynamic Port Mapping に対応しているので、ELB 側の神対応が待たれます.

ちなみに、この後の話全般に共通しますが、Amazon ECS はリソースの管理と利用には敏感ですが、リソースの準備に関しては鈍感です. 個人的にはこれは Amazon ECS が混じりっ気なしのコンテナ管理サービスであるとすることをゴールとして設計/実装されているためだと考えていますが、ポート以外のリソースでもハマることは良くあると思います.

2015/12/24 追記: 最近の機能追加によって実現できる現実的かつ理想的な解決策

先日の Amazon ECS のアップデート (SA 岩永さんによる日本語での紹介はこちら) によって解決できるようになりました.

具体的には、minimumHealthyPercent0 % に、maximumPercent100 % に設定することで、1 台のインスタンスでもポートの重複問題を気にせずに新バージョンのコンテナを動かせます. 上に紹介した記事を読んでいただけると、なぜこの設定がそういう動きに繋がるのかが分かると思います. ぜひご一読ください.

(ただしこの方法はサービスに少なからずダウンタイムが発生しますので、あくまでも Amazon ECS 自体のテストドライブだったり、開発中の利用だったりに用途が限られる気はします)

複数のコンテナを走らせはじめる頃に知っておきたい

単純なリソース不足

CPU に関しては多少足りなくても足腰が弱るだけですが、メモリが足りないとコンテナさんが死にます.

どう解決するのか

事前にどのくらいのリソースが必要か検討しておく以外にはありません. 対象としては CPU/メモリ/ポート くらいを想定しておけば動かなくて困るという事態はとりあえず回避できると思います. (サクッと安心して動かすためのこのへんのリソースプランニングのための機能/ツールが欲しいなぁと最近よく思います. もしくはリソース不足の時の通知とかあったら特に開発中とかは嬉しい.)

また、コンテナ界の雄 Google さんのように無限のリソースプールがある場合にはこの問題にハマることはまずありません. この「無限のリソースプール」を用意してそこで大量のコンテナを動かすことはコンテナ運用の上では理想的ですが、実際にはお金的な意味で厳しいです.

セキュリティグループ/VPC まわり

複数の ECS インスタンスがジョインしているクラスタ内でコンテナを走らせる場合、そのコンテナがどのインスタンスで走るのかを厳密にはユーザーは指定できません. つまり、100 台のインスタンスがクラスタに参加している場合、リソースの空きさえあれば、100 台すべてがコンテナの配置対象となり得ます.

この際にわかりやすく問題になるのがセキュリティグループです. Web サーバーコンテナ用に 80 ポートを開けていたインスタンスだったのに、ポート 8080 を必要とする API サーバーコンテナが配置されてしまったため通信できない、ということが起こりえます.

1 つのクラスタ内で動かすコンテナはすべて同じポート番号で動くように Task Definition を定義してしまえばひとまず問題は解決できますが、この方法では Amazon ECS + Docker のメリットが半減してしまいます.

まずはコスト面. これまでの EC2 インスタンスそのままの運用では、ミドルウェアやアプリケーションライフサイクル(夜は停止とか)の差異、セキュリティなどの理由で複数のアプリケーションを同一インスタンスに共存させてリソースを使い切る試みはあまり行われていなかったと思います. しかし、アプリケーションをコンテナ化することによって、同じインスタンスの中でコンフリクトなく複数のアプリケーションを動かすことができるようになりました. もしセキュリティグループやサブネット、ルーティングの設定に影響されてクラスタに所属させられるインスタンス台数が減ってしまうと、コストメリットを享受しにくい構成になってしまいます.

次に可搬性の面. そもそもコンテナ技術を使ってアプリケーションを運用しようとしているのに、インスタンスの設定に引っ張られて配置先が決まってしまうのはコンテナの高い可搬性を生かせていない気がします.

どう解決するのか

「1 つのクラスタに全インスタンスをジョインさせ、どこでコンテナが動いてもいい状態をつくる」ことがゴールであるとすると、「どのコンテナが動いても大丈夫なように最大公約数的な設定を行う」ことが最善の手だと思います. (一概にすべてをカバーできるネットワーク/セキュリティ設定をする方がいいというわけでもありませんが)

この問題についてはセキュリティ要件なども絡んでくるため、プロジェクトによってはあえて「解決しない」というのも 1 つの方法だと思います.

サービスディスカバリー

難しい話を抜きにして書くと、Web サーバーが動く Web コンテナが、ビジネスロジックが動く API コンテナを必要としている場合に、どうやって Web コンテナから API コンテナ を見つけるの?という話です.

ここでは以下の 2 つの状況が想定されます.

お試しの延長で複数コンテナを動かす場合

Web コンテナと API コンテナ は 1 つの Task Definition の中に書かれており、link オプションで Web から API が参照可能になっていることでしょう.

この場合、特にサービスディスカバリーの問題は起きません.

さらにお試しが延長された場合

なんだかいい感じに動いてきたので、API コンテナをスケールさせて複数動かしたくなってきたとしましょう.

そうすると作業としては

  1. Task Definition を分割して Web コンテナ / API コンテナがそれぞれ定義されたものを作る
  2. Service も 2 つにして、1つは Web コンテナを、もう1つは API コンテナを動かすようにする
  3. Web Service の Desired Count は 1、API Service の Desired Count は 2

という流れになりそうです.

Task Definition が分割されたということは、 link オプションで Web コンテナから API コンテナを探すことはできません. こまった.

どう解決するのか

巷では Consul を使って… という事例がよく見つかりますが、 AWS の場合は ELB を使うのが最も楽かつ高機能です. もちろん ELB 自体も高機能なんですが、ここでいう「高機能」は Amazon ECS としてみた場合により良くなるという意味です. Amazon ECS は Service と ELB を組み合わせてあげると、コンテナを動かす際に、 ELB にアタッチされたかどうかまでを含めて「正しく動いているか」を判定してくれます. 素敵です.

今回の例でいうと、API Service が ELB に紐づけられており、Web コンテナからは ELB 経由で参照する、という形になります.

その他にも Route 53 で 任意のホスト名がインスタンスを向くよう定義して… といった方法があるかもしれませんが、Amazon ECS ではコンテナがどのインスタンスで動くのかは予想できないため得策ではないでしょう. また、Amazon ECS のメリットを最大限享受するためには、どこでコンテナが動くかなんて予想しないで済むアーキテクチャーになっているほうが健全です.

ぼちぼちの規模のクラスタを動かしはじめる頃に考えておきたい

ぼちぼちの規模のクラスタを動かす、ということはそこで動く製品/サービスは本番運用が見えてきているものかもしれません. 運用について考えておくべきところはコンテナであるかどうかはあまり関係ありませんが、素の VM と比較してコンテナだからこそ早めに考えておきたいことがいくつかあります.

継続的なデプロイのイメージを事前に持っておく

いくつもの Service がクラスタで動くようになってくると、当然 Task Definition の数も増えてきます.

デプロイのたびに手作業で Task Definition の更新と Service の設定をやっていては、職人さんが何人いても足りません.

特に Amazon ECS はサクッとコンテナをクラスタリングするまでが非常に簡単なので、このあたりを深く考えずにガンガン動かせてしまいます. せっかくコンテナを使って可搬性を高めたのに、人手不足が原因でプロジェクトの動きが遅くなってしまっては元も子もありません. 継続的なデプロイについて事前にしっかりと検討しておきましょう.

このあたりは各チームのデプロイ戦略とも関わってくるので一概にはまとめられませんが、簡単な事例を Container with AWS Advent Calendar 2015 で書こうと思ってます.

もちろん、コンテナでなくても継続的なデプロイについて考えておくことは必要ですが、今までに VM で培ったデプロイ知識が役に立たない局面もあるため、事前に検討しておくと後々はかどります.

ssh したら負け

ちょっとストレートな物言いですが、そもそもコンテナがどこで動いているかを考えずに Docker コンテナのクラスタを構成/管理できることが Amazon ECS のメリットの一つでもあるので、ssh で ECS インスタンスに入らなければいけない状態になった場合はアプリケーションやアーキテクチャー運用方法に問題があると思った方がいいでしょう.

ssh しなくて済むよう、Amazon ECS がユーザー任せにしていることを事前に知っておきましょう.

書き始めるとながーくなるので、また別の機会で紹介したいなと思っています.

番外編

ここでは番外編として巷でよく取り上げられる話を書きます.

オートスケーリングさせたいのに

AWS な皆様にはお馴染みオートスケーリングですが、今のところ Amazon ECS 界隈では「イイ!」と言える手法が確立されていないように思います.

個人的には Amazon ECS ネイティブでの対応を熱望していますが、現時点ではそのあたりの話は出てきていません.

スケーリングを自前でやるとすると、Service のメトリクスを監視してコンテナを増減させる、という動きを実装してしまう方法が一番わかりやすいですが、ここでも ECS は十分なインスタンスがあるか(余分なインスタンスがないか)については考えてくれないので、コスト最適化を考慮してインスタンスの増減までやるとなるとちょいとめんどくさいです.

このあたりの問題を僕の所属しているチームでどのように解決しているかについても、機会があればどこかで書きたいと思っています.