A scalable REST API built with FastAPI, deployed on AWS via Auto Scaling Group with full CI/CD.
- FastAPI with async support and automatic OpenAPI documentation
- SQLModel ORM with MySQL database integration
- Alembic database migrations
- Structured JSON logging with request tracing (X-Request-ID)
- StatsD metrics for API call counts and response time per endpoint
- CloudWatch Agent integration with automatic dashboard deployment on first EC2 boot
- Health check endpoint with database connectivity verification
- User management with email verification via SNS → Lambda → Mailgun
- BCrypt password hashing with strength validation
- Environment-based configuration using Pydantic Settings
- KMS-encrypted EBS, RDS, S3, and Secrets Manager via Terraform
- HTTPS enforced at the ALB with ACM certificates; HTTP redirects to HTTPS
webapp/
├── alembic/ # Database migrations
│ ├── versions/ # Migration scripts
│ └── env.py # Alembic configuration
├── app/
│ ├── api/routes/ # Route handlers (user, course, syllabus, healthz, metadata)
│ ├── core/ # Config, auth, db, exceptions, logging, middleware, statsd
│ ├── model/ # SQLModel database models
│ ├── schemas/ # Pydantic request/response schemas
│ └── services/ # Business logic (user, course, syllabus, SNS, S3, health)
├── scripts/ # Packer provisioning scripts (see scripts/README.md)
├── tests/ # Integration test suite
├── docs/
│ └── API.md # Full API reference
├── .github/
│ └── workflows/
│ ├── ci.yaml # Integration tests + Packer validation (push + PR)
│ └── build.yaml # AMI build + DEMO deploy (PR merged → main)
├── main.py # Application entrypoint
├── pyproject.toml # Project dependencies & config
└── multi-cloud.pkr.hcl # Packer template (AWS + GCP)
- Python 3.14+
- MySQL 8.0+
- uv
# Copy environment templates
cp .env.example .env
cp .env.db.example .env.db
# Edit with your settings
vi .env
vi .env.db
# Install dependencies
uv sync
# Run (development)
fastapi dev main.py
# Run (production)
fastapi run main.pyThe API is available at http://localhost:8000.
Migrations run automatically on startup. No manual
alembic upgrade headneeded in production.
Full request/response details: docs/API.md
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /healthz |
None | Health check with DB verification |
| POST | /v1/user |
None | Create user account |
| GET | /v1/user/self |
Basic | Get authenticated user |
| PUT | /v1/user/self |
Basic | Update authenticated user |
| GET | /v1/user/verify |
None | Verify email address |
| GET | /v1/courses |
Basic | List all courses |
| GET | /v1/courses/{id} |
Basic | Get course by ID |
| POST | /v1/courses |
Basic | Create course |
| PUT | /v1/courses/{id} |
Basic | Update course |
| DELETE | /v1/courses/{id} |
Basic | Delete course |
| POST | /v1/courses/{id}/syllabus |
Basic | Upload syllabus to S3 |
| DELETE | /v1/courses/{id}/syllabus |
Basic | Delete syllabus from S3 |
HTTP Basic Authentication. Username must be a valid email address.
Authorization: Basic base64(email:password)
| Variable | Default | Description |
|---|---|---|
AWS_REGION |
— | AWS region |
AWS_S3_BUCKET_NAME |
— | S3 bucket for syllabus storage |
AWS_SNS_TOPIC_ARN |
— | SNS topic for email verification |
STATSD_HOST |
localhost |
StatsD host |
STATSD_PORT |
8125 |
StatsD port |
LOG_FILE |
— | Optional file path for JSON logs |
DEBUG |
false |
Enables /docs, /redoc, SQL logging |
| Variable | Description |
|---|---|
DATABASE_HOST |
MySQL host |
DATABASE_PORT |
MySQL port (default 3306) |
DATABASE_USER |
MySQL username |
DATABASE_PASSWORD |
MySQL password |
DATABASE_NAME |
Database name |
On AWS,
.env.dbis written by the EC2user_data.shscript at launch time. The password is fetched from AWS Secrets Manager — it is never stored in the AMI or Terraform state.
# Install dev dependencies
uv sync --group dev
# Copy test env template
cp .env.test.example .env.test
# Run all tests
uv run pytest -v --cov=app --cov-report=term-missing
# Run specific file
uv run pytest tests/test_user_api.py -vuv run ruff check .
uv run ruff format .# Create a new migration
alembic revision --autogenerate -m "description"
# Apply manually (not needed in production — runs on startup)
alembic upgrade head
# Rollback one step
alembic downgrade -1Runs on every push and pull request to main:
- Start MySQL service container
- Install Python 3.14 + uv
- Run linting (ruff)
- Run Alembic migrations
- Run pytest with coverage
- Upload test report artifact
- Validate Packer template (fmt + validate)
Required GitHub secrets (environment: CI):
| Secret | Description |
|---|---|
MYSQL_ROOT_PASSWORD |
MySQL root password for the test container |
MYSQL_DATABASE |
Test database name |
MYSQL_USER |
Test database user |
MYSQL_PASSWORD |
Test database password |
AWS_REGION |
AWS region (used by app config during tests) |
AWS_S3_BUCKET_NAME |
S3 bucket name (used by app config during tests) |
AWS_SNS_TOPIC_ARN |
SNS topic ARN (used by app config during tests) |
AWS_DEMO_ACCOUNT_ID |
DEMO account ID (for Packer validate) |
GCP_PROJECT_ID |
GCP dev project ID (for Packer validate) |
GCP_DEMO_PROJECT_ID |
GCP demo project ID (for Packer validate) |
DB_NAME |
DB name passed to .env.setup for Packer validate |
DB_USER |
DB user for .env.setup |
DB_PASSWORD |
DB password for .env.setup |
APP_GROUP |
App group for .env.setup |
APP_USER |
App user for .env.setup |
APP_DIR |
App dir for .env.setup |
DATABASE_HOST |
DB host for .env.setup |
DATABASE_PORT |
DB port for .env.setup |
DATABASE_NAME |
DB name for .env.setup |
DATABASE_USER |
DB user for .env.setup |
DATABASE_PASSWORD |
DB password for .env.setup |
AWS_SNS_TOPIC_ARN |
SNS ARN for .env.setup |
Fires automatically when ci.yaml (Integration Tests) completes successfully on main. This happens on every PR merge.
Pipeline:
validate-packer → build-ami (DEV) → deploy-demo
- validate-packer: Packer fmt check + validate
- build-ami: Packer build in DEV (AMI shared with DEMO); extracts AMI ID
- deploy-demo:
- Reconfigures AWS CLI to DEMO account credentials
- Derives the Launch Template ID from the DEMO ASG
- Creates a new Launch Template version with the latest AMI
- Updates the ASG to use
$Latest - Starts an instance refresh (
MinHealthyPercentage: 50) - Polls until
Successful; exits non-zero onFailed/Cancelled
Required GitHub secrets (environment: Packer):
| Secret | Description |
|---|---|
AWS_ACCESS_KEY_ID |
DEV account IAM access key |
AWS_SECRET_ACCESS_KEY |
DEV account IAM secret key |
AWS_DEFAULT_REGION |
DEV account region |
AWS_DEMO_ACCESS_KEY_ID |
DEMO account IAM access key |
AWS_DEMO_SECRET_ACCESS_KEY |
DEMO account IAM secret key |
AWS_DEMO_REGION |
DEMO account region (falls back to AWS_DEFAULT_REGION) |
DEMO_ASG_NAME |
Name of the DEMO Auto Scaling Group (e.g. webapp-demo-asg) |
GCP_CREDENTIALS |
GCP service account JSON |
GCP_PROJECT_ID |
GCP dev project ID |
GCP_DEMO_PROJECT_ID |
GCP demo project ID |
# Initialize plugins (first time only)
packer init multi-cloud.pkr.hcl
# Build for both AWS and GCP
packer build \
-var="aws_demo_account_id=<DEMO_ACCOUNT_ID>" \
-var="gcp_project_id=<GCP_DEV_PROJECT_ID>" \
-var="gcp_demo_project_id=<GCP_DEMO_PROJECT_ID>" \
multi-cloud.pkr.hcl
# AWS only
packer build -only="amazon-ebs.ubuntu_mysql" \
-var="aws_demo_account_id=<DEMO_ACCOUNT_ID>" \
multi-cloud.pkr.hclThe built AMI is automatically shared with the DEMO AWS account and GCP demo project.
For provisioning script details, see scripts/README.md.
- HTTPS only: ALB enforces HTTPS (port 443); HTTP (port 80) redirects to HTTPS with 301
- EC2 isolation: Application port ingress allowed only from ALB security group — not from the internet
- KMS encryption: EBS, RDS, S3, and Secrets Manager all use dedicated Customer Managed Keys with 90-day rotation
- Secrets Manager: DB password and Mailgun credentials stored in Secrets Manager, fetched at runtime — never baked into AMI or user data
- BCrypt passwords: User passwords hashed with bcrypt + salt
- Least-privilege IAM: EC2 and Lambda roles scoped to the minimum required resources
Internet
│ HTTPS (443)
▼
ALB (SSL termination, ACM certificate)
│ HTTP (app_port)
▼
EC2 Instances (Auto Scaling Group)
│ │ │
▼ ▼ ▼
RDS MySQL S3 Bucket SNS Topic
(private) (private) │
▼
Lambda Function
│
▼
Mailgun API (email)
All sensitive data: AWS Secrets Manager (KMS-encrypted)
All EBS/RDS/S3: Customer Managed KMS Keys (90-day rotation)