From f76982bb57cd45c5335796a4cbf0a5c3e8026d6c Mon Sep 17 00:00:00 2001 From: "A.Klivtsov" Date: Sun, 4 Jan 2026 23:01:46 +0300 Subject: [PATCH] microservices example --- Dockerfile.service1 | 20 +++ Dockerfile.service2 | 20 +++ README.md | 314 +++++++++++++++++++++++++++++++++++++++++++- docker-compose.yaml | 57 ++++++++ requirements.txt | 4 + service-2-main.py | 206 +++++++++++++++++++++++++++++ 6 files changed, 619 insertions(+), 2 deletions(-) create mode 100644 Dockerfile.service1 create mode 100644 Dockerfile.service2 create mode 100644 docker-compose.yaml create mode 100644 requirements.txt create mode 100644 service-2-main.py diff --git a/Dockerfile.service1 b/Dockerfile.service1 new file mode 100644 index 0000000..d91644d --- /dev/null +++ b/Dockerfile.service1 @@ -0,0 +1,20 @@ +# Используем official Python runtime как базовый образ +FROM python:3.11-slim + +# Устанавливаем рабочую директорию в контейнере +WORKDIR /app + +# Копируем файл зависимостей +COPY requirements.txt . + +# Устанавливаем зависимости +RUN pip install --no-cache-dir -r requirements.txt + +# Копируем код приложения +COPY service-1-main.py main.py + +# Экспонируем порт +EXPOSE 8001 + +# Команда для запуска приложения +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8001"] diff --git a/Dockerfile.service2 b/Dockerfile.service2 new file mode 100644 index 0000000..44bb209 --- /dev/null +++ b/Dockerfile.service2 @@ -0,0 +1,20 @@ +# Используем official Python runtime как базовый образ +FROM python:3.11-slim + +# Устанавливаем рабочую директорию в контейнере +WORKDIR /app + +# Копируем файл зависимостей +COPY requirements.txt . + +# Устанавливаем зависимости +RUN pip install --no-cache-dir -r requirements.txt + +# Копируем код приложения +COPY service-2-main.py main.py + +# Экспонируем порт +EXPOSE 8002 + +# Команда для запуска приложения +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8002"] diff --git a/README.md b/README.md index 3b3e982..5629860 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,313 @@ -# ApiGatewayTest +# Примеры тестирования микросервисов через API Gateway -Минимально жизнеспособный пример работы api gateway \ No newline at end of file +## Структура проекта + +``` +. +├── service-1-main.py # User Service +├── service-2-main.py # Order Service +├── requirements.txt # Зависимости +├── Dockerfile.service1 # Docker образ для Service 1 +├── Dockerfile.service2 # Docker образ для Service 2 +└── docker-compose.yaml # Конфигурация Docker Compose +``` + +## Запуск контейнеров + +```bash +# Построить образы и запустить контейнеры +docker-compose up --build + +# Запустить в фоновом режиме +docker-compose up -d --build + +# Просмотреть логи +docker-compose logs -f + +# Остановить контейнеры +docker-compose down +``` + +## Service 1 - User Service (порт 8001) + +### Health Check +```bash +curl http://localhost:8001/ +curl http://localhost:8001/health +``` + +### Получить список пользователей +```bash +curl http://localhost:8001/users +``` + +### Получить пользователя по ID +```bash +curl http://localhost:8001/users/1 +``` + +### Создать нового пользователя +```bash +curl -X POST http://localhost:8001/users \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Петр Иванов", + "email": "petr@example.com", + "age": 30 + }' +``` + +### Обновить пользователя +```bash +curl -X PUT http://localhost:8001/users/1 \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Иван Петров (обновлено)", + "email": "ivan.updated@example.com", + "age": 29 + }' +``` + +### Удалить пользователя +```bash +curl -X DELETE http://localhost:8001/users/1 +``` + +### Получить статистику +```bash +curl http://localhost:8001/stats +``` + +## Service 2 - Order Service (порт 8002) + +### Health Check +```bash +curl http://localhost:8002/ +curl http://localhost:8002/health +``` + +### Получить список продуктов +```bash +curl http://localhost:8002/products +``` + +### Получить продукт по ID +```bash +curl http://localhost:8002/products/1 +``` + +### Создать новый продукт +```bash +curl -X POST http://localhost:8002/products \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Мышка беспроводная", + "price": 1500.00, + "quantity": 20 + }' +``` + +### Обновить продукт +```bash +curl -X PUT http://localhost:8002/products/1 \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Ноутбук (обновлено)", + "price": 55000.00, + "quantity": 3 + }' +``` + +### Удалить продукт +```bash +curl -X DELETE http://localhost:8002/products/1 +``` + +### Получить список заказов +```bash +curl http://localhost:8002/orders +``` + +### Получить заказ по ID +```bash +curl http://localhost:8002/orders/1 +``` + +### Создать новый заказ +```bash +curl -X POST http://localhost:8002/orders \ + -H "Content-Type: application/json" \ + -d '{ + "user_id": 2, + "product_id": 2, + "quantity": 2 + }' +``` + +### Получить статистику +```bash +curl http://localhost:8002/stats +``` + +## Тестирование через Python requests + +```python +import requests +import json + +BASE_URL_SERVICE1 = "http://localhost:8001" +BASE_URL_SERVICE2 = "http://localhost:8002" + +# Проверка здоровья Service 1 +response = requests.get(f"{BASE_URL_SERVICE1}/health") +print(f"Service 1 Health: {response.json()}") + +# Получение всех пользователей +response = requests.get(f"{BASE_URL_SERVICE1}/users") +print(f"Users: {response.json()}") + +# Создание пользователя +user_data = { + "name": "Тест Пользователь", + "email": "test@example.com", + "age": 25 +} +response = requests.post(f"{BASE_URL_SERVICE1}/users", json=user_data) +print(f"Created User: {response.json()}") + +# Проверка здоровья Service 2 +response = requests.get(f"{BASE_URL_SERVICE2}/health") +print(f"Service 2 Health: {response.json()}") + +# Получение всех продуктов +response = requests.get(f"{BASE_URL_SERVICE2}/products") +print(f"Products: {response.json()}") + +# Создание заказа +order_data = { + "user_id": 1, + "product_id": 3, + "quantity": 5 +} +response = requests.post(f"{BASE_URL_SERVICE2}/orders", json=order_data) +print(f"Created Order: {response.json()}") +``` + +## Конфигурация API Gateway + +Для проверки работы API Gateway, вы можете использовать: + +1. **Kong** - популярный API Gateway +2. **NGINX** - простой и быстрый +3. **Traefik** - автоматическое обнаружение сервисов +4. **AWS API Gateway** - облачное решение + +### Пример конфигурации NGINX + +```nginx +upstream service1 { + server service-1:8001; +} + +upstream service2 { + server service-2:8002; +} + +server { + listen 8000; + server_name localhost; + + location /api/users { + proxy_pass http://service1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + location /api/products { + proxy_pass http://service2; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + location /api/orders { + proxy_pass http://service2; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } +} +``` + +## Основные характеристики микросервисов + +### Service 1 - User Service +- **Порт**: 8001 +- **Функционал**: Управление пользователями (CRUD) +- **Endpoints**: + - `GET /` - Проверка статуса + - `GET /health` - Health check + - `GET /users` - Список пользователей + - `GET /users/{id}` - Получить пользователя + - `POST /users` - Создать пользователя + - `PUT /users/{id}` - Обновить пользователя + - `DELETE /users/{id}` - Удалить пользователя + - `GET /stats` - Статистика + +### Service 2 - Order Service +- **Порт**: 8002 +- **Функционал**: Управление продуктами и заказами (CRUD) +- **Endpoints**: + - `GET /` - Проверка статуса + - `GET /health` - Health check + - `GET /products` - Список продуктов + - `GET /products/{id}` - Получить продукт + - `POST /products` - Создать продукт + - `PUT /products/{id}` - Обновить продукт + - `DELETE /products/{id}` - Удалить продукт + - `GET /orders` - Список заказов + - `GET /orders/{id}` - Получить заказ + - `POST /orders` - Создать заказ + - `GET /stats` - Статистика + +## Логирование и мониторинг + +Оба сервиса включают: +- Встроенное логирование (INFO уровень) +- Health check endpoints +- Метрики статистики +- Документацию Swagger (автоматическая генерация) + +### Доступ к Swagger документации + +- Service 1: `http://localhost:8001/docs` +- Service 2: `http://localhost:8002/docs` + +## Отладка + +```bash +# Проверить статус контейнеров +docker-compose ps + +# Просмотреть логи конкретного сервиса +docker-compose logs service-1 +docker-compose logs service-2 + +# Подключиться к контейнеру +docker-compose exec service-1 /bin/bash + +# Проверить сетевое подключение между сервисами +docker-compose exec service-1 curl http://service-2:8002/health +``` + +## Требования + +- Docker 20.10+ +- Docker Compose 2.0+ +- Python 3.11+ (для локального запуска без Docker) +- curl или PostMan для тестирования API + +## Лицензия + +MIT diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..bb75837 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,57 @@ +version: '3.8' + +services: + # Service 1 - User Service (порт 8001) + service-1: + build: + context: . + dockerfile: Dockerfile.service1 + container_name: user-service + ports: + - "8001:8001" + environment: + - SERVICE_NAME=user-service + - SERVICE_PORT=8001 + networks: + - microservices-network + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8001/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + labels: + service.type: "microservice" + service.name: "user-service" + service.port: "8001" + + # Service 2 - Order Service (порт 8002) + service-2: + build: + context: . + dockerfile: Dockerfile.service2 + container_name: order-service + ports: + - "8002:8002" + environment: + - SERVICE_NAME=order-service + - SERVICE_PORT=8002 + networks: + - microservices-network + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8002/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + labels: + service.type: "microservice" + service.name: "order-service" + service.port: "8002" + +networks: + microservices-network: + driver: bridge + name: microservices-network diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f2c118f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +fastapi==0.109.1 +uvicorn[standard]==0.27.0 +pydantic==2.5.3 +python-multipart==0.0.6 diff --git a/service-2-main.py b/service-2-main.py new file mode 100644 index 0000000..2529a80 --- /dev/null +++ b/service-2-main.py @@ -0,0 +1,206 @@ +""" +Второй микросервис - Service 2 +Предоставляет API для управления заказами и продуктами +""" +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel +from typing import List, Optional +from datetime import datetime +import logging + +app = FastAPI( + title="Service 2 - Order Service", + description="Микросервис для управления заказами и продуктами", + version="1.0.0" +) + +# Логирование +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Модели данных +class Product(BaseModel): + id: int + name: str + price: float + quantity: int + +class ProductCreate(BaseModel): + name: str + price: float + quantity: int + +class Order(BaseModel): + id: int + user_id: int + product_id: int + quantity: int + total_price: float + created_at: str + +class OrderCreate(BaseModel): + user_id: int + product_id: int + quantity: int + +# Имитация базы данных +products_db = { + 1: {"id": 1, "name": "Ноутбук", "price": 50000.00, "quantity": 5}, + 2: {"id": 2, "name": "Монитор", "price": 15000.00, "quantity": 10}, + 3: {"id": 3, "name": "Клавиатура", "price": 3000.00, "quantity": 25}, +} + +orders_db = { + 1: { + "id": 1, + "user_id": 1, + "product_id": 1, + "quantity": 1, + "total_price": 50000.00, + "created_at": "2025-01-01T10:00:00" + }, +} + +next_product_id = 4 +next_order_id = 2 + +# Маршруты +@app.get("/", tags=["Health"]) +async def root(): + """Проверка работоспособности Service 2""" + logger.info("Health check для Service 2") + return { + "service": "Service 2 - Order Service", + "status": "operational", + "version": "1.0.0" + } + +@app.get("/health", tags=["Health"]) +async def health_check(): + """Health check endpoint""" + return {"status": "healthy", "service": "order-service"} + +# Endpoints для продуктов +@app.get("/products", response_model=List[Product], tags=["Products"]) +async def list_products(skip: int = 0, limit: int = 10): + """Получить список всех продуктов""" + logger.info(f"Получение списка продуктов (skip={skip}, limit={limit})") + products_list = list(products_db.values()) + return products_list[skip:skip + limit] + +@app.get("/products/{product_id}", response_model=Product, tags=["Products"]) +async def get_product(product_id: int): + """Получить продукт по ID""" + logger.info(f"Получение продукта с ID: {product_id}") + if product_id not in products_db: + logger.warning(f"Продукт с ID {product_id} не найден") + raise HTTPException(status_code=404, detail="Продукт не найден") + return products_db[product_id] + +@app.post("/products", response_model=Product, tags=["Products"]) +async def create_product(product: ProductCreate): + """Создать новый продукт""" + global next_product_id + logger.info(f"Создание нового продукта: {product.name}") + + new_product = { + "id": next_product_id, + "name": product.name, + "price": product.price, + "quantity": product.quantity + } + products_db[next_product_id] = new_product + next_product_id += 1 + + logger.info(f"Продукт создан с ID: {new_product['id']}") + return new_product + +@app.put("/products/{product_id}", response_model=Product, tags=["Products"]) +async def update_product(product_id: int, product: ProductCreate): + """Обновить продукт""" + logger.info(f"Обновление продукта с ID: {product_id}") + if product_id not in products_db: + raise HTTPException(status_code=404, detail="Продукт не найден") + + updated_product = { + "id": product_id, + "name": product.name, + "price": product.price, + "quantity": product.quantity + } + products_db[product_id] = updated_product + return updated_product + +@app.delete("/products/{product_id}", tags=["Products"]) +async def delete_product(product_id: int): + """Удалить продукт""" + logger.info(f"Удаление продукта с ID: {product_id}") + if product_id not in products_db: + raise HTTPException(status_code=404, detail="Продукт не найден") + + del products_db[product_id] + return {"message": f"Продукт {product_id} удален"} + +# Endpoints для заказов +@app.get("/orders", response_model=List[Order], tags=["Orders"]) +async def list_orders(skip: int = 0, limit: int = 10): + """Получить список всех заказов""" + logger.info(f"Получение списка заказов (skip={skip}, limit={limit})") + orders_list = list(orders_db.values()) + return orders_list[skip:skip + limit] + +@app.get("/orders/{order_id}", response_model=Order, tags=["Orders"]) +async def get_order(order_id: int): + """Получить заказ по ID""" + logger.info(f"Получение заказа с ID: {order_id}") + if order_id not in orders_db: + logger.warning(f"Заказ с ID {order_id} не найден") + raise HTTPException(status_code=404, detail="Заказ не найден") + return orders_db[order_id] + +@app.post("/orders", response_model=Order, tags=["Orders"]) +async def create_order(order: OrderCreate): + """Создать новый заказ""" + global next_order_id + logger.info(f"Создание нового заказа для пользователя {order.user_id}") + + if order.product_id not in products_db: + raise HTTPException(status_code=404, detail="Продукт не найден") + + product = products_db[order.product_id] + if product["quantity"] < order.quantity: + raise HTTPException(status_code=400, detail="Недостаточно товара на складе") + + total_price = product["price"] * order.quantity + + new_order = { + "id": next_order_id, + "user_id": order.user_id, + "product_id": order.product_id, + "quantity": order.quantity, + "total_price": total_price, + "created_at": datetime.utcnow().isoformat() + } + + orders_db[next_order_id] = new_order + product["quantity"] -= order.quantity + next_order_id += 1 + + logger.info(f"Заказ создан с ID: {new_order['id']}") + return new_order + +@app.get("/stats", tags=["Stats"]) +async def get_stats(): + """Получить статистику по заказам""" + logger.info("Получение статистики") + total_revenue = sum(order["total_price"] for order in orders_db.values()) + return { + "total_orders": len(orders_db), + "total_products": len(products_db), + "total_revenue": total_revenue, + "service": "order-service" + } + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8002)