A comprehensive AWS CDK-based infrastructure project for managing park and recreation facility reservations. This system provides secure, scalable APIs for both administrative management and public booking functionality.
- Clone and install dependencies:
git clone <repository-url>
cd reserve-rec-api
yarn install- Configure AWS credentials:
aws configure
# Bootstrap CDK (first time only)
cdk bootstrap- Deploy to development environment:
yarn deploy:devFor local testing and development:
# Start local admin API
yarn run:admin:full
# Start local public API
yarn run:public:full- ποΈ Architecture Overview - System design and stack relationships
- π Stack Descriptions - Detailed breakdown of each infrastructure component
- π οΈ Development Setup - Environment configuration and prerequisites
- π Deployment - Local and remote deployment instructions
- π Security Considerations - Authentication, authorization, and data protection
- π Troubleshooting - Common issues and debugging tips
This project uses AWS Cloud Development Kit (CDK) to define infrastructure as code, consisting of a number of interconnected stacks that provide a complete reservation management system:
βββββββββββββββββββ βββββββββββββββββββββ βββββββββββββββββββ
β Core Stack β β Identity Stacks β β OpenSearch Stackβ
β β β β β β
β β’ KMS Keys β β β’ Admin Cognito β β β’ Search Serviceβ
β β’ Lambda Layers ββββββ β’ Public Cognito ββββββ β’ Fine-grained β
β β’ IAM Roles β β β’ User Pools β β Access Controlβ
βββββββββββββββββββ βββββββββββββββββββββ βββββββββββββββββββ
β β β
β β β
βΌ βΌ βΌ
βββββββββββββββββββ βββββββββββββββββββββ βββββββββββββββββββ
β Data Stacks β β API Stacks β β Future Stacks β
β β β β β β
β β’ Reference ββββββ β’ Admin API ββββββ β
β Data (DDB) β β β’ Public API β β β
β β’ Transactional β β β’ REST Endpoints β β β
β Data (DDB) β β β’ Lambda Functionsβ β β
βββββββββββββββββββ βββββββββββββββββββββ βββββββββββββββββββ
Foundation services and shared resources
- KMS Keys: Encryption for data at rest and in transit
- Lambda Layers: Shared code and dependencies (Base Layer, AWS Utils Layer)
- API Gateway Logging: Centralized API request/response logging
- IAM Roles: Cross-stack service permissions
Authentication and authorization services
- Admin User Pool: Cognito user pool for administrative users
- Admin Identity Pool: Identity pool for administrative users (Azure Login, BCSC Login)
- Admin Groups: Role-based access control (park managers, system admins)
- Public User Pool: Cognito user pool for public users
- Self-registration: Email verification and password policies
- Guest Access: Limited functionality for non-registered users
Search and analytics engine
- OpenSearch Domain: Managed search service with fine-grained access control
- Security: IAM and FGAC integration with Cognito identity providers
- Initialization Lambda: Automated domain setup and index templates
Persistent data storage and management
- DynamoDB Tables: Park information, geozones, facilities, activities, products, policies, and other quasi-static configurations
- Reference Data Streaming: To synchronize OpenSearch
reference-dataindexes. - Audit Table: For recording changes to static data
- PubSub Table: For webhook subscriptions
- DynamoDB Tables: Reservations, bookings, payments, and user activity
- Transactional Data Streaming: To synchronize OpenSearch
transactional-dataindexes.
REST API endpoints and business logic
- Data Management Endpoints: Park administration, user management, reporting
- Lambda Functions: Business logic for administrative operations
- API Gateway: RESTful endpoints with request validation
- Authorization: Integration with admin Cognito user pool
- Booking Endpoints: Facility search, availability, reservation creation
- Lambda Functions: Public-facing business logic
- Search Endpoints: Public facing search logic
- Node.js: Version 20 or higher
- AWS CLI: Version 2.x configured with appropriate credentials
- AWS CDK: Version 2.x
The project supports multiple deployment environments configured via cdk.json:
{
"context": {
"dev": {
"DEPLOYMENT_NAME": "dev",
"AWS_REGION": "ca-central-1",
"IS_OFFLINE": "false",
"FAIL_FAST": "false"
},
"test": {
"DEPLOYMENT_NAME": "test",
"AWS_REGION": "ca-central-1",
"IS_OFFLINE": "false",
"FAIL_FAST": "false"
},
"prod": {
"DEPLOYMENT_NAME": "prod",
"AWS_REGION": "ca-central-1",
"IS_OFFLINE": "false",
"FAIL_FAST": "false"
},
"local": {
"DEPLOYMENT_NAME": "local",
"AWS_REGION": "ca-central-1",
"IS_OFFLINE": "true",
"FAIL_FAST": "false"
}
}
}- DEPLOYMENT_NAME: the name of the deployment (often the environment name)
- AWS_REGION: AWS region to deploy the app into (almost always
ca-central-1) - IS_OFFLINE: If
"true", the synthesizing/deployment operations will not attempt to connect to remote AWS servers for context/configuration variables (if synthesizing locally for example). - FAIL_FAST: Abort synthesis of downstream stacks if an error occurs (useful for prototyping)
The StackPrimer class enforces a consistent naming convention across all AWS resources to ensure uniqueness, traceability, and proper organization. This convention follows the pattern:
{AppName}-{DeploymentName}-{StackName}-{ResourceName}
- AppName: The application identifier (e.g.,
ReserveRecApi) - DeploymentName: The environment or deployment instance (e.g.,
Dev,Test,Prod) - StackName: The name of the relevant stack (e.g.,
CoreStack,AdminApiStack,ReferenceDataStack) - ResourceName: The specific resource identifier within the stack
// Lambda function in the Admin API stack
ReserveRecApi-Dev-AdminApiStack-UserManagementFunction
// DynamoDB table in the Reference Data stack
ReserveRecApi-Dev-ReferenceDataStack-ReferenceDataTable
// KMS key in the Core stack
ReserveRecApi-Test-CoreStack-DatabaseEncryptionKey
// Cognito User Pool in the Admin Identity stack
ReserveRecApi-Test-AdminIdentityStack-AdminUserPoolThe system automatically manages stack dependencies. Deployment order is important, as the resources produced in some stacks are consumed by others. The current deployment order is:
- CoreStack
- AdminIndentityStack
- PublicIdentityStack
- OpenSearchStack
- ReferenceDataStack
- TransactionalDataStack
- AdminApiStack
- PublicApiStack
-
Clone the repository:
git clone <repository-url> cd reserve-rec-api
-
Install dependencies:
yarn
-
Configure AWS credentials:
aws configure
-
Bootstrap CDK (first time only):
cdk bootstrap
- Context Resolution: Environment settings loaded from
cdk.json - SSM Parameter Store: Runtime configuration stored in AWS Systems Manager
- Secrets Manager: Sensitive data stored securely in AWS Secrets Manager
- Local Development: Configuration loaded from
/src/scripts/tools/local-testing/sam-config.jsonfor offline testing
This application is optimized for deployment of multiple stacks into a remote AWS environment. Local deployment is possible, but not fully supported yet.
Local deployment uses AWS SAM to generate a local API for testing at http://localhost:3000. This necessitates a SAM template with which to generate the API.
CDK synth/deploy operations do not generate a SAM template, so a script has been written to extract the appropriate resources from the generated CDK templates and create a SAM template from them.
In an effort to reduce the amount of Docker containers used in local development (an issue that tends impedes rapid prototyping by slowing down repeated deployments), the construction of Lambda Layer dists that would otherwise occur as part of a local SAM API deployment has been extracted from the process. Lambda Layer construction can now occur separately from local SAM API deployment and only needs to be rerun if the contents of the layers change.
Additionally, this application now has multiple APIs, so each must be generated and deployed separately from one another. As of now there are two APIs:
- admin
- public
Variables that can be configured for local enviroment development are stored in /src/scripts/tooling/local-testing/sam-config.json.
For local development and testing:
# Synthesize CloudFormation templates locally
yarn synth:local
# Build Lambda layers for local testing
yarn build:layers
# Run script to generate SAM template from generated CDK templates
yarn build:<api> (admin or public)
# Start local API for admin functions
yarn run:<api> (admin or public)
# Do all the above in one step
yarn run:<api>:full (admin or public)
When deploying to a remote environment, it is important to ensure that the configuration variables for each stack are readily available in the remote AWS Parameter Store and Secrets Manager.
As per the naming convention of resources, each stack should have a config variable in Parameter Store that follows the pattern:
/{AppName}/{DeploymentName}/{StackName}/config
Example of the config parameter for CoreStack in the dev environment:
/reserveRecApi/dev/coreStack/config
Refer to the defaults variable in each stack instantiation file for the default structure of the stack's config parameter.
Deploy to specific environments:
# Development environment
yarn synth:dev (optional)
yarn deploy:dev
# Test environment
yarn synth:test (optional)
yarn deploy:test
# Prior to deployment, the log level can be set to inspect the deployment process
export LOG_LEVEL=debugThe main orchestrator that manages the entire deployment lifecycle:
class CDKProject {
// Application configuration
getAppName() // Returns "ReserveRecApi"
getDeploymentName() // Returns environment (dev/test/prod)
getRegion() // Returns AWS region
isOffline() // Checks if deploying offline
}Handles stack initialization and configuration resolution:
class StackPrimer {
// Stack setup
prime() // Initializes stack configuration
nameConstructs() // Generates CDK construct identifiers
getDeploymentConfig() // Loads environment-specific settings
getSecrets() // Resolves AWS Secrets Manager values
}Common functionality for all stacks:
class BaseStack extends Stack {
// Resource management
createScopedId() // Generates unique resource identifiers
getConstructId() // Gets the identifier of a construct
}Each stack has the the following structure:
- defaults
- stack definition (extends BaseStack)
- stack creation function
Each stack declares default values for any variables or resources that it will create. The defaults variable at the head of each stack file has the following structure:
const defaults = {
constructs: {
constructName: {
name: 'ConstructName'
}
}
config: {
configVariable: "configValue"
}
secrets: {
secretName: {
name: 'SecretName'
}
}
}constructscontains a list of every construct/resource that the stack declares. To follow the same naming convention across deployments, construct names are standardized. To reference the standardized construct name, use `this.getConstructId('constructName'). For example:
const defaults = {
constructs: {
tableName: {
name: 'DynamoDBTableName'
}
}
}
class TableStack extends BaseStack {
// ...
const table = new dynamodb.Table(
this,
this.getConstructId('tableName'),
{ props }
)
}
// Resulting table construct name:
// reserveRecApi/env/tableStack/dynamoDBTableNameconfigcontains a copy of the configuration file found in the SSM config parameter for that stack. Values required by the stack but not provided in SSM will assume thedefaults.configvalue via JS object merge. To reference config values in the stack, use `this.getConfigValue('configVariable'). For example:
const defaults = {
config: {
defaultTimezone: 'America/Vancouver',
overrides: {
importedDynamoDBTableName: 'DynamoDBTable'
}
}
}
class TableStack extends BaseStack {
// ...
const tz = this.getConfigValue('defaultTimezome');
const importedTableName = this.getConfigValue('overrides')?.importedDynamoDBTableName;
if (importedTableName) {
// use imported table reference
this.table = dynamodb.Table.fromTableName(this, `${this.getConstructId('tableName')}-Imported`, importedTableName);
} else {
// initialize new DynamoDBTable
}
// Resulting table construct name:
// reserveRecApi/env/tableStack/dynamoDBTableName-Imported
}secretscontains a list of secret names that the stack will look for in the remote AWS Secrets Manager. Secret names follow the same naming convention as components. To reference secret values in a stack, usethis.getSecretValue('secretName'). For example:
const defaults: {
secrets: {
osPassword: {
name: 'OpenSearchMasterUserPassword'
}
}
}
class OpenSearchStack extends BaseStack {
// ...
const osMasterUserPW = this.getSecretValue('osPassword');
// Script will check Secrets Manager for
// reserveRecApi/env/openSearchStack/openSearchMasterUserPassword
}In offline mode, the script will not look for secrets.
To inherit functionality from the CDKProject class, all stacks should extend the BaseStack class.
class NewStack extends BaseStack {
constructor(scope, primer) {
super(scope, primer, defaults);
// ...
}
}scope: Application scope (CDKProject.this)primer: Stack primer reference for the stack.
Since JavaScript class constructors are synchronous, the BaseStack constructor cannot asynchronously look for remote config and secrets values prior to creating its constructs. Therefore, prior to instantiating a class, a StackPrimer is used for asynchronous configuration.
The StackPrimer class ingests defaults and fetches the remote config and secrets values, and generates the standardized construct names following the naming convention. Its prime() function should be run before instantiating the stack.
const primer = new StackPrimer(scope, stackKey, defaults);
// stackKey: any name for the stack, ie 'tableStack'The stack creation function is an asynchronous function that bundles the stack and the stack primer and returns the newly created stack. It is called by CDKProject to initialize the stack.
async function createTableStack(scope, stackKey) {
try {
const primer = new StackPrimer(scope, stackKey, defaults);
await primer.prime();
return new TableStack(scope, primer);
} catch (error) {
throw new Error(`Error creating TableStack: ${error}`);
}
}In a multistack deployment, there are producer stacks and consumer stacks. Producer stacks initialize AWS resources/constructs and export their references to SSM. Consumer stacks import these references at deployment time. As a result, stacks can theoretically be deployed independently from one another, though it is always wise to not specify which stack needs deploying - cdk deploy will run through all stacks and determine which stacks need updating, and leave the rest unchanged.
// Exporting from one stack (BaseStack method)
this.exportReference(scope, key, value, description);
this.exportReference(this, 'baseLayer' this.baseLayer, 'Lambda Layer containing basic shared functions');
// Importing in another stack (BaseStack method)
const importedReference = this.resolveReference(scope, key);
const baseLayer = this.resolveReference(this, 'baseLayer');- AWS CDK Developer Guide
- Amazon DynamoDB Developer Guide
- Amazon OpenSearch Service Developer Guide
- Amazon Cognito Developer Guide
/docs/- Detailed API documentation/src/scripts/- Deployment and utility scripts/test/- Test documentation and examples