IT/Node.js

[Node.js] serverless computing with AWS Lambda (feat.puppeteer)

sohyun_ 2021. 7. 15. 15:46
반응형

이 페이지는 AWS Lambda 서비스와 serverless 프레임워크를 이용하여 별도의 서버 없이 코드를 실행하는 방법에 대해 설명하고 있다.

 

lambda
: 람다(Λ, λ: 그리스어 알파벳의 열한번째 글자)

사전적 의미와는 다르지만, 프로그래밍에서는 익명함수(Anonymous function)를 지칭하는 용어로 람다함수를 사용하기도 한다.

 

AWS Lambda & serverless Framework

AWS Lambda는 서버 프로비저닝 또는 관리, 워크로드 인식 확장 로직 생성, 이벤트 통합 유지, 런타임 관리 없이 코드를 실행할 수 있는 서버리스 컴퓨팅 서비스이다. 코드를 zip파일 또는 컨테이너 이미지로 업로드하면 자동으로 컴퓨팅 실행 능력을 할당하고, 이벤트를 기반으로 코드를 실행한다. Node.js, Python, Go, Java 등의 언어로 작성하고 서버리스 및 컨테이너 도구(AWS SAM 또는 Docker CLI)를 사용하여 기능을 구축, 테스트 및 배포까지 할 수 있다.

 

Lambda의 장점

  • 관리할 서버가 없음
  • 지속적 크기 조정
  • 밀리초 측정을 통한 비용 최적화 - 컴퓨팅 시간에만 비용을 지불
  • 모든 규모에서 일관된 성능

 

1) serverless 프레임워크 설치

작성한 코드를 aws lambda에 쉽게 배포할 수 있도록 도와주는 프레임워크 serverless 를 설치해서 사용한다 (자세한 가이드라인 및 설명은 링크의 Documentation 참조)

npm install -g serverless
sudo npm install -g serverless // Error: EACCES: permission denied 발생할 경우
serverless create --template [템플릿]
ex) serverless create --template aws-nodejs

// 템플릿 종류

aws-nodejs, aws-python, aws-python3, aws-groovy-gradle, aws-java-maven, aws-java-gradle, aws-scala-sbt, aws-csharp, aws-fsharp, aws-nodejs, openwhisk-nodejs, openwhish-python, openwhisk-swift, google-nodejs, plugin, hello-world.....

 

The Serverless Application Framework 등장!

따로 버전 설정을 하지 않아서 가장 최신 버전인 v2.44.0으로 설치되었다.

이 과정까지 완료되면 serverless.yml 파일이 생성되는데, 환경설정을 해줄 수 있는 가장 중요한 역할의 Configuration 파일이다. provider 항목은 기본적으로 제공하고 있어서 iam role만 추가했고, region과 runtime의 경우 프로젝트의 속성에 맞게 수정하면 된다.

event, layer, webpack 등등 다른 항목들도 추가로 설정 가능 (작성할 수 있는 설정이 너무나도 많아서 위에 링크로도 첨부)

특정 시간에 슬랙으로 리포트 전송하는 프로젝트를 생성했기 때문에 편의상 그대로 SendReport, SendSlackReport의 함수명을 사용할 예정이다.

 

2) node 설정

모듈의 설치, 삭제, 업그레이드 및 의존성을 관리해주는 node package manager를 통해 초기화를 해주면 package.json 파일이 생성되는 것을 확인할 수 있다. 이후에는 일반적인 방법으로 npm install 을 통해 설치하여 사용한다.

npm init -y
npm install [모듈명]
ex) npm install puppeteer

 

3) aws 계정 설정

어떤 aws 계정에 연결해서 lamda에 배포하게 되는지 설정이 필요하다.

(혹시나 permission denied 에러가 발생한다면, IAM에서 해당 계정이 람다에 배포할 수 있는 권한이 있는지 확인해봐야한다)

serverless config credentials --provider aws --key [key] --secret [secret]

 

4) 로컬 환경 실행 (Documentation)

serverless invoke local --function [함수명]
ex) serverless invoke local --function sendReport

실행하면 handler.js의 sendReport 함수에 작성한대로 터미널에 결과값이 노출되고, 일반 코드와 동일하게 console.log로 데이터 확인도 가능하다.

여기서는 handler를 깔끔하게 확인하고 싶어서 해당 handler.js 파일이 아닌 다른 파일에서 서비스단을 구성하여 호출하고 있다.

일반적인 코드를 작성할 때 처럼 dynamodb, mysql, request 통한 외부 api 호출 등 다양하게 사용이 가능하다.

module.exports.sendReport = async event => {
  const result = await cron.sendSlackReport();
  return {
    statusCode: 200,
    body: JSON.stringify(
      {
        message: '[finish] send slack report!',
        body: {
          result,
        },
        input: event,
      },
      null,
      2
    ),
  };
};

 

5) 배포 (Documentation)

serverless deploy // 처음 배포할 때

serverless deploy function --function [함수명]
ex) serverless deploy function --function sendReport

aws-sdk처럼 aws 기본 모듈이 아니라면 node_modules를 포함해서 올려야하는데 Lambda에는 배포 용량 제한이 있어서 주의해야한다.

직접 업로드 시 50MB, 레이어 계층 포함해 압축 해제 250 MB....(참고링크)

배포하면 AWS 콘솔에서 자동으로 생성된 함수 [서비스명-env-함수명]을 확인해 볼 수 있다. 환경을 설정하지 않으면 default는 prd

 

Lambda + puppeteer

람다의 용량제한과 puppeteer의 잘못된 만남으로 날아간 나의 아까운 일주일에 대해서 설명해보려고 한다.

크롤링할때 가장 일반적으로 사용하는 puppeteer는 local-chromium 이라는 내장된 chrome을 포함하고 있어서 용량이 굉장히 굉장히 크다. JavaScript heap out of memory 또는 Request must be smaller than 69905067 bytes for the UpdateFunctionCode operation 에러를 만났다면 용량 체크를 해야한다.

반대로 용량을 줄이려다가 cannot find module '모듈명'을 만났다면 node_modules이 잘 올라갔는지 확인해보고, severless.yml package-patterns에서 제외되지는 않았는지 확인해야한다.

 

// ex. 52.06MB로 용량초과 Error

@serverless-chrome/lambda, puppeteer-lambda, node-html-to-image,... 시행착오를 거치면서 만난 모듈들과 layer, webpack방식 모두 직접 설정하려하니 용량 제한의 벽에 부딪혀 실패했다.

layer는 s3에 업로드 한 파일을 읽어오게 하는 방식도 있는데, puppeteer는 대체 어떻게 만들었는지 용량제한으로 아무것도 되지 않았다.

 

결론적으로 사용할 모듈은 chrome-aws-lambda인데, puppeteer-core와 puppeteer에 의존성이 있기 때문에 모두 설치해주어야한다. 버전은 아래 표를 참고해서 9.0.0으로 통일한 후 ^(caret)을 제거해 버전까지 픽스시켰다.

npm install --save chrome-aws-lambda@9.0.0 puppeteer-core@9.0.0 puppeteer@9.0.0

가장 먼저 작성했던 serverless.yml 파일에서 package patterns 작성한 내용을 다시 보면, node modules에서 설정한 모듈들의 파일을 제외해서 올리겠다고 되어있다.

chrome-aws-lambda는 로컬에서 실행하기 위해서 설치했고, deploy 할때는 layer를 이용할 것이기 때문에 package에는 포함시키지 않아도 된다. puppeteer 또한 의존성 때문에 설치하였기 때문에, 용량이 큰 .local-chromium은 포함시키지 않는다.

package:
  patterns:
    - '!node_modules/chrome-aws-lambda/**'
    - '!node_modules/puppeteer/.local-chromium/**'

가장 메인이 되는 layer는 Google Chrome for AWS lambda as a layer!

동일한 이슈를 겪는 분들이 많아서인지 리전별, 버전별로 arn만 복사해서 사용할 수 있도록 레이어가 만들어져있다. (천사님♥)

(TMI. timeout은 기본 6초인데, 생각보다 크롤링하는데 오래걸려서 에러가 나지 않게 30초로 설정)

functions:
  sendReport:
    handler: handler.sendReport
    layers:
      - arn:aws:lambda:ap-northeast-2:764866452798:layer:chrome-aws-lambda:24
    timeout: 30

AWS Lambda 콘솔에서 Layer - Add a layer 클릭하고 Specify an ARN에서 복사한 내용 붙여넣으면 끝!

 

// puppeteer code

const chromium = require('chrome-aws-lambda');

const browser = await chromium.puppeteer.launch({
  args: chromium.args,
  defaultViewport: chromium.defaultViewport,
  executablePath: await chromium.executablePath,
  headless: true,
  ignoreHTTPSErrors: true,
});

const page = await browser.newPage();
const page.goto(url);
await page.waitForSelector();
if (browser) await browser.close();

 

// node-html-to-image code

npm install node-html-to-image --save

html을 이미지로 변환하는 방법에는 쉽게 설치해서 사용할 수 있는 모듈 node-html-to-image 가 있지만, 이 모듈 또한 puppeteer를 사용하고 있어서 chrome-aws-lambda를 사용하기 위해 분리하려고 한다.

How to Convert HTML to an Image Using Puppeteer in Node.js 를 참조해서 코드를 작성하였다.

(puppeteerArgs, screenshotArgs는 빈 객체지만 없으면 설정이 달라지는지 여백이 생기고 이미지가 제 멋대로여서 일단 추가함)

const chromium = require('chrome-aws-lambda');

const nodeHtmlToImage = async function nodeHtmlToImage(html) {
  let imageBuffer;
  const browser = await chromium.puppeteer.launch({
    args: chromium.args,
    executablePath: await chromium.executablePath,
    puppeteerArgs: {},
    headless: true,
    ignoreHTTPSErrors: true,
  });
  try {
    const page = await browser.newPage();
    await page.setContent(html);

    const content = await page.$('body');
    imageBuffer = await content.screenshot({ omitBackground: false, screeshotArgs: {} });

  } catch (err) {
    console.log('nodeHtmlToImage Error');
  } finally {
    await browser.close();
  }
  return imageBuffer;
};

 

Cron job with EventBridge

cron 동작을 시키기위해서는 serverless.yml에 schedule 설정을 할 수도 있고, AWS 콘솔 trigger를 설정할 수도 있다. 여기서는 코드 변경없이 작업하기 위해서 콘솔을 이용하기로 한다.

trigger 탭에서 Add trigger를 선택하고, EventBridge (CloudWatch Events) 에서 Rule name, schdule expression만 필수로 입력하면 끝

스케쥴은 일반적인 crontab처럼 쓸 수도 있고, rate (5 minutes) / rate(1 day)처럼 설정할 수도 있다. (참고문서)

cron(Minutes Hours Day-of-month Month Day-of-week Year) // UTC 타임존

ex) 매일 한국시간 오후 2시 15분에 실행
cron(15 5 ? * * *)

콘솔에서 Layers와 EventBridge (cloudWatch Events)가 확인된다면 마지막 테스트

success가 뜨고 에러 메세지가 없다면 1차 완료!

trigger 설정한 시간에도 제대로 동작한다면 최종 완료! (설정한 시각에 에러가 발생하면 반복해서 실행할 수도 있음 자동 retry)

 

반응형