この記事で学ぶこと
前回は VPC でクラウドに敷地を作り、サブネットとセキュリティグループで「外から触れるのは窓口だけ」の構造を組みました。今回はその敷地の上に、いよいよアプリを載せます。
#20〜#24 のコンテナ編で、アプリをイメージに固めて docker compose up で起動できるようになりました。あれで「動かす」は完成です。しかし本番運用で求められるのは「動かし続ける」ことで、この2つの間には見た目以上に大きな壁があります。その壁を埋めるのがコンテナオーケストレーションという仕組みで、AWS での代表が ECS と Fargate です。
この記事を終えると以下ができるようになります。
- 「動かす」と「動かし続ける」の間にある問題を挙げ、オーケストレーターが何を解決するのかを説明できる
- 「望ましい状態を宣言し、調整ループが維持する」という考え方を、実際に観察した挙動から説明できる
- ECS の部品(クラスター・タスク定義・タスク・サービス)を対応付けて、構成図を読み書きできる
この回は言語に依存しません。Go編・Python編どちらの読者もそのままお読みください。AWSアカウントも不要です。手を動かすパートは、手元の Docker に同梱されているミニチュアのオーケストレーターで再現します。
見出しの 🔴🟡⚪ は学習の優先度です。🔴は確実に覚える、🟡は理屈を理解する、⚪は今は流してOK。迷ったら🔴だけ確実に押さえてください。
🔴 「動かす」と「動かし続ける」の間にある壁
docker compose up -d でアプリが起動しました。ここから本番運用が始まるとして、次の問いに答えられるでしょうか。
- 深夜3時にアプリのプロセスが異常終了したら、誰が起動し直すのか
- コンテナではなく、それを動かしているマシンごと壊れたら?
- アクセスが増えて2台では足りなくなったら、誰が3台目を立てるのか
- 新バージョンのデプロイ中も、リクエストを1件も落とさないためには?
docker compose はどれにも答えてくれません。compose は「今、この1台のマシンで起動する」ための道具であって、起動した後のことは面倒を見ないからです。全部を人間が手作業でやるなら、交代制で24時間画面を見張ることになります。
24時間営業の店で考えてみてください。オーナーが決めるのは「売り場には常に2人立っていること」というシフトの方針です。誰かが急に休んだら代わりを呼ぶ、繁忙期は3人に増やす、といった実際の調整は店長の仕事で、オーナーがスタッフ1人1人に毎朝電話をかけたりはしません。
コンテナオーケストレーターは、この店長にあたるソフトウェアです。人間は方針だけを決め、個々のコンテナへの対応はオーケストレーターに任せます。
🔴 命令ではなく宣言。調整ループという考え方
店長の仕事ぶりをもう少し正確に言葉にします。ここが今回の核心です。
docker run を2回実行するのは命令です。「今すぐ2つ起動しろ」という指示で、実行した瞬間のことしか保証しません。1つが落ちても、命令はもう終わっているので誰も気づきません。
オーケストレーターへの指示は宣言です。「このアプリはレプリカ2で動いているべきである」という望ましい状態(desired state)を登録します。するとオーケストレーターは、現実の状態(actual state)と宣言を絶えず見比べ、ズレを見つけたら現実の側を直します。この見比べて直し続ける仕組みを調整ループ(reconciliation loop)と呼びます。
| 命令(docker run) | 宣言(オーケストレーター) | |
|---|---|---|
| 指示の意味 | 今すぐ起動しろ | 常に2つ動いているべき |
| コンテナが落ちたら | 落ちたまま | ズレを検知して起動し直す |
| 増やしたいとき | もう1回コマンドを打つ | 宣言の数字を書き換える |
この考え方は ECS 固有のものではありません。Kubernetes も、これから使うどのオーケストレーターも、中心にあるのは同じ調整ループです。逆に言うと、これを一度体で理解すれば、どのツールに移っても土台は共通です。
【手を動かす】ミニチュアのオーケストレーターで調整ループを観察する
理屈はここまでにして、実物を見ます。ECS を動かすには AWS アカウントと料金が要りますが、都合のいいことに、普段使っている Docker には Swarm というオーケストレーターが同梱されています。調整ループの挙動は ECS と同じ思想なので、これで観察します。
まずオーケストレーター機能を有効にします。
docker swarm init
私の環境(WSL2)では、これが一発では通りませんでした。
Error response from daemon: could not choose an IP address to advertise since this system has multiple addresses on different interfaces (10.255.255.254 on lo and 172.25.242.82 on eth0) - specify one with --advertise-addr
複数のネットワークインターフェースがあると、他のマシンに対してどのIPを名乗るか決められない、というエラーです。今回は1台だけで完結するので、ループバックアドレスを指定して通します。
docker swarm init --advertise-addr 127.0.0.1
Swarm initialized: current node (ym3mw4ssl7k495xwjf937xsvk) is now a manager.
準備ができました。ここに「宣言」を登録します。イメージは #32 でも使った、自分のホスト名を返すだけの小さなWebアプリです。
docker service create --name app --replicas 2 traefik/whoami:v1.10
overall progress: 2 out of 2 tasks
verify: Waiting 5 seconds to verify that tasks are stable...
verify: Service ucds5qvnqh4n19cm2nnl8aztv converged
service という単語に注目してください。起動したのはコンテナそのものではなく、「app はレプリカ2で動いているべき」という宣言を維持する係です。converged(収束した)という表現も宣言的な世界観の言葉で、「現実が宣言に一致した」と報告しています。状態を見ます。
docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
ucds5qvnqh4n app replicated 2/2 traefik/whoami:v1.10
REPLICAS 2/2 は「宣言2に対して現実2」という意味です。この分数の読み方を覚えると、障害時に一目で状況がわかります。
障害を起こして、自己修復を見る
いよいよ本題です。動いているコンテナを1つ、わざと殺します。まず実体のコンテナ名を調べます。
docker ps --filter name=app --format '{{.Names}}'
app.1.pyr1d5jjjoa4keqphk8cz6suf
app.2.sb9vduqvbteggotsqbxewyjwe
末尾のIDは環境ごとに違うので、自分の環境の表示に読み替えてください。1つ目を強制削除します。障害でプロセスが突然死した状況の再現です。
docker rm -f app.1.pyr1d5jjjoa4keqphk8cz6suf
数秒待ってから、サービスの状態を見ます。
docker service ps app --format 'table {{.Name}}\t{{.CurrentState}}\t{{.Error}}'
NAME CURRENT STATE ERROR
app.1 Running 4 seconds ago
\_ app.1 Failed 9 seconds ago "task: non-zero exit (137)"
app.2 Running 21 seconds ago
この3行をじっくり読んでください。まず履歴の行(\_ 付き)に、殺された古い app.1 が Failed として記録されています。exit 137 は 128 + 9、つまりシグナル9(SIGKILL)で強制終了された印です(#20 でプロセスとシグナルに触れました)。そしてその上に、新しい app.1 が Running 4 seconds ago で立っています。
私たちは削除しただけで、復旧のコマンドは一切打っていません。調整ループが「宣言は2、現実は1」というズレを検知し、勝手に埋めたのです。深夜3時の異常終了に人間が起きる必要がない理由がこれです。
スケールも宣言の書き換え
アクセスが増えた想定で、4台にします。打つのは「宣言の数字を変える」コマンドだけです。
docker service scale app=4
verify: Service app converged
docker service ps app --filter desired-state=running --format 'table {{.Name}}\t{{.CurrentState}}'
NAME CURRENT STATE
app.1 Running 12 seconds ago
app.2 Running 30 seconds ago
app.3 Running 5 seconds ago
app.4 Running 5 seconds ago
app.3 と app.4 が足されました。#32 で学んだとおり、これが成立するのはアプリがステートレスだからです。どの台に当たっても同じ応答なので、ただ数を増やせばいい。ステートレス設計とオーケストレーションは、セットで初めて力を発揮します。
ローリングアップデート
最後にデプロイです。設定を変えて(新バージョンの配布に相当します)、更新をかけます。
docker service update --env-add VERSION=2 app
verify: Service app converged
docker service ps app --format 'table {{.Name}}\t{{.CurrentState}}'
NAME CURRENT STATE
app.1 Running 5 seconds ago
\_ app.1 Shutdown 5 seconds ago
\_ app.1 Failed 34 seconds ago
app.2 Running 8 seconds ago
\_ app.2 Shutdown 9 seconds ago
app.3 Running 10 seconds ago
\_ app.3 Shutdown 10 seconds ago
app.4 Running 7 seconds ago
\_ app.4 Shutdown 7 seconds ago
全タスクの旧版が Shutdown になり、新版が Running に置き換わっています。ここで Running の時刻を見比べてください。10秒前、8秒前、7秒前、5秒前と、1台ずつ順番に入れ替わっています。全部を同時に落とすと一瞬サービスが消えるので、オーケストレーターは新しいのを立てては古いのを畳む、を1台ずつ繰り返します。これがローリングアップデートで、無停止デプロイの正体です。
確認が終わったら片付けます。
docker service rm app
docker swarm leave --force
🔴 ECSの部品を対応付ける
いま観察した仕組みの AWS マネージド版が ECS(Elastic Container Service)です。登場する部品は4つで、すべて今日見たものに対応します。
| ECS の部品 | 役割 | いま見たものでは |
|---|---|---|
| タスク定義 | 何をどう動かすかの設計図。イメージ、CPU・メモリ、環境変数 |
service create に渡した内容 |
| タスク | 設計図から起動された実行中のコンテナ(の組) | app.1、app.2 の1つ1つ |
| サービス | desired count を宣言し、調整ループで維持する係 |
app サービスそのもの |
| クラスター | タスクを動かす場所のまとまり | Swarm を有効にしたマシン |
タスク定義が「レシピ」、タスクが「レシピから作った料理」、サービスが「常に2皿並んでいるように作り続ける係」という関係です。落ちたタスクをサービスが立て直す、desired count の変更でスケールする、ローリングで置き換える。挙動は今日観察したとおりです。
そして配置は前回の敷地の上に乗ります。タスクはプライベートサブネットに置き、パブリックサブネットのロードバランサー(ALB)がリクエストを受けてタスクに振り分けます。ALB はサービスと連携していて、タスクが増減すると振り分け先も自動で追随します。セキュリティグループは #34 の設計どおり、タスクの SG は「ALB の SG から 8080」だけを許可します。#32 のミニチュアで nginx がやっていた窓口の役を、本番では ALB が担う、という対応です。
🟡 Fargate。ホストマシンの面倒まで見たくない
ここまで話を1つ省略していました。タスクは結局、どこかの物理的なマシンの上で動きます。そのマシンは誰が用意するのか、という問題です。
ECS には2つの動かし方(起動タイプ)があります。EC2 起動タイプは、タスクを載せるための仮想マシン(EC2 インスタンス)を自分で用意する方式です。マシンの OS 更新、セキュリティパッチ、マシン自体のスケールと、コンテナ以前の面倒がすべて自分持ちになります。せっかく店長(サービス)を雇ったのに、店舗の建物の管理は自分でやっている状態です。
Fargate は、そのホストマシンの存在ごと AWS に任せる方式です。タスク定義に「CPU 0.25、メモリ 512MB」と書けば、AWS がどこかのマシンでタスクを動かします。どのマシンかは見えませんし、見る必要もありません。マシンの管理作業が消え、タスクが使ったぶんだけの課金になります。いわゆるサーバーレスの一形態です。
| Fargate | EC2 起動タイプ | |
|---|---|---|
| ホストの管理 | 不要(AWSが持つ) | 自分(パッチ、スケール、監視) |
| 課金 | タスクのCPU・メモリ×時間 | インスタンスの稼働時間 |
| 向くケース | ほとんどのWebアプリ | GPU が要る、常時高負荷で原価を詰めたい |
判断基準は単純で、迷ったら Fargate です。EC2 起動タイプを選ぶのは、GPU のような Fargate が対応しない要件があるか、負荷が常時高く張り付いていてインスタンスを確保して詰めたほうが安いと計算で示せるときだけです。ホスト管理の人件費は請求書に載らないので見落とされがちですが、確実に払っています。
🟡 デプロイの全体像
#22 のマルチステージビルドで作ったイメージが本番に届くまでを、部品でつなぎます。
- イメージをビルドし、ECR(AWS のコンテナレジストリ。ビルドしたイメージの置き場です)に push する
- 新しいイメージを指すタスク定義の新リビジョンを登録する
- サービスを新リビジョンに更新する
3 を実行すると、今日 Swarm で見たローリングアップデートが始まります。サービスは新しいタスクを立て、ロードバランサーのヘルスチェック(/healthz のような監視用エンドポイントへの応答確認)が通ってから、古いタスクを畳みます。デプロイした新版が実は起動に失敗する場合、ヘルスチェックが通らないので古い版が生き残ります。ロールバックも「前のリビジョンに戻す」だけで、同じ仕組みで転がります。
実務での判断基準
🔴 コンテナはペットではなく家畜として扱う
インフラ運用の有名な言い回しに「ペットと家畜(Pets vs Cattle)」があります。ペットは名前を付けて世話をし、病気になったら治します。家畜は番号で管理し、問題が出たら群れから外して入れ替えます。オーケストレーションの世界では、コンテナは家畜です。
具体的には、動いているタスクの中に入って設定を直す、ファイルを置く、といった「手当て」をしません。調整ループがタスクを入れ替えた瞬間、手当ての内容は消えます(今日の実験で見たとおり、タスクは断りなく作り直されます)。直したいことがあれば、イメージかタスク定義を直して置き換える。ログはタスクの中に残さず外部に送る(#38 で扱います)。状態を持たせない(#32 のステートレス原則)。すべて「いつ入れ替わっても困らないか」という同じ問いから導けます。
🔴 desired count は2以上、AZは2つ以上に散らす
desired count 1 の構成は、ローリングアップデートのたびに一瞬タスクが1つだけになり、そのタスクの障害がそのまま全断になります。本番は最低2、そして #34 で作った2つの AZ のプライベートサブネットに散らして置きます。これでタスク単位の障害は調整ループが、データセンター単位の障害は AZ 分散が受け止めます。どちらも今日と前回の内容をつなげるだけで、追加の道具は要りません。
🟡 オーケストレーターに任せるために、アプリ側が守る契約
自己修復もローリングも、アプリが次の契約を守っていることが前提です。1つ、SIGTERM を受けたら処理中のリクエストを終えて自分で終了する(グレースフル・シャットダウン。畳まれるときにまず穏便な停止要求が来ます)。2つ、ヘルスチェック用のエンドポイントが「本当に応答できる状態か」を返す。起動直後のウォームアップ中に OK を返すと、準備前のタスクにリクエストが流れます。オーケストレーターは万能の店長ではなく、報告が正確なスタッフがいて初めて機能します。
⚪ その先にあるもの
コンテナオーケストレーターの最大手は Kubernetes で、AWS でのマネージド版が EKS です。部品の名前は変わりますが(サービスは Deployment、タスクは Pod に相当)、中心が調整ループであることは変わりません。また、CPU使用率などに応じて desired count 自体を自動で増減させるオートスケーリングという仕組みもあります。宣言する数字を機械に決めさせる、という一段上の自動化です。今は名前だけで十分です。
確認テスト
知識問題
Q1. 「docker run を2回実行して2台動かす」のと「サービスに desired count 2 を宣言する」のは、起動直後の見た目は同じです。決定的に違うのはどこか、障害時の挙動を対比して説明してください。
Q2. 調整ループとは何ですか。今回の実験で「コンテナを強制削除したのに数秒後に復旧していた」のはなぜか、宣言・現実という言葉を使って説明してください。
Q3. ECS のタスク定義・タスク・サービス・クラスターの関係を、料理か店番か、何か身近なものに喩えて説明してください。
Q4. Fargate と EC2 起動タイプの違いは何ですか。「迷ったら Fargate」と言える理由と、EC2 起動タイプをあえて選ぶ場面を1つ挙げてください。
実技問題
Q5. #34 で設計した VPC(2つの AZ に パブリック・プライベートサブネットが1つずつ)の上に、Webアプリを ECS / Fargate で配置します。①ALB とタスクをどのサブネットに置くか、②desired count をいくつにするか、③タスクの SG のインバウンドルール、の3点を理由付きで設計してください。アプリは 8080 で待ち受けるものとします。
確認テスト 解答・解説
Q1
docker run は命令なので、実行が終わった時点で役目を終えています。その後コンテナが1つ落ちても、気づいて再起動する主体がどこにもいないため、落ちたままになります。
desired count 2 の宣言は「常に2台であるべき」という状態の登録なので、サービスの調整ループが現実を監視し続けます。1台落ちて宣言とズレた瞬間、それを検知して新しいタスクを起動し、2台に戻します。「起動して終わり」か「維持し続ける」か、という違いです。
Q2
調整ループは、登録された望ましい状態(宣言)と現実の状態を絶えず見比べ、ズレがあれば現実の側を宣言に近づける、オーケストレーターの中心的な仕組みです。
実験では「レプリカ2」という宣言が登録されていました。コンテナを強制削除すると現実が1になり、宣言との間にズレが生まれます。調整ループがこのズレを検知し、新しいタスクを起動して現実を2に戻しました。人間が復旧コマンドを打っていないのに直ったのは、復旧という操作をしたのではなく、宣言との差分が自動で埋められたからです。
Q3
一例です。タスク定義はレシピ(イメージ、CPU・メモリ、環境変数という材料と分量の指定)、タスクはレシピから作られた1皿の料理、サービスは「ショーケースに常に2皿並んでいるように」作り続ける調理係、クラスターは厨房です。
喩えは自分の言葉で構いません。大事なのは、設計図(定義)と実体(タスク)が分かれていること、サービスが個々のタスクではなく「あるべき数」を管理していることの2点が入っていることです。
Q4
違いは、タスクを載せるホストマシンを誰が管理するかです。EC2 起動タイプでは自分が EC2 インスタンスを用意し、OS 更新やパッチ、マシン自体のスケールに責任を持ちます。Fargate ではホストの存在が隠され、タスク定義に書いた CPU・メモリのぶんだけ AWS が実行環境を用意します。
「迷ったら Fargate」の理由は、ホスト管理という作業がまるごと消え、その人件費と事故リスク(パッチ漏れなど)を払わなくて済むからです。EC2 起動タイプを選ぶのは、GPU のように Fargate が対応しない要件があるときや、負荷が常時高くインスタンス単位で確保したほうが安いと計算で示せるときです。
Q5
模範解答です。
- ① ALB は2つの AZ のパブリックサブネットに、タスクは2つの AZ のプライベートサブネットに置く。タスクを外部から直接触れなくし、窓口を ALB に一本化するため
- ② desired count は 2 以上とし、2つの AZ に分散させる。1 だとデプロイや障害のたびに全断の瞬間ができる。AZ 分散により、データセンター単位の障害でも片側で継続できる
- ③ タスクの SG は「送信元 ALB の SG、ポート 8080」の1行だけ許可する。IP ではなく SG 参照で書くことで、タスクやALBの増減にルールが追随する
ポイント
- #34 の「外に出すのは説明できるものだけ」を適用すると、パブリックに置くのは ALB だけになる
- desired count とヘルスチェックの設計は、調整ループに「正確な報告」を渡すためのもの。オーケストレーションの信頼性は宣言の質で決まる
- この構成図は #32 の3層そのもの。ミニチュアで組んだ構造が、部品を替えてそのまま本番になっている
まとめ
- compose は「動かす」道具で、「動かし続ける」には自己修復・スケール・無停止デプロイが要る。それを担うのがオーケストレーター
- オーケストレーターへの指示は命令ではなく宣言。「あるべき状態」を登録すると、調整ループが現実とのズレを検知して埋め続ける。強制削除したコンテナが勝手に復旧するのはこのため
- ECS の部品は、タスク定義(設計図)、タスク(実体)、サービス(desired count を維持する係)、クラスター(置き場)。タスクはプライベートサブネットに置き、ALB が窓口に立つ
- Fargate はホストマシンの管理ごと AWS に任せる起動タイプ。迷ったら Fargate、EC2 起動タイプは理由を説明できるときだけ
- コンテナは家畜として扱う。タスクの中を手で直さず、イメージと定義を直して置き換える。グレースフル・シャットダウンと正直なヘルスチェックがアプリ側の契約
次回は #36 AWS③ Aurora / RDS です。アプリは載りましたが、データベースがまだ手元のコンテナのままです。3層の「金庫」をマネージドサービスに任せるとはどういうことか。バックアップ、レプリカ、フェイルオーバーという、DB を動かし続けるための仕組みを見ていきます。