TECH PLAY

NTTドコモビジネス

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

613

この記事は、 NTT Communications Advent Calendar 2024 の記事です。 この記事では、OSINT(Open Source Intelligence)の基本的な考え方と、分析の際に重要となる認知バイアスへの対処方法について解説していきます。 また、実際の分析で使われる競合仮説分析(ACH: Analysis of Competing Hypotheses)という手法についても紹介します。 はじめに OSINTとは Intelligenceとは 本記事の目的 OSINTを成功させるためのマインドセット OSINTの特徴 マインドセットの基本原則 認知バイアスとは OSINTにおける認知バイアスのよくある例 認知バイアスを放置するリスク 認知バイアスを取り除くための手法:競合仮説分析 競合仮説分析の基本概要 手法のステップ まとめ こんにちは。イノベーションセンターの竹﨑( @z4ck_key )です。普段はNetwork Analytics for Security PJ 1 (通称NA4Sec)というチームで脅威インテリジェンスの分析業務を行っています。 この記事では、脅威インテリジェンス業務のみならず使えるOSINT(Open Source Intelligence)の基本的な考え方と、分析の際に重要となる認知バイアスへの対処方法について解説していきます。 また、実際の分析で使われる競合仮説分析(ACH: Analysis of Competing Hypotheses)という手法についても紹介します。 はじめに OSINTとは OSINTとは、 O pen S ource INT elligenceの略で、一般に公開されている情報を収集し、アナリストが分析することで生まれる成果物や一連のプロセスを指す専門用語です。 インターネット上のウェブサイト、ソーシャルメディア、公的文書など、誰でもアクセス可能な情報源を活用して、必要な情報を体系的に収集・分析します。 これは元を辿ると軍事情報機関で使用されていた手法でしたが、現在ではビジネスインテリジェンスや脅威分析など、さまざまな分野で活用されています。 Intelligenceとは 日本語で「情報」と訳されがちな言葉に、 Data , Information , Intelligence がありますが、この3つはそれぞれ指し示す意味が微妙に異なります。 Data ただそこにあるだけの値。 e.g.) 「ある組織のウェブサイトのアクセス数が1日1000件」という数値。 Information dataから読み取れる事象。 e.g.) 先月と比べてアクセス数が30%増加している。 Intelligence 分析や解釈を経て意思決定に活用できる形へ加工された知見。 e.g.) 競合他社の新製品発表に伴う市場の関心の高まりがアクセス数増加の要因として考えられ、今後の対策が必要。 このように、Intelligenceは生のデータや情報を分析・解釈し、実際のアクションにつなげられる形(これを Actionable と言ったりします)に昇華させたものと言えます。 OSINTは前述の通りOpen Source INTelligence なので、ただ情報を集めるだけではなくIntelligenceに整形して集めた情報をActionableにする必要があります。 本記事の目的 最近、OSINTという単語がセキュリティを業務にしている人やCTFに参加している人、およびその人達のSNSなどでよく見られるようになっています。 それ自体は喜ばしい一方で、OSINTにおける分析プロセスやマインドセットが触れられず具体的な方法論ばかりが注目されがちだと感じます。 本記事では、OSINTを実施する上で大切なマインドセットや、 分析の際問題になりがちな認知バイアスへの付き合い方について言及します。 それ故に、この記事内で具体的な情報取得のHow toは触れません。ご承知おきください。 OSINTを成功させるためのマインドセット OSINTの特徴 OSINTは前述の通り公開されている情報を収集することが分析の起点です。 それ故に、情報源には多様性があり、質のばらつきも存在することが前提となります。 取得できた情報全てが正しいと 確信を持てることは少ない ということです。 このような特徴を踏まえ、OSINTを効果的に実施するためには、 分析者の主観や偏見を排除 し、 客観的な視点を維持する ことが極めて重要です。 そのため、以下のようなマインドセットを持つことが必要不可欠です。 マインドセットの基本原則 先入観を持たないこと 収集時に自分の仮説を押し付けない 異なる視点や情報に対してオープンな姿勢を保つ 情報の信憑性を常に疑うこと 取得した情報が全て正しいと思い込まない 情報源の信頼性を評価するための基準を持つ これらの原則を意識することで、予想外の情報を発見したり、偽誤情報や古い情報に直面した際にも、冷静に対応できる可能性が高まります。 しかし、どれだけ先入観を排除し、情報の信憑性を疑うことを意識しても、私たちの思考には無意識の偏りが存在します。 次に、その偏りを引き起こす「認知バイアス」とは何かについて見ていきましょう。 認知バイアスとは 認知バイアスとは、人間の思考や判断に影響を与える心理的な傾向や偏りのことを指します。 これは私たちの脳が情報処理を効率化しようとする過程で自然に生じる現象であり、特にOSINTのような情報分析において大きな課題となります。 分析者は自身の認知バイアスを理解し、それを意識的に制御することが重要です。 OSINTにおける認知バイアスのよくある例 情報を収集している中で、よく直面しがちな認知バイアスの例について、いくつか紹介します。 例1: 確証バイアス 自分の仮説に合致する情報だけを集めてしまうバイアスのこと。 例えば、「調査中のAPTは中国のグループだ」という仮説を立てた場合、その仮説を裏付ける情報ばかりを重視し、反証となりうる情報を無意識的に無視してしまったりするのがこの例。 例2: アンカリング効果 最初に得た情報がその後の分析に過度に影響を与える心理現象のこと。 人間の脳は新しい情報を処理する際、既知の情報を基準点として利用する傾向を持つため、これが原因とされている。 認知バイアスを放置するリスク これらの認知バイアスを放置して分析を進めることは、インテリジェンスの正確性が信用できなくなり、誤解にもとづく意思決定の可能性が生まれてしまうことにつながります。 自分がより主張したい情報だけを集めたり、都合のいいように情報を解釈して意思決定してしまう経験は皆さんにもあることでしょう。 OSINTは分析した結果をもとに次のアクションに繋げるためのものなので、誤解にもとづく意思決定をするということは意思決定者にとって喜ばしくない結果を引き起こすことになります。 認知バイアスを取り除くための手法:競合仮説分析 前述した認知バイアスの影響をなるべく最小限に抑え、複数の可能性を体系的に評価するためのメソッドとして、 競合仮説分析 (ACH: Analysis of Competing Hypotheses) が存在します。 本節では、競合仮説分析の概要とその手法について説明します。 競合仮説分析の基本概要 競合仮説分析は諜報機関で使われていた手法であり、アメリカ国防総省の情報機関CIAの情報分析官リチャーズ・J・ホイヤーによって広く啓蒙されました。その時の書籍はCIAによって一般に 公開されている ので、興味があればぜひ見てみてください。 この手法の最大の特徴は、 仮説を1つずつ検討するのではなく、複数の仮説を同時に評価するプロセスを採用している点 です。 単一の結論に急いで飛びつくのではなくあらゆる可能性を慎重に検討することで、認知バイアスを除去し特定の仮説へ偏った結論に至るリスクを軽減します。 分析官は自身の直感や先入観に頼るのではなく、証拠に基づいて各仮説を公平に評価することが求められます。 筆者の周辺では、脅威インテリジェンス (CTI: Cyber Threat Intelligence) を生成する目的で行う分析において、実際に使用されることも多いです。 手法のステップ 競合仮説分析は以下のプロセスに分割して定義されています。 仮説列挙 可能性のある仮説をできるだけ多く列挙します。 仮説の列挙は個人で実施するとこの時点でバイアスに呑まれてしまう可能性が存在するので、本来は色々な人の意見を募って実施するべきとされています。 現代においては生成AIの助けを借りることで、個人であっても比較的幅広い仮説を列挙できるでしょう。 証拠集め 各仮説を裏付けたり反証したりするための証拠を収集します。 OSINT文脈においては公開情報から証拠を収集しますが、インテリジェンス分析の際にはOSINTに限らず他の情報源も活用して情報を収集できるとより望ましいでしょう。 比較 仮説列挙フェーズで列挙したそれぞれの仮説に対して、証拠集めフェーズで収集した証拠がどのように寄与するかをマトリックスにして分析します。 今回は、過去に自分が作成したマトリックスを例に挙げます。 作成したマトリックスは以下のようになり、この作業でどの証拠がどの仮説に寄与するかの関係性が可視化されます。 この例では、 + :仮説を支持する、 - :仮説を反証する、 0 :どちらでもない、という表記で仮説を評価しています。 再評価 比較フェーズで一旦評価したマトリックスを、以下を実施して改良します。 精査の結果、新しい仮説や証拠があれば追加。 裏付ける証拠がない仮説を削除。 これにより、関連性の薄いと判断できる仮説の棄却と不足している証拠を他の仮説への寄与度込みで評価できます。 仮説選定 再評価フェーズで絞り込まれた仮説の中から、最も多くの証拠によって支持される仮説を選定します。 反証可能性の検証 このフェーズが競合仮説分析の考え方においては重要なフェーズになります。 仮説選定のフェーズで絞り込んだ仮説に対して、それぞれの仮説がどの程度特定の証拠に依存するかを以下の観点で確認します。 主要な仮説は 1つか少数の証拠に依存しているか 重要な証拠が正確だという確信度はどの程度あるか 証拠は時間の経過とともに変化する可能性がある事象か。変化する場合は仮説にどのような影響を与えるか。 このフェーズを踏むことにより、分析の段階で混入した主観的なバイアスを取り除くきっかけが生まれます。 レポーティング 最終的に意思決定者にこれらの分析結果を報告する必要があるので、レポーティングをする必要があります。 この時に注意すべきことは、主となる仮説を裏付けに使用した証拠とともに提示し、それを 推定的な表現 を用いて報告することです(あくまでもこの分析結果はfactではなく、一番確からしい仮説なため)。 最後に評価を補強するために検討した他の仮説や分析過程も提示することで、意思決定者が結論の背後にある理由を理解できるようになります。 まとめ OSINTを成功させるためには、単に情報を収集するだけでは不十分です。次のアクションに繋がる形で情報を加工し、価値あるIntelligenceに昇華させる必要があります。 そのプロセスの中で重要なのは、分析者自身が持つ認知バイアスを理解し、それを可能な限り排除することです。認知バイアスが混入した分析は、誤った結論を導き、意思決定を誤らせるリスクを孕んでいます。 本記事で紹介した 競合仮説分析(ACH) は、認知バイアスを克服し、客観性を高めるための有効な手法です。この手法を用いることで、複数の仮説を公平に評価し、最も支持される仮説を導き出すプロセスが体系化されます。 OSINTは、日々変化する複雑な情報環境の中で、情報を正しく理解し真偽を見分ける為に、専門家だけでは無く個々人にとってもますます重要なスキルとなってくるでしょう。 本記事を通じて、OSINTの実践における心構えや具体的な手法について理解を深めていただけたなら幸いです。 最後に、OSINTは「情報の力」を活用するためのツールです。適切なマインドセットを持ち、分析の質を高める努力を惜しまないことで、その力を最大限に引き出すことができるでしょう。 というわけで、 NTT Communications Advent Calendar 2024 5日目の記事でした。 明日もお楽しみに。 NA4Secプロジェクトについては、このブログの記事  サイバー脅威インテリジェンス(CTI)配信はじめました  をご覧ください。 ↩
この記事は、 NTT Communications Advent Calendar 2024 及び ProtoPedia Advent Calendar 2024 の19日目の記事です。 こんにちは、AIロボット部(社内サークル)部員の宮岸( @daiking1756 )です。 普段は5GI部で映像系商材のプリセールス的なお仕事をしています。 最近急に寒くなってきましたが、昨年から使い始めた アームウォーマー によって、タイピングに影響することなく腕を温めながら生活しています。 部屋全体を温めると頭がボーっとして眠くなるので、暖房は弱めに設定するのが好みなのです。 電熱線などが入っているものもありますが、私の場合どうしても手に違和感があって、タイピングしにくくなるので、このタイプに落ち着きました。 さて、本記事では 3日目の記事 では触れなかったロボット部の活動の紹介として、 ヒーローズリーグ(旧: MashupAward) という開発コンテストの20回記念の展示会にロボット部として参加した話をまとめます。 はじめに ヒーローズリーグ(旧: Mashup Awards)とは MA/HL20thイベントでやったこと ロボットカフェについて temiのセッティング 当日ロボットカフェを利用した方の声を紹介 展示した作品について おわりに はじめに タイトルの通り、イベントでロボットカフェと作品展示をやったよーという記事なのですが、 イベント自体の紹介などもしつつ前提から書いていきたいと思います。 ヒーローズリーグ(旧: Mashup Awards)とは Wikipedia では下記のように説明されています。 Mash up Awards(マッシュアップアワード)は、2006年にスタートしたWeb開発者が自ら開発したWebサイトやスマートフォンのアプリケーション等の技術、デザイン、アイデアを競い合うコンテストである。略称 MA。 2006年から2018年までMashup Awards、2019年以降はヒーローズリーグと改称して開催されている。 上記の通り、Mashup Awards時代に12年、ヒーローズリーグになってからは今年で6年目。 その長い歴史と、作品応募数の多さ(最大500 over, 今年も300 over)から、日本最大級の開発コンテストと呼ばれることもあるイベントです。 Mashup Awards時代の歴史については 「この技術いつ流行った?」という観点で運営の方がまとめてくださっています。 また、先日行われた 【20回記念イベント】 #ヒーローズリーグ & #MashupAwards を振り返る のYouTubeLiveで私も少し喋りましたが、 これまでの20回の大会の軌跡がまとまっています。 私自身は2018年に初めて参加し、そこから毎年何かしらの作品を応募しています。 特にコロナ前に行われた2018年、2019年の決勝戦の熱気は今でも忘れることができず、 確実に私がメイカー活動を始めた強烈なきっかけとなったイベントになりました。 今年は20回目の記念大会ということで、 コロナ明けぶりに オフラインイベント(決勝配信 & 展示会) が12/7(土)に開催されたのでした。(以下、「MA/HL20thイベント」 とします) MA/HL20thイベントでやったこと ドコモがメインスポンサーを務めていることもあり、 会場として docomo R&D OPEN LAB ODAIBA の提供、 賞の提供 、 スポンサーLT の3点で関わりました。 また、NTT Comのロボット部としては普段作成しているロボットの展示と、ロボットカフェの運営を行いました。 ロボットカフェについて ロボット部では普段部員が自由にロボットやIoT関連の作品を制作しています。 その活動の一環として、今年6月にNTTグループ内でモノづくりを行っている有志での社内イベントにて、ロボットカフェの取り組みを行いました。 ロボットカフェとは、 temi という自律走行型パーソナルロボットがコーヒーを運んでくれるカフェのことです。 お客さんはカウンターでコーヒーを注文し座席に座ると、temiにゃん(今回はtemiのセリフにネコ属性を付与しています)が一生懸命コーヒーを座席まで運んでくれます。 コーヒーは社内でも有名な?バリスタの方が拘りのコーヒーを一杯ずつ丁寧に注いでくれました。 (週末は 江東区のお店 に立ってコーヒーを淹れているそう) 今回は6月に社内向けに実施したロボットカフェの取り組みの一部をバージョンアップして、一般向けのイベントで実施しました。 temiのセッティング temiは Buddiotte という専用のソフトウェアを使うことで、ブロックプログラミングで制御できます。 「フロアのマッピング」や「指定した座席まで移動し、定位置まで戻ってくる」という単純な動きであれば、temiの標準機能で実現できます。 画像の赤枠エリアは禁止エリア(仮想壁)としてtemiが立ち入らないようになっています。 今回は机に当たらないように注意しながら経路を設定しました。 今回はBuddiotteを利用することで、挨拶をしながらドリンクを運ぶであったり、指定した座席へコーヒーを運んだ後にユーザのタッチを起因に待機場所まで戻ってくるなどの複雑な動きを簡単に実装できました。 フロアのマッピングをしている様子は撮影し忘れてしまったのですが、マッピングをする際には追従モードを利用します。 追従モードでは先行して歩く人の後ろをtemiが追従する形で自走をします。 temiは追従をしながらLIDARセンサによってフロアの構造物・障害物を認識することでマッピングが実施されるという流れです。 当日ロボットカフェを利用した方の声を紹介 当日は約50人ほどの方にコーヒーを提供しました。 コーヒーの味はもちろん、ロボットの愛くるしい仕草で皆さんを笑顔にしてくれました。 A1:ロボットカフェさん コーヒーをいただいた ドンピシャで座った席に来てくれた 帰る時だけにゃんつけるのがあざとい? #ヒーローズリーグ pic.twitter.com/w0wFUtTLwO — パスコンパス (@pscmps) 2024年12月7日 x.com そして当日は配信室にもコーヒーをお届けしましたよ。 その時の様子はこちら。 www.youtube.com (また、同YouTubeLiveの 5:57:38~ はスポンサーLTとして5分ほど喋りました) 展示した作品について ロボット部のブースで展示した作品は下記の4つです。 懐中電灯(偏光)で操作するロボット (詳細は今年のアドカレ 3日目の記事 ) 電気ネコ 高専かるた (詳細は昨年のアドカレ 7日目の記事 ) HANAPIKU - Awesome Nose Interface - ロボット部の設営完了! お時間ある方はぜひお台場まで遊びに来て下さい〜🙌 #ヒーローズリーグ pic.twitter.com/xnhc3TcPc5 — みやぎdaiking⊿🌗 (@daiking1756) 2024年12月7日 x.com それ以外の展示作品の情報は ProtoPediaのイベントページ にまとまっています。 当日は約40個の作品が展示され、会場の熱気は、コロナ前の決勝戦を彷彿させるものでした🔥 特に展示者同士の「これ裏側どうなってるの?」「そのセンサの使い方は考えつかなかった!」などとコミュニケーションが弾むあの時間は、 何にも代えられないほど楽しい時間だな改めて感じました。 特に懐中電灯ロボットの偏光板の使い方には、皆さん興味を持って頂いたようで、 作成したメンバーは楽しそうに仕組みを説明していました。 C1: @ daiking1756さん 光の向きで操縦するマイコンレスカーがすごい! 偏光板を使うことでCdsの抵抗値のバランスを崩すことでモータを駆動させている仕組み 目から鱗でかなりちゃんと操縦できることに驚き #ヒーローズリーグ pic.twitter.com/e3JRx6pGfA — パスコンパス (@pscmps) 2024年12月7日 x.com ブラウザから操作できる猫型ロボット 操作画面のアニメーションが3Dモデルをフラットに表現しているの好き #ヒーローズリーグ pic.twitter.com/IXZO0bpCfn — 妄想発明家ZAWAWORKS (@zawa_works) 2024年12月7日 x.com その他、イベント当日の様子はXにて #ヒーローズリーグ で検索するとたくさんヒットするので、ぜひご覧ください。 おわりに ロボット部の部員仲間と本イベントに参加できたことがとても良い経験になりました。 参加したロボット部の部員からも下記のような感想が挙がりました。 技術イベントに参加するのがご無沙汰だったのでとても刺激になった。久々に何か私的に作ってみたい! 個性的な作品はもちろん、作った人の話を直接聞けるのがとても面白かった。ロボットカフェもたくさんの人に楽しんでもらえて良かった。 さまざまな発想の作品を見たり聞いたりできて楽しかった。自分も自由な発想でモノづくりを楽しんでいきたいと思った。 社内サークルって、活動内容だけでなく、色んな人が持っている技術や文化やコミュニティが入り混じる、 いわゆる 越境 が面白さの醍醐味だと思いますが、 今回はそれを存分に味わえた気がしています。 今後も社内外のイベントで作った作品をドシドシ展示していきます🙌 また、本イベントの準備は3月頃からスタートしましたが、過去の経験に加えて、 ドコモアカデミー という 半年間の社内研修を終えた直後でフッ軽なマインドになれていたことも、今回の取り組みに結びついたと実感しています。 私自身はさまざまなバックグラウンドの人とわちゃわちゃするのが好きなので、越境による化学反応を楽しみつつ、これからも仕事に励んでいきます。 最後に、自分のキャリアや人生に大きな影響を与えたMA/HLというイベントに、今回会社として関われたことが何より幸せでした。 それでは明日の記事もお楽しみに〜👋
この記事は、  NTT Communications Advent Calendar 2024  17日目の記事です。 みなさんこんにちは、イノベーションセンターの平木です。 Network Analytics for Security 1 (以下、NA4Sec)プロジェクトのメンバーとして活動しています。 この記事では、利用終了ドメイン名観測の環境構築の知見を共有していきます。 明日の記事では観測されたログの分析結果を紹介したいと思います。 利用終了ドメイン名観測分析施策の背景と目的 工夫した点 DNSクエリのみならずWebアクセスログも取得する パブリッククラウドとマネージドサービスを利用する 構成 大量アクセスを捌くコツ コスト まとめ 利用終了ドメイン名観測分析施策の背景と目的 昨今、廃止したドメイン名がドロップキャッチ 2 され被害にあうケースが多発しています。 NTTドコモグループでも、過去にドコモ口座で使用していたドメイン名が廃止後にオークションにかけられた事案がありました 3 。 ドロップキャッチされることで、フィッシングサイトなどで悪用されるリスクがあります。そこで、これらドロップキャッチの被害を最小限に抑えるために、NTTコミュニケーションズでは利用終了したドメイン名を永年保有する方針で運用を開始しました。 ただ一方で、組織が使用していないドメイン名を保有し続けることはインターネットの健全性に悪影響を及ぼす可能性があります。 そこで、利用終了ドメイン名の観測を通し、実際にどのような通信を受けているのか、そしていつ手放すことができるのか、また手放すために通信を減らすことはできないかなどを検討すべく、2024年に利用終了ドメイン名の観測分析の施策を立ち上げる運びとなりました。 工夫した点 DNSクエリのみならずWebアクセスログも取得する ドメイン名の観測のため、当然DNSクエリログを収集しますが、同時にWebアクセスログも収集することにしました。 Webアクセスログも収集するようにした理由は、アクセス元やアクセス用途を特定し、より効果的な対策につなげるためです。 DNSクエリログは情報量が少なく、それ単体では誰がどこから何の用途でアクセスしてきたかを推し量ることは困難です。 そこでDNSクエリ結果の主たる用途の1つとして考えられるWebアクセスに着目し、WebサーバへのHTTP/HTTPSアクセスも収集できる形を要件としました。 さらに、より幅広くWebアクセスログを収集するため、サブドメイン名も対象としました。 サブドメイン名は、Passive DNS 4 から取得したサブドメイン名を登録することにしました。 パブリッククラウドとマネージドサービスを利用する 今回AWSを使いやすい状況が整っていたこと、またコストを抑えすぐに利用できる点も勘案し、AWSで観測環境を構築することにしました。 一方、分析・ログ保管については、社内リソース有効活用の観点で全社データ基盤を採用しています。 また、運用の手間を減らしつつもセキュリティを担保できるよう、マネージドサービスを使うことにしました。 Amazon EC2などでサーバを立てて構築した場合に比べ、マネージドサービスの方がAWS側のセキュリティ対応範囲が広くなるため、運用の手間が減るというメリットがあります。 構成 構成の全体像は下記の通りです。 送信元として、サービスが終了したが消し忘れられたリンク(残存するリンク)や、消し忘れの監視通信(残存監視通信)、 そしてインターネット全体で定常的に発生している探索通信などを想定しています。 それら通信をAWSのAmazon Route53、Amazon CloudFrontで受け、ログをJSON化しています。 JSON化されたログは、全社データ基盤に保存され、全社データ基盤のJupyter Notebookを使って分析できるようになります。 なお、送信元に対しては「何もページを返さない」(Access Denied)形を取ることで、 Amazon CloudFrontの利用料金を月数十円以下と低く抑えて運用しています。 DNSクエリログ、Webアクセスログ処理の詳細な流れは以下の通りです。 Amazon Route 53がDNSクエリを受ける DNSクエリログがAmazon CloudWatch Logsに蓄積される(4.の処理の負荷分散のため、分散して蓄積) ログは1日1回、Amazon EventBridgeのCreateExportAPIにより、Amazon S3バケットにExportされる Exportされたタイミングで、AWS Lambda関数が立ち上げられ、JSON化される(AWS Lambdaの設定により、Amazon S3のファイル生成をトリガーに、AWS Lambdaを立ち上げている) 全社データ基盤がJSON化されたファイルを1日1回取得する Amazon CloudFrontがWebアクセスを受ける WebアクセスログがAmazon S3へ出力される(Amazon CloudFrontの設定により、ログをAmazon S3へ出力している) Amazon S3にログが生成されると、AWS Lambda関数が立ち上げられJSON化される(AWS Lambdaの設定により、Amazon S3のファイル生成をトリガーに、AWS Lambdaを立ち上げている) 全社データ基盤がJSON化されたファイルを1日1回取得する 大量アクセスを捌くコツ 今回の構成では、AWS Lambda関数の作りが最もキーになったと感じています。 当初は入力としてAWS Lambda関数1処理当たり数行〜数百行程度のデータを想定していましたが、観測するドメイン数が増えると、数万行のデータも現れ、タイムアウトやエラーが増えていきました。 これらタイムアウトやエラーに対応すべく試行錯誤を重ね、いくつかのAWSサービスを試したものの、最終的にはAWS Lambda関数(Pythonプログラム)の改良が、高速性やエラーの低減に最も効いたと感じています。 特にAWS Lambda関数を作るに当たっては「無駄な処理をなくすこと」が一番のポイントだったと考えています。 メモリに読み込むファイルは最小限にする(例えば全行は読み込まず、必要な数行のみ読み込む) ファイルをフィルタするにあたって、テキストのままフィルタする(JSON化後にvalueでフィルタしてしまうと、入力データが増えるにつれ重くなるため) コスト 全体のコストのうち、AWSの運用にかかるコストを紹介します。 2024年5月〜10月までのAWS利用料金を可視化したのが下図(左)、アクセス数を可視化したのが下図(右)となります。 下図(左)について、横軸が月、縦軸が月当たりのAWS利用料金となっていて、システムが安定しドメイン名が一定数(24)で推移した7〜10月の料金が安定していることが分かります。この時期の料金は概ね15000円前後となっています。 下図(右)について、横軸が月、縦軸がアクセス数となっていて、ドメイン名を増やしたタイミング(丸をつけた箇所)で、アクセス数が増えたことが分かります。 また、より詳細なコスト分析が以下の図です。 コストの主因はAWS Lambda、Amazon Route 53、Amazon S3であることが分かります。 AWS Lambdaについては、アルゴリズムが最適化されておらず金額が高くなっていました。 そこで、11月末にアルゴリズムを改善したところ、大幅な高速化に成功しています。 そのため、11月以降のAWS Lambdaによる支出は、大幅に低減される見込みです。 Amazon Route 53については、DNSレコード維持費が75%を占めています。 コストの内訳は、DNSZone75%、DNSQuery25%でした。 つまり、75%がDNSレコードの維持費用ということが分かります。 Amazon S3については、全社データ基盤の連携コストがほとんどを占めていると考えています。 コストの内訳は、API Request99.3%、Storage0.7%でした。 API Requestの中身は分かりませんが、全社データ基盤からのJSONファイルの取得によるものと考えています。 まとめ 利用終了ドメイン名の観測分析施策の、観測環境について振り返りました。 明日の記事では、この環境で収集したログの分析結果について紹介する予定です。お楽しみに。 NA4Secプロジェクトについては、このブログの記事  サイバー脅威インテリジェンス(CTI)配信はじめました  をご覧ください。 ↩ ドロップキャッチについては、JPNICの ドロップキャッチとは にて分かりやすく説明されています。 ↩ ドコモ口座の件については、ITmedia NEWSの 「ドコモ口座」のドメイン、ドコモが取り戻す 出品の経緯をGMO含め聞いた をご覧ください。 ↩ Passive DNSについては、エヌ・エフ・ラボラトリーズ株式会社のエンジニアブログ サブドメイン名列挙の方法についてまとめてみた にて分かりやすく説明されています。 ↩
この記事では、 NTT Communications Advent Calendar 2024 16 日目の記事です。本記事では MLflow という実験管理 OSS を Google Cloud の Vertex AI Experiments に置き換えを検討してみた話について記載しています。 はじめに 結論 話題の中心となる実験管理機能 浮き彫りになった課題 パフォーマンス面 セキュリティ面 Vertex AI Experiments によるアプローチ 検討の中でぶち当たった壁 前提知識の整理 テスト実行のメタデータの扱い方 API コールの上限 考察と今後 さいごに はじめに こんにちは、イノベーションセンターの林です。普段はノーコード AI 開発ツールである「 Node-AI 」というプロダクトでソフトウェアエンジニアとして開発に携わっています。また、SRE・オブザーバビリティ・Google Cloud といった領域も好きで趣味で勉強していたりします。 Node-AI は AI 開発ツールということもあり、アプリケーションの構成の中に機械学習系のタスクを担うコンポーネントも存在しています。その中で MLflow と連携しているエンドポイントがあり、その部分において運用続ける中で課題が浮き彫りになりました。 本記事では、この課題に対するアプローチとして同じく実験管理機能を提供する Google Cloud の Vertex AI Experiments に置き換える検討をしたことについてまとめていきます。 結論 簡単に本記事の結論をまとめると、 現時点での MLflow の利用方法では Vertex AI Experiments に置き換えるのは厳しいという結果でした 。 Google Cloud のリソースはものによって上限が割り当てられていてサービスの過度な負荷を防いでいます。Vertex AI Experiments というサービスにも実験情報の記録や取得について上限が割り当てられています。 検証の中で単純なユースケースにも関わらず、この上限に当たってしまいエラーが発生する事態に陥りました。上限は申請によって緩和できますが、実際のユースケースを想定したときに多少緩和しただけでは同様のエラーに陥ることが考えられました。 これは Node-AI の MLflow の利用の仕方が起因しており、 本来の想定されている利用の仕方から逸脱していることが根本原因だと考えています 。まずは、その点の見直しから整理していく必要があると結論に至りました。 以降では、実験管理機能の説明、 MLflow・Vertex AI Experiments の紹介、検証する中でぶち当たった壁、その壁に取り組んでみての考察と今後という流れで執筆しています。 話題の中心となる実験管理機能 前提知識として、実験管理について説明します。実験管理の目的は、実験 = モデルの評価状況を記録することで後から比較・モデル選定を可能とすることです。一般的に管理対象となるのは、下記の要素が挙げられるようです。 コードバージョン:どのようなコードで実験をしたか データバージョン:どのようなデータで学習・テストをしたか ハイパーパラメータ:ハイパーパラメータとして何を使用したか メトリクス:実験の結果、どのような結果が得られたか 環境:実際に動作したときのマシンとしてどのようなものを使用していたか つまるところ、実験を再現するための情報とその結果の記録が主要な機能と言えそうです。 この 実験管理機能を提供している代表的な OSS が MLflow となります。MLflow にはこの実験管理機能に加えて、記録した精度を比較しやすくする可視化機能、記録した情報に紐づくモデル保存機能なども提供しています。 引用: https://mlflow.org/docs/latest/getting-started/quickstart-2/index.html#compare-the-results 浮き彫りになった課題 Node-AI では上述した通り、機械学習系のタスクを担うコンポーネントが存在しており、一部のエンドポイントで MLflow と連携しています。連携方法としては、データベースに対する取得・保存のためのインターフェースとして MLflow の実験管理に関する API を利用しています。 この MLflow は導入当初問題なく利用できていたのですが、運用を続ける中でユーザー数の増加や学習規模の増大によって以下のような課題が顕著となりました。ここでは 2 つ取り上げたいと思います。 パフォーマンス面 Node-AI ではモデル作成時に複数回の学習を実行します。この複数回の学習結果をニアリアルタイムで取得し UI 上に描画したいがために、学習量ごとの保存に加えて 1 秒間に 3~4 回の取得の API コールをしています。 この利用の仕方では書き込み及び読み取りの処理が遅く、学習開始から完了までの一連のリクエストのボトルネックになっています。これは同時に学習を開始するユーザー数が多いほど、負荷は増大してパフォーマンスの劣化を引き起こします。 セキュリティ面 クリティカルなものとして、定期的に検出される脆弱性によって緊急のアップデート作業が必要となる点です。下記は、昨年末に検出された脆弱性の例です。 機械学習のライフサイクルを管理するプラットフォーム「MLflow」に3件の脆弱性が明らかとなった。いずれも「クリティカル(Critical)」とレーティングされている。 サーバ上にファイルを配置することが可能となるパストラバーサルの脆弱性「CVE-2023-6015」が判明。また認証なしにファイルを上書きし、リモートよりコマンドを実行してデータやモデルにアクセスが可能となる「CVE-2023-6018」が明らかとなった。 さらに「REST API」の認証をバイパスしてアカウントの作成が可能となる「CVE-2023-6014」が判明している。 CVE番号を採番した脆弱性情報報告サイトのhuntrでは、共通脆弱性評価システム「CVSSv3.1」のベーススコアを「CVE-2023-6015」「CVE-2023-6018」のいずれも最高値である「10」とした。また「CVE-2023-6014」は「9.1」と評価している。重要度はいずれも「クリティカル(Critical)」。 引用: https://www.security-next.com/151212 こういった側面に加えて、OSS 特有のセルフでのメンテナンスコストの高さも相まって運用の負荷を増大させる要因となっています。 Vertex AI Experiments によるアプローチ 私自身が Google Cloud を好んでいることと実験管理機能を提供しているという 2 点から Vertex AI Experiments を置き換えの検討候補としました。 Vertex AI Experiments に関する説明は、公式ドキュメントより引用します。 Vertex AI Experiments は、さまざまなモデル アーキテクチャ、ハイパーパラメータ、トレーニング環境を追跡および分析し、テストの実行の手順、入力、出力を追跡する際に役立つツールです。Vertex AI Experiments では、テストデータセットに対して、トレーニングの実行中に、モデルの集計のパフォーマンスを評価することもできます。この情報を使用して、特定のユースケースに最適なモデルを選択できます。 引用: https://cloud.google.com/vertex-ai/docs/experiments/intro-vertex-ai-experiments?hl=ja 上述した一般的な要素に加えて実行順序や関連したオブジェクトの紐付けといったところも可能になっているようです。 Vertex AI Experiments に期待した点としては、 Google マネージドなサービスというところでのセキュリティ面の課題からの解放およびコンピューティングスペックのオートスケーリングによるパフォーマンス面の課題からの解放という点 でした。(検討開始当初は、前者は間違いなく実現できると思っていましたが、後者は要検証項目となっていました。) 検討の中でぶち当たった壁 検討は大きく下記の手順で進めました。 Vertex AI Experiments の理解 現状の実装における MLflow の利用の仕方の理解 現状の実装の MLflow を Vertex AI Experiments で置き換えられるかの検証 また、検討過程は Architecture Decision Record(ADR) として GitHub Issue にまとめました。こちらの記事執筆にも非常に役立っています。 Vertex AI Experiments の理解 では、主に公式ドキュメントの読み込みと用意されているサンプルコードを触ったりでどういった特性があるのか、利用の仕方ができるのかなどを確認しました。サービスによるのですが、公式ドキュメントに Colab や Colab Enterprise ですぐに環境を立ち上げてサンプルコードを実行できるリンクが用意されていたりします。 引用: https://cloud.google.com/vertex-ai/docs/experiments/intro-vertex-ai-experiments?hl=ja 上図内の「Colab Enterprise で開く」をクリックすると、Google Cloud Console に遷移して Vertex AI の Colab Enterprise が開きます。 現状の実装における MLflow の利用の仕方の理解 では、自分自身今までに触ったことがないコンポーネントだっためチームのテックリードにコードを解説してもらいながら理解を進めました。 それらの手順を経て、 現状の実装の MLflow を Vertex AI Experiments で置き換えられるかの検証 に進みました。Vertex AI Experiments は MLflow と一部互換性があり、どの程度現状のコードを置き換えられるかという点にフォーカスして進めました。 そこでぶち当たった壁 = 課題について 2 点紹介します。 前提知識の整理 実際の課題について説明する前に単語の認識を合わせたいと思います。Vertex AI Experiments の公式ドキュメントを参考にします。 1 回の学習記録を 「テスト実行」 と呼びます。テスト実行にはその学習で設定したハイパーパラメータ・学習結果のメトリクス・その他の環境値などが記録されます。コード上では ExperimentRun というオブジェクトで表現されます。 テスト実行には、ユーザー定義の指標、パラメータ、実行、アーティファクト、Vertex リソース(たとえば、PipelineJob)を含めることができます。 引用: https://cloud.google.com/vertex-ai/docs/experiments/intro-vertex-ai-experiments?hl=ja#experimentrun また、上記のテスト実行の集合を 「テスト」 と呼びます。コード上では Experiment というオブジェクトで表現されます。 テストは、ユーザーが入力アーティファクトやハイパーパラメータなどのさまざまな構成をグループとして調査できるパイプライン実行に加えて、一連の n 個のテスト実行を含むことができるコンテキストです。 https://cloud.google.com/vertex-ai/docs/experiments/intro-vertex-ai-experiments?hl=ja#experiment テスト実行のメタデータの扱い方 最初の大きな課題となったのは、 テスト実験のメタデータの扱い方 です。Node-AI の実装の中では、特定のタイミングのテスト実験結果を取得して描画する画面が存在します。 学習方法はクロスバリデーションの有無やパラメータ探索の有無などいくつかの組み合わせが存在する上に、学習完了後にまとめて描画するのではなく ニアリアルタイムに描画していることから特定のタイミングの結果を取得する必要がありました 。これには、 テスト実行にメタデータを付与して高速に検索する ことで対応しています。 これを実現するために、MLflow の API の中でも set_tags() を利用しています。その他にも利用しているものもありますが、主だったものとして log_metrics() と log_params() というものがあります。それぞれの簡単な用途が下記になります。 log_metrics():学習毎の評価制度を記録する log_params():学習毎のパラメーターを記録する set_tags():学習毎のメタデータを記録する 上述した通り、Vertex AI Experiments は MLflow に一部互換性があり、これらの API を置き換えられると検証がスムーズに進むため大きなポイントでした。しかし、そんなに甘いことはなく テスト実行のメタデータを記録する set_tags() のみ実装されていませんでした。 この課題は、Vertex AI Experiments を扱う Python ライブラリである google-cloud-aiplatform がどのような構造になっているかを読み解くことで解決しました。 こちらにまとめるとボリュームが 1.5 倍になりそうなので、詳細は「 Vertex AI Experiments の実態 - コードを辿った先にあったもの - 」という資料と「 Vertex AI Experiments をコードから読み解いてみた 」というブログにまとめています。前者の資料はここで述べている課題にフォーカスした内容で登壇した際のものです。後者のブログは実際にどのようにコードを読み解いたのかという内容になっていますので、興味ある方は両方を合わせて読んでいただけると幸いです。 ここでは、その結論のみ述べます。コードを読み解くと Vertex AI Experiments は Vertex ML Metadata というメタデータ管理サービスの実験管理観点でのラッパーであること がわかりました。Vertex AI Experiments の Experiment オブジェクトや ExperimentRun オブジェクトの実態は、Vertex ML Metadata の Context というオブジェクトに紐づいていました。 このことをきっかけに Context オブジェクトを読み解くと、 Context オブジェクトは自らのメタデータを更新するメソッドが実装されており、こちらを利用することでテスト実行のメタデータを扱えました 。結果として課題だった、 set_tags() の置き換えを実現できました。 API コールの上限 上述の一部互換課題を解決したことで、MLflow の置き換え検証を進めることができました。置き換え最中は何度も実行と修正を繰り替えすので、実験管理機能を使用する操作の規模は最小限のものとしていました。 一通りコードの置き換えが完了したので、実験管理機能を使用する操作の規模を徐々に大きくしていたところ下記のエラーが発生しました。 ERROR 429 Quota exceeded for quota metric 'Resource management (CRUD) requests' and limit 'Resource management (CRUD) requests per minute per region' of service 'aiplatform.googleapis.com' for consumer 'project_number:***'. reason: "RATE_LIMIT_EXCEEDED". 少し解説すると、 aiplatform.googleapis.com とは Vertex AI 周りの API エンドポイントのドメインを指します。AI Platform は Vertex AI の前身のサービス名だったと思います。 Quota exceeded for quota metric 'Resource management (CRUD) requests' ということなので、 リソースに対する操作の指標が上限に達したと言われています。 Vertex AI に限らずですが、Google Cloud のリソースにはものによっては上限が割り当てられています。Vertex AI に関しては、 こちら のページから各種上限を確認できます。この中でも リソース管理(CRUD)リクエストの 1 分間あたりのリクエスト数が 600 というものが上述のエラーに該当します。 Google Cloud では、割り当てを使用して公平性を確保し、リソースの使用量と可用性の急増を抑えます。割り当ては、Google Cloud プロジェクトで使用できる Google Cloud リソースの量を制限します。割り当ては、ハードウェア、ソフトウェア、ネットワーク コンポーネントなど、さまざまなリソースタイプに適用されます。 引用: https://cloud.google.com/vertex-ai/docs/quotas?hl=ja 今回でいえば、 テスト実行を検索して取得する部分でこちらのエラーが発生していることがわかりました 。上述した通り学習を実行した際、ニアアリルタイムにテスト実行の結果を取得するために秒間 3~4 回ほどの取得リクエストを投げています。この操作時間が長ければそれだけリクエスト数が増加することとなります。 結果として、 この操作が Vertex AI Experiments におけるテスト実行情報の取得について想定を超えるような使い方となっており、API コールの上限に達したということ でした。 こちらの上限は申請すれば緩和させることができるのですが、今回のような検証レベルのユースケースでこの上限に達してエラーが出たということは、複数人が同時に該当の操作をするような実際のユースケース場合では上限を緩和させてもすぐに達してしてしまうことが考えられました。 考察と今後 上述した検証レベルのユースケースで API コールの上限に達したという点について、 現状の Node-AI の MLflow への API コールの仕方は想定外の使い方であることが考えられました 。だからこそ、MLflow がパフォーマンスがボトルネックになっているとこいうことにつながっているのかもしれません。 こちらの検討で現状の Node-AI の該当の実装があまりイケていないことが改めてわかりました。今後取り得る選択肢としては、 API コールを減らすような実装の見直ししたのちに MLflow を Vertex AI Experiments に置き換えるといったところでしょうか 。 バックエンドの API コールの減少は、フロントの振る舞いも加味して調整する必要があるので、慎重に進める必要がありそうです。 さいごに 今回 Node-AI で利用している MLflow のパフォーマンス面とセキュリティ面の課題に対して Google Cloud の Vertex AI Experiments というサービスの置き換えを検討しました。 検討の中で、テスト実行のメタデータの扱いと API コールの上限の 2 つの課題にぶつかり、前者は解決できたものの後者はクリティカルなものでした。この検討の考察から取り得る選択肢を洗い出すといったとこまでを本記事でまとめました。 かなり長くなってしまいました。ここまでお読みいただいた方、ありがとうございました! 明日以降のブログにもご期待ください!
この記事は、 NTT Communications Advent Calendar 2024 14日目の記事です。 脆弱性対応の分野で注目度が高まりつつあるSSVCの概要と、その運用方法について紹介します。 脆弱性対応の課題 公開される脆弱性の増加 実際に悪用される脆弱性は一部に過ぎない 脆弱性の悪用までが高速化 CVSSによる脆弱性のトリアージ SSVCとは SSVCを構成する要素 ステークホルダーのロールの特定 決定すべき優先度(Decision)の特定 Decisionが取りうる値(Outcome)の定義 Outcomeの算出に使う入力の決定(Decision Points) Outcomeの算出方法の決定(Policy) 要素のまとめ 既定のDecision Modelの活用 Deployer Decision Model SSVCの活用 組織に合わせたModelの選択 SSVC運用のデータの取得 まとめ こんにちは。イノベーションセンターの志村( @mshim03 )です。 普段はMetemcyber PJというチームでSBOMを利用した脆弱性対応の研究開発の取り組みや、Network Analytics for Security(通称NA4Sec) PJで攻撃インフラの解明・撲滅に関する技術開発、自組織のセキュリティ運用に取り組んでいます。 本記事では、脆弱性対応の文脈で注目度が高まっているSSVCとは何か、どのような問題を解決するのか、どのように利用すべきかについて紹介します。 本記事では、サーバやソフトウェアが依存するソフトウェア(パッケージ)で公開されている脆弱性情報を特定し、パッチ対応などを行うことを脆弱性対応と呼称します。 脆弱性対応の課題 サービスを開発、運用する上では、利用しているソフトウェアなどで発見された脆弱性に対応していくことが欠かせません。 公開された脆弱性に対してパッチ適用を実施したり、適切な設定変更や緩和策を適用するなど、悪用を防ぐための活動が必要になります。 しかし現在の脆弱性対応には、以下のような課題が存在しています。 公開される脆弱性の増加 CVE-ID (CVE:共通脆弱性識別子のID) が付与された脆弱性の数は年々増加傾向にあります。 2023年の公開数29,066件に対し、2024年は37,514件 (12/9現在) の脆弱性がすでに公開されており、急速に脆弱性公開数が増加していることがわかります。 CVE Details より 実際に悪用される脆弱性は一部に過ぎない 大量の脆弱性が公開される一方で、実際に悪用される脆弱性の数はわずかです。 悪用が確認された脆弱性は全体の6%ほどだったという調査結果が存在しています。 1 脆弱性の悪用までが高速化 実際に悪用される脆弱性は一部ですが、攻撃者にとって有用な場合は即座に攻撃が開始される傾向にあります。 例えば、2024年に発生したPalo Alto Networks製品の脆弱性は、4月12日に情報が公開され、4月14日時点ですでに国内での攻撃が観測されていました。 2 すなわち、昨今の脆弱性を取り巻く環境は以下のような状態といえます。 脆弱性の数は増加しており、大量の脆弱性情報が届けられる 一方で大半の脆弱性は悪用されない ただし攻撃者にとって有用な脆弱性な場合、即座に対応しないと被害を受ける蓋然性が高い そのためすべての脆弱性に対応するというよりも、対処が必要な脆弱性を素早く特定・対応することが必要になっています。 このような活動は脆弱性のトリアージと呼ばれることもあります。 CVSSによる脆弱性のトリアージ トリアージを実施する際の問題は「何を基準に優先順位を決定するのか」という点があります。 悪用された際の影響の大きさや、悪用がどの程度考えられるかといった観点で優先度を考える必要があります。 脆弱性の危険度を表す指標としてはCVSS(共通脆弱性評価システム)が有名です。 CVSSは脆弱性に関するオープンな評価指標であり、悪用時の影響や攻撃の前提条件などをもとに0~10.0 までの値で算出されます。 一方でCVSSは攻撃の技術的評価をスコアリングしたものであり、意思決定に利用するには以下のような問題があると指摘されています。 脆弱性対応でよく使われるCVSS基本スコアは脆弱性を技術的観点で評価したもので、悪用可能性などは考慮していない 具体的な意思決定までサポートがされない CVSSスコアがある値より上なら即座に修正すべき、といった基準を与えるものではない CVSSスコアは高いものの実際には悪用困難で攻撃が発生していない脆弱性が存在するなど、CVSSスコアのみをトリアージに使うのは現実的でないことが多いです。 そのような状況の中、脆弱性対応の意思決定をサポートする概念としてSSVCが注目されています。 SSVCとは SSVC (Stakeholder-Specific Vulnerability Categorization)は、カーネギーメロン大学ソフトウェア工学研究所によって提案された手法です。 脆弱性管理に関わるステークホルダー (脆弱性に対するパッチを作成する開発者、脆弱性に対するパッチを適用するソフトウェア利用者など)のニーズに基づいて脆弱性に優先順位をつけ、対応順を明確にできる方法論となっています。 SSVCは以下のドキュメントで詳細が紹介されています。 https://certcc.github.io/SSVC/ SSVCは、Decision Tree(決定木)を用いて、数値ではなく脆弱性対応の優先度を出力します。 そのため最終的に算出度される優先度が明確であり、なぜその優先度が算出されたかを即座に理解できます。 以下はSSVCが用意している、ソフトウェア利用者(Deployer)向けの決定木(一部抜粋)です。 四角で囲まれたDecision Points(後述)の値によって判定が分岐し、最終的に1つの優先度が出力されます。 以下の例ですと、 「Exploitation(脆弱性の悪用状況)」が「PoC(悪用コードのPoCが存在)」、「Exposure(システムのネットワーク接続状態)」が「controlled(制限されている)」、 「Automatable(脆弱性の悪用を自動化できるか)」が「no」、「Human Impact(脆弱性の影響)」が「low」 ならば、最終的に「defer(現時点では脆弱性に対応する必要はない) 」という結果が出力されます。 SSVCの決定木の一部: 3 SSVCを脆弱性管理に活用しようという動きは近年増しており、IPAが発行する 脆弱性対応における リスク評価手法のまとめ で言及されるなど、存在感を高めています。 SSVCを構成する要素 SSVCの方法論を構築する要素を、 SSVC Howto ページをベースに紹介します。SSVCを利用して優先度を算出するためには、決定しなければならない要素がいくつかあります。 ステークホルダーのロールの特定 SSVCは “Stakeholder Specific” という名が表すように、脆弱性管理におけるステークホルダー、すなわちどのように脆弱性対応に関わるか、という概念が重要になります。 SSVCのドキュメントでは、以下のようなステークホルダーの立場が想定されています。 Supplier ソフトウェア開発者。脆弱性が発見された時にパッチを作成する役割を担う Deployer ソフトウェア利用者。提供されたパッチを環境に適用する役割を担う Coordinator CERTなどの立場が想定されている。脆弱性対応を統制したり、脆弱性情報の展開などをする役割を担う。 決定すべき優先度(Decision)の特定 脆弱性対応への関わり方によって、決定すべき優先度は変わります。 例えばSupplierが算出すべき優先度は、「どの順序で脆弱性のパッチを作成・提供すべきか」になりますし、Deployerであれば「どの順序で脆弱性パッチを適用するか」になります。 このように、ステークホルダーが決定すべき優先度を、SSVCではDecisionと呼びます。 Decisionが取りうる値(Outcome)の定義 Decisionの取りうる値が定義されている必要があります。 SSVCではこれをOutcomeと呼びます。 SSVCが提供するDeplolyer向けのModelでは、以下のようなOutcomeが定義されています。 Defer 現時点で対応しない。 Scheduled 定期的なメンテナンスで対応する。 Out-of-cycle 定期的なメンテナンスは別に、早期に軽減策か修正策を適用する (次の機会か、必要ならば作業時間外での実施)。 Immediate 脆弱性に即時対応する。すべてのリソースをできるだけ早く脆弱性の修正に集中させる。必要であれば組織の通常の運用を停止させる。 優先度はImmediateが最も高く、Deferが最も低いです。 Outcomeの算出に使う入力の決定(Decision Points) どのような情報をもとにOutcomeを算出するかを決定します。 この入力をDecision Pointsと呼びます。 Decision Pointsの例としては、脆弱性の悪用状況、脆弱性の対象となるシステムのネットワーク接続状態などがあります。 SSVCではさまざまなDecision Pointsを定義しています。 以下ページより参照できます。 https://certcc.github.io/SSVC/reference/decision_points/ Outcomeの算出方法の決定(Policy) Decision PointsからOutcomeを算出する方法を決定します。 これをPolicyと呼びます。PolicyはDecision Pointsを入力として受け取り、Outcomeを返す関数と考えることができます。 SSVCはPolicyの表現として決定木を使うことが一般的です。 決定木を活用することで、各Decision PointsがどのようにOutcomeの算出に使われるのか明確になります。 要素のまとめ SSVCを構成する要素をおさらいします。 Stakeholder Decision Outcome Decision Points Policy SSVCを構成するこれらの要素をまとめた概念をDecision Modelと呼びます。 SSVCを用いた意思決定をする場合は、自組織のニーズにあった適切なDecision Modelを決定し、それを運用に落とし込む必要があります。 既定のDecision Modelの活用 SSVCを利用するために、Decision Modelを一から構築するのは難易度が高いです。 そのためSSVCは、あらかじめSupplier、Deployer、Coordinatorのステークホルダー向けのDecision Modelを提供しています。 それぞれ Supplier Decision Model 、 Deployer Decision Model 、 Coordinator Decision Models 4 と呼ばれています。 Deployer Decision Model 最も多くのユーザに関係するであろう、Deployer Decision Modelについて紹介します。 このModelでは、Stakeholderは「Deployer」、Decisionは「パッチを適用する優先度」になります。 Outcomeは前述したDefer、Scheduled、Out-of-cycle、Immediateです。 Policyは以下の決定木で表されます。 この決定木はDeployer Treeと呼称されます。 Deployer Tree: 5 Deployer Treeは以下のようなDecision Pointsを有しています。 脆弱性自体の情報と、脆弱性対応の対象となる資産の情報が主に含まれています。 Exploitation 現時点での脆弱性の悪用状況。時間と共に変化しうる PoCの有無や、実際の攻撃が悪用されているかによって判断される System Exposure システム・サービスのアタックサーフェース(攻撃可能な境界)のアクセス可能状態 インターネットなどからアクセス可能か、アクセスが制限されているかなどによって決定される Automatable 脆弱性を利用した攻撃が自動化可能か(人間のインタラクションがなくても実行可能か) Human Impact その脆弱性が及ぼす影響の大きさを表す。Safety Impact と Mission Impactから算出される 6 Safety Impact 脆弱性が安全性に与える影響 Mission Impact 組織のミッションに不可欠な機能へ与える影響 SSVCの活用 SSVCを実際に活用する際のポイントを2つ紹介します。 組織に合わせたModelの選択 Deployer Decision Modelをそのまま採用するには、資産管理を適切に実施し、資産の外部への露出状況や用途をデータとして継続的に収集する必要があります。 これは組織の規模や状態によっては難しいことがあります。 そのような場合、既存のModelをカスタマイズしたり、組織のポシリーに合致するようなDecisionやDecision Pointsを採用することも可能です。 7 SSVCのドキュメントでは、組織のリソースに応じて以下のように、段階的にDecision Modelを成長させていく例が示されています。 https://certcc.github.io/SSVC/howto/acuity_ramp/#an-acuity-ramp-in-action CISA KEV 8 を利用 Exploitationを利用 Exploitation, System Exposureを利用 Exploitation, System Exposure, Automatable を利用 Exploitation, System Exposure, Automatable, Mission Impact, Safety Impact を利用 SSVC運用のデータの取得 SSVCはDecision Points のためのデータをどのように収集するかは定義していません。 ( Reference ページに目安となる方法は紹介されています。) 組織はDecision Pointsの決定に必要なデータを収集し、適切にマッピングする方法を決定する必要があります。 Deployer TreeのDecision Modelを利用する場合、収集すべき情報は主に脆弱性自体の情報と、ソフトウェアをデプロイしている環境の情報になります。 脆弱性自体の情報としてExploitationやAutomatableなどの情報がありますが、これらはCISAが情報を提供しています。 この情報は Vulnrichment GitHub Repository や、CVEの CISA ADP から参照できます。 9 CISA ADPは、 CVE Website で提供される脆弱性ページから参照できます。 10 View JSON を選択すればJSON形式で取得可能で、プロセスの自動化に役立てられます。 11 環境情報の収集は資産管理と深く紐づいており、組織の資産管理のデータをDecision Pointsに反映する方法を定義する必要があります。 またShodanなどの公開デバイスの検索サービスや、ASM(Attack Surface Management)サービスを利用してのデータ収集も可能でしょう。 まとめ SSVCを活用することで、脆弱性対応の優先度を算出し、組織の対応順序を明確にできます。 また利用するデータとその影響が明確になり、改善しやすい脆弱性対応が可能になります。 SSVCを活用することで何を決定すべきか、そのためにどんな情報が必要かを明確にできます。 意思決定プロセスが明確になることで、改善も容易になっています。 NTTコミュニケーションズではSSVCとSBOMを組み合わせて脆弱性対応の意思決定をサポートするツールを開発しています。 SBOM、SSVCというワードに興味がある方はぜひチェックしてください。 https://github.com/nttcom/threatconnectome それでは、明日の記事もお楽しみに! https://jp.tenable.com/blog/epss-shows-strong-performance-in-predicting-exploits-says-study-from-cyentia-and-first ↩ https://www.jpcert.or.jp/pr/2024/PR_Report2024Q1.pdf ↩ https://github.com/CERTCC/SSVC/blob/679610194d4062d5638aeb6df6d0f561a0c415c9/docs/pdf/ssvc_2_deployer_SeEUMss.pdf ↩ 複数形になっているのは、Coordinator向けのModelは2つ存在しているからです。受け取った脆弱性レポートに基づいて調整を実施するかを決定する Triage に関するModelと、脆弱性を公開するかを決定する Publication に関するModelです。 ↩ https://github.com/CERTCC/SSVC/blob/679610194d4062d5638aeb6df6d0f561a0c415c9/docs/pdf/ssvc_2_deployer_SeEUMss.pdf ↩ Human Impactを脆弱性ごとに算出することが現実的かについては議論があります。 FutureVuls Blog などをご参照ください。 ↩ 独自のDecision PointsやDecisionを採用する例は、SSVCの Prepare to Use SSVC に記載されています。業界規制やSLAをDecision Pointsとして採用する事例が紹介されています。 ↩ CISA KEV は、CISAが公開している悪用が確認された脆弱性のカタログです。 ↩ Authorized Data Publisher(ADP) は、認定された組織がCVEレコードに追加情報を付与できる仕組みです。詳細はCVE Websiteの ADPページ を参照ください。 ↩ ブログ内で紹介したPalalto脆弱性(CVE-2024-3400)の場合、次のURLでCISA ADPを含んだ情報が公開されています: https://www.cve.org/CVERecord?id=CVE-2024-3400 ↩ CVE-2024-3400の場合、次のURLからJSON形式で取得できます: https://cveawg.mitre.org/api/cve/CVE-2024-3400 ↩
この記事は、 NTT Communications Advent Calendar 2024 13 日目の記事です。 フロントエンドのインフラ構成を見直すことで運用コストを 99%削減できた事例についてご紹介します。 目次 目次 はじめに 見直しの背景 これまでの環境 課題 実現方法 工夫した点 動的コンテンツを表示させる 設定の自動化 変更後の効果 おわりに はじめに こんにちは、 NeWork 開発チームの栄です。 普段はオンラインワークスペースサービス NeWork の開発エンジニアをしています。 この記事では、これまで NeWork で利用していたフロントエンドのインフラ構成を見直した結果、運用コストを 99%削減できた事例をご紹介します。 見直しの背景 これまでの環境 NeWork のサービス基盤は Google Cloud 上で提供しており、フロントエンドは Google App Engine (以下、App Engine) を利用しています。 課題 この運用を続けてきてきましたが、App Engine の運用コストが高いという課題がありました。 利用者ごとに異なる動的なコンテンツを配信するために App Engine を利用していましたが、App Engine はインスタンスの起動時間に応じて課金がされるサービスです。 起動していない時間は料金が発生しませんが、NeWork では利用人数や利用頻度からパフォーマンス面を考慮して常時複数台のインスタンスを起動していました。 そのため、毎月かなりのコストがかかっており、NeWork が利用する Google Cloud の料金の多くを占めるのがこの App Engine の料金でした。 実現方法 上述の背景を考慮してインフラ構成見直しの検討を始めたところ、Firebase Hosting というサービスが候補にあがりました。 1 Firebase Hosting は Firebase 社が提供するサービスの 1 つで、ウェブアプリを公開できるウェブホスティングサービスです。 静的ウェブアプリや単一ページウェブアプリの配信に適しており、CDN(コンテンツ配信ネットワーク)にキャッシュが保存されることでコンテンツを高速に配信できます。 料金に関しては利用時間に応じた課金体系ではなく、下記項目の実際の使用量に応じて課金がされるのですが、あらかじめ無料枠が用意されており、無料枠のしきい値を超える使用量に対してのみ課金がされます。 コンテンツの保存に必要なストレージ容量 エンドユーザーに転送されたデータの量 無料枠で十分なストレージ容量があること、NeWork は 1 つの画面で大半の操作を完結できるのであまり多くのデータ量が転送されないと想定されたため、料金を大きく削減できると見込んで Firebase Hosting への移行を決定しました。 工夫した点 動的コンテンツを表示させる 通常 Next.js では動的コンテンツを配信するために Node.js サーバーが必要になるのですが、Firebase Hosting へ移行する場合 Node.js サーバーを利用できないため、すべて静的コンテンツ(HTML/CSS/JavaScript)に変換してから利用する必要があります。 そのため、Next.js の Static Exports というウェブアプリを静的コンテンツとしてエクスポートできる機能を利用して Firebase Hosting に設定をしてみました。 しかし、その場合は動的コンテンツを配信できなかったため、契約者ごとに表示内容が異なるページをうまく表示ができませんでした。 例:本来は契約者ごとに利用できるルームの数が違うはずなのに、全ての契約者のルーム数が同じになってしまう。 もう少し詳細に調べたところ、Firebase Hosting にはあらかじめ正規表現を定義しておき、パターンに一致する URL に対して適切なページを表示できるリライト機能がありました。 この機能を利用することで、動的コンテンツの配信に対応ができました。 設定の自動化 しかし、新たなページが増えるごとに URL パターンを手動で追加して管理するのは保守性に優れません。 何か手立てがないか調べたところ、Next.js がビルド時に生成する routes-manifest.json というファイルに動的コンテンツのページ URL と正規表現のセットで出力されることがわかりました。 // routes-manifest.jsonファイル ※一部のみ記載 { " version ": 3 , " pages404 ": true , " basePath ": "", " dynamicRoutes ": [ { " page ": " /contractors/[[...contractorId]] ", // ページURL " regex ": " ^/contractors(?:/(.+?))?(?:/)?$ ", // ページURLの正規表現 " routeKeys ": { " contractorId ": " contractorId " } , " namedRegex ": " ^/contractors(?:/(?<contractorId>.+?))?(?:/)?$ " } // 以下省略 ] // 以下省略 } そこでスクリプトを作成して自動で管理できるようにすることで、設定の抜け漏れや間違いが発生しないようにしました。 // わかりやすさを考慮して、エラー処理等は省略しています。 const fs = require ( "fs" ) ; const path = require ( "path" ) ; // JSONファイルを読み込む関数 const readJsonFile = ( filePath ) => { const content = fs . readFileSync ( filePath , "utf-8" ) ; return JSON . parse ( content ) ; } ; // Next.js の Dynamic Routes を Firebase Hosting の rewrites で利用できる形に変換する関数 const generateRewrites = ( dynamicRoutes ) => { return dynamicRoutes . map (({ regex , page }) => ({ regex , destination : ` ${ page } .html` , })) ; } ; const main = () => { // 1. routes-manifest.jsonファイルを読み込む const routesManifestPath = process . argv [ 2 ] ; // 2. Firebase Hosting で利用する rewrites ルールを生成 const { dynamicRoutes } = readJsonFile ( routesManifestPath ) ; const rewrites = generateRewrites ( dynamicRoutes ) ; // 3. あらかじめ作成したFirebase Hosting の設定ファイルテンプレートに rewrites ルールを結合 const firebaseTemplatePath = path . join ( __dirname , "firebase.template.json" ) ; const firebaseConfig = readJsonFile ( firebaseTemplatePath ) ; firebaseConfig . hosting . rewrites = rewrites ; // 4. Firebase Hosting の設定ファイルを出力 const firebaseConfigPath = path . join ( __dirname , "firebase.json" ) ; fs . writeFileSync ( firebaseConfigPath , JSON . stringify ( firebaseConfig , null , 2 )) ; } ; main () ; 最終的には下記のような設定ファイルが作成されることになります。 // firebase.json ※一部のみ記載 { " hosting ": { " target ": " web ", " public ": " public ", " cleanUrls ": false , " trailingSlash ": false , " ignore ": [ " firebase.json ", " **/.* ", " **/node_modules/** " ] , " headers ": [ // 以下省略 ] , " rewrites ": [ { " regex ": " ^/contractors(?:/(.+?))?(?:/)?$ ", " destination ": " /contractors/[[...contractorId]].html " } // 以下省略 ] } } 変更後の効果 課題にあった「 App Engine のランニングコストが高い」ですが、劇的に改善される結果となりました。 見直し前の料金: 月額およそ 380,000 円 見直し後の料金: 月額およそ 1,400 円 そのため、年額換算するとおよそ 4,400,000 円のコストを削減しており、インフラ構成見直し前と比較すると 99%以上のコストを削減できる結果となりました。 おわりに この記事では、インフラ構成を見直すことで運用コストを大幅に削減できた事例についてご紹介しました。 今回はサービス全体の中のフロントエンド部分のみの変更となりましたが、他にも改善できるところはあると感じているので、継続してコスト改善を検討できればと考えています。 サービスに使用している技術の制約などで必ずしもこのような取り組みができるとは限りませんが、運用コストを大きく削減できた一例として皆さまのご参考になれば幸いです。 現在 NeWork はどなたでも無料でお試しできますので、もしプロダクトや使われている技術に興味を持っていただけたらぜひ触ってみてください。 明日のアドベントカレンダーもお楽しみに。 Firebase Hosting 公式ドキュメント ↩
この記事は、 NTT Communications Advent Calendar 2024  12日目の記事です。 Azure Databricksを使ってレイクハウスアーキテクチャのログ基盤を構築し、 構造化されていないアプリケーションログの保管や加工、分析を試します。 はじめに レイクハウスアーキテクチャ ログ基盤とレイクハウス Azure Databricksでアプリケーションログを分析する Azure Databricksの準備 Terraformを使ったリソース作成 カタログとスキーマの作成 ログの取り込み ログの加工 BronzeからSilver SliverからGold ログの分析 (可視化) まとめ 参考文献 はじめに こんにちは、コミュニケーション&アプリケーションサービス部の吉仲です。 2022年度に入社し、初期配属からメール系システムと文書要約APIの開発・運用業務に取り組んでいます。 今回は業務から少し離れ、自身が興味のあるデータエンジニアリングの分野を題材にします。 本記事では、Azure Databricksを使ってレイクハウスアーキテクチャのログ基盤を構築し、 そこへ非構造化ログを取り込み、データの加工やダッシュボードでの可視化を試していきます。 本記事に含まれる内容は以下の通りです。 なぜログ基盤をレイクハウスアーキテクチャにしたいか Azure Databricksの構築 (Terraformを使用) Delta Live Tablesを使ったパイプラインの実装 サンプルログを地図上にマッピングして可視化するダッシュボードの作成 レイクハウスアーキテクチャ レイクハウスとは、 Databricks が提唱している新しいタイプのデータマネジメントアーキテクチャです。 www.databricks.com 簡単に言うと、レイクハウスはデータ レイク とデータウェア ハウス の良いとこ取りをしたアーキテクチャです。 レイクハウスは、構造化データから非構造化データまでを一元的かつ効率的に扱うことができ、 ビジネスインテリジェンス (BI) とAI/機械学習のどちらにも対応します。 ログ基盤とレイクハウス ログ基盤を構築する目的として、ログの 保管 と 分析 の2つがあると思います。 この保管と分析を 低コスト かつ 効率的 に実現できる点に、ログ基盤でレイクハウスアーキテクチャを取るメリットがあると考えます。 各種のアプリケーションやシステムにおいて、全てが構造化ロギングを行っているとは限りません。 例えば、OSSのメール転送エージェント (MTA) であるpostfixでは、メール配送時に以下のようなログを出力します。 見ての通りjsonやxml形式で出力されておらず、なおかつ1回のメール配送の情報が複数行に点在しています。 Dec 12 08:13:10 mtaserver postfix/smtpd[1180]: connect from example.com[192.0.2.1] Dec 12 08:13:10 mtaserver postfix/smtpd[1180]: ABCDEF01: client=example.com[192.0.2.1] Dec 12 08:13:10 mtaserver postfix/cleanup[1187]: ABCDEF01: message-id=<message.id@mtaserver.example.com> Dec 12 08:13:11 mtaserver postfix/smtpd[1180]: disconnect from example.com[192.0.2.1] ehlo=1 mail=1 rcpt=1 data=1 quit=1 commands=5 Dec 12 08:13:11 mtaserver postfix/qmgr[264]: ABCDEF01: from=<from@example.com>, size=2200, nrcpt=1 (queue active) Dec 12 08:13:11 mtaserver postfix/smtp[1738]: ABCDEF01: to=<to@example.net>, relay=example.net[192.0.2.2]:25, delay=0.3, delays=0.2/0/0.07/0.03, dsn=2.0.0, status=sent (250 2.0.0 Ok: queued as ABCDEF01) Dec 12 08:13:11 mtaserver postfix/qmgr[264]: ABCDEF01: removed このようなログは未加工のままでは扱いが難しいため、加工して構造化したログを用意した上で分析します。 ただし、加工によって少なからず情報が抜け落ちてしまうことがあります。 将来的に、加工方法や分析目的が変化/追加されることを考慮すると、未加工のログ (生ログ) も保管しておきたいです。 データレイクとデータウェアハウスを組み合わすことで、生ログの保管と加工済みログの分析を効果的に実現できます。 しかし、この構成ではデータを二重で持つことになり、データ保存コストが高くなりやすいです。 ここで、レイクハウスの出番です。 データレイク : さまざまなデータを低コストで保管できる データウェアハウス : 高度な分析を効率的に行える この両方の強みを兼ね備えたレイクハウスにより、生ログの保管と加工済みログの分析を 低コスト かつ 効率的 に実現できると考えます。 また、後述するマルチレイヤーのデータ設計により、加工方法や分析目的の変化/追加に対して柔軟で拡張性の高い基盤を実現できます。 Azure Databricksでアプリケーションログを分析する ここでは、 Azure Databricks を使ってログ基盤としてのレイクハウスを構築し、サンプルのログを分析してみます。 具体的には、前述のpostfixサンプルログを保管し、分析するところまでをAzure Databricksで試します。 なお、ログ基盤を構築・運用する上ではログの 収集 も非常に重要なポイントですが、今回はフォーカスしません。 簡単のため、データはAzure Databricksに直接アップロードすることとします。 Azure Databricksの準備 まずはじめに、以下のような構成でAzure Databricksを構築します。 リソースグループの中に以下のリソースを作成します。 仮想ネットワーク ネットワークセキュリティグループ Azure Databricksワークスペース ワークスペースを作成すると、自動的に専用のリソースグループと以下のリソースが作成されます。 ストレージアカウント (Azure Data Lake Storage Gen2, ADLS2) Azure Databricks用のアクセス コネクタ マネージドID Databricks上のデータマネジメント機能 Unity Catalog の外部ロケーションとしてADLS2を登録している状態が出来上がります。 1 Terraformを使ったリソース作成 今回は Terraform を使ってリソースを作成していきます。 前提は以下の通りです。 Azureで「共同作成者」ロールが割り当てられたアカウントを保持していること Azure CLI およびTerraformを実行できる環境があること Azure CLIで上記のアカウントにログイン済みであること こちら を参考に、 Azureのリソースグループと仮想ネットワーク、Azure Databricksワークスペースを定義します。 今回はリソースの名前に "databricksdemo" というprefixを付けます。 Terraformコード terraform { required_providers { azurerm = "~> 4.0" random = "~> 3.6" databricks = { source = "databricks/databricks" version = "1.59.0" } } } data "azurerm_client_config" "current" {} data "external" "me" { program = [ "az" , "account" , "show" , "--query" , "user" ] } locals { subscription_id = "<AzureサブスクリプションID>" prefix = "databricksdemo" tags = { "Environment" = "Demo" "Owner" = lookup (data.external.me.result, "name" ) } region = "japaneast" cidr = "10.179.0.0/20" } provider "azurerm" { subscription_id = local.subscription_id features {} } provider "databricks" { host = azurerm_databricks_workspace.this.workspace_url } resource "azurerm_resource_group" "this" { name = "$ { local.prefix } -rg" location = local.region tags = local.tags } resource "azurerm_virtual_network" "this" { name = "$ { local.prefix } -vnet" resource_group_name = azurerm_resource_group.this.name location = azurerm_resource_group.this.location address_space = [ local.cidr ] tags = local.tags } resource "azurerm_network_security_group" "this" { name = "$ { local.prefix } -nsg" resource_group_name = azurerm_resource_group.this.name location = azurerm_resource_group.this.location tags = local.tags } resource "azurerm_subnet" "public" { name = "$ { local.prefix } -public" resource_group_name = azurerm_resource_group.this.name virtual_network_name = azurerm_virtual_network.this.name address_prefixes = [ cidrsubnet (local.cidr, 3 , 0 ) ] delegation { name = "databricks" service_delegation { name = "Microsoft.Databricks/workspaces" actions = [ "Microsoft.Network/virtualNetworks/subnets/join/action" , "Microsoft.Network/virtualNetworks/subnets/prepareNetworkPolicies/action" , "Microsoft.Network/virtualNetworks/subnets/unprepareNetworkPolicies/action" , ] } } } resource "azurerm_subnet_network_security_group_association" "public" { subnet_id = azurerm_subnet.public.id network_security_group_id = azurerm_network_security_group.this.id } resource "azurerm_subnet" "private" { name = "$ { local.prefix } -private" resource_group_name = azurerm_resource_group.this.name virtual_network_name = azurerm_virtual_network.this.name address_prefixes = [ cidrsubnet (local.cidr, 3 , 1 ) ] delegation { name = "databricks" service_delegation { name = "Microsoft.Databricks/workspaces" actions = [ "Microsoft.Network/virtualNetworks/subnets/join/action" , "Microsoft.Network/virtualNetworks/subnets/prepareNetworkPolicies/action" , "Microsoft.Network/virtualNetworks/subnets/unprepareNetworkPolicies/action" , ] } } } resource "azurerm_subnet_network_security_group_association" "private" { subnet_id = azurerm_subnet.private.id network_security_group_id = azurerm_network_security_group.this.id } resource "azurerm_databricks_workspace" "this" { name = "$ { local.prefix } -workspace" resource_group_name = azurerm_resource_group.this.name location = azurerm_resource_group.this.location sku = "premium" managed_resource_group_name = "$ { local.prefix } -workspace-rg" tags = local.tags custom_parameters { no_public_ip = true virtual_network_id = azurerm_virtual_network.this.id private_subnet_name = azurerm_subnet.private.name public_subnet_name = azurerm_subnet.public.name public_subnet_network_security_group_association_id = azurerm_subnet_network_security_group_association.public.id private_subnet_network_security_group_association_id = azurerm_subnet_network_security_group_association.private.id } } data "databricks_node_type" "smallest" { local_disk = true depends_on = [ azurerm_databricks_workspace.this ] } data "databricks_spark_version" "latest_lts" { long_term_support = true depends_on = [ azurerm_databricks_workspace.this ] } resource "databricks_cluster" "this" { cluster_name = "$ { local.prefix } -cluster" node_type_id = data.databricks_node_type.smallest.id spark_version = data.databricks_spark_version.latest_lts.id data_security_mode = "SINGLE_USER" autotermination_minutes = 10 spark_conf = { "spark.databricks.cluster.profile" : "singleNode" , "spark.master" : "local[*]" , "spark.databricks.unityCatalog.volumes.enabled" : "true" , } custom_tags = { "ResourceClass" : "SingleNode" } } output "databricks_host" { value = "https://$ { azurerm_databricks_workspace.this.workspace_url } /" } Terraform コマンドにより、上記で定義したリソースを作成します。 terraform init terraform plan terraform apply # 途中で "yes" を入力 # ... # Outputs: # databricks_host = "https://<AzureDatabricksワークスペースのURL>/" リソースグループ <prefix>-rg (今回は databricksdemo-rg ) の中に以下のリソースが作成されます。 <prefix>-nsg <prefix>-vnet <prefix>-workspace また、リソースグループ <prefix>-workspace-rg が自動的に作成され、その中に以下のリソースが作成されます。 dbmanagedidentity dbstorage<randam-string> unity-catalog-access-connector 以上で準備は完了です。 terraform apply 実行後に表示されるDatabricksワークスペースのURLをクリックし、Databricks UIを開きます。 カタログとスキーマの作成 Databricksでは、 メダリオンアーキテクチャ というマルチレイヤーのデータ設計が推奨されています。 具体的な説明はここでは省略しますが、このアーキテクチャにより、加工方法や分析目的の変化/追加に対して柔軟になります。 メダリオンアーキテクチャの実装方法については こちら を参考にし、以下のような設計とします。 demo というカタログを作成し、その中にBronze/Sliver/Goldレイヤー用のスキーマを作成します。 bronze :postfixの生ログをそのまま保存 silver :各行から必要な情報 (client IP等) を抽出し、構造化したログを保存 gold :複数行に跨るメール配送の情報を結合し、分析用に整備したログを保存 上記のカタログとスキーマは、以下の操作により作成します。 サイドバーで [カタログ] をクリック カタログ一覧の右上の [⚙]>[外部ロケーション] をクリックし、外部ロケーションのURLを控える サイドバー上部の [新規]>[ノートブック] から新しいノートブックを作成して以下を実行 (適宜、クラスターの起動とノートブックのアタッチを実施) %sql CREATE CATALOG demo MANAGED LOCATION ' <外部ボリュームのURL>/demo ' ; CREATE SCHEMA demo.bronze MANAGED LOCATION ' <外部ボリュームのURL>/demo/bronze ' ; CREATE SCHEMA demo.silver MANAGED LOCATION ' <外部ボリュームのURL>/demo/silver ' ; CREATE SCHEMA demo.gold MANAGED LOCATION ' <外部ボリュームのURL>/demo/gold ' ; 【参考】スキーマ作成後の状態 以上でカタログとスキーマの作成は完了です。 ログの取り込み 次に、Databricks UI上で前述のpostfixサンプルログをBronzeレイヤーに取り込みます。 なお、本格的なログ基盤を構築する場合は、Azureの Event Hubs や Data Factory 等を使ってデータソースからログを取り込みます。 以下の操作でボリュームとディレクトリを作成し、サンプルログをアップロードします。 先ほどのノートブックを開き、以下のクエリを実行 %sql CREATE VOLUME demo.bronze.maillog; サイドバーの [新規]>[データを追加またはアップロード] をクリック サンプルログを maillog というファイルとしてアップロードし、 送信先パスに /Volumes/demo/bronze/maillog/postfix/2024-12-12 を入力して [アップロード] をクリック 同様に、日時等を変えたサンプルログ②を /Volumes/demo/bronze/maillog/postfix/2024-12-13 にアップロードしておきます。 サンプルログ② Dec 13 18:15:30 mtaserver postfix/smtpd[1181]: connect from example.com[192.0.2.3] Dec 13 18:15:30 mtaserver postfix/smtpd[1181]: ABCDEF02: client=example.com[192.0.2.3] Dec 13 18:15:30 mtaserver postfix/cleanup[1188]: ABCDEF02: message-id=<message.id@mtaserver.example.com> Dec 13 18:15:31 mtaserver postfix/smtpd[1181]: disconnect from example.com[192.0.2.3] ehlo=1 mail=1 rcpt=1 data=1 quit=1 commands=5 Dec 13 18:15:31 mtaserver postfix/qmgr[265]: ABCDEF02: from=<from@example.com>, size=2200, nrcpt=1 (queue active) Dec 13 18:15:31 mtaserver postfix/smtp[1739]: ABCDEF02: to=<to@example.net>, relay=example.net[192.0.2.2]:25, delay=0.5, delays=0.4/0/0.07/0.03, dsn=2.0.0, status=sent (250 2.0.0 Ok: queued as ABCDEF02) Dec 13 18:15:31 mtaserver postfix/qmgr[265]: ABCDEF02: removed アップロードしたサンプルログをDatabricksで確認してみます。 先ほどのノートブックで以下を実行します。 %sql SELECT * from read_files( " /Volumes/demo/bronze/maillog/postfix/2024-12-*/maillog " , format => " text " ) Databricks上で、保管したサンプルログの中身を表示できるようになりました。 ただし、このままではログの検索や分析が難しいため、データ加工により構造化ログを作成していきます。 ログの加工 ここでは、 Delta Live Tables (DLT) パイプラインを使ってログを加工することで、 BronzeからSilver、そしてGoldレイヤーにデータをインジェストしていきます。 BronzeからSilver Bronzeレイヤーに保存した生ログを構造化してSilverレイヤーにインジェストします。 postfixログの構造化のために、 Logstash で使われるGrokという正規表現と、PythonでGrokを扱うための pygrok を利用します。 postfixログ用のGrokパターンは以下のものを使います。 これはあくまでサンプルログ用のパターンで、実際のログを構造化するためにはより複雑なパターンが必要になります。 postfixサンプルログ用のGrokパターン # common patterns PROCESS ([\w._\/%\-]+) PROGRAM (postfix[\w\-.]*) PROCESS_AND_PID %{PROGRAM:program}\/%{PROCESS:process}(?:\[%{NUMBER:pid}\])? QUEUEID (?:[0-9A-F]{6,}|[0-9a-zA-Z]{12,}|NOQUEUE) EMAIL_ADDRESSPART [a-zA-Z0-9_.+-=:~]+ EMAIL_ADDRESS %{EMAIL_ADDRESSPART}@%{EMAIL_ADDRESSPART} RELAY (?:%{HOSTNAME:relayhost}(?:\[%{IP:relayip}\](?::[0-9]+(.[0-9]+)?)?)?) CLIENT (?:%{HOSTNAME:clienthost}(?:\[%{IP:clientip}\](?::[0-9]+(.[0-9]+)?)?)?) POSREAL [0-9]+(.[0-9]+)? STATUS sent|deferred|bounced|expired|delivery RESPONSECODE [0-9][0-9][0-9] DSN %{NONNEGINT}.%{NONNEGINT}.%{NONNEGINT} # postfix/smtp POSTFIX_SMTP %{QUEUEID:qid}: to=<%{EMAIL_ADDRESS:to_addr}>, relay=<?%{RELAY}>?, delay=%{POSREAL:delay}, delays=%{DATA:delays}, dsn=%{DSN:dsn}, status=%{STATUS:result} \(%{DATA:reason}\) # postfix/smtpd POSTFIX_SMTPD %{POSTFIX_SMTPD_CLIENT}|%{POSTFIX_SMTPD_CONNECTS} POSTFIX_SMTPD_CLIENT %{QUEUEID:qid}: client=<?%{CLIENT}>? POSTFIX_SMTPD_CONNECTS (?:dis)?connect from %{CLIENT}(?: %{DATA:command_counts})? # postfix/cleanup POSTFIX_CLEANUP %{QUEUEID:qid}: (resent-)?message-id=<?%{DATA:messageid}>? # postfix/qmgr POSTFIX_QMGR %{QUEUEID:qid}: (?:removed|from=<(?:%{EMAIL_ADDRESS:from_addr})?>(?:, size=%{NUMBER:size}, nrcpt=%{NUMBER:nrcpt} \(%{GREEDYDATA:queuestatus}\))?) # aggregate all patterns POSTFIX_PREFIX (?:%{SYSLOGTIMESTAMP:timestamp}|%{TIMESTAMP_ISO8601:timestamp}) (?:%{SYSLOGFACILITY} )?%{SYSLOGHOST:logsource} %{PROCESS_AND_PID}: POSTFIX_LOG %{POSTFIX_PREFIX} (?:%{POSTFIX_SMTP}|%{POSTFIX_SMTPD}|%{POSTFIX_QMGR}|%{POSTFIX_CLEANUP}) このファイルを前述の手順で demo.bronze の管理ボリュームの other/grok/postfix.grok にアップロードしておきます。 続いて、DLTパイプラインを作成します。 サイドバーから [Delta Live Tables] を開き、画面右上の [パイプラインを作成] をクリックします。 以下の内容で設定後、[作成]をクリックします。(他はデフォルトでOK) パイプライン名: databricksdemo-bronze-to-silver 製品エディション: Core パス: (空のまま) ストレージオプション: Unity Catalog カタログ: demo ターゲットスキーマ: silver クラスターモード: 固定サイズ ワーカー: 1 ワーカータイプ: Standard_D3_v2 作成完了後、パイプライン詳細画面の右サイドバーからソースコード (ノートブック) のURLを開きます。 開いたノートブックに以下のソースコードを記載します。 # 依存関係インストール %pip install pygrok== 1.0 . 0 regex== 2024.11 . 6 # Grokパターンの定義 import json from pygrok import Grok grok_path = "/Volumes/demo/bronze/other/grok/" grok_pattern = "^%{POSTFIX_LOG}$" grok = Grok(grok_pattern, custom_patterns_dir=grok_path) # スキーマ、データ変換処理の定義 from pyspark.sql.functions import ( col, concat, current_date, from_json, lit, month, pandas_udf, regexp_extract, to_timestamp, year, ) from pyspark.sql.types import StringType, StructField, StructType keys = [ "timestamp" , "logsource" , "program" , "process" , "pid" , "qid" , "clienthost" , "clientip" , "messageid" , "command_counts" , "from_addr" , "size" , "nrcpt" , "queuestatus" , "to_addr" , "relayhost" , "relayip" , "delay" , "delays" , "dsn" , "result" , "reason" , ] schema = StructType([StructField(k, StringType(), True ) for k in keys]) @ pandas_udf ( "string" ) def parse_grok_udf (log_line): return log_line.apply( lambda x: json.dumps({k: v for k, v in grok.match(x).items() if v is not None }) ) # Bronzeレイヤーからのデータ読込み (一時ビュー作成) from datetime import datetime import dlt volume_path = f "/Volumes/demo/bronze/maillog/postfix/{datetime.now():%Y-%m}-*/maillog" # デモのため月次バッチ @ dlt.view () def raw_data (): return spark.read.format( "text" ).load(volume_path) # Silverレイヤーのテーブル作成 this_year = year(current_date()) dt_format = "yyyy MMM dd HH:mm:ss" @ dlt.table ( name= "maillog_postfix" , table_properties={ "quality" : "silver" }, ) def structured_data (): return ( dlt.read( "raw_data" ) .withColumn( "parsed" , from_json(parse_grok_udf(col( "value" )), schema)) .select( "parsed.*" ) .withColumn( "timestamp" , to_timestamp(concat(lit(this_year), lit( " " ), col( "timestamp" )), dt_format)) .sort(col( "timestamp" ).asc()) ) DLTパイプラインのクラスター databricksdemo-bronze-to-silver に接続後、[検証] をクリックします。 検証が完了して画面下部にグラフが描画された後、[起動] をクリックしてパイプラインを実行します。 すると、以下のように demo.silver.maillog_postfix というテーブルが作成されます。 作成されたテーブルは以下の通りです。 今回はGrokパターンを作り込んでいないので、完全には構造化できていない部分もあります ( command_counts や reason など)。 この部分も含めた構造化の難易度はそれほど高くないように思いますが、今回はここまでにします。 SliverからGold Goldレイヤーには、分析の目的に合わせたテーブルを作成します。 ここでは、メール送信もしくは受信の接続元IPを地図上にマッピングし、 異常な接続元の有無を可視化するような分析を実施していきます。 Goldレイヤーにテーブルを作成するために、以下の内容を実施します。 GeoLite2 から以下のCSVファイルをダウンロードし 2 、Databricksに取り込む GeoLite2-City-Blocks-IPv4.csv GeoLite2-City-Locations-en.csv 複数行に跨るメール配送の情報を結合 (1キューID/1レコードの状態にする) GeoLite2を使って、各メール配送ログのクライアントIPの地理情報を取得 1.について、postfixサンプルログやGrokパターンファイルと同様の手順で、 demo.bronze.other.geolite2 にCSVファイルをアップロードします。 その後、以下のコードを実行して demo.silver にテーブルを作成します。 ( spark.read.format( "csv" ) .option( "Header" , True ) .option( "inferSchema" , True ) .load( "/Volumes/demo/bronze/other/geolite2/<csv_file>" ) # CSVファイルを指定 .write.format( "delta" ) .mode( "overwrite" ) .saveAsTable( "demo.silver.<table_name>" ) # テーブル名を指定: `geolite2_city_blocks_ipv4` or `geolite2_city_locations_en` ) 2.について、前述の手順と同様にDLTパイプライン databricksdemo-silver-to-gold を作成し、 以下のソースコードを記載した上でパイプラインを実行します。 なお、地理情報のマッピング方法は こちら を参考にさせていただきました。 # IPアドレスと地理情報のマッピングのための変換処理の定義 import ipaddress as ip from pyspark.sql.functions import pandas_udf, col @ pandas_udf ( 'long' ) def to_network_address (cidr): return cidr.apply( lambda x: int (ip.IPv4Network(x).network_address)) @ pandas_udf ( 'long' ) def to_broadcast_address (cidr): return cidr.apply( lambda x: int (ip.IPv4Network(x).broadcast_address)) @ pandas_udf ( 'long' ) def to_address_int (cidr): return cidr.apply( lambda x: int (ip.IPv4Address(x))) # GeoLite2のテーブルの読み込み df_ip_blocks = spark.table( "demo.silver.geolite2_city_blocks_ipv4" ) df_locations = spark.table( "demo.silver.geolite2_city_locations_en" ) # 1キューID/1レコードに結合したビューの作成 import dlt from pyspark.sql.functions import collect_list, first from pyspark.sql.types import DateType @ dlt.view () def concatenated_data (): return ( spark.table( "demo.silver.maillog_postfix" ) .alias( "df" ) .groupBy(col( "timestamp" ).cast(DateType()).alias( "date" ), "logsource" , "qid" ) .agg( first( "timestamp" ).alias( "timestamp" ), first( "program" ).alias( "program" ), collect_list( "process" ).alias( "processes" ), first( "clienthost" , ignorenulls= True ).alias( "clienthost" ), first( "clientip" , ignorenulls= True ).alias( "clientip" ), first( "messageid" , ignorenulls= True ).alias( "messageid" ), first( "from_addr" , ignorenulls= True ).alias( "from_addr" ), first( "to_addr" , ignorenulls= True ).alias( "to_addr" ), first( "result" , ignorenulls= True ).alias( "result" ), first( "reason" , ignorenulls= True ).alias( "reason" ), ) .filter(col( "qid" ).isNotNull()) ) # Goldレイヤーのテーブル作成 @ dlt.table ( name= "maillog_postfix_with_locations" , table_properties={ "quality" : "gold" }, ) def maillog_postfix_with_locations (): return ( dlt.read( "concatenated_data" ) .hint( "range_json" , 65536 ) .join( df_ip_blocks.alias( "b" ), [ to_address_int(col( "clientip" )) > to_network_address(col( "b.network" )), to_address_int(col( "clientip" )) < to_broadcast_address(col( "b.network" )), ], "left" , ) .alias( "f" ) .join( df_locations.withColumnRenamed( "geoname_id" , "geoname_id_2" ).alias( "l" ), col( "f.geoname_id" ) == col( "l.geoname_id_2" ), "left" , ) ) パイプライン実行後は、以下のような demo.gold.maillog_postfix_with_locations テーブルが作成されます。 ログの分析 (可視化) 最後に、Goldレイヤーに作成したテーブルを使って、地図上に接続元をマッピングするダッシュボードを作成します。 なお、上で記載したpostfixサンプルログは例示用IPアドレスを使っているため、地理情報を取得できません。 以降の例では、適当なIPアドレスを使ったサンプルログでダッシュボードを作成しています。 ダッシュボード作成の手順は以下の通りです。 カタログから demo.gold.maillog_postfix_with_locations テーブルを開き、画面右上の [作成]>[ダッシュボード] をクリック 作成画面で、下部のナビゲーションから [ビジュアライゼーションを追加] をクリックしてボックスを配置 (デフォルトで配置されているビジュアライゼーションは削除してOK) 右側のメニューで、ビジュアライゼーションのパラメータを以下の通りに設定 可視化: ポイントマップ 緯度: latitude 経度: longitude 画面右上の [公開] をクリックし、アクセス許可のある人を確認した上で再度 [公開] をクリック 作成したダッシュボードは以下の通りです。 これにより、例えば社内メールが海外から送信されているといった事象を可視化できるようになりました。 本記事でのログ分析はここまでとします。 もちろん、Goldレイヤーのテーブルを使ってより高度な分析 (異常検知のモデル作成など) も可能だと思います。 まとめ 本記事では、Azure Databricksを使って非構造化ログ (postfixのサンプルログ) の保管から加工、可視化までを試しました。 Databricksでは、ログが構造化されているかを問わず、一元的なログ管理・分析が可能です。 同じUI上でパイプライン実装からデータ探索、ダッシュボード作成まで可能であり、非常に便利なプラットフォームと感じました。 また、今回は紹介しませんでしたが、Databricksにはマネージドの MLflow が組み込まれています。 レイクハウスはBI/AIの両方と親和性が高いことも強みなので、次はDatabricksを使ったMLOpsにも挑戦してみたいです。 最後までご覧いただきありがとうございました!それでは、明日の記事もお楽しみに! 参考文献 https://www.databricks.com/ https://github.com/databricks/terraform-provider-databricks/ https://registry.terraform.io/providers/databricks/databricks/latest/docs https://learn.microsoft.com/ja-jp/azure/databricks/ データブリックス・ジャパン (2022)『データブリックスクイックスタートガイド』 プロジェクトによっては、自動作成されるもの以外のストレージを外部ロケーションとしてUnity Catalogに登録する方法が適している場合もあります。 ↩ MaxMind のウェブサイトでユーザー登録することで、GeoLite2のデータをダウンロードできるようになります。 ↩
はじめに 開発部署へのOJTとしての挑戦 実際の開発業務でぶつかった壁 開発規模が大きい レビュアーにとって分かりやすいコードを書けない WebUIの開発において自分の想定と違う挙動になることがある 勉強方法の確立の難しさ 学習方法を確立する必要性 学習において大事だと感じたこと IT技術を体系的に(教科書的に)学ぶことは古い 効率的な学習方法 学習の高速道路 際立った個性 高速道路の高速化 超一流・一流になるために目指すべき人物像 NTTコミュニケーションズが与える学習の機会 おわりに 補足資料:ReactとRedux Reactとは JavaScriptのみの場合 Reactを導入した場合 Reactを使ってみた個人的な感想 Reduxとは Reactのみを使った場合 Reduxを導入した場合 Reduxを使ってみた個人的な感想 参考文献 はじめに こんにちは。情報セキュリティ部SMO2G・兼務イノベーションセンターテクノロジー部門MetemcyberPJの千坂知也と申します。今年新卒入社した一年目です。よろしくお願いいたします。 MetemcyberPJではOSSコントリビューターとして開発の業務に参加し、特にWebUI側の開発業務に多く携わってきました。 もともと大学院では、Pythonを使ってシミュレーション(厳密にいうとアルゴリズムの評価関数)のコードを書いていたもののWebUI系の開発言語は書いたことがなかったため、OJT先で一から学んだことになります。 言うまでもなく超一流のエンジニアではありませんが、OJTで気づいた(あるいは自分なりに感じた)学びや参考になった学習理論について特に学生、新入社員向けにまとめてみようかと思います。もし何か感じ入るものがあれば幸いです。 開発部署へのOJTとしての挑戦 初期配属として情報セキュリティ部に所属し、7月まで新入社員として研修を受けておりました。インシデントハンドリングや脆弱性に関する体系的な知識を会得する研修が多いなか、Webアプリ開発という研修があり、もともとモノ作りが好きだったことも相まって開発業務に興味を持ったのが始まりです。 情報セキュリティ部での一通りの研修が終わったのち、OJTとして新規事業開発しているイノベーションセンターに行ってみないかと打診され、これは良い機会だということでSBOM管理ソリューション「Threatconnectome」 1 の開発チームに参加させていただきました。大学院までPythonを使ってコーディングをした経験はあったものの、WebUI系の開発言語に触れたことはほとんどなく、一からのスタートでした。 まずは、自らが望んで行ったOJT先でぶつかった壁について紹介していきたいと思います。 実際の開発業務でぶつかった壁 開発規模が大きい 実際にイノベーションセンターに来て開発業務に携わり、まず最初に感じたことは「あたりまえだがWebアプリ開発研修のときと開発規模が違いすぎる」ということ。 既存機能を修正しようと思ったものの、まず該当のファイルがどこにあるのか分からない。ようやくそれっぽいファイルを見つけてもどこが該当のコードか分からない。該当のコードを見つけてもどう修正すればよいか分からない。とりあえず周りのコードを眺めつつ何とか真似できないかと試行錯誤してみました。修正できたものの単純な修正に1日かかってしまいました(今なら15分くらいで終わる気がします)。 レビュアーにとって分かりやすいコードを書けない 大学院生のときに書いていたコードってめちゃくちゃだったんだと今更反省した記憶があります。あのレベルのコードを業務で書いていたら、製品になりません。 コードの品質をあげるための機能としてPR(Pull Request:開発者のローカルリポジトリでの変更を他の開発者に通知する機能。機能追加や改修など、作業内容をレビュー・マージ担当者やその他関係者に通知し、レビューしてもらう機能)があります。 ここで大事だと感じたのがコードをレビュー側の人間の立場になって書くということ。レビュアーが読んで理解できないコードを書いたり、そもそも読む気にならないコードを書いたりしたらその時点でアウトです。私だと条件分岐が多すぎて複雑すぎるという理由ではねかえされたことがありました。 読みやすいコードを書くって今まで意識してこなかったのですが、これがすごく難しいわけです。自分なりに読みやすく書いてみたつもりでも、これどういう意味?って言われることもしばしばあります。 WebUIの開発において自分の想定と違う挙動になることがある WebUIの開発では自分のイメージしていたものと違う挙動になることが多いと感じました。Pythonでシミュレーションのコードを書いていた時は数値計算のみを行っていたので想定通りの挙動になることがほとんどでしたが、WebUI系のコードは見た目に関わる部分を実装するため想定外の挙動になることが多くありました。例えば、ここのtoggle buttonのこの部分だけ背景色を変えたいなって思っても違う部分が変わるなどがありました。 勉強方法の確立の難しさ 上記の壁にぶつかって感じたのが、やはり知識をつけないことには始まらないということ。どうやら少しだけ調べてみると、より簡潔にコードを書く方法だったり、特殊な挙動をするコードの書き方などがあるようでした。そのような知識がないため、壁にぶつかったときの対処法の手段が乏しく、途方に暮れたことが何度もあったため、手持ちの武器を増やす必要があると感じました。 MetemcyberPJではWebUI側の開発言語としてJavaScript、フロントエンドライブラリとしてReactとReduxというものを用いているため、まずはReactやReduxなどの公式ドキュメントを見てみようと思いました。最初に感じたのは「難しい」。公式ドキュメントは分量が多いうえに全体的に同じテンションで書いてあるため、どこが重要なのかなど初見ではいまいち分かりませんでした。かといって全部読むってなると量が多すぎました(まぁ甘えですが)。 ちなみにReactとReduxに関して本記事の末尾でどんな技術なのかを簡潔にご紹介しております(詳しい内容になるととても収まらない分量になるので割愛します)。 興味のある方はご一読ください。 学習方法を確立する必要性 ReactとReduxの概要を見ると、学生さんなどで使ったことのない方だとなんのこっちゃって感じかと思います。 ただ先に述べた通り、実際にはこれよりはるかに細かい機能や理解しなければならない事柄を製品は多く含み、 かつそれを適切な箇所で適切に利用することで「コードの品質」は向上し、製品として成り立ちます。 少なくとも超一流と呼ばれるエンジニアの方々は当たり前のようにそれらを実践しています(もちろん、開発言語がJavaScriptとは限りませんが)。 さて、では私のように全くコードを書いた経験のない人間はどのように技術に関して理解していかなくてはいけないのでしょうか? そもそもどのような姿勢で学んでいけばよいのでしょうか? 実をいうと学生時代の勉強方法や(多くの学生や新入社員がやりがちな)資格取得のための勉強はあまり効率的な学習方法とは言えないと考えています。 それは現在の(基礎的な)IT技術と呼ばれるものが思っている以上に広範囲であり、かつ各技術が相互に関係しているため、横断的に取得していかなければならないからです。 以上のことから私自身が効率的に知識を習得するために一から学習方法を見直す必要があると感じました。 そのなかで、参考になった学習方法や学習理論、あるいはあまり良くない学習方法と感じたものを紹介していきたいと思います。 学習において大事だと感じたこと 私自身がOJTを通して大事だと思ったあるいは非効率的に感じた学習方法や参考になった学習理論についてご紹介します。 IT技術を体系的に(教科書的に)学ぶことは古い 従来の教科書的な勉強方法はIT業界では古いかもしれないということです。 学生時代は教科書を机の上に広げて、そこに書いてある理論なり公式なりをノートに書き写して学ぶのが一般的かと思います。 資格勉強でも何百ページもある参考書を広げて大事だと思われるポイントをノートにとるなり、マーカーをつけるなりするのかもしれません。 しかしこのような勉強法でIT技術を学ぶにはいくらかの問題点があるように感じます。 まず学生時代の勉強法に関しては、「学校の先生」という強力な解説つきなのと文部科学省の検定済教科書という あらかじめ体系立てて勉強できるように内容(適切な難易度の練習問題があるなど)、分量ともに精査された教材を使っているという点です。 資格勉強に関しても単一の分野(IT分野ならネットワーク、セキュリティなど)を決められた試験範囲内の内容に絞って記述されているだけなので、 実際の業務に直結するかというと実はそうではありません。資格に関しては自身が保持している能力の証明方法の一種であり、勉強の手段としてはあまり適していないと思います。 すなわちこのような勉強法は間違っているとまでは言いませんが、超一流になるためには遠回りすぎる気がします。 事実、ReactやReduxの公式ドキュメントを読んだところで、練習問題があるわけでもないし、そもそも分量が多すぎてよほどの英才でもなければ短時間であれだけを読んでその全体概要を理解するのは困難です。 また先に述べた通りIT技術というのはそのそれぞれが相互に関係しており、複雑につながりあっています。セキュリティ製品を作っているはずなのに、RTKqueryというデータキャッシングの技術を使わなくてはいけなくなったり、alembicというデータベース移行ツールを利用する必要があったりします。つまり極めて広い分野を横断的に習得する必要があり、そのための学習方法として学生時代のやり方などは非効率的であるということです。 効率的な学習方法 さて、では超一流になるための効率的な学習方法とは何なんでしょうか。 少なくとも現段階で私が効果あったと考えているのは、真似ること、恐怖心を持たないこと、(見知らぬ)多くの人と議論を交わすことになります。 まず、真似ることに関してです。多くの開発業務の現場では(開発以外の仕事でもそうですが)いわゆる超一流の方が一定数居ます。その分野の業務に長年従事し精通している方々です。 そういった方々は少なくともそれでお金を稼ぎ、ご飯を食べ続けているのですから、それ相応の成果を出し続けている方々なわけです。 まず (プライドを捨てて) この人たちのやり方を真似ることが重要になります。 はっきりいって公式ドキュメントや資格の勉強を丸一日行うより、1時間そういった方々の業務をみて、真似て作業したほうがはるかに有益かと思います。 よく言いますが、バタフライの泳法を覚えるのに一日中Youtubeでバタフライの動画を見るより、バタフライで泳げる人の真似を1時間した方が良いのと一緒です。 こうすることで、どういった手順で業務にあたり、どういった点に留意し、どういった知識が直接的に必要なのかを体験できます。 次に恐怖心を持たない(怖気づかない)ことが重要になります。 学生の頃や新入社員の頃だと、自分の立場や能力のせいでなかなか意見を言えなかったり、質問ができないという方が多いと思います(少なくとも私はそうでした)。 「こんな初歩的で未熟な質問して見下されないか」、「あの先輩(上司)忙しそうだし質問したらダメだよな」、「こんなこと言ったら(会社内での)自分の立場が悪くなる」 みたいなノイズが入って、理解が不十分なことをそのままにしておいたりタスクを適当にこなそうとするケースが多いのでないでしょうか。 でも考えてみると、 学生に関していうとプロ(超一流、即戦力)レベルの人間なんてごくごく一部のよほど優秀な方以外いません。 なので新入社員なんて(当然私も含めて)ほぼ全員未熟です。未熟で当然なんです。 あと先輩(上司)なんて全員何時でも忙しいです。役職が上がれば上がるほど忙しいはずです。 自分なりの意見を述べて、会社内での立場が悪くなるのだったら、さっさと転職しましょう。 要するに学生だと教授、新入社員だと先輩・上司という「教師」がいるはずなのに、それを利用しないという人間が多いのだと思います。 という風に考えるとそもそもそんなノイズのために自分の理解が不十分なままなのはもったいないというか、愚策であることが分かります。 (1つ注意なのが、あくまで言葉遣いには気を付けるべきかと思います。積極的に質問、意見を通すことと口汚いことは全く別です。) 最後に多くの人と議論を交わすこと。 実際の業務遂行にフォーカスした外部の研修の受講機会や何らかのイベントへの参加を促されたら積極的に挑戦することです。 実際の開発現場にいても、普段は慣れ親しんだ方々としか会話や議論をしません。それもすでにチーム内で凝り固まりつつある価値観のもとでです。 全く出会ったこともない人と議論を交わすことで、想定もしていないような観点からの指摘や意見を受けることができ、新たな知見を得ることが多くあります (学生だと学会がこれにあたるのかもしれません)。 私自身、OJTでMetemcyberに来て、直接的に成長につながったと感じたのは上記の3つです。 ところでこのように学ぶためには実をいうとマインド(学ぶ姿勢)が重要だと考えています。 というのもそもそもの話としてある程度の学習意欲やバイタリティがないと上の3つなんて実行しうるわけがないのです。 超一流になるための学習意欲の根底部分を支えるのはマインドです。 次は、このマインドについて私なりに学ぶことが多かった学習理論を将棋と野球の話を交えてしてみようかと思います。 学習の高速道路 完全に私事なんですが将棋が好きで、さまざまな棋士の対局を見るのが趣味の1つです。棋士の中でも2017年に永世七冠を達成されたまごうことなき大レジェンド羽生善治九段はその人柄まで含めて大ファンなのですが、彼は2006年にIT企業の経営コンサルタント梅田望夫氏のベストセラー「ウェブ進化論」のなかで 「学習(知)の高速道路」という言葉を用いて現代将棋の学習環境の在り様を表現しています[1]。 これはIT技術の進歩とインターネットの普及が進んだことにより、従来は対局機会を得ることが困難であった優れた対戦相手と多くの対戦をこなす機会が得られるようになったことで 誰でも効率的な学習環境で将棋を学ぶことができる、ということを指した言葉です。 しかしその後、羽生九段は「高速道路を走り抜けた先では大渋滞が起きている」とも述べています。これは誰でも簡単に効率的な学習環境の中で学ぶことができるため、ある程度の学習を積んだもの同士の間だと、他者よりも抜きんでることが困難になっていることを表した言葉です。 そして彼は2009年に同じく梅田望夫著「シリコンバレーから将棋を観る」のなかで以下の式を示しました[2]。 $超一流 = 才能 × 対象への深い愛情ゆえの没頭 × 際立った個性$ 右辺の三要素が超一流になるため必要なものであり、なかでも 際立った個性 が重要だと羽生九段は説いています。つまり他者よりも抜きんでるためには誰にもない際立った個性が必要なわけです。そしてこの式は 将棋以外のどの分野にも当てはまる とも述べました。 際立った個性 別の例を1つだけ挙げますと、今や世界的スーパースターであるMLBのロサンゼルス・ドジャース所属の大谷翔平選手も際立った個性を持った人物と言えます。 大谷選手は近代プロ野球において極めて稀な投手と野手を兼任する、いわゆる二刀流の選手です。 当初こそ多くの野球有識者やプロ野球OBの方々は二刀流に関して反対をしていました。 しかしNPBで唯一3度の三冠王を獲得した落合博満氏は大谷選手がプロ野球選手になりたての2013年にNHKのサンデースポーツという番組にて、「二刀流をやることに大賛成。 これほどの 個性 を持った選手は少なくともここ30~40年出てきていない」と述べました。 大谷選手は言うまでもなく類まれな才能の持ち主であり、誰よりも深い野球への愛情を持ち、それでいて誰にも負けない際立った個性があったからこそ、超一流のプロ野球選手になれたのではないでしょうか。 まさに先に羽生九段が述べた超一流の式になぞらえているわけです。 高速道路の高速化 一方で羽生九段は2017年に自身著の「人工知能の核心」のなかで「学習の高速道路を走るなかで、大量の情報を得ることに追われて、かえって自分の頭で課題を解決する時間がなくなっていくことを懸念している」とも述べています[3]。 実をいうと、生成AIが出現してからこの懸念はますます深まったように思います。「AIが何でも課題解決してくれるから自分の頭で考える必要はない」と盲信している方が多いのではないでしょうか。 生成AIの出現以後、学習の高速道路はさらに高速化したように思います。 正直なことを申し上げますと、私自身もいわゆるChatGPT、GitHub Copilotなどはよく利用します。といいますか利用しない日はないといっても良いくらいです。 単純な処理を行うコードやその場に適した文脈などを素早く考えることは自分がやるより、もはやAIにやらせた方が効率的だと感じているからです。 他方で、あくまで出力結果を人間の目で精査する必要はあります。生成AIは要求や質問に対し、優れた回答を素早く出力するところまでは高い精度でできるようになっていますが、 それでも絶対ではありません。さらに言うとそもそも複雑な状況下ではその要求や質問自体ができないケースもあります(AIに正確な指示を与え、期待する出力を得るためのプロンプトを設計・改善するプロンプトエンジニアリングというのはありますが)。 そのため、「AIが何でも課題解決してくれるから自分の頭で考える必要はない」という盲信は些か危険を帯びている気がします。 AIを完全に信じ切ってしまうと、その出力結果の正誤を判断できずに結果として想定と全く違った挙動をおこなうシステムを構築しかねません。 つまり、ここでも知識を十二分に備えたうえで出力結果の正誤を判断し、AIを適切に利用する能力を身に着ける必要があるということです。 言い換えれば、少なくとも現在のIT業界における超一流と呼ばれる人たちは巧にAIを活用できる人材であり、生成AIの使い方まで一流な人間になります。 恐らくこの能力は、IT業界に関わらず、これから先の世の中で超一流と呼ばれるために確実に保持しなくてはならないものだと思われます。 AIを盲信せず、むしろAIを巧みに利用できる人材になる必要があるわけです。 超一流・一流になるために目指すべき人物像 よく学校の先生や会社の先輩、上司がさまざまな事柄(学校なら勉強、仕事なら今自分が従事している業務)に対して 努力・研鑽 せよ、なんて言います。でも思うのですが、上の式の右辺のいずれもが欠けている状況で誰が努力なんてできるのでしょうか? その分野に関する才能もなく、愛情や興味もなく、個性らしい個性もない状況で努力ができるとしたらきっとその人はある意味で人知を超えた強靭な精神力の持ち主だと思います(学校の勉強に関しては多くの場合において、それが強要されてしまっているのが現状ではありますが)。 そういう意味で言うと、3つとも携えるのがもちろん良いのでしょうが、そうはいかないのが現実でもあります。個人的にはできるだけ愛情、興味を持てる分野を探すこととその中で自分だけの個性を作り出すことが重要なのだと感じています。才能に関しては自分で制御できない部分が多いと思っているのが正直なところなので、せめて対象への深い愛情ゆえの没頭と際立った個性を持てるように意識してみるわけです。そうするときっと超一流とまではいかなくとも一流にはなれるのではないでしょうか? NTTコミュニケーションズが与える学習の機会 まず私が感謝したいのはOJTという形でMetemcyberPJに参加させていただいて開発業務の学びの機会を頂いたということ。 先の式において、私自身に自負できるほどの対象への深い愛情があるかは定かではありませんが、好きなこと(興味のあること)を挑戦する機会を提供してくれる会社であることは間違いないです。 1つだけ誤解しないでいただきたいのは簡単な道を選ばせてくれるというわけではないという会社ではないということです。 むしろ自ら険しい山を登ることを奨励し賛同してくれる場所だと感じました。 おわりに 非常に拙くかつ偉そうな文章となってしまいましたが、最後までお読みいただきありがとうございます。 開発系の業務に携わってみたいと思いつつも、いざやってみると難しいことだらけ。それでも今学んでいることがおぼろげながらでも自分の未来につながっているという実感を得ています。 向学心があり、好きなことをとことん突き詰めたいという方はぜひNTTコミュニケーションズの門戸を叩いてみてはいかかでしょうか。 以上で、 NTT Communications Advent Calendar 2024 11日目を終わらせていただきます。 補足資料:ReactとRedux Reactとは Reactは、UIを小さな再利用可能な「コンポーネント」という単位に分割して開発できるフロントエンドJavaScriptライブラリです[4]。 コンポーネントとはUIを構成する要素で、例えばクリックするとユーザー名を表示するという処理を行うコンポーネントなどがあげられます。 また、Reactではコンポーネントごとに「状態(state)」というものを管理し、状態が変更されるたびに自動的にUIが再描画されるという特徴があります。 ここで状態とは、簡単に言うとコンポーネントが持つデータのことです。 例えば、+のボタンを押すとカウンターの数字が1ずつ増えるという簡単なアプリを例にすると、 画面が描画されている状態ではカウンターの数字は0である必要があります。この「カウンターの数字」をReactでは「状態 (state)」と呼びます。 そして、+のボタンが1回押されるごとにカウンターの数字に1を足すという処理を行い、状態を更新する必要があります。 この状態の更新は、状態に変化があるたびに行われます。 Reactは状態とUIの同期が自動で行われるため、手動でDOM操作(ユーザーの操作に応じて動的にコンテンツを更新したりすること)を行う必要がないという利点があります。 では、ChatGPTに提案してもらったJavaScriptのみの場合と、Reactを導入した場合の簡単なコード例を見てみましょう。 JavaScriptのみの場合 <!DOCTYPE html> < html lang = "en" > < head > < meta charset = "UTF-8" > < meta name = "viewport" content = "width=device-width, initial-scale=1.0" > < title > JavaScript Only Example </ title > </ head > < body > < div > <!-- 初期状態のテキスト --> < p id = "text" > Click the button </ p > <!-- ボタン --> < button id = "button" > Click me </ button > </ div > < script > // ボタンとテキスト要素を取得 const textElement = document . getElementById ( 'text' ) ; const button = document . getElementById ( 'button' ) ; // ボタンのクリックイベント button . addEventListener ( 'click' , function () { // テキストを変更 textElement . innerText = "Hello, World!" ; }) ; </ script > </ body > </ html > JavaScriptのみの場合では、DOM操作する際、 どのタイミングでどのように変更するかを明示的に記述する必要があり、 コードが複雑になりやすいというデメリットがあります。 また、状態が変更された際にUIを手動で更新する必要があります。 // ボタンのクリックイベント button . addEventListener ( 'click' , function () { // テキストを変更 textElement . innerText = "Hello, World!" ; }) ; 本コードでは上記の部分が該当の箇所になります。巨大な開発規模になると、 このように手動でDOMを直接操作するのは非常に大きなコストとなります。 次に同じ処理を行うReactのコードを見てみましょう。 Reactを導入した場合 import React , { useState } from "react" ; function MyComponent () { const [ text , setText ] = useState ( "Click the button" ) ; const handleClick = () => { setText ( "Hello, World!" ) ; } ; return ( <div > <p > { text } < / p> <button onClick={handleClick}>Click me< / button > < / div> ) ; } このコードでは、 MyComponent という名前のコンポーネントを定義しています。 useState("Click the button")という部分にて初期値(初期状態)として「Click the button」という文字列を設定し、 textという変数にその状態を保持させます。 setTextはtextの状態を更新するための 関数 であり、 この場合ボタンをクリックしたときにhandleClickという関数が実行され、そのなかでsetTextが実行されることでtextの状態を更新しています。 今回の場合、setText("Hello, World!")にてtextの状態を「Hello, World!」に更新します。 Reactを使ってみた個人的な感想 最初こそ全く意味が分かりませんでしたが、ある程度の理解ができた後だとなるほどReactを導入したほうが確かに直感的で分かりやすい。 useStateを使った状態管理ができない状況でコードを書くというのがもはや想像できない状況ですらあります。 しかしながら、Reactだけだと各状態はそのコンポーネント内部で完結しており、親コンポーネントや他のコンポーネントと状態を共有するために プロパティ(props)を介して渡す必要があります。これは大規模な開発になればなるほど、状態管理が複雑化することを意味しています。 そのデメリットを克服したのがReduxというライブラリです。 Reduxとは Reduxは、状態をコンポーネント内ではなく、アプリケーション全体で一元管理するためのフロントエンドJavaScriptライブラリです[5]。 Redux自体はReactとは独立した別のライブラリですがReactとの相性が良く、ほとんどの場合においてReactと組み合わせて使われます。 Reduxを導入することでReactのコンポーネントは、 Reduxストア(アプリケーション全体の状態を一元管理するためのオブジェクト)というところから必要なデータを取得し、 状態を変更するためのアクションをディスパッチ(dispatch)するということを行います。 ディスパッチとはアクションをストアに送るためのメソッドのことで、アクションは状態を更新するための指示を含んだオブジェクトになります。 これをストアに送ることで、リデューサー(アクションに基づいてアプリケーションの状態を更新する関数)が実行され、新しい状態が作られます。 このようにReduxでは、状態の変更をReduxストア内のリデューサーによって管理します。 Reduxは状態のグローバル管理するライブラリということになります。 では、こちらもChatGPTに作成してもらった簡単なコードを見てみましょう。 Reactのみを使った場合 import React , { useState } from 'react' ; // Counterコンポーネント:useStateを使ってカウントを管理 function Counter () { const [ count , setCount ] = useState ( 0 ) ; // 状態変数countとその更新関数setCountを定義 const handleIncrement = () => { setCount ( count + 1 ) ; } ; const handleDecrement = () => { setCount ( count - 1 ) ; } ; return ( <div > <p > Count: { count } < / p> <button onClick={handleIncrement}>Increment< / button > <button onClick = { handleDecrement } >Decrement < / button> < / div > ) ; } // Appコンポーネント:Counterコンポーネントを表示 function App () { return ( <div > <h1 > Counter Example ( Without Redux ) < / h1> <Counter / > < / div> ) ; } export default App; このコードでは、Counterコンポーネントというものを含んでいるAppコンポーネントが最初に表示されます。 Counterコンポーネントは、countという変数によって状態を管理し、その状態を表示するものです。 Incrementボタンをクリックすると、handleDecrement関数コンポーネントが呼び出され、 setcountによってcount変数の値が1増えます。同様にDecrementボタンがクリックされると、handleIncrement関数コンポーネントが呼び出され、 setcountによってcount変数の値が1減るという処理を行っています。 状態が更新されるたびに、Counterコンポーネントは再レンダリングされ、新しいcountの値が画面に反映されます。 次にReduxを利用した場合のコードを見てみましょう。 Reduxを導入した場合 // actions.js export const increment = () => ({ type : 'INCREMENT' }) ; export const decrement = () => ({ type : 'DECREMENT' }) ; // reducer.js const initialState = { count : 0 } ; export const counterReducer = ( state = initialState , action ) => { switch ( action . type ) { case 'INCREMENT' : return { ... state , count : state . count + 1 } ; case 'DECREMENT' : return { ... state , count : state . count - 1 } ; default: return state ; } } ; // store.js import { createStore } from 'redux' ; import { counterReducer } from './reducer' ; export const store = createStore ( counterReducer ) ; まず、こちらはアクション(actions.js)、リデューサー(reducer.js)、ストア(store.js)を定義したコードとなります。 今回の場合、actions.jsにてincrementとdecrementという状態を更新するためにディスパッチするアクションを作成する関数を定義しています。 reducer.jsでは現在の状態とアクションを受け取って、新しい状態を返すcounterReducerを定義しており、この場合カウントを増減させるためのロジックを含んでいます。 store.jsのcreateStoreでストアを作成し、リデューサーを引数として渡して状態を管理します。ストアは状態を保持し、 アクションが送られるとリデューサーを通じて状態を更新するという処理が行われます。 import React from 'react' ; import { useSelector , useDispatch } from 'react-redux' ; import { increment , decrement } from './actions' ; function Counter () { // Reduxの状態(store)のcountを取得 const count = useSelector ( state => state . count ) ; const dispatch = useDispatch () ; const handleIncrement = () => { dispatch ( increment ()) ; // INCREMENTアクションをディスパッチ } ; const handleDecrement = () => { dispatch ( decrement ()) ; // DECREMENTアクションをディスパッチ } ; return ( <div > <p > Count: { count } < / p> <button onClick={handleIncrement}>Increment< / button > <button onClick = { handleDecrement } >Decrement < / button> < / div > ) ; } function App () { return ( <div > <h1 > Counter Example ( With Redux ) < / h1> <Counter / > < / div> ) ; } export default App; こちらのコードは実際のUI部分を実装しているコードになります。 Counterコンポーネント内のボタンをクリックすると、handleIncrementやhandleDecrementが呼ばれ、 それぞれincrement()やdecrement()アクションがディスパッチされます。 これらのアクションは、Reduxストアに送信されます。 ストアは、送られてきたアクション(INCREMENTやDECREMENT)に基づいて状態を更新します。 この処理は、counterReducerというリデューサーによって行われることになります。 ストアの状態(count)が更新されると、useSelectorフックを使って最新の状態がCounterコンポーネントに反映され、 画面に表示されるカウントが増減されます。 Reduxを使ってみた個人的な感想 こちらはReactよりも初見はさらに意味が分からないかったというのが正直なところです。 ただ状態管理をグローバルで行うことができるため、MetemcyberPJのように大規模な開発になると使った方が 明らかにコードの保守性や拡張性が向上するのかと思います。 実はこのReduxにはToolkit(公式ライブラリ)としてデータフェッチング(データの取得)、キャッシュ管理を簡潔かつ分かりやすく行う RTKqueryというものもありますが、こちらの内容まで書き出すとものすごい分量になるので、今回は割愛させていただきます。 参考文献 梅田望夫, 『ウェブ進化論 本当の大変化はこれから始まる』, ちくま新書, 2006. 梅田望夫, 『シリコンバレーから将棋を観る―羽生善治と現代』,中央公論新社, 2009. 羽生善治, 『人工知能の核心』, NHK出版, 2017. Meta, 『React Webとネイティブユーザーインターフェースのためのライブラリ』( https://ja.react.dev/learn/describing-the-ui ), 2024. Dan Abramov and the Redux documentation authors, 『Redux A JS library for predictable and maintainable global state management』( https://redux.js.org/tutorials/essentials/part-1-overview-concepts ), 2024. 開発プロダクトの内容に関しては今回の記事の本旨から外れるため割愛させていただきます。 ↩
この記事は、 NTT Communications Advent Calendar 2024 10日目の記事です。 先日、自前のMedia over QUICの実装を IETF 121のハッカソン へ持ち込んで相互接続試験に参加してきました。 その結果、他の参加者の実装との相互接続に成功し、Working Groupのリストに名前を記載いただけました。 本記事では、Media over QUICの概要や動向を紹介し、ハッカソンでの体験について報告します。 はじめに Media over QUICとは? 概要 プロトコルの構成 通信の参加者 何が嬉しいの? メディアのフリーズを防げる 映像や音声以外も扱える オンデマンドとリアルタイムを統合できる サードパーティのネットワークと連携しやすくなる 現在の動向と未来予想 仕様安定化に向けた議論の継続 WebRTCの拡張機能との競合 ハッカソンの参加報告 IETFハッカソンについて ハッカソンにおける取り組み そして延長戦へ 学び おわりに 勉強会の宣伝 謝辞 はじめに こんにちは、イノベーションセンター所属の 前田 です。 WebRTCプラットフォーム「 SkyWay 」のR&Dを担当しています。 最近は、次世代のメディア伝送技術として提案されているMedia over QUIC(以下、MoQ)なども研究しており、同チームの 内田 とともに内製で開発した実装「 moq-wasm 」をOSS公開しています。 先日(2024年11月)、標準化貢献を目的として IETF 121 Dublin のハッカソンへmoq-wasmを持ち込んで参加してきましたので、その様子や成果について報告します。 なお、MoQは現在策定中の比較的新しい概念ですので、本記事ではその概要の解説から行います。 Media over QUICとは? 概要 Media over QUICはその名のとおり、QUICでメディアを伝送するための技術を意味します。WebTransportにも対応しているのでブラウザからでも利用可能です。 プロトコルの構成 プロトコルスタックは以下のとおりです。 MoQのプロトコルスタック(出典: 中間会議資料 ) MoQには大きく分けて以下2つの層が存在します。 Media over QUIC Transport(以下、MOQT) ストリーミングフォーマット MOQTの層ではメディアを送信するための共通のルールや振る舞いが提供されます。 MOQTのドラフト を確認すると、メッセージの種類やフォーマットなどが定義されていることがわかります。 ストリーミングフォーマットの層ではMOQTの上でどのようにメディアを扱うかが定義されます。例えば、 WARP というストリーミングフォーマットでは以下のことが定められています。 WebCodecsを活用した LOC というメディアパッケージ手法を主に採用すること (扱うメディアは基本的に音声・映像のみ) 送信するメディアの情報を相手へ伝えるためにCatalogという仕組みを用いること この記事を書いている2024年12月現在、WARPはまだ個人ドラフトの状態ですが、もうすぐ正式にWorking Group(以下、WG)ドラフトになる 1 予定です。 通信の参加者 MoQの通信には主に以下3種類の参加者が存在します。 Original Publisher: メディアの送信を開始する Relay: メディアを受信し、(必要に応じてキャッシュし、)それを他の参加者へ転送する End Subscriber: メディアを受信し、それを他の参加者へ転送しない Original / Endというワードが必要なのは、RelayがOriginal Publisherに対するSubscriberとなり、End Subscriberに対するPublisherとなるため、それらと区別するためです。 もしもメディアを双方向に送り合いたい場合、アプリケーションの中でメディアごとにOriginal PublisherとEnd Subscriberを入れ替えれば実現できます。 Relayの数は定められていません。アプリケーションのユースケースやその規模などに応じてRelayの台数を増やすことでスケーリングできます。 何が嬉しいの? MoQは主に以下の2つのユースケースに対し、柔軟性を高めながらカバーしていくことが期待されています。 HLS(HTTP Live Streaming)などを用いるようなメディアコンテンツ配信 WebRTCなどを用いるようなリアルタイムコミュニケーション 具体的には以下が主なメリットだと思っています。 メディアのフリーズを防げる 主にHLSなどTCPベースの配信手段を活用する場合、パケットの送信に失敗すると次のパケットの送信を待たせて先に失敗したパケットを再送します。結果、パケットの再送が成功するまで後続のパケットが詰まってしまい、再生しているメディアがフリーズしてしまいます 2 。 QUICはTCPとUDPの特性を併せ持っているため、パケットの送信に失敗してもそのパケットの再送を待たずに後続のパケットの送信を行えます。これによって、ネットワーク障害などが発生している場合を除き、再生中のメディアのフリーズを抑えることができます。 映像や音声以外も扱える 現在最も議論が進んでいるストリーミングフォーマットは音声・映像を扱うWARPですが、それ以外のストリーミングフォーマットを定義することで全く違う種類のメディアを送信することもできます。 例の1つとして、MoQ上でチャットアプリケーションを実現する方法が 個人ドラフト として提案されています。 また、過去に別のWGの会議の中でXRに使用する3Dデータの送信手段の案の1つとしてMoQが言及されたこと 3 などもあり、将来的に幅広いユースケースで活用される可能性があります。 オンデマンドとリアルタイムを統合できる これまでは、HLSのようなオンデマンド性とWebRTCのような高いリアルタイム性を両立した形でメディアコンテンツ配信を実現できるプロトコルが存在しませんでした。 一方、MoQは必要に応じてRelayにキャッシュを持たせることができ、WebRTCと同程度の遅延で送信できるため、これを実現できます。 過去の映像の再生中に、シームレスに現在の映像へ切り替えるような使い方も可能です 4 。 サードパーティのネットワークと連携しやすくなる MoQは標準化の範囲にCDNの領域を含んでいることも大きな特徴の1つです。MOQTのドラフトの中では、アプリケーションから独立したサードパーティのネットワークを活用できるようになることを目標の1つとして定義しています。 また、送信されるメディアの秘匿性が保たれたまま転送されることを前提として定義していることもあり、これまで以上にCDNの選択・連携がしやすくなると予想されます。 現在の動向と未来予想 まず結論から述べると、MoQが広く活用され始めるまでにはまだまだ時間がかかると思っています。 理由は大きく分けて2つあります。 仕様安定化に向けた議論の継続 MoQ WGはIETF以外にも中間会議で議論を進めています。中間会議は2週間に1度開催されており、回によっては2日連続で6時間以上の会議を開催することもあります 5 。 MoQ WGの今後の会議予定日 また、現在のMOQTのドラフトの最新バージョンは07であり、まだ比較的若い状態だと思われます。さらに、ストリーミングフォーマットは一番議論が進んでいるWARPすらWGドラフトになっていません。 これらを踏まえると、仕様が安定するまでに今後も多くの議論が必要になると予想されます。 WebRTCの拡張機能との競合 現在リアルタイムコミュニケーションの用途で広く活用されているWebRTCは、あらゆるユースケースへ対応するために機能の拡張が進められている最中です。 一例として、扱うメディアの柔軟性を向上できるRTPTransportやリアルタイム配信用途で活用しやすくするためのWHIP / WHEPなどが提案され、各プラットフォームで実装が進められています。 今後もこのような拡張が続くことで、リアルタイム通信の領域においては、ある程度のユースケースはWebRTCでカバーできるようになっていくことが予想されます。 以上の理由から、MoQは仕様が定まるまでに数年かかり、その後特定のユースケースに向けて徐々に市場に浸透していくような流れになるのではないかと考えています。 特に、大規模な音声・映像配信のユースケースにおける活用が先行するのではないかと予想しています。 ハッカソンの参加報告 ここからはハッカソンの参加報告と題してIETFにおけるハッカソンの概要や私の取り組みをご紹介します。 IETFハッカソンについて IETFハッカソンはIETFの最初の2日間の土日で開催されます。ハッカソンでは特定の技術の専門家が一箇所に集まり、アイディア出しや実装、相互接続試験などを行います。 ハッカソンの参加方法はオンサイトとリモートの2通りから選べます。 オンサイトの会場にはいくつものテーブルが用意されており、特定の技術に興味のある人が集まったら入口付近に貼られたボードへ技術名を書くことでテーブルを確保します。 テーブルボード(MoQ WGは#10を確保) 現在、MoQ WGの取り組み内容は特に指定されておらず、参加者は自由に議論したり自分のタスクに取り組んだりできます。 MoQの専門家だけではなくQUICやWebRTCなど関連技術の専門家が集まったり、あるいはたまに訪れてくださるので、その都度興味深い話を聞くことができます。 また、ハッカソン期間中に相互接続試験が行われています。もしも自分の実装を持っていれば試験に参加できます。 相互接続試験の結果はハッカソンの終わりではなくMoQ WGの会議の初めに発表されます。 ハッカソンにおける取り組み 私はオンサイトでハッカソンに参加し、moq-wasmの実装を持っていたため相互接続試験に取り組みました。 相互接続試験は参加者が実装した MoQ をそれぞれ接続するものです。 Relayの実装を持っている参加者はRelayサーバのURLを公開し、Original Publisher / End Subscriberの実装を持っている参加者は公開されたRelayサーバへの接続を試みます。 moq-wasmはRelay / Original Publisher / End Subscriberのすべてを実装しているため、Relayサーバの公開と他の参加者の繋ぎこみの両方を行いました。 これらの相互接続試験に関するやり取りはその場で、もしくはWGのSlack上で行われます。 RelayサーバのURLはSlackで公開されますが、基本的にはすぐに接続に成功せず、その場やSlack上でコミュニケーションを取りながら問題のある箇所を探します。 私の場合、オンサイト会場にいたLorenzo Miniero氏からいくつかのご指摘をいただき、問題を修正することで相互接続を成功させることができました。 また、他の参加者から報告された接続失敗の結果を分析してフィードバックすることで、その方の実装の修正に貢献できました。 結果として、いくつかの実装との接続に成功したため、WGの相互接続試験リストに「NTT(Tetta)」と名前を載せていただき、MoQ WGの会議の中で報告いただけました。 相互接続試験の結果 6 (出典: 会議のアーカイブ動画 ) そして延長戦へ 実は会議の中で報告された結果は私が満足できる結果ではありませんでした。というのも、最終的な試験表はIETF開催の約2週間前にリリースされたドラフトバージョン07ベースの結果を元に作成されましたが、私が持ち込んだのはバージョン06ベースの実装だったため一部のメッセージしか疎通しなかったという結果となったためです。 (ハッカソンの終盤で07へのバージョンアップに取り組みましたが、他の参加者との接続確認は完了しませんでした) ハッカソンの終わり際に、MoQ WG ChairのAlan Frindell氏に上手くいかず悔しかった旨を伝えたところ、なんと「後日集まって延長戦をやろう」と言ってもらえました。 私はこれを非常に嬉しく思い、IETFの会議の合間に準備を進めました。 そして、IETF 6日目の木曜日にThe Forumという名の部屋 7 に集まり延長戦を行いました。 結果として、メディアを送信するためのメッセージを含め、上記の表における「moxygen (Alan, Daniel, ...)」のServer実装や「Meetecho (Lorenzo)」のClient/Server実装と一通り疎通を確認できました。 学び 相互接続試験では各参加者の実装方法や実行環境が異なるため、MoQの領域ではないところでいくつかの問題が生じました。私の場合はQUICやセキュリティに関する箇所で問題が発生し、解析や修正に時間がかかりました。この経験から、MoQだけではなく関連技術についても理解を深めることが重要だと学びました。 おわりに 今回はMoQ WGの相互接続試験に初めて参加し、いくつかの実装と相互接続できることを示したり、他の参加者の実装を改善した形で貢献できました。 今後もmoq-wasmのメンテナンスを続け、さらなる貢献を目指していきたいと考えています。 勉強会の宣伝 現在MoQは非常に議論が活発で、仕様もまだまだ大きく変わるので追いかけるのは大変だと感じています。 不定期で 勉強会 を開催しておりますので、MoQに興味があるという方は気軽にご参加ください。 なお、先日開催された勉強会では以下の資料で登壇しました。特にメッセージやシーケンスについて詳しく解説しています。 詳細が気になる方はぜひご確認ください。 謝辞 I would like to extend my gratitude to Alan, Lorenzo, and all the others who helped me. Thank you so much! 何か提案がある場合、まずは個人ドラフトとしてWGに提出し、WG全体で扱うべきと判断されたらWGドラフトになります。 ↩ この問題をHead of Line Blockingと呼びます。 ↩ MOPS WGの 過去の会議 で言及されています。 ↩ WGではこの特徴のことを「Live Edge」と呼んでおり、現在具体的な実現方法について議論中です。 ↩ これにより多くのことが中間会議で決まってしまうことを避けるため、QUIC WGの進め方の一部を取り入れてコンセンサスを得ることがIETF 121で提案されました。 ↩ 空欄は接続試験に失敗したことを表しているとは限りません。今回参加しなかった方の項目や、サポートしている伝送方法(Raw QUICベースかWebTransportベース)の違いにより試験できない項目を含んでいます。 ↩ IETF期間中は基本開放されている大広間であり、休憩スペースとして利用したり参加者同士でコミュニケーションを取る場として活用できます。 ↩
こんにちは、NTTコミュニケーションズの 現場受け入れ型インターンシップ2024 に参加した大学2年生の大堀です。X(旧:Twitter)では @LimitedChan (限界ちゃん)というハンドルネームでサイバーセキュリティに関して活動しています。 普段は大学で情報系の学習をしながら、個人的に脅威インテリジェンスをはじめとしたサイバーセキュリティに対して興味があり、さまざまな学習に取り組んでいます。 2024年8月26日〜9月6日までの2週間、「脅威インテリジェンスを生成・活用するセキュリティエンジニア/アナリスト」としてNetwork Analytics for Security(通称:NA4Sec)プロジェクトに参加しました。 この記事では、私がインターンシップで取り組んだ内容について紹介します。 Na4Secプロジェクトとは? インターンシップ参加の経緯 インターンシップ概要 今回作成したもの 環境説明 環境構築 Elasticsearch + Kibana + Fleet Server編 OpenCTI編 Elastic Agent編 Cobalt Strike - OpenCTIでの分析 OpenCTIでの分析 Cobalt Strikeのレポート作成 Cobalt Strike - EDRでの検知 データ構造の変換 Streamの設定 アラートルールの設定 Windowsから通信を発生させてみる 学んだこと おわりに Na4Secプロジェクトとは? 私が参加したNA4Secプロジェクトは「NTTがインターネットの安全性を守る社会的責任を果たす」という理念をもとに、「攻撃インフラの特定と撲滅」に挑むプロジェクトです。このプロジェクトには、NTT Comイノベーションセンターを中心に、NTTセキュリティ・ジャパンやエヌ・エフ・ラボラトリーズのメンバーが集結し、日夜攻撃インフラを追跡しています。 また、このプロジェクトのもう1つの特徴として、NTT Comグループ内における脅威インテリジェンスチームの役割を担っています。有事において脅威インテリジェンスを提供し、意思決定を支援することで、NTT Comグループのセキュリティを強化しています。 インターンシップ参加の経緯 私がこのインターシップへ応募した理由は、今までに経験したことが実務ではどのように活かせるかを体感したいと思ったためです。今までセキュリティ・キャンプ全国大会などのイベントへの参加や日々の学習、記事執筆を通じて知識を深めてきました。このインターシップを通して、自分のスキルと実務で求められるスキルのギャップを確認し、さらなる成長を目指したいという意欲もあり、応募を決めました。 インターンシップ概要 インターンシップでは1週目と2週目に大きく分けて2つの活動を経験しました。1週目では、もう1人のインターン生とともに業務をし、2週目ではそれぞれに合ったテーマを決めていただき業務をさせていただきました。 1週目 脅威調査(Cobalt Strikeとその悪用) 脅威探索(Cobalt Strike C2サーバに関して) 脅威情報発信(フィッシングサイトに関して) 2週目 TIP + EDR + SIEMの基盤システム構築と運用 今回は2週目の業務内容について紹介していきます。 今回作成したもの ユーザ端末が危険性のあるIPアドレスに接続してしまうなどといった事象を検知し、アラートを生成するための基盤システムを作成しました。脅威情報を登録し、収集・分析・共有するTIP(Threat Intelligence Platform)、ユーザ端末上で脅威を検知するEDR(Endpoint Detection and Response)、情報を管理しアラート通知するSIEM(Security Information and Event Management)の3つから構成されます。 環境説明 今回の環境ではTIPとしてOpenCTI、SIEMとしてElasticsearch、Kibana、Fleet Server、EDRとしてElastic Agentを使用しました。それぞれのツールについて簡単に説明します。 ツール名 説明 Elasticsearch オープンソース型の分散型検索エンジン。大量のデータから全文検索や分析、リアルタイムでのデータ処理を効率的に行う。 Kibana データの可視化ツールとして、Elasticsearchと連携し、ダッシュボードやグラフを作成してデータを視覚的に確認する(①)。 OpenCTI サイバー脅威インテリジェンス(CTI)システムで、脅威情報を収集・分析・共有するためのプラットフォーム。Elasticsearchをデータストレージとして利用する(②)。 Fleet Server Elastic Agentを中央で管理および監視する(③)。 Elastic Agent ホスト上でログなどを収集するElasticのエージェントツール。拡張機能を利用することで、EDRとしての機能を担い、ログデータをElasticsearchへ送信する(④)。 これらを利用して以下のように構築しました。 上記の環境では、Windows環境はユーザ端末、Ubuntu環境はSOC(Security Operation Center)の役割を模擬したものになります。SOC側で収集・分析した脅威情報がWindows端末のEDR機能に反映されることによって、リアルタイムで脅威を検知する仕組みとなっています。 環境構築 Elasticsearch + Kibana + Fleet Server編 GitHub上の公開リポジトリ *1 にあるDocker Composeファイルを利用し、Elasticsearch + Kibana + Fleet Serverをセットアップしました。 また、環境に合わせて各種env, ymlファイルを編集し、Elasticsearchに対してHTTPS接続するために証明書を設定しました。 構築を終えたあと、実際にKibanaUI(localhost:5601)に接続をしてみると以下のような画面が表示されます。 ここから、データを可視化したり検索したりできます。 OpenCTI編 こちらもGitHub上で公開されているDockerイメージ *2 を利用し、OpenCTIをセットアップしました。 また、公式ドキュメントを参考にしてenvファイルを作成し、ElasticsearchとHTTPSで連携するために証明書を設定しました。構築を終えたあと、実際にOpenCTI(localhost:8080)に接続をしてみると以下のような画面が表示されます。ここから、脅威情報を収集したり分析したりできます。 設定を終えた後、初めのうちはOpenCTIにアクセスできましたが、時間を開けると立ち上がらなくなる問題が発生しました。調査したところ、Elasticsearch + Kibana + Fleet Serverの環境を構築したときのymlファイルにElasticsearchの設定がすでに記載されていたため、OpenCTIのymlファイルに記載されているElasticsearchの設定と競合していたのが原因だと判明しました。そのため、OpenCTIのdocker-compose.ymlファイルからElasticsearchの設定を削除することで解決しました。 また、便利な機能として、OpenCTIはConnectorと呼ばれるプラグインを使うことで外部のインテリジェンスを自動で取り込むことができます。より効率的にインテリジェンスを収集したかったため、ymlファイルを編集して「mitre」「cve」「malpedia」の3つのプラグインも導入してみました。 Elastic Agent編 次にEDR機能を担うElastic Agentを構築し、Intergration(拡張機能)としてElastic Defendを導入しました。 Intergrationは必要なコマンドをWindows上で実行するだけで導入でき、設定はKibanaUI上で行えます。 Cobalt Strike - OpenCTIでの分析 おおかたの環境構築が終わったところで、1週目で得たCobalt Strikeに関する脅威情報をOpenCTIに登録し、分析をしてみました。 Cobalt Strikeとは Cobalt Strikeは、企業のセキュリティテストを行うために作られた正規のツールです。しかし、実際に攻撃に利用できることから、不正に改造されたバージョン(クラック版)が出回っています。サイバー攻撃者はこのクラック版のCobalt Strikeを悪用している事例が多く見られており、問題になっています。 OpenCTIでの分析 1週目では、Cobalt Strikeが実際に悪用された事例をダイヤモンドモデル *3 と呼ばれる手法を用いて分析しました。今回はその中の1つの事例をピックアップしてOpenCTIに登録し、さらなる分析をしてみました。 ここからは、OpenCTIでの分析を詳しく説明していきます。 OpenCTIに登録する際には、STIX2.1 *4 と呼ばれる脅威情報を表現するための標準化されたデータモデルを使って情報を登録する必要があるため、そのデータモデルに従って情報をSTIXオブジェクトとして登録しました。 今回はCobalt Strikeに関連したマルウェア、関連ファイル、URL、ドメイン、C2サーバなどの観測値(Observables)がSTIXオブジェクトとして登録されています。 Cobalt Strikeのレポート作成 STIXオブジェクトを登録したあとは、OpenCTIの機能を使って脅威情報をまとめた「レポート」を作成しました。 ここで説明する「レポート」とはSTIXで定義されている概念の1つで、特定のトピックに関する脅威情報の集合体を指します。「レポート」では、個々のSTIXオブジェクトのほかに、STIXオブジェクト同士の関係性を記述できます。 OpenCTIでは、それらの関係性をグラフ構造で図示できます。 以下の図は、1週目に調査したCobalt Strikeを悪用した事例の1つを「レポート」化したものになります。 図に表してみることで、Cobalt Strike単体での攻撃がメインではなく、他のマルウェアや悪意のあるファイルと組み合わせて攻撃が行われていることが俯瞰的に把握できるようになりました。 Cobalt Strike - EDRでの検知 OpenCTIに登録したCobalt Strikeに関する脅威情報をもとに、EDRであるElastic Agentを使用して脅威を検知してみました。今回は、Cobalt StrikeのC2サーバで使われたIPアドレスをインジケーターとして登録し、Windows上でそのIPアドレスに接続があった場合にアラートを出すようにKibana上から設定しました。 以下がEDRでの検知の大まかな流れです。 1.不正なIPアドレスに接続 Windows上でCobalt StrikeのC2サーバのIPアドレスに接続を試みる。このとき、登録されたインジケーターIPアドレスへの接続試行がElastic Agentで検知される。 2.イベントの収集と送信 Elastic Agentの検知がイベントデータとして記録され、Fleet Serverを介してElasticsearchに送信され、データベース上に保存される。 3.Kibanaでのアラートの発生 Elasticsearchに保存されていたイベントデータがKibanaのアラートルールにマッチし、アラートがトリガーされてダッシュボードに表示される。 (4.)OpenCTIでの脅威インテリジェンス管理 OpenCTIもUbuntu端末上で稼働しているため、Kibanaでアラートが発生した際にKibanaと同時にOpenCTIでも脅威情報を確認できる。 データ構造の変換 OpenCTI上のデータ構造とKibana上のデータ構造が異なるため、そのままでは利用できません。そのため、Kibanaのアラートルールで使用されているECS(Elastic Common Schema) *5 を適用するため、OpenCTIのデータ構造をECSに変換する必要があります。この変換のために、OpenCTIにElastic Connectorを導入しました。これはOpenCTIのComposeファイルにElastic Connectorの情報を記述することで導入できます。 Streamの設定 OpenCTIとKibana間でリアルタイムにデータをやり取りするため、Streamを設定しました。StreamはOpenCTIとKibana間でのデータを送受信するための機能です。OpenCTI上で設定することで、Kibanaでデータを受信できます。 アラートルールの設定 アラートルールはKibanaUI上で設定できます。ECSのリファレンスやOpenCTIが出しているドキュメントを参考に、以下のように設定しました。 Windowsから通信を発生させてみる 以下の図に示されているのは、Windows端末からUbuntuサーバのIPアドレスに対する通信がアラートとして検知されたものとなっています。 本来は、Cobalt StrikeのC2サーバへの通信を検知することを目的としていました。しかし、検証環境がクローズドなためCobalt StrikeのC2サーバのIPアドレスへの通信を発生させにくいという問題が発生しました。そのため、代替案としてUbuntuサーバのIPを用いた検証を実施しました。この手法によりアラートの検出ロジックが正常に機能していることを確認しました。 原理的には、STIXオブジェクトとして登録したCobalt StrikeのC2サーバのIPアドレスに対する通信が発生した場合も、同様に検知可能であることが期待されます。 学んだこと 今回のインターンシップを通して、以下のようなことを学べました。 環境構築について 自分で環境を構築したり機能を追加したりすることで、システム同士の連携やデータのやり取りの仕組みを理解できました。今まで、すでに完成されている環境やツールを使用することが多かったので、自分の手を動かすことの大切さも学べました。 また、今回の場合インターネットで検索してもなかなか情報が見つからないことが多かったため、公式ドキュメントを読むことの重要性を再認識しました。 構築時には多くのトラブルシューティングに直面して、心が折れそうになることもありました。しかし、エラーログや公式ドキュメントを確認し、それでも原因がわからない場合は積極的に質問することで解決ができました。この経験を通じてコミュニケーションの重要性を学べました。さらに、セキュアな環境構築の方法も教えていただいたので、これからの環境構築で意識していきたいと思いました。 脅威インテリジェンスについて 実際に脅威インテリジェンスを用いてEDRを運用することで、脅威インテリジェンスがサイバーセキュリティにおいて重要なものであると再認識しました。 また、普段から脅威情報に関するレポートや記事を積極的に読むことは大切だと感じました。今回のインターンシップでは、特にCobalt Strikeの脅威分析に関するレポートを読むことで、その脅威に対する知識を深められました。 おわりに 普段ではなかなか経験できないようなことを体験でき、非常に充実した2週間でした。脅威インテリジェンスに関する知識を深められただけでなく、実際にその知識を活かせたことがとても嬉しかったです。 この貴重な機会を与えてくださったNA4Secプロジェクトの皆さま、本当にありがとうございました。また、神田さん、鮫嶋さん、益本さん、坪井さん、皆川さん、大変お世話になりました。ありがとうございました。
この記事は、 NTT Communications Advent Calendar 2024 9日目の記事です。 この記事では、SkyWayを使ったアプリ開発が爆速になるCLIツールを作った話を紹介します。 CLIツールにどのような機能を実装したのか、機能を実装する際にどのようなことを考えたのかについて、詳しく説明します。 はじめに 注意事項 skyway-cliの機能 skyway-cli channel watch skyway-cli token serve skyway-cliを設計するときに考えたこと Unix哲学に則ること 設定値を柔軟に変えられること skyway-cliを実装するときに考えたこと インストールを簡単にする テストを書きやすくする おわりに 参考情報 はじめに 皆さまこんにちは。イノベーションセンター SkyWay DevOps プロジェクト所属の @sublimer です。 SkyWay は、ビデオ・音声通話機能を簡単にアプリケーションに実装できる、リアルタイムコミュニケーションを実現するためのプラットフォームです。 SkyWayではこれまで、JavaScript、iOS、Androidの各プラットフォーム向けにSDKを提供してきました。 今年は、これら3つのプラットフォームに加えて、Linux向け、Unity向けのSDKも提供を開始しました(Unity向けSDKは現在ベータ版です)。 また、録音・録画機能を利用するためのRecording API、SkyWayのChannelの作成と情報の参照をするためのChannel APIの提供も始めました。 このようにSkyWayではさまざまな機能をどんどんリリースしているため、これまで実現が難しかった機能も簡単に実装できるようになっています。 一方で、機能が増えることでSkyWayを使ったアプリの開発をいかに効率よく進めていくかが課題となります。 そこで、SkyWayを使ったアプリ開発がより効率的に進められるようにするためのCLIツール、「skyway-cli」を作成しました!! github.com 注意事項 今回開発したCLIツールは公式として提供している機能ではありません。 あくまでも個人的に開発したものであり、SkyWay公式のサポートは提供していません。 利用される場合は、公式のツールではないことをご理解の上でご利用ください。 もし不明点や不具合があった場合は、GitHubリポジトリのissueとして連絡いただければ、できる限り対応いたします。 skyway-cliの機能 skyway-cliには、以下の11個のサブコマンドが実装されています。 skyway-cli token: SkyWayの認証情報であるSkyWay Auth Tokenを生成します。 --admin フラグをつけると、SkyWay Admin Auth Tokenを生成します。 decode: SkyWay Auth Token、SkyWay Admin Auth Tokenをデコードします。 --pretty フラグをつけると、デコード結果を整形して表示します。 serve: SkyWay Auth Tokenを払い出すためのHTTPサーバーを起動します。 verify: SkyWay Auth Tokenが有効なものかどうかを検証します。無効なパラメーターが入っている場合、エラーとしてその情報を出力します。 channel create: Channel APIを用いて、SkyWayのChannelを作成します。 find: Channel APIを用いて、SkyWayのChannelを検索します。 get: Channel APIを用いて、SkyWayのChannelの情報を取得します。 watch: SkyWayのChannelにおいて発生したイベント情報をリアルタイムで取得し、表示します。 recording start: Recording APIを用いて、SkyWayの録音・録画を開始します。 stop: Recording APIを用いて、SkyWayの録音・録画を停止します。 get: Recording APIを用いて、SkyWayの録音・録画の情報を取得します。 これらのサブコマンドのうち、特に便利なものを2つ紹介します。 skyway-cli channel watch このコマンドを使うと、以下の例のようにSkyWayのChannelにおいて発生したイベント情報をリアルタイムで取得し、表示できます。 (実行結果はあくまでも説明用のものです) $ skyway-cli channel watch --id 0ea5bcde-a6f4-4931-9223-f5fdace5ec78 {"type":"MemberAdded","data":{"channel"... // Memberの入室 {"type":"StreamPublished","data":{"channel"... // Publicationのpublish {"type":"StreamUnpublished","data":{"channel"... // Publicationのunpublish {"type":"MemberRemoved","data":{"channel"... // Memberの退室 このコマンドを使うことで、SkyWayのChannelへのMemberの参加・退出の情報や、Publication、Subscriptionのpublish、subscribeの情報を、手軽にリアルタイムで確認できます。 内部の実装としては、RTC-APIサーバーにWebSocketで接続し、指定したChannelのイベント情報を受信して表示しています。 通常、この処理はSDK内部で行われますが、SDKにおいて実行している処理と同等の処理をCLIツール内で実装することで、CLIツールからもSkyWayのChannelのイベント情報をリアルタイムで取得できるようにしました。 なお、SkyWayでは、通信状況を記録、可視化するための Analytics機能 を提供しています。 Analytics機能は、過去のデータも含めてWebブラウザ上で簡単に通信状況や品質を確認できます。 運用時に過去の通信状況を確認したい場合はAnalytics機能、アプリ開発時に手軽にリアルタイムで通信状況を確認したい場合は skyway-cli channel watch コマンドのように、状況に合わせて使い分けることをおすすめします。 skyway-cli token serve このコマンドを使うと、デフォルトでは8080番ポートでHTTPサーバーを起動し、SkyWay Auth Tokenを払い出すAPIが利用できるようになります。 $ skyway-cli token serve ⇨ http server started on [::]:8080 通常SkyWay Auth Tokenは、ユーザーのアプリケーションの認証認可ロジックを踏まえて適切な権限を設定して払い出す必要があります。 こうすることで、セキュアなアプリケーションを実装できますが、「ちょっと動作を確認できればOK」という場合には払い出し用のサーバーを作るのが少し手間になってしまいます。 SkyWay Auth Tokenの生成はフロントエンドでも実行可能ですが、シークレットキーをフロントエンドのソースコードに書き込むのはセキュリティ上好ましくありません。 skyway-cli token serve コマンドを使うことで、フロントエンドのソースコードにシークレットキーを書き込まずに、手軽にSkyWay Auth Tokenを払い出すAPIを利用できるようになります。 skyway-cliを設計するときに考えたこと 今回skyway-cliを設計するにあたって、以下の2つのポイントを考慮しました。 Unix哲学に則ること 設定値を柔軟に変えられること Unix哲学に則ること skyway-cli token decode コマンドを例に説明します。 このコマンドは、標準入力からSkyWay Auth Tokenを受け取り、デコード結果を標準出力に出力します。 エラーがあれば、標準エラー出力にエラーメッセージを出力し、終了ステータス 1 で終了します。 この挙動は、「過度の対話的インタフェースを避ける」、「すべてのプログラムをフィルタにする」というUnix哲学に則っています。 SkyWay Auth Tokenの入力方法には、標準入力から受け取る方法以外にも、ファイルとして読み込んだり、対話型のプロンプト経由で読み込む方法が考えられます。 これらの方法でも入力を受け付けることはできますが、標準入力から受け取る方法は他のプログラムの出力をパイプで受け取ることもでき、対話型のプロンプトのルールを覚える必要もありません。 このように、シンプルで自動化もしやすい点が、標準入力からデータを受け取ることのメリットです。 もちろん、 --input-flie のようなオプションを追加してファイルからの入出力に対応することもできますが、リダイレクトやパイプを使えば必ずしもコマンド側でファイルの入出力ができるようにする必要はありません。 このような、「Worse is better(劣る方が優れている)」の考え方もCLIツールを設計する際には重要だと考えています。 設定値を柔軟に変えられること skyway-cliは、コマンド実行時のパラメーターを以下の3つのいずれかの方法で受け取ることができます。 設定ファイルに記述された設定値 環境変数に設定された設定値 コマンドライン引数で指定された設定値 通常は設定値を明示的に指定する必要はなく、設定ファイルに記述された設定値を利用します。 一時的に設定値を変更したい場合は、環境変数やコマンドライン引数で指定できます。 SkyWayでは、アプリケーションIDとシークレットキーの2つの認証情報を使ってSkyWay Auth Tokenを生成します。 これらの認証情報は、制限の範囲内であれば複数払い出すことができます。 ユーザーによっては、開発用、試験用、本番環境用のように、複数の認証情報を使い分けたいケースが想定されます。 複数の設定値を柔軟に変えられるようにしておけば、普段は設定ファイルに書かれた開発用の認証情報を使い、試験や本番環境の不具合調査の時だけ環境変数で試験用の認証情報を指定するといった使い方ができるようになります。 skyway-cliを実装するときに考えたこと skyway-cliを実装するにあたっては、以下の2つのポイントを考慮しました。 インストールを簡単にする テストを書きやすくする インストールを簡単にする CLIツールを開発するにあたって、インストールのしやすさは非常に重要です。 インストール方法は、以下のようなやり方が考えられます。 OSごとのパッケージマネージャー経由でインストールできるようにする GitHubリリースページからバイナリをダウンロードして使う プログラミング言語ごとのパッケージマネージャー経由でインストールできるようにする 1つめの方法は、APTやHomebrewを使う方法ですが、配布のための追加の作業が必要になるため、学習コストなどが発生します。 2つめの方法はシンプルで配布の自動化もしやすいですが、OSによってはダウンロードしたバイナリファイルの実行がブロックされたり、ユーザーにパスの設定をしてもらう必要があるなど、ユーザー側に手間が発生します。 3つめの方法は、プログラミング言語のランタイムを準備してもらうというユーザー側の手間が発生しますが、メジャーなプログラミング言語であればそこまでハードルは高くないと考えられます。 また、パッケージマネージャーにはインストールだけでなくバージョン管理やアップデートの機能も入っているため、ツールの配布の観点では非常に使いやすい仕組みです。 以上を踏まえ、今回は3つめの方法を採用しました。 また、利用するプログラミング言語はGoを採用しました。 GoでCLIツールを実装して配布することには、以下のようなメリットがあります。 OSごとの差分をある程度吸収できる パッケージレジストリへの公開が不要 CLIツールを作るための実績のあるライブラリがある Goは、各OS向けのバイナリを容易にビルドできます。 CLIツールは複数のOSに対応するのが望ましいため、移植性の観点が重要となります。 また、GoはGitHubリポジトリをパッケージレジストリとしてそのまま利用できるため、パッケージレジストリへの公開の処理が不要です。 これにより、CI/CDパイプラインでのリリース処理を省略でき、GitHubリポジトリにpushするだけで容易にリリースができるようになります。 加えて、実績のあるCLIツール開発用ライブラリがある点もGoを選択した理由です。 CLIツール開発用のライブラリには、 Cobra を利用しています。 Cobraは、KubernetesのCLIツールである kubectl やGitHubのCLIツールである GitHub CLI など、多くのCLIツールで採用されているライブラリです 1 。 CLIツールを作るための技術選定にはあまり自信がなかったので、今回は実績のあるライブラリを選択しました。 CLIツールをGoで実装したことで、 go install コマンドを実行するだけで簡単にインストールができるようになり、さまざまなOSにも対応している利便性の高いCLIツールを実装できたと考えています。 テストを書きやすくする 前述の通り、CLIツールは最終的な入出力が標準入出力であるため、そのままではテストが難しくなります。 CLIツールのテストが難しくなる要因は、入出力のインターフェースが標準入出力である点だと考えています。 入出力のインターフェースが標準入出力となっている場合、stdinやstdoutをモックする必要があり、テストの実装コストが高くなります。 ただし、標準入出力も含めてE2Eテストのようなことをする必要は必ずしも無く、CLIツールの内部のロジックと標準入出力の処理を分離すれば、CLIツールのテストを容易に書くことができます。 今回CLIツールを作るにあたって、以下のルールを決めて実装を進めることにしました。 標準入出力、標準エラー出力、終了コードに関する処理は Command.Run 2 に実装する Command.Run 内でCLIツール固有のロジックを呼び出し、そのロジックに対してテストを書く このルールに従うことで、CLIツールの内部のロジックと標準入出力の処理を分離し、CLIツールの内部のロジックに対してテストを書くことでテストを書きやすくしています。 余談ですが、Cobraには CheckErr 3 という関数があり、エラーを渡すといい感じにエラーメッセージを標準エラー出力に出力し、終了コード 1 でコマンドの実行を終了します。 この関数を使うことで、以下のようなエラーハンドリングのコードを無くすことができます。 // before result, err := doSomething() if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit( 1 ) } // after result, err := doSomething() cobra.CheckErr(err) おわりに 本記事では、CLIツールにどのような機能を実装したのか、機能を実装する際にどのようなことを考えたのかについて紹介しました。 GoでCLIツールを作るのは初めてだったのですが、Cobraの開発体験が非常に良く、作っていてとても楽しかったです。 また、Unix哲学はCLIツールを作る上で非常に参考になる考え方だと感じました。 Unix哲学に沿っている多くのCLIツールのように、skyway-cliも長く使われるツールになると良いなと思っています。 使ってみた感想や機能追加の要望、バグ報告などがありましたら、GitHubリポジトリのissueに投稿していただけると嬉しいです。 明日10日目の記事は、tetterさんのMedia over QUICに関する記事の予定です。 それでは、明日もお楽しみに!! 参考情報 UNIXという考え方 | Ohmsha Worse Is Better - 過去を知り、未来に備える。技術選定の審美眼 2019 edition / Worse Is Better - Understanding the Spiral of Technologies 2019 edition - Speaker Deck https://github.com/spf13/cobra/blob/main/site/content/projects_using_cobra.md ↩ https://pkg.go.dev/github.com/spf13/cobra@v1.8.1#Command.Run ↩ https://pkg.go.dev/github.com/spf13/cobra@v1.8.1#CheckErr ↩
この記事は、 NTT Communications Advent Calendar 2024  7日目の記事です。 こんにちは!イノベーションセンターの外村です。 日頃は twada 塾 と呼ばれる社内向けソフトウェア開発研修を運営しています。最近チームを異動し、 ノーコード・ローコードで 時系列分析 AI を作ることができる Node-AI の開発にもジョインしました。 本記事ではフルマネージド開発環境である Google Cloud の Cloud Workstations についてその特徴と構築方法を述べたいと思います。 はじめに フルマネージド開発環境 Cloud Workstations の概要 Cloud Workstations の構築 Terraform を用いた Google Cloud の設定 idle_timeout と running_timeout host persistent_directories container Workstation のためのコンテナイメージのビルドとプッシュ おわりに はじめに みなさん、新しい部署やプロジェクトに参加したらまず何をしますか? そう、開発環境の構築ですよね。 でも、環境構築はなかなか大変です。 まずは開発用の端末を購入するところから始まります。 そして、端末が届いて意気込んでセットアップを始めても、手順がやたら複雑だったり、 最新バージョンではうまく動かないことがあります。結果、構築に1週間近くかかることも…。 さらに、「やっと動いた!」と思って開発を始めても、数週間や数ヶ月後に先輩社員の環境と動作が違うことに気づく…なんてこと、 経験ありませんか?そのうえ、当時は快適だった端末も数年後には性能不足や故障で交換が必要になったりするんですよね。 このような課題を解決してくれるフルマネージド開発環境がここ数年注目を集めています。 フルマネージド開発環境 フルマネージド開発環境とは、開発者が開発に専念できるように 開発環境の設定や管理をクラウドプロバイダーがサポートするサービスです。 これにより環境構築の手間から解放され、端末管理の負担が軽減されます。 フルマネージド開発環境は以下の特徴を持ちます。 即時利用可能: 開発環境を即座にセットアップできる。 端末性能に依存しない: 開発環境はクラウド上で動作するため、低スペックの端末でも快適に利用可能。 スケーラブル: 必要な分だけの性能を拡張・縮小可能。 一貫性のある環境: チームメンバー全員が同じ設定の環境で作業できる。 今回は私が日頃利用している Google Cloud の Cloud Workstations について、特徴やその構築方法をお伝えします。 Cloud Workstations の概要 Cloud Workstations は、Google Cloud が提供するフルマネージド開発環境です。 事前に開発環境の設定や構成を定義しておき、利用者に払い出すことができます。 ブラウザベースの IDE やローカルのコードエディタ(IntelliJ IDEA Ultimate、VS Code など)を通じてアクセスできます。 Google Cloud ならではの特徴として、Google Cloud の IAM を用いたアクセス制御や、 Gemini Code Assist など、他の Google サービスとの連携が円滑になっています。 Cloud Workstations の全体像は以下のようになっています。 Workstation Cluster: 特定のリージョンに置く Workstation のグループです。(Cluster というと Kubernetes Cluster を想像しますが、関係はありません) Workstation Config: 「Workstations の構成」と表現されています。開発環境のテンプレート。開発環境の性能や利用するコンテナイメージなどを定義します。 Workstation: 実際にアクセスする開発環境。実態は Google Compute Engine (GCE) のインスタンスです (1 Workstation につき 1 GCE インスタンスが立ち上がってきます)。 Cloud Workstations の構築 ここで構築する Workstation のポイントをあらかじめ決めておきます。 ポイント1: Workstation の GCE インスタンスについて、IP 予約料金の削減やセキュリティを考慮してパブリック IP アドレスが付与されないようにする。 ポイント2: Workstation 利用者にはそれぞれの Workstation を払い出す。払い出される Workstation は同一の環境とする。 ポイント3: Workstation 利用者は、自分に払い出された Workstation のみ閲覧・利用できる (他者のものを閲覧・確認できない)。 ポイント4: Workstation の開発環境は、カスタマイズしたコンテナイメージにより構築される。 ポイント1 については ドキュメント に記載があり、 ネットワークで限定公開の Google アクセス、もしくは Cloud NAT を構成する必要があります。今回は Cloud NAT を構成します。 また、ポイント4 については、Google Artifact Registory にプッシュしたコンテナイメージが利用されるように設定します。 この方法ではカスタマイズしたコンテナイメージだけでなく、プルするためのサービスアカウントも必要になります。 コンテナイメージのプッシュについては手動で行うこととします。 構成は以下のイメージですね。 これらの構成を効率よく管理するために、インフラストラクチャの設定をコードとして管理する Infrastructure as Code(IaC) の考え方を採用します。 そのためのツールとして、Terraform を用います。 Terraform は HashiCorp が提供するオープンソースのインフラストラクチャ管理ツールで、クラウドリソースの構成や管理を宣言的なコードで実現できます。 これを使えば、ネットワーク設定やコンテナイメージリポジトリなどを効率よく構築し、設定の再現性や変更管理が容易になります。 Terraform を用いた Google Cloud の設定 それでは Google Cloud の設定を Terraform で実施していきましょう。 構築にあたり、事前に自身のアカウントに対して IAM で オーナー と サービス アカウント トークン作成者 のロールを付与しておきます。 また、対象プロジェクトに gcloud auth application-default login でログインしておきましょう。 Terraform の Provider (特定のクラウドプロバイダーやサービスと連携するためのプラグイン) については、 google と google-beta を用います。 手元のツールのバージョンは以下です。 Google Cloud SDK 495.0.0 Terraform v1.9.2 on darwin_arm64 + provider registry.terraform.io/hashicorp/google v6.11.2 + provider registry.terraform.io/hashicorp/google-beta v6.11.2 Cloud Storage に tfstate を保存するためのバケットを作成しましょう。 gcloud storage buckets create gs://workstations-tfstate-bucket \ --location = asia-northeast1 \ --default-storage-class = standard バケットが作成されたことを確認しておきます。 $ gcloud storage buckets list --format =" table(name, location) " NAME LOCATION workstations-tfstate-bucket ASIA-NORTHEAST1 ここからは Terraform 用のファイル作成をします。 Terraform 用のフォルダを用意して provider.tf、backend.tf、variables.tf を作成してください。 なお、variables.tf には各種パラメータ定義しています。必要に応じて変更してください。 # provider.tf terraform { required_providers { google = { source = "hashicorp/google" version = "~> 6.1" } } required_version = ">= 1.3.0" } provider "google" { project = var.project_id region = var.region } provider "google-beta" { project = var.project_id region = var.region } # backend.tf terraform { backend "gcs" { bucket = "workstations-tfstate-bucket" prefix = "terraform/state" } } # variables.tf variable "project_id" { default = "your project ID" # ご自身が利用するプロジェクト ID に変更してください。 } variable "region" { default = "asia-northeast1" # リージョンを変更したい場合は変更してください。 } variable "workstation_cluster_machine_type" { default = "e2-standard-4" # 利用者に払い出される Workstations のマシンスペック (GCE のスペックを参照してください) } variable "workstation_cluster_persistent_disk_size_gb" { default = 100 # 利用者に払い出される Workstations の SSD ディスクサイズ (GB) } variable "user_accounts" { description = "List of user email accounts that need Workstations" type = list ( string ) default = [ # 利用者の Google アカウントを列挙してください "ws.user01@gmail.com" , "ws.user02@gmail.com" , "ws.user03@gmail.com" , ] } 肝である main.tf をステップに分けて作成します。 まずは main.tf に Workstations が立ち上げるネットワーク部分を記載していきましょう。 # main.tf # VPC ネットワーク resource "google_compute_network" "workstation_network" { name = "workstation-cluster" auto_create_subnetworks = false project = var.project_id } # サブネット resource "google_compute_subnetwork" "workstation_subnet" { name = "workstation-cluster" ip_cidr_range = "10.0.0.0/24" network = google_compute_network.workstation_network.name private_ip_google_access = true } # Cloud NAT 用 IP アドレス resource "google_compute_address" "workstation_nat_ip" { name = "workstation-nat-ip" } # Cloud Router resource "google_compute_router" "workstation_router" { name = "workstation-router" network = google_compute_network.workstation_network.name } # Cloud NAT resource "google_compute_router_nat" "workstation_nat" { name = "workstation-nat" router = google_compute_router.workstation_router.name nat_ip_allocate_option = "MANUAL_ONLY" source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES" nat_ips = [ google_compute_address.workstation_nat_ip.self_link ] min_ports_per_vm = 64 udp_idle_timeout_sec = 30 tcp_established_idle_timeout_sec = 1200 } 上記はポイント1 に関連する記述ですね。 この記述は、workstation-router というルータを作成し、同時に Cloud NAT が有効化される状態を定義しています。 ここではサブネットを 10.0.0.0/24 としています。 内部 IP アドレスは Workstation Cluster や Workstation に払い出されるので、 サブネットは余裕をもった大きさにしておくと良さそうです。 つづいて以下の記述を追加します。 # main.tf へ追記 # コンテナイメージを格納するためのリポジトリ resource "google_artifact_registry_repository" "workstation_image_repository" { repository_id = "workstation-image" format = "DOCKER" location = var.region description = "Artifact Registry for Cloud Workstations base images" } # コンテナイメージをプルするためのサービスアカウント resource "google_service_account" "workstation_cluster_service_account" { account_id = "workstation-cluster-sa" display_name = "Workstation Cluster Service Account" } # サービスアカウントへリポジトリ読み取り権限を付与 resource "google_project_iam_member" "workstation_cluster_service_account_roles" { project = var.project_id role = "roles/artifactregistry.reader" member = "serviceAccount:$ { google_service_account.workstation_cluster_service_account.email } " } 上記はポイント4 に関連する部分です。 Artifact Registory に workstation-image というリポジトリを作成しています。 加えて workstation-cluster-sa というサービスアカウントを作成し、Artifact Registory の読み取り権限を付与しています。 つづいての追記内容はこちら。 # main.tf へ追記 # Workstation Cluster resource "google_workstations_workstation_cluster" "workstation_cluster" { workstation_cluster_id = "workstation-cluster" provider = google-beta network = google_compute_network.workstation_network.id subnetwork = google_compute_subnetwork.workstation_subnet.id location = var.region labels = { environment = "development" } } # Workstation Config resource "google_workstations_workstation_config" "workstation_config" { provider = google-beta workstation_cluster_id = google_workstations_workstation_cluster.workstation_cluster.workstation_cluster_id workstation_config_id = "workstation-config" location = var.region idle_timeout = "3600s" # 1 hour running_timeout = "43200s" # 12 hours host { gce_instance { machine_type = var.workstation_cluster_machine_type disable_public_ip_addresses = true service_account = google_service_account.workstation_cluster_service_account.email # リポジトリ読み取り用サービスアカウント } } persistent_directories { mount_path = "/home" gce_pd { size_gb = var.workstation_cluster_persistent_disk_size_gb fs_type = "ext4" disk_type = "pd-ssd" reclaim_policy = "DELETE" } } container { image = "$ { var.region } -docker.pkg.dev/$ { var.project_id } /$ { google_artifact_registry_repository.workstation_image_repository.repository_id } /workstation-custom:latest" # Workstations コンテナイメージ (リージョン-docker.pkg.dev/プロジェクトID/workstation-image/workstation-custom:latest) } } 上記では Workstation Cluster と Workstation Config を定義しています。 とくに Workstation Config にはいくつかのパラメータを深掘りします。 idle_timeout と running_timeout どちらもリソース連続稼働によるコストを削減するための設定です。 idle_timeout は Workstations が操作を受け付けなかったときに一時停止されるまでの時間、 running_timeout は Workstations が一度立ち上がってから VM がシャットダウンされるまでの時間を定義できます(0にすることで機能をオフにもできます) 。 host Workstations が立ち上がるための GCE インスタンスのスペックや設定を記述します。 この中の service_account はポイント4 に関連しており、 先ほど記載したコンテナイメージプル用のサービスアカウントが参照されるように記載しています。 persistent_directories GCE インスタンスにマウントされる永続化ディスクの定義です。 /home にマウントしています。 ここは注意点ですが、Workstations がシャットダウンされると GCE インスタンスは削除されます。 再起動時に GCE インスタンスは再作成されますが、以前に作成したファイルなどは消えてしまうのです。 /home に永続化ディスクをマウントすることで、再起動時の GCE インスタンスでも /home での作業を引き継ぐことができます。 ただし reclaim_policy を DELETE に設定しているため、 Workstation が削除されたときに永続化ディスクも削除される設定になっている部分は意識しておきましょう。 container Workstation のコンテナイメージです。ポイント4 に関連しています。 以前のステップで記載した workstation-image リポジトリを参照しています。 このリポジトリの workstation-custom イメージの latest を使用する設定になっています。 (workstation-custom というイメージは Terraform の手順とは別でプッシュします) それでは、最後の追記内容です。 # main.tf へ追記 # アカウント の Workstation resource "google_workstations_workstation" "workstations" { provider = google-beta for_each = toset (var.user_accounts) workstation_id = "workstation-$ { replace ( replace (each.key, "@" , "-" ), "." , "-" ) } " workstation_cluster_id = google_workstations_workstation_cluster.workstation_cluster.workstation_cluster_id workstation_config_id = google_workstations_workstation_config.workstation_config.workstation_config_id location = var.region lifecycle { replace_triggered_by = [ google_workstations_workstation_config.workstation_config.id ] # Workstation Config } } # アカウントに対して workstations オペレータ閲覧者の権限を付与 resource "google_project_iam_member" "workstation_operation_viewer" { for_each = toset (var.user_accounts) project = var.project_id role = "roles/workstations.operationViewer" member = "user:$ { each.key } " } # アカウントごとに、自分の Workstations のみに対して workstations ユーザ権限を付与 resource "google_workstations_workstation_iam_member" "workstation_iam_user" { provider = google-beta for_each = google_workstations_workstation.workstations workstation_id = each.value.id workstation_cluster_id = google_workstations_workstation_cluster.workstation_cluster.workstation_cluster_id workstation_config_id = google_workstations_workstation_config.workstation_config.workstation_config_id location = var.region role = "roles/workstations.user" member = "user:$ { each.key } " } 上記では、Workstation 作成とアカウントへの権限付与を実施しています。ポイント2 および ポイント3 に関連する記述ですね。 for_each を用いて variables.tf に定義していた user_accounts のアカウントそれぞれに Workstation や権限付与の処理を行っています。 Workstation に対しては lifecycle の replace_triggered_by を設定しておきました。 Workstations にすでに参照されている Workstation Config を更新する際、依存関係が存在するせいで更新ができないと怒られる場合があります。 トリガーに Workstation Config を指定することで、Workstation Config の更新とともに Workstation の再作成され、更新が可能になります。 Workstation が一度削除されることになります (現状の設定だと永続化ディスクも一度削除されます) が、管理上都合がよいので設定しています。 ポイント3 について、あるユーザに自分の Workstation のみを使わせたい場合は以下の権限整理をすることで実現できます。 プロジェクトに対して roles/workstations.operationViewer Workstation に対して roles/workstations.user 必要なファイルは完成しました! 最後に terraform apply をしてあげれば Google Cloud の設定は完了です。 Workstation Cluster には 20分以上かかることもあるため、実行時間が長くても焦らないでください (自分は 30分かかりました)。 Workstation のためのコンテナイメージのビルドとプッシュ Google Cloud の設定は完了しましたが、最後にもう一仕事残っています。 Workstation のためのコンテナイメージがまだでしたね。 公式ドキュメント を参考に進めていきましょう。 まずベースイメージですが、こちらも 公式 により公開されています。 ここでは VS Code ベースの us-central1-docker.pkg.dev/cloud-workstations-images/predefined/code-oss:latest を使用します。 今回はこのイメージを拡張し、新たに Terraform がインストールされたイメージを作成します。 Dockerfile を以下のように作成します。 # Dockerfile FROM us-central1-docker.pkg.dev/cloud-workstations-images/predefined/code-oss:latest # 環境のアップデートと Terraform のインストール RUN apt-get update && \ apt-get install -y gnupg software-properties-common curl && \ curl -fsSL https://apt.releases.hashicorp.com/gpg | gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg && \ echo " deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $( lsb_release -cs ) main " | tee /etc/apt/sources.list.d/hashicorp.list && \ apt-get update && \ apt-get install -y terraform && \ terraform -version さらに拡張したい場合は、上記にならってレイヤを追加していけばよいです。 また、処理が多い場合はシェルスクリプトファイルに分けたくなってくるかもしれません。 その場合、コンテナの /etc/workstation-startup.d に シェルスクリプトを追加しておくことで 辞書順に実行を行ってくれる みたいですね。 それではコンテナイメージのビルドからプッシュまで行っていきます。 REGION や PROJECT_ID はご自身の環境に合わせて変更してください。 # 変数の定義 set REGION " asia-northeast1 " # リージョン set PROJECT_ID " Your Porject ID " # プロジェクトID set REPOSITORY_NAME " workstation-image " # Artifact Registry リポジトリ名 set IMAGE_NAME " workstation-custom " # Docker イメージ名 set ADDITIONAL_TAG " latest " # 追加タグ # 小文字の UUID を生成 (ハイフンなし) set VERSION (uuidgen | tr -d ' - ' | tr ' A-Z ' ' a-z ' ) # イメージ タグの構築 set IMAGE_URI " $REGION -docker.pkg.dev/ $PROJECT_ID / $REPOSITORY_NAME / $IMAGE_NAME : $VERSION " set ADDITIONAL_URI " $REGION -docker.pkg.dev/ $PROJECT_ID / $REPOSITORY_NAME / $IMAGE_NAME : $ADDITIONAL_TAG " # Docker ビルド docker build --platform linux/amd64 -t " $IMAGE_URI " . # タグ追加 docker tag " $IMAGE_URI " " $ADDITIONAL_URI " # Docker Push docker push " $IMAGE_URI " docker push " $ADDITIONAL_URI " latest のタグを持つイメージがプッシュされることを確認しておきましょう。 gcloud artifacts docker images list " $REGION -docker.pkg.dev/ $PROJECT_ID / $REPOSITORY_NAME / $IMAGE_NAME " \ --include-tags --format =" table[createTime, tags](createTime, tags) " CREATE_TIME TAGS 2024-11-27T09:32:02 9f8f7c0d69c04be095d2c4ff5d4cfb1a,latest 構築が完了しました! ご自身の Workstation が起動することを確認してください。 また、ターミナルを用いるとインストールされた terraform を確認できると思います。 おわりに フルマネージド開発環境として、Cloud Workstations を構築しました。 ここでまとめた構築方法をもとに、今後は twada 塾の受講生向け開発環境の払い出しを進めていく予定です。 それでは、明日の記事もお楽しみに!
こんにちは。NTTコミュニケーションズでエバンジェリストをやっている西塚です。今日が10年目の結婚記念日です。 この記事は、 NTT Communications Advent Calendar 2024 6日目の記事です。 情報通信白書 によると、デジタルデータの活用が企業経営に対して効果があると複数の先行研究で明らかにされています。 ビッグデータを活用している企業はそうでない企業に比べて、イノベーションの創出が統計学的に有意な差で多いと言われています。 私自身もNTTコミュニケーションズにおいて全社データ基盤を立ち上げて、社内システムからデータを収集し、 データサイエンティストと協力しながら、蓄積された膨大なデータを活用してビジネス価値を生み出す取り組みを行ってきました。 さて、近年の生成AIブームに乗り、データサイエンティスト達は従来の機械学習・AI技術に加えて生成AIをデータ活用に利用する取り組みをしています。 最終的に生成AIはデータサイエンティストの仕事を奪うことになるのでしょうか? 結論から言えば、生成AIはデータサイエンティストの仕事を一部奪うかもしれません。 しかし、それはデータサイエンスそのものの重要性が低下するという意味ではなく、むしろ「仕事の民主化」が進むことを意味します。 生成AIをデータ基盤の中で活用することで、データを効率的に管理し、活用できる環境が整うのです。 全社データ基盤の紹介 セキュリティ(Security) 高性能(High Performance) 安定性(Stability) 使いやすさ(Usability) 可観測性(Observability) Snowflakeによるクラウドリフトとデータ連携 基盤だけでは活用は進まない データを集めるプロセスの膨大な負担 蓄積するだけでは価値を生まない データサイエンスの民主化 生成AIはデータサイエンスのどの仕事を置き換えるか? 非構造化データの構造化 データクレンジング 探索的データ分析(EDA: Explanatory Data Analysis) 特徴量エンジニアリングの支援 PoC紹介 最後に 全社データ基盤の紹介 2020年に構築を開始した全社データ基盤は、当初オンプレで組み上げました。 データウェアハウス(DWH: Data Ware House)としてHadoopを採用し、データ活用のための標準インターフェースとしてTrino(旧Presto)を提供しました。 Trinoの取り組みについては2021年のアドベントカレンダーにて 高性能分散SQLエンジン「Trino」最速ガイド として紹介しました。 データサイエンティストの活躍については 社内でデータ分析コンペティションを開催しました で紹介しています。 全社データ基盤は「DLX(Datalake for Everything)」と名付けられ、「セキュリティ(Security)」「高性能(High Performance)」「安定性(Stability)」「使いやすさ(Usability)」「可観測性(Observability)」の5つの信条のもとに設計されました。 セキュリティ(Security) もっとも重要な信条は、社内機密データを取り扱う際のセキュリティの確保です。 情報漏えいを防ぐための厳格なセキュリティ対策が施されており、データの機密性と安全性が最優先されています。 これにより、企業の重要情報が不正アクセスや漏えいのリスクから守られます。 社外のセキュリティ会社によるペネトレーション試験を定期的に行い、セキュリティ向上および安全確保を実施しています。 高性能(High Performance) 高性能とは、大量のデータに対する高速な処理能力を指します。大規模データ分析の要求にこたえられるよう、HadoopやTrinoなどの分散処理フレームワークが採用されています。 安定性(Stability) 安定性の確保は、システムの中断なく安定したデータ活用を可能にします。障害に強い冗長設計やフェイルオーバー機能により、高い可用性を実現しています。 使いやすさ(Usability) 使いやすさは、データ活用の敷居を下げることを目指しています。Trinoの標準SQLによる一貫したデータアクセスが可能となり、データ分析のスピードと精度が飛躍的に向上しました。 可観測性(Observability) 可観測性とは、システム状況を的確に監視し、運用を最適化することを意味します。各種メトリクスの一元的な監視で、迅速な障害対応や負荷分散が可能になります。 こうした5つの信条に基づいて設計することで、セキュリティを確保しつつ、高速で安定した全社データ基盤が実現しました。 社内のユーザは組織の壁を超えて効率的かつ安全にデータを活用できるようになりました。 Snowflakeによるクラウドリフトとデータ連携 2023年からはクラウド型データウェアハウス(DWH)としてSnowflakeを採用し、オンプレからのクラウドリフトを進めています。 SnowflakeはSnowflake同士でデータシェアリング(データ共有)する機能を有しています。 データのメタデータや権限情報のみ共有する仕組みを採用しているため、データを物理的にコピーせずにリアルタイムでのデータ共有が可能になっています。 実は、NTTグループの間ではSnowflakeによる会社を超えたデータ共有の事例が進んでいます。 我々のデータ基盤も、主に以下の2つのSnowflakeと接続しています。 NTT持株のデータ基盤との接続: 共通系ITシステムのデータについて持株と共有しています NTTドコモのデータ基盤との接続: ドコモとの一部事業統合に伴い、「ドコモビジネス」の運営に伴う共同利用宣言に基づいて、NTTドコモのデータ基盤とデータ共有しています 基盤だけでは活用は進まない 全社データ基盤の構築に伴い、以下のような課題に直面しました。 データを集めるプロセスの膨大な負担 データを集約し、正確に整理する作業はデータ活用の約8割の稼働を占めるとも言われています。 データエンジニアの育成を進めていますが、稼働が非常にかかるところです。 蓄積するだけでは価値を生まない データがデータベースに眠っているだけでは、ビジネスに役立つ知見を得ることはできません。 ビッグデータを活用するためには、高度な分析や洞察を引き出すデータサイエンスの力が求められます。 従来のデータエンジニアリングやデータサイエンスは、専門知識を持つ人材に依存していました。 しかし、生成AIが登場したことで、この構造が大きく変わろうとしています。 データサイエンスの民主化 近年のLLM(大規模言語モデル)の技術進化により、自然言語対話によるデータサイエンス業務の民主化が進みつつあります。 例えば、「Function Calling」によりLLM側で適切なタスクを選択し、関数を呼び出すことが可能となっています。 この機能を活用することで、LLMと全社データ基盤を接続し、自然言語を介してデータを操作するユーザーインターフェースを実現できます。 このような考えはSnowflakeを始めとしたクラウドDWH製品においても、一般的になっています。 Snowflake社は先日11/20に Anthropic社のClaude 3.5 Sonnetのモデルがデータベース上で使えるようになることを 発表 しました 。 また、Databricks社は 独自のLLM(DBRX) の開発を進めています。 各種のDWH製品に自然言語によるデータ探索やグラフ描画機能が搭載されつつあり、このようなインターフェースが主流になることが想定されています。 生成AIはデータサイエンスのどの仕事を置き換えるか? 生成AIは、以下のような業務を効率化・自動化する可能性を秘めています。 非構造化データの構造化 非構造化データ(例: テキストや画像)を表形式のデータに変換し、分析可能な形に整える。 データクレンジング 不完全なデータを補完したり、投入後のデータ処理(ETL: Extract, Transform, Load)のアシストを行う。 探索的データ分析(EDA: Explanatory Data Analysis) 異常値やトレンドを検出し、日々のレポート作成を自動化する。 特徴量エンジニアリングの支援 モデル精度を向上させるための特徴量設計をサポートする。 これらのタスクは従来、データサイエンティストやエンジニアが多大な時間と労力をかけて行っていたものです。 しかし、生成AIは疲れ知らずであり、コストも安いため、これらの仕事の効率化ができます。 企業データは量が膨大ですので人間が見るのは非人道的です。 ETLやEDAなどは、寝てる間に生成AIに任せましょう。 将来的には、生成AIがデータ基盤内で日々蓄積される膨大なデータを整理し、異常値を検出し、必要に応じて分析の方向性を提案してれるような世界観を目指していきたいと考えています。 まとめると、生成AIによるデータサイエンスの民主化とは以下のステップで表されます。 非構造化データの処理やクレンジングの自動化で手間を削減 データ分析結果の可視化や異常検知において生成AIのアシストで専門知識を不要に ビジネスパーソンが対話的にデータを活用し、インサイトを得る環境が整備される PoC紹介 最後に、夢物語を語ってるのではなく実際に動くPoCがあることを紹介します。 我々のデータ基盤のアクセスログに対し、異常値を検出し対話的に深掘りができるチャットbotに人格を付与してslackに住まわせています。 以下の会話は、異常とされたアクセスログについて自然言語で深掘りしています。 以下のようなシステムセッティングで動かしています。 非常に面白いユースケースだと個人的に思います。 最後に 生成AIによる企業データの利用においては、RAGによる非構造データの利用がより注目されているように感じますが、 データ基盤との接続にこそ価値があると考えています。 データエンジニアやデータサイエンティストの仕事を肩代わりし、その役割をさらに高度化する可能性があります。 また、生成AIはデータサイエンスの民主化を加速し、専門的なスキルを持たない人々でもデータの価値を引き出せる未来を実現します。 これにより、データ活用の裾野が広がり、企業全体でのデータドリブンな意思決定が進みます。 私たちは引き続き、この可能性を追求していきたいと考えています。
この記事は、 NTT Communications Advent Calendar 2024 4日目の記事です。 はじめに この記事はコミュニケーション&アプリケーションサービス部でビジネスdアプリを開発している木村、立木、富田、西谷の共同執筆です。 今回は、NTTコミュニケーションズで提供するモバイルアプリ、「ビジネスdアプリ」のアーキテクチャに焦点を当て、サーバレスサービスをどのように活用しているかを2回にわたって紹介します。 この記事(前編)では、開発背景やサーバレスサービスを活用したアーキテクチャの概要を中心に解説します。後編では、具体的な運用やCI/CDの仕組みに焦点を当てる予定です。 なお、本記事の内容は2024年8月2日にGoogle Cloud Next Tokyo '24で発表した講演をベースに再構築したものです。 講演資料はこちら 目次 はじめに 目次 ビジネスdアプリとは? 内製開発の工夫 開発言語の統一 機能で担当割り サーバレスサービスの導入 サーバレスのメリット サーバレスサービスを使用する上での工夫 アーキテクチャの概要 コンピューティング ストレージ データ管理 行動分析 終わりに ビジネスdアプリとは? 「 ビジネスdアプリ 」は、ドコモのビジネスユーザーを主な対象としたポータルアプリです。 仕事中・プライベートどちらにも使えるお得な特典や、経営層向け・従業員向けに、最新ニュースやオリジナルコンテンツを提供し、業界ごとに役立つ情報をスムーズに取得できます。 また、NTTコミュニケーションズが提供する業務効率化につながるサービス(ビジネスdシリーズ)の無料プラン/無料版をアプリ上で簡単に申し込みができ、本格導入を検討できる仕組みを実現しています。 ビジネスdアプリでは、AndroidとiOSの両方に対応し、どのデバイスでも統一された操作感を提供しています。 これらの機能を、わずか3か月で構築するために工夫したアーキテクチャの詳細をこれからご紹介します。 内製開発の工夫 ビジネスdアプリは、社員を中心としたスクラムでの内製開発となっています。 そこで、主に以下の3つの工夫を取り入れました。 開発言語の統一 機能で担当割り サーバレスサービスの導入 開発言語の統一 全ての層を以下の通りJavaScript系の技術で統一しました。 フロントエンド: React.js、Vue.js バックエンド: Node.js モバイルアプリ: React Native これにより、フロントエンド、バックエンド、モバイルアプリのコードベースを統一し、開発チームの誰もがどの機能も開発できるようにしました。この統一性により、スムーズなコミュニケーションと短期間での開発を進めることができます。 また、React Nativeを採用した理由として、1つのソースからiOS/Androidのアプリを一度に開発できるということもあります。 機能で担当割り チームメンバーに対して機能単位の担当を割り当て、フロントエンドからバックエンド、モバイルアプリまで同じ担当が一貫して同じ機能の開発をするようにしました(例:ユーザー認証やプッシュ通知など)。これにより、各メンバーがシステム全体を理解しやすくなり、コミュニケーションコストを大幅に減らすことができました。 また、属人化を防ぐため、各種ローテーション、ドキュメント化、API化するなどして全体で共有する工夫を行なっています。 サーバレスサービスの導入 短期間の構築であったため、基本的に開発者はプログラム開発に集中する環境を整える必要がありました。「ビジネスdアプリ」では、その解決策として、Google Cloudのサーバレスサービス、フルマネージメントサービスを全面的に採用し、サーバーの構築や運用にかかる手間を最小限に抑えました。 たとえば、Google App Engine(以下、App Engine)やCloud Firestore(以下、Firestore)を使うことで、オートスケールに対応できます。 ここからはサーバレスサービスの導入について解説します。 サーバレスのメリット 「ビジネスdアプリ」では、Google Cloudのサーバレスサービスを最大限に活用しました。特にメリットを感じたのは以下の4点です。 構築作業の軽減 冗長化やスケーリング設定など、従来なら時間がかかる作業を簡易にできます。たとえばデータベースだと、FirestoreやCloud Spanner(以下、Spanner)を使うことで冗長化やスケーリングなどの対応が不要になり、プログラムに集中できました。 運用監視の簡略化 Google Cloudが提供するサーバレスサービスは障害が発生しても自動復旧します。また、Google Cloudが提供するモニタリング機能を活用することで、監視に関わる開発を減らすことができました。 EOL(End of Life)検討が不要 他社が多数導入しているサーバレスサービスを採用することで、Google Cloudによる長期的なサービス提供が期待でき、ライフサイクル管理の負担を軽減しました。 セキュリティ対策の簡易化 サーバレスサービスのセキュリティ確保はGoogle Cloudが対応しているため、IAMなどの権限管理を適切に行う必要はありますが、システム全体のセキュリティ対策の検討が簡易化できます。 サーバレスサービスを使用する上での工夫 サーバレスサービスには多くのメリットがありますが、注意すべき点もいくつか存在します。これらの点に対して、私たちのチームが工夫した点を紹介します。 デプロイ作業の自動化 サーバレスサービスのデプロイは容易ですが、それでも人手で繰り返しのデプロイを行うと稼働がかかります。そのため、GitHub Actionsを活用したCI/CDパイプラインを構築し、デプロイ作業を完全に自動化しました。 エンドツーエンド監視 Google Cloudの障害は Google Cloud Service Health で確認できますが、掲載されるより前に障害が発生している場合があります。そのため、サーバレスサービスの障害の早期検知のためにユーザー操作を疑似することで監視する、エンドツーエンドの監視を定期的に自動実行しています。これにより、ユーザー体験に影響を与える問題を素早く検出し、対応可能にしました。 リリースノートの定期的な確認 サーバレスサービスは頻繁にアップデートされます。破壊的なアップデートはもちろん、新機能によって今よりやりやすくなるという場合もあるため、リリースノートを定期的に確認しサービス変更時に迅速に対応する必要があります。 セキュリティ監視の実施 セキュリティ対策は基本的にGoogle Cloudが行なっているとはいえ、システム全体のセキュリティを確保するために、ログのチェックなどは開発者が定期的に確認する必要があります。そこで、Cloud Loggingを活用してアラート通知を設定し、問題が発生した際にすぐ対応できる仕組みを整えています。 サーバレスサービスに関する学習 サーバレスサービスはWebで公開されている情報が基本的に少ないため、Google Cloudが提供している研修を受講したり、Google Cloudに関する資格取得を社員の目標とすることで、チームメンバー全員が一定の技術水準を確保できるようにしています。 アーキテクチャの概要 本アプリのアーキテクチャでは、以下の技術スタックを採用し、シンプルかつ拡張性のある設計を目指しました。 コンピューティング 通常時の処理はApp Engineを採用し、一時的に高負荷な処理が必要になる場合にはCloud Runを採用しました。 App Engine:Google Cloudが提供する完全マネージド型のサーバレスプラットフォーム。 Cloud Run:コンテナ化されたアプリケーションをデプロイし、オンデマンドでスケーリングを行うサーバレスサービス。利用したリソース分だけ課金されるため、コスト効率も高く、負荷が急増する場面でも柔軟に対応可能です。 ストレージ Cloud Storage:オブジェクトストレージとして大容量データの保存をサポートするサービス。本アプリでは、画像、動画、その他の静的ファイルを管理しています。 データ管理 複雑なクエリを必要としない部分にはFirestoreを、複雑なクエリが求められる部分にはSpannerを採用し、それぞれの特性を活かしたデータ管理を実現しました。 Firestore:NoSQLのリアルタイムデータベースで、高速な読み書き性能と高いスケーラビリティを備えています。 Spanner:分散型リレーショナルデータベース(RDBMS)で、グローバル規模の一貫性と高可用性を備えています。 行動分析 Google Analytics:ユーザーの行動データを収集・分析するツール。本アプリでは、クリックイベント等を集計しています。 BigQuery:大規模データを分析するGoogle Cloudのデータウェアハウス。Google AnalyticsからBigQueryにログをシンクさせることで、ユーザーの操作履歴やイベントデータの詳細を分析しています。 それぞれの詳細および説明を割愛した機能については後編で解説しますのでお楽しみに。 終わりに 今回の記事では、ビジネスdアプリの紹介やアーキテクチャの概要、サーバーレスアーキテクチャのメリットについて紹介しました。 2月ごろに公開予定の後編の記事では、大規模なトラフィックを処理するためのアーキテクチャや、行動データ収集に関するアーキテクチャについて、より細かく解説する予定です。 また、現在ビジネスdアプリの開発チームでは、 生成AIを活用した開発効率化 を検討しております。具体的には、コード生成やテストの自動化、ログ分析の効率化といった分野での応用を検討しており、生成AIを導入することで、開発スピードの向上やチームの負担軽減を目指しています。 まだ模索中の取り組みではありますが、良い成果が得られれば、そちらも後日実際の効果や導入プロセスの詳細をブログ記事として皆さまにお伝えしたいと考えています! そんな ビジネスdアプリ は 11月29日に新しくタスク管理と社内報の機能をリリースしました! どちらも基本的なタスク管理(ToDo管理)機能や社内報の機能(グループ内のお知らせ配信・権限設定など)が含まれており、追加の申し込みなしで無料で使うことができます。 その他にも、お得なクーポンや中小企業向けのニュースコンテンツもあるので、もし興味があれば以下のリンク・QRコードからダウンロードしてみてください! ダウンロードリンク ちなみにタスク管理機能・社内報機能の開発においても、開発の上での改善(デプロイ自動化など)を継続的に実施しているため、機会があれば別記事で紹介できればと思っています。 それでは、明日の記事もお楽しみに!
こんにちは、情報セキュリティ部の原田とイノベーションセンターの竹中です。この記事では、モバイルネットワークのユーザプレーン技術である SRv6 MUP(Segment Routing over IPv6 Mobile User Plane)の解説と社内で行った検証についてご紹介いたします。 モバイルネットワークのアーキテクチャとSRv6 MUP 従来のモバイルネットワーク SRv6 Mobile User Plane 5Gネットワークへの SRv6 MUP の適用 MUP コントローラの設計 MUP コントローラの構成 PDU セッション確立時のシーケンス 検証 IS-IS の設定 IS-IS の確認 MUP Segment の設定 BGP による経路情報の広告 BGP の設定確認 疎通確認 UE-DN 間の通信 UE同士の折り返し通信 おわりに モバイルネットワークのアーキテクチャとSRv6 MUP SRv6 MUP とは、SRv6のネットワークプログラマビリティにより、モバイルネットワークのユーザープレーン (U-Plane) をシンプルかつ柔軟に構築するための技術です。 従来のモバイルネットワーク SRv6 MUP の詳説の前に、まずは現状のモバイルネットワークのアーキテクチャについて解説します。モバイル端末が通信するとき、すべてのユーザトラフィックは基地局と交換機(UPF: User Plane Function)との間において構築されたトンネル(GTP-U)を介して、UPFから先の宛先ネットワークに転送されます。一方で、5Gでは、低遅延な処理を実現するためにMECを用いてユーザ近傍のノードで計算処理を行うユースケースや、自動運転における車車間通信のように端末同士で通信するユースケースなどが想定されています。しかし、先述のアーキテクチャだと、たとえトランスポートネットワーク的に近い距離のノード同士の通信であったとしても、User Plane Function (UPF) を介するためにパケットの転送距離は増大します。 上の図は従来のアーキテクチャにおいて UE1 から UE2 にパケットを転送する際のパケットの流れを示しています。GTP-U が基地局 (RAN) と UPF の間で構築されるため、図に示す U-Plane において UE1 発のパケットはトランスポートネットワークの先にある UPF を一度経由した上で、宛先である UE2 へ送信されます。 SRv6 Mobile User Plane SRv6 Mobile User Plane (SRv6 MUP) は、モバイルネットワークにおける U-Plane 部分に SRv6 を適用する技術です。SRv6 の持つネットワークプログラミング機能を活用してGTP-UパケットとSRv6パケットをステートレスに相互変換したり、従来のGTP-UパケットのU-Plane処理をSRv6パケットのファンクション処理に置き換えることで、U-Plane通信を従来のアーキテクチャと比較してより柔軟に扱います。 RFC9433 では、SRv6 MUP のさまざまな U-Plane 適用手法が言及されています。本検証では IETF でも議論中である、 draft-mhkk-dmm-mup-architecture-01 の MUP アーキテクチャ (以下、採用アーキテクチャ)を参考にモバイルネットワークを構築し、UE と DN 間の通信や UE 間の通信を実現しました。 採用アーキテクチャの適用によって、SRv6 ネットワークのさまざまな拠点に点在する MUP 処理可能なルータ(MUP PE)各々がモバイルネットワークのU-Planeにおける GTP-U エンドポイントの役割を担うことで、トランスポートネットワークにおける最適経路とは遠く離れた位置に存在し得る UPF を介さない通信が可能になります。 また、既存の5Gネットワークの構成要素 (5GC、gNB) をそのまま利用しつつ、U-Plane をネットワークプログラマビリティが高く柔軟にパケットを処理できる SRv6 ネットワークへと置き換えることが可能になります。 上の図は採用アーキテクチャにおいて UE1 から UE2 にパケットを転送する際のパケットの流れを示しています。MUP PE が UPF の役割を担うことによって GTP-U が基地局 (RAN) と MUP-PE の間で構築されるため、図に示すU-Planeにおいて UE1 発のパケットは SRv6 MUP ネットワークへ流入する MUP PE で折り返して宛先である UE2 へ送信されます。 5Gネットワークへの SRv6 MUP の適用 採用アーキテクチャに従って U-Plane に SRv6 MUP ネットワークを適用するためには、SRv6 MUP ネットワークにおいて以下の機能が必要になります。 SRv6 MUP ネットワーク内のそれぞれの MUP PE が持つ N3 ネットワークセグメント、N6 ネットワークセグメント (総称して MUP Segment) の情報を共有し、 各 MUP PE が 5GC の管理するセッション情報を経路情報として受信した上で、 MUP Segment とセッション情報を元に U-Plane のパケット転送用経路を作成する 採用アーキテクチャにおいては、MUP PE 間で自身が接続している N3・N6 ネットワークセグメントを BGP を用いて経路情報として共有します。 ここで N3 ネットワークセグメントは MUP Segment のうち Interwork Segment として扱われ、関連する経路情報は Interwork Segment Discovery(ISD) 経路として扱われます。 ISD 経路情報には、SRv6 ネットワークに所属する全ての MUP PE が、対応する Interwork Segment へ到達するために必要な情報が含まれます。 また、 N6 ネットワークセグメントは MUP Segment のうち Direct Segment として扱われ、関連する経路情報は Direct Segment Discovery(DSD) 経路として扱われます。 DSD 経路情報には、SRv6 ネットワークに所属する全ての MUP PE が、対応する Direct Segment へ到達するために必要な情報が含まれます。 続いて 5GC が管理するセッション情報の受信についてです。 5G ネットワークのアーキテクチャにおいて、Session Management Function (SMF) と UPF との間では Packet Forwarding Control Protocol (PFCP) を用いてセッション情報を伝達していますが、採用アーキテクチャでは セッション情報は MUP コントローラ (MUP-C) によって経路情報に変換され、BGP を用いて MUP-C から MUP PE に共有されます。 セッション情報のうち、アクセス側の情報は Type 1 Session Transformed (T1ST) 経路として、コア側の情報は Type 2 Session Transformed (T2ST) 経路として扱われます。 採用アーキテクチャにおいて、T1ST 経路情報は MUP PE が ISD と組み合わせてuplinkの経路を生成するために、T2ST 経路情報は MUP PE が DSD と組み合わせてdownlinkの経路を生成するために利用します。uplinkとdownlinkのそれぞれの経路が SRv6 MUP ネットワーク内に適用されることで E2E の通信が可能となります。 本ブログにおいては、SMF から UPF へ送信されるセッション情報に対応する経路を生成する MUPコントローラの設計・実装と、実装した MUP コントローラを 5GC と SRv6 ネットワークの中継機器として適用することで SRv6 MUP を実現します。 なお、以降では説明のために N3 ネットワークセグメントへの接続を持つ MUP PE を特に MUP GWと呼びます。 MUP コントローラの設計 MUP コントローラの構成 採用アーキテクチャにおける MUP コントローラ(MUP-C)は、 SMF と SRv6 ネットワークの中間に位置します。UE のセッション情報が含まれる PFCP パケットを解釈し、経路を決定する機能群(以下、セッション解析機能)と、実際に BGP で経路を設定する機能群(経路広告機能)からなる構成としました。 セッション解析機能部では、PFCP パケットに含まれる UE のセッション情報を取得し、T1ST 経路と T2ST 経路をそれぞれ生成します。T1ST 経路は UE の IP アドレス、TEID、QFI、gNB の IP アドレスから構成され、T2ST 経路は UPF アドレスのプレフィックスと TEID から構成されます。 経路広告機能部では、GoBGP を使用しました。SRv6 MUP のアーキテクチャでは、BGP によって経路情報を広告します。GoBGP では MUP に対応した BGP 拡張が実装されているため、これを使うことにしました[ https://github.com/osrg/gobgp/blob/master/docs/sources/srv6_mup.md ]。セッション解析機能部で生成した経路情報は、gRPC で GoBGP に投入されます。 PDU セッション確立時のシーケンス 続いて、本検証での PDU セッションが確立するまでのシーケンスについて解説します。こちらの図は UE の接続要求をトリガーとした PDU セッション確立までの流れです。通常の 5GC との差異は、N4 でやり取りする相手が UPF ではなく MUP-C な点、MUP-C と MUP PE 間で BGP を用いて経路を広告する点です。赤字の箇所が新たに追加したシーケンスとなります。 既存の 5GC を変えない形で SRv6 MUP を利用するため、SMF から見た MUP-C の振る舞いは UPF と同等になるよう実装しました。内部的には、N4 Session Establishment/Modification Requestを受け取ると、セッション解析機能部分で T1ST 経路と T2ST 経路を生成するのに必要なパラメータをパースしたのち、GoBGP に gRPC で経路情報を投入します。gRPC リクエストを受け取った GoBGP は配下の MUP PE に経路を広告します。広告が完了すると、MUP-C は SMF に対して N4 Session Establishment/Modification Response を返し、以降は通常の PDU セッション確立のシーケンスに戻ります。 検証 今回の検証で使用した機材や OSS は下記のとおりです。MUP-C におけるセッション情報取得部分は前章「MUP コントローラの設計」を元に内製開発しました。 5GC…free5GC UE/RAN…UERANSIM SRv6 MUP対応ルータ...古河電工 FITELnet F220 EX (検証用ファームウェア) MUPコントローラ(セッション解析部)...内製開発 MUPコントローラ(経路広告部)...GoBGP、FRRouting 検証構成は下図のとおりです。 続いて、トポロジに従った SRv6 ネットワークを構成するための機器設定と状態を確認します。 なお、各種 5GC の config や各インターフェース設定などの基本設定や SRv6 利用のための初期設定などはすでに実施されているものとし、SRv6 ネットワークを構成するための IS-IS の設定と、MUP の経路情報を交換するための BGP の設定と確認を実施します。 IS-IS の設定 IS-IS を用いて、SRv6 encap されたパケットを転送するための経路情報を交換します。 なお本検証におきましては、MUP-C も FRRouting を用いて IGP domain に所属していますが、BGP セッションのための Loopback Address 広告と IPv6 (not SRv6) のパケット転送用の設定のみのため、設定の紹介は割愛します。 MUP PE DN への接続に利用するための Locator を定義して広告します。 interface Loopback 1 ipv6 address 2001:db8::91 ipv6 address 2001:db8:91:c0a8:ac0c::5b ipv6 router isis core exit ! interface Port-channel 2 ipv6 enable ipv6 router isis core exit ! interface Port-channel 3 ipv6 enable ipv6 router isis core exit ! router isis core is-type level-2 net 49.0000.0000.0091.00 srv6 locator N6DN topology ipv6-unicast exit ! segment-routing srv6 encapsulation source-address 2001:db8:91:c0a8:ac0c::5b locator N6DN 2001:db8:0:91::/64 exit MUP GW DN と RAN への接続に利用するための Locator をそれぞれ定義して広告します。 interface Loopback 1 ipv6 address 2001:db8::100 ipv6 router isis core exit ! interface Port-channel 2 ipv6 enable ipv6 router isis core exit ! interface Port-channel 3 ipv6 enable ipv6 router isis core exit ! router isis core is-type level-2 net 49.0000.0000.0100.00 srv6 locator N3RAN srv6 locator N6DN topology ipv6-unicast exit ! segment-routing srv6 encapsulation source-address 2001:db8::100 locator N3RAN 2001:100::/32 locator N6DN 2001:db8:0:100::/64 exit IS-IS の確認 IS-IS によって各ルータの Loopback Address と SRv6 Locator の経路を相互に学習していることが確認できます。 MUP PE MUP-PE#sh ipv6 route isis Codes: K - kernel route, C - connected, S - static, R - RIPng, O - OSPFv3, B - BGP, T - Tunnel, i - IS-IS, V - VRRP track, Iu - ISAKMP SA up, It - ISAKMP tunnel route, Ip - ISAKMP l2tpv2-ppp Dr - DHCPv6-PD-relay, Dc - DHCP-client, Ds - DHCP-server, r - RA L - Local Breakout > - selected route, * - FIB route, p - stale info i > * 2001:100::/32 [115/30] via fe80::beef:beef:beef:beef, port-channel3, 00:00:12 i > * 2001:db8::100/128 [115/30] via fe80::beef:beef:beef:beef, port-channel3, 00:00:12 i > * 2001:db8::101/128 [115/20] via fe80::beef:beef:beef:beef, port-channel3, 00:00:12 i > * 2001:db8:0:100::/64 [115/30] via fe80::beef:beef:beef:beef, port-channel3, 00:00:12 MUP GW MUP-GW#sh ipv6 route isis Codes: K - kernel route, C - connected, S - static, R - RIPng, O - OSPFv3, B - BGP, T - Tunnel, i - IS-IS, V - VRRP track, Iu - ISAKMP SA up, It - ISAKMP tunnel route, Ip - ISAKMP l2tpv2-ppp Dr - DHCPv6-PD-relay, Dc - DHCP-client, Ds - DHCP-server, r - RA L - Local Breakout > - selected route, * - FIB route, p - stale info i > * 2001:db8::91/128 [115/30] via fe80::beef:beef:beef:beef, port-channel3, 00:01:25 i > * 2001:db8::101/128 [115/20] via fe80::beef:beef:beef:beef, port-channel3, 00:01:25 i > * 2001:db8:0:91::/64 [115/30] via fe80::beef:beef:beef:beef, port-channel3, 00:01:25 i > * 2001:db8:91:c0a8:ac0c::5b/128 [115/30] via fe80::beef:beef:beef:beef, port-channel3, 00:01:25 MUP Segment の設定 MUP PE には N6 に接続するための Direct Segment、MUP GW には Direct Segment に加えて、N3 に接続するための Interwork Segment をそれぞれ N6DN VRF、N3RAN VRF として定義します。 MUP PE ip vrf N6DN description N6DN rd 65000:2 route-target import 100.0.0.101:1 route-target import 65000:2 route-target export 65000:2 segment-routing srv6 locator N6DN segment-routing srv6 mup-segment direct 65000:2 exit ! interface Port-channel 4 ip vrf forwarding N6DN ip address 192.168.60.11 255.255.255.0 ipv6 enable exit MUP GW ip vrf N3RAN description N3RAN rd 65000:1 route-target import 100.0.0.101:1 route-target import 65000:1 route-target export 65000:1 segment-routing srv6 locator N3RAN segment-routing srv6 mup-segment interwork exit ! ip vrf N6DN description N6DN rd 65000:2 route-target import 100.0.0.101:1 route-target import 65000:2 route-target export 65000:2 segment-routing srv6 locator N6DN segment-routing srv6 mup-segment direct 65000:2 exit ! interface Port-channel 4 ip vrf forwarding N3RAN ip address 192.168.173.13 255.255.255.0 exit BGP による経路情報の広告 BGP によって MUP PE、MUP GW で MUP Segment と VPN 経路を広告します。本検証では N3、N6 はそれぞれ IPv4 ネットワークのため、IPv4 SRv6-MUP と VPNv4 の Address Family を広告します。 本検証においては MUP-C の GoBGP が Route Reflector も兼ねているため、GoBGP には Route Reflector 用の config を投入します。 MUP PE router bgp 65000 bgp router-id 100.0.0.91 bgp log-neighbor-changes neighbor 2001:db8::101 remote-as 65000 neighbor 2001:db8::101 update-source loopback 1 ! address-family vpnv4 segment-routing srv6 neighbor 2001:db8::101 activate neighbor 2001:db8::101 capability extended-nexthop-encoding neighbor 2001:db8::101 send-community both exit ! address-family ipv4 srv6-mup neighbor 2001:db8::101 activate neighbor 2001:db8::101 send-community both exit ! address-family ipv4 vrf N6DN redistribute connected redistribute static exit ! exit MUP GW router bgp 65000 bgp router-id 100.0.0.100 bgp log-neighbor-changes neighbor 2001:db8::101 remote-as 65000 neighbor 2001:db8::101 update-source loopback 1 ! address-family vpnv4 segment-routing srv6 neighbor 2001:db8::101 activate neighbor 2001:db8::101 capability extended-nexthop-encoding neighbor 2001:db8::101 send-community both exit ! address-family ipv4 srv6-mup neighbor 2001:db8::101 activate neighbor 2001:db8::101 send-community both exit ! address-family ipv4 vrf N3RAN redistribute connected redistribute static exit ! address-family ipv4 vrf N6DN redistribute connected redistribute static exit MUP-C (GoBGP) MUP-C:~/gobgp$ cat conf.toml [global.config] as = 65000 router-id = "100.0.0.101" [[neighbors]] [neighbors.config] peer-as = 65000 local-as = 65000 neighbor-address = "2001:db8::91" [neighbors.transport.config] local-address = "2001:db8::101" [neighbors.route-reflector.config] route-reflector-client = true route-reflector-cluster-id = "0.0.0.1" [[neighbors.afi-safis]] [neighbors.afi-safis.config] afi-safi-name = "ipv4-mup" [[neighbors.afi-safis]] [neighbors.afi-safis.config] afi-safi-name = "l3vpn-ipv4-unicast" [[neighbors]] [neighbors.config] peer-as = 65000 local-as = 65000 neighbor-address = "2001:db8::100" [neighbors.transport.config] local-address = "2001:db8::101" [neighbors.route-reflector.config] route-reflector-client = true route-reflector-cluster-id = "0.0.0.1" [[neighbors.afi-safis]] [neighbors.afi-safis.config] afi-safi-name = "ipv4-mup" [[neighbors.afi-safis]] [neighbors.afi-safis.config] afi-safi-name = "l3vpn-ipv4-unicast" BGP の設定確認 BGP セッションによって SRv6 MUP を実現するための MUP Segment や VPN 経路が受信できていることを確認します。 MUP GW N6DN VRF に DN 向けの経路をインストールできているか、また MUP PE から広告された DSD 経路を受け取っているかを確認します。 MUP-GW#sh ip route vrf N6DN VRF: N6DN Codes: K - kernel route, C - connected, S - static, R - RIP, O - OSPF, B - BGP, T - Tunnel, i - IS-IS, V - VRRP track, Iu - ISAKMP SA up, It - ISAKMP tunnel route, Ip - ISAKMP l2tpv2-ppp Dc - DHCP-client, L - Local Breakout > - selected route, * - FIB route, p - stale info B > * 192.168.60.0/24 [200/0] via 2001:db8:0:91:45::, Tunnel1, 00:31:53 MUP-GW#sh ip route vrf N6DN 192.168.60.0/24 Routing entry for 192.168.60.0/24 Known via "bgp", distance 200, metric 0, best, redistributed Encapsulation Information: Tunnel Type: SRv6 Tunnel IF: Tunnel1 (Data: 0xf6a02e50) Tunnel ID: 229 Tunnel Endpoint: 2001:db8:0:91:49:: (System VRF-ID: 0) Tunnel Parameter: (SID list) 2001:db8:0:91:49:: Last update 02w1d22h ago 2001:db8:0:91:49::, Tunnel1 (Tunnel-ID:229), RD 65000:2, System VRF-ID 2, NHD LINK Tunnel1 (36), refcnt 4 MUP-GW#sh ip bgp ipv4 srv6-mup dsd detail [Direct Segment Discovery route] Route Distinguisher: 65000:2 BGP routing table entry for 100.0.0.91 Local 2001:db8::91 from 2001:db8::101 (100.0.0.91) Origin IGP, localpref 100, valid, internal, best Extended Community: RT:65000:2 SRv6-MUP:65000:2 Originator: 100.0.0.91, Cluster list: 0.0.0.1 Path Identifier (Remote/Local): /0 Last update: Sun Nov 3 20:16:17 2024 MUP PE MUP PE が ISD 経路を受け取れているかを確認します。 MUP-PE#sh ip bgp ipv4 srv6-mup isd detail [Interwork Segment Discovery route] Route Distinguisher: 65000:1 BGP routing table entry for 192.168.173.0/24 Local 2001:db8::100 from 2001:db8::101 (100.0.0.100) Origin IGP, localpref 100, valid, internal, best Extended Community: RT:65000:1 Originator: 100.0.0.100, Cluster list: 0.0.0.1 BGP Prefix-SID: SRv6 L3VPN 2001:100:42:: (L:16.16, F:16.0, T:0.0) End.M.GTP4.E Path Identifier (Remote/Local): /0 Last update: Sun Nov 3 20:58:48 2024 疎通確認 UE-DN 間の通信 まずは下記に示す、SRv6 ネットワークを介した UE-DN 間の通信について検証します。UE から DN の先のサーバに向けて ping を実行します。パケットはピンク色で示した経路を通ります。 UE の接続 UE を 5GC にアタッチし、PDU セッションを確立します。 MUP-C がセッション情報を T1ST/T2ST 経路に変換し、広告します。 mup-c:~$ gobgp global rib -a ipv4-mup | grep t1st *> [type:t1st][rd:100.0.0.101:1][prefix:10.10.10.1/32] 0.0.0.0 00:01:05 [{Origin: ?} {Extcomms: [100.0.0.101:1]}] mup-c:~$ gobgp global rib -a ipv4-mup | grep t2st *> [type:t2st][rd:100.0.0.101:1][endpoint-address-length:64][endpoint:192.168.172.12][teid:0.0.0.2] 0.0.0.0 00:01:07 [{Origin: ?} {Extcomms: [100.0.0.101:1], [65000:2]}] 受信した経路情報の確認 PDUセッションを確立したことにより、SRv6 MUP ネットワークにセッション情報を示す T1ST/T2ST 経路が広告されることを確認します。 MUP PE での確認例 MUP-PE#sh ip bgp ipv4 srv6-mup st1 10.10.10.1/32 [Type 1 Session Transformed route] Route Distinguisher: 100.0.0.101:1 BGP routing table entry for 10.10.10.1/32 Local 2001:db8::101 from 2001:db8::101 (100.0.0.101) Origin incomplete, localpref 100, valid, internal, best Extended Community: RT:100.0.0.101:1 Originator: 100.0.0.101, Cluster list: 0.0.0.1 TEID 00000001 (1), QFI 0x0, Endpoint 192.168.173.12 Path Identifier (Remote/Local): /0 Last update: Wed Nov 20 21:20:48 2024 MUP-GW#sh ip bgp ipv4 srv6-mup st2 192.168.172.12 [Type 2 Session Transformed route] Route Distinguisher: 100.0.0.101:1 BGP routing table entry for 192.168.172.12 Local 2001:db8::101 from 2001:db8::101 (100.0.0.101) Origin incomplete, localpref 100, valid, internal, best Extended Community: RT:100.0.0.101:1 SRv6-MUP:65000:2 Originator: 100.0.0.101, Cluster list: 0.0.0.1 Path Identifier (Remote/Local): /0 Last update: Wed Nov 20 21:21:18 2024 また、受け取った T1ST/T2ST を元に U-Plane 用の経路が生成されていることを確認します。 MUP GW (uplinkの経路情報) N3 から受け取った GTP-U パケットのヘッダが decap されて N6DN VRF の経路情報を参照することがわかります。 BGP による経路情報の広告において N6DN VRF には MUP PE が広告する DN への経路情報が含まれているため、DN へパケットが転送されます。 MUP-GW#sh ip route vrf N3RAN 192.168.172.12/32 Routing entry for 192.168.172.12/32 Known via "bgp", distance 200, metric 0, best, redistributed Encapsulation Information: Tunnel Type: SRv6 Tunnel IF: Tunnel1 (Data: 0xf6a02ae0) Tunnel ID: 229 Tunnel Endpoint: 2001:100:4c:: (System VRF-ID: 0) Last update 02w0d23h ago 2001:100:4c::, Tunnel1 (Tunnel-ID:229), RD 65000:1, System VRF-ID 1, NHD LINK Tunnel1 (38), refcnt 1 MUP-GW#sh segment-routing srv6 sid 2001:100:4c:: SID Function Context Owner State -------------------------- ------------ -------------------------------------------------- ----- --------- 2001:100:4c:: H.M.GTP4.D 'N3RAN':bsid BGP InUse Locator : N3RAN Length : 128 Nexthop : 2001:db8:0:100:41:: Link-ID : 37 Created : Mon Nov 4 20:26:40 2024 (02w1d23h ago) MUP-GW#sh segment-routing srv6 sid 2001:db8:0:100:41:: SID Function Context Owner State -------------------------- ------------ -------------------------------------------------- ----- --------- 2001:db8:0:100:41:: End.DT4 'N6DN':DN lookup BGP InUse Locator : N6DN Length : 128 VRF : N6DN Created : Tue Oct 29 17:32:14 2024 (03w1d01h ago) MUP PE (downlinkの経路情報) MUP GW から受け取った ISD 経路に登録されている End.M.GTP4.E Function を用いる経路が生成されていることが確認できます。 また、転送に利用する SID ( 2001:100:42:c0a8:ad0c::100 ) の Argument 部分 ( c0a8:ad0c::100 ) には MUP GW にて GTP-U パケットに変換するための gNB アドレス (=c0a8:ad0c=192.168.173.12 )、QFI (=0)、TEID (=1) の情報が含まれています。 MUP-PE#sh ip route vrf N6DN 10.10.10.1/32 Routing entry for 10.10.10.1/32 Known via "bgp", distance 200, metric 0, best, redistributed Encapsulation Information: Tunnel Type: SRv6 Tunnel IF: Tunnel1 (Data: 0xf6a01930) Tunnel ID: 230 Tunnel Endpoint: 2001:100:42:c0a8:ad0c::100 (System VRF-ID: 0) Tunnel Parameter: (SID list) 2001:100:42:c0a8:ad0c::100 Last update 00:05:44 ago 2001:100:42:c0a8:ad0c::100, Tunnel1 (Tunnel-ID:230), RD 65000:2, System VRF-ID 1, NHD LINK Tunnel1 (20), refcnt 1 ping による疎通確認 UE(10.10.10.1)とサーバ(192.168.60.12)間で ping を実行した結果を示します。gNB でキャプチャしたGTP-U パケットはこちらです。 続いて、GTPカプセリングされたICMPパケットがMUP-GWを通過した際のSRv6パケットです。先程のGTP-U パケットがSRv6パケットに置き換わっていることが分かります。 downlink において MUP-PE を通過した際の SRv6 パケットです。src には UPF のアドレス(192.168.172.12→c0a8:ac0c)が、 dst には gNB のアドレス(192.168.173.12→a0c8:ad0c)、QFI(0)、TEID(1) が埋め込まれています。 downlink 方向の GTP-U パケットです。downlink の SRv6 の src/dst アドレスに埋め込まれた UPFのアドレス、gNBのアドレス、TEID が GTP-U パケットに反映されています。これで、UE に正しく ping reply が届くことを確認しました。 UE同士の折り返し通信 続いて、SRv6 MUPによって経路遅延が削減できるユースケースである、MUP GWによるUE同士の折り返しとなる通信について検証します。片方のUEからpingを実行し、もう片方のUEで受信します。トラフィックはピンク色で示した経路を通ります。 UE の接続 UE を 5GC にアタッチし、PDU セッションを確立します。 MUP-C がセッション情報を T1ST/T2ST 経路に変換し、広告します。 MUP-C:~$ gobgp global rib -a ipv4-mup | grep t1st *> [type:t1st][rd:100.0.0.101:1][prefix:10.10.10.3/32] 0.0.0.0 00:40:26 [{Origin: ?} {Extcomms: [100.0.0.101:1]}] *> [type:t1st][rd:100.0.0.101:1][prefix:10.10.10.2/32] 0.0.0.0 00:00:04 [{Origin: ?} {Extcomms: [100.0.0.101:1]}] MUP-C:~$ gobgp global rib -a ipv4-mup | grep t2st *> [type:t2st][rd:100.0.0.101:1][endpoint-address-length:64][endpoint:192.168.172.12][teid:0.0.0.4] 0.0.0.0 00:00:06 [{Origin: ?} {Extcomms: [100.0.0.101:1], [65000:2]}] *> [type:t2st][rd:100.0.0.101:1][endpoint-address-length:64][endpoint:192.168.172.12][teid:0.0.0.3] 0.0.0.0 00:40:28 [{Origin: ?} {Extcomms: [100.0.0.101:1], [65000:2]}] 経路情報の確認 UE を接続したことにより、SRv6 MUP ネットワークにセッション情報を示す T1ST/T2ST 経路が広告されることを確認します。今回は UE を2台接続したため、2種類の T1ST 経路が見えます。 MUP PE での確認例 MUP-PE#sh ip bgp ipv4 srv6-mup st1 10.10.10.2/32 [Type 1 Session Transformed route] Route Distinguisher: 100.0.0.101:1 BGP routing table entry for 10.10.10.2/32 Local 2001:db8::101 from 2001:db8::101 (100.0.0.101) Origin incomplete, localpref 100, valid, internal, best Extended Community: RT:100.0.0.101:1 Originator: 100.0.0.101, Cluster list: 0.0.0.1 TEID 00000001 (1), QFI 0x0, Endpoint 192.168.173.14 Path Identifier (Remote/Local): /0 Last update: Mon Nov 18 20:36:58 2024 MUP-PE#sh ip bgp ipv4 srv6-mup st1 10.10.10.3/32 [Type 1 Session Transformed route] Route Distinguisher: 100.0.0.101:1 BGP routing table entry for 10.10.10.3/32 Local 2001:db8::101 from 2001:db8::101 (100.0.0.101) Origin incomplete, localpref 100, valid, internal, best Extended Community: RT:100.0.0.101:1 Originator: 100.0.0.101, Cluster list: 0.0.0.1 TEID 00000002 (2), QFI 0x0, Endpoint 192.168.173.12 Path Identifier (Remote/Local): /0 Last update: Mon Nov 18 20:37:24 2024 MUP-PE#sh ip bgp ipv4 srv6-mup st2 192.168.172.12 [Type 2 Session Transformed route] Route Distinguisher: 100.0.0.101:1 BGP routing table entry for 192.168.172.12 Local 2001:db8::101 from 2001:db8::101 (100.0.0.101) Origin incomplete, localpref 100, valid, internal, best Extended Community: RT:100.0.0.101:1 SRv6-MUP:65000:2 Originator: 100.0.0.101, Cluster list: 0.0.0.1 Path Identifier (Remote/Local): /0 Last update: Mon Nov 4 20:26:05 2024 また、受け取った T1ST/T2ST 経路と MUP PE / MUP GW 間で交換した MUP Segment の情報を元に U-Plane 用の経路が生成されていることを確認します。 UE 間通信では MUP GW で折り返して通信するため、MUP GW の経路情報を確認します。 MUP GW (uplink の経路情報) N3 から受け取った GTP-U パケットのヘッダが decap されて N6DN VRF の経路情報を参照することがわかります。 BGP による経路情報の広告において N6DN VRF には MUP PE が広告する DN への経路情報が含まれているため、DN へパケットが転送されます。 MUP-GW#sh ip route vrf N3RAN 192.168.172.12/32 Routing entry for 192.168.172.12/32 Known via "bgp", distance 200, metric 0, best, redistributed Encapsulation Information: Tunnel Type: SRv6 Tunnel IF: Tunnel1 (Data: 0xf6a02ae0) Tunnel ID: 229 Tunnel Endpoint: 2001:100:4c:: (System VRF-ID: 0) Last update 02w0d23h ago 2001:100:4c::, Tunnel1 (Tunnel-ID:229), RD 65000:1, System VRF-ID 1, NHD LINK Tunnel1 (38), refcnt 1 MUP-GW#sh segment-routing srv6 sid 2001:100:4c:: SID Function Context Owner State -------------------------- ------------ -------------------------------------------------- ----- --------- 2001:100:4c:: H.M.GTP4.D 'N3RAN':bsid BGP InUse Locator : N3RAN Length : 128 Nexthop : 2001:db8:0:100:41:: Link-ID : 37 Created : Mon Nov 4 20:26:40 2024 (02w1d23h ago) MUP-GW#sh segment-routing srv6 sid 2001:db8:0:100:41:: SID Function Context Owner State -------------------------- ------------ -------------------------------------------------- ----- --------- 2001:db8:0:100:41:: End.DT4 'N6DN':DN lookup BGP InUse Locator : N6DN Length : 128 VRF : N6DN Created : Tue Oct 29 17:32:14 2024 (03w1d01h ago) MUP GW (downlink の経路情報) GTP-U ヘッダが decap されて N6DN に転送されてきた UE 向けのパケットは、 GTP encap されて N3RAN へ転送されます。ここで、 System VRF-ID: 1 は N3RANを示しています。 MUP-GW#sh ip route vrf N6DN 10.10.10.2/32 Routing entry for 10.10.10.2/32 Known via "bgp", distance 200, metric 0, best, redistributed Encapsulation Information: Tunnel Type: SRv6 Tunnel IF: Tunnel1 (Data: 0xf6a036d0) Tunnel ID: 229 Tunnel Endpoint: 192.168.173.14 (System VRF-ID: 1) Tunnel Parameter: (GTP encap) System VRF-ID: 1 Endpoint: 192.168.173.14 TEID: 1 QFI: 0x0 Last update 01d01h29m ago 192.168.173.14, Tunnel1 (Tunnel-ID:229), RD 65000:2, System VRF-ID 2, NHD LINK Tunnel1 (36), refcnt 4 MUP-GW#sh ip route vrf N6DN 10.10.10.3/32 Routing entry for 10.10.10.3/32 Known via "bgp", distance 200, metric 0, best, redistributed Encapsulation Information: Tunnel Type: SRv6 Tunnel IF: Tunnel1 (Data: 0xf6a02388) Tunnel ID: 229 Tunnel Endpoint: 192.168.173.12 (System VRF-ID: 1) Tunnel Parameter: (GTP encap) System VRF-ID: 1 Endpoint: 192.168.173.12 TEID: 2 QFI: 0x0 Last update 01d01h28m ago 192.168.173.12, Tunnel1 (Tunnel-ID:229), RD 65000:2, System VRF-ID 2, NHD LINK Tunnel1 (36), refcnt 4 ping による疎通確認 UE 間(10.10.10.2 / 10.10.10.3)とサーバ(192.168.60.12)間で ping を実行した結果を示します。gNB でキャプチャしたGTP-U パケットはこちらです。 下側のgNBでキャプチャした結果を示します。また、SRv6 NW側にはパケットが到達せず、MUP GWで折り返して通信できていることが確認できました。 おわりに 本記事では、我々の環境にて SRv6 MUP の検証を実施した際のネットワークやコントローラの設計、そして5Gネットワークを使用した疎通確認までの一連の流れを紹介いたしました。特にSRv6 MUPによる経路遅延の削減効果が大きいUE間通信について検証し、動作を確認しました。今後もSRv6 MUPの動向について、継続的にウォッチしていきます。
AIロボット部(社内サークル)では、子ども向けロボット教室を開催しました。 今年は、懐中電灯の光を利用して進行方向を指示するロボットを制作しました。 偏光板と光抵抗を使った分圧回路を活用し、簡単な電子工作の知識で実現可能な仕組みを採用しました。 ファミリーデーとロボット教室 ロボット教室の内容 方向指示のしくみ 準備に関して プロトタイプの作成 1. 手軽に入手できる部品で構成する 2. できるだけシンプルな仕組みにする 3. ロバスト性の確保 基板作成、調達 昨年の反省は活かされたのか? 当日の様子 おわりに(+社外イベントへの出展のおしらせ) 但し書き こちらは、NTT Communications Advent Calendar 2024の3日目の記事です。 こんにちは、ロボット部 部員の上田です! 本日はロボット部が夏に行った、子ども向けロボット教室の内容を書きます。 ※ ロボット部は社内の非公式なサークル活動です。ただいま絶賛部員を募集中です! なお、以前に書いた 記事はこちら ファミリーデーとロボット教室 NTTドコモグループでは毎年夏にファミリーデーという催しがあり、NTT Comでは社員の家族をオフィスに招いてNTT Comの取り組みの紹介やワークショップを行っています。 このファミリーデーに枠を貰い、我々ロボット部で子ども向けロボット教室を開催しました。 今年のファミリーデーの様子は、NTT ComのX(旧Twitter)アカウントの ポスト でも紹介されています。 ロボット教室の内容 ロボット部では、昨年(2023年)からロボット制作教室を開催しています。 ファミリーデーの数ヶ月前から、有志メンバーで企画内容の立案や検証、製造などの作業を進めました。 今年は偏光と光抵抗を利用した分圧回路を利用して進行方向を指示するロボット(ラジコン)の工作教室を行いました。 完成品はこちらになります。 手前の懐中電灯と基板上の白いセンサ部分に偏光板を張り付けています。 なお、ロボットの上に乗っているは、gooのキャラクター メグたんをモデリングして3Dプリンタで出力した人形です。 操作方法は、下記の図のように懐中電灯の光をセンサーに当て、懐中電灯を回すことにより進行方向を指示します。 子どもたちが組み立てたロボットを実際に走らせている様子がこちらになります。 工夫したポイントとしては、偏光の性質と抵抗分圧回路、コンパレータ、モータ、ダイオード、トランジスタといった電子工作をする際の基本的な知識のみを使って実現可能な仕組みにしています。 方向指示のしくみ 今回は、懐中電灯と偏向板を使って進行方向を指示する方法に関して解説します。 まずは、偏光板の性質に関する確認です。 1 下記画像は偏光板を2枚用意し、一方の偏光板を45度ずつ回転させた際の様子です。 2枚の偏光板のなす角により明るさ(透過率)が変化する様子を確認できます。 上記の画像では、偏光板を2枚重ねていました。では、一方の偏光板を光源側に、もう片方の偏光板をセンサー側に設置したらどうなるでしょうか。 この場合も、光源からセンサーに届く光量は、光源側の偏光板とセンサー側の偏光板のなす角により変化するはずです。 実際に実験した際の様子が、下記の映像になります。光源となる懐中電灯に偏光板を貼り付け、光量により抵抗値が変化するセンサー(一般にCdSセルと呼ばれる光抵抗)にも偏光板を貼り付けています。 懐中電灯(光源側の偏光板)を回転させることによりセンサー側の偏光板の光の透過率が変化し、抵抗値も変化していることを下記映像から確認できます。 これだけで、ロボットに進行方向を指示できそうな気もします。 しかし、普段我々が生活する環境には、懐中電灯以外にも照明や太陽光などが存在します。さらに、これらの光量は同じ部屋でも場所によって変化することもあり、光抵抗の抵抗値を意図せず変化させてしまいます。 このため、光抵抗1個だけでは懐中電灯側の偏光板とセンサー側の偏光板のなす角を検出するのが難しくなってしまいそうです。 それでは、偏光(板)を用いてどのようにロボットに進行方向を指示すれば良いでしょうか? いくつか方法は考えられそうですが、今回はまず2つの光抵抗と1つの懐中電灯、3枚の偏光板(各光抵抗と懐中電灯にそれぞれ1枚)を使ってみます。 下記画像は、実際の光抵抗と偏光板の画像です。 現存している当時の回路図では、下記のようになっています。 挙動としては、可変抵抗とコンパレータを利用して、2つの光抵抗の抵抗値がほぼ等しい(バランスがとれた状態)場合、左右のモーターを同時に動かします。 バランスが崩れた場合は、崩れた方向に応じて左右どちらか片方のモータのみを動作させます。 このため、ロボットは懐中電灯側の偏光版とセンサーのなす角が一定値以内に収まる方向へ向かって進むようになります。 下記の映像は、実際に走らせた際のものです。 ただ、この回路では懐中電灯(偏光)をあてたときのみ走行させ、懐中電灯を当てていないときは停止させるといった挙動を実現するのは難しそうです 2 。 そこで光抵抗を2個追加し、4つの光抵抗と1つの懐中電灯、5枚の偏光板(各光抵抗と懐中電灯にそれぞれ1枚)を使ってみます。 具体的には、下記図のように直列に接続した光抵抗を2組用意し、十字に交わるように配置します。 その後、配置した光抵抗の上に偏光板をかぶせます。 下記の図に示す方法で光抵抗に偏光板をかぶせます。 (直列接続した各光抵抗にかぶせる偏光板のなす角が90度となるようにします。) これにより、偏光が当たっていないときは4つの光抵抗に当たる光量はほぼ等しいため、抵抗値がほぼ等しく(バランスが取れた状態)になるはずです(各光抵抗の距離が近いため、部屋の場所などによって変化する光量の違いは無視できるものと仮定します)。 対して、偏光がセンサーに当たっている場合は少なくともどちらか片方の組の光抵抗の抵抗値のバランスが崩れた状態になります。 そこで、上記画像の青色の組(②と④)の抵抗値のバランスが崩れた場合は直進。緑色の組(①と③)の抵抗値のバランスが崩れた場合は、崩れた方向に従って左右のモーターのどちらか一方のみを回転さる。全ての抵抗値のバランスが取れている場合は、偏光(懐中電灯)が当てられていないと判定し、停止するような回路を作ってみます。 実際の回路図は、下記のようになりました。 なお、最終版の回路には安全装置としてのポリスイッチの追加など、変更が加えられています。 以上が、懐中電灯と偏向板を利用して進行方向を指示する方法の解説になります。 準備に関して 昨年(2023年)のロボットは、有線リモコンを使って機体を操作する仕様でした。 そのため回路がシンプルで、部品点数も少ないというメリットがありました。 しかし、有線リモコンの端子圧着作業が大変(ロボット1台につき8か所の圧着が必要)でした。 そこで、今年はできるだけ圧着作業を減らし、シンプルな構成にしたいということになりました。 最終的に今年のロボットは、懐中電灯で操作するロボットを仕様になりました。 プロトタイプの作成 実際に作成したプロトタイプを走行させた際の様子がこちらになります。 プロトタイプの作成にあたっては、次の点を意識していました。 手軽に入手できる部品で構成する できるだけシンプルな仕組みにする ロバスト性の確保 1. 手軽に入手できる部品で構成する 部品は、日本国内(特に秋葉原)で手軽に入手できる部品を利用することを意識していました。 理由は入手容易性にあります。量産時に部品の不足が判明した可能性などを考えると、秋葉原などにある実店舗で直接入手できることが重要です。 実際、昨年は圧着ミスなどにより圧着端子が足りなくなり、秋葉原に急いて買い出しに行ったと記憶しています。 今回は、仮に参加者などから回路を含めて自作したいといったリクエストがあった場合のことも考えて、秋葉原などに実店舗がありオンラインでも注文可能な秋月電子通商さまが扱っている部品を主に利用させていただきました。 2. できるだけシンプルな仕組みにする マイコンを使わずに、偏光の性質と抵抗分圧回路、コンパレータ、モータ、ダイオード、トランジスタ(MOSFET)といった電子工作をする際の基本的な知識のみを使って実現可能な仕組みにしました。 マイコンを使わなかった理由としては、価格やプログラム作成や書き込みの手間を減らしたかったためです。 ただ、今回は電子回路を主役にしたかったという提案者(筆者)の個人的なわがままの影響もあります。 完全に個人の主観になりますが、昨今のプログラミングブームの影響もあり、マイコンを使ってしまうと電子回路よりもソフトウェアに注目が向かってしまうのではないかと考えました。 そこで、今回はあえてマイコンを使わずに、簡単な電子回路のみを組み合わせてラジコンという一見複雑な仕組みが必要そうなシステムの実現に挑戦してみました。 これにより、電子回路そのものへの興味を持つきっかけになればと思った次第です。 3. ロバスト性の確保 計画当初では、懐中電灯は参加者自身で用意していただく予定でした。そのため、懐中電灯の光のスペクトルが分からず、広範囲の波長の光に反応するセンサーを利用する必要がありました。 また、時間や天気により会場に差し込む太陽光の光量も変化するという問題があり、比較的明るい部屋でもセンサーが飽和することなく、偏光を検出できる必要がありました。 そこで、フォトダイオードやフォトトランジスタ、CdSセルなど複数の光センサーから条件を満たすセンサーを探したところ、CdSセルを利用するのが良いとの結論に至りました。 可能であれば、欧州のRoHS指令等で規制対象となっているカドミウムを含むCdSセルの利用は避けたかったのですが、CdSセル以外のセンサーを使って安定して動作する回路の実現には至りませんでした(こちらは今後の課題です)。 基板作成、調達 ブレッドボードを利用したプロトタイプ作成後のプリント基板作成は、経験のある部員が行いました。 また、一部の部品は3Dプリンタを利用して作成しました。 モデリングツールは部員やパーツによって異なりますが、BlenderやFreeCAD、OpenSCADを利用しているとの認識です。 昨年の反省は活かされたのか? 昨年(2023年)は、大量のハンダ付け、大量のケーブルの圧着、大量の部品の仕上げ加工、のように準備が大変でした。 では、今年はどうだったのかというと、改善したのは、ケーブルの圧着作業が1台当たり8か所から2か所に減ったのみでした。 ハンダ付けの量はむしろ悪化し、基板1当たり30分以上の時間がかかるようになってしまいました。 ただ、部員の増加や分業、昨年の経験を踏まえたスキルの向上、リード部品を効率よく曲げるための治具の作成により、当初の想定よりスムーズに準備が進みました。 おかげで、今年は昨年よりも比較的余裕を持って準備を進めることが出来ました。 当日の様子 ロボット教室は、1日2回、各回15名で2日間にわたって実施しました。 こちらが当日の様子になります。参加者は小学生の方が一番多かったです。 なお、組み立て作業などは、保護者の監督の下で行っていただきました。 実際の走行の様子はこちらになります。 なお当日は、組み立てまで完了し無事に動かせるようになった方がいる一方、ハードウェアの不具合や調整不足などにより、思ったように操作できない車体なども出てきてしまいました。 ハードウェアの不具合の原因としては、はんだ付け不良によるものと思われるものが多くありました。 こちらは、事前に動作確認を行い不具合を見つけ次第修正したのですが、一部は見つけきれ無かったようです(予備基板と交換しました)。 調整不足の原因としては、単純に調整手順が多く難しいという問題があります。 今回制作した機体は下記図の方法で半固定抵抗の調整する必要があります。 しかし、センサーの一部のみに手などの影がかかると正確な調整が出来ずに、上手く調整できないといった問題が多く発生してしまいました。 上記のような問題もあり、盛り上がりや満足度としては昨年(2023年)のロボット相撲大会より落ちてしまったのではないかと感じています。 おわりに(+社外イベントへの出展のおしらせ) 最後まで読んでくださりありがとうございます。 また、本イベントに参加してくださった皆さま、支援してくださった皆さまに感謝申し上げます。 もし来年も機会がありましたら、(内容は未定ですが)今年の反省もふまえてより良いものを提供できればと思います。 最後に、こちらのロボット(ラジコン)は、2024年12月07(土)に docomo R&D OPEN LAB ODAIBA で開催される 【20th 記念展示会】Mashup Award & #ヒーローズリーグ で展示予定です。 直前の案内となってしまいましたが、もしご興味がありましたらお立ちよりください。 但し書き 本記事には欧州のRoHS指令等で規制対象となっているカドミウム(有害物質)を含む電子部品(一般にCdSセルと呼ばれる光センサ)が登場します。該当センサを破砕すると有害物質を曝露することになります。また、廃棄する際は自治体の指示に従って廃棄してください。本記事の内容を実践する場合は、左記の内容に留意ください。 本記事の内容を参考にされる場合は、自己責任でお願いします。 今回は直線偏光を利用します。 ↩ この記事を書きながら思ったのですが、同時に両方の車輪を回転させることを諦めれば後述する方法を用いなくても実現できそうな気がしてきました。しかし、執筆時点では未検証です。 ↩
この記事は、 NTT Communications Advent Calendar 2024 2 日目の記事です。 perf の Python インタプリタを使って KVM Exit/Entry のレイテンシを計測してみます。 はじめに KVM の仕組み CPU トレースを取得する perf をビルドする Python コードを書く 独自スクリプトを用いて perf.data を集計する まとめ 参考文献 はじめに こんにちは。 SDPF クラウド・仮想サーバーチームの杉浦 ( @Kumassy_ ) です。 普段は OpenStack の開発・運用をしており、最近は仮想マシンの性能解析やトラブルシューティングなどに取り組んでいます。 perf は Linux のパフォーマンス解析に有用なツールです。 perf を使うことで、パフォーマンスカウンタの値を読んだり、プロファイリング、トレーシングなど 様々なデータを取得することができます 。 取得したデータを分析するには perf report や perf script が利用できますが、込み入ったデータを解析したいときには python または perl で書かれた独自のスクリプトを使うことができます。 今回は perf の Python インタプリタを使って KVM Exit/Entry のレイテンシを計測してみます。 KVM の仕組み QEMU/KVM を利用した仮想環境では、ユーザーが作成した仮想マシンは QEMU のユーザープロセスとして実行されます。 算術演算など簡単な命令は、一般的なユーザープロセスと同じようにユーザー空間で処理されますが、 I/O などのセンシティブ命令はハイパーバイザ側で処理する必要があります。 仮想マシンがセンシティブ命令を実行しようとすると、仮想マシンの実行が一時停止し、 KVM Exit というイベントが発生します。 ハイパーバイザは KVM Exit が発生した理由 (Exit Reason) を調べて適切に処理し、仮想マシンの実行を再開します。このとき KVM Entry イベントが発生します。 このサイクルを図示すると次の図のようになります。 KVM Exit が発生してから KVM Entry が完了するまで仮想マシンは一時停止しているので、このレイテンシが長いと問題です。 このような解析には perf kvm サブコマンドも利用できますが、今回は KVM Exit/Entry のレイテンシを題材に、 perf の独自スクリプトを書いて分析してみます。 CPU トレースを取得する まずは解析する CPU トレースを取得します。 仮想マシンを実行する物理マシンには複数の VM が収容されています。 あとで解析しやすいように、 CPU affinity を調整することで、解析対象 VM の vCPU を特定の物理 CPU コアに pinning しておきます。 $ sudo virsh vcpupin instance-0000xxxx 0 124 $ sudo virsh vcpupin instance-0000xxxx 1 125 $ sudo virsh vcpupin instance-0000xxxx 2 126 $ sudo virsh vcpupin instance-0000xxxx 3 127 以下のようなコマンドで kvm:kvm_exit と kvm:kvm_entry イベントのトレースを取得します。 $ sudo taskset -c 31 perf record -C 124-127 -e kvm:kvm_exit -e kvm:kvm_entry -- sleep 9000 perf script を使えばこのように解析できますが、少々見にくいです。 $ perf script -i /path/to/perf.data CPU 0/KVM 18292 [124] 151678.240967: kvm:kvm_exit: vcpu 0 reason MSR_WRITE rip 0xffffffff9ae6cf64 info1 0x0000000000000000 info2 0x0000000000000000 intr_info 0x00000000 error_code 0x00000000 CPU 0/KVM 18292 [124] 151678.240969: kvm:kvm_entry: vcpu 0, rip 0xffffffff9ae6cf66 CPU 0/KVM 18292 [124] 151678.240982: kvm:kvm_exit: vcpu 0 reason MSR_WRITE rip 0xffffffff9ae6cf64 info1 0x0000000000000000 info2 0x0000000000000000 intr_info 0x00000000 error_code 0x00000000 CPU 0/KVM 18292 [124] 151678.240983: kvm:kvm_entry: vcpu 0, rip 0xffffffff9ae6cf66 CPU 0/KVM 18292 [124] 151678.241044: kvm:kvm_exit: vcpu 0 reason EXTERNAL_INTERRUPT rip 0xffffffff9b0c99be info1 0x0000000000000000 info2 0x0000000000000000 intr_info 0x800000ec error_code 0x00000000 そこで、独自スクリプトでは以下の 2 つを目標とします。 Exit Reason ごとに KVM Exit/Entry のレイテンシをヒストグラムとして表す 上記ヒストグラムをコアごとに分けて表示する perf をビルドする perf の Python インタプリタを利用するには、 perf の libpython ビルドオプションが有効化されている必要があります。 私のチームで利用できる Ubuntu 22.04 環境では、 linux-tools-common に含まれる perf にはこのオプションが有効化されていませんでした。 $ sudo perf version --build-options perf version 5.15 dwarf: [ on ] # HAVE_DWARF_SUPPORT dwarf_getlocations: [ on ] # HAVE_DWARF_GETLOCATIONS_SUPPORT glibc: [ on ] # HAVE_GLIBC_SUPPORT syscall_table: [ on ] # HAVE_SYSCALL_TABLE_SUPPORT libbfd: [ OFF ] # HAVE_LIBBFD_SUPPORT libelf: [ on ] # HAVE_LIBELF_SUPPORT libnuma: [ on ] # HAVE_LIBNUMA_SUPPORT numa_num_possible_cpus: [ on ] # HAVE_LIBNUMA_SUPPORT libperl: [ OFF ] # HAVE_LIBPERL_SUPPORT libpython: [ OFF ] # HAVE_LIBPYTHON_SUPPORT libslang: [ on ] # HAVE_SLANG_SUPPORT libcrypto: [ on ] # HAVE_LIBCRYPTO_SUPPORT libunwind: [ on ] # HAVE_LIBUNWIND_SUPPORT libdw-dwarf-unwind: [ on ] # HAVE_DWARF_SUPPORT zlib: [ on ] # HAVE_ZLIB_SUPPORT lzma: [ on ] # HAVE_LZMA_SUPPORT get_cpuid: [ on ] # HAVE_AUXTRACE_SUPPORT bpf: [ on ] # HAVE_LIBBPF_SUPPORT aio: [ on ] # HAVE_AIO_SUPPORT zstd: [ OFF ] # HAVE_ZSTD_SUPPORT libpfm4: [ OFF ] # HAVE_LIBPFM そこで、以下のように Linux カーネルのソースコードをダウンロードし、 tools/perf をビルドします。 $ sudo apt install make gcc flex bison pkg-config $ sudo apt install libzstd1 libdwarf-dev libdw-dev binutils-dev libcap-dev libelf-dev libnuma-dev python3 python3-dev python-setuptools libssl-dev libunwind-dev libdwarf-dev zlib1g-dev liblzma-dev libaio-dev libtraceevent-dev debuginfod libpfm4-dev libslang2-dev systemtap-sdt-dev libperl-dev binutils-dev libbabeltrace-dev libiberty-dev libzstd-dev $ curl -O https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.15.tar.xz $ tar xvf linux-5.15.tar.xz $ cd linux-5.15/tools/perf # python3-config パッケージを利用するために `PYTHON=python3` が必要でした $ make PYTHON=python3 libpython オプションを有効化できました。 $ ./perf version --build-options perf version 5.15.0 dwarf: [ on ] # HAVE_DWARF_SUPPORT dwarf_getlocations: [ on ] # HAVE_DWARF_GETLOCATIONS_SUPPORT glibc: [ on ] # HAVE_GLIBC_SUPPORT syscall_table: [ on ] # HAVE_SYSCALL_TABLE_SUPPORT libbfd: [ on ] # HAVE_LIBBFD_SUPPORT libelf: [ on ] # HAVE_LIBELF_SUPPORT libnuma: [ on ] # HAVE_LIBNUMA_SUPPORT numa_num_possible_cpus: [ on ] # HAVE_LIBNUMA_SUPPORT libperl: [ on ] # HAVE_LIBPERL_SUPPORT libpython: [ on ] # HAVE_LIBPYTHON_SUPPORT libslang: [ on ] # HAVE_SLANG_SUPPORT libcrypto: [ OFF ] # HAVE_LIBCRYPTO_SUPPORT libunwind: [ on ] # HAVE_LIBUNWIND_SUPPORT libdw-dwarf-unwind: [ on ] # HAVE_DWARF_SUPPORT zlib: [ on ] # HAVE_ZLIB_SUPPORT lzma: [ on ] # HAVE_LZMA_SUPPORT get_cpuid: [ on ] # HAVE_AUXTRACE_SUPPORT bpf: [ on ] # HAVE_LIBBPF_SUPPORT aio: [ on ] # HAVE_AIO_SUPPORT zstd: [ on ] # HAVE_ZSTD_SUPPORT libpfm4: [ OFF ] # HAVE_LIBPFM Python コードを書く 準備が整ったので、いよいよ CPU トレースを解析する Python スクリプトを記述します。 詳しい解説やサンプルコードが perf-script-python(1) にあるので、 man を参考に作業します。 まずはテンプレートを生成します。 テンプレートは perf script -g で生成できますが、 perf record -e で指定したイベントごとに関数を実装するので、 perf.data を渡す必要があります。 $ ~/linux-5.15-build/tools/perf/perf script -i /path/to/perf.data -g python generated Python script: perf-script.py 生成されたファイルをみると、以下のようになっています。 # $ cat perf-script.py # (snip) def trace_begin (): print ( "in trace_begin" ) def trace_end (): print ( "in trace_end" ) def kvm__kvm_exit (event_name, context, common_cpu, common_secs, common_nsecs, common_pid, common_comm, common_callchain, exit_reason, guest_rip, isa, info1, info2, intr_info, error_code, vcpu_id, perf_sample_dict): print_header(event_name, common_cpu, common_secs, common_nsecs, common_pid, common_comm) print ( "exit_reason=%s, guest_rip=%u, isa=%u, " \ "info1=%u, info2=%u, intr_info=%u, " \ "error_code=%u, vcpu_id=%u" % \ (flag_str( "kvm__kvm_exit" , "exit_reason" , exit_reason), guest_rip, isa, info1, info2, intr_info, error_code, vcpu_id)) print ( 'Sample: {' +get_dict_as_string(perf_sample_dict[ 'sample' ], ', ' )+ '}' ) for node in common_callchain: if 'sym' in node: print ( " \t [%x] %s" % (node[ 'ip' ], node[ 'sym' ][ 'name' ])) else : print ( " [%x]" % (node[ 'ip' ])) print () def kvm__kvm_entry (event_name, context, common_cpu, common_secs, common_nsecs, common_pid, common_comm, common_callchain, vcpu_id, rip, perf_sample_dict): print_header(event_name, common_cpu, common_secs, common_nsecs, common_pid, common_comm) # (snip) print () def trace_unhandled (event_name, context, event_fields_dict, perf_sample_dict): print (get_dict_as_string(event_fields_dict)) print ( 'Sample: {' +get_dict_as_string(perf_sample_dict[ 'sample' ], ', ' )+ '}' ) # (snip) perf record -e で指定したイベント名ごとに関数が定義されていることと、イベントの付加情報が引数として渡されることがわかります。 KVM Exit/Entry のレイテンシを計測するには、グローバルなデータ構造を用意し、 kvm__kvm_exit に渡されたイベント発生時刻から kvm__kvm_entry に渡されたイベント発生時刻を引けばよさそうです。 今回の例では、 vCPU ごとに専用の物理 CPU コアを割り当てているので、 common_cpu を使えば kvm__kvm_exit イベントと kvm__kvm_exit イベントを対応づけることができます。 最後に kvm__kvm_exit が発生したときの時刻を last_kvm_exit_at に、そのときの Reason を last_kvm_exit_reason へ保存するようにします。 これらの情報をコアごとに保存するため、 defaultdict を使ってコア番号をキーとする辞書を使います。 kvm__entry イベントの発生時刻から last_kvm_exit_at に保存された時刻を引き、レイテンシとして集計します。 イベントの発生時刻は整数部分 common_secs と小数部分 common_nsecs に分けて渡されることに注意します。 これらを結合してイベントの発生時刻を計算するには Util パッケージの nsecs 関数を使い、次のようにします。 nsecs(common_secs, common_nsecs) exit_reason は整数で渡されるので、文字列表現に対応づけるには arch/x86/include/uapi/asm/vmx.h を参考にするとよいです。 最終的なコードは以下のようになりました。 # $ cat perf-script-kvm-exit-entry.py # perf script event handlers, generated by perf script -g python # Licensed under the terms of the GNU GPL License version 2 # The common_* event handler fields are the most useful fields common to # all events. They don't necessarily correspond to the 'common_*' fields # in the format files. Those fields not available as handler params can # be retrieved using Python functions of the form common_*(context). # See the perf-script-python Documentation for the list of available functions. from __future__ import print_function import os import sys sys.path.append(os.environ[ 'PERF_EXEC_PATH' ] + \ '/scripts/python/Perf-Trace-Util/lib/Perf/Trace' ) sys.path.append( '/home/openstack/work_ksugiura/linux-5.15-build/tools/perf/scripts/python/Perf-Trace-Util/lib/Perf/Trace' ) from perf_trace_context import * from Core import * from Util import nsecs import numpy as np from collections import defaultdict ms = 1000 * 1000 exit_reason = ( "EXCEPTION_NMI" , "EXTERNAL_INTERRUPT" , "TRIPLE_FAULT" , "INIT_SIGNAL" , "N/A" , "N/A" , "N/A" , "INTERRUPT_WINDOW" , "NMI_WINDOW" , "TASK_SWITCH" , "CPUID" , "N/A" , "HLT" , "INVD" , "INVLPG" , "RDPMC" , "RDTSC" , "N/A" , "VMCALL" , "VMCLEAR" , "VMLAUNCH" , "VMPTRLD" , "VMPTRST" , "VMREAD" , "VMRESUME" , "VMWRITE" , "VMOFF" , "VMON" , "CR_ACCESS" , "DR_ACCESS" , "IO_INSTRUCTION" , "MSR_READ" , "MSR_WRITE" , "INVALID_STATE" , "MSR_LOAD_FAIL" , "N/A" , "MWAIT_INSTRUCTION" , "MONITOR_TRAP_FLAG" , "N/A" , "MONITOR_INSTRUCTION" , "PAUSE_INSTRUCTION" , "MCE_DURING_VMENTRY" , "N/A" , "TPR_BELOW_THRESHOLD" , "APIC_ACCESS" , "EOI_INDUCED" , "GDTR_IDTR" , "LDTR_TR" , "EPT_VIOLATION" , "EPT_MISCONFIG" , "INVEPT" , "RDTSCP" , "PREEMPTION_TIMER" , "INVVPID" , "WBINVD" , "XSETBV" , "APIC_WRITE" , "RDRAND" , "INVPCID" , "VMFUNC" , "ENCLS" , "RDSEED" , "PML_FULL" , "XSAVES" , "XRSTORS" , "N/A" , "N/A" , "UMWAIT" , "TPAUSE" "N/A" , "N/A" , "N/A" , "N/A" , "N/A" , "BUS_LOCK" , "NOTIFY" , ) def trace_begin (): global last_kvm_exit_at last_kvm_exit_at = defaultdict( lambda : 0 ) global last_kvm_exit_reason last_kvm_exit_reason = defaultdict( lambda : - 1 ) global kvm_entry_latencies kvm_entry_latencies = defaultdict( lambda : defaultdict( lambda : [])) print ( "in trace_begin" ) def trace_end (): global last_kvm_exit_at global kvm_entry_latencies print ( "in trace_end" ) print ( "kvm_entry_latencies" ) for core, reason_latencies in kvm_entry_latencies.items(): print ( "Core: " , core) for reason, latencies in reason_latencies.items(): print ( "Reason: [%d] %s" %(reason, exit_reason[reason])) max_latency = max (latencies) bins = [ 2 **i for i in range ( int (np.log2(max_latency)) + 2 )] hist, bins = np.histogram(latencies, bins=bins) for i in range ( len (hist)): print ( "[%16d, %16d) %d" % (bins[i], bins[i+ 1 ], hist[i])) def kvm__kvm_exit (event_name, context, common_cpu, common_secs, common_nsecs, common_pid, common_comm, common_callchain, exit_reason, guest_rip, isa, info1, info2, intr_info, error_code, vcpu_id, perf_sample_dict): global last_kvm_exit_at global last_kvm_exit_reason global kvm_entry_latencies last_kvm_exit_at[common_cpu] = nsecs(common_secs, common_nsecs) last_kvm_exit_reason[common_cpu] = exit_reason def kvm__kvm_entry (event_name, context, common_cpu, common_secs, common_nsecs, common_pid, common_comm, common_callchain, vcpu_id, rip, perf_sample_dict): global last_kvm_exit_at global last_kvm_exit_reason global kvm_entry_latencies if last_kvm_exit_at[common_cpu] > 0 : latency = nsecs(common_secs, common_nsecs) - last_kvm_exit_at[common_cpu] reason = last_kvm_exit_reason[common_cpu] if latency > 1 * ms and reason != 12 : print ( "exit-entry: [%d]: [%d.%d]: reason: %s, %d" % (common_cpu, common_secs, common_nsecs, exit_reason[reason], latency)) kvm_entry_latencies[common_cpu][reason].append(latency) last_kvm_exit_at[common_cpu] = 0 def trace_unhandled (event_name, context, event_fields_dict, perf_sample_dict): print (get_dict_as_string(event_fields_dict)) print ( 'Sample: {' +get_dict_as_string(perf_sample_dict[ 'sample' ], ', ' )+ '}' ) def print_header (event_name, cpu, secs, nsecs, pid, comm): print ( "%-20s %5u %05u.%09u %8u %-20s " % \ (event_name, cpu, secs, nsecs, pid, comm), end= "" ) def get_dict_as_string (a_dict, delimiter= ' ' ): return delimiter.join([ '%s=%s' %(k, str (v)) for k,v in sorted (a_dict.items())]) 独自スクリプトを用いて perf.data を集計する では、作成したスクリプト perf-script-kvm-exit-entry.py を用いて KVM Exit/Entry のレイテンシを解析してみます。 Exit Reason ごと、コアごとにヒストグラムを作成すると以下のようになりました。 出力は非常に長いので、一部だけ掲載します。 $ perf script -i /path/to/perf.data -s /path/to/perf-script-kvm-exit-entry.py exit-entry: [126]: [151856.170064250]: reason: EXTERNAL_INTERRUPT, 1522602 exit-entry: [125]: [151915.737653830]: reason: EXTERNAL_INTERRUPT, 1282485 exit-entry: [124]: [152201.81772565]: reason: EXTERNAL_INTERRUPT, 2198819 exit-entry: [127]: [152672.999539636]: reason: EXTERNAL_INTERRUPT, 1341486 exit-entry: [124]: [152812.995271815]: reason: EXTERNAL_INTERRUPT, 1475909 Core: 124 Reason: [32] MSR_WRITE [ 1, 2) 0 [ 2, 4) 0 [ 4, 8) 0 [ 8, 16) 0 [ 16, 32) 0 [ 32, 64) 0 [ 64, 128) 0 [ 128, 256) 17842 [ 256, 512) 1884223 [ 512, 1024) 2344850 [ 1024, 2048) 2567959 [ 2048, 4096) 653205 [ 4096, 8192) 81909 [ 8192, 16384) 1771 [ 16384, 32768) 620 [ 32768, 65536) 5 [ 65536, 131072) 0 [ 131072, 262144) 0 [ 262144, 524288) 1 [ 524288, 1048576) 0 [ 1048576, 2097152) 1 Reason: [1] EXTERNAL_INTERRUPT [ 1, 2) 0 [ 2, 4) 0 [ 4, 8) 0 [ 8, 16) 0 [ 16, 32) 0 [ 32, 64) 0 [ 64, 128) 0 [ 128, 256) 0 [ 256, 512) 0 [ 512, 1024) 112 [ 1024, 2048) 390 [ 2048, 4096) 7145 [ 4096, 8192) 13116 [ 8192, 16384) 69835 [ 16384, 32768) 23903 [ 32768, 65536) 44 [ 65536, 131072) 20 [ 131072, 262144) 1 [ 262144, 524288) 1 [ 524288, 1048576) 3 [ 1048576, 2097152) 11 [ 2097152, 4194304) 1 Reason: [48] EPT_VIOLATION [ 1, 2) 0 [ 2, 4) 0 [ 4, 8) 0 [ 8, 16) 0 [ 16, 32) 0 [ 32, 64) 0 [ 64, 128) 0 [ 128, 256) 0 [ 256, 512) 0 [ 512, 1024) 37 [ 1024, 2048) 286 [ 2048, 4096) 940 [ 4096, 8192) 848 [ 8192, 16384) 223 [ 16384, 32768) 785 [ 32768, 65536) 17 [ 65536, 131072) 1 [ 131072, 262144) 6 [ 262144, 524288) 11 [ 524288, 1048576) 4 全体を眺めてみると、 EXTERNAL_INTERRUPT は頻度が高くレイテンシも大きい傾向にあることがわかりました。 EPT_VIOLATION の頻度は低いものの、発生したときのペナルティは大きいようです。 今回の実験のみから何かを導き出すのは難しいですが、ホスト OS の構成や、ゲスト OS の種類、ゲスト OS のワークロードを変更したときの差分に着目すると、有意義な結果が得られるかもしれません。 まとめ perf には独自の Python スクリプトを用いて CPU トレースを集計できる機能があります。デフォルトの表示を変えたいときや、独自の集計処理を書きたいときに有用です。 kvm:kvm_exit と kvm:kvm_entry というトレースポイントを使うことで、 KVM Exit / Entry のレイテンシを集計し、ヒストグラムとして表示しました。 参考文献 第5回 x86プロセッサの仮想化支援機能Intel VT & AMD-V | gihyo.jp Linux perf Examples perf-script-python(1)
この記事は、 NTT Communications Advent Calendar 2024 1日目の記事です。 こんにちは、イノベーションセンターの加藤です。普段はコンピュータビジョンの技術開発やAI/機械学習(ML: Machine Learning)システムの検証に取り組んでいます。一方で、兼務で生成AIチームに参加し、大規模言語モデル(LLM: Large Language Model)に関する技術の調査を行なっています。 音声アシスタントをLLMベースで作成する際、ユーザーの入力音声を一旦テキストに変換し、LLMに応答させた後、その応答文から読み上げ音声を生成するというカスケード方式がこれまで取られてきています。 一方最近ではMini-Omni 1 など、音声を入力として音声を出力するLLMを一貫して学習可能なエンドツーエンド方式も登場してきています。音声アシスタントのようにユーザーとやりとりするシステムにおいて、ユーザーの入力が終わってからアシスタントが応答を返し始めるまでの時間はレイテンシと呼ばれ、ユーザーの体験に直結する大事な指標ですが、エンドツーエンド方式ではこのレイテンシが短くなる傾向にあります。 しかしながら、利用するLLMモデルや読み上げ音声のカスタマイズの柔軟性という面で既存のカスケード方式にも長所があります。 今回はストリーム処理を活用することで、カスケード方式の欠点であるレイテンシの短縮に取り組んだ結果を紹介します。 目次 目次 音声対話システムの仕組み VADを用いてユーザーの会話終わりを検出する ASRを用いてユーザーの発話内容を文字列に変換する LLMを用いてチャットボットの応答を文字列として取得する TTSを用いて応答文を読み上げさせる 各処理を順に行うGUIを作成 各モジュールのレイテンシを測る TTSのストリーム処理 文で区切る 文節で区切る ユーザーからの割り込み まとめ 参考資料 音声対話システムの仕組み 今回作成する音声対話システムは次のモジュールで構成されています。 VAD(Voice Activity Detection, 音声区間検出) ASR(Automatic Speech Recognition, 自動音声認識) LLM(Large Language Model, 大規模言語モデル) TTS(Text to Speech, 音声合成) これらのモジュールを使い、以下の流れで処理を行います。 VADを用いてユーザーの会話終わりを検出する ユーザーがシステムに音声を送信する際、話し終わったタイミングで送信ボタンを押すというUIも考えられますが、今回はスムーズな会話を実現するために自動で発話の終わりを検出します。推論モデルは Silero VAD を利用し、マイクから入力された音声を32msの粒度で発言中かどうかを判定します。そして発話の終わりを検知し、無音時間が閾値の300msを超えたら、音声バッファから喋っている部分を切り出し次の処理に回します。 from silero_vad import load_silero_vad, get_speech_timestamps, VADIterator import gradio as gr import numpy as np import torch import librosa from typing import Tuple, Optional, List AudioType = Tuple[ int , np.ndarray] class VAD : def __init__ (self): self.SAMPLING_RATE = 16000 self.vad_iter = VADIterator( load_silero_vad(onnx= True ), sampling_rate=self.SAMPLING_RATE, min_silence_duration_ms= 300 , ) self.reset() def reset (self): self.vad_iter.reset_states() self.buffer = np.empty( 0 , dtype=np.float32) self.ptr = 0 self.orig_sr = None self.start = None self.end = None def __call__ (self, audio: AudioType): """ストリーミングされる音声を受け取り、発話が終わった時のみ音声バッファを返す""" sr, wave = audio self.orig_sr = sr window = int ( 512 * sr / self.SAMPLING_RATE) if wave.dtype == np.int16: wave = wave.astype(np.float32) / 32768.0 # Convert to mono if stereo if wave.ndim > 1 : wave = wave.mean(axis= 1 ) self.buffer = np.concatenate([self.buffer, wave]) while len (self.buffer) - self.ptr > window: chunk = self.buffer[self.ptr:self.ptr + window] chunk = librosa.resample(chunk, orig_sr=sr, target_sr=self.SAMPLING_RATE) chunk = torch.tensor(chunk) speech_dict = self.vad_iter(chunk, return_seconds= True ) self.ptr += window if speech_dict: if "start" in speech_dict: self.start = speech_dict[ "start" ] elif "end" in speech_dict: self.end = speech_dict[ "end" ] speech = self.buffer[ int (self.start * self.orig_sr) : int (self.end * self.orig_sr)] return (sr, speech) return None ASRを用いてユーザーの発話内容を文字列に変換する VADにより会話の終了を検知したら、今までのバッファを音声認識モデルに入力し、LLMに入れるための文字列に変換します。モデルは Whisper を利用しました。 import whisper class ASR : def __init__ (self): self.whisper_sr = whisper.audio.SAMPLE_RATE self.whisper_model = whisper.load_model( "large-v3" ) def __call__ (self, value: AudioType): sr, wave = value if wave.dtype == np.int16: wave = wave.astype(np.float32) / 32768.0 wave = librosa.resample(wave, orig_sr=sr, target_sr=self.whisper_sr) result = self.whisper_model.transcribe(wave, language= "ja" , temperature= 0.0 ) return result[ "text" ] LLMを用いてチャットボットの応答を文字列として取得する チャット用にチューニングされたLLMである CALM3-22B-Chat を利用し、ユーザーの発話内容に対して応答をさせます。 今回は複数回のやり取りを想定し、会話記録として history を受け取り応答文を返す形にします。 from transformers import AutoModelForCausalLM, AutoTokenizer, TextStreamer, TextIteratorStreamer class LLM : def __init__ (self): self.llm_model = AutoModelForCausalLM.from_pretrained( "cyberagent/calm3-22b-chat" , device_map= "auto" , torch_dtype= "auto" ) self.llm_tokenizer = AutoTokenizer.from_pretrained( "cyberagent/calm3-22b-chat" ) self.prompts = [ { "role" : "system" , "content" : "あなたは親切なAIアシスタントです。" } ] def __call__ (self, history: List[ dict ]) -> str : token_ids = self.llm_tokenizer.apply_chat_template( self.prompts + history, add_generation_prompt= True , return_tensors= "pt" ) gen = self.llm_model.generate( input_ids=token_ids.to(self.llm_model.device), return_dict_in_generate= True , max_new_tokens= 300 , do_sample= False ) seq = gen.sequences seq = seq[:,token_ids.shape[ 1 ]:] # skip prompt response = self.llm_tokenizer.batch_decode(seq, skip_special_tokens= True )[ 0 ] return response TTSを用いて応答文を読み上げさせる 最後にTTSとして ESPnet を利用し、LLMからの応答文を音声に変換します。 from espnet2.bin.tts_inference import Text2Speech from espnet_model_zoo.downloader import ModelDownloader class TTS : def __init__ (self): model_tag = "kan-bayashi/jsut_full_band_vits_prosody" downloader = ModelDownloader(cachedir=Path.home() / ".cache" / "espnet_model_zoo" ) self.text2speech = Text2Speech( **downloader.download_and_unpack(model_tag), # No vocoder device= "cuda" , noise_scale= 0.333 , noise_scale_dur= 0.333 , always_fix_seed= True , ) def __call__ (self, text: str ): return self.forward(text) @ torch.no_grad () def forward (self, inpt) -> AudioType: wav = self.text2speech(inpt)[ "wav" ].cpu().numpy() return self.text2speech.fs, wav 各処理を順に行うGUIを作成 この記事ではGUIとして Gradio を利用します。 このプログラムではVADの判定により会話部分の区間が確定した後、ASR, LLM, TTSが順に実行されます。 VADで保存されている記録中の音声バッファなど、ユーザーに依存する状態は本来Gradioを使い管理する必要がありますが、本記事では簡単のため実装を省略しています。 作成したGUIは以下の画像のようになりました。 各モジュールのレイテンシを測る 一通りシステムが完成したので、次はどのモジュールがどれだけ時間をかけているか測定します。 人力で音声を入力すると話し終わりから応答までのレイテンシを測りにくいので、入力音声はTTSで作成し、TTSによる入力文の再生終了を話し終わりとみなして実行時間を計測しました。 GPU環境はNVIDIA H100 GPUを1枚利用しました。 入力文:AIによって私たちの暮らしはどのように変わりますか?簡潔に説明してください。 * 入力音声長:5.050 sec * ASR処理時間:0.787 sec * LLM処理時間:4.730 sec (0.033 sec / token) * 応答文:AIは私たちの暮らしに多岐にわたる影響を与えます。まず、日常生活の効率化が挙げられます。スマートホームデバイスや音声アシスタントが家事や日常のタスクを自動化し、時間を節約します。また、医療分野では診断や治療の精度が向上し、早期発見や個別化医療が進みます。さらに、交通や物流の分野では、自動運転車やドローン配送が普及し、移動や配送が迅速かつ効率的になります。教育分野では、AIを活用した個別学習プログラムが学習者の理解度に応じた指導を提供し、教育の質が向上します。最後に、エンターテインメントやクリエイティブ産業でも、AIが新しいコンテンツの生成やパーソナライズされた体験を提供し、私たちの楽しみ方を一変させます。 * 応答音声長:48.228 sec * TTS処理時間:0.198 sec * 全体レイテンシ(ASR + LLM + TTS + その他処理時間):5.895 sec 以上のように5.895秒が計測された全体レイテンシとなります。実際にユーザーが喋り終わってからアシスタントが喋り始めるまでには、喋り終わりが判定されるまでの300ミリ秒や、生成音声をブラウザーに渡す際のエンコード処理などの時間が追加でかかります。 しかし全体を通して一番のボトルネックはLLMの処理時間で、レイテンシの約80%を占めていることが分かります。 次の章では、LLMのストリーム出力を活用することでこのレイテンシを低減する手法や、それに伴って新しく可能になる機能を紹介します。 TTSのストリーム処理 文で区切る LLMの文章生成は逐次的に単語を出力するため、応答全体が揃うまで待たなくても音声を再生し始めることができます。 例えば、LLMからの出力に句点が入り次第音声を出力することで、返答の音声が始まるまでのレイテンシを短縮できます。本来はテキスト全体からイントネーションや発音の長さなどを決定し音声合成するため、文章全体を入力した場合とTTSの結果が少し異なってしまいますが、文を跨いで音声の調子が影響することは考えにくいため、レイテンシの短縮を優先して採用しました。実際に生成した音声も、文章全体から生成した場合とほぼ同じ結果が得られ、文単位で区切る手法は有用であることがわかりました。 # ただのTextIteratorStreamerは漢字が来るまで内部でバッファしてしまうのでバッファリングを無効化する class EagerTextIteratorStreamer (TextIteratorStreamer): def put (self, value): if len (value.shape) > 1 and value.shape[ 0 ] > 1 : raise ValueError ( "TextStreamer only supports batch size 1" ) elif len (value.shape) > 1 : value = value[ 0 ] if self.skip_prompt and self.next_tokens_are_prompt: self.next_tokens_are_prompt = False return self.token_cache.extend(value.tolist()) text = self.tokenizer.decode(self.token_cache, **self.decode_kwargs) printable_text = text[self.print_len :] self.token_cache = [] self.print_len = 0 self.on_finalized_text(printable_text) class LLMStream (LLM): def __call__ (self, history: List[ dict ]) -> str : token_ids = self.llm_tokenizer.apply_chat_template( self.prompts + history, add_generation_prompt= True , return_tensors= "pt" ) streamer = EagerTextIteratorStreamer(self.llm_tokenizer, skip_prompt= True , skip_special_tokens= True ) thread = Thread( target=self.llm_model.generate, kwargs= dict ( input_ids=token_ids.to(self.llm_model.device), max_new_tokens= 300 , do_sample= False , streamer=streamer)) thread.start() yield from streamer class TTSStream (TTS): def __call__ (self, text_streamer): pattern = re.compile( r".+[.!。\n]" ) # 句点・改行のタイミングでTTSに渡す result = "" ptr = 0 for text in text_streamer: # LLMからの出力をストリームで受け取る result += text while ptr < len (result): m = pattern.search(result, ptr) if m: proc_text = result[ptr:m.end()] audio = self.forward(proc_text) yield proc_text, audio # 処理した文章と音声をストリームで返す ptr = m.end() else : break # process remaining text if len (result[ptr:]) > 0 : audio = self.forward(result[ptr:]) yield result[ptr:], audio 先ほどの例ならば「AIは私たちの暮らしに多岐にわたる影響を与えます。」まで出力された時点でTTSの実行や再生バッファへの入力を開始することで、これ以降のLLM推論によるレイテンシを隠蔽し、5.895秒から1.580秒まで短縮できました。 また、今回の実験ではH100を使っているというのもあり、基本的に生成される音声の長さよりも処理時間のほうが十分に短いため、LLMやTTSの処理が間に合わず再生バッファを枯渇させるというようなことは滅多に起こらないことがわかります。 先ほどの入力ケースでは下図のように、約50秒の応答音声を生成し切るのにかかった時間は5秒弱であり、その後はバッファされた音声を再生するだけとなっていることがわかります。 文節で区切る 前節では文単位で区切ってTTSに入力していましたが、どれくらい細かい区切りまでなら違和感のない音声を生成できるのでしょうか。今回は文節までなら別々に生成しても大丈夫だろうという仮定を置き実験してみました。 今回利用しているTTSモデルは VITS と呼ばれるものですが、前処理として Open JTalk による音素推定が行われています。これは入力したテキストから漢字の読み・単語の分割・アクセント位置などを解析し、音素と呼ばれる発音表記に変換する処理のことで、例えば「東京都に住む」という入力に対しては t o [ o ky o o ] t o n i # s u ] m u というような列に変換されます。ここで [ はピッチの上昇、 ] はピッチの下降、 # は文節の区切りを指し、読みやすく書き換えると ト↑オキョオ↓トニ/スム となります。この前処理結果を活用して、以下のような処理を行います。 LLMから新しく文章が生成されるたびに推定音素列を更新し、文節の切れ目が分かっているところまでの音素列をTTSに入力します。 この処理はOpenJTalkの前処理結果を抽出することで実現できます。 class TTSStream2 (TTS): def __call__ (self, text_streamer): clean = self.text2speech.preprocess_fn.text_cleaner to_tok = self.text2speech.preprocess_fn.tokenizer.text2tokens to_ids = self.text2speech.preprocess_fn.token_id_converter.tokens2ids fulltext = "" ptr = 0 token_buffer = [] token_ptr = 0 tts_input = [] for text in text_streamer: fulltext += text fulltoken = to_tok(clean(fulltext)) while True : token_buffer = fulltoken[token_ptr:] shift = len (token_buffer) if "#" in token_buffer[token_ptr:]: shift = min (token_buffer.index( "#" ), shift) elif "_" in token_buffer[token_ptr:]: shift = min (token_buffer.index( "_" ), shift) else : # not found break tts_input += token_buffer[:shift+ 1 ] token_ptr += shift+ 1 if len (tts_input) > 0 : ints_input = np.array(to_ids(tts_input), dtype=np.int64) audio = self.forward(ints_input) yield fulltext[ptr:], audio tts_input = [] # flush ptr = len (fulltext) # process remaining text tts_input = to_tok(clean(fulltext))[token_ptr:] if len (tts_input) > 0 : ints_input = np.array(to_ids(tts_input), dtype=np.int64) audio = self.forward(ints_input) yield fulltext[ptr:], audio この結果レイテンシを1.284秒まで短縮できました。 しかし、文節に区切ってTTSを行うと、音声の調子が少し不自然になってしまいました。以下の音声は「こんにちは」という入力に対する文単位TTSと文節単位TTSの結果です。 文単位TTS 文節単位TTS 文節間に不自然な間があったり、「どの/ような」のアクセントがおかしくなっていたりしています。 これは入力を文節ごとに制限したせいで、文節を跨ぐときの発音間隔や、文節が連なることによるイントネーションの変化が推定できなかったためと考えられます。 そこで次はTTS入力時に前後1文節をマージンとして一緒に推定し、目的の文節の音声のみを切り出すという方法をとってみました。 この処理を行うためには文章上の位置と読み上げ音声の位置を対応づける必要がありますが、 VITSの内部で生成される各音素の発音長を取り出すことで、生成した音声から目的の文節が読み上げられている区間を抽出できます。 これを行うと、レイテンシが1.284秒から1.290秒に少し悪化しますが、イントネーションが以下のように改善しました。 しかしながら、文節の継ぎ目でノイズが混入しており、生成音声を綺麗に切り貼りするのは難しいようです。 まとめると以下の表になります。 手法 レイテンシ(秒) 品質 シーケンシャル 5.895 ⚪︎ ストリーム処理(文単位) 1.580 ⚪︎ ストリーム処理(文節単位) 1.284 × ストリーム処理(文節単位+前後1文節マージン) 1.290 △ 音声の自然さという点では文単位で区切るのが限界のようです。システムプロンプトを工夫することで、応答の一文の長さ自体を短くすることも大事でしょう。 ユーザーからの割り込み 文章生成から音声出力までがストリーム処理になった場合、出力をいつでも中断できるという利点が生まれます。これとVADを組み合わせることで、応答中にユーザーが発言したときにLLM推論や音声出力を中断し、新しい応答を生成できます。ただし、アシスタントの応答音声をなんらかのスピーカーで出力させている場合は、応答の音声がそのままマイクに入ってしまってユーザーの発言と誤認してしまう問題を解決する必要があり、実用上はなかなか困難を伴うと思います。 まとめ 今回はGradioとLLMを用いて、ユーザーの音声を受け取り応答を音声で返す対話システムを作ってみました。このようなシステムではユーザーの発言が終わってからアシスタントが話し始めるまでのレイテンシがユーザーの体験に大きく影響しますが、LLMやTTSをストリーム処理することでレイテンシを短縮できました。また、ストリーム処理を実装することで、途中で推論をキャンセルできるという利点があることも紹介しました。 明日のアドベントカレンダーもお楽しみに。 参考資料 ChatGPT3.5 Turboを利用した対話システム https://developers.cyberagent.co.jp/blog/archives/44592/ Mini-omniのデモ https://huggingface.co/spaces/gradio/omni-mini/tree/main https://arxiv.org/abs/2408.16725 ↩
データ駆動型の意思決定が重要視される現代のビジネス環境において、プロダクト開発におけるデータ活用は不可欠です。 この記事では、開発中の新サービス COTOHA Insight Detector(仮称。以下、CID)でのデータ活用を通じて得られた知見と、データ活用の重要性について紹介します。 はじめに なぜデータ活用が大切か プロジェクトのゴールとチームの課題 プロジェクトの実施内容 ゴール設定 ヒアリングシート CJM作成 データ取得、分析環境の準備 仮説作成 検証 分析の課題 プロジェクトの振り返りと今後 おわりに はじめに こんにちは、デジタル改革推進部データドリブンマネジメント推進部門(以下、DDM)の組橋とコミュニケーション&アプリケーションサービス部(以下、C&A) CIDチームの村上です。 (組橋)DDMは、全社のデータ活用を推進する部署で、現在私は今回紹介するような各プロダクトのデータ活用の支援を実施したり、データ活用の研修を実施しています。 (村上)C&Aは、新サービスやアプリケーションを開発する部署で、現在私はCIDのプロダクトオーナーをしています。 今回はDDMのメンバーと一緒にリリース前のサービスに対するデータ活用を実施したプロジェクトの内容について説明します。 なぜデータ活用が大切か 本題に入る前に、なぜデータ活用が重要なのかについて触れておきます。 プロダクト開発において、ユーザーの声を聞くことは重要です。しかし、ときには「人はウソをつく」こともあります。 これはなにも悪意があってウソをつくことを指しているわけではありません。ユーザーの言葉が実態と乖離しているケースが発生しうるのです。 例えば、ユーザーが「この機能は結構使う」と言っていても、実際のデータを見るとほとんど使用していないことがあります。 他にも「これは基本機能なので、新規ユーザーのほとんどが使うだろう」と想定していても、実際にはかなりのユーザーが離脱していることもあります。 このような齟齬が生じる理由は、人間の記憶や認識が必ずしも正確ではないからです。 また、ユーザーが自身の行動を客観的に把握できていない場合もあります。 そのため、主観的な意見や直感だけでなく、データで検証することが重要になります。 プロジェクトのゴールとチームの課題 CIDのデータ活用プロジェクト(以下、本プロジェクト)におけるゴールは下記の通りと定めました。 ユーザー行動に関する定量的な仮説検証をCIDチームのみで実施できるようにすること このゴールとした理由については、プロダクト開発における下記のような課題をチームで抱えていたことに起因します。 プロダクト開発の課題 PoCなどを通じてご利用いただいているお客さまからは好評をいただいているものの、実際にはあまり使われないことがある データ分析の課題 ログをただ出しているだけで整理されていない(トラシュー目的のみ) どうすれば定量的に検証できるか不明 まず1点目の「良いと言ってるのに使ってくれない」と言う課題についてです。どんなプロダクトも技術的に優れている、あるいはUIが綺麗であるとしても、最終的にユーザが使ってくれないと意味がありませんし売れません。 そのためには開発段階からさまざまなターゲットユーザへのヒアリングやPoCなどを通し、どんな問題を解決するためにどんな人がどのように使ってくれるのか、と言う仮説検証をたくさん実施することになります。 その仮説検証について、特に「定性評価」を行なって評価する場合は、ターゲットユーザにある機能についてどう思っているかインタビューをします。 このインタビューでは、「とても良い」「便利で使いやすい」「もう少しサクサク動いてほしい」などポジティブ/ネガティブ問わず、さまざまな評価を得られます。 場合よってはポジティブな意見の割合が大きく、それを信じて特定の機能を価値あるものとして採用したとしても、実際にはユーザに使われない、なんてことがとてもよくあります。 これは社交辞令、日本人的な調和を優先とした回答、相手への配慮など色々な要因があると思われます。 しかし、先ほど述べた通り、直感は大切ですが「人はウソをつく」ので、やはりデータによる細かい検証が必要だと思います。 今のプロダクト開発は「定性評価」は欠かさず実施していましたが、データを用いた「定量評価」により各機能の利用状況などを仮説検証する、と言う流れはまだできていませんでした。 2点目についてですが、こちらもプロダクト開発にはよく見られる問題です。 トラブルシューティングや安全な運用のためにアプリケーションの動作ログそのものは保持しているのですが、それが開発段階でデータ分析の観点では生かされていなかった、と言うものになります。 また先述のような「定量評価」を素早く実施するためにどのような形でデータを保持するのか、どんな観点でデータを集計すればいいのかなど、雰囲気はわかっていても実際に自分たちで一気通貫に実施する経験やノウハウが足りていませんでした。 プロジェクトの実施内容 先のゴールを達成するために、本プロジェクトでは下記について実施しています。 ゴール設定 ヒアリングシート CJM作成 データ取得、分析環境の準備 仮説作成 検証 ゴール設定 最終的なゴールについては先に述べた通りですが、ゴールについてはプロダクトやチーム状況で変化します。 例えば別のチームでは、すでにサービスはリリース済みで、ユーザ離脱のタイミングをカスタマージャーニーマップに沿ってフェーズ遷移の分析をすることによる「顧客流入・離脱の仮説検証」に取り組んでいました。 一方自分たちのチームでは、まだサービスリリース前ということもありPoCなどは何件も実施していましたが、流入離脱を分析できるほどまだデータがなかったこともあり、既存機能の仮説検証をする方向でゴールを設定しました。 ヒアリングシート 企業の多くがそうであるように、NTT Comでもデータ分析組織であるDDMと実際に分析を実施したいサービス開発部署とで組織が分かれていることもあり、まずはお互いにプロダクトの状況をしっかり把握することが肝要でした。 そこで、下記のような内容を項目別に整理してみました。 プロダクト開発においては、エレベータピッチ、リーンキャンバスやバリュープロポジションキャンバスなどで整理されているものが多く含まれていると思います。 プロダクトの背景 NSM/KPI等の指標 収支状況 ペルソナ プロダクトの特徴 社内ステークホルダー データ活用状況 等 これらを活用することで、プロダクトの現在の状況や課題についてDDMと認識を合わせていきました。 CJM作成 カスタマージャーニマップ (以下、CJM) とは、顧客が商品やサービスを購入・利用するまでの道のりをわかりやすく整理したもので、プロダクト開発の際にも活用されます。 プロダクト開発用に全体のCJMは作成済みだったのですが、今回ゴールで設定していた「既存機能の仮説検証」に対応するため、トライアル時のユーザが触る機能をかなり詳細化したものを作り直しました。 こうすることでユーザが目的を達成するためにトライアルでどの機能をどういう順番で触り、どこに不満を感じどんな感情に変わっていくのか仮説を細かく定義でき、データ分析でどこを対象に定量検証するか、のイメージを固められました。 データ取得、分析環境の準備 具体的にどんな仮説を検証するのかは後述しますが、今回データ分析を実施するにあたり、商用環境のログを構造化していく必要があります。 NTT Comでは社内にDLXという社内の共用データ分析環境があるので、それを使う案やGCP(我々のプロダクトを構築している)上だけで完結させる案を検討しましたが、PJや案件の状況に応じて変わると思います。 検討したポイントは下記です。 学習コストがどのくらいか 分析にかかる時間はどのくらいか(リアルタイム性) データ設置に関するセキュリティ 変更容易性 データ連携のやりやすさ 最終的にはGCP上で完結させ、StackDriverからGCSに書き出したログを定期的に構造化して、分析を実施する形に落ち着きました。 仮説作成 今回ゴールである「ユーザー行動に関する定量的な仮説検証をCIDチームのみで実施できるようにすること」を達成するために、「既存機能について定量的な仮説検証」に取り組みました。 自分たちのプロダクトはいくつかの機能がすでにユーザヒアリングなどを通して価値があると判定されたものがいくつか存在しています。 そのうちの1つの機能について下記のような仮説を考えました。 当初の仮説: 「ユーザは分析結果画面で結果を確認するときに、<対象別、課題表示>、<課題別、対象表示> 両方のモードを使い分けて課題特定をしているはずだ。」 CIDにはVOC(お客さまの声)を分析する機能があるのですが、それを①「対象」別にどんな「課題」を持っているのかを表示する機能に加えて、②「課題」別にそれがどんな「対象」に分布しているのかを表示する機能が具備されています。 今回はこの②が本当に価値あるかを定量的に明らかにしようというわけです。 結論から述べるとこの仮説はデータ分析には不適切だと言うことがわかりました。 この仮説には下記の要素が抜け落ちているからです。 仮説検証後の具体的なアクション 仮説の判定をする具体的な目標数値 どちらもデータ分析結果が明らかになった後で決めると余計なバイアスがかかってしまうという共通の問題があります。 1については、結果が明らかになった後でその後のアクション(別の機能をためそう、検証機能自体を拡張しようなど)を決定してしまうとデータ分析の結果前提で自分たちの都合の良いアクションを決定してしまう可能性があります。 2については、例えば以下のような2つの仮説があるとき、両者は利用率が結果30%だったという結果は共通ですが、判定結果としての受け取り方が全く異なります。 利用率が20%を超えると思っていたが、30%だった。 利用率が40%を超えると思っていたが、30%だった。 後から数値を決めてしまうと都合のいいように判定結果を変えられてしまいます。 以上を踏まえて、ブラッシュアップ後の仮説は下記の通りとしました。 プラッシュアップ後の仮説: 「課題自体を特定したいユーザは分析結果画面で結果を確認するときに、<対象別、課題表示>モードの表示回数と比較して、<課題別、対象表示>モードの表示回数が50%以上となっているはずだ。」 合わせて仮説検証後の具体的なアクションも下記と定めました。 50%以上の場合:高頻度に<課題別、対象表示>モードを活用していると考えられるため、「課題を特定し、それに対して対策を講じるべき対象を決める」と言うケースにおいてCIDの価値を感じてくれているので現状のままでOK。 50%未満の場合:この機能が使われていない。課題ベースで分析の深掘りをしない理由を深ぼってヒアリングする。単純に機能に気がついていないのか必要性を感じないのかなど理由を探る。 実際にデータを見る前にここまで決めてからやっとデータの中身精査に取り掛かりました。 検証 下記は実際にあるPoCで使い始めたお客さまの一定期間の例です。 モード 表示回数 割合 <対象別、課題表示> 145 - <課題別、対象表示> 17 11.7% ユーザのヒアリング内容をもとに開発した機能は、実際に触った後のヒアリングでもとても肯定的な意見が多かったにも関わらず、実際のところ想定よりも使われていなかったことがわかりました。 正直この結果はユーザインタビューによる定性的な調査結果とは少しずれている結果だったので、チームとしても新しい発見であり真摯に受け止めて次回以降のユーザヒアリングに繋げていこうと思います。 最後に実際に実施した際に起きた問題点などについてもすこし触れておこうと思います。 分析の課題 キャッシュによる分析誤差 一部の機能についてはAPIコール回数などで定量評価をしようとすると、キャッシュが動作するなどして実際よりコール回数が小さくなってしまうなどの問題があるため、ユーザ行動を正確に把握するためにはフロントエンド側でもログをGoogle Analyticsなどで取得する必要がありました。 ログの構造化が煩雑 ログ設計もトラブルシューティング目的がメインだったりすると、後からリクエストやレスポンスのパラメータを詳細に見ようとした時にうまくシリアライズされていないと中身がわからなかったり、jsonとして読み取れなかったりして苦労することがありました。具体的には下記のような形のログだと分析ができません。 プロジェクトの振り返りと今後 CIDチームは、本プロジェクトを通じて以下の知見を得ました。 データ活用の一連の流れを経験できた ログの保存、整理方法 定量検証における仮説の立て方(基準値やnext action、有効な仮説) プロジェクト後、CIDチーム内では以下の変化が見られました。 Google Analytics (GA) 導入やログ整形の優先度が上がった データに基づいた議論・意思決定が目標になった 毎日利用量をSlackに流すようになった 日々の議論でも、パッとコマンドを叩くだけで実データによる仮説の裏付けができるようにしていきたいと考えています。 DDMでは、本プロジェクトで得られたノウハウはドキュメントにまとめ社内展開や、また研修コンテンツへの組み込みを検討しています。 スムーズな支援のために、支援メニューとして整理することも実施していきたいと考えています。 おわりに この記事では、プロダクトグロースにおけるデータ活用の重要性と具体的に実施した内容、得られた知見について紹介しました。 今後も、データから得られる知見を最大限に活用し、さらに価値ある取り組みを追求していきたいと考えています。
みなさんこんにちは、イノベーションセンターの益本 (@masaomi346) です。 Network Analytics for Security (以下、NA4Sec) プロジェクトのメンバーとして活動しています。 この記事では注意喚起を兼ねて、特殊詐欺を例に犯罪者のコミュニティで行われている活動を紹介します。 ぜひ最後まで読んでみてください。 警告 特殊詐欺について どのように詐欺に加担させようとするのか 特殊詐欺の裏で行われていること 1. 案件の紹介 受け子・出し子・かけ子 かけ子の仲介 荷受け・空き家の確認 SIMカードの契約 電話番号の契約 本人確認のなりすまし 偽造免許作成 2. 犯罪で使う道具の販売 空き部屋の紹介 銀行口座の販売 SIMカードの販売 3. 銀行口座や決済サービスのアカウントの買取など 銀行口座の買取 メルカリアカウント買取・レンタル 4. 違法薬物の販売 犯罪に加担するとどうなってしまうのか 特殊詐欺を減らす取り組み SNS上の闇バイトの募集に警告 AIを使った闇バイトの募集検知 金融機関との連携 何かあったときの相談先 さいごに 警告 この記事では犯罪者のコミュニティに関する話が出てきますが、 気軽に犯罪者のコミュニティへアクセスすることを推奨しているわけではありません 。 相手と直接接触しない場合であっても、犯罪者から捕捉される可能性があります。その点を念頭に置いてください。 特殊詐欺について 特殊詐欺とは、さまざまな手口で被害者と対面することなく信頼させ、最終的には現金などをだまし取る詐欺犯罪をいいます。 特殊詐欺の手口と対策 特殊詐欺の事件はよく話題になり、特に闇バイトで詐欺の実行役として犯罪に加担する人達が問題になっています。 闇バイトという単語をXのトレンドで定期的に見かけるぐらいに注目されています。 特殊犯罪による被害や詐欺に加担してしまう人を減らすべく、各所で注意喚起が出ています。 東京都 特殊詐欺加害防止特設サイト 闇バイトに注意!あなたを犯罪に巻き込む手口 / 実家にこまめに電話を! 特殊詐欺対策にもなります \ オレオレ詐欺などの特殊詐欺。家族が被害に遭わないか心配ですね。まめに電話で連絡を取り合っていると安心です。在宅時でも留守電にしておく、公的機関の名を出されても信用しないなどの対策も教えてあげましょう。 https://t.co/sAi8dZnB1t pic.twitter.com/oW8f7wbxcA — 政府広報オンライン (@gov_online) 2022年5月2日 【SNSなどで求人情報を探している方へ】 本年8月以降、相次いで発生している凶悪な強盗事件について、具体的な事例や特徴等をまとめました。参考にしていただき、この種の求人には応募しないようにしてください。 #警察 #強盗 #犯罪 #求人 #高額 #高収入 #即日 #運び #送迎 #ホワイト #簡単 pic.twitter.com/bd5ADjG5iO — 警察庁 (@NPA_KOHO) 2024年10月25日 どのように詐欺に加担させようとするのか 闇バイトや裏バイトなどとして、以下で募集されています。 SNS 掲示板サイト 求人サイト etc. 特に、SNSで募集されているのをよく見かけます。 以下の画像のように、さまざまなキーワードで募集をかけて犯罪に加担させようとします。 また、口座買取やアカウント買取という形で、犯罪に加担させようとすることもあります。 特殊詐欺の裏で行われていること 特殊詐欺に関わっている犯罪者達がどのような活動をしているのか、あまりピンとこないと思います。 特殊詐欺のコミュニティで行われている活動内容を実際のメッセージを見ながら紹介していきます。 ※ここに書かれているものはほんの一部であり、すべてこうというわけではありません。 1. 案件の紹介 リクルーターや実行役向けにさまざまな案件が紹介されています。 受け子・出し子・かけ子 かけ子の仲介 荷受け・空き家の確認 SIMカードの契約 電話番号の契約 本人確認のなりすまし 偽造免許作成 2. 犯罪で使う道具の販売 犯罪を支援するための道具を販売している人達が存在しています。 空き部屋の紹介 銀行口座の販売 SIMカードの販売 3. 銀行口座や決済サービスのアカウントの買取など 買い取ることで、詐欺などで使う振り込み先の口座を確保しています。 銀行口座の買取 メルカリアカウント買取・レンタル 4. 違法薬物の販売 闇バイトの募集から違法薬物の販売や配送に加担させられることがあります。 犯罪に加担するとどうなってしまうのか 犯罪に加担すると、いろんなものを失うことになります。 実名報道されて名前が残り続けることになる 実刑判決を受ければ、膨大な時間を失うことになる 場合によっては賠償金を支払わなければならなくなる 家族や友達含め誰からも信用されなくなる ここ最近闇バイトによる強盗が話題になっていますが、強盗による量刑は皆さまが想像されるよりも重く、特に強盗殺人罪や強盗致死罪の場合だと最低でも無期懲役になります。 また、強盗の準備をするだけでも強盗予備罪に問われる可能性があります。 闇バイトに加担して逮捕された人が口を揃えて後悔していることを語る記事が出ています。 “ルフィ”事件の実行役「闇バイト応募は終わりの始まり」 逮捕の男「闇バイトはもうやめようと思った」横浜強盗殺人 特殊詐欺を減らす取り組み 注意喚起以外にも、特殊詐欺を減らすためにさまざまな取り組みが行われています。 ほんの一例を紹介します。 SNS上の闇バイトの募集に警告 SNS上の闇バイトの募集ポストに対して、以下の画像のように警告を発信している取り組みが見られます。 日夜闇バイトの募集が行われていないか監視しています。 目立つように返信や引用して警告することで、被害を減らそうとしています。 警察庁 警視庁 千葉県警察 神奈川県警察 埼玉県警察 大阪府警察 京都府警察 兵庫県警察 愛知県警察 三重県警察 北海道警察 山梨県警察 長野県警察 福井県警察 岡山県警察 愛媛県警察 鹿児島県警察 AIを使った闇バイトの募集検知 投稿されたものが闇バイトの募集かどうかの判断を効率化するためにAIを活用する取り組みも見られます。 目視で確認すると時間がかかるため、ある程度の自動化も重要になっていきます。 闇バイトの募集投稿、AIで自動検知…人の目で探すより最大34倍の速さで警告発出 バイトル、生成AIを活用した「闇バイトチェックAI」を開始 金融機関との連携 不正な口座情報を共有したり、特殊詐欺のアポ電発生時に自動音声で連絡するなど、さまざまな方法で被害を減らそうとしています。 “不正な口座情報”を金融機関に提供…全国初の協定 埼玉県警 特殊詐欺防止に期待! 山形県警が新たに導入した「シン・オートコール」とは 何かあったときの相談先 もしあなたが、特殊詐欺の実行役として犯罪をさせられそうになっていたら、何も怯えることなく警察に相談しましょう。 警察が犯罪に加担しないように呼びかけており、相談した後にさまざまな対策をとってくれています。 また、あなたがもっている情報が犯罪組織の撲滅につながることもあります。 闇バイト 警察の呼びかけ強化以降 応募者など保護のケースも 状況によっては匿名で通報したい時もあるかと思います。 そのときは、下記の匿名通報ダイヤルへ通報するという手もあります。 匿名通報ダイヤル 匿名で通報できるだけでなく、通報した後どうなったか確認できます。 さいごに 今回紹介した特殊詐欺に限らず犯罪の分業化が進んでおり、一部の人を逮捕してもなかなか犯罪がなくなりません。 そのため、犯罪を減らすには、犯罪者を逮捕するだけでなく犯罪で利益を出なくすることも重要になります。 犯罪を知ることで被害を減らすヒントにつながるかもしれません。 本記事により、被害を受ける人を少しでも減らせるといいと筆者は考えています。