Skip to main content

CDK Infrastructure Development

This guide covers development patterns for the VAMS AWS CDK infrastructure, including nested stack architecture, Lambda builder conventions, API route wiring, configuration management, and security patterns.

Technology Stack

ComponentDetails
FrameworkAWS CDK v2 (TypeScript), aws-cdk-lib
VAMS VersionDefined in config.ts (VAMS_VERSION)
Node Lambda RuntimeNODEJS_20_X
Python Lambda RuntimePYTHON_3_12
Lambda Memory5308 MB (all functions)
Lambda Timeout15 minutes (all functions)

Nested Stack Architecture

VAMS deploys as a root stack (CoreVAMSStack) with 10+ nested stacks. Understanding the dependency chain is essential for adding new resources.

Dependency Chain

Key Nested Stacks

StackFilePurpose
VPCBuildernestedStacks/vpc/vpcBuilder-nestedStack.tsVPC, subnets, VPC endpoints
StorageResourcesBuildernestedStacks/storage/storageBuilder-nestedStack.tsAmazon DynamoDB tables, Amazon S3, Amazon SNS, Amazon SQS, AWS KMS
AuthBuildernestedStacks/auth/authBuilder-nestedStack.tsAmazon Cognito, SAML, external OAuth
ApiGatewayV2AmplifynestedStacks/apiLambda/apigatewayv2-amplify-nestedStack.tsAmazon API Gateway V2, Lambda authorizer
ApiBuildernestedStacks/apiLambda/apiBuilder-nestedStack.tsAll API routes and Lambda wiring
StaticWebBuildernestedStacks/staticWebApp/staticWebBuilder-nestedStack.tsAmazon S3 + Amazon CloudFront or ALB hosting
PipelineBuildernestedStacks/pipelines/pipelineBuilder-nestedStack.tsProcessing pipeline orchestrator

Cross-Stack Shared Interfaces

The storageResources interface (defined in storageBuilder-nestedStack.ts) is the primary shared interface. It exposes 20+ Amazon DynamoDB tables, Amazon S3 buckets, Amazon SNS topics, Amazon SQS queues, AWS KMS keys, and Amazon CloudWatch audit log groups to all downstream stacks.

Lambda Builder Pattern

All Lambda builder files in lib/lambdaBuilder/ follow a strict, consistent pattern. There are approximately 17 builder files containing 40+ function builders.

Standard Function Signature

export function buildSomeFunction(
scope: Construct,
lambdaCommonBaseLayer: LayerVersion,
storageResources: storageResources,
config: Config.Config,
vpc: ec2.IVpc,
subnets: ec2.ISubnet[]
): lambda.Function {

Standard Lambda Configuration

const name = "functionName";
const fun = new lambda.Function(scope, name, {
code: lambda.Code.fromAsset(path.join(__dirname, "../../../backend/backend")),
handler: `handlers.{category}.${name}.lambda_handler`,
runtime: LAMBDA_PYTHON_RUNTIME,
layers: [lambdaCommonBaseLayer],
timeout: Duration.minutes(15),
memorySize: Config.LAMBDA_MEMORY_SIZE,
vpc:
config.app.useGlobalVpc.enabled && config.app.useGlobalVpc.useForAllLambdas
? vpc
: undefined,
vpcSubnets:
config.app.useGlobalVpc.enabled && config.app.useGlobalVpc.useForAllLambdas
? { subnets: subnets }
: undefined,
environment: {
MY_TABLE_NAME: storageResources.dynamo.myTable.tableName,
},
});

Four Required Security Calls

Every Lambda builder function must include all four security calls after creating the function:

// 1. KMS permissions (if encryption enabled)
kmsKeyLambdaPermissionAddToResourcePolicy(fun, storageResources.encryption.kmsKey);

// 2. Auth table access + audit log setup
setupSecurityAndLoggingEnvironmentAndPermissions(fun, storageResources);

// 3. Global environment variables (Cognito auth flag)
globalLambdaEnvironmentsAndPermissions(fun, config);

// 4. CDK Nag suppression for S3 grant patterns
suppressCdkNagErrorsByGrantReadWrite(scope);
All Four Calls Required

Missing setupSecurityAndLoggingEnvironmentAndPermissions breaks authorization checking in Lambda handlers. Missing kmsKeyLambdaPermissionAddToResourcePolicy causes access denied errors when AWS KMS encryption is enabled. Always include all four calls.

What the Security Helpers Do

HelperActions
kmsKeyLambdaPermissionAddToResourcePolicyGrants AWS KMS Decrypt, Encrypt, GenerateDataKey, ReEncrypt, ListKeys, CreateGrant, ListAliases
setupSecurityAndLoggingEnvironmentAndPermissionsAdds env vars for auth tables and 9 audit log groups. Grants DynamoDB read on auth, constraints, userRoles, roles tables. Grants Amazon CloudWatch PutLogEvents on audit log groups.
globalLambdaEnvironmentsAndPermissionsSets COGNITO_AUTH_ENABLED based on Amazon Cognito and VPC configuration
suppressCdkNagErrorsByGrantReadWriteSuppresses AwsSolutions-IAM5 for Amazon S3 and resource wildcards

API Route Wiring

Routes are wired in apiBuilder-nestedStack.ts using the attachFunctionToApi helper.

Attaching a Function to Amazon API Gateway

// Build the function
const myFunction = buildMyNewFunction(
this,
lambdaCommonBaseLayer,
storageResources,
config,
vpc,
subnets
);

// Wire to API Gateway -- one call per route
attachFunctionToApi(this, myFunction, {
routePath: "/my-resource/{resourceId}",
method: apigateway.HttpMethod.GET,
api: api,
});
attachFunctionToApi(this, myFunction, {
routePath: "/my-resource",
method: apigateway.HttpMethod.POST,
api: api,
});

The attachFunctionToApi helper creates an ApiGatewayV2LambdaConstruct which:

  1. Grants invoke permission to the Amazon API Gateway service principal
  2. Creates an HttpLambdaIntegration
  3. Adds the route to the API

Route Conventions

Routes use RESTful conventions with path parameters:

GET    /database/{databaseId}/assets              # List assets
GET /database/{databaseId}/assets/{assetId} # Get single asset
POST /database/{databaseId}/assets # Create asset
PUT /database/{databaseId}/assets/{assetId} # Update asset
DELETE /database/{databaseId}/assets/{assetId} # Delete asset

Unauthenticated paths (no authorizer): /api/amplify-config, /api/version.

Configuration System

Three-Tier Fallback Chain

Configuration values resolve in this order:

  1. CDK context (-c key=value)
  2. config/config.json file
  3. Environment variables
  4. Hardcoded defaults

The entry point bin/infra.ts calls Config.getConfig(app) then Service.SetConfig(config).

Key Constants

ConstantValue
VAMS_VERSIONCurrent release version (see config.ts)
LAMBDA_PYTHON_RUNTIMERuntime.PYTHON_3_12
LAMBDA_NODE_RUNTIMERuntime.NODEJS_20_X
LAMBDA_MEMORY_SIZE5308

Adding a New Configuration Property

Adding a new config property requires three steps:

// 1. Add to ConfigPublic interface in config/config.ts
app: {
myNewFeature: {
enabled: boolean;
someOption: string;
}
}

// 2. Add backward-compatibility check in getConfig()
if (config.app.myNewFeature == undefined) {
config.app.myNewFeature = {
enabled: false,
someOption: "",
};
}

// 3. Add validation if constraints exist
if (config.app.myNewFeature.enabled && !config.app.myNewFeature.someOption) {
throw new Error("Configuration Error: myNewFeature requires someOption when enabled");
}

Also update both template files: config.template.commercial.json and config.template.govcloud.json.

Service Helper

The service helper provides partition-aware ARN and endpoint generation. It supports four AWS partitions: aws (commercial), aws-us-gov (GovCloud), aws-cn (China), and aws-iso (isolated).

Initialization

// In bin/infra.ts -- MUST be called before any Service() usage
const config = Config.getConfig(app);
Service.SetConfig(config);

Usage

import { Service, Partition } from "../helper/service-helper";

// Partition-aware endpoint
Service("S3").Endpoint;

// Partition-aware ARN
Service("DYNAMODB").ARN("table/myTable");

// IAM principal
Service("LAMBDA").Principal;

// Current partition string
Partition(); // "aws", "aws-us-gov", etc.
Never Hardcode Partitions

Never use arn:aws: in resource ARNs. This breaks deployments in AWS GovCloud (arn:aws-us-gov) and other partitions. Always use the Service() helper or Partition() for partition-aware values.

AWS GovCloud Considerations

When config.app.govCloud.enabled is true, several constraints apply.

Required Configuration

SettingRequired ValueReason
useGlobalVpc.enabledtrueVPC required for GovCloud
useCloudFront.enabledfalseAmazon CloudFront not available
useLocationService.enabledfalseAmazon Location Service not available

GovCloud-Specific Behavior

  • FIPS endpoints used via config.app.useFips
  • AwsSolutions-COG3 CDK Nag rule suppressed (AdvancedSecurityMode not available)
  • ALB deployment replaces Amazon CloudFront for static web hosting
  • VPC endpoints are conditional on feature flags

CDK Nag Compliance

CDK Nag is always enabled with AWS Solutions checks. Every new resource must pass these checks or have justified suppressions.

Adding Suppressions

Every CDK Nag suppression requires a detailed justification:

import { NagSuppressions } from "cdk-nag";

NagSuppressions.addResourceSuppressions(
resource,
[
{
id: "AwsSolutions-IAM5",
reason:
"Wildcard permissions required for dynamic S3 object " +
"access within VAMS asset buckets. Scope is limited " +
"to deployment-specific buckets.",
},
],
true
);
Suppressions Require Justification

Never add a suppression with a generic reason like "Suppressed" or "Not applicable". Explain why the suppression is acceptable in the VAMS security context.

Common Suppressions

RuleTypical Reason
AwsSolutions-IAM5Amazon S3 grantRead/grantReadWrite generates IAM wildcard actions
AwsSolutions-IAM4AWS managed policies used for Lambda execution roles
AwsSolutions-COG3Cognito AdvancedSecurityMode not available in GovCloud

Feature Flags

Feature flags are defined in common/vamsAppFeatures.ts:

enum VAMS_APP_FEATURES {
GOVCLOUD,
ALLOWUNSAFEEVAL,
LOCATIONSERVICES,
ALBDEPLOY,
CLOUDFRONTDEPLOY,
NOOPENSEARCH,
AUTHPROVIDER_COGNITO,
AUTHPROVIDER_COGNITO_SAML,
AUTHPROVIDER_EXTERNALOAUTHIDP,
}

Features are tracked in the enabledFeatures array on CoreVAMSStack and persisted to Amazon DynamoDB by CustomFeatureEnabledConfigNestedStack. The frontend reads these from /api/secure-config at runtime.

Adding a New Feature Flag

  1. Add the constant to VAMS_APP_FEATURES enum in common/vamsAppFeatures.ts
  2. Add the config option in ConfigPublic interface in config/config.ts
  3. Push to enabledFeatures array in core-stack.ts when the feature is enabled
  4. Gate frontend UI with feature check on config.featuresEnabled

Adding New Resources

Adding a New Lambda Function

  1. Create the builder function in lib/lambdaBuilder/{domain}Functions.ts
  2. Follow the standard pattern with all four security calls
  3. Grant Amazon DynamoDB table permissions (grantReadData or grantReadWriteData)
  4. Wire to Amazon API Gateway in apiBuilder-nestedStack.ts using attachFunctionToApi()

Adding a New Amazon DynamoDB Table

  1. Add the table to storageResources interface in storageBuilder-nestedStack.ts
  2. Create the table in storageResourcesBuilder() with AWS KMS encryption if enabled
  3. Apply RemovalPolicy.DESTROY
  4. Update Lambda builders to pass the table name as an environment variable
  5. Grant appropriate permissions in Lambda builders

Adding a New Nested Stack

  1. Create lib/nestedStacks/{name}/{name}Builder-nestedStack.ts extending NestedStack
  2. Accept config, storageResources, and other shared resources as constructor parameters
  3. Instantiate in core-stack.ts with addDependency(storageResourcesNestedStack)
  4. Export resources needed by other stacks via public properties
Explicit Dependencies

Always use nestedStack.addDependency(otherStack) when one nested stack depends on another. Implicit ordering through resource references alone is not sufficient.

Anti-Patterns

Anti-PatternCorrect Approach
Hardcode arn:aws: in ARNsUse Service() or Partition()
Skip any of the 4 security callsInclude all four in every Lambda builder
Forget VPC conditional on LambdaAlways check useGlobalVpc.enabled && useForAllLambdas
Hardcode Lambda runtime or memoryUse LAMBDA_PYTHON_RUNTIME and Config.LAMBDA_MEMORY_SIZE
Add config without backward compatAdd undefined check in getConfig()
Forget to call Service.SetConfig()Must be called in bin/infra.ts before any Service() usage
Create resources without CDK Nag reviewAdd justified suppressions as needed
Ignore GovCloud constraintsCheck feature flags before using CloudFront, Location Service
Use grantReadWrite without Nag suppressionAlways pair with suppressCdkNagErrorsByGrantReadWrite(scope)

Key Files Reference

PurposeFile
CDK entry pointbin/infra.ts
Config and constantsconfig/config.ts
Root stacklib/core-stack.ts
Storage resourceslib/nestedStacks/storage/storageBuilder-nestedStack.ts
API routeslib/nestedStacks/apiLambda/apiBuilder-nestedStack.ts
Security helperslib/helper/security.ts
Service helperlib/helper/service-helper.ts
Feature flagscommon/vamsAppFeatures.ts

Next Steps