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

ストレッチ

今日の開脚幅は開始前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

暑い日が続く

0時に寝て6時に起きた。今日は業務時間の大半が打ち合わせだった。

正史 諸葛亮孔明

「第十章 蜀の再建と出師表」を読んだ。

孔明の話し相手になっている 蔣琬 (しょうえん) のプロフィール情報がある。孔明の死後、後継者として内政を取り仕切ったらしい。孔明の後を継ぐ人物は相対比較されることから低い評価になりがちだが、蔣琬はそれでも評価が低くないことから時代が異なればもっと高い評価を得た政治家だったのかもしれない。むしろ孔明が北伐を5回もできた背景は留守中の内政を蔣琬始め、出師表で取り上げられた人物たちがうまく運営していたといった話しも出てくる。軍事は国庫を大いに逼迫し、蜀は魏や呉と比べて国力が劣る国家であったことから内政の負担も大きかったと思われる。

出師表 (すいしのひょう) という言葉を知らなかった。出師表とは臣下が出陣するときに君主に奉る文章のことを指す。孔明が君主の劉禅に奉った文章がとても有名らしい。2つあるので前出師表と後出師表と区別する場合もある。後出師表は孔明が書いたものではないという説もあるらしい。一般に出師表と言えば前出師表のことを指す。この出師表のどういったところがその時代の他の文章と異なるのか、また優れているのかが説明されている。孔明の人格者としての振る舞いや忠義が表れていると言える。時代が大きく違うせいか、出師表の内容を読んで私はどうとは感じなかったものの、これは私が提唱するよい開発文化の1つ「書く」ということの重要性ではないかとも思えてきた。当時、孔明と直接話せる人はせいぜい数十人から百数十人程度であろうから、出師表のような文章で多くの兵士や国民に影響を与えた事例の1つと言えるのかもしれない。