ひでメモ

プログラムについて勉強したことを書きます。たぶん。

iTerm2 の Profile 保存機能を使ってワンクリックで開発環境を立ち上げる

前置き

開発マシンをずっとスリープのまま使っていたらめちゃくちゃ重くなっていた(再起動したらめちゃくちゃ早くなってびっくりした)ことをきっかけに週一でマシンを再起動するようにしました。
そしたら、再起動の度に仮想マシン立ち上げて、ssh でログインしてコンテナ立ち上げて、別のタブで npm run watch して…
という作業が週一で発生するようになりました(今まではターミナルを開きっぱなしでした)

これらがめんどくさすぎるので自動化しました!

概要

iTerm2 の Profile 保存機能を使ってワンクリックで開発環境を立ち上げられるようにしました。

なお、仮想マシンMac へのログイン時に自動起動するようにスクリプトを登録しました(後述)

仮想マシンにログインして」と書いているのは私が Docker for Mac を使っておらず、Ubuntu仮想マシン上で Docker を動かしているからです。
なので、ちょっと特殊な環境ですが、逆に情報があまりないようなので参考になれば幸いです。

私の使用例

プロジェクトごとに以下の3タブを開くようにしました。(現状2セットあります)

  1. 仮想マシンにログイン後、プロジェクト直下まで移動して Docker コンテナを起動する。その後はプロジェクト直下でコマンドを叩く用においておく
  2. ファイルの変更を検知してフロントをビルドしてくるコマンドを実行
  3. ローカルのプロジェクト直下へ移動

設定しておけば上記の処理を自動で行いつつタブを開く操作がワンクリックでできます。

Profile の作成

「Preferences > Profiles」から Profile を作成できます。

iTerm2 の Profile 設定
※ ぼかしているのはプロジェクト名とかが入っているので念の為です
今回使用する主な設定項目は以下です。

  • Tags:タグをつけることで整理したり、タグ単位でまとめてタブを開くことができます。
  • Send Text at Start:ここに記載したコマンドをタブが開いた後に実行します。(Docker コンテナを起動したり、ssh でログインしたり)
  • Working Directory:作業ディレクトリが指定できます。上記のコマンドはこのディレクトリに移動した状態で実行してくれます。

Profile はコピーできるのでワンセット作ったら、コピーしてパスだけ変えるといったことでき便利です!
私も2セット目は1分くらいでできました!

以下はちょっと私が苦戦したことです。(iTerm2 はあまり関係ないです)

ssh でログイン後にコマンドを実行する + ssh 接続を保持する

結論から言うと-tオプションに続けてコマンドを入力するといけました!
(前述した通り自前の仮想マシンで Docker を動かしているので ssh する必要がありました)

以下のコマンドは192.168.50.10仮想マシンへログイン後
~/project_aというディレクトリに移動して
./vendor/bin/sail up -dというコマンドを実行します。

ssh 192.168.50.10 -t "cd ~/project_a ; ./vendor/bin/sail up -d ;  bash --login"

ここで私がつまずいたのは以下の2点です。ここがこの記事のメインです

このコマンド中では.bashrcなどは(おそらく)読み込まれないことに気づけませんでした。
sailコマンドはよく使うのでエイリアスを貼っていたのですがそれが読み込まれず手こずりました。
※ プロジェクトは Laravel Sail 環境です

次に、最後のbash --loginがないとコマンドが終わったらそのまま ssh 接続を終了してしまいますので、もしコマンド終わった後にログインしたままにしたいなら付与する必要があります。

保存した Profile をタグ単位でまとめて開く

タグでまとめてタブを開く

iTerm2 のアイコンを右クリックして「New Tab > Profileで設定したタグ名 > Open All」でタグ単位でまとめてタブを開くことができます。
プロジェクト単位でタグをつけておけば、ワンクリックで開発環境が立ち上がって、ディレクトリも指定の位置にいる、という状態になります!

開いている Profile の保存

command + shift + sで開いているタブの状態(開いている Profile?)を名前をつけて保存できます。
画像のメニューにある「Restore Arrangement」でそれが一気に復元できます。
今関わっているプロジェクトを全部まとめて保存しておく、とか良さそうですね。

仮想マシンをログイン時に自動起動する

以下のような起動スクリプトを書いてdocker-machine_up.commandのような名前で保存します。

cd ~/develop/docker
vagrant up

「システム環境設定 > ユーザとグループ > ログイン項目」に上記のスクリプトを登録すれば完了です。

感想

やはり自動化はエンジニアの本懐ですよね…(そうじゃない)
リモートのサーバに接続して、DB にログインした状態にするとかもできそうでいろいろ応用範囲が広そうです!

telnet の代わりに curl でポート番号が開いているかどうか確認するコマンド

Mac とか Ubuntu とかは telnet コマンドが入ってないことが体感多いんですが curl ならたいてい入ってます。
Docker コンテナの設定を変えたりしたときにポート番号が開いてるかをすぐ確かめるのに便利です。

 curl -v telnet://localhost:80

-v がないとポートが開いていた場合は何も表示が出ない状態で、応答待ちになってしまうのでつけるをおすすめします。

【SQL・Laravel】大量の withCount を半生SQLに書き換えて高速化した話

状況がピンポイントすぎるのですがたまーに有り得そうだなと思い、自分の備忘録も兼ねて記事を書きました。
いろいろなテーブルに対して異なる条件で withCount を大量に行ったため、処理が重く画面が表示できない(リクエストがタイムアウトする)状態だったので可読性を犠牲にして高速化した話です。

「半生SQL」と書いたのは使えるところは Laravel を使ったからです。

処理概要

イメージとしては以下のような 1 対 多 の関係のテーブルがあり、特定のステータスの変更依頼がある注文の数をカウントするという処理でした。

注文と変更依頼のER図

上記の2つのテーブルを結合した場合以下のようになります。

注文.ID 注文変更依頼.ID 注文変更依頼.ステータス
1 10 依頼中
1 11 対応済み
1 12 依頼中
2 13 依頼中

変更依頼同時に複数出せるんかいというツッコミはおいておいて(あくまでイメージなので)、紐づく注文変更依頼は複数あります。
カウントしたいのは「「依頼中」のステータスの注文変更依頼が紐付いている注文が何件あるか?」であるため、「依頼中」の注文変更依頼が何件あろうとも紐付いてさえいれば1件としてカウントします。

つまり、上記の例では件数は以下のようになります。
依頼中がある注文:2件
対応済みがある注文:1件

このように単純に「注文変更依頼」の数をカウントするのではなく、あくまで「注文」の件数をカウントしないといけないのがややこしいところでした。

対応

変更前の実装

先に変更前の実装のイメージ書いておきます。

   $query
         ->withCount(['changeRequests as status_irai_tyu' => function ($q) {
            $q->where('status', ChangeRequest::STATUS_IRAI_TYU);
        }])
        ->withCount(['changeRequests as status_taiou_zumi' => function ($q) {
            $q->where('status', ChangeRequest::STATUS_TAIOU_ZUMI);
        }])
  // 以下に10個くらい続く

Eloquent で素直に書こうと思ったらこうなりますよね、という感じでした。
いざステージングで動かしたらデータ件数が多くタイムアウトしてしまったようです。
(念の為ですが上記のようなコードを書いたのは私ではないです…!)

対応方針

紐づくテーブルが 1対多 になっているのがやりづらいのでじゃあ 1対1 になるようにしよう、ということで以下のように該当するステータスの注文変更依頼があればカラムを1に(フラグを立てる)なっているようなテーブルを作ります。

注文.ID ステータス依頼中 ステータス対応済み
1 1 0
2 0 0
3 0 1

このようなテーブルに対してステータスごとにカウントするSQLを書けばできそうです。
以後このテーブルを「注文変更依頼ステータス」テーブルと呼びます。

「注文変更依頼ステータス」テーブルを作る

以下のSQLで 「注文変更依頼ステータス」テーブルを作ることができます。

SELECT
  order_id,  -- 注文ID
  MAX(CASE WHEN status = 0 THEN 1 ELSE 0 END) as status_irai_tyu, -- ステータス依頼中
  MAX(CASE WHEN status = 1 THEN 1 ELSE 0 END) as status_taiou_zumi, -- ステータス対応済み
  <中略>
FROM  change_request
GROUP BY order_id;

ステータスフラグになるカラムを作成するこのSQLを簡単に解説します。

MAX(CASE WHEN status = 0 THEN 1 ELSE 0 END) as status_irai_tyu

MAX関数の仲のCASE文で
特定のステータスのときに 1 (フラグON)、
それ以外のときは 0(フラグOFF)となるようにします。
GROUP BY order_idで注文IDでグルーピングされるので、そのグループの中でMAXを取ることによって、ひとつでも 1 があればその注文IDのstatus_irai_tyuカラムは 1 になります。
どれか1つでも TRUE であればよい OR 演算と同じです。

テキストだけだと少しわかりづらいので具体的なテーブルを書きました。
記事の先頭に出したテーブルを CASE 文でフラグに変換した場合以下のようになります。
※これは GROUP BY でまとめる前として見てください

注文.ID 注文変更依頼.ID ステータス依頼中 ステータス対応済み
1 10 1 0
1 11 0 1
1 12 1 0
2 13 1 0

この表を GROUP BY と MAX で注文IDごとにまとめるとこうなります。

注文.ID ステータス依頼中 ステータス対応済み
1 1 1
2 1 0

注文ID:1 はステータスが「依頼中」の変更依頼が2件あるんですが MAX で最大値を取っているので結局 1 になります。

これで「注文テーブル」に対して 1対1 となる「注文変更依頼ステータス」テーブルができました!

結合&合計

最後に「注文テーブル」と「注文変更依頼ステータス」テーブルを結合して合計していきます。

SELECT
  SUM(status_irai_tyu) as irai_tyu_count, -- ステータス依頼中の合計
  SUM(status_taiou_zumi) as taiou_zumi_count, -- ステータス対応済みの合計
  <中略>
FROM order
JOIN (
  SELECT
    order_id,  -- 注文ID
    MAX(CASE WHEN status = 0 THEN 1 ELSE 0 END) as status_irai_tyu, -- ステータス依頼中
    MAX(CASE WHEN status = 1 THEN 1 ELSE 0 END) as status_taiou_zumi, -- ステータス対応済み
    <中略>
  FROM  change_request
  GROUP BY order_id;
) as change_request_status ON change_request_status.order_id = order.id;

最終的には以下のような各ステータスの注文件数のテーブルになります。

ステータス依頼中件数 ステータス対応済み件数
2 1

どんどん行を減らしていく作業でしたね。

Laravel に書き起こす

いやー終わった終わった 生の SQL をべたっと貼り付けるわけにはいかないのでできるだけ Eloquent に変換します。

上で書いた最終的な SQL を Laravel で書くとこのようになります。
ChangeRequest Order はそれぞれテーブルと対応するモデルです。

$changeRequestStatusTable = ChangeRequest::query()
    ->selectRaw('order_id')
    ->selectRaw('MAX(CASE WHEN status = 0 THEN 1 ELSE 0 END) as status_irai_tyu')
    ->selectRaw('MAX(CASE WHEN status = 1 THEN 1 ELSE 0 END) as status_taiou_zumi');
    
Order::query()->joinSub(
    $changeRequestStatusTable,
    'change_request_status',
    fn ($join) => $join->on('change_request_status.order_id', '=', 'order.id')
)
    ->selectRaw('SUM(status_irai_tyu) AS status_irai_tyu_count')
    ->selectRaw('SUM(status_taiou_zumi) AS status_taiou_zumi_count');

※ステータスの判定している箇所は実際は数値ではなくて定数を使っています

感想

タイムアウトするくらいの重さだったページが1~2秒で表示されるようになったのはやっぱり嬉しいですね!

そして以前に複雑なビューテーブルを作ったときもお世話になったのですが、ミック先生のSQL指南書が今回も役に立ちました!

達人に学ぶ SQL徹底指南書 | ミック | 工学 | Kindleストア | Amazon

まだ EXISTS などは全然使いこなせていないのですがこの本で知った CASE 文だけでもかなり役に立ってるのでそれだけでも元が取れたと感じます。

余談

www.lucidchart.com

ER図の作成を上記のツールを使ってみました。
今回のような書き捨てのような図であればさくっとできてよかったです。

git pull すると error: cannot lock ref が出る

以下のようなエラー文が出て git pull できないことがありました。

# git pull
error: cannot lock ref 'refs/remotes/origin/feature/#100_fix_application': is at 1d5d3c7777d883a428ffaf37a6f3ea2e77efccd9 but expected 9b681a8a212a74e003c4b9c44f4a9e36f17d8779
From ssh://git.hogehoge/fuga/piyo
 ! 9b681a8a..1d5d3c77  feature/#100_fix_application -> origin/feature/#100_fix_application  (unable to update local ref)

以下のコマンドで解決!

git remote prune origin  

参考 stackoverflow.com

【Laravel】レート制限の判定方法とホワイトリストのやり方

記事はたくさんあるんですがコードを見てへ~となったのでメモです。
あとうちのプロジェクトで実装している簡易ホワイトリストの実装も。

概要

プロジェクトではログイン処理がN分内にM回試行されると一時的なロックをかけるようにしています。(もちろんセキュリティのためです)
そのロック処理に Laravel のレート制限ミドルウェアを使用しているのですが、そもそも何を基準にロックしてるんだっけ?という話になり少し調べてみました。

レート制限の基準

以下を見ると接続先のドメイン名とIPアドレスを連結した後にハッシュ化した値でカウントしているようです。
(認証前の場合。認証後は User ID などになるはず)

protected function resolveRequestSignature($request)
    {
        if ($user = $request->user()) {
            return sha1($user->getAuthIdentifier());
        } elseif ($route = $request->route()) {
            return sha1($route->getDomain().'|'.$request->ip());   // ドメイン名とIPをつなげてハッシュ
        }

        throw new RuntimeException('Unable to generate the request signature. Route unavailable.');
    }

github.com

特定のIPアドレスだけレート制限をかけない

開発や検証のために特定のIPアドレスだけ制限をかけないようにしたいということはあると思います。
障害発生時に弾かれていたら大変ですし、普段でも仕事に支障が出るかもしれないです。

そこで、以下のように ThrottleRequests ミドルウェアを少し拡張してホワイトリストに登録したIPアドレスはレート制限の判定をスルーするようにしています。

use Illuminate\Routing\Middleware\ThrottleRequests as ThrottleRequestsOrigin;

class ThrottleRequests extends ThrottleRequestsOrigin
{
    public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1, $prefix = '')
    {
        // 許可されたIPは判定を行わない
        if (in_array($request->ip(), config('throttle.whitelist_ip'))) {
            return $next($request);
        }

        // ホワイトリストに該当しないIPアドレスは継承元にそのまま流す
        return parent::handle($request, $next, $maxAttempts, $decayMinutes, $prefix);
    }
}

ホワイトリストには自社やお客さんの運営担当などのIPアドレスを登録しています。
今のところ数も多くないため config に直接記載しています。

GitLab の CI/CD でデプロイ先に応じて環境変数を変える

目的

フロントエンドで使用する環境変数を環境ごとに変えたいため、ビルド前に環境変数をセットするのが目的です。

Laravel Mix を使うと頭に MIX が付いている環境変数をフロントエンドと共有できますが、当然フロントエンドのビルド時点で設定されている必要があります。
このビルド時点で環境変数をセットするというのを CI 上でやりたいというのが動機です。

gitlab-ci.yaml の書き方

以下のようにworkflow:rulesを使って、各環境に応じたブランチがプッシュされた場合に環境変数を切り替えるようにしました。

GitLab CI/CD パイプライン設定リファレンス | GitLab

workflow:
  rules:
    - if: $CI_COMMIT_REF_NAME == "staging"
      variables:
        MIX_HOGE_URL: https://example.com/staging
    - if: $CI_COMMIT_REF_NAME == "master"
      variables:
        MIX_HOGE_URL: https://example.com/production
    - when: always

## 以下にジョブの設定などが続く

$CI_COMMIT_REF_NAMEという変数は現在のブランチ名(またタグ名)になるため、この変数を判定に使い環境によって設定する値を切り替えます。
最後のwhen: always がないと if に書いたブランチ以外 CI/CD が走らないので注意です。
変数が増えてきたら include などを使って別ファイルに切り分けてもよさそう。

有効でなかった設定

以下の CI/CD の変数の設定で enviroment という項目があり、デプロイ先によって変数の値を変えられます。
gitlab-docs.creationline.com

しかし、この設定は特定の環境を指定するとデプロイのジョブのみで有効になってしまうため、フロントエンドのビルドのジョブの時点では環境変数がセットされませんでした。
※すべての環境に適用する設定にすると CI/CD の開始時点からセットされていました

【Laravel】Sail のデフォルト Web/AP サーバで複数リクエストを並列処理する

概要

Laravel Sail では複数のリクエストを並列で処理できないかと思ったら実はできました!
ついでに開発環境もちょっとだけ早くなったよ、という話です。

Laravel Sail 便利ですよね

現在のプロジェクトの開発環境は Laravel Sail を使用しています。

readouble.com

Sail を使用すれば簡単に Laravel の Docker 開発環境が作れます。

また、docker compose ~~~ のコマンドが sail up のようにデフォルトでエイリアスが作られていて簡単に叩けるようになっていたりして便利です。
例えば、sail artisanに続けてコマンドを実行すれば migratetinkerなどがコンテナに入らずに実行できます!

このように便利な Sail なのですが先日ひとつ問題が発生しました。

APIタイムアウトするという事象が発生

システムA から システムB へ API で連携する改修を行っていました。
その中で以下のように相互に API を叩き合う処理を追加し試したところ処理が終わらないということが発生しました。

  1. システムAがBのAPI1を実行
  2. システムBはそのAPI1の処理の途中で、システムAのAPI2を実行
  3. システムBからの応答がなかったとしてシステムAの処理がタイムアウト

タイムアウトの原因

このタイムアウトの原因は Laravel Sail の Web/AP サーバがシングルプロセスだったためでした。

以下のようにお互いがお互いの処理完了を待ってしまった結果、タイムアウトしてしまいました。

  1. システムAはAPI1の応答を待っている ※この時点でひとつしかないプロセスを専有
  2. システムBはAPI1の途中でAPI2で情報を取得しようとするが、システムAはシングルプロセスなのでこれに応答できない

ただ、実装を見直した結果、相互にAPIを叩き合う必要はなかったのでこの問題は回避できました。

なんでシングルプロセスなの?

Laravel Sail は Web/AP サーバに PHP のビルドインサーバを使用しており、こちらが基本的にはシングルプロセスでしか動作しないためでした。

PHP: ビルトインウェブサーバー - Manual

マニュアルの一番上に書いてありますね… 残念…

と思ったら実はできそう

この記事を書いていて気づいてしまったのですが、PHP7.4 以降であれば環境変数を指定することで並列で処理を実行できるようです…!
一番上に「単一のシングルスレッドプロセスしか実行しない」って書いてあったから騙された…!

ビルトインウェブサーバに対して複数のリクエストを並列で投げる必要があるテストコードのために、 複数のワーカーをフォークさせるよう設定できるようになりました。
サーバを起動する前に欲しいワーカーの数を PHP_CLI_SERVER_WORKERS 環境変数に設定してください。

試してみました

docker-compose.yamlに以下のようにPHP_CLI_SERVER_WORKERS環境変数を追加してコンテナを起動してみました。

        environment:
            WWWUSER: '${WWWUSER}'
            LARAVEL_SAIL: 1
            PHP_CLI_SERVER_WORKERS: 5  # 追加!

必ず呼び出される index.php などに sleep で5秒待機する処理を追加して、curl で同じ URL を取得するように複数のタブで並列に叩いてみました。
その結果、PHP_CLI_SERVER_WORKERSを指定した場合だと並列で curl を実行してもほぼ同じタイミングで応答が返ってきました!
環境変数を指定しない場合だと同タイミングではなく、先に叩いた curl の応答が返ってきた数秒後に次の応答が返ってきました)

少しだけ高速化

ふと思って開発環境で画面を開いて見たところ1〜2秒くらいですが表示速度が上がっていました。

特に意識してなかったのですが、今まではシングルプロセスだったのでフロントから並列で叩かれるAPIも直列で処理していたわけです。
これが、並列で処理して応答を返してくれるようになったので表示速度が少しだけあがりました!

ただ、あまり並列数を上げすぎるとどこかで処理が集中しすぎるのか応答が返ってこない API が出てくるためプロセス数は 3 程度に抑えるようにしました。

開発環境でも早いに越したことことはないのでラッキーでした!

おまけ:どこに Sail はビルドインサーバで動いてるって書いてるの?

ここの Tips に Sail で php artisan serve が動いているという記載がありました。

Laravel Sail 8.x Laravel

そしてこちらのページにはSailへPHP開発サーバの代わりにOctaneを使いとありました。

Laravel Octane 8.x Laravel

もっとわかりやすく書いてくれていたページがあった気がしたのですが見つけられなかった…!