🗿

Serverless Framework 사용 후기

Created
7/12/2021, 7:49:00 AM
Tags
AWS
Database
Backend
Subtitle
너무 훌륭하고...
어느 날과 다름없이 람다에 함수를 추가하려고 AWS 계정에 접속했다.
해당 계정은 여러 팀원들이 사용하고 있다보니, 개인이 필요할 때마다 추가하고 있었는데 그러다 보니 처음보는 API들이 꽤 많이 생겼다.
< 아니.. 누구세요? 누구신데.. 저희 계정에 계신거죠? >
이렇게 기능이 공유가 되지 않는 상황 자체가 달갑지 않았고, Python을 메인으로 하는 개발자도 더 이상 없어서 런타임도 굉장히 거슬렸다.
그 외 코드리뷰, 개발 환경등 불편한 요소들이 많아서 이번 기회에 고치고자 Serverless Framework를 도입했다.
Serverless Framework는 AWS Cloudformation을 관리 배포하는 기술이다. 이를 통해서 코드 수준에서 여러 AWS Resource들을 유지 관리 할 수 있다는 장점이 있다.

Tutorial

Serverless framework는 여러 Provider들을 지원하는데, 이 중에서 AWS만 살펴보자.
설치하기
npm install -g serverless
Shell
serverless cli를 설치한다. 이를 통해 deploy packing 등을 한다.
인증 하기
SERVERLESS가 적용할 AWS 계정을 환경변수에 추가해야 한다.
이 때 여러 profile을 적용하거나 IAM으로 적용할 수는 있는데 권한 추가가 필요하기 때문에 지금은 생략한다.
export AWS_ACCESS_KEY_ID=<your-key-here> export AWS_SECRET_ACCESS_KEY=<your-secret-key-here> # 환경변수에 추가한다.
Shell
템플릿 설치 및 배포하기
serverless # 이후 원하는 런타임 및 템플릿 설치 serverless deploy
Shell
처음이라면 Starter로 어떻게 만들어지는지 추천하는 편이다.
뭔가 순식간에 일어났지만 Cloudformation 및 Lambda에 잘 배포된 것을 확인 할 수 있을 것이다. 이제 차차 업그레이드 해보자.

Typescript를 포함한 개발환경 입히기

기왕 로컬에서 작업할 것이라면, 가능하면 우리가 사용하고 있는 개발환경과 유사하게 만들어주고 싶었다.
Webpack을 이용하면 배포 직전에 js로 packing하면서 타입 스크립트를 사용할 수 있을 뿐만 아니라 Lambda에서 Layer를 추가하여 Dependencies를 추가하는 방식을 사용하지 않고도 편하게 Dependencies를 사용할 수 있을 것이라 생각했다.
// webpack.config.js const path = require('path') const nodeExternals = require('webpack-node-externals') const slsw = require('serverless-webpack') const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin') const isLocal = slsw.lib.webpack.isLocal module.exports = { mode: isLocal ? 'development' : 'production', entry: slsw.lib.entries, externals: [nodeExternals()], devtool: 'source-map', resolve: { extensions: ['.js', '.json', '.ts'], }, output: { libraryTarget: 'commonjs2', path: path.join(__dirname, '.webpack'), filename: '[name].js', }, target: 'node', module: { rules: [ { test: /\.(ts|js)x?$/, exclude: /node_modules/, use: [ { loader: 'cache-loader', options: { cacheDirectory: path.resolve('.webpackCache'), }, }, 'babel-loader', ], }, ], }, plugins: [new ForkTsCheckerWebpackPlugin()], }
JavaScript
Webpack 설정은 serverless-webpack 의 예제를 참고하여 작성하였다.
그 외에도 lint나 jest등을 추가하여 개발환경을 구성할 수 있었다.
아래에 간단히 boilerplate를 만들어서 업로드 했으니 참고 하실 수 있을것 같다.

Lambda Function 테스트 하기

로컬환경으로 가져오면서 Lambda 함수들에 대한 테스트 코드를 작성할 수 있게 됬다.
이 때, 꽤 재미있는 부분은 Lambda에서 작성하는 기능들이 외부 기능에 의존하는 함수가 많다는 점이다.
예를 들면 Slack, Open API, AWS-SDK 등등...
때문에 TDD를 포함한 테스트를 지속적으로 해야 하는 경우 Test Double, Mocking을 통해 의존성을 제거하고 외부 기능에 대한 사이드 이펙트를 제거해주는 것이 필요했다.
그러다 보니 이전에 I/O 에 대한 의존성 완전분리에 대한 글이 생각나서 겸사겸사 Persistency Layer(DataBase ORM)에 대한 의존성도 제거해보았는데 확실히 테스트가 빨라졌다. 뿐만 아니라 테스트 작성 전에는 데이터베이스를 연결하지 않아도 될까? 라는 걱정이 많았는데 오히려 함수 flow에만 집중할 수 있어서 좋은 경험을 한 거 같다.
아래는 테스트의 일부분을 가져왔다.
보는 것처럼 s3 upload혹은 orm Model 들을 Mocking을 적극적으로 사용하여 테스트를 작성했다.
/* eslint-disable @typescript-eslint/ban-ts-comment */ jest.mock("../models/VideoProductModel"); jest.mock("../utils", () => ({ ...jest.requireActual("../utils"), uploadToS3: jest.fn(), })); import { buildDirectoryPath, SERVICE_SUB_DOMAIN, uploadToS3 } from "../utils"; import getDefaultSitemap from "../views/sitemap/getDefaultSitemap"; import getIndexSitemap, { INDEX_TYPE } from "../views/sitemap/getIndexSitemap"; import { buildVideoSitemap, } from "../views/sitemap/buildResourceSitemap"; import { VideoProductModel, } from "../models"; import createSiteMap, { getTotalPage } from "../views/sitemap/createSiteMap"; describe("sitemap.xml 생성 요청시", () => { VideoProductModel.findAll = jest.fn(() => [{ id: 1, contentID: 1 }]); VideoProductModel.count = jest.fn(() => 10000); afterEach(() => { jest.clearAllMocks(); }); describe("서비스 별 사이트맵 생성시에", () => { test("권장 URL 25000개 이하일 경우에 XML파일을 하나 업로드 한다.", async () => { //@ts-ignore PostModel.count = jest.fn(() => 1000); const { totalPage, totalCount } = getTotalPage(await PostModel.count()); await createSiteMap(SERVICE_SUB_DOMAIN.COMMUNITY, [ { type: INDEX_TYPE.BASIC_POST, totalCount, totalPage, buildSitemap: builBasicdPostSitemap, }, ]); expect(uploadToS3).toHaveBeenCalledTimes(1); }); test("권장 URL 25000개 이상일 경우에는 인덱스를 나눠 XML를 업로드 한다.", async () => { const DEFAULT_PAGE = 1; const INDEX_PAGE = 1; //@ts-ignore PostModel.count = jest.fn(() => 260000); const { totalPage, totalCount } = getTotalPage(await PostModel.count()); await createSiteMap(SERVICE_SUB_DOMAIN.COMMUNITY, [ { type: INDEX_TYPE.BASIC_POST, totalCount, totalPage, buildSitemap: builBasicdPostSitemap, }, ]); expect(uploadToS3).toHaveBeenCalledTimes( DEFAULT_PAGE + INDEX_PAGE + totalPage ); }); }); });
JavaScript

Serverless.yml 설정하기

공식 문서에 설정 옵션값에 대한 자세한 설명이 있기 때문에 업로드한 bolier-plate의 설정을 바탕으로 간단하게만 확인해보자.
service: serverless plugins: - serverless-webpack // serverless에 webpack을 사용하기 위한 plugin custom: webpack: webpackConfig: 'webpack.config.js' includeModules: forceInclude: - pg // 코드 내에 implicit하게 사용하는 라이브러리 같은 경우에는 웹팩에 포함되지 않아서 - pg-hstore // 다음과 같이 명시해줘야 하는 경우가 있음 - mysql2 provider: name: aws vpc: // 프로젝트 전체에 vpc설정을 할 수 있다. 필요하다면 추가할 수 있다. securityGroupIds: region: ${file(./config.${self:provider.stage}.json):SECURITY_GROUB_ID} subnetdIds: region: ${file(./config.${self:provider.stage}.json):SUBNET_ID} runtime: nodejs12.x stage: ${opt:stage,'dev'} region: ${file(./config.${self:provider.stage}.json):REGION} lambdaHashingVersion: 20210708 environment: COMMON_CDN_S3_BUCKET: ${file(./config.${self:provider.stage}.json):COMMON_CDN_S3_BUCKET} PG_URI: ${file(./config.${self:provider.stage}.json):PG_URI} SERVERLESS_AWS_ACCESS_KEY_ID: ${file(./config.${self:provider.stage}.json):SERVERLESS_AWS_ACCESS_KEY_ID} SERVERLESS_AWS_SECRET_ACCESS_KEY: ${file(./config.${self:provider.stage}.json):SERVERLESS_AWS_SECRET_ACCESS_KEY} functions: hello: # 함수명 handler: src/apis/sample.hello # 함수의 위치 environment: COMMON_CDN_S3_BUCKET: ${file(./config.${self:provider.stage}.json):COMMON_CDN_S3_BUCKET} PG_URI: ${file(./config.${self:provider.stage}.json):PG_URI} SERVERLESS_AWS_ACCESS_KEY_ID: ${file(./config.${self:provider.stage}.json):SERVERLESS_AWS_ACCESS_KEY_ID} SERVERLESS_AWS_SECRET_ACCESS_KEY: ${file(./config.${self:provider.stage}.json):SERVERLESS_AWS_SECRET_ACCESS_KEY} events: - schedule: rate(2 hours) #CloudWatch 추가 - http: #API GATEWAY 추가 method: post path: hello integration: lambda goodbye: handler: src/apis/sample2.goodbye events: - http: method: get path: goodbye integration: lambda
YAML

환경변수

file(./config.dev.json):PG_URI 를 통해 파일로 혹은
${env:PG_URI} 로 시스템 환경변수의 값을 환경변수로 업로드 할 수 있다.

Stage

${self:provider.stage} 로 배포 Stage를 명시할 수 있다.
배포 시에 --stage [스테이지 명] 으로 업로드 가능하다.

그외 장단점 및 마무리

대체적으로 많은 것을 지원해주고, 커뮤니티도 커서 만족스러운 경험을 했다.
하지만 해당 구조를 적용하면서 업로드 하는 .js 파일이 너무 커서 Lambda 대시보드에 나오지 않는 부분은 생각보다 불편했다.
< 업로드 이후 테스트가 불편하다. >
하지만 테스트 코드를 작성할 수 있기 때문에, 테스트를 더 잘 작성하는 것을 강제하는 느낌이라서 오히려 괜찮은거 같기도 하고... 개선해야 할 사항이기도 하다.
장점으로는 앞서 봐왔듯이 람다 내에 작성할 때보다 개발환경이 우수하다.
그리고 가장 큰 장점은 히스토리를 관리하고 코드리뷰가 가능해졌다는 점이다.
어떤 팀원이 코드를 작성해서 PR을 요청을 함으로써, 해당 기능에 대한 컨텍스트를 공유하고 그로 인해 Bus Factor를 줄일 수 있게 된다.
이 외에도 빠르게 API를 개발할 수 있어서, 종종 사용할 것 같다.