back check スクラム開発体制における PjM の役割

こんにちは、匠平@show60です

昨年10月、ROXX の back check 事業部に開発エンジニアとして入社して3年が経つころにプロジェクトマネージャー (以下 PjM) へと肩書きが変わりました。

ここではスクラム開発を採用する back check 開発チームにおける PjM の役割について紹介したいと思います。

チームが直面してきた課題

2021年の春ころ、back check 開発チームはプロダクトバックログアイテム (以下 PBI) の不足という課題に直面していました。PBI がないとはつまり開発チームのお仕事がないことであり喫緊の課題でした。

主にプロダクトオーナー (PO) が中心となって顧客・他部署からのフィードバックの収集の見直し、リファインメントの効率化などを進めることで解消に向かっていったのですが、将来に渡って継続的に PBI を安定供給できる体制を整える必要があります。

PO、PjM の役割

一般的にイメージされる PjM のお仕事としては、プロジェクト範囲の定義・要件定義、予算・リソース・品質・リスクの管理、進捗管理・報告などがあります。

The roles and responsibilities of the Project Manager are as follows:

  • Define project scope
  • Gather requirements
  • Identify activities, dependencies, sequencing, and time estimates.
  • Identify resources needed
  • Manages the budget
  • Reports to business leadership on project progress
  • Focuses on process
  • Allocates tasks
  • Prioritizes features
  • Ensure quality
  • Manage vendors
  • Manages risk

参考: Project Manager vs Scrum Master vs Project Owner

固定のスクラムチームで自社プロダクトを運用する back check では、予算・リソース管理などは PO が持っており、品質・リスク・進捗に関することは開発チームが責任を持っています。

PO の役割についてはスクラムガイド (記述時点最新の2020年版) にその定義が存在します。

プロダクトオーナーは、効果的なプロダクトバックログ管理にも責任を持つ。たとえば、

  • プロダクトゴールを策定し、明⽰的に伝える。
  • プロダクトバックログアイテムを作成し、明確に伝える。
  • プロダクトバックログアイテムを並び替える。
  • プロダクトバックログに透明性があり、⾒える化され、理解されるようにする。

参考: スクラム公式ガイド 2020年版

この説明に加えて、「上記の作業は、プロダクトオーナーが⾏うこともできるが、他の⼈に委任することもできる。いずれの場合も、最終的な責任はプロダクトオーナーが持つ。」とあります。back check における PjM はまさにこの部分を担っており、これら PO の仕事の一部を委任される形で PBI の作成を行っています。

現在は主に CS や顧客からの要望・フィードバックから課題を見つけ出し、その課題に対するアプローチを考えて要件定義するまでの役割を担っています。作成された解決策は PO との確認を経て、プロダクトバックログは PO の責任下から外れることなく PBI として開発チームへ届けられます。

チーム分割の布石として

PjM という役割の採用はチームが直面していた PBI 不足の解消という短期的な目的だけでなく、将来的な開発チームの構成に向けた中長期な目的のための一ステップでもあります。

back check 開発チームはこの1年でスクラムガイドで推奨される10名を越えるまでにメンバー拡充してきました。1枚岩のスクラムチームでは小回りが効きづらく、すでに身動きが取りづらいところも散見されておりチームの分割の検討が開始されています。

チーム分割は、人数規模の問題を解決するためだけではなく、技術的・ビジネス的にも価値のある必要があります。

分割案のチェックリスト

  1. ミッションがあるか
  2. そのミッションはチームに限定合理的な意思決定をもたらす重力を生み出さないか
  3. 定常的に仕事が存在するか
  4. アーキテクチャ、ビジネス境界、チーム境界が一致しているか
  5. 分割後のチームのチームリーダーを任せられる人物はいるか
  6. 分割後、メンバー増加に十分耐えうるか

texta.pixta.jptexta.pixta.jp

各チームにはスクラムマスターと開発メンバーが存在し、PO が各チームに開発してもらいたい PBI を割り振るようなチーム分割をイメージしています。その際に PjM は自分が要件定義を担当した PBI の受け渡しの説明だけでなく、その後のメンバー間との対話を引き続き担うことも可能です。

PO とのコミュニケーションで齟齬があると致命的な問題になってしまうため、普段のコミュニケーションや信頼関係の構築が重要です。また当然他部署との連携も必要なので、全方面において良い関係を構築していく必要があります。

まとめ

ご紹介させていただいたように back check 開発チームの PjM は、CS や顧客からの要望・フィードバックからの課題発見から、解決方法の提案、要件定義までを主に行っています。 ユーザーにとっての負の解消だけでなく、その期待を超えるようなアイデアを考えて提案することもできます。

私がこれまで開発メンバーとして関わってきたころと比べ、ユーザーへの価値を決める初期のステップに携わることが多いため、実際にユーザーに届いて使ってもらえるときの喜びをより一層感じることができています。

当然それだけの責任も伴いますし実際に使われるまでは不安も大きいのですが、チームメンバーはその実現のための技術的な相談や必要なサポートをしてくれます。みんなで良いものを作ることにとことん向き合えることにとてもやりがいを感じています。

最後に

そんな私たち back check チームに興味をお持ちいただける方、リファレンスチェックという新しい市場のスタンダードを一緒に作っていきたいという仲間を絶賛募集しています。 ご紹介させてもらった PjM のポジションだけでなく、開発ポジション、デザイナーも募集していますので

herp.careers

herp.careers

herp.careers

MySQL 8.0 パーティショニングを理解する

この記事は個人ブログと同じ内容です

www.ritolab.com


パーティショニングは MySQL 5.1 から利用できますが、パーティショニングとは何者なのか。MySQL 8.0 でパーティショニングを理解していきます。

パーティショニング

パーティショニングでは、指定したルールに従いテーブルの各行をパーティション分割したファイルシステムに配分します。

これによって同じテーブルのレコードでもそれぞれ配分された箇所が内部的にはあたかも別のテーブルのように個別の場所に格納されます。

テーブルは 1 つで操作もこれまでと変わりませんが、ファイルシステムにおける内部的なデータの持ち方としてはパーティション分割されたそれぞれの場所に保存されていきます。

こうすることでパーティショニングすべき状況と合致する場合はデータの読み出しをはじめとした操作を効率的に行うことができ、クエリ実行時のパフォーマンスを向上させることができます。

dev.mysql.com

環境

MySQL 8.0 にて操作を行います。ストレージエンジンは InnoDB です。

MySQL 8.0 のパーティショニング方式

MySQL 8.0 でのパーティショニングは「水平パーティショニング」という方式が採用されています。これは、テーブルの各レコードを異なる物理パーティションに割り当てる方式です。

一方で、「垂直パーティショニング」と呼ばれる、カラムベースで異なる物理パーティションに割り当てる方式がありますが、これについては MySQL 8.0 ではサポートされていません。

対応ストレージエンジン

MySQL 8.0 でのパーティショニングついて対応しているストレージエンジンは InnoDB と NDB のみです。その他(MyISAM など)のストレージエンジンではパーティショニングを利用できません。

パーティションテーブル作成時の制約

テーブルのパーティション分割を行う際に、パーティションキーとして指定するすべてのカラムは、主キーを含めテーブルに含まれる可能性のあるすべての一意キーの一部である必要があります。

つまり、パーティション分割を行う際に指定するカラムは、そのテーブルにプライマリーキー、もしくはユニークキーを設定するのであればそれを構成するカラムである必要があります。(逆にこれらのキーを設定しない場合はその制約を受けません)

パーティショニングタイプ

MySQL 8.0 で利用可能なパーティショニングのタイプは、大きく分けて 4 つのタイプ +α があります。

RANGE パーティショニング

パーティショニング時に指定したカラムの範囲に含まれる値(整数)に基づいて行をパーティションに割り当てます。

  • ある数値を収録しているカラムの数値範囲(〜1000000、〜2000000, ... )
  • ある日付を収録しているカラムの年範囲(〜2022, 〜2023, ...)
CREATE TABLE `logs` (
    `logged_at` datetime NOT NULL,
    .,
    .,
    .
)
PARTITION BY RANGE (year(`logged_at`)) (
    PARTITION p2019 VALUES LESS THAN (2020),
    PARTITION p2020 VALUES LESS THAN (2021),
    PARTITION p2021 VALUES LESS THAN (2022),
    PARTITION p2022 VALUES LESS THAN (2023),
    PARTITION p2023 VALUES LESS THAN (2024),
    PARTITION pmax VALUES LESS THAN MAXVALUE
);

RANGE COLUMNS パーティショニング

DATE 型・DATETIME 型のカラムをパーティショニングキーとしてパーティション分割したい場合は RANGE COLUMNS パーティショニングを用います。

CREATE TABLE `employees` (
    `hired` date NOT NULL,
    .,
    .,
    .,
)
PARTITION BY RANGE  COLUMNS(hired) (
    PARTITION p202201 VALUES LESS THAN ('2022-02-01'),
    PARTITION p202202 VALUES LESS THAN ('2022-03-01'),
    PARTITION p202203 VALUES LESS THAN ('2022-04-01'),
    PARTITION p202204 VALUES LESS THAN ('2022-05-01'),
    PARTITION pmax VALUES LESS THAN (MAXVALUE)
);

LIST パーティショニング

パーティショニング時に指定したリストに基づいて行をパーティションに割り当てます。

  • あるグループを識別するための番号リストによるパーティション(A=3,5,6,9,17, B=1,2,10,11,19,20, ...)
CREATE TABLE `members` (
    `name` varchar(255) NOT NULL,
    `country_id` smallint unsigned NOT NULL
)
PARTITION BY LIST (`country_id`) (
    PARTITION pAfricaZone VALUES IN (247,213,244,256,20,251,291,233),
    PARTITION pAmericaZone VALUES IN (1,53,54,57,297,503,598,593),
    PARTITION pAsiaZone VALUES IN (66,81,94,357,850,886,963),
    PARTITION pEuropeZone VALUES IN (7,39,44,353,355,372,380,998),
    PARTITION pOceaniaZone VALUES IN (61,64,672,677,683,685,687,690)
);

LIST COLUMNS パーティショニング

文字列や DATE 型などのリストでパーティション分割したい場合は LIST COLUMNS パーティショニングを用います。

CREATE TABLE `residents` (
    `name` varchar(255) NOT NULL,
    `prefecture` varchar(255) NOT NULL
)
PARTITION BY LIST  COLUMNS(prefecture) (
    PARTITION pHokkaidoRegion VALUES IN ('北海道'),
    PARTITION pTohokuRegion VALUES IN ('青森県','岩手県','宮城県','秋田県','山形県','福島県'),
    PARTITION pkantoRegion VALUES IN ('東京都','茨城県','栃木県','群馬県','埼玉県','千葉県','神奈川県'),
    PARTITION pChubuRegion VALUES IN ('新潟県','富山県','石川県','福井県','山梨県','長野県','岐阜県','静岡県','愛知県'),
    PARTITION pKinkiRegion VALUES IN ('京都府','大阪府','三重県','滋賀県','兵庫県','奈良県','和歌山県'),
    PARTITION pChugokuRegion VALUES IN ('鳥取県','島根県','岡山県','広島県','山口県'),
    PARTITION pShikokuRegion VALUES IN ('徳島県','香川県','愛媛県','高知県'),
    PARTITION pKyushuRegion VALUES IN ('福岡県','佐賀県','長崎県','大分県','熊本県','宮崎県','鹿児島県','沖縄県')
);

HASH パーティショニング

指定したパーティション数に基づいて、行をパーティションに均等に割り当てます。

RANGE や LIST では集めたいものでパーティショニングしていましたが、HASH パーティショニングにおいては、指定値のハッシュに基づいて均等にパーティションに配分されていきます。

CREATE TABLE `diaries` (
    `create_date` date NOT NULL,
    `title` varchar(255) NOT NULL,
    `content` text NOT NULL
)
PARTITION BY HASH (to_days(`create_date`)) PARTITIONS 3;

指定するカラムの値、または式の返り値は整数である必要があります。(上記の例では DATE 型を整数に変換(to_days())した値を指定している)

例えばパーティションを 3 つに分割するとして、振り分け先パーティションの決定は以下のように行われます。

2022-03-01 -> MOD(TO_DAYS('2022-03-01'),3) -> 1
2022-03-02 -> MOD(TO_DAYS('2022-03-02'),3) -> 2
2022-03-03 -> MOD(TO_DAYS('2022-03-03'),3) -> 0
2022-03-04 -> MOD(TO_DAYS('2022-03-04'),3) -> 1
2022-03-05 -> MOD(TO_DAYS('2022-03-05'),3) -> 2
2022-03-06 -> MOD(TO_DAYS('2022-03-05'),6) -> 0
.
.
.

このように剰余計算で配分先パーティションを決定しているため、値が y=ax のような直線的、比例的に増加していくものであると配分が均等になり良いです。

LINEAR HASH パーティショニング

CREATE TABLE `diaries` (
    `create_date` date NOT NULL,
    `title` varchar(255) NOT NULL,
    `content` text NOT NULL
)
PARTITION BY LINEAR HASH (year(`create_date`)) PARTITIONS 3;

HASH パーティショニングとの違いは配分するパーティションを決定するための算出方法です。

HASH パーティショニングは剰余で算出していた一方で、LINEAR HASH パーティショニングではビット論理積を使ったアルゴリズムを利用するので計算が速いという利点があります。

// 配分先パーティションの決定
V = POWER(2, CEILING(LOG(2,3))) = 4 // パーティション数(=4)以上の 2 の累乗を算出
N = YEAR('2022-03-01') & (V(=4) - 1)
   = 2022 & 3
   = b'11111100110' & b'11'
   = 2 // パーティション p2 へ格納

2022 年のレコードであれば パーティション p2 へ配分されることを実際に insert して確認してみます。

INSERT INTO diaries (create_date, title, content) VALUES ('2022-03-01', 'test', 'test');
mysql> SELECT PARTITION_NAME,TABLE_ROWS FROM INFORMATION_SCHEMA.PARTITIONS WHERE TABLE_NAME = 'diaries';
+----------------+------------+
| PARTITION_NAME | TABLE_ROWS |
+----------------+------------+
| p0             |          0 |
| p1             |          0 |
| p2             |          1 |
+----------------+------------+

パーティション p2 へ配分されることを確認できました。

ちなみに計算上パーティション数以上の数値になってしまうことがあり、その場合は算出された値を元に更に処理が行われます。

// 算出された値がパーティション数以上の場合
V = POWER(2, CEILING(LOG(2,3))) = 4
N = YEAR('2023-03-01') & (V(=4) - 1)
   = 2023 & 3
   = b'11111100110' & b'11'
   = 3 // パーティション数以上であるのでこの値を元に更に計算
N' = N(=3) & (V(=4)/2 - 1)
   = 3 & 1
   = b'11111100111' & b'1'
   = 1 // パーティション p1 へ格納

2023 年のレコードであれば パーティション p1 へ配分されることを実際に insert して確認してみます。

INSERT INTO diaries (create_date, title, content) VALUES ('2023-03-01', 'test', 'test');
mysql> SELECT PARTITION_NAME,TABLE_ROWS FROM INFORMATION_SCHEMA.PARTITIONS WHERE TABLE_NAME = 'diaries';
+----------------+------------+
| PARTITION_NAME | TABLE_ROWS |
+----------------+------------+
| p0             |          0 |
| p1             |          1 |
| p2             |          1 |
+----------------+------------+

パーティション p1 へ配分されることを確認できました。

この再計算が行われることについては、V としてパーティション数以上の 2 の累乗を算出している関係上発生しています。

---
P = 3(パーティション数)
V = POWER(2, CEILING(LOG(2, P))) = 4
---

つまり、

  • P=2 なら V=2
  • P=3 なら V=4
  • P=4 なら V=4
  • P=5 なら V=8

といった具合になるので、パーティション数が 5 の場合には初回の計算時点では N=0-8 の値が算出されてくることになります。そして、値が 5 以上であれば再計算が走ります。(しかも 5-8 で再計算すると N=0-1 にしかならないので配分されるパーティションに偏りがでます)

ただしこれは回避可能で、それにはシンプルにパーティション数を 2 の累乗で設定すればパーティション数よりも大きな V にはならないため、再計算を回避でき、一度の計算で算出しきれるようになります。(P= 2|4|8|16|32|64|128|256|512|1024)

KEY パーティショニング

こちらも HASH パーティショニング同様、指定したパーティション数に基づいて、行をパーティションに均等に配分します。

HASH パーティショニングでは、判定する元となる値の算出をユーザー側で定義(DATE型を整数に変換したり)しましたが、KEY パーティショニングの場合は MySQL 側で提供されます。

パーティショニングキーを指定しない場合はプライマリキー、ユニークキーの順で存在するカラムがパーティショニングキーとして利用されますが、パーティショニングキー(カラム)を指定する場合も含めて、制約がいくつかあります。

  • プライマリキーがある場合は、その一部かすべてを構成しているカラムであること。
  • プライマリキーは無くてユニークキーがある場合はユニークキーがパーティショニングキーとして使われる。
    • この場合も同じく、カラムがユニークキーの一部かすべてを構成しているカラムであること。
    • ユニークキーの場合、NOT NULL 制約がついていること。
CREATE TABLE `players` (
    `id` bigint unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(255) NOT NULL,
    PRIMARY KEY (`id`)
)
PARTITION BY KEY () PARTITIONS 10; -- id がパーティショニングキーとして利用される

サブパーティショニング(複合パーティショニング)

パーティション分割したものを更にパーティション分割するというものです。

RANGE または LIST でパーティション化されたテーブルに対して、 HASH もしくは KEY でのサブパーティショニングが可能です。

CREATE TABLE `logs` (
    `logged_at` datetime NOT NULL,
    .,
    .,
    .,
    .
)
PARTITION BY RANGE (year(`logged_at`))
    SUBPARTITION BY HASH (to_days(`logged_at`)) (
    PARTITION p2019 VALUES LESS THAN (2020) (
        SUBPARTITION s0 ENGINE = InnoDB,
        SUBPARTITION s1 ENGINE = InnoDB
    ),
    PARTITION p2020 VALUES LESS THAN (2021) (
        SUBPARTITION s2 ENGINE = InnoDB,
        SUBPARTITION s3 ENGINE = InnoDB
    ),
    PARTITION p2021 VALUES LESS THAN (2022) (
        SUBPARTITION s4 ENGINE = InnoDB,
        SUBPARTITION s5 ENGINE = InnoDB
    ),
    PARTITION p2022 VALUES LESS THAN (2023) (
        SUBPARTITION s6 ENGINE = InnoDB,
        SUBPARTITION s7 ENGINE = InnoDB
    ),
    PARTITION pmax VALUES LESS THAN MAXVALUE (
        SUBPARTITION s8 ENGINE = InnoDB,
        SUBPARTITION s9 ENGINE = InnoDB
    )
);

パーティショニングでのパフォーマンス向上を試す

パーティショニングを行い、パフォーマンスがどれくらい向上するのかをみてみます。

CREATE TABLE `logs` (
    `logged_date` datetime NOT NULL
)
PARTITION BY RANGE (year(`logged_date`)) (
    PARTITION p2020 VALUES LESS THAN (2021),
    PARTITION p2021 VALUES LESS THAN (2022),
    PARTITION p2022 VALUES LESS THAN (2023)
);

RANGE パーティショニングを採用しました。ここに約 2400 万件のレコードを挿入して、それぞれのパーティションに配分してあります。

+--------------------+
| count(logged_date) |
+--------------------+
|           24300030 |
+--------------------+
+----------------+------------+
| PARTITION_NAME | TABLE_ROWS |
+----------------+------------+
| p2020          |    8086082 |
| p2021          |    8086074 |
| p2022          |    8086026 |
+----------------+------------+

他に、パーティションなしと、パーティション無しだがインデックスありのパターンで 3 回試して比較してみます。

-- パーティションなし・インデックスなし
mysql> SELECT logged_date FROM logs WHERE logged_date >= '2022-01-01 00:00:00' AND logged_date <= '2022-12-31 23:59:59';
 -> 8100010 rows in set (15.18 sec)

-- パーティションなし・インデックスあり
mysql> SELECT logged_date FROM logs WHERE logged_date >= '2022-01-01 00:00:00' AND logged_date <= '2022-12-31 23:59:59';
 -> 8100010 rows in set (6.37 sec)

-- パーティションあり
mysql> SELECT logged_date FROM logs PARTITION (p2022);
 -> 8100010 rows in set (4.98 sec)

パーティションもインデックスも無いテーブルに比べて、インデックスだけ張ったテーブルでは約 2.4 倍早く、パーティションありだと約 3 倍早くなりました。

パーティション無くてもインデックス張っていればそれなりですが、パーティションあると更に早くなっています。

以下は実行計画です。

-- パーティションなし・インデックスなし
+----+-------------+-----------+------------+------+---------------+------+---------+------+----------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref  | rows     | filtered | Extra       |
+----+-------------+-----------+------------+------+---------------+------+---------+------+----------+----------+-------------+
|  1 | SIMPLE      | logs      | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 23707015 |    11.11 | Using where |
+----+-------------+-----------+------------+------+---------------+------+---------+------+----------+----------+-------------+

-- パーティションなし・インデックスあり
+----+-------------+----------+------------+-------+----------------------------+----------------------------+---------+------+----------+----------+--------------------------+
| id | select_type | table    | partitions | type  | possible_keys              | key                        | key_len | ref  | rows     | filtered | Extra                    |
+----+-------------+----------+------------+-------+----------------------------+----------------------------+---------+------+----------+----------+--------------------------+
|  1 | SIMPLE      | logs     | NULL       | range | our_logs_logged_date_index | our_logs_logged_date_index | 5       | NULL | 11708590 |   100.00 | Using where; Using index |
+----+-------------+----------+------------+-------+----------------------------+----------------------------+---------+------+----------+----------+--------------------------+

-- パーティションあり
+----+-------------+---------+------------+------+---------------+------+---------+------+---------+----------+-------+
| id | select_type | table   | partitions | type | possible_keys | key  | key_len | ref  | rows    | filtered | Extra |
+----+-------------+---------+------------+------+---------------+------+---------+------+---------+----------+-------+
|  1 | SIMPLE      | logs    | p2022      | ALL  | NULL          | NULL | NULL    | NULL | 8086026 |   100.00 | NULL  |
+----+-------------+---------+------------+------+---------------+------+---------+------+---------+----------+-------+

注目すべきは rows(黄色の部分)で、やはりパーティションで区切られている分、スキャンしようとしている行数も比較的少ないです。

パーティションなし・ありで比較すると、パーティションありは、インデックスなしの約 34%、インデックスありの約 70 % ほどでした。

次に、これらのレコードを削除してみます。

-- パーティションなし・インデックスなし
DELETE FROM your_logs WHERE logged_date >= '2022-01-01 00:00:00' AND logged_date <= '2022-12-31 23:59:59';
 -> 8100010 rows affected (44.61 sec)

-- パーティションなし・インデックスあり
DELETE FROM our_logs WHERE logged_date >= '2022-01-01 00:00:00' AND logged_date <= '2022-12-31 23:59:59';
 -> 8100010 rows affected (6 min 38.57 sec)

-- パーティションあり
ALTER TABLE my_logs DROP PARTITION p2022;
 -> 0 rows affected (0.17 sec)

パーティションなしの DELETE ステートメントは 1 行ずつ削除していくのでやはり時間がかかりますが、パーティションありの DROP PARTITION は TRUNCATE TABLE ステートメント的に処理してくれるため早いです。

パーティションなし・ありで比較すると、インデックスもないテーブルの 263 倍早く、インデックスありの方は index 再構築入る分更に遅いので、それに比べると 2345 倍早い。という結果になりました。

まとめ

大規模データを扱う時にとても有利になりそうなパーティショニング。概念を知っておくことは大切だなと思います。

今回で全てに触れたわけではないので、またの機会に更に潜ってみたいと思います。

dev.mysql.com


現在 back check 開発チームでは一緒に働く仲間を募集中です!!

herp.careers

https://herp.careers/v1/scouter/GMnGlADgFBtQherp.careers

herp.careers

herp.careers

herp.careers

herp.careers

herp.careers

M1 mac で MySQL コンテナを使う方法

この記事の Canonical はこちらです。

toyo.hatenablog.jp

backcheck事業部の前田です。

つい先日、我が家にM1 Macが届きました。
「Dockerまわりがつらいよ」という話は聞いていましたが、私もしっかりと MySQL のコンテナが立ち上がりませんでした。
軽くググっても「むむ・・・?🤔」と思う結果だったので、自分でしっかりまとめてみようと思います。

そもそもの問題

M1 MacDocker 公式の MySQL コンテナをそのまま使おうとすると、以下のエラーが出て使用できません。

ERROR: no matching manifest for linux/arm64/v8 in the manifest list entries

これは雑に説明すると、「このコンテナは ARM64 に対応してないよ」ということです。
そして、 M1 チップは ARM64 アーキテクチャを採用しています。

解決する方法

色々と調べてみると、主に3つの方法があるようです。

  • arm64v8/mysql イメージを使用する
  • mysql/mysql-server イメージを使用する
  • --platform linux/amd64 オプションを使用する

順番に見ていきましょう。

MySQLのみを使いたいなら arm64v8/mysql

Docker の公式リポジトリに、 AMD64 (Intel の CPU のアーキテクチャ) 以外のプラットフォームの案内があります。

github.com

それによると、

Some images have been ported for other architectures, and many of these are officially supported (to various degrees).

とあり、程度差はありますが、公式にサポートされているイメージがあることがわかります。
そして、その MySQL のイメージが arm64v8/mysql です。

hub.docker.com

使い方は公式の MySQL イメージとほぼ同じなので、イメージを書き換えるだけで使用できます。

チーム開発をしていて、 Intel CPU の人と M1 チップの人の両方がいる場合、 docker-compose.override.yml を使用すると便利です。

# docker-compose.yml

version: '3'  
services:  
    mysql:  
        # デフォルトでは mysql イメージを使う  
        image: 'mysql:8.0'  
        ports:  
            - '${FORWARD_DB_PORT:-3306}:3306'
        environment:  
            MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'  
            MYSQL_DATABASE: '${DB_DATABASE}'  
            MYSQL_PASSWORD: '${DB_PASSWORD}'  
            MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'  
        volumes:  
            - 'sailmysql:/var/lib/mysql'
        networks:  
            - sail
        healthcheck:  
            test: ["CMD", "mysqladmin", "ping"]  

    some_other_containers:  
        ...  
# docker-compose.override.yml

version: "3"  
services:  
    mysql:  
        # M1 チップの人は arm64v8/mysql で上書きする  
        image: 'arm64v8/mysql:8.0-oracle'  

一方で、 mysql はOSの選択肢が debianoracle があるのに対し、 arm64v8/mysqloracle しかありません。
なので、 監視ツールなどで Debian でしか動かないツールを使いたい場合には使用できません。
また、本番は Debian のコンテナで動いていて、なるべく本番に近い環境で開発したい場合も向きません。

設定ファイル差異をなくすなら mysql/mysql-server

Docker 公式のイメージは ARM64 をサポートしていませんが、 MySQL 公式のイメージは ARM64 をサポートしています。
MySQL 公式のイメージが mysql/mysql-server です。

hub.docker.com

このイメージを使用すれば、M1 チップでも (docker-compose.override.yml を書かずとも) 同じ docker-compose.yml で動かすことができます。

一方で、 Docker 公式のイメージではないため、 MYSQL_PASSWORD のような環境変数による初回設定には対応していないようです。
そのため、既存で Docker 公式のイメージを使用しており、環境変数による初回設定を行っている場合は、mysql/mysql-server へのマイグレーションはコストがかかりそうです。

MySQL コンテナをカスタマイズしてるなら --platform linux/amd64

MySQL コンテナで MySQL 以外のミドルウェアをインストールしていたり、込み入った設定をしていたりなど、他イメージに移行することが難しい場合があります。
その際に有効なのが --platform linux/amd64 オプションです。

Docker for Mac の公式ページに、以下のように書かれています。

docs.docker.com

Not all images are available for ARM64 architecture. You can add --platform linux/amd64 to run an Intel image under emulation. In particular, the mysql image is not available for ARM64. You can work around this issue by using a mariadb image.

雑に和訳すると、「全部のイメージが ARM64 で動くわけじゃないよ。 --platform linux/amd64 オプションを付ければ Intel イメージをエミュレーション上で動かすことができるよ。特に mysql イメージは ARM64 じゃ動かないよ。 mariadb のイメージを使って解決することもできるよ。」
といった感じでしょうか。

docker-compose.yml を使用する場合は、以下のように書くといけます。

# docker-compose.yml

version: '3'  
services:  
    mysql:  
        image: 'mysql:8.0'  
        platform: 'linux/amd64'  

ひとつ気になることがあります。
docker-compose file v3 では、ドキュメントから platform オプションが消えています。 (v2 にはあります。)
ですが、リリースノートを見ても「 platform オプションを消した」という記述は見当たりません。
現状サポートされているのか、 非推奨な機能なのかわからない状態です。

ところで、このオプションを指定するとエミュレーション上で動作するということが重要です。エミュレーションをするということは、以下のことを指します。

なので、 --platform linux/amd64 オプションを使用するのは、最終手段にすることをおすすめします。


現在 back check 開発チームでは一緒に働く仲間を募集中です!!

herp.careers

herp.careers

herp.careers

herp.careers

herp.careers

herp.careers

herp.careers

閉包テーブル(closure table)でツリー構造を表現する

この記事は個人ブログと同じ内容です

www.ritolab.com


www.oreilly.co.jp

SQL アンチパターン」という書籍を読んでいて、ナイーブツリー(素朴な木)という、ツリー構造(再帰的な階層構造)の表現について書かれた章があり面白かったので試してみました。

隣接リスト(adjacency list)

よくある(んだけどツラいよねっていう)パターンとして「隣接リスト」というものが紹介されていました。これは、コメントのテーブルに親を識別するための parent_id をカラムに追加するというものです。

DDL

create table comments
(
    comment_id   bigint unsigned auto_increment primary key,
    parent_id    bigint unsigned null,
    bug_id       bigint unsigned not null,
    author       bigint unsigned not null,
    comment_date datetime        not null,
    comment      text            not null,
    constraint comments_author_foreign
        foreign key (author) references accounts (account_id),
    constraint comments_bug_id_foreign
        foreign key (bug_id) references bugs (bug_id),
    constraint comments_parent_id_foreign
        foreign key (parent_id) references comments (comment_id)
)

f:id:ro9rito:20220228143752p:plain

たしかにお手軽な方法ですが SELECT がつらい。

自身のコメントと、子を 1 件だけ使いたいのであれば join しても取り回しはできます。

-- コメントと直近の子を取得
SELECT
    c1.*,
    c2.*
FROM comments  c1
LEFT JOIN comments c2 ON c1.parent_id=c2.comment_id;

ですが、階層構造が無制限であることを考えると join が増えるだけでなく、その結合数を制御することは困難です。

-- この流れで直近の子から更に子を取ろうとすると JOIN が増えていく
SELECT 
    c1.*,
    c2.*,
    c3.*
FROM comments  c1
LEFT JOIN comments c2 ON c1.parent_id=c2.comment_id
LEFT JOIN comments c3 ON c2.parent_id=c3.comment_id;

併せて結合は列が増えるので取り回しの効率も悪く、結局該当コメントを全件、行として取ってくるという選択に落ち着くことになりそうです。(そして comments テーブルに parent_id がある意味とは..という気持ちになる)

-- 該当バグのコメントを全て取得
SELECT
    *
FROM comments
WHERE bug_id = 1;

ちなみに、INSERT は簡単。そして子孫関係の更新も簡単。parent_id を更新すれば良いので、コメントの付け替えは容易です。

ただ、書籍にも書いてありましたが、私は隣接リストの最大に辛い点は削除だなと思いました。

外部キー制約を設定しているので、好き勝手にコメントを削除することができません。少なくとも削除したいコメントの子孫として紐付いているコメントは全て削除する必要があり、しかも制約のおかげで最下層から順に削除していかなければなりません。

もし指定のコメントだけを削除したい場合は、削除したいコメントの子の parent_id を別のコメントに紐付けてからでないと指定のコメントのみを削除できません。(指定のコメントだけを削除して、その子らを昇格させたい場合は親の comment_id に付け替えれば良いので問題は無い。)

色々とツラいのは体験できました。

閉包テーブル(closure table)

書籍では隣接リストに変わるパターンの提案として、3 つのパターンが示されていました。 その中で個人的に最もしっくりきたのは「閉包テーブル」です。

閉包テーブルは、comments テーブルには子孫関係の情報を持たせず、子孫関係の情報は別のテーブルに持たせます。

DDL

create table comments
(
    comment_id   bigint unsigned auto_increment
        primary key,
    bug_id       bigint unsigned not null,
    author       bigint unsigned not null,
    comment_date datetime        not null,
    comment      text            not null,
    constraint comments_author_foreign
        foreign key (author) references accounts (account_id),
    constraint comments_bug_id_foreign
        foreign key (bug_id) references bugs (bug_id)
);

-- comments の子孫関係を表現する
create table tree_paths
(
    ancestor   bigint unsigned not null,
    descendant bigint unsigned not null,
    primary key (ancestor, descendant),
    constraint tree_paths_ancestor_foreign
        foreign key (ancestor) references comments (comment_id),
    constraint tree_paths_descendant_foreign
        foreign key (descendant) references comments (comment_id)
);

f:id:ro9rito:20220228144003p:plain

それぞれのコメントレコードに関して、その親と子の関係を収録しているのが tree_paths のレコードです。

階層構造の最上階、最も親のコメントに関しては自身が子孫のどちらにも自身を指定しているようなレコードになっています。

こちらも INSERT は簡単。子孫関係の更新も、対象コメントの親や子は簡単に判別できるので、こちらもコメントの付け替えは容易です。

隣接リストでツラみポイントであったコメントの削除はどうかというと、tree_path から該当のレコードを削除すればその子孫も含め構造から切り離されるため、制約を受けることなく容易に行うことができました。 さらに切り離したコメント以下の子孫関係や、コメントレコード自体も全て残るため、その後の処遇も柔軟に行えそうです。

コメントの削除時に子孫のコメントたちを昇格させるのも簡単で、子レコードの親カラムの値を変更してあげるだけです。

set @DELETE_COMMENT_ID = 4;
select @ANCESTOR := `ancestor` as `id` from `tree_paths` where `descendant` = @DELETE_COMMENT_ID;

-- 削除対象のコメントの子らを一段上へ昇格させる
update tree_paths
set ancestor = @ANCESTOR
where ancestor = @DELETE_COMMENT_ID
and descendant in (
    select x.id from (
        select descendant as id from tree_paths where ancestor = @DELETE_COMMENT_ID
    ) as x
);

-- 削除対象コメントの関係を削除
delete from tree_paths
where ancestor = @ANCESTOR
and descendant = @DELETE_COMMENT_ID;

扱いやすさもそうですが、テーブルの表現として、コメントそのものとそれらの関係性が分離されているのはとてもシンプルで良いなと思いました。

ツリー構造のしやすさ

最後に、折角ツリー構造の話なので階層構造も作ってみます。

閉包テーブルと PHP を使ってツリー構造にしてみます。

SQL

select
    comments.comment_id,
    comments.comment,
    comments.comment_date,
    accounts.account_name,
    tree_paths.ancestor,
    tree_paths.descendant
from comments
    inner join tree_paths on comments.comment_id = tree_paths.descendant
    inner join accounts on comments.author = accounts.account_id
where comments.bug_id = 1;

この結果が array in stdClass で返ってくるとして、再帰関数を噛ませて階層構造を作ります。

PHP

/** @var object[] $commentList */
$addChild = function (array $comments, array &$treeData) use ($commentList, &$addChild)  {
    /** @var object $comment */
    foreach ($comments as $comment) {
        $treeData[$comment->comment_id] = (array)$comment;

        $children = array_filter(
            $commentList,
            fn (object $item) => $item->ancestor === $comment->comment_id && $item->ancestor !== $item->descendant
        );

        if (empty($children)) {
            continue;
        }

        $treeData[$comment->comment_id]['child'] = [];
        $addChild($children, $treeData[$comment->comment_id]['child']);
    }
    $treeData = array_values($treeData);
};

// 起点となる最上階のコメント
$parentItems = array_filter($commentList, fn (object $comment) => $comment->ancestor === $comment->descendant);

$treeData = [];
// 子孫を付与
$addChild($parentItems, $treeData);

return $treeData;

結果を表示(クリックで展開)

Array
(
    [0] => Array
        (
            [comment_id] => 1
            [comment] => このバグの原因は何かな?
            [comment_date] => 2022-02-26 01:45:40
            [account_name] => Fran
            [ancestor] => 1
            [descendant] => 1
            [child] => Array
                (
                    [0] => Array
                        (
                            [comment_id] => 2
                            [comment] => ヌルポインタのせいじゃないかな?
                            [comment_date] => 2022-02-26 02:45:40
                            [account_name] => Ollie
                            [ancestor] => 1
                            [descendant] => 2
                            [child] => Array
                                (
                                    [0] => Array
                                        (
                                            [comment_id] => 3
                                            [comment] => そうじゃないよ。それは確認済みだ。
                                            [comment_date] => 2022-02-26 03:45:40
                                            [account_name] => Fran
                                            [ancestor] => 2
                                            [descendant] => 3
                                        )

                                )

                        )

                    [1] => Array
                        (
                            [comment_id] => 4
                            [comment] => 無効なインプットを調べてみたら?
                            [comment_date] => 2022-02-26 04:45:40
                            [account_name] => Kukula
                            [ancestor] => 1
                            [descendant] => 4
                            [child] => Array
                                (
                                    [0] => Array
                                        (
                                            [comment_id] => 6
                                            [comment] => よし、じゃあチェック機能を追加してもらえるかな?
                                            [comment_date] => 2022-02-26 06:45:40
                                            [account_name] => Fran
                                            [ancestor] => 4
                                            [descendant] => 6
                                            [child] => Array
                                                (
                                                    [0] => Array
                                                        (
                                                            [comment_id] => 7
                                                            [comment] => 了解、修正したよ。
                                                            [comment_date] => 2022-02-26 07:45:40
                                                            [account_name] => Kukula
                                                            [ancestor] => 6
                                                            [descendant] => 7
                                                        )

                                                )

                                        )

                                    [1] => Array
                                        (
                                            [comment_id] => 5
                                            [comment] => そうか、バグの原因はそれだな。
                                            [comment_date] => 2022-02-26 05:45:40
                                            [account_name] => Ollie
                                            [ancestor] => 4
                                            [descendant] => 5
                                        )

                                )

                        )

                )

        )

)

RDB への問い合わせ結果は行列の表なのでツリー構造にすること自体はアプリケーション(プログラム)側で行う想定のため、そこのコストに関しては隣接リストだろうが閉包テーブルだろうがあまり変わらないかなとは思いました。

まとめ

書籍では隣接リストに変わるパターンとして「経路列挙(Path Enumeration)」「入れ子集合(Nested Set)」「 閉包テーブル(closure table)」の 3 つのパターンが紹介されていました。

隣接リストを含め、全部でこの 4 パターンの良いところやツラい点、良いけどトレードオフになる点などが紹介されていたので、書籍の中でも閉包テーブルはエレガントだとしていたのはありつつも、どれを選択するかは実現するものと照らし合わせてメンテナンスしやすいものを適材適所判断していければ良いなと感じました。(まあでも個人の見解では優勝は閉包テーブルかな)

SQL アンチパターン」という書籍では沢山のアンチパターンとその解決策が紹介されていて読んでいてとても勉強になるしテーブル設計上の視野を広げてくれる良書だと思うので、これからもいつでも手に取れるところに置いておきたいと思います。

www.oreilly.co.jp


現在 back check 開発チームでは一緒に働く仲間を募集中です!!

herp.careers

herp.careers

herp.careers

herp.careers

herp.careers

herp.careers

herp.careers

RSGT2022 の動画が公開されたので、社内で視聴会をやりました

この記事は個人ブログと同じ内容です

RSGT2022 の動画が公開されたので、社内で視聴会をやりました


みなさん、こんにちは! ROXX で back check のエンジニア兼、スクラムマスターをやっています。ぐっきーこと山口壮太 (@Area029S)です。

さて、Regional SCRUM GATHERING Tokyo 2022(以下、RSGT2022)が終わって早2ヶ月が経ちました。 私も仕事の傍ら、 RSGT で知り合ったアジャイルコミュニティの方が開催されているイベントに参加したり、家族でキャンプしたりとバタバタと予定を詰め込んでいたらあっという間に時間が過ぎてしまいました。

RSGT の視聴会やったよ

ありがたいことに今年も RSGT のセッション動画が公開されましたね。 ROXX でも、 agent bank と back check の両事業部で、スクラム開発をおこなっているということで、両事業部みんなで集まって視聴会をしてみました。

会の概要

毎月開催している ROXX Dev Meetup という社内勉強会の1時間枠を使って、 RSGT のセッションの動画を、社内の開発メンバーみんなで試聴しながらわいわいする会を行いました。 題材にしたのは、両事業部どちらも参考にできそうなものということで、あなたのSprint Goalは、機能してますか?プロダクトバックログ Deep Diveを見ました。

超余談ですが、30分以上あるセッションを、1.5倍速でみることでなんとか1時間の枠で納めました。

視聴会の様子

Discord で動画を配信しながらチャットでわいわいしました。

視聴会をやってみた感想

セッションで出てきた、気になった箇所やわからない箇所をみんなで補足しあいながら視聴する体験がよかったです。 また、みんなでひとつの題材を取り扱うと、セッションででてきたワードをその後、共通言語として使うことができるというのもよかったと感じました。

逆に残念だったこととしては、今回1時間の枠に絞ってしまったため、視聴後にディスカッションができなかったことです。 視聴中に出てきたコメントをふりかえったり、そのまま OST をやったりできていたらより良かったと思います。

RSGT2022 のセッションの中で、チームで共有したいセッションはまだまだあるので、今後ちょこちょこ視聴会をやっていきたいと思います。

参考資料

youtu.be youtu.be

さいごに

さいごに少しだけ宣伝です。

現在 back check 開発チームは一緒にはたらく仲間を募集中です!! 私たちと一緒に、 back check を通して「信頼が価値を持ち、信頼によって報われる社会の実装」に挑戦してみませんか?

herp.careers herp.careers herp.careers herp.careers herp.careers herp.careers herp.careers

アジリティーを高めるために目的不確実性をコントロール下に置く

この記事は下記記事と同じです

note.com

最近PdM活動を行っている中で、アジャイル開発における対峙する不確実性の捉え方を変える事によって、生産性が大きく向上しそうな体験を感じることができたので、言語化してみる。

想定読者

  • リリースした後にCSから「これじゃ使えない」と言われる
  • 振り返りの場で顧客価値について言及されない
  • リリースしたのに使ってくれているのかどうかわからない
  • 本当に使ってくれるかわからない のに長期間開発してリリースしている
  • 施策を計画的にリリースすることに重きを置いている

アジャイル開発は不確実性を受け入れる開発

改めて12の原則を照らし合わせてみると本質が書いてあるなぁと思う。 https://agilemanifesto.org/iso/ja/principles.html

顧客満足を最優先し、 価値のあるソフトウェアを早く継続的に提供します。

要求の変更はたとえ開発の後期であっても歓迎します。 変化を味方につけることによって、お客様の競争力を引き上げます。

とある通り、変化することを受け入れるというマインドセットそのものがアジャイル開発における重要なピースの一つになっている。

ではどういった変化を受け入れるべきなのか。「エンジニアリング組織論への招待」では3つの不確実性がソフトウェア開発に含まれているという

  • 目的不確実性
    • 何を作るのかという不確実性
  • 方法不確実性
    • どうやって作るのかという不確実性
  • 通信不確実性
    • コミュニケーション上発生する不確実性

本書においてはアジャイル開発では「目的不確実性と方法不確実性の両方に対して段階的にアプローチする」と書いてあるが、これをしっかり回すことは難しい。

f:id:kotamat:20220130154039p:plain

通常の開発では方法不確実性、通信不確実性に目が行きがち

なぜ不確実性を減らしていくことが難しいのか。 それはそれぞれの不確実性が対象領域がかなり異なっているためだと思っている。

f:id:kotamat:20220130154105j:plain
不確実性の対象領域

  • 目的不確実性
    • ビジネス的な成長と顧客理解の合わさったところ
  • 方法不確実性
    • スケジュールの予測、ベロシティーの安定化
  • 通信不確実性
    • チーム内における情報の非対称性

また、それぞれの不確実性の対処方法も異なる。

  • 目的不確実性
  • 方法不確実性
    • 多面的な見積もりやベロシティ計測による予測可能性の向上
  • 通信不確実性
    • KPTなどでの振り返りによる情報非対称性の解消や、心理的安全性の確保

開発チームという枠組みの中での改善を行おうとすると、影響を及ぼせるのは方法不確実性と通信不確実性のみとなってしまうので、振り返りの場で上がってくるイシューも、方法不確実性や通信不確実性の話題に終止してしまう。

目的不確実性はマーケット環境や顧客理解、ビジネスサイドの動き方といった事業部全体を包含しなければ不確実性の探索が難しいため、目的不確実性をコントロール下に置くことに対して難しさを感じたり、はなから無理だと決めつけて管轄外と捉えるようになってしまう。

対処方法も対象領域も異なりすぎるため、短絡的に考えれば開発チームに閉じる方法不確実性と通信不確実性のみを扱うのが気が楽である。ただ、目的不確実性もコントロール下に置ければ難易度は上がるものの、開発チームにとっても事業部ひいては顧客にとっても良いのではないかと思っている。

目的不確実性をコントロール下に置くとどうなるか

ざっと下記のメリットがあると思う。

  • どこまで作れば完成なのかがアウトカムベースでわかる。
  • まずは最低限何を作ればいいかがわかる
  • 顧客への価値提供を身にしみて感じることができる
  • 最も重要なことに取り組んでいる事がわかる。
  • BizとDev双方協力して顧客への価値提供していることを一体感を持って感じることができる

アジャイル開発においては、目的不確実性と方法不確実性を双方合わせて解消していくプロセスを踏んでいくことになるが、これを前提に開発することを念頭に入れることで、初手は完璧に作りすぎる必要がないことを理解出来、最もコアで不確実性の高いものを先に作る重要性を理解できる。 そうすると、「これって無駄に作りすぎているんじゃないか」とか、「このまま長期で開発していくとダレてしまう」などの不要な懸念や不安を感じることなく、目的に向かって迷いなく進んでいくことができる。 また、全てを完璧に作る必要がないため、思考のリソース的にも「考えを遅延させて重要なことだけを考えればいい」状態が継続されるため、常に重要なことに取り組んでいるという状態を作ることができる。

目的不確実性をコントロール下に置かない、計画駆動な開発スタイルでいったほうが確かに「モノが作れるスピード」は段違いで早くなるかもしれない。ただそれが顧客に価値を届けられたかどうかをベースに考えていないことで、「これってなんのために作ったんだろう」という、作った後に大きな手戻りが発生する危険をはらみ、中長期的にモチベーションが下がっていってしまう。

小さく答え合わせをしながら、手段と目的の不確実性を言ったり来たりして、少し遠回りしてでも最終的には遠くの地点にたどり着ける。そういった感覚を得られるのではないかと思っている。

目的不確実性の解消には技術が必要だがすべてをやらないといけないわけではない

ではどうやれば目的不確実性をコントロール下におけるのか。それはプログラミングスキルとは違った、技術が求められる。

  • ビジネス理解
  • プロダクトゴール、プロダクトビジョンの策定
  • 仮設構築力
  • 開発外の部署の理解
  • 顧客理解
  • アセットとPLの次元の違い
  • etc..

これらはプロダクトマネジメントに求められるスキルであり、非常に多岐に渡る。これらを一朝一夕で身につけるのは非常に難しい。自分自身もまだこのスキルを身につけられたと自信を持って言える状態ではないし、果たしてそんな日が来るのだろうかという不安を覚えるくらい、非常に幅広く、深いスキルが要求される。

ただ、全てが完璧にならないとコントロール下に置けないかというとそうではないと思う。目的不確実性は0 or 100の世界ではなく、次第に少しずつ下げていくものであるため、そこに5でも10でも関与し下げられる事ができるのであれば、それだけでもとても価値のあることであるし、上記に述べたメリットを享受することができる。

まずは技術を習得する前に、できることからやっていくのがいいかなと思う。

目的不確実性を下げるために開発の観点からやれること

目的不確実性は下記の様に、ある特定のフェーズを通過するごとに下がっていく。 f:id:kotamat:20220130154141p:plain

このような活動に積極的に関与していくことだったり、もしここに上がっているフェーズをすっ飛ばしてものを作ろうとしている場合は、一旦立ち止まって「なんのために作ろうとしているのか」というのを考え、たとえ間違ってもいいので、後から検証できるような形に持っていくというのでもいいかもしれない。

また、「1秒でも早く価値を届けるためにどうすればいいか」というのを考え、やれることをやっていってもいいかもしれない。

特におすすめなのは 施策をリリースした後にCSと顧客に展開するのではなく、施策の出来上がりの解像度が少しでも上がったらすぐにFBを求めに行く というもの

  • まずは動くものを作るというアジャイルの原則を否が応でも実現する必要が出てくる
  • 顧客へのヒアリングより圧倒的に速いスピードでFBを貰える。
  • CSが「これだとお客さんに使ってもらえない」という意見がリリース後に出てくることがなくなる。
  • CSに事前に展開しておくことで、リリース後に顧客に使ってもらえるような準備をリリース前に実施してもらえる
  • 例えばCSが顧客と商談するときに、施策に対して顧客からFBをもらい、リリース前に調整ができるかもしれない。

というところで、少なくともリリース時には 「CSが顧客に使ってもらえる」状況まで目的不確実性を下げる ことができ、かつその プロセス自体も対して難しいことを要求しているものでもないため、すぐに取りかかれる難易度でもある。

また スプリントゴールを設定後に事業部に展開する というのも良い。質は多少劣るもののいくつか同様の効果を得られる。

これ以外にもあるかもしれないが、まずは自分のできるところからやってみるとよい

まとめ

目的不確実性をコントロール下に置く重要性と、まずできることからやっていこうという話をさせてもらった。 「もっとこうしたほうがいいんじゃないか」「こういう観点もありそう」みたいな意見があればコメントにいただけると嬉しいです。 あと、プロダクトマネジメントや開発手法などに関して、一緒にお話していただける方も募集中です。 https://twitter.com/kotamats にDMいただければと思います。

Github Actions でのプルリク作成時に特定のファイルの存在を知らせる

この記事は個人ブログと同じ内容です

www.ritolab.com


Github Actions を用いて定期的にプルリクエストを作成する際に、特定のファイルの存在を知らせたかったのでやってみます。

特定のファイルの存在を知らせる

知らせるのは「通知」という意味ではなくて、プルリクの説明欄にこれらのファイル名を記載して、いちいち確認しなくてもこれらのファイルが存在していることをわかるようにします。

定期に自動でプルリクを作成しているような状況なのでフィーチャーブランチのプルリクというよりはリリースのプルリクやローカルに向けたプルリクなどを想定しているとして、例えば PHP フレームワークの Laravel だと、マイグレーションファイルとかサンプルの環境変数ファイルの変更とかは、プルリクの中身を確認しなくても追加や変更の存在がわかるといいな。みたいな状況感です。(プルリクを取り込んだけど環境変数追加あったの見逃しててエラーになるみたいなのを防ぎたいモチベーション)

プルリクの説明欄への記載

hub コマンドでプルリクを作成する場合は、 -m オプションでプルリクのタイトルと説明を指定できます。

最初の 1 行目がプルリクのタイトルになり、空行を挟むとそれ以降が説明欄の文章になります。

MESSAGE="ここがプルリクのタイトルになる

## これ以降が説明欄の文章になる
* テスト
* test

markdown で書ける。
"

hub pull-request -m "$MESSAGE" -b main -h dev

上記コマンドで作成されたプルリクは以下のようになります。

f:id:ro9rito:20220125182046p:plain

特定のファイルを抽出する

差分から検索して抽出しようと思います。

MESSAGE="プルリクのタイトルです。\n\n"

# 変更ファイルを取得
DIFF=`git diff main dev --name-only`

# 探したいファイルのパスや名前で該当の行を抽出
ENV_FILES=`echo "$DIFF" | sed -n '/\.env\./p'`
MIGRATION_FILES=`echo $DIFF | sed -n '/database\/migrations\//p'`

# 説明欄のコメントを追加
if [ -n "$ENV_FILES" ]; then
  MESSAGE+="環境変数ファイルの変更が含まれます。\n$ENV_FILES\n\n"
fi
if [ -n "$MIGRATION_FILES"  ]; then
  MESSAGE+="マイグレーションファイルが含まれます。\n$MIGRATION_FILES\n\n"
fi

echo -e "$MESSAGE"

git diff で差分を持ってきます。

ここでは main ブランチと dev ブランチの差分を、--name-only オプションでファイル名のみを取得しています。(変更があった、新規に追加されたファイルの一覧が入ってくる。)

入ってきたファイルリスト($DIFF)に対して sed -n で指定の文字列を含む行のみを抽出します。

あとはコメントを組み立てて出力です。

上記を実行するとこんな感じで出力されます。

プルリクのタイトルです。

環境変数ファイルの変更が含まれます。
.env.example

マイグレーションファイルが含まれます。
database/migrations/2022_01_22_000000_create_users_table.php
database/migrations/2022_01_22_000000_create_books_table.php

hub コマンドでプルリクを作成するとこんな感じです。

f:id:ro9rito:20220125182150p:plain

Github Actions で作成

上記を踏まえてこれらを Github Actions 側に実装します。

.github/workflows/xxxx.yml(メッセージ・プルリク作成部分のみ抜粋)

# ブランチ名作成
- name: Make branch name
  id  : setting
  env:
    TZ: 'Asia/Tokyo'
  run : |
    DATE=`date +"%Y%m%d_%H%M%S"`
    BRANCH_NAME="release/$DATE"
    echo ::set-output name=branch_name::$BRANCH_NAME

# プルリクのメッセージ作成
- name: Make pull request message
  id: message_making
  env:
    BRANCH_NAME: ${{ steps.setting.outputs.branch_name }}
  run: |
    MESSAGE="$BRANCH_NAME\n\n"
    
    DIFF=`git diff main dev --name-only`
    ENV_FILES=`echo "$DIFF" | sed -n '/\.env\./p'`
    MIGRATION_FILES=`echo "$DIFF" | sed -n '/database\/migrations\//p'`
    if [ -n "$ENV_FILES" ]; then
      MESSAGE+="環境変数ファイルの変更が含まれます。\n$ENV_FILES\n\n"
    fi
    if [ -n "$MIGRATION_FILES"  ]; then
      MESSAGE+="マイグレーションファイルが含まれます。\n$MIGRATION_FILES\n\n"
    fi
    echo ::set-output name=message::$MESSAGE

# ブランチ作成
- name: Create Branch
  uses: peterjgrainger/action-create-branch@v2.1.0
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  with:
    branch: ${{ steps.setting.outputs.branch_name }}

# プルリクエスト作成
- name: Create Pull Request
  id: create_pull_request
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    BRANCH_NAME: ${{ steps.setting.outputs.branch_name }}
    MESSAGE: ${{ steps.message_making.outputs.message }}
  run: |
    PULL_REQUEST_MESSAGE=`echo -e "$MESSAGE"`
    hub pull-request -m "$PULL_REQUEST_MESSAGE" -b main -h $BRANCH_NAME

1 点だけポイントで、組み立てたメッセージはエスケープ文字を使用している(改行部分)ため、hub pull-request で指定する前に echo -e で出力してエスケープ文字を有効にしておきます。

# エスケープ文字を有効にする
PULL_REQUEST_MESSAGE=`echo -e "$MESSAGE"`

Github Actions を回して実行してみます。

f:id:ro9rito:20220125182338p:plain

抽出されたファイルのリストが説明欄に書き込まれてプルリクが作成されました。

まとめ

定期のプルリクエスト作成に限らず、手動でのプルリク作成時にもこういったファイルを検出してコメントするみたいな事に応用できそうだなと思いました。

任意のファイル抽出やコメントへの書き込み(プルリク作成も含め)は、探せば GitHub Marketplace にワークフローが公開されているかもしれないので探してみるのも良いかもしれません。