ECS間通信にService Connectを使わず、Service Discoveryを選んだ理由

おそらく誰しも一度は、なんでこんな感じになってしまったんだろう？みたいなインフラ構成に出くわしたことがあるんじゃないでしょうか。

自分の場合は、2つのアプリケーションを別のVPC上のECSで動かしていて、ECS間の通信をインターネット経由で行っているという構成が放置されていました。VPCをまたいでいるため、ECSタスク同士の通信がNAT GatewayやALBを経由しており、パフォーマンス、セキュリティ、コスト、どれをとってもあまりよくない状態でした。

![スクリーンショット 2025-05-06 9.01.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/37d04d6d-8b3f-440b-9984-f7aadbef6a82.png)

## 結論：Service Connectを断念してService Discoveryで構成した

AWSのドキュメントやECSの設定画面では、"Service Connect推奨"と書かれていて、Cloud Mapを使ったService Discovery(サービス検出)構成はなんとなく古いやり方に思えたのですが、結果的にService Connectを使わず、Service Discoveryだけで構成しました。

![スクリーンショット 2025-05-05 12.54.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/afae12fd-84d4-4c80-8381-04dc54878f5b.png)

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/5170a7c7-2a75-4cae-8f1c-7c252e1db774.png" width=400>

## Blue/GreenデプロイとService Connectは共存できない
構成としては、接続元のECS(以降A)から補助的な機能をもつ接続先のECS(B)に通信する形です。AのタスクはALB経由で外部公開しており、デプロイには`CodeDeploy`によるBlue/Green方式を採用しています。
トラフィックの切り替え制御でもしものときのロールバックが簡単だったり、最小ダウンタイムでのリリースができるのでBlue/Greenデプロイを選択したのですが、ECSのサービス定義でService Connectを有効にしてデプロイを試みたところ、以下のようなエラーが返ってきてしまいました。

```
InvalidParameterException: DeploymentController#type CODE_DEPLOY is not supported by ECS Service Connect.
```

なんと、Service Connectは`CodeDeploy`ベースのBlue/Greenデプロイと併用できませんでした。

この時点で、自分の構成ではService Connectを採用できないと判断しました。

## そもそものService Connectのよさ

Service Connectを有効にすると、Envoyというサイドカーのプロキシを各タスクに自動で入れてくれます。

- 内部DNSの抽象化(clientAliases)
- トラフィックの可視化(Service Graph)
- mTLSやセキュリティポリシー強化

など、サービス間通信をモダンにしてくれます。

でも、Envoyが裏で動いているので、その分のリソース(メモリ、CPU)が必要になるというデメリットもあります。例えば、FargateだとvCPUとメモリサイズで料金が変わってくるので、リソース増でコストも上がってしまいます。

今回は接続先(B)が補助的なマイクロサービスで
、Service Graphでの可視化までは必要なしと考え、Service Discoveryだけでシンプルにいこうと判断しました。

## Service ConnectとService Discoveryの簡単な比較

| 項目           | Service Connect | Service Discovery                |
| ------------ | ------------------------ | ----------------------------------- |
| サイドカー        | 必要(Envoy)                | 不要                                |
| 通信パス         | ECS A → Envoy A → Envoy B → ECS B         | ECS A → ECS B                          |
| CPU・メモリ負荷    | 増える(サイドカーあり)                      | 軽い(直接通信)                                |
| Blue/Green対応 | 非対応                     | 対応可能          |
| DNS名の簡潔さ     | clientAliasesで短縮可能      | `service.service.local`など長め |

より詳細に知りたい方は、以下の記事がとても参考になります。

https://dev.to/aws-builders/comparing-5-methods-of-ecs-interservice-communication-including-vpc-lattice-51f0

## Service Discovery(Cloud Map)の導入手順
今回採用したService Discovery構成を組む手順を紹介します。Cloud Mapを使って、接続先のECSサービス(B)を名前解決できるようにします。

### 1. 名前空間(Cloud Map)の作成
まずはCloud Mapの名前空間を作成します。名前空間はサービスのドメインのようなもので、たとえば `service.local` のように使います。

![スクリーンショット 2025-05-05 14.51.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/2d968ea8-d353-435d-a822-afecb6ef8b8e.png)

Terraformで作成する場合は以下のようになります。

```tf
resource "aws_service_discovery_private_dns_namespace" "service_local" {
  name        = "service.local"
  description = "Private DNS namespace"
  vpc         = <vpc_id>
}
```

### 2. サービスの作成
この名前空間に対して、ECSサービスB用の「サービス」を作成します。

![スクリーンショット 2025-05-05 14.59.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/ec786898-6ca4-447e-916c-56e6f7dc7903.png)

```tf
resource "aws_service_discovery_service" "service_b" {
  name         = "service-b"
  namespace_id = aws_service_discovery_private_dns_namespace.service_local.id

  dns_config {
    namespace_id = aws_service_discovery_private_dns_namespace.service_local.id
    dns_records {
      type = "A"
      ttl  = 30
    }
    routing_policy = "MULTIVALUE"
  }

  health_check_custom_config {
    failure_threshold = 3
  }
}
```

### 3. セキュリティグループの作成

#### 接続元(ECS A)
通信元のECSタスクが、ECS Bにアクセスできるように双方向のルールを設定します。

```tf
resource "aws_security_group" "service_a_ecs_sg" {
  name        = "service-a-ecs-sg"
  description = "Security group for ECS A (caller)"
  vpc_id      = <vpc-id>
}

resource "aws_security_group_rule" "service_a_to_b_egress" {
  type                     = "egress"
  from_port                = 8080
  to_port                  = 8080
  protocol                 = "tcp"
  security_group_id        = aws_security_group.service_a_ecs_sg.id
  source_security_group_id = aws_security_group.service_b_ecs_sg.id
}
```

#### 接続元(ECS B)

```tf
resource "aws_security_group" "service_b_ecs_sg" {
  name        = "service-b-ecs-sg"
  description = "Security group for ECS B (callee)"
  vpc_id      = <vpc-id>
}

resource "aws_security_group_rule" "service_b_from_a_ingress" {
  type                     = "ingress"
  from_port                = 8080
  to_port                  = 8080
  protocol                 = "tcp"
  security_group_id        = aws_security_group.service_b_ecs_sg.id
  source_security_group_id = aws_security_group.service_a_ecs_sg.id
}

# 必要であれば外部通信も許可
resource "aws_security_group_rule" "service_b_egress_all" {
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  security_group_id = aws_security_group.service_b_ecs_sg.id
  cidr_blocks       = ["0.0.0.0/0"]
}
```

### 4. アプリケーションの接続先を変更
ECS(A)のアプリケーションから、ECS(B)を`http://service-b.service.local:8080`のようにアクセスできるようになります。ここで`:8080`のようにポート番号を明示する必要がある点に注意が必要です。


### 5. 接続先(ECS B)のサービス/タスク定義を更新

#### サービス定義
接続先(ECS B)のサービス定義で、Service Discoveryに作成したCloud Mapの名前空間とサービスを登録します。

![スクリーンショット 2025-05-05 17.17.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/e7bc8ec8-20bd-4712-b050-fc085c942ae4.png)

ecspressoでサービス定義を管理している場合は以下を追加します。

```json
"serviceRegistries": [
    {
        "registryArn": "arn:aws:servicediscovery:ap-northeast-1:*********:service/srv-*********"
    }
]
```

#### タスク定義
接続先ECS Bの前段にALBがなくなるので、ヘルスチェックをECS自身でできるようにタスク定義へ`healthCheck`を追加します。以下はコンテナポート`8080`を使用している例です。

```json
  "containerDefinitions": [
    {
      (略)
      "portMappings": [
        {
          "containerPort": 8080,
          "protocol": "tcp",
        }
      ],
      "healthCheck": {
        "command": [
          "CMD-SHELL",
          "curl -f http://localhost:8080/health || exit 1"
        ],
        "interval": 30,
        "timeout": 5,
        "retries": 3,
        "startPeriod": 10
      }
    }
```

ヘルスチェックで`curl`を叩くので、コンテナイメージに`curl`が含まれていない場合は、Dockerfileに`curl`をインストールする記述(ex. `RUN apk add --no-cache curl`)を追加します。

## Service Connectの導入手順
今回はBlue/Greenデプロイとの併用ができないため採用しませんでしたが、参考までにService Connectの導入手順も紹介します。

### 1. 名前空間(Cloud Map)の作成
Service Discoveryと同様、`service.local`などのプライベートDNS名前空間をCloud Map上に作成します。Terraformも同様です。

:::note info
補足：Service Connectでは「サービス登録」を手動で行う必要はありません。ECSのデプロイ時に自動でCloud Mapに登録されます。
:::

### 2. セキュリティグループの作成
Service Connectも内部ではEnvoyを経由したTCP通信のため、基本的には通常のECS間通信と同様にポート指定のセキュリティルールが必要です。

#### 接続元(ECS A)
通信元のECSタスクが、ECS Bにアクセスできるように双方向のルールを設定します。

```tf
resource "aws_security_group" "service_a_ecs_sg" {
  name        = "service-a-ecs-sg"
  description = "Security group for ECS A (Service Connect caller)"
  vpc_id      = <vpc-id>
}

resource "aws_security_group_rule" "service_a_to_b_egress" {
  type                     = "egress"
  from_port                = 8080
  to_port                  = 8080
  protocol                 = "tcp"
  security_group_id        = aws_security_group.service_a_ecs_sg.id
  source_security_group_id = aws_security_group.service_b_ecs_sg.id
}
```

#### 接続元(ECS B)

```tf
resource "aws_security_group" "service_b_ecs_sg" {
  name        = "service-b-ecs-sg"
  description = "Security group for ECS B (Service Connect callee)"
  vpc_id      = <vpc-id>
}

resource "aws_security_group_rule" "service_b_from_a_ingress" {
  type                     = "ingress"
  from_port                = 8080
  to_port                  = 8080
  protocol                 = "tcp"
  security_group_id        = aws_security_group.service_b_ecs_sg.id
  source_security_group_id = aws_security_group.service_a_ecs_sg.id
}

# 必要であれば外部通信も許可
resource "aws_security_group_rule" "service_b_egress_all" {
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  security_group_id = aws_security_group.service_b_ecs_sg.id
  cidr_blocks       = ["0.0.0.0/0"]
}
```

### 3. サービス定義の更新(ECS A)
コンソール画面からService Connectの設定を行います。

![スクリーンショット 2025-05-05 17.56.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/e36e4cd4-9ac9-450c-b3ae-23134fb142d7.png)

ecspressoでサービス定義を管理している場合は、`serviceConnectConfiguration`を追加します。

```json
  "serviceConnectConfiguration": {
    "enabled": true,
    "namespace": "service.local",
    "services": [
      {
        "portName": "http",
        "discoveryName": "service",
        "clientAliases": [
          {
            "port": 80,
            "dnsName": "service"
          }
        ]
      }
    ]
  },
```

:::note warn
注意：CodeDeploy(Blue/Greenデプロイ)とService Connectは併用できません。
`DeploymentController#type CODE_DEPLOY is not supported`というエラーになります。
:::

#### アプリケーション内の向き先変更

アプリケーション側では、以下のように `clientAliases.dnsName`にあわせて通信先を指定します。

```
http://service
```

:::note info
ポート番号の指定は不要です。
:::

### 4. サービス定義の更新(ECS B)
ECS B側も同様にService Connect設定を行います。

![スクリーンショット 2025-05-05 17.17.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/e7bc8ec8-20bd-4712-b050-fc085c942ae4.png)

ecspressoの記述も同様です。

```json
  "serviceConnectConfiguration": {
    "enabled": true,
    "namespace": "service.local",
    "services": [
      {
        "portName": "http",
        "discoveryName": "service",
        "clientAliases": [
          {
            "port": 80,
            "dnsName": "service"
          }
        ]
      }
    ]
  },
```

### 5. タスク定義の更新(ECS B)
ALB を使わない構成になるため、タスク自身がヘルスチェックを返すように構成します。

```json
  "containerDefinitions": [
    {
      (略)
      "portMappings": [
        {
          "containerPort": 8080,
          "protocol": "tcp",
        }
      ],
      "healthCheck": {
        "command": [
          "CMD-SHELL",
          "curl -f http://localhost:8080/health || exit 1"
        ],
        "interval": 30,
        "timeout": 5,
        "retries": 3,
        "startPeriod": 10
      }
    }
```

ヘルスチェックで`curl`を叩くので、コンテナイメージに`curl`が含まれていない場合は、Dockerfileに`curl`をインストールする記述(ex. `RUN apk add --no-cache curl`)を追加します。

## まとめ
Service ConnectはBlue/Greenデプロイとの相性に制限があるものの、DNS名のシンプルさや将来的な拡張性、トラフィック可視化など多くの利点があります。

今回のように小規模でBlue/Greenが必要なケースではService Discoveryが適していますが、中〜大規模なマイクロサービス環境やセキュリティ要件が高い環境では、Service Connectを選ぶ価値があると思います。