【AWS】ECSタスク内でのコンテナ間通信

**2022/5/5 追記**
あとがきにサービス検出について試した記事のリンクを追加

# はじめに
ECSで1つのタスクとして複数コンテナを稼働させ、コンテナ間で通信したいと思った時に上手くできなかった部分と対応を記載します。
ローカル環境で構築した際にはDocker Composeを使用しており、ymlファイルで定義するサービス名を使用してコンテナ間通信をすることができましたが、ECS環境ではIPアドレスでの通信でないと上手くいかなかったため自分なりに調査してみました。

# 構成図
はじめにECSで構築した際の構成図は以下のようになります。
![diagram01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1410257/808c5e12-0853-3c2f-f194-deb8dfa8f3c9.png)

* サービスの起動タイプは「EC2」
* タスク定義でネットワークモードを「bridge」
* タスク定義で複数のコンテナを定義
    * Nginx
    * Gunicorn
* Nginxコンテナについてはポートマッピングによって動的ポートを使用

また、Nginx設定ファイルは以下のような内容となります。

```bash:Nginx設定ファイル(ネットワークモード：bridge使用時)
upstream my_app {
  server 172.17.0.3:8000;
}

server {
  listen 80;

  location /static {
    alias /public/static;
  }

  location / {
    proxy_pass http://my_app;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_set_header X-Forwarded-Proto $scheme;
  }
}
```

# ECSタスクのネットワークモード
タスク定義を作成する際にネットワークモードの値を`<default>`として設定していました。
`<default>`を選択するとDockerのデフォルトネットワークモードが設定されると記載があり、ネットワークモードとして`bridge`が使用される形となります。

`bridge`ではタスクをホストするECSコンテナインスタンス上で実行されるDockerの組み込み仮想ネットワークを使用し、ネットワークブリッジを介してECSコンテナインスタンスとコンテナの通信を行います。

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/task-networking-bridge.html

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/bestpracticesguide/networking-networkmode.html#networking-networkmode-bridge

実際にタスクを稼働させた状態でECSコンテナインスタンスにSSH接続し、`docker network inspect bridge`コマンドを実行したところ、コンテナはDockerにデフォルトで存在する`bridge`ネットワークに所属していることが確認できました。

```bash:コンテナのネットワーク所属状況確認
$ docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "3d766c975e3c09d782ecf4e5be49d260546f11e7e1f7e23dafb6bd1ab8ba4628",
        "Created": "2022-03-12T08:22:43.49860314Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "34050b8cd2a208a2e649789a6ff42bb5f9276e5ed071c68577e25cb4b5841711": {
                "Name": "ecs-ecs-django-25-gunicorn-ccb3e384c695da963100",
                "EndpointID": "6ace247bc2f3e3fd275cd827558999cd641bb94fc23fbfcf426c59f72de90da1",
                "MacAddress": "02:42:ac:11:00:03",
                "IPv4Address": "172.17.0.3/16",
                "IPv6Address": ""
            },
            "700b1e0c75298ec83ecd49581a4bd49812f7728ba320829508a51a6d63076110": {
                "Name": "ecs-ecs-django-25-nginx-ccf8e0f2a0bec2d1c101",
                "EndpointID": "8a75afbe1ebd59e3b7fcb9644e1775f5c276eed562ecbcfee53c41449d167541",
                "MacAddress": "02:42:ac:11:00:02",
                "IPv4Address": "172.17.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]

```

# bridgeネットワーク
ECSではDockerにデフォルトで存在する`bridge`が使用されましたが、Docker自体の機能としてはユーザー定義のbridgeネットワークを作成し、コンテナを接続することができます。
Dockerドキュメントを参照すると、ユーザー定義のbridgeネットワークの方がデフォルトのbridgeネットワークよりも優れているという記載があります。

>User-defined bridge networks are superior to the default bridge network.

また、デフォルトのbridgeネットワークについて以下の記載がありました。

>The default bridge network is considered a legacy detail of Docker and is not recommended for production use. Configuring it is a manual operation, and it has technical shortcomings.

技術的な欠点があるとのことでユーザー定義のbridgeネットワークとの違いが記載されています。

https://docs.docker.com/network/bridge/#differences-between-user-defined-bridges-and-the-default-bridge

違いの1つ目として以下のように記載されています。

>User-defined bridges provide automatic DNS resolution between containers.

ユーザー定義のbridgeネットワークではコンテナ間の名前解決が提供されるとのことです。
また、詳細を見てみるとデフォルトのbridgeネットワークでは`--link`オプションを使用しない限り、IPアドレスでのみ相互に通信が取れるとの記載もありました。

ECSタスクでのコンテナ間通信が名前で出来なかった原因はこのあたりにありそうです。

# Docker Composeのネットワーク
Docker Composeで使用するymlファイル内で明示的にネットワークを作成することも可能ですが、指定をしない場合には自動的にユーザー定義のbridgeネットワークが作成され、定義したコンテナがそのネットワークに所属します。

Docker Composeで作成したコンテナ同士がymlファイル内で定義したサービス名を使用して通信できていたのは、自動作成されたユーザー定義のbridgeネットワークにコンテナが所属していたことで名前による通信ができていたためであると思われます。

# awsvpcモードの使用
ではECSタスク内でのコンテナ間通信をどのようにして実現するかについてですが、今回はECSタスクのネットワークモードを`bridge`ではなく`awsvpc`を使用するように変更してみました。

`awsvpc`ではタスクそれぞれに対してENIとプライベートIPアドレスが割り当てられます。

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/task-networking-awsvpc.html

そして下記URLのドキュメントを参照すると以下のような記載があります。

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/bestpracticesguide/networking-networkmode.html#networking-networkmode-awsvpc

> さらに、同じタスクに属するコンテナが、localhost インターフェイス経由で通信できるようになります。

ネットワークモードとして`bridge`を使用していた時にはDockerのデフォルトbridgeネットワーク(172.17.0.0/16)のIPアドレスでしか同タスク内でのコンテナ間通信ができていませんでしたが、`awsvpc`では「localhost」を使用してコンテナ間通信が実現できます。

# awsvpcモード使用にあたっての変更点
ネットワークモードを`bridge`から`awsvpc`に変更するにあたっての設定変更点を以下に記載します。

* タスク定義でネットワークモードを`awsvpc`に変更
* Nginx設定ファイルの内容変更

##### タスク定義でネットワークモードを`awsvpc`に変更
タスク定義のネットワークモード設定を`awsvpc`に変更します。
`bridge`モードではホストポートとして動的ポートを使用していましたが、`awsvpc`モードではホストとコンテナで異なるポートを使用することができないため、ホストとコンテナで同じポート(80)を使用することになります。
それに伴ってセキュリティグループで動的ポートを許可していた部分を使用するポート(80)を許可する形に変更しました。

##### Nginx設定ファイルの内容変更
今回の場合は同タスク内でNginxとGunicornのコンテナ間通信を実施しますが、ネットワークモードを`awsvpc`に変更するにあたって、Nginxの設定ファイルも以下のように変更しました。

```bash:Nginx設定ファイル(変更後：awsvpc使用時)
upstream my_app {
  server localhost:8000;
}

server {
  listen 80;

  location /static {
    alias /public/static;
  }

  location / {
    proxy_pass http://my_app;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_set_header X-Forwarded-Proto $scheme;
  }
}
```

# 構成図(awsvpcモード使用後)
`awsvpc`モードを使用した時の構成図は以下のようになります。
![diagram02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1410257/882b3959-fe48-7c85-e8d7-1a823212cf04.png)

# 注意点
`awsvpc`ではタスクそれぞれに対してENIが割り当てられますが、実際にENIがアタッチされるのはECSコンテナインスタンスに対してとなるため、インスタンスタイプのENI最大数について注意が必要です。

今回ECSコンテナインスタンスのインスタンスタイプを`t2.micro`として構成していましたが、タスク数を2つに増やそうとした時に以下のエラーが発生しました。

```
service ecs-django-service was unable to place a task because no container instance met all of its requirements.
The closest matching container-instance 907f60dd991e41a1870d931b3eb96803　encountered error "RESOURCE:ENI".
```

`t2.micro`のENI最大数が「2」となるため、ECSコンテナインスタンスがホストレベルのプロセスで使用する分を差し引くと、タスクで使用できるENIの数は「1」となります。
それによってタスクを2つ稼働させようとした時に上記エラーが発生したようです。

https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/using-eni.html#AvailableIpPerENI

ECSコンテナインスタンスのインスタンスタイプを`t2.small`に変更したうえで、タスク数を2つに変更したところ問題なく起動することができました。

また、ENIトランキングという機能を使用することでENIとタスクを1対1で紐付けるのではなく、1対多で紐付けることができるようです。
ENIトランキングを使用するにはサポートされているインスタンスタイプを使用する必要があります。

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/container-instance-eni.html

# あとがき
同タスク内でのコンテナ間通信を実現することができましたが、実際にアプリケーションへの負荷が高まって、スケーリングする時にNginxとGunicornのコンテナが1対1の関係で増えていく必要があるのかなと疑問に思いました...
それぞれが別々にスケーリングできるような環境についても確認してみたいと思います。

**2022/5/5 追記**
サービス検出を使用した構成についても試してみました。

https://qiita.com/zeems/items/3722ae47b2ca539dbfd8