Production-grade, modular, scalable Terraform codebase deploying a serverless processing pipeline across isolated AWS Accounts (dev, uat, prod).
+-------------------------------------------------------------+
| Target AWS Account |
| |
| +-----------------------------------------------------+ |
| | VPC | |
| | | |
| | +------------------------+ +-------------------+ | |
| | | Private Subnet A & B | | Public Subnet | | |
| | | | | | | |
| | | +----------------+ | | +-------------+ | | |
| | | | Lambda Function|---|--|->| NAT Gateway |--|--|--> External APIs
| | | +----------------+ | | +-------------+ | | (HTTPS / Port 443)
| | | | | | +-------------------+ | |
| | | | +--------------------------+ | |
| | | | Port 3306 | | |
| | | v v | |
| | | +----------------+ +-------------+ |
| | | | Existing MySQL | | SNS Topic | |
| | | | Database | | (Events) | |
| | | +----------------+ +-------------+ |
| | +------------------------+ ^ | |
| +-------------------------------------------------|-------+ |
| | |
| +-------------------------+ | |
| | AWS Secrets Manager | | |
| | (MySQL Credentials) |<----------------------+ |
| +-------------------------+ |
+-----------------------------------------------------------------+
- AWS Lambda:
- Attached to existing VPC private subnets.
- Egress rules: Port 443 to
0.0.0.0/0(via existing NAT Gateway for outbound API communication) and Port 3306 to the existing MySQL Database Security Group. - Automated ingress rule injection on existing DB security group to accept traffic from Lambda SG.
- Least-privilege IAM execution role (
AWSLambdaVPCAccessExecutionRole, scoped CloudWatch logs, scoped Secrets Manager read, scoped SNS publish).
- AWS SNS Topic:
- Encrypted topic (AWS KMS) for publishing service events.
- Resource policy restricting
sns:Publishto the Lambda execution role.
- Existing Resources (Inputs):
- VPC ID, Private Subnets, existing DB Security Group, Secrets Manager Secret ARN.
serverless-infrastructure/
├── environment/
│ ├── dev/
│ │ ├── backend.hcl
│ │ ├── env.conf
│ │ ├── parameters.tfvars
│ │ └── terraform.tfvars
│ ├── uat/
│ │ ├── backend.hcl
│ │ ├── env.conf
│ │ ├── parameters.tfvars
│ │ └── terraform.tfvars
│ └── prod/
│ ├── backend.hcl
│ ├── env.conf
│ ├── parameters.tfvars
│ └── terraform.tfvars
├── modules/
│ ├── compute/lambda/
│ ├── events/{eventbridge,sns}/
│ └── storage/{s3,ssm-parameter-store}/
├── policies/iam/
└── scripts/terraform.sh
Each environment (dev, uat, prod) is completely segregated into its own folder. This guarantees:
- Isolated State: Dev operations can never corrupt UAT or Production state.
- Distinct IAM & Permissions: Deployments target distinct AWS Account IDs with separate AWS profiles or assumed roles.
- Environment Parity: All environments leverage identical modules (
modules/sns,modules/lambda) while tuning sizing parameters (memory, timeout, retention) via.tfvars.
Before a first deployment, replace every REPLACE_WITH_* value in the selected environment's terraform.tfvars and env.conf. The VPC, private subnet, database security-group, and account ID are deliberately not inferred or committed.
The Lambda is deployed as a Node.js 24 container image, not a ZIP archive. CI/CD must publish an image before the first Terraform apply:
# Build and publish image from service application repository:
cd ../service
scripts/build-and-push-image.sh latest
# Subsequent CI/CD release: push and update the existing Lambda
scripts/build-and-push-image.sh "$GIT_SHA" serviceThe script builds a Lambda-compatible container image from the application repository and pushes it to the ECR repository. If passed a function name, it also updates that Lambda to the pushed image. For release deployments, use an immutable tag (for example, a Git commit SHA) and update the environment's image_uri accordingly before the initial Terraform creation.
Authenticate to the target AWS account before running commands:
# For Dev Account
export AWS_PROFILE=<environment-profile>
# Or specify temporary credentials:
# export AWS_ACCESS_KEY_ID=...
# export AWS_SECRET_ACCESS_KEY=...
# export AWS_SESSION_TOKEN=...Configure the S3 backend in each environment's backend.hcl file:
terraform {
backend "s3" {
bucket = "terraform-state-<env>-<account-id>"
key = "serverless-infrastructure/<env>/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-state-lock-<env>"
encrypt = true
}
}# Formats code across modules
make fmt
# Dev
make init ENV=dev
make dev-plan # or make plan ENV=dev
make dev-apply # or make apply ENV=dev
# UAT
make init ENV=uat
make uat-plan
make uat-apply
# Prod
make init ENV=prod
make prod-plan
make prod-applyterraform init -backend-config=environment/dev/backend.hcl
terraform plan \
-var-file=environment/dev/terraform.tfvars \
-var-file=environment/dev/parameters.tfvars
terraform apply \
-var-file=environment/dev/terraform.tfvars \
-var-file=environment/dev/parameters.tfvars- Network Isolation: Lambda placed strictly in private subnets with no public IP.
- Strict Egress:
- Port
443outbound to0.0.0.0/0routed through existing NAT Gateway. - Port
3306outbound targeted only to existing DB SG.
- Port
- Least-Privilege IAM:
- No wildcard
*permissions on sensitive actions. - Secrets Manager read scoped strictly to DB secret ARN.
- SNS publish scoped strictly to created topic ARN.
- CloudWatch logs scoped to the specific function log group.
- No wildcard
- SNS Encryption: Server-side encryption enabled via AWS KMS.
- Resource Tagging: Default provider tags ensure cost tracking and auditing across all created resources.