作ってわかる！ Ingress

# CKAに合格してもIngressがよくわからない...

Kubernetesを学ぶと、最初の方でサービス系の基本的なリソースとしてPod, Deployment, Serviceが出てきます。ここまで知って、「Deploymentを使って複数のPodを作って冗長化して、Serviceで複数のPodにトラフィックをよしなに送ってくれるんだね。動いていないPodがあったらそこには通信しない仕組みもあるんだね。まあなんと素敵！！」と多くの人が（おそらく）感動します。

さらにConfigMapとかSecretとかPV,PVCとかがある程度わかってくると、「もうだいたいKubernetesわかってきたんじゃね？自分で作れるんじゃね！？」という気分になってきます。

そんな高揚感を打ち砕くように突如現れるのが、**Ingress**。

「うっ...、Ingressかぁ... Ingressねぇ...」

CKAやCKADで試験勉強をした場合、KodeKloudやkiller.shでIngressの問題が出てきます。よくわかっていないながらも何回かやっていくうちに一応解けるようになり、晴れてCKA合格！

だけど、Ingressに関してはずっと自信が持てない。。。
私にとってのIngressはそんな存在でした。

そこで本記事は、Ingressを使う必要性を考察しつつ、オンプレKubernetes上で実際にIngressリソースを作り理解を深めることを目指して執筆します。

# なぜIngressを使うのか？

Ingressの説明については、以下のkubernetes.ioをご参照ください。

https://kubernetes.io/ja/docs/concepts/services-networking/ingress/

上の記事には「Ingressの機能として負荷分散、SSL終端、名前ベースの仮想ホスティングの3つがある」と書かれていますが、これらはIngressを使わなくても、例えばnginxで実現できます。nginxの設定ファイル（`nginx.conf`等）で、負荷分散は`location`ディレクティブ内、SSL終端は `ssl_` で始まる設定値、名前ベースの仮想ホスティングは`server_name`にホスト名を設定すればOKです。
なので別にIngressを使わなくても、nginxのDeploymentとServiceを立ち上げれば同じことができるわけです。

ではなぜIngressを使うのか？

それは、**Ingressを使うと「負荷分散」「SSL終端」「名前ベースの仮想ホスティング」等の設定をKubernetesマニフェストでシンプルに設定でき、汎用性も上がるから**です。

もしリバースプロキシ的な機能をIngressではなくて、nginx単体で実装しようとするとどうなるでしょうか。
この場合、ConfigMapに``nginx.conf``を自分で用意して、nginxのDeploymentにそれをマウントする手間が必要になります。また、nginx以外のリバースプロキシツール（TraefikやApache等）を使いたくなった場合に、設定ファイルを一から作り直さなければなりません。場合によっては、新しく使うツールの設定ファイルの仕様の調査から必要になってしまうでしょう。

これを**Ingressで実現すると、使うリバプロツールによらずに全く同じ書き方で、負荷分散やSSL終端等の設定を表現できます。**

以下は簡単なIngressのマニフェストの例です。

```yaml:simple-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: simple-ingress
  namespace: default
spec:
  ingressClassName: nginx # ここでリバプロツールを指定している
  rules:
  - host: example.com     # 「名前ベースの仮想ホスティング」の設定
    http:
      paths:
      - path: /           # 「負荷分散」の設定
        pathType: Exact
        backend:
          service:
            name: example-service
            port:
              number: 80
  tls:                    # 「SSL終端」の設定
  - hosts:
    - example.com
    secretName: cert
```

マニフェストにもコメントしていますが、どのリバプロツールを使うかは `spec.ingressClassName` で指定できます。例えばnginxからTraefikに変えたくなった場合は、`ingressClassName: traefik` と書き換えるだけでおおかた済みます。Ingressを使って非常にシンプルになるのがおわかりいただけますでしょうか！

尚、後述しますが、nginxやTraefikを使うためには各々の「Ingressコントローラー」のインストールが必要です。使えるIngressコントローラーは以下をご参照ください。

https://kubernetes.io/ja/docs/concepts/services-networking/ingress-controllers/

# Ingressを作ってみる

それでは実際にIngressを作ってみましょう。

## 事前状況の確認

`kubectl`コマンドで、IngressのリソースはKubernetesのデフォルトで使える状態であることがわかります。

```
$ kubectl api-resources | grep -e ^NAME -e networking
（出力結果は少し編集）
NAME             SHORTNAMES  KIND
ingressclasses               IngressClass
ingresses        ing         Ingress
networkpolicies  netpol      NetworkPolicy
```

しかしながら、IngressコントローラーをインストールしていないのでIngressを正常にデプロイすることはできません。Ingressコントローラーをインストールするとそれに対応するIngressClassリソースが作成されますが、初期状態だと以下のようにリソースが存在しないです。

```
$ kubectl get ingressclasses
No resources found
```

そこでまずは、何かしらのIngressコントローラーのインストールが必要となります。

## Ingress NGINX Controller のインストール

今回は、[nginxのIngressコントローラー](https://github.com/kubernetes/ingress-nginx/blob/helm-chart-4.11.2/charts/ingress-nginx/values.yaml)をHelmチャートでインストールします。
HelmチャートのインストールはHelmfileが便利です。以下のYAMLファイルを`helmfile`コマンドでインストールします。

```yaml:helmfile-ingress-nginx.yaml
repositories:

- name: ingress-nginx
  url: https://kubernetes.github.io/ingress-nginx

releases:

- name: ingress-nginx
  namespace: ingress-nginx
  createNamespace: true
  chart: ingress-nginx/ingress-nginx
  version: 4.11.2
```

実行コマンド
```
helmfile apply -f helmfile-ingress-nginx.yaml
```

:::note info
上記を実施するには、``helm`` および ``helmfile`` のインストールが必要です。
:::

間もなくして、ingress-nginx-controller のリソースが作成されます。

```
$ kubectl get deploy -n ingress-nginx
NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
ingress-nginx-controller   1/1     1            1           22s
```
```
$ kubectl get svc -n ingress-nginx
NAME                                 TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)                      AGE
ingress-nginx-controller             LoadBalancer   10.110.19.229   <pending>     80:31958/TCP,443:31030/TCP   22s
ingress-nginx-controller-admission   ClusterIP      10.102.14.65    <none>        443/TCP                      22s
```

IngressClassにも、ingress-nginx-controllerに対応するリソースが追加されたことがわかります。

```
$ kubectl get ingressclasses
NAME    CONTROLLER             PARAMETERS   AGE
nginx   k8s.io/ingress-nginx   <none>       4m4s
```

先にお伝えしておくと、この後作成する**Ingressに宛てたリクエストはIngressコントローラーを経由してバックエンドのリソースに届きます**。少しくどいですが、上の例だとIngressコントローラーは「`ingress-nginx-controller` のServiceリソース」です。

したがって、疎通は`ingress-nginx-controller`のNodePortを使って以下のように確認できます。
※ 【注意！】現時点ではまだIngressを作っていないのでつながりません

**httpの場合**
```
curl http://[hostname]:31958
```

**httpsの場合**
```
curl https://[hostname]:31030
```

## バックエンド用のリソースの作成

最初にバックエンド用のリソースを作成します。マニフェストで作るのが面倒なので、以下のコマンドでnginxとhttpdのDeployent,およびServiceリソースを作ります。

```
kubectl create deployment backend-nginx --image=nginx --replicas=1
kubectl expose deployment backend-nginx --port=80 --target-port=80 --name=backend-nginx
kubectl create deployment backend-httpd --image=httpd --replicas=1
kubectl expose deployment backend-httpd --port=80 --target-port=80 --name=backend-httpd
```

リソースが作成できたことを確認します。
```
$ kubectl get po -n default -l app
NAME                             READY   STATUS    RESTARTS   AGE
backend-httpd-5f8458bbcd-nchlb   1/1     Running   0          13s
backend-nginx-8cb49db7d-d2jf2    1/1     Running   0          25s

$ kubectl get svc -n default -l app
NAME            TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
backend-httpd   ClusterIP   10.110.192.166   <none>        80/TCP    14s
backend-nginx   ClusterIP   10.97.122.179    <none>        80/TCP    23s
```

## Ingressの作成

準備は整ったので、お待ちかねのIngressリソースを作成します。

IngressもYAMLを書かずとも、以下のようなコマンドで一応作れます。
```
kubectl create ingress \
ing-multipath \
-n default \
--class=nginx \
--annotation nginx.ingress.kubernetes.io/rewrite-target=/ \
--rule="/nginx=backend-nginx:80" \
--rule="/httpd=backend-httpd:80"
```

上記コマンドの最後に ``--dry-run=client -o yaml`` を加えてYAMLで出力すると、次のようなマニフェストになるのを確認できます。
```yaml:ing-multipath.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: / # 後で説明
  creationTimestamp: null
  name: ing-multipath
  namespace: default
spec:
  ingressClassName: nginx
  rules:
  - http:
      paths:
      - backend:
          service:
            name: backend-nginx
            port:
              number: 80
        path: /nginx
        pathType: Exact
      - backend:
          service:
            name: backend-httpd
            port:
              number: 80
        path: /httpd
        pathType: Exact
```

`nginx.ingress.kubernetes.io/rewrite-target: /` というannotationは、バックエンドにリクエストを転送する際にパスを書き換えるのに必要です。例えば ``/nginx``のパスで来たリクエストは、``backend-nginx``のServiceには ``/`` のパスに書き換えられて転送されます。これを書いておかないと、``backend-nginx``にも``/nginx``のパスでリクエストされて "404 Not Found" が返ってきます。（おそらく多くの人が一度はここでハマるはず...）

:::note info
各Ingressコントローラーに固有の設定は、上の例のように annotation に設定を書く必要があります。本格的に業務でIngressを使うことになった場合、 annotation を活用することが必須になるので、是非覚えておいてください！
:::

さて、以下のようにIngressリソースが作成されました。
```
$ kubectl get ing -n default
NAME            CLASS   HOSTS   ADDRESS   PORTS   AGE
ing-multipath   nginx   *                 80      7s
```

実際にリクエストして、バックエンドにリソースが届くかを確認してみましょう。

今回作っているk8sクラスターのノードは一つで、IPアドレスが ``10.0.0.4``です。
```
$ kubectl get node -owide | awk '{print $1 " " $5 " " $6}' | column -t
NAME         VERSION  INTERNAL-IP
ip-10-0-0-4  v1.29.8  10.0.0.4
```

そこで、``10.0.0.4``とhttpのNodePortを目がけてcurlを叩いてみます。

```
$ curl http://10.0.0.4:31958/nginx 2>/dev/null | head -n 4
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
```

```
$ curl http://10.0.0.4:31958/httpd 2>/dev/null | head -n 4
<html><body><h1>It works!</h1></body></html>
```

nginxとhttpdのバックエンドに、見事にリクエストが届きました！

### 補足：パスの置き換え規則について

上の例では、バックエンドのパスの置き換え規則を次のように書きました。
```yaml
metadata:
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec
  rules:
  - http:
      paths:
      - path: /nginx
        pathType: Exact
        ...
```
これだとバックエンドのpathが ``/nginx`` 一つであれば問題ないですが、例えば ``/nginx/about-us.html`` や ``/nginx/contact.html`` のように、``/nginx/`` の下に複数のサイトがあるとうまく動きません。
この場合は次のように、``rewrite-target``と``path``に正規表現を使うことで解決します。
```yaml
metadata:
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$2 # pathの正規表現でマッチした2番目のグループのテキストに書き換える
spec
  rules:
  - http:
      paths:
      - path: /nginx(/|$)(.*)            # 正規表現にする
        pathType: ImplementationSpecific # 後述
        ...
```
``pathType: ImplementationSpecific`` は[ドキュメント](https://kubernetes.io/docs/concepts/services-networking/ingress/#%E3%83%91%E3%82%B9%E3%81%AE%E3%82%BF%E3%82%A4%E3%83%97)を見ると「このpathTypeはIngressClassに依存する」と書かれているので、``ingress-nginx-controller``がよしなに処理してくれるものと思われます。
``pathType: Prefix``にしても動きはしたのですが、Ingress作成時に以下のワーニングが出たので、``ImplementationSpecific``に変更しました。

```
Warning: path /nginx(/|$)(.*) cannot be used with pathType Prefix
```

## Ingressのカスタマイズ

上で作った ``ing-multipath.yaml`` のマニフェストをカスタマイズする事例を紹介します。

### ホスト名を指定

``spec.rules[].host`` でクライアントがリクエストするホスト名を指定できます。

```yaml
...
spec:
  ingressClassName: nginx
  rules:
  - http:
      paths:
        ...
    host: example.com # 追加
```

この場合、クライアントはIPアドレスではなく、次のように``example.com``でリクエストする必要があります。

```
curl http://example.com:31958/nginx
```

``example.com`` はクライアント側で名前解決してノードのIPアドレス（今回だと``10.0.0.4``）に変換する必要があります。動作検証が目的であれば、``/etc/hosts``に ``10.0.0.4 example.com`` を一行追記するのが手軽でしょう。

名前解決しなくても、以下のようにヘッダーにホスト名を追加することでもリクエスト可能です。

```
curl http://10.0.0.4:31958/nginx -H "Host: example.com"
```

ちなみに``example.com``を指定せずにIPアドレスでリクエストすると、`ingress-nginx-controller`でホスト名が不正とみなされて404エラーになります。

```
$ curl -I http://10.0.0.4:31958/nginx 2>/dev/null | head -n 1
HTTP/1.1 404 Not Found
```

### SSL/TLS証明書を使ってHTTPSでリクエスト

``spec.tls`` にSSL/TLSに関する情報を追記すると、自分で用意した証明書を使ってHTTPSで通信可能になります。

```yaml
spec:
  ...
  tls:                           # ここ以降は追加した行
  - hosts:
    - example.com
    secretName: cert-selfsigned
```

:::note info
マニフェストに ``spec.tls`` を記載しなくてもHTTPSで通信可能ですが、証明書はデフォルトで用意されたものが使われます。
:::

``secretName`` で指定するSecretリソースには[ドキュメント](https://kubernetes.io/ja/docs/concepts/services-networking/ingress/#tls)を参照すると、以下のようにSSL通信用のサーバ証明書と秘密鍵を格納する必要があります。

```yaml
$ kubectl get secret -n default cert-selfsigned -oyaml | cut -c 1-48 | head -n 6
apiVersion: v1
kind: Secret
data:
  ca.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk # CA証明書（必須ではないかも）
  tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tC # サーバ証明書
  tls.key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktL # 秘密鍵
```

SSL/TLSの自己証明書の作成は、例えば以下に記載された手順が参考になります。

https://github.com/zealvora/certified-kubernetes-security-specialist/blob/main/domain-2-cluster-hardening/ingress-security.md

またKubernetesのリソースだと [cert-manager](https://cert-manager.io/docs/) を使えば割と簡単に作れます。

SSLに関するSecretリソースをIngressのマニフェストに追記して反映すると、以下のコマンドでhttpsでのリクエストが可能になります。
※ ちなみに `-k`オプションは、SSL/TLS証明書の検証を無効化するためのオプションです。

```
curl -k https://10.0.0.4:31030/nginx
```

余談ですが、IngressコントローラーによってはSSLに関する設定を ``spec.tls`` ではなくて、annotationで行うものもあります。例えばAWSのALB(Application Load Balancer)の場合は、``alb.ingress.kubernetes.io/certificate-arn`` の annotation で指定します。

## IngressコントローラーをTraefikに変えてみる



続いて、[Traefik](https://github.com/traefik/traefik-helm-chart)を二つ目のIngressコントローラーとして作ってみます。
Ingress NGINX Controllerと同様に、以下のHelmfileを使って作成します。

```yaml:helmfile-ingress-traefik.yaml
repositories:

- name: traefik
  url: https://traefik.github.io/charts

releases:

- name: traefik
  namespace: ingress-traefik
  createNamespace: true
  chart: traefik/traefik
  version: v30.1.0
```

以下のコマンドを実行します。
```
helmfile apply -f helmfile-ingress-traefik.yaml
```

TraefikのIngressコントローラーが作成されたことを確認します。
```
$ kubectl get deploy -n ingress-traefik
NAME      READY   UP-TO-DATE   AVAILABLE   AGE
traefik   1/1     1            1           11s

$ kubectl get svc -n ingress-traefik
NAME      TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)                      AGE
traefik   LoadBalancer   10.105.179.76   <pending>     80:30883/TCP,443:32586/TCP   16s
```

``80:30883/TCP,443:32586/TCP`` と書かれているので、httpだと30883のポート、httpsの場合は32586のポートにアクセスすれば問題なさそうです。

また、IngressClassにも `traefik` が追加されているのを確認できます。
```
$ kubectl get ingressclasses
NAME      CONTROLLER                      PARAMETERS   AGE
nginx     k8s.io/ingress-nginx            <none>       26h
traefik   traefik.io/ingress-controller   <none>       33s
```

準備ができたので、Traefik用のIngressリソースのマニフェストを作ります。

```yaml:ing-multipath-traefik.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ing-multipath-traefik
  namespace: default
  annotations:
    # nginxのIngressの "nginx.ingress.kubernetes.io/rewrite-target"
    # と同等のアノテーション（詳細は後述）
    traefik.ingress.kubernetes.io/router.middlewares: default-stripprefix@kubernetescrd
spec:
  ingressClassName: traefik # nginxから変更
  rules:
  - http:
      paths:
      - backend:
          service:
            name: backend-nginx
            port:
              number: 80
        path: /nginx
        pathType: Exact
      - backend:
          service:
            name: backend-httpd
            port:
              number: 80
        path: /httpd
        pathType: Exact
```

nginxのIngressからの変更点は「``annotations`` のKeyValue」と「``ingressClassName`` の値」、この2つだけです。

尚Traefikでは、パスを書き換えるのに[Middleware](https://doc.traefik.io/traefik/middlewares/overview/)リソースを別途作る必要があります。以下のように、Middlewareのマニフェストにおいて``spec.stripPrefix``で削除したいプレフィックス列挙するものを作ります。

```yaml:ing-multipath-traefik-middleware.yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: stripprefix
  namespace: default
spec:
  stripPrefix:
    prefixes:
      - "/nginx"
      - "/httpd"
```
Ingressリソースのannotationで、作成したMiddlewareを以下のように参照する必要があります。
```yaml
traefik.ingress.kubernetes.io/router.middlewares: default-stripprefix@kubernetescrd
```
値はどうやら ``[namespace]-[resource-name]@kubernetescrd``のように書く必要がありそうです。

上記のIngressおよびMiddlewareのリソースを作成すると、以下のようにTraefikのIngressコントローラーでもバックエンドにリクエストが届くことを確認できます！

```
$ curl http://10.0.0.4:30883/nginx 2>/dev/null | head -n 4
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
```

```
$ curl http://10.0.0.4:30883/httpd 2>/dev/null | head -n 4
<html><body><h1>It works!</h1></body></html>
```

# おわりに

実際にIngressを手を動かして作ってみることで、少し理解が進みました。
本記事の最初の方で、「もしIngressを使わなかったら、``nginx.conf``のConfigMapを用意して、Deploymentにマウントする必要がある。これは手間だしイケていない」と書きましたが、実は私が個人で動かしているKubernetesクラスターが今その（イケていない？）作りになっています。なので近々Ingressに書き換えようかな。
そして次は、Istioを理解することを目指します！