typescript 프로젝트 + lambda SAM으로 관리해보기

dev_qorh·2022년 8월 8일
1

CatchCatch

목록 보기
8/18

이전에 SAM으로 관리했던 lambda 서비스의 형태를 다시 생각해보며 기존에 초기화 하였던 프로젝트를 SAM으로 관리할 수 있도록 하기로 했다.

AWS 관련 서비스 준비

SAM init으로 접할 수 있었던 위의 초기 프로젝트를 저번에 구축했던 typescript 프로젝트로 연장시키는 것을 목표로 한다. CDK와 같은 IasC를 지향하지 않고 SAM을 사용할 예정이기에 VPC, Route, RDS 및 S3 등은 먼저 구축 후 도입할 수 있는 지에 대해 알아보고자 했다.
vpc를 하나 생성했고,
amazon serverless aurora RDS 클러스터를 하나 생성했다. vpc는 위에서 만든 곳에다가 연결한 후, vpc 보안 그룹에서 rds로 연결되는 경우에 백엔드 5000 포트의 인바운드 설정을 해두었다.
(물론, lambda 환경에서 그것을 어떻게 하는 지는 모르지만, 하다보면 실마리가 나오리라)
구글링을 하다가 찾아낸 lambda + rds 연결 시의 template.yaml 파일의 일부이다. 여기서는 public database subnet group을 지정하였다. 내가 오늘 생성한 vpc에 연결된 rds는 어떻게 되어 있는 지 확인해 보았다.
public과 private로 기본 설정되어 있는 서브넷 그룹의 라우팅 대상이 각각 인터넷 게이트웨이/엔드포인트로 다른 것을 확인했다. 이 정보를 어디서 쓰는 지는 아직 모르지만 우선 계속한다. rds에서 제공하는 host를 사용하여 위 스크립트에서 사용하는 것처럼 연결을 해보고자 한다.

삽질 시작입니다. 다음 인용구까지 스킵하셔도 됩니다.

0. sam 초기화 하기

sam init으로 만든 프로젝트에서 template.yaml만 쏙 빼와 내 기존 프로젝트 최상단에 위치시켜준다.이후 package.json을 비교하여 설치할 패키지를 설치해 주어야 한다. jest와 같은 패키지는 나중에 설치해도 괜찮지만, esbuild의 경우 꼭 가져오도록 한다. (sam의 빌드 방식을 esbuild로 설정하기 때문. 혹은 tsc 컴파일 코드를 빌드 과정에 포함해도 되겠지만)
그리고, 진입점에 대한 handler를 작성하고, template.yaml의 핸들러 위치, code 위치 등을 수정해 준다. handler 작성은 https://docs.aws.amazon.com/lambda/latest/dg/typescript-handler.html를 참고하여 진행한다. 진입점 파일에 로컬 데이터베이스가 일단 있지만 테스트를 위해 일단 남겨두자. template에 기존 helloworld 함수의 property를 내 프로젝트에 맞게 잘 명시한 후 sam local start-api를 테스트 했을 때는 함수를 찾지 못하는 상황이 발생하였다. handler에 명시햇던 index.lambdaHandler가 없다고 하여, 해당 오류를 검색해보니 다들 빌드를 진행하고 테스트를 하는 것을 볼 수 있었다

그래서, sam build 를 진행한 후 다시 start-api를 해보니 127.0.0.1:3000/hello 에 대한 요청으로 잘 동작하는 것을 확인했다. 위에 보이는 fatal ECONNREFUSED 에러는 도커 환경에서 실행된 127.0.0.1:3306에 대한 mysql 연결이 당연하게도 도커 환경에는 mysql이 없기 때문에 실행되지 않은 것으로 보인다.

#### 근데 build를 매번 수행해야 하는거야..?
local 실행 환경을 구성하자. build를 하지 않고 테스트를 할 수 없다는 것은 개발을 10배 더 느리게 진행하겠다는 것과 다름없다. 검색을 조금 해본 결과, node code를 그냥 manual 하게 실행하여 테스트를 진행하는 것을 알 수 있었다.위와 같이 local.ts로 파일을 만든 다음, yarn local 명령어가 ts-node ./src/index.local 로 동작하도록 설정해 주면 express 앱을 실행함으로써 테스트 할 수 있게 되었다.

++ lambda sam 구조에서 local이란
구현하기 힘들다. express 라우팅을 지원하는 라이브러리가 있기도 하지만, express 혹은 우리가 흔히 채택하는 구조의 코드로 배포하는 데에는 추가적인 코드 명세가 꽤나 많이 필요하다. 그 이유로

그래서, 먼저 사용법을 확실히 익히고 배포를 진행함과 동시에 테스트를 하는 것이 정신건강에 이로울 듯 하다.

1. RDS 연결하기

cloudformation - template anatomy documentation에서는 template.yaml의 resources 영역에서 aws 리소스들을 명시할 수 있다고 한다. 이어 property 타입들을 확인해보자.
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbcluster.html

하지만 생성했던 리소스에 대한 import 설정이 없는 것 같아서, 관련 문서를 찾아보았다.
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resource-import-new-stack.html

스택에 기존 리소스들을 import 하거나, 기존 리소스들을 사용하여 새로운 스택을 만들 수 있다고 한다. 기존 리소스들을 사용하여 새로운 스택을 만들고자 할 때는, 각 리소스들에 대한 deletionPolicy 속성이 꼭 포함되어야 한다고 한다. 이것은, 스택이 삭제되어도 리소스가 유지될 수 있도록 하는 옵션이라고 한다. (sam 예제에서 생성된 s3는 왜 삭제되지 않은건가)
또, 리소스에 대한 유니크한 식별자를 사용해야 한다. 이를 위해 각 서비스 콘솔에서 식별자를 알아와야 한다. 바로 위 내용을 적고 7시간 동안 삽질하게 되었다. serverless aurora mysql을 rds 엔진으로 배포하고 엔드포인트로 접근하려고 했는데, vpc 설정을 했는데도 불구하고 자꾸만 접속이 안되는 것이다.

검색을 이리저리 하고서 알았다.

serverless rds는 엔드포인트로 접근하는 것이 안된다고 한다.

지금 생각해보면, 계속 유지하는 방식이 아니기 때문에 그런가보다 싶은 생각이 든다.겨우겨우 삽질 끝에 그냥 rds 인스턴스를 생성했다. 프리 티어를 사용하여 꽤나 무료 범주 안에서 운영을 해볼 수 있을 듯 싶다. 그리고, 인바운드 규칙으로 내 공유기 ip를 설정하여 나만 접속 가능하도록 설정했다. (실제 서비스 테스트에서는 vpc 서브넷 그룹을 동일하게 해주어 접속 가능하도록 인바운드 규칙을 수정할 예정이다)yarn local을 실행하면, typeorm에 의해 schema가 업데이트 되는 것을 확인할 수 있었다. (query가 표출되는 것은 typeorm의 datasource 호출 시 logging을 true로 하였기 때문)

삽질 끝!

sam cloudformation으로 모든 리소스 관리 결정

sam 초기화 부분은 동일하지만, 이후 리소스들은 sam 템플릿으로 모두 관리되도록 한다.
템플릿을 구축하는 순서를 정해야겠다 생각했다. 그렇지 않고서는 많은 옵션들을 선제적으로 채택할 수가 없었기 때문이다. 구현하고자 하는 기능을 우선으로 생각하면,

  1. lambda 함수 구현
  2. lambda 배포를 위한 s3 생성
  3. 공유 코드 구현
  4. rds 생성으로 lambda와 db 연동 코드 구현
  5. vpc 설정, api gateway 설정

lambda 함수를 구현하기 위해서는, 함수를 호출하며 넘겨주는 event에 대해 자세히 찾아볼 필요가 있었다. 그리고 event는 범용적으로 사용될 수 있다는 걸 알게되었다. https://docs.aws.amazon.com/lambda/latest/dg/lambda-services.html
위 문서에서는 lambda 함수가 다른 리소스들과 event를 사용하여 상호작용할 수 있으며 그 목록을 보여주고 있다. 즉, lambda 함수의 event는 독립적으로 구성하기 보다는, Api Gateway의 http 요청 생성을 event로 만든 후 그것을 lambda로 전달할 수 있는 구성이었다.

이전에 예제 sam 템플릿을 배포하여 생성하거나 sam local start-api 등으로 테스트한 함수에 접근하지 않고, 생성된 api gateway 포인트를 사용하여 event를 생성 및 확인할 수 있는지 알아보도록 하자. api gateway는 AWS::Serverless::Api를 템플릿에 명시하는 것으로 사용할 수 있었다.
https://docs.aws.amazon.com/ko_kr/serverless-application-model/latest/developerguide/sam-resource-api.html를 참고하여 template을 구축하도록 한다.
위에서 사용하고자 하는 함수를 api 서비스를 사용하여 만들고자 한다면

sam build 오류 발생


위와 같은 오류가 발생해, debug 옵션을 활성화 한 다음, npm install 구문이 있는 곳을 찾아보았다. 해당 py 파일 내에서 명령어 production을 --omit=dev 로 바꾸어 주면 될 듯 하다. (버전 문제인 것으로 추측, https://stackoverflow.com/questions/9268259/how-do-you-prevent-install-of-devdependencies-npm-modules-for-node-js-package)하지만 안타깝게도 관련 변수로 지정되어 실행되는 데 이 상위 파일까지 가서 옵션을 바꾸는 것 외에 다른 방법이 있을까 싶어 이리 저리 찾아보았다.

  1. debug를 통해 확인된 npmrc 파일에서 --omit=dev, --production=false 옵션 부여 후 실행 -> 실패
  2. 새로 sam init을 통해 만든 소스 build -> 성공
  3. 새 프로젝트의 예제 Hello-world 함수 폴더 본 프로젝트로 복사 후 sam build HelloFunction실행 -> 성공
  4. tsconfig의 내용을 그대로 복사한 후 다시 원 함수 sam build UserFunction(본 프로젝트 함수 가명) 실행 -> 실패

약 2시간을 허비하고 나서 그냥 새 프로젝트를 초기화 하고 내가 손보는 선에서 작업을 이어나가는게 좋겠다는 생각이 들었다. (typescript 프로젝트를 초기화 할 때 잘 모르고 가져다 쓴 코드 규칙 혹은 라이브러리가 많았기에)
aws sam 실행 과정 중 내 컴퓨터의 npm 커맨드 실행이 오류가 있는 것도 아니라서 issue로 남길 수 없었기 때문이다. (프로젝트 중간에 컴퓨터 실행환경을 node 14.x -> 16.x로 업데이트 했지만 새 프로젝트는 잘 실행 되었기에)

새 프로젝트로부터 template 편집하기

global 속성을 통해 리소스들의 공통 설정을 세팅할 수 있다고 한다. 최신 노드 환경인 nodejs16.x를 명시해준다.

위에서 편집했던 것처럼 template을 다시 편집해준다. 다만, helloworld 예제에서 Outputs:HelloWorldApi가 지정되어 있는 것이 중복될 것 같지만 build를 해서 오류가 있다면 구조를 알기 더 좋을 듯하다.sam build 명령어를 입력하니 속성 명시에 오류가 있는 것처럼 보였다. 이럴 경우 하나하나 찾는 것 보다 sam validate --profile {설정되어 있는 프로필} 명령어를 사용하면 위와 같이 잘못된 부분을 알려주니 참고하도록 한다.
배포 시도 시, 위와 같은 에러가 발생한다.

이는 위에서 추측한대로 api 게이트웨이 리소스를 명시하고 helloworldfunction과 연결해주었지만 output에서 다른 게이트웨이를 또 helloworldfunction과 연결하고 있기에 unresolved 되었다고 하는 듯 했다. output에 내용을 삭제해주고 sam build를 다시 진행한 후, sam deploy --guided --profile {설정 프로필}으로 배포해주도록 하자.
잘 생성된 것을 확인할 수 있다!하지만 접속 링크를 잘 보면,, //dev/hello인 것을 확인할 수 있다. 이는 template에서 path를 설정할 때 /hello로 명시해주어서 /가 두 번 입력된 패스가 생성된 것으로 추측된다.. 나중에 바꿔주어야 할 듯 싶다.

1. lambda 함수들이 공유할 레이어 작성

lambda 함수 각각이 db 커넥션을 유지하거나 공통된 함수를 갖게되는 것은 상당한 자원 낭비이다. 이에 lambda는 레이어라는 개념을 도입하여 모든 요청들이 통과할(실행할) 공유 리소스를 제공할 수 있게 한다. 구축된 레이어를 각 lambda 함수마다 사용해줄 것으로 명시해준다면, microservice 형태의 인프라를 구축할 수 있게된다. (사용하지 않아도 가능하지만 분명히 더 효율적으로 가능하다)

https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html
SAM에서 layer를 사용하는 것은 AWS::Serverless::Function의 Properties:Layers에 설정을 명시해 주면 되고, 설정 내용은 cloudformation에서 구축한 layer의 Arn을 명시해주면 된다고 한다.

Resources:
  function:
    Type: AWS::Serverless::Function
    Properties:
      Handler: index.handler
      Runtime: nodejs12.x
      CodeUri: function/.
      Description: Call the Lambda API
      Timeout: 10
      # Function's execution role
      Policies:
        - AWSLambdaBasicExecutionRole
        - AWSLambda_ReadOnlyAccess
        - AWSXrayWriteOnlyAccess
      Tracing: Active
      Layers:
        - !Ref libs
  libs:
    Type: AWS::Lambda::LayerVersion
    Properties:
      LayerName: blank-nodejs-lib
      Description: Dependencies for the blank sample app.
      Content:
        S3Bucket: my-bucket-region-123456789012
        S3Key: layer.zip
      CompatibleRuntimes:
        - nodejs12.x      
    

layer의 arn은 template 내에서 생성한 layer의 logical name을 ref로 사용하면 얻을 수 있다고 한다. 위의 예제와 같이 libs 레이어를 function에서 !Ref libs로 사용하는 것을 확인하자.

사용하는 방법을 알았으니 이제는 만들어야 한다. 공식 문서에서는 빌드 과정에서 layer를 zip 파일로 압축하는 과정이 보이지 않았다.

여기서부터 아래 인용구가 나오기 까지는 삽질입니다. 넘어가셔도 됩니다.

아래 내용에서는 AWS::Serverless::LayerVersion 리소스가 역할을 수행할 수 있음을 말해주고 있다. node 환경에서는, zip 파일 속에서 layer 폴더를 만든 후 {만든 layer 폴더}/nodejs/node_modules/ 에 설치된 종속성 파일들을 넣어주면 된다고 한다.
https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/building-layers.html
https://github.com/aws-samples/aws-lambda-layers-aws-sam-examples/blob/master/sharp-layer/template.yaml
위 링크는 aws blog에 게제된 예제이다. layer를 단독으로 template으로 사용하고 있어서 헷갈릴 수 있지만, 결국 구조는 아래와 같이 구성된다.

testLayer
└───nodejs   
│	└───node_modules
│		│	module A
│		│	module B
│		│	...
│
│	index.js
│	package.json

신기한 구조이지만, 우선 위 구조를 토대로 코딩해본다.

layer 폴더 아래에 nodejs/node_modules를 위치시킨다. index.js에서는 간단한 함수를 export 해주고 template에서는 AWS::Serverless::LayerVersion를 명시해주도록 한다.

여기서 중요한 점은, 다른 예제에서 Content 속성으로 S3버킷의 경로를 명시한다는 것과 달리 sam build 과정에서 layer도 빌드되도록 ContentUri를 명시해주었다는 점이다.sam validate로 체크해준 후, sam build를 진행하고, sam deploy까지 완료해본다.

++layer에서 test함수를 export 했지만 사용하는 부분이 없다는 것을 깨닫고 사용에 대한 자료를 찾아보았다. 아래는 aws 페이지에서 node 환경의 layer를 사용하는 예시이지만 이에 착안하여 명시해줄 수 있을 것 같았다.

https://www.youtube.com/watch?v=-r4GJlkdJo0&t=164s

레이어를 만든 다음 mysql을 포함하게 하였고 그것을 handler 함수에서 사용할 수 있는지 확인하던 도중, import mysql from 'mysql' 구문이 되지 않는 것을 확인하였다. 에러를 찾아보니 https://stackoverflow.com/questions/61325987/aws-lambda-layers-error-when-call-api-cannot-find-module 위 글에서, node_modules는 레이어의 nodejs/node_modules의 모듈들을 모두 lambda 런타임 시 opt/ 아래로 간다고 설명한다. 이해 하기 쉽게 console.log(process.env.NODE_PATH)를 출력해보니답변들 중에 이 절대 경로를 사용하여 import ~ from '/opt/nodejs/node_modules/~' 모듈을 사용하는 것은 하드코딩의 방식이기에 지양하였고 정상적으로 폴더 구조를 구성해준 다음 import ~ from '~'처럼 일반적으로 패키지를 불러오고자 하였다.

삽질 끝, 여기서부터 제 기준에서 작동되는 방법이었습니다.

단순하게도 오류는 런타임 환경에서 모듈을 불러오지 못한다거나, import하였지만 undefined라던가 하는 문제였다. layer 폴더를 만들고 모듈을 옮기거나 하는 방법은 되지 않았고, sh이나 bat파일로 빌드 후 node_modules를 layer에 담은 후 zip파일로 압축하는 스크립트를 짜는 것은 확실하였으나 더 편한 방법을 찾고 싶었다.

https://github.com/Envek/aws-sam-typescript-layers-example/blob/master/Makefile
여기서는 layer를 만드는데에 makefile을 사용한다.다만 node_modules 폴더를 옮기는 것보다는 package.json을 옮긴 후 종속성을 설치하는 방식이다. layer의 ContentUri를 루트 폴더로 지정하는 것을 보고 makefile을 사용하지 않아보기로 했다. 그랬더니 최상단에서 관리되는 node_modules가 알아서 레이어로 생성되었고 자연스럽게 내가 template에서 명시한 함수는 따로 종속성을 갖지 않는 구조가 되었다.

당장은 production 시의 node_modules 관리가 필요하다는 불편함 밖에는 없으나, package.json을 옮기거나 sh 스크립트로 node_modules를 옮기는 방법들 또한 개발과 배포 단계의 node_modules는 어차피 다르기 때문에 수동으로 해야하지 않을까 싶다. 결론적으로 스택 배포에 성공했고, 다음과 같이 mysql 모듈의 로드에 성공했다.

여러가지 패키지를 추가해서 배포를 시도했을때 용량 초과되는 문제가 있었다. 이는 typescript 빌드를 위한 esbuild 사용에서 production으로 설치되지 않아 모든 종속성이 설치되기 때문에 발생되는 문제였다. 해결을 위해서 esbuild의 옵션을 지정하기엔, aws sam의 python 코드 내에서 invoke 되는 명령어의 파라미터 등을 명시적으로 바꿔야 했기에 다른 방법을 찾아보았다. makefile을 사용하려고 시도했을 때는 빌드 후의 파일을 사용하여 로컬에서 테스트가 되지 않았던 점과, sam deploy 시에 내부에서 실행되는 sam build 과정이 완전하지 못하여 실패하였다.
결국 aws cloudformation package 옵션을 사용해서, 다음과 같은 과정을 거친 후에야 제대로 된 개발을 진행할 수 있었다.
1) api 작성
2) yarn run build를 포함한 make 파일 수동 실행으로 build
3) package을 사용하여 template에서 지정한 빌드 후 폴더를 압축하여 s3에 업로드
4) sam deploy를 사용하여 package로 생성된 template으로 배포
sam template에서 함수와 레이어의 경로를 build 결과물의 경로로 잡아주었기 때문에, sam local start-api 와 같은 로컬 테스트도 가능했다.

2. secret key 사용하기

layer를 구축하는 데 성공하였으니 kakao api 로그인하는 함수를 만들고 이를 테스트 하도록 한다. kakao api를 사용하기 위해서는 kakao 측에서 등록된 key 값을 사용하여야 하는데, 이 값들을 코드에 노출시키는 것은 위험하므로 따로 관리할 수 있는 방법을 찾기로 한다.
Secret Manager에서 새로운 키값을 만들게 되면, aws-sdk를 사용해 소스코드 내에서 secret manager에 접근, 필요한 키 값들을 가져올 수 있게 된다.
하지만 만드는 것이 다가 아닌, function의 권한 설정등도 필요하다. template 작성 방법은 아래 글을 참고 했다.
https://medium.com/nerd-for-tech/how-to-use-aws-secret-manager-and-ses-with-aws-sam-a93bb359d45a

template.yaml에 한글이 들어가면 안된다. sam cli의 실행 도중에 encoding 에러가 발생할 수도 있다.

테스트를 위한 코드를 작성하고 결과를 확인해보면,정상적으로 Secret Manager의 키 값을 가져올 수 있는 것을 확인할 수 있다. 에러코드는 axios 호출 실패의 예외처리가 출력된 것이니 신경쓰지 않아도 된다.

3. rds 연동하기 (with VPC)

앞서 rds를 만들면서 vpc를 생성했다. db접근을 허용할 ip를 설정할 수 있었고 설정된 ip 외에는 접근이 차단되었다. subnet은 생성하지 않았었는데, lambda 함수를 생성한 시점에서 rds와 연동되는 subnet과 접근 권한 설정 등을 진행해본다.

코드 내에서 rds와 연결하기 위해서는 username, password 등의 정보가 필요하다. api key를 secrets manager에서 가져온 것과 같이 db정보도 secrets manager가 관리할 수 있도록 하자.
https://aws.amazon.com/ko/blogs/security/rotate-amazon-rds-database-credentials-automatically-with-aws-secrets-manager/ db 정보도 secret manager로 관리가 되도록 위와 같이 코드를 작성한 다음, template.yaml에 사용할 새 lambda 함수를 작성하고 local에서 테스트 해보았다.

하지만 local 환경에서는 매번 image를 빌드하여 global scope의 변수에 대한 유지를 확인할 수 없는 것으로 보였다. 그래서 위 내용을 토대로 빌드 후 배포하여 실행환경에서 어떻게 동작하는 지 확인해보고자 하였다. 코드에서는 전역 변수인 dbConnection이 없을 경우 dbConnection 변수를 연결로써 사용하게 한다. 만약 연결이 있다면, init을 쏘는 간단한 케이스이다.초 회 호출 시
이 후 호출 시

다행히도, 호출되어 생성된 컨테이너가 전역 변수로써 연결을 지속하고 있는 모습을 확인할 수 있었다. 또, 작성한 대로의 typeorm이 잘 먹히는 것도 확인할 수 있었다.

일단 마무리

우선 이 정도로 프로젝트를 유지한 다음, 레이어를 통한 구현을 우선적으로 한 후 구성을 마무리하는 것으로 한다.

profile
기술로써 가치를 만들고 싶은 사람입니다.

0개의 댓글