이 글은 이전 회사에서 운영 중인 서비스에 AWS Lambda, Serverless Framework, Node.js를 사용해 작성된 백엔드에서 MSA를 적용했던 글입니다.
처음 내가 이 서비스를 개발하기 시작했을 때는, 모놀리식으로 구성했었다. 초기 서비스였기 때문에 MSA를 적용해야 할 필요성을 느끼지 못했고, 추후 서비스가 커지게 되면 적용할수도 있지 않을까? 라고 어렴풋이 생각만 하고 있었다.
그런데, 서비스에서 사용되는 API가 점점 많아지니 몇 가지 문제가 생겼는데, 이게 개발자 입장에서 정말 불편했다. 아래는 문제점들이다.
service: my-service
frameworkVersion: '3'
useDotenv: true
custom:
serverless-offline:
useChildProcesses: true
noPrependStageInUrl: true
httpPort: 8081
lambdaPort: 8080
customDomain:
domainName: api.my-service.com
basePath: '${self:provider.stage}'
stage: ${self:provider.stage}
route53Region: ap-northeast-
certificateName: '*.my-service.com'
createRoute53Record: false
certificateArn: 'arn:aws:acm:ap-northeast-2:00000000000:certificate/blahblah'
endpointType: regional
plugins:
- serverless-offline
- serverless-latest-layer-version
- serverless-plugin-split-stacks
- serverless-domain-manager
package:
individually: true
patterns:
- '!.git/**'
- '!.gitignore'
- '!.DS_Store'
- '!npm-debug.log'
- '!.serverless/**'
- '!.serverless_plugins/**'
- '!.eslintrc'
- '!.idea/**'
- '!bitbucket-pipelines.yml'
- '!package-lock.json'
- '!README.md'
- '!node_modules/**'
- '!test/**'
- '!src/**'
- 'src/utils/**'
- 'src/lib/**'
- 'src/models/**'
- '!lambda-layer/**'
provider:
name: aws
deploymentBucket:
name: backend-deployment
deploymentMethod: direct
runtime: nodejs18.x
region: ap-northeast-2
stage: ${opt:stage, 'dev'}
environment:
TZ: Asia/Seoul
NODE_PATH: './:/opt/node_modules'
STAGE: ${sls:stage}
DB_DIALECT: ${env:DB_DIALECT}
DB_HOST: ${env:DB_HOST}
DB_PORT: ${env:DB_PORT}
DB_USERNAME: ${env:DB_USERNAME}
DB_PASSWORD: ${env:DB_PASSWORD}
DB_NAME: ${env:DB_NAME}
functions:
#-----------------------------------------------------------------------------#
getUsers:
handler: src/user/getUsers/index.getUsers
events:
- http:
method: get
path: /users
cors: true
layers:
- arn:aws:lambda:ap-northeast-2:722419581403:layer:test-backend-layer:latest
package:
patterns:
- 'src/user/getUsers/**'
#-----------------------------------------------------------------------------#
getUser:
handler: src/user/getUser/index.getUser
events:
- http:
method: get
path: /user
cors: true
layers:
- arn:aws:lambda:ap-northeast-2:722419581403:layer:test-backend-layer:latest
package:
patterns:
- 'src/user/getUser/**'
이건 serverless.yml
파일이다. 이 파일은 배포를 위한 serverless 프레임워크 설정, 각 함수를 작성하는 파일이다. 예시 파일이라 API는 두개만 작성되어 있다.
그런데, 지금은 getUsers, getUser API 밖에 없는데 만약 저 API가 100개까지 늘어난다고 생각해보자. 상품들을 조회하는 getProducts API, 상품을 등록하는 createProduct API 등 각각 메서드와 도메인이 다른 API들이 계속 추가되는 상황이라면, 기존 API를 찾기가 매우 힘들어진다.
물론 command+F(Mac 기준)를 사용해서 API명으로 찾으면 바로 찾을 수 있기는 하지만.. 하나의 거대한 파일 안에 모든 API가 작성되어 있는건 확실히 복잡했다.
첫번째 문제는 당장은 무시할 수 있다고 쳐도, 배포시간이 오래 걸리는 이 문제는 정말 그냥 지나치기가 어려웠다. API가 100개가 넘어가는 시점부터는 간단하게 수정할 게 있어서 수정 후 백엔드 코드를 배포하면, 배포가 약 10~15분 정도 걸리는 상황이 발생했다.
시작한지 얼마 안 된 서비스가 고작 몇 달만에 이렇게 배포시간이 오래 걸리는건 말이 안된다고 생각했고, 무엇보다 개발 서버에 직접 수정사항을 반영하며 고쳐야 하는 버그의 경우에는 정말 답답했다. 4번의 테스트를 해야 하는 경우엔, 무려 1시간이나 기다려야 했었다.
그런데, 신규 기능 개발이 바빠서 해결을 못하던 상황이었는데, 결국 신규 API를 추가했을 때 배포 시 버그가 발생해서 해결할 수 밖에 없는 상황이 왔었다.
그러던 중, API가 40개 정도 넘어가는 시점에 발생한 버그가 있었다. (아직 API 개수가 채 100개도 넘기지 않았을 시점이다) 에러 내용은 아래와 같다.
The CloudFormation template is invalid: Template format error: Number of resources, 201, is greater than maximum allowed, 200
위 버그의 원인은 AWS CloudFormation 스택 당 리소스 제한 정책을 위반했기 때문에 발생한 버그였다. 즉, 스택 당 리소스 제한이 200개라고 하면, 200개를 넘겼기 때문에 생기는 버그였다.
그런데, 문제는 나도 여기까지는 이해했는데 AWS CloudFormation이 갑자기 왜 나오는지, 그리고 스택은 또 뭐고 리소스는 왜 제한을 하는건지, 하나도 이해가 되지 않던 상황이었다. 그래서 당시 내 생각의 흐름은 아래와 같았다.
- 먼저 AWS CloudFormation이 왜 에러 로그에 보이는지부터 알아보자.
- 그리고 AWS CloudFormation이 하는 일에 대해 알아보자.
- 이후 왜 CloudFormation에서 리소스를 제한하는지도 확인해보자.
- 그러고 나서 CloudFormation의 리소스 제한 정책을 늘릴 수 있는지, 또는 리소스를 줄일 수 있는지를 생각해보자.
먼저, 이 버그는 배포 시 발생한 버그이다. 그럼, 먼저 백엔드 코드를 어떻게 배포하는지 확인해보자.
이 서비스의 백엔드는 Serverless Framework를 통해 AWS Lambda에 각 함수(API)를 배포한다. 좀 더 자세히 설명하면, 배포 시에는 Serverless Framework에서 제공하는 명령어인 sls deploy
를 실행해 배포를 진행한다. 이 명령어를 실행하면 세부적으로 다음과 같은 작업들이 아래와 같은 순서대로 진행된다.
중요한 내용은 3번이다. 즉, 현재 벡엔드에서 사용 중인 Serverless Framework는 배포할 때 실제 기능을 담당하는 Lambda 함수 뿐만 아니라 IAM 권한, 그리고 에러 발생 시 디버깅에 필수적인 CloudWatch 로그 구성 등의 여러 유용한 작업들을 CloudFormation에 한꺼번에 정의해서 배포하는 것이다.
CloudFormation 템플릿은 AWS 리소스들을 생성하는 명령 파일이라고 생각하면 된다. 위에서 설명했듯이, 아래와 같은 다양한 서비스의 설정이 포함되어 있다.
AWS는 이 CloudFormation 템플릿(코드)을 기반으로 해당 리소스들을 생성하여, 사용자가 지정한 대로 인프라를 자동으로 배포하고 관리할 수 있게 한다. 즉, EC2, Lambda 등과 같은 리소스를 직접 AWS 콘솔에서 수동으로 생성하는 불편함을 덜어준다.
AWS CloudFormation에 대한 좀 더 자세한 설명은 아래 링크를 참고하자.
CloudFormation의 리소스 제한은 시스템의 성능과 안정성을 유지하기 위한 것이다. 너무 많은 리소스가 포함된 스택은 배포와 관리가 복잡해질 수 있기 때문에 이러한 제한이 필요하다.
아래 글은 CloudFormation의 정책에 관련된 세부 사항을 정리한 글이다.
그럼, 먼저 1차원적으로 CloudFormation의 이 리소스 제한 정책을 상향할 수 있는지 찾아보았다. AWS와Serverless Framework 문서를 아무리 뒤져봐도 그런 방법은 보이지 않았다.
결국, 사용자가 임의로 이 제한 정책을 건드리는건 불가능하다고 결정을 내리고 리소스를 줄일 수 있는 방법은 없는지 찾아보았다.
어떻게든 리소스가 200개를 넘기지 않을 방법을 생각해보았다. 그런데, Serverless Framework를 통해 배포하는 하나의 Lambda 함수에는 최소 3개의 CloudFormation 리소스가 있다.
AWS::Lambda::Function
: 실제 기능을 나타내는 리소스AWS::Lambda::Version
: 특정 버전의 함수를 나타내는 리소스 (이를 통해 빠르고 쉬운 롤백이 가능)AWS::Logs::LogGroup
: CloudWatch 로그에 함수를 기록할 수 있는 리소스설명에서 알 수 있다시피 1, 2, 3번 모두 없어서는 안될 필수 기능들이다. 1번은 말할 것이 없고, 2번이 안된다면 배포가 잘못된다면 서비스를 돌이킬 수가 없는 상황이 발생하고, 3번이 없다면 디버깅을 할 수가 없을 것이다.
그럼 최소 하나의 API 당 3개의 리소스가 필요해진다. 그런데, 현재 백엔드는 API Gateway도 같이 이용하고 있기 때문에, 몇가지 리소스가 추가된다.
AWS::Lambda::Permission
: API Gateway가 해당 함수를 호출할 수 있도록 허용AWS::ApiGateway::Resource
: 엔드포인트에 대한 리소스 경로 구성AWS:ApiGateway::Method
: 엔드포인트에 대한 HTTP 메서드를 구성이 리소스들 또한 뺄 수는 없다고 생각했다.
Q. API Gateway는 왜 같이 사용하는 건가요?
A. API Gateway는 RESTful API를 생성하고 관리하며, 클라이언트 요청을 Lambda 함수로 라우팅하고 응답을 반환하는 역할을 한다. 또한 인증, 속도 제한, 모니터링 등의 기능을 제공하여 API를 더 안전하고 관리하기 쉽게 도와준다. 사실상 AWS로 서비스를 배포한다면 필수.
결국, 리소스 자체를 줄이는 것 또한 불가능하다고 판단했다.
위 내용을 종합하면 한 함수 당 CloudFormation 리소스의 개수는 6개다. 그럼 200개 리소스 제한에 걸리지 않는 상한선은 33개이다. 즉, API를 33개까지만 만들 수 있다는 것이다. 말이 안 된다.
이러면 Serverless Framework + Lambda를 사용해서는 서비스를 만들 수 없는 수준이라고 판단해서 당연히 해결방법이 있을거라고 생각하고 문서를 더 찾아봤다. 찾아보니, 이런 상황에 사용할 수 있는 CloudFormation에서 제공하는 중첩 스택(Nested Stack)을 사용할 수 있었다.
중첩 스택이란, 다른 스택의 자식으로 생성된 스택이다. 예를 들면, 아래와 같은 형태의 스택 구조가 있다고 가정해보자. 그럼, A 스택 하위에 B 스택, 그 하위에 C 스택, 다시 그 하위에 D 스택이 있게 되는 형태이다.
예를 들어, 대부분의 스택에 사용되는 DB 설정(A)이 있다고 가정해보자. DB 설정은 한번 초기에 설정하면 변화가 적은 요소이다. 똑같은 DB 설정을 복사하여 CloudFormation 템플릿에 붙여 넣는 대신 DB 설정 전용 템플릿(A)을 생성할 수 있다. 그런 다음 리소스를 사용하여 다른 템플릿(B) 내에서 해당 템플릿(A)을 참조하기만 하면 된다. 아래 코드는 중첩 스택을 사용한 예시다.
중첩 스택 사용 전
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
InstanceType:
Type: String
Default: 't2.micro'
Description: 'The EC2 instance type'
Environment:
Type: String
Default: 'Production'
Description: 'The deployment environment'
Resources:
MyEC2Instance:
Type: 'AWS::EC2::Instance'
Properties:
ImageId: ami-1234567890abcdef0
InstanceType: !Ref InstanceType
MyS3Bucket:
Type: 'AWS::S3::Bucket'
중첩 스택 사용 후
AWSTemplateFormatVersion: '2010-09-09'
Resources:
MyFirstNestedStack:2
Type: 'AWS::CloudFormation::Stack'
Properties:
TemplateURL: 'https://s3.amazonaws.com/amzn-s3-demo-bucket/first-nested-stack.yaml'
Parameters:
# Pass parameters to the nested stack if needed
InstanceType: 't3.micro'
MySecondNestedStack:
Type: 'AWS::CloudFormation::Stack'
Properties:
TemplateURL: 'https://s3.amazonaws.com/amzn-s3-demo-bucket/second-nested-stack.yaml'
Parameters:
# Pass parameters to the nested stack if needed
Environment: 'Testing'
DependsOn: MyFirstNestedStack
여기서 중요한건, 각각의 스택은 리소스를 200개까지 가질 수 있게 된다는 것이다.
따라서, 하나의 스택만 사용하는게 아니라 이렇게 중첩 스택을 사용한다면 CloudFormation의 200개 리소스 제한 정책을 피할 수 있게 된다.
그럼, 중첩 스택을 어떻게 적용할까? 위의 예시대로 하면 좋겠지만, 현재 나는 Serverless Framework를 사용해 Lambda 함수를 배포하고 있기 때문에 CloudFormation의 템플릿이 자동으로 생성, 구성되고 있다. 그래서 Serverless Framework에서 사용할 수 있는 도구가 있는지 찾아봤더니(관련 문서), 다음 두개의 플러그인을 찾을 수 있었다.
난 이 두개의 플러그인 중, serverless-split-stacks 플러그인을 선택했다. serverless-nested-stacks 플러그인의 GitHub Readme를 읽어봤는데, 이 플러그인이 CloudFormation 스택을 어떻게 분할, 구성하는지는 나와 있는데, 플러그인 사용자가 직접 세부적으로 커스텀을 할 수 있는 방법이 적혀 있지 않았다.
그에 반해 serverless-split-stacks는 제한 사항, 커스텀 방법 등에 대한 설명이 자세하게 작성되어 있었다. 적용 과정은 아래와 같다.
1. 설치
npm install serverless-split-stacks --save-dev
2. serverless.yml
파일에 작성
# serverless.yml
custom:
splitStacks:
perFunction: false
perType: false
perGroupFunction: true
plugins:
- serverless-split-stacks
나는 위 3개의 옵션 중, perGroupFunction 옵션을 사용했다. 각 도메인 별로 스택을 분리할 수 있다면 관리하기 편할거라고 생각했다.
3. 커스텀을 위해 루트 디렉토리에 stack-map.js
파일 생성
// stack-map.js
module.exports = (resource, logicalId) => {
if (logicalId.includes('User')) return { destination: 'UserStack' };
if (logicalId.includes('Product')) return { destination: 'ProductStack' };
if (logicalId.includes('Cart')) return { destination: 'CartStack' };
if (logicalId.includes('Payment')) return { destination: 'PaymentStack' };
};
도메인 자체는 훨씬 더 많았지만 예시 코드이기 때문에 4개의 도메인으로 나눴다. 루트 디렉토리에 stack-map.js
파일을 생성하면 각 조건에 따라서 각 도메인에 해당되는 스택이 생성되고, logicalId ‘User’가 포함되어 있다면 UserStack으로, ‘Product’가 포함되어 있다면 ProductStack으로 그룹화되도록 만든 코드다.
4. sls deploy
명령어 실행
마지막으로, sls deploy
명령어를 통해 배포를 진행한다.
여기까지 진행했을 때, 결과적으로 CloudFormation의 200 Resource 제한 에러가 더 이상 발생하지 않고 성공적으로 배포가 진행되었다. 그러나 여전히 문제점은 남아있었다.
그럼, 지금까지 중첩 스택에 대한 설명을 읽은 사람들은 다음과 같은 의문이 생길 수 있다.
Q. 그럼, 중첩 스택을 적용해도 하나의 거대한 메인스택 하위에 여러 중첩 스택이 분할되는 형태 아닌가요? 그럼 아까 말한 배포 시간이 오래 걸리는 문제와 API를 찾기 어렵다는 문제는 해결되지 않은 거잖아요.
- A. 그렇다. 결국 배포가 되지 않는 가장 치명적인 버그만 해결된거지 앞서 말한 두가지 문제점은 해결되지 못했다. 여전히
serverless.yml
파일은 거대하고, 배포 시간은 10~15분이 걸렸다.
애초에 serverless-split-stacks 플러그인의 Readme에서도 이 방법을 사용해 스택을 분할하는 것이 임시방편이라고, 자기들이 만든 플러그인 사용을 추천하지 않았다. 그래서 근본적인 해결책을 찾게 되었다.
아래 Serverless Framework의 문서에서는 거대한 서비스를 Micro Service로 분할하라고 한다. 이 방법을 가장 추천한다고 한다.
물론 이미 리소스 제한 버그 해결 과정에 이 문서를 읽었어서 MSA 적용이 가장 좋은 해결책인건 알고 있었다. 그러나 다음과 같은 부분이 마음에 걸렸다.
MSA 적용을 늦게 한 이유는 아래와 같이 불확실한 부분이 있었기 때문이다. (처음 도입해보니까)
현재 배포가 자동화 되어 있는 상황에서 각 도메인에 해당하는 작은 서비스로 분할하면, 각 서비스별로 따로 배포해야 되나? 그럼 도메인이 10개라면 배포를 각각 10개를 해줘야 하는건가? 오히려 더 귀찮은 일이 많아지는건 아닐까?
실제 운영중인 서비스의 구조를 크게 바꾸는 일이기 때문에 처음엔 도입에 소극적이었다. 이 방법이 더 좋은걸 알고 있음에도 불구하고 중첩 스택을 먼저 적용한 이유도 서비스의 안정성 때문이었다.
일단, API를 찾기 힘들던 1번 문제가 해결된다. 모든 API가 하나의 거대한 serverless.yml
파일에 있는 것이 아닌, 각 도메인 별로 따로 yml 파일이 생성되기 때문에(ex: user.yml
, product.yml
) 각 도메인에 맞는 API를 확인하면 되기 때문이다.
또한, 2번 문제인 배포 시간이 오래 걸리는 문제 또한 해결된다. sls deploy
명령어를 실행할 때마다 모든 API를 변경점이 있는지 체크하고 다시 배포했기 때문에 배포 시간이 많이 소요된 것이었는데, 유저 도메인, 상품 도메인 별로 따로 배포하게 되면 API 개수가 확실히 적어지기 때문에 배포 시간 또한 개선될 것이라고 생각했다.
먼저, 기존의 serverless.yml
파일을 확인해보자. 코드에 보이는 함수는 총 4개인데, 총 함수가 100개가 넘는다고 가정하겠다. 모든 함수는 serverless.yml
파일에 작성되어 있다.
## serverless.yml
service: my-service
frameworkVersion: '3'
useDotenv: true
custom:
serverless-offline:
useChildProcesses: true
noPrependStageInUrl: true
httpPort: 8081
lambdaPort: 8080
customDomain:
domainName: api.my-service.com
## basePath 추가
basePath: '${self:provider.stage}'
stage: ${self:provider.stage}
route53Region: ap-northeast-
certificateName: '*.my-service.com'
createRoute53Record: false
certificateArn: 'arn:aws:acm:ap-northeast-2:00000000000:certificate/blahblah'
endpointType: regional
functions:
#-----------------------------------------------------------------------------#
getUsers:
handler: src/user/getUsers/index.getUsers
events:
- http:
method: get
path: /users
cors: true
layers:
- arn:aws:lambda:ap-northeast-2:722419581403:layer:test-backend-layer:latest
package:
patterns:
- 'src/user/getUsers/**'
#-----------------------------------------------------------------------------#
getUser:
handler: src/user/getUser/index.getUser
events:
- http:
method: get
path: /user
cors: true
layers:
- arn:aws:lambda:ap-northeast-2:722419581403:layer:test-backend-layer:latest
package:
patterns:
- 'src/user/getUser/**'
#-----------------------------------------------------------------------------#
getProducts:
handler: src/product/getProducts/index.getProducts
events:
- http:
method: get
path: /products
cors: true
layers:
- arn:aws:lambda:ap-northeast-2:722419581403:layer:test-backend-layer:latest
package:
patterns:
- 'src/product/getProducts/**'
#-----------------------------------------------------------------------------#
createOrder:
handler: src/order/createOrder/index.createOrder
events:
- http:
method: post
path: /order
cors: true
layers:
- arn:aws:lambda:ap-northeast-2:722419581403:layer:test-backend-layer:latest
package:
patterns:
- 'src/order/createOrder/**'
## ....
이후, 기존의 하나의 serverless.yml 파일을 도메인 별로 나눠야 한다. 루트 파일에 다음 3개의 yml 파일을 생성한다.(user.yml
, product.yml
, order.yml
…)
실제로 내가 분리한 도메인은 10개가 넘었지만, 여기서는 user, product, order 등 세개의 도메인으로만 나눈다고 가정하겠다.
그럼, 예시로 user.yml
파일을 확인해보자.
## user.yml
service: user-service
frameworkVersion: '3'
useDotenv: true
custom:
customDomain:
domainName: api.my-service.com
basePath: '${self:provider.stage}/v1-user'
stage: ${self:provider.stage}
route53Region: ap-northeast-2
certificateName: '*.my-service.com'
createRoute53Record: false
certificateArn: 'arn:aws:acm:ap-northeast-2:722419581403:certificate/blahblah'
endpointType: regional
functions:
#-----------------------------------------------------------------------------#
getUsers:
handler: src/user/getUsers/index.getUsers
events:
- http:
method: get
path: /users
cors: true
layers:
- arn:aws:lambda:ap-northeast-2:722419581403:layer:test-backend-layer:latest
package:
patterns:
- 'src/user/getUsers/**'
#-----------------------------------------------------------------------------#
getUser:
handler: src/user/getUser/index.getUser
events:
- http:
method: get
path: /user
cors: true
layers:
- arn:aws:lambda:ap-northeast-2:722419581403:layer:test-backend-layer:latest
package:
patterns:
- 'src/user/getUser/**'
handler: src/user/getUserDailyTasks/index.getUserDailyTasks
원래 기존의 serverless.yml
파일에서 해줬던 초기 설정도 모두 넣어줘야 한다. 기존 serverless.yml
파일과 다른 점은 service name, 그리고 customDomain의 basePath 부분이다.
serverless.yml
: api.my-service.comuser.yml
: api.my-service.com/userproduct.yml
: api.my-service.com/productorder.yml
: api.my-service.com/order이미 구매해 둔 커스텀 도메인이 있다면, 해당 커스텀 도메인의 각 하위 도메인을 설정해줘야 한다. 방식은 위와 같다.
이 부분은 직접 AWS에서 스크린샷으로 남기고 싶은데, 지금은 해당 서비스의 AWS에 접근할 수 없어서 상세하게 작성이 어렵다. 따라서 AWS 콘솔에서 작업할 상세 내용은 아래 링크를 확인하자.
기존 서비스는 Github Action을 통해서 백엔드 배포를 자동화했었다. 그런데, 나는 각 도메인 별로 서비스를 나누더라도 특정 서비스를 따로 배포하는게 아니라, 만약 해당 도메인에 수정사항이 있다면 배포 시 자동으로 해당 변경 사항을 감지하여 해당 서비스가 배포됐으면 했다.
먼저, 로컬에서 작업한 내역을 dev 환경에 배포하는 dev-ci
파일을 확인해보자.
name: DEV CI
on:
push:
branches:
- develop
tags:
- 'development-**'
jobs:
## PR의 변경 사항이 포함된 브랜치를 체크아웃하고 빌드를 수행
Build:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v3
with:
fetch-depth: 0
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
## node modules 캐싱하기
- name: Cache node modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.OS }}-build-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.OS }}-build-
${{ runner.OS }}-
- name: Install Dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: npm install
## 파일 변경 감지 부분
- name: Watch changed files
uses: dorny/paths-filter@v2
id: changes
with:
base: develop
filters: |
user:
- 'src/user/**'
- 'user.yml'
product:
- 'src/product/**'
- 'product.yml'
order:
- 'src/order/**'
- 'order.yml'
# Serverless Framework 설치
- name: Install Serverless Framework
run: npm install -g serverless
# USER 함수 배포
- name: Deploy User Service
if: steps.changes.outputs.user == 'true'
run: sls deploy --config user.yml --stage dev --verbose
# PRODUCT 함수 배포
- name: Deploy Product Service
if: steps.changes.outputs.product == 'true'
run: sls deploy --config product.yml --stage dev --verbose
# ORDER 함수 배포
- name: Deploy Product Service
if: steps.changes.outputs.product == 'true'
run: sls deploy --config product.yml --stage dev --verbose
# 배포가 정상적으로 완료됐을 경우에 디스코드로 배포 성공/실패 여부 알림을 보내준다.
- name: Send deploy result to Discord
uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK }}
status: ${{ job.status }}
title: 'DEV 배포'
content: '<@0000000000000>'
파일 변경 감지 부분의 코드에 대해 설명하자면, 예를들면, src/user/**
경로, 즉 user 폴더 내부에서 파일이 변경되거나 추가, 또는 삭제 된다면 해당 변경사항을 감지하여 user 도메인의 함수를 재배포한다.
steps.changes.outputs.<도메인명>
이 true
가 되는데, 이 때 실행 명령어가 sls deploy
가 아닌 sls deploy —config <도메인명>.yml
이 된다. sls deploy는 기본적으로 루트 위치의 serverless.yml
파일을 배포하는데, 위와 같이 —config
prefix 뒤에 특정 파일명을 작성해주면, 해당 파일을 배포하게 된다.피처 개발을 하든, 버그를 수정하든 일반적으로 작업 중인 도메인에만 변경사항이 생기기 때문에, 해당 도메인의 함수들만 재배포하게 된다.
여기까지 진행하면, 이전과 똑같이 sls deploy
명령어를 실행하면 작업한 도메인의 함수들만 재배포한다.
매번 배포할 때마다 모든 의존성 패키지를 재설치하는 것은 불필요하다고 생각했다. 그래서 만약 배포 시 새로 추가, 업데이트 또는 삭제된 패키지가 있을 경우에만 의존성 패키지를 재설치하고, 그렇지 않을 경우에는 npm install
을 건너 뛴다.
## node modules 캐싱하기
- name: Cache node modules
uses: actions/cache@v3
with:
path: node_modules
## package-lock.json 파일의 변경사항을 통해 의존성 패키지 변경을 감지한다.
key: ${{ runner.OS }}-build-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.OS }}-build-
${{ runner.OS }}-
- name: Install Dependencies
## 새로 추가, 업데이트 또는 삭제된 패키지가 있을 경우에만 npm install
if: steps.cache.outputs.cache-hit != 'true'
run: npm install
가장 힘들었던 배포 시간 문제가 해결되었다. 배포시간이 15분에서 평균 2분 이내로 단축되었다.
이제 100개가 넘는 함수를 한꺼번에 재배포하는 것이 아닌, 변경사항이 있는 도메인의 함수들만 재배포 하기 때문이다. 또한, 의존성 캐싱을 통해 1~2분 정도 배포 시간을 추가로 더 단축시켰다. 덕분에 버그 수정 및 개발 환경에서 테스트할 일이 있을 때 개발 생산성이 많이 향상되었다. (동료 개발자 한 분이 배포할 때 살 것 같다고 얘기했을 때 매우 뿌듯했음..)
사실 지금의 구조가 완전한 MSA라고 보기에는 무리가 있지만, 초기 서비스에서 Lambda에서 요구하는 수준으로 도메인을 나눴다고 보면 될 것 같다.