FsCDK Multi-Tier Application Example
This example demonstrates how to build a complete multi-tier web application using FsCDK with AWS best practices.
Architecture Overview
Architecture Details
This example demonstrates a production-ready multi-tier application with security best practices: - High Availability: Multi-AZ deployment across 2 availability zones - Security: VPC isolation, security groups, encryption at rest and in transit - Scalability: Auto-scaling Lambda, Multi-AZ RDS with read replicas - Performance: CloudFront CDN for global content delivery
Architecture Diagram

Production-ready multi-tier web application showing VPC network segmentation, security zones, and data flow. Components: CloudFront CDN → Application Load Balancer → Lambda Functions → RDS PostgreSQL, with S3 static assets and Cognito authentication.
Key Components: - CloudFront CDN: Global content delivery with HTTPS/TLS 1.2+ - Application Load Balancer: Multi-AZ load balancing in public subnets - Lambda Functions: Serverless compute in private subnets with auto-scaling - RDS PostgreSQL: Multi-AZ database with encryption and automated backups - S3: Static asset storage with versioning and encryption - Cognito: Managed authentication and user management
Security Layers: - Public subnets: ALB with internet-facing access - Private subnets: Lambda and RDS with no direct internet access - Security groups: Least-privilege network access control - NAT Gateway: Controlled outbound internet access for private resources
Network Flow: 1. User requests → CloudFront → Internet Gateway → ALB (public subnet) 2. ALB → Lambda functions (private subnet, security group SG-Lambda) 3. Lambda → RDS database (private subnet, security group SG-Database, inbound only from SG-Lambda) 4. Lambda outbound → NAT Gateway → Internet Gateway (for API calls) 5. Static content → S3 → CloudFront cache
Note: To generate this diagram, use the specifications in
docs/img/DIAGRAM_SPECIFICATIONS.mdwith tools like Cloudcraft, Draw.io, or Lucidchart.
Example Stack
#r "../src/bin/Release/net8.0/publish/Amazon.JSII.Runtime.dll"
#r "../src/bin/Release/net8.0/publish/Constructs.dll"
#r "../src/bin/Release/net8.0/publish/Amazon.CDK.Lib.dll"
#r "../src/bin/Release/net8.0/publish/System.Text.Json.dll"
#r "../src/bin/Release/net8.0/publish/FsCDK.dll"
open Amazon.CDK
open Amazon.CDK.AWS.S3
open Amazon.CDK.AWS.RDS
open Amazon.CDK.AWS.EC2
open Amazon.CDK.AWS.Lambda
open Amazon.CDK.AWS.Cognito
open Amazon.CDK.AWS.CloudFront
open FsCDK
Best Practices Demonstrated
Security
- Least Privilege: Security groups deny all by default
- Encryption: RDS and S3 use encryption at rest
- Strong Authentication: Cognito with MFA and strong password policy
- Private Subnets: Database and Lambda in private subnets
- No Public Access: Database not publicly accessible
High Availability
- Multi-AZ: VPC spans multiple availability zones
- Multi-AZ RDS: Database replicated across AZs
- Automated Backups: 7-day retention with preferred window
- CloudFront CDN: Global content delivery
Cost Optimization
- Right-sized Instances: t3.small for RDS, appropriate memory for Lambda
- Single NAT Gateway: Development/staging configuration
- S3 Lifecycle Rules: Automatic transition to cheaper storage
- Regional CDN: PriceClass100 for US/Canada/Europe
Performance
- HTTP/2: CloudFront uses HTTP/2
- IPv6: Enabled for better connectivity
- Lambda Insights: Performance monitoring
- X-Ray Tracing: Distributed tracing enabled
Operational Excellence
- Automated Backups: RDS backup retention
- Auto Minor Upgrades: RDS automatically updates
- Monitoring: Lambda Insights and X-Ray
- Tagging: All resources properly tagged
Deployment
|
Environment Variables
Create a .env file:
AWS_ACCOUNT=123456789012
AWS_REGION=us-east-1
Monitoring
After deployment, monitor your application:
- CloudWatch Logs: Lambda function logs
- RDS Performance Insights: Database performance
- CloudFront Metrics: CDN performance and cache hit rate
- X-Ray Service Map: Distributed tracing visualization
Scaling
To scale for production:
- Increase NAT gateways to 2+ for HA:
natGateways 2 - Upgrade RDS instance:
instanceType (InstanceType.Of(InstanceClass.MEMORY5, InstanceSize.LARGE)) - Add more Lambda functions with ALB
- Expand CloudFront price class:
priceClass PriceClass.PRICE_CLASS_ALL
Cost Estimation
Approximate monthly costs (us-east-1) (at of Oct25):
- VPC + NAT Gateway: ~$32
- RDS t3.small (Multi-AZ): ~$50
- Lambda (1M requests, 512MB): ~$10
- S3 (100GB, with lifecycle): ~$2
- CloudFront (PriceClass100, 100GB transfer): ~$8.50
- Cognito (10k MAU): Free
Total: ~$102/month (excluding data transfer)
Security Checklist
- [x] All data encrypted at rest
- [x] All data encrypted in transit (TLS 1.2+)
- [x] Security groups follow least privilege
- [x] Database in private subnet
- [x] No hardcoded credentials
- [x] MFA available for users
- [x] Strong password policy enforced
- [x] Automated backups enabled
- [x] Deletion protection enabled
Next Steps
- Add Application Load Balancer for Lambda
- Implement API Gateway for REST API
- Add Route53 for custom domain
- Configure WAF for CloudFront
- Set up CloudWatch alarms
- Implement CI/CD pipeline
<summary> Factory helpers to build common IBehaviorOptions for S3 and HTTP origins. These helpers are useful if you prefer to construct behaviors and pass them via defaultBehavior/additionalBehavior. </summary>
<summary>Provides information about, and means to manipulate, the current environment and platform. This class cannot be inherited.</summary>
System.Environment.GetEnvironmentVariable(variable: string, target: System.EnvironmentVariableTarget) : string
<summary>Creates an AWS CDK Stack construct.</summary>
<param name="name">The name of the stack.</param>
<code lang="fsharp"> stack "MyStack" { lambda myFunction bucket myBucket } </code>
<summary>Creates an AWS CDK App construct.</summary>
<code lang="fsharp"> app { context [ ("environment", "production"); ("feature-flag", true) ] stackTraces true } </code>
<summary>Adds context to the App with a key-value pair.</summary>
<param name="config">The current stack configuration.</param>
<param name="keys">The context key-value pairs to add.</param>
<code lang="fsharp"> app { context [ ("environment", "production") ("feature-flag", true) ] } </code>
<summary>Sets the stack description.</summary>
<param name="config">The current stack configuration.</param>
<param name="desc">A description of the stack.</param>
<code lang="fsharp"> stack "MyStack" { description "My application stack" } </code>
<summary>Adds tags to the stack.</summary>
<param name="config">The current stack configuration.</param>
<param name="tags">A list of key-value pairs for tagging.</param>
<code lang="fsharp"> stack "MyStack" { tags [ "Environment", "Production"; "Team", "DevOps" ] } </code>
type BlockPublicAccess = inherit DeputyBase new: options: IBlockPublicAccessOptions -> unit member BlockPublicAcls: Nullable<bool> member BlockPublicPolicy: Nullable<bool> member IgnorePublicAcls: Nullable<bool> member RestrictPublicBuckets: Nullable<bool> static member BLOCK_ACLS: BlockPublicAccess static member BLOCK_ACLS_ONLY: BlockPublicAccess static member BLOCK_ALL: BlockPublicAccess
--------------------
BlockPublicAccess(options: IBlockPublicAccessOptions) : BlockPublicAccess
<summary> Enables or disables versioning for the S3 bucket. **Security Best Practice:** Enable versioning for: - Critical data that requires audit trails - Data subject to compliance requirements (HIPAA, SOC2, etc.) - Production buckets storing business data **Cost Consideration:** Versioning stores all versions of objects, increasing storage costs. Only disable for: - Temporary/cache buckets - Build artifacts with short lifecycle - Development/testing buckets **Default:** false (opt-in for cost optimization) </summary>
<param name="value">True to enable versioning, false to disable.</param>
<param name="config">The current bucket configuration.</param>
<code lang="fsharp"> bucket "production-data" { versioned true // Enable for production } bucket "cache-bucket" { versioned false // Disable for temp data } </code>
<summary>Creates a VPC configuration with AWS best practices.</summary>
<param name="name">The VPC name.</param>
<code lang="fsharp"> vpc "MyVpc" { maxAzs 2 natGateways 1 cidr "10.0.0.0/16" } </code>
<summary>Sets the maximum number of Availability Zones to use.</summary>
<param name="config">The current VPC configuration.</param>
<param name="maxAzs">The maximum number of AZs (default: 2 for HA).</param>
<code lang="fsharp"> vpc "MyVpc" { maxAzs 3 } </code>
<summary>Sets the number of NAT Gateways.</summary>
<param name="config">The current VPC configuration.</param>
<param name="natGateways">The number of NAT gateways (default: 1 for cost optimization).</param>
<code lang="fsharp"> vpc "MyVpc" { natGateways 2 } </code>
<summary>Sets the CIDR block for the VPC.</summary>
<param name="config">The current VPC configuration.</param>
<param name="cidr">The CIDR block (e.g., "10.0.0.0/16").</param>
<code lang="fsharp"> vpc "MyVpc" { cidr "10.0.0.0/16" } </code>
<summary>Creates a Security Group configuration.</summary>
<param name="name">The Security Group name.</param>
<code lang="fsharp"> securityGroup "MySecurityGroup" { vpc myVpc description "Security group for my application" allowAllOutbound false } </code>
<summary>Sets the VPC for the Security Group.</summary>
<param name="config">The current Security Group configuration.</param>
<param name="vpc">The VPC.</param>
<code lang="fsharp"> securityGroup "MySecurityGroup" { vpc myVpc } </code>
<summary>Sets the description for the Security Group.</summary>
<param name="config">The current Security Group configuration.</param>
<param name="description">The description.</param>
<code lang="fsharp"> securityGroup "MySecurityGroup" { description "Security group for my application" } </code>
<summary>Sets whether to allow all outbound traffic.</summary>
<param name="config">The current Security Group configuration.</param>
<param name="allow">Whether to allow all outbound (default: false for least privilege).</param>
<code lang="fsharp"> securityGroup "MySecurityGroup" { allowAllOutbound true } </code>
<summary>Creates an RDS Database Instance with AWS best practices.</summary>
<param name="name">The database instance name.</param>
<code lang="fsharp"> rdsInstance "MyDatabase" { vpc myVpc postgresEngine PostgresEngineVersion.VER_15 instanceType (InstanceType.Of(InstanceClass.BURSTABLE3, InstanceSize.SMALL)) multiAz true backupRetentionDays 7.0 } </code>
<summary>Sets the VPC.</summary>
<summary>Sets PostgreSQL as the database engine with a specific version.</summary>
<summary>Sets the instance type.</summary>
type InstanceType = inherit DeputyBase new: instanceTypeIdentifier: string -> unit member IsBurstable: unit -> bool member SameInstanceClassAs: other: InstanceType -> bool member ToString: unit -> string static member Of: instanceClass: InstanceClass * instanceSize: InstanceSize -> InstanceType member Architecture: InstanceArchitecture
--------------------
InstanceType(instanceTypeIdentifier: string) : InstanceType
<summary>Sets the allocated storage in GB.</summary>
<summary>Sets the database name.</summary>
<summary>Enables or disables Multi-AZ deployment.</summary>
<summary>Sets the backup retention period in days.</summary>
<summary>Enables storage encryption.</summary>
<summary>Enables or disables deletion protection.</summary>
<summary>Sets whether the database is publicly accessible.</summary>
<summary>Sets the VPC subnets.</summary>
type SubnetSelection = interface ISubnetSelection new: unit -> unit member AvailabilityZones: string array member OnePerAz: Nullable<bool> member SubnetFilters: SubnetFilter array member SubnetGroupName: string member SubnetType: Nullable<SubnetType> member Subnets: ISubnet array
--------------------
SubnetSelection() : SubnetSelection
<summary>Adds a security group.</summary>
<summary>Sets the preferred backup window.</summary>
<summary>Sets the preferred maintenance window.</summary>
<summary> Creates an S3 lifecycle transition rule for moving objects to different storage classes. Transitions reduce storage costs by automatically moving objects to cheaper storage tiers. </summary>
<code lang="fsharp"> transition { storageClass StorageClass.GLACIER transitionAfter (Duration.Days(90.0)) } </code>
<summary> Sets the storage class to transition to. Common classes: GLACIER (low-cost archival), DEEP_ARCHIVE (lowest cost, rare access), INTELLIGENT_TIERING (automatic cost optimization), GLACIER_IR (instant retrieval). </summary>
<param name="storageClass">The target storage class.</param>
type StorageClass = inherit DeputyBase new: value: string -> unit member ToString: unit -> string member Value: string static member DEEP_ARCHIVE: StorageClass static member GLACIER: StorageClass static member GLACIER_INSTANT_RETRIEVAL: StorageClass static member INFREQUENT_ACCESS: StorageClass static member INTELLIGENT_TIERING: StorageClass static member ONE_ZONE_INFREQUENT_ACCESS: StorageClass
--------------------
StorageClass(value: string) : StorageClass
<summary> Sets when objects transition after creation (use Duration.Days()). Example: transitionAfter (Duration.Days(90.0)) moves objects after 90 days. </summary>
<param name="duration">Time after object creation to transition.</param>
<summary>Creates a Cognito User Pool with AWS best practices.</summary>
<param name="name">The user pool name.</param>
<code lang="fsharp"> userPool "MyUserPool" { signInWithEmail selfSignUpEnabled true mfa Mfa.OPTIONAL } </code>
<summary>Enables email only as sign-in alias.</summary>
<summary>Enables or disables self sign-up.</summary>
<summary>Sets MFA configuration.</summary>
<summary>Sets password policy.</summary>
type PasswordPolicy = interface IPasswordPolicy new: unit -> unit member MinLength: Nullable<float> member PasswordHistorySize: Nullable<float> member RequireDigits: Nullable<bool> member RequireLowercase: Nullable<bool> member RequireSymbols: Nullable<bool> member RequireUppercase: Nullable<bool> member TempPasswordValidity: Duration
--------------------
PasswordPolicy() : PasswordPolicy
<summary>Sets account recovery method.</summary>
<summary>Creates a Cognito User Pool Client.</summary>
<param name="name">The client name.</param>
<code lang="fsharp"> userPoolClient "MyAppClient" { userPool myUserPool generateSecret false } </code>
<summary>Sets the user pool.</summary>
<summary>Enables or disables secret generation.</summary>
<summary>Sets authentication flows.</summary>
type AuthFlow = interface IAuthFlow new: unit -> unit member AdminUserPassword: Nullable<bool> member Custom: Nullable<bool> member User: Nullable<bool> member UserPassword: Nullable<bool> member UserSrp: Nullable<bool>
--------------------
AuthFlow() : AuthFlow
<summary>Sets token validities.</summary>
<summary>Creates a Lambda function configuration.</summary>
<param name="name">The function name.</param>
<code lang="fsharp"> lambda "MyFunction" { handler "index.handler" runtime Runtime.NODEJS_18_X code "./lambda" timeout 30.0 } </code>
<summary>Sets the runtime for the Lambda function.</summary>
<param name="config">The function configuration.</param>
<param name="runtime">The Lambda runtime.</param>
<code lang="fsharp"> lambda "MyFunction" { runtime Runtime.NODEJS_18_X } </code>
type Runtime = inherit DeputyBase new: name: string * ?family: Nullable<RuntimeFamily> * ?props: ILambdaRuntimeProps -> unit member RuntimeEquals: other: Runtime -> bool member ToString: unit -> string member BundlingImage: DockerImage member Family: Nullable<RuntimeFamily> member IsVariable: bool member Name: string member SupportsCodeGuruProfiling: bool member SupportsInlineCode: bool ...
--------------------
Runtime(name: string, ?family: System.Nullable<RuntimeFamily>, ?props: ILambdaRuntimeProps) : Runtime
<summary>Sets the handler for the Lambda function.</summary>
<param name="config">The function configuration.</param>
<param name="handler">The handler name (e.g., "index.handler").</param>
<code lang="fsharp"> lambda "MyFunction" { handler "index.handler" } </code>
<summary>Sets the code source from a Code object.</summary>
<param name="config">The function configuration.</param>
<param name="path">The Code object.</param>
<code lang="fsharp"> lambda "MyFunction" { code (Code.FromBucket myBucket "lambda.zip") } </code>
<summary>Sets the timeout for the Lambda function.</summary>
<param name="config">The function configuration.</param>
<param name="seconds">The timeout in seconds.</param>
<code lang="fsharp"> lambda "MyFunction" { timeout 30.0 } </code>
<summary>Sets the memory allocation for the Lambda function.</summary>
<param name="config">The function configuration.</param>
<param name="mb">The memory size in megabytes.</param>
<code lang="fsharp"> lambda "MyFunction" { memory 512 } </code>
<summary>Sets the description for the Lambda function.</summary>
<param name="config">The function configuration.</param>
<param name="desc">The function description.</param>
<code lang="fsharp"> lambda "MyFunction" { description "Processes incoming orders" } </code>
<summary>Specifies which subnets in the VPC the function should use.</summary>
<param name="config">The function configuration.</param>
<param name="subnets">Subnet selection.</param>
<code lang="fsharp"> lambda "MyFunction" { vpcSubnets (SubnetSelection.SubnetType SubnetType.PRIVATE_WITH_EGRESS) } </code>
<summary>Adds one or more security groups to the function's network configuration.</summary>
<param name="config">The function configuration.</param>
<param name="sgs">List of security groups.</param>
<code lang="fsharp"> lambda "MyFunction" { securityGroups [ sgA; sgB ] } </code>
<summary>Sets environment variables for the Lambda function.</summary>
<param name="config">The function configuration.</param>
<param name="env">List of key-value pairs for environment variables.</param>
<code lang="fsharp"> lambda "MyFunction" { environment [ "KEY1", "value1"; "KEY2", "value2" ] } </code>
<summary>Sets the tracing mode for AWS X-Ray.</summary>
<param name="config">The function configuration.</param>
<param name="tracing">Tracing mode (e.g., ACTIVE, PASS_THROUGH).</param>
<code lang="fsharp"> lambda "MyFunction" { tracing Tracing.ACTIVE } </code>
<summary>Sets the Lambda Insights version to enable enhanced monitoring.</summary>
<param name="config">The function configuration.</param>
<param name="version">Insights layer version.</param>
<code lang="fsharp"> lambda "MyFunction" { insightsVersion LambdaInsightsVersion.VERSION_1_0_135_0 } </code>
<summary>Creates a CloudFront distribution with AWS best practices.</summary>
<param name="name">The distribution name.</param>
<remarks> Example: cloudFrontDistribution "MyCDN" { s3DefaultBehavior myBucket defaultRootObject "index.html" domainName "static.example.com" priceClass PriceClass.PRICE_CLASS_100 } </remarks>
<summary>Sets the default behavior from a pre-built IBehaviorOptions.</summary>
<summary>Sets the default root object (e.g., "index.html").</summary>
<summary>Sets the comment for the distribution.</summary>
<summary>Sets the HTTP version preference.</summary>
<summary>Enables or disables IPv6.</summary>
<summary>Sets the minimum TLS protocol version.</summary>
<summary>Sets the price class.</summary>
<summary>Enables logging to an S3 bucket (optionally with a prefix and cookies flag).</summary>
FsCDK