tseki blog

my study room

Jellyfin のラジオ録音を「聴き溜め」る Android アプリ Kikidame を作った — 設計の話と、ClaudeCode で書いた話

自宅の Jellyfin サーバにラジオ録音とポッドキャストを溜めていて、それを通勤や車の中で聴くための Android アプリを作りました。名前は Kikidame(聴き溜め)。録り溜めた回を、時間のあるときに手元で聴く、というアプリの用途そのものです。

この記事は前半が「何ができるか」、後半が「どう作ったか」です。後半には、再生位置をサーバに送らない設計、多言語化の方式、Jellyfin 10.10 対応で踏んだ穴、そしてコードのほぼ全部を Claude Code に書かせた開発の進め方と、その結果ストアに載せられなかった話を書きます。

何のためのアプリか

Jellyfin には公式アプリ、Findroid、Finamp と良いクライアントがありますが、どれも映像か音楽が中心です。ラジオ録音やポッドキャストのような「回が積み上がる音声」を聴くには、2 つ足りないものがありました。

  1. 番組単位で自動的に手元に置き、聴き終えたら消すこと
  2. サーバが落ちていても完全に成立すること

Kikidame はこの 2 点だけを目的にしています。逆に、音楽(回が積み上がらない)とオーディオブック(古い章から順に聴く)は対象外です。

保持ルールで自動ダウンロード

番組ごとに「同期対象にするか」と、2 つの保持ルールを決めます。

  • 最新 N 回まで保持 — 公開日の新しい順に N 回を超えた分は手元に置かない
  • 再生済みなら削除 — 聴き終えた回は次の同期で消す

あとは 6 時間ごとの同期が、不足分をダウンロードし余剰分を消します。手動でダウンロードした回は「固定」になり、ルールでは消えません。Wi-Fi のみの設定もあります。

オフライン優先

聴くのは常に手元のファイルです。再生位置と再生済みも手元だけで持ち、サーバへは送りません(理由は後述)。電波の無い場所でも、サーバのメンテナンス中でも、アプリの動作は何も変わりません。

再生位置の記録と再開

2 時間のラジオ録音を一気に聴くことはまず無いので、「どこまで聴いたか」の扱いはこのアプリの中心です。

  • 再生中は 10 秒ごとに位置を保存し、次に開いたときはそこから始まります。一覧の行にも進捗バーが出ます
  • 末尾の 2 分以内まで進んだら再生済みになります。エンディングの曲を飛ばして止めても再生済みになり、次に開くと先頭から。再生済みは手動でも切り替えられます
  • 開いて数秒で閉じた回(5 秒未満)は記録しません。⏭ の連打や誤タップで「途中の回」が増えないようにするためです
  • 再生記録は、保持ルールでファイルが消えても残ります。再ダウンロードしても続きから聴けます

再開は端末をまたいでも切れません。車の Android Auto で 30 分聴いて降りると、スマホのミニプレイヤーに同じ回が同じ位置で載っています(Auto はスマホの投影なので、実は同じプロセスが動いているだけです)。アプリのプロセスが死んだ後でも、Bluetooth ヘッドセットの ▶ や車との接続で最後の回が続きから始まり、Auto の「続きから」タブには途中の回が番組をまたいで最近順に並びます。

その他

  • ±10 秒、倍速(1.0〜2.0)、スリープタイマー(時間指定/この回の終わりまで)
  • Android Auto 対応。車の画面で「続きから」「よく聴く」「番組」をたどって再生でき、車を降りたらスマホのミニプレイヤーに同じ回が載っています
  • 英語と日本語の UI(Android 13 以上ならアプリごとに切り替え)
  • 対応サーバは Jellyfin 10.10 以上

想定は 1 サーバ・1 ユーザー・1 ライブラリで、「聴くのはこのアプリだけ」です。ここから先は、その前提がなぜ設計に効いているかの話です。

設計 1: 再生位置をサーバに送らない

普通に考えると、再生位置はサーバに保存して端末間で同期したくなります。Jellyfin にもそのための API(UserData の PlaybackPositionTicks)があります。最初の設計では「手元が正、オンライン復帰時に一方向で送る」にしていました。

ところが調べると、Jellyfin の音楽ライブラリでは再生位置がほぼ使われません。映像なら「続きから再生」が出ますが、音楽アイテムの resume 挙動はサーバ設定に左右され、2 時間のラジオ録音の位置を預けても、設定次第で失われます。送る先が使わない値を、時計のずれやタイムスタンプの粒度を気にしながら同期する意味は薄い。

そこで「送らない」に倒しました。

  • 再生位置と再生済みは Room のテーブル playback_states だけが持つ
  • 「聴くのはこのアプリだけ」という前提を明示し、他端末や Web との併用は想定しない
  • 将来 iOS 版や複数端末が要るなら、そのとき送信を復活させる

再生位置の規則は core/domain の純粋関数にまとめてあり、UI・サービス・Android Auto のどこから再生しても同じ判断になります。上で書いた「末尾 2 分」「5 秒」はここの定数です。

object PlaybackRules {
    val PLAYED_THRESHOLD = 2.minutes      // 末尾からこの範囲に達したら再生済み
    val NOT_STARTED_THRESHOLD = 5.seconds // これ未満で未再生なら「聴き始めていない」

    fun isNearEnd(position: Duration, runtime: Duration): Boolean =
        position > Duration.ZERO && runtime - position <= PLAYED_THRESHOLD

    /** 位置の更新。末尾付近なら再生済みを立てる。一度立ったら、シークで戻しても自動では下ろさない */
    fun advance(state: PlaybackState, position: Duration, runtime: Duration, now: Instant) =
        state.copy(position = position, played = state.played || isNearEnd(position, runtime), updatedAt = now)

    /** 再生開始時の位置。残りが閾値以下なら先頭から、そうでなければ保存位置から */
    fun resumePosition(position: Duration, runtime: Duration): Duration =
        if (isNearEnd(position, runtime)) Duration.ZERO else position
}

Android 側はこれを呼ぶだけです。PositionPersister が Player.Listener で 10 秒ごとに advance を Room に書き、再生画面も Auto の onSetMediaItems も resumePosition で開始位置を決めます。規則が 1 箇所にあるので、後から Auto を足したときに「同じ規則で記録される」ことを新しく検証する必要がありませんでした。

もう一つ、この設計で効いたのが再生記録のライフサイクルです。手元のファイルが保持ルールで消えても、再生記録は残します。消えると「再生済みの回が、次の同期で再ダウンロードされ、また再生済みなので消える」という往復が起きるからです。記録は各回がサーバに存在する限り生き、ファイルとは別の寿命を持ちます。

設計 2: 多言語化 — 文字列を解決するのは Compose だけ

最初は UI の文字列が Kotlin に日本語で直書きされていました(241 箇所)。英語圏の Jellyfin ユーザーにも使ってもらうために strings.xml に出したのですが、問題は Composable の外で作る文言でした。同期のスナックバー(「各回 12 を取得しました」)、ViewModel のエラー、再生失敗の通知など、約 70 箇所が String を Flow で流していました。

選択肢は 2 つです。

  • (A) Context を注入して作る側で getString し、String のまま流す
  • (B) UiText(Res(id, args) / Plural / Plain / Joined の sealed interface)を流し、表示する Composable が resolve() する

(B) の UiText はこれだけの型です。

sealed interface UiText {
    data class Plain(val text: String) : UiText
    data class Res(@StringRes val id: Int, val args: List<Any>) : UiText
    data class Plural(@PluralsRes val id: Int, val quantity: Int, val args: List<Any>) : UiText
    data class Joined(val parts: List<UiText>, val separator: UiText = Plain("")) : UiText
}

@Composable
fun UiText.resolve(): String = when (this) {
    is UiText.Plain -> text
    is UiText.Res -> stringResource(id, *args.toTypedArray())
    is UiText.Plural -> pluralStringResource(id, quantity, *args.toTypedArray())
    is UiText.Joined -> parts.map { it.resolve() }.joinToString(separator.resolve())
}

作る側はこう書きます。Context は出てきません。

// LibraryRefresher(同期の結果をスナックバーへ)
say(UiText.Plural(R.plurals.sync_program_checked, episodes))
// → 英語 "Checked 242 episodes"、日本語「各回 242 を確認」

// 表示側(Composable)
LaunchedEffect(Unit) {
    viewModel.messages.collect { snackbarHostState.showSnackbar(it.resolve(context)) }
}

(A) は Android でも普通に使われる現実的な方法で、既存の型を変えずに済みます。それでも (B) にしました。決め手はテストです。(A) だと 150 箇所の日本語アサーションを context.getString(...) に書き換えて Robolectric に載せることになり、テストの意味は変わらない。(B) なら assertEquals(UiText.Res(R.string.sync_fetched, 12), actual) で「どのメッセージが、どの引数で選ばれたか」を検証でき、プレーン JVM のままです。ついでに、保持されているエラー文言が言語切替に追従するようになり、Service・Worker・UI が共有するシングルトンに Context を混ぜずに済みました。

作る側 5 つ、出す側 5 箇所の改修で、241 箇所の抽出本体に比べれば 1 割程度の上乗せでした。

英語の文言は、CONTEXT.md(用語集)に各用語の英語名を先に決めてあったので、Program / Episode / Publisher / Pinned / Starred / On Hold / Gone のように機械的に揃えられました。用語集を先に作っておく効果は、多言語化のときに一番はっきり出ます。

設計 3: Jellyfin 10.10 で踏んだ穴

対応範囲を「10.10 以上」と宣言する以上、10.10 で本当に通るかを確かめる必要があります。jellyfin-sdk-kotlin 1.9 は Jellyfin 12 向けで、古いサーバの応答をデコードできない箇所があります。

コンテナで 10.10.7 と 12.0.0 を立て、合成ライブラリ(ffmpeg で作った無音の m4a にタグを付けたもの)を入れて、JellyfinGateway の全メソッドを実サーバに当てる統合テストを書きました。環境変数が無ければスキップされるので、CI の ./gradlew test はそのままです。

落ちたのは 1 箇所でした。番組の存在確認に GET /Items/{id} を使っていたのですが、この API は MediaSources 込みの全フィールドを返します。10.10 は MediaStream.IsOriginal を返さず、SDK 1.9 はそれを必須と見なすので、デコードで落ちる。

// 変更前: 全フィールドが返り、10.10 では MediaStream.IsOriginal の欠落でデコードに失敗する
val album = try {
    api.libraryApi.getItem(UUID.fromString(id)).content
} catch (e: InvalidStatusException) {
    if (e.status == 404) return null else throw e
}
if (album.type != BaseItemKind.MUSIC_ALBUM) return null

// 変更後: 基本フィールドだけ返り、無い ID・番組でない ID は空の一覧になる
val result = api.libraryApi.getItems(
    GetItemsRequest(ids = listOf(UUID.fromString(id)), includeItemTypes = listOf(BaseItemKind.MUSIC_ALBUM)),
).content
return result.items.firstOrNull()?.toServerProgram()

「404 なら消失」だった判断が「空なら消失」に変わっただけで、決定そのものは同じでした。

このとき、設計の決定記録(ADR)に「/Items/{albumId} を取り、404 なら Gone」と旧実装の詳細が書いてあったのをレビューで拾われ、書き直しています。決定記録に実装の詳細を書きすぎると、こういう保守が要ります。

Android Auto で踏んだ穴

Media3 の MediaLibraryService で Auto に対応したときに 2 つ。

  1. 各回の 2 行目に公開日を出そうと MediaMetadata.subtitle を設定しても、Auto には番組名が出る。Media3 の LegacyConversions は displayTitle が無いと title → artist → album の順で 3 行を埋め、subtitle を捨てます。ブラウズ用の項目にだけ displayTitle を置くと通ります
  2. 「最後に聴いていた回を再開する」onPlaybackResumption を実装しても、システムからは一切呼ばれない。原因は Manifest に androidx.media3.session.MediaButtonReceiver が宣言されていないこと。Media3 はこのレシーバの有無で再開機能ごと有効・無効を切り替えます(canResumePlaybackOnStart() の中身は broadcastReceiverComponentName != null だけ)。バイトコードを読むまで分かりませんでした
<!-- これが無いと Media3 は再開(SystemUI の isRecent / onPlaybackResumption)を有効にしない -->
<receiver android:name="androidx.media3.session.MediaButtonReceiver" android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MEDIA_BUTTON" />
    </intent-filter>
</receiver>

再開そのものは、Room から「最近聴いた回」を 1 件引いて、アプリの再生画面と同じ番組のキューを組むだけです。

override fun onPlaybackResumption(session: MediaSession, controller: ControllerInfo) = scope.future {
    val recent = libraryRepository.getRecentlyListened(limit = 1).firstOrNull()
        ?: return@future MediaItemsWithStartPosition(emptyList(), C.INDEX_UNSET, C.TIME_UNSET)
    programQueueStartingAt(recent)   // 番組の手元にある回を古い順に積み、resumePosition から
}

車が無くても Desktop Head Unit(DHU)で確認できます。WSL2 で動かすには libc++ を入れ、stdin を閉じないように起動する、初回はスマホ側の許可画面を 5 秒以内に進める、といった細かい罠があり、docs/development.md にまとめてあります。

Claude Code で書いた、という話

ここからは開発の進め方です。正直に書くと、Kotlin のコードはほぼ全部 Claude Code が書いています。私は Java と C# の経験はありますが Kotlin と Compose は学習中で、自分で書いたコードはほとんどありません。代わりに、次のことを人がやりました。

決めること。 用語集(CONTEXT.md)を先に作り、「番組」「各回」「配信元」「固定」「判断保留」のような言葉の意味と、使わない言葉(「アルバム」「トラック」「お気に入り」)を決めました。設計上の分岐は、着手前に Claude と一問一答で潰し(推奨案を出させ、私が選ぶ)、決定は ADR に落とします。この記事の「設計 1〜3」は、その ADR をほぼそのまま書き起こしたものです。

分けること。 実装は issue 単位で subagent に振り、それぞれが git worktree で別ブランチを切ります。同時に 2 本走らせるときは「触るファイル」を先に分け、衝突は strings.xml の追記だけになるように issue に書きます。

レビューを回すこと。 PR が来たら、別の read-only な subagent に 5 つの観点(規約、バグ、履歴、過去のレビュー指摘、コード内コメントとの整合)でレビューさせ、指摘を実装側に戻します。このレビューで拾われた本物のバグがいくつもあります。たとえば「最近聴いた回」を SQL で limit * 2 件引いてから Kotlin で絞る実装は、limit = 1 のとき上位 2 件が条件外だと空になる — 2 人のレビュアーが独立に同じ指摘をしました。閾値を SQL のバインドパラメータに渡す形に直しています。

実機で確かめること。 adb でスクリーンショットを撮って Claude に読ませ、DHU は名前付きパイプで tap x y を送って操作しました。IzzyOnDroid 向けのスクリーンショットは、実機に一時的な別ユーザーを作り(pm create-user。メインユーザーのデータは触らない)、テスト用コンテナに英語の架空の番組を入れて撮っています。

うまくいかなかったことも書いておきます。Claude は一度、Android Auto の再開が効かない原因を「通知コントローラにライブラリコマンドが無いから」と推定して直したのですが、実機で試すと変わらず、真因は上に書いた MediaButtonReceiver でした。推定は「実機で確かめるまで信じない」が正解で、実機に入れる手順が軽いことがそのまま品質になります。

ストアには載せられなかった

配布は GitHub Releases と Obtainium です。当初は IzzyOnDroid(開発者署名の APK をそのまま配る F-Droid 系のリポジトリ)に載せる予定で、Fastlane の説明文とスクリーンショットまで用意しました。ところが申請の直前に App Inclusion Policy を読むと、こうあります。

We are strongly opposed to apps which are fully or in part created by generative AI tools. Vibe-coded apps will be rejected.

申請テンプレにも「AI 使用レベル(None〜Dominant)」の必須項目があります。正直に書けば Dominant で、却下対象です。虚偽の申告はしたくないので、申請しませんでした。F-Droid 本家も同じ方針です。

ストア側の判断は理解できます。人が読んでいないコードが大量に流れ込めば、審査は成り立ちません。一方で、用語集と ADR で決定を人が持ち、レビューと実機確認を回した上で出しているコードと、プロンプト一発のものが同じ扱いになるのは、今の段階では仕方がないとも思います。F-Droid クライアントから入れたい人のために、自前のリポジトリを GitHub Pages で配ることを次の候補にしています。

まとめ

  • 用途を「回が積み上がる音声」に絞ると、再生位置をサーバに送らない・手元のファイルだけで再生する、という割り切りができる
  • 文言の解決を Compose に寄せると、多言語化がテストと両立する
  • 対応サーバの版は、コンテナで実サーバに当てるテストで担保する
  • Claude Code に書かせるなら、人は「言葉と決定」を持ち、レビューと実機確認を仕組みにする。そして、それでもストアの AI ポリシーには引っかかる

ソースと課題は https://github.com/t-seki/kikidame にあります。Jellyfin にラジオ録音を溜めている人は、試してもらえると嬉しいです。

AtCoder を `acr new abc400` ですぐに始められる Rust 製 CLI を作った話

AtCoder のコンテストを解くとき、毎回地味に面倒だったのが「開始前の準備」と「提出周り」でした。
ディレクトリを切って、Cargo.toml を書いて、サンプルケースをブラウザからコピペして、エディタとブラウザを並べて……

そんな面倒さを全部 1 コマンドに押し込んだ CLI ツールが acr です。

github.com

"AtCoder-Rust" を略して "acr" としました。 Rust 製のCLIで、acr new abc400 とだけ叩けば、Cargo ワークスペースが生成され、サンプル入力が全問分 fetch され、エディタと問題ページがブラウザで開き、あとは解き始めるだけ——そういう体験を目指しました。

この記事では、acr でできることを一通り紹介したうえで、開発中にハマった「Cloudflare Turnstile 問題」や、配布・リリースまわりの工夫なども書いていきます。

30 秒で見る acr

# インストール(Linux / macOS)
curl --proto '=https' --tlsv1.2 -LsSf \
  https://github.com/t-seki/acr/releases/latest/download/acr-cli-installer.sh | sh

# 初期設定(エディタとブラウザを対話式に設定)
acr init

# ログイン(ブラウザから REVEL_SESSION クッキーを貼るだけ)
acr login

# コンテスト開始
acr new abc400

これだけで abc400/ 配下に A〜F 問題の Cargo プロジェクトが並び、abc400/a/src/main.rs がエディタで、問題 A のページがブラウザで開きます。

解き終わったら、その問題のディレクトリで:

acr test     # サンプルケースで一括テスト(色付き)
acr submit   # テストを通してからブラウザで提出画面を開く

主要機能

acr new — ワンコマンドのワークスペース生成

acr new abc400 は、裏で AtCoder の問題ページを叩いてサンプルケースを全部 scrape し、tests/1.in・tests/1.out 形式で保存します。
Cargo.toml には後述の "AtCoderで利用可能" な依存一覧がピン留めされ、src/main.rs はユーザーが ~/.config/acr/template.rs に置いたテンプレートから生成されます。

--at フラグ — 開始時刻まで待機する

コンテストに備えて、acr new abc400 --at 21:00 と打っておけば、指定時刻まで待機して、タスクが公開された瞬間にワークスペースを生成してくれます。
--at 直後は AtCoder 側も混み合うので、初回リトライの間隔を短くしたり 404 をリトライ対象に含めたり、このあたりはコンテスト実戦で試しながら何度か調整を入れました。

同じ待機メカニクスを使った acr virtual abc300 も用意しています。過去コンテストを選んで、任意の時刻からバーチャル参加を始められます。

"AtCoderで利用可能" な依存クレートのピン留め

ローカルではコンパイルできてもジャッジで CE になったら悲しいですよね。
acr new が生成する Cargo.toml は、AtCoder ジャッジが提供しているクレートセットとバージョンをそのままピン留めします。
ac-library-rs・proconio・itertools・num・nalgebra・ndarray・petgraph・rustc-hash などが最初から使えて、ローカルで通れば基本的にジャッジでも通ります。

ジャッジ側のクレートセットが更新されたときは、acr update -d で既存ワークスペースの Cargo.toml を追従させられます。

エディタ/ブラウザ統合

acr init でエディタとブラウザコマンドを設定でき、フラグ込みでも OK です:

acr config editor "code --new-window"
acr config browser "firefox --new-window"

WSL2 から Windows 側の Chrome を叩くような、パスにスペースが入るケースも shlex でパースしています。

テンプレート共有

~/.config/acr/template.rs がコードファイルを作成する際のテンプレートになります。
他人のテンプレートを使いたいときは:

acr template add https://gist.github.com/someone/abcdef1234
acr template add https://github.com/someone/dotfiles/blob/main/atcoder.rs
acr template show    # 現在のテンプレートを表示
acr template reset   # ビルトインに戻す

GitHub の blob URL や Gist のきれいな URL は自動で raw URL に書き換えます。
上書き前には .bak に退避するので、気軽に試せる設計にしています。

開発の裏側

なぜ Rust か

(1) 自分が AtCoder を解くなら Rust を使いたかった
(2) インストールやカスタマイズが容易なツールが欲しかった
(3) 単一バイナリで配れるので macOS / Linux / Windows のユーザーに同じ体験を届けやすい

という理由です。

Cloudflare Turnstile との戦い

開発中に一番大きかった方針転換は、AtCoder が Cloudflare Turnstile を導入していたことでした。
これにより、従来の CLI ツールが使っていた「フォームに username / password を POST して cookie を受け取る」タイプの自動ログインが、事実上できなくなってしまいました。

最初はログインや提出もターミナルから行う実装を考えていましたが、Turnstile によるエラーがどうしても解消できませんでした。
ターミナルから提出・結果確認が出来たら嬉しいとは思いつつも、Headless Chrome を使ったりなどはしたくなかったので、「ログインと提出はブラウザに任せる」 方針に倒しました。

  • acr login は AtCoder のログインページをブラウザで開き、ユーザーが DevTools から REVEL_SESSION cookie 値を 1 回だけコピペする方式に
  • 提出 (acr submit) はテストを通したうえで、提出ページをブラウザで開く。コードは事前にクリップボード (arboard crate を使用) に載せておくので、提出画面で Ctrl+V → Submit の 2 アクションで送れる

「完全自動」を諦める代わりに、「CLI ツールが壊れ続けない」という安心感を取った形です。

配布パイプライン

個人ツールを真面目に配るのは初めてだったので、ここもけっこう勉強になりました。現状は 4 ルートで配っています:

  1. Prebuilt binary(shell / PowerShell インストーラ)— cargo-dist が GitHub Actions で macOS・Linux・Windows のバイナリを作って Release に添付
  2. Homebrew tap(t-seki/acr)— 同じく cargo-dist が formula を生成
  3. cargo binstall — prebuilt を引いてくる
  4. cargo install acr-cli — ソースからビルド

リリース自体は release-please を使った Conventional Commits 駆動で、
feat: や fix: を main にマージすると自動的に "Release PR" が立ち、マージするとタグと GitHub Release が作成されます。
タグ push をトリガーに cargo publish が走りますが、このとき crates.io の Trusted Publishing を使っていて、OIDC 経由で短命トークンを発行しているので、crates.io のトークンをリポジトリシークレットに置かずに済むのが便利でした。

この一式を組むまでに CI/CD の試行錯誤が地味に多く、特に release-please と cargo-dist のどちらに GitHub Release の所有権を持たせるかに関しては少々苦労しました。

これから

数週間このツールを使ってみて、使い心地に関してはわりと満足しています。

直近の課題としては、ローカルテスト実行時のパフォーマンス改善あたりを考えています。
もし要望や issue がありましたら GitHub リポジトリ までお願いします。

おわりに

acr は「コンテストに集中するための環境を整える」ことを目的に作った小さなツールですが、
Cloudflare 越しにどう対処するか、Rust 製 CLI をどう配るか、といった副産物の学びが多くて楽しかったです。
Rust で競プロやっている方、もしよかったら試してみていただけると嬉しいです。

私も AtCoderのレーティング上げ頑張ります。

curl で AWS の SigV4 署名ができるようになったらしいのでやってみた

  • きっかけ
  • HTTP API の作成
  • curl でアクセスする
  • --aws-sigv4 オプションについて

きっかけ

Tori さんのツイートを見て試してみたくなったのでやってみます。
HTTP API に IAM 認証を設定し、curl でアクセスしてみます。

続きを読む

Amazon Cognito で複数の Idp を使って1人のユーザーを認証する

課題

例えば「Googleでサインイン」を併用している場合、
特に対策を行わない状態では、
同じメールアドレスでも Cognito UserPool のユーザーと Google のユーザーが別々に作成されます。

f:id:aba_0921:20210620001511p:plain

f:id:aba_0921:20210620001939p:plain

こんな時に、
同じメールアドレスのユーザーが統合され、
1人のユーザーが複数の認証方法を使ってサインイン出来るようにする方法についてメモします。

続きを読む

ソースコードが読みにくい理由

GWも折返し地点に差し掛かりましたが、皆さんいかがお過ごしでしょうか。

やや話題の「ユニコーン企業のひみつ」を読んで
組織やビジョナリー・カンパニー的な仕事のモチベーションに想いを馳せつつ、
GWの前半は手元のコードを眺めていました。

プロジェクトで決められたことしかやらないうちに
負債が溜まったコードとはまさにこのこと。

今日は、いろいろと弄っているうちに感じた
「このコードが読みにくい理由」
を書き留めておきます。

読みやすさには個人差があるのと、
C# 以外のケースでは必ずしも当てはまらない内容かもしれません。

続きを読む

kubeadm & containerd on Vagrant(VirtualBox)

kubeadm を使った kubernetes クラスターの作成を行います。
dockershim が非推奨になることもあり、今回は containerd を使ってみたいと思います。

続きを読む