Red Hat OpenShift on IBM Cloud(VPC): Private Routerを作成してみる

#1. はじめに
Red Hat OpenShift on IBM Cloud(VPC)では、以下の2つのオプションがあります。
https://cloud.ibm.com/docs/openshift?topic=openshift-plan_clusters#vpc-workeruser-master

- Public and private service endpoints
- Private service endpoint only

IBM Cloud Portalからクラスターを作成した場合は"Public and private service endpoints"になり、デフォルトでPublic Router（Public NWからからアクセスできるルーター）が構成されています。でも、Direct LinkなどPrivate NW経由でアクセスしたい場合はPrivate Routerが必要になるため、別途作成が必要になります。この記事では、Private Routerの作成方法について以下のドキュメントを参照しながら試してみます。
https://cloud.ibm.com/docs/openshift?topic=openshift-openshift_routes#private-routes


#2. 初期状態の確認

以下はPublic Routerの構成情報です。
`default`というIngress Controllerによってrouter-defaultというPublic向けのRouterが構成されています。

```shell-session:構成前情報
$ oc get ingresscontroller --all-namespaces
NAMESPACE                    NAME      AGE
openshift-ingress-operator   default   13d

$ oc get svc,pods,route -n openshift-ingress
NAME                              TYPE           CLUSTER-IP       EXTERNAL-IP                            PORT(S)                      AGE
service/router-default            LoadBalancer   172.21.164.127   xxxxxxxx-us-south.lb.appdomain.cloud   80:32005/TCP,443:32360/TCP   12d
service/router-internal-default   ClusterIP      172.21.72.253    <none>                                 80/TCP,443/TCP,1936/TCP      12d

NAME                                  READY   STATUS    RESTARTS   AGE
pod/router-default-69cbd48fc7-4pwgx   1/1     Running   0          2d17h
pod/router-default-69cbd48fc7-h55kx   1/1     Running   0          2d17h

NAME                                      HOST/PORT                                                                                                   PATH       SERVICES                  PORT   TERMINATION   WILDCARD
route.route.openshift.io/router-default   router-default.myroksclustervpc-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-0000.us-south.containers.appdomain.cloud   /healthz   router-internal-default   1936                 None
```

#3. Private Routerを作成する

OpenShift 4.xのRouterは、Ingress Controllerによって作成・管理されています。いったんRouterが作成されてしまえば、HTTP/HTTPSのトラフィック処理はRouterによって実施されるため、Ingress Controllerが直接関与することはありません。
ここでは、[IBM Cloud docs](https://cloud.ibm.com/docs/openshift?topic=openshift-openshift_routes#private-routes)に従って、Private Routerを作成・管理するIngress Controllerを作成します。


```yaml:privateIngressController.yaml
apiVersion: operator.openshift.io/v1
kind: IngressController
metadata:
  name: private
  namespace: openshift-ingress-operator
spec:
  replicas: 2
  domain: roksvpc.internal
  endpointPublishingStrategy:
    loadBalancer:
      scope: Internal
    type: LoadBalancerService
```

ちなみに、spec.endpointPublishingStrategy.loadBalancer.scope=Internalとなっているところで、Private NWのみに公開する旨の指定をしています。[ソースコードでの説明はこちら](https://github.com/openshift/api/blob/release-4.3/operator/v1/types_ingress.go#L194)。
ドメインはPrivate NW用に取得しておきます。本番環境では[ここ](https://qiita.com/testnin2/items/aab8411c69d8950de3bb)などを参照して、VPC用のPrivate DNSのゾーンを作成しておくと良いでしょう。
**ただし、2020年7月時点において、VPCのPrivate DNSには[CNAMEに対するワイルドカードは指定できないという制約がある](https://cloud.ibm.com/docs/dns-svcs?topic=dns-svcs-what-are-wildcard-records#wildcard-limitations)ので注意してください。**

以下、このマニフェストファイルを実行します。`oc patch`部分は本来は要らないのですが、現在のRed Hat OpenShift on IBM Cloud(VPC)のバグであり、本来は`externalTrafficPolicy=Local`が正しい仕様だということなので変更しています。

```shell-session
$ oc apply -f privateIngressController.yaml
$ oc patch service router-private -n openshift-ingress -p '{"spec":{"externalTrafficPolicy":"Cluster"}}'

$ oc get ingresscontroller --all-namespaces
NAMESPACE                    NAME      AGE
openshift-ingress-operator   default   13d
openshift-ingress-operator   private   56s

$ oc get svc,pods,route -n openshift-ingress
NAME                              TYPE           CLUSTER-IP       EXTERNAL-IP                            PORT(S)                      AGE
service/router-default            LoadBalancer   172.21.164.127   xxxxxxxx-us-south.lb.appdomain.cloud   80:32005/TCP,443:32360/TCP   13d
service/router-internal-default   ClusterIP      172.21.72.253    <none>                                 80/TCP,443/TCP,1936/TCP      13d
service/router-internal-private   ClusterIP      172.21.53.252    <none>                                 80/TCP,443/TCP,1936/TCP      29s
service/router-private            LoadBalancer   172.21.240.98    yyyyyyyy-us-south.lb.appdomain.cloud   80:31050/TCP,443:31260/TCP   29s

NAME                                  READY   STATUS    RESTARTS   AGE
pod/router-default-69cbd48fc7-4pwgx   1/1     Running   0          3d
pod/router-default-69cbd48fc7-h55kx   1/1     Running   0          3d
pod/router-private-56cb4748c6-gx8vn   1/1     Running   0          29s
pod/router-private-56cb4748c6-tg7b6   1/1     Running   0          29s

NAME                                      HOST/PORT                                                                                                              PATH       SERVICES                  PORT   TERMINATION   WILDCARD
route.route.openshift.io/router-default   router-default.myroksclustervpc-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-0000.us-south.containers.appdomain.cloud ... 1 more   /healthz   router-internal-default   1936                 None

```

また、このマニフェストファイルの実行により、Private-To-PrivateのVPC Load Balancerも作成されており、NodePort経由で`router-private` Podにトラフィックを転送可能になりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62127/8e624068-88a3-ca1b-0617-6ae691eb19a2.png)


#4. Routeの作成

既存のServiceをexposeしてRouteを作成します。

```
$ oc expose service hello-world --hostname www.roksvpc.com

$ oc get route
NAME          HOST/PORT                    PATH   SERVICES      PORT       TERMINATION   WILDCARD
hello-world   www.roksvpc.com ... 1 more          hello-world   8080-tcp                 None
```

#5. 検証

今回は面倒（？）だったのでこんなやり方で確認していますが、本来は`xxxxxxxx-us-south.lb.appdomain.cloud`とか`yyyyyyyy-us-south.lb.appdomain.cloud`はCNAMEとして登録しておくべきものです。



```shell-session:VPC上のVSIから検証
$ oc get service -n openshift-ingress
NAME                      TYPE           CLUSTER-IP       EXTERNAL-IP                            PORT(S)                      AGE
router-default            LoadBalancer   172.21.164.127   xxxxxxxx-us-south.lb.appdomain.cloud   80:32005/TCP,443:32360/TCP   13d
router-internal-default   ClusterIP      172.21.72.253    <none>                                 80/TCP,443/TCP,1936/TCP      13d
router-internal-private   ClusterIP      172.21.53.252    <none>                                 80/TCP,443/TCP,1936/TCP      19h
router-private            LoadBalancer   172.21.240.98    yyyyyyyy-us-south.lb.appdomain.cloud   80:31050/TCP,443:31260/TCP   19h


$  host xxxxxxxx-us-south.lb.appdomain.cloud
xxxxxxxx-us-south.lb.appdomain.cloud has address 52.116.xx.xx
xxxxxxxx-us-south.lb.appdomain.cloud has address 52.116.xx.xx

$  host yyyyyyyy-us-south.lb.appdomain.cloud
yyyyyyyy-us-south.lb.appdomain.cloud has address 10.240.64.15
yyyyyyyy-us-south.lb.appdomain.cloud has address 10.240.128.14

#Public NW経由でのアクセス
$ curl -H "Host: www.roksvpc.com" xxxxxxxx-us-south.lb.appdomain.cloud
Hello world from hello-world-1-h8zcz! Your app is up and running in a cluster!

#Private NW経由でのアクセス
$ curl -H "Host: www.roksvpc.com" yyyyyyyy-us-south.lb.appdomain.cloud
Hello world from hello-world-1-h8zcz! Your app is up and running in a cluster!
```

`www.roksvpc.com`というRequested Hostに対して、`router default`も`router private`のどちらに対してもこのRouteは公開されています。実際に今回作成したRouteをよくみてみると、`www.roksvpc.com`というRequested Hostに対して、`router-default`も`router-private`も登録されていることがわかります。

```
$ oc describe route hello-world
Name:			hello-world
Namespace:		syasuda
Created:		25 seconds ago
Labels:			app=hello-world
			app.kubernetes.io/component=hello-world
			app.kubernetes.io/instance=hello-world
Annotations:		<none>
Requested Host:		www.roksvpc.com
			  exposed on router default (host myroksclustervpc-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-0000.us-south.containers.appdomain.cloud) 25 seconds ago
			  exposed on router private (host roksvpc.internal) 26 seconds ago
Path:			<none>
TLS Termination:	<none>
Insecure Policy:	<none>
Endpoint Port:		8080-tcp

Service:	hello-world
Weight:		100 (100%)
Endpoints:	172.17.103.76:8080, 172.17.79.136:8080, 172.17.79.147:8080 + 2 more...
```

これは、実際に各々のRouterにログインしてみてもわかります。

```shell-session
$ oc rsh -n openshift-ingress router-default-69cbd48fc7-4pwgx cat /var/lib/haproxy/conf/haproxy.config|grep hello-world
backend be_http:syasuda:hello-world
  server pod:hello-world-1-h8zcz:hello-world:172.17.103.76:8080 172.17.103.76:8080 cookie 2c590602f05c7e43ec6a6efc7c60e153 weight 256 check inter 5000ms
  server pod:hello-world-1-b8ng9:hello-world:172.17.79.136:8080 172.17.79.136:8080 cookie dc09a373d3e3bfc84663d80ccf6fce9e weight 256 check inter 5000ms
  server pod:hello-world-1-mtpzt:hello-world:172.17.79.147:8080 172.17.79.147:8080 cookie 6001d244ba91955a14d807b3842e2f69 weight 256 check inter 5000ms
  server pod:hello-world-1-hlnq9:hello-world:172.17.88.134:8080 172.17.88.134:8080 cookie 08c0e5cfd626696a46123636974171bb weight 256 check inter 5000ms
  server pod:hello-world-1-hbr8h:hello-world:172.17.88.139:8080 172.17.88.139:8080 cookie 8cd637af1c6bd737923b5b35deda115b weight 256 check inter 5000ms

$ oc rsh -n openshift-ingress router-default-69cbd48fc7-4pwgx cat /var/lib/haproxy/conf/os_http_be.map|grep hello
^www\.roksvpc\.com(:[0-9]+)?(/.*)?$ be_http:syasuda:hello-world

$ oc rsh -n openshift-ingress router-private-56cb4748c6-gx8vn cat /var/lib/haproxy/conf/haproxy.config|grep hello-world
backend be_http:syasuda:hello-world
  server pod:hello-world-1-h8zcz:hello-world:172.17.103.76:8080 172.17.103.76:8080 cookie 2c590602f05c7e43ec6a6efc7c60e153 weight 256 check inter 5000ms
  server pod:hello-world-1-b8ng9:hello-world:172.17.79.136:8080 172.17.79.136:8080 cookie dc09a373d3e3bfc84663d80ccf6fce9e weight 256 check inter 5000ms
  server pod:hello-world-1-mtpzt:hello-world:172.17.79.147:8080 172.17.79.147:8080 cookie 6001d244ba91955a14d807b3842e2f69 weight 256 check inter 5000ms
  server pod:hello-world-1-hlnq9:hello-world:172.17.88.134:8080 172.17.88.134:8080 cookie 08c0e5cfd626696a46123636974171bb weight 256 check inter 5000ms
  server pod:hello-world-1-hbr8h:hello-world:172.17.88.139:8080 172.17.88.139:8080 cookie 8cd637af1c6bd737923b5b35deda115b weight 256 check inter 5000ms

$ oc rsh -n openshift-ingress router-private-56cb4748c6-gx8vn cat /var/lib/haproxy/conf/os_http_be.map|grep hello
^www\.roksvpc\.com(:[0-9]+)?(/.*)?$ be_http:syasuda:hello-world

```

#6. Routerのシャーディング

つまり、Routeはこのままだと全てのRouter公開されてしまっています。もし「必ずこのRouteはこちらのRouterにしか公開したく無い！」というのであれば、RouterのShading機能を使うとよいでしょう。

- https://rcarrata.com/openshift/ocp4_route_sharding/
- https://access.redhat.com/documentation/ja-jp/openshift_container_platform/4.5/html/networking/nw-ingress-sharding_configuring-ingress

要は、特定のRouteやNamespaceにラベルを付けて、それをSelectorで検索して合致したもののみをRouteとして登録するというやり方です。でもどういう方法がスマートなんでしょうね？

- もともとprivate NW側にしか公開する意図がないアプリケーションにroute-default経由でアクセスする際には、Hostヘッダにそのprivate側で構成しているFQDNを指定する必要があるので、そのFQDN情報が知られていなければ直ちに脅威になるものではないと思われる。
- 一般的には、public公開しているサービスはprivate側からアクセスできても差し支えがないことが多いので、不用意に外部には公開したくないために構成したいというケースの方が多いと思われる。よってrouter-defaultにRoute Selector/NameSpace Selectorを付けて、マッチングしたRoute以外は外部に公開しないのが本来のあるべき姿のように思える。この場合、既存の（システムが作成した）Route/NameSpaceのラベルを変更・管理する必要がある。

```yaml:route-default
namespaceSelector: 
    matchExpressions:
    - key: router
      operator: NotIn 
      values:
      - private
```
```shell-session:namespaceにlabelを設定する例
$ oc label namespace <project> "router=private"
```