vuetify のコンポーネント調査

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

vuetify で検索フォームのコンポーネント作成

ページング処理ができた。次に検索リクエストのための検索条件を扱うフォームを汎用コンポーネントで作ってみることにした。コンポーネントを生成するための検索条件のオブジェクトを外部から渡して、あとはよしなに grid に構成要素を配置する。v-date-picker のところは本当はもっと凝った作りをしないといけない。ここでは型で分岐してコンポーネントを配置する概念を表しているだけ。v-col は cols は1から12までの数字を受け取る。この数値を調整して v-spacer を入れることで行の位置調整もできるのがひと工夫しているところ。イベントハンドラーは click:search とか click:searchClearのように名前を付け替えて、外部から意図したイベントのみをフックできるように考慮している。

<template>
  <v-container>
    <v-row dense>
      <v-col v-for="item in conditions" :cols="item.cols" :key="item.label">
        <v-switch
          v-if="Boolean === item.type"
          v-model="item.value"
          :label="$t(item.label)"
          :clearable="true"
        />

        <v-text-field
          v-if="String === item.type"
          v-model="item.value"
          :label="$t(item.label)"
          :clearable="true"
        />

        <v-text-field
          v-if="Number === item.type"
          v-model="item.value"
          type="number"
          :label="$t(item.label)"
          :clearable="true"
        />

        <v-date-picker
          v-if="Date === item.type"
          v-model="item.value"
          :clearable="true"
        />

        <v-spacer v-if="null === item.type">
          <!-- use an empty block for grid layout -->
        </v-spacer>
      </v-col>
    </v-row>
    <v-row>
      <v-col cols="4">
        <v-btn
          v-text="$t('label.clearSearchCondition')"
          @click="_searchClear"
        />
      </v-col>
      <v-col cols="3">
        <v-btn color="primary" v-text="$t('label.search')" @click="_search" />
      </v-col>
    </v-row>
  </v-container>
</template>
<script lang="ts">
import { PropType, defineComponent } from '@vue/composition-api';
export interface SearchConditionItem<T = any> {
  type: PropType<T> | null;
  name: string;
  label?: string;
  col: number;
  value?: any;
  fromValue?: string | null;
  toValue?: string | null;
}

export default defineComponent({
  components: {},
  props: {
    conditions: {
      type: Array as PropType<SearchConditionItem[]>,
      default: () => [],
    },
  },
  setup(props, context) {
    return {};
  },
  methods: {
    _search(value: any) {
      this.$emit('click:search', value);
    },
    _searchClear(value: any) {
      for (const c of this.conditions) {
        c.value = null;
        c.fromValue = null;
        c.toValue = null;
      }
      this.$emit('click:searchClear', value);
    },
  },
});
</script>

呼び出し側ではこんな感じ。任意の conditions を渡し、検索ボタンをクリックしたときのイベントハンドラーを登録する。

<search-condition-form
  :conditions.sync="searchCondition"
  @click:search="search"
  v-on="$listeners"
/>

vuetify で初めてコンポーネントを作ってみた。雰囲気だけで実装している個人的な所感だけど、template, script, style を1つのファイルに同梱する考え方が私には馴染まない。1つのファイルに複数の構文が混在する認知負荷が気になるのと、1つのファイルに複数のコードを同梱しているメリットが私には感じられない。もしかしたら小さいシンプルなコンポーネントなら見通しがよいのかもしれない。しかし、業務での開発だと一定の複雑さをもつコンポーネントの方が大半だと思うので1ファイルが1画面におさまらない。どうせエディターを画面分割して複数画面でソースを読むのであれば、その画面のソースが1つのファイルでも別のファイルでも私にとってあまり大差ない。ファイル間の依存関係さえ適切に管理できればファイルは用途ごとに分割できた方が人間にとってわかりやすいのではないかとも思う。一方でフレームワーク側からみたら依存関係の解決はやや煩雑な処理になるので開発や依存管理がシンプルになってビルドが速くなるといったメリットがあったりするのかもしれない。どうなんだろう?

vuejs の template 調査

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

連日のサービスイン作業

引き続きサービスインの運用対応は大変そうでちゃんと検証していない修正を慌ててマージしようとしているからテスト環境まで壊れてて関係ない開発にも影響が出ていた。今日も別の施設のサービスインだったらしくて、ある機能がないとそのサービスインの切り替え作業ができないという話しだったそうで、当日に慌てて pr を作ってマージしてた。先週からわかっていた必要な機能を実装してなくて、週末は残業も休出もしてなくて、今日になって慌てて修正してマージしてた。昔の開発と比べてがんばっててできないのではなくて、いまの開発はがんばってないからできないという雰囲気になったなという印象。

vuejs の template と expression

あるフォームのコンポーネントを作ろうと思って interface を定義していてデフォルト値をテンプレート側に指定できるといいんじゃないかと考えた。というのは typescript の interface のメンバーは値を保持できないから。例えば、次のようなコードで :cols="item.col ?? 2" のように表現できたら嬉しいように思う。

<v-row dense v-for="item in conditions" :key="item.label">
  <v-col :cols="item.col ?? 2">
    {{ element }}
  </v-col>
</v-row>

余談だけど、?? は null 合体演算子という名前は知っていたけど、これを英語で何と呼ぶのか知らなかった。Nullish coalescing operator と言う。ググってみると vuejs の issue でもそこそこ議論されていて vue3 からサポートするとしながら、根強い要望があるのか? vue2 でも 2.7 でサポートしたらしい。こういうモダンな javascript の expression を ESNext syntax と呼んだりするみたい。それすらも知らなかった。

たまたまうちで使っているのは vue 2.6.14 なので vue 2.7 で動くのかどうか検証できないけど、いま使っている nuxtjs2 との依存関係があるのでそれ次第で vue 2.7 にアップグレードの可否が決まるらしい。全然フロントエンドの開発がわからないので、こういう基本的なところで引っかかると背景を調べるのに時間がかかる。

vuetify のイベントリスナーの調査

4時に寝て7時に起きた。日曜日にたくさん寝たせいか、昨日は眠れなかった。サービスインの運用対応はまだまだドタバタしていてデータの不整合に苦しんでいるみたい。大変そうだけど、なにもやることない。

vuetify の v-data-table のページング処理

昨日から vuetify のページング処理を調査している。コンポーネント的には2種類ある。

  • v-pagination: 汎用のページングコンポーネント
  • v-data-table: data table のコンポーネント (ページング機能がある)

既存のアプリケーションは nuxtjs で実装されているので vuetify や vue.js のサンプルコードをそのまま動かせるわけではない。丸1日、試行錯誤していてビューと値の束縛、イベントの伝搬などの振る舞いをだいたい理解できた。宣言的なフレームワークなので振る舞いを理解できれば開発量は少なく済む。但し、理解するまで振る舞いを理解するのに設定を試行錯誤で試して動かすのでデバッグは時間がかかる。一覧画面で使っている v-data-table のページング処理対応から始める。Server-side paginate and sort を参考にしながら v-data-table に加えた主な変更はこれら。

<v-data-table
  ...
  :server-items-length="serverItemsLength"
  :disable-pagination="disablePagination"
  :hide-default-footer="hideDefaultFooter"
  :options.sync="options"
  :footer-props="{
    itemsPerPageOptions: [10, 20, 50, 100],
  }"
  v-bind="$attrs"
  v-on="$listeners"
>

v-data-table をラップするコンポーネントでは次のようにイベントリスナーを登録する。

<my-nice-component
  ...
 :server-items-length="serverItemsLength"
 :disable-pagination="false"
 :hide-default-footer="false"
  @update:options="dataOptionsHandler"
>

  async dataOptionsHandler(options: DataOptions) {
    const page = options.page ?? 1;
    const limit = options.itemsPerPage;
    const offset = limit * (page - 1);
    // limit/offset を使った web api リクエスト
  }

vm.$listeners によると v-on="$listeners" のように書くと、ラップしているコンポーネントのイベントリスナーをよしなに伝搬してくれるみたい。これはこれで便利だけど、イベントリスナーの定義がどこにもでてこないのでコード検索ができなくなる。最近の宣言的なフレームワークの流行りと言えばそうだけど、知ってないとなんで動くのかわからないコードにはなる。

週明けのサービスイン

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

3つ目のサービスイン

またまた私は勘違いしていて明日だと思っていたら今日が3つ目のサービスインだった。約1ヶ月ぶりのサービスイン になる。もう3回目なので要領よく切り替え作業やるのかなと眺めてたけど、新たなトラブルもいくつかあって、これまで同様、ドタバタしているようにみえる。私は本番環境にアクセスできないので何かトラブルがあっても聞いた内容から類推で助言を述べるしかできず、とはいえ、何かあったら質問がくるかもしれないからハドルに入って成り行きを見守ってないといけない。とくに手伝うこともないのにメンバーの作業が完了するまで待ってないといけない。この切り替え作業や運用対応をやっていると要件定義やコードレビューなどは放置されるのでまた作業のスケジュールが先送りになる。私は別に困らないけど、しばらくだらだらした開発が続く。

夏バテ

目次

だいぶ暑さはましになってきた感じはあるけど、なんか調子よくなくて寝てた。お腹空いたら外に出掛けるきっかけにもなるんだけど、暑さで食欲もあまりなくて珍しく1日中なにも食べなかった。

休日の本番障害

夕方から寝ていて何度か起きたものの、そのままずっと寝ていた。あまりないことなんだけど、珍しくたくさん眠れた。

ストレッチ

今日の開脚幅は開始前160cmで、ストレッチ後163cmだった。計測の仕方がややいい加減だった気もしたが、先週より少しよくなったということにしておく。右腰の張りが強いのと肩が前に入りがちなので肩を開いて姿勢を保つように心がけるとよいとアドバイスをいただいた。もう通い始めて1年半ぐらい経つ。トレーナーさんも大半が入れ替わっていて通い始めたときに話しかけてくれた私が知っているトレーナーさんはほとんどいない。1年半も経つと人は変わっていくなというのを実感している。私の最初のトレーナーさんは社内制度で別の店舗の助っ人に行っているのでいなくなった人たちが辞めているわけでもないとは思うけど、1-2年で人が入れ替わってもサービスは継続していかないといけないし、会社ってそういうものだなと実感する機会でもある。

aws インフラの調子が悪い?

1-2週間ぐらい前からテスト環境を含めると複数回発生している eks クラスターの障害 がたまたま土曜日の夜という休日に発生した。いま eks クラスターのインフラの振る舞いを把握しているのは私だけなので、気付いてから指示を出して問題が発生している k8s ノードの削除 (ec2 インスタンスの削除) で復旧させるワークアラウンドで復旧させた。私は本番環境にアクセスできないので詳しい調査はできない。状況を正しく把握できてはいないけれど、k8s ノードが死んだり生き返ったりする不安定な状況に発生しているらしく、k8s ノードを削除して新規に作り直すと復旧することがわかっている。NotReady と Ready を繰り返したりしてアプリケーションの振る舞いが不安定になる。NotReady,SchedulingDisabled になれば、おそらく drain して k8s ノードが入れ替わってくれるのだけど、そうならない不安定な状況があるみたい。これ以上の調査は aws のサポートに問い合わせないとわからない。

mvp とその目的

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

隔週の雑談

顧問のはらさんと隔週の打ち合わせ。先日の勉強会で聞いた mvp の考え方 について気になったので相談してみた。はらさんと相談していて mvp とは一般論としてこうだと語ることはできないということが理解できた。大事なのは mvp でやりたい目的であって、その目的によって mvp が何になるかは変わってくる。

  • コンシューマー向け (toC) と業務向けの (toB) でアプリケーションは全く異なる
    • toC 向けはニーズや要件がまったくわからない
    • toB 向けはニーズや要件が明確にわかっている
  • mvp が何になるかは目的によって異なる
    • toC 向けはニーズや要件を把握するのが目的になる場合がある
      • PMF (Product Market Fit) という概念もある
    • toB 向けはニーズや要件に合致するものかどうかを確認するのが目的になる

あと フリーランス協会 という団体があるのを教えていただいた。フリーランスは会社員と比較して守られていない箇所がたくさんある。例えば、取引先とトラブルになって損害賠償が発生するとかがある。うちの会社に限って言えば、役員は基本的に労災保険の適用外になる。この仕組みを見直そうといった取り組みや制度があるという記事もみかけたりはする。そういった保険やら福利厚生やらいろんなものをパッケージングして比較的安い金額で提供してくれるらしい。はらさんによると、なぜ○○協会といった団体がたくさん作られるかというと、団体では受けられるが、個人では受けられないサービスがあるからだという説明をされていた。

むきなおり

チームで改善のための会議があった。会議のホストがいなくて、誰も何も準備をせずに会議が始まり、課題は何なのかを会議中に相談して決めるみたいな、これまでもそうだったようにだらだらした会議の始まり方をした。私はすぐにいなくなる人間なのであまり今後の方針についてはコメントしないようにしている。議論の行方だけを聞いていた。ストーリーポイントとベロシティの運用が混乱をもたらし始めていて、PO がベロシティは納期の対する進捗度合いを把握するために計測していると発言していて取り返しがつかなくなっているように感じた。いつまでに何ができかるかの期日を出さないといけないという使命感からストーリーポイントをスケジュールと同視ことから抜け出せていない。周りの知人と話したり、私自身の解釈としてもストーリーポイントとベロシティで把握できるのはチームの成熟度や成長の度合いだけになる。1年前よりもベロシティが増えているのであればチームは成長したと言える。言わばたったこれだけのことしか分からないし、成長の度合いに関すること以外に使うべきではない。

ストーリーポイントの誤解と誤用は、特にプロセスがチームから部署、企業へとスケールして行くときに、時間をかけて増幅されていきます。これらの誤解と誤用を正すのは、時間と労力を要します。

ストーリーポイントを使うのをやめよう で書かれているように、ストーリーポイントという抽象化された数値が独り歩きして、自分たちの都合のよい指標になると考え始めている。この記事によると、これが周りに広がっていくにしたがって混乱が増幅していくように書かれている。いま正にそういう雰囲気をみていて実感した。

spring boot におけるプロキシ実装

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

spring boot における簡易的なミドルウェアの実装

bff (backend for frontend) の web api サーバーから別の web api サーバーのエンドポイントを呼び出す処理を書いていてただプロキシする処理のためだけに controller を書くのも面倒だなと気付いた。controller よりも低いレイヤで raw request を扱うにはどうしたらいいかを調べてみたら spring の Filter を使うのが最も簡単そうにみえた。その spring の Filter の1つである OncePerRequestFilter を使って簡易的なプロキシの処理を実装してみた。OncePerRequestFilter はリクエストに対して1度だけ呼ばれることが保証された Filter になる。これらのドキュメントがどこにあるかわからなかったので baeldung のチュートリアル What Is OncePerRequestFilter? をみた方が早いかもしれない。

Filter が複数あるときは実行順序を指定したいときは Order というアノテーションで制御できる。

@Component
@Order(30)
public class MyFilter extends OncePerRequestFilter {
  ...
}

OncePerRequestFilter の doFilterInternal メソッドをオーバーライドすれば任意のミドルウェアっぽい処理を実装できる。

    @Override
    protected void doFilterInternal(
        HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
        // ここに任意の前処理を実装する
        filterChain.doFilter(request, response);
        // ここに任意の後処理を実装する
    }

あとは HttpServletRequest と HttpServletResponse を直接操作してリクエストから必要な情報を取り出して、クライアントでリクエストして返ってきたレスポンスをそのまま返してあげればよい。簡単に実装したものが次になる。クライアントには 以前に少し調べたことを書いた WebClient を使っている。WebClient じゃなければ、もう少しシンプルに書ける気もする。

@Component
@Order(30)
public class RequestForwardFilter extends OncePerRequestFilter {
    @Autowired
    private List<RequestForwardMatcher> matchers;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        var matcher = this.getMatcher(request);
        if (matcher.isEmpty()) {
            filterChain.doFilter(request, response);
            return;
        }
        writeResponse(response, this.forwardRequest(request, matcher.get().getWebClient()));
    }

    private Optional<byte[]> forwardRequest(HttpServletRequest request, WebClient webClient) throws IOException {
        var method = HttpMethod.resolve(request.getMethod().toUpperCase());
        var originalUri = request.getRequestURI();
        var uri = this.getUri(originalUri, request.getQueryString());
        var spec = this.getSpec(method, webClient, uri, request.getReader());
        return spec.retrieve().bodyToMono(byte[].class).blockOptional();
    }

    private void writeResponse(HttpServletResponse response, Optional<byte[]> bytes) throws IOException {
        if (bytes.isEmpty()) {
            return;
        }
        var stream = response.getOutputStream();
        stream.write(bytes.get());
        stream.flush();
        stream.close();
    }

    private byte[] getRequestBody(BufferedReader reader) {
        return reader.lines().collect(Collectors.joining()).getBytes();
    }

    private String getUri(String originalUri, String queryString) {
        var path = originalUri.replace("/api", "");
        if (StringUtils.hasLength(queryString)) {
            return String.format("%s?%s", path, queryString);
        }
        return path;
    }

    private WebClient.RequestHeadersSpec<?> getSpec(
            HttpMethod method, WebClient webClient, String uri, BufferedReader reader) {
        switch (method) {
            case GET:
                return webClient.get().uri(uri);
            case POST:
                return webClient.post()
                        .uri(uri)
                        .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                        .bodyValue(this.getRequestBody(reader));
            case PUT:
                return webClient.put()
                        .uri(uri)
                        .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                        .bodyValue(this.getRequestBody(reader));
            case DELETE:
                return webClient.delete().uri(uri);
            default:
                throw new RuntimeException(String.format("Unsupported HTTP method: %s", method));
        }
    }

    private Optional<RequestForwardMatcher> getMatcher(HttpServletRequest request) {
        for (var matcher : this.matchers) {
            if (matcher.match(request)) {
                return Optional.of(matcher);
            }
        }
        return Optional.empty();
    }
}

matcher のサンプル実装。

public class RequestForwardMatcher {
    private static final String ASTERISK = "*";

    private final Pattern pattern;
    private final List<String> methods;
    private final WebClient webClient;

    public RequestForwardMatcher(String pattern, List<String> methods, WebClient webClient) {
        this.pattern = Pattern.compile(pattern);
        this.methods = methods;
        this.webClient = webClient;
    }

    private boolean matchMethod(String method) {
        if (this.methods.contains(ASTERISK)) {
            return true;
        }
        return this.methods.contains(method);
    }

    public boolean match(HttpServletRequest request) {
        if (!matchMethod(request.getMethod())) {
            return false;
        }
        return this.pattern.matcher(request.getRequestURI()).matches();
    }

    public WebClient getWebClient() {
        return this.webClient;
    }
}

たまには画面作り

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

リファクタリングとインフラ移行

ここ2週間ほどリファクタリングやらインフラ変更やらをしてきて、来週からまた新しい施設がサービスインするので区切りとしてリファクタリングは一旦終わりにする。今日がその集大成となるインフラ移行も含めた本番リリースだった。インフラ移行するときはなにかしら障害が起きる前提で待機しているものの、今日もすんなりと意図した通りに移行できて、してやったりではあるものの、モノ足りなさで拍子抜けしてしまった。また昨日から社内 wiki にも minikube の使い方、k8s cronjob の設計、バッチ処理の設計と実装についてドキュメントなどを書いていた。いままですべて私が1人で担当していたものを他メンバーでも作業できるようにドキュメント化した。近いうちにいなくなるので引き継ぎのドキュメントにもなる。

nuxt で画面作り

ここ最近2種類の web api の機能を作ったのでその管理画面も2つ作る必要がある。私はフロントエンド開発の素人なので他のメンバーが作ってくれないかと声をかけてはいたけど、みんな忙しいようなので私が作ることにした。今週は nuxtjs の新規画面の開発をがんばってみようと思う。既存のソースを読む限りはそんなに複雑ではなさそう。素人が雰囲気で実装しても動くんじゃないかと思っている。ソースコードを読んでいて url 設計はめちゃくちゃだし、一覧画面にはページング機能も実装されていない。素人がソースを読んで基本的な骨子や機能が正しく実装されてないことがわかってしまうのは品質レベルとしてなにかがおかしい。圧倒的低品質と呼ぶのか、こんなことが起こってしまうのはよい開発文化がないせいなのだろうと考えている。

リーン思考の考え方

1時に寝て6時に起きた。夜中に3回ぐらい起きたかな。夜あまり眠れない。

リーン思考を学ぶ勉強会

スクラムマスター主催の勉強会があった。スクラムマスターは折に触れてリーン思考が大事という発言をするので気にはなっていた。リーンという概念はトヨタ生産方式から触発されている。リーン思考はフロー効率の方を重視するらしい。

  • フロー効率: 価値を素早く提供する
  • リソース効率: リソースが遊び無く稼働している

どちらも効率の話しをしているが、なんの効率をあげるかが異なる。i/o におけるレイテンシとスループットの関係に似ている。レイテンシを上げればスループットは下がるが、スループットを上げればレイテンシは下がるという反比例の関係となる。おそらくフロー効率とリソース効率の話しがトヨタから出ているのであれば、工場の稼働率の話しとも関連があると考えられるのでお互いにトレードオフの関係が成り立つのではないかと推測する。リーン思考は小さく作って育てていくことを指していて、同義語として mvp, イテレーティブ、バーティカルスライドなどがある。スクラムは戦術であり、リーン思考を実践するのは戦略となる。リーン思考を具体的にどう実践するかは自分たちで考える必要がある。あとは用語や概念の詳細な説明が主だった。リーン思考のメリットとして次があげられていた。

  • 早いタイミングでフィードバックを得られる
  • 早く作るとリスクを早期発見できる
  • 小さい状態でフィードバックをもらった方が手戻りが少なくなる

一方でリーン思考の概念はパラダイムシフトを伴うので難しいと説明されていた。難しい背景は次になる。

  • ウォータフォールはリソース効率を重視する考え方である
  • 産業革命の時代に科学的管理法としてウォータフォール的な考え方が広まった
    • 工場でのモノヅクリにはうまくいった
    • ソフトウェア開発ではあまり適用できない

また MVP(Minimum Viable Product)の意味を理解する。そして、なぜ私はEarliest Testable / Usable / Lovableを好むのか。 の考え方の例としてこの記事のスライドを引用していた。これはこれで理解できるけれど、これはプロダクトアウトの考え方であって、業務アプリケーションはまた違うのではないかと私には懸念に思えた。自分たちのプロジェクトにとってどう実践していくかが重要になるが、そういった話しはとくになかった。プロダクトオーナーからも現状とリーン思考の概念とのギャップや違和感を感じて質問していたようにみえた。

リーン思考は難しくてなかなか実践できないといった説明があったところに私は違和感を感じた。リーン思考とは無関係にそもそもプロジェクトマネジメントは難しい。現実のプロジェクトマネジメントは生きものなのでケーススタディや教科書通りの論理でできるものではないと私は考えている。現実のプロジェクトにおける、ヒト・モノ・カネ・情報の4つを考慮した上で意思決定を適切なタイミングで行うことに正解はない。歴史に if はないので重要な意思決定を下してそれがどんな結果になろうと、別の意思決定をしたときの未来を知ることはできない。うまくいかなかったときにできることはそのふりかえりだけで、リーン思考を実践できていないという考え方はやや責任転嫁しているようにも受け取れた。

k8s ノードの削除方法がわからない

1時に寝て7時に起きた。寝冷えしてお腹痛い。

eks クラスターの障害

日曜日にテスト環境の eks クラスターで障害が発生していた。k8s ノードが NotReady になっていて、しばらくすると NotReady,SchedulingDisabled に変わって、それから新しい k8s ノードが起動して古いものが削除されて置き換わった。おそらくエラーが発生し始めてから1時間ほどはかかっていたと思う。わりと時間がかかるので明らかに k8s ノードが不調だと人間が判断しているなら ec2 インスタンスの切り替えを早くやりたい。k8s の公式ドキュメントの Use kubectl drain to remove a node from service では次の手順で行うように書いてある。

$ kubectl drain <node name>

drain が正常終了すれば安全に k8s ノードを削除してよいのかな?

$ kubectl delete node <node name>

eks クラスターで障害が発生していたときに drain を実行するとエラーになったのでそのまま delete node したら k8s ノードは削除されたものの、自動的に新しい k8s ノードが起動しなかった。aws のマネジメントコンソールから ec2 インスタンスを調べたら起動したままだったので強制的に ec2 インスタンスを終了させたところ、オートスケールの設定から ec2 インスタンスが起動してきて復旧した。但し、このやり方は k8s が意図した手順ではないようにも思える。軽く調べた範囲では k8s ノードの正しい削除方法 (置き換え方法?) がみつからなかった。そんなことを日曜日に確認していたら月曜日にほぼ同じ現象が本番環境の eks クラスターでも発生した。私は一度経験していたので同僚に指示して経過を観察していた。ここで書いたのと同じような手順で復旧した。おそらく aws 側のなにかのメンテナンス作業でうちの eks クラスターだと k8s ノードが死んでしまうような作業があったのではないか?と疑いをもっている。