From fa1e865f5094e1a036182ffffbc4cd7a1d9a55f7 Mon Sep 17 00:00:00 2001 From: Ryan Wilson Date: Sat, 14 Feb 2026 16:15:40 -0800 Subject: [PATCH] Fix ALB listener default action, auto-import numberOfAzs, and correct docs - Fix HTTP listener in spicy-alb.ts missing default action when no certificate is provided, which would cause CDK synth to fail - Auto-import numberOfAzs from VPC stack exports (NumberOfAZs) in cluster, service, and ALB stacks when not provided via context - Fix CDK_SYNTH_EXAMPLES.md ALB examples using raw vpcId/subnetIds that don't match the actual fromContext() implementation (requires clusterName) - Fix docs overstating "only clusterName required" to list actual required params - Remove package-lock.json and add to .gitignore (project uses pnpm) Co-Authored-By: Claude Opus 4.6 --- .gitignore | 3 ++ docs/CDK_SYNTH_EXAMPLES.md | 45 ++++++++++--------- docs/ECS_SERVICE.md | 6 +-- resources/lib/constructs/spicy-alb.ts | 37 ++++++++------- resources/lib/stacks/spicy-alb-stack.ts | 12 ++++- .../lib/stacks/spicy-ecs-cluster-stack.ts | 12 ++++- .../lib/stacks/spicy-ecs-service-stack.ts | 12 ++++- 7 files changed, 80 insertions(+), 47 deletions(-) diff --git a/.gitignore b/.gitignore index 64d87ac..f8ec465 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ node_modules cdk.out OLD_STUFF/ OLDER_STUFF/ + +# Use pnpm only +package-lock.json diff --git a/docs/CDK_SYNTH_EXAMPLES.md b/docs/CDK_SYNTH_EXAMPLES.md index 60773e4..18565a7 100644 --- a/docs/CDK_SYNTH_EXAMPLES.md +++ b/docs/CDK_SYNTH_EXAMPLES.md @@ -111,7 +111,7 @@ pnpm exec cdk synth \ ### Minimal Cluster (No Load Balancers) - Using CloudFormation Imports -**Minimal props approach:** Only `vpcStackName` required. All VPC details auto-import from VPC stack exports. +**Minimal required props:** `vpcStackName` and tags (`ownerTag`, `productTag`). All VPC details auto-import from VPC stack exports, including `numberOfAzs`. ```bash pnpm exec cdk synth \ @@ -128,7 +128,7 @@ pnpm exec cdk synth \ - VPC ID from `${vpcStackName}-VPCID` - VPC CIDR from `${vpcStackName}-VPCCIDR` -- Number of AZs from `${vpcStackName}-NumberOfAZs` +- Number of AZs from `${vpcStackName}-NumberOfAZs` (or override with `-c numberOfAzs=N`) - Private subnet IDs from `${vpcStackName}-PrivateSubnetA1ID`, `${vpcStackName}-PrivateSubnetB1ID`, etc. - Public subnet IDs from `${vpcStackName}-PublicSubnetAID`, `${vpcStackName}-PublicSubnetBID`, etc. (if `createExternalLoadBalancer=true`) - Availability zones auto-derived from region and number of AZs @@ -216,14 +216,13 @@ The ALB stack creates a persistent Application Load Balancer that can be shared ### Internet-Facing ALB +**Uses CloudFormation imports:** Only `clusterName` (or `clusterStackName`) and `numberOfAzs` required. VPC details and subnets are auto-imported from cluster/VPC stack exports. `numberOfAzs` is also auto-imported from the VPC stack if not provided. + ```bash pnpm exec cdk synth \ -c stackType=alb \ -c stackName=my-service-alb \ - -c vpcId=vpc-12345678 \ - -c vpcCidrBlock=10.0.0.0/16 \ - -c availabilityZones=ca-central-1a,ca-central-1b \ - -c subnetIds=subnet-xxx,subnet-yyy \ + -c clusterName=my-cluster \ -c scheme=internet-facing \ -c certificateArn=arn:aws:acm:ca-central-1:123456789:certificate/xxx \ -c logsBucketName=my-cluster-logs \ @@ -233,13 +232,21 @@ pnpm exec cdk synth \ -c environment=dev ``` +**What auto-imports:** + +- VPC stack name from `${clusterStackName}-VPCStackName` (cluster stack export) +- VPC ID from `${clusterStackName}-VPC` (cluster stack export) +- VPC CIDR from `${vpcStackName}-VPCCIDR` (VPC stack export) +- Number of AZs from `${vpcStackName}-NumberOfAZs` (VPC stack export) +- Public subnet IDs from `${vpcStackName}-PublicSubnetAID`, etc. (for internet-facing) +- Private subnet IDs from `${vpcStackName}-PrivateSubnetA1ID`, etc. (for internal) + **Creates:** - Application Load Balancer (internet-facing) - Security Group for ALB -- HTTP Listener (port 80) +- HTTP Listener (port 80) with HTTP→HTTPS redirect - HTTPS Listener (port 443) with SSL certificate -- HTTP→HTTPS redirect rule - ALB access logs to S3 (if logsBucketName provided) ### Internal ALB @@ -248,10 +255,7 @@ pnpm exec cdk synth \ pnpm exec cdk synth \ -c stackType=alb \ -c stackName=my-service-alb \ - -c vpcId=vpc-12345678 \ - -c vpcCidrBlock=10.0.0.0/16 \ - -c availabilityZones=ca-central-1a,ca-central-1b \ - -c subnetIds=subnet-aaa,subnet-bbb \ + -c clusterName=my-cluster \ -c scheme=internal \ -c certificateArn=arn:aws:acm:ca-central-1:123456789:certificate/xxx \ -c logsBucketName=my-cluster-logs \ @@ -267,10 +271,7 @@ pnpm exec cdk synth \ pnpm exec cdk synth \ -c stackType=alb \ -c stackName=my-service-alb \ - -c vpcId=vpc-12345678 \ - -c vpcCidrBlock=10.0.0.0/16 \ - -c availabilityZones=ca-central-1a,ca-central-1b \ - -c subnetIds=subnet-xxx,subnet-yyy \ + -c clusterName=my-cluster \ -c scheme=internet-facing \ -c certificateArn=arn:aws:acm:ca-central-1:123456789:certificate/xxx \ -c logsBucketName=my-cluster-logs \ @@ -295,7 +296,7 @@ pnpm exec cdk synth \ ### Minimal Service (No Load Balancer) - Using CloudFormation Imports -**Minimal props approach:** Only `clusterName` is required. VPC info auto-imports from cluster stack exports. +**Minimal required props:** `clusterName`, `serviceName`, `image`, `containerPort`, and tags (`ownerTag`, `productTag`). All VPC info auto-imports from cluster/VPC stack exports. ```bash pnpm exec cdk synth \ @@ -316,11 +317,11 @@ pnpm exec cdk synth \ - VPC stack name from `${clusterStackName}-VPCStackName` (cluster stack export) - VPC ID from `${clusterStackName}-VPC` (cluster stack export) - VPC CIDR from `${vpcStackName}-VPCCIDR` (VPC stack export, using imported VPC stack name) -- Private subnets from `${vpcStackName}-PrivateSubnetA1ID`, etc. (VPC stack exports) - Number of AZs from `${vpcStackName}-NumberOfAZs` (VPC stack export) +- Private subnets from `${vpcStackName}-PrivateSubnetA1ID`, etc. (VPC stack exports) - Logs bucket from `${clusterStackName}-logs-s3-bucket` (cluster stack export) -**Note:** The service stack imports the VPC stack name from the cluster stack export `${clusterStackName}-VPCStackName`, then uses that to import all VPC details (CIDR, subnets, AZs) from the VPC stack exports. +**Note:** The service stack imports the VPC stack name from the cluster stack export `${clusterStackName}-VPCStackName`, then uses that to import all VPC details (CIDR, subnets, AZs) from the VPC stack exports. `numberOfAzs` can be provided explicitly via `-c numberOfAzs=N` but will auto-import from the VPC stack if omitted. ### Service with Cluster ALB - Using CloudFormation Imports @@ -784,7 +785,7 @@ pnpm exec cdk synth \ | `vpcCidrBlock` | - | VPC CIDR (required, or auto-imported from `${vpcStackName}-VPCCIDR` using imported VPC stack name) | | `availabilityZones` | - | Comma-separated AZs (required, or auto-derived from VPC stack imports with `numberOfAzs`) | | `privateSubnetIds` | - | Private subnet IDs (required, or auto-imported from `${vpcStackName}-PrivateSubnetA1ID`, etc.) | -| `numberOfAzs` | - | Number of AZs (required when importing subnets from VPC stack) | +| `numberOfAzs` | auto | Number of AZs (auto-imported from VPC stack, or override explicitly) | | `publicSubnetIds` | - | Public subnet IDs (for individual ALB) | | `serviceName` | - | Service name (required) | | `image` | - | Docker image URI (required) | @@ -918,8 +919,8 @@ pnpm exec cdk synth -c stackType=vpc -c stackName=test -c ownerTag=T -c productT pnpm exec cdk synth -c stackType=ecs-cluster -c stackName=test -c vpcStackName=test-vpc -c ownerTag=T -c productTag=t -c componentTag=c -c environment=d ``` -**Service:** +**Service (using CloudFormation imports):** ```bash -pnpm exec cdk synth -c stackType=ecs-service -c stackName=test -c clusterName=test -c vpcId=vpc-123 -c vpcCidrBlock=10.0.0.0/16 -c availabilityZones=ca-central-1a -c privateSubnetIds=subnet-123 -c serviceName=test -c image=nginx:latest -c containerPort=80 -c ownerTag=T -c productTag=t -c componentTag=s -c environment=d +pnpm exec cdk synth -c stackType=ecs-service -c stackName=test -c clusterName=test -c serviceName=test -c image=nginx:latest -c containerPort=80 -c ownerTag=T -c productTag=t -c componentTag=s -c environment=d ``` diff --git a/docs/ECS_SERVICE.md b/docs/ECS_SERVICE.md index 26a2047..97e7956 100644 --- a/docs/ECS_SERVICE.md +++ b/docs/ECS_SERVICE.md @@ -18,7 +18,7 @@ Deploy ECS services with mixed capacity provider strategies for EC2 + Fargate bu ### Minimal Jenkinsfile (EC2 Only) - Using CloudFormation Imports -**Minimal props:** Only `clusterName` required. VPC info auto-imports from cluster stack exports. +**Minimal required props:** `clusterName`, `numberOfAzs`, `serviceName`, `image`, `containerPort`, and tags (`ownerTag`, `productTag`). VPC info auto-imports from cluster/VPC stack exports. ```groovy @Library(["spicy-automation@main"]) _ @@ -49,11 +49,11 @@ spicyECSService( - VPC stack name from `${clusterStackName}-VPCStackName` (cluster stack export) - VPC ID from `${clusterStackName}-VPC` (cluster stack export) - VPC CIDR from `${vpcStackName}-VPCCIDR` (VPC stack export, using imported VPC stack name) +- Number of AZs from `${vpcStackName}-NumberOfAZs` (VPC stack export, or override with `numberOfAzs` parameter) - Private subnets from `${vpcStackName}-PrivateSubnetA1ID`, etc. (VPC stack exports) -- Number of AZs from `${vpcStackName}-NumberOfAZs` (VPC stack export) - Logs bucket from `${clusterStackName}-logs-s3-bucket` (cluster stack export) -**Note:** The service stack imports the VPC stack name from the cluster stack export `${clusterStackName}-VPCStackName`, then uses that to import all VPC details (CIDR, subnets, AZs) from the VPC stack exports. +**Note:** The service stack imports the VPC stack name from the cluster stack export `${clusterStackName}-VPCStackName`, then uses that to import all VPC details (CIDR, subnets, AZs) from the VPC stack exports. `numberOfAzs` is required by the Jenkins pipeline but auto-imports from VPC stack exports when using CDK directly. ### Mixed Capacity Strategy (EC2 + Fargate Burst) diff --git a/resources/lib/constructs/spicy-alb.ts b/resources/lib/constructs/spicy-alb.ts index d0b8ab6..d9968ca 100644 --- a/resources/lib/constructs/spicy-alb.ts +++ b/resources/lib/constructs/spicy-alb.ts @@ -161,12 +161,6 @@ export class SpicyAlb extends Construct { this.loadBalancer.logAccessLogs(props.logsBucket, prefix); } - // HTTP Listener - this.httpListener = this.loadBalancer.addListener('HTTPListener', { - port: 80, - protocol: elbv2.ApplicationProtocol.HTTP, - }); - // HTTPS Listener (if certificate provided) if (props.certificateArn) { this.httpsListener = this.loadBalancer.addListener('HTTPSListener', { @@ -178,17 +172,28 @@ export class SpicyAlb extends Construct { messageBody: 'No target groups configured', }), }); + } - // HTTP→HTTPS redirect (if enabled, default true) - if (props.redirectHttpToHttps !== false) { - this.httpListener.addAction('RedirectToHTTPS', { - action: elbv2.ListenerAction.redirect({ - protocol: 'HTTPS', - port: '443', - permanent: true, - }), - }); - } + // HTTP Listener - redirect to HTTPS if certificate provided, otherwise serve as primary + if (props.certificateArn && props.redirectHttpToHttps !== false) { + this.httpListener = this.loadBalancer.addListener('HTTPListener', { + port: 80, + protocol: elbv2.ApplicationProtocol.HTTP, + defaultAction: elbv2.ListenerAction.redirect({ + protocol: 'HTTPS', + port: '443', + permanent: true, + }), + }); + } else { + this.httpListener = this.loadBalancer.addListener('HTTPListener', { + port: 80, + protocol: elbv2.ApplicationProtocol.HTTP, + defaultAction: elbv2.ListenerAction.fixedResponse(404, { + contentType: 'text/plain', + messageBody: 'No target groups configured', + }), + }); } // Blue/Green DNS configuration diff --git a/resources/lib/stacks/spicy-alb-stack.ts b/resources/lib/stacks/spicy-alb-stack.ts index 9b80aa4..5cf7dfc 100644 --- a/resources/lib/stacks/spicy-alb-stack.ts +++ b/resources/lib/stacks/spicy-alb-stack.ts @@ -129,10 +129,18 @@ export class SpicyAlbStack extends cdk.Stack { // Import all VPC details from VPC stack exports const vpcCidrBlock = cdk.Fn.importValue(`${vpcStackName}-VPCCIDR`).toString(); + + // numberOfAzs: use context value if provided, otherwise auto-import from VPC stack export const numberOfAzsRaw = app.node.tryGetContext('numberOfAzs'); - const numberOfAzs = numberOfAzsRaw ? parseInt(numberOfAzsRaw, 10) : NaN; + let numberOfAzs: number; + if (numberOfAzsRaw) { + numberOfAzs = parseInt(numberOfAzsRaw, 10); + } else { + const imported = cdk.Fn.importValue(`${vpcStackName}-NumberOfAZs`).toString(); + numberOfAzs = parseInt(imported, 10); + } if (!numberOfAzs || Number.isNaN(numberOfAzs)) { - throw new Error('numberOfAzs is required in context (2-4) to import VPC subnets for ALB.'); + throw new Error('numberOfAzs is required (2-4). Provide via context or ensure VPC stack exports NumberOfAZs.'); } const azs = ['A', 'B', 'C', 'D'].slice(0, Math.min(Math.max(numberOfAzs, 1), 4)); diff --git a/resources/lib/stacks/spicy-ecs-cluster-stack.ts b/resources/lib/stacks/spicy-ecs-cluster-stack.ts index 7154e95..b28cdde 100644 --- a/resources/lib/stacks/spicy-ecs-cluster-stack.ts +++ b/resources/lib/stacks/spicy-ecs-cluster-stack.ts @@ -243,10 +243,18 @@ export class SpicyEcsClusterStack extends cdk.Stack { 'vpcStackName is required. Provide vpcStackName to import all VPC details from VPC stack exports.' ); } + // numberOfAzs: use context value if provided, otherwise auto-import from VPC stack export const numberOfAzsRaw = app.node.tryGetContext('numberOfAzs'); - const numberOfAzs = numberOfAzsRaw ? parseInt(numberOfAzsRaw, 10) : NaN; + let numberOfAzs: number; + if (numberOfAzsRaw) { + numberOfAzs = parseInt(numberOfAzsRaw, 10); + } else { + // Auto-import from VPC stack export + const imported = cdk.Fn.importValue(`${vpcStackName}-NumberOfAZs`).toString(); + numberOfAzs = parseInt(imported, 10); + } if (!numberOfAzs || Number.isNaN(numberOfAzs)) { - throw new Error('numberOfAzs is required in context (2-4) to import subnets from the VPC stack.'); + throw new Error('numberOfAzs is required (2-4). Provide via context or ensure VPC stack exports NumberOfAZs.'); } // Tags diff --git a/resources/lib/stacks/spicy-ecs-service-stack.ts b/resources/lib/stacks/spicy-ecs-service-stack.ts index becb5ac..7e9d6b3 100644 --- a/resources/lib/stacks/spicy-ecs-service-stack.ts +++ b/resources/lib/stacks/spicy-ecs-service-stack.ts @@ -187,10 +187,18 @@ export class SpicyEcsServiceStack extends cdk.Stack { // Import all VPC details from VPC stack exports const vpcCidrBlock = cdk.Fn.importValue(`${vpcStackName}-VPCCIDR`).toString(); + + // numberOfAzs: use context value if provided, otherwise auto-import from VPC stack export const numberOfAzsRaw = app.node.tryGetContext('numberOfAzs'); - const numberOfAzs = numberOfAzsRaw ? parseInt(numberOfAzsRaw, 10) : NaN; + let numberOfAzs: number; + if (numberOfAzsRaw) { + numberOfAzs = parseInt(numberOfAzsRaw, 10); + } else { + const imported = cdk.Fn.importValue(`${vpcStackName}-NumberOfAZs`).toString(); + numberOfAzs = parseInt(imported, 10); + } if (!numberOfAzs || Number.isNaN(numberOfAzs)) { - throw new Error('numberOfAzs is required in context (2-4) to import VPC subnets.'); + throw new Error('numberOfAzs is required (2-4). Provide via context or ensure VPC stack exports NumberOfAZs.'); } const azs = ['A', 'B', 'C', 'D'].slice(0, Math.min(Math.max(numberOfAzs, 1), 4));