今日学んだこと:Webリクエストの旅とセキュリティグループ

AWS上でRailsアプリケーションを動かすとき、ブラウザからのリクエストは一気にデータベースへ届くわけではない。

実際には、ALB、ECSタスク、Nginx、Rails、RDSといった複数の登場人物がいて、それぞれが役割を分担しながらリクエストをバケツリレーしている。


リクエストの旅:ブラウザからDBまで

まずは、ユーザーがブラウザでアクセスしてから、RailsがDBに問い合わせるまでの全体像を見てみる。

リクエストの旅:ブラウザから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アドレスを知っていたとしても、次の条件が揃わなければ接続できない。

  1. ネットワーク的に到達できること
  2. セキュリティグループで許可されていること
  3. DBの認証情報が正しいこと

このうち最初の「到達できること」を潰しておくのが強い。

外から直接届かない場所に置き、さらに db-sgecs-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