Media 파일들을 S3에 저장하기

Django의 Media 파일들을 S3에 옮겨봅시다.
Posted on 2024-02-14 by GKSRUDTN99
Django로 웹사이트 만들기 Django AWS S3

기존에 만든 블로그는 포스팅에 사용하는 이미지들이 로컬 웹 서버에 저장되고 있었습니다.


이미지가 늘어나면 EC2의 저장용량 문제가 생기는데,
S3를 사용하는 편이 비용 면에서나, 후에 트래픽 관리면에서나 좋을 것 같아 사용해보기로 하였습니다.



1. AWS S3 버킷 만들기

AWS 문서에 따르면,
버킷은 Amazon S3에 저장된 객체에 대한 컨테이너입니다


S3에 객체를 저장하려면, 버킷을 새로 만들어야 하는데요,
AWS에 로그인한 뒤, S3 서비스로 들어갑니다.


좌측 메뉴바에서 버킷을 선택한 뒤,
주황색 버킷 만들기 버튼을 선택합니다.


일반 설정 부분입니다.
원하는 버킷 이름을 짓고, S3를 사용할 리전(지역)도 선택합니다.


객체 소유권 설정 부분입니다.
객체 별로 소유권을 설정해서, 같은 버킷 내에 있는 객체이지만, ACL 설정을 통해 특정 AWS 계정만 접근할 수 있는 권한을 줄 수 있다고 합니다.


후에 Django 프로젝트 설정에서 Default ACL 설정을 통해 객체들에게 'public-read' 권한을 줄 것 이므로, 활성화 버튼을 눌러줍니다.


객체와 버킷에 대한 퍼블릭 엑세스 차단 설정 부분입니다.
이 부분은 S3를 사용하는 상황에 따라 다르게 설정해야 한다고 하는데,


저는 S3에 블로그에 들어갈 이미지들만 저장할 것 이므로,
체크를 해제해 퍼블릭 엑세스를 허용 한뒤, 진행하겠습니다.


나머지 설정은 기본 설정으로 두고,
버킷을 생성합니다.



2. S3에 접근할 유저 추가하기

S3에 접근 가능한 유저를 생성하여,
해당 유저의 Id와 Secret Key값을 Django에 추가하여 사용할 겁니다.


AWS에서, IAM 서비스로 들어갑니다.


좌측 탭에서 사용자를 선택하고,
파란색 사용자 추가 버튼을 선택합니다.


사용자 이름을 작성하고,
하단에서는 액세스 키 - 프로그래밍 방식 액세스를 선택합니다.


저희는 이 사용자를 S3에 접근 가능한 그룹을 생성하고,
이 그룹안에 넣어줄 겁니다.
이어 나오는 창에서 그룹 생성 버튼을 클릭합니다.


그룹 이름을 작성하고,
하단에서는 s3full을 검색한 뒤, 나오는 AmazonS3FullAccess를 체크하고, 그룹을 생성합니다.


이후 사용자 생성 과정은 별도의 설정 없이,
기본 설정으로 진행하겠습니다.


이어 나오는 액세스 키와 비밀 엑세스 키는,
후에 다시 확인하는 것이 불가하므로 .csv 파일을 받아놓습니다.



3. Django 프로젝트 설정

pip를 통해 boto3django-storages를 설치합니다.

$ pip install boto3
$ pip install django-storages


프로젝트의 settings.py파일을 수정합니다.
INSTALLED_APPSstorages를 추가합니다.

INSTALLED_APPS = [
    ...
    'storages',
]


settings.py 하단에 아래 내용을 추가합니다.


# S3 Storages
AWS_ACCESS_KEY_ID = '.csv 파일에 있는 Access Key Id'
AWS_SECRET_ACCESS_KEY = '.csv 파일에 있는 Secret Access Key'
AWS_REGION = 'ap-northeast-2'

AWS_STORAGE_BUCKET_NAME = 'codecamper-blog-bucket'
AWS_S3_CUSTOM_DOMAIN = '%s.s3.%s.amazonaws.com' % (AWS_STORAGE_BUCKET_NAME, AWS_REGION)
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'


현재 제 블로그는 nginx를 사용하고 있으므로, nginx.conf 파일도 수정합니다.
/media/ 경로로 요청이 들어오면 기존에는 alias로 로컬 디렉토리를 가리키고 있었지만,
이제는 S3 서버로 요청을 보내 응답받아 이미지를 돌려줘야 하므로, proxy_pass를 통해 S3 경로르 적어줍니다.

server {
    ...
    location /media/ {
        client_max_body_size 10M;
        proxy_pass https://codecamper-blog-bucket.s3.ap-northeast-2.amazonaws.com/;
    }
}


위 작업까지 마치고 runserver를 통해 실행한 뒤,
admin 서버를 통해 파일을 올려보면 정상적으로 AWS 콘솔에서 S3에 파일이 올라간 것을 확인할 수 있습니다.



3. 번외 - 로컬에 있던 image들을 S3로 옮기기

저는 이전에도 글들을 포스팅하며 사용했던 이미지들은 이미 웹 서버의 내부 저장소에 있는 상태입니다.


이 이미지들을 custom command를 만들어,
그 command로 S3로 옮겨보겠습니다.


프로젝트 폴더의 앱 폴더 내에 /management/commands 디렉토리를 생성하고,
해당 디렉토리 내에 migrate_media_files_to_s3.py 라는 새 파일을 하나 생성한 뒤, 아래 코드를 붙여넣습니다.


from django.core.management.base import BaseCommand
from django.conf import settings

import logging
import os
import boto3
from botocore.exceptions import ClientError


class Command(BaseCommand):

    def handle(self, *args, **options):
        # create session to s3
        session = boto3.session.Session()
        s3_client = session.client(
            service_name='s3',
            aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
            aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY
        )

        # process migrate local media files to s3
        for full_path in self._get_all_media_file():
            try:
                upload_path = self._split_path_to_upload(full_path)
                args = dict()
                args["ACL"] = 'public-read'
                s3_client.upload_file(full_path, settings.AWS_STORAGE_BUCKET_NAME, upload_path, args)
                print(f"success upload {upload_path}")
                logging.info(f"success upload {upload_path}")
            except ClientError as e:
                print(f"failed upload {upload_path}")
                logging.error(f"{e}: {upload_path}")

    def _get_all_media_file(self) -> [str]:
        """
        this method is used to retrieve all media files contained in this project.
        :return: list of string path file
        """
        files = []
        for r, d, f in os.walk(settings.MEDIA_ROOT):
            for file in f:
                files.append(os.path.join(r, file))
        return files

    def _split_path_to_upload(self, full_path: str) -> str:
        """
        This function is used to get the string media path
        :param full_path: string path of file
        :return: string media path to upload s3
        """
        media_root = settings.MEDIA_ROOT
        upload_path = full_path.split(media_root)[-1]
        upload_path = self._remove_leading_slash(upload_path)
        return upload_path

    def _remove_leading_slash(self, path: str) -> str:
        """
        저의 기존 media_root가 'media'로, 뒤에 붙는 '/'가 없었기 때문에,
        split 후 path 제일 앞에 /가 붙어 이를 없애는 함수입니다.
        사용하시는 분에 따라 삭제하셔도 되는 함수입니다.
        """
        if path[0] == '/':
            return path[1:]
        else:
            return path


그런 다음, 쉘에서 python manage.py migrate_media_files_to_s3로 실행하면, 로컬에 있던 이미지들이 S3로 업로드됩니다.