Blog
Infrastructure-as-Code to power composable commerce AI modules

In the second part of this multi-part series of articles, we present MACA, our open-source solution for AI-composable commerce.

With Radixia Maca, our open-source initiative for AI-driven composable commerce, we're not just rethinking e-commerce—we're re-architecting it. At the heart of this architecture is infrastructure-as-code, which ensures consistency, traceability, and repeatability across deployments. In this chapter, we shift from design principles to execution, implementing the complete serverless infrastructure for our platform using AWS CDK (Cloud Development Kit). This gives us the flexibility to define complex cloud architectures in code, leverage reusable constructs, and scale components independently.
In this section, we showcase how to implement the complete infrastructure for our serverless e-commerce using AWS CDK. This approach allows us to define infrastructure as code, making it easier to manage, version, and deploy.
CDK Project Structure
Our CDK project is organized into modular constructs, each responsible for a specific part of the infrastructure:
- Main Stack: Coordinates all constructs and defines dependencies
- API Gateway Construct: Manages REST APIs for the frontend
- Bedrock Search Construct: Configures semantic search with Bedrock
- Data Storage Construct: Manages DynamoDB and other storage services
- Frontend Construct: Configures frontend hosting
- MCP Server Construct: Implements the Model Context Protocol server
- Strapi Service Construct: Configures the Strapi service on Fargate
Let's look at the implementation of each component in detail.
Main Stack
The main stack (ServerlessEcommerceStack) is the entry point of our CDK infrastructure. It coordinates all constructs and defines dependencies between them.
// lib/serverless-ecommerce-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { ApiGatewayConstruct } from './api-gateway-construct';
import { BedrockSearchConstruct } from './bedrock-search-construct';
import { DataStorageConstruct } from './data-storage-construct';
import { FrontendConstruct } from './frontend-construct';
import { McpServerConstruct } from './mcp-server-construct';
import { StrapiServiceConstruct } from './strapi-service-construct';
export class ServerlessEcommerceStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Create the data storage construct
const dataStorage = new DataStorageConstruct(this, 'DataStorage');
// Create the MCP server construct
const mcpServer = new McpServerConstruct(this, 'McpServer', {
vpc: dataStorage.vpc,
productsTable: dataStorage.productsTable,
commerceLayerSecret: dataStorage.commerceLayerSecret,
strapiSecret: dataStorage.strapiSecret,
});
// Create the Bedrock search construct
const bedrockSearch = new BedrockSearchConstruct(this, 'BedrockSearch', {
productsTable: dataStorage.productsTable,
commerceLayerSecret: dataStorage.commerceLayerSecret,
strapiSecret: dataStorage.strapiSecret,
mcpServerUrl: mcpServer.serviceUrl,
});
// Create the Strapi service construct
const strapiService = new StrapiServiceConstruct(this, 'StrapiService', {
vpc: dataStorage.vpc,
strapiSecret: dataStorage.strapiSecret,
commerceLayerSecret: dataStorage.commerceLayerSecret,
syncLambda: dataStorage.strapiClSyncLambda,
});
// Create the API Gateway construct
const apiGateway = new ApiGatewayConstruct(this, 'ApiGateway', {
semanticSearchLambda: bedrockSearch.semanticSearchLambda,
orderProcessorLambda: dataStorage.orderProcessorLambda,
inventoryManagerLambda: dataStorage.inventoryManagerLambda,
});
// Create the frontend construct
const frontend = new FrontendConstruct(this, 'Frontend', {
apiEndpoint: apiGateway.apiEndpoint,
strapiEndpoint: strapiService.serviceUrl,
});
// Outputs
new cdk.CfnOutput(this, 'ApiEndpoint', {
value: apiGateway.apiEndpoint,
description: 'API Gateway endpoint',
});
new cdk.CfnOutput(this, 'FrontendUrl', {
value: frontend.distributionUrl,
description: 'Frontend URL',
});
new cdk.CfnOutput(this, 'StrapiServiceUrl', {
value: strapiService.serviceUrl,
description: 'Strapi service URL',
});
new cdk.CfnOutput(this, 'McpServerUrl', {
value: mcpServer.serviceUrl,
description: 'MCP server URL',
});
}
}
Data Storage Construct
The DataStorageConstruct manages all data storage resources, including DynamoDB, Secrets Manager, and related Lambda functions.
// lib/data-storage-construct.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as sns from 'aws-cdk-lib/aws-sns';
import * as events from 'aws-cdk-lib/aws-events';
import * as targets from 'aws-cdk-lib/aws-events-targets';
import * as path from 'path';
export interface DataStorageConstructProps {
// Optional properties
}
export class DataStorageConstruct extends Construct {
public readonly productsTable: dynamodb.Table;
public readonly ordersTable: dynamodb.Table;
public readonly inventoryTable: dynamodb.Table;
public readonly commerceLayerSecret: secretsmanager.Secret;
public readonly strapiSecret: secretsmanager.Secret;
public readonly vpc: ec2.Vpc;
public readonly strapiClSyncLambda: lambda.Function;
public readonly orderProcessorLambda: lambda.Function;
public readonly inventoryManagerLambda: lambda.Function;
public readonly lowStockTopic: sns.Topic;
constructor(scope: Construct, id: string, props?: DataStorageConstructProps) {
super(scope, id);
// Create a VPC for services
this.vpc = new ec2.Vpc(this, 'VPC', {
maxAzs: 2,
natGateways: 1,
});
// Create DynamoDB tables
this.productsTable = new dynamodb.Table(this, 'ProductsTable', {
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
removalPolicy: cdk.RemovalPolicy.DESTROY, // Only for development environment
});
// Add a global secondary index for category
this.productsTable.addGlobalSecondaryIndex({
indexName: 'CategoryIndex',
partitionKey: { name: 'category', type: dynamodb.AttributeType.STRING },
projectionType: dynamodb.ProjectionType.ALL,
});
this.ordersTable = new dynamodb.Table(this, 'OrdersTable', {
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
removalPolicy: cdk.RemovalPolicy.DESTROY, // Only for development environment
});
this.inventoryTable = new dynamodb.Table(this, 'InventoryTable', {
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
removalPolicy: cdk.RemovalPolicy.DESTROY, // Only for development environment
});
// Create secrets for credentials
this.commerceLayerSecret = new secretsmanager.Secret(this, 'CommerceLayerSecret', {
secretName: 'commerceLayer/credentials',
description: 'Commerce Layer credentials',
generateSecretString: {
secretStringTemplate: JSON.stringify({
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
endpoint: 'https://yourdomain.commercelayer.io',
}) ,
generateStringKey: 'password',
},
});
this.strapiSecret = new secretsmanager.Secret(this, 'StrapiSecret', {
secretName: 'strapi/credentials',
description: 'Strapi CMS credentials',
generateSecretString: {
secretStringTemplate: JSON.stringify({
apiKey: 'your-api-key',
endpoint: 'https://your-strapi-endpoint.com',
}) ,
generateStringKey: 'password',
},
});
// Create an SNS topic for low stock notifications
this.lowStockTopic = new sns.Topic(this, 'LowStockTopic', {
displayName: 'Low stock notifications',
});
// Create Lambda functions
this.strapiClSyncLambda = new lambda.Function(this, 'StrapiClSyncLambda', {
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'index.handler',
code: lambda.Code.fromAsset(path.join(__dirname, '../lambda/strapi-cl-sync')),
timeout: cdk.Duration.seconds(30),
memorySize: 256,
environment: {
COMMERCE_LAYER_SECRET_ARN: this.commerceLayerSecret.secretArn,
STRAPI_SECRET_ARN: this.strapiSecret.secretArn,
PRODUCTS_TABLE: this.productsTable.tableName,
},
});
this.orderProcessorLambda = new lambda.Function(this, 'OrderProcessorLambda', {
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'index.handler',
code: lambda.Code.fromAsset(path.join(__dirname, '../lambda/order-processor')),
timeout: cdk.Duration.seconds(30),
memorySize: 256,
environment: {
COMMERCE_LAYER_SECRET_ARN: this.commerceLayerSecret.secretArn,
ORDERS_TABLE: this.ordersTable.tableName,
FROM_EMAIL: 'noreply@serverlessday.com',
EMAIL_TEMPLATE_NAME: 'OrderConfirmation',
},
});
this.inventoryManagerLambda = new lambda.Function(this, 'InventoryManagerLambda', {
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'index.handler',
code: lambda.Code.fromAsset(path.join(__dirname, '../lambda/inventory-manager')),
timeout: cdk.Duration.seconds(30),
memorySize: 256,
environment: {
COMMERCE_LAYER_SECRET_ARN: this.commerceLayerSecret.secretArn,
INVENTORY_TABLE: this.inventoryTable.tableName,
LOW_STOCK_THRESHOLD: '5',
LOW_STOCK_TOPIC_ARN: this.lowStockTopic.topicArn,
},
});
// Grant necessary permissions
this.commerceLayerSecret.grantRead(this.strapiClSyncLambda);
this.strapiSecret.grantRead(this.strapiClSyncLambda);
this.productsTable.grantReadWriteData(this.strapiClSyncLambda);
this.commerceLayerSecret.grantRead(this.orderProcessorLambda);
this.ordersTable.grantReadWriteData(this.orderProcessorLambda);
this.commerceLayerSecret.grantRead(this.inventoryManagerLambda);
this.inventoryTable.grantReadWriteData(this.inventoryManagerLambda);
this.lowStockTopic.grantPublish(this.inventoryManagerLambda);
// Create an EventBridge rule to run the inventoryManagerLambda daily
new events.Rule(this, 'DailyInventorySync', {
schedule: events.Schedule.cron({ minute: '0', hour: '0' }),
targets: [new targets.LambdaFunction(this.inventoryManagerLambda)],
});
}
}
Bedrock Search Construct
The BedrockSearchConstruct configures semantic search using Amazon Bedrock and the Model Context Protocol.
// lib/bedrock-search-construct.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
import * as path from 'path';
export interface BedrockSearchConstructProps {
productsTable: dynamodb.Table;
commerceLayerSecret: secretsmanager.Secret;
strapiSecret: secretsmanager.Secret;
mcpServerUrl: string;
}
export class BedrockSearchConstruct extends Construct {
public readonly semanticSearchLambda: lambda.Function;
public readonly bedrockAgentId: string;
constructor(scope: Construct, id: string, props: BedrockSearchConstructProps) {
super(scope, id);
// Create an IAM role for Bedrock access
const bedrockRole = new iam.Role(this, 'BedrockRole', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
],
});
// Add policies for Bedrock access
bedrockRole.addToPolicy(
new iam.PolicyStatement({
actions: [
'bedrock:InvokeModel',
'bedrock:InvokeAgent',
'bedrock:CreateAgent',
'bedrock:UpdateAgent',
],
resources: ['*'], // Limit to specific resources in production
})
);
// Create the Lambda function for semantic search
this.semanticSearchLambda = new lambda.Function(this, 'SemanticSearchLambda', {
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'index.handler',
code: lambda.Code.fromAsset(path.join(__dirname, '../lambda/semantic-search')),
timeout: cdk.Duration.seconds(30),
memorySize: 512,
environment: {
COMMERCE_LAYER_SECRET_ARN: props.commerceLayerSecret.secretArn,
STRAPI_SECRET_ARN: props.strapiSecret.secretArn,
PRODUCTS_TABLE: props.productsTable.tableName,
MCP_SERVER_URL: props.mcpServerUrl,
},
role: bedrockRole,
});
// Grant necessary permissions
props.commerceLayerSecret.grantRead(this.semanticSearchLambda);
props.strapiSecret.grantRead(this.semanticSearchLambda);
props.productsTable.grantReadData(this.semanticSearchLambda);
// Set the Bedrock agent ID (in a real implementation, this would be created via CDK)
this.bedrockAgentId = 'agent-id-placeholder';
// Outputs
new cdk.CfnOutput(this, 'SemanticSearchLambdaArn', {
value: this.semanticSearchLambda.functionArn,
description: 'ARN of the semantic search Lambda function',
});
}
}
MCP Server Construct
The McpServerConstruct implements the Model Context Protocol server that acts as a connector between Bedrock Agents and data sources.
// lib/mcp-server-construct.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ecr from 'aws-cdk-lib/aws-ecr';
import * as ecsPatterns from 'aws-cdk-lib/aws-ecs-patterns';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as path from 'path';
export interface McpServerConstructProps {
vpc: ec2.Vpc;
productsTable: dynamodb.Table;
commerceLayerSecret: secretsmanager.Secret;
strapiSecret: secretsmanager.Secret;
}
export class McpServerConstruct extends Construct {
public readonly serviceUrl: string;
constructor(scope: Construct, id: string, props: McpServerConstructProps) {
super(scope, id);
// Create an ECR repository for the Docker image
const repository = new ecr.Repository(this, 'McpServerRepository', {
repositoryName: 'mcp-server',
removalPolicy: cdk.RemovalPolicy.DESTROY, // Only for development environment
});
// Create an ECS cluster
const cluster = new ecs.Cluster(this, 'McpServerCluster', {
vpc: props.vpc,
});
// Create an execution role for the ECS task
const executionRole = new iam.Role(this, 'McpServerExecutionRole', {
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy'),
],
});
// Create a task role for the ECS task
const taskRole = new iam.Role(this, 'McpServerTaskRole', {
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
});
// Grant necessary permissions
props.commerceLayerSecret.grantRead(taskRole);
props.strapiSecret.grantRead(taskRole);
props.productsTable.grantReadData(taskRole);
// Create a Fargate service
const fargateService = new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'McpServerService', {
cluster,
memoryLimitMiB: 1024,
cpu: 512,
desiredCount: 2,
taskImageOptions: {
image: ecs.ContainerImage.fromAsset(path.join(__dirname, '../docker/mcp-server')),
containerPort: 3000,
environment: {
COMMERCE_LAYER_SECRET_ARN: props.commerceLayerSecret.secretArn,
STRAPI_SECRET_ARN: props.strapiSecret.secretArn,
PRODUCTS_TABLE: props.productsTable.tableName,
NODE_ENV: 'production',
},
taskRole,
executionRole,
},
publicLoadBalancer: true,
});
// Configure health check
fargateService.targetGroup.configureHealthCheck({
path: '/health',
interval: cdk.Duration.seconds(60),
timeout: cdk.Duration.seconds(5),
healthyThresholdCount: 2,
unhealthyThresholdCount: 5,
});
// Configure auto scaling
const scaling = fargateService.service.autoScaleTaskCount({
minCapacity: 2,
maxCapacity: 10,
});
scaling.scaleOnCpuUtilization('CpuScaling', {
targetUtilizationPercent: 70,
scaleInCooldown: cdk.Duration.seconds(60),
scaleOutCooldown: cdk.Duration.seconds(60),
});
// Set the service URL
this.serviceUrl = `http://${fargateService.loadBalancer.loadBalancerDnsName}`;
// Outputs
new cdk.CfnOutput(this, 'McpServerServiceUrl', {
value: this.serviceUrl,
description: 'MCP Server service URL',
}) ;
}
}
You're halfway in — the full article lives on our blog.
Continue reading on blog.radixia.ai →Free to read. Subscribe there to get new posts by email.
