1. はじめに
Red Hat OpenShift on IBM CloudにはClassic Infrastructure版とVPC版がありますが、この記事ではClassic Infrastructure版で確認しています。VPC版はこちらです。
OpenShiftではアプリケーションPodで稼働するサービスをRouteを使って外部公開することが可能です。でも、実際に外部からのアクセスはどのような経路を辿ってアプリケーションPodにアクセスしているのでしょうか?
Red Hat OpenShift on IBM Cloudでは、ここに公式の説明がありますが、この記事では実際に処理を追いかけてみることで、実装を深く理解したいと思います。
最初に結論を書いておきますが、
- DNSによる名前解決を行い、NLB Podが保護するVIPを取得。
- 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経由で割り振りを行う。
- Router Pod(router-default-xxxxxx)にアクセス
- Router PodはPrivate IP(172.30.xx.xx)を持つ
- HAProxyを利用してL7 Load Balancerを提供
- HAProxyの機能で(HTTPヘッダを元に)Application Podに割り振りを行う。
- 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アドレスが返ってきます。
$ 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の構成を確認してみます。
/ # 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の構成も確認してみます。
/ $ 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箇所からしか出力されていないが)
/ $ 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の構成を確認してみます。
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