zhirnova_alyona_lab_3
This commit is contained in:
27
zhirnova_alyona_lab_3/Docker-compose.yaml
Normal file
27
zhirnova_alyona_lab_3/Docker-compose.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
services:
|
||||
|
||||
producer_service:
|
||||
container_name: producer_service
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./producer_service/Dockerfile
|
||||
expose:
|
||||
- 8080
|
||||
|
||||
cartoon_service:
|
||||
container_name: cartoon_service
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./cartoon_service/Dockerfile
|
||||
expose:
|
||||
- 8081
|
||||
|
||||
nginx:
|
||||
image: nginx:latest
|
||||
ports:
|
||||
- "80:80"
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf
|
||||
depends_on:
|
||||
- producer_service
|
||||
- cartoon_service
|
||||
70
zhirnova_alyona_lab_3/README.md
Normal file
70
zhirnova_alyona_lab_3/README.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Лабораторная работа №3 - REST API, Gateway и синхронный обмен между микросервисами
|
||||
|
||||
## Цель
|
||||
изучение шаблона проектирования gateway, построения синхронного обмена между микросервисами и архитектурного стиля RESTful API.
|
||||
|
||||
### Задачи
|
||||
1. Создать 2 микросервиса, реализующих CRUD на связанных сущностях.
|
||||
2. Реализовать механизм синхронного обмена сообщениями между микросервисами.
|
||||
3. Реализовать шлюз на основе прозрачного прокси-сервера nginx.
|
||||
4. Оформить отчёт в формате Markdown
|
||||
|
||||
### Инструменты для выполнения:
|
||||
1. Docker
|
||||
2. Docker Compose
|
||||
3. Python
|
||||
4. Postman
|
||||
|
||||
## Компоненты системы:
|
||||
1. Cartoon Service (cartoon_service/) - сервис для управления мультфильмами
|
||||
2. Producer Service (producer_service/) - сервис для управления режиссерами
|
||||
3. Nginx - обратный прокси и балансировщик нагрузки
|
||||
4. Docker Compose - оркестрация контейнеров
|
||||
|
||||
## API Endpoints:
|
||||
GET / - Получить все мультфильмы
|
||||
GET /{cartoon_uuid} - Получить мультфильм по ID
|
||||
GET /full - Получить все мультфильмы с информацией о продюсерах
|
||||
GET /full/{cartoon_uuid} - Получить мультфильм с информацией о продюсере
|
||||
GET /by-producer/{producer_uuid} - Получить мультфильмы по продюсеру
|
||||
POST / - Создать новый мультфильм
|
||||
PUT /{cartoon_uuid} - Обновить мультфильм
|
||||
DELETE /{cartoon_uuid} - Удалить мультфильм
|
||||
|
||||
GET / - Получить всех режиссеров
|
||||
GET /{producer_uuid} - Получить режиссера по ID
|
||||
GET /with-cartoons/{producer_uuid} - Получить режиссера с его мультфильмами
|
||||
GET /check/{producer_uuid} - Проверить существование режиссера
|
||||
POST / - Создать нового режиссера
|
||||
PUT /{producer_uuid} - Обновить режиссера
|
||||
DELETE /{producer_uuid} - Удалить режиссера
|
||||
|
||||
Пример POST http://localhost/cartoon_service/:
|
||||
```json
|
||||
{
|
||||
"title": "Король Лев",
|
||||
"year": 1994,
|
||||
"producer_id": "997aa4c5-ebb2-4794-ba81-e742f9f1fa30"
|
||||
}
|
||||
```
|
||||
|
||||
## Особенности реализации:
|
||||
1. Микросервисная архитектура - каждый сервис независим и выполняет одну бизнес-функцию
|
||||
2. Взаимодействие через HTTP - сервисы общаются через REST API вызовы
|
||||
3. Контейнеризация - все компоненты запускаются в Docker контейнерах
|
||||
4. Автоматическая сборка - Docker Compose автоматически собирает и запускает все сервисы
|
||||
5. Единая точка входа - Nginx обеспечивает единый endpoint для всех сервисов
|
||||
|
||||
## Запуск:
|
||||
1. На компьютере должны быть установлены Docker и Docker Compose
|
||||
2. Скачайте папку с репозитория
|
||||
3. Перейдите в директорию с yml
|
||||
4. Запустите с помощью команды:
|
||||
`docker-compose up --build`
|
||||
5. Откройте Postman
|
||||
|
||||
## Что делает данная лабораторная работа:
|
||||
Проект представляет собой микросервисное приложение для управления базой данных мультфильмов и их режиссеров. Система состоит из двух независимых сервисов, взаимодействующих через REST API, и обратного прокси на базе Nginx для маршрутизации запросов.
|
||||
|
||||
## Видео:
|
||||
https://rutube.ru/video/private/22f82905c7e737e2863bd9726cedf3e5/?p=FkA762T6Z6K0TDyHZ5Yaeg
|
||||
11
zhirnova_alyona_lab_3/cartoon_service/Dockerfile
Normal file
11
zhirnova_alyona_lab_3/cartoon_service/Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY cartoon_service/cartoon_service.py .
|
||||
|
||||
CMD ["python", "cartoon_service.py"]
|
||||
94
zhirnova_alyona_lab_3/cartoon_service/cartoon_service.py
Normal file
94
zhirnova_alyona_lab_3/cartoon_service/cartoon_service.py
Normal file
@@ -0,0 +1,94 @@
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
from uuid import UUID, uuid4
|
||||
import requests
|
||||
import uvicorn
|
||||
|
||||
|
||||
app = FastAPI(root_path="/cartoon_service", title="Cartoon Service", description="Service to manage cartoons and their producers", version="1.0.0")
|
||||
|
||||
producers_url = "http://producer_service:8080/"
|
||||
|
||||
class Cartoon(BaseModel):
|
||||
uuid: UUID
|
||||
title: str
|
||||
year: int
|
||||
producer_id: UUID
|
||||
|
||||
class CartoonCreate(BaseModel):
|
||||
title: str
|
||||
year: int
|
||||
producer_id: UUID
|
||||
|
||||
cartoons: List[Cartoon] = [
|
||||
Cartoon(uuid=UUID("89fa1e7a-7e88-445e-a4d8-6d4497ea8f19"), title="Большое путешествие", year=2006, producer_id=UUID("eb815350-c7b9-4446-8434-4c0640c21995")),
|
||||
Cartoon(uuid=UUID("0351ee11-f11b-4d83-b2c8-1075b0c357dc"), title="Ходячий замок", year=2004, producer_id=UUID("997aa4c5-ebb2-4794-ba81-e742f9f1fa30")),
|
||||
Cartoon(uuid=UUID("dfc17619-7690-47aa-ae8e-6a5068f8ddec"), title="Горбун из Нотр-Дама", year=1996, producer_id=UUID("694827e4-0f93-45a5-8f75-bad7ef2d21fe")),
|
||||
]
|
||||
|
||||
@app.get("/", response_model=List[Cartoon], summary="Get all cartoons", description="Retrieve a list of all cartoons.")
|
||||
def get_all_cartoons():
|
||||
return cartoons
|
||||
|
||||
@app.get("/full", summary="Get all cartoons with producer info", description="Retrieve all cartoons with additional producer information.")
|
||||
def get_all_cartoons_with_producers():
|
||||
producers = requests.get(producers_url).json()
|
||||
response = []
|
||||
for cartoon in cartoons:
|
||||
producer_info = next((p for p in producers if p["uuid"] == str(cartoon.producer_id)), None)
|
||||
if producer_info:
|
||||
response.append({**cartoon.dict(), "producer_info": producer_info})
|
||||
return response
|
||||
|
||||
@app.get("/by-producer/{producer_uuid}", response_model=List[Cartoon], summary="Get cartoons by producer", description="Retrieve all cartoons for a given producer.")
|
||||
def get_cartoons_by_producer(producer_uuid: UUID):
|
||||
return [cartoon for cartoon in cartoons if cartoon.producer_id == producer_uuid]
|
||||
|
||||
@app.get("/{cartoon_uuid}", response_model=Cartoon, summary="Get a cartoon by ID", description="Retrieve a single cartoon by its ID.")
|
||||
def get_cartoon_by_id(cartoon_uuid: UUID):
|
||||
cartoon = next((cartoon for cartoon in cartoons if cartoon.uuid == cartoon_uuid), None)
|
||||
if not cartoon:
|
||||
raise HTTPException(status_code=404, detail="Cartoon not found")
|
||||
return cartoon
|
||||
|
||||
@app.get("/full/{cartoon_uuid}", summary="Get cartoon with producer info", description="Retrieve a cartoon with additional producer information.")
|
||||
def get_cartoon_with_producer_info(cartoon_uuid: UUID):
|
||||
cartoon = next((cartoon for cartoon in cartoons if cartoon.uuid == cartoon_uuid), None)
|
||||
if not cartoon:
|
||||
raise HTTPException(status_code=404, detail="Cartoon not found")
|
||||
|
||||
producer_info = requests.get(f"{producers_url}{cartoon.producer_id}").json()
|
||||
return {**cartoon.dict(), "producer_info": producer_info}
|
||||
|
||||
@app.post("/", response_model=Cartoon, summary="Create a new cartoon", description="Add a new cartoon to the database.")
|
||||
def create_cartoon(cartoon: CartoonCreate):
|
||||
# Check if producer exists
|
||||
response = requests.get(f"{producers_url}check/{cartoon.producer_id}")
|
||||
if response.status_code == 404:
|
||||
raise HTTPException(status_code=404, detail="Producer not found")
|
||||
|
||||
new_cartoon = Cartoon(uuid=uuid4(), **cartoon.dict())
|
||||
cartoons.append(new_cartoon)
|
||||
return new_cartoon
|
||||
|
||||
@app.put("/{cartoon_uuid}", response_model=Cartoon, summary="Update a cartoon", description="Update an existing cartoon by its ID.")
|
||||
def update_cartoon(cartoon_uuid: UUID, cartoon_update: CartoonCreate):
|
||||
cartoon = next((cartoon for cartoon in cartoons if cartoon.uuid == cartoon_uuid), None)
|
||||
if not cartoon:
|
||||
raise HTTPException(status_code=404, detail="Cartoon not found")
|
||||
|
||||
cartoon.title = cartoon_update.title
|
||||
cartoon.year = cartoon_update.year
|
||||
cartoon.producer_id = cartoon_update.producer_id
|
||||
return cartoon
|
||||
|
||||
@app.delete("/{cartoon_uuid}", summary="Delete a cartoon", description="Remove a cartoon by its ID.")
|
||||
def delete_cartoon(cartoon_uuid: UUID):
|
||||
global cartoons
|
||||
cartoons = [cartoon for cartoon in cartoons if cartoon.uuid != cartoon_uuid]
|
||||
return {"detail": "Cartoon deleted successfully"}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run("cartoon_service:app", host="0.0.0.0", port=8081, reload=True)
|
||||
49
zhirnova_alyona_lab_3/nginx.conf
Normal file
49
zhirnova_alyona_lab_3/nginx.conf
Normal file
@@ -0,0 +1,49 @@
|
||||
events { worker_connections 1024; }
|
||||
|
||||
http {
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name localhost;
|
||||
|
||||
location /producer_service/ {
|
||||
proxy_pass http://producer_service:8080/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /cartoon_service/ {
|
||||
proxy_pass http://cartoon_service:8081/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /producer_service_service/docs {
|
||||
proxy_pass http://producer_service:8080/docs;
|
||||
}
|
||||
|
||||
location /cartoon_service/docs {
|
||||
proxy_pass http://cartoon_service:8081/docs;
|
||||
}
|
||||
|
||||
location /producer_service/openapi.json {
|
||||
proxy_pass http://producer_service:8080/openapi.json;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /cartoon_service/openapi.json {
|
||||
proxy_pass http://cartoon_service:8081/openapi.json;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
zhirnova_alyona_lab_3/producer_service/Dockerfile
Normal file
11
zhirnova_alyona_lab_3/producer_service/Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY producer_service/producer_service.py .
|
||||
|
||||
CMD ["python", "producer_service.py"]
|
||||
122
zhirnova_alyona_lab_3/producer_service/producer_service.py
Normal file
122
zhirnova_alyona_lab_3/producer_service/producer_service.py
Normal file
@@ -0,0 +1,122 @@
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel
|
||||
from uuid import UUID, uuid4
|
||||
import requests
|
||||
from typing import List, Optional
|
||||
import uvicorn
|
||||
|
||||
|
||||
app = FastAPI(root_path="/producer_service", title="Producer API", description="API для управления продюсерами", version="1.0.0")
|
||||
|
||||
|
||||
class Producer:
|
||||
def __init__(self, name: str, surname: str, uuid_: Optional[UUID] = None):
|
||||
self.uuid_ = uuid_ if uuid_ else uuid4()
|
||||
self.name: str = name
|
||||
self.surname: str = surname
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"uuid": str(self.uuid_),
|
||||
"name": self.name,
|
||||
"surname": self.surname
|
||||
}
|
||||
|
||||
def to_dict_with_cartoons(self, cartoons: list):
|
||||
return {
|
||||
"uuid": str(self.uuid_),
|
||||
"name": self.name,
|
||||
"surname": self.surname,
|
||||
"cartoons": cartoons
|
||||
}
|
||||
|
||||
|
||||
class CreateProducerRequest(BaseModel):
|
||||
name: str
|
||||
surname: str
|
||||
|
||||
|
||||
class UpdateProducerRequest(BaseModel):
|
||||
name: Optional[str]
|
||||
surname: Optional[str]
|
||||
|
||||
|
||||
producers: List[Producer] = [
|
||||
Producer(name="Хаяо", surname="Миядзаки", uuid_=UUID("997aa4c5-ebb2-4794-ba81-e742f9f1fa30")),
|
||||
Producer(name="Гари", surname="Труздейл", uuid_=UUID("694827e4-0f93-45a5-8f75-bad7ef2d21fe")),
|
||||
Producer(name="Стив", surname="Уильямс", uuid_=UUID("eb815350-c7b9-4446-8434-4c0640c21995"))
|
||||
]
|
||||
|
||||
cartoons_url = "http://cartoon_service:8081/"
|
||||
|
||||
|
||||
@app.get("/", response_model=List[dict])
|
||||
def get_all():
|
||||
"""Получение списка всех продюсеров."""
|
||||
return [producer.to_dict() for producer in producers]
|
||||
|
||||
|
||||
@app.get("/{uuid_}", response_model=dict)
|
||||
def get_one(uuid_: UUID):
|
||||
"""Получение продюсера по идентификатору."""
|
||||
for producer in producers:
|
||||
if producer.uuid_ == uuid_:
|
||||
return producer.to_dict()
|
||||
raise HTTPException(status_code=404, detail="Продюсер с таким uuid не был найден")
|
||||
|
||||
|
||||
@app.get("/with-cartoons/{uuid_}", response_model=dict)
|
||||
def get_one_with_cartoons(uuid_: UUID):
|
||||
"""Получение продюсера со списком его мультфильмов."""
|
||||
for producer in producers:
|
||||
if producer.uuid_ == uuid_:
|
||||
response = requests.get(f"{cartoons_url}by-producer/{uuid_}")
|
||||
if response.status_code != 200:
|
||||
raise HTTPException(status_code=404, detail="Не удалось получить мультфильмы продюсера")
|
||||
return producer.to_dict_with_cartoons(response.json())
|
||||
raise HTTPException(status_code=404, detail="Продюсер с таким uuid не был найден")
|
||||
|
||||
|
||||
@app.get("/check/{uuid_}", response_class=JSONResponse)
|
||||
def check_exist(uuid_: UUID):
|
||||
"""Проверка наличия продюсера по идентификатору (для cartoon_service)."""
|
||||
for producer in producers:
|
||||
if producer.uuid_ == uuid_:
|
||||
return JSONResponse(content="", status_code=200)
|
||||
return JSONResponse(content="", status_code=404)
|
||||
|
||||
|
||||
@app.post("/", response_model=dict)
|
||||
def create(producer: CreateProducerRequest):
|
||||
"""Создание нового продюсера."""
|
||||
new_producer = Producer(name=producer.name, surname=producer.surname)
|
||||
producers.append(new_producer)
|
||||
return new_producer.to_dict()
|
||||
|
||||
|
||||
@app.put("/{uuid_}", response_model=dict)
|
||||
def update_by_id(uuid_: UUID, producer_update: UpdateProducerRequest):
|
||||
"""Изменение данных продюсера по идентификатору."""
|
||||
for producer in producers:
|
||||
if producer.uuid_ == uuid_:
|
||||
if producer_update.name is not None:
|
||||
producer.name = producer_update.name
|
||||
if producer_update.surname is not None:
|
||||
producer.surname = producer_update.surname
|
||||
return producer.to_dict()
|
||||
raise HTTPException(status_code=404, detail="Продюсер с таким uuid не был найден")
|
||||
|
||||
|
||||
@app.delete("/{uuid_}", response_class=JSONResponse)
|
||||
def delete(uuid_: UUID):
|
||||
"""Удаление продюсера по идентификатору."""
|
||||
for producer in producers:
|
||||
if producer.uuid_ == uuid_:
|
||||
producers.remove(producer)
|
||||
return JSONResponse(content="Продюсер успешно удалён", status_code=200)
|
||||
raise HTTPException(status_code=404, detail="Продюсер с таким uuid не был найден")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run("producer_service:app", host="0.0.0.0", port=8080, reload=True)
|
||||
3
zhirnova_alyona_lab_3/requirements.txt
Normal file
3
zhirnova_alyona_lab_3/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
fastapi
|
||||
uvicorn
|
||||
requests
|
||||
Reference in New Issue
Block a user