ADliveテックブログ

ADlive株式会社の技術ブログです。

Puppeteer on AWS LambdaをTypescriptからサクッと使う(2019年9月版)

f:id:tech_adlive:20190904121055p:plain

はじめに

Puppeteerはプログラムからブラウザ(Chrome or Chromium)を操作して利用できるNode.js用のライブラリです。Puppeteerを使用することで、

などが簡単に行えます。

Puppeteerは裏側でブラウザを起動するため、CPUやメモリなどのリソースを多く使用します。PuppeteerをAWS Lambda上で利用することで、同時実行の際のCPU負荷やメモリ使用量を気にすることなく、スケールする環境で利用できるようになります。

PuppeteerをLambdaを利用するための記事はたくさん存在しますが、古いものだと色々と手順が面倒だったり、動かなかったりするものもあります。

ここでは、2019年9月時点で、PuppeteerをLambda上でTypescriptから使うための、最短な手順をご紹介します。

事前準備

以下を事前に行っておいてください。

プロジェクトのセットアップ

Serverless Frameworkのテンプレートから、Typescriptベースのプロジェクトを作成します。 以下のコマンドを実行します。

mkdir example
cd example

sls create  --template aws-nodejs-typescript
npm install

まずは1度デプロイしてみる

まずこの時点でデプロイして動作させて、ビルドやデプロイができることを確認しておきます。これにより、この後で追加・修正によって発生した問題の切り分けを行いやすくなります。

以下のコマンドでLambdaにデプロイします。

sls deploy
...
api keys:
  None
endpoints:
  GET - https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/hello
functions:
  hello: example-dev-hello
layers:
  None

上記のログがコンソールに出力されます。ログ中のendpointsのURLにブラウザでアクセスしてみましょう。(エラーが発生した場合は、事前準備を確認してみてください)

{
  "message": "Go Serverless Webpack (Typescript) v1.0! Your function executed successfully!",
  "input": {
    "resource": "/hello",
    "path": "/hello",
...

ブラウザに上記のようなJSONが出力されれば、デプロイは成功です。

Puppeteerのセットアップ

次にPuppeteerを利用するための設定を追加していきます。

puppeteer-corechrome-aws-lambdaのモジュールをインストールします。また、Puppeteerの型をTypescriptで認識するために @types/puppeteerを、次の作業のためにwebpack-node-externalsもインストールしておきます。

以下のコマンドでインストールできます。

npm i puppeteer-core
npm i chrome-aws-lambda
npm i @types/puppeteer -D
npm i webpack-node-externals -D

次に、Puppeteerの対応のために、webpack.config.jsを修正します。

webpack.config.jsの先頭で「webpack-node-externals」を読み込み、設定に「externals: [nodeExternals()]」を追加します。これにより、node_modules配下を1つのファイルに結合しない設定になります。

webpack.config.js

const path = require('path');
const slsw = require('serverless-webpack');
const nodeExternals = require('webpack-node-externals'); // 追加
...
module.exports = {
  mode: slsw.lib.webpack.isLocal ? 'development' : 'production',
  ...
  target: 'node',
  externals: [nodeExternals()], // 追加
  module: {
  ...
...

serverless.ymlを修正する

serverless.ymlを3箇所修正します。

1. NodeJSのバージョンを8.10に

Lambdaで使用するnodeのバージョンを「10」から「8.10」に変更します。これは「chrome-aws-lambda」モジュールが「nodejs8.10」環境にしか対応していないためです。

2. メモリサイズとタイムアウトをデフォルト値から変更する

PuppeteerをLambdaで使用する場合、メモリはある程度大きなものにしておく必要があります。ここでは1024にします。また、タイムアウト値も、デフォルトの6秒では短すぎるため、30秒に変更します。

3. webpackIncludeModulesを有効にする

また、nodeExternalsに関連したカスタム設定として「webpackIncludeModules: true」を追加して、モジュールがパッケージに含まれるようにします。

変更後のserverless.ymlは以下のようになります。

serverless.yml

...
provider:
  name: aws
  runtime: nodejs8.10 # 変更

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

# 以下追加
custom:
  webpackIncludeModules: true

コードの修正

最後にhandler.tsをPuppeteerを使用したコードに修正します。ここでは

  1. 対象のURLをパラメーターで受け取り
  2. 対象URLにPuppeteerを使ってアクセスし
  3. ページのタイトルを取得して返す

という処理を追加します。

handler.ts

import 'source-map-support/register';

import { APIGatewayProxyHandler } from 'aws-lambda';
import * as chromium from 'chrome-aws-lambda';

import { Browser } from 'puppeteer';

export const hello: APIGatewayProxyHandler = async (event, _context) => {
  const { url } = event.queryStringParameters;
  let browser: Browser = null;
  try {
    browser = await chromium.puppeteer.launch({
      args: chromium.args,
      defaultViewport: chromium.defaultViewport,
      executablePath: await chromium.executablePath,
      headless: chromium.headless,
    });
    const page = await browser.newPage();
    await page.goto(url);
    const title = await page.title();
    return {
      statusCode: 200,
      body: JSON.stringify({
        title,
        url,
      }, null, 2),
    };
  } finally {
    if (browser !== null) {
      await browser.close();
    }
  }
};

再度デプロイする

再度sls deployコマンドでデプロイします。エンドポイントに対して、

https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/hello?url=https%3A%2F%2Fadlive.asia%2F

のようにurlパラメータ付きでアクセスします。すると以下のようにタイトルが取得できているのが確認できました。

{
  "title": "営業専門家のWEBマーケティング|アドリヴ株式会社‐ADlive. Inc.",
  "url": "https://adlive.asia/"
}

今回のソースコードGitHubにあげていますので参考にされてください。

github.com

デプロイしたアプリを削除しておく

不要になったらsls removeですべてのリソースを削除しておきましょう。

終わりに

手軽にスケールするPuppeteer環境が使えるのって素晴らしいですね。今回のサンプルは誰でも外からアクセスできるので、実運用の際はVPC Endpointなどを利用して、アクセス制限をかけて利用しましょう。

Spring BootでDeveloper ToolsとLiveReloadを使ってサクサクWeb開発

はじめに

 

少し前までのJavaでのWeb開発って、

  1. コードを書く
  2. Tomcatを再起動
  3. 数十秒待つ
  4. ブラウザリロードする

こんなイメージありませんか?

再起動の待ち時間の間にうっかりTwitterやはてぶを開いてしまって、何やっているか忘れてしまっていたり・・・。待ちにより思考が分断され、なかなか開発効率があがらない状態になったりします。これが嫌で他の言語などに流れていってしまった方も多いのではないでしょうか。

JRebelやいくつかのフレームワークでは動的なリロード(Hot Reloading)をサポートしていますが、有償だったり設定が面倒だったりで、あまり普及しているとは言い難い状況でした。

Spring BootでDevelper ToolsとLiveReloadを使うと他の言語でのWeb開発と同様、再起動不要でサクサク開発ができるようになります。開発者は「コードを書いて保存する」だけです。Tomcatの再起動はもちろんブラウザのリロードも不要になります。

Spring BootのドキュメントにはDevelper Toolsの設定方法がありますが、LiveReloadの詳しい説明はLiveReloadのサイトを参照する必要があったり、IntelliJ上での設定はWeb上に点在していたりして、きちんと動作させるのには少しだけコツがいります。

このエントリでは、一からSpring Bootのプロジェクトを作成し、Spring BootのDevelper ToolsとLiveReloadを設定して、IntelliJ上で動作させるまでをステップバイステップで説明します。

前準備

JDKIntelliJ IDEAはインストールしておきます。JDKは8で確認していますが、JDK 11でも同様に動作するはずです。またIntelliJ IDEAはCommunity 版の2018.3で動作確認していますが、Ultimate版でも基本的には同じです。

また、サンプルコードはKotlinで書いていますが、こちらもJavaで書いても基本的に同じはずです。適時、Javaで試される方への補足を入れています。また、Kotlin版、Java版の最終のソースコードへのリンクも文末に掲載していますので参考にされてください。

Spring Framework 5からKotlinがサポートされていますし、Web+DB PRESS Vol. 109でKotlin/Spring Bootでの特集が組まれていたり盛り上がってきています。もしKotlin試したことない方がいたら、本エントリでぜひお試しくださいませ。

www.infoq.com

gihyo.jp

Spring Initializrを使ってプロジェクトの雛形を作成

最初にプロジェクト雛形をSpring Initializrのサイト上で作成します。
IntelliJ IDEA Ultimateをお使いの方は、[File] -> [New Project] -> [Spring Initializr]で起動するウィザードからもほぼ同様の操作が可能です。)

  1. https://start.spring.io/をブラウザで表示します。
  2. 以下のように設定を変更します。
    (変更する箇所は太字にしています)

    Project: Gradle Project
    Language: Kotlin
    Spring Boot: 2.1.3
    Project Metadata:
     - Group: com.example
     - Artifact: demo
    Dependencies: 以下の3つを追加
     - Web
     - Thymeleaf
     - DevTools

    以下のような設定になっているはずです。 

    f:id:tech_adlive:20190318165844p:plain

    Spring Initializrの設定


  3. [Generate Project]をクリックして、プロジェクトの雛形ファイルをダウンロードします。
  4. ダウンロードしたzipを適当なディレクトリで解凍します。

IntelliJからプロジェクトを起動する

次にプロジェクトをIntelliJにインポートし、Spring Bootを起動してみましょう。

  1. IntelliJ起動時のダイアログで[Import Project]を選択するか、すでに別のプロジェクトを起動している場合は[File] -> [New] -> [Project from Existing Sources...]を選択し、解凍したディレクトリ直下の[settings.gradle]を選択して、[Open]を実行します。f:id:tech_adlive:20190318171405p:plainf:id:tech_adlive:20190318171608p:plain
  2.  Gradleの設定画面がでるので、そのまま[OK]を押します。

    f:id:tech_adlive:20190318171714p:plain

  3. 右のサイドバーのGradleのパネルから、デバッグ実行で[bootRun]タスクを実行して、Spring bootを起動します。Springのロゴと「Tomcat started on port(s): 8080」というメッセージがコンソールに出れば起動完了です。f:id:tech_adlive:20190319150553p:plain
  4. f:id:tech_adlive:20190318172038p:plain

  5. http://localhost:8080/」にアクセスします。以下の画面が表示されればSpring Bootが起動しています。(まだ画面がないのでエラー画面が表示されています)

    f:id:tech_adlive:20190318172218p:plain

HTMLファイルとコントローラーを追加する

1. 以下の3つのファイルを追加します。

src/main/kotlin/com/example/demo/IndexController.kt

package com.example.demo

import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping

@Controller
@RequestMapping("")
class IndexController {
  @GetMapping
  fun index(): String = "index"
}

src/main/resouces/templates/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
</head>
<body>
  <h1>Hello LiveReload!</h1>
  <script src="/js/index.js"></script>
</body>
</html>

src/main/resouces/static/js/index.js
(jsディレクトリは存在しないので作成してください)

console.log("Welcome LiveReload from js"); 

 2. [Stop] -> [Stop All]を押して、Gradleから起動しているSpring Bootを停止します。

f:id:tech_adlive:20190319150700p:plain

 3. GradleからbootRunを起動します。

 4.「http://localhost:8080/」をリロードします。以下の画面とブラウザのコンソールにログが表示されれば成功です。

f:id:tech_adlive:20190318184518p:plain

f:id:tech_adlive:20190318184500p:plain

Developer Toolsの自動再起動機能を使うための設定(build.gradle)

  1. [build.gradle]に以下の設定を追加して保存します。
    Javaの方は「"$buildDir/classes/kotlin/main"」を「"$buildDir/classes/java/main"」にする必要があります。)

    apply plugin: 'idea'
    
    idea {
        module {
            inheritOutputDirs = false
            outputDir = file("$buildDir/classes/kotlin/main") 
        }
    }
  2. Gradleの設定を読み込むか聞いてくるので[Enable Auto-import]をクリック。
    f:id:tech_adlive:20190318180553p:plain

Developer Toolsの自動再起動機能を使うための設定(IntelliJのプロジェクト設定)

  1. IntelliJで[CMD + Shift + A] (Windowsの場合はCtrl + Shift + A) を実行して、「Build project automatically」と入力し、[Build project automatically]を選択。

    f:id:tech_adlive:20190318175602p:plain

  2. [Build project automatically]にチェックを入れて[OK]をクリック。

    f:id:tech_adlive:20190318175706p:plain

    この設定はプロジェクトごとに保存されます。

Developer Toolsの自動再起動機能を使うための設定(IntelliJのRegistry設定)

  1. IntelliJで[CMD + Shift + A] (Windowsの場合はCtrl + Shift + A) を実行して、「Registry」と入力し、[Registry...]を選択。
    f:id:tech_adlive:20190318175813p:plain

  2. 以下の2つのキーの値を変更して、[Close]をクリック。(アルファベット順にキーが並んでいるので目グレップしてください。)compiler.automake.allow.when.app.running: チェックを入れる
    compiler.automake.postpone.when.idle.less.than: 100 500 (2019-04-18修正)
    f:id:tech_adlive:20190318175950p:plain
    「compiler.automake.allow.when.app.running」はコードを保存すると自動でコンパイルが実行される設定です。チェックを入れることで、コード保存すると自動でコンパイルが実行され、自動再起動が走るようになります。

    「compiler.automake.postpone.when.idle.less.than」はコードを保存してから、コンパイルが実行されるまでの待ち時間の設定です。デフォルトは3秒で少し長いため、100500ミリ秒に変更することで、コンパイルをすぐ実行し、自動再起動が走るようにしています。

    ※「compiler.automake.postpone.when.idle.less.than」の値ですが、プロジェクト内のコードが増えてコンパイル時間が長くなってくると再起動時にコンパイルが間に合わずにエラーが発生する場合があります。プロジェクトのサイズやマシンスペックに合わせて適時変更してください。

    これらの設定はIntelliJ上のグローバルな設定になります。

静的ファイルが反映されるようにする

実は今までの設定では、HTMLテンプレートやJSファイルが自動反映されません。さきほどGradleのbootRunを再起動したのはそのためです。自動反映させるためには、Gradleからではなく@SpringBootApplicationがついているmainメソッドから直接Spring Bootを起動させます。これは1度だけ行えば、その後はGradleから起動しても自動反映されるようになります。(ここについては詳細は不明ですが、おそらくIntelliJ内でのファイルの監視が関係いるのではないかと。理由をご存知の方がいましたらコメントいただけるとありがたいです。)

また、Gradleから起動しているSpring Bootを再起動することにより、IntelliJのプロジェクト設定、Registry設定の反映も兼ねています。

以下の手順でおこないます。

  1. [Stop] を押して、Gradleから起動しているSpring Bootを停止します。

    f:id:tech_adlive:20190319150729p:plain

  2.  [DemoApplication.kt]を右クリックして[Run]を実行します。実行後Spring Bootが起動したら、1ど同じ[Stop]ボタンを停止します。
    f:id:tech_adlive:20190319104406p:plain

  3. 右のサイドバーのGradleのパネルから、[bootRun]タスクを実行して、Spring bootを起動します。(最初のGraldeからの起動操作と同じ)

Developer Toolsによる自動再起動機能を確認する

それでは、自動再起動機能が機能するかどうか確認してみましょう。

  1. 「src/main/kotlin/com/example/demo/DemoApplication.tk」を開いて、printlnで起動時にログを出力するように修正して、保存する。

    f:id:tech_adlive:20190318181335p:plain

    すると自動再起動が走り、コンソールにSpringのロゴが再度表示されログが表示あれるはずです。

    f:id:tech_adlive:20190318181356p:plain

    成功です!出力する文字列を変更するたびに自動再起動が実行されます。

LiveReloadを有効にする

自動再起動の設定は完了しましたが、これだと

  1.  コードを変更する
  2.  再起動を待つ
  3.  ブラウザをリロードする(早すぎるとサーバーが動いていないのでエラー画面になる)

と、まだ3ステップもあり、開発精神安定上よくありません。ここからが本番です。コントローラーとビューを追加して、LiveReloadの設定を追加し、

  1. コードを変更する -> 再起動とブラウザリロードが自動的に走り最新のアプリが画面に表示される。

という1ステップの状態にします。

  1. Chrome Web StoreからLiveReloadのChrome拡張をインストールします。
  2. LiveReloadをロードするscriptタグをコードをHTMLに追加して、ブラウザをリロードします。

src/main/resouces/templates/index.html

  ...
  <script src="/js/index.js"></script>
  <script>document.write('<script src="http://'
  + location.host.split(':')[0]
  + ':35729/livereload.js"></'
  + 'script>')</script>
</body>
</html>

LiveReloadは35729ポートで起動します。コードの変更があるとこのポートを経由して、ブラウザに変更通知を送り、LiveReloadがブラウザをリロードします。


index.jsのコンソールログ、index.htmlの変更、DemoApplication.ktなどKotlinのコード変更、いずれを実行しても画面が自動的に再読込されて、最新のコードが反映された画面が表示されます。

これでTomcatを再起動することも、ブラウザをリロードすることもなく、開発をし続けることができるようになりました!

ちなみにChrome拡張はLiveReloadに必須ではないですが、インストールすることでランタイムエラーなどで画面が表示されない状態のときも、エラーが解消した時点で、自動リロードされたり、より広い範囲で「自動的にリロード」されるようになります。

LiveReloadを本番環境で無効にする

LiveReloadをロードするscriptタグは本番では不要なので、本番環境では出力されないようにします。Developer Tools自体はbuild.gradleでruntimeOnlyになっているので、本番環境では同梱されません。なので「Developer Toolsのクラスが存在するか?」で開発時かどうかを判定し、scriptタグの出力をコントロールします。以下のようにDevToolsUtil.ktを追加し、index.htmlのscriptタグにth:if属性を追加します。これにより、本番環境ではscriptタグが出力されないようになります。
(細かい点ですが、LiveReload Chrome拡張は一度設定するとこのscriptタグがなくても自動で同様のスクリプトを挿入します。scriptが挿入されていないことは、Chrome拡張を切ってから確認してみてください。)

src/main/kotlin/com/example/demo/DevToolsUtil.kt

package com.example.demo

import org.springframework.util.ClassUtils

class DevToolsUtil {
    companion object {
        @JvmStatic fun isDeveloping(): Boolean {
            return ClassUtils.isPresent("org.springframework.boot.devtools.settings.DevToolsSettings",
                    ClassLoader.getSystemClassLoader())
        }
    }
}

src/main/resouces/templates/index.html

...
<script src="/js/index.js"></script>
<script
th:if="${T(com.example.demo.DevToolsUtil).isDeveloping()}">document.write('<script src="http://'
+ location.host.split(':')[0]
+ ':35729/livereload.js"></'
+ 'script>')</script>
</body>
</html>

DevToolsUtilのcompanion objectはJavaで言うところのstaticメソッドの定義、@JavaStaticはJavaからstatic関数を呼び出す際の命名規則Java風にするものです。これにより、ThymeleafのHTMLのEL式で、直感的にstaticメソッドisDevelopingが呼び出せるようになります。詳細は以下の記事を参照ください。

qiita.com

なんか動かないな?って思ったら

筆者の環境では、開発環境やブラウザを立ち上げたまま、Macをスリープさせて次の日にそのまま開くとLiveReloadやHTMLファイルの更新が反映されなくなることがありました。その他、動かない場合、以下を順番に実行すればどれかで解消するはずです。

  • ブラウザのスーパーリロード
  • ブラウザのキャッシュを削除
  • Javaのゾンビプロセスが残っていたら停止する

まとめ

Developer ToolsとLiveReloadを使って、気持ちよく効率的な開発をぜひ体験してみてください。GitHubにもコードは上げていますので、こちらも参考にしてみてください。

ADlive技術ブログ、はじめました。

2019年3月1日にADlive(アドリヴ)株式会社にジョインしたあがたです。

ADliveはWebマーケティング会社で、私は1人目の技術者兼CTOとしてADliveをマーケティングだけではなく、技術にも強いADliveと呼ばれるようにしたいと考えています。というわけで本日から、技術ブログをはじめます。これからいろいろと技術を中心に記事を書いていきたいと思います。よろしくおねがいします!

ADliveへの入社は以下のようにThe Bridgeさんに取り上げていただきました。取り上げていただいたThe Bridgeの池田さん、ありがとうございました!

thebridge.jp