Presigned URL을 통해 S3 객체 다운로드

Hyungeun Lee·2024년 11월 10일

안녕하세요. 이번 시간에는 AWS S3의 Presigned URL에 대해 자세히 알아보는 시간을 가지겠습니다.

Presigned URL이란?

Presigned URL은 AWS S3와 같은 클라우드 스토리지 혹은 MINIO 오브젝트 스토리지에서 제공하는 기능으로, 다른 사람이 해당 스토리지에 대한 보안 자격 증명 혹은 권한 없이 객체를 업로드하거나 다운로드할 수 있도록 합니다. 이는 API key를 직접적으로 공유하지 않고도 다른 유저에게 객체에 접근 권한을 임시적으로 부여할 수 있다는 장점이 있습니다.

Presigned URL의 장점

  1. 보안성

    • 제한된 시간 동안만 접근 가능
    • API 키를 직접 공유하지 않음
    • HTTPS를 통한 안전한 전송
  2. 효율성

    • URL이 객체 단위로 구성되어 불필요한 트래픽 감소
    • 비용 절약 효과
    • 사용자가 직접 서버에 접속할 필요 없음
  3. 유연성

    • 업로드/다운로드 모두 지원
    • 만료 시간 설정 가능
    • Content-Type 지정 가능

실제 사용 사례

  1. 파일 공유 시스템

    • 임시 다운로드 링크 생성
    • 대용량 파일 안전한 공유
  2. 업로드 시스템

    • 사용자의 프로필 이미지 업로드
    • 문서 제출 시스템
  3. 미디어 스트리밍

    • 임시 미디어 액세스 URL 제공
    • 보안이 필요한 콘텐츠 전송

구현 코드

1. 기본 설정

import logging
import boto3
from botocore.exceptions import ClientError
import os
import requests
from urllib.parse import urlparse

# 로깅 설정
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# S3 클라이언트 초기화
def initialize_s3_client():
    """Initialize S3 client with environment variables
    
    Required environment variables:
    - AWS_ACCESS_KEY_ID
    - AWS_SECRET_ACCESS_KEY
    - AWS_DEFAULT_REGION
    """
    try:
        s3_client = boto3.client(
            's3',
            aws_access_key_id=os.getenv('AWS_ACCESS_KEY_ID'),
            aws_secret_access_key=os.getenv('AWS_SECRET_ACCESS_KEY'),
            region_name=os.getenv('AWS_DEFAULT_REGION', 'ap-northeast-2')
        )
        return s3_client
    except Exception as e:
        logger.error(f"Failed to initialize S3 client: {e}")
        return None

2. Presigned URL 생성

def create_presigned_url(bucket_name, object_name, s3_client, expiration=3600, method='get_object', content_type=None):
    """Generate a presigned URL for S3 object upload or download
    
    :param bucket_name: string
    :param object_name: string
    :param s3_client: boto3.client
    :param expiration: Time in seconds for the presigned URL to remain valid
    :param method: S3 method ('get_object' for download, 'put_object' for upload)
    :param content_type: string, optional. e.g., 'image/jpeg', 'application/pdf'
    :return: Presigned URL as string. If error, returns None.
    """
    try:
        params = {
            'Bucket': bucket_name,
            'Key': object_name
        }
        
        # Content-Type 설정 (업로드의 경우)
        if method == 'put_object' and content_type:
            params['ContentType'] = content_type
        
        response = s3_client.generate_presigned_url(
            method,
            Params=params,
            ExpiresIn=expiration,
            HttpMethod='https'  # HTTPS 강제
        )
        
        logger.info(f"Presigned URL generated for {method}")
        return response
    except ClientError as e:
        logger.error(f"Error generating presigned URL: {e}")
        return None

3. 파일 업로드/다운로드

def upload_file_with_presigned_url(presigned_url, file_path, content_type=None):
    """Upload a file using presigned URL
    
    :param presigned_url: Presigned URL for upload
    :param file_path: Local path of file to upload
    :param content_type: string, optional. e.g., 'image/jpeg'
    :return: Boolean indicating success/failure
    """
    try:
        with open(file_path, 'rb') as f:
            headers = {}
            if content_type:
                headers['Content-Type'] = content_type
            
            response = requests.put(presigned_url, 
                                 data=f.read(),
                                 headers=headers)
        
        if response.status_code == 200:
            logger.info(f"Successfully uploaded file: {file_path}")
            return True
        else:
            logger.error(f"Failed to upload file. Status code: {response.status_code}")
            return False
    except Exception as e:
        logger.error(f"Error uploading file: {e}")
        return False

def download_file_with_presigned_url(presigned_url, local_path):
    """Download a file using presigned URL with proper error handling
    
    :param presigned_url: Presigned URL for download
    :param local_path: Local path to save the downloaded file
    :return: Boolean indicating success/failure
    """
    try:
        response = requests.get(presigned_url)
        if response.status_code == 200:
            with open(local_path, 'wb') as f:
                f.write(response.content)
            logger.info(f"Successfully downloaded file to: {local_path}")
            return True
        else:
            logger.error(f"Failed to download file. Status code: {response.status_code}")
            return False
    except Exception as e:
        logger.error(f"Error downloading file: {e}")
        return False

4. 사용 예제

def main():
    # 환경 변수 설정 확인
    required_env_vars = ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY']
    for var in required_env_vars:
        if not os.getenv(var):
            logger.error(f"Missing required environment variable: {var}")
            return

    # S3 클라이언트 초기화
    s3_client = initialize_s3_client()
    if not s3_client:
        return

    # 설정
    bucket_name = "your-bucket-name"
    object_name = "path/to/your/file.jpg"
    local_file = "local_file.jpg"
    
    # 이미지 업로드를 위한 Presigned URL 생성
    upload_url = create_presigned_url(
        bucket_name, 
        object_name, 
        s3_client, 
        method='put_object',
        expiration=3600,  # 1시간
        content_type='image/jpeg'
    )
    
    # 파일 업로드
    if upload_url:
        success = upload_file_with_presigned_url(
            upload_url, 
            local_file,
            content_type='image/jpeg'
        )
        print(f"Upload success: {success}")

if __name__ == "__main__":
    main()

보안 고려사항

  1. URL 만료 시간 설정

    • 최소 권한 원칙에 따라 필요한 만큼만 설정
    • 일반적으로 몇 분에서 몇 시간 사이로 설정
    • 장기 액세스가 필요한 경우 별도의 인증 메커니즘 고려
  2. HTTPS 사용

    • 모든 Presigned URL은 HTTPS를 통해 접근
    • 데이터 전송 시 암호화 보장
  3. Content-Type 제한

    • 업로드 시 허용된 파일 형식만 지정
    • 보안 취약점 방지
  4. URL 공유 주의사항

    • 안전한 채널을 통해 URL 공유
    • URL에 포함된 토큰 노출 주의
    • 공개 채널에 URL 게시 금지

마치며

오늘은 Presigned-URL 에 대해서 알아봤습니다. 감사합니다.

0개의 댓글