· 15分で読了

なぜ ECS でサービスをデプロイすべきなのか

# ソフトウェアエンジニアリング
この記事は中国語から自動翻訳されたものです。翻訳によりニュアンスが失われている場合があります。

僕は小規模チームで AWS を使うのが大嫌いだ。でも、もし不幸にも AWS で Web サービスを立てなければならないなら、僕は ECS を強く勧める。

なぜ不幸なのかについては、また今度話そう🥲。

ECS とは何か

ECS(Elastic Container Service)は AWS のコンテナオーケストレーションサービスで、Docker container のライフサイクル管理を担ってくれる。始める前に、いくつかの核心概念がある。

Cluster

計算リソースの論理的なグルーピングだ。データセンターのようなものだと思えばよい。すべての container はこの cluster の中で動く。Cluster 自体には費用はかからない。費用がかかるのは中で動く計算リソースだ。

Task Definition

container の仕様書だ。どの Docker image を使うか、CPU とメモリをどれだけ割り当てるか、環境変数、port mapping、log 設定などを定義する。変更のたびに新しい revision が作られるので、追跡や rollback がしやすい。

Task

Task Definition に基づいて実際に動く container instance だ。1つの Task Definition から複数の Task を同時に動かせる。常駐型にも単発型にもできる。

Service

Task を管理する上位抽象だ。指定した数の Task が継続して動くように保証し、Task が落ちれば自動で新しいものを立ち上げる。ALB 連携、ネットワーク、Security Group との接続も担い、トラフィックを正常な Task に流してくれる。

Fargate

AWS が提供する serverless の実行エンジンだ。Fargate を使えば、自分で EC2 instance を管理する必要はなく、container に必要な CPU とメモリを伝えるだけでよい。基盤となる機械は AWS が面倒を見てくれる。もう一方の選択肢は EC2 launch type で、自分で機械を管理する分、柔軟性は高いが運用負担も重くなる。

簡単に言うと、関係はこうだ。Cluster の中に複数の Service があり、それぞれの Service が Task Definition に基づいて複数の Task を起動し、その Task は実際には Fargate または EC2 上で動く。

どうしても AWS を使うなら、まず ECS を考える

上の長いリストを見ると、直感的には EC2 を 1 台立てて動かすほうが単純そうに見える。

表面上はそうだ。しかし 1 台の EC2 を面倒見るのはペットを飼うようなもので、病気にもなるし老いるし、継続的な世話が必要になる(以下はすべて実話の惨劇だ😢)。

  • OS セキュリティアップデート: kernel patch、glibc の脆弱性、OpenSSL の更新。更新しなければ裸で走っているのと同じだし、更新すれば application を壊すかもしれない
  • メモリ管理: Swap を少なくしすぎれば OOM kill、増やしすぎれば性能低下。OOM が起きると SSH すら入れず、console の前でただ呆然とするしかない
  • Log rotation: 設定を忘れると半年後に disk が満杯になり、サービスはそのまま落ちる
  • Process 管理: application が crash したら誰が再起動するのか。systemd か、pm2 か。どれも個別に設定が必要だ
  • SSH key 管理: 誰に接続権限があるのか。退職者の key は消したのか。(答えを知らないほうが幸せだ)
  • デプロイ方法: rsyncscp か、自作の deploy script か。毎回デプロイのたびに祈ることになる
  • 環境ドリフト: 半年稼働した EC2 と新規に立てたものは同じではない。誰かが何を手で入れて、何を変えたのか、誰も覚えていないし、誰も再構築する勇気がない

Container の思想は正反対だ。毎回のデプロイはクリーンな image から始まり、環境は drift しない。壊れたら消してやり直せばよい。

EC2 だって Launch Template や AMI を使えば環境ドリフトを防げるし、Ansible で各 Instance の状態を揃えることもできる、という意見もあるだろう。しかし実務では次の点を考慮する必要がある。

  • ECS は本質的に Docker image を起動する仕組みであり、Dockerfile は通常 application code と同じ Repository に置かれるため追跡しやすく、ローカルでも Debug しやすい。一方 Ansible は別管理になりがちで、開発者の認知負荷を間接的に増やす
  • AMI / Launch Template を管理するために、さらに AMI 管理用のツール、たとえば Packer のようなものを導入する必要があり、多くのアプリケーションにとっては overkill だ

僕自身と周囲の友人たちの経験(ここ 3 年の印象、友人 4 人)を総合すると、規模の大きめな会社はほぼコンテナ化へ移行している。

うまく設計すれば、VPS Instance より安くなる可能性もある。コンテナ化には運用面でも大きな利点がある。たとえば環境の一貫性、デプロイの再現性、リソース利用率、水平スケーリング能力だ。

次の選択肢は ECS か EKS だ。

EKS の敷居は思ったより高い

EKS は AWS 上の managed Kubernetes だ。

聞こえはいいが、Kubernetes 自体の複雑さがすでにかなり高い。Pod、Deployment、Service、Ingress、ConfigMap、Secret、Namespace、Helm chart、そして各種 CRD。EKS は control plane を管理してくれるだけで、残りは自分でやる必要がある。

チームに専任の SRE がいて、すでに Kubernetes を使っているなら、EKS は合理的な選択だ。しかし 3〜10 人程度のチームの大半にとっては、これは牛刀をもって鶏を裂くようなものだ。ECS はかなりシンプルで、しかも EC2 よりいくつかの利点がある。

機械を管理したくないなら Fargate を使う

ECS を Fargate と組み合わせれば、container がどの image を動かし、CPU とメモリをどれだけ必要とするかを定義するだけでよい。AWS が下層の機械を処理してくれる。先に挙げた OS patch、disk 監視、SSH key 管理は全部気にしなくてよい。支払うのは container が実際に動いた時間であって、遊んでいる機械の代金ではない。

Blue/Green デプロイが標準で使える

Blue/Green デプロイの概念は直感的だ。今 production で動いている版が Blue、新バージョンを別の container 群で動かしたものが Green だ。両方とも同時に存在するが、トラフィックはまだ Blue に向いている。test listener(たとえば 8080 port を開く)を通して Green を先に検証し、問題がないと確認してからトラフィックを切り替えられる。

従来の rolling update と比べて、Blue/Green の最大の利点は次の通りだ。

  • 切り替え前にテストできる: 新バージョンはすでに production 環境で動いており、同じ database、同じ環境変数を使っているので、test listener 経由で機能が正常か確認できる。デプロイが終わってから爆発していることに気づく必要がない
  • トラフィック切り替えを段階的にできる: CodeDeploy は Canary と Linear 戦略をサポートしている。たとえば最初に 10% のトラフィックを 5 分観察し、問題がなければ全面切り替えする、という形だ。一発 all-in ではない
  • 事故時の rollback が一発でできる: Green が爆発したのか。CodeDeploy console でボタンを 1 つ押せば Blue に戻せる。git revert も不要、pipeline の再実行も不要、task definition の手動修正も不要だ

EC2 上で rollback するとどうなるかと比べてみよう。SSH で入って、古い版に手動で戻し、process を再起動し、祈る。Blue/Green の体験はこの点で圧倒的に良い。

Blue/Green デプロイの核心はこの記事を参考にできる。これは優秀な元同僚 Henry が Hahow でどうやって Blue/Green デプロイを実践したかを共有したものだ。9 年経っていても、概念はそのまま当てはめられる。

Docker image 管理と ECR 連携

ECS は ECR とネイティブに統合されており、Task Definition で ECR の image URI を直接指定し、IAM 権限を整えれば pull できる。

重要な原則は latest tag を使わないこと だ。毎回の build で commit hash を tag にすれば、production 上でどの code が動いているのか常に分かり、問題が起きたときに追跡できる。ECR は tag immutability を設定できるので、仕組みとして既存 tag の上書きを防げる。

ECR には Lifecyle Policy を設定でき、一定のルールに従って Docker images を保持し、不要または期限切れのものを自動削除できる。

安全性の観点からは、開発環境と本番環境は完全に分離すべきだ。AWS では別々の Account で分けるのが基本であり、ECR も環境ごとに分けるべきだ。

ただしそうすると、同じ Docker image を別々の ECR に置くことになる。これは安全性のためのトレードオフだ。僕自身は、すべて同じ ECR ソースを共有するほうが好きだ。正解があるわけではなく、チームが今何を目指しているかで決まる。

Auto Scaling

ECS Service は auto scaling をネイティブにサポートしており、CPU 使用率、メモリ、ALB の request count に応じて task 数を自動調整できる。SQS の queue 数に応じて動的に調整する、といった独自の設定も可能だ。

設定は複雑ではなく、target tracking policy を 1 つ定義すればよい。

EC2 にも Auto Scaling Group はあるが、AMI や launch template を維持し、新しく立ち上がった instance が既存と一致するように保つ必要がある。ECS + Fargate の scaling は container を数個増やすだけで済み、すっきりしている。

注意すべき点として、Blue/Green デプロイ中は auto scaling が停止する。これは pipeline の前後で script を使って scaling policy を制御することで対処できる。

Logging と監視

ECS は container の stdout/stderr を CloudWatch Logs に直接送れる。Task Definition で awslogs log driver を設定するだけだ。EC2 に CloudWatch Agent を入れたり、log group を設定したり、log rotation を扱ったりする必要はない。container を消して再起動しても、log は CloudWatch に残る。

CloudWatch Container Insights を組み合わせれば、各 service、各 task の CPU / メモリ / ネットワーク使用量を直接確認できる。node exporter を自分で入れたり、Prometheus を設定したりする必要もない。

セキュリティ上の考慮

ECS + Fargate には、よく見落とされるセキュリティ上の利点がある。SSH がないことだ。

制限のように聞こえるが、実際は良いことだ。SSH がなければ、誰も「一時的に」入って設定を変えられないし、こっそりソフトを入れることもできないし、ログアウトを忘れることもない。すべての変更は Task Definition と CI/CD pipeline を通して行う必要があり、最初から immutable infrastructure になる。

Debug が必要なら、ECS Exec(SSM Session Manager ベース)で container に入れるし、各 session には監査記録も残る。

小結

ECS は胸が高鳴るような技術選択ではないが、十分で、安定していて、学習曲線も妥当だ。AWS の世界では、退屈な選択肢こそが最良の選択肢であることが多い(より金を食う選択肢でもある)。

デプロイから設計を始める

AWS 上でコンテナサービスを作るとき、僕が最も重要だと思うことは何かと聞かれたら、こう答える。まずデプロイを固めろ。コードが git push されてから production で動くまでを、先に明確にしておくのだ。

ECS のデプロイとは、最新の code を Docker Image にパッケージし、Task Definition を更新し、Deployment を起動する一連の流れを指す。

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

  • テスト環境: 柔軟性を最優先し、コードをマージしてからデプロイ環境に反映されるまでの時間と複雑さを極力減らす
  • Staging 環境: 本番とできるだけ同じフローを保ち、あらゆるエラーを本番前に発見できるようにする
  • 本番環境: テスト環境と厳格に分離し、開発者が本番環境に直接触れないようにする

デプロイは開発ライフサイクルの中で最も長く影響する部分だ。ネットワーク設計は一度作れば基本的にあまり変えないし、監視は後から少しずつ足せる。しかしデプロイは毎日、毎 PR で触るものだ。デプロイ体験が悪いと、チーム全体の開発効率が落ちる。

AWS の世界では、デプロイはたいてい CodePipeline + CodeBuild + CodeDeploy の流れを取る。vendor lock-in が気になるなら、一部の工程は別のものに置き換えてもよい。

たとえば GitHub Actions を使う場合、AWS 公式の Actions も連携用に提供されている。しかしどのルートを取るにせよ、次の処理は必要になる。

  1. Build image を作って ECR に push する
  2. Task Definition の image URI を更新する
  3. ECS Service を更新してデプロイを起動する
  4. service が安定するのを待つ

production 環境なら、Blue/Green デプロイ(CodeDeploy 経由)、承認フロー、アカウント間での ECR image 同期がさらに必要になるかもしれない。1 層増えるごとに、失敗の可能性も 1 つ増える。

IaC は早ければ早いほどよい

これらの AWS リソースを Console で手動クリックだけで作ると、恐怖の中で生きることになる。

ある日誰かがうっかり Security Group の inbound rule を変えたり、IAM Policy が「一時的に」変更されたまま戻されなかったりすると、原因を見つけるのに丸 1 日かかるかもしれない。まして 2 つ目の環境(staging)を作ろうとしたとき、production をどう設定したのか誰も覚えていないことに気づくだろう。

Terraform で AWS リソースを管理するのは、初日からやるべきことだ。「あとで時間ができたら整理する」ものではない。

これは、デプロイの手軽さと IaC を追求する際のトレードオフでもある。Terraform の中心思想は、宣言的な記述によって Infra を望ましい状態に保つことだ。しかし実際には、ECS のデプロイは通常次のことだけを含む。

  • 新しい task definition を宣言し、今回デプロイする Docker image tag を適用する
  • Service を更新してデプロイを起動する

毎回 Terraform でデプロイするのは少し面倒だ。基盤は通常変わらないからだ。ここで僕が今よく使う設定を示す。

Terraform の lifecycle block を使えば、Task Definition の image URI 変更を無視できる。つまり Terraform は基盤を管理し、CI/CD pipeline は application のデプロイを管理する という役割分担にでき、互いに干渉しない。ECS のデプロイで僕が最も実用的だと思う技巧の 1 つだ。

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 年では 1,300 時間になり、コストは約 130 万台幣 に相当する。

Bug の隠れコスト

デプロイが難しいことによるもう 1 つの隠れコストは、bug 率の上昇だ。各変更の失敗に追加で 8 時間の調査と修復が必要だと仮定する。

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

低頻度デプロイは、bug 修正に月 72 時間多く費やすことになる。1 年では 864 時間で、約 86 万台幣 だ。

これはまだ次を含んでいない。

  • 機会コスト: エンジニアが debug に使う時間は、本来なら新機能開発に使えた
  • ユーザー離脱: bug が production に出れば UX に影響し、MAU 減少の損失は定量化しづらいが確実に存在する
  • 心理コスト: 毎回デプロイが爆弾解除のようなチームは、士気が高くなるはずがない

負の循環

社長は技術に詳しくなく、毎回開発チームが問題を起こしていると思っている。

チームがデプロイ改善を求めても、短期的には製品への直接的な影響が見えにくいため、後回しにされやすい。デプロイフローは改善されず、コストは現場の開発者がそのまま負担し、信頼は下がり続け、社長も開発チームの提案を信用しなくなる。

この循環のコストは想像以上に大きい。

  • 改善遅延の複利コスト: 先ほど計算した複雑なデプロイの年間隠れコストは約 225 万。改善が 1 年遅れれば、その分がそのまま消える。2 年遅れれば 450 万だ
  • 人員流動コスト: 士気低下の直接的な結果は離職だ。エンジニアの補充コストは年収の 50%〜150% 程度(採用、面接、onboarding、立ち上がり期間)で、年収コスト 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 の請求書を理解するために費やす時間を含んでいない。

だいたい junior エンジニア 1 人分の年収に相当する。

デプロイフローには初日から投資する価値がある。早く正しく作れば作るほど、毎日、毎 PR で節約できるコストは複利で積み上がる。

まとめ

もしチームがすでに AWS に縛られているなら、ECS は僕が今最も勧めるコンテナサービスだ。EC2 より気楽で、EKS より実務的で、Fargate を組み合わせれば運用負担は大きく下がる。

ただし ECS 自体はパズルの一片にすぎない。実際に動かすには、VPC のネットワーク設計、ALB のトラフィック分配、IAM の権限管理、ECR の image 管理、CloudWatch の監視設定まで面倒を見る必要がある。

これらの要素は互いに絡み合っていて、どれか 1 つの設定ミスでも半日デバッグする羽目になりうる。ECS を選ぶのは出発点でしかなく、周辺の基盤をまとめて設計して初めて完了だ。

最近ちょうど複数のプロジェクトで似たニーズがあったので、この記事は僕自身の考えを整理したものだ。もしこの記事に読者から反応があれば、実務で ECS のデプロイフローとアーキテクチャをどう設計するかを紹介しよう。

あるいは、そもそも AWS がいらないのかもしれない?