Red Hat OpenShift on IBM Cloud(Classic Infrastructure): Podまでのアクセス経路(DNS -> NLB -> Router -> Pod)を追いかけてみる


1. はじめに

Red Hat OpenShift on IBM CloudにはClassic Infrastructure版とVPC版がありますが、この記事ではClassic Infrastructure版で確認しています。VPC版はこちらです。

OpenShiftではアプリケーションPodで稼働するサービスをRouteを使って外部公開することが可能です。でも、実際に外部からのアクセスはどのような経路を辿ってアプリケーションPodにアクセスしているのでしょうか?
Red Hat OpenShift on IBM Cloudでは、ここに公式の説明がありますが、この記事では実際に処理を追いかけてみることで、実装を深く理解したいと思います。

最初に結論を書いておきますが、

  1. DNSによる名前解決を行い、NLB Podが保護するVIPを取得。
  2. NLB Pod(ibm-cloud-provider-ip-<IPアドレス>-xxxxxxxx)にアクセス。
    • NLB=Network Load Balancer = L4 Load Balancer
    • 所謂、Kubernetesのtype: LoadBalancerのCloud Provider実装。
    • NLB Podでは、keepalivedを利用してVIP(Public IP)を保護
    • (keepalivedではなく)iptablesを使ってRouter Podにk8s NW経由で割り振りを行う。
  3. Router Pod(router-default-xxxxxx)にアクセス
    • Router PodはPrivate IP(172.30.xx.xx)を持つ
    • HAProxyを利用してL7 Load Balancerを提供
    • HAProxyの機能で(HTTPヘッダを元に)Application Podに割り振りを行う。
  4. Application Podにアクセス

という流れになります。

2. 事前準備

この環境では、TOK02/TOK04/TOK05にまたがるマルチゾーンクラスターを利用しています。
また、以下のようにアプリケーションを展開し、Routeを作成します。これによって、外部からRoute経由でこのアプリケーションPodにアクセス可能になります。

$ oc new-app --name hello-world https://github.com/IBM/container-service-getting-started-wt --context-dir="Lab 1"

$ oc scale --replicas=5 dc hello-world

$ oc expose service hello-world

$ oc get pods,svc,route
NAME                       READY   STATUS      RESTARTS   AGE
pod/hello-world-1-2r2dr    1/1     Running     0          8m17s
pod/hello-world-1-build    0/1     Completed   0          8m50s
pod/hello-world-1-deploy   0/1     Completed   0          8m19s
pod/hello-world-1-gssg9    1/1     Running     0          25s
pod/hello-world-1-kw7bp    1/1     Running     0          25s
pod/hello-world-1-sk48q    1/1     Running     0          25s
pod/hello-world-1-x27l5    1/1     Running     0          25s

NAME                  TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
service/hello-world   ClusterIP   172.21.81.250   <none>        8080/TCP   8m53s

NAME                                   HOST/PORT                                                                                                                       PATH   SERVICES      PORT       TERMINATION   WILDCARD
route.route.openshift.io/hello-world   hello-world-syasuda.myrokscluster43-xxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-0000.jp-tok.containers.appdomain.cloud ... 1 more          hello-world   8080-tcp                 None

3. Routerへのアクセス経路を追いかける

3-1. DNS名前解決

Routeで公開されたFQDNを名前解決すると、TOK02/TOK04/TOK05の複数拠点のPublic IPアドレスが返ってきます。

DNS名前解決
$ dig A +noall +answer @1.1.1.1 hello-world-syasuda.myrokscluster43-xxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-0000.jp-tok.containers.appdomain.cloud

なお、これらのアドレスは、router-tok02/router-tok04/router-tok05などのLoadBalancer Serviceで構成されているEXTERNAL-IPと同一です。

$ oc get services -n openshift-ingress
NAME                      TYPE           CLUSTER-IP       EXTERNAL-IP       PORT(S)                      AGE
router-default            LoadBalancer   172.21.200.228   128.168.xx.xxx    80:31712/TCP,443:32543/TCP   64d
router-internal-default   ClusterIP      172.21.57.171    <none>            80/TCP,443/TCP,1936/TCP      64d
router-tok02              LoadBalancer   172.21.205.181   161.202.xx.xxx    80:30370/TCP,443:32261/TCP   41s
router-tok04              LoadBalancer   172.21.108.161   128.168.xx.xxx    80:31380/TCP,443:30034/TCP   64d
router-tok05              LoadBalancer   172.21.103.123   165.192.xx.xxx    80:32357/TCP,443:30142/TCP   64d

ここでは、128.168.xx.xx宛の処理を追いかけてみたいと思います。

3-2. NLB Pod

128.168.xx.xx を持つPodを探してみると、ibm-cloud-provider-ip-<IPアドレス>-xxxxxxxxというPodが見つかります。これはNLB Podと呼ばれているPodであり、router-tok02/router-tok04/router-tok05などのLoadBalancer Service(=Router Service)の実体にあたります。

$ oc get pods --all-namespaces -o wide |grep 128.168.xx.xx
ibm-system                                              ibm-cloud-provider-ip-128-168-xx-xx-c8b78c69b-hpc2k              1/1     Running      0          33d     10.192.109.137   10.192.109.137   <none>           <none>
ibm-system                                              ibm-cloud-provider-ip-128-168-xx-xx-c8b78c69b-xbpdq              1/1     Running      0          33d     10.192.109.197   10.192.109.197   <none>           <none>

次に、NLB Podの構成を確認してみます。

$ oc rsh -n ibm-system ibm-cloud-provider-ip-128-168-xx-xx-c8b78c69b-hpc2k

/ # ps -ef
PID   USER     TIME  COMMAND
    1 root      3:32 /usr/local/bin/keepalived
   22 root      0:00 /usr/sbin/keepalived --dont-fork --dump-conf --log-console --log-detail --release-vips --address-monitoring
   23 root     34:12 /usr/sbin/keepalived --dont-fork --dump-conf --log-console --log-detail --release-vips --address-monitoring
   26 root      0:00 /bin/sh
   32 root      0:00 ps -ef

このPodの実態はkeepalivedが動いていることがわかります。
keepalived.confの構成を確認してみます。

keepalived.conf
/ # cat /etc/keepalived/keepalived.conf
global_defs {
    vrrp_mcast_group4 224.0.0.18
}

vrrp_instance vip-128.168.xx.xx {
    state BACKUP
    interface eth1
    virtual_router_id 14
    priority 100
    nopreempt
    virtual_ipaddress {
        128.168.xx.xx
    }
}

この128.168.xx.xxをVIPとして扱い、障害時には別Podに引き継げるように構成されていることがわかります。ただし、keepalived.confには転送先の情報が出力されていません。つまり、keepalivedはVIPの管理だけを担当しており、パケットの割り振りは担当していません。代わりに、NLB Podにおける割り振り処理はiptablesで実施されます(iptablesはkube-proxyによって制御されます)。iptablesでKUBE-SERVICES Chainを確認してみます。

/ # iptables -L "KUBE-SERVICES" -v -n -t nat |grep "loadbalancer"
    0     0 KUBE-FW-DUBAWALOAOQGHLZQ  tcp  --  *      *       0.0.0.0/0            128.168.xx.xx       /* openshift-ingress/router-tok04:http loadbalancer IP */ tcp dpt:80
    0     0 KUBE-FW-LIGRWE2RGSK5GETQ  tcp  --  *      *       0.0.0.0/0            165.192.xx.xx       /* openshift-ingress/router-tok05:https loadbalancer IP */ tcp dpt:443
    0     0 KUBE-FW-VEWSEELRREPOPKKP  tcp  --  *      *       0.0.0.0/0            161.202.xx.xx      /* openshift-ingress/router-tok02:http loadbalancer IP */ tcp dpt:80
    0     0 KUBE-FW-MBAZS3WDHL45BPIZ  tcp  --  *      *       0.0.0.0/0            128.168.xx.xx       /* openshift-ingress/router-default:https loadbalancer IP */ tcp dpt:443
    0     0 KUBE-FW-QDH42CU33EM2QUE5  tcp  --  *      *       0.0.0.0/0            128.168.xx.xx       /* openshift-ingress/router-tok04:https loadbalancer IP */ tcp dpt:443
    0     0 KUBE-FW-DXUYFP57AWJVW53Q  tcp  --  *      *       0.0.0.0/0            161.202.xx.xx      /* openshift-ingress/router-tok02:https loadbalancer IP */ tcp dpt:443
    0     0 KUBE-FW-HEVFQXAKPPGAL4BV  tcp  --  *      *       0.0.0.0/0            128.168.xx.xx       /* openshift-ingress/router-default:http loadbalancer IP */ tcp dpt:80
    0     0 KUBE-FW-OMKAGPVWEYNWKEWW  tcp  --  *      *       0.0.0.0/0            165.192.xx.xx       /* openshift-ingress/router-tok05:http loadbalancer IP */ tcp dpt:80

(もう忘れてしまったかもしれませんが128.168.xx.xx宛の処理を追いかけていたので)ここでは、80番ポートである一番上の"KUBE-FW-DUBAWALOAOQGHLZQ"を追いかけてみます。

/ # iptables -L "KUBE-SVC-DUBAWALOAOQGHLZQ" -v -n -t nat
Chain KUBE-SVC-DUBAWALOAOQGHLZQ (3 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 KUBE-SEP-HGNA5KMN5EENFF6N  all  --  *      *       0.0.0.0/0            0.0.0.0/0            statistic mode random probability 0.50000000000
    0     0 KUBE-SEP-H7OGC53763Z5IA6P  all  --  *      *       0.0.0.0/0            0.0.0.0/0

以上より、KUBE-SEP-HGNA5KMN5EENFF6NおよびKUBE-SEP-H7OGC53763Z5IA6Pに等確率で割り振りを行なっていることがわかります(SEPはService Endpointの意味でしょう)。

/ # iptables -L "KUBE-SEP-HGNA5KMN5EENFF6N" -v -n -t nat
Chain KUBE-SEP-HGNA5KMN5EENFF6N (1 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 KUBE-MARK-MASQ  all  --  *      *       172.30.34.96         0.0.0.0/0
    0     0 DNAT       tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp to:172.30.34.96:80

/ # iptables -L "KUBE-SEP-H7OGC53763Z5IA6P" -v -n -t nat
Chain KUBE-SEP-H7OGC53763Z5IA6P (1 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 KUBE-MARK-MASQ  all  --  *      *       172.30.97.241        0.0.0.0/0
    0     0 DNAT       tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp to:172.30.97.241:80

結果、NLB Podでは128.168.xx.xx宛のリクエストは172.30.34.96:80および172.30.97.241:80に等確率でDNATされていることがわかります。

ちなみに、NLB Podからどこに割り振られているかは、本来は以下から確認することができます。

$ oc describe service/router-default -n openshift-ingress|grep -i endpoints
Endpoints:                172.30.34.96:80,172.30.97.241:80
Endpoints:                172.30.34.96:443,172.30.97.241:443
$ oc describe service/router-tok02 -n openshift-ingress|grep -i endpoints
Endpoints:                172.30.34.96:80,172.30.97.241:80
Endpoints:                172.30.34.96:443,172.30.97.241:443
$ oc describe service/router-tok04 -n openshift-ingress|grep -i endpoints
Endpoints:                172.30.34.96:80,172.30.97.241:80
Endpoints:                172.30.34.96:443,172.30.97.241:443
$ oc describe service/router-tok05 -n openshift-ingress|grep -i endpoints
Endpoints:                172.30.34.96:80,172.30.97.241:80
Endpoints:                172.30.34.96:443,172.30.97.241:443

3-3. Router Pod

NLB Podが割り振りしている172.30.34.96:80および172.30.97.241:80は何でしょうか?

# oc get pods --all-namespaces -o wide|grep -e 172.30.34.96 -e 172.30.97.241
openshift-ingress                                       router-default-5d4497844b-fqhd6                                   1/1     Running      0          12d     172.30.34.96     10.132.94.75     <none>           <none>
openshift-ingress                                       router-default-5d4497844b-w4jpg                                   1/1     Running      0          12d     172.30.97.241    10.193.75.36     <none>           <none>

これにより、router-defaultというPodに割り振られていることが分かります。これはRouter Podです。Router Podは1つのクラスターにデフォルトで2つ作成されています。このRouter Podにログインして構成を確認してみます。

# oc rsh -n openshift-ingress router-default-5d4497844b-fqhd6

sh-4.2$ ps -ef
UID         PID   PPID  C STIME TTY          TIME CMD
1000280+      1      0  1 Jun17 ?        05:18:04 /usr/bin/openshift-router
1000280+   4713      1  1 02:38 ?        00:00:02 /usr/sbin/haproxy -f /var/lib/haproxy/conf/haproxy.config -p /var/lib/haproxy/run/haproxy.pid -x /var/lib/haproxy/run/haproxy.sock -sf 4703 4693
1000280+   4720      0  0 02:42 pts/0    00:00:00 /bin/sh
1000280+   4726   4720  0 02:42 pts/0    00:00:00 ps -ef

Router Pod内ではHAProxyが稼働していることが分かります。HAProxyの構成を確認してみます。

/var/lib/haproxy/conf/haproxy.config
sh-4.2$ cat /var/lib/haproxy/conf/haproxy.config
(途中略)
frontend public

  bind :80
  mode http
  tcp-request inspect-delay 5s
  tcp-request content accept if HTTP
  monitor-uri /_______internal_router_healthz

  # Strip off Proxy headers to prevent HTTpoxy (https://httpoxy.org/)
  http-request del-header Proxy

  # DNS labels are case insensitive (RFC 4343), we need to convert the hostname into lowercase
  # before matching, or any requests containing uppercase characters will never match.
  http-request set-header Host %[req.hdr(Host),lower]

  # check if we need to redirect/force using https.
  acl secure_redirect base,map_reg(/var/lib/haproxy/conf/os_route_http_redirect.map) -m found
  redirect scheme https if secure_redirect

  use_backend %[base,map_reg(/var/lib/haproxy/conf/os_http_be.map)]

  default_backend openshift_default

# public ssl accepts all connections and isn't checking certificates yet certificates to use will be
# determined by the next backend in the chain which may be an app backend (passthrough termination) or a backend
# that terminates encryption in this router (edge)
frontend public_ssl

  bind :443
  tcp-request  inspect-delay 5s
  tcp-request content accept if { req_ssl_hello_type 1 }

  # if the connection is SNI and the route is a passthrough don't use the termination backend, just use the tcp backend
  # for the SNI case, we also need to compare it in case-insensitive mode (by converting it to lowercase) as RFC 4343 says
  acl sni req.ssl_sni -m found
  acl sni_passthrough req.ssl_sni,lower,map_reg(/var/lib/haproxy/conf/os_sni_passthrough.map) -m found
  use_backend %[req.ssl_sni,lower,map_reg(/var/lib/haproxy/conf/os_tcp_be.map)] if sni sni_passthrough

  # if the route is SNI and NOT passthrough enter the termination flow
  use_backend be_sni if sni

  # non SNI requests should enter a default termination backend rather than the custom cert SNI backend since it
  # will not be able to match a cert to an SNI host
  default_backend be_no_sni
(途中略)
# Plain http backend or backend with TLS terminated at the edge or a
# secure backend with re-encryption.
backend be_http:syasuda:hello-world
  mode http
  option redispatch
  option forwardfor
  balance leastconn

  timeout check 5000ms
  http-request set-header X-Forwarded-Host %[req.hdr(host)]
  http-request set-header X-Forwarded-Port %[dst_port]
  http-request set-header X-Forwarded-Proto http if !{ ssl_fc }
  http-request set-header X-Forwarded-Proto https if { ssl_fc }
  http-request set-header X-Forwarded-Proto-Version h2 if { ssl_fc_alpn -i h2 }
  # Forwarded header: quote IPv6 addresses and values that may be empty as per https://tools.ietf.org/html/rfc7239
  http-request add-header Forwarded for=%[src];host=%[req.hdr(host)];proto=%[req.hdr(X-Forwarded-Proto)];proto-version=\"%[req.hdr(X-Forwarded-Proto-Version)]\"
  cookie 50180e4b662b224b6f27aade3ab06d5c insert indirect nocache httponly
  server pod:hello-world-1-2r2dr:hello-world:172.30.150.205:8080 172.30.150.205:8080 cookie 7dc27513af58658ee891b9a99d171e72 weight 256 check inter 5000ms
  server pod:hello-world-1-kw7bp:hello-world:172.30.237.72:8080 172.30.237.72:8080 cookie 16b5b8f6fa87b14eceaee9bb30ccab4b weight 256 check inter 5000ms
  server pod:hello-world-1-gssg9:hello-world:172.30.34.127:8080 172.30.34.127:8080 cookie ae3543b5497fe908a7ecb8d3aac1b121 weight 256 check inter 5000ms
  server pod:hello-world-1-sk48q:hello-world:172.30.71.240:8080 172.30.71.240:8080 cookie 01ab9b2e14d412bb6c02c327b59a0c6d weight 256 check inter 5000ms
  server pod:hello-world-1-x27l5:hello-world:172.30.97.254:8080 172.30.97.254:8080 cookie e42a33a091d7baffdda4d61065997f3c weight 256 check inter 5000ms

以上より、serverのセクションからわかるとおり、HAProxyがアプリケーションPod(hello-world)に直接割り振りを行なっていることがわかります。
また、backend be_http:syasuda:hello-worldにて、router-pod内でのリクエストの処理が分類されています。実際、以下でhello-world-syasuda.myrokscluster43-xxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-0000.jp-tok.containers.appdomain.cloudというHost ヘッダを含む場合は、be_http:syasuda:hello-worldで定義される設定を利用する旨の記述があります。

sh-4.2$ cat /var/lib/haproxy/conf/os_http_be.map|grep hello
^hello-world-syasuda\.myrokscluster43-xxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-0000\.jp-tok\.containers\.appdomain\.cloud(:[0-9]+)?(/.*)?$ be_http:syasuda:hello-world

(参考)
もし、$ oc expose service hello-world --hostname www.yasuda.comのように独自ドメインを使用した場合(というかこっちの方が一般的だが)は、以下のようになっている。

sh-4.2$ cat /var/lib/haproxy/conf/haproxy.config|grep backend|grep hello-world
backend be_http:syasuda:hello-world
sh-4.2$ cat /var/lib/haproxy/conf/os_http_be.map|grep hello
^www\.yasuda\.com(:[0-9]+)?(/.*)?$ be_http:syasuda:hello-world