import { App, StorageFactory, LambdaFunction } from "@fjall/components-infrastructure";
import { Code, Runtime, FunctionUrlAuthType, HttpMethod } from "aws-cdk-lib/aws-lambda";
import { PolicyDocument, PolicyStatement } from "aws-cdk-lib/aws-iam";
import { SubnetType } from "aws-cdk-lib/aws-ec2";
import { Duration } from "aws-cdk-lib";
import { Construct } from "constructs";
const app = App.getApp("ProductionApp");
// Create database
const database = app.addStorage(
StorageFactory.build("ProductionDB", {
type: "FreeTier",
databaseName: "production"
})
);
// Get VPC for Lambda deployment
const vpc = app.getDefaultVpc();
// API Lambda with Function URL (in a construct for access to 'this')
class ApiStack extends Construct {
constructor(scope: Construct, id: string) {
super(scope, id);
// API Lambda with Function URL
const apiFunction = new LambdaFunction(this, "RestApi", {
// Code configuration
code: Code.fromAsset("./api", {
exclude: ["*.test.js", "node_modules"]
}),
handler: "api/index.handler",
runtime: Runtime.NODEJS_18_X,
// VPC configuration (required for database access)
vpc: vpc,
vpcSubnets: { subnetType: SubnetType.PRIVATE_WITH_EGRESS },
// Execution configuration
timeout: 30,
memorySize: 1024,
lambdaDescription: "Production REST API handler",
roleDescription: "API Lambda execution role",
// Environment variables
environment: {
NODE_ENV: "production",
LOG_LEVEL: "info",
DATABASE_HOST: database.getHostEndpoint(),
DATABASE_PORT: database.getHostPort(),
DATABASE_NAME: database.getDatabaseName(),
DATABASE_SECRET_ARN: database.getCredentials().secretArn,
CACHE_TTL: "3600"
},
// Function URL with CORS
enableFunctionUrl: true,
functionUrlAuthType: FunctionUrlAuthType.NONE,
functionUrlCors: {
allowedOrigins: ["https://example.com"],
allowedMethods: [
HttpMethod.GET,
HttpMethod.POST,
HttpMethod.PUT,
HttpMethod.DELETE
],
allowedHeaders: [
"Content-Type",
"Authorization",
"X-Requested-With"
],
allowCredentials: true,
maxAge: Duration.hours(1),
exposeHeaders: ["X-Request-Id"]
},
// IAM permissions
inlinePolicy: {
"secrets-manager": new PolicyDocument({
statements: [
new PolicyStatement({
actions: ["secretsmanager:GetSecretValue"],
resources: [database.getCredentials().secretArn]
})
]
}),
"s3-access": new PolicyDocument({
statements: [
new PolicyStatement({
actions: ["s3:GetObject", "s3:PutObject"],
resources: ["arn:aws:s3:::my-app-uploads/*"]
})
]
}),
"dynamodb-cache": new PolicyDocument({
statements: [
new PolicyStatement({
actions: [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:DeleteItem"
],
resources: ["arn:aws:dynamodb:*:*:table/api-cache"]
})
]
})
},
// Tags
tags: {
Environment: "production",
Application: "rest-api",
CostCenter: "engineering"
}
});
// Allow Lambda to connect to database
database.connections.allowDefaultPortFrom(apiFunction);
// Background worker Lambda
const workerFunction = new LambdaFunction(this, "BackgroundWorker", {
code: Code.fromAsset("./worker"),
handler: "worker/index.handler",
runtime: Runtime.NODEJS_18_X,
// VPC configuration
vpc: vpc,
vpcSubnets: { subnetType: SubnetType.PRIVATE_WITH_EGRESS },
// Execution configuration
timeout: 300, // 5 minutes
memorySize: 2048,
lambdaDescription: "Background job processor",
// Environment variables
environment: {
DATABASE_HOST: database.getHostEndpoint(),
DATABASE_PORT: database.getHostPort(),
DATABASE_NAME: database.getDatabaseName(),
DATABASE_SECRET_ARN: database.getCredentials().secretArn,
BATCH_SIZE: "100"
},
// IAM permissions
inlinePolicy: {
"full-access": new PolicyDocument({
statements: [
new PolicyStatement({
actions: ["secretsmanager:GetSecretValue"],
resources: [database.getCredentials().secretArn]
}),
new PolicyStatement({
actions: ["sqs:ReceiveMessage", "sqs:DeleteMessage"],
resources: ["arn:aws:sqs:*:*:worker-queue"]
})
]
})
}
});
// Allow worker Lambda to connect to database
database.connections.allowDefaultPortFrom(workerFunction);
}
}
// Instantiate the stack
new ApiStack(app, "ApiStack");