2022/5/5 追記
あとがきにサービス検出について試した記事のリンクを追加
はじめに
ECSで1つのタスクとして複数コンテナを稼働させ、コンテナ間で通信したいと思った時に上手くできなかった部分と対応を記載します。
ローカル環境で構築した際にはDocker Composeを使用しており、ymlファイルで定義するサービス名を使用してコンテナ間通信をすることができましたが、ECS環境ではIPアドレスでの通信でないと上手くいかなかったため自分なりに調査してみました。
構成図
- サービスの起動タイプは「EC2」
- タスク定義でネットワークモードを「bridge」
- タスク定義で複数のコンテナを定義
- Nginx
- Gunicorn
- Nginxコンテナについてはポートマッピングによって動的ポートを使用
また、Nginx設定ファイルは以下のような内容となります。
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コンテナインスタンスとコンテナの通信を行います。
実際にタスクを稼働させた状態でECSコンテナインスタンスにSSH接続し、docker network inspect bridge
コマンドを実行したところ、コンテナはDockerにデフォルトで存在するbridge
ネットワークに所属していることが確認できました。
$ 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ネットワークとの違いが記載されています。
違いの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アドレスが割り当てられます。
そして下記URLのドキュメントを参照すると以下のような記載があります。
さらに、同じタスクに属するコンテナが、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の設定ファイルも以下のように変更しました。
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
モードを使用した時の構成図は以下のようになります。
注意点
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つ稼働させようとした時に上記エラーが発生したようです。
ECSコンテナインスタンスのインスタンスタイプをt2.small
に変更したうえで、タスク数を2つに変更したところ問題なく起動することができました。
また、ENIトランキングという機能を使用することでENIとタスクを1対1で紐付けるのではなく、1対多で紐付けることができるようです。
ENIトランキングを使用するにはサポートされているインスタンスタイプを使用する必要があります。
あとがき
同タスク内でのコンテナ間通信を実現することができましたが、実際にアプリケーションへの負荷が高まって、スケーリングする時にNginxとGunicornのコンテナが1対1の関係で増えていく必要があるのかなと疑問に思いました...
それぞれが別々にスケーリングできるような環境についても確認してみたいと思います。
2022/5/5 追記
サービス検出を使用した構成についても試してみました。