Posts for: #2022/08

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

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つと言えるのかもしれない。

ふりかえりとむきなおり

23時に寝て何度か起きながら7時に起きた。なんか体調が悪い。

ふりかえりとむきなおり

毎週火曜日はふりかえりの日。今週もスプリントゴールは未達に終わったわけだけど、未達が普通で稀に達成できるのが常態化しつつある。悪く言えば ゾンビスクラム 状態と言えるのかもしれない。サービスインのゴタゴタも解消したので PO からもツッコミがあってスプリントゴール達成できない問題が再燃した。私からみたらこんなところか。

  • スプリント初期は前スプリントの残タスクをやるのが常態化している
  • メンバーにやる気と実力がない
  • コミュニケーションコストが高くてオーバーヘッドが大きい (スクラムイベント、確認や待ち時間など)
  • フルタイムで働いていないメンバーがいる (ちょくちょくメンバーも休暇をとる)
  • スプリントが1週間と短過ぎる

その議論をしている中でスクラムマスターが むきなおり をしようといった結論になった。私は用語を知らなかったので調べてみた。

この3点を満たしながら、事業をふりかえって、行きたい方向へとむきなおることが今回の合宿の狙いでした。ただふりかえるだけではなく、あるべき姿との差から、今後の方向性を決めることを、特に「むきなおり」と名前付けしています。ふりかえり、むきなおる。今回の合宿はギルドワークスの今後の方針と向き合うための機会としました。

事業をふりかえって、行きたい方向へむきなおる

ふりかえりの結果から方向性を変えることを呼ぶらしい。私はまったく理解できていないのだけど、普通のふりかえりをして改善するときは何と呼ぶのだろうか。ただの言葉遊びじゃない?という気もする。また後日、そのためのイベントをするそうなのでそのときに理解を深めてみる。

孔明の史実を読み始めた

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

正史 諸葛亮孔明

パリピ孔明 がおもしろかったので孔明の記事などを読んだりしていた。

私、姓は諸葛、名は亮、字を孔明と申します。

作品中のこの挨拶が印象に残っている。キャッチフレーズのようなものが挨拶というのも珍しい?そんなこんなもふくめて孔明の本も読んでみることにした。

「第十一章 第一次北伐」を読んだ。

孔明が軍事で手腕を振るうようになるのは劉備の死後になる。北伐は第一次から第五次まである。そのうちの第一次北伐の失敗は 泣いて馬謖を斬る の故事で有名である。wikipedia の説明では正史と演義でこの故事に関する記述は異なっていることが書かれている。本書では、馬謖が副将の王平の諫言に従わず、山頂に布陣したことそのものは悪い策ではなかったと擁護されている。山頂から地の利をとって一刻も早く要衝を通過したい敵の張郃の軍にとって厄介な配置と考えることもできる。馬謖の失敗は実戦経験が乏しかったことで水源の確保を怠っていたことだという。かたや敵将の張郃は歴戦の名将であることから水源の確保ができていないことを看破して馬謖が布陣する近くの川や水源を確保してしまった。水源を確保することなど軍事に限らず当たり前の話しであり、戦術書に「水源を断て」などと記述しているものはないという。馬謖軍の布陣をみただけでそのことを見抜いた張郃の応用戦術を褒めている。さらに戦争に敗れただけであればまだよかったが、馬謖は敗北の責任を逃れるために逃亡したらしい。その承認欲求とプライドのために自分が負けた事実を受け入れられなかった。本書では、孔明の任命責任も大きいと締め括られている。馬謖が孔明の愛弟子であるから、実績のある諸将よりも私情を優先して実績をつけさせてあげようと抜擢した。その結果、馬謖もより大きな実績を挙げようと行動して失敗してしまった。どういう思いで涙を流したかは本書では書かれていなかった。