· 17分で読了

なぜサービスデプロイに ECS を使うべきなのか

この記事は中国語から自動翻訳されたものです。翻訳によりニュアンスが失われている場合があります。

僕は小規模チームでAWSを使うのが大嫌いだが、もし不運にもAWSでWebサービスを構築しなければならないなら、ECSを使うことを強くおすすめする。

なぜ不運なのかについては、また次回話そう🥲。

ECS とは何か

ECS(Elastic Container Service)は、AWSのコンテナオーケストレーションサービスであり、Dockerコンテナのライフサイクル管理を担ってくれる。本題に入る前に、いくつかの中核となる概念を押さえておこう。

Cluster

計算リソースの論理的なグループ。データセンターのようなものと考えることができ、すべてのコンテナはこのクラスター内で動作する。クラスター自体に料金は発生せず、課金対象となるのはその中で動く計算リソースだ。

Task Definition

コンテナの設計書(仕様書)。使用するDockerイメージ、割り当てるCPUやメモリ、環境変数、ポートマッピング、ログ設定などを定義する。変更するたびに新しいリビジョンが生成されるため、追跡やロールバックが容易だ。

Task

Task Definitionに基づいて実際に起動されたコンテナインスタンス。1つのTask Definitionから同時に多数のTaskを実行できる。常駐型と単発型の2つに分かれる。

Service

Taskを管理する上位の抽象化レイヤー。指定された数のTaskが継続して実行されることを保証し、あるTaskが停止した場合は自動的に新しいものを立ち上げる。また、ALBとの連携、ネットワーク、セキュリティグループを担い、正常なTaskにトラフィックをルーティングする。

Fargate

AWSが提供するサーバーレスのコンピュートエンジン。Fargateを使えば、EC2インスタンスを自分で管理する必要がなく、コンテナに必要なCPUとメモリの量をAWSに伝えるだけで、基盤となるマシンはAWSが面倒を見てくれる。もう1つの選択肢はEC2起動タイプ(launch type)で、自分でマシンを管理するため柔軟性は高くなるが、運用の負担も大きくなる。

簡単に言うと、関係性はこうだ。Clusterは複数のServiceを含み、各ServiceはTask Definitionに基づいて複数のTaskを実行し、Taskは実際にFargateまたはEC2上で稼働する。

どうしても AWS を使わざるを得ないなら、迷わず ECS を検討すべき

上のリストを見ると、直感的にはEC2を1台立ち上げて動かしたほうがシンプルに思えるかもしれない。

表面的にはそうだ。しかし、1台のEC2を管理するのはペットを飼うようなもので、病気にもなるし、老いるし、継続的な世話が必要になる(以下はすべて実際にあった悲劇だ😢):

  • OSのセキュリティアップデート:カーネルパッチ、glibcの脆弱性、OpenSSLの更新。更新しなければ丸裸で晒されているようなものだし、更新すればアプリケーションが壊れるかもしれない
  • メモリ管理:Swapの設定が少なすぎればOOM Killerに落とされ、多すぎればパフォーマンスが低下する。OOMが発生したときにはSSHすら入れず、マネジメントコンソールをただ呆然と眺めることしかできない
  • ログローテーション:設定を忘れ、半年後にディスクがいっぱいになってサービスが突然落ちる
  • プロセスマネジメント:アプリケーションがクラッシュしたとき、誰が再起動してくれるのか?systemd?pm2?どれも個別設定が必要だ
  • SSHキーの管理:誰がログイン権限を持っているのか?退職した人のキーは削除したか?(答えは知らないほうがいいかもしれない)
  • デプロイ方法rsyncscp?自作のデプロイスクリプト?デプロイのたびにお祈りすることになる
  • 環境ドリフト:半年間稼働したEC2は、新規起動したマシンとは別物になっている。誰が何を手動インストールし、どの設定を変更したのか、誰も覚えておらず、誰も再構築する勇気が出ない

コンテナの哲学は真逆だ。デプロイのたびにクリーンなイメージから起動し、環境ドリフトは起きず、壊れたら破棄してやり直せばいい。

EC2でもLaunch TemplateやAMIを使えば環境ドリフトを防げるし、Ansibleを使えば各インスタンスの状態を統一できると言う人もいるだろう。しかし、実務上では以下の点を考慮しなければならない。

  • ECSは本質的にDockerイメージで起動する。Dockerfileは通常アプリケーションコードと同じリポジトリにあるため追跡が容易で、ローカル環境でもデバッグできる。一方でAnsibleは通常別個に管理されるため、開発者の認知的負荷が増加する
  • AMIやLaunch Templateを管理するために、Packerなどの「AMI管理」ツールをさらに導入する必要があり、多くのアプリケーションにとってはオーバーキルとなる

僕自身や周囲の友人の経験(ここ3年ほどの印象で4人ほど)を総合しても、現在ある程度規模の大きい企業はほぼすべてコンテナ化へとシフトしている。

適切に設定すれば、VPSインスタンスよりもコストを抑えられる可能性がある。コンテナ化は運用面でも、環境の一致性、デプロイの再現性、リソース利用効率、水平スケーリング能力など、大きな優位性を持っている。

次の選択肢はECSかEKSか、だ。

EKS のハードルは想像以上に高い

EKSはAWSにおけるマネージドKubernetesだ。

耳触りは良いが、Kubernetes自体の複雑さはすでに極めて高い。Pod、Deployment、Service、Ingress、ConfigMap、Secret、Namespace、Helm chart、さらには各種CRD。EKSはコントロールプレーンを管理してくれるだけで、残りはすべて自分たちで対応しなければならない。

チームに専任のSREがいて、すでにKubernetesを使っているならEKSは妥当な選択肢だ。しかし、3〜10人程度のほとんどのチームにとって、それは牛刀をもって鶏を裂くようなものだ。ECSはずっとシンプルであり、EC2と比べてもいくつかの優位性がある。

サーバーマシンを管理したくないなら Fargate を使おう

ECSとFargateを組み合わせれば、コンテナで実行するイメージ、必要なCPUとメモリを定義するだけで、基盤となるマシンはAWSが処理してくれる。前述したOSパッチ、ディスク監視、SSHキー管理などに頭を悩ませる必要は一切ない。支払うのはコンテナが実際に稼働した時間だけであり、アイドル状態のマシンではない。

Blue/Green デプロイが組み込みで提供されている

Blue/Greenデプロイの概念は非常に直感的だ。現在プロダクション環境で動いているバージョンをBlueとし、新バージョンを別のコンテナ群にデプロイしてGreenとする。2つの環境が同時に存在するが、トラフィックはまだBlueに向けられている。テストリスナー(例えばポート8080を開放するなど)を通じてGreenに問題がないか先に検証し、確認が取れてからトラフィックを切り替える。

従来のローリングアップデートと比較して、Blue/Greenの最大のメリットは次の通りだ:

  • 切り替え前に事前テストができる:新バージョンはすでにプロダクション環境で動いており、同じデータベース、同じ環境変数を使用しているため、テストリスナー経由で機能が正常か確認できる。デプロイ完了後にバグに気づくといった事態を防げる
  • トラフィックの切り替えを段階的に行える:CodeDeployはCanaryやLinear戦略をサポートしている。例えばトラフィックの10%を先に流して5分間様子を見、問題がなければすべて切り替えるといった運用が可能で、一発勝負にならずに済む
  • 問題発生時にワンクリックでロールバック可能:Greenが落ちた?CodeDeployのコンソールでボタンを1つ押すだけでBlueに戻せる。git revertも、パイプラインの再実行も、Task Definitionの手動修正も不要だ

EC2でのロールバックと比較してみてほしい。SSHでログインし、手動で旧バージョンに切り替え、プロセスを再起動し、祈る。Blue/Greenにおける体験はこれとは比べものにならないほど快適だ。

Blue/Greenデプロイの本質的な考え方については、こちらの記事を参考にしてほしい。優秀な元同僚のHenryがHahowでどのようにBlue/Greenデプロイを実践したかを共有したもので、9年経った今でもそのコンセプトはそのまま通用する。

Docker イメージ管理と ECR との統合

ECSとECRはネイティブに統合されており、Task Definition内でECRのイメージURIを直接指定し、IAM権限を適切に設定すればプルできる。

重要な原則がある。それは、latest タグを使わないことだ。ビルドごとにコミットハッシュをタグとして使用すれば、プロダクションで実行されているコードのバージョンが常に把握でき、インシデント発生時の追跡が可能になる。ECRはタグのイミュータビリティ(tag immutability)をサポートしており、既存のタグの上書きを仕組みとして防ぐことができる。

ECRではライフサイクルポリシーを設定でき、一定のルールに従ってDockerイメージを保持し、未使用または期限切れのものを自動削除できる。

セキュリティの観点を前提とすると、開発環境と本番環境は完全に分離されるべきだ。AWSでは別のアカウントで区切るのが一般的なので、ECRも環境ごとに分けるべきだろう。

しかしそうなると、同じDockerイメージを別々のECRにプッシュすることになり、これはセキュリティを考慮したトレードオフとなる。僕個人としては、すべて同じECRソースを共有するほうが好みだ。正解があるわけではなく、チームが現在目指すゴールによって決まる。

Auto Scaling

ECS Serviceはオートスケーリングをネイティブでサポートしており、CPU使用率、メモリ、あるいはALBのリクエスト数に応じてTask数を自動調整できる。また、SQSのキュー数に応じた動的調整など、独自の設定も可能だ。

設定は複雑ではなく、ターゲット追跡ポリシー(target tracking policy)を定義するだけで済む。

EC2にもAuto Scaling Groupはあるが、AMIや起動テンプレート(Launch Template)を保守し、新しく立ち上がったインスタンスが既存のものと一致しているか確認しなければならない。ECS + Fargateのスケーリングなら、コンテナをいくつか追加で立ち上げるだけなので、極めてシンプルで無駄がない。

注意すべき点は、Blue/Greenデプロイ中はオートスケーリングが一時停止することだ。これにはパイプラインの前後にスクリプトを挟んでスケーリングポリシーを制御することで対処できる。

ロギングとモニタリング

ECSはコンテナの標準出力/標準エラー出力をCloudWatch Logsに送信する機能をネイティブでサポートしている。Task Definitionで awslogs ログドライバーを設定するだけでよい。EC2のようにCloudWatch Agentをインストールしたり、ロググループを設定したり、ログローテーションを気にする必要はない。コンテナを破棄して再起動しても、ログはCloudWatch上に残り続ける。

CloudWatch Container Insightsと組み合わせれば、各サービスや各タスクのCPU、メモリ、ネットワーク使用率を直接確認できる。自分でnode_exporterをインストールしたりPrometheusを設定したりする必要はない。

セキュリティ上の考慮事項

ECS + Fargateには、セキュリティ面で見落とされがちな大きな利点がある。それは**「SSHがない」**ということだ。

制約のように聞こえるかもしれないが、実は素晴らしいことだ。SSHがないということは、誰かが「一時的に」ログインして勝手に変更したり、こっそりソフトウェアを入れたり、ログアウトを忘れたりすることができない。すべての変更はTask DefinitionとCI/CDパイプラインを経由しなければならず、生まれながらにしてイミュータブルインフラストラクチャ(immutable infrastructure)となる。

デバッグが必要な場合は、ECS Exec(SSM Session Managerベース)を使ってコンテナに入ることができ、各セッションの監査ログも記録される。

ここまでのまとめ

ECSは胸が躍るような技術選定ではないかもしれないが、十分機能し、安定しており、学習曲線も妥当だ。AWSの世界では、「退屈な選択肢」こそが往々にして最善の選択肢である(よりお金がかかる選択肢でもあるが)。

デプロイから逆算してフローを設計する

AWS上でコンテナサービスを構築する際、最も重要なことは何かと聞かれたら、僕は**「まずデプロイを固めること」**と答える。コードがgit pushされてからプロダクション環境で動作するまでの流れを、最初に明確にしておくのだ。

ECSのデプロイとは、最新バージョンのコードをDockerイメージとしてビルドし、Task Definitionを更新して、デプロイメントをトリガーするプロセスを指す。

テスト環境と本番環境のデプロイフローは、目的に応じて分けて考えるべきだ:

  • テスト環境:柔軟性を最優先とし、コードマージから環境へデプロイされるまでの時間と複雑さを極力減らす
  • ステージング(Staging)環境:本番環境と可能な限り同一のフローを維持し、リリース前にあらゆるエラーを確実に検知できるようにする
  • 本番(Production)環境:テスト環境と厳格に分離し、開発者が本番環境に直接触れられないようにする

デプロイは開発ライフサイクル全体の中で最も長く影響を及ぼし続ける部分だ。ネットワーク設計は一度作れば基本的に滅多にいじらないし、監視は後から少しずつ補強できるが、デプロイは毎日、PRごとに必ず向き合う作業だ。デプロイ体験が悪いと、チーム全体の開発効率が引きずり落とされる。

AWSの世界では、デプロイは通常CodePipeline + CodeBuild + CodeDeployという構成を取る。ベンダーロックインを懸念する場合は、一部のフローを変更することも可能だ。

例えばGitHub Actionsを使う場合、AWS公式のActionも用意されている。しかしどちらの道を選ぼうと、以下の処理が必要になる:

  1. イメージをビルドしてECRへプッシュ
  2. Task DefinitionのイメージURIを更新
  3. ECS Serviceを更新してデプロイをトリガー
  4. サービスが安定するまで待機

プロダクション環境であれば、さらにBlue/Greenデプロイ(CodeDeploy経由)、承認フロー、アカウント間のECRイメージ同期なども必要になるかもしれない。レイヤーが1つ増えるごとに、エラーが起きる可能性も1つ増える。

IaC の導入は早ければ早いほど良い

これらのAWSリソースをマネジメントコンソール上で手動クリックして管理していると、常に恐怖の中で生きることになる。

ある日誰かが誤ってセキュリティグループのインバウンドルールを変更してしまったり、IAMポリシーが「一時的」に変更されたまま戻し忘れられたりしたら、原因特定に丸一日潰れることもある。ましてや2つ目の環境(ステージング)を作ろうとしたとき、プロダクション環境がどう設定されていたのかさっぱり思い出せないことに気づくだろう。

TerraformによるAWSリソースの管理は初日からやるべきことであり、「後で暇なときに整理しよう」ではいけない。

これもまた、デプロイの利便性とIaCを両立させる際に考慮すべきトレードオフだ。Terraformの中心思想は、宣言的な記述によってインフラを期待する状態にすることにある。しかし実務上、ECSのデプロイで関係するのは通常以下の点だけだ:

  • 今回デプロイするDockerイメージタグを適用した、新しいTask Definitionを宣言する
  • Serviceを更新してデプロイをトリガーする

インフラ自体は普段変更されないため、毎回のデプロイまでTerraformを経由するのは少々面倒だ。ここで、僕が普段よく行っている設定を紹介する。

Terraformの lifecycle ブロックを使えば、Task Definitionのimage URIの変更を無視できる。これにより、インフラはTerraformが管理し、アプリケーションのデプロイはCI/CDパイプラインが管理するという役割分担ができ、両者が衝突しなくなる。これは僕がECSデプロイにおいて最も実用的だと感じているテクニックだ:

resource "aws_ecs_service" "app" {
  # ...
  lifecycle {
    ignore_changes = [task_definition]
  }
}

IaCがなければ、インフラはブラックボックスと化す。中身がどうなっているかを知っているのは最初に設定した張本人だけであり、そしてその人物はたいていすでに退職している。

デプロイの複雑さがいかにコストを蝕むか

多くのチームは「デプロイが少し遅くても構わない」「手順が数ステップ増えるだけだ」と思いがちだが、こうした一見些細な摩擦が積み重なることで発生するコストは、数字として可視化できる。

  • デプロイが遅い
  • デプロイごとの手順が多い
  • 手動操作によりミスが発生する確率が高まる
  • 開発者の認知的負荷が増大する
  • 大規模な変更を恐れるようになる
  • より致命的なエラーの温床となる

これらの隠れたコストを数値化してみよう:

本番反映までの時間(Time to Production)

5人チームを想定し、エンジニア1人あたりの年間人件費(福利厚生、機材込み)を約200万台湾ドルとすると、時給換算で約1,000元になる。

指標シンプルなデプロイ複雑なデプロイ
1回のデプロイ時間10分45分
週あたりのデプロイ回数15回5回
デプロイ失敗率3%15%
失敗後の修復時間15分2時間
デプロイに費やす週の総時間約3時間約8時間

デプロイ作業そのものだけでも、複雑なデプロイはシンプルなデプロイに比べて毎週5時間多くかかる。5人チームで計算すると毎週約25時間が無駄になり、年間では1,300時間、コスト換算で約130万台湾ドルに達する。

バグに起因する隠れたコスト

デプロイの困難さがもたらすもう一つの隠れコストは、バグ発生率の上昇だ。変更失敗のたびに追加で8時間の調査と修復が必要になると仮定しよう:

指標高頻度デプロイ低頻度デプロイ
月間デプロイ回数60回20回
変更失敗率5%30%
月間失敗回数3回6回
1回あたりの修復コスト(工数)8時間16時間(問題が複雑化しがち)
月間の総修復コスト24時間96時間

低頻度デプロイでは、バグ修正に毎月72時間余計に費やすことになり、年間で864時間、約86万台湾ドルの損失となる。

これには以下が含まれていない:

  • 機会損失:エンジニアがデバッグに費やしている時間は、本来なら新機能の開発に充てられたはずだ
  • ユーザーの離脱:プロダクション環境でバグが出ればユーザー体験を損ない、MAUの低下による損失は定量化が難しいものの確実に存在する
  • 心理的コスト:デプロイのたびに時限爆弾の解体作業をしているようなチームで、士気が高まるはずがない

負のスパイラル

技術に詳しくない経営陣は、開発チームが毎回ミスばかりしているように感じる。

チームがデプロイフローの改善を要求しても、短期的にはプロダクトに直接的な影響が見えにくいため、後回しにされがちだ。デプロイフローが改善されないまま、そのコストは現場の開発者が直接背負わされ、信頼は失われ続け、経営陣も開発チームからの提案に耳を貸さなくなる。

このスパイラルがもたらすコストは、想像以上に高くつく:

  • 改善を遅らせることによる複利コスト:先ほど算出した通り、複雑なデプロイによる年間の隠れコストは約225万台湾ドル。改善が1年遅れればその金額がそのまま蒸発し、2年遅れれば450万台湾ドルになる
  • 人材流動コスト:士気の低下がもたらす最も直接的な結果は離職だ。エンジニアの採用・代替コストは概ね年俸の50%〜150%(採用活動、面接、オンボーディング、立ち上がり期間)と言われており、年俸コスト200万台湾ドルで計算すると1人あたり約100万〜300万台湾ドル。5人チームでデプロイ体験の悪さにより毎年1人余計に離職した場合、追加コストは年間100万〜300万台湾ドルになる
  • 信頼の赤字による意思決定コスト:経営陣が技術チームを信頼できず、技術的な提案を却下し、技術的負債が積み上がり続ける。仮に四半期ごとに1件の改善提案が却下され、それぞれが年間50万台湾ドル削減できるものだったとすると、年間4件却下されることで約200万台湾ドルの節約機会を失うことになる
項目年間コスト
デプロイの隠れコストの継続発生225万台湾ドル
余計な離職・採用コスト100万〜300万台湾ドル
技術改善の遅延による機会損失約200万台湾ドル
合計約525万〜725万台湾ドル/年

これはエンジニア2〜3人分の年収にほぼ匹敵し、時間の経過とともにさらに悪化していく。改善が遅れれば遅れるほど累積コストは膨らみ、離職者は増え、負のスパイラルを断ち切るのが難しくなる。

まとめ

控えめに見積もっても、5人のチームが複雑なデプロイフローによって被る毎年の隠れコストは約200万〜250万台湾ドルに達する。これにはNAT Gatewayが毎月静かに溶かしていく数千ドル、アイドル状態のリソース費用、そしてAWSの請求書を解読するために費やす時間などは含まれていない。

ジュニアエンジニア1人分の年収とほぼ同等だ。

デプロイフローは初日から投資する価値がある。早い段階で正しく構築できれば、日々、そしてPRごとに節約されるコストが複利で積み上がっていく。

おわりに

もしチームがすでにAWSに縛られている(AWSを使うことが前提になっている)なら、ECSは僕が現在最も推奨するコンテナサービスの選択肢だ。EC2よりも手がかからず、EKSよりも現実的で、Fargateと組み合わせれば運用負担を大幅に削減できる。

だがECS自体はパズルの1ピースに過ぎない。実際に運用するには、VPCのネットワーク設計、ALBのトラフィックルーティング、IAMの権限管理、ECRのイメージ管理、CloudWatchの監視設定なども対応しなければならない。

これらのコンポーネントは互いに絡み合っており、1つでも設定を間違えれば何時間もデバッグに追われることになる。ECSを選ぶのは単なるスタート地点であり、周辺のインフラを合わせて設計しきってこそ完了と言える。

最近、たまたま複数のプロジェクトで同様のニーズがあったため、この記事は僕自身の考えをまとめたものだ。もしこの記事に読者からの反響があれば、実務でECSのデプロイフローとアーキテクチャをどのように設計するかを紹介しようと思う。まあ、誰も反応してくれなくても書くつもりだけど😂

あるいは、そもそもAWSなんて必要ないかもしれない?

関連記事

他のトピックを探索