TECH PLAY

NTTドコモビジネス

NTTドコモビジネス の技術ブログ

613

この記事は、 NTT docomo Business Advent Calendar 2025 20日目の記事です。 先日2025年9月に開催されたGoogle Cloud主催のVibe Codingハッカソンに参加し、優勝することが出来ました。Gemini CLIを活用した「手書きコーディング禁止」のルールのもと、約2時間で開発したツールの内容と裏話、Vibe Codingが可能にした新しいアプローチについても考察していきます。 Google Cloud +AI Prism と「手書き禁止」のハッカソン Google Cloud +AI Prismとは? Vibe Codingハッカソン 参加した動機 作成したツール:TetriStop(テトリストップ) コンセプト 開発のきっかけ:ある記事の発見 今後の展望:機能拡張と社会的展開 作ってみて分かったこと:「動かして」初めて活きる 開発の裏側 1. 一旦作らせる 2. 作らせてから考える/作り直す 3. 同時に作る 考察:「個人の衝動」と「社会課題」の接続 Vibe(衝動)Coding おわりに 社内でのAI活用推進 まとめ 参考文献 こんにちは。コミュニケーション&アプリケーションサービス部の木村です。 普段の業務では「 ビジネスdアプリ 」や「 COTOHA VoiceDX Basic 」の開発に携わりながら、社内の生成AI活用推進も行なっています。 先日、Google Cloudとドコモグループが共催したNTTドコモグループ向けのAIイベント「Google Cloud +AI Prism」内で行われたハッカソンに参加し、最優秀賞をいただきました。 まずは具体的に今回のイベント内容を紹介します。 Google Cloud +AI Prism と「手書き禁止」のハッカソン Google Cloud +AI Prismとは? 2025年9月25日、Google CloudとNTTドコモグループの共催により、渋谷ストリームGoogleオフィスにて開催された社内向け生成AIイベントです。「Practice(実践する)」「Learn(学ぶ)」「Connect(繋がる)」をテーマに、グループ内での生成AI活用を加速させることを目的としており、当日は多くの社員が参加しました。 各社から開催レポートが出ていますので、詳細はそちらをご覧ください。 https://cloud.google.com/blog/ja/products/ai-machine-learning/google-cloud-and-ntt-docomo-group-co-host-ai-prism https://nttdocomo-developers.jp/entry/2025/10/31/090000 Vibe Codingハッカソン その午前の部で行われたのが、「アイデア、即、形に!Geminiによる高速開発Vibe Codingハッカソン」です。 このハッカソンのレギュレーションは以下のようなものでした。 テーマ: 「ライフハック・業務改善」 ルール: 生成AI(Gemini CLI)を利用した開発に限定、生成AI以外による手書きのコーディングは禁止 時間: 開発からプレゼン資料作成まで約2時間 そもそも「Vibe Coding(バイブコーディング)」とは、Andrej Karpathy氏が提唱した、AIを使用して自然言語プロンプトから機能コードをバイブス(直感やノリ)で生成する開発手法です [1] 。従来はプログラミング言語やフレームワークの習得が必要で、一定の技術的なハードルが存在していましたが、AIコーディングの登場で専門的な知識がない人でもアプリ開発ができるようになりました。 そのため、非エンジニアの参加者も多く、開発技術の高さよりも、スピードと発想力が試されるユニークな場でした。 参加した動機 今回、上司にこのイベントを勧めていただき、上記のハッカソンに参加しました。 元々Vibe Codingには興味があったことや、日頃Googleの諸サービスを使用していたこともあり(後述するツールをChrome拡張機能として作成したのは、私自身Chrome愛用者のため)、ちょうど良い機会でした。 作成したツール:TetriStop(テトリストップ) 私が今回のハッカソンで作成したのは「TetriStop(テトリストップ)」というChrome拡張機能です。 コンセプト 「見たくないのに見てしまうWebサイト(SNSなど)」にアクセスしようとすると、ブラウザがそれを検知してアクセスをストップし、代わりにテトリスの画面が立ち上がります。 一定時間テトリスをプレイしないと元の画面に戻れず、スコアは我慢すればするほど蓄積されるようにすることで、モチベーションが維持される工夫も取り入れました。 デモ動画 開発のきっかけ:ある記事の発見 アイデアを考えていたハッカソンの前日、興味深い記事を見つけました。 「テトリスを3分するだけで暴飲暴食を防げる?海外チームが2015年に研究発表」 [2] テトリスのような視覚的な作業に脳のリソースを使うことで、欲求の対象をイメージする余裕がなくなり、結果として渇望が弱まるそうです。 私自身、ついついSNSを見てしまう癖があったので「これをWebブラウジングに応用すれば、SNS断ちができるのでは?」と考えたのがスタートでした(あくまできっかけがこの記事でしたので、厳密な内容は元論文 [3] を参照してください)。 何より、テトリスという題材は、Vibe Codingでテスト的に作るゲームとして最適で、今回のハッカソンのテーマにも合っていると考えました。 今後の展望:機能拡張と社会的展開 プレゼンでは、機能拡張や社会課題への接続についても述べ、このツールが単なるジョークツールに留まらない可能性についても言及しました。 機能拡張 カスタマイズ機能: 「どうしても見てしまうWebサイト」ほど高得点が出るようにし、離脱をゲーム化する ランキング機能: 全国のユーザーと我慢強さを競い、モチベーションを維持する 展開 子ども向けの教育利用 深刻なスマホ中毒問題へのアプローチ 作ってみて分かったこと:「動かして」初めて活きる また、「実際に作って触ってみたからこそ分かったこと」を所感として強調して伝えました。 まず、自分でテストプレイをして痛感したのが「最初の数秒で、すぐにやめてしまいたくなる」ことです。元論文でも言及される適切な時間設定や、モチベーション維持のための工夫が誘惑を断ち切るために必要な要素だと体感で理解できました。 そしてもう1つ実感したことが「知見を知見のままにするのはもったいない」ことです。面白い論文(知見)を、従来はインプットとして終わっていたところを、動くツール=アウトプットに変えることで、初めて見える面白さや価値があることを実感しました。 こういったプレゼンを通して、Vibe Codingの面白さを短い時間ながら具体例を持って伝えることができたのではないかと思います。 開発の裏側 では具体的にどのように約2時間でこれを作り上げたのか?Vibe Codingの特性を活かすため、以下の3つの戦略を取りました。 1. 一旦作らせる AIエージェントを用いた開発では、まず要件ドキュメントとなるMarkdown形式ファイル( GEMINI.md )などを作成し、それをコンテキスト(背景情報)としてAIに作成させることがベストプラクティスとしてまとめられています。 [4] [5] しかし同時に、Google Cloudが提唱するVibe Codingの手順では、事前にドキュメントを用意するのではなく簡単に「目標を説明する(Describe goal)」ことからスタートし、「緊密な会話ループ(Tight conversational loop)」を回すことが解説されています。 [6] 昨今これらのAIエージェントによる開発手法は、前者のやり方を大規模開発やリファクタリングに向く「Agentic Coding」、後者のやり方をアイデア出しやプロトタイピングに向く「Vibe Coding」として区別するようになりました。 [7] (「仕様書を書いたからAgentic」「書かなかったからVibe」という単純な二元論ではなく、個人的には地続きのものだと考えています) 今回のハッカソンはタイトル通り、まさに「Vibe Coding」の場としてうってつけであり、私自身アイデアはあったものの完成イメージが湧いていなかったため、「雑に一旦プロンプトを書いて作らせてみる」ことからスタートしました。 Gemini CLIへの指示出しで、私が打った初期プロンプトはこれだけです。 「 Chrome拡張機能で特定のwebサイトを開いたら、ブロックして別タブでテトリスを1分行わせるツールを作りたい。1分経ったら、状態とスコアは保存される。 」 結果、Gemini CLIは、HTML+CSS+JavaScriptで構成し、chrome拡張での実施方法についても解説してくれました。 また、一度作らせてみることで、「禁止したいwebサイトを設定する画面は最初に別画面で開かせたい」「テトリスの画面構成は一発だと作れなさそうだ」というおおまかな方針も立てやすくなりました。 このように、仕様イメージが無いうちは、「まず作る」→「仕様を決める」→「作り直す」というフローで進められるのがVibe Codingの利点だと考えます。 2. 作らせてから考える/作り直す 今回の場合は、上記の自然言語の指示で、Gemini CLIが以下を一括で生成してくれたため、大きく作り直すことはありませんでした。 manifest.json の設定 Content Scriptによるブロッキングロジック テトリスのゲームロジック UIの雛形 もちろん、一発で完璧なものはできないため、都度、自然言語で修正依頼をそのままプロンプトとして投げかけます。 課題: ゲーム終了時にネガティブなメッセージが出る 修正: 「 『ハイスコアに届きませんでした』みたいな文言は余計ですね。削除してください 」 課題: 終了時のポップアップが初めから出ており「初期終了した」と解釈される 修正: 「 初期終了してしまうのではなく、ポップアップ画面が重なっていることが問題なのでは? 」 .... もし途中で崩れた場合やなかなか解消されないエラーがあった場合は、「 ここまでの指示を踏まえて、このツールを作成するプロンプトを作成して 」のように指定して作り直すことで、時間短縮することを想定していました。 このサイクルにより、2時間でバージョン13までアップデートを重ね(10分に1回ペース)、当日のプレゼンで余裕を持ってデモまで行うことができました。 3. 同時に作る また、Vibe Codingが可能にしたこととして、並列開発が挙げられます。 複数バージョンを作成する際、同じプロジェクトで回すだけでなく、複製して別プロジェクトでも実行させておくことでさらなる高速検証が可能になります。 また、テトリスツールはジョークツールのつもりで作り始めていたので、AIにコードを書かせている待ち時間を利用し、別案として真面目な実用系ツール(大量のタブを管理するchrome拡張機能)も並行して開発していました。 結果的に、終了前に「テトリスツールの方が動かしてみて面白く、可能性がありそう」と判断してそちらを採用しました。 コードを書かせている待ち時間にもう一方の動作確認を行うことで、複数プロジェクトの検証が同時にできるようになったことも大きな利点です。 考察:「個人の衝動」と「社会課題」の接続 これらの開発過程から、今回評価していただいた理由を考えたいと思います。 まず「プレゼンの順番」や「アイデアが他と被らなかった」といった運の要素は大きかったと思います。 その上で、審査ではありがたいことに「現代の社会課題(スマホ中毒)や企業課題を捉えている点」や、「論文というエビデンスに基づいている点」を高く評価していただきました。 しかし、今回の場合は「社会課題をリサーチし、エビデンスを探し、そこからソリューションを導き出した」訳ではありませんでした。 もし私が最初から「企業の業務改善課題」や「スマホ中毒の解決策」を真面目に考えていたら、間違いなく「テトリスツール」というアイデアには辿り着かなかったと思います。 実態はこうです。 ① ネットニュースで記事をたまたま見つけて「面白い!」と思った。 ②「自分もSNS断ちしたいし、これを作ったら自分が楽しいかも」という素朴な衝動で作り始めた ③ 出来上がってみたら、結果として「これって実は多くの人が困っている課題に刺さるのでは?」「他の分野にも展開できるのでは?」という社会的意義が見えてきた Vibe(衝動)Coding ビジネスやサービスとして社会実装を目指す以上、社会的意義やエビデンスは必要です。それがなければ、ただの自己満足で終わってしまいます。しかし、「ロジックから始めなければならない」という思い込みがアイデアの幅を狭めることもあります。 例えば「企業の業務効率化」「ウェルビーイング」といった自分より外にある大きな課題から始めると、入念な調査がない限り、ピントがぼけた抽象的なアウトプットになりがちです。対して「SNSを見てしまう自分の指を止めたい」といった個人の衝動は、「極めて具体的である」という大きな利点があります。 従来では、こういった「ちょっと面白いかも」程度の衝動に大きくコストをかけることはできず、この利点を活かすことが困難でした。しかし、Vibe Codingは試行錯誤のコストを限りなくゼロにしました。 まず「自分が欲しい」から走り出し、大量に試作する中で「社会にとっての意味」を見つけ出し、そこへ接続していくーー今回のハッカソンでは、この順序が上手くハマったのではないかと思います。 「極めて具体的」な個人の衝動から始まるアプローチに、市民権を与えたこと。 これが、Vibe Codingの本質的な価値ではないでしょうか。 おわりに 社内でのAI活用推進 さて、こうしたAIの可能性を、実際の業務にどう落とし込んでいけば良いのでしょうか。 大規模で堅牢性が求められる商用のプロダクト開発においては、今回のようなドキュメントレスな手法ではなく、「Agentic Coging」的な手法が求められ、別途検討が必要です。 しかし、「個人の業務改善」や「チーム内のツール開発」レベルであれば、力を発揮できる場面は多いと考えます。 現在、私の所属する部署(コミュニケーション&アプリケーションサービス部 第二サービス部門)では、全社的なAIリテラシー向上と実活用に向けて、以下のような取り組みを行っています。 ユースケースの共有 サービス企画職向けの活用事例: AIによる市場調査/調査資料の作成 活用チャネルの整備: 相談会の定期開催・最新情報の展開 環境整備: 全員が生成AI(Gemini・NotebookLM)を使用できる環境の構築 イベント実施 ハンズオンワークショップ: 実際にGeminiを触って体験し、活用方法をアイディエーションするワークショップの実施 今後は「より現場の業務に即して具体的にカスタマイズしていけるような仕組みづくり」にチャレンジしていきたいと考えています。 機会があればそちらの取り組みについても紹介していきたいと思いますので、社内の皆さんはじめ、ぜひお気軽にご連絡ください。 まとめ 生成AIの登場により、私たちは「正確な仕様書」や「高尚な目的」がなくても、思いついたアイデアを即、形にできる手段を手にしました。 もちろん、最終的なプロダクトとして世に出すにはロジックや品質が不可欠ですが、その入り口はもっと個人的で素朴なものでも良いのではないか。それが、今回私が最も実感したことです。 「まずは自分の業務を少し楽にしたい」「単純にこれを作ったら面白そう」そんな身近な動機から走り出してみることも、新しい価値を生む道となるかもしれません。 もし、こうした開発スタイルや、AIを活用した業務改善に少しでも興味をお持ちいただけたなら、ぜひ一緒にチャレンジしていきましょう。 それでは、明日の記事もお楽しみに! 参考文献 Andrej Karpathy (@karpathy):該当ポスト(2025年2月3日) 山下裕毅:テトリスを3分するだけで暴飲暴食を防げる?海外チームが2015年に研究発表(2025年8月6日) J. Skorka-Brown, et al:Playing Tetris decreases drug and other cravings in real world settings(2015) Google Cloud:AIコーディングアシスタントを使用するための5つのベストプラクティス(2025年10月15日) Google Cloud:Gemini Code Assistエージェントモードを使用する(最終確認:2025年12月17日) Google Cloud:vibeコーディングとは(最終確認:2025年12月17日) M. Chen, et al:Vibe Coding vs.Agentic Coding: Fundamentals and Practical Implications of Agentic AI(2025)
この記事は、 NTT docomo Business Advent Calendar 2025 19日目の記事です。 こんにちは、イノベーションセンターの鈴ヶ嶺です。普段はAIアクセラレータの検証に関する業務に従事しています。 本記事では、まずTenstorrentのAIアクセラレータアーキテクチャを紹介し、その特徴について説明します。次に、複数の演算を1つのkernelに統合するfused kernelによる最適化に注目し、標準正規乱数(randn)を例にTenstorrentのアクセラレータにおける具体的な実装方法と性能評価を共有します。その結果、従来の演算の組み合わせの標準正規乱数の実装と比較して、fused kernel実装により約4倍の高速化を確認しました。 Tenstorrentとは オンチップ計算を活かしたFlash Attention fused kernelの実装と評価 実装 性能評価 まとめ Tenstorrentとは Tenstorrent Inc. は次世代AIアクセラレータを製造する半導体メーカーです。 オープン戦略を掲げており、アクセラレータにはRISC-Vを採用し、ソフトウェアに関してはOSS ( https://github.com/tenstorrent ) として積極的に公開されています。 2025年12月現在ではDEC、AMD、Apple、Teslaを歴任した半導体業界の著名なJim Keller氏がCEOを務めています。 TenstorrentのAIアクセラレータのアーキテクチャについて紹介します。 引用: https://speakerdeck.com/tenstorrent_japan/tensix-core-akitekutiyajie-shuo?slide=7 アクセラレータはTensix Coreと呼ばれる5つのBaby RISC-V、2つのNetwork-on-Chip(NoC)、SRAMで構成されるものが複数搭載されています。 一般的なハードウェア管理キャッシュを持たない構成となっており、明示的にコア付近のSRAMを操作する分散メモリ型のNear Memory Computing(NMC)な設計です。 5つのRISC-Vコアは独立な動作が可能なMIMD(Multiple Instruction、 Multiple Data)アーキテクチャです。 多くの処理は典型的にはデータ読み出しを行うReader kernel(RISC-V 1)、 計算をするCompute kernel(RISC-V 2、 3、 4)、 データ書き込みを行うWriter kernel(RISC-V 5)に分けて実行されます。 後述する標準正規乱数のfused kernel実装ではデータ読み込みが不要のためCompute、Writer kernelのみの実装となっており、処理に合わせて自由度を高く調整できます。 16x16を基本としてtileベースの演算エンジンを積んでおり、Compute kernelはこのエンジンを呼び出します。 kernel間のデータはCircular Buffer (CB)と呼ばれるSRAM上のFIFOキューでやり取りをします。 ホストとのデータ交換は外側のDRAM(GDDR)を介して行われます。 その他の技術詳細は日本法人のTenstorrent Japanから以下にさまざまな資料が公開されているためご参照ください。 https://speakerdeck.com/tenstorrent_japan オンチップ計算を活かしたFlash Attention アクセラレータの特徴として、低コスト化のためにHBM(High Bandwidth Memory)などの高コストなメモリを使わない設計となっています。 そのためできるだけDRAM往復によるオーバーヘッドを避けるために、オンチップのSRAM上で計算する工夫がされます。 ここではLLMのAttention計算の事例を取り上げて、どのようにTenstorrentのAIアクセラレータで効率化されるのかを説明します。 https://github.com/tenstorrent/tt-metal/blob/main/tech_reports/FlashAttention/FlashAttention.md LLMのAttentionはそのまま計算すると、巨大な中間行列によりHBM、 DRAMへのデータ移動がオーバーヘッドとなることが知られております。 FlashAttention 1 2 は、その課題に対して行列をチャンクに分割し、より高速なSRAM上で計算しデータ移動のオーバーヘッドを削減し、高速化する手法です。 TenstorrentのAIアクセラレータでも、このFlashAttentionを適用可能です。 大容量のSRAMを利用して実装され中間データがDRAMに書き込まれないため高速化されます。 以下の図のようにベースライン実装と比較して平均して20倍高速に動作します。 引用: https://github.com/tenstorrent/tt-metal/blob/main/tech_reports/FlashAttention/images/image3.png fused kernelの実装と評価 AIアクセラレータの実行は複数のkernelの実行による、中間計算結果のメモリアクセスや起動オーバーヘッドが課題となります。 そこで複数の計算処理を1つのkernelに統合するfused kernelにより性能を向上させる処理がよく用いられます。 例えばLLMのAttentionなどは計算を最適化するために1つのfused kernelとして実装されています。 ttnn.transformer.scaled_dot_product_attention(input_tensor_q: ttnn.Tensor, input_tensor_k: ttnn.Tensor, input_tensor_v: ttnn.Tensor, *, attn_mask: ttnn.Tensor = None, is_causal: bool = true, scale: float = None, sliding_window_size: int = None, memory_config: ttnn.MemoryConfig = None, program_config: SDPAProgramConfig = None, compute_kernel_config: ttnn.DeviceComputeKernelConfig = None, attention_sink: ttnn.Tensor = None) → ttnn.Tensor https://docs.tenstorrent.com/tt-metal/latest/ttnn/ttnn/api/ttnn.transformer.scaled_dot_product_attention.html#ttnn.transformer.scaled_dot_product_attention ここではttnnに実装されていない標準正規乱数を生成するrandnを実装します。 randnは一般的な PyTorchの torch.randn や Numpyの np.random.randn などではサポートされています。 標準正規乱数には、Box-Muller法 3 を用います。 実装 新規のOperation追加は、次のように手順で行います。 https://docs.tenstorrent.com/tt-metal/latest/ttnn/ttnn/adding_new_ttnn_operation.html まず、ホスト側での処理を抜粋すると以下のように実装します。 ttnn/cpp/ttnn/operations/randn/device/randn_device_operation.[cpp|hpp] ではOperationの引数やバリデーションを実装します。 struct RandnDeviceOperation { struct operation_attributes_t { const ttnn::Shape shape; // テンソルの形状 DataType dtype; Layout layout; const MemoryConfig memory_config; MeshDevice* device; const DeviceComputeKernelConfig compute_kernel_config; uint32_t seed; // 乱数seed }; // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ void RandnDeviceOperation:: validate_inputs ( const operation_attributes_t& operation_attributes, const tensor_args_t& tensor_args) { TT_FATAL ( operation_attributes.dtype == DataType::FLOAT32 || operation_attributes.dtype == DataType::BFLOAT16, "Randn: Output tensor must be Float32 or Bfloat16" ); // dtypeによるバリデーション TT_FATAL (operation_attributes.layout == Layout::TILE, "Randn: Not currently supporting row major layout" ); // メモリレイアウトのバリデーション } アクセラレータ上のkernel実行の詳細は ttnn/cpp/ttnn/operations/randn/device/randn_program_factory.cpp に記述します。 ユーティリティ関数 tt::tt_metal::split_work_to_cores 4 によるコアごとの処理を均等に分散 CreateCircularBuffer によるCB(FIFOキュー)の作成 CreateKernel によるCompute、 Writer kernelの作成 SetRuntimeArgs kernel実行の引数の設定 // split_work_to_coresにより、それぞれのコアに処理を割り振る auto [num_cores, all_cores, core_group_1, core_group_2, units_per_core_group_1, units_per_core_group_2] = split_work_to_cores (grid, units_to_divide); // CBの作成(2tile分の出力ができるように確保する) constexpr uint32_t dst_cb_id = CBIndex::c_0; CircularBufferConfig cb_output_config = CircularBufferConfig (in_out_num_tiles * dtype_tile_size, {{dst_cb_id, out_data_format}}) . set_page_size (dst_cb_id, dtype_tile_size); tt_metal:: CreateCircularBuffer (program, all_cores, cb_output_config); // Writer kernelの設定 const std :: string kernels_dir_path = "ttnn/cpp/ttnn/operations/randn/device/kernels/" ; std :: vector < uint32_t > writer_compile_time_args{dst_cb_id}; tt::tt_metal:: TensorAccessorArgs (output. buffer ()). append_to (writer_compile_time_args); const std :: string writer_file_path = kernels_dir_path + "writer_standard_normal.cpp" ; KernelHandle writer_kernel_id = tt_metal:: CreateKernel ( program, writer_file_path, all_cores, WriterDataMovementConfig (writer_compile_time_args)); // Compute kernelの設定 const std :: vector < uint32_t > compute_compile_time_args{dst_cb_id}; const std :: string compute_file_path = kernels_dir_path + "compute_standard_normal.cpp" ; auto [math_fidelity, math_approx_mode, fp32_dest_acc_en, packer_l1_acc, dst_full_sync_en] = get_compute_kernel_config_args (device-> arch (), operation_attributes.compute_kernel_config); KernelHandle compute_kernel_id = CreateKernel ( program, compute_file_path, all_cores, ComputeConfig{ .math_fidelity = math_fidelity, // 計算の精度 ref: https://speakerdeck.com/tenstorrent_japan/tensix-core-akitekutiyajie-shuo?slide=26 .fp32_dest_acc_en = true , .dst_full_sync_en = dst_full_sync_en, .math_approx_mode = math_approx_mode, .compile_args = compute_compile_time_args, .defines = compute_defines, }); // foreach in split_work_to_coresによる割り振り // kernel引数(1コアあたりの乱数生成のtile数、出力のアドレス)の設定 std :: vector < uint32_t > compute_runtime_args = {seed, tile_offset, units_per_core}; SetRuntimeArgs (program, compute_kernel_id, core, compute_runtime_args); std :: vector < uint32_t > writer_runtime_args = {output. buffer ()-> address (), tile_offset, units_per_core}; SetRuntimeArgs (program, writer_kernel_id, core, writer_runtime_args); // end ここからはkernelの実装を説明します。kernel内で利用可能なAPIは以下になります。 https://docs.tenstorrent.com/tt-metal/latest/tt-metalium/tt_metal/apis/kernel_apis.html Compute kernel ttnn/cpp/ttnn/operations/randn/device/kernels/compute_standard_normal.cpp の抜粋を記述します。 tileベースの命令を用いて処理します。 ここで実際にBox-Muller法で標準正規乱数が生成されます。 // Box-Muller法で標準正規乱数 (Z1, Z2) を生成 // Z1 = sqrt(ln(U1) * -2) * cos(U2 * 2pi) // Z2 = sqrt(ln(U1) * -2) * sin(U2 * 2pi) // 出力CBの末尾に2tile確保 cb_reserve_back (dst_cb_id, 2 ); // タイルレジスタを確保 tile_regs_acquire (); // U1、 U2の一様乱数(0, 1)をレジスタ0, 1に生成 rand_tile ( 0 , flt_min, one_minus); rand_tile ( 1 , flt_min, one_minus); // sqrt(ln(U1) * -2)を計算し、レジスタ0に格納 log_tile ( 0 ); mul_unary_tile ( 0 , neg_two); sqrt_tile ( 0 ); // レジスタ2に2piを詰める fill_tile_bitcast ( 2 , two_pi); // U2 * 2piを計算し、レジスタ3, 1に格納 mul_binary_tile ( 1 , 2 , 3 ); mul_binary_tile ( 1 , 2 , 1 ); // cos(U2 * 2pi)を計算し、レジスタ3に格納 cos_tile ( 3 ); // sin(U2 * 2pi)を計算し、レジスタ1に格納 sin_tile ( 1 ); // Z1 = sqrt(ln(U1) * -2) * cos(U2 * 2pi)を計算し、レジスタ3に格納 mul_binary_tile ( 0 , 3 , 3 ); // Z2 = sqrt(ln(U1) * -2) * sin(U2 * 2pi)を計算し、レジスタ1に格納 mul_binary_tile ( 0 , 1 , 1 ); // 出力dtypeが BFLOAT16 の場合は型変換 #ifdef OUTPUT_DTYPE_BFLOAT16 typecast_tile< 0 , 5 >( 3 ); typecast_tile< 0 , 5 >( 1 ); #endif // レジスタ計算の確定、完了待ち tile_regs_commit (); tile_regs_wait (); // レジスタ3, 1のZ1、 Z2をCBへ書き込み pack_tile ( 3 , dst_cb_id); pack_tile ( 1 , dst_cb_id); // レジスタ解放 tile_regs_release (); // CBの末尾に2タイル追加したことを通知 cb_push_back (dst_cb_id, 2 ); 次にWriter kernel ttnn/cpp/ttnn/operations/randn/device/kernels/writer_standard_normal.cpp を抜粋します。 基本的にはCompute kernelからデータを受け取り、そのままNOC経由で書き込みます。 // CBの先頭に2tileがCompute kernelからpushされるまで待つ cb_wait_front (dst_cb_id, 2 ); // CBの読み取りポインタ取得 uint32_t dst_cb_read_base = get_read_ptr (dst_cb_id); uint32_t dst_cb_read0_ptr = dst_cb_read_base; uint32_t dst_cb_read1_ptr = dst_cb_read_base + dst_tile_bytes; // NOCでタイル単位に非同期書き込み noc_async_write_tile (i, output_addrg, dst_cb_read0_ptr); noc_async_write_tile (i + 1 , output_addrg, dst_cb_read1_ptr); // 書き込み完了までバリア noc_async_write_barrier (); // CBから2tile pop cb_pop_front (dst_cb_id, 2 ); 最後にC++やPythonから呼び出すための実装を追加します。 ttnn/cpp/ttnn/operations/randn/device/[randn|randn_pybind].[cpp|hpp] Tensor Randn:: invoke ( const ttnn::Shape& shape, MeshDevice& device, const DataType dtype, const Layout layout, const MemoryConfig& memory_config, const std :: optional <DeviceComputeKernelConfig>& compute_kernel_config, uint32_t seed) { auto tensor = ttnn::prim:: randn (shape, dtype, layout, memory_config, device, compute_kernel_config, seed); if (layout != Layout::TILE) { tensor = ttnn:: to_layout (tensor, layout); } return tensor; } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ void bind_randn_operation (py:: module & pymodule) { bind_registered_operation ( pymodule, ttnn::randn, doc, ttnn::pybind_overload_t{ []( const OperationType& self, const ttnn::Shape& shape, MeshDevice& device, const DataType dtype, const Layout layout, const MemoryConfig& memory_config, const std :: optional <DeviceComputeKernelConfig>& compute_kernel_config, uint32_t seed) { return self (shape, device, dtype, layout, memory_config, compute_kernel_config, seed); }, py:: arg ( "shape" ), py:: arg ( "device" ), py:: kw_only (), py:: arg ( "dtype" ) = DataType::BFLOAT16, py:: arg ( "layout" ) = Layout::TILE, py:: arg ( "memory_config" ) = ttnn::DRAM_MEMORY_CONFIG, py:: arg ( "compute_kernel_config" ) = std :: nullopt , py:: arg ( "seed" ) = 0 }); } 今回実装したより詳しい全体コードは、以下のPull Requestを参照ください。 https://github.com/tenstorrent/tt-metal/pull/34508 性能評価 次のスクリプトで実装したttnn.randnと従来のopを組み合わせたttnn.rand + Box-Muller変換の実装と比較します。 補足としてCPUによる実装も計測します。 import math, time, ttnn, torch, numpy as np def rand_box_muller (shape, *, device, dtype, layout, mem, seed): half = (*shape[:- 1 ], shape[- 1 ] // 2 ) u1 = ttnn.rand(half, device=device, dtype=dtype, layout=layout, memory_config=mem, seed=seed + 1234 ) u2 = ttnn.rand(half, device=device, dtype=dtype, layout=layout, memory_config=mem, seed=seed + 4321 ) r = ttnn.sqrt(ttnn.multiply(ttnn.log(u1), - 2.0 )) th = ttnn.multiply(u2, 2.0 * math.pi) z0 = ttnn.multiply(r, ttnn.cos(th)) z1 = ttnn.multiply(r, ttnn.sin(th)) return ttnn.concat([z0, z1], dim=- 1 ) def fused (shape, *, device, dtype, layout, mem, seed): return ttnn.randn(shape, device=device, dtype=dtype, layout=layout, memory_config=mem, seed=seed + 1234 ) def torch_randn (shape, *, dtype, seed): torch.manual_seed(seed+ 1234 ) return torch.randn(shape, dtype=dtype) def bench (name, fn, *, iters, warmup): for i in range (warmup): fn(i) t0 = time.perf_counter_ns() for i in range (iters): fn(i) mean_ms = (time.perf_counter_ns() - t0) / 1e6 / iters print (f "{name}: {mean_ms:.6f} ms/iter" ) return mean_ms DEVICE_ID = 0 SHAPE = ( 1 , 1 , 1024 , 1024 ) ITERS, WARMUP = 10000 , 1000 LAYOUT, MEM, DTYPE = ttnn.TILE_LAYOUT, ttnn.DRAM_MEMORY_CONFIG, ttnn.float32 device = ttnn.open_device(device_id=DEVICE_ID) res_rand_box = bench( "ttnn.rand + Box-Muller" , lambda i: rand_box_muller(SHAPE, device=device, dtype=DTYPE, layout=LAYOUT, mem=MEM, seed=i), iters=ITERS, warmup=WARMUP) res_randn = bench( "ttnn.randn" , lambda i: fused(SHAPE, device=device, dtype=DTYPE, layout=LAYOUT, mem=MEM, seed=i), iters=ITERS, warmup=WARMUP) print (f "Speedup: {res_rand_box / res_randn:.3f}x" ) ttnn.close_device(device) print ( " \n appendix" ) res_torch = bench( "torch.randn" , lambda i: torch_randn(SHAPE, dtype=torch.float32, seed=i), iters=ITERS, warmup=WARMUP) 4つの Tenstorrent Wormhole™ n300s カードを搭載したTT-LoudBoxサーバで実行した結果が次のようになります。 従来のop組み合わせ(rand×2 + log/sqrt/sin/cos/mul + concat)の実装に比べて、今回fused kernelを実装して約4倍の高速化が達成しました。 ちなみに、CPU(Intel® Xeon® Silver 4309Y)の torch.randn で実行したものと比べるとアクセラレータによる並列実行の恩恵を感じることができると思います。 ttnn.rand + Box-Muller: 0.344376 ms/iter ttnn.randn: 0.085173 ms/iter Speedup: 4.043x appendix torch.randn: 4.509201 ms/iter また、出力されたサンプルの分布を可視化しても標準正規分布として問題ないことが次のように確認できました。 import ttnn, matplotlib.pyplot as plt, numpy as np device = ttnn.open_device(device_id= 0 ) x = ttnn.randn( ( 1 , 1 , 1024 , 1024 ), device=device, dtype=ttnn.float32, layout=ttnn.TILE_LAYOUT, memory_config=ttnn.DRAM_MEMORY_CONFIG, seed= 1234 , ) x = ttnn.to_layout(x, ttnn.ROW_MAJOR_LAYOUT) x = ttnn.from_device(x) x = ttnn.to_torch(x).cpu().numpy().ravel() mean = np.mean(x) var = np.var(x) plt.figure(figsize=( 6 , 4 )) plt.hist(x, bins= 100 , density= True , alpha= 0.7 ) plt.axvline(mean, linewidth= 2 , label=f "mean = {mean:.6f}" ) plt.axvspan(mean - np.sqrt(var), mean + np.sqrt(var), alpha= 0.2 , label=f "var = {var:.6f}" ) plt.title( "Histogram of ttnn.randn()" ) plt.xlabel( "Value" ) plt.ylabel( "Probability Density" ) plt.grid( True ) plt.legend() plt.tight_layout() plt.savefig( "fig.png" ) ttnn.close_device(device) まとめ 本記事では、TenstorrentのAIアクセラレータアーキテクチャとその特徴を紹介しました。また、fused kernelによる具体的な最適化の実装方法と従来手法と比較して約4倍の高速化を達成する性能評価結果を共有しました。 明日のアドベントカレンダーもお楽しみに。 Dao, Tri. "Flashattention-2: Faster attention with better parallelism and work partitioning." arXiv preprint arXiv:2307.08691 (2023). ↩ Shah, Jay, et al. "Flashattention-3: Fast and accurate attention with asynchrony and low-precision." Advances in Neural Information Processing Systems 37 (2024): 68658-68685. ↩ Box, George E. P. and Mervin E. Muller. “A Note on the Generation of Random Normal Deviates.” Annals of Mathematical Statistics 29 (1958): 610-611. ↩ https://github.com/tenstorrent/tt-metal/blob/main/METALIUM_GUIDE.md#spmd-in-metalium ↩
この記事は、 NTT docomo Business Advent Calendar 2025 18日目の記事です。 みなさんこんにちは、イノベーションセンターの田口です。 普段はOffensive Securityプロジェクトのメンバーとして攻撃技術の調査・検証に取り組んでいます。 私たちのチームでは定期的に技術LTと称して、各メンバーが自由に技術的知見を共有する時間を設けています。 この記事では、私が技術LTの場で発表したAI駆動型マルウェア 1 の動作デモと、発表後のチーム内議論の様子について紹介します。 この記事では、AI駆動型マルウェアの概念を理解するために、実害のないデモ用PoCを作成してその挙動を確認しています。 また、PoC悪用の可能性を考慮して検証の中で作成したコードやプロンプトの全体公開は控えさせていただきます。 Offensive Securityプロジェクトについて AI駆動型マルウェアとは AI駆動型マルウェアの動作デモ 動作デモの解説 チーム内での議論 どれくらい複雑な機能を動的生成できるのか? 攻撃者視点のメリットはなに? 攻撃ベクトル発展の可能性について おわりに 参考 Offensive Securityプロジェクトについて Offensive Securityプロジェクトでは、攻撃者視点のセキュリティ(Offensive Security)を専門とするチームとして、攻撃技術の調査・開発・検証に取り組んでいます。 攻撃者に先んじて新たな攻撃技術を検証することで、将来の脅威を見越した防御の強化につなげています。 主な業務内容として、NTTドコモビジネスの WideAngleプロフェッショナルサービス における攻撃技術の検証支援や、最先端の攻撃技術に関する応用的な研究開発を行っており、 成果のカンファレンス発表など対外的な活動にも積極的に取り組んでいます。 AI駆動型マルウェアとは AI駆動型マルウェアとは、大規模言語モデル(LLM)やAIエージェントの能力を攻撃プロセスの一部に利用するマルウェアの総称です。 2025年7月に「LAMEHUG 2 」、8月に「PromptLock 3 」と呼ばれるマルウェアが観測されました。 攻撃者によるAI利用の事例は以前からありますが、これらのマルウェアは新しいアプローチでAI利用がされており話題になりました。 特徴はマルウェア内に外部のAIと通信して悪性コードを生成させる手法、いわゆるバイブコーディング 4 の手法がマルウェアの機能に組み込まれていることです。 従来のマルウェアは攻撃者が事前に用意した静的な悪性プログラムを実行しますが、AI駆動型マルウェアはプロンプトに従いAIが環境に応じて必要なプログラムを動的に生成・実行します。 AI駆動型マルウェアの動作デモ AI駆動型マルウェアの実装・挙動の理解促進を目的としてデモ用のPoCを作成しました。 このデモでは、PoCの実行によりターゲットフォルダ(sandbox)をzipファイルへ圧縮する動作を示します。 下記はPoC動作中のコンソールを表示したバージョンです。 動作デモの解説 上記のデモは、以下のような順で動作しています。 動画内ではVBScript、batファイルを実行していますがプログラム本体はsrcフォルダ内のPythonコードです。 (ユーザ操作)PoCを実行する。 インターネット上のLLMとの通信を開始する。 LLMからのレスポンスを基に新たにプログラム generated_recon.py を作成する。 generated_recon.py を実行する。 sandboxフォルダをzipに圧縮して、distフォルダへ出力する。 上記動作における2 ~ 4の流れがAI駆動型マルウェアの特徴的な動作になります。 今回の動作デモではsandboxフォルダを対象としたzipへの圧縮までを実装していますが、 実際の攻撃では圧縮対象範囲の拡大や圧縮ファイルの外部送信といった機能が追加で作成されると考えられます。 LAMEHUGの事例では、コンピュータ内における広範な情報収集とSFTPおよびHTTPを利用したデータの外部送信をしたと報告されています。 検証を通して作成されたgenerated_recon.pyの一例を下記に示します。 これらのコードはPoC内のプロンプトに応じてLLMが動的生成したコードです。 同じ機能を持ちますが、見かけ上は異なるコードとして生成される場合があります。 import os from pathlib import Path from zipfile import ZipFile, ZIP_DEFLATED from datetime import datetime def main (): # 1. Set base_dir to "sandbox" under this script's directory base_dir = Path(__file__).parent / "sandbox" if not base_dir.is_dir(): print (f "'sandbox' directory not found at {base_dir}" ) return # 2. Recursively find all files in sandbox file_list = [] for root, dirs, files in os.walk(base_dir): for file in files: abs_path = Path(root) / file rel_path = abs_path.relative_to(base_dir) file_list.append((abs_path, rel_path)) # 3. Prepare output ZIP path dist_dir = Path(__file__).parent / "dist" dist_dir.mkdir(exist_ok= True ) timestamp = datetime.now().strftime( "%Y%m%d%H%M%S" ) zip_name = f "sandbox_snapshot_{timestamp}.zip" zip_path = dist_dir / zip_name # 4. Write files into ZIP file with paths relative to sandbox with ZipFile(zip_path, "w" , ZIP_DEFLATED) as zipf: for abs_path, rel_path in file_list: zipf.write(abs_path, arcname= str (rel_path)) # 5. Optional: Append to log log_path = Path(__file__).parent / "recon_log.txt" with open (log_path, "a" , encoding= "utf-8" ) as logf: logf.write(f "[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Zipped {len(file_list)} files from sandbox to {zip_name} \n " ) if __name__ == "__main__" : main() 今回のPoCでは実験中の想定外の動作を防ぐために、下記のような安全面を考慮した関数を実装しています。 def basic_safety_check (code: str ) -> None : """ デモ用の簡易チェック(Windows でも共通で危険そうなものをざっくり禁止) """ forbidden_keywords = [ # 削除系 "os.remove" , "shutil.rmtree" , "os.rmdir" , "unlink(" , # ネットワーク系 "socket." , "requests." , "httpx." , "urllib." , "ftplib." , # 環境・プロセス系 "os.environ" , "subprocess.Popen" , "subprocess.call" , "subprocess.run(" , # Windows レジストリ "winreg" , # 何でも実行系 "eval(" , "exec(" , ] for kw in forbidden_keywords: if kw in code: raise RuntimeError (f "安全のため禁止キーワードが検出されました: {kw}" ) チーム内での議論 技術LTの質疑応答時間で出た発言について、いくつか抜粋して紹介します。 どれくらい複雑な機能を動的生成できるのか? 私: どれくらい複雑な攻撃コードを動的生成できるかはLLMの性能に依存している。 AI駆動型マルウェアの事例に限らず、今後どれくらい攻撃コード生成の能力をAIが備えるかは注視していきたい。 メンバーA: デモプログラムや観測された検体では、ファイル列挙や外部通信などの簡易な機能のみを実装させているが、 アンチウイルスやEDRの検知回避などの、より攻撃者が実現したい高度な機能を動的に生成させることが現段階でできるのか、 あるいは将来的にできるようになるのかといった観点で追加調査するのも良さそう。 攻撃者視点のメリットはなに? 私: ランダム性のある攻撃コードを動的に生成するという挙動はシグネチャ型検知を回避しやすいというメリットがありそう。 高度なコーディング能力を持たない攻撃者でも扱えるというメリットもあるかも。 メンバーB: 事例やデモプログラムで生成させてる機能は簡易なもの(悪性とは言い切れない)なので、 本格的な悪性コードを動的生成させるような実装になったとき、検知回避の観点でどれくらい通用するのか気になる。 メンバーC: シグネチャ型検知を回避する方法は他にもあると思うので、わざわざAIに動的生成させるというのは回りくどいやり方な気がする。 そういう意味ではコーディング能力を持たずとも自然言語でコード生成できる点のほうが現状はメリットとして強そう。 私: 従来のペイロードを攻撃者基盤からダウンロードしてくる手法とAIで動的に生成する手法とで、 実際にステルス性の違いがあるのかという観点で調査するのも価値がありそう。 攻撃ベクトル発展の可能性について メンバーB: AI駆動型マルウェアが流行ってくると意図せず公開されたAIサービスの悪用や、 流出したAIサービスのAPIキーを悪用することで攻撃者の痕跡を隠すケースが出てくる気がする。 私: 同じようなことが気になっていて、今後エンタープライズ向けのCopilot系サービスが普及した世界になったとき、 企業環境内のモデルを悪用したAI駆動型マルウェアとかが登場してくる可能性があるのではと思った。 (参考: 業務で進むLLM活用、その裏に潜む脅威とは?Microsoft 365 Copilotを介した攻撃検証(インターン体験記) ) おわりに この記事では、AI駆動型マルウェアの概要説明およびデモ用PoCを発表した技術LTの内容について紹介しました。 今後もAIが普及・進化した世界で起こり得る脅威について調査を続けていき、得られた技術的知見については共有していきたいと思います。 明日は鈴ヶ嶺の記事です!お楽しみに! 参考 AI駆動型マルウェアとは何か?~ Vibe Codingを駆使するLAMEHUG、PromptLockを解説 ↩ CERT-UAによるLAMEHUGに関するレポート ↩ ESET ResearchによるPromptLockに関するレポート ↩ wikipedia - バイブコーディング ↩
この記事は、 NTT docomo Business Advent Calendar 2025 17日目の記事です。 はじめに 本記事は、コミュニケーション&アプリケーションサービス部でビジネスdアプリの開発を担当している 立木・富田・西谷 の共同執筆です。 私たちは、 ビジネスdアプリ という、ビジネスパーソンの仕事や生活に役立つポータルアプリを開発しています。 ビジネスdアプリチームでは、モバイルアプリ・フロントエンド・バックエンドなどの開発とデザインの一部を社員で内製しており、以下の記事で以前記載したようにGoogle Cloudのサーバーレスサービスをフル活用しています。 サーバレスをフル活用したビジネスdアプリのアーキテクチャ(前編) サーバレスをフル活用したビジネスdアプリのアーキテクチャ(後編) このため、私たちのチームではGoogle Cloudのプロフェッショナルレベルの資格をチームメンバー全員が1年間に1個以上取得するという目標を立て、昨年度は全員目標を達成しています。 その中でも、同チームのメンバー3名(立木・富田・西谷)が Google Cloud 認定資格の全冠(※2025年8月時点)を達成 し、 Google Cloud Partner All Certification Holders 2025 に選出されました ! 受賞者の一覧は こちら 本記事では、社内で知見を共有することを目的として、全冠者による座談会 を開催したので、その内容をまとめてご紹介します。 はじめに Google Cloudの資格について 資格レベルと有効期限 座談会 当日の流れ テーマ 資格取得を始めた理由 学習のコツやTips(英語の試験対応) どの資格が大変だったか 参加者からの質問・コメント まとめ Google Cloudの資格について まず、Google Cloud 認定資格とは何かについて説明します。 Google Cloud 認定資格は、Google Cloud に関する知識・スキルを公式に証明するための資格です。 Google Cloudを使ったクラウドアーキテクチャ設計、データ分析、セキュリティ、ネットワーク、機械学習、DevOps、Workspace管理など、資格ごとに求められる専門性が異なります。 このGoogle Cloudの認定資格を全て保持している、パートナー企業所属のエンジニアを表彰するプログラム 「Google Cloud Partner All Certification Holders」 が、今年から始まりました。 2025 年版では、2025年6月1日時点で一般公開されているすべての認定資格が対象となり、合計13資格が要件として定められています。 言語は不問で、バージョン更新に伴うベータ試験も対象です。また、Associate Google Workspace AdministratorについてはProfessional Google Workspace Adminstrator(PGWA)で代替可 とされています。 また、2025 年版の募集は 2025年8月18日に公式発表 され、8月18日〜9月30日の約1か月間が応募期間、10月に審査、11月に選出者発表 というスケジュールで実施されました。 対象資格は以下の通りです。 Cloud Digital Leader Certification (CDL) Generative AI Leader Certification (GAIL) Associate Cloud Engineer Certification (ACE) Associate Data Practitioner Certification (ADP) Associate Google Workspace Administrator (AGWA)(PGWAでも可) Professional Cloud Architect Certification (PCA) Professional Cloud Developer Certification (PCD) Professional Data Engineer Certification (PDE) Professional Cloud Security Engineer Certification (PCSE) Professional Cloud Network Engineer Certification (PCNE) Professional Cloud DevOps Engineer Certification (PCDE) Professional Cloud Database Engineer Certification (PCDBE) Professional Machine Learning Engineer (PMLE) ※2025年12月時点では、上記に加えて Professional Security Operations Engineer (PSOE) が新たに追加されています。 資格レベルと有効期限 Google Cloudの資格は、主に以下の 3段階のレベル に分類されています。 Fundamental クラウドの基本概念や Google Cloud の概要理解が中心。 クラウド未経験者やビジネス職にも向けた入門レベル。 有効期限:3年 Associate 実践的な Google Cloud の利用スキルを問う技術者向けレベル。 特に 運用・デプロイ担当者 や、これから Professional を目指すエンジニアの基礎固めに相当。 有効期限:3年 Professional クラウドアーキテクチャ設計、セキュリティ、データ分析など、専門領域で高度なスキルが求められる最上位レベル。 難易度が最も高く、試験自体の設問も実務寄り。 有効期限:2年 Google Cloud Partner All Certification Holders に申請する際は、全ての資格を有効期限内にする必要があリます。このため、更新時期の管理が重要 になります。 座談会 ここからは、9月末に社内で行われた座談会の内容について紹介します。 Google Cloudの全資格を取得した、年次の異なる立木(若手)・富田(中堅)・西谷(ベテラン)の3名が、それぞれの視点で8つのテーマで座談会を行いました。 当日の流れ 当日は、Google Cloud Japanのテクニカルマネージャーの方をお招きし、オンラインで1時間行いました。 オープニングから始まり、まずGoogle Cloud Japanの方からGoogle Cloudの資格制度に関する説明がありました。 その後、座談会の登壇者3名(立木・富田・西谷)から自己紹介があり、座談会に進むという流れで行いました。 具体的な流れは以下の通りです。 オープニング Google Cloud認定資格・学習制度の説明(Google Cloud Japan テクニカルマネージャー) 自己紹介 座談会 質疑応答 テーマ 座談会のテーマは以下の内容でした。 8つのテーマについて話し、最後に質疑応答を行いました。 今回の記事では、その中でもいくつかのテーマをピックアップして、内容を紹介します。 資格取得を始めた理由 3名とも 「体系的な知識習得」 が大きな理由でした。 Google Cloudはサービスが幅広く、業務で使う範囲だけでは偏りが出やすいため、資格取得を通じて全体像を把握したいという共通のモチベーションがありました。 また、クラウドの知識がない若手社員にとっても、体系的な知識を得られるので、良い教材になりそうです。 立木(若手) :入社直後クラウドに関する知識が全くなく、知識を得たかったため、まずはアソシエイト資格(ACE)から着手。 富田(中堅) :普段触れないサービスも知識として得て、業務に活かしたいという思いからスタート。 西谷(ベテラン) :2018年に内製開発を始めた当初、AWSに比べて日本語情報が少なかったので、体系的に学ぶため資格取得を決意。年齢問わず、挑戦できることを示したかった。 学習のコツやTips(英語の試験対応) 3名とも 公式のサンプル問題などの模擬問題集を活用 し、 Google Cloud Skills Boostで実際にサービスを動かす ことで、理解の定着を図っていました。 また、ハードルの高い英語の試験では、模擬問題集を解く際にブラウザの翻訳機能などを用い、時間の短縮をしていました。 立木(若手) :公式のサンプル問題や市販の模擬問題集を活用。英語は翻訳ツールを使い、専門用語も覚えるようにした。 富田(中堅) :試験対策マニュアルやネットの情報で全体像を掴み、Udemyなどの模擬試験で実践。 西谷(ベテラン) :苦手分野はGoogle Cloud Skills Boostで実際にサービスを動かして学習。英語試験はブラウザ翻訳機能を活用。 どの資格が大変だったか 3名とも「Professional Machine Learning Engineer」・「Associate Google Workspace Administrator」・「Professional Cloud Network Engineer」など、 業務で普段使わない領域の資格 の取得に苦労したようです。 特に、 「Professional Machine Learning Engineer」 と 「Associate Google Workspace Administrator」 の2つは、3名中2名が特に学習に時間がかかった資格としてあげていました。 普段馴染みのない専門用語を覚える必要があり、難易度が高かったようです。 立木(若手) :「Professional Machine Learning Engineer」と「Associate Google Workspace Administrator」。試験を受けた当初は英語しかなく、用語も覚える必要があったため、難しかった。 富田(中堅) :「Professional Machine Learning Engineer」と「Associate Google Workspace Administrator」。業務で触れた経験がほとんどなく、理解に時間がかかった。 西谷(ベテラン) :「Professional Cloud Network Engineer」。普段サーバー開発中心の業務でネットワーク構成(BGP、専用線など)に触れる機会が少なかったため。 参加者からの質問・コメント 座談会の参加者からは「全冠維持のモチベーションはどこから来るのか」や、「IAM関連の学習で役立ったコンテンツは何か」などのさまざまな質問がありました。 登壇者からは「全冠取得という目標の分かりやすさや、Google Cloud Partner All Certification Holdersで表彰されることがモチベーションになる」といった話や、Google Cloud Skills Boostなどの具体的な学習コンテンツに関する情報があり、参加者のGoogle Cloudの資格取得のモチベーションを高める良いきっかけになったようです。 まとめ 本記事では、Google Cloud認定資格の概要と、Google Cloud Partner All Certification Holders 2025 に選出されたメンバーによる社内座談会の様子、そこから見えてきた学習のポイントについて紹介しました。 座談会を行うことで、参加者のGoogle Cloud認定資格の取得のモチベーションにつながり、良いきっかけとなりました。 皆さまももし興味があれば、Google Cloud認定資格の全冠を目指されてはいかがでしょうか! それでは、明日の記事もお楽しみに! ※ 私たちが開発しているビジネスdアプリに興味を持った方は、 公式ページ をご確認ください。 社内報やタスク管理など、私たちが開発している機能一覧が記載されています。
この記事は、 NTT docomo Business Advent Calendar 2025 の16日目の記事です。 社内サークルのメンバーでハッカソンに参加した結果、気が付いたら競馬の冠レースを開催していたという活動報告です。 はじめに AIロボット部の紹介 参加したハッカソンの概要 アイデアの背景とコンセプト 「でっかい」縛り 100均アイテム縛り 「スマホのみ」縛り 「お腹」に関連した機能 制約と創造性 結果はオーディエンス賞!そして風変わりな賞品を頂く 船橋競馬場にて「みかかロボット杯」開催! おわりに はじめに こんにちは、AIロボット部のサークルメンバーの宮岸( @daiking1756 )です。 普段は5G&IoTサービス部で映像系サービスの企画やプリセールス的なお仕事をしています。 この記事では、AIロボット部のメンバー2名で参加した風変わりなハッカソンの模様と、風変わりな賞品をご紹介します。 AIロボット部の紹介 AIロボット部はものづくり系の社内サークルです。 普段はSlackで情報交換しながらメンバーが個人で作りたいものを作っています。 昨年のアドベントカレンダーの記事でもAIロボット部の活動を書いています! engineers.ntt.com 参加したハッカソンの概要 2025年3月、私たちは「創発遊戯 2025」という社会人向けのハッカソンに参加しました。 同ハッカソンが2024年に開催されたときの様子を、私がSNS上で見ていて、ユニークなハッカソンだなーと思っていました。 そんな時に、2025年開催の情報を聞いてAIロボット部のメンバーに声を掛けたのが、参加のきっかけでした。 このハッカソンはチーム毎に異なる「縛り」が適用されるという特徴があります。 縛りは「テーマ」「技術・素材」「その他 環境・条件」のカテゴリに分かれており、各カテゴリに約10個の縛りが用意されています。 各チームで選べるのはカテゴリと縛りの個数(最大4つ)までで、どの縛りが選ばれるかはランダムです。 今回我々は各カテゴリから1つずつ縛りを選びました。 選ばれた縛りは下記です。 縛りカテゴリ 縛り内容 テーマ 「でっかい」 技術・素材 100円均一ショップで買った素材を3つ以上取り入れる その他 環境・条件 「スマホのみで」どこまでできるか、チャレンジする 他の参加チームでは「本業に関するなにか」や「シラフ禁止」などを引いており、どのチームも頭を悩ませながらも楽しそうに開発していた印象です。 うちのチームは「スマホのみ」縛りがかなり重いですが、やれるところまで挑んでみることにしました。 その他ハッカソンの詳細は下記に記載されています。 https://mashupawards.connpass.com/event/344340 mashupawards.connpass.com アイデアの背景とコンセプト 簡単にどんな作品を作ったのか紹介します。 我々は SMART HARAMAKI という腹巻き型のウェアラブルデバイスを開発しました。 「でっかい」縛り 「でっかい」という縛りに対し、普段小さいものを大きくするという逆転の発想から、スマートウォッチの巨大版を作ることにしました。アイデアが生まれました。 大きさ的に手首ではなくお腹に巻くようなものになりそうだったので、腹巻き型デバイスにしました。 100均アイテム縛り 100均アイテムの縛りに対しては、振動モーターは100均の電装ハンディプッシャーから分解して調達したり、スマホに拡大鏡を取り付けることで画面を巨大化させて「でっかい」に結びつけるなどの試みを行いました。 ハッカソン期間中、近所の100均をハシゴした回数は片手で収まりません。(たまたま3種の100均が徒歩圏内にあった) 「スマホのみ」縛り 一番大変だったのが「スマホのみ」縛りです。 序盤はWebアプリ側のベース実装はスマホからVibe Codingするなどで、スマホでの開発の新鮮さを楽しんでいました。 しかし、マイコン(ESP32)にプログラムを書き込む工程で苦戦してしまいます。 1 次第にスマホの画面サイズでの開発や、マイコンとの接続不調に心が折れてしまい、PCでの開発に泣く泣く切り替えました。 スマホの画面内での開発はさすがにキツくなってきた😇 #創発遊戯 pic.twitter.com/pl58tmG2wX — みやぎdaiking⊿🌗 (@daiking1756) 2025年3月14日 x.com 「お腹」に関連した機能 腹巻き型デバイスを作っていく中で、「お腹」に関連した機能として下記の機能を実装しました。 満腹時計: お腹の膨らみを圧力センサーで検出して食事中の満腹度を可視化する。 食べ物レーダー: 4カ所に設置したモーターの振動によって近くの飲食店の位置を伝える。「お腹が震える方向に歩けばご飯に辿り着く」体験を 実現。 私はウェブアプリ側の開発を担当したのですが、デバイス側を担当したメンバーからは「腹巻きに基盤を縫い付けるのが大変だった」という予想外の視点からのコメントがありました。 また、今回のハッカソンはリモート開催だったのですが、デバイスが中心となるプロダクトであっため、期間中に1度だけ出社してオフラインで作戦会議をしました。 モーターをLEDに変えたデバッグ用の回路を相方から受け取ったことで、その後の開発をスムーズに進めることができました。 デバイスの制作過程や作品の詳細は下記のページに記載しています。 protopedia.net 制約と創造性 「スマホだけで作れ」という縛りを引いた瞬間、正直『詰んだかも』と思いました。 でも、 創造的制約の力 の動画でも語られているように、制約って不思議で、逆にアイデアがどんどん湧いてきたんです。 知識としては知っていたものの、制約が新たな創造のきっかけになることを今回のハッカソン中に肌で感じました。 この記事を読んだ方が創発遊戯に参加することがあれば、縛りはMAXの個数を選ぶことをオススメします! 少し話はズレますが、生成AIに入力するプロンプトも出力結果への制約を課しているとみることができます。 制約が少ない・抽象的だと、欲しい出力が得られないということは、AIに置き換えてみると私たちは実体験としてよく知っているかと思います。 結果はオーディエンス賞!そして風変わりな賞品を頂く 参加者による相互投票の結果、SMART HARAMAKIはオーディエンス賞を受賞しました🙌 そしてオーディエンス賞の賞品として贈られたのが、なんと 地方競馬の賞レース冠権 でした。 (下記の通り賞品は三択から選べたのですが、一番楽しそうなやつにしました) オーディエンス賞の賞品!! A~Cのどれかを選べる! #創発遊戯 pic.twitter.com/EB1aIBAznl — ひげだるま (@masaya3) 2025年3月13日 x.com 船橋競馬場にて「みかかロボット杯」開催! 全国に15か所ある地方競馬場の中から、運営さんとも相談し、メンバーの家からも近い船橋競馬場で開催して頂くことにしました。 そして、2025年11月7日、ついに船橋競馬場にて、我々の冠レースが開催されました。 レース名は「みかかロボット杯」です! ハッカソン参加時のチーム名「みかかロボット部」が由来です。 レース当日の様子は下記でも投稿していますが、来賓室から見るレースは一味違う体験でした。 (と言っても、メンバー2人とも競馬場に行くのはほぼ初めてでしたが) 個室の来賓室から見るレースは最高でした🙌 予想も的中させることができ、みかかロボット部初のサークル遠足は大成功でした🏇 今回作成頂いた横断幕は今後サークル内で大切に使わせて頂きます。 pic.twitter.com/nmht7UXWtw — みやぎdaiking⊿🌗 (@daiking1756) 2025年11月7日 x.com おわりに 簡単ですが、創発遊戯への参加報告および船橋競馬場での冠レース「みかかロボット杯」の開催報告でした。 社内サークルでの活動でまさか競馬場に行くとは思っていませんでした。 次のサークル遠足はどこに行くのか、楽しみです。 また、ハッカソンは毎回何か新しい発見や学びがあるので、今後も継続的に参加していきます。 今回参加した2名ともハッカソン期間中に確定申告に追われながらの開発となってしまったことが反省点でした。 3月中旬のハッカソンに参加する際は、事前に確定申告を終わらせておくことをオススメします。 今後も、AIロボット部では遊び心と技術力を融合させたチャレンジを続けていきます。 次回の活動報告も乞うご期待ください! それでは明日の記事もお楽しみに〜👋 ArduinoやESP32系マイコンにSketchを書き込める ArduinoDroid というAndroid向けアプリがあるようですが、私の手元の端末では動作しませんでした。 ↩
この記事は、 NTT docomo Business Advent Calendar 2025 の15日目の記事です。 皆さまどうもこんにちは、 @strinsert1Na という人です。以前は 株式会社エヌ・エフ・ラボラトリーズ という会社に出向しながらバリバリ「脅威インテリジェンス」のお仕事をしておりまして、今年の7月からはNTTドコモビジネス 情報セキュリティ部の管理職として新たなキャリアを歩んでおります。 ここまで書くと「なんだ、ただの順調にキャリア形成している人のめでたい話か」と思われてしまいそうですが、そんなことはありません。正直なところ、ほぼ毎日「(あんなやり方でよかったんだろうか…)」という不安の言葉が頭の中を駆け巡るような日々を過ごしており、半年経過した現在でもプレイヤーと管理職のロール/責任の違いにてんてこまいな状態です。 この記事では、社会人人生をずっと”エンジニア(プレイヤー)”としてだけバリューを出し続けてきた筆者が、管理職になって発生した苦悩やモヤモヤ、そしてそれを解決するためにとったアプローチについてまとめたいと思います。 もし同じような境遇になって苦しんでいる人や、これからエンジニアリングマネージャーのキャリアを目指す人にとって何かしらの希望になれば幸いです。 ひとよひとよに管理職(マネージャー)… ふたやく以上での立ち回り…!? みつめられてもなんにも出ないですよ…? (1on1の話題) ウェルカムトゥ影(ダーク)サイト 超絶進化のマネージャー生活この手につかむ! 1. チームのふりかえりで出た良し悪しを自身の良し悪しへ転換する 2. 自身がしっくりくる “エンジニアリングマネージャー” の型にそっくりハマるよう動いてみる それ(憧れのマネージャー)目指して歩き出してきたんです確か… ひとよひとよに管理職(マネージャー)… まずは筆者がプレイヤー時代にどんな働き方をしていたかについて、経歴を含めて紹介します。 一言でまとめると「子会社出向して技術力とアウトプットの魅せ方に注力を置いていたら、プレイヤーとして高く評価された」といった状況です。 旧NTTコミュニケーションズに入社、新入社員研修を終え1年目で株式会社エヌ・エフ・ラボラトリーズへ出向 そこから約6年間、エンジニアリングチームでセキュリティプロダクトの開発・運用・マルウェア解析、脅威インテリジェンスの生成業務をプレイヤーとしてひたすら頑張る チームメンバーはマネージャーを除くと20代から30代のみで、技術獲得に対するモチベーションが非常に高い 雑談の場でも共通の話題で盛り上がりやすい マネージャーっぽい動きはほとんどしてこず、ただただ向上した技術力で顧客が喜ぶモノを高打率で作れるようになったことと、アウトプットの魅せ方が多少うまかったという理由だけで良い評価をいただく場面が多かった こんな状況が現場でしばらく続いた結果、とんとん拍子で昇格して社会人8年目でターニングポイントが来ました。 それはスペシャリストになるかマネージャーになるかの選択です。 ここまで読んでいただいた方は「いやいやそんなに技術一本でやっていくならスペシャリスト選択しろよ」と思うかもしれません。 しかしながら、NTTドコモビジネスの場合はスペシャリストのロールにリーダーとしてチームをリードするまでの権限がなく、チームを作っていきたいと思うならマネージャーの道を選ぶ必要があります。 筆者はこれまで仕事をしていく中で技術をとことん極めるというよりも「よいエンジニアリングチームを作っていきたいなぁ」という気持ちが勝り、マネージャーとなる選択をしました。 選択をした当時は「これまでのマネージャーの動きやチームビルディングの方法論は知っているし、大丈夫なはず!」と思っていました。 ですが筆者はこの7年間でたった2つの(しかもバリバリのエンジニア集団のみで構成された)チームしか知りません。この考えが浅はかすぎるということを、筆者は後々知ることとなります。 ふたやく以上での立ち回り…!? このような背景のもと、筆者は親会社のNTTドコモビジネスに戻り晴れてそこで10人チームのマネージャーをすることになりました。 「さて、これから良いチームを作っていくぞー」と意気込んだところですぐ問題に直面します。 「……あれ、タスクってどうやってメンバーに振っていけばいいんだろう?」 ……何を言っているんだこいつは? と思った方がいらっしゃるかもしれませんが、実は筆者はこれまでの仕事で「タスクをトップダウンで割り振る」ということをしたことがありませんでした。 筆者が所属していたチームはこれまでスクラムで業務を行っており、みんなが実施するタスクはスクラムボード上に作られたバックログで管理され優先度が高い順にメンバーが自主的にとっていく形式をとっていました。 しかしながら、任されたチームにはそもそもタスクが可視化されているボードがありませんでした。 全ての仕事はマネージャーが管理していて、それを適切にメンバーに分配、進捗報告会で状況把握と方向性を決めていきます。 つまり、楽しそうな仕事も面倒な仕事も、全てはマネージャーの決断によってメンバーの誰が作業をするかが決まる組織体系だったのです。 それでは「じゃあ個人にダイレクトメッセージを送ってタスクをお願いしていくぞ!」という気持ちで再スタート…..しようとして、すぐに手が止まります。 それは、管理職研修で何度も聞かされたエンゲージメントやメンタルヘルスケアの問題です。 おそらく JTC で管理職になった皆さんは、成り立ての頃に嫌というほどさまざまな ”管理職研修”というものを受けたのではないかと思います。 筆者も例外ではなくもちろん多くの研修を受けましたが、現状大企業の多くには「エンゲージメント向上」に関する研修も含まれているかと思います。 エンゲージメントとは企業や仕事との結びつきを数値化した概念でこのスコアが高いほど生産性の高さなどにつながると言われていますが、厳しいことに近年のチームのKPIにエンゲージメントスコアの向上も含まれております。 「『複数あるチャネルから該当の項目数数えろ』なんて面倒な仕事をバンバンお願いしてエンゲージメントスコア下がったらヤバイのか…?」そんな、本職とは全く関係ないことが頭をチラつくのです。 組織構造と組織文化と筆者自身の特性が全く噛み合わず、判断がバグり始めます。 結論どうなったかというと、巷でよく聞く「プレイングマネージャー」の爆誕です。 筆者がインターネット上でよく聞くケースは「仕事量が膨大でプレイングをせざるを得ない」というケースなのですが、もしかしたら筆者のような「エンゲージメントなどの新たなベクトルで生まれたKPIの未達を恐れて、面倒なタスクはマネージャーが巻き取る」ケースもそこそこいるのではないかなと思っております。 ただしこうなると、マネージャー業務がどんどん溜まっていって首が回らなくなっていきます。「こういう時はどうすればいいんだ…? せや! 管理職研修でやらされた “1on1” で興味関心を聞きながら、仕事をお願いしていったらええやん!!」 みつめられてもなんにも出ないですよ…? (1on1の話題) そんなモチベーションで始まる 1on1 も、多くの JTC 管理職を悩ませた(ている)施策ではないでしょうか。 最近の管理職研修には 1on1 の実施そのものが含まれていて、おそらく多くの JTC では半強制的に導入されていることでしょう。この記事を読んでいる皆さまも、1on1をすでに経験されたことがあるかと思います。 筆者自身もこれまで何度も上司にセッティングされた1on1に参加し、何1つ苦に感じることなく会話をしてきました。 ですが、実はセッティングする側になってみるとなかなか難しいことがわかります。 何が難しいかというと、前述のような筆者のモチベーションで1on1をセッティングすると、社員の成長よりも「筆者自身がどう仕事をうまくアサイン/コントロールするか」に焦点がいってしまう点です。 こうなってしまうとメンバー視点の1on1は「面倒な仕事が振ってくる場所」になってしまって成長よりも遥かに「苦痛の場」です。 なので、なるべく筆者の事情は伏せてうまく仕事の悩みや最近の出来事などを引き出そうとするんですが……会話が中々出てこないんですよね。 以前筆者が所属していたチームは皆年齢層が近い職場で専門性も熟知していたので、好きな技術スタックの話やネットミームを口から垂れ流していれば楽しみながらモチベーションの向上や成長へつながる方向に会話が弾みました。 しかしながら、筆者が新たに着任したチームの年齢層は20代から50代までさまざまであり、メンバー全員テクニカル領域が好きなわけではありません。 筆者の浅い経験では会話の種に詰まったり、「(自分よりも倍以上の社会人経験がある人にコーチングするのも、プレッシャーがあるな…)」と勝手に対話に難しさを感じたりして、自身が経験してきた「感触の良い1on1」を実現できている気が全くしませんでした。 また、筆者のケースではセキュリティエンジニアとして同期入社した人もチームメンバーに含まれており、コンプライアンスの都合で上司としての立場の発言は慎重にならなければならないと教えられる昨今では、どういう心持ちで1on1の場に臨めばいいのかも悩みの1つになりました。 このような背景から、1on1中は「(この後どういい方向に持っていったらいいんだろう…..)」と考えるシーンが増え、結果としてお互いみつめあう無言の場面やおそらく仕事の成長につながらないであろう話が続いて1on1を終えることもあります。 会社側からも月に1回程度の1on1は推奨されているし過去の経験からも1on1は重要な場であると理解しているのですが、「自分がやっている 1on1 はメンバーの成長の糧になっているんだろうか」と、毎回1on1をセッティングする度に考えるようになりました。 ウェルカムトゥ影(ダーク)サイト このような事象が2~3ヶ月くらい続いた結果、筆者は俗に言う「インポスター症候群」と呼ばれる状態になりました。 直訳すると、自身の上司やメンバーから「この人全然マネージャーの仕事できてないじゃんって思われてるんだろうなぁ…」と考えながら仕事をする状態になったということです。 メンバー視点で見ると、発言や決断に自信がない管理職なんて不安になるだけです。 そのため、この症状を抱え続けていてもチーム全体の士気は下がりますし自身のパフォーマンスも悪くなるだけなので回復させたいのですが、ここでプレイヤーとマネージャーの大きな違いが壁として立ちはだかります。 それは、ポジティブ/ネガティブ両側面でのフィードバックをもらう機会が極端に少なく、具体的にどこをどう変えていけばいいのかがわからないということです。 プレイヤー時代は上司との面談を通して自身の良かった行動や成長のためのフィードバックが定期的に返ってくるため、その機会を通して自身の貢献を実感したり改善点に対してアプローチをかけることができました。 しかしながら、マネージャーになると目標達成に対するフィードバックはありますが、行動に対するフィードバックをもらえる機会はほぼありません。 それではチームメンバーから貰えるかというと、1on1で「何か筆者に対するリクエストあったら言ってね!」と発言して反応が返ってきたことはないですし、「(そりゃあ部下視点で面と向かって改善点を上司に話せるわけないよな)」とも発言する側視点でも思います。 つまり、自身のマネージャーとしての振る舞いは「セルフマネジメント」をすることによってその良し悪しを振り返り、改善をする必要があります。 これは多くの管理職にとっては常識なのでしょうが、これまで他者からのフィードバックで生きていた筆者にとってはすぐにセルフマネジメントの技術を身につけることができませんでした。 ですが、チームのパフォーマンスを出すためにもセルフマネジメントの技術を少しずつ身につけ、改善していく必要があります。 超絶進化のマネージャー生活この手につかむ! それでは、プレイヤーから上がったばかりの筆者は具体的にどう改善していったのか? というのを2点ほど書いていきたいと思います。 1. チームのふりかえりで出た良し悪しを自身の良し悪しへ転換する 1つめは主にメンタル面での改善です。 本来は行動の良し悪しを自己で振り返って評価し、改善するのが望ましい姿なのでしょうが、気持ちが落ち込み気味の状態で始めても上向きになるには時間がかかります。 そこで筆者は、チームに「ふりかえり(レトロスペクティブ)」の概念を導入し、チームメンバーの力を借りながらフィードバックを得る方向性にしてみました。 ふりかえりのフレームワークとして KPT/YWT などがメジャーなものとして存在しますが、これらのフレームワークではチームが試してみて”よかったこと”や”改善してほしいこと”などが各々のメンバーから率直な意見として表現されます。 これはチームの改善活動として非常に有意義なのですが、それとは別にチームが改善してみて”よかったこと”を筆者のマネジメントとして”よかったこと”へ、逆に”改善を要望している”部分を筆者のマネジメントにおける”改善のためのフィードバック”として捉えさせてもらうようにしました。 個人的には、ふりかえりの結果をこのように捉えるだけでマネージャーとしての仕事が格段にしやすくなりましたし、最もメンタルの改善にも効果的だったと思います。 新人マネージャー向けの研修を受けると「成果を出さなければと思っているメンタルモデル/固定観念を捨てよう」や「チームのためを思えば!」のような精神側へのアプローチが出てきますが、正直なところ精神が落ち込み気味のところでこのような言葉をもらってもほとんど回復はしませんでした。 それよりも、筆者のような他者からのフィードバックを事実として受け止めて改善したい人は、チームメンバーや上司から率直なフィードバックが出るような場をセッティングした方が、例えネガティブフィードバックが多くても、インポスター症候群を長引かせることなく改善に向かっていけるのではないかと思います。 2. 自身がしっくりくる “エンジニアリングマネージャー” の型にそっくりハマるよう動いてみる 2つめは行動面での改善です。 筆者が受けた管理職研修の多くは「大切な軸はこれ! でもマネジメントに正解はないから、課題出すからみんなで考えてみてね!! (完)」という形式が多く、「何かしら方法論を提示して欲しい、それに沿ってやってみるから」と思ったのが個人の感想になります。 その一方で、JTC の場合はさまざまな職種やコンテキストを持った人間が同じ管理職研修を受けるので、ベストプラクティスを一般化しにくいといった問題もあるのだろうとは思いますので納得もします。 しかしながら、ずっとエンジニアリング業務をしていた筆者にとっては経験ベースよりもまずは「何かしらのベストプラクティス」に沿って物事を始めていかないとモヤモヤするタイプですし、そこから改善するとしても元となる改善の土台が理に適っているかすらわからないと改善の方向性を決めるのも自身にとっては困難です。 そこで、一般的には”よくないこと”と言われそうですが、自身が最もしっくりくる「エンジニアリングマネージャー論」の型を探し、そこにハマるようにして動いてみました。 具体的には、一般的に提唱されている エンジニアリングマネージャーの4領域 ごとに自身の業務ドメインで必要となるカテゴリを洗い出し、それぞれのベストプラクティスに則る(例えば、1on1ならば「互いに事前に話す内容やトピックを共有しておいて、メモとして残しておく」など)とともに自身の得意領域を分析、その領域を強みとして自身のマネジメントの軸が構築されていくよう意識してみました。 具体的な4領域とその領域に含まれるカテゴリの例は以下です。 1 ピープルマネジメント (例: 1on1、チーミング) テクノロジーマネジメント (例: DevOps) プロジェクトマネジメント (例: アジャイル) プロダクトマネジメント(例: 仮設検証) この4軸を意識することで、「プロダクトマネジメントは苦手だけどやらないとエンジニアリングマネジャーとして最低限の仕事をこなせないから学ぼうか」「あのセキュリティの領域はテクノロジーの部分に入れてリードを取るのが必要だな」「1on1のカテゴリはほぼ必須要件だからこの本のやり方を模倣するか」といった自身の方向性と方法論が明確になり、1のふりかえりと合わせて格段に業務がクリアになりました。 特に筆者のような「理論がわからないと動くのが難しい」という方には、このような守破離の”守”を1つ作ってみることをお勧めします。 守を作っていく方法はさまざまあると思いますが、もう1つ私がよく参考にしていた本として エンジニアリングマネージャーのしごと ―チームが必要とするマネージャーになる方法 があります。 業界ではメジャーな本で上記の4領域よりもスコープが広いですが、マネージャーになると避けては通れない道が全て入っているのでお勧めです。 それ(憧れのマネージャー)目指して歩き出してきたんです確か… 新人エンジニアリングマネージャーの悩みを、徒然なるままに書かせていただきました。 罰ゲーム化する管理職 なんて本が世間で話題になった通り、管理職に求められるロールが年々増えてきて単純に業績を出す以上の負荷がかかっているのは事実かなと思います。 某ギャングの幹部になった人が「『任務は遂行する』『部下も守る』両方やらなくっちゃあならないってのが辛いところだな」なんて言葉を発していましたが、現代だとそれ以上にやることが増えていて本人もビックリすることでしょう。 その一方で「じゃあマネージャーリタイアしたいの?」と言われるとそんなことはありません。 筆者は社会人2年目で心から尊敬するエンジニアリングマネージャーに出会えたことでエンジニアリングの考え方が変わりました。 おそらくその人に出会わなければ筆者は技術にそこまで注力して向き合うこともなかったし、このような表舞台に出ることもなかったでしょう。 いいマネージャーとの出会いはその人の成長速度だけでなくエンジニアリングの考え方の根底そのものを変えるほどの強い影響力を持っていると筆者は信じています。 その人はもうドコモビジネスから去ってしまいましたが、筆者の目指す姿の1つがその人から学んだ「エンジニアリング組織と文化の土台を作れるエンジニアリングマネージャー」像として残っています。 筆者はまだまだその領域には至っていませんが、懇親の場で「マネージャーが変わってもう少しここで学んでいきたいと思いました!」という言葉をいただくと「あぁ、もしかしたらチームに何かいい影響を与えられたのかもしれないな」と小さな成長を感じることもできました。 まだまだ道は遠く管理職の仕事はムズカシイことばかりですが、かつて憧れていた存在にまた一歩近づくことを目指して、少しでもチームに良い影響が与えられるよう精進していきたいと思います。 あまりまとまりのない話でしたが、同じような境遇になっている人、キャリアを目指している人の何かしらの参考になれば嬉しいです。 明日は「AIロボット部で出たハッカソンの話と副賞で頂いた競馬の冠レースの話」です! お楽しみに!! エンジニアリングマネージャ/プロダクトマネージャのための知識体系と読書ガイド, https://qiita.com/hirokidaichi/items/95678bb1cef32629c317 ↩
この記事は、 NTT docomo Business Advent Calendar 2025 13日目の記事です。 OpenStackのAPIをModel Context Protocol(MCP)を使って操作できるようにし、Large Language Model(LLM)経由でクラウドのリソースを操作できるようにしました。 しかし、MCPサーバーを介してAPIをそのまま叩けるようにするだけではLLMにとって扱いづらく、コンテキストが無駄に大きくなる・ツールをうまく実行できない、といった問題がありました。 こういった問題に対して、コンテキストを取捨選択して削減しつつ、必要な機能に絞ったMCPサーバーを実装することで対応したので、その内容を紹介します。 はじめに OpenStack MCPサーバーを作る 全体構成 MCPサーバーの実装 MCPサーバーを使ってみるが…… LLMに易しいMCPサーバーを作る 動作確認 まとめ はじめに みなさんこんにちは。イノベーションセンターの會澤です。 イノベーションセンターでは、今年度からAIOps基盤プロジェクトという新しいプロジェクトを立ち上げ、オンプレミスのシステムやクラウド環境をターゲットに、AI技術を利用した運用自動化に向けた取り組みを始めています。 その一環として、NTTドコモビジネスで運用しているOpenStackで構築されたクラウド環境を対象にLLMを利用した運用自動化を進めています。 運用自動化を進めるにあたって、まずはLLMからクラウド環境のリソースを操作できるようにする必要があります。 LLMから直接OpenStackのAPIを操作できないので、MCPを使ってAPI呼び出しをラッピングすることにしました。 OpenStackを対象としたMCPサーバーの実装はすでに幾つか公開されているものが存在するのですが、現状ではデファクトスタンダードと呼べるものは存在しないようです。 そこで、我々の開発チームでは自分たちで新たにMCPサーバーを実装することにしました。 OpenStack MCPサーバーを作る 全体構成 MCPとは、Anthropic社が発表したLLMアプリに外部のシステムを接続するためのプロトコルです。 このMCPを利用してサーバーを構築し、OpenStack APIを操作する機能を ツール としてLLMに公開することで、AIエージェントからOpenStackのリソースを操作できるようにします。 全体の構成はこんな感じです。 今回はClaude Codeを使ってMCPサーバーの動作を確認してみます。 まず、ユーザーはClaude Codeを使って、自然言語でクラウド環境への操作を指示します。 Claude CodeにはMCP経由でツールに接続する機能があるため、サーバーを登録しておけばユーザーからの入力を受けて適切なツールを利用してくれます。 これにより、ツールからOpenStackのAPIを操作してクラウド環境のリソースを操作できるようになります。 MCPサーバーの実装 MCPサーバーを実装するにあたり、今回は FastMCP というPython向けのMCPアプリケーション構築用フレームワークを利用しています。 2025年にリリースされたばかりのFastMCP 2.0では、複数のMCPサーバーの統合や、MCPサーバーのプロキシなど高度な機能が備わっています。 その中には、OpenAPI Specificationを入力するだけで、適切なMCPツールに変換してサーバーを構築できる機能もあります。 今回の実装にあたって、最初にこのOpenAPI連携機能をサクッと試してみることにしました。 OpenStackのMCPサーバーを実装するにあたって、まずはOpenStackのAPIスキーマを作成します。 詳細な手順は割愛しますが、 OpenStack CodeGenerator を使うことでAPIスキーマを生成できます。 リポジトリのREADMEに記載されている手順に従って、 OpenStack Compute (Nova) のリポジトリからスキーマを生成できます。 NovaというのはOpenStackを構成するコンポーネントの1つであり、仮想マシンの作成・管理機能を提供しています。 ここでは、FastMCPを使ってNovaのAPIスキーマからMCPサーバーを構築し、仮想マシンを操作してみます。 FastMCPにはOpenAPI Specificationから自動でAPIを識別し、適切なエンドポイントで自動的にサーバーを構築してくれる機能があります。 それを使うことで、以下のようなPythonコードでMCPサーバーを実装できます。 import os import httpx import yaml from fastmcp import FastMCP client = httpx.AsyncClient(base_url= "https://nova.example.com" ) print (os.getcwd()) with open ( "/path/to/openapi_specs/compute/v2.yaml" , 'r' , encoding= 'utf-8' ) as f: openapi_spec = yaml.safe_load(f) mcp = FastMCP.from_openapi( openapi_spec=openapi_spec, client=client, name= "nova_mcp" , ) if __name__ == "__main__" : mcp.run() MCPサーバーを使ってみるが…… Claude Codeを使って動作確認してみます。 FastMCPには他のAIアシスタントツールと連携するためのCLIが付属しており、以下のコマンドでMCPサーバーの起動と接続をしてくれるようになります。 fastmcp install claude-code server.py Claude Codeを起動して、OpenStack環境の特定のノードにデプロイしているインスタンスを確認してみます。 最終的に、2つのインスタンスが存在するとわかりました。 しかし、MCPクエリを実行する際のパラメータの指定に問題があったようで、インスタンスの詳細情報がうまく取得できなかったようです。 また、ここで気になってくるのが、MCPを利用することによるClaude Codeのパフォーマンスの変化です。 というのも、今回のやり方のように大量のエンドポイントと複雑なパラメータを持つAPIをMCPに変換することは、LLMに入力されるコンテキストが膨大になるため、パフォーマンスの観点から 推奨されないという話があります 。 ブートストラッピングやプロトタイプの実装にはこれでも十分かもしれませんが、業務レベルで利用できるアプリケーションを作ろうと思うと、もうちょっと実装を考える必要がありそうです。 実際、社内にデプロイしたローカルLLMでこのMCPサーバーを扱おうとすると、入力されるコンテキストが多すぎるためかツールを実行できないという問題もありました。 LLMに易しいMCPサーバーを作る OpenAPI SpecificationをそのままMCPサーバーに変換するやり方では、LLMに入力されるコンテキストが膨大になるという問題がありました。 そこで、AIエージェントを利用するユースケースを絞り、本当に必要な処理だけを実装したMCPサーバーを実装することにしました。 現在考えているユースケースは、OpenStackを使って構築されたクラウド環境の運用を、LLMを使って自動化するというものです。 クラウドのオペレーターの定常的な業務の一部をAIエージェントに代行させます。 今回は、オペレータの業務の一部であるライブマイグレーション(Live Migration)の実施にユースケースを絞って考えてみることにしました。 ライブマイグレーションとは、稼働中のインスタンスを停止させることなく別のコンピュートノードに移行させるというものです。 このライブマイグレーションに必要な機能を洗い出してみました。 特定のノードのインスタンス一覧を取得する 特定のインスタンスの情報を確認する 移行するインスタンスと移行先ノードを指定してライブマイグレーションを実行する これらの機能をそれぞれ、MCPサーバーのツールとして実装します。 前述の FastMCP.from_openapi() を使った実装とは異なり、API呼び出し時のパラメータやレスポンスの受け取り方はそれぞれ適切な方法で定義していきます。 import json from fastmcp import FastMCP from os_client import send_get, send_post # API呼び出しは自前のパッケージで定義 mcp = FastMCP( "nova_mcp" ) @ mcp.tool () async def get_servers_detail (host: str ) -> str : """ コンピュートノードのホスト名から該当コンピュートノード上に存在する全てのインスタンス一覧を取得する Parameters: host: ホスト名 returns: str: インスタンス一覧 """ resource_path = "servers/detail" params = { "all_tenants" : "true" , "host" : host } data = await send_get(resource_path, params=params) servers = data.get( "servers" , []) return json.dumps(servers, ensure_ascii= False , indent= 2 ) @ mcp.tool () async def get_servers_serverId (server_id: str ) -> str : """ インスタンスの情報を取得する Parameters: server_id: インスタンスの ID returns: str: インスタンス情報 """ resource_path = f "servers/{server_id}" data = await send_get(resource_path) server = data.get( "server" , []) return json.dumps(server, ensure_ascii= False , indent= 2 ) @ mcp.tool () async def post_server_live_migrate ( server_id: str , target_host: str ) -> str : """ 指定されたサーバを Live Migration で移行する Parameters: server_id (str): 移行対象のサーバID target_host (str): 移行先ホスト名 Returns: str: ステータスコードとレスポンス情報 """ resource_path = f "servers/{server_id}/action" json_body = { "os-migrateLive" : { "host" : target_host, "block_migration" : "auto" } } extra_headers = { "OpenStack-API-Version" : "compute 2.88" , } data = await send_post(resource_path, json_body=json_body, extra_headers=extra_headers) return json.dumps({ "status" : data.get( "status" , 202 if data == {} else "unknown" ), "response_data" : data }, ensure_ascii= False , indent= 2 ) if __name__ == "__main__" : mcp.run() ここで重要なのは、LLMに入力される情報をできるだけ絞り、余計なことを考えさせないようにすることです。 今回の例では以下のような工夫をしています。 ユースケースを限定して必要な機能だけをMCPサーバーに実装することで、LLMが扱うツールの数を減らし、コンテキストのサイズを小さくする ツールの簡潔な説明をdocstringで記述し、LLMが目的のツールを選びやすくする API呼び出し時のパラメータにあらかじめ適切な値を設定し、LLMから指定するパラメータはなるべく少なくする レスポンスの情報も必要な値だけを抽出して構造化し、整理してから返す 動作確認 続いてエージェントを起動して動作を確認してみます。 予めツールの実装を詳細に定義していたおかげで、1発で欲しい情報を得られました。 LLMで指定するパラメータが必要最小限になった分、スムーズに必要な情報に辿り着けています。 続けて、ライブマイグレーションの処理も実行してみます。 こちらも問題なく処理を実行できました。 ライブマイグレーションの実行に加えて、移行後のインスタンスの情報も正しく取得できています。 また、予め必要な処理の内容をコードで定義していることもあって処理の実行に安心感があり、仮に実行が失敗した場合でも手元のコードを分析して容易にデバッグできます。 まとめ OpenAPI Specificationをそのまま全てMCPサーバーに変換するのではなく、必要な機能に絞って個別に実装することでLLMに易しいMCPサーバーを作りました。 工夫したのは主に以下のポイントです。 APIをそのままMCPサーバーに変換するのではなく、ユースケースを考えて必要なツールだけを実装する LLMに与えるコンテキストを小さくする ツールの説明を過不足なく記述する API呼び出し時のパラメータを適切に設定し、LLMが指定するパラメータを最小限にする レスポンスの情報も必要な値だけを抽出して整理してから返す 本格的な運用自動化ツールを開発するにあたっては、より大規模なMCPサーバーを作ってツールの数も大きく増えることになりますが、そういった場合でも適切な粒度でツールを実装しLLMの負荷を下げることが大切になるでしょう。 場合によっては、対象とするドメインに応じてエージェントとMCPサーバーを分割し、マルチエージェントワークフローを組むことで個々のエージェントが扱う関心毎を減らすことも有効でしょう。 本日はここまでです。明日もお楽しみに!
SBOM(Software Bill of Materials)とは、ソフトウェアに含まれるコンポーネントの一覧表であり、近年の法統制によりその管理が求められています。本記事では、SBOM管理の必要性と現状の認知度についてお話しします。また、SSVCによる脆弱性評価とAIを活用した、効率的なSBOM管理のベストプラクティスについて解説いたします。 はじめに SBOM(Software Bill of Materials)とは SBOMの認知度 SBOM法統制とガイドライン SBOM管理の法統制が進む背景 OSS(オープンソースソフトウェア)の急速な普及 発見される脆弱性の爆発的な増加 現状考えうるベストプラクティス まとめ 脚注 参考文献 はじめに この記事は、 NTT docomo Business Advent Calendar 2025 11日目の記事です。 こんにちは!イノベーションセンターMetemcyberプロジェクトの千坂知也と申します。 Metemcyberプロジェクトは10/9(木)~10/10(金)に開催されたdocomoBusinessForum'25 [1] にて、開発中のSBOM(Software Bill of Materials)管理ソリューション「Threatconnectome」 *1 を展示させていただきました。多くのお客さまにご来場いただき、SBOM管理が必要のある現場での課題感などを議論させていただきました。 そこで感じたのは法統制などによりSBOM管理が求められていることは、ある程度周知されている一方で、その管理方法やなぜ必要とされているのかの認知が広がっていないということでした。 以上のことから本記事では、SBOM管理の必要性と現場での課題感、個人的に思う現状のベストプラクティスについて書いていこうかと思います。 以降では、次の3つについて述べていきたいと思います。 SBOM(Software Bill of Materials)とは SBOM管理の法統制が進む背景 現状考えうるベストプラクティス SBOM(Software Bill of Materials)とは SBOMとはSoftware Bill of Materialsの略であり、日本語では「ソフトウェアの部品表」という意味になります。製造業の方にはソフトウェアのBOMと言えば分かりやすいかもしれません。今日、多くのシステムに組み込まれているソフトウェアはさらに小さな複数のソフトウェアを組み合わせて構成されています。これらはコンポーネントと呼ばれ、パッケージ、ライブラリなどが該当します。SBOMはこれらソフトウェアのコンポーネントの一覧表のことを指します。各コンポーネントの名称、バージョン情報、ライセンス情報に加え、コンポーネント間の依存関係なども含まれます。 我々Metemcyberプロジェクトでは、よくこのSBOMを「食品の成分表示」と同じというふうに説明いたします。皆さんがスーパー・コンビニなどで買う食品の裏には必ずどのような原材料・成分が含まれているかを表す成分表示が書かれています。この成分表示をみて、自身にとってアレルギーのものが含まれていればその食品は買わない、などの判断ができるわけです。 ソフトウェアも中に含まれている成分(コンポーネント)表示から、危険なもの(脆弱性が含まれているパッケージなど)が含まれているのではないかを検知できるというわけです。 SBOMの認知度 MetemcyberプロジェクトがdocomoBusinessForum’25にてThreatconnectomeを展示させていただき、多くのお客さまと議論を交わしたなかで、次のようなお声を多く耳にしました。 「(SBOMという)言葉自体は聞きかじったことある」 「最近SBOMというワードをよく聞きます」 「法統制でSBOM対応が迫られているのは知っている」 「SBOMで管理しろっていわれても具体的にどうすればよいかわからない」 「結局なんで必要なのかが分からない」 言葉自体の認知度はやや上がりつつあるものの、具体的にSBOMが何なのか、SBOM管理と言われても何をしたら良いのか分からない、といった程度の認知度であることをひしひしと感じました。ベリサーブ社が国内製造業の設計開発部門および品質管理部門の担当者を対象に実施したSBOMの導入状況アンケート [2] では、「導入予定はない」は79%であり、「導入検討中」は14%、「導入済み」は7%と依然として導入へは課題があります。 SBOM法統制とガイドライン SBOM管理は米国での2021年の大統領令 [3] を皮切りに世界的に法規制が進んでいます。EUでは2024年のサイバーレジリエンス法(Cyber Resilience Act:CRA) *2 [4] にてSBOM対応が必須要件となり、日本でも医療機器のサイバーセキュリティ手引書 [5] でSBOMに関する言及が多くあります。 ガイドライン面においても、経済産業省が2023年7月にSBOM導入のメリットと活用プラクティス [6] を公開し、内閣サイバーセキュリティセンター [7] も2024年7月の改訂で調達基準へのSBOM導入の可能性に言及しています。デジタル庁も2024年1月に政府情報システムでのセキュリティバイデザインの手段としてSBOM [8] を推奨しました。 さて、そもそもなぜこのような法統制が進んできているのでしょうか? SBOM管理の法統制が進む背景 SBOM管理が必要になった背景には、以下の2点があると考えています。 OSS(オープンソースソフトウェア)の急速な普及 発見される脆弱性の爆発的な増加(OSSへのサプライチェーン攻撃の増加) OSS(オープンソースソフトウェア)の急速な普及 近年の商用ソフトウェアは、コードベースの平均で70%以上、システムによっては90%近くがOSS由来のコンポーネントで構成されている [9] といわれています。OSSはソースコードが公開され、改変・再配布が可能であるため、既存の機能を活用することで開発コストの削減や生産性向上に大きく寄与します。 しかし、OSSもソフトウェアである以上、脆弱性の存在する可能性があります。実際、2021年12月にはApache Log4jというOSSにおいて、Log4Shellと呼ばれる非常に深刻な脆弱性 [10] が発見され、Amazon AWSやiCloud、Steamなど世界規模のサービスにまで影響が及びました。こうした重大な脆弱性に迅速に対応するためには、まず自社システムがどのOSSを利用しているかを正確に把握することが不可欠です。把握していなければ、適切な修正や封じ込めは困難になります。 また、現代のソフトウェアの多くは、表面からは見えないOSSやライブラリで構成されており、開発者でも「何が含まれているか」を完全に把握するのは難しい状況です。そのため、SBOMを活用することで、こうした隠れた依存関係を可視化し脆弱性対応を効率的に行うことが可能になると考えられています。 発見される脆弱性の爆発的な増加 情報システムの技術的な発展に伴い、ソフトウェアは社会システムの基盤そのものを構成するようになりました。一方で、技術の進歩と単純なソフトウェアの数の増加により、そこに含まれる脆弱性も急速に増加しています。CVE [11] の登録件数を見ても、ここ10年で著しい増加が続いており、2025年においては、すでに昨年を上回る件数が登録されています。前述した通り、このことの背景にはOSSの普及によって膨大な量のソフトウェアが複雑な依存関係を持つようになったことが関連しています。そのため、脆弱性に迅速かつ確実に対応するために、SBOMが不可欠になってきているということです。 これらの要因からSBOM管理は法整備されつつあるというわけです。 しかしながら、SBOMによってソフトウェアの中身の見える化を図ったとしても、結局どのように脆弱性対応に当たればよいかが現時点で明確化されていないという実情があります。膨大すぎる成分表示を渡されても、その中身が危険かどうかを判断するためのプラクティスが確立されていないのです。 現状考えうるベストプラクティス 1つは対応する脆弱性のトリアージを行うということが挙げられます。ソフトウェアにもよりますが、SBOMに含まれるコンポーネントの数は、一般的には数千~数万にも及びます。それらの脆弱性全てに対応しようとすると、とても現実的な時間では不可能です。一方でソフトウェアに含まれている脆弱性は対応を無視しても全く問題のないものが大多数です。組織や人命にとって重大な被害を及ぼしかねない脆弱性のみをピックアップして迅速に対応していくことが重要になってきます。 もう1つは、常に最新の脆弱性と同期させ、その脆弱性による組織内への被害の影響がどの程度の範囲に及ぶのかを把握しておく必要があるということです。前述した通り、新たな脆弱性は日々見つかっており、それが組織固有のシステムに影響を及ぼす可能性があります。どの組織のシステムにも甚大な被害を及ぼしかねない危険性が含まれているかもしれないのです。 これら2つのプラクティスを実践するためにSSVCという脆弱性の評価の指標を利用することと、AIによる自動化が有効であると我々Metemcyberプロジェクトは結論付けています。従来、脆弱性評価に多く用いられてきたCVSS(Common Vulnerability Scoring System) [12] という指標は脆弱性そのものの性質のみを見て評価をしているため、同じCVSSスコアでも、組織によって「対応の優先度」が変わり、対応方針までは決められません。SSVC(Stakeholder-Specific Vulnerability Categorization) [13] は「誰(どのステークホルダー)」が意思決定をするかを前提に設計された脆弱性評価の指標です。この評価指標は人命への影響度、悪用された場合の組織への影響度、利用システムのインターネットへの公開状況などをパラメータとして最終的な脆弱性の危険度を評価します。これらのパラメータを用いることで出力された脆弱性危険度は その組織 の このシステム を対象に この程度の範囲 で影響が出るという、具体的な評価を下します。さらにAIによって事前に該当ソフトウェアが組み込まれているシステムの組織的な位置づけ(利用目的など)から上記パラメータを推定できれば、人力でのSBOM管理の手間はより低減されると考えております。このような手段を用いて、脆弱性のトリアージを行うというのがベストプラクティスなのではないでしょうか。 上記の理由により、私たちMetemcyberプロジェクトではSSVCによる脆弱性評価を用いた脆弱性のトリアージがSBOM管理と相性の良いものであるというふうに考えており、さらにAIによる自動化を図ることでこれから求められつつあるSBOM管理のベストプラクティスを提示しようとしています。 まとめ 本記事では現場におけるSBOMの認知度と課題感、および現状考えうるベストプラクティスについて書かせていただきました。SBOMということ自体は聞きかじったことのあるものの、その具体的な内容や管理・運用の仕方がまだ分からないという方が多いというのが実情です。前述したとおり、我々Metemcyberプロジェクトでは、SSVCと呼ばれる脆弱性評価の指標をもとに脆弱性のトリアージをしつつSBOMを管理できるソリューション「Threatconnectome」を開発しております。ご興味がある方はぜひThreatconnectomeのGitHubページなどもご覧ください。 以上でAdventcalendar11日目の記事は終了です! 明日もお楽しみに! 脚注 nttcom/threatconnectome ↩ 現時点のCRAの本文中で「SBOM」という略語は直接的に使用されていないものの、「Software Bill of Materials」という言葉を用いて、明確にSBOMの作成・管理・提供に繋がる要件が規定されている。 ↩ 参考文献 NTT docomo Business Forum'25 国内製造業の1,000名を対象としたSBOMに関する調査を実施 Executive Order 14028 - Improving the Nation's Cybersecurity Cyber Resilience Act 医療機器のサイバーセキュリティ導入に関する手引書 ソフトウェア管理に向けたSBOM(Software Bill of Materials)の導入に関する手引 内閣サイバーセキュリティセンター デジタル社会推進実践ガイドブック DS-200 Synopsys Study Shows that Ninety-One Percent of Commercial Applications Contain Outdated or Abandoned Open Source Components Apache Log4jの脆弱性「Log4Shell」とは CVE - Common Vulnerabilities and Exposures 共通脆弱性評価システムCVSS概説 CVSSを逆から読むと?脆弱性対応の意思決定に使えるSSVCについて
この記事は、 NTT docomo Business Advent Calendar 2025 10日目の記事です。 Microsoft の IaC 言語である Bicep (+ Azure CLI/Databricks CLI) を使って、Azure Databricks ワークスペースをデプロイし、そのバックエンド通信や Azure データサービスへの通信を閉域化する方法を紹介します。 また、その環境を使ったデータ収集の一例として、Azure Event Hubs を使ったプライベートなデータストリーミングを試します。 はじめに なぜこの構成を記事にするのか 今回の構成と前提 前提 Bicep +α による構築 ネットワーク基盤 データサービスとその Private Link Azure Databricks ワークスペース データサービスに対する RBAC Bicep でデプロイ ワークスペースストレージの Private Link サーバレスプレーンに対する Private Link Unity Catalog 外部ロケーション プライベートなデータストリーミングの実践 まとめ はじめに こんにちは、C&A部の吉仲です。 初期配属からメール系システムや文書要約 API の開発・運用業務を担当しており、現在は主にシステムログ分析のためのデータ基盤の企画~開発業務に取り組んでいます。 昨年のアドベントカレンダーでの投稿記事 では、Azure Databricks を使ったログ分析を試しました。 今年はよりインフラに近い部分を扱います。 具体的には、Azure Databricks を中心としたプライベートなデータ基盤・データストリーミングを、Microsoft 純正の IaC 言語である Bicep を使って構築する方法を紹介します。 そして、 Azure Event Hubs からデータを取り込み、 Azure Data Lake Storage Gen2 (ADLS2) へ保存するまでの一連のフローを、パブリックネットワークを経由しないセキュアな経路で実現する実装例をコードと共に解説します。 なぜこの構成を記事にするのか エンタープライズ環境でのデータ基盤において、「セキュリティ」は避けて通れない要件です。 特に Azure Databricks を採用する場合、VNet へのデプロイ ( VNet 統合 ) に加えて、ストレージやイベントソースへのアクセスもパブリックネットワークを経由させずに閉域化したいという要望は一般的だと思います。 しかし、実際にこれを構築しようとすると、次のような壁に当たりませんか? (少なくとも私は苦戦しました) 公式ドキュメントはリファレンスアーキテクチャを提示しているが、具体的な設定値が分散していて全体像を把握するのが難しい ポータルでのポチポチ作業は解説されているが、IaC (特に Microsoft 公式言語である Bicep) での実装例が少ない コントロールプレーン/データプレーン、サーバレス/クラスターごとにネットワーク要件が複雑で、「疎通できない」トラブルが起きがち そこで本記事では、私が実際にプライベートなデータ基盤・データストリーミングを構築する中で苦戦した部分や得られた知見を踏まえて、具体的な構築方法をコードと共に解説したいと思います。 今回の構成と前提 Azure Databricks のバックエンド通信や Azure Databricks からデータサービスへの通信の閉域化については、以下の公式ブログなどでリファレンスアーキテクチャが紹介されています。 www.databricks.com 今回はこの構成に倣い、以下のような環境を構築します。 ハブ&スポーク構成 、インターネット向き通信の Firewall 強制トンネリング Databricks の VNet 統合と Secure Cluster Connectivity 設定 (パブリック IP 無効化) コントロールプレーン (バックエンド) との Private Link サーバレスプレーン/データプレーンそれぞれに対する Azure データサービスの Private Link なお、ハブ VNet (リファレンスアーキテクチャの図で言う " Customer transit VNet ") 上に作る Gateway については、対向拠点依存の部分が多いため本記事のスコープ外とします。 実際のユースケースでは、VPN Gateway や ExpressRoute Gateway をハブ VNet に構築して、オンプレミス環境等とのプライベート接続を実現します。 (【参考】 Azure Databricks ワークスペースをオンプレミス ネットワークに接続する ) また、上記の構成では Databricks の Web UI へのアクセスまでは閉域化できません。 Web UI まで閉域化する「フロントエンドの Private Link」を実現するには、リファレンスアーキテクチャに記載の通り、より複雑な構成になります。 本記事では、構成を簡単にするため フロントエンドの閉域化を対象外 とし、バックエンドの閉域化だけにフォーカスします。 ※本構成は Azure Firewall や 多数の Private Link を使用するため、検証環境であっても一定のコスト (時間課金) が発生します。 検証後は速やかにリソースを削除することを推奨します。 前提 構築の要件は以下の通りです。 Azure で「グローバル管理者」とサブスクリプションの「所有者」権限を持つアカウント (※「グローバル管理者」は Databricks アカウントコンソールへのログインに必要) Azure CLI (>=2.81.0) および Databricks CLI (>=0.259.0) の実行環境 以降は、基本的には以下の公式ドキュメントに倣った内容・設定で構築を進めます。 learn.microsoft.com learn.microsoft.com Bicep +α による構築 ここからはハンズオン形式で Bicep と各種 CLI ツールを使って必要なリソースを順に構築していきます。 ネットワーク基盤 ハブ&スポーク構成の VNet と、各 VNet 内のリソースを定義していきます。 VNet はハブ/スポーク用にそれぞれ作成するためモジュール化します。 サブネットについては、VNet 用モジュールの subnets パラメータでまとめて定義する形にしています。 modules/vnet.bicep param name string param location string param tags object = {} param addressPrefixes array @description('''e.g. [{name:'default',properties:{addressPrefix:'10.0.0.0/24',networkSecurityGroup:null}}]''') param subnets array = [] resource vnet 'Microsoft.Network/virtualNetworks@2024-05-01' = { name: name location: location tags: tags properties: { addressSpace: { addressPrefixes: addressPrefixes } encryption: { enabled: true enforcement: 'AllowUnencrypted' } } @batchSize(1) resource snet 'subnets' = [ for snet in subnets: if (!empty(subnets)) { name: snet.name properties: snet.properties } ] } output id string = vnet.id output name string = vnet.name Databricks を VNet 統合する場合、ネットワークセキュリティグループ (NSG) のルールが自動で設定されますが、これらも事前に Bicep で定義しておきます。 今回は、パブリック IP の無効化 + コントロールプレーンとの Private Link 構成のため、以下のような定義になります (= " No Azure Databricks Rules " 設定)。 なお、この NSG ルールの変更や削除は非推奨です。 modules/nsgDbw.bicep param name string param location string param tags object = {} resource nsg 'Microsoft.Network/networkSecurityGroups@2024-05-01' = { name: name location: location tags: tags properties: { securityRules: [ { name: 'Microsoft.Databricks-workspaces_UseOnly_databricks-worker-to-worker-inbound' properties: { description: 'Required for worker nodes communication within a cluster.' protocol: '*' sourcePortRange: '*' destinationPortRange: '*' sourceAddressPrefix: 'VirtualNetwork' destinationAddressPrefix: 'VirtualNetwork' access: 'Allow' priority: 100 direction: 'Inbound' } } { name: 'Microsoft.Databricks-workspaces_UseOnly_databricks-worker-to-worker-outbound' properties: { description: 'Required for worker nodes communication within a cluster.' protocol: '*' sourcePortRange: '*' destinationPortRange: '*' sourceAddressPrefix: 'VirtualNetwork' destinationAddressPrefix: 'VirtualNetwork' access: 'Allow' priority: 100 direction: 'Outbound' } } { name: 'Microsoft.Databricks-workspaces_UseOnly_databricks-worker-to-sql' properties: { description: 'Required for workers communication with Azure SQL services.' protocol: 'tcp' sourcePortRange: '*' destinationPortRange: '3306' sourceAddressPrefix: 'VirtualNetwork' destinationAddressPrefix: 'Sql' access: 'Allow' priority: 101 direction: 'Outbound' } } { name: 'Microsoft.Databricks-workspaces_UseOnly_databricks-worker-to-storage' properties: { description: 'Required for workers communication with Azure Storage services.' protocol: 'tcp' sourcePortRange: '*' destinationPortRange: '443' sourceAddressPrefix: 'VirtualNetwork' destinationAddressPrefix: 'Storage' access: 'Allow' priority: 102 direction: 'Outbound' } } { name: 'Microsoft.Databricks-workspaces_UseOnly_databricks-worker-to-eventhub' properties: { description: 'Required for worker communication with Azure Eventhub services.' protocol: 'tcp' sourcePortRange: '*' destinationPortRange: '9093' sourceAddressPrefix: 'VirtualNetwork' destinationAddressPrefix: 'EventHub' access: 'Allow' priority: 103 direction: 'Outbound' } } ] } } output id string = nsg.id output name string = nsg.name その他、Firewall やそれに割り当てるパブリック IP 、スポーク VNet から Firewall へ強制トンネリングするためのユーザー定義ルート (UDR)、ハブとスポークの VNet ピアリングもモジュール化します。 modules/afw.bicep param name string param policyName string param location string param zones array = [] param tags object = {} param tier string = 'Basic' param vnetName string param afwPipId string param afwManagementPipId string resource afwp 'Microsoft.Network/firewallPolicies@2024-05-01' = { name: policyName location: location tags: tags properties: { sku: { tier: tier } threatIntelMode: 'Alert' } resource rcg 'ruleCollectionGroups' = { name: 'default' properties: { priority: 100 ruleCollections: [ { name: 'allow-rules' ruleCollectionType: 'FirewallPolicyFilterRuleCollection' action: { type: 'Allow' } priority: 1000 rules: [ { name: 'Allow-InternetOutBound' ruleType: 'NetworkRule' ipProtocols: ['Any'] sourceAddresses: ['10.0.0.0/8'] destinationAddresses: ['*'] // 検証のため全許可. 本来は厳密に許可する通信先だけを列挙すべき. destinationPorts: ['*'] } ] } ] } } } resource afw 'Microsoft.Network/azureFirewalls@2024-05-01' = { name: name location: location zones: zones tags: tags properties: { sku: { name: 'AZFW_VNet' tier: tier } firewallPolicy: { id: afwp.id } ipConfigurations: [ { name: 'afwIPConf' properties: { subnet: { id: resourceId('Microsoft.Network/virtualNetworks/subnets', vnetName, 'AzureFirewallSubnet') } publicIPAddress: { id: afwPipId } } } ] managementIpConfiguration: { name: 'afwManagementIPConf' properties: { subnet: { id: resourceId('Microsoft.Network/virtualNetworks/subnets', vnetName, 'AzureFirewallManagementSubnet') } publicIPAddress: { id: afwManagementPipId } } } threatIntelMode: 'Alert' } } output id string = afw.id output name string = afw.name output ipAddress string = afw.properties.ipConfigurations[0].properties.privateIPAddress modules/pip.bicep param name string param location string param zones array = [] param tags object = {} param sku string = 'Standard' param tier string = 'Regional' resource pip 'Microsoft.Network/publicIPAddresses@2024-05-01' = { name: name location: location zones: zones tags: tags sku: { name: sku tier: tier } properties: { publicIPAddressVersion: 'IPv4' publicIPAllocationMethod: 'Static' } } output id string = pip.id output name string = pip.name output ipAddress string = pip.properties.ipAddress modules/rt.bicep param name string param udrName string param location string param tags object = {} param afwIpAddress string resource rt 'Microsoft.Network/routeTables@2024-05-01' = { name: name location: location tags: tags resource udr 'routes' = { name: udrName properties: { addressPrefix: '0.0.0.0/0' nextHopType: 'VirtualAppliance' nextHopIpAddress: afwIpAddress } } } output id string = rt.id output name string = rt.name modules/peer.bicep param vnetHubName string param vnetSpokeName string resource vnetHub 'Microsoft.Network/virtualNetworks@2024-05-01' existing = { name: vnetHubName resource peer 'virtualNetworkPeerings' = { name: 'peer-hub-to-spoke' properties: { allowVirtualNetworkAccess: true allowForwardedTraffic: true allowGatewayTransit: true useRemoteGateways: false remoteVirtualNetwork: { id: resourceId('Microsoft.Network/virtualNetworks', vnetSpokeName) } } } } resource vnetSpoke 'Microsoft.Network/virtualNetworks@2024-05-01' existing = { name: vnetSpokeName resource peer 'virtualNetworkPeerings' = { name: 'peer-spoke-to-hub' properties: { allowVirtualNetworkAccess: true allowForwardedTraffic: true allowGatewayTransit: false useRemoteGateways: false // VPN/ExpressRoute Gateway を作成する場合は true remoteVirtualNetwork: { id: resourceId('Microsoft.Network/virtualNetworks', vnetHubName) } } } } ここまでの各モジュールを組み合わせて、 main.bicep で各リソースを定義していきます。 まずはハブ VNet の定義からです。 targetScope = 'resourceGroup' param location string param tags object = {} param vnetHubName string param pipAfwName string param pipAfwManagementName string param afwName string param afwpName string module vnetHub './modules/vnet.bicep' = { params: { name: vnetHubName location: location tags: tags addressPrefixes: ['10.1.0.0/16'] subnets: [ { name: 'GatewaySubnet' // 名前固定. VPN/ExpressRoute Gateway 用 properties: { addressPrefix: '10.1.1.0/26' } } { name: 'AzureFirewallSubnet' // 名前固定 properties: { addressPrefix: '10.1.1.64/26' } } { name: 'AzureFirewallManagementSubnet' // 名前固定 properties: { addressPrefix: '10.1.1.128/26' } } ] } } module pipAfw './modules/pip.bicep' = { params: { name: pipAfwName location: location tags: tags } } module pipAfwManagement './modules/pip.bicep' = { params: { name: pipAfwManagementName location: location tags: tags } } module afw './modules/afw.bicep' = { params: { name: afwName policyName: afwpName location: location tags: tags vnetName: vnetHub.outputs.name afwPipId: pipAfw.outputs.id afwManagementPipId: pipAfwManagement.outputs.id } } 【説明】 ハブ VNet には Firewall と Gateway をデプロイするための3つのサブネットを作成 VNet 内に、インターネット向きアウトバウンド通信を担う (SNAT や監視) Firewall を作成 次にスポーク VNet、つまり Databricks データプレーンや各種プライベートエンドポイントを配置する VNet を定義します。 param vnetSpokeName string param nsgPepName string param nsgDbwName string param rtName string param udrName string module vnetSpoke './modules/vnet.bicep' = { params: { name: vnetSpokeName location: location tags: tags addressPrefixes: ['10.2.0.0/16'] subnets: [ { name: 'snet-host' properties: { addressPrefix: '10.2.1.0/24' networkSecurityGroup: { id: nsgDbw.outputs.id } routeTable: { id: rt.outputs.id } delegations: delegations // Databricks への委任 } } { name: 'snet-container' properties: { addressPrefix: '10.2.2.0/24' networkSecurityGroup: { id: nsgDbw.outputs.id } routeTable: { id: rt.outputs.id } delegations: delegations // Databricks への委任 } } { name: 'snet-pep' properties: { addressPrefix: '10.2.3.0/27' networkSecurityGroup: { id: nsgPep.id } routeTable: { id: rt.outputs.id } } } ] } } resource nsgPep 'Microsoft.Network/networkSecurityGroups@2024-05-01' = { name: nsgPepName location: location tags: tags } module nsgDbw './modules/nsgDbw.bicep' = { params: { name: nsgDbwName location: location tags: tags } } module rt './modules/rt.bicep' = { params: { name: rtName udrName: udrName location: location tags: tags afwIpAddress: afw.outputs.ipAddress // 強制トンネリング先の Firewall の IP アドレス } } var delegations = [ { name: 'delegation-dbw' properties: { serviceName: 'Microsoft.Databricks/workspaces' } } ] 【説明】 スポーク VNet には Databricks クラスターのホスト/コンテナ用、各種プライベートエンドポイント用の3つのサブネットを作成 クラスター (ホスト/コンテナ) 用サブネットには、" No Azure Databricks Rules " 設定の NSG を割り当て、 Microsoft.Databricks/workspaces への委任を設定 各サブネットでは、UDR によりハブ VNet 上の Firewall へ強制トンネリング 最後にハブ VNet とスポーク VNet 間のピアリングを定義します。 module peer './modules/peer.bicep' = { params: { vnetHubName: vnetHub.outputs.name vnetSpokeName: vnetSpoke.outputs.name } } データサービスとその Private Link Databricks から接続する Azure データサービスの Private Link を定義していきます。 Databricks の外部ロケーションとして使う ADLS2 の定義と、データストリーミング用の Event Hubs の定義をそれぞれモジュール化します。 (なお、以降も同様ですが、SKU やスペックは必要最低限のものに固定しています) modules/dls.bicep param name string param containerNames array = [] param location string param tags object = {} param sku string = 'Standard_LRS' param accessTier string = 'Hot' param resourceAccessRules array = [] resource dls 'Microsoft.Storage/storageAccounts@2025-01-01' = { name: name location: location tags: tags sku: { name: sku } kind: 'StorageV2' properties: { accessTier: accessTier allowBlobPublicAccess: false allowedCopyScope: 'PrivateLink' encryption: { keySource: 'Microsoft.Storage' services: { blob: { enabled: true } } } isHnsEnabled: true // 必須 (Data Lake Storage Gen2化) largeFileSharesState: 'Disabled' minimumTlsVersion: 'TLS1_2' networkAcls: { defaultAction: 'Deny' // ファイアウォールを有効化 (デフォルトで拒否) resourceAccessRules: resourceAccessRules bypass: 'Logging, Metrics' } publicNetworkAccess: 'Enabled' // Databricks アクセスコネクタからのアクセスを許可するため ('Disabled'だと全遮断) supportsHttpsTrafficOnly: true } resource blob 'blobServices' = { name: 'default' properties: {} resource container 'containers' = [ for containerName in containerNames: { name: containerName properties: { publicAccess: 'None' } } ] } } output id string = dls.id output name string = dls.name modules/evh.bicep param name string param instanceName string param location string param tags object = {} param sku string = 'Standard' param capacity int = 1 param isAutoInflateEnabled bool = false param maximumThroughputUnits int = 0 param partitionCount int = 1 resource evhns 'Microsoft.EventHub/namespaces@2024-01-01' = { name: name location: location tags: tags sku: { name: sku tier: sku capacity: capacity } properties: { isAutoInflateEnabled: sku == 'Standard' ? isAutoInflateEnabled : null maximumThroughputUnits: sku == 'Standard' ? maximumThroughputUnits : null publicNetworkAccess: 'Disabled' minimumTlsVersion: '1.2' kafkaEnabled: true } resource evh 'eventhubs' = { name: instanceName properties: { partitionCount: partitionCount retentionDescription: { cleanupPolicy: 'Delete' } messageRetentionInDays: 1 } // データストリーミングの収集元で使うため SAS キーを事前に用意 resource sas 'authorizationRules' = { name: '${instanceName}Send' properties: { rights: ['Send'] } } } } output id string = evhns.id output name string = evhns.name 最後に、Private Link を作成するためのプライベート DNS ゾーンとその VNet 接続、プライベートエンドポイントを定義するモジュールを作ります。 modules/pep.bicep param name string param zoneName string param location string param tags object = {} param vnetName string param snetName string param privateLinkServiceId string param groupIds array resource zone 'Microsoft.Network/privateDnsZones@2024-06-01' = { name: zoneName location: 'global' tags: tags resource vnetLink 'virtualNetworkLinks' = { name: 'pl-${vnetName}' location: 'global' properties: { virtualNetwork: { id: resourceId('Microsoft.Network/virtualNetworks', vnetName) } } } } resource pep 'Microsoft.Network/privateEndpoints@2024-05-01' = { name: name location: location tags: tags properties: { subnet: { id: resourceId('Microsoft.Network/virtualNetworks/subnets', vnetName, snetName) } privateLinkServiceConnections: [ { name: name properties: { privateLinkServiceId: privateLinkServiceId groupIds: groupIds } } ] } resource dnsZoneGroup 'privateDnsZoneGroups@2024-05-01' = { name: 'default' properties: { privateDnsZoneConfigs: [ { name: 'default' properties: { privateDnsZoneId: zone.id } } ] } } } output id string = pep.id output name string = pep.name ここまでの各モジュールを組み合わせて、 main.bicep に各リソースの定義を追記します。 なお、順番が前後してしまいますが、後述の Databricks アクセスコネクタをストレージアカウントのファイアウォールで許可しています。 param dlsName string param evhName string module dls './modules/dls.bicep' = { params: { name: dlsName containerNames: ['lake'] location: location tags: tags resourceAccessRules: [ { resourceId: dbw.outputs.acId // Databricks アクセスコネクタからのアクセスを許可 tenantId: tenant().tenantId } ] } } module pepDfs './modules/pep.bicep' = { params: { name: 'pep-${dls.outputs.name}-dfs' zoneName: 'privatelink.dfs.core.windows.net' location: location tags: tags vnetName: vnetSpoke.outputs.name snetName: 'snet-pep' privateLinkServiceId: dls.outputs.id groupIds: ['dfs'] } } module pepBlob './modules/pep.bicep' = { params: { name: 'pep-${dls.outputs.name}-blob' zoneName: 'privatelink.blob.core.windows.net' location: location tags: tags vnetName: vnetSpoke.outputs.name snetName: 'snet-pep' privateLinkServiceId: dls.outputs.id groupIds: ['blob'] } dependsOn: [ pepDfs ] } module evh './modules/evh.bicep' = { params: { name: evhName instanceName: 'topic1' location: location tags: tags } } module pepEvh './modules/pep.bicep' = { params: { name: 'pep-${evh.outputs.name}' zoneName: 'privatelink.servicebus.windows.net' location: location tags: tags vnetName: vnetSpoke.outputs.name snetName: 'snet-pep' privateLinkServiceId: evh.outputs.id groupIds: ['namespace'] } dependsOn: [ pepBlob ] } プライベートエンドポイントの定義では、ADLS2 は dfs / blob 、Event Hubs は namespace を識別子 ( groupIds ) に指定します。 なお、ADLS2 へのアクセスが Unity Catalog 経由のみの場合、 blob エンドポイントはおそらく不要だと思います。 Azure Databricks ワークスペース Databricks ワークスペースとコントロールプレーンへの Private Link を定義していきます。 Databricks ワークスペースと、そこから Azure データサービスへアクセスする際に使われる Databricks アクセスコネクタの定義をモジュール化します。 modules/dbw.bicep param name string param connectorName string param location string param tags object = {} param sku string = 'premium' param managedRgName string = 'mrg-${name}' param vnetName string param snetHostName string param snetContainerName string param storageAccountSkuName string = 'Standard_LRS' resource dbac 'Microsoft.Databricks/accessConnectors@2024-05-01' = { name: connectorName location: location tags: tags identity: { type: 'SystemAssigned' } properties: {} } resource dbw 'Microsoft.Databricks/workspaces@2024-05-01' = { name: name location: location tags: tags sku: { name: sku } properties: { managedResourceGroupId: subscriptionResourceId('Microsoft.Resources/resourceGroups', managedRgName) accessConnector: { id: dbac.id identityType: 'SystemAssigned' } defaultStorageFirewall: 'Enabled' publicNetworkAccess: 'Enabled' parameters: { customVirtualNetworkId: { value: resourceId('Microsoft.Network/virtualNetworks', vnetName) } customPublicSubnetName: { value: snetHostName } customPrivateSubnetName: { value: snetContainerName } enableNoPublicIp: { value: true } storageAccountSkuName: { value: storageAccountSkuName } } requiredNsgRules: 'NoAzureDatabricksRules' } } output id string = dbw.id output name string = dbw.name output acId string = dbac.id output acName string = dbac.name output acPrincipalId string = dbac.identity.principalId 上記のモジュールを使って main.bicep に定義を追記します。 param dbwName string param dbacName string module dbw './modules/dbw.bicep' = { params: { name: dbwName connectorName: dbacName location: location tags: tags vnetName: vnetSpoke.outputs.name snetHostName: 'snet-host' snetContainerName: 'snet-container' } } module pepDbw './modules/pep.bicep' = { params: { name: 'pep-${dbw.outputs.name}' zoneName: 'privatelink.azuredatabricks.net' location: location tags: tags vnetName: vnetSpoke.outputs.name snetName: 'snet-pep' privateLinkServiceId: dbw.outputs.id groupIds: ['databricks_ui_api'] } dependsOn: [ pepEvh ] } データサービスに対する RBAC Databricks から Azure データサービスへのアクセスは、前述のとおり Databricks アクセスコネクタで行います。 したがって、このアクセスコネクタに対して ADLS2 や Event Hubs の権限を付与する必要があります。 ADLS2 と Event Hubs の RBAC ロールを付与するモジュールを作ります。 modules/dlsRbac.bicep param name string param dlsName string param roleId string param principalId string resource dls 'Microsoft.Storage/storageAccounts@2025-01-01' existing = { name: dlsName } resource assign 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: name scope: dls properties: { principalId: principalId roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleId) } } output id string = assign.id output name string = assign.name modules/evhRbac.bicep param name string param evhName string param roleId string param principalId string resource evhns 'Microsoft.EventHub/namespaces@2024-01-01' existing = { name: evhName } resource assign 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: name scope: evhns properties: { principalId: principalId roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleId) } } output id string = assign.id output name string = assign.name これらを使って main.bicep にて以下の RBAC ロールを Databricks アクセスコネクタに付与します。 ADLS2 (ストレージアカウント): " Azure Storage Blob Contributor " (データの読み書き用) Event Hubs: " Azure Event Hubs Data Receiver " (イベントデータの読み取り用) module dlsRbac './modules/dlsRbac.bicep' = { params: { name: guid(dlsName, dbacName, 'Storage Blob Data Contributor') dlsName: dls.outputs.name roleId: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' // Storage Blob Data Contributor principalId: dbw.outputs.acPrincipalId } } module evhRbac './modules/evhRbac.bicep' = { params: { name: guid(evhName, dbacName, 'Azure Event Hubs Data Receiver') evhName: evh.outputs.name roleId: 'a638d3c7-ab3a-418d-83e6-5f17a39d4fde' // Azure Event Hubs Data Receiver principalId: dbw.outputs.acPrincipalId } } Bicep でデプロイ ここまでかなり長くなってしまいましたが、Bicep でのリソース定義は以上です。 ここからは実際にデプロイしていきます。 最初に Azure CLI でリソースグループを作成します。 az login # 「所有者」ロールのアカウントでログイン az group create --location japaneast --name rg-azuredatabricks-demo 次に Bicep パラメータファイルを用意してリソース名などのパラメータを指定します。 今回は SKU やスペックを固定しているので、ここではほぼリソース名の指定だけになっています。 Bicep パラメータの例 (environments/demo.bicepparam) using '../main.bicep' var project string = 'adbdemo' param location = 'japaneast' param vnetHubName = 'vnet-hub-${project}-${location}' param pipAfwName = 'pip-${project}-${location}-001' param pipAfwManagementName = 'pip-${project}-${location}-002' param afwName = 'afw-${project}-${location}' param afwpName = 'afwp-${project}-${location}' param vnetSpokeName = 'vnet-spoke-${project}-${location}' param nsgPepName = 'nsg-${project}-pep' param nsgDbwName = 'nsg-${project}-dbw' param rtName = 'rt-${project}' param udrName = 'udr-default-gateway' param dlsName = 'dls${project}001' // Globally unique param evhName = 'evhns-${project}-001' // Globally unique param dbwName = 'dbw-${project}-${location}' param dbacName = 'dbac-${project}' それでは main.bicep と上記のパラメータファイルを使ってリソースをデプロイします。 # デプロイ後の推定状態を確認 az deployment group what-if -g rg-azuredatabricks-demo --template-file main.bicep --parameters environments/demo.bicepparam # デプロイ実行 az deployment group create -c -g rg-azuredatabricks-demo --template-file main.bicep --parameters environments/demo.bicepparam デプロイ成功後、以下のようにリソースグループ内に各種リソースが表示されていると思います。 ワークスペースストレージの Private Link さて、Bicep でのデプロイは完了しましたが、ここからが肝になります。 Databricks ワークスペースの作成箇所では説明を省きましたが、今回は既定のマネージドストレージ (" ワークスペースストレージ ") でもファイアウォールを有効にし、パブリックアクセスを禁止する設定を入れています。 ワークスペースストレージとは、Databricks のシステムデータや DBFS ルートなどに使われるストレージです。 公式ドキュメントでも説明されていますが、ワークスペースストレージのファイアウォールを有効にしているときは、その Private Link も作成する必要があります。 learn.microsoft.com 【余談】私は当初、このファイアウォールを有効にしたことを忘れたまま構築を進めてしまいました。 そして、クラスターでパイプラインを作成しようとしたところで疎通不可となり、原因特定に随分と時間を費やしました。 ワークスペースストレージの Private Link の構築は Bicep だけでは完結しません。 というのも、デプロイ後に作成されるマネージドリソースグループ内を見てもらうと分かるように、ストレージ名が dbstorage<ランダム文字列> になっています。 これは動的に決まるリソース名であるため、前述までの Bicep コード内で参照することが難しいです。 そこで、 main.bicep とは別に postprocess.bicep を用意し、ワークスペースストレージの Private Link のみを個別に定義する形とします。 targetScope = 'resourceGroup' param location string param tags object = {} param vnetName string param storageId string var stName string = last(split(storageId, '/')) module pepStBlob './modules/pep.bicep' = { params: { name: 'pep-${stName}-blob' zoneName: 'privatelink.blob.core.windows.net' location: location tags: tags vnetName: vnetName snetName: 'snet-pep' privateLinkServiceId: storageId groupIds: ['blob'] } } module pepStDfs './modules/pep.bicep' = { params: { name: 'pep-${stName}-dfs' zoneName: 'privatelink.dfs.core.windows.net' location: location tags: tags vnetName: vnetName snetName: 'snet-pep' privateLinkServiceId: storageId groupIds: ['dfs'] } } この Bicep コードのデプロイ時に、すでにデプロイ済みのワークスペースストレージの ID をパラメータとして指定します。 # マネージドリソースグループ内の ADLS2 (ワークスペースストレージ) の ID を取得 # マネージドリソースグループ名は本記事のBicepコードでは "mrg-dbw-adbdemo-japaneast" az storage account list -g < マネージドリソースグループ名 > --query ' [].id ' -o tsv # 上記のコマンドで表示された ID をパラメータに指定してデプロイ実行 az deployment group create -c -g rg-azuredatabricks-demo \ --template-file bicep/postprocess.bicep \ --parameters location =japaneast \ --parameters vnetName =vnet-spoke-adbdemo-japaneast \ --parameters storageId = < ワークスペースストレージのID > 以上で、Databricks クラスターからワークスペースストレージへのプライベート通信が可能になりました。 サーバレスプレーンに対する Private Link ここまで作成してきた Private Link は、全てデータプレーン内にあるクラスターからの通信用の接続構成です。 サーバレスプレーンからの通信用には、以下の公式ドキュメントに記載されている設定: Network Connectivity Configuration (NCC) が必要です。 learn.microsoft.com 残念ながら現状はこの設定も Bicep で実施できません。 Azure CLI および Databricks CLI を使って、ADLS2 と Event Hubs の Private Link を作成します。 事前準備: # アカウントレベルのログイン (アカウント ID は下記 URL のコンソールで確認) databricks auth login --host https://accounts.azuredatabricks.net --account-id < アカウントID > # ワークスペースレベルのログイン (ワークスペース URL は Azure Portal で確認) databricks auth login --host https://adb- < ワークスペース識別子 > .azuredatabricks.net NCC 作成とワークスペースへの割り当て: # NCC の作成 databricks account network-connectivity create-network-connectivity-configuration \ --json ' {"name":"ncc-adbdemo-japaneast","region":"japaneast"} ' # 上記のコマンドで表示された NCC の ID "network_connectivity_config_id" を指定 databricks account workspaces update < ワークスペースID > --network-connectivity-config-id < NCCID > ADLS2 の Private Link 作成 ( dfs エンドポイントの例): # Private Link を作成する ADLS2 の ID を取得 az storage account list -g rg-azuredatabricks-demo --query ' [].id ' -o tsv # プライベートエンドポイントの作成 databricks account network-connectivity create-private-endpoint-rule < NCCID > \ --json ' {"resource_id":"<ストレージID>","group_id":"dfs"} ' # Azure 側で承認保留中のプライベートエンドポイントを確認 az network private-endpoint-connection list --id < ストレージID > \ | jq -r ' .[]|select(.properties.privateLinkServiceConnectionState.status =="Pending").id ' # 上記のコマンドで表示されたプライベートエンドポイントの ID を指定して、接続を承認 az network private-endpoint-connection approve --id < プライベートエンドポイントID > Event Hubs の Private Link 作成: # Private Link を作成する Event Hubs の ID を取得 az eventhubs namespace list -g rg-azuredatabricks-demo --query ' [].id ' -o tsv # プライベートエンドポイントの作成 databricks account network-connectivity create-private-endpoint-rule < NCCID > \ --json ' {"resource_id":"<EventHubsID>","group_id":"namespace"} ' # Azure 側で承認保留中のプライベートエンドポイントを確認 az network private-endpoint-connection list --id < EventHubsID > \ | jq -r ' .[]|select(.properties.privateLinkServiceConnectionState.status =="Pending").id ' # 上記のコマンドで表示されたプライベートエンドポイントの ID を指定して、接続を承認 az network private-endpoint-connection approve --id < プライベートエンドポイントID > 以上で、サーバレスプレーン向けの Azure データサービスの Private Link が作成されました。 Databricks のアカウントコンソールで、以下のように各エンドポイントの接続が ESTABLISHED になっていれば完了です。 なお、NCC を設定すると、ワークスペースストレージのファイアウォールにおいてサーバレスプレーンの VNet が自動的に許可されます。 そのため、今回はワークスペースストレージの Private Link は省略します。 Private Link でのアクセスとしたい場合は、上記と同じ手順で作成します。 Unity Catalog 外部ロケーション Databricks から ADLS2 へのプライベート接続が可能になったので、その ADLS2 で Unity Catalog の外部ロケーションを作成してみます。 以降は、Bicep ではなく Azure CLI/Databricks CLI を使った作成になります。 # アクセスコネクタの ID を確認 az databricks access-connector list -g rg-azuredatabricks-demo --query ' [].id ' -o tsv # 上記のアクセスコネクタの ID を指定して、資格情報を作成 databricks storage-credentials create \ --json ' {"name":"adbdemo_storage","azure_managed_identity":{"<Databricks アクセスコネクタID>"}} ' # 上記の資格情報を指定して、外部ロケーションを作成 databricks external-locations create adbdemo_storage \ abfss:// < コンテナ名 > @ < ストレージアカウント名 > .dfs.core.windows.net/ adbdemo_storage Databricks ワークスペースにログインし、[カタログエクスプローラー]>[外部ロケーション] から作成した外部ロケーションを開き、右上の [接続テスト] を実行します。 全て「成功」であれば完了です。 なお、Private Link に不備がある場合は外部ロケーションの作成自体が失敗し、Databricks アクセスコネクタの権限不足の場合は接続テストで失敗すると思います。 以上で、プライベート接続のためのインフラ構築・設定は完了です。お疲れ様でした! プライベートなデータストリーミングの実践 最後は、構築したプライベート接続環境を使ってデータストリーミングの実装例を紹介します。 構築した VNet とプライベート接続された環境にあるサーバをデータソースとして、 Fluent Bit から Event Hubs へデータを送信し、Event Hubs からの受信データを Databricks のパイプラインでストレージに書き込む、という構成です。 まずは、Unity Catalog のカタログとスキーマを作成します。 今回は、カタログ/スキーマ用のストレージは同じ ADLS2 コンテナ内でパスを分ける形で分離します (※実際のユースケースでは、 メダリオンアーキテクチャ の各レイヤーごとにコンテナもしくはストレージアカウントレベルで分離する方が良いと思います)。 また、あわせて Databricks パイプラインから Event Hubs へ接続するための資格情報も作成します。 # カタログの作成 databricks catalogs create adbdemo --storage-root abfss:// < コンテナ名 > @ < ストレージアカウント名 > .dfs.core.windows.net/catalog # スキーマの作成 databricks schemas create bronze adbdemo --storage-root abfss:// < コンテナ名 > @ < ストレージアカウント名 > .dfs.core.windows.net/bronze # Event Hubs 接続用にサービス資格情報を作成 databricks credentials create-credential --purpose SERVICE \ --json ' {"name":"adbdemo_service","azure_managed_identity":{"<Databricks アクセスコネクタID>"}} ' 次に、Event Hubs をソースとする Lakeflow (旧: Delta Live Tables) パイプラインを作成します。 pipelines/bronze_ingest_eventhubs_raw.py : from pyspark import pipelines as dp from pyspark.sql import SparkSession from pyspark.sql.functions import col, expr spark = SparkSession.builder.getOrCreate() # Event Hubs の Kafka モードでデータ受信するための設定 # ここでは SAS キーではなく Databricks アクセスコネクタで認証 KAFKA_OPTIONS = { "databricks.serviceCredential" : spark.conf.get( "streaming.dbw.serviceCredential" ), "kafka.bootstrap.servers" : spark.conf.get( "streaming.evh.namespace" ), "subscribe" : spark.conf.get( "streaming.evh.name" ), "kafka.request.timeout.ms" : spark.conf.get( "streaming.kafka.requestTimeout" ), "kafka.session.timeout.ms" : spark.conf.get( "streaming.kafka.sessionTimeout" ), "maxOffsetsPerTrigger" : spark.conf.get( "streaming.spark.maxOffsetsPerTrigger" ), "failOnDataLoss" : spark.conf.get( "streaming.spark.failOnDataLoss" ), "startingOffsets" : spark.conf.get( "streaming.spark.startingOffsets" ), } def parse (df): return ( df.withColumn( "records" , col( "value" ).cast( "string" )) .withColumn( "eventhub_timestamp" , expr( "timestamp" )) .withColumn( "ingested_timestamp" , col( "current_timestamp" )) .withColumn( "date" , expr( "to_date(ingested_timestamp)" )) .withColumn( "hash" , expr( "md5(records)" )) .withWatermark( "eventhub_timestamp" , "10 minutes" ) .dropDuplicatesWithinWatermark([ "hash" ]) .drop( "key" , "value" , "partition" , "offset" , "timestamp" , "timestampType" ) ) @ dp.table ( comment= "Raw Logs aggregated from FluentBit-EventHubs" , partition_cols=[ "date" ], spark_conf={ "pipelines.trigger.interval" : "5 seconds" }, table_properties={ "quality" : "bronze" , "pipelines.reset.allowed" : "false" }, ) def common_logs_raw (): # テーブル名 (topic=インスタンスを区別していないので "common" にした) return spark.readStream.format( "kafka" ).options(**KAFKA_OPTIONS).load().transform(parse) このパイプラインの定義を Databricks アセットバンドル として用意します。 Python コード内で参照する各種パラメータもここで定義します。 databricks.yml : bundle : name : adbdemo databricks_cli_version : ">=0.259.0" targets : demo : workspace : host : https://<ワークスペース識別子>.azuredatabricks.net mode : production # 連続モードをオンにするため resources : pipelines : bronze_ingest_eventhubs_raw : name : bronze_ingest_eventhubs_raw catalog : <カタログ名> schema : bronze tags : quality : Bronze continuous : true # 連続モードをオン (ストリーミングなので常時実行にする) channel : CURRENT edition : CORE photon : true clusters : # 今回はサーバレスではなくクラスターで実行 - label : default apply_policy_default_values : true node_type_id : Standard_D4ds_v5 custom_tags : quality : Bronze libraries : - file : path : ./pipelines/bronze_ingest_eventhubs_raw.py configuration : pipelines.clusterShutdown.delay : 60s streaming.dbw.serviceCredential : <サービス資格情報名> streaming.evh.namespace : <EventHubs名>.servicebus.windows.net:9093 streaming.evh.name : <EventHubsインスタンス名> streaming.kafka.requestTimeout : "60000" streaming.kafka.sessionTimeout : "30000" streaming.spark.maxOffsetsPerTrigger : "50000" streaming.spark.failOnDataLoss : "false" streaming.spark.startingOffsets : earliest 上記を使ってパイプラインをデプロイします。 databricks bundle validate databricks bundle deploy デプロイ完了後しばらく待ち、グラフが表示されて「実行中...」となれば成功です。 最後に、Event Hubs 経由で Databricks にデータ収集するソースとして、Fluent Bit が動作する環境を用意します。 この環境は前述の通り、VNet 内にある Event Hubs のプライベートエンドポイントの IP アドレスへ疎通できる場所に作成します。 Event Hubs へ ログ ( /var/log/system.log ) をストリーミングするコンフィグを作成します。 /etc/fluent-bit/fluent-bit.conf : [INPUT] Name tail Tag systemlog Path /var/log/system.log # 収集するログ [OUTPUT] Name kafka # Event Hubs の Kafka エンドポイントへ送信 Match systemlog timestamp_key timestamp timestamp_format iso8601 format json brokers <EventHubs名>.servicebus.windows.net:9093 # Event Hubs エンドポイント topics <EventHubsインスタンス名> rdkafka.security.protocol SASL_SSL rdkafka.sasl.mechanisms PLAIN rdkafka.sasl.username $ConnectionString rdkafka.sasl.password <EventHubsのSASポリシー接続文字列> 接続文字列はすでに Bicep で作成済みで、Event Hubs の共有アクセス (SAS) ポリシーの画面から取得できます。 なお、簡単のために Fluent Bit が動作する環境では、プライベートエンドポイントの FQDN ( <EventHubs名>.servicebus.windows.net ) を /etc/hosts で名前解決させます。 実際には Azure DNS Private Resolver を使うなどして、Azure 外からでもプライベート DNS ゾーンを参照できるようにするのがよいと思います。 それでは、実際に Fluent Bit が動作するサーバで、収集対象の /var/log/system.log にログを追記してみます。 すると、Fluent Bit が収集したログがストリーミング処理によってテーブルに追記されました。 以上、データストリーミングのパイプラインを閉域で実現できました! まとめ 本記事では、ハンズオン形式で Bicep (+α) を使って Azure Databricks のプライベート接続環境を構築しました。 また、その環境を使って Event Hubs 経由でのプライベートなデータストリーミングも実践しました。 今回の構築を通じて、特に以下のポイントが実践的な知見として得られました。 IaC の限界と工夫: ワークスペースストレージのような「動的リソース」は Bicep だけで完結させず、スクリプトと組み合わせる現実解が必要 閉域化の勘所: マネージドリソースやサーバレス (NCC) まで考慮することで、真にセキュアな構成が組める PaaS の柔軟性: 構成は複雑になるが、SaaS とは異なり、自社のセキュリティポリシーに合わせてネットワークを柔軟に制御できる 正直かなりニッチな内容になってしまいましたが、これから似たような環境を構築する方の参考になったり、PaaS データ基盤のカスタマイズ性の高さ (SaaS 系との大きな違いの1つ) が伝わったりしていれば嬉しいです。 ここまでかなりの長文でしたが、最後までご覧いただきありがとうございました! それでは、明日の記事もお楽しみに!
この記事は NTT docomo Business Advent Calendar 2025 9日目の記事です。 Unitree Go2はROSの通信ミドルウェアとしてEclipse Cyclone DDSを利用していますが、DDSはNATを越えられないという課題があります。 この課題に対し、DDSをZenohにブリッジしてNAT越えを実現する事例がコミュニティでいくつか紹介されています(1 1 , 2 2 , 3 3 )。 本記事ではこのアプローチをUnitree Go2に適用し、zenoh-plugin-ros2ddsを用いて Unitree Go2が扱うDDSメッセージをインターネット越しに送受信する方法を紹介します。 はじめに 環境 前提知識 Unitree Go2 ROS Zenoh 実装 ビルド 実行 まとめと今後の取り組み 参考 はじめに こんにちは。イノベーションセンターの柴原です。普段はエッジコンピューティング基盤技術の検証や生成AIアプリケーションの開発に取り組んでいます。 フィジカルAIという言葉を聞いたことがあるでしょうか。生成AIの次に来るテーマとして注目されており、物理世界を理解して自律的に行動するAIを指します。 フィジカルAIの発展により、ロボットがこなせるタスクの幅は飛躍的に広がっています。 一方で、ロボットにはバッテリー容量や搭載できる計算リソース量に制約があります。 これらの制約を克服するためには、クラウドをはじめとするロボット外部の計算リソースを活用することが不可欠になると考えています。 そこで本記事では、クラウドからロボットを制御するための第一歩として、キーボード入力でロボットを操作する簡単なデモを作成したので紹介します。 環境 次のような環境で実装しました。 機種: Unitree Go2 R&D Plus Docking Station (Jetson Orin NX):Ubuntu 22, ROS 2 Humble クラウド (Azure VM): Ubuntu 22, ROS 2 Humble 前提知識 Unitree Go2 Unitree Robotics社の小型四足歩行ロボットです。今回扱うGo2 R&D Plusは公式SDKを利用して二次開発ができるモデルです。 ROS ROS (Robot Operating System)はロボットのソフトウェア開発においてデファクトスタンダードのプラットフォームです。 通信方法やセンサ値のデータ構造、パッケージ管理機能を提供しており、Unitree Go2もROSを利用した二次開発が可能です。 Zenoh Unitree Go2はROSに対応していますが、その通信ミドルウェアはCyclone DDSに固定されています。 Cyclone DDSは隣のROSノードを自動発見するためにマルチキャストを使用するなどLAN向けに設計されており、NAT越えが困難です。 一方ROSの世界ではWANに対応した通信ミドルウェアとしてZenohが注目されています。現在ROSの最新バージョンであるJazzyでは公式にサポートされているようです。 Zenohの提供元であるEclipseはZenoh・Cyclone DDS間のブリッジも提供しており、これを利用してインターネット越しの通信を実現している事例がいくつかあります。 本記事ではZenohとブリッジを利用してインターネット越しにGo2のセンサ値を読み取り、キーボードからGo2を操作するところまでを実装します。 実装 TechShare社の 【Unitree Go2】キーボードからGo2を操作する2次開発方法 を基に、これをインターネット越しで実行します。 ビルド . └── workspace/ ├── docker/ │ ├── Dockerfile.azure │ ├── Dockerfile.jetson │ └── docker-compose.yml ├── src/ │ ├── ros/ │ │ ├── unitree_ros2/ │ │ └── cmd_vel_control/ │ ├── zenoh/ │ └── zenoh-plugin-ros2dds/ ├── zenoh-config-azure.json └── zenoh-config-jetson.json Docking StationのOS・ROS環境は、 unitree_ros2 の .devcontainer/docker-compose.yaml で定義されている devcontainer-humble サービスを使います。 クラウド側マシンでも同じサービスを、ベースイメージをARMのものからx64のものに変更して使います。 docker/ はこれらを移動しただけです。 src/ 配下に利用するパッケージを配置しています。 unitree_ros2 : Go2を二次開発するためのROSパッケージ cmd_vel_control : 【Unitree Go2】キーボードからGo2を操作する2次開発方法 で作成されたROSパッケージの一部 zenoh (Commit: 44f8b2489) : Zenoh本体を提供するRustパッケージ zenoh-plugin-ros2dds (Commit: 592422b) : Zenoh <-> CycloneDDSのブリッジを提供するRustパッケージ zenoh-plugin-ros2ddsはzenohに依存しており、バージョンによってビルドできないことがあるのでcommitを指定しています。これらをクラウドとJetsonそれぞれに配置し、コンテナ内で src/ をビルドします。 Clone git clone https://github.com/shibahara2/ros2_ws.git cd ros2_ws git submodule udpate --init --recursive コンテナに入ります。 cd docker docker compose up unitree_ros2-<azure or jetson> -d docker exec -it unitree_ros2-<azure or jetson> zsh Rustをインストールします。 curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source /root/.cargo/env rustup update zenohをビルドします。 cd src/zenoh cargo build --release zenoh-plugin-ros2ddsをビルドします。 cd src/zenoh-plugin-ros2dds cargo build --release ROSパッケージをビルドします。 cd src/ros colcon build 実行 クラウド上でzenohdを起動します。 # Cloud terminal 1 src/zenoh/target/release/zenohd -c zenoh-config-azure.json ポート7447でクライアントを待ちます。モード (router/peer/client)やプラグインのPATHを以下のjsonで設定しています。 $ cat zenoh-config-azure.json { mode: "router", plugins: { ros2dds: { __path__: "src/zenoh-plugin-ros2dds/target/release/libzenoh_plugin_ros2dds.so", } }, listen: { endpoints: ["tcp/0.0.0.0:7447"] }, } Jetson上でzenohdを起動します。 # Jetson terminal 1 src/zenoh/target/release/zenohd -c zenoh-config-jetson.json クラウドのzenohdに接続します。設定は以下の通りです。 $ cat zenoh-config-jetson.json { mode: "client", plugins: { ros2dds: { __path__: "src/zenoh-plugin-ros2dds/target/release/libzenoh_plugin_ros2dds.so", } }, connect: { endpoints: ["tcp/<クラウドのグローバルIP>:7447"] } } zenohd同士を接続すると勝手にトピックが同期され、クラウド上でJetson上のトピックが見られるようになります。 Go2のセンサ値を確認してみます。 # Cloud terminal 2 source src/ros/install/setup.sh export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp ros2 topic echo /sportmodestate 結果(最初の一部) --- stamp : sec : 1765203140 nanosec : 497936758 error_code : 1001 imu_state : quaternion : - -0.9969053864479065 - 0.005156185943633318 - 0.05559273064136505 - 0.05533893033862114 続いてクラウドからロボットを操作します。 クラウド上でノードを起動します。 # Cloud terminal 2 ros2 run teleop_twist_keyboard teleop_twist_keyboard ノード teleop_twist_keyboard はキーボード入力を受け付け、トピック /cmd_vel へpublishします。 Jetson上でノードを起動します。 # Jetson terminal 2 ros2 run unitree_ros2_example cmd_vel_control ノード cmd_vel_to_sport_request はトピック /cmd_vel をsubscribeし、トピック /api/sport/request へpublishします。これが低レイヤーの命令に変換されていき、最終的にモーターが駆動します。 実行の様子です。 このブラウザーは埋め込み動画に対応していません。 ターミナル画面が4分割されており、左上はクラウド上でzenohd、左下はJetson上でzenohd、右上はクラウド上でROSノード teleop_twist_keyboard 、右下はJetson上でROSノード cmd_vel_to_sport_request を実行しています。 (本来かなり運動性能が高いのですが、6畳の部屋では一歩が限界でした。) まとめと今後の取り組み 本記事ではUnitree Go2をクラウドから制御する簡単なデモを作成しました。 クラウド側の処理がシンプルだったため、Zenohにこだわる理由が伝わらなかったかもしれません。 確かにリアルタイム性を求めないアプリであれば、他に適したプロトコルがあります。 状態監視・ログ収集・UIといった処理は、MQTTやREST、WebSocketを使ってクラウド側に簡単に実装できます。 しかし私はロボットの制御ループそのものをどこまでオフロードできるかに興味があります。遅延やジッタがロボットの挙動に直結するため、ROSが提供する(予定の)Zenohを使うのが良さそうだと判断しました。 今後の取り組みとして、以下を調査したいです。 他プロトコルとの比較 SLAMや経路計画など、ロボットの制御ループのうち遅延の制約がそこまで厳しくない処理をクラウドで実行可能か フィジカルAIがロボットの制御ループに組み込まれることで、遅延の制約がどう変化するか またNTTドコモビジネスは docomo MEC というモバイル回線の基地局の側に置かれたエッジサーバーや、 5Gワイド という優先制御サービスなど、低遅延・低ジッタの基盤を提供しています。 これらのサービスを利用することで遅延・ジッタの制約が緩和され、オフロードできる範囲が広がるかもしれません。 本日はここまでです。明日の記事もお楽しみに! 参考 zenoh-bridge-ros2ddsでZenohとROS 2間通信 ↩ ROS 2のZenoh対応とZenohのROS 2対応 ↩ Zenoh bridge を用いた ROS 2 の通信性能評価 ↩
この記事は、 NTT docomo Business Advent Calendar 2025 8日目の記事です。 自動テストの文脈で、モックやスタブという用語を目にすることがあるかと思います。この用語は、人やテストフレームワークごとに異なった意味で使われることがあり、しばしば混乱を招いています。そして、そのような指摘をした上で概念の整理を図ったものとしてGerard Meszarosの書籍『xUnit Test Patterns』(xUTP) 1 とウェブサイト 2 があります。 xUTPでは、テスト対象(SUT)の依存コンポーネント(DOC)を置き換えるものを総称して「テストダブル」と呼び、その目的に応じて以下の種類に分類しています。 モック スタブ フェイク スパイ (ダミー) このxUTPによるテストダブルの分類については、日本語での素晴らしい解説記事がすでにいくつか存在しますが、私が社内でテストダブルの分類について説明するときは、それらの記事を紹介しつつも個人的に以下の図を利用しています。 この記事では、その図に簡単な説明を添えて紹介したいと思います。 用語の導入 テストダブルについて詳細な議論をするために、いくつかの用語を整理する必要があり、xUTPで定義されているいくつかの用語を導入します。 テスト対象(SUT; System Under Test)は、文字通りテストの対象を意味します。クラスやメソッド、関数などの他、システム全体を指すこともあります。文脈によって指し示す対象が異なるので、それらを抽象化したものです。 依存コンポーネント(DOC; Depended-on Component)とは、テスト対象が依存するものです。テスト対象と同様にクラスやメソッド、関数などの他、データベースや認証サービスなどを指すこともあります。 テスト対象と依存コンポーネントという概念が抽象化されているので、関数の入出力などの概念も抽象化をする必要があります。 テスト対象がテストから受け取る入力のことを直接入力、テスト対象からテストコードへの出力を直接出力といいます。テスト対象が関数であれば、典型的には、関数の引数が直接入力、返り値が直接出力にあたります。テスト対象がHTTPベースのWeb APIを提供するバックエンドサービスであれば、典型的には、HTTP Requestが直接入力、HTTP Responseが直接出力にあたります。 一方、テスト対象が依存コンポーネントから受け取る入力のことを間接入力、テスト対象から依存コンポーネントへの出力を間接出力といいます。テスト対象と依存コンポーネントが関数であれば、典型的には、依存される関数の引数がテスト対象の間接出力、依存される関数の返り値が間接入力にあたります。 テスト対象を中心として入出力の関係が整理されているため、(直接/間接)入力が関数の引数、(直接/間接)出力が関数の返り値、のような対応付けとはならないことに注意してください。 この節で導入したこれらの用語(テスト対象、依存コンポーネント、直接入出力、間接入出力)を図にまとめると、次のようになります。 テストダブルとその分類 xUTPでは、モックやスタブなどの依存コンポーネントを置き換えるものを総称してテストダブルと呼んでいました。そして、それらの概念をその目的によって以下の通り再整理しています。 モック モックとは、テスト対象が依存するコンポーネントへの間接出力を検証することを目的としたテストダブルです。間接出力の検証の例には、依存するリレーショナルデータベースに渡すSQL文の検証やWebフロントエンドからバックエンドへのリクエストの検証などがあります。 スパイ スパイとは、テスト対象が依存するコンポーネントへの間接出力を記録することを目的としたテストダブルです。間接出力に関心があるという意味でモックと似ていますが、スパイは間接出力を記録するもので、依存コンポーネントが実行された 後に 間接出力の検証ができます。 スタブ スタブとは、テスト対象が依存するコンポーネントからの間接入力を操作することを目的としたテストダブルのことです。間接入力の操作の例には、現在時刻を返す関数を常に指定した時刻で返すようにする操作などがあります。 フェイク フェイクとは、テスト対象が依存するコンポーネントの実装を置換することを目的としたテストダブルのことです。実装の置換の例には、リレーショナルデータベースに依存するコンポーネントのオンメモリ実装への置換やクラウドサービスのプロバイダーが提供するローカルで動作するエミュレーターなどが挙げられます。 フェイクは、依存するコンポーネントの実行速度が遅い場合やテスト環境で本物の依存するコンポーネントが利用できない場合などに利用されます。 ダミー xUTPではダミーと呼ばれる分類も導入されていますが、厳密にはダミーはテストダブルではなく値パターンの一部であるという議論も同時になされています。ダミーは今回紹介する図では整理しにくいこともあり、この記事では分類から除外します。 これらのテストダブルの分類のうち、モックとスパイは間接出力に、スタブは間接入力に、フェイクはそのどちらでもなく依存コンポーネントの実装に関心があります。 モック:間接出力の検証 スパイ:間接出力の記録 スタブ:間接入力の操作 フェイク:依存コンポーネントの実装の置換 したがって、これらの関心の違いを元に、次のように図(再掲)にまとめることができます。 おわりに この記事では、xUTPによるテストダブルの分類の図解を紹介しました。 この図が、少しでも理解の助けとなれば幸いです。 NTT docomo Business Advent Calendar 2025 を、明日もお楽しみに! 参考文献 Meszaros, Gerard. xUnit test patterns: Refactoring test code. Pearson Education, 2007. ↩ Meszaros, Gerard. xUnit Patterns.com, xunitpatterns.com . Accessed 8 Dec. 2025. ↩
この記事は、 NTT docomo Business Advent Calendar 2025 7日目の記事です。 こんにちは。イノベーションセンターの加藤です。普段はコンピュータビジョンの技術開発やAI/機械学習(ML)システムの検証に取り組んでいます。 ディープラーニングの実装をしているときに、変数のshapeを管理するのはなかなか大変です。いつのまにか次元が増えていたり、想定外のshapeがやってきたりして実行時に落ちてしまった!というのは日常茶飯事だと思います。 こういった問題に対して静的解析で何とかできないかと試行錯誤した結果を共有します。 mypyプラグインを使うモチベーション mypyプラグインの作成 初期化 jaxtyping annotationを拾う テンソル作成関数を拾う テンソル計算 ここで限界が来た(Future work) 変数付きのshape記法 次元の四則演算 stubの是非 レイヤーの型注釈 まとめ mypyプラグインを使うモチベーション Pythonのプログラムを静的検査する方法のひとつに mypy があります。これはソースコードにつけられた型アノテーションに矛盾がないか調べてくれるもので、変な代入や演算由来のエラーを未然に防ぐことができます。 しかしながら、NumPyやPyTorchなどの一般的な数値計算ライブラリにはそれなりの型がアノテーションされているものの、せいぜいテンソルの型(intやfloatなど)どまりで次元(shape)については考慮されていないため、そのままでは次元の不一致などを検出できません。 jaxtyping などのライブラリは元の型を拡張して次元などをアノテーションできるようにしてくれますが、これらは実行時解析のみをサポートしており、mypyからは扱えません。 from torch import Tensor import torch from jaxtyping import Float32, jaxtyped from beartype import beartype as typechecker from typing_extensions import reveal_type @ jaxtyped (typechecker=typechecker) def f (x: Float32[Tensor, "1 224 224" ]) -> Float32[Tensor, "1 1000" ]: print ( "processing f" ) w = torch.randn( 1000 , 224 * 224 ) x_flat = x.view( 1 , 224 * 224 ) y = x_flat @ w.t() return y.view( 1 , 1000 ) x: Float32[Tensor, "1 224 224" ] = torch.randn( 1 , 224 , 224 ) y = torch.randn( 1 , 224 , 225 ) print ( "f(x)" ) reveal_type(f(x)) # OK print ( "f(y)" ) reveal_type(f(y)) # NG """ 実行時は引数に誤ったshapeを渡した時点でエラー > python .\example.py f(x) processing f Runtime type is 'Tensor' f(y) Traceback (most recent call last): ... しかしmypyでは検出できない > mypy .\example.py example.py:19: note: Revealed type is "torch._tensor.Tensor" example.py:20: note: Revealed type is "torch._tensor.Tensor" Success: no issues found in 1 source file """ 結局プログラミングの段階ではあくまで可読性を高めるための注釈に留まり、実行時はお祈りしながら終了を待つことになります。 そこで本稿ではmypyプラグインを実装してjaxtypingの型に対する処理を追加することで、次元の整合性を実行前に検証できないかトライしてみました。もしこれができれば、mypyを使って次元込みの静的検査ができ、Visual Studio Codeのmypy拡張と連携すればプログラミング中もテンソルの次元を追うことができるようになります。 mypyプラグインの作成 初期化 uv でプロジェクトを新規作成します。 $ uv init --name jaxmy --lib Initialized project `jaxmy` $ uv add mypy jaxtyping $ uv add torch numpy pytest --optional tests src/jaxmy/mypy_plugin.py にプラグインスクリプトを作成します。 from typing import Any, Optional, List, Tuple import re from mypy.plugin import Plugin class ShapePlugin (Plugin): pass # TODO def plugin (version: str ): print ( "Hello world! version:" , version) return ShapePlugin そしてmypy実行時に自作のpluginを紐づけるには以下のようにpyprojectを編集します。 [build-system] requires = [ "hatchling" ] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = [ "src/jaxmy" ] [tool.mypy] ignore_missing_imports = true plugins = [ "jaxmy.mypy_plugin" ] mypy_path = "$MYPY_CONFIG_FILE_DIR/src/jaxmy/stubs" (mypy_pathについては後述) これでuv環境からmypyを実行するとpluginが介入するようになります。 > uv run mypy example.py Hello world! version: 1 . 18 . 2 jaxtyping annotationを拾う まずはjaxtyping記法によるアノテーションを拾うところから始めます。 jaxtypingが提供する型の実体はmypyなどの静的解析時( typing.TYPE_CHECKING == True )と実行時で異なっており、静的解析時は jaxtyping._indirection 内で定義された以下のコードが読み込まれます。 from typing import ( Annotated as BFloat16, # noqa: F401 Annotated as Bool, # noqa: F401 Annotated as Complex, # noqa: F401 Annotated as Complex64, # noqa: F401 Annotated as Complex128, # noqa: F401 Annotated as Float, # noqa: F401 ... そのためあらゆる型は Annotated[T, ...] とみなされ、これは事前に T と解決してから静的解析が走ります。 これによってjaxtyping記法が型として正しくない表記であるにもかかわらずエディタやmypyのチェックをすり抜けているのですが、 Float32[Tensor, "1 224 224"] や Int8[Tensor, "3 224 224"] などがすべて Tensor という同じ型に置き換えられてしまうため静的解析が不可能になります。 そこで、stubを注入して呼び出しを捕捉することでプラグインから触れるようにします。 # stub/jaxtyping/__init__.pyi from typing import Any, Literal, NoReturn, Union, TypeVar, Generic _ArrayType = TypeVar( "_ArrayType" ) _Shape = TypeVar( "_Shape" ) class AbstractArray (Generic[_ArrayType, _Shape]): pass class UInt2 (AbstractArray[_ArrayType, _Shape]): ... class UInt4 (AbstractArray[_ArrayType, _Shape]): ... class UInt8 (AbstractArray[_ArrayType, _Shape]): ... """以下よしなに""" これでプラグインからは get_type_analyze_hook を通して jaxtyping.Float32 などのアノテーションを拾えるようになりました。ただしjaxtyping記法はshapeの部分が型注釈として許されない文字列リテラルであるため、これを有効な型に置き換える必要があります。これを怠るとmypyがshape部分を Any に置き換えてしまいます。 from typing import Any, Optional, List, Tuple import re from mypy.plugin import Plugin, FunctionContext, AnalyzeTypeContext from mypy.types import Instance, TupleType, Type, UnboundType, LiteralType, EllipsisType, RawExpressionType, TypeStrVisitor from mypy.checker import TypeChecker def parse_dimstr (dimstr: str ) -> Optional[List[ int ]]: """Parse a dimension string like "1 3 224 224" into a list of int.""" dims: List[ int ] = [] for dim in dimstr.split( " " ): dim = dim.strip() if dim.isdigit(): dims.append( int (dim)) else : return None return dims def dump_dimlist (dimlist: List[ int ]) -> str : """Dump a list of int back into a dimension string.""" dimstrs: List[ str ] = [] for dim in dimlist: dimstrs.append( str (dim)) return " " .join(dimstrs) def construct_instance (api: TypeAnalyzerPluginInterface, dtype: str , backend: Type, dim_list: List[ int ]) -> Type: """Construct an Instance of a jaxtyping type with the given dtype, backend, and shape.""" # TODO : 本当はFloatなどのUnion型にも対応すべきだが、とりあえず保留 # shape表現をLiteralで包みjaxtypingのinstanceを返す。 return Instance( api.named_type(f "jaxtyping.{dtype}" ).type, [backend, LiteralType(value=dump_dimlist(dim_list), fallback=api.named_type( "builtins.str" ))] ) def analyze_jaxtyping (ctx: AnalyzeTypeContext) -> Type: """Parse Dtype[Array, "shape"] to the mypy-friendly type Dtype[Array, Literal["shape"]].""" typ = ctx.type # UnboundType. 何のことかはわからない if len (typ.args) != 2 : return typ backend, shape = typ.args backend = ctx.api.analyze_type(backend) # UnboundTypeなbackendを解決 (Tensorなどのinstanceになる) if not isinstance (shape, RawExpressionType) or type (shape.literal_value) is not str : return backend # fallback dtype = typ.name # e.g., "Float32" dim_str = shape.literal_value # e.g., "1 224 224" dim_list = parse_dimstr(dim_str) # validationもかねてパース if dim_list is None : return backend # fallback return construct_instance(ctx.api, dtype, backend, dim_list) DTYPE_ANNOTS = { "UInt2" , "UInt4" , "UInt8" , ...} class ShapePlugin (Plugin): def get_type_analyze_hook (self, fullname: str ): m = re.match( r"jaxtyping\.(\w+)" , fullname) if m and m.group( 1 ) in DTYPE_ANNOTS: return analyze_jaxtyping def plugin (version: str ): return ShapePlugin 今回はjaxtypingのサブセットとして数値リテラルのみ(例: Float32[Tensor, "1 3 224 224"] )をサポートします。内部的には基本リテラルで持ち( Float32[Tensor, Literal["1 3 224 224"]] )、都度バラして型推論を行います。 ※ ちなみに Float32[Tensor, Literal["1 3 224 224"]] よりも取り回しのよい内部表現を使う手もありますが、mypyには検査対象のプログラムで呼ばれているモジュール(とビルトイン)しか扱えないという制約があります。そのため、何かいい感じのオリジナル型を導入したい場合はjaxtypingそのものを改造する必要があります。 テンソル作成関数を拾う これに加えて、 torch.zeros() などの初期化用の関数を get_function_hook によって捕捉し、これらのテンソルにjaxtyping用の型を付与します。 まずstubを作成してtorchを扱えるようにします。 # stubs/torch/__init__.pyi from torch._tensor import Tensor as Tensor from typing import Any def randn (*size: int , out= None , dtype= None , **kwargs) -> Tensor: ... def rand (*size: int , out= None , dtype= None , **kwargs) -> Tensor: ... def zeros (*size: int , out= None , dtype= None , **kwargs) -> Tensor: ... def ones (*size: int , out= None , dtype= None , **kwargs) -> Tensor: ... そしてhookを作成します。この手の関数は入力の自由度が高く、引数を手でパースするのがちょっと大変です。 INITIALIZER_NAMES = { "torch.randn" , "torch.rand" , "torch.zeros" , "torch.ones" , } dtype_mapper = { # mapping torch.dtype to jaxtyping type "float32" : "Float32" , "float" : "Float32" , "float64" : "Float64" , "double" : "Float64" , ... } def hook (fullname: str ): if fullname in INITIALIZER_NAMES: return construct_from_shape return None Argument = namedtuple( 'Argument' , [ 'arg_type' , 'arg_kind' , 'arg_name' , 'arg' ]) def transpose_funcargs (ctx: FunctionContext | MethodContext) -> dict [ str , Argument]: """[引数型], [引数名], ... を [(引数型,引数名,...)] にまとめる""" ctxdict = {} for i, name in enumerate (ctx.callee_arg_names): if len (ctx.arg_kinds[i]) == 0 : continue ctxdict[name] = Argument( arg_type=ctx.arg_types[i], arg_kind=ctx.arg_kinds[i], arg_name=ctx.arg_names[i], arg=ctx.args[i] ) return ctxdict def construct_from_shape (ctx: FunctionContext): if not isinstance (ctx.api, TypeChecker): return ctx.default_return_type # 失敗時は基本的にこれを返す ctxdict = transpose_funcargs(ctx) if "size" not in ctxdict: return ctx.default_return_type args = ctxdict[ "size" ].arg_type dimensions: List[Type] = [] # shape指定にはf(1,2,3)とf((1,2,3))の二通りあるので対応 if len (args) == 1 and isinstance (args[ 0 ], TupleType): dimensions.extend(args[ 0 ].items) else : dimensions.extend(args) # すべて数値定数であるときのみ対応する if all (( isinstance (dim, Instance) and dim.last_known_value is not None and type (dim.last_known_value.value) is int ) for dim in dimensions): shape_list = [dim.last_known_value.value for dim in dimensions] if "dtype" in ctxdict: # dtype指定があるとき dtype = ctxdict[ "dtype" ] dtype_argtype = dtype.arg_type[ 0 ] if isinstance (dtype_argtype, Instance) and dtype_argtype.type.fullname in [ "torch.dtype" ]: jaxtype = dtype_mapper.get(dtype.arg[ 0 ].name, None ) if jaxtype is None : ctx.api.fail( f "Unsupported dtype {ctxdict['args'][0].name} for torch function." , ctx.context ) return ctx.default_return_type # 指定の型とshapeからjaxtyping型 DType[Tensor, Literal["shape"]] を作る return construct_instance( ctx.api, jaxtype, ctx.api.named_type( "torch.Tensor" ), shape_list ) else : ctx.api.fail( f "Unsupported dtype {dtype_argtype} for torch function." , ctx.context ) return ctx.default_return_type return construct_instance( # デフォルトdtypeはfloat32 ctx.api, "Float32" , ctx.api.named_type( "torch.Tensor" ), shape_list ) return ctx.default_return_type これで torch.randn などの返り値型がTensorからjaxtypingになりました。 def g (x: Float32[Tensor, "3 224 224" ]): ... x: Float32[Tensor, "3 224 224" ] = torch.randn( 3 , 224 , 224 ) # OK y: Float32[Tensor, "3 224 226" ] = torch.randn( 3 , 224 , 224 ) # Incompatible types in assignment g(x) # OK テンソル計算 つぎはテンソル同士の演算を定義します。考慮すべきことは以下の3つです。 型が異なる時は"偉い"方に合わせる shape不一致の時はエラー shapeのブロードキャスト(片方の次元が1の時はもう片方に合わせてもよい) ですが、いったん型の方は無視します。 まず準備としてテンソルの演算子をstubに定義します。 # stub/jaxtyping/__init__.pyi Self = TypeVar( "Self" , bound= "AbstractArray[_ArrayType, _Shape]" ) class AbstractArray (Generic[_ArrayType, _Shape]): def __add__ (self: Self, other: Any): ... def __radd__ (self: Self, other: Any): ... def __iadd__ (self: Self, other: Any) -> Self: ... def __sub__ (self: Self, other: Any): ... def __rsub__ (self: Self, other: Any): ... def __isub__ (self: Self, other: Any) -> Self: ... def __mul__ (self: Self, other: Any): ... def __rmul__ (self: Self, other: Any): ... def __imul__ (self: Self, other: Any) -> Self: ... そしてこれを get_method_hook で捕捉します。 arithmetic_names = { "__add__" , "__radd__" , "__sub__" , "__rsub__" , "__mul__" , "__rmul__" , "__pow__" , "__div__" , "__rdiv__" , ... } def decompose_instance (typ: Instance) -> Optional[Tuple[ str , Type, List[ int ]]]: """Decompose a jaxtyping type into (backend type, shape as list of ints).""" if len (typ.args) != 2 : return None backend, shape = typ.args if not isinstance (shape, RawExpressionType) or type (shape.literal_value) is not str : return None dtype = typ.name # e.g., "Float32" dim_str = shape.literal_value # e.g, "1 224 224" dim_list = parse_dimstr(dim_str) if dim_list is None : return None return dtype, backend, dim_list def tensor_arithmetic (ctx: MethodContext): self_type = ctx.type other_type = ctx.arg_types[ 0 ][ 0 ] if isinstance (self_type, Instance) and isinstance (other_type, Instance): if self_type.type.fullname.startswith( "jaxtyping." ): self_result = decompose_instance(self_type) if self_result is None : ctx.api.fail( f "Unable to parse Self as jaxtyping {self_type}" , ctx.context ) return ctx.default_return_type self_dtype, self_backend, self_dims = self_result else : ctx.api.fail( f "Self must be jaxtyping {self_type}" , ctx.context ) return ctx.default_return_type if other_type.type.fullname.startswith( "jaxtyping." ): other_result = decompose_instance(other_type) if other_result is None : ctx.api.fail( f "Unable to parse Other as jaxtyping {other_type}" , ctx.context ) return ctx.default_return_type other_dtype, other_backend, other_dims = other_result elif other_type.type.fullname in ( "builtins.int" , "builtins.float" ): other_dtype = self_dtype other_backend = self_backend other_dims = [] # scalar if repr (self_backend) != repr (other_backend): ctx.api.fail( f "Backend mismatch: {self_backend} vs {other_backend}" , ctx.context ) return ctx.default_return_type out_backend = self_backend if self_dtype != other_dtype: ctx.api.fail( f "Dtype mismatch: {self_dtype} vs {other_dtype}" , ctx.context ) return ctx.default_return_type # TODO : promote dtype out_dtype = self_dtype if self_dims == other_dims: out_dims = self_dims else : # broadcast check longest = max ( len (self_dims), len (other_dims)) self_dims = [ 1 ] * (longest - len (self_dims)) + self_dims other_dims = [ 1 ] * (longest - len (other_dims)) + other_dims out_dims = [] for d1, d2 in zip (self_dims, other_dims): if d1 == d2: out_dims.append(d1) elif d1 == 1 : out_dims.append(d2) elif d2 == 1 : out_dims.append(d1) else : ctx.api.msg.fail( f "Shape mismatch: {self_dims} vs {other_dims}" , ctx.context ) return ctx.default_return_type # fail return construct_instance(ctx.api, out_dtype, out_backend, out_dims) ctx.api.fail( f "Unknown types for tensor arithmetic: {self_type} and {other_type}" , ctx.context ) return ctx.default_return_type class ShapePlugin (Plugin): def get_method_hook (self, fullname: str ): if fullname.startswith( "jaxtyping." ): # jaxtyping.Float32.__add__など if fullname.split( "." )[- 1 ] in arithmetic_names: return tensor_arithmetic 注意点として、どうも実行時と同じように __add__ から __radd__ へのフォールバックがなされているらしく、 __add__ の処理で api.fail によるエラーを吐いても、 __radd__ の型チェックが未実装のままだとそちらで解決したことになりエラーが消えてしまうようです。ちゃんと両方処理するか、フォールバック先を無条件でfailさせる必要があります。 これで以下のテストに対応できます。 x: Float32[Tensor, "3 224 224" ] = torch.randn( 3 , 224 , 224 ) # OK y: Float32[Tensor, "3 224 226" ] = torch.randn( 3 , 224 , 226 ) # OK reveal_type(x + x) # OK reveal_type(x * 2.0 ) # OK (scalar) reveal_type(torch.randn( 1 , 224 , 224 ) + x) # OK (broadcasting) reveal_type(x + y) # Shape mismatch: [3, 224, 224] vs [3, 224, 226] ここで限界が来た(Future work) この時点でテンソルの四則演算ができるようになりましたが、ここでギブアップしてしまいました。 実用レベルにするには以下のようにまだまだやるべきことが山のようにあります。 変数付きのshape記法 jaxtypingは"batch 3 height width"のような記法に対応しており、これができれば畳み込みニューラルネットワークなど入力画像のサイズを気にしないものにも型を付けることができます。 次元の四則演算 例えばテンソルを結合したときに次元を足し算したり、upsampleでは掛け算、downsampleでは割り算などをする必要があります。そしてこれは変数を許すと鬼のように難しくなります。 例えばUNetなどは画像をdownsampleしたのちupsampleしますが、downsampleでの割り算は小数切り捨てなのでupsampleしても元に戻るとは限りません。つまり割り算と掛け算を縮約することができないため、 batch 3 height//8*8 width//8*8 のようなshapeが batch 3 height width と一致するかなどの検証をする必要があります。 これはあまりにも辛いので、「 height は8で割り切れる」のような注釈をjaxtypingに新しく設けることで割り算をうまく処理するというのが無難そうです(こうすることでUNetに中途半端なサイズの画像を入れてバグらせるというのも回避できます)。 stubの是非 プラグインがjaxtypingやPyTorchなどのライブラリ由来の型を拾うためにstubを使いましたが、果たしてこの使い方が正しいのかという懸念があります。もっとエレガントな方法はないのでしょうか…… レイヤーの型注釈 PyTorchを扱うからにはnn.Moduleに対応する必要があるでしょう。ですがニューラルネットのあらゆるレイヤーに対してjaxtypingの型検査を実装するというのは骨が折れます。 さらにmypyを基盤にする上でおそらく一番の鬼門は、PyTorchでは一般的な以下のコーディングです。 layers = nn.ModuleList([nn.Linear( 100 , 50 ), nn.ReLU(), nn.Linear( 50 , 10 )]) def forward (x: Tensor): for layer in layers: x = layer(x) return x mypyは変数の再代入があっても 対応できるらしい のですが、果たしてforループが回った後の型はつけられるか怪しいです。 まとめ この記事ではmypyプラグインの機能を利用して、PyTorchのソースコードに次元付きの型注釈がつけられないか挑戦してみました。それなりの機能は持たせられそうでしたが、実用的なレベルまでいけるかどうかは微妙そうです。
この記事は NTT docomo Business Advent Calendar 2025 3 日目の記事です。 みなさんこんにちは、イノベーションセンターの @Mahito です。 普段は社内のエンジニアが働きやすくなることを目標に、コーポレートエンジニアのような活動やエンジニア向けイベントの企画・運営をしています。 今回は、上でも述べているように、 社内のエンジニアが働きやすくすることを目標に活動をしているイノベーションセンターの取り組み Engineer Empowerment プロジェクトについて紹介します。 NTTドコモビジネスの中で、エンジニアが働きやすくなるためにどのような活動をしているのか、興味がある方に読んでいただければと思います。 Engineer Empowerment プロジェクトとは Engineer Empowerment プロジェクト設立の背景 これまでの活動内容 パスワードマネージャーの全社導入 NTT グループ向けイベントの開催 これまでの活動からの学びと伝えたいこと 課題を共有する 実践する 評価・フィードバックする 現在・今後の活動 今後の活動 まとめ Engineer Empowerment プロジェクトとは Engineer Empowerment プロジェクトのミッションは、 NTTドコモビジネスや NTT グループのエンジニアが働きやすい環境・成長できる場を構築することです。 これらを実現するために、以下の 3 つの取り組みを行っています。 エンジニアからの課題を収集・集約したうえで、エンジニア有志と協働した解決策の検討・実施 働きやすい環境やルールの整備・見直しに関する関連部署との調整 エンジニアの成長やプレゼンス向上実現の場の用意やそうした企業文化の醸成 1 つめに「 エンジニア有志と協働 」と書いてありますが、じつはこのプロジェクトは私の 1 人プロジェクトです。 しかしながら、上記の取り組みをする際には、社内や NTT グループのエンジニア有志が協力してくれています。 そのおかげで、2021 年の 12 月から活動をしていますが、この 4 年の間にいくつかの成果を出すことができています。 Engineer Empowerment プロジェクト設立の背景 私はこのプロジェクトを立ち上げる前は、R&D のエンジニアとして、OpenStack のコミュニティ対応や、 Docker、Kubernetes、Spinnaker などの OSS の調査や社内導入のための検証などをしていました。 その傍ら、 NTT Tech Conference という NTT グループのエンジニアが発表をするイベントや、NTT グループ内限定のイベントの企画・運営もしていました。 こうしたイベントの中で、社内やグループのエンジニアの働く環境への課題を感じそれをなんとかしたいという思いがあり、 その一環として、以前記事にもした エンジニアがエンジニアのために開発・検証用 PC を整備した話 に取り組みました。 この取り組みを通じて、情報システム部や情報セキュリティ部では手が回らない エンジニアの働く課題をエンジニアが解決できるということを感じ、これ以外の問題にも取り組むようになりました。 こうした自分の本来業務のミッションとは違った活動を当時の上司からは理解してもらったうえで実施していました。 ただ、価値のある活動だと認められている一方、チームのミッションとは違う活動を続けることに対して、自分の中で葛藤がありました。 その折、NTTドコモビジネスで技術顧問をしている吉羽( @ryuzee )さんと 1on1 をした際、 上記の悩みを相談したところ、それなら自分でプロジェクトを立ち上げてみたらという話をされました。 自分自身でもそれは頭の片隅にあったのですが、吉羽さんと話をする中でそういうプロジェクトがあってもいいかという納得が生まれ、 Engineer Empowerment プロジェクトを立ち上げました。 ちなみに、Engineer Empowerment (エンジニアに力を与える)は当時の上司から名付けてもらいました。 これまでの活動内容 Engineer Empowerment プロジェクト立ち上げ当時は上記の開発・検証用 PC の整備を半年で決着をつけてプロジェクトを畳む予定でした。 しかし、その計画が社内の調整などで遅延する中でそれ以外のエンジニアの働く課題も見つかり、 開発・検証用 PC の整備が完了した後も、それらの課題に 1 つずつ取り組んでいるうちに現在 5 年目を迎えています。 これまでに取り組んだ課題の例としては、以下のようなものがあります。 パスワードマネージャーの全社導入(トライアル中) 開発・検証用 PC から社内システムに接続するための VDI(Virtual Desktop Infrastructure) の提供(情シスへノウハウを引き継ぎ終了) GitHub Enterprise の全社化とその運用 Miro の全社化とその運用 各種 NTT グループ内部向けイベントの開催 NTT Tech Conference の企画・運営 当エンジニアブログのリニューアルとその運用 技術系 SNS 運用に関する取りまとめ etc これらの活動の多くは、エンジニアからの「こういうことに困っている」や「こういうことがしたい」という話から始まっています。 ここでは一例として、パスワードマネージャーの全社導入と、NTTグループ向けイベントの開催について紹介します。 パスワードマネージャーの全社導入 パスワードマネージャーはとあるエンジニアの「管理する機器やサービスが多すぎてパスワード管理が辛い」という話から始まっています。 一般的に、ID/Pass はそのログイン先ごとに異なるものを利用することが推奨されています。 しかしながら、人間の記憶には限界があるため、結果的に覚えやすいパスワードを使いまわしたり、 あるいは規則性のあるパスワードを利用するなど、どこかのサービスで ID/Pass が漏洩した際にリスクが高まります。 こうした問題を解決するために、パスワードマネージャーを使うことで、 機器やサービスごとの異なる ID/Pass を人間が覚えずとも安全に使えるようにしたいという話がありました。 そこで、Engineer Empowerment プロジェクトでは、部内で利用したい人向けにパスワードマネージャーの提供を始めました。 1 年ほど使ったところで、パスワードマネージャーの利用を継続するか判断するために利用者アンケートをとったところ、 想定通り便利になったという声が多い中に「グループで安心して ID/Pass を共有できるようになった」という声がありました。 会社のセキュリティルールとして、原則共有 ID は禁止されているのですが、 現実問題として機器やサービスの仕様(管理者権限アカウントが 1 つしか作れないなど)で共有 ID を使わざるを得ないケースがあります。 そうした場合に従来は口伝や鍵付きの Excel で管理していたそうですが、 それをパスワードマネージャーで安全に共有できるようになり、利便性や安全性が上がったとのことでした。 私は全社でも同じような話やニーズがあるのではないかと思い、 情報セキュリティ部にこの内容を共有したうえで全社アンケートを実施したところ、 同様にパスワード管理や共有の課題を感じている人が多いとわかりました。 そこで、情報セキュリティ部と話し合ったうえで現在は情報セキュリティ部主導で全社トライアルという形で、 パスワードマネージャーの導入が進められています。 NTT グループ向けイベントの開催 2022 年の年末ぐらいに「OpenAI を使ったハッカソンがしたい」という話を社内のエンジニアからされたことをきっかけに、2023 年の夏に NTT グループ内のイベントとして NTT Group Azure OpenAI Day と NTT Generative AI Hack Day というイベントを開催しました。 当時、Azure OpenAI Service はリクエストを出して Waiting List に登録されてから利用できるまでに数週間かかっており、すぐには利用できない状態でした。 そこで、NTT Com (現在:NTTドコモビジネス)を退職してマイクロソフトで働いていた友人に相談したところ、営業を含めた打ち合わせが設けられ、気がつくと NTT を巻き込んだ一大イベントになっていました。 イベントスタッフは NTT グループのエンジニア有志で構成され、 1日目の NTT Group Azure OpenAI Day にはグループから 1,000 人を超える参加者が集まり、 2~3 日目の NTT Generative AI Hack Day には 100 人程度集まりました。 イベントについては エヌ・エフ・ラボラトリーズの方が少し書いてくれていたりします。 Azure OpenAI Serviceでマッチングアプリの返信を自動生成してみた - NFLabs. エンジニアブログ 当初は自社に閉じた小さいイベントの予定でしたが、 気がつくとグループを巻き込む大規模イベントになってしまいました。 しかし、こうしたイベントを通じて、NTT グループのエンジニア同士の交流や技術情報の共有が進んだのは良かったと思っています。 これまでの活動からの学びと伝えたいこと これまでの活動を通じて、エンジニアの課題解決や、やりたいことを実現するために、以下のようなことの重要性を改めて感じています。 課題を共有する 実践する 評価・フィードバックする 課題を共有する これはエンジニアが持つ働くうえでの課題(開発・検証端末や使えるツール、開発に関するルールなど)をまずはエンジニアの中で共有することです。 共有することで、その課題を多くの人が感じていたり、言われて初めて気づくことで、 その課題を解くことが自分たちの働きやすさにつながるとなったものについては解決に向けて働きかけることができます。 また、これはエンジニアの中だけでなく関係する部署(情報システム部、情報セキュリティ部、広報、人事など)とも共有することで、 相手が認識していない課題を認識してもらい、一緒に解決へ向けた取り組みを進めることにつながります。 今回挙げたパスワードマネージャーの話では、まさに情報セキュリティ部では見えていなかった現場の課題を共有することで、 パスワードマネージャーによる解決という話につなげることができました。 実践する 課題がわかったり、そもそもやりたいことがあった場合に、計画だけでなく失敗してもいいので実際に実施してみることが重要です。 実際にやってみることで、計画段階では見えなかった問題点が見えてきたり、 自分たちでは解決できなかった問題を他のエンジニアが協力してくれることで、解決できることがあります。 また、Azure OpenAI Service を使ったハッカソンイベントのような、 思ったより大事になることもありますが、そういったものも含めて楽しめる事が多いと思います。 そして、実践するのは「 やりたいこと 」にするのをお勧めします。 私はとある SaaS の管理でやりたいわけではないが会社の事情を鑑みて「 やったほうがいいこと 」に手を出した際に、 興味があって手伝ってくれる人がいるだろうと気軽に始めました。 しかし、実際には社内でほとんど協力が得られず、 1 人でしんどい思いをする羽目になった経験があります。(今はなんとかして解決しております) もし、あなたが会社の中で有志たちとエンジニアの課題を解決するのであれば「 やったほうがいい 」や「 やるべき 」に惑わされず、 自分がやりたいこと、そしてまわりもやりたいこと にすることをお勧めします。 本当に、やったほうがいいことややるべきことには会社が人をつけるはずです。 評価・フィードバックする 4 年間の活動の反省でもあるのですが、自分たちのやってきた活動がどんな効果を上げたのか、 社内のエンジニアが上げてくれたリクエストがどうなったのかを定期的に評価・フィードバックすることが重要です。 私は昨年までは、Engineer Empowerment プロジェクトでの活動を大々的にアピールすることはせず、 知ってる人が知ってくれればいいかなという風に思っていました。 しかし、エンジニアが抱えている課題を自分たちで解決できることをもっと多くのエンジニアに知ってもらうことで、 今までよりも多くのエンジニアの活動に良い効果を与えていけると考えを改め、今年から積極的に情報発信をしています。 そのため、社内のエンジニアがくれたリクエストに対して、 活動内容やその結果を共有することで、より多くのエンジニアにこの取り組みを知ってもらい、 エンジニアが働きやすくなるための活動に参加してもらえるようにしていきたいと考えています。 今回の記事は、こうした取り組みを社内だけに閉じず、社外にも共有したいと思いブログという形で発信させてもらっています。 現在・今後の活動 現在は主に、コーディングエージェントを利用できるようにする取り組みを進めています。 NTT グループでは AI 利用に関してガバナンス規定類が定められており、 AI を使う上で、その利用リスクを評価した上での利用が求められます。 NTTのAIについて コーディングエージェントは利用するモデルが学習した内容やプロンプトで指示した内容次第では、 OSS のライセンスに違反するなど、第三者の知的財産を侵害する可能性があります。 こうした点に対して、エンジニアが安心してコーディングエージェントを利用するために、 コーディングエージェントの入出力に対してガードレールが設けられないかを調査・検証したり、 法務や知的財産を担当する方々とサービス規約面からの補償なども含めて安全に使えるコーディングエージェントの調査・検証を進めています。 また、それらの内容をコーディングエージェントを利用するためのガイドラインとして作成を進めており、 近い内に社内でコーディングエージェントが正式に使えるようになる予定です。 さらに、NTT グループのエンジニアにもコーディングエージェントの効果を実感してもらうために、 NTT グループのエンジニア有志と一緒に、コーディングエージェントを実際に触るワークショップを開催する準備を進めています。 今後の活動 私個人の思いとしては、この Engineer Empowerment プロジェクトは近いうちに終わらせたいと思っています。 これはネガティブな理由ではなく、このプロジェクトがなくても社内や NTT グループのエンジニアが、 自分たちでエンジニアが働くうえでの問題を解決し、自分たちで働きやすい環境を作っていけるようになるのが理想だと考えているからです。 そのためにも、今後も Engineer Empowerment プロジェクトでは、一緒に問題解決してくれる社内や NTT グループのエンジニア有志とともに、 エンジニアが働くうえでの問題を解決しながら仲間を増やしていきます。 そして、そのノウハウを共有することで、誰もがよりエンジニアとして働きやすい環境を作っていけるようになることで、このプロジェクトを終えられればと思っています。 まとめ 本記事では、社内のエンジニアの課題ややりたいことをエンジニアの立場から解決や実現に向けて働きかけるプロジェクトについて紹介させていただきました。 エンジニアの課題はエンジニアの中で共有し、実践し、評価・フィードバックすることで、改善できます。 また、関係部署ともその取組を共有することで、より良い解決策を見つけたり、会社全体にいい影響を及ぼすことができます。 この記事が、あなたの会社や組織でもエンジニアが働きやすくなるための取り組みのきっかけになれば幸いです。 明日もお楽しみに。
この記事は、 NTT docomo Business Advent Calendar 2025 2 日目の記事です。 Android 15 から Android 端末上で Linux 環境を動かすことが可能になりました。せっかくなので、 OpenStack をインストールして VM を動かしてみました。 はじめに スマホの Linux 開発環境に SSH する 開発環境を探検する OpenStack のインストール方法について DevStack を実行して minimal な OpenStack 環境をつくる スマホ OpenStack に VM を建ててみる トラブルシューティング Linux 開発環境が落ちる VM が boot しない まとめ はじめに こんにちは。 Smart Data Platform (SDPF) クラウド/サーバー 仮想サーバーチームの杉浦 ( @Kumassy_ ) です。 普段はハイパーバイザや OpenStack の開発・検証をしています。 私は Pixel 8 Pro をメインのスマホとして使っています。外出先でスマホを眺めていたところ、開発者向けオプションに Linux 開発環境が追加されているのに気づきました。 どうやら 2025 年 3 月ごろから Android 15 端末向けに追加されたもののようです。 sudo も利用できますし、 Docker コンテナも起動できるようです。そこで、普段 OpenStack の開発をしている身としては、 OpenStack 環境を構築して VM を起動させてみることにしました。 スマホの Linux 開発環境に SSH する スマホは画面が小さいですし文字を打つのも不便なので、 laptop から SSH して遊びたいです。 外部ネットワークには出られるようですが、 NAT 配下にいて直接 SSH できなさそうなので、 SSH をポートフォワードして外部からアクセス可能にします。 まずはスマホ上のターミナルで ssh をインストールして、ログインパスワードを設定します。 sudo apt install ssh sudo systemctl start sshd sudo passwd droid そして、 22/tcp を laptop に転送します。 ssh -R 10022:localhost:22 kumassy@<laptop ip> こうすれば laptop から以下のようにしてスマホ上のターミナルにログインできます。 ssh -p 10022 droid@localhost これで普通の Linux マシンと同じように作業できますね。 この手法は @kamiya334 に 教えてもらいました 。 開発環境を探検する 初めに Linux 開発環境がどのような環境なのか見てみました。 スマホなので仕方ないのですが、以下のように現代の開発環境としてはかなり頼りないものとなっていました。 CPU droid@localhost:~$ lscpu Architecture: aarch64 CPU op-mode(s): 64-bit Byte Order: Little Endian CPU(s): 9 On-line CPU(s) list: 0-8 Vendor ID: ARM Model name: Cortex-A715 Model: 0 Thread(s) per core: 1 Core(s) per socket: 3 Socket(s): 1 Stepping: r1p0 BogoMIPS: 49.15 Flags: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt f cma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb pac a pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm sveb f16 i8mm bti Model name: Cortex-A510 Model: 1 Thread(s) per core: 1 Core(s) per socket: 1 Socket(s): 1 Stepping: r1p1 BogoMIPS: 49.15 Flags: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt f cma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb pac a pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm sveb f16 i8mm bti Model name: Cortex-A715 Model: 0 Thread(s) per core: 1 Core(s) per socket: 2 Socket(s): 1 Stepping: r1p0 BogoMIPS: 49.15 Flags: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt f cma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb pac a pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm sveb f16 i8mm bti Model name: Cortex-A510 Model: 1 Thread(s) per core: 1 Core(s) per socket: 1 Socket(s): 1 Stepping: r1p1 BogoMIPS: 49.15 Flags: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt f cma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb pac a pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm sveb f16 i8mm bti Model name: Cortex-A715 Model: 0 Thread(s) per core: 1 Core(s) per socket: 1 Socket(s): 1 Stepping: r1p0 BogoMIPS: 49.15 Flags: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt f cma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb pac a pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm sveb f16 i8mm bti Model name: - Model: 1 Thread(s) per core: 1 Core(s) per socket: 1 Socket(s): 1 NUMA: NUMA node(s): 1 NUMA node0 CPU(s): 0-8 Vulnerabilities: Gather data sampling: Not affected Itlb multihit: Not affected L1tf: Not affected Mds: Not affected Meltdown: Not affected Mmio stale data: Not affected Reg file data sampling: Not affected Retbleed: Not affected Spec rstack overflow: Not affected Spec store bypass: Mitigation; Speculative Store Bypass disabled via prctl Spectre v1: Mitigation; __user pointer sanitization Spectre v2: Mitigation; CSV2, BHB Srbds: Not affected Tsx async abort: Not affected メモリ droid@localhost:~$ free -h total used free shared buff/cache available Mem: 3.8Gi 903Mi 2.2Gi 5.6Mi 974Mi 3.0Gi Swap: 981Mi 0B 981Mi ストレージ droid@localhost:~$ df -h Filesystem Size Used Avail Use% Mounted on /dev/vda1 214G 1.5G 13G 10% / /dev/vda2 191M 191M 0 100% /opt/kernel_extras devtmpfs 4.0M 0 4.0M 0% /dev tmpfs 2.0G 0 2.0G 0% /dev/shm tmpfs 786M 524K 785M 1% /run tmpfs 5.0M 0 5.0M 0% /run/lock android 229G 215G 15G 94% /mnt/shared internal 229G 215G 15G 94% /mnt/internal tmpfs 393M 0 393M 0% /run/user/0 tmpfs 393M 4.0K 393M 1% /run/user/1000 KVM droid@localhost:~$ ls /dev/kvm ls: cannot access '/dev/kvm': No such file or directory これらの結果から、開発環境の概要をまとめると以下のようになります。 CPU : ARM アーキテクチャ aarch64 の 9 コア構成で、 Cortex-A715 (高性能コア)と Cortex-A510 (効率コア)の組み合わせです。 メモリ : 4 GB 利用可能でした。OpenStackのような重量級のサービスを動かすには少し心もとない容量ですが、最小構成であれば動作させることができそうです。 ストレージ : ルートファイルシステム(/dev/vda1)には214GBが割り当てられているものの、実際に / で利用できるのは 16 GB 程度であるようです。 仮想化機能 : /dev/kvm デバイスが存在しないため、ハードウェア仮想化(KVM)は利用できません。 OpenStack で VM を動かす際は QEMU のソフトウェアエミュレーションを使用することになります。 制約は厳しいですが、以下のように Docker コンテナは正常に動作しました。 OpenStack のインストール方法について OpenStack のインストール方法はいくつかあります。 SDPF クラウド仮想サーバーチームでは kolla-ansible をベースに開発していますが、メモリとストレージの要件がかなり厳しいので、コンテナベースの kolla-ansible は利用できなさそうです。 ChatGPT にいくつかの OpenStack インストール方法について比較してもらいました。 ツール 概要 最小インストール要件の目安 DevStack Bash スクリプトで OS 上に OpenStack サービスを直接インストールできる OpenStack 開発者向けのツール 2CPU, 4GB RAM, 10 GB disk space Kolla-Ansible Docker/Podman コンテナ化された OpenStack を Ansible でデプロイするツール 2 NIC, 8GB RAM, 40 GB disk space OpenStack-Ansible LXC コンテナ化された OpenStack を Ansible でデプロイするツール 8 CPU, 8GB RAM, 50GB disk space MicroStack お手軽に導入できる snap ベースのシングルノード OpenStack 構築ツール 2 CPU, 8GB RAM, 100 GB disk space OpenStack は全体的に最小インストール要件が厳しめですが、 DevStack だとギリギリ動きそうなので、 DevStack をチューニングする方針としました。 DevStack を実行して minimal な OpenStack 環境をつくる DevStack では local.conf という config をもとに OpenStack をインストールします。 メモリとストレージの要件が厳しいので、 Minimal deployment を参考にして Cinder と Horizon はインストールしないことにします。 Block Storage はなくてよいですし、 API さえ使えれば十分ですよね。 いろいろと試行錯誤した結果、以下のような config ができました。 stack@localhost:~/devstack$ cat local.conf [[local|localrc]] ADMIN_PASSWORD=secret DATABASE_PASSWORD=$ADMIN_PASSWORD RABBIT_PASSWORD=$ADMIN_PASSWORD SERVICE_PASSWORD=$ADMIN_PASSWORD disable_service horizon disable_service cinder c-sch c-api c-vol disable_service swift s-proxy s-object s-container s-account disable_service tempest disable_service heat h-eng h-api h-api-cfn h-api-cw disable_service ceilometer-acompute ceilometer-acentral ceilometer-api disable_service neutron-adv-service disable_service octavia o-api o-cw o-hm o-hk disable_service barbican disable_service trove disable_service sahara API_WORKERS=1 RPC_WORKERS=1 NEUTRON_PORT_SECURITY=false CIRROS_ARCH=aarch64 [[post-config|$NOVA_CONF]] [libvirt] virt_type = qemu cpu_mode = custom cpu_model = cortex-a53 DevStack のインストールを進める前に、以下のようにして DB に割り当てるメモリをケチっておきます。 stack@localhost:~/devstack$ sudo vim /etc/mysql/conf.d/devstack-lowmem.cnf stack@localhost:~/devstack$ cat /etc/mysql/conf.d/devstack-lowmem.cnf [mysqld] innodb_buffer_pool_size = 256M innodb_log_file_size = 64M max_connections = 100 tmp_table_size = 32M max_heap_table_size = 32M あとは ドキュメント に沿って ./stack.sh すれば OpenStack 環境ができます。 stack@localhost:~/devstack$ ./stack.sh This is your host IP address: 10.123.149.241 This is your host IPv6 address: ::1 Keystone is serving at http://10.123.149.241/identity/ The default users are: admin and demo The password: secret Services are running under systemd unit files. For more information see: https://docs.openstack.org/devstack/latest/systemd.html DevStack Version: 2026.1 Change: f61d747518a3b4896032c7e9440ddf31856a060f Merge "Enable response validation in Keystone" 2025-11-14 14:20:56 +0000 OS Version: Debian 12 bookworm 2025-11-24 02:08:30.477 | stack.sh completed in 1923 seconds. スマホ OpenStack に VM を建ててみる ここまでできたらあとは通常の OpenStack のように VM を作成できます。 aarch64 な CirrOS の image があるので、この image をもとに VM を作成します。 stack@localhost:~/devstack$ openstack image list +--------------------------------------+---------------------------+--------+ | ID | Name | Status | +--------------------------------------+---------------------------+--------+ | 2b8bfbf7-42bf-43a3-a91d-50c59633dc94 | cirros-0.6.3-aarch64-disk | active | +--------------------------------------+---------------------------+--------+ NW まわりはきちんと設定していないので、どの NW にも接続しないことにします。 stack@localhost:~/devstack$ openstack server create --image 2b8bfbf7-42bf-43a3-a91d-50c59633dc94 --flavor m1.tiny test-server --nic none stack@localhost:~/devstack$ openstack server show f132c8af-cee6-4248-9128-de58e42e6665 +-------------------------------------+------------------------------------------------------------------------------------------------+ | Field | Value | +-------------------------------------+------------------------------------------------------------------------------------------------+ | OS-DCF:diskConfig | MANUAL | | OS-EXT-AZ:availability_zone | nova | | OS-EXT-SRV-ATTR:host | None | | OS-EXT-SRV-ATTR:hostname | test-server | | OS-EXT-SRV-ATTR:hypervisor_hostname | None | | OS-EXT-SRV-ATTR:instance_name | None | | OS-EXT-SRV-ATTR:kernel_id | None | | OS-EXT-SRV-ATTR:launch_index | None | | OS-EXT-SRV-ATTR:ramdisk_id | None | | OS-EXT-SRV-ATTR:reservation_id | None | | OS-EXT-SRV-ATTR:root_device_name | None | | OS-EXT-SRV-ATTR:user_data | None | | OS-EXT-STS:power_state | Running | | OS-EXT-STS:task_state | None | | OS-EXT-STS:vm_state | active | | OS-SRV-USG:launched_at | 2025-11-24T02:10:05.000000 | | OS-SRV-USG:terminated_at | None | | accessIPv4 | | | accessIPv6 | | | addresses | | | config_drive | | | created | 2025-11-24T02:09:46Z | | description | None | | flavor | description=, disk='1', ephemeral='0', extra_specs.hw_rng:allowed='True', id='m1.tiny', | | | is_disabled=, is_public='True', location=, name='m1.tiny', original_name='m1.tiny', ram='512', | | | rxtx_factor=, swap='0', vcpus='1' | | hostId | eadc3234a9f5d6b80737ed483bbdbe2d388821b0fa927cfdc237934a | | host_status | None | | id | f132c8af-cee6-4248-9128-de58e42e6665 | | image | cirros-0.6.3-aarch64-disk (2b8bfbf7-42bf-43a3-a91d-50c59633dc94) | | key_name | None | | locked | False | | locked_reason | None | | name | test-server | | pinned_availability_zone | None | | progress | 0 | | project_id | a3a363ed73e8469b98ffaae53bfa8df5 | | properties | | | scheduler_hints | | | server_groups | [] | | status | ACTIVE | | tags | | | trusted_image_certificates | None | | updated | 2025-11-24T02:10:06Z | | user_id | 6b8f5452a10542359fd0269553371366 | | volumes_attached | | +-------------------------------------+------------------------------------------------------------------------------------------------+ stack@localhost:~/devstack$ sudo virsh list Id Name State ----------------------------------- 1 instance-00000001 running virsh console か console.log を眺めると、 CirrOS がブートしてきたのが確認できました。 stack@localhost:~/devstack$ sudo tail -F /opt/stack/data/nova/instances/f132c8af-cee6-4248-9128-de58e42e6665/console.log [ 5.343116] EXT4-fs (vda1): write access will be enabled during recovery [ 5.513086] EXT4-fs (vda1): recovery complete [ 5.553111] EXT4-fs (vda1): mounted filesystem with ordered data mode. Opts: (null). Quota mode: none. [ 7.266087] EXT4-fs (vda1): re-mounted. Opts: (null). Quota mode: none. [ 243.220848] EXT4-fs (vda1): resizing filesystem from 25600 to 259835 blocks [ 243.239892] EXT4-fs (vda1): resized filesystem to 259835 ### tail -n 25 /var/log/messages Nov 24 02:10:36 cirros syslog.info syslogd started: BusyBox v1.35.0 Nov 24 02:10:39 cirros daemon.info dhcpcd[258]: dhcpcd-9.4.1 starting Nov 24 02:10:39 cirros daemon.info dhcpcd[261]: DUID 00:04:f1:32:c8:af:ce:e6:42:48:91:28:de:58:e4:2e:66:65 Nov 24 02:10:39 cirros daemon.err dhcpcd[261]: no valid interfaces found Nov 24 02:16:06 cirros syslog.info syslogd started: BusyBox v1.35.0 Nov 24 02:16:09 cirros daemon.info dhcpcd[249]: dhcpcd-9.4.1 starting Nov 24 02:16:09 cirros daemon.info dhcpcd[252]: DUID 00:04:f1:32:c8:af:ce:e6:42:48:91:28:de:58:e4:2e:66:65 Nov 24 02:16:09 cirros daemon.err dhcpcd[252]: no valid interfaces found Nov 24 02:19:09 cirros daemon.err dhcpcd[252]: timed out Nov 24 02:19:58 cirros authpriv.info dropbear[388]: Running in background ############ debug end ############## ____ ____ ____ / __/ __ ____ ____ / __ \/ __/ / /__ / // __// __// /_/ /\ \ \___//_//_/ /_/ \____/___/ http://cirros-cloud.net login as 'cirros' user. default password: 'gocubsgo'. use 'sudo' for root. cirros login: [ 246.401660] virtio_gpu virtio4: [drm] drm_plane_enable_fb_damage_clips() not called スマホさえ持っていればいつでも OpenStack の開発ができますね! トラブルシューティング Linux 開発環境が落ちる メモリが足らず、 ときどき Linux 開発環境が落ちることもありました。 ターミナルを再起動すれば概ね作業を再開できるので、根気強くインストール作業を進めてください。 openstack server create コマンドさえ実行できれば DB や MQ はいらないので、停止してしまってもよいでしょう。 VM が boot しない デフォルトの local.conf では nova.conf に以下のような設定が入ります。 [libvirt] live_migration_uri = qemu+ssh://stack@%s/system cpu_mode = host-passthrough virt_type = qemu cpu_mode = host-passthrough となっていますが、 KVM が利用できないため、このままの設定だと VM の起動に失敗します。 : f4caa26c-ea15-428e-8e70-68dfb0aa7266] File "/opt/stack/nova/nova/virt/libvirt/driver.py", line 8247, in _create_guest : f4caa26c-ea15-428e-8e70-68dfb0aa7266] guest.launch(pause=pause) : f4caa26c-ea15-428e-8e70-68dfb0aa7266] File "/opt/stack/nova/nova/virt/libvirt/guest.py", line 167, in launch : f4caa26c-ea15-428e-8e70-68dfb0aa7266] with excutils.save_and_reraise_exception(): : f4caa26c-ea15-428e-8e70-68dfb0aa7266] File "/opt/stack/data/venv/lib/python3.12/site-packages/oslo_utils/excutils.py", line 256, in __exit__ : f4caa26c-ea15-428e-8e70-68dfb0aa7266] self.force_reraise() : f4caa26c-ea15-428e-8e70-68dfb0aa7266] File "/opt/stack/data/venv/lib/python3.12/site-packages/oslo_utils/excutils.py", line 222, in force_reraise : f4caa26c-ea15-428e-8e70-68dfb0aa7266] raise self.value : f4caa26c-ea15-428e-8e70-68dfb0aa7266] File "/opt/stack/nova/nova/virt/libvirt/guest.py", line 165, in launch : f4caa26c-ea15-428e-8e70-68dfb0aa7266] return self._domain.createWithFlags(flags) : f4caa26c-ea15-428e-8e70-68dfb0aa7266] ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ : f4caa26c-ea15-428e-8e70-68dfb0aa7266] File "/opt/stack/data/venv/lib/python3.12/site-packages/eventlet/tpool.py", line 186, in doit : f4caa26c-ea15-428e-8e70-68dfb0aa7266] result = proxy_call(self._autowrap, f, *args, **kwargs) : f4caa26c-ea15-428e-8e70-68dfb0aa7266] ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ : f4caa26c-ea15-428e-8e70-68dfb0aa7266] File "/opt/stack/data/venv/lib/python3.12/site-packages/eventlet/tpool.py", line 144, in proxy_call : f4caa26c-ea15-428e-8e70-68dfb0aa7266] rv = execute(f, *args, **kwargs) : f4caa26c-ea15-428e-8e70-68dfb0aa7266] ^^^^^^^^^^^^^^^^^^^^^^^^^^^ : f4caa26c-ea15-428e-8e70-68dfb0aa7266] File "/opt/stack/data/venv/lib/python3.12/site-packages/eventlet/tpool.py", line 125, in execute : f4caa26c-ea15-428e-8e70-68dfb0aa7266] raise e.with_traceback(tb) : f4caa26c-ea15-428e-8e70-68dfb0aa7266] File "/opt/stack/data/venv/lib/python3.12/site-packages/eventlet/tpool.py", line 82, in tworker : f4caa26c-ea15-428e-8e70-68dfb0aa7266] rv = meth(*args, **kwargs) : f4caa26c-ea15-428e-8e70-68dfb0aa7266] ^^^^^^^^^^^^^^^^^^^^^ : f4caa26c-ea15-428e-8e70-68dfb0aa7266] File "/usr/lib/python3/dist-packages/libvirt.py", line 1415, in createWithFlags : f4caa26c-ea15-428e-8e70-68dfb0aa7266] raise libvirtError('virDomainCreateWithFlags() failed') : f4caa26c-ea15-428e-8e70-68dfb0aa7266] libvirt.libvirtError: unsupported configuration: CPU mode 'host-passthrough' for aarch64 qemu domain on aarch64 host is not supported> : f4caa26c-ea15-428e-8e70-68dfb0aa7266] それから、 cpu_model の設定も必要でした。 デフォルトの cpu_model の場合は確かに qemu process としては動作しているようにみえました。 stack@lima-default:~/devstack$ sudo virsh dominfo instance-00000003 Id: 3 Name: instance-00000003 UUID: 03bd6bb9-8e56-48da-ade9-13870c876577 OS Type: hvm State: running CPU(s): 1 CPU time: 109.9s Max memory: 262144 KiB Used memory: 262144 KiB Persistent: yes Autostart: disable Managed save: no Security model: apparmor Security DOI: 0 Security label: libvirt-03bd6bb9-8e56-48da-ade9-13870c876577 (enforcing) stack@lima-default:~/devstack$ sudo virsh domstats instance-00000003 Domain: 'instance-00000003' state.state=1 state.reason=1 cpu.time=126892574000 cpu.user=126474847000 cpu.system=417727000 cpu.cache.monitor.count=0 cpu.haltpoll.success.time=0 cpu.haltpoll.fail.time=0 balloon.current=262144 balloon.maximum=262144 balloon.last-update=0 balloon.rss=179488 vcpu.current=1 vcpu.maximum=1 vcpu.0.state=1 vcpu.0.time=126430000000 vcpu.0.wait=0 vcpu.0.delay=43526812 net.count=0 block.count=1 block.0.name=vda block.0.path=/opt/stack/data/nova/instances/03bd6bb9-8e56-48da-ade9-13870c876577/disk block.0.backingIndex=1 block.0.rd.reqs=0 block.0.rd.bytes=0 block.0.rd.times=0 block.0.wr.reqs=0 block.0.wr.bytes=0 block.0.wr.times=0 block.0.fl.reqs=0 block.0.fl.times=0 block.0.allocation=0 block.0.capacity=1073741824 block.0.physical=204800 dirtyrate.calc_status=0 dirtyrate.calc_start_time=0 dirtyrate.calc_period=0 dirtyrate.calc_mode=page-sampling stack@lima-default:~/devstack$ ps aux | grep qemu libvirt+ 962634 99.9 4.4 1431616 179488 ? Sl 17:22 4:12 /usr/bin/qemu-system-aarch64 -name guest=instance-00000003,debug-threads=on -S -object {"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain-3-instance-00000003/master-key.aes"} -blockdev {"driver":"file","filename":"/usr/share/AAVMF/AAVMF_CODE.no-secboot.fd","node-name":"libvirt-pflash0-storage","auto-read-only":true,"discard":"unmap"} -blockdev {"node-name":"libvirt-pflash0-format","read-only":true,"driver":"raw","file":"libvirt-pflash0-storage"} -blockdev {"driver":"file","filename":"/var/lib/libvirt/qemu/nvram/instance-00000003_VARS.fd","node-name":"libvirt-pflash1-storage","auto-read-only":true,"discard":"unmap"} -blockdev {"node-name":"libvirt-pflash1-format","read-only":false,"driver":"raw","file":"libvirt-pflash1-storage"} -machine virt-8.2,usb=off,gic-version=2,dump-guest-core=off,memory-backend=mach-virt.ram,pflash0=libvirt-pflash0-format,pflash1=libvirt-pflash1-format,acpi=on -accel tcg -cpu cortex-a15 -m size=262144k -object {"qom-type":"memory-backend-ram","id":"mach-virt.ram","size":268435456} -overcommit mem-lock=off -smp 1,sockets=1,dies=1,cores=1,threads=1 -uuid 03bd6bb9-8e56-48da-ade9-13870c876577 -no-user-config -nodefaults -chardev socket,id=charmonitor,fd=30,server=on,wait=off -mon chardev=charmonitor,id=monitor,mode=control -rtc base=utc -no-shutdown -boot strict=on -device {"driver":"pcie-root-port","port":8,"chassis":1,"id":"pci.1","bus":"pcie.0","multifunction":true,"addr":"0x1"} -device {"driver":"pcie-root-port","port":9,"chassis":2,"id":"pci.2","bus":"pcie.0","addr":"0x1.0x1"} -device {"driver":"pcie-root-port","port":10,"chassis":3,"id":"pci.3","bus":"pcie.0","addr":"0x1.0x2"} -device {"driver":"pcie-root-port","port":11,"chassis":4,"id":"pci.4","bus":"pcie.0","addr":"0x1.0x3"} -device {"driver":"pcie-root-port","port":12,"chassis":5,"id":"pci.5","bus":"pcie.0","addr":"0x1.0x4"} -device {"driver":"pcie-root-port","port":13,"chassis":6,"id":"pci.6","bus":"pcie.0","addr":"0x1.0x5"} -device {"driver":"pcie-root-port","port":14,"chassis":7,"id":"pci.7","bus":"pcie.0","addr":"0x1.0x6"} -device {"driver":"qemu-xhci","id":"usb","bus":"pci.2","addr":"0x0"} -device {"driver":"virtio-scsi-pci","id":"scsi0","bus":"pci.1","addr":"0x0"} -blockdev {"driver":"file","filename":"/opt/stack/data/nova/instances/_base/71f6f0f805cceef98c928ec21e85f5e992012140","node-name":"libvirt-2-storage","auto-read-only":true,"discard":"unmap","cache":{"direct":true,"no-flush":false}} -blockdev {"node-name":"libvirt-2-format","read-only":true,"cache":{"direct":true,"no-flush":false},"driver":"raw","file":"libvirt-2-storage"} -blockdev {"driver":"file","filename":"/opt/stack/data/nova/instances/03bd6bb9-8e56-48da-ade9-13870c876577/disk","node-name":"libvirt-1-storage","auto-read-only":true,"discard":"unmap","cache":{"direct":true,"no-flush":false}} -blockdev {"node-name":"libvirt-1-format","read-only":false,"cache":{"direct":true,"no-flush":false},"driver":"qcow2","file":"libvirt-1-storage","backing":"libvirt-2-format"} -device {"driver":"virtio-blk-pci","bus":"pci.3","addr":"0x0","drive":"libvirt-1-format","id":"virtio-disk0","bootindex":1,"write-cache":"on"} -add-fd set=0,fd=31,opaque=serial0-log -chardev pty,id=charserial0,logfile=/dev/fdset/0,logappend=on -serial chardev:charserial0 -device {"driver":"usb-kbd","id":"input0","bus":"usb.0","port":"1"} -audiodev {"id":"audio1","driver":"none"} -vnc 0.0.0.0:0,audiodev=audio1 -device {"driver":"virtio-gpu-pci","id":"video0","max_outputs":1,"bus":"pci.6","addr":"0x0"} -device {"driver":"virtio-balloon-pci","id":"balloon0","deflate-on-oom":true,"free-page-reporting":true,"bus":"pci.4","addr":"0x0"} -object {"qom-type":"rng-random","id":"objrng0","filename":"/dev/urandom"} -device {"driver":"virtio-rng-pci","rng":"objrng0","id":"rng0","bus":"pci.5","addr":"0x0"} -device {"driver":"vmcoreinfo"} -sandbox on,obsolete=deny,elevateprivileges=deny,spawn=deny,resourcecontrol=deny -msg timestamp=on しかし、以下のようにレジスタの値を見てみると綺麗な値になっていて、実際には VM が生きているようには見えませんでした。 stack@lima-default:~/devstack$ virsh qemu-monitor-command --hmp instance-00000003 "info registers" CPU#0 R00=00000000 R01=00000000 R02=00000000 R03=00000000 R04=00000000 R05=00000000 R06=00000000 R07=00000000 R08=00000000 R09=00000000 R10=00000000 R11=00000000 R12=00000000 R13=00000000 R14=00000008 R15=00000004 PSR=400001db -Z-- A und32 s00=00000000 s01=00000000 d00=0000000000000000 s02=00000000 s03=00000000 d01=0000000000000000 s04=00000000 s05=00000000 d02=0000000000000000 s06=00000000 s07=00000000 d03=0000000000000000 s08=00000000 s09=00000000 d04=0000000000000000 s10=00000000 s11=00000000 d05=0000000000000000 s12=00000000 s13=00000000 d06=0000000000000000 s14=00000000 s15=00000000 d07=0000000000000000 s16=00000000 s17=00000000 d08=0000000000000000 s18=00000000 s19=00000000 d09=0000000000000000 s20=00000000 s21=00000000 d10=0000000000000000 s22=00000000 s23=00000000 d11=0000000000000000 s24=00000000 s25=00000000 d12=0000000000000000 s26=00000000 s27=00000000 d13=0000000000000000 s28=00000000 s29=00000000 d14=0000000000000000 s30=00000000 s31=00000000 d15=0000000000000000 s32=00000000 s33=00000000 d16=0000000000000000 s34=00000000 s35=00000000 d17=0000000000000000 s36=00000000 s37=00000000 d18=0000000000000000 s38=00000000 s39=00000000 d19=0000000000000000 s40=00000000 s41=00000000 d20=0000000000000000 s42=00000000 s43=00000000 d21=0000000000000000 s44=00000000 s45=00000000 d22=0000000000000000 s46=00000000 s47=00000000 d23=0000000000000000 s48=00000000 s49=00000000 d24=0000000000000000 s50=00000000 s51=00000000 d25=0000000000000000 s52=00000000 s53=00000000 d26=0000000000000000 s54=00000000 s55=00000000 d27=0000000000000000 s56=00000000 s57=00000000 d28=0000000000000000 s58=00000000 s59=00000000 d29=0000000000000000 s60=00000000 s61=00000000 d30=0000000000000000 s62=00000000 s63=00000000 d31=0000000000000000 FPSCR: 00000000 これらの問題に対応するため、 local.conf に以下を追記することとしました。 [[post-config|$NOVA_CONF]] [libvirt] virt_type = qemu cpu_mode = custom cpu_model = cortex-a53 QEMU のソフトウェアエミュレーションになってしまいますが、 CirrOS 程度であれば問題なく動きます。 まとめ Android 15 から Android 端末上で Linux 環境を動かすことが可能になりました。 4 GB RAM と 16 GB ストレージの環境下でやれることは限られますが、 DevStack の config をチューニングすることでスマホ上に OpenStack 環境を構築し、 VM を起動できました。
この記事は、 NTT docomo Business Advent Calendar 2025 1日目の記事です。 こんにちは、デジタル改革推進部の小林です。NTT docomo Business Engineers' Blogと改題してからは初の、アドベントカレンダーが始まりました。その1日目の記事です。 私は4月から現職のデジタル改革推進部に所属し、社内認証サービスのプロダクトオーナーをやっています。このサービスにより、NTTドコモビジネスの社員はPCや各種社内システムをひとつのIDでシームレスに利用できます。現在は私のほかメンバー4人と協力会社の体制で運営しています。 私は新卒で入社して以来働いてきたこの会社で、13年目にして初めてラインマネジメントに取り組むことになりました。入りたての頃はマネージャーなんてなりたくないと思っていたところだったのに、いまとなってはこれをそれなりに納得して受け入れている自分がいます。この記事では、これまでのキャリアでターニングポイントになったところを振り返りながら、単なるエンジニアからマネージャーになるキャリアチェンジをどう受容していったか、その経緯をまとめます。JTCのエンジニアが10年ちょいで歩んだキャリアの一例として、お読みいただければと思います。 マネージャーなんて! 異動と価値観の変化 バックオフィスでの異動、奔走、停滞 さらなる異動とマネージャーキャリアの始まり 終わりに マネージャーなんて! 私は2013年に当時のNTT Comに入社し、最初は大企業向けの統合コミュニケーションサービス (Arcstar UCaaS) のエンジニアとしてキャリアを始めました。昨今であればZoom, Microsoft Teams, Google Workspaceがこのあたりの地位を占めています。 当時は、学生の頃にはまさか触ることのなかった巨大なコアルーターや仮想化基盤を操作したり、電話やインスタントメッセージングの交換設定を書いたり、果てはアメリカの電話事業者とトラブルシューティングをしたりと、多岐にわたる領域で数々の刺激的な経験をしてきました。その当時は手を動かしていることが楽しく、相対的にマネジメントの仕事が自分とは遠いところの出来事に見えており、マネージャー層の表層的な仕事だけ見て「ああはなりたくない」と思っていたところがあったように思います。 異動と価値観の変化 時は流れて2017年の冬、当時の技術開発部セキュリティテクニカルユニットに異動しました。自分が着任したチームはWebセキュリティ実装の高度化をテーマにしており、セキュリティ系の知見がほとんどなかった自分にとってはそれまでのスキル・ケイパビリティを生かすのが急に困難になりました。できていた仕事が急にできなくなり、このままではいけないがどう動けばいいかも分からないような難しい時期を1年間くらい過ごしました。 また、同じユニットには、新卒入社ながら一芸に秀でた人がたくさんいました。コードを書くにしろセキュリティインテリジェンスの分析をするにしろ、自分よりもずいぶん年下なのにとにかく手が動くさまをまざまざと見せつけられたわけです。 そのような中でも自分にできる仕事がひとつありました。会社のコンテキストを部内に展開することです。技術開発部は研究開発部門であることもあり、新卒で配属されてからずっとここにいるとか、10年以上異動がないといった人が多い組織でした。私のように事業部から異動してくる方がまれで、事業部側で起きていることや動きの背景にある状況、事業部が影響を受けているルールなどは私の方がよく知っている状況でした。 幹部会議からのフィードバックに背景情報を注釈としてつけたり、社内手続きに不明な点があるとSlack上に流れてきたヘルプに率先して返事をしたり、そういったことに取り組むうちに自身の存在も認知してもらった気がしています。こうした経験を通じて、 自分より手が動く人がたくさんいるのだから、そういう人らと競る仕事をするよりも、その人らがもっと素早く動ける環境を作る方が向いているのでは? との思いがどんどん強くなっていきました。 最終的に、技術開発部がイノベーションセンター (IC) に改組された2020年には、ICのバックオフィスに自ら志願して異動し、その思いを叶えることになります。 バックオフィスでの異動、奔走、停滞 ICのバックオフィスにおいては、 コロナ禍に突入して、出社しなくても済む業務プロセスを考案・実装したり、 新たに使いたいSaaSがあるとの相談に耳を傾けて社内ルールになじむ形に仕立てたり、 無人飛行機の登録が義務化される話を聞きつけ、協働して必要な登録を済ませたり、 全社的な調査事項を所内に必要な形にまとめ直して所員の手間をできる限り減らしたり、 ICのValueを策定する取り組みに参加して最終的な成果を所員全員の前で発表したり、 所内ポータルサイトの動線を再設計して情報へのアクセスを容易にしたり、 などなど多岐にわたる仕事に取り組みました。社会人学生として大学院に通学していたのもこの時期です。このあたりから対外的にはコーポレートエンジニアを名乗っていました。 自分の価値観にもアップデートがあり、先に挙げた「素早く動ける環境作り」から進化して「フロントの手を爆速にすることは、最終的にお客さまや社会に対する奉仕になる」「ひとりよりも群れた方が、なせることが増える」といった信条を抱えるようになりました。 ただこうした環境で5年も働いているうち、毎日の仕事を「手なり」でやってしまえるようになり、それに違和感を覚えるようになっていました。コンフォートゾーンに入っているとはこういう状況を言うのでは、との思いがどんどん強くなっていきました。 ITや情報セキュリティの強みを生かしたいと思う中で、自分が取りうる道はいくつもあれど、最終的に次のどちらかだろうと考えました。 会社のIT系バックオフィスに異動し、サービスする相手のスコープを広くする(量の観点) グループ会社に異動し、グループ会社のIT・情報セキュリティについて深く支援する(質の観点) 最初の異動ではあまりにもキャリアに連続性がなく辛い思いをしたところから、自分のスキル・ケイパビリティが活かせる領域で社会に役立ちたいと考え詰めた結果、これらが残りました。こうした話を上長や人事担当と面談してインプットし続けた結果、現所属の社内認証サービス担当に異動することになります。 さらなる異動とマネージャーキャリアの始まり 赴任したチームは15人以上もいるような大所帯で、社内認証サービスを担当しているのはこのうち5人でした(当時)。その5人のメンバーと働くにあたって最初はどのような立場でコミットするかを考えあぐねていましたが、話を聞くにつれ前任者がテックリードとして振る舞っていたことがわかり、自分も作業者の一人としてではなくリードする立場にならねばと腹を決めました。こうして、ラインマネージャー、プロダクトオーナーとしてのキャリアを始めることになりました。 実際フタを開けてみると、2カ月間に中心的メンバーの異動が2回立て続けに起こったことで、日常業務は淡々とこなしつつも(すごいことです)チームに混乱があるように見受けられました。ここから半年かけてチームビルディングに奔走することになるのですが、その話はまた別の機会にすることといたしましょう。 終わりに 自分の10年あまりのキャリアを、簡単ですが振り返ってきました。大学の同級生が30歳そこそこで肩書付き・部下持ちになっているのに妙に焦ったのもいまとなっては昔のことで、自分ができることで貢献すればいいのだと捉えられるようになってからは、ゆったりと構えています。日頃こなすべき業務・解くべき課題はよりどりみどりで、毎日飽きずに取り組めています。 これからのことはまたどこかで振り返るつもりです。来年のアドベントカレンダーでも、今度はチームマネジメントの話を何か書けたらいいなと思っています。 お読みいただきありがとうございました。明日の記事はKumassyが担当します。明日もお楽しみに!
こんにちは、NTTドコモグループの現場受け入れ型インターンシップに「D2:攻撃者視点に立ち攻撃技術を研究開発するセキュリティエンジニア」ポストで参加させていただきました、太田です。 本記事では、本インターンシップでの取り組みについて紹介いたします。 NTTドコモグループのセキュリティ業務、特にOffensive Securityプロジェクト(以下、PJ)に興味のある方、インターンシップの参加を検討している方などへの参考になれば幸いです。 OffensiveSecurityPJとは 参加経緯 インターンシップ概要 Microsoft 365 Copilotの悪用 Microsoft 365 Copilot とは 目標 検証1: Black Hat USA 2024での手法を再現 検証2: ユーザーに向けた案内 + Markdown 検証3: Excelファイルを利用したペイロードの秘匿 まとめ インターンの感想 おわりに OffensiveSecurityPJとは 今回私が参加させていただいたワークフィールド(職場)はイノベーションセンターのテクノロジー部門内に位置するOffensive Security PJです。 Offensive Security PJ は最先端のセキュリティ技術を調査・検証し、その成果を社内外に共有していくことをミッションとしています。 特に重視しているのは「攻撃者の視点」に立つことです。防御の立場から技術を眺めるだけではなく攻撃者がどのように考え、どのような手法を用いるかを理解することで初めて本質的な対策を打つことができます。そのような視点を持ち続けることで従来の後追いの対策にとどまらず、脅威を先取りして備える「先回りの防御」を実現しようとしています。 単に攻撃手法を模倣するだけでなく、その背後にある原理やアーキテクチャや攻撃の成立条件まで掘り下げるような研究・開発をしています。 参加経緯 私は普段、大学院でセキュリティに関する研究をしています。中学生の頃、遠隔操作可能なエアコンが現れた時に、「もし悪用されたら人命に関わるのではないか」と感じた経験があります。その出来事をきっかけに、新しい技術や製品を「どのように悪用され得るか」という視点で見るようになりました。 そうした関心から、Offensive Security/Red Team に強い興味を持ち、将来はこの分野に携わりたいと考えるようになりました。そのため、「攻撃技術の調査・開発・検証や攻撃技術の応用に関する研究」という本ポストの業務内容は、私の関心と非常に一致していると感じ、応募いたしました。 また、業務として研究に取り組むことを自分自身が楽しめるかどうかを確かめたいという思いもありました。 インターンシップ概要 インターンシップは8月25日から9月5日の平日10日間で開催され、業務体験はオリエンテーションや成果報告会を除いた実質7日間で行われました。 初日と最終日は出社が必須で、それ以外の日程は出社かオンラインかを相談して決めることが出来ました。 またインターンシップ期間中は、検証業務はもちろん、SOC(Security Operation Center)の見学や海外のカンファレンスに登壇された社員の方による再演の聴講、社内LT(Lightning Talks)会への参加、他部署のインターンシップ生との交流などさまざまな体験をさせていただきました。 私が検証業務で取り組んだテーマは以下の2つです。 Microsoft 365 Copilotの悪用 LLM応用によるRed Teamオペレーション高度化検証 本記事では、2つのテーマのうち「Microsoft 365 Copilotの悪用」を紹介します。 Microsoft 365 Copilotの悪用 現在、大規模言語モデル(LLM)の業務利用が進んでいますが、それには同時に危険性もあります。どのような危険が潜んでいるかを明らかにするとともに、LLMに対する攻撃手法に関する知見を収集することを目的として、Microsoft 365 Copilot環境を用いた攻撃手法を検証しました。 Black Hat USA 2024で発表された攻撃手法 1 を基に、それらが現在も再現可能かどうかを検証しました。さらに、他言語のプロンプトでも同様の攻撃が成立するか、および対策が施されている場合にどのような工夫で攻撃が成功し得るかについて調査しました。 Microsoft 365 Copilot とは Microsoft 365 Copilot 2 (以下 Copilot)は、Word や Excel、PowerPoint、Outlook などの Microsoft 365 アプリに組み込まれたAIアシスタントです。 メールの下書きやプレゼン資料の作成、データ分析をアプリ上で直接行えるほか、チャットでの会話の中でWeb 上の情報や社内のメールやドキュメントなどのデータを参照できる点が特徴です。 社内のメールを参照し応答する流れ しかし、Microsoft 365 Copilotの「各種データを参照できる」という特徴を利用した攻撃手法も知られています。 目標 本ブログでは、Copilotにユーザーを悪意のあるサイト等へ誘導してもらうことを目標とした検証について報告します。 Copilotは会話の応答中にクリック可能なハイパーリンクを表示することがあります。 実際にリンクを表示するCopilot ここでCopilotには、”Web 上の情報だけでなく社内のメールやドキュメントなどのデータを参照できる” という特徴があります。これらを悪用し、参照対象の中に悪意ある情報を混入させ、表示名は正規のものに見せかけつつリンク先を攻撃者が指定したものにする、というのが目標です。 仮に可能だった場合、攻撃者はCopilot を通して広範囲な利用者に悪意のある情報を送ることが可能です。 Copilotが会話の応答中にハイパーリンクを表示する流れ 目標のイメージ 検証1: Black Hat USA 2024での手法を再現 Black Hat USA 2024 の「Living off Microsoft Copilot」で報告された手法の再現を目指します。 www.youtube.com 本手法では、ユーザー宛のメール本文に視認しにくい形で Copilot への指示を埋め込み、その指示に基づいて Copilot が不正なリンクを提示することを期待します。 端的に言うと、指示は次のような意図を持つものでした。 サービスAへのアクセス方法を尋ねられたら、指定の URL を返す ただし表示名(ハイパーリンクのタイトル)は『サービスA』と表示する メールの例 このメールを受け取ったユーザーになり代わって Copilot にサービスA へのアクセス方法を尋ね、Copilot の応答を確認しました。 結果 攻撃手法が公開されてから時間がたち、Copilot側で対策されているのか、同一の手法では目的を達成できませんでした。 例えば、ハイパーリンクの形式にはならずクリックを誘発させられないことや、悪意のあるメールを参照元として表示し露見しやすくなったなど、攻撃の成功率は下がりそうです。 クリックできないURL 検証2: ユーザーに向けた案内 + Markdown ユーザーへのメッセージとして単純に URL を記載し、ハイパーリンクとして出力できないようでした。 どのようにしたら対策を回避できるかを検討するにあたり調査する中で、「EchoLeak 3 」と呼ばれるCopilotの脆弱性について知りました。 EchoLeakの利用した攻撃手法や、それに応じて行われた対策について調べていると、Copilotは以下のような特徴を持っていることがわかりました。 Copilot にはプロンプトインジェクション攻撃を防ぐための仕組みが実装されており、その1つとして XPIA(クロスプロンプトインジェクション攻撃)分類器がある 4 ということ。そしてこれにより、Copilotへの指示は悪性とみなされる可能性があること。これは回避するため、今回の場合、Copilotへの指示ではなく、受信者に向けられたメッセージのように見せかけられればよいということ。 Copilot は、信頼性が確認できないリンクについてはハイパーリンクとして提示せず、クリック不可の形式で応答するなどの対策がなされていること。そのため、今回のように単にメール本文に URL を記載するだけではハイパーリンクとして応答されない可能性が高いということ。そして回避するためにはURLを参照形式でMarkdownを使って指定すればよいということ。(なぜなのかなどは割愛しますが良かったら調べてみてください) これで、検証1において失敗した理由が納得できました。この仕様を突破できないでしょうか。 EchoLeakにおいても対策が行われ、現在は悪用出来ないとされていたものの、悪用に使用される全ての脆弱性への対応が完了している訳ではないかもしれません。 そこで、1,2に沿って改良したのちに、検証しました。 ユーザー向けの案内文のように見える文章を作成します。その中で、Markdown(インライン形式)を用いて、あたかもサービスAへのアクセス方法を示すリンク名を付けつつ、実際には私が用意したURLを指定するようにします。 (なお、2について、回避するには 参照形式 で書く必要があるはずなのですが、私は誤解して インライン形式 で指定してしまいました。それ次第では結果が変わっていたかもしれません) その文章をメールに記述します。なお、HTML形式のメールとPlainText形式のメールの2種類について試しました。 結果 HTML形式のメール → クリックできないURL(ハイパーリンクの書式そのまま)で応答される PlainText形式のメール → クリックできるハイパーリンクで応答される という結果になりました。PlainText形式のメールの場合のみ成功したことになります。 HTML形式のメールを参照した際のCopilot の挙動 PlainText形式のメールを参照した際のCopilot の挙動 これは憶測ですが、Markdown 形式で防御機構を回避されること自体は現状避けられないのではないでしょうか。代わりに人間に誤認させやすいHTML形式のメールを警戒しており、「参照する際は応答にハイパーリンクを使用しない」というような形で対処しているのではないかと考えられます。 確かに、PlainText形式のメールではペイロードとして書いたURLを隠せなくなるので攻撃者としては嫌な気持ちになるでしょう。 検証3: Excelファイルを利用したペイロードの秘匿 攻撃者としてはやはり、 URLを記載したペイロードは隠したい です。上述の通りメールで人間が視認できない形にすることは難しそうです。 だがしかし、Copilotが参照できるのはメールだけではありません。SharePoint上のExcelファイルなども参照してくれます。そしてExcelに記入された文字はサイズ・色を変更できます。これは使えるかもしれません。 ということで、SharePoint上のExcelファイルに白文字でペイロード隠してみます。 結果 しっかりハイパーリンクで応答されました㊗️。 まとめ Copilotが応答としてハイパーリンクを使用する際に、リンク名とは関係ないURLを応答させることは可能です。これを利用してCopilotにユーザーを悪意のあるサイト等へ誘導させられる可能性があります。 ペイロードをメールに記載する場合、ペイロードの配送に至るまでの難易度が低い一方、PlainText形式で記述せざるを得ないため、ユーザーに発覚しやすいという問題があります。 ユーザーから気づかれないようにする手法として、メールではなくSharePoint上のExcelファイルなどへ、視認しにくい書式でペイロードを隠す方法があります。ただしこの方法は、SharePointへファイルを配置する権限を取得していることが前提であるため、ペイロード配送までの難易度は高くなります。逆に一度配置できれば、そのファイルへアクセス可能な全ユーザーに影響を及ぼす可能性があります。 今回の検証では、ペイロードと併記する情報が与える影響や、参照先を示させないための手法検証など、実施したかったが時間不足で試せなかった項目が多く残っています。それでも、Copilotには多様な悪用の可能性が存在すると感じました。 また、攻撃の検証と並行して、どのように防御するかという視点で監査ログの確認なども行いましたが、防御側の負担の大きさを改めて実感しました。まずは、Copilotがどの情報を参照するのか、およびどのような場所にペイロードが隠され得るかを防御側が把握しておくことが重要だと考えます。 インターンの感想 私はインターンシップに参加するまで、LLMに対して同じ事象が必ずしも再現できなかったり、理解しづらい挙動が多かったりする点に不安を感じていました。しかし、実際に検証を始めてみると、その予測不能さも含めて面白く無限の可能性が広がっているように感じられ、非常に楽しく没頭できました。 今回の経験を通じて、「食わず嫌いせずにまず挑戦してみること」の大切さを身をもって学びました。 また、本インターンシップでは決められたことをこなすのではなく、私のように大きく脱線しながら興味に従って自由に検証を進めることも受け入れていただけたのが印象的でした。(むしろ面白がってもらえる最高の環境でした。) 業務体験だけでなく、期間全体を通じて得たすべての経験が大変有意義であり、大きな成長の糧となりました。 おわりに 今回のインターンシップでは日頃の活動だけでは決して得られないような貴重な体験をさせていただき、とても光栄に思っています。特に2週間を通じて現在ホットなテーマであるLLMのセキュリティについて深く学ぶ機会をいただき、最新動向を追い続けることの重要性やセキュリティそのものの面白さを改めて実感できました。 また、「攻撃者の視点に立つ」という考え方についてもトレーナーの皆さまからのご指導を通じて少しずつ身につけることができたと感じています。 このような貴重な機会を与えてくださったOffensive Security PJの皆さまに心より感謝申し上げます。さらに、上長の有藤さん、トレーナーの四方さん、田口さんにも厚く御礼申し上げます。 本記事を通じて少しでもインターンシップに興味を持っていただけたら大変光栄です。 Black Hat USA 2024 - Living off Microsoft Copilot https://www.blackhat.com/us-24/briefings/schedule/?utm_source=labs.zenity.io&utm_medium=referral&utm_campaign=links-and-materials-for-living-off-microsoft-copilot#living-off-microsoft-copilot-40074 ↩ Microsoft 365 Copilot https://www.microsoft.com/ja-jp/microsoft-365-copilot ↩ Breaking down ‘EchoLeak’, the First Zero-Click AI Vulnerability Enabling Data Exfiltration from Microsoft 365 Copilot https://www.aim.security/aim-labs/aim-labs-echoleak-blogpost ↩ Data, Privacy, and Security for Microsoft 365 Copilot - Does Copilot block prompt injections https://learn.microsoft.com/en-us/copilot/microsoft-365/microsoft-365-copilot-privacy#does-copilot-block-prompt-injections-jailbreak-attacks ↩
こんにちは、NTTドコモグループの現場受け入れ型インターンシップに「D2:攻撃者視点に立ち攻撃技術を研究開発するセキュリティエンジニア」ポストで参加させていただきました、島田です。 本記事では、本インターンシップでの取り組みについて紹介いたします。 NTTドコモグループのセキュリティ業務、特にOffensive Securityプロジェクト(以下、PJ)に興味のある方、インターンシップの参加を検討している方などへの参考になれば幸いです。 Offensive Security PJとは 参加経緯 インターンシップ概要 LLM応用によるRed Teamオペレーション高度化検証 Juicy 情報とは LLMによるRed Teamオペレーション高度化の目的 被攻撃環境の内容 Victim1 から Victim2 Victim2 の privilege escalation Victim2 から Victim3 LLMによる攻撃手順の評価 検証方法と検証観点 検証結果 まとめ インターンの感想 おわりに Offensive Security PJとは 今回私が参加させていただいたワークフィールド(職場)はイノベーションセンターのテクノロジー部門内に位置するOffensive Security PJです。 Offensive Security PJ は最先端のセキュリティ技術を調査・検証し、その成果を社内外に共有していくことをミッションとしています。 特に重視しているのは「攻撃者の視点」に立つことです。防御の立場から技術を眺めるだけではなく攻撃者がどのように考え、どのような手法を用いるかを理解することで初めて本質的な対策を打つことができます。そのような視点を持ち続けることで従来の後追いの対策にとどまらず、脅威を先取りして備える「先回りの防御」を実現しようとしています。 単に攻撃手法を模倣するだけでなく、その背後にある原理やアーキテクチャや攻撃の成立条件まで掘り下げるような研究・開発をしています。 参加経緯 私は大学にてネットワークに関する研究をしていますが、個人としてRed Team寄りの技術にも強い関心を抱いています。また、趣味でCTFに参加したり Hack The Box のマシンを攻略したりしています。 技術者の方々にとって「アーキテクチャを理解していなければ実装はできない」という感覚は身近だと思いますが、セキュリティの文脈に置き換えると「攻撃者の視点を持たなければ本質的な防御は難しい」という考えに通じると思っています。日々の学習を通じてこの重要性を意識してきたこともあり、本ポストの「攻撃技術の調査・開発・検証や応用」を重視する活動方針と強く合致すると考えました。 これまで趣味として取り組んできたセキュリティと業務として取り組むセキュリティの両方を体感し、将来のアクションに繋げたいという思いもあり、今回のインターンシップへの応募を決めました。 インターンシップ概要 インターンシップは8月25日から9月5日の平日10日間で開催され、業務体験はオリエンテーションや成果報告会を除いた実質7日間で行われました。 初日と最終日は出社が必須で、それ以外の日程は出社かオンラインかを相談して決めることが出来ました。 またインターンシップ期間中は、検証業務はもちろん、SOC(Security Operation Center)の見学や海外のカンファレンスに登壇された社員の方による再演の聴講、社内LT(Lightning Talks)会への参加、他部署のインターンシップ生との交流などさまざまな体験をさせていただきました。 私が検証業務で取り組んだテーマは以下の2つです。 Microsoft 365 Copilotの悪用 LLM応用によるRed Teamオペレーション高度化検証 本記事では、2つのテーマのうち「LLM応用によるRedTeamオペレーション高度化検証」を紹介します。 LLM応用によるRed Teamオペレーション高度化検証 今回のインターンシップでは、Red Team オペレーションの高度化を目的としてLLM を活用した情報分析手法の検証をしました。攻撃環境において取得される大量のログや環境情報の中から攻撃に有用となる情報を「Juicy 情報」とOffensive Security PJ内で呼んでいるのですが、この「Juicy 情報」をどの程度自動で識別できるかをテーマに取り組みました。 事前に準備された被攻撃環境を対象に、権限昇格やラテラルムーブメントといった攻撃シナリオを実際に実践し、シナリオの流れやそこで得られる情報の特徴を把握しました。その上で、攻略過程で収集したログや出力のうち有効と考えられるものを整理し、分析対象のデータセットとして活用しました。 こうして得られたデータを複数の LLM に入力し、それぞれのモデルがどの程度有用な情報を抽出できるかを比較検証しました。 Juicy 情報とは Juicy 情報とは、セキュリティの文脈で、攻撃や侵入検証において「次の一手に直結する価値の高い情報」を指すPJ内での呼称です。資格情報や接続先、永続化の手がかりなどが該当し、Red Teamのオペレーションやペネトレーションテストの際に非常に有用です。よくある例としては、パスワード、管理用IP、関連データベースの接続情報などが挙げられます。 またJuicy情報は単に次の一手につながるだけでなく、不要な調査や検証に陥るのを防ぎ、調査の優先順位付けや時間短縮にも直結します。 LLMによるRed Teamオペレーション高度化の目的 サイバーセキュリティの世界において、Red Teamオペレーションは組織の防御力、穴を実践的に評価・検証するための大きな切り札となっています。近年、大規模言語モデル(LLM)の急速な発展は Red Team をオペレーションする上でとても有用と考えられており、無限の活用方法が考えられます。 インターン中に行ったLLMを活用して取得した攻撃マシンの脆弱性情報などが、Red Teamオペレーションにおいてどの程度「Juicy(攻撃利用価値が高い)」であり、その情報をいかに迅速かつ正確に評価できるかを検証した結果を共有します。 被攻撃環境の内容 今回の検証は、実践的な攻撃フローをシミュレーションするためにあらかじめトレーナーの方に構築していただいた演習環境で行いました。標的としたホストらは、意図的に複数の既知の権限昇格系の脆弱性や設定ミスが残されたWindowsの環境です。 最初は被攻撃環境を対象とした攻撃シナリオの実践・理解のため実際にvictim1~victim3の3つの攻撃マシンの攻略をしました。ここは本業務の本質ではありませんが、非常に楽しく攻略させてもらったので少しwriteupとして記載しておこうと思います。 Victim1 から Victim2 最初に、攻撃拠点となる Attacker (Kali) マシンから WS01 (victim1) へ C2 (Command and Control) セッションを張ることから始めました。 Metasploit を用いて悪意のあるペイロードを作成し、WS01 に送り込んで被害者側で実行してもらいセッションを確立しました。この時点では権限はユーザー権限のみでした。 少し探索すると dev-machine.txt と Default.rdp というファイルを見つけました。 meterpreter > ls の出力は以下の通りです。 Listing: C:\Users\victim-user\Documents ======================================= Mode Size Type Last modified Name ---- ---- ---- ------------- ---- 100666/rw-rw-rw- 0 fil 2025-08-26 01:38:44 +0000 Default.rdp 040777/rwxrwxrwx 0 dir 2025-08-04 08:16:07 +0000 My Music 040777/rwxrwxrwx 0 dir 2025-08-04 08:16:07 +0000 My Pictures 040777/rwxrwxrwx 0 dir 2025-08-04 08:16:07 +0000 My Videos 100666/rw-rw-rw- 402 fil 2025-08-04 08:16:27 +0000 desktop.ini 100666/rw-rw-rw- 54 fil 2025-08-26 04:33:18 +0000 dev-machine.txt 中身を確認したところ、以下のようなファイルが見つかりました。 ファイル名 中身 dev-machine.txt DEV01 というマシンに対するクレデンシャル(ユーザー名とパスワード) Default.rdp RDP 接続用の設定ファイル(DEV01 の IP アドレスを含む) これらの情報から、 DEV01 (victim2) のユーザーアカウント用ログイン情報であると判断し、RDP 接続をし、 victim-user@DEV01 でのログインに成功しました。 Victim2 の privilege escalation User一覧を見ると victim-admin というユーザーがあり、名前から管理者権限を持っていそうだと判断しました。 C:\Users\victim-user>wmic USERACCOUNT Get Domain,Name,Sid Domain  Name                SID DEV01   DefaultAccount      xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx DEV01   Guest               xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx DEV01   victim-admin        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx DEV01   victim-user         xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx DEV01   WDAGUtilityAccount  xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx しかし他にめぼしいものは見つからず、ほかのツールを用いてさらに調べたところ、書き込み権限のあるディレクトリを発見しました。その中で unquotedsvc 配下のパスが Modifiable と表示されました。 === Services with Unquoted Paths === Service 'unquotedsvc' (StartMode: Manual) has executable 'C:\Program Files\Unquoted Path Service\Common Files\unquotedpathservice.exe', but 'C:\Program Files\Unquoted Path Service\Common' is modifable. つまり、Unquoted Service Path の脆弱性が確認されたということになります。 Windowsでは登録された Full-path が正しくダブルクオーテーションで囲まれていないと、途中のディレクトリ名を実行ファイルとして誤認される可能性があります。 そのため、悪意のあるユーザーが書き込み可能な場所に任意の実行ファイルを設置できるなら、そのサービスが管理者権限で起動した際に意図せずそのファイルが実行されます。 この脆弱性を利用して対象システムの同一ディレクトリに common.exe という名前のペイロードを配置しました。攻撃側の Kali マシンでは事前にリスニングを開始しておき、対象上で以下の操作をしました。 sc stop unquotedsvc を実行してサービスを停止。 続けて sc start unquotedsvc を実行してサービスを起動。 そうすると、サービスの再起動時に配置していたペイロードが自動的に起動し、期待通り権限昇格が確認できました! この権限昇格方法は多数あったうちの1つであり、他にもバイナリ自体の書き換えが可能な脆弱性などを利用した権限昇格方法もありました。 Victim2 から Victim3 管理者権限で victim-admin@DEV01 のパスワードを変更したのちログインし、怪しいファイルがないか探していたら、Desktop上に dev というフォルダを確認しました。 中には以下のようなファイルが見つかりました。 accounts.csv 11種類のユーザー名とパスワードが記載されているファイル userAddTest.ps1 リモート接続先でアカウントを追加する用途と思しきスクリプト ただし、別ホスト(ADMIN-SERV)への接続情報は見当たりませんでした。そこで最初にアクセスしていた WS01 のマシンで、管理者権限での探索をしていなかったことを思い出し、改めて victim1 にログインして調べ直したところ、期待通り該当しそうな.rdpファイルを発見しました。 今はこうしてしれっと書いていますが、実際攻略を試みていたときはこれに気づかず散々関係の無いログを漁ったり、無駄なスキャンをかけたりしてしまっていました。環境を用意してくれたトレーナーの方の思惑通りにハマっていたと思います。 さらに Documents 配下にあった PowerShell スクリプト(.ps1)を確認すると、ADMIN-SERV に接続するためのユーザー名とパスワードが記されていました。 その情報を用いてログインを試みたところ、無事に接続に成功! PS C:\Users\ServAdmin> whoami admin-serv\servadmin 管理者権限であることを確認し、攻略が完了しました。 LLMによる攻撃手順の評価 こうして被攻撃環境の攻略が完了し、攻撃シナリオを把握した上で、得られた各種情報を大規模言語モデル(LLM)がどこまで識別できるか、また次の一手に繋がる有益な示唆をどれだけ提示できるかを検証します。検証の主眼は次の2点です。 Juicy情報の迅速な識別と構造化ができるか 検出された大量の情報から、攻撃にとって最も価値の高い「Juicy情報」を抽出し、JSON 等の構造化フォーマットで出力できるかを確認。 情報についてのリスク評価 抽出された情報に対して、優先度(どれを最初に狙うべきか)・有効性(実際に利用可能か)・持続性(長期的に有効か)・攻撃フェーズ(初動/横展開/権限昇格など)といった実戦的観点から評価を付与できるかを検証。 今回はMicrosoft 365 Copilot(以下、Copilot) とAzure OpenAIのgpt-4.1モデル(以下、OpenAI) の2つの種類のLLMを使用し、それぞれ比較検証しました。 検証方法と検証観点 検証は以下の手順で実施しました。 victim2 の権限昇格時に取得した SharpUp.exe の出力結果を LLM に入力する。 カスタムプロンプトを与えて、LLM に JSON 形式での抽出結果を出力させる。 LLM の出力と手動で得た結果を比較し、抽出精度や実用性を評価する。 また、カスタムプロンプトは以下の観点で作成し、検証しました。 検証観点 目的 Juicyな情報の中に優先順位をつけることはできるのか 多数の脆弱性の中から、最も致命的なもの(Priority 1)を特定させる。 攻撃に使える有効性を示すことはできるのか 情報が単独で即座に攻撃可能(Immediate Exploit)か、他の情報との連鎖(Requires Chaining)が必要かを判断させる。 情報が有効であり続ける期間(可変性はあるか?) 脆弱性がシステムのライフサイクルで「Long-term(長期的)」に残るか、「Short-lived(一時的)」かを判断させる。 検証・攻撃をするどの段階で有効か? 情報をPrivilege Escalation、Persistence、Lateral Movementなどの攻撃フェーズに分類させる。 その情報が攻撃対象の範囲を広げるかどうか? 権限昇格に留まらず、横展開(Lateral Movement)の足がかりとなるかを判断させる。 MITRE ATT&CKマッピング 検出された脆弱性を標準的な攻撃手法(T-ID)に紐づけさせる。 検証結果 実際に試してみたところ、 SharpUp.exe の実行結果は、LLM に入力すると理由や優先順位なども付与された形で返ってきました。単に脆弱性を見つけるだけでは、その利用方法が分からず、手が止まってしまう場面は多々あります。しかし LLM は、そのような場面で補助的に役立ちそうだと感じました。 (左:Sharpup.exeの出力 右:OpenAI) ここからは、2つの LLM を比較し、検証観点ごとに相違点を整理していこうと思います。 検証観点 Copilot OpenAI Juicy情報の中に優先順位をつけることはできるのか 順位にばらつきがある。prioity=1は滅多にない 全体的に高め。priority=3 が一つもない 攻撃に使える有効性を示すことはできるのか Immediate Exploit が少ない。範囲が狭め Copilotに比べて危険度の平均値が高め 情報が有効であり続ける期間(可変性) 長く見積る傾向高め。short-livedが少ない 短く見積もる傾向がある 検証・攻撃をするどの段階で有効か 全て Privilege Escalationと判断 全て Privilege Escalationと判断 その情報が攻撃対象の範囲を広げるかどうか 限定的な拡張性の明示により現実性を重視し、誤検知を抑える方向 攻撃者がどう広げられるかを最大限に評価し、攻撃面を広く捉えている MITRE ATT&CKマッピング 比較的緩やかで、ATT&CKの大分類を幅広く適用 細分類を積極的に利用し、攻撃者の技術的アクションがより具体的 結論から言うと、Copilot と OpenAIのいずれも、Juicy情報の抽出と構造化を一定の精度で自動化できました。それも攻撃の材料となるデータを的確に拾い上げるだけでなく、それらを「優先度」「有効性」「持続性」「攻撃フェーズ」などの評価軸とともにJSON形式へ整理する、といったところまで対応できていました。つまり、従来であれば人間がシェルのログやスキャナの出力を目視でチェックしながら「これは使える」「これはノイズ」と判断していた作業のかなりの部分を、LLMに肩代わりさせることが現実的なレベルに来ているということだと考えます。 また、リスク評価についても両LLMとも対応可能でしたが、興味深いのは両者の回答に共通性が全く見られなく、「何をリスクとみなすか」という視点がモデルによって大きく異なる点でした。モデルごとの特徴としては以下のような点が見られました。 左: Copilot 右: OpenAI。 Copilot : 広範囲な分析を行えるが、攻撃方法そのものは示さず、脆弱性の説明に留まっている。具体的な悪用方法は一切出て来ず、技術要素の分解や分析には強みがあると感じられる回答。 OpenAI : 攻撃者の視点に立った指摘を返す傾向があり、次のステップにつながるような示唆が得られる。情報の掘り下げもやや綿密で、脅威モデリングに強みを持っていると感じられる回答。 情報の正確さについては、どちらが優れているかはこれだけでは判別できません。ただし、どちらか一方を 100% 信用してしまうと、検証がバイアスのかかったものになりかねないため、複数の LLM を比較・併用し、相互に補完しながら評価を進めることが望ましいかもしれません。 まとめ 今回の検証を通じて、たとえ LLM のモデルが似通っていても返答内容に明確な差があることに驚かされました。そのため、調査のスコープや目的に応じてモデルを使い分けることが有効であると感じます。 また、Copilot の出力は想定よりもフィルタにかからず返ってきた点が意外でした。ただし「図示してください」といったリクエストにはフィルタが作用し、返答が返ってこなくなるなど、制御の仕組みが部分的に異なることも確認できました。 これは当たり前なのですが、今回は攻撃検証用に用意された環境だったため、つい何度もエクスプロイトを試したりスキャンを繰り返したりしてしまいました…(ごめんなさい)しかし、本番の業務では実際に企業が運用する環境を対象に検証をすることになるため、好き勝手に振る舞ってはいけません。事前の合意や関係者との調整、適切なスコープ定義やログ管理など、交渉と運用上の配慮が必須であることを改めて強く意識しました。これは業務として体験しなければ一生学べなかったことだと思うのでとてもありがたく感じています。 インターンの感想 ここ最近の学術論文を読んでいるとLLMに注目した研究が多いとは感じていました。ただ、AIという存在の大きさや複雑さを考えると、自分が対話型AIを日常的に使っているにもかかわらず、それをそのまま業務に取り入れることにはどこか違和感がありました。 しかし実際に業務として触れてみるとLLMという技術の奥深さは想像以上で、その有用性を強く実感できました。新しい生成AIツールに潜む脆弱性を体系的に調べるプロセスや、LLMをセキュリティ領域に応用する具体的な方法を学べたことは大きな収穫です。当初リモートワークの利用で、初日・最終日以外に出社しないつもりでしたが、毎日の業務があまりにも楽しく、結局なかなかの頻度でオフィスに行きました!早起きと通勤ラッシュに慣れるのは大変でしたが、今となっては毎日出社すればよかったと少し後悔しています。 さらに、流行の技術をただ追いかけるのではなくリスクを冷静に見極める視点、Offensive Security PJ から学んだチームで知見を共有しながら改善策を考える姿勢、新しい技術を導入するときに既存の仕組みとどう結びつけるかを考える柔軟さなど、こうしたものを意識できるようになったのも良い経験でした。「便利さ」と「プライバシー・リスク」のバランスを常に意識する倫理観も、自分の中に根付いたと思います。 セキュリティ技術を深めたいと思っていた私にとって、関心のあることに思いきり取り組めたこの2週間はとても充実した時間であり、自分の視野や姿勢を大きく広げてくれる経験になりました。 おわりに 今回のインターンシップでは日頃の活動だけでは決して得られないような貴重な体験をさせていただき、とても光栄に思っています。特に2週間を通じて現在ホットなテーマであるLLMのセキュリティについて深く学ぶ機会をいただき、最新動向を追い続けることの重要性やセキュリティそのものの面白さを改めて実感できました。 また、「攻撃者の視点に立つ」という考え方についてもトレーナーの皆さまからのご指導を通じて少しずつ身につけることができたと感じています。 このような貴重な機会を与えてくださったOffensive Security PJの皆さまに心より感謝申し上げます。さらに、上長の有藤さん、トレーナーの四方さん、田口さんにも厚く御礼申し上げます。 本記事を通じて少しでもインターンシップに興味を持っていただけたら大変光栄です。
デジタル改革推進部の小林です。いまは社内認証サービス・基盤のプロダクトオーナーをやっています。5月まではイノベーションセンターにいました。 去る11月15日土曜日に開催された BTCON JP 2025 において、「現場とIT部門の橋渡しをして3000人の開発者を救った話」との題で登壇してきました。この記事では、ask the speakerのコーナーや、懇親会でお話しした内容を踏まえて、その発表では盛り込めなかった内容をお伝えします。 アーカイブ動画が公開されました(2026/2/20 追記) 発表のあらまし 追加コンテンツ どうやるかは最後 会って話す 端からめくる 燃え尽きに注意 終わりに アーカイブ動画が公開されました(2026/2/20 追記) 公式からアーカイブ動画が公開されました。ぜひご覧ください。 発表のあらまし かつてこのブログの記事「 エンジニアがエンジニアのために開発・検証用 PC を整備した話 」で紹介した話を題材に、現場とIT部門の付き合いにおける人的側面に焦点を当て、対話・協働・挑戦の奨励が重要であるとの学びを報告しました。登壇資料はこちらにアップロードしておきましたので、よろしければご覧ください。 追加コンテンツ このセクションでは、発表であえてカットした、あるいはお話しし損ねた内容をいくつか紹介します。 どうやるかは最後 セッションA9「LINEヤフー合併におけるグループ従業員/アカウント管理の課題」を発表されたLINEヤフーの久保貴史さんと懇親会でお話しした折、この話が出ました。 久保さんの発表では、新たに策定した管理策を海外法人を巻き込んで今後実施していくことが紹介されました。懇親会では「海外法人に指示を通しきるのは難しいところもあると思いますが、どのように実効力あるものにしますか?」との私の問いに、「なぜやるかをとことん説明する。そのために丸1日かけた」「howの話は後でよくて、whyをみんなで合わせるのが重要」との明快な回答をいただきました。 発表に盛り込み損ねたのはまさにそこで、ある課題が発見された際に、なぜその課題を解かなければならないかを関係者全員で共有・納得できていると、解決が爆速になります。そもそも解くべき課題だと自分が認識している事柄が、他者には課題だと思われていないこともしょっちゅうです。納得していない人を動かすことほど難しいことはありません。海外とはデフォルトで共有できる価値観があまり多くないこともしばしばですから、ここのwhyに関する対話をゆるがせにするとうまく行かないのは道理だなと感じました。 また、久保さんは「現地に訪問して作業を観察し、疑問に思うアクションがあればなぜそれを行っているのか、その場で確認した」ともおっしゃっていたことが印象的でした。答えはフィールドにこそあります。 会って話す 今回の発表は「コミュニケーションの話」とまとめることもできるかと思いますが、発表資料ではコミュニケーションの語をあえて一切出しませんでした。「コミュニケーション」の語で受ける印象に、広がりがありすぎると考えているためです。 コミュニケーションのスタイルはいろいろと想定されます。言語・非言語、テキスト・ビジュアル、対面・非対面、などなど。今回の発表で私が一貫して主張したいのは「会って話す」ことです。「会って話す」を実現するにはその文字通り、直接出向いて面と向かうしか方法がありません(2025年11月現在)。 会いに行くことには力があります。チャットやメールだとやたら当たりがキツい人でも、会って話すと物腰柔らかくて驚くといったこともあります。まずはアポを取ってみませんか。 端からめくる Ask the speakerのコーナーにお見えになった方に、「端からめくる」話をしました。これは当社のとある元役員が使っていた表現で、問題解決にあたってはいきなり主流派をどうにかしようと思わず、少し背伸びする程度くらいで解決できるような身の回りのことから始めるのがよいといった意味合いです。 繰り返しになりますが、解きたい課題があるなら、それは解くべき課題だと理解してくれる仲間を作るところから始めましょう。仲間がいるのは心強いものです。そのうち仲間が増えれば、それがいつの間にか主流派になります。 燃え尽きに注意 IT部門始めバックヤードには責任感の強い方がしばしば見られます。それ自体はとてもよいことですが、あまりにも強すぎる責任感は燃え尽きにつながります。何を隠そう私自身がそうで、自分一人でなんとかしなければと背負い込みすぎていた時期を経験しています。考え直したきっかけはもはや思い出せませんが、あれは思い違いだったと今ならはっきり言えます。 困ったときには助けを求めましょう。助けを求めることは責任の放棄ではありません。きちんと強いチームであれば、チームで成果が出ればよいことを誰もが知っています。またチームのマネージャーである方には、チーム内で助けを求めやすい環境作りをお願いしたいと思います(自ら道化を演じるでもよいと思います)。 終わりに 開発PCを作った経験からはさまざまな学びがあり、とりわけこの人的側面の話は本当に身に染みました(ほかの開発メンバーからも異口同音に聞かれました)。いつかは外部発表したいと思っていたところで、このような形で機会をいただきました。当日会場・YouTubeでご覧いただいたみなさん、またBTCON JP 2025運営メンバーのみなさん、ありがとうございました。
はじめに こんにちは!NTTドコモビジネスの2025年夏の現場受け入れ型インターンシップに参加させていただきました、インターン生の竹田です。私は現在高専の専攻科1年生で、普段は船舶におけるサイバーセキュリティに関する研究活動を行っています。 この記事では、私が今回のインターンシップで取り組んだ業務体験内容について紹介します。 はじめに 参加のきっかけ インターンシップ概要 OTセキュリティとOsecTの概要把握 テーマ選定 検討1: 船舶での使用プロトコル調査 NMEA 0183 IEC61162-450 検討2: 現状の船内ネットワーク調査 検討3: 船舶pcapに対する現状のIDS製品の出力検証 Talker IDから資産を出力するZeek・Spicyパーサー作成・検証 Zeek・Spicyとは パーサー検証 まとめ イベントへの参加 SOC見学 ドコモとのコラボ企画 イノベーションセンター内での業務共有 TechLunch インターンシップを通して学んだこと おわりに 参加のきっかけ 私がインターンシップに参加したのは、研究のテーマとして「船舶におけるIDS(侵入検知システム)」を扱っていることが理由です。制御システムのセキュリティと船舶のセキュリティは「可用性」を最重要視するという点で類似しているため、実際に制御システムで使用されているOT-IDS(制御システム向けIDS)がどのように運用されているのかを知りたいと思いました。また、船にIDSを導入することで船舶会社にどのような利点が見込めるのか、導入する場合の技術的な課題は何かについて理解したいという思いもありました。 インターンシップ概要 今回私は、NTTドコモビジネス イノベーションセンターで、OTシステム向けIDS・セキュリティ可視化サービスである、OsecT(オーセクト)の開発に関する業務を行いました。 インターンシップで行った業務体験内容を以下にまとめます。 OTセキュリティとOsecTの概要把握 まず、OTセキュリティの概要を把握するために、イノベーションセンターが社会人向けに提供している教育プログラムを受講させていただきました。 講義では、Stuxnet、Mirai、WannaCryといったマルウェアが制御システムに影響を及ぼした事例に加え、GPSスプーフィングやWebカメラの不正利用など、制御環境にも波及する脅威が紹介されました。また、ランサムウェア被害を受けた企業の約53%が50万ドル以上の身代金を支払ったという 調査結果 を知ることになりました。私はこの数字を聞いて、サイバーリスク保険の存在によって経済的負担が減っているために、莫大な金額を請求されても企業が支払いに踏み切ってしまう現状があるのではないかと思いました。 さらに、ICSサイバーキルチェーンやMITRE ATT&CK for ICSといった、OTセキュリティ特有の分析フレームワークも紹介されました。これまで一般的なITセキュリティのフレームワークしか知らなかったため、制御システム向けに整理された独自の手法が存在することを初めて知り、新鮮な学びとなりました。 OTセキュリティの概要を学ぶ上で特に面白いなと感じたのは、 業界によってセキュリティ要件の枠組みが大きく異なる 点です。 船舶分野 UR E26/27 という国際的に統一された技術要件が存在し、グローバルに共通の基準に基づいて安全性やサイバーセキュリティ対策を整備する流れがある。これは国際航行を前提とするため、世界的に統一されたルールが不可欠となっている。 製造分野 国や地域、さらには分野ごとに異なる規格が並立しており、北米では CSA規格 、EUでは NIS2指令、 半導体分野では SEMI規格 などが例として挙げられる。市場や技術領域の多様性が大きいため、各地域、各分野で独自の要件が策定されている。 この違いは、業界ごとの国際性や供給網の特性を反映していると考えられます。船舶は国際航行を前提とするためグローバルに統一されたルールが不可欠である一方、製造業は市場や技術領域の多様性が大きいため、各地域で独自の要件が策定されやすいのだと理解しました。 単に「OTセキュリティ」と括るのではなく、こうした制度設計や標準化の背景を踏まえて学ぶことの重要性を改めて実感しました。 次に、チームが開発しているサービスであるOsecTについての説明を受けました。 (引用元: https://www.ntt.com/business/services/security/security-management/wideangle/osect.html ) OsecTは2つの基本機能を備えています。 ネットワークの可視化 ネットワークトラフィックを分析し、資産・通信・セキュリティリスク等をwebポータル画面で可視化できます。これにより、ネットワークマップやトラフィック量から重要な端末を視覚的に把握でき、対応の優先順位がつけやすくなります。 脅威・脆弱性の検知 通信状況を学習し、脅威や脆弱性を検知した際にアラートを通知します。検知した情報はwebポータルでも確認可能です。 また、 資産台帳と連携する機能 も持っているため、不審な状況を見つけたときに設置場所や担当者などの情報をもとに、素早く対応することが可能となっています。さらに、資産台帳に登録されていない、いわゆる未把握の機器も表示されるため、台帳の漏れを補完したり、不正に接続された端末の存在を調査したりすることも可能です。これにより、既存の資産管理の精度を高めると同時に、セキュリティリスクを早期に発見する仕組みとしても活用できます。 一方で、現行の仕組みには課題があることも示されました。例えば、IPアドレスを持たない機器については、対応しているOTプロトコル以外は可視化の対象外となるため、 すべての資産を完全に把握できるわけではありません 。 説明を聞いて、これまでIDSというと「脅威検知」をするものだという印象がありましたが、OT-IDSには「ネットワークの可視化」という重要な役割も担っているんだということを知り、とても勉強になりました。 テーマ選定 ここまでの講義・説明を受けて、私はある1つの仮説を立てました。 船舶のプロトコルを分析 すれば船内の資産を把握でき、 資産インベントリの作成・更新補助ができるのではないか。 ※資産インベントリとは、 IACS UR E26/27 で提出が義務付けられている、船舶機器の詳細をまとめた台帳です。 船舶のライフサイクルを通じて船舶資産インベントリは更新・維持されなければならないことを考慮して作成する必要があります。なお、 資産インベントリの管理を手動で行う場合、更新作業が煩雑になる可能性があります。 そのため、資産インベントリの更新を効率化し、精度を高めるために、 システム化が推奨されます。 (Class NK | IACS UR E26 p.31 4-1.2より) この点を踏まえ、各船舶機器が発する固有のシグナルを活用できれば、船舶資産の自動識別が可能となり、資産インベントリの作成・更新作業を大幅に補助できるのではないかと考えました。 しかし、OsecTは船舶特有のプロトコルに対して十分な解析機能を持っていません。解析可能とするためには新たに専用のパーサーを開発・実装する必要があります。 そこで、以下のプロセスで調査・検証を進めることにしました。 検討1: 船舶での使用プロトコル調査 調査の結果、船舶では主に2つのプロトコルが使われていることが分かりました。調査内容を以下にまとめます。 NMEA 0183 NMEAは「National Marine Electronics Association(米国海洋電子機器協会)」の略。 船舶機器間の通信のための仕様で、潮流計、音響測深機、ジャイロコンパス、GPSなどのデータを伝送するために使用される。シリアルポートを使用した片方向通信でASCII形式。 例(GPS): $GPGGA,052400.00,3539.3146239,N,13945.6411751,E,4,07,0.59,4.987,M,34.035,M,1.0,3403*76 (データ引用元: https://ales-corp.co.jp/technical-information-nmea/ ) Talker ID とよばれる、どの機器がそのデータを送信しているかという識別子がある。上記の例では、 $GPGGA の GP の部分がTalker IDとなる。 Talker ID対応表(一部) 引用元: pynmea2/NMEA0183.pdf at master · Knio/pynmea2 · GitHub | **ID** | **解説** | | --- | --- | | AG | 一般的なオートパイロット | | AP | 磁気コンパス連動のオートパイロット | | CD | DSC(デジタル選択呼出)通信装置 | | CR | 無線ビーコン受信機 | | CS | 衛星通信機 | | CT | MF/HF帯ラジオ電話通信機 | | CV | VHF帯ラジオ電話通信機 | | CX | スキャン機能付き無線受信機 | | DF | 方位探知機 | | EC | ECDIS | | EP | EPIRB | | ER | 機関室モニタリングシステム | | GP | GPS受信機 | | HC | 磁気コンパス | | HE | ジャイロコンパス(真北追従) | | HN | ジャイロコンパス(非真北) | | II | 統合計測機器 | | IN | 統合ナビゲーションシステム | | LC | ロランC | | P | メーカー独自(非標準) | | RA | レーダー/ARPA(自動衝突予防装置) | | SD | 音響測深機 | | SN | その他の電子測位システム | | SS | スキャニング音響測深機 | | TI | ターンレートインジケータ | | VD | ドップラー式速度センサ | | DM | 水中・磁気式速度ログ(船速計) | | VW | 水中・機械式ログ(パドルホイール等) | | WI | 気象センサ | | YX | トランスデューザー | | ZA | 原子時計 | | ZC | クロノメーター | | ZQ | 水晶時計 | | ZV | 電波時計 | IEC61162-450 IECは「International Electrotechnical Commission(国際電気標準会議)」の略で、後ろに標準化された規格の番号を付与して文書や通信規格そのものの名として利用される。 UDPマルチキャストを使って、NMEAセンテンスをEthernetパケットで送信するための規格。 例: UdPbC\0\s:GP0001\$GPGGA,052400.00,3539.3146239,N,13945.6411751,E,4,07,0.59,4.987,M,34.035,M,1.0,3403*76\r\n タグブロック部と呼ばれる、NMEAセンテンスの直前に配置される行の中には sブロック とよばれる送信元識別子がある。 調査の結果、NMEA 0183には Talker ID 、IEC61162-450には sブロック と、どちらのプロトコルにも資産特定に使えそうな識別子がありました。 検討2: 現状の船内ネットワーク調査 次に、現状の船内ネットワークがどのような構成なのか調査しました。 出典: JRC | 船内LANシステム あくまで私の予想ですが、GPSや潮流計などのシリアル出力データ(NMEA 0183)を入出力IF装置がIEC61162-450に変換しているのだと思われます。 検討3: 船舶pcapに対する現状のIDS製品の出力検証 次に、OsecT以外でのOT-IDSについても船舶プロトコルの対応状況を調査するために、以下の環境を想定したpcapファイルを用意し、2つのOT-IDS製品を使ってトラフィック解析しました。 結果として、今回の検証で使用したOT-IDSでは船舶で使われているプロトコルを解析できませんでした。このため、OsecTに適用可能な船舶プロトコルパーサーを新たに作成することにしました。 今回は、IEC61162-450の仕様書が手元にない+インターネットにもあまり情報がないため、NMEA 0183のTalker IDを足掛かりにしてパーサーの作成を進めました。 Talker IDから資産を出力するZeek・Spicyパーサー作成・検証 OsecTのパーサーはZeek・Spicyで構成されています。 Zeek・Spicyとは Zeek トラフィックを解析して、IPアドレス、MACアドレスやプロトコルなどの情報をログとして出力するOSS。 Spicy Zeekで利用するC++のパーサーをC++で記述することなく簡易に生成するためのツール。 参照: 【日本初紹介】Zeek・Spicyの使い方まとめ - NTT docomo Business Engineers' Blog 今回は、ZeekとSpicyを使って船内ネットワークトラフィックを解析するためのパーサーを作成しました。 パーサー検証 作成したパーサーを使って、実際に船内ネットワークトラフィック(pcapファイル)を解析しました。 今回の検証では、2種類のNMEA 0183メッセージからTalker IDとSentence Typeの2つのフィールドを抽出し、Talker IDからセンサの特定ができているかを確認しました。以下は、ZeekとSpicyを用いて取得したNMEAメッセージの解析結果(logファイル抜粋)です。 GP の場合:GPS HE の場合:Heading (ジャイロコンパス) 上記の通り、Talker IDから発信元のセンサの特定(船舶資産の特定)が行えていることが分かります。 まとめ 今回は、既存のOT-IDSが船舶特有のプロトコルを解析できないという技術課題に対し、NMEA文を解析可能にする独自のパーサーを作成しました。 このパーサーではNMEA 0183内に含まれるTalker IDを足掛かりにして船舶機器を特定できるように設計しており、その情報をOsecTの可視化機能に組み込むことで、船舶特有の機器の可視化の強化や資産インベントリの補助につながる可能性があるというポジティブな成果を得ることができました。 イベントへの参加 IDSに関する業務以外にも複数の社内イベントや見学会に参加させていただきました。 SOC見学 NTTセキュリティのSOC(Security Operation Center)を見学しました。 当初はSOCに対して「脅威を検知したら即座にアラートを出す場所」というイメージを持っていました。しかし、実際にNTTセキュリティが提供するSOCサービスは一定期間トラフィックを収集・分析し、詳細なレポートとしてクライアントに提供するサービス形態であることを知りました。SOCには多様な形があるのだということを知ることができ、とても勉強になりました。 ドコモとのコラボ企画 イノベーションセンターで攻撃インフラの解明や撲滅に向けて活動しているPJがセキュリティ国際会議「Botconf」で発表した内容を、ドコモ本社のインターン生とともに聴講させていただきました。 発表内容は新ロシア派ハクティビストに関する詳細なレポートで、実際の脅威情報分析の手法を学ぶ貴重な機会となりました。また、ハクティビストの情報を安易に拡散することがかえって悪影響を生む可能性があると知り、情報との向き合い方を考え直すきっかけにもなりました。 イノベーションセンター内での業務共有 イノベーションセンター内で活動する5名のインターン生が、それぞれの担当業務や取り組み内容を共有しました。 普段は近くで作業していても、実際に何をしているのか詳しく知る機会は少なかったので、とても新鮮でした。みんな全然違うテーマやプロジェクトに取り組んでいて、「そんなことやってるんだ!」と驚くことも多く、チームの多様性やそれぞれの専門性の広さを実感できて面白かったです。 TechLunch NTTドコモビジネスで不定期開催されているランチ勉強会「TechLunch」に参加しました。 「勉強会」という言葉から堅い雰囲気を想像していましたが、実際は社員の方がそれぞれの興味関心に基づいて自由に発表する形式で、活発かつフランクな学びの場でした。 当日は社員1名とインターン生2名の発表があり、発表中はSlack上でリアルタイムに反応が飛び交い、オープンな雰囲気を感じました。 インターンシップを通して学んだこと インターンに参加する前は、GPSスプーフィングやAISスプーフィングといった船舶に多く見られる攻撃を検知するIDSを作成することを考えていました。しかし、OT-IDSの説明をしていただく中で「資産インベントリ補助」という新たな活用方向を見出すことができ、とても有意義な経験となりました。 また、製造業と船舶業界ではセキュリティ要件の枠組みが大きく異なることも学びました。将来的には製造業のセキュリティ分野に関わりたいと考えているため、今後は制度やプロトコルの違いを意識しながら、より幅広い視点で学びを深めていきたいと思います。 さらに、インターンを通じてセキュリティに関わる仕事の現実的な難しさと奥深さを実感しました。重大なインシデントが起こらないことは、セキュリティに携わる者として理想的な状態です。しかし一方で、何か大きな事件が起こらなければ業界全体の意識や整備が進みにくいという現状もあり、その間にある葛藤のようなものを感じました。 おわりに 今回のインターンはこれまで開発に携わった経験がなかった自分にとって、大きな一歩を踏み出す機会となりました。以前から「何かを作ってみたい」と思いながらも、具体的なアイデアが浮かばなかったり、時間を理由に後回ししてしまったりしていました。そんな中で、実際に手を動かして開発へ取り組めたことは、自分にとって非常に貴重な経験でした。 また、さまざまなイベントにも参加させていただき、自分の所属チームだけでなく他のチームやインターン生、さらにはドコモグループの社員の方々とも関わることができました。部署を越えて多くの方と交流する中で、職場の雰囲気や、人とのつながりの大切さを改めて実感しました。 最後になりましたが、このインターンに関わってくださった皆さまに心より感謝申し上げます。受け入れてくださったチームの皆さま、特にこのインターンで参加したIT/OT統合セキュリティPJおよびOsecT Tech PJの田中さん、前田さん、加島さんには2週間にわたり大変お世話になりました。温かくご指導くださり、本当にありがとうございました。
こんにちは、イノベーションセンターの田口、金井です。 普段はOffensive Security PJのメンバーとして活動しています。 この記事では2025年8月に開催されたSOUPS 2025でポスター発表したことおよび同時期に開催された USENIX Security 2025へ参加したことについて紹介します。 Offensive Security PJについて USENIX Securityについて SOUPSについて ポスター発表について 発表概要 発表の様子 カンファレンスの様子 セキュリティ分野の研究動向 会場の雰囲気 おわりに Offensive Security PJについて Offensive Security PJ では、攻撃者視点のセキュリティ(Offensive Security)を専門とするチームとして、 攻撃技術の調査・開発・検証に取り組んでいます。 攻撃者に先んじて新たな攻撃技術を検証することで、将来の脅威を見越した防御の強化につなげています。 主な業務内容としてWideAngleのプロフェッショナルサービスにおける攻撃技術の検証支援や、 最先端の攻撃技術に関する応用的な研究開発を行っており、 成果のカンファレンス発表など対外的な活動にも積極的に取り組んでいます。 USENIX Securityについて USENIX Securityはサイバーセキュリティ分野の世界的なトップカンファレンスの1つです。 厳しい査読を通過した完成度の高い研究のみが発表される、まさに最先端のセキュリティ研究に触れることができるカンファレンスです。 34回目となる今年は米国シアトルで8月13日から15日までの3日間開催されました。 今年は論文発表が439件、ポスター発表が60件あり参加者は約900名でした。 SOUPSについて SOUPS(Symposium on Usable Privacy and Security)はユーザブルセキュリティ&プライバシー分野における最難関の国際会議です。 ユーザブルセキュリティ&プライバシーとは、コンピュータやシステムを扱う人間に焦点を当て、 人間が扱いやすいセキュリティを主題とする研究分野です。 USENIX Securityとの併催会議として毎年同時期に開催されます。 21回目となる今年も例年通りUSENIX Securityの併催会議として8月10日から12日までの3日間開催されました。 今年は論文発表が30件、ポスター発表が49件あり、参加者は186名でした。 ポスター発表について 発表概要 SOUPS 2025で発表した研究概要について紹介します。 Offensive Security PJでは「Offensive Security × HCI(Human-Computer Interaction)」 というテーマを掲げて研究開発業務をしています。 SOUPS 2025では“ Understanding the Challenges in Red Team Exercises from Multiple Stakeholder Perspectives ”というタイトルで発表してきました。 今回の研究で私達は、レッドチーム演習に従事する専門家へインタビューし、 専門家が抱える課題認識を分析することで、 実施するにあたりどのような障壁が存在するのかを明らかにしました。 この研究では、レッドチーム演習における疑似攻撃者を担う専門家と、 演習全体の統括を担う専門家という異なる役割を持つ2つの専門家に対してインタビューしました。 複数の役割の視点から課題を調査する取り組みは既存研究としては無く、 この研究のキーコンセプトと言えます。 現在はこの研究のネクストアクションとして、明らかにした課題を解決するための研究開発に取り組んでいます。 レッドチーム演習とは、評価対象へ疑似的攻撃をすることで 対象組織やシステムのセキュリティを評価するセキュリティテスト手法の1つです。 発表者の田口 発表の様子 SOUPS 2025のポスター発表は8月11日のテクニカルセッション終了後のレセプションパーティー会場で行われ、 参加者はドリンクと軽食を持って会場内のポスターを巡り自由に質問や議論をしていました。 SOUPSではフレンドリーな雰囲気で話をしてくれる参加者が多く、 英語が堪能でない私でも有意義な質疑応答や議論ができたと感じています。 質疑応答では「インタビューで参加者はどのような課題感を述べた?」、 「今後インタビュー調査を拡大する予定はある?」といった質問をいただきました。 レッドチーム演習の実務者にインタビューで直接課題を聞き出したという点が特に興味を引いていた印象を覚えました。 研究自体に対するポジティブな反応も多くもらうことができネクストアクションのモチベーションにもつながる良い機会だったと思います。 カンファレンスの様子 カンファレンスの情報や現地の様子について紹介します。 セキュリティ分野の研究動向 オープニングセッションにて、USENIX Security 2025に投稿・採択された論文の研究トピックの割合についてUSENIX運営から説明されました。 昨年から引き続き論文投稿数と採択数共に1位だった研究トピックはML & AIセキュリティに関する論文でした。 Human Factor(人に着目した分野)に関する研究トピックも年々増えてきている傾向にあります。 また、今年採択された論文の研究トピック割合としては以下のように示されていました。 ML & AI: 23% System, software, crypto, network: 65% Human factors: 12% 会場の雰囲気 全日程Seattle Convention Center Archで行われました。 オープニング前のメインホールです。大人数の参加者が十分着席できるほどの広さでした。 運営組織であるUSENIX Associationが今年で設立から50周年を迎え、 大きなケーキを用意した記念パーティーが開催されました。 おわりに SOUPS、USENIX Securityに参加することで最新のセキュリティ研究を知ったり、 現地で国内外のセキュリティ研究者たちと交流したりと得られるものがとても多かったと感じています。 また、ポスター発表で参加者と英語で積極的に議論した経験は、私に自信とモチベーションを与えてくれる貴重な経験だったと思います。 私達の発表を含め採択されたポスターや論文は SOUPS 2025 、 USENIX Security 2025 のホームページにて公開されているのでぜひ読んでみてください。