PipeCDでECS Service DiscoveryでのCanaryリリースを簡易実装したので解説します

## Summary<!-- omit in toc -->

- PipeCDで「ECSにおいてELBではなくService Discoveryでサービス間通信しているケースでのCanaryリリース」を簡易的に実装しました。本記事はその解説です。
- 実装の特徴は下記です。（いずれも["Route53方式"](https://qiita.com/t-kikuc/items/69957ee2c6b490a01b6d)とは逆になっています。）
    - Pros
      - デプロイにおいてELBやターゲットグループ、Route53等の追加コンポネントを必要としない
      - 本実装を導入しても、サービスのエンドポイントを変更する必要がない
      - ServiceDiscoveryのDiscoverInstances APIを利用していても、Canaryにアクセスできる
    - Cons
      - Canaryトラフィックの量を柔軟に指定できない
      - Canaryトラフィックの量を増減させる際に時間がかかる
      - Blue/Greenは未対応


## Agenda<!-- omit in toc -->

- [はじめに](#はじめに)
- [構成](#構成)
- [デプロイフロー](#デプロイフロー)
  - [0. デプロイ前の状態](#0-デプロイ前の状態)
  - [1. Canaryのデプロイ](#1-canaryのデプロイ)
  - [2. Primaryの更新](#2-primaryの更新)
  - [3. 古いTaskSetの削除](#3-古いtasksetの削除)
- [コード](#コード)
- [残った課題](#残った課題)
- [余談：　この構成に至った背景](#余談-この構成に至った背景)
- [所感](#所感)




## はじめに

- この記事は、AWS Containers Advent Calendar 2023の24日目の記事です。
- 「ECS Service Discoveryでサービス間通信しているケースでのCanaryリリース」をPipeCDで"簡易的に"実装したので、その仕組みと背景を解説します。
  - 現在[v0.46.0のrc0以降のversionにてPreview中](https://github.com/pipe-cd/pipecd/releases/tag/v0.46.0-rc0)です。
  - フィードバックいただけると大変ありがたいです。
- 先日公開した下記記事よりシンプルな別実装となります。
    - [ECS Service DiscoveryでもRoute53を使ってCanaryとBlue/Greenデプロイをどうにかやってみた](https://qiita.com/t-kikuc/items/69957ee2c6b490a01b6d)
    - 以下では、上記の記事の方式を"Route53方式"と呼称します。
    - "Route53方式"は、本記事では実現できていないBlue/Greenや柔軟なCanaryに対応するために考案・検証したものです。（PipeCDには未実装です）


### PipeCDとは

- PipeCDとは
    　(1)Progressive Deliveryを 
    　(2)GitOpsスタイルで 
    　(3)あらゆるPlatform（Kubernetes, ECS, Lambda, Terraform, Cloud Run, etc.)で
実現するための、OSSのCDツールです。
- [CNCFのSandboxプロジェクトです。](https://developers.cyberagent.co.jp/blog/archives/42448/)

https://pipecd.dev/

## 構成

- DNSでのディスカバリの場合
![overall-dns.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635929/d0a9e8c2-b61a-06b4-ef71-57e1867b3af4.png)

- APIでのディスカバリの場合
![overall-api.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635929/51a2150d-42e4-4c72-ca50-f0d4c04ff0bd.png)


- Canaryへのトラフィックの割合は、[PrimaryのTask数]と[CanaryのTask数]の比率によって決まります。
- DNSでのディスカバリでも、ServiceDiscoveryのDiscoverInstances APIでのディスカバリでも、Primary TaskSetと同じエンドポイントでCanaryにアクセス可能です。
    - "Route53方式"との大きな違いです。"Route53"方式では新設のCNAMEを使う必要があります。
- デプロイのフローとしては、Canaryのタスクセットを作成することでCanaryにトラフィックが流れ、問題がなければPrimaryも更新するかたちです。


## デプロイフロー

以下で、デプロイの流れを追っていきます。
※上述の通りDNSでもAPIでも同様にアクセス可能ですが、以下では簡易化のためにDNSのみ記載しています。

### 0. デプロイ前の状態

- PrimaryのTaskが稼働中です。
- `Service-1`は、Service Discoveryの`srv-1.ns-1`を利用する設定にしてあります。

![before-deploy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635929/c59ac1a9-0c74-d72c-110a-2cbd35a4d5d1.png)

### 1. Canaryのデプロイ

- 新しいタスク定義を使用するCanary TaskSetをデプロイしていきます。
- イメージを伝えるために図を3枚に分けていますが、実際には[1-1]が終わった直後に[1-2][1-3]も自動で完了します。

### 1-1. CanaryのTaskSet作成

- Service Discoveryの設定はサービスがもつため、TaskSetにService Discovery関連の設定を施す必要はありません。
- Canary TaskSetのTask数は下記のように決まります。
    - Canary TaskSet作成時に`scale`というプロパティの値を1〜100で指定します。
    - ```[サービスのTask数(=PrimaryのTask数)] x [scale(%)]``` を整数値に切り上げた値が、CanaryのTask数となります。
        - `scale`は、ECSの[CreateTaskSet API](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/ecs/create-task-set.html)の scale オプションに対応しています。

![canary-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635929/be5417ca-220d-5b4a-7d41-1c1f63fa8783.png)


### 1-2. CanaryのTaskがCloudMapとRoute53に自動で登録される

- `Service-1`のService Discovery設定を使われるため、CanaryもPrimaryと同じCloudMap,Route53空間に登録されます。

![canary-2-records.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635929/5586f35f-a3aa-2505-8eba-9a7dbb7ece4d.png)


### 1-3. Canaryにもトラフィックが流れ始める

- Route53とCloudMap上で作成されたCanaryのレコードは、Multivalue RoutingによってPrimaryのレコードと混ざって回答されます。
    - これにより、CanaryのTaskSetにもトラフィックが流れるようになります。
    - そのため、[PrimaryのTask数]と[CanaryのTask数]によって、Canaryへのルーティング比率が決まります。

![canary-3-complete.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635929/7c1bef20-8469-c2bc-7a5d-203d2e97ad8d.png)

- ELBを利用するケースと異なるのは、TaskSetおよびTaskが起動された段階でCanaryにもトラフィックが流れ出す点です。ELBを利用するケースでは、ELBのリスナールールを編集するまでCanaryにトラフィックは流れません。

### ロールバックについて

- ここまでで、Canaryの作成とCanaryへのアクセスが完了しました。
- もしCanaryで異状が判明してロールバックしたい場合は、CanaryのTaskSetを削除することで、デプロイ開始前の状態に戻ることができます。
    - CloudMapおよびRoute53からもCanaryのレコードが削除され、デプロイ前の状態に戻ります。

### 2. Primaryの更新

- Canaryに問題がない場合は、サービス全体をアップデートしていきます。
- このフェーズに進む前に、承認フェーズ等を挟むこともできます。
- 先ほどと同様に、イメージを伝えるために図を3枚に分けていますが、実際には[2-1]が終わった直後に[2-2][2-3]も自動で完了します。

### 2-1. 新しいPrimary TaskSetの作成

- Canaryで使用した新しいタスク定義で、新しくTaskSetを作成します。

![primary-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635929/df1c948b-76df-96ac-b0f5-ea85bbfbc2ca.png)


### 2-2. 新しいPrimaryのTaskがCloudMapとRoute53に自動で登録される

![primary-2-records.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635929/2d15c271-2b06-c731-20bd-637926a95319.png)

### 2-3. 新しいPrimaryにもトラフィックが流れる

- 一時的に、TaskSet間のTraffic Weightが変わります。
    - サービスのキャパシティを落とさないことが最優先事項のため、Weight値の厳密な維持にはこだわっていません。

![primary-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635929/275cd94a-daa4-f8ed-d28a-34e373639f1b.png)


## 3. 古いTaskSetの削除

- Canaryおよび古いPrimaryのTaskSetを削除することで、サービスのバージョンアップが完了します。

![clean.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635929/309e840d-dd50-e3b6-8101-796a18ac72b1.png)

## コード

- 実装のPRはこちらです -> https://github.com/pipe-cd/pipecd/pull/4690
- コードとしては、ELBを利用したケースでのCanaryとほとんど変わりません。
  - 相違点としては、ターゲットグループを指定不要にした点と、ELBのリスナールールを編集するステージを除外した点ぐらいです。
  - Service Discovery自体に関する操作は特になく、Canary用のTaskSetを作ればあとはService Discoveryが自動でやってくれます。
- (自分で実装しておいてなんですが、正直リファクタリングしたい点は色々あります。）


## 残った課題

- 本実装の課題として、下記3点があります。
  1. Canaryトラフィックの量を柔軟に指定できない
  2. Canaryトラフィックの量を増減させる際に時間がかかる
  3. Blue/Greenは不可

### 課題1. Canaryトラフィックの量を柔軟に指定できない

- 本実装では、`scale`で指定した値と異なる割合のトラフィックがCanaryに流れることがあります。
- これは、先述の通り「整数値への切り上げ」が行われるためです。
    > ```[サービスのTask数(=PrimaryのTask数)] x [scale(%)]``` を整数値に切り上げた値が、CanaryのTask数となります。


    - 例） Primary Taskが5個、`scale`が10(%)の場合、 5x10%=0.5が1に切り上げられます。
        - この時 Primary:Canary=5:1 となるため、Canaryには 1/6=16% (≠10%)のトラフィックが流れます。

- これに伴って、Canary割合として任意の値を指定できません。

### 課題2. Canaryトラフィックの量を増減させる際に時間がかかる

- 本実装では、PrimaryとCanaryとのトラフィック比率の制御を、タスク数の調整によって行なっています。
- よって、トラフィックを増やしたいTaskSetではTaskを起動する必要があり、逆に減らすためにはTaskを停止する必要があります。
- これはELBやRoute53上でWeightの設定値を編集する場合と異なり、時間がかかってしまいます。

### 課題3. Blue/Greenは未対応

- 課題2に関連しますが、Primary(Blue)へのトラフィックをゼロにしてCanary(Green)に切り替える際には、PrimaryのTaskを全て停止させる必要があります。
- しかしこの場合、Canaryに問題が発生してPrimaryにロールバックさせる際に、PrimaryのTaskを再度起動した上でCanaryを削除する必要があります。これは時間がかかるため、変更失敗時の影響が大きくなってしまいます。
- トラフィック切り替えに時間がかかることを許容するのであれば実装可能ではありますが、現在の実装と一部処理が異なることもあり、未対応となっています。


## 余談： この構成に至った背景

- 例によって「どこでWeightをコントロールするか」問題です。

- 最初はCloudMap上のインスタンスをRegister/Deregisterすれば、擬似的にWeightを調整できる良いのでは？と考えていました。
    - 「Canaryを起動した後にPrimaryを全てDeregisterすれば、Blue/Greenもできるのでは？」と
- しかし検証したところ、「CloudMapからTaskを手動でDeregisterすると、そのTaskは停止される」ことが判明しました。
    - 停止時にTaskのログとして下記が出ました。
        > Missing instance for task in (service-registry arn:aws:servicediscovery:ap-northeast-1:my-account-id:service/my-service-id)
    - これにより「起動しているけどDeregisterしておいたTaskを、Registerで高速に紐付ける」ができません
    - また、Taskが停止されると、サービスのキャパシティ維持のために自動でTaskが起動されます。
      - これにより、CloudMapに登録されるTask数が、結局Deregister前と変わらないことになります。
- ちなみにService Connectでは、CloudMapの手動操作が非推奨であることが公式ドキュメントに明記されています。
    - https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/service-connect.html
        > 重要
Amazon ECS Service Connect がお客様のアカウントに AWS Cloud Map サービスを作成します。手動でのインスタンスの登録/登録解除、インスタンス属性の変更、サービスの削除を行ってこれらの AWS Cloud Map リソースを変更すると、アプリケーショントラフィックまたは以降のデプロイで予期しない動作が発生することがあります。



## 所感

- この実装にチャレンジするまでは「Canaryができれば自然とBlue/Greenもできるだろう（逆もまた然り）」と思っていましたが、そうではないことを痛感しました。
- AWSのAPIをHackしすぎるリスクも学べました。
- より良い方法がある方は、ぜひ教えてください。