Files
spicy-automation/resources/lib/stacks/spicy-vpc-stack.ts
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

198 lines
7.2 KiB
TypeScript

import * as cdk from 'aws-cdk-lib/core';
import { Construct } from 'constructs';
import { SpicyVpc, SpicyVpcProps, SubnetCidrConfig } from '../constructs/spicy-vpc';
/**
* Props for SpicyVpcStack
* Extends SpicyVpcProps but makes tags optional since we can derive from context
*/
export interface SpicyVpcStackProps extends cdk.StackProps {
/**
* VPC configuration - all SpicyVpcProps except tags (derived from stack props)
*/
readonly vpcConfig?: Omit<SpicyVpcProps, 'tags'>;
/**
* Owner tag
*/
readonly ownerTag: string;
/**
* Product tag
*/
readonly productTag: string;
/**
* Component tag
*/
readonly componentTag: string;
/**
* Build identifier (e.g., git SHA)
*/
readonly buildTag?: string;
}
/**
* SpicyVpcStack - A CloudFormation stack that creates a Spicy VPC
*
* This stack can be deployed via CDK CLI or from Jenkins pipelines.
* Configuration can be provided via:
* 1. Direct props
* 2. CDK context (cdk deploy -c key=value)
* 3. Environment variables
*/
export class SpicyVpcStack extends cdk.Stack {
/** The VPC construct */
public readonly spicyVpc: SpicyVpc;
constructor(scope: Construct, id: string, props: SpicyVpcStackProps) {
super(scope, id, props);
// Create the VPC
this.spicyVpc = new SpicyVpc(this, 'SpicyVpc', {
...props.vpcConfig,
tags: {
owner: props.ownerTag,
product: props.productTag,
component: props.componentTag,
build: props.buildTag,
},
});
}
/**
* Create a SpicyVpcStack from CDK context values
* This is useful when deploying from Jenkins where context is passed via -c flags
*/
static fromContext(scope: Construct, id: string, stackProps?: cdk.StackProps): SpicyVpcStack {
const app = scope.node.root as cdk.App;
// Required context values
const ownerTag = app.node.tryGetContext('ownerTag') ?? 'SpicyTeam';
const productTag = app.node.tryGetContext('productTag') ?? 'spicy';
const componentTag = app.node.tryGetContext('componentTag') ?? 'spicy-VPC';
const buildTag = app.node.tryGetContext('build');
// Optional VPC configuration from context
const vpcCidr = app.node.tryGetContext('vpcCidr') as string | undefined;
const vpcTenancy = app.node.tryGetContext('vpcTenancy') as 'default' | 'dedicated' | undefined;
const numberOfAzs = app.node.tryGetContext('numberOfAzs') as string | undefined;
const availabilityZones = app.node.tryGetContext('availabilityZones') as string | undefined;
const createPrivateSubnets = app.node.tryGetContext('createPrivateSubnets') as string | undefined;
const createAdditionalPrivateSubnets = app.node.tryGetContext('createAdditionalPrivateSubnets') as
| string
| undefined;
const publicSubnetTag = app.node.tryGetContext('publicSubnetTag') as string | undefined;
const privateSubnetATag = app.node.tryGetContext('privateSubnetATag') as string | undefined;
const privateSubnetBTag = app.node.tryGetContext('privateSubnetBTag') as string | undefined;
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;
// Parse subnet CIDRs from context
const subnetCidrs = SpicyVpcStack.parseSubnetCidrsFromContext(app);
// Build VPC config using a mutable object then cast
const vpcConfigBuilder: {
vpcCidr?: string;
vpcTenancy?: 'default' | 'dedicated';
numberOfAzs?: 2 | 3 | 4;
availabilityZones?: string[];
createPrivateSubnets?: boolean;
createAdditionalPrivateSubnets?: boolean;
publicSubnetTag?: string;
privateSubnetATag?: string;
privateSubnetBTag?: string;
subnetCidrs?: SpicyVpcProps['subnetCidrs'];
enableIpv6?: boolean;
interfaceEndpoints?: SpicyVpcProps['interfaceEndpoints'];
} = {};
if (vpcCidr) vpcConfigBuilder.vpcCidr = vpcCidr;
if (vpcTenancy) vpcConfigBuilder.vpcTenancy = vpcTenancy;
if (numberOfAzs) vpcConfigBuilder.numberOfAzs = parseInt(numberOfAzs, 10) as 2 | 3 | 4;
if (availabilityZones) {
vpcConfigBuilder.availabilityZones = availabilityZones.split(',');
}
if (createPrivateSubnets !== undefined) {
vpcConfigBuilder.createPrivateSubnets = createPrivateSubnets === 'true';
}
if (createAdditionalPrivateSubnets !== undefined) {
vpcConfigBuilder.createAdditionalPrivateSubnets = createAdditionalPrivateSubnets === 'true';
}
if (publicSubnetTag) vpcConfigBuilder.publicSubnetTag = publicSubnetTag;
if (privateSubnetATag) vpcConfigBuilder.privateSubnetATag = privateSubnetATag;
if (privateSubnetBTag) vpcConfigBuilder.privateSubnetBTag = privateSubnetBTag;
if (subnetCidrs && Object.keys(subnetCidrs).length > 0) {
vpcConfigBuilder.subnetCidrs = subnetCidrs;
}
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',
};
}
return new SpicyVpcStack(scope, id, {
...stackProps,
ownerTag,
productTag,
componentTag,
buildTag,
vpcConfig: vpcConfigBuilder,
});
}
/**
* Parse subnet CIDR configurations from CDK context
*/
private static parseSubnetCidrsFromContext(app: cdk.App): SpicyVpcProps['subnetCidrs'] {
const cidrs: SpicyVpcProps['subnetCidrs'] = {};
const azLetters = ['a', 'b', 'c', 'd'] as const;
for (const az of azLetters) {
const azUpper = az.toUpperCase();
const publicCidr = app.node.tryGetContext(`publicSubnet${azUpper}Cidr`);
const private1Cidr = app.node.tryGetContext(`privateSubnet${azUpper}1Cidr`);
const private2Cidr = app.node.tryGetContext(`privateSubnet${azUpper}2Cidr`);
if (publicCidr || private1Cidr || private2Cidr) {
const config: SubnetCidrConfig = {
publicCidr: publicCidr ?? '',
private1Cidr: private1Cidr ?? '',
};
if (private2Cidr) {
config.private2Cidr = private2Cidr;
}
cidrs[az] = config;
}
}
return cidrs;
}
}