LaravelのバッチをDiddagで運用したら少し幸せになった話
どうも、くずきです。
弊社の本番環境のバッチはスピード優先で作ったのもあって、処理ごとに分けてはいるものの連携を取ろうとすると以下のことをしなきゃいけない。
- 手動で1つ目のバッチを回す
- 1つ目が終わったら次のを回す
- それが終わったら次を回す・・・
コマンド一つ叩くだけなので、大きな作業は発生しないにしろバッチとバッチの間の無駄な時間やどこまでバッチが終わって、どれくらいかかって、こけた場合はどこでこけたのか・・など色々問題があった。
そこで、Laravel
のタスクスケジュール使う手もあったんだけどフローの可視化やリトライ処理を簡単にやりたいと思っていたので、前から良さげだなとおもっていたDigdag
を導入してみました。
Digdagとは
あらゆる手動作業を自動化しよう!!というコンセプトで作られたワークフローエンジンです。
細かい説明は、作成者でもある古橋さんの資料を参考にしましょう。。。
www.slideshare.net
とりあえずいいところとしては、
- ymlっぽいやつで簡単にかける
- 高可用性な構成にできる
- 分散処理できる
- テスト段階ながら、UI(可視化など)が提供されている
かなと思う。
既存バッチの把握
まずは、既存バッチを把握する。
Step1 ~ Step4までバッチが分かれており、簡単な図を混ぜながら説明。
- Step1: 処理したいデータをキューに入れる
- sshでログインしてバッチを叩く
- 3時間前後で終わる
- Step2: キューに入ってるデータを処理する
- バックグラウンドで常に動いてるため、特にすることはない
- 外部のデータに依存するので、長いと数日かかる
Digdagへの移行
Digdag
のインストールについては今回は省く。
ここに書いてある通りやればすぐ使えると思う。
Getting started — Digdag 0.9.5 documentation
一応載せとく。
$ curl -o ~/bin/digdag --create-dirs -L "https://dl.digdag.io/digdag-latest" $ chmod +x ~/bin/digdag $ echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc $ source ~/.bashrc
ちなみに、batch server 1
にインストールしてある。
次にPHP
の起動については特に用意はされていないので>sh
コマンドを使う。
sh>: Shell scripts — Digdag 0.9.5 documentation
このままだとStep2
の処理はバックグラウンドで動いてるため、Step3
へのフックがない。
そこで、Step2
が終わっているかの処理(キューにデータがあるかなど)を行うバッチを追加で書いた。
Step2_task_check
public function handle() { $this->line(Carbon::now() . ': start step2_task_check'); for(;;) { if (キューの中身があるか) { // 0を返す return config('command.exit_code.SUCCESS'); } sleep(60); } $this->line(Carbon::now() . ': end step2_task_check'); // 1を返す return config('command.exit_code.ERROR'); }
0 or 1
を返すようにすれば、Digdag
側で成功か否かを判断してくれる。
ここまで準備したらあとはyml
っぽいやつをかけば終わり。
BatchFlow
timezone: UTC +setup: echo>: start ${session_time} +disp_current_date: echo>: ${moment(session_time).utc().format('YYYY-MM-DD HH:mm:ss Z')} +step1: sh>: php /var/www/app/artisan app:step1 +step2: sh>: php /var/www/app/artisan app:step2_check +step3: sh>: php /var/www/app/artisan app:step3 +step4: sh>: php /var/www/app/artisan app:step4 +teardown: echo>: finish ${session_time}
ほぼほぼチュートリアルのままで完成。
特にエラー処理とかDigdag
の売りなことは特にしてないんだけど、まずは少し幸せになるという目標だったので自分的にはこれだけでもかなり楽になったし、細かくバッチを作れば組み合わせも楽なので嬉しい。
Digdagに処理をpushする
batch server 1
にDigdag Server
は起動してある。
digdag push batch_flow
こんな風にpush
する。
Digdag UI で起動&確認
Digdag UI
はDigdag
を起動したら勝手に立ち上がっている。
ただ、外向けのポート開けてないっぽいため65432
にssh
のトンネル作って確認する。
ssh -L 8000:localhost:65432 ec2-user@ip -i "~/.ssh/xxx.pem"
その後、push
された処理があるため、それを起動すれば完了。
終わりに
Digdag
の色々良いところは使いきれてないけど、「手動を自動化する」というミッションに関してはすごく簡単にできたので良いと思います。
次くらいまでには、障害に強い構成にしたり、バッチのリラン、Slack
への通知などやる予定。
tmux + ssh + peco + direnvで複数のAWSのサーバーへログインするやり方
どうも、くずきです。
今回は自分がいつも使っているスクリプトについて紹介したいと思います。
(色々な方のスクリプトをパクってくっつけてるだけですw)
tmuxの導入
tmux
とは端末を多重化するツール(ソフト)。
これだけでも色々かけるんだけど、色々な人がやってくれてるからグーグルで最上位にきてた記事だけ貼っとく。
tmux + sshで複数ホストへの同時接続
tmux
ではサーバーの複数のホストへ同時に接続できる。
完成形録画したかったのだが、、ちょっとうまく撮れなかったので参考サイトでイメージつかんで欲しい。
また、この記事にあるtssh
というtmux
で簡単に同時接続できるスクリプトがあるのでそちらを使う。記事からスクリプトのリンクが切れてるため一応貼っとく。
(ブログ兼スクリプトの作者様ありがとうございます・・・)
tssh
コマンドとして使いたいため、/usr/local/bin/tssh
に中身をコピーしておく。
ただ、今回の使いかだと少し改造する必要があるため、一部変更させてもらう。
# cmd="ssh $SSH_OPTION $host" cmd="ssh $SSH_OPTION $USER@$host" # $USERを追加
pecoの導入
peco
とはコンソールから標準入力を検索し、その検索結果を標準出力として返すことができる検索ツールみたいなもんである。
ここらへんもたくさん導入方法あるから頑張って入れよう。
AWS CLIの導入
AWS コマンドラインインターフェイス(CLI - AWS サービスの制御・管理)|AWS
コンソールからAWS
にログインするためには、公式が用意しているCLI
を入れる。
また、AWS
からのJSON
レスポンスを扱いやすくするために、jq
と言われるJSON
の扱いに長けたツールもいれとく。
https://stedolan.github.io/jq/
tssh + peco + aws cli(+jq)を使ったログイン
/usr/local/bin/flogin
に以下のコードをコピペしよう。
ちなみになんでflogin
かというと、ファイナルログインの略だ。
iplist=$(aws ec2 describe-instances --filters "Name=instance-state-name,Values=running" "Name=tag:Name,Values=*" | jq -r '.Reservations[].Instances[] | {InstanceName: (.Tags[] | select(.Key=="Name").Value), PublicIpAddress} | "\(.InstanceName) \(.PublicIpAddress)"' | sort | peco | cut -d " " -f 2) tssh $iplist
何をしているかというと、AWS CLI
でrunning
中のEC2
情報をとってきて、jq
でホスト名 + IP に整形している。
その後peco
に渡すことで検索(複数選択)を可能とし、そこから標準出力でIP
のリストをスペース区切りで出力し、tssh
に渡している。
これだけでも同時ログインができて便利だが
# aws cliで使う export AWS_ACCESS_KEY_ID=access_id export AWS_SECRET_ACCESS_KEY=secret_key # tsshでのsshログイン時に使う export SSH_OPTION="-i ~/.ssh/xxx.pem" export USER=ec2-user
環境ごとにこれらを設定しなくてはいけない。
direnvで環境ごとにexport
direnv
はディレクト単位で変数を設定できる。マジ便利。
ディレクトリの中で
direnv edit .
で環境ごとにexport
設定をしてあげればよい。
自分の使い方は/会社/サービス名/環境(本番など)
で区切っている。
設定が終わったら、あとは入りたい環境にcd
して、flogin
を実行すればいい。
終わりに
だいぶインストールはしょったけど、個人や自分の会社、他社さんのサービスを同時に管理している時はほんと助かってる。
改善点としては、AWS
のログイン時に指定したサービスしかそもそもでないようにしたいなぁと。そしたらpeco
もいらなくなるか・・・。(それはそれで使いづらいかもだけど・・)
Reactでテーブルをソートするやり方
どうも、くずきです。
React
でテーブルをソートしようとした際に、View
自体のソートはあるんですが
サーバーと連動したやつがなかったので実装してみました。
やりたいこと
上記の画像はすでに完成品だけど、
∧
と∨
の部分のようなソートボタンを作る- ソート中のカラムはどれなのか色をつける
- ソートが降順か昇順かわかるようにする
- ソートした際URLが変わるようにする。
が、できれば問題ない。
ソートした場合にデータ取りに行く必要はあるんだけど、以前紹介したURL
が変更された際にリソースと再取得するという処理を親のコンポーネントに仕込んでいるので、URL
さえ変更すればいい。
コンポーネントの準備
まずは、テーブル用のコンポーネントの準備。
色々省略して、render
部分あたりのみ。
render() { return( <table> <thead> <tr> <th className="prof-image"></th> <th className="name"> <p className="text-left"> <Link to={TableUtil.getSortUrl("name")} //このカラムがソートされるURL className={TableUtil.getSortClass("name")} //このカラムがソートされている場合のクラス > <span>名前</span> </Link> </p> </th> </tr> </thead> <tbody> {tableContent} // ソートされるごとに変わるtbodyの中身 </tbody> </table> ); }
重要なのは、Link
部分が
- 自身がソートされるURL
を持つ
- 自身がソートされてる場合デザインを変更するためのクラスの付与
を行う設計する(よくありがちなやつ)。
クエリパラーメーターを連想配列にする
上記のことを行うために、クエリパラメーターを確認しどのカラムがソートされていて、昇順/降順なのか確認する必要がある。
そのために、まずはクエリパラーメーターを扱いやすいように連想配列にする。
Query.js
export function getQueryParam() { let vars = {}, max = 0, hash = "", array = ""; let url = window.location.search; hash = url.slice(1).split("&"); max = hash.length; for (let i = 0; i < max; i++) { array = hash[i].split("="); if (array[1] !== undefined) { vars[array[0]] = decodeURI(array[1]); } } return vars; }
書き方は突っ込まずに、とりあえず格納した連想配列を返している。
自身がソートされるURLを持つ
ソートされるカラムをsc
、昇順降順をsd
とする。
Table.js
export function getSortUrl(sortColumn) { let sortUrl = `${window.location.pathname}?`; let query = Query.getQueryParam(); // クエリパラメーターにscがなかった場合、受け取ったカラムでパラメーターを作る(デフォルト降順[desc]) if (!query["sc"]) { sortUrl += `sc=${sortColumn}&`; sortUrl += `sd=desc&` } for(let key in query) { // pagenationは除外 if (key === "page") { continue } // scが存在する且つ受け取ったカラムではない場合、ソートするカラムを上書き if(key === "sc" && query["sc"] !== sortColumn) { sortUrl += `${key}=${sortColumn}&`; continue; } // sdが存在する且つscが受け取ったカラムではない場合、降順にする if(key === "sd" && query["sc"] !== sortColumn) { sortUrl += `${key}=desc&`; continue; } // sdが存在する且つscが受け取ったカラムの場合、ソートの順番を逆にする if(key === "sd" && query["sc"] === sortColumn) { let sdCol = (query["sd"] === "desc")? "asc" : "desc"; sortUrl += `${key}=${sdCol}&`; continue; } // その他色々付いているクエリパラーメーターはそのままURLに含める sortUrl += `${key}=${query[key]}&`; } return sortUrl.slice(0, -1); }
だいぶゴリゴリゴリラな書き方だけど、最適化はそのうち・・・。
自身のカラムと比較してなかったらソートするカラムを追加、ある場合はソートを逆にするなど行なっている。
ソートのデザインを変更するためのクラスの付与
自身がソート対象だった場合、active
を付与してデザインを変更する。
Table.js
export function getSortClass(sortColumn) { let className = "sort"; let query = Query.getQueryParam(); if (query["sc"] === sortColumn) { className = `${className} active ${query["sd"]}`; } return className; }
CSS部分
Saas
だけと貼っておく。
.sort { position: relative; display: inline-block; padding-right: 14px; &:before { position: absolute; top: 20%; right: 0; width: 5px; height: 5px; border-right: 1px solid gray; border-bottom: 1px solid gray; content: ''; -webkit-transform: rotate(45deg); -ms-transform: rotate(45deg); transform: rotate(45deg); } &.active { &:before { border-color: red } } &.asc { &:before { top: 5px; -webkit-transform: rotate(225deg); -ms-transform: rotate(225deg); transform: rotate(225deg); } } }
終わりに
ゴリゴリゴリラだからもうちょっとクールに直したい。
今年はRedash
のあどべんとかれんだーやることなりました。
HighCharts(React)でグラフをHoverした際に画像を表示する
どうも、くずきです。
以前HighCharts
と呼ばれる、JS
でのグラフ系ライブラリでかなり人気っぽさそうなやつを使った。
綺麗だし、簡単だしと良いところ盛りだくさんなライブラリだけど、商用利用の場合は有料のため注意。
React
で利用するために以下のライブラリを利用。
ReactHighstock
などがうまくインポートできなかったりする難点があるけど・・。
今回利用するバージョン
ライブラリ | バージョン |
---|---|
react | 15.6.1 |
react-highcharts | 13.0.0 |
tooltipでHover時に画像を表示する
HighCharts
はところどころでカスタマイズできるので良い。
Hover時の挙動はtooltip
の項目で変更可能です。
tooltip: { useHTML: true, formatter: function() { if(this.series.type === 'line') { return false; } return '<img src="' + this.series.userOptions.thumbnails[this.point.x] + '" width="200" height="200" />'; } }
何をしているかというと、棒(column)と折れ線(line)の複合グラフを想定していて、
折れ線グラフの際はfalse
を返すことで何もしません。
それ以外は、series
あるthumbnails
という配列の中身を表示させてます。(後述)
thumnails
じゃなくてもカスタムの名前でOK。
表示された画像をクリックすると遷移させる
厳密に言うと、グラフ自体をクリックしても遷移する。
こちらも、URLs
というカスタム項目の中に配列でいれておけばよい。
series: [{ type: 'column', name: '棒グラフ', yAxis: 1, point: { events: { click: function() { var url = this.series.userOptions.URLs[this.x]; if (url) window.open('http://'+url); } } }, URLs: ['url_1', 'url_2'...], thumbnails: ['thumbnails_url_1','thumbnails_url_2'...] }, { type: 'line', name: '折れ線グラフ', } ]
events
のclick
項目に関数をしていすれば簡単に実装できる。
参考になりそうなサイト
Laravel + Reactで画像をs3にアップロードする
どうも、くずきです。
今回使用するバージョン
ライブラリ | バージョン |
---|---|
react | 15.6.1 |
Laravel | 5.5.19 |
league/flysystem-aws-s3-v3 | 1.0.18 |
React
周りはcreate-react-app
のバージョン1.4.0
を使って作成している。
Laravelでs3にアップロードする準備
Laravel
のfilesytem
機能をleague/flysystem-aws-s3-v3
を使って簡単に拡張できる。
公式の説明がわかりやすくて、そちらを参考にした方が良いです。
コードだけのせておく
// ControllerでAPIの準備しておく public function upload(Request $request) { $item_image_path = ""; if ($request->hasFile('item_image_file')) { $item_image_path = $request->item_image_file->store('images/item', 's3'); } return response()->json(["href" =>"//".env('CDN_DOMAIN')."/" . $item_image_path]); }
Reactでファイルをアップロードする
自分はここで少しはまった。
Javascript
ではアップロード時に取得したファイルデータをそのままアップロードしても、multipart/form-data
扱いにならない。
そこで、FormData
モジュールを使って変換してあげる必要がある。
受け取ったデータ(target
のfiles
の中)をFormData
のappend
関数にいれてあげるだけでよい。
const formData = new FormData(); formData.append('item_image_file', e.target.files[0]); UploadAction.uploadItemImage(formData); uploadStore.on("item_image", (v) => { this.props.onChange(v.href); });
今回はアップロード後にURL
が返ってくるので、それを表示している。
終わりに
わかってしまえば簡単だった。
laravel-adminでパスワードの設定の仕方
どうも、くずきです。
こないだLaravel
+ React
でWebサービスを構築した際に、社内の人間が使う管理画面も作ることになりました。
社内の人が使う管理画面なんて絶対に工数かけたくないので、調べた結果
の記事で試していた、
を使ってみた。結論から言うと神ってるほど楽。それはまた別記事で・・。
今回はlarave-admin
でパスワードを扱う場合のやり方について書こうと思う。
パスワードの隠し方
larave-admin
ドキュメントがある程度そろっており、password
の設定方法も記載されている。
http://laravel-admin.org/docs/#/en/model-form-fields?id=password-input
ただ、これだと若干わかりづらく、誤って$form->password('password')
と設定するとパスワードが丸見えという状態になってしまう。
そこで、第二引数に
$form->password('password', trans('admin.password'));
trans('admin.password')
をつけることによって、パスワードが隠れる状態にできる。
確認用パスワードの設定
大体のサービスでは、パスワード設定時には確認用に入力することがあると思う。
laravel-admin
でも簡単に設定でき、先に結論を書いてしまうと
$form->password('password', trans('admin.password')) ->rules('required|confirmed') ->default(function ($form) { return $form->model()->password; }); $form->password('password_confirmation', trans('admin.password_confirmation')) ->rules('required') ->default(function ($form) { return $form->model()->password; });
Laravel
にすでにあるバリデーションと同じように、rules
にconfirmed
の設定と
設定した入力名 + _confirmation
の入力を用意すれば実装できる。
パスワードの暗号方法と前のパスワードとの比較
パスワードも大体のサービス(というかほぼ全部)では暗号化されていると思う。
じゃあどうやって、パスワードの暗号化と前のパスワードとの比較を行うかというと
$form->saving(function (Form $form) { if ($form->password && $form->model()->password != $form->password) { $form->password = bcrypt($form->password); } });
saving
関数は保存する直前の処理を書くことができ、これを利用する。
保存直前に入力パスワードの比較し、前回と異なった場合はbcrypt
で暗号化し、入力値を上書きする。
ここまでの方法全てのコードをまとめる。
$form->password('password', trans('admin.password')) ->rules('required|confirmed') ->default(function ($form) { return $form->model()->password; }); $form->password('password_confirmation', trans('admin.password_confirmation')) ->rules('required') ->default(function ($form) { return $form->model()->password; }); $form->ignore(['password_confirmation']); $form->saving(function (Form $form) { if ($form->password && $form->model()->password != $form->password) { $form->password = bcrypt($form->password); } });
終わりに
やってみると簡単だったが、ドキュメントだけだとわかりづらいかも。
react-jsonschema-formで外部バリデーションを使用する方法
どうも、くずきです。
こないだReact
でフォームを作る際にreact-jsonschema-formと呼ばれる便利なライブラリを使ったんですが、外部からのバリデーション入れるのに詰まったのでその解決法を書きたいとお見ます。
react-jsonschema-formとは
React
でフォーム作る際に、JSON
で設定ファイルを書いてしまえば自動的に作ってくれるという便利なものです。
Github
のスターが2300(2017/10月時)もあり、色々なところで使われているライブラリです。
簡単に作れるのでお試しで作りたいとか、デザイナーいないからそれぽいの作りたい時に重宝されるみたい。
自分的にはこのライブラリは独自のフォームの作成にも対応していて、インターフェースを統一しながら自分のプロダクトに合わせたものが作れるのが強みなのではないかと思う。
(近々カスタムフォーム周りの説明は記事にします)
バリデーションについて
このライブラリではバリデーションも簡単で、JSON
にバリデーションの内容を書くだけでエラー文言を出してくれます。
また、カスタムのバリデーションに関しても、
function validate(formData, errors) { if (formData.pass1 !== formData.pass2) { errors.pass2.addError("Passwords don't match"); } return errors; } const schema = { type: "object", properties: { pass1: {type: "string", minLength: 3}, pass2: {type: "string", minLength: 3}, } }; render(( <Form schema={schema} validate={validate} /> ), document.getElementById("app"));
このように、pass1
とpass2
にはJSON
で長さのバリデーションをつけ、validate
関数によってカスタムのバリデーションを追加できます。
しかしながら、弊社のプロダクトではアカウントの登録時にemail
の重複やサーバー側とフロント側でバリデーションを揃えたいという要望があり、調べた限りですと対応していないようでした。
Storeを利用した外部バリデーション
上記の問題を解決するために、すべてのForm
をカスタムフォームにし、
Form(error用のStoreに登録) -> Action(サーバーからのレスポンス) -> Store(エラー登録) -> Form(Storeからエラーを受け取ったら表示)
のような流れで実装しました。
Form
のコンポーネント
class SelectField extends Component { constructor(props) { super(props); this.state = { formData: props.formData?props.formData : null, errors: null }; this.handleError = this.handleError.bind(this); } componentWillReceiveProps(props) { this.setState({ formData: props.formData }); } componentWillMount() { errorStore.addValidListener(this.handleError); } componentWillUnmount() { errorStore.removeValidListener(this.handleError); } handleError(_errors) { let errors = Validation.getValidationErrors(_errors, this.props.name); this.setState({ errors: errors }); } render() { return ( ... ); } }
特段珍しいことはせず、フォームごとにerrorStore
に登録。
バリデーションの中身はすべてきてしまうため、そのフォームのエラーがあるかgetValidationErrors
で検索。
一応getValidatonErrors
export function getValidationErrors(errors, name) { let res = []; _.map(errors, (v,k) => { if (k === name) { _.map(v, (v2, k2) => { res.push(v2); }); } }); return res; }
Action
export function itemEdit(data, id) { let url = API.Item.Edit.replace(":id", id); request .post(url) .send(data) .use(AuthUtil.bearer) .end(function(err, res){ if (err === null) { let body = res.body.success; dispatcher.dispatch({type:"ITEM_EDIT", body}); } else { dispatcher.dispatch({type:"ERROR", res}); // レスポンスがエラー時はErrorStoreに投げる } }); }
エラーだった際にErrorStore
に投げる。
ErrorStore
class ErrorStore extends EventEmitter { handleActions(action) { if(action.type === "ERROR"){ // エラー通知 if(action.res.hasOwnProperty("statusCode")) { switch (action.res.statusCode) { case 500: break; case 400: this.emit(EVENT.ERROR.VALIDATION, action.res.body.errors); break; case 401: this.emit(EVENT.ERROR.AUTHENTICATION, action.res); break; default: break; } } } } addValidListener(listener) { this.on(EVENT.ERROR.VALIDATION, listener); } removeValidListener(listener) { this.removeListener(EVENT.ERROR.VALIDATION, listener); } } const errorStore = new ErrorStore(); dispatcher.register(errorStore.handleActions.bind(errorStore)); export default errorStore;
ErrorStore
ではステータスコードによって通知先(emit)を変えてるので、いろいろなエラーに対応できる。(今の所)
問題点
このやり方だと問題があり、フォームごとにStore
に登録するためEventEmitter.defaultMaxListeners
の上限を変えないと怒られてしまいます。
そもそも登録しすぎるとメモリ食うのでフォームを登録している親コンポーネントがErrorStore
に登録し、子のフォームに伝えるやり方がパフォーマンス的には良いのかなと思ってますが・・・なるべく独立して使いやすくしたい。。
おまけ
ここで議論されてるっぽい。(Redux-formだとそれっぽいこと簡単にできそう・・・・)