본문 바로가기
Back-end

AWS 서버리스 단축URL 서비스 만들기 - 3

by 노아론 2021. 7. 25.

이전 글 : AWS 서버리스 단축URL 서비스 만들기 - 2

이번 글에선 본격적으로 Lambda에서 코드를 작성해 볼 것이다
간단한 기초 사용을 다루고 단축 URL에서 사용할 기능 구현에 대해 진행해본다.

 

1. AWS 람다 다루기

AWS Lambda 페이지에 들어가서 함수를 생성한다
Author from scartach를 누르고, 함수 이름과 런타임을 지정한다

런타임은 공통적으로 Python 3.6버전을 선택하고 Create function을 눌러 함수를 생성한다

 

생성된 함수의 모습이다.
Code source에 기본 템플릿이 마련되어있다.

 

 

함수를 실행하면 아래와 같이 응답이 나올 것이다.

{
  "statusCode": 200,
  "body": "\"Hello from Lambda!\""
}

 

Test  탭을 눌러 테스트 이벤트를 작성해본다
기본 템플릿 안에서 파라미터를 받는 별도의 부분은 없기에 아래처럼 Request Body는 "{}" 로 둔다

그리고 Test 버튼을 눌러 실행하면, 어떻게 결과가 나오는지 확인할 수 있다

 

2. vscode에서 람다 함수 다루기

vscode에서 앞으로 작업할 프로젝트를 연 뒤에, AWS Toolkit의 Lambda를 확인해본다
나의 경우, shorten-url-lambda 이름으로 프로젝트를 열었다

우리가 방금 만든 lambda-practice 함수가 나온 것을 볼 수 있다.
해당 함수 이름에 우측 클릭을 하여 Download를 누른다

 

다운로드하길 원하는 폴더를 선택한다.
기존에 shorten-url-lambda 로 폴더를 선택하였기에 이 폴더를 선택하였다.
만일 다른 폴더를 희망하면 Select a different folder로 고르면 된다

 

성공적으로 람다 함수 코드를 받아왔다
선택한 폴더에 람다 함수 이름인 lambda-practice 로 폴더가 생기고, lambda_function.py 파일이 다운로드된 것을 확인할 수 있다

 

우리가 앞으로 만들 단축 URL 서비스 API에는 GET Method를 통해 Path 파라미터를 전달하고, POST Method를 통해 JSON 타입의 Request Body를 전달할 것이다.

그렇다면, 람다에서 값을 어떻게 전달할 수 있는지 알아보도록 한다
간단한 코드를 작성해보았다

import json


def lambda_handler(event, context):
    original_url = event.get("original_url")

    return {
        'original_url': original_url
    }
    
"""

{
  "original_url": "https://google.com"
}

라고 값을 지정하여 전달하면
응답 body로 

{
  "original_url": "https://google.com"
}
를 받게 된다.

"""

 

이제 vscode에 설치한 AWS Toolkit 익스텐션을 이용해서 배포해볼 것이다.

vscode의 AWS Toolkit 탭에서 해당 람다 함수 이름을 우클릭한다.
Upload Lambda... 를 클릭한다

 

업로드하는 방식에 대한 설정 입력을 묻는다.
우린 폴더를 업로드 하는 것으로 진행한다


작업했던 람다 함수 폴더인 lambda-practice를 선택한다

 

디렉토리 빌드 선택 창에선 No를 선택한다.
SAM을 사용하지 않고 선택된 폴더를 압축하는 방식으로 올릴 것이다.

 

이제 위 코드가 어떻게 동작하는지 AWS Lambda 에서 테스트를 해본다

위에서 의도한대로 동일하게 나옴을 확인하였다.
이제 어느 정도 Lambda 함수에서 파라미터를 다루는 법을 알게 되었다.

그럼 본격적으로 단축 URL 서비스 API를 구현해보는 과정으로 넘어간다

 

2. S3 버켓 생성하기

우선 API를 구현하기 전에 할 일 있다.

단축 URL을 생성할 때 hufs.me/q2e4f 형태의 링크로 접속 시 구글로 가도록 하려면, q2e4f라는 값을 어딘가에 저장해야하고, 이 값을 구글 주소인 원본URL과도 연결할 수 있어야 한다

 

단축 URL 서비스에서 아래 노란 박스 부분에 대한 작업이다.

이러한 저장 공간으로 S3를 사용할 것이다
그럼 AWS S3에서 버켓을 바로 생성해보자

 

버켓의 이름은 우리가 이후에 만들 웹페이지 주소와 동일하게 두도록 한다.
만일 도메인을 만들지 않았거나 웹페이지 주소를 따로 생각해둔게 없다면 이후에 다시 S3버켓을 생성하면 된다.
이러한 경우엔 우선 단축 URL 서비스의 버켓임을 알 수 있는 임의의 이름으로 정하면 된다.

Block all public access 에 대해선 모두 해제하고, 경고 창에 뜨는 체크버튼을 클릭하면 된다

이것으로 버켓의 초기 설정은 끝이다.
드디어 본격적으로 Lambda를 사용하여 API를 구현해 볼 것이다

 

3. 단축 URL 생성 API 구현하기

이제 단축 URL 서비스 API 중 Generator API를 만들어 볼 것이다.
단축 URL 서비스 아키텍처에서 두개의 람다 중 노란 박스 부분의 단축 URL을 생성하는 람다 함수를 만드는 차례다.

 

지금까지 진행했던 방법과 동일하게 shorten-url-generator 이름으로 함수를 생성해본다
입력받은 원본 URL을 단축한 형태로 S3에 저장하는 동작을 하게 된다.

 

우선 이 함수에선 boto3 라는 라이브러리를 사용한다.
boto3 라이브러리를 사용하면 Python 코드에서 AWS 서비스와 쉽게 통합할 수 있다.

먼저 핵심 로직을 담은 lambda_handler 함수를 작성해보자

import boto3
import os
import botocore
from botocore.client import Config


def lambda_handler(event, context):
    BUCKET_NAME = os.environ['S3_BUCKET']
    # 환경변수로 관리한다
    NATIVE_URL = event.get("native_url")
    s3 = boto3.client('s3', config=Config(signature_version='s3v4'))
    # S3 연결을 위한 클라이언트

    while (True):
        key_size = int(os.environ["SHORT_KEY_SIZE"])
        # 사이즈 값은 환경변수로 관리한다
        short_id = generate_random(key_size)
        # 생성할 키의 길이를 파라미터로 전달한다
        short_key = "url/" + short_id
        if is_key_available(s3, BUCKET_NAME, short_key):
            break
           	# 키 사용이 가능하면 while문을 나간다
        else:
            print("short_key collision: " + short_key + ". Retrying.")
            # 키가 이미 존재하면 재생성을 위해 다시 while문을 돈다

    is_put_success = put_object_on_s3(s3, BUCKET_NAME, short_key, NATIVE_URL)
    # S3에 오브젝트를 추가하는 함수. 이후 구현할 예정이다

    if not is_put_success:
        short_id = None
        # 오브젝트 추가에 실패하면 key는 실제로 없는 값이 되기에 변수에 None으로 저장한다

    return {
        "short_id": short_id,
        "forward_url": os.environ['FORWARD_HOST'],
        "native_url": NATIVE_URL
    }
   	# API 응답 결과

 

키를 생성하고 S3에 해당 키가 없으면 오브젝트 등록을, 있다면 다시 키를 생성하는 로직이 담겨있다.
생성이 완료되면 웹에서 얻을 short_id 키, 단축 URL에서 사용될 호스트, 원본 URL을 반환한다

generate_random 함수는 단축 URL에 쓰일 랜덤 키를 생성하는 동작을 담는다
랜덤한 값을 만드는 방법은 다양한 방법이 있지만, 이 진행에선 random, string 내장 라이브러리를 이용해서 알파벳 소문자와 숫자를 이용해서 n 크기를 가진 랜덤 값을 만든다 

import random
import string


def generate_random(n):
    return ''.join(random.SystemRandom().choice(string.ascii_lowercase +
                                                string.digits)
                   for _ in range(n))

 

is_key_available 함수는 생성된 랜덤 키가 S3에 등록될 수 있는지 확인하는 동작을 한다
등록될 수 있는지에 대한 방법은 S3버켓에서 랜덤키 값으로 존재하는 오브젝트가 있는가로 확인한다
없다면 (NOT_FOUND_ERROR - HTTP Status code 404) 사용가능한 키로 판별한다
접근이 가능하지 않은 경우에 나오는 status code인 403도 함께 두었다

def is_key_available(s3_client, bucket, key):
    try:
        s3_client.head_object(Bucket=bucket, Key=key)
        return False
    except botocore.exceptions.ClientError as e:
        code = e.response['Error']['Code']
        if code == '403' or code == '404':
            return True
        raise e

 

랜덤 키를 생성하였고 해당 키가 사용가능한지도 확인하였다면
이제 S3에 오브젝트를 추가하면 된다

put_object_on_s3 함수에 해당 동작을 담았다.
boto3로 생성한 s3 클라이언트 객체로 S3 버켓 이름, key, 리다이렉트할 url을 파라미터로 담는다.

우린 오브젝트에서 redirect_location 메타데이터만 저장하고 사용할 것이기에 Body의 값은 빈 바이트인 b'' 로 두었다

def put_object_on_s3(s3, bucket_name, key, redirect_location):
    try:
        s3.put_object(Bucket=bucket_name,
                      Key=key,
                      Body=b'',
                      WebsiteRedirectLocation=redirect_location,
                      ContentType="text/plain")
        return True
    except Exception as e:
        print("Error: ", e)
        return False

 

아래는 전체 코드 내용이다.

import boto3
import os
import random
import string
import botocore
from botocore.client import Config


def generate_random(n):
    return ''.join(random.SystemRandom().choice(string.ascii_lowercase +
                                                string.digits)
                   for _ in range(n))


def is_key_available(s3_client, bucket, key):
    try:
        s3_client.head_object(Bucket=bucket, Key=key)
        return False
    except botocore.exceptions.ClientError as e:
        code = e.response['Error']['Code']
        if code == '403' or code == '404':
            return True
        raise e


def put_object_on_s3(s3, bucket_name, key, redirect_location):
    try:
        s3.put_object(Bucket=bucket_name,
                      Key=key,
                      Body=b'',
                      WebsiteRedirectLocation=redirect_location,
                      ContentType="text/plain")
        return True
    except Exception as e:
        print("Error: ", e)
        return False


def lambda_handler(event, context):
    native_url = event.get("native_url")
    s3 = boto3.client('s3', config=Config(signature_version='s3v4'))

    BUCKET_NAME = os.environ['S3_BUCKET']
    while (True):
        key_size = int(os.environ["SHORT_KEY_SIZE"])
        short_id = generate_random(key_size)
        short_key = "url/" + short_id
        if is_key_available(s3, BUCKET_NAME, short_key):
            break
        else:
            print("short_key collision: " + short_key + ". Retrying.")

    is_put_success = put_object_on_s3(s3, BUCKET_NAME, short_key, native_url)
    if not is_put_success:
        short_id = None

    return {
        "short_id": short_id,
        "forward_url": os.environ['FORWARD_HOST'],
        "native_url": native_url
    }

 

이제 이 코드를 Lambda에 업로드 해본다.
자세한 업로드 방법은 위에서 한 방법과 동일하므로 생략한다.

성공적으로 업로드가 되었다.

 

이제 AWS에서 확인해보자

아래 코드 소스에서 정상적으로 반영됨을 확인할 수 있다.

 

이제 소스코드에서 환경변수로 관리하고 있는 값들을 설정해본다

 

필요한 환경변수인 S3_BUCKET 과 SHORT_KEY_SIZE 값을 설정한다
S3_BUCKET 은 우리가 만든 S3 버켓 이름으로 한다.
만일 아직 도메인을 생성하지 않았다면 임의의 버켓 이름으로 두고, S3 버켓을 다시 생성하고 환경변수 값을 수정하면 된다. 

SHORT_KEY_SIZE는 랜덤키의 길이이다.
원하는 값으로 지정하면 되며 (한 글자로 나타낼 수 있는 값)^(SHORT_KEY_SIZE) 만큼 생성 가능하니 이를 고려하여 정하면 된다.

FORWARD_HOST는 hufs.me/a1zq 에서 hufs.me 값. 즉, 호스트를 나타내는 값이다.
이 또한 아직 도메인을 생성하지 않았다면 임의의 값을 지정해두면 된다

 

그럼 Lambda Test 탭으로 넘어가 테스트를 작성해보자
native_url 를 넘겨 단축 URL 생성 응답을 받아볼 것이다

 

저장하고 테스트를 눌렀더니 아래와 같이 나온다

응답 형태는 정상적인 것 같지만, short_id 값이 예상하던 랜덤 값이 아닌, null로 나온다.
이 문제에 대해서 해결해보자

 

print로 두었던 예외처리 로그에서 출력된 내용을 발견할 수 있다.
읽어보니 PubObject 동작 접근이 거부된 것을 알 수 있다.

 

이러한 접근 거부 문제가 일어난 것은 람다 함수에 대해 권한설정을 하지 않았기 때문이다

따라서 ConfigurationPermission으로 들어가 Execution role의 권한을 수정해본다

 

Attach policies를 누른다

 

검색 창에서 AmazonS3FullAccess 를 검색하여 클릭한다. 그리고 하단의 Attach policy버튼을 눌러 추가한다

Source ARN로는 학습 환경에서의 편의를 위해 모든 S3 버켓에 접근할 수 있는 AmazonS3FullAccess 정책을 사용한다
이후 실제 프로덕션에선 권한을 사용하는 버켓으로만 좁히는 것을 권장한다

권한이 추가되었다면 아래와 같이 추가된 것을 볼 수 있다

다시 Lambda 테스트를 해보니 성공적으로 우리가 원하는 short_id 값을 가지는 것을 확인해 볼 수 있다

 

 

4. 람다를 API Gateway에 연결하기 

지금까지 만들어본 람다 함수는 Test에서만 동작을 하였고, API 엔드포인트로서 작동하는 것은 확인하지 못하였다.
이번 차례엔 API Gateway를 트리거로 연결해서 Lambda 함수를 호출하도록 해볼 것이다. 

이번 과정을 거치면 외부에서 호출할 수 있는 어느정도 모양새를 갖춘 API가 완성된다.

shorten-url-generator 람다 함수에서 Add Trigger를 선택한다.

 

트리거로 API Gateway를 선택하고 Create an API를 선택해 새로운 Gateway를 생성한다
타입은 REST API로 선택한다

Additional settings에서 몇가지 설정을 할 것이다.

API name은 기본으로 생성되나, 희망하는 API이름이 있다면 변경하면 된다.
Deployment stage는 default로 두고, metrics and error logging에 대한 활성화를 체크한다
API에 대한 인증 처리는 따로 두지 않으므로 Security는 open으로 둔다

CORS란 Cross-Origin Resource Sharing의 약자로 다른 origin의 리소스에 접근에 대해 관리하는 정책이다.
내가 운영하지 않는 사이트에서 나의 API에 정보를 요청해서 악의적인 행동을 할 수 있는 것을 막을 수 있다.
더 자세한 설명은 mozilla 문서에서 확인할 수 있다. 

API Gateway가 생성이 되었다.
이제 해당 Gateway로 이동하여(파란 링크를 누르면 된다) 여러가지 설정을 해본다

 

API Gateway의 Resources에서 Create Method 를 눌러 POST Method를 선택한다

 

POST setup에서 Integration type은 Lambda Function으로 둔다.
Lambda Proxy Integration을 사용하면 status code 등 요청과 status code를 포함한 응답 값에 대해 풍부하게 설정할 수 있지만 해당 설정이 필요한 API는 아니기에 따로 활성화하진 않는다.

활성화 하는 경우 람다 함수 코드에서 약간의 변경이 필요하다.
Lambda Proxy Integration은 이후 Forward API에서 다룰 예정이다. 

Lambda Function 칸에는 생성했던 람다 함수 이름으로 지정하고 Save 버튼을 누른다.

 

직접 API 요청을 보내 동작을 확인하기 전에, API Gateway에서 제공하는 테스트를 사용해본다
메소드 실행에 대한 다이어그램에서 Client에 있는 번개 아이콘의 TEST 버튼을 누른다. 

 

요청 Body를 입력하고 Test버튼을 누르면 람다 함수 테스트에서 봤던 것과 같은 응답 Body가 나오는 것을 확인할 수 있다.

 

성공적임을 확인하였으니 Actions에서 Deploy API를 눌러 default stage에 배포를 한다

 

배포 이후, HTTP 클라이언트 도구인 Postman을 이용해서 요청을 보내보았고, 성공적인 응답이 반환되는 것을 확인하였다.

성공적으로 생성이 되었고 S3 버켓에 들어가서 nrb0이 S3에 오브젝트로 저장되어 있음까지 확인해 볼 수 있다

 

nrb0 오브젝트의 Metadata에서도 지정한 원본 URL 값이 담겨있음을 볼 수 있다.

 

이것으로 단축 URL 생성 람다 함수 구현과 API Gateway연결을 마치면서 서비스 API의 한 부분을 완성하였다.

다음 글 에서는 nrb0을 path 파라미터로 넘겨 지정한 URL(https://github.com/roharon)로 리다이렉트하는 동작을 담당하는 Forward API를 구현해 볼 것이다

댓글