Homework #5: Flask API and EC2 Deployment
EE 547: Spring 2026
Assigned: 25 March
Due: Tuesday, 8 April at 23:59
Gradescope: Homework 5 | How to Submit
- Python 3.11+ required
- Docker Desktop installed and running
- AWS CLI configured with valid credentials
- HW#4 Problem 2 DynamoDB table (
arxiv-papers) populated with data
Overview
Build an authenticated REST API with Flask and deploy it to a live EC2 instance. The API serves paper metadata from DynamoDB, and the deployment makes it accessible over the public internet.
Getting Started
Download the starter code: hw5-starter.zip
unzip hw5-starter.zip
cd hw5-starterProblem 1: Flask REST API for ArXiv Papers
Implement a REST API that serves ArXiv paper metadata from DynamoDB, with JWT authentication on protected endpoints.
Use only Flask, boto3, PyJWT, gunicorn, and Python standard library modules. Do not use Django, FastAPI, or other web frameworks.
Part A: API Endpoints
Create app.py that implements the following endpoints. All endpoints except /api/stats, /api/login, and the arxiv redirect require a valid JWT in the Authorization header.
Unprotected:
GET /api/stats— Server statisticsTrack request counts in memory (resets on restart).
{ "status": "healthy", "uptime_seconds": 3842, "region": "us-west-2", "table": "arxiv-papers", "requests": { "total": 247, "by_status": {"200": 210, "401": 25, "404": 8, "302": 4} } }POST /api/login— Authenticate and receive JWTRequest:
{"username": "admin", "password": "secret"}Success (200):
{"token": "eyJhbGciOiJIUzI1NiIs...", "expires_in": 3600}Invalid credentials (401):
{"error": "Invalid credentials"}
Protected (require Authorization: Bearer <token>):
GET /api/papers?category={category}&limit={limit}— Recent papers in categoryDefault limit: 20.
{ "category": "cs.LG", "papers": [ { "arxiv_id": "2301.12345", "title": "Paper Title", "authors": ["Author One", "Author Two"], "published": "2023-01-15T10:30:00Z" } ], "count": 5 }GET /api/papers/{arxiv_id}— Full paper details by ID{ "arxiv_id": "2301.12345", "title": "Paper Title", "authors": ["Author One", "Author Two"], "abstract": "Full abstract text...", "categories": ["cs.LG", "cs.AI"], "published": "2023-01-15T10:30:00Z" }If the paper does not exist, return 404:
{"error": "Paper not found", "arxiv_id": "2301.99999"}GET /api/papers/author/{author_name}— Papers by author{ "author": "Author Name", "papers": [...], "count": 3 }GET /api/papers/search?category={category}&start={date}&end={date}— Papers in date rangeDates are ISO-8601 format (
YYYY-MM-DD). Bothstartandendare required.{ "category": "cs.LG", "start": "2023-01-01", "end": "2023-12-31", "papers": [...], "count": 12 }Return 400 if
category,start, orendis missing.GET /api/papers/keyword/{keyword}?limit={limit}— Papers matching keywordDefault limit: 20.
{ "keyword": "transformer", "papers": [...], "count": 8 }GET /api/papers/{arxiv_id}/arxiv— Redirect to paper on arxiv.orgReturns a 302 Found redirect to
https://arxiv.org/abs/{arxiv_id}. No JSON body.If the paper does not exist in your table, return 404 (not a redirect).
Error responses:
All errors return JSON with an error field:
- 400 — Missing or invalid parameters
- 401 — Missing or invalid token
- 404 — Resource not found
- 500 — Server error
Part B: JWT Authentication
Implement token-based authentication using PyJWT.
Users: Define a USERS dict in your application with at least two users:
USERS = {
"admin": "secret",
"viewer": "readonly"
}Plaintext passwords are fine here — the focus is on the token flow, not password storage.
Token creation (/api/login):
import jwt
import time
SECRET_KEY = "your-secret-key" # Hardcoded is fine for this assignment
def create_token(username):
payload = {
"sub": username,
"iat": int(time.time()),
"exp": int(time.time()) + 3600,
"role": "admin"
}
return jwt.encode(payload, SECRET_KEY, algorithm="HS256")Standard JWT claims (RFC 7519):
sub— Subject: identifies who the token representsiat— Issued At: Unix timestamp of token creationexp— Expiration: Unix timestamp after which the token is rejected
JWT payloads also support arbitrary custom claims. The role field above is application-specific metadata embedded directly in the token, available on decode without a database lookup.
Token validation (protected endpoints):
A Python decorator wraps a function with additional behavior — @app.route is one. The same pattern works for authentication: a @require_auth decorator that runs before the route handler, checks the token, and returns 401 if invalid. The route handler only executes if the token is valid.
The decorator should:
- Read the
Authorizationheader - Extract the token after
Bearer - Decode and validate with PyJWT
- Return 401 if the header is missing, malformed, or the token is invalid/expired
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Part C: DynamoDB Integration
The API reads from the arxiv-papers DynamoDB table (the same table and GSIs from HW#4 Problem 2).
Configuration via environment variables:
import os
AWS_REGION = os.environ.get("AWS_REGION", "us-west-2")
DYNAMODB_TABLE = os.environ.get("DYNAMODB_TABLE", "arxiv-papers")Each endpoint corresponds to a DynamoDB access pattern:
| Endpoint | DynamoDB Operation |
|---|---|
GET /api/papers?category= |
Main table query, sort key descending |
GET /api/papers/{arxiv_id} |
GSI (PaperIdIndex) |
GET /api/papers/author/{name} |
GSI (AuthorIndex) |
GET /api/papers/search?... |
Main table, sort key range |
GET /api/papers/keyword/{kw} |
GSI (KeywordIndex) |
Part D: Dockerfile
A Dockerfile and requirements.txt are provided in the starter code.
Code: Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENV PYTHONUNBUFFERED=1
EXPOSE 8080
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "--workers", "2", "app:app"]
Code: requirements.txt
flask>=3.0
boto3>=1.28.0
pyjwt>=2.8.0
gunicorn>=21.2.0
The Dockerfile uses gunicorn as the production WSGI server (not the Flask development server). For local development, run directly with Python:
pip install -r requirements.txt
python app.pyYour app.py should include a development entry point:
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080, debug=True)Part E: Local Testing
Build and run with Docker:
docker build -t arxiv-api:latest .
docker run --rm -p 8080:8080 \
-e AWS_ACCESS_KEY_ID \
-e AWS_SECRET_ACCESS_KEY \
-e AWS_SESSION_TOKEN \
arxiv-api:latestThe -e flags pass your local AWS credentials to the container. If you use named profiles, export the credentials first.
Test your endpoints:
# Stats (no auth required)
curl http://localhost:8080/api/stats
# Login — copy the token from the response
curl -s -X POST http://localhost:8080/api/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "secret"}'
# Protected endpoints (replace <token> with the value from login)
curl -H "Authorization: Bearer <token>" \
"http://localhost:8080/api/papers?category=cs.LG&limit=5"
curl -H "Authorization: Bearer <token>" \
http://localhost:8080/api/papers/author/LeCun
# Should return 401
curl http://localhost:8080/api/papers?category=cs.LG
# Should also return 401
curl -H "Authorization: Bearer invalid-token" \
http://localhost:8080/api/papers?category=cs.LGDeliverables
See Submission.
We will validate your submission by running the following commands from your q1/ directory:
docker build -t arxiv-api:latest .
docker run --rm -p 8080:8080 \
-e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_SESSION_TOKEN \
-e DYNAMODB_TABLE=arxiv-papers \
arxiv-api:latestWe will then verify:
- Stats endpoint responds without authentication and includes uptime and request counts
- Login returns a valid JWT for correct credentials and 401 for incorrect
- All five query endpoints return correct JSON structure
- Arxiv redirect returns 302 with correct Location header
- Protected endpoints return 401 without a token
- Protected endpoints return 401 with an expired or invalid token
- Error responses use correct HTTP status codes (400, 401, 404)
- All endpoints respond within 2 seconds
Problem 2: Deploy to EC2
Deploy your Problem 1 API to an EC2 instance accessible over the public internet.
Part A: EC2 Instance
Launch an Ubuntu EC2 instance with the following configuration:
- AMI: Ubuntu Server 24.04 LTS
- Instance type: t2.micro (free tier eligible)
- Key pair: Use an existing key pair or create a new one
- Network: Default VPC, public subnet, auto-assign public IP enabled
- Security group: Allow inbound on port 22 (SSH) and port 8080 (your API)
The instance must have a public IP to be reachable from outside AWS. Instances in private subnets or without a public IP are only accessible from within the VPC.
If you still have a key pair and security group from the demo, you can reuse them. Add a rule for port 8080 if it’s not already open:
| Type | Port | Source | Description |
|---|---|---|---|
| Custom TCP | 8080 | 0.0.0.0/0 | Flask API |
Part B: IAM Role
Create an IAM role that grants the instance read access to your DynamoDB table.
1. Create a policy with the following permissions:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:Query",
"dynamodb:GetItem",
"dynamodb:BatchGetItem",
"dynamodb:Scan",
"dynamodb:DescribeTable"
],
"Resource": "arn:aws:dynamodb:*:*:table/arxiv-papers*"
}
]
}The wildcard after arxiv-papers covers the table and its GSIs. DescribeTable allows boto3 to check the table exists at startup.
2. Create a role with trusted entity type “AWS service” → “EC2”, and attach the policy.
3. Attach the role to your running instance: Actions → Security → Modify IAM role.
Verify on the instance:
aws sts get-caller-identityThis should return an ARN containing your role name. sts:GetCallerIdentity requires no IAM permissions — it always works when a role is attached. If it fails with a credentials error, the role is not attached or hasn’t propagated yet (wait a minute and retry).
Part C: Deploy
Copy your Q1 application to the instance and run it.
Copy files from your local machine:
scp -i <key-file> -r q1/ ubuntu@<public-ip>:~/arxiv-apiSSH into the instance:
ssh -i <key-file> ubuntu@<public-ip>
cd arxiv-apiInstall dependencies and run with gunicorn:
pip install -r requirements.txt
gunicorn --bind 0.0.0.0:8080 --workers 2 --daemon app:appThe --daemon flag runs gunicorn in the background. It persists after you disconnect.
On EC2 with an attached IAM role, boto3 automatically finds credentials through the instance metadata service. No environment variables or aws configure needed.
Part C2: Docker Deployment
Running gunicorn directly works, but the application depends on the correct Python version and packages being installed on the instance. Docker packages the application with its dependencies into a single image — the same image that runs locally runs identically on EC2.
Stop the running gunicorn process:
pkill gunicornBuild and run the container:
docker build -t arxiv-api:latest .
docker run -d -p 8080:8080 arxiv-api:latestThe -d flag runs the container in the background. Containers on EC2 with an attached IAM role automatically have access to the instance credentials through the metadata service — no -e AWS_ACCESS_KEY_ID flags needed.
Docker must be installed on the instance. If you launched with the user-data script from the demo, it is already present. Otherwise install Docker manually (see Demo 03a § Instance Setup).
Your final deployment should use Docker.
Part D: Verification
From your local machine, verify the deployment:
# Stats
curl http://<public-ip>:8080/api/stats
# Login
curl -X POST http://<public-ip>:8080/api/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "secret"}'
# Query (use the token from login)
curl -H "Authorization: Bearer <token>" \
"http://<public-ip>:8080/api/papers?category=cs.LG&limit=3"All three requests must succeed. If stats responds but queries fail with 500 errors, the IAM role likely lacks DynamoDB permissions.
Part E: README
Create q2/README.md documenting:
- Your EC2 instance’s public IP address
- The IAM role ARN attached to the instance
- Whether you deployed with Docker or direct gunicorn
- Any issues encountered during deployment and how you resolved them
Deliverables
See Submission.
We will test your deployed API from outside AWS:
curl http://<your-ip>:8080/api/stats
curl -X POST http://<your-ip>:8080/api/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "secret"}'
curl -H "Authorization: Bearer <token>" \
"http://<your-ip>:8080/api/papers?category=cs.LG&limit=5"We will verify:
- Instance is reachable on port 8080
- Stats, login, and query endpoints respond correctly
- DynamoDB queries return data (not empty results)
- Server remains running (not a foreground process tied to your SSH session)
Keep your instance running until grades are posted. t2.micro is free tier eligible (750 hours/month). Terminate after grading to avoid charges if your free tier has expired.
README.md
q1/
├── app.py
├── Dockerfile
└── requirements.txt
q2/
└── README.md