AROでのエグレス（送信）ロックダウンの仕組み

## はじめに
本記事は[OpenShiftアドベントカレンダー(2023年)](https://qiita.com/advent-calendar/2023/openshift)の7日目の記事です。

本エントリでは、Azure Red Hat OpenShift (ARO) で新しく追加されたエグレスロックダウン機能について、その簡単な内容と仕組みを紹介できればと思います。

### AROとは
[ARO](https://learn.microsoft.com/ja-jp/azure/openshift/intro-openshift)はAzure上で展開できるRed Hat OpenShiftのフルマネージドサービスで、AzureサブスクリプションがあればすぐにOpenShiftクラスタを展開して使い始めることができます。
ノードやネットワークなどのインフラリソースからクラスタまでをRed HatとMicrosoftが管理・更新・監視しており、サポートも両社連携して提供されます。
また、Azureのマネージドサービスならではのメリットの一例として、Microsoft Entra ID(旧Azure AD)との認証連携の容易性などがあげられます。

## AROクラスタとネットワーク構成
AROをデプロイすると、クラスタはAzureの仮想ネットワーク(VNet)内に展開されます。
AROのマスターノードとワーカーノードはそれぞれVNet内の各サブネットに配置され、その前段にロードバランサ(LB)が展開されることで、APIアクセスやIngress/Routeアクセスを処理できます。
![aro-network.png](https://learn.microsoft.com/ja-jp/azure/openshift/media/concepts-networking/aro-networking-diagram.png#lightbox)

### プライベートクラスタで閉域構成を実現する
デフォルトではAROクラスタを作成すると、上記のロードバランサにはパブリックIPアドレスが付与されて外部からのアクセスを受け付けます。しかし、要件に応じてパブリックIPアドレスを持たないプライベートクラスタを構成することも可能です。

https://learn.microsoft.com/ja-jp/azure/openshift/howto-create-private-cluster-4x

たとえば、自社の環境やセキュリティ要件でAzureクラウドで原則パブリックIPアドレスの割り当てが禁止されていたり、AROクラスタのクライアントがオンプレ環境にあってAzure環境とはExpressRouteなどの閉域ネットワーク接続経由でのアクセスを想定する場合など、こういった閉域構成が必要になる場合があります。

パブリックIPアドレスを持たない[プライベートクラスタを作成する](https://learn.microsoft.com/ja-jp/azure/openshift/howto-create-private-cluster-4x#create-the-cluster)には、Portalでの作成時にAPI、イングレスの可視性オプションをそれぞれ"非公開"に指定するか、もしくは、az aro createコマンド(CLI)での作成時にオプションとして"--apiserver-visibility Private"および"--ingress-visibility Private"を指定します。

::: note warn
APIもイングレスもプライベートにした場合、ブラウザからのコンソールアクセスもocコマンドも仮想ネットワーク内（もしくは閉域接続しているオンプレ環境等）からしか利用できないので注意してください。
:::

### エグレス（送信）トラフィックの制御
上記は主にLBを経由した __受信__ トラフィックの閉域化についての話でした。
AROクラスタを閉域化する場合、同様に __送信__ トラフィックについてもコントロールするケースが一般的です。これはたとえば、ワーカーノード上で動くユーザのコンテナアプリケーションから外部に通信するケースや、クラスタ基盤の各ノードから更新のためにレポジトリやコンテナレジストリに外部アクセスするケースで考えられます。

なお、デフォルトでは送信トラフィックはどのように扱われるか、というと、上記のネットワーク構成でも言及した各ノードの前段に置くロードバランサがNAT(SNAT)を行うことで、ロードバランサのフロントエンドIPから外部アクセスができるようになっています。
AzureにおけるVMの外部アクセス方法は構成に応じて多岐のパターンに分かれるため、少々複雑に感じるかもしれません。弊社のチームメンバーがわかりやすくblogにまとめてくれていますので、ぜひこちらも参考にしてみてください。

https://jpaztech.github.io/blog/network/snat-options-for-azure-vm/

さて、ここでもしプライベートクラスタを構成するとLBはパブリックIPアドレスを付与されないため、LBからのSNATによる外部接続はできないことになります。
こうしたケースのために、AROクラスタでは各ノードがLB経由のSNATではなく、[ユーザ定義ルート(UDR)](https://learn.microsoft.com/ja-jp/azure/virtual-network/virtual-networks-udr-overview#custom-routes)による明示的なルーティングに従って外部アクセスができる構成オプションが提供されています。
ユーザ定義ルート(UDR)はサブネット単位に割り当て可能なカスタムなルーティングテーブルです。これをマスタノード用サブネットやワーカーノード用サブネットに割り当てて、たとえば送信トラフィックのネクストホップをすべてAzure Firewallに集約することで送信トラフィックを制御する、といったことが実現できます。
このようにエグレス（送信）トラフィックをすべて明示的に管理・制御することをエグレス（送信）ロックダウンと呼んでいます。

### 従来のエグレス（送信）ロックダウンの仕組み
上記のように、AROプライベートクラスタを作成し、送信トラフィックもエグレスロックダウンを構成した場合に、実際どのように送信アクセスを許可・拒否するか、について考えます。
Azure Firewallを例にすると、Azure FirewallはL4（ネットワークルール）、L7（アプリケーションルール）それぞれで通信の許可(Allow)、拒否(Deny)が設定できますので、AROクラスタが必要な外部通信先のIPアドレス、FQDNを明示的に許可し、必要ないその他の通信を拒否することになります。
具体的には、認証のためのEntra ID(Azure AD)、Azure管理プレーン(ARM)、ARO用のコンテナレジストリ、その他必要に応じたBLOBや監視等のサービスへの通信を個別に許可する必要があります。実は、Azureネイティブのマネージドk8sクラスタサービスであるAKSでは、AKS用の外部通信要件が[AKSクラスタ用FQDNタグ](https://learn.microsoft.com/ja-jp/azure/aks/outbound-rules-control-egress)として提供されており、このFQDNタグだけ許可すればよい、という便利な状況がありました。AROはその点、少し手間がかかってしまっていました。

### 新しいエグレス（送信）ロックダウンの仕組み
こうした経緯をふまえ、今年2023年6月の更新でこの管理を大幅に簡素化できる、エグレスロックダウン機能が登場しました。

https://learn.microsoft.com/ja-jp/azure/openshift/concepts-egress-lockdown

要点としては、AROクラスタの各ノードから外部の必要な通信について、VNet内に1つ仮想NICであるPrivate Endpointを配置し、それらの通信はすべてそのPrivate Endpoint経由でエグレス（送信）アクセスができるようなった、というものです。
下図の青く塗られている"Azure Red Hat OpenShift service"というのが概念的には外部通信用プロキシにようにふるまっているとも理解できます。その先にあるコンテナレジストリや管理プレーン(Azure Resource Manager: ARM)へはプロキシ経由でのアクセスが可能です。

![concepts-egress-lockdown.png](https://learn.microsoft.com/ja-jp/azure/openshift/media/concepts-networking/190-azure-red-hat-openshift-network-architecture-0921.png#lightbox)

[Private Endpoint](https://learn.microsoft.com/ja-jp/azure/private-link/private-endpoint-overview)というのは、VNetの外のサービスへアクセスするためにPrivate Linkという"引き込み線"をVNetまで伸ばし、VNet内に作られた仮想NICエンドポイントへのアクセスで外部通信が実現できるAzureの機能です。
仮想NICはVNet内にありますので、従来のように外部アクセスのためのルーティングなどは不要となり、各ノードから直接アクセスが可能となります。

具体的に、どのような接続先(FQDN)がプロキシされるのか、については以下に情報がまとまっています。前述したように、Entra ID（Azure AD）、コンテナレジストリ、ARM等が該当します。言い換えると、これらのアクセスについては従来のようにAzure Firewall経由でなくても閉域接続が可能になっているということです。

https://learn.microsoft.com/ja-jp/azure/openshift/howto-restrict-egress

:::note info
現時点では新規作成したAROプライベートクラスタでは既定でエグレスロックダウンが有効になります。自分のクラスタで確認するには以下のコマンドを実行します。今回検証に利用したクラスタでも実行してみたところ **"Egress Lockdown Feature Enabled"** と出力されました。

```
$ oc get cluster.aro.openshift.io cluster -o go-template='{{ if .spec.gatewayDomains }}{{ "Egress Lockdown Feature Enabled" }}{{ else }}{{ "Egress Lockdown Feature Disabled" }}{{ end }}{{ "\n" }}'
```
:::

## エグレス（送信）ロックダウンの実装を見てみる
かなり前置きが長くなりましたが、本題として、では実際どのようにこの閉域アクセスが実装されているのか、実環境で確認してみたいと思います。

検証した環境は以下の通りです。

| item | version | note |
|---------|---------|----|
| ARO | 4.12 | API/ingressはprivate |

またVNetのアドレスパラメータも記載しておきます。一番下のVM用サブネット以外はAROクラスタをPortalから作成した際のデフォルト値です。

| item | CIDR | note |
|---------|---------|----|
| aro-vnet | 10.0.0.0/16 | 以下subnetを含むVNet |
| master subnet | 10.0.0.0/27 | Masterノード用subnet|
| worker-subnet | 10.0.0.128/25 | Workerノード用subnet |
| vm-subnet | 10.0.1.0/24 | 作業VM用subnet。クラスタ展開後に手動作成 |



AROクラスタデプロイ後にAzure PortalからARO管理用リソースグループのリソース一覧を見てみます。（下図は画面の都合上リソースの一部を添付したものです。）たしかにPrivate Endpointができていました。確認したところ、IPアドレスは10.0.0.5となっており、master-subnet内に仮想NICとして展開されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51832/a1d76cf1-c7c1-d90e-d10a-9926033a142b.png)

その他のIPアドレスも簡単に整理します。わかりやすいように名前は一部省略・簡略化しています。

| item | ip address | note |
|---------|---------|----|
| master0-nic | 10.0.0.8 | |
| master1-nic | 10.0.0.7 | |
| master2-nic | 10.0.0.10 | |
| internal LB | 10.0.0.4 | master用LB |
| worker0-nic | 10.0.0.134 | |
| worker1-nic | 10.0.0.132 | |
| worker2-nic | 10.0.0.133 | |
| ingress LB | 10.0.0.254 | worker用LB |
| egress PE | 10.0.0.5 | 外部送信用Private Endpoint ★今回の主役★ |
| sre-pls | 10.0.0.6 | SRE用PrivateLinkService (for RH/MS) *1 |
| oper-win | 10.0.1.4 | 作業用VM(Windows) |
| oper-ubu | 10.0.1.5 | 作業用VM(Linux) |

:::note info
SRE用PrivateLinkServiceは、マネージドサービスプロバイダであるMicrosoft/Red Hatがメンテナンス等の目的でAROクラスタにアクセスするために既定で構成されるリンクネットワークです。本エントリでは詳細な説明は割愛します。
:::

ここまで調べて、なるほど、各ノード(master,worker)から指定の外部接続については、プロキシであるPrivate Endpoint(10.0.0.5)にアクセスすればよい。実際にはIPアドレス直打ちではなく、FQDNアクセスをする過程で名前解決ができればよいのだな、と推測をしました。

### Private DNSゾーンを探すが・・・
通常よくある構成パターンとして、Private Endpointの名前解決にはPrivate DNSゾーンを利用します。

https://learn.microsoft.com/ja-jp/azure/private-link/private-endpoint-dns

詳細は省きますが、VNet内でのみ利用できる名前解決手段として、Private DNSゾーンを作成し、たとえば、管理プレーン(ARM)へのアクセスURLである"management.azure.com"を"10.0.0.5"に解決できるようエントリを構成し、そのゾーンを当該VNet(aro-vnet)にリンクすることでVNet内からはこのゾーンによる名前解決が可能になります。

しかし、実際にリソースグループを探してみましたが、Private DNSゾーンはどこにも作られていないことがわかりました。

ん？ではどうやってPrivate Endpointへアクセスできるのだろう？(management.azure.comへアクセスするとそのままパブリックIPへ接続してしまうのでは？）と疑問がわきました。
実際にaro-vnet内に作業用VMを展開して、nslookupで名前解決を試してみます。

::: note info
VNet内から名前解決を行うと既定で168.63.129.16というIPアドレスを持つ、[Azureにより提供されるDNSサーバ]((https://learn.microsoft.com/ja-jp/azure/virtual-network/virtual-networks-name-resolution-for-vms-and-role-instances?tabs=redhat))に問い合わせが行われます。
:::

```
PS C:\Users\sodo> nslookup management.azure.com
Server:  UnKnown
Address:  168.63.129.16

Non-authoritative answer:
Name:    arm-fdweb.eastus.cloudapp.azure.com
Address:  20.62.130.212
Aliases:  management.azure.com
          management.privatelink.azure.com
          arm-frontdoor-edge-geo.trafficmanager.net
          eastus.management.azure.com
          arm-frontdoor-eastus.trafficmanager.net
          eastus.web.management.azure.com
```

やはりパブリックIP(20.62.130.212)へ解決されてしまいます。これが10.0.0.5に解決されることを期待していたのですが、Private DNSゾーンが構成されていないため、この動きはおかしくありません。

### master/workerノード内部での名前解決
次に、oc debug nodeコマンドで実際にmaster/workerノードへアクセスして、同じ名前解決を試してみます。(ノードに入った後で chroot /host を実行しています）
先ほどとは異なり、OpenShiftではmaster/workerノード上の名前解決はローカルホストの53番ポートで名前解決が行われます。

```
sodo@arovm02:~$ oc debug node/sodoaro1130-mk7w2-worker-eastus1-f79st
Temporary namespace openshift-debug-pw6q4 is created for debugging node...
Starting pod/sodoaro1130-mk7w2-worker-eastus1-f79st-debug-hq4s7 ...
To use host binaries, run `chroot /host`
Pod IP: 10.0.0.134
If you don't see a command prompt, try pressing enter.
sh-4.4# chroot /host
sh-4.4# nslookup management.azure.com
Server:10.0.0.134
Address:10.0.0.134#53

Name:management.azure.com
Address: 10.0.0.5

sh-4.4# 
```

OpenShiftのノードからであれば、期待した通りにPrivate Endpoint(10.0.0.5)に名前解決ができることがわかりました。(上記はworkerノードですが、masterノードでも同様）

これはおそらく、AROクラスタ内もしくはノード内で追加の名前解決の仕組みが構成されているという仮説を立てて、これについてもう少し調べてみます。
すると、ノードローカルのdnsmasqの設定で、直接名前解決が定義されていることがわかりました。以下、workerノードでの確認結果です。(masterノードでも同様でした)

```
sodo@arovm02:~$ oc debug node/sodoaro1130-mk7w2-worker-eastus1-f79st
Temporary namespace openshift-debug-f48j7 is created for debugging node...
Starting pod/sodoaro1130-mk7w2-worker-eastus1-f79st-debug-r2kt4 ...
To use host binaries, run `chroot /host`
Pod IP: 10.0.0.134
If you don't see a command prompt, try pressing enter.
sh-4.4# chroot /host
sh-4.4# cat /etc/dnsmasq.conf 

resolv-file=/etc/resolv.conf.dnsmasq
strict-order
address=/api.sodoaro1130.eastus.aroapp.io/10.0.0.4
address=/api-int.sodoaro1130.eastus.aroapp.io/10.0.0.4
address=/.apps.sodoaro1130.eastus.aroapp.io/10.0.0.254
address=/agentimagestorewus01.blob.core.windows.net/10.0.0.5
address=/agentimagestorecus01.blob.core.windows.net/10.0.0.5
address=/agentimagestoreeus01.blob.core.windows.net/10.0.0.5
address=/agentimagestoreweu01.blob.core.windows.net/10.0.0.5
address=/agentimagestoreeas01.blob.core.windows.net/10.0.0.5
address=/eastus-shared.prod.warm.ingest.monitor.core.windows.net/10.0.0.5
address=/gcs.prod.monitoring.core.windows.net/10.0.0.5
address=/gsm1318942586eh.servicebus.windows.net/10.0.0.5
address=/gsm1318942586xt.blob.core.windows.net/10.0.0.5
address=/gsm1580628551eh.servicebus.windows.net/10.0.0.5
address=/gsm1580628551xt.blob.core.windows.net/10.0.0.5
address=/gsm479052001eh.servicebus.windows.net/10.0.0.5
address=/gsm479052001xt.blob.core.windows.net/10.0.0.5
address=/maupdateaccount.blob.core.windows.net/10.0.0.5
address=/maupdateaccount2.blob.core.windows.net/10.0.0.5
address=/maupdateaccount3.blob.core.windows.net/10.0.0.5
address=/maupdateaccount4.blob.core.windows.net/10.0.0.5
address=/production.diagnostics.monitoring.core.windows.net/10.0.0.5
address=/qos.prod.warm.ingest.monitor.core.windows.net/10.0.0.5
address=/login.microsoftonline.com/10.0.0.5
address=/management.azure.com/10.0.0.5
address=/arosvc.azurecr.io/10.0.0.5
address=/arosvc.eastus.data.azurecr.io/10.0.0.5
address=/imageregistry6nfjs.blob.core.windows.net/10.0.0.5
user=dnsmasq
group=dnsmasq
no-hosts
cache-size=0
sh-4.4# 
```

この内容を見ると、以下のように名前解決が定義されていることが確認できます。

- 定義された外部サービス(ARMなど)はPrivate Endpoint(10.0.0.5)に名前解決
- APIアクセス(api)はAPI用LB(10.0.0.4)に名前解決
- ingressアクセス(*.apps)はingress用LB(10.0.0.254)に名前解決

それ以外のcluster.localや外部アドレスについてはOpenShift内部DNSの仕組みに則っているものと考えます。Red Hat織さんのブログが参考になります。
なお、ブログではCNIプラグインはOpenShift-SDNでしたが、現バージョンのARO(OpenShift)ではデフォルトのCNIはOVN-Kubernetesなので細かい差異はあるかと思いますが、名前解決の優先度などは変わってないはずです。

https://rheb.hatenablog.com/entry/dnsmasq-and-gdb

## わかったこと
ここまでかなり長くなってしまいましたので、今回わかったことをふまえて以下に整理します。

- AROでは閉域利用のためのプライベートクラスタ構成が取れる
- API/ingressともにプライベート化するとそれぞれのLBにパブリックIPが付与されず、LBを介したSNAT外部疎通はできない。このため従来は、LB経由ではなくユーザ定義ルート(UDR)で外部通信経路を構成する必要があった
- 2023年6月のAROの更新により、プライベートクラスタではエグレスロックダウンが既定で有効化され、基盤側で必要な外部通信(ARMやEntra ID等)についてはPrivate Endpoint経由で直接送信できるようになった
- 通信先FQDNをPrivate EndpointのIPに名前解決する仕組みが必要となるが、特にユーザが設定を行う必要はなく、実装としてmaster/workerノード内部でdnsmasqの設定が直接定義されていることが確認できた

補足すると、基盤側による外部通信だけでなく、ユーザワークロードなども外部通信を行いますが、従来のUDR+Azure Firewallによるエグレストラフィック管理ができなくなったわけではありません。
その際、基盤送信トラフィックは今回の方式でPrivate Endpointへ通信を行い、それ以外のユーザ要件による外部通信はユーザ管理の責務範囲でUDR+Azure Firewallを利用した外部通信を行う、というように2つの方式を併用する形になるでしょう。
ある意味、基盤側はマネージドの仕組みで、ユーザ側はユーザ管理の範囲で、と責任分界点が明確になったともいえるかと思います。

## まとめ
本エントリでは、AzureのマネージドOpenShiftサービスAROで閉域構成をとる際のエグレス（送信）トラフィックのロックダウンの仕組みについて紹介しました。
詳細については、エントリ内の各リンクURLも参考にしてください。