AWS Lambda: Apex로 관리하기

이전 글: AWS Lambda: 시작하기

Lambda를 처음 쓰기 시작했다면 코드를 웹 콘솔에서 직접 수정하거나, ZIP 파일을 통해 배포하고 관리해야하는 것이 불편하게 느껴질 수 있을 것이다. AWS SDK가 제공되고 있으니 이런 작업들을 자동화하는 도구를 직접 구현할 수도 있겠지만, 이미 잘 구현되어 사용할 수 있는 도구들이 있다. ApexServerless Framework가 바로 그것이다.1

이번 글에서는 그 중 Apex에 대해서 다룬다. 사실 사용자 수는 Serverless Framework쪽이 더 많은 것으로 보이지만 필자는 아직 사용해 본 적이 없어서 비교 평가가 어렵다. 대신 이 질답을 참조하시길 바란다.

시작하기

먼저, AWS CLI가 있는지 확인해보고, 없다면 설치하자.

Apex는 Go로 구현되어있으며 curl을 통해 내려받고 설치할 수 있다.

1
$ curl https://raw.githubusercontent.com/apex/apex/master/install.sh | sh

사용하기 위해서는 당연히 AWS Credential이 필요하다. Credential은 여러가지 방법으로 전달할 수 있는데 여기서는 ~/.aws 경로를 이용할 것이다. AWS CLI를 이용해서 새로운 프로필을 만든다.

1
2
3
4
5
$ aws configure --profile [name]
AWS Access Key ID [None]: [your-access-key-id]
AWS Secret Access Key [None]: [your-secret-access-key]
Default region name [None]: [your-region-name]
Default output format [None]: json

참고로, Access Key ID와 Secret Access Key는 AWS IAM Console에서 얻을 수 있다. 만약 유저가 없다면 새로 등록하자. Apex에서 필요로 하는 IAM Policy는 여기를 참조하면 된다.

Credential 설정까지 마쳤으면 이제 Apex를 사용할 프로젝트의 디렉토리로 이동하자. 그 뒤에, AWS_PROFILE 환경 변수를 지정하고, apex init을 사용해서 프로젝트를 시작할 수 있다.

1
2
$ export AWS_PROFILE=[name]
$ apex init

프로젝트 명까지 입력하면 이제 Apex를 사용해 디플로이 및 테스트를 할 환경이 갖춰진 것이다. 시험삼아 apex deploy를 입력하면 함수가 배포되는 것을 확인할 수 있다.

대략적인 사용법

Apex로 생성한 프로젝트의 대략적인 골격은 아래와 같다.

1
2
3
4
5
6
7
├─ functions
│ ├─ function-a
│ │ └─ index.js
│ ├─ function-b
│ │ └─ index.js
│ └─ ...
└─ project.json

apex init으로 project.json 파일과 functions 디렉토리가 생겼다. project.json은 프로젝트의 여러가지 설정 및 변수가 담기는 파일이며, functions는 실제 배포될 소스코드 파일이 위치할 디렉토리다.

functions 디렉토리 아래에 위치하는 여러 디렉토리는 실제 배포되는 각각의 Lambda 함수다. 말하자면, 위의 디렉토리 구조에서 function-afunction-b는 각각 하나의 함수이고, 그 아래에 위치한 index.js가 반영될 소스파일이다.

Apex는 몇 가지 유용한 명령어를 제공하는데, 실제로 프로젝트를 운영하면서 자주 사용하게 될 명령어는 많지 않다. 다음은 자주 사용하는 명령어에 대한 대략적인 설명이다.

  • apex build [function name]: Lambda에서 사용할 수 있는 ZIP파일을 빌드한다.
  • apex deploy ?[function name]: 말 그대로 Lambda에 코드를 디플로이하는 명령이며, 내부적으로 apex build를 실행한 결과물을 디플로이한다.
  • apex invoke [function name]: Lambda function을 실행하는 명령어다. 이 명령어는 로컬이 아니라 실제 Lambda에서 실행되므로 주의.
  • apex logs [function name]: Lambda의 실행으로 찍힌 로그를 출력하는 명령어다. console.log 등으로 찍은 로그도 볼 수 있다.

사실 apex build 빼고 나머지 세 가지의 명령어를 주로 사용하게 된다. 아까 apex deploy를 했었다면 이미 hello라는 이름의 함수가 배포되어 있을테니 apex invoke 명령어를 사용해보자.

1
2
$ apex invoke hello
{"hello":"world"}

이번엔 apex logs를 사용해서 어떤 로그가 찍히는 지 확인해보자.

1
2
3
4
5
6
$ apex logs hello
/aws/lambda/apex-typescript-example_hello 2017-07-09T08:30:03.563Z undefined starting function
/aws/lambda/apex-typescript-example_hello START RequestId: cdfd7251-6480-11e7-a35e-81de7320e51b Version: 1
/aws/lambda/apex-typescript-example_hello 2017-07-09T08:30:03.565Z cdfd7251-6480-11e7-a35e-81de7320e51b processing event: {}
/aws/lambda/apex-typescript-example_hello END RequestId: cdfd7251-6480-11e7-a35e-81de7320e51b
/aws/lambda/apex-typescript-example_hello REPORT RequestId: cdfd7251-6480-11e7-a35e-81de7320e51b Duration: 2.02 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 18 MB

아마도 크게 위와 다르지 않은 결과를 얻었을 것이다. 실제로 위의 명령어들만 잘 사용해도 기본적인 운영은 할 수 있다.

프로젝트 구조 문제

실제 Node.js 프로젝트에서 외부 라이브러리 없이 단일 파일만으로 원하는 비즈니스 로직을 구성할 수 있는 가능성은 실질적으로 없다. 따라서 필연적으로 npm 패키지를 설치하고 참조하게 된다. 이 때, package.json파일과 node_modules 디렉토리는 대체 어디에 위치해야 하는가라는 고민이 생긴다. 뿐만 아니라, 여러 함수에서 공용으로 사용하는 소스코드가 있고, DRY 원칙을 깨고 싶지 않다면 이 소스코드는 어느 함수의 아래에 위치해야 하는가? 아까 잠깐 보았던 프로젝트의 골격을 다시 한 번 보자.

1
2
3
4
5
6
7
├─ functions
│ ├─ function-a
│ │ └─ index.js
│ ├─ function-b
│ │ └─ index.js
│ └─ ...
└─ project.json

이상적인 구조는 공용 코드 및 package.jsonnode_modules가 특정 함수 아래가 아니라 최상단 경로에 위치하는 것이다. 하지만 node_modules는 실제 배포될 소스코드에 포함되어야 하기 때문에 node_modules는 당연히 각각의 함수, 말하자면 function-a 아래에 위치할 수 밖에 없다고 잠깐 생각했다.

그런데 문득, 공용 소스코드와 라이브러리, 그리고 거기에 의존성을 가지는 여러 개의 엔트리 파일이라는 구조. 이거 webpack에서 이미 많이 해본 것 아닌가? 하는 생각이 들었다. webpack을 사용하기로 마음을 먹고, “이 신박한 생각을 아무도 하지 못했겠지! 혹시 난 천재..?” 라는 생각을 하면서 찾아 본 결과, Apex에서 예제까지 제공하는 것을 보고 자괴감에 빠졌다(…)

각설하고, Apex로 제대로 Lambda를 관리하기로 마음먹었다면 번들링은 선택이 아닌 필수라고 생각한다. 따라서 여기에서는 webpack을 이용해 프로젝트를 구성해 볼 것이다. babel을 이용한 예제는 위에 링크했으므로 여기에서는 TypeScript과 함께 번들링하는 예제를 다루겠다. (사실 거의 비슷하다.)

프로젝트 구조 잡기

번들링을 하기로 결정했으므로 이제 개발하는 원본 소스코드와 배포되는 소스코드가 달라지게 될 것이다. 따라서 프로젝트 구조에 약간의 변경을 주어야 한다.

1
2
3
4
5
6
7
├─ functions
│ ├─ hello
│ │ ├─ src
│ │ │ └─ index.ts
│ │ └─ lib
│ └─ ...
└─ project.json

각각의 함수 아래에 src라는 이름의 디렉토리를 추가했고. 원본 파일을 새로 만든 디렉토리 아래에 위치시킨다. 기왕하는 김에 TypeScript 파일으로 변경해준다. 이제 webpack을 통해 이 파일을 빌드하고 그 결과물은 lib 디렉토리 아래에 들어갈 것이다. 다만 여기서 lib 디렉토리를 미리 만들어 줄 필요는 없다. webpack이 자동으로 만들어 줄 것이다.

함수 디렉토리 아래에 index.js 파일이 사라지면 Apex는 이를 파일이 없는 함수로 인식해 빌드할 때 오류를 뱉어낸다. 현재 디렉토리가 변경되었으므로 오류가 발생할 것이다. 따라서 function.json 파일을 함수 바로 아래에 추가한다.

Apex는 몇 가지 Task에 대한 Hook을 제공한다. 지원하는 Task 중에는 빌드 전에 실행되는 Hook도 있는데, apex deploy를 실행하면 알아서 빌드가 진행되므로 디플로이 시 번들링을 자동화 할 수 있다. Hook은 project.json을 수정하면 되는데, 아래와 같이 설정한다.

"hooks"아래에 들어간 것들이 바로 Hook 설정이다. "build"는 ZIP 파일 생성 전에 실행되는 Hook이며, "clean"은 디플로이가 모두 끝난 시점에 실행되는 Hook이다. 보면 Hook이 실행되는 경로가 함수 디렉토리의 바로 아래라는 것을 짐작할 수 있을 것이다. "handler"에 대해서는 조금 아래에서 설명한다.

아직 webpack.config.js 파일을 만들지 않았다. 아래처럼 파일을 만들자. 참고로 여기서 사용될 npm 모듈들(webpack 및 loader들)은 별도로 설치하라고 언급하지 않을 것이다.

중요한 부분만 설명하겠다. 일반적으로 webpack을 사용하는 이유와는 달리 Node.js에서 돌아갈 JS파일을 빌드해야하므로, Target을 "node"로 잡아준다. 그리고 생소한 "commonjs2"가 눈에 띄는데, 이는 Node.js에서 사용하는 module.exports를 포함하겠다는 것이다. 또한, webpack이 실행되는 위치가 각각의 함수 디렉토리의 아래 경로이므로 이 파일의 위치가 아닌 프로세스가 실행된 위치를 리턴하는 process.cwd()를 사용한다.

AWS SDK를 사용하는 경우, npm의 aws-sdk 모듈을 설치하지 않아도 Lambda에서는 기본적으로 런타임에 포함되어있으므로 externals에 적어주면 된다.

Uglify는 하면 좋고, 굳이 하지 않아도 된다. 필자는 브라우저에서 실행되는 파일이 아니므로 하지 않는다.

이제 tsconfig.json 파일을 추가하고, tslint.json 파일도 적당한 룰을 붙여 추가하자.

완성된 프로젝트의 구조는 아래와 같아야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
├─ functions
│ ├─ hello
│ │ ├─ src
│ │ │ └─ index.ts
│ │ └─ function.json
│ └─ ...
├─ node_modules
├─ project.json
├─ webpack.config.js
├─ tsconfig.json
├─ tslint.json
└─ package.json

이제 아까 확장자만 바꾸었던 TypeScript 파일을 수정해 줄 차례다.

원래 생성했었던 index.js 파일과 비교해보자. 함수를 내보내는 방식이 default를 이용하는 것으로 바뀐 것을 확인할 수 있을 것이다. 이게 바로 위에 project.json 파일에서 "handler""lib.default"라고 쓴 것에 해당한다. lib 디렉토리의 index.js 파일에서 내보내는 default 함수를 바라보게 한 것이다.

마침내 빌드가 잘 되는지 테스트를 해볼 시간이다.

1
$ apex build hello > hello.zip

hello.zip 파일을 압축 풀고, lib 디렉토리의 index.js 파일을 확인해보면 빌드가 된 결과물을 확인할 수 있을 것이다. 잘 빌드가 되었다면 디플로이도 해보고 apex invoke 명령어를 이용해 테스트도 해보자.

1
2
$ apex deploy hello
$ apex invoke hello

여기까지 잘 끝냈다면 npm 모듈을 사용하기는 정말 쉽다. 시험삼아 저번 글에서도 사용했던 lodash를 설치하고 사용해보자. lodash를 설치한 뒤 index.ts 파일을 아래와 같이 수정한다.

그리고 바로 다시 디플로이와 실행을 해보자. rand라는 프로퍼티에 랜덤하게 정수가 출력되면 성공이다.

마치며

이 일련의 과정은 사실 내가 회사에서 업무를 진행하면서 했던 작업들의 일부다. 진행하면서, webpack 같은 번들러없이 Apex로 Lambda를 관리하기는 좀 어렵겠다는 인상을 강하게 받았다. 하지만 한 번만 고생(?)해서 일단 구조를 잡아주고 나면, 이후 개발은 편하게 진행할 수 있다. 이 글에서는 다루지 않았지만, 개발하다보면 으레 필요해지는 멀티 스테이지도 필수라고 할 수 있는데, 이것을 포함해서 apex-typescript-boilerplate라는 이름의 저장소에 올라가 있으니 참고하시기 바란다.


  1. 1.Node.js 기준이며, Python은 또 다른 도구들이 있다.