LoginSignup
4
1

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

Last updated at Posted at 2020-06-30

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にアクセス

という流れになります。

image.png

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
    }
}

eth1の構成も確認してみます。

VRRPで保護されるportable IPがeth1に付与されている。
/ $ ip a show dev eth1
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000
    link/ether 06:d2:12:a3:a5:f5 brd ff:ff:ff:ff:ff:ff
    inet xxx.xxx.xx.xx/27 brd xxx.xxx.xx.xx scope global noprefixroute eth1
       valid_lft forever preferred_lft forever
    inet 128.168.xx.xx/32 scope global eth1
       valid_lft forever preferred_lft forever
    inet6 fe80::4d2:12ff:fea3:a5f5/64 scope link
       valid_lft forever preferred_lft forever

結果、128.168.xx.xxをVIPとしてeth1に付与し、障害時には別Podに引き継げるように構成されていることがわかります。

(2023年8月補足)
この環境では取得し損ねたが、以下は別環境でのtcpdumpの結果。確かにVRRPプロトコルがeth1(Public Interface)上で実行されていることが分かる。(1 node構成なので1箇所からしか出力されていないが)

NLB Pod内で実行したtcpdumpの結果
/ $ sudo tcpdump -i eth1 vrrp -nn
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), snapshot length 262144 bytes
02:04:07.571433 IP 161.202.xx.xx > 224.0.0.18: VRRPv2, Advertisement, vrid 93, prio 100, authtype none, intvl 1s, length 20
02:04:08.571544 IP 161.202.xx.xx > 224.0.0.18: VRRPv2, Advertisement, vrid 93, prio 100, authtype none, intvl 1s, length 20
02:04:09.571598 IP 161.202.xx.xx > 224.0.0.18: VRRPv2, Advertisement, vrid 93, prio 100, authtype none, intvl 1s, length 20
02:04:10.571682 IP 161.202.xx.xx > 224.0.0.18: VRRPv2, Advertisement, vrid 93, prio 100, authtype none, intvl 1s, length 20
02:04:11.571790 IP 161.202.xx.xx > 224.0.0.18: VRRPv2, Advertisement, vrid 93, prio 100, authtype none, intvl 1s, length 20VRRPv2, Advertisement, vrid 222, prio 100, authtype none, intvl 1s, length 20


/ $ sudo tcpdump -i eth0 vrrp -nn
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
(出力なし)

(補足終わり)

ただし、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
4
1
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1