2023년 1월 1일, 올해의 첫날은 코딩으로 알차게(?) 시작을 했습니다. 1월 말까지 기존에 제작한 대나무숲 슬랙 앱을 계속 운영하기 위해서 AWS Lambda로 마이그레이션 하는 작업이 필요했기 때문입니다.
저는 AWS 인프라에 익숙한 편은 아니었는데, 그러다 보니 개발 과정에서도 삽질을 많이 했습니다. 그래도 AWS를 완전 처음 써본 건 아니어서 다행히 당일에 배포까지 성공적으로 할 순 있었습니다. 마이그레이션 과정 자체는 어렵지 않았는데, AWS 권한 설정 등에서 시간을 많이 빼앗겼더라구요.
그래서 오늘은 기존에 Heroku 인스턴스로 호스팅하던 슬랙 앱을 AWS Lambda로 마이그레이션한 후기를 공유하고자 합니다. 이번 글을 통해 슬랙 앱 배포에 AWS Lambda를 활용하고자 하는 분들에게 도움이 되었으면 좋겠습니다.
서버리스로 마이그레이션을 하게 된 이유
작년 8월, Heroku 공식 블로그에서 무료 호스팅의 단계적 중단을 발표했다
저는 작년 5월에 글또 슬랙에서 사용할 대나무숲 앱을 제작하고 그 후기를 공유한 적이 있습니다. 당시 제작한 앱은 Heroku에서 제공하는 클라우드 컴퓨팅 서비스인 Dyno을 이용해 호스팅하고 있었습니다. 계정 별로 한 달에 최대 1000시간까지 무료로 쓸 수 있는데, 한 달이 720시간이니까 인스턴스 하나 정도는 과금 없이 계속 쓸 수 있었죠.
다만 무료 플랜이다 보니 Heroku에서 리소스 최적화를 위해 Dyno에 여러 정책들을 적용해 둔 게 있습니다. 하루에 18시간만 쓸 수 있게 한다던가, 30분 동안 아무 요청을 받지 않으면 자동으로 잠들어버린다던가… 그래서 Dyno에 요청을 보내게 되면 메시지 전송이 종종 실패하는 불편함이 있었습니다. 뭐 무료로 쓰는 거니까 할 말은 없지만 눈에 거슬리는 것은 어쩔 수 없더군요.
그런데 그 와중에 8월에 뜬금없이 Heroku에서 무료 호스팅 서비스를 단계적으로 종료한다는 발표를 하더라구요. 무료 플랜은 최종적으로 11월까지만 제공되고 그 이후에는 유료화된다는 소식이었습니다.
다행히 해당 기수가 끝날 때까지는 무료 호스팅으로 서비스를 운영할 순 있었지만, 그 이후에도 슬랙 봇을 활용하려면 Heroku의 적당한 대체제를 찾아야 했습니다. 사실 과금을 하고 쓰려면 쓸 순 있었지만, 다른 서비스로 갈아타면 무료로 쓸 수 있는데 굳이…?
24시간 중에 실제 서버가 동작하는 시간은 1~2분 남짓, 해당 시간만 컴퓨팅 리소스를 쓸 수 없을까?
그렇게 대체제를 찾아보다가 문득 ‘이게 항상 인스턴스를 띄워놓을 필요가 있나?’ 라는 생각이 들었습니다. 약 6개월 간의 운영 기간을 되돌아볼 때, 하루에 평균적으로 4~5회 정도의 요청만 처리하면 됐거든요. 많이 써봤자 20회 정도?
지금 상황을 살펴보니 서버를 굳이 항상 띄우고 있을 필요도 없고, 요청 횟수가 그렇게 많은 편도 아니며, 요청이 들어올 때만 컴퓨팅 리소스를 잠깐 사용하면 되더라구요. 옳다구나 하고 생각난 것이 바로 서버리스(Serverless)였습니다. 인프라를 관리할 필요 없이, 내가 필요한 만큼만 컴퓨팅 리소스를 사용할 수 있으니까요!
사실 다양한 업체에서 서버리스 서비스를 제공하고 있습니다. 대표적으로 마이크로소프트의 Azure Function, 구글의 Google Cloud Function, AWS의 Lambda 등이 있죠.
슬랙 공식 홈페이지에서 추천하는 호스팅 서비스 목록도 있더라
저는 대나무숲 슬랙 앱 제작에 Bolt.js를 활용했는데, Bolt.js에서 빌트인으로 AWS Lambda 호환을 위한 도구를 제공할 뿐만 아니라 예제 코드와 튜토리얼도 있길래 큰 고민 없이 AWS Lambda를 선택했습니다.
개발 과정
지금까지는 AWS Lambda를 선택한 이유에 대해 알아보았고, 지금부터는 Bolt.js에 TypeScript를 적용한 봇을 서버리스로 배포한 과정에 대해 한 단계씩 알아봅니다.
IAM 계정 설정
IAM 계정 생성 단계는 유튜브 영상이 있으니 참고하시면 좋을 것 같아요.
일단 루트 사용자로 로그인하고 IAM 사용자를 새로 만들자
우선 AWS 계정이 필요한데요, AWS에는 루트 사용자 와 IAM 사용자 라는 두 가지 타입의 계정이 있습니다. 루트 사용자는 AWS에 접속할 수 있는 최고 권한을 가진 계정이고, IAM 사용자는 특정한 목적을 달성하기 위해 루트 사용자에서 권한을 제한하여 생성한 계정입니다. 루트 사용자는 모든 권한을 갖고 있기 때문에 보안상의 이유로 사용하지 않고, IAM 계정을 새로 만들어 사용할 예정입니다.
우선 루트 사용자로 로그인을 했다고 가정하고, IAM 사용자를 새로 만들어보겠습니다. 저는 브라우저로 AWS 콘솔에 접속해 만들었습니다.
저는 이미 만들어둔 IAM 사용자가 하나 있긴 한데, 튜토리얼이니 새로 한 번 만들어보겠습니다. 우측 상단의 사용자 추가 버튼을 클릭합니다.
이름은 bamboo-forest-tutorial
로 하겠습니다. 어차피 프로그래밍 방식으로만 사용할거라 콘솔 엑세스 활성화 는 체크하지 않아도 괜찮습니다.
다음 단계로 넘어가면 해당 계정을 통해 구체적으로 어떤 작업을 할 수 있는지에 대한 권한을 설정할 수 있습니다. 필요한 권한만 선택해서 추가하는 게 좋은데, 이게 워낙 종류가 많아서 어떤 걸 선택해야 할지 모르겠더라구요.
나중에 Lambda 배포할 때 Serverless라는 프레임워크를 활용할 텐데, 이 프레임워크에서 요구하는 권한도 추가해야 합니다.
Serverless에서 필수적으로 요구하는 권한에 대한 자료와 직접 삽질하면서 알게 된(…) 바에 의하면 이 정도로 정책을 주니까 되었습니다. 정책이라는 것은 여러 권한들을 편의상 묶어놓은 건데요, 요게 어떤 역할을 하는지에 대해서는 후술 하겠습니다.
- AmazonAPIGatewayAdministrator
- AmazonS3FullAccess
- AWSCloudFormationFullAccess
- AWSLambda_FullAccess
- CloudWatchLogsFullAccess
- IAMFullAccess
이렇게 IAM 사용자 추가를 완료하면 위와 같은 화면이 나옵니다. 이제 이 계정을 사용해서 AWS CLI에 계정 정보를 등록하고 Lambda를 배포할 예정인데요, 이를 프로그래밍 방식으로 제어하려면 사용할 계정의 ID와 패스워드 역할을 하는 Access Key ID
와 Secret Access Key
가 필요합니다. 이를 발급하러 한 번 가봅시다.
해당 사용자의 보안 자격 증명 탭을 살펴보면, 액세스 키 를 새로 발급할 수 있는 섹션이 있습니다. 여기서 새 엑세스 키를 만듭니다.
나중에 AWS CLI를 활용할 예정이므로 첫 번째 선택지를 클릭합니다.
그러면 이렇게 Access Key ID
와 Secret Access Key
를 확인할 수 있는데요, .csv 파일 다운로드 버튼을 클릭해 다운로드합니다. 그냥 복붙만 해두면 나중에 까먹을지도 몰라요.
AWS CLI 설치
다음으로 AWS 서비스를 프로그래밍 방식으로 제어할 수 있도록 도와주는 도구인 AWS CLI를 로컬에 설치합니다. 다운로드 가이드 페이지에 접속하여 직접 패키지 파일을 다운로드받거나, Mac 유저라면 Homebrew를 활용해서도 설치할 수 있습니다.
brew install awscli
AWS CLI 설치가 완료되면 터미널을 열고 IAM 사용자 정보를 이용해 자격 증명을 합니다. 자격 증명은 쉽게 말해 로그인이라고 생각해 두셔도 좋습니다.
aws configure
아까 다운로드받은 csv 파일에서 Access Key ID
와 Secret Access Key
를 차례대로 입력합니다. 저는 리전은 us-east-1
, 출력 형식은 json
으로 설정했습니다. 저는 기존에 저장해 둔 정보가 있어서 마스킹된 정보가 옆에 뜨네요.
만약 제대로 저장되었는지 확인하고 싶다면 아래 명령어를 입력해 자격 증명된 계정 정보를 확인 가능합니다.
aws sts get-caller-identity
아까 생성한 IAM 사용자 이름 bamboo-forest-tutorial
이 잘 나오네요.
Serverless 프레임워크 설치
다만 AWS CLI만으로는 환경 설정, 테스트, 디버깅, 배포와 같은 설정을 다루는 것이 매우 까다롭고 복잡한데요, 이 과정을 좀 더 쉽게 도와주는 Serverless 프레임워크를 활용하고자 합니다.
우선 Serverless 프레임워크를 설치합니다. 공식 문서에서는 NPM을 활용하는 방법이 적혀 있는데, Homebrew를 활용해도 됩니다. Homebrew, 그저 빛…
npm install -g serverless
# 또는
brew install serverless
설치가 완료되었다면 아래 명령어가 제대로 동작해야 합니다.
serverless help
Bolt.js 애플리케이션 수정
만약 처음 슬랙 앱을 처음 만들거나 Bolt.js가 익숙하지 않다면 공식 문서에 있는 예제 코드를 참고하면 됩니다. JavaScript 버전, TypeScript 버전도 있고 AWS Lambda가 이미 적용된 버전도 있으니 참고하여 선택하면 됩니다.
기존에 만들어둔 대나무숲 슬랙 앱은 튜토리얼 코드를 참고해 아래와 같이 수정했습니다.
- import { App } from "@slack/bolt";
+ import { App, AwsLambdaReceiver } from "@slack/bolt";
+ import { AwsCallback, AwsEvent, AwsResponse } from "@slack/bolt/dist/receivers/AwsLambdaReceiver";
+ const awsLambdaReceiver = new AwsLambdaReceiver({
+ signingSecret: SLACK_SIGNING_SECRET,
+ });
const app = new App({
token: SLACK_BOT_TOKEN,
- signingSecret: SLACK_SIGNING_SECRET,
+ receiver: awsLambdaReceiver,
});
- (async () => {
- await app.start(process.env.PORT || 3000);
- })();
+ const handler = async (
+ event: AwsEvent,
+ context: any,
+ callback: AwsCallback
+ ): Promise<AwsResponse> => {
+ const handler = await awsLambdaReceiver.start();
+ return handler(event, context, callback);
+ };
+ module.exports.handler = handler;
크게 bolt.js에서 제공하는 AwsLambdaReceiver
라는 Lambda 배포용 유틸리티 클래스를 적용한 것이 눈에 띕니다. 또한 기존의 서버를 띄우는 과정이 사라지고 함수 핸들러를 외부에 전달하기 위해 Node.js의 모듈 문법 modules.exports
를 쓰고 있는 모습도 볼 수 있네요.
사실 Lambda로 마이그레이션하기 위해 코드를 수정해야 하는 부분은 이게 다입니다. 생각보다 간단하죠?
serverless.yml
작성
다음으로는 Serverless 프레임워크에서 사용할 설정 파일을 작성해야 합니다. 여기에는 환경 설정, 테스트, 디버깅, 배포 옵션들을 명시합니다.
먼저 프로젝트의 루트 디렉토리에 serverless.yml
파일을 생성합니다. 설정 가능한 옵션들은 공식 문서에서 더 자세하게 찾을 수 있는데요, 저는 여러 삽질을 거쳐 아래와 같이 작성했습니다.
# 원하는 서비스 이름 작성
service: serverless-bolt-js
frameworkVersion: '3'
provider:
name: aws
# 원하는 배포 환경 이름을 CLI로 받기 위한 옵션, 디폴트는 dev
stage: ${opt:stage, 'dev'}
runtime: nodejs14.x
deploymentMethod: direct
functions:
slack:
# Lambda 함수의 진입점
# 현재 프로젝트는 TypeScript로 작성되어 있으므로 dist 폴더에 컴파일된 파일이 있음
# 만약 JavaScript로 작성되어 있다면 해당 프로젝트 내 Lambda 함수 진입점을 설정
handler: dist/app.handler
events:
- http:
path: slack/events
method: post
plugins:
# 로컬에서 Lambda 함수를 테스트하기 위한 플러그인
- serverless-offline
# 환경 변수를 .env 파일로 설정하기 위한 플러그인
- serverless-dotenv-plugin
# .env 파일을 사용할지 여부
useDotenv: true
# 각 플러그인에서 추가로 설정할 옵션들
custom:
serverless-offline:
# 디폴트로 엔드포인트에 환경 이름이 붙는데 이를 비활성화
noPrependStageInUrl: true
dotenv:
# .env 파일에 포함시키지 않을 환경 변수
exclude:
- GITHUB_TOKEN
그리고 현재 두 개의 플러그인을 사용하고 있는 것을 알 수 있는데요, 개발 디펜던시로 아래처럼 설치해주면 됩니다.
npm install --save-dev serverless-offline serverless-dotenv-plugin
# 또는
yarn add -D serverless-offline serverless-dotenv-plugin
제가 쓰고 있는 환경 변수 관리 파일은 개발용 .env
, 실제 배포용 env.production
으로 나누어 관리 중입니다. GITHUB_TOKEN
같은 경우는 release-it을 활용한 릴리스 자동화를 위해 추가해 둔 거라, 실제 배포 시에는 제외해 두었습니다.
# .env
SLACK_BOT_TOKEN=...
SLACK_SIGNING_SECRET=...
SLACK_BAMBOO_CHANNEL=...
GITHUB_TOKEN=...
provider.stage
값에 따라 가져오는 환경 변수가 다르니, 환경 분리가 필요하다면 여기 적힌 내용을 참고해 설정하시면 되겠습니다.
로컬에서 테스트하기
위와 같이 Serverless 설정도 완료했다면, 배포 전 로컬에서 제대로 동작하는지 확인을 해봐야겠죠? 터미널을 열고 아래 명령어를 입력해 Serverless를 로컬에서 실행해 봅시다.
serverless offline
# 또는
sls offline
그러면 위 사진처럼 실행 가능한 엔드포인트가 뜨게 됩니다. 다만 이 엔드포인트는 현재 로컬에서만 접근 가능한 상태이므로, 외부에서 접근할 수 있게 ngrok을 사용합니다.
Serverless의 http 기본 포트 번호는 3000이니, 아래 명령어를 실행합니다.
ngrok http 3000
여기서 나오는 URL을 복사한 후, 슬랙 앱 설정 페이지에서 Event Subscriptions 의 URL로 설정해 줍니다.
만약 설정이 제대로 됐다면 Verified 라는 체크 표시가 뜹니다. 그 후 우측 하단의 Save Changes 버튼을 클릭해 저장합니다.
Interactivity & Shortcuts 에서도 동일하게 설정해 줍니다. 솔직히 이 URL 입력창 왜 두 개로 나눴는지 아직도 잘 모르겠네요.
슬랙에서 발생시킨 이벤트가 ngrok을 통해 로컬 Serverless에서도 잘 수신된다
여기까지 잘 따라왔다면 로컬에서도 Lambda 메서드가 잘 작동하는 것을 확인할 수 있습니다. 오른쪽은 ngrok의 웹 인터페이스(localhost:4040
)인데요, 슬랙에서 발생시킨 이벤트가 로그에 잘 남는 것을 볼 수 있죠.
AWS Lambda에 배포하기
로컬에서 앱이 잘 작동하는지 확인했으니, 이제 AWS에 실제 배포를 해봅시다. 우선 저는 package.json
파일에 배포 스크립트를 아래와 같이 추가했습니다.
{
"scripts": {
"deploy:dev": "NODE_ENV=development sls deploy --verbose",
"deploy:production": "NODE_ENV=production sls deploy --stage production --verbose"
}
}
아까 serverless.yml
설정할 때 이야기했던 것처럼 분리된 환경으로 배포하고자 두 개의 스크립트로 추가했습니다. 참고로 --verbose
옵션은 배포 진행 로그를 자세하게 보여주니, 추가하시는 것을 추천드립니다. 배포는 일단 개발용으로 해봅시다.
npm run deploy:dev
명령어를 실행하면 배포가 어떤 순서로 진행되는지 확인할 수 있습니다. 그리고 왜 그렇게 IAM 사용자에 이상한(?) 권한들을 많이 줬는지도 알 수 있습니다.
Packaging
Excluding development dependencies for service package
Retrieving CloudFormation stack
Creating CloudFormation stack
Uploading
Uploading CloudFormation file to S3
Uploading State file to S3
Uploading service serverless-bolt-js.zip file to S3 (51.08 MB)
Updating CloudFormation stack
Retrieving CloudFormation stack
Removing old service artifacts from S3
엄밀하게 구분되는 건 아니지만 대충 이런 느낌입니다.
- AmazonAPIGatewayAdministrator: Lambda 함수를 외부에서 실행시키기 위해 URL을 생성하고 부착
- AmazonS3FullAccess: S3에 Lambda 실행에 필요한 코드 및 의존성들을 압축하여 업로드
- AWSLambda_FullAccess: Lambda 메서드 실행
- IAMFullAccess: 해당 IAM 사용자로 Lambda 메서드 실행
- CloudWatchLogsFullAccess: Lambda 실행 결과를 로깅
- AWSCloudFormationFullAccess: 위 전체 과정들에 필요한 인프라들을 템플릿으로 명시하고 관리
배포가 끝나면 최종적으로 콘솔에 엔드포인트가 하나 나오게 됩니다.
이것을 복사해 아까 설정한 ngrok URL 대신 붙여넣기 하면 끝!
Serverless에서 CloudWatch를 이용한 로그 설정 과정도 포함된 만큼, 사용 지표 역시 쉽게 확인할 수 있습니다.
마무리
이렇게 해서 어렵지 않게(?) 슬랙 앱을 Lambda에 배포할 수 있었는데요, 덕분에 300명 규모의 커뮤니티에서도 안정적이고 빠른 반응 속도로 슬랙 앱을 운영할 수 있게 되었습니다.
AWS 생태계와 Serverless 프레임워크가 익숙하지 않다 보니 헤맸던 부분도 있었지만 일단 잘 작동하는 것을 보니 또 뿌듯하더라구요. 서버리스 컴퓨팅의 장점을 그대로 누릴 수 있다는 점이 특히 좋았던 것 같습니다.
만약 여러분도 슬랙 앱을 배포해야 한다면, Lambda와 같은 서버리스 컴퓨팅을 이용해 보세요!