Overview

외부에 HTTP 요청으로 자원 공유 시 보안에 특히 신경을 써야 합니다. AWS CloudFront + Lambda@Edge 서비스를 이용하면 HTTP 요청 시 사용자 지정 함수로 원하는 목적에 따라 처리 가능합니다.

이번 글에서는 오리진 서버에게 요청 시 헤더에 Authorization Key 값을 추가하여 인증을 통해 특정 사용자만 자원에 접근할 수 있도록 권한을 추가해보겠습니다.

작업 진행 순서는 다음과 같습니다.

  1. Origin Server 생성
  2. CloudFront 생성
  3. CloudFront 오류 응답 설정
  4. CloudFront 이벤트에 의한 Lambda@Edge 트리거
  5. Route 53 도메인 연결

1. Origin Server 생성 AWS S3 Bucket을 Origin Server로 사용하겠습니다.

1) AWS S3 Bucket 생성

edge

- AWS S3 Bucket에 계층을 만들어 이미지 파일 저장 - CloudFront에서 제공하려는 모든 객체를 AWS S3 Bucket에 배치 - AWS S3 Bucket 해당 자원에 대한 GetObject 정책 추가

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::dev-kwakjs-test/ex-img/*"
        }
    ]
}


2. CloudFront 생성

AWS CloudFront는 콘텐츠를 사용자에게 빠르고 안전하게 전송하는 서비스입니다.

1) Distributions WEB (HTTP통신) 선택

edge

2) Origin Server 설정

edge

- Origin Domain Name : 대상 S3 Bucket - Origin Path : 기본 경로 지정 (ex-img/)

💡이번 글에서는 특정한 최종 사용자에게 권한을 부여하는 용도이기 때문에 권한이 필요한 S3 경로 패턴을 명확하게 지정하여 “ex-img/*” 에 접근할 때만 Lambda@Edge 함수가 실행되도록 하여 비용을 절감합니다.

3) CloudFront Distributions WEB 생성

배포까지 10~15분 정도 소요됩니다.

- 배포 전

edge

- 배포 후

edge


3. CloudFront 오류 응답 설정

Lambda@Edge 함수 실행 후 HTTP 4xx 또는 5xx 오류를 반환할 때 CloudFront에서 최종 사용자에게 반환하려는 사용자 지정 오류 페이지를 만들 수 있습니다.

edge

- 403 오류를 반환할 경우 기본 XML 형식으로 응답

1) 403-response.json 파일 생성

{"meta": {"code": 403, "error_message": "Forbidden"}, "data": {}}

2) S3 Bucket 기본경로 하위 폴더에 업로드

edge

3) Error Pages 생성 Step-1

edge

4) Error Pages 생성 Step-2

edge

- S3 Bucket 에 업로드한 에러 페이지 경로 추가

5) Error Pages 생성 Step-3

edge

- 필요한 오류 상태에 대한 사용자 지정 오류 페이지를 추가합니다. - 현재 CloudFront 에서 사용자 지정 오류 페이지를 반환할 수 있는 HTTP 상태 코드입니다. - 400, 403, 404, 405, 414, 416 - 500, 501, 502, 503, 504

6) JSON 형식의 오류 응답 확인

edge



4. CloudFront 이벤트에 의한 Lambda@Edge 트리거

CloudFront로 생성된 4가지 이벤트 시점에 Lambda@Edge 함수 호출하여 요구 사항에 맞게 로직을 추가할 수 있습니다.

1) CloudFront로 생성된 4가지 이벤트

edge

(1) Viewer Request

- 클라이언트에서 이벤트가 도착하고 들어오는 HTTP 요청에 접근할 때 동작 - 이 특정 이벤트는 요청된 개체가 이미 캐시되어 있는지 여부에 관계없이 동작

(2) Origin Request

- 요청된 객체가 엣지 로케이션에 캐시되지 않았을 경우에만 동작

(3) Origin Response

- 오리진이 요청에 대한 응답을 반환한 후에 동작

(4) Viewer Response

- 엣지 로케이션 뷰어에 응답을 반환하기 전에 이벤트가 동작

💡요청된 개체가 캐시 생성 여부와 상관없이 모든 요청에서 실행되어야 해서 Viewer Request 이벤트 시점에 로직을 추가하겠습니다.

2) Lambda@Edge 함수 추가

서버 관리 부담 없이 전 세계적으로 빠르게 배포 가능하며, User-Agent 헤더를 기반으로 특정 객체를 사용자에게 전송 및 특정 헤더에 대한 접근 제어를 추가할 수 있는 유용한 서비스입니다.

특정 헤더에 인증을 추가하여 응답 헤더 값을 재정의하는 Lambda@Edge 함수를 추가하겠습니다.

(1) Python 소스 코드

# -*- coding: UTF-8 -*-
import json
import logging

logging.getLogger().setLevel(logging.INFO)


def _generate_change_request_info():
    """
    변경할 요청 정보 생성

    401 상태 변경일 경우만 처리

    Returns:
        dict: request (요청데이터)
    """

    headers_request = {
        'cache-control': [
            {
                'key': 'Cache-Control',
                'value': 'no-cache',
            }
        ],
        'content-type': [
            {
                'key': 'Content-Type',
                'value': 'application/json',
            }
        ]
    }

    # 변경할 상태 정보
    change_status_value = 401
    change_status_description = 'Unauthorized'

    add_meta_dict = {
        'code': change_status_value,
        'error_message': change_status_description,
    }

    body_request_dict = {
        'meta': add_meta_dict,
        'data': {},
    }

    body_request_json = json.dumps(body_request_dict)

    request_dict = {
        'headers': headers_request,
        'status': change_status_value,
        'statusDescription': change_status_description,
        'body': body_request_json,
    }

    return request_dict


def lambda_handler(event, context):
    """
    CloudFront ViewerRequest 이벤트처리

    헤더에 Authorization Key 값으로 접근권한 추가

    Returns:
        dict: request (요청데이터)
    """

    logging.info('event::{}'.format(event))

    try:
        request_dict = event.get('Records')[0].get('cf').get('request')

        # 헤더에 Authorization Key 값으로 접근 여부 확인
        auth_key = request_dict.get('headers').get('authorization')[0]['value']

        if auth_key != 'brandi.token':
            logging.warning('유효하지 않은 접근')

            # 변경할 요청 정보 생성 (401)
            request_dict = _generate_change_request_info()

    except Exception as e:
        logging.error('exception::{1}'.format(e.args[0], e))

        return {
            'status': 500,
            'statusDescription': 'Internal server error',
        }

    logging.info('request_dict::{}'.format(request_dict))

    return request_dict

💡오류 응답 설정 시 ‘401’ 상태에 대한 사용자 지정 오류 페이지를 반환할 수 없어, 오류 상태 ‘401’ 경우에만 Body 항목에 Json 형태로 재정의하여 응답 값을 반환합니다.

(2) Lambda@Edge 설정

edge

1. 버지니아 리전에서만 Lambda@Edge 배포 가능합니다. 2. 런타임은 Node.js 10.x, Node.js 8.10, Python 3.7 중 하나로 선택해야 합니다. 3. 이 역할에는 AWS CloudWatch Logs에 로그를 업로드 할 수 있는 권한이 추가되어야 합니다. 4. Memory 128MB 이내로 설정 가능합니다. 5. Viewer Request, Viewer Response 이벤트는 실행시간 5초 이내로 설정 가능합니다.

(3) Lambda@Edge 배포

edge


(4) 버전 게시 (CloudFront Event 연결)

edge

- Distribution : CloudFront Distribution 선택 - CloudFront Event : Viewer Request 선택

(5) Lambda@Edge 함수 생성

배포까지 10~15분 정도 소요됩니다.

- 배포 전

edge

- 배포 후

edge


(6) 결과 확인 - Authorization 미인증

edge

- 유효하지 않은 접근일 경우 오류 코드 ‘401’ 응답 데이터 반환됩니다.

- Authorization 인증

edge

- 인증된 정상적인 접근일 경우 요청한 컨텐츠가 보여집니다.

5. Route 53 도메인 연결

Route 53 는 가용성과 확장성이 뛰어난 클라우드 DNS 웹 서비스입니다. 등록된 도메인에 레코드를 추가 후 CloudFront CNAME 으로 매핑하여 사용자에게 노출될 도메인을 연결하겠습니다.

도메인 등록은 생략하겠습니다.

1) CloudFront CNAME 추가

edge

- Route 53로 등록된 도메인을 CloudFront CNAME 으로 연결합니다.

2) CloudFront CNAME 추가 확인

edge


3) Route 53 레코드 추가

edge

- 배포된 CloudFront Domain Name으로 Target을 선택합니다.

4) 도메인 연결 확인

edge


Conclusion

AWS CloudFront + Lambda@Edge 서비스를 이용하여 HTTP 요청 시 권한을 추가하여 특정 사용자만 자원에 접근하는 방법을 알아보았습니다.
그 외에도 Lambda@Edge 함수로 지능적으로 처리할 수 있는 여러 예제가 AWS 개발자 안내서에 설명되어 있으니 참고하면 될 거 같습니다.

참고 자료

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


곽정섭 과장 | R&D 개발1팀
kwakjs@brandi.co.kr
브랜디, 오직 예쁜 옷만