Files
spicy-automation/docs/plans/2026-02-14-dual-stack-vpc-implementation.md
Ryan Wilson 2e02b02023 Add dual-stack IPv6 VPC with egress optimization and VPC endpoints
- Add Amazon-provided IPv6 /56 CIDR block with auto-carved /64 per subnet
- Add Egress-Only Internet Gateway for free IPv6 outbound from private subnets
- Add IPv6 routes: public subnets via IGW, private subnets via EOIGW
- Add IPv6 NACL entries for subnet tier 2
- Add DynamoDB gateway endpoint (free, alongside existing S3)
- Add 6 interface endpoints: ECR, ECR Docker, CloudWatch Logs, STS,
  Secrets Manager, SSM with shared security group
- Add enableIpv6 prop (default true) and interfaceEndpoints config
- Update VPC stack with context params for all new features
- Include design doc and implementation plan

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 17:18:18 -08:00

22 KiB

Dual-Stack VPC with Egress Optimization — Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Add IPv6 dual-stack support and VPC endpoints to the SpicyVpc construct to eliminate most NAT Gateway egress costs.

Architecture: The VPC gets an Amazon-provided IPv6 /56 CIDR. All subnets become dual-stack (/64 each). Public subnets route IPv6 via the existing IGW. Private subnets route IPv6 via a new Egress-Only IGW (free, outbound-only). Interface VPC endpoints handle AWS service traffic without NAT. NAT Gateways remain as IPv4 fallback.

Tech Stack: AWS CDK (TypeScript), L1 CfnResources (to match existing construct pattern), L2 InterfaceVpcEndpoint for interface endpoints.

Design doc: docs/plans/2026-02-14-dual-stack-vpc-egress-optimization-design.md


Task 1: Add new props and interface endpoint config type

Files:

  • Modify: resources/lib/constructs/spicy-vpc.ts:30-107 (SpicyVpcProps interface)

Step 1: Add the VpcInterfaceEndpointConfig interface and new props

Add after the SpicyVpcTags interface (line 25) and before SpicyVpcProps:

/**
 * Configuration for VPC Interface Endpoints
 */
export interface VpcInterfaceEndpointConfig {
  /** ECR API endpoint (for image metadata) @default true */
  readonly ecr?: boolean;
  /** ECR Docker endpoint (for image layer pulls) @default true */
  readonly ecrDocker?: boolean;
  /** CloudWatch Logs endpoint @default true */
  readonly cloudwatchLogs?: boolean;
  /** STS endpoint (for IAM role assumption) @default true */
  readonly sts?: boolean;
  /** Secrets Manager endpoint @default true */
  readonly secretsManager?: boolean;
  /** SSM endpoint (for Parameter Store, Session Manager) @default true */
  readonly ssm?: boolean;
}

Add to SpicyVpcProps interface before the closing brace:

  /**
   * Enable IPv6 dual-stack with Egress-Only Internet Gateway.
   * Adds Amazon-provided IPv6 CIDR, assigns /64 to each subnet,
   * and routes private subnet IPv6 egress through EOIGW (free, outbound-only).
   * @default true
   */
  readonly enableIpv6?: boolean;

  /**
   * Configuration for VPC Interface Endpoints.
   * Only applies when createVpcEndpoints is true.
   * Each endpoint costs ~$0.01/hr but eliminates NAT data processing costs for that service.
   * @default All endpoints enabled (ECR, ECR Docker, CloudWatch Logs, STS, Secrets Manager, SSM)
   */
  readonly interfaceEndpoints?: VpcInterfaceEndpointConfig;

Step 2: Verify TypeScript compiles

Run: cd /Volumes/Engineering/Kodeniks/spicy-automation/resources && pnpm exec tsc --noEmit Expected: Clean compilation (new props are optional, no consumers break)

Step 3: Commit

git add resources/lib/constructs/spicy-vpc.ts
git commit -m "feat(vpc): add IPv6 and interface endpoint props to SpicyVpcProps"

Task 2: Add IPv6 CIDR block and Egress-Only Internet Gateway

Files:

  • Modify: resources/lib/constructs/spicy-vpc.ts — constructor body, after DHCP options (after line 225)

Step 1: Add IPv6 CIDR block and EOIGW after DHCP association

Insert after the VPCDHCPOptionsAssociation block (after line 225), before the public route table section:

    // IPv6 Dual-Stack Configuration
    const enableIpv6 = props.enableIpv6 ?? true;
    let ipv6CidrBlock: ec2.CfnVPCCidrBlock | undefined;
    let egressOnlyIgw: ec2.CfnEgressOnlyInternetGateway | undefined;

    if (enableIpv6) {
      // Add Amazon-provided IPv6 /56 CIDR block to the VPC
      ipv6CidrBlock = new ec2.CfnVPCCidrBlock(this, 'IPv6CidrBlock', {
        vpcId: cfnVpc.ref,
        amazonProvidedIpv6CidrBlock: true,
      });
      ((ipv6CidrBlock.node.defaultChild as cdk.CfnResource) ?? ipv6CidrBlock).overrideLogicalId('IPv6CidrBlock');

      // Egress-Only Internet Gateway (outbound IPv6 only, drops unsolicited inbound)
      egressOnlyIgw = new ec2.CfnEgressOnlyInternetGateway(this, 'EgressOnlyInternetGateway', {
        vpcId: cfnVpc.ref,
      });
      ((egressOnlyIgw.node.defaultChild as cdk.CfnResource) ?? egressOnlyIgw).overrideLogicalId(
        'EgressOnlyInternetGateway'
      );
    }

Step 2: Verify TypeScript compiles

Run: cd /Volumes/Engineering/Kodeniks/spicy-automation/resources && pnpm exec tsc --noEmit Expected: Clean (variables declared but not yet used — that's fine, used in Task 3)

Step 3: Commit

git add resources/lib/constructs/spicy-vpc.ts
git commit -m "feat(vpc): add IPv6 CIDR block and Egress-Only Internet Gateway"

Task 3: Add IPv6 CIDRs to subnets and IPv6 routes

Files:

  • Modify: resources/lib/constructs/spicy-vpc.ts — inside the azLetters.forEach loop

This is the most complex task. We need to:

  1. Add IPv6 CIDR to each subnet (public, private1, private2)
  2. Add ::/0 route to public route table via IGW
  3. Add ::/0 routes to private route tables via EOIGW

Step 1: Add IPv6 route on public route table

After the existing PublicSubnetRoute (line 244), add:

    // Public subnet IPv6 route via IGW
    if (enableIpv6) {
      const publicIpv6Route = new ec2.CfnRoute(this, 'PublicSubnetIpv6Route', {
        routeTableId: publicRouteTable.ref,
        destinationIpv6CidrBlock: '::/0',
        gatewayId: igw.ref,
      });
      ((publicIpv6Route.node.defaultChild as cdk.CfnResource) ?? publicIpv6Route).overrideLogicalId(
        'PublicSubnetIpv6Route'
      );
    }

Step 2: Add IPv6 CIDR to public subnets

Inside the azLetters.forEach loop, after the public subnet CfnSubnet is created (after line 274 — the overrideLogicalId call), add:

      // Assign IPv6 /64 to public subnet
      if (enableIpv6 && ipv6CidrBlock) {
        const publicIpv6Cidr = cdk.Fn.select(
          index,
          cdk.Fn.cidr(
            cdk.Fn.select(0, cfnVpc.attrIpv6CidrBlocks),
            256,
            '64'
          )
        );
        publicSubnet.ipv6CidrBlock = publicIpv6Cidr;
        publicSubnet.assignIpv6AddressOnCreation = true;
        publicSubnet.addDependency(ipv6CidrBlock);
      }

Step 3: Add IPv6 CIDR to private subnet 1 and EOIGW route

Inside the if (createPrivateSubnets) block, after private subnet 1 is created and its route table association (after line 370), add:

        // Assign IPv6 /64 to private subnet 1 (offset by numberOfAzs to avoid public subnet range)
        if (enableIpv6 && ipv6CidrBlock && egressOnlyIgw) {
          const private1Ipv6Cidr = cdk.Fn.select(
            numberOfAzs + index,
            cdk.Fn.cidr(
              cdk.Fn.select(0, cfnVpc.attrIpv6CidrBlocks),
              256,
              '64'
            )
          );
          privateSubnet1.ipv6CidrBlock = private1Ipv6Cidr;
          privateSubnet1.assignIpv6AddressOnCreation = true;
          privateSubnet1.addDependency(ipv6CidrBlock);

          // IPv6 egress route via Egress-Only IGW
          const private1Ipv6Route = new ec2.CfnRoute(this, `PrivateSubnet${azUpper}1Ipv6Route`, {
            routeTableId: privateRouteTable1.ref,
            destinationIpv6CidrBlock: '::/0',
            egressOnlyInternetGatewayId: egressOnlyIgw.ref,
          });
          ((private1Ipv6Route.node.defaultChild as cdk.CfnResource) ?? private1Ipv6Route).overrideLogicalId(
            `PrivateSubnet${azUpper}1Ipv6Route`
          );
        }

Step 4: Add IPv6 CIDR to private subnet 2 and EOIGW route

Inside the if (createAdditionalPrivateSubnets && cidrs.private2Cidr) block, after private subnet 2 route table association (after line 436), add (before the NACL section):

          // Assign IPv6 /64 to private subnet 2 (offset by 2*numberOfAzs)
          if (enableIpv6 && ipv6CidrBlock && egressOnlyIgw) {
            const private2Ipv6Cidr = cdk.Fn.select(
              2 * numberOfAzs + index,
              cdk.Fn.cidr(
                cdk.Fn.select(0, cfnVpc.attrIpv6CidrBlocks),
                256,
                '64'
              )
            );
            privateSubnet2.ipv6CidrBlock = private2Ipv6Cidr;
            privateSubnet2.assignIpv6AddressOnCreation = true;
            privateSubnet2.addDependency(ipv6CidrBlock);

            // IPv6 egress route via Egress-Only IGW
            const private2Ipv6Route = new ec2.CfnRoute(this, `PrivateSubnet${azUpper}2Ipv6Route`, {
              routeTableId: privateRouteTable2.ref,
              destinationIpv6CidrBlock: '::/0',
              egressOnlyInternetGatewayId: egressOnlyIgw.ref,
            });
            ((private2Ipv6Route.node.defaultChild as cdk.CfnResource) ?? private2Ipv6Route).overrideLogicalId(
              `PrivateSubnet${azUpper}2Ipv6Route`
            );
          }

Step 5: Update NACL rules to include IPv6

In the NACL section for private subnet 2, after the existing outbound rule (after line 472), add IPv6 NACL entries:

          // Allow all inbound IPv6
          if (enableIpv6) {
            const naclInboundIpv6 = new ec2.CfnNetworkAclEntry(
              this,
              `PrivateSubnet${azUpper}2NetworkACLEntryInboundIpv6`,
              {
                networkAclId: nacl.ref,
                ruleNumber: 101,
                protocol: -1,
                ruleAction: 'allow',
                egress: false,
                ipv6CidrBlock: '::/0',
              }
            );
            ((naclInboundIpv6.node.defaultChild as cdk.CfnResource) ?? naclInboundIpv6).overrideLogicalId(
              `PrivateSubnet${azUpper}2NetworkACLEntryInboundIpv6`
            );

            // Allow all outbound IPv6
            const naclOutboundIpv6 = new ec2.CfnNetworkAclEntry(
              this,
              `PrivateSubnet${azUpper}2NetworkACLEntryOutboundIpv6`,
              {
                networkAclId: nacl.ref,
                ruleNumber: 101,
                protocol: -1,
                ruleAction: 'allow',
                egress: true,
                ipv6CidrBlock: '::/0',
              }
            );
            ((naclOutboundIpv6.node.defaultChild as cdk.CfnResource) ?? naclOutboundIpv6).overrideLogicalId(
              `PrivateSubnet${azUpper}2NetworkACLEntryOutboundIpv6`
            );
          }

Step 6: Verify TypeScript compiles and synth

Run:

cd /Volumes/Engineering/Kodeniks/spicy-automation/resources
pnpm exec tsc --noEmit
pnpm exec cdk synth -c stackType=vpc -c stackName=test-vpc -c ownerTag=T -c productTag=t -c componentTag=c 2>&1 | grep -E '(IPv6|EgressOnly|Ipv6)'

Expected: Clean compilation. Synth output should show IPv6CidrBlock, EgressOnlyInternetGateway, and Ipv6Route resources.

Step 7: Commit

git add resources/lib/constructs/spicy-vpc.ts
git commit -m "feat(vpc): add IPv6 CIDRs to all subnets and EOIGW routes for private subnets"

Task 4: Add DynamoDB Gateway Endpoint

Files:

  • Modify: resources/lib/constructs/spicy-vpc.ts — VPC endpoints section (after line 513)

Step 1: Add DynamoDB gateway endpoint

After the existing S3 endpoint block (after line 531), add:

      // DynamoDB VPC Endpoint (Gateway type - free)
      const dynamoDbEndpoint = new ec2.CfnVPCEndpoint(this, 'DynamoDBVPCEndpoint', {
        vpcId: cfnVpc.ref,
        serviceName: `com.amazonaws.${cdk.Stack.of(this).region}.dynamodb`,
        routeTableIds: privateRouteTables.map((rt) => rt.ref),
        policyDocument: {
          Version: '2012-10-17',
          Statement: [
            {
              Action: '*',
              Effect: 'Allow',
              Resource: '*',
              Principal: '*',
            },
          ],
        },
      });
      ((dynamoDbEndpoint.node.defaultChild as cdk.CfnResource) ?? dynamoDbEndpoint).overrideLogicalId(
        'DynamoDBVPCEndpoint'
      );

Step 2: Verify TypeScript compiles

Run: cd /Volumes/Engineering/Kodeniks/spicy-automation/resources && pnpm exec tsc --noEmit

Step 3: Commit

git add resources/lib/constructs/spicy-vpc.ts
git commit -m "feat(vpc): add DynamoDB gateway VPC endpoint"

Task 5: Add Interface VPC Endpoints

Files:

  • Modify: resources/lib/constructs/spicy-vpc.ts — after gateway endpoints section

Interface endpoints need:

  • A security group allowing HTTPS from the VPC CIDR
  • Private DNS enabled
  • Subnets (one per AZ in private subnet 1)

Since we have an imported this.vpc (L2) and the private subnets as ISubnet references, we can use the L2 InterfaceVpcEndpoint construct here.

Step 1: Add interface endpoints section

After the DynamoDB endpoint (end of gateway endpoints section), add:

      // Interface VPC Endpoints (cost ~$0.01/hr each, but eliminate NAT data processing)
      const endpointConfig = props.interfaceEndpoints ?? {};

      // Security group for all interface endpoints
      const endpointSecurityGroup = new ec2.CfnSecurityGroup(this, 'VpcEndpointSecurityGroup', {
        vpcId: cfnVpc.ref,
        groupDescription: 'Security group for VPC Interface Endpoints',
        securityGroupIngress: [
          {
            ipProtocol: 'tcp',
            fromPort: 443,
            toPort: 443,
            cidrIp: vpcCidr,
            description: 'Allow HTTPS from VPC IPv4',
          },
          ...(enableIpv6
            ? [
                {
                  ipProtocol: 'tcp',
                  fromPort: 443,
                  toPort: 443,
                  cidrIpv6: '::/0',
                  description: 'Allow HTTPS from VPC IPv6',
                },
              ]
            : []),
        ],
      });
      ((endpointSecurityGroup.node.defaultChild as cdk.CfnResource) ?? endpointSecurityGroup).overrideLogicalId(
        'VpcEndpointSecurityGroup'
      );

      const privateSubnetIds = Array.from(this.privateSubnets1.values()).map((s) => s.subnetId);
      const region = cdk.Stack.of(this).region;

      const interfaceEndpointServices: { id: string; service: string; enabled: boolean }[] = [
        { id: 'ECRApiEndpoint', service: `com.amazonaws.${region}.ecr.api`, enabled: endpointConfig.ecr ?? true },
        {
          id: 'ECRDockerEndpoint',
          service: `com.amazonaws.${region}.ecr.dkr`,
          enabled: endpointConfig.ecrDocker ?? true,
        },
        {
          id: 'CloudWatchLogsEndpoint',
          service: `com.amazonaws.${region}.logs`,
          enabled: endpointConfig.cloudwatchLogs ?? true,
        },
        { id: 'STSEndpoint', service: `com.amazonaws.${region}.sts`, enabled: endpointConfig.sts ?? true },
        {
          id: 'SecretsManagerEndpoint',
          service: `com.amazonaws.${region}.secretsmanager`,
          enabled: endpointConfig.secretsManager ?? true,
        },
        { id: 'SSMEndpoint', service: `com.amazonaws.${region}.ssm`, enabled: endpointConfig.ssm ?? true },
      ];

      for (const ep of interfaceEndpointServices) {
        if (!ep.enabled) continue;

        const endpoint = new ec2.CfnVPCEndpoint(this, ep.id, {
          vpcId: cfnVpc.ref,
          serviceName: ep.service,
          vpcEndpointType: 'Interface',
          privateDnsEnabled: true,
          subnetIds: privateSubnetIds,
          securityGroupIds: [endpointSecurityGroup.ref],
        });
        ((endpoint.node.defaultChild as cdk.CfnResource) ?? endpoint).overrideLogicalId(ep.id);
      }

Step 2: Verify TypeScript compiles and synth

Run:

cd /Volumes/Engineering/Kodeniks/spicy-automation/resources
pnpm exec tsc --noEmit
pnpm exec cdk synth -c stackType=vpc -c stackName=test-vpc -c ownerTag=T -c productTag=t -c componentTag=c 2>&1 | grep -c 'VPCEndpoint'

Expected: Clean compilation. Grep should show 8 VPCEndpoint resources (S3 + DynamoDB gateways + 6 interface).

Step 3: Commit

git add resources/lib/constructs/spicy-vpc.ts
git commit -m "feat(vpc): add interface VPC endpoints for ECR, CloudWatch, STS, SecretsManager, SSM"

Task 6: Add new CloudFormation outputs and expose EOIGW

Files:

  • Modify: resources/lib/constructs/spicy-vpc.ts — class properties (line ~159) and addOutputs method

Step 1: Add new public properties

After the s3Endpoint property (line 159), add:

  /** Egress-Only Internet Gateway (IPv6 outbound from private subnets) */
  public readonly egressOnlyInternetGateway?: ec2.CfnEgressOnlyInternetGateway;

Step 2: Assign EOIGW to the property

In the constructor, where egressOnlyIgw is created (Task 2), after creating the EOIGW, assign it:

      this.egressOnlyInternetGateway = egressOnlyIgw;

Step 3: Update addOutputs signature and add IPv6 outputs

Update the addOutputs method to accept the new parameters. Add enableIpv6 and cfnVpc to the method signature:

Change the method signature to:

  private addOutputs(
    cfnVpc: ec2.CfnVPC,
    azLetters: string[],
    publicRouteTable: ec2.CfnRouteTable,
    privateRouteTables: ec2.CfnRouteTable[],
    createPrivateSubnets: boolean,
    createAdditionalPrivateSubnets: boolean,
    enableIpv6: boolean
  ): void {

At the end of the addOutputs method, before the closing brace, add:

    // IPv6 outputs
    if (enableIpv6 && this.egressOnlyInternetGateway) {
      const eoigwOutput = new cdk.CfnOutput(this, 'EgressOnlyInternetGatewayId', {
        value: this.egressOnlyInternetGateway.ref,
        description: 'Egress-Only Internet Gateway ID',
        exportName: `${stack.stackName}-EgressOnlyIGW`,
      });
      eoigwOutput.overrideLogicalId('EgressOnlyInternetGatewayId');
    }

Step 4: Update the addOutputs call site

Update the addOutputs call in the constructor to pass the new parameter:

    this.addOutputs(
      cfnVpc,
      azLetters,
      publicRouteTable,
      privateRouteTables,
      createPrivateSubnets,
      createAdditionalPrivateSubnets,
      enableIpv6
    );

Step 5: Verify TypeScript compiles and synth

Run:

cd /Volumes/Engineering/Kodeniks/spicy-automation/resources
pnpm exec tsc --noEmit
pnpm exec cdk synth -c stackType=vpc -c stackName=test-vpc -c ownerTag=T -c productTag=t -c componentTag=c 2>&1 | grep 'EgressOnly'

Expected: Clean compilation. Output should show the EOIGW output.

Step 6: Commit

git add resources/lib/constructs/spicy-vpc.ts
git commit -m "feat(vpc): add EOIGW property and IPv6 CloudFormation outputs"

Task 7: Update VPC stack to pass new context params

Files:

  • Modify: resources/lib/stacks/spicy-vpc-stack.tsfromContext method

Step 1: Add context parsing for new props

In the fromContext method, after the existing context parsing (around line 88), add:

    const enableIpv6 = app.node.tryGetContext('enableIpv6') as string | undefined;

    // Interface endpoint overrides
    const disableEcrEndpoint = app.node.tryGetContext('disableEcrEndpoint') as string | undefined;
    const disableEcrDockerEndpoint = app.node.tryGetContext('disableEcrDockerEndpoint') as string | undefined;
    const disableCloudwatchLogsEndpoint = app.node.tryGetContext('disableCloudwatchLogsEndpoint') as string | undefined;
    const disableStsEndpoint = app.node.tryGetContext('disableStsEndpoint') as string | undefined;
    const disableSecretsManagerEndpoint = app.node.tryGetContext('disableSecretsManagerEndpoint') as string | undefined;
    const disableSsmEndpoint = app.node.tryGetContext('disableSsmEndpoint') as string | undefined;

In the vpcConfigBuilder type, add:

      enableIpv6?: boolean;
      interfaceEndpoints?: SpicyVpcProps['interfaceEndpoints'];

After the existing config assignments (around line 124), add:

    if (enableIpv6 !== undefined) {
      vpcConfigBuilder.enableIpv6 = enableIpv6 !== 'false';
    }

    // Build interface endpoint config if any overrides provided
    if (
      disableEcrEndpoint ||
      disableEcrDockerEndpoint ||
      disableCloudwatchLogsEndpoint ||
      disableStsEndpoint ||
      disableSecretsManagerEndpoint ||
      disableSsmEndpoint
    ) {
      vpcConfigBuilder.interfaceEndpoints = {
        ecr: disableEcrEndpoint !== 'true',
        ecrDocker: disableEcrDockerEndpoint !== 'true',
        cloudwatchLogs: disableCloudwatchLogsEndpoint !== 'true',
        sts: disableStsEndpoint !== 'true',
        secretsManager: disableSecretsManagerEndpoint !== 'true',
        ssm: disableSsmEndpoint !== 'true',
      };
    }

Step 2: Add the SpicyVpcProps import if not already imported

The file already imports SpicyVpc and SpicyVpcProps on line 3 — verify this covers the new VpcInterfaceEndpointConfig type (it doesn't need to be imported separately since it's referenced via SpicyVpcProps['interfaceEndpoints']).

Step 3: Verify TypeScript compiles

Run: cd /Volumes/Engineering/Kodeniks/spicy-automation/resources && pnpm exec tsc --noEmit

Step 4: Commit

git add resources/lib/stacks/spicy-vpc-stack.ts
git commit -m "feat(vpc): add IPv6 and endpoint context params to VPC stack"

Task 8: Full synth verification and final commit

Step 1: Run full type check

Run: cd /Volumes/Engineering/Kodeniks/spicy-automation/resources && pnpm exec tsc --noEmit Expected: Clean

Step 2: Synth VPC stack and verify all resources

Run:

cd /Volumes/Engineering/Kodeniks/spicy-automation/resources
pnpm exec cdk synth -c stackType=vpc -c stackName=test-vpc -c ownerTag=T -c productTag=t -c componentTag=c 2>&1 | grep -E 'Type.*AWS::'

Expected: Should include:

  • AWS::EC2::VPCCidrBlock (IPv6CidrBlock)
  • AWS::EC2::EgressOnlyInternetGateway
  • AWS::EC2::Route entries for IPv6 (::/0)
  • AWS::EC2::VPCEndpoint (8 total: S3, DynamoDB, ECR, ECR Docker, Logs, STS, SecretsManager, SSM)
  • AWS::EC2::SecurityGroup for endpoints

Step 3: Synth VPC with IPv6 disabled (regression check)

Run:

pnpm exec cdk synth -c stackType=vpc -c stackName=test-vpc -c ownerTag=T -c productTag=t -c componentTag=c -c enableIpv6=false 2>&1 | grep -c 'IPv6\|EgressOnly'

Expected: 0 matches (no IPv6 resources when disabled)

Step 4: Synth ECS cluster stack to verify no regressions

Run:

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 -c numberOfAzs=2 2>&1 | tail -5

Expected: Clean synth, no errors

Step 5: Verify all changes are clean

Run: git diff --stat Review that only spicy-vpc.ts and spicy-vpc-stack.ts were modified.