mkcertで生成した証明書をHAProxyにインポートしローカルでSSLオフロードする
前回、ローカル環境でHAProxyを使った分散環境が用意できたので、HTTPS接続にも対応させてみる
mkcert で自己証明書を生成する
mkcert というツールを使い、ローカルCA証明書を作成・インポートして、サーバー証明書を作成します。 手順は、mkcert のGitHubページに従って進めます。
mkcert のインストール
brewでインストールします。nssはFireforx からアクセスする場合に必要です。
brew install mkcert brew install nss
ローカル CA 証明書を作り 端末にインポートする
まずは自己認証局(CA)を作るため、mkcert -install
を実行します。途中でrootパスワードを求められたら、端末の管理者パスワードを入力します。
mkcert -install Created a new local CA 💥 Sudo password: The local CA is now installed in the system trust store! ⚡️ The local CA is now installed in the Firefox trust store (requires browser restart)! 🦊 The local CA is now installed in Java's trust store! ☕️
ローカルのCA証明書が作成されMacにインポートされているか確認します。 mkcert -CAROOT
で表示されるディレクトリに CAのルート証明書と秘密鍵が作成されます。インポートは 「Keychain Access」を開き左側のメニューで「System」を選び、「Certificates」タブを選ぶと、"mkcert ....." というルートCA証明書がインポートされていることが確認できます。
サーバー証明書を生成する
mkcertで作成したCAを利用してサーバー証明書を作成します。今回は localhost
ドメインに対する証明書を作成してみます。mkcert localhost
とコマンド実行すると、localhost.pem と localhost-key.pem が生成されます。
mkcert localhost Created a new certificate valid for the following names 📜 - "localhost" The certificate is at "./localhost.pem" and the key at "./localhost-key.pem" ✅ It will expire on 23 April 2023 🗓
ドメインとしては、他にもワイルドカード (*.example.com) や ループバックアドレス (127.0.0.1) などでも作成できる。
HAProxyにSSLオフロード設定を行う
サーバー証明書をHAProxyにインポートする
HAProxyにインポートする証明書ファイル(.pem)は、1つのファイルにまとめる必要があるため、先程作成したキーペアを1つにまとめておきます。
HAProxy version 2.3.19 - Configuration Manual
cat localhost.pem localhost-key.pem > cert.localhost.pem
作成したPemファイルをDockerコンテナで利用するようにDockerfileを修正します
FROM haproxy:2.3 COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg COPY cert.localhost.pem /usr/local/etc/haproxy/cert.localhost.pem
設定ファイルを編集する
haproxy.cfg に 証明書のパス、httpヘッダーへのx-forwardedの追加、httpからhttpsへのリダイレクトを追加します。
... frontend http-in bind *:80 default_backend servers # サーバー証明書のパスを指定 bind :443 ssl crt /usr/local/etc/haproxy/cert.localhost.pem # x-forwarded-for を有効にし、プロトコルとポートもheader にセットする option forwardfor http-request set-header x-forwarded-proto https http-request set-header x-forwarded-port 443 # httpからのアクセスをhttpsにリダイレクトする redirect scheme https if !{ ssl_fc } ...
最後にHAProxyのコンテナイメージを再ビルドして実行します。 またホスト側から443ポートでアクセスできるようにすること。
... services: haproxy: build: ./haproxy depends_on: - app01 - app02 - app03 ports: - 80:80 - 443:443 ...
動作確認
ブラウザから https でアクセスできました。httpでアクセスした場合もhttpsにリダイレクトします。 またSpringBootアプリのセッション管理も正常でした。
ローカルでSSLやLBの動作確認できるの便利。 今日の設定内容は証明書を除いてGitHubのリポジトリにあります。
参考記事
多機能なロードバランサとして使える多機能プロクシサーバー「HAProxy」入門 | さくらのナレッジ
HA構成のアプリのHTTPセッション管理にRedis を使う
今更な話ではありますが、AWSのECS Fargate と ALBを使った簡単なデモアプリの環境構築をTerraformで進めていて、セッション管理について調べていたら「ALB側でパーシステンス (スティッキーセッション)を実装するより、ErastiCashなどをセッションストアとして使う」というのを知りました。
個人的にHA構成の経験がほとんどなくRedisも使ったことが無かったことと、またローカル環境でHA構成の動作確認が行えると便利だと思ったので作ってみました。
構成
ローカル環境で動作確認をしたかったので、Docker-compose を使って以下のような構成を用意します。
- ロードバランサ: HAProxy
- アプリケーションコンテナ: Spring Boot
- セッションストア: Redis
Spring Boot アプリ
まずはSpring Boot (Spring MVC)を使って、セッションの状態を確認できる簡単なアプリを作ります。
こちらはセッション管理するBean
package net.kazokmr.study.hasession; import org.springframework.stereotype.Component; import org.springframework.web.context.annotation.SessionScope; import java.io.Serializable; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @Component @SessionScope public class Reception implements Serializable { private static final long serialVersionUID = -3101986789734320497L; private String host; private LocalDateTime receptionAt; private String message; public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public String getHost() { return host; } public void setHost(String host) { this.host = host; } public String getReceptionAt() { return receptionAt == null ? null : DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss").format(receptionAt); } public void setReceptionAt(LocalDateTime receptionAt) { this.receptionAt = receptionAt; } }
コントローラではこのBeanに「状態」「ホスト名」「実行日時」を登録するメソッドとセッションを破棄するメソッドを用意してリクエストを受け付けます。
package net.kazokmr.study.hasession; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpSession; import java.net.InetAddress; import java.net.UnknownHostException; import java.time.LocalDateTime; @RestController public class ReceptionController { private final Reception reception; @Autowired public ReceptionController(Reception reception) { this.reception = reception; } @GetMapping("/hello") public String hello() throws UnknownHostException { if (reception.getReceptionAt() == null) { reception.setHost(InetAddress.getLocalHost().getHostName()); reception.setReceptionAt(LocalDateTime.now()); reception.setMessage("セッション開始"); } else { reception.setMessage("セッション中"); } return String.format("%s, ", reception.getMessage()) + String.format("Host: %s, ", reception.getHost()) + String.format("受付日時: %s", reception.getReceptionAt()); } @GetMapping("/bye") public String goodbye(HttpSession session) { session.invalidate(); return "セッション切断"; } }
これをIntelliJから起動しChromeからアクセスすると以下のように出力します。
Spring Boot アプリをDockerコンテナで起動する
このアプリケーションをDockerコンテナから起動してアクセスしてみます。尚、ホスト名をわかりやすくするために起動時のコマンドに -h オプションを付けます。
docker run --name <コンテナ名> -p 8080:8080 -h docker-container-1 <イメージ>
HAProxy をDockerで起動する
HAProxyのDockerイメージは公式に記載されている通り、公式イメージに haproxy.cfg を上書きしたイメージをビルドして起動します。
FROM haproxy:2.3 COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg
haproxy.cfg はアプリケーションコンテナを3つ用意し、checkで死活監視とラウンドロビンで振り分けているだけとなります。
global daemon maxconn 256 defaults mode http timeout connect 5000ms timeout client 50000ms timeout server 50000ms frontend http-in bind *:80 default_backend servers backend servers balance roundrobin server app01 app01:8080 check server app02 app02:8080 check server app03 app03:8080 check
このhaproxyコンテナとSpring Bootアプリケーションコンテナを docker-compose で起動します。
app01 - 03 は予めDockerイメージを作成しています。またhostnameオプションを使って、どのコンテナにアクセスしているわかりやすくしました。
version: '3' services: haproxy: build: ./haproxy ports: - 80:80 app01: image: sessiontest:latest hostname: docker-container-1 app02: image: sessiontest:latest hostname: docker-container-2 app03: image: sessiontest:latest hostname: docker-container-3
結果
haproxy ではパーシステンスを行っていないためセッションが保持されません。なのでブラウザをリロードする度に、ラウンドロビン方式によって別のアプリコンテナに対して新規接続を行ってしまいます。
HAProxyでパーシステンスを実現する場合
以下のブログの内容を参考にhaproxy.cfg のbackend セクションで、JSESSIONID に振り分け先のserver追加してcookie にセットすることで2回目以降のアクセスも同じアプリケーションサーバーに接続するようにします。
Load Balancing, Affinity, Persistence & Sticky Sessions
HAProxyでHTTPロードバランシング - CLOVER🍀
backend servers balance roundrobin cookie JSESSIONID prefix nocache server app01 app01:8080 check cookie app01 server app02 app02:8080 check cookie app02 server app03 app03:8080 check cookie app03
haproxyのDockerイメージを再ビルドして起動したところ、セッションが継続されました。
Redis でセッション管理を行う
本題はこちらなので、HAProxyは、再度Cookie設定を戻してセッションが継続されない状態に戻します。
アプリをRedisに対応させる
Reidsをキャッシュストアとして利用するように Spring Boot アプリを変更します。
はじめに開発用にRedisのDockerコンテナを起動します。 Docker
docker run --name redis -p 6379:6379 -d redis
Spring Boot アプリケーション側の設定変更はこちらのチュートリアルを参考にしました。 https://docs.spring.io/spring-session/docs/current/reference/html5/guides/boot-redis.html
RedisをSessionキャッシュにするために Spring Session Data Redis を依存関係に追加します。
更にアプリケーションからRedisに接続すためのクライアントとしてJedis も追加します。 Jedisは前述のチュートリアルでは含まれていませんが、これを入れないとSpring Boot起動時にRedisに接続できずにエラーとなってしまいます。
<dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency>
最後に application.properties で sessionの保存先をredis に変えます。
spring.session.store-type=redis
これだけでHttpSessionの保存先をRedisに変えるための springSessionRepositoryFilter beanを生成してくれるようです。 またRedisの接続先やSessionタイムアウトの管理なども application.properties に追加してカスタマイズできますが、ローカルのRedisに接続するだけであればこれだけでOKです。
詳細は前述のチュートリアルをご覧ください。
Redis に保存されているセッション情報を見る
Docker Composeで環境を作る前に、IntelliJからアプリケーションを起動してRedisにどのように格納されているか見るため、アプリにアクセスしていない状態から確認します。データの検索はRedisのクライアントツールであるRedis-cli を使用します。このツールは Redisにバンドルされているので、Redisコンテナの中に入り利用してみます。
docker exec -it redis /bin/bash root@redis:/data# redis-cli 127.0.0.1:6379>
接続したら keys * と実行すると保存されているKeyの一覧が表示されます。まだアプリケーションにアクセスしていないので (empty array) と出力されます。
127.0.0.1:6379> keys * (empty array)
今度はブラウザからアクセスしてから検索します。
127.0.0.1:6379> keys * 1) "spring:session:sessions:expires:7f57263c-0743-471f-b4cc-5e3d41b60e3d" 2) "spring:session:sessions:7f57263c-0743-471f-b4cc-5e3d41b60e3d" 3) "spring:session:expirations:1611233700000"
"spring:session:" から始まるKeyが3つ出力されました。この中で2)にセッションデータが入っているようです。
type <key>
でデータの形式を確認するとhash と出力されたので hgetall <key>
で検索します。
127.0.0.1:6379> hgetall spring:session:sessions:7f57263c-0743-471f-b4cc-5e3d41b60e3d 1) "sessionAttr:scopedTarget.reception" 2) "\xac\xed\x00\x05sr\x00%net.kazokmr.study.hasession.Reception\xd4\xf3\x87s#\xeev\x8f\x02\x00\x03L\x00\x04hostt\x00\x12Ljava/lang/String;L\x00\amessageq\x00~\x00\x01L\x00\x0breceptionAtt\x00\x19Ljava/time/LocalDateTime;xpt\x00\x11MacBook-Pro.localt\x00\x15\xe3\x82\xbb\xe3\x83\x83\xe3\x82\xb7\xe3\x83\xa7\xe3\x83\xb3\xe9\x96\x8b\xe5\xa7\x8bsr\x00\rjava.time.Ser\x95]\x84\xba\x1b\"H\xb2\x0c\x00\x00xpw\x0e\x05\x00\x00\a\xe5\x01\x15\x15\x18\x02:|FXx" 3) "creationTime" 4) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01w$\xe6\xd1\xf6" 5) "maxInactiveInterval" 6) "\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\a\b" 7) "lastAccessedTime" 8) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01w$\xe6\xd1\xf6"
所々にbeanのデータが読めるのでそれっぽいですがわかりづらいですね。そこで下記の記事に書かれているようにセッションデータのシリアライズ方法をJava標準のものからJson形式にしてみます。
Spring Boot+Spring SessionでスケーラブルなステートフルWebアプリが簡単につくれるよ〜 #nginx - Qiita
余談でSpring Securiy も使うなら以下の記事のようにSpring Securityの拡張モジュールの適用もした方が良いと思います。
Spring Security 4.2 主な変更点 #Java - Qiita
package net.kazokmr.study.hasession; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializer; @Configuration public class HttpSessionConfig { @Bean public RedisSerializer<Object> springSessionDefaultRedisSerializer() { return new GenericJackson2JsonRedisSerializer(); } }
(注意) JSON形式に変更した結果、LocalDateTimeで持っていて受付日時のシリアライズで例外が発生してしまいました。本来なら対応するべきですが今回はString型で取得した現在時刻を保持するように変えました。
アプリを起動する前にRedisに残っているデータを全て削除して、再起動しセッションデータを検索します。
127.0.0.1:6379> hgetall spring:session:sessions:a3b43f71-3c5b-48e6-a700-807d7bcb48c5 "sessionAttr:scopedTarget.reception" "{\"@class\":\"net.kazokmr.study.hasession.Reception\",\"host\":\"MacBook-Pro.local\",\"receptionAt\":\"2021/01/21 21:50:27\",\"message\":\"\xe3\x82\xbb\xe3\x83\x83\xe3\x82\xb7\xe3\x83\xa7\xe3\x83\xb3\xe9\x96\x8b\xe5\xa7\x8b\"}" "creationTime" "1611233427618" "maxInactiveInterval" "1800" "lastAccessedTime" "1611233427618"
セッション情報がJSON形式で出力され、生成日時や最終アクセス日時なども見やすくなりました。
とこでRedisに保管されているSESSIONキーですが、これはJSESSIONIDではなく、SESSIONという専用のCookieを使っているようです。ですが、Redisで検索するとUUIDの形式で出力されますが、Chromeの開発者モードで見るとUUIDでは出力されていません。なのでChormeのCokkieからSESSION情報を検索するのは難しそうです。
RedisというかSpring Sessionを利用すると、SessionIDのCookie名が JSESSIONIDではなく SESSION
になるようです。名称などのCookieのカスタマイズは以下のように CookieSeriarizer Beanを定義してあげれば良さそう
Redis を追加したDocker Composeでアプリを起動する
docker-compose.yml にはRedisを追加し、Redis > app01-03 > HAProxy の順番に起動するように依存性を追加しました。
version: '3' services: haproxy: build: ./haproxy depends_on: - app01 - app02 - app03 ports: - 80:80 redis: container_name: redis image: redis app01: image: sessiontest:latest hostname: docker-container-1 depends_on: - redis app02: image: sessiontest:latest hostname: docker-container-2 depends_on: - redis app03: image: sessiontest:latest hostname: docker-container-3 depends_on: - redis
また Spring BootアプリのコンテナもDocker Compose のRedisと接続できるように以下の設定を変更しました。
- Docker Compose 専用の application-docker.properties を追加し、compose上のcontainer_name: redis に接続するようにホスト名を追加しました。
spring.redis.host=redis
アプリケーションコンテナをビルドする前に、Maven で clean package を実行するようにしました。これは maven のTestフェーズでRedisとの接続が必要なため、ローカル環境のRedisコンテナにアクセスしてMavenビルドを実行したかったためです。
アプリケーションコンテナ用のDockerfile の内容を変更してDockerイメージを再ビルドしました。 上記のNo.1と2に対応する形で、マルチステージビルドを止め、ENDPOINTでjarを起動時にapplication-docker.properties を参照するようにprofileのactivate 設定オプションを追加しました。
FROM openjdk:11-jre COPY ./target/*.jar . EXPOSE 8080 ENTRYPOINT ["java","-jar","ha-session-0.0.1-SNAPSHOT.jar","--spring.profiles.active=docker"]
感想
新しい技術では無く沢山の情報もあったことやRedisのセッション管理もSpring Session Data Redisが良い感じで行ってくれるので難しくは無かった。
ただ Redisでセッション管理するにはアプリケーション側の方で設定を行うことになるので利用しているフレームワークなどによっては導入コストが掛かる場合もあると思うので、その場合はロードバランサーのパーシステンスの採用を検討した方が良いかもしれない。
これでローカルの開発環境で冗長化構成の動作確認を簡単に行うテンプレートを用意することができた。これに、HTTPS接続とRDBMSの接続も追加しておくと個人的にはより便利になりそう。
JetBrains Academy で Java Developer コースを進めてみた
今週末で無料で受けられる期間が終了するので、使ってみた感想を書きます。
動機
8月に Twitterか何かで 見かけて「最近コードを書いていないなー」って思ったので、鉛切った身体を鍛え直すつもりで、最大で2ヶ月ちょっとは無料で受けられるので使ってみました。
進め方
ログインするとまずはTrack と呼ばれる、言語ごとのコース選択があります。私は Java Developer を選択しました。 他にはPython Developer があり、Kotlin Developer や Web Developer などがβ版として選択ができます。
次にプロジェクトを選びます。プロジェクトはこの学習のなかで完成させるアプリケーションとなっていて、プロジェクトの中で5−6のステージに別れています。各ステージではトピックと呼ばれる特定のスキルに対する説明と演習からなる学習が並んでおり、トピックを順番にこなすとステージの最後にプロジェクトの課題が提示され、これまでのトピックで学んだことを活用しながら、課題をクリアします。
トピックの演習とステージの課題は、IntelliJ IDEAと連動しておりIntelliJ でコードを書いて評価します。 ちなみに間違えても特にペナルティはなく、何度もでも挑戦できますし、クリアしないと次に進めない仕組みになっています。 また各演習については参加している他のメンバーがコメントしたりヒントを提示していたり、クリアすると他の人のコードを見たり、自分のコードを公開したりできます。 海外のプログラマーのコードと自分のコードを比較するのも勉強になることもあったり、あまり変わらないなーと思ったり、なんでそんな書き方しているんだろ?って思ったりと面白かったです。
プロジェクトとトピック
プロジェクトはJava Developerだと32種類あり、Easy, Medium, Hard, Challenging の4つの難易度になっています。 トピックは、Javaに関する様々なスキルを学ぶことができ、Java以外にもデータベースやフロントエンドに関するトピックスや、ソフトウェア工学やアルゴリズム、代数・幾何と言ったジャンルもあり、全部で403のトピックがあり、知らないことも多かったりしてとても勉強になりました。
Java Developer – JetBrains Academy
JetBrains Academy - Learn programming by building your own apps
進捗
2ヶ月続けて、11プロジェクト(Easy:1, Medium: 3, Hard:5 , Challenging:2 )完了し、トピックは 221/403 (55%) でした。 Hard位だと割とスムーズにクリアできるのですが、Challengingは時間がかかりました。平日は朝と夜に少しずつ進めて、休日に家にいるときに進めていましたが、子供の面倒や外出などもあって、まとまって進める時間はあまり取れませんでした。 (一応、クリアしたプロジェクトはGitHubに上げているのですが、課題の回答などにもなり規約とかも有りそうなのでPrivateにしてあります。)
感想
Javaについて言えば、色々な範囲を細かくカバーして演習が出来たのでとても勉強になった。特にLambda や StreamAPI などは前職のJavaのバージョン的な都合もあって、あまり触れる機会がなくて、IDEの予測機能などを駆使したパワープレイ的な感じで書くことが多く、いまいち理解できてなかったのですが、関数型インターフェースの学習から始まり、StreamAPIの様々な使い方をトピックとして学べたので力がついたと思います。
またアルゴリズムの勉強や代数・幾何の連立方程式や行列式などの計算プログラムを作るプロジェクトもあり、学生時代を思い出しながら復習できたこと、あるいはブロックチェーン の基本的な仕組みが理解できるプロジェクトなどに取り組むことで新しいことも覚えられたのがとても良かったです。
またコースは全て英語になっているので、細かいニュアンスや専門用語にわからない部分が多かったりしました。問題文を見ても何を求められているのかがわからなかったりする部分が多く、DeepLは欠かせなかったです。 海外の人が「この問題の説明はよくわからないな」「英語というのは時折、正確に伝えないことがある。」みたいなことをコメントしていることもあり、自分の英語スキルの問題だけではなさそうでしたが。
またプロジェクトの課題についても、難易度に関係なく説明が大雑把だったりして、どうしたらいいのかを探りながら進めたり、初期の頃は「このプロジェクトのステージやトピックでやったことを使ってクリアしないと」という気持ちが強すぎて、シンプルにコードを書くことができなかったりしました。
今後
無料期間が過ぎたら受講料が発生するのですが、現時点で中途半端な進捗なのとまだ学習してみたいトピックもあるので、多分継続することになると思います。最初のEasyコースが簡単だったので2ヶ月あれば終わるんじゃないかなと思っていたのですが甘過ぎました。
と、JetBrains Academyがベストかは分かりませんが、IntelliJを使いながらプログラミングスキルを上げつつ、英語の勉強にもなるので自分にはあっているなとは感じました。
BDD in Action 2nd Edition 第6章を読んだ
BDD in Action 2nd Edition の第6章は、正しい要件を定義することの最終章として、前章で探索した主要な例を使って、フィーチャーのシナリオをGherkinフォーマットで書く方法を説明しています。
前半は、Gherkinの主要なキーワードの用途や使い方の説明で、これはCucumberのチュートリアルなどでも学べる内容でした。
この中で知らなかったキーワードにはBackground
があり、複数のScenarioの冗長性を排除することができます。
これは書籍に載っていたことではありませんが、キーワードにRule
とExample
が追加されていました。
Rule
は Feature
の中で、Scenario
をグループ化できます。Example
はScnario
の別名なので、用途はScenario
と変わりません。
Rule
とExample
は、より実例マッピングに近い形式で表現できます。
章の後半では、優れたGherkinの書き方を紹介しています。
Gherkin を書く際に気を付けることとして以下のようなことが挙げられていました。どれもプログラミングの原則でよく言われていることと似ています。
シナリオでは宣言を書き、命令的な指示は書かない。(Howではなく、Whatを書く)
操作することや入力する内容などを1つ1つ詳細に書くのではなく、シナリオで達成したい目的に対して何をしなければいけないかに着目して書くとよい。1つの関心事に着目してシナリオを書く。 特に初めはシンプルな関心事から初めて、徐々に複雑なシナリオを組み立てる。
シナリオの登場人物に個性を付ける(ペルソナを作る) ユーザーや管理者のような味気ない役割だけではなく、目標・能力・背景などを付けると良い。また初めからペルソナを詳細に書くことに時間を費やさずにシナリオを書きながら付け足すと良い。
シナリオには必要なことだけを書く。容易に想定できることは省略する。
とあるフィーチャーのシナリオを書く時に、そのフィーチャー(画面)にたどり着くまでの汎用的で容易に想像がつく手順まで記述しない。特にGivenについてはシナリオを検証するために必要な前提条件だけを書く。既存のテストスクリプトやテストコードの手順をそのままシナリオとして書かない。
これらは指示・命令的なステップが多いから。「比較する」や「確認する」が含まれているシナリオは大抵、テストスクリプトのような書き方になりがち。各シナリオは独立して実行できるようにする シナリオごとに必要な前提条件を宣言して独立して実行できるようにする。一連のシナリオの纏まりを順番に実行する必要がある構造にはしない。(あるシナリオが失敗したら、後続のシナリオが実行できないことが無いようにする。)
個人的には経験上、指示的な書き方になりがちなので、この辺を気をつけていきたい。
ここまでの章で、シナリオをまとめたFeatureファイルを成果物とした、要件定義の進め方を学んできました。これまで自分が行って来たやり方とは全然違っているので慣れるまでに時間がかかりそうだが、作るべきことにより着目して明確に分かりやすい書き方で整理できそうに思えました。
BDD in Action 2nd Edition 第5章
5章を読み終えましたので、学んだことをまとめてみます。
5章は、BDDの活動サイクルの中の Illustrateフェーズでの作業として、3〜4章の Speculateフェーズで特定し、詳細化したフィーチャー、あるいはユーザーストーリーを明確にしていきます。
フィーチャーの内容を明確にするための手段は「例を使ったビジネス側との会話」です。
例を使って話し合う
話し合いは、ユーザやステークホルダーも参加して、フィーチャーの受け入れ基準を理解し、定義し、合意していく。
話し合いでは、良く適切な質問ができなかったり、脱線したりして、思うように進まずに長引くことがある。
フィーチャーに対して、ビジネス側の言葉を使った例で説明することで、話し合いが円滑に進む。
例を使って話う会う時は反例も出すことで矛盾や誤解を知り、不確実なことを洗い出して明確にすることができる。
例を使って理解したことを確認したり拡張したりするために追加の例を使う。
表を使って例を整理する
表はインプットと期待する結果が簡単に説明できるような要件に適している、データ中心的なアプローチ
デシジョンテーブルを作るイメージ。だけどこの段階では網羅することよりも要件の理解を優先する
実例マッピング (Example Mapping)
フィーチャー(ストーリー)に紐づくビジネスルールや制約、そしてルールに対する例と反例のリストを特定していく。
目的は、フィーチャーやルール(受け入れ基準)の共通理解を得ること
挙げられたルール、または例の数が多いと、フィーチャーが大きいか、複雑なことを示していて分割した方が良いかもしれない。
話し合いは一つのフィーチャー(ストーリー)に対して、25-30分のタイムボックスで行い、そのフィーチャーに集中する。
話し合いの最後に「作業を始めるために十分な理解を得られたか?」「更に話し合う必要があるか?」を投票して決める。
話し合いで回答できない例や質問は、別に記録しておいて、その話し合いではそれ以上議論しないようにする。
例を追加していく時は「もしこうだった場合は?」「そのルールはいつもそうなのか?」「ルールが適用できない場合はあるか?」などの視点で質問すると良い。
フィーチャーマッピング
大きく、不明確なフィーチャーに適している。
実例マッピングと違う点は「例をステップ単位に分けて説明する」「ステップの最後に期待する結果となる結論(Consequence)を書く」などがある。
実例マッピングが「ビジネスルールを基に例を挙げていく」ように進めるとしたら、フィーチャーマッピングはどちらかといえば「ストーリーから想定される例を上げていって、ビジネスルールや制約を特定する」ようなイメージに読み取れた。
フィーチャーマッピング自体は、大きなフィーチャー(あるいはエピック)をルールや制約単位に分割することも目的の一つのなので、その後実例マッピングを使って理解を深めていくと良さそう。
スリーアミーゴス
例を使った話し合いでは、テスター、プロダクトオーナー (ビジネスアナリスト)、開発の3つの役割を持ったメンバーがそれぞれの視点でルールや例を質問していくと良い。
テスターは、細部に注意を払い、検証することに着目して、エッジケースなど見落としがちなシナリオを指摘する
プロダクトオーナー は、ビジネス視点の妥当性や相対的な価値に着目して、シナリオを指摘する
開発者は、技術的な検討事項を指摘する。
まとめ(感想)
顧客と要件定義を行う時に、わからないことがあれば例を使って話し合うことがたまにあったが、それを意図的に利用して要件の理解を深めることが目的。
付箋あるいはホワイトボードツールを使って、ルールと例を整理する。
ここで作ったルールや例のうち主要なものは、Gherkinのシナリオ(Given...When...Then)として使うことになる。だけどこの時点ではそれを意識して書く必要はない。実際このフェーズで上がった例のほとんどはシナリオとしては使わないらしい。
この5章は、初版には載っていなかった(あるいは前後の章の中で少し触れていた程度だった)が、2nd Editionになって、「フィーチャーやストーリーを定義する」と「Gherkinのためのシナリオファイルを書く」の間にこの章が作られたことを考えると、これまでフィーチャーからシナリオファイルに変換する作業が難しかったので、例を用いた様々なテクニックが使われるようになったのかなと推測した。
次の6章は、要件定義について解説した第2部の最後に当たり、成果物としていよいよシナリオを書いてFeatureファイルを完成させていくようなので楽しみ。
BDD in Action, Second Edition 第4章を読んだ
4章を読み終えたのでまとめ。*1
さて4章は、3章で出てきたフィーチャーの詳細化の方法を説明しています。
3章で疑問に思ったフィーチャーの定義
3章でフィーチャーと特定された中には「これはフィーチャーなのか?」と思う物がありました。 kazokmr.hatenablog.com
この疑問について章の最初に触れており、フィーチャーに限らずこれらの概念として用いられる用語は、扱う方法論によって少しずつ定義が異なることがよくあり、混乱の基になっていると書かれていました。
そこでBDD(あるいは、この本)の中では、用語を次のように定義しています。
- この本では、主にケイパビリティ、フィーチャー、ユーザーストーリー、例(Example)の4つを用いて説明する。
- ケイパビリティは、ユーザーやステークホルダーが「何かを行うための能力」を表し、ソフトウェアの実装に依存しない。
- フィーチャー, エピック, ユーザーストーリーは、ケイパビリティをサポートするためのもの。 扱う規模が違うが概念はほぼ同じで「エピック => フィーチャー > ユーザーストーリー *2」となる。
フィーチャー
前章で利用したインパクトマッピングにおいて、最後の成果物(Deliverable)をフィーチャーとします。 ちなみに、前段の影響(Impact)を、ケイパビリティとして扱います。
インパクトマッピングから、フィーチャーを定義すると次のようになります。
Feature: "成果物(Deliverable) " In order to "影響 (Impact)" As a "関係者(Actors)" I want to "成果物をビジネス用語で表す。"
起点となるGoalがここには現れませんが、「そのフィーチャー(Deliverable)は、ケイパビリティ(Impact)を関係者(Actors)に与えるために提供する。」と表現できるので、個人的にフィーチャーが考え易くなると感じました。
また、3章および元々のインパクトマッピングでは「影響をHow、成果物をWhat」と説明しているが、フィーチャーを定義するときは「影響をWhat、成果物をHow」と表している。 これについては、目標を機転に成果物を特定していく時と、特定した成果物を表現する時の違いと考えればそれほど違和感を持たなかった。*3
フィーチャーとユーザーストーリーの違い
BDDでは通常、ユーザーストーリーは、フィーチャーを小さく分割して定義していくそうです。*4
ユーザーストーリーがフィーチャーと大きく異なる異なるのは「それ単独でリリースする価値があるかどうか?」と言う点であると説明されていました。*5
ユーザーストーリーの主な概念は(目的?)は次のようになります。
フィーチャーについてより深く考えていく過程で、思いついた具体的な状況や例を一つ一つをユーザーストーリーとすると作りやすいと感じました。
Real Options と Deliberate Discovery
訳すと「現実的な選択」と「意図的な発見」となるかなと思います。
ステークホルダーの目標を達成するためのケイパビリティに役立つと考えるソリューション、つまりフィーチャーを検討する中で不確実なことが出てきます。それらは大抵、自分たちの無知や誤解から生じます。このような不確実性に向き合って要件に対する共通理解を得るために、この2つの原則について説明しています。
Real Options (現実的な選択)
不確実性が原因で、最適だと考えるフィーチャーが思いつかない、あるいは確信が持てない場合に用いる代替案や保険的なフィーチャーを考えておく事を指します。
この目的は「決断を可能な限り先延ばしにする事」にあり、最適なフィーチャーを考える時間を確保することにあります。 そのためにReal Optionにはいくつかのルールがあります
- Real Optionにも価値を持つ必要がある。
- Real Optionに期限を設ける。
- 何事もすぐに決断をしない。
- でも、最適なフィーチャーである確信が持ったら、期限を待たずに実装する。
Deliberate Discovery (意図的な発見)
最適なフィーチャーを特定するために、無知なことを学んだり、誤解を無くして共通認識を持つ事を積極的に進める考え方で、不確実なことはプロジェクトのリスクになるので、早く解消する事を目的とします。
本に書いてあった説明だと、例えばバックログリファインメントで、どのフィーチャーから取り上げていくかを考えたときに、「最適な方法が明確なもの」よりも「不確実性というリスクを持つもの」から優先して取り掛かることで、プロジェクト全体の明確さを底上げした方がいいとありました。実際にはバランスが重要ではありますが。
この2つの原則を併用して、不確実性を取り除いた最適なフィーチャーを積極的に特定していくと良さそうです。
感想
これまでは最初から、断片的なユーザーストーリーを作ろうとして、ストーリーが上手く定義できなかったり整理できないことが多かったので、「ケイパビリティ -> フィーチャー > ユーザーストーリー」と順番に思考していくと作りやすそうだと感じだので、チャレンジしていきたい。
(おまけ) describe と illustrate
どちらも 「説明する」 と言う意味だが、この章では主にDescribeを使っていて、次の章からはIllustrateフェーズと呼ぶように後者が話の中心になる事を明示していたので、この意味の本質的な違いを調べてみました。
describe
- 「詳細化する」と言う意味もある。
- 大きなフィーチャーを小さく分割していき、ユーザーストーリーに分割していく流れは、まさに詳細化だと感じました。
illustrate
- 「解説する」「例となる」と言う意味合いが強い。
- 明確にするようなハッキリとさせる様子があると感じました。
- 次章で紹介する例(example)などは正に、この意味合いになると思います。
*1:ちなみにこれを書いている時点で、次の5章までしか公開されていない。
*2:実際には、フィーチャーとユーザーストーリーには大きな性質の違いがあるのですが、それは後述します。
*3:後者において成果物を実装するコードと捉えればHowと言えるし、そのフィーチャーの目的と捉えれば影響すなわちケイパビリティをWhatと表現することはそれほどおかしくは無いと思う。
*4:他の方法には、大きなテーマからユーザーストーリーを考え出す方法があり、どちらから言えば自分はこの考え方だった。
*5:ちなみにビジネス価値はシステムが対象としている業界によって変わるので、ある場合はユーザーストーリーと定義された物が、違う状況ではフィーチャーとして価値が出せることもあります。
BDD in Action Second Edtion の3章を読んだ感想
BDD in Action Second Editionの3章を読み終わりましたので、この章の感想を記録しておきます。
第3章は、Specurate(推測)フェーズとして、プロジェクトの最上位のコンセプトとなるビジョンをスタートに、ビジネスゴール, ケイパビリティ ,フィーチャー と順番に特定する方法を説明しています。
BDDのためというよりは、問題の解決方法を仮定するまでの考え方と色々なテクニックを紹介しています。
また章を通して「コミュニケーションを重ねること」と「解決方法を仮定して証明すること」の重要性を説いているようにも感じました。
ビジョン
ビジョンステートメントを作る。本の中では「キャズム」の著者が提唱し「アジャイル サムライ」のエレベーターピッチの書き方でも採用されているテンプレートを使っている。
ビジネスゴール
ビジョンに基づいて、達成したい事と達成する事で得られる利益を定義する。SMARTを活用した明確な目標を立てる。要求されたことの本質的なゴールを特定するために、その要求が「ビジョンにとってなぜ有益なのか?」を明らかにするためには、なぜなぜ分析を使っている。これについては「ザ・ゴール」を読むとよりわかりやすそう。
インパクトマッピング
「ゴール -> 関係者(アクター) -> インパクト(影響/効果) -> 成果物」と関係性を定義することで、必要なケイパビリティあるいはフィーチャーを特定していく。
- ゴール: ペインポイントとして、現在抱えている問題を解決する方法を定義する。 (もちろんSMARTやなぜなぜ分析も使って洗練させると良いと思う。)
- 関係者: ゴールに関係するステークホルダーやユーザーを定義する。直接的/間接的を問わず、ゴールに対して恩恵を受ける人を考える。
- インパクト: 関係者がそのように振る舞うことで、ゴールが達成できると仮定する行動を書く。
- 成果物: その行動を起こすための「何か」を仮定する。ソフトウェアやツールで実現することにこだわる必要な無い。
インパクトマッピングは、仮定の検証を繰り返す事でアップデートしていく。なので検証で判断するための測定基準も考える。
パイレートキャンバス
インパクトマッピング、パイレートメトリクス、そして制約理論*1を組み合わせて、幅探索アプローチで(TODO: フィーチャーやケイパビリティ)を特定するやり方。
パイレーツメトリクスはAARRR*2と呼ばれる、Aqcuition(獲得)、Activation(活性化)、Retention(保持)、Referral(紹介)、Return(利益)の5つの視点で測定基準で分析を行うこと。本の中ではこれらの測定基準を使って、ToCにおけるボトルネック、つまり制約を特定していく。
パイレートキャンバスはこれを応用して、この5つの視点ごとに業務の制約とその制約を測る指標を考える。その後にこの制約と指標を目標にして、インパクトマッピングと同じように関係者やインパクト、成果物を特定する。この成果物をエピックになる。
インパクトマッピングとパイレートキャンパスの使い分け
明確で優先するべき目標が定まっているかどうかがポイントと感じた。目標に対する関係者・インパクト・成果物を特定するプロセスはどちらも同じなので、インパクトマッピングはどのみち使っていくことになる。
パイレートキャンパスは目標を考えることから始めるので、多くの目標が生まれ、それぞれに対するインパクトや成果物を順番に特定する幅優先アプローチとなる。この方法は労力や時間は掛かるが、幅広いアイデアが生まれる可能性が高く、どの目標から優先して取り掛かるべきかが把握しやすいことが特徴。
パイレートキャンバスを使って、ペインポイントとなる潜在的な制約(ボトルネック)を特定し、それを向上させるためのケイパビリティ(エピック)について、ソフトウェアによる解決方法に拘らずに話し合って見つけていくやりとりは、劇中作とは言え興味深く読むことができました。
感想
相手の要望を初めから、ソフトウェアで解決する方法から考えたり、相手からも「ソフトウェアをこうして欲しい。」といった感じで要求されることが多いかった。これはつまり、Featureの要望や検討をしていると言うことになるのだが、正しいフィーチャーを特定するためには、まず「何ができるようになると嬉しいのか」といったケイパビリティを特定するプロセスが重要でした。
この章ではプロジェクトや企業のビジョンから、フィーチャーを特定するまでの説明だったが、ゴールとケイパビリティの特定に重点が置かれているように感じた。フィーチャーについては大枠を掴んだだけといった感じだったので、次章以降でこの荒削りなフィーチャーを洗練する方法を説明していくと思います。
英語の表現を日本語で表すのが難しい言葉が多くなってきた。そのまま英語で表現した方が意味がより理解できる気がする。この章は特にそれを感じた。