ingress-nginxでのCanary機能と根本的課題

[ingress-nginxはどうやって動いているのか（１）](https://qiita.com/s-shirayama/items/886ef497fc2247784b7f)、[ingress-nginxはどうやって動いているのか（２）](https://qiita.com/s-shirayama/items/a590b40cf6bbf448aecc)と続けて、ingress-nginxがどのような仕組みで動いているのかを見てきた。

この記事では、ingress-nginxが提供するCanary機能の概要と、メンテナーの方が現時点（v0.26.1）で『Fundamental Issue(根本的な課題)』と言っているある課題について見ていく。

# Canary機能

[公式ドキュメント](https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#canary)では、

> In some cases, you may want to "canary" a new set of changes by sending a small number of requests to a different service than the production service. The canary annotation enables the Ingress spec to act as an alternative service for requests to route to depending on the rules applied.

とあるように、Canary機能用の独自AnnotationをIngress用のyamlファイルに記述しておくことで、クライアントから来たリクエストの数％を別のServiceに流したりすることができる。

現時点でサポートしているのは、以下の４つのパターン。

| Name | Description |
| ---- | ----------- |
| canary-weigh | %を指定し、特定の割合でルーティング先を変えることができる。 |
| canary-by-cookie | 特定のCookieの名前を設定しておくことで、そのCookieの値が`Always`の時にルーティング先を変えることができる。 |
| canary-by-header | 特定のHeaderの名前を設定しておくことで、そのHeaderの値が`Always`の時にルーティング先を変えることができる。 |
| canary-by-header-value | `canary-by-header` で指定したHeaderの値が、ここで指定した値の場合にルーティング先を変えることができる。 |

## Canaryの例

```yaml:ingress.yaml
kind: Ingress
spec:
  rules:
  - host: foobar.com
    http:
      paths:
        - path: /
          backend:
            serviceName: foo-service
            servicePort: 1234
```

```yaml:ingress-canary.yaml
kind: Ingress
metadata:
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
  rules:
  - host: foobar.com
    http:
      paths:
        - path: /
          backend:
            serviceName: bar-service
            servicePort: 1234
```

この２つのyamlを適用すると、`foobar.com`へのアクセスは90%が`foo-service`にルーティングされて、10%が`bar-service`にルーティングされる。

実際にはIngressリソース以外に、ingress-nginxをIngress ControllerとしてデプロイするためのDeploymentやServiceのyamlが必要だが、そちらについては以下のサイトなどを参照してほしい。

- https://github.com/ContainerSolutions/k8s-deployment-strategies/tree/master/canary/nginx-ingress
- https://www.elvinefendi.com/2018/11/25/canary-deployment-with-ingress-nginx.html

## 内部的な処理フローとコードリーディング

[処理③：Canary](https://qiita.com/s-shirayama/items/a590b40cf6bbf448aecc#%E5%87%A6%E7%90%86canary )を参照。

# Canary機能の根本的な課題

ingress-nginxのメンテナーの１人であるElvinEfendiが、Canary機能の実装上の根本的な課題として、以下のようなコメントをしている。

https://github.com/kubernetes/ingress-nginx/issues/4368#issuecomment-515574775

> I think fundamental issue is that canary is configured based on host and path combination but when it is actually applied to a request, we use $proxy_upstream_name, which is based on namespace, service and port. This is the problem.

どういうことかというのを、具体的な例で見ていく。

```yaml:ingress.yaml
kind: Ingress
spec:
  rules:
  - host: foobar.com
    http:
      paths:
        - path: /
          backend:
            serviceName: foo-service
            servicePort: 1234
        - path: /foobar
          backend:
            serviceName: foo-service
            servicePort: 1234
```

```yaml:ingress-canary.yaml
kind: Ingress
metadata:
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
  rules:
  - host: foobar.com
    http:
      paths:
        - path: /foobar
          backend:
            serviceName: bar-service
            servicePort: 1234
```

このような２つのyamlで、`foobar.com/foobar`に対して、Canaryを設定する。
これがこの部分のことである。

> canary is configured based on host and path combination

しかし、内部的な実装としては、[ingress-nginxはどうやって動いているのか（２）#オブジェクトの関連図](https://qiita.com/s-shirayama/items/a590b40cf6bbf448aecc#%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E9%96%A2%E9%80%A3%E5%9B%B3) にある通り、`Backend`（namespace, service, portで一意）に`AlternativeBackends`として、Canaryとして追加されるルーティング先（`Backend`）が格納される。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315538/07a0ffd7-e436-20c6-02b9-eb15dd1af4d6.png)

これが、

> we use $proxy_upstream_name, which is based on namespace, service and port.

の部分のことを示している。
この、**設定の仕方**と**内部実装**の違いが、いくつかの問題を生じさせている。

## 具体的な課題①：複数Path指定のIngressでCanaryがうまく動かない

[Issue#4028: Canary ingress with multiple paths behaves improperly](https://github.com/kubernetes/ingress-nginx/issues/4028)

実は先程例であげた２つのyamlを適用した場合、想定どおりにCanary機能が動作しない。

| パス | 期待する挙動 | 実際の挙動 |
| ---- | -------- | ------ |
| /    | 100%: foo-service | **90%: foo-service, 10%: bar-service** |
| /foobar | 90%: foo-service, 10%: bar-service | 90%: foo-service, 10%: bar-service |

`foo-service`の`Backend`の`AlternativeBackends`に、`bar-service`の`Backend`が設定されており、`/foobar`というPath情報は`Backend`では考慮されないため、どのパスへのリクエストももれなくCanaryの対象となってしまっている。

これが、ElvinEfendiが述べている"根本的な課題"のことである。

処理の流れ的にはこんな感じ（なはず）

1. `/` へのリクエストをnginxが受け取る
2. `nginx.conf`のServer>Locationの設定に応じて、メインの`Backend`が選択される（今回の場合は`foo-service`）
3. 実際のルーティング処理を行うLuaのBalancerモジュールでは、`foo-service`の`Backend`に設定されている`AlternativeBackends`を見て、10%の確率でリクエストを`bar-service`へルーティングする

## 具体的な課題②：Canary指定時のログの情報が正しくない

[Issue#4368: Incorrect logs and ngx variables when using canary Ingresses](https://github.com/kubernetes/ingress-nginx/issues/4368)

先程例であげた２つのyamlを適用した場合、Canaryで`bar-service`にリクエストがルーティングされた場合にも、ログには`foo-service`の情報が記録されてしまうというもの。

これは、ログに記録する情報はnginx.confで設定される値を利用していることに起因している。Canary機能を利用しない場合はそれで問題ないが、Canary機能を利用する場合、LuaのBalancerモジュールでルーティング処理時に、ルーティング先の`Backend`が変更される可能性がある。現在の実装ではその場合も、ログに書かれる情報の更新を行っていないため、正しくない情報がログに記録されることになる。

## 具体的な課題③：同一Serviceで複数Hostnameの場合のCanaryの挙動が正しくない

[Issue#4667: Ingresses with different host point to same upstream cause confusion](https://github.com/kubernetes/ingress-nginx/issues/4667)

別々のHostnameが同一のサービスへルーティングされる場合に片方のHostnameでCanaryを適用すると、もう片方のHostnameへのアクセスもCanaryの対象となってしまうというもの。

```yaml:ingress.yaml
kind: Ingress
spec:
  rules:
  - host: foobar1.com
    http:
      paths:
        - path: /
          backend:
            serviceName: foo-service
            servicePort: 1234
  - host: foobar2.com
    http:
      paths:
        - path: /
          backend:
            serviceName: foo-service
            servicePort: 1234
```

```yaml:ingress-canary.yaml
kind: Ingress
metadata:
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
  rules:
  - host: foobar1.com
    http:
      paths:
        - path: /
          backend:
            serviceName: bar-service
            servicePort: 1234
```

上記のyamlを適用した場合、`foobar1.com`に対して設定したつもりのCanaryの設定が、`foo-service`（の`Backend`）へ適用されてしまうため、`foobar2.com`へのアクセスも同様のCanaryの設定で動いてしまう。

これも[具体的な課題①：複数Path指定のIngressでCanaryがうまく動かない](#具体的な課題①：複数Path指定のIngressでCanaryがうまく動かない)と同様に、設定の仕方と内部実装の矛盾が根本原因となっている。

# 解決案

次回はこの根本的課題の解決案について検討してみたい。
→ [ingress-nginxのCanary機能の根本的課題の解決案](https://qiita.com/s-shirayama/items/45ff0a6c703d5bb8759b)