I had this issue, and eventually sorted it out.
My solution below is to:
- Set up the ECS in private subnet
- Add AWS PrivateLink endpoints in VPC
Post my CDK code here for reference. I pasted some documentation links in the function comments for you to better understand its purpose.
This is the EcsStack:
export class EcsStack extends Stack {
    constructor(scope: cdk.App, id: string, props: EcsStackProps) {
        super(scope, id, props);
        this.createOrderServiceCluster(props.vpc);
    }
    private createOrderServiceCluster(serviceVpc:ec2.IVpc) {
        const ecsClusterName = "EcsClusterOfOrderService";
        const OrderServiceCluster = new ecs.Cluster(this, ecsClusterName, {
          vpc: serviceVpc,
          clusterName: ecsClusterName
        });
        // Now ApplicationLoadBalancedFargateService just pick a randeom private subnet.
        // https://github.com/aws/aws-cdk/issues/8621
        new ecs_patterns.ApplicationLoadBalancedFargateService(this, "FargateOfOrderService", {
          cluster: OrderServiceCluster, // Required
          cpu: 512, // Default is 256
          desiredCount: 1, // Default is 1
          taskImageOptions: { 
            image: ecs.ContainerImage.fromRegistry("12345.dkr.ecr.us-east-1.amazonaws.com/comics:user-service"),
            taskRole: this.createEcsTaskRole(),
            executionRole: this.createEcsExecutionRole(),
            containerPort: 8080
          },
          memoryLimitMiB: 2048, // Default is 512
          // creates a public-facing load balancer that we will be able to call 
          // from curl or our web browser. This load balancer will forward calls 
          // to our container on port 8080 running inside of our ECS service.
          publicLoadBalancer: true // Default is false
        });
    }
    /**
     * This IAM role is the set of permissions provided to the ECS Service Team to execute ECS Tasks on your behalf.
     * It is NOT the permissions your application will have while executing.
     * https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_execution_IAM_role.html
     * @private
     */
    private createEcsExecutionRole() : iam.IRole {
        const ecsExecutionRole = new iam.Role(this, 'EcsExecutionRole', {
            //assumedBy: new iam.ServicePrincipal(ecsTasksServicePrincipal),
            assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
            roleName: "EcsExecutionRole",
        });
        ecsExecutionRole.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonEC2ContainerRegistryReadOnly'));
        ecsExecutionRole.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('CloudWatchLogsFullAccess'));
        return ecsExecutionRole;
    }
    /**
     * Creates the IAM role (with all the required permissions) which will be used by the ECS tasks.
     * https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html
     * @private
     */
    private createEcsTaskRole(): iam.IRole {
        const ecsTaskRole = new iam.Role(this, 'OrderServiceEcsTaskRole', {
            //assumedBy: new iam.ServicePrincipal(ecsTasksServicePrincipal),
            assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
            roleName: "OrderServiceEcsTaskRole",
        });
        ecsTaskRole.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonEC2ContainerRegistryReadOnly'));
        ecsTaskRole.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('CloudWatchLogsFullAccess'));
        ecsTaskRole.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonS3ReadOnlyAccess'));
        return ecsTaskRole;
    }
}
This is code snippet of the VpcStack:
export class VpcStack extends Stack {
    readonly coreVpc : ec2.Vpc;
    constructor(scope: cdk.App, id: string) {
        super(scope, id);
        this.coreVpc = new ec2.Vpc(this, "CoreVpc", {
            cidr: '10.0.0.0/16',
            natGateways: 1,
            enableDnsHostnames: true,
            enableDnsSupport: true,
            maxAzs: 3,
            subnetConfiguration: [
            {
              cidrMask: 28,
              name: 'Public',
              subnetType: ec2.SubnetType.PUBLIC,
            },
            {
              cidrMask: 24,
              name: 'Private',
              subnetType: ec2.SubnetType.PRIVATE,
            }
          ]
        });
   
        this.setupInterfaceVpcEndpoints();
    }
    /**
     * Builds VPC endpoints to access AWS services without using NAT Gateway.
     * @private
     */
    private setupInterfaceVpcEndpoints(): void {
        // Allow ECS to pull Docker images without using NAT Gateway
        // https://docs.aws.amazon.com/AmazonECR/latest/userguide/vpc-endpoints.html
        this.addInterfaceEndpoint("ECRDockerEndpoint", ec2.InterfaceVpcEndpointAwsService.ECR_DOCKER);
        this.addInterfaceEndpoint("ECREndpoint", ec2.InterfaceVpcEndpointAwsService.ECR);
        this.addInterfaceEndpoint("SecretManagerEndpoint", ec2.InterfaceVpcEndpointAwsService.SECRETS_MANAGER);
        this.addInterfaceEndpoint("CloudWatchEndpoint", ec2.InterfaceVpcEndpointAwsService.CLOUDWATCH);
        this.addInterfaceEndpoint("CloudWatchLogsEndpoint", ec2.InterfaceVpcEndpointAwsService.CLOUDWATCH_LOGS);
        this.addInterfaceEndpoint("CloudWatchEventsEndpoint", ec2.InterfaceVpcEndpointAwsService.CLOUDWATCH_EVENTS);
        this.addInterfaceEndpoint("SSMEndpoint", ec2.InterfaceVpcEndpointAwsService.SSM);
    }
    private addInterfaceEndpoint(name: string, awsService: ec2.InterfaceVpcEndpointAwsService): void {
        const endpoint: ec2.InterfaceVpcEndpoint = this.coreVpc.addInterfaceEndpoint(`${name}`, {
            service: awsService
        });
        endpoint.connections.allowFrom(ec2.Peer.ipv4(this.coreVpc.vpcCidrBlock), endpoint.connections.defaultPort!);
    }
}