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

# 1. はじめに

Red Hat OpenShift on IBM CloudにはClassic Infrastructure版とVPC版がありますが、この記事ではVPC版でのアクセス経路を確認しています。[Classic Infrastructure版はこちら](https://qiita.com/testnin2/items/22307e5a2eda851c1438)です。

OpenShiftではアプリケーションPodで稼働するサービスをRouteを使って外部公開することが可能です。でも、実際に外部からのアクセスはどのような経路を辿ってアプリケーションPodにアクセスしているのでしょうか？
Red Hat OpenShift on IBM Cloudでは、[ここ](https://cloud.ibm.com/docs/openshift?topic=openshift-ingress-about-roks4#classic-multi-roks4)に公式の説明がありますが、この記事では実際に処理を追いかけてみることで、実装を深く理解したいと思います。

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

1. DNSによる名前解決を行い、VPC Load BalancerのIPアドレスを取得。
2. VPC Load Balancerにアクセス
    - VPC Load Balancerが、所謂Kubernetesのtype: LoadBalancerのCloud Provider実装。
    - VPC Load Balancerに届いた80/TCPと443/TCPへのアクセスを、**L4レベルで**、Worker node上の**NodePort**に割り振る。
    - `externalTrafficPolicy=Local`が設定されているため、Router Podが存在するWorker nodeにしか割り振りが行われない（それ以外のWorker nodeにはヘルスチェックに失敗して割り振りから除外される。https://cloud.ibm.com/docs/openshift?topic=openshift-vpc_lb_healthcheck）。
        -  (2020/07/09追記 -> 2023/07/25文言を修正）Red Hat OpenShift on IBM Cloudでは、`externalTrafficPolicy=Cluster`がデフォルト値であり、全てのWorker nodeのNode Portに割り振りが行われるのがデフォルトの動作のようです。ただし、検証時のバージョンにおいては、`externalTrafficPolicy=Local`がデフォルトとなっていたクラスターもありました。もちろん、
        -  (2023/07/25文言を追記）一般的に、`externalTrafficPolicy=Local`にすると、「Worker nodes上のkube-proxyはローカルに存在するエンドポイントへのプロキシーリクエストだけを処理し、他のノードへはトラフィックを転送しなくなるため、クライアントの送信元IPアドレスが保持される」と、Kubernetesのドキュメントなどには書かれてあります。しかし、Red Hat OpenShift on IBM Cloud(VPC)では、クライアントの送信元IPアドレスはWorker nodeに到達する前に、既にVPC Load Balancer(ALB: Protocol=TCP)にてVPC Load BalancerのIPアドレスに置き換えられているため、クライアントの送信元IPアドレスは保持されていません。クライアントの送信元IPアドレスを取得したい場合は、PROXYプロトコルを有効にする必要があります。[参考: Ingress の構成 - ソース IP アドレスの保持](https://cloud.ibm.com/docs/openshift?topic=openshift-ingress-configure)
3. Router Pod(router-default-xxxxxx)にアクセス
    - Router PodはPrivate IP(172.17.xx.xx)を持つ
    - HAProxyを利用してL7 Load Balancerを提供
    - HAProxyの機能で（HTTPヘッダを元に）Application Podに割り振りを行う。
4. Application Podにアクセス

という流れになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62127/8f49d3d4-170f-580e-1420-39658114bf18.png)





# 2. 事前準備

この環境では、DAL1/DAL2/DAL3にまたがるVPC上のマルチゾーンクラスターを利用しています。
また、以下のようにアプリケーションを展開し、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-45m9j    1/1     Running     0          3m36s
pod/hello-world-1-b75zr    1/1     Running     0          3m37s
pod/hello-world-1-build    0/1     Completed   0          4m24s
pod/hello-world-1-deploy   0/1     Completed   0          3m39s
pod/hello-world-1-gl5sd    1/1     Running     0          3m37s
pod/hello-world-1-j8w4f    1/1     Running     0          3m36s
pod/hello-world-1-rb8nt    1/1     Running     0          3m37s

NAME                  TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
service/hello-world   ClusterIP   172.21.14.37   <none>        8080/TCP   4m25s

NAME                                   HOST/PORT                                                                                                        PATH   SERVICES      PORT       TERMINATION   WILDCARD
route.route.openshift.io/hello-world   hello-world-syasuda.myroksclustervpc-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-0000.us-south.containers.appdomain.cloud          hello-world   8080-tcp                 None

```

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

## 3-1. DNS名前解決

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

```shell-session:DNS名前解決
$ dig A +noall +answer @1.1.1.1 hello-world-syasuda.myroksclustervpc-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-0000.us-south.containers.appdomain.cloud
hello-world-syasuda.myroksclustervpc-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-0000.us-south.containers.appdomain.cloud.	300 IN CNAME xxxxxxxx-us-south.lb.appdomain.cloud.
xxxxxxxx-us-south.lb.appdomain.cloud. 120 IN A	52.116.xxx.xxx
xxxxxxxx-us-south.lb.appdomain.cloud. 120 IN A	52.116.xxx.xxx
```

このCNAMEで構成されているFQDNが、まさにVPC Load BalancerのFQDNです。
<img width="1279" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62127/aa7652f0-24a6-ce42-64e7-361e36d75097.png">
<img width="1279" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62127/b0a19c4a-0db3-2234-0bb6-d9f4ea4cafb0.png">


## 3-2. VPC Load Balancer

以下のようにVPC Load BalancerはPort80/443で待ち受けしていますが、HTTP/HTTPSとしてではなくTCPとしてのL4レベルでの待ち受けを行なっています。
<img width="1279" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62127/f36d3a16-7991-0f64-de66-045191f06875.png">

Port 80/443でVPC Load Balancerにアクセスされたトラフィックは、割り振り時にそれぞれPort 32005/32360が利用されています。

<img width="1279" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62127/21595dbb-9609-a8b8-df07-5178c3c227b4.png">
<img width="1279" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62127/7f944e91-eac8-8d86-7355-ec3e5ced00aa.png">
<img width="1279" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62127/95ffb9fa-815d-058e-a893-a38def14c540.png">


```
$ oc get services --all-namespaces|grep -e NAME -e LoadBalancer
NAMESPACE                                               NAME                                          TYPE           CLUSTER-IP       EXTERNAL-IP                            PORT(S)                      AGE
openshift-ingress                                       router-default                                LoadBalancer   172.21.164.127   xxxxxxxx-us-south.lb.appdomain.cloud   80:32005/TCP,443:32360/TCP   19h

$ oc describe service -n openshift-ingress router-default
Name:                     router-default
Namespace:                openshift-ingress
Labels:                   app=router
                          ingresscontroller.operator.openshift.io/owning-ingresscontroller=default
                          router=router-default
Annotations:              service.kubernetes.io/ibm-load-balancer-cloud-provider-ip-type: public
Selector:                 ingresscontroller.operator.openshift.io/deployment-ingresscontroller=default
Type:                     LoadBalancer
IP:                       172.21.164.127
LoadBalancer Ingress:     xxxxxxxx-us-south.lb.appdomain.cloud
Port:                     http  80/TCP
TargetPort:               http/TCP
NodePort:                 http  32005/TCP
Endpoints:                172.17.74.10:80,172.17.93.91:80
Port:                     https  443/TCP
TargetPort:               https/TCP
NodePort:                 https  32360/TCP
Endpoints:                172.17.74.10:443,172.17.93.91:443
Session Affinity:         None
External Traffic Policy:  Cluster
Events:                   <none>

# oc get pods -o wide --all-namespaces|grep -e 172.17.74.10 -e 172.17.93.91
openshift-ingress                                       router-default-69cbd48fc7-49q77                                   1/1     Running     0          19h   172.17.93.91     10.240.0.5     <none>           <none>
openshift-ingress                                       router-default-69cbd48fc7-6mmbl                                   1/1     Running     0          19h   172.17.74.10     10.240.64.5    <none>           <none>
```


この結果をよく読んでみましょう。

- EXTERNAL-IPがVPC Load BalancerのFQDNになっている。つまり、`type:LoadBalancer`の実装はClassic InfrastructureではNLB Podだったが、VPCではVPC Load Balancerであることが分かる
- VPC Load Balancerから割り振られている32005/TCPと32360/TCPは、NodePortである。
- Node Portからは、`172.17.74.10`および`172.17.93.91`に割り振られている。これは、Router Podである。

## 3-3. Router Pod
VPC Load Balancerからノードに割り振られたアクセスは、同一Worker node上にあるRouter Podに転送されます。Router PodではHAProxyが稼働しており、そこから該当のアプリケーションPodに割り振りが行われます。このあたりはClassic Infrastructureの時と同じですね。

```
$ oc rsh -n openshift-ingress router-default-69cbd48fc7-49q77
sh-4.2$ ps -ef
UID         PID   PPID  C STIME TTY          TIME CMD
1000020+      1      0  0 Jul01 ?        00:02:51 /usr/bin/openshift-router
1000020+    916      1  0 01:00 ?        00:00:02 /usr/sbin/haproxy -f /var/lib/haproxy/conf/haproxy.config -p /var/lib/haproxy/run/haproxy.pid -x /var/lib/ha
1000020+    923      0  0 01:10 pts/0    00:00:00 /bin/sh
1000020+    929    923  0 01:11 pts/0    00:00:00 ps -ef


sh-4.2$ cat /var/lib/haproxy/conf/haproxy.config
（途中略）
# 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-45m9j:hello-world:172.17.111.9:8080 172.17.111.9:8080 cookie 0bb2b8528eac3ab6386b92c9a7ced00f weight 256 check inter 5000ms
  server pod:hello-world-1-b75zr:hello-world:172.17.115.137:8080 172.17.115.137:8080 cookie 745d3eb2f6eb5e81e282bbef3de8ac38 weight 256 check inter 5000ms
  server pod:hello-world-1-rb8nt:hello-world:172.17.123.76:8080 172.17.123.76:8080 cookie 4505838bc8e37737b8f7b548ee86aaa1 weight 256 check inter 5000ms
  server pod:hello-world-1-j8w4f:hello-world:172.17.67.14:8080 172.17.67.14:8080 cookie 12c420b6de1b2062042a09b0cfdb645c weight 256 check inter 5000ms
  server pod:hello-world-1-gl5sd:hello-world:172.17.74.18:8080 172.17.74.18:8080 cookie e9437ccfd5b296555d9e85b6ed275482 weight 256 check inter 5000ms


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

```