スクラムの悪いところをあげてみる

1時に寝て7時に起きて午前中はだらだらしていた。午後からオフィスでちょっと作業して夜はドラクエタクトしてた。

スクラムの悪いところ

金曜日に メタバース雑談会 に参加してチームビルディングや課題管理の雑談になった。そのときに課題管理やスクラムの概要を話してみたけど、多様なメンバーだといま一つ手応えがなくてコンテキストを揃えないと組織論の話しは難しいなと思えた。コンテキストが共有できていない状況ではあまり雑談に向く話題ではなかった。そんな中でスクラムの悪いところを考える機会が最近多いので簡単にまとめてみようと思う。もちろん、スクラムにはよいところもたくさんあって世の中に普及しているのはそれがためだと思う。よいところはすでにあちこちで言われているだろうから、あえて、ここでは悪いところを整理してみようと思う。

  • 心理的安全性のないチームではぬるま湯と化す
  • プロダクトオーナーに超人を要求する
  • スクラムイベントが時間とともに形骸化していく

スクラムが悪いのではなくチーム (組織) が悪いということもできるが、スクラムを1年近くやっていて陥っているのでスクラムの影響は大きいと考える。

心理的安全性のないチームではぬるま湯と化す

スクラムでは個人の責任を問われることはなく、名前の通り、チーム一丸となって課題に取り組むことを意図するプラクティスなので、悪く言えば、個人の怠慢が問われることもまずない。ここでいう怠慢は悪意をもったものではないとしても 社会的手抜き を容易に発生させる。個々のメンバーの責任を減少させると必然的に裁量も減少してしまう気がしている。ある課題がうまくいかなくても自分の責任とは言えない状況であればそのまま放置してチームが解決してくれるのを待つという選択が合理的になるケースが出てくる。これは人間の習性として抗いがたいと私は考えている。楽をしたいのが人間だと思う。必然的に楽な方に引き寄せられていく。克己心とまで言わなくても自分を律して課題に取り組んでいないとスクラムの枠組みではいくらでも楽をしてしまえる。業務としてその品質や納期に一定の厳しさをもっていないチームは簡単にぬるま湯と化してしまう。

プロダクトオーナーに超人を要求する

以前、こみやさんと話していたときに出た話題。スクラムはすべての責任はプロダクトオーナーが負うことになっている。プロダクトオーナーは開発者でないことが多いと思われる。建前としては開発がうまくいかないことの責任をすべてプロダクトオーナーが負うことになるが、これはフェアとは言い難い状況もある。はらさんと話していると、プロダクトオーナーは無茶苦茶な要求を開発者にしてしまいがちなのでそれをなだめたり指導したりするポジションとしてスクラムマスターがいるという話しも聞く。うちのプロダクトオーナーは人格者でまったく無茶な要求をしない。おそらく現場出身の方なのでシステムに疎いことは周りも理解していて開発責任を問われていないのではないか推測する。プロダクトオーナーに責任が集中している割にスクラムマスターが盾になることで開発への口出しは制限される。またスクラムマスターも業務上の責任をなんら負っていない。スクラムマスターの助言を遂行する責任はすべてプロダクトオーナーに求められる。スクラムは業務の役割分担としてプロダクトオーナー、スクラムマスター、開発リーダーの三位一体のような構成を取るものの、責任をプロダクトオーナーに押し付けつつ、プロダクトオーナーの権限を奪う構成ともみれる。

スクラムイベントが時間とともに形骸化していく

スクラムに不慣れなチームがスクラムガイドに書いてあることを実践していくには少し時間がかかるし、スクラムマスターのようなスクラムガイドを熟知しているメンバーに指導を仰ぐのは理に適っている。しかし、半年もやればスクラムガイドの手順やワークフローをメンバーは習熟する。実際にうちのスクラムマスターはふりかえりのファシリテーション以外にやることがないとちょくちょく発言していて、実際にデイリースクラムも半分ぐらい休んでいる。ファシリテーション以外で業務のイニシアティブを取ることはない。スクラムマスターがドメイン知識を身につけるべきかどうか、私はまだわからないが、うちのスクラムマスターはドメイン知識を習得していないので業務の話にはまったく入ってこれない。少し前に ゾンビスクラム という言葉を教えてもらったが、スクラムイベントのいくつかはただこなすだけの業務になりつつある。うちのチームのスプリントの実態やストーリーポイント運用は私からみたら課題だらけだが、ダメなところも含めてその予定調和に慣れてしまって複雑で難しい問題を改善しようとしていないように私からはみえている。

戦略シミュレーションと特性

ストレッチ

今日の開脚幅は開始前158cmで、ストレッチ後161cmだった。先週よりやや数字が悪くなった。それでも先週はストレッチを受けていても体全体のだるさのようなものを感じていたのが、今回はちゃんと筋を伸ばしている感覚があって先週の体調の悪さはなくなったように思える。右股関節周りの詰まりに加えて、右前ももの張りがいつもより大きかった気がする。毎週ストレッチを受けていると物理的な体調の良し悪しもわかるのがよい。

わざと負ける理由

一昨日からリアルタイム対戦 にはまっている。数をこなしていてわかってきたことがたくさんある。戦略シミュレーションゲームはやればやるほど学びがある。

  • パーティーの特性によって相性のよい対戦相手とそうじゃないのがある
    • リアルタイム対戦のマッチングは選択できないのでマッチングしないと相手がわからない
    • 相性の悪い相手だとマッチング時点で辞退する (負けを認める) 相手もいる
  • こっちの戦略に呼び込むための布石がいる
    • 3つぐらい戦略を用意して最終的にどの戦略に呼び込むかを相手の動きをみながら考えないといけない
    • キャラを動かす制限時間が15秒なのでわりと忙しい
    • こちらの布石にはめて最終的に勝てると達成感がある
      • 中盤まで負けていて向こうに勝ったと思わせて逆転できるとなお嬉しい
      • ダークドレアム は奇数ターン (1, 3, 5, 7 …) ごとに攻撃力・守備力・すばやさ・かしこさが1段階上がるバフがかかる
        • ダークドレアムはすばやさが低いので徐々にすばやさが上がっていって先制できるようになると最後の1手違いで勝つ状況が出来上がる
        • ダークドレアムで5ターンまで戦闘を継続できれば勝率が高い
  • すばやさの高いパーティーには何もできないから勝てない
    • 高すばやさ (且つ、高火力または状態異常) パーティーには絶対に勝てない
      • リアルタイム対戦でダークドレアムがあまり使われていない理由だと思う
    • 初手前にパーティーの半分ぐらいがやられている
    • 相手の攻撃が届かない初期配置がないので絶対に防げない
  • 状態異常攻撃の主体パーティーは対戦していておもしろくない
    • 混乱・麻痺・眠り・魅了といった状態異常になると2ターン何もできない
    • 状態異常攻撃 + 高すばやさのキャラの攻撃を防ぐ方法はない
  • パーティー編成のウェイト制限がうまく調整されている
    • 高ランク (ステータス高い) のキャラばかりを編成できないようにウェイト制限がある
    • これにより、パーティー編成が4人か5人かにわかれる
    • 低ランク (ステータス低い) のキャラはウェイトも低いので5人目には入れやすい

本題のわざと負ける理由だが、わかってきた。リアルタイム対戦のマッチングは同じランク内で行われる。上位のランクになればなるほど、対戦相手も強くなる (強くなければ上位のランクに上がれない) 。一定量のポイントを獲得するとランクアップしていけるが、負けるとポイントが下がるペナルティがつく。一定のランクで勝ち負けを繰り返すとそのランクに留まり続けるということができる。私も自分の限界までランクアップしてみて気付いたのは周りも強いので限界に到達すると勝ったり負けたりを繰り返す。ここで別のルールで勝った回数に応じてコインが支払われるボーナスがある。強い人が低いランクで勝ち数を稼ぐのは限界に近いランクで勝ったり負けたりを繰り返すよりもはるかに効率がよい。それはリアルタイム対戦のマッチングに時間制限があるからなおさらそうなる。実力が均衡した相手と時間をたくさん使って2勝3敗を繰り返すよりも、わざと負けながら弱い相手に勝ち続ける5勝?敗を繰り返す方が絶対数としての勝ち数を稼ぐには効率がよい。おそらく負けたときのペナルティがあるランクからわざと負ける人が現れ始めていると思う。なるほどなぁ。

dto に対するリフレクションの是非

1時に寝て7時に起きた。

ドラクエタクトのリアルタイム対戦

もう2年間もずっとゲームし続けている。最近 リアルタイム対戦 モードがリリースされた。全然やる気なかったんやけど、リアルタイム対戦で得たコインでもらえるアイテムが魅力的なのでやることにした。そして、実際にアルタイム対戦をやってみるとはまる。運営の手の平でゲームさせられている。

  • 人間が相手で狡猾な戦略で負けると悔しい
  • 人間が相手の戦略の方が創意工夫があって学びになる
  • 実際にやり始めるとおもしろくなってきてずっとやってしまう

できる時間を制限しているというのもうまいやり方だなと思っている。朝・昼・晩の7-9時、12-14時、19-22時に制限している。そこまでしてリアルタイムに人間同士をマッチングしてゲームさせる必要があるのか?という素朴な疑問に私は辿りつくが、おそらくゲーム開発者からみたらそうじゃない大事なユーザー体験があるのだろうと推測する。あと不思議なことが1つ。マッチングしていると、たまにわざと負けてくれる人がマッチングされる。チームのメンバーが1人で向こうが先行なら戦いを辞退 (こちらの勝ちになる) するし、こちらが先行でもすぐにやっつけられる。ちゃんと統計をとってないけど、20回に1回ぐらいの頻度でわざと負けてくれる人とマッチングする。あの人たちは一体どういう理由でわざと負けているんだろう?

リフレクションのユーティリティを作った

いまお手伝いで開発している api サーバーは外界と内部のデータの境界を明確にわけていて、外向けのオブジェクト定義と内部向けのオブジェクト定義が異なる。ほとんど同じデータであっても dto を介して値を受け渡ししないといけない。そうすると、次のような dto と他のオブジェクトとの値渡しのための処理が型ごとにあちこちに実装されている。

private MyRecord toMyRecord(MyDataInput in) {
    var record = new MyRecord();
    record.id = in.id
    record.name = in.name;
    record.someId1 = in.someId1;
    record.someId2 = in.someId2;
    record.someId3 = in.someId3;
    record.sortOrder = in.sortOrder;
    record.createUser = in.createUser;
    record.updateUser = in.updateUser;
    ...
}

メンバー数が20-30ぐらいあると、たまに値のセット忘れがあったり、あとから追加したメンバーの保守ができてないとか、たまにトラブルが起きる。これ自体は間違っているわけじゃなくて境界を明確にわけるメリットもあるのでプログラミングの煩雑さとトレードオフと言える。

最近、私が管理系の web api のエンドポイントを作る機会が多いせいか、dto と外部向けのオブジェクトを明確にわける必要のない要件もあったりする。試しにリフレクションを使って同名のフィールド間の値の受け渡しは自動でやってみたらどんな感じかな?と思って作ってみた。

public class ReflectionUtil {

    private ReflectionUtil() {
        throw new AssertionError("ReflectionUtil is a utility class");
    }

    private static <T> Field getField(Class<T> klass, String fieldName) {
        try {
            return klass.getDeclaredField(fieldName);
        } catch (NoSuchFieldException e) {
            return null;
        }
    }

    public static <T1, T2> T2 mapFieldValues(T1 fromInstance, T2 toInstance) {
        var fromClass = fromInstance.getClass();
        for (var toField : toInstance.getClass().getDeclaredFields()) {
            toField.setAccessible(true);
            var fromField = getField(fromClass, toField.getName());
            if (fromField != null) {
                fromField.setAccessible(true);
                try {
                    toField.set(toInstance, fromField.get(fromInstance));
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                }
            }
        }
        return toInstance;
    }
}

これを使うと、先のコードがこれだけで済む。煩わしい値の受け渡しだけのコードを削減できる。

var record = ReflectionUtil.mapFieldValues(in, new MyRecord());

こんなことやると、セキュリティ的によくないとか反論されるかな?と思いながら pr を出してみたら思いの外、好評だったのでちょっと使ってみようと思う。

次のお仕事探し

22時に寝て5時に起きた。前日はあまり寝てなかったら眠くなった。午前中に昨日やり残した開発のお仕事を4時間ほどやってから自分の会社の雑務をしていた。

智将・諸葛孔明の兵法

智将・諸葛孔明の兵法の書評 をみかけた。amazon.co.jp に本の情報があることは確認できたが、1987年に出版された本なので在庫はないから諦めていた。たまたま先日メルカリで売っているのをみつけたので購入した (300円) 。第一部生涯 7.馬謖を斬るのところで第一次北伐の背景や概要が説明されている。馬謖が山の上に陣取ったのは短期決戦を意図してその優位性を取ろうとしたのだろうと著者の推察が述べられている。また馬謖を斬るにいたったのは軍律を守ることをすべてに優先したと説明されている。みずからが軍律を破ってしまうと蜀全体の維持が不可能になるだろうと著者も推察が述べられている。そのときに涙したことも触れているが、涙した理由の詳細については言及していない。

リモートワーク前提のお仕事

以前、登録した人材紹介プラットフォームremogu さんがある。待遇のよい案件はほぼ東京か、あっても大阪になる。私は神戸に住んでいるので基本的にリモートワークでないとお仕事を探すのは難しい。remogu さんはリモートワーク前提なのでフィルター条件が1つ減るので検索しやすい。11月から新しいお仕事を探さないといけない。remogu さんのプロフィール情報を求職のステータスに更新しておいた。すると翌日に過去に面談したエージェントさんから連絡があって、すぐに希望に沿った案件を4つほど提案してくれた。その業務内容を今日みていたらどれも私の希望した業務内容に合致するものだったので優先順位とともに職務経歴書を更新して返信した。以前、面談したときもこのエージェントさんの印象はよかった。なにかしら相性があるのか、よい印象をもつエージェントさんだとよい提案をしてくれる。この前、初めて登録した人材紹介会社のエージェントさんとの面談はいまひとつだったのでなにかしらエージェントさんの質なのか、人間力の差による違いがあるんだなと思う。順番に面談していってマネジメントのキャリアを得られるかどうか、私にとっては大きな挑戦の1つになるので求職活動をがんばりたい。

アプリケーションとジョブの違いと一時停止

2時に寝て5時に起きた。たった3時間しか寝てないのに夜中に1回は起きている。スポットで9時から会議が入ったのでそれまでに朝のお仕事を終わらせようと思ったら早起きできた。6時前にはオフィスに行ってお仕事を始められた。

アプリケーションからジョブへの移行

k8s の cronjob を活用する前は spring の Scheduled アノテーションで定期実行処理を書いていた。アプリケーションサーバー内の1スレッドが定期実行していた。これはスケールアウト前提のアプリケーションサーバーには向いてなくて、複数のアプリケーションサーバーをスケールアウトさせてデプロイすると、定期処理も複数実行されてしまい、同時実行できない類の処理だと問題になる。k8s の cronjob は同時実行しない設定などもあるため、k8s の cronjob へ移行している。

移行作業の準備をしていてアプリケーションサーバーの pod は replicas を調整することで一時停止の代替となるオペレーションができる。

$ kubectl scale --replicas=0 deployment/my-app
deployment.apps/my-app scaled

移行時のアプリケーションサーバーはこれで無効にしておき、cronjob に入れ替えるといった切り戻し可能な状態で移行できる。cronjob も suspend のフラグを使うことで一時停止できるので検証しながら双方を切り替えることができる。

$ kubectl patch cronjob my-batch-job1 -p '{"spec": {"suspend": true}}}'
cronjob.batch/my-batch-job1 patched

ストーリーポイント捨てた方がよい

2時に寝て5時に起きて7時半に起きた。夜も眠れなくなってきた。

ストーリーポイント改善への再考

プランニングしていたときに調査チケットをスパイクにしようという話題がでたときにストーリーポイントを割り振るかどうかという話しになった。スクラムマスターは5ポイント割り振ればよいと以前話していた気がする。そのときは時間がなかったから反論しなかったけど、ポイントと工数は別だからそれはよくないと反論した。割り当てたストーリーポイントに対して「○○さんならどのぐらいの時間でできますか?」と質問しているのがすでにおかしい。便宜上、ストーリーポイントはベロシティを計測して時間にマッピングされるかもしれないが、ストーリーポイントを直接時間にマッピングし始めると、担当者の個人差で大きく運用が変わってきてしまう。ストーリーポイントとスケジュールのアンマッチな部分を実際のスクラム運用でどう改善するのか?をつぶやいていたら、みずおちさんが返信してくれた。

見積もる期間をなるべく小さくしてスケジュールを提示しないというのが戦略として正しいと思う。

あと実際には、打ち合わせの日程調整やレビュー待ちなどの待ち時間もあり、ストーリーポイントの複雑さや開発の工数だけでなく、リードタイムの概念もある。スケジュールは納期が決まっているけれど、リードタイムが伸びてしまったときに納期は伸びてくれないのでそのギャップもなにかしら反映する必要はあるが、ストーリーポイントではチケットの依存関係やリードタイムの遅れを反映することはできない。

リファレンス

k8s クラスターの service account とロール

23時に寝て6時に起きた。前日は夕方からだらだらしてた。

k8s クラスターの権限管理

初期のクラスターを私が構築したわけではないため、権限周りはあまりよくわかっていない。k8s には2つのユーザーアカウントがある。

  • user account: 人向け
  • service account: アプリケーション向け

In this regard, Kubernetes does not have objects which represent normal user accounts. Normal users cannot be added to a cluster through an API call.

https://kubernetes.io/docs/reference/access-authn-authz/authentication/#users-in-kubernetes

但し、ユーザーアカウントを表すオブジェクトはなく、api 経由でクラスターにユーザーを追加したりはできない。ユーザーは存在しないけど、認証はできる仕組みを私は知らないので関心がある。それはまた今度調べるとして、今回は service account の権限を変更した。service account は pod 内から k8s の api サーバーにアクセスするときの認証などに使われる。デフォルトの権限だと、api サーバーのエンドポイントへの書き込み権限がない (?) ようなので追加する。

In order from most secure to least secure, the approaches are:

  1. Grant a role to an application-specific service account (best practice)
  2. Grant a role to the “default” service account in a namespace
  3. Grant a role to all service accounts in a namespace
  4. Grant a limited role to all service accounts cluster-wide (discouraged)
  5. Grant super-user access to all service accounts cluster-wide (strongly discouraged)

ServiceAccount permissions

セキュアな順番として上から自分たちの環境にあうかどうかを判断すればよい。私の場合、わざわざ新規に service account を作るほどの要件ではないため、2番目の default の service account にロールを与えることにした。RoleBinding というキーワードがあるので既存のクラスターロールから edit という権限を与える。

$ kubectl create rolebinding default-edit-role --clusterrole=edit --serviceaccount=default:default --namespace default
rolebinding.rbac.authorization.k8s.io/default-edit-role created

ここでは default-edit-role という RoleBinding を作成した。

$ kubectl get rolebinding default-edit-role -o yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  creationTimestamp: "2022-08-08T08:27:12Z"
  name: default-edit-role
  namespace: default
  resourceVersion: "152660975"
  uid: 6de13b71-3103-448c-805a-66e9400f61c3
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: edit
subjects:
- kind: ServiceAccount
  name: default
  namespace: default

これで cronjob から job を作成する権限が付与された。Write access for Endpoints によると、新規に作成した v1.22 以降のクラスターではエンドポイントに対する write アクセス権限が異なるように記載されている。一方で v1.22 にアップグレードしたクラスターは影響を受けないとも記述されている。私がいま運用しているクラスターは v1.21 から v1.22 にアップグレードしたクラスターなので edit ロールでうまくいったのかもしれない。クラスターのバージョンによって設定が異なるかもしれない。

wiremock を使った kubernetes モックサーバーのテスト拡張

2時に寝て7時に起きて9時までだらだらしてた。

Kubernetes Clients のライブラリ実装

昨日の続き 。ある程度、クライアントの振る舞いを確認できたので自前のライブラリを作ることにした。ライブラリなのでテストをちゃんと書きたいと思って単体テストのやり方を調べてたら Kubernetes Clients のリポジトリにも単体テストはほとんどなくて、どうも e2e テストの方を重視しているようにもみえた。github issues を検索してみたら次の issue をみつけた。

なにかしら単体テストの仕組みを作った方がいいんじゃないかという提案と一緒に issue の作者?かどうかはわからんけど、wiremock を使ったテストのサンプルコードをあげていた。名前だけは聞いたことがあったけど、過去に使ったこともなく、どういうものか全くわかってない。ドキュメントを軽く読んでみたら http モックサーバーらしい。issue の内容を参考にしながら wiremock のドキュメントをみて junit5 のテスト拡張を書いてみた。これが適切な実装かはあまり自信がないけど、こんな感じでモックサーバーとモッククライアントの junit5 のテスト拡張を実装した。これは static なモックサーバーの設定になるので wiremock 自体の起動コストは速く感じた。ライブラリのテストとしては申し分ない。いまのところは自画自賛。

@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface KubernetesApiClient {
}
public class SetupKubernetesWireMock implements BeforeAllCallback, BeforeEachCallback, ExtensionContext.Store.CloseableResource {
    private static final Logger logger = LogManager.getLogger(SetupKubernetesWireMock.class.getName());
    private static int PORT = 8384;
    private static WireMockServer wireMockServer = new WireMockServer(options().port(PORT));
    private static boolean started = false;

    private ApiClient apiClient;

    @Override
    public void beforeAll(ExtensionContext context) throws Exception {
        if (!started) {
            wireMockServer.start();
            started = true;
            this.configureMockClient();
            var basePath = String.format("http://localhost:%d", wireMockServer.port());
            this.apiClient = ClientBuilder.standard().setBasePath(basePath).build();
            logger.info("started kubernetes wiremock: {}", basePath);
            context.getRoot().getStore(GLOBAL).put("SetupKubernetesWireMock", this);
        }
    }

    @Override
    public void beforeEach(ExtensionContext context) throws Exception {
        for (var instance : context.getRequiredTestInstances().getAllInstances()) {
            for (var field : instance.getClass().getDeclaredFields()) {
                if (field.isAnnotationPresent(KubernetesApiClient.class)) {
                    field.setAccessible(true);
                    field.set(instance, this.apiClient);
                }
            }
        }
    }

    @Override
    public void close() throws Throwable {
        wireMockServer.stop();
    }

    private void configureMockClient() throws IOException {
        // add static stubs for mock client
        configureFor("localhost", PORT);
        this.stubForBatchGetCronjobs();
        this.stubForBatchGetSingleCronJob("my-job1");
        this.stubForBatchGetSingleCronJob("my-job2");
        this.stubForBatchGetSingleCronJob("my-job3");
        this.stubForBatchPostJob();
        this.stubForBatchGetJob();
    }

    private byte[] getContents(String name) throws IOException {
        var json = new File(this.getClass().getResource(name).getPath());
        return Files.readAllBytes(Paths.get(json.getPath()));
    }

    private void stubForBatchGetCronjobs() throws IOException {
        var contents = this.getContents("/fixtures/kubernetes/batch/cronjobs.json");
        var path = urlPathEqualTo("/apis/batch/v1/namespaces/default/cronjobs");
        stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(contents)));
    }

    private void stubForBatchGetSingleCronJob(String jobName) throws IOException {
        var contents = this.getContents(String.format("/fixtures/kubernetes/batch/%s.json", jobName));
        var url = String.format("/apis/batch/v1/namespaces/default/cronjobs/%s", jobName);
        var path = urlPathEqualTo(url);
        stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(contents)));
    }

    private void stubForBatchPostJob() throws IOException {
        var name = "/fixtures/kubernetes/batch/post-my-job.json";
        var contents = this.getContents(name);
        var url = "/apis/batch/v1/namespaces/default/jobs";
        var path = urlPathEqualTo(url);
        stubFor(post(path).willReturn(aResponse().withStatus(200).withBody(contents)));
    }

    private void stubForBatchGetJob() throws IOException {
        var name = "/fixtures/kubernetes/batch/get-my-job.json";
        var contents = this.getContents(name);
        var url = "/apis/batch/v1/namespaces/default/jobs/my-job";
        var path = urlPathEqualTo(url);
        stubFor(get(urlPathEqualTo(url)).willReturn(aResponse().withStatus(200).withBody(contents)));
    }
}

Kubernetes Clients のサンプル実装

23時に寝て7時半に起きた。夜中も2回ぐらい起きる。暑さでまいってきた。

ストレッチ

今日の開脚幅は開始前159cmで、ストレッチ後162cmだった。数字は悪くない。いつもはストレッチを受けていると疲労しているところが伸びることで体が軽くなっていく感覚があるのだけど、今日は体全体がだるくてストレッチを受けていてもなんかしんどいなぁとだるさを感じていた。コロナに感染してないと思うけど、夏バテの状態をそのままストレッチにも持ち込んだような感覚があった。腰の張りや肩甲骨の硬さなどが少し気になったかな。トレーナーさんには立ったときの姿勢が少し前よりで重心のバランスがよくないといったアドバイスをされた。とくにどこが悪いというわけでもないのになんかしんどい。

Kubernetes Clients のサンプル実装

Kubernetes Clients の調査 の続き。java クライアントを使って minikube でいくつか動かしてみた。openapi で生成した rest api クライントが提供されている。デフォルト設定でも minikube で普通に動いたのでおそらく裏で $HOME/.kube/config をみたり /var/run/secrets/kubernetes.io/serviceaccount/token を読み込んで認証ヘッダーに設定してくれたりするのだと推測する。

public ApiClient getKubernetesClient() throws IOException {
    var client = Config.defaultClient();
    io.kubernetes.client.openapi.Configuration.setDefaultApiClient(client);
    return client;
}

このクライアントを使って java/kubernetes/docs/ 配下にある api インスタンスを生成する。例えば、cronjob や job を扱うならば BatchV1Api というドキュメントがある。BatchApi だけでも3つのドキュメントがあるのでちょっとやり過ぎな気もする。

  • BatchApi.md
  • BatchV1Api.md
  • BatchV1beta1Api.md

kubectl コマンドで使う cronjob から手動で job を設定するのを実装してみる。

$ kubectl create job --from=cronjob/my-schedule-job my-manual-job

細かい設定はちゃんと調べないといけないけど、一応はこれで動いた。cronjob のオブジェクトを取得して job のオブジェクトを生成して create するだけ。

var api = new BatchV1Api(this.getKubernetesClient());
var cronJob = api.readNamespacedCronJob(cronJobName, NAMESPACE_DEFAULT, null);
var ann = Map.of("cronjob.kubernetes.io/instantiate", "manual");
var metadata = new V1ObjectMeta().name(newJobName).annotations(ann);
var spec = cronJob.getSpec().getJobTemplate().getSpec();
spec.setTtlSecondsAfterFinished(10);
var job = new V1Job()
        .apiVersion(cronJob.getApiVersion())
        .kind("Job")
        .spec(cronJob.getSpec().getJobTemplate().getSpec())
        .metadata(metadata);
var result = api.createNamespacedJob(NAMESPACE_DEFAULT, job, null, null, null, null);

手動で作成した job の pod は終了後にゴミとして残ってしまうので ttl を設定すれば自動的に削除できることに気付いた。

spec.setTtlSecondsAfterFinished(10);

k8s クラスターの内部、つまり pod 内からリクエストするには ServiceAccount permissions を適切に設定しないといけない。ひとまずローカルの minikube で super user 権限にしたらリクエストはできた。実運用では適切なロールを定義して適切に権限設定しないといけない。

kubectl create clusterrolebinding serviceaccounts-cluster-admin \
  --clusterrole=cluster-admin \
  --group=system:serviceaccounts

会議だらけの1日

0時に寝て6時に起きた。夜中に暑さと気分の悪さで起きるのが常態化してきた。今日は珍しく4つの会議が重なって喋りつくして後半バテた。

隔週の雑談

顧問のはらさんと隔週の打ち合わせ。前隔週を1回飛ばしたので話題がたくさんあった。

もう1年近くスクラムをやっているのでスクラムの弱点や欠点なども少しずつみえてきた。しかし、はらさんと話していると、はらさんが実践してきたスクラムと私がやっているスクラムにはいくつか違いがある。同じスクラムというガイドに沿っていても、実際は組織やプロジェクトの内容によって大きな乖離が生じるというのも事実のように思える。プロジェクトのサービスインに伴い、大きなふりかえりやストーリーポイント運用の見直しを経てわかったことを雑談しながら理解を深めた。主には次の3つにまとめられる。

  • スクラムマスターの限界
  • スクラムと心理的安全性
  • ストーリーポイントという信仰

PO でも開発者でもないスクラムマスターはプロジェクトが経過する時間とともに貢献度が減っていく。ヒト・モノ・カネ・情報というプロジェクトのリソースのうち、もっとも変動するのは間違いなくヒトである。プロジェクトの初期とマイルストーンの区切りにおいてヒトだけが経験を経て大きく成長する。成長しないヒトがいるとすれば、それはそのヒトがさぼっているか、プロジェクトにマッチングしていないと言える。普通のプロジェクトマネジメントにおいて成長しないメンバーなど存在しない。大きなふりかえりによって、驚いたことにスクラムマスターだけがそうじゃなかった。安西先生の言葉を借りるとこうだった。

まるで成長していない ………

PO も開発者も日々の業務からドメイン知識を深めていき、大きく成長しているのに「ただ観ていただけ」のスクラムマスターは全くメンバーの話しについてこれていない。それが上辺だけの浅いふりかえりで終わり、総括はおろか、今後の改善の施策もまとめられないという結果になった。ファシリテーションをスクラムマスターが担っているからそうなってしまう。また過度に感情に配慮したふりかえりをするとネガティブな事実や結果を直視しようとしない。関係する誰かが責任を感じたり嫌な思いをさせる可能性はあるが、それらを認めないと次の改善の施策をたてられない。これは日本の会社ではよく起こることに思える。うちのチームは仲が良いだけのぬるま湯チームになっていてゾンビスクラムにも近付きつつある。

もう1つ、スクラムをやらなくてもよい背景の1つとして、チケット駆動開発と業務のワークフロー最適化の話題をつぶやいてみた。

神戸市さんと雑談

Kobe x Engineer’s Lab という取り組みを担当されている職員さんとその運営会社の担当者と面談した。前から三宮.devのすみよしさんから話しを聞いていたので、そのつてで面談の依頼がきたと思っていたら、全然違う知人からの紹介だったので驚いた。ざっくばらんにエンジニア不足の社会問題の解決のために「神戸市として」どんな施策があるか、コミュニティはどう運営すればいいか、これまでに彼らがやってきたことの成果などを聞きながら雑談していた。エンジニア不足という社会問題と地域性は相性が悪い。私から意見したことはこれらかな。

  • コミュニティはコントロールできない
  • (個人や有志の) コミュニティとお金は相性が悪い

前者について、私自身、プログラマーのコミュニティと深く関わってきたことから、彼らがこの1-2年やってきたことの課題感はいくつか類推できる。コミュニティを中核に、自分たちの意図した目的を達成するのは相当に難しい。コミュニティに人を集めるにはコンテンツが必要になる。コンテンツのないコミュニティに人が集まることはない。多くのケースでコンテンツは主催が提供する。主催がコンテンツを作れなければ人を集められない。そして、コミュニティに人を集めるのも大変な労力だが、集まったとしても多様な集団の行動やモチベーションなどを、他人がコントロールして導くのは難しい。個人や有志のコミュニティは、主催が負担のないレベルで長く続けているうちになにか起きるかもしれないといったふわっとした願望で取り組むのがよい。コンテンツを提供し続ける主催の熱意は不可欠だが、必要以上にがんばり過ぎると燃え尽きてしまってやはり続かない。コンテンツと持続性の2つが優れたコミュニティを運営する上で最初にぶつかる課題かなと私は思う。

エージェント会社と雑談

次のお仕事探しに新しいフリーランス向けのキャリアエージェントの会社と面談してみた。11月開始だと9月中旬ぐらいに出てくる案件になり、いまから探していても時期があわないといった話だった。職務経歴書をまだ提出していなかったせいか、なんとなくマッチングしていない雰囲気がした。そのキャリアエージェントの会社が扱っている案件と法人としての私の働き方があっていないのかもしれない。手応えがなかったので別のキャリアエージェントの会社とも話しつつ次のお仕事を開拓していくことに決めた。

Kubernetes Clients の調査

23時に寝て6時に起きた。

k8s cronjob の手動実行

いろんな定期/バッチ処理を k8s の cronjob に置き換えつつある。これまでアプリケーションサーバーでスケジュール実行していたものも本来サーバーである必要はないのでサーバーアプリケーションから cli アプリケーションに移行したりしている。そうやって定期実行ジョブが増えてくると、今度は調査やデバッグ目的で任意のタイミングで実行したくなる。kubectl を使って次のように実行できる。

$ kubectl create job --from=cronjob/my-schedule-job my-manual-job

この cli を実行すると、cronjob のマニフェストから my-manual-job というジョブの pod が生成されて実行される。開発者ならこれでよいのだけど、非開発者も調査や検証目的で実行したい。そのためには非開発者向けのインターフェースを作らないといけない。本当は chatops のように slack apps によるコマンド実行ができるとカッコよいのだけど、k8s クラスターと slack 間の認証やセキュリティの仕組みを作る必要があって、既存の仕組みがないならそこはセキュリティリスクにも成り得るのでちょっと控えたい。そうすると、既存のサーバーアプリケーションの web api のインターフェースで提供できるようにしたい。複数の言語向けに Kubernetes Clients が提供されている。これを使って cronjob の手動実行を実装できそうな気がする。時間があれば週末に軽く調べてみようと思う。

  • python
  • c#
  • javascript
  • java
  • c
  • haskel
  • go
  • ruby