0

AWS LambdaでCircleCIを毎日ビルドする

定期的にCircleCIでのテストを実行するAWS Lambda Functionを書いた。

shokai/circleci-daily-build: kick CircleCI rebuild everyday

とくにソースコードの変更が無くても定期的にCIを実行しておきたい場合がある。
依存ライブラリを毎日全て最新版にアップデートしてテストするとか、スクレイピング対象のHTMLが変更されていたらすぐに気づきたい場合とか。


天気予報をスクレイピングで取ってくるライブラリを定期的に実行している様子。毎朝テストされて結果がSlackに通知される。




CircleCIにビルドを頼む


CircleCIにはAPIがあるので外部からビルドを要請できる。
https://circleci.com/account/api でAPIキーを取得して使う。

curlとかでやってもいいんだけど、circleciというnpmを使うと2,3行書くだけで指定したプロジェクトのテストを実行できる。


AWS LambdaからCircleCIにビルドを頼む


新横浜Twitter botをAWS Lambdaとcoで作ったに書いたneoyokohama-botをテンプレとして使った。
我ながらわりとよく出来ていてsrc/以下だけ入れ替えたら完成した。


これをgit cloneして
https://github.com/shokai/circleci-daily-build

設定ファイルを作って中身を書く
% cp sample.env .env
% cp sample.project-list.json project-list.json


ビルドする
% npm install
% npm run build
% npm test # ログイン情報が正しいかとか
% npm start # ローカルで動かしてみる


zipを作って
% npm run zip

lambdaにアップロードして、イギリス時間で1時(日本で10時)にテスト実行するようにしておいた。

0

crontabで日本語を使うのにLANGを設定し忘れてた

LANG=ja_JP.UTF-8
をcrontabの上の方に書いておけば良かった

書かないと、Rubyの場合ARGVに日本語を渡してcrontabから呼んだらencodingのエラーになる

incompatible character encodings: UTF-8 and ASCII-8BIT (Encoding::CompatibilityError)



crontabで定期的に時刻を読み上げる

これを動かそうとしていた
crontabで実行した時だけ引数に日本語渡せなくて困ってた

say-time
#!/usr/bin/env ruby
now = Time.now
s = now.min == 0 ? "#{now.hour}時" : "#{now.hour}時#{now.min}分"
cmd = "say #{s} #{ARGV.join ' '}"
puts cmd
system cmd

30分ごとに実行
SHELL=/usr/local/bin/zsh
HOME=/Users/sho
PATH=/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/$HOME/bin:$PATH
LANG=ja_JP.UTF-8

0,30 * * * * say-time です -o otoya > /dev/null 2>&1
時刻の末尾に付けたい文字列や、sayに渡したいオプションはそのまま渡せる。
「8時30分です」とか言ってくれる。

0

mochaのitとifを間違えるのでESLintのrule pluginを自作した

mochaでtestを書く時にitという関数を使うのだが、ifと書き間違えている事に気づかず30分ぐらいハマった。前にも同じ事があったのに30分かかったので、この調子だと何度でもハマる自信がある。


itがifでも文法エラーが起こらないし、そのテストコードの部分が実行されないだけなので気づかない。
シンタックスハイライトされてても文字の色で判別なんて人間には不可能。
間違え探しみたいなデバッグをしたくないからテスト書くのに、そのテスト自体が間違え探しなのはどうなのと思う。

もっと細かく考えると

  • if文に定数を渡せるのがおかしい
  • カンマ演算子の存在自体が悪い
  • itって関数名どうなの
  • そもそもtestの中でif文なんて必要ない

というのがある。


eslint-if-in-test作った

テストコードの中でif文を使う理由が思いつかないので、ESLintでなんとかする事にした。ESLintはプラグインを簡単に自作できるのですぐできた。

https://www.npmjs.com/package/eslint-plugin-if-in-test

describe()の中でif文を使っていたら警告してくれる


他のソリューション

eslint-if-in-test作っている時にESLint自体のテストがうまく書けなくてtwitterで愚痴っていたら色々教えてもらえた。

ESLintには既に色々プラグインがある
特にno-constant-conditionはif文に定数渡したら警告してくれるので良いなと思った。

というかこっちで十分だと思う。完成してから知った。


ESLintのruleを作る

ESLintのruleの作り方を調べたのでメモしておく。かなり簡単に作れる。

まずドキュメントを読んでいくとruleの作り方というのがあるんだけど、ESLintのソースコードの中のlibの下にファイル作ってねとか言われる。
pluginの作り方の方を見たほうが良さそう。


ESLintのpluginを作る

pluginの中にruleをまとめて、npmとして公開できる。
yeomanにテンプレートがある。

% mkdir eslint-plugin-if-in-test
% cd eslint-plugin-if-in-test/
% npm i yo generator-eslint -g
% yo eslint:plugin
(質問に答える)
% npm i

テンプレができる

最終的にこうなった。 lib/rules/if.js と tests/lib/rules/test_if.js を自分で書いた。
├── README.md
├── lib
│   ├── index.js
│   └── rules
│   └── if.js
├── package.json
└── tests
└── lib
└── rules
└── test_if.js

罠もあった。
作られたテンプレのpackage.jsonで指定されているESLintが1.2.0と古い物なので最新(1.10)に直した。

あとlib/index.jsのrequireindex npmの呼び出し方が間違ってて動かないので自分でrequireした

この辺は後でプルリクしたい。


pluginにRuleを書く

ESLintはJavaScriptを解析してAST(抽象構文木)を作って、それを解析して警告などを出す。
ASTのnodeのtypeをruleで指定しておくとそれを受け取れるので、適当に解析してエラーがあったらreportを返せばいい。

ASTはAST explorerでもみれる。


とりあえず対象ファイルがtestディレクトリの中にあるかをcontextで確認して、if文があったらcontext.reportするようにした。
lib/rules/if.js
var path = require("path");
var chalk = require("chalk");

module.exports = function(context){

// テストコードのディレクトリ内だけ判定
var testDir = path.resolve(context.options[0].directory);
var testDirPattern = new RegExp("^" + testDir + "/");
if(!testDirPattern.test(context.getFilename())) return {};

// "if" statement in test-code
return {
"IfStatement": function(node){
var code = context.getSourceCode().getText(node).slice(0, 15) + "~~~";
context.report({
node: node,
message: code.replace(/^if/, chalk.bold.underline('if'))
+ " is probably typo of "
+ code.replace(/^if/, chalk.bold.underline('it'))
});
}
}
};

// .eslintのバリデータ JSON-Schema
module.exports.schema = [
{
type: "object",
properties: {
directory: { // directoryプロパティがstring型で必須
type: "string"
}
},
additionalProperties: false
}
];

ESLintが出すnodeの種類はestree/spec.mdにリストがある。

実装サイズの小さそうなpluginをnpmjs.comで探して参考にもした。
lodashをrequireする時にlodash全体を読み込んでいたら警告するのとか
https://github.com/eslint-plugins/eslint-plugin-lodash/blob/master/src/rules/import.js

console.logを使っていたら警告とか
https://github.com/joeybaker/eslint-plugin-no-console-log/blob/master/lib/rules/no-console-log.js


pluginを動かす

作ったpluginが動くかどうかは、mochaでテスト書いてる適当なアプリの中にインストールすればいい。
開発中のnpmを読み込む場合はnode_modules/内にシンボリックリンク貼るといい。
% cd node_modules/
% ln -s ../../eslint-if-in-test eslint-if-in-test
たぶんnpm linkを使っても良さそうだけどやったことない。


.eslintrc に設定する。module.exports.schemaで指定したオプションで、test/をdirectoryとして渡す。
{
"plugins":[
"if-in-test"
],
"rules":{
"if-in-test/if": [1, {"directory": "test"}]
}
}

if-in-testのルールを読み込んでlintできる。
% eslint test/*.js



describeの中のif文のみ警告する


もっとまともに書く。itはdescribeの中でしか使わない。

ESLintのルールを自作しよう! – Yahoo! JAPAN Tech Blog
を読んでいたら、node typeの末尾に:exitを付けたらtree walkして出る時に拾える事を知った。

nodeへの出入りをカウントする事でとても簡単にifがdescribeの中にあるかどうか判定できた。
lib/rules/if.js
var path = require("path");
var chalk = require("chalk");

module.exports = function(context){

// テストコードのディレクトリ内だけ判定
var testDir = path.resolve(context.options[0].directory);
var testDirPattern = new RegExp("^" + testDir + "/");
if(!testDirPattern.test(context.getFilename())) return {};

var describeCallCount = 0; // describe呼び出しの中にいるかカウントする
return {
"CallExpression": function(node){ // 何かの関数呼び出しに入った
if(node.callee.name === "describe") describeCallCount += 1;
},
"CallExpression:exit": function(node){ // 出た
if(node.callee.name === "describe") describeCallCount -= 1;
},
"IfStatement": function(node){
if(describeCallCount < 1) return;

// describe()の中の時
var code = context.getSourceCode().getText(node).slice(0, 15) + "~~~";
context.report({
node: node,
message: code.replace(/^if/, chalk.bold.underline('if'))
+ " is probably typo of "
+ code.replace(/^if/, chalk.bold.underline('it'))
});
}
}
};

// validator for .eslint
module.exports.schema = [
{
type: "object",
properties: {
directory: {
type: "string"
}
},
additionalProperties: false
}
];


最初はnode.parentを辿って確認してたんだけど、カウントする方が楽だったのでやめた。


ESLint Ruleのtestを書く


ルールのテストも書ける。

http://eslint.org/docs/developer-guide/working-with-plugins#testing
http://eslint.org/docs/developer-guide/working-with-rules#rule-unit-tests

ESLint組み込みのRuleTesterにvalidとinvalidを渡す
tests/lib/rules/test_if.js
var path = require("path");

var rule = require("../../../lib/rules/if")
var RuleTester = require("eslint").RuleTester;

var ruleTester = new RuleTester();
ruleTester.run("rule \"if\"", rule, {
valid: [
{
code: "if(true){ }",
options: [{directory: "test"}],
filename: path.resolve("test/foo/bar/baz.js")
},
],
invalid: [
{
code: "describe('foo', function(){ if('should ~~', function(){ }); }); // \"if\" in describe",
options: [{directory: "test"}],
errors: [{message: null, type: "IfStatement"}],
filename: path.resolve("test/foo/bar/baz.js")
}
]
});

mochaでテストできる
% mocha tests/lib/rules/test_*.js


Testerにfilenameを渡せる事を教えてもらえてテストが実現できた。

0

新横浜Twitter botをAWS Lambdaとcoで作った

新横浜は新幹線が止まるしイベント施設が複数あるので、土日はなんらかの目的をもって訪れる人が多いみたいでニコニコ笑顔の人が多くて良い。そのかわりイベントの入場前・退場後の時間帯は強烈に混雑して駅に入れなくなる事がまれによくある。
そこでtwitter botにイベント情報を喋らせることにした。毎朝ツイートするので危険を察知できる。
そのうち混雑度を算出する機能も追加したい。あと開場時間も。


ソースコード
https://github.com/shokai/neoyokohama-bot

coを使うと、非同期処理を同期的に書いて順番に実行したり、あるいはasync.jsみたいに同時実行して完了を待ち受けて合流させたりがコールバック地獄を起こさず簡単に書けるので、こういうバッチ処理的な順序が大事な処理は書きやすいだろうなと思って色々試していたらいつの間にかtwitter botになってた。

まず横浜アリーナと日産スタジアムの予定を同時に取得し、Tweetする。Tweetしつつ同時に天気予報を取得する。先のTweetのstatus_idをin_reply_to_status_idにセットしつつ天気予報をTweetする事で連続tweetになる。この間約3秒である。

スクリプトはAWS Lambdaで毎朝1回実行している。AWS Lambdaはメモリ使用量×処理時間で課金されるが、毎日約48MB×3秒で月に4.3GBなので、無料枠の400000GBに十分収まっている。同じようなbotをあと10万個ぐらい無料でデプロイできる。

Lambdaについてぐぐると、最近はPython使ってる記事が多いというかほぼJavaScriptで書いてる記事が無いような?状況だけど、たぶんコールバック地獄がつらいからLambdaに向いてないとか思われてそう。
coでだいたい解決するし、それどころか複数のIO待ちする処理を同時実行する等も書きやすいからJavaScriptいいと思う。

以下、coやLambdaについて忘れないようにメモしておく

async/awaitで非同期処理を同期的に書く

まず最初に、12月半ばごろ、esnext – Async Functions – Qiitaを見て、非同期処理を同期的に書けるasync/awaitという文法が作られているのだなあと思って調べていた
function foo(wait = 1000){
return new Promise((resolve) => {
console.log(`wait ${wait} msec`);
setTimeout( () => {
resolve(`done waiting ${wait} msec`);
}, wait);
});
}

(async function(){
const result = await foo(1000); // コールバックじゃなくて同期的に呼び出せる
console.log(result);
})();
babelでトランスパイルするとGeneratorを使ってwhileで回して待ち続ける感じになっているみたいだけどよくわからなかった。regenerator-runtimeが使われている。


coで非同期処理を同期的に書く

そういえばGeneratorで同期的に書けるやつっていうと、coを使ったことなかったな、と思いだして触りだした。
coもまたPromiseを返す関数を同期的に扱える。
import co from "co"

co(function *(){
const result = yield foo(1000); // 上と同じ関数fooを同期的に呼び出す
console.log(result);
});
async/awaitと似たような感じになる。


coの方はもっと高機能で、Generatorを包んだco自体がPromiseを返すのでさらに別のcoに食わせたりできる。coを使って書いた関数をライブラリとして提供して、それをmainのcoから呼び出して・・・というのを何重もできる。
co.wrap(generator)すると即時実行せずに関数として定義できるのでcallにthisを渡せばES6のclass構文にうまくハマる。

src/yokohama-arena.es6
// 横浜アリーナのイベント情報class
class YokohamaArena{

constructor(){
this.url = "http://www.yokohama-arena.co.jp/event/";
}

getEvents(){ // 全イベント取得
return co.wrap(function *(){
const html = yield this.getHtml(); // 非同期でhtmlを取得
const events = this.parseHtml(html); // parseは同期処理
return events;
}).call(this);
}

getMajorEvents(){ // 設営日を除いたイベント取得
return co.wrap(function *(){
const events = yield this.getEvents();
return events.filter((i) => { return !(/設営日/.test(i.title)) });
}).call(this);
}

getHtml(){
return new Promise((resolve, reject) => {
superagent
.get(this.url)
// (略)


coで複数の非同期処理を同時実行する

上のようにイベント施設ごとのスクレイピングモジュールをそれぞれ作っておいて、coで同時に呼び出す。
yieldにオブジェクトを渡すと全部同時に評価して、完了したら結果をまとめて返してくれる。下の例だとevents.arenaで横浜アリーナのイベント情報が、events.nissanで日産スタジアムの情報が得られる。

src/main.es6
  co(function *(){
const events = yield {
arena: arena.getMajorEvents(),
nissan: nissan.getMajorEvents()
};
});


同時実行はyieldに配列を渡してもよい。
ES6の分割代入を使ったら、tweetを投稿しつつ同時に天気予報を取得するのが綺麗に書けた。
    const [tweet, forecast] = yield [
twitterClient.update({status: tweetText}),
weather.getForecast()
];


coのエラー処理

co自体がPromiseを返すので、まとめてcatchできる。
co(function *(){
/** なんかエラーが起こりうる処理 **/
}).catch((err) => {
console.error(err.stack || err); // スタックトレースがあればprintする。無ければエラーそのものを
});

Node.jsでAWS Lambdaを書く

AWS Lambdaはアプリケーションのプロセスではなくlambda、つまり関数単位でホスティングしてくれるサービス。AWSにあるDBやqueueなどのサービスにデータが書き込まれたらそれに反応してLambda Functionが起動して小さな仕事をしてすぐ終了するような感じで使われる。functionの起動さえ十分に速ければ、確かにずっとプロセスが起きていてメモリを専有しているような普通の実装よりも効率がいい。

ためしにfunctionを作ってみるとわかりやすい。新規作成するとblueprint(ある程度できてるサンプルコード)から選択させられるので、node-execを選ぶと一番シンプルでいい。
child_processでshellコマンドをexecするサンプルになっている。

eventはevent.cmdを参照してchild_process.execするのに使われているが、これはActionsのドロップダウンメニューからJSON形式で編集できる。
contextにはawsRequestIdやinvokeidが入っていて、これでAWS上のどういう環境で実行されているかが見れる。ローカルマシンで実行しているかとか判別するのにも使える。

Configurationタブを見ると、index.jsというファイル名とhandlerという関数名が指定されている。
基本形はmodule.exports.handlerを宣言したindex.jsというファイル1つで、これがAWS上でrequireされて呼び出される。
module.exports.handler = function(event, context){
/** なんか処理 **/
context.done(null, "完了");
}
context.doneかcontext.failを呼ばないと設定したtimeout時間まで延々動き続けて課金されるので注意する。

co自身から出るPromiseでエラーをcatchした時も、context.failさせる。


AWS Lambdaでnpmを使う

ファイル単一でformに貼り付けて実行するのではなく、zipで固めてアップロードでもいい。
node_modules/ 以下も一緒にzipすればいい。最大50MBまでいける。

Using Packages and Native nodejs Modules in AWS Lambda | AWS Compute Blog
Lambda上でネイティブのOpenCV拡張を呼ぶために、EC2インスタンス作ってOpenCVをビルドして静的リンクして全部zipで固めている例もある。

このへんはserverlessを使えば全部CLIでできるみたいだけど、他のAWSの機能を使う予定が無いし生のAWS Lambdaを触ってみたかったので今回はzipでアップロードでやる事にした。


AWS LambdaでES6を使う

AWS Lambdaのnodeはv0.10.36なので、ちょっと古い。
ES6(ES2015)を実行するにはbabelで事前にコンパイルしておく必要がある。babel-registerで逐次実行しようとしたらsyntax errorになった。もちろんローカルのnode 0.10では動いているんだけどどうして動かないのかわからない。require-hookがLambdaでは効かないようになっているのかもしれない。

事前にsrc/以下をbabelでES5に変換してからzipで固めた。
% babel src/ --out-dir dist/ --source-maps inline
% zip -r bundle.zip index.js .env dist/ node_modules/
node_modules/ の中に実行に必要ないモジュールもたくさん混じってしまっているが、面倒なので全部まとめてbundle.zipに固めてしまった。npmが436個もあるけど11MB程度におさまったのでまあいいや。

ローカルで実験する

このtwitter botはAWSの機能は何も使っていないので、ローカルでも動く。

AWS Lambdaで実行された時はindex.jsがrequireされてそこからexportされてるhandlerメソッドが呼ばれる。
ローカルでも同じような動作をするrun.jsを用意して、そこから実行した。
run.js
var index = require("./index");
index.handler();

テスト

CircleCIでやってる
https://circleci.com/gh/shokai/neoyokohama-bot
ESLintかけてからmochaでクローラ等のモジュール毎のテストもやって、
一応buildしてzip作ってdry runまで
test:
override:
- npm run test
- npm run build
- npm run zip
- DRY=true npm start

AWS Lambdaに環境変数を渡す

twitterへの投稿にはtwitter npmを使った。
OAuthのaccess tokenが必要だが、こういうのはgitリポジトリにコミットしたくないので環境変数で渡したい。

でもAWS Lambdaには環境変数を渡す機能がないので、dotenv npmを使った。dotenvは.envファイルに書いた環境変数をloadできるので、.envファイルをzipに含めるようにした。
serverlessもdotenv使ってる。

require("dotenv").load({silent: true});
import Twitter from "twitter";

const client = new Twitter({
consumer_key: process.env.CONSUMER_KEY,
consumer_secret: process.env.CONSUMER_SECRET,
access_token_key: process.env.ACCESS_TOKEN_KEY,
access_token_secret: process.env.ACCESS_TOKEN_SECRET
});
なおdotenvはloadしたファイルの中でしかprocess.envが上書きされないっぽい。プログラムの頭でやっても効果ない。


AWS Lambdaを定期的に実行する

ここまででだいたいスクリプトができたので、あとは毎朝実行するように設定する。
event sourceにscheduled eventを追加してcrontabのような記法で書く。
タイムゾーンがUTCなので、イギリスだから日本から9時間時差がある。毎日23時(日本の8時)に実行するようにした。

scheduled eventは英語版のドキュメントにのみ説明されている。日本語ドキュメントだと項目丸ごと無いのでしばらく困惑した。
Using AWS Lambda with Scheduled Events – AWS Lambda

crontabっぽいけど末尾にyearがある。

cron(Minutes Hours Day-of-month Month Day-of-week Year)



タイムゾーンを日本に設定する

スクレイピングしてきたイベントのリストから今日開催されるものを判定して抜き出す必要があるので、タイムゾーンを日本時間に設定したほうが計算しやすい。
Herokuでは環境変数TZを上書きしてやってたけどAWS Lambdaには環境変数を設定する機能が無い・・・ので、index.jsの冒頭で設定してしまう事にした。
process.env.TZ = "Asia/Tokyo";
これ以降はDateがちゃんと比較できる
Date.prototype.isToday = function(){
const today = new Date();
return this.getYear() === today.getYear() &&
this.getMonth() === today.getMonth() &&
this.getDate() === today.getDate();
};


やっぱserverless使おうかな

気温もtweetさせたいんだけど、前日との差をtweetしたほうがいいのでSimpleDBか何かに書き込みたい。なのでやっぱりserverless使ったほうが良さそうな気がしている。

あと、in_reply_to_status_idを使って連続tweetしてるんだけど処理が速すぎてtwitterに認識してもらえない。
間に数秒ディレイを入れたら認識されたけど、それではlambdaっぽくないので2つのlambda functionに分けてscheudled eventも1分ずらしてDBで最後のstatus_idを受け渡すかー、それならなおのことserverless使ったほうが良さそうだなとか思った。イベントソースが複数あってそれをGUIでポチポチやっていくのは混乱しそうなので全部コードでやりたい。

混雑度予想と開場時間もほしい。

0

フライパンでローストビーフを作った

ローストビーフとグレービーソースを作った。何度作ってもおいしいのでレシピを書いておく。

料理としては簡単なのだが、牛モモ肉が手に入りづらい。(今はクリスマスだからわりと手に入りやすい)
自家製ローストビーフが流行ったら近所のスーパーにも常時置かれるようになる気がするので流行ってほしい。


感想

人間が実際に手を動かす時間が短いので楽でよい。適当に焼いて放っておけば肉はできる。トンカツ等の方が難しいぐらいのレベル。

アルミホイルで包んで余熱で調理したり、肉を事前に室温に戻しておく必要があるので、そこだけ時間がかかる。
ソースが重要。グレービーソースでぐぐるとみんな違う作り方していたので気にせず適当に作ったら最高にうまくできた。


材料

牛モモ肉の塊
オリーブオイル
クレイジーソルト

キッチンペーパー
アルミホイル

ソース(グレービーソースのような物)

肉を焼いた後の油
玉ねぎ 1/2個 みじん切り
セロリ 1/2本 葉も含めてみじん切り
バター
赤ワイン
はちみつ スプーン3,4杯ぐらい?

手順

肉の準備

冷蔵庫から出して常温に戻しておく。芯まで冷えていると余熱で調理できない。
肉をキッチンペーパーで拭く
塩とクレイジーソルトを全面に塗りこむ

焼く

フライパンにオリーブオイルを5mmぐらいしいて加熱しておく
肉の全面(6面)を3分ぐらいずつ焼く
強火でいい
わりと焦げていい


放置

アルミホイル(2重)で包んで保温し、1時間ほど置いておく。タオルでもかけておくと保温しやすい。
肉完成


ソースを作る

肉を焼いた後の油を加熱する。アルミホイルに溜まった肉汁も混ぜる
セロリ、玉ねぎを入れて軽く炒める
バター、赤ワイン、水を加えて煮つめる(見た目で焦げがわかりにくいので水の量に気をつける)
セロリが固いと嫌なのでしっかり煮るべき、水を足して2回煮詰めた。
はちみつを入れる
ソース完成、冷蔵しておく

食べる

ソースだけ電子レンジで温める
冷蔵したままだと油が固まっている
甘い味は温かいほうがおいしい
肉は暖めなくてもいいが少し温める派