AWS Lambda + Typescript + PuppeteerでWebスクレイピング


AWS Lambda + Typescript + PuppeteerでWebスクレイピング

前提条件

  • 開発環境
    • Windows10
    • Node.js 12.14.0-x64
    • AWS CLI
    • VS Code
  • AWSアカウント
  • IAMユーザー※

※IAMユーザーはServerlessFrameworkのリソース作成に必要な権限が付与されていること(今回はAdministratorAccessを使用)、AWS CLIに--profile serverlessで設定していることを前提とする

Serverless Framework のプロジェクト作成

AWSリソースの管理にServerless Frameworkを使います。AWS Lambdaを使う上で必要な面倒くさいことを大体やってくれます。すごい。

Serverless Frameworkのインストール

サービス作成コマンドを使うためにグローバルインストールします。
どうしてもグローバルに入れたくない人は、後述するファイル群を自作しても良いです。

> npm install -g serverless

> serverless --version
Framework Core: 1.60.4
Plugin: 3.2.6
SDK: 2.2.1
Components Core: 1.1.2
Components CLI: 1.4.0

サービスの作成

aws-nodejs-typescriptでサービスを作成します。

> serverless create --template aws-nodejs-typescript --name scraping-service --path scraping-service

以下のファイルが作成されます。

scraping-service
|-.vscode ※vscode(ドット無し)で作成されるので、先頭にドットを追加する
| `-launch.json
|-.gitignore
|-handler.ts
|-package.json
|-serverless.yml
|-tsconfig.json
`-webpack.config.js

グローバルモジュールの削除

> npm remove -g serverless

各種設定の調整

.gitignore

そのまま使います。

.gitignore
# package directories
node_modules
jspm_packages

# Serverless directories
.serverless

# Webpack directories
.webpack

Serverless Framework

  • NPMモジュールをLambda Layerに入れるための設定
    • includeModules: false
    • plugins:serverless-layersを追加
    • deploymentBucket:にS3バケット名を指定 ※S3バケットは事前に作成します
  • テンプレートでAPIGatewayのエンドポイントが設定されているので削除
  • stage:を引数から受け取れるように設定
  • profile:にAWS CLIのprofile名を設定
  • region:にデプロイ先のリージョンを設定
  • functions:に関数定義execute:を追加して、メモリサイズとタイムアウト時間を設定
serverless.yml
service:
  name: scraping-service
# app and org for use with dashboard.serverless.com
#app: your-app-name
#org: your-org-name

custom:
  webpack:
    webpackConfig: ./webpack.config.js
    includeModules: false

# Add the serverless-webpack plugin
plugins:
  - serverless-layers
  - serverless-webpack

provider:
  name: aws
  stage: ${opt:stage, 'dev'}
  profile: serverless
  region: ap-northeast-1
  runtime: nodejs12.x
  #apiGateway:
  #  minimumCompressionSize: 1024 # Enable gzip compression for responses > 1 KB
  environment:
    AWS_NODEJS_CONNECTION_REUSE_ENABLED: 1
  deploymentBucket:
      name: scraping-service-deploy # 事前にS3バケットを作成してバケット名を指定

functions:
  #hello:
  #  handler: handler.hello
  #  events:
  #    - http:
  #        method: get
  #        path: hello
  execute:
    handler: handler.execute
    memorySize: 1024
    timeout: 30

NPMモジュール

  • 必要なパッケージを追加。Chromeはchrome-aws-lambdaを使用します。
    • npm install chrome-aws-lambda
    • npm install puppeteer-core
    • npm install -D @types/puppeteer-core
    • npm install -D serverless
    • npm install -D serverless-layers
  • scriptsにローカル実行用のコマンドを追加します。

※chrome-aws-lambdaでインストールされるバイナリがWindowsでうまく動かないので、ローカル実行は普通にインストールしたChromeで代替しています。WindowsとLambda(AmazonLinux?)でうまく両用できるパッケージがあれば切り替えたいところ。

package.json
{
  "name": "scraping-service",
  "version": "1.0.0",
  "description": "Serverless webpack example using Typescript",
  "main": "handler.js",
  "scripts": {
    "local": "node ./node_modules/serverless/bin/serverless invoke local -f execute",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "dependencies": {
    "chrome-aws-lambda": "^2.0.1",
    "puppeteer-core": "^2.0.0",
    "source-map-support": "^0.5.10"
  },
  "devDependencies": {
    "@types/aws-lambda": "^8.10.17",
    "@types/node": "^10.12.18",
    "@types/puppeteer-core": "^2.0.0",
    "fork-ts-checker-webpack-plugin": "^3.0.1",
    "serverless": "^1.60.4",
    "serverless-layers": "^1.4.3",
    "serverless-webpack": "^5.2.0",
    "ts-loader": "^5.3.3",
    "typescript": "^3.2.4",
    "webpack": "^4.29.0",
    "webpack-node-externals": "^1.7.2"
  },
  "author": "The serverless webpack authors (https://github.com/elastic-coders/serverless-webpack)",
  "license": "MIT"
}

Typescript

そのまま使います。

tsconfig.json
{
  "compilerOptions": {
    "lib": ["es2017"],
    "removeComments": true,
    "moduleResolution": "node",
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "sourceMap": true,
    "target": "es2017",
    "outDir": "lib"
  },
  "include": ["./**/*.ts"],
  "exclude": [
    "node_modules/**/*",
    ".serverless/**/*",
    ".webpack/**/*",
    "_warmup/**/*",
    ".vscode/**/*"
  ]
}

Webpack

  • source-mapの設定を調整
webpack.config.js
const path = require('path');
const slsw = require('serverless-webpack');
const nodeExternals = require('webpack-node-externals');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');

module.exports = {
  context: __dirname,
  mode: slsw.lib.webpack.isLocal ? 'development' : 'production',
  entry: slsw.lib.entries,
  devtool: slsw.lib.webpack.isLocal ? 'inline-source-map' : 'source-map',
  resolve: {
    extensions: ['.mjs', '.json', '.ts'],
    symlinks: false,
    cacheWithContext: false,
  },
  output: {
    libraryTarget: 'commonjs',
    path: path.join(__dirname, '.webpack'),
    filename: '[name].js',
  },
  target: 'node',
  externals: [nodeExternals()],
  module: {
    rules: [
      // all files with a `.ts` or `.tsx` extension will be handled by `ts-loader`
      {
        test: /\.(tsx?)$/,
        loader: 'ts-loader',
        exclude: [
          [
            path.resolve(__dirname, 'node_modules'),
            path.resolve(__dirname, '.serverless'),
            path.resolve(__dirname, '.webpack'),
          ],
        ],
        options: {
          transpileOnly: true,
          experimentalWatchApi: true,
        },
      },
    ],
  },
  plugins: [
    // new ForkTsCheckerWebpackPlugin({
    //   eslint: true,
    //   eslintOptions: {
    //     cache: true
    //   }
    // })
  ],
};

NPMモジュールのインストール

> npm install

※この時点で新しいバージョンが出ているモジュールが存在するので、必要に応じでnpm outdatedで確認してバージョンアップしましょう。

Lambda関数

ハンドラ

handler.ts
import 'source-map-support/register';
import { Handler } from 'aws-lambda';
import * as chromium from 'chrome-aws-lambda';

export const execute: Handler = async (_event, _context) => {
  let result;
  let browser;

  try {
    // chrome-aws-lambda でインストールされるバイナリがWindowsで動かないため
    // ローカルで実行する際は通常インストールしたChromeを代替とする
    browser = await chromium.puppeteer.launch({
      args: chromium.args,
      ignoreDefaultArgs: process.env.IS_LOCAL ? ['--single-process'] : [],
      defaultViewport: chromium.defaultViewport,
      executablePath: process.env.IS_LOCAL ?
        'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe' : await chromium.executablePath,
      headless: chromium.headless, // ここをfalseにするとChromeのウインドウが表示されるので、開発時にうまくいかない場合は変更すると良いです
    });

    const page = await browser.newPage();

    await page.goto('https://example.com');

    result = await page.title();
  } finally {
    if (browser) {
      await browser.close();
    }
  }

  return {
    statusCode: 200,
    body: JSON.stringify({
      message: `Title: ${result}`,
    }, null, 2),
  };
}

実行

> npm run local
...
{
    "statusCode": 200,
    "body": "{\n  \"message\": \"Title: Example Domain\"\n}"
}

デバッグ

VS Codeでデバッグ > 構成を開く

launch.json
{
  "configurations": [
    {
      "name": "Lambda",
      "type": "node",
      "request": "launch",
      "runtimeArgs": ["--inspect"],
      "program": "${workspaceFolder}/node_modules/serverless/bin/serverless",
      "args": ["invoke", "local", "-f", "execute", "-d", "{}"],
      "outFiles": ["${workspaceFolder}/.webpack/service/*"]
    }
  ]
}

ブレークポイントを設定してF5でデバッグ実行

デプロイ

デフォルトではdevステージにデプロイされます。
本番と分けたい場合は-sオプションでステージを指定できます。

> npx sls deploy -v

CloudFormationを利用して必要なリソースが作成されます。らくちん。

AWS上で実行

こちらも同じく-sオプションでステージを指定できます。

> npx sls invoke -f execute
{
    "statusCode": 200,
    "body": "{\n  \"message\": \"Title: Example Domain\"\n}"
}

CloudWatchで実行ログを確認できます。

おわり

最後に不要な課金が発生しないようにAWSリソースを削除しましょう。

> npx sls remove

実際のスクレイピング処理が完成したら、CloudWatch Eventsのスケジュールなり、SNS Topicなりをトリガーに設定しましょう。
一部はserverless.ymlでも設定できます。
更新はデプロイコマンドを実行するだけ。