White Box技術部

WEB開発のあれこれ(と何か)

【主にスクラム向け】プロダクトバックログフォーマットと運用フローについて

プロダクトバックログのテンプレートが欲しい

「プロダクトバックログなんて、そんな何個も作るものじゃないでしょ」

とか思っていたときもあったのですが、ここ数年、公私合わせて、年2、3個はプロダクトバックログ(PBL)を作り、その都度フォーマットを作っていたので、 いい加減(自分のためにも)PBLのフォーマットを決め、テンプレートを用意するようにしました。

これが「さいきょうのPBLだ!」

最強なのかはともかく、現時点で一番わかりやすく運用しやすいPBLのフォーマットは、以下のようになりました。

f:id:seri_wb:20190610002920j:plain

テンプレートにはGoogleスプレッドシートを利用しています。

なぜかというと、現状ではこれが一番全体の把握がしやすく、多くの人が特に追加費用無しでアクセス可能であり、独自カスタマイズを許容できるツールになるからです。
セルに記述するというのも、アイテムを追加する人の心理的障壁を下げていますし、途中を許容しつつ議論ができるのも利点だと思うので、 特に強いこだわりがないのであれば、現状はスプレッドシート一択で良いのではないでしょうか。

PBL項目

スプレッドシートの各項目は以下のような用途を想定しています。

項目 用途
No 通番。優先度で前後させても通番を振り直しはしない
エリア 開発範囲を示すときに使う。LeSS Hugeのエリアに相当
ステータス プロダクトバックログアイテムの状態を表す(詳細は後述)
誰が 誰のための価値になるものを作るのかを明示する。ユーザストーリのWho
何をしたい どういったことを実現するのか。ユーザストーリのWhat
それはなぜか(価値) どういう価値が提供されるのか(リリース後に検証する内容)を書く。ユーザストーリのWhy
ポイント ストーリポイントを書く
レビューチェック内容 このアイテムでどういったことを実現するのかを具体的に書く。スプリントレビューの受け入れ基準に相当(How)
備考 SKIPにした理由や、アイテム分割した際の情報などを書く

PBLに携わる人数が多ければ、起票者の項目もあると便利です

スクラムでのPBL運用フロー

ではこのPBLをスクラムのイベントに沿って運用するフローを提示してみたいと思います。

1. プロダクトバックログリファインメントを実施

  1. 各自がプロダクトバックログにTODOでアイテムを追加する(事前記入だったり、時間を取ってやったりする)
  2. プロダクトバックログリファインメントを実施し、アイテムの上の方から内容を確認していく
    1. TODOを追加した人から内容の説明を受け、チーム内のゴール認識を合わせる(レビューチェック内容を決める)
    2. アイテムのストーリーポイントを見積もる
    3. アイテムの状況に合わせて、ステータスを変更する
  3. アイテムの粒度が大きすぎたり、不明瞭な場合は、実現可能な粒度にアイテムを分割して、再度内容を精査する

ステータスは以下のような意味合いで利用します。

ステータス 意味
TODO アイテムを起票した状態で、まだチームへの展開が行われていない
READY 展開された内容が、現時点で着手可能
NOT READY 展開された内容が、まだ着手不可能
DOING スプリントで実施中
DONE アイテムが完了している
WAIT 現状議論する段階にない(議論できない)
PENDING どうするかを保留した
SKIP 一旦実施しないこととした

2. スプリントプランニングを実施

PBL上位のアイテムでステータスがREADYとなっているものからスプリントで実施するアイテムを決め、そのアイテムのステータスをDOINGにする。

選んだアイテムはスプリントバックログ(ホワイトボードを使うのがお奨め)に移動し、タスクを詳細化・細分化する。

3. スプリントレビューを実施

スプリントで作成した成果(プロダクトインクリメント)を、PBLのレビューチェック内容と照らし合わせながらレビューする。

実現できていればステータスはDONEにし、追加で実現したいことが見つかったならPBLに新しいアイテムをTODOで追加する。

4. 繰り返し

これらを繰り返していく。

まとめ

個人的には、価値を生み出す作業にどれだけ注力できるかが、アジャイル開発の目指すところかなと思っているので、それ以外の部分については可能な限り力をかけずに済ませるようにしたいです。

ちょっとでもPBLのフォーマットに迷うくらいであれば、とりあえずこのフォーマットに書き殴ってみて、後で適当なものに切り替えてみるとかはどうでしょうか。

【MySQL 8向け】MyBatis Migrationsのコンテナ化

JVM系のマイグレーションツールの導入

新しくSpring Bootの開発環境をコンテナ上で作る上で、DBマイグレーションツールのMyBatis Migrationsをコンテナで使えるようしたので、その手順をまとめておきます。

f:id:seri_wb:20190605223604p:plain

Docker Composeの構成

作っていた開発環境のマイグレーション関連部分に絞ってディレクトリ構成を表すと、以下のようになっています。

$ tree -d
.
├── sample
│   └── migration
│       ├── drivers
│       └── sample
│           ├── environments
│           └── scripts
└── mysql
    ├── conf.d
    ├── data
    ├── log
    ├── sql
    └── tmp
        └── data

プロジェクトルートにdocker-compose.ymlを置き、それ起因でコンテナを運用するようにしています。

マイグレーション自体はdocker composeを持つリポジトリとは別で管理しているので、 アプリケーション名のディレクトリ(ここではsample)配下に、migrationディレクトリとしてgit cloneして利用するイメージです。

  • docker-compose.yml
version: '3.7'
services:
  migration:
    build:
      context: ./sample/migration/
    image: sample-migration
    container_name: sample_migration
    command: /bin/bash
    tty: true
    working_dir: "/sample/migration"
    volumes:
      - ./sample/migration:/sample/migration:cached
    depends_on:
      - db

  db:
    image: mysql:8
    container_name: sample_db
    environment:
      MYSQL_ROOT_PASSWORD: "root"
      #MYSQL_DATABASE: "sample"
      MYSQL_USER: "admin"
      MYSQL_PASSWORD: "admin"
    volumes:
      - ./mysql/sql:/docker-entrypoint-initdb.d
      - ./mysql/conf.d:/etc/mysql/conf.d
      - ./mysql/data:/var/lib/mysql
      - ./mysql/tmp/data:/tmp/data
      - ./mysql/log:/var/log/mysql
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    ports:
      - "3306:3306"

このdocker-compose.ymlでは、MySQL 8のコンテナを立ち上げ、その後マイグレーション用のコンテナを立ち上げています。

MySQLのコンテナイメージ

MySQLのコンテナイメージでは、MYSQL_DATABASEを指定することで、予めDBを作成することもできるのですが、 DBは複数利用することを考え、コンテナイメージ作成時に実行されるinit.sqlで作成するようにしています。

CREATE DATABASE sample CHARACTER SET utf8mb4;
GRANT ALL ON *.* TO 'admin'@'%' WITH GRANT OPTION;
CREATE USER 'admin'@'localhost' IDENTIFIED BY 'admin';
CREATE USER 'admin'@'127.0.0.1' IDENTIFIED BY 'admin';
FLUSH PRIVILEGES;
MyBatis Migrationsのコンテナイメージ

本題のMyBatis Migrationsのコンテナイメージは、OpenJDKのイメージをベースに作成します。

  • sample/migration/Dockerfile
FROM openjdk:11-slim

RUN apt-get update && apt-get install -y \
        wget \
        unzip \
    && apt-get clean \
    && rm -rf /var/lib/apt

RUN wget https://github.com/mybatis/migrations/releases/download/mybatis-migrations-3.3.5/mybatis-migrations-3.3.5-bundle.zip \
    && unzip mybatis-migrations-3.3.5-bundle.zip -d /usr/local \
    && rm mybatis-migrations-3.3.5-bundle.zip

ENV MYBATIS_MIGRATIONS_HOME /usr/local/mybatis-migrations-3.3.5
ENV PATH $PATH:$MYBATIS_MIGRATIONS_HOME/bin

WORKDIR /sample/migration

COPY . .
CMD ["/bin/bash"]

docker-compose up -d後(コンテナイメージ起動後)、コンテナに入ってマイグレーションスクリプトのテンプレートを作成します。

mkdir sample
cd sample
migrate init

この際、コマンド実行ディレクトリにdriversディレクトリが作成されますが、複数DBのスクリプト管理をすることを考え、WORKDIR配下にdriversを移動させます。

実際に利用するMySQLJDBCコネクタは、以下からダウンロードし、先ほどのディレクトリに配置してください。

その後、プロパティの以下を生成された値から変更します。

  • time_zoneをAsia/Tokyoに
  • script_char_setをアンコメント
  • JDBCに必要な設定を追加(driverのクラスに注意)
  • send_full_scriptをアンコメントしてfalseに
  • driver_pathをJDBCコネクタを配置したコンテナ内の絶対パス

  • sample/migration/sample/environments/development.properties

## Base time zone to ensure times are consistent across machines
time_zone=Asia/Tokyo

## The character set that scripts are encoded with
script_char_set=UTF-8

## JDBC connection properties.
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://db:3306/sample
username=admin
password=admin

#
# A NOTE ON STORED PROCEDURES AND DELIMITERS
#
# Stored procedures and functions commonly have nested delimiters
# that conflict with the schema migration parsing.  If you tend
# to use procs, functions, triggers or anything that could create
# this situation, then you may want to experiment with
# send_full_script=true (preferred), or if you can't use
# send_full_script, then you may have to resort to a full
# line delimiter such as "GO" or "/" or "!RUN!".
#
# Also play with the autocommit settings, as some drivers
# or databases don't support creating procs, functions or
# even tables in a transaction, and others require it.
#

# This ignores the line delimiters and
# simply sends the entire script at once.
# Use with JDBC drivers that can accept large
# blocks of delimited text at once.
send_full_script=false

# This controls how statements are delimited.
# By default statements are delimited by an
# end of line semicolon.  Some databases may
# (e.g. MS SQL Server) may require a full line
# delimiter such as GO.
# These are ignored if send_full_script is true.
delimiter=;
full_line_delimiter=false

# If set to true, each statement is isolated
# in its own transaction.  Otherwise the entire
# script is executed in one transaction.
# Few databases should need this set to true,
# but some do.
auto_commit=false

# If set to false, warnings from the database will interrupt migrations.
ignore_warnings=true

# Custom driver path to allow you to centralize your driver files
# Default requires the drivers to be in the drivers directory of your
# initialized migration directory (created with "migrate init")
driver_path=/sample/migration/drivers

# Name of the table that tracks changes to the database
changelog=CHANGELOG

# Migrations support variable substitutions in the form of ${variable}
# in the migration scripts.  All of the above properties will be ignored though,
# with the exception of changelog.
# Example: The following would be referenced in a migration file as ${ip_address}
# ip_address=192.168.0.1

これで準備完了です。

使い方

実際に利用する際は、initしたときのように起動後にコンテナ内に入ってmigrate upを実行でもいいですが、以下のように直接実行することもできます。

$ docker-compose up -d
$ docker-compose exec migration bash -c "cd sample && migrate up"

初回の起動スクリプトなどを組む場合は、こちらの使い方が便利です。

手順は以上です。よいコンテナライフを!

PythonでのAWS Lambda開発メモ(利用開始編)

Lambdaを使うことになった経緯

Lambdaの利用経験がなかった私が、Lambda利用を決めた流れは

  1. 1日1回のS3のファイルチェック処理が必要になる
  2. ファイルがなかったらSlack通知してくれればいい(簡単なスクリプトでOK
  3. 今のところ、AWS上にサーバは立ってない
  4. 調べてみたらLambdaもスケジュール実行できるらしい
  5. ならインフラコストを考えるとLambdaがいいか

でした。

Lambdaで利用する言語については、JupyterやGlueと合わせてPythonに。

LambdaやPythonの学習コストは、自分が払って展開すれば大したことにはならないだろうという見込みもあり、

「ついに私もサーバレスか・・・」

と感慨深く利用を始めました。

Lambda利用のために何をやったらいいのか?

Lambdaはマネージドサービスなので、AWS Console内で作業ができるようになっています。 エディタも用意されており、そこのテンプレートコードにあるlambda_handle関数に処理を書けば、指定のトリガーで実行される仕組みです。

言えば簡単なのですが、以下を懸念していました。

  • Pythonのライブラリ利用について
  • コード管理
  • ログ管理
  • テスト実行(テストコードではなく)
  • デプロイの自動化

なので、今回はこれらについて記事にしようと思います。


Pythonのライブラリ利用について

Lambdaを利用した際に、最初から用意されているライブラリはBoto3で、それ以外を利用したい場合は、利用するPythonのコードと一緒にzipでまとめて、アップロードする必要がありました。

今回はSlack通知の処理部分にrequestsを使いたかったので、zipでまとめてアップロードしています。

コード管理

そんな事もあって、幸か不幸かLambdaのコンソール上だけでは完結しなかったため、Lambdaプロジェクト用のGitHubリポジトリを作成し、コードは当該リポジトリ配下で記述するようにしました。

ディレクトリ構成はLambdaコンソールを参考に、リポジトリルート直下に関数名のディレクトリを作成して、そこにコードを置き、リポジトリルート直下には以下のようなシェルファイルを作成して、アップロード用のzipを作る、といった具合の構成です。

rm -rf upload.zip
pip3 install requests -t ./someLambdaFunction
cd someLambdaFunction
zip -r ../upload.zip *

しくじりポイント

zipは展開後が関数名のディレクトリになるのではなく、ソースコードの配置になっていないと駄目でした。

Lambdaコンソール上では、例えばsomeLambdaFunction/lambda_function.pyのような構成になっているので、てっきりsomeLambdaFunctionディレクトリもzipに含めるのかと思っていましたが、含めてしまうとsomeLambdaFunction/someLambdaFunction/lambda_function.pyのようになり、動作しなくなります。

環境変数の利用

WebhookのURLなどは環境変数を利用することで、コードから除去することができます。

ログ管理

ログについては標準出力に出しさえすれば、特に設定もなくCloudWatch Logsで見れました。

テスト実行(テストコードではなく)

関数のテスト実行はLambda上でテスト用のトリガーを作成して、それを実行すればできます。

処理内部で外部パラメータを利用しないのであれば、トリガーで渡すパラメータはデフォルトのままで問題ありません。

ローカルで開発・実行できるように準備もしたのですが、アクセスキーの発行を渋られて、結局Lambda上でのみ動作テストすることになってしまいました。。

デプロイの自動化

デプロイは少し曲者で、AWSのマネージドで完結させることはできなそうでした。

最初はCodeDeployでいけるかと思ったのですが、CodeDeployはデプロイされているLambdaのバージョンを切り替えることしかできないようでした。 CloudFormationを使えばいけるのかもしれませんが、ちょっとイメージしているのとは違うというか、CIと流れるように実行したかったといいますか。

lambda-uploaderを使えばできそうだったのですが、アクセスキーが・・・
AWS Lambda Pythonをlambda-uploaderでデプロイ | DevelopersIO

というわけで、これはまだ手動です。

Lambdaを使ってみて

「思っていたより簡単に利用できてびっくりした」というのが素直な感想です。

サーバの用意もいらないし、実行のコスト(金額的な意味で)もほとんどかからないしで、いいとこづくめだなと。 でもその恩恵に預かれるのはLambdaを知っている人だけなんだなぁと思うと、読みかけのサーバレス本をちゃんと読んで、もっと使いこなせるようになっておこうという気になりました。

知は力なりとは言ったものですね。

ログ出力指針の書き方

アプリケーションのログ出力指針を作ったときに、どんな事を考えていたのかを思い出しながら、 ログ出力指針について書いていきたいと思います。

ログ指針作成にあたって

そもそもなぜログが欲しいのか?何に利用するのか?

これに対する答えを置いておかないと、内容がぶれてしまうため、最初は目的を定義しました。

目的の作成

目的としてすぐ思いついたのは障害時対策のような守りの要素でしたが、 ビッグデータに取り込んで学習に使うというような攻めの要素にも使いたかったので、 目的には両方を明記しました。

目的を定めたあとは、どのタイミングでログが出力されていればいいのか、必要となる情報はなんなのか、解析しやすいフォーマットは・・・

のように思考が進んだのですが、作成した指針もどんなときにも利用できるというわけでもないだろうし、 無条件に鵜呑みにされると再度考察する機会がなくなると思い、この指針の対象とする範囲を定義しました。

適用範囲の設定

適用範囲を定めたことで、範囲外のログはどういう扱いにするか?という課題が出てきたのですが(例でいうとサーバログ)、 このときは「自分たちはここの範囲のログを整理することとして考えています。他は私たちが持ちますか?そちらで持ちますか?」というように 関連するチームに聞いて調整をしました。

もしサーバログも持つことになっていた場合は、ログ指針別で定義するのではなく、これを拡張していたと思います。

どんなときログを埋め込むのか?または埋め込まないのか?

あとはレビューでも客観的にチェックができるように、ログの埋め込むポイントを定義しました。

出力禁止項目も定義することで、出しては駄目な項目がないかを、このタイミングでチェックしたり話し合ったりしていました。

ログのフォーマットは?

フォーマットに関しては扱いやすさを考慮して設定しました。

解析時にはLTSVが便利だったので、ファイルにはLTSV。
開発中のコンソールは見れればいいということでTSVにしました。

細かい出力順などは利用するフレームワークに基本倣うようにしています。

出力項目については、出せるもの、できれば出したいものなど、とりあえず列挙し、適時選定していこうというスタイルを取りました。

ログの保持方法

ここらへんまできて、アプリケーションとしてどういうものになるのかを置かなければいけないと思っていました。 Fluentdで収集するのか、保持期間はどうするのか、アプリケーション起動にはコンテナを使うのか、起動するインフラ環境はオンプレなのかクラウドなのか、などなど。

このときはインフラ担当者と作業責務が分かれていたため、何をどっちが担当するのかを一つずつ確認しながら行う必要があり、けっこう大変でした。

そしてそろそろ面倒くさいなーと思い、まとめに入ったログ指針が以下のようなものです。


ログ出力の目的

以下の目的のため、ログ出力を行います。

  • 攻撃や事故の予兆を検知し、早期対策するため
  • 攻撃や事故の事後調査のため
  • アプリケーションの運用監査のため
  • 行動ログから、アプリケーション改善の指針を定めるため

ログ出力要件

ログ出力の要件を記載します。

本設計の範囲とするログの種類

本ログ出力指針は、アプリケーションが出力するログを対象とします。

サーバへのアクセスログは対象外としますが、アプリケーションからDBへのアクセス(CURD)は対象とします。

ログに記録するべき内容・イベント

ユーザが画面上で行うイベントと、認証・アカウント管理や重要情報へのアクセスに関するイベントを記録してください。 特に個人情報に対してのアクセスは必ず記録するようにお願いします。

またログはユーザIDやユニークIDなど、行動実施者とイベントが紐付けれるように記録してください。

認証・アカウント管理や重要情報へのアクセスに関するイベント
  • ログイン・ログアウト(失敗も含む)
  • アカウントロック
  • ユーザ登録・削除
  • パスワード変更
  • 重要情報の参照
  • 主だった操作(商品購入、送金、メール送信など)

出力禁止項目

以下の内容はログに出力しないようにしてください。

  • パスワード
  • OAuth関連のキー情報
  • 個人情報

ログ出力項目

ログには、4W1H(いつ、誰が、どこで、何を、どのように)に従って、以下の項目を出力します。

  • 処理日時
  • アクセス元情報(ユーザIDなど)
  • アクセス対象(URL、ページ名、スクリプトIDなど)
  • 操作内容(閲覧、変更、削除など)
  • 捜査対象(リソースID、カテゴリIDなど)
  • 操作結果(成功・失敗、処理件数など)

また、アプリケーションのクラッシュ情報、Stacktraceなどを出力します。

ログフォーマット

LTSVで出力します。

ただし、コンソールにはTSVで出力します。

出力項目
項目 キー オプション
ログの出力日時 date yyyy-MM-dd HH:mm:ss.SSS
スレッド thread
ログレベル level
ロガーの名前 logger
ユーザID user
セッションID session
アクセスURL uri
ログメッセージ message
サンプル
date:2018-10-20 19:09:42.216 thread:reactor-http-nio-3   level:DEBUG logger:jp.sample.api.search.repository.ItemSearchRepository message:アプリケーションで設定するログメッセージ

ログ出力先

アプリケーションのログは、/var/log/app配下にアプリケーション単位で出力します。

例えばproduct-name(アプリケーション名)の場合は、/var/log/app/product-nameに用途別でログが出力されます。

ログファイル一覧

ログファイル名 用途
application.log アプリケーションが出力するログ
application_error.log アプリケーションログのERRORレベルのみ
gc.log JavaアプリケーションのGCログ
java_error%p.log Javaアプリケーションが異常終了した場合のログ(プロセス単位)
heap/dump.log ヒープエラーのダンプ

アプリケーションログのローテーション

アプリケーションログは日単位でローテーションします。

ログの保存期間

アプリケーションサーバ上では最低1週間、最大1ヶ月保持します。 不要になったログ(過去ログ)の削除は、アプリケーション側では実施せず、サーバ側(logrotate)で実施します。

その他、CloudWatchにログを転送し、長期保存を行います。

ログレベルの利用基準

ログレベル 意味合い 用途
FATAL 回復不可能なエラー 利用しない
ERROR エラー 想定外の処理の発生(システムエラー)に関する情報を出力する。Exception情報がある場合は、Stacktraceも本レベルで出力する。
WARN 警告 システム上エラーではないが、通常発生し得ない(発生を期待していない)処理の発生を記録する。業務処理のエラーを記録する場合も、本レベルで出力する。
INFO 情報 各種イベントの発火・実行の事実を出力する(行動ログ)
DEBUG デバック用の情報 開発時に参考となる情報を出力する。パラメータ値を出力する場合は、基本的にDEBUGとする。
TRACE デバッグよりも詳細な動作トレース 利用しない
Stacktraceの出力タイミング

エラーが発生した場合のStacktraceは、発生箇所で都度出力するのではなく、 エラー処理を実際に行う終端で出力します。

参考

LTSVの活用

LTSVはTSVでもあるので、タブ区切りの場所さえわかれば、以下のようにcutでデータを取得することが出来ます。

$ tail -n 10 application.log | cut -f 5

LTSVはキーがついているので、grepで対象のキーを指定して取得することができ、

$ egrep -o "message:[^タブ文字]*" application.log

※タブ文字はCtrl-vを入力後、Tabキーを押下することで入力できます

以下のようなPerlワンライナーで意図したキーのみを抽出し、好きにフォーマットすることも出来ます。

$ cat application.log | perl -F'\t' -nale '%h=map{split/:/,$_,2}@F;print"$h{date}\t$h{thread}\t"'



終わりに

いかがでしょうか?未完成なところも目につくと思います。

ですがこういうものは作ってそのまま見られなくなるか、また日の目を浴びたときに古くなっているということでそのまま捨てられたりするものだと思うので、 完璧なものでなくても良いと個人的には思います。

ではなぜログ指針の話をしたかというと、このような指針を元に、 自分たちのチームはどう考えてシステムを作っていくのかの意識合わせを行い、観念を共有することがシステム開発では一番大事だと思っているからです。

何もなければ考えをすり合わせるのも難しいので、適時このように課題への解決案を形にしながら、チームでチームのベースを作っていくのがいいのではないでしょうか。

それに捨てられず、ずっとメンテナンスされて、新しいメンバーがチームを知るために使える資料になっている可能性もないわけではないですからね。

参考文献

  • 安全なWebアプリケーションの作り方
  • 仕事ではじめる機械学習