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
そのまま使います。
# 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:
を追加して、メモリサイズとタイムアウト時間を設定
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?)でうまく両用できるパッケージがあれば切り替えたいところ。
{
"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
そのまま使います。
{
"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の設定を調整
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関数
ハンドラ
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でデバッグ > 構成を開く
{
"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
でも設定できます。
更新はデプロイコマンドを実行するだけ。