TECH PLAY

キャディ株式会社

キャディ株式会社 の技術ブログ

93

こんにちは、Infrastructureチームの前多(@kencharos) です。AIの進化におびえながら、電気の資格の勉強でもしようかと考えている日々です。物理はいいですね。 さて、CloudSQLやAlloyDBで初回構築時に設定されるデフォルトユーザーをそのまま使っている方はいらっしゃいますか?いませんよね? 今回の話はAIネタではなくもっと地味なお話です。 データベースの権限設定のつらみ Terraform Postgres Provider の採用 パスワードの扱いについて CI/CDでの実行 1. Argo CD + k8s での Job実行 2. GitHub Actionsから踏み台サーバーに接続して実行する ownerの変更やowner指定には要注意 まとめ おまけ: 第三の選択肢、Crossplane データベースの権限設定のつらみ スキーマの設定を自動マイグレーションするサードパーティのサービスをそのまま使う場合ならともかく、 自分たちで開発しているサービスであれば、マイグレーションとアプリケーションの実行でDBユーザーの権限は分けた方が安全ですし、 複数サービスでDBを共有する場合なら、データベースやスキーマレベルで権限を分けたくなります。 また、AIツールがSQLを実行することも今後はあり得ると思います。 その場合もAIツールが勢い余ってすごいSQLを発行しても大丈夫なように権限を絞ったDBユーザーを使うようにしておきたいものです。 Google CloudのCloudSQLやAlloyDBではDBユーザーやIAMユーザーの作成はできます。 しかし、RDBMS内の権限付与(Grantなど)は結局RDBMSにログインしてSQLを実行する必要があります。 この手のSQLはどうやって管理しているでしょうか? prismaなどのアプリケーションのスキーママイグレーションで管理するのも手段の1つです。 ですが最初にマイグレーション用の権限を持つユーザーは作っておく必要があります。 また何らかの理由で開発サイクルとは別のサイクルでDBユーザーを追加、修正することは多々あります。 レプリケーション用のユーザーが欲しいとか、新しいメンバーが参加したからそのIAMユーザーを追加したいとか、要はスキーマ定義とは独立して管理したいわけです。 筆者らは、このような権限付与を手順書にSQLを書いて手動で実行していました。 苦痛を伴う作業と認識しながら、よい代替手段が当時は思いつきませんでした。 これにはいくつか問題があります。 複数環境で繰り返し実行が必要 変更の都度、現状の状態から細かいGrantやRevokeが必要 手順書の修正などで過去の権限付与の内容の修正などが面倒 などの理由から、微妙に権限付与の内容が環境ごとに異なってしまうことがあります。 意図した通りに権限付与ができているのかが把握しづらくなります。 サービスの拡大に伴ってデータベースの数も増えてきたこともありどうにかしなきゃと思い、解決策を探し始めました。 Terraform Postgres Provider の採用 権限管理のツール化を行うにあたり、要件は次の3つです。 手動ではなくCICDなどから実行して作業を自動化できること 宣言的に権限付与を管理できること PostgreSQLの様々な機能(extension, database, schema, grant, default_privileges, replication slotなど)に対応できること 当初はYAMLに権限付与設定を書いて、自作しようかと思っていましたが、 terraform の postgresql provider を見つけたのでこれでいいやとなりました。 ただし、posgresql providerはサードパーティのプラグインですので、扱いには気をつけてください。 要件と概ね一致していましたし、筆者らはTerraformを常用しています。 hclファイルで宣言的に権限設定を管理できるのが大きな理由でした。 (欲を言えば、hclよりも読みやすいフォーマットの方がよかったのですが、それも今ならClaudeなどでサマリできそうだし、まあいいやとなっています) これを使って、以下のような権限設定を作成します。 ownerとなるmigration user、アプリケーション実行用のapplication userを作成する database, schemaは独自のものを作る application userにschema, tableへのgrantとdefault privilegesを設定する。 上記をhclで表現したものが次の通りです。 terraform { required_version = "1.10.5" backend "local" { path = "sample.state" } required_providers { postgresql = { source = "cyrilgdn/postgresql" version = "1.26.0" } } } // プロバイダの設定。ここではデフォルトユーザーやadminユーザーを指定する provider "postgresql" { host = "x.x.x.x" port = 5432 database = "postgres" username = "postgres" password = var.admin_password sslmode = "disable" connect_timeout = 15 } // userの作成 resource "postgresql_role" "migration" { name = "migration" login = true # password_woはstateにパスワードを記録しない。パスワードを変更したい場合は password_wo_versionを変更する password_wo = var.migration_password password_wo_version = "1" bypass_row_level_security = true } resource "postgresql_role" "application" { name = "application" login = true password_wo = var.application_password password_wo_version = "1" } // databaseの作成 resource "postgresql_database" "app_db" { name = "app_db" owner = postgresql_role.migration.name allow_connections = true } // schemaの作成 resource "postgresql_schema" "schema" { name = "app" owner = postgresql_role.migration.name } resource "postgresql_grant" "grant_to_schema" { database = postgresql_database.app_db.name role = postgresql_role.application.name schema = postgresql_schema.schema.name object_type = "schema" privileges = [ "USAGE" ] } # grant role to table resource "postgresql_grant" "grant_to_application" { database = postgresql_database.app_db.name role = postgresql_role.application.name schema = postgresql_schema.schema.name object_type = "table" # objectsが空の場合は all # objects = [] # privileges は明示する。ここに無いものは revoke 対象になる。 privileges = [ "SELECT" , "INSERT" , "UPDATE" , "DELETE" , "REFERENCES" , "TRIGGER" , "TRUNCATE" ] } # default privileges を指定して、application userに対して新規テーブルの権限を付与する resource "postgresql_default_privileges" "default_privs" { role = postgresql_role.application.name database = postgresql_database.app_db.name schema = postgresql_schema.schema.name owner = postgresql_role.migration.name object_type = "table" privileges = [ "SELECT" , "INSERT" , "UPDATE" , "DELETE" , "REFERENCES" , "TRIGGER" , "TRUNCATE" ] } あとは、初期ユーザーがあるデータベースがあれば、terraform applyで想定通りの権限付与ができます。 宣言的に権限を記述しているので、例えば上記のgrantからprivilegesを足したり削除したりすると、それがapply時にgrantやrevokeとなって実行されるようになります。 基本的にはこれで十分なのですが、細かい点で気をつけたところを次に述べます。 パスワードの扱いについて 前述のサンプルでは、 password_wo を用いてユーザーパスワードを設定しました。 resource "postgresql_role" "application" { name = "application" login = true password_wo = var.application_password password_wo_version = "1" } これは事前にパスワードがわかっている場合に、 実行時引数や環境変数で application_password にパスワードを渡すようにすればstateにパスワードは残らなくなります。 一方でpostgresql provider 1.26.0の時点でこのリソースはephemeral resourceではありません。 ですので次のように、Terraformで動的に生成したパスワードを直接設定するような操作は現時点ではできません。 ephemeral "random_password" "password" { length = var.length special = false upper = true lower = true min_upper = 1 min_numeric = 1 min_lower = 1 } resource "postgresql_role" "application" { name = "application" login = true # これはできない. password_wo = ephemeral.random_password.password.result password_wo_version = "1" } そのため現時点では、筆者らはGoogle Secret Managerに一旦ランダムパスワードを生成・保存して、それを取り出して使うという設計としています。 ランダムパスワードの生成はCloudSQLやAlloyDBのインスタンス作成と同時に行う別のTerraformで行っているので、それと権限設定のTerraformとサイクルを分けています。 CI/CDでの実行 宣言的に権限を記述できるようになったので、バージョン管理もできるしCI/CDとの連携もできるとベストです。 ただし、筆者らの環境ではセキュリティの理由でデータベースサーバーにPublic IPを付与してないので、通常のGitHub ActionsからTerraformを実行してもデータベースに接続できません。 そこで以下の2案を考え、2の踏み台サーバーへ接続する方法を採用しました。 1. Argo CD + k8s での Job実行 GitHubのチェックアウトからterraform実行までをスクリプト化して、k8sのJobとして定義しArgo CDからJobを実行します。 GitHub Appsを準備して、次のようなスクリプトで動きます。 # checkout repo echo " ${ _APPS_PRIVATE_KEY } " > /tmp/gh_cert header = $( echo -n ' {"alg":"RS256","typ":"JWT"} ' | base64 -w 0 ) now = $( date " +%s " ) iat = $((${now} - 60 )) exp = $((${now} + ( 10 * 60 ))) payload = $( echo -n " { \" iat \" : ${iat} , \" exp \" : ${exp} , \" iss \" : ${GITHUB_APPS_ID} } " | base64 -w 0 ) unsigned_token = " ${header} . ${payload} " signed_token = $( echo -n " ${unsigned_token} " | openssl dgst -binary -sha256 -sign /tmp/gh_cert | base64 -w 0 ) jwt = " ${unsigned_token} . ${signed_token} " access_token = $( curl -s -X POST \ -H " Authorization: Bearer ${jwt} " \ -H " Accept: application/vnd.github.v3+json " \ " https://api.github.com/app/installations/ ${GITHUB_APPS_INSTALLATION_ID} /access_tokens " \ | jq -r " .token " ) #checkout git clone https://x-access-token: ${access_token} @github.com/ < org > / < repo > cd < repo > export ADMIN_USER = ${TF_VAR_admin_user} export ADMIN_PASSWORD = ${TF_VAR_admin_password} export DB_HOST = ${TF_VAR_db_host} # run terraform terraform init terraform apply -auto-approve ただ、できればGitHub Actionsで実行したかったので見送りました。 2. GitHub Actionsから踏み台サーバーに接続して実行する 筆者らはデータベースにアクセスする際はVPC内にある踏み台サーバーにアクセスしています。 それと同じことをGitHub Actionsから行います。 前提条件として、次のようなセキュリティ対策を行なっておくと良いです。 Workload Identityを使用する Identity Aware Proxy(IAP) を設定する この方法を採用するにあたって、以下の記事が参考になりました、ありがとうございます。 zenn.dev ポイントとしては、記事にあるようにCloudSDKを別途インストールした上で、beta computeでSSHトンネルを作成することです。 # connect bastion server by beta gcloud beta compute ssh " ${bastion_host} " \ --zone =" ${bastion_zone} " \ --ssh-flag =" -L 127.0.0.1:5432: ${db_host} :5432 " \ --ssh-flag =' -N ' \ --ssh-flag =' -f ' \ --tunnel-through-iap \ --quiet sleep 10 export TF_VAR_admin_user = ${admin_name} export TF_VAR_admin_password = ${admin_password} # 踏み台に接続済みなので、localhostで良い。 export TF_VAR_db_host =localhost terraform init terraform apply --auto-apporve これで、手動でSQLを実行することなく権限設定ができるようになります。 ownerの変更やowner指定には要注意 postgresql providerを使うと、記述の変更が差分反映のSQLとなるので便利ですが、あらゆる変更に対応できない場合があることには注意が必要です。 例えばdatabaseやschemaのownerを変えたりするには、superuserやowner自身である必要があります。 一方で、CloudSQL, AlloyDBのデフォルトユーザーは、 supueruserではなくそれと似て非なるcloudsqladminやalloydbadminというロールが付与されています。 それでsuperuserが必要とされるような変更操作ができないことがあります。 そんな変更が発生する頻度は少ないので、もしそうなった場合は素直にSQLを直接実行して対処する方が良いです。 1つだけ困ったのは、 publicationやreplication slotの作成です。これらのリソースの作成もsuperuserかownerでなくてはいけないので、 Terraformで管理したい場合はproviderに設定するユーザーをdatabaseのownerに切り替える必要があります。 そのためエイリアスを付与したproviderを用意して、特定リソースにそのproviderを指定します。 # 通常のprovider設定 provider "postgresql" { host = var.db_host port = var.db_port database = var.default_database username = var.admin_user password = var.admin_password } # databaseオーナーのprovider設定 provider "postgresql" { alias = "relication" host = var.db_host port = var.db_port database = var.default_database username = var.replication_user password = var.replication_user_password } resource "postgresql_replication_slot" "replication" { # relication slot作成用のproviderを指定する provider = postgresql.replication name = "replication_slot" plugin = "pgoutput" database = "xxxxx" } まとめ GitHub Actionsから踏み台サーバーになかなか接続できなかったり、replication slotの作成でsuperuserじゃない問題に直面した時は頭を抱えましたが、 色々あってできるようになりました。 苦労はありましたが、その効果は絶大で新規サービスの開発で新しいデータベースができた時でも複数環境分のセットアップが迅速、かつ正確にできるようになりました。 興味があれば試してみてもらえたり、他にこんなやり方があるよというコメントもお待ちしています。 おまけ: 第三の選択肢、Crossplane この記事を書くにあたって、改めて色々調べていたら、 Crossplaneのpostgres provider を見つけました。 Crossplaneはk8s上のカスタムリソースとして、インフラなどの様々なリソースを管理するプロダクトです。 なのでこれを使えば、YAMLでデータベースの各種権限を設定できるし、Argo CDを使ってGitOpsでマニフェストを管理できそうです。 Terraformを実行することなく、権限設定が同期できるのでとても便利だなと感じました。 Corssplaneを導入する計画は今のところないのですが、もし導入するならやってみたいと思います。
こんにちは、製造業データ活用クラウド CADDi Drawer でQAを担当しているOshiroです。 業務としては、開発チームと並走し品質保証に関する活動をしています。コードを書くことはほぼ無く、主にテスト設計のドキュメント作成やテスト実施、リリースにおける運用作業に携わっています。 また、自作のカエルのキャラ絵文字を描いて、みんなに使ってもらう「カエルの人」としても活動しています。 このような絵文字を作っています。 今回は、AI活用で非エンジニアでも可能なツール生成術の一例を紹介したいと思います。 PCにツール類のインストール作業は一切不要で、主に Google Workspace のサービスを利用します。 ※Gemini Canvas、Google Sitesの利用を前提としています。管理者の権限設定によっては機能がOFFになっている可能性があります。 やったこと Gemini Canvas でテストデータ生成ツールを構築 Google Sites で社内共有 背景:テストデータって作るのって意外と大変 私がQAとして関わっているプロダクトは、大量の図面データや関連文書を検索してインサイトを得るという特性を持っています。そのため、大量のテストデータが必要となります。 このテストデータを準備するのが意外と大変で、テスト工程が始まる段階でその都度作成したり、保管場所を探しに行ったり、細かな時間を消費していました。 データ作りの対応方法がバラバラだと、人によって対応スピードが変わるので改善をする必要がありました。 解決策の模索:AI活用でどうにかしたい 手軽にテストデータを大量に作るためには、ツールを作れば良いと考えました。 調べると、Geminiの中でCanvasという機能を使えば、画面プレビューをしながらコードが書けると分かりました。 これならUI付きのツールを作れるかもしれないと思い試してみることにしました。 試してみる:Gemini Canvas Gemini画面を開きツール「Canvas」を選択し、とりあえず動作確認してみました。 「図面のサンプルデータの画像が生成できるツールを作りたいです」 という大雑把なプロンプトを書いて送信。 結果、それらしいものが1発で生成されました。 初回で生成された時の状態はこのような形で表示されました。 画像もダウンロードできる。これは使えそうだと確信しました。 ここからは以下のようなプロンプトで、対話形式で機能を追加していきました。 プロンプト指示 実装された機能 「大量データを生成可能にして、出力をPDFファイルにまとめたい」 PDF一括ダウンロード機能 「PDFに保存する画像の数を指定できるようにしたい」 ページ数指定機能 「ランダム性を持たせて次々に新規生成できる形にしたい」 リアルタイム・ランダム生成機能 「新規生成された画像はPDF生成前のストックとして使えるようにしたい」 PDF生成前のストック機能 といった流れで微調整を重ねてブラッシュアップしていき、ツールができあがっていきました。 出来上がったツールの状態がこちらです。 Canvasコードの画面には共有ボタンでソースコードをコピーする機能があり、HTMLのソースコードとしてシェアできるようになっていました。 社内公開してみる:Google Sites 社内では、Google Sitesで共有することにしました。 Google ドライブ で「サイト」を作って、Canvasで生成したソースコードを貼り付けるだけ。 (サイドバー「挿入」 -> 埋め込む -> 埋め込みコード の入力欄) あとはページ設定の調整やURLとなる文字列を決めつつ公開ボタンを押していく簡単な作業でした。 サイトの公開設定としては社内のGoogleログイン状態でないと、アクセスできない状態にしています。 共有して、他のチームにも使えるようにしたところ、各所で喜びの声が聞けました。 「動作確認のために利用するテストデータとして使わせてもらいます」 「こういうのほしかった」 「大量データ生成できて助かってる」 元々は自分のために作ったツールではありますが、みんなの役に立てたので良かったなと感じています。 生成したコード自体は、業務には強く影響しない領域に配置しています。 バグがあったとしてもプロダクトには影響なく、再度Gemini Canvasに読み込ませて追加修正できるので手軽にメンテナンスすることが可能です。 サイトには、新たなページもどんどん追加していけるので効率化できるツールを現在も増やしていっています。 高機能な3Dデータの生成ツールも作れたりします。 こちらが、3Dデータをプレビューしながら穴を開けたり突起物を追加したりできるようなツールです。 この3Dデータ生成ツールは、エンジニアからも「なにこれ!?すごい!Google Sitesでこんなの動かせるの?」と驚かれました。 躓いたポイント・工夫したこと 機能を追加していくうちに、Gemini Canvasでは動くが、Google Sitesにコードを貼り付けた時には動かないことがありました。 その時は、「Google Sitesに貼り付けたら動かなかった。対応して欲しい」という指示で、GeminiがGoogle Sitesでも動作するコードに書き換えてくれます。 また、ランダム性を持たせることには少し悩む部分はありました。Google Sitesに埋め込む際、工夫したのが「ランダムな文言の出し方」です。 毎回AIに考えさせると動作が重くなる懸念があります。そして何より「想定外の変な言葉」が出てくるリスクがあります。 そこで、あらかじめ安全で適切な文言リストをソースコードの中に用意し、そこからランダムに抽出する仕組みにしました。 このリスト作成時には、「著作権や倫理的に問題ないか」をQA目線でチェックしました。 これにより、「爆速で動き、かつ事故が起きない」という安心の社内ツールが完成しました。 終わりに 今回は、ツールのセットアップなしで、非エンジニアでも可能なデータ生成ツール作成の一例を紹介しました。 まずは自分が楽になるためのツールを作り始めたことがきっかけでしたが、自分が困っていたことであれば他の人も同じように困っているかもしれないと思い共有したところ感謝され、自分のチームだけでなく他のチームにも貢献ができました。 また、このツールを使ってもらうことでフィードバックをもらい、新たなアイディアも生まれてきているのでこの活動は続けていこうと考えています。 Gemini Canvasは画面プレビューしながらのコード生成が手軽で強力であると感じています。 一つのアイディアとして参考にしていただければと思います。
こんにちは、昨年キャディ株式会社に入社した佐野です。入社して初めての技術記事になります。 この記事では、これまでの開発経験を振り返りながら、それが推論システムの運用にどう活きたのかを整理してみようと思います。 対象読者 機械学習システムを運用したことがないSRE、バックエンドエンジニアの方々 これから機械学習システムを運用していくSRE、バックエンドエンジニアの方々 伝えたいこと 機械学習のモデル開発経験や機械学習システムの運用経験がなくても、機械学習システム(推論システム)の運用に貢献できます。SREの領域と被るものが大半であるため、今までの経験を活かせることが大半です。 入社前までの経験 直近5年くらいはバックエンド、SREを往復するようなキャリアでした。APIの設計・開発から始まり、DB設計、システム設計などを色々やっておりました。機械学習の経験はというと、プロジェクトで半年ほど画像認識で使用する画像をOpenCVでコネコネしていたことはあります。ただそれも7年以上前なので、記憶の彼方に消えてしまってます。 配属先のチーム キャディに入社後の私の配属先のチームは、Analysis Platformチームで、機械学習システム(推論システム)を開発・運用しています。具体的には、モデル開発以外(システム設計、API設計・開発、インフラ構築など)の業務を全て担っています。 役に立った経験 実際チームでシステムを運用、タスクを消化していく中で感じたことは、バックエンド、SREの経験をそのまま使えるということです。例えば、以下のような経験はそのまま活かすことができます。 システム設計 データリソースの選定 モニタリングやアラートの設計/運用 ネットワーク設計 など システム設計(非同期設計) システム設計については、推論システムだからといって特別なものはほぼなく、通常のシステム設計の考え方(非同期設計など)に似た部分が多いように感じています。その中でも特に非同期設計の経験は役に立ちます。なぜ役に立つかは後述します。 データリソースの選定 この経験も非常に有用だと考えています。推論APIの設計にも依存しますが、モデルによってはAPIのレスポンスが肥大しがちです。例えば物体検出のモデルでは、検出数によってレスポンスが大きくなることもあり得ます。この推論結果をどのデータリソースに格納するのが望ましいかは腕の見せ所であり、面白いポイントだと感じています。Cloud SQLのようなRDB、mongoDBのようなNoSQL、GCSのようなストレージサービスなど様々な選択肢がある中で、要件に応じた選定を行う必要があります。これを決めていくにあたり、これまでのデータリソースの運用経験が非常に活きると考えています。 モニタリングやアラートの設計/運用 推論システムといえど、運用という点ではこれまでのシステムとほぼ変わりません。SLOの定義や、アラート疲れを起こさないための運用設計など、まさにSREの知見が求められるシーンが多かったです。 また私が入ったタイミングで、既存の推論システムをGKEへ移行しており、システムをいかに安全に移行するか、モニタリングをどう再設計するかなどの知見も非常に有用でした。 zenn.dev 通常システムとの違い 今までの経験がいきるとはいえ、推論システムと通常システムには多少違いがあります。自分の中で感じた部分を少し書ければと思います。 非機能要件: 非同期処理にしたい モデルによって推論に時間がかかることはしばしばあります。例えば物体検出のモデルでは、検出数が多ければ多いほど推論に時間がかかるため、処理時間が数十秒になることも珍しくありません。そのため非機能要件を定めるに当たっては注意が必要になります。システムを運用する視点では、非同期処理によせることができれば、同期処理と比較し、シビアに処理時間を気にしなくて良くなります。 また、当たり前ですがGPUは通常のインスタンスと比較してコストが高くなります。そのためGPUを使用しないときは、インスタンスを停止してコストを抑える必要があります。具体的にはkubernetesの場合はKEDAのScaledObjectなどを使い、使用していない期間のインスタンスの停止などの工夫を行い、コストを抑える必要があります。 このようなこともあり、非同期処理の経験が非常に活かせると考えています。 技術選定: GPUを利用するための術を知りたい 推論システムを運用していく上で避けて通れないのは、GPUの運用です。私の知る限りGoogle Cloudでは、Compute EngineはもちろんのことマネージドサービスであるVertex AI、サーバーレスであるCloud RunでもGPUを扱えます。これらの選択肢の中で、何が要件としてマッチしているかは検討できるようになる必要があると感じています。 docs.cloud.google.com 余談ですが、Compute Engineのマシンタイプでは、A2 マシンシリーズやN1 マシンシリーズ(T4)、G2マシンシリーズ(L4)という名前が出てきます。最初これらのアルファベットを聞いて、何を言ってるのかさっぱりわからなかった記憶があります。 docs.cloud.google.com リソース管理: GPUを効率的に使いたい GKE上でGPUを効率的に使用するにはいくつか方法があります。マルチインスタンスやタイムシェアリングなどの方法があります。私はまだ実装経験はありませんが、これらをどのように使っていくかはまさに推論システムの特徴なのではと感じています。 docs.cloud.google.com ライブラリ管理: マイナーバージョンの更新であっても細心の注意を払いたい 機械学習で使用されるライブラリであるnumpyやtorchなどのバージョン更新には細心の注意が必要です。たとえマイナーバージョンの更新であってもnumpyは2.0系統であれば、推論結果に影響が出る可能性があるようです。私もPR作成時に意図せずバージョンが上がった時があり、機械学習エンジニアに指摘してもらうことがありました。 最後に まだまだ入門したばかりで、学ぶことも多々あると思いますが、SREやバックエンドでの経験は確実に活き、運用できていると感じます。皆さんもぜひチャレンジしてみてくださいね!
こんにちは。キャディ株式会社の Analysis Platform Group で MLOpsエンジニアを務めているAmaniです。 普段はキャディの各サービスの裏側で稼働する機械学習基盤やバックエンドの開発、およびアプリケーションとの連携部分を担当しています。 前半期は、社内の機械学習ワークロードを既存のマネージドサービスから Google Kubernetes Engine (GKE) に移行するプロジェクトを進めていました。 その過程で、shadow deploy / mirroringの重要性を改めて強く認識する出来事がありましたので、本記事では、その背景と学びについて書きます。 背景:機械学習ワークロードのGKE移行 Shadow deploy導入までの経緯 とはいえ、本番データでは簡単にテストできない 解決策としての shadow deploy / mirroring コストとの向き合い方 まとめ 背景:機械学習ワークロードのGKE移行 前半期は、社内の機械学習ワークロードを Google Kubernetes Engine (GKE) に集約するプロジェクトを進めていました。 従来の構成では、ワークロードは大きく同期推論と非同期推論の2種類に分かれており、Google Cloud の Vertex AI、Compute Engine、Cloud Run を組み合わせて構築されていました。 これらを運用・開発の両面で扱いやすくするため、ワークロードをGKEへ統合しています。 移行の背景や全体的なメリットについては、同じAnalysis Platform GroupのHirokaが以下の記事で詳しくまとめているので、興味がある方はそちらもぜひ参照してください。 zenn.dev 移行対象となるモデルは合計で15個あり、それぞれが異なる特性(パフォーマンス要件やリソース消費)を持っています。 例えば、CADDi にアップロードされる図面に含まれる製造業特有の記号を検知するモデルや、それらを分類するモデル、文字を抽出するモデルなど、多様な役割のモデルが含まれています。 さらに、これらのモデルを呼び出す経路も一様ではなく、APIや大規模な前処理パイプラインなど、合計で5種類以上のクライアントが存在していました。 クライアントの多くは別チームによって開発・運用されているため、移行にあたっては各チームとの調整も重要なポイントでした。 これだけのモデル数・クライアント数を対象に、Claude CodeやDevinといったツールも活用しながら、グローバルなメンバーで短期間に移行を完了させた点については、それ自体で一つの記事になる規模の取り組みですが、本記事ではその中でも、shadow deployによって得られたMLシステムの信頼性に焦点を当てます。 Shadow deploy導入までの経緯 MLモデル移行作業は、まず同期推論モデルから開始しました。影響範囲が比較的小さいモデルを選定し、パイロットとして移行を進めました。 信頼性を担保するため、不具合が発生する前提でロールバックしやすい設計とし、あらかじめロールバックプランも用意しました。 移行後の構成に対しては、関係チームと連携しながら負荷試験・動作確認・精度検証を実施し、必要なチューニングを行ったうえで、クライアントの接続先を新しいシステムへ切り替えました。 切り替え直後は大きな問題もなく、安定して稼働しているように見えましたが、切り替えから2日後、一部のリクエストで以下のようなエラーがバースト的に発生しました。 {"error":{"code":400,"message":"Invalid request: Input should be a valid dictionary or object to extract fields from","status":"INVALID_ARGUMENT"}} 影響範囲は限定的でしたが、原因を即座に特定できなかったため、SLO内ではあったものの、リスク回避を優先して移行前の推論システムへロールバックを実施しました。 後続の調査により、原因は上流システムから送られてくる一部リクエストに含まれる例外的なヘッダーであることが判明しました。 移行前のVertex AI ベースのシステムではこのヘッダーを許容していた一方で、移行後のシステムではエラーとして扱われていました。 さらに、このヘッダーは本番環境のごく一部のリクエストにのみ含まれており、サンプリングされたテストデータでは再現されないパターンでした。 このようなケースは、オフラインテストだけで事前にカバーすることが難しい典型例といえます。 結果として、ロールバックしやすい設計と事前に準備していたプランにより、障害に発展する前に旧システムへ切り戻すことができて、その後、該当のケースに対応する形で新システムの修正を行いました。 今回の事例はヒヤリハットにとどまりましたが、ここで改めて感じたのは、 複数チームが関与する複雑なシステムほど、本番データに基づく検証が不可欠になる という点です。 とはいえ、本番データでは簡単にテストできない 一方で、本番データには顧客データが含まれますので、セキュリティやガバナンスの観点から、自由にテストに使えるものではありません。 この「必要だけど使えない」というギャップは、特にMLシステムにおける推論結果の品質や安定性の検証において大きな課題となります。 たとえば、 データ分布の偏り 想定外の入力パターン 上流システムの揺らぎ といった要素は、オフラインのテストデータではどうしても再現しきれません。 セキュリティやガバナンスの制約もあり、実質的には本番相当の環境で検証を進める必要がありました。ここでいう「本番でテスト」は直接本番システムに影響を与えるものではなく、本番と同じ入力を用いて裏側で挙動を観測する、いわゆるshadow deployを指しています。 解決策としての shadow deploy / mirroring 上記のヒヤリハットを踏まえ、移行プロジェクトの残りの対象モデルでは、簡易的なshadow deploy / mirroringを導入することにしました。 Shadow deployとは、短く言うと「新しいバージョンを旧バージョンと並行で動かし、本番と同じ入力を受けさせながら、その出力はシステムやユーザーには影響を与えない形で比較対象として扱うデプロイ手法の一つ」です。 今回の移行プロジェクトでは、移行後のモデルを「セキュリティが担保されている本番環境で本番と同じ入力」で裏側で動かし続け、既存モデルと並行して挙動を観察しました。もちろん、CIテストやstaging環境での負荷試験・精度試験など、デプロイ前の一通りの検証を終えたうえで、最終確認として実施しています。 移行対象の既存システム図(イメージ)は以下の通りです。 図1 既存システム図 スケジュール制約を踏まえ、まずは「検知と差分把握」にフォーカスした最小構成で進め、以下の図のようなシステムを構築しました。なお、図が複雑にならないように、クライアントおよびモデルはそれぞれ1つに簡略化して示しています。 図2 Shadow稼働期間中のシステム図 推論結果の比較検証に加えて、裏側で両システムのログやパフォーマンスを継続的に監視していました。その結果、いくつかの例外的なケースもユーザー影響を出さずに本番環境で発見することができ、調整後に本番切り替えを行うことができました。本番切り替え後のシステム図は以下の通りです。 図3 切り替え後のシステム 比較検証期間中に検知・修正できた例外的なケースとしては、以下のようなものがありました。 新旧システム間のパフォーマンス・リソース差分 従来は Vertex AI や Compute Engine 上で実装されていた推論基盤を、GKE上のLitServeへ移行したことで、実行基盤およびフレームワークの両面に変更が入りました。そのため、既存システムと同等以上のパフォーマンスが得られるかを改めて検証する必要がありました。 その検証にあたっては、テストデータでは本番トラフィックの分布や特性(検出対象の数など)を正確に再現することが難しいという課題がありましたが、shadow deploy を用いることで、本番データを活用した確実な検証が可能になりました。 またこの過程では、timeout設計の重要性も再認識しました。特にバッチ処理や長時間実行されるリクエストでは、旧システムとの前提の違いによりタイムアウトの発生条件が変わり、設定値の見直しが必要になりました。あわせて、timeoutを長く設定しすぎるとリソース占有時間が増え、結果としてコストが増加するため、性能だけでなくコストとのバランスを踏まえた設計が重要になります。 実データでのみ発生するエラー 検知対象が例外的に多い図面において、システムリソースが圧迫されるケースがありました。元システムでは問題なく処理できていたものの、移行後のシステムではエラーが発生しました。 この問題は、LitServe、Kubernetes、Google Cloud Pub/Sub のパラメータチューニング(同時メッセージ数、batch size など)によって解決しました。 精度のギャップ 一部のモデルでは、新システム側で前処理が正しく適用されていないケースがあり、旧システムとの差分として推論結果の精度ギャップが発生しました。 これは shadow deploy によって初めて検知できた問題であり、前処理の実装漏れに起因するものでした。 上記の経験を踏まえ、特に複雑なMLシステムにおいては、shadow deployの仕組みを使って継続的にデータの性質や分布を把握することが、より信頼性の高いシステムにつながると強く実感しました。 この考え方自体はセオリーとして理解していたものの、今回の経験を通じてその重要性を改めて認識しました。 こうしたメリットを得られることが前提となる一方で、次に論点となるのはコスト面です。特にMLのワークロードではGPUリソースの利用も伴うため、無視できない要素になります。 この点についてはトレードオフもあるため、次の章で整理します。 コストとの向き合い方 Shadow deployは、トラフィックの複製と追加の推論実行が発生するため、インフラコスト(特にGPUによる推論コスト)の面でオーバーヘッドが生じます。 ただし、用途に応じて設計することで、無駄なコストは抑えられます。 例えば: 機能確認が目的の場合 フルスケールである必要はなく、縮小構成で数日間動かすことで、挙動や例外ケースの検知には十分なケースが多いです。 負荷耐性の確認が目的の場合 フルスケールで短時間(数時間〜1日)実施する方が効率的です。 さらに、すべてのリクエストを対象とするのではなく、トラフィックの一部のみをshadow側に流すといった制御を行うことで、コストと検証精度のバランスを取ることも可能です。 このように目的と検証対象を明確にすることで、「必要十分なshadow環境」を設計できます。 まとめ 今回の移行を通して、特にMLシステムにおいては「本番環境でしか見えない挙動」が一定割合で存在するという前提に立つ必要があると感じました。 オフラインテストやstaging環境での検証だけでは、データ分布や上流システムの揺らぎまではカバーしきれません。 そのギャップを埋める手段として、shadow deployは有効です。特に複雑なMLシステムにおいては実質的に必須の仕組みだと考えています。 一方で、コストや運用負荷とのトレードオフがあるため、目的を明確にした設計が重要になります。 さらに技術面以外の学びとして、プロジェクト管理の観点では、MLモデル(そしてAPI全般)の移行は単純にモデル数に比例するものではなく、各モデルに紐づく呼び出し元クライアントとの関係性に依存して工数が増えるという点があります。特に1つのモデルに複数のクライアントが存在する場合、その調整や影響確認の分だけ工数は増加します。 今回の移行プロジェクトでは最小構成でshadow deployを導入しましたが、今後は Istio のトラフィックミラーリングなど、より標準化された仕組みの活用も検討しています。
※本記事は、こちらの記事を和訳したものです stratomere.com 序章 航空機が音速を超えると、衝撃波が発生する。衝撃波は周囲の空気が伝わる速度よりも速く移動するため、周囲の空気がその擾乱を吸収しきれず、不連続性が生じる。これがソニックブームである。 池の水面に広がる波紋のように、衝撃波は外側へと伝播し、その影響は距離とともに薄れていく。AIサービスの需要は、技術サプライチェーンが吸収できる速度をはるかに超えて拡大している。マイクロソフトのCEOによれば、当初は大規模な言語モデルの学習と実行にGPUが必要だったが、今ではそれらを動かすためのエネルギーが課題となっている。こうした波紋は私たちの日常生活にも波及し、データセンターの需要が電力網の容量を圧迫するにつれて電気料金が高騰し、限られた半導体供給を巡って家電製品同士の競争が激化するにつれて、家電製品の価格も上昇している。 物理的なサプライチェーンは、ソフトウェアのようなスピードで変化に対応できない。半導体工場や発電所の建設には、数十億ドルもの設備投資と数年にわたる期間、そして建設と運営に必要な人材が不可欠である。需要が明確で資金も潤沢にある場合でも、生産能力を一夜にして拡大することはできない。この媒体は、急激な変化に抵抗する。 物理的および経済的な制約によって計算能力が制限される世界において、ソフトウェアアーキテクチャはますます企業戦略の表現としての役割を担うようになっている。それは、顧客の需要と希少で高価なリソースとのバランスを取り、それによって何が拡張可能で、何が停滞し、何が非経済的になるかを決定する。 豊富な資源に基づいて構築されたソフトウェア 数十年にわたり、ソフトウェアはハードウェア性能の着実な向上という環境の中で発展してきた。ムーアの法則のおかげでトランジスタ数は約2年ごとに倍増し、より高速なプロセッサ、より高速なネットワーク、そしてより多くのメモリが実現した。デナードのスケーリングは、トランジスタ密度の向上と駆動電圧の低下によって電力密度を抑制することで、この進歩を持続させることを可能にした。しかし、メーカーがマルチGHz帯に進出し、コア電圧がトランジスタの閾値電圧に近づくにつれて、この傾向は鈍化した。小型化に伴う非効率性が増幅されたことも相まって、電力密度は持続不可能な熱特性に近づいた。 IntelのXeon 6やAMDのEPYC 9005シリーズといった最新のハイエンドデータセンターCPUは、前世代と同じクロック周波数範囲で動作し、性能向上の大部分はシングルコア性能の向上ではなくコア数の増加によるものであり、ソケットあたり200コア近くに達することもある。しかし、キッチンで料理人を増やしても必ずしも夕食の準備が速くなるわけではないように、コア数を増やしてもコンピュータの速度が自動的に向上するわけではない。マルチコアCPUは高度な協調メカニズムを必要とし、これらの新たな協調の課題をソフトウェアに押し上げ、ロック、セマフォ、そして並列コンピューティングに特化した研究分野を生み出した。これらの概念は、ソフトウェアチームに大きな認知負荷をかけ、顧客に機能を提供する方法だけでなく、基盤となるハードウェアの利用率を最大化して高性能なユーザーエクスペリエンスを提供する方法についても考えなければならない。 ハードウェアによってもたらされるこうした複雑さに対処するために、私たちは詳細を隠すための抽象化を作り出す。モバイルアプリ開発者はAPIのことだけを考えればよく、3GPPやLTEといったAPIを支えるセルラー技術を理解する必要がない。ウェブサイトを構築する人は、豊富なアニメーションやグラフィックに集中でき、それらを支えるグラフィックレンダリングパイプラインについて心配する必要はない。ジョン・オースターハウトは著書『ソフトウェア設計の哲学』の中で、この原則を的確に捉えている。つまり、複雑さをカプセル化するシンプルなインターフェースを備えた「深い」モジュールを構築することで認知負荷を軽減し、組織の拡張性を高めることができる。 パブリッククラウドの登場はこの抽象化をさらに推し進めた。物理的なコンピューティングリソースはAPIの背後に移り、サーバーの調達やデータセンターの稼働開始を待つ必要がなくなった。新しいコンピューティングクラスターはクリック一つで利用できる。すべてのハードウェアを調達するために必要な設備投資さえも、従量制の運用費用として抽象化された。クラウドプロバイダーは規模の経済性を活用することで、一見すると豊富なコンピューティングリソースを提供し、摩擦をほとんど感じることなく、数千台のサーバーに垂直方向および水平方向に拡張できる選択肢を与えた。今や、特定のソフトウェアパフォーマンスの課題を、単により多くのコンピューティングリソースとメモリを投じることで、つまり資金調達によって解決するオプションが生まれたのだ。しかし、現代のワークロードの要求の急激な変化により、これらの回避策の一部が限界に達し始めている。 メモリの逼迫 抽象化は問題を分解し、人間が扱いやすくするのに役立つが、根本的なパフォーマンス上の課題を解決するものではない。並列化されたワークロードが増加するにつれて、特にGPUやその他のアクセラレータにおいて顕著に見られるように、課題は並列計算を実行することから、十分なデータを効率的に供給することへと変化した。 1990年代後半にNvidiaによって普及したGPUは、もともと並列化が容易で反復的なグラフィックス処理パイプラインを高速化するために開発された。これらの特殊な技術は並列処理の限界を押し広げてきた。実際、最新のデータセンターGPUは数万個のコアを搭載し、同時に動作して毎秒数千兆回の計算を実行する。追いきれないほどのゼロの数である。同様に、数万人の料理人がいるキッチンでは、全員を忙しくさせるために大量の食材が絶えず流れなければならない。演算コアにデータを流し込むこの能力、しばしばメモリ帯域幅と呼ばれるもの、は容易にボトルネックになり得る。 メモリ技術は長年にわたって進歩してきたが、トランジスタと電力密度の課題によりシングルコアCPUのパフォーマンス向上が大幅に鈍化したのと同様に、メモリ密度の向上も鈍化している。より多くのメモリチップを並列に配置することで帯域幅を拡張できるが、PCB上の物理的なスペースは有限であり、密度と帯域幅は相互に関連する制約となる。高度に並列化されたワークロードは、対応する大量のメモリと、データをやり取りするための帯域幅を必要とする。ほとんどのコンピュータは、半世紀前に誕生して以来、何度か改訂されてきたDRAMと呼ばれるメモリ技術を使用している。最新の改訂版であるDDR5は、チャネルあたり50GB/秒以上を実現しており、DDR4の約2倍の速度である。これらは大きな進歩だが、GPUの性能が向上するにつれて、大規模言語モデルのワークロードはメモリにさらに多くのものを要求するようになった。計算能力が利用可能なメモリ技術を追い越すこの現象は、しばしば「メモリの壁」と呼ばれる。 このメモリの壁を打破するのに役立つ技術の一つが、HBM(高帯域幅メモリ)だ。2010年代半ばに初めて登場したHBMは、メモリチップを垂直に積み重ねることで、より高いビット密度と帯域幅を実現する。各階に専用エレベーターを備えた高層マンションを想像してみよう。これにより、より多くの人が同時に建物に出入りできる。シリコンを積み重ね、これらの「エレベーター」を織り込む複雑な工程のため、HBMは標準的なDRAMと比較して製造工程における歩留まりと効率に課題を抱えている。この課題に加え、ハイパースケーラーからの需要も、今日見られるメモリ不足の一因となっている。GPUサプライチェーンで始まった衝撃波は、メモリにまで及んでいる。 CPUは中心的な役割を担わなくなる 今日のメモリ制約が顕在化するずっと前から、ハードウェアとソフトウェアのアーキテクチャは、スループットの向上と専門化の進展に対応するために再編成されてきた。過去10年間、シングルコアCPUの性能はほぼ停滞している一方で、ネットワーク帯域幅は桁違いに増加した。現代のオフィスネットワークは一般的にギガビットイーサネットで配線されており、ほとんどのデータセンターは現在100GbEをはるかに超える速度で稼働している。わずか2年前の2024年には、800GbEが標準化された。これは、4K映画1本を1秒未満で転送できる十分な帯域幅であり、単一のCPUコアが処理できる速度をはるかに上回る。 GPU間通信の需要の高まりが、高帯域幅化の傾向を加速させている。今日の大規模言語モデルは非常に大きいため、単一のGPUでは実行できない。GPUのクラスタ全体が必要となる。データ量が単一のCPUで効率的に処理できる量を超えて増加するにつれ、最新のシステムはCPUをデータパスから切り離すことで対応してきた。高速トラフィックをCPU経由でルーティングするのではなく、専用のシリコンがデータ転送を直接処理し、CPUはオーケストレーションの役割を担い、操作の設定、ポリシーの適用、例外処理を行う。NVLinkなどの専用インターリンクはこの変化を象徴するものであり、CPUを経由せずにGPU同士を直接接続することで、低ジッター、低遅延、高帯域幅を実現する。 このパターンはGPUに限ったものではない。イーサネットNIC(ネットワークインターフェースカード)は、高度なコンピューティングデバイスへと進化し、VXLANなどの最新のカプセル化プロトコルからRDMA(リモートダイレクトメモリアクセス)などの技術まで、あらゆる処理をCPUからオフロードできるようになった。従来、NICはネットワークパケットをオペレーティングシステムのネットワークスタックに渡し、ネットワークスタックがペイロードを抽出してユーザー空間プロセスに転送して処理を行っていた。しかし今日では、ネットワークトラフィックが高速すぎて、CPUによる効率的な処理が困難になっている。現代のデータセンターの基準からすると比較的平凡な100Gbpsでも、単一のCPUコアでネットワークを飽和させることはほぼ不可能である。ネットワークが高速すぎるため、CPUは利用可能な帯域幅を十分に活用できるほど十分なバイト数を迅速に送信することができない。このCPUボトルネックを回避するため、NICはCPUの関与なしに、RAMから直接データを読み込み、処理し、ネットワーク経由で送信する。 同様に、ストレージの世界では、NVMe SSDはCPUを介してデータを処理する代わりに、PCIeバス上でDMA(ダイレクトメモリアクセス)を利用してシステムメモリとの間でデータを転送する。これは、レストランの荷降ろし場からパントリーまで専用の独立した経路を作り、厨房の料理人が食材を運ぶ手間を省くことと同じようなものだ。ストレージとネットワークの両方におけるこれらの進歩により、NVMe-oF(NVMe-over-Fabrics)などの技術を利用して、ネットワーク経由でリモートのNVMeドライブにほぼローカル接続と同等の速度でアクセスできるようになった。これらの技術は、最新の高度なNICを活用して高帯域幅のデータ転送をCPUからオフロードする。 ワークロードパターンの変化に伴うこうした技術的変化は、ハードウェアだけにとどまらない。大規模な需要ショックはシステムが動作する環境そのものを再構築し、ソフトウェアアーキテクチャはシステムが動作するハードウェアと一緒になって処理を行う。 物理とソフトウェアが出会う場所 データベースは、コンピューティング、メモリ、ストレージ、ネットワークの交点に位置し、これらのダイナミクスを非常に明確に把握できる。データベースは性能の技術的な限界で動作し、レイテンシとスループット、一貫性と可用性など、様々なトレードオフを強いられる。 Cassandraなどの有名なデータベースで採用されているシェアードナッシング(Shared Nothing)アーキテクチャは、リソースの完全な分離を重視し、各ノードが独自のCPU、メモリ、ストレージを備えている。他のノードへの依存関係はないが、ワークロードが一定でない場合、ハードウェアリソースがアイドル状態になる可能性があることを意味する。CPU、メモリ、ストレージの比率は動的に変更できないため、ハードウェア利用率を最大化するには、比較的安定したワークロードパターンが必要となる。イベントログ用のデータベースは、大容量のストレージを必要とするが、読み取りクエリは非常に少ないため、計算要件は低くなる。eコマースストアで使用される製品データベースは、買い物客からの読み取りクエリが多数発生する可能性があるが、ストレージ容量は比較的少なく、販売される製品数に応じてのみ拡張される。言い換えれば、シェアードナッシングアーキテクチャでは、リソース比率のミスマッチがアイドル状態のシリコンへの支払いやパフォーマンスのボトルネックに直結する可能性がある。 対照的に、シェアードストレージアーキテクチャは、データストレージ層をCPUやメモリなどのコンピューティングリソースから分離する。この概念は、GoogleがBigQueryで初期に取り組んだことに端を発している。これにより、CPUとメモリのリソースをストレージ要件とは独立して拡張できるため、特にクラウド環境において、より高いリソース利用率を実現できる。2025年にDatabricksに買収されたデータベース企業Neonは、コンピューティングとストレージの分離を可能にするために、Postgresストレージ層の大部分を再設計した。これにより、データ量とは独立して負荷に応じてリソースをスケールアップおよびスケールダウンできるようになり、常時専用リソースを必要としない一種の「サーバーレス」データベースが実現した。 ElasticsearchやClickHouseのようなシステムもこれと似たシェアードストレージアーキテクチャを採用しており、Amazon S3などのサービスからの安価で耐久性の高いオブジェクトストレージの可用性によって実現されることが多い。これらのストレージサービスは、読み取り後書き込み整合性や条件付き書き込みのようなプリミティブを提供することで、同時読み取りと書き込みを処理できる分散した一貫性のあるストレージを提供するという難問を解決に貢献する。Apache IcebergやDelta LakeといったオープンなLakehouseフォーマットは、この変化を象徴するものであり、コモディティ化された共有ストレージの上に、より高レベルのデータセマンティクスが重ねられている。 ベクトル埋め込みを利用したAI関連サービスの需要が急増したことで、ベクトル検索を中心とした新しいデータベース開発が急増し、この分離が主流になりつつある。現在の最先端のベクトル検索アルゴリズムには、複雑なチューニングなしで動的な挿入とリコール性能のバランスが取れていることから多くのデータベースで採用されているHNSW(階層型ナビゲーション可能なスモールワールド)のバリエーションが含まれている。しかし、これらのグラフベースのアルゴリズムは、メモリ内のデータ構造を走査する必要があるため、メモリをかなり消費し、データベースの計算コストを増大させる。ストレージ容量とは独立して計算リソースを動的にスケールアップおよびスケールダウンできるアーキテクチャを採用することで、データベースは限られたハードウェアリソースの利用率を高めることができる。 ワークロードパターンが変化し、ハードウェアが進化するにつれて、データベースアーキテクチャは、そのアーキテクチャに内在するトレードオフを浮き彫りにせざるを得なくなった。今日の状況では、コンピューティング、メモリ、ストレージ、ネットワークという4つの柱をどれだけ密接に結合するかという決定は、パフォーマンス特性だけでなく、ハードウェアの利用率、コスト、スケーラビリティにも影響する。データベースはシステムの物理的な限界に近い位置にあるが、その根底にあるダイナミクスはデータベース特有のものではない。ソフトウェアが強い制約の下で動作する場合、アーキテクチャの選択は技術的な成果だけでなく、経済的な実現可能性にも影響を与え始める。ここに明確な答えはない。どのアーキテクチャも、どの制約が最も強く影響するかという賭けを内包している。それを誤ると、もはや存在しない世界に合わせて最適化したことになる。 圧力下における抽象化 抽象化は無料ではない。それらが生み出された環境の前提条件に従って価格付けられている。数十年にわたり、ハードウェアの普及が進んだことで、そのコストは容易に無視されてきた。しかし、物理的な制約が強まるにつれて、抽象化に内在する隠れたコストが再び明らかになりつつある。 抽象化の中には、実質的にコストがかからないものもある。例えば、スマートポインタやジェネリクスなどが挙げられる。これらのメカニズムは、実行時のパフォーマンスを損なうことなく、開発者の認知負荷を軽減する。一方、コストがかかるものもある。ガベージコレクションは、実行時に一時停止が発生する代わりに、開発者が変数のライフタイムを管理する必要をなくす。仮想関数は、パフォーマンスの低下と引き換えに、実行時の動的なディスパッチを可能にする。組み込みシステムや高性能コンピューティングでは、C、C++、Rustといった言語が広く使われている。これらの言語は、開発の複雑さや認知負荷を犠牲にして、実行時の動作を制御可能にする。これは、すべてのバイトとすべてのサイクルに目に見えるコストがかかる状況では、合理的な選択と言える。 現代において最も重要なソフトウェアの一つであるLinuxカーネルは、ユーザー空間のプログラムがハードウェアの詳細を気にしなくて済むようにしてきた。そのシステムコールはハードドライブやNICに関する詳細を抽象化し、メモリ、ファイル、ネットワークのようなプリミティブを扱うための安定したインターフェースを公開する。カーネルはドライバーを通じてハードウェアを直接管理・通信し、最終的にそれらを抽象化された汎用リソースとしてユーザー空間に提供する。これらは抽象化だけでなく、関心の分離も提供する——カーネルはハードウェアリソースが適切に管理・保護されることを保証し、ユーザー空間はユースケースにより集中する。 ハードウェアの限界で動作する場合、これらの抽象化のパフォーマンスオーバーヘッドは、利点を上回り始める可能性がある。ネットワーキングとストレージ技術がCPU性能に対して大幅に進歩するにつれ、増大する需要に追いつくソフトウェアの能力が重要になる。メモリのコピーやコンテキストスイッチは、毎秒数百万回繰り返されると、その負荷が蓄積される。LLMベースのサービスに対する現在の需要は、こうしたプレッシャーを大幅に加速させている。その衝撃波は、ハードウェアの不均一な進歩によって既に負荷がかかっていた抽象化全体に波及し、もはや無視できないコストを露呈させている。 例えば、数十ギガバイトものリアルタイム高解像度ビデオをネットワーク経由でSSD搭載のリモートストレージデバイスに送信するアプリケーションを考えてみよう。データは、ビデオ送信アプリケーションのユーザー空間メモリからカーネル、NIC、光ファイバーケーブルを経由して受信側のNIC、受信側のカーネル、ユーザー空間、そして再びカーネルへとコピーされ、最後にSSDに書き込まれる。各ステップでデータは再配置され、時には仮想的なデジタルエンベロープにカプセル化され、受信側で取り出される。コンピュータからより多くのパフォーマンスを求めるにつれ、この仮想的な書類のやり取りのオーバーヘッドが明らかになる。 このシャッフルは、カーネルとユーザー空間の抽象化レイヤーの結果だ。データはカーネルを介して出入りする。2010年代後半、データセンターのネットワークは100Gbpsを超え、この抽象化を回避する「近道」を見つけたいという要望が高まった。DPDK(Data Plane Development Kit)は、カーネルの抽象化を完全にバイパスし、カーネルを介さずにユーザー空間アプリケーションがNICと直接通信できるようにする業界標準フレームワークである。ネットワークスタックの責任をアプリケーションに移し、カーネルのセキュリティ保証と多重化機能の一部を犠牲にする代わりに、処理速度を向上させる。メモリコピーとコンテキストスイッチを削減することで、DPDKは、高帯域幅でパフォーマンスを重視するアプリケーションが、カーネルを経由する場合と比較して2~3倍の効率を実現できるようにしている。DPDKにはトレードオフがあり、万能の解決策ではないが、機会費用の方程式が変化するにつれ、業界がこれらの抽象化の境界を再構築するために動いてきた多くの方法の一つだ。 環境の変化、技術の進歩、ワークロードの変化に伴い、抽象化の概念を再検討する必要が生じている。コスト、パフォーマンス、経済性に関する暗黙の前提は、プレッシャーがかかった時に明確になる。これらのマクロな動きに気づかないか、適応を拒むことは、隠れた負債の一形態を積み重ねる——システムとプロダクトは健全に見えるが、負荷がかかると破綻してしまうのである。 ハードウェアへの意識 2025年以降にリリースされたデータセンター向けGPUには、数年前には存在しなかった4ビットおよび8ビット浮動小数点表現であるFP4とFP8での動作に特化したモジュールがシリコンに組み込まれている。これらの超低解像度数値表現は、LLMのメモリ効率と演算効率を向上させるために開発されたものであり、トランジスタレベルのハードウェアに組み込まれているという事実は、新たなワークロードとハードウェア設計の密接な連携を反映している。ソフトウェア要件に関する想定は、ハードウェアが急速に変化するワークロードパターンに適応するにつれて、短期間でシリコンに組み込まれていく。 同様に、CUDAエコシステムにおける専用ライブラリであるFlashAttention-3は、最新のNvidiaアーキテクチャで導入されたLLM最適化ハードウェアプリミティブを活用することで、トランスフォーマーベースの学習と推論を高速化している。これは、動作対象ハードウェアに関する深い知識に基づいて記述されたソフトウェアであり、メモリ操作と計算処理を並列化します。GPUメーカーとソフトウェアエコシステム間の共同設計のペースは、この分野におけるハードウェアとソフトウェア開発の緊密な統合を示している。かつて数十年を要したソフトウェアのニーズと専用シリコンの間のフィードバックループは、今や数年に短縮されている。 物理的な制約が強い場合、ハードウェアへの意識は最適化のための手段から戦略的な手段へと変化する。しかし、パフォーマンスがハードウェア固有の機能の活用にますます依存するようになると、それらの機能を制御する者が優位に立つことになる。共同設計は価値を生み出すが、同時に依存関係も生み出す。パフォーマンスへの道がベンダー固有のシリコンを通る場合、抽象化レイヤーを誰が制御するかという問題は、市場支配力の問題となる。 半導体ベンダーは、顧客を自社ハードウェアに囲い込む強い動機を持っている。ベンダー固有の機能を使用するかどうかというアーキテクチャ上の選択はすべてが賭けであり、今日のパフォーマンスを犠牲にして将来の柔軟性を確保するか、パフォーマンスを犠牲にして選択の自由を維持するかの選択となる。抽象化は最小公倍数効果に直面し、すべてのハードウェアプラットフォームに共通する機能はコモディティ化されたものだけとなる。限られたハードウェアリソースからパフォーマンスを引き出すには、最新かつ最高のハードウェア機能を活用するための抜け道が必要となる。これらの機能は、移植可能な抽象化の外、ベンダー固有の扉の向こう側に存在する。 機械学習の分野では、XLA、TVM、Tritonがこの緊張関係を典型的に表している。これらのコンパイラ技術は、高レベルモデルをハードウェア固有の実行可能コードに変換することで、PyTorchなどのフレームワークが比較的ハードウェアに依存しない状態を維持し、多様化するハードウェア環境において選択肢を確保できるようにしている。Googleは、LLMが主流になる何年も前に導入した自社開発のTPU(テンソル・プロセッシング・ユニット)と呼ばれるチップ上でLLMを実行するために、XLAに多額の投資を行ってきた。彼らはシリコンとコンパイラの両方を構築した。抽象化レイヤー自体が、独自のハードウェアを活用してパフォーマンスを向上させつつ、ソフトウェアスタックを外部のチップサプライヤーへの依存から切り離す戦略的な資産となった。 しかし、制約条件下でパフォーマンスを最大化するためにハードウェアの認識が重要であるにもかかわらず、その影響は一様ではない。共同設計には、エンジニアリング作業、人的資本、そして複数の技術分野にわたる長期的なメンテナンスにおいて、多大なコストがかかる。このコストを償却できるのは、これらのアーキテクチャ上の選択が持続的にサポートできる場合に限られる。結果として、ハードウェア認識を最大限に活用できるのは、規模、資本、そして長期的な計画期間が既に存在する分野に集中する傾向がある。そして、この分野で最も有利な立場にあるのは、パブリッククラウドプロバイダーである。 豊かさの後のクラウド クラウドコンピューティングは、大規模な運用には物理インフラの所有が不可欠だった時代に登場した。導入にはサーバー、ネットワーク、電力契約などに数百万ドルもの費用がかかり、キャパシティプランニングには多額の資本が必要だった。また、企業はリソースの利用率が低いリスクを負わなければならなかった。 クラウドコンピューティングの主要な価値提案の一つは、計算リソースを豊富かつ代替可能に見せることだった。弾力性はクラウド利用者にとって当然の前提となり、キャパシティプランニングを後回しにできるほどになった。開発者はピーク需要に対応するためにリソースを準備する必要がなくなり、クラウドプロバイダーが提供する事実上無限のサーバー群から必要なリソースを引き出し、使用した分だけ料金を支払うだけで済むようになった。 このモデルは、必要な時にインフラが十分に利用可能であること、価格が予測可能であること、そして規模の経済性を活かせるプロバイダーが遊休設備の利用リスクを吸収するという暗黙の前提に基づいていた。規制はなかったものの、数十年にわたる継続的なハードウェア改良による豊富な供給量と、ハイパースケールクラウドプロバイダー間の競争が相まって、これらの前提は概ね維持されてきた。 今日のGPU、メモリ、電力といった物理的な制約のある世界では、その均衡は崩れつつある。弾力性はもはや無条件ではない。供給は迅速に対応できず、需要は衰える兆しを見せない。供給と需要の両方が非弾力的になると、供給量と価格は想定される環境条件ではなく、不確実性の要因となる。 クラウドプロバイダーはこれまで概ね製品の価格改定を避けてきたが、実際には使用量に応じた課金ではなく、割り当て量に応じた課金を行っている。容量へのアクセスは、クラウドプロバイダーとの事前契約、最低利用額契約、長期契約によって、いわば順番待ちの列に並び続けることがますます重要になっている。柔軟性は、可用性と商用契約に左右されるようになった。 物理的な制約がある状況下では、クラウドプロバイダーはインフラサービスプロバイダーというよりも、むしろ資本配分者に近い存在と言える。その規模の大きさによって、希少なリソースへの優先的なアクセスと価格設定が可能となり、さらにリソースを自社で開発する選択肢も得られる。マクロレベルでは、リソースの調達を一元化し、下流のクラウド顧客にアクセスを再分配すると同時に、大規模なユーザーベース全体でリスクを分散させる。 クラウドコンピューティングは、システムから設備投資をなくしたわけではない。むしろ、設備投資を集中させたのだ。リソースが豊富にある時代には、その集中はほとんど目立たなかったが、リソースが不足する時代には、それが顕在化する。クラウド自体は依然として存在するが、その役割は変化した。物理的な制約に対する仮想的な緩衝材から、そうした制約の中でリソースを割り当てるためのメカニズムへと変化したのである。 現実の厳密さ クラウドが物理的限界に対する緩衝材として機能しなくなるにつれ、それらの限界はソフトウェアシステム自体の中に再現される。デナードのスケーリングが鈍化するにつれて、複雑さがハードウェアからソフトウェアへと移行したのと同様に、今度はリソース不足の問題がクラウドインフラストラクチャからより高レベルのソフトウェアへと押し上げられていく。何を抽象化し、何を構築し、何を最適化するかという決定は、コスト構造、利益率、そして競争優位性を直接的に左右する。これらの決定をもはや後回しにはできない。 これは理論上の話ではない。電力、メモリ、コンピューティングといった分野では、需要が供給をますます上回っており、既にプレッシャーがかかっている。リソースの競合はもはや一時的な異常事態ではなく、エンジニアは日々の開発作業において、割り当てやレート制限に直面することが増えている。これらは、意図的なアーキテクチャ上の決定を必要とするトレードオフを迫られる、不均衡のシグナルだ。 すべての製品やビジネスがこうした限界で運用されているわけではない。多くの企業は、収益に対するコンピューティングコストが安価で、パフォーマンスが許容範囲内であり、規模が物理的な制約よりも人やプロセスによって左右されるような環境で事業を展開している。このような状況では、抽象化に注力し、より上位のボトルネックに焦点を当てることが合理的な判断となる。しかし、こうした状況は常に変化する。ワークロードが進化するにつれて、かつては当然と思われた前提が崩れることがある。アーキテクチャ上の決定は、将来の顧客需要、技術進歩、そして構造的な優位性がどこから生まれると予想されるかといった点に対する、一種のコミットメント、つまり賭けなのである。 アーキテクチャ設計は、物理的な制約と組織の意図とのインターフェースだ。基本原理から考えることで、制約について論理的に考察し、理想ではなく現実を直視したシステムを設計することができる。アーキテクチャをエンジニアリング部門に委任する技術的な決定事項として扱う組織は、暗黙のうちに、現在の抽象化を形作った環境が今後も存続するという賭けをしていることになる。すべてのシステムには、それが構築された世界に関する前提が組み込まれている。重要なのは、世界が変化する前に、それらの前提を検証することである。
皆さんこんにちは、Product Designerの保川です。春らしい季節になってきましたね。 今日はプロダクト開発を進めるにあたり、私たちが顧客をどのように捉え、インサイトを得ているのかお話ししたいと思います。 B2Bの顧客理解はなぜ難しいのか? 決める上司と使う部下 組織の構造に埋もれがちなユーザーペイン 業務には沢山のしがらみと感情がある 「点」ではなく「面」で捉える難しさと醍醐味 「生産構造×業種」で見えてくる調達業務の奥深さ 見積は手段、本当に解決したい「手前の悩み」 ペルソナは必ずしも正解ではない アーキタイプとは 誰か、よりどう動くか 最強のリソースは社内から 現場で決めて、動く 他者のレンズを通して顧客理解を深める デザイナーは「想い」をつなぐ架け橋 B2Bの顧客理解はなぜ難しいのか? B2Bプロダクト業界においてよく言われるのが、B2Cと比べて「ユーザーのインサイトが取りにくい」ということです。インタビューのお返しにギフトをわたすこともできないし、Eメールキャンペーンも通用しません。でもそれだけではないです。 決める上司と使う部下 B2Bはなぜユーザーインサイトを得ることが難しいのか。それは、意思決定者(購買者)とユーザーは別物である、ということが一つ大きな要因だと考えています。 例えば、プロダクトを購入する意思決定者はマネージャー以上、もしくは小規模な会社であればCEOであったりします。しかし、実際のユーザーは一般社員の場合も多い。この両者は、プロダクトを全く異なる目線で見ています。 意思決定者は、投資や説明責任、コスト削減の視点からプロダクトを評価します。対して、ユーザーは、日々の仕事の成果やミスの有無、効率の観点からプロダクトを見ます。この視点の違いこそが、プロダクトが現場で「詰む」原因になります。 組織の構造に埋もれがちなユーザーペイン ユーザーは意思決定者から、「会社の意向だからこれ(プロダクト)を使って欲しい。」といきなり言われるかもしれません。ユーザーのペインは会社の構造や政治によってうまく意思決定者に伝わらないかもしれません。そして、プロダクトのユーザーの思いは意思決定者が主となる会議などの表舞台には一切出てこないかもしれません。 そうなると、プロダクトをユーザーが普段どのように使い、何に使いにくいと感じているのか、そしてどんな潜在ニーズがあるのか。そんな声は当然埋もれてしまいます。 立場やバイアスというノイズが介在するため、ユーザーと同じ景色(ビュー)を見ることは想像以上に困難である 業務には沢山のしがらみと感情がある また、業務において、ユーザーは様々なしがらみにとらわれています。例えば、彼らは単にプロダクトを使って行う仕事だけをしているわけではないということ。まず、プロダクトを使う仕事がそもそもメインの仕事なのか?例えば調達業務であれば、1日3件調達する人と、50件調達する人では、工程や効率への要求が大きく異なります。また、1件において何品目扱うかによって、まとめて編集したいのか、保存が大事なのかなども変わってきます。 さらに、ユーザーの仕事にまつわる上司や同僚からの期待、プレッシャー、など様々な感情的なタスクもこなしている可能性があります。そして、それはユーザー自体が気づいていない部分でもあったりするのです。 「点」ではなく「面」で捉える難しさと醍醐味 実際のユーザーを理解するには、彼らだけでなく全体を捉えることも重要です。誰が、どんな思いをもち、どんな関係で仕事をしていて、どんな仕事の一環でプロダクトを使ってくれているのか。そして彼らはどういう人達と協働しているのか。また、意思決定者とはどんな関係性にあるのか。(ここまでやるのは本当に大事、ですが、難しい!) だからこそ、B2Bのユーザーインサイトを得ることは難しく、とてもチャレンジングです。けれども、それこそが醍醐味です。 ユーザーの声が埋もれがちだからこそ、Product Designerはユーザーインサイトを様々な角度から取りに行く必要があるし、そうしなければ、本当にユーザー目線のプロダクトを作ることができなくなってしまう、重要な役目を果たしています。 「生産構造×業種」で見えてくる調達業務の奥深さ 私はキャディに入社したときに、「調達業務」とは何か?すら全く知らない状況でした。そこで、手当たり次第に製造業の動画をあさっていたところ、同僚が社内に調達経験がある人が複数いることを教えてくれました。 私は、早速アポをとり、調達の業務についてインタビューを6人のユーザーに行いました。そこで、同じ調達といっても量産と少量多品種によって大きく彼らの仕事内容や業務の見方が異なることがわかりました。 見積は手段、本当に解決したい「手前の悩み」 例えば、量産においては「見積業務は判断材料に使える材料」「なぜこの価格なのかの説明責任を果たすもの」であったりするのに対して、少量多品種においては、「下流の流れをとにかく止めないために調整するもの」であったり。当初の私は、「調達の人って見積依頼をかけ、条件が変わったら再見積を行い、価格の合意をするのだろう。」という漠然とした理解がありましたが、インタビューを通して、調達担当の方が本当に解決したいのは、見積を取ることではなくて、もっと手前の状況を整えることだということを理解しました。 そして、最も興味深かったのは、それが生産構造だけでなく、業種においても大きく異なるということです。例えば、プラントを作るくらいの規模の調達と、治具の調達とでは、規模も、個数も異なるため、気にする部分も全く違う。だから、調達のバラエティは生産構造×業種分あるとも言えます。 ペルソナは必ずしも正解ではない アーキタイプとは 一般的に、ユーザー理解といえば、ユーザーインサイトから具体化したユーザー像(ペルソナ)を作ることが推奨されますが、特にキャディのプロダクトにおいては必ずしもその必要はないと考えています。先に述べた理由が大きいですが、業種や製造形態による違いで、課題も意思決定構造も大きく変わるため、「XX歳、製造現場出身調達」のようなペルソナは機能しにくいです。 代わりに、役割や製造構造に応じた一定のユーザーグループであるアーキタイプの方が相性がよいと考えています。 アーキタイプとはペルソナよりも広い概念であり、「行動パターン、動機、目標」にフォーカスを当てたユーザー分類です。 誰か、よりどう動くか B2B、特に生産構造×業種の数だけ変数のある製造業においては、具体的な「個人(Who)」を描写するよりも、「どういう状況で、なぜ動くのか(Why,How)」という行動原理で捉えたほうが、ユーザーへの解像度は圧倒的に高まります。 私たちのプロダクトには、具現化したユーザー像を満足させる以上のプロダクトの価値があると考えています。だからこそ、この複雑なアーキタイプを深く理解し、それぞれの構造に最適化された価値を届けられるよう、今以上に現場の解像度をとことん上げる必要があります。 製造構造と業種の数だけ解像度を上げるなんて、そんな簡単な話じゃないんじゃないの?と思われるかもしれません。でも、キャディなら可能だと思っています。それは、社内リソースが強力だからです。 最強のリソースは社内から キャディには祖業であるManufacturing事業を含め、何百人の顧客と関わったプロフェッショナルたちが多くおり、彼らから学ぶことがものすごく多いです。顧客と関わるSales、CSの一人ひとりの頭の中には解像度の高い顧客(意思決定者並びにユーザー)がいて、彼らのレンズを通して顧客を理解することが最も近道であり、有力な情報収集になると考えています。 現場で決めて、動く これこそがキャディの強みだと私が考えていることの一つは、ビジネスサイドからプロダクトフェーズまで、一丸の意識が強い、ということです。例えば、直接CSとコンタクトをとって顧客訪問に同席をお願いさせてもらったり、定例会議に参加させてもらったり、ということが現場レベルでできます。CSの言葉を借りて言えば「こういう現場レベルで何をやる・やらないの判断ができるのは強い。」のです。なぜなら、個々の主体性と責任で物事を推進しているからです。 これこそまさに、キャディがスピード感と一体感を持って仕事ができる現場である所以なのかもしれません。 他者のレンズを通して顧客理解を深める また、先人たちのお陰で顧客のニーズが定性データとして共有される仕組みも社内では整っています。こういった取り組みは、プロダクトの末端にいるユーザーのインサイトを様々なステークホルダーを経由して、異なる角度から得られる、大事なインサイトです。 こういった断片的な情報の塊を集めてユーザーのインサイトはより分厚いものになっていくのだと思います。 また、キャディには「1日検証会」というものがあり、そこでは潜在的なユーザーに実際にプロダクトを触ってもらう、という機会もあります。こういった機会を通して口頭ではわからないユーザーの行動パターンや、ITリテラシー、宝の山となるインサイトを得ることもできました。 デザイナーは「想い」をつなぐ架け橋 Product Designerの役割は、インサイトをつなぎ、構造として再解釈することだと考えています。ユーザーは一人ひとり異なる存在であり、その背景にはパーソナリティや経験など多くの要因があります。 だからこそ、n=1の事象をそのまま扱うのではなく、抽象化しながら意味を見出していく必要があります。 また、私は社内の人たちの仕事を理解することもProduct Designerとしては重要な部分であると思っています。キャディのメンバーを知れば知るほど、プロダクトに熱く、顧客を大切にしている人たちなのだと感じます。そして、それ以上に、ビジネス文脈を含んだユーザーのニーズが色濃く見えてきます。 明確な正解があるわけではありません。だからこそ、プロダクトに関わる一人ひとりの想いや情報を取り続けながら、仮説と検証を繰り返していく。そのプロセス自体が、この仕事の難しさであり、面白さだと感じています。
Release notes used to be one of those tasks everyone agreed was important, but nobody really owned. We’d ship a release, someone would scramble to collect changes from developers, and eventually a Markdown file or sometimes an Excel sheet would appear usually incomplete, sometimes outdated, and almost always inconsistent in tone and structure. A few months ago, I got tired of that loop and built an automated release note generation system. It’s now part of our release workflow, and more importantly, it gave QA a much more reliable way to verify changes. The Problem The Idea The Solution How It Works 1. Release branch creation 2. Tag generation 3. Slack trigger 4. Devin AI runs the playbook 5. Version diff analysis 6. Release note generation 7. Slack delivery The Prompt We Use Impact QA got a reliable way to verify changes Better visibility for stakeholders Consistency across releases Reduced ambiguity Lessons Learned 1. Version comparison is the foundation 2. Risk analysis adds real value 3. Fully automated doesn’t mean zero oversight Closing Thoughts The Problem Before automation, release notes were assembled manually. The process looked roughly like this: Developers would drop notes in Slack, PR descriptions, or sometimes directly into Excel Someone (usually from QA or a PM) would try to consolidate them Important changes were sometimes missed Technical changes were either too vague or too detailed There was no consistent structure across releases The biggest issue wasn’t just the time it was lack of reliability. QA engineers often had to double-check commits anyway because: Some changes weren’t documented The impact of changes wasn’t clear Dependencies between components weren’t not visible So instead of helping QA, release notes became something they couldn’t fully trust. The Idea Instead of asking humans to summarize changes after the fact, we shifted the source of truth: Let the system generate release notes directly from version differences. That meant: Comparing actual code changes between releases Using commit history and PR data as inputs Letting AI handle summarization and structuring This removed the dependency on manual input and made the process deterministic. The Solution We built an automated pipeline that generates release notes by comparing: The current release branch/tag The previous release tag From that diff, the system collects: Commits Pull requests Code changes PR descriptions and comments Then an AI agent (Devin) processes that data and generates: Structured release notes Categorized changes (features, fixes, etc.) Risk analysis for QA Output in both English and Japanese Everything is posted back into Slack as a .md file. How It Works Here’s the workflow we run in production. 1. Release branch creation We follow a simple convention: release/{version} This becomes the anchor point for the release. 2. Tag generation A release tag (e.g., v2.3.0) is created. This allows us to: - Identify the current version - Locate the previous version tag - Establish a clear comparison range 3. Slack trigger Once the release tag is created, a GitHub Action sends a request to a Slack webhook that’s wired into a Slack workflow. That workflow posts a message in Slack and tags @devin with the playbook macro, which then kicks off the automation. Additionally, we kept a manual fallback anyone can trigger the same process by tagging @devin with the playbook macro directly in Slack. This is useful in cases where we want to regenerate notes or run it outside the standard release flow. This setup keeps everything inside the team’s existing workflow while removing any manual steps no one needs to remember to run scripts or trigger jobs separately. @Devin !generate_bilingual_release_notes !deep ========================= Repository : example-repo Release branch : release/v17.3 ========================= 4. Devin AI runs the playbook Devin executes a predefined prompt that drives the entire process. It: - Parses version information - Finds the previous release - Analyzes commits, PRs, and diffs - Combines intent (PR descriptions) with actual code changes - Generates structured, bilingual release notes - Performs risk and dependency analysis 5. Version diff analysis This is the core step. The system: Lists all commits between two versions Maps commits to PRs Extracts relevant file changes Identifies impacted modules This ensures the output is based on what actually changed not what someone remembered. 6. Release note generation The output is a structured Markdown file with: Summary Features / Enhancements / Bug Fixes / Technical Improvements PR list Risk & dependency analysis It also includes: English version Japanese version (collapsible section) 7. Slack delivery The generated .md file is posted back into the same Slack thread. This keeps the entire release conversation in one place and easy to track. The Prompt We Use This is the core prompt that powers the system. It’s doing most of the orchestration work: # TASK: Generate Bilingual Release Notes File with Risk Analysis ## 1. ROLE & GOAL You are an expert technical writer and AI development agent with the ability to read and understand code. Your goal is to autonomously generate comprehensive **bilingual (English and Japanese)** release notes, complete with a detailed **risk and dependency analysis** for the testing team, and output the result as a complete Markdown file. ## 2. CONTEXT & ASSUMPTIONS - **Input:** You will be given the repository name and the current release branch name. - **Language Output:** English and Japanese. - **Final Output:** A single Markdown ( ` .md ` ) file. - **Audiences:** - **Primary (main body):** Product managers and stakeholders. - **Secondary (final section):** The Testing Team. - **Project Structure Assumptions:** - Release branches follow the pattern: ` release/{version} ` . - Releases are marked with git tags that follow semantic versioning (e.g., ` v2.0.0 ` or ` 2.0.0 ` ). ## 3. INSTRUCTIONS Follow these steps precisely: 1. **Initial Analysis:** - The **Project Name** is the ` REPOSITORY_NAME ` provided as input. - The **Current Release Branch** is the ` RELEASE_BRANCH_NAME ` provided as input. - verify above name is actually match with repository name as project name before writing to output file. 2. **Determine Versions:** - From the Current Release Branch name, parse the **Current Version**. - Scan the repository's git tags to find the **Previous Version** tag immediately preceding the Current Version. This is your comparison starting point. 3. **Synthesize Change Information (Crucial Step):** - To build a complete understanding of each change, analyze three sources between the Previous Version and the Current Release Branch: i. **Commit Messages & PR Descriptions:** To understand the developer's *intent*. ii. **Actual Code Diffs:** To understand what was *actually* implemented. iii. **Synthesis:** Combine intent with ground truth. While synthesizing, pay close attention to inter-module dependencies and the potential blast radius of changes to inform the risk assessment. 4. **Collate Pull Requests:** - Gather a complete list of all Pull Request titles and numbers that were merged between the **Previous Version** and the **Current Release Branch**. This raw list is for inclusion in its own section. 5. **Categorize and Rewrite for Stakeholders:** - Group the synthesized changes from Step 3 into the primary categories: ` New Features ` , ` Enhancements ` , ` Bug Fixes ` , and ` Technical Improvements ` . - Rewrite each item for the product/stakeholder audience, focusing on user benefits. **You must generate this content in both English and Japanese.** 6. **Perform Risk and Dependency Analysis for Testing Team:** - For each major impacted area identified in your synthesis, perform the following detailed analysis: - **Risk Level:** Assign a risk level: `High`, `Medium`, or `Low`. Base this on the change's complexity, its centrality to the application (e.g., changes to authentication are `High` risk), and the potential for unintended side effects. - **Components and Dependencies:** Clearly state the primary component that was changed. Then, list other key components or features that depend on it. - **You must generate this entire analysis in both English and Japanese.** 7. **Assemble the Final File:** - Format all generated bilingual content into a single Markdown file named ` RELEASE_NOTES_{Identified_Version}.md ` . This file is your final output. - Set the release date to today's date: **August 28, 2025**. ## 4. OUTPUT FORMAT Produce a single Markdown file named ` RELEASE_NOTES_{Identified_Version}.md ` . The content must strictly follow this bilingual template, with the Japanese section nested inside a collapsible ` <details> ` tag. # Release Notes: {Project _ Name} v{Current _ Version} **Generate Date:** August 28, 2025 ## Summary A brief, one-to-two-sentence overview of the most important changes included in this release version. --- ## ✨ New Features - [A clear, benefit-oriented description of the first new feature.] - [Description of the second new feature.] ## 🚀 Enhancements - [Description of an enhancement to an existing feature.] - [Description of a performance or UI improvement.] ## 🐛 Bug Fixes - [A simple explanation of the bug that was fixed, e.g., "Fixed an issue where reports would fail to generate for users in the 'Guest' role."] - [Another bug fix description.] ## 🛠️ Technical Improvements - [A high-level summary of a technical or backend change, e.g., "Upgraded the database engine to improve overall application performance and security."] - [Another technical improvement summary.] --- ## 📦 Included Pull Requests A complete list of Pull Requests included in this release. - [PR #{number}] {PR _ Title} - [PR #{number}] {PR _ Title} - [PR #{number}] {PR _ Title} --- ## 🧪 For the Testing Team: Risk & Impact Analysis ### High-Risk Areas - **[Impacted Module/Feature 1]:** [Brief English description of the change.] - **Risk Level:** ` High ` - **Dependencies:** [List of dependent components, e.g., "User Profile, Reporting Module, API v2"] ### Medium-Risk Areas - **[Impacted Module/Feature 2]:** [Brief English description of the change.] - **Risk Level:** ` Medium ` - **Dependencies:** [List of dependent components] ### Low-Risk Areas - **[Impacted Module/Feature 3]:** [Brief English description of the change.] - **Risk Level:** ` Low ` - **Dependencies:** [List of dependent components] --- < details > < summary > 🇯🇵 日本語のリリースノート (Japanese Release Notes) </ summary > # リリースノート: {Project _ Name} v{Current _ Version} **作成日:** 2025年8月28日 ## 概要 (Summary) このリリースに含まれる最も重要な変更点の概要(1〜2文程度)。 --- ## ✨ 新機能 (New Features) - [新機能1の分かりやすく、メリット中心の説明。] - [新機能2の説明。] ## 🚀 機能改善 (Enhancements) - [既存機能の改善に関する説明。] - [パフォーマンスやUIの改善に関する説明。] ## 🐛 不具合修正 (Bug Fixes) - [修正された不具合の簡単な説明。例:「ゲストロールのユーザーがレポートを生成できない問題を修正しました。」] - [その他の不具合修正に関する説明。] ## 🛠️ 技術的改善 (Technical Improvements) - [技術的な変更やバックエンドの変更に関する概要。例:「システムのセキュリティを向上させるため、コアサーバーの依存関係を更新しました。」] - [その他の技術的改善に関する概要。] --- ## 📦 含まれるプルリクエスト (Included Pull Requests) このリリースに含まれるプルリクエストの完全なリストです。 - [PR #{number}] {PR _ Title} - [PR #{number}] {PR _ Title} - [PR #{number}] {PR _ Title} --- ## 🧪 テストチーム向け: リスクと影響範囲の分析 (Risk & Impact Analysis) ### 高リスク領域 (High-Risk Areas) - **[影響を受けるモジュール/機能1]:** [変更内容の簡単な日本語説明。] - **リスクレベル (Risk Level):** ` 高 ` - **依存関係 (Dependencies):** [依存するコンポーネントのリスト] ### 中リスク領域 (Medium-Risk Areas) - **[影響を受けるモジュール/機能2]:** [変更内容の簡単な日本語説明。] - **リスクレベル (Risk Level):** ` 中 ` - **依存関係 (Dependencies):** [依存するコンポーネントのリスト] ### 低リスク領域 (Low-Risk Areas) - **[影響を受けるモジュール/機能3]:** [変更内容の簡単な日本語説明。] - **リスクレベル (Risk Level):** ` 低 ` - **依存関係 (Dependencies):** [依存するコンポーネントのリスト] </ details > --- ## ACTION REQUIRED Execute the analysis and generate the bilingual release notes file as the final output. **REPOSITORY_NAME:** ` {{Repository}} ` **RELEASE_BRANCH_NAME:** ` {{Release branch}} ` ## Forbidden actions do not push to any branch In practice, the most important part is step 3 combining intent with actual implementation. That’s what prevents misleading summaries. Impact QA got a reliable way to verify changes Instead of digging through commits from scratch, QA now uses release notes as a baseline for double verification. They can: Cross-check listed changes against actual behavior Focus on high-risk areas first Understand dependencies between components This didn’t just make things faster it made validation more structured and less error-prone. Better visibility for stakeholders Product managers get clear, structured release notes automatically. The categorization is consistent, and the language is adapted for non-engineering audiences. Consistency across releases Every release note now follows the same structure: Same sections Same formatting Same level of detail This makes it much easier to scan and compare releases over time. Reduced ambiguity We used to see a lot of questions like: Was this included in the release? Did we fix this issue? Is this area risky? Now those answers are usually already in the release notes. Lessons Learned 1. Version comparison is the foundation The biggest improvement came from switching to version-based diffing. Once we relied on: Previous tag → Current release Everything became more accurate and complete. 2. Risk analysis adds real value Release notes alone are useful, but adding risk analysis made them actionable. QA can immediately identify: Where failures are more likely What areas need deeper testing Which components are interconnected Recommended Test Focus : Suggest whether this change requires deep functional testing, broad UI regression, or specific edge-case validation (e.g., "Focus on data integrity for Guest roles" or "Perform wide CSS regression across mobile viewports"). 3. Fully automated doesn’t mean zero oversight We still review release notes occasionally, especially for larger changes. But instead of writing everything manually, we’re reviewing something that’s already mostly correct. Closing Thoughts This system isn’t complex. It’s mostly: Git diffing A structured prompt A simple automation flow But it changed how we handle releases. Release notes went from being an unreliable afterthought to something QA can actually use for validation. If you’re still writing release notes manually, start with version comparison. That alone solves most of the problem. The rest is just refinement.
市役所からのお知らせです。 _人人人人人人人人人人人人人人人人人人人人人人人人人人_ >  この記事では IEEE754 偶数丸めを仮定しています! <  ̄YYYYYYYYYYYYYYYYYYYYYYYYYY ̄ 述語によっては丸め方向が大事なものがあるのですが、いちいちお断りするの大変ですからね。世は大省エネ時代です。 こんなみなさまに読んでいただきたい! ところでみなさま! 初めて算術回路を学んだときの感動を憶えていらっしゃいますか? みなさま「足し算なんて私の知っている筆算を回路に落とすだけでしょう、私は天才!人生楽勝!」 みなさま「……高さ (= 回路の最長路長) O(log w) !?」(w はワード長) ご存知なかった方はこの機会にそちらもチェックしていただきつつ、まずは私の一勝ということでね。 そういえば算術回路についての名著『アルゴリズムイントロダクション』の第2版 第3巻(今手に入るのですかねこれ)に書いてありました。第3版で廃止されたのですが、時代ですね🙃 ともあれこういうのまだまだやりたいではありませんか! 浮動小数点数 & 計算幾何 𝐼𝑠 𝐴𝑙𝑙 𝑌𝑜𝑢 𝑁𝑒𝑒𝑑(文法ミス)です。やっていきましょう! Section 1: 適応的精度 (Adaptive Precision) の概要 適応的精度 (Adaptive Precision) は、状況に応じて計算精度を徐々に上げていくことで、最小限のコストで厳密な不等式判定を可能にする手法で、丸め誤差をうまく再利用するのがポイントです。 Adaptive Precision の詳細については Shewchuk (1997) に詳しいです。みなさまもぜひです。 適応的精度(Adaptive Precision)を知ったきっかけ 3D CAD といえば B-Rep (Boundary Representation) です。要はトポロジー(面などの接続関係)と、ジオメトリー(それらの方程式)を別で管理するということですね。 これがまた大変です。決まった形をしていませんから、機械学習がとても大変です。(実はここ最近 B-Rep 向けの機械学習もアツいのですがそれはまた別のお話🖐️) というわけでみんな大好きルールベースになるわけですが、これアルゴリズムをある程度頑健にしておかないとデバッグが難しいのですよね。そのへんでいろいろ調べていたら知ってしまった†深淵†のひとつです。私の右目が疼く前に次のセクションへ GO! です。 適応的精度(Adaptive Precision)の動機は? 計算幾何あるあるをご紹介しましょう。 たとえば、2 次元の与えられた 3 点 A, B, C の配置がこの順に反時計回り、時計回り、共線(同一直線上)のどれであるかを判定する問題を考えてみましょう。 みなさま「こういうのは行列式を使えばよいのですよね。数学に詳しすぎて照れてしまいますね」 みなさま「あれ、こ〜れ境界値周りの挙動が一貫していなかったらぶっ壊れますでございますわ〜☀️☀️☀️」 共感していただけましたでしょうかね。要は別に微妙な結果がどちらに転ぶのかをすごく気にしている訳ではなくて、大事なのは ズレるのはよいとしてどちらに転ぶかが一貫していなかったら全体がぐちゃぐちゃになってしまう ということです。 反時計回りであるかどうかなど、Yes/No が定まるものを 述語(predicate) といい(例:反時計回りか?円の内側か?など)、これを厳密に判定する手法は 厳密幾何計算 (Exact Geometric Computation, EGC) と呼ばれていて、数値そのものを正確に計算する厳密算術計算 (Exact Arithmetic Computation) とは区別されます。符号などの判定だけを厳密にするので、数値全体を厳密化するより軽いのがポイントです。 せっかくですから、 Kettner, Lutz, et al. (2008) から印象的な図表をいくつか引用しましょう。 印象的な図表 1 まずは反時計回り・時計回り・共線判定がいかに数値的に不安定であるのかを見ていきましょう。 キャプションにある 3 点 A, B, C を、ほんのちょっとだけ動かした点 A', B', C' が、どちら向きに並んでいるかを浮動小数点数演算で判定した結果です。 反時計回りになっている 👉 青色 時計回りになっている 👉 赤色 共線になっている 👉 黄色 ちなみに黒色の細い対角線は、真に共線になるべきところです。え!?!? もはや絨毯として販売できそうなカオスっぷりです。 印象的な図表 2 次は、反時計回り・時計回り・共線判定が不安定なせいで、いかに派手に幾何アルゴリズムが \ドンガラガッシャーン💥/ するのかを見ていきましょう! むかしむかしある座標平面に、点 p₁ さんと、点 p₂ さんと、点 p₃ さんと、点 p₄ さんが、この順に反時計回りの凸四角形を構成しながら暮らしていました。 おばあさんが川で洗濯をしていると、大きな桃🍑が流れてきました。 その桃🍑を切ると、なんと点 p₁ のすぐ右に点 p₅ がおぎゃあ、おぎゃあと……… ってこれいつまでやればよいでしょうかね。止めないと私無限に続けますよ!?!? そんなことより凸性です! 点 p₅ を追加した結果は (a) の通りです。 点 p₁ と点 p₅ の差が丸め誤差に吸収されて、ちょっと凹んでしまっていまね。ウーンム…… みなさま「こんなん誤差やん。うおw」 私「1εを笑うものは1εに泣きますよ。」 ここで (b) の拡大図をご覧ください! 点 p₆ を追加するとあらふしぎ。前提が壊れているのでぐ〜〜〜〜っちゃぐちゃです。 (c) は細かい実装バリエーションの結果です。わかる人にだけわかるご説明をすると、点 p₆ から「見える」edge は p₄p₅ と p₁p₂ (のみ)で、どちらを先に発見するかで結果が変わるというわけですね。 これでもう「うおw」とは言わせませんよ。 いたるところで大活躍する基本的な述語 4 選! Orient2d みなさまの記憶力は素晴らしいため、問題を憶えていらっしゃいます。 2 次元において、点 c が直線 ab のどちら側にあるかは次の行列式で決まります。 ところでこれ命名の流派たくさんあって毎回ウーンムとなります。厳密計算の分野における古典的な呼び方はおそらく Orient2d, Orientation2d なのですけれども、たとえば nalgebra では perp (perpendicular product の略?) と呼ばれていたり、私はじめ競プロ経験のある方は ccw (counter-clockwise の略)を使うことが多かったり、単に det と書く方もいらっしゃったりなどです。 Orient3d 3 次元バージョンです。萌え萌えでなくて申し訳ないです。 3 次元において、点 d が平面 abc のどちら側にあるかは次の行列式で決まります。 InCircle 2 次元において、点 d が円 abc の内外どちらにあるかは次の行列式で決まります。 InSphere 3 次元において、点 e が球 abcd の内外どちらにあるかは次の行列式で決まります。 これらの主な用途を挙げておきますね。聞いたことのない方には先着で読み上げ音声をプレゼントです。 アルゴリズム 活躍する述語 凸包構築 Orient2d 線分交差判定・線分群の交差列挙 ( Bentley–Ottmann algorithm ) Orient2d ドロネー三角形分割 Orient2d, InCircle ボロノイ図 の構築 ( Fortune’s Algorithm ) InCircle メッシュの refinement ( Delaunay Refinement ) Orient2d, InCircle 3 次元でも基本的には同様の問題があって、対応する述語が活躍することが多いです。 Adaptive Precision のアプローチ お話がそれましたわ。これらの述語をどういうアプローチで「必要なときだけ厳密に」「お爆速に」行うのかをみていきましょうですわ〜〜〜 大事なことは、 近似計算でも結果が 0 から大きく離れていたら信じて OK (= 推定誤差上界より十分大きい)ということです。 ある精度で近似計算を行います。 誤差上界を評価します。 0 から大きく離れている場合 👉 return 結果 0 に近い場合 👉 精度を上げて 1 行目にもどる or どこかの段階で任意精度計算で厳密な判定を行います。 アッ、まだ帰らないでいただいて…… わかります。 みなさま「ハイハイ、どうせ任意精度(= 多倍長)小数でしょ。┐(´д`)┌」 私「面白いのはここからです!!!(☝ ՞ਊ ՞)☝」 これでうまくいくのは当たり前ではありませんかと、ええ、うんうんそれは彼氏さんが悪いです。 なんと Adaptive Precision の方法は 動的メモリ確保なし・ほとんど条件分岐なし で行うことができます。しかも 精度を上げて再計算するときには前回の計算の途中結果を使いまわすことができます 。その裏にあるのは、丸め誤差をキレイに分類するための、まるで回路図のような洗練されたアルゴリズムです。 たとえば Orient2d は下図のようになります。 アッ、まだ帰らないでいただいて…… 大丈夫です、私もわかりませんでした。 目標まとめ やりたいのはこちらでしたね。 Orient2d, Orient3d, InCircle, InSphere などの述語を厳密に判定したいです。 境界値に近くない場合は計算が早く終わっていただきたいです。 動的メモリ確保なしです。(従って 普通の任意精度小数は使えません! ) 条件分岐は必要最低限です。(従って 算術計算なんかで場合分けは使えません! ) 全部は大変ですから、みなさまがご興味を持っていただけそうなところまでご招待したいと思います。 Section 2: 足し算・掛け算の実装 2 つの浮動小数点数の和や差を、どうやって表しましょうか 2 つの浮動小数点数 a, b の和 a + b や積 ab の正確な値は、1 つの浮動小数点数では表現できません。 しかし! ここで軽率に普通の任意精度小数を使ってしまうと、たとえば 2¹⁰²³ と 2⁻¹⁰²² の和などはと〜〜〜んでもない長さになってしまいますね。長いものがお好きな方以外は悲しくなってしまいます。 そこで、 和による表現 です。a + b や ab を 2 つの浮動小数点数 x, y の和で表してしまいましょう。といっても何でもアリにしてしまうと後で困るので、上手い条件を課します。a + b や ab の場合は実は x が丸めた値、y が丸め誤差に一致するようにするのがツウなのですが、一旦もう少し一般のお話をいたしましょうか。 a + b と等しい expression 任意精度小数の一般的な表現 有限小数 x に対し、それを浮動小数点数 x₁, x₂, ..., xₙ の総和 x = x₁ + x₂ + ⋯ + xₙ で表したものを x の表現 (Expression) と呼ぶことにしましょう。 そしてしばしば次のような条件を課します。 nonoverlapping 表現 x = x₁ + x₂ + ⋯ + xₙ が nonoverlapping であるという用語を定義していきましょう。 まずは準備です。浮動小数点数 a, b が次のいずれかの条件を満たすとき、a と b は nonoverlapping であるといいます: a の最下位ビットが、b の最上位ビットよりも真に大きい b の最下位ビットが、a の最上位ビットよりも真に大きい a = 0 b = 0 表現 x = x₁ + x₂ + ⋯ + xₙ は、そのどの相異なる 2 項も nonoverlapping であるときに nonoverlapping であるといいます。 「ビットの範囲が重なっていない」と言い換えるとわかりやすいかもでしょうかね。 nonoverlapping の概念図 nonadjacent 次は、表現 x = x₁ + x₂ + ⋯ + xₙ が nonadjacent であるという用語を定義していきましょう。 まずは準備です。浮動小数点数 a, b が次のいずれかの条件を満たすとき、a と b は nonadjacent であるといいます a の最下位ビットが、2b の最上位ビットよりも真に大きい b の最下位ビットが、2a の最上位ビットよりも真に大きい a = 0 b = 0 表現 x = x₁ + x₂ + ⋯ + xₙ は、そのどの相異なる 2 項も nonadjacent であるときに nonadjacent であるといいます。 「ビットの範囲の間に 1 bit 以上隙間が空いている」と言い換えるとわかりやすいかもでしょうかね。 nonadjacent の概念図 みなさま「わざわざ隙間を 1 開ける理由はなんですか?」 わかります。浮動小数点数演算 a + b や ab の結果を丸めた値 x と丸め誤差 y の関係を思い出してみましょう。 計算機イプシロン ε を用いて |y| ≤ (x の最上位ビット) · ε / 2 が成り立ちますね? はい、一つ隙間が空いているため 𝑣𝑖𝑐𝑡𝑜𝑟𝑦 です。逆にこれ以上開けるのを保証するのは不可能です。 誤差をバッチリ管理して算術演算をする方法 記法 浮動小数点数 a, b に対して、a + b, a - b, ab を丸めた値をそれぞれ a ⊕ b, a ⊖ b, a ⊗ b と表します。 浮動小数点数の和 a + b 簡単のため |a| ≥ |b| を仮定しましょう。すると筆算の様子はだいたいこういう感じですね。実際には a, b, x, y のどれか負になったりもしますけれども、そのあたりはご愛嬌です。 x は浮動小数点数演算でそのままGO!です。y は真の値 x の差 (a + b) - x を丁寧に計算すれば良いですね。具体的には y = b - (x - a) と表すと途中どこにも丸め誤差が生じなくて 𝑤𝑖𝑛 です。👇 疑似コード みなさま「え、あの、 |a| ≥ |b| ってつまりそれを保証するために条件分岐が入りますよね???」 ウーンム、痛いところを突かれましたね。しかし実は y = (a - (x - (x - a))) + (b - (x - a)) がそれを解決するのですよ。証明は難しくはありませんが場合わけが多いですから、みなさまのご担当ということでよろしくお願いさせていただいてですね。そのかわり明日の給食当番は私にお任せください🖐️ Expression e と浮動小数点数 b の和 結果となる expression は長さが 1 長くなります。この世界では演算をしても項の個数が減らないという地獄みたいな設定ですからね🫠 e = e₁ + e₂ + ⋯ + eₙ が nonadjacent かつ、「一部に 0 があるかもしれないことを除いて絶対値が単調増加」という前提条件を課して、次の回路で計算すると、実はその性質が保たれます。 Expression 同士の和 e + f ドーーン!Go Bold の精神です。 浮動小数点数同士の積 ab これまた賢いですよね。実は入力 a, b をともに上位ビットと下位ビットに分解するとうまくいきます。というかよく考えたらそうでもしないと精度が足りないのは明らかですよね。(p 桁の正整数と q 桁の正整数の積が常に p + q - 1 桁であるという事実に注意です。) それに注意した上で、足し算のとき同様に、丸め誤差が出ないように少しずつ差を計算していく方針です。疑似コードを追っていただいてもわかるかもしれませんし、難しければぜひとも原典をです。 Expression e と浮動小数点数 b の積 次のような回路で計算できて、項の個数は 2 倍になりますね。 とはいえ論文中のどの述語の実装でも掛け算は浮動小数点数同士でしか行っていませんから、今回必要はないみたいですね。 これで一通り回路のパーツが揃いました! あとは各述語 Orient2d, Orient3d, InCircle, InSphere を実装していくだけです! ……というのがまあ地獄みたいな作業なのですよね。 あと念の為注意なのですが、出力される expression の項は 0 を含むことがあります。例えば a + b を浮動小数点数で計算しても全く誤差の出ない場合は y = 0 になりますよね。これをハンドルしようと思うと当然場合分けが出てくるため、意図的に放置して、固定演算だけで済むようにしております。賢いですね。 Section 3: 各種幾何述語の adaptive precision 実装(の方針) 目標の下方修正 Orient2d のみを実装しましょう。 えいやだっていやじゃないですか。—— みつを Orient2d の展開 定義を展開しましょう。 これをそのまま回路に起こした結果がこちらです。 アッ、まだ帰らないでいただいて…… いまから理解できますから、たぶん、4 割くらいは。(あの?) 最初の引き算 まずは入力を引き算して、主要項と誤差項にわかれますね。このとき、 主要項はオーダー O(1)を、誤差項はオーダー O(ε) を持つ とみなしましょう。実際には誤差項は主要項のε倍よりもずっと小さいこともありますが、小さい分にはよいのでこのまま形式的に突っ走っていきましょう! 次の掛け算 さらに図の左半分と右半分それぞれについて {主要項, 誤差項} と {主要項, 誤差項} の 4 種類の掛け算をして、8 つずつ、16 個の項を導出しましょう。これは図の左から順にオーダー O(1), O(ε), O(ε), O(ε²), O(ε), O(ε²), O(ε²), O(ε³) (左半分だけ記述)を持ちます。あとはこれらを全て足し引きしたものが厳密な答えです。 厳密な答え D 実際に Expression の足し引きを行なったものが D です。いかにこれを計算しないかが大事ですね。以降、さまざまな近似をみていきましょう! 最も粗い近似 A 浮動小数点数でそのまま計算した結果に相当します。 A の誤差範囲 慣れている方ならおかわり🍚いただけると思いますけれども、Orient2d 唯一の危険ポイントは減算 x₅ − x₆ による桁落ちです。そこで |x₅| + |x₆| の定数倍により評価していきましょう。 こちらが温めておいた誤差限界です🍳(料理番組風) |A| がこの値以上でしたら、sign(A) = sign(D) が保証されます。 え、私は証明を追ったのかですって? さて、次は A と D の中間の精度の値です。 第2の近似: B' これは正直なところ A とほとんど変わらないので、私はあまり存在意義がわからなかったのですが、なにかご存知の方いらしたら教えていただきたいです。 B’ というのは Expression B の浮動小数点数近似です。 みなさま「そんなの B の O(1) 項を取れば良いだけでしょうそんな、またまたぁ〜〜」 わかります。私もそう勘違いしたせいで無限にお時間が溶けました。ここで扱っている expression というのは、主要ビットから貪欲になるべく長くとったものとは限らないということに注意です。ここで躓かなかった方は 𝑌𝑜𝑢 𝑘𝑛𝑜𝑤 𝑡𝑜𝑜 𝑚𝑢𝑐ℎ、バッドエンドです。 実は論文中(Theorem 23)に、主要項を抽出するのに使えるアルゴリズムが記載されています。詳細はさておき、条件分岐が出てきているのはちょっと悲しいところですね。(ちなみにループは今回固定長なので消せます。) 誤差上界をはこちらです。A とあまり変わらなくて悲しいですね。 第3の近似: C O(ε) のオーダーを持つ項を片っ端から集めていることがわかりますね。 誤差限界はこちらです: これでもどうしようもなければ D を計算するということですね。賢いです。 Section 4: 現代の厳密計算 結局私個人が厳密幾何計算を実装する(ハメになる)機会はなかったのですが、では現代ではもう使われていない技術なのでしょうかね。だとしたら、とても寂しいですね。 最後に、この手の厳密述語 (exact predicate) が現代のライブラリでどう扱われているかを軽く見ます。 CGAL にありましたよ!! こちら、どなたが使っているのですか!? CGAL (Computational Geometry Algorithms Library) というライブラリがあります。計算幾何のあらゆる叡智の詰まった老舗の C++ ライブラリなのですが、そこを探してみると、 Predefined Kernels というクラス群が見つかります。これは幾何計算をしたり、幾何的オブジェクトを構築したりするときのカーネルです。 命名規則を見ると、exact predicate と exact construction が見えますね。Exact predicate というのは今回ご説明したように、「述語の yes/no を厳密に判定する」ことで、比較的簡単かつあまり大きなコストがかからないと言われています。(簡単ですか??????) 一方 exact construction は非常に難しいです。というのも predicate のときのように「誤差を評価してフィルタリング」のようなテクは通用せず、すべてを厳密計算する必要があります。そのうえ 複雑な制約条件により与えられる点は長大な有理数や複雑な方程式で表される からです。「構築した点を基準に構築する」とかにも対応しないといけませんからね。 OCCT にはなさそうですね??? CAD の超有名 OSS であるところの OCCT (Open Cascade Technology) もチェックです! 一応探してみましたが、なさそうな気がします。 根拠列挙ターイム! OCCT は他の CAD カーネル同様、tolerance ベースで管理を行っていますね。 CAD カーネルは点の構築が大前提のソフトですから、exact predicate もうまく活躍できなそうです。 Tolerance によるヒーリングの技術も発達しているので、それで実は実用上困らないのかもしれません。 実際、非常に繊細で複雑な計算である Boolean 演算でも tolerance ベースで計算していることが、 Boolean Operations の Guide を参照するとわかります。 BTW: 機械学習ってなんぼのもんなんですかね さて、ここまで読んでくださったアルゴ系エンジニアのみなさま、機械学習はお好きですか? みなさま「機械学習がなんぼのもんじゃい✊💥」 みなさま「私たちの憧れたヒューリ🥒スティックプログラミングいずくにかです!」 え、今日日そんなこと思いませんよって、ぐぬぬぅ…… というか私が☝️でしたというだけなのですけれども、最近は変わりました。 機械学習は、やや矮小化かもしれませんけれども「書かなくていい部分が増えただけ」で、古典的手法のエッセンスは残っている気がするのですよね、少なくとも現時点では。 良いヒューリスティックは大抵「天才的な気づき」から始まるものの、その後はつらいモグラ叩きになりがちです。機械学習は、そのモグラ叩きを「最適化問題」として正面から殴れるのが最高に気持ち良いです! Transformerのような汎用手法が全盛の今でも、何が本質かを見極めてベクトルを練り上げる工程は、極めて domain-specific です。そこには間違いなく、古典の英知が息づいています。 現実世界には「理論上解けるはずなのに、放置されている面白い最適化問題」が山積みです。その山を、古典の英知と最新の技術で一緒に崩しませんか? そんな仲間を、我々キャディのチームは待っています😉 speakerdeck.com まとめ 何かをするのに遅すぎるということはありません。 おととい 4/11 は私のお誕生日でした。プレゼント🎁を送れなかったよという方、あとはわかりますね。
こんにちは、Infrastructure Teamの宮本( @m1yam0t0 )と申します。 本記事では、キャディの権限昇格システムの取り組みを紹介します。 目次 目次 はじめに 内製システムから Google Cloud PAM への移行 PAM の利用資格の設定 PAM の運用で工夫していること Slack 通知機能の実装 Devin による利用資格設定の自動化 まとめ はじめに みなさんは、パブリッククラウドの権限はどのように管理されていますか? IAMでメンバーに必要な権限を付与していますでしょうか?TerraformでIaC管理されていますでしょうか? キャディでは、最小権限の原則に従って、開発者には閲覧系の必要最低限の権限のみを付与しています。 *1 開発・運用で追加の権限が必要になった場合は、Just-In-Time(JIT) Accessの仕組みで一定期間だけ権限昇格できるようになっています。 開発者が権限を申請し、承認者が承認してはじめて権限が付与されます。 最小権限の原則を徹底することで、昨今利用が広がっているAIエージェントを活用した場合にも、本番環境の権限を持っていないため、誤って操作してしまうリスクを低減できます。 本記事ではこのJIT Access Systemを内製システムから Google Cloud Priviledged Access Manager(PAM) に移行したお話を紹介いたします。 内製システムから Google Cloud PAM への移行 キャディでは、2022年より内製のJIT Access Systemを運用しておりました。 しかし、この内製のJIT Access Systemについて、認可制御に課題があったため、IAM Condition を組み合わせて権限が付与できるように改修を検討していました。 そんな中、Google Cloud PAM のアップデートで、権限のスコープが設定可能になったことを知り、検証をしてみたところ、内製のシステムを改修しづつけるより、効率良く目的を達成できることがわかりました。 IAM release notes  |  Identity and Access Management (IAM)  |  Google Cloud Documentation 上記を踏まえて、以下の理由から、キャディのJIT Access SystemをGoogle Cloud PAMへ移行する価値があると判断し、移行を実施いたしました。 権限のスコープを細かく設定でき、取得する権限を必要最小限に抑えられる Cloud Loggingに監査ログが残るため、通知や監査に活用できる Google Cloudマネージドのサービスであるためメンテナンス不要 PAM の利用資格の設定 Google Cloud PAMでは、取得したい権限のセットを利用資格として定義し、必要な権限に対応する利用資格を選択して申請します。 Terraform Provider が公式で提供 されているため、IaCで管理できます。 利用資格はユースケースごとに複数作成するため、 以下のようなTerraform moduleを定義し、変数を入力することで容易に設定できるようにします。 resource "google_privileged_access_manager_entitlement" "entitlement" { provider = google-beta entitlement_id = var.entitlement_id location = var.location parent = var.parent max_request_duration = var.max_request_duration eligible_users { principals = var.eligible_users } privileged_access { gcp_iam_access { resource_type = var.resource_type resource = var.resource dynamic "role_bindings" { for_each = toset (var.roles) content { role = role_bindings.value } } } } approval_workflow { manual_approvals { require_approver_justification = var.require_approver_justification steps { approvers { principals = var.approvers } approvals_needed = var.approvals_needed approver_email_recipients = var.approver_email_recipients } } } additional_notification_targets { admin_email_recipients = var.notification_emails } requester_justification_config { unstructured {} } } 実際に利用する箇所では以下のようにTerraform moduleを呼び出して定義しています。 申請・承認するユーザを設定できるため、チームによって申請可能な利用資格を設定できます。 module "pam_org_gcs_bucket_read_access" { source = "../../modules/pam" # 利用資格名 entitlement_id = "gcs-bucket-read-access" # Organization, Folder, Project 単位で指定可能 parent = "organizations/$ { local.organization_id } " location = "global" resource_type = "cloudresourcemanager.googleapis.com/Organization" resource = "//cloudresourcemanager.googleapis.com/organizations/$ { local.organization_id } " # 取得したい role を定義 roles = [ "roles/storage.bucketViewer" , "roles/storage.objectViewer" , ] # 申請可能なユーザ eligible_users = [ "group:users@caddi.com" , ] # 承認可能なユーザ approvers = [ "group:approvers@caddi.com" , ] # 最大の申請期間 max_request_duration = "14400s" # 必要な承認の数 approvals_needed = 1 } PAM の運用で工夫していること Slack 通知機能の実装 PAMの通知機能はメール通知のみでSlackへの通知には標準では対応していません。 しかし、既存のJIT Access SystemではSlack通知するようにしていたため、利用者体験が変わらないようにする必要がありました。 そこで、PAMの監査ログの内容をパースしてSlack通知するAPIを実装しました。 以下のような仕組みで動作しています。 Pub/Subを経由して、実装したSlack通知APIに送信 Cloud Loggingに保存されているPAMの監査ログをLog routerでPub/Subに転送 Pub/Subの Push Subscription を使ってCloud Runで動作しているSlack通知APIにHTTP POST 実際の構成図は以下です。 PAM通知機能の構成図 上記のSlack通知APIを利用して、既存システムの使用感はそのままに、SlackでPAMの申請・承認を通知できるようにしました。 PAMのSlack通知 Slack通知の内容についても、利便性を上げるために様々な改善をしています。 承認結果のメッセージを申請したメッセージのスレッドに紐づけて投稿 申請内容に不備がある場合は、申請内容の修正を促すように警告文を自動で投稿 申請のステータスによって、SlackのAttachmentの色を変更 Devin による利用資格設定の自動化 PAMの利用資格の中に取得したい権限が存在しない場合、利用資格を修正する必要があります。 PAMの移行当初は取得できる権限が不足しており、利用者から多く依頼を受けていました。 その度にTerraformの定義を修正しレビューするのは大変です。 そこで、Devinを使って、Terraformの修正からPRの作成を自動でしてもらうようにしました。 Devin Playbook で利用資格を修正するPR作成作業を定型化しています。 利用者がSlack Workflowで追加してほしい権限を入力するとDevinが呼び出され自動でPRを作成します。 あとは、チームメンバーがPRをレビュー、マージして反映するだけです。 Devin による利用資格の変更PRの作成 まとめ 権限の最小化を徹底するために導入していたJIT Access Systemを、内製システムからGoogle Cloud PAMに移行しました。 PAMに移行したことで最小粒度の認可制御が可能になり、より安全にGoogle Cloud を運用できるようになりました。 また、AIエージェントの活用により運用の改善を効率良く実施できました。本当に便利なものですね。 みなさんがGoogle Cloud PAMを導入するときの参考になれば幸いです。 *1 : Terraformで管理
こんにちは、キャディで Quote というアプリケーションを開発している plant こと石田 ( @plant_ja ) です。 ハーネスエンジニアリングという言葉を目にする機会が増えてきましたね。「何をやるべきか」については OpenAI の Harness engineering: leveraging Codex in an agent-first world や逆瀬川さんの Claude Code / Codex ユーザーのための誰でもわかるHarness Engineeringベストプラクティス が非常に参考になります。 本記事では「何を」ではなく「どう始めるか」に焦点を当てています。Claude Code の場合だと Rules、Skills、Hooks、Subagents、さらには custom linter や pre-commit や pre-push、これらの構成要素を眺めているだけで「ちゃんとやろうとしたら大変そうだな」という気持ちになりますよね。 この記事では、そのハードルを下げるための1つのシンプルなアプローチを紹介します。 この記事のゴール ハーネス整備のハードル ルール整備によって変わったこと なぜうまくいったのか : 枠組みがあると更新のハードルが下がる 始め方 : 枠組みだけ先に作る プロンプト例 Rules 以外にも枠組みは作れる 終わりに この記事のゴール この記事のゴールは、ハーネスエンジニアリングの着手のハードルを下げることです。 「完璧なハーネスを最初から作り込まなきゃ」と構えるのではなく、「とりあえず枠組みを作って、そこからチームで育てていく」という発想を持って、気軽にハーネスの整備を始められるようになることが目標です。 ハーネス整備のハードル コーディングエージェントに実装させた結果、期待に満たないコードが生成されることがありますよね。 効率の悪さを感じながらも、その度に毎回「コードから自明なコードコメントは不要です」「 as unknown as のような危険な型キャストは使わないでください」という細かい指摘をすることになっていました。アーキテクチャ周りのドキュメントは充実していたのですが、実装の細部のガイドラインはまだまだ整備されていませんでした。 重要性は理解したものの、ハーネス整備にあたってのいくつかの心理的なハードルによって中々整備が進められていないという状況が数週間ほど続きました。 「そもそもハーネスエンジニアリングについての目線をチームで揃えないと」 「どういう形式でルールを整備するべきなんだろう、先にチームで議論しないと」 「ルールファイルを新たに追加したくなったけど、流石に実装 PR とは分けたほうがいいよな」 そんなことを考えつつ、ハーネスエンジニアリングについての目線をチームで揃えたり、custom linter を整備するなどの活動を進めたのですが、「やっぱり Rules ファイルの整備が手っ取り早く品質を向上させるためのアプローチだ」と考え、重い腰を上げて自分が開発しているアプリケーションのバックエンドの実装ルールを Claude Code の Rules ファイルとして整備することにしました。 ▶ 補足 : Claude Code の Rules とは Claude Code には CLAUDE.md というファイルにプロジェクトの指示を書く仕組みがありますが、指示が増えてくると1ファイルでは管理しづらくなります。 Rules は .claude/rules/ ディレクトリに Markdown ファイルを置くことで、指示をトピックごとに分割して管理できる仕組みです。例えば testing.md 、 api-design.md のようにファイルを分けられます。 さらに、YAML frontmatter で paths を指定すると、特定のファイルパスに一致するファイルを Claude が読んだときにだけルールが読み込まれる conditional rules として機能します。例えば paths: ["src/api/**/*.ts"] と指定すると、API 関連のファイルを扱うときだけそのルールが適用されます。 paths を指定しないルールはセッション開始時に常に読み込まれ、 CLAUDE.md と同じ扱いになります。 詳しくは 公式ドキュメント を参照してください。 Rules ファイルは、レイヤーを横断した汎用的な Rules (coding-style.md, testing.md) や、レイヤー毎の実装ルール (usecase.md, usecase_testing.md) に大別することとしました。 Rules を整備するために、既存の実装から実装パターンを推論して Rules ファイルを生成するスキルを作成しました。ファイルの生成自体はそこまで時間がかからずにできたのですが、実装方針がふわっとしている部分について意思決定を下したり、細かいニュアンスの修正など、想像以上にルールのレビューに時間がかかり、全てのレイヤーに対してのルールを作り切るまでに丸一日を費やしました。 しかし、ハーネスエンジニアリングの土台整備という観点だと、ここまでやる必要はなかったということに後に気づくことになります。 ルール整備によって変わったこと バックエンドの Rules ファイルが一通り整った上で、実装を進める中で「これもルールに盛り込みたいな〜」という点がいくつも出てきます。そこで、ルール更新用のスキル( /update-coding-rule )を作り、 /update-coding-rule コードから自明なコードコメントは不要です という指示を Claude Code に出すと、対応するルールファイルを探して追加・更新してくれるようにしました。実装の途中で出る細かい不満が、そのままチームで管理している品質基準へのフィードバックになるわけです。 PR レビューのコメント URL を渡しても同じことができるようにしました。 /update-coding-rule <PR のコメント URL> と指示するだけで、スキルが該当スレッドを取得して、PR レビューでの指摘をルール化してくれます。 このスキルを何回か使っていく中で、 「実装中の細かい不満をチームの品質基準に反映させる」という作業に対しての心理的なハードルがとても下がっている ことに気づきました。 ▶ 補足 : /update-coding-rule のスキル定義テンプレート 自分が使っているスキルをプロジェクト固有の部分を取り除いてテンプレート化したものです。 .claude/skills/update-coding-rule/SKILL.md として配置すれば、 /update-coding-rule で呼び出せます。 --- name: update-coding-rule description: フィードバックや PR レビューコメントからルールを一般化し、ルールドキュメントに反映する。 argument-hint: "[PR comment URL or feedback text]" --- # コーディングルール更新スキル フィードバックを一般化し、コーディングルールドキュメントに反映する。 ## フロー ### 1. フィードバックの収集 - PR コメント URL が渡された場合は GitHub API でスレッドを取得する - テキストが渡された場合はそのまま使う - 不明確な場合は AskUserQuestion で確認する ### 2. フィードバックの汎用化 具体的なフィードバックを、再利用可能なルールに変換する。 - 具体的な変数名・クラス名を抽象化する - 特定のコード修正指示から、背景にある原則を抽出する - GOOD / BAD のコード例を添える ### 3. 配置先の決定 ルールドキュメントのディレクトリ構成を確認し、どのファイルに追加すべきかを判断する。 既存ルールとの重複・矛盾がないかも確認する。 ### 4. ユーザーへの確認 AskUserQuestion で以下を提示し、確認を取る。 - 一般化したルールの内容 - 配置先ファイルとその理由 - 重複・矛盾の有無 ### 5. ルールファイルの更新 確認が取れたら、対象のルールドキュメントにルールを追記する。 なぜうまくいったのか : 枠組みがあると更新のハードルが下がる ここからが気づきです。ルールが育つようになった理由は、「充実したルールファイルを最初から用意した」からではなく、「ルールファイルをどこに・どんな形で置くか」という枠組みが決まっていたからです。 枠組みがあると、ハーネス整備に際してのハードルがグッと下がります。ハーネスの形式は既に決まっているので、その中身をリッチにしていくという営みは日常の開発の延長線上で容易に行うことができます。 この経験を経て、 重要なのはルール内容の充実度ではなく、誰でも更新しやすい状態になっていること だと気づきました。内容がスカスカでも、「これはここに追加すればいいんだ」という道筋が見えていることの方が大切なのだと。 始め方 : 枠組みだけ先に作る ここからが実践です。私は1日かけてルール全体をいきなり整備しましたが、そこまでやる必要はありませんでした。 枠組み(ほぼ空のルールファイル群)だけ先に作ってしまえば、始められます。 プロンプト例 プロジェクトのアーキテクチャをざっと分析してもらい、Rules ファイルの骨組みを作るプロンプトの例です。 自分がメインに利用しているのが Claude Code なので、Claude Code に特化した内容になっています。 このプロジェクトのアーキテクチャを分析して、Rules ファイルの骨組みを作ってください。 ## ルールドキュメントの種類 ルールドキュメントは下記の 2 種類に大別します。 ### 1. 汎用ルール レイヤーを横断する共通方針です。ユーザーにどのような汎用ルールを作成したいかを インタビューした上で、必要なものを作成してください。 例 : `coding-style.md`, `testing.md`, `error-handling.md`, `observability.md` ### 2. レイヤー別ルール レイヤーごとにサブディレクトリを作り、実装ルールとテストルールを分離します。 例 : `usecase/usecase.md`, `usecase/usecase_testing.md` ## ルールドキュメントの要件 - 各ルールドキュメントには概要のセクションを用意して、ルール自体の説明を書く - coding-style.md の例 : XXXプロジェクトのコーディングスタイルに関する横断ルール - usecase/usecase_testing.md の例 : Usecase 固有のテストルール。共通のテストルールは testing.md を参照。本ドキュメントの規約が testing.md と競合する場合、本ドキュメントを優先する。 - 既存のコードから明らかに読み取れるルールがあれば、セクションを追加して1~2個ほど簡潔に記入しても構わない ## .claude/rules/ のインデックスファイル ルールドキュメント本体の置き場所はユーザーにインタビューして決めてください。 `.claude/rules/` 配下のファイルはインデックスとして扱い、ルール本体への参照だけを記載します。ディレクトリ構成はルール本体のディレクトリ構造と一致させるようにします。 frontmatter の `paths` で対象ファイルパスを指定し、本文にはルールドキュメントへのパスを書きます。 以下は Usecase のテストルールのインデックスファイルの例です。 --- paths: - "src/usecase/**/*.spec.ts" --- # Usecase テストルール Usecase のテストを新規作成・修正する場合、計画開始前もしくは作業開始前に以下のルールファイルを読み込むこと: <ルールドキュメントへのパス> このプロンプトを Claude Code に渡すと、「ここに何を置くか」という地図ができます。 ▶ 補足 : なぜ .claude/rules/ をインデックスとして扱うのか Claude Code の Rules は paths を指定することで conditional rules として設定できます。該当パスのファイルをセッション中に初めて Read したタイミングでルールが読み込まれる仕組みです。 ルールの内容がリッチになってトークン数が増えてくると、コンテキストウィンドウを圧迫したり、attention が分散して本当に必要なタイミングで必要なルールが考慮されにくくなるという問題が起きやすくなります。 これを避けるために、 .claude/rules/ 配下のファイルはインデックス(参照先のパスだけを記載した軽量なファイル)として扱い、ルール本体は別の場所に置くという構成を採用しています。こうすることで、conditional rules として読み込まれるトークン数を最小限に抑えつつ、エージェントが必要に応じてルール本体を読みに行く形にできます。 Rules 以外にも枠組みは作れる 今回は Rules の整備を例に挙げましたが、同じ発想は Rules ファイル以外でも使えると考えています。例えば Skills であれば、API の stub を生成するスキル( /create-api-stub )を最低限の機能で作っておくだけで、使う中で「リクエストパラメーターのバリデーションもこの時点で入れるようにしよう」などといったフィードバックが自然と集まるようになります。Hooks であれば、Stop hooks や PostToolUse hooks などの各タイミングで走らせるための専用のスクリプト (例: stop-hook-script.js) を用意しておき、そこを少しずつ充実させていくなどのアプローチが取れるかもしれません。 枠組みと責務が明確だと、更新・拡張のハードルが下がる。 Rules に限らず、この原則は共通しているはずです。 終わりに ハーネスエンジニアリングは、完璧な仕組みを設計してからスタートするものではないと思っています。見出しだけのルールファイルでも、空っぽのスキルでも、「ここに足していける」という場所があるだけで、チームの誰かが気づいたことを反映できるようになります。 開発とハーネス整備を区別せず、同じタスクの中で完結させる。この感覚が根づくと、ハーネスは勝手に育っていくのではないでしょうか。 この記事で取り上げたような課題感をお持ちの方は、ぜひ枠組みを作るところからスタートしてみませんか? また、キャディでは一緒に働く仲間を大募集中なので、興味があれば下記もぜひご覧ください。 speakerdeck.com
はじめに 何が辛かったのか 毎回詳細なプロンプトを書くのが辛い AIエージェントのタスク完了まで面倒を見るのが辛い これらを並列で実行しているのが辛い 解決方針 詳細な設計ドキュメントの作り込み Usecase Design Doc 細かい実装指示・計画・実行をAIエージェントに委譲 タスクの分割方針 AIエージェントへの実装委譲 AIのお世話からの解放 - 得られた成果 開発速度の向上 PRレビュー自体の認知負荷の軽減 現在直面している課題 設計書の細かい誤りの増幅 設計とPRレビューのボトルネック化 まとめ はじめに こんにちは。CADDi Quoteのサーバーサイドの開発を担当しています、majimacchoです。私たちのチームでは全員がAIエージェントを活用して実装しPR作成まで行なっています。 私自身を含め、全く自分でコードを書かなくなったメンバーもいます。AIエージェントを使ってから個人のアウトプットは大きく増えましたが、その分 AIのマネジメント(お世話)の負荷 が高まっていました。 今回紹介するUsecase Design Docと呼ばれるドキュメントと関連する施策を通して、このAIのお世話の負荷を軽減しました。またそれに付随して1日にマージできるPRの数は2倍になっています。 この記事では、AIエージェントを利用した開発で増大するAIのお世話の負荷をどう軽減したかを、具体的な開発プロセスと合わせてお伝えします。 何が辛かったのか 私は大きく以下の3つのことが辛いと感じていました。 毎回詳細なプロンプトを書くのが辛い AIエージェントのタスク完了まで面倒を見るのが辛い これらを並列で実行しているのが辛い 毎回詳細なプロンプトを書くのが辛い AIが実装を行う際に「OOを実装して」のような曖昧な指示では期待通りのコードを作成してくれません。1つのPRを作成するのに、かなり多くの説明をしなければいけないことに負担を感じていました。AIしか読まない詳細な文書を書くことに虚無感すら覚えていました。様々なツールやスキルによって、ある程度個々の負担は軽減できますが、それでもAIのための指示を、その都度、正確にしなければいけませんでした。 AIエージェントのタスク完了まで面倒を見るのが辛い 最近のAIエージェントは十分賢くなったので、全ての挙動を見張る必要はなくなりました。 しかし、特にClaude Codeを利用している場合は、プランを確認したり、ツールの利用を許可したりするところで、まだ人間の操作を必要としています。 PR作成後もそのまま受け入れられる品質の時は良いですが、修正が必要な場合、何度もやり取りが発生したり、場合によってはセッションをクリアする必要があります。 これらを並列で実行しているのが辛い 上記の問題は、直列で作業している時には大きくなかったのですが、並列で仕事をするようになってから顕著になってきました。 特に、 1人で同時に5つ以上 のタスクをAIエージェントと実装するようになってからは、認知負荷が顕著に上がってしまいました。 AIコーディング以前は並列で作業することが間違っているという考え方が主流だったと思います。しかし、AIエージェントを待っている間に別のエージェントを動かさないと機会損失しているような感覚があり、ほとんどの時間に、AIを並列稼働させています。 ローカルでGit Worktreeを管理する必要も出てきたり、開発サーバーのポートが競合することもあります。手動でmainブランチをマージするのを忘れて、何度もコンフリクトを起こしました。 コンテキストスイッチも大きくなり一時期は並列数を3つまでに制限することもありました。 解決方針 上述の課題を以下の方針で解決しました。 詳細な設計ドキュメントの作り込み 細かい実装指示・計画・実行をAIエージェントに委譲 詳細な設計ドキュメントの作り込み 今回重要だったことは、細かい指示を出さなくても、AIエージェントが質の高いアウトプットを出せることです。 試行錯誤する中で、Usecase Design Docという形でより細かい設計内容をまとめることで、AIエージェントのアウトプットの質が高められることがわかりました。 Usecase Design Doc 以前はユースケースレベルの設計はPRのDescriptionやホワイトボードツールに散在しており、各開発者の暗黙知に依存していました。これを明文化することで、AIにとっての実装のエントリーポイントになると同時に、レビュー時の照合基準としても機能しています。 このドキュメントには、ユースケースシナリオに閉じた設計を記載します。具体的には以下の内容を含んでいます。 サマリー(アクター・操作対象・操作内容の定義) 関連テーブルのCRUD表 シーケンス図(トランザクション境界を含む) 主要なドメインエンティティのインターフェース(型定義・ファクトリメソッド) 関連するAPIインターフェース(API仕様書から抜粋) 既存コードへの変更箇所(レイヤーごと) 実装状況チェックリスト(集約単位・レイヤー単位) 以下は架空のユースケースを例にしたUsecase Design Docのサンプルです。実際のプロダクトのものではありませんが、記述の粒度やフォーマットは実際に使用しているものと同等です。ただし、サンプル内のユースケース・設計・コーディング規約は実際のものとは異なります。 # EX-a1: ユーザーはお気に入り商品をブックマークする ## サマリー - ログインユーザーが商品に対して、ブックマーク(お気に入り登録)を行う ## 関連のテーブル | テーブル | 操作 | 操作内容 | | :--- | :---: | :--- | | Product | R | ブックマーク対象の商品が存在することを確認する | | Bookmark | CR | ブックマークの存在確認(R)、新規作成(C) | | BookmarkCount | U | 商品のブックマーク数を +1 更新する | ## シーケンス図 ```mermaid sequenceDiagram autonumber actor User as ログインユーザー participant API as Product API<br/>[REST] participant DB as App DB<br/>[PostgreSQL] User->>API: POST /products/{productId}/bookmarks API->>DB: 商品の存在確認 alt 商品が存在しない API-->>User: 404 Not Found else 商品が存在する API->>DB: 既存ブックマークの確認 alt 既にブックマーク済み API-->>User: 409 Conflict else 未ブックマーク rect rgba(100, 150, 255, 0.15) Note over API,DB: トランザクション API->>DB: Bookmarkを保存 API->>DB: BookmarkCountを+1更新 end API-->>User: 201 Created end end ``` ## 主要なドメインエンティティのインターフェース > 実際のドキュメントではここにエンティティの型定義とファクトリメソッドを記載します。 > 例: ` type Bookmark = Readonly<{ id: BookmarkId; userId: UserId; productId: ProductId; ... }> ` > 例: ` const Bookmark = { create: (...) => Bookmark, ... } ` ## APIインターフェース > 実際のドキュメントではここにAPIのパスパラメータ・リクエストボディ・レスポンスの定義をテーブル形式で記載します。 > 例: パスパラメータ ` productId: string ` 、レスポンス ` 201 Created / 404 Not Found / 409 Conflict ` ## 既存コードの変更 > 実際のドキュメントではここにドメイン・ユースケース・インフラストラクチャ・プレゼンテーション等のレイヤーごとに、変更対象のファイルパスと変更内容をテーブル形式で記載します。 ## 実装状況 - [ ] 同期処理 - [ ] Bookmark 集約 - [ ] ドメインの実装 - [ ] ユースケースの実装 - [ ] プレゼンテーションの実装 細かい実装指示・計画・実行をAIエージェントに委譲 今までの開発ではAIエージェントに実装させる中でも、以下のようなタスクのために、AIエージェントとローカル環境に注意を向けなければいけませんでした。 最初のプロンプトの入力 コンテキストがいっぱいになった時のセッションマネジメント Toolの利用の許可 今回は、これらに注意を払わずに、設計とPRレビューに集中するプロセスを構築しました。 Usecase Design Docの内容を元にユースケースシナリオごとに実装タスクを計画します。 タスクの分割方針 ユースケース全てを1つのPRで実装するには大きすぎるため、基本的には以下の軸で分割します。 レイヤー単位(Presentation / Domain / Application) DDDの集約単位 同期・非同期の処理単位 この分割の基準は先ほどのUsecase Design Docのテンプレートに含まれています。 各PRは200〜300行の変更に収まることを目安にしています。この分割により、PR1件あたりの情報量が制限されます。さらに、レビュアーが把握すべき知識の範囲もレイヤーや集約の境界内に限定されます。 AIエージェントへの実装委譲 タスク分割が完了したら、以下の流れでAIエージェントに実装を委譲します。 Usecase Design Docの実装状況の欄のうち、未実装の項目の一番上を実施タスクとする Devin Searchで関連コード・ドキュメントを収集 : タスクを指定して、Usecase Design Docと関連する既存コードをDevinに読み込ませる DevinでPR作成まで実行 : タスク定義に基づいてDevinが実装し、PRを作成する CIのDevin Reviewで自動レビュー : 作成されたPRに対してCI上でDevinによる自動レビューが走る 設計ドキュメントが十分に詳細化されていれば、この一連の流れに人間が介入する必要はありません。お昼前にタスクを投入し、出来上がったPRを午後にレビューするという非同期な作業サイクルが成立します。 AIのお世話からの解放 - 得られた成果 従来のような並列でAIのタスクとセッションを管理しなければいけないような環境から脱却し、心には平穏が訪れました。 心理的、認知的に楽になっただけではなく、業務上も以下の成果が得られました。 開発速度の向上 従来のAIを活用した開発と比較して 1日にマージされるPRの数が2倍になっています。 これはAIエージェントが人間を待たずに作業ができるようになったことと、アウトプットの質が上がって手戻りが減ったことによるものです。 また、今までAIエージェントへのプロンプトやセッション管理を通じた直接的なマネジメントから、AIエージェントの環境整備を行なって、セッション横断で効果のある施作に時間が割けている事もその要因の1つです。 さらに、並列数の上限も特になくなり、その日のPRレビューに割ける時間から逆算して実装タスクの数を決めるようになりました。 PRレビュー自体の認知負荷の軽減 詳細な設計ドキュメントがPRレビュー時にあることで、PRの内容をすぐに理解できるようになりました。今まではなんとなく頭にあった情報を思い出してレビューしていました。今は詳細な設計ドキュメントと照らし合わせながらレビューしているのでレビュー時のコンテキストスイッチによる負荷も小さくなっていると感じています。 現在直面している課題 設計書の細かい誤りの増幅 今回、主要なインターフェースの設計もAIに書かせていましたが、軽微な間違いを含んだまま実装フェーズに進み、そのままPR作成まで行われるケースがありました。一見些細なインターフェースの誤りでも、実装を通じて増幅され、結果として大きなズレにつながります。開発者が実装に伴走していれば、PR作成前の段階で気づけていたはずですが、今回のプロセスではPR作成後に初めて問題が表面化しました。設計ドキュメント自体のレビュー精度を高めるか、実装の途中段階でフィードバックを得る仕組みを整えることが今後の課題です。 設計とPRレビューのボトルネック化 よく言われていることではありますが、実装速度が上がったことで、その前後の設計とPRレビューの速度が開発スピード全体を決めるようになりました。 特に設計は、AIに手放しで任せるのは難しい領域が大きく、まだまだ時間がかかってしまうところです。 PRレビューについてはハーネスエンジニアリング *1 によって不要になるという論調もありますが、本番環境で、お客様が利用しているサービスにPRレビューなしでマージするには課題が多いのも現状です。 まとめ AIエージェントに指示を出す前の段階で実装内容が自明になることで、AIエージェントの管理工数を削減し、開発者の認知負荷を軽減することができました。 この取り組みを通して確認できたことは、実装時にAIエージェントに指示内容を考えるよりも、設計時点で詳細な実装までイメージできるようなドキュメントを作成した方が効率が良いことです。 この設計ドキュメントを、どれだけ正確に無駄なく作れるかということが開発速度と品質を左右します。 AIエージェント自体をどれだけうまく活用するかということも現時点では重要ですが、それらはツール側の成熟によって自然と解決していきます。一方で設計はまだまだ人間が考えるべきところが多く残されていると思います。 もしチームで試すなら、最小限の構成としてUsecase Design Docの整備から始めることをおすすめします。ユースケースごとにサマリー・シーケンス図・実装状況チェックリストをまとめたドキュメントを1つ作り、それをコードベースの特徴に合わせて分割した上でAIエージェントに渡して実装させてみてください。設計の曖昧さがどこにあるかがすぐにわかり、改善サイクルが回り始めます。 このプロジェクトは、まだ進行中で試行錯誤しているところも多分にあります。プロジェクト完了後にふりかえり記事を出してその時に答え合わせをしようと思っています。 *1 : My AI Adoption Journey - Mitchell Hashimoto , Harness Engineering - OpenAI
こんにちは、ReliabilityグループでQAエンジニアをやっているyokota( @katawara )です。昨年の10月に入社しました。 入社直後のバタバタも落ち着いてきて、ようやく本格的にClaude Codeを使い始めたのですが、いろいろと試行錯誤を重ねてみているので、その中の活動のひとつをご紹介しようかと思います。 目次 目次 背景 Claude Codeのサブエージェント機能 MAGIシステムとは? スリーアミーゴスとは? サブエージェント x MAGIシステム = スリーアミーゴス 実際にやってみた 神田(PdM人格) 袴田(エンジニア人格) 秋山(QA人格) 設計のポイント おためしで動かしてみた サブエージェントなしとの比較 やってみてわかったこと 今後やりたいこと 背景 Claude Codeを活用するにあたり、大事になってくるのはコンテキストマネジメントかなと思っています。 *1 ところが、コンテキストマネジメントを意識しながらQAのお仕事をやらせようとすると、なかなか難しいことがわかってきます。ビジネス的な背景を理解しながら、技術的な制約も把握しつつ、仕様の裏や隙間も考える、という感じで、 一度にたくさんのことを把握しながら深く考えなければいけません 。 コードと複数サービスにまたがるドキュメント(キャディの場合はConfluenceやMiro、Figma、Google Spreadsheetなど)をMCPやCLIで読ませつつ、それらの分析をひとつのプロンプトで全部やらせようとすると、どうしてもアウトプットが薄くなったり、ある観点が抜け落ちたりしがちです。 そこで今回目をつけたのがサブエージェントという手段です。 Claude Codeのサブエージェント機能 Claude Codeには「サブエージェント」という仕組みがあります。 .claude/agents/ 配下にマークダウンファイルを置くだけで、独自のペルソナを持ったエージェントが定義できます。定義の仕方もシンプルで、冒頭にメタ情報を書き、続けてそのエージェントへの指示を書くだけです。 会話の中で @エージェント名 という形で呼び出せるほか、エージェント側からさらに別のエージェントに委任するといったこともできます。 また、サブエージェントを利用すれば、サブエージェントが使うコンテキストはメインセッションのものとは別になるので、コンテキスト消費の節約にもなります。 この発想が、自分に2つのものを連想させました。それがタイトルの 「MAGIシステム」 と 「スリーアミーゴス」 です。 MAGIシステムとは? アニメ「新世紀エヴァンゲリオン」に登場する、3台のスーパーコンピュータで構成された意思決定システムのことです。 3台はそれぞれ「メルキオール」「バルタザール」「カスパー」という名前がついており、開発者である赤木ナオコ博士の「科学者」「母親」「女性」という3つの人格が設計に反映されています。3台がそれぞれ独立して判断を下し、多数決で結論を出す、という仕組みです。 スリーアミーゴスとは? スリーアミーゴスというのは、アジャイル開発の文脈で聞かれることのある言葉です。PdM・エンジニア・QAの三者が同席して仕様について話し合う、というプラクティスのことを指します。 三者が揃うことで、ビジネス的な視点、技術的な実現方法の視点、品質保証の視点が交わり、仕様の抜け漏れや曖昧さを早期に発見できます。 サブエージェント x MAGIシステム = スリーアミーゴス ここまで、MAGIシステムとスリーアミーゴスには共通点があります。 いずれも3つの人格がそれぞれ独立して考えるという点です。 つまり、 メルキオール、バルタザール、カスパーの代わりに、PdM、エンジニア、QAの三者が動く世界が再現できれば、コンテキストマネジメントを適切にこなしつつ、通常のプロンプトを動かすよりも精度が高いアウトプットが出てくるのではないか 、という仮説を立てました。 実際にやってみた ということで、この発想をもとに、3人の人格を作ってみました。 名前がないと呼ぶときに不便かなということで、名前もつけています。なぜこの名前なのかわかった方はたぶん同世代だと思います。笑 神田(PdM人格) ビジネス価値・ユーザー価値の観点で要件を整理します。ユーザーが迷わず目的を達成できるかどうか、価値が毀損される可能性はないか、といったトピックに特に関心を払います。 袴田(エンジニア人格) 技術的な実現方法・境界値・制約条件を分析します。アプリケーションコードの実装リスクや、インフラの制約から来る潜在的なリスクに特に関心を払います。 秋山(QA人格) テスト観点・エッジケース・仕様の裏や隙間といったものに注目します。正常な動作だけでなく、意図と違う行動をされたときの挙動や、インシデントにつながりかねないリスクに特に関心を払います。 以下は一例ですが、たとえば、こんな感じで書いています。 おおまかな下書きはClaudeに書いてもらっています 設計のポイント こだわったのは、エージェントの定義を「誰が(ペルソナ)」と「何をするか(ワークフロー)」に分けた点です。 エージェント自体はあくまで汎用的なペルソナとして定義し、具体的な作業はプロンプト側で指定します。こうすることで、同じエージェントをいろんな場面で使い回しができるようになります。 また、それぞれのエージェントが参照すべきドキュメントについては、あえて変えるようにしています。同じものを全員が参照できてしまうと、アウトプットにバリエーションが出ないかなというところで、分散させるという選択をしています。 誰が何を見られるなどを考えた雑な図 最終的には、それぞれ一時アウトプットを出すような形にしていて、メインセッションでそれらを統合して全体としてのアウトプットを出すことにしています。 おためしで動かしてみた 試しに、Playwrightの公式サンプルとして有名な TodoMVC を題材に、3人に同じアプリを別々の視点で分析させてみました。 神田さん(PdM人格) は、ユーザーストーリーやハッピーパス・エッジケースといった表現で整理してくれました。テストすべきものとして、実際の操作でありそうなパターンや、見た目の挙動みたいなところにフォーカスが当たっています。 受け入れ基準を出してくるあたりPdMっぽい 袴田さん(エンジニア人格) は、データ永続化やバリデーションといった観点で整理してくれました。テストすべきものとして、ページリロードや状態遷移といった、技術的に何か変化がありそうなところにフォーカスが当たっています。 実装で気を遣いそうな部分を見ている雰囲気 秋山さん(QA人格) は、ちょっと意地悪な視点からのシナリオを出してくれました。テストすべきものとして、異常値や境界値、アクセシビリティ、非機能要件といったところにフォーカスが当たっています。 テスト観点の提示数は一番多かった サブエージェントなしとの比較 サブエージェントなしでも同じことをしてみました。 テストケースだけで言えば、サブエージェントなしで31件、サブエージェントありで43件となりました。 左が素の状態、右がサブエージェント利用の状態です 数だけが重要というわけではないですが、見比べてみると、ハッピーパス外のことに意識を払っていたり、技術的な裏側まで見ていたりする部分があり、非常に興味深いです。 やってみてわかったこと 3人がそれぞれ独立して分析することで、アウトプットの形式はだいぶばらつきますが、全部をまとめると、視点のバランスがとれながらも、ちょっと突っ込んだ調査ができたアウトプットになりそうです。「テスト」という言葉でひとまとめにしていた作業が、PdMとして考えること・エンジニアとして考えること・QAとして考えることとして分解されることで、AIとして動きやすくなっているのかもしれません。 また、冒頭話題にしていたコンテキスト消費についても、この例では25%程度に抑えられており、実運用に投入して様子が見られそうという雰囲気があります。 今後やりたいこと 今後は、発生頻度が比較的高めの業務でどこまでやれるかから見ていこうと思っています。 幸いにして、キャディはいろんな意思決定をドキュメントに残す文化があるので、インプットに使えるものは多くあります。 最終的には、テスト観点のたたきを出してみたり、仕様書を取りまとめてみたりなど、いろんな業務に適用できるといいなと思います。 この記事がどなたかの参考になれば幸いです。 現場からは以上です! *1 : 自社の記事にはなりますが、 こちらの記事 は自分もすごく勉強になりました
この記事は CADDi Tech/Product Advent Calendar 2025 14日目の記事です。 Executive Summary 生成 AI アプリで評価プロセス改善 PoC をした 評価制度をアセット化し、生成 AI ツールを組み合わせることによって、評価プロセスを支援した 「メンバーの思考の整理」「メンバーからマネージャーへのコミュニケーションの改善」というポジティブな効果が得られた (画像は実際のシステム、入力されているテキストは架空の人物・チーム・業務) はじめに キャディのエンジニアリングマネージャーの橋本です。 突然ですが、この記事を読んでくださっている皆さんにひとつ聞きたいことがあります。あなたの会社にある評価プロセス、たとえば目標設定、月次振り返り、期末の自己評価、評価フィードバックなどは、あなたの仕事にとってポジティブに働いているでしょうか? 評価プロセスと業務はつながっていないこともある この質問に対する回答は、人によってグラデーションが出やすいものだと思います。「もちろん Yes さ、目標を設定することで集中できるし、フィードバックによって最高の成長機会を得られているよ!」という人もいれば、「うーん、どちらかといえば No かな。評価って時間を取られるけど、あんまり業務に対してポジティブに働いている実感はないんだよね」という人もいると思います。 私自身、エンジニアリングマネージャーになる前は典型的な後者のタイプで、正直に白状すると「評価って面倒だな」と思いながら、会社の評価プロセスに従って毎期の評価を受けていました。 そんな中、ここ1~2年は生成 AI の登場によって、エンジニアリングにおける設計やコーディング、サービス運用における働き方は大きく変化してきました。 今や生成 AI は設計の頼もしい相棒ですし、コーディングに関しては業務のあり方そのものが変わりつつあります。サービスの運用においても、生成 AI のポテンシャルはすでに広く知られているところであります。 生成 AI がエンジニアリングを変える、マネジメントは? エンジニアリングマネージャーになった私にとっては、生成 AI のマネジメント領域への適用については非常に興味深いものでした。特に評価の領域については、前述のとおり、エンジニアリングマネージャーに就任する前からぼんやりとした課題感を持っており、取り組んでみるのにちょうどよい題材だと考えました。 この記事では、エンジニアリングマネージャーというエンジニアでありかつマネージャーである私が、マネジメントの課題を生成 AI とエンジニアリングの力で解消しようとした、そんな試みを紹介します。 課題領域 どうして評価プロセスがしばしばエンジニアにとって自分の仕事を支援してくれるものと感じられないのでしょうか? 評価プロセスはエンジニアリングより手触り感が低い 要因はいくつかあるとは思いますが、ひとつの大きな仮説として、エンジニアリングで取り扱う題材にくらべて評価プロセスの “手触り感” が薄い、ということを仮説として立てました。 すなわち、エンジニアリングの業務は実行される行為を思い浮かべることが容易であるのに対し、評価プロセスにおける記述は実際の行動を想像することが難しいという特徴があり、エンジニアリングの業務と結びつけられていないのではないか、という仮説です。 実際、上の画像にキャディの評価基準からの抜粋と、実際の業務でありがちなタスクを並べてみましたが、実際の業務タスクと比べ、評価基準が抽象的で手触り感がないことが確認できると思います。 では、なぜ評価プロセスはこんなにも手触り感がないのでしょうか?私はその原因を、会社の外と中のそれぞれに分けて考えてみました。 評価制度の手触り感を一般知識と会社特有の観点に分解 まず、会社の外側にある理由として、「評価制度」に関する一般知識が説明から省かれがちであるこということが挙げられます。 たとえば、キャディ Tech では、「5軸に沿ったコンピテンシー評価」を導入しています。評価制度のプロであれば、この説明文からこの評価制度がどういう設計なのか、すなわち コンピテンシー評価とは何か 他の評価手法、例えば成果評価とは何が違うのか どのような課題を解決する評価制度なのか メンバー、マネージャーがそれぞれ注意するべきことがなにか ということが想像できることでしょう。(まるでソフトウェアエンジニアがマイクロサービスアーキテクチャについて生き生きと語るときのように!) しかし、実際にはエンジニアは評価制度のプロではないので、これらの知識を必ずしも有しているわけではありません。 また、会社の内側にある理由として、評価制度はその会社固有の価値観や信念を取り扱うものであるから、というものがあります。評価制度は、その会社にどのような人が集まるか、どのような行動が促進されるかに強い影響力を持ちます。そのため、より良い評価制度を設計すればするほど、評価制度にはその会社の個性が出やすいという特徴があります。別の言い方をすれば、同じ「ソフトウェアエンジニア」という職種であっても、ある会社と別の会社では評価体系が全然違うということすらありえるのです。 結果的に、評価制度はしばしばハイコンテキストなものになり、過去の経験による知識による解釈を難しくします。その会社に新しく入社したメンバーにとって解釈することが難しいのはもちろん、ずっとその会社にいたメンバーにとっても、事業の特性やフェーズが変わることによって評価制度はしばしば影響を受けるため、正しく評価制度を理解しつづけることは難しいという特徴があります。 解決領域 既存のアプローチ メンバーとマネージャーの間の対話によって知識差分を埋めてきた 課題領域に対する従来のアプローチでは、業務を深く理解しているマネージャーが、メンバー一人ひとりに合わせたコーチングを通じて会社全体に理解と納得を作り上げていくというアプローチが取られてきました。 この方法は、うまくいくケースがある一方で、以下のような課題があります 効率性の課題:マネージャーがメンバーのことをよく知る必要があり、メンバー・マネジメント双方のリソースを使う 実効性の課題:マネージャーに高い対人スキルと業務理解を要求するため、実効性にばらつきがある 従来のマネジメントは効率性・実効性に課題があることも ちょうどこの課題は冒頭に紹介した、 評価って時間を取られる → 効率性が低い あんまり業務に対してポジティブに働いている実感はない → 実効性が低い という実感とも繋がってきます。 生成 AI を統合したアプローチ - 概要 マネジメントの代わりに生成 AI と社内資料の整備を組み合わせる 既存のソリューションの課題に対し、私は生成 AI を活用することで「一人ひとりの状態に合わせたコーチング」を実現し、評価制度に関する一般知識、およびメンバーごとの理解度の差分を埋められるのではないかと考えました。 ただし、生成 AI では会社固有の知識を取り入れることができないため、合わせて生成 AI がアクセス可能なアセットを作ることを実施しました。 「生成 AI がアクセス可能なアセット」とは 生成 AI がアクセス可能なアセットとはなんでしょうか?これはただの社内ドキュメントとは違うのでしょうか? 生成 AI の活用を考える際に、最も重要な制約のひとつがコンテキストウィンドウです。LLM(Large Language Model; 生成 AI の基盤となる機構)には、特定の長さの文章までしか文脈を正しく捉えられないという課題があります [1]。コンテキストウィンドウとはあるモデルが捉えられる経験上の文脈の長さを示しており、例えば ChatGPT 4o では約13万トークンまでは妥当に取り扱えるとされています [2]。コンテキストウィンドウの制約は、LLM への入力を無制限に大きくできるわけではないということを示唆しています。 コンテキストウィンドウの制約を回避する方法は今までに多く研究されており、その代表的なもののひとつが AI エージェントです [3]。AI エージェントはデータ取得と推論を交互に繰り返すことによって、必要な情報を必要な分だけ取得し、必要な分だけ覚えておくことを実現し、アセットを効果的に利用することができます。 必要な情報だけを取得することで限られたコンテキストウィンドウを活用 AI エージェントの選定 では AI エージェントにとって優しいアセットはどのようなものでしょうか? 実際に PoC (proof of concept; 不確実性が高い部分を切り出してクイックに検証をすること)アプリケーションを作る際には、まず利用する AI エージェントを選定し、その AI エージェントにとって優しいアセットを作ることにしました。 今回は AI エージェントとして、すでに開発業務で使っていた Cline を使用することにしました。Cline はテキストエディタである VSCode の拡張機能で、通常コーディングの AI 支援として使われます。 今回 Cline を AI エージェントとして採用したのは、今回の取り組みがうまくいくかどうか不確実性が高く、クイックにユーザーを巻き込んだ検証を行うため すでに活発に開発・検証されたコンテキストエンジニアリング技術を利用する ユーザーとの対話インターフェースをすでに提供してくれているアプリケーションを利用する ユーザーもエンジニアなので開発業務で使っていた Cline を活用することに障壁がない という観点で利点が大きいと考えたためです。 評価制度アセットの構築 Cline は標準でディレクトリ内部のテキストファイルを解析し、検索してくれます [4]。既存の社内のアセットは Confluence 上に記載されたドキュメント Google Docs 上に記載されたドキュメント Google Spreadsheet 上に記載されたドキュメント があったため、これらをすべてマークダウンに変換し、Git レポジトリとして管理をすることにしました。Confluence、および Google Spreadsheet 上のドキュメントは Google Docs に貼り付けることができ、Google Docs はそのままマークダウンに変換できるため、マークダウンへの変換は容易に行うことができました また、元ファイルに対してコピー操作を行うため、新たに作成されたアセットについては、 インポート日 インポート作業者 元資料の URL を YAML フロントマターを利用して付与しました。この操作によって、アプリケーションの利用者がアセットが古くなっていた場合に自動的に最新の情報を確認し、必要に応じて更新できるようにしました。 分散した社内資料を Git repo に集約し管理する 具体的には以下のような YAML フロントマターがすべての資料の冒頭に書き込まれています。 --- title: "評価軸定義" import_date: "2025-06-24" source_url: "https://docs.google.com/spreadsheets/d/..." imported_by: "システム管理者" version: "1.0" --- # 評価軸定義 ... また、より AI エージェントが関連するドキュメントを探索できるように、アセットの追加時には関連資料を末尾に追加するようにしました。 AI エージェントが資料を連鎖的に辿れるようにアセットを作る 例えば評価制度の FAQ アセットについては、以下のように評価制度の概要や、評価軸の定義に対するドキュメントへのリンクを記載しました。 --- title: "評価制度FAQ(よくある質問と回答)" import_date: "2025-06-24" source_url: "https://docs.google.com/spreadsheets/d/..." imported_by: "システム管理者" version: "1.0" --- # 評価制度FAQ(よくある質問と回答) ... ## 関連資料 - [HELIX制度概要](helix-system-overview-2025.md) - [Career Track別の期待活躍イメージ](career-track-expectations-2025.md) - [評価軸定義](evaluation-axis-definitions-2025.md) - [Track別・Grade別期待活躍レベル一覧](track-grade-expectations-matrix-2025.md) - [Helix目標設定ガイド](helix-goal-setting-guide-2025.md) アセットの構築上の工夫 上記の YAML フロントマターの設定や、メタデータの更新を人力で行うと必ず抜け漏れが発生します。 特に、今回はアセットを作成したり、管理したりするために、エンジニアリングマネージャーである私だけでなく、Tech HR(Human Resource; 人事のこと)にも協力してもらいました。 そこで今回アセットを構築する際には、コミット前に AI エージェントにレビューを行わせ、自動で付け加えられる場合には AI エージェント自ら追記し、そうでないメタデータについては修正を要求するような工夫を行いました。 すなわち、AI エージェントを利用者だけが使うのではなく、アセット開発者も活用できるようにすることで、アセットの品質を保つようにしました。 AI エージェントに対する指示 AI エージェントをさらに有効に働かせるために、初期プロンプトを自動で与える Cline rules [5] も活用しました。 Cline では初期プロンプトをカスタマイズでき、またその組み合わせも変えることができます。 今回はアセットを作る人と使う人、それぞれで注目するべきポイントが違うため、Cline rules を複数作成し、Cline rules の切り替え機能を活用することで、アセットを作る人も使う人も使いやすい環境を整えました。 使う人のペルソナに応じたプロンプトを事前に用意 特にメンバー向けの Cline rules には以下のような指示を記載したことによって、AI エージェントに期待していることを明示し、また AI エージェントが回答できる限度を超えている場合には、適切にエスカレーションされるように配慮しました。 ### 評価アシスタントとしての心構え - 中立性の維持: 特定の評価結果に偏らない客観的なアドバイス - 継続的な学習: 評価制度の変更や更新に対応 - 実用性の重視: 理論だけでなく実践的なアドバイスを提供 ## サポートが必要な場合 - 評価制度の詳細: 人事担当者に確認を依頼 - 複雑な目標設定: 上司やメンターとの相談を推奨 結果 実際の PoC アプリケーションの挙動 以下は私が架空のエンジニアを想定し、自己評価の支援を依頼したときの実際のシステムの挙動です。 ユーザーが「自己評価の支援をして」と入力する AI エージェントがエントリーポイントとなるドキュメントを読み込む AI エージェントが関連するドキュメントを辿ってさらに読み込む AI エージェントがユーザーを対話的にコーチングしながら実務に連動した評価プロセスを提供する どのような支援が必要かをヒアリング 評価対象期間を特定する 役職・職位を確認する ユーザーの現状に合わせて対話を続ける。この後に複数の自由記述を含むステップが入る 最終的に自己評価として以下のような markdown が出力される 評価期間: 2025年4月~2025年9月 グレード: xxx 所属: Tech本部 xxx Team --- ## エグゼクティブサマリー FY25Q1-Q2期間においては、検索領域における機能拡充として、xxx の開発をリードしました。要件定義から設計、実装、デリバリーまでの全フェーズを通じて、複数チーム(xxx Team、xxx Team、xxx Team)を横断的に率い、技術的な不確実性を段階的に解消しながらプロジェクトを成功に導きました。 ... --- ## 評価軸別の実績と自己評価 ### 1. Expertise(技術力・専門性) #### 実績 本プロジェクトでは、以下の技術的な課題に対して高度な専門性を発揮しました: 技術的課題の特定と解決 - 既存の検索インデックス(Elasticsearch)のアーキテクチャを分析し、複数データソースを統合した新しいインデックス設計を主導 - ... ... #### 自己評価(ノッチ: x2) MG4 の評価基準である「大きなチームや他チームも関係するプロダクトに対して、全体的な構想を描き、設計することができる」 、... の要件を満たしていると考えます。 一方で、 ... については今後の課題と認識しています。 --- ### 2. Delivery(価値創造・提供) ... --- ## 総合評価と今後の成長課題 ### 強みとして発揮できた点 1. 技術的専門性とリーダーシップの両立: 検索技術の専門性を活かしながら、複数チームをリードする役割を果たせました ... ### さらに伸ばせる可能性がある点 1. 定量的な評価指標の活用: プロセス改善において、より定量的な指標(DORA metrics等)を活用した継続的改善サイクルの確立 ... ### 次期(FY25Q3-Q4)に向けた改善アクション 1. 本部戦略への貢献: 本部全体の技術戦略策定プロセスに積極的に参加し、検索領域以外の知見も獲得 ... --- ## 証跡・参考資料 - プロジェクト計画書: [Confluence](https://example.com) ... メンバーからの定性的なフィードバック 今回、実験的に半期の評価サイクルにあわせて、実際の評価プロセスで PoC アプリケーションを利用可能にしユーザーのフィードバックを収集しました。 対象はエンジニアリング組織全体で、利用は任意とする形式で運用しました。最終的に、全体の約3割のメンバーが実際に利用しました。 利用したメンバーから、ポジティブなフィードバックとして以下のようなものがありました 自己評価を書くときの初動ハードルが下がった 自分では気づいていなかった観点(協働や影響範囲など)を指摘してくれるのがありがたい 全体的に、文章の整形よりも思考の整理についてポジティブなフィードバックが多かったことが特徴的でした。AI が自分の行動を、評価の考え方に沿って構造化してくれるため、「何を書けばいいか」が明確になり、記述の負荷が軽減されたという声が多くありました。 一方で、以下のようなフィードバックも多く見られました。 AIが出してくれた文章をたたき台として修正する形がちょうどいい 実際、生成結果をそのまま使う人はほとんどおらず、多くの利用者が「AI の出力を土台にして、自分の言葉にリライトする」スタイルをとっていました。これは、生成 AI が解決できる課題として、文書化そのものよりも、知識へのアクセスを補助する部分が大きいという当初の仮説を支持する結果だと考えられます。 マネージャーからのフィードバック また、メンバーが PoC アプリケーションを利用したマネージャーからもフィードバックを得ることができました。 全体的にポジティブな内容が多く、 各評価軸に沿った構造的な書き方をされていて読みやすかった 行動と成果のつながりが明確でわかりやすかった トーンや文体が統一されており、比較しやすかった というフィードバックが得られました。これまでは文章表現の違いによって、評価プロセスのアウトプットの解釈が割れることがありましたが、一定のフォーマットでアウトプットが整理されることで、よりコンテンツの議論に集中できるようになったものだと考えられます。 今後の課題 PoC アプリケーションの実験的な導入によって見えてきた課題もありました。 利用者は全体の約3割にとどまり、まだ十分に浸透していない 評価制度そのものの記述や記載が曖昧な場合、AI エージェントの出力もぶれる つまり、AI が整理してくれるのは「構造化」と「言語化」の部分であって、評価の根幹にある制度設計やマネジメントの解像度については、アセットの継続的な改善を要するということがわかりました。 また、浸透課題については、体験の改善が必要だと考えられます。現状ではエディタと拡張機能のインストール、さらには Git レポジトリからのアセットダウンロードをすべてメンバー自身が実行する必要があるため、利用開始のハードルが高いという課題が見られました。PoC によって利用をすることによって得られるベネフィットが明らかになったため、踏み込んで社内サーバにホスティングし、環境構築を不要にしていき、より多くのメンバーが活用できる環境を作っていこうと考えています。 まとめ 今回の記事では、私たちが取り組んだ評価 x 生成 AI PoC、すなわち評価制度を AI が利用可能なアセットとして整備し、それらを適切に連携させることが、評価プロセスの効率化と質的向上に効果的であったことをご紹介しました。 実は、今回ご紹介した、AI の力で「ハイコンテキストな情報を構造化し、活用する」アプローチは、キャディの製造業領域におけるビジネスやプロダクトにおいても核となる考え方です。 AI と情報資産の連携は、まだまだこれからどんどん発展していく領域です。もし今回のブログでキャディってどんな会社なんだろう?と興味を持っていただいた方は、ぜひカジュアル面談で一緒にお話ししましょう! https://open.talentio.com/r/1/c/caddi-jp-recruit/pages/78398 参考文献 [1] Hahn, M. (2020). Theoretical Limitations of Self-Attention in neural sequence models. Transactions of the Association for Computational Linguistics, 8, 156–171. [2] OpenAI Platform . Available at: https://platform.openai.com/docs/models/gpt-4o (Accessed: 09 December 2025). [3] Yao, S., Zhao, J., Yu, D., Du, N., Shafran, I., Narasimhan, K., & Cao, Y. (2022). ReAct: Synergizing Reasoning and Acting in Language Models. arXiv preprint arXiv:2210.03629 [4] Cline-tool guide. Available at: https://docs.cline.bot/exploring-clines-tools/cline-tools-guide (Accessed: 09 December 2025). [5] Cline Rules. Available at: https://docs.cline.bot/features/cline-rules (Accessed: 09 December 2025).
こんにちわ、Core Infrastructure チームの前多です。膝が痛い。 こちらは キャディ株式会社のアドベントカレンダー の3日目の記事です。 先日、弊社の同僚からCADDiのアーキテクチャと開発組織に変遷に関する発表が行われました。 14:55〜E会場 キャディ株式会社/CADDiの発表資料 「事業状況で変化する最適解。進化し続ける開発組織とアーキテクチャ」を公開しました🙌 よろしければお手元でもご覧ください! https://t.co/DrStp16fon #アーキテクチャcon_findy — CADDi.tech (@CaddiTech) 2025年11月21日 私たちのプロダクトのインフラは Terraform で構成しています。 プロダクトがロンチされてから3年以上経っていて、その発展に従ってTerraformの構成も大きく変化してきました。 この記事ではプロダクトのTerraformがどのように変化してきたかを紹介していきます。 というのは建前で、どこかでTerraformネタで発表しようと思って溜めていてたネタだったんですが、機会がなかったのでここで記事にしました。 CADDi Drawer 初期(2021-2022) 図面管理SaaSとしての基本的な機能を作ってロンチした頃。 この頃は、SaaSの機能は少なくTerraformの構成も次のようにシンプルなものでした。 terraform ├ environments │ ├ dev │ │ ├ main.tf │ │ └ variables.tf │ ├ stg │ └ prod └ modules   ├ cloudsql   │ └ main.tf   ├ iam   ├ gke   ├ gcs   ├ network   ├ pubsub   └ secret environments は terraform のapply対象、state管理の対象となるルートモジュールで、ここでGoogle Cloudのプロジェクトや環境ごとのパラメータを持っています。 moduels配下は、ルートモジュールから参照されるもので、Google Cloud に作成する実際のリソースを管理しています。 当時はモジュールは、Google Cloudの機能相当で分割していたようです。 networkモジュールでVPCやサブネットを、gkeモジュールでGKEクラスタやノードプールを、のようにインフラの共通リソースの定義から始まって、 それを踏襲して Cloud Pub/Subが欲しくなったので pubsubモジュールを、のようにモジュールをクラウドの機能単位で作っていました。 (後から振り返りますが、これはモジュールの作りとしてはあまり良くはないことがわかってきます) また当時から、このリポジトリには Terraformと Terraform Providerの更新を自動化する仕組みや、PullRequestのステータスに合わせて Plan/Applyを自動化する仕組みを導入していました。 これがあったからこそ、後続のリファクタリングがうまくいったと言っても過言ではありません。 では次にどうなったのかを見てみましょう。 CADDi Drawer 成長期(2023-2024) 機能強化やCADDi Quoteなどのプロダクトの追加といった様々な追加開発を行なっていた頃です。 開発に関わるメンバーも増え、チーム体制を取ったりと組織面でも大きな変化があった時期です。 この頃の Terraform の構成はおおよそ次のようになっていました。 terraform ├ environments │ ├ dev │ │ ├ main.tf │ │ └ variables.tf │ ├ stg │ └ prod └ modules   ├ cloudsql   │ ├ main.tf   │ └ service_a.tf   ├ bigquery   ├ iam   │ ├ main.tf   │ └ service_a.tf   ├ gke   ├ network   ├ pubsub   ├ gcs   │ ├ main.tf   │ └ service_a.tf   ├ secret   │ ├ main.tf   │ └ service_a.tf   └ some_saas ディレクトリ構成的にはあまり変わっていません。 Google Cloud で利用するAPIの追加に従ってモジュールが増える他、 この頃には Google Cloud以外の外部SaaSも使い始めてその管理用のモジュール(some_saasとしておきます)も追加されました。 そして、Google Cloud の機能単位で作成された pubsub,secret,iamなどのモジュールは 複数の開発チームが相乗りして、それぞれ必要とするリソースを追加していました。 この状態のまま、Terraform のコードが増えていったため、次のような困りごとが出てくるようになりました。 1つの修正で複数モジュールを修正する必要がある Google Cloudのリソース同士は依存性を持つことがあります。最も良くあるのが、リソースに対してサービスアカウントのIAM Roleを付与するパターンです。 この場合、リソース単位でまとめたモジュールだとモジュール間の依存性が生まれます。 GCS バケットとサービスアカウントを作成してIAM Roleを割り当てるには現状のモジュール構成だと以下のようになります。 iamモジュール内でサービスアカウントを設定して、memberをoutputで返す。 resource "google_service_account" "some_sa" { account_id = "some_sa" } output "some_sa_member" { value = resource.google_service_account.some_sa.member } gcsモジュールでGCSバケットを作成し、variableでSAメンバー名を受け取り、IAMロールを付与する # gcs module resource "google_storage_bucket" "some_bucket" { name = "some_bucket" project = var.project_id location = "ASIA-NORTHEAST1" force_destroy = false } resource "google_storage_bucket_iam_member" "iam_member_example" { bucket = google_storage_bucket.some_bucket.name role = "roles/storage.user" member = var.some_sa_member } ルートモジュールでmodule間のoutputとvariableを渡します。 module "iam" { source = "../../modules/iam" } module "gcs" { source = "../../modules/iam" # iam moduleの outputのSA memberを渡す some_sa_mamber = module.iam.some_sa_member # リソースが追加されるたびに variableが増えていく hoge_sa_member = module.iam..... } とある開発チームが、GCSバケットと権限を設定するためには、二つのモジュールを修正し、モジュール間のパラメータの受け渡し(outputとvariable)を追加する必要があります。 こういったことがGCSやPub/Subなど様々なリソースで起きるので、何かしらの変更が起きるたびに複数のモジュールにまたがる修正が必要でした。 複数のリリース対象が混じっている Google Cloudと 他のSaaS のTerraform 構成が一つのstateに混在した結果、SaaSのみをアップデートしたくてもGoogle Cloud側のリソースの修正も混じっていてリリースタイミングの調整が必要になることがありました。 このようなことから、今後更なる機能追加の障害になると考え、モジュール構成の見直しとstateの分割を検討しました。 モジュール構成の見直し 一般的なプログラミングにおける良いモジュールとは、モジュール間の依存が少なく、モジュールの中には関連が強いものが集まる、つまり疎結合・高凝集であることです。 なんからの修正に対して単一のモジュールのみの修正で済んだり、他のモジュールに影響を与えずにモジュールの追加・削除ができることが望ましい姿であると言えます。 複数の開発チームが同時に開発している状況では、チームが開発している各サービスでリソースをまとめるのが適切だろうと判断しました。 次のようなモジュール構成にすることにしました。 terraform ├ environments │ ├ dev │ │ ├ infra │ │ │ ├ main.tf │ │ │ ├ infra.tf │ │ │ ├ app_a.tf │ │ │ └ app_b.tf │ │ └ some_saas │ │   └ main.tf │ ├ stg │ │ ├ infra │ │ └ some_saas │ └ prod │   ├ infra │   └ some_saas └ modules   ├ cloudsql   ├ network   ├ gke   ├ service_a   │ ├ iam.tf   │ ├ gcs.tf   │ ├ pubsub.tf   │ └ secret.tf   ├ service_b   └ service_c modulesについては、開発チームが作成しているサービス単位でモジュールを作成しそこにそのサービスで使うリソースをまとめます。 ただし、VPCや GKEなどの共通基盤として利用するリソースはそのままです。 ルートモジュールについては、Google Cloud とその他のSaaSについてはこの時点でstateを分けることにしたので、階層を下げました。 Google Cloudのルートモジュールについては単一のtfファイルでモジュールの呼び出しをしていたものを、アプリケーション単位でファイル分割します。 これは将来的にstateを分割することも考慮しています。 こうすることで、前述の GCSバケットとIAMの設定については同一モジュール内で定義が済むことになります。 resource "google_service_account" "some_sa" { account_id = "some_sa" } resource "google_storage_bucket" "some_bucket" { name = "some_bucket" project = var.project_id location = "ASIA-NORTHEAST1" force_destroy = false } resource "google_storage_bucket_iam_member" "iam_member_example" { bucket = google_storage_bucket.some_bucket.name role = "roles/storage.user" member = google_service_account.some_sa.member } outputもvariableも不要になり、すっきりします。 ですが、モジュールの構成を変えるというは単にソースコードを直せば良いというわけではありません。 どうやってこれを達成したかを次に解説します。 同一state内のリソースの移動 Terraformのリソース定義は、名称を変更するだけでも stateとの差分が発生するので リソースの削除と新規作成という結果になります。 これは、Terraformの仕様上しょうがない部分で、stateを tarraform state mv のようなコマンドで直接修正するという方法があります。 developer.hashicorp.com ただコマンドによるstate の変更は、stateを直接更新してしまうので、試行錯誤しながら作業を進めていくのは難しいです。 Terraform 1.1 から moved block, import block, removed block という機能が提供されました。 developer.hashicorp.com これは、stateの変更をしたい内容をルートモジュールに記載しておくことで、変更の結果を加味してplan/apply を行なってくれる機能です。 これを使えば、変更の結果を試行錯誤しつつ作業を進められます。 例えば、前述のgcs, iam モジュールの内容を service_a というモジュールに移動する場合、移動先のモジュールのソースコードを書いて、 次のような moved block を書きます。 moved { from = module.gcs.google_storage_bucket.some_bucket to = module.service_a.google_storage_bucket.some_bucket } moved { from = module.gcs.google_storage_bucket_iam_member.iam_member_example to = module.service_a.google_storage_bucket_iam_member.iam_member_example } moved { from = module.iam.google_service_account.some_sa to = module.service_a.google_service_account.some_sa } movedブロックがある状態で planをすると、モジュールを変更した状態で比較が行われるので基本的に差分は無しになります。 名称のミスなどがあって差分が出た場合でも、安心して修正ができます。 余談ですが、この作業はモジュール内のリソースの一覧を出力するなどしてある程度は機械化できるのですが、結構大変でした。 当時はcopilotが登場したくらいの頃だったので、今ならAIツールでもっと賢くできるかもしれません。 以下の画像が一気にモジュール構成を変えた時のPRのサマリです。この量でもplan結果はほぼ差分なしでした。 補足 import, removed block 基本的には moved ブロックだけで事足りるのですが、作業を進めていく上でいくつか個別対処したことがあります。 1つめは、複数のモジュールで同一のリソースが定義されているというものでした。 IAM ロールを割り振る iam_member リソースは、色々なモジュールで定義されていて、モジュールの変更を見直していたら全く同じ内容が出てきて一つにマージする必要がありました。 単純にまとめてしまうと、片方のiam_memberリソースが削除扱いになるので場合によってはiam_memberが消えてしまう可能性もあります。 この場合、removed blockによってTerraformのstateでだけそのリソースを無かったことにします。 removed { from = modue.iam.google_storage_bucket_iam_member.some_member lifecycle { destroy = false } } destroy = false でGoogle Cloudからはリソースを削除しないとを明示することに注意します。 2つめは、Terraformで管理されていないリソースがある環境でだけあったというもので、これは import block でterraform stateに取り込みます。 import { to = module.service_c.google_service_account.some_sa id = "projects/$ { var.project_id } /serviceAccounts/some_sa@$ { var.project_id } .iam.gserviceaccount.com" } import は import したいリソースごとに id を指定します。idに何を書くかはリソースによって異なるので、ドキュメントを読んで正しいIDを指定することに注意します。 state分割でのリソースの移動 state を分割する場合、 前述の moved ブロックは使用できません。 state mv コマンドでモジュール単位で別のstate に移動していきます。 state の移動元と移動先それぞれで、removedブロック,importブロックを使えばひょっとすると代替できるかもしれません。 しかし import ブロックは上で述べた通りID指定が必須なので移動したいリソースのIDを列挙するのは困難なのでお勧めしません。 stateをまたいだリソースの移動は次の手順で行います。 移動元、移動先それぞれのルートモジュールでstateをローカルにダウンロードして、ローカルのstateを参照する stateまたぎでmoduleを移動する 両方のルートモジュールで plan を実行し、差分がなければローカルstateをリモートにpushする 次のスクリプトで1,2を自動化します。 #!/bin/bash # 移動元ルートモジュールのパス SRC = $1 # 移動先ルートモジュールのパス TARGET = $2 # スペース区切りでルートモジュール内の移動するmodule 名のリスト。 MODUELS = $3 base_dir = $( pwd ) echo " SRC ローカルにstateをダウンロード " cd $SRC terraform init terraform state pull > ${base_dir} / ${TARGET} /src.tfstate echo " TARGET ローカルにstateをダウンロード " cd $base_dir cd $TARGET terraform init terraform state pull > target.tfstate # localのstateを使うように一時ファイルで上書き cat << EOF > override.tf terraform { backend "local" { path = "target.tfstate" } } EOF # ローカルstateを使うようにinit をやり直す terraform init -reconfigure # モジュールリストごとにstateをmove for module in $( tr ' ' ' \n ' <<< ${MODUELS}) do echo " move module. ${module} " # state-out で移動先のstate ファイルを指定する terraform state mv -state = src.tfstate -state-out = target.tfstate module. ${module} module. ${module} done この状態で plan を実施して、差分がないようなら 次のスクリプトで更新後のstateを反映します。 #!/bin/bash SRC = $1 TARGET = $2 base_dir = $( pwd ) echo " TARGET: リモートのstateを使うように設定し直して、state をpushする " cd ${TARGET} # push target state rm override.tf terraform init -reconfigure terraform state push target.tfstate echo " SRC: state をpushする " cd ${base_dir} mv $TARGET /src.tfstate $SRC /src.tfstate cd $SRC terraform state push src.tfstate planのチェックもスクリプトで自動化してしまえば全作業が自動化できそうです。 現在そして将来 これまでの作業で、ある程度モジュールの独立性が確保されたため、Terraformのコード修正は比較的楽になりました。 その結果、Terraform コードが増えたので今は次のような問題を抱えています。 plan/apply にかかる時間が増えている, 3-5分かかっている リソースが増えすぎていて、モジュールやリソースのオーナーがわかりづらくなっている これらの問題についてどのように解決するかは現在進行形ですが、次のように考えています。 stateをアプリケーション単位で分割し、plan/applyを並列化する ルートモジュールごとに default labelを付与して、生成されるリソースのオーナーがわかるようにする stateを分割すると、state間の情報共有をどうするかやapplyの順序といった問題が出てきます。 多分完璧なやり方はないだろうと思っているので、ある程度の妥協をしつつ進めていくのかなと思っています。 何か良いアイデアをお持ちの方はぜひ教えてください。 まとめ モジュールは関連の強いリソースでまとめましょう。そして関連は技術的な軸ではなく開発組織の軸で考えましょう そこが思い浮かばないなら、無理にモジュールにしなくても良いです モジュールの構成をしくじっても、どうにかなります。気合と根性で解決したことも今ならAIで楽になるはず moved blockが使えなければ詰んでいたので、Terraformのアップデートは運用に組み込みましょう これを見ているあなたもぜひ、我が家のTerraformの歴史を公開してみてください
こんにちは、柴犬がかわいい。Tech本部の前多です。 先日、弊社でApache IcebergとTrinoによる活用事例についての記事を上げました。 caddi.tech 記事では、Icebergへのデータ投入について次の記述がありました。 ユーザがアップロードしたCSVファイルをパースしてIcebergに保存する 図面の解析結果を一定間隔のバッチで受け取りIcebergに保存する 実際のところ、ファイルからIcebergへのデータ投入はサイズによっては困難なことがありました。 今回はIcebergへのデータ投入に関するTopicをお伝えします。 データ投入で発生した課題 私たちは、クエリエンジンとしてTrinoを採用しています。 データ投入の経路はCSVファイルしかないので、CSVファイルを解析して一行ごとに TrinoのInsert文 を発行すれば十分だろうと考えていました。 また、TrinoのInsert分は以下のような複数行の一括投入も可能なので、それである程度効率よく処理ができるだろうと踏んでいました。 INSERT INTO iceberg.some_schema.some_table VALUES ( 1 , ' test1 ' ), ( 2 , ' test2 ' ),,,,,; 少量のデータでは、この方法でも問題はありませんでした。 しかし性能テストのために1000万件程度のデータを投入しようとし始めた時から、次の問題がでてきました。 1. 時間がかかりすぎる テストデータの投入を前述のtrinoの複数行INSERTを使って、10行から200行の範囲でまとめて挿入する方法で当初行っていました。 10万件程度の投入はおおよそ15分程度で終わっていたので許容範囲だと思っていましたが、 100万件の投入を超えたあたりからどんどん一度のINSERTにかかる時間が伸びていくようになりました。 Icebergのメタデータファイルの増加、GCSの負荷増加、Trinoクラスタの負荷増加などさまざまな理由が考えられますが、Trinoで連続したデータ投入を行うのは難しいのではと思い始めました。 Trinoクラスタのスケールアップなどにより改善した可能性はありますが当時は後述する別の手段を採用しています。 2. Iceberg メタデータが増え続ける Icebergはテーブル単位のレコード操作についてトランザクションのサポートがあり、トランザクションごとにデータファイルやメタデータ、マニフェストファイルが作成されます。 これは、細かいトランザクションを何度も行うとメタデータファイルが肥大化していきます。 Icebergには、古いメタデータファイルをコミット時に破棄する write.metadata.delete-after-commit.enabled というオプションがあるのですが、 これは現時点ではTrinoでサポートされていません。Issueはありますが、まだ進行中です。 iceberg.apache.org github.com 数百万件のレコードを投入した時点で、メタデータファイルは何度もInsertを繰り返した結果100MBを超える状態となっているものもあり、 これがデータ投入が遅くなった要因の1つであったと考えます。 なるべく一度のトランザクションでデータをまとめて投入する、メタデータファイルをメンテナンスするなどの必要性がわかりました。 3. ファイル単位のトランザクション制御ができない Trinoはトランザクションに関するSQLはありますがほとんどのコネクタではサポートされていません。 SQL statement support — Trino 474 Documentation Trino Iceberg connectorも同様で、原則的にauto commitで動作します。auto commit以外を設定するとエラーになりました。 そのため、Icebergに対する複数のSQL実行に対するトランザクションはなく、Trinoでは1回のINSERTでIcebergのトランザクションとなります。 よって、ファイルの各行を分割してINSERT文を発行すると、細かいコミットが詰まれていくので、ファイルデータの途中にエラーがあって処理を停止した場合、Icebergには中途半端なデータが残ったままになります。 ただしこの仕様は事前に把握していました。 そこで、今回は投入するデータに投入元のファイルIDを持たせて、中途半端なデータは後から削除できる仕様としています。 そのため、大きな問題にはなりませんが、できるならファイル単位でIcebergへのデータ投入が成功したか失敗したかのどちらかになっているのが望ましいです。 このように、私たちのケースのようなそこそこのサイズのファイルをIcebergに投入するにあたって、Trino経由のデータ投入では扱いづらいことがわかってきました。 Trino以外の手段ではApache Sparkを使うのが王道だったと思いますが、当時Trinoに加えてSparkクラスタも構築するのは現実的ではありませんでしたし、上記全ての問題が解決するのかはわかっていませんでした。 そこで、IcebergのJava APIを使用して直接Icebergにデータを書き込むことにしました。 なお、余談ですがその時に Apache Beam® (Google Cloudのマネージドサービス、Dataflowの中身)も使えないかを見ていました。 確認したところApache BeamのIcebergサポートはバッチモードではレコード1件につき1コミットとなるようで、今回の要件にはマッチしないと判断しました。基本的にはApache Beamはストリームで扱った方が良さそうです。 Iceberg Java APIについて Iceberg Java APIはIcebergテーブルフォーマットに従ったデータファイル、メタデータ、マニフェストファイルを作成し、Catalogと連携してファイルのコミットを行ってくれるライブラリです。 あまり解説されているサイトは少ないのですが公式や、Tarbularのブログのほか日本での事例解説があり、参考にさせていただきました。 iceberg.apache.org www.tabular.io knowledge.sakura.ad.jp 今回解説するソースコードの全量は こちら にあります。 Docker compose, テストコードもあるので手元で試せます。 Catalogの取得、テーブルの作成 まずは、Catalogを取得します。 今回はREST Catalogを使用し、CatalogのURIやオブジェクトストレージの認証情報を設定して初期化します。 public static RESTCatalog getCatalog(String catalogUri) { var catalog = new RESTCatalog(); Map<String, String> catalogConfig = new HashMap<>(); catalogConfig.put( "type" , "rest" ); catalogConfig.put( "uri" , catalogUri); // TODO , 実際の環境に合わせて設定内容を変えること catalogConfig.put( "io-impl" , "org.apache.iceberg.aws.s3.S3FileIO" ); catalogConfig.put( "s3.endpoint" , "http://localhost:9000" ); catalogConfig.put( "s3.path-style-access" , "true" ); catalogConfig.put( "s3.region" , "us-east-2" ); catalogConfig.put( "s3.access-key-id" , "admin" ); catalogConfig.put( "s3.secret-access-key" , "password" ); catalog.initialize( "rest" , catalogConfig); return catalog; } Catalog経由でIcebergテーブルを操作します。 テーブルの取得や作成、スキーマ変更などができます。 テーブルを作る場合はスキーマやパーティションなどの定義が必要で、今回は4項目を持つスキーマを用意します。 /** 4項目を持つテーブルのスキーマの例 */ public static final Schema SCHEMA_SAMPLE = new Schema( List.of( Types.NestedField.required( 1 , "id" , Types.UUIDType.get()), Types.NestedField.required( 2 , "name" , Types.StringType.get()), Types.NestedField.required( 3 , "price" , Types.IntegerType.get()), Types.NestedField.required( 4 , "registered_at" , Types.TimestampType.withZone()))); /** name属性のハッシュ値によるパーティションの例 */ public static final PartitionSpec SAMPLE_PARTITION = PartitionSpec.builderFor(SCHEMA_SAMPLE) .bucket( "name" , 16 ).build(); Catalogにスキーマ、パーティション、テーブルプロパティなどを設定してテーブルを作成します。 テーブルのオブジェクトストレージ上のパスも自分で決めます。論理的なテーブル名と同じにしてしまうとリネームや名前の衝突などで困るため、ハッシュ値などを含めた方が良いでしょう。 // namespaceの取得 var ns = catalog.loadNamespaceMetadata(Namespace.of(namespace)); // オブジェクトストレージのテーブルのパス var location = ns.get( "location" ) + "/" + table + "-" + UUID.randomUUID().toString().replaceAll( "-" , "" ); var table = catalog // ネームスペース、テーブル名、スキーマの指定 .buildTable(TableIdentifier.of(Namespace.of(namespace), table), schema) .withLocation(location) // パーティション、ソートオーダーなどの指定 .withPartitionSpec(partitionSpec) .withSortOrder(sortOrder) // テーブルプロパティの指定 .withProperties( Map.of( "write.metadata.delete-after-commit.enabled" , "true" , "write.metadata.previous-versions-max" , "100" , "write.object-storage.enabled" , "true" )) .create(); ここまでが下準備です。次から実際にIcebergテーブルにデータを書き込んでいきます。 シンプルなデータ投入手順 Icebergテーブルはデータファイルやメタデータファイル、マニフェストテーブルから構成されています。 Java APIを使ったプログラムでは、主にデータファイルを作成します。 データファイルに連なるマニフェスファイルやコミットで生成するメタデータファイルについてはAPIやCatalogの内部で隠蔽されているので、あまり意識する必要はありません。 まずはパーティションがないテーブルのようなシンプルな実装例を紹介します。 Catalog, tableがある前提で、トランザクションを開始してAppendオペレーションを開始し、データファイルを作成するための DataWriter を取得します。 var catalog = TableUtil.getCatalog(restCatalogUri); var tbl = TableUtil.getOrCreateTableAndNamespace( catalog, namespace, table, SampleDefinition.SCHEMA_SAMPLE, PartitionSpec.unpartitioned(), SortOrder.unsorted()); // トランザクションを開始して、Appendオペレーションを開始する。 var transaction = tbl.newTransaction(); var append = transaction.newAppend(); // データファイルのパスは自分で決める。ハッシュ、日時などを入れて衝突しないようにする。 var fileId = OffsetDateTime.now().format(DateTimeFormatter.ofPattern( "yyyy-MM-dd_HHmmss" )) + "_" + UUID.randomUUID(); // メタデータ類とは異なるパスに配置されるように /data を含める String filepath = tbl.location() + "/data/" + fileId + ".parquet" ; // DataWriterの取得 var file = tbl.io().newOutputFile(filepath); var dataWriter = Parquet.writeData(file) .schema(tbl.schema()) .createWriterFunc(GenericParquetWriter::buildWriter) .overwrite() .withSpec(PartitionSpec.unpartitioned()) .build(); 上記では、テーブルから newTransaction でトランザクションを開始して、次にトランザクションから newAppend でAppendオペレーションを開始していますが、 オペレーションが1つだけならtableから直接Appendオペレーションを作成できます。 オペレーションの種類は こちら にあります。 Appendはデータを追加するだけの単純なオペレーションで、トランザクション競合も起きません。そのほかに削除、データ更新など複数のオペレーションがありますが、今回のケースはデータ追加だけを行いますので触れません。 興味がある方は以下の記事を参考にしてください。 bering.hatenadiary.com データファイルのパスも自分で決める必要があります。ハッシュ値、タイムスタンプを含めて衝突を避けたり、オブジェクトストレージのパスを分散させて効率を高めたりするなどの工夫は自分で行います。 DataWriterへ、ファイルの一行ごとにParquet形式のデータに変換して書き込んでいきます。 最初に定義したテーブルスキーマのフィールドと型を一致させる必要があります。型変換についてはGitHubのコードを参照してください。 var record = GenericRecord.create(tbl.schema()); try ( var lines = new JsonlReader(input)) { // data add to parquetWriter while (lines.hasNext()) { var r = lines.next(); var row = record.copy(TableUtil.convertRecord(tbl.schema(), r)); dataWriter.write(row); } } レコードの追加が終わったら、書き込みを close で終了させ、その後データファイルに変換した後Appendオペレーションにデータファイルを登録します。 もし、レコードの件数やサイズに応じて複数のデータファイルを生成したい場合は、繰り返しデータファイルの生成を行なってオペレーションに登録します。 最後にAppendオペレーションのcommit、トランザクションのcommitを行うと、Catalogで競合状態を確認します。 問題なければ各種マニフェストファイルが生成、コミットされます。 これで、Icebergのデータ投入は完了です。 // writing finish, then commit data file. dataWriter.close(); var dataFile = dataWriter.toDataFile(); append.appendFile(dataFile); // commit append.commit(); transaction.commitTransaction(); データの取得、確認 ユニットテストでIcebergからデータを取得してみます。 データ取得は Scan で行います。データファイル単位で取得する方法とレコード単位で取得する方法があり、ここでは後者の方法を採用します。 以下のように、 IcebergGenerics.read(table) からselect, whereを指定してScanオブジェクトを取得し、Scanから1件ずつレコードを取得していきます。 var result = new ArrayList<Map<String, Object>>(); var scan = IcebergGenerics.read(table) // select, where の指定ができる //.select("id", "name") //.where(Expressions.lessThan("price", 100)) .build(); for ( var i = scan.iterator(); i.hasNext(); ) { var data = i.next(); var map = new HashMap<String, Object>(); for ( int k = 0 ; k < data.size(); k++) { var field = SampleDefinition.SCHEMA_SAMPLE.findField(k + 1 ); map.put(field.name(), data.get(k)); } result.add(map); } 余談ですが、通常のSQLと異なり、集計、関数、ソートといった操作や結合はサポートされていませんので、こういった操作はクエリエンジン側で行います。これらの操作がコストが高くなる理由がわかります。 テストを実行すると、Icebergへのデータ投入とその確認が検証できます。 また、以下のようにMinioコンソールで作成されたファイルを確認できます。 メタデータファイルと、1件のデータファイルが確認できます。 また、このデータはTrinoからクエリすることももちろん可能です。 Partitionに対応したデータファイルの書き込み 前述の方法は単純で件数が少ないテーブルであれば十分ですが、一方でJava APIが提供する機能はプリミティブなものだと私は感じました。 例えば、パーティションキーごとにデータファイルを分けたり、サイズに応じてデータファイルを分割するのは自分で行う必要があります。 そういった時に役立つのが org.apache.iceberg.ioパッケージの便利クラス です。 Partitionに対応したデータファイルを作成可能な PartitionedFanoutWriter がありますのでこれを使ってみます。 以下のように、appenderFactory, outputFileFactoryを生成してこれをPartitionedFanoutWriterに渡します。 outputFileFactoryはファイルを作成する情報となるpartitionId,taskId,ファイルフォーマットを受け取り、パーティションごとに分割したデータファイルのパスに含めます。 もし対象テーブルにパーティションがない場合は、 UnpartitionedWriter を代わりに使います。 ファイルサイズを指定でき、 UnpartitionedWriter を使う場合でもファイルサイズでデータファイルが分割できるので便利です。 Writerを作った後のレコード挿入はこれまで通りです。 var appenderFactory = new GenericAppenderFactory(tbl.schema()); // 複数プロセスで同時に挿入する場合は、partitionId, taskIdをプロセスごとに分けないと、同名のファイルを作ってしまう。 int partitionId = 1 ; int taskId = 1 ; var outputFileFactory = OutputFileFactory.builderFor(tbl, partitionId, taskId).format(FileFormat.PARQUET).build(); final PartitionKey partitionKey = new PartitionKey(tbl.spec(), tbl.spec().schema()); // partitionの有無に応じて、 Writerの実装を分ける。 // writerはサイズを加味してデータファイルを分割し、 // さらにPartitionedFanoutWriterは、パーティションの値でデータファイルを分割する。 var writer = partitioned ? new PartitionedFanoutWriter<Record>( tbl.spec(), FileFormat.PARQUET, appenderFactory, outputFileFactory, tbl.io(), DATAFILE_MAX_SIZE) { @Override protected PartitionKey partition(Record record) { partitionKey.partition(record); return partitionKey; } } : new UnpartitionedWriter<Record>( tbl.spec(), FileFormat.PARQUET, appenderFactory, outputFileFactory, tbl.io(), DATAFILE_MAX_SIZE); var record = GenericRecord.create(tbl.schema()); try ( var lines = new JsonlReader(input)) { // data add to parquetWriter while (lines.hasNext()) { var r = lines.next(); var row = record.copy(TableUtil.convertRecord(tbl.schema(), r)); writer.write(row); } } レコードの挿入が終わったら、writerが作ったデータファイル一覧をappendオペレーションに追加してコミットします。 for ( var dataFile : writer.dataFiles()) { append.appendFile(dataFile); } LOG.info( "insert complete. append commit" ); append.commit(); transaction.commitTransaction(); テストを実行し生成されたファイルを見ると、 /data/nnnn/nnnn/nnnn/nnnnnnnn/<field>_bucket=[hash]/ というパスでデータファイルが分割されていることがわかります。 マニフェストリストファイル(avro形式)にはデータファイルの情報が含まれています。これを確認すると、4つのデータファイルに分割されていることがわかります。 このスナップショットは、Web上でAvroを解析してくれる https://konbert.com/ の表示内容です。 これで、Java APIを使用したPartitionありのテーブルのデータ投入もできました。 まとめ Java APIを直接利用することで、私たちの場合以下のような改善ができました。 1000万件のデータ投入が、全く終わらない状況から15分に短縮できた 性能テストのデータ投入のための改善だったが、ユーザーファイル取り込みや他システムのデータ取り込みの高速化に流用できた 1ファイルのデータ投入がIceberg上の1トランザクションで実行できるようになった 一方で、Java APIの利用は、データ追記、あるいは洗い替えのための全データ削除といった単純なオペレーションで高速化が必要な場合のみに留めています。 その理由は、Java APIはデータファイル単位での操作に特化しているためです。 例えば、データの更新は、更新対象のレコードを含むデータファイルを特定し、更新後のレコードを含むデータファイルを作成し直して上書きするか無効化するといった操作が必要です。 org.apache.iceberg.ioパッケージ には変更操作をまとめてくれるような機能がありそうですが、それでも難しい操作であることには変わりはなく、このような場合はSparkやTrinoで抽象化された仕組みを使った方が良いでしょう。 以上です、クエリエンジンの仕組みと気持ちがちょっとわかるようになりました。
こんにちは。Drawer Growth グループの江良です。 キャディが「製造業 AI データプラットフォーム」の構想を打ち出してから半年ほどが経ちました。 caddi.com このコンセプトの実現にあたっては、「AI」の部分だけでなく、「データ」の部分を支える仕組みづくりも重要になってきます。今回は、私が携わっているプロジェクトで導入した Apache Iceberg とその使いどころについて紹介したいと思います。 製造業におけるデータ活用の難しさ 本題に入る前に、まずは背景について少し補足します。 (Iceberg の話だけを読みたい人は「採用したアーキテクチャ」のところまでスキップしてください。) モノづくり産業における会社には多種多様なデータが存在する 製造業の世界で登場するデータにはさまざまなものがあります。 詳しくは キャディ、製造業AIデータプラットフォームとしての、第二章。|加藤/キャディCEO でも紹介されていますが、具体例を挙げると以下の通りです。 分類 具体例 構造化データ ・実績データ(見積実績、受注実績、発注実績、製造実績、検査実績、出荷実績、請求実績、在庫実績など) ・マスタデータ(顧客情報、製品、仕入れ先、工程、設備情報、検査器具、チャージなど) 半構造化データ ・CAD 非構造化データ ・図面 ・写真 ・文書(仕様書、不具合報告書、議事録など) (会社の規模にもよりますが)少なくとも十数種類 〜 百数種類のデータが企業内に存在することがイメージできるかなと思います。 当然ながら、それぞれのデータのスキーマは異なります。データのサイズや更新頻度も様々です。実績データに関しては、一億件近くの規模のデータが存在するケースもあります。 データのフォーマットは会社ごとに異なる 図面は、書き手の意図を確実に読み手に伝達するため、JIS 規格に基づいて標準化されています。一方で、表題欄と呼ばれる図面のメタデータ(図面番号、尺度、部品名称、設計者名、承認者名、使用する材質など)を記載する欄の様式は各社が自由に設定できます。 CAD に関しても、どのソフトウェアを使用しているかは各社でバラバラです。 実績データやマスタデータの管理方法は当然各社で異なります。PLM/PDM や ERP といったソフトウェアで管理されていることが多いですが、製造業全体で「標準」と言えるような規格はありません。 データの「活用」に向けたハードル こういった多種多様なデータを活用するためには、まず、非構造化データや半構造化データをなんらかの方法で構造化する必要があります。その上で、データ同士をなんらかの方法で紐づけて、データ同士の連関がわかるようにする必要があります。 データのフォーマットは会社ごとに異なり、さまざまなバリエーションがあります。そのため、「データ同士がどうすれば紐づくか」も一意には決まりません。 ここまでの話をまとめると、 さまざまなスキーマのデータを柔軟に取り扱うことができ、 データ同士をどのカラムで紐づけるべきかを柔軟に選択でき、 大規模なデータセットを取り扱える こういった要件を満たすことが、製造業におけるデータの「活用」を実現する上では求められます(製造業に限った話ではないかもしれませんが)。 データを活用するための一般的な解決策 さて、ここまで説明してきたような課題を解決するためにはどうすればいいでしょうか?一般的には、データエンジニアリングによるアプローチが考えられるかなと思います。 三行くらいで簡単にまとめるとこんな感じ。 データエンジニアリングを専門とするチームを組成し、 データレイクに生データを集め、 ETL パイプライン等を通じてデータを活用可能にする Snowflake 等の登場により、企業がデータ分析を始める際のハードルは大きく下がってきている印象があります。しかしながら、こうしたことを実現するためには、依然としてデータエンジニアリングを専門とするエンジニアが手を動かす必要があります。 改めて、先ほどまとめた課題を再掲します。 さまざまなスキーマのデータを柔軟に取り扱うことができ、 データ同士をどのカラムで紐づけるべきかを柔軟に選択でき、 大規模なデータセットを取り扱える (加えて、製造業に特有のユースケースに特化した機能を提供できる) 上記のような機能を SaaS として提供することで、データをよりかんたんに活用できる状態にしたい、そのための方法を考えてほしい、というのが、ぼくの所属するチームのここ半年のミッションでした。 データレイクハウスの登場 先ほど、データを活用するための一般的な解決策としてデータレイクについて触れました。大規模なデータセットを活用していく上で、データレイクのアーキテクチャは有効ですが、一方で課題もあります。 代表的な課題としては、データの一貫性に関する課題があります。データはあくまで GCS 等のストレージに配置されているだけの状態にあるため、RDBMS でいうところのトランザクションのような概念はありません。そのため、複数のプロセスから同時に書き込みをするとデータが壊れてしまう可能がありますし、中途半端に書き込みがされた状態のデータが予期せず参照されてしまう可能性もあります。 こうした課題から、近年、データレイクハウスと呼ばれるアーキテクチャが注目されてきています。 データレイクハウスアーキテクチャは、データを保存するストレージのレイヤと、データに対して SQL を実行するクエリのレイヤを分離し、その間にメタデータのレイヤを設けているのが大きな特徴です。メタデータのレイヤを設けることで、ストレージ上のデータをテーブルであるかのように抽象化したり、ACID トランザクションを実現したりすることができます。 www.databricks.com それぞれのレイヤで採用できる代表的なツールは以下の通りです。 メタデータのレイヤでは、Open Table Format と呼ばれる仕様に従ってデータが管理されます。この仕様に従ってデータを保存することで、トランザクションなどの便利な機能が使えるほか、クエリのレイヤでどのツールを使うか(Spark、Hive、Flink、Trino など)がユースケースに応じて選択可能になります。 採用したアーキテクチャ 前置きが長くなりました。キャディでの Iceberg の使いどころについての話に移ります。 キャディでは、CADDi Drawer が扱うデータのうち、構造化データを扱うサービスにて Iceberg を使用しています。構造化データのうち、特に実績にまつわるデータはレコード件数が多い傾向にあります。スキーマが不定だったり、紐付け項目が一意に定まらなかったりするという特徴も相まって、RDBMS を素朴に利用してアプリケーションを設計すると、中長期的に期待するパフォーマンスが出せないのではないか、という懸念がありました。 一方で、データの更新頻度は少なく、データの追加操作がメインのユースケースであることから、「RDBMS 以外の選択肢は本当にないのか?」を検討し、紆余曲折を経て Iceberg に辿り着きました。 各レイヤで何を採用したか 先ほど、データレイクハウスアーキテクチャはクエリ、メタデータ、ストレージの 3 つのレイヤで構成される、ということについて説明しました。それぞれのレイヤで採用できるツールにはいくつか選択肢がありますが、CADDi Drawer では Trino、Iceberg、GCS(Google Cloud Storage)を採用しました。 Open Table Format が掲げるテーマとして代表的なものに「バッチとストリーミングの統合」があります。ストリーミングのユースケースを満たすなら、Apache Spark を採用し、Structured Streaming 機能を活用するといった選択肢も考えられます。 iceberg.apache.org ですが、SQL のインタフェースを通じてデータをクエリできれば十分であり、検討時点ではストリーミングのユースケースが見当たらなかったため、比較的導入コストの小さい Trino を採用しています。(リリースまでのスケジュールが非常にタイトであったこと、今回ユーザに提供する機能はあくまでベータ版であったこと、といった事情もあったりします。) Iceberg に関しては AWS など BigTech 各社が力を入れていることから興味を持ち、採用を決めました。 データレイヤーに関しては、キャディでは Google Cloud を全面的に採用していることから GCS を採用することに決めました。 「ベータ版としての提供なのであれば BigQuery でもいいのでは…?」という考えも頭をよぎりましたが、不特定多数のユーザーに BigQuery を用いた機能を解放するとクエリコストのコントロールが難しくなりそうなため、候補からは外しました。 アーキテクチャの詳細 アーキテクチャ図は以下の通りです。 構造化データを扱うマイクロサービスは、キャディの中では珍しく Java を採用しています。静的型付けのある言語で開発したかったのと、Trino や Iceberg などのライブラリとの親和性の高さから採用を決めています。 処理の大まかな流れは以下の通りです。 ユーザがアップロードした CSV をパースして Iceberg に保存する 図面の解析結果を一定間隔のバッチで受け取り Iceberg に保存する Iceberg のデータを用いてデータの紐付けを解決し、「図面に紐づく構造化データ」を UI に表示できるようにする 緑色の線が「ユーザが CSV をアップロードしてから Iceberg に登録されるまで」の流れを表し、赤色の線が「図面の解析結果が Iceberg に登録されるまで」の流れを表しています。別のジョブを通じてデータ同士の紐付けを解決して Iceberg に書き戻し、この「解決済み」のデータを REST API から返却して、ユーザ向けの画面に表示しています。 Trino は GKE クラスタ上に用意した専用のノードにデプロイして稼働させています。コーディネータがクエリを受信し、実行計画を立てて、ワーカに対して指示を送ります。ワーカはコーディネータからタスクを受け取り、データを実際に処理します。 Iceberg Catalog としては Databricks 社の iceberg-rest-image を利用しており、こちらも GKE クラスタ上にデプロイして稼働させています。カタログの情報は AlloyDB に永続化し、ファイルの実態は GCS に保存しています。 github.com Iceberg Catalog にも選択肢がいくつかあります。詳しく知りたい方は下記の記事を参照ください。 bering.hatenadiary.com 大量のデータの INSERT 操作は、パフォーマンスの観点から Iceberg Java API を通じて実施しています。 iceberg.apache.org 所感 Iceberg および Trino を採用したことにより、 テナントごとに異なる、さまざまなスキーマのデータを柔軟に取り扱うことができる データ同士をどのカラムで紐づけるべきかを柔軟に選択できる 大規模なデータセットを取り扱える といった、当初目的としていたアーキテクチャ特性を満たすサービスを構築できました。 データの書き込み性能のスループットに関しては、1000 万件規模のデータの登録が 15min 程度で完了し、読み込み性能に関しても一般的な Web アプリケーションとして違和感のないレスポンスタイムで安定して結果を返すことを確認できました。 今後の課題 ここまで、Iceberg 導入の背景と使いどころについて説明してきました。 直近のゴールは達成できたものの、今後取り組みたいこと、改善したいポイントはたくさんあります。 全社を横断したプラットフォームへの進化 Iceberg を使った仕組みは、現在、あくまで CADDi Drawer の中の一機能という立ち位置です。将来的には CADDi Drawer のデータだけではなくCADDi Quote のデータも横断して取り扱えるよう、アプリケーションとプラットフォームに分割し、アプリケーションを横断して利用できるようにしていく必要があります。 また、こちらのインタビューでも語られている通り、製造業 AI データプラットフォーム CADDi には、今後も新規アプリケーションを追加していくことを想定しています。 www.fastgrow.jp 「3 年で数十個」 という目標を達成する上で、Iceberg を使った基盤を全社を横断したプラットフォームに進化させていく取り組みは急務といえます。 Iceberg の機能をもっと使い倒したい Iceberg にはトランザクション管理に関する仕様が定義されています。この仕様に従って実装されたクエリエンジンを利用することで、更新データの競合が疑われる場合に該当の操作を abort し、データの一貫性を保証することができます。 現時点ではデータの追記(AppendFiles)しか利用していないため、下記の資料で解説されているような同時書き込み時における課題には直面していません。 speakerdeck.com また、Iceberg には in-place table evolution という仕様が定義されています。これはテーブルのスキーマを ALTER TABLE 文を発行して変更したり、テーブルのパーティションを行うキーを後から変更したりすることができる、という機能です。 iceberg.apache.org 現時点では、一度定義したテーブルのスキーマを変更するような機能を提供していないため、この課題には直面していませんが、早晩対応が必要になりそうな予感がしています。 また、Iceberg を全社を横断したプラットフォームに進化させていく上では、各アプリケーションのデータベースに永続化されているデータを、ストリーミング処理を通じてニアリアルタイムに連携できるようにしていく必要も出てきそうです。 やることがたくさんあって大変なわけですが、これはこれで「Iceberg の真価を発揮できるチャンスがたくさんある」と言い換えることもできそうです。 マルチテナント SaaS におけるテナント分離の課題 書籍『マルチテナント SaaS アーキテクチャの構築』でも語られている通り、SaaS を提供する事業者としては、異なるテナントのデータが誤って参照されてしまうことのないよう、テナントの分離を強制する仕組みの構築が重要となります。 CADDi Drawer では、Iceberg のスキーマをテナントごとに作成し、テナントごとのテーブルをスキーマ内に作成することでデータを物理的に分離しています。異なるテナントのデータを参照できないようにする仕組みはアプリケーションのレイヤに実装しています。 こういった仕組みはアプリケーションのレイヤだけでなく、インフラのレイヤにも導入し、多層的なテナント分離を実現したいところです。ですが、現在採用している Iceberg Catalog にはそういったアクセスコントロールに関する機能はないため、やむなく断念しています。 Apache Polaris では、RBAC モデルをベースとした柔軟なアクセスコントロールの仕組みが提供されるようです。現時点では Incubation のステータスにあるため採用を見送ったのですが、正式版がリリースされた際には載せ替えを検討しています。 polaris.apache.org Iceberg の利用を検討している方は動向をウォッチしてみると良いかもしれません。 おわりに いかがだったでしょうか。 Iceberg の採用を検討している方の参考になれば幸いです。 最後に宣伝で、キャディではエンジニアを採用しています。本記事を読んで、「製造業の AI データプラットフォーム」構想に興味を持った方、今後の課題を一緒に解決していきたいと感じた方はぜひご連絡ください。 recruit.caddi.tech
こんにちは、Data&Analysis部(D&A)です。 D&Aでは週1回、機械学習の勉強会を開催しており、本記事は、勉強会の内容を生成AIを活用して記事にまとめたものものです。 ※勉強会内容公開の経緯は こちら ※過去の勉強会は「社内勉強会」タグからもご覧いただけます。 概要 Qwen2-VL の概要 技術的な特徴 主なベンチマーク結果と性能 関連モデル モデルの利用とライセンス 結論と感想 参考リンク 概要 今回の勉強会ではAlibaba Cloud が開発した Vision-Language Model (VLM) である Qwen シリーズ、特に Qwen2-VL の特徴、性能、関連モデルについて話しました。 調査した動機は、Qwenシリーズは日本語の性能が高いとされており、そのマルチモーダルモデルが画像解析を扱う我々の事業領域にマッチしていることです。またDeepSeek R1の蒸留モデルの中にQwenシリーズがあることが調査の更なる動機です。 具体的にはQwen2-VL の技術的な詳細、ベンチマーク結果、多言語対応、そして最新の Qwen 2.5 VL についてです。 また検索エンジンモデルへの応用事例や、今話題のdeepseekの開発したVLMの簡単な紹介も行います Qwen2-VL の概要 Alibabaが開発しているQwen シリーズには複数のモデルが存在します。今回はその中でマルチモーダルモデルのQwen2-VL に焦点を当てました。 Qwen2-VL は、静止画像だけでなく、ビデオや UI 操作など、多様な視覚モダリティに対応することを目指しています。 モデルサイズには複数のバリエーションがあり、最大で 720億パラメータ、最小で 20億パラメータ程度のものがあります。 パラメータの比較(論文より引用) 技術的な特徴 ここではQwen2-VLで紹介されている特徴の中で特に興味深いものを挙げます。 任意の解像度への対応: 後に解説するRoPEの2次元拡張である2D-RoPEで画像と位置情報をエンコードすることで様々な画像サイズに対応できます。論文中で「Naive Dynamic Resolution」というキーワードで紹介されています。 M-RoPE: RoPE (Rotary Position Embedding) を拡張した Multimodal Rotary Position Embedding (M-RoPE) を導入し、文字列から動画までのモダリティを扱えるようになっています。これにより、1D (文字列)、2D (画像)、そして3D(動画)のエンコードが可能になっています。 主なベンチマーク結果と性能 ここではQwen2-VLで紹介されているベンチマークの結果のうち興味深いものを挙げます。 ベンチマーク比較(論文より引用) 主要なベンチマークで、GPT-4V(ision) や Gemini Pro などの競合モデルと比較して、遜色ない、あるいは一部で上回る性能を示しています。 特に、ドキュメント理解 (VQ) やチャート理解 (UA) のタスクにおいて、良好な結果が得られています。 また複数の言語でのベンチマーク結果で、日本語においても一定の性能を発揮することが示されています。 特にマルチリンガル OCR ベンチマークの結果として、Qwen2-VL が日本語にも比較的良く対応しており、日本語を扱う用途での利用が期待されます。 GPT-4oとQwen2-VL-72Bの多言語での性能の比較(論文より引用) 関連モデル Janus-Pro: DeepSeek が開発したマルチモーダルモデルで、エンコーダーに SigLIP-L を採用しています。SigLIPは固定解像度での入力で、文書画像のような高密度なタスクにおいては Qwen2-VL の方が優位性があるかもしれません。 ColQwen2: Qwen2-VL-2B-Instruct をベースに、画像検索 (Visual Retriever) 用に ColBERT strategy を用いて訓練されたモデルです。 Google の PaliGemma を用いた場合と比較して、Qwen2-VL を用いることで日本語文書検索の性能向上が期待されます。 Qwen 2.5 VL: 最新のバージョンとして言及されており、言語モデルのデコーダーに Qwen 2.5 の言語モデルを使用し、ビジョンエンコーダーの一部を効率化したものが採用されています。既存の API 提供モデルと比較しても遜色ない性能を発揮するようです。 モデルの利用とライセンス Qwen シリーズのモデルは Hugging Face で公開されており、容易に試すことができます。 ただしモデルのライセンスについては注意が必要で、ソースコードのライセンスとモデル自体のライセンスが異なる場合があります。特に商用利用を検討する場合は、ライセンス契約の詳細を確認する必要があります。 具体的には、Qwen2VL-72Bは Qwenライセンス であり、商用利用かつユーザー数が一定以上いるサービスに利用する場合にはライセンス契約が必要です。Qwen2-VL-2B, やQwen2-VL-7Bであれば apache-2.0 なので、もう少し気軽に利用できます。 結論と感想 Qwen2-VL は、画像から動画までの推論や任意の解像度での推論を可能にする Vision-Language Model であり、高いベンチマーク性能と多言語対応能力を持っています。 日本語のベンチマークで高い性能を持った公開モデルは嬉しいですね。 Qwen2.5-VLの動向から今後は言語モデルの進化による推論能力の向上や学習の効率化が見込めそうです。また画像や動画に限らず他のモダリティの拡張もあり得るのではないでしょうか。公開されてるモデルなので今後も動向を伺いたいと思います。 参考リンク リンク一覧はこちらをクリック [2409.12191] Qwen2-VL: Enhancing Vision-Language Model's Perception of the World at Any Resolution Qwen2-VL [2410.07073] Pixtral 12B [2104.09864] RoFormer: Enhanced Transformer with Rotary Position Embedding [2307.06304] Patch n' Pack: NaViT, a Vision Transformer for any Aspect Ratio and Resolution Qwen2.5 Technical Reportの中に潜る - ABEJA Tech Blog Large Vision Language Model (LVLM) に関する最新知見まとめ (Part 1) - Speaker Deck 【Qwen2-VL】画像や動画を異なる解像度で処理できる最新VLM | AI-SCHOLAR | AI:(人工知能)論文・技術情報メディア Qwen2-VL : ローカルで動作するVision Language Model | by Kazuki Kyakuno | axinc | Medium vidore/colqwen2-v0.1 · Hugging Face [2412.15115] Qwen2.5 Technical Report [2501.15383] Qwen2.5-1M Technical Report deepseek-ai/Janus-Pro-1B · Hugging Face deepseek-ai/deepseek-llm-7b-base · Hugging Face GitHub - deepseek-ai/Janus: Janus-Series: Unified Multimodal Understanding and Generation Models https://zenn.dev/yumefuku/articles/pdf-search-colqwen2
はじめに こんにちは。 バックエンドエンジニアの松本です。今回は、会計システムの開発を通じて、 CADDi におけるプロダクト開発の様子を紹介します。 2024年3月現在、CADDiでは2つのサービスを提供しています。1つは図面データ活用クラウド「CADDi Drawer」で、もう1つは加工品製造サービス「CADDi Manufacturing」です。 今回、後者の加工品製造サービス「CADDi Manufacturing」向けに、 会計システムを構築しました。これは、生産管理システムや拠点管理システムから取得した各種情報を基にして、会計仕訳データを生成し、経理部門に公開する役割を持ちます。 はじめに 会計システムのアーキテクチャとその狙い 計算処理を少しずつ進める 会計数値の妥当性をダッシュボードに表示する 会計システムのモデリングと最初の開発 仕訳の流れを整理して、ドメインモデル、データベースモデルを作る ユーザーの言葉で話す 最初の開発をどの機能にするか検討する 会計というドメインを Rust で表現する New Type Pattern と Phantom Type Pattern 会計台帳を Rust で表現する State Machine を型で表現する おわりに 会計システムのアーキテクチャとその狙い 「CADDi Manufacturing」は、以下の特徴があり、会計システムとしての難しさはここにあります。 多品種小ロットの取引のため、1つ1つの取引ごとの数量が少なく取引数が多い 多くの顧客、多くのサプライパートナーと取引を行うため、サプライチェーンが複雑 計算処理を少しずつ進める システムは生産管理システムや拠点管理システムがデプロイされているKubernetesクラスタ上にCronJobとしてデプロイされています。 CronJobの処理が始まると、対象月の入出荷などのイベントを上流システムのBigQueryから抽出します。そのイベントを会計データに変換し、アプリケーションのCloudSQLに永続化します。最後に、その月の会計データとして経理部門が参照するBigQueryに転送します。 flowchart LR 上流システムのBigQuery -- イベント --> CronJob CronJob -- 会計データ --> CloudSQL CloudSQL -- 会計データ --> 経理部門のBigQuery 会計システムは月に一度、「締め」を行い計算結果を確定し、BigQueryのデータをバランスシートなどを生成するシステムに登録します。月末になり、全てのイベントが上流システムで登録されないと、その月の会計データは確定しません。しかし、後続の会計プロセスが存在するために、「締め」は翌月上旬の数日間のうちに実施する必要があります。 実際にはユーザの入力不備やシステムの不具合が発生することも考えられますから、かなりタイトなスケジュールで原因を特定し修正する必要があります。そこで、もっと早期にこれらの問題を発見できないかと考え、CronJobを毎日実行するようにして、対象月の初日から実行した日の前日までの会計計算を行う仕組みとしました。 gantt dateFormat MM-DD axisFormat %d tickInterval 1month section 3月2日の処理 3月1日まで計算 :2014-03-01, 1d section 3月3日の処理 3月2日まで計算 :2014-03-01, 2d section 3月4日の処理 3月3日まで計算 :2014-03-01, 3d section 3月5日の処理 3月4日まで計算 :2014-03-01, 4d この仕組みにより、月末を待つことなく、毎日少しずつ増えるイベントを対象に実際の処理を実行し、チェックを行うことができるようになりました。結果として、「締め」を余裕を持って行うことができるようになっています。 達人プログラマー第二版 Tip 42 「少しずつ進めること―――常に」 会計数値の妥当性をダッシュボードに表示する 「CADDi Manufacturing」では毎月大量の取引を行っており、人間による妥当性チェックには限界があります。できるだけ自動的に検証することはできないかと考えて、検証機能をデザインしました。 検証機能の1つを紹介しますと、一定期間中の製品の入庫と出庫のイベントによって変動した在庫数量の合計と、その期間の開始と終了の間の在庫数の差分が一致しているかをチェックしています。 flowchart LR 1a[入庫: 2個] --> 1b["イベントの合計: (2 - 1 = 1) 個"] 1c[出庫: 1個] --> 1b 1d[開始時点の在庫数: 1個] --> 1f["在庫数の差分: (2 - 1 = 1) 個"] 1e[終了時点の在庫数: 2個] --> 1f 1b --> 1g[1 == 1: OK] 1f --> 1g 2a[入庫: 2個] --> 2b["イベントの合計: (2 - 2 = 0) 個"] 2c[出庫: 2個] --> 2b 2d[開始時点の在庫数: 1個] --> 2f["在庫数の差分: (2 - 1 = 1) 個"] 2e[終了時点の在庫数: 2個] --> 2f 2b --> 2g[0 != 1: NG] 2f --> 2g この検証機能により、次の項目を検証することが可能になりました。 上流システムが、ヌケモレやダブりなく、入庫、出庫イベントを送信しているか? 会計システムが、間違いなく入庫、出庫イベントを会計データに変換しているか? この検証結果は Datadog 上にダッシュボード化されていて、一目で異常が発生したかどうか、異常の発生した割合がどれくらいかが分かる仕組みとなっています。 Datadog Dashboard 会計システムのモデリングと最初の開発 開発初期は以下の流れで設計を進めました。 仕訳 *1 の流れを整理する ドメインモデルとデータベースモデルを作る 最初の開発をどの機能にするか決める 仕訳の流れを整理して、ドメインモデル、データベースモデルを作る まず、以下の様な図で仕訳の流れを整理しました。 flowchart LR k[買掛金] -- 入荷 --> s[仕掛品] s -- 製品完成 --> p[製品] p -- 原価計上 --> 売上原価 イベントによって、どのように仕訳の勘定科目が移り変わって行くのかを図示しています。例えば1つ目の矢印では、「入荷」というイベントによって、「買掛金」という勘定科目の金額が増えるととともに、「仕掛品」という勘定科目の金額が増えることを示しています。 この図を用いて、生産管理システムで発生する入荷や製品完成などのイベントによって、どのような仕訳が生まれるのかを経理部門と認識を合わせます。 Miro上に描かれたラフなポンチ絵を使っておおまかに擦り合わせていきます。 そして、以下のようなドメインモデルとデータベースモデルを初期に作成し、経理部門にレビューしてもらいながら進めていたのですが、ここで違和感を感じ始めます。 データベースモデル ドメインモデル ユーザーの言葉で話す レビュー会では目立った指摘を受けることなく設計が進んでいました。手戻りが少ないのは嬉しいですが、正しいものがきちんと設計できているのか、不安視する声もエンジニアからは上がってきます。 そんなある日、とあるレビュー会で処理の内容を説明するために、仕訳の表を用いて説明をしたときのことです。経理部門からはいつもよりも多くの発言を頂き、とても有意義なディスカッションが実施できたのを記憶しています。 仕訳の表 考えてみれば、ドメインモデルやデータモデルはエンジニアの言語です。仕訳の表は経理部門の言語です。経理部門の言語でエンジニアが会話したことにより、経理部門の理解が進んだ結果、有意義なディスカッションが発生したのだと考えています。 ドメインエキスパートの日々の仕事内容にまで踏み込んで会話して初めて、良いプロダクトができる、ということを実感したエピソードでした。 達人プログラマー第二版 Tip 78 「ユーザーとともに働き、ユーザーのように考える」 最初の開発をどの機能にするか検討する 設計は進めていたものの、開発すべき仕訳の種類は多種多様で、どこから手をつければ良いか全く検討がついていませんでした。ただ、チームでは次に該当する機能を開発してリリースしよう、と話をしていました。 仕訳はごく一部にしぼる システムアーキテクチャ全体を串刺す 一部でもビジネスに貢献できる 最終的に、製品仕訳についてイベントを収集して検証する機能を開発することに決定しました。 製品仕訳に関わるイベントの収集 製品仕訳に関わる仕訳の生成と保存 製品仕訳と在庫数の検証 製品仕訳について、システムアーキテクチャ全体を串刺して開発することにより、アーキテクチャに起因するリスクを早期に洗い出す狙いです。この機能はうまく完成し、その後は取り扱う仕訳の種類を増やしていくことで開発を進めることができました。これは、「曳光弾」と呼ばれる開発手法です。 達人プログラマー第二版 Tip 20 「目標を見つけるには曳光弾を使うこと」 会計というドメインを Rust で表現する New Type Pattern と Phantom Type Pattern 金額や数値、IDなどの単純な項目は基本的に "New Type Pattern" を使用しています。"New Type Pattern"を使用することで、在庫数を金額に代入してしまうような、単純な代入のミスによる不具合の発生を防ぐことができます。 同種の値は同じようなロジックを持つ事が多いですから、 "Phantom Type Pattern" の利用も積極的に行います。 "Phantom Type Pattern" については以下の記事を参照ください。 caddi.tech 下の例をご覧ください。加工後の数である ProcessedQuantity と在庫数である InventoryQuantity を別の型として表現しています。さらに、"Phantom Type Pattern"を使用して i32 との相互変換処理は共通のものを定義しています。 use std :: marker :: PhantomData; pub struct TaggedQuantity < T: quantity_type :: QuantityType > { value: i32 , quantity_type: PhantomData < T > , } pub type InventoryQuantity = TaggedQuantity < quantity_type :: Inventory > ; pub type ProcessedQuantity = TaggedQuantity < quantity_type :: Processed > ; pub mod quantity_type { use std :: fmt :: Debug; // Trait 制約をつけるための trait pub trait QuantityType : Eq + PartialEq + Debug {} // PhantomData の型パラメータに渡すための抽象的な型 #[derive( Debug , Eq , PartialEq )] pub struct Inventory ; impl QuantityType for Inventory {} // PhantomData の型パラメータに渡すための抽象的な型 #[derive( Debug , Eq , PartialEq )] pub struct Processed ; impl QuantityType for Processed {} } impl< T: quantity_type :: QuantityType > TaggedQuantity < T > { pub fn signum ( & self ) -> i32 { self .value. signum () } } impl< T: quantity_type :: QuantityType > From < i32 > for TaggedQuantity < T > { fn from (value: i32 ) -> Self { Self { value, quantity_type: PhantomData :: < T > {}, } } } 会計台帳を Rust で表現する 会計台帳を表現する会計仕訳のコードサンプルは以下です。 // 一定期間の台帳全体 pub struct AccountingJournal { id: JournalId, transactions: Vec < AccountingTransaction > , } // 台帳の1行 pub struct AccountingTransaction { id: AccountingTransactionId, accounting_date: AccountingDate, occurred_at: EventDateTime, entries: AccountingEntrySet, } pub enum AccountingEntrySet { // 製品完成というイベントに対応するレコード ProductComplete ( AccountingInventoryEntry, AccountingWorkInProcessProductEntry, ), // ・・・ 各種イベントごとの定義が続く } // 台帳の1行を構成する要素で、勘定科目「製品」の金額を示す pub struct AccountingInventoryEntry { id: EntryId, amount: TotalAmount, quantity: InventoryQuantity, } // 台帳の1行を構成する要素で、勘定科目「仕掛品」の金額を示す pub struct AccountingWorkInProcessProductEntry { id: EntryId, amount: TotalAmount, quantity: InventoryQuantity, } 台帳全体を表す AccountingJournal 、台帳の一行を表す AccountingTransaction 、1つの金額と勘定科目をセットにした Accounting**Entry などの要素を用いて台帳という概念を表現しています。 最終形に至るまで何度もこのドメインの設計は見直しを行っています。最初はチームに会計知識が少ないところからスタートしましたが、開発を経るごとに知識が高まり、以前に書かれたコードの見直しが必要になったためです。 ドメイン知識をRustのような言語で厳密にコード化すると、コンパイラに指摘された箇所からドメインへの理解が曖昧な点が分かることがあります。そのような気づきからドメイン知識をアップデートしてコードを改善し、ドメインへの理解を深めていく活動はとても楽しいものです。 達人プログラマー第二版 Tip 65 「早めにリファクタリングすること、そしてこまめにリファクタリングすること」 State Machine を型で表現する もう1つコード例を紹介しましょう。 バッチ処理は以下の流れで実行されます。 初期化 イベントから仕訳(Journal)を生成する 検証してReportを生成する 以下は、1回のバッチ処理の進捗状況を示すクラスです。 // 初期化後の状態 pub struct CreationSetInitialized { id: JournalCreationSetId, target_month: YearMonth, } impl CreationSetInitialized { pub fn create_journal ( self , journal_id: JournalId) -> CreationSetJournalCreated { CreationSetInventoryCreated { id: self .id, target_month: self .target_month, journal_id, } } } // 仕訳(Journal)生成後の状態 pub struct CreationSetJournalCreated { id: JournalCreationSetId, target_month: YearMonth, journal_id: JournalId, } impl CreationSetJournalCreated { pub fn create_report ( self , report_id: ReportId, ) -> CreationSetReportCreated { CreationSetReportCreated { id: self .id, journal_id: self .journal_id, report_id, } } } // Report生成後の状態 pub struct CreationSetReportCreated { id: JournalCreationSetId, target_month: YearMonth, journal_id: JournalId, report_id: ReportId, } 状態ごとに別々の型を定義しています。処理が進むに従って情報が追加されるので、フィールドが増えていくようにしています *2 。このような実装にすることで、以下のメリットがあります。 状態ごとに型が定義できるので可読性が高くなる Option を排除して分岐を少なく記述できる おわりに 今回は、会計システムのアーキテクチャと設計の進め方、Rustの実装サンプルを紹介しました。 会計システムでは、モノづくり産業のほんの一部である会計という世界をシステムに落とし込む難しさ、面白さに向き合うことができました。CADDiでは、「リアルな世界をシステムに落とし込む難しさ×面白さ」に向き合う開発エンジニアを募集しています。 エンジニア向け採用情報 *1 : 企業のお金の流れを記録するもの *2 : Domain Modeling Made Functional: Tackle Software Complexity with Domain-Driven Design and F# という本を参考にしました
TL;DR エラーハンドリングを行う目的 エラーハンドリングが適切に行われているとどう嬉しいか 1. エラーの発生原因が分かる 2. レスポンスステータスを型安全に出し分けることが可能になる どうエラーハンドリングを行うのか 実装方法 エラー型の定義で気を付けるべきポイント なぜanyhowを利用しないのか エラーハンドリングを行う上で持っている課題感 Drawer Growth グループ バックエンドエンジニアの中野です。今回は、私が所属するチームで gRPC API を開発する際に実践している Rust でのエラーハンドリングについて紹介していきます。 TL;DR エラーの発生原因がわかるようにエラー型を定義することが大切。 anyhow は使わずに自前のエラー型を定義して利用する。 エラーハンドリングを行う目的 そもそもなぜエラーハンドリングを行う必要があるのでしょうか。私が所属するチームでは、以下目的を達成するためにエラーハンドリングを行っています。 発生したエラーに関する情報をログに含めて、調査しやすくするため。 API の利用者に適切なレスポンスを返すため。 エラーハンドリングが適切に行われているとどう嬉しいか エラーハンドリングが適切に行われている場合、我々は以下のような出力をログに埋め込むことができるようになります。 DrawingServiceError * DrawingPageUseCaseError * DrawingPageError * Failed to cast 0 to PageNumber. PageNumber should be greater than 0. * out of range integral type conversion attempted 実行可能なコードは こちら 。 エラーハンドリングを適切に行なった場合に嬉しいポイントは2つあります。 1. エラーの発生原因が分かる 調査の際に「該当エラーはどの経路を通ってなぜ発生したのか」がログからすぐにわからないと辛いです。例えばどの API が呼び出されて発生したエラーなのか、コードベースにおけるどのレイヤーで発生したエラーなのか、といった情報がログを見るだけでわかると調査がスムーズに進みます。 エラー発生元のメッセージを見てみます。 * Failed to cast 0 to PageNumber. PageNumber should be greater than 0. * out of range integral type conversion attempted この出力がポイントで、このメッセージを見るだけで、「PageNumber は 0 より大きい数字である必要があるが、0 を PageNumber にキャストしようとして失敗した」という情報を得ることができます。 これがもし、以下のようなメッセージだと、ログを見るだけでは何が問題だったのかわからなくなり、エラーハンドリングを行なう旨みが半減してしまいます。 // 悪い例1 // `std::num::TryFromIntError`に定義されたメッセージだけが出力される * out of range integral type conversion attempted // 悪い例2 // 0を何にキャストしようとして失敗したのか」がわからないメッセージが出力される * Failed to cast 0 * out of range integral type conversion attempted これらの例を見るとわかるように、エラーメッセージを定義する際にはアプリケーションにおける文脈をエラーメッセージに残すことが大切です。 (補足)stack trace を出力することでもエラーの発生経路はわかります。しかし、以下の技術的な理由から私たちのチームでは stack trace を出力することをやめました。 チーム開発としてどこでエラーログを出力するのかポリシーを決めるのが難しかった。 stack trace を出すにはエラーが発生した箇所でログを出力する必要があるが、ログ出力するコードを書くのを忘れる可能性がある。 そのため、stack trace が本来持っている役割の一部をエラーメッセージに持たせる設計としています。 2. レスポンスステータスを型安全に出し分けることが可能になる 何かエラーが発生し、API 呼び出しが失敗した場合には、発生したエラーによって ステータスコード を出し分ける必要があります。エラーを型で表現していると、このステータスコードの出し分けを型安全に行うことができて嬉しいです。 後ほど具体的に紹介する方法を使うと、エラー型を新しく定義するたびにエラーをステータスコードに変換する箇所でコンパイルエラーが発生し、エンジニアにステータスコードの定義を強制させることができます。そのためステータスコードへの変換漏れや意図しないステータスコードに変換されてしまう可能性をなくすことが可能です。これによって、問題に気づくのがランタイムからコンパイル時にシフトレフトでき、エラーを定義するする手間を考えてもトータルの開発スピードを向上させることができると考えています。 どうエラーハンドリングを行うのか 実装方法 必要なエラー型を Enum で定義していきます。この際、 実装を簡略化するために thiserror という crate を用いています。 thiserror::Error を derive すると、自分で実装しなくてもコンパイル時に std::error::Error trait が実装され楽をできます。 以下は DrawingUseCase で利用するためのエラー型の例です。 #[from] attribute をつけると From trait が実装されるので、 ? 演算子を使ってエラーハンドリングしていくことが可能になります。 このようなエラー型を我々のチームでは基本 trait 毎に定義するようしています。 use thiserror :: Error; #[derive( Debug , Error)] pub enum DrawingUseCaseError { #[error( "DrawingRepository" )] DrawingRepository ( #[from] DrawingRepositoryError), #[error( "CompanyRepository" )] CompanyRepository ( #[from] CompanyRepositoryError), } // DrawingUseCase trait pub trait DrawingUseCase { async fn create_drawing ( & self , command: CreateDrawingCommand, ) -> Result < Drawing, DrawingUseCaseError > ; } // create_drawingの実装例 fn create_drawing ( & self , command: CreateDrawingCommand, ) -> Result < Drawing, DrawingUseCaseError > { // この関数は Result<Company, CompanyRepositoryError>を返り値に持つ // CompanyRepositoryErrorに対して#[from] attributeがつけられているので、 // ?演算子でDrawingUseCaseErrorへの変換が可能になっている。 let company = get_company_by_name (command. company_name ()) ? ; ... drawing } エラーを伝播させていくと、最終的には API のエンドポイントとなる箇所において自分たちで定義したエラー型を tonic::Status に変換する必要があります。このマイクロサービスの実装では、gRPC サービスを実装する際のデファクトである tonic という crate を利用しています。別の crate を利用している場合には、 tonic::Status の箇所を適宜ステータスコードを表す別の型に読み替えてください。 この変換処理を行うために、定義したエラー型を tonic::Status に変換する ToErrorStatus trait と ErrorHandler struct を定義します。 trait ToErrorStatus { fn build ( self , error_message: String ) -> tonic :: Status; } struct ErrorHandler < 'a , Error: std :: error :: Error > ( & 'a Error); そして ToErrorStatus trait をこれまで定義してきたエラー型に対してそれぞれ実装していきます。ここでは DrawingUseCaseError に対する ToErrorStatus の実装だけを例に出していますが、同様にその他のエラー型に対しても ToErrorStatus を実装していく必要があります。例えば、 ErrorHandler(repository_error).build(error_message) が呼び出されているので RepositoryError 型にも ToErrorStatus を実装する必要があります。 impl ToErrorStatus for ErrorHandler < '_ , DrawingUseCaseError > { fn build ( self , error_message: String ) -> Status { use DrawingUseCaseError :: * ; match self . 0 { DrawingRepository ( DrawingRepositoryError :: Repository (repository_error)) => { ErrorHandler (repository_error). build (error_message) } DrawingRepository ( DrawingRepositoryError :: ParseDrawingId (_)) => { Status :: with_error_details ( Code :: Internal, error_message, ErrorDetails :: new ()) } CompanyRepository ( CompanyRepositoryError :: Repository (repository_error)) => { ErrorHandler (repository_error). build (error_message) } } } } 最後に、以下のような関数を定義して、gRPC API のエンドポイントとなる Result<tonic::Response<HogeResponse>, tonic::Status> を返り値とする関数内で呼び出せるようにします。あとはエンドポイントとなる関数内でエラーハンドリングを行う際に、必要に応じて to_error_status 関数を呼び出せば OK です。 pub ( crate ) fn to_error_status (error: impl Into < GrpcServiceError > ) -> Status { use GrpcServiceError :: * ; let error: GrpcServiceError = error. into (); let error_message = error. to_traverse_error_message (); let mut status = { let error_message = error_message. clone (); match & error { DrawingService (service_error) => { ErrorHandler (service_error). build (error_message) } CompanyService (service_error) => { ErrorHandler (service_error). build (error_message) } } }; status. set_source ( Arc :: new (error)); // 任意の方法でログを出力する // println!("{error_message}"); status } #[derive( Debug , Error)] pub ( crate ) enum GrpcServiceError { #[error( "DrawingService" )] DrawingService ( #[from] DrawingServiceError), #[error( "CompanyService" )] CompanyService ( #[from] CompanyServiceError), } このコードで利用している to_traversal_error_message メソッドはエラーの source を辿って全てを 1 つの String にまとめるための関数です。以下のエラーメッセージは to_traverse_error_message メソッドを利用して出力した例です。 DrawingServiceError * DrawingPageUseCaseError * DrawingPageError * Failed to cast 0 to PageNumber. PageNumber should be greater than 0. * out of range integral type conversion attempted to_traverse_error_message メソッドを利用しない場合、 DrawingServiceError だけが出力され、 DrawingServiceError の source を辿ったそれ以下の出力はなくなります。 to_traverse_error_message メソッドの実装は こちら にあるので、気になる方は確認してみてください。 このメソッドと同様のことは anyhow の debug 出力でも可能ですが、以下の理由から自前で関数を実装しています。 anyhow で wrap するのが面倒だった。 anyhow の他の機能は不要で debug 出力だけが欲しかった。 anyhow を使いたくなかったので、間違えて利用することが無いように crate から依存を外したかった。 エラー型の定義で気を付けるべきポイント 気を付けるべきポイントとして「エラー型を共通化しないこと」が挙げられます。我々のチームの場合、Infra 層で利用している crate である sea-orm が返すエラーをマッピングしている RepositoryError 型以外は基本的に共通化せず個別で定義するようにしています。そのため、例えば以下のように、2 つの別の UseCase のエラーの variants の中身がほぼ同じになることもあり得ます。 // 良い例 use thiserror :: Error; #[derive( Debug , Error)] pub enum DrawingUseCaseError { #[error( "DrawingRepository" )] DrawingRepository ( #[from] DrawingRepositoryError), #[error( "CompanyRepository" )] CompanyRepository ( #[from] CompanyRepositoryError), } #[derive( Debug , Error)] pub enum SalesUseCaseError { #[error( "DrawingRepository" )] DrawingRepository ( #[from] DrawingRepositoryError), #[error( "SalesRepository" )] SalesRepository ( #[from] SalesRepositoryError), } エラー型の variants に重複が増えてくると、つい「 DrawingUseCaseError と SalesUseCaseError を統合して UseCaseError にしてしまおう」という誘惑に駆られるのですが、それはあまりいいアイデアではありません。 // よくない例 use thiserror :: Error; #[derive( Debug , Error)] pub enum UseCaseError { #[error( "DrawingRepository" )] DrawingRepository ( #[from] DrawingRepositoryError), #[error( "CompanyRepository" )] CompanyRepository ( #[from] CompanyRepositoryError), #[error( "SalesRepository" )] SalesRepository ( #[from] SalesRepositoryError), } なぜなら、そうしてしまうと前述したエラーハンドリングを適切に行なった場合の嬉しいポイントである「何が原因でエラーが発生したのかが分かる」が失われてしまうからです。 UseCaseError 以外も全て共通化してしまった場合、ログの出力は以下のようになり、「0 を PageNumber にキャストしようとして発生したエラーである」ことしかわからなくなってしまいます。 ServiceError * UseCaseError * DomainError * Failed to cast 0 to PageNumber. PageNumber should be greater than 0. * out of range integral type conversion attempted なぜanyhowを利用しないのか Rust でエラーハンドリングを行う際によく名前が挙がる crate として anyhow がありますが、上記説明の通り我々は anyhow を利用していません。 anyhow の利用例はリポジトリのサンプルを見るとよくわかります。anyhow を利用すると、関数の返り値を anyhow::Result<i32> のように定義することで関数内では ? 演算子を使うだけでよくなり、自分でエラー型を定義する手間が省けます。一時的に利用するだけのスクリプトを書く際や利用者が限られる開発者用ツールを作る際など、エラー型を厳密に定義する必要がなく、anyhow を使うと楽に実装できるケースも多々あります。しかし、我々のケースのようにクライアントから利用される Web API を作る場合にはステータスコードの出し分けが必要であるはずですし、運用のために適切なログも必要になるはずです。この場合多少面倒でも、自分自身でエラー型を定義した方が型安全でデバッグに有益なコードを書くことができ、トータルの生産性は高くなると考えています。 実は以前、弊社で別のアプリケーションにおける Web API を作る際に anyhow を用いて実装したことがあったのですが、とても辛い結果になったという過去があります。具体的な辛いポイントとしては以下の要素などが挙げられます。 context を引き回すために常に with_context をつけて回らなければいけない。 ログを見てもエラーの発生箇所を示すだけで原因がわからない。 ステータスコードの出し分けがエラーメッセージの文字列に頼るしかない。 エラーハンドリングを行う上で持っている課題感 ステータスコードの出し分けをする箇所の実装がごちゃつくことを現状の課題感として持っています。コードベースの成長に伴い UseCase や Infra、Domain 層の種類も増え、 ToErrorStatus を実装するコードがどんどん肥大化して見通しが悪くなってきます。型で守ることができているとはいえ、ここはもう少し上手くやる方法がないか頭を悩ませているところです。
はじめに こんにちは。CADDiでバックエンドエンジニアとして働いている中山です。 今日は、プロダクト開発において大量Seedデータの管理基盤としてAirtableを使ったら開発体験が素晴らしかったのでご紹介しようと思います。 ※ 以下の内容はAirtableの契約プランによって機能が異なること、執筆時にはできないが今後機能が追加されてできるようになっている可能性があることはご了承ください。 はじめに 背景 Airtableとは Airtableでできること UI上で操作が完結し、データの追加/編集がサクサクできる 表計算ソフトでおなじみの便利機能がたくさんある Web APIでCRUD操作ができる IDの生成をAirtableにお任せできる RDBのようにテーブル間にリレーションを作成できる Airtable Automation & Airtable Scripting 細かく権限管理ができる Airtableでできないこと データベース間で同期できるテーブル数に上限がある。 RDBのようなカスケード削除の機能がない 実際に使ってみて おわりに 背景 私が開発に携わっているプロダクトではDBのテーブル数が80程度あり、そのうち約半数のテーブルにSeedデータ(※1)を投入する必要があります (このプロダクトの詳細については割愛させてください、それだけで記事になってしまいます)。開発当初はコード上でデータを定義していましたが、以下の課題がありました。 データの量が多く開発工数が膨らむ データ実装だけで1スプリント終わってしまうなんてことも、、、 実装ミスが多発。レビューでも気づかれずに不具合に データ間のリレーションや実装漏れなど いい感じの変数名を考えるのが面倒 少しのデータ変更を反映するだけでもリリースサイクルに合わせないといけない これらの課題を解決するために、我々のチームは SeedデータをAirtableで管理する ことを決めました。 ※1: ユーザーがシステムを使うために最初にDBに入れておく必要があるデータ (例: フォームで使う選択肢) Airtableとは Airtableは表計算ソフトとデータベースの機能を併せ持つ、Airtable社が提供しているクラウドベースのデータベースツールです。 airtable.com UIは以下のようになっていて、RDBでいうところのUser(左上のタブ)がテーブルを、行がレコードを、列がカラムを表しています。以下はサンプルでUserとCompanyテーブルを実装しています。 AirtableのUI Airtableでできること UI上で操作が完結し、データの追加/編集がサクサクできる AirtableはGUIベースで操作でき、コードで実装するよりも格段に早くデータを作成することができます。また、一般的な表計算ソフトと同じような感覚で使えるため、エンジニア以外でも簡単に操作することができます。 またコードだといい感じの変数名を考える必要があり面倒(似たような名前の表現を迷ったり、やたらと長い変数名になってしまったり)でしたが、Airtableであれば不要です。 表計算ソフトでおなじみの便利機能がたくさんある AirtableではSortやFilter、GroupBy、Lookupといった表計算ソフトでおなじみの機能が使えることでデータの視認性が格段に向上します。 また、カラム毎にデータ型の入力制限(テキスト、 数値、 選択肢、 .etc)や、関数による値の自動生成、テーブルやカラムに説明文の記載といった便利な機能がたくさんあります。以下の例だと、age列には数値以外入力できないようにし、Name列には関数でLastNameとFirstNameを結合させる、みたいなことができます。 カラムのカスタマイズ例 これらの機能を使いこなすことによって、ミスの予防や早期発見に繋がり安全に早く開発できるようになりました。 Web APIでCRUD操作ができる AirtableにはWeb APIが用意されており、基本的なCRUD操作が可能で、JavaScript(TypeScript)やRuby、 .NET、 Python等で書くことができます。 このAPI経由でAirtableからDBへデータを投入しています。 Airtable Web API IDの生成をAirtableにお任せできる AirtableのレコードにはデフォルトでRecord IDが付与されます(わかりやすいようにテーブルに表示させています)。 このIDもAPIで取得できるため、そのままDBのIDとして使うことができます。 Record ID RDBのようにテーブル間にリレーションを作成できる AirtableにはLinkedRecordというデータ種別があり、以下だとUserとCompany列がそれにあたります。 Linking Records in Airtable | Airtable Support UI上では”A株式会社”や”山田太郎”のような値が表示されていますが、セルに格納されている値はRecord IDです。 APIで取得できるのもRecord IDなので、そのままRDBの外部キー制約を満たす形で投入できます。(これが表計算ソフトとデータベースツールの機能を併せ持っていることの良さです) このLinkedRecordのリレーションは1:1、1:n、n:m全てに対応しており、設定で制限をかけることもできます。 Linked Record Airtable Automation & Airtable Scripting Airtable Automationsという機能があり、簡単なWorkflowを組むことができます。 Airtable Automations - Get More Work Done | Airtable 例えば、以下ではUserテーブルにレコードが作成されたら特定のSlackチャネルにメッセージを通知する、みたいなことができます。(もちろんもっと色々できます) Airtable Automationsの例 また、Airtable Scriptingを使えば、JavaScriptで書いたスクリプトをWorkflowに組み込むこともできます。 Airtable Scripting 我々のチームではデータの入力漏れがないかを定期的にチェックするスクリプトを実装してミスを早期発見できる仕組みを自動化していました。 細かく権限管理ができる Airtableでは、ユーザー毎に細かく権限管理ができます。 例えば、操作に慣れているエンジニアのみレコードの削除が可能、であったり管理者以外はテーブルの定義(カラムのデータ型やFilter条件など)を変更できないようにするなど、用途に合わせて自由度高く権限を設定することができます。 これによって操作に不慣れなメンバーの操作ミスによって環境が壊れてしまった、などのリスクを減らすことができます。もし環境が壊れてしまった場合でもバックアップされているのでSnapshotによって過去の状態に戻すことも可能です。 今回紹介した機能はごく一部で、Airtableにはまだまだ便利な機能があるので興味ある方は公式ドキュメントを御覧ください。 airtable.com Airtableでできないこと Airtableを活用することで様々なメリットがあることを紹介しましたが、実現できなかったこともあります。 データベース間で同期できるテーブル数に上限がある。 通常、開発環境毎にAirtableを用意して運用すると思いますが、最上位プランでもAirtable間で同期できるテーブル数に上限があり全てのテーブルを同期できませんでした。 Getting started with Airtable sync | Airtable Support 手動によるデータ同期は手間やミス予防の観点で許容できなかったため、全環境に対して共通のAirtable1つだけで運用しています。 不安はありましたが、データを反映する際はdevelopやstaging環境で、データ反映後に動作確認したうえで本番環境に反映させるため、半年以上運用してトラブルになったことはほとんどありません。 RDBのようなカスケード削除の機能がない 先述した通りLinkedRecordという仕組みでデータ間にリレーションを作成することができますが、RDBのカスケード削除のような依存関係のあるデータを一括で削除する仕組みがありません。(ネットには要望の声が多数あり、将来的には実装されるかもしれません) 手動で関連するデータを削除してまわる運用ではミスを防げないため、我々のチームではデータを削除したい場合はフラグを付けてFilterでデータ投入対象から弾くという工夫をして運用しています。 上記のようにできないことはあるものの、今のところ運用の工夫でカバーできています。 実際に使ってみて 結論としてAirtableをSeedデータの管理基盤にしたことは良い判断だったと思います。 一番良かったことはデータの更新サイクルを素早く回せるようになったことだと思います。 リリースサイクルとは別にAirtableを変更してデータ投入のジョブを実行するだけで反映できるため、ちょっとした文言の変更やフォームの選択肢を一つ追加してほしい、といった要望に対して素早く対応できるようになりました。 加えてデータの実装速度が向上し、かつミスも減少したことで開発効率が劇的に改善し、機能開発など本質的な開発に多くの時間を使えるようになったことも大きなメリットです。 おわりに 本稿では、Seedデータの管理基盤としてAirtableを活用することの利点やできないこと、それに対する運用上の工夫を紹介しました。もし大量のSeedデータを取り扱うことになったとき、Airtableを使う方法があるということを選択肢の一つとして検討してもらえれば幸いです。 CADDiでは現在、私たちと一緒に開発を推進してくださるメンバーを募集しています。 以下に採用情報を掲載しますので、興味のある方はぜひご連絡お待ちしています! recruit.caddi.tech