From 33a771fd966e2f800f22471f388d4032ea4cb656 Mon Sep 17 00:00:00 2001 From: aakash16 Date: Tue, 2 Sep 2025 23:58:41 +0530 Subject: [PATCH 01/10] add gitignore --- .idea/.gitignore | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .idea/.gitignore diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml From 589f61f8545c9ce25b0d2e8c1aa423f193e730de Mon Sep 17 00:00:00 2001 From: aakash16 Date: Tue, 2 Sep 2025 23:59:03 +0530 Subject: [PATCH 02/10] add gitignore workflow --- .github/workflows/.gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/workflows/.gitignore diff --git a/.github/workflows/.gitignore b/.github/workflows/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.github/workflows/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file From ba008e3bc6649f1181993bdb2a454f2704ec0102 Mon Sep 17 00:00:00 2001 From: aakash16 Date: Tue, 2 Sep 2025 23:59:38 +0530 Subject: [PATCH 03/10] add Dockerfile --- Dockerfile | 18 ++++++++++++++++++ app.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..961881c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +# Use official Python base image +FROM python:3.11-slim + +# Set working directory +WORKDIR /app + +# Copy requirements and install +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Expose app port +EXPOSE 8080 + +# Run the app +CMD ["python", "app.py"] diff --git a/app.py b/app.py index 997d548..8a6debe 100644 --- a/app.py +++ b/app.py @@ -134,4 +134,4 @@ def post(self): api.add_resource(UppercaseText, "/uppercase") if __name__ == "__main__": - app.run(debug=True) \ No newline at end of file + app.run(host="0.0.0.0", port=8080, debug=False) \ No newline at end of file From dd7057e09ab8f665617ee0cdb5dc57b83823cf3c Mon Sep 17 00:00:00 2001 From: aakash16 Date: Wed, 3 Sep 2025 00:03:25 +0530 Subject: [PATCH 04/10] add ci workflow --- .github/workflows/ci.yml | 44 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..69713e3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,44 @@ +name: CI - Build and Push Docker Images to ECR (Test & Prod) + +on: + push: + branches: + - main + workflow_dispatch: # allows manual trigger too + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Log in to Amazon ECR + uses: aws-actions/amazon-ecr-login@v2 + + - name: Build and push Docker image to TEST ECR + run: | + IMAGE_TAG=${GITHUB_SHA} + ECR_REGISTRY=${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com + ECR_REPOSITORY_TEST=${{ secrets.ECR_REPOSITORY_TEST }} + + docker build -t $ECR_REGISTRY/$ECR_REPOSITORY_TEST:$IMAGE_TAG . + docker push $ECR_REGISTRY/$ECR_REPOSITORY_TEST:$IMAGE_TAG + + - name: Build and push Docker image to PROD ECR (only on manual trigger) + if: github.event_name == 'workflow_dispatch' + run: | + IMAGE_TAG=${GITHUB_SHA} + ECR_REGISTRY=${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com + ECR_REPOSITORY_PROD=${{ secrets.ECR_REPOSITORY_PROD }} + + docker build -t $ECR_REGISTRY/$ECR_REPOSITORY_PROD:$IMAGE_TAG . + docker push $ECR_REGISTRY/$ECR_REPOSITORY_PROD:$IMAGE_TAG From e3ab8eca0821c8a4cb2502143f6d183510bd9ab2 Mon Sep 17 00:00:00 2001 From: aakash16 Date: Wed, 3 Sep 2025 00:57:31 +0530 Subject: [PATCH 05/10] Update workflow to push latest tag for test and prod --- .github/workflows/ci.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69713e3..4c8ad2f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,7 @@ on: push: branches: - main - workflow_dispatch: # allows manual trigger too + workflow_dispatch: # allows manual trigger for prod deploy jobs: build: @@ -30,8 +30,15 @@ jobs: ECR_REGISTRY=${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com ECR_REPOSITORY_TEST=${{ secrets.ECR_REPOSITORY_TEST }} + # Build with commit SHA docker build -t $ECR_REGISTRY/$ECR_REPOSITORY_TEST:$IMAGE_TAG . + + # Tag as latest + docker tag $ECR_REGISTRY/$ECR_REPOSITORY_TEST:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY_TEST:latest + + # Push both docker push $ECR_REGISTRY/$ECR_REPOSITORY_TEST:$IMAGE_TAG + docker push $ECR_REGISTRY/$ECR_REPOSITORY_TEST:latest - name: Build and push Docker image to PROD ECR (only on manual trigger) if: github.event_name == 'workflow_dispatch' @@ -40,5 +47,12 @@ jobs: ECR_REGISTRY=${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com ECR_REPOSITORY_PROD=${{ secrets.ECR_REPOSITORY_PROD }} + # Build with commit SHA docker build -t $ECR_REGISTRY/$ECR_REPOSITORY_PROD:$IMAGE_TAG . + + # Tag as latest + docker tag $ECR_REGISTRY/$ECR_REPOSITORY_PROD:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY_PROD:latest + + # Push both docker push $ECR_REGISTRY/$ECR_REPOSITORY_PROD:$IMAGE_TAG + docker push $ECR_REGISTRY/$ECR_REPOSITORY_PROD:latest From e32cd28dcf5d2accb2a9528482ab28cd1c310ce1 Mon Sep 17 00:00:00 2001 From: aakash16 Date: Wed, 3 Sep 2025 01:13:03 +0530 Subject: [PATCH 06/10] Debug docker image tags before push --- .github/workflows/ci.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c8ad2f..1a4ee50 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,10 +33,13 @@ jobs: # Build with commit SHA docker build -t $ECR_REGISTRY/$ECR_REPOSITORY_TEST:$IMAGE_TAG . - # Tag as latest + # Tag same image as latest docker tag $ECR_REGISTRY/$ECR_REPOSITORY_TEST:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY_TEST:latest - # Push both + # Debug + docker images + + # Push both tags docker push $ECR_REGISTRY/$ECR_REPOSITORY_TEST:$IMAGE_TAG docker push $ECR_REGISTRY/$ECR_REPOSITORY_TEST:latest @@ -50,9 +53,9 @@ jobs: # Build with commit SHA docker build -t $ECR_REGISTRY/$ECR_REPOSITORY_PROD:$IMAGE_TAG . - # Tag as latest + # Tag same image as latest docker tag $ECR_REGISTRY/$ECR_REPOSITORY_PROD:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY_PROD:latest - # Push both + # Push both tags docker push $ECR_REGISTRY/$ECR_REPOSITORY_PROD:$IMAGE_TAG docker push $ECR_REGISTRY/$ECR_REPOSITORY_PROD:latest From 2840540b1a24eb91aaf8b1858559938f6cd5fdcb Mon Sep 17 00:00:00 2001 From: aakash16 Date: Wed, 3 Sep 2025 01:17:26 +0530 Subject: [PATCH 07/10] Debugging ci --- .github/workflows/ci.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a4ee50..74567ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,14 +30,16 @@ jobs: ECR_REGISTRY=${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com ECR_REPOSITORY_TEST=${{ secrets.ECR_REPOSITORY_TEST }} + echo "Building image with tag: $IMAGE_TAG" + # Build with commit SHA docker build -t $ECR_REGISTRY/$ECR_REPOSITORY_TEST:$IMAGE_TAG . - # Tag same image as latest + # Tag as latest docker tag $ECR_REGISTRY/$ECR_REPOSITORY_TEST:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY_TEST:latest # Debug - docker images + docker images | grep $ECR_REPOSITORY_TEST # Push both tags docker push $ECR_REGISTRY/$ECR_REPOSITORY_TEST:$IMAGE_TAG From 7446ee816eec16ad51164956904311110edb8f93 Mon Sep 17 00:00:00 2001 From: aakash16 Date: Wed, 3 Sep 2025 01:22:20 +0530 Subject: [PATCH 08/10] Refactor ci jobs --- .github/workflows/ci.yml | 47 ++++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74567ae..a9950f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,40 +24,59 @@ jobs: - name: Log in to Amazon ECR uses: aws-actions/amazon-ecr-login@v2 - - name: Build and push Docker image to TEST ECR + # ----------------------------- + # TEST ENVIRONMENT + # ----------------------------- + + - name: Build Docker image for TEST (SHA) run: | IMAGE_TAG=${GITHUB_SHA} ECR_REGISTRY=${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com ECR_REPOSITORY_TEST=${{ secrets.ECR_REPOSITORY_TEST }} - - echo "Building image with tag: $IMAGE_TAG" - - # Build with commit SHA docker build -t $ECR_REGISTRY/$ECR_REPOSITORY_TEST:$IMAGE_TAG . - # Tag as latest + - name: Tag Docker image for TEST as latest + run: | + IMAGE_TAG=${GITHUB_SHA} + ECR_REGISTRY=${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com + ECR_REPOSITORY_TEST=${{ secrets.ECR_REPOSITORY_TEST }} docker tag $ECR_REGISTRY/$ECR_REPOSITORY_TEST:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY_TEST:latest - - # Debug docker images | grep $ECR_REPOSITORY_TEST - # Push both tags + - name: Push Docker image to TEST ECR (SHA and latest) + run: | + IMAGE_TAG=${GITHUB_SHA} + ECR_REGISTRY=${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com + ECR_REPOSITORY_TEST=${{ secrets.ECR_REPOSITORY_TEST }} docker push $ECR_REGISTRY/$ECR_REPOSITORY_TEST:$IMAGE_TAG docker push $ECR_REGISTRY/$ECR_REPOSITORY_TEST:latest - - name: Build and push Docker image to PROD ECR (only on manual trigger) + # ----------------------------- + # PROD ENVIRONMENT (manual trigger) + # ----------------------------- + + - name: Build Docker image for PROD (SHA) if: github.event_name == 'workflow_dispatch' run: | IMAGE_TAG=${GITHUB_SHA} ECR_REGISTRY=${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com ECR_REPOSITORY_PROD=${{ secrets.ECR_REPOSITORY_PROD }} - - # Build with commit SHA docker build -t $ECR_REGISTRY/$ECR_REPOSITORY_PROD:$IMAGE_TAG . - # Tag same image as latest + - name: Tag Docker image for PROD as latest + if: github.event_name == 'workflow_dispatch' + run: | + IMAGE_TAG=${GITHUB_SHA} + ECR_REGISTRY=${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com + ECR_REPOSITORY_PROD=${{ secrets.ECR_REPOSITORY_PROD }} docker tag $ECR_REGISTRY/$ECR_REPOSITORY_PROD:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY_PROD:latest + docker images | grep $ECR_REPOSITORY_PROD - # Push both tags + - name: Push Docker image to PROD ECR (SHA and latest) + if: github.event_name == 'workflow_dispatch' + run: | + IMAGE_TAG=${GITHUB_SHA} + ECR_REGISTRY=${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com + ECR_REPOSITORY_PROD=${{ secrets.ECR_REPOSITORY_PROD }} docker push $ECR_REGISTRY/$ECR_REPOSITORY_PROD:$IMAGE_TAG docker push $ECR_REGISTRY/$ECR_REPOSITORY_PROD:latest From bbfddebff809dd6eb0cb749565ef8f3e7ec0ce89 Mon Sep 17 00:00:00 2001 From: aakash16 Date: Wed, 3 Sep 2025 11:16:18 +0530 Subject: [PATCH 09/10] Add root path --- app.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app.py b/app.py index 8a6debe..b4e20c5 100644 --- a/app.py +++ b/app.py @@ -133,5 +133,9 @@ def post(self): api.add_resource(Records, "/records") api.add_resource(UppercaseText, "/uppercase") +@app.route("/") +def health(): + return "OK", 200 + if __name__ == "__main__": app.run(host="0.0.0.0", port=8080, debug=False) \ No newline at end of file From 44fa8f13db5375d477990cdfc8c170a63e5c9f33 Mon Sep 17 00:00:00 2001 From: aakash16 Date: Wed, 3 Sep 2025 15:42:21 +0530 Subject: [PATCH 10/10] Refactor app and dockerfile --- Dockerfile | 28 +++++++--- app.py | 147 ++++++++++++++--------------------------------------- 2 files changed, 57 insertions(+), 118 deletions(-) diff --git a/Dockerfile b/Dockerfile index 961881c..51e7874 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,30 @@ -# Use official Python base image -FROM python:3.11-slim +# Stage 1: Base image with dependencies +FROM python:3.11-slim AS base -# Set working directory WORKDIR /app -# Copy requirements and install +# Install system deps (if you ever need psycopg2, mysqlclient, etc.) +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +# Install Python dependencies COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt +RUN pip install --no-cache-dir -r requirements.txt gunicorn + +# Stage 2: Final runtime image +FROM python:3.11-slim AS final + +WORKDIR /app + +# Copy installed dependencies from base +COPY --from=base /usr/local /usr/local # Copy application code COPY . . -# Expose app port +# Expose container port EXPOSE 8080 -# Run the app -CMD ["python", "app.py"] +# Run with Gunicorn (production-ready WSGI server) +CMD ["gunicorn", "--bind", "0.0.0.0:8080", "app:app"] diff --git a/app.py b/app.py index b4e20c5..3ba92ac 100644 --- a/app.py +++ b/app.py @@ -1,141 +1,68 @@ from flask import Flask, jsonify, request from flask_restful import Api, Resource from flasgger import Swagger - import book_review +import logging +import signal, sys app = Flask(__name__) api = Api(app) swagger = Swagger(app) +# Logging setup +logging.basicConfig(level=logging.INFO) + +# Graceful shutdown for ECS +def handle_sigterm(*args): + app.logger.info("Received SIGTERM, shutting down gracefully...") + sys.exit(0) + +signal.signal(signal.SIGTERM, handle_sigterm) + + class UppercaseText(Resource): def get(self): - """ - This method responds to the GET request for this endpoint and returns the data in uppercase. - --- - tags: - - Text Processing - parameters: - - name: text - in: query - type: string - required: true - description: The text to be converted to uppercase - responses: - 200: - description: A successful GET request - content: - application/json: - schema: - type: object - properties: - text: - type: string - description: The text in uppercase - """ text = request.args.get('text') - + if not text: + return {"message": "Missing 'text' parameter"}, 400 return jsonify({"text": text.upper()}) - + + class Records(Resource): def get(self): - """ - This method responds to the GET request for returning a number of books. - --- - tags: - - Records - parameters: - - name: count - in: query - type: integer - required: false - description: The number of books to return - - name: sort - in: query - type: string - enum: ['ASC', 'DESC'] - required: false - description: Sort order for the books - responses: - 200: - description: A successful GET request - schema: - type: object - properties: - books: - type: array - items: - type: object - properties: - title: - type: string - description: The title of the book - author: - type: string - description: The author of the book - """ - - count = request.args.get('count') # Default to returning 10 books if count is not provided + count = request.args.get('count') sort = request.args.get('sort') + try: + books = book_review.get_all_records(count=count, sort=sort) + except Exception as e: + app.logger.error(f"Error fetching records: {e}") + return {"error": "Internal Server Error"}, 500 + return {"books": books}, 200 - # Get all the books - books = book_review.get_all_records(count=count, sort=sort) - return {"books": books}, 200 - class AddRecord(Resource): def post(self): - """ - This method responds to the POST request for adding a new record to the DB table. - --- - tags: - - Records - parameters: - - in: body - name: body - required: true - schema: - id: BookReview - required: - - Book - - Rating - properties: - Book: - type: string - description: the name of the book - Rating: - type: integer - description: the rating of the book (1-10) - responses: - 200: - description: A successful POST request - 400: - description: Bad request, missing 'Book' or 'Rating' in the request body - """ - - data = request.json - print(data) - - # Check if 'Book' and 'Rating' are present in the request body + data = request.json or {} if 'Book' not in data or 'Rating' not in data: - return {"message": "Bad request, missing 'Book' or 'Rating' in the request body"}, 400 - # Call the add_record function to add the record to the DB table - success = book_review.add_record(data) - - if success: - return {"message": "Record added successfully"}, 200 - else: + return {"message": "Bad request, missing 'Book' or 'Rating'"}, 400 + try: + success = book_review.add_record(data) + except Exception as e: + app.logger.error(f"Error adding record: {e}") return {"message": "Failed to add record"}, 500 - + return {"message": "Record added successfully"} if success else {"message": "Failed to add record"}, 200 api.add_resource(AddRecord, "/add-record") api.add_resource(Records, "/records") api.add_resource(UppercaseText, "/uppercase") -@app.route("/") + +@app.route("/health") def health(): - return "OK", 200 + return jsonify(status="healthy"), 200 + +# Only for local dev (Gunicorn runs in ECS) if __name__ == "__main__": - app.run(host="0.0.0.0", port=8080, debug=False) \ No newline at end of file + app.run(host="0.0.0.0", port=8080, debug=False)