minio는 S3와의 호환성을 제공하며, 컨테이너 환경에 적합하고, 간편하다는 장점이 있는 오브젝트 스토리지 서비스입니다.
이전 프로젝트에서 S3를 사용해본 경험이 있기 때문에, 이미지 서버의 스토리지 솔루션으로 MinIO를 선택하게 되었습니다.
(호환성을 위해 에러메시지조차 S3와 동일한 것을 확인 할 수 있었습니다.)
저는 docker-compose를 사용해 간편하게 minio를 container로 실행할수 있게 하였습니다.
version: "3.8"
services:
minio:
image: minio/minio
container_name: minio-container
volumes:
- ./minio-data:/data
ports:
- 9000:9000
- 9020:9020
environment:
MINIO_ACCESS_KEY: test
MINIO_SECRET_KEY: test
command: server /data --console-address ":9020"
restart: always
여기서 눈 여겨 봐야 할 부분은 설정한 9020포트를 통해 브라우저로 MINIO를 탐색/설정 할 수 있습니다.
또한 저는 이미지 서버는 fast api를 사용해 구축하였기 때문에
pip install minio
로 minio 라이브러리를 추가합니다.
게시글 작성시 이미지가 포함될 경우 다음과 같은 시퀀스를 통해 이미지와 게시글이 저장됩니다.
여기서 확인해야 하는 부분은 vue에서 생성한 uuid를 통해 저장된 이미지를 식별하고 조회할 수 있습니다.(url이 아닙니다.)
fast api의 코드는 다음과 같습니다.
# router.py
def get_image_service() -> ImageService:
return ImageService(get_minio_client())
def get_minio_client():
return minioClient
@router.post("/image")
def upload_image(image: UploadFile = File(...), description: str = Form(...), service: ImageService = Depends(get_image_service)):
result = service.upload_image(image, description)
if result:
return {"filename": image.filename, "description": description}
else:
raise HTTPException(status_code=400, detail="Image could not be uploaded")
# minio.py
minioClient = Minio('localhost:9000',
access_key='test',
secret_key='test',
secure=False)
# service.py
class ImageService:
def __init__(self, client):
self.client = client
bucket_exists = client.bucket_exists("images-test")
if not bucket_exists:
client.make_bucket("images-test")
def upload_image(self, file, filename):
try:
data = file.file.read()
length = len(data)
self.client.put_object("images-test", filename, io.BytesIO(data), length, file.content_type)
except Exception as e:
print(e)
return False
return True
여기서 주요 포인트는 ImagerService 객체가 생성되면서, 버킷이 존재하지 않는다면, 버깃을 생성하도록 작성하였습니다.
또한, fast api의 Depends를 사용하여 DIP 의존성 역전 원칙을 적용하였습니다.
이렇게 작성하고 해당 endpoint로 postman을 사용해 이미지를 저장해보겠습니다.
저장한 이후 아까 설정한 9020포트로 버킷을 확인해보면
위와 같이 저장되어있는 것을 확인 할 수 있으며,
Preview를 통해 이미지를 직접 확인 할 수도 있습니다.
minIO는 S3와는 다르게 저장된 객체에 대해서 직접적인 URL을 제공하지 않기 때문에, 생각보다 까다로운 부분입니다.
게시글에 첨부된 이미지의 조회는 다음과 같은 시퀀스를 통해 이뤄집니다.
게시글에 포함된 image uuid를 통해 image server(fast api)에 사인된 URL을 요청하게 됩니다.
브라우저는 사인된 URL을 통해서 이미지를 확인 할 수 있습니다.
그렇기 때문에 게시글 작성의 경우, board-server와 image-server를 각각 비동기를 이용하여 병렬적으로 처리할 수 있지만, 조회의 경우에는 반드시 순차적으로 처리해야 합니다.
사인된 URL은 특정 객체에 대해 설정된 시간 동안에만 임시 접근 권한만을 제공하며, 만료될 경우 접근 권한을 제공하지 않습니다.
다음과 같은 코드를 통해 사인된 URL을 요청할 수 있습니다.
# ImageService
def get_image(self, image_name):
try:
result = self.client.presigned_get_object("images-test1", image_name, expires=datetime.timedelta(seconds=10))
except Exception as e:
print(e)
return False
return result
저는 Vue에서 게시글 조회하는 경우에만 이미지를 조회할 수 있도록 구성하고 싶기 때문에 10초로 매우 짧게 설정하였습니다.
minIO는 제한이 없는 URL을 제공하지 않기 때문에
만약 제공된 URL을 통해 이미지를 외부로 공유하고 싶다면, 긴 시간(extream example: 100 year)을 할당하여 공유하는 방법이 있습니다.
2024.03.15) 이런식으로 사용한다면 DB에 게시글을 저장할 때 UUID를 저장하는 것이 아니라. URL을 저장하는 방법도 사용할 수 있겠네요!
postman으로는 다음과 같이 요청할 수 있습니다.
사전에 설정된 uuid(위의 "fastapi.png")를 통해 URL을 요청합니다.
해당 URL로 접속하면 설정한 시간 내에는 정상적으로 이미지를 조회 할 수 있는 것을 확인 할 수 있습니다.
만약 설정된 시간이 지났다면, 위와 같이 403 Forbidden을 통해 접근을 제한하는 것 또한 확인 할 수 있습니다.
오늘은 fast api와 minIO를 사용하여, 이미지를 저장하고, 조회하는 기능을 구현해 봤습니다.
image server에 추가적으로 구현하고 싶은 기능은, 여러 이미지를 한번에 저장하는 기능, 여러 이미지를 한번에 조회하는 기능, 이미지를 용량에 맞춰 압축하여 저장하는 기능 등을 구현할 예정입니다.