AWS上でRailsアプリケーションを動かすとき、ブラウザからのリクエストは一気にデータベースへ届くわけではない。
実際には、ALB、ECSタスク、Nginx、Rails、RDSといった複数の登場人物がいて、それぞれが役割を分担しながらリクエストをバケツリレーしている。
リクエストの旅:ブラウザからDBまで
まずは、ユーザーがブラウザでアクセスしてから、RailsがDBに問い合わせるまでの全体像を見てみる。
流れを文章にすると、次のようになる。
ブラウザ
↓ HTTPS(443番ポート)
ALB
↓ ターゲットグループ経由
ECSタスク
├── Nginx
│ ↓ リバースプロキシ
└── Rails
↓ ActiveRecord経由でSQL発行
RDS(MySQL)
大事なのは、1つのレイヤーだけで全部を処理しているわけではないということ。
ALBは外から来たリクエストを受ける入口で、ECSタスク群のどこへ流すかを決める。ECSタスクの中ではNginxがリクエストを受け、静的ファイルを返したりRailsへ転送したりする。Railsはビジネスロジックを処理し、必要に応じてActiveRecord経由でRDSへSQLを発行する。
ECSタスクは「1コンテナ」ではない
今回の図では、ECSタスクの中にNginxとRailsを描いている。
ここで勘違いしやすいのが、1タスク = 1コンテナとは限らないという点だ。
実際の現場では、1つのECSタスクの中に次のような複数コンテナが同居することがある。
ECSタスク
├── Nginxコンテナ
├── Railsコンテナ
├── Fluent Bitコンテナ(ログ収集)
└── Datadog Agentコンテナ(監視)
タスクは「一緒に動かすコンテナのまとまり」と考えるとわかりやすい。
NginxとRailsのように、同じライフサイクルで起動・停止してほしいものを1つのタスクにまとめるイメージだ。
ALBとNginxの役割の違い
ALBとNginxはどちらも「リクエストを受けて、どこかへ渡す」という点では似ている。
ただし、見ている範囲が違う。
| ALB | Nginx | |
|---|---|---|
| いる場所 | ECSタスク群の前 | ECSタスクの中 |
| 主な役割 | 複数タスクのどれへ流すかを決める | タスク内でRailsへ転送する、静的ファイルを返す |
| 判断材料 | ホスト名、パス、ターゲットグループ | URLパス、ヘッダー、アプリへの転送設定 |
| 比喩 | 建物全体の受付 | 部署内の案内係 |
つまり、ALBはタスク間の振り分け、Nginxはタスク内の振り分けを担当している。
この階層の違いを分けて考えると、「ALBがあるのに、なぜNginxも必要なのか」が理解しやすくなる。
ターゲットグループは「宛先のラベル」
ALBがECSへリクエストを流すとき、直接「このコンテナ」と固定で指定しているわけではない。
間に ターゲットグループ がある。
ターゲットグループは、同じ役割を持つコンテナ群をまとめるラベルのようなものだ。
ALB
↓ buyer-tg というターゲットグループへ流す
ECS Task 1
ECS Task 2
ECS Task 3
デプロイで新しいタスクが起動すると、そのタスクはターゲットグループに登録される。ALBはターゲットグループに入っている健康なタスクへリクエストを流す。
これにより、新しいコンテナへ少しずつ切り替えたり、古いコンテナを外したりできる。結果として、デプロイ中でもサービスを止めずに切り替えやすくなる。
セキュリティグループのバケツリレー
次に、通信制御の視点で見る。
リクエストの経路がわかっても、それぞれの場所で「その通信を受け入れてよいか」を許可していなければ、通信は通らない。
セキュリティグループは、リソース単位で設定する仮想ファイアウォールだ。
今回の構成では、ざっくり次のような関所がある。
インターネット
↓ 0.0.0.0/0 から 80/443 を許可
ALB(alb-sg)
↓ alb-sg からのみ許可
ECS(ecs-sg)
↓ ecs-sg からのみ 3306 を許可
RDS(db-sg)
ポイントは、各リソースが自分の入口で「誰からなら受けるか」を決めていること。
ALBはインターネットからのHTTP/HTTPSを受ける。ECSはALBのセキュリティグループから来た通信だけを受ける。RDSはECSのセキュリティグループから来たMySQL通信だけを受ける。
このように、入口を1つずつ絞っていくことで、DBへ直接インターネットから届かない構成になる。
なぜIPではなくSGを送信元にするのか
セキュリティグループでは、送信元にIPアドレスではなく、別のセキュリティグループを指定できる。
たとえば db-sg に次のようなルールを書く。
3306番ポートを、ecs-sg を持つリソースからのみ許可
これは、「このIPからだけ許可」ではなく、ecs-sgというバッジを持っているリソースなら許可という書き方だ。
ECSのタスクは、デプロイやスケールアウトのたびにIPアドレスが変わる。もしIPで許可していたら、タスクが入れ替わるたびにセキュリティグループのルールも更新しなければならない。
そこで、IPではなくSGを指定する。
IP指定:住所が変わるたびにルールを書き換える必要がある
SG指定:同じバッジを持っていれば、住所が変わっても通せる
この考え方は、ECSのように動的に増減するリソースと相性がよい。
ターゲットグループとセキュリティグループの違い
ターゲットグループとセキュリティグループは、どちらもECSのコンテナ周辺に出てくるため混同しやすい。
ただし、目的はまったく違う。
| ターゲットグループ | セキュリティグループ | |
|---|---|---|
| 目的 | ALBがどこへ流すかを決める | リソースが誰から受けるかを決める |
| 束ねるもの | 同じ役割のターゲット群 | 通信を許可するルール |
| 見る人 | ALB | 各リソース自身 |
| 比喩 | 宛先のラベル | 入場の鍵 |
同じECSタスクに対して、次の2つが同時に関係する。
ECSタスク
├── ターゲットグループ:buyer-tg に登録される
└── セキュリティグループ:ecs-sg を持つ
ALBは「buyer-tg に登録されている健康なタスクへ送ろう」と判断する。
一方で、ECS側は「このリクエストは ecs-sg のルール上、受け入れてよいか」を判断する。
つまり、ターゲットグループは宛先選び、セキュリティグループは入場チェックだ。
DBをプライベートサブネットに置く理由
RDSは基本的にプライベートサブネットに置く。
これは、データベースを「鍵がかかっている場所」に置くだけでなく、そもそもインターネットから直接到達できない場所に置くためだ。
仮にDBのIPアドレスを知っていたとしても、次の条件が揃わなければ接続できない。
- ネットワーク的に到達できること
- セキュリティグループで許可されていること
- DBの認証情報が正しいこと
このうち最初の「到達できること」を潰しておくのが強い。
外から直接届かない場所に置き、さらに db-sg で ecs-sg からの3306番だけ許可する。これにより、DBはアプリケーションからのみ使える裏側の金庫になる。
今日のチェックポイント
自分の言葉で説明できるようにしたいポイントは次の5つ。
- ALBとNginxの違い:ALBはタスク間の振り分け、Nginxはタスク内でRailsへ転送する
- ターゲットグループの役割:ALBが流す先をまとめる宛先ラベル
- セキュリティグループの役割:リソース単位で誰からの通信を受けるか決める鍵
- SGを送信元に指定する理由:ECSタスクのIPが変わっても、SGというバッジで許可できるから
- DBをプライベートサブネットに置く理由:インターネットから直接到達できないようにするため
まとめ
Webリクエストは、ブラウザからALB、ECSタスク、Nginx、Rails、RDSへと順番に渡っていく。
その流れの中で、ターゲットグループは「どこへ送るか」を決めるためのラベルであり、セキュリティグループは「誰から受けるか」を決めるための鍵になる。
この2つを分けて理解できると、AWS構成図を見たときに、リクエストの流れと通信許可の境界がかなり読みやすくなる。
特に大事なのは、DBを外に見せないこと。ALB、ECS、RDSの各レイヤーで必要な通信だけを許可し、バケツリレーのように安全な経路を作るのが基本だ。
参考文献・リンク
https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-target-groups.html
https://docs.aws.amazon.com/AmazonECS/latest/userguide/task_definitions.html
https://docs.aws.amazon.com/vpc/latest/userguide/security-group-rules.html
https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_VPC.WorkingWithRDSInstanceinaVPC.html