← Back to Blog

Building a Modular Pulumi Serverless Starter

2025-10-18•7 min read•Branko Petric

Building a Modular Pulumi Serverless Starter

I've lost count of how many times I've set up the same serverless infrastructure: Lambda functions, API Gateway, DynamoDB, IAM roles, permissions. Every new project or client meant copy-pasting infrastructure code, tweaking hardcoded values, and hoping I didn't miss anything.

TL;DR

I created a production-ready Pulumi starter template that:

  • Auto-discovers Lambda functions from your filesystem
  • Supports multiple API Gateways with custom mappings
  • Makes DynamoDB completely optional
  • Requires zero code changes - everything is configured via YAML
  • Includes proper IAM roles, encryption, point-in-time recovery, and all the production best practices

šŸ‘‰ GitHub Repository

The Problem: Serverless Infrastructure is Repetitive

Every serverless project I worked on needed roughly the same stack:

  1. Lambda functions (usually multiple)
  2. API Gateway to expose them
  3. DynamoDB for state (sometimes)
  4. IAM roles with least-privilege permissions
  5. Environment-specific configurations (dev, staging, prod)
  6. Maybe a custom domain with SSL

The infrastructure code was 80% identical across projects. Only the specifics changed: function names, table schemas, API routes, etc.

But every time I started a new project, I'd either:

  • Start from scratch (waste of time)
  • Copy-paste from an old project (technical debt from day one)
  • Use a generic template that didn't fit my needs

None of these options felt right.

What I Wanted: A Smart Starter Template

I had a clear vision of what would make my life easier:

1. Auto-Discovery Drop a folder in lambda_functions/ and have it automatically deploy as a Lambda function. No manual configuration needed.

2. Flexible API Gateway Mapping Sometimes I need one API Gateway. Sometimes I need to split functions across multiple gateways (public vs. internal, for example). The template should handle both.

3. Everything Configurable Change function names, runtimes, memory, timeouts, table schemas - all via YAML config. No touching Python code.

4. Optional Resources Not every project needs DynamoDB. Not every project needs a custom domain. Make it easy to turn things on and off.

5. Production-Ready Defaults Encryption enabled. Point-in-time recovery on. Least-privilege IAM. I want secure defaults, not a playground.

The Solution: Pulumi Serverless Starter

Here's what I built.

Feature 1: Lambda Auto-Discovery

The best feature, in my opinion. Here's how it works:

Your project structure looks like this:

lambda_functions/
ā”œā”€ā”€ hello/
│   └── index.py
ā”œā”€ā”€ user/
│   └── index.py
└── auth/
    └── index.py

That's it. Drop folders in lambda_functions/, and the template automatically:

  1. Discovers them
  2. Creates Lambda functions
  3. Sets up IAM roles
  4. Configures environment variables
  5. Integrates with DynamoDB (if enabled)

No manual configuration. Zero boilerplate.

The code that makes this magic happen is in infrastructure/lambda_discovery.py:

def discover_lambda_functions(functions_directory: str) -> List[str]:
    """Discover Lambda functions by scanning the functions directory."""
    if not os.path.exists(functions_directory):
        pulumi.log.warn(f"Lambda functions directory not found: {functions_directory}")
        return []

    function_names = []
    for item in os.listdir(functions_directory):
        item_path = os.path.join(functions_directory, item)
        if os.path.isdir(item_path) and not item.startswith('.'):
            function_names.append(item)

    return sorted(function_names)

Simple, but powerful.

Feature 2: Per-Function Configuration

Sometimes you need to override defaults for specific functions. Maybe one Lambda needs more memory, or a different runtime.

Just add a function.json in that function's directory:

{
  "runtime": "python3.12",
  "handler": "app.handler",
  "memorySize": 1024,
  "timeout": 60,
  "environmentVariables": {
    "LOG_LEVEL": "DEBUG"
  }
}

The template merges this with defaults. No code changes needed.

Feature 3: Multiple API Gateway Support

This was a game-changer for me. Real-world projects often need multiple API Gateways:

  • Public API for customers
  • Internal API for admin tools
  • Partner API with different authentication

The template lets you map functions to different API Gateways via simple config:

# Deploy all functions to one API Gateway (default)
serverless-starter:apiGatewayMappings: ""

# Or split them across multiple gateways
serverless-starter:apiGatewayMappings: "public-api:hello,user;admin-api:auth,manage"

This creates two separate API Gateways:

  • public-api with hello and user functions
  • admin-api with auth and manage functions

Each gets its own URL, its own CloudWatch logs, completely isolated.

The parsing logic is clean:

def parse_api_mappings(mappings_config: str) -> Dict[str, List[str]]:
    """Parse API Gateway mappings from configuration string."""
    if not mappings_config:
        return {}

    mappings = {}
    api_groups = mappings_config.split(';')

    for group in api_groups:
        if ':' not in group:
            continue
        api_name, functions = group.split(':', 1)
        function_list = [f.strip() for f in functions.split(',')]
        mappings[api_name] = function_list

    return mappings

Feature 4: Optional DynamoDB

Not every serverless project needs a database. So I made DynamoDB completely optional:

# Enable DynamoDB (default)
serverless-starter:enableDynamoDB: true

# Disable it
serverless-starter:enableDynamoDB: false

When disabled, Lambda functions deploy without DynamoDB permissions. Clean and simple.

Feature 5: Everything is Configurable

Here's the full configuration schema in Pulumi.yaml:

config:
  # Project Configuration
  serverless-starter:projectName:
    type: string
    default: serverless-app

  # DynamoDB
  serverless-starter:enableDynamoDB:
    type: boolean
    default: true
  serverless-starter:dynamodbTableName:
    type: string
    default: app-table

  # Lambda Defaults
  serverless-starter:lambdaDefaultRuntime:
    type: string
    default: python3.11
  serverless-starter:lambdaDefaultMemorySize:
    type: integer
    default: 256

  # API Gateway
  serverless-starter:apiGatewayMappings:
    type: string
    default: ""

Everything has sensible defaults. Override only what you need in Pulumi.dev.yaml:

config:
  aws:region: us-east-1
  serverless-starter:projectName: my-awesome-api
  serverless-starter:lambdaDefaultMemorySize: 512
  serverless-starter:apiGatewayMappings: "public:hello;internal:admin"

No Python code changes. Ever.

The Architecture

The project is structured for modularity:

pulumi-serverless-starter/
ā”œā”€ā”€ __main__.py                    # Orchestration
ā”œā”€ā”€ infrastructure/
│   ā”œā”€ā”€ lambda_discovery.py        # Auto-discovery logic
│   ā”œā”€ā”€ lambda_function.py         # Lambda + IAM setup
│   ā”œā”€ā”€ api_mappings.py            # API Gateway mapping
│   ā”œā”€ā”€ api_gateway.py             # API Gateway creation
│   ā”œā”€ā”€ dynamodb.py                # DynamoDB tables
│   └── domain.py                  # Custom domains (optional)
└── lambda_functions/
    └── hello/
        └── index.py               # Your function code

Each module has a single responsibility. Want to customize Lambda creation? Edit lambda_function.py. Need different API Gateway behavior? Update api_gateway.py.

Deployment in Practice

Getting started is dead simple:

# Clone the repo
git clone https://github.com/petricbranko/pulumi-serverless-starter
cd pulumi-serverless-starter

# Install dependencies
pip install -r requirements.txt

# Configure (optional - has good defaults)
pulumi config set aws:region us-east-1
pulumi config set projectName my-api

# Deploy
pulumi up

That's it. Within 30 seconds, you have:

  • All Lambda functions deployed
  • API Gateway with working endpoints
  • DynamoDB table (if enabled)
  • Proper IAM roles and permissions
  • CloudWatch logging configured

Check your outputs:

pulumi stack output apiGateways
{
  "default": {
    "url": "https://abc123.execute-api.us-east-1.amazonaws.com/dev",
    "functions": ["hello"]
  }
}

Test it:

curl https://abc123.execute-api.us-east-1.amazonaws.com/dev
{
  "message": "Hello, World!",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "environment": "dev"
}

Real-World Use Cases

Since building this, I've used it for:

1. Client MVP Development Spin up a new serverless backend in minutes. Add functions as features evolve. No infrastructure bottlenecks.

2. Microservices Architecture Each microservice gets its own API Gateway. Clear separation, independent scaling, isolated failures.

3. Multi-Environment Deployments

pulumi stack init staging
pulumi config set projectName staging-api
pulumi up

pulumi stack init prod
pulumi config set projectName prod-api
pulumi config set lambdaDefaultMemorySize 1024
pulumi up

Each stack gets its own isolated infrastructure.

4. Learning Platform I use this to teach Pulumi and serverless architecture. Students focus on Lambda logic, not infrastructure boilerplate.

What's Next

I'm actively using and improving this template. Planned features:

  • API Gateway authorizers (Cognito, Lambda)
  • Event-driven triggers (S3, SNS, SQS)
  • Step Functions integration for workflows
  • X-Ray tracing for debugging
  • API Gateway caching configuration
  • VPC integration for private resources

Want something specific? Open an issue on GitHub.

Lessons Learned

Building this taught me a few things:

1. Auto-discovery > Manual configuration The mental overhead of registering each Lambda manually was huge. Auto-discovery eliminated that entirely.

2. Configuration schema matters Pulumi's typed configuration caught so many mistakes before deployment. Type safety isn't just for application code.

3. Defaults should be production-ready Making encryption and PITR default to "on" means security isn't an afterthought.

4. Flexibility without complexity The template handles simple cases (one API Gateway) and complex cases (multiple gateways) with the same code. That's the sweet spot.

Getting Started

The fastest way to try this:

git clone https://github.com/petricbranko/pulumi-serverless-starter
cd pulumi-serverless-starter
pip install -r requirements.txt
pulumi config set aws:region us-east-1
pulumi up

Check the README for detailed configuration options.

Wrapping Up

I built this because I was tired of repeating myself. Every serverless project shouldn't require reinventing infrastructure.

Now when I start a new project, I:

  1. Clone this repo
  2. Add my Lambda functions
  3. Tweak YAML config
  4. Deploy

Infrastructure takes minutes instead of hours. I spend time building features, not wiring up IAM policies.

If you're building serverless applications on AWS, I hope this saves you as much time as it's saved me.


Questions or feedback? Open an issue on GitHub or contribute a PR. I'd love to hear how you're using it.

Want to customize it for your team? The code is modular and well-documented. Fork it, make it yours, and share what you build.

Happy deploying! šŸš€

Subscribe for More

Get weekly DevOps tips and tutorials delivered to your inbox. No spam, ever.