TECH PLAY

株式会社モバイルファクトリー

株式会社モバイルファクトリー の技術ブログ

226

こんにちは、駅メモ!開発チームエンジニアの id:hayayanai です! 最近、 VoidZero から Vite+ がリリースされました。 Vite+ は Vite 8、Vitest、Oxlint、Oxfmt などを統合した「Web のための統合ツールチェーン」です。 駅メモ!は現在 Vue + Vite 7 + Vitest + ESLint(チーム独自ルール有り)+ Prettier + Stylelint で開発されており、Vite+ のツールはまだ導入していません。 AI で開発が高速化した現代、数十倍速いと謳う Vite+ のツールチェーンは気になります。 ただ、そもそも各ツールがどれくらい Vue 対応しているのか、どう設定すれば良いのかがわからなかったので、テンプレを使って確認することにしました。 Vite+ 経由の vp create vue と従来の pnpm create vue@latest で生成されるプロジェクト設定を比較し、Vue プロジェクト特有の注意点を整理します。 検証環境 プロジェクトの作成 Vite+ 経由 従来の create-vue 生成される設定ファイルの比較 Vite+ プロジェクトの構成 従来の create-vue プロジェクトの構成 共通: eslint.config.ts 共通: Lint 実行順 Linter 比較: Oxlint vs ESLint 検証用コンポーネント Oxlint の結果 ESLint の結果 CSS/Style Lint について 型チェックの違い テスト Formatter 比較: Oxfmt vs Prettier Vue SFC のフォーマット対応 全体比較表 まとめ Linter 型チェック Formatter CSS Lint 検証環境 vp v0.1.16 (Vite+) create-vue v3.22.2 (pnpm create vue@latest) Node.js v24.14.1 pnpm v10.33.0 プロジェクトの作成 Vite+ 経由 vp create vue vue-viteplus ◇ Which package manager would you like to use? pnpm ◇ pnpm@10.33.0 installed ◇ Which agents are you using? Claude Code ◇ Which editor are you using? VSCode ◇ Set up pre-commit hooks to run formatting, linting, and type checking with auto-fixes? Yes Generating project… Running: pnpm dlx create-vue ┌ Vue.js - The Progressive JavaScript Framework │ ◇ Project name (target directory): │ vue-viteplus │ ◇ Use TypeScript? │ Yes │ ◇ Select features to include in your project: │ Vitest (unit testing), Linter (error prevention), Prettier (code formatting) │ ◇ Select experimental features to include in your project: │ Replace Prettier with Oxfmt │ ◇ Skip all example code and start with a blank Vue project? │ No Scaffolding project in /Users/yanai/project/vue-viteplus... │ └ Done. ✔ Merged vue-viteplus/.oxlintrc.json into vue-viteplus/vite.config.ts ✔ Merged vue-viteplus/.oxfmtrc.json into vue-viteplus/vite.config.ts Wrote agent instructions to CLAUDE.md Rewrote imports in 4 files ✔ Merged staged config into vue-viteplus/vite.config.ts ◇ Dependencies installed ◇ Code formatted ◇ Scaffolded vue-viteplus 出力を見ると Running: pnpm dlx create-vue とあり、内部で create-vue を呼んでいることが分かります。create-vue でプロジェクトを生成した後に、Vite+ が以下の変換をかけています。 .oxlintrc.json → vite.config.ts の lint ブロックにマージ .oxfmtrc.json → vite.config.ts の fmt ブロックにマージ vite / vitest の import パスを vite-plus に書き換え pre-commit フック( vp staged )の設定を統合 つまり vp create vue は create-vue のラッパーで、生成物を Vite+ 向けに変換しているだけのようです。 従来の create-vue pnpm create vue@latest vue-create-vue ┌ Vue.js - The Progressive JavaScript Framework │ ◇ Use TypeScript? │ Yes │ ◇ Select features to include in your project: │ Vitest (unit testing), Linter (error prevention), Prettier (code formatting) │ ◇ Select experimental features to include in your project: │ none │ ◇ Skip all example code and start with a blank Vue project? │ No Scaffolding project in /Users/yanai/project/vue-create-vue... │ └ Done. こちらは従来通りのシンプルな Scaffold です。create-vue 側でも「Replace Prettier with Oxfmt」の選択肢が出ますが、今回は Oxfmt との比較のため Prettier を選びました。 生成される設定ファイルの比較 以降のコードブロックは主要部分の抜粋です。 Vite+ プロジェクトの構成 package.json { " scripts ": { " dev ": " vp dev ", " build ": " run-p type-check \" build-only {@} \" -- ", " build-only ": " vp build ", " type-check ": " vue-tsc --build ", " test:unit ": " vp test ", " lint ": " run-s lint:* ", " lint:oxlint ": " vp lint . --fix ", " lint:eslint ": " eslint . --fix --cache ", " format ": " vp fmt src/ " } , " devDependencies ": { " eslint ": " ^10.1.0 ", " eslint-plugin-vue ": " ~10.8.0 ", " eslint-plugin-oxlint ": " ~1.57.0 ", " eslint-config-prettier ": " ^10.1.8 ", " vite ": " catalog: ", " vite-plus ": " catalog: ", " vitest ": " catalog: " } } vite.config.ts: import { defineConfig } from "vite-plus" import vue from "@vitejs/plugin-vue" export default defineConfig( { staged : { "*" : "vp check --fix" , } , fmt : { semi : false , singleQuote : true , } , lint : { plugins : [ "eslint" , "typescript" , "unicorn" , "oxc" , "vue" , "vitest" ] , env : { browser : true } , categories : { correctness : "error" } , options : { typeAware : true , typeCheck : true } , } , plugins : [ vue() ] , } ) defineConfig を 'vite-plus' からインポートしていて、Vite の設定に加え lint (Oxlint)、 fmt (Oxfmt)、 staged (pre-commit フック)の設定が1つのファイルにまとまっています。 vite と vitest は、 pnpm-workspace.yaml の catalog: により @voidzero-dev のものに解決されています。 従来の create-vue プロジェクトの構成 package.json { " scripts ": { " dev ": " vite ", " build ": " run-p type-check \" build-only {@} \" -- ", " build-only ": " vite build ", " type-check ": " vue-tsc --build ", " test:unit ": " vitest ", " lint ": " run-s lint:* ", " lint:oxlint ": " oxlint . --fix ", " lint:eslint ": " eslint . --fix --cache ", " format ": " prettier --write --experimental-cli src/ " } , " devDependencies ": { " eslint ": " ^10.1.0 ", " eslint-plugin-vue ": " ~10.8.0 ", " eslint-plugin-oxlint ": " ~1.57.0 ", " eslint-config-prettier ": " ^10.1.8 ", " oxlint ": " ~1.57.0 ", " prettier ": " 3.8.1 ", " vite ": " ^8.0.3 ", " vitest ": " ^4.1.2 " } } .oxlintrc.json: { " plugins ": [ " eslint ", " typescript ", " unicorn ", " oxc ", " vue ", " vitest " ] , " env ": { " browser ": true } , " categories ": { " correctness ": " error " } } .prettierrc.json: { " $schema ": " https://json.schemastore.org/prettierrc ", " semi ": false , " singleQuote ": true , " printWidth ": 100 } 共通: eslint.config.ts 前述の通り vp create vue は内部で create-vue を実行しているため、eslint.config.ts は両プロジェクトで同一です。 import { defineConfigWithVueTs, vueTsConfigs, } from "@vue/eslint-config-typescript" import pluginVue from "eslint-plugin-vue" import pluginVitest from "@vitest/eslint-plugin" import pluginOxlint from "eslint-plugin-oxlint" import skipFormatting from "eslint-config-prettier/flat" export default defineConfigWithVueTs( { name : "app/files-to-lint" , files : [ "**/*.{vue,ts,mts,tsx}" ] } , ...pluginVue.configs[ "flat/essential" ], vueTsConfigs.recommended, { ...pluginVitest.configs.recommended, files : [ "src/**/__tests__/*" ] } , ...pluginOxlint.buildFromOxlintConfigFile( ".oxlintrc.json" ), skipFormatting ) ただし、Vite+ プロジェクトではこの eslint.config.ts に落とし穴があります。 Vite+ の公式ガイド では .oxlintrc.json の使用は推奨されておらず、 vite.config.ts の lint ブロックへ設定を集約する方針です。実際、 vp create vue で .oxlintrc.json は vite.config.ts へマージされた後に削除されています。 しかし eslint.config.ts の buildFromOxlintConfigFile(".oxlintrc.json") はそのまま残っています。存在しないファイルを参照すると eslint-plugin-oxlint: could not find oxlint config file: .oxlintrc.json と警告が出て空配列を返すため、ルール重複の無効化が効きません。 つまり、Oxlint と ESLint で同じ違反が重複報告される状態になります。 回避策は2つあります。 1つ目は vite.config.ts から lint ブロックを直接インポートする方法です。 eslint.config.ts は TypeScript ですから、 vite.config.ts の default export から .lint を取り出して buildFromOxlintConfig に渡せます。 // eslint.config.ts import viteConfig from './vite.config' // 変更前: ファイルが存在しないため機能しない ...pluginOxlint.buildFromOxlintConfigFile( ".oxlintrc.json" ), // 変更後: vite.config.ts の lint ブロックをそのまま渡す ...pluginOxlint.buildFromOxlintConfig(viteConfig.lint), 2つ目は vite.config.ts の lint ブロックを削除し、 .oxlintrc.json に設定を一本化する方法です。 vite-plus の issue によると、現状の実装では .oxlintrc.json 等の専用設定ファイルが優先され、 vite.config.ts はフォールバックとして使われます。 .oxlintrc.json があればそちらが読み込まれます。 { " plugins ": [ " eslint ", " typescript ", " unicorn ", " oxc ", " vue ", " vitest " ] , " env ": { " browser ": true } , " categories ": { " correctness ": " error " } , " options ": { " typeAware ": true , " typeCheck ": true } } eslint.config.ts の修正が不要で済みますが、Vite+ の「 vite.config.ts に集約する」方針とは外れます。 共通: Lint 実行順 2026年4月時点で、create-vue は Oxlint をデフォルトで同梱しています。前述の package.json にある通り、 pnpm lint ( run-s lint:* )で lint:oxlint → lint:eslint の順に直列実行されます。 create-vue 側では eslint-plugin-oxlint が .oxlintrc.json を読み取り、Oxlint と重複する ESLint ルールを自動で無効化してくれます。 # Vite+ vp run lint # → vp lint . --fix ... Oxlint(vp経由) # → eslint . --fix --cache ... ESLint(直接呼び出し) # create-vue pnpm lint # → oxlint . --fix ... Oxlint(直接呼び出し) # → eslint . --fix --cache ... ESLint(直接呼び出し) vp lint は Oxlint だけを実行する組み込みコマンドで、ESLint は Vite+ に統合されていません。 そのため、テンプレートでは ESLint を eslint コマンドで直接呼ぶ構成になっています。 Vite+ のタスクランナーを活用したい場合は、 vite.config.ts の run.tasks に定義を移行すると良さそうです。 run.tasks で定義したタスクはデフォルトでキャッシュが有効なため、入力ファイルに変更がなければ再実行がスキップされます。 なお、 run.tasks のタスク名は package.json の scripts と重複できないため、移行する場合は package.json 側の lint スクリプトを削除します。 // package.json の lint 関連スクリプトを run.tasks に移行する例 run: { tasks: { lint: { command: 'vp lint . --fix && eslint . --fix --cache' , input: [{ auto : true } , '!.eslintcache' ] , } , } , } , eslint の --cache を使うと .eslintcache が書き出され、 vp run がそれを入力の変更と見なしてタスクキャッシュがヒットしません。 input で '!.eslintcache' を指定し、キャッシュファイルを変更検知の対象外にすることで併用できます。 キャッシュ機能については ESLint ではなくタスクランナー側のもので十分かもしれませんが、 --cache の有無による差異は今回未検証です。 Linter 比較: Oxlint vs ESLint 検証用コンポーネント 検証用に、意図的に Lint 違反を仕込んだ Vue コンポーネントを用意しました。 < script setup lang = "ts" > import { ref } from "vue" // unused expression (correctness) const x = 1 x // prefer-as-const (typescript) let y = "hello" as "hello" const items = ref ([ { id : 1 , name : "Apple" } , { id : 2 , name : "Banana" } , ]) </ script > < template > <!-- v-for without :key --> < li v- for = "item in items" > {{ item.name }} </ li > <!-- v-if and v-for on same element --> < div v- for = "item in items" v-if= "item.id > 0" :key= "item.id" > {{ item.name }} </ div > </ template > < style scoped> .unused-class { color : redd; } </ style > Oxlint の結果 # Vite+ vp lint src/components/LintTest.vue x eslint(no-unused-expressions): Expected expression to be used , - [src/components/LintTest.vue: 6 : 1 ] 5 | const x = 1 6 | x : ^ `---- x typescript-eslint(prefer-as-const): Expected a ` const ` assertion instead of a literal type annotation. , - [src/components/LintTest.vue: 9 : 20 ] 8 | // prefer-as-const (typescript) 9 | let y = 'hello' as 'hello' : ^^^^^^^ ` ---- Found 0 warnings and 2 errors. Finished in 369ms on 1 file with 132 rules using 10 threads. # create-vue pnpm exec oxlint -c .oxlintrc.json src/components/LintTest.vue ...(同一の 2 件) Found 0 warnings and 2 errors. Finished in 24ms on 1 file with 116 rules using 10 threads. Oxlint は <script> 内の違反を検出しましたが、 <template> / <style> の問題はスルーされています。Oxlint は .vue ファイルの <script> ブロックしか Lint しないためです。 oxc の互換性ページ にもある通り、Vue/Svelte/Astro 等のフレームワークでは script ブロックのみが対象です。 vue プラグインを有効にしても、script ブロック内の Vue 関連ルール(ref の使い方など)しか動きません。 SFC テンプレートの Lint 対応は oxc#15761 で追跡されていますが、まだ実装されていません。 また、Oxlint には ESLint の JS プラグインを読み込む JS plugins 機能もありますが、eslint-plugin-vue は動きません。 JS plugins の 制限事項 に「Custom file formats and parsers (e.g. Svelte, Vue, Angular)」は未対応と明記されています。 eslint-plugin-vue はカスタムパーサー( vue-eslint-parser )で .vue ファイル全体をパースしてテンプレートの AST をルールに渡す仕組みのため、この制限に該当しています。 ルール数の差(132 vs 116)は、 typeAware: true で型情報を使ったチェック(floating promise の検出等)が追加されるためです。 なお、Vite+ テンプレートの typeCheck: true は Vue プロジェクトでは実質的に使えないようです。 vp lint src/ のようにディレクトリを指定すると .ts ファイルもチェック対象になります。 しかし、 .ts から .vue をインポートしている箇所で tsgo がモジュール解決に失敗し TS2307: Cannot find module エラーが出ます。 上の検証でファイルを直接指定しているのはこの問題を回避するためです。 ESLint の結果 # Vite+(eslint-plugin-oxlint が機能していない) vp exec eslint src/components/LintTest.vue src/components/LintTest.vue 6 : 1 error Expected an assignment or function call and instead saw an expression @typescript-eslint/no-unused-expressions 9 : 5 error 'y' is never reassigned. Use 'const' instead prefer-const 9 : 5 error 'y' is assigned a value but never used @typescript-eslint/no-unused-vars 9 : 20 error Expected a `const` instead of a literal type assertion @typescript-eslint/prefer-as-const 19 : 3 error Elements in iteration expect to have 'v-bind:key' directives vue/require-v-for-key 22 : 30 error The 'items' variable inside 'v-for' directive should be replaced with a computed property that returns filtered array instead. You should not mix 'v-for' with 'v-if' vue/no-use-v-if-with-v-for ✖ 6 problems ( 6 errors, 0 warnings) # create-vue(eslint-plugin-oxlint が正常動作) pnpm exec eslint src/components/LintTest.vue src/components/LintTest.vue 9 : 5 error 'y' is never reassigned. Use 'const' instead prefer-const 9 : 5 error 'y' is assigned a value but never used @typescript-eslint/no-unused-vars 19 : 3 error Elements in iteration expect to have 'v-bind:key' directives vue/require-v-for-key 22 : 30 error The 'items' variable inside 'v-for' directive should be replaced with a computed property that returns filtered array instead. You should not mix 'v-for' with 'v-if' vue/no-use-v-if-with-v-for ✖ 4 problems ( 4 errors, 0 warnings) Vite+ 側は Oxlint で検出されている no-unused-expressions と prefer-as-const も ESLint から報告されて6件。前述の通りルール重複の無効化が効いていません。 create-vue 側は eslint-plugin-oxlint が正常動作し、Oxlint との重複ルールが ESLint 側で無効化されるため4件。 どちらも、 eslint-plugin-vue により <template> 内の Vue 固有の問題も検出されています。 CSS/Style Lint について 検証用コンポーネントの <style> に color: redd; というタイポを仕込みましたが、Oxlint でも ESLint でも引っかかりませんでした。 どちらのテンプレートも CSS の Lint は対象外のようです。 Vue 公式のツーリングガイドの Linting セクション でも案内されているのは eslint-plugin-vue による JavaScript/テンプレートの Lint だけで、CSS/Style の Lint には触れていません。 CSS の Lint が必要なら、これまで同様 Stylelint と Vue プラグインを別途入れることになりそうです。 型チェックの違い pnpm type-check はどちらも vue-tsc --build で同じです。 Vite+ 側は vite.config.ts に typeAware: true (型認識 Lint ルールの有効化)と typeCheck: true (tsgo 経由の型チェック同時実行)の設定があります。 ただし前述の通り tsgo は .vue を読めないため、 .vue の型チェックには引き続き vue-tsc が必要です。 テスト どちらも Vitest です。 Vite+ ではインポートパスが 'vitest' から 'vite-plus/test' に、コマンドが vitest から vp test に変わりますが、設定内容やテストの書き方は同じです。 Formatter 比較: Oxfmt vs Prettier Oxfmt は Prettier との出力互換を謳っており、JavaScript/TypeScript の conformance test を 100% パスしています。 Vue SFC のフォーマット対応 <template> と <style> もフォーマットできるのか気になったため、わざと崩した Vue ファイルで試しました。 <!-- フォーマット前 --> < template >< div class = "foo" >< p v-if= "true" > hello </ p ></ div ></ template > < style scoped>.foo{ color : red ; font-size : 16px ; display : flex ; justify-content : center }</ style > <!-- フォーマット後(Oxfmt / Prettier どちらも同一の結果) --> < template > < div class = "foo" >< p v-if= "true" > hello </ p ></ div > </ template > < style scoped> .foo { color : red ; font-size : 16px ; display : flex ; justify-content : center ; } </ style > Oxfmt でも Prettier でも <template> と <style> をフォーマットでき、出力結果は同一でした。乗り換えて問題なさそうです。 公式ドキュメント によると Prettier の約30倍の速度とのこと。小規模プロジェクトだと体感差はありませんが、大規模プロジェクトでは差が出そうです。 全体比較表 項目 Vite+ ( vp create vue ) create-vue ( pnpm create vue@latest ) Linter Oxlint ( vp lint ) + ESLint Oxlint + ESLint ESLint 設定 同一 (ただし eslint-plugin-oxlint の修正が必要) 同一 Vue template lint ESLint 経由で対応 ESLint 経由で対応 CSS lint なし なし typeCheck tsgo が .vue を読めず実質使えない なし テスト Vitest ( vp test ) Vitest Formatter Oxfmt ( vp fmt ) Prettier (Oxfmtも案内される) ビルド Vite ( vp build ) Vite ( vite build ) 設定の統合 vite.config.ts に集約 個別ファイル ( .oxlintrc.json , .prettierrc.json ) Pre-commit フック .vite-hooks/pre-commit → vp staged なし (要別途設定) まとめ vp create vue と pnpm create vue@latest で生成されるプロジェクトを比較した結果をまとめます。 Linter Oxlint は .vue の <script> ブロックしか Lint できない <template> や <style> は対象外 Vue template の Lint には引き続き ESLint(eslint-plugin-vue)が必要 これは Vite+ でも create-vue でも同じ create-vue は Oxlint をデフォルトで同梱している ESLint 連携の落とし穴 Vite+ テンプレートでは .oxlintrc.json がマージ後に削除されることへの対応が無く、ルール重複の無効化が壊れる vite.config.ts の lint ブロックを import するか、 .oxlintrc.json に一本化することで対処できる Lint 実行 どちらも Oxlint → ESLint の直列実行 型チェック typeCheck: true は Vue プロジェクトでは実質使えない tsgo が .vue をインポートした .ts ファイルで TS2307 エラーを出すため .vue の型チェックには引き続き vue-tsc が必要 Formatter Oxfmt は Prettier と同一の出力 <template> / <style> もフォーマットできる Prettier からの乗り換えで困ることはなさそう CSS Lint どちらのテンプレートも対象外 必要なら Stylelint を別途入れることになる Vite+ を選ぶメリットは Linter/Formatter 単体の差分よりも、 vite.config.ts への設定一元化と vp コマンドによる統合にありそうでした。 Vue 固有の Lint や型チェックについてはまだ ESLint + vue-tsc 頼りなため、Oxlint の Vue テンプレート対応や tsgo の .vue サポートが整えば、設定が楽になりそうです。 今回はテンプレートの設定比較だけでしたが、気になるのはやはり実際のプロジェクトでの速度差です。 次回は Vue ファイルが約2000個存在する駅メモ!のフロントエンドで、Lint/Format/ビルドがどれくらい速くなるか実測してみます。お楽しみに!
こんにちは、モバファクエンジニアの id:knj-mf です。 今回は TypeScript の型レベルプログラミングでちょっと面白いものを作ったので紹介したいと思います。 何を作ったの? TypeScript の型レベルプログラミングは、予想に反して様々なものが実装できてしまうことで有名だったりします。 type-challenges のように、「これは普通のプログラミングで実装するものでは?」と思ってしまうようなものまで実装できてしまいます。そこで、作ってみたものが下記になります。 早速、動作を紹介します。このような Brainf**k プログラムの文字列型が… このように、型計算上で解釈されてしまう!というものです。 ある程度の形になるものはできたので、この記事では、型レベルプログラミングと書き味の近い (個人差があります) Haskell 実装と照らしながら、どのように考えてこの「型」を実装していったのかを紹介します。 cwd-k2/bf-in-type のリポジトリ に実装があるので、気になる方は手元で動作や実装を見てみてください。 Brainf**k? たった 8 つの命令からなる難読プログラミング言語です。言語の仕様としてかなり単純明快ではありますが、チューリング完全として知られています。(ちょっと企業の公式ブログには載せづらい表記を含むので、今回は ** という風に伏せさせていただきます…) 要素として、次の 4 つのものを持ちます。 要素 内容 プログラムテープ 実行するプログラム列 メモリテープ 値を記録するセルの列 プログラムポインタ 現在参照しているプログラム命令列上の位置 メモリポインタ 現在参照しているメモリテープの位置 8 つの命令は次のような単純なものです。 命令 内容 > メモリポインタをインクリメント(次のセルへ) < メモリポインタをデクリメント(前のセルへ) + 現在セルの値をインクリメント - 現在セルの値をデクリメント . 現在セルの値を ASCII 文字として出力 , 1 バイト読み込み、現在セルへ格納 [ 現在セルが 0 なら、対応する ] の直後へジャンプ ] 現在セルが 0 でなければ、対応する [ の直後へジャンプ ざっくり、プログラムテープ上に記載された 8 つの命令の列を順次実行しながらメモリテープの値を書き換えつつ、適宜 I/O していく形のプログラミング言語になります。 実際の Brainf**k プログラムそのものはまったく実用性がないのですが、この簡単な命令セットからなる言語処理系の実装には教育的価値があります。結構書いてみたことがあるというエンジニアの方も多いのではないでしょうか。 TypeScript の型レベルプログラミング ところで、TypeScript には (TypeScript に限りませんが) 型レベルプログラミングがあります。本当に単純な例だと、下記のようなものです。 type ExtendsObject < T > = T extends object ? true : false これが何をしているのかというと、型チェックの際に実施される型計算を実装しているということです。上記のような条件分岐などのロジックが型レベルで解決されてしまうということですね。 この型レベルプログラミングなのですが、表現力はさておき、チューリング完全な系になってしまっているとのもっぱらの評判です。 型から型を新たに計算できてしまうということは… 楽しいプログラミングの時間の始まりですね。 実装方針 長くなってしまうので、以降では Brainf**k を BF と記載することにします。 BF 処理系を型レベルに落とし込むにあたって、次の 4 つの要素に分けて考えます。 テープ構造体 ( Tape ) — メモリ・プログラムを共通して表現するデータ構造 現在位置を持ちつつ、前後に移動する能力を持つ 評価器 ( Runner ) — メモリテープとプログラムテープを束ねた実行状態 メモリを変化させつつプログラムポインタを移動するため、同時に扱う アクション ( Action ) — 1 ステップ実行の結果として外界に要求する効果 (なにもしない / 入力 / 出力 / 終了) 評価ループ ( Exec ) — アクションを解釈して評価器を回し、入力を消費しつつ出力を蓄積するメインループ 型レベルプログラミングでは副作用を素直に書けないため、入出力を「アクション型」としてデータに落としておき、外側のループでそれを解釈する形にしたのがポイントです。以降、この順で各要素の実装を見ていきます。 また、適宜参考実装として Haskell の実装も合わせて示しています。 TypeScript 実装は v5.4 以降で動作確認しています。 実装上の制約 制約として、実装レベルに効いてくるものもあります。数値での演算や数値⇔文字の変換が基本的にできない、というものです。不可能ではないですが、タプル (型レベル配列) の length を取るような実装になりがちなのでまわりくどくなります。 今回は ASCII 範囲でインクリメント・デクリメントを考えるだけなので、気合いで誤魔化すことができます。 NumToCharMap[65] のように参照すると 'A' という型に解決される、というマップを定義しました。 数値文字変換、インクリメント・デクリメントマップの実装 export type NumToCharMap = [ '\x00' , '\x01' , '\x02' , '\x03' , '\x04' , '\x05' , '\x06' , '\x07' , '\x08' , '\x09' , '\x0A' , '\x0B' , '\x0C' , '\x0D' , '\x0E' , '\x0F' , '\x10' , '\x11' , '\x12' , '\x13' , '\x14' , '\x15' , '\x16' , '\x17' , '\x18' , '\x19' , '\x1A' , '\x1B' , '\x1C' , '\x1D' , '\x1E' , '\x1F' , '\x20' , '\x21' , '\x22' , '\x23' , '\x24' , '\x25' , '\x26' , '\x27' , '\x28' , '\x29' , '\x2A' , '\x2B' , '\x2C' , '\x2D' , '\x2E' , '\x2F' , '\x30' , '\x31' , '\x32' , '\x33' , '\x34' , '\x35' , '\x36' , '\x37' , '\x38' , '\x39' , '\x3A' , '\x3B' , '\x3C' , '\x3D' , '\x3E' , '\x3F' , '\x40' , '\x41' , '\x42' , '\x43' , '\x44' , '\x45' , '\x46' , '\x47' , '\x48' , '\x49' , '\x4A' , '\x4B' , '\x4C' , '\x4D' , '\x4E' , '\x4F' , '\x50' , '\x51' , '\x52' , '\x53' , '\x54' , '\x55' , '\x56' , '\x57' , '\x58' , '\x59' , '\x5A' , '\x5B' , '\x5C' , '\x5D' , '\x5E' , '\x5F' , '\x60' , '\x61' , '\x62' , '\x63' , '\x64' , '\x65' , '\x66' , '\x67' , '\x68' , '\x69' , '\x6A' , '\x6B' , '\x6C' , '\x6D' , '\x6E' , '\x6F' , '\x70' , '\x71' , '\x72' , '\x73' , '\x74' , '\x75' , '\x76' , '\x77' , '\x78' , '\x79' , '\x7A' , '\x7B' , '\x7C' , '\x7D' , '\x7E' , '\x7F' , ] & { [ i: number ] : ' \x00 ' } ; export type CharToNumMap = { '\x00' : 0 x00 , '\x01' : 0 x01 , '\x02' : 0 x02 , '\x03' : 0 x03 , '\x04' : 0 x04 , '\x05' : 0 x05 , '\x06' : 0 x06 , '\x07' : 0 x07 , '\x08' : 0 x08 , '\x09' : 0 x09 , '\x0A' : 0 x0A , '\x0B' : 0 x0B , '\x0C' : 0 x0C , '\x0D' : 0 x0D , '\x0E' : 0 x0E , '\x0F' : 0 x0F , '\x10' : 0 x10 , '\x11' : 0 x11 , '\x12' : 0 x12 , '\x13' : 0 x13 , '\x14' : 0 x14 , '\x15' : 0 x15 , '\x16' : 0 x16 , '\x17' : 0 x17 , '\x18' : 0 x18 , '\x19' : 0 x19 , '\x1A' : 0 x1A , '\x1B' : 0 x1B , '\x1C' : 0 x1C , '\x1D' : 0 x1D , '\x1E' : 0 x1E , '\x1F' : 0 x1F , '\x20' : 0 x20 , '\x21' : 0 x21 , '\x22' : 0 x22 , '\x23' : 0 x23 , '\x24' : 0 x24 , '\x25' : 0 x25 , '\x26' : 0 x26 , '\x27' : 0 x27 , '\x28' : 0 x28 , '\x29' : 0 x29 , '\x2A' : 0 x2A , '\x2B' : 0 x2B , '\x2C' : 0 x2C , '\x2D' : 0 x2D , '\x2E' : 0 x2E , '\x2F' : 0 x2F , '\x30' : 0 x30 , '\x31' : 0 x31 , '\x32' : 0 x32 , '\x33' : 0 x33 , '\x34' : 0 x34 , '\x35' : 0 x35 , '\x36' : 0 x36 , '\x37' : 0 x37 , '\x38' : 0 x38 , '\x39' : 0 x39 , '\x3A' : 0 x3A , '\x3B' : 0 x3B , '\x3C' : 0 x3C , '\x3D' : 0 x3D , '\x3E' : 0 x3E , '\x3F' : 0 x3F , '\x40' : 0 x40 , '\x41' : 0 x41 , '\x42' : 0 x42 , '\x43' : 0 x43 , '\x44' : 0 x44 , '\x45' : 0 x45 , '\x46' : 0 x46 , '\x47' : 0 x47 , '\x48' : 0 x48 , '\x49' : 0 x49 , '\x4A' : 0 x4A , '\x4B' : 0 x4B , '\x4C' : 0 x4C , '\x4D' : 0 x4D , '\x4E' : 0 x4E , '\x4F' : 0 x4F , '\x50' : 0 x50 , '\x51' : 0 x51 , '\x52' : 0 x52 , '\x53' : 0 x53 , '\x54' : 0 x54 , '\x55' : 0 x55 , '\x56' : 0 x56 , '\x57' : 0 x57 , '\x58' : 0 x58 , '\x59' : 0 x59 , '\x5A' : 0 x5A , '\x5B' : 0 x5B , '\x5C' : 0 x5C , '\x5D' : 0 x5D , '\x5E' : 0 x5E , '\x5F' : 0 x5F , '\x60' : 0 x60 , '\x61' : 0 x61 , '\x62' : 0 x62 , '\x63' : 0 x63 , '\x64' : 0 x64 , '\x65' : 0 x65 , '\x66' : 0 x66 , '\x67' : 0 x67 , '\x68' : 0 x68 , '\x69' : 0 x69 , '\x6A' : 0 x6A , '\x6B' : 0 x6B , '\x6C' : 0 x6C , '\x6D' : 0 x6D , '\x6E' : 0 x6E , '\x6F' : 0 x6F , '\x70' : 0 x70 , '\x71' : 0 x71 , '\x72' : 0 x72 , '\x73' : 0 x73 , '\x74' : 0 x74 , '\x75' : 0 x75 , '\x76' : 0 x76 , '\x77' : 0 x77 , '\x78' : 0 x78 , '\x79' : 0 x79 , '\x7A' : 0 x7A , '\x7B' : 0 x7B , '\x7C' : 0 x7C , '\x7D' : 0 x7D , '\x7E' : 0 x7E , '\x7F' : 0 x7F , } & { [ k : string ]: 0 x00 ; } ; export type DecrementMap = [ 0 x7F , 0 x00 , 0 x01 , 0 x02 , 0 x03 , 0 x04 , 0 x05 , 0 x06 , 0 x07 , 0 x08 , 0 x09 , 0 x0A , 0 x0B , 0 x0C , 0 x0D , 0 x0E , 0 x0F , 0 x10 , 0 x11 , 0 x12 , 0 x13 , 0 x14 , 0 x15 , 0 x16 , 0 x17 , 0 x18 , 0 x19 , 0 x1A , 0 x1B , 0 x1C , 0 x1D , 0 x1E , 0 x1F , 0 x20 , 0 x21 , 0 x22 , 0 x23 , 0 x24 , 0 x25 , 0 x26 , 0 x27 , 0 x28 , 0 x29 , 0 x2A , 0 x2B , 0 x2C , 0 x2D , 0 x2E , 0 x2F , 0 x30 , 0 x31 , 0 x32 , 0 x33 , 0 x34 , 0 x35 , 0 x36 , 0 x37 , 0 x38 , 0 x39 , 0 x3A , 0 x3B , 0 x3C , 0 x3D , 0 x3E , 0 x3F , 0 x40 , 0 x41 , 0 x42 , 0 x43 , 0 x44 , 0 x45 , 0 x46 , 0 x47 , 0 x48 , 0 x49 , 0 x4A , 0 x4B , 0 x4C , 0 x4D , 0 x4E , 0 x4F , 0 x50 , 0 x51 , 0 x52 , 0 x53 , 0 x54 , 0 x55 , 0 x56 , 0 x57 , 0 x58 , 0 x59 , 0 x5A , 0 x5B , 0 x5C , 0 x5D , 0 x5E , 0 x5F , 0 x60 , 0 x61 , 0 x62 , 0 x63 , 0 x64 , 0 x65 , 0 x66 , 0 x67 , 0 x68 , 0 x69 , 0 x6A , 0 x6B , 0 x6C , 0 x6D , 0 x6E , 0 x6F , 0 x70 , 0 x71 , 0 x72 , 0 x73 , 0 x74 , 0 x75 , 0 x76 , 0 x77 , 0 x78 , 0 x79 , 0 x7A , 0 x7B , 0 x7C , 0 x7D , 0 x7E , ] & { [ i: number ] : 0x7F ; } ; export type IncrementMap = [ 0 x01 , 0 x02 , 0 x03 , 0 x04 , 0 x05 , 0 x06 , 0 x07 , 0 x08 , 0 x09 , 0 x0A , 0 x0B , 0 x0C , 0 x0D , 0 x0E , 0 x0F , 0 x10 , 0 x11 , 0 x12 , 0 x13 , 0 x14 , 0 x15 , 0 x16 , 0 x17 , 0 x18 , 0 x19 , 0 x1A , 0 x1B , 0 x1C , 0 x1D , 0 x1E , 0 x1F , 0 x20 , 0 x21 , 0 x22 , 0 x23 , 0 x24 , 0 x25 , 0 x26 , 0 x27 , 0 x28 , 0 x29 , 0 x2A , 0 x2B , 0 x2C , 0 x2D , 0 x2E , 0 x2F , 0 x30 , 0 x31 , 0 x32 , 0 x33 , 0 x34 , 0 x35 , 0 x36 , 0 x37 , 0 x38 , 0 x39 , 0 x3A , 0 x3B , 0 x3C , 0 x3D , 0 x3E , 0 x3F , 0 x40 , 0 x41 , 0 x42 , 0 x43 , 0 x44 , 0 x45 , 0 x46 , 0 x47 , 0 x48 , 0 x49 , 0 x4A , 0 x4B , 0 x4C , 0 x4D , 0 x4E , 0 x4F , 0 x50 , 0 x51 , 0 x52 , 0 x53 , 0 x54 , 0 x55 , 0 x56 , 0 x57 , 0 x58 , 0 x59 , 0 x5A , 0 x5B , 0 x5C , 0 x5D , 0 x5E , 0 x5F , 0 x60 , 0 x61 , 0 x62 , 0 x63 , 0 x64 , 0 x65 , 0 x66 , 0 x67 , 0 x68 , 0 x69 , 0 x6A , 0 x6B , 0 x6C , 0 x6D , 0 x6E , 0 x6F , 0 x70 , 0 x71 , 0 x72 , 0 x73 , 0 x74 , 0 x75 , 0 x76 , 0 x77 , 0 x78 , 0 x79 , 0 x7A , 0 x7B , 0 x7C , 0 x7D , 0 x7E , 0 x7F , 0 x00 , ] & { [ i: number ] : 0x00 ; } ; テープ構造体 BF では、メモリを用意してポインタ操作・ポインタを介した操作が前提になっています。 もちろん型レベルプログラミングで副作用は記述しにくいため、ポインタ前提となっている部分を再考し、同じ表現力の別の形に置き換える必要があります。 メモリ、プログラムを同じテープ構造で捉えます。今着目している値、その左右に列が続いている様子を考えたのが下記のような構造になります。 テープ構造体の実装 このような構造体は、Haskell での data 宣言と同じような形で、TypeScript の型ではオブジェクト型による宣言ができます。 data Tape a = Tape { prevs :: [a] , curr :: a , nexts :: [a] } extends unknown[] によって単なる配列型ではなく、各要素が独立した 型レベル配列としてのタプル を利用できます。 export type Tape < Hs extends unknown [], C , Ts extends unknown []> = { h : Hs c : C t : Ts } ここでいくつかの基本的な操作も定義してしまいましょう。 現在の値に対する操作 インクリメント・デクリメント 読み出し、書き込み テープ上の移動 着目するヘッドを左右に移動する操作 対応する [ , ] へのジャンプは繰り返しによって実現する 基本操作の実装 Tape a から新しい Tape a を作る ( Tape a -> Tape a ) という形の実装となります。 -- | 次の要素に移動 next :: Tape a -> Tape a next (Tape prevs curr (n : nexts)) = Tape (curr : prevs) n nexts -- | 前の要素に移動 prev :: Tape a -> Tape a prev (Tape (p : prevs) curr nexts) = Tape prevs p (curr : nexts) -- | 現在の要素をインクリメント incr :: Enum a => Tape a -> Tape a incr (Tape prevs curr nexts) = Tape prevs (succ curr) nexts -- | 現在の要素をデクリメント decr :: Enum a => Tape a -> Tape a decr (Tape prevs curr nexts) = Tape prevs (pred curr) nexts -- | 現在の要素を取得 get :: Tape a -> a get (Tape _ curr _) = curr -- | 現在の要素を設定 put :: a -> Tape a -> Tape a put a (Tape prevs _ nexts) = Tape prevs a nexts TypeScript の型でも同様に、 Tape を受け取って新しい Tape を作成するという方針で実装できます。 [infer H, ...infer Hs] のパターンマッチングにより、型レベル配列の要素 (head, rest) を扱うことができてしまいます。 export type Prev < M > = M extends Tape < [infer H , ... infer Hs] , infer C , infer Ts > ? Tape< Hs , H , [C , ... Ts] > : never export type Next < M > = M extends Tape < infer Hs , infer C , [infer T , ... infer Ts] > ? Tape< [C , ... Hs] , T , Ts > : never export type Incr < M > = M extends Tape < infer Hs , infer C extends number , infer Ts > ? Tape< Hs , IncrementMap [C], Ts > : never export type Decr < M > = M extends Tape < infer Hs , infer C extends number , infer Ts > ? Tape< Hs , DecrementMap [C], Ts > : never export type PutC < M , C > = M extends Tape < infer Hs , unknown , infer Ts > ? Tape < Hs , C , Ts > : never プログラム実行 基本的な構造、操作は定義してしまったので、次はインタプリタとして重要な実行について考えます。 評価器としての実行系内部 (メモリ・プログラムポインタ) と外界とのやりとりを含む効果の管理の部分を、次のような形で切り分けます。 型レベルプログラミングでは入出力をそのまま扱うことはできないので、入力待ちや出力があるということは特別な状態として表現することにします。 評価器の内部状態 こちらは至ってシンプルです。 状態はメモリ、プログラムのテープ (現在位置を保持する) から成る これを評価に通すことによって、次の実行に関する状態が出てくる data Machine = Machine { memory :: DT.Tape Int , program :: DT.Tape Char } type Runner < M , P > = { mem : M prg : P } 外部とのやりとりを含むアクション 今のメモリ・プログラムを含む、先程の構造を評価して得られるアクションです。 -- | 何もしない、入力要求、出力要求、終了の 4 つのアクションを持つ data WithAction a = ActionN { hold :: a } -- ^ 外部には何もしない | ActionI { hold :: a } -- ^ 入力要求 | ActionO { hold :: a, out :: Int } -- ^ 出力要求 | ActionE -- ^ 終了 これを型レベルプログラミングで再現すると、ADT よりは個別の型として定義してあげて、後で extends などの条件分岐してあげる方が素直になります。 type ActionN < R > = { action : "N" ; runner : R } type ActionI < R > = { action : "I" ; runner : R } type ActionO < R , O > = { action : "O" ; runner : R ; output : O } type ActionE = { action : "E" } 8 つの命令に対する操作の整理 評価器の状態とアクションを型として定義できたので、次はプログラムの示す命令を処理していく実装も考えていきます。 これは最初に確認した BF の 8 つの命令に対して、次の評価器の状態と計算の効果を含む全体を返す形で定義していけば良いです。 インクリメント デクリメント 次を参照 (ポインタインクリメント) 前を参照 (ポインタデクリメント) while (ジャンプ) while end (ジャンプバック) getchar putchar 命令→次の状態・アクション さて、図で整理できたので、実装にそのまま落としていきます。 現在の命令ポインタが指す命令に応じて、次の Action と状態を返します。 -- | 次のステップを実行し、状態とアクションを返す step :: Machine -> WithAction Machine step machine = case pc of '+' -> ActionN $ machine { memory = DT.incr (memory machine), program = DT.next (program machine) } '-' -> ActionN $ machine { memory = DT.decr (memory machine), program = DT.next (program machine) } '>' -> ActionN $ machine { memory = DT.next (memory machine), program = DT.next (program machine) } '<' -> ActionN $ machine { memory = DT.prev (memory machine), program = DT.next (program machine) } '[' -> ActionN $ machine { program = if mc == 0 then skip (program machine) else DT.next (program machine) } ']' -> ActionN $ machine { program = if mc /= 0 then back (program machine) else DT.next (program machine) } ',' -> ActionI { hold = machine { program = DT.next (program machine) } } '.' -> ActionO { hold = machine { program = DT.next (program machine) }, out = DT.get (memory machine) } _ -> ActionE where (pc, mc) = (,) <$> DT.get . program <*> DT.get . memory $ machine TypeScript で書いても、ほとんど同じ対応があります。 type Step < R > = R extends Runner < infer M extends TapeMm , infer P extends TapePg > ? P[ 'c' ] extends '+' ? ActionN< Runner < Incr < M >, Next < P >>> : P[ 'c' ] extends '-' ? ActionN< Runner < Decr < M >, Next < P >>> : P[ 'c' ] extends '>' ? ActionN< Runner < Next < M >, Next < P >>> : P[ 'c' ] extends '<' ? ActionN< Runner < Prev < M >, Next < P >>> : P[ 'c' ] extends '[' ? ActionN< Runner < M , M [ 'c' ] extends 0 ? Skip < P > : Next < P >>> : P[ 'c' ] extends ']' ? ActionN< Runner < M , M [ 'c' ] extends 0 ? Next < P > : Back < P >>> : P[ 'c' ] extends ',' ? ActionI< Runner < M , Next < P >>> : P[ 'c' ] extends '.' ? ActionO< Runner < M , Next < P >>, M [ 'c' ]> : ActionE : never; 状態・アクション→継続 次は状態、アクションを受けて、次のステップに継続していくループを実装していきます。 上記の step を実行し、その Action に応じた操作を実行していきます。 -- | 入力を消費・出力を収集しながら step を繰り返す loop :: (Machine -> WithAction Machine) -> (String, Machine) -> String loop step (input, machine) = go (step machine) where -- アクションに対応した動作を実行し、再帰に進む go (ActionN machine') = loop step (input, machine') -- そのまま次へ go (ActionI machine') = loop step (iTail, machine'') where -- 入力を消費してメモリに書き込み、次に進む (iHead : iTail) = input machine'' = machine' { memory = DT.put (fromEnum iHead) (memory machine') } go (ActionO machine' out) = toEnum out : loop step (input, machine') -- 出力を収集し、次に進む go ActionE = [] -- 終端 TypeScript の型の方では、今回は文字列の累積を保持する形で実装しています。ちょっと命名が異なってしまっていますが、やっていることは同じです。 type Exec < R , I extends string , O extends string = '' > = Step < R > extends infer WithAction ? WithAction extends ActionN< infer Q > ? Exec< Q , I , O > : WithAction extends ActionI< infer Q > ? I extends ` ${ infer F }${ infer S } ` ? Exec< Read < Q , CharToNumMap [F]>, S , O > : Exec< Read < Q , 0>, I , O > : WithAction extends ActionO< infer Q , infer N extends number > ? Exec< Q , I , ` ${ O }${ NumToCharMap [N] } ` > : WithAction extends ActionE ? O : never : never; まとめ TypeScript で Brainf**k 処理系の型レベルプログラムの実装について見ていきました。 補足として、TypeScript の型レベルプログラミング実行系には次のような制約があります。 型の再帰評価回数、つまり実行できるステップ数が制限されている Tape 構造体の保持する要素列の長さに制限がある (どちらも大体 1,000 程度のイメージ) 一方、このような制限がある中でも、冒頭に示した例のように簡単な Hello World の例までは実装できてしまいます。 みなさんもぜひ自分の型レベルプログラミングに挑戦してみてください。 私が今回示した実装も最善ではないと思います。「もっと良いものを書いてみよう」など、楽しんでみてください。 付録 Haskell のコード全文を掲載しておきます。 cwd-k2/bf-in-type のリポジトリ と比較する、または手元でテスト実行するなどしてください。 ディレクトリ構成 . ├── Data │   └── Tape.hs ├── Interpreter.hs └── Main.hs Data/Tape.hs module Data.Tape ( Tape( .. ), zeros, fromList, next, prev, incr, decr, get, put, ) where -- | テープ様構造体 -- * 前後に無限に要素があり、現在要素 (針の先にあるもの) を中心に配置している -- -- > <-prev- ... 4 5 6 <<7>> 8 9 10 ... -next-> data Tape a = Tape { prevs :: [a] , curr :: a , nexts :: [a] } deriving Show -- | ゼロ初期化された無限長のテープ zeros :: Enum a => Tape a zeros = Tape (repeat $ toEnum 0 ) (toEnum 0 ) (repeat $ toEnum 0 ) -- | リストからテープを作成 fromList :: [a] -> Tape a fromList (x : xs) = Tape [] x xs fromList [] = undefined -- 今回は特に考えずに未定義とする -- | 次の要素に移動 next :: Tape a -> Tape a next (Tape prevs curr (n : nexts)) = Tape (curr : prevs) n nexts -- | 前の要素に移動 prev :: Tape a -> Tape a prev (Tape (p : prevs) curr nexts) = Tape prevs p (curr : nexts) -- | 現在の要素をインクリメント incr :: Enum a => Tape a -> Tape a incr (Tape prevs curr nexts) = Tape prevs (succ curr) nexts -- | 現在の要素をデクリメント decr :: Enum a => Tape a -> Tape a decr (Tape prevs curr nexts) = Tape prevs (pred curr) nexts -- | 現在の要素を取得 get :: Tape a -> a get (Tape _ curr _) = curr -- | 現在の要素を設定 put :: a -> Tape a -> Tape a put a (Tape prevs _ nexts) = Tape prevs a nexts Interpreter.hs module Interpreter ( bf ) where import qualified Data.Tape as DT import Data.List (unfoldr) -- | メモリとプログラムを持つ data Machine = Machine { memory :: DT.Tape Int , program :: DT.Tape Char } deriving Show -- | 何もしない、入力要求、出力要求、終了の 4 つのアクションを持つ data WithAction a = ActionN { hold :: a } -- ^ 外部には何もしない | ActionI { hold :: a } -- ^ 入力要求 | ActionO { hold :: a, out :: Int } -- ^ 出力要求 | ActionE -- ^ 終了 deriving Show -- | 対応する @']'@ までプログラムをスキップする skip :: DT.Tape Char -> DT.Tape Char skip = skipInner 0 where skipInner n program = let program' = DT.next program in case DT.get program' of '[' -> skipInner (n + 1 ) program' ']' -> if n == 0 then program' else skipInner (n - 1 ) program' _ -> skipInner n program' -- | 対応する @'['@ までプログラムを戻す back :: DT.Tape Char -> DT.Tape Char back = backInner 0 where backInner n program = let program' = DT.prev program in case DT.get program' of ']' -> backInner (n + 1 ) program' '[' -> if n == 0 then program' else backInner (n - 1 ) program' _ -> backInner n program' -- | 次のステップを実行し、状態とアクションを返す step :: Machine -> WithAction Machine step machine = case pc of '+' -> ActionN $ machine { memory = DT.incr (memory machine), program = DT.next (program machine) } '-' -> ActionN $ machine { memory = DT.decr (memory machine), program = DT.next (program machine) } '>' -> ActionN $ machine { memory = DT.next (memory machine), program = DT.next (program machine) } '<' -> ActionN $ machine { memory = DT.prev (memory machine), program = DT.next (program machine) } '[' -> ActionN $ machine { program = if mc == 0 then skip (program machine) else DT.next (program machine) } ']' -> ActionN $ machine { program = if mc /= 0 then back (program machine) else DT.next (program machine) } ',' -> ActionI { hold = machine { program = DT.next (program machine) } } '.' -> ActionO { hold = machine { program = DT.next (program machine) }, out = DT.get (memory machine) } _ -> ActionE where (pc, mc) = (,) <$> DT.get . program <*> DT.get . memory $ machine -- | 入力を消費・出力を収集しながら step を繰り返す loop :: (Machine -> WithAction Machine) -> (String, Machine) -> String loop step (input, machine) = go (step machine) where -- アクションに対応した動作を実行し、再帰に進む go (ActionN machine') = loop step (input, machine') -- そのまま次へ go (ActionI machine') = loop step (iTail, machine'') where -- 入力を消費してメモリに書き込み、次に進む (iHead : iTail) = input machine'' = machine' { memory = DT.put (fromEnum iHead) (memory machine') } go (ActionO machine' out) = toEnum out : loop step (input, machine') -- 出力を収集し、次に進む go ActionE = [] -- 終端 -- | Bf プログラムから、入力を受け取って出力を返す関数を作る bf :: String -> [Char] -> String bf program input = loop step (input', machine) where input' = input ++ repeat ' \0 ' machine = Machine { memory = DT.zeros , program = DT.next $ DT.fromList ( "#" ++ program ++ "#" ) } Main.hs module Main where import Interpreter -- | ハローワールドする Bf プログラム helloWorld :: String helloWorld = "++++++++++[>+++++++>++++++++++>+++++++++++>+++>+++++++++>+<<<<<<-]>++.>+.>--..+++.>++.>---.<<.+++.------.<-.>>+.>>." -- | エコーする Bf プログラム echo :: String echo = "+[,.]" main :: IO () main = do let getOutputBf = bf helloWorld putStr $ getOutputBf "こんにちは \n "
皆さま、こんにちは! 駅メモ!開発チームエンジニアの id:szeto です。 今回は、毎月行う報酬配布作業について、これまで行っていた本番環境のCLIでのスクリプト実行から、管理画面(運営が駅メモ!のゲーム設定を閲覧・更新できるシステム)の操作に変更した事例を皆さまに共有したいと思います。この取り組みのおかげで、運用効率が約66%改善されました! これまでは本番サーバーにSSHでログインし、スクリプトを実行していましたが、Webブラウザから安全に、いくつかのクリックで操作できるように改善しました。この記事では、その背景、実装方法、そして得られた効果についてご紹介します。 目次 目次 背景・当時の課題 従来の運用フロー 具体的な課題 解決アプローチ 改善方針の検討 UI化の基本方針 期待される効果 実装詳細 UI設計 ワークフロー 実装で遭遇した課題と対応 課題1: 配布対象者数が多く画面表示に課題 課題2: 本実行結果の表示によるブラウザのパフォーマンス問題 バックエンド実装 API設計 運用・効果測定 まとめ 背景・当時の課題 駅メモ!では、特定のイベント形式にランキング要素があるため、イベント終了後に上位プレイヤーの皆さまへ報酬配布を行っています。従来、この作業は本番環境でエンジニアが直接CLIでスクリプト実行により実施していました。 従来の運用フロー 従来の作業フローは、ドキュメント作成から作業の見守り募集、DBバックアップ、本番サーバーのCLIでのスクリプト実行、ログ確認、告知作成まで、多くのステップと時間を要していました。 【改善前】本番サーバーのCLIでスクリプト実行 ━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1. ドキュメント管理サイトで手順書作成 2. 作業の見守り募集 3. DBスナップショット作成 (x3) ⏱️ 15分 ━━━━━━━━━━━━━━━━━━━━━━━━━━━ ↓ ━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4. 本番サーバーにSSH接続 5. CLIでのスクリプト実行 ⚠️ ヒューマンエラーリスク $ script1 --dry-run $ script2 --dry-run 6. 結果確認 7. 本番実行 ⚠️ ヒューマンエラーリスク $ script1 --execute $ script2 --execute 8. ログ確認・アーカイブ ⏱️ 75分 x 2人 = 150分 ━━━━━━━━━━━━━━━━━━━━━━━━━━━ ↓ ━━━━━━━━━━━━━━━━━━━━━━━━━━━ 9. 告知文章作成 10. レビュー・公開 ⏱️ 25分 ━━━━━━━━━━━━━━━━━━━━━━━━━━━ 合計: 190分 (約3時間10分) 具体的な課題 この運用方法には、いくつかの課題がありました。 まず、操作の記録についてです。SSHによる本番サーバーへの直接ログインが必要で、操作ログの閲覧に手間がかかり、後から見返しづらい状況でした。 次に、ヒューマンエラーのリスクです。本番環境でスクリプトを実行する前には、パラメータやコマンドの内容を何度も確認する必要があり、1つのミスも許されないという大きな責任とプレッシャーが常につきまとっていました。 そして作業効率の問題です。 特に以下の作業に手間がかかっていました。 DBバックアップ 3プラットフォーム(コロプラ版駅メモ!・駅メモ!・アワメモ!)それぞれでAWSマネジメントコンソールを開いて手動操作が必要でした。各プラットフォームごとにコンソールを開き、スナップショット作成を実行し、完了するまで画面を確認し続ける…という作業を3回繰り返していました。完了通知もないため、定期的にコンソールを確認する必要があり、待ち時間が発生していました。 CLIでのスクリプト実行 対象イベントや配布環境の指定、ログファイルの作成など、すべて手動でコマンドに入力する必要がありました。本番環境での実行のため、実行前に何回もコマンドを再確認する必要があり、また手動操作でのミスを防ぐため同時実行が基本禁止となっており、スクリプト実行だけで毎回75分 x 2人という作業時間がかかっていました。 告知文章作成 報酬配布後、毎回お知らせ文章を手動で作成し、レビュー・公開する作業が必要でした。 これらの課題により、運用担当者の負担も大きく、より安全かつ作業負荷を軽減できる仕組みが求められていました。 解決アプローチ これらの課題を解決するため、報酬配布作業を管理画面から実行できるようにする方針を立てました。これにより、エンジニアだけでなく、プランナーも安全に作業を実行できることを目指しました。 改善方針の検討 改善方針を検討しました。 1つ目は報酬配布専用のスタンドアロンツールを開発する案です。しかしこれは認証基盤などを新規に構築する必要が生じます。2つ目は既存の管理画面に機能を追加する方法です。 また、プランナーから「自分たちで報酬配布を実行できるようにしたい」という要望がありました。実現できれば、エンジニアの作業時間をほぼゼロにすることも可能になります。 検討の結果、管理画面への統合を採用しました。理由は、まずエンジニアの作業効率化から始め、将来的にプランナーへの権限開放も検討できるという段階的な改善が可能なためです。また、既存の認証・認可機能を活用できるため新規にセキュリティ基盤を構築する必要がない点も大きな決め手となりました。 UI化の基本方針 まず、報酬配布に関連する自動化機能として、DBバックアップを実行するSlackワークフローを作成しました。従来は3プラットフォームそれぞれでAWSコンソールを開いて手動操作していましたが、Slackワークフローから1回の操作で3プラットフォーム全てのバックアップを実行できるようになりました。ワークフローが裏側でAWS APIを呼び出してスナップショットを作成し、完了するとSlackに通知が届きます。実行状況と結果はログとして保存されます。 次に、管理画面に以下の機能を追加しました。 ドライラン機能を実装しました。実行前に配布対象者と配布アイテムを確認でき、既存スクリプトの標準出力と同等の内容を表示します。プランナーでも理解できる形式にしました。 本実行機能では、ドライランの結果を確認後に本実行ボタンを有効化します。実行ログはJSON形式でダウンロード可能で、Slack通知機能も備えています。 また、従来は複数のスクリプトに分かれていた報酬配布作業を1つの機能に統合しました。 最後に、報酬を配布した後にお知らせを非公開状態で自動作成する機能を追加しました。テンプレート文言を使用するため、プランナーはレビュー・公開のみ実施すれば作業は完了です。 期待される効果 この改善により、以下の工数削減を見込んでいました。 作業項目 改善前 改善後 削減 エンジニア準備 15分 0分 -15分 エンジニアx2実行 150分 60分 -90分 プランナー告知 25分 5分 -20分 合計 190分 65分 -125分(約66%削減) 【改善後】管理画面でUI操作 ━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1. Slackワークフローでバックアップ実行 ⏱️ 自動化 ━━━━━━━━━━━━━━━━━━━━━━━━━━━ ↓ ━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2. 管理画面にアクセス ✅ SSH不要 3. イベント選択 4. [ドライラン実行] クリック ✅ 誤操作防止 5. 画面で結果確認 6. [実行を確認] クリック ✅ 操作記録 7. Slack通知を確認 ⏱️ 30分 x 2人 = 60分 ━━━━━━━━━━━━━━━━━━━━━━━━━━━ ↓ ━━━━━━━━━━━━━━━━━━━━━━━━━━━ 8. お知らせ下書き自動生成済み 9. レビュー・公開 ⏱️ 5分 ━━━━━━━━━━━━━━━━━━━━━━━━━━━ 合計: 65分 (約1時間) 💡 125分削減 (約66%の時間削減) 実装詳細 UI設計 管理画面に専用のランキング報酬配布ページを追加しました。 ワークフロー 操作の流れは3ステップで明確化しました。 Step 1: イベント選択 Step 2: ドライラン結果確認 Step 3: 実行リクエスト送信完了 実装で遭遇した課題と対応 実装中にいくつかの課題に直面しました。 課題1: 配布対象者数が多く画面表示に課題 ランキング報酬の配布対象者が数千人規模になると、ユーザーリストが非常に長くなります。その結果、画面の可読性が低下しました。 【対応前】スクロールが大変 ━━━━━━━━━━━━━━━━━━━━━ イベント情報 配る報酬 配布結果 1位: 太郎 2位: 花子 ... 999位: 健太 <- スクロールが大変 1000位: さくら 実行ボタン <- 遠い! ━━━━━━━━━━━━━━━━━━━━━ 対応策として、画面右側に固定のナビゲーションメニューを追加し、各セクションへワンクリックでジャンプできるようにしました。 課題2: 本実行結果の表示によるブラウザのパフォーマンス問題 本実行の結果表示では、数千件の配布対象ユーザーを配列からフィルタリングし、それぞれに対応する個別リンクをマッピングして画面に出力する必要がありました。初回の本番配布時、この結果をそのまま画面上にレンダリングしたところ、ブラウザが長時間フリーズしてしまいました。配布自体は成功していたものの、フリーズが解消されるまで長時間待つ必要があり、運用担当者の作業時間を削減するという本来の改善目的に反する状態でした。 【対応前】 実行完了! 配布結果: 1位: 太郎 - ✅ 配布完了 2位: 花子 - ✅ 配布完了 ... 999位: 健太 - ✅ 配布完了 <- 数千件のデータで 1000位: さくら - ✅ 配布完了 ブラウザがフリーズ 【対応後】 実行中 - Slackで通知します 報酬配布結果確認リンク: 🔗 1位 [ランキング] <- 新しいタブで開く 🔗 50位〜51位 [ランキング] <- ボーダーラインのみ 🔗 100位〜101位 [ランキング] リンク提供 🔗 1000位〜1001位 [ランキング] 対応策として、詳細な配布結果は個別ページへのリンクとして提供しました。これにより、確認が必要なときに新しいタブで開けるようにしました。 バックエンド実装 既存の配布スクリプトを活用しつつ、管理画面から呼び出せるようAPI化しました。 既存の報酬配布スクリプトが安定して動作していたため、ゼロから作り直すのではなく、既存資産を活かす方針を採用しました。 API設計 管理画面からのリクエストを受け付け、報酬配布処理を実行するAPIを実装しました。 運用担当者 ↓ ━━━━━━━━━━━━━━━━━━━━━━━━━━━ 管理画面 ・イベント選択 ・ドライラン実行 ・本実行 ━━━━━━━━━━━━━━━━━━━━━━━━━━━ ↓ API呼び出し ━━━━━━━━━━━━━━━━━━━━━━━━━━━ バックエンドAPI ・dry_run: true -> 即座に結果 ・dry_run: false -> 非同期実行 ━━━━━━━━━━━━━━━━━━━━━━━━━━━ ↓ スクリプト実行 ━━━━━━━━━━━━━━━━━━━━━━━━━━━ 既存スクリプト群 ・報酬配布 ・勲章付与 ━━━━━━━━━━━━━━━━━━━━━━━━━━━ ↓ DB操作 ━━━━━━━━━━━━━━━━━━━━━━━━━━━ データベース ・報酬データ ・ランキングデータ ━━━━━━━━━━━━━━━━━━━━━━━━━━━ ↓ 完了通知 ━━━━━━━━━━━━━━━━━━━━━━━━━━━ Slack 「報酬配布が完了しました」 ━━━━━━━━━━━━━━━━━━━━━━━━━━━ 報酬配布APIでは、既存スクリプトを呼び出して報酬を配布します。APIが呼ばれると従来の配布スクリプトを順次実行します。スクリプトの実行順序は従来の運用フローと同じです。実行完了時にはSlackへ通知を送信するようにしています。 お知らせ下書き自動作成機能では、工数削減を目的として報酬を配布した後に自動的にお知らせの下書きを作成します。配布完了直後に非公開状態のお知らせを自動生成し、テンプレート文言を使用するためプランナーはレビュー・公開のみ対応すれば完了します。これにより、告知作成の工数を約80%削減できました。 ログ管理も改善しました。従来の標準出力形式から構造化されたJSON形式へ変更し、ログファイルを自動生成して管理画面からダウンロード可能にしました。JSON形式のためプログラムでの解析が容易で、トラブルシューティング時の調査が効率化されました。 また、ログ管理をサポートする補助的なAPIも実装しました。過去の実行ログ一覧を取得するAPIと、指定したログファイルをダウンロードするAPIです。これにより管理画面から実行履歴を簡単に確認できます。 運用・効果測定 実際に運用を開始し、期待される効果で見込んでいた内容と比較してみました。 作業時間の削減については、ほぼ期待通りの結果となりました。UI化により手動操作のミスリスクが軽減されたことで、従来禁止していた3プラットフォーム同時配布が可能になり、作業時間は60分×2人から30分×2人に短縮されました。お知らせ文章を作成する作業も省けるようになっています。 一方、期待以上の効果だったのは、配布実施者の心理的なプレッシャーの軽減です。もうコマンドを打つ必要がないため、本番環境での操作に対する緊張感が和らぎました。これは当初の工数削減という目的を超えた、想定外のメリットでした。 まとめ 今回は、ランキング報酬配布作業のUI化についてご紹介しました。 本番環境のCLIでのスクリプト実行から管理画面操作への変更により、作業時間を約66%削減できました。しかし、最も大きな効果は数値では測れないものでした。それは「本番環境でコマンドを打つ必要がなくなった」ことによる心理的なプレッシャーの軽減です。 エンジニアとしてCLI操作に慣れていたため、当初は「UI化してもそんなに変わらないだろう」と効果に疑問を持っていました。しかし実際に運用してみると、ボタンをクリックするだけで済む安心感は想像以上でした。技術的には単純な変更でも、手作業を減らすことで運用の質は大きく変わります。もし似たような定型的な手作業がチームにある場合、ぜひUI化を検討してみてください。 今まで報酬配布は全てエンジニアが担当していましたが、将来的には他のメンバーも実行できる可能性が見えてきました。運用の効率化を実現できた本プロジェクトは、チーム全体にとって大きな前進となったと感じています!
はじめに 駅メモ!開発チームの id:kaidan388 です。 昨年の6月に新機能として「アルバム機能」をリリースしました。私はこの開発でリーダーを務めました。 このアルバム機能は、駅メモ!の中でも特に規模の大きな開発となりました。また、これまでの新機能がバトル面の強化やチェックイン・アクセスのしやすさが中心で、ライフログを強化する試みはあまり例がなかったため、「本当にユーザーの皆様に楽しんでいただけるだろうか」という懸念も当初はありました。 今回は、この大規模かつ前例のない開発を無事にリリースまで進めるために、チームで試みた2つの工夫について、そのメリットとデメリットを交えてご紹介します。 はじめに アルバム機能について 工夫1:大規模ドッグフーディングの実施 良かった点 1. クオリティの向上 2. 最適な画質の模索 難しかった点 1. 工数の予想が難しくなる 2. コードの整合成が取れない 3. 環境準備のコスト まとめ 工夫2:アジャイル的な週次動作確認 メリット 1. バグの徹底的な洗い出し 2. 段階的なクオリティアップ デメリット リファクタリングに時間がかかる おわりに アルバム機能について 駅メモ!は、おかげさまで11周年を迎えることができました。これを「20年続くゲーム」にしていくために、ゲームを継続して遊ぶこと自体が楽しさにつながる機能を、より増やしていきたいと考えました。 駅メモ!には、ユーザーの皆様の「おでかけの記録」を残すライフログという側面があります。 しかし、これまでの新機能はバトル面の強化や、駅の回収(アクセス)をしやすくする強化が中心で、このライフログという側面に関わる機能強化は比較的少ない状態でした。 そこで今回、ライフログ機能の強化を行うことになりました。 様々な軸が考えられましたが、まずは情報量が多く、ユーザー様が後から振り返る価値を感じやすい「写真」や「画像データ」の素材としての価値に着目し、それらを記録として残す機能の開発を進めることにしました。 また合わせて、以前からユーザー様からのご要望も多かった「過去の移動ログを残し、いつでも見返せる機能」も追加しました。移動の記録と写真を同時に見ることで、お出かけの思い出としてよりリッチに記録できる仕組みを目指しました。 工夫1:大規模ドッグフーディングの実施 前例のない機能だったため、開発の初期段階から「まずは社員で集中的に遊んでみて、そのフィードバックを元に仕様を変更していく」という方針を前提に進めました。 もちろん、駅メモ!では普段からリリース前に開発したものを社員で動作確認しています。 しかし今回はその規模を拡大し、駅メモ!開発チーム外からも、普段から駅メモ!で遊んでいる社員に参加を呼びかけました。最終的に、普段の動作確認の4~5倍の人数の協力を得ることができました。 この取り組みでわかった、良かった点と難しかった点をご紹介します。 良かった点 1. クオリティの向上 最大のメリットは、やはりクオリティの向上です。 人数が多いだけでなく、駅メモ!の熟練者から、チーム外のライトユーザーまで、多様な視点からの意見が数多く集まりました。これらの意見を集約することで、UI/UXをどのように変更すべきかが明確になりました。 例えば、ドッグフーディング中に参加者から「本当にただ写真を記録するための道具になってしまっていて、ゲームらしい『楽しい』という感情が湧きづらい」という指摘がありました。 このフィードバックを受け、急遽、画像を投稿する際に以下のようなミニでんこ画像を表示する仕様を追加しました。 かわいいでんこの画像を追加することで、シンプルに画面を華やかにしたり、でんこと一緒に記録して旅をしている感覚を高めたりする効果を狙っています。 この追加によってユーザー様の感情にどういった変化が生まれるかを定量的に計測するのは難しいですが、追加後の開発メンバーやドッグフーディング参加者の反応を見る限り、非常に好評でした。 2. 最適な画質の模索 画質についても、シビアな調整が必要でした。 画質を良くしすぎると画像1枚あたりの容量が嵩み、特にサーバーからの配信コストが非常に高くなってしまいます。かといって、画質が低すぎると「画像を記録する」という体験そのものの価値を損ねてしまいます。 ドッグフーディングを何度も行い、参加者から画質についての具体的なフィードバックを受けることで、コストと体験のバランスを見極め、細かく調整することができました。 難しかった点 1. 工数の予想が難しくなる フィードバックに応じて仕様を変更すると、当然工数も増えます。 後から工数が増えることで、開発の完了時期の予想が難しくなってしまいました。 対応策としてアルバム開発では、以下のようにチケット状況をグラフにして可視化し、期限までに完了できるかに注視しつつリソースの調整などを行いました。 傾きが急になっている箇所は、まさにドッグフーディングの結果を受け工数の見積もりが増加したタイミングです。 2. コードの整合成が取れない フィードバックに応じて仕様が変更されることで、開発初期に書いたコードと整合成が取れなくなり、結果定期的にリファクタが必要になりました。 これも工数を増やすことにつながります。 3. 環境準備のコスト そもそも、「本番のアプリで、一部の社員だけがアルバム機能(開発中のもの)を遊べるようにする」という環境を準備する作業自体にも、相応の時間がかかります。 今回は、2種類のビルド成果物を用意しておき、ユーザによってアルバム機能を含むビルド成果物と含まないビルド成果物を出しわける、という方法を取りました。 目的は達成されますが、2回ビルドが必要になるのであらゆる場面で時間がかかってしまい、コストが増えてしまいます。 まとめ クオリティが確実に上がるという大きなメリットがある反面、その分エンジニアの対応負荷が高くなるトレードオフの関係にあります。 何にでもこの規模のドッグフーディングを行うのはコストパフォーマンスが悪いと感じたため、プロジェクトの重要度や特性に応じて実施を判断すべきだと感じました。 工夫2:アジャイル的な週次動作確認 駅メモ!では通常、ある程度仕様やデザインが固まってから開発(実装)を開始する、ウォーターフォール型に近い進め方を取ることが多いです。 しかし今回は、 プロジェクト全体の規模が非常に大きい 前述のドッグフーディングの結果、仕様変更が予想される という理由から、従来のように「ある程度の要件が固まるのを待ってから開発する」という進め方ができませんでした。 そこで今回は、要件が固まりきるのを待たず、すでにある程度仕様が確定している画面から順番に開発を着手していきました。 特にドッグフーディングの実施日がマイルストーンとなるため、そこまでにコア機能(最低限、画像がアップロードできる部分など)を完成させる必要がありました。細かいデザイン調整は後回しにして、まずは動くものを優先する、といった判断も行いました。 感覚としては、「いつもの半分の開発工数で、7割くらいの完成度のものを作る」ことを求められるような状況でした。 また、開発と並行して、週に1〜2回のペースで、そこまでの開発進捗についてアルバム機能開発チーム内で動作確認会を行いました。 結果として、「1週間単位で計画立て→開発→動作確認→次の計画立て」という、アジャイル開発に近い体制を取ることになりました。 この方針にも、当然ながら良い点と悪い点がありました。 メリット 1. バグの徹底的な洗い出し まめに動作確認を繰り返すため、バグを早期に、かつ徹底的に潰すことができます。 実際、今回のアルバム機能は規模が大きかったにも関わらず、リリース時に機能起因の不具合は1件も発生しませんでした。これは大きな成果だと感じています。 2. 段階的なクオリティアップ 大規模なドッグフーディングにかける前、まずは開発チーム内で何度も動作確認を実施しました。これにより、チーム内で見つけた分かりづらい文言やUIの細かい調整を事前に実施できました。その結果、クオリティ向上につながりました。 デメリット リファクタリングに時間がかかる この進め方の宿命ですが、一度書いたコードを、後からの仕様変更に伴って何度も書き直していくことになります。 開発の終盤になるほどコードベースは全体的に混沌としていき、変更作業が辛くなっていく、という場面も多々ありました。 「変更に強いコードを書く」という基本がいかに大事か、改めて痛感させられました。 おわりに 今回は、駅メモ!の「アルバム機能」開発において試みた、「大規模ドッグフーディング」と「アジャイル的な週次動作確認」という2つの工夫をご紹介しました。 どちらもメリットだけでなく、工数の増加やコードの複雑化といったデメリットも抱えていますが、前例のない大規模開発を進める上では非常に有効な手段だったと感じています。 リリース後、多くのユーザー様がアルバム機能をご利用くださり、お出かけの思い出を写真と共に記録していただいている様子を拝見し、開発チーム一同、大変嬉しく思っています。 これからも駅メモ!を長く楽しんでいただけるよう、チーム一同、開発と改善を続けてまいります。
こんにちは、エンジニアの id:mp0liiu です。 非常に遅くなってしまいましたが、昨年の7/4にPerlの最新安定バージョンである5.42がリリースされたので新機能や変更点についてまとめます。 source::encoding プラグマが追加され、デフォルトで有効に スコープ内のソースコードに期待する文字コードの指定をするプラグマ source::encoding が追加されました。 指定できるのは ascii と utf8 のみです。 use source::encoding 'ascii' するとスコープ内のソースコードに非ASCII文字が存在している場合、コンパイルエラーが発生するようになります。 use source::encoding 'utf8' は use utf8 と同等です。 v5.41 以降の feature bundle 1 ではデフォルトで use source::encoding 'ascii' が有効になります。 従来では次のように use utf8 していないのに非ASCII文字を扱うようなコードは、エラーになることなく意図していない挙動をしてしまうことがありました。 say length "あいうえお" ; # 本当は5文字だが, use utf8 していないので 15 が出力される use source::encoding 'ascii' することで、そのようなコードはコンパイルエラーになるので事前に気づくことができるようになります。 use v5.42 ; # use source::encoding 'ascii' も有効になる say length "あいうえお" ; # コンパイルエラー: Use of non-ASCII character 0xE3 illegal when 'use source::encoding "ascii"' これからは日本語など非ASCII文字を使うコードはきちんと use utf8 してから書くようにしましょう。 なお、 source::encoding 'ascii' はコメントやPODでも非ASCII文字があるとコンパイルエラーになるので注意が必要です。 __DATA__ , __END__ セクション以降に書く分には問題ありません。 any, all 演算子の追加 List::Util の any, all と同じ挙動をします。 演算子として実装されているためコードブロックのスタックフレームが作られずより高速に実行できるとのことでしたが、ベンチマークをいろいろとってみたところ大きいパフォーマンスの差はないものの、コードブロック内の処理やリストの要素数によってどちらの方がパフォーマンスがよいかが変わってしまいました。 なのでこだわる場合は自分で該当部分のパフォーマンスを計測することをおすすめします。 参考までに List::Util と関数と演算子とでそれぞれベンチマークをとったので参考にしてみてください。 keywordのほうがパフォーマンスが良い場合(any) Benchmark: running keyword_any, list_util_any for at least 1 CPU seconds... keyword_any: 2 wallclock secs ( 1.13 usr + 0.00 sys = 1.13 CPU) @ 495.58/s (n=560) list_util_any: 1 wallclock secs ( 1.11 usr + 0.00 sys = 1.11 CPU) @ 355.86/s (n=395) Rate list_util_any keyword_any list_util_any 356/s -- -28% keyword_any 496/s 39% -- keywordのほうがパフォーマンスが良い場合(all) Benchmark: running keyword_all, list_util_all for at least 1 CPU seconds... keyword_all: 1 wallclock secs ( 1.11 usr + 0.00 sys = 1.11 CPU) @ 503.60/s (n=559) list_util_all: 1 wallclock secs ( 1.04 usr + 0.00 sys = 1.04 CPU) @ 358.65/s (n=373) Rate list_util_all keyword_all list_util_all 359/s -- -29% keyword_all 504/s 40% -- List::Utilのほうがパフォーマンスが良い場合(any) Benchmark: running keyword_any, list_util_any for at least 1 CPU seconds... keyword_any: 2 wallclock secs ( 1.06 usr + 0.00 sys = 1.06 CPU) @ 16.04/s (n=17) list_util_any: 1 wallclock secs ( 1.06 usr + 0.00 sys = 1.06 CPU) @ 17.92/s (n=19) Rate keyword_any list_util_any keyword_any 16.0/s -- -11% list_util_any 17.9/s 12% -- List::Utilのほうがパフォーマンスが良い場合(all) Benchmark: running keyword_all, list_util_all for at least 1 CPU seconds... keyword_all: 1 wallclock secs ( 1.05 usr + 0.00 sys = 1.05 CPU) @ 16.19/s (n=17) list_util_all: 1 wallclock secs ( 1.06 usr + 0.00 sys = 1.06 CPU) @ 17.92/s (n=19) Rate keyword_all list_util_all keyword_all 16.2/s -- -10% list_util_all 17.9/s 11% -- ベンチマークに使用したコードはこちら use v5.42 ; use Benchmark qw( timethese cmpthese ) ; use List::Util (); use utf8 ; binmode STDOUT, ':encoding(UTF-8)' ; my @ary = ( 1 .. 1000000 ); say "keywordのほうがパフォーマンスが良い場合(any)" ; cmpthese( timethese(- 1 , +{ keyword_any => sub { use experimental qw( keyword_any ) ; any { $_ == 500 } @ary ; }, list_util_any => sub { List::Util::any { $_ == 500 } @ary ; }, }) ); print " \n " ; say "keywordのほうがパフォーマンスが良い場合(all)" ; cmpthese( timethese(- 1 , +{ keyword_all => sub { use experimental qw( keyword_all ) ; all { $_ < 500 } @ary ; }, list_util_all => sub { List::Util::all { $_ < 500 } @ary ; }, }) ); print " \n " ; say "List::Utilのほうがパフォーマンスが良い場合(any)" ; cmpthese( timethese(- 1 , +{ keyword_any => sub { use experimental qw( keyword_any ) ; any { $_ == @ary / 2 } @ary ; }, list_util_any => sub { List::Util::any { $_ == @ary / 2 } @ary ; }, }) ); print " \n " ; say "List::Utilのほうがパフォーマンスが良い場合(all)" ; cmpthese( timethese(- 1 , +{ keyword_all => sub { use experimental qw( keyword_all ) ; all { $_ < @ary / 2 } @ary ; }, list_util_all => sub { List::Util::all { $_ < @ary / 2 } @ary ; }, }) ); print " \n " ; レキシカルなメソッドを宣言できるようになった my sub のように my method でスコープ内でのみ呼び出すことのできる、レキシカルなメソッドを宣言できるようになりました。 また、レキシカルメソッドを呼び出すための演算子 ->& も追加されました。 use v5.42 ; use experimental 'class' ; class Point { my method hoge { say "hoge" ; } method wrap { $self -> &hoge ; } } Point->new->wrap(); # hoge $self->&method は method($self) の糖衣構文です。 同じクラス内でもスコープが違えば呼び出すことはできないですし、継承先のクラスから呼び出すこともできません。 switch 機能とスマートマッチング演算子の削除が無期限の延期に Perl5.38 で非推奨となり、5.42で削除予定だったswitch 機能(given-when構文)とスマートマッチング演算子は削除が無期限に延期となり、これらを使っても実験的機能であることの警告は発生しないようになりました。 switch機能はデフォルトでは無効になっており、個別に有効にするか、v5.34 までの feature bundle で有効になります。 v5.35 以降の feature bundle では無効になります。 { use v5.10 ; given ( 100 ) { when ( $_ % 2 == 0 ) { print " $_ is even" ; } default { print " $_ is odd" ; } } } { use v5.36 ; given ( 100 ) { # syntax error when ( $_ % 2 == 0 ) { print " $_ is even" ; } default { print " $_ is odd" ; } } } { use feature 'switch' ; given ( 100 ) { when ( $_ % 2 == 0 ) { print " $_ is even" ; } default { print " $_ is odd" ; } } } スマートマッチングはデフォルトで有効ですが、 smartmatch 機能として有効/無効を切り替えられるようになりました。 v5.40 までの feature bundle では有効になっており、v5.42 以降では無効となります。 { use v5.41 ; print 'A' ~~ [ 'A' .. 'D' ] ? 'Included' : 'Not included' ; # syntax error } { use feature 'smartmatch' ; print 'A' ~~ [ 'A' .. 'D' ] ? 'Included' : 'Not included' ; } あくまで後方互換性のためのことを考えると削除が難しかったので残しておいている、という感じがするのでこれからswitch機能やスマートマッチングを多用するコードを書くのはおすすめできないです。 特にスマートマッチングはオペランドごとの挙動を覚えるのが難しいのでやめておいたほうが良いでしょう。 switch機能は直接条件式を記述するなどスマートマッチングを利用しないように使う限りにおいては使用しても問題ないかなと思いますが、 だとしても代替として match構文 が提案されており、実験的実装も作られ後々実装される可能性があるので、今まで通りコードを書くのが一番無難かなと思います。 フィールド変数の attribute :writer が追加 クラス構文のフィールド変数の値を更新する setter を自動生成する attribute が追加されました。 スカラ変数のみ対応しています。 class Point { field $x :writer :param; field $y :writer :param; } my $p = Point->new( x => 20 , y => 40 ); $p->set_x ( 60 )->set_y( 100 ); # writerはインスタンス自身を返すのでメソッドチェーンも可能 引数を指定すると指定した名前で setter を生成します。 Moose系のクラスビルダーのアクセサと違ってフィールド名と同名のアクセサで getter / setter 両方として使えるようにできないので注意が必要です。 パッケージの区切り文字としてのアポストロフィを無効にできるようになった Perlではパッケージ名の区切り文字に :: を利用しますが、Perl4の頃は ' を利用しており、その互換性を保つためPerl5になってからもずっと ' をパッケージ名の区切り文字として利用できるようになっていました。 ' をパッケージ名の区切り文字として利用することは Perl5.38 で非推奨となり、 Perl5.41.3 で一旦削除されましたが、議論の後にデフォルトでは復活し、プラグマで有効/無効を切り替えられるようになりました。 以下のように apostrophe_as_package_separator 機能として有効/無効を切り替えられるようになっています。 use feature 'say' ; use POSIX; use feature 'apostrophe_as_package_separator' ; say $ POSIX' VERSION ; # $POSIX::VERSION と同じ no feature 'apostrophe_as_package_separator' ; say $ POSIX' VERSION ; # コンパイルエラー apostrophe_as_package_separator は use v5.42 で無効になります。 use v5.42 ; say $ POSIX' VERSION ; # コンパイルエラー chdir が CORE:: 名前空間に追加された コア関数と同名の関数がパッケージ内に定義されている際、曖昧さを避けて呼べるよう CORE:: にいくつか組み込み関数が追加されていってるのですが、その流れの1つかと思われます。 二項演算子で左項が否定されるのが不自然な場合に警告が発生するように !$x < $y のようなコードがあったとして、比較演算子で左項を本当に否定したいことはまずなくて、通常は条件式全体を否定したい場合が多いと思います。 そのような場合に警告が発生するようになりました。 ! $x < $y # 警告が発生: Possible precedence problem between ! and numeric lt (<) 拘束演算子(=~など)、 cmp 、 <=> 以外の比較演算子、isa演算子でこの警告が発生するようです。 このような場合は否定された演算子を使うか、かっこで優先順位を明示するか、優先順位の低い論理否定演算子 not を利用するようにしましょう。 $x >= $y !( $x < $y ) not $x < $y builtin モジュールの indexed 関数で配列のインデックスと値の組のリストを生成し、2変数のforループでイテレーションするコードのパフォーマンスが改善 Perl5.40で追加された、組み込み関数を提供する builtin モジュールの indexed 関数を利用することで、配列のindexと要素の列挙が楽に書けるようになっていました。 use v5.40 ; my @array = qw( red blue green ) ; for my ( $index , $value ) (indexed @array ) { say " $index => $value " ; } ただし、これは配列のインデックスと値のリストを実際に生成する点など、通常の for (@array) のようなループ文と比べて効率的でないコードとなっていました。 5.42からは内部的には配列のインデックスと値のリストを実際に生成するのではなく、通常の for (@array) と同じ方法で配列をイテレーションするようになりました。 まとめ source::encoding プラグマが追加されことは影響が大きそうで、 use utf8 していないころに書かれたコードは見直したほうがいいかもしれません。 その他は今回も細かい改善点が多いといった感じですが、Perlに足りなかった機能が追加されたりPerl4のころの構文を無効にできるようになったりと確実に過去のバージョンより使いやすくなっています。 次のバージョンでは名前付き引数が追加されるなど、大きな変更がありそうで楽しみです。 この記事では書けなかったこともあるので詳しいことが気になった方は 公式ドキュメント もぜひ読んでみてください。 use v5.42; のような構文のことです。Perlでは後方互換性を保つため古い機能は無効にできるように、新しい機能は有効にできるようになっていますが、 feature bundle はそういった各機能を、バージョンごとに推奨されるものをまとめて有効/無効にしてくれるプラグマになっています。 ↩
はじめに こんにちは。駅メモ!開発チームの id:Emiyo です。今回は駅メモのサービスのインフラ環境の EC2 インスタンスをx86_64からAArch64へ移行したため、その話をしようと思います。 なぜARM 単刀直入にいうと、コスト削減のためです。駅メモ!のサービスではAWSの Intel/AMD が載ったインスタンス(x86_64)を使っていました。しかし、公式によるとAWSが開発したAWS Gravitonが搭載されているARMインスタンス(AArch64)が同じスペックのx86_64より20%ほど費用対効果が良いため、移行を検討していました。 そして、全面移行から3ヶ月が経過しました。昨年同月のコストと比較した結果、年間で約20%のEC2コスト削減を実現できる見込みです。 事前検証 まず、ARM移行の是非について、社内で検証しました。考慮すべき問題は主に二つありました。一つ目は会社のサービスがARMインスタンス上で問題なく稼働できるかということです。二つ目は移行することで本当にコストを削減できるかということです。 一つ目については、自社サービス本体のみならず、本番環境と同一の条件で開発を継続できるよう、社内の開発ツール等の検証も行いました。この段階で、ARMインスタンス上で動作しないモジュールの有無を確認し、互換性のないモジュールが発見された場合の対応策についても検討しました。 二つ目に関しては、社内のベンチマーク環境でx86_64とARMインスタンスの性能を直接比較し、費用対効果の検証を実施しました。検証の際は、精度の高い判断を行うため、複数のスペック(インスタンスサイズ)を用いて比較を行っていました。 また、最終的な目的はコスト削減であるため、単なるアーキテクチャ間の比較にとどまらず、x86_64の新旧世代やサイズ変更による性能変化(縦比較)についても並行して検証しました。 検証の結果、以下の2点が明らかになりました。第一に、現行のx86_64と同等のスペックを持つARMインスタンスにおいては、同等のパフォーマンスを維持しつつ、コストを5%削減可能であるという結果が得られました。第二に、現行より1ランク上位のARMインスタンスを検証したところ、コストは4%上昇するものの、パフォーマンスが20%向上することが確認されました。 以上の結果を踏まえ、今回は「現行より1ランク上位のARMインスタンス」への移行を決定しました。インスタンス単価はわずかに上昇しますが、20%の性能向上により、稼働させるインスタンス総数を最適化(削減)できるため、最終的なインフラコストを最も大きく抑えられると判断したためです。 計画 社内状況により本格的な移行作業は最初の検証から1年後となりました。これに合わせ、移行の3ヶ月前から準備を再開したのですが、この1年でインフラ環境に大きな変化が生じていました。 まず、OSが Ubuntu 20.04 から 24.04 へアップデートされ、さらに既存の x86_64 インスタンスのスペックも引き上げられていました。検証当時のデータが現在の環境にそのまま適用できないと判断し、改めてパフォーマンスなどを再検証を行うこととしました。 また、1年前の検証段階では「ARM非対応のモジュール」がいくつか発見されていました。しかし、当時はそれらが近いうちに廃止される予定だったため、移行時には問題にならないと判断し、特段の対応は見送っていました。ところが、この1年で状況が一変します。諸般の事情により該当モジュールは引き続き不可欠な機能として利用継続となったため、急遽ARM環境での互換性を確保するための改修、あるいは代替手段の検討という、想定外の対応を迫られることとなりました。 これらの予期せぬ課題に対し、他チームとも密に連携しながら代替策や改修方針を改めて検討し、柔軟に対応を進めました。 移行の3ヶ月前から余裕を持って準備と現状確認に着手していたことが功を奏し、スケジュールを大きく乱すことなく、確実な移行へと繋げることができました。 実際の移行 検証段階では、サービスが問題なく動作することを確認しましたが、本番環境への全面移行を即座に行うには慎重を期す必要がありました。 そのため、まずは段階的な移行プロセスを採用しました。第一段階として、開発を主管するエンジニア数名にARMベースの開発環境を提供し、日常的な業務において予期せぬ不具合が発生しないかを一定期間観測しました。 続いて第二段階として、本番環境に数台のARMインスタンスを限定的に投入し、実際のトラフィック下での挙動をモニタリングしました。これらのプロセスを経て、十分な安定性が確認できた段階で、最終的な全環境のARM移行を完了させました。 移行結果 コスト削減以外にも、移行によって大きな副次的メリットが得られました。 CI/CD環境の高速化 『駅メモ!』チームでは、CI/CD環境としてAmazon EC2を用いたセルフホスト型のGitHub Actionsを構築しています。この実行環境をARMインスタンスへ変更したところ、CIの実行速度が約30%向上しました。 イベント運用時の安定性と効率化 『駅メモ!』では、レイドイベント等の高負荷時にパフォーマンスを維持するため、インスタンス数を増強しています。ARM移行後は、従来よりも少ない増加台数で負荷に対応可能となり、インスタンス数の推移もより安定しました。 まとめ 今回のARM移行では、ベンチマーク環境が重要な役割を発揮しました。本来同じスペックの移行だけを考慮していましたが、1ランク上のスペックのパフォーマンス検証もできたことにより、より広い選択肢が視野に入りました。そのおかげで、最終的により費用対効果が良いインスタンスを選択したことで、インフラコストの削減に成功しました。 また、事前に余裕のある移行スケジュールを設定したことで、今回のような前提条件の変化や想定外の技術的課題が生じた際にも、立ち止まることなく素早く計画を調整し、柔軟に対応することができました。この経験から、大規模な環境移行においては、余裕を持った移行計画の策定こそが重要であると改めて実感しました。
はじめに こんにちは。駅奪取チームの id:kawa-mf です。 今回は、駅メモチームから駅奪取チームへ異動して、業務領域がどのように変化したのかについて、記述しようと思います。 前提としてなのですが、使用している言語(Perl, Vueなど)や環境といったものはほとんど同じで、チーム独自のものは少ないです。 1つ大きな差があるとすると、駅メモチームは人数が多く、駅奪取チームは人数が少ないことです。 今回の記事では、チームによって、どれくらい業務内容が変化するのかを書いて、これから入社を考えている人の参考にしてもらえると嬉しいなという温度感くらいで書いていこうと思います。 これ以降は、駅メモチームと駅奪取チームのことは、駅メモと駅奪取と記述させていただきます。 変わったこと 1つのプロジェクトに対する開発エンジニアの人数 駅メモ 2~5人 駅奪取 基本的に1人 上記はコーディングを進める人が一人という意味です レビューなどは、チームメンバーが行ってくれます 期限などが近い場合はヘルプに入って、一時的に3人になったりはします 一つのプロジェクトに対し、割り当てられる人数からもわかるように、駅メモのプロジェクトは比較的大きく、駅奪取は周年開発のようなものでも普通くらいの大きさのように感じます。 駅奪取だと、相談などはいつでもできる環境は整っていますが、1つのプロジェクトに対して、1人のエンジニアがコーディングを行います。 チーム開発的な複数人で進めるといった経験を積むことは難しいですが、1人で進める上で良い点もあります。 開発をする上で、何が必要かなどの見積もりから全て行う必要があるので、コーディング部分以外の知識も身につけることができます。 今回、駅奪取14周年で追加した新ニャッシュの都市ニャッシュを開発したのですが、初めての見積もりを行いました。 見積もりでは、他のニャッシュのスキルの詳細を詳しく知らなかったので、その部分についても調査しながら進めたこともあり、少し時間がかかってしまいました(+1日程度)。 見積もり以降の実装については、基本的に1人で行いました。 今回は、そこまで不具合が発生しなかったのですが、不具合が発生した場合は実装した人が一番の理解者のはずなので、その人が対応にあたることが多く、責任も重くなっていると感じました。 AWS周辺の作業 駅メモ 開発チームが見るのは、フロントエラーやサーバーエラーなど メンテナンスやDBのエラーなどは、インフラチームが担当 駅奪取 全て担当 駅奪取だと、人数が少ないためインフラの業務も行う必要があります。 こちらは普段の業務で変わった部分が多いです。 例えば、毎週の負荷分析で今までよりも見る範囲が広くなったことや、CloudWatchアラームをTerraform経由で追加するなどです。 ページごとのデザインについて 開発における流れとして、デザイナーが画面のデザインを考える → エンジニアやディレクターを交えて、デザインが問題ないのかを考える → エンジニアがコードに落とし込む(落とし込む際にもより良いデザインを思いつけば、デザインを調整する) というのが通常の流れだと思います。 駅メモだと、その流れに沿って開発が行われていましたが、駅奪取には専属のデザイナーが不在です。 基本的には、デザインもチームで考えて、考えたものをデザイナーの方に添削していただくみたいな形が多いです。 これまで、デザインについて、深く考えたことがなかったのですが、この辺りについても考える必要があります。 配属されてすぐに既存のページの表示改善を行いました。 しかし、変更方法が悪く、ユーザーからの問い合わせもあり、最終的にはチケットの要件から変更しました。 軽微なデザインを追加/変更するのみでも難しいということを実感しました。 個人的な感想なのですが、配属されてから1年ほど経ちますが、デザインを考える部分は全く慣れないです... 駅メモと駅奪取を比較し、良い点と残念な点(駅奪取視点) 良い点 AWSでエラー確認などを含めてなのですが、対応する範囲が広くなったことで、より周辺部分について知識を得ることができた点 残念な点 駅メモだと複数人で開発していることもあり、チーム開発という感じがするのですが、駅奪取だと個人開発の感覚から抜け出せない点 駅奪取チームで個人的にやりたいこと 普段の開発では経験するのが少ない、インフラ周りの作業 ユーザーのニーズに合わせた良さそうなデザイン設計
駅奪取チームの id:konakawa です。 以前駅奪取で、デプロイ戦略に起因して、特定ケースにおいてサーバのファイルのタイムスタンプが巻き戻ってしまうことがありました。 これにより、キャッシュバスティングをすり抜けて古いキャッシュが混ざってしまい、不具合の原因となってしまいました。 本記事では、この問題について概説し、とった対応を含めて紹介します。 同じようなインフラ構成のサービスの役に立てば幸いです。 問題概要 デプロイ戦略について 駅奪取では、最新のファイルが以下の2通りの経路で各 EC2 インスタンスへ届けられる仕組みとなっています。 デプロイ時: デプロイ用のサーバーから、rsync でファイル一式を配布 メタデータも同期される オートスケール時: AMI から起動したインスタンスが git pull を実行 Git は内容が同一のファイルは更新しないため、その場合はメタデータが更新されない これにより、後述するような特定ケースにおいてはファイルのメタデータの異なるインスタンスが混在するような構成になっていました。 問題となる時系列 本プロダクトで、あるときのフロントビルド結果の変遷として以下のようなものがありました。 ある時点でのビルド結果(以下 A とする)が存在する 変更が入ったビルド結果(以下 B とする)がデプロイされる 上記変更を revert したビルド結果(以下 A' とする)がデプロイされる 単に変更とそのリバートを順に反映しており、ありふれた経緯かと思います。 この経緯では A と A' は、内容は同一ですがファイルのタイムスタンプが異なるものになります。 以上の手順を踏んだとき、インスタンスが持つビルド結果は デプロイ時点で起動していたインスタンス: A' オートスケールで上がってきたインスタンス: AMI の持つビルド結果が A の場合、更新されず A のまま という状態が混在することになります。 キャッシュ このプロダクトでは、キャッシュバスティングのために、静的ファイルの URL をそのファイルのタイムスタンプ(mtime)を元に生成します。 すなわち、ファイルの更新があった際は URL が更新されるため、古いキャッシュを掴み続けることがない仕組みです。 しかし前述のように、オートスケールで起動したインスタンスでは、ファイルが A 時点のタイムスタンプを持ったままになります。 そうしたインスタンスから返される HTML には、 A のタイムスタンプを元にした静的ファイルへの URL が埋め込まれます。 CDN やブラウザのキャッシュは、このタイムスタンプ情報を含む URL をキーに紐づきます。 ここで、例えば以下のようなシナリオで、古い A の URL に新しい B の内容をキャッシュすることがあります。 A のタイムスタンプを使った URL へのリクエストが発生し、そのキャッシュの TTL が切れていればオリジンへリクエストを行う。 このとき B が最新である期間であれば、オリジンから B のファイルが返され、A の URL に B の内容がキャッシュされる状態になる。 ブラウザでキャッシュを掴んでいる場合、If-Modified-Since によりキャッシュの更新確認を行う。 キャッシュのタイムスタンプとオリジンのファイルのタイムスタンプを比較して、後者の方が新しければオリジンのファイルを使うが、 オリジンでも巻き戻りが起こっていれば、ブラウザで掴んだキャッシュを使用することになる。 この状態でパスに用いているタイムスタンプの巻き戻りが起きると、キャッシュバスティングが正しく機能しなくなります。 すなわち、最新のファイルの内容が A' であるにも関わらず、古い B の内容が使われてしまいます。 この結果、ビルド結果に A(') のものと B のものが混在することになり、不具合が発生する原因となってしまいます。 対策 FSx 化やあるいはイミュータブルインフラ化など、インフラ側の仕組みを変えることで、今回の mtime 巻き戻りのような状態の不一致が起きなくなります。 これらは本件以外にも有効ですが、比較的重い対応になります。 これらを導入するまでビルドの反映をリバートできないというのは不便なため、少なくとも本件の対応としてはこれらは見送りました。 オートスケールで上がってくるインスタンスが最新の情報を取得する方法を git pull から rsync に揃えるというのも1つですが、rsync で配る元のサーバが生きていることに依存してしまうため、これも避けたいです。 そこで、ファイルの名前自体に内容ハッシュを付加する対応をしました。 これであれば非常に軽量に入れられ、かつ前述したところの A と B はファイル自体が違うため、キャッシュが混じることもなくなります。 まとめ インスタンスの状態を更新するのに git pull を用いていたが、ファイルの内容が変わらない場合はメタデータが更新されない タイムスタンプの巻き戻りが発生することで、キャッシュバスティングがあっても古いキャッシュが混じる パスやクエリパラメータでなく、ファイル名レベルで変更を反映することで解決できた ここまでご覧いただきありがとうございます。 本記事が似た問題に遭遇した方などの参考になれば幸いです。
概要 こんにちは、駅メモ!開発チームエンジニアの id:hayayanai です! 11/14-15に開催された YAPC::Fukuoka 2025 へ参加してきました。今回はそのレポートです。 「レポートを書くまでが YAPC」とのことで、社内ドキュメントとして共有したものを手直しして、このブログにも投稿しておきます。 yapcjapan.org 講演を聴いたり会場を見て回ったりして、業務で活かせないかな〜と考えたことを書き残しています。 概ね講演メモ→感想という感じの順番で書いてます。LTについてはいくつか聴きましたが、気になったものを少しピックアップして書いてます。 参加を決めた理由 会社のスポンサーチケットがあった YAPC行ってみたかった モバファクの社員が元々結構参加していて、興味があった 福岡行きたい Perl あまりわからないけど、ちょっと詳しくなりたいな 他のカンファレンスとYAPCの雰囲気の違いを感じてみたかった Day 1 オープニング パッションを感じるオープニングトーク なぜ強調表示できず ** が表示されるのか — Perlで始まったMarkdownの歴史と日本語文書における課題 by hkws GPTのレスポンスで、強調表示できたりできなかったりする カギカッコやスペースで強調されたりされなかったり CommonMarkの仕様に由来 CommonMarkはまだv0だけど、実態はv1みたいな感じ Markdownの歴史 ** のオリジナル→CommonMarkの変化 Markdownには強調と強い強調がある strongタグに変換 →emタグに変換の順で処理 **や*のネストを処理に困って、left/right-flankingの導入をした 分かち書きする言語でバグる 対策は考えられてるけど、CJK friendlyなParser/Rendererを利用しないといけない 感想 Markdown、文章の改行周りや箇条書きの方言も多くて大変だなと思ってた。 自分が使うMarkdownパーサーはどんな解釈ルールで解釈されてるか、確認したほうが良さそう。 CJKを意識した実装になってないものを使ってたら意図しない表示の原因になるかも。 エディタとレンダラーで同じパーサーを使っている場合は問題なさそうだけど、異なるパーサーを使っている場合はさらに注意が必要そう。 Introducing RFC9111 by 小山健一郎 参考書籍: Web配信の技術 今回は共有キャッシュの話 HTTPキャッシュの実装は参照してるRFCが色々ある FastlyはRFC9111 CloudflareはRFC7234/RFC2616 RFC7234が多め 新しく作るなら、廃止されているのでRFC9111が良いだろう ミドルウェアがたくさんある どのリクエストをキャッシュしていいかというのもRFC9111に書かれている Tests for HTTP Cachesを使うとローカルでテストができる CDNのキャッシュの実装を見ると良い 感想 開発者ツールのNetworkタブ見るときも、no-cacheの挙動とか覚えておくと良さそう? キャッシュの利用を考えるとき、できるだけユーザーに近い側のキャッシュを使うというのは改めて大事だなと思った。 AIコーディングの弱点、やっぱりプログラミングは人間が(も)勉強しよう by きしだ なおき Transformerの解説 最近は変数の入力を大きくしても性能がほぼサチっている 電気や人間が供給できる文章数に限界 チャットで人間による評価が入ってきてパフォーマンス向上(GPT3.5〜) Function Calling LLMから外部関数を呼び出すように MCP 一旦知識を吐き出させて、ユーザー入力と知識を投入させると精度が出やすい Reasoning AIの計算 3桁までの数字は1トークン LLMはユニットテストが書けるものは賢くなる 非機能要件は大体苦手になる 長いコンテキストの対応 1Mトークン対応と謳っているが、性能が出るのは30Kトークンくらい どんどん遅くなっていく なぜ同じ失敗が続くのか 間違い同士で注目して、大事な情報だと認識してしまう コンテキストを汚さずコンパクトに。 汚れたときにコンテキストから消し去るのは、coding agentが頑張っている部分 壁打ちと実際に作業させるチャットを分けると良い ダメな情報はダメなものとして認識してくれない 否定文を上手く理解してくれないのと同じ プログラミングの話 感想 「失敗を繰り返さないようにログとして持たせる」vs「失敗のログが重視されてしまって誤りを繰り返される」のバランスが大変そう。 ダラダラ書いてると人間とAIどちらにも優しくないコード書きがちだから、コードのユーザビリティを意識していきたいなと思った。 最近はAIが人間のプログラミング学習を支援してくれる機能もあって人間の勉強もしやすいよなと思っている。研修に活きてきたりするんだろうか。 「データ無い!腹立つ!推測する!」から「データ無い!腹立つ!データを作る」へ ― ゼロからデータを作り、チームで育てられるようにするまで SNSアカウント一覧の中にCPANのIDもある データは必要になってから重要性に気づく 推論は… 人間がわからないものは機械にもわからない 推論せずに済むなら推論しないほうが良い データを作る 利用できる状態まで、データのソースを整理する 辞書作りの例 DuckDBを利用 スプレッドシートからはじめよ ツールのフロントエンドとしてデファクト 前処理など、データ作りは大変 SPOF回避や、マイルストーン設定が大事 手順は変えてもいいが、一貫性は持たせないといけない 感想 データが欲しくなったときに、そもそもデータが無いことに気づくのあるなと…。 (本題とは逸れるけど)論理削除フラグみたいなbooleanを、delete_fgじゃなくてdeleted_atで持つテクニック、ちょっと前にXで見たけどわかりやすそうだなと思った。 DuckDBが各種プログラミング向けにライブラリがあるよと紹介されたとき、Perlにもあるんだろうか…?と疑ってしまったけれど、ちゃんとあるようだった。 大規模OSSに一人で立ち向かうには WebKitのスクリプトはPerl/Ruby/Pythonで書かれている 高校生からOSSに貢献 Prettierは人手不足でメンテナーに WebKit, JavaScriptCoreの話 WebKit開発者のランクの話 fix typoだけじゃなくて、数年単位で責任持ってやっていく 可能な限り時間を捻出して、集中してやる 最も大事 コードを読む(AIに読ませる)→手を動かしてデータを見る プロジェクト内の常識に沿って書けるようにする 他の貢献者の活動を見る 寝よう コードを書く気が起きないときは、AIに書かせる 自分の助けのためにAIを使うのではなく、書かせる部分の性能について もっと進化してほしい このパッチと似た最適化を別のところに適用させたりはうまくいく 感想 PerlもWebフロントエンドのOSSに比べれば人手不足だろうなという気がするので、Perl書けるエンジニアとして何らかの貢献してみたいなと思った。 プログラミング言語の勉強の話、知らなかった言語がスラスラ読み書きできるようになるのは楽しかったよなぁと、昔の記憶が思い起こされた。 他のコントリビューターのコードを見るのは、OSSに限らず、複数人で開発しているリポジトリなら大事だよなと感じた。 「バイブス静的解析」でレガシーコードを分析・改善しよう by hitode909 Perlの静的解析の話 プロジェクト固有の静的解析ツールをAIに作らせて、AIの信用できない部分をカバー 静的解析は構文木をパースするけど、バイブス静的解析はコードベース内の記法のみ扱う PPI等ではなく、正規表現で作る 言語で扱える全ての記法に対応する必要ないよね 偽陰性は、デッドコードが残るだけだから問題なし 偽陽性は不具合のもとで危ない 確認、実行は人間の目に留めておくと良い デッドコード削除のカスタムコマンドを作っておくと便利 useしてないのにメソッド呼び出しするコードの改善 正規表現でやってたのを、AIにPPI版を実装してもらう PPI版が遅すぎるから、同じ結果になるように正規表現の方を改善 感想 「デッドコードの一生」の図が、この通りにやったことあるな〜となって、どこも一緒なんだなという気持ち。 use忘れでメソッド呼び出しとか、どこでも問題になるんだなぁ…😭 LT 「文字列→日付」の落とし穴 標準のモジュールで変換する場合も、バリデーションは手前でしないと意図しない変換になる場合がある 感想 結構トラップな挙動…。 高速化&コスト半減!? GitHub Actionsのサードパーティーマネージドランナーの比較 by occhi みんなCIは高速化したい ランナー GitHub ホステッド セルフホステッド サードパーティ←この話 感想 サードパーティランナーがGitHub ホステッドのランナーより安くなる理由ってどこから来るんだろうか 2ヶ月で新規事業のシステムを0から立ち上げるスタートアップの舞台裏 新規事業の立ち上げ: トップダウン 再現性の考察 リサーチは人力 SRE含めエンジニアは5人 あらゆるものが無いところから 技術スタックは買えない(Ruby on Rails) ユーザーストーリーマッピング Spec-Driven Development AIにPR作ってもらう 仕様書等もコミットした 1週間に40回デプロイ AIの0→1は瞬発力がすごい AIが散らかしたコードをきれいにした 感想 RailsでAIの恩恵受けづらいの、やはりPerlと似たようなところがあるのかなと思った。 【お楽しみ】10分トークN連発 by YAPC::Fukuoka チーム (40分) 幕間CMを支えている技術 React + GitHub Pages or Vercelでやる Window Management APIでフルスクリーンを制御する display:none でできるだけローカルキャッシュを使う ネットワークが不安定なことの対策 感想 てっきりOBSとか使ってるのかと思ってたけど、手作りで丁寧に作られていた 後天的Perl Rubyを先天的に獲得 Perlの . を文字列結合からメソッド呼び出しにできた 感想 これはPerl? それともRuby? クイズ思い出した できるもんなんだなぁ log 対数ではない Perl/Ruby界隈はLTSVが主流 Goはslogが来て構造化ログのトレンド? JSON Linesがいい感じ Claude CodeのログもJSON Lines thinkingのログ見たり テスト・静的解析のログをsub agentにさせて、コンテキストを食いつぶすのを避ける試みをしてる ハルシネーションには注意 iframeを許可するようにheaderで許可すると良い 感想 テスト・静的解析のログをsub agentにさせるの面白そう。自動でやってくれる未来も来そう 頑なに再代入しない! 理由 if文の中で場合分けして変化していくコード…。わかりにくい 読みやすさ・バグのリスク低減・保守性UP jsの例 letの時点で、変数の流れを追いかけることになる constで即時実行関数にする。などで対応 デメリット検証 実行時間比較 IIFE > 関数を作る >= 再代入でフラグ設定 OSSで実践した node-lambda 値が不変で安心 関数をつくることに意識が向きやすい ユニットテストが書きやすくなるメリット バグが生まれにくいというところにもつながる 感想 jsの即時実行関数みたいなことを、たまにperlでもdoやsubでやったりするので、わかりみが深かった。 発表の経過時間とスライドの進み具合がウサギとカメでわかるの良いなと思った Rabbit というものらしい? LT 銅鑼で発表が打ち切られるの面白い プロジェクトの空気を読んで開発してくれるPerlのAIツールがほしい by kobaken 最近のPerlは初学者やAIに優しくなってる 古いものと混ざってわかりづらい 本を書いて、初学者やAIにも優しくした 感想 新しいバージョンに移行したときとか、適宜情報をアップデートしていくのは人間・AIどちらにも大事だよなと思った Pythonを"理解"しているコーディングエージェントが欲しい!! by nikkie Linter で指摘する 修正するところはsub agent に任せている 感想 修正をsub agent に任せるのはなるほど〜となった。 基盤モデルのアーキテクチャを改造してみよう - 時系列基盤モデルのマルチモーダル拡張事例の紹介 by himura467 離脱する時期や、一度落ちたら戻らないから、その手前でアラートを出せないかをやってみた 感想 ゲームの離脱防止に繋がるかも? Day 2 起床後、ホテルで急なお仕事をやっつける。ちょっと遅刻して会場入り 旧から新へ: 大規模ウェブクローラのPerlからGoへの置き換え by motemen 感想 20年の歴史のあるプロダクトを移行するって判断がすごい。 Perl実装に切り戻す判断をあらかじめ決めておくのも大事だよなと思う。 やっぱりPerl→Goでリソース減るんだなぁ。 gRPC/OpenAPIエンドポイントをPerl側から呼び出して共有←良さそう。 OpenTelemetry対応、Perlでやるよりは楽だろうから言語移行のメリット出て良さそう。 Perl の生きのこり Perlの歴史の話 CGI mod_perl PSGI/Plack Interface/実装 の関係 複雑なアプリケーション 環境構築問題 Carton コードの問題 CIで問題検知 複雑化するプロダクトにPerlはどう立ち向かってる? プログラミング言語の変化 perlはbackward-compatibilityだけではなく、bugward-compatibilityも重視 でも、OOPは変化した v5.42は組み込みで class がついてくる try catchもある スマホ対応による変化 ChatCPT, AIによる変化 新しい書き方を推し進める動機づけ experimentalでないなら try catchはおすすめ Try::Tinyはデファクトだけどハマりどころがある 感想 後方互換性を保ちながら、時代に合わせて発展していったのがPerlなんだなと再認識 機密情報の漏洩を防げ! Webフロントエンド開発で意識すべき漏洩ポイントとその対策 モダンなフロントが漏洩させやすい理由 テンプレートエンジンとの違い ViewライブラリでUIを組む 特にSSRも当たり前 Nextでif文使って出し分けるだけだと、キャンペーン情報の存在が漏れる 対策 サーバーから情報をfetch フレームワーク特有の仕組みを使う 漏洩の有無を調べる方法 grep 開発者ツールのNetworkパネル テクニック CIでもgrep Taint API(React) 実行時エラーでページを見られなくできる GraphQLでdata fetch スキーマに書かれたfieldしか取れなくなる resolverの実装コストとトレードオフ 感想 Taint API、Nuxtにも無いかな〜 モダンなフレームワーク、バックエンドとフロントエンドの境界が曖昧な感じで、それが漏洩に繋がりやすいのかなと思った。 読む技術・書く技術・伝える技術 - 15年続けて分かった持続可能なオープンソース開発 by azu 15年くらいオープンソース開発 アウトプットではなく、アウトカムを目指している 実際の影響・成果 時間が必要 燃え尽きまでに段階がある 技術的依存を増やし、心理的負荷を減らす 技術的依存はコントロール可能 心理的負荷はコントロール不可 JSer.infoの話 新しい情報を数千から自動収集 人間のキュレーション さらに興味を持って見てもらう 将来的には自動化される未来もあるかも 13記事溜まったら公開で、心理的負担の減少 textlintの話 No core rule 自分でルールを選んでね コアにルールを持つと、ルールのissueがコアに集まる=心理的負担増 ユーザーがプラグイン(ルール)を選ばないといけないのがデメリット ユーザーがAIになって難しさが変化 JavaScript Primerの話 書籍版とウェブ版がある ウェブ版は常に公開されているから、完成へのハードルが下がる 章を書くときに、Design Docを作ってから書き始める Design DocからAIにドラフトを書かせて第一歩とする 既知の言葉で未知を説明する サンプルコードの実行結果があっているかをテストで確認 コントリビューターを増やす 変化への対応 著者を増やして心理的負担を減らす 報酬も出す 100人以上 Stale issue/PRの自動閉鎖とか、心理的負担の低減に役立つ 技術的依存は交換(更新)可能だからいい感じ 更新を継続するにあたって、習慣化はどれくらい大事だと感じていますか? 勝手に習慣、癖になっている 感想 JSer.infoはよく読んでいるけれども、inputの部分がとても大きいことを始めて知った。 ただその中で、技術による自動化を組み込んでいるのが面白いなと思った。 textlintもよく使っていて、技術記事に特化したルールやAIっぽさを消すルール等、自分で目的に応じて調整できるのが合ってるなとは思う。 JavaScript Primerも新人時代に読んだ記憶があって、最新の内容を学べて良かったなと思った。 裏側でかなりの人や技術が働いているんだなと StaleなIssueやチケットを、ちゃんと閉じていくのは大事なんだろうなという気持ち 探求の技術 週1でブログを書いている なぜ探求するのか おもしろさが原動力 追いかけるのではなく、楽しむ 報酬や評価を目的(外発的動機づけ)にするよりも、自分の興味や好奇心から(内発的動機づけ)のほうが、長期的に続きやすい アウトプットは自分のため 外からの評価より、自分の学びを目的にする 他人へ説明するために書くことで学習効果が生まれる 誰かが読むかもという意識 習慣の力 意志力に頼らず行動できる 歯磨きをやるぞ!ってやるのは少ない。習慣になってるから 技術ブログを書くぞ!ではなく、習慣化している きっかけ→ルーティン→報酬→きっかけ 習慣化を根付かせた例 歯磨きを習慣化させたアメリカの例 きっかけの創出 すぐできるような行動。(歯に舌で触れる) 即座に報酬を得られないと習慣化が難しい 虫歯がなくなって健康よりも、ミントで爽快感を得ることをアピール 技術探求を習慣化するには 情報収集チャネルを用意 X, はてなブックマーク, RSSリーダー, 会社のSlackチャンネル ルーティンを決める 習慣が途切れることを嫌に思わせる (損失回避バイアス) すぐに取りかかれるように環境を用意しておく 報酬を用意 いいねやアクセス数は報酬にしない 外発的動機づけになってしまう 自分でコントロールできるものが良い 記録を残す工程は自動化する 課題 情報過多・キャッチアップの限界 アウトプットの質と量のジレンマ どっちを優先すべきか わかりやすい記事が書ける理由 自分が一番わかっていないから 技術ブログを書くのは、自らの学習の手段 精通してから書くのではなくて、逆 試行錯誤しながら理解を含めていくライブ感 躓きやすいポイントを押さえられる 結論ファーストであるかは必ずしもそうではない 上司への報告ではないから コンテキストファーストで、導入部で読者の前提知識を揃える Stack Overflowがこの構成 Situation->Complication->Question (状況→複雑化→疑問) テンプレートを用意 導入部 課題定期 解決策 実装例 結論 参考文献 登壇経験が文章力を高めた AIに技術記事を書かせるのはどうか 学習の機会は失ってしまう 壁打ち相手として文書構成を検討 ラバーダッキング効果 文章校正 いきなり編集は任せない。出力フォーマットを指定しつつ、指摘のみにしておく 自分の知識を増幅する道具とすると良い 感想 自分がXで情報収集しているのも、普段の癖を活かせてるから良いのかなと思った 技術広報を推進するチームのメンバーとしては、各々の開発環境に技術ブログ執筆用のリポジトリをクローンしておいてもらうのが良いのかもと思った きっかけ作りに良いかも 執筆までの第一歩を下げられそう 今後の業務で使いそうなので、また読み直したい LT ghqの秘密 ghqが対応してるバージョン管理システムの話 感想 GoogleもPerforceだったんですねぇ 今はPiper 伝統的日本企業のソフトウェアエンジニアになって無双しよう! toCサービスたくさん 内製開発組織がない YAPC参加で認知拡大 感想 カンファレンスでスポンサーして認知拡大させるのは良さそう 企業スポンサーってそういう目的もあるよねと グッズでサンリオほしい 自社IPのグッズ良いなとなった スポンサーブース 全部回れました コンプリート景品の煎餅はお土産に ブースの方とも色々話せた 時間によっては登壇者の方も 懇親会 クラフトビールを出している店が有名らしいと聞いた ピーチのビールを飲んだ。美味でした 聞いてみた感じPerl書いたことない人や、使ってない会社も多めだった Ruby(on Rails)やPHP書いている人は結構見かけた 学生支援制度は素晴らしい パックマンルールで立っている方がちらほらいて、スッと会話に混ざりやすかった YAPCのセッションやAI、PerlやRubyは皆の共通の話題として話が広がりやすかった印象 登壇していた方や学生さんとも話せて良い学びになった 色んな人と話して、今後の業務に活かせそうなアイデアがたくさん浮かんだ 既に社内で提案してみたり もっと話したい! 全体の感想 以前お世話になった方にも久しぶりにご挨拶できてハッピー 福岡に住んでいる同僚や知り合いにも会えてハッピー Slidoで思いついたことをサクッと質問できるので楽だった lintエラー対応をsub agentにやらせてコンテキストを節約するの流行ってそう 社内にシェアしていきたい AIの話は多め 開発でAI使ってる話が多め。プロダクトに組み込むAIの話もあった Perl詳しくないから理解できるか不安だったけど、知らない人にもわかるような導入から始まるセッションが多くてわかりやすかった モバファクはPerlコミュニティ的には話せるネタを持ってたりするのかもしれない? 他のカンファレンスよりもホームのような雰囲気で参加できた 来年は東京。次は前夜祭から参加したい
駅奪取チームの id:kimkim0106 です。 モバファクでは全社での Gemini の導入、エンジニアへは GitHub Copilot と Cursor を導入など、AI を活用した生産性向上に継続的に取り組んでいます。 駅奪取チームにおいても、積極的に AI 活用を行っており、以前からコードレビュー業務の改善として PR-Agent を導入しています。 tech.mobilefactory.jp ですが、既存のツールにも問題点があったため、Claude Code を活用できないかと考え、検証を行いました。 プライベートでの開発に Claude Code をすでに使っているメンバーがいたことも理由の 1 つです。 新規であったり小規模なプロジェクトで活用できることは分かっていたものの、規模が大きく複雑なプロジェクトでの活用はやってみないとわからなかったため、検証を行うこととなりました。 検証内容 検証は、当初は 6 月から 2 か月間でしたが、効果があったため延長し、現在も検証を継続しています。 以下のような内容を検証しました。 どのような業務で活用できるか 今までのツールでも、単純な作業の代替はできていましたが、複雑なタスクをこなすことができるかを検証しました。とくに、大規模かつ長期運営プロダクトでの活用というのは業務でないと検証できないことです。 とくに、駅奪取は 2011 年からサービスを開始しており、14 年近く運営を続けている長期運営プロダクトです。 長期運営プロダクトでは、さまざまな技術的な変遷や歴史的な経緯があり、そうした複雑なコードベースでの活用というのは業務でないと検証できないことです。 費用対効果 導入するにあたっては、やはり効果があるという事実が後押しになるかと思います。 以前 GitHub Copilot の検証時も、検証時のアンケートから費用対効果がわかり、導入へとつながっています。 今回も、Claude Code 導入に当たってかかる費用に対する効果について、目標を定めました。 ただし、短期的には効果がなかったとしても、不具合が減るなどで中長期的に効果が出る可能性も考えていました。 また、AI コーディングツール導入によって逆に生産性が下がっているという記事も見かけたので、ここについてはしっかり検証してきたいと思っていました。 www.itmedia.co.jp Max 契約するメリットがどれぐらいあるか Pro プランや 100 ドルの Max プランだと、すぐにリミットに達してしまうため、できる限り多く検証できるように 200 ドルの Max プランにしました。 また従量課金の場合、上限がないため最大の費用が予想しづらかったのも理由の 1 つです。 ccusage を使えば従量課金だった場合のコストがわかるので、どれぐらい使っていたかを調査しました。 github.com 検証結果 業務でも活用することができ、また費用対効果もあったため、引き続き使用することとなりました。 業務での活用例 長期運営プロダクトの複雑なコードベースであっても、Claude Code を十分に活用することができました。 MCP サーバーがあれば、ドキュメントやチケットの情報も参照できるため、他のツールよりも自走力が高かったです。 モバファクで使っている Backlog や DocBase はいずれも公式で MCP サーバーが用意されています。 grep で置き換えるには複雑なものも、Claude Code にさせることができ、工数を削減することができました。 たとえば、ライブラリのバージョンアップでまとめて変更が必要な場面等で活用ができました。 タスク状況にもよりますが、普段のコーディング業務の大半を Claude Code にさせることができ、1 週間ほとんどコードを自分で書かないということもありました。 時間にも余裕が生まれたため、ユーザーには影響はないものの警告のログが出ている箇所の修正や、コミットフックの改善など、第二領域的なタスクにも着手ができるようになり、将来的な負債を防ぐことにも繋がりそうです。 ただしすべての業務で活用できるわけでなく、駅奪取チームでは定常業務の効率化は難しいことがわかりました。 すでに効率化ができていたのと、Google Workspace などの MCP 連携していないツールにある情報を参照する必要のあるタスクであったりするためです。 費用対効果 当初の検証期間である 2 か月間で、費用対効果があることを確認できました。 1 スプリント内で、エンジニアの消化するストーリーポイント(SP)が増加したうえ、検証前に設定した目標 SP を超える事ができました。 興味深いのは、Claude Code の性能低下が話題になっていた時期に SP が下がっていた点です。 見積もりの誤差も考えられますが、性能低下が業務にも影響があったのではと考えています。 Max 契約するメリットがどれぐらいあるか 従量課金に比べて、コスト削減が大きく、Max プランは必須とも言えるかと思います。 多い人は、1 か月で 600 ドル相当の使用をしていました。 ネクストアクション 引き続き、業務での活用を広げていきたいと思っています。 現在、Claude Code Action の検証も行っており、昨年導入した PR Agent を代替できないかというのを考えています。 Claude Code の場合、より広い範囲のコードを参照できるため、より深いコンテキストを必要とするレビューも可能になるのではないかと考えています。 また、他チームでの Claude Code の検証開始をきっかけに、チーム横断での情報共有の場として「AI 定例」ミーティングも生まれました。 今後も Claude Code に限らず AI コーディング周りの知見を全社で高めていきたいと思っています。
概要 参加を決めた理由 聴いた講演 オープニング キーノート webpack 依存からの脱却!快適フロントエンド開発を Viteで実現する Storybook 駆動開発で実現する持続可能な Vue コンポーネント設計 昼食 生成AI時代のWebアプリケーションアクセシビリティ改善 Inside Vitest: テストフレームワークアーキテクチャの詳細解説 Vue で 3D を楽しむ Vue.jsでつくる実験映像 AI駆動で進める依存ライブラリ更新 ─ Vue プロジェクトの品質向上と開発スピード改善の実践録 ライトニングトーク アウトプットから始めるOSSコントリビューション〜eslint-plugin-vueの場合〜 個人でデジタル庁のデザインシステムをVue.jsで作っている話 React Nativeならぬ"Vue Native"が実現するかも?新世代マルチプラットフォーム開発フレームワークのLynxとLynxのVue.js対応を追ってみよう chocoZAPサービス予約システムをNuxtで内製化した話 Introducing Vite DevTools Vue Quiz スポンサーブース 懇親会 感想 概要 こんにちは、駅メモ!開発チームエンジニアの id:hayayanai です! 2025/10/25 に開催された Vue Fes Japan 2025 の参加レポートです。 講演を聴いたり会場を見て回ったりして、業務で活かせないかな〜と考えたことを書き残しています。 スライドから得られた情報のメモと、感想が混じっています🙇 入口のオブジェ Vue Fes Japan 2025 - Vue Fes Japan 2025 - タイムテーブル 参加を決めた理由 ふと技術カンファレンスに行ったことないなと思い、どんな感じなのか興味が出ました。 イベント後に公開される資料を見ることはありますが、リアルタイムで聴くのとはまた違うかなと。 そんなときにVue Fesの開催情報を見かけました。 駅メモ!のフロントエンドでVue.jsを使っているのもあり、トレンドや社外の情報を知る良い機会になると考えました。 チームのSlackで情報を共有したところ、興味のあるエンジニアが数人集まったため、皆で参加することにしました。 さらに、自身が技術広報を推進するチームのメンバーでもあるので、技術イベントでスポンサーがどんな取り組みをしているかを見たいなと思いました。 聴いた講演 オープニング カッコいいカウントダウン映像でスタート Vue製ではなく 自作レンダラ だそう 来場者が800人超えとのこと いくつか部屋があるからかそこまで人数多いとは感じなかった 全ての発表で英語と日本語で文字起こし+同時翻訳がある キーノート Babel, gulpは古いという話 プロダクトから剥がしたいなと強く思った ナウいツールたちの話 Rust, Go製のツールが強くて高速 そしてVite Plus デファクトになってほしい気持ち ただエンタープライズ向けには有料だったり 今はOSS中心のツールを利用しているわけですが、開発部分で商業ツールに依存するのはどうだろう?という雑談をした Perforceは有償ですけど業界によってはGit同様に利用されているのだから、値段次第で選択肢には入ってきそうという気持ち リリースされたら試したいけど、先に古いものの整理か アーリーアクセスちょっと気になってる webpack 依存からの脱却!快適フロントエンド開発を Viteで実現する 10年という歴史があるところも含め、自分が関わっているプロジェクトと似た課題があったような印象 MPAで100以上のページが存在 HTMLテンプレートに対してcreateAppでVueコンポーネントをマウントするところ ViteのBackground Integration など、設計的にも今後に活かせそうな部分が多いと感じた ベンチマーク指標は以下とのこと ビルド時間 開発サーバー起動時間 HMR反映時間 Storybook 駆動開発で実現する持続可能な Vue コンポーネント設計 既にストーリーを作成する運用があった状態 ストーリーを書くのを実装の前に持ってきたという変更 ストーリー書くときは、コンポーネントはスケルトンでpropsのみ メリット props ファーストで整理される 実装忘れや認識ズレが消える 表の情報と、裏のロジックが整理される 設計やデータの流れが実装前に整理されそうなのが良さげに感じた。 昼食 お弁当などは無いので近くで食べることに (お弁当が出るタイプのカンファレンスもあるということを知る) 会場のビル 内でナンとダルカレーを頼んだ 土曜日だからかレストラン全然空いてなかった 😇 ナンとダルカレー 生成AI時代のWebアプリケーションアクセシビリティ改善 WCAGについて JISにも対応がある ちょっと古いから、今から対応するならWCAG 2.2を参考にすればOK デジタル庁のガイドブック も参考になる コード生成手法の比較 指示なし Zero-Shot: 配慮するように指示 Few-Shot: 正誤コード例を提示 Self-Criticism: 生成したコードをレビューし1回修正 LLM > 人間でアクセシブルなコードが作れる 特に指示しないほうが良い結果になった Few-Shotは一番悪かった AIに悪い例与えると逆に悪いコード書くみたいなこと、たまによくある。 過剰にアクセシビリティ情報を適用して違反が増えたことも 過剰なaltとか自分も見覚えあったので共感 WAI-ARIA対応 MCPサーバーを作ったとのこと https://github.com/yamanoku/aria-validate-mcp-server Chrome DevTools MCPより、Playwright MCPのほうがアクセシビリティのチェックには現状有用 Linterでもチェック可能 eslint-plugin-vuejs-accessibility Markuplint Inside Vitest: テストフレームワークアーキテクチャの詳細解説 Vitestの中身の話 Browser Modeだと実際にブラウザ上でテストが動くのを見られる Vitest v4でVRTもできるようになったし、どんどん便利になってる印象 isolationやpoolの話 今のところデフォルト設定で速度は困ってないなと 今後高速化考えるなら、今のうちに他のオプションで動くような書き方にするのもアリかも?と思ったりした Vue で 3D を楽しむ スライドに合わせて投票ができて、画面に反映される仕組み リアクションも送ることができて、上から絵文字が降ってくる 😄 投票画面 Vueで3D扱うなら…という紹介があって、TresJSでの実際の例に進んだ Understand 3D Scenes in Vue 型など開発する上で色々問題があったけれど… glTFのオブジェクトの例 それを解決するツールキットを作ったとのこと。 GitHub - toddeTV/gltf-type-toolkit: This plugin generates type-safe glTF file representations in TypeScript and optimizes the loading and bundling of models in web projects, while being bundler-agnostic (Vite, Rollup, Webpack, esbuild, Rspack, ...). デモの中で実際にFirefox, Chromeの違いが出ていて、動作確認の重要性がわかった 影やライティングなど、細かい部分で使い方は把握する必要がありそう パフォーマンスやバンドルサイズが気になるところ 基本2Dなプロジェクトに、ちょっとリッチな表現を行うために3Dを導入するとかあり? 聴講者とコミュニケーション取れる感じの発表、流行っていく? Vue.jsでつくる実験映像 映像の制作ツールをVue(PWA)で作っている UIツールも作っている 直接Vueを使う場面もある 目的達成のための手段としてのVue(プログラミング)の例をたくさん見られた カメラ活用含め、ブラウザやnodeで触れられるAPI増えてるなという印象 映像作ると聞いてProcessingで考えが止まっていたけど、もっと色々できるなと気付かされた AI駆動で進める依存ライブラリ更新 ─ Vue プロジェクトの品質向上と開発スピード改善の実践録 プロジェクトの進め方 AI時代の新人育成 ペアプロでどんなプロンプト書いてるか見るとか AIの活かし方 など、結構盛りだくさんだった Composition API化が全120ファイルで2週間 昔一度検討して実行しなかったことがあるけど、やってみて駄目だったらブランチごと捨てるというのも良いなと思った 最近のツールなら膨大な作業も完遂しやすそう 現在Claude Code使ってるのもあって、 tsumiki フレームワークが導入しやすそうだったので取り入れたいなと思う ライトニングトーク 下記以外にもいくつか聴きましたが、気になったものを少しピックアップ アウトプットから始めるOSSコントリビューション〜eslint-plugin-vueの場合〜 きっかけって大事だなと OSSで一個バグ見つけてるの思い出したから、PR作ってみようかなと思った 個人でデジタル庁のデザインシステムをVue.jsで作っている話 React+Tailwindしか存在しないと思ってたので、Vueのみで作られてるものがあるのは学び デジタル庁のデザインシステムを使っておけば、大きな間違いは無いよなという謎の信頼を持っている React Nativeならぬ"Vue Native"が実現するかも?新世代マルチプラットフォーム開発フレームワークのLynxとLynxのVue.js対応を追ってみよう React Nativeが羨ましいと思っていたので、Vueでnativeが書けるならとても欲しい 今後が気になるところ https://github.com/rahul-vashishtha/lynx-stack/tree/lynx-vue-implementation/packages/vue chocoZAPサービス予約システムをNuxtで内製化した話 戻るボタン対応の話が、共感の嵐だった あれは大変…。 自前で履歴管理のスタックを用意したとのこと Introducing Vite DevTools 満員で入れず。そんなことあるんだ Vue Quiz 大敗北 WatchEffectとflushオプションは何かに使えるかも? 自分の経験したプロジェクトもそうだけど、reactiveじゃなくてref使ってる方が多いみたい? スポンサーブース 主に昼休憩で回ったからか結構混んでた。 時間の関係でくじ引きまでできず…。 見かけた出し物: Vueクイズ バグを見つけよう! くじ引き ウェブアプリ Xのフォローやポストで何かもらえるよ系 2次元コードだけじゃなくて、NFCタグでXアカウントに飛べるところも シール貼ってアンケート 付箋書いてボードに貼るタイプ ビラ配布 スポンサー繋がりで気付いたこととしては、企業Tシャツ着て講演聴いてる方もいらっしゃったなと。 頂いたラムネを食べながらこのドキュメントを書き上げました。 懇親会 0回戦敗退。 チケットは早めに取ろう。 Vue FesのXもフォローしたし来年は大丈夫なはず…! 感想 Viteとその周辺がトレンド スポンサーしている会社は記憶に残った 自分はインプットもアウトプット、どっちも足りてない もう少し体系的に学んでいきたい 登壇できるまでの心理的・技術的ハードルを超えていきたい 会場が東京駅周辺で便利 オフラインの技術イベント初参加だったけれど、しっかり楽しめた メモを取る端末どうしようかなという悩みは生まれた 登壇者と話す時間も確保されていて、色々質問できて良かった オンラインでも質問できることはあるけど、直接話せるのはやはり良い
駅メモ!開発基盤チームの id:xztaityozx です。 今回は駅メモ!で利用している GitHub Actions の監視について書こうと思います。 前提 駅メモ!チームでは CI/CD 環境として Amazon EC2 を用いた Self-Hosted な GitHub Actions を構築しています。Webhook をトリガーに EC2 インスタンスが起動されるため、開発者は特に意識することなく CI/CD を利用することができるようになっています。 しかしながら、ノーメンテナンスで運用できるというわけではありません。日々 EC2 で使うイメージ(AMI)や IaC の更新、ワークフローの修正・改善などを行っています。CI/CD が速いことは開発者それぞれに大きなメリットとなるため、パフォーマンス改善は特に重要なタスクです。 こういった運用を行う上で重要になるのが、GitHub Actions の監視です。ワークフローの実行時間や成功率などを継続的に監視することで、問題の早期発見や改善効果の把握が可能になります。 監視の構築 先述の通り、GitHub Actions のパフォーマンス測定のためにワークフローの実行時間、EC2 インスタンスが起動してからジョブが開始されるまでの時間、ワークフローの成功率などを収集しなければなりません。これらの情報は GitHub Actions に関する Webhook のペイロードを収集・解析することで得ることができます。例えば、 workflow_run はワークフローの実行に関する情報を提供してくれます。 // 例 { " action ": " requested ", " workflow_run ": { " id ": 123456789 , " created_at ": " 2025-05-01T12:00:00Z ", " updated_at ": " 2025-05-01T12:05:00Z ", " status ": " requested ", // ... } , } action プロパティはざっくり言うとワークフロー全体の状態を示している値です。 requested , in_progress , completed の 3 種類が存在し、それぞれの状態へ移行したときに Webhook が送られます。ここからワークフロー実行がリクエストされた時間、開始された時間、完了した時間がわかるため、ワークフローの実行時間やキューイング時間を計算できます。 他にも workflow_job イベントの workflow_job.steps プロパティから、各ステップごとにどれくらい時間がかかったかを知ることができます。 // 例 { " action ": " completed ", " workflow_job ": { " steps ": [ { " name ": " Checkout ", " status ": " completed ", " conclusion ": " success ", " number ": 1 , " started_at ": " 2025-05-01T12:00:10Z ", // 開始時間 " completed_at ": "2025-05-01T12:00:20Z" // 完了時間。↑との差分がステップの実行時間 } , { " name ": " Run tests ", " status ": " completed ", " conclusion ": " success ", " number ": 2 , " started_at ": " 2025-05-01T12:00:21Z ", " completed_at ": " 2025-05-01T12:04:50Z " } ] } } これらの情報を計算するためには、一旦ペイロードを保存し、後で解析するようにします。駅メモ!チーム内にはこのような仕組みが既にあります。それは以前の記事で紹介した開発メトリクスのダッシュボードです。 tech.mobilefactory.jp 詳しくはぜひ上記の記事を読んでいただきたいのですが、簡単に説明すると、API Gateway + Lambda + S3 でデータを収集し、EC2 上の Elasticsearch + Grafana で可視化を行うというものです。Elasticsearch へのデータ投入は EC2 上で行っています。 Elasticsearch にデータを載せてしまえばあとは好きなように可視化するだけです。作成されたダッシュボードは週 1 回のペースでチーム全体に共有され、改善点があれば都度対応しています。 まとめ 今回は GitHub Actions の監視ダッシュボードを構築している話を書きました。これにより、CI/CD のパフォーマンスを継続的に監視し、問題・異常・改善の効果を把握しやすくなりました。私自身も活用の場面がかなり増えてきています。 今後はまだ収集できていない情報(CPU, メモリ利用率など)の追加、ダッシュボードの確認の仕方など、改善を続けていきたいと思います。また何か知見があったら記事にしたいと思います。 余談 開発メトリクスダッシュボードの記事とこの記事の両方を読むと時系列がややおかしいので補足です。 実は GitHub Actions の監視ダッシュボードは開発メトリクスのダッシュボードを作成する前に作られていました。実験的に実装したものでしたが、ある程度うまく動いていたことから開発メトリクスダッシュボードの構築時に参考にされました。 今回、開発メトリクスダッシュボードの出来が良かったこと、複数のダッシュボードがあると管理が面倒ということで、古い実装を廃止し開発メトリクスダッシュボードに統合したという背景でした。
対象のエラー XcodeからアプリをApp Store Connectにアップロードする際、以下のエラーに遭遇することがあります。 Invalid XXX icon. The XXX icon in the asset catalog in 'XXX.app' can't be transparent or contain an alpha channel これは配信しようとしているアプリのアイコンに透明度情報(アルファチャンネル)が含まれている際に発生するエラーです。 このエラーはValidate Appを実行することで検知できます。 Validate Appで検知できると何が嬉しいのか 自分が所属するチームでは以下のようなリリース作業フローを採用していました。 アイコン変更 → 動作確認 → レビュー → Distribute App このフローでリリース作業をする場合、最後のDistribute Appの段階でエラーが発生するとアイコン修正をしてやり直す必要があります。 また、特定の日時にアイコン変更をリリースしたい場合、レビュー通過からDistribute Appまで期間が空き、エラーに気づく頃には時間の余裕がなくなっているかもしれません。 以下のようにアイコン変更の直後にValidate Appを実行することでエラーを事前に検知し、手戻りを防ぐことができます。 アイコン変更 → Validate App → 動作確認 → レビュー → Distribute App Validate Appの実行方法 Xcodeのメニューから Product > Archive を選択し、アプリをアーカイブする アーカイブが完了すると表示されるOrganizer画面で、対象のビルドを選択し Validate App ボタンをクリックする 表示されるダイアログに従って、Validate を実行する アイコンにアルファチャンネルが含まれている場合、この段階でエラーが検出されます。 この際、アルファチャンネル以外の項目もチェックされるため内容によっては無視しています。 例えばApp Store Connectにすでにアップロード済みのバージョンでValidate Appを実行すると以下のようなエラーが出ますが、画像のチェックだけしておきたいケースでは無視で問題ありません。 おわりに 今回はアルファチャンネルエラーがValidate Appで検知できることを紹介しました。 XcodeのValidate Appが具体的にどのようなチェックをするのか公式ドキュメントに記載されていないため、チーム内で検証した内容を記事にしてみました。 では、良いXcodeライフを!
駅奪取チームでエンジニアをしている id:kebhr です。 大きな git リポジトリで git コマンドを実行した際、OOM Killer によって git プロセスが強制終了される問題に遭遇しました。その原因と対処法について共有します。 TL;DR git maintenance の自動実行が原因で OOM が発生する場合は、以下のコマンドで無効化できます。 git config --local maintenance.auto false 背景 私たちのチームでは、画像や JS/CSS などの静的ファイルを専用の EC2 インスタンス (t3a.small) から配信しています(以降、「static インスタンス」と呼びます)。これらの静的ファイルは GitHub 上のリポジトリで管理されており、画像ファイルを含むため、リポジトリのサイズは大きくなっています。 static インスタンスは静的ファイルの配信に特化しており、通常は nginx が動作するだけなので、コストを抑えるために t3a.small(メモリ 2GB)という小さなインスタンスタイプを選択しています。 運用フローは以下の通りです: インスタンス起動時:ユーザーデータで GitHub から最新のリポジトリを git fetch 運用中:本番環境への変更を rsync で同期(git は使用しない) 問題の発生 2025 年 4 月頃から、static インスタンスの起動時に git プロセスが OOM Killer によって強制終了される事象が発生するようになりました。 Apr 9 10:05:02 ip-10-55-5-153 kernel: [ 268.544103] Out of memory: Killed process 1965 (git) total-vm:3838224kB, anon-rss:1392756kB, file-rss:536kB, shmem-rss:0kB, UID:5000 pgtables:3928kB oom_score_adj:0 原因の調査 当初、git fetch が失敗していると考えましたが、実際には git fetch は正常に完了していました。そのため、別のプロセスが原因と考えました。 開発環境で再現させながらプロセスツリーを監視したところ、git fetch の後に自動実行される git maintenance のサブプロセスが OOM の原因であることが判明しました。 2246 ? Ss 0:00 /usr/lib/git-core/git maintenance run --auto --no-quiet --detach 2247 ? S 0:00 \_ /usr/lib/git-core/git gc --auto --no-quiet --no-detach 2251 ? S 0:00 \_ /usr/lib/git-core/git repack -d -l --cruft --cruft-expiration=2.weeks.ago --keep-pack=pack-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.pack 2252 ? Sl 1:56 \_ /usr/lib/git-core/git pack-objects --local --delta-base-offset .git/objects/pack/.tmp-2251-pack --keep-true-parents --honor-pack-keep --keep-pack=pack-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.pac git maintenance について git maintenance は、リポジトリへの書き込み操作(fetch、commit、merge、rebase など)の後に自動的に実行される最適化処理です。リポジトリのパフォーマンスを維持するために、不要なオブジェクトの削除やパックファイルの再編成を行います。 しかし、大きなリポジトリでは pack-objects プロセスが大量のメモリを消費し、メモリ容量の小さいインスタンスでは OOM を引き起こす可能性があります。 対処法 git の maintenance.auto オプションを false に設定することで、自動実行を無効化できます。 git config --local maintenance.auto false この設定により、git fetch などの操作後も git maintenance は実行されなくなります。 まとめ メモリ容量が限られた環境で大きな git リポジトリを扱う場合、git maintenance の自動実行が OOM の原因となることがあります。特に、今回のケースのように「起動時に一度だけ git fetch を実行し、その後は git を使用しない」という運用では、maintenance.auto を無効化しても実用上の問題はありません。 ただし、この対処法は以下の条件下でのみ有効です: git fetch や git pull を定期的に実行しない環境 リポジトリの更新を git 以外の方法(rsync など)で行う環境 通常の開発環境や、定期的に git 操作を行う環境では、メンテナンスの無効化は推奨されません。その場合は、メモリに余裕のあるインスタンスタイプへの変更や、定期的な手動メンテナンスの実施を検討してください。
はじめに 駅奪取チームの id:konakawa です。 モバイルファクトリーでは、前年度と今年度の新卒で行う新卒同期勉強会というものがあります。 この会は参加者を何人かずつのグループに分けて行うのですが、その組み合わせについて 毎回同じ人と一緒になっている気がする 職種が偏ってしまうことがある 社会人年数も偏らないようにしたい といった偏りの問題を抱えていました。 これまでは人力で頑張って組み合わせを作っていましたが、上記条件を可能な限り満たす組み合わせを考えようとすると非常に手間がかかります。 去年は AI に組み合わせの作成をお願いしましたが、偏りの問題をうまく解決することは難しそうでした。 今回はこの会の組み合わせを CP-SAT ソルバー の力を借りて作成した話になります。 問題について この問題は Social Golfer Problem (SGP) と呼ばれる問題に非常に似ています。 SGP とは、簡単には以下のような問題です。 何人かのゴルファーが、複数週にわたって定期的にゴルフをプレイする。各週、全員がいくつかの同じ人数のグループに分かれてプレイする。このとき、同じペアのゴルファーが 2 回以上同じグループでプレイしないようなスケジュールを組みたい。 SGP は SAT (satisfiability problem, 充足可能性問題) と呼ばれる問題に帰着できることが知られており、SAT は NP 完全であることが示されています。 すなわち、SGP は 解を見つけることは多項式時間でできるかわからない 解の候補が実際に解かどうか検証することは多項式時間でできる ような問題です。 「解の候補が実際に解かどうか検証することは多項式時間でできる」ため、この「解の候補」の探し方を上手くやれば、現実的な時間で解の 1 つが見つかるかもしれません。 一方、新卒同期勉強会の組み合わせを考える問題は、これに制約を加えて拡張したものと捉えられそうです。 今年度は、満たしたい制約を以下のように定めました。 任意のペアが同じグループになる回数を均等にする グループの全員が同じ職種にならない 25 卒が 3 人以上同じグループにならない 任意の 3 人組が 2 回連続で同じグループにならない 任意の 2 人組が 3 回連続で同じグループにならない 「開催回」「参加者」「グループ」をそれぞれインデックスにもち、ある開催回である参加者があるグループに属するかどうか表す真偽値型配列 assignments[times_meeting][member][group] を用意してやれば、上記の制約は全て assignments を用いた線形な不等式で書くことができます。 例えば、任意の 3 人組が 2 連続で同じグループにならないことは # 参加者1・参加者2・参加者3が1回目と2回目の勉強会でグループ1にならない assignments[1][1][1] + assignments[1][2][1] + assignments[1][3][1] + assignments[2][1][1] + assignments[2][2][1] + assignments[2][3][1] < 6 , # 参加者1・参加者2・参加者3が1回目と2回目の勉強会でグループ2にならない assignments[1][1][2] + assignments[1][2][2] + assignments[1][3][2] + assignments[2][1][2] + assignments[2][2][2] + assignments[2][3][2] < 6 , ... のような線形不等式で表すことができます。 真偽値の変数のみからなる不等式制約を充足する解が存在するか という問題を 0-1 ILP (Integer Linear Programming) と呼びます。 任意の SAT の問題は 0-1 ILP に帰着できることが知られており、 SGP を真偽値の線形不等式制約で拡張した今回の問題は 0-1 ILP に属すると言えそうです。 0-1 ILP も NP 完全であることが知られており、すなわち上手く解の候補を探すことができれば現実的な時間で解が見つかるかもしれません。 制約プログラミング このような問題を解決するプログラミングパラダイムとして、制約プログラミング (CP, Constraint Programming) が存在します。 制約プログラミングは、問題を変数とその変数の制約という形で記述し、その制約を満たす解を見つける手法です。 記述した制約を専用のソルバーに渡すと、「上手く解の候補を探す」部分をやってくれます。 今回は Google が提供している CP-SAT ソルバー を使用しました。 CP-SAT ソルバー は C++, C#, Java, Python に提供されていて、今回は Python で制約プログラミングを行いました。 CP-SAT ソルバーを用いた制約プログラミングでは、比較的直観的に制約を記述していけます。 先ほど線形不等式の例で取り上げた、任意の 3 人組が 2 連続で同じグループにならない制約を用いて、コード例を記載します。 from ortools.sat.python import cp_model model = cp_model.CpModel() TOTAL_MEETINGS = 10 TOTAL_GROUPS = 3 GROUP_SIZE = 4 # name は仮名 MEMBERS = [ { 'name' : 'ito' , 'role' : 'planner' }, { 'name' : 'kato' , 'role' : 'developer' }, { 'name' : 'kobayashi' , 'role' : 'designer' }, { 'name' : 'nakamura' , 'role' : 'planner' }, { 'name' : 'saito' , 'role' : 'developer' }, { 'name' : 'sato' , 'role' : 'designer' }, { 'name' : 'suzuki' , 'role' : 'planner' }, { 'name' : 'takahashi' , 'role' : 'developer' }, { 'name' : 'tanaka' , 'role' : 'designer' }, { 'name' : 'watanabe' , 'role' : 'planner' }, { 'name' : 'yamada' , 'role' : 'developer' }, { 'name' : 'yoshida' , 'role' : 'designer' }, ] TOTAL_MEMBERS = len (MEMBERS) # 論理変数: assignments[meeting_idx][member_idx][group_idx] = その回でそのメンバーがそのグループにいるかどうか assignments = [[[ None for _ in range (TOTAL_GROUPS)] for _ in range (TOTAL_MEMBERS)] for _ in range (TOTAL_MEETINGS)] for meeting_idx in range (TOTAL_MEETINGS): for member_idx in range (TOTAL_MEMBERS): for group_idx in range (TOTAL_GROUPS): assignments[meeting_idx][member_idx][group_idx] = model.NewBoolVar(f 'a_{meeting_idx}_{member_idx}_{group_idx}' ) # 任意の3人組が2連続で同じグループにならない for member_i in range (TOTAL_MEMBERS): for member_j in range (member_i + 1 , TOTAL_MEMBERS): for member_k in range (member_j + 1 , TOTAL_MEMBERS): # 連続するミーティングをチェック for meeting_idx in range (TOTAL_MEETINGS - 1 ): for group_idx in range (TOTAL_GROUPS): # 今回のミーティングで3人が同じグループにいる current_meeting_trio = model.NewBoolVar(f 'trio_m{meeting_idx}_g{group_idx}_{member_i}_{member_j}_{member_k}' ) model.AddBoolAnd([ assignments[meeting_idx][member_i][group_idx], assignments[meeting_idx][member_j][group_idx], assignments[meeting_idx][member_k][group_idx] ]).OnlyEnforceIf(current_meeting_trio) model.AddBoolOr([ assignments[meeting_idx][member_i][group_idx].Not(), assignments[meeting_idx][member_j][group_idx].Not(), assignments[meeting_idx][member_k][group_idx].Not() ]).OnlyEnforceIf(current_meeting_trio.Not()) # 次回のミーティングで3人が同じグループにいる next_meeting_trio = model.NewBoolVar(f 'trio_m{meeting_idx+1}_g{group_idx}_{member_i}_{member_j}_{member_k}' ) model.AddBoolAnd([ assignments[meeting_idx + 1 ][member_i][group_idx], assignments[meeting_idx + 1 ][member_j][group_idx], assignments[meeting_idx + 1 ][member_k][group_idx] ]).OnlyEnforceIf(next_meeting_trio) model.AddBoolOr([ assignments[meeting_idx + 1 ][member_i][group_idx].Not(), assignments[meeting_idx + 1 ][member_j][group_idx].Not(), assignments[meeting_idx + 1 ][member_k][group_idx].Not() ]).OnlyEnforceIf(next_meeting_trio.Not()) # 両方が同時にTrueになることを禁止 model.AddBoolOr([ current_meeting_trio.Not(), next_meeting_trio.Not() ]) このような形で一通り制約を記述して、ソルバーに解を探してもらうと、数秒から 1 分程度の現実的な時間で上記制約を全て満たす組み合わせを見つけることができました! === 第1回 === - グループ1: yamada (developer), yoshida (designer), watanabe (planner), kato (developer) - グループ2: tanaka (designer), ito (planner), saito (developer), kobayashi (designer) - グループ3: suzuki (planner), takahashi (developer), sato (designer), nakamura (planner) === 第2回 === (以下省略) 制約の追加 この方法であれば途中で条件が変わっても、制約を追加して再度問題を解くことで解決することができます。 例えば、初回の勉強会を実施した後に、トータルの開催回数が増えたとします。 この場合、「初回の組み合わせが特定のものである」という制約を追加した上で、開催数を増やして再度プログラムを実行することで、既に開催されたものを考慮した上で全体の偏りをなくした組み合わせを見つけることができます。 また、司会やタイムキーパーの回数を均等にするなど、更に制約を追加して拡張することもできそうです。 まとめ 毎年組み合わせの作成が課題になっていた勉強会について、制約プログラミングで問題を解決し、最適な組み合わせを見つけることができました。 制約プログラミングの手法であれば、将来的な拡張も行える上、機械に組み合わせ作成を任せるので時間もかかりません。 似たような問題に遭遇したら、ぜひ制約プログラミングのことを思い出してみてください!
こんにちは、駅メモ!チームの id:charines です。 今回は駅メモ!のデータ管理におけるフィクスチャ関連の改善の事例を通じて、駅メモ!チームの改善業務への取り組みを紹介します。 課題の背景と目的 駅メモ!ではゲームに必要なマスターデータをフィクスチャファイルとして管理しており、開発環境での書き出しと本番環境での読み込みによって日々データの更新を行なっています。 しかしサービスも開始 10 年を超え、定期開催されるガチャやイベントを始めとしたデータが肥大化していき、フィクスチャの書き出しがタイムアウトするなど更新業務に支障をきたす場面が増えてきていました。 これを解決するために、今後更新予定のない古いデータをアーカイブし、読み書きの対象外とする実装を行いました。 実装 核となる実装はシンプルで、フィクスチャ管理に利用しているライブラリの処理に、管理対象のレコードを限定するような処理を追加しています。 具体的には Perl のモジュール DBIx::Fixture::Admin のクラスを継承し、アーカイブ機能のための追加の実装を行いました。 DB 上のデータを取得する処理を含むプライベートメソッド _dump_yaml において、 + my $active_record_conditions = $config->{active_record_conditions}; + if (exists $active_record_conditions->{$schema}) { + $sql .= ' WHERE ' . $active_record_conditions->{$schema}; + } my $rows = $dbh->selectall_arrayref( $sql, +{ Slice => +{} } ); のように config ファイルから取得した条件をクエリとして差し込むことで、アーカイブ範囲でないレコードのみが対象となるようにしています。 (クエリ文字列を直接連結しているためプロダクトに使うには安全でない実装ですが、今回は社内システム向けの実装なので簡易的なものにしています。) config ファイルには +{ # `テーブル名 => 条件` の形で記載する active_record_conditions => +{ # 10 と 25 は常設のガチャ gacha => '`id` >= 1234 OR `id` IN (10, 25)' , }, } のように、一定以上新しいものや常設されているガチャをクエリの形式で記載しています。 この実装によって、単純なフィクスチャファイルの書き出しでは最も重いテーブルで 7.0 秒ほどかかっていたものを 3.7 秒ほどに短縮し、より複雑な処理が伴う箇所では 10 分以上かかっていたものを数秒まで短縮することでタイムアウトの問題を解決できました。 駅メモ!チームにおける改善タスクへの取り組み 駅メモ!チームにおけるデータの更新はエンジニアよりもプランナーが中心に行うことも多く、今回の改善タスクもプランナーの困りごとを技術的に解決した例の一つでした。 このように駅メモ!チームではエンジニアでは気づきにくい業務改善を取りこぼさないようにするために、プランナーとエンジニアが一緒に参加する改善タスクを共有するための場を設けたり、時にはエンジニアが普段プランナーが行う業務を代わって行い、エンジニア視点でないと気づきにくい改善点を探すなどの取り組みをしています。 他にもプランナーの業務効率化をいつまでにどの程度達成するか具体的に目標を定めるなど、チームとして積極的にエンジニア以外が対象の業務改善も進めることで、よりスムーズにミスのないコンテンツを作成できるよう継続的に取り組んでいます。 おわりに 今回は改善タスクとして取り組んだフィクスチャファイルのアーカイブ機能実装について、具体的な実装からチームとしての改善タスクの取り組み方までを紹介させて頂きました。 この記事が読者の皆様の技術的課題の解決や、改善タスクの取り組み方への参考の一つになれば幸いです!
駅メモ!チームでエンジニアをしている id:stakHash です。 開発活動に関わるデータを収集し、開発生産性を測るためのメトリクス(便宜的に「開発メトリクス」と呼びます)を可視化するための仕組みを作りました。 その目的や設計などについてまとめました。 どんなものを作ったのか なぜ作ったのか どう作ったのか どう使っているのか まとめ どんなものを作ったのか 開発生産性を測るためのデータを簡単に収集し、可視化するためのダッシュボードです。 画像はごく一部ですが、このようなグラフの形で各種メトリクスを見ることができます。 GitHub PR 関連のダッシュボード例 2025 年 6 月 16 日現在では、以下のようなメトリクスを可視化したダッシュボードがあります。 テスト所要時間の推移や、特に時間のかかっているテスト ビルド・デプロイ所要時間の推移 コード品質の推移 なぜ作ったのか GitHub リポジトリなどをベースに開発メトリクスを集計するような SaaS もありますが、今回は内製することにしました。 理由は大きく 2 点あります。 「スモールスタートしたかった」 こと、 「既に部分的にデータが収集されていた」 ことです。 Four Keys をはじめとした開発メトリクスは広く認知を得ていますが、駅メモ!チーム内では活用事例が少ない状態でした。新機能を追加するプロジェクトの範囲で計測されることがあったり、個人がスクリプトを書いて部分的に計算したりすることはありましたが、継続的に計測されているものは限られていました。 長期的な計測を改善活動に結びつける仕組み・文化づくりが必要だったため、最初から金銭的コストをかけて SaaS を使うという選択肢は取りづらかったのです。まず MVP を作成し、必要に応じて SaaS の導入を検討しよう、という流れになりました。 そんな中、自動テストの運用に関わるメトリクスは既に継続的に計測されていました。例えば GitHub Actions がランナーを掴むまでの時間、成功率、中断率などです。 この仕組みを参考にしつつ、より包括的なデータを収集できる仕組みを作ることにしました。 どう作ったのか 構成は非常にシンプルです。全て AWS 上でホストしています。 構成図 集計対象のデータは様々なワークフローに散らばっているため、どのような環境でも送信しやすいように HTTP リクエストで受け付けます。 リクエストボディは以下のような形式です。 { " type ": " metrics-type ", " payload ": { " key1 ": " value1 ", " key2 ": " value2 ", ... } , } 受けたリクエストの type を元に分類し、まず S3 バケットに格納します。 ここではまだデータの整形や集計は行わず、 payload をほぼそのままログの形で保存します。分析が進んでいく中で、求める集計方法は変化していくことが想定されるためです。生のデータを保持しておくことで、集計方法が改善されても再集計すれば良く、対応が簡単です。 S3 バケットに集められたログを可視化する仕組みは、 Elasticsearch と Grafana を利用しました。 類似のダッシュボードツールは他にも Redash や、検索エンジンとバンドルされた OpenSearch ダッシュボード等もありますが、今回は社内で利用実績のあった Grafana にしました。 Grafana ではブラウザ上からグラフィカルにダッシュボードを作成できます。 作成したダッシュボードは JSON 形式で出力できるので、これを Git 管理し、インスタンス起動時に terraform で構成しています。 ID の類やバージョン情報には、ダッシュボードを作成・編集する際に自動で割り当てられるものもあります。これらはスクリプトを書いて JSON から削除しておくと、 terraform で扱うときに無駄な差分が減って管理しやすいです。 resource "grafana_dashboard" "dev_metrics" { for_each = toset ( [ "dashboard-name" , ... ] ) config_json = file ( "$ { path.module } /dashboard-models/$ { each.value } .json" ) } 弊社では各エンジニアに利用する開発環境として EC2 インスタンスを提供しているので、今回は簡単のため Elasticsearch と Grafana をホストする EC2 インスタンスを用意しました。 EC2 インスタンスを起動したタイミングで、 S3 バケットに蓄積したログを取得・集計し、 Elasticsearch にロードします。 前回の起動時から未集計のデータが大量に追加されていると集計に時間がかかってしまうこともありますが、現在の運用上そこまで大量に積まれないため、これで問題ありません。 どう使っているのか 週に 1 回前後 EC2 インスタンスを起動し、最新のデータを取り込んで各種メトリクスの推移を確認しています。 直近 1 週間で大きな変化はないか、もしあればその原因は何が考えられるか、すぐに改善できるものか否かなどを検討し、必要に応じて改善タスクとして登録します。 こうして登録されたタスクは、他の改善タスクと合わせて毎週優先度や担当チームが割り当てられます。 定期的にチェックすることで、例えばランダムに失敗するユニットテストを見つけて修正したり、ESLint などのルールを調整したりといった改善に繋げられています。 その他にも、何か改善を行った際の効果測定にも利用することがあります。 まとめ 開発生産性を向上させ、ひいてはプロダクトを改善すること目的に、AWS の各サービスや Grafana を利用し、開発メトリクスを集計するダッシュボードを作りました。 もし「これから集計したいけど、SaaS の導入は難しい」というようなシーンがありましたら、設計の参考になれば幸いです。
はじめに こんにちは。駅メモ!開発チームの id:k-nishioka です。今回は、駅メモ!開発チームの 1 ユニットが 5 年間にわたって取り組んできた開発手法についてお話ししたいと思います。 アジャイル開発にスクラム開発の要素を取り入れながら続けてきた運用について、 最新のスクラムガイド(2020 年版スクラムガイド)を参照しながら行った取り組みについてご紹介します。 スクラムガイド 私たちのチームでの運用方法 私たちのチームは現在 11 人のメンバーから構成されています。 スクラムガイドによるとチームサイズは通常 10 人以下が推奨されています。 今回は、そのような状況でどのようにスクラムを効果的に運用してきたかについてまとめました。 デイリースクラムの実施 まず、日々の進捗確認としてデイリースクラムを活用しています。 進行を当番制で毎朝 15 分デイリースクラムを行い、チーム全員で進捗や課題を共有しています。 水曜日のスプリント切り替えサイクル スプリントは 1 週間ごとのサイクルで運営しており、毎週水曜日は次のスプリントへの重要な切り替え日となります。この日にはまず、1 時間半のスプリントレトロスペクティブを実施します。 その後の休憩を挟んで 1 時間はスプリントプランニングに充てられます。このプロセスでは、あらかじめスクラムマスターが選定したタスクに対し、プランニングポーカーを使って見積もりを行います。 また今後の開発スケジュールに関してはスプレッドシートで管理し一覧性を高めています。これと Backlog というタスク管理ツールを併用しており、これらが自分たちのプロダクトバックログになっています。 私たちはまとまった時間でスプリントレビューを実施していません。 スプリントレビューの目的はスプリントの成果を検査し、インクリメントを提示することでフィードバックや協力を引き出し、リスクを減らしプロダクトの価値の最適化を進めることです。 スプリントレビューの時間を細かく分割してタスクとして切り出し、動作確認の時間を確保しています。 スプリントレトロスペクティブでの議題 Figjamというオンラインホワイトボードツールを利用しリアクションの多いものや各個人が優先して話したいものを議題としています。 ここでは失敗だけでなく成功例も拾い、その両方からネクストアクションを立てるように意識しています。 私たちのアジャイル開発におけるメリット 1. 責任感の向上 各メンバーが交代でデイリースクラム・レトロスペクティブの進行役を務めることで、全員が進行を理解し責任感が生まれ、スムーズな運営が可能になっています。 2. モチベーションの向上 改善ばかりが話題にあがりがちな振り返りですが、成功例を共有することでチーム全体の士気が高まり、モチベーションを向上させています。 3. ミーティング時間の短縮 チーム規模が大きくなるにつれ、ミーティング時間が増加しやすいです。 そこで定常的な業務が多い私たちのチームではスプリントプランニングなら短縮できるのではないかと考えました。 実際にスクラムマスターがプロダクトオーナーと協議を事前に行い、タスクをあらかじめ選定しておくことで時間の短縮を図っています。 4. プロジェクトリスクの減少 タスクを細かく分割し、ゴールを明確に定めたうえで進行しているため、万が一大きな問題が発生した際も早い段階で差し戻しができ、早期のリカバリーを可能としています。 各タスクのゴールについては、定常的なものに関してはモブプロなどで定期的見直し、差し込みに関してはそのフローをあらかじめ決めておくことで明確にしています。 5. 効率的な優先順位付け プロダクトバックログを用いて期限や終了要件を設定することで、タスクの前後関係やその期限が把握できるため、優先順位が明確になっています。 私たちのチームでは差し込みのタスクが発生することもあります。 差し込みタスクの対応をするために、スプリント内の落とすタスクの選択・担当の変更を素早く決定することができます。 私たちのチームでの課題 スクラム開発を通じて様々な成果を上げてきましたが、いくつかの課題にも直面しています。 ミーティング時間の増加 私たちのチームでは、レトロスペクティブを月あたり 6 時間行っています。この時間はフルリモートでのコミュニケーションを促進する貴重な機会である一方、スクラムガイドの 3 時間を超える値となり作業時間が減少する要因となっています。 そこで進行を一定期間固定することによって、振り返りの質を落とすことなく時間の短縮を図っており、現在月あたり 1 時間の短縮に成功しています。 振り返りの質の不安定さ レトロスペクティブの進行を当番制にしているため、各回の振り返りの質にばらつきが生じます。特に、経験の浅いメンバーが進行を担当する際には、学びの機会となるものの進行スキルの向上が必要とされています。 こちらも進行を一定期間固定することで学びをすぐに活かせるよう図っています。 さいごに 今回はサイズが大きいアジャイル開発の運用についてまとめてみました。 現在も改善を続けており、またよりフレームワークに沿ったスクラム開発も実施予定のため、 いずれそれも記事にできると良いかなと考えています。 少しでも読まれた方のお力になると嬉しいです!
こんにちは、駅メモ!開発チームエンジニアの id:hayayanai です! 駅メモ!のフロントエンド開発では、Linter として ESLint や Stylelint、それらの Vue 関連のプラグインを導入しています。 これらの開発支援を利用していく中で、既存の ESLint ルールのエラー文だけを変えたいという話が上がりました。 一般的なエラー文を出すよりも、チームとしての方針を明記した方が対応しやすいということです。 一から ESLint のカスタムルールを作る方法については、調べてみると既に多くの情報がありました。 一方で、既存のルールを上書きする方法の情報は少なく、実装に苦戦しました。 そこでこの記事では、既存のルールのエラー文を上書きしたカスタムルールを作る方法を紹介します。 サンプルプロジェクト 今回は例として no-magic-numbers のルールを上書きします。 エラー文を日本語で出力しつつ、どういう風に直して欲しいかを記載するように変更します。 サンプルを ESLint Online Playground で見る 環境 ESLint v9.25.1 Flat Config を使用 全体の構成 . ├── eslint.config.mjs ├── rules │ ├── index.mjs │ └── no-magic-numbers.mjs └── src └── example.js rules/index.mjs import noMagicNumbers from "./no-magic-numbers.mjs" export default { rules: { "no-magic-numbers": noMagicNumbers, }, } rules/no-magic-numbers.mjs /* 上書きしたいルールの RuleModule を取得する */ import { Linter } from "eslint" const linter = new Linter({ configType: "eslintrc" }) // "flat" だと getRules の呼び出しでエラーになる const noMagicNumbersRuleModule = linter.getRules().get("no-magic-numbers") console.log(noMagicNumbersRuleModule) /* 上書きして export する */ export default { ...noMagicNumbersRuleModule, meta: { ...noMagicNumbersRuleModule.meta, docs: { ...noMagicNumbersRuleModule.meta.docs, url: "https://tech.mobilefactory.jp/", // 社内のドキュメント等に置き換えると便利 }, messages: { ...noMagicNumbersRuleModule.meta.messages, noMagic: "マジックナンバーは禁止です: {{raw}}。`MAGIC_NUMBER_` から始まる定数を使用してください", }, }, } 参考: console.log(noMagicNumbersRuleModule) の出力結果 { meta: { type: 'suggestion', docs: { description: 'Disallow magic numbers', recommended: false, frozen: true, url: 'https://eslint.org/docs/latest/rules/no-magic-numbers' }, schema: [ [Object] ], messages: { useConst: "Number constants declarations must use 'const'.", noMagic: 'No magic number: {{raw}}.' } }, create: [Function: create] } eslint.config.mjs import myRules from "./rules/index.mjs" /** @type {import('eslint').Linter.Config[]} */ export default [ { plugins: { "my-rules": myRules, }, rules: { // "no-magic-numbers": "off", // 既存のルールが有効になっているなら off にすると良い "my-rules/no-magic-numbers": "error", }, }, ] src/example.js let a a = 3.14 a = 299792458 解説 大まかに以下のような流れでルールの上書きを実現しています。 既存のルールを上書きするために no-magic-numbers の RuleModule を取得 取得した RuleModule の中の、カスタマイズしたい部分を変更 ルールとして export eslint.config.mjs でカスタムルールを import して有効化 (必要に応じて既存のルールを off にしてエラーが重複しないようにする) ポイントとしては以下の通りです。 Flat Config を使用していれば、カスタムルールのために別途プラグインの package を作るなどの手間が無くて簡単 RuleModule の export のされ方はプラグインによって異なるので、実装を読みつつ調整する必要あり meta.messages の key( messageId ) はルール毎に異なるので、 console.log 等で確認しながら対応するとスムーズ カスタマイズ後の動作確認 コマンドラインで実行 % npx eslint src/example.js 2 : 5 error マジックナンバーは禁止です: 3.14 。 `MAGIC_NUMBER_` から始まる定数を使用してください my-rules/no-magic-numbers 3 : 5 error マジックナンバーは禁止です: 299792458 。 `MAGIC_NUMBER_` から始まる定数を使用してください my-rules/no-magic-numbers ✖ 2 problems ( 2 errors, 0 warnings) Cursor(VSCode) 上の表示 Cursor 上の出力結果 my-rules/no-magic-numbers のリンクをクリックすると、 rules/no-magic-numbers.mjs 中の url に指定したページへ飛ぶことができます。社内ドキュメントなどをリンクとして設定すると便利だと思います。 Cursor に修正を指示すると、上書きした後のエラー文を読んで適切に提案してくれました。画像は claude-3.7-sonnet での結果です。 Cursor に修正を指示した結果 まとめ ESLint v9 の Flat Config 環境で、既存のルールの検出方法をそのままに、エラー文を上書きしたカスタムルールを作る方法を紹介しました。 エラー文章に対応方針を書いておくと、どう直して欲しいかをエンジニアや AI に伝えやすくなります。 皆さんもぜひ試してみてください。 冒頭、実装時に苦戦したと書きましたが、執筆時 GPT に聞いたらそれっぽい方針を示してくれました…。今後頼りになるケースが増えそうで、ワクワクしてきますね! 参考 https://eslint.org/docs/latest/extend/custom-rules https://eslint.org/blog/2022/08/new-config-system-part-2/#from---rulesdir-to-runtime-plugins
こんにちは。駅メモエンジニアの id:kawa-mf です。 アワメモ公式サイトで、Nuxt2からNuxt3に移行しました。 しかし、Nuxt3をAWS Serverlessにデプロイする際にLambdaの制限容量を超えてしまい、ECRを利用することで解決したので、こちらについて書いていきます。 経緯 移行以前より、Lambdaの制限容量である250MB中、240MB以上を使用していました Nuxt2からNuxt3から移行するにあたり、Lambdaの制限容量である250MBを超えてしまいました 元の構成は以下のようになっていました Nuxt3へ移行するにあたり、既存のパッケージの更新や置換を行いました 結果、Lambdaの制限容量を超えてしまいました (Nuxt2の時から、残り数MBみたいな状態が続いていて、制限容量を超えるのは時間の問題でした...) 容量を圧迫した主な原因としては、依存しているパッケージの更新などです Lambdaの制限容量が超えてしまい、回避策として候補に上がったものについて AWS Serverlessの利用を中止し、AWS Amplifyに移行する AWS Serverlessのv4から有料になるので、別のものに移行したいということから、候補に上がりました しかし、弊社の運用ポリシーの観点から、使用することができませんでした Elastic Container Registryの導入 元の構成を大きく変える必要がなかった 弊社の別のプロジェクトで導入経験があった 今後の拡張性を考えて、ECRを導入することにしました ECRを導入後の構成 上記の問題を解消するためにECR (Elastic Container Registry) を導入しました 導入した結果、以下のような構成に変わりました 元々、S3にはビルド後のファイルや画像などが配置されていました その全てをECRに移したので、S3は廃止し、ECRからこれらの情報を取得するように変更しました 実際に変更したコード アワメモ公式サイトのデプロイでは元から、CodeBuildを使用していました 今回は、CodeBuildからDockerのビルドも追加で行うように変更しました 以下では、Serverless.yml、Dockerfile、buildspec.ymlの変更点について説明します Serverlessの調整 変更前 functions: handler: name: function-handler handler: .nuxt/dist/serverless.handler # memorySizeなどの他の設定 変更後 functions: handler: name: function-handler package: packageType: Image image: 'ECRのurl:latest' # latestを指定 url: true 上記には記述していないのですが、S3を使用しなくなったので、それに関連するものを削除しました 他にも、esbuildの導入、functions内のeventsやCacheBehaviorsの調整といった細かい修正は行なっています Dockerfileの追加 こちらはECRの導入にあたり、新しく作成しました nuxt.config.tsで出力先のディレクトリは、lambda-distに変更しています FROM public.ecr.aws/lambda/nodejs:18 as base RUN npm install -g yarn@1.22.21 ENV NODE_ENV $NODE_ENV # ----- builder FROM base AS builder WORKDIR /work COPY ./package.json ./yarn.lock ./ RUN yarn install # Copy remaining files for build COPY . . # Build the application RUN yarn build # ----- app FROM base WORKDIR ${LAMBDA_TASK_ROOT} COPY --from=builder /work/lambda-dist ${LAMBDA_TASK_ROOT}/lambda-dist COPY --from=builder /work/.nuxt ${LAMBDA_TASK_ROOT}/.nuxt COPY --from=builder /work/node_modules ${LAMBDA_TASK_ROOT}/node_modules CMD ["lambda-dist/server/index.handler"] buildspecの調整 一部抜粋したものです ( $ から始まるのは環境変数です) phases: pre_build: commands: - aws ecr get-login-password --region ECRを置いているリージョン | docker login --username AWS --password-stdin $ACCOUNT build: commands: - yarn - docker build -t $ECR_IMAGE_NAME . # dockerビルド - docker tag $ECR_IMAGE_NAME:$ECR_TAG $ACCOUNT/$ECR_IMAGE_NAME:$ECR_TAG # ECRへpush post_build: commands: - docker push $ACCOUNT_ID.dkr.ecr.ap-northeast-1.amazonaws.com/$ECR_IMAGE_NAME:$ECR_TAG - yarn global add serverless@3.38.0 - sls deploy --verbose - aws cloudfront create-invalidation --distribution-id $DISTRIBUTION_ID --paths "/*" まとめ AWS Amplifyは運用ポリシーの関係上、使用することができなかった ECRの導入 Lambdaの制限容量を気にする必要がなくなった 弊社で導入実績があったため、導入に際してセキュリティなど考えることが少なかった 今後は、Nuxt2からNuxt3へ移行するにあたり苦戦した点や、AWS Serverlessのv4から有料になるにあたり、対応したことについては機会があれば書こうと考えています。 ※ 一部の記述を訂正しました (2025/05/14 更新)