-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- README, application and tests added. - Github Actions workflow added
- Loading branch information
1 parent
5ce3e46
commit f9bdef2
Showing
6 changed files
with
367 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
name: Build and Push Docker Image | ||
|
||
on: | ||
push: | ||
paths: | ||
- 'greeter/**' # Trigger this workflow when changes occur in greeter directory. | ||
pull_request: | ||
paths: | ||
- 'greeter/**' # Trigger this workflow for pull requests involving greeter directory. | ||
|
||
jobs: | ||
build: | ||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
# Step 1: Check out the code from the repository | ||
- name: Checkout repository | ||
uses: actions/checkout@v3 | ||
|
||
# Step 2: Set up Docker Buildx | ||
- name: Set up Docker Buildx | ||
uses: docker/setup-buildx-action@v2 | ||
|
||
# Step 3: Log in to Docker Hub | ||
- name: Log in to Docker Hub | ||
uses: docker/login-action@v2 | ||
with: | ||
username: ${{ secrets.DOCKER_USERNAME }} | ||
password: ${{ secrets.DOCKER_PASSWORD }} | ||
|
||
# Step 4: Build and push Docker image using commit SHA as tag | ||
- name: Build and push Docker image | ||
run: | | ||
IMAGE_NAME="juanroldan1989/greeter" | ||
IMAGE_TAG=$(git rev-parse --short HEAD) # Use short commit SHA as tag | ||
echo "Building Docker image with tag $IMAGE_TAG" | ||
docker build -t $IMAGE_NAME:$IMAGE_TAG ./greeter | ||
echo "Pushing Docker image $IMAGE_NAME:$IMAGE_TAG" | ||
docker push $IMAGE_NAME:$IMAGE_TAG | ||
# Step 5: Clean up Docker images | ||
- name: Clean up Docker images | ||
run: docker rmi $IMAGE_NAME:$IMAGE_TAG |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
# Stage 1: Build stage | ||
FROM python:3.9-slim AS builder | ||
|
||
# Set the working directory | ||
WORKDIR /app | ||
|
||
# Install build dependencies for psycopg2 and other compiled libraries | ||
RUN apt-get update && apt-get install -y \ | ||
curl \ | ||
gcc \ | ||
libpq-dev \ | ||
build-essential \ | ||
&& rm -rf /var/lib/apt/lists/* | ||
|
||
# Copy the requirements file (`greeter` folder) and install dependencies | ||
COPY requirements.txt /app/requirements.txt | ||
RUN pip install --no-cache-dir --prefix=/install -r /app/requirements.txt | ||
|
||
# Stage 2: Final stage with only the necessary components | ||
FROM python:3.9-slim | ||
|
||
# Set the working directory | ||
WORKDIR /app | ||
|
||
# Install runtime dependencies (including `curl` for health-check of Flask app) | ||
RUN apt-get update && apt-get install -y libpq5 curl \ | ||
&& rm -rf /var/lib/apt/lists/* | ||
|
||
# Create a non-root user and group, and set permissions | ||
RUN useradd -m -d /home/nonrootuser nonrootuser | ||
USER nonrootuser | ||
|
||
# Copy only the installed dependencies from the builder stage, adjust ownership | ||
COPY --from=builder --chown=nonrootuser:nonrootuser /install /usr/local | ||
|
||
# Copy the application code and set correct ownership and permissions | ||
COPY --chown=nonrootuser:nonrootuser . /app | ||
|
||
# Set environment variables | ||
ENV FLASK_APP=app.py | ||
ENV REDIS_HOST=redis | ||
ENV REDIS_PORT=6379 | ||
|
||
# Expose the port the app runs on | ||
EXPOSE 5000 | ||
|
||
# Use non-root user to run the application | ||
USER nonrootuser | ||
|
||
# Run the Flask application | ||
CMD ["flask", "run", "--host=0.0.0.0", "--port=5000"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
# Greeter App | ||
|
||
This is a Flask-based microservice that communicates with two other services, | ||
|
||
**Name API** and **Greeting API**, | ||
|
||
to generate a personalized greeting message. | ||
|
||
## Features | ||
|
||
- Fetches a random name from the **Name Service**. | ||
- Fetches a random greeting from the **Greeting Service**. | ||
- Combines the name and greeting to return a friendly message. | ||
- Health check endpoint for monitoring the API status. | ||
- Includes logging for better error tracking and debugging. | ||
- Lightweight, containerized, and easy to deploy. | ||
|
||
## Endpoints | ||
|
||
### `GET /greet` | ||
|
||
Fetches a greeting and name from the external services and returns a combined message. | ||
|
||
- Response: | ||
|
||
```bash | ||
{ | ||
"message": "Hello, Alice!" | ||
} | ||
``` | ||
|
||
### `GET /health` | ||
|
||
Returns the health status of the service. | ||
|
||
- Example Response: | ||
|
||
```bash | ||
{ | ||
"status": "healthy" | ||
} | ||
``` | ||
|
||
## How to Run | ||
|
||
1. Clone the repository: | ||
|
||
```bash | ||
git clone <repository-url> | ||
cd <repository-directory> | ||
``` | ||
|
||
2. Create a virtual environment and install dependencies: | ||
|
||
```bash | ||
python3 -m venv venv | ||
source venv/bin/activate # On Windows: venv\Scripts\activate | ||
pip install flask | ||
``` | ||
|
||
3. Run the application: | ||
|
||
```bash | ||
python app.py | ||
``` | ||
|
||
4. Access the API: | ||
|
||
Open your browser or a tool like curl or Postman and navigate to: | ||
|
||
```bash | ||
http://localhost:5000 | ||
``` | ||
|
||
## Environment Variables | ||
|
||
- `NAME_SERVICE_URL`: URL for the name generator service. Default is http://name:5001/name. | ||
- `GREETING_SERVICE_URL`: URL for the greeting service. Default is http://greeting:5002/greeting. | ||
|
||
Example: | ||
|
||
```bash | ||
export NAME_SERVICE_URL="http://localhost:5001/name" | ||
export GREETING_SERVICE_URL="http://localhost:5002/greeting" | ||
``` | ||
|
||
## Deployment | ||
|
||
### Docker | ||
|
||
- Build and run the Docker container: | ||
|
||
```bash | ||
docker build -t name-api . | ||
docker run -d -p 5001:5001 name-api | ||
|
||
docker build -t greeting-api . | ||
docker run -d -p 5002:5002 greeting-api | ||
|
||
docker build -t greeter-api . | ||
docker run -d -p 5000:5000 greeter-api | ||
``` | ||
|
||
- Access the API at http://localhost:5000 | ||
|
||
### Using ECS and EKS | ||
|
||
This Flask app is designed to be easily deployable in AWS ECS and EKS clusters. | ||
|
||
Here's an overview of how to deploy it: | ||
|
||
- ECS Deployment: | ||
|
||
1. Create a Docker image of the app and push it to a container registry like Docker Hub or Amazon Elastic Container Registry (ECR). | ||
2. Use Terraform or AWS CloudFormation to define the ECS task and service. | ||
3. Deploy the app as an ECS service behind an Application Load Balancer (ALB) to ensure high availability. | ||
|
||
- EKS Deployment: | ||
|
||
1. Build and push the Docker image to a container registry. | ||
2. Define a Kubernetes deployment and service YAML file to deploy the app in the EKS cluster. | ||
3. Use kubectl to apply the deployment and service definitions. | ||
4. Expose the service using a LoadBalancer service type or an Ingress resource. | ||
|
||
## Unit Tests | ||
|
||
Unit tests are provided to ensure the correctness of the API. The tests cover the following: | ||
|
||
- Status code verification. | ||
- JSON content type verification. | ||
- Proper handling of external service failures. | ||
- Ensuring the response contains a valid farewell phrase. | ||
|
||
### How to Run Unit Tests | ||
|
||
Ensure you have Python and the required dependencies installed: | ||
|
||
```bash | ||
pip install -r requirements.txt | ||
``` | ||
|
||
Run the unit tests: | ||
|
||
```bash | ||
python -m unittest test_app.py | ||
``` | ||
|
||
### Test Coverage | ||
|
||
The following tests are included: | ||
|
||
- `test_greet_status_code`: Verifies that the `/greet` endpoint returns a 200 status code. | ||
- `test_greet_content_type`: Ensures the response has a content type of `application/json`. | ||
- `test_greet_response_format`: Verifies that the response contains a `message` key. | ||
- `test_health_status_code`: Verifies that the `/health` endpoint returns a `200` status code. | ||
- `test_health_response_format`: Ensures that the `/health` endpoint returns the correct status in JSON format. | ||
|
||
## Contributing | ||
|
||
Contributions are welcome! Feel free to open an issue or submit a pull request if you have any improvements or suggestions. | ||
|
||
## License | ||
|
||
This project is licensed under the MIT License. See the LICENSE file for more details. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
from flask import Flask, jsonify | ||
import requests | ||
import logging | ||
import os | ||
|
||
app = Flask(__name__) | ||
|
||
# Set up logging | ||
logging.basicConfig( | ||
level=logging.INFO, | ||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" | ||
) | ||
logger = logging.getLogger(__name__) | ||
|
||
# External service URLs (configurable via environment variables) | ||
NAME_SERVICE_URL = os.getenv("NAME_SERVICE_URL", "http://name:5001/name") | ||
GREETING_SERVICE_URL = os.getenv("GREETING_SERVICE_URL", "http://greeting:5002/greeting") | ||
|
||
app = Flask(__name__) | ||
|
||
@app.route('/greet', methods=['GET']) | ||
def greet(): | ||
try: | ||
# Fetch name and greeting with timeouts | ||
name_response = requests.get(NAME_SERVICE_URL, timeout=5) | ||
greeting_response = requests.get(GREETING_SERVICE_URL, timeout=5) | ||
|
||
# Raise exceptions for HTTP errors | ||
name_response.raise_for_status() | ||
greeting_response.raise_for_status() | ||
|
||
# Parse JSON responses | ||
name = name_response.json().get("name") | ||
greeting = greeting_response.json().get("greeting") | ||
|
||
return jsonify(message=f"{greeting}, {name}!") | ||
|
||
except requests.RequestException as e: | ||
logger.error(f"Failed to fetch data from external services: {str(e)}") | ||
return jsonify(error=f"Failed to fetch data from external services: {str(e)}"), 503 | ||
except Exception as e: | ||
logger.error(f"An unexpected error occurred: {str(e)}") | ||
return jsonify(error=f"An unexpected error occurred: {str(e)}"), 500 | ||
|
||
@app.route('/health', methods=['GET']) | ||
def health(): | ||
return jsonify(status='healthy') | ||
|
||
if __name__ == '__main__': | ||
app.run(host='0.0.0.0', port=5000) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
Flask==3.0.3 | ||
python-dotenv==1.0.1 | ||
requests==2.26.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import unittest | ||
from unittest.mock import patch | ||
import json | ||
from app import app | ||
|
||
class GreeterServiceTestCase(unittest.TestCase): | ||
def setUp(self): | ||
"""Set up the test client before each test.""" | ||
self.app = app.test_client() | ||
self.app.testing = True | ||
|
||
@patch('requests.get') | ||
def test_greet_with_mocked_services(self, mock_get): | ||
"""Test the /greet endpoint with mocked external services.""" | ||
mock_get.side_effect = [ | ||
unittest.mock.Mock(status_code=200, json=lambda: {'name': 'Alice'}), # Mock for name service | ||
unittest.mock.Mock(status_code=200, json=lambda: {'greeting': 'Hello'}) # Mock for greeting service | ||
] | ||
|
||
response = self.app.get('/greet') | ||
data = json.loads(response.data) | ||
|
||
self.assertEqual(response.status_code, 200) | ||
self.assertEqual(data['message'], 'Hello, Alice!') | ||
|
||
def test_greet_status_code(self): | ||
"""Test that the /greet endpoint returns a 200 status code.""" | ||
response = self.app.get('/greet') | ||
self.assertEqual(response.status_code, 200) | ||
|
||
def test_greet_content_type(self): | ||
"""Test that the /greet endpoint returns JSON content.""" | ||
response = self.app.get('/greet') | ||
self.assertEqual(response.content_type, 'application/json') | ||
|
||
def test_greet_response_format(self): | ||
"""Test that the /greet endpoint contains a 'message' key.""" | ||
response = self.app.get('/greet') | ||
data = json.loads(response.data) | ||
self.assertIn('message', data) | ||
|
||
def test_health_status_code(self): | ||
"""Test that the /health endpoint returns a 200 status code.""" | ||
response = self.app.get('/health') | ||
self.assertEqual(response.status_code, 200) | ||
|
||
def test_health_response_format(self): | ||
"""Test that the /health endpoint contains a 'status' key.""" | ||
response = self.app.get('/health') | ||
data = json.loads(response.data) | ||
self.assertIn('status', data) | ||
|
||
if __name__ == '__main__': | ||
unittest.main() |