이전 작성한 게시글에서는 Serverless Framework의 배포 과정 중 발생한 오류 상황과 그 해결 방법에 대해 작성한적이 있었다. 간단히 요약하자면,
Serverless Framework의 배포 과정이 강제로 종료되면 AWS CloudFormation의 Stack이 비정상적으로 생성될 수 있고, 이 상태에서 다시 배포를 시도하면 “Stack … does not exist” 오류 메시지가 발생하게 된다.
이전 글을 작성한 후 겪었던 오류 상황과 해결 방법, 그리고 동일한 문제가 발생하지 않도록 수정하는 방법을 정리하여 Serverless Github Issue에 제출하였는데, Repo 관리자와 수정 방안에 대해 의견을 나눈 뒤, Pull Request에 대한 요구를 받게 되었다.
그렇게 오픈 소스에 기여하는 첫 번째 기회를 얻게 되었다.
🥕목표
AWS CloudFormation의 Stack을 생성하기 전 동일한 Stack이 있는지 확인하고, 해당하는 Stack의 상태가 REVIEW_IN_PROGRESS일 경우 에러 메시지를 출력하라.
문제 해결 순서
- serverless deploy cli 명령어로 에러메시지를 상세하게 출력하는 옵션 확인하기
- 디버깅 도구 환경 설정하기
- 배포 파이프라인 분석하기
- AWS CloudFormation의 Stack을 생성하는 코드 확인하기
- Stack의 존재 및 상태 여부를 검사하는 코드 확인하기
- Stack의 상태가 REVIEW_IN_PROGRESS인 경우 에러 메세지를 출력하는 코드 작성하기
- 유닛 테스트 코드 작성하기
해당하는 프로젝트에서 위 목표를 달성하기 위해 문제 해결 순서를 대략적으로 정리하여, 해결하려고하는 문제를 세분화 하였다. 실제 문제를 해결하는 과정은 생각한 순서에서 변경될 수 있지만, 대략적인 청사진을 그려나가면서 문제를 구체화 시킬 수 있도록 구성하였다.
⚒️ Debugging
솔직히 말하자면, 오랫동안 Serverless Framework를 사용했지만 어떤 언어로 작성되어있는지, 어떤 구조로 구성되어 있는지에 대해서는 잘 알지 못했다. 그러나, 이번에는 문제를 해결하기 위해 프로젝트의 코드를 Clone하고 디버깅 환경을 구성하는 것으로 첫걸음을 내딛어 보겠다.
1️⃣ serverless-cli command option 확인하기
디버깅을 진행하기 전에, 가장 먼저 프로젝트를 실행했을 때 더욱 상세한 로그를 출력받을 수 있는 모드를 설정하는 것이 중요하다고 생각했다. 일반적으로 terraform, node와 같은 다양한 CLI 프로그램들은 help 명령어를 이용해 해당 프로그램이 제공하는 command option을 확인할 수 있었는데, Serverless도 마찬가지로 동일한 기능을 제공할 것이라 생각했다.
serverless deploy --help 명령어를 입력하여 배포 과정에서 사용할 수 있는 옵션들은 어떤 것들이 있는지 확인해보자.
위의 이미지와 같이 디버깅할 때 도움이 될 것으로 보이는 2가지의 옵션을 확인할 수 있었다. 상세한 출력 결과를 표시할 수 있도록 도와주는 --verbose옵션과 Serverless의 디버그 로그를 출력하는 --debug 옵션이 가장 눈에 띈다.
지금부터 이 2가지의 옵션을 사용하여 디버깅 환경을 구성하고, 프로젝트의 흐름을 더욱 정확하게 이해할 수 있도록 도움을 받아보자.
2️⃣ 디버깅 도구 환경 설정하기
일반적인 오픈소스 프로젝트와 마찬가지로, Serverless Framework의 소스코드는 수많은 코드로 구성되어 있다. 그렇기 때문에, 작은 프로젝트들에서 사용하는 console.log()를 작성하고, 실행된 결과를 확인하는 방법으로 디버깅을 진행하기는 어렵다. 만약 console.log()와 같은 방법으로 디버깅하게 된다면, 수정된 소스코드를 확인하기위해 매번 프로그램을 재실행해야 하고, AWS 리소스들이 생성되어 의도치 않은 비용이 발생할 수도 있기 때문이다.
그렇다면, 이렇게 크고 복잡한 프로젝트를 분석하기 위해서 어떤 방법을 사용해야할까? IDE에 내장되어 있는 디버깅 도구를 이용하면, 생각한 비즈니스 로직이 정상적으로 동작하는지 확인할 수 있고, 특정 데이터가 예상한 대로 존재하는지 확인할 수 있게될 것이다.
Serverless Framework의 소스코드를 분석하기 위해 WebStorm IDE에서 제공하는 디버깅 도구를 기준으로 설명을 진행하겠다.
- Working directory: Serverless Framework를 실행할 폴더를 정의한다.
- JavaScript file: 디버깅을 진행할JavaScript 파일을 정의한다.
- Application parameters: Serverless Framework CLI에서 사용하는 옵션을 정의한다.
먼저, /bin/serverless.js 파일을 실행하도록 디버깅 환경을 구성했다. 또한, “Stack … does not exist” 에러가 발생하는 별도의 Serverless 프로젝트를 구성하고, 해당 디렉토리를 Working Directory로 설정하여 프로젝트를 실행하도록 구성했다. 마지막으로, 실행 순서를 명확하게 파악할 수 있도록 --verbose와 --debug 옵션을 추가하였다.
이렇게 모든 디버깅 환경을 구성한 후, 디버깅 도구를 실행하여 에러가 발생하는지 확인해보자.
Process Console에서도 정상적으로 에러가 발생하였다. 그렇다면, 이제부터 Serverless Framework의 소스 코드를 하나씩 뜯어보도록 하자.
3️⃣ 배포 파이프라인 분석하기
디버깅 도구를 이용하여 “serverless deploy 명령어 입력 → Serverless Framework 실행 → Serverless Framework 에러 발생” 에 해당하는 비즈니스로직이 어떤 코드에서 어떤 순서대로 처리되는지 분석하도록 하겠다.
- bin/serverless.js 파일은 Serverless Framework CLI의 실행파일이며, 주어진 환경변수, command option 등을 입력받아 해당 명령을 실행한다.
- script/serverless.js 파일은 Serverless Framework CLI의 로직을 담고 있는 스크립트 파일이다. 해당 파일은 bin/serverless.js를 통해 호출되어 Command option과 같은 정보를 받아서 처리한다.
- script/serverless.js 파일에서 serverless.yml 파일을 읽어들여 프로젝트에 구성되어 있는 설정 정보들을 조회하고, cloud provider에 해당하는 pluginManager를 호출한다.
- pluginManager는 Serverless Framework의 플러그인을 관리하는 모듈로, 플러그인을 실행하거나 Hook을 호출하는 역할을 한다. 입력받은 Command에 해당하는 Hook을 하나씩 꺼내, runHooks() 메서드에서 Hook들을 실행한다.
- Hook은 before, at, after 총 3가지로 구성되며, 하나의 Hook이 완료되기 위해선 3가지가 순서대로 실행된다.
- Hook은 before, at, after 중 일부가 존재하지 않을 수도 있다.
- 예를들어 before Hook은 존재하지 않고 at, after Hook만 존재할 수도 있다.
- 각각의 Hook은 플러그인에서 제공하는 이벤트에 대한 동작을 수행하며, AWS 리소스를 생성하거나 다양한 비즈니스 로직을 처리한다.
- 모든 Hook이 처리된 후, Serverless Framework는 종료된다.
이외에도 특정 Hook이 처리되지 않았거나, 예상치 못한 오류가 발생하는 경우 Serverless Framework는 에러를 출력하고 종료할 수 있다.
Hook의 Naming Convention
<PROVIDER>:<MAIN_EVENT>:<SUB_EVENT>
Serverless Framework의 Hook은 기본적으로 위와 같은 구조를 가지고 있다.<PROVIDER> 또는 <SUB_EVENT>는 존재하지 않을 수 있지만, <MAIN_EVENT>는 무조건 존재한다.
Hook은 pluginManager의 runHooks() 메서드에서 실행되며, 최종적으로 완성된 Hook은 AwsDeploy Class와 같이 지정된 Plugin Class에 있는 메서드를 실행하게된다.
4️⃣ AWS CloudFormation Stack을 생성하는 코드 확인하기
AWS CloudFormation의 Stack이 비정상적으로 생성됨.
현재 발생한 문제는 위와 같다. 그렇다면 가장 먼저 확인해야할 것은 AWS CloudFormation Stack이 어떤 코드에서 생성되는지 확인하는 것이다. 에러 메시지가 발생하는 부분을 확인하기 위해, 디버깅 도구를 이용하여 Code가 어떤 순서대로 진행되는지 확인해보자.
Process Console에서 Serverless Framework의 시작부터 종료될 때 까지 여러가지의 디버그 로그가 출력되는것을 확인할 수 있다.
디버그 로그에서 확인할 수 있듯이 “Stack … does not exist” 에러가 발생하기 전 가장 마지막에 실행되는 Hook은 “aws:deploy:deploy:createStack”이다. Hook의 <MAIN_EVENT>에서 유추할 수 있듯이 AwsDeploy Class의 createStack 메서드에서 처리되는 것을 확인할 수 있다.
create-stack() 메서드는 위와 같이 구성되어 있다. 여기서 특별한 점은 생성하려는 Stack을 AWS에 “describeStacks” 명령어로 Stack의 상태 정보를 조회하게된다.
describeStacks는 AWS Documentation에서 확인할 수 있듯이 다양한 Response 중에서 StackStatus를 반환하게 되는데, 이 정보는 현재 조회한 Stack의 상태를 가지고 있다. 이 정보를 바탕으로 에러 케이스를 작성하게 된다면, 비정상적인 상태를 가지고 있는 Stack을 분류할 수 있게 되고, 발생한 문제를 해결할 수 있게 될 것이다.
5️⃣ Stack의 상태가 REVIEW_IN_PROGRESS인 경우 에러 메세지를 출력하는 코드 작성하기
create-stack() 메서드에서 생성하려는 Stack이 존재할 경우, 상태 정보를 조회하는 것을 확인할 수 있었다. 따라서 describeStacks에서 반환된 StackStatus가 REVIEW_IN_PROGRESS 인 경우에 에러 메시지가 발생하도록 코드를 작성해보자.
const inactiveStateNames = new Set(['REVIEW_IN_PROGRESS']);
...
const stackStatus = data.Stacks[0].StackStatus;
if (inactiveStateNames.has(stackStatus)) {
const errorMessage = [
'Service cannot be deployed as the CloudFormation stack ',
`is in the '${stackStatus}' state. `,
'This may signal either that stack is currently deployed by a different entity, ',
'or that the previous deployment failed and was left in an abnormal state, ',
"in which case you can mitigate the issue by running 'sls remove' command",
].join('');
throw new ServerlessError(errorMessage, 'AWS_CLOUDFORMATION_INACTIVE_STACK');
}
return BbPromise.resolve('alreadyCreated');
- 비정상적인 StackStatus를 inactiveStateNames Set에 정의한다.
- data.Stacks[0]에 존재하는 StackStatus를 변수로 선언한다.
- StackStatus를 inactiveStateNames Set에 존재하는지 확인한다.
- 만약 inactiveStateNames Set에 현재 StackStatus가 존재한다면, 에러 메시지를 출력한다.
작성한 코드가 정상적으로 동작하는지 확인하기 위해, 다시 serverless deploy 명령어를 실행해보자.
예상한대로 CloudFormation Stack이 REVIEW_IN_PROGRESS 상태 일 때, 에러메시지가 출력되었다.
이렇게 Serverless Framework에서 발생하는 문제 상황을 해결하였다. 그렇지만 아직까지 하나의 문제가 남아 있는데, 작성한 코드가 다른 모듈에 영향을 끼치거나 불완전한 배포가 진행될 수 있는 문제가 있는지 확인이 필요하다. 따라서, 작성한 코드가 정상적으로 수행되는지 검증하는 유닛 테스트 코드를 작성하고, 현재 존재하는 Serverless Framework의 유닛 테스트 및 통합 테스트가 정상적으로 동작하는지 확인해보도록 할 것 이다.
이 과정에서 Mocha, Chai.js, Sinon.js와 같은 테스트 프레임워크를 사용하여 유닛 테스트 코드를 작성하도록 할 것이다.
6️⃣ 유닛 테스트 코드 작성하기
Serverless Framework의 유닛 테스트 코드는 Mocha를 기반으로 구성되어 있다. Mocha 테스트 프레임워크는 대표적으로 JavaScript에서 사용하는 Jest와는 다르게 Test Runner의 기능만 지원한다. 그렇기 때문에 외부 의존성이 있는 코드를 테스트할 때 필요한 Mock, Stub, Spy 기능을 지원하는 sinon.js와 코드를 검증할 때 필요한 Assertion 기능을 구현하는 chai.js를 함께 사용하여 하나의 테스트 코드를 구성하게된다.
그렇다면, 어떤 장점이 있기 때문에 Mocha 테스트 프레임워크를 사용하게 되는걸까?
Mocha는 다른 테스트 프레임워크와는 다르게 BDD (Behavior Driven Development)를 추구하는 테스트 프레임워크이다. 그렇기 때문에 Jest와 같은 다른 테스트 프레임워크와는 다르게 영어 문장을 해석하는 것처럼 테스트 코드를 분석할 수 있게 되어 어떤 행위를 하는지 명확하게 이해할 수 있도록 도와준다.
또한, Mocha는 테스트 코드를 작성하는 개발자가 원하는대로 다양한 라이브러리를 조합하여 사용할 수 있도록 유연성을 제공하게 된다. 그로 인해 개발자의 스타일에 맞게 테스트 코드를 작성할 수 있게 된다.
AWS Deploy 과정에서 발생하는 유닛 테스트 코드는 위와 같이 구성되는데, Hook 단위로 테스트 코드를 구성하는 것이 아니라 비즈니스 로직에서 발생하는 중요한 예외 케이스에 대해서 작성하게된다. 따라서, 작성하게 될 유닛 테스트 코드는 “Stack의 상태가 REVIEW_IN_PROGRESS일 때, 에러 메시지가 정상적으로 발생하는가?”를 검증하도록 작성해보자.
테스트 코드에서도 알 수 있듯이 describeStacksStub이 호출될 때, 반환값에 존재하는 StackStatus를 REVIEW_IN_PROGRESS 로 반환하도록 설정하였고, 아래에 있는 expect 구문에서 "AWS_CLOUDFORMATION_INACTIVE_STACK"에 해당하는 에러가 발생하는지 검증하도록 작성하였다.
해당하는 유닛 테스트 코드는 정상적으로 동작하였고, 이를 통해 작성한 코드가 정확하게 작동하며, 예상한대로 에러를 처리하는 것을 확인할 수 있었다. 결국, 처음 생각한 모든 목표를 달성하게되었다.
🎈Pull Request
Serverless Framework를 fork한 Repo에서 모든 작업을 완료한 후, PR을 작성했다. Github Issue에서 궁금한 점을 해결해준 Repo 관리자와 함께 비즈니스 로직과 Code Convetion에 맞도록 코드를 다듬어 나갔다.
그렇게 여러 차례의 코드 리뷰를 거쳐 최종적으로 Github Activity에 새로운 뱃지가 생성되게 되었다.
🐋결론
최근에는 규모가 있는 소스코드를 분석하고 싶다는 생각이 가득하였는데, 오픈소스를 분석하고 코드 리뷰를 받으며 많은 것을 깨닫게 된 것 같다.
작은 문제로 보이는 상황에서 디버깅을 시작하여, Issue를 작성하고, PR을 작성한 후, 마침내 Open Source Contributor의 첫 걸음을 내딛게 되었다.
'Infrastructure > Serverless Framework' 카테고리의 다른 글
[Serverless] Stack <STAK_NAME> does not exist 디버깅 (0) | 2023.03.25 |
---|