본문 바로가기
★ 프로젝트 + 트러블 슈팅 ★

Lambda@Edge / CloudFront / Singed URL / S3 트러블 슈팅

by 리승우 2023. 12. 17.

최근, 이미지 get서빙 부분에 보완하면 좋겠는 점이 생겨서 아래와 같이 진행해보기로 하였다. (일부분만 발췌)

 

CF(cloudfront)에 S3를 연동하여 인증된 사용자만 CF를 통하여 이미지를 가지고 오게함과 더불어, 캐시 기능도 활용하여 보다 빠른 이미지 서빙이 가능하게 하는 것이 목적이였다. 

 

추가적으로, S3에서 가져오는 이미지를 Webp확장자 변환 및 리사이징하는 것도 목적이였다. 

(Path 암호화 Decode 부분은 우선 배제하고 진행하였다)

 

이를 위해 진행한 샘플 테스트 과정을 아래에 옮겨 기록해두려한다.

 

우선 위와 같이 운영할 경우의 이점을 파악하였다.

 

CF+ S3 운영이점 및 방법조사 내용


운영 이점

https://aws.amazon.com/ko/blogs/korea/amazon-s3-amazon-cloudfront-a-match-made-in-the-cloud/

운영 방법

https://velog.io/@rungoat/AWS-S3%EC%99%80-CloudFront-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0

https://dingdingmin-back-end-developer.tistory.com/entry/S3%EB%A5%BC-CloudFront%EB%A1%9C-%EC%A0%9C%EA%B3%B5%ED%95%98%EA%B8%B0

https://earth-95.tistory.com/128

https://three-beans.tistory.com/entry/AWSCloudFront-CloudFront%EB%A1%9C-S3-%EC%A0%95%EC%A0%81-%EC%BB%A8%ED%85%90%EC%B8%A0-%EC%A0%9C%EA%B3%B5%ED%95%98%EA%B8%B0


 

그 후 직접 아래와 같이 진행하였다.

 

CF + S3 연동 샘플테스트


작업순서

  1. bucket 생성 (디폴트값으로 진행)
  2. cloudfront 생성 및 배포진행
    1. 1에서 생성한 bucket을 원본도메인으로 설정
    2. 원본액세스 설정을 아래와 같이 진행 (이외 설정들은 디폴트값으로 진행)

3. cloudfront의 정책을 복사하여, bucket의 정책에 붙여넣음으로써, cloudfront에 대한 액세스를 허용시킴

 

4. cloudfront의 배포도메인 + bucket 이미지로 URL 접속 시도 Hit from cloudfront가 뜨는 것을 보아, Cloudfront 엣지로부터 캐싱되어 있는 파일 수신이 잘됨

 

처음 수신할 경우 아래와 같이 Miss from cloudfront가 뜸

  • 엣지에 아직 전파되지않아, 해당 파일이 존재하지 않거나 요청시간을 넘어서 Origin-CloudFront로 파일을 수신했기 때문,재시도하면 Hit from cloudfront가 뜸

 

 

그러고나서, 인증된 사용자만 CF를 통해 S3에 접근할 수 있게끔 Signed URL을 적용하였다.

 

CF Signed URL 사용이유 및 적용방법


사용이유

https://advancedweb.hu/how-to-use-cloudfront-signed-urls/

https://velog.io/@leehaeun0/AWS-CloudFront에-Signed-URLs-적용하기

https://velog.io/@leehaeun0/미디어파일-S3에서-CloudFront로-이사하기-feat-CORS

https://blog.msalt.net/250

https://blog.naver.com/PostView.naver?blogId=ijoos&logNo=221560372612

 

작업순서

1. RSA(비대칭키) 개인키 생성 및 해당 개인키로부터 공개키 추출 후 생성

openssl genrsa -out private_key.pem 2048

openssl rsa -pubout -in private_key.pem -out public_key.pem

 

2. CF 공개키 등록 및 생성

 

3. CF에 키 그룹 등록

 

4. 본인이 배포한 CF에 아래와 같이 키그룹 연결 후 저장

 

5. 기존에 연결되었던 URL로 다시 접근 시, 아래와 같이 이제는 제한됨

(Signed URL을 배정받지않고 진행했기 때문에)

 

Signed URL서명 생성은 아래 공식문서를 통해 생성하면 된다.
필자가 유지보수중인 애플리케이션은 PHP이므로, 아래 공식문서를 보았다.

https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/CreateURL_PHP.html

 

PHP를 사용한 URL 서명 생성 - Amazon CloudFront

이 페이지에 작업이 필요하다는 점을 알려 주셔서 감사합니다. 실망시켜 드려 죄송합니다. 잠깐 시간을 내어 설명서를 향상시킬 수 있는 방법에 대해 말씀해 주십시오.

docs.aws.amazon.com

 

이제 마지막 단계로, 

PHP + CF(Signed URL) + Lambda@Edge + S3를 모두 연동하여 이미지 get시 리사이징 및 확장자 변환이 되게 하도록 하려고한다.

 

그러기위해 추가로 진행해야할 작업은 아래와 같았다.

 

작업순서

  1. Lambda@Edge 생성 (이미지 처리를 위한 Pillow 모듈 있어야함)
  2. cloudfront에 1번에서 생성한 Lambda@Edge 연동
  3. cloudfront에 접근할 시, 트리거가 되어 이미지 처리 Lambda@Edge가 작동된 뒤 처리된 이미지를 response 받음

그런데 위 작업대로 하던 중 뜻밖의 이슈를 많이 만나게 되었다. (매우 힘들었다)

 

Lambda@Edge 필수 전제조건

무조건, 버지니아 북부 (us-east-1)에 생성해야한다.

왜?

글로벌 서비스인 CF가 트리거가 되어 해당 람다가 실행될 때, 각 나라별 리전에 배치된 람다로 실행된다. 

이때 버지니아 북부에 있는 것을 기점으로 각 리전에 람다가 복제되어 생성되기 때문에, 

꼭 생성 시작점은 버지니아 북부로 해야한다.

이슈사항

1. Lambda@Edge에 ‘from PIL import Image’ 식으로 적고 cloudfront와 연동한 후 실행할 시 아래와 같은 오류 발생함

[ERROR] Runtime.ImportModuleError: Unable to import module 'lambda_function': No module named 'PIL'
Traceback (most recent call last):
=> 해결하기 위해, Layer로 추가하는 방법을 서칭

 

1.1 pillow layer를 제공하는 라이브러리 활용
https://velog.io/@silver_bell/AWS-Lambda-PIL-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0-python
https://github.com/keithrozario/Klayers/tree/master/deployments/python3.7/arns

위대로 Klayers에서 제공해주는 여러 arn을 접목해봤는데 모두 지원되지 않는 것으로 노출되어 다음 해결 방안으로 넘어감

 

1.2 cloud9을 통해 layer를 만든 뒤 Lambda layer로 추가하고 cloudfront에 연동
-> 1.2 사항으로 대체 진행
https://kbkb456.tistory.com/114

 

cloud9 터미널 입력내용
swlee:~/environment $ pip install pillow -t ~/python/lib/python3.7/site-packages
Collecting pillow
Downloading Pillow-10.1.0-cp39-cp39-manylinux_2_28_x86_64.whl (3.6 MB)
|████████████████████████████████| 3.6 MB 5.8 MB/s
Installing collected packages: pillow
Successfully installed pillow-10.1.0
swlee:~/environment $ cd ~/python/lib/python3.7/site-packages
swlee:~/python/lib/python3.7/site-packages $ ls
PIL Pillow-10.1.0.dist-info Pillow.libs
swlee:~/python/lib/python3.7/site-packages $ zip -r pillow_layer.zip .

swlee:~/python/lib/python3.7/site-packages $ ls
PIL Pillow-10.1.0.dist-info Pillow.libs pillow_layer.zip

swlee:~/python/lib/python3.7/site-packages $ aws lambda publish-layer-version --layer-name pillow_37 --zip-file fileb://pillow_layer.zip --compatible-runtimes python3.7
{
"Content": {
"Location": "https://prod-04-2014-layers.s3.us-east-1.amazonaws.com/snapshots/829532403061/pillow_37-90a9ccda-8d36-46c4-82af-0c38c4b1077a?versionId=wwRkur44giAJJXS2tjv4tjHct_SnCRXe&X-Amz-Security-Token=IQoJb3JpZ2luX2VjENj%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCXVzLWVhc3QtMSJHMEUCIQCao9sJnOx4x9SQcUbNOPzUxgwNd5OC28RfPUEkKTgEfgIgdpz4HOIPHxMv5thivLlFgscPFm2dD4Evs%2FjxEtcNQ7gqrwUIURAAGgw3NDk2Nzg5MDI4MzkiDCYzhN5pzGX9ehr0DSqMBUEUp%2BL5SmJpfzUzuN4rLihjJd28S8a4U5168u9Y2mxqcTzX7DrjmoTBlO%2FpXs%2BWYcNd3%2BR7qzX5tn6SKMwGyIwukkXPY1d4ocTRbHWo0f7nf8KJLU543O8UB6iQ0L9lIU8UQCpzqCKFFZzCvAaeT%2FKvTGapJWrVrRaUnJMNoJRvVTypgzt6d1yb2ua69UvQfwyw%2F8anbJ1R9SOZmnJS5X%2BRWB1YJQBkZ3IMiuXIf%2BYC3b6SV48zK47TpJF04eERJNaaUrK8i9UCh1iD8rRlrgm5R59dYoY1xw1NiZ0OyjAngXc1DmMYRQfr9nQcXtK8iw1Tjc3uyrAOGiZJCMuhGHpa27VLQBuThvqFPU0rKHY95x46nVlIGe3v3oh0CicaCON2NRPCKD6h4n%2BKmoPauZeorH1W8UHUUfGv2ilwJcKMpcQXCGgjxnqtDlZ08igrBDVgfNOyp2CnU%2F0d3lQYGd8%2BbX6tp9RAv5D9zvXOb0TchGHWz25tt4vx0HL7AzQi9aR99fSVHI6LRUbCeqmBgCnwmTMEaVJD18YYczCxtMvuMPKavlM%2BW%2FwzBxIR50KlVSL%2FwrLIDqtjqr5HdBvnOt9b3GBjfokHjck4cu1HUBSwTWby6iK4MUdYgzBL991tmGISoA4AApJugU0sF6P0ExRzEFwblv%2BrdUBa16COhc%2BNbNXXKj%2FUwiv6z%2Fc6A5fyXHvztNGoGiC1Y4e1jk4QAF0poCoP8%2BW%2BoAs9LHZIlfsp24%2F5383D%2BQr9WEZj0l2pA1R1gM9DH4p%2Batjkt4rj9VIqyJ7GBAKRA3A6%2BG%2FahU8A58xsDL2dWFxfTML6LyFOA4GhytCFSYwhxErLX3CmIvX66%2BOnyg1WLKmpgz0woIjpqwY6sQEqH6YMqkm9j5t1ynQRm46fPQO81n6kWBTFhHUuOwDK98fSwC0LgssRacJv7jMKdu7aobpb95OGz8x6Fh6gbiCJa5qdvr1u37ve6f8XVdb%2BZctN%2FNNXxz3klAB5j9QRTuyPB2LvsZhdxCIzrHwEjyHrybd1EBUF8khR9qNjq7cb3ER%2FEqGij2XoOVHQXB9ejw50vS97pYrZ%2FULUzAruwPD3Lc2svJi276l%2FIzTSgBQbNEY%3D&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20231214T001817Z&X-Amz-SignedHeaders=host&X-Amz-Expires=599&X-Amz-Credential=ASIA25DCYHY345WUH6MS%2F20231214%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Signature=64b4c79e064e7c2dcb149468da3abb7b7c1cec787ac84d7590604aa46b8aa661",
"CodeSha256": "1b5jJRvCci5Y5dnfmsZMVlhEr+Hkoxfah5A98ablANg=",
"CodeSize": 4002364
},
"LayerArn": "arn:aws:lambda:us-east-1:829532403061:layer:pillow_37",
"LayerVersionArn": "arn:aws:lambda:us-east-1:829532403061:layer:pillow_37:1",
"Description": "",
"CreatedDate": "2023-12-14T00:18:22.563+0000",
"Version": 1,
"CompatibleRuntimes": [
"python3.7"
]
}

위에 있는 LayerVersionArn을 아래 이미지와 같이 설정하였음

 

그러고 cloudfront에 연결할 시, 아래 오류 발생함

오류내용
The function cannot have a layer. Function: arn:aws:lambda:us-east-1:829532403061:function:resize-convert-image-mk3-37:6

 

 

왜 그런걸까 조사해보니 아래와 같았다.

 

 

 

이제 어떻게 해결하지?
layer말고 Lambda 자체에 pillow 모듈을 통째로 넣는 방법이 있어, 해당 방법으로 진행해보기로 하였다.

 

1.3 pillow 모듈을 통쨰로 Lambda 함수에 직접 포함시키기

  1. 로컬에서 패키지 설치 및 코드 작성
    pip install Pillow -t /path/to/your/code
  2. 해당 폴더에 lambda 작성
  3. 모두 한 폴더에 모은 뒤, 해당 폴더를 lambda에 옮김
    (왼쪽 상단의 resize-convert-image-mk3-37 폴더를 lambda에 옮겼음

 

 

 

4. 헌데, 실행할 시 아래와 같은 오류가 도출됨

[ERROR] Runtime.ImportModuleError: Unable to import module 'lambda_function': cannot import name '_imaging' from 'PIL' (/var/task/PIL/__init__.py)
Traceback (most recent call last):

 

 

대체 이유가 뭘까....

했지만 명확히 판별되지 않던 와중, 동일한 문제로 골머리를 앓고있는 아래 글을 보았고 참고하여 해결을 진행해보았다.
https://velog.io/@kpl5672/CloudFront-LambdaEdge%EB%A1%9C-on-demand-image-resize-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0

=> 허나 이번에는 아래 오류가 발생하였다.

[ERROR] Runtime.ImportModuleError: Unable to import module 'lambda_function': No module named 'PIL'
Traceback (most recent call last)

 

PIL 모듈을 계속 못 읽으니, 아래와 같이 한번 맞춰봄
그 결과 정상작동 됨 (lib 관련 오류도 발생하여 PILLOW.libs도 추가함)

 

* 허나 추후에 webp 확장자 변환 시 해결이 안되는 이슈가 있어, 아래와 같이 맞추니 해결되었다.

 

아~ 드디어 이부분이 해결됐다.

이제 python으로 리사이징 및 확장자 변환코드를 구성하였다.

리사이징 및 확장자 변환은

URL접근 시, 쿼리스트링으로 붙어있는 h,w,f 값을 통해 동적으로 이미지 리사이징 및 확장자 변환을 할것이다.

 

캐싱을 위해, 아래와 같이 CF 동작 편집을 하면 더 좋다

 

 

Python Lambda@Edge 코드


* response의 값에 있는 BodyEncoding 키값을 base64로 설정해줘야한다 (이거 안하면 무조건 안됨)

https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/lambda-examples.html

import urllib.parse
import boto3
from PIL import Image
from io import BytesIO
import base64

def lambda_handler(event, context):
    request = event['Records'][0]['cf']['request']
    uri = request['uri']
    
    query_string = request['querystring']
    query_string_parameters = urllib.parse.parse_qs(query_string)

    s3_object_key = urllib.parse.unquote(uri[1:])

    s3 = boto3.client('s3', region_name='각자 리전 맞춰서')
    response = s3.get_object(Bucket='버킷이름', Key=s3_object_key)

    # read를 이용하여, 해당 객체의 데이터를 바이트로 읽어오게되며, 이미지 파일이라면 이 바이트 데이터를 이용하여 이미지를 처리할 수 있다
    image_data = response['Body'].read()

    h = int(query_string_parameters.get('h', [600])[0])
    w = int(query_string_parameters.get('w', [600])[0])
    f = query_string_parameters.get('f', ['JPEG'])[0]

    processed_image_data = process_image(image_data, height=h, width=w, format=f)
    
    # 이미지 데이터를 base64로 인코딩 후, base64로 인코딩된 바이트 데이터를 utf-8 문자열로 디코딩
	# => 바이트(이진) 데이터를 base64를 통해 ASCII문자열 데이터로 인코딩 한다음, utf-8로 디코딩
    # 왜 그래야하나?
    # => HTTP 응답의 body에는 텍스트 데이터만을 담을 수 있기 때문이며, 이미지 데이터와 같은 이진 데이터를 텍스트로 표현하기 위해서 Base64 인코딩을 사용함.
    # => 그 후 인코딩된 결과를 UTF-8 문자열로 반환하여 사용함. 이렇게 변환된 base64 문자열을 HTTP 응답의 body로 설정하고 BodyEncoding을 base64로 설정하여 Lambda@Edge에게 이 데이터를 base64로 디코딩해야 함을 나타냄
    base64_image_data = base64.b64encode(processed_image_data).decode('utf-8')
    
    response = {
        'status': '200',
        'statusDescription': 'OK',
        'body': base64_image_data,  
        'bodyEncoding': 'base64',  # Lambda@Edge에서는 base64로 설정해야 하는 경우도 있음
        'headers': {
            'content-type': [{'key': 'Content-Type', 'value': f'image/{f.lower()}'}]
        }
    }
    
    return response

def process_image(image_data, height, width, format='jpeg'):
    # Pillow 라이브러리를 사용하여 이미지 크기 조정
    # BytesIO는 바이트 데이터를 파일처럼 다룰 수 있게 해주는 파이썬 모듈 중 하나.
    # Image.open()은 파일이나 파일과 유사한 객체를 기대하는데, 바이트 데이터는 파일이 아니기 때문에, 이를 파일처럼 다루기 위해서 BytesIO를 사용하여 바이트 데이터를 파일처럼 처리할 수 있게 하였다
    image = Image.open(BytesIO(image_data))
    image = image.resize((width, height))

    # 이미지를 처리한 후, 그 결과를 바이트 형태로 저장하기 위해 임시 버퍼를 생성하는 부분.
    output_buffer = BytesIO()

    # 이미지 포맷을 대문자로 변환하여 저장
    format = format.upper()

    # Image.save()는 파일이나 파일과 유사한 객체를 기대하는함, 동일한 이유로 이미지를 바이트 데이터로 직접 저장하기 위해 BytesIO로 만든 버퍼를 사용함
    # image.save를 통해 output_buffer에 이미지를 저장하게 됨
    image.save(output_buffer, format=format)

    # 아래 메서드를 통해 버퍼에 저장된 전체 데이터를 바이트로 얻어옴
    processed_image_data = output_buffer.getvalue()

    return processed_image_data

 

최종 모양새는 아래와 같음.

import urllib.parse
import boto3
from PIL import Image
from io import BytesIO
import base64

def lambda_handler(event, context):
    request = event['Records'][0]['cf']['request']
    uri = request['uri']

    domain = request['origin']['s3']['domainName']

    query_string = request['querystring']
    query_string_parameters = urllib.parse.parse_qs(query_string)

    s3_object_key = urllib.parse.unquote(uri[1:])

    bucket_name = domain.split('.s3')[0]
    bucket_region = domain.split('.s3.')[1].split('.amazonaws')[0]

    s3 = boto3.client('s3', region_name=bucket_region)
    response = s3.get_object(Bucket=bucket_name, Key=s3_object_key)

    image_data = response['Body'].read()

    # 쿼리 매개변수 확인 및 기본값 설정
    h = query_string_parameters.get('h')
    w = query_string_parameters.get('w')
    f = query_string_parameters.get('f', ['WEBP'])[0]
    q = query_string_parameters.get('q')
    
    # 크기 조정이 지정된 경우에만 처리
    if h and w:
        h = int(h[0])
        w = int(w[0])
    elif h:
        h = int(h[0])
    elif w:
        w = int(w[0])
    else:
        h = None
        w = None

    # 품질이 지정된 경우에만 처리
    if q:
        q = int(q[0])
    else:
        q = 90  # 품질에 대한 기본값 설정
    
    processed_image_data = process_image(image_data, height=h, width=w, format=f, quality=q)
    
    base64_image_data = base64.b64encode(processed_image_data).decode('utf-8')
    
    response = {
        'status': '200',
        'statusDescription': 'OK',
        'body': base64_image_data,  
        'bodyEncoding': 'base64',  # Lambda@Edge에서는 base64로 설정해야 하는 경우도 있음
        'headers': {
            'content-type': [{'key': 'Content-Type', 'value': f'image/{f.lower()}'}]
        }
    }
    
    return response

def process_image(image_data, height, width, format, quality):
    # Pillow 라이브러리를 사용하여 이미지 처리
    image = Image.open(BytesIO(image_data))

    # 크기 조정이 필요한 경우에만 처리
    if height and width:
        image = image.resize((width, height))
    elif width:
        aspect_ratio = image.height / image.width
        new_height = int(width * aspect_ratio)
        image = image.resize((width, new_height))
        # width 비례한 h 도출 및 리사이징
        # h만 있을 일 없음
        # 둘다 없으면 리사이징 진행 안함 


    # 조정된 이미지 데이터를 바이트로 변환
    output_buffer = BytesIO()

    # 이미지 포맷을 대문자로 변환하여 저장
    format = format.upper()

    image.save(output_buffer, format=format, quality=quality)

    processed_image_data = output_buffer.getvalue()

    return processed_image_data

 

이제 실행을 해보겠다.

 

아래 URL로 접근할 것이다.

https://{CF도메인}/{버킷 객체이름}?h=400&w=400&f=webp

 

와! 잘된다. 

오늘 너무 힘들었다. 잠시 쉬어야겠다.

 

 

추후 고려해야할 점

Lambda@Edge에서는 response.body가 변경된 경우, 1MB까지만 허용됨.
만약 1MB를 넘게되면 S3에 별도로 저장하고, 그걸 리다이렉트 시켜주는 방법으로 풀어야할 수도 있음

 

 

참고문서

https://medium.com/daangn/lambda-edge%EB%A1%9C-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94-on-the-fly-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A6%AC%EC%82%AC%EC%9D%B4%EC%A7%95-f4e5052d49f3

https://github.com/hoony9x/aws-lambda-edge-img-resize-function/blob/master/lambda_function.py

https://velog.io/@su-mmer/CloudFront%EC%99%80-LambdaEdge%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A6%AC%EC%82%AC%EC%9D%B4%EC%A7%95

https://developjuns.tistory.com/53

https://heropy.blog/2019/07/21/resizing-images-cloudfrount-lambda/

https://wired.company/blog_original/?q=YToxOntzOjEyOiJrZXl3b3JkX3R5cGUiO3M6MzoiYWxsIjt9&bmode=view&idx=15848347&t=board

https://lotuus.tistory.com/160

https://lotuus.tistory.com/165

https://docs.aws.amazon.com/ko_kr/lambda/latest/dg/lambda-edge.html

 

댓글