ストーリー詳細化を交えた agent bank 開発チームの現在の開発手法

こんにちはみなさん ROXXのagent bank開発チームのniisan-tokyoです。

開発手法は、それこそ各チームで千差万別でだと思います。そこには、メンバーのスキルの特性や事業・プロダクトの性格など様々な事情が入り込んでいます。 メジャーなフレームワークであるスクラムも、スクラムガイドとセレモニーはあれど、細かい進め方は各チームによって変わるのではないかと思います。 今回は我々のチームの現在の開発手法を解説しますので、皆様の参考になればと思います。

現在の開発の流れ

現在の我々は一週間をスプリントの単位としており、その流れは以下のようになっています。

f:id:niikura23:20201014151453p:plain
1スプリント(一週間)の開発の流れ

ある程度はスクラムの流れを踏襲しているのですが、割と特徴的なやり方がふくまれているので、その点を解説します。

ストーリー詳細化

これはバックログにあるユーザストーリーをタスク分解できる状態に持っていく作業となります。 ここで詳細度を上げたストーリーを金曜日のプロダクトバックログリファインメントで扱うようにします。

ストーリー詳細化の詳しい説明はあとに回しますが、基本的には月曜日と火曜日に実施し、水曜日と木曜日は予備日に当てます。 担当者はスプリントごとに変わりますが、月曜・火曜で完了したのであれば、スプリントタスクに合流します。

スプリントのタスク

基本的には実装のタスクとなります。 ここで実現されたユーザーストーリーを、インクリメントとしてスプリントレビューに持ち込みます。

スプリントレビュー

実現できたストーリーをステークホルダに見せて、出来た機能と運用のイメージを解説し、フィードバックをもらいながらリリースのタイミングについて相談します。

振り返り

スプリントを振り返り、次のスプリントをより良くするために取るべきアクションを決めます。 我々のチームでは、オーソドックスな KPTA(Keep, Problem, Try, Action) を採用しています。

リファインメント

プロダクトバックログリファインメントのことですが、我々のチームではユーザーストーリーの詳細度を上げ、プランニングレディな状態に持ち込むためのイベントになっています。 我々のチームではマルチチームリファインメントというやり方を導入しており、この手法は以下のようなタイムボックスを設定して進められます。

  • 15min: ストーリーごとに小チームに別れ、ストーリーについての質問事項と、ストーリーを実現するためにやるべきことのリストを作成する
  • 20min: すべての小チームが集まり、各小チームごとに質問をPOに行い、やることを解説して他のチームからの質問・指摘を受け付ける
  • 15min: 再び小チームに別れ、ストーリーに対するやるべき事の追加・修正を実施し、新しい質問事項を集める。また、可能であればこの時点でストーリーポイントをつける
  • 20min: 再びすべての小チームが集まり、各小チームごとに質問をPOに行い、やることを解説して他のチームからの質問・指摘を受け付ける
  • 10min: やるべきことの修正を行い、まだであればストーリーポイントを付け、ストーリーを実現するためのタスクを作成する

プランニング

次のスプリントで、どのくらいストーリーを実現できるか、どの程度のタスクを実行できるかを見積もり、どのように進めるかを議論します。 これは、以下のように進めています。

  • POによる優先度に基づく並べ替え ( ざっくりストーリーごと単位 )
  • ストーリーに紐付かないタスクにポイントを付ける ( ストーリーから分割したタスクはポイントを付けない )
  • タスクをより効率に進めるために、優先度を壊さないレベルでの並べ替えと進め方の議論
  • 各日にちごとのチェックポイントを作成

チェックポイントは実際のタスクとして作っておいて、各日毎の進捗の様子を可視化できるようにします。

f:id:niikura23:20201014155952p:plain
Jira上でチェックポイントの作成

ユーザーストーリーの詳細化

我々の開発で大きな特徴があるところとすると、スプリント内にこのユーザーストーリーの詳細化を入れているところです。 これは設計のような作業で、なにをやればいいかの、どうなったら完了になるかを定義し、ユーザーストーリーをリファインメント可能な状態にする作業になります。

ユーザーストーリー

ユーザーストーリーとは、プロダクトに追加したい機能を、「〇〇が□□する」というユーザーの行動で記述したものになります。 例えば、プロダクトにメモ機能を導入したい場合は「ユーザーはプロダクト上でメモを残す」のようなストーリーが作られます。

我々のプロダクトにおいて、ユーザーストーリーは述語だけでなく主語もとても大事です。 というのも、我々のプロダクトである agent bank は、求人DBという性格上、主語となりうる人物が、エージェント様、求人企業の人事様、そして弊社の運営事務局と、少なくとも3者以上いるわけです。 そして、主語が誰になるかで作るものもガラリと変わります。 例えば、通知機能一つとっても、エージェント様であればメールでの通知になりますが、運営事務局だとslackになるなどですね。

なぜユーザーストーリーの詳細化の時間をとるのか

本当に大雑把なストーリーはPOが作ります。 しかし、現在POはほとんどコードをさわらないのと、圧倒的に時間が足りないということから、ある程度実装イメージが付いて、どこまで詳細化すればリファインメントできるかがわかる開発チームが、詳細化を担当することになります。 この作業にはまとまった時間が必要になりますので、開発チームから数人選出して、ストーリーの詳細化を集中的に実行するようになりました。

ユーザーストーリーの詳細化の作業内容

ユーザーストーリーの詳細化の方法は、ストーリーによって大きく変わりますが、リファインメントするのに必要な作業としては以下のようなものになります。

  • 背景の整理
  • 受け入れ要件の作成 ( 完了条件の定義 )
  • 画面イメージの作成
  • 指標の策定
  • ステークホルダとの事前共有

背景の整理

ストーリーを実現すると、何がいいのかというのを整理します。これをやっておかないと、どういった戦略に基づくものなのか、どのようなことができればよいのか、誰がステークホルダなのかが何もわからないです。 あらゆる作業の土台になるため、凄まじく重要です。

受け入れ要件作成、画面イメージの作成

受け入れ要件は、ユースケースのリストで定義され、これがすべて満たされれば、ストーリーを実現できたとみなせるものです。 例えば、「ユーザーはメモを残せる」というストーリーの受け入れ要件を考えると、

  • ユーザーはメモ作成画面でメモを入力し送信する
  • ユーザーはメモ閲覧画面でメモを閲覧できる
  • ユーザーはメモ閲覧画面からメモ編集画面に移動できる
  • ユーザーはメモ編集画面でメモを編集できる

といった感じです。 また、これらとともに画面イメージも作成し、実際にユーザがどのように使うかをシミュレートできるようにします。

指標の作成

ストーリーが実現できても、ちゃんとユーザーが使って価値を届けられているかわからないといけないので、上手く行ったかどうかを評価するための指標を作成する必要があります。 先のメモを残すであれば、メモを残しているユーザーが全体の何%いるか、などですね。

ステークホルダとの事前共有

これからこんな物を作りますよ、というのをステークホルダの方々に共有します。 作ったはいいが、いざレビューで「なにこれ?」ってなったら大変なので、実装する前段階でステークホルダと共有し、フィードバックをもらっておきます。

現在の所感とまとめ

というわけで、現在我々が開発の流れを、大雑把に解説しました。

今のところはいい感じにワークしているようにも思いますが、これがベストかと言われると全くそういうわけでもなく、なにか新しいトライがでてくればどんどん試していきたいと思っています。

とはいえ、ベストの手法なんて、本当にあるのでしょうかね?私としては、各チームごとの特性に従って、改善を繰り返して限りなくベストに近づけていくというのが、やはり常道だなって思っています。

今回はそんなところです。

おまけ

今の状況下でも弊社はエンジニアの採用もやってます。 リモートでカジュアル面談出来ますので、興味ある方は私のtwitterにDMなりメンションつけてコメントしてくれれば、つなぎますので、お気軽にどうぞ。

twitter.com

Vue3 について語る会をした

ROXX backcheck 事業部でエンジニアやってる、秋葉です!

祝Vue.js3リリース

弊社の2つのプロダクト、agent bankback check ともに、フロントエンドの技術として、 Vue.js を採用しており、Vue3への対応はROXXとして必ず課題となっていきます。

Vue3を語る会をした

f:id:akki_megane:20201012210712p:plain ↑の経緯がありVue3について、「ROXX全体でキャッチアップが必要だよね!」 って感じで企画し、Vue3についてのお勉強しようの会をしました。

内容としては

  • Vue3 の新機能について
  • なくなる機能について
  • Nuxt への対応
  • ROXX として Vue3にどう取り組んでいく?

当日は以下の資料を使って、用意したサンプルコードを適時動かしながら 進めました。

スライド
docs.google.com

サンプルコード
Vue3の機能
https://github.com/BBCapMegane/hello-vue3

Vue3 + TS
https://github.com/BBCapMegane/vue3_training

スライド内容については、kazupon さんの資料を参考に作成させていただきました。
speakerdeck.com

感想と発見

感想から言うと、すごく楽しかったです!

当日は自分が中心で話していたんですが、質問や疑問は随時受け付けていたので発表というよりは、ディスカッションに近い形で実施できたことが大きかったです。

実際のデモでコードを動かしていくなかで、「これ変えたらどうなる?」とか、 「この機能はこういうところに使えるよね!」などなど、
いろんな意見が出てきてとても盛り上がました。

agentbank チームは、Composition API をすでにプロダクトに入れているので色々な知見を共有してれまたし、Vue3についても自分の知らない情報や、新機能の使い所など新しい発見ができました。

出た意見として、雑にメモった内容↓

  • Fragments は、ルートがなくなるのはとてもいいけど、直でCSSのスタイル当てられないくなるよね
  • Reactivity APIs
    • readonly は実行時しかわからないので、使いづらそう
    • watchEffect は副作用が可能
      • watchEffect 内の変更は検知しないので使いやすそう
      • watch みたいにループはしないのはいい
  • Composition APIの設計
    • 強みとしては、「処理の共通化」ができる
    • setup が肥大化しそう、書き方に工夫が必要そう
    • Composition API は設計判断が難しそう
  • Vue3とNuxtのIE対応について
    • 対応可否について、SaaS 企業としてこれはかなり気になる

まとめ

昨今の状況下もあり、弊社エンジニアは基本在宅勤務なので、今回はオンライン上での実施となりました。

オフィスに集まらず、完全にオンライン上で社内イベントをやるのは今回が始めてで、「自分がずっと喋っているだけだったら、きついなー」と思っていましたが、
当日は参加してくれた方々が積極的にディスカッションをしてくれたので、とても助かりました。

オフィスに出社しない状況で両開発組織、交流が薄れているので今後もこのような会を実施できればと思っています!

AWS lambdaでrcloneを動かしてGDriveをS3にバックアップする

CTO室情報システム担当の吉澤です!

AWS Lambda Custom RuntimeとCloudWatch Eventを使ってGDriveをS3にバックアップする自動化を行いました。

Custom Runtimeについては以下を参照にしてください。 docs.aws.amazon.com

ファイル構成

.
├── bootstrap
├── function.sh
├── jq
├── rclone
├── .rclone.conf

function.sh

function handler() {
  ID=`echo $1 | /var/task/jq -r '.id'`
  NAME=`echo $1 | /var/task/jq -r '.name'`
  DATE=`date +%Y%m%d`

  cp /var/task/.rclone.conf /tmp
  /var/task/rclone dedupe --dedupe-mode rename --drive-skip-shortcuts ${ID}:
  /var/task/rclone sync ${ID}: s3:roxx-gdrive-backup/sync/${NAME} --config=/tmp/.rclone.conf --fast-list --drive-alternate-export --drive-skip-shortcuts
}

bootstrapは以下を参照にしてください。 docs.aws.amazon.com

jsonを読み込むためのjqと、rcloneのバイナリも一緒にパッケージ化します。 --drive-alternate-export--drive-skip-shortcutsについては以下を参照してください。 rclone.org

.rclone.confはあらかじめローカルで設定したものをコピペします。

.rclone.conf

[gdrive]
type = drive
scope = drive
token = ***
team_drive = ***

[s3]
type = s3
provider = AWS
env_auth = true
region = ap-northeast-1
acl = private
storage_class = GLACIER

S3のポリシーは、S3 Permissionsの項のExample policyを参考に設定してください。 rclone.org

引数

{'id':'gdrive', 'name':'Googleドライブ'}

今回はIT監査対応でバックアップを作成しました。 rcloneはいろんなストレージに対応しているので、バックアップ手法を模索している方検討してみてください。

reviewdog で構文チェックや静的解析の結果をプルリクのレビューコメントとして出力する

こちらのブログは個人ブログと同じ内容です

www.ritolab.com


プルリクエストを作成した時に構文チェックや静的解析を走らせてソースコードのチェックを行っていた際に、検査に通らなかった場合は結果エラーと表示・通知されますが、その度に GithubActions / CircleCI / TravisCI などの結果画面へ遷移して検査に通らなかった箇所を確認するのって少しだけ面倒だったりします。

そんな時は、reviewdog を使って、検査結果をプルリクのレビューコメントに出力してあげると手間も減って少しだけ幸せになれます。

今回は、reviewdog で構文チェックや静的解析の結果をプルリクのレビューコメントとして出力してみます。

reviewdog

reviewdog は、レビューコメントを出力してくれるツールです。
https://github.com/reviewdog/reviewdog

検証環境

今回使用するツールは以下の通りです

  • reviewdog
  • Github Actions
  • PHP_CodeSniffer(構文チェック)
  • PHPStan(静的解析)

今回は Github Actions に reviewdog を導入して、プルリクエストで構文チェックと静的解析を走らせ、検査に通らないものがあった場合にその結果をレビューコメントとして出力したいと思います。

一部 PHP フレームワーク Laravel のセットアップが出てきますが、特に Laravel のコードは出てきません。プロジェクトルートで各検査を実行している事を明示するために残しているだけなので、他の FW やスクラッチでも設定としては問題ありません。

構文チェックのエラーをコメントに出力する

まずは、PHP_CodeSniffer にて構文チェックを行い、エラーがあればプルリクのコメントに出力するようにします。

reviewdog なしで通常の構文チェックを走らせる場合の Action は以下になっています。(以下すべて yml は該当部分のみ抜粋して表示しています)

.github/workflows/ci.yml

jobs:
  # 構文チェック
  lint:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: rito328/composite-run-steps-action-laravel-setup@v1 # Laravel setup
    - name: lint
      run: ./vendor/bin/phpcs --report=emacs --standard=phpcs.xml ./

(Laravel setup のところは、個人的に Laravel のセットアップをまとめている Composite Action なので、試す場合は削除して各々の定義を追加してください)

ここに reviewdog を導入してレビューコメントを出力するようにするので、以下のように変更します。

.github/workflows/ci.yml

jobs:
  # 構文チェック
  lint:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: rito328/composite-run-steps-action-laravel-setup@v1
    # 1. reviewdog の setup action を追加
    - uses: reviewdog/action-setup@v1
      with:
        reviewdog_version: latest
    - name: lint
      env:
        # 2. reviewdog が コメントを書き込めるように token をセットする
        REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      # 3. 構文チェックの結果を reviewdog へ渡す
      run: ./vendor/bin/phpcs --report=emacs --standard=phpcs.xml ./ | reviewdog -reporter=github-pr-review -efm='%f:%l:%c:%m'
  1. reviewdog の setup action を追加して reviewdog を導入しています
  2. reviewdog が コメントを書き込めるように GITHUB_TOKEN をセットしています
  3. 構文チェックの結果を reviewdog へ渡しています

ポイントとしては、コメントを書き込める用に環境変数 REVIEWDOG_GITHUB_API_TOKEN に GITHUB_TOKEN 渡してあげることがまず一つです。ただ結果を渡すだけでは、書き込みの権限がなくて失敗してしまいます。

2つ目は、lint 結果のフォーマットです。

通常のエラーレポートの出力フォーマットでは reviewdog 側で簡単に読み取ることができないので、--report=emacs を指定して、出力フォーマットを通常のものから変更しています。

その上で、コメントのフォーマットを reviewdog 側で指定しています。(-efm='%f:%l:%c:%m')

Reporting
https://github.com/squizlabs/PHP_CodeSniffer/wiki/Reporting

これで、構文チェックに引っかかったものが、プルリクのレビューコメントに出力されるようになります。

f:id:ro9rito:20200902095337p:plain

静的解析のエラーをコメントに出力する

次は、PHPStan にて静的解析を行い、エラーがあればプルリクのコメントに出力するようにします。

まず、reviewdog なしで通常の構文チェックを走らせる場合の Action は以下になっています。

.github/workflows/ci.yml

jobs:
  analyse:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: rito328/composite-run-steps-action-laravel-setup@v1
    - name: analyse
      run: ./vendor/bin/phpstan analyse

各々セットアップして、PHPStan を実行しているだけの、ベーシックな定義です。

これに、reviewdog を導入するために以下の記述に変更します。

.github/workflows/ci.yml

jobs:
  # 静的解析
  analyse:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: rito328/composite-run-steps-action-laravel-setup@v1
    # 1. reviewdog の setup action を追加
    - uses: reviewdog/action-setup@v1
      with:
        reviewdog_version: latest
    - name: analyse
      env:
        # 2. reviewdog が コメントを書き込めるように token をセットする
        REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      # 3. 静的解析の結果を reviewdog へ渡す
      run: ./vendor/bin/phpstan analyse --error-format=raw --no-progress | reviewdog -reporter=github-pr-review -f=phpstan

基本的な部分は構文チェックの時と変わりませんが、一つだけポイントがあります。

run: ./vendor/bin/phpstan analyse --error-format=raw --no-progress | reviewdog -reporter=github-pr-review -f=phpstan

reviewdog は PHPStan 用のメッセージフォーマットを既に持っているので、-f=phpstan とするだけで済むのですが、今回の書き方の場合、PHPStan 側のエラー出力がデフォルトでは読み取れないので、--error-format=raw を指定してエラーの出力フォーマットを変更してあげる必要があります。

PHPStan - Output Format
https://phpstan.org/user-guide/output-format

これで、静的解析に引っかかったものが、プルリクのレビューコメントに出力されるようになります。

f:id:ro9rito:20200902102925p:plain

補足

PHP_CodeSniffer と PHPStan のエラー出力のフォーマットをデフォルトから変更しましたが、基本的デフォルトでは検査結果は人が確認しやすいような書式で出力されるので、シンプルな出力に変更する事で reviewdog から読み取れるようにしています。

ただ、reviewdog 自体は checkstyle を読めるので、今回のように出力フォーマットを変更しなくても設定次第でいけるのかもというのは一応補足しておきます。(検証はしていません)

(とはいいつつ、-f=phpstan で使う場合、PHPStan の場合は raw フォーマット前提のようです)
https://github.com/reviewdog/errorformat/pull/22/files

まとめ

reviewdog を導入するとそのプルリクを出した際の検査結果がコメントが書き込まれるので、おそらく最も早くレビューのコメントがつく。という事を考えると、人が実際にレビューを行う前に基本的な部分が自動で指摘されるので、全体的なレビュー労力の削減にもなりそうですね。

本当はこういった類のものはプルリク出す前に全て潰してしまえたら最良ですが、漏れる時もありますし、reviewdog にかわいく(コメントに犬のアイコンが出てきます)指摘してもらうのも良いかもしれません。

Github Actions の複合ステップアクションを利用してアクションを分離・再利用する

こちらのブログは個人ブログと同じ内容です

www.ritolab.com


Github が 2020 年 8 月 7 日に、複合ステップアクションという機能をリリースしました。

GitHub Actions: Composite Run Steps
https://github.blog/changelog/2020-08-07-github-actions-composite-run-steps/

なかなか便利そうだったのでこれを試してみたいと思います。

複合ステップアクション(Composite run steps actions)

複合ステップアクションとは、1つのアクション内で複数のワークフローの実行手順を組み合わせることができる機能です。

よく使うアクションを1つにまとめておいて、それを必要な箇所で使用(再利用)することができます。

ワークフローを切り出しておけば、例えば別のリポジトリで同じような環境で開発を行ったときにそれを再利用できるという感じです。

https://docs.github.com/en/actions/creating-actions/about-actions#composite-run-steps-actions

検証環境

今回は、Laravel で構築した PHP アプリケーションの CI を回す辺りを想定してこの機能を利用してみたいと思います。

また、Github Actions では以下のチェックを走らせようと思います。

ワークフローの定義

まずは、従来の方法でワークフローを定義してみます。

今回の場合、Github Actions で CI を回す場合に必要なのは、アプリケーションのセットアップとユニットテスト用の DB のセットアップ、そして各チェックの実行です。

各チェックごとにジョブを分けて実行するとして定義すると、例えば以下のような感じになります。

laravel/.github/workflows/ci.yml

name: App check

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

jobs:
  # 構文チェック
  lint:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Copy .env
      run: php -r "file_exists('.env') || copy('.env.example', '.env');"
    - name: Install Dependencies
      run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
    - name: Generate key
      run: php artisan key:generate
    - name: Directory Permissions
      run: chmod -R 777 storage bootstrap/cache
    - name: lint
      run: composer lint
  # 静的解析
  analyse:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Copy .env
      run: php -r "file_exists('.env') || copy('.env.example', '.env');"
    - name: Install Dependencies
      run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
    - name: Generate key
      run: php artisan key:generate
    - name: Directory Permissions
      run: chmod -R 777 storage bootstrap/cache
    - name: analyse
      run: composer analyse
  # ユニットテスト
  tests:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Copy .env
      run: php -r "file_exists('.env') || copy('.env.example', '.env');"
    - name: Install Dependencies
      run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
    - name: Generate key
      run: php artisan key:generate
    - name: Directory Permissions
      run: chmod -R 777 storage bootstrap/cache
    - name: Create Database
      run: |
        mkdir -p database
        touch database/database.sqlite
    - name: Execute tests (Unit and Feature tests) via PHPUnit
      env:
        DB_CONNECTION: sqlite
        DB_DATABASE: database/database.sqlite
      run: vendor/bin/phpunit

そして CI を回すとそれらは成功します。

f:id:ro9rito:20200816090231p:plain

ただし、この yml の書き方の場合、ジョブごとに同じアプリケーションのセットアップが記述されていて冗長です。

ここを、複合ステップアクションで共通化していきます。

複合ステップアクションの作成

詳細な手順は公式に案内があります。
https://docs.github.com/en/actions/creating-actions/creating-a-composite-run-steps-action
これに沿って進めていこうと思います。

複合ステップアクションを作成するには、リポジトリが必要です。まずは、切り出すアクションを設置するためのパブリックリポジトリを作成します。

リポジトリを作成したら、そこに設置するアクションを作成します。

(ちなみに、適当に v1 というブランチを切って進めています。)

master
 \
    v1

Laravel をセットアップするアクションを定義します。

action.yml

inputs:
  name:
    description: 'Setup Laravel'
runs:
  using: "composite"
  steps:
    - name: Copy .env
      run: php -r "file_exists('.env') || copy('.env.example', '.env');"
      shell: bash
    - name: Install Dependencies
      run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
      shell: bash
    - name: Generate key
      run: php artisan key:generate
      shell: bash
    - name: Directory Permissions
      run: chmod -R 777 storage bootstrap/cache
      shell: bash

アクションを作成したらリモートリポジトリへ push します。

ラベルを付与する

リモートリポジトリへブランチを push したら、プルリクを作成して、そこで「v1」というラベルを付与(無い場合は作成)します。

f:id:ro9rito:20200816090524p:plain

これで準備は完了です。(ブランチを生かしておけばプルリクはマージしても大丈夫です。)

複合ステップアクションの利用

切り出したアクションを実際に利用してみます。

laravel/.github/workflows/ci.yml

name: App check

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: rito328/composite-run-steps-action-laravel-setup@v1
    - name: lint
      run: composer lint
  analyse:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: rito328/composite-run-steps-action-laravel-setup@v1
    - name: analyse
      run: composer analyse
  tests:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: rito328/composite-run-steps-action-laravel-setup@v1
    - name: Create Database
      run: |
        mkdir -p database
        touch database/database.sqlite
    - name: Execute tests (Unit and Feature tests) via PHPUnit
      env:
        DB_CONNECTION: sqlite
        DB_DATABASE: database/database.sqlite
      run: vendor/bin/phpunit

Laravel のセットアップの部分をまるっと以下に置き換えています。

- uses: rito328/composite-run-steps-action-laravel-setup@v1

「 rito328/composite-run-steps-action-laravel-setup 」はリポジトリ名で「 v1 」はラベルです。(v1 とか v2 とか以外が使えるかは試していません)

冗長な記述が無くなったのでとてもすっきりしました。

これを Github Actions で動かすと、複合ステップアクションが読み込まれ、アプリケーションのセットアップやチェックも行われている事が確認できます。

f:id:ro9rito:20200816090651p:plain

アクションを分離させて再利用できました。

まとめ

ドキュメント見ながらざっと試してみました。個人的な感想としては、

  • 切り出したアクションのリポジトリが private だと利用側で読み込めなかった。それはそうかという気持ち反面、同じオーナーなら許してほしい気持ち反面。
  • ラベルつけるのが面倒。ラベル(@v1)無しでもできないのかな。

といったところでした。

とはいえ、アクションを分離できて再利用できるのはとても有用だなと思います。

同じ環境のアプリケーションがどんどん増えていくような環境下だと、再利用が捗って活躍しそうですね。

ちなみにリリースはまだファーストステップみたいで、これからも機能強化が行われていくようです。 https://github.com/actions/runner/issues/646

現場からは以上です。

HSTSってなんだっけ

この記事は個人ブログの転載になります。

kotamat.com

HSTSに関して、話題に上がったことがあったので、改めてHSTSは何なのかをまとめてみました。

TL; DR

  • HTTPSを強制するもの
  • TLDベースとレスポンスヘッダーベースがある
  • 導入するときは気をつけないといけない

HSTSの導入モチベーション

HTTPSが有効ではないと、中間者攻撃をされる危険性があることは承知の上で議論を進めていきます。 わからない方はhttps httpとかでググってみよう

ただ、例えばHTTPでアクセスしたあとにHTTPSにリダイレクトするようなサイトを構築している場合、最初のアクセスのタイミングで中間者攻撃されるような危険性のある環境でクライアントが接続しに来ると、いくらHTTPSで疎通していたとしても攻撃される危険性が残ってしまいます。

機微なCookieに対してSecure属性をつけるなど他での対策はできますが、当然それだけでは不足です。

ここで、「初回アクセス時からHTTPSを強制する」事ができれば、上記のような危険性を排除できます。これがHSTSが達成したいモチベーションです

HSTSするには

TLDベースで対処する

.dev.app など、比較的最近発行されたTLDではHTTPSを強制することが標準で備わっていることがあります。こういったドメインを用いればHSTSは強制的に有効になります。

レスポンスヘッダーで対処する

とはいえ大抵のアプリケーションは .com だったり .co.jp だったりを使っていると思います。こういったドメインはHSTSを強制する力はないため、独自で設定する必要があります。 「初回リクエスト時にHTTPSで接続する」ということがブラウザに知られている必要があるため、レスポンスヘッダーを用いて「このウェブサイトはHSTSだよ」ということを事前に伝えて置く必要があります。

そこで用いられるヘッダーが Strict-Transport-Security ヘッダーです。

使い方

このヘッダーには3つのディレクティブがあります。

ディレクティブ 説明
max-age ブラウザが記憶する、HTTPSに強制する時間(秒)
includeSubDomains 当該ドメインだけではなく、サブドメインもすべて対象にする(省略した場合は当該ドメインだけ)
preload preload対象に入れる(後述)

このディレクティブを適切に設定し、任意のリクエストのレスポンスヘッダーに追加するだけで、「次からの接続は」HTTPSが強制されるようになります

注意点

ここで大事なのはmax ageとincludeSubDomainsです。 max ageがながければ長いほど当然セキュアではあるのですが、その間証明書が失効するなどHTTPSで接続できない時間があると、ブラウザはそのウェブサイトへ接続できなくなってしまいます。 またincludeSubDomainが設定されればサブドメインにもその効果が波及されます。一つのドメインを踏むだけで、他のサブドメインに対しても効果が波及するのでよりセキュアではありますが、サブドメインがHTTPのみしか許可できない場合は、当然そこには接続できません。

例えば簡易的に CNAME などで外部サービスにサブドメインを発行しているケースだと、Let's EncryptなどでHTTPS化していないケースが有り、そういったサービスを使う場合はHTTPS化されません。 そのようなドメインは局所的にしか使わないものかもしれませんが、一度includeSubDomain下に入ってしまえばそのドメインも対象になってしまうので、その外部サービスが使えなくなってしまいます。

またこれはmax-ageが消えるまで有効です。つまり初期のタイミングでは特にそのようなサービスと提携しておらず、問題なく接続できたとしても、後日そのようなサービスと連携する際に障害になるといったケースも発生しえます。

また、レスポンスヘッダーに付与されるという特性上、ブラウザにとっての初回アクセス時はどうしてもHTTP接続になるケースが発生しえます。これを防ぐ仕組みとしてpreloadというものが提供されています。

preload

preloadは仕様書では定義されていないものですが、Googleによって提供され、昨今の主要なブラウザには搭載されている機能となっています。 サポートしているブラウザはこちら

preloadをつけることによって、「そのブラウザ」ではなく「全ブラウザ」に対してHTTPS強制することが伝わります。 つまり誰かが生贄になってそのサイトに接続したあとは(タイムラグはありますが)それ以降そのサイトに訪れる人は全員HTTPSが矯正されるということです。

タイムラグを極力減らしたい方は https://hstspreload.org にて対象ドメインを設定することでその速度を早めることができます。

当初達成したかった目的はこのpreloadを使うことによって達成しうるのですが、当然上記で上げた注意点を考慮する必要があります。

preloadの設定ができるということは当然削除もできます

https://hstspreload.org/removal/

にて削除リクエストを送ることができますが、このリクエストが処理されるのはChromeユーザで6~12週間、他のブラウザはそれ以上の時間がかかるので、より一層設定には注意を払う必要があります。

無効化

Strict-Transport-Security: max-age=0 を設定することによって無効化できます。

まとめ

HTTPSが当たり前になってきている中で、HSTSの対応が求められるケースは増えて生きているかと思います。 上記注意点を考慮し、設定していきましょう。

Mockeryでのモックの作り方を調べてみた

この記事は個人ブログの転載になります。

toyo.hatenablog.jp

とある方から、「なんで静的メソッドはモックできないんですか?」ときかれたときに、「そういえば、Mockがどういう原理で動いているかいまいち知らないなー」と思ったので、モックがどのように作られているのかを調べてみました。

僕は普段はMockeryを使用しているので、Mockeryでのモックの生成のされ方をまとめます。

docs.mockery.io

Mockeryは色々な種類のモックが作れるので、今回は一番ベーシックな「スタブやモック」の生成過程を追ってみました。

コードでいうと、以下のような感じのやつ。

<?php
Mockery::mock(Post::class);

以下ではこのコードを例にとって説明していきます。

大まかな流れ

雑にまとめると、以下です。

  1. Postクラスを継承したモッククラスの定義を作成する
  2. 定義したモッククラスをロードする
  3. ロードしたモッククラスのインスタンスを生成する

です。もっと Refrection Class が関わってくるのかなと身構えていたのですが、とてもシンプルでした。

モッククラスの定義

Mockeryではモッククラスごとにクラスの定義を行っています。
つまり、

<?php

// あくまで疑似コードです
class MockedPost {
}

みたいなコードを、都度生成しているということです。

モッククラスのテンプレート

ただし、モッククラスには、 shouldRecieveandReturn などのメソッドが共通して必要です。
そのため、モッククラスの定義のテンプレートのようなものが存在しています。
このテンプレートが、 Mockery\Mock です。

github.com

このクラスの中で shouldRecieve などの定義も書かれています。
つまり、 Mockery::mock(Post::class) で生成したインスタンスの振る舞いは、このクラスの内容を読めばわかるということです。

テンプレートの書き換え

Mockeryではモッククラスごとにクラスの定義を行っているため、クラス名の衝突については考えないといけません。
また、PHPには型が存在しているため、それぞれのモッククラスは、引数で渡されたクラスの型である必要があります。
今回でいうと、生成されたモッククラスは Post 型である必要があります。

なので、テンプレートを書き換える必要があります。
テンプレートのファイルを file_get_contentsで読み込み、文字列として変数に入れます。
その文字列を str_replace などを用いて書き換えていきます。

元のテンプレートは以下のような感じです。

<?php

// ...略

class Mock implements MockInterface
{
    // ...略
}
クラス名の書き換え

クラス名が衝突するとエラーになるため、ユニークなクラス名が必要です。
そのため、テンプレートのクラス名を書き換える必要があります。

クラス名は Mockery_0_Post という命名になります。

0 の部分は連番です。
たとえばモックを2個生成した場合は、以下のようになります。

<?php

// Mockery_0_Post クラスのインスタンス
$post1 = Mockery::mock(Post::class);

// Mockery_1_Commentable クラスのインスタンス
$post2 = Mockery::mock(Commentable::class);

よって、クラス定義は以下のような状態になります。

<?php

// ...略

class Mockery_0_Post implements MockInterface
{
    // ...略
}
型情報の追加

生成されたモッククラスはPost型である必要があります。
ただ、この段階でモッククラスの型は Mockery_0_Post, MockInterface だけです。
そのため、Postクラスを継承することで解決します。
つまり、ドキュメントにも書いてありますが、 final キーワードのついているクラスのモックは(継承できないので)作成できません。1

クラス定義は以下のような状態になります。

<?php

// ...略

class Mockery_0_Post extends Post implements MockInterface
{
    // ...略
}
メソッド呼び出しの抑制

継承をすることで型を付与することはできますが、問題がひとつ残ります。
それは、Postクラスのメソッドが継承されてしまうということです。

たとえば、PostクラスにgetTitle()というメソッドが存在していたとして、Postをモックしたインスタンスを呼ぶと、そのままPost::getTitle()が呼ばれてしまいます。
別の挙動に差し替えるためにモックを使いたいので、これではモックの意味がありません。

モッククラスの定義に、Postで定義されているすべてのメソッドを再定義(オーバーライド)することで、この問題に対応します。
Postで定義されているすべてのメソッドの取得は、Postクラスのリフレクションクラスインスタンスを作成し、そのインスタンスgetMethods メソッドを使用することで実現できます。

https://www.php.net/manual/ja/reflectionclass.getmethods.php

ちなみに、メソッドの内容はほぼ固定で、以下が入ります。

<?php

$argc = func_num_args();
$argv = func_get_args();
$ret = $this->_mockery_handleMethodCall(__FUNCTION__, $argv);
return $ret;

最終的に、クラス定義は以下のようになります。

<?php

// ...略

class Mockery_0_Post extends Post implements MockInterface
{
    // ...略
    
    public function getTitle(): string {
        $argc = func_num_args();
        $argv = func_get_args();
        $ret = $this->_mockery_handleMethodCall(__FUNCTION__, $argv);
        return $ret;
    }
}

クラス定義のロード

クラス定義はできましたが、これは現状ただの文字列です。
これをどうにか読み込まないと、クラスとして使えません。

「どうやって読み込むんだろう・・・?」と思っていましたが、

<?php
eval("?>" . $definition->getCode());

github.com

のように、evalを使用して読み込んでいる感じでした。

PHPevalのドキュメントには

PHP 開始タグを含めてはいけません。

とありますが、クラス定義の先頭はPHP開始タグで始まっているので、 ?> を先頭につけて相殺しているようです。

モックインスタンスの生成

さて、モッククラスの定義をし、定義の読み込みもできたので、あとはインスタンスを生成するだけです。

ただし、Postクラスにコンストラクタが宣言されていた場合を考えないといけません。

<?php

class Post
{
    private string $title;

    public function __construct(string $title)
    {
        $this->title = $title;
    }
    
    public function getTitle(): string
    {
        // ...略
    }
}

このような場合、モッククラスはPostクラスを継承しているので、インスタンスを生成するためには $title が必要になります。
モックとしては、コンストラクタの処理は実行させたくない場合が多いと思います。
また、その際はそもそも引数を渡す意味もなく、引数の生成のためのコード2も書きたくないですよね。
なので、コンストラクタを呼ばずにインスタンスを生成します。

そのため、モッククラスのリフレクションクラスのインスタンスを作成し、 newInstanceWithoutConstructor メソッドを使用して、コンストラクタ無しにモックインスタンスを作成します。

https://www.php.net/manual/ja/reflectionclass.newinstancewithoutconstructor.php

これで無事にモックのインスタンスを生成できました。

まとめ

僕はまだモックを知らなかった頃は

<?php

$mock = new class extends Post
{
    public function getTitle(): string
    {
        return 'dummy';
    }
};

のように無名クラスを使用して書いていました。

今回は有名なモックライブラリの中を調べてみたわけですが、基本的な発想は同じなんだなと思いました。
簡単な発想にどこまで向き合えるか、が有名ライブラリへの道なのかもしれません。

おまけ1: インターフェースのモック

さて、Postはクラスでしたが、インターフェースのモックの場合がどのようになるかも追ってみました。

<?php

interface Commentable
{
    function writeComment(string $content): void;
}

Mockery::mock(Commentable::class);

基本的にはクラスの場合と同じです。

ただし、クラス定義の部分で extends していた部分は implements になります。

- class Mockery_0_Post extends Post implements MockInterface
+ class Mockery_0_Commentable implements MockInterface, Commentable

その他は大きな違いはありませんでした。

おまけ2: なぜ静的メソッドはモックできないのか

モックの生成過程が見えてくれば、静的メソッドのモックができない理由もみえてきます。

<?php

function getAllPosts(): array
{
    return Post::all();
}

たとえばこのようなコードがあったとします。
Postのモックインスタンスを作成することは可能です。
Postのモッククラスの静的メソッドの内容を差し替えることも可能でしょう。

ただし、モッククラスは Mockery_0_Post クラスであり、 Post クラスではないのです。
つまり、 Mockery_0_Post::all() は自由に定義できても、 Post::all() の内容はそのままです。

そのため、 Post::all() をそのままモックすることはできません。

エイリアスモック

これを解決するために、Mockeryでは「エイリアスモック」という機能があります。

<?php
Mockery::mock('alias:' . Post::class);

このように書くと、モッククラスの定義が以下のように変化します。

- class Mockery_0_Post extends Post implements MockInterface
+ class Post extends \stdClass implements MockInterface

引数で渡したクラス名がそのままモッククラスのクラス名になりました。

当然、すでにPostクラスが読み込まれている場合には、クラス名が衝突してエラーになります。
なので、エイリアスモックは、Postクラスが読み込まれる前に読み込む必要があります。3

つまり、「本物のコードが読み込まれる前に、モックのコードを読ませて、本物のコードが読み込まれないようにしちゃえ」という作戦なわけですね。

エイリアスモックとフレームワーク

エイリアスモックを使えば静的メソッドもモック可能」なわけですが、実コードではなかなか上手くいかないことが多いです。

というのは、たいていのプロダクトではLaravelなどのフレームワークを使用していると思います。
そして、テストコードを書く際も、フレームワークの機能を使用して書く場合が多いでしょう。
そのため、テストの実行時には、フレームワークの初期化などを行われますが、その際に、モックしたいクラスが読み込まれてしまうことが多いのです。
たとえばLaravelの場合だと、サービスプロバイダによってユーザが定義したクラスが読み込まれます。
その場合、個々のテストケースに処理が渡ってきたときにはすでにモックしたいクラスが読み込まれてしまっているため、 エイリアスモックを使えないことが多いのです。

エイリアスモックという手段は存在しているものの、「基本的には静的メソッドはモックできない」と考えたほうが良いかもしれません。


  1. ドキュメントにもありますが、finalキーワードがついている場合は プロキシパーシャルモック を作成できます。

  2. 今回はstringなので生成が楽でいいのですが、オブジェクトを引数に取る場合は、実引数に指定するオブジェクトを事前に生成する必要があり、コードが冗長になります。

  3. ここらへんの挙動についてはPHPのオートロードについて理解しておく必要があります。