【AWS CF】「The target group does not have an associated load balancer」エラーの原因と解決方法

# 概要
AWS CloudFormationで以下のエラーに遭遇しました。
本記事ではその原因と解決方法を記載します。

```
Resource handler returned message: "Invalid request provided: CreateService error: The target group with targetGroupArn arn:aws:elasticloadbalancing:ap-northeast-1:1234567890:targetgroup/Sample-Web-Blue/00b3e54db86b7e99 does not have an associated load balancer.
```

# 原因は依存関係にあった
このエラーは、「LBに紐づいているターゲットグループが存在せずに、サービスが作成されているよ」ということになります。

しかし、実際のテンプレートでは、LBのリスナーもターゲットグループも作成していました。

```yaml
  #*******************************************************************************
  # TargetGroup
  # https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-taskdefinition.html
  #*******************************************************************************
  TargetGroupWebBlue:
    Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
    Properties:
      HealthCheckIntervalSeconds: 5
      HealthCheckPath: "/"
      HealthCheckPort: "traffic-port"
      HealthCheckProtocol: "HTTP"
      HealthCheckTimeoutSeconds: 2
      HealthyThresholdCount: 2
      UnhealthyThresholdCount: 2
      Matcher:
        HttpCode: "200"
      Name: !Sub ${SysTag}-Web-Blue
      Port: 80
      Protocol: "HTTP"
      VpcId: !Sub ${rVpcId}
      TargetGroupAttributes:
        -
          Key: deregistration_delay.timeout_seconds
          Value: 0
  TargetGroupWebGreen:
    Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
    Properties:
      HealthCheckIntervalSeconds: 5
      HealthCheckPath: "/"
      HealthCheckPort: "traffic-port"
      HealthCheckProtocol: "HTTP"
      HealthCheckTimeoutSeconds: 2
      HealthyThresholdCount: 2
      UnhealthyThresholdCount: 2
      Matcher:
        HttpCode: "200"
      Name: !Sub ${SysTag}-Web-Green
      Port: 80
      Protocol: "HTTP"
      VpcId: !Sub ${rVpcId}
      TargetGroupAttributes:
        -
          Key: deregistration_delay.timeout_seconds
          Value: 0

  #*******************************************************************************
  # Listener
  # https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-listener.html
  #*******************************************************************************

  HttpListenerRuleWebBlue:
    Type: 'AWS::ElasticLoadBalancingV2::ListenerRule'
    Properties:
      Actions:
        - Type: forward
          ForwardConfig:
            TargetGroups:
              - TargetGroupArn: !Ref TargetGroupWebBlue
                Weight: 1
      Conditions:
        - Field: host-header
          Values:
            - xxx.sample.com
      ListenerArn: !ImportValue HttpDefaultListener-XXX
      Priority: 2
  HttpListenerRuleWebGreen:
    Type: 'AWS::ElasticLoadBalancingV2::ListenerRule'
    Properties:
      Actions:
        - Type: forward
          ForwardConfig:
            TargetGroups:
              - TargetGroupArn: !Ref TargetGroupWebGreen
                Weight: 1
      Conditions:
        - Field: host-header
          Values:
            - xxx.sample.com
      ListenerArn: !ImportValue HttpDefaultListenerTest-XXX
      Priority: 2

  #*******************************************************************************
  # Service
  # https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-service.html
  #*******************************************************************************
  ServiceXXX:
    Type: AWS::ECS::Service
    DependsOn: HttpListenerRuleWebBlue
    Properties: 
      Cluster: !Sub ${rEcsName}
      DesiredCount: !Ref TaskDeployNumWeb
      ServiceName: !Sub ${SysTag}
      LaunchType: "EC2"
      PlacementStrategies:
        - Field: "attribute:ecs.availability-zone"
          Type: spread
        - Field: "instanceId"
          Type: spread
      DeploymentController:
        Type: CODE_DEPLOY
      LoadBalancers:
        - TargetGroupArn: !Ref TargetGroupWebBlue
          ContainerPort: 80
          ContainerName: Web
      TaskDefinition: !Ref TaskWeb
```

あれ、やっぱりちゃんと記載しているよなぁ...
おかしいなぁ...と詰まりました。

ということで、以下の記事を見つけました。

[The target group does not have an associated load balancer](https://stackoverflow.com/questions/53971873/the-target-group-does-not-have-an-associated-load-balancer)

上記の記事では、CloudFormationがリソースを作成する際の依存関係のところで、問題が発生していると指摘しています。

つまり、今回で言えば、`ServiceXXX`リソースが依存する`HttpListenerRuleWebBlue`リソースが正しく作成される前に、`ServiceXXX`リソースが作成されようとしていたため、このエラーが発生したと言えます。

`ServiceXXX`の`LoadBalancers`プロパティが`TargetGroupWebBlue`を指定しているので、そのターゲットグループはロードバランサーに関連している必要があります。したがって、`TargetGroupWebBlue`がロードバランサーに関連付けられていること必要があります。

エラーメッセージにあった`does not have an associated load balancer`の意味がこれでわかりました。

CloudFormationは、リソースの作成順序を決定する際に、リソース間の依存関係を自動的に解決しようとしてくれますが、リソース間で相互に依存し合う場合や、外部リソースに依存する場合などに、依存関係の解決が難しくなることがあるようです。

まず、今回は以下のような関係でした。

・`HttpListenerRuleWebBlue`は`TargetGroupWebBlue`を必要としている
・`HttpListenerRuleWebBlue`は、`HttpDefaultListener-XXX`（外部スタックのテンプレートからインポート）を必要としている
・`HttpDefaultListener-XXX`は`LoadBalancer`を必要としている
・`ServiceXXX`は`TargetGroupWebBlue`を必要としている

ここで、おそらくCloudFormationは以下の順序で作成を試みたのでしょう（推測）。

①ふむふむ、`HttpListenerRuleWebBlue`は`TargetGroupWebBlue`を必要としているのか、それでは先に`TargetGroupWebBlue`を作らないと。
②`TargetGroupWebBlue`を作ったぞ。
③お、`TargetGroupWebBlue`を必要としているリソースが他にもある。`ServiceXXX`で`!Ref`されているからこっち先に作っておこう。
④あれ、`ServiceXXX`は`HttpListenerRuleWebBlue`が必要だって。そんなリソースないぞ？
↓
結果：`ServiceXXX`のターゲットグループにELBが紐づいていない、というエラーになる

# 解決策は`DependsOn`
上記記事のように、`DependsOn: Listener`を追加してあげます。
これだけでOKです！

```yaml
  ServiceXXX:
    Type: AWS::ECS::Service
    DependsOn: HttpListenerRuleWebBlue # これを追加！
    Properties: 
```

`DependsOn`プロパティは、CloudFormationスタック内のリソース間の依存関係を定義するために使用されます。特定のリソースが他のリソースに依存している場合、`DependsOn`を使用して、依存関係を明示的に設定できるとのこと。

もう少し丁寧にすると、CloudFormationに対して`ServiceXXX`リソースは`HttpListenerRuleWebBlue`リソースに依存しているよ、ということを明示的に伝えます。
すると、`HttpListenerRuleWebBlue`リソースが正常に作成されてから `ServiceXXX`リソースが作成されるように制御されます。
そして、`HttpListenerRuleWebBlue`リソースの作成には`TargetGroupWebBlue`リソースが正しく関連付けられているため、エラーが解消された、というわけです。

これにより、CloudFormationはリソースを適切な順序で作成または更新することができました。
CloudFormationプロフェッショナルへの道は遠い...