Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

Puppeteer를 AWS Lambda에서 실행하기

Puppeteer를 소개했는데 내가 Headless Chrome을 사용하면서 계속하던 작업은 Headless Chrome을 AWS Lambda에 올리는 것이다. Headless Chrome으로 AWS Lambda에서 웹사이트 스크린샷 찍기에서 설명했듯이 Lambda에서 사용할 수 있으면 다른 대부분 환경에서 사용할 수 있고 운영 걱정 없이 사용할 수 있다.

Puppeteer가 사용하기 편하다는 걸 알았으니 이를 이용해서 Lambda에 올려봤다. 이미 Lambda에도 Headless Chrome이 동작하는 것은 확인했으니 이론상은 문제없지만, Puppeteer가 Chromium을 내장하고 있어서 확인이 필요했다.

Puppeteer 코드

Lambda로 테스트하기 위해 간단한 예제를 작성해 보자.

const puppeteer = require('puppeteer');

const getTitle = () => {
  return puppeteer.launch()
    .then((browser) => {
      return browser.newPage()
        .then((page) => {
          return page.goto('https://blog.outsider.ne.kr/', { waitUnitl: 'networkidle' })
            .then(() => page.evaluate(() => {
               return document.querySelector('title').innerHTML;
            }))
        })
        .then((title) => {
          browser.close();
          return title;
        });
    });
};

exports.handler = (event, context, callback) => {
  getTitle()
    .then(context.succeed)
    .catch(context.fail);
};

위 코드는 Puppeteer로 이 블로그에 접속해서 <title>의 내용을 반환하는 간단한 코드이다. 이 코드를 이용해서 Lambda를 올려보자.

headless_shell 사용

Puppeteer만 설치하고 이 파일 하나만 있어서 Zip으로 압축하면 68MB가 나오고(macOS 기준) puppeteer에 설치된 Chromium은 macOS 용이므로 Lambda에서 사용하려면 Linux용을 사용해야 한다.

지난 글에서 Chromium 프로젝트에서 headless_shell을 빌드해서 사용했는데 Puppeteer를 사용하기 위해 v62.0.3198.0용 headless_shell을 올려두었다. [Puppeteer 릴리스 페이지](https://github.com/GoogleChrome/puppeteer/releases에 가면 타겟 Chromium 버전을 볼 수 있는데 여기서는 Puppeteer v0.10.2를 사용한다.(v0.11.0용 headless_shell은 아직 빌드를 하지 못했다.) 이 headless_shll을 다운 받아서 실행 권한(+x)을 주어야 한다.

const getTitle = () => {
  return puppeteer.launch({
    executablePath: process.env.CHROME_PATH,
    dumpio: true,
    args: [
      '--no-sandbox',
      '--vmodule',
      '--single-process',
    ],
  })
    .then((browser) => {
      return browser.newPage()
        .then((page) => {
          return page.goto('https://blog.outsider.ne.kr/', { waitUnitl: 'networkidle' })
            .then(() => page.evaluate(() => {
               return document.querySelector('title').innerHTML;
            }))
        })
        .then((title) => {
          browser.close();
          return title;
        });
    });
};

앞의 코드에서 getTitle함수를 다시 보면 puppeteer.launch()에 옵션을 추가했다. 가장 중요한 부분은 executablePath 옵션이고 여기서는 환경변수 CHROME_PATH로 다운받은 headless_shell을 바라보도록 /var/task/headless_shell로 지정할 것이다. dumpio: true 부분은 Chrome의 동작을 디버깅하려고 로깅을 추가한 것이고 args 부분은 리눅스에서 사용하려고 크롬의 플래그를 추가한 것이다. 크롬의 기본 플래그는args 옵션을 사용한다.

Puppeteer의 Node.js 6.x 지원

용량문제를 해결하기 위해서 이전 글에서 webpack을 사용한다고 했다. npm으로 설치한 모듈에서 필요한 코드만 포함되도록 해서 50MB 제한을 넘지 않으려면 꼭 필요한 설정이다.

처음에는 기존과 같게 webpack으로 dist.js를 빌드했는데 실행하려고 보니 dist.js 파일에 async/await 코드가 포함되면서 오류가 발생했다. Lambda는 현재 Node.js 6.10까지만 지원한다.

Puppeteer는 초기에 나올 때는 Node.js 7.6.0 이상으로 만들어졌으므로 async/await를 많이 사용하고 있었고 저장소의 예제도 모두 async/await로 되어 있다. 하지만 이후 Node.js v6에 대한 지원이 추가되었다.

Puppeteer의 index.js를 보면 다음과 같이 되어 있다.

let folder = 'lib';
try {
  new Function('async function test(){await 1}');
} catch (error) {
  folder = 'node6';
}

module.exports = require(`./${folder}/Puppeteer`);

이는 런타임에서 async 함수의 존재를 확인하고 타겟 폴더를 바꿔준다. 저장소에는 node6이라는 폴더가 없지만 빌드할 때 async/await를 Node.js v6에 맞게 트랜스파일하므로 설치한 버전에서는 이 폴더가 존재한다. 하지만 webpack을 코드를 실행하면서 dist.js를 만드는 것이 아니므로 이 동적인 require()를 제대로 처리 못 하고 두 가지 버전의 파일을 모두 가져오면서 문제가 되는 것이다.

// webpack.config.js
const path = require('path');

module.exports = {
  target: 'node',
  entry: './index.js',
  output: {
    path: path.resolve(__dirname),
    filename: 'dist.js',
    libraryTarget: 'commonjs2',
  },
  externals: {
    'aws-sdk': 'aws-sdk',
  },
  resolve: {
    alias: {
      puppeteer: 'puppeteer/node6/Puppeteer.js',
    },
  },
};

이는 webpack 설정에서 resolve로 사용 파일을 지정해주면 해결할 수 있다.

Lambda에서 실행

이제 다 만들었으므로 Lambda에 올려보자. webpack으로 빌드한 뒤 dist.jsheadless_shell 2개의 파일을 압축하면 44MB의 zip 파일을 만들 수 있다.

AWS Lambda에 zip을 올리는 화면

AWS Lambda에서 새 Lambda를 만들고 zip 파일을 올리고 환경변수를 설정했다.

Lambda가 정상적으로 실행된 화면

테스트 실행을 하면 Puppeteer로 내 블로그의 제목을 잘 가져온 것을 볼 수 있다.

2017/09/29 23:25 2017/09/29 23:25