메이킹/메이킹 프로젝트

카카오톡 학식봇 AWS Lambda로 전환하기

트리맨스 2022. 9. 12. 18:26
반응형


2년 6개월 전 쯤, 카카오톡 챗봇으로 학식 정보를 알고 싶어서 봇 서비스를 하나 만들고 운영했던 적이 있다.
https://tre2man.tistory.com/158?category=795873

카카오톡 학식봇 만들기 - 2

카카오 오픈빌더 사용하기 학식을 알려주는 카카오톡 봇을 만들기 위해 필요한 것만 간단히 알아보자. 먼저 봇 제네릭 메뉴를 설정해 보자. 봇 제네릭 메뉴는 채팅창 하단에 고정되어 있는 버튼

tre2man.tistory.com


지금 생각하면 상당히 부끄러운 실력이지만, 그래도 어떻게든 프로덕트(?)를 만들었다는 것에만 심취해서 방치하고 있었다. 그렇게 몇 년이 지나 방치되었던 프로젝트에도 서서히 문제가 생기기 시작했다. 큰 문제는 아니지만, 내가 생각했던 문제는 다음과 같다.

  1. EC2에 직접 올렸기 때문에 인스턴스를 켜 둔 시간만큼 돈이 나갔다. 한달에 서버비만 5만원 정도 지출이 되고 있었다. 대학생때는 서버비로만 월 5만원을 사용할 수 있는 사람이라면 유복한 사람이였고, 안타깝게도 나는 유복하지 못했다. 나를 포함한 대부분의 사람들은 비싼 가격이라고 생각되었을 것이다.
  2. 배포가 너무 불편했다. 기존의 서비스는 EC2에 접속하고, 파싱 프로그램 돌리고, nohup으로 앱서버 돌리고... 여튼 코드가 변경되었을 때 배포하기도 불편했으며 시간이 지나면 배포 방법을 까먹어서 계속 찾아보고 그랬다.
  3. 코드가 너무 보기 불편했다. 각 기능에 따라서 파일 구조를 나누고 주석도 좀 달려있고 그래야하는데, 코드를 나중에 보고 있자 하니, 과거의 내가 원망스럽기 마련이였다.
  4. 보안에 취약하다. 기존의 API 앤드포인트는 누구에게나 열려있었기 때문에 악의적으로 DDOS 공격을 날릴 경우, 내 서버는 과부하가 걸릴 예정이였다. 다행히도 그런일은 없었지만, 혹시나 그런일이 생겼을 경우에 난감해질 우려가 있을 것이다.


정리해보니 생각보다 문제가 많네? 🙀 그래서 모든 문제를 해결하기 위해서 예전에 작성한 코드를 새로 뜯어고칠 각오를 하고, 새로운 서버를 만들기로 했다.

문제 해결 구상하기


위에서 말한 문제들을 해결하기 위해서 하나씩 생각해 보았다.
1번 문제는 비용의 문제였다. 현재 서버는 대화 요청이 월 10000건도 안되는데...월 5만원씩 내는건 너무 불합리하다고 생각이 되었다. 그래서 API 요청 수에 따라서 비용을 청구하는 서비스 또는 서버 비용 자체가 적은 서비스를 찾다 보니 서버리스 라는 기술이 눈에 보였다. 서버리스는 API 요청 수 및 할당한 메모리 양에 따라서 요금이 달라진다. 만약 1024MB의 메모리를 먹는 함수에서 월 10000건의 요청이 들어오는데, 넉넉잡아 1개 요청당 500ms의 시간이 걸린다고 하면 월 110원(???) 이 나온다. 여기에 임시 스토리지도 사용하지 않으니까 매우 저렴하게 이용이 가능하다. 계산은 아래 사이트에서 진행했다.
https://calculator.aws/#/addService/Lambda

AWS Pricing Calculator

calculator.aws


2번 문제는 배포의 문제였다. 하지만 이러한 고민도 서버리스를 사용하면 바로 해결이 된다. 서버리스의 배포는 간단하게 sls deploy 명령어를 입력하면 (물론 세부 설정이 필요함) 자동 배포가 되며, 서버 관리에 대한 공수를 아마존에 맡김으로서 해당 공수도 줄어들게 된다. 여기에 github action을 연결하여 배포마저 자동으로 연결하면 github에 코드를 업로드하는 동시에 바로 배포까지 자동으로 된다.

3번 문제는...순수하게 개발자의 문제이다. 이것은 내 문제가 맞다. 마음이 아프다. 그래서 이번에 새로 작성할 프로그램은 파이썬이 아닌 익숙한 타입스크립트로 작성할 예정이다. 역시 type-safe한 언어는 나랑 조금 맞는 것 같다. (조건부)

4번 문제는 보안에 관련된 설정을 추가해야 한다. 서버리스로 배포할 예정인데, 역시 가장 유명한 아마존의 람다 서비스를 사용할 것 같다. 이 때 API Gateway에서 API 호출을 할 때 key를 사용해서 접근하는 방법이 있다. 만약 설정한 key가 요청한 key와 다를 경우, 람다 함수 호출을 막아버린다. 어차피 호출이 월 1억건을 넘지 않을 테지만...다행히도 API Gateway단에서 먼저 호출 가능 유무를 판별할 수가 있다. 그래서 잘못된 key가 들어와도 람다 호출 횟수엔 영향을 미치지 않는다.

한가지 더 문제가 있는데, 기존의 코드는 유저의 정보를 로컬 excel파일로 저장하여 식당 정보와 시간 정보를 매칭시켰다. 하지만 서버리스는 콜드 스타트를 하게 되면 해당 정보가 사라지게 된다. 그래서 찾아본 것이 서버리스 기반의 DB인데, DynamoDB 라는 것이 눈에 들어왔다. 어차피 key, value 두 개의 정보만 저장할 것인데 DynamoDB의 온디맨드 모드로 사용하게 되면 비용이 매우 절감될 것 이라는 예측을 했다. 그래서 온디맨드 모드, 월별 읽기 및 쓰기 수 10000번으로 계산하니 월 500원도 나오지 않게 된다. 그래서 유저 정보에 대한 DB는 DynamoDB를 사용하기로 했다.

프로그램 작성하기


코딩은 그냥 하면 된다.
serverless 프로젝트를 처음 생성하게 되면 Javascript 프로젝트로 생성이 되는데, 이에 대한 몇 가지 설정을 하게 되면 Typescript로 생성이 된다. 해당 방법은 아래 링크에 잘 나와 있다.
https://tre2man.tistory.com/299

Typescript를 이용한 어플리케이션 Lambda에 배포하기

요즘 lambda가 재밌어 보여서 계속 알아보는 중이다. 람다 (서버리스) 애플리케이션에 대한 설명은 다음 사이트에 잘 나와있다. https://www.redhat.com/ko/topics/cloud-native-apps/what-is-serverless 서버리스..

tre2man.tistory.com


DynamoDB는 일단 NoSQL 기반의 DB 서비스다. 구조는 json 마냥 key, value로 이루어진 한 쌍의 데이터를 기반으로 확장하는 구조다. 나의 경우에는 제일 간단하게 key, value만 있는 DB를 제작할 예정이므로 createData(createDB) 및 readData(readDB) 함수를 만들었다. createData 작업의 경우에는 기존에 key가 존재한다면, 자동으로 update 작업이 된다.

export const readDB = async (userKey: string): Promise<UserInfo | null> => {
  try {
    return new Promise((resolve) => {
      dynamo.get(
        {
          TableName: "kumohbob",
          Key: {
            id: userKey,
          },
        },
        (err, data) => {
          resolve(data.Item as UserInfo);
        }
      );
    });
  } catch (e) {
    return null;
  }
};

export const createDB = async (
  userKey: string,
  value: string
): Promise<boolean> => {
  try {
    return new Promise((resolve) => {
      dynamo.put(
        {
          TableName: "kumohbob",
          Item: {
            id: userKey,
            value,
          },
        },
        (err, data) => {
          resolve(true);
        }
      );
    });
  } catch (e) {
    console.log(e);
    return false;
  }
};


이후 학식 데이터 파싱 작업은 axios 및 cheerio를 이용해서 제작했다. 해당 라이브러리를 사용하는 방법은 조금만 구글링하면 잘 나오니, 여기에는 사용법에 관해서는 따로 적지 않을 것이다. 데이터를 불러와서 텍스트만 추출한 뒤, 카카오에서 요구하는 양식에 맞게 값을 반환하면 쉽게 파싱할 수 있을 것이다.

삽질 기록


해당 프로그램을 제작하면서 삽질을 조금 했다. 이에 대한 기록을 정리해 볼려고 한다.

1. 잘 작성했는데, 자꾸 Internal server error를 뱉음!
컴퓨터는 거짓말을 하지 않는다. 클라이언트 입장에서는 해당 에러를 알 수 없으니, cloudwatch에 들어가서 해당 에러에 대한 로그를 볼 수 있다. 해당 로그에는 "Malformed Lambda proxy response" 이라는 에러를 뿜는 것을 확인할 수 있었다. 이에 대한 해결책을 찾아보니...원인은 나에게 있었다. 결론부터 말하자면 응답 데이터의 양식을 지키지 않았기 때문이다. API Gateway에서 반환하는 응답 양식은 아래의 메뉴얼에 당당하게 요구하고 있다. 나는 statusCode가 없었기 때문에 자꾸 저러한 오류를 뿜는 것이였다.
https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-output-format

Set up Lambda proxy integrations in API Gateway - Amazon API Gateway

Set up Lambda proxy integrations in API Gateway Understand API Gateway Lambda proxy integration Amazon API Gateway Lambda proxy integration is a simple, powerful, and nimble mechanism to build an API with a setup of a single API method. The Lambda proxy in

docs.aws.amazon.com


2. 파싱이 되다 안되다 그런다.
이거는 진짜 모르겠다. 하루 지나서 배포하니까 갑자기 잘 되는데, 이거의 원인을 잘 모르겠다. 성능 문제라고 생각되면 메모리를 늘리거나, 실행 최대 시간을 늘리는 것도 하나의 방법이 될 것 같다.

3. API Gateway에 key 적용
이거는 serverless 공식문서에 있긴 한데 솔직히 이해가 잘 안가서, 아래의 플러그인을 사용했다.
https://www.serverless.com/plugins/serverless-add-api-key

Serverless Framework: Plugins

The Serverless Framework Plugin Registry. Search thousands of Serverless Framework plugins.

www.serverless.com

해당 플러그인을 사용하면 손쉽게 apiKey를 생성하고 key의 값까지 바로바로 생성할 수 있었다.

4. 환경 변수 분리하기
민감한 정보에 대해서는 무조건 공개된 장소에 올리면 안된다. 해당 정보들은 남들이 알 수 없는 곳에 저장하는 것이 원칙이다. 그래서 나의 경우에는 로컬 환경에서는 dotenv, github action 환경에서는 secret에 모든 키들을 저장해 두었다. 이렇게 해야지 내가 작성한 코드를 공개하면서도, 민감한 정보에 대해서는 숨길 수 있다. secret은 repo를 public으로 해도 소유자가 지정한 공동 작업자 이외에는 secret을 볼 수가 없다. secret은 다음 사진의 빨간 네모에서 확인 가능하다. (좌측 하단)

severless.yml에서 환경변수를 불러올 때는 ${env:환경변수} 형식으로 불러오면 된다. 이게 github action에서 secret을 불러오는 방법이랑 달라서 많이 헷갈렸다. (github action에서 secret을 불러올 때는 ${{ secrets.시크릿 }} )

service: kakaobob-lambda

frameworkVersion: "3"

plugins:
  - serverless-plugin-typescript
  - serverless-offline
  - serverless-add-api-key

custom:
  apiKeys:
    - name: kakaobob_key
      value: ${env:X_API_KEY}

provider:
  name: aws
  runtime: nodejs14.x
  region: ap-northeast-2
  memorySize: 1024
  timeout: 30
  environment:
    MY_AWS_ACCESS_KEY_ID: ${env:AWS_ACCESS_KEY_ID}
    MY_AWS_SECRET_ACCESS_KEY: ${env:AWS_SECRET_ACCESS_KEY}

functions:
  main:
    handler: src/controller.main
    events:
      - http:
          path: /message
          method: post
          cors: true
          private: true


5. 람다의 예약된 환경변수
처음에는 AWS credential에 사용될 환경변수를 AWS_ACCESS_KEY_ID 로 지정했으나, 해당 환경변수를 람다에 배포하려는 순간 에러가 떴다. 에러 메시지를 해석하니, 예약된 환경변수로 배포가 불가능하다는 것이다. 이게 뭔 소리인가 확인하니, 람다를 배포할 때 특정 환경변수들은 이미 예약이 되어 있는 것 같았다. (람다에 배포할 때만 에러가 뜬다! ci 환경에서는 에러 안뜸) 아래 참고 자료에 관련된 링크가 있다.

6. serverless에 아마존 계정 정보 인증하기
aws-cli 깔고 별 짓을 다 해야 하는 줄 알았는데, 환경변수로 AWS_ACCESS_KEY_ID 와 AWS_SECRET_ACCESS_KEY만 잘 적용해 주면 알아서 적용이 된다. 이것도 아래 링크에 달아 두었다.

7. 시간대 문제
람다 내부의 시간대 기본값은 UTC이다. 그래서 코드단에서 직접 timezone을 설정해 주어야 한다. 나는 dayjs를 사용하고 있으므로 utc + timezone 플러그인을 사용해서 Asia/Seoul 지역으로 timezone을 수동으로 설정해 주었다.

코드 전체


https://github.com/tre2man/kakaobob_lambda

GitHub - tre2man/kakaobob_lambda

Contribute to tre2man/kakaobob_lambda development by creating an account on GitHub.

github.com

급하게 만드느라 코드가 영 좋지 못하다... 나중에 불편한 기능들을 수정할 필요가 있어 보인다.

참고자료


https://www.serverless.com/framework/docs/providers/aws/guide/variables

Serverless Framework Variables

The Serverless Framework documentation for AWS Lambda, API Gateway, EventBridge, DynamoDB and much more.

www.serverless.com

https://docs.aws.amazon.com/ko_kr/apigateway/latest/developerguide/api-gateway-api-key-source.html

API 키 소스 선택 - Amazon API Gateway

API 키 소스 선택 사용량 계획을 API와 연결하고 API 메서드에서 API 키를 활성화할 때 API에 수신되는 모든 요청에는 API 키가 포함되어야 합니다. API Gateway는 이 키를 읽고 사용량 계획에 있는 키와

docs.aws.amazon.com

https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html

Using AWS Lambda environment variables - AWS Lambda

In some cases, you may need to use the following format: region = os.environ.get('AWS_REGION')

docs.aws.amazon.com

https://www.serverless.com/framework/docs/providers/aws/guide/credentials#using-aws-access-keys

Serverless Framework - AWS Credentials

The Serverless Framework documentation for AWS Lambda, API Gateway, EventBridge, DynamoDB and much more.

www.serverless.com


반응형