IBM Kubernetes Service (IKS) でIPアドレス制限を実装する

# はじめに
IBM Kubernetes Service（以降IKSとする）でデプロイしているアプリにIPアドレス制限（アプリに対し特定のIPアドレスを持つユーザーのみからのアクセスを許可する）の要望があり検証を行ったため、手順等をQiitaにまとめてみました。
参考程度に読んでいただければと思います。

## IKS検証環境
バージョン1.24.10
クラシック・クラスター（VPCクラスターの場合少し手順が異なるかと思いますのでご注意ください）

## 前提
* IBM CloudにてIKSクラスターが作成されていること
* IKSクラスター内にIPアドレス制限を実装したいアプリがデプロイされていること(参考:[CLI でアプリをデプロイする方法](https://cloud.ibm.com/docs/containers?topic=containers-deploy_app#app_cli))。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411784/301ca1f3-3649-8c3d-9c03-e422ae094fdb.png)


## 概要
IKSでデプロイしたアプリに対してIPアドレス制限を実装する手段はいくつかあると思いますが、今回自分が試したのは以下の2つです。

1. ServiceでのIPアドレス制限
2. IngressでのIPアドレス制限
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411784/f510ec19-c6ef-a8fd-913a-152b2037c67f.png)


まず前提として、アプリはロードバランサーで設定を行うことでIP制限を実装することができます。
IKSではロード・バランサーをServiceかIngressで実装することが可能です。
Serviceでロード・バランサーを実装する場合はネットワーク・ロード・バランサー（NLB）、Ingressでロード・バランサーを実装する場合はアプリケーション・ロード・バランサー(ALB)になります。
よって、IKSでIPアドレス制限を実装する手段は2つあることになります。
参考： [アプリの公開サービスの選択](https://cloud.ibm.com/docs/containers?topic=containers-cs_network_planning)

どちらの手段を取るかについては、ほぼイコールでどちらでロード・バランサーを実装するかという判断になります。
ルーティングルールをカスタマイズしないのであれば「1.ServiceでのIPアドレス制限」を選択可能ですが、Ingressを使用してルーティングルールを実装する（アプリをhttps化したい・エラー画面を表示させたいなどの場合）は「2.IngressでのIPアドレス制限」での実装になります。
どちらを取るか迷われた場合は基本的に「2.IngressでのIPアドレス制限」で実装する方針で問題ないかと思います。

ご自身のアプリの要件に沿って実装方針を決め、それぞれの手順を踏んでください。

# 事前準備
* デプロイに必要なIBM Cloud CLIとKubernetes CLIのセットアップを完了させる(参考：[CLI のセットアップ](https://cloud.ibm.com/docs/containers?topic=containers-cs_cli_install#cs_cli_install_steps))
* アクセスを許可するIPアドレスとレンジを決めておく

# 1. ServiceでのIPアドレス制限
まずはServiceでのIPアドレス制限を実装する手順についてです。
こちらは[公式のドキュメント](https://cloud.ibm.com/docs/containers?topic=containers-loadbalancer#lb_config)にセットアップ手順が書いてあるので、こちらに沿って実装していきます。

## 手順概要
① アカウントでプロビジョンされているサブネットの確認
② NLBにてIP制限をする設定を組み込んだServiceを作成する
③ Serviceが作成されたことを確認する
④ IPアドレス制限が機能していることを確認する

## ① アカウントでプロビジョンされているサブネットの確認
以下のコマンドを実行します。
<クラスターID>としている箇所はご自身のクラスターのIDに置き換えた上で実行をしてください。
```
$ ibmcloud ks cluster get --cluster <クラスターID> --show-resources
```

出力内容からサブネット情報を確認します。
```
$ ibmcloud ks cluster get --cluster <クラスターID> --show-resources
クラスター <クラスターID> とそのすべてのリソースを取得中...
OK

名前:                                         sample-cluster
ID:                                           <クラスターID>
状態:                                         normal
状況:                                         All Workers Normal
作成日:                                       2022-07-06T09:44:43+0000
ロケーション:                                 tok02
...
サブネット VLAN //以下の値を参照する
VLAN ID   サブネット CIDR      パブリック   ユーザー管理
2234945   xxx.xxx.xxx.xxx/xx   false        false
2234943   xxx.xxx.xxx.xxx/xx   true         false 
```

## ② NLBにてIP制限をする設定を組み込んだServiceを作成する
ドキュメントを参考に、以下のようなyamlファイルを作成します。(名前はmynlb.yamlとします)
```mynlb.yaml
apiVersion: v1
kind: Service
metadata:
  name: myloadbalancer
  annotations:
    service.kubernetes.io/ibm-load-balancer-cloud-provider-ip-type: <public_or_private>
    service.kubernetes.io/ibm-load-balancer-cloud-provider-vlan: "<vlan_id>"
spec:
  type: LoadBalancer
  loadBalancerSourceRanges: 
    - "<アクセスを許可するIPアドレスのレンジ>"
  selector:
    <selector_key>: <selector_value>
  ports:
   - protocol: TCP
     port: <port>
     targetPort: <port> # Optional. By default, the `targetPort` is set to match the `port` value unless specified otherwise.
```
置き換えが必要な設定値の説明は以下の通りです。
- service.kubernetes.io/ibm-load-balancer-cloud-provider-ip-type
    NLBのタイプをprivateまたはpublicから選択します。今回サービスはインターネットからのアクセスが想定されるためpublicを選択します。
- service.kubernetes.io/ibm-load-balancer-cloud-provider-vlan
   前の手順で出力さえた情報のうち、publicのVLAN IDを記載します。
- loadBalancerSourceRanges
   アクセスを許可するIPアドレスのレンジを記載します。
- selector
    アプリをデプロイしたyamlファイルの spec.template.metadata.labels セクションで使用した、ラベル・キー（＜selector_key＞）と値 (＜selector_value＞)を記載します。
- port
  Serviceがlistenするportの番号です。アプリをデプロイしたyamlファイルの、spec.containers.portsセクションのcontainerPortの値を記載します。

例として、port 3000を指定してデプロイしたアプリ「app: sample-app」に対するServiceが「111.222.333.0/24」のIPアドレスのレンジからのアクセスを許可するyamlファイルは以下のようになります。

```mynlb.yaml
apiVersion: v1
kind: Service
metadata:
  name: myloadbalancer
  annotations:
    service.kubernetes.io/ibm-load-balancer-cloud-provider-ip-type: publicc
    service.kubernetes.io/ibm-load-balancer-cloud-provider-vlan: "2234943"
spec:
  type: LoadBalancer
  loadBalancerSourceRanges: 
    - "１１１.222.333.0/24"
  selector:
    app: sample-app
  ports:
   - protocol: TCP
     port: 3000
     targetPort: 3000 
```
※ IPアドレスを複数記載する場合は文頭にハイフン(-)をつけた上で改行し追記してください。
yamlファイルができたら以下コマンドにてServiceを作成します。
```
$ kubectl apply -f mynlb.yaml
```

## ③ Serviceが作成されたことを確認する
NLB サービスが正常に作成されたことを確認します。 サービスが作成され、アプリが利用可能になるまでに数分かかることがあります。
```
$ kubectl describe service myloadbalancer
```
実行すると以下のような出力があります。
出力内容から、EventにてServiceの作成が完了しているか、yamlにて設定した内容が反映されているかを確認してください。
```
$ kubectl describe service myloadbalancer
NAME:                   myloadbalancer
Namespace:              default
Labels:                 <none>
Selector:               app=sample-app
Type:                   LoadBalancer
Location:               tok02
IP:                     172.21.xxx.xxx
LoadBalancer Ingress:   169.xx.xxx.xxx
Port:                   <unset> 3000/TCP
NodePort:               <unset> 32040/TCP
Endpoints:              172.30.xxx.xxx:8080
Session Affinity:       None
External Traffic Policy:     Cluster
LoadBalancer Source Ranges:  111.222.333.0/24
Events:
    FirstSeen    LastSeen    Count    From            SubObjectPath    Type     Reason                      Message
    ---------    --------    -----    ----            -------------    ----     ------                      -------
    10s            10s            1        {service-controller }      Normal CreatingLoadBalancer    Creating load balancer
    10s            10s            1        {service-controller }        Normal CreatedLoadBalancer    Created load balancer
```

## ④ IPアドレス制限が機能していることを確認する
yamlファイルにてloadBalancerSourceRangesに記載したアクセスを許可するIPアドレスからアプリにアクセスし(前手順にて出力された"LoadBalancer Ingress"のIPアドレスからアクセスできます)、アクセスができることを確認してください。

続いてloadBalancerSourceRangesに記載されていないIPアドレスからアプリにアクセスすると、接続が拒否されアプリにアクセスができないことを確認してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411784/c3422f06-6c91-79fe-c062-ca1040f13603.png)

手順は以上です。

## 補足
EKSでの話のようですが、[こちらの記事](https://blog.kumano-te.com/activities/set-ip-whitelist-to-eks-nlb-service)にて「設定可能なIPアドレスは60件までで、それ以上を設定すると機能しなくなる」との記載がありましたので、たくさんIPアドレスを設定する場合はご注意ください。

# 2. IngressでのIPアドレス制限
こちらの手順についてはIKSのドキュメントに記載がないため、[こちらのEKSでの実装に関する記事](https://kazkokk.hatenablog.com/entry/2021/06/14/220548)を参考させていただき検証を行いました。

## 事前準備
事前準備としてアプリに接続するためのServiceと、IPアドレス制限前のIngressを作成します。
すでに作成済みのものがあれば、こちらは飛ばして問題ありません。

### Serviceの作成
以下のようなService作成用のyamlファイルを作成します。
```myservice.yaml
apiVersion: v1
kind: Service
metadata:
  name: myservice
spec:
  type: ClusterIP
  selector:
    <selector_key>: <selector_value>
  ports:
   - protocol: TCP
     port: <port>
     targetPort: <port> # Optional. By default, the `targetPort` is set to match the `port` value unless specified otherwise.
```
置き換えが必要な設定値の説明は以下の通りです。
- selector
    アプリをデプロイしたyamlファイルの spec.template.metadata.labels セクションで使用した、ラベル・キー（＜selector_key＞）と値 (＜selector_value＞)を記載します。
- port
  Serviceがlistenするportの番号です。アプリをデプロイしたyamlファイルの、spec.containers.portsセクションのcontainerPortの値を記載します。

「1. ServiceでのIPアドレス制限」で作成したServiceと異なる点は以下の2点です。
* NLBの設定項目が削除されたこと
* Ingressと接続するため、Serviceタイプが"ClusterIP"になっていること


yamlを作成したら、以下のコマンドを実行しSeriviceを作成してください。
```
$ kubectl apply -f myservice.yaml
```

### Ingressの作成
以下のようなIngress作成用のyamlファイルを作成します。（今回はパブリックALBを前提として定義します）
```myingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: myingress
  annotations:
    kubernetes.io/ingress.class: "public-iks-k8s-nginx"
spec:
  tls:
  - hosts:
    - <ドメイン名>
    secretName: <TLSシークレット名>
  - host: <ドメイン名>
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service: 
            name: myservice
            port:
              number: 3000
```
置き換えが必要な設定値の説明は以下の通りです。（詳細は[こちら](https://cloud.ibm.com/docs/containers?topic=containers-managed-ingress-setup)）
* tls.hosts/host
IBM提供のIngressサブドメインまたはカスタム・ドメインで置き換えます。

* tls.secretName
TLS証明書が保管されている Kubernetes シークレットの名前に置き換えます。

yamlを作成したら、以下のコマンドを実行しIngressを作成してください。(現時点のIngressを作成するとアプリが制限なく公開されるのでご注意)
```
$ kubectl apply -f myingress.yaml
```

myingress.yaml内のhostに定義したドメインにアクセスし、アプリにアクセスできるか確認してください。


## 手順概要
① アクセス元のIPアドレスを保持する設定にALBを変更する
② IPアドレス制限設定を行う
③ IPアドレス制限が機能していることを確認する

## ① アクセス元のIPアドレスを保持する設定にALBを変更する
こちらの手順に関しては[IKSのドキュメント](https://cloud.ibm.com/docs/containers?topic=containers-comm-ingress-annotations#preserve_source_ip_classic)に記載があるため、そちらに沿って設定変更を行います。

### 前動作確認
まず、ALBにてIPアドレス制限をかける場合はアクセス元のIPアドレスは保持する必要がありますが、デフォルトではALBはアクセス元のIPアドレスは保持されません。
よってIKSにデプロイしたアプリ側のバックエンドでリクエストヘッダー内の'x-forwarded-for'を出力すると、実際のアクセス元のIPアドレスではなく、アプリがデプロイされているワーカー・ノードのIPアドレスが表示されます。

以下node.jsのバックエンドサーバーにてIPアドレスを出力する例です。
```server.js
app.use((req, res, next) => {
  const ip = req.headers['x-forwarded-for']||'null';
  console.log('IP : ' + ip) //ワーカー・ノードのIPアドレスが出力される
  return next()
})
```

アプリで出力させるのが難しい場合は、ALB Podのログから確認することも可能です。
ALB Podのログを確認するには、まず以下のコマンドにてALB PodのIDを確認します。

```
$ kubectl get pods -n kube-system | grep alb
```
続いて、以下のコマンドにて対象のALB Podのログを出力します(コマンドはログの最後20行を出力させるものとなっています)。
＜ALB_pod_ID＞となっている箇所は前手順で確認したALB PodのIDに置き換えてください。
```
$ kubectl logs --tail=20 <ALB_pod_ID> nginx-ingress -n kube-system
```

これらを本来のアクセス元のIPアドレスを保持させたいとなった場合は、ALBの設定変更が必要となります。
設定変更前のIPアドレスの受け渡し状況を確認したら、本題の設定変更に移ります。

### ALBの設定変更
今回は単一のALBのアクセス元のIPアドレスを保持をセットアップします。
ALBの設定変更をするために、まず対象となるALBのIDを取得します。
```
$ kubectl get svc -n kube-system | grep alb
```
出力例は以下の通りです。
```
$ kubectl get svc -n kube-system | grep alb
public-xxxxx-alb1   LoadBalancer   aaa.aaa.aaa.aaa     bbb.bbb.bbb.bbb   cc:ccccc/TCP,ddd:ddddd/TCP   44m
```
対象となるALBのIDを確認したら、以下のコマンドを実行します。
＜ALB_ID＞としている箇所は、前手順で確認した対象ALBのIDに置き換えて実行してください。
```
$ kubectl edit svc <ALB_ID> -n kube-system
```
実行するとALBの設定ファイルをviモードで編集できるようになります。
変更が必要な箇所は、"spec.externalTrafficPolicy"です。こちらの設定値の値がデフォルトで「Cluster」になっているところを「Local」に変更します。
```
...
  clusterIPs:
  - xxx.xxx.xxx.xxx
  externalTrafficPolicy: Local //この設定値を「Cluster」から「Local」に変更
  internalTrafficPolicy: Cluster
  ipFamilies:
  - IPv4
...
```
編集が完了したら、viモードと同様「:wq」で保存・終了してください。

### 後動作確認
編集完了直後から設定が反映されます。こちらで確認した限り、ALBのダウンタイムは発生しませんでした。
アプリないしはALB Podのログからきちんと変更が反映されているかを確認してください。

ちなみにこのexternalTrafficPolicyの設定がClusterのままだと後続のIPアドレス制限設定が機能しないので、ご注意ください。


## ② IPアドレス制限設定を行う
設定変更の反映が確認できたら事前準備で作成したmyingress.yamlにIPアドレス制限の設定を加えます。
以下のように、"nginx.ingress.kubernetes.io/whitelist-source-range"設定を追加したyamlを作成してください。
```myingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: myingress
  annotations:
    kubernetes.io/ingress.class: "public-iks-k8s-nginx"
    nginx.ingress.kubernetes.io/whitelist-source-range: <アクセスを許可するIPアドレスのレンジ>
spec:
  tls:
  - hosts:
    - <ドメイン名>
    secretName: <TLSシークレット名>
  - host: <ドメイン名>
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service: 
            name: myservice
            port:
              number: 3000
```
※ IPアドレスを複数記載する場合はコンマ(,)区切りで1文で記載してください。
yamlを作成したら、以下のコマンドを実行しIngressを更新してください。
```
$ kubectl apply -f myingress.yaml
```

## ③ IPアドレス制限が機能していることを確認する
Ingressの更新が完了したら、動作確認をします。

nginx.ingress.kubernetes.io/whitelist-source-rangeで定義した、アクセスを許可するIPアドレスからアプリにアクセスし、アプリにアクセスが可能であることを確認してください。

続いて、nginx.ingress.kubernetes.io/whitelist-source-rangeに定義していないIPアドレスからアプリにアクセスすると、403エラーでアクセスが拒否されることを確認してください。

手順は以上です。


# 最後に
手順が分かってしまえば簡単にIPアドレス制限を実装できることがわかりました。
アプリの改修をせずに制限を実装できるのも魅力的ですね。

ちなみに、IngressでIPアドレス制限を実装した場合デフォルトではnginxの403エラー画面が表示されますが、こちらのエラー画面のカスタマイズもできることを確認してますので気が向いたらこちらも記事にしたいと思います。
こちらの手順については[この記事](https://stackoverflow.com/questions/60080132/how-to-set-up-a-custom-http-error-in-kubernetes)で割と丁寧に記載があるので気になる方は参考にしていただければと思います。

# 参考文献
* IBM Kubernetes Service公式ドキュメント
https://cloud.ibm.com/docs/containers?topic=containers-getting-started&interface=ui
* サブネットマスクとは
https://www.cman.jp/network/term/subnet/p3/
* EKSのロードバランサーでIPアドレスのアクセス制限を設定する
https://blog.kumano-te.com/activities/set-ip-whitelist-to-eks-nlb-service
* NGINX Ingress ControllerによるIPアクセス制御
https://kazkokk.hatenablog.com/entry/2021/06/14/220548
* How to set up a custom HTTP error in Kubernetes
https://stackoverflow.com/questions/60080132/how-to-set-up-a-custom-http-error-in-kubernetes
* 送信元アドレスを許可する(ホワイトリスト形式)
https://tips.weseek.co.jp/602344e4b536bb0048568df5

おわり


# 更新履歴
* 2023/5/12 公開
* 2023/6/5 IPアドレスの複数記載方法を追記