Repository: pulumi/pulumi-awsx (ts)

Issue:
Misleading error message for invalid ECS task memory configuration
### What happened?

I just bumped the memory for the two containers of my `FargateService` task from 4GiB + 12GiB to 8GiB + 32GiB. When attempting to deploy this new configuration, I get the following error:

```
    aws:ecs:TaskDefinition (my-service-prod):
      error: 1 error occurred:
      	* failed creating ECS Task Definition (my-service-prod-718b5d41): ClientException: The 'memory' setting for container 'worker' is greater than for the task.
```

I could specify the exact memory for the task, but the docstring for `taskDefinitionArgs.memory` says "If not provided, a default will be computed based on the cumulative needs specified by [containerDefinitions]", so this seems like a bug

This seems very similar to #188

### Example

```
const service = new awsx.ecs.FargateService(`my-service-${stack}`, {
  cluster: cluster.arn,
  assignPublicIp: true,
  desiredCount: 2,
  deploymentMinimumHealthyPercent: 100,
  deploymentMaximumPercent: 200,
  taskDefinitionArgs: {
    taskRole: {
      roleArn: role.arn,
    },
    containers: {
      server: {
        command: ['yarn', 'server'],
        image: image.imageUri,
        cpu: 1024,
        memory: 1024 * 8,
        essential: true,
      },
      worker: {
        command: ['yarn', 'worker'],
        image: image.imageUri,
        cpu: 3072,
        memory: 1024 * 32,
        essential: true,
      },
    },
  },
});
```

### Output of `pulumi about`

```
CLI          
Version      3.105.0
Go Version   go1.21.6
Go Compiler  gc

Plugins
NAME        VERSION
aws         5.33.0
awsx        1.0.2
cloudflare  5.0.0
docker      3.6.1
nodejs      unknown
tls         4.10.0

Host     
OS       darwin
Version  14.2.1
Arch     arm64

This project is written in nodejs: executable='/Users/rpmccarter/.nvm/versions/node/v20.10.0/bin/node' version='v20.10.0'

Current Stack: Mintlify/leaves/staging

... (can send if needed, but would prefer to keep private)

Dependencies:
NAME                VERSION
@pulumi/aws         5.33.0
@pulumi/awsx        1.0.2
@pulumi/cloudflare  5.0.0
@pulumi/pulumi      3.60.0
@pulumi/tls         4.10.0
@types/node         16.18.22
rimraf              5.0.5
typescript          5.3.3

Pulumi locates its logs in /var/folders/dn/z0by0dcj1gnbkjr6_t71hp_m0000gn/T/ by default
```

### Contributing

Vote on this issue by adding a 👍 reaction. 
To contribute a fix for this issue, leave a comment (and link to your pull request, if you've opened one already). 


Pull request description:
Ensure requested ECS container resources are within Fargate limits
In the prior implementation, if an ECS task's requested resources surpassed the Fargate limits, the system defaulted to the maximum resources permitted by Fargate.
This led to obscure AWS API errors indicating that the containers were requesting more resources than the task could provide.

Fixes #1220 

Patch:
diff --git a/awsx/ecs/fargateMemoryAndCpu.ts b/awsx/ecs/fargateMemoryAndCpu.ts
index 21eb96b3..5bc2ccc0 100644
--- a/awsx/ecs/fargateMemoryAndCpu.ts
+++ b/awsx/ecs/fargateMemoryAndCpu.ts
@@ -64,15 +64,15 @@ type FargateContainerMemoryAndCpu = {
 
 export function calculateFargateMemoryAndCPU(containers: FargateContainerMemoryAndCpu[]) {
   // First, determine how much VCPU/GB that the user is asking for in their containers.
-  let { requestedVCPU, requestedGB } = getRequestedVCPUandMemory(containers);
+  const { requestedVCPU, requestedGB } = getRequestedVCPUandMemory(containers);
 
-  // Don't exceed the max CPU that can be requested. No need to worry about a
-  // min as we're finding the first config that provides *at least* this amount.
-  requestedVCPU = Math.min(requestedVCPU, maxVCPU);
-
-  // Don't exceed the max memory that can be requested. No need to worry about
-  // a min as we're finding the first config that provides *at least* this amount.
-  requestedGB = Math.min(requestedGB, maxMemGB);
+  // Ensure that the requested resources are within the bounds of what Fargate can support.
+  // Task creation will fail if we allocate a smaller amount of CPU or memory than what the containers request
+  if (requestedVCPU > maxVCPU || requestedGB > maxMemGB) {
+    throw new Error(
+      `Requested resources exceed the maximum allowed for Fargate. Requested: ${requestedVCPU} vCPU and ${requestedGB}GB. Max: ${maxVCPU} vCPU and ${maxMemGB}GB.`,
+    );
+  }
 
   // Get all configs that can at least satisfy this pair of cpu/memory needs.
   const config = fargateConfigsByPriceAscending().find(


Test patch:
diff --git a/awsx/ecs/fargateMemoryAndCpu.test.ts b/awsx/ecs/fargateMemoryAndCpu.test.ts
index c1cf2b1b..01eb4b80 100644
--- a/awsx/ecs/fargateMemoryAndCpu.test.ts
+++ b/awsx/ecs/fargateMemoryAndCpu.test.ts
@@ -48,4 +48,63 @@ describe("max vcpu and memory", () => {
     expect(memCpu.cpu).toEqual("8192");
     expect(memCpu.memory).toEqual("21504");
   });
+
+  it("does not throw error if containers request exactly the maximum resources fargate allows", async () => {
+    expect(() => {
+      calculateFargateMemoryAndCPU([
+        {
+          cpu: (maxVCPU * 1024) / 2,
+          memory: (maxMemGB * 1024) / 2,
+        },
+        {
+          cpu: (maxVCPU * 1024) / 2,
+          memory: (maxMemGB * 1024) / 2,
+        },
+      ]);
+    }).not.toThrowError();
+  });
+  it("throws error if containers request more resources than fargate allows", async () => {
+    expect(() => {
+      calculateFargateMemoryAndCPU([
+        {
+          cpu: 16 * 1024,
+          memory: 120 * 1024,
+        },
+        {
+          cpu: 16 * 1024,
+          memory: 120 * 1024,
+        },
+      ]);
+    }).toThrowError(
+      `Requested resources exceed the maximum allowed for Fargate. Requested: 32 vCPU and 240GB. Max: ${maxVCPU} vCPU and ${maxMemGB}GB.`,
+    );
+  });
+  it("throws error if containers request more CPU than fargate allows", async () => {
+    expect(() => {
+      calculateFargateMemoryAndCPU([
+        {
+          cpu: (maxVCPU + 1) * 1024,
+          memory: maxMemGB * 1024,
+        },
+      ]);
+    }).toThrowError(
+      `Requested resources exceed the maximum allowed for Fargate. Requested: ${
+        maxVCPU + 1
+      } vCPU and ${maxMemGB}GB. Max: ${maxVCPU} vCPU and ${maxMemGB}GB.`,
+    );
+  });
+  it("throws error if containers requests more memory than fargate allows", async () => {
+    expect(() => {
+      calculateFargateMemoryAndCPU([
+        {
+          cpu: maxVCPU * 1024,
+          memory: (maxMemGB + 1) * 1024,
+        },
+      ]);
+    }).toThrowError(
+      `Requested resources exceed the maximum allowed for Fargate. Requested: ${maxVCPU} vCPU and ${
+        maxMemGB + 1
+      }GB. Max: ${maxVCPU} vCPU and ${maxMemGB}GB.`,
+    );
+  });
 });