aws / aws-cdk

The AWS Cloud Development Kit is a framework for defining cloud infrastructure in code
https://aws.amazon.com/cdk
Apache License 2.0
11.34k stars 3.76k forks source link

(ecs): FargateService fails on imported Subnets if availability zone attribute is missing #30104

Open so0k opened 1 month ago

so0k commented 1 month ago

Describe the bug

Creating a new ecs.FargateService with specific subnet-ids via its vpcSubnets: ec2.SubnetSelection property fails even when only subnet-id is required.

Note: we have an environment constraint and can't use vpc.fromLookup() during CDK Synth to use CDK Context and automatically load all subnets and their attributes.

Due to environment constraints, our AWS CDK Construct must depend on existing CloudFormation Stack Exported Outputs in the AWS Environment.

Sample code:

const privateSubnetsRef = Fn.importValue(
  stackName + "-privatesubnets",
);
const privateSubnets = Fn.split(",", privateSubnetsRef).map(s => ec2.Subnet.fromSubnetId(this, s, s))

vpc.selectSubnets({
  subnets: privateSubnets,
})

Work Around:

// Create FargateService with default `networkConfiguration`
const svc = new ecs.FargateService(this, "FargateService", {
  taskDefinition,
  cluster: this.ecsCluster,
  desiredCount: this.data.desired,
  // Use default subnetSelection ...
  // Public subnets if `assignPublicIp` is set, otherwise the first available one of Private, Isolated, Public, in that order.
  // vpcSubnets: this.vpc.selectSubnets({
  //   subnets: privateSubnets,
  // }),
  // required, else CDK creates new SecurityGroup
  securityGroups: [this.securityGroup],
});

// Use AWS CDK escape hatch to override networkConfiguration without `vpc.selectSubnets` limitations....
// https://docs.aws.amazon.com/cdk/v2/guide/cfn_layer.html#develop-customize-escape-l2
const cfnSvc = svc.node.defaultChild as ecs.CfnService;
cfnSvc.networkConfiguration = {
  awsvpcConfiguration: {
    subnets: Fn.split(",", privateSubnetsRef),
    securityGroups: [Fn.ref("SecurityGroup")],
  },
};

Expected Behavior

Providing the ecsFargateService an explicit list of SubnetIds should not require the Availability Zone attribute per Subnet ... (the bug is caused by - L480:

https://github.com/aws/aws-cdk/blob/46168aac07c0d3f50ad10c31801751d083474081/packages/aws-cdk-lib/aws-ec2/lib/vpc.ts#L474-L486

Current Behavior

Error is thrown

You cannot reference a Subnet's availability zone if it was not supplied. Add the availabilityZone when importing using Subnet.fromSubnetAttributes()

      236 |       // ecs.FargateService -> launchType == "FARGATE"
      237 |       // TODO: is ServiceName: &configCode, required?
    > 238 |       const svc = new ecs.FargateService(this, "FargateService", {
          |                   ^
      239 |         taskDefinition,
      240 |         cluster: this.ecsCluster,
      241 |         desiredCount: this.data.desired,

      at ImportedSubnet.get availabilityZone [as availabilityZone] (node_modules/.pnpm/aws-cdk-lib@2.140.0_constructs@10.3.0/node_modules/aws-cdk-lib/aws-ec2/lib/vpc.js:1:35138)
      at node_modules/.pnpm/aws-cdk-lib@2.140.0_constructs@10.3.0/node_modules/aws-cdk-lib/aws-ec2/lib/vpc.js:1:2861
          at Array.map (<anonymous>)
      at Object.get availabilityZones [as availabilityZones] (node_modules/.pnpm/aws-cdk-lib@2.140.0_constructs@10.3.0/node_modules/aws-cdk-lib/aws-ec2/lib/vpc.js:1:2852)
      at ImportedVpc.reifySelectionDefaults (node_modules/.pnpm/aws-cdk-lib@2.140.0_constructs@10.3.0/node_modules/aws-cdk-lib/aws-ec2/lib/vpc.js:1:7431)
      at ImportedVpc.selectSubnetObjects (node_modules/.pnpm/aws-cdk-lib@2.140.0_constructs@10.3.0/node_modules/aws-cdk-lib/aws-ec2/lib/vpc.js:1:4733)
      at ImportedVpc.selectSubnets (node_modules/.pnpm/aws-cdk-lib@2.140.0_constructs@10.3.0/node_modules/aws-cdk-lib/aws-ec2/lib/vpc.js:1:2705)
      at FargateService.configureAwsVpcNetworkingWithSecurityGroups (node_modules/.pnpm/aws-cdk-lib@2.140.0_constructs@10.3.0/node_modules/aws-cdk-lib/aws-ecs/lib/base/base-service.js:1:25843)
      at new FargateService (node_modules/.pnpm/aws-cdk-lib@2.140.0_constructs@10.3.0/node_modules/aws-cdk-lib/aws-ecs/lib/fargate/fargate-service.js:1:3382)
      at new EcsService (src/index.ts:238:19)
      at Object.<anonymous> (test/ecs-service-stack.test.ts:113:5)

Reproduction Steps

import { Fn } from "aws-cdk-lib";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as ecs from "aws-cdk-lib/aws-ecs";
import { Construct } from "constructs";

export interface EcsServiceTestProps {
  /**
   * The environment stack name passed in during synth
   *
   * required to import environment stack exported outputs
   */
  environmentStackName: string;
}

export class EcsServiceTest extends Construct {
  constructor(scope: Construct, name: string, props: EcsServiceTestProps) {
    {
      super(scope, name);

      // Define "Environment" Stack Imports
      const vpcRef = Fn.importValue(props.environmentStackName + "-vpc");
      const availabilityZonesRef = Fn.importValue(
        props.environmentStackName + "-azs",
      );
      const ecsClusterNameRef = Fn.importValue(
        props.environmentStackName + "-cluster",
      );
      const privateSubnetsRef = Fn.importValue(
        props.environmentStackName + "-privatesubnets",
      );

      // Import VPC, Subnets and ECS Cluster
      const vpc = ec2.Vpc.fromVpcAttributes(this, "Vpc", {
        availabilityZones: Fn.split(",", availabilityZonesRef),
        privateSubnetIds: Fn.split(",", privateSubnetsRef),
        vpcId: vpcRef,
      });
      const privateSubnets = Fn.split(",", privateSubnetsRef).map((s) =>
        ec2.Subnet.fromSubnetId(this, s, s),
      );
      const cluster = ecs.Cluster.fromClusterAttributes(this, "Cluster", {
        clusterName: ecsClusterNameRef.toString(),
        vpc: vpc,
      });

      const cpu = 1014;
      const memoryLimitMiB = 100;
      const taskDefinition = new ecs.FargateTaskDefinition(
        this,
        "TaskDefinition",
        {
          cpu,
          memoryLimitMiB,
        },
      );

      taskDefinition.addContainer("MyService", {
        image: ecs.ContainerImage.fromRegistry("my-repo/my-image"),
        cpu,
        memoryLimitMiB,
      });

      new ecs.FargateService(this, "FargateService", {
        taskDefinition,
        cluster,
        desiredCount: 2,
        // This fails
        vpcSubnets: vpc.selectSubnets({
          subnets: privateSubnets,
        }),
        // required, else CDK creates new SecurityGroup
        securityGroups: [
          ec2.SecurityGroup.fromSecurityGroupId(
            this,
            "SecurityGroup",
            Fn.ref("SecurityGroup"),
          ),
        ],
      });

      // // AWS CDK escape hatch to override networkConfiguration and work around failure
      // // https://docs.aws.amazon.com/cdk/v2/guide/cfn_layer.html#develop-customize-escape-l2
      // const cfnSvc = svc.node.defaultChild as ecs.CfnService;
      // cfnSvc.networkConfiguration = {
      //   awsvpcConfiguration: {
      //     subnets: Fn.split(",", privateSubnetsRef),
      //     securityGroups: [Fn.ref("SecurityGroup")],
      //   },
      // };
    }
  }
}

Possible Solution

Either:

Additional Information/Context

No response

CDK CLI Version

2.140.0

Framework Version

2.140.0

Node.js Version

20.12.2

OS

WSL

Language

TypeScript

Language Version

5.3.3

Other information

No response

so0k commented 1 month ago

@khushail - any details missing to reproduce?