💭

ECS環境をCDKのL3で作成した時にApplicationListener.fromLookup()がエラーになる時の対応

2024/01/07に公開

先日CDKでaws-ecs-patternsのApplicationLoadBalancedFargateServiceを使った構築を進めていたら、ALBのリスナー設定で以下のissueと同様の状態になり手こずりました。

https://github.com/aws/aws-cdk/issues/20996

どうにか解決出来ないかと調査したので、その備忘録となります。

環境

CDK: 2.118.0

再現

以下のようなスタックを作成します。

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs'
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'
import * as ecsPatterns from 'aws-cdk-lib/aws-ecs-patterns'

export class Test1Stack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const vpc = new ec2.Vpc(this, 'VPC');

    const cluster = new ecs.Cluster(this, 'Cluster', { vpc });

    const service = new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'Service', {
      cluster,
      memoryLimitMiB: 512,
      cpu: 256,
      taskImageOptions: {
        image: ecs.ContainerImage.fromRegistry("nginx"),
      },
    });
  }
}

デプロイが完了した後に、以下の内容を追記してみます。

    const listener = elbv2.ApplicationListener.fromLookup(this, 'ALBListener', {
      loadBalancerArn: service.loadBalancer.loadBalancerArn,
      listenerArn: service.loadBalancer.listeners[0].listenerArn,
      listenerProtocol: elbv2.ApplicationProtocol.HTTP,
      listenerPort: 80
    })
    listener.addAction('FixedResponseAction', {
      priority: 10,
      conditions: [
        elbv2.ListenerCondition.pathPatterns(['/ok']),
      ],
      action: elbv2.ListenerAction.fixedResponse(200, {
        contentType: 'text/plain',
        messageBody: 'OK',
      })
    })

すると、デプロイコマンド実行時に以下のようなエラーが発生します。

Error: All arguments to look up a load balancer listener must be concrete (no Tokens)

原因

このエラー文はどこでひっかかっているかというと、aws-cdk-libでは以下の記述のチェックに該当してしまっています。

https://github.com/aws/aws-cdk/blob/v2-release/packages/aws-cdk-lib/aws-elasticloadbalancingv2/lib/alb/application-listener.ts#L138-L140

Token.isUnresolved(options.listenerArn) という部分を見る限り、listerArnに問題がありそうです。

そこで、console.log(service.listener.listenerArn) と記述して確認してみたところ、確かに、ARNの文字列ではなく、以下の内容が出力されました。

${Token[TOKEN.193]}

ここで表示されているTokenは、要するにCDK実行後に値が確定するもののようです。

これは、構築時にはまだ値がわからないが、AWS CDK後で利用可能になるトークンをエンコードする方法です。AWS CDKはこれらのプレースホルダトークンを呼び出します。この場合は、文字列としてエンコードされたトークンです。

https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/tokens.html

つまり、ちゃんと具体的な値が渡されていないからロードバランサーのリスナーを探せないよといった内容のエラーということですね。

対応

fromApplicationListenerAttributes()を使う

ALBのリスナーを検索するメソッドは fromLookup() だけでなく、 fromApplicationListenerAttributes() もあります。
こちらの方は渡すパラメータにTokenがあっても問題なく動作します。

https://github.com/aws/aws-cdk/blob/v2-release/packages/aws-cdk-lib/aws-elasticloadbalancingv2/lib/alb/application-listener.ts#L161-L163

    const listener = elbv2.ApplicationListener.fromApplicationListenerAttributes(this, 'ALBListener', {
      listenerArn: service.loadBalancer.listeners[0].listenerArn,
      securityGroup: service.loadBalancer.connections.securityGroups[0],
      defaultPort: 80
    })
    listener.addAction('FixedResponseAction', {
      priority: 10,
      conditions: [
        elbv2.ListenerCondition.pathPatterns(['/ok']),
      ],
      action: elbv2.ListenerAction.fixedResponse(200, {
        contentType: 'text/plain',
        messageBody: 'OK',
      })
    })

L3を使わない

自動で作成されるALBやリスナーを使わず、全て明示的に記述してしまうという方針です。この方式だと、リスナーは自身で作成して定義しているので、そもそもfromLookup()で取得する必要がありません。
単純なALB+ECSの設定ならaws-ecs-patternsは非常に便利ですが、リスナー設定やターゲットグループ等の設定が複雑になる場合は使わないという選択も有りかと思いました。

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs'
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'

export class Test2Stack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const vpc = new ec2.Vpc(this, 'VPC');

    const cluster = new ecs.Cluster(this, 'Cluster', { vpc });

    const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef');

    taskDefinition.addContainer('DefaultContainer', {
      image: ecs.ContainerImage.fromRegistry('nginx'),
      portMappings: [{ containerPort: 80 }],
      memoryLimitMiB: 512,
    });

    const service = new ecs.FargateService(this, 'Service', {
      cluster,
      taskDefinition,
    });

    const lb = new elbv2.ApplicationLoadBalancer(this, 'LB', { vpc, internetFacing: true });
    const listener = lb.addListener('Listener', { port: 80 });
    listener.addTargets('ECS1', {
      port: 80,
      targets: [service],
    });

    listener.addAction('FixedResponseAction', {
      priority: 10,
      conditions: [
        elbv2.ListenerCondition.pathPatterns(['/ok']),
      ],
      action: elbv2.ListenerAction.fixedResponse(200, {
        contentType: 'text/plain',
        messageBody: 'OK',
      })
    })
  }
}

参考

Discussion