Skip to content

Infrastructure

mantle generate infra reads your src/lambdas/ directory and mantle.config.ts and writes OpenTofu (.tf) files to infra/.

Final result

bash
mantle generate infra
# Creates: main.tf, variables.tf, outputs.tf, api_routes.tf
#          lambda_<name>.tf per handler
#          storage_<name>.tf, database.tf, eventbridge.tf (if configured)

Step 1: Configure your project

Declare infrastructure in mantle.config.ts:

typescript
import { defineConfig } from "@j0nathan-ll0yd/core";

export default defineConfig({
  name: "my-api",
  database: { provider: "aurora-dsql" },
  eventbridge: { bus: "my-api-events" },
  storage: [{ name: "uploads", cloudfront: true }],
  queues: [{ name: "order-processing", visibilityTimeoutSeconds: 60 }],
});

Step 2: Structure your Lambda files

Lambda type is determined by which subdirectory under src/lambdas/ the file lives in:

src/lambdas/
  api/           # API Gateway routed (filename = <name>.<method>.ts)
  eventbridge/   # EventBridge-triggered
  scheduled/     # CloudWatch scheduled
  standalone/    # Direct invocation
  sqs/           # SQS consumer
  s3/            # S3 event-triggered
  websocket/     # WebSocket API Gateway

API handler filenames determine routes and HTTP methods:

FileRouteMethodLambda name
api/items/index.get.ts/itemsGETItemsGet
api/items/index.post.ts/itemsPOSTItemsPost
api/items/[id]/index.delete.ts/items/{id}DELETEItemsIdDelete
api/ping.get.ts/pingGETPing

Use index.<method>.ts when the handler responds at the directory path itself.

Step 3: Run the generator

bash
mantle generate infra

Generated files include a header:

hcl
# This file is generated by Mantle CLI
# To customize, remove the first line and this file will be skipped on re-generation.

To eject a file (take manual control), remove the first header line. The generator skips ejected files on subsequent runs.

Module structure

All modules accept name_prefix and tags from the core module:

hcl
module "core" {
  source       = "../node_modules/@j0nathan-ll0yd/cli/modules/core"
  project_name = "my-api"
  environment  = var.environment
}

module "api" {
  source      = "../node_modules/@j0nathan-ll0yd/cli/modules/api-gateway"
  name_prefix = module.core.name_prefix
  tags        = module.core.common_tags
}

module "lambda_get_items" {
  source             = "../node_modules/@j0nathan-ll0yd/cli/modules/lambda"
  name_prefix        = module.core.name_prefix
  function_name      = "GetItems"
  region             = module.core.region
  account_id         = module.core.account_id
  assume_role_policy = module.core.lambda_assume_role_policy
  xray_policy_arn    = module.core.lambda_xray_policy_arn
  tags               = module.core.common_tags
}

Available modules:

ModulePurpose
modules/coreNaming prefix, common tags, IAM assume-role policies, X-Ray policy
modules/lambdaLambda function, IAM role, log group, optional triggers
modules/api-gatewayREST API with stages, authorizer, usage plans
modules/queueSQS queue + DLQ + CloudWatch alarm
modules/storageS3 bucket with encryption, versioning, optional CloudFront
modules/idempotencyDynamoDB table for Lambda Powertools idempotency
modules/observabilityCloudWatch dashboards, alarms, SNS topics
modules/eventbridgeEventBridge custom event bus
modules/dynamodbDynamoDB table with GSIs, TTL, PITR
modules/websocket-apiWebSocket API Gateway
modules/database/aurora-dsqlAurora DSQL cluster

Resources follow the naming convention ${environment}-${project_name}-ResourceName (PascalCase for logical resources, kebab-case for S3/SQS).

Storage edge functions

A storage bucket with cloudfront: true can attach a viewer-request CloudFront Function backed by a CloudFront KeyValueStore — runtime-togglable request gating (feature flags, privacy gates, maintenance modes) that takes effect within seconds, before the cache:

typescript
storage: [{
  name: 'data',
  cloudfront: true,
  edgeFunction: {
    source: 'src/edge/focus-gate.js',          // cloudfront-js-2.0, relative to project root
    seedKeys: [{ key: 'hiding', value: '0' }], // provisioned once; runtime writes win afterwards
  },
}],

The generator emits the function (cloudfront-js-2.0, published), the KeyValueStore, the viewer-request association on the bucket's distribution, and the seeded keys. Seeded key values are drift-ignored, so a Lambda toggling a flag at runtime is never reverted by a later apply. Lambdas that write flags (via @aws-sdk/client-cloudfront-keyvaluestore) get a scoped CloudFrontKvsWrite inline policy automatically; expose the store ARN with a *_KVS_ARN env var (see Resource Binding). Note the KVS data plane requires SigV4A request signing.

Environment variable auto-wiring

The generator scans handler files for getRequiredEnv() / getOptionalEnv() calls and:

  1. Wires standard env vars (API_BEARER_TOKEN, DSQL_ENDPOINT, etc.) from project config
  2. Creates Terraform variables for non-standard env vars
  3. Resolves *_BUCKET and *_FUNCTION_NAME patterns to module outputs instead of variables
hcl
# Auto-generated for getOptionalEnv('LOCATIONIQ_API_KEY', '')
variable "locationiq_api_key" {
  description = "LOCATIONIQ_API_KEY environment variable"
  type        = string
  sensitive   = true
  default     = ""
}

Variables containing key, token, or secret are automatically marked sensitive = true.

*_BUCKET env vars also generate an inline S3Access IAM policy. *_FUNCTION_NAME env vars generate an Invoke<Name> IAM policy.

Lambda configuration

For EventBridge and scheduled handlers, pass config directly to defineEventBridgeHandler / defineScheduledHandler. Use defineLambda() only for standalone handlers:

typescript
import { defineLambda } from "@j0nathan-ll0yd/core";

defineLambda({
  timeout: 60,
  memorySize: 256,
  deadLetterQueue: true,
  retryAttempts: 2,
  bind: { DATA_BUCKET: "files" },
});
OptionTypeDefaultDescription
timeoutnumberTimeout in seconds
memorySizenumber128Memory in MB
reservedConcurrencynumberReserved concurrent executions
ephemeralStoragenumber512Ephemeral storage in MB
deadLetterQueuebooleantrue = auto-generate SQS DLQ. See Dead Letter Queues.
retryAttemptsnumberMax retry attempts (0–2)
bindRecord<string, string>Map env var names to config resource names. See Resource Binding.

Ejected files

If you eject main.tf and add new Lambdas, manually add their invoke_arn to the redeployment_trigger hash — the generator silently skips ejected files.

Path parameter directories ([id]) become {id} in the API Gateway path_part and have brackets stripped from Terraform resource identifiers.

Next steps