This project is part of the Be A Man (BAM) Challenge completed by T.I.Q.S..
It demonstrates how to serve a static Avengers-themed website from Amazon S3 that connects to a Flask API hosted on an EC2 instance via Gunicorn + Nginx.
The static website lets users:
- Configure the API base URL (Captain America)
- Check service health (Iron Man)
- Send echo JSON payloads (Thor)
- Run fun number stats (Hulk)
- Frontend: Hosted on Amazon S3 static site
- Backend: Flask app running under Gunicorn on EC2 (Amazon Linux 2023)
- Reverse Proxy: NGINX routes traffic from port 80
-
Create bucket
-
Enable Static Website Hosting
- Open your bucket
- Go to Properties tab

- Scroll down to Static website hosting and click Edit

- Static website hosting: Enable
- Index document:
index.html
- Click Save changes

Your S3 bucket website endpoint will be:
http://bam-static-site-tiqs.s3-website-us-east-1.amazonaws.com -
Bucket Policy (Public Read)
-
CORS (Cross-origin resource sharing)
-
Upload static files
- Go to Objects tab and click Upload

- Add the following static files:
index.htmlcaptain-america.jpghulk.jpgiron-man.jpgthor.jpgscript.js
- Verify all files are staged for upload.
- Leave all other sections closed and as default.
- Scroll down to the bottom of the page and click Upload.

- Verify all six files are uploaded with the word
Succeededunder status. - The green bar at the top say
Upload succeeded.
- Go to Objects tab and click Upload
-
Go to Security Groups
-
Inbound rules
Type Protocol Port range Source Description SSH TCP 22 Anywhere IPv4 SSH HTTP TCP 80 Anywhere IPv4 HTTP -
SKIP OUTBOUND RULES!!
-
Add tag:
name→bam-static-site-sg.
-
Go to Instances
-
Key Pair (NOTE: You will need a key pair to SSH into your EC2 instance)
-
Network settings
- Firewall (security group):
- Click Select existing security group
- Choose
bam-static-site-sg
-
Configure storage: leave as default
- Go to your EC2 Instance Summary
- Click the Connect button on the top right side.

- Leave everything as default on this page and click the Connect button on the bottom right of the page.

Copy and paste the whole thing into your EC2 Instance Connect terminal (and press ENTER):
# ========= Config =========
APP_USER="ec2-user" # OS user that runs the app
APP_DIR="/home/${APP_USER}/bam-flask" # App directory
SERVICE_NAME="bam-flask"
GUNICORN_BIND="127.0.0.1:8000" # Gunicorn bind address
ALLOWED_ORIGIN="*" # For production, set to your S3 Website URL
NGINX_SITE="/etc/nginx/conf.d/${SERVICE_NAME}.conf"
# ==========================
# System update + core packages
sudo dnf -y update
sudo dnf -y install nginx python3-pip python3-virtualenv
# Create/own app directory
sudo mkdir -p "${APP_DIR}"
sudo chown -R "${APP_USER}:${APP_USER}" "${APP_DIR}"
# -----------requirements.txt -----------------
cat > "${APP_DIR}/requirements.txt" <<'REQS'
flask==3.0.3
gunicorn==22.0.0
flask-cors==4.0.1
REQS
# ---------------------------------------------
# ------------------ app.py -------------------
cat > "${APP_DIR}/app.py" <<'PY'
from flask import Flask, jsonify, request
from flask_cors import CORS
import os
app = Flask(__name__)
allowed = os.environ.get("ALLOWED_ORIGIN", "*").strip()
if not allowed or allowed == "*":
CORS(app, resources={r"/api/*": {"origins": "*"}})
else:
CORS(app, resources={r"/api/*": {"origins": [o.strip() for o in allowed.split(",") if o.strip()]}})
@app.route("/api/health", methods=["GET"])
def health():
return jsonify(status="ok", service="bam-flask-api")
@app.route("/api/echo", methods=["POST"])
def echo():
data = request.get_json(silent=True) or {}
return jsonify(ok=True, you_sent=data)
@app.route("/api/fun", methods=["GET"])
def fun():
nums = request.args.get("nums", "2,4,6,9")
arr = [float(x) for x in nums.split(",")] if nums else []
return jsonify(count=len(arr), sum=sum(arr), avg=(sum(arr)/len(arr) if arr else None))
PY
# ---------------------------------------------
# ------------------ wsgi.py ------------------
cat > "${APP_DIR}/wsgi.py" <<'PY'
from app import app
if __name__ == "__main__":
app.run()
PY
# ---------------------------------------------
# Python venv + dependents
sudo -u "${APP_USER}" bash -lc "python3 -m venv ${APP_DIR}/.venv"
sudo -u "${APP_USER}" bash -lc "${APP_DIR}/.venv/bin/pip install --upgrade pip"
sudo -u "${APP_USER}" bash -lc "${APP_DIR}/.venv/bin/pip install -r ${APP_DIR}/requirements.txt"
# systemd service for Gunicorn
sudo tee "/etc/systemd/system/${SERVICE_NAME}.service" > /dev/null <<EOF
[Unit]
Description=BAM Flask API (gunicorn)
Wants=network-online.target
After=network-online.target
[Service]
User=${APP_USER}
WorkingDirectory=${APP_DIR}
Environment=PATH=${APP_DIR}/.venv/bin
Environment=ALLOWED_ORIGIN=${ALLOWED_ORIGIN}
ExecStart=${APP_DIR}/.venv/bin/gunicorn --workers 2 --timeout 60 --bind ${GUNICORN_BIND} wsgi:app
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now "${SERVICE_NAME}"
# Nginx reverse proxy :80 -> Gunicorn :8000
if [ -f /etc/nginx/conf.d/default.conf ]; then sudo mv /etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf.bak; fi
if [ -f /etc/nginx/conf.d/ssl.conf ]; then sudo mv /etc/nginx/conf.d/ssl.conf /etc/nginx/conf.d/ssl.conf.bak; fi
sudo tee "${NGINX_SITE}" > /dev/null <<EOF
server {
listen 80 default_server;
server_name _;
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options SAMEORIGIN;
location / {
proxy_pass http://${GUNICORN_BIND}/;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_http_version 1.1;
}
}
EOF
sudo nginx -t
sudo systemctl enable --now nginx
sudo systemctl reload nginx || true
# Helpful hint
# I'm not sure if this portion is actually needed but I left it in there just in case
EC2_DNS=$(curl -s http://169.254.169.254/latest/meta-data/public-hostname || true)
echo "All set."
[ -n "$EC2_DNS" ] && echo "Test from your laptop: curl -i http://${EC2_DNS}/api/health"
echo "For production CORS, edit ALLOWED_ORIGIN in /etc/systemd/system/${SERVICE_NAME}.service then:"
echo "sudo systemctl daemon-reload && sudo systemctl restart ${SERVICE_NAME}"We will test the EC2 API Base URL to see if the Flask App backend components are working correctly before we present the S3 Static Website.
-
Go to EC2 Instance Summary, copy the Public DNS.
-
Test the EC2 Instance API endpoints: with
healthandfun?nums=1,2,3,4,5,6,8http://<EC2_PUBLIC_DNS>/api/health http://<EC2_PUBLIC_DNS>/api/fun?nums=1,2,3,4,5,6,8 -
Open a new browser tab and go to Avengers themed custom S3 Static Bucket Endpoint Website to present your Flask API components:
http://bam-static-site-tiqs.s3-website-us-east-1.amazonaws.com/-
In the Configure box next to
Captain America, type your EC2 Base API URL:http://<EC2_PUBLIC_DNS>/ -
Click Save
-
Scroll Down and click GET API Health button next to
Iron Man. It should read as:{ "service": "bam-flask-api", "status": "ok" } -
Scroll Down to Echo box and type
class-7-rocksin the top box next toThor. -
Click the POST API Echo button. It should read as:
{ "ok": true, "you_sent": { "text": "class-7-rocks" } } -
Scroll Down to the Fun Stats box and modify the numbers (i.e.
1,2,3,4,5,6,7,8) next to Hulk. -
Click the GET Fun Stats button. It should read as:
{ "avg": 4.5, "count": 8, "sum": 36 }
| Issue | Fix |
|---|---|
| Nginx shows default 404 page | Remove or disable /etc/nginx/conf.d/default.conf and reload Nginx. |
| CORS errors in browser | Ensure ALLOWED_ORIGIN in systemd service matches your S3 website endpoint【133†source】. |
| Gunicorn not starting | Check logs with: journalctl -u bam-flask -e |
| API unreachable externally | Verify EC2 Security Group allows inbound on port 80. |
| S3 site shows AccessDenied | Ensure bucket policy allows s3:GetObject for *. |
- Author: T.I.Q.S.
- Group Leader: John Sweeney















































