CDKでBlueGreenデプロイ作成時にinstallから進まない場合の1要因

## 背景

2022年2月現在CDK（CloudFormation）では、ECSのタスク定義を変更しデプロイする際にBlueGreenデプロイに対応していない。デプロイコントローラをCODE_DEPLOYにしているECSサービスではデプロイしようとするとエラーが発生する。そのため、CDKで初回デプロイ時にCDKで定義したタスクを起動させそれ以降はCodeDeployでtaskdef.jsonに記載されたタスク定義をもとにデプロイを実施することにした。

CloudFormationによるBlueGreenデプロイをする方法も存在するが、グリーンタスクセット作成後のターゲットグループの置き換えを実施する際に待ち時間を設定できず動作確認ができないため採用は諦めた。

ALBやECSのリソースはCDKで作成し、CodeDeployを手作業で構築してBlueGreenデプロイを実現する。

## 現象

デプロイのライフサイクルではInstallのイベントで「進行中」、デプロイのステータスでは「ステップ 1:置き換えタスクセットのデプロイ」のまま数十分経過しても、何も反応がない状況になった。

調べてみると原因は多様でCodeDeployのIAMロールで権限が足りていない原因や、そもそもコンテナが正常に動作せずヘルスチェックで失敗、プレースホルダー設定ミス（appspec.ymlやtaskdef.json）が原因等の記事がいくつか見つかった。
しかし、ECSタスクは起動していてロードバランサのヘルスチェックが成功し、踏み台のEC2インスタンスからも疎通確認することができた（セキュリティグループで許可した上で）。タスク起動はできているのでCodeDeployのサービスロールでPassRole等の問題もない。また、appspec.ymlの内容とタスク定義のJSONを確認したがプレースホルダーが想定通り置き換えられている。

試しにWeb画面から手作業でECSサービスを再構築するとデプロイできた。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359375/3029ecea-d918-adad-df62-f4b46efd7d14.png)

## 原因
Web画面から手作業で作成したECSサービスとCDKで作成したECSサービスをAWS CLIでJSONとして出力した（aws ecs describe-services）。するとCDKで作成したECSサービスにはロードバランサに2つのターゲットグループが含まれていることがわかった。

```json:CDKによる ECS Service
{
    "services": [
        {
            "serviceArn": "arn:aws:ecs:ap-northeast-1:xxx:service/EcsCluster/FargateService",
            "serviceName": "FargateService",
            "clusterArn": "arn:aws:ecs:ap-northeast-1:xxx:cluster/EcsCluster",
            "loadBalancers": [
                {
                    "targetGroupArn": "arn:aws:elasticloadbalancing:ap-northeast-1:xxx:xxx/xxx/xxx",
                    "containerName": "web",
                    "containerPort": 80
                },
                {
                    "targetGroupArn": "arn:aws:elasticloadbalancing:ap-northeast-1:xxx:xxx/xxx/xxx",
                    "containerName": "web",
                    "containerPort": 80
                }
            ],
            "serviceRegistries": [],
            "status": "ACTIVE",
            "desiredCount": 1,
            "runningCount": 0,
            "pendingCount": 1,
            "launchType": "FARGATE",
            "platformVersion": "1.4.0",
            "taskDefinition": "arn:aws:ecs:ap-northeast-1:xxx:task-definition/xxx:xxx",
            "deploymentConfiguration": {
                "maximumPercent": 200,
                "minimumHealthyPercent": 50
            },
```

```json:手作業による ECS Service
{
    "services": [
        {
            "serviceArn": "arn:aws:ecs:ap-northeast-1:xxx:service/EcsCluster/FargateServiceTesagyou",
            "serviceName": "FargateServiceTesagyou",
            "clusterArn": "arn:aws:ecs:ap-northeast-1:xxx:cluster/EcsCluster",
            "loadBalancers": [
                {
                    "targetGroupArn": "arn:aws:elasticloadbalancing:ap-northeast-1:xxx:targetgroup/xxx/xxx",
                    "containerName": "web",
                    "containerPort": 80
                }
            ],
            "serviceRegistries": [],
            "status": "ACTIVE",
            "desiredCount": 1,
            "runningCount": 1,
            "pendingCount": 0,
            "launchType": "FARGATE",
            "platformVersion": "1.4.0",
            "taskDefinition": "arn:aws:ecs:ap-northeast-1:xxx:task-definition/xxx:xxx",
            ...
```

CDKのソースコードを確認すると、たしかにBlueGreenデプロイに使用するテストリスナーとメインリスナーで2つ追加している。推測だが2つで1つのタスクセットとしてターゲットグループが登録されてしまっているため、2つのターゲットグループをデプロイ時に置き換えようとして失敗しているのではないか。

```python
        alb = elbv2.ApplicationLoadBalancer(self, "Alb",
                                            vpc=vpc,
                                            internet_facing=True,
                                            )

        main_listener = alb.add_listener("MainListener",port=80,open=True,)
        target1 = main_listener.add_targets("Target1", port=80, targets=[service])
        test_listener = alb.add_listener("TestListener",port=8080,open=True,)
        target2 = test_listener.add_targets("Target2", port=80, targets=[service])
```

そのため、テスト用のターゲットグループはECSサービスに関連付けずに別途用意するように変更した。

```python
        main_listener = alb.add_listener(
            "MainListener",
            port=80,
            protocol=elbv2.Protocol.HTTP,
            open=True,
        )

        target_group1 = main_listener.add_targets(
            "Target1",
            port=80,
            targets=[service],
            health_check=health_check,
        )

        test_listener = alb.add_listener(
            "TestListener",
            port=8080,
            protocol=elbv2.Protocol.HTTP,
            open=True,
            default_action=elbv2.ListenerAction.forward(
                target_groups=[target_group1],
            )
        )

        # targetsにECSサービスを指定せず、ターゲットタイプがIPのターゲットグループを別途作成
        target_group2 = elbv2.ApplicationTargetGroup(
            self, 'Target2',
            port=80,
            protocol=elbv2.ApplicationProtocol.HTTP,
            target_type=elbv2.TargetType.IP,
            vpc=vpc,
            health_check=health_check,
        )
```

この方式に切り替えたところ無事デプロイすることができた。ロードバランサと紐付いていないように見えるがデプロイする過程で紐付けてくれた。

## コメント
CDKだと内部でよしなに設定してくれるが、想定と違った定義になっているかもしれない。そんな時は手作業で作成したものと比較してみるという調査方法も別のトラブル時に活用できそう。

CDKは同じような定義をいくつかの書き方で実装できるのでもっとわかりやすい方法があるかもしれない。

## 参考

* [describe-services — AWS CLI 1.22.63 Command Reference](https://docs.aws.amazon.com/cli/latest/reference/ecs/describe-services.html)