Deployment Diagram for our AWS Server

Backend Diagram

We created a camping and national park review site.

Our project integrates a Flask-based backend (running on port 8887) and a JavaScript frontend (running on port 4887) to provide seamless communication via RESTful APIs. Here’s a summary of the key features and setup:

Key Features

  • Review API: Handles CRUD operations for user reviews with authentication using JWT tokens.
  • Gemini API: Integrates Google Generative AI for camping advice, storing chatbot conversations in the database.
  • Quiz API: Manages quiz results, assigning national parks based on user responses and supporting CRUD operations.
  • Location API: Tracks and manages user-specific location data linked to channels with secure token-based authentication.
  • Analytics API: Processes and retrieves data on star ratings and review counts for reporting.
  • Post API: Enables CRUD operations for posts, associating them with users and channels.

Setup Details

  • Frontend-Backend Communication: The frontend fetches data from the backend through HTTP requests, with the backend handling data processing and database interactions.
  • Subdomain and CORS: Cross-Origin Resource Sharing (CORS) is configured in Flask to allow the frontend (port 4887) to securely communicate with the backend (port 8887) during development.

This structure ensures smooth integration, secure interactions, and efficient handling of user data across both frontend and backend.

College Board Requirements Met:

  1. Use of HTTP and RESTful APIs: I created the star API that interacts with the backend using GET, POST, PUT, and DELETE functions (see below). This ties into our national park page through the overall star review section.
  2. Performance Optimization: I use caching by storing the stars in the cache until the “Submit” button is selected, as well as indexing the database for previous records, thus meeting Performance Optimization requirements.
  3. Monitoring and Logging: Logging occurs through the console log, with error statements being printed for debugging
  4. Security and Authentication: Each action in my API requires the authentication token, which fetches the JWT token from the cache for each browser session

Snippet of Code for College Board Requirements:

class _RATING(Resource):
        @token_required()
        def post(self):
            """Handle both storing and fetching ratings."""
            current_user = g.current_user
            data = request.get_json()

            # If 'stars' is in the request body, handle storing a rating
            if 'stars' in data:
                # Validate required fields
                if not data or 'stars' not in data or 'channel_id' not in data:
                    return {'message': 'Missing required fields (stars, channel_id)'}, 400

                stars = data['stars']
                channel_id = data['channel_id']

                # Validate stars
                if not isinstance(stars, int) or stars < 1 or stars > 5:
                    return {'message': 'Invalid star rating. Must be an integer between 1 and 5.'}, 400

                # Check if the channel exists
                channel = Channel.query.get(channel_id)
                if not channel:
                    return {'message': 'Channel not found'}, 404

                # Create or update the rating
                rating = Rating.query.filter_by(user_id=current_user.id, channel_id=channel.id).first()
                if rating:
                    rating.stars = stars  # Update the stars if the rating already exists
                else:
                    rating = Rating(stars=stars, user_id=current_user.id, channel_id=channel.id)
                    db.session.add(rating)

                db.session.commit()
                return {'message': 'Rating submitted successfully', 'rating': rating.read()}, 201

            # If 'stars' is NOT in the request body, assume it's a fetch request
            elif 'user_id' in data and 'channel_id' in data:
                user_id = data.get('user_id')
                channel_id = data.get('channel_id')

                # Validate request data
                if not user_id or not channel_id:
                    return {'message': 'Missing user_id or channel_id in request body'}, 400

                # If user_id is a string (e.g., a name like "toby"), map it to its ID
                if isinstance(user_id, str):  # If user_id is passed as a name
                    print(f"Searching for user with name: {user_id}")
                    user = Frostbyte.query.filter_by(_uid=user_id).first()
                    if not user:
                        print(f"User '{user_id}' not found in the database.")
                        return {'message': f'User "{user_id}" not found'}, 404
                    user_id = user.id
                    print(f"Found user: {user.name} with ID: {user.id}")

                # Query the Rating table for the user's rating for the given channel
                rating = Rating.query.filter_by(user_id=user_id, channel_id=channel_id).first()

                if not rating:
                    return {'message': 'No rating found for the specified user and channel'}, 404

                return jsonify({'stars': rating.stars})

            # If neither case matches, return an error
            return {'message': 'Invalid request'}, 400

        @token_required()
        def put(self):
            """
            Update a post.
            """
            # Obtain the current user
            current_user = g.current_user
            # Obtain the request data
            data = request.get_json()
            # Find the current post from the database table(s)
            post = Frostbyte.query.get(data['id'])
            if post is None:
                return {'message': 'Post not found'}, 404
            # Update the post
            post._title = data['title']
            post._content = data['rating']
            post._channel_id = data['channel_id']
            # Save the post
            post.update()
            # Return response
            return jsonify(post.read())

        @token_required()
        def get(self):
            """Retrieve all ratings for a post."""
            data = request.get_json()

            if not data or 'channel_id' not in data:
                return {'message': 'Channel ID is required'}, 400

            ratings = Rating.query.filter_by(channel_id=data['channel_id']).all()
            if not ratings:
                return {'message': 'No ratings found for this channel'}, 404

            return jsonify({
                "ratings": [rating.read() for rating in ratings]
            })
        
        @token_required()
        def delete(self):
            """Delete all ratings by a specific user."""
            data = request.get_json()
            user_id = data.get('user_id')

            # Validate user_id
            if not user_id:
                return {'message': 'Missing user_id in request body'}, 400 ############### Example of Logging in the console ###############

            # Query the User table to ensure the user exists
            user = Frostbyte.query.filter_by(_uid=user_id).first()  # Match by _uid
            if not user:
                return {'message': f'User "{user_id}" not found'}, 404

            # Delete all ratings by the user
            deleted_count = Rating.query.filter_by(user_id=user.id).delete()
            db.session.commit()

            if deleted_count == 0:
                return {'message': 'No ratings found for the specified user'}, 404

            return {'message': f'Deleted {deleted_count} rating(s) for user "{user_id}"'}, 200

Ideal final project

-> Host both the Flask backend and frontend on an AWS EC2 instance, ensuring communication between the two. The deployment should support:

Scalability: Handling Multiple Users Efficiently

  • Using multiple workers in gunicorn helps process several requests at the same time, preventing slowdowns.
  • Running background tasks with Celery and Redis keeps the main app responsive by handling time-consuming jobs separately.
  • Scaling the backend horizontally (adding more instances) using Docker or Kubernetes helps handle more users without overloading a single server.
  • Optimizing database performance with connection pooling and caching makes sure queries run quickly, even with many users.

Security: Protecting Data and Access

  • Setting up firewall rules blocks unwanted traffic and restricts access to only necessary services.
  • Enabling SSL/TLS encryption ensures data sent between users and the server is secure.
  • Using JWT tokens, API keys, or OAuth controls who can access different parts of the app.
  • Running the app in a secure Docker container with limited permissions reduces security risks.

Reliability: Keeping the Backend Running Smoothly

  • Automated deployment with tools like GitHub Actions ensures updates happen without breaking the app.
  • Setting up logging and monitoring with tools like Prometheus or Logstash helps track errors and performance issues in real time.
  • Using backup strategies and automatic rollbacks prevents downtime in case of failures.

Domain Integration: Routing Traffic Properly

  • Setting up a subdomain (http://frostbyte.stu.nighthawkcodingsociety.com) keeps it organized.
  • Using a reverse proxy (like Nginx) can help direct traffic efficiently while improving security.
  • Configuring DNS settings correctly ensures that users can reach the backend without connection issues.

Networking Stack in Github Pages:

As outlined in our diagram above, our project utilizes all four networking layers to offer a cohesive experience to the user.

1. Application Layer: HTTP/DNS in Action

Frontend (GitHub Pages)

The frontend, built with modern JavaScript, uses the Fetch API to make HTTP(S) requests to the backend. These requests perform key operations like creating and updating reviews or fetching quiz results.

  • A domain name resolves to the backend’s IP address using DNS (frostbyte.stu.nighthawkcodingsociety.com)
  • The browser sends HTTP requests, specifying methods (e.g., GET, POST, PUT, DELETE), headers, and optional JSON payloads for CRUD operations.
  • For example, if a user submits a review, the frontend sends a POST request to /api/review with the review data in JSON format.

Backend (AWS EC2 with Docker)

The backend processes these requests using Flask and interacts with the SQLite database to handle:

  • Create: Adding new records like user reviews or quiz results.
  • Read: Retrieving stored data, such as user reviews or analytics.
  • Update: Modifying existing data entries.
  • Delete: Removing records from the database.

Flask sends HTTP responses with status codes, headers, and JSON data back to the frontend for display.

Security Matters

Certbot manages SSL/TLS certificates, ensuring all communication between the frontend and backend is encrypted via HTTPS.

2. Transport Layer: TCP/UDP for Reliable Communication

HTTP(S) requests are transmitted over TCP, ensuring reliable delivery of data. Here’s the flow:

  • Request: Nginx routes incoming TCP traffic to the correct Docker container based on the URL path. A three-way TCP handshake establishes a secure connection.
  • Response: The backend sends responses over the same TCP connection, guaranteeing that data arrives intact and in order.

3. Network Layer: Routing the Packets

At this layer, TCP segments carrying HTTP requests are encapsulated into IP packets. These packets are:

  • Sent: Routed across the internet to the AWS EC2 instance.
  • Received: AWS handles routing and load balancing before the backend processes the request.

The backend sends the response back to the client in the same way.

4. Physical Layer: The Hardware

Data ultimately travels through physical infrastructure—cables, routers, and wireless access points—before reaching the client. Whether the user is on Wi-Fi, Ethernet, or mobile data, the process remains seamless.

Our Tools: The Backbone of Development

  • Docker: Ensures consistent backend deployment across development, testing, and production.
  • Nginx: Routes incoming traffic and manages connections between the frontend and backend.
  • Certbot: Secures communication with SSL/TLS certificates.
  • SQLite: Stores data for CRUD operations, from reviews to analytics.
  • JavaScript Fetch API: Powers asynchronous HTTP requests from the frontend.
  • Flask: Provides RESTful APIs for handling requests and interacting with the database.

College Board Requirement Met:

  1. Deployment Strategies: We will be using AWS for our deployment and continuous deployment pipelines will automate the process
  2. Domain Name System: Our configured EC2 instance will be mapped to the frostbyte.stu.nighthawkcodingsociety.com domain through DNS resolution, as outlined in the diagram above

Deployment Process

Deployment Process Using Docker & Cockpit for a GitHub Pages Frontend & Backend Site

Prepare the Backend (Flask + Docker)

  • Create a Dockerfile in the backend directory to containerize the Flask app.
  • Ensure gunicorn is set up to serve the app on a specified port (e.g., 8087).
  • Build and test the Docker image locally:

      docker build -t my-backend .
      docker run -p 8087:8087 my-backend
    
  • Push the image to a container registry (e.g., Docker Hub or GitHub Container Registry).

Deploy the Backend with Cockpit

  • Access Cockpit on the server (Cockpit is a web-based Linux server manager).
  • Use Docker inside Cockpit to pull and run the backend container.
  • Set up the container to restart automatically and expose the correct port.
  • Use Nginx as a reverse proxy to route traffic properly to the backend.

Set Up the Frontend on GitHub Pages

  • Ensure the frontend (static files) is inside the docs/ folder or a separate repo.
  • Configure the repository’s GitHub Pages settings to serve from docs/ or the main branch.
  • Update frontend API requests to use the correct backend URL (http://frostbyte.stu.nighthawkcodingsociety.com)

Domain & Traffic Management

  • Set up DNS records to point your custom domain and subdomain to GitHub Pages (frontend) and Cockpit (backend).
  • Configure HTTPS (SSL/TLS) for both frontend and backend using GitHub’s built-in SSL for Pages and Let’s Encrypt for the backend.

Monitoring & Updates

  • Use Cockpit’s logs and performance metrics to monitor the backend.
  • Automate deployment with GitHub Actions to update the backend when pushing new changes.

This setup ensures a smooth deployment process, with GitHub Pages serving the frontend and Docker + Cockpit managing the backend efficiently.

Frontend and Backend Setup: Port 8102

We have set the following configuration in config.yml in frontend:

```yml Server: https://frostbyte.stu.nighthawkcodingsociety.com Domain: stu.nighthawkcodingsociety.com Subdomain: frostbyte ```

In assets/api/config.js, we set:

export var pythonURI; if (location.hostname === "localhost" || location.hostname === "127.0.0.1") { pythonURI = "http://localhost:8102"; // Same URI for localhost or 127.0.0.1 } else { pythonURI = "https://frostbyte.stu.nighthawkcodingsociety.com"; }

In the backend, we set these configurations:

  • main.py

    if __name__ == "__main__": # change name for testing app.run(debug=True, host="0.0.0.0", port="8102")

  • Dockerfile

    <code> 
    FROM docker.io/python:3.11
    
    WORKDIR /
    RUN apt-get update && apt-get upgrade -y && \
        apt-get install -y python3 python3-pip git
    COPY . /
    
    RUN pip install --no-cache-dir -r requirements.txt
    RUN pip install gunicorn
    
    ENV GUNICORN_CMD_ARGS="--workers=1 --bind=0.0.0.0:8102"
    
    EXPOSE 8102
    ENV FLASK_ENV=deployment
    
    CMD [ "gunicorn", "main:app" ] 
    </code>
    
  • docker-compose.yml

    version: '3' services: web: image: frostbyte.stu build: . env_file: - .env ports: - "8102:8102" volumes: - ./instance:/instance restart: unless-stopped

  • nginx_file

    server { listen 80; listen [::]:80; server_name frostbyte.stu.nighthawkcodingsociety.com;

        location / {
            proxy_pass http://localhost:8102;
    
            # Preflighted requests
            if ($request_method = OPTIONS) {
                add_header "Access-Control-Allow-Credentials" "true" always;
                add_header "Access-Control-Allow-Origin"  "https://nighthawkcoders.github.io" always;
                add_header "Access-Control-Allow-Methods" "GET, POST, PUT, DELETE, OPTIONS, HEAD" always;
                add_header "Access-Control-Allow-MaxAge" 600 always;
                add_header "Access-Control-Allow-Headers" "Authorization, Origin, X-Origin, X-Requested-With, Content-Type, Accept" always;
                return 204;
            }
        }
    }   </code>
    

How We Will Set UP AWS:

  1. Login with provided credentials
  2. Clone our backend repo: (blackstar3092/frostbyte_backend)
  3. Our .env file will contain our passwords and Gemini API key, and we will initialize our database with ./scripts/db_init.py
  4. To build the docker environment: docker-compose build & docker-compose up -d
  5. Finally, we will test our server with curl docker ps # looking for port 8102 curl localhost:8102

Next, we will set up our domain on Route53 (domain will be frostbyte.stu.nighthawkcodingsociety.com) and test DNS through WSL.

Finally, we have already set up Nginx on our backend; we will work on setting it up on the AWS server to function with our backend database, as well as setting up Certbot for authentication.

Notes from Deployment Tech Talk

AWS EC2 Instance:

  • In hosted zones: domains are listed (the ones under stu.nighthawkcodingsociety.com is the place to create)
  • A-type DNS records: Map record name directly to an IP address
  • NS: maps to a name service (DNS Lookup sites) –> Database of names to IPs
  • IP address needed: 3.129.120.200
  • Nginx configuration file: listener for internet
  • We need a .env file (with passwords and our gemini key)
  • consider creating another user
  • Instance is kept outside of virtual machine so the database doesn’t clear upon every restart of the virtual machine