βοΈ AWS Serverless Blog Platform (www.sblog.stsproj.com)
Two frontends. Two APIs. Strict security boundaries. Event-driven by default. 100% Infrastructure as Code.
- Project Overview
- Architecture Overview
- AWS Services Used
- Lambda Functions
- API Design β API Gateway
- Database Design β DynamoDB
- Authentication β Amazon Cognito
- Event-Driven Architecture β EventBridge
- Secure Media Handling β Presigned URLs
- CI/CD Pipeline
- Observability & Monitoring
- Security Design
- Infrastructure as Code β Terraform
- Frontend Applications
- Project Structure
- Key Skills Demonstrated
- Author
This is a production-style serverless blog platform built end-to-end on AWS β from compute and storage to CI/CD and observability. The goal was to design a real-world cloud system that solves the common pitfalls of hobby AWS projects: mixed authentication levels, publicly accessible storage buckets, synchronous blocking for async work, and manually configured resources.
| Common Demo Project Problem | How this project solves it |
|---|---|
| Admin and public APIs share the same Gateway | Two completely separate API Gateways with different trust levels |
| S3 buckets are publicly accessible | All buckets private β media served exclusively via CloudFront OAC |
| Security is bolted on | Cognito authorizer is enforced at the API Gateway layer, not in Lambda code |
| Async work (emails, cleanup) blocks user responses | Fully decoupled via EventBridge β users get instant responses |
| Single IAM role shared by all functions | 16 granular per-function IAM policies (least privilege by design) |
| Infrastructure manually created in the Console | 100% Terraform β every resource is codified and version-controlled |
| No observability | X-Ray tracing, CloudWatch alarms, structured logging, and a unified dashboard |
| Deployments are risky | Canary deployments via CodeDeploy with automatic rollback on alarm |
The platform is split into three clearly separated zones. Failure in one zone does not cascade to the others.
flowchart TD
classDef userStyle fill:#f97316,stroke:#c2410c,color:#fff
classDef publicStyle fill:#2563eb,stroke:#1d4ed8,color:#fff
classDef adminStyle fill:#16a34a,stroke:#15803d,color:#fff
classDef asyncStyle fill:#7c3aed,stroke:#6d28d9,color:#fff
classDef dbStyle fill:#475569,stroke:#334155,color:#fff
classDef busStyle fill:#d97706,stroke:#b45309,color:#fff
classDef alarmStyle fill:#dc2626,stroke:#b91c1c,color:#fff
subgraph PUBLIC["π Public Zone"]
PB(["π€ User"]):::userStyle --> PCF["βοΈ CloudFront"]:::publicStyle
PCF --> PS3["ποΈ S3 Β· React SPA"]:::publicStyle
PCF --> PAGW["π API Gateway Β· No Auth"]:::publicStyle
PAGW --> PL["β‘ Lambda Β· Read Only"]:::publicStyle
PL --> DDB[("ποΈ DynamoDB")]:::dbStyle
end
subgraph ADMIN["π Admin Zone"]
AB(["π€ Admin"]):::userStyle --> ACF["βοΈ CloudFront"]:::adminStyle
ACF --> AS3["ποΈ S3 Β· React CMS"]:::adminStyle
ACF --> AAGW["π‘οΈ API Gateway Β· Cognito JWT"]:::adminStyle
AAGW --> AL["β‘ Lambda Β· Full CRUD"]:::adminStyle
AL --> DDB2[("ποΈ DynamoDB")]:::dbStyle
AL --> EB(["π EventBridge Bus"]):::busStyle
end
subgraph ASYNC["β‘ Async Zone"]
NL["π Notifications Lambda"]:::asyncStyle --> SES["π§ SES Β· Email"]:::asyncStyle
CL["π§Ή Cleanup Lambda"]:::asyncStyle --> S3M["ποΈ S3 Β· Delete Media"]:::asyncStyle
NL -->|retries exhausted| DLQ1["β οΈ SQS DLQ"]:::alarmStyle
CL -->|retries exhausted| DLQ2["β οΈ SQS DLQ"]:::alarmStyle
DLQ1 & DLQ2 --> CWA["π¨ CloudWatch Alarm"]:::alarmStyle
end
EB -->|LeadCreated| NL
EB -->|PostDeleted| CL
style PUBLIC fill:#eff6ff,stroke:#2563eb,color:#1e3a5f
style ADMIN fill:#f0fdf4,stroke:#16a34a,color:#14532d
style ASYNC fill:#f5f3ff,stroke:#7c3aed,color:#3b0764
| Service | Role in this Project |
|---|---|
| AWS Lambda | All backend compute β 6 single-purpose functions + 1 shared layer |
| Amazon API Gateway | REST API layer β 2 separate gateways (public and admin) with throttling |
| Amazon DynamoDB | Primary database β 2 tables, on-demand billing, GSIs for efficient querying |
| Amazon S3 | 3 private buckets: public frontend, admin frontend, and media storage |
| Amazon CloudFront | 2 CDN distributions β path-based routing, TLS termination, OAC to S3 |
| Amazon Cognito | Admin authentication β User Pool as a native API Gateway authorizer |
| Amazon EventBridge | Custom event bus β async decoupling of API writes from side effects |
| Amazon SES | Transactional email β sends lead notification emails to admin |
| Amazon SQS | 3 Dead Letter Queues β captures and alerts on failed async operations |
| AWS CodePipeline | 3 separate CI/CD pipelines (backend, admin frontend, public frontend) |
| AWS CodeBuild | Builds Lambda packages and Vite SPAs, reads config from SSM |
| AWS CodeDeploy | Canary Lambda deployments β 10% traffic for 5 min, auto-rollback |
| AWS X-Ray | Distributed tracing across Lambda, API Gateway, and DynamoDB |
| Amazon CloudWatch | Structured logs, custom alarms, and a unified observability dashboard |
| AWS IAM | 16 custom per-function least-privilege policies |
| AWS KMS | Encryption of CodePipeline build artifacts |
| AWS SSM Parameter Store | Build-time and runtime configuration injection |
| Amazon SNS | Alarm notification topic β emails on CloudWatch alarm triggers |
| Amazon Route 53 | DNS routing to CloudFront distributions |
Six single-purpose Lambda functions β each with its own IAM role, log group, and alarm set. No god functions.
| Function | Trigger | Responsibility | Services Accessed |
|---|---|---|---|
admin_blog_posts |
API Gateway (Admin) | Full CRUD + lifecycle (publish, archive, delete) on posts | DynamoDB (R/W), EventBridge |
public_posts_lambda |
API Gateway (Public) | Read-only access to published posts | DynamoDB (Query via GSI) |
leads_lambda |
API Gateway (Both) | Create leads (public) Β· Read leads (admin) | DynamoDB (R/W), EventBridge |
presign_lambda |
API Gateway (Admin) | Generate time-limited S3 upload URLs | S3 (PutObject only) |
notifications_lambda |
EventBridge | Send lead notification email | SES |
cleanup_lambda |
EventBridge | Delete S3 media when a post is deleted | S3 (DeleteObject only) |
Shared Lambda Layer: All functions use a common layer containing the AWS SDK and X-Ray SDK, reducing deployment package sizes and keeping dependencies consistent.
Runtime: Node.js 18.x
Tracing: AWS X-Ray (active mode)
Log retention: 7 days per function
Aliases: live (production traffic) Β· beta (canary target)
Blast radius design: If the Notifications Lambda fails, it has zero impact on the blog API or admin CMS. Each function fails independently.
Two completely separate REST APIs serve two completely different audiences.
PUBLIC API GATEWAY ADMIN API GATEWAY
βββββββββββββββββ βββββββββββββββββ
No authentication Cognito User Pool Authorizer
GET /posts GET /admin/posts
GET /posts/{postId} POST /admin/posts
POST /leads PUT /admin/posts/{id}
DELETE /admin/posts/{id}
POST /admin/posts/{id}/publish
POST /admin/posts/{id}/unpublish
POST /admin/posts/{id}/archive
POST /admin/media/upload_url
GET /admin/leads
CloudFront as the single entry point:
All traffic enters through CloudFront distributions. A CloudFront Function rewrites path prefixes (/api/* β /*) before forwarding to API Gateway, so both frontends use the same domain for API and static assets β eliminating CORS entirely.
Posts Table
| Attribute | Type | Role |
|---|---|---|
postId |
String (PK) | Primary key |
status |
String | DRAFT / PUBLISHED / ARCHIVED |
publishedAt |
String | ISO timestamp used for sort |
authorId |
String | Author reference |
title, content, tags |
Various | Post content fields |
Global Secondary Indexes:
| GSI Name | Partition Key | Sort Key | Query Use Case |
|---|---|---|---|
publishedAtIndex |
status |
publishedAt |
Public blog β list all published posts in date order |
authorIDIndex |
authorId |
createdAt |
Admin view β list posts by author |
CQRS (Lite) Approach:
The public Lambda only has Query permission on the GSI. The admin Lambda has full CRUD. The same table serves both read and write paths β but IAM enforces the segregation so the public function physically cannot write to the table.
Leads Table
Stores lead submissions from the public blog. Admin Lambda can read all leads. Only leads Lambda can write.
Amazon Cognito User Pool provides JWT-based authentication for the admin CMS.
sequenceDiagram
actor Admin as π€ Admin Browser
participant Amplify as π± AWS Amplify
participant Cognito as π Amazon Cognito
participant APIGW as π API Gateway
participant Lambda as β‘ Lambda
Admin->>Amplify: Enter credentials
Amplify->>Cognito: Authenticate
Cognito-->>Amplify: β
JWT ID Token
Amplify-->>Admin: Signed in
Admin->>APIGW: API request + Authorization Bearer JWT
APIGW->>Cognito: Validate token
alt β
Valid Token
Cognito-->>APIGW: Authorized
APIGW->>Lambda: Invoke
Lambda-->>Admin: 200 OK
else β Invalid or Missing Token
Cognito-->>APIGW: Unauthorized
APIGW-->>Admin: 401 β Lambda never invoked
end
Key design decisions:
- Admin-create-only β no self-registration. Users must be provisioned by an admin.
- Authorization at the infrastructure layer β Cognito is enforced at API Gateway, not in Lambda code. Lambda never receives an unauthenticated request.
- Public API has zero Cognito dependency β public users are never impacted by auth service issues.
A custom EventBridge bus decouples API handlers from their side effects.
flowchart TD
classDef userStyle fill:#6e2f1a,stroke:#e59866,color:#fff
classDef lambdaStyle fill:#145a32,stroke:#27ae60,color:#fff,rx:6
classDef dbStyle fill:#1b2631,stroke:#717d7e,color:#fff,rx:6
classDef busStyle fill:#784212,stroke:#f39c12,color:#fff
classDef asyncStyle fill:#4a235a,stroke:#9b59b6,color:#fff,rx:6
classDef dlqStyle fill:#7b241c,stroke:#e74c3c,color:#fff,rx:6
U(["π€ User"]):::userStyle -->|POST /leads| LL["β‘ Leads Lambda"]:::lambdaStyle
A(["π Admin"]):::userStyle -->|DELETE /admin/posts/:id| AL["β‘ Admin Lambda"]:::lambdaStyle
LL --> DDB1[("ποΈ DynamoDB<br/>Write Lead")]:::dbStyle
LL -->|"β
instant 201"| U
LL -->|"π€ emit LeadCreated"| EB(["π EventBridge Bus"]):::busStyle
AL --> DDB2[("ποΈ DynamoDB<br/>Delete Post")]:::dbStyle
AL -->|"β
instant 200"| A
AL -->|"π€ emit PostDeleted"| EB
EB -->|LeadCreated rule| NL["π Notifications Lambda"]:::asyncStyle
NL --> SES["π§ SES Β· Email to Admin"]:::asyncStyle
NL -->|fail after 10 retries| DLQ1["β οΈ SQS DLQ"]:::dlqStyle
EB -->|PostDeleted rule| CL["π§Ή Cleanup Lambda"]:::asyncStyle
CL --> S3M["ποΈ S3 Β· Delete Media"]:::dbStyle
CL -->|fail after 10 retries| DLQ2["β οΈ SQS DLQ"]:::dlqStyle
DLQ1 --> CWA["π¨ CloudWatch Alarm"]:::dlqStyle
DLQ2 --> CWA
Why this matters for the user: The user gets an instant API response. The email notification or media cleanup happens behind the scenes without adding to their response time.
Resilience: Each EventBridge rule targets the Lambda with a retry policy (up to 10 retries). Failed events after exhausting retries are sent to an SQS Dead Letter Queue, which triggers a CloudWatch alarm.
File uploads bypass Lambda entirely using the Valet Key pattern.
sequenceDiagram
actor Browser as π₯οΈ Browser
participant APIGW as π API Gateway Admin
participant Cognito as π Cognito Authorizer
participant Lambda as β‘ Presign Lambda
participant S3 as ποΈ S3 Media Bucket
participant CF as βοΈ CloudFront CDN
Note over Browser,CF: π Phase 1 β Request Upload Permission
Browser->>APIGW: POST /admin/media/upload_url
APIGW->>Cognito: Validate JWT
Cognito-->>APIGW: β
Authorized
APIGW->>Lambda: Invoke
Lambda->>Lambda: π Validate content-type (image/jpeg Β· image/png)
Lambda->>S3: Generate presigned PUT URL Β· 5 min TTL
S3-->>Lambda: Presigned URL
Lambda-->>Browser: presigned_url in response body
Note over Browser,CF: π€ Phase 2 β Direct Upload (No Lambda Β· No API Gateway)
Browser->>S3: PUT presigned URL with file bytes
S3-->>Browser: β
200 OK
Note over Browser,CF: πΌοΈ Phase 3 β Serve via CDN
Browser->>CF: GET /media/image.jpg
CF->>S3: Fetch via OAC (private bucket)
S3-->>CF: Image bytes
CF-->>Browser: β‘ Cached image response
Security note: The S3 media bucket has no public access. The presign Lambda validates allowed content-types before generating the URL β preventing malicious file types from being hosted.
Three separate CodePipeline pipelines ensure that a change to the public frontend never triggers a backend deployment.
flowchart TD
classDef sourceStyle fill:#1a4f7a,stroke:#2e86c1,color:#fff,rx:6
classDef buildStyle fill:#145a32,stroke:#27ae60,color:#fff,rx:6
classDef deployStyle fill:#4a235a,stroke:#9b59b6,color:#fff,rx:6
classDef successStyle fill:#1d6a27,stroke:#2ecc71,color:#fff,rx:6
classDef rollbackStyle fill:#7b241c,stroke:#e74c3c,color:#fff,rx:6
GH["π¦ GitHub<br/>Source Code"]:::sourceStyle --> BE & AF & PF
BE["π§ Backend Pipeline"]:::sourceStyle --> CB_BE["ποΈ CodeBuild<br/>Package Lambda ZIPs"]:::buildStyle
CB_BE --> CD["π CodeDeploy<br/>Canary 10% Β· 5 min"]:::deployStyle
CD -->|"β
healthy"| LIVE["β¨ Lambda live alias<br/>100% v2"]:::successStyle
CD -->|"π¨ alarm fires"| RB["β©οΈ Auto Rollback<br/>to v1"]:::rollbackStyle
AF["π₯οΈ Admin FE Pipeline"]:::sourceStyle --> CB_AF["ποΈ CodeBuild<br/>npm build + SSM env inject"]:::buildStyle
CB_AF --> S3_AF["βοΈ S3 Sync +<br/>CloudFront Invalidate"]:::deployStyle
PF["π Public FE Pipeline"]:::sourceStyle --> CB_PF["ποΈ CodeBuild<br/>npm build + SSM env inject"]:::buildStyle
CB_PF --> S3_PF["βοΈ S3 Sync +<br/>CloudFront Invalidate"]:::deployStyle
CodeDeploy shifts 10% of Lambda traffic to the new version for 5 minutes before completing the rollout. If a CloudWatch alarm fires during that window, traffic is automatically rolled back to the previous version β no manual intervention needed.
flowchart TD
classDef deployStyle fill:#1a4f7a,stroke:#2e86c1,color:#fff,rx:6
classDef watchStyle fill:#784212,stroke:#f39c12,color:#fff,rx:6
classDef successStyle fill:#145a32,stroke:#27ae60,color:#fff,rx:6
classDef rollbackStyle fill:#7b241c,stroke:#e74c3c,color:#fff,rx:6
classDef decisionStyle fill:#17202a,stroke:#717d7e,color:#fff
D["π New Lambda Version v2<br/>deployed"]:::deployStyle --> S["π Traffic Shift<br/>90% v1 Β· 10% v2"]:::deployStyle
S --> W["β±οΈ 5 Minute<br/>Watch Window"]:::watchStyle
W --> M["π CloudWatch<br/>Monitors error alarms"]:::watchStyle
M --> OK{"π Alarm<br/>fired?"}:::decisionStyle
OK -->|"β
No alarms"| FULL["π 100% traffic to v2<br/>Deployment complete"]:::successStyle
OK -->|"π¨ Alarm fired"| RB["β©οΈ Automatic rollback<br/>100% back to v1"]:::rollbackStyle
CodeBuild reads environment config from SSM Parameter Store and injects Terraform-generated values (API URLs, Cognito IDs, CloudFront domains) as Vite environment variables at build time β no hardcoded values, no manual copy-pasting between services.
The three pillars of observability are all implemented.
Every Lambda function and API Gateway stage is instrumented with X-Ray. A single user request produces a trace spanning CloudFront β API Gateway β Lambda β DynamoDB, with subsegments for each service call.
All Lambda functions emit structured JSON logs with a correlationId, so a single request can be traced across multiple log groups.
| Alarm | Metric | Threshold | Action |
|---|---|---|---|
| Lambda error rate | Errors per function |
> 1 per 5 min | SNS β email |
| API Gateway 5xx errors | 5XXError |
> 1 per 5 min | SNS β email |
| DLQ message count | ApproximateNumberOfMessages |
> 0 | SNS β email |
| DynamoDB throttles | ThrottledRequests |
> 1 | SNS β email |
| CloudFront 5xx rate | 5xxErrorRate |
> 5% | SNS β email |
A unified CloudWatch Dashboard provides a single-pane-of-glass view across all Lambda functions, API Gateways, DynamoDB tables, and queues.
Security is layered across every tier β not bolted on after the fact.
Each Lambda function has exactly the permissions it needs. Nothing more.
| DynamoDB Posts | DynamoDB Leads | S3 Write | S3 Delete | EventBridge | SES | |
|---|---|---|---|---|---|---|
admin_blog_posts |
β CRUD | β Publish | ||||
public_posts_lambda |
β Query only | |||||
leads_lambda |
β CRUD | β Publish | ||||
presign_lambda |
β PutObject | |||||
notifications_lambda |
β SendEmail | |||||
cleanup_lambda |
β DeleteObject |
16 custom IAM policies β each scoped to a specific resource ARN. No wildcards. No shared roles.
flowchart TD
classDef layer1Style fill:#1a4f7a,stroke:#2e86c1,color:#fff,rx:6
classDef layer2Style fill:#145a32,stroke:#27ae60,color:#fff,rx:6
classDef layer3Style fill:#7b241c,stroke:#e74c3c,color:#fff,rx:6
classDef layer4Style fill:#4a235a,stroke:#9b59b6,color:#fff,rx:6
classDef layer5Style fill:#7d6608,stroke:#f1c40f,color:#000,rx:6
classDef endpointStyle fill:#1b2631,stroke:#717d7e,color:#fff
R(["π Incoming Request"]):::endpointStyle
R --> L1["π Layer 1 Β· Network Edge<br/>CloudFront HTTPS only Β· TLS 1.2<br/>No direct S3 or Lambda URLs"]:::layer1Style
L1 --> L2["π Layer 2 Β· Authentication<br/>Cognito JWT at API Gateway<br/>for all admin routes"]:::layer2Style
L2 --> L3["π‘οΈ Layer 3 Β· Authorization<br/>16 per-Lambda IAM policies<br/>Public Lambda cannot write"]:::layer3Style
L3 --> L4["π Layer 4 Β· Data<br/>S3 Block Public Access Β· CloudFront OAC<br/>DynamoDB encryption at rest"]:::layer4Style
L4 --> L5["β
Layer 5 Β· Application<br/>Content-type validation Β· Input validation<br/>No self-registration"]:::layer5Style
L5 --> RES(["β¨ Request Processed<br/>Securely"]):::endpointStyle
The entire platform is defined in Terraform. Zero manual console configuration.
| Terraform File | AWS Resources Created |
|---|---|
lambda.tf |
6 Lambda functions, 1 shared layer, aliases, log groups |
api_gateway.tf |
2 API Gateways, stages, methods, Cognito authorizer |
database.tf |
2 DynamoDB tables with GSIs, encryption, contributor insights |
s3_buckets.tf |
3 S3 buckets with lifecycle rules, versioning, CORS, policies |
cloudfront.tf |
2 CloudFront distributions, OAC, custom cache behaviours |
auth.tf |
Cognito User Pool and App Client |
eventbridge.tf |
Custom event bus, rules, targets, retry policies, DLQ targets |
lambda.tf |
SQS Dead Letter Queues |
ci_cd_*.tf |
3 CodePipeline pipelines, CodeBuild projects, CodeDeploy groups |
cloudwatch_dashboard.tf |
Dashboard, alarms, SNS topic |
iam/ |
16 custom IAM policies with resource-level scoping |
ssm.tf |
8 SSM parameters for build-time config injection |
ses.tf |
SES verified email identity |
route53.tf |
DNS records pointing to CloudFront |
Terraform version: ~> 1.14
AWS Provider: ~> 5.0
Region: eu-west-1 (Ireland)
Resource prefix: sblg- (e.g. sblg-posts, sblg-media-bucket)
A full-featured content management system for managing posts, media, and leads.
| Library | Purpose |
|---|---|
| React 19 + TypeScript | UI framework with type safety |
| TanStack Query v5 | Server state management and caching |
| React Hook Form + Zod | Form handling with schema validation |
| AWS Amplify v6 | Cognito authentication integration |
| TipTap v3 | Rich text editor for post content |
| Tailwind CSS v4 + Radix UI | Accessible, utility-first UI |
| Zustand | Lightweight client state management |
An intentionally lightweight, read-only blog viewer.
| Library | Purpose |
|---|---|
| React 19 + TypeScript | UI framework |
| TanStack Query v5 | API data fetching with caching |
| Axios | HTTP client |
Design decision: The public frontend has no auth library, no rich text editor, no form framework β it only reads and displays content. Keeping it lean reduces bundle size and attack surface.
AWS_SERVERLESS_BLOG/
β
βββ backend/ # Lambda function source code (Node.js)
β βββ admin_blog_post/ # Posts CRUD + lifecycle operations
β βββ public_posts_lambda/ # Read-only published post access
β βββ leads_lambda/ # Lead creation + admin reads
β βββ presign_lambda/ # S3 presigned URL generation
β βββ notifications_lambda/ # SES email on new lead
β βββ cleanup_lambda/ # S3 media cleanup on post deletion
β βββ blog_lambda_layer/ # Shared Layer (AWS SDK + X-Ray SDK)
β
βββ infrastructure/ # All AWS infrastructure as Terraform
β βββ main.tf # Provider config
β βββ lambda.tf # Lambda functions + aliases + layers
β βββ api_gateway.tf # REST APIs + Cognito authorizer
β βββ database.tf # DynamoDB tables + GSIs
β βββ s3_buckets.tf # S3 buckets + bucket policies
β βββ cloudfront.tf # CDN distributions
β βββ auth.tf # Cognito User Pool
β βββ eventbridge.tf # Event bus + rules + DLQs
β βββ ci_cd_*.tf # 3 CI/CD pipelines
β βββ cloudwatch_dashboard.tf # Dashboard + alarms
β βββ ssm.tf # Parameter Store entries
β βββ iam/policies/ # 16 granular IAM policy documents
β
βββ admin-frontend/ # React + Vite admin CMS (TypeScript)
β βββ src/
β βββ api/ # Axios API client functions
β βββ components/ # Reusable UI components
β βββ pages/ # Route-level page components
β βββ hooks/ # Custom React hooks
β βββ store/ # Zustand auth store
β
βββ public-frontend/ # React + Vite public blog (TypeScript)
β βββ src/
β βββ api/ # Public API client functions
β βββ components/ # Blog UI components
β βββ pages/ # Route-level page components
β
βββ buildspec.backend.yml # CodeBuild spec β Lambda packaging + deploy
βββ buildspec.admin-frontend.yml # CodeBuild spec β Admin SPA build + S3 sync
βββ buildspec.public-frontend.yml # CodeBuild spec β Public SPA build + S3 sync
This project was built to showcase practical, production-relevant AWS skills. Here's what each section of the system demonstrates:
| Skill Area | Demonstrated By |
|---|---|
| Serverless architecture | 6 Lambda functions, API Gateway, DynamoDB β zero servers to manage |
| AWS security best practices | 16 least-privilege IAM policies, Cognito auth, private S3 + CloudFront OAC |
| Event-driven design | EventBridge custom bus, pub/sub pattern, decoupled async workflows |
| Infrastructure as Code | 100% Terraform β no manual console changes, all resources version-controlled |
| CI/CD on AWS | 3 CodePipeline pipelines with CodeBuild, CodeDeploy canary deployments |
| Observability | X-Ray distributed tracing, structured CloudWatch logging, custom alarms |
| DynamoDB data modelling | GSI design for efficient read patterns, CQRS-lite via IAM separation |
| Resilience patterns | SQS Dead Letter Queues, retry policies, canary rollback, bulkhead isolation |
| CDN & static hosting | CloudFront with path-based routing, S3 SPA hosting, TLS termination |
| Secure file handling | Presigned URLs (Valet Key pattern), content-type validation |
| Configuration management | SSM Parameter Store for build-time config injection via CodeBuild |
| Cost optimisation | On-demand DynamoDB, S3 lifecycle rules, CloudFront caching, short log retention |
Shaun AWS Cloud Engineer
Every resource, every IAM policy, every alarm β all defined in the infrastructure/ directory. No ClickOps.

