Argo Rollouts と Nginx Ingress Controller による Canary デプロイメント

# 概要
Kubernetes で GitOps を実現するツールである `Argo CD`と、Kubernetes で blue/grenn デプロイや canary リリースを実現するためのデプロイメントコントローラーである`Argo Rollouts`を使用して、canaryリリースの動作検証をしてみます。

# Argo Rollouts と Nginx Ingress Controller の連携の必要性
Kubernetes は標準で`トラフィックミラーリング`や`ヘッダーによるルーティング`や`パーセンテージ指定による負荷分散`などの細かいトラフィック管理を実現できるような機能はありません。

Kubernetesでアプリケーションの異なるバージョンへのトラフィックの割合を制御する唯一の方法は、svc のセレクターに基づいて pod のグループにトラフィックをルーティングするエンドポイントを提供することです。つまりバージョンのレプリカ数を操作することで実現できる単純な負荷分散です。

この負荷分散方式だと Pod 単位 での割合でしか分散することができません。`Argo Rollouts`も Kubernetesの標準機能を使用しているので、実現できる Canaryデプロイメントもこの程度です。

しかし`Argo Rollouts`は、サービスメッシュリソースを操作してロールアウトの目的に一致させることにより、きめ細かいトラフィック管理を実現することが可能です。Argo Rollouts は現在次のサービスメッシュをサポートしています。

- Istio
- Nginx Ingress Controller
- AWS ALB Ingress Controller
- Service Mesh Interface(SMI)

私の環境では`Nginx Ingress Controller`にて各サービスへのトラフィック管理をしているため、Nginx Ingress Controller と Argo Rollouts を連携させることで、きめ細かいトラフィック管理ができる canaryデプロイメントを実現します。

Nginx Ingress Controller はそれ単体で`トラフィックミラーリング`や`ヘッダーによるルーティング`や`パーセンテージ指定による負荷分散`を使用したcanary の仕組みが備わっており、アプリケーションの異なるバージョン間のトラフィック管理のために、いくつかの特別なアノテーションを備えた2つ目のIngressオブジェクト(Canary Ingress)を導入することにより、トラフィックを分割する機能を提供します。

Argo Rollouts Controller は、動的に Canary Ingress を生成することで、Nginx Ingress Controller と連携します。

# 構成図
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306374/31768f1a-c070-e6ca-2601-bfedfaa4f5bc.png)
基本の構成はこのようになります。このうち`tomozo-canary`Ingressは、Argo Rollouts Controller に自動生成されることになります。


# 各マニフェスト
## Ingress
Nignx Ingress Controller用 の Ingress です。`argo rollouts`を使用することで追加で必要な設定は特にありません。
くどいようですが、canary用のIngressは自動生成されるため、ここで定義する必要はありません。

```yaml:ing.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: tomozo
  annotations:
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/use-regex: "true"
spec:
  rules:
  - host: tomozo.tokyo
    http:
      paths:
      - backend:
          serviceName: tomozo-stable
          servicePort: 80
        path: /v1/tomozo/.*
```

## Service
通常(安定版)用の Service として `tomozo-stable`。Canary用の Service として `tomozo-canary`を定義します。名前以外は全て同じ設定値で問題ありません。

```yaml:svc.yaml
---
kind: Service
apiVersion: v1
metadata:
  name: tomozo-stable
spec:
  selector:
    app: tomozo
  ports:
    - port: 80
---
kind: Service
apiVersion: v1
metadata:
  name: tomozo-canary
spec:
  selector:
    app: tomozo
  ports:
    - port: 80
```

## rollout
`strategy:`より上は deployment と書き方変わりません。大事なのは trafficRouting で nginx を指定しているところです。
`stableIngress`でしていた Ingress をベースに、 Canary-Ingress を自動生成します。
また`annotationPrefix`,`additionalIngressAnnotations`で指定した内容が、自動生成される Canary-Ingressに設定されます。
私の場合は、 Ingress で正規表現を使用しているので`use-regex: 'true'`を追加しました。


また Nginx の canary 用として、以下の４つの annotation があります。

|Name|Description|
|:--|:--|
|canary-weigh|パーセンテージを指定し、特定の割合でルーティング先を変えることができます|
|canary-by-cookie|特定のCookieの名前を設定しておくことで、そのCookieの値がリクエストに含まれている際に、アクセスを新バージョン側に振ることができる|
|canary-by-header|特定のHeaderの名前を設定しておくことで、そのHeaderの値がリクエストに含まれている際に、アクセスを新バージョン側に振ることができる|
|canary-by-header-value|canary-by-header で指定したHeaderの値が、ここで指定した値の場合に、アクセスを新バージョン側に振ることができます|

`carry-weigh`については、`strategy.canary.steps.setWeight`で指定した内容が反映されるため、ここであえて指定する必要はありません。
今回は `canary-by-header`と`canary-by-header-value`を設定してみました。

```yaml:rollout.yaml
---
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: tomozo
  labels:
    app: tomozo
spec:
  selector:
    matchLabels:
      app: tomozo
  template:
    metadata:
      labels:
        app: tomozo
    spec:
      serviceAccountName: tomozo
      containers:
        - name: tomozo
          image: tomozo-repo/tomozo:0.0.1
# -----この行から上は元のDeploymentと一緒------
# -----この行から下はRolloutの拡張部分------
  strategy:
    canary:
      stableService: tomozo-stable
      canaryService: tomozo-canary
      trafficRouting:
        nginx:
          stableIngress: tomozo
          annotationPrefix: nginx.ingress.kubernetes.io
          additionalIngressAnnotations:
             use-regex: 'true'
             canary-by-header: 'X-Canary'
             canary-by-header-value: 'true'
      steps:
      - setWeight: 10
      - pause: {}
```


# Canary デプロイメントの流れ
```yaml:strategy.canary.steps
      steps:
      - setWeight: 10
      - pause: {}
```
rollout のマニフェストで設定した`strategy.canary.steps`の場合、どのような流れになるかを簡単な図で追っていきます。

## 通常時
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306374/78bd1047-9681-d6e7-db0f-9db207b4f8c5.png)
通常時はこのようになっており、 アクセスは Service: `tomozo-stable` を通って ver.1の pod にルーティングされます。
`Service: tomozo-canary` もver.1 の pod にルーティングされていますが、Nginxからはアクセスを振っていません。

## アプリケーションのリリース時
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306374/938c8bc2-6367-ea97-6582-459982a9334c.png)
コンテナのタグを ver.2 にしたマニフェストを反映させると、ver.2用の ReplicaSet が作成され、 `Service:tomozo-canary`からルーティングされます。また `wetWeight`の値が Nginx に反映され、 10%のアクセスが ver.2 のアプリケーションにルーティングされます。

## アプリケーションのリリース完了
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306374/0a841cd4-f894-bddb-d5c7-50498b855cbe.png)
`strategy.canary.steps`で設定している pause を解除すると、ver.2のアプリケーションが `Service:tomozo-stable`からもルーティングされるようになり、 ver.1のアプリケーションは削除されます。これでリリース完了となります。
なお、pauseの解除は コマンドでもできますし、 ArgoCD のGUI上からも操作可能です。

# 終わりに
Argo Rollouts を Nginx Ingress Controller と連携させることにより、お手軽かつきめ細かいCannaryデプロイメントを実現することができました。