NuxtMeetUp#9 オールスターズを開催しました

こんにちは株式会社ROXXの西澤 央貴です。

皆様のおかげで、NuxtMeetUpも第9回目を迎えました。 そこで今回は、今までのNuxtMeetUpで協賛頂いた企業様をあつめ、各社からLT登壇していただくという形を試みました。

今回のスポンサーである株式会社メルペイ様のご協力のおかげで過去最大規模である300人定員のNuxtMeetUpを行うことが出来ました。ありがとうございました!

会場

会場は六本木ヒルズ森タワーの18階にてやらせていただきました。

f:id:hiroki-nishizawa:20190827112358j:plain

今回300人定員のなか120人ほど当日キャンセルが出てしまい、参加者自体は220人ほどでしたが改めて見てもすごい人数だったなと思います。暑い中ご参加いただき本当にありがとうございます。

発表

今回スポンサー枠のメルペイさん含め8人の方にLTをしていただきました。全てを書くと長くなってしまうためいくつかピックアップして書いていきたいと思います。

Nuxt.js+TypeScriptのベストプラクティスを考えてみる

株式会社サイバーエージェント様からはTanaka yuiさんがご登壇してくれました。

f:id:hiroki-nishizawa:20190827112442j:plain

speakerdeck.com

最近良く耳にするTypeScriptとNuxt.jsでのAPI周りで別ドメインAPIを叩く際にいい方法はないのかということについて話してくれました。Nuxt.jsでBFF APIを持つという話があり良さそうだなと思いました。

Nuxt.jsとテストコード

株式会社エス・エム・エス様からはkeitaさんが話してくださいました。

f:id:hiroki-nishizawa:20190827112531j:plain

speakerdeck.com

スライド見ていただけるとわかると思いますが、Nuxt.jsでのテストの種類・目的・書き方についてお話してくれました。ComponentsやVuex、E2Eのテストについえてそれぞれサンプルコードもあるのでとてもわかり易いなと思いました

Nuxt マイクロサービスを3つに分割した話

株式会社メルペイ様からはtanakaworldさんがご登壇してくれました。

f:id:hiroki-nishizawa:20190827112609j:plain

speakerdeck.com

プロダクトチームの粒度に合わせてマイクロサービスを分割したという実例をもとに話してくださり、今後同じようにマイクロサービスを分割しないといけない課題にあたっている方には貴重なセッションだったのではと思います。

スライドまとめ

今記事では細かく書くことが出来ませんでしたが、LINE株式会社様・株式会社ピースオブケイク様・株式会社ガイアックス様の代表として登壇してくださり本当にありがとうございました。

懇親会

ミートアップですので発表と同じくらい大事な懇親会の様子です。

f:id:hiroki-nishizawa:20190827113950j:plain

f:id:hiroki-nishizawa:20190827113959j:plain

100名以上の方が残ってくださりとても大盛りあがりでした。

まとめ

今回オールスターズということもあり大変豪華なメンバーにご登壇いただきとても濃い時間を過ごすことができました。

次回の開催はまだ未定ですが、詳細が決まり次第connpassで告知させていただきます。もしご協力していただける企業様がいましたら、ご連絡いただけますと幸いです。

最後に

現在、株式会社ROXXでは、エンジニア、デザイナーの募集をしております。

興味のある方は、是非下記からご応募お願い致します!

www.wantedly.com

www.wantedly.com

www.wantedly.com

スクラムマスターとしての考え方[CSM]

はじめに

こんにちは株式会社ROXXでスクラムマスターをやっている西澤 央貴です。

先日認定スクラムマスター(CSM)の研修を受けに行ってきました。講師は日本人で唯一の認定スクラムトレーナー(CST)である江端 一将さんの研修を受講しました。

この研修で最も学べたことはスクラムの知識だったりチームを自立させる方法とかではなく、スクラムマスターとしての考え方だったと思っています。そのため全てをこの記事に書ききることは出来ませんが、特に印象深かった考え方について書いていきたいと思います。

提案は論理的になっていて計測できなければならない

前提として基本的にはスクラムマスターがなにかの意思決定をする権限はありません。なのでスクラムマスターの発言は提案になり、提案するという行動は他の人の時間を使うという行為になります。そのため発言する際には責任を持たなければならないということを常々おっしゃっていました。

論理的ではない提案をしてしまった時、提案された側はたまたま同じアイディアを持っていない限り動こうとは思わないかと思います。

江端さんの話を聞いていて、相手が協力的に動いてくれる条件として以下になるのではと思いました。

  • 提案したアイディアの理由が理にかなっていて、それを実行したことによりチームがよくなっているということを計測できる仕組みになっている
  • チームとしてやりたいと思っていたものに矢印が向いていて提案したものはそこに辿り着くためのプロセスになっているとなおよし

トップダウンではないスクラムにおいて、上記2つが出来ているからこそチームが協力的に動いてくれるかと思っています。

最後の一人になっても考え続けなければならない

先程も話しましがスクラムマスターに決定する権限は基本的にありません。なので振り返りなどでtryが全然決まらない時に決め方を提案したり、決める前にもっといい案がないかを考えたりはスクラムマスターは行うと思いますが最終的に決めるのは開発チームになります。

ただ決まった後もスクラムマスターはそれよりもいい案はないのか、もっとチームを今よりよくする案は本当にないのかを考え続けなければいけないです。変わらないというのは現状維持なのでスクラムをマスターしている人の考え方として本当にいいのかということを考えさせられました。

スプリントレトロスペクティブは答え合わせ

江端さん曰く、スクラムの公式イベントにあるスプリントレトロスペクティブはスクラムマスターにとって答え合わせの時間。

スクラムマスターはチームの現状把握として課題は先に洗い出して置かなければなりません。チームの本質的な課題が分からなければチームがよりよくなる提案など出来ないです。そのため先にチームの現状を把握できるようにしておかなければいけません。そういう意味でスクラムマスターにとってスプリントレトロスペクティブは答え合わせなんだと思います。

飲み会の席で聞いて驚いたのですが、江端さんの頭の中には参加者36人の3日間のタイムラインが頭の中にはあると言っていました。

現状自分の力だと頭の中にメンバー全てのタイムラインを作成することが出来ないので、現在はチームのタイムラインはチームに貼ってもらっています。それとは別で個人のタイムラインを自分が作成してスプリントレトロスペクティブまでに課題を探し出してリストを作成するという取り組みを行っています。

CSMの感想

結果的には1日目に出された議題に対して最後まで結論が出ずにCSMは終了してしまいました。1日目の議題から課題が膨れ上がっていく中で、その一つ一つの課題に対してとにかく論理的に解を出すことが求められました。「その解によって本当にチームがよくなると思っているのか」「発言する時には36人全員の手を止めることなるがその責任を持って発言しているか」「チームが協力的に動いてくれるにはどのような提案をすればいいのか」を常に考え続ける3日間でした。

自分がスクラムマスターを学ぶきっかけになったのは、開発チームの人数が多くなったため「1チームだったのを2チームに分けたほうがいいよね」「1チームだったらなんとかなるけど2チームをPOだけで見るのは辛いよね」ということからスクラムマスターを置くことになりスクラムマスターを任命され今に至ります。

江畑さんの言葉になりますが「上司からスクラムマスターを任命されたからスクラムマスターになれるのではなく、スクラムマスターとしての動きをしていて、周りからスクラムマスターみたいだねと言われて初めてスクラムマスターになれる」とおっしゃっていました。自分はまだ前者になるので周りからスクラムマスターだねと言われるように、「論理的に考えられているか」「他によりよくなる方法は本当にないか」を常に考え行動していきたいと思います。

ただ論理や指標についてずっと頭を使い続けた3日間。研修期間中はもちろんでしたが、研修終わって2週間経った今でも夢に出てくるのはやめてください江端さん。。。

最後に

チームが成長していくにあたり、これからもメンバーを増やしてもっと生産性の高いチームにしていきたいと思っています。

新規事業や既存事業の拡大も考えているため自分の力で事業を成長させたいエンジニアを絶賛募集中です!

興味のある方は下記からご応募いただくか、こちらまでご連絡ください!!

www.wantedly.com

www.wantedly.com

yarn workspace で複数パッケージを同一レポジトリで管理する

SARDINEエンジニアの @jiskanulo です。jiskaと書いて「ゆうすけ」と読んで欲しい中二病を20年近く患ってるんですが誰にも伝わらないのが最近の悩みです。

SARDINEでは、たとえばお客さまが見る画面やROXX社内の人間が操作する管理画面、それらで共通で参照するパッケージ、API通信をやりとりするサーバー...などなど様々な用途のレポジトリが存在していますが、現在それら複数パッケージを同一レポジトリで管理する Monorepo 化を進めています。

MonoRepo化を進めているかの詳細はCTO @kotamats が登壇しました Monolith→MultiRepo→MonoRepoにいく上でのリポジトリ戦略 もご覧いただけますと幸いです。

この記事では yarn workspace を用いてMonorepoを新規構築するための手順をまとめます。

また、今回の記事を通して作成したレポジトリは https://github.com/jiska/yarn-workspace-example にpushしています。

yarn workspace

yarn workspace は複数パッケージを一つのレポジトリで管理するための機能です。 それぞれのパッケージでpackage.jsonを保持しつつ、yarn.lockはプロジェクトのrootに1つだけ作られるようになります。

workspaceを有効にするためにpackage.json"private": true, "workspaces": ["packageのパス"] の記述を追加します。

{
  "name": "yarn-workspace-example",
  "version": "1.0.0",
  "main": "index.js",
  "author": "yusuke mori <210861+jiska@users.noreply.github.com>",
  "license": "MIT",
  "private": true,
  "workspaces": [
    "packages/*"
  ]
}

パスにはワイルドカード指定ができるのでこの場合は packages ディレクトリ以下すべてのディレクトリがworkspace管理の対象に含まれます。

パッケージを追加する

package-one , package-two とパッケージを追加してみます。package-oneはNuxtをただ追加しただけの素ページ、package-twoは vue create で作成したページです。

package.json, yarn.lockは以下のように配置されます。

.
├── package.json
├── packages
│   ├── package-one
│   │   └── package.json
│   └── package-two
│       └── package.json
└── yarn.lock

yarn workspace <workspace_name>

各パッケージのディレクトリにcdせずにプロジェクトrootから各パッケージに向けて yarn runyarn add を行うことができます。

詳細は https://yarnpkg.com/en/docs/cli/workspace をご確認ください。

yarn workspace package-one devyarn workspace package-two serve している様子をAsciinemaにアップロードしています。

asciicast

git pre-commit hookを調整する

pre-commit hookの調整に Huskylint-stagedESLint を追加します。

これらはプロジェクト全体で利用したいのでプロジェクトrootのpackage.jsonに追加します。 追加するためには -W オプションを付与する必要があります。

yarn add --dev -W husky lint-staged eslint

pre-commit hookが動作している様子をAsciinemaにアップロードしています。

asciicast

今回は新規パッケージを作成するため0からレポジトリを構築しましたが、既存のレポジトリを追加したり npm publish の手順については触れませんでした。

それらのニーズを叶えるには Lerna を使うとよさそうです。LernaとYarn workspaceは共存可能なので今回の記事の内容と合わせて試してみてください。

最後に

株式会社ROXXでは一緒に SARDINEback check を作っていくメンバーを随時募集しています。 この記事を読んでROXXに興味を持ってくれた方はぜひご応募ください。   www.wantedly.com

v-for 内でコンポーネント、クラス名、イベントハンドラまで動的に指定する

ROXX エンジニアの匠平(@show60)です。

同じようなタグが繰り返されているのを見ると、どうにかスッキリまとめられないもんかなと思いますよね。

今回はアイコンコンポーネントを例に、 v-for 内で動的な指定と、クラス名、イベントハンドラの設定までやってみようと思います。

v-for のループ内で任意のカスタムタグを指定する

ユースケースとしては、バラバラのコンポーネントを並べたいときなどでしょう。

1 つのアイコンのみを含んだコンポーネントがあるとして、並びに規則性のある場合には 1 つずつタグを置いてスタイルを設定するよりもコード量が減ってスッキリします。

下記のコードを例にすると、各アイコンコンポーネントが並ぶとき、コードの見通しはいいのですが同じクラス名が入ってくると冗長になりがちなのですっきりとさせたい気持ちです。

<icon-news-feed class="icon"/>
<icon-my-page class="icon"/>
<icon-settings class="icon"/>

配列の要素名と同じアイコンを指定する

下記のようなアイコンの名称が入った配列を用意します。

// 並べたいアイコン
icons: [
  'news-feed',
  'my-page',
  'settings'
]

テンプレートでこのように記述します。

// template
<ul>
  <li
    v-for="(icon, index) in icons"
    :key="index"
  >
    <component 
      :is="icon-${icon}"
    />
  </li>
</ul>

Vue の is 属性を使うことでコンポーネントを動的に生成できるため、単純な v-for だけで表現できます。

注意が必要なのは、当然ですがこれらのアイコンコンポーネントの import は個別に必要ということです。

ここでは 3 つのコンポーネントを例にしていますが、もっとアイコンが増えると利用価値も十分あるかと思います。

また、このアイコンを並べるコンポーネントを用意し、並べたいアイコン名を配列にして親コンポーネントから props で渡すだけで指定することができます。

API — Vue.js

component タグにクラス名を追加すると、呼ばれているアイコンコンポーネントにクラス名が追加されます。こちらも動的に指定できますので例を挙げてみます。

クラス名を動的に指定

ナビバーやフッターなどでは、上記のようにアイコンを並べることがあるかと思いますが、現在のページのアイコンのみ色を変えたいですね。その場合は、動的にクラス名を指定すればよさそうです。

// template
<ul>
  <li
    v-for="(icon, index) in icons"
    :key="index"
  >
    <component 
      :class="getClass(icon)"
      :is="icon-${icon}"
    />
  </li>
</ul>

...

<script>
// import で各アイコンコンポーネントを呼び出し、 export 内で components に記述する

export default {
  methods: {
    getClass(iconName) {
      const path = this.$route.path
      if (iconName === path) {
        return 'is_selected'
      }
    }
  }
}
</script>

// style で is_selected に任意のスタイルを当てる

v-for のループ内で任意のイベントハンドラを指定する

Vue.js 2.6 からディレクティブの引数を動的に指定できるようになりました。

テンプレート構文 — Vue.js

要するに v-bind:●●="something" , v-on:●●="something()" の●●を動的に指定できるのですが、ユースケースとしてはあまり多くないのかなという印象です。

上記で挙げている例でいうと、ある条件下ではアイコンの click 時にイベントが発火し、別の条件下では mouseover でイベント発火するようなケースですね。サイドメニューのサブメニューの開閉を切り分けたいときなんかには使えそうな印象です。

他には下記のように、アイコンが示すページ内にいるときにはイベントハンドラを与えないという処理には使えそうです。

// template
<ul>
  <li
    v-for="(icon, index) in icons"
    :key="index"
    @[getEvent(icon)]="handleEvent()"
  >
    <component 
      :is="icon-${icon}"
    />
  </li>
</ul>

...

<script>
export default {
  methods: {
    getEvent(iconName) {
      const path = this.$route.path
      // 今開いているページのアイコンにクリックイベントを与えない
      if (iconName !== path) {
        return 'click'
      }
    },
    handleEvent() {
      console.log('何かを発火!!')
    }
  }
}
</script>

叩いたメソッドの handleEvent 内で分岐することが多いかもしれませんが、このように書くこともできそう、という所感です。

PHPStorm と VSCode のデフォルト設定では、イベント名 ( handleEvent() のところ) にシンタックスハイライトが効かないので、コードの読みやすさの面ではあまり良くないですね。

この用途ではコード量は減らないため、別のイベントハンドラを与えたいときに使う、が有用のようです。

動的引数の値の成約

ドキュメント内にもありますが、動的引数の指定にはいくつか制約があります。上記のケースで、 method で @[getEvent(icon)] のように指定せず直接記述したいと思いましたが、下記のような記述はできません。

@[ ] 内には演算子は書けないようです。

// 不正な記述
@[$route.path !== icon ? 'click' : '' ]="doSomething()"

例えば icon: {event: 'click'} のようなオブジェクトが渡ってきたときに、 icon.eventclick を指定したいところですが、こちらも同様に記述できません。そのため上記のように method で与えてあげる必要があります。

// 不正な記述
@[icon.event]="doSomething()"

制約が多いため用途は限定的かもしれません。

動的引数の式には構文上の制約があります。というのも、スペースや引用符のような一部の文字は、HTML の属性名としては不正な文字だからです。また、in-DOM テンプレートを使う場合は、大文字のキーも避ける必要があります。 テンプレート構文 — Vue.js

さいごに

コンポーネントからクラス名、イベントハンドラまでを動的に配置してみましたが、イベントハンドラについては用途が限られる印象なので、開発の大きな助けになるイメージではなさそうでした。良い事例があれば教えていただけるとありがたいです。

最後に、弊社 ROXX では自社プロダクト一緒に成長させていくエンジニアを募集しています!

ご興味をお持ちの方はぜひご連絡ください!

www.wantedly.com

www.wantedly.com

理想の開発組織に沿った行動を表彰しました!

こんにちは kotamat です。

弊社の開発チームでは、理想の開発組織像というものを定義しております

techblog.scouter.co.jp

詳細は上記リンクを参考にしていただきたいですが、それを実現するために3つのバリューを設定しております。

  • ROCK
    • 個々のプレゼンスが組織のプレゼンスとなる
  • JAZZ
    • 自分史上最高のプルリクを出す
  • PROGRESSIVE
    • 好奇心を持って体系化する

弊社では3ヶ月に一回の評価を行っており、その評価において上記バリューを体現した人を選定し、表彰することにしました。

もともとはバリューの評価は給与に反映していたのですが、プロダクトが複数にまたがっていく中で、どういったバリューが評価されるのかが埋没化され、バリュー自体が形骸化されてしまうことを懸念しておりました。

他のメンバーにもわかる形で表彰することにより、他のメンバーも「こういうことをすれば評価されるのか」というのを認識できるようになるため、今Qからは表彰することになりました。

それでは早速ですが、受賞者を紹介させていただきます!

2019年第3Qの評価者はこの3名!

ROCK賞: @masaakikunsan

f:id:kotamat:20190807142218j:plain

SCOUTER Conference vol.01 - connpassを主体的に運用していたり、NuxtMeetup - connpassや外部勉強会への積極的な登壇、また他のプロダクトのフロントエンドの技術力底上げなど、多方面で存在感を発揮していたところが ROCK でした!

本人からのコメントです。

ROCK賞あざます!僕は僕らしく生きていただけですが、表彰されて嬉しいです!(イキり) 会社の今後と自分のやりたいことを上手く紐付けて今後もROCKしていきたいと思います! イベント系に関してはメンバーの協力がないとできなかったのでこの場を借りて感謝の気持ちを伝えたいと思います、ありがとうございました!

JAZZ賞: @ktraoy

f:id:kotamat:20190807142752j:plain

システムの大改修のプランニングをするために、先日の開発合宿をしていたのですが、大改修でやることの物量が読めない中、臨機応変に合宿でのアウトプットを最大化するために動いていた点が JAZZ でした!

本人からのコメントです。

Jazz賞の表彰いただきありがとうございます。 前Qからスクラムマスターとしての役割に取り組み始めた所なので、このように評価をしていただけたのは、チームのおかげだと思ってます。 また合宿の活動の前後で共に進行役を担ったPOとスクラムマスターの協力のおかげでもあります。ありがとうございます。 まずは、プロジェクトがまだ完了していないので、プロジェクトの成功に取り組みます。その後も事業の成功のためにチームとPOをサポートしていけるように、自己研鑽を積んでいきたいと思ってます。

PROGRESSIVE: @tsmd44

f:id:kotamat:20190807143013j:plain

SARDINEのモノレポ化やk8sの提案など、今抱えている技術課題を新しい技術の導入によって解決に向かえている点が PROGRESSIVE でした!

本人からのコメントです。

この度、Progressive賞をいただき大変うれしく思います! 業務委託にも関わらず、モノレポ化など通常の開発以外にも様々な業務を任せていただけているのは、POをはじめメンバーの理解とサポートのおかげです。 プロダクト的にも開発的にも技術で解決できる課題はまだまだあると感じており、開発スピードをさらに加速できるよう日々研鑽に励んでまいりたいと思います。 引き続きどうぞ宜しくお願いいたします!

最後に

f:id:kotamat:20190807143246j:plain

最後に表彰式に参加したメンバー全員で記念撮影。 次回は他の人も受賞できるよう、バリューの体現を期待しております!

株式会社ROXX ✕ ドラッカー風エクササイズ

はじめに

こんにちは株式会社ROXXのhirokinishizawaです。

いきなりですが現在SARDINE開発チームの体制はこの様になっています。

f:id:hiroki-nishizawa:20190730191203p:plain

もともと3月から開発チームは2チームあったのですが人数も少なかったというのもありスクラムマスターというロールはいませんでした。両チーム人数も増えてきて6月からスクラムマスターを入れるということになりました。

スクラムマスターになってから1ヶ月程たって、メンバー間やチーム間で価値観や期待することに対して疑問も上がってきたので「ドラッカー風エクササイズ」を行いました。

ドラッカー風エクササイズとは

ドラッカー風エクササイズはアジャイルサムライという本にある、チームにおける期待をすり合わせるための手法です。たった4つの質問を通じて、お互いの考えや価値観、期待のすり合わせを行います。

  • 自分は何が得意なのか?
  • 自分はどういうふうに仕事をするか?
  • 自分が大切に思う価値は何か?
  • チームメンバーは自分にどんな成果を期待していると思うか?

たった4つの質問と書きましたが回答する側は結構考える内容になっていると思います。

自分が得意なことは結構出てくるイメージですが、大切に思う価値やチームメンバーからどんな期待をされているかはすぐに出てこない人も多いかと思います。

株式会社ROXX ✕ ドラッカー風エクササイズ

スクラムチーム全体でのチームビルディングということもあり、今回は少しカスタマイズをして各チーム内と、ロール間という2パートを用意しました。

チーム内

以下の質問でチーム内での期待することを各々チームで認識をあわせました。

  1. 自分が大切に思う価値は何か
  2. 自分がどうやって貢献していくつもりか
  3. 他のメンバーに期待していることは何か

f:id:hiroki-nishizawa:20190801145522j:plain

各々発表する時間をとってメンバー間での期待すり合わせを行ってもらいました。「他のメンバーから期待されていたことを見て新しい発見があったか」や「メンバー間での大切にしていることの違い」など見てもらいます。

ロール間

以下の質問で各ロールに期待することを全体で認識をあわせました。

  1. 自分たちのロールが大切にする価値は何か
  2. 自分たちのロールが期待されていることは何か
  3. 他のロールに期待していることは何か

f:id:hiroki-nishizawa:20190801145553j:plain

ロール間で期待のすり合わせを行った理由はスクラムマスターの役割はなんなのかという話があがったためパートを用意しました。

スクラムチームの成熟度によってスクラムマスターの役割も変わっていくため、現段階でチームでどのような期待をされているのか。また「PO→スクラムマスター」「各開発チーム→スクラムマスター」だけではなく、「PO→各開発チーム」「各開発チーム→PO」も行うことで、チーム間で差分があるのかを改めて認識できました。

まとめ

今回はスクラムチーム全体でやりましたが、チーム毎にチームの成熟度は違うため、チームのフェーズによってカスタマイズを行っていくのが良いと思います。

新しいメンバーが入ってきた時などチームビルディングとしてドラッカー風エクササイズは有効なので、みなさんもぜひ試してみてください!

さいごに

チームが成長していくにあたり、これからもメンバーを増やしてもっと生産性の高いチームにしていきたいと思っています。

新規事業や既存事業の拡大も考えているため自分の力で事業を成長させたいエンジニアを絶賛募集中です!

興味のある方は下記からご応募いただくか、弊社CTO:kotamatまでご連絡ください!!

www.wantedly.com

www.wantedly.com

LaravelでIP制限機能の実装

はじめに

こんにちは、株式会社ROXXの開発責任者の小平(@ryotakodaira )です。 業務では、SARDINEという人材紹介会社向けの業務管理システムを開発・運用をしています。

規模の大きい人材紹介会社がSARDINEを利用するにあたって、システムの利用時に 自社のIPからのみアクセスを許可 したいという開発案件が発生したため、備忘録的に開発としてどのような対応を行ったのかを残そうと思います。

準備

今回の開発で達成したいこととしては、

「システムの利用時に 自社のIPからのみアクセスを許可 したい」となっており、

ユーザー毎に自社のIPアドレスを設定できるようにすることでした。

当然ですが、認証を先に済ませないとどのIPアドレスを許可するかの判断をシステム側で行うことができないため、認証済みのリクエストが来たときに必ずIP制限の評価が走るミドルウェアを実装することとしました。

早速、ミドルウェアを作っていきます。

Laravelのartisanコマンドでミドルウェアクラスのファイルを作ることができるので、そちらを利用します。

$ php artisan make:middleware CustomIpLimitation

ミドルウェアの実装

<?php

namespace App\Http\Middleware;

use App\Entities\User;
use Closure;
use Symfony\Component\HttpFoundation\IpUtils;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;

/**
 * Class CustomIpLimitation
 * @package App\Http\Middleware
 */
class CustomIpLimitation
{
    /**
     * @var array
     */
    // ①
    const ACCEPTED_IPS_GROUP_BY_USER = [
        // user_id => ['127.0.0.1/0', '127.0.0.1/1']
        1 => [
            '127.1.1.1/32',
            '127.2.2.2/32',
        ],
    ];

    /**
     * @see https://ip-ranges.amazonaws.com/ip-ranges.json
     */
    // ②
    const CF_IPS = [
        // CloudFrontのIPレンジ
        '13.124.199.0/24',
    ];

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request $request
     * @param  \Closure $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        // ③
        if (!app()->runningUnitTests() && !app()->environment('production')) {
            return $next($request);
        }

        /** @var User $user */
        $user = $request->user();

        // ④
        $allowedIps = $this->allowedIps4AuthenticatedUser($user->id);

        // ⑤
        if (empty($allowedIps)) {
            return $next($request);
        }

        // ⑥
        $request::setTrustedProxies(
            [$request->server->get('REMOTE_ADDR')] + self::CF_IPS,
            $request::HEADER_X_FORWARDED_AWS_ELB
        );
        $clientIp = $request->ip();

        // ⑦
        if (!IpUtils::checkIp($clientIp, $allowedIps)) {
            throw new AccessDeniedHttpException('IPNotAllowed');
        }

        // ⑧
        return $next($request);
    }

    /**
     * @param int $userId
     * @return array
     */
    protected function allowedIps4AuthenticatedUser(int $userId): array
    {
        return self::ACCEPTED_IPS_GROUP_BY_USER[$userId] ?? [];
    }
}

実装内容について順を追って説明していきます。

ACCEPTED_IPS_GROUP_BY_USER という定数に、 IP制限を設定するユーザーIDをkey、許可したいIPアドレスの配列をvalue 、とした配列を定義します。

本来はこれらの情報は何らかのデータベースで永続化し、都度データベースからデータを引いてくるべきですが、本投稿では定数に定義する形で進めます。

CF_IPS という定数に、CloudFrontのIPレンジを設定しています。

弊社のサービスはCloudFrontを使用しているため後々の処理でCFのIPレンジが必要となります。

こちらも本来は定数として定義するのではなく、都度、以下のURLを参照するなどをして最新のIPレンジを取得するようにした方が良いでしょう。

https://ip-ranges.amazonaws.com/ip-ranges.json

(体感ですが、CFのIPレンジは1,2週間に1度くらいのペースでアップデートがかかります。)

こちらはあってもなくてもどちらでも良いですが、開発中にIP制限に引っかかってしまい非常に面倒だったため、開発中はこの機能を無視するようにしています。

phpunit実行時, 本番環境以外では機能を無視するようにしています。

で定義したIPリスト( ACCEPTED_IPS_GROUP_BY_USER )をクライアントからリクエストを送信したユーザーIDで検索して、そのユーザーIDに対してIP制限が設定されていた場合は、許可するIPアドレスの配列を返却しています。

で取得した許可するIPアドレスの配列が空の場合は、IP制限を設定していないものとみなしそのままミドルウェアの処理を抜けます。

クライアントのIPアドレスを取得する前に setTrustedProxies を行い、サービス提供者が直接管理しているリバースプロキシのリストをセットします。

セットすべきリバースプロキシの内容はサービスのインフラ構成により異なってきますが、弊社のサービスの場合は CF -> ALB -> EC2 となっているため、

self::CF_IPS で最初に定義したCFのIPレンジを取り、 $request->server->get('REMOTE_ADDR') で直前(ALB)のIPレンジを取ってそれらをセットしています。 ここは完全にインフラ構成に依存していますが、そのことを最初は考慮できていなかったため割と躓いたポイントです。

その後、 $request->ip() で正確なクライアントIPアドレスを取得することができます。

クライアントのIPアドレスが許可されたIPアドレスの範囲内にあるかを検証しています。

SymfonySymfony\Component\HttpFoundation\IpUtils::checkIp メソッドを使えば一発で評価することができます。

許可されたIPアドレスの範囲内になかった場合はその時点でエラーをクライアントに対して返却しています。

クライアントのIPアドレスが許可されたIPアドレスの範囲内であれば、正常とみなしミドルウェアを抜けて終了となります。

後は app/Http/Kernel.php などで今回作成したミドルウェアを通るように設定してあげれば完了です。

最後に

事業・サービスが成長していくにあたって、これからもメンバーを増やしていきたいと思っています。

新規事業や既存事業の拡大も考えているため自分の力で事業を成長させたいエンジニアを絶賛募集中です!

興味のある方は下記からご応募いただくか、@ryotakodairaにご連絡ください!!

www.wantedly.com

www.wantedly.com