非常にググりにくい事柄なんですが、なるべく浮世離れしないタイトルとして書きます。

EC2(正確にはENI)からの名前解決、つまりリゾルバとしてのOutboundには、全て制限がかかっています。1024パケット/秒です。

VPC での DNS の使用 – Amazon Virtual Private Cloud

ECS DNS というキーワードで探すと、ほぼ間違いなくService Discoveryがうじゃうじゃ出てきますが、これはリゾルバ・キャッシュサーバの話です。 これをEC2で回避する方法はAWSが書いてくれています。

Avoid DNS Resolution Failures with EC2 Linux

似たようなことをECSで、なるべくエレガントにやります。わざわざこう書くのは、ちょっと泥臭いという意味です。

どういうとき問題になるか

そもそもこのポストを見ているひとは、今現に困っているはず。ですが意識合わせ。

  • LinuxでDNSローカルキャッシュしていない
  • 1台のEC2でかなりのトラフィックをさばいている
  • 現に名前解決しましたとかエラーが出てる
  • 負荷試験とかやってて、性能が頭打ちしてる

ECS & EC2で、詰め込むコンテナの数もかなり多い場合に起こりやすい現象です。

どうすればよいか

上記のAWSのドキュメントを読めばわかりますが、ローカルキャッシュを持つようにして、とにかくEC2(ENI)からOutboud:53 を抑制するしか手はありません。
VPC内のリゾルバに対する問い合わせの回数が問題ではないのです、1歩でも53番ポートへ出ようとする通信全てが対象なので、自前でリゾルバを1台用意しても無駄です。
コンテナの場合は、コンテナ内部からの名前解決をケアすることになります。つまりはコンテナ内の /etc/resolv.conf を何らかの方法で書き換える必要があります。

  • どうやって変えるのか
  • 何(IPアドレス)に変えるのか

これが主題。

最終構成

DNSキャッシュ機能

まずは何らかの方法で、DNSキャッシュサーバをコンテナインスタンス内に立てます。他にも選択肢がありそうですが、メジャーどころで2択

  • unbound
  • dnsmasq

どちらを選ぶかは好きな方で良いのですが、dnsmasqのほうがおそらくフットプリントが小さいし、必要なことにマッチしているので良いと思います。ただし訳があってunboundにも触れます。
選んだキャッシュサーバを各EC2に一つづつ立ち上げます。お好きな方法で良いです、EC2に直接インストールするか、ECSならDAEMONモードのServiceとして、一家に一台を実現するか、ここではDAEMONで動かします。

コンテナからの参照

各EC2に立てるDNSキャッシュサーバーをプライマリ、VPCごとについてるリゾルバをセカンダリにします。
コンテナの中のresolv.conf をいじれば良いとなります。ホストのではなく。

コンテナ内のresolv.conf

ECS & EC2なら、(おそらくほとんどのケースで)ホストのresolv.confがコピーされます。
仮にホストで直接キャッシュサーバーを動かしたとして、

  • primary: 127.0.0.1
  • secondary: VPC のリゾルバ

と書いた場合ホスト自身は名前解決問題なしですが、コンテナはから見ると127.0.0.1 はコンテナ自分自身のloを指すことになり、当然 port:53 でサービスしていないのでNGです、上記例の設定ならは、コンテナ内からの名前解決は、必ず1手目でタイムアウトしてセカンダリであるvpcのリゾルバに行くので、この設定は意味なしです。

じゃあ、ホストのローカルIPを挿せばうまく行くのかと言ったらうまく行かないです。やればわかりますが、ホストのローカルIPで問い合わせたが、レスポンスを返したIPが違うって怒られます。

なので、コンテナから見たゲートウェイ (bridgeの場合) 172.17.0.1固定を指します。

IP固定してOKなん?

他所では場合によっては固定じゃないっていっぱい書いてありますが、ここでは大丈夫です。
説明すると長いので、断言できることだけ。少なくともECSのブリッジモードは、bridgeという名前のついたデフォルトのdockerネットワークにすべてのコンテナが入ります。よって、Gatewayは 172.17.0.1 で確定です。Docker自体の仕様がかわったり、ECSのブリッジモードでネットワーク名をユーザーが新設できるようになるとかならない限り、ここは崩れません。
当たり前ですが、リゾルバにホスト名は書けないので、同タスク内のコンテナ同士の名前解決をLink等でできるようにしてもIPを書かねばならないので意味ないです。

ECS/EC2 での実装

キャッシュサーバ(コンテナ)の構築

サーバというものの、全EC2に立つのでそのつもりで。とりあえずecs-cliで書いてみました。 書き忘れましたが、キャッシュサーバ(コンテナ)と、メインのアプリコンテナは別タスクとなります。
docker-compose.yml

version: '3'

services:
  dnsmasq:
    image: himaoka/dnsmasq:latest
    ports:
      - 53:53/tcp
      - 53:53/udp
    cap_add:
      - NET_ADMIN

Dockerイメージの中身はGithubで見てください。注目は cap_add: NET_ADMIN です。コイツがないとコケます。でこれがあるからFargateやawsvpcモードでは使えません。更に補足としてこの cap_add はManagementConsleからは見えません(JSONベタ書きはいけるかもしれない)。ECS-CLIですら機能のすべてを網羅していませんが、タスク定義作成にブラウザポチポチはすぐに卒業したほうがいい、terraform や aws-cli などをすぐに使えるようにしてください。

このコンテナにより、コンテナインスタンスの port:53 でDNSリクエストに応答します。

ecs-params.yml

version: 1
task_definition:
  services:
    dnsmasq:
      mem_limit: 128M
      mem_reservation: 128M

多分こんなにメモリはいらない。 これで定義したタスク定義を、サービスとして設定、DAEMONで動かします。
まあ、ここでコンテナにこだわる理由はないです。よくわからんならEC2で素で動かしても全く問題なし。

アプリコンテナ

とりあえず何でもいいんですが、手っ取り早く動かして、docker exec で動いているコンテナに入り込んで確認します。なんでただ死なないコンテナであれば何でもOK

docker-compose.yml

version: '3'

services:
  nginx:
    image: nginx
    dns:
      - 172.17.0.1
      - <VPCのリゾルバのIP>

この dns はこれまた awsvpcモードでは使えません 理由はわかりません。できても良さそうなもんだけど。こっちはブラウザポチポチでも一応設定可能です。
確認方法は exec で入り込んで dig コマンドとか突っ込んで確認してください。

もう一つの方法・コンテナインスタンス側が譲歩する

基本コンテナインスタンスの resolv.confをコピーするという動きがるので、コンテナインスタンス内部側のリクエストは犠牲にして、コンテナ内部のリクエストだけをちゃんとしてやることもできます。単純にコンテナインスタンスの /etc/resolv.conf のプライマリを 172.17.0.1 にするだけです。
コンテナインスタンスからの名前解決はプライマリはすべて失敗、セカンダリとしてVPCのリゾルバを挿せば、常に1手損ですが通ることは通ります。
タスク定義を直さないでインフラの豪腕でなぎ倒すことは一応可能です。エレガント?な方法は下記です。

Amazon EC2 Static DNS Ubuntu Debian

Fargateならどうすんの?

まず、必要となるか否かですが、なんとも言えないです。Fargateということは一つのタスク全部で1024という制限になるので、上限に引っかかる可能性は減ります。
じゃあ、絶対に大丈夫か?と言われると場合によるかもしれない。1タスクに10コンテナとかあり、それらが各々がガンガン名前を引くならば絶対にOKとは言い切れない。

と私は思うので、一応試しました。ただし結論から書くと、きれいな方法は取れないです。

  • EC2という概念がなくなるので、メインアプリのコンテナと、DNSキャッシュコンテナは同タスクとする必要がある
  • DNSキャッシュサーバのListenは、メインアプリのコンテナからみても 127.0.0.1:53 となるので、nameserverは127.0.0.1 とすればよい

対して阻害要因です

  • dnsmasqのcap_add: NET_ADMINはFargateでは使えない
  • メインアプリコンテナのDNSとして、タスク定義としてdnsを指定することはできない (awsvpcモードの制約)

Fargateの結論、一応できたけど、、

dnsmasqのcap_addはどうにもならないので、unboundにします。
dnsも無理なんで、Dockerの ENTRYPOINTで/env/resolv.confを無理やり書き直します。
で、一応動きました。いい方法とは言えないので、解説はしませんがこの概要だけ書いて終わります。

unboundの注意点

unboundでやるときの注意点ですが、dnsmasqとこちらは違って、ルートDNSへのヒントを持っているので、権威サーバのレコードであれば自力のみで解決できます。対してdnsmasqはキャッシュはするものの、自力で権威サーバを調べ上げることはできない。
じゃあ、なんの問題もなくね?
いやいや、 問題大有りですわ。何も設定してないと、Route53の Private Hosted Zoneのレコードが全部死にます。

先に答えを書くと、unboundで行くならば、必ず すべてのレコードをVPCのリゾルバにForwardしてください。

厄介な Private Hosted Zone

個人的には嫌いな機能です。Private
Privateは作るときに必ずアサインするVPCを指定しますが、これは VPCのリゾルバに権威サーバに飛ばさずに Private Hosted Zone に横流しするルートを作ります。もちろん特定のレコードだけですが、これがあるのですべてのレコードをVPCのリゾルバにForwardするのがインフラ屋としての正しい解です。
Privateのレコードがこれ以上増えないってわかっているならば、個別ドメインに対してのForward設定してもいいです、がECSやってるならService Discoveryの重要性もわかっているはずです。でもってService Discovery の実態は Private Hosted Zone です。特定ゾーンだけForwardしてると、Service Disvoveryをある日いきなり使い始めたら終わりです。

おまけ unboundで起動時警告が出る

cap_add: SYS_RESOURCE でいいはず、もしくはulimitをタスク定義側で指定。  

おまけ2 ubuntu の場合

あんまり調べてないですが、ubuntu ユーザーなんで。ubuntu はデフォルトで 127.0.0.53:53 でListenしています。ホストの resolv.conf も 127.0.0.53 を参照していますので、そのまんまやるとホストの resolv.conf をコピーして不味そうですが、うまく行っています(書き換わる)。理由は調べてないですが、昔はちょっと問題があったらしい。今回のテストをローカルPCでやるときにちょっと手こずった。

元記事はこちら

EC2 DNS名前解決制限をECSでも回避する方法