astamuse Lab

astamuse Labとは、アスタミューゼのエンジニアとデザイナーのブログです。アスタミューゼの事業・サービスを支えている知識と舞台裏の今を発信しています。

おためしpuppeteer

お久しぶりでございます。

本来のアップ予定であった、水曜日をおとし今この時間にやっと投稿させていただいた、

季節外れのインフルエンザ(A型)にやられてしまった、scalaでバックエンドを開発しているaxtstar(@axtstart)です。

まだ出勤するのは、はばかられる状態なので、リモートワーク中です。

実は何気に(前職も通して)初めてのリモートワークだったりして、新鮮というか、なんというか、

ただ、slackで仕事のやり取りを普段からしているためか、それほどの差は感じないですね。

社会の進化を感じます。

さて今回なのですが、だいぶ前の回で、phantomJS の話を書いたかと思うのですが、

その関連というか、headless chromeをあやつる、nodeJSで書かれたpuppeteerというののちょっとした紹介です。 github.com

背景

現在プロジェクトで使用している自動化ツールは、PhantomJSなのですが、

諸事情で他の自動化ツールを探している状態です。

こちらはそのあたりの、味見的な記事になります。

また、NodeJSのバージョンの高いものを利用するため、Ecmascriptのバージョンも新しく、

モダンな開発手法が可能ではないかという期待もあって、試してみました。

PhantomJSは独自実装のため、NodeJSができることができるとはならず、かつESの新しい機能はトランスパイルの必要がありました

Install

npm install puppeteer

でchroniumを含めてインストールしてくれます。

※↑Mac OS X

ただし、私のUbuntuでの動作には追加で以下の内容が必要でした。

sudo apt-get install gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget

curl -O https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb

sudo dpkg -i google-chrome-stable_current_amd64.deb

サンプル(公式そのまま)

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');
  await page.screenshot({path: 'example.png'});

  await browser.close();
})();

こちらは、公式ページからコピペしたスクリプトです。 これを、nodejsで動かせば、example.comを開いて、スクリーンショットを保存します。

ソースをご覧いただいてわかる通り、async,awaitというES7からのpromiseの糖衣構文を利用してかなり簡潔に記載できるようになっています。

※公式にも記載がある通り、上記のサンプルはNodeJSのv7.6.0以降が必要です。

実際の挙動

puppeteerのlauch時に、headlessをfalseにできたり、

キーボードの文字を打つスピードを調整できたりします。

  const browser = await puppeteer.launch({
    headless: false,
    slowMo: 50 // slow down by 50ms
  });

ページの生成とエラーハンドリング

    const page = await browser.newPage();

    page.on('error', err=> {
      console.log('error happen at the page: ', err);
    });

    page.on('pageerror', pageerr=> {
      console.log('pageerror occurred: ', pageerr);
    })

pageの表示

    await page.goto('http://exsample.com/', {
      timeout: 30000 //default 30s = 30000ms
    }); 

タイピング

    await page.type('input#textBox','typing...');

Wait

    await page.waitFor(5000);

文字列の内容の確認

    let h1 = await page.$('h1');
    let valueHandle1 = await h1.getProperty('innerHTML');
    
    assert.ok(await valueHandle1.jsonValue, 'TEST');

クリック

    await page.click('button.submit');

まとめ

○インストールが非常に簡単でドキュメントも豊富

○トランスパイルの必要なく、ES7の構文が利用できる

○NodeJSの機能が使えるため、Web画面の自動化を超えたような処理に応用が可能な手応えがある

○chroniumを使用するため、メモリ使用量は大きい

PhantomJSで結構苦労していた部分がすんなりできました。 また、書き方に統一感があり、かなり使いやすい印象でした。 自動テストはメモリ使用量は若干多めなのですが、今後自動テスト環境でも使って行きたいなと感じさせるものがありました。

今回はこの辺で。。

最後に

アスタミューゼでは現在、エンジニア・デザイナーを募集中です。 興味のある方はぜひ下記バナーからご応募ください。

groonga冗長構成で運用改善した話

はじめまして。インフラ・開発部のtorigakiと申します。 入社して初めての投稿となります。よろしくお願いします。

弊社は検索エンジンとしてgroongaを使用しているのですが、今回はgroongaの運用改善をした話を書きたいと思います。

groongaの運用構成

groongaにはElasticSearchのように複数台でクラスタを構成する機能がないため、弊社では2台のgroongaサーバーを立ててマスター・スレーブの冗長構成をとっています。

しかしながら、MySQLのようにレプリケーション機能がないため、マスター・スレーブそれぞれに対してインサート処理を実施していました。 しかし、この方法ではマスター・スレーブ間でデータの整合性がとれなくなる可能性があります。

そこで、インサート後にマスターのデータをスレーブにリストアすることによりこの問題を解決させました。

以下に具体的な方法を記載してきます。

改善前のインサートとバックアップ取得の構成

改善前の構成は以下になります。

f:id:astamuse:20180327143656p:plain

  • マスターにデータをインサート後、スレーブにデータをインサート(インサートは週1回)。
  • バックアップの取得
  • マスターのgroongaプロセスを停止。(マスターを停止してもアプリからの接続はスレーブで賄うことができるので問題ありません)
  • gsutilコマンドでgroongaのデータファイルをアップロード。
  • アップロード完了後groongaプロセスを起動。

改善後の構成

f:id:astamuse:20180327143806p:plain

  • スレーブのデータはバックアップからリストアするためスレーブへの更新処理は停止。
  • GCSからスレーブにファイルコピー(リストア)する。
  • 各処理の処理工程をリアルタイムで確認するため、各処理の完了時にSlackに通知する。

リストア処理概要

  1. スレーブのgroongaプロセスを停止。
  2. 既存データファイルを削除(gsutil rsyncではファイル上書きされないため、データファイルは一旦削除します)。
  3. gsutilコマンドでデータファイルをダウンロード。
  4. ダウンロード完了後groongaプロセスを起動。

データチェック

  • スレーブのgroonga起動後にマスターとのデータ整合性をチェックします。
  • チェック方法は各テーブルのレコード数を取得しマスターとスレーブで一致するか確認をします。

全体の処理の流れ

  1. マスター機のデータ更新
  2. マスター機のgroongaプロセス停止
  3. マスター機のデータファイルをアップロード
  4. マスター機のgroongaプロセス起動
  5. スレーブ機のgroongaプロセス停止
  6. スレーブ機のデータ削除
  7. スレーブ機にデータファイルをダウンロード
  8. スレーブ機のgroongaプロセス起動
  9. マスターとスレーブのデータ整合性チェック

上記処理を順番に実行するバッチスクリプトを作成し、cronで定期実行するようにしました。

バッチスクリプトで工夫したこと

バッチスクリプトはシェルで書きました。 以下にスクリプトで工夫した点について記載していきます。

Slack通知スクリプト

Slackに通知するスクリプトはこちらのサイトを参考にさせていただきました。

エラー処理

エラー発生時にSlackにエラーコード込で通知されるように以下のように書きました。

# slack通知用スクリプト
SLACK_NOTI_PATH="bin/webhooks.sh"

# エラー処理
error() {
   echo ''$1'の実行結果はエラーコード('$2')でした。' | ${SLACK_NOTI_PATH}
   echo 'groongaバッチ処理: 異常終了' | ${SLACK_NOTI_PATH}
   exit 1
}

# backup
backup.sh || error 'backup.sh' $?

上記の場合、backup.sh でエラーが発生するとその旨をSlackに通知してexit 1 でスクリプトを停止させます。

gsutilのオプション

rsync に「-m」 オプションをつけることにより、並列に同期をさせるようにします。 サーバー負荷は上がりますが、ファイルのアップロード・ダウンロードが高速なります。 (ファイルのアップロード・ダウンロードじはgroongaプロセスを停止しているので、サーバー負荷が上がっても問題ありません)

gsutil -m rsync -r ${DIR} gs://${GCS_BACKET_NAME}/${HOSTNAME}/${DATE}/

データ整合性チェック

以下コマンドでマスター・スレーブ両方のgroonga各テーブルのレコード数を取得し、比較することによって同一データであるとことをチェックします。

 # テーブル一覧取得
for table1 in `curl -s http://${HOST}:${port}/d/table_list | jq -r -c '.[1][][1]' | sed -e '1,1d'`
do
      # カウント取得
      COUNT1=`curl -s  http://${HOST}:${port}/d/select.json?table=${table1}\&limit=0 | jq '.[1][][0][]'`
      echo ${port} ":" ${table} ":" ${COUNT} >> /tmp/${HOST}_count.txt
done

データはJSON形式で取得できるのでjqコマンドで加工し、取得した結果をファイル出力して最後にdiffコマンドで差分がないことを確認しています。

まとめ

今回の改善により、マスター・スレーブ間でデータの不整合が出ることを防ぐことができました。

また、スレーブへのデータ更新につきましても、リストア処理がインサート処理よりも高速なため改善することができました。

あと、各処理の完了時にSlackに通知するようにしたため、サーバーにログインして確認するという手間が省け、かつ処理異常時にも即座に気づくことができるようにもなりました。

弊社では引き続きエンジニア・デザイナーを募集中ですので、ご興味のある方は下からご応募いただければと思います。

Play Framework 2.6でFilterに触れてみる

f:id:astamuse:20170815181256p:plain

こんにちは、開発部のyanagita@リモートワーク中です。
宮崎はだんだん暖かくなり、もうすこしで桜が咲き出しそうな雰囲気が漂ってます。
桜といえば春、春といえば出会いの季節です。
アスタミューゼでは多くの方との出会いを心待ちにしています。

前回は弊社のWeb Framework asta4dのscala対応について書きましたが、今回は元に戻ってPlay Frameworkに関する事でFilterについて書きたいと思います。

前提

今回もPlay Framework 2.6のScala版で説明します。Javaの方はJavaに読み替えて頂ければとm(_ _)m

Play Frameworkの標準Filterと設定

Play Framework 2.6では前バージョンの2.5から新しいFilterが一つ追加になり、5つのFilter機能が標準で使用可能になっています。

  1. Gzip encoding filter
  2. Security headers filter
  3. CORS filter
  4. CSRF filter
  5. Allowed hosts filter
  6. Redirect HTTPS filter ← 2.6から追加

このうち、Security header filter、CSRF filter、AllowedHostFilterはデフォルトで適用されるFilterになります。
もし、残りのFilterも適用したい場合はapplication.confにFilterの追加定義を行います。

# conf/application.conf
# Gzip filterを追加する例
play.filters.enabled += "play.filters.gzip.GzipFilter"

逆にFilterを外す場合

# conf/application.conf
#CORS filterを削除する例
play.filters.disabled += "play.filters.csrf.CSRFFilter"

次に各Filterのについて触れます。

Gzip encoding filter

Gzip encoding filterは、レスポンスデータのgzip圧縮をサポートするFilterです。
詳細な設定はapplication.confで行います。

# conf/application.conf
play.filters.gzip.contentType {

    # Gzip圧縮を行なうContent-Typeを設定(デフォルト値:空)
    whiteList = [ "text/*", "application/javascript", "application/json" ]

    # Gzip圧縮を行わないContent-Typeを設定(デフォルト値:空)
    blackList = []
}

whiteList、blackListが共に空の場合、全レスポンスがgzip圧縮の対象となります。
whiteListのみ設定した場合、設定したContent-Typeのレスポンスのみがgzip圧縮の対象となります。
blackListのみ設定した場合、設定したContent-Type以外のレスポンスがgzip圧縮の対象となります。 whiteList、blackList共に設定した場合、whiteListが優先されblackListは適用されないのでご注意下さい。

Security headers filter

セキュリティに関するHeaderパラメータを自動設定します。
各セキュリティパラメータの設定は下記の通りです。

# conf/application.conf
play.filters.headers {

    # X-Frame-Options設定。nullの場合は指定なし(デフォルト値:"DENY")
    frameOptions = "DENY"

    # X-XSS-Protection設定。nullの場合は指定なし(デフォルト値:"1; mode=block")
    xssProtection = "1; mode=block"

    # X-Content-Type-Options設定。nullの場合は指定なし(デフォルト値:"nosniff")
    contentTypeOptions = "nosniff"

    # X-Permitted-Cross-Domain-Policies設定。nullの場合は指定なし(デフォルト値:"master-only")
    permittedCrossDomainPolicies = "master-only"

    # Content-Security-Policy設定。nullの場合は指定なし(デフォルト値:"default-src 'self")
    contentSecurityPolicy = "default-src 'self'"

    # Referrer-Policy設定。nullの場合は指定なし(デフォルト値:"origin-when-cross-origin, strict-origin-when-cross-origin")
    referrerPolicy = "origin-when-cross-origin, strict-origin-when-cross-origin"
}

CORS filter

CORS(Cross-Origin Resource Sharing)は、ドメインの異なるWebアプリケーション間のリクエスト制御設定を行います。
こちらもapplication.confで設定を行います。

# conf/application.conf
play.filters.cors {

    # CORSを適用するルートパス(デフォルト値:/)
    pathPrefixes = ["/"]

    # アクセス許可するドメイン。nullの場合は制限無し(デフォルト値:null)
    # 指定する場合は、["astamuse.com", ...]
    allowedOrigins = null

    # preflightリクエストで許可するメソッド。nullの場合は制限なし(デフォルト値:null)
    # 指定する場合は、["GET", "POST"]
    allowedHttpMethods = null

    # preflightリクエストで許可するHttpヘッダ。nullの場合は制限なし(デフォルト値:null)
    # 指定する場合は、["Accept"]
    allowedHttpHeaders = null

    # 許可する独自のHttpヘッダ。nullの場合は制限なし(デフォルト値:null)
    exposedHeaders = []

    # Credentials情報の使用の有無(デフォルト値:true)
    supportsCredentials = true

    # CORSメタデータのキャッシュ有効期間(デフォルト値:1 hour)
    # 設定値はduration
    preflightMaxAge = 1 hour

    # pathPrefixesに該当しないパスへのアクセス許可有無(デフォルト値:false)
    serveForbiddenOrigins = false
  }

CSRF filter

CSRF (cross-site request forgeries) は、 2.6からデフォルトFilterに追加されたので2.5以前から移行される場合は対応が必要になります。
CSRFのチェックはForm内にtoken情報を含む必要があります。一般的なtoken情報の設定方法は下記の2パータンがあります。

  • action属性のパスにtoken情報を付加するケース

template.scala.html

@import helper._

@form(CSRF(routes.SampleController.index())) {
    ...
}

HTML

<form method="POST" action="/index?csrfToken=1234567890abcdef">
   ...
</form>

 
* formタグ内にパラメータを付加するケース

template.scala.html

@import helper._

@form(routes.SampleController.index()) {
   @CSRF.formField
    ...
}

HTML

<form method="POST" action="/index">
  <input type="hidden" name="csrfToken" value="1234567890abcdef"/>
   ...
</form>

Allowed hosts filter

有効host以外からのアクセスを遮断するFilterです。遮断時はHTTP codeは400を返します。
有効hostの設定もapplication.confで行います。

# conf/application.conf
play.filters.hosts {
    # 有効なホスト名を設定(デフォルト値:localhost, .local)
    allowed = ["localhost", ".local"]
  }

Allowed hosts filterはデフォルト設定のFilterになります。confファイルを環境別に用意している場合に、有効hostの設定が漏れていると環境を変えた時にアクセスできない問題が発生するので注意が必要です。
開発中などホスト制限を行わない場合は、"."を設定することでホスト制限なくアクセスできます。
サブドメインも含めてた制限を行なう場合は、"." + ホスト名で行えます。
(".astamuse.com"と設定した場合、"astamuse.com"、"sub.astamuse.com"が該当します。)
ポートが変更されている場合は、ポート番号を含んで設定します。
(localhostで8080ポートの場合、"localhost:8080")

Redirect HTTPS filter

httpでのアクセスをhttpsにリダイレクトするFilterです。リダイレクト時のHTTP codeはデフォルトで308になります。
308であればリダイレクト時のリクエストメソッドが変更されないので予期しないリクエストケースはなくなりますが古いブラウザでは動かない欠点があります。
別のcodeへの変更は下記の通り設定します。

# conf/application.conf(デフォルト値:308)
play.filters.https.redirectStatusCode = 301

さいごに

至れり尽くせりのFilterが揃ってます。
Play Frameworkに触れた際にはFilterにも是非触ってみて下さい。

アスタミューゼではまだまだエンジニア&デザイナを募集しています。
気になる方は下からご応募下さい!新しい出会いをメンバー一同お待ちしてます!

Copyright © astamuse company, ltd. all rights reserved.