AWS Glue(Spark) で自己参照ルールが必須な理由を理解する

みなさま、こんにちは。

後輩に「GoF のデザインパターンは 48 の殺人技みたいなものだよ。」と説明したら、そもそもキン肉マンを知らずジェネレーションギャップを感じてしまった今日この頃です。

最近、データ周りの作業で **AWS Glue** を使う機会が多いのですが、その中で特に **VPC 内のリソースへアクセス**するときに登場する **自己参照ルール（self-referencing security group）** の「役割」と「なぜ必要なのか」がふわふわしていたので、備忘としてまとめようと思います。

## 目的
以下の理解を目的とします。
- **自己参照ルールとは何か**
- **Apache Sparkのアーキテクチャを考慮し、Glue（Sparkモード）でなぜ自己参照ルールが必須になるか**

また最後に、Glue ジョブが作成する ENI や、IP枯渇リスクへの注意点についても記載します。

## 自己参照ルールとは

### 概要

あらためて、AWS のセキュリティグループ（SG)では、インバウンド/アウトバウンドルールのソースは以下を指定できます：[^1]
1. **単一のIPアドレス、もしくは CIDR表記のIPアドレスの範囲**
    - 例: `203.0.113.1/32`
2. **プレフィックスリストのID**
    - 例: `pl-1234abc1234abc123`
3. **セキュリティグループの ID**
    - 例: `sg-1234567890abcdef0`

このうち、「3. **セキュリティグループの ID**」において、ソースとして「**自分自身のセキュリティグループのID**」を指定することが、いわゆる「自己参照ルール（`self-referencing security group`）」です。

自身がソースとなることです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/759541/e1258bb5-b22e-4cd7-a90f-8ec316980e13.png" width=500 />


### 使いどころ

クラスタや分散システムのように、**ノード同士が双方向通信**をするケースで必要となります。
**「クライアント→サーバー」の一方向通信** あれば、自己参照ルールは必ずしも必要ありません。

少しずつ見ていきます。

#### 自己参照ルールが必ずしも必要ではない場合

最初に自己参照ルールを知った時、単一責任の原則の悪魔に脳みそを支配された私は混乱しました。
例えばALB→EC2のを構築するときは、大まかに下記のようなSGを設定するはずです。

| セキュリティグループ名 | インバウンドルール | アウトバウンドルール |
|------------------------|-------------------|---------------------|
| **alb-sg** | TCP 443 (HTTPS) from 0.0.0.0/0 (インターネットから) | All Traffic to 0.0.0.0/0 (すべての送信トラフィック許可) |
| **ec2-sg** | TCP 80 (HTTP) from alb-sg (ALBからのみ) | All Traffic to 0.0.0.0/0 (すべての送信トラフィック許可) |

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/759541/e61c6f5c-20ca-4a7a-87df-e55861f148f2.png)

仮に自己参照ルールを用いて書き直すと下記のようになります。

| セキュリティグループ名 | インバウンドルール | アウトバウンドルール |
|------------------------|-------------------|---------------------|
| self-referencing-sg | TCP 80 from self-referencing-sg (自分自身)<br>TCP 443 from 0.0.0.0/0 (インターネットから) | All Traffic to 0.0.0.0/0 (すべての送信トラフィック許可) |

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/759541/6e1a6625-06d5-4397-98fb-6754f4467c19.png)

先ほどと比べるとEC2への443ポートへアクセス可能となっています。
これはSGを共有した副作用であり、仮にEC2が443を解放していないとしても、Firewallレベルでの保護が不十分になります。

さらに、EC2へSSHするため22ポートを開放する場合、ALBにも22ポートが開放されることになります。ALBにSSH接続する必要はないため、これは不要なセキュリティリスクを生み出します。

では、どのようなときに自己参照ルールが必要になるのでしょうか。

#### 自己参照ルールが必要になる場合

要は**同じSGを付与されたインスタンス同士が、お互いに任意のポートで自由に通信できるようにしたいとき**に必要となります。

代表的なのが分散システムです。

少し抽象化して1つのマスターノードと、複数のチャイルドノードが存在していると仮定しましょう。

マスターノードは、チャイルドノードのコンピューティングリソースの空き具合を見て、タスクを割り当てるとします。（ここで言うタスクは単純なプロセスの実行）
またそれぞれのチャイルドノードは複数のタスクを処理可能であり、それぞれが異なるポートでマスターとやりとりします。
さらに、チャイルドノード同士も何らかの通信を行う可能性も考慮すると下記のような概念図になると思います。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/759541/ed32b182-d108-4c26-922f-0e46cd0501a9.png" width=500 />


このような構成の場合に自己参照ルールが活躍します。

例えばマスターノードと、各チャイルドノードに対応するEC2に自己参照ルールを許可すると下記の利点が得られます。

1. **スケールアウトに対応しやすい**  
   オートスケーリングでノードが増えたり減ったりするとき、個別の SG を紐付けするのは管理が面倒です。共通の SG に自己参照ルールを設けておけば、追加されたインスタンスが自動的に同じルールでクラスタ通信を行えます。

2. **任意ポート通信が簡単に許可できる**  
   分散システムでは、通信ポートがエフェメラルだったり、複数ポートにまたがったりします。自己参照ルール (All TCP) を設定しておけば、同一SG間での制限を一括でクリアできるため、いちいちポートを固定しなくても済みます。

## Apache Sparkについて

### 概要

Apache Spark(Spark)とは分散システムで、統一された計算エンジンとビッグデータ処理のためのライブラリセットです。Hadoopと異なり、ストレージと計算が分離されており、様々なデータソースに直接アクセスできます。

詳しくは[^2] をご参照ください。

主要なコンポーネントとして**Driver** と **Executor**が存在しています。まずはそれらについて簡単に解説します。

### Driver と Executor

#### Driver 

クラスタ全体を管理し、タスクを Executor に割り振り、結果を集約する役割を果たします。

`Driver` はクラスター内の1つのノードで実行され、Sparkアプリケーションの中心として機能します。アプリケーションのライフサイクル全体を通じて、すべての関連情報を維持する不可欠なコンポーネントです。[^3]

#### Executor
`Driver` によって空いているノードに割り当てられる実行コンポーネントです。主な役割は2つあります。[^3]

1. **Driverから割り当てられたコードの実行**：実際の計算処理を担当します。
2. **計算状態のDriverへの報告**：処理の進捗や結果、エラーなどの情報をDriverノードに送り返します。
 
`Executor` は、Sparkの高速処理を実現するために、他の`Executor` 同士と相互通信する機能も備わっています：

- **キャッシュの共有**：`Executor` はデータを内部にキャッシュし、他の `Executor` がそのキャッシュを利用することで、データソースへの重複アクセスを減らし、パフォーマンスを向上させます。これはHadoopの課題だったI/Oボトルネックを解消します。[^4]
- **シャッフル処理**：データの再分配（シャッフル）が必要な場合、`Executor` 間でデータ転送が発生します。この通信も効率的に行われるよう最適化されています。[^5]

### アーキテクチャ

[^6] を参考にSparkの分散処理アーキテクチャを図示すると下記のようになります：

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/759541/0b01ef00-cee0-445f-827d-5c154b95080a.png" width=500 />

ほうほう。

つまり、`Driver` が `Executor` を管理し、`Executor` 同士は相互通信を行うことで処理の高速化を行い、処理が終われば `Driver` に通知する。クラスタモードの場合は、これらの相互作用はネットワークを必要とする、ですか。

おや？

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/759541/53ed3a52-6cf0-48d0-8895-6ad2b04967ef.png" width=500 />

**これをVPC内で実行するなら自己参照ルール、必要じゃね？**

そうです、ネテロ会長。
Glueでは、Sparkのコンポーネント（`Driver`と`Executor`）が相互にやりとりするために自己参照ルールが必要になるんです！[^7]

## Glueのクラスタモードで発生する内部通信

Glue のジョブ（Sparkモード）を起動すると、**Driver 用のコンテナ**と**Executor 用のコンテナ**が複数立ち上がり、それぞれ**別々の ENI(Elastic Network Interface)** に紐づきます。

下記のような構成イメージです。

```
 +--------------------+   +--------------------+
 |  Executor (ENI #1) |   |  Executor (ENI #2) |  <--- Both attach the same "glue-self-ref-sg"
 +--------+-----------+   +--------+-----------+
          |                        |
          | (任意ポート)           　| (任意ポート)
          |                        |
          +---------- Driver (ENI #D) ---------+  <--- Also has "glue-self-ref-sg"

```

Spark Driver と Executor は**ランダムなポート**で通信するため、**Glue 用 SG に自己参照（All TCP）** が設定されていないと、お互いが通信できずジョブが失敗すると言うわけです。

続いてハンズオンを通して、本当に自己参照ルールが必要なのかと言う点と、作成されるENIについて調査していきます。

## ハンズオン検証

自己参照ルールの有無がどのように Glue ジョブの実行に影響するか、また Glue ジョブ実行時に作成される ENI 数を確認する目的で下記の構成を用意しました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/759541/fb2b6860-f691-44d7-baba-cca75c675068.png" width=500 />

Terraformはこちら▼

https://github.com/iisyos/glue_poc

Glue 用のセキュリティグループでは、インバウンドに `self = true` を設定し、TCP 全ポートを同一 SG 間で許可しています。


```tf
resource "aws_security_group" "glue_self_ref_sg" {
  name        = "${var.app_name}-glue-sg"
  description = "Security Group for Glue with self-reference rule"
  vpc_id      = module.vpc.vpc_id

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    description = "Self reference rule for Glue"
    from_port   = 0
    to_port     = 65535
    protocol    = "tcp"
    self        = true # <--ココ
  }
}

```

検証ポイント
1. 自己参照ルールがない場合、Glue ジョブがどのように失敗するかを確認
2. Glue ジョブに指定したワーカー数と、作成される ENI の関係を確認

### 前準備

<details><summary>リソース作成 & テストデータ投入</summary>

1. **リソースの作成**
```zsh
$ pwd
# glue_poc/terraform

$ terraform apply
```

2. **テストデータ作成**
作成した EC2 にセッションマネージャー で接続し、RDS に初期データを投入します。

```zsh
$ aws ssm start-session --target i-xxx
$ /home/ssm-user/connect_to_rds.sh

# MySQL [db]> 
```

```sql
CREATE TABLE IF NOT EXISTS `glue_demo` (
  `id` INT AUTO_INCREMENT PRIMARY KEY,
  `name` VARCHAR(50) NOT NULL
);

INSERT INTO `glue_demo` (`name`) VALUES ('Alice');
INSERT INTO `glue_demo` (`name`) VALUES ('Bob');
```

その後、Visual ETLを起動し、下記のようにぽちぽちワークフローを組んでみます。

![スクリーンショット 2025-03-02 18.14.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/759541/4b0f2cb2-da32-4ec3-add6-9bd9de769d84.png)

実行は成功し、Athenaでクエリすることができました🎉

![athena.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/759541/f79f9fab-8df4-426d-ad1f-96b9cc5e96c2.png)


</details>

### 検証

#### 1. 自己参照ルールがない場合の Glue ジョブ失敗


自己参照ルールを一旦削除してからジョブを実行し、Glue がどのようなエラーを返すか確認します。

AWS マネジメントコンソールから、Glue 用 SG の自己参照ルールを手動で削除
![スクリーンショット 2025-03-02 18.22.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/759541/17dbef34-2bd2-45e2-baea-a7f035682222.png)

その状態で、Jobを実行すると...
```
AccountId:xxxx and JobName:glue-demo and JobRunId:jr_xxx failed to execute with exception At least one security group must open all ingress ports.To limit traffic, the source security group in your inbound rule can be restricted to the same security group (Service: AWSGlueJobExecutor; Status Code: 400; Error Code: InvalidInputException; Request ID: xxx; Proxy: null)
```

それっぽい理由で失敗していますね。
Glue (Spark) では、`Driver` ⇔ `Executor` 間で任意ポート通信が必要となるため、このように**自己参照ルールがないとジョブが起動できない**ことが分かります。

#### 2. Glue ジョブのワーカー数と ENI 作成数

Glue ジョブでは「ワーカー数」を指定できます。[^8]
Spark 内部では `Driver` が1つ、残りを `Executor` として割り当てるイメージです。

例: ワーカー数 = 2
　　 Driver 1 + Executor 1 → ENI 2つ

例: ワーカー数 = 4
　 　Driver 1 + Executor 3 → ENI 4つ

実行ログにも、複数の Executor の起動メッセージが出力されます。以下は抜粋例です。

```
INFO Utils: Successfully started service 'org.apache.spark.network.netty.NettyBlockTransferService' on port 35065.
INFO Utils: Successfully started service 'org.apache.spark.network.netty.NettyBlockTransferService' on port 41557.
INFO  org.apache.spark.util.Utils [Thread-7] 60 Successfully started service 'sparkDriver' on port 46391.
```

このように ワーカー数に比例して ENI の数が増加します。

:::note warn
今回の検証では、私がめんどくさがり...ゴホンゴホン。簡単のため、アプリケーション（EC2）とGlueジョブが1つのサブネットに同居していました。多数の Glue ジョブを起動すると、ENI が大量に生成され **Private IP** を消費します。

そのため、**同居の際はサブネットの IP 割り当てを十分確保しておきましょう。**[^9]
:::

## まとめ

- **自己参照ルール (self-referencing security group)** とは、同一SGに所属するリソース間で通信を許可する設定。
- 分散システム（例: Spark）では **Driver ⇔ Executor、Executor ⇔ Executor** 間通信が必要なため、**Glue (Spark) を VPC内で動かす**場合は自己参照ルールが必須。
- Glue ジョブはワーカー数に応じて複数の ENI を一時的に作成し、エフェメラルポートで通信する。
- IPアドレス枯渇リスクや、SGルールの設定ミスに注意しながら構成するのがポイント。


## 考察

少し端折ってしまったのですが、実際にはGlueジョブをワーカー数４で実行した際、下記のようなログが出ていました。

|id| @message |
|---| --- | 
|1| INFO Utils: Successfully started service 'org.apache.spark.network.netty.NettyBlockTransferService' on port 35065.| 
|2| INFO Utils: Successfully started service 'org.apache.spark.network.netty.NettyBlockTransferService' on port 41557.| 
|3| INFO Utils: Successfully started service 'org.apache.spark.network.netty.NettyBlockTransferService' on port 46119.| 
|4| INFO 8418 org.apache.spark.util.Utils [Thread-7] 60 Successfully started service 'org.apache.spark.network.netty.NettyBlockTransferService' on port 43013. |
|5| INFO 6421 org.apache.spark.util.Utils [Thread-7] 60 Successfully started service 'sparkDriver' on port 46391. | 

当初予想していたのは、ワーカー数4に対応して4つの関連するログが作成されることでしたが、ログには5つのサービス起動が記録されていました。
id 1〜3とid 4は同じ`NettyBlockTransferService`を起動していますが、ログの形式が異なります。id 5は`sparkDriver`の起動を示しています。

ENIは依然として4つのみ作成されていたため、おそらく `Driver`コンテナ上に`Executor`と通信するためのインターフェイス的なサービスが立ち上がったのでは、と考えます。

## 感想

> AWS Glue がコンポーネント間で通信できるようにするには、すべての TCP ポートに対して自己参照のインバウンドルールを持つセキュリティグループを指定します。[^7]

この一文を理解するためにいろいろ勉強しました。
特に[^6]の本は名著と言われているだけあり、ハンズオンも充実しており非常にわかりやすかったです。[^2]の記事もすごくわかりやすかったですし、[^4]の資料は何度読み直したかわかりません。コミュニティが広いOSSの利点をもろに享受させていただきました。

学んでみて下記の意味が理解できた気がします。

> As of the time this writing, Spark is the most actively developed open source engine for this task; making it the de facto tool for any developer or data scientist interested in big data.[^3]

似たような疑問を抱いている方々に、この記事が少しでも参考になれば幸いです。

## 謝辞

Apache Sparkを理解するにあたって、Databricks社が公開しているインターネット上のブログや記事、書籍等を広く参考にさせていただきました。
この場を借りてお礼申し上げます。


[^1]: [セキュリティグループルールの構成要素](https://docs.aws.amazon.com/ja_jp/vpc/latest/userguide/security-group-rules.html#security-group-rule-components)
[^2]: [Apache Sparkとは何か](https://qiita.com/taka_yayoi/items/31190da754106b2d284e#apache-spark%E3%81%A8%E3%81%AF)
[^3]: [Under the Hood Getting started with core architecture and basic concepts](https://www.databricks.com/resources/ebook/apache-spark-under-the-hood)
[^4]: [学んで動かす！ Sparkのキホン](https://www.scsk.jp/lib/product/oss/pdf/Spark.pdf)
[^5]: [202108 AWS Black Belt Online Seminar 猫でもわかる、AWS Glue ETLパフォーマンス・チューニング 前編](https://www.slideshare.net/slideshow/202108-aws-black-belt-online-seminar-aws-glue-etl/250037648#18)
[^6]: [Learning Spark Lightning-Fast Data Analytics](https://pages.databricks.com/202003-US-EB-Learning-Spark-2nd-Edition_Thank-you-ebook.html)
[^7]: [AWS Glue から Amazon RDS データストアに JDBC 接続するための Amazon VPC の設定](https://docs.aws.amazon.com/ja_jp/glue/latest/dg/setup-vpc-for-glue-access.html)
[^8]: [AWS Glue で Spark ジョブに関するジョブプロパティの構成](https://docs.aws.amazon.com/ja_jp/glue/latest/dg/add-job.html)
[^9]: [AWS Glue のエラー「The specified subnet does not have enough free addresses to satisfy the request」（指定されたサブネットにはリクエストを満たすのに十分な空きアドレスがありません）を解決するにはどうすればよいですか。](https://repost.aws/ja/knowledge-center/glue-specified-subnet-free-addresses)