【AWS】CDKで定義するAWS CodeDeployのデプロイ戦略【デプロイ】

# はじめに
Well-Architected Frameworkのデプロイに関するベストプラクティスではデプロイ失敗時の影響の制限やロールバックの自動化が記載されています。
プロジェクトの特徴や制限により必ずしもベストプラクティスを実施できるとは限りませんが、様々なデプロイやロールバックの方法を知っておくことは様々な特徴のあるプロジェクトにベストな選択をするためにとても有効です。
今回の記事ではAWS CoDeployによるカナリアデプロイのデプロイ戦略を実施する方法を記載しました。
また、CodeDeployだけではなく実際の現場ではIaCツールを利用することが多いため、AWS CDKを利用してリソース定義とCodeDeployによるデプロイ戦略の実施を一度に行う方法を記載しました。

この記事に関連した登壇内容：2025年1月7日 JAWS-UG 朝会

https://speakerdeck.com/naonana777/depuroinobesutopurakuteisu

登壇したイベント

https://jawsug-asa.connpass.com/event/331909/

# なぜデプロイ戦略が必要か
デプロイ(≒リリース)時に既存サービスから新サービスに一括で機能の切り替えを行う場合、システムのユーザ全員に素早く新機能を使ってもらえる半面、もしデプロイに失敗により処理結果の不正やシステムダウンが発生するとその失敗が全ユーザーに対して影響を与えてしまいます。
そのため、機能を段階的にリリースする方法(フェーズデプロイ)や、新機能を一部のユーザに利用してもらってから全ユーザ対象にリリースする方法(カナリアデプロイやリニアデプロイ)、ユーザを既存機能にアクセスさせながら新機能は既存機能と同じ構成の別の実行環境を作成してそこでテストを行ってから新機能がある環境にアクセス先を切り替える方法(ブルー/グリーンデプロイ)などのデプロイ戦略をとる場合があります

# なぜCodeDeployを使うのか
例えばAmazon Route53の加重ルーティングポリシーでアクセスされる割合を変更したり、API Gatewayでステージを分ける方法でデプロイ戦略を実現することは可能ですが、切り替えや設定変更を行うのに手間がかかる場合があります。
CodeDeployを利用することでブルー/グリーンデプロイやカナリアデプロイなどのデプロイ戦略の定義を簡単に行うことができます。

# なぜCDKなどのIaC(Infrastructure as Code)ツールを使うのか
マネジメントコンソールを利用することで各サービスの設定とCodeDeployの設定を行うことは可能ですが、検証環境や本番環境など複数の環境を利用している場合、それぞれで定義を行う必要があり、同じ作業をする手間と設定ミスのリスクが高くなります。
IaCツールを使うことで各リソース定義とCodeDeployによるデプロイ戦略の実行の定義をコードで定義することができ、複数環境に対しても同じコードを流用できるため同じ作業をする手間と設定ミスのリスクが低減できます。

# 関連するWell-Architected Frameworkのベストプラクティス
## OPS 6. どのようにデプロイメントリスクを軽減するのですか?
https://docs.aws.amazon.com/ja_jp/wellarchitected/latest/framework/ops-06.html

Well-Architected Framework のOPS 6に属するチェック項目では、デプロイの失敗の影響を制限するため自動ロールバックやデプロイ戦略、そのほかデプロイに関する計画について言及されています
様々な制限や特徴のあるすべてのプロジェクトに対してベストな唯一のデプロイ方法はありませんが、デプロイ時に検討すべき内容が詳細に記載されており、プロジェクトに最適なデプロイ方法を決めるための参考にできます。


# 実装例
## バージョンアップ対象のアーキテクチャ
APIGatewayからLambda関数を呼ぶ構成図
![apigateway.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192949/81e75600-048e-fca6-ef4f-032a32f7ab31.png)


## デプロイ内容
Lambda関数の処理の改修

## デプロイ戦略
カナリアデプロイを実施する
デプロイ後の5分間、既存機能90%、新機能5%の割合でユーザが機能にアクセスできるようにする
5分間の間にもしエラーが発生したらロールバックする(既存機能100%にする)
5分間の間にもしエラーが発生しなかったら新機能100％の割合でアクセスさせデプロイ完了にする

## デプロイ戦略を考慮したアーキテクチャ
・DeveloperはCDKを利用してCloudFormationテンプレートを作成してデプロイを行います
・CDKの定義内容にはAPI GatewayとLambda以外にCodeDeployの定義とロールバックに利用するCloudWatchアラームの定義を追加します
![CodeDeployカナリアデプロイ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192949/52503c42-3b22-3df7-aff3-beb967098cb1.png)


### 構成のポイント
【CDK】
- APIGatewayとLambda関数のアーキテクチャの定義と一緒にCodeDeployの定義を行う

【CodeDeploy】
- CodeDeployのデプロイ構成でどのくらいの時間、どのくらいの割合のトラフィックに対してエラーを監視するかを定義する
※今回の例ではLambdaの新バージョンに10%のトラフィックを割り当てて5分間監視
- CodeDeployのデプロイグループで新バージョンのLambdaのメトリクスを監視して、一定時間以内に定義したCloudWatchアラームが発生したらロールバックする


## CDK定義内容
### 【必要な定義】
- API Gateway
- AWS Lambda(関数)
- AWS Lambda(新バージョンのエイリアス)
- AWS CodeDeploy(アプリケーション)
- AWS CodeDeploy(デプロイメントグループ)
→Amazon CloudWatchアラーム
- AWS CodeDeploy(デプロイ設定)

### 【定義詳細】
[CDKプロジェクト]/lib/[定義ファイル].ts
※CDKプロジェクトをTypeScriptを利用している場合

- AWS Lambda(関数)
```typescript:
    const 【Lambda関数定義名】 = new NodejsFunction(this, `【Lambda関数名】`,{
      functionName: `【Lambda関数名】`,
      entry: path.join(REPOSITORY_TOP, "【Lambda関数プログラムのパス(ローカル環境)】"),
      handler: 'handler',
      runtime: lambda.Runtime.NODEJS_20_X,
      memorySize: 128,
      timeout: cdk.Duration.seconds(30),
    })

```
　＜ポイント＞
 　・通常のLambdaka関数の定義と同様
  
- AWS Lambdaエイリアス

```typescript:
    const 【エイリアス定義名】 = new lambda.Alias(this, 'Alias', {
      aliasName: '【エイリアス名】',
      version: 【ラムダ関数定義名】.currentVersion,
    });

```
　＜ポイント＞
 　・CodeDeployのデプロイ中にトラフィックを分散するバージョンが紐づくエイリアスを定義する
  
- API Gateway
```typescript:
    const 【API定義名】 = new apigateway.RestApi(this, 'RestApi', {
      restApiName: 【API名】,
      deployOptions: {
        stageName: 【APIステージ名】,
      },
    });

    【API定義名】.root
      .addResource('【リソース名】')
      .addMethod('GET', new apigateway.LambdaIntegration(【エイリアス定義名】));
```
　＜ポイント＞
 　・メソッドの呼び出し先(LambdaIntegration)の引数にはLambdaの定義ではなく、エイリアス定義を指定する
 　　→Lambdaの定義を指定してしまうとCodeDeployでトラフィック割合を設定しても100％新バージョンにアクセスするので注意
  
- AWS CodeDeploy(アプリケーション)
```typescript:
    const 【デプロイアプリケーション定義名】 = new codedeploy.LambdaApplication(this, 'Application', {
      applicationName: `【デプロイアプリケーション名】`,
    });
```
　＜ポイント＞
 　・CodeDeployのアプリケーションを定義する

- AWS CodeDeploy(デプロイ設定)
```typescript:
    const 【デプロイ設定定義名】 = new codedeploy.LambdaDeploymentConfig(
      this,
      '【デプロイ設定定義名】',
      {
        trafficRouting: new codedeploy.TimeBasedCanaryTrafficRouting({
          interval: Duration.minutes(5),
          percentage: 10,
        }),
      },
    );
```
　＜ポイント＞
 　・CodeDeployのデプロイ設定を定義する
 　・今回の例えは5分間(interval: Duration.minutes(5))、10％(percentage: 10)
  　　のトラフィックを新バージョンに移す
 　・TimeBasedCanaryTrafficRoutingのカナリアデプロイを選択しているため、
  　　5分以内にエラーがない場合は100%のトラフィックを新バージョンに移します

- AWS CodeDeploy(デプロイグループ)
→Amazon CloudWatchアラーム

```typescript:

    const 【デペロイグループ定義名】 =
      new codedeploy.LambdaDeploymentGroup(
        this,
        '【デペロイグループ定義名】',
        {
          deploymentConfig: 【デプロイ設定定義名】,
          application: 【デプロイアプリケーション定義名】,
          alias: 【エイリアス定義名】,
        },
      );

    【デペロイメントグループ定義名】.addAlarm(
      new cloudwatch.Alarm(this, 'ErrorAlarm', {
        comparisonOperator:
          cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
        threshold: 1,
        evaluationPeriods: 1,
        metric: 【エイリアス定義名】.metricErrors(),
      }),
    );
```
　＜ポイント＞
 　・CodeDeployのデプロイグループを定義する
 　・関連するデプロイアプリケーションとデプロイ設定、エイリアスを紐づける
 　・ロールバックの契機となるCloudWatアラームの条件を定義する
 　・今回の例では1回以上エラーのアラートが発生したらロールバックする


## デプロイ方法
・通常のデプロイ方法と同じく
cdk synth --profile 【プロファイル名】
でCloudFormationテンプレートを生成し、
cdk deploy --profile 【プロファイル名】
でデプロイを実行してください

※以下の図はCloudFormationテンプレートをApplicationComposerで表示した結果を示します
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192949/a68b2d8d-2d75-9edc-e3c2-bb80e3487c61.png)

# デプロイ実行時の動作検証
## 検証０：デプロイ前のAPIの状態
「Hello V1 Lambda」というメッセージを返すAPIがあることを確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192949/b29244d8-e585-0eba-5fe0-cfc5efd266b3.png)

## 検証１：エラーが発生しないデプロイ
### 同じAPIを実行して既存バージョンと新バージョンのLambdaが呼び出されることを確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192949/35375f06-1a1c-dbaa-ef2d-63b0bf05df80.png)

### デプロイ中はCodeDeployを画面からデプロイ進捗を確認できる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192949/51fb8e6e-fc5e-7e3c-613d-183f9649c4dc.png)

Lambda関数の「エイリアス」でも設定した割合でトラフィック制御が設定されていることを確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192949/81cdbd06-dfbf-a684-4ce3-9fbe0b3803f4.png)

### 5分間エラーが発生しない条件を達成したデプロイ完了後、新バージョンに100%のトラフィックが設定されることを確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192949/c09559eb-fe0b-9dca-972f-2e8fbbacb28a.png)


## 検証２：エラーが発生するデプロイ(ロールバック確認)
### Lambda関数のコードに以下のコードを追加して、エラーが発生するようにする
throw new Error();

### エラーが発生するバージョンにアクセスしたことを確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192949/42622b94-d407-bd4c-522d-e1e67c401e19.png)
### CloudWatchアラームが発生したことを確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192949/0e80dcdc-953d-af75-652c-24ce660fde3b.png)
### CodeDeployによりロールバックが発生し、既存バージョンが100%の割合のアクセスに代わっていることを確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192949/d1867168-0da1-1948-6168-2ab9ddccf2fc.png)

### ロールバック後、エラーが発生しない既存バージョンのみアクセスされることを確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192949/d136fa62-2f1c-dfef-d351-b9537dc2094a.png)
### Lambda関数のエイリアスでもアクセス割合が分かれていないことを確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192949/21514afe-4ec7-1997-17db-a2d8d84054cf.png)


# さいごに
デプロイ戦略はプロジェクトの特性や制限によって変わってくると思います。
どんなプロジェクトでも最適なデプロイ戦略が実施できるように、1つの方法にこだわらず様々な実装方法を学び、その実現方法、メリットデメリットを認識しておきましょう


# 関連記事

CloudFormationとCodePipelineに関する記事も投稿しました

https://qiita.com/Nana_777/items/0160ec6a52a901a59599


CDKとテストに関する記事

https://qiita.com/Nana_777/items/fa9f5074099f88ea35ba