A production-ready serverless URL shortener demonstrating advanced AWS cloud engineering, enterprise security practices, and cost-optimized serverless architecture.
π‘ Frontend Application: Check out chainy-web for the React frontend. πΉπΌ Chinese Documentation: See README_ZH.md for Traditional Chinese guide.
- AWS Lambda: TypeScript-based microservices with optimized cold start performance
- API Gateway: HTTP API with custom authorizers and enterprise-grade security
- DynamoDB: Single-table design with Global Secondary Indexes for optimal performance
- S3 + CloudFront: Global CDN with intelligent caching and custom domain SSL
- AWS WAF: Custom security rules with DDoS protection and bot mitigation
- IAM Roles: Least privilege access with fine-grained permissions
- SSM Parameter Store: Encrypted secrets management with version control
- JWT Authentication: Custom Lambda authorizer with Google OAuth 2.0 integration
- CloudWatch: Custom metrics, dashboards, and automated alerting
- AWS Budgets: Real-time cost monitoring with anomaly detection
- SNS: Automated notifications and alerting system
- Comprehensive Logging: Structured logging with log aggregation
graph TB
Browser["Browser/Client"]
API["API Gateway"]
L1["Redirect Lambda"]
L2["CRUD Lambda"]
DB["DynamoDB"]
S3["S3 Events"]
CW["CloudWatch"]
AN["Analytics"]
Browser -->|"GET /{code}"| API
Browser -->|"CRUD /links"| API
API --> L1
API --> L2
L1 --> DB
L2 --> DB
L1 --> S3
L2 --> S3
L1 --> CW
L2 --> CW
S3 --> AN
backend.tf # Remote state definition (edit with your S3/DynamoDB details)
main.tf # Root module wiring all submodules
tfvars/ # (optional) directory for environment-specific tfvars files
modules/
api/ # API Gateway HTTP API + routes + Lambda permissions
db/ # DynamoDB table definition for short links
events/ # S3 bucket (with lifecycle policies) for domain events
lambda/ # Redirect + CRUD Lambdas and IAM roles
handlers/ # TypeScript Lambda sources
lib/ # Shared TypeScript utilities (DynamoDB client)
scripts/ # esbuild bundling script for Lambda packages
dist/ # Generated Lambda bundles (created by `npm run package`)
README.md # English documentation
README_ZH.md # Traditional Chinese documentation
web/ # Minimal static web client for generating short links
variables.tf # Root input variables
outputs.tf # Root outputs
package.json, tsconfig.json
- Terraform 1.9+ - Infrastructure as code
- AWS CLI - Configured with appropriate credentials
- Node.js 20+ - For Lambda function development
- Create remote state resources:
cd bootstrap
terraform init
terraform apply \
-var="state_bucket_name=your-unique-chainy-state-bucket" \
-var="lock_table_name=chainy-terraform-locks"- Update backend configuration in
backend.tfwith your bucket and table names.
# Install dependencies and build Lambda functions
npm install
npm run package
# Initialize Terraform
terraform init
# Create configuration file
cp terraform.tfvars.example terraform.tfvars
# Edit terraform.tfvars with your settings
# Deploy infrastructure
terraform apply# Get API endpoint
API_ENDPOINT=$(terraform output -raw api_endpoint)
# Create a short link
curl -X POST "$API_ENDPOINT/links" \
-H "Content-Type: application/json" \
-d '{"target": "https://example.com", "owner": "test-user"}'
# Test redirect
curl -I "$API_ENDPOINT/generated-code"The Lambda module expects pre-built bundles under dist/redirect and dist/create before you run terraform plan or terraform apply. Use the included Node tooling:
npm install # install TypeScript + esbuild + AWS SDK
npm run package # bundles handlers into dist/redirect and dist/createRe-run npm run package whenever you change the TypeScript source before deploying.
Create a terraform.tfvars (or environment-specific dev.tfvars) with at least:
environment = "dev"
region = "ap-northeast-1"
# Optional additional environment variables for Lambda
lambda_additional_environment = {}environmentcontrols resource naming, tags, and outputs;regiondefaults toap-northeast-1and can be adjusted as needed.lambda_additional_environmentcan be used to add additional environment variables; hash salts are retrieved from AWS Systems Manager Parameter Store by default at paths/chainy/<environment>/CHAINY_HASH_SALTand/chainy/<environment>/CHAINY_IP_HASH_SALT, which can be overridden viahash_salt_parameter_name/ip_hash_salt_parameter_name.- Use
aws ssm put-parameter --type SecureString --value "$(openssl rand -hex 32)"to create salts and ensure Lambda IAM roles havessm:GetParameterpermissions.
The /web directory contains a lightweight static page (HTML/CSS/JS) for creating short links without leaving the browser. To preview locally:
cd web
python -m http.server 4173Open http://localhost:4173, fill in the API endpoint (e.g. https://xxxx.execute-api.ap-northeast-1.amazonaws.com) and create short links instantly. For deployment you can aws s3 sync web/ s3://<your-web-bucket> --delete and invalidate CloudFront, or plug the build into the CI/CD workflow.
-
Copy
terraform.tfvars.exampleβterraform.tfvars(create the example file if you prefer) and set values:environment = "dev"region = "ap-northeast-1"(or your preferred region)- Optional overrides:
redirect_build_dir,create_build_dir,extra_tags.
-
Initialize:
terraform init -backend-config="bucket=your-state-bucket" \ -backend-config="key=dev/chainy.tfstate" \ -backend-config="region=ap-northeast-1" \ -backend-config="dynamodb_table=your-lock-table"
-
Validate the configuration:
terraform fmt # optional formatting terraform validate -
Review the plan:
terraform plan -var="environment=dev" -
Apply when ready:
terraform apply -var="environment=dev"
Terraform outputs include the API endpoint, DynamoDB table name, and the events S3 bucket.
After terraform apply, note the api_endpoint output (e.g. https://abc123.execute-api.ap-northeast-1.amazonaws.com).
-
Create a short link
curl -X POST "$API_ENDPOINT/links" \ -H "Content-Type: application/json" \ -d '{"target": "https://example.com/docs", "owner": "alice"}'
Response includes the generated
code. -
Resolve a short link
curl -I "$API_ENDPOINT/yourCode"Expect a
301withLocation: https://example.com/docs. -
Inspect or manage a link
curl "$API_ENDPOINT/links/yourCode" curl -X PUT "$API_ENDPOINT/links/yourCode" \ -H "Content-Type: application/json" \ -d '{"target": "https://example.com/updated"}' curl -X DELETE "$API_ENDPOINT/links/yourCode"
- Link creation:
POST /linkstriggers the create Lambda, which writes metadata to DynamoDB and immediately appends alink_createJSONL object to the events S3 bucket. - Redirect:
GET /{code}invokes the redirect Lambda. It looks up the target in DynamoDB, increments click counters, and asynchronously logs alink_clickevent to S3 while returning a301response. - Lifecycle events: Update/delete endpoints also append JSONL records so downstream analytics stay in sync.
- Analytics storage: S3 objects are partitioned by event type and date/hour (e.g.
link_click/dt=2024-09-30/hour=13/...). - Insights: Use Athena (via an external table over the JSONL keys) or import into QuickSight/ETL jobs for dashboards.
- Lambda hashes
owner,user_agent, and (if present) IP addresses before persisting, keeping only SHA-256 digests for grouping while hiding raw strings. Wallet signatures are never storedβonly flagwallet_signature_present. - Wallet addresses are masked (first 4 / last 4 characters) and referer/target URLs are normalised to origin + path; query strings and other sensitive fragments are removed.
- Optional Web3/marketing metadata such as
wallet_provider,wallet_type,chain_id,dapp_id, UTM tags, geo/ASN, Accept-Language, inferred device/browser families, transaction value/currency, token symbol/address, and partner/project identifiers are retained in coarse form for analytics while the original sensitive values are either hashed, masked, or normalised. tags/feature_flagsarrays are trimmed to at most 10 entries for cost control. Asensitive_redactedflag indicates events that had fields sanitised so downstream jobs can branch if needed.
With fewer than 10k events per month, the direct-to-S3 approach keeps costs to pennies:
- S3 PUT: $0.005 per 1,000 requests β β $0.05 for 10k events.
- S3 storage: JSONL events (a few KB each) stay under a few cents per month; lifecycle expires or transitions them after
click_events_retention_days(default 30 days). - Lambda: Millisecond execution time leads to <$0.01/month at the stated volume.
- Backend API:
https://9qwxcajqf9.execute-api.ap-northeast-1.amazonaws.com - Lambda Functions: create, redirect (both active)
- DynamoDB:
chainy-dev-chainy-linkstable - S3 Storage: Events and web hosting buckets
- SSM Parameters: Secure hash salt storage
- API Authentication: API Key with rate limiting
- SSL Certificate: Pending DNS validation
- CloudFront: Waiting for SSL certificate
- Custom Domain:
chainy.luichu.dev(pending SSL)
- Redirect Function: Returns 404 (investigating)
- CloudFront Output: Not available until SSL validation
- Automated checks run through
.github/workflows/ci.yml(TODO: Add CI/CD workflow file): Node.js dependencies (npm install),npm run typecheck,npm run test, and Terraformfmt/validate(root + bootstrap, with backends disabled). - To enable plan/apply from GitHub Actions, configure OIDC access for Terraform (see docs/architecture.md for recommended next steps) and supply cloud credentials via repo secrets.
When you want to tear down the scaffold, run:
terraform destroy -var="environment=dev"- CloudFront + Custom Domain β front the API and redirects with a custom hostname and SSL certificate managed by ACM.
- Cognito + OAuth β secure CRUD APIs with Cognito-hosted auth flows or federated identity providers.
- QuickSight Dashboard β visualize click analytics sourced from the S3/Athena dataset.
- Budgets + Alerts β add AWS Budgets or Cost Anomaly Detection to avoid surprises.
- GitHub Actions CI/CD β configure an OIDC trust to deploy Terraform plans from GitHub securely.
Happy building and good luck studying for AWS SAA + Terraform Associate!