Building a Modular Pulumi Serverless Starter
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:
- Lambda functions (usually multiple)
- API Gateway to expose them
- DynamoDB for state (sometimes)
- IAM roles with least-privilege permissions
- Environment-specific configurations (dev, staging, prod)
- 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:
- Discovers them
- Creates Lambda functions
- Sets up IAM roles
- Configures environment variables
- 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-apiwithhelloanduserfunctionsadmin-apiwithauthandmanagefunctions
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:
- Clone this repo
- Add my Lambda functions
- Tweak YAML config
- 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.