현재 진행중인 프로젝트에서 실서비스를 운영중 서비스 이용에 큰 문제를 주는 에러가 발생했다. 문제는 이틀동안 해당문제가 발생하는지 모르고 있다가 한 고객의 문의로 알게 되었다.(정말 감사합니다😂ㅠㅠ) AWS CloudWatch에서 로그를 확인하니 이틀간 상당수의 유저들이 해당 에러 때문에 불편을 겪은 것을 확인하였다ㅜ
적은 인원으로 기능 개발에 집중하다보니 운영에 신경을 못썼고 그제서야 심각성을 인지하고 빠른 오류 인지를 위해 Error log를 Slack으로 알림받기로 결정하였다.
AWS lambda란 FaaS(Function as a Service)로 서비리스 컴퓨팅 서비스이다.
오류가 뜨면 서버에서 에러로그를 슬랙으로 보내주면 되지 않는가?라고 생각할 수 있다.
그렇게 되면 API를 추가할 때마다 에러처리로직에 Slack으로 알림을 주는 로직을 추가해줘야하는데 이런 귀찮고 실수가 잦은 부분을 따로 분리하여 CloudWatch Log에서 어떤 에러든 확인되면 lambda 함수를 실행해 로그를 Slack에 뿌려주도록 설계하였다
💡 서버리스란
서버를 따로 관리하지 않고 클라우드에게 서버의 관리를 맡기고 오직 로직(코드)에만 집중할 수 있게해주는 서비스이다. 서버리스의 한 갈래인 FaaS의 경우 개발자가 코드를 함수단위로 작성하고 배포하면 AWS에서 개발자가 직접 관리하지 않아도 되는 서버에서 실행된다.
클라우드는 매우 강력한 기능들을 제공하지만 동시에 사용하기 매우 어렵고 복잡하다. 각 리소스마다 정책설정하고 리소스끼리 연결시키는 등 AWS console를 이용하면
직관성과 가독성이 다소 떨어지는 경향이 있다.
복잡한 인프라 설계를 보다 편리하게 관리하고 직관적으로 이해할 수 있도록 많은 IaC(Infra as Code)도구들이 생겨났고 Serverless로 아키텍쳐를 설계하는 부분에서는 Serverless Framework
가 잘 만들어져 있으며 이 도구를 이용하기로 결정했다.
내가 작성할 시스템의 젠체적인 흐름은 아래와 같고 흐름에 대한 설계를 Serverless Framework를 이용해서 작성할 예정이다.
1. CloudWatch에서 로그이벤트 발생
2. Log가 Error일 경우 lambda 함수를 실행
3. lambda 함수에서 slack으로 로그 내용을 전송하는 구조이다.
💡 Iac(Infra as Code)
인프라 설계를 YML과 같은 설정파일을 이용해서 코드로써 관리하는 도구들이다.
각 자원별로 정책설정, 자원끼리 연동 등 AWS console에서 하나하나 마우스를 클릭하면서 했던 일들, cli를 이용해서 명령어를 쳐야했던 부분들을 하나의 파일로 관리하고 실행시킬 수 있다.
먼저 Serverless Framework 설치한다. 필자는 npm을 사용하여 설치하였다.
npm install -g serverless
프로그래밍으로 인프라 설계를 하기 위해서는 AWS에서 해당 액세스를 허락 받은 사용자만 가능하다. 그러므로 AWS IAM에서 사용자를 추가해준다. (참고 - Serverless Framework는 배포시 AWS Cloudformation 코드로 변환되어 배포된다.)
(IAM - 사용자 - 사용자 추가)
(꼭 AWS 액세스 유형은 프로그래밍 방식 액세스를 선택하여야 한다!)
(권한설정은 AWS 리소스 제어권한을 의미하는데 일단 모든 권한을 허락하였다)
(마지막에 나오는 액세스 키 ID
와 비밀 액세스 키
는 따로 저장해두자)
권한설정을 끝마쳤으니 해당 IAM 유저로 로그인한다.(serverless framework를 설치하였다면 serverless
명령어를 사용할 수 있으며 sls
로도 사용가능하다
serverless config credentials \
--provider aws \
--key {액세스 키 ID} \
--secret {비밀 액세스 키} \
AWS를 코드로 제어할 수 있는 환경설정을 마쳤다. 이번엔 람다코드를 작성 하기위한 개발환경을 구축해보자. 람다함수는 파이썬이나 Go, Node.js 등으로 작성할 수 있다. 필자는 Node.js + typescript 환경을 이용하였다. 환경을 만들기 위해 0부터 모든 코드를 짤 수 있지만 템플릿을 이용해 기본 환경을 구축해보자.
아래 명령어를 입력시 serverless framework에서 제공해주는 템플릿들을 볼 수 있다.
sls create --help
aws-nodejs 템플릿을 이용해 프로젝트를 만든다. aws-nodejs-typescript 템플릿을 이용해 처음부터 typescript가 적용된 프로젝트를 만들 수 있지만 typescript는 직접 설정해 보기로 한다.
sls create -t aws-nodejs -p {프로젝트 이름}
루트 디렉터리에 serverless.yml
파일이 있는데 해당 파일이 바로 설계 코드를 작성하는 파일이다! 이어서 typescript를 적용해보자
참고자료
npm install -D serverless-plugin-typescript typescript
위 명령어로 설치 후 serverless.yml
파일에 아래 명령어를 입력해준다.
plugins:
- serverless-plugin-typescript
추가적으로 애초에 nodejs 템플릿으로 만들었기 때문에 몇몇 필수 라이브러리들의 타입스크립트 버전이 설치가 안되어있을 것이다. 고로 원할한 타입스크립트 사용을 위하여 몇몇 가지를 설치해준다.
npm install -D @types/aws-lambda @types/node
마지막으로 루트에 tsconfig.json파일을 만들고 typescript 환경설정을 해준다.
tsconfig.json 옵션들에 대한 참고자료 바로가기
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2016",
"outDir": ".build",
"noImplicitAny": true,
"strictPropertyInitialization": true,
"moduleResolution": "node",
"sourceMap": true,
"rootDir": "./",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strictNullChecks": true,
"resolveJsonModule": true,
"strict": true,
"allowJs": true,
"allowSyntheticDefaultImports": true,
"lib": [
"es2015"
],
"typeRoots": [
"@types",
"node_modules/@types"
]
}
}
이로써 기본적인 환경설정 구축은 끝났지만 개발을 하면서 필요한 몇몇 라이브러리가 있다. 그때그때 설치하면서 사용하기로 하자
serverless.yml
에서 CloudWatch의 LogEvent 중 Error가 발생하면 지정한 람다함수를 실행하도록 설계해보자.
# 이름
service: adevspoon-pipeline
# serverless framwork 버전
frameworkVersion: '3'
# serverless framework 플러그인들 정의
plugins:
- serverless-plugin-typescript
# 인프라와 관련된 정의 및 환경설정들
provider:
name: aws
runtime: nodejs14.x
region: ap-northeast-2
timeout: 30
memorySize: 128
stage: ${opt:stage, 'dev'}
# 람다함수들의 정와와 실행될 이벤트 정리
functions:
myCloudWatchLog:
handler: {파일이름}.{export한 함수 명}
events:
- cloudwatchLog:
logGroup: '{logGroup 이름}'
filter: '?ERROR ?Exception'
- cloudwatchLog:
logGroup: '{logGroup 이름}'
filter: '?ERROR ?Exception'
하나씩 살펴보자.
가장 상단의 두 섹션은 프로젝트 디렉터리의 이름과 framwork 버전을 뜻한다.
# 이름
service: adevspoon-pipeline
# serverless framwork 버전
frameworkVersion: '3'
이 부분은 serverelss framework의 플러그인들을 추가하는 섹션이다.
이미 설치한 typscript 말고도 앞으로 2가지의 플러그인들을 설치하고 이 섹션에 추가할 예정이다.
# serverless framework 플러그인들 정의
plugins:
- serverless-plugin-typescript
provider 부분은 어떤 클라우드 제공업체를 사용할 것인가 등 사용하는 인프라 환경에 대해 정의해 놓을 수 있다. 코드를 실행할 환경, aws region 등을 설정해놓았다.
맨 아래 stage
는 배포할 환경을 뜻한다.
${opt:stage, 'dev'}
를 설명하자면 구축한 환경을 배포할때 sls deploy
라는 명령어를 통해 aws에 배포하게 되는데
이때 아무런 옵션을 주지 않으면 기본적으로 dev로 배포한다는 뜻이고 sls deploy --stage prod
와 같이 --stage 옵션을 추가하면 배포환경을 명시적으로 지정할 수 있다. stage를 이용해서 dev나 prod 등 환경을 나누어 개발할 수 있다.
# 인프라와 관련된 정의 및 환경설정들
provider:
name: aws
runtime: nodejs14.x
region: ap-northeast-2
timeout: 30
memorySize: 128
stage: ${opt:stage, 'dev'}
이 섹션이 메인이다. functions
란 람다함수를 정의해 놓는 섹션이다.
myCloudWatchLog
- 람다함수의 이름
handler
- 람다 실행시 동작할 함수코드
events
- 람다를 실행시킬 이벤트
작성된 코드의 events 파트를 보면 cloudwatchLog에서 지정한 LogGroup에서 발생하는 이벤트 중 Error거나 Exception을 발생했을때라고 정의되어있다.
정의된 event가 부합되었을때 handler에 작성된 함수가 실행된다
events에는 -
를 이용해서 다수의 이벤트를 구독하도록 작성이 가능하다.
# 람다함수들의 정와와 실행될 이벤트 정리
functions:
myCloudWatchLog:
handler: {파일이름}.{export한 함수 명}
events:
- cloudwatchLog:
logGroup: '{logGroup 이름}'
filter: '?ERROR ?Exception'
- cloudwatchLog:
logGroup: '{logGroup 이름}'
filter: '?ERROR ?Exception'
람다함수를 작성하기 이전에 Slack으로 알림을 보내기 위해선 Slack에 앱을 추가하고 해당 앱에 권한 설정이 필요하다.
슬랙 api에 들어가서 'Create New App' 클릭해주자(클릭 후 from scratch 선택, 앱 네임, workspace를 지정해준다)
아래와 같은 페이지가 나올텐데 Bots를 선택해준다
좌측 메뉴의 OAuth & Permissions 선택 - Scopes의 Bot Token Scopes에서 write에 관한 권한을 추가해주자(잘 알고 계시다면 필요한 것만 골라서!)
좌측 메뉴의 base information을 선택 - Install your app을 클릭하여 workspace에 앱을 추가해주자
다시 OAuth & Permissions로 돌아가면 Token이 생성될 것인데 복사 후 잘 간직하자 Slack API를 사용하기 위해서 꼭 필요한 토큰이다
마지막으로 슬랙의 workspace에서 원하는 채널에 만든 앱을 초대해준다
함수 내부에서는 받은 이벤트를 slack으로 noti를 해주는 코드를 작성할 것이다.
우선 기본적으로 작성되는 handler.ts(handler.js 파일 확장자 변경)을 열고 아래와 같이 작성했다.
export { default as sendErrorLogToSlack } from './src/sendErrorLogToSlack';
다음으로 src라는 디렉터리를 만들고 내부에 sendErrorLogToSlack.ts
를 만들어 준다.
slack에서 제공해주는 REST API를 사용해도 문제없지만 라이브러리를 사용해서 간편하게 사용하기로 했다.
npm install @slack/web-api
마지막으로 위에서 발급한 Slack Token, 메시지를 보낼 Slack내 채널ID를 숨겨주기 위해서 .env
파일을 만들어 주고 서버리스 내에서 .env
파일을 사용하기 위해서 플러그인으로 serverless-dotenv-plugin
를 설치해 준다.
npm install -D serverless-dotenv-plugin
# serverless.yml 파일에도 작성하기
plugins:
- serverless-plugin-typescript
- serverless-dotenv-plugin
아래는 sendErrorLogToSlack.ts
파일의 코드이다. CloudWatch의 로그를 json으로 디코딩 -> 메세지 셋팅 -> 슬랙 메시지 전송순으로 작성하였다.
import { ChatPostMessageResponse, ErrorCode, WebClient } from '@slack/web-api';
import {
CloudWatchLogsDecodedData,
CloudWatchLogsEvent,
CloudWatchLogsEventData,
CloudWatchLogsHandler,
} from 'aws-lambda'
import * as zlib from 'zlib'
const slackClient = new WebClient(process.env.SLACK_BOT_TOKEN);
const sendErrorLogToSlack = async (event: CloudWatchLogsEvent) => {
// CloudWatch LogEvent Decoding
const compressedData: CloudWatchLogsEventData = event.awslogs;
if (!compressedData) return
const payload = Buffer.from(compressedData.data, 'base64');
const decodedData = JSON.parse(zlib.unzipSync(payload).toString()) as CloudWatchLogsDecodedData;
const errorMessage = decodedData.logEvents.map((e) => e.message).join(" & ")
// Slack Message Setting
const slackMessage = `
❓❓Error
▶️ Log Date: ${(new Date()).toISOString().replace("T", " ").replace(/\..*/, '')}
▶️ Log Group: ${decodedData.logGroup}
▶️ Log Message: ${errorMessage}
`;
// Notify to Slack
try {
const messageResponse: ChatPostMessageResponse = await slackClient.chat.postMessage({
text: slackMessage,
channel: `${process.env.SLACK_ERROR_LOG_CHANNEL_ID}`
});
} catch (error) {
console.log(`Notify to Slack Error ${error}`);
}
};
export default sendErrorLogToSlack;
아래 명령어만 입력하면 배포완료!
sls deploy
개발환경 별로 다르게 배포를 하고 싶다면 --stage
옵션을 사용하면 된다.
sls deploy --stage prod
여러 레퍼런스를 찾던 중 내가 작성한 방법에서 Cloudwatch와 lambda 사이에 AWS SNS(알림을 주는 서비스)를 사용하는 예시들이 많았다. 굳이 SNS를 왜 사용할까 곰곰히 생각해보았는데 SNS가 하나의 멀티포트의 역할을 해주는 것이 아닐까 싶다. 내가 구현한 방식은 1:1 방식으로는 문제없으나 혹여나 1:N 방식으로 알림을 주어야할때 번거로운 코드들이 추가되어야 할 것 같다.
(또 다른 이유를 아시는 분들은 댓글로 알려주시면 감사하겠습니다!)
여전히 해결하지 못한 문제이다. 여러 개의 Log group을 모두 한 handler에 등록시키고 싶은데 yml파일의 logGroup 설정에서 와일드카드(*)의 사용이 금지되어있다. 일단은 할 수 없이 모든 logGroup을 하드코딩해 놨지만 마음에 들지는 않는다ㅋㅋ..
(방법을 아시는 분들은 댓글로 알려주시면 감사하겠습니다!)
json으로 Decoding하는 과정에서 Node.js의 내장 함수인 Buffer를 사용하였다. 정확히 알고 사용한 부분이 아니므로 공부를 할 필요성이 있다.