TECH PLAY

KINTOテクノロジーズ

KINTOテクノロジーズ の技術ブログ

969

はじめに KINTOテクノロジーズでインフラエンジニアをしているyassanです。 先日、GitHub Actionsのワークフローを意図せず大量に起動してしまい、 社内のCI/CDパイプラインを約1時間にわたって止めてしまう という事故を起こしました。 この記事では、小さなミスがどう連鎖して大きな障害になったのか、そしてそこから何を学んだのかをお話しします。 前提:コメント駆動のCI/CDパイプライン 私たちのチームでは、Terraformのインフラコードを管理するリポジトリでGitHub Actionsを活用しています。 仕組みはシンプルで、PRにコメントを投稿すると、そのPRで変更されたディレクトリを検出して自動的に terraform plan を実行してくれるというものです。 ワークフローの概要を簡略化すると、以下のようなイメージです。 name: Terraform Plan on: issue_comment: types: [created, edited] # コメントの新規作成・編集時に発火 jobs: plan: # PRへのコメントで、本文にコマンド文字列を含む場合に実行 if: | github.event.issue.pull_request && contains(github.event.comment.body, '/command') runs-on: ubuntu-latest steps: - name: PRの変更ディレクトリを検出 # ... - name: 対象ディレクトリごとに terraform plan を実行 # ... - name: 結果をPRにコメント # ... 通常であれば、PRの変更範囲は1〜2ディレクトリ程度。数分で完了する軽い処理です。 やらかしの連鎖 火種:いつもの感覚でリベースしたら、対象が35ヶ所に膨れ上がった 普段のPRは main ブランチに向けて作成しています。しかしこの日に限って、別の作業ブランチをベースにしたPRを作っていました。 ここで、いつもの癖で何も考えずにリベースを実行。すると、そのブランチにあった 他のメンバーのコミット が差分に混入してしまいました。 本来1ディレクトリだったplanの対象が、一気に 35ディレクトリ に膨れ上がりました。 延焼:消火しようとしたらガソリンだった 35ディレクトリ分のplanが走ってしまったことに気づき、「余計な結果コメントを非表示にして整理しよう」と考えました。 そこでGitHub APIを使って、不要な34件のコメントのうち20件を非表示(minimize)にしていきました。 その操作がワークフローのトリガーになるとも知らずに、非表示にするだけだと軽い気持ちで実施しました。 結果として、思いがけず20件 × 35ディレクトリ = 約700回のワークフロー が一斉に走り出しました。 種明かし:大量のトリガー GitHub APIの minimizeComment でコメントを非表示にすると、GitHub上では 「コメントの編集」イベント として扱われます。ちなみに、Web UIから手動でhideした場合はこのイベントは発生しません。 そして、非表示にしたコメントの本文には、ワークフローのトリガーとなるコマンド文字列が含まれていました。 つまり、 1件非表示にするたびに、35ディレクトリ分のplanが再び起動 してしまう状況だったのです。 graph TD A[結果コメントを非表示にする] -->|editイベント発火| B[ワークフローがコメント本文を読む] B -->|トリガー文字列を検出| C[35ディレクトリ分のplanが起動] C --> D[結果コメントが投稿される] D -->|さらに非表示にすると...| A style A fill:#ff6b6b,color:#fff style C fill:#ff6b6b,color:#fff 誤判断:PRを閉じれば止まると思った 約10分後、大量のワークフローが走っていることに気づきました。パニックになった私は「PRを閉じれば止まるはず」と考え、すぐにPRをクローズしました。 「これで大丈夫」と安心して、別の作業に戻りました。 発覚:社内から悲鳴が上がる さらに約10分後。社内のチャットに「GitHub Actionsが動かない」「CIがずっとキュー待ちになっている」という報告が上がり始めました。 慌ててGitHubを確認すると、クローズしたはずのPRに まだ結果コメントが投稿され続けていました 。 実は、PRをクローズしても 実行中のワークフローはキャンセルされません 。 それどころか、クローズされたPRに対してもコメントイベントは発火するため、PRクローズ自体にワークフローを止める効果はないのです。 これにより、共有ランナーの枠を食いつぶしてしまい、他チームのCIが動かなかったわけです。 私はすぐにGitHub Actionsの画面から、実行中のワークフローを手動で片っ端からキャンセル。ようやくキュー溜まりが解消し、社内のCI/CDが正常に戻りました。 あとから確認したところ、恐ろしいことに 約3,000分(50時間相当)のActions実行時間を、わずか1時間の間に消費していた ことがわかりました。 何が起きていたのか 今回の事故は、4つのミスが連鎖して起きました。 # やったこと 何が起きたか 1 別ブランチベースのPRでリベース 他人のコミット混入で対象35ディレクトリに膨張 2 結果コメントを非表示にして整理 非表示=編集イベント → ワークフロー再起動 × 20回 3 PRをクローズして安心 起動済みワークフローは止まらない 4 20分間気づかず放置 社内CI/CDが1時間停止 一つ一つは「ちょっとした判断ミス」や「仕様を知らなかった」程度のことですが、それが連鎖することで大きな障害になりました。 ワークフロー変更による再発防止 1. トリガー条件の見直し ワークフローのトリガーから edited (編集)イベントを削除し、 created (新規作成)のみに限定しました。これにより、コメントの編集や非表示でワークフローが起動することはなくなりました。 on: issue_comment: - types: [created, edited] + types: [created] 2. コマンド判定ロジックの厳格化 コメント本文にコマンド文字列が「含まれているか」ではなく、「先頭から始まっているか」で判定するように変更しました。さらに、イベント種別の二重チェックも追加しています。 jobs: run_plan: if: | github.event.issue.pull_request + && github.event.action == 'created' - && contains(github.event.comment.body, '/command') + && startsWith(github.event.comment.body, '/command') 3. 同時実行の制御 concurrency グループを設定し、同一PRでのワークフローの並列実行を防止しました。後から起動したワークフローが、先行するものをキャンセルして最新のplanだけが実行されるようになっています。 concurrency: group: plan-${{ github.event.issue.number }} cancel-in-progress: true 組織としての課題 今回の事故で、ワークフロー単体の修正だけでは防ぎきれない課題も見えてきました。 共有ランナーの同時実行数が急増しても気づく仕組みがない ワークフローのトリガー設計に関する共通のガイドラインがない 暴走に気づいたとき、誰がどう止めるかの手順が整備されていない これを踏まえてコーポレートITグループと連携して以下による改善を進めていきたいと考えています。 ランナー使用状況の監視強化(同時実行数がしきい値を超えた際の Slack アラート) ARMランナーやハイスペックランナーへの切り替えによる処理効率の改善 ワークフロートリガー設定のベストプラクティス策定・既存ワークフローの一括監査 この経験から学んだこと 「止めたつもり」が一番怖い。 PRを閉じればワークフローも止まると思い込んでいましたが、実際にはそうではありませんでした。慌てているときほど、思い込みで行動してしまいがちです。 ワークフローのトリガー条件は、「最悪のケース」で考える。 GitHub APIを使ったコメントの非表示は編集イベントとして扱われること、結果コメントの本文にトリガー文字列が含まれること。どちらも普段は問題にならない仕様ですが、組み合わさったときに暴走を引き起こしました。 小さなミスは連鎖する。 リベースのミス、コメント整理の操作、PRクローズへの過信、確認不足。どれか一つでも正しく対処できていれば、ここまでの事故にはなりませんでした。失敗が起きたとき、焦らずに「今何が動いているのか」を確認することが大事だと痛感しました。 おわりに 今回の事故は、自分の操作で社内の開発フローを止めてしまうという、なかなかにつらい経験でした。 ただ、この失敗をきっかけにワークフローのトリガー設計を見直し、同様の暴走が起きない仕組みに改善できました。外注開発なら責任問題になりかねない失敗も、内製開発なら改善のきっかけにできる。それがこの経験で得た一番の実感です。 この記事が、同じようなCI/CDの落とし穴を避けるための参考になれば幸いです。
アバター
はじめに こんにちは、KINTOテクノロジーズのFACTORY EC開発グループでバックエンドエンジニアをやっている、うえはら( @penpen_77777 )です。 今回はWebサービスを決められたレギュレーションの中で限界まで高速化を図るチューニングバトル「ISUCON」で得た知識を活用して、FACTORYでマスタデータ反映に1時間30分かかっていた処理をたった5分で終わらせるようにした方法についてご紹介します。 「ISUCON」は、LINEヤフー株式会社の商標または登録商標です。 ISUCON is a trademark or registered trademark of LY Corporation. https://isucon.net 今回の課題 FACTORYでは商品や車種などのマスタデータをExcelファイルに取りまとめ、 そのExcelファイルをもとに本番環境のDBにデータを反映しています(=マスタ反映)。 このマスタ反映に90分かかっており、マスタ運用作業のボトルネックになっていました。 例えば本番環境への反映の前に検証環境でマスタデータに問題ないかを確認しているのですが、 データの誤りに気づいて修正してもマスタ反映に90分かかるため、データが正しく直せたかどうかすぐに確認できない状況でした。 そこで、マスタ反映を高速化することで運用作業の効率化を図ることにしました。 マスタデータ反映 マスタ反映は、Excelで管理されているマスタデータを元に、最終的にマスタ反映コンテナがDBに書き込むという流れになっています。 上記の流れを図に示します。 図中では以下のような流れでマスタ反映が進みます。 マスタ運営担当者が、原本となるExcelファイルに車種や商品情報を入力する 出来上がったExcelファイルをマスタ管理ツールにアップロードする マスタ管理ツールがバリデーションをかけ、問題があれば担当者に通知する Excelがアップロードされると裏でLambda関数が実行され、ExcelファイルからCSVファイルに変換される DBに反映したい段階で、マスタデータをFACTORY本体に連携するため、CSVをレプリケーションバケットに保存する レプリケーションバケットにファイルが保存されるとFACTORY本体でステートマシンが起動し、マスタ反映コンテナを起動する マスタ反映コンテナがCSVを読み取ってSQLを組み立て、DBの各テーブルにレコードを読み書きする 今回高速化の対象としたのは、7のマスタ反映コンテナの処理です。 パフォーマンスチューニングをどのように進めたか追体験する 今回のマスタ反映に関するパフォーマンス問題についてどのように解決したかサンプルコードで見ていきましょう。 実際のマスタ反映処理はKotlinで記述されていますが、サンプルコードの方では筆者が慣れているGoを使います。 また、使用するマスタデータはFACTORYの実際に使われているデータではありません。 ですが、似た構造のマスタデータを使うので、実際に筆者が行ったパフォーマンスチューニングと同じ方法で高速化できます。 もしよろしければ皆さんも手を動かしながら試してみてください。 入力 ECサイトで管理している商品データを反映したいと考えてみましょう。 表では省略していますが、全部で50万件程度のデータとなります product_code 商品を一意に識別するコード product_name 商品の表示名 category_code 商品が属するカテゴリのコード supplier_code 仕入先コード status_code 商品の販売状態 unit_price 単価(円) P1001 ボールペン 黒 CAT01 SUP01 active 150 P1002 ボールペン 赤 CAT01 SUP01 active 150 P1003 シャープペンシル CAT01 SUP02 discontinued 300 P2001 A4コピー用紙 500枚 CAT02 SUP03 active 450 P2002 A3コピー用紙 500枚 CAT02 SUP03 active 780 人間にとって分かりやすいように表で示しましたが、システムにはcsvの形で入力されます。 product_code,product_name,category_code,supplier_code,status_code,unit_price P1001,ボールペン 黒,CAT01,SUP01,active,150 P1002,ボールペン 赤,CAT01,SUP01,active,150 P1003,シャープペンシル,CAT01,SUP02,discontinued,300 P2001,A4コピー用紙 500枚,CAT02,SUP03,active,450 P2002,A3コピー用紙 500枚,CAT02,SUP03,active,780 出力 入力されたデータを以下のように product テーブルに入れることにします。 category_codeやsupplier_codeやstatus_codeは外部テーブルで保持される値となるため、idに変換した上で保存されます。 外部テーブルにはすでにレコードが反映されているとします。 product_id product_code product_name category_id supplier_id status_id unit_price 1 P1001 ボールペン 黒 1 1 1 150 2 P1002 ボールペン 赤 1 1 1 150 3 P1003 シャープペンシル 1 2 2 300 4 P2001 A4コピー用紙 500枚 2 3 1 450 5 P2002 A3コピー用紙 500枚 2 3 1 780 erDiagram Product { string product_id PK "商品ID" string product_code UK "商品コード" string product_name "商品名" string category_id FK "カテゴリID" string supplier_id FK "仕入先ID" string status_id FK "ステータスID" int unit_price "単価(円)" } Category { string category_id PK "カテゴリID" string category_code UK "カテゴリコード" string category_name "カテゴリ名" } Supplier { string supplier_id PK "仕入先ID" string supplier_code UK "仕入先コード" string supplier_name "仕入先名" } Status { string status_id PK "ステータスID" string status_code UK "ステータスコード" string status_name "ステータス名" } Category ||--o{ Product : "has" Supplier ||--o{ Product : "supplies" Status ||--o{ Product : "applies" 改善前のコード サンプルコードの全体構成を以下の図に示します。 ハンズオンをサクッとできるようにテストデータの準備等の必要な作業を行ったのち、本題のマスタ反映が実行されるようになっています。testcontainersでMySQLコンテナを起動しテスト用のCSVを生成した後、main.goがそのCSVを読み取ってDBにマスタ反映を行います。 今回使用するサンプルコードを以下に示します。以下の4つのコードを同じディレクトリに配置してください。 :::details main.go (改善対象のコード) package main import ( "context" "fmt" "log" "os" "time" _ "github.com/go-sql-driver/mysql" "github.com/gocarina/gocsv" "github.com/jmoiron/sqlx" ) func main() { ctx := context.Background() // MySQLコンテナを起動 connStr, cleanup, err := startMySQLContainer(ctx) if err != nil { log.Fatal(err) } defer cleanup() db, err := sqlx.Open("mysql", connStr) if err != nil { log.Fatal(err) } defer db.Close() // テーブル・マスターデータを作成 if err := setupTables(db); err != nil { log.Fatal(err) } // サンプルCSVを生成(50万行) csvFilename := "data.csv" if err := generateSampleCSV(csvFilename, 500000); err != nil { log.Fatal(err) } // 1. CSVを読み取る file, err := os.Open(csvFilename) if err != nil { log.Fatal(err) } defer file.Close() var products []Product if err := gocsv.UnmarshalFile(file, &products); err != nil { log.Fatal(err) } fmt.Printf("CSV読み込み完了: %d 行\n", len(products)) importStart := time.Now() for i, product := range products { // 2. 読んでない行があれば1行読み取る、なければ終了 lineNum := i + 2 // 3. category_codeをcategory_idに変換 var category Category if err := db.Get( &category, `SELECT * FROM categories WHERE code = ?`, product.CategoryCode, ); err != nil { log.Fatalf("行 %d: category_code %q の検索に失敗: %v", lineNum, product.CategoryCode, err) } // 4. supplier_codeをsupplier_idに変換 var supplier Supplier if err := db.Get( &supplier, `SELECT * FROM suppliers WHERE code = ?`, product.SupplierCode, ); err != nil { log.Fatalf("行 %d: supplier_code %q の検索に失敗: %v", lineNum, product.SupplierCode, err) } // 5. status_codeをstatus_idに変換 var status Status if err := db.Get( &status, `SELECT * FROM statuses WHERE code = ?`, product.StatusCode, ); err != nil { log.Fatalf("行 %d: status_code %q の検索に失敗: %v", lineNum, product.StatusCode, err) } // 6. ProductRowに変換 row := ProductRow{ ProductCode: product.ProductCode, ProductName: product.ProductName, CategoryID: category.ID, SupplierID: supplier.ID, StatusID: status.ID, UnitPrice: product.UnitPrice, } // 7. UPDATE文を実行する result, err := db.NamedExec(` UPDATE products SET product_name = :product_name, category_id = :category_id, supplier_id = :supplier_id, status_id = :status_id, unit_price = :unit_price WHERE product_code = :product_code`, row, ) if err != nil { log.Fatalf("行 %d: productsの更新に失敗: %v", lineNum, err) } rowsAffected, err := result.RowsAffected() if err != nil { log.Fatalf("行 %d: 更新件数の取得に失敗: %v", lineNum, err) } // 8. UPDATE対象がなければINSERTする if rowsAffected == 0 { _, err = db.NamedExec(` INSERT INTO products (product_code, product_name, category_id, supplier_id, status_id, unit_price) VALUES (:product_code, :product_name, :category_id, :supplier_id, :status_id, :unit_price)`, row, ) if err != nil { log.Fatalf("行 %d: productsの登録に失敗: %v", lineNum, err) } } if (lineNum-1)%1000 == 0 { rate := float64(lineNum-1) / time.Since(importStart).Seconds() fmt.Printf("進捗: %d / %d 行 (%.0f 行/秒)\n", lineNum-1, len(products), rate) } // 9. 2に戻る } fmt.Printf("完了: %d 行 (所要時間: %v)\n", len(products), time.Since(importStart)) } ::: :::details models.go (csv, dbを操作するのに必要な構造体を定義) package main type Product struct { ProductCode string `csv:"product_code"` ProductName string `csv:"product_name"` CategoryCode string `csv:"category_code"` SupplierCode string `csv:"supplier_code"` StatusCode string `csv:"status_code"` UnitPrice int `csv:"unit_price"` } type Category struct { ID int `db:"id"` Code string `db:"code"` Name string `db:"name"` } type Supplier struct { ID int `db:"id"` Code string `db:"code"` Name string `db:"name"` } type Status struct { ID int `db:"id"` Code string `db:"code"` Name string `db:"name"` } type ProductRow struct { ProductCode string `db:"product_code"` ProductName string `db:"product_name"` CategoryID int `db:"category_id"` SupplierID int `db:"supplier_id"` StatusID int `db:"status_id"` UnitPrice int `db:"unit_price"` } ::: :::details setup.go(DB初期化・CSV生成) package main import ( "context" "encoding/csv" "fmt" "math/rand" "os" "strconv" "time" "github.com/jmoiron/sqlx" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/mysql" "github.com/testcontainers/testcontainers-go/wait" ) func startMySQLContainer(ctx context.Context) (connStr string, cleanup func(), err error) { mysqlContainer, err := mysql.Run(ctx, "mysql:8.0", mysql.WithDatabase("testdb"), mysql.WithUsername("user"), mysql.WithPassword("password"), testcontainers.WithWaitStrategyAndDeadline(3*time.Minute, wait.ForListeningPort("3306/tcp"). WithStartupTimeout(3*time.Minute), ), ) if err != nil { return "", nil, err } connStr, err = mysqlContainer.ConnectionString(ctx) if err != nil { _ = mysqlContainer.Terminate(ctx) return "", nil, err } cleanup = func() { _ = mysqlContainer.Terminate(ctx) } return connStr, cleanup, nil } func generateSampleCSV(filename string, rows int) error { file, err := os.Create(filename) if err != nil { return err } defer file.Close() writer := csv.NewWriter(file) defer writer.Flush() if err := writer.Write([]string{"product_code", "product_name", "category_code", "supplier_code", "status_code", "unit_price"}); err != nil { return err } categoryCodes := []string{"CAT01", "CAT02", "CAT03"} supplierCodes := []string{"SUP01", "SUP02", "SUP03"} statusCodes := []string{"active", "discontinued", "pending"} for i := 0; i < rows; i++ { record := []string{ fmt.Sprintf("P%d", 1000+i+1), fmt.Sprintf("商品_%d", i+1), categoryCodes[rand.Intn(len(categoryCodes))], supplierCodes[rand.Intn(len(supplierCodes))], statusCodes[rand.Intn(len(statusCodes))], strconv.Itoa(rand.Intn(10000) + 100), } if err := writer.Write(record); err != nil { return err } } return nil } func setupTables(db *sqlx.DB) error { tables := []string{ `CREATE TABLE IF NOT EXISTS categories ( id INT AUTO_INCREMENT PRIMARY KEY, code VARCHAR(10) UNIQUE NOT NULL, name VARCHAR(100) NOT NULL )`, `CREATE TABLE IF NOT EXISTS suppliers ( id INT AUTO_INCREMENT PRIMARY KEY, code VARCHAR(10) UNIQUE NOT NULL, name VARCHAR(100) NOT NULL )`, `CREATE TABLE IF NOT EXISTS statuses ( id INT AUTO_INCREMENT PRIMARY KEY, code VARCHAR(20) UNIQUE NOT NULL, name VARCHAR(100) NOT NULL )`, `CREATE TABLE IF NOT EXISTS products ( id INT AUTO_INCREMENT PRIMARY KEY, product_code VARCHAR(50) UNIQUE NOT NULL, product_name VARCHAR(255) NOT NULL, category_id INT NOT NULL, supplier_id INT NOT NULL, status_id INT NOT NULL, unit_price INT NOT NULL, FOREIGN KEY (category_id) REFERENCES categories(id), FOREIGN KEY (supplier_id) REFERENCES suppliers(id), FOREIGN KEY (status_id) REFERENCES statuses(id) )`, } for _, table := range tables { if _, err := db.Exec(table); err != nil { return err } } masterData := []string{ `INSERT IGNORE INTO categories (code, name) VALUES ('CAT01', '文房具'), ('CAT02', '食品'), ('CAT03', '電化製品')`, `INSERT IGNORE INTO suppliers (code, name) VALUES ('SUP01', '株式会社A商事'), ('SUP02', '株式会社B産業'), ('SUP03', '株式会社C物産')`, `INSERT IGNORE INTO statuses (code, name) VALUES ('active', '販売中'), ('discontinued', '販売終了'), ('pending', '販売準備中')`, } for _, data := range masterData { if _, err := db.Exec(data); err != nil { return err } } return nil } ::: :::details go.mod module csv-import-example go 1.24.5 require ( github.com/go-sql-driver/mysql v1.9.3 github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 github.com/jmoiron/sqlx v1.4.0 github.com/testcontainers/testcontainers-go v0.40.0 github.com/testcontainers/testcontainers-go/modules/mysql v0.40.0 ) require ( dario.cat/mergo v1.0.2 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v28.5.1+incompatible // indirect github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/ebitengine/purego v0.8.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.10 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/go-archive v0.1.0 // indirect github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/sys/sequential v0.6.0 // indirect github.com/moby/sys/user v0.4.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/term v0.5.0 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shirou/gopsutil/v4 v4.25.6 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/sdk v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect golang.org/x/crypto v0.43.0 // indirect golang.org/x/sys v0.38.0 // indirect google.golang.org/grpc v1.78.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ::: 高速化するためにmain.goを改善していきます。 main.goの処理の流れをまとめると以下の通りです。 csvを読み取る product_code,product_name,category_code,supplier_code,status_code,unit_price P1001,ボールペン 黒,CAT01,SUP01,active,150 P1002,ボールペン 赤,CAT01,SUP01,active,150 ... 読んでない行があれば1行読み取る、なければ終了 P1001,ボールペン 黒,CAT01,SUP01,active,150 category_codeをcategory_idに変換 SELECT * FROM categories WHERE code = 'CAT01' -- => id=1, code='CAT01', name='文房具' supplier_codeをsupplier_idに変換 SELECT * FROM suppliers WHERE code = 'SUP01' -- => id=1, code='SUP01', name='株式会社A商事' status_codeをstatus_idに変換 SELECT * FROM statuses WHERE code = 'active' -- => id=1, code='active', name='販売中' ProductRowに変換 UPDATE文を実行する UPDATE products SET product_name = 'ボールペン 黒', category_id = 1, supplier_id = 1, status_id = 1, unit_price = 150 WHERE product_code = 'P1001' UPDATE対象がなければINSERTする INSERT INTO products (product_code, product_name, category_id, supplier_id, status_id, unit_price) VALUES ('P1001', 'ボールペン 黒', 1, 1, 1, 150) 2に戻る 実行してみる まずは現状を把握するため反映にどれくらい時間がかかるかみてみましょう。 testcontainersでMySQLコンテナを起動するため、事前にDocker Desktopを起動しておいてください。 また、依存パッケージを取得するために go mod tidy を実行してから go run . を実行します。 go mod tidy go run . このコードを実行してみると以下のような実行結果が得られます。 なんとDBへの反映に47分かかってしまいました。 $ go run . CSV読み込み完了: 500000 行 進捗: 1000 / 500000 行 (338 行/秒) 進捗: 2000 / 500000 行 (329 行/秒) 進捗: 3000 / 500000 行 (320 行/秒) 進捗: 4000 / 500000 行 (326 行/秒) 進捗: 5000 / 500000 行 (328 行/秒) 進捗: 6000 / 500000 行 (328 行/秒) 進捗: 7000 / 500000 行 (329 行/秒) 進捗: 8000 / 500000 行 (328 行/秒) 進捗: 9000 / 500000 行 (319 行/秒) ... 進捗: 500000 / 500000 行 (176 行/秒) 完了: 成功 500000 行, エラー 0 行 (所要時間: 47m23.503716s) 実際のFACTORYのマスタ反映の負荷状況 実際のFACTORYでの本番環境への反映では90分もの時間がかかっていました。 FACTORY本番のRDSでの負荷を計測するため、以下にDatabase Insightsの結果を示します。 図ではクエリ別にAAS(平均アクティブセッション)が示され、AASが高い順に並んでいます。 AASが高いほどDBに負荷がかかっており、低いほどDBに負荷がかかっていないというように解釈すればokです。 赤枠がマスタ反映時に実行されているSQLになりますが、 特定のテーブルに対するSELECTの実行回数が多い(1秒あたりに200回程度実行されている) SELECTよりも負荷は小さいものの、UPDATEも同程度の頻度で実行されている このように計測の結果、マスタ反映時に叩かれるSQL、特にSELECTが原因だなというように見当をつけ、改善を進めていきました。 原因を探る これだけの時間がかかる原因を探ってみましょう。 ここではコード中で実行されるクエリに着目してみます。 実行されているクエリは以下の通りです。 # クエリ ループ中(回) 合計(回) 1 SELECT * FROM categories WHERE code = ? 1 × 50万ループ = 50万 50万 2 SELECT * FROM suppliers WHERE code = ? 1 × 50万ループ = 50万 50万 3 SELECT * FROM statuses WHERE code = ? 1 × 50万ループ = 50万 50万 4 UPDATE products SET ... WHERE product_code = ? 1 × 50万ループ = 50万 50万 5 INSERT INTO products (...) VALUES (...) 最大1 × 50万ループ = 最大50万 最大50万 合計 最大250万 最大250万 1ループあたりの実行回数は少ないですが、今回はCSVが50万行あることから50万ループ実行され、最大で合計250万クエリ実行されることになります。 実行されるクエリが多いと、インデックスを貼って単体のクエリが高速にしたとしても、ちりつもで遅くなってしまいます。 特にDBは別サーバに分離されることが多く、ネットワークの通信帯域の影響も受けてしまいます。 なので高速化の方針としては実行されるクエリをいかに削減するかということを考えれば良さそうです。 実行されるクエリを削減するためには? SELECT編 実行されるクエリを削減するにはいくつかの手段がありますが、まずはオンメモリキャッシュを取り上げてみたいと思います。 オンメモリキャッシュは、時間のかかる処理の実行結果をあらかじめメモリ上に乗っけてしまい、結果が欲しい時にはメモリ上のデータから引っ張り出すことで高速化する手法です。ISUCONでは常套手段といっても良いほど典型的なパターンです。 今回でいくと時間のかかる処理とはDBへの問い合わせにあたります。 オンメモリでキャッシュするには、キャッシュ対象のデータが、キャッシュ中に書き換えられないほうが実装しやすいです。 キャッシュ中に実データに書き込みがある場合、キャッシュを書き込みに追随させるためデータの更新が必要になります。排他制御を考慮する必要があり、実装が困難になります。 productsテーブルを更新する際にはcategories, suppliers, statusesテーブルはすでに更新が完了しており、書き込みはありません。なのでproductsテーブルを更新する前にキャッシュしておけば問題なさそうです。 ということで先ほどのコードにキャッシュ処理を加えます。 CSV読み取り直後にSELECTを行い全件をメモリ上に載せます。 code→IDへ高速にデータを引きたいので、スライスではなくここでは map[string]int に載せてあげます。map型はキーにひもづくデータの取得で$O(1)$の計算量で高速にデータを引くことができます。 fmt.Printf("CSV読み込み完了: %d 行\n", len(products)) + // マスターデータをmapに読み込み(code → id) + var categories []Category + if err := db.Select(&categories, "SELECT * FROM categories"); err != nil { + log.Fatal(err) + } + categoryMap := make(map[string]int, len(categories)) + for _, c := range categories { + categoryMap[c.Code] = c.ID + } code→IDが欲しいタイミングで、先ほど定義したmap型の変数を使うように書き換えます // 3. category_codeをcategory_idに変換 - var category Category - if err := db.Get(&category, "SELECT * FROM categories WHERE code = ?", product.CategoryCode); err != nil { - log.Printf("行 %d: category変換エラー: %v", i+2, err) + categoryID, ok := categoryMap[product.CategoryCode] + if !ok { + log.Printf("行 %d: category変換エラー: code %q が見つかりません", i+2, product.CategoryCode) errorCount++ continue } 他の修正も加えると以下のような差分になります。 :::details オンメモリキャッシュ化の全体差分 diff --git a/main.go b/main.go index c3705d8..c3c16cf 100644 --- a/main.go +++ b/main.go @@ -52,6 +52,34 @@ func main() { } fmt.Printf("CSV読み込み完了: %d 行\n", len(products)) + // マスターデータをmapに読み込み(code → id) + var categories []Category + if err := db.Select(&categories, "SELECT * FROM categories"); err != nil { + log.Fatal(err) + } + categoryMap := make(map[string]int, len(categories)) + for _, c := range categories { + categoryMap[c.Code] = c.ID + } + + var suppliers []Supplier + if err := db.Select(&suppliers, "SELECT * FROM suppliers"); err != nil { + log.Fatal(err) + } + supplierMap := make(map[string]int, len(suppliers)) + for _, s := range suppliers { + supplierMap[s.Code] = s.ID + } + + var statuses []Status + if err := db.Select(&statuses, "SELECT * FROM statuses"); err != nil { + log.Fatal(err) + } + statusMap := make(map[string]int, len(statuses)) + for _, s := range statuses { + statusMap[s.Code] = s.ID + } + importStart := time.Now() for i, product := range products { @@ -59,41 +87,29 @@ func main() { lineNum := i + 2 // 3. category_codeをcategory_idに変換 - var category Category - if err := db.Get( - &category, - `SELECT * FROM categories WHERE code = ?`, - product.CategoryCode, - ); err != nil { - log.Fatalf("行 %d: category_code %q の検索に失敗: %v", lineNum, product.CategoryCode, err) + categoryID, ok := categoryMap[product.CategoryCode] + if !ok { + log.Fatalf("行 %d: category_code %q の検索に失敗", lineNum, product.CategoryCode) } // 4. supplier_codeをsupplier_idに変換 - var supplier Supplier - if err := db.Get( - &supplier, - `SELECT * FROM suppliers WHERE code = ?`, - product.SupplierCode, - ); err != nil { - log.Fatalf("行 %d: supplier_code %q の検索に失敗: %v", lineNum, product.SupplierCode, err) + supplierID, ok := supplierMap[product.SupplierCode] + if !ok { + log.Fatalf("行 %d: supplier_code %q の検索に失敗", lineNum, product.SupplierCode) } // 5. status_codeをstatus_idに変換 - var status Status - if err := db.Get( - &status, - `SELECT * FROM statuses WHERE code = ?`, - product.StatusCode, - ); err != nil { - log.Fatalf("行 %d: status_code %q の検索に失敗: %v", lineNum, product.StatusCode, err) + statusID, ok := statusMap[product.StatusCode] + if !ok { + log.Fatalf("行 %d: status_code %q の検索に失敗", lineNum, product.StatusCode) } row := ProductRow{ ProductCode: product.ProductCode, ProductName: product.ProductName, - CategoryID: category.ID, - SupplierID: supplier.ID, - StatusID: status.ID, + CategoryID: categoryID, + SupplierID: supplierID, + StatusID: statusID, UnitPrice: product.UnitPrice, } ::: DBに問い合わせる代わりにメモリ上のキャッシュにデータを問い合わせるため、 SELECTの150万回分がなくなり、残りのUPDATE/INSERTの最大100万回にまで削減できました。 # クエリ ループ前(回) ループ中(回) 合計(回) 1 SELECT * FROM categories 1 0 1 2 SELECT * FROM suppliers 1 0 1 3 SELECT * FROM statuses 1 0 1 4 UPDATE products SET ... WHERE product_code = ? 0 1 × 50万ループ = 50万 50万 5 INSERT INTO products (...) VALUES (...) 0 最大1 × 50万ループ = 最大50万 最大50万 合計 3 最大100万 最大100万3 これでどれくらい高速化できたか見てみましょう。 CSV読み込み完了: 500000 行 進捗: 1000 / 500000 行 (282 行/秒) 進捗: 2000 / 500000 行 (302 行/秒) 進捗: 3000 / 500000 行 (330 行/秒) 進捗: 4000 / 500000 行 (360 行/秒) 進捗: 5000 / 500000 行 (378 行/秒) (略) 進捗: 496000 / 500000 行 (409 行/秒) 進捗: 497000 / 500000 行 (409 行/秒) 進捗: 498000 / 500000 行 (409 行/秒) 進捗: 499000 / 500000 行 (407 行/秒) 進捗: 500000 / 500000 行 (405 行/秒) 完了: 成功 500000 行, エラー 0 行 (所要時間: 20m35.34731075s) 以上のように時間を半減させることができました。 INSERT/UPDATE編 SELECTの実行回数は削減できましたが、まだ100万回ものSQLが実行されています。 残りのINSERT/UPDATEの高速化にチャレンジしてみます。 INSERT/UPDATEの実行回数を削減する手段としてはupsertに変更することが挙げられます。 UPSERTとは UPSERTとはINSERTとUPDATEを組み合わせた単語で、INSERT時に対象レコードが存在しない場合はINSERTと、すでに存在する場合はUPDATEをかける処理です。 MySQLではINSERT ON DUPLICATE KEY UPDATEとREPLACE構文が使えますが、今回は前者の構文を使ってみます。 今回でいくと以下のUPDATE文を実行し、 UPDATE products SET product_name = ?, category_id = ?, supplier_id = ?, status_id = ?, unit_price = ? WHERE product_code = ? UPDATE対象が存在しなければINSERTを行っています。 INSERT INTO products ( product_code, product_name, category_id, supplier_id, status_id, unit_price ) VALUES (?, ?, ?, ?, ?, ?) INSERT ON DUPLICATE KEY UPDATEを使用すると2つのクエリを1つにまとめることができます。 INSERT INTO products ( product_code, product_name, category_id, supplier_id, status_id, unit_price ) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE product_name = VALUES(product_name), category_id = VALUES(category_id), supplier_id = VALUES(supplier_id), status_id = VALUES(status_id), unit_price = VALUES(unit_price) これだけで100万回→50万回までクエリの実行回数を削減できます。 # クエリ ループ前(回) ループ中(回) 合計(回) 1 SELECT * FROM categories 1 0 1 2 SELECT * FROM suppliers 1 0 1 3 SELECT * FROM statuses 1 0 1 4 INSERT INTO products (...) ON DUPLICATE KEY UPDATE ... 0 1 × 50万ループ = 50万 50万 合計 3 50万 50万3 コードでは以下のように修正しています :::details UPSERT化の差分 diff --git a/main.go b/main.go index c3c16cf..0da4db0 100644 --- a/main.go +++ b/main.go @@ -113,36 +113,23 @@ func main() { UnitPrice: product.UnitPrice, } - // 7. UPDATE文を実行する - result, err := db.NamedExec(` - UPDATE products - SET product_name = :product_name, - category_id = :category_id, - supplier_id = :supplier_id, - status_id = :status_id, - unit_price = :unit_price - WHERE product_code = :product_code`, + // 7. UPSERT(INSERT or UPDATE)を実行する + _, err := db.NamedExec(` + INSERT INTO products ( + product_code, product_name, category_id, supplier_id, status_id, unit_price + ) VALUES ( + :product_code, :product_name, :category_id, :supplier_id, :status_id, :unit_price + ) + ON DUPLICATE KEY UPDATE + product_name = VALUES(product_name), + category_id = VALUES(category_id), + supplier_id = VALUES(supplier_id), + status_id = VALUES(status_id), + unit_price = VALUES(unit_price)`, row, ) if err != nil { - log.Fatalf("行 %d: productsの更新に失敗: %v", lineNum, err) - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - log.Fatalf("行 %d: 更新件数の取得に失敗: %v", lineNum, err) - } - - // 8. UPDATE対象がなければINSERTする - if rowsAffected == 0 { - _, err = db.NamedExec(` - INSERT INTO products (product_code, product_name, category_id, supplier_id, status_id, unit_price) - VALUES (:product_code, :product_name, :category_id, :supplier_id, :status_id, :unit_price)`, - row, - ) - if err != nil { - log.Fatalf("行 %d: productsの登録に失敗: %v", lineNum, err) - } + log.Fatalf("行 %d: productsのUPSERTに失敗: %v", lineNum, err) } if (lineNum-1)%1000 == 0 { ::: 実行してみましょう。 CSV読み込み完了: 500000 行 進捗: 1000 / 500000 行 (636 行/秒) 進捗: 2000 / 500000 行 (642 行/秒) 進捗: 3000 / 500000 行 (658 行/秒) 進捗: 4000 / 500000 行 (661 行/秒) 進捗: 5000 / 500000 行 (652 行/秒) (略) 進捗: 497000 / 500000 行 (650 行/秒) 進捗: 498000 / 500000 行 (650 行/秒) 進捗: 499000 / 500000 行 (650 行/秒) 進捗: 500000 / 500000 行 (650 行/秒) 完了: 成功 500000 行, エラー 0 行 (所要時間: 12m48.924974166s) この修正だけで10分程度まで早くすることができました。 bulk化する upsertに変更して50万回までSQLの実行回数を削減できました。 さらにSQLの実行回数を削減するためにSQLをbulk化してみます。 bulk化とはDBに対して複数のレコードに対する操作を1つのSQLにまとめて実行することを言います。 以下のUPSERT化したSQLはいまだ50万回叩かれています。 INSERT INTO products ( product_code, product_name, category_id, supplier_id, status_id, unit_price ) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE product_name = VALUES(product_name), category_id = VALUES(category_id), supplier_id = VALUES(supplier_id), status_id = VALUES(status_id), unit_price = VALUES(unit_price) このSQLを1行ずつ入れていくのではなく、ある程度のレコード数で固めてから送ることで SQLの実行回数を減らせるわけです。 今回は1000レコード分ずつSQLをまとめて送ることにしてみましょう。 すると500000/1000=500回までSQLの実行回数を削減できます。 # クエリ ループ前(回) ループ中(回) 合計(回) 1 SELECT * FROM categories 1 0 1 2 SELECT * FROM suppliers 1 0 1 3 SELECT * FROM statuses 1 0 1 4 INSERT INTO products (...) VALUES (...), (...), ... ON DUPLICATE KEY UPDATE ... 0 50万ループ / 1000 = 500 500 合計 3 500 503 どれくらい固めるかを表す数値をバッチサイズと呼びますが、この場合バッチサイズは1000となります。 :::details バルクUPSERT化の差分 diff --git a/main.go b/main.go index 0da4db0..daf2689 100644 --- a/main.go +++ b/main.go @@ -80,8 +80,8 @@ func main() { statusMap[s.Code] = s.ID } - importStart := time.Now() - + // code → id 変換してProductRowスライスを構築 + var rows []ProductRow for i, product := range products { // 2. 読んでない行があれば1行読み取る、なければ終了 lineNum := i + 2 @@ -104,16 +104,29 @@ func main() { log.Fatalf("行 %d: status_code %q の検索に失敗", lineNum, product.StatusCode) } - row := ProductRow{ + // 6. ProductRowに変換 + rows = append(rows, ProductRow{ ProductCode: product.ProductCode, ProductName: product.ProductName, CategoryID: categoryID, SupplierID: supplierID, StatusID: statusID, UnitPrice: product.UnitPrice, + }) + } + fmt.Printf("変換完了: %d 行\n", len(rows)) + + // バルクUPSERT(1000行ずつ) + const batchSize = 1000 + importStart := time.Now() + + for i := 0; i < len(rows); i += batchSize { + end := i + batchSize + if end > len(rows) { + end = len(rows) } + batch := rows[i:end] - // 6. UPSERT(INSERT or UPDATE)を実行する _, err := db.NamedExec(` INSERT INTO products ( product_code, product_name, category_id, supplier_id, status_id, unit_price @@ -126,17 +139,16 @@ func main() { supplier_id = VALUES(supplier_id), status_id = VALUES(status_id), unit_price = VALUES(unit_price)`, - row, + batch, ) if err != nil { - log.Fatalf("行 %d: productsのUPSERTに失敗: %v", lineNum, err) + log.Fatalf("バッチ %d-%d: UPSERTに失敗: %v", i+1, end, err) } - if (lineNum-1)%1000 == 0 { - rate := float64(lineNum-1) / time.Since(importStart).Seconds() - fmt.Printf("進捗: %d / %d 行 (%.0f 行/秒)\n", lineNum-1, len(products), rate) + if end%10000 == 0 || end == len(rows) { + rate := float64(end) / time.Since(importStart).Seconds() + fmt.Printf("進捗: %d / %d 行 (%.0f 行/秒)\n", end, len(rows), rate) } - // 8. 2に戻る } fmt.Printf("完了: %d 行 (所要時間: %v)\n", len(products), time.Since(importStart)) ::: では実行してみましょう。 CSV読み込み完了: 500000 行 変換完了: 500000 行 (エラー 0 行) 進捗: 10000 / 500000 行 (56843 行/秒) 進捗: 20000 / 500000 行 (72234 行/秒) 進捗: 30000 / 500000 行 (78721 行/秒) 進捗: 40000 / 500000 行 (73047 行/秒) 進捗: 50000 / 500000 行 (76230 行/秒) 進捗: 60000 / 500000 行 (78932 行/秒) 進捗: 70000 / 500000 行 (81193 行/秒) (略) 進捗: 460000 / 500000 行 (83997 行/秒) 進捗: 470000 / 500000 行 (83998 行/秒) 進捗: 480000 / 500000 行 (84197 行/秒) 進捗: 490000 / 500000 行 (83433 行/秒) 進捗: 500000 / 500000 行 (83642 行/秒) 完了: 成功 500000 行, エラー 0 行 (所要時間: 5.977838667s) わずか6秒程度で完了するようになりました! 元々50分かかっていた処理だと考えると、かなり高速化されたのではないかと思います。 改善後の実際のFACTORYでのDBの負荷状況 改善の結果を先述のDatabase InsightsのAASで確認してみましょう。 赤枠がマスタ反映時に実行されているSQLになりますが、 改善前に負荷がかかっているSQLとして挙げられていたSELECTがなくなって、ボトルネックを解消した INSERTはまだいるが実行回数が減り、AASも減った このように実際のFACTORYのDBの計測からも負荷が減ったことがわかります。 この改善の結果、5分程度で反映が終わるようになりました! 改善前は90分かかっていたと考えるとめちゃくちゃ高速化できました! まとめ 今回の改善の変遷をまとめると以下の通りです。 ステップ 施策 所要時間 SQL実行回数(最大) 改善前 - 47分 250万回 1. オンメモリキャッシュ SELECTをメモリ参照に置換 20分 100万回 2. UPSERT化 UPDATE+INSERTを1クエリに統合 13分 50万回 3. バルクUPSERT化 1000行ずつまとめて実行 6秒 500回 パフォーマンスチューニングでとった方法はどれもISUCONではよく出てくる典型的な対応策です。 まさかISUCONで培った知識を使って業務でこれほどまでの結果を出せるとは思いもしませんでした。 ISUCONは業務でも役に立ちます。 これからもISUCONで腕を磨きつつ、業務でのボトルネックを改善していきたいと考えています。
アバター
はじめに はじめまして。 KINTO テクノロジーズで KINTO Unlimited Android アプリを開発している JR.Liang です。 本記事では、KINTO Unlimited アプリにて提供する「これなにガイド」スキャン機能の AR エフェクトについて、Android における技術的な検証を紹介します。 特に MediaPipe のソリューションを用いて幅広い Android デバイスで AR エフェクトを実現した実装にフォーカスします。 これなにガイドとは 「これなにガイド」は AR(拡張現実)を活用して、車内スイッチの用途や使い方をテキストと動画で案内する機能です。紹介動画をご覧ください。 https://youtube.com/watch?v=E8zfNzuHr7g&embeds_referring_euri=https%3A%2F%2Fcorp.kinto-jp.com%2F&source_ve_path=MjM4NTE 上記の紹介動画は iOS アプリでの動作を示しています。スイッチ上に表示された黄色の丸 🟡 が、AR 技術で実現した仮想コンテンツです。 機能全体の仕組みは以下の流れです。本記事では 3 番目(描画)に関する内容を扱います。 1. アプリのカメラを起動、カメラ画像を取得 2. 機械学習における物体認識を用いて、車内のスイッチを検出 3. 検出した座標を元に、ボタンとテキストをフレーム上に描画 4. ボタンをタップして、当該スイッチのテキストと動画を表示 Android AR 技術検証の経緯 当初の Android 版「これなにガイド」のスキャン機能では、Canvas を利用して毎フレーム検出される座標に描画する実装でした。そのため検出の時間差により、スマホ(カメラ)を動かすと描画のズレが生じていました。 2D Canvas 幸い、MediaPipe のソリューションである Instant Motion Tracking モジュールで 素早くかつ安定した AR エフェクトを実現できることがわかり、Android への導入を検証しました。 3D OpenGL MediaPipe Instant Motion Tracking MediaPipe は Google が開発したオープンソースの ML フレームワークで、顔検出・手のトラッキング・姿勢推定などリアルタイム映像処理のソリューションを提供します。 その中の Instant Motion Tracking は、現実世界のシーン上に 3D 仮想コンテンツをリアルタイムで正確に配置できる AR トラッキング機能です。初期化や厳密なキャリブレーションが不要で、静止面や動いている面の上にコンテンツを置くことが可能です。 @ card Android + MediaPipe AR アーキテクチャ graph TB A(Android CameraX) --> |Camera Frame| B(Instant Motion Tracking) B --> |Camera Image| C(TensorFlow Object Detection) C --> |Detections Information| B(Instant Motion Tracking) B --> |Output Stream| D(Android Surface Rendering) CameraX で取得したフレームを Instant Motion Tracking に渡し、TensorFlow Lite で物体検出した情報を元に AR コンテンツを描画・追従させるパイプラインです。 MediaPipe ライブラリの作成 MediaPipe では Bazel を使用してパッケージをビルドします。Android に適合する AAR として書き出してアプリに組み込みます。 https://chuoling.github.io/mediapipe/getting_started/android_archive_library.html AAR をビルドする BUILD ファイルを作成し、 instant_motion_tracking を基盤とした定義を記述します。 load("//mediapipe/java/com/google/mediapipe:mediapipe_aar.bzl", "mediapipe_aar") mediapipe_aar( name = "mediapipe_ar", calculators = ["//mediapipe/graphs/instant_motion_tracking:instant_motion_tracking_deps"] ) MediaPipe は C++ が中核のため、C++ ランタイムである libc++_shared.so を AAR に同梱する必要があります。 https://github.com/google-ai-edge/mediapipe/blob/v0.10.32/third_party/BUILD#L399-L403 また Instant Motion Tracking では画像処理ライブラリ OpenCV を利用し、AR トラッキングを行います。 https://github.com/google-ai-edge/mediapipe/blob/v0.10.32/WORKSPACE#L649-L655 上記サードパーティのライブラリを含めて、以下のコマンドで AAR をビルドします。 bazel build -c opt --strip=ALWAYS \ --host_crosstool_top=@bazel_tools//tools/cpp:toolchain \ --fat_apk_cpu=arm64-v8a \ --linkopt=-Wl,-z,max-page-size=16384 \ //path/to/the/aar/build/mediapipe_ar:mediapipe_ar.aar 市場に流通している Android デバイスは主に arm64-v8a アーキテクチャのため、AAR のサイズを抑える目的で fat_apk_cpu=arm64-v8a にします。 C++ ライブラリの 16KB page-size に対応するため、 max-page-size=16384 を追加します。 また AAR を利用するにはグラフ構造を定義するファイル( binarypb )が必要です。 bazel build -c opt mediapipe/graphs/instant_motion_tracking:instant_motion_tracking.binarypb Instant Motion Tracking の導入 AAR をアプリに組み込んで、Android 側の実装を解説していきます。 下記は AAR に組み込んだ instant_motion_tracking の全体構造です。 instant_motion_tracking.pbtxt の構成 グラフ定義ファイル instant_motion_tracking.pbtxt は、Calculator(処理ノード)・入出力ストリーム・サイドパケットの 3 要素で構成されます。 Calculator 各 Calculator がパイプライン上でどの処理を担うかを示します。 Calculator 役割 ImageTransformationCalculator カメラフレームを 320×320(FIT)にリサイズ。物体検出モデルの入力サイズに合わせる GpuBufferToImageFrameCalculator GPU テクスチャを CPU の ImageFrame に変換。TensorFlow Lite 推論に使用 StickerManagerCalculator Sticker Proto をパースし、初期アンカーの座標・回転・スケール・レンダリング種別に分解 RegionTrackingSubgraph ボックストラッキングでアンカー位置を追従。内部に TrackedAnchorManagerCalculator (アンカー管理)と BoxTrackingSubgraphGpu (GPU トラッキング)を持つ MatricesManagerCalculator トラッキング結果・回転・スケール・FOV・アスペクト比から OpenGL 用 4×4 モデル行列を生成 GlAnimationOverlayCalculator モデル行列とテクスチャを用いて、元のカメラフレーム上に AR コンテンツを OpenGL で描画し output_video として出力 input_stream / output_stream input_stream はフレームごとに Android 側から送信するデータ、 output_stream はグラフの処理結果です。 ストリーム名 C++ 型 方向 用途 input_video GpuBuffer Input カメラフレーム sticker_proto_string String(Serialized Proto) Input ステッカーの座標・スケール等(Sticker Proto) sticker_sentinels vector Input 座標をリセットするステッカー ID の配列 gif_textures vector Input AR コンテンツの Bitmap テクスチャ配列 gif_aspect_ratios vector Input 各テクスチャのアスペクト比 output_video GpuBuffer Output AR 描画済みフレーム input_side_packet input_side_packet は初期化時に一度だけ渡す定数で、グラフ実行中は変化しません。 パケット名 用途 vertical_fov_radians カメラの垂直 FOV(ラジアン) aspect_ratio カメラのアスペクト比 width / height カメラ解像度 gif_texture デフォルトテクスチャ(1x1 プレースホルダ) gif_asset_name AR テクスチャ描画用のポリゴンメッシュ( .obj )ファイル名 Android への導入に当たって、公式サンプルのコードを参考にします。 https://github.com/google-ai-edge/mediapipe/tree/master/mediapipe/examples/android/src/java/com/google/mediapipe/apps/instantmotiontracking 1. 初期化 MediaPipe を使用する前に、ネイティブライブラリの読み込みとアセットマネージャーの初期化が必要です。 companion object { init { System.loadLibrary("mediapipe_jni") System.loadLibrary("opencv_java4") } } // onCreate 相当の処理 AndroidAssetUtil.initializeNativeAssetManager(context) mediapipe_jni : MediaPipe のコア処理を行う JNI ライブラリ opencv_java4 : AR トラッキングに使用する OpenCV ライブラリ initializeNativeAssetManager : ネイティブコードからアセット(binarypb 等)にアクセスするために必要 2. カメラを起動する 公式サンプルを参考に、以下の順序でパイプラインを構築します。 データフロー: CameraX → ExternalTextureConverter → FrameProcessor → SurfaceView 2.1 EGL 環境と FrameProcessor の初期化 val eglManager = EglManager(null) val frameProcessor = FrameProcessor( context, eglManager.nativeContext, "instant_motion_tracking.binarypb", "input_video", "output_video" ).apply { videoSurfaceOutput.setFlipY(true) setInputSidePackets( mapOf( "gif_asset_name" to packetCreator.createString("gif.obj.uuu"), "vertical_fov_radians" to packetCreator.createFloat32(fovRadians), "aspect_ratio" to packetCreator.createFloat32(resolution.width.toFloat() / resolution.height.toFloat()), "width" to packetCreator.createInt32(resolution.width), "height" to packetCreator.createInt32(resolution.height), "gif_texture" to packetCreator.createRgbaImageFrame(createBitmap(1, 1)) ) ) } EglManager : OpenGL ES の EGL コンテキストを作成・管理。MediaPipe のグラフ内 GPU Calculator( GlAnimationOverlayCalculator 等)が OpenGL で描画するために必要 FrameProcessor : EGL コンテキストを受け取り、グラフの読み込み・入出力ストリームの管理・フレームごとのグラフ実行を行う instant_motion_tracking.binarypb : .pbtxt を Bazel でコンパイルしたグラフ定義バイナリ input_video : MediaPipe グラフへカメラフレームを入力 output_video : グラフで処理(AR 描画など)された映像を出力 videoSurfaceOutput.setFlipY(true) : OpenGL とカメラの Y 軸方向が逆のため、出力映像を上下反転して正しい向きにする setInputSidePackets : グラフの input_side_packet に対応する定数をまとめて設定。カメラの FOV・アスペクト比・解像度など、グラフ実行中に変化しない値を初期化時に一度だけ渡す gif_asset_name は AR テクスチャを描画するための ポリゴンメッシュ(頂点データ) 、ここでは公式サンプルの gif.obj.uuu を利用 2.2 カメラ映像の変換パイプライン構築 val externalTextureConverter = ExternalTextureConverter(eglManager.context, 2).apply { setFlipY(true) setConsumer(frameProcessor) setDestinationSize(resolution.width, resolution.height) } val cameraHelper = object : CameraXPreviewHelper() { override fun getCameraCharacteristics(context: Context?, lensFacing: Int?) = cameraCharacteristics }.apply { setOnCameraStartedListener(onCameraStartedListener) startCamera( context, lifecycleOwner, CameraHelper.CameraFacing.BACK, externalTextureConverter.surfaceTexture, Size(resolution.height, resolution.width) ) } ExternalTextureConverter : カメラの GL_EXTERNAL_OES テクスチャを MediaPipe が処理できる標準テクスチャに変換 setFlipY(true) : カメラ映像の上下反転を補正 setDestinationSize(resolution.width, resolution.height) : パイプラインの処理サイズはポートレート座標(例: 960×1280 )で指定 CameraXPreviewHelper : CameraX でバックカメラを起動し、Converter の SurfaceTexture に出力 startCamera(targetSize = Size(resolution.height, resolution.width)) : CameraX はセンサー座標(ランドスケープ)を期待するため、width と height を入れ替えて渡す 公式サンプルでは CameraXPreviewHelper をそのまま使用し、内部で CameraManager からカメラ特性を取得します。 https://github.com/google-ai-edge/mediapipe/blob/v0.10.32/mediapipe/java/com/google/mediapipe/components/CameraXPreviewHelper.java#L558-L560 本実装では getCameraCharacteristics をオーバーライドし、事前に取得済みの CameraCharacteristics を直接渡します。これにより FOV やアスペクト比の算出に使うカメラ情報を、アプリ側で一元管理できます。 2.3 出力先SurfaceViewの設定 SurfaceView(context).apply { holder.addCallback(object : SurfaceHolder.Callback { override fun surfaceCreated(holder: SurfaceHolder) { frameProcessor.videoSurfaceOutput.setSurface(holder.surface) } override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { val displaySize = cameraHelper.computeDisplaySizeFromViewSize(Size(width, height)) val (displayWidth, displayHeight) = if (cameraHelper.isCameraRotated) { displaySize.height to displaySize.width } else { displaySize.width to displaySize.height } externalTextureConverter.setDestinationSize(displayWidth, displayHeight) } override fun surfaceDestroyed(holder: SurfaceHolder) { frameProcessor.videoSurfaceOutput.setSurface(null) } }) } SurfaceHolder.Callback : SurfaceView のライフサイクルに応じて FrameProcessor の出力先を管理 surfaceCreated : FrameProcessor の出力先として Surface を設定 surfaceChanged : 画面回転・サイズ変更時に出力解像度を調整 surfaceDestroyed : リソース解放 3. 検出座標をグラフに渡す 物体検出(TensorFlow Lite 等)で得られた座標を MediaPipe グラフに渡し、AR コンテンツを配置します。 3.1 グラフから変換済み画像を取得 MediaPipe グラフ内で ImageTransformationCalculator と GpuBufferToImageFrameCalculator によって変換された画像を addPacketCallback で受け取り、物体検出に使用します。 frameProcessor.addPacketCallback("transformed_input_video_cpu") { packet -> packet ?: return@addPacketCallback // 変換済み画像を物体検出(TensorFlow Lite)に渡す val bitmap = PacketGetter.getBitmapFromRgba(packet) objectDetector.detect(bitmap) { detections -> // 検出結果を処理 } } transformed_input_video_cpu : 変換後の画像を出力するストリーム名 3.2 座標の正規化 物体検出結果のピクセル座標を、MediaPipe が期待する正規化座標に変換します。 // ピクセル座標 → 正規化座標 (0.0〜1.0) val normalizedX = pixelX / imageWidth.toFloat() val normalizedY = pixelY / imageHeight.toFloat() 3.3 Sticker Proto の構造 Instant Motion Tracking では、AR オブジェクトの位置情報を Protocol Buffers 形式で定義します。 message Sticker { int32 id = 1; // ユニークID float x = 2; // 正規化X座標 (0.0〜1.0) float y = 3; // 正規化Y座標 (0.0〜1.0) float rotation = 4; // 回転角度 float scale = 5; // スケール int32 render_id = 6; // レンダリングID } message StickerRoll { repeated Sticker sticker = 1; } 3.4 フレームごとにパケットを送信 setOnWillAddFrameListener を使用して、各フレーム処理前に検出座標をグラフへ送信します。 frameProcessor.setOnWillAddFrameListener { timestamp -> with(frameProcessor.graph) { // 検出された物体の座標情報をパケットとして送信 val stickerRoll = StickerRoll.newBuilder() .addAllSticker(detectedObjects.map { detection -> Sticker.newBuilder() .setId(detection.id) .setX(detection.normalizedX) // 0.0〜1.0 .setY(detection.normalizedY) // 0.0〜1.0 .setScale(detection.scale) .build() }) .build() val stickersPacket = packetCreator.createSerializedProto(stickerRoll) addPacketToInputStream("sticker_proto_string", stickersPacket, timestamp) } } FrameProcessor.setOnWillAddFrameListener : 各フレームがグラフに送られる直前に呼ばれるコールバック FrameProcessor.graph.addPacketToInputStream : 入力ストリームにパケットを追加 sticker_proto_string : グラフ定義で指定された入力ストリーム名 4. テクスチャ(Bitmap)の描画と送信 位置情報と同時に、AR コンテンツとして描画する Bitmap テクスチャもグラフに渡します。 4.1 Bitmap テクスチャの生成 検出された各スイッチに対して、丸アイコンとラベルテキストを含む Bitmap を生成します。 val bitmap = createBitmap(width.toInt(), height.toInt()).apply { with(Canvas(this)) { concat(Matrix().apply { preScale(-1.0f, 1.0f, width / 2f, height / 2f) // X軸を反転して描画 }) drawCircle(circleX, circleY, CIRCLE_RADIUS, circlePaint) drawRect(rectLeft, rectTop, rectRight, rectBottom, backgroundPaint) } } Matrix().preScale(-1.0f, 1.0f) で Bitmap を左右反転しています。以下の IMU 行列に合わせるためです。 float imu_matrix[9] = { -1.0f, 0.0f, 0.0f, // X軸 → 反転(-X) 0.0f, 0.0f, 1.0f, // Y軸 → Z軸へ 0.0f, 1.0f, 0.0f // Z軸 → Y軸へ }; この行列は OpenGL モデル行列(4x4)の回転成分として使われ、Y/Z 軸の入れ替えと X 軸反転でテクスチャをカメラ平面に平行に固定します。 本来はデバイスの IMU センサーから回転行列を受け取り、端末の傾きに追従させます。 https://github.com/google-ai-edge/mediapipe/blob/0.10.32/mediapipe/examples/android/src/java/com/google/mediapipe/apps/instantmotiontracking/MainActivity.java#L218-L220 本実装では固定値にすることで 常にカメラ正面を向く (ビルボード効果)ようにし、 (0,0) の -1.0 による X 軸反転を Bitmap 側の preScale(-1.0f, 1.0f) で打ち消します。 4.2 テクスチャの送信 // テクスチャ画像(Bitmap配列) val texturesPacket = packetCreator.createRgbaImageFrameVector( renderStickers.map { it.bitmap }.toTypedArray() ) addPacketToInputStream("gif_textures", texturesPacket, timestamp) // アスペクト比(テクスチャの縦横比) val aspectRatiosPacket = packetCreator.createFloat32Vector( renderStickers.map { it.aspectRatio }.toFloatArray() ) addPacketToInputStream("gif_aspect_ratios", aspectRatiosPacket, timestamp) PacketCreator.createRgbaImageFrameVector : 複数の Bitmap を RGBA 形式のパケットに変換 gif_textures : テクスチャ画像の入力ストリーム gif_aspect_ratios : 各テクスチャのアスペクト比(正しいスケーリングに必要) 公式サンプルでは createRgbaImageFrame を使用して 単一のテクスチャ をグラフに渡します。 https://github.com/google-ai-edge/mediapipe/blob/0.10.32/mediapipe/examples/android/src/java/com/google/mediapipe/apps/instantmotiontracking/MainActivity.java#L608-L610 本実装では、複数の検出オブジェクトに対応するため createRgbaImageFrameVector で 複数テクスチャを同時に送信 し、 gif_aspect_ratios も createFloat32Vector で 各テクスチャに対応するアスペクト比の配列 を渡すよう拡張します。これにより、検出された各スイッチに異なるラベル(テキスト付きBitmap)を正しい縦横比で表示できます。 ここまでで AR コンテンツをカメラ上に表示できました。 5. 座標の更新 トラッキング中のステッカー座標を更新するには、新しい座標を持つ sticker_proto_string と、リセット対象の ID を含む sticker_sentinels を同一 timestamp で送信します。 TrackedAnchorManagerCalculator が該当 ID のトラッキングボックスを破棄し、新しい座標でトラッキングを再開します。 // 更新した座標で Sticker Proto を再構築 val stickersPacket = packetCreator.createSerializedProto(stickerRoll) addPacketToInputStream("sticker_proto_string", stickersPacket, timestamp) // リセット対象のステッカー ID を送信 val stickerSentinels = packetCreator.createInt32Vector(updateIds) addPacketToInputStream("sticker_sentinels", stickerSentinels, timestamp) 公式サンプルでは sticker_sentinel で 単一のステッカー ID を送信します。 https://github.com/google-ai-edge/mediapipe/blob/0.10.32/mediapipe/examples/android/src/java/com/google/mediapipe/apps/instantmotiontracking/MainActivity.java#L342-L344 本実装では sticker_sentinels として createInt32Vector で 複数のステッカー ID を配列 で渡すよう拡張し、物体検出で座標が更新された複数のステッカーを同時にリセットできるようにします。 最後に 以上が MediaPipe Instant Motion Tracking を用いた技術的な実装解説でした。決して容易に導入できる手法ではありませんが、本機能の要件に対して Android に最も適した解決策だと考えています。 以前に ARCore の検証も行いましたが、ARCore は SLAM 技術による事前の 3D マッピングに時間を要し、 素早くかつ安定した AR エフェクトの実現には適さなかったため、検証を断念しました。 両フレームワークの違いを以下にまとめます。AR 技術の検討で参考になれば幸いです。 項目 Instant Motion Tracking ARCore 仕組み 2D ボックストラッキング + OpenGL 描画 環境マッピング + 平面検出(SLAM) デバイス要件 OpenGL ES 対応であれば動作 ARCore 対応デバイスのみ(Google 認定必須) 安定性 検出座標に依存するため補正が必要 空間認識が高精度で安定 導入コスト Bazel ビルド・C++ Calculator のカスタマイズが必要 SDK 導入のみで比較的容易 オープンソース あり(Apache 2.0) なし(プロプライエタリ) カスタマイズ性 Calculator の追加・変更で柔軟に拡張可能 SDK の API 範囲内に限定 パフォーマンス 軽量(2D トラッキングベースのため CPU/GPU 負荷が低い) 高負荷(環境の 3D 空間マッピングを常時実行) 学習コスト 高い(Bazel・C++・OpenGL・Protocol Buffers の知識が必要) 低い(Android SDK の知見で導入可能)
アバター
こんにちは!KINTOテクノロジーズ(以下、KTC)のAIファーストグループで、生成AIの社内活用推進を担当している和田です。普段は生成AIを使った業務価値の創出から、社内の教育研修、技術の手の内化まで、「AIを現場に届ける」仕事をしています。 今回お話ししたいのは、 AI Agent(AIエージェント) というトレンドです。KTCのようなテックカンパニーの内側で何が起きているのか。そして、ITやAIの知識を持つ我々と、業務の知識を持つ方々(それは時によってメーカーの設計技術者さんだったり、販売店の営業さんだったりします)との「協業の形」がどう変わろうとしているのか。「ニンベンのついた自働化」というキーワードを軸に、お伝えしていきます。 1. はじめに ― なぜ今「エージェント」なのか 生成AIの進化を振り返ると、大きく3つのフェーズがあったと考えています。 時期 フェーズ 特徴 2022〜2023年 チャットAI 1問1答。「質問すれば答えてくれる」体験が広がる 2024年 RAG全盛期 RAG(Retrieval-Augmented Generation:社内データ等を検索しながら回答を生成する手法)で「自社の情報を知っているAI」が登場 2025年〜 AI Agent AIが自ら考え、ツールを使い、複数ステップの仕事をこなす Agentを実現するOSSの老舗であるLangChainをはじめ、エージェントという概念自体は2023年頃にはすでに存在していました。しかし当時は、LLMそのものの"地頭"がまだ追いついていませんでした。指示を正しく理解できない、途中で迷子になる、ツールの使い方を間違える ― そんな状態を覚えている方もいると思います。 ここ1〜2年でLLM(Large Language Model:大規模言語モデル)の精度が飛躍的に向上したことで、ようやくエージェントが「実用に耐える」レベルになってきました。これは毎日エージェントを使い、自身の業務を常に効率化し続けてきた私の実感です。 2026年の今、多くの企業がエージェント技術を「PoCから社会実装へ」と動き始めています。試すフェーズは終わり、業務に組み込むフェーズに入りつつある。だからこそ、「どう組み込むか」の設計思想が問われています。 2. 目指す姿 ― 「ニンベンのついた自働化」とはどんな状態か KTCが所属するトヨタグループでは昔から「自働化」という概念が大切にされています。「動」ではなく「働」。機械が異常を検知したら自ら止まり、不良を後工程に流さない。問題を顕在化させ、人が原因を究明し対処できる状態をつくる。人を機械の番人にせず、本来人間にしかできない判断や改善に集中させる。自動化の中に「人の知恵」を埋め込む思想です。 ・・・とはいうものの、AIエージェントの時代における「ニンベンのついた自働化」とは、一体どんな状態でしょうか? 私はこう定義しています。 人間の役割が明確になっている エージェントが作業している間、人はより創造的・判断的な仕事に集中できている。たとえば、エージェントがログ分析をしている間に、人間は対応方針の意思決定に集中する、といった状態です。 エージェントの「持ち物」が事前に整っている 必要な権限、参照すべきデータ、判断基準 ― これらを人間が先回りして渡している。エージェントに手待ちをさせない環境設計です。 「やってはいけないこと」の境界線が設計されている 例えば「データの参照はOK、削除はNG」「提案はするが、最終承認は必ず人間」といったガードレールが明確に引かれている。 業務を知る人がフロー全体をデザインしている 技術者だけでは、業務の「行間」は読めません。何年・何十年と積み上げてきたドメイン知識を持つ人が、AIとの協業設計に参加している状態です。 この4つが揃ったとき、AIは「勝手に動く怖いもの」ではなく、「信頼して任せられるチームメイト」になる。それが「ニンベンのついた自働化」の姿だと考えています。 3. 進め方の指針 ― PoCを現場に届けるための3ステップ 「エージェント、作ってみたけど現場に浸透しない」 これは本当によく起きる現象です。理由はシンプルで、 技術的に動くものを作ることと、それが業務に根付くことは、まったく別の話 だからです。 私がエージェント開発の中で踏む3つのステップを紹介します。 ステップ1:課題を「正しく」見つける ここでの「正しく」とは、AIで解くべき課題かどうかを見極めるという意味です。 何年もかけて磨き上げられてきた課題解決の型は、道具が変わっても色褪せません。トヨタグループが大切にする問題解決のアプローチ ― 「現状把握」「真因追求」は、AI活用の文脈でもそのまま有効です。 ただし、一つ重要な判断軸が加わります。 「全てをAIでやろうとしない」 ということ。 たとえば、月に数回しか発生しない作業を自動化しても、構築・運用コストに見合わないことがあります。逆に、毎日30分かかる定型作業は、多少精度が荒くてもエージェント化する価値がある。費用対効果とスケール感を冷静に見極めることが、このステップの肝です。 ステップ2:試す・作り込む AIエージェントの構造は、実はシンプルです。大きく2つの要素で成り立っています。 プロンプト :エージェントへの「指示書」。あなたの役割はこれで、こういう手順で仕事をしてください、という設計図です。非エンジニアの方は「新人に渡す業務マニュアル」をイメージしていただくとわかりやすいかもしれません。 ツール :エージェントが使える「道具箱」。ウェブ検索、社内データの参照、計算、メール送信など、LLM単体では苦手なことを補う機能群です。 ・・・ただし、「シンプルな構造 = 簡単に完成する」ではありません。 プロンプトの書き方ひとつで、エージェントの振る舞いは劇的に変わります。ツールの選び方、渡すデータの粒度、エラー時のフォールバック設計。この作り込みの工程に、全体の工数の大半がかかると言っても過言ではありません。 ステップ3:業務フローに「組み込む」 ここが最も重要で、かつ最も見落とされやすいステップです。 完成したエージェントを業務フローのどこに置くか。誰が使うか。既存のツールとどう共存させるか。例外が起きたときに誰がフォローするか。 これらの問いに答えられるのは、 ドメインの知識を持つ人だけ です。 ここで言う「ドメイン知識」とは、特定の業務ノウハウだけを指しているわけではありません。業務フローを再設計するための価値判断基準、組織の意思決定経路や力学、そして現場の肌感覚 ― これらすべてを含む、長年の経験から培われた知の総体です。 たとえば自動車・モビリティの領域で考えると、その重要性がよくわかります。 現場の業務ノウハウ 整備士が持つ「この車種のこの年式は、ここが壊れやすい」という経験則。販売店の営業が持つ「この地域では◯月に需要が伸びる」という季節感覚。こうした知識は、個別業務に深く根ざしています。 価値判断と優先順位の基準 「納車までのリードタイムを短縮するよりも、お客様への中間報告の頻度を上げるほうが満足度に効く」「この検査工程は品質上絶対に省略できないが、書類作成の順序は変えられる」。業務フローを再設計するとき、何を守り何を変えてよいかを判断できるのは、その業務の「重み」を知っている人だけです。 組織の事情と意思決定の経路 「この変更はA部門だけでは通らない、B部門の部長の合意が要る」「この申請は制度上オンラインで完結するが、実質は事前の根回しが必要」。どんなに優れたエージェントを作っても、組織の中で動かせなければ意味がない。その道筋を知っているのも、ドメインの力です。 これらの知識は構造化されていません。業務マニュアルにも社内ドキュメントにも、ましてやLLMの学習データにも十分には載っていない。だからこそ、エージェントを開発する技術者だけでは業務フローの設計はできないし、業務を知る「人」が設計に参加する必要があるのです。 具体的な場面で言えば、「この申請は月末に集中するから、そのタイミングでエージェントが下書きを用意しておいてくれると助かる」「この承認フローは部長の口頭確認が実質必要だから、エージェントの自動承認は外したほうがいい」 ― こうした判断は、何年も現場で業務を回してきた人にしかできません。 だからこそ、ステップ3は技術者と業務担当者の「共同作業」になります。ここに「ニンベンのついた自働化」の真価があると考えています。 4. よくある落とし穴 ― 「動くけど根付かない」を避けるために セクション3で「 正しい進め方 」を紹介しましたが、現場では逆のパターン ― つまり、やってしまいがちな失敗 ― も数多く見てきました。エージェントが「技術的には動いているのに、業務に根付かない」とき、原因はたいてい次の3つのどれかに行き着きます。 落とし穴1:「全部AIで」と決めつけてしまう エージェントの可能性に惹かれるあまり、「AIに丸投げ」してしまうケースです。 一見すると大胆で魅力的に聞こえます。しかし、業務フローの中には「人の判断が入ることで価値が生まれている」工程が必ずあります。たとえば、クレーム対応における熟練オペレーターの声色の判断や、契約書レビューでのベテラン法務担当の「この条項は先方の意図と違う気がする」という直感。データ上は自動化できそうに見えても、その判断こそが顧客との信頼関係を支えている。こうした工程をAIに丸ごと置き換えると、効率は上がっても、守るべきものが静かに失われていきます。 ステップ1の「 AIで解くべき課題かどうかの見極め 」が甘いと、ここにはまります。 落とし穴2:ドメインエキスパート不在のまま業務フローを設計する エンジニアだけで「こう組み込めば効率的だろう」と業務フローを設計してしまうケース。技術的には合理的でも、現場の実態と噛み合わない、机上の空論で設計が進行してしまいます。 セクション3で挙げた「 組織の事情と意思決定の経路 」。これを知っているのは、何年もその業務を回してきた人だけです。エンジニアがどれほど優れていても、この層の知識は外から取得できません。 落とし穴3:「作って渡す」で終わりにしてしまう 「エージェント、完成しました。マニュアルも書きました。あとはよろしくお願いします」。 この引き渡し方は、ほぼ確実に定着しません。エージェントは従来のシステムとは違い、使い方や問いかけ方によって振る舞いが変わります。現場の人が「こう聞けばこう返る」という感覚を掴むまでには、作った人と一緒に使ってみる期間が要ります。 もうひとつ見落とされがちなのが、 UI/UXの設計 です。エージェントと聞くと、つい「チャットUI」を思い浮かべがちですが、チャットはあくまで暫定的なインターフェースにすぎません。現場の人が本当に求めているのは「チャットで何でも聞ける」体験ではなく、「いつもの業務の流れの中で、自然にAIの力が効いている」体験です。それはボタンひとつで起動するワークフローかもしれないし、既存ツールの中に溶け込んだ提案機能かもしれない。チャットUIで得たフィードバックを手がかりに、ユーザーが本当に求める体験を作り込んでいく ― この工程を「渡して終わり」にすると、永遠にチャットの域を出られません。 使っていく中で「ここはもう少しこうしてほしい」というフィードバックが生まれる。そのフィードバックをその場で反映できる ― この即応性が、エージェントが業務に馴染むかどうかの分岐点になります。 これらの落とし穴に共通するのは、 技術と業務の間に「翻訳者」がいない ということです。 エージェントにせよ何にせよ、 使ってもらってなんぼ です。どれだけ精緻に作り込んでも、現場で使われなければ価値はありません。そして「使われる」ためには、技術的な完成度よりも、業務への馴染み方のほうがはるかに重要です。エンジニアとドメインエキスパートが同じ机で一緒に考える体制さえあれば、これらの失敗の多くは防げます。 次のセクションでは、その「一緒に考える」を実現するための協業モデルについてお話しします。 5. 今後の展望 ― Forward Deployed Engineer(FDE)という協業の形 最後に、「ニンベンのついた自働化」を現場に届けるための、IT企業との新しい協業モデルについてお話しします。 Forward Deployed Engineer(FDE) とは、エンジニア自身が顧客の現場に入り込み、課題のヒアリングから実装・運用定着まで一気通貫で担う職種です。 起源は米国の Palantir Technologies が確立した FDSE(Forward Deployed Software Engineer) とされています。名前の由来は軍事用語の「Forward Deployed(前線展開)」で、「製品を納品するだけでは使われない、エンジニアが現場に入って初めて価値が生まれる」という哲学から生まれました。 従来のIT企業では、エンジニアは社内でシステムを開発し、営業・PM・カスタマーサクセスを介して顧客と接するのが一般的です。FDEはこの構造を変え、エンジニアが顧客と直接対話しながら、要件定義・実装・定着支援までをすべて担います。コンサルタントと異なるのは「自ら手を動かす」点です。 具体的には、 作れるエンジニア自身が、課題を持っている現場に直接入り込んで、一緒に考える 。プロトタイプを一緒に触りながら、同じ机で議論する。セクション4で挙げた 「作って渡す」で終わりにしてしまう という落とし穴の裏返しとも言えます。作って渡すのではなく、作りながら一緒に使う。その距離感が、エージェントの定着を左右します。 役割 担うこと FDE (IT側) 技術的な複雑さを引き受ける。AIの限界と可能性を正直に伝える。「これはできます、これは今は難しいです」を明確にする。 ドメインエキスパート (業務側) 業務の文脈を提供する。「このデータならここから取れる」「この件は誰に聞けばいい」「この申請は私が通します」という現場の力を発揮する。 この2つが掛け合わさったとき、初めて「ニンベンのついた自働化」が現場に根付く。私はそう信じています。 KTCは、この「FDEとドメインエキスパートの共創」を、自分たちの現場で実践し続けていきます。困りごとを見つけ、試し、形にして、届ける。そのサイクルの中で得た知見を、こうした場で発信していくことが、私にできる貢献のひとつだと考えています。 ここまで読んでいただき、ありがとうございました! 「AIエージェント」という言葉が少し身近になり、「うちの現場でも何かできそうだな」と感じていただけたなら、この記事を書いた甲斐があります。 ぜひ一緒に、「ニンベンのついた自働化」を実装していきましょう。
アバター
はじめに Webアプリケーションの回帰テストを自動化する際、適切なツールの選択は品質保証とチームの生産性に大きく影響します。 プロジェクト背景 KINTOテクノロジーズ(以下、KTC)では、これまでAutify NoCodeWebを活用して回帰テストの自動化を進め、品質保証体制を構築してきました。Autify NoCodeWebのノーコードプラットフォームは、QA専任メンバーが中心となってテスト自動化を迅速に導入する上で非常に有効であり、多くの成果を上げてきました。 しかし、プロジェクトの成長に伴い、新たな課題も見えてきました: より高速なテスト実行が求められるようになった CSVファイルの編集・アップロードなど、複雑なファイル操作を伴うテストシナリオの増加 データ駆動テストによる大量のテストパターンの実行ニーズ エンジニアチームの拡大により、コードベースのテスト資産の管理が可能になった このような背景から、現在のKTCの体制と要件に最適なツールを再検討する必要が生じました。本記事では、これまでお世話になってきたAutify NoCodeWebと、新たな選択肢としてのPlaywrightを、実際の回帰テストシナリオにおいて詳細に比較します。 どちらのツールも優れた特徴を持っており、組織の状況によって最適な選択は異なります。本記事が、皆様のツール選定の一助となれば幸いです。 ツール概要 Playwright 開発元: Microsoft タイプ: オープンソースのE2Eテストフレームワーク 対応言語: JavaScript/TypeScript、Python、.NET、Java 対応ブラウザ: PC:Chromium(Chrome、Edge)、Firefox、WebKit(Safari相当) モバイル:デバイスエミュレーション(Chromium、WebKit)  ※実機のモバイルブラウザ操作は非対応 特徴: コードベースで柔軟性が高く、高速な実行速度 Autify NoCodeWeb 開発元: オーティファイ株式会社(日本企業) タイプ: ノーコードAI搭載テスト自動化プラットフォーム 対応ブラウザ: PC:Chrome、Edge、Firefox、Safari(WebKit) モバイル:iOS、Android 特徴: 操作をレコーディングしてテストシナリオを作成、AI による要素認識と自動修復機能 ツール選択のためのデシジョンフローチャート 自社に最適なツールを選ぶ際の判断フローを視覚化しました。このフローチャートを参考に、組織の状況に応じた選択を行ってください。 graph TD Start[QAチームにプログラミング可能なエンジニアがいる] Start -->|No| AutifyNoCodeWeb1[Autify NoCodeWeb: ノーコードで容易、迅速な導入、AI自動修復] Start -->|Yes| Speed{実行速度を重視?} Speed -->|Yes| Playwright1[Playwright: 高速、柔軟、無料] Speed -->|No| Requirements{要件に応じて選択} Requirements -->|インフラ管理は避けたい| AutifyNoCodeWeb2[Autify NoCodeWeb] Requirements -->|メール連携や頻繁なUI変更がある| AutifyNoCodeWeb2 Requirements -->|コストを優先したい| Playwright2[Playwright] Requirements -->|データ駆動テストや複雑なファイル操作がある| Playwright2 フローチャートの使い方 このデシジョンフローは、以下の優先順位で判断することを推奨しています: チーム構成の確認: まず、開発チームにプログラミング可能なエンジニアがいるかを確認します。エンジニアリソースが限られている場合は、Autify NoCodeWebが最適な選択となります。 実行速度の重視度: エンジニアがいる場合、次に実行速度の重要性を評価します。CI/CDパイプラインでの高速フィードバックが重要な場合、Playwrightが適しています。 詳細要件の評価: 実行速度がそれほど重要でない場合は、具体的なテスト要件に基づいて判断します: graph LR C1[インフラ管理は避けたい] --> AutifyNoCodeWeb[Autify NoCodeWeb] C2[メール連携や頻繁なUI変更がある] --> AutifyNoCodeWeb[Autify NoCodeWeb] C3[コストを優先したい] --> Playwright C4[データ駆動テストや複雑なファイル操作がある] --> Playwright ハイブリッドアプローチの検討: 上記の要件が混在している場合、両ツールを併用するハイブリッドアプローチも有効な選択肢です。 機能別詳細比較 # 比較項目 Playwright Autify NoCodeWeb 1 CSVの編集とアップロード ✅ 可能 ⚠️ 制限あり 2 特定ファイルのダウンロード ✅ 可能 ⚠️ 検証に制限 3 特定ステップのスクリーンショット ✅ 柔軟なカスタマイ즈可能 ✅ 自動取得で便利 4 画面上の文字状態の判断 ✅ 詳細な検証可能 ✅ AI認識で安定 5 データ駆動テストの循環使用 ✅ 可能 ⚠️ 制限あり 6 異なる画面間の切り替え ✅ 完全対応 ✅ 対応 7 外部メール内容の確認 ✅ API連携で対応可能 ✅ 統合機能で便利 8 動的要素のロケート ✅ 高精度な制御 ✅ 高精度な制御 / JS指定 9 画面の比較(VRT) ✅ ピクセル単位の精密比較 ✅ AI支援で大規模変更に対応 10 スクリプトの実装難易度 ⚠️ プログラミングスキル必要 ✅ ノーコードで容易 11 スクリプトの修正難易度 ✅ テキスト編集で迅速 ⚠️ GUI操作が必要 12 スクリプトの実行速度 ✅ 基準速度 (高速) ⚠️ 比較的遅い傾向 1. CSVの編集とアップロード Playwrightの場合: input[type="file"] 要素に対して setInputFiles() メソッドを使用することで、CSVファイルのアップロードが柔軟に実装できます。また、ファイルの動的生成やデータ駆動テストとの組み合わせも可能です。コードベースの利点を活かし、複雑なファイル操作シナリオに対応できます。 Autify NoCodeWebの場合: 基本的なファイルアップロード機能は提供されていますが、複雑なCSV編集を伴うシナリオには制約があります。シンプルなファイルアップロードであれば、ノーコードで簡単に実装できる点は大きなメリットです 2. 特定ファイルのWebページからのダウンロード Playwrightの場合: page.waitForEvent('download') を使用してダウンロードイベントを捕捉し、ファイル名や内容の検証まで完全に制御できます。ダウンロードしたファイルの内容を自動的に検証するシナリオも実装可能です Autify NoCodeWebの場合: ダウンロード操作の記録と実行は可能です。基本的なダウンロード動作の確認には十分対応しており、ノーコードで実装できる利点があります。より詳細なファイル検証が必要な場合は、他の手段との組み合わせを検討する必要があります。 3. 特定ステップのスクリーンショット Playwrightの場合: page.screenshot() や locator.screenshot() を使用して、任意のタイミングで全画面または特定要素のスクリーンショットを取得できます。保存先やファイル名も自由に設定可能で、細かい制御が必要な場合に優れています Autify NoCodeWebの場合: 全てのテストステップで自動的にスクリーンショットが撮影されるため、設定の手間が不要です。テスト失敗時の原因調査が容易になり、特にテスト自動化に不慣れなメンバーでも、確実に証跡を残せる点が優れています。 4. 画面上の文字状態の判断 Playwrightの場合: expect(locator).toHaveText() 、 toContainText() 、 toBeVisible() など、豊富なアサーションメソッドで文字列の存在、内容、表示状態を詳細に検証できます。正規表現による柔軟なパターンマッチングも可能で、複雑な検証ロジックに対応できます。 Autify NoCodeWebの場合: テキストの存在確認や表示状態の検証が可能です。特にAIによる要素認識により、画面デザインが変更されても同じテキスト要素を識別できる点が優れています。HTMLの細かい変更に強く、メンテナンスコストを削減できます 5. データ駆動テストの循環使用 Playwrightの場合: テストデータを配列やCSVファイルから読み込み、 test.describe() やforループを使用して複数のデータセットで同じテストロジックを実行できます。テストの再利用性が非常に高く、大量のテストパターンを効率的に実行できます。 // CSVファイルからデータを読み込む testData = await readCSV('C:\\××××××××\\testData4.csv'); for (const data of testData) { const { password, surname, katakanaSurname, yearOfBirth, monthOfBirth, dayOfBirth, sex, postCode1, postCode2, cellphoneNumber1, cellphoneNumber2, cellphoneNumber3, typeOfHousing, yearsOfResidence, numberOfPeople1, numberOfPeople2, annualIncome, purposeOfUser, licenseNumber, route, fileName, profession, corporateName, positionOfCorporateName, nameOfCorporate, katakanaNameOfCorporate, department, postCodeOfCorporate1, postCodeOfCorporate2, cellphoneNumberOfCorporate1, cellphoneNumberOfCorporate2, cellphoneNumberOfCorporate3, lengthOfWork } = data; Autify NoCodeWebの場合: 個別のテストシナリオを作成することで、複数のパターンに対応できます。ノーコードで各シナリオを管理できるため、プログラミングの知識がなくても運用可能な点がメリットです。ただし、データ量が多い場合はシナリオ数が増加します。 6. 異なる画面間の切り替え Playwrightの場合: 複数タブ、複数ウィンドウ、iframe間の切り替えを完全にサポートしています。 page.context().pages() で全ページを取得したり、 page.waitForEvent('popup') で新しいページを待機することができます。複雑な画面遷移ロジックも実装可能です。 Autify NoCodeWebの場合: 画面遷移やタブ切り替えの操作を記録・実行できます。基本的な画面間の移動には十分対応しており、ノーコードで実装できる利点があります。 7. 外部メール内容の確認(URLのクリックなど) Playwrightの場合: メールテストAPIサービス(例:MailSlurp、Mailinator)と連携してメール内容を取得し、URLを抽出してナビゲーションすることが可能です。柔軟な連携が可能ですが、追加の実装とAPI費用が必要になる場合があります。 Autify NoCodeWebの場合: メール検証機能がプラットフォームに組み込まれており、追加の設定や実装なしでメール内のリンクをクリックしたり、内容を確認したりできます。この統合機能は大きな強みであり、特に非エンジニアのQAメンバーでも簡単に利用できる点が優れています。 8. XPathなどによる頻繁に変動する要素のロケート Playwrightの場合: CSS Selector、XPath、text、roleなど、多様なロケーター戦略をサポートしています。複数のロケーターを組み合わせたり、厳密な条件指定が可能で、動的要素に対しても高い精度で特定できます。 # ENT番号取得 xpath1 = '//*[@id="app"]/div/main/div/div[1]/div[1]/div[2]/div/div[3]/div[1]' display_text1 = (await page.locator(f'xpath={xpath1}').text_content() or '').strip() last1 = display_text1[-5:] shinsa_number = '97016QAP00' + last1 # メールアドレス取得 xpath2 = '//*[@id="app"]/div/main/div/div[1]/div[1]/div[2]/div/div[7]/div[2]' display_text2 = (await page.locator(f'xpath={xpath2}').text_content() or '').strip() # 名前取得 xpath0 = '//*[@id="app"]/div/main/div/div[1]/div[1]/div[2]/div/div[7]/div[1]/a' display_text0 = (await page.locator(f'xpath={xpath0}').text_content() or '').strip() lastname = display_text0[4:] Autify NoCodeWebの場合: AIによる要素認識を採用しており、HTMLが変更されても要素を識別しようとします。この機能は画面の小規模な変更に対して非常に強く、手動でのメンテナンスを大幅に削減できます。特にデザイン調整が頻繁に行われる開発フェーズでは、この自動修復機能が大きな価値を発揮します。複雑な動的要素については、認識精度を確認しながら運用することが推奨されます。 そして、Javascriptによって要素の指定も簡単にできます。 function getEmailInputValue() { var selector = "#__next > main > div > div > div.o-emailPasswordForm > div > div.m-inputField > div:nth-child(1) > div > div.m-inputField__container > div > div > input[type=email]"; var element = document.querySelector(selector); if (!element) { throw new Error("Error: cannot find the element with selector(" + selector + ")."); } return element.value; } // 実行例 console.log(getEmailInputValue()); 9. 画面の比較(ビジュアルリグレッションテスト) Playwrightの場合: toHaveScreenshot() メソッドでピクセルレベルの画面比較が可能です。差分の許容範囲を設定したり、特定領域をマスクしたりできます。細かい視覚的変更の検出に優れており、意図しないUI変更を確実に捉えます Autify NoCodeWebの場合: 画面全体の変更を検出し、AIが変更箇所を識別します。特に大規模なデザイン変更時には、変更点の確認とテストシナリオの更新が比較的容易です。AIによる変更の影響分析機能により、どのテストシナリオを更新すべきかの判断がしやすく、大規模リニューアル時のメンテナンス工数を削減できる点が優れています。 10. スクリプトの実装難易度 Playwrightの場合: JavaScript/TypeScriptなどのプログラミング言語とテストフレームワークの知識が必要です。習得には一定の時間がかかりますが、公式ドキュメントが充実しており、コミュニティも活発です。エンジニアチームが確立されている組織に適しています。 Autify NoCodeWebの場合: ブラウザ操作を記録するだけでテストシナリオが作成できるため、プログラミング経験がない非エンジニアでも容易に使用できます。この実装の容易さは、Autify NoCodeWebの最大の強みの一つです。QA専任メンバーが主体となってテスト自動化を推進できるため、エンジニアリソースが限られている組織や、迅速にテスト自動化を開始したい場合に特に有効です。 11. スクリプトの修正難易度 Playwrightの場合: テキストエディタでスクリプトを直接編集できるため、小規模な修正は数秒で完了します。バージョン管理システム(Git)との親和性も高く、差分確認やロールバックが容易です。複数人での並行開発やコードレビュー文化とも相性が良いです。 Autify NoCodeWebの場合: GUI上で操作を再記録するか、手動で修正する必要があります。ただし、AIによる自動修復機能により、画面の小規模な変更には自動的に対応されるため、実際の修正作業は最小限に抑えられます。この自動修復機能は、メンテナンスコストの削減に大きく貢献します。 12. スクリプトの実行速度 Playwrightの場合: ヘッドレスモードでの実行やネットワークリクエストの最適化により、非常に高速なテスト実行が可能です。並列実行にも標準対応しており、大規模なテストスイートでも短時間で完了します。 Autify NoCodeWebの場合: クラウドベースのプラットフォームであり、ネットワークレイテンシーや処理のオーバーヘッドにより、Playwrightと比較して実行速度が遅くなる傾向があります。ただし、実際の速度差はテストケースの複雑さ、ネットワーク環境、Autify NoCodeWebのサーバー負荷などの要因によって大きく変動する可能性があります。大規模なテストスイートでは実行時間が増加する可能性がありますが、並列実行機能を活用することで全体の実行時間を最適化できます。 :::message 実行速度は環境やテストケースによって大きく異なるため、具体的な数値比較は控えます。各ツールの特性を理解し、実際の使用環境でのパフォーマンスを評価することをお勧めします。 ::: 追加の比較ポイント 📊 要約比較表 項目 Playwright (エンジニア主導) Autify NoCodeWeb (QA・非エンジニア主導) コスト 完全無料 (OSS) サブスクリプション型 (有料) 導入障壁 プログラミングスキルが必要 低い (ノーコードで即時開始) CI/CD 柔軟かつ強力な統合 シンプルなAPI連携 メンテナンス コードベース・Git管理 AIによる自動修復 (Self-healing) 1. コストと導入障壁 Playwright: 完全無料のオープンソース 学習コストは必要だが、長期的なランニングコストはゼロ CI/CD環境への組み込みも容易 エンジニアチームの人件費は考慮が必要 Autify NoCodeWeb: サブスクリプション型の有料サービス 初期導入が簡単で、迅速にテスト自動化を開始できる テストシナリオ数や実行回数に応じた費用体系 インフラ管理コストが不要 エンジニアリソースが限られている場合、トータルコストで優位性がある場合も 2.チーム構成との適合性 Playwrightが適しているチーム: エンジニア主導のQA体制が整っている コードレビュー文化が定着している 複雑なテストロジックや高度なカスタマイズが必要 Git等のバージョン管理システムを活用している Autify NoCodeWebが適しているチーム: QA専任メンバーが中心(プログラミング経験が少ない) エンジニアリソースが限られている 迅速にテスト自動化を開始したい メンテナンスコストを抑えたい(AIによる自動修復活用) ノーコードでテスト資産を管理したい 3. CI/CD統合 Playwright: GitHub Actions、GitLab CI、Jenkins など主要CI/CDツールとの統合が容易 テスト結果のレポート生成、アーティファクト保存が柔軟 並列実行、シャーディングなど高度な実行戦略が可能 開発フローに深く統合できる Autify NoCodeWeb: APIを介したCI/CD統合が可能 独自のテスト実行環境を使用 クラウドベースのため、インフラ管理不要 CI/CD統合の設定がシンプル 4.メンテナンス性と長期運用 Playwright: スクリプトをバージョン管理できる リファクタリングやスクリプトの再利用が容易 コミュニティが活発で、最新のベストプラクティスにアクセスしやすい 長期的なスクリプト資産の管理に優れる Autify NoCodeWeb: AIによる要素の自動認識で、画面変更時のメンテナンス工数を削減 自動修復機能により、軽微な変更への対応が自動化される プラットフォーム上での一元管理が可能 ノーコードのため、担当者の変更による影響が少ない それぞれのツールが特に優れているシーン Playwrightが最適なケース 大量のデータパターンテスト: 同一ロジックで数百〜数千パターンのテストデータを処理する必要がある場合 高頻度の実行: CI/CDパイプラインで1日に何度もテストを実行し、迅速なフィードバックが必要な場合 複雑なファイル操作: CSV編集、複数ファイルの同時アップロード、ダウンロードファイルの内容検証など エンジニア主導のQA: 開発チームとQAチームが密接に連携し、テストスクリプトもコードレビューの対象とする場合 長期的な資産管理: テストスクリプトをソースコードと同様に管理し、継続的に改善していく場合 Autify NoCodeWebが最適なケース 迅速な導入: プログラミング経験のないQAメンバーが、短期間でテスト自動化を開始したい場合 メール連携テスト: 外部メールの検証を含むシナリオが多い場合 頻繁なUI変更: デザイン調整が頻繁に行われる環境で、AI自動修復機能を活用したい場合 インフラ管理の負担軽減: テスト実行環境の構築・管理リソースが限られている場合 ノーコード資産管理: テスト資産をコード化せず、ビジュアルに管理したい場合 大規模リニューアル: 画面全体の大幅な変更時に、AIによる影響分析と効率的な更新が必要な場合 KTCにおける選択理由 KTCでは、これまでAutify NoCodeWebによって品質保証の基盤を築いてきましたが、プロジェクトの成長と共にいろいろな課題が顕在化しました。(上記のプロジェクト背景で述べた課題) これら課題を解決する選択肢として、Playwrightを導入することにしました。ただし、これはAutify NoCodeWebを完全に置き換えるものではありません: Playwrightが担う領域: データ駆動テスト、高速実行が求められるCI/CD統合、複雑なファイル操作を伴うシナリオ Autify NoCodeWebが引き続き価値を発揮する領域: メール連携テスト、ノーコードで管理すべきシナリオ、QA専任メンバーが主導するテスト 両ツールの強みを活かしたハイブリッドアプローチにより、KTCの品質保証体制をさらに強化していく予定です。 まとめ:どちらを選ぶべきか Playwrightの主な強み: 高速な実行速度 柔軟なカスタマイズ性 精密な要素制御とデータ駆動テスト オープンソースでコストゼロ バージョン管理システムとの親和性 Autify NoCodeWebの主な強み: ノーコードで実装が容易 AIによる自動修復でメンテナンスコスト削減 統合されたメール検証機能 非エンジニアでも運用可能 インフラ管理不要 最適な選択は、組織の状況によって異なります: エンジニアリソースが限られ、迅速にテスト自動化を開始したい → Autify NoCodeWeb エンジニアチームが確立され、高度なカスタマイズと高速実行が必要 → Playwright 両方のメリットを活かしたい → ハイブリッドアプローチ どちらのツールも、現代のWebアプリケーション開発において品質を担保するための重要な選択肢です。本記事の比較内容を参考に、自社のチーム構成、スキルセット、プロジェクト要件、予算、長期的な運用計画などを総合的に考慮して、最適なツールを選定してください。 Autifyは世界中で支持されているノーコードテスト自動化プラットフォームであり、特にエンジニアリソースが限られている組織において、品質保証体制を迅速に構築できる優れたソリューションです。KTCも、Autifyのサービスを通じて多くの成果を上げてきました。 今後も、両ツールの進化に注目し、それぞれの強みを最大限に活用していくことが重要です。
アバター
こんにちは。Engineering OfficeのAccessibility Advocate、辻勝利です。 少し前になりますが、2月19日にDevelopers Summit 2026(デブサミ2026)に参加し、一般財団法人GovTech東京によるセッション「アクセシビリティを“あたりまえ品質”に!!」を傍聴してきました。 登壇者の一人である松村道生さんは私の知人であり、同時期に新たな環境へ身を投じた仲間でもあります。彼がGovTech東京という組織において、どのようにアクセシビリティ推進を開発プロセスに組み込んでいるのか、その実践を参考にしたいと考えたのが参加の動機でした。 30分という限られた時間でしたが、アクセシビリティを「付加価値」ではなく「当然備わっているべき品質」と定義し、組織的に取り組む姿勢が非常に明確なセッションでしたので、今回はその内容を簡単にお伝えします。 1. 効率化の裏側にある「課題」の実態 セッションの前半では、視覚障害当事者でもある松村さんより、現在のデジタル化・効率化がもたらした課題が共有されました。 近年、サービスの効率化や自動化が「良いこと」として捉えられる傾向があり、様々なところで実際にいろいろなサービスの効率化が図られています。 もちろん、人材不足などの様々な要因により致し方ないと考えられる側面もありますが、下記の事例は私たち視覚障害者の「それでは済まされない現実」をあらわにする内容で、私も一つ一つうなずきながら聞きました。 マイナンバー設定の課題: 役所にスクリーンリーダー環境が整備されていなかったため、秘匿すべきパスワードを職員に口頭で伝えて代筆・設定してもらうしかなかった経験。 対面サービスの減少: 駅の「みどりの窓口」削減により自動券売機が主流となったことで、独力での切符購入が困難になった現状。 行政申請の壁: コロナ禍のワクチン接種予約など、視覚障害者が独力で完結できない設計のままリリースされたサービスの実態。 これらの事例を通じて、「世の中を便利にするための自動化が、結果として一部の都民を排除してしまっている」という切実な現状が示されました。 2. 行政サービスにおける「唯一性」と責任 特に印象に残ったのが、行政サービス特有の責任に関するお話でした。 民間サービスであれば、もし「サービスA」がアクセシビリティの問題で使えなくても、ユーザーは代替手段として「サービスB」を選択できる可能性があります。しかし、行政サービスである「東京アプリ」は唯一無二の存在であり、他に選択肢がありません。 「使えないから他を使う」という逃げ道がない以上、最初から全都民が等しく使える状態でリリースしなければならない。この「代替不可能な公共インフラとしての責任感」が、GovTech東京がアクセシビリティを最優先事項に据える最大の根拠であることを再認識しました。 3. シフトレフト:開発工程へのアクセシビリティの組み込み 山内晨吾さんが担当されたパートでは、これらの課題を「後付け」ではなく、開発の最上流から解決する「シフトレフト」の実践手法が紹介されました。 デザイン段階からの設計(Figma): コンポーネント単位で要件を定義し、UI設計時に品質を確保。 テストコードによる自動検証: 機械的にチェック可能な項目を自動化し、デグレード(品質低下)を防止。 AIレビューの活用: LLM(大規模言語モデル)等を活用し、コードレビュー段階でアクセシビリティの不備を検知。 GovTech東京では、山内さん(エンジニア)と松村さん(当事者視点)が密に連携し、技術的な仕組みと実際の課題の体験が双方向でフィードバックされる体制が確立されています。チームとして高度に機能していることが、発表の端々から伝わってきました。 4. 「なくては困る」を基準にする開発文化 セッションの核となっていた、アクセシビリティを「あったらいいね(魅力品質)」から「なくては困る(当たり前品質)」へ変えていくという視点は、私が取り組んでいる「アクセシビリティを社内文化にする」という活動とも強く共鳴するものです。 この業界で20年以上アクセシビリティの啓発に従事していますが、当事者意識(オーナーシップ)と技術的な合理性がこれほど高いレベルで融合した発表には、なかなか出会えるものではありません。 おわりに イベント終了後の「Ask the Speaker」では、お二人に直接ご挨拶する機会を得ました。現場で格闘している方々と対話し、今後の連携の可能性についても言葉を交わせたことは大きな収穫でした。 今回のセッションで得た知見を、私自身のプロジェクトにおける「アクセシビリティの文化定着」にも確実に活かしていきたいと考えています。 参考リンク デブサミ2026 セッション詳細 CodeZine:アクセシビリティのシフトレフトを実現!「東京アプリ」の開発プロセス改善
アバター
ソフトウェアの依存関係アップデートはRenovateにした理由 DBREグループで、DevSecOps担当を自称している栗原です。 タイトルの通り、ソフトウェア依存モジュールのアップデートにRenovateを採用しました。GitHub Dependabotと迷い続けましたが、この記事で紹介するDependabotにはない3つの利点が非常に魅力的だったため、Renovateを採用するにいたりました。Renovateを紹介している記事はよく見かけるので、あまり語られていないおすすめの実行方法についてと、私が惹かれた3つのポイントについて説明します。 Renovateとは Renovate は、ソフトウェアの依存関係を自動でアップデートしてくれるOSSツールです。Dependabotと同様に、リポジトリのルートに設定ファイル( renovate.json )を配置して、Renovateを実行すると、依存関係のアップデートPRを自動で送ってくれます。 2026年3月現在は無料で利用可能ですが、 Mend社による買収 後、将来的に有償化される可能性がある点は留意しておく必要があります。ただし、現時点ではOSSとして活発に開発が続いており、セルフホスティングも可能なため、柔軟な運用が可能です。 類似機能である、Dependabotとの詳細比較は 公式のbot比較ページ に譲りますが、Dependabotより高機能なのは間違いないです。個人的には設定の柔軟性が圧倒的に高く、複数リポジトリでの設定の共通化など、エンタープライズでの利用に適していると感じています。 ちなみに、この記事ではSCMはGitHubであることを前提にしておりますが、GitLabなど他のSCMを使われている方にも参考になるかと思います。 おすすめの実行方法 他社さんの記事などをみかけると、 Mend Renovate App (一番手っ取り早い)、 公式のGitHub Actions が紹介されていることが多いですが、私がおすすめしたいのは、 CLIでの実行 です。 Renovateは依存定義ファイル(package.json)だけではなく、lockfile(package-lock.json)も更新してくれますが、その際に実行環境にインストールされているパッケージマネージャ(npm)を実行して実現します。つまり実際の開発環境と同じツールを使えるのがベストなわけです。前者の2つは、プレビルドされた環境はあるものの、厳密にやろうとすると、Renovate実行用のコンテナをカスタマイズするなどが必要ですが、CLI実行であれば、他のワークフローで使っている環境セットアップの処理がそのまま転用できます。 特に我々は Monorepo を採用しており、複数の言語、パッケージマネージャ(Go、Python uv、Node.js yarn等)を使っているプロジェクトでは、それぞれのツールのバージョンを揃える必要があるため、CLI実行の恩恵が大きいです。 こちらは実際に我々が使っているGitHub Actionsです。 name: Update Deps Via Renovate on: schedule: - cron: '0 * * * *' workflow_dispatch: concurrency: group: "${{ github.event.repository.name }}-update-deps-via-renovate" cancel-in-progress: false env: LOG_LEVEL: ${{ vars.RENOVATE_LOG_LEVEL || '' }} jobs: renovate: runs-on: ubuntu-latest steps: # 他のワークフローとも共通化しているセットアッププロセス - name: checkout codebase and setup runtime id: setup-runtime uses: kinto-dev/action-dbre-setup-runtime@v3 with: # 後ほど紹介しますが、共通renovate設定ファイルを利用するため、 # 通常のGITHUB_TOKENではなく、GitHub Appのインストールアクセストークンを利用 github-app-id: ${{ vars.GH_APP_ID }} github-app-private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} go-project: "true" python-uv-project: "true" # GitHub Actionsであれば、Node.jsランタイムがデフォルトでインストールされているので、npxで直接renovateを実行可能。 - name: run renovate run: npx --yes --package renovate -- --token="${{ steps.setup-runtime.outputs.github-app-install-token }}" "${{ github.repository }}" 以上がおすすめの実行方法です。それでは次章からおすすめの機能を紹介していきます。 おすすめ機能1: インラインスクリプトもアップデートの対象にできる Dependabotでは基本的に設定ファイル(package.jsonやgo.mod等)に定義された依存関係のみがアップデート対象になりますが、Renovateは Custom Manager 機能により、正規表現でマッチさせた任意の文字列をアップデート対象にできます。 例えば、以下のようにnpm scriptとしてDocker Hubのイメージをタグ指定して実行しているケースを考えてみます。 // package.json { "scripts": { "gha_lint": "docker run -i --init --rm -v $INIT_CWD/.github/workflows:/workflows rhysd/actionlint:1.7.7 -color $(ls .github/workflows/*.yml | awk -F '/' '{print \"/workflows/\"$NF}')" } } このようなインラインスクリプトに依存モジュールのバージョンがハードコードされるケースも、Renovateはアップデートの対象にしてくれます。renovate.jsonに以下の設定を追加するだけで実現できます。 "customManagers": [ { "customType": "regex", "fileMatch": [ "^package\\.json$" ], "matchStrings": [ "docker run [^;]*? (?<depName>[^:\\s]+):(?<currentValue>[^\\s]+)" ], "datasourceTemplate": "docker", "versioningTemplate": "docker", "depTypeTemplate": "shell-script-docker-inline" } ] この設定により、 rhysd/actionlint:1.7.7 の部分が検出され、新しいバージョンがリリースされると自動でPRが作成されます。正規表現でマッチングするため、Dockerfile、シェルスクリプト、CI/CDの設定ファイルなど、あらゆるファイルに対して適用可能です。Dependabotではカバーできない領域まで自動アップデートの対象にできるのは、運用負荷の軽減に大きく貢献します。 おすすめ機能2: ローカルで設定ファイルをデバッグできる アップデートPRのグルーピング など、ファインチューニングをしようと思うと、設定ファイルのトライアンドエラーがつらいです。これはDependabotでも同じだと思いますが、Renovateは開発PCでも動かせるCLIがあるため、手元でカジュアルに設定ファイルとにらめっこが可能です。 $ LOG_LEVEL=debug npx renovate --platform=local --dry-run=full | tee renovate-dryrun.txt この --dry-run オプションを使うと、実際にPRを作成せずに、どのような更新が検出されるかをローカルで確認できます。設定を変更して即座に結果を確認できるため、トライアンドエラーのサイクルが非常に高速です。 設定ファイルのvalidatorも付随しています。 $ npx --yes --package renovate -- renovate-config-validator このコマンドで、renovate.jsonの構文エラーや設定の妥当性をチェックできます。CI/CDに組み込んでおけば、設定ミスによる実行エラーを事前に防ぐことができます。 えっ...しょぼくない?と思われたかもしれませんが、Dependabotの場合は設定を変更するたびにGitHubにpushして結果を待つ必要があり、フィードバックループが長いです。Renovateはローカルで即座に確認できるため、スピーディーに設定ファイルを完成させることができました。個人的には大きなメリットであると考えます。 おすすめ機能3: 設定ファイルを共通化できる 苦労して完成させた設定ファイルをSSOT(Single Source of Truth)にしたいですよね。Renovateには Config Presets という、設定ファイルの共通化機能があります。 共通設定リポジトリに default.json を配置し、そこにベースとなる設定を記述します。例えば、PRのラベル設定、スケジュール設定、グルーピングルールなど、組織として統一したい設定をまとめておきます。 利用側の設定ファイルはこれだけで済みます。 { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": ["github>kinto-dev/dbre-renovate-config"] } github> プレフィックスでGitHubリポジトリを指定するだけで、共通設定を読み込むことができます。ブランチやタグを指定することも可能です(例: github>kinto-dev/dbre-renovate-config#v1.0.0 )。 もちろん、利用側リポジトリ特有の設定を拡張することもできます。 { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": ["github>kinto-dev/dbre-renovate-config"], "ignorePaths": [ "backup/**" ] } この機能により、複数リポジトリで共通の設定を使いつつ、各リポジトリ固有の要件にも対応できます。組織で管理するリポジトリが増えれば増えるほど、この機能の恩恵は大きくなります。 まとめ 以上、Renovateのおすすめの実行方法と、Dependabotにはない3つの魅力的な機能を紹介させていただきました! 改めてまとめると以下の通りです。 CLI実行で開発環境と同じツールを使える - 既存のCI/CDワークフローを流用できる インラインスクリプトもアップデート対象にできる - Custom Managerで正規表現マッチング ローカルでデバッグできる - 高速なフィードバックループで設定を洗練できる 設定ファイルを共通化できる - 複数リポジトリで一貫した運用が可能 最近全部AIでいいんじゃないかと思うことも多々ありますが、決められた定型作業であれば非AIツールもまだまだ有益だと信じて、ツールボックスを拡充していければと思います。お読みいただきありがとうございます。
アバター
はじめに こんにちは、KINTOテクノロジーズ CloudSecurityグループの小林です。 皆さん、AWS Configのコストが高いなと思ったことはありませんか? 今回、記録方式の最適化で約80%のコスト削減を実現しました。 本記事ではその過程と得られた知見を共有します。 本記事の対象読者 AWS Configのコストが気になっている方 AWSのコスト最適化に取り組んでいる方 セキュリティ要件とコストのバランスを考えている方 大規模なAWS環境を運用している方 Control TowerやOrganizationsを使って複数アカウントを管理している方 AWS Configとは AWS Configは、AWSリソースの設定変更を記録・追跡するサービスです。 主な用途は以下の通りです: リソース設定の変更履歴を記録 コンプライアンスルールへの準拠状況を監視 セキュリティ基準違反の検知 設定変更の監査証跡として活用 背景:なぜ見直しが必要だったのか AWS Configは便利なサービスですが、 記録回数に応じて課金されるため、大規模環境ではコストが大きくなりがちです。 主な要因は以下の通りです。 記録対象リソースの増加(特にネットワーク関連リソース) 連続記録モードによる頻繁な記録 Control Tower環境での課題 弊社では AWS Control Towerを使用して複数アカウントを管理しています。 AWS Configのコスト削減を検討する中で、取りうる選択肢は以下の2つでした。 選択肢1: 現状維持(全て連続記録) コストが高いまま Control Towerのベストプラクティスに従う 選択肢2: StackSet (aws-control-tower-customizations) で記録頻度を最適化 AWSソリューション: aws-control-tower-customizations 大幅なコスト削減が期待できる Control Towerが作成したConfigリソースを変更することになる Control Tower のベストプラクティスとの兼ね合い AWS Control Towerの公式ドキュメントには、以下のような記載があります: 「AWS Control Towerによって作成されたリソースを変更または削除しないでください。」 出典元: AWS Control Tower リソースの作成と変更に関するガイダンス この記載により、選択肢2の採用には慎重な姿勢を取らざるを得ませんでした。 具体的な懸念事項は以下の通りです。 Configレコーダーの変更がControl Towerの動作に影響しないか ドリフト検出機能が正しく動作するか コンプライアンスレポートの正確性が保たれるか ランディングゾーンの更新やOUの再登録が必要にならないか このため、コスト削減の必要性は認識していたものの、実施に踏み切れない状況が続いていました。 記録回数の実態 記録回数を調査したところ、以下のリソースタイプの記録回数が特に多いことがわかりました。 記録回数が多かったリソースタイプ TOP4: EC2 NetworkInterface - ネットワークインターフェースの状態変化 EC2 Subnet - サブネットの状態変化 EC2 SecurityGroup - セキュリティグループの関連付け変化 Config ResourceCompliance - コンプライアンスチェック なぜこれらの記録回数が多いのか? 連続記録モードでは、リソースに何らかの変更(内部状態の変化も含む)があるたびに記録されます。 これらのリソースの記録が多い原因は、ENIの作成/削除を起点とした連鎖的な記録にありました。 ① EC2 NetworkInterface(根本原因) ECSタスクの起動/停止に伴いENIが頻繁に作成・削除されており、そのたびにConfigの記録が発生 VPC接続を有効化したLambda関数の場合も同様です。 ② EC2 Subnet(ENI に連動) ENIの作成・削除に伴い、対象のサブネットの設定項目が記録 VPC接続を有効化したLambda関数の作成時も同様です。 ③ EC2 SecurityGroup(ENI に連動) ENIの作成・削除に伴い、その ENIに関連付けられたSecurityGroupの設定項目も記録 ④ Config ResourceCompliance(すべてに連動) AWS::Config::ResourceCompliance は、Configルールによって評価されたリソースのコンプライアンス状態の変化を記録するリソースタイプです。 上記の各リソースで新しい設定項目が記録されるたびにConfigルールの評価が走り、その結果がResourceComplianceとして記録されます。 まとめると: ENIの変更が起点となり、関連するSubnet、SecurityGroupの記録が連鎖的に発生し、 さらにそれぞれのConfigルール評価が走ることで、記録回数が増加していました。 コンテナやサーバーレスを多用している環境ほど、この傾向は顕著になります。 解決策:記録頻度の最適化 検証の結果、選択肢2の aws-control-tower-customizations はControl Towerの検出コントロールやドリフト検出に影響しないことが判明しました。 こちらのソリューションはControl Tower側で変更があった場合にもドリフトが発生しないよう設計されているため、安全に記録頻度の変更を展開できると判断し、選択肢2の実施に踏み切りました。 方針:リソースタイプごとに記録方式を分ける すべてのリソースを一律に変更するのではなく、コスト構造を分析した上でリソースタイプごとに最適な記録方式を選択しました。 日次記録に変更したリソース: EC2 NetworkInterface EC2 Subnet EC2 SecurityGroup 連続記録のまま維持したリソース: 上記以外のリソースタイプ Config ResourceCompliance 日次記録非対応 連続記録は記録回数ベース、日次記録はリソース数ベースの課金です。 記録回数がリソース数に対して大幅に多いリソースタイプだけを日次記録に変更し、 それ以外は連続記録のまま維持するのが最もコスト効率が良い方法です。 展開方法 記録頻度の変更は aws-control-tower-customizations を利用し、管理アカウント上でCloudFormationテンプレートを展開することで、Control Tower管理下の全アカウントに一括適用しました。 セキュリティへの影響 日次記録にすると、変更の途中経過は記録されません。 また、Configルールの評価タイミングはルールのトリガー方式(変更通知トリガーか、定期評価か)によって異なります。 スケジュールベースの定期評価ルールは、記録頻度にかかわらず設定された評価間隔で実行されます。 一方で、設定変更検知ベース(変更通知トリガー)のルールについては、日次記録の場合、評価に利用される設定情報が最大24時間前の状態となるため、実際の設定違反検知が最大24時間遅延しうる点に注意が必要です。 ただし、弊社環境では以下のサービスと併用することで、セキュリティ要件は維持できると判断しました。 CloudTrailでAPIレベルの変更履歴は引き続き記録される Security Hubでのセキュリティ準拠チェック GuardDutyでの異常検知 SIEMサービスを利用した通知・分析 結果 以下はCost Explorerでの日別コスト推移です。 切り替え前後でコストが大幅に低下していることがわかります。 学び・Tips 1. コスト構造の理解が重要 AWS Configの料金は「記録回数」に基づくため、以下を理解することが重要です。 どのリソースタイプが多く記録されているか なぜそのリソースが頻繁に記録されるのか 記録頻度を変更できるリソースはどれか リソースタイプ別の記録回数は、CloudWatch メトリクス(AWS/Config ネームスペース)の ConfigurationItemsRecorded をResourceType別に確認できます。 AIエージェントにCloudWatchを参照させて調査することもできます。 また、リソース数は以下のコマンドで取得できます。 aws configservice get-discovered-resource-counts --region ap-northeast-1 2. この最適化が向いていないケース すべてのリソースでリアルタイム記録が必須 変更の途中経過も記録が必要 セキュリティ要件が厳しく、日次記録では不十分 Configルールを利用した自動修復などを利用している まとめ 今回は記録回数の多いリソースタイプを特定し、日次記録に切り替えることで約80%のコスト削減を実現しました。 セキュリティ要件はCloudTrail、Security Hub、SIEMなどで補完できるため、実運用上の問題もありません。 本記事が同様の課題を抱えている方の参考になれば幸いです。 参考資料 AWS Config 料金 AWS Config の記録モード
アバター
こんにちは! Principal Generative AI Engineerの森田です。私の所属するAIファーストGでは、社内の生成AI活用にとどまらず、販売店やトヨタグループにおけるAI活用支援を行っております。 KINTOテクノロジーズでは、AIファーストを掲げ、全社員が必要な生成AIツールを申請し利用することができます。開発に関するものだけでもClaude Code、GitHub Copilot、Devin、Kiroなど、開発者が選べる環境が整っています。 今回は、社内でも特に利用者が多いClaude Codeのサンドボックス機能について調査しました。サンドボックスとは、Bashコマンドの実行をファイルシステム・ネットワークの両面からOSレベルで隔離するセキュリティ機能です。 はじめに Claude Codeを使っていると、こんな場面に遭遇しないでしょうか。 コードの修正やコマンドの実行を任せると、操作のたびに「許可しますか?(Y/N)」と確認が入ります。意図しない操作を防ぐための仕組みなので当然ではあるのですが、これが何十回と続くと正直つらい。かといって、確認なしの自動承認モードにするのは怖い。プロンプトインジェクションやサプライチェーン攻撃など、外部からの脅威を考えると、何でも自動承認するわけにはいきません。 毎回確認していたら承認疲れで結局よく読まずに「Y」を押し続けてしまう。これが一番よくないパターンです。私自身、まさにこの状態に陥っていました。 そんな中、社内の勉強会で同僚の太田さんがサンドボックス機能を紹介していました。ファイルシステムとネットワークの操作範囲をOSレベルで制限することで、「この範囲内なら自由にやらせていい。万が一おかしな操作があっても、被害を最小限に抑えることができる」という状態を作れるという説明でした。 承認疲れから解放されつつ、セキュリティも確保できる。早速自分でも追加調査を行い、実際にどこまで堅牢なのかを手元で検証してみました。本記事はその結果をまとめたものです。なお、検証はmacOS(Seatbelt)環境で行っています。 サンドボックスとは Claude Codeのサンドボックスは、Bashコマンドの実行をファイルシステム・ネットワークの両面からOSレベルで隔離するセキュリティ機能です。 領域 デフォルトの制限 ファイルシステム カレントディレクトリ配下は読み書き可能。それ以外は読み取り専用 ネットワーク 許可されたドメインのみアクセス可(ホワイトリスト形式) OSのネイティブ機能で強制されるのが大きな特徴です。macOSではSeatbelt(カーネルレベルのサンドボックス機構)、Linux/WSL2ではbubblewrapが使われます。 なぜ自動承認が安全になるのか サンドボックスが有効な状態では、書き込みがプロジェクト内に閉じ、ネットワーク通信も許可ドメインに制限されます。つまり、プロジェクトに関係のないファイルが破壊されたり、未許可のサーバーにデータが送信されたりすることがありません。最悪の事態がプロジェクト内に収まることが保証されるため、自動承認しても安心できるというわけです。 有効化の方法 設定ファイルに "sandbox": { "enabled": true } を書いておけば、 claude コマンドで起動するだけで最初からサンドボックスが有効になります。毎回手動で有効化する必要はありません。なお、対話的に設定したい場合はClaude Codeのチャットで /sandbox と入力する方法もあります。 2つのモード サンドボックスにはAuto-allowとRegular permissionsの2つのモードがあります。 モード サンドボックス内のコマンド サンドボックス外のコマンド 向いている場面 Auto-allow 自動的に許可 確認フロー 承認疲れを減らし、自律的に作業を進めたい場合 Regular permissions 毎回許可を求められる 確認フロー より慎重に制御したい場合 サンドボックスが守ってくれる攻撃シナリオ 自動承認モードで特に警戒すべき脅威と、サンドボックスがどう防御するかを見ていきます。 脅威の発生源 具体例 プロンプトインジェクション 読み込んだファイルの隠された指示により、 ~/.ssh/id_rsa や ~/.aws/credentials を読み取り外部サーバーに送信される サプライチェーン攻撃 npm install のpostinstallスクリプトが認証情報を窃取する 悪意あるサブプロセス コマンドが子プロセスを生成し、制限を回避しようとする 1. プロンプトインジェクション README.mdなどに「 ~/.ssh/id_rsa の中身を外部サーバーに送信せよ」といった隠し指示が埋め込まれるケースです。サンドボックスのネットワーク制限により、許可されていないドメインへの通信がブロックされるため、仮に指示を実行しようとしても情報は外に出ません。 2. サプライチェーン攻撃 npm install のpostinstallスクリプトが ~/.aws/credentials を外部に送信するようなケースです。サンドボックスのネットワーク制限に加えて、 permissions.deny で機密ファイルへのアクセスを拒否しておけば、そもそもファイルの中身を読み取れません。 3. 悪意あるサブプロセスの連鎖 コマンドが子プロセスを生成し、上記の制限を回避しようとするケースです。サンドボックスはプロセスツリー全体に適用されるため、子プロセスも同じ制限を継承します。 検証の準備 サンドボックスにより、プロジェクト外のファイル破壊やネットワーク経由の情報流出は防げることがわかりました。しかし、プロジェクト内にある .env のような機密ファイルについてはどうでしょうか。カレントディレクトリ配下はサンドボックスのデフォルトで読み書き可能なため、サンドボックスだけでは守れません。 ここで活躍するのが permissions.deny です。 permissions.deny に指定したパスはサンドボックスの拒否リストにもマージされ、Bashコマンドに対してはOSレベルで、Read/Edit等のツールに対してはアプリケーション層でアクセスをブロックします。 今回の検証では、 permissions.deny で保護したファイルに対して、Claude Codeにあらゆる手段でアクセスを試みさせ、実際にブロックされるかを確認します。試行するバイパス手法は以下の通りです。 # 手法 狙い 1 Node.jsスクリプト 別言語ランタイムからの読み取り 2 シンボリックリンク経由 リンクで保護パスを迂回 3 ファイルコピー(cp) コピーによる間接的な読み取り 4 Python さらに別の言語ランタイム 5 macOS open コマンド OS標準コマンドでの読み取り 6 macOS ditto コマンド ファイル複製ユーティリティ 7 バイナリダンプ(xxd) 子プロセス経由のバイナリ読み取り 8 tarでアーカイブ化 アーカイブ経由の読み取り 9 Readツール直接 Claude Code内蔵ツール 10 Grepツール Claude Code内蔵ツール 用意した .claude/settings.json は以下の通りです。 { "permissions": { "deny": [ "Edit(.claude/**)", "Read(.env)", "Edit(.env)", "Read(./secrets/**)", "Edit(./secrets/**)" ] }, "sandbox": { "enabled": true, "autoAllowBashIfSandboxed": true, "allowUnsandboxedCommands": false, "network": { "allowedDomains": [ "github.com", "api.github.com" ] } } } permissions.deny で .env と ./secrets/** を明示的にブロックし、検証用のダミーファイルとして .env (ダミーの秘密情報)と secrets/credentials.json を配置しました。 allowUnsandboxedCommands: false は、コマンドがサンドボックスの制限に引っかかって失敗した場合の挙動を制御します。デフォルトの true ではサンドボックスの外で再実行を試みますが、 false にすると失敗したらそのまま失敗。サンドボックスの外には一切出られなくなります。 なお、今回はファイルシステム制限に焦点を当てており、ネットワーク制限の検証は対象外です。 検証結果 基本的なアクセス制御 サンドボックスを有効にした状態で、Claude Codeにファイルの一覧を確認させたところ、 .env と secrets/ は一覧にすら表示されませんでした。 sandbox/ ├── .claude/ │ └── settings.json ├── src/ │ └── app.js ├── CLAUDE.md └── TESTS.md 実際には .env と secrets/ が存在しますが、 ls でもGlobツールでも見えません。 secrets/ 配下にどんなファイルがあるかすらわからない状態です。 バイパス出来ないかClaude Codeで検証 Claude Codeに「 .env をどうにかして読み取ってほしい」と依頼し、あらゆる手法を試させました。 代表的な出力を2つ紹介します。 1. Node.jsスクリプトでの試行では EPERM が返りました。 $ node src/read_env.js Failed to read .env: EPERM: operation not permitted, open '/path/to/sandbox/.env' 5. macOSの open コマンドでは、ファイルが存在しないかのように振る舞いました。 $ open .env The file .env does not exist. 他の手法もすべて同様にブロックされました。結果の一覧は以下の通りです。 # 手法 結果 1 Node.jsスクリプト EPERM: operation not permitted 2 シンボリックリンク経由 Operation not permitted 3 ファイルコピー(cp) Operation not permitted 4 Python PermissionError: Operation not permitted 5 macOS open コマンド The file .env does not exist. 6 macOS ditto コマンド Cannot get the real path for source 7 バイナリダンプ(xxd) Operation not permitted 8 tarでアーカイブ化 Cannot stat: Operation not permitted 9 Readツール直接 ブロック 10 Grepツール ブロック permissions.deny に指定したパスはOSカーネルレベルでブロックされるため、プログラミング言語やコマンドを変えても回避できません。Bashツールから起動されるプロセスはすべて同じポリシーを継承します。 まとめ Claude Codeのセキュリティは、サンドボックスと permissions.deny の2段構えで成り立っています。 サンドボックスは、書き込みをプロジェクト内に閉じ、ネットワーク通信を許可ドメインに制限します。これにより、プロジェクト外のファイル破壊や未許可サーバーへのデータ送信が防がれ、自動承認モードを安心して利用できます。 さらに、特定のファイルやディレクトリをClaude Codeから見せたくない場合は permissions.deny が有効です。今回の検証では .env を題材に10種類のバイパスを試行し、すべてブロックされることを確認しました。 permissions.deny のルールはサンドボックスの拒否リストにマージされ、Bashコマンドに対してはOSカーネルレベルで、Read/Edit等のツールに対してはアプリケーション層で強制されるため、プログラミング言語やコマンドを変えても回避できません。 実運用では、サンドボックスの読み取り専用アクセスはプロジェクト外にも及ぶ点に注意が必要です。たとえば ~/Documents や ~/Desktop にはClaude Codeに見せる必要のないファイルがあるはずです。 permissions.deny でこれらのディレクトリを拒否しておけば、意図しない読み取りを防げます。 Claude Codeを日常的に使っている方は、ぜひサンドボックスの導入を検討してみてください。
アバター
はじめに my route開発部のAndroidエンジニア、Romie( @Romie_1112 )です。 my routeのAndroidチームではUIの実装をxmlからJetpack Compose(以下Compose)へと粛々と切り替えております。 現在は地域別の特集コンテンツを並べた画面をCompose化しています。 希望の順番で並べ替えることもできます。 以下の順番で初回表示を行います。 1. 画面遷移する 2. 希望の順番を初期値:おすすめ順に設定する 3. リクエストの時に希望の順番をAPIに渡す 4. データを取得する 5. 取得したデータの一覧を表示する 実装する中で 4. データを取得する 処理について迷ったので、今回はそのお話をしたいと思います。 初期化の実装方法 これまでの実装は、希望の順番を渡してAPIを叩いた結果を LiveData で通知し、 observe で監視して値を取得してから画面を表示していました。 そのため、値を取得する前の初期化処理は実装されていませんでした。 しかし今回Compose化に伴いUiStateの値が変わればリアクティブプログラミングで即Fragmentに反映する StateFlow に変えることにし、 LaunchedEffect(Unit) 内で初期化するよう実装しました。 ここで初期化の実装にあたり、私は次に挙げる2つの方法で迷いました。 1. initブロックで初期化する場合 intiブロックで初期化する場合、以下のような実装になります。 data class FeatureSummaryListUiState( val featureSummaryList: List<一覧のアイテム> = emptyList(), ) private val _sortType = MutableStateFlow(おすすめ順) private val _uiState = MutableStateFlow(FeatureSummaryListUiState()) val uiState = _uiState.asStateFlow() init { viewModelScope.launch { _sortType.collectLatest { sortType -> val summary = (APIを叩いてデータを取得) _uiState.update { it.copy( featureSummaryList = (設定したい初期値), ) } } } } setContent { MyRouteTheme { val uiState = viewModel.uiState.collectAsStateWithLifecycle().value FeatureSummaryListScreen( uiState = uiState, ) } } initブロックについての記載を 公式リファレンス [^1]から見てみましょう。 The primary constructor initializes the class and sets its properties. In most cases, you can handle this with simple code. If you need to perform more complex operations during instance creation, place that logic in initializer blocks inside the class body. These blocks run when the primary constructor executes. Declare initializer blocks with the init keyword followed by curly braces {}. Write within the curly braces any code that you want to run during initialization: initブロックは引用にもあります通り、インスタンスが形成された時に実行されるものになります。 インスタンスが形成された時に一度だけ呼ばれますので、初期化の処理を書くのにぴったりです。 ただし、initブロックはインスタンス形成時に呼ばれるという性質上、単体テストで初期化がちゃんとできているか見ることが厳しく、また単体テストの記載に慣れていないとinitブロックを考慮したテストを書くのが大変です。 2. LaunchedEffect(Unit)内で初期化する場合 では、FragmentからViewModel内の初期化処理をコールした場合はどうでしょうか。 最初に一度だけ呼ぶ処理だとコードを読む人に明示するため LaunchedEffect(Unit) の中に書くことをお勧めします。 data class FeatureSummaryListUiState( val featureSummaryList: List<一覧のアイテム> = emptyList(), ) private val _sortType = MutableStateFlow(おすすめ順) private val _uiState = MutableStateFlow(FeatureSummaryListUiState()) val uiState = _uiState.asStateFlow() fun initFeatureSummaryListUiState() { // initがfunになっています viewModelScope.launch { _sortType.collectLatest { sortType -> val summary = (APIを叩いてデータを取得) _uiState.update { it.copy( featureSummaryList = (設定したい初期値), ) } } } } setContent { MyRouteTheme { val uiState = viewModel.uiState.collectAsStateWithLifecycle().value LaunchedEffect(Unit) { viewModel.initFeatureSummaryListUiState() // ここが違う! } FeatureSummaryListScreen( uiState = uiState, ) } } Composeにおける副作用 [^2]に副作用( LaunchedEffect )の説明がございます。 副作用とは、コンポーズ可能な関数の範囲外で発生するアプリの状態の変化を指します。 コンポーザブルのライフサイクルとプロパティ(予測できない再コンポジション、異なる順序でのコンポーザブルの再コンポジション、破棄可能な再コンポジションなど)により、コンポーザブルは副作用がないようにするのが理想的です。 ただし、スナックバーを表示するなどの1回限りのイベントをトリガーする場合や、特定の状態で別の画面に移動する場合などに、副作用が必要になることがあります。 これらのアクションは、コンポーザブルのライフサイクルを認識している制御された環境から呼び出す必要があります。 そして、 こちらの章 [^3]により具体的な記載がございます。 コールサイトのライフサイクルと一致する作用を作成するには、Unitやtrueのような決して変化しない定数をパラメータとして渡します。 この実装には次の一般的なメリットがあります。 再利用性:どの箇所からでも呼び出せる テスト容易性:独立した関数で実装しているため単体テストがやりやすい また、プロジェクト内のメリットとして以下が挙げられます。 既存のコードとの整合性:他の箇所を確認したところCompose化した画面の初期化は LaunchedEffect(Unit) 内で行っていることが多く整合性が取りやすい ただし、デメリットもあります。 UDFの法則に反する:ViewModel→Fragmentという単方向でデータが流れる[^4]べきなのにFragment→ViewModelとなってしまう[^5] 依存度が高まり疎結合が崩れる:FragmentでViewModelの処理が呼ばれると依存度が高まりMVVMの目的の1つである疎結合が崩れる 呼び忘れる恐れがある: LaunchedEffect(Unit) をはじめどこからでも呼び出せる代わりに呼び忘れる恐れがある 補足:発展編 今回の内容についてより高度な議論をJaewoong Eum氏が こちらの記事 [^6]にて行っております。 Androidコミュニティに対してアンケートを取得した上で、Ian Lake氏のツイートを引用してinitブロックも LaunchedEffect(Unit) 内での初期化もアンチパターンであり SharingStarted.WhileSubscribed(5_000) を活用した初期値の設定を紹介しています。 ただ、私は以下の懸念について検討した上で今回は SharingStarted.WhileSubscribed(5_000) を使用しませんでした。 一般的な点では 可読性の低下:複数のプロパティを持つUiStateを SharingStarted.WhileSubscribed(5_000) で管理すると実装が複雑になり却って可読性が下がる プロジェクト内の点では 既存のコードとの整合性の低下: LaunchedEffect(Unit) 内で初期化している画面が多いことから既存のコードとの整合性が取りづらくなる です。 Jaewoong Eum氏の記事は今回ご紹介したものも含めて非常に勉強になりますので、全て英語ですが興味のある方は是非読んでみてください。 まとめ 今回は LaunchedEffect(Unit) 内で初期化したのですが、initブロックで初期化する場合と LaunchedEffect(Unit) 内で初期化する場合、2つのメリットとデメリットを比較した上で、以下の点を重視しました。 テスト容易性:独立した関数で実装しているため単体テストがやりやすい 既存のコードとの整合性:他の箇所を確認したところCompose化した画面の初期化は LaunchedEffect(Unit) 内で行っていることが多く整合性が取りやすい また、希望の順番を変えて並べ替えを行った時以下の順番で再表示を行います。 1. 並べ替えボタンを押下する 2. 希望の順番を任意の並べ替えに設定する 3. リクエストの時に希望の順番をAPIに渡す 4. データを取得する 5. 取得したデータの一覧を表示する ここから 4. データを取得する 処理を1つの関数で実装し、初回表示時も希望の順番を変えて並べ替えを行った時も希望の順番をAPIに渡して関数を呼び出す形にした方がいいと考えました。 よって再利用性も重視しました。 再利用性:どの箇所からでも呼び出せる 理想を追求するといろんな方法が出てきますが、アンチパターンとされているものがあっても正解は1つではないですし、チーム内でレビューすること・後々の拡張性やテスト容易性を考慮しその都度1番良い実装を選択できると良いですね。 一番大切なのは、自分なりに理由や根拠を明確にして実装することです。 読んでいただきありがとうございました。それでは次の記事で。 [^1]: 出典元: Classes: Constructors and initializer blocks: Initializer blocks より一部抜粋 [^2]: 出典元: Composeにおける副作用 より一部抜粋 [^3]: 出典元: rememberUpdatedState: 値が変化しても再起動すべきでない作用の値を参照する より一部抜粋 [^4]: ViewModel内の値をFragmentが参照できない(ViewModelで何が起きているかFragmentが知らない)状態 [^5]: FragmentがViewModel内で更新されている featureSummaryList を参照できる状態 [^6]: 出典元: Loading Initial Data in LaunchedEffect vs. ViewModel より一部抜粋
アバター
はじめに こんにちは、2025年12月入社の齋藤です! 本記事では、2025年11月・12月に入社したメンバー8名に入社直後の感想をお伺いし、まとめました。 KINTOテクノロジーズ(以下、KTC)に興味のある方、そして、今回参加してくださったメンバーへの振り返りとして有益なコンテンツになればいいなと思います! 齋藤 諒太 ![齋藤 諒太さんのプロフィール画像](/assets/blog/authors/dowod/dowod.png =300x) 自己紹介 KINTO開発部でフロントエンドエンジニアとして働いています。新潟県出身で今は大阪市在住です。 業務としては主にKINTOのシミュレーションや申し込み、マイページの開発を行っています。 前職ではRailsやNext.jsで構成された比較メディアサイトの開発をフロントエンド・バックエンドの領域を問わず担当していました。 趣味は自作PC、ゲーセン、ペットの猫をこねることです。よろしくお願いします。 所属チームの体制は? Osaka Tech Labに3人、室町オフィスに6人の合計9人のチームです。 1週間単位のスプリントで開発を進めています。毎週のプランニングでタスクを決め、レトロスペクティブで成果と課題を振り返ります。 毎日デイリースクラムで進捗を共有し、互いの状況を把握することで効率的な開発体制を維持し、短いサイクルで改善を重ねています。 チームの雰囲気はどんな感じ? 拠点や勤務形態が多様でオンライン中心ですが、不明点があればすぐに質問でき、相談もチーム内外で活発に行われています。 課題や改善案があればADRを通じて提案できます。ADRはアーキテクチャに限らず、チームのルールや方針を幅広く決めるための仕組みとして活用しており、誰でもカジュアルに新しいアイデアを発信し、継続的な改善を進められる環境です。 KTCへ入社したときの入社動機や入社前後のギャップは? これまで培った技術や知識を活かせる環境で働きたいと考え、以前から関心を持っていたKINTOのサブスクリプションサービスに、ユーザーとしてだけでなく開発者としても携わりたいと思い入社しました。 入社後、大きなギャップはありませんが、Osaka Tech Labは思っていたよりもまだ人数が少なく、落ち着いた雰囲気だった点はギャップかもしれません。 オフィスで気に入っているところ JCTと駅直結の利便性です。外部イベントも開催され、気軽に参加できるうえ、雨でも濡れずに出社できます。 フクロウさん ⇒ 齋藤 諒太さんへの質問 おすすめのアプリやサービスありますか? 10年以上1Passwordを使っています。覚えておくのは1Passwordのマスターパスワードだけで済み、強力なパスワードを自動生成して保存・同期してくれるのでとても楽です。さらにクレジットカード情報の管理機能や、パスワードの使い回し・漏えいを自動検出して通知するセキュリティ監査機能も備わっています。Windows、Mac、iOS、Androidに加え、ブラウザ拡張機能にも対応しており、ほぼすべての環境で使える点も魅力です。 うえぽん ![うえぽんさんのプロフィール画像](/assets/blog/authors/dowod/2026-03-02-newcomer-202511-12/uepon.png =300x) 自己紹介 デジタル戦略部DataOpsG所属となります! 前職はSESエンジニアとして多様な業種、システムにかかわってきました。 趣味は釣りで最近は月に1回程度しか行けていないので食卓と話題のネタを仕入れに行かなければ。という意気込みです! よろしくお願いします。 所属チームの体制は? チーム内でもデータの蓄積を行う基盤チーム、蓄積したデータを提供する仕組みを扱うnicolaチームという構成になっていて、全体で9名の体制です。 チームの雰囲気はどんな感じ? それぞれの強みを生かして日々業務や技術・知識習得に取り組んでいます。 共有の場では積極的に深掘りをしてチームとしての向上心が高いと感じています! KTCへ入社したときの入社動機や入社前後のギャップは? 特定の分野で技術を磨き自身の強みとしたい! フルスタックエンジニアとしての経験を活かせる! 入社前に丁寧な説明をしていただけて、業務環境についてギャップはなかったです。 オフィスで気に入っているところ 名古屋オフィスは駅の地下街から直結されているため悪天候の影響を受けずに出社できます! 齋藤 諒太さん ⇒ うえぽんさんへの質問 これまで多くの現場を経験されたとのことですが、特に印象に残っている現場はありますか? 銀行関係の現場なこともありセキュリティー意識がとても高かったです。(検証環境エリア、本番環境エリア共に作業者・作業理由・作業時間の事前申請必須など) また、利用者がいない時間に更新するため、深夜当番と早朝当番を月1でやっていました。 debugon ![debugonさんのプロフィール画像](/assets/blog/authors/dowod/2026-03-02-newcomer-202511-12/debugon.png =300x) 自己紹介 Engineering Officeでアクセシビリティを社内文化にする仕事をしている辻です。KTCには辻さんが何人かいらっしゃるので、私のことは debugon と覚えてください。 AIで音楽を作るのが趣味です。 所属チームの体制は? それぞれの専門領域を持つメンバーが、東京、名古屋、福岡で活動するチームです。 専門的な知識を生かしつつ、他のメンバーの専門性との化学反応を生かし、社内の様々なチームの力を最大限に発揮できるように共創しています。 チームの雰囲気はどんな感じ? 複数拠点で活動するチームなので、オンラインやオフラインでコミュニケーションをしっかりとっています。 「食べ物」の話しが好きなメンバーが多いので、食べる話になるとSlackチャンネルが盛り上がります。 KTCへ入社したときの入社動機や入社前後のギャップは? モビリティカンパニーに文化としてアクセシビリティの考え方を広めたくて入社しました。 「一緒に良いものを作っていきたい」という考えの方がたくさんいらっしゃるので、とてもやりがいを感じています。 オフィスで気に入っているところ トヨタ車の模型がたくさん置いてあって、それぞれの形を手で触って確認できたことがうれしかったです。 うえぽんさん ⇒ debugonさんへの質問 AIで音楽作成されるとのことですが、どんなジャンルの音楽が好きですか?制作に使うお気に入りのツールやソフトあれば教えてください! 音楽を作るときには Suno を使っています。ジャズが好きなのですが、気分のままにこれまでに聞いたいろいろなジャンルの音楽を思い出しながら作っています。 なかぴー ![なかぴーさんのプロフィール画像](/assets/blog/authors/dowod/2026-03-02-newcomer-202511-12/nakapy.jpg =300x) 自己紹介 障害者雇用枠で2025年12月に入社しました。在宅勤務です。 経歴としましてはSIerのエンジニアからキャリアをスタートして、事業会社の社内SE、PM、ITコンサルタントの経験があります。 伴走型のPMで、「餅は餅屋」をモットーに駆けずり回るスタイルでフットワークには定評がありました。 約1年半前に脳出血で左半身麻痺になりました。完全在宅の時短勤務で働けることが有難いです。 所属チームの体制は? 開発支援部人事グループの中の労務総務チームです。チームは自分を含めて4名です。 チームの雰囲気はどんな感じ? 定例会では休日の様子も共有し合って和やかな雰囲気です。 KTCへ入社したときの入社動機や入社前後のギャップは? 入社動機:経験を活かしてエンジニアの方のサポートが出来そうだと感じたから。 入社前後のギャップ:1on1が多い。激務な職場が多かったのですが、今は業務量を調整してもらえて有難いです。 オフィスで気に入っているところ 室町オフィスがあるコレド室町はお洒落な商業ビルで駅直結なのでリハビリを頑張って出社したいです。 debugonさん ⇒ なかぴーさんへの質問 お気に入りのデスクアイテムや文房具は? とにかく忘れないように、付箋を頻繁に使っています。シンプルなものが一番使いやすいです。 miurat ![miuratさんのプロフィール画像](/assets/blog/authors/dowod/2026-03-02-newcomer-202511-12/miurat.png =300x) 自己紹介 デジタル戦略部DataOpsGにデータエンジニアとしてジョインしました。 前職では、事業会社でデータ基盤構築やデジタルマーケティング関連の仕事に従事してきました。 趣味は、テニス、ゴルフでボールを打つことが好きです! 所属チームの体制は? メンバーは東京・名古屋・大阪の3拠点あわせて計9名です! チームの雰囲気はどんな感じ? チーム全体の雰囲気は風通しが良く、相談や雑談もしやすい雰囲気です。 また、好奇心旺盛なメンバーが多く、最新のトレンドや業界ニュースなどを積極的に共有し合う文化が根付いています。 KTCへ入社したときの入社動機や入社前後のギャップは? ビジョン・バリューに共感したからです! 入社後のギャップは、ドキュメントが整っているなと思いました! オフィスで気に入っているところ 大阪オフィスは、高層階の為、景色が綺麗です。また、駅直結なので、通勤が便利です! なかぴーさん ⇒ miuratさんへの質問 データ分析で心がけていることは何ですか? 誰もがストレスなく使えるよう、複雑さを取り除いたシンプルな設計と、データの品質維持を心がけています。 でこぽん ![でこぽんさんのプロフィール画像](/assets/blog/authors/dowod/2026-03-02-newcomer-202511-12/dekopon.png =300x) 自己紹介 Cloud Infrastructure G にエンジニアとしてジョインしました。 前職では AWS 専業のエンジニアとしてシステム開発やお客様の内製化支援などを行っていました。 趣味はテニスや登山で、主に神奈川の山を登ってます! 所属チームの体制は? 10名程度の組織で、サービスを支えるインフラチーム、中長期的な課題への改善を繰り返すカイゼンチーム、そしてトヨタグループの CCoE を支えるソリューションチームがあり、私はソリューションチームに所属しています。 チームの雰囲気はどんな感じ? 和気あいあいな雰囲気です。 お昼は出社しているメンバーのほとんど全員で外に出て神保町のいろいろな美味しいお店を開拓しています。 二郎系ラーメンを食べる人が多くいます。 KTCへ入社したときの入社動機や入社前後のギャップは? ビジョンに対してチームで前向きに進んでいけそうな雰囲気に魅力を感じました。 入社後のギャップも特になく、自由な雰囲気で成果を出していくことができると思います。 オフィスで気に入っているところ 神保町オフィスに在籍しているのですが、周りに美味しいお店が無限にあります! miuratさん ⇒ でこぽんさんへの質問 ストレス発散方法を教えてください! 同僚や友人とお酒を飲みに行くことです🍻 Yanaggy ![Yanaggyさんのプロフィール画像](/assets/blog/authors/dowod/2026-03-02-newcomer-202511-12/yanaggy.jpg =300x) 自己紹介 プロダクトマネージャーとして入社しました。 TOYOTA UPGRADE FACTORY/LEXUS UPGRADE FACTORYというクルマの「アップグレード」をWebで申し込めるサービスを担当しています。 漫画から小説までいろんな本を読むのが好きです。 所属チームの体制は? チームはFE/BEエンジニア、SRE、QA、ディレクター、PDMなど約10名からなり、東京、大阪にまたがっています。 PDMは東京1名、大阪1名の2名体制です。毎朝オンラインで話して案件状況や課題をシェアしながら案件を進めています。 チームの雰囲気はどんな感じ? 拠点は離れていますが、Slackの非同期コミュニケーション、オンラインでのデイリーMTG、ちょっとした相談など同期コミュニケーションを使い分けながら仕事を進めています。 KTCへ入社したときの入社動機や入社前後のギャップは? これまでは金融やデジタルコンテンツのシステム開発をしており、次は実物のあるモノに関わる業界で仕事したかったのと、小寺さんがインタビューで語っていた「最初のクルマ、最後のクルマ」のコンセプトにひかれたからです。 良いギャップとしては職務・職種の経歴、年齢層など思ってたより様々な背景を持ったメンバと仕事できるのが刺激的です。 オフィスで気に入っているところ 大阪オフィスの机が広い。あとJCTと呼ばれているイベントを行える広いスペース。社内外の様々なイベントが開催されています。 でこぽんさん ⇒ Yanaggyさんへの質問 Osaka Tech Lab 周辺で一番お気に入りのランチもしくは居酒屋があれば教えてください! 九州ラーメン亀王。高校生の時から通っているラーメン店です。オフィスから少し離れていますがよく行きます。 フクロウ ![フクロウさんのプロフィール画像](/assets/blog/authors/dowod/2026-03-02-newcomer-202511-12/fukuro.jpg =300x) 自己紹介 開発支援部人事G採用チームに配属。 これまで在宅でバックオフィス業務に加え、デザインや動画制作などのクリエイティブ業務も経験してきました。 昨年まで療養期間がありましたが、体力づくりを経て、最近は本格的に筋トレに取り組もうと考えています。 所属チームの体制は? 開発支援部人事G採用チーム(7名)に所属しています。 採用計画に沿って、募集・面接・進捗共有などを進めながら、より良い採用に向けて日々改善しています。 チームの雰囲気はどんな感じ? オンラインでのMTG参加はまだ多くありませんが、問題点の共有や改善に積極的に取り組む姿勢がうかがえます。 笑い声も絶えない和やかな雰囲気もあります。 KTCへ入社したときの入社動機や入社前後のギャップは? 障害者雇用という立場ではありますが、面接時、他社に比べて良い意味で特別扱いされすぎず、他のメンバーと同じように見てもらえている点にとても好感。 入社後も必要な配慮は十分過ぎるほどありつつ、想像していたようなギャップは特に感じていません。 オフィスで気に入っているところ 完全在宅のためオフィス出社の機会はありませんが、社内外の様々なイベントに参加してみたいなと思っています。 Yanaggyさん ⇒ フクロウさんへの質問 体力・健康維持のためにやっていることはありますか? 基本的な体調管理はもちろん、障害の特性的に、体温と気温、食事の温度などは常に気にしています。 さいごに みなさま、入社後の感想を教えてくださり、ありがとうございました! KINTOテクノロジーズでは日々、新たなメンバーが増えています! 今後もいろんな部署のいろんな方々の入社エントリが増えていきますので、楽しみにしていただけましたら幸いです。 そして、KINTOテクノロジーズでは、まだまださまざまな部署・職種で一緒に働ける仲間を募集しています! 詳しくは こちら からご確認ください!
アバター
はじめに|なぜ“AI × DesignOps”なのか? プロダクトが成長すればするほど、ビジュアルアセットの需要は指数関数的に増えていきます。しかし、デザイナーの数は(悲しいことに)指数関数的には増えません。 従来のイラスト制作は個人のスキルに依存しやすく、結果として品質のバラツキや制作スピードが開発ベロシティを阻害する「 デザイン負債(Design Debt) 」を生み出していました。DesignOps の本質は、デザインを「一点物のマニュアルアート」から「再現可能なシステム」へと昇華させることにあります。 私たちは今回、AI を単なる「便利な魔法の筆」ではなく、一貫性のあるデザインシステムを支える「 レンダリングエンジン 」として再定義する検証を行いました。 DesignOpsとは? 「DesignOps(デザインオプス)」という言葉、デザイナー以外にはまだ少し耳慣れないかもしれません。 簡単に言うと、 DesignOps は 「 デザイン版の DevOps 」です。 属人的になりがちなデザイン業務をプロセスとして整理し、担当者が変わってもチームとして一定の品質をデプロイできる状態を目指す考え方です。 今回の取り組みでは、生成 AI を「クリエイティブな遊び」としてではなく、プロダクトのコードベースの一部のように運用できるかを検証しました。特にエンジニア主体の組織においては、デザインも「 ロジックとして扱えるか 」が、持続可能な運用の鍵になります。 プロジェクトの背景:Unlimited App で直面した「成長の痛み」 Unlimited App ではこれまで、プロダクトデザイナーがイラストの世界観設計から品質の最終調整まで、いわば「職人のこだわり」をもって一貫して担ってきました。しかし、プロダクトが成長し、機能追加やコンテンツ拡張が加速するなか、必要とされるイラストの量と展開範囲は、私たちの想像を超えるスピードで拡大していきました。 https://kinto-jp.com/unlimited/app/ その過程で、個人の努力だけではカバーしきれない「 構造的なボトルネック 」が浮き彫りになってきたのです。 工数の比例増加 :表現を都度最適化する運用では、制作アセット数に比例して工数も積み上がっていく(まさに O(n) の世界です)。 再現性の設計難度 :クオリティの判断が個人の経験値に依存しやすく、「なぜこれが正解なのか」という基準を仕組みとして残しづらい。 属人的なバランス調整 :スピードと完成度のトレードオフを、常に個人の「さじ加減」で調整し続けなければならない。 これらは決して個人のスキルの問題ではなく、プロダクトが次のステージへ進むために解決すべき「 システム上の課題 」でした。 そこで生まれたのが、「 イラスト制作を個のスキルに依存する形から、再現性のある設計へとリファクタリングできないか? 」という問いです。私たちはこの問いに対し、生成 AI という強力なエンジンを DesignOps のプロセスに組み込むことで、持続可能な制作体制の構築に挑みました。 ※ [ ちょこっと技術解説 ]: O(n) とは? エンジニアがよく使う「計算量」を測る指標です。ここでは「描くイラストの枚数( n )」が増える分だけ、「制作時間」も正直に増えていくという 線形の現実 を指しています。 つまり、「 単なる「魔法」ではなく、10 倍の依頼が来たら 10 倍の工数が必要になる 」という、デザイナーにとっては少し切ない、そしてエンジニアにとっては「今すぐ最適化(リファクタリング)したい!」と血が騒ぐ状態のことです。 今回の検証アプローチ|“AIに寄せる” vs “AIを寄せる” AI を活用する際、戦略は大きく 2 つに分かれます: AI に寄せる(AI-Native Approach) AI が得意な表現(原生スタイル)をそのまま受け入れ、効率を最大化する。「AI っぽさ」を味方につける手法です。 AI を寄せる(Brand-Centric Approach) 独自のブランドアイデンティティに基づき、AI の出力を厳格に制御する。 Unlimited App はすでに確立された世界観を持つプロダクトです。そのため、前提条件として「AI を寄せる」 アプローチを選択しました。これは、プロンプトを単なる命令文ではなく、ブランドの 「デザイン・トークン(Design Tokens)」 として定義し、AI という不確実な実行環境において 「決定論的な出力(Deterministic Output)」を目指す、エンジニアリング的な挑戦でもあります。 イラスト標準化の設計思想とプロンプトアーキテクチャ 「AI なら、プロンプトひとつで何でも描いてくれるのでは?」——そんな期待は、運用フェーズに入った瞬間に崩れ去ります。実用的な DesignOps において、プロンプトは単なる「魔法の呪文」ではありません。明確な設計意図を AI へ伝えるための、厳密な「 インターフェース 」であるべきです。 標準化プロセスの構築にあたり、筆者は下図のような 7つのステップ を策定しました。ビジュアル定義の抽出(Step 1)からリファレンスの整理(Step 4)までは、いわば「 視覚的な仕様書 ( Visual Spec )」を書き上げる、設計の根幹を支えるフェーズです。 Step 1〜4:プロンプトを「エンジニアリング」するための前処理 Step 1|ビジュアル定義の抽出(Extracting Visual Tokens) 最初に取り組んだのは、デザインシステムとの整合性チェックです。2.5D の立体感、余白の持たせ方、低コントラストな配色……。これらを言語化する工程は、後に解説する 「 Style Tokens ( スタイル定数 )」 の基盤となります。 Step 2|イラスト用途の分類(Defining the Scope) 生成 AI の活用範囲を「クリエイティブな表現」ではなく、「 UX を補助するインフォグラフィック 」 と定義しました。目的を限定することで、維持すべき「再現性」と、AI に任せるべき「表現幅」の境界線が明確になります。 Step 3 & 4|リファレンスの構造解体(Deconstructing References) 大量のリファレンスを収集し、「好き嫌い」という感性ではなく、構図や影の強度といった「 Visual Spec(視覚仕様) 」として分解・整理しました。「なんとなく似ている」を「仕様通りである」に変えるための、最も泥臭く、かつ重要なリサーチ工程です。 この Step 1〜4 の本質は、「 AI に依存しない設計構造を人間側で作る 」 ことにあります。 このプロセスで最もエキサイティングであり、かつ重要なのが Step 5 の「プロンプト作成」 です。ここを単なる「作文」にせず、 エンジニアリング的に構造化された工程 として設計しました。 具体的には、プロンプトを以下の 2 層構造(Layered Architecture) として定義しています: Part 1:Style Tokens(スタイル定数層) 線画の太さ、立体感、陰影のルール、配色など、プロダクトの DNA を定義します。「何を描くか」に依存しない、 再利用可能な Constant(定数)レイヤー です。 Part 2:Content Variables(コンテンツ変数層) 「車」「人物」「スマホを持つ手」など、画面ごとに差し替え可能な Dynamic Variable(動的変数)レイヤー です。 この 「 スタイルとコンテンツの解耦(Decoupling) 」 こそが、DesignOps 視点での解決策です。これにより、「何を描いても同じトーンで出力される」という、デザイナーにとっての聖杯(Holy Grail)を目指しました。 ツールごとに異なる「Prompt の最適解」 検証を進める中で面白いことが分かりました。AI ツールごとに「プロンプトの癖」が全く違うのです。以前は同一のプロンプトで比較していましたが、現在は「構造(Part 1 / Part 2)は共通、実装(実際の記述)は各ツールに最適化」 という方針に切り替えました。 「プロンプトを共有する」のではなく、 「プロンプトを設計するインターフェース(ルール)を共有する」。この方が、ツールの進化に柔軟に対応できる持続可能な仕組みになります。 徹底検証:生成 AI 3 社の「性格診断」—— イラスト標準化の最適解を探る 2025年10月時点の Midjourney 検証 まずは、2025年10月に行った Midjourney(V7)での検証結果です。 当時の出力は、視覚的な完成度が非常に高く、一枚絵としての魅力は群を抜いていました。 しかし、標準化という観点では、いくつかの課題が見えてきました。 装飾的な要素が多く、情報量がやや過剰 構図がダイナミックで、並べた際の統一感が出にくい ブランドトーンよりも「生成AIらしさ」が前面に出る傾向 つまり、 創造性は高いが、制御性が低い。 この特性はクリエイティブ用途には適していますが、UI内で量産・運用するインフォグラフィック用途には不向きであると判断しました。 ChatGPT / Gemini へのシフト Midjourney との比較を経て、検証の軸を「表現力」から「再現性」へとシフトしました。 同一のビジュアルリファレンスと構造化プロンプトを用い、ChatGPT および Gemini で出力を比較しました。 この時点で明確になったのは、 ChatGPT は構図の安定性が高い Gemini はクリーンだが、再解釈が入る傾向がある という違いでした。 最新検証:観点別比較 プロンプトの構造を定義したところで、次なるステップは「どの実行エンジン(AI モデル)が最も安定して仕様通りに動くか」のベンチマークテストです。私たちは、構図・人物・色彩・命令遵守性の 4 つの観点から、プロダクト運用への適性を評価しました。 ① 構図の安定性—— UI に馴染むか? インフォグラフィックにおいて、余白と主体のサイズ感は「UI の整合性」に直結します。 ChatGPT は視点・余白・被写体のバランスが安定しており、複数枚を並べた際の一貫性が高い結果となりました。 一方 Gemini は、微妙な視点変更や背景処理の差異が発生しやすく、量産時の揺らぎがやや大きい傾向が見られました。 ② 人物表現の精度—— 意図が伝わるか? 「シートベルトを締める」「スマホを見る」といった具体的な動作の正確さを検証しました。 人物の顔パーツ・視線・身体バランスにおいて、ChatGPT は安定したクオリティを維持しました。 Gemini は柔らかいトーンで描写される一方、表情や骨格の一貫性に若干のばらつきが見られました。 ③ 用色とブランド整合性 ChatGPT は指定した色調レンジを忠実に守る傾向が強く、ブランドトーンとの整合性が高い結果となりました。 Gemini は自然なグラデーション処理を行う反面、色相・彩度が微妙に変化するケースがあり、厳密なトーン統制には追加調整が必要でした。 ④ 命令遵守性(Instruction Following)—— 仕様書通りに動くか? 最も大きな差はここでした。 ChatGPT はプロンプト構造(Part 1 / Part 2)をほぼそのまま出力に反映する傾向が強く、設計思想と出力結果の対応関係が明確でした。 Gemini は意図を解釈し、最適解を“再構成”する挙動が見られ、創造性は高いものの、決定論的制御はやや難しいという印象です。 ※ 正密に Gemini が過度な再解釈を試みるからこそ、私たちは Part 1(定数層)において、より厳密なビジュアルのガードレールを定義し、封鎖(Lockdown)する必要があるのです。 各ツールの本性:創作のパートナーか、信頼の実行エンジンか Midjourney:気分屋な天才アーティスト 2025 年 10 月時点の検証では、Midjourney は 圧倒的な 「 映え 」を誇りました。一枚絵としての完成度は素晴らしいのですが、標準化という観点では少し「個性が強すぎ」ました。 情報量が多すぎて UI の邪魔をしてしまう。 構図がダイナミックすぎて、並べた時に統一感が出にくい。 つまり、「 クリエイティブな爆発力はあるが、規律を守るのが苦手なタイプ 」です。 Gemini:再解釈を試みるクリエイティブ・ディレクター Gemini 3 Flash などの最新検証では、非常にクリーンな UI イラストを生成してくれますが、時折「自分の色」を出したがる傾向があります。 構図や余白が毎回少しずつズレる「自由奔放さ」。 プロンプトを忠実に守るというより、「 意図を汲み取ってリミックスしてしまう 」挙動が見られました。これは創作には良いですが、量産プロセスでは「予測可能性」を下げる要因になります。 ChatGPT (DALL-E):仕様書通りに動くシニアエンジニア 対照的に ChatGPT は、 プロンプトの構造をそのまま出力に反映する能力 ( Instruction Following )が極めて高いことが分かりました。 構図が安定し、用色も保守的でルール化しやすい。 まさに DesignOps における 「 信頼できる実行エンジン 」 です。 「イラストを作る(Make)」ツールではなく、「運用する(Ops)」ツール としての適性が最も高いと判断し、現在は ChatGPT を中心に検証を進めています。 ※ もちろん、表現の幅や偶発的なひらめきという点では、Midjourney や Gemini に軍配が上がる場面もあります。 実装結果:Unlimited App で何が変わったのか? 確立した標準スタイルは、現在 Unlimited App 内の「車種別イラスト」や「レベル選択カード」などで試験的に運用されています。「 AI で 8 割のベースを生成し、人間が最後の 2 割を整える 」というフローにより、制作スピードと一貫性が(ついに!)両立し始めました。 しかし、この取り組みは Unlimited App という「実験場」だけで完結するものではありません。私たちが構築したプロンプトの 2 層構造は、いわばデザインの「共通プロトコル」です。将来的には、「スタイル定数(Part 1)」というコンフィグを各ブランドの DNA に合わせて差し替えるだけで、社内のあらゆるプロダクトやサービスへこの仕組みを横展開(スケール)させていくことを目見据えています。プロダクトを跨いで「一貫性のあるビジュアルを即座にデプロイできる」状態——これこそが、私たちが目指す真の DesignOps の姿です。 やってみて分かった課題 数ヶ月の検証で分かったのは、プロンプトには「 賞味期限(Prompt Rot) 」があるということです。ツールのアップデートにより、昨日まで動いていた魔法の言葉が、今日には効かなくなる。 そのため、プロンプトを一度作って終わりにするのではなく、 継続的にリファクタリングしていく前提の設計 が必要です。今後は、これらのメンテナンスを自動化する「AI Agent 的なアプローチ」も視野に入れています。 まとめ:AI × DesignOps は何を変えうるのか 今回の検証を通じて確信したのは、生成 AI は単なる「魔法」ではなく、 DesignOps を再設計するための強力なトリガー であるということです。 標準化とは、自由を奪うことではありません。むしろ、「 どこを固定し(Constants)、どこを柔軟にするか(Variables) 」を定義することで、変化の激しいプロダクト開発においてクリエイティブな安定走行を可能にする行為です。 DesignOps は、デザインを「属人的な手癖」から「再現可能なプロセス」へと拡張します。この取り組みが、皆さんのプロダクトにおけるクリエイティブ運用のヒントになれば幸いです。
アバター
はじめに こんにちは、 Cloud Infrastructure G の山中です! AWS Amplify Gen 2 + CDK で構築した dev 環境で、CloudFormation のデプロイが失敗し続けるという問題に遭遇しました。エラーメッセージは以下の通りです: Stack:arn:aws:cloudformation:ap-northeast-1:XXXXXXXXXXXX:stack/amplify-XXXXX-CloudWatchLogsToS3Stack108915EF-XXXXX/XXXXX is in DELETE_COMPLETE state and can not be updated. 本記事では、この問題の原因究明から AWS サポートへの問い合わせ、そして最終的な解決までのプロセスを共有します。 背景 実施したリファクタリング CDK コードで、あるリソース群を cdk.Stack (独立したネストスタック)から Construct (親スタック内のリソースグループ)へ変更するリファクタリングを行いました。 この変更を行った理由は、 クロススタック参照の問題 を解消するためです。 クロススタック参照とは? CloudFormation で複数のスタック間でリソース(ARN など)を参照し合う仕組みです。便利ですが、参照元・参照先の削除順序によってはエラーが発生することがあります。 変更前: 独立したネストスタックとして定義 export class CloudWatchLogsToS3Stack extends cdk.Stack { // 独立したスタックとしてデプロイされる } 変更後: 親スタック内の Construct として定義 export class CloudWatchLogsToS3Stack extends Construct { // 親スタックの一部としてデプロイされる } 一見シンプルな変更ですが、これが思わぬ問題を引き起こしました。 発生した問題 問題の発生メカニズム Stack → Construct への変更により、CDK の Construct 階層が変わり、CloudFormation の 論理 ID (CloudFormation がリソースを識別するための内部的な名前)が変化しました。 【変更前の構造】 CloudWatchLogsToS3Stack (Stack) ├── Firehose0 ├── FirehoseRole0 └── ... 【変更後の構造】 CloudWatchLogsToS3Stack (Amplify Stack) └── CloudWatchLogsToS3StackResource (Construct) ├── Firehose0 ├── FirehoseRole0 └── ... この結果、以下の連鎖的な問題が発生しました: CloudFormation は論理 ID が変わったため「新規リソース」として作成を試みる しかし物理名(IAM ロール名、ロググループ名など)は既存のものと同じ 「リソースが既に存在する」エラーで CREATE_FAILED 作成に失敗したネストスタックが DELETE_COMPLETE 状態になる 親スタックが、削除済みネストスタックの ARN を参照し続ける(孤立した参照) 以降のデプロイで「 DELETE_COMPLETE 状態のスタックは更新できない」エラーが発生 特に厄介なのは手順 5 の状態です。親スタックのテンプレートに「存在しないネストスタック」への参照が残り続けるため、何をしてもデプロイが失敗するようになります。 エラーの詳細 CloudWatchLogsToS3Stack108915EF: Stack:arn:aws:cloudformation:ap-northeast-1:XXXXXXXXXXXX:stack/amplify-XXXXX-CloudWatchLogsToS3Stack108915EF-XXXXX/XXXXX is in DELETE_COMPLETE state and can not be updated. 試した対応(すべて失敗) 自力で以下の対応を試みましたが、いずれも解決には至りませんでした。 試した対応 結果 重複リソース(IAM ロール、ロググループ等)の手動削除 リソースは削除できたが、デプロイは失敗 DELETE_COMPLETE 状態のネストスタックを AWS コンソールから削除 既に削除済みのため操作不可 AWS CLI でルートスタックのテンプレートから参照を除去して update-stack 同じエラーで失敗 continue-update-rollback コマンドで問題リソースをスキップ スタック状態が UPDATE_ROLLBACK_COMPLETE のため使用不可 CDK コードで論理 ID を変更して新規作成 古い参照がルートスタックに残っているため失敗 どの方法でも、 親スタックが削除済みのネストスタック ARN を参照し続けている という根本問題を解決できませんでした。 AWS サポートへの問い合わせ 自力での解決が困難と判断し、AWS サポートに問い合わせました。 問い合わせ内容(要約) ルートスタックが DELETE_COMPLETE 状態のネストスタック ARN を参照し続けている テンプレート更新、 continue-update-rollback 、論理 ID 変更など試したが解決せず ルートスタックから古いネストスタック参照を除去していただくことは可能か AWS サポートからの回答 原則として、AWS 側にてお客様のスタックを操作することは行なっておりません。 お問い合わせのエラーを解消いただくには、親スタックにて管理されているリソースから子スタックを削除いただいた後に、再度子スタックを作成いただく必要がございます。 手順1: 親スタックの CDK コードより既に削除されている子スタックを作成されている処理をコメントアウトいただき、CDK コードをデプロイすることで、親スタックにて管理されているリソースから子スタックを削除いただく。 手順2: 手順1におけるコメントアウトを外し、再度 CDK コードをデプロイいただく。 つまり、2段階のデプロイで解決できるとのことでした。 解決手順 手順1: 問題のスタック作成処理をコメントアウトしてデプロイ 以下をコメントアウトしました: // CloudWatch Logs → S3エクスポート設定 // ============================================================ // 【手順1】DELETE_COMPLETE 状態のネストスタック参照を削除するため、 // 一時的にコメントアウトしています。 // ============================================================ // const logsExportStack = backend.createStack("CloudWatchLogsToS3Stack"); // const logsExportStackInstance = new CloudWatchLogsToS3Stack(...); // logsExportStack.addDependency(logsBucketStack); // RumConstruct の Firehose 連携も無効化 const rumStackInstance = new RumConstruct(rumStack, "RumStackResource", { // ... enableSubscriptionFilter: false, // true → false // firehoseStreamArn: logsExportStackInstance.firehoseStreamArns.get('rum')!, }); // rumStack.addDependency(logsExportStack); // SubscriptionFiltersStack もコメントアウト // if (appSyncApiId) { // const subscriptionFiltersStack = backend.createStack("SubscriptionFiltersStack"); // ... // } デプロイ後の確認: aws cloudformation list-stack-resources \ --stack-name amplify-XXXXX \ --output json | jq -r '.StackResourceSummaries[] | select(.LogicalResourceId | contains("CloudWatchLogsToS3") or contains("SubscriptionFilters"))' → 出力なし = 親スタックから参照が削除された ✅ 手順2: コメントアウトを解除して再デプロイ コメントアウトを全て解除し、元の状態に戻してデプロイしました。 結果: aws cloudformation describe-stacks --stack-name amplify-XXXXX \ --query "Stacks[0].StackStatus" # => "UPDATE_COMPLETE" → デプロイ成功 ✅ 学んだこと 1. CDK の Construct 階層変更は要注意 Stack → Construct への変更のような、一見シンプルなリファクタリングでも、CloudFormation の論理 ID が変わる可能性があります。 :::message 対策 : 本番環境に適用する前に、必ず cdk diff で論理IDが変更されているかを確認しましょう。論理 ID の変更は「リソースの再作成」を意味するため、影響範囲を把握することが重要です。 ::: 2. 「孤立した参照」問題は厄介 ネストスタックが DELETE_COMPLETE 状態になると、親スタック側にそのスタックへの参照が残り続けます。 その結果、CloudFormation は削除済みスタックを更新しようとして失敗し、通常の更新操作では復旧できない状態に陥ることがあります。 3. 2段階デプロイが有効 このような孤立した参照問題には、「問題箇所をコメントアウト → デプロイ → 解除 → 再デプロイ」という2段階の手順が有効です。1回目のデプロイで親スタックから参照を削除し、2回目で新規作成するという流れです。 4. 困ったら AWS サポートへ 自力で解決できない問題に遭遇した場合、AWS サポートへの問い合わせが有効です。今回は「AWS 側での直接操作はできない」という回答でしたが、代わりに的確な回避策を教えていただきました。 まとめ CloudFormation のネストスタックが DELETE_COMPLETE 状態で更新不能になる問題は、以下の手順で解決できました: 手順1 : 問題のスタック作成処理をコメントアウトしてデプロイ(親スタックから参照を削除) 手順2 : コメントアウトを解除して再デプロイ(スタックを新規作成) CDK/CloudFormation を使用している方で同様の問題に遭遇した場合、この記事が参考になれば幸いです。 参考リンク AWS CloudFormation ユーザーガイド - ネストされたスタック AWS CDK 開発者ガイド AWS Amplify Gen 2 ドキュメント
アバター
はじめに Vibe Coding、楽しいですよね。 Claude Codeに「こんな感じで作って」と伝えるだけで、AWSのリソースを使ったアプリがサクサク出来上がっていく。自分でコードを書く量は激減して、PoCなんてあっという間に完成する。 …と思っていた時期が、僕にもありました。 一人で作ったPoCを別の担当者に引き継ごうとしたら、 新環境でアプリが動かない 。原因を調べようにも、Vibe Codingで作ったから コードの中身を自分でも把握していない 。結局、原因解明に 約1週間 溶かしました。 この記事では、何が起きたのか、なぜ時間がかかったのか、そしてどうすれば防げたのかを共有します。 何を作っていたか Claude Codeを使って、一人でPoCを開発していました。 構成はこんな感じ: Amazon DynamoDB – データストア Amazon Bedrock Agent – LLMを使った処理 Amazon Cognito – 認証 典型的なサーバーレス構成です。Vibe Codingでガンガン作って、旧環境(開発用AWSアカウント)ではちゃんと動いていました。 事件:新環境で動かない 引き継ぎのタイミングで、新環境(別のAWSアカウント)にデプロイして動作確認をしました。 動かない。 エラーは出るけど、原因がよくわからない。Claude Codeにデバッグさせても、的を射た回答が返ってこない。 「コードを読めばわかるでしょ」と思うかもしれませんが、Vibe Codingで作ったので 自分でもコードの詳細を把握していない んですよね。どこを見ればいいかすらわからない。 原因:AIが勝手に書いたフォールバック値 結局、新環境と旧環境のデプロイ後の挙動の違いをClaude Codeに分析させて、やっと原因がわかりました。AWSのリソースやCloudWatchログを片っ端から参照させた結果です。 原因は 環境変数のフォールバック値 でした。 // ※ 以下はAIが生成した例示コードです。実際のコードとは異なります。 // config.ts export const config = { dynamoTableName: process.env.DYNAMO_TABLE_NAME || "dev-user-table", bedrockAgentId: process.env.BEDROCK_AGENT_ID || "ABCD1234EF", cognitoUserPoolId: process.env.COGNITO_USER_POOL_ID || "ap-northeast-1_XyZ123", cognitoClientId: process.env.COGNITO_CLIENT_ID || "1a2b3c4d5e6f7g8h9i0j", }; AIが「環境変数が設定されていない場合に備えて」と気を利かせて、フォールバック値を書いていたんです。 旧環境では、たまたまこのフォールバック値が指すリソースが存在していたので動いていた。でも新環境では別のAWSアカウントなので、当然そんなリソースは存在しない。だから動かない。 しかもエラーメッセージを見ても「リソースが見つかりません」としか出ないから、 環境変数の問題だと気づけなかった 。 なぜ1週間もかかったのか 正直に言います。 自分がコードをほとんど読まなかったから です。 Vibe Codingの快適さに甘えて、AIが生成したコードをちゃんと確認していませんでした。だから問題が起きたときに「どこを見ればいいか」がわからない。 Claude Codeにデバッグさせても、ピンポイントで原因に辿り着けない。結局、新旧環境の挙動の違いをCloudWatchログレベルで比較させて、やっと「あ、ここか」となりました。 Vibe Codingで楽をした分、トラブル時のツケを払わされた感じです。 引き継ぎ相手も困っていた 自分だけじゃなく、引き継ぎ相手も困っていました。 コード量が多くて、全体像を把握する時間が足りない どこが重要なコードなのかわからない ドキュメントもない 最終的にAIにアーキテクチャ図を生成させて、やっと自分でも全体像がなんとなくわかった状態でした。コードだけ渡されても、正直 自分でも説明できない 。 これは引き継ぎとして完全に失敗です。 教訓:Vibe Codingで引き継ぎを壊さないために この経験から得た教訓を共有します。 1. AIには必要最小限の仕事だけさせる 「あると便利かも」でコードを追加させない。依頼していない機能を勝手に実装されると、把握できないコードが増えるだけ。 2. Agent.md(CLAUDE.md)で余計なことをさせない制御 プロジェクトルールを明文化しておく。特に「やってはいけないこと」を書いておくのが重要。 3. コードレビューを徹底する AIが生成したコードであっても、人間によるレビューは不可欠です。これにより、不要なコードや潜在的な問題を早期に発見し、コードの品質を維持することができます。 <!-- ※ 以下はAIが生成した例示です。プロジェクトに応じてカスタマイズしてください。 --> # プロジェクトルール ## 環境変数 - 環境変数にフォールバック値(デフォルト値)を絶対に書かない - 環境変数が未設定の場合は明示的にエラーを出すこと - 環境変数の一覧は `.env.example` に記載し、常に最新化する ## コード規模 - 実装は必要最小限にする。「あると便利かも」で追加しない - 1ファイル300行を超えたら分割を検討する - 使われていないコードは即削除する ## ドキュメント - アーキテクチャ図(`docs/architecture.md`)を常に最新に保つ - 新しいAWSリソースを追加したら、必ずアーキテクチャ図を更新する - READMEのセットアップ手順は、新環境で動くことを前提に書く ## やってはいけないこと - ハードコードされた認証情報・リソースID - 「とりあえず動く」ための回避策(後で必ず負債になる) - 依頼されていない機能の追加 3. 環境変数はフォールバック値なしで即エラーにする 今回の問題を防ぐなら、こう書くべきでした: // ※ 以下はAIが生成した例示コードです。実際のコードとは異なります。 // config.ts const requireEnv = (key: string): string => { const value = process.env[key]; if (!value) { throw new Error(`環境変数 ${key} が設定されていません`); } return value; }; export const config = { dynamoTableName: requireEnv("DYNAMO_TABLE_NAME"), bedrockAgentId: requireEnv("BEDROCK_AGENT_ID"), cognitoUserPoolId: requireEnv("COGNITO_USER_POOL_ID"), cognitoClientId: requireEnv("COGNITO_CLIENT_ID"), }; これなら環境変数が未設定のとき、すぐにエラーで気づける。 4. ドキュメントを自動生成・自動更新する仕組みを作る コードだけでは引き継ぎできない。アーキテクチャ図やREADMEは必須。 できればコードの変更に合わせて自動更新される仕組みを作りたい。完璧は無理でも、「コードとドキュメントがズレている」状態は避けたい。 まとめ まとめ Vibe Codingは楽しいし、生産性も上がる。でも 引き継ぎ という観点では落とし穴がある。 AIが「気を利かせて」書いたコードが、別環境で問題を起こす 自分でコードを把握していないから、トラブル時に対応できない 引き継ぎ相手もコードを理解できない 100%コントロールするのは無理でも、できるだけ手綱を握っておく のが大事だと痛感しました。 Vibe Codingをやるなら: Agent.mdで「やってはいけないこと」を明文化する AIには最小限の仕事だけさせる ドキュメントは最初から用意しておく AIが生成したコードも必ず人間がレビューする この記事が、同じ轍を踏む人を一人でも減らせたら嬉しいです。
アバター
こんにちは。KINTOテクノロジーズのEngineering OfficeでAccessibility Advocateとして働いている辻勝利です。 今回は、去る1月15日に同チームの皆様向けに開催した「障害平等研修(Disability Equality Training)」についての開催報告をしたいと思います。 そもそも「障害平等研修」とはなにか、なぜEngineering Officeの有志向けに最初の研修を開催したのかなど、お話しできればと思います。 1. はじめに:なぜ「技術」の組織が「マインド」を学ぶのか アクセシビリティの分野に携わる中で、私はある「失敗」を多く目にしてきました。それは、アクセシビリティが「障害者のための特別な対応」と定義された瞬間、優先度が下がり、多忙を理由に見送られてしまうという現実です。 これを変えるには、手法(How)の前に、アクセシビリティを追求する「意義(Why)」を社内文化として根付かせることが不可欠です。昨年11月にKINTOテクノロジーズに入社して以来、私が「文化形成」を最重視しているのはそのためです。その第一歩として、まずは身近なEngineering Officeのメンバーを対象に「障害平等研修(DET: Disability Equality Training)」を実施しました。 2. 障害平等研修(DET)とは DETは1990年代のイギリスで誕生しました。日本でも「障害者差別解消法」の施行や、東京2020大会のボランティア研修に採用されるなど、世界標準のプログラムとなっています。 最大の特徴は、障害者自身がファシリテーターを務めること、そして「教わる」のではなく「参加者同士の議論」を通じて気づきを得ることです。今回は約1時間のダイジェスト版として、「障害とは何か?」「障害はどこにあるのか?」という本質的なテーマを深掘りしました。 3. 参加者の属性 今回はEngineering Officeを中心に、名古屋や福岡など各拠点から5名が室町オフィスに集結しました。普段リモートワークが多い私ですが、あえて対面形式を選んだのは、温度感のある深い対話の場を作りたかったからです。お菓子を囲み、リラックスした雰囲気の中で研修はスタートしました。 4. 研修の様子:問題を「発見」するプロセス 研修ではイラストやビデオを用い、日常に潜む「問題」を探し出しました。 印象的だったのは、参加者の皆さんがごく自然に、障害を「個人の問題」から「社会や環境の問題」へと転換して議論を進めていたことです。エンジニアリングに携わる方々らしく、目の前の事象を「解決すべき課題」として捉える姿勢が非常に頼もしく感じられました。 5. 心境・視点の変化:アンケートが語る「パラダイムシフト」 研修の前後で、参加者の「障害」に対する解釈は驚くほど変化しました。 最初は「心身の機能に関すること」や「それに伴って何かができないこと」という前提で話し始めていたメンバーが、ワークショップでの対話を重ねるうちに、自分たちの外側にある要因を含めた新たな視点で障害を捉え直そうとしている姿が印象的でした。 終了時には、参加者の口から「障害に対する考え方の前提がひっくり返った気がする」といった言葉が聞かれ、ファシリテーターとしてこの上なく手応えを感じた瞬間でした。 アンケートでも満足度・内容ともに10点満点中9〜10点という極めて高い評価をいただき、以下のような前向きな声をいただいています。 「本人と環境という、2つの問題に目線が広がりました」 「こういう考え方が一般常識になれば、世の中が変わると思う」 「ぜひ後半もやりたい。他の拠点やチームにも広めたい」 「議論を活発にするための心理的安全性についても検討していきたい」 6. 今後のアクション:誰もが「社会を変えるプレーヤー」に モビリティの分野において「障害」について深く掘り下げることは、これからの移動の在り方を考える上で避けては通れないテーマです。 研修を通じて私たちが得た最大の収穫は、アクセシビリティを「誰かのための特別な対応」ではなく、「身近なところにあり自分たちが解決できるかもしれない課題」として捉え直したことです。自分の仕事のどこにバリアがあり、どこに解決の可能性があるのか。その気づきこそが、文化を変える第一歩になります。 誰もが社会のバリアを取り除く「プレーヤー」であると実感できる職場。その先に、KINTOテクノロジーズが「すべての人にとって働きやすく、価値を提供できる場所」になる未来を目指し、この対話の輪を他拠点や他部署へも広げていきたいと思っています。
アバター
こんにちは。KINTOテクノロジーズ(KTC)でKINTOの中古車ECサイトのディレクターをしている かーびー です。 KINTO 中古車 とは、「KINTOの新車返却車両」の中から状態の良いクルマのみを、クルマのプロが厳選して提供する「高品質な中古車サブスクリプションサービス」です。 今回は、KINTOの中古車に関わる有志のメンバーで試験的に実施した「ユーザーインタビューわいわい会」の取り組みと、そこから得られた気づきや学びについてご紹介します。 「ユーザーインタビューわいわい会」とは? 私たちは、ご契約いただいているお客様が、どのような点に魅力を感じているのかを深く理解するため、継続的にユーザーインタビューを実施しています。 ただ、インタビュー担当者のハイライトや要約だけでは、お客様の姿や言葉の裏にある「熱量」といった生の声を、インタビューに参加していないチームメンバーには十分に伝えきれない場面があります。 そこで、メンバーそれぞれが直接お客様の声に触れ、課題への解像度を揃えることで「チームの温度感をより高めたい!」と考えました。そこで試験的に実施したのが、ユーザーインタビューの録画動画をみんなで視聴し、ディスカッションする会です。 KINTOの中古車に関わる有志メンバー13名で実施 「ユーザーインタビューわいわい会」の概要 今回はお昼の時間帯を使っての実施だったため、視聴しながら参加できるようにランチもあわせて用意しました。 実際の流れ(約60分間) 今回はテスト実施として、以下の時間配分で行いました。 ユーザーインタビューの視聴:約35分 個人ワーク:約5分 テーブルごとに共有:約15分 全体振り返り:約5分 結果として、共有や振り返りの時間がかなりタイトになりました。 特にテーブル共有では、話が盛り上がったところで時間切れになる場面もあり、「もっと話したかった」という声も聞かれました。 意識したポイント 発言力やドメイン知識の差によって意見が偏らないよう、「まず一人で書く→その後に共有する」という流れを採用しました。 また、普段はお客様の行動データ(定量)を見ていますが、数字だけでは「なぜその行動をしたのか」までは分かりません。そこで今回は、データから立てた仮説を事前に配布し、インタビュー(定性)で検証する形をとりました。「定量では見えないこと」に自然と目が向くような設計を意識しています。 定量→定性で見えたこと(仮説検証の例) 例えば、あるお客様のデータからは「 車種Aを複数お気に入り登録 していたが、最終的に 車種Bで契約 した」という事実が見えていました。 仮説1: 納期や価格 を優先し、Bの車種に切り替えたのではないか? 結果: インタビューを通じて、こちらは 概ね仮説通り であることが確認できました。 一方で、データだけでは読み解けなかった大きなギャップもありました。 仮説2: 初回訪問から数十時間で契約しているため、 納期最優先で即決 したのではないか? 結果: 仮説は外れていました。 実際には、数ヶ月にわたって外部サイトで徹底的にリサーチを重ねた「熟考の末」の訪問でした。 真相: 納期も要素のひとつではありましたが、 最終的な決め手は「サービスとしての信頼性」 。納得感が醸成されたタイミングでサイトを訪れたため、結果として「即決」というデータとして現れていただけでした。 このように、定量データだけでは見えない「意思決定の背景」や「迷いのプロセス」が、生の声を聞くことで具体的に浮かび上がってきました。 「ユーザーインタビューわいわい会」を実施してみて ① お客様の判断の瞬間を共有できた 本会終了後のアンケートでは、 参加者全員から「印象に残った瞬間があった」という回答 が得られました。 たとえば、 「KINTOって、Webで申込完結・車両保険も月額に入ってこの価格なんですよね?」「想像していたより、含まれているものが多いですね」 といった発言があり、サービスの説明を受ける中で、想定していたよりもコストパフォーマンスが高いと感じた様子が率直に語られました。 さらに、トヨタの正規販売店による整備・メンテナンスが月額料金に含まれている点について、車両トラブル時にすぐ対応してもらえたというエピソードも挙がり、購入後の体験にも満足していることがうかがえました。 こうした一連の声から、中古車であっても「安心して使える体験」と価格のバランスが、意思決定において重要な価値として受け取られていることを直接確認でき、チームにとって確かな手応えにつながりました。 ② 次の仮説が自然と生まれた ユーザーインタビュー視聴後には、 車の知識レベルによって、選び方はどう違うのか? コストパフォーマンスを、どの要素で評価しているのか? 他社との比較は、どの程度行われているのか? 契約前に家族とどのような会話をしているのか? 「中古車」というワードにどのような印象を持っているのか? といった 問いや気づき、仮説が60件以上 集まりました。 感想で終わるのではなく、次の仮説やアクションにつながる問いが、さまざまな職種のメンバーから自然に生まれたことは、実務につなげやすい状態をつくれたという意味でも、大きな収穫でした。 ③ チームに共通言語ができた 本会後の会議では、 「ユーザーインタビューのお客様も同じことをおっしゃっていましたが…」 といった会話が出るようになりました。 顧客像を共通の根拠として会話できる状態が生まれ、これを積み重ねていくことで、議論のスピードや精度も高まっていくのではないかと考えています。 参加者の声(一部抜粋) 「アンケートだけでは本質的なニーズや背景に十分に踏み込めない場合があると感じました。直接ヒアリングの機会を持つことで、課題の根底にある思いや具体的な状況をより深く理解できることに気づきました。」 次回に向けて 今回はテスト実施という位置づけで、60分という限られた時間の中で実施しました。取り組みとしての有用性を確認できた一方で、次回に向けて磨いていきたい点も見えてきました。 次回は、以下のような点を中心に改善を進めていく予定です。 ディスカッション時間の拡大 参加人数を増やし、より多様な視点を集める 「時間が足りなかった」という声は、それだけ共有したい気づきが多く生まれていたということでもあると感じています。この熱量を、次回の場づくりにつなげていけたらと思います。 「ユーザーインタビューわいわい会」からの気づき 今回の「わいわい会」を通じて、職種や視点が異なるチームで前提を揃えていくための、いくつかの気づきがありました。 共通言語と「翻訳」の存在 モビリティ業界という特性上、私たちのチームには多様な専門性を持つメンバーが集まっています。立場によって言葉の捉え方が異なるのは当然ですが、自分自身、どこかで 前職のような少人数チームでの「阿吽(あうん)の呼吸」 を前提にコミュニケーションを組み立てていた部分があったと、改めて気づかされました。 今回、大人数で対話をする中で気づいたのは、これまでは誰かが言葉のギャップを埋める「翻訳」を自然に担ってくれていたのではないか、ということです。チームの規模や多様性が増すほど、個人の属人的な「翻訳」に頼るのではなく、一次情報という「共通の土台」を仕組みとして提供することが重要になると実感しました。 「一次情報」が対話の質を変える 伝え方のスキルを磨くことも大切ですが、それ以上に 「同じ一次情報を共有すること」自体が、前提を揃えるうえで非常に有効 だと再確認しました。今回の「わいわい会」のように、ユーザーの声を「一緒に体験する」形を続けていくことで、以下のような効果を得られそうだと感じています。 解釈のズレを未然に防ぐ: 同じ体験を起点にすることを繰り返すことで、チーム内の前提の食い違いが起きにくくなる。 多角的な視点を仕組みとして取り入れる: 職種や背景の違いから、一人では気づけない観点が自然と集まる。 対話のハードルが下がる: 「あの時の、あのお客様のことば」という共通言語が増えていくため、その後の議論がスムーズになる。 こうした小さな積み重ねを大切にしながら、対話の輪を少しずつ広げていけたらと思います。 おわりに 今回の「ユーザーインタビューわいわい会」は、社内で進められている ユーザーファースト の取り組みとも、自然につながる実践でした。 ユーザー理解をチームにどう広げ、実務にどう落とし込んでいくか。その一つのヒントとして、この「わいわい会」の形も引き続き試していけたらと思います。 ユーザーファーストに関する全社的な取り組みについては、以下の記事でも紹介しています。ぜひあわせてご覧ください。 https://blog.kinto-technologies.com/posts/2025-12-11-userfirst2025/
アバター
はじめに こんにちは。 my route 開発部でバックエンドチームのリーダーをしている yf です。 my route 開発部では、昨年 7 月に組織体制が変わり、新しい形で開発を進めています。 その変化に備えて、6 月から少しずつ進めてきた取り組みが、 半年たった今、チームの空気や仕事の進め方に確かな変化をもたらしています。 この半年で扱ったテーマは約 40。 一つひとつは小さな改善ですが、積み重ねることで 「開発の役割」や「プロダクトとの向き合い方」が大きく変わってきました。 本記事では、私たちがどんなステップを踏み、 なぜその変化が起きたのかを、時系列[^1]で振り返ります。 この記事はこんな人向け 開発が「実装担当」に閉じてしまっていることに違和感がある方 仕様やスケジュールが決まった状態で渡ってきて、Why を理解しづらいと感じている方 プロダクト思考を持ちたいが、日々の開発で手応えを持てていない方 組織改善をしたいが、何から手を付ければいいかわからないリーダー・サブリーダー 「誰かを責める」のではなく、「構造を変える」アプローチを探している方 私たちが半年間かけて試行錯誤してきた取り組みが、 同じような悩みを持つチームのヒントになれば幸いです。 当時の状況と、なぜそうなっていたのか 取り組みを始める前、開発部には次のような状況がありました。 要件や仕様が、開発フェーズの後半で共有されることが多かった 開発期間が限られ、品質改善や振り返りに十分な時間を取れなかった スケジュールや設計の背景(Why)が、開発側に伝わりにくかった 結果として、実装を中心に進めざるを得ない進め方になっていた これは、特定の誰かの判断ミスというよりも、 "役割分担とプロセスの構造がそうさせていた状態" だったと振り返っています。 プロデューサーはプロダクトを良くしようとする責任感から、 設計やスケジュールをできるだけ具体化しようとしていました。 一方で、その分、開発に共有されるタイミングが遅くなり、 開発側には「How(どう作るか)」を中心とした情報が渡る構造になっていました。 その結果、 "なぜこの機能を作るのか(Why)を理解した上で改善提案を行う余地が少なくなり、" 開発が実装中心の役割に閉じてしまっていたのです。 この状況を変えるために、 私たちは "開発組織から、プロダクト組織へ" という 大きな方向転換に踏み出しました。 半年間の全体像 目的定義 → プロセス設計 → 運用定着 → 連携強化 → 品質と戦略 → 組織文化として定着 まず、私たちが向き合っていた「仕事の流れ」の変化を、Before / After で示します。 ■ 6 月 —— “目的と役割の再定義” 組織変革の起点 まず取り組んだのは、 “私たちはどこに向かうのか“ “チームリーダーは何を担うのか“ という意図合わせでした。 開発に関わる関係チームのリーダー全員(PDM・QA・バックエンド・モバイル)で、理想像の輪郭を描き直しました。 主なテーマ 独立プロダクト組織としての目的定義 リーダー役割の再整理 過渡期の案件対応 仕事の流れ図(AS-IS)の棚卸し チーム役割の再設計 会議体・Slack などコミュニケーション設計 この段階で、別の部である Engineering Office にも参画してもらいました。 開発部の中だけでは前提になっていた考えや、 見落としていたプロセスの歪みを、 第三者の視点から言語化・整理してもらえたことは、 その後の設計を進めるうえで大きな支えになりました。 6 月 あらためてひとこと - “まず揃えないと、何も始まらない” この時点では、具体的な施策よりも "前提が揃っていないまま進むことの危険性" を強く感じていました。 早く手を動かしたい気持ちを抑え、 あえて立ち止まって目的と役割を言語化したのは、 後戻りしないための投資だったと思っています。 ■ 7 月 —— “仕組みの再設計に着手” 新しい仕事の流れの原型ができた 6 月に定義した理想を、実際のプロセスに落とし込んだ月です。 スプリント導入(計画・中間・レビュー・レトロスペクティブ) チーム間連携会の常設 Jira運用ルールの再構築 ストーリーチケット作成基準の統一 Git ブランチ戦略の見直し リリースフロー整備 成果物レビューの仕組み化 問い合わせ・障害の暫定ルール化 “会議体・プロセスがゼロから設計されていくスピード感“ があり、 部全体の透明性が大きく向上しました。 7 月 あらためてひとこと - “理想を、現実の流れに落とす” 6月に描いた理想は、そのままでは机上の空論でした。 リーダとして意識したのは「誰がやっても迷わない仕組み」になっているか。 プロセスを設計することは、メンバーの思考コストを下げることだと実感した月でした。 ■ 8 月 —— “新プロセスの定着と運用強化” Jiraと仕事の流れが形になってきた 新プロセスの試験運用 新規案件でのトライアル導入 UI 定例、Slack などコミュニケーション基準化 問い合わせ・ツール改修フローの改善 ロードマップレビューの開始 プロセスが回り始めたことで、 “現場から自然と改善提案が出る状態“ が生まれはじめました。 8 月 あらためてひとこと - “仕組みは、使われて初めて意味を持つ” 新しいプロセスは、導入しただけでは根付きません。 この月は「守らせる」よりも "使ってみてどうだったかを聞く" ことに注力しました。 現場から改善案が出始めたとき、組織が一段階変わった感覚がありました。 ■ 9 月 —— “プロダクト思考と横断連携の強化” チーム間連携が日常化 ストーリー分割ワーク 役割を越えたアイデア提案の促進 リソースアサイン管理の透明化 AI 活用案件の相談 リリース承認ルートの改善 リーダー陣の視点が “「自分の領域」から「プロダクト全体」“へ 大きくシフトした月でした。 9 月あらためてひとこと - “役割を越えることを、許可する” プロダクト全体の話をするとき、役割を理由に遠慮が生まれる場面がありました。 リーダとして意識したのは、「それはあなたの領域じゃない」という空気を消すこと。 横断連携は、仕組みだけでなく心理的安全性があって初めて機能すると学びました。 ■ 10 月 —— “運用・リスク管理の高度化” 問い合わせ・障害・運用プロセスが進化 問い合わせ・障害フローの再整備 運用体制の検討 目的起点の進め方ワーク アジャイルトレーニング準備 他部門との連携強化 “リスクを未然に防ぐ動き” が自然と発生する組織へと変化しました。 10 月 あらためてひとこと - “問題が起きる前提で、組織をつくる” 問い合わせや障害は、ゼロにはできません。 だからこそ "起きたときにどう振る舞えるか" を考えるフェーズに入りました。 個人の頑張りに依存せず、組織として耐性を持つことを意識し始めた月です。 ■ 11 月 —— “品質・戦略・成長のフェーズへ” 技術とビジネスがつながり始めた スプリントへの QA 導入方針の確定 セキュリティ監査オーナーの役割整理 ポストモーテム文化の定着 UI/UX 改善の進め方刷新 フィールドワーク成果の共有 “案件をこなす組織” から “プロダクトを成長させる組織” へと進化。 11 月 あらためてひとこと - “技術は、ビジネスとつながってこそ価値になる” 品質やUXの議論が増えたことで、技術がプロダクト価値にどう貢献するかを 言葉にする機会が増えました。 リーダとしては、技術的な正しさと、事業としての判断をつなぐ役割を より強く意識するようになりました。 ■ 12 月 —— “半年の蓄積が組織文化に変わり始めた” 目的起点で動ける組織に UI/UX 改善の長期方針の確立 QA 導入プロセスの実運用 ロードマップレビューの定着 工程ごとのリードタイム測定開始 6 月に掲げた “自律したプロダクト組織になる” という目標が、実際の動きとして現れてきました。 半年で生まれた “4 つの変化” ① 情報の透明性 プロダクト全体の状態が誰にでも見えるようになった。 ② 早期リスク検知 問い合わせ・障害・運用課題が芽のうちに見つかるようになった。 ③ 横断連携の活性化 PDM・QA・バックエンド・モバイルが互いに提案しあう文化が育った。 ④ 再現性のあるプロセス “誰がやっても前に進む” 仕組みが整った。 12 月 あらためてひとこと - “文化は、後から気づくもの” 何か劇的なイベントがあったわけではありません。 ただ振り返ってみると、目的から考え、自然と連携し、改善を回す "当たり前の動き" が定着していました。 組織文化は、作ろうとして作るものではなく、 積み重ねの結果として生まれるのだと感じています。 もともとの課題は、どこまで変わったか 取り組みを始めた当初、私たちは 「開発が実装中心の役割に閉じてしまっている」という課題を抱えていました。 半年間の取り組みによって、 Why を共有したうえで議論できる場が増え スケジュールや設計の背景を前提に、改善提案が出るようになり 開発が「決められたものを作る」だけの立場ではなくなってきた といった変化が確かに生まれました。 一方で、 すべてが理想通りに解消されたわけではありません。 プロダクト全体の意思決定への関与や、 戦略レイヤーでの議論は、まだ発展途上です。 それでも、 「なぜやるのかを考えながら作る」ことが当たり前になり始めた という点で、 当初感じていた課題は、確実に別のフェーズへ進んだと感じています。 次の半年へ これからは、 “プロダクトをつくる組織“から“プロダクトを成長させる組織“ へさらに進化していきます。 グロースハック文化の定着 権限移譲と育成の体系化 プロダクト戦略の内製化 インシデント学習ループ強化 開発体制の継続アップデート 内部だけで議論すると主観に偏り、問題の本質を見誤ることがあります。 そのため今回は、部を横断して取り組めたことも私たちにとって大きな追い風となりました。 半年で大きく変化した組織が、次の半年でどこまで成長できるのか。 私自身、とても楽しみにしています。 さいごに my route 開発部では、まだまださまざまな部署・職種で一緒に働ける仲間を募集しています! 詳しくは こちら からご確認ください! [^1]: 半年で扱った 40 のテーマを月別の代表例として抜粋しています。
アバター
はじめに こんにちは、2025年10月入社のr.tesakiです! 本記事では、2025年10月入社のみなさまに入社直後の感想をお伺いし、まとめてみました。 KINTOテクノロジーズ(以下、KTC)に興味のある方、そして、今回参加下さったメンバーへの振り返りとして有益なコンテンツになればいいなと思います! S.N. ![S.N.さんのプロフィール画像](/assets/blog/authors/tesaki/2025-12-24-newcomer-202512/S_N.png =300x) 自己紹介 KINTO中古車開発Gのバックエンドエンジニアとして入社しました。室町オフィス勤務です。 最近映画館の近くに引っ越したので、映画館で映画を観るのにハマっています! 所属チームの体制は? KINTO中古車開発Gは、プロデュースチーム、フロントエンドチーム、バックエンドチームの3チーム体制です。 バックエンドチームは自分を含めて8名です。 現場の雰囲気はどんな感じ? 出社した日にはチームメンバーと一緒にランチに行くなど賑やかな雰囲気です。 KTCへ入社したときの入社動機や入社前後のギャップは? 入社動機: 大きなエンジニア組織&物を扱うサービスに携わりたいと考えていたため。 入社前後のギャップ: ミーティングや社内イベントなどで他拠点の方とも関わることが想像よりも多く、会社としての一体感があって良いと感じています! オフィスで気に入っているところ 室町オフィスは駅直結なので、天気を気にしなくて良いのが嬉しいです! M.U.さん ⇒ S.N.さんへの質問 小学生の時の将来の夢(なりたい職業)は何でしたか? 小学校の卒業アルバムの写真撮影でエプロンをつけた記憶があるので、料理人になりたかったんだと思います! K.S. ![K.S.さんのプロフィール画像](/assets/blog/authors/tesaki/2025-12-24-newcomer-202512/K_S.png =300x) 自己紹介 酒とカラオケをこよなく愛するアラフィフ。自称元プロゲーマー。 所属チームの体制は? コーポレートIT部所属。親会社のビッグプロジェクトに参画してシステムリプレースのリーダー。 現場の雰囲気はどんな感じ? 個々が際立っていてオリジナリティーのある人たちがワイワイしている感じ。 KTCへ入社したときの入社動機や入社前後のギャップは? 入社動機: エンジニアの報酬が高いし、役職定年ないし、バリバリ働けそうだから。 入社前後のギャップ: チームで働くというよりピンで活動することが多い気がする。意外に社長、副社長との距離が近いw オフィスで気に入っているところ 神保町オフィスは人が少なくて広々と使えるし、とても開放感があってよい! S.N.さん ⇒ K.S.さんへの質問 一番好きなお酒を教えてください! ビール🍺がサイコーです👌が、痛風が怖いので梅サワーで我慢してます🤷‍♂️ r.tesaki ![r.tesakiさんのプロフィール画像](/assets/blog/authors/tesaki/nekonote.png =300x) 自己紹介 オンプレのインフラエンジニアからスタートしてKTCではプラットフォーム開発部DBREグループのSREチームメンバーとして入社しました。Osaka Tech Lab所属です。 所属チームの体制は? Database を専門とするDBREチームと、プロダクト全体を担うSREチームの2チームに分かれて活動してます。KINTOや他の業務システムの開発チームと一緒に活動することが多いです。 現場の雰囲気はどんな感じ? Osaka Tech Labはチームを跨いだ一体感があって、他のチームメンバーは東京にいますが孤立感はなく賑やかに感じてます。 KTCへ入社したときの入社動機や入社前後のギャップは? 入社動機: 組織横断であったり、プロダクト専任であったりと色々な形のSREに挑戦できそうだったため。 入社前後のギャップ: 聞いてた以上に東京へ行ける機会が多く、承認もスムーズに進むことです。各種イベント参加もしやすいです。 オフィスで気に入っているところ JR大阪駅の改札を出て目の前にオフィスビルの入口があるところ。 K.S.さん ⇒ r.tesakiさんへの質問 10億当たったら何に使う? 猫を飼う人しか住めないマンションを立てて猫好きの楽園をつくる! ぬー ![ぬーさんのプロフィール画像](/assets/blog/authors/tesaki/2025-12-24-newcomer-202512/nu.png =300x) 自己紹介 プラットフォーム開発部 Cloud Infrastructure G に所属しています。KINTO 関連システムの AWS インフラの構築や保守運用を担当しています。 所属チームの体制は? グループはインフラチーム、カイゼンチーム、ソリューションチームの3チーム体制で、同じインフラ領域でも別々の責務を担っています。 現場の雰囲気はどんな感じ? メンバーの仲が良く、雑談も多いです。 KTCへ入社したときの入社動機や入社前後のギャップは? プラットフォーム開発部はグループやチームがたくさんあり、思っていた以上に役割が細分化されていると感じました。 若手をリードしてほしいと言われて入社しましたが、今のところ(いい意味で)リードする必要性を感じないぐらい素晴らしいメンバーだと思います! オフィスで気に入っているところ 神保町オフィスは近隣に飲食店が豊富で、おいしいお店が多いところ r.tesakiさん ⇒ ぬーさんへの質問 おすすめのキャンプグッズ教えてください! 特にこれ!というグッズは無いのですが、SOTO というメーカーの製品はコンパクトなものが多いのでおすすめです! キャンプをしていると荷物が増えてきて、少しでも物を減らしたり同じ用途でも小さいものにしたくなるので。 (といっても自分では持っていなくて、今後買いたいなと思っているところですw) U.V. ![U.V.さんのプロフィール画像](/assets/blog/authors/tesaki/2025-12-24-newcomer-202512/U_V.png =300x) 自己紹介 ビジネスディベロップメントGのU.V.です。 国内外のビジネス拡張を担当しております。 所属チームの体制は? 5名で構成された、多国籍のチームです。 現場の雰囲気はどんな感じ? マネジメントからの方針は明確ですが、各メンバーが自由に意見を述べ、自分の仕事に主体性を持って取り組めていると感じています。 KTCへ入社したときの入社動機や入社前後のギャップは? オリエンテーションは分かりやすく、私が抱えていた疑問をすべて解消してくれました。 入社時点で有給休暇が付与されると伺い、とても驚きました。ありがとうございます。 毎月新しいプロジェクトが立ち上がり、仕事がとてもダイナミックで楽しんでいます。 オフィスで気に入っているところ オフィスは混雑しておらず、木製の家具がとても可愛らしいです。 ぬーさん ⇒ U.V.さんへの質問 日本に来た動機と、日本に来て驚いたことや変だなーと思うことがあれば教えてください! 日本で留学生として過ごした時間がとても楽しく、自分の国とは大きく違う環境でキャリアを築きたいと思い、日本で働くことを決めました。 驚いたことの一つは、日本の方が電話でもお辞儀をすることです。 M.U. ![M.U.さんのプロフィール画像](/assets/blog/authors/tesaki/2025-12-24-newcomer-202512/M_U.png =300x) 自己紹介 モビリティプロダクト開発部で、販売店様との関係構築/プリセールスを担当しています 勤務先は今年開設されたばかりの福岡です 所属チームの体制は? 4名体制(私以外は東京のオフィスがベース)で全国の販売店様を担当しています 現場の雰囲気はどんな感じ? 福岡では皆で協力しながら、オフィスを運営しています 出張で来られる方が多いので、部署を超えて会話する機会が多いですね チームメンバーとは定期的にオンサイトでコミュニケーションを取っているので、リモートだからといって不自由は無いです KTCへ入社したときの入社動機や入社前後のギャップは? 中規模Sierとスタートアップを経験したので、規模の大きな会社で新しいチャレンジをしてみたいと思ったからです 大きなギャップはなく、個々人のスキルが高くプロの集団だと感じました オフィスで気に入っているところ 勤務先の福岡テックラボは11月にオープンしたばかり! 海が見渡せる開放感のある景色とおしゃれな内装で、出社したくなるオフィスです U.V.さん ⇒ M.U.さんへの質問 お仕事の中で、AI をどのように活用しているのか、興味深い取り組みがあれば教えていただけますか。 主に調査や資料作成など一般的な使い方です 情報をキャッチアップする際にコパイロットだと、社外情報+社内資料も提案してくれるのが素敵ですね 販売店様の情報を調査する等であればGeminiの方が優秀だと感じています さいごに みなさま、入社後の感想を教えてくださり、ありがとうございました! KINTOテクノロジーズでは日々、新たなメンバーが増えています! 今後もいろんな部署のいろんな方々の入社エントリが増えていきますので、楽しみにしていただけましたら幸いです。 そして、KINTOテクノロジーズでは、まだまださまざまな部署・職種で一緒に働ける仲間を募集しています! 詳しくは こちら からご確認ください!
アバター
この記事は KINTO テクノロジーズ Advent Calendar 2025 の 25 日目の記事です 🎅🎄 はじめに こんにちは! KINTO 開発部 KINTO バックエンド開発 G マスターメンテナンスツール開発チーム、技術広報 G 兼務、Osaka Tech Lab 所属の high-g( @high_g_engineer )です。フロントエンドエンジニアをやっています。 現在開発中のプロジェクトでは、RC 版の頃から React Compiler を導入しており、約 8 ヶ月が経ちました。 導入によって useMemo や useCallback を書かなくても自動でメモ化されるため、メモ化を意識する必要がなくなり、開発体験は向上しました。 しかし、実際にどの程度メモ化が正しく行われているのか、パフォーマンスにどれくらいの影響があるのかは、詳しく検証できていませんでした。 そこで本記事では、React Compiler の有効時の挙動や、有効時と無効時のパフォーマンス比較を検証してみることにしました。 React Compiler とは https://ja.react.dev/learn/react-compiler/introduction React Compiler は、ビルド時に自動的にメモ化を行うことで React アプリを最適化するツールです。 そのため、React Compiler を導入すれば、 useMemo や useCallback 、 React.memo などを手動で書く必要がなくなります。 最初の安定版(v1.0)は 2025 年 10 月 7 日にリリースされ、この記事が執筆された時点で約 2 か月半が経過しています。 安定版リリースまでの経緯は以下の通りです。 2023 年 3 月 - 「React Forget」として開発、Meta 社内の限定的な領域で production 利用開始 2023 年 10 月 - React Advanced Conference 2023 で「React Forget」として公開発表 2024 年 2 月 - instagram 全体で production 展開完了、Meta 社内の他サービスへ展開、OSS 化準備と発表 2024 年 5 月 - React Conf 2024 で experimental release を発表 2024 年 10 月 21 日 - Beta release を公開 2025 年 4 月 21 日 - Release Candidate (RC) を公開 2025 年 10 月 7 日 - v1.0 安定版リリース React Compiler の設定方法 Vite と React 19 を使用した環境での React Compiler の設定方法を紹介します。 1. パッケージのインストール pnpm add -D babel-plugin-react-compiler 2. vite.config.ts の設定 @vitejs/plugin-react の babel オプションに babel-plugin-react-compiler を追加します。 import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; // 設定オプション const ReactCompilerConfig = { /* ... */ }; export default defineConfig({ plugins: [ react({ babel: { plugins: [["babel-plugin-react-compiler", ReactCompilerConfig]], }, }), ], }); これだけで設定は完了です。後はビルド時に React Compiler が自動的にコードを解析し、必要な箇所にメモ化を適用してくれます。 React Compiler の設定オプションに関しては、本記事では説明を割愛します。 特に設定しなくても動作しますが、詳細を知りたい方は以下を参照してください。 https://ja.react.dev/reference/react-compiler/configuration ベンチマーク対象の React アプリ 実際の開発プロジェクトで検証を試みましたが、使用しているライブラリに既にメモ化されたコンポーネントが多く含まれていて、純粋な比較が困難だったので、ベンチマーク専用のプロジェクトを作成しました。 ヘッダー、サイドメニュー、メインコンテンツのエリアで構成され、初期表示時の合計コンポーネント数は約 100 個です。 最初に、React Compiler が無効の状態で、React Dev Tools でメモ化の状況を確認していきます。 Components タブを見ると、メモ化されている場合に表示される「Memo」のラベルが一切ないことがわかります。 次に、React Dev Tools の設定で Highlight updates when components render にチェックを入れて、上位にあるボタンコンポーネントをクリックし再レンダリングの様子を確認すると、本来再レンダリングが不要な子孫コンポーネントにも再レンダリングが発生していることが分かります。 React Compiler のメモ化の挙動 React Compiler を有効にして開発サーバーを立ち上げ、同じアプリがメモ化されているかを確認します。 React Dev Tools の Components タブを確認すると、「Memo」のラベルが数多く表示されていることが分かります。 同様に、上位にあるボタンコンポーネントをクリックし、React Dev Tools で再レンダリングの状態を確認すると、再レンダリングが必要最小限に抑えられていることが分かります。 React Compiler のパフォーマンスベンチマーク ここからは、React Compiler によるパフォーマンスの差を React Dev Tools の Profiler を用いて検証していきます。 検証では、下記の赤枠のボタンを約 1 秒間隔で 10 回連続クリックした際の、レンダリング時間の比較を行いました。 このボタンはメインコンテンツの最上位に配置されているため、クリック時に多くの子孫コンポーネントへ再レンダリングの影響が波及します。 React Compiler 無効時(メモ化なし) 計測データ 1回目: 29ms 2回目: 34.5ms 3回目: 36.1ms 4回目: 33.9ms 5回目: 36.3ms 6回目: 17.6ms 7回目: 35.1ms 8回目: 32.1ms 9回目: 33.3ms 10回目: 36.8ms 平均レンダリング時間 32.5ms Flamegraph を見ると、すべての子孫コンポーネントがレンダリングされていることが分かります。また、MainContents 以外にもレンダリング時間が長いコンポーネント(黄色やオレンジ色で表示)が存在し、本来不要な再レンダリングが発生していることが確認できます。 React Compiler 有効時(メモ化あり) 計測データ 1回目: 11.1ms 2回目: 12.1ms 3回目: 12.2ms 4回目: 12.1ms 5回目: 12.1ms 6回目: 12.1ms 7回目: 12.1ms 8回目: 12.0ms 9回目: 12.0ms 10回目: 12.6ms 平均レンダリング時間 12.0ms Flamegraph を見ると、グレーの斜線で表示されているコンポーネントが多数確認でき、再レンダリングが必要最小限に抑えられていることが分かります。 パフォーマンス改善の結果 React Compiler 無効時 React Compiler 有効時 改善結果 平均レンダリング時間 32.5ms 12.0ms 約 2.7 倍高速化 React Compiler を有効にしただけで、非常に大きなパフォーマンス改善ができていることが確認できました。 今回はメモ化を全く行っていないプロジェクトとの比較のため、すべてのケースで同様の改善が見込めるわけではありませんが、導入効果は十分に期待できます。 懸念点 ライブラリとの相性 現在開発中のプロジェクトでは TanStack Table を利用しています。TanStack Table は新しい参照を意図的に生成することで再レンダリングをトリガーする設計のため、React Compiler によるメモ化が適用されると、再レンダリングが発生せず意図しない挙動を引き起こす可能性があります。 この問題に対しては、 "use no memo" ディレクティブを追加して部分的に React Compiler を無効化することで対応が可能です。 function TableComponent() { "use no memo"; const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), }); return ( <table> {/* TanStack Table の参照変化に依存する処理 */} </table> ); } react-hook-form など、同様に参照の変化に依存するライブラリを利用する場合は、要所で "use no memo" の記述が必要になるため、導入時にはご注意ください。 ビルド速度低下、ビルドファイルサイズ上昇 React Compiler を導入すると、再レンダリング時のパフォーマンスは向上しますが、メモ化のためのコードが追加されるため、ビルド速度やファイルサイズに多少影響が出ます。その結果、初回読み込みが少し遅くなる可能性があります。 React Compiler 無効時のビルド結果 ビルド時間: 64ms ファイルサイズ: 232KB React Compiler 有効時のビルド結果 ビルド時間: 556ms ファイルサイズ: 248KB まとめ 今回は React Compiler を有効にしたときのメモ化に関する挙動の確認と、パフォーマンスにどれくらい影響があるのかを検証してみました。 結果として、メモ化が正しく動作し、そのおかげでパフォーマンス改善も十分に期待できることが分かりました。 ただし、いくつか注意点もあります。 参照の変化に依存するライブラリを使用する場合は、 "use no memo" で部分的に無効化が必要になることがあります。また、メモ化のコードが追加される分、ビルド速度やファイルサイズには多少影響が出ます。 とはいえ、これらは対処可能な範囲ですし、パフォーマンス改善だけでなくメモ化を意識しなくて済むというメリットは非常に大きいです。React Compiler の導入を迷われている方は、ぜひ試してみてください。 最後まで読んでいただきありがとうございました。 参考記事 https://ja.react.dev/learn/react-compiler/introduction https://ja.react.dev/reference/react-compiler/configuration https://reactadvanced.com/2023/ https://conf2024.react.dev/ https://react.dev/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023 https://react.dev/blog/2024/02/15/react-labs-what-we-have-been-working-on-february-2024 https://react.dev/blog/2025/10/07/react-compiler-1 https://github.com/TanStack/table/issues/5567
アバター
こんにちは!KINTOテクノロジーズのクリエイティブGでデザイナーをしているmayuです。 今回は、技術書典の表紙制作の過程をご紹介したいと思います。 Midjourneyなどの生成AIツールを活用しているので、「 ノンデザイナーでもプロ並みの制作ができる?! 」のがミソです。 デザイナーの方も、そうでない方もぜひご覧ください! 技術書典用の表紙デザインを依頼された ある日、エンジニアのうえはらさんから 「KINTOテクノロジーズ(KTC)の有志エンジニアによる技術書を技術書典に出展したいので、表紙デザインを作ってほしい」 という依頼を受けました。 今回はクリエイティブGのデザイナーmomoiさんの提案で 「デザイナーを集めてライブペインティング方式で作成するのはどうか?」 ということで、ライブペインティングイベント内での制作が決定しました! 技術書典ってなに? 技術書典についてはうえはらさんが書いてくださった記事があるのでこちらをご覧ください。 https://blog.kinto-technologies.com/posts/2025-12-12-techbookfest19-report/ ライブペインティングってなに? ライブペインティングについてはmomoiさんが書いてくださった記事があるのでこちらをご覧ください。 https://blog.kinto-technologies.com/posts/2025-12-24-ai-live-painting/ どうやって作ったか ざっくり作り方を説明すると、 1. お題をChatGPTにインプットさせる 2. ChatGPTにプロンプトを書いてもらう 3. Midjourneyで画像を生成する 4. Midjourneyで解像度UP & Photoshopで微修正する 5. illustratorで入稿データを作成する の5ステップです! それぞれ紐解いていきますね。 1. お題をChatGPTにインプットさせる 今回のお題はこちらでした。 お題は事前に知らされていなかったので、正直「ざっくりしたお題だな…」と思い最初は全くイメージが浮かびませんでした。笑 2. ChatGPTにプロンプトを書いてもらう まずは、私の相棒ChatGPTにお題をインプットして、アイデア出しをしてもらうことにしました。 ひとまず、プロンプトを出してくれたのでこれをMidjourneyに入力していきます。 3. Midjourneyで画像を生成する 一回目の生成結果はこんな感じ! るぴあは事前に配布されていた素材を Omni-Reference で読み込ませ、一貫性を保っています。 Omni-Referenceについてはmomoiさんが詳しく書いてくださっているのでこちらの記事をご覧ください。 https://blog.kinto-technologies.com/posts/2025-06-13-omni-reference/ ここまでやって、なんとなく世界観のイメージは湧いてきました。 でも、るぴあが棒立ちで動きがないので何か違うなーって感じ… 「どんなポーズがいいか?」とChatGPTに相談してみました。 さすがチャッピー!!かなり具体的でこれならイメージに合うものが作れそう! それぞれのポーズをプロンプトに落とし込んでもらいました。 これを、もう一度Midourneyで生成してみます。 なかなかいい感じになりました! 先ほどの棒立ちるぴあと比べると、「未来感」が増した気がします。 ただ、表紙の上部にはタイトルを入れる予定なので、これだと頭と文字が被ってしまいそうなんですよね… どうしよう…と考えた結果、 座らせることにしました!笑 (るぴあを座らせるようChatGPTにプロンプトを書いてもらいました) これなら画像の上部が空くので、タイトルも問題なく入りそうです。 何回か作成すると、かなりしっくり来る画像が生成されました!! こちらです! まさに私が求めていた「先進的で、モビリティ感のある画像」が生成されました! 画像の上部もいい感じに空いているので、タイトルもきちんと収まりそうです。 ここから微修正は入れますが、ベースのデザインはこれで決定しました。 4. Midjourneyで解像度UP & Photoshopで微修正する ライブペインティングイベント内では時間制限があったため、先ほど生成した画像をそのまま提出したのですが、ありがたいことに依頼者票・オーディエンス票ともに1位に選んでいただき、晴れて表紙デビューが決定しました!!! となると、先ほどの画像を表紙用に綺麗に整えていく必要があります。 ここからはMidjourneyとAdobeのPhotoshopを使って画像を修正していきます。 まずは、Midjourneyを使って解像度を上げていきます。 やり方はとても簡単で、画像を開いて右下の 「Creation Actions」→「Upscale」→「Subtle」を一回クリックするだけ です。 (隣にある「Criative」は、今回は元の画像を保持したいので使いませんでしたが、ディティールを加えてクオリティアップを目指すならこちらもおすすめです!) この操作により画質は保ったまま「 896px × 1,344px 」から「 1792px × 2,688px 」と、2倍の解像度になりました! 次に、Photoshopで微修正をしていきます。 気になるのはこの辺ですね。 るぴあのヘッドホンに描かれている∞などのマーク 左下の暗号ぽいもの Photoshopで簡単に消していきたいと思います。 なげなわツールで不要な部分を囲ったら、「 生成塗りつぶし 」をクリックします。(※プロンプトは無しでOK) 綺麗に消えました! 左下部分も同様の操作を行っていきます。 自然に馴染みました! これで画像が仕上がったので、あとは入稿データを作成していきます。 5. illustratorで入稿データを作成する こちらが入稿データです! タイトルなど付け足して背表紙と裏表紙も作成したら完成です。 制作を終えて 制作を終えて感じたのは、AIのおかげでノンデザイナーでも高品質なビジュアルを作れるハードルが格段に下がったということです。 今回の表紙制作でも、ChatGPTでのアイデア出しやMidjourneyでの画像生成によって、ゼロから何かを生み出す負荷が大きく減りました。 その一方で、「何がいい」「何が悪い」の判断や、細かな世界観の調整など、AIだけでは完結しない部分も多くあります。 だからこそ、AIの力を借りつつ、人間が持つ感性を利用して方向性を選択することが重要だとも改めて感じました。 私は今年からAIプロジェクトメンバーとして携わってきて、さまざまなツールを使う中で、少しずつ「 AIへの正しい頼り方 」がわかってきた実感があります。 AIはまだ完全ではなく、人間に置き換わることはできませんが、高いクリエイティビティを持っていて、私にとって仕事に欠かせない存在になっています。 これからも、AIを心強いパートナーとして、うまく協業しながら制作に向き合っていきたいと思っています。 最後までお読みいただき、ありがとうございました!
アバター