こんにちは、以前「サービスメッシュと AWS App Mesh に入門した」という記事を書きました。
AWS App Mesh では、メッシュ上にデプロイされているアプリケーションのトラフィックは、すべてサイドカーとしてデプロイされている Envoy Proxy を経由します。 これは Pod 内から Pod 外への通信と、Pod 外から Pod 内のアプリケーションの両方です。
しかし既存のサービスに AWS App Mesh を導入するときは、クライアントやサーバーの指し先を localhost に書き換えることはありません。 この記事では、AWS App Mesh がどのようにして、各 Pod が Envoy Proxy をプロキシするのかを解決します。
カスタムコントローラによる注入
前回の記事で説明したとおり、Amazon EKS 上ならカスタムコントローラによって、Pod に Envoy Proxy サイドカーが注入されます。 カスタムコントローラは Pod に対して、以下の 2 つのコンテナを注入します。
- ネットワークを初期化する init コンテナ
- サイドカーコンテナとなる Envoy Proxy
同じ Pod 内のコンテナは同じネットワークを利用します。 init コンテナがネットワークを設定することで、サービスが利用するネットワークを初期化できます。
init コンテナの処理
App Mesh 上にデプロイする Pod は、Egress(Pod 外への通信)と Ingress(Pod への通信)の両方がサイドカーコンテナの Envoy Proxy を経由します。 init コンテナは、Pod 外への通信を Envoy Proxy の Egress ポートに流し、Pod への通信を Envoy Proxy の Ingress ポートに流すよう初期化します。
このコンテナの内部ではシェルスクリプトで、 iptables
コマンドによって Pod 内のネットワークを設定します。
スクリプト全体はこちらから確認できます。
このスクリプトは主に 3 つの処理をします。
initialize()
... ネットワーク設定の初期化enable_egress_routing()
Egress ネットワークの設定enable_ingress_redirect_routing()
... Ingress ネットワークの設定
ネットワーク設定の初期化
まずはネットワーク設定の初期化です。
これはスクリプトとの initialize()
に記述されています。
まずはネットワークルールをする前に、 iptables
コマンドで新たなチェインを作成します。
チェインとはiptables
のルールまとめた単位で、チェインに対してルールを追加したり、条件を元に利用するチェインを選択できます。
if [ ! -z "$APPMESH_APP_PORTS" ]; then
iptables -t nat -N APPMESH_INGRESS
fi
iptables -t nat -N APPMESH_EGRESS
$APPMESH_APP_PORTS
は Pod 内のサービスが待ち受けるポート番号で、サービスがリクエストを受け付けるなら Ingress ネットワークのチェインを作成します。
(スクリプトには他にも mangle テーブルを設定していますが、現在は利用されていないようです)
Egress ネットワークの設定
Pod 内から Pod 外へのネットワーク設定は enable_egress_routing()
に記述されています。
Pod 内からの通信を全て、Envoy Proxy の $APPMESH_ENVOY_EGRESS_PORT
(15001)に転送するよう設定します。
ただし Envoy Proxy 自身は直接 Pod 外に通信する必要があります。
# (1) Envoy Proxy自身のUID/GIDからの通信はEnvoy Proxyを経由しない
[ ! -z "$APPMESH_IGNORE_UID" ] && \
iptables -t nat -A APPMESH_EGRESS \
-m owner --uid-owner $APPMESH_IGNORE_UID \
-j RETURN
[ ! -z "$APPMESH_IGNORE_GID" ] && \
iptables -t nat -A APPMESH_EGRESS \
-m owner --gid-owner $APPMESH_IGNORE_GID \
-j RETURN
# (2) 特定のポート、アドレスもEnvoy Proxyを経由しない
[ ! -z "$APPMESH_EGRESS_IGNORED_PORTS" ] && \
iptables -t nat -A APPMESH_EGRESS \
-p tcp \
-m multiport --dports "$APPMESH_EGRESS_IGNORED_PORTS" \
-j RETURN
[ ! -z "$APPMESH_EGRESS_IGNORED_IP" ] && \
iptables -t nat -A APPMESH_EGRESS \
-p tcp \
-d "$APPMESH_EGRESS_IGNORED_IP" \
-j RETURN
# (3) それ以外の通信はEnvoy ProxyのEgressポートにリダイレクト
iptables -t nat -A APPMESH_EGRESS \
-p tcp \
-j REDIRECT --to $APPMESH_ENVOY_EGRESS_PORT
# (4) ローカル宛以外の全ての通信にAPPMESH_EGRESSチェインを使う
iptables -t nat -A OUTPUT \
-p tcp \
-m addrtype ! --dst-type LOCAL \
-j APPMESH_EGRESS
- Envoy Proxy の UID(1337) と、GID(1337) からのパケットは、RETURN ターゲットを指定します。
$APPMESH_ENVOY_INGRESS_PORT
(22、SSH ポート)宛と、$APPMESH_EGRESS_IGNORED_IP
(169.254.169.254、EC2 インスタンスのメタデータ)宛の TCP パケットは、 RETURN ターゲットを指定します。- それ以外の TCP パケットは REDIRECT ターゲットで APPMESH_ENVOY_EGRESS_PORT(15001)に転送します。
- ローカル宛以外の全ての TCP パケットに、APPMESH_EGRESS チェインを適用します。
RETURN ターゲットは、以降のパケットの評価をやめます。 これにより一部のパケットを、Envoy Proxy ではなく Pod 外と直接やりとりできます。
Ingress ネットワークの設定
Pod 外から Pod 内へのネットワーク設定は enable_ingress_redirect_routing()
に記述されています。
本来アプリケーションが listen しているポート ($APPMESH_APP_PORTS
)宛のパケットを、 Envoy Proxy の $APPMESH_ENVOY_INGRESS_PORT
ポート(15000)に転送するよう設定します。
# (1) 全ての$APPMESH_APP_PORTS宛のパケットをEnvoy ProxyのIngressポートにリダイレクト
iptables -t nat -A APPMESH_INGRESS \
-p tcp \
-m multiport --dports "$APPMESH_APP_PORTS" \
-j REDIRECT --to-port "$APPMESH_ENVOY_INGRESS_PORT"
# (2) ローカル以外からの全ての通信にAPPMESH_INGRESSチェインを使う
iptables -t nat -A PREROUTING \
-p tcp \
-m addrtype ! --src-type LOCAL \
-j APPMESH_INGRESS
- 全ての
$APPMESH_APP_PORTS
宛のパケットを Envoy Proxy の Ingress ポートにリダイレクトします。 - ローカル以外からの全ての通信に APPMESH_INGRESS チェインを適用します。
まとめ
以上が Kubernetes マニフェストをほぼ修正することなく、App Mesh を導入できる仕組みです。 透過的にサービスメッシュが使える背景には、トラディッショナルな方法でネットワークを設定しているんですね。 App Mesh が動く仕組みがわかると、トラブル時のデバッグやネットワークの知識が深まりますね。