Skip to content

Commit

Permalink
Greeter Application added
Browse files Browse the repository at this point in the history
- README, application and tests added.
- Github Actions workflow added
  • Loading branch information
juanroldan1989 committed Jan 12, 2025
1 parent 5ce3e46 commit f9bdef2
Show file tree
Hide file tree
Showing 6 changed files with 367 additions and 0 deletions.
45 changes: 45 additions & 0 deletions .github/workflows/build-and-push-greeter.yaml
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
51 changes: 51 additions & 0 deletions greeter/Dockerfile
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"]
164 changes: 164 additions & 0 deletions greeter/README.md
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.
50 changes: 50 additions & 0 deletions greeter/app.py
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)
3 changes: 3 additions & 0 deletions greeter/requirements.txt
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
54 changes: 54 additions & 0 deletions greeter/test_app.py
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()

0 comments on commit f9bdef2

Please sign in to comment.