Capistrano3で並列制御を行う

Capistranoでは、デフォルトではすべてタスクが並列で実行されるようになっていますが、次の設定で並列実行できるサーバの数を制御できます。

設定方法

  • タスク個別に設定する場合
task :uptime do
  on roles(:all), { in: :groups, limit: 5, wait: 2 } do
    execute :uptime
  end
end

この場合は、5台ずつ実行し、間2秒待機ということになります。 例えば、全部10サーバ実行するとしたら、10サーバのうち5台タスクが実行される => 2秒待ち => 残り5台タスクが実行される という流れになります。

  • デフォルトを変更する場合
SSHKit.config.default_runner_config = { in: :groups, limit: 5, wait: 2 }

設定の内容は上記と同じです。 個別にタスクが設定されている場合は、そちらが優先されます。

デプロイタスクで使えるか?

以前、10台を超えるようなアプリケーションサーバへデプロイした際に、クライアントのオープンできるファイルディスクリプタの上限を超えてしまいデプロイができないという問題がありました。 その際は、ファイルディスクリプタの上限値を上げて対応しましたが、根本的な対応として並列で実行できるサーバの数を調整して対応できないかを調べてみました。

結果的には、deploy:publishing のタスク(currentディレクトリの切り替え) https://github.com/capistrano/capistrano/blob/96fdb4c8c54772fa6d5a4ed29cc6df8a2f65ee7b/lib/capistrano/tasks/deploy.rake#L99-L107 にかかる時間差がgroupごとに発生してしまうため、このままだと、これが許容できる場合はOK。できない場合はNGとなります。

ほとんどのケースでは、アプリケーションを同時に反映して欲しいのでほぼNGでしょう。

対策として、

Rake::Task["deploy:symlink:release"].clear
namespace :deploy do
  namespace :symlink do
    desc "Symlink release to current"
    task :release do
      on release_roles(:all), in: :parallel do
        tmp_current_path = release_path.parent.join(current_path.basename)
        execute :ln, "-s", release_path, tmp_current_path
        execute :mv, tmp_current_path, current_path.parent
      end
    end
  end
end

こういう感じで、上書きしてあげればよさそうですが、ちょっと無理矢理感が強いので避けたい気持ち・・・ アップデートで内容が変わる可能性もありますし。 何か解決できる案があれば教えてください。

参考: https://github.com/capistrano/sshkit#parallel http://asobo.hatenablog.jp/entry/2016/01/17/115631 http://blog.livedoor.jp/sonots/archives/39007111.html

GETリクエストのボディはPHPで取得できるの?試してみた

定期的に業務で不正なリクエストがないか見ています。 その調査でふと、PHPはGETリクエストに含まれるボディの値が取得できてしまうのか、 取得可能であればそこも気にしなければなぁ、ということで調べてみました。

GETリクエストのボディ?

GETは指定したURIの情報を取得するためのもので、通常ボディは含めませんが、やろうと思えば指定は可能です。

A payload within a GET request message has no defined semantics; sending a payload body on a GET request might cause some existing implementations to reject the request.

https://tools.ietf.org/html/rfc7231#section-4.3.1

RFCでも、特にダメですとは言われてはいない。 一部、リクエストを拒否する既存の実装があるかもね。くらい

環境

  • サーバ: PHP7.3 + PHP-FPM + nginx (docker)
  • リクエストの送信: Burp Suite

まずは、スーパーグローバル変数 $GET $POST $_REQUEST で確認

PHPファイルを用意する

以下のように、$GET, $POST, $_REQUESTで受け取れるPHPファイルを用意しました。

<?php
echo "\$_GET query_param: " . $_GET["query_param"] . "¥n";
echo "\$_GET body_param: " . $_GET["body_param"] . "¥n";
echo "\$_POST query_param: " . $_POST["query_param"] . "¥n";
echo "\$_POST body_param: " . $_POST["body_param"] . "¥n";
echo "\$_REQUEST query_param: " . $_REQUEST["query_param"] . "¥n";
echo "\$_REQUEST body_param: " . $_REQUEST["body_param"] . "¥n";

リクエストを準備する

POSTリクエス

POST /index.php?query_param=hoge HTTP/1.1
Host: www.example.com:8888
Content-Type: application/x-www-form-urlencoded
Origin: http://www.example.com:8888
Content-Length: 9

body_param=huga

GETリクエスト(body付き)

GET /index.php?query_param=hoge HTTP/1.1
Host: www.example.com:8888
Content-Type: application/x-www-form-urlencoded
Origin: http://www.example.com:8888
Content-Length: 9

body_param=huga

まずは、POSTリクエストの結果

HTTP/1.1 200 OK
Server: nginx/1.15.12
Date: Sun, 14 Jun 2020 03:02:41 GMT
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
X-Powered-By: PHP/7.3.11
Content-Length: 143

$_GET query_param: hoge
$_GET body_param:
$_POST query_param:
$_POST body_param: huga
$_REQUEST query_param: hoge
$_REQUEST body_param: huga
  • $_GETでリクエストBobyの値は取れませんでした。
  • $_POSTでクエリパラメータの値は取れませんでした。

まずは、予想通り

続いて今回確認したかった、GETリクエストの結果

HTTP/1.1 200 OK
Server: nginx/1.15.12
Date: Sun, 14 Jun 2020 03:07:42 GMT
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
X-Powered-By: PHP/7.3.11
Content-Length: 135

$_GET query_param: hoge
$_GET body_param:
$_POST query_param:
$_POST body_param:
$_REQUEST query_param: hoge
$_REQUEST body_param:

このようになりました。 GETメソッドが来た場合は、$GET, $POST, $_REQUESTではリクエストbodyの値は取得できないことがわかりました。

php://input を使ってみる

php://input は読み込み専用のストリームで、 リクエストの body 部から生のデータを読み込むことができます。

https://www.php.net/manual/ja/wrappers.php.php

これを使ってみます。

PHPファイルを用意

<?php

$hoge = file_get_contents('php://input');
echo $hoge;

リクエストを準備する

先ほどと同じやつです。

GET /index.php?query_param=hoge HTTP/1.1
Host: www.example.com:8888
Content-Type: application/x-www-form-urlencoded
Origin: http://www.example.com:8888
Content-Length: 15

body_param=huga

結果

HTTP/1.1 200 OK
Server: nginx/1.15.12
Date: Sun, 14 Jun 2020 03:14:09 GMT
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
X-Powered-By: PHP/7.3.11
Content-Length: 15

body_param=huga

リクエストボディの値が、そのままの形で取得できました! (POST, PUT, PATCH, DELETEでも確認してみましたが、結果は同じでした。)

まとめ

  • $GET $POST $_REQUEST では、GETリクエストのボディに含まれる値は取得できない
  • php://input を使うと、GETリクエストのボディに含まれる値はそのままの形で取得できる
    • おそらく、HTTPのメソッド関係なく取得できるものと思われます

ということがわかりました〜。

以上

Ruby技術者認定試験(Gold)どうやって勉強していたっけ?というのを思い出した

2年前くらいに、Silver受かったときにブログを書いたのですが、

https://takapi86.hatenablog.com/entry/2018/05/13/230504

その後、Goldを受かったまま何もアウトプットできていなかったので、書いていこうと思います。

2年前の話なので、今は状況が変わってしまっているかもしれませんが、参考までに

Ruby技術者認定試験とは?

こちらをご参照ください https://www.ruby.or.jp/ja/certification/examination/

Rubyの対象バージョンは2.1なので、ちょっと古いなという感じです。 今回は、Goldの話。

試験を受ける目的

業務で、Railsアプリを触り始めた当初の目標は、とにかくコードを書いて成果を出すことが第一目標でした。 最初はとにかく、覚えゲーでもなんでも良いので、Rails, Rubyを覚える。もちろん基礎的なことはもちろん大事だけど、まず成果として出せるようにし業務に貢献できるようにする。という作戦で進めていました。

しばらくして、そこそこ成果を出せるようになったので、改めて、基礎からやり直して行こうと思い、メタプログラミングRubyEffective Rubyを読み進めていました。しかし、「へー」とある程度理解はできたのですものの、ここに書かれている内容をじゃあ再現させてみてと言われると難しく、ある程度実際にコードを書いたり、練習したりしていく必要があると感じたため、資格試験を利用してみよう思ったのがきっかけです。

試験の対策(Gold)

どのような問題がでるか確認する

を斜め読みし、大体こういう感じで、出題されるのだなというのを確認しました。

理解する

実際に、試験対策を行う前に以下を読んで、復習しました。

アウトプットする

頭の整理が必要なものはアウトプットするようにしました。

練習する

あとは、問題を解きまくっていきます。 上記の、RubyExaminationで、9割くらい取れるようになると合格が見えてくると思います。

そのほか、所感

資格試験自体の難易度は高くなく、合格教本に載っている内容の挙動が理解できていれば問題なく合格できると思います! 暗記することがSilverよりも少ないので、一度理解してしまえば、Goldの方が楽かもしれません。

ボリュームも多くないので、Goldの試験対策の期間は1,2ヶ月くらいを見ておくと良いかもしれません。 僕はSilver終わってから、1ヶ月後に取得しました。

資格試験、ボリュームが多いと途中で飽きちゃったりするのですが、これぐらいのボリューム感だとちょうど良く、しっかりと基礎が学べるので、おすすめです!

合格すると以下のような認定証が送られてきます。(嬉しい

f:id:takapi86:20200613164340p:plain

phpのmemcachedライブラリを2系から3系にあげたらSession Lockに関するワーニングが頻繁に発生するようになったので対応した

PHPバージョンアップに伴い、memcachedライブラリを2系から3系にバージョンアップしたところ、以下のようなエラーが度々発生するようになってしまいました。 (結果的には、ローカル環境で気付けたため、本番導入時には解決した状態で適用することができました。)

Warning: session_start(): Unable to clear session lock record in /var/www/app/index.php on line 6
Warning: session_start(): Failed to read session data: memcached (path: memcached:11211) in ...

どんなときに発生するか

  • そこそこレスポンス待つような処理の実行中に、同じセッションで別の作業をしようとしたとき
  • xdebugでのリモートデバッグでレスポンスタイムアウトになり、その後再度リクエストを送ったとき(このケースではずっとロックが外れなくなってしまう)

原因

原因としては、memcached2系から3系でセッションロックの期限に関する設定項目が変更されたためでした。

memcached2系の設定

memcached.sess_lock_expire で、ロック待ちの時間を設定することができます。 0(デフォルト)のときは、max_execution_time の値が設定されます。 (max_execution_timeのデフォルトは30秒 https://www.php.net/manual/ja/info.configuration.php

https://github.com/php-memcached-dev/php-memcached/blob/2.2.0/memcached.ini#L21-L25

(余談ですが、php.netにある設定はmemcached2系の設定しか載ってないんですよね。) https://www.php.net/manual/ja/memcached.configuration.php#ini.memcached.sess-lock-wait

memcached3系の設定

3系では、ロック待ちの時間が細かく設定できるようになっていましたが、 デフォルトのままだと max_execution_time の値は見ずに、固定で150msとなってしまっており、ここの値が大きく下がったことが原因でした。 https://github.com/php-memcached-dev/php-memcached/blob/v3.1.5/memcached.ini#L7-L19

対応

今まで、memcached2系のときと同じ設定になるようにしました。 (今のところは問題起きてはなさそうです。)

最後に

原因を調べてみても、同じような現象になっている人の記事は見つかるものの、ロック取得に失敗している => じゃあ、値を増やしてみましょう的な対応しか書かれていないものが多く、ちゃんとした原因がわからずでしたが、本家の設定をちゃんと読んでいくことで、原因が突き止められたのでよかったです。

なお、原因の調査には、以前 php-memcached 2.2.0 と 3.1.4 でデータの互換性があるのか確認する で使用した環境を使いました。検証環境をDockerで立ち上げられるよう残しておくと何かと役立つ時があってよいですね。

github.com

以上。

Railsチュートリアル用にdocker環境を作った

社の新人研修でRailsチュートリアルをやっていくことになったのでDocker環境を用意した。(僕は講師陣の方

github.com

結果的にはDockerとRails両方覚えなければならないので混乱させてしまう恐れがあるということで、ローカル環境でやっていこうということになったのだけれども、講師側は問題なく使えると思うので残しておく

困ったことや気づいたこと

最近Railsを全く触っていなく、浦島太郎状態だったので、困ったことや気づいたことを書いていく (何か思い出したら追記していく)

  • Rails5.1.6に対して、Rubyいくつを入れれば良いかわからなかった。

Railsチュートリアル第4版を進めていくことになっており、Railsは5.1.6を使う予定だったが、2020/05/27現在最新のRuby2.7.1だと色々とワーニングが出ていたので、Ruby2.6.6を使うようにした。

  • Docker環境だと、bundle installするときに--path vendor/bundleというのが要らなくなるのでは?

以前開発していたときは、ローカルでセットアップをしたので、プロジェクトごとにgemを分けるため、bundle install --path vendor/bundle をするようにしていたが、Docker環境だと分ける必要がなくなるので、gemがグローバルでインストールされるディレクトリをボリュームマウントして使えば良さそうだった。(実際、所属しているチームで使用しているRailsアプリのDocker構成もそうなっていた)

Rails、色々思い出していかなければ・・・

デバッグツールとしてBurp Suiteを使っている

例えば、

  • ファイルのアップロード処理
  • CSVのインポート処理
  • 入力項目の多い登録処理

上記のような処理をE2Eでデバッグ実行しようとすると、ファイルを準備したり値を画面からいっぱい設定したり・・・それを何度も実行しようとするとそこそこ手間だったりします。 さらにテストコードがないようなコードの挙動を調査するときはもう大変。

今まで私はそのようなケースでは、一度chromeのCopy as cURLcurlコマンドを取得してそれを使うようにしていたのですが、 最近では、Burp Suiteを使っています。

Burp Suiteというとセキュリティ診断などで使用するイメージが強いかもしれませんが、 ローカルプロキシツールがメインとなるツール(でいいのかな?)でリクエストの保持・内容の変更などが行なえるので、 これを利用しデバッグをしています。

私がデバッグするときは、このBurp SuiteのRepeater機能を使っています。 インストール方法やRepeater機能の使い方については、以下に丁寧に書かれているので、こちらを見ていただけるとわかるかと思います。

ローカルプロキシツールBurpの使い方 その1 ~ Proxy機能編 ~ | パーソルテクノロジースタッフ株式会社

ローカルプロキシツールBurpの使い方 その2 ~ Repeater機能編~ | パーソルテクノロジースタッフ株式会社

xdebugを併用する

さらに、PHPの場合、xdebugのリモートデバッグを併用するともっと楽にデバッグできます。

以下は、昔書いた Docker + PhpStorm 環境でXdebugのリモートデバッグが使えるようにするための手順

takapi86.hatenablog.com

私がやっているデバッグの一連の流れ

私はよく以下の方法で、デバッグを行っています。

  • ローカルプロキシを設定済みのブラウザを立ち上げる
  • Burp Suiteの[Proxy]タブ->[Intercept]タブで開いた画面からInterceptをoffにしておく
  • デバッグしたいWebアプリケーションの処理を実行する
  • Burp Suiteの[Proxy]タブ->[HTTP history]タブをクリックするとこれまでにリクエストした一覧が出てくるので、デバッグしたいリクエストを[右クリック]->[Send to Repeater]をクリック
  • [Repeater]タブをクリックすると、送ったリクエストの内容が表示されるので、[Send]ボタンを押す
  • そうすると、先程画面から実行したリクエストが再度発行されるので、デバッグしたいタイミングでこの[Send]ボタンを押していく
    • ※ リクエストの内容を変えたい場合は、同画面の[Params]から変更することができます。
  • xdebugで、ブレイクポイントを置いてあげるとそこで止まるので、そこで変数の値をみたり色々しながら、デバッグしていく

といった流れでよく作業をしています。 ※ CSRF対策などでワンタイムトークンを発行している場合など、上手くいかないケースもあるので注意

何かもっと良い方法があれば教えていただけると嬉しいです。

以上、参考までに

capistranoデプロイ時にデプロイするファイルをウィルススキャンする

プライベートの検証環境を使って、 capistranoデプロイ時にデプロイするファイルをウィルススキャンするようにしてみました。

やっていることは、capistranoデプロイ時にclamdscanしているだけです。 上手くいってそうなので、メモとして残しておきます。

使用した環境

capistranoを実行する環境

デプロイ先

実行するタスクを準備

以下のタスクを準備します。 今回はウィルス検知時に即時ウィルスを削除して欲しいので、オプションは --remove を設定しました。 ※ デプロイ先でclamdscanを実行できるようにしておくこと

namespace :clamd do
  desc 'execute clamdscan'
  task :scan do
    on roles(:all) do
      execute :clamdscan, "--remove", release_path
    end
  end
end

そして、deploy:publishing の前に追加したタスクを実行するようにします。

before 'deploy:publishing', 'clamd:scan'

無事ウィルススキャンができるようになりました。

00:03 deploy:set_current_revision
      01 echo "2f1f01d423a5302f423e9b5b5add61e64880b83b" > REVISION
    ✔ 01 root@sshd 0.049s
00:03 clamd:scan
      01 clamdscan --remove /var/www/app/releases/20200521155121
      01 /var/www/app/releases/20200521155121: OK
      01
      01 ----------- SCAN SUMMARY -----------
      01 Infected files: 0
      01 Time: 0.021 sec (0 m 0 s)
    ✔ 01 root@sshd 0.074s
00:03 deploy:symlink:release
      01 ln -s /var/www/app/releases/20200521155121 /var/www/app/releases/current
    ✔ 01 root@sshd 0.048s
      02 mv /var/www/app/releases/current /var/www/app
    ✔ 02 root@sshd 0.049s

ウィルス検知時は、このようにデプロイが失敗します。

00:03 deploy:set_current_revision
      01 echo "a1590ccbb73f48b484651db1e04aeec737a3d380" > REVISION
    ✔ 01 root@sshd 0.049s
00:03 clamd:scan
      01 clamdscan --remove /var/www/app/releases/20200521160613
      01 /var/www/app/releases/20200521160613/eicar: Eicar-Signature FOUND
      01 /var/www/app/releases/20200521160613/eicar: Removed.
      01
      01 ----------- SCAN SUMMARY -----------
      01 Infected files: 1
      01 Time: 0.037 sec (0 m 0 s)
(Backtrace restricted to imported tasks)
cap aborted!
SSHKit::Runner::ExecuteError: Exception while executing as root@sshd: clamdscan exit status: 1

今回検証した環境は、dockerで動かせるようにしたので、動きを確認したい場合は以下の環境でお試しすることができます。

github.com

では