회원가입

파이썬 pre signed url 생성 및 파일 업로드 aws s3

NULL 2022-03-26

 

pre signed url 이란?


AWS S3 공식 문서 설명

미리 서명된 URL의 생성자가 해당 객체에 대한 액세스 권한을 보유할 경우, 미리 서명된 URL은 URL에서 식별된 객체에 대한 액세스를 부여합니다. 즉, 객체를 업로드하기 위해 미리 서명된 URL을 수신하는 경우, 미리 서명된 URL의 생성자가 해당 객체를 업로드하는 데 필요한 권한을 보유하는 경우에만 객체를 업로드할 수 있습니다.

https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/userguide/PresignedUrlUploadObject.html

 

간단하게 pre signed url 설명하면 파일을 올릴 건데 그 파일을 올리기 위한 url 주소라고 생각하면 된다.

올리기 위한 url 주소에 파일 정보를 담아서 request(PUT 메서드)를 보내면 된다.

 

'당연하게 아니냐?' 라는 생각을 하겠지만, 여기서 이 url 은 백엔드 서버 endpoint로 요청하는 것이 아닌 AWS가 만들어준 주소로 파일을 올려야 하는 것이다.

 

AWS가 만들어준 주소?? 가 무엇인지 아래에 설명하겠다.

 

 

AWS S3 pre signed url 예제 코드


참고 사이트https://docs.aws.amazon.com/code-library/latest/ug/s3_example_s3_Scenario_PresignedUrl_section.html

 

[아래 코드는 최근 코드와 많이 달라렸습니다.]

main.py 파일

# 옛날 코드 상단의 참고 aws 참고 사이트에서 코드를 확인해주세요

import argparse
import logging
import boto3
from botocore.config import Config
from botocore.exceptions import ClientError
import requests

logger = logging.getLogger(__name__)


def generate_presigned_url(s3_client, client_method, method_parameters, expires_in):
    """
    Generate a presigned Amazon S3 URL that can be used to perform an action.

    :param s3_client: A Boto3 Amazon S3 client.
    :param client_method: The name of the client method that the URL performs.
    :param method_parameters: The parameters of the specified client method.
    :param expires_in: The number of seconds the presigned URL is valid for.
    :return: The presigned URL.
    """
    try:
        url = s3_client.generate_presigned_url(
            ClientMethod=client_method,
            Params=method_parameters,
            ExpiresIn=expires_in
        )
        logger.info("Got presigned URL: %s", url)
    except ClientError:
        logger.exception(
            "Couldn't get a presigned URL for client method '%s'.", client_method)
        raise
    return url


def usage_demo():
    logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')

    print('-' * 88)
    print("Welcome to the Amazon S3 presigned URL demo.")
    print('-' * 88)

    parser = argparse.ArgumentParser()
    parser.add_argument('bucket', help="The name of the bucket.")
    parser.add_argument(
        'key', help="For a GET operation, the key of the object in Amazon S3. For a "
                    "PUT operation, the name of a file to upload.")
    parser.add_argument(
        'action', choices=('get', 'put'), help="The action to perform.")
    args = parser.parse_args()

    s3_client = boto3.client(
        's3',
        region_name='ap-northeast-2',
        aws_access_key_id='aws_iam_access_key',
        aws_secret_access_key='aws_iam_secret_access_key',
        config=Config(signature_version='s3v4')
    )
    client_action = 'get_object' if args.action == 'get' else 'put_object'
    # 'Key' 에는 파일 이름이 들어가야한다.
    url = generate_presigned_url(s3_client, client_action, {'Bucket': args.bucket, 'Key': args.key}, 1000)

    print("Using the Requests package to send a request to the URL.")
    response = None
    # 읽기 혹은 업로드
    if args.action == 'get':
        response = requests.get(url)
    elif args.action == 'put':
        print("Putting data to the URL.")
        try:
            with open(args.key, 'r') as object_file:
                object_text = object_file.read()
            response = requests.put(url, data=object_text)
        except FileNotFoundError:
            print(f"Couldn't find {args.key}. For a PUT operation, the key must be the "
                  f"name of a file that exists on your computer.")

    if response is not None:
        print("Got response:")
        print(f"Status: {response.status_code}")
        print(response.text)

    print('-' * 88)


if __name__ == '__main__':
    usage_demo()

 

실행하기

# get: S3 업로드 되어있는 파일 url 가져오기
# put: 업로드할 url 생성 후, S3에 파일 업로드하기
python main.py s3버킷이름 현재폴더파일명 {get혹은put}

 

 

pre signed url을 사용하는 이유


다운로드 같은 경우, 권한이 있는 사용자만 접근할 수 있게

업로드 같은 경우, 권한이 있는 파일 혹은 사용자만 업로드할 수 있도록 만들기 위해서다.

 

추가로 파일 업로드 같은 경우 백엔드 서버로 파일 업로드를 관리하면 백엔드 서버에 저장 시키거나 S3 에 업로드 하더라도 form-data 로 데이터를 받아 업로드 시켜야 하는데, pre signed url은 백엔드 메모리도 소모되지 않으며 바로 S3url를 요청 받아 업로드하면 되니 백엔드 서버 업로드 방식을 최적화 할 수 있다.

 

 

[PUT] 권한이 있는 파일 / AWS가 만들어준 주소 (업로드)


- 권한이 있는 파일

'Key' 안에 있는 파일명과 업로드하는 파일명이 같아야한다.

url = generate_presigned_url(s3_client, client_action, {'Bucket': args.bucket, 'Key': args.key}, 1000)

 

- AWS가 만들어준 주소

1000초간 업로드가 유효한 주소

(코드에서는 url을 만들고 바로 업로드 한다)

 

직접 업로드 해보기 (POSTMAN 사용)

위에 코드처럼 주석을 쳐놓자 (82~84 번째 줄)

 

그리고 url을 생성하자.

 

POSTMAN에서 생성된 url을 주소창에 넣고, bodybinary로 설정 후, 파일을 올리자.

(url 복붙할 때, 줄바꿈에 신경쓰자 POSTMAN 쿼리 params에 줄바꿈이 들어갔다. 지우자)

 

Body 탭에 들어가 binary로 수정 후, 파일을 업로드하자.

필자는 아래 이미지를 업로드 하겠다.

 

정상적으로 업로드가 완료되면 200 STATUS 코드가 나온다.

 

파일이 정상적으로 업로드 되는 것을 확인할 수 있다.

 

업로드 된 파일 확인

 

 

[GET] AWS가 만들어준 주소 (다운로드)


AWS가 만들어준 주소 이게 무엇인지 자세히 알아보자.

 

아래 사진처럼 필자가 접근할 파일 "test.txt"가 S3에 올라가 있다.

 

위 코드를 작성한 후, 실행해보면

 

위 코드 처럼 특정 url이 만들어지면서 이게 AWS가 만들어준 주소이다.

 

해당 url로 접근하면

S3에 올라가져 있는 파일을 다운로드 받을 수 있다.

 

url의 접근 권한은 코드에서 입력한 것 처럼 1000초 이다.

url = generate_presigned_url(s3_client, client_action, {'Bucket': args.bucket, 'Key': args.key}, 1000)

 

만약 한번 다운로드를 했다면 캐싱되어있어 계속 다운로드를 할 수 있다.

하지만 다운로드를 하지 않은 곳에 1000 초 지난 뒤 다운로드를 시도하는 경우 에러난다.

(시크릿 브라우저로 테스트 가능)

 

S3 권한으로 안되는 에러

버킷 권한 -> 버킷 정책 -> 아래 추가

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:GetObject",
                "s3:PutObject"
            ],
            "Resource": [
                "arn:aws:s3:::버킷명/*",
                "arn:aws:s3:::버킷명"
            ]
        }
    ]
}

 

[S3 업로드 안되는 버그 : 2023-03-05 최신화]

설정은 잘 했는데, 파일이 업로드 안되는 경우가 있습니다.

이럴 때 S3 권한 때문인지?? 하루 지나니깐 됐었습니다... 

이유는 잘 모르겠습니다.

 

사용하고 있는 presigned url 함수

import boto3 as boto3
from botocore.config import Config
import requests


def generate_presigned_url(file_name, _type='common', unique=0, expires_in=1000):
    s3_client = boto3.client(
        's3',
        region_name='ap-northeast-2',
        aws_access_key_id=settings.AWS_IAM_ACCESS_KEY,
        aws_secret_access_key=settings.AWS_IAM_SECRET_ACCESS_KEY,
        config=Config(signature_version='s3v4')
    )
    try:
        response = s3_client.generate_presigned_post(
            Bucket=settings.AWS_S3_BUCKET_NAME,
            Key=f'{_type}/{unique}/{uuid.uuid4()}_{file_name}',
            Conditions=[
                ['content-length-range', 0, 10485760]
            ],
            ExpiresIn=expires_in
        )
        return response
    except ClientError as e:
        return JsonResponse({'result': 'fail'})


def upload_file_to_presigned_url(presined_url, presigned_data, file):
    try:
        response = requests.post(
            url=presined_url,
            data=presigned_data,
            files={'file': file},
        )
        return response.status_code
    except Exception as e:
        return 400


## 사용하는 경우
response = generate_presigned_url(
    '파일이름',
    _type='story_image',
    unique='유니크한 정의',
)
upload_file_to_presigned_url(response['url'], response['fields'], 파일)

가장 최근에는 위와 같은 코드를 이용해 사용하고 있습니다.

0 2
스미스
스미스
안녕하세요. 질문이 있습니다. 보통은 form-data로 파일을 올리는데 s3에 binary로 업로드를 해야하는 이유가 무엇일까요? pre-signed url 문제인가요 아니면 포스트맨 문제인가요?
2023-03-07
NULL
NULL
postman 을 이용해서 업로드해서 binary로 전달하고 있는 겁니다. form-data file 로 올리셔도 문제 없으실 겁니다!
2023-03-11
뼈와 살
프로젝트를 혹은 직장에서 일하면서 겪게 되는 Blocker, 문제 등 나에게 뼈와 살이 되어 성장시켜주게 한 문제점을 어떻게 해결했는지를 기록하는 게시판이다.
Yesterday: 750
Today: 547