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 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 16:15:40 -08:00
parent 68684df471
commit fa1e865f50
7 changed files with 80 additions and 47 deletions

3
.gitignore vendored
View File

@@ -8,3 +8,6 @@ node_modules
cdk.out
OLD_STUFF/
OLDER_STUFF/
# Use pnpm only
package-lock.json

View File

@@ -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
```

View File

@@ -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)

View File

@@ -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({
// 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

View File

@@ -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));

View File

@@ -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

View File

@@ -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));