Header menu logo FsCDK

Local Testing with LocalStack

Test your FsCDK infrastructure locally without deploying to AWS using LocalStack.

What is LocalStack?

LocalStack emulates AWS services on your local machine, enabling: - Fast iteration without AWS deployment delays - Zero AWS costs during development - Offline development and testing - Integration tests in CI/CD pipelines

Quick Setup

1. Install LocalStack

# Using pip
pip install localstack

# Using Homebrew (macOS)
brew install localstack/tap/localstack-cli

# Using Docker directly
docker run -d -p 4566:4566 localstack/localstack

2. Install CDK Local Wrapper

npm install -g aws-cdk-local

3. Start LocalStack

localstack start -d

Using FsCDK with LocalStack

Your FsCDK code works unchanged! Just use cdklocal instead of cdk:

# Synthesize CloudFormation template
cdklocal synth

# Deploy to LocalStack
cdklocal deploy

# Destroy stack
cdklocal destroy

Example: Testing a Lambda Function

#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 FsCDK
open Amazon.CDK
open Amazon.CDK.AWS.Lambda
open Amazon.CDK.AWS.DynamoDB

// Define your stack normally
stack "LocalTestStack" {
    description "Stack for local testing with LocalStack"

    table "Users" {
        partitionKey "userId" AttributeType.STRING
        billingMode BillingMode.PAY_PER_REQUEST
    }

    lambda "UserFunction" {
        runtime Runtime.DOTNET_8
        handler "Handler::process"
        code "./publish"
        environment [ "TABLE_NAME", "Users" ]
    }
}

Deploy to LocalStack:

cdklocal deploy LocalTestStack

Test your function:

# Invoke Lambda locally
aws --endpoint-url=http://localhost:4566 lambda invoke \
    --function-name UserFunction \
    response.json

# Query DynamoDB locally
aws --endpoint-url=http://localhost:4566 dynamodb scan \
    --table-name Users

Integration Testing Example

Use LocalStack in your test suite:

open Expecto
open System.Diagnostics

[<Tests>]
let integrationTests =
    testList "LocalStack Integration" [
        test "can deploy and invoke lambda" {
            // Deploy to LocalStack
            let deploy = Process.Start("cdklocal", "deploy --require-approval never")
            deploy.WaitForExit()
            Expect.equal deploy.ExitCode 0 "Deployment should succeed"
            
            // Test your resources via AWS CLI with local endpoint
            // ...
        }
    ]

Configuration

LocalStack uses standard AWS environment variables:

# Point AWS SDK to LocalStack
export AWS_ENDPOINT_URL=http://localhost:4566

# Use dummy credentials (LocalStack doesn't validate)
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
export AWS_DEFAULT_REGION=us-east-1

Supported Services

LocalStack supports 80+ AWS services. Most FsCDK resources work including: - Lambda, DynamoDB, S3, SQS, SNS - API Gateway, EventBridge, Step Functions - VPC, EC2, RDS (basic functionality) - CloudWatch, IAM, Secrets Manager

Check LocalStack coverage for specific service support.

Limitations

CI/CD Integration

Add LocalStack to GitHub Actions:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: LocalStack/setup-localstack@v0.2.0
      - run: cdklocal deploy
      - run: dotnet test

Resources


Pro Tip: Start with LocalStack for rapid iteration, then verify in a real AWS dev account before production deployment.

namespace FsCDK
namespace Amazon
namespace Amazon.CDK
namespace Amazon.CDK.AWS
namespace Amazon.CDK.AWS.Lambda
namespace Amazon.CDK.AWS.DynamoDB
val stack: name: string -> StackBuilder
<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>
custom operation: description (string) Calls StackBuilder.Description
<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>
val table: name: string -> TableBuilder
<summary>Creates a DynamoDB table configuration.</summary>
<param name="name">The table name.</param>
<code lang="fsharp"> table "MyTable" { partitionKey "id" AttributeType.STRING billingMode BillingMode.PAY_PER_REQUEST } </code>
custom operation: partitionKey (string) (AttributeType) Calls TableBuilder.PartitionKey
<summary>Sets the partition key for the table.</summary>
<param name="config">The current table configuration.</param>
<param name="name">The attribute name for the partition key.</param>
<param name="attrType">The attribute type (STRING, NUMBER, or BINARY).</param>
<code lang="fsharp"> table "MyTable" { partitionKey "id" AttributeType.STRING } </code>
[<Struct>] type AttributeType = | BINARY = 0 | NUMBER = 1 | STRING = 2
field AttributeType.STRING: AttributeType = 2
custom operation: billingMode (BillingMode) Calls TableBuilder.BillingMode
<summary>Sets the billing mode for the table.</summary>
<param name="config">The current table configuration.</param>
<param name="mode">The billing mode (PAY_PER_REQUEST or PROVISIONED).</param>
<code lang="fsharp"> table "MyTable" { billingMode BillingMode.PAY_PER_REQUEST } </code>
[<Struct>] type BillingMode = | PAY_PER_REQUEST = 0 | PROVISIONED = 1
field BillingMode.PAY_PER_REQUEST: BillingMode = 0
val lambda: name: string -> FunctionBuilder
<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>
custom operation: runtime (Runtime) Calls FunctionBuilder.Runtime
<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>
Multiple items
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
property Runtime.DOTNET_8: Runtime with get
custom operation: handler (string) Calls FunctionBuilder.Handler
<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>
custom operation: code (Code) Calls FunctionBuilder.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>
custom operation: environment ((string * string) list) Calls FunctionBuilder.Environment
<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>
namespace System
namespace System.Diagnostics

Type something to start searching.