NTTドコモビジネスのブログ - TECH PLAY

TECH PLAY

NTTドコモビジネス

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

632

この記事は、 NTTコミュニケーションズ Advent Calendar 2023  20日目の記事です。 はじめに こんにちは。 コミュニケーション&アプリケーションサービス部の吉仲です。 新卒2年目で、普段はB向け/C向けメールシステムと文書要約APIサービスの開発・運用に関する業務に取り組んでいます。 今回は、昨年から引き続き話題の生成AIのひとつ、大規模言語モデル (LLM: Large Language Model) を題材に、LLMを使って文章を「やさしい」表現へ言い換える例を紹介します。 この記事の内容 この記事では、以下の内容を扱います。 やさしい日本語 言い換え技術とテキスト平易化 LLMを使ったやさしい日本語への言い換え 前半にやさしい日本語、言い換え技術・テキスト平易化について簡単に解説し、後半はLLMによるやさしい日本語への言い換えの例を紹介します。 なお、この記事ではLLMの技術的詳細やプロンプトエンジニアリングの細かいテクニックには触れません。 はじめに この記事の内容 なぜ「やさしい日本語」を使うのか? 言い換え技術とテキスト平易化 言い換え技術 テキスト平易化 LLMでやさしい日本語へ言い換える Zero-Shotプロンプト Few-Shotプロンプト ガイドラインを活用したプロンプト LangChain Chainsを使った二段階の言い換え おわりに なぜ「やさしい日本語」を使うのか? 厚生労働省によると、令和4年10月時点の外国人労働者の数は180万人を超え、過去最高を更新しています 1 。 今後も労働人口の減少やグローバル化を背景に、日本に住む外国人はますます増え、その国籍もさらに多様化していくと考えられます。 日本に住む (あるいは訪れる) 外国人へ正しく情報を伝えるためには、多言語翻訳・通訳のほか、 やさしい日本語 での情報提供も有効な手段となります 2 。 また、在留外国人に限らず、より多くの人に伝わりやすい日本語表現を使うことが、ユニバーサルコミュニケーション 3 へのアプローチにもなると考えます。 やさしい日本語の書き方や話し方について、出入国在留管理庁と文化庁がガイドラインを作成しています。 www.bunka.go.jp 書き方のガイドラインでは、日本人に分かりやすい文章、そして外国人にも分かりやすい文章の作り方のポイントが紹介されています。 テキストでのコミュニケーションが一層増えた昨今、このガイドラインを読みながら自身の文章の書き方を一度振り返ってみると良いかもしれません。 かく言う私も、普段の仕事の中で難しく回りくどい書き方やカタカナ語を多用してしまい、分かりやすさを意識できていないことが多いです。 やはりすぐにやさしい日本語を書き出すことは難しく、どうしても推敲に時間がかかってしまうと感じます。 そこで今回は、LLMを用いて文章を自動的にやさしい日本語へ言い換えることを試してみます。 なお、この記事ではあくまで文章の分かりやすさにフォーカスすることとします。 文書全体の読みやすい構成や展開には触れません。 言い換え技術とテキスト平易化 ここからは自然言語処理 (NLP: Natural Language Processing) の話になります。 言い換え技術 NLPでの重要な問題のひとつにテキストの持つ曖昧さがあります。 この曖昧さには以下の問題が含まれます。 多義性の問題:同じ言語表現でも意味が異なる 同義性の問題:(近似的に) 同じ意味でも言語表現が異なる 前者は例えば「多義性解消」といったキーワードなどで、NLPの黎明期から研究されています。 後者は主に 言い換え (Paraphrase) 技術として研究されています。 言い換え技術とは、以下の認識技術と生成技術のことをまとめて指します。 言い換え認識:意味が同じ言語表現かどうかを判定する 言い換え生成:ある言語表現から (意味を保ちながら) 別の表現に変換する 後者の言い換え生成では、ニューラル機械翻訳モデル 4 を使い、同じ言語の中で翻訳する手法が近年の主流です 5 。 さらには、報酬を同義性や文法性、目的の言い換えの指標 (例えば、簡単な表現にしたければ難易度) に基づき設計した強化学習の手法も存在します 6 。 テキスト平易化 テキスト平易化 (Text Simplification) は、言い換え生成タスクのひとつです。 テキスト平易化では、入力文の意味を保ちながら、難しい表現をより 易しい 表現へ言い換えます。 例えば、あるテキストの中の表現「捺印する」を「はんこを押す」という表現へ変換していきます。 最近では以下の論文のように、ChatGPTなどのLLMを用いてテキスト平易化タスクを解く手法 7 も研究されています。 arxiv.org この論文では、以下のようなZero-ShotもしくはFew-Shotプロンプト 8 でテキスト平易化を行っています。 (※例は論文内のZero-Shotプロンプトの図から引用) I want you to replace my complex sentence with simple sentence. Keep the meaning same, but make them simpler. Complex: His next work, Saturday, follows an especially eventful day in the life of a successful neurosurgeon. Simple: {Outputs} テキスト平易化に限る話ではないですが、LLMはごく少数の例 (今回は難しい文と易しい文のペア) を用意するだけで、 もっと言えば例を用意しなくとも、さまざまなタスクを解いてしまいます。 以降では、このLLMでのZero-Shot/Few-Shotプロンプトによって、やさしい日本語への言い換えを行います。 さらに、上記の「やさしい日本語」ガイドラインを活用し、 押さえるべきポイントをプロンプトに組み込むことで、より上手くやさしい日本語へ言い換えられないか模索してみます。 LLMでやさしい日本語へ言い換える 前置きが長くなりましたが、ここからが本題の「LLMによるやさしい日本語への言い換え」の話です。 今回は以下の環境・モデルを使用します。 Azure OpenAI GPT-4 (32kトークン) また、やさしい日本語への言い換え例として、前述のガイドラインの例文を引用させていただきます。 以降では、ガイドライン中の以下の例文をやさしい日本語へ言い換えてみます。 父母が共に外国籍の場合、出生届が受理されると、子供が生まれた日から60日の間は、 在留資格を有することなく住民票が作成されます。 子供が生まれた日から60日を超えて日本に滞在しようとする場合は、 子供が生まれた日から30日以内に最寄りの地方出入国在留管理官署において、 在留資格の取得申請をする必要があります。 Zero-Shotプロンプト まずは、Zero-Shotプロンプトでテキスト平易化を行います。 プロンプトは前述の論文を参考に以下のように設計します。 I want you to replace my complex sentence with simple and easy-to-read sentence in Japanese. Keep the meaning same, but make them simpler. Complex: """ {Input} """ Simple: 指示文は英語のまま 9 、読みやすさについての指示 ("easy-to-read sentence") と、日本語での言い換え指示 ("in Japanese") を追加しています。 結果は以下のようになりました。 (※実行環境はAzure AI Studio チャットプレイグラウンド) 「資格を有することなく」を「資格なしで」に言い換えるなど、表現の平易化はできていそうです。 一方で「60日を超え」が「60日以上」になったり、申請場所の情報が抜けたりなどが見られます。 もし情報提供などでこの出力を使うなら、やや情報の不正確さ・欠落が気になるかなと思いました。 Few-Shotプロンプト 次に、Few-Shotプロンプトでテキスト平易化を行います。 プロンプトはZero-Shotのときと同様のものを使います。 プロンプトに組み込む例として、再びガイドライン中の例文をお借りします。 結果は以下のようになりました。 Zero-Shotのときに見られた情報の不正確さ・欠落がやや緩和されつつ、やさしい表現になっていますね。 ただ、プロンプトに入れた例が1つだけなのもあり、箇条書きで読みやすくするような言い換えは再現できていません。 ガイドラインを活用したプロンプト ガイドラインに載っている言い換え例は以下のような文章です。 日本で生まれた子供が、 ・日本国籍を持たない ・出生後60日を超えて引き続き日本に滞在したい ときは、出生した日から30日以内に、地方出入国在留管理局で在留資格取得の申請を行う必要があります。 上で紹介したZero-Shot/Few-Shotプロンプトでは残念ながら再現できていません。 そこで、この例のような言い換えを目標に、ガイドライン中のアドバイスをプロンプトに組み込んでみます。 (ただし「父母が共に外国籍」→「子供が日本国籍を持たない」のような言い換えはさすがに難しいと思うので、可能な範囲での再現を目指します) プロンプトは以下のように設計します。 ガイドライン中のアドバイスを英語に訳して追加しています。 このプロンプトをテンプレートとし、Zero-Shot/Few-Shotでの言い換えを試します。 I want you to replace my complex sentence with simple and easy-to-read sentence. Keep the meaning same, but make them simpler in Japanese. Follow the instructions below; 1. organize what I want to say, 2. select and discard information, 3. keep each sentence short (only one thing to say in one sentence), 4. use bullet points if there are more than three things to say, 5. use no roundabout phrases. Complex: """ {Input} """ Simple: 結果は以下の通りです。 Zero-Shotプロンプトによる言い換え : Few-Shotプロンプトによる言い換え : 後者の結果は、ガイドライン中の言い換え例にいくらか近くなったのではないでしょうか。 もう少しプロンプトを作り込むとしたら、複数の前提条件を箇条書きするような指示を与えると良いかもしれません。 今回はここまでとしますが、元のべた書きの文章よりもかなり分かりやすくなったと思います。 LangChain Chainsを使った二段階の言い換え ここまで触れませんでしたが、ガイドラインには「外国人にも分かりやすい文章」のアドバイスも含まれます。 最後はこちらにも挑戦してみます。 外国人にも分かりやすい日本語へ言い換えるためのプロンプトは以下のように設計します。 日本語の非ネイティブ向けであることを明示し、ガイドライン中のアドバイスを指示に組み込んでいます。 I want you to replace my sentence with easy sentence for non-native of Japanese. Keep the meaning same, but make them easier in Japanese. Follow the instructions below; 1. use simple phrases, 2. note the amount of kanji and supplement Furigana of difficult kanji. Input: """ {Input} """ Output: 今回は最初から外国人にも分かりやすい文章を生成するのではなく、前章で生成した言い換えの結果をさらに言い換える形とします。 LangChain 10 のChainsのうち SimpleSequentialChain は、1つめの出力を2つめのプロンプトに渡すという処理を簡単に実装できます。 今回はそれほど複雑な処理ではありませんが、せっかくなのでこれを使用してみます。 実装には以下のライブラリを使用します。 langchain==0.0.345 openai==0.28 ソースコードは以下の通りです。 Azure OpenAIの各種情報や、Few-Shotプロンプトに使う例文、入力文は適宜埋めてください。 import os from langchain.chains import LLMChain, SimpleSequentialChain from langchain.chat_models import ChatOpenAI from langchain.prompts import PromptTemplate os.environ[ "OPENAI_API_TYPE" ] = "azure" os.environ[ "OPENAI_API_KEY" ] = "xxxxx" os.environ[ "OPENAI_API_BASE" ] = "https://your-resource-name.openai.azure.com/" os.environ[ "OPENAI_API_VERSION" ] = "2023-07-01-preview" os.environ[ "DEPLOYMENT_NAME" ] = "xxxxx" template_for_simple_ja = "xxxxx" # プロンプトのテンプレート、入力文の位置には`{input}`を入れる template_for_easy_ja = "xxxxx" # プロンプトのテンプレート、入力文の位置には`{input}`を入れる def main (): llm = ChatOpenAI( model_name= "gpt-4-32k" , model_kwargs={ "deployment_id" : os.environ[ "DEPLOYMENT_NAME" ]}, temperature= 0 , ) prompt_1 = PromptTemplate(input_variables=[ "input" ], template=template_for_simple_ja) chain_1 = LLMChain(llm=llm, prompt=prompt_1) prompt_2 = PromptTemplate(input_variables=[ "input" ], template=template_for_easy_ja) chain_2 = LLMChain(llm=llm, prompt=prompt_2) overall_chain = SimpleSequentialChain(chains=[chain_1, chain_2], verbose= True ) text = "xxxxx" # 言い換えたい入力文 print (overall_chain(text)) if __name__ == "__main__" : main() このプログラムの実行結果は以下のようになります。 $ python3 text-simplification.py > Entering new SimpleSequentialChain chain... """ 両親が外国籍の場合、子供が生まれたら、 ・60日間は在留資格なしで住民票が作成されます。 ・60日を超えて日本に滞在する場合、生まれてから30日以内に在留資格の申請が必要です。 ・申請は最寄りの地方出入国在留管理官署で行います。 """ """ お父さんとお母さんが外国の人だったら、赤ちゃんが生まれたら、 ・60日間は、ビザ(ざいりゅうしかく)がなくても、住んでいるところを登録(とうろく)できます。 ・60日より長く日本にいるなら、赤ちゃんが生まれてから30日以内にビザの申し込み(もうしこみ)が必要(ひつよう)です。 ・申し込みは、近くの出入国管理事務所(しゅつにゅうこくかんりじむしょ)でやります。 """ > Finished chain. {'input': '父母が共に外国籍の場合、出生届が受理されると、子供が生まれた日から60日の間は、在留資格を有することなく住民票が作成されます。子供が生まれた日から60日を超えて日本に滞在しようとする場合は、子供が生まれた日から30日以内に最寄りの地方出入国在留管理官署において、在留資格の取得申請をする必要があります。', 'output': '"""\nお父さんとお母さんが外国の人だったら、赤ちゃんが生まれたら、\n・60日間は、ビザ(ざいりゅうしかく)がなくても、住んでいるところを登録(とうろく)できます。\n・60日より長く日本にいるなら、赤ちゃんが生まれてから30日以内にビザの申し込み(もうしこみ)が必要(ひつよう)です。\n・申し込みは、近くの出入国管理事務所(しゅつにゅうこくかんりじむしょ)でやります。\n"""'} 私は日本語ネイティブのため、非ネイティブが分かりやすい表現になったかどうかは判断しづらいですが、ガイドライン中の例文には近くなったのではと思います。 ただ、「漢字の量に注意」という指示が「在留資格」→「ビザ」というやや異なる意味への単語の言い換えを誘発してしまっています。 例えば在留外国人向けの資料や案内を作成したいとき、AIによる多言語翻訳で対応すると、誤訳の修正などのコストが大きくなりがちだと思います。 一方、やさしい日本語への言い換えで対応するなら、上記の程度の間違いであれば修正コストも高くないのではと思います。 おわりに この記事では、LLMによってやさしい日本語へ言い換える例を紹介しました。 もしすぐにやさしい日本語で書くことが難しいと感じるなら、ぜひこの記事を参考にLLMを使った言い換えを試してみてください。 また、今回は定量評価まで行いませんでしたが、言い換え技術・テキスト平易化の評価手法も奥が深い研究分野だと思います。 気になった方はぜひ調べてみてください。 最後までご覧いただきありがとうございました!それでは、明日の記事もお楽しみに! 『 「外国人雇用状況」の届出状況まとめ(令和4年10月末現在) 』 (厚生労働省, 令和5年1月27日) ↩ 『 東京都在住外国人向け 情報伝達に関するヒアリング調査報告書 』によると、機械翻訳された母国語よりもやさしい日本語の方が好まれる傾向にある。 ↩ 『 「ユニバーサルコミュニケーションデザイン」(UCD)とは 』 (ユニバーサルコミュニケーションデザイン協会 (UCDA)) ↩ " Sequence to Sequence Learning with Neural Networks " (Sutskever et al., arXiv 2014) ↩ " Exploring Neural Text Simplification Models " (Nisioi et al., ACL 2017) ↩ " Text Simplification with Reinforcement Learning Using Supervised Rewards on Grammaticality, Meaning Preservation, and Simplicity " (Nakamachi et al., AACL 2020) ↩ " Sentence simplification via large language models " (Feng et al., arXiv 2023) ↩ " Language Models are Few-Shot Learners " (Brown et al., arXiv 2020) ↩ 生成の質向上 (経験的には英語のプロンプトの方が良い) とトークン数削減のため。 ↩ https://www.langchain.com/ ↩
この記事は、 NTT Communications Advent Calendar 2023 19日目の記事です。 この記事では、TypeScript未経験のインターン生にすぐにSkyWayの開発に取り組んでもらうために、TypeScriptの学習用コンテンツを作成した話を紹介します。 学習用コンテンツでどのようなスキルを身に着けてもらったのか、効果的に学ぶためにどのような点を工夫したのかについても説明します。 はじめに 学習用コンテンツの目的 TypeScript学習用コンテンツの紹介 取り組んでもらった結果 より高度な内容について おわりに はじめに 皆さまこんにちは。イノベーションセンター SkyWay DevOps プロジェクト所属の @sublimer です。 SkyWayのチームでは、今年の8〜9月に現場受け入れ型のインターンシップを実施しました。 インターン生を受け入れるにあたって、すぐにSkyWayの開発に取り組んでいただけるようにTypeScriptの学習用コンテンツを作ったので、本記事ではその紹介をします。 学習用コンテンツの目的 今回コンテンツを制作するにあたって、以下のスキルを学ぶことを目的として内容を検討しました。 また、インターンシップの期間は2週間で時間が限られているため、学んでほしい内容には優先順位を付けて、優先度が高いものから順に学んでもらうようにしました。具体的には下記の通りです。 TypeScriptでのサーバーサイドアプリケーション開発 ドキュメント作成 コードレビューのreviewee テストコード CI/CD セキュリティ SkyWayでは、サーバーサイドアプリケーションをTypeScriptで開発しているため、まずはTypeScriptでのサーバーサイドアプリケーション開発を学んでもらうことを目的としました。 また、普段の開発では仕様を考える際に草案をドキュメント化して議論したり、書いたコードをGitHub上でレビューしているため、これらの点についても経験できるようにしました。 テストコードやCI/CDについては、アプリケーションを作る上では必ずしも必要ではないのですが、商用のサービスを開発する上では必要となるスキルであるため、これらについても学べるようにしました。 また、セキュリティについても観点の1つとして意識できるようにしました。 TypeScript学習用コンテンツの紹介 今回は、「指定されたnpmパッケージをインストールしても大丈夫そうか判定するAPI」というお題で、アプリケーションを作ってもらうことにしました。 このアプリケーションを開発するためには、以下のような機能を実装する必要があります。 npmのAPIを利用して、指定されたnpmパッケージの情報を取得する GitHubのAPIを利用して、対応するGitHubリポジトリの情報を取得する GitHubのAPIを利用して、対応するGitHubリポジトリのownerの情報を取得する 以下のような基準を満たしているかどうかを判定して、その結果に応じてレスポンスを返す(基準はあくまでも一例) 直近1年以内に更新がなされているか(アクティブ度合い) インストール数が500以上あるか(利用実績の有無) コントリビューターが5人以上いるか(複数人がコードに目を通しているか) 実装にあたっては、SkyWayのサーバーサイドアプリケーションでも利用しているフレームワークである Fastify を利用してもらいました。 (余談ですが、 Fastifyを利用している組織の一覧 にSkyWayも載せていただきました。) SkyWayのシステムでは、あるアプリケーションから別のアプリケーションのAPIを呼び出す処理が複数あります。 そのため、APIを呼び出し、その結果を元に処理を行うという部分は、SkyWayの開発を体験する際に役立つと考えました。 npmやGitHubのAPIを利用するメリットは、レートリミットなどの制限はあるものの、今回のアプリケーションで利用する範囲においてはAPIキーなどの認証情報を用意する必要がないことです。 今回はインターンシップという限られた時間の中で開発をしてもらう都合上、アカウントや認証情報の準備が不要なAPIのみを利用して、開発ができるようにしました。 また、今回はAPI呼び出しの処理をクライアントライブラリを使わずに実装してもらうことにしました。 APIのレスポンスはJSON形式となっているため、TypeScriptでレスポンスのデータを扱う際は、型定義を用意する必要があります。 一例として、GitHubのリポジトリ情報を取得するAPIは、以下のドキュメントにあるようなデータを返します。 docs.github.com 実際のアプリケーションではこれらのデータのうち一部しか使わないので、必要なデータのみを抽出した、以下のような型定義を用意する必要があります。 type RepositoryInfo = { name: string ; full_name: string ; owner: { login: string ; id: number ; } ; stargazers_count: number ; created_at: string ; updated_at: string ; pushed_at: string ; } ; JSONのデータを元に型定義を作成することで、TypeScriptでは型による表現がどのようになっているのかを学べます。 ドキュメントの作成やコードレビューの観点についても、APIドキュメントを作成してもらったり、GitHub上でコードレビューをしてもらうことで、実際の開発に近い形で経験できるようにしました。 また、テストコードやCI/CDについても、取り組みやすいようにコンテンツを検討しました。 具体的には、「基準を満たしているかどうかを判定する」というロジックは複数の条件の組み合わせで判定する必要があるため、テストコードで挙動を担保する価値が大きい部分です。 特にこの部分について、テストコードを書いてもらうことで、テストコードの価値をより実感してもらえるのではないかと考えました。 加えて、外部のAPIを呼び出す箇所はモックを利用してテストを実行することになるため、どの部分をどのようにモックするかという観点についても学べるようにしました。 テストにおいて何をどこまでモックすべきかについては、 fukabori.fmの第13回が非常に参考になる のでおすすめです。 fukabori.fm さらに、セキュリティについても、直接ではないものの意識できるようにしています。 npm等のパッケージマネージャーを利用する際は、サプライチェーン攻撃などのセキュリティリスクについて意識する必要があります。 例えば、使おうと思ったパッケージが長い間メンテナンスされていなかったりインストール数が非常に少ない場合、脆弱性の対応が行われなかったり、悪意のあるコードが含まれてリリースされても誰も気づかない、といったリスクが想定されます。 そのため、今回のコンテンツでは、npmパッケージの情報を取得し、その情報を元に判定の処理を実装してもらうことで、npmなどのパッケージマネージャーを利用する際のセキュリティについても意識できるようにしました。 ソフトウェア開発のサプライチェーンセキュリティについては、NTT Communications Advent Calendar 2023 14日目の記事で詳しい解説をしていますので、こちらもぜひご覧ください。 engineers.ntt.com このアプリケーションの開発では以下のようなタスクを行うため、身につけてほしいスキルを効率的に学ぶことができます。 TypeScriptでのサーバーサイドアプリケーション開発(必須) TypeScriptの書き方の理解 Fastifyの使い方の理解 非同期処理(Promise、async/await)の扱い方の理解 APIの設計 ドキュメント作成(必須) コードレビュー(必須) テストコードの追加(ここまでできるとGood) GitHub ActionsによるCI(ここまでできるとGood) Cloud Runへのデプロイ(ここまでできるとGood) デプロイの自動化(できなくても大丈夫) 新機能の実装(できなくても大丈夫) タスクについては、多すぎても少なすぎてもつまらなくなってしまうと考え、多めに準備した上で「必須」「ここまでできるとGood」「できなくても大丈夫」にカテゴライズして、必須から先は進捗状況に応じて取り組んでもらうことにしました。 取り組んでもらった結果 今年の8月28日から9月8日にかけてインターンシップに参加してくださった鈴木さんに、実際にこちらのコンテンツに取り組んでいただきました。 鈴木さんはTypeScriptの経験は無いとのことでしたが、「できなくても大丈夫」のタスクまで実施していただき、良い意味で期待を裏切っていただきました。 そして、その後の商用環境で動いているコードの改善にもスムーズに取り組んでいただきました。 鈴木さんのインターンシップの取り組みは、以下のブログ記事に詳しく書かれているので、ぜひご覧ください。 engineers.ntt.com より高度な内容について 今回は、サーバーサイドアプリケーションの実装、テストコードの追加、CI/CDなどに取り組んでいただきましたが、このコンテンツはより発展的なものにできると考えています。 一例として、以下のような応用的な取り組みが考えられます。 OpenAPIなどのAPI定義を元にした、ドキュメントやクライアントライブラリの自動生成 OpenTelemetryやAPMなどを用いたアプリケーションのモニタリング&改善 RESTではなく、GraphQLを利用してみる 生成AIと組み合わせた判定処理の高度化 npm以外のPackage Registryへの対応 また、題材自体は言語やフレームワーク、クラウドサービスに依存する箇所は少ないため、GoやPython、Rustのような他の言語や、Azure、AWSといったGoogle Cloud以外のクラウドサービスを利用してもらうことも可能です。 もし、オンボーディング用のネタに困っている方がいれば、ぜひ参考にしてみてください。 おわりに SkyWayでのインターン生に取り組んでもらった、オンボーディング用のコンテンツについて紹介しました。 インターンは期間が限られているため、参加者のスキルと実際に業務に取り組むためのスキルの間にギャップがある場合は、できるだけ時間をかけずに必要なスキルを身に着けてもらう必要があります。 今回のコンテンツは人にもよるかとは思いますが、TypeScript未経験でも概ね2〜3日程度で一通り取り組めるボリュームになっています。 また、実際のサービス開発に近い形で取り組めるように、ドキュメント作成やコードレビュー、テストコードの追加やCI/CDなども取り入れています。 インターン生の受け入れに限らず、オンボーディングやプログラミング学習用のコンテンツを作成する際の参考になれば幸いです。 以上、 NTT Communications Advent Calendar 2023 19日目の記事でした。明日もお楽しみに!
この記事は、 NTT Communications Advent Calendar 2023 18日目の記事です。 はじめに この記事はCOTOHA Call Center開発チームの福田、立木、木村の共同執筆です。 この記事では、私たちが普段の開発業務の中で工夫している自動化関連の取り組みについて共有します。 私たちはCOTOHA Call Centerというサービスをスクラム手法で開発し、福田はスクラムマスター、立木と木村は開発者として参画しています。 COTOHA Call Centerの概要 COTOHA Call Centerは簡易なコールセンター機能を搭載したIP電話のサービスです。 IVRのカスタマイズや音声ガイダンス作成、AIオペレーター機能を実装しており、WEBブラウザ版とモバイル版をリリースしています。 基本的に全てJavaScript/TypeScriptで実装しており、WEBブラウザ版ではフロントエンドはVue.js、バックエンドはNode.js+Expressを採用しています。 また、モバイル版はReact Nativeを採用しており、基本的に単一のコードベースでiOS/Androidを同時に実装していますが、通話部分など一部の機能はネイティブ(Swift/Java)で実装しています。 自動化の背景 開発業務に携わっていると同じ内容の作業を何度も行うような機会が発生します。例えば検証環境への反映、システムの基本的な機能に関するテスト、モバイルアプリのテスターへの配布などがあります。スクラムのように短いリリースサイクルで開発する手法だと、そのような定型作業の頻度が高くなります。COTOHA Call CenterではWEBブラウザ版、iOS版、Android版を開発しており定型作業のボリュームが多いため自動化の取り組みを積極的に進めてきました。 自動化している作業は多くありますが、この記事ではモバイルに関する取り組み内容について共有します。 CI/CDの取り組み GitHub Actions まず、GitHub Actionsについて説明します。 GitHub Actionsは、GitHubが提供する自動化プラットフォームサービスです。リポジトリ内で事前にさまざまな「ワークフロー」を定義し、自動的にビルド、テスト、デプロイなどを実行し、開発作業の手間を減らすことができます。 Self-hosted Runner GitHub Actionsの実行環境には2種類あります。 GitHub-hosted Runner :GitHubが提供するクラウドの実行環境で実行する Self-hosted Runner :ユーザーが自分で準備したコンピュータやサーバーで実行する GitHub Actionsで通常使われるのはGitHub-hosted Runnerで、私たちも以前はこちらを使用していました。 一方で、(privateリポジトリが対象の場合)GitHub-hosted Runnerと比較したときにSelf-hosted Runnerには下記のメリットがあります。 内部ネットワーク上で安全に実行できる 従量課金がない 以上の理由から、Self-hosted Runnerを採用して環境を構築しました。 Self-hosted Runnerを使う際は、自身でサーバーを管理するため、ハードウェアのリソースを確保することや、外部アクセスの制限や通信の安全性に注意が必要です。 加えて、環境構築でつまづきやすいポイントと対処法を簡単に紹介しておきます。 Runnerの登録ができない GitHubのトークンやリポジトリのURLが正確であるか確認してみてください。私たちの場合トークン切れが原因でした。 RunnerがOfflineになる ./run.sh を実行しただけではターミナルを閉じたときに利用できなくなってしまいます。actions-runnerをインストールしたフォルダに移動し、 ./svc.sh install および ./svc.sh start コマンドをサービスとして実行することで起動し続けることが可能です。 使用用途 私たちは以下の用途でGitHub Actionsを利用しています。 これらの作業は、GitHub Actionsを活用することで、プッシュしたタイミングやプルリクエストをマージしたタイミングなど任意のタイミングで自動的に実行できます。 統合・単体テスト(jest) 統合テストおよび単体テストを行うテストフレームワークには主にjestを活用しています。 jestとはJavaScriptのテストフレームワークであり、私たちのプロジェクトではwebアプリとモバイルアプリ両方の開発で利用しています。特にReact Nativeではデフォルトで使用できるため導入の手間なく扱えます。 デプロイ 本番環境やステージング環境に自動でデプロイします。私たちの場合、開発者のプルリクエストをマージする際に、クラウド環境上のWEBサーバーとAPIサーバーに自動でデプロイするように設定しています。 テスト配布 テスト用のモバイルアプリを配布します。詳細は後述します。 テスト配布の自動化 COTOHA Call Centerではモバイルアプリに処理を追加、変更する度に動作確認用のテストアプリを配布しています。 モバイルアプリはiOS版とAndroid版を開発しており、開発業務が立て込んでいる時には1日に両OSに対して5回ほどテストアプリの配布をする時もあり、稼動削減のために自動化を行なっています。 単純な稼働削減のみではなく、設定ファイル誤りなどのヒューマンエラー対策にもなっており、動作確認時の手戻りも減らすことができました。 iOS版とAndroid版について、それぞれどのようにテスト配布を実装したかを説明します。 iOS版のテスト配布 iOS版のテスト配布は開発ブランチへのプルリクエストがマージされた時に実行するよう実装しています。 テスト配布処理の実現には以下のものを使用しています。 自動処理の起動:GitHub Actions アプリビルド:Xcode Cloud アプリ配布:TestFlight 実際の処理の流れは以下の通りです。 開発ブランチへのプルリクエストのマージ GitHub Actionsのワークフロー開始(Xcode Cloud上で実行) ビルドしたいブランチのチェックアウト Ruby、Nodeのセットアップ 環境変数の読み込み アプリのバージョンコード編集 アプリのビルド ビルドしたファイルをTestFlightへアップロード TestFlightがテスターへアプリ配布 実装作業としてはワークフローのyml作成とXcode Cloud設定のみで、WEB上にも参考事例が多いため導入しやすかったです。 手動で行うと手間と時間がかかる処理を自動化できるので導入効果はとても高いと感じています。 1点だけ導入時に注意しなければいけないことがあります。Xcode Cloudには処理時間のプランがあります。例えば100時間のプランであれば、Xcode Cloudに処理を実行させられる時間は100時間のみとなります。複数のチームでApple Developer Programを共有している場合はあっという間に上限時間に達してしまうこともありえます。 導入する時にはあらかじめ Xcode Cloudのプラン と、他のチームがどれくらいXcode Cloudを使い込んでいるか確認すると良いでしょう。 Android版のテスト配布 Android版のテスト配布はiOS版と同様に、開発ブランチへのプルリクエストがマージされた時に実行するよう実装しています。 テスト配布処理の実現には以下のものを使用しています。 自動処理の起動:GitHub Actions アプリビルド:Self-hosted Runners アプリ配布:Firebase App Distribution 実際の処理の流れは以下の通りです。 開発ブランチへのプルリクエストのマージ GitHub Actionsのワークフロー開始(Self-hosted Runners上で実行) ビルドしたいブランチのチェックアウト JDK、Android SDK、Nodeのセットアップ アプリのバージョンコード編集 環境変数の読み込み アプリのビルド ビルドしたアプリをFirebase App Distributionへアップロード Firebase App Distributionがテスターへアプリ配布 実装作業としてはワークフローのyml作成のみでiOS版より一手間少なくて楽でした。こちらもWEB上に多くの参考事例があります。 iOS版同様に導入効果はとても高いです。 処理時間の上限については リポジトリの種類とプラン によって変わってくるので、こちらも導入時には上限の確認と他のチームの利用状況を確認した方が良いでしょう。 モバイルアプリのテスト自動化 また、COTOHA Call Centerではモバイルアプリのテスト自動化にも取り組んでいます。 COTOHA Call Centerでは以下の2つのテストを導入しています。 E2Eテスト(MagicPod) Roboテスト E2Eテストは導入に向けた検証が終わり、今後本格導入していく予定です。 Roboテストはすでに実装しており、リグレッションテスト時などに実行しています。 それぞれの導入の背景や役割について説明していきます。 E2Eテスト(MagicPod) COTOHA Call Centerでは、E2Eテストとして MagicPod を導入することになりました。 MagicPodとは、GUIでモバイルアプリのE2Eテストを作成できるサービスで、内部では Appium というオープンソースのテスト自動化フレームワークを使用しています。 Appiumをそのまま用いる場合はテストコードを書く必要がありますが、MagicPodはノーコードでテストを作成できます。 本格的に導入する前に、事前の検証をすることになり、基本的なシナリオを書きました。 テストシナリオは、以下の通りです。 コールセンターアプリとしての基本機能を網羅したものになっています。 初回起動&ログイン キーパッド画面で発信 通話後、メモを保存し、通話終了処理 発着信履歴の内容確認 ログアウト後の表示確認 シナリオを作成し実行した感想としては、GUI上で簡単に操作するだけでテストが実行できるため、使いやすく便利だと感じました。 実は途中までAppiumを用いたテストの自動化を試みたのですが、他の機能開発もある中でなかなかモバイルアプリのテストの実装に時間をかけることができないという課題がありました。 その点、MagicPodはGUI上で操作するだけでテストが簡単に作成できるので、テスト作成にかける工数が大幅に減少しました。 逆に、少し手間がかかる部分としては、iOSとAndroidを同じシナリオでテストするために、アプリの実装の修正が必要な点です。 MagicPodのFAQページ にも記載されているのですが、同じテストシナリオでiOS/Android両方をテストするためには、各UI要素に共通のaccessibility idを割り振っておくなどの修正が必要となります。 地味に手間がかかる部分ではあリますが、これはAppiumで実装した時も手間は同じなのでそこまで問題にはならないと思います。 また、コールセンターアプリ特有の問題として、保留や取次保留など、複数のユーザーを必要とするケースがあります。 こういったケースでは一方が保留した際にもう一方で表示が変更されるかなど、複数の端末を用いて各端末の状態を考慮したテストシナリオを作成する必要があります。 まだ本格的に利用していないのですが、このような場合はMagicPod上で完全に自動化するのは難しそうな印象です。 (もし方法があるのであれば、ぜひ教えていただけると嬉しいです) ですが、メリットが大きく、総じて便利なサービスなので、今後本格的に導入することを決めました。 今後はシナリオ数を増やし、将来的にはモバイルアプリのテストの自動化率を高めていこうと考えています。 Roboテスト COTOHA Call Centerでは、Androidのテストとして Roboテスト も導入しています。 Roboテストとは、Googleのデータセンターでホストされているデバイス上で行われる、UI構造を自動で分析してランダムに動くテストのことです。 COTOHA Call Centerでは想定していない操作方法をした場合の確認やクラッシュの確認など、リグレッションテストの補助に使用しています。 また、Roboテストでは「Roboスクリプト」というものを記述することで、特定の動作をテスト中に実行できます。 COTOHA Call Centerではログインをしないと通話などの機能が使用できないため、ログインを最初に行うといった制御をしています。 おわりに このような形で私たちは自動化に取り組み続けています。今後も新しい自動化の取り組みに挑戦する機会があればまたブログの記事にしたいと考えています。 それでは、明日の記事もお楽しみに!
この記事は、  NTT Communications Advent Calendar 2023  16日目の記事です。 こんにちは! クラウド & ネットワークサービス部の外村です。 普段は VxF 基盤 という 社内サービス用クラウドの開発・運用をしつつ、ソフトウェアエンジニア育成研修である twada 塾 の研修運営をしています。 今回は自己研鑽と業務効率化を目的として大規模言語モデル (以下、LLM) を用いたチャットボットの開発に挑戦しました。 LLM を用いたアプリケーション開発に興味がある方や、LLM の選択肢として Azure OpenAI Service を検討されている方へ参考になればと思います。 本記事では以下の技術を中心に取り扱います。 Azure OpenAI Service LangChain を用いた Function calling (非同期処理) の実装 開発のきっかけと開発したもの Azure OpenAI Service Azure OpenAI Service API API の利用 Azure OpenAI Service API におけるリージョンごとのレスポンス時間の比較 Function calling LangChain LangChain の Tool と Agent を用いた Function calling の実装 Function calling で実行させる関数の定義 Tool の作成 Fuction calling を実行できる Agent の作成 チャット用 API の作成 アプリケーションの起動 おわりに 開発のきっかけと開発したもの 私が担当する VxF 基盤は Flexible InterConnect (以下、FIC) に直結しているクラウドなのですが、 開発や試験をするなかで FIC 上に仮想ルータを作成したりコネクションを作成したりすることがしばしばあります。 こういった作業を楽にできたらなという思いでチャットボットを作成しました。 API を通じて必要な情報を教えてあげると、FIC に対して API を実行し、リソース作成してくれます。 LLM として Azure OpenAI Service の GPT-4、アプリケーションフレームワークとして FastAPI と LangChain を用いました。 アプリケーションの全体像は以下のようなイメージです。 アプリケーションの動作結果は以下のようになりました。 $ curl -X POST -H " Content-Type: application/json " -d ' {"request_message":"こんにちは"} ' http:// 127 . 0 . 0 . 1 /v1/chat { " response_message " : " こんにちは!何かお手伝いできることがありますか? " } $ curl -X POST -H " Content-Type: application/json " -d ' {"request_message":"FIC でルータを作成するために必要なことを教えてください。"} ' http:// 127 . 0 . 0 . 1 /v1/chat { " response_message " : " FICルータを作成するためには以下の情報が必要です: 1. **名前(name)**:ルータの名前を指定します。これは一意である必要があります。 2. **ユーザーIPアドレス(userIpAddress)**:ルータのユーザーIPアドレスを指定します。例えば、 \" 192.168.0.0/27 \" のような形式で指定します。 3. **エリア(area)**:ルータが配置されるエリアを指定します。 \" JPEAST \" または \" JPWEST \" から選択します。 4. **冗長化(redundant)**:ルータの冗長化を指定します。デフォルトはTrue(冗長化あり)です。 5. **パブリックサービスエンドポイント(isPublicServiceEndpoint)**:パブリックサービスエンドポイントを使用するかどうかを指定します。デフォルトはFalse(使用しない)です。 これらの情報を元に、FICルータの作成リクエストを送信します。ただし、ルータの作成は非同期で行われ、完了までに時間がかかることをご了承ください。 " } $ curl -X POST -H " Content-Type: application/json " -d ' {"request_message":"それでは FIC ルータを作成してください"} ' http:// 127 . 0 . 0 . 1 /v1/chat { " response_message " : " もちろんです。FICルータの作成に必要な情報を教えていただけますか? 1. ルータの名前 2. ユーザーのIPアドレス 3. エリア(JPEAST または JPWEST) 4. 冗長化の有無(デフォルトはTrue) 5. パブリックサービスエンドポイントの有無(デフォルトはFalse) " } $ curl -X POST -H " Content-Type: application/json " -d ' {"request_message":"はい、以下の情報で FIC のルータを作成してください。名前は tono-test-router, エリアはJPEAST、ユーザIPは192.168.0.0/27 でお願いします。"} ' http:// 127 . 0 . 0 . 1 /v1/chat { " response_message " : " FICルータの作成を開始しました。ルータのIDはF022300001179です。作成が完了するまで少々お待ちください。 " } 以降の章では、それぞれの技術要素の詳細や実装の内容について説明していきたいと思います。 Azure OpenAI Service Azure OpenAI Service は、Microsoft Azure 上で OpenAI が提供する LLM を利用できるサービスです。 ChatGPT でも用いられている GPT-4 や GPT-3.5 (以下、まとめて GPT と呼びます) を Azure 上でデプロイし、 Azure OpenAI Studio や OpenAI API を用いて利用できます。 Azure OpenAI Service には次のような特徴があります。 Azure が提供する他のサービスとの連携が容易 Azure が提供するセキュリティ機能を利用可能 複数のリージョンにデプロイすることが可能 ChatGPT で発表された機能は順次 Azure 側でも利用可能になり、Function calling 機能や Fine-Tuning も利用可能です。 機能ごとに利用できるモデルバージョンや API バージョンがあるため、公式ドキュメントをチェックしましょう。 また、2023年11月時点では利用するリージョンによってはデプロイ可能なモデルに差分があったり、 需要増にともない一部機能が制限されていました。 Azure OpenAI Service API Azure OpenAI Service を利用した実装にあたり、 API call を同期処理にするか非同期処理にするか少し検討しておきたいと思います。 API の利用 Azure OpenAI Service の API は、OpenAI 公式のモジュールを使うことで利用できます。下記のコードで実行可能です。 Azure ではない OpenAI API との違いは openai.api_type や openai.api_base 、 engine といった指定が必要なことでしょうか。 api_version も Azure で提供されたバージョンにする必要があります。 import openai openai.api_type = "azure" # プラットフォームの宣言 openai.api_base = "https://test-azure-openai-deployment01.openai.azure.com/" # 作成した Azure OpenAI Service のエンドポイント openai.api_version = "2023-07-01-preview" # API バージョン openai.api_key = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # api-key message_text = [ { "role" : "system" , "content" : "You are an AI assistant that helps people find information." , }, { "role" : "user" , "content" : "こんにちは、API とはなにか教えてください。" }, ] completion = openai.ChatCompletion.create( engine= "test-gpt35-turbo-deploy01" , # デプロイした GPT messages=message_text, temperature= 0 , max_tokens= 800 , top_p= 0.95 , frequency_penalty= 0 , presence_penalty= 0 , stop= None , ) print (completion[ "choices" ][ 0 ][ "message" ][ "content" ]) # 回答結果 Azure OpenAI Service API におけるリージョンごとのレスポンス時間の比較 上記のコードを用いて、各リージョンあるいは ChatGPT で提供されている GPT で API レスポンス時間に差があるのか 計測してみました。 比較対象として、Azure OpenAI Service 上の 3 リージョン(Japan East、US East、UK South) にデプロイした GPT と、 OpenAI で提供されている GPT を用いました。 全てのモデルは gpt-3.5-turbo (0613) を利用し、「こんにちは、API とはなにか教えてください。」というプロンプト (LLM に入力する文章) を5回なげたときのレスポンス時間 (秒) を表にまとめています。 Azure Japan East Azure US East Azure UK South OpenAI 1 回目 5.5 10.2 9.0 19.7 2 回目 4.1 4.3 9.9 18.1 3 回目 10.5 5.5 5.4 16.9 4 回目 5.3 3.7 4.8 13.0 5 回目 8.0 4.0 5.8 18.6 平均 6.7 5.5 7.0 17.3 上記の結果から以下のことが言えそうです。 どの GPT でも毎回の実行結果にばらつきがある Azure OpenAI Service のリージョン間ではおおきな差分はない Azure OpenAI Service では、どのリージョンでも 10秒程度レスポンスにかかることがある Azure OpenAI Service と ChatGPT では ChatGPT のほうがレスポンスに時間がかかっている 上記の結果から、GPT との通信期間中も他の処理が止まらないように、チャットボットは非同期処理を前提に開発する必要がありそうですね。 Function calling Function calling は OpenAI API で提供されている、 GPT を外部システムと連携させることができる機能です。 事前に実行可能な関数を実装しておき、GPT に「きみは必要に応じてこんな関数を実行できるよ」というように関数の情報を渡すことで GPT が必要に応じてその関数の実行判断をしてくれる機能です。 定義した関数をどのような引数を用いて実行すればよいかという判断も行ってくれます。Function calling を用いたアプリケーションのシーケンス図は以下のようになります。 Function calling は強力な機能ですが、GPT からのレスポンスに関数実行の指示がある場合は関数を実行して また GPT に結果を渡す... といった処理は開発者で書く必要があります。 しかし、LangChain を利用することでこの処理を書く必要はなくなります。詳しくは後述します。 LangChain LangChain は LLM を用いたアプリケーション開発を円滑に進めるための OSS ライブラリです。 ChatGPT の普及とともに勢いを伸ばしている OSS であり、GitHub での Issue 数は 2023年11月時点で 1.7K にも上ります。 Python に加え、最近では TypeScript でも実装されたようです。 LLM を用いたアプリケーションでは、多くの場合下記のような実装が必要になります。 LLM 単体では会話履歴を保持できないため、アプリケーション側で管理する必要がある LLM が未知な領域に対して、情報収集機能を持たせる必要がある等 LangChain はこのような実装をライブラリとして提供する OSS という理解をしておけば良さそうです。 LangChain の Tool と Agent を用いた Function calling の実装 Tool とは、Web 検索する関数、データベースを検索する関数、slack にメッセージを送る関数など、外部システムと連携するための関数です。 Agent とは、プロンプトに対して Tool をどの順番でどのように実行すればよいかを判断し実行してくれる仕組みです。 例えば 「ある作家の出版履歴を slack に通知してほしい。」というプロンプトを受け付けた場合、 Web 検索する Tool を実行し出版履歴を取得、その後 slack にメッセージを送る Tool を実行する、といった処理を実行してくれます。 LangChain では自作の関数を Tool として定義し、Agent に読み込ませることで Function calling を実現できます。 詳しく見ていきましょう。 以下は、アプリケーションで用いた Python とモジュールのバージョン情報です。 Python : 3.11.7 openai : 0.28.0 langchain : 0.0.348 fastapi : 0.104.1 httpx : 0.25.2 pydantic : 2.5.2 Azure OpenAI Service の GPT は以下の設定で用いています。 Reagion : Japan East Model : gpt4 API version : 2023-07-01-preview Function calling で実行させる関数の定義 ここでは FIC ルータ作成 API を一例にとり、Tool を作成します。 まずは FIC ルータ作成のための関数 createFicRouter を実装しましょう。 今回は非同期で動作させたいので、非同期実行をサポートする httpx を用いて作成しています。 また、返り値として FIC から返ってくるレスポンス (json) が返却されるようにします。 import httpx from fastapi import HTTPException import os async def createFicRouter ( name, userIpAddress, area, redundant= True , isPublicServiceEndpoint= False ): token = await getToken() # FIC 操作のための token 取得 (詳細は省略) url = "https://api.ntt.com/fic-eri/v1/routers" headers = { "X-Auth-Token" : token, "Content-Type" : "application/json" , } body = { "router" : { "name" : name, "area" : area, "redundant" : redundant, "userIpAddress" : userIpAddress, "isPublicServiceEndpoint" : isPublicServiceEndpoint, "tenantId" : os.environ[ "FIC_TENANT_ID" ], } } # POST API の実行 async with httpx.AsyncClient(timeout= None ) as client: r = await client.post(url, json=body, headers=headers) if r.status_code not in [ 202 ]: raise HTTPException( status_code= 503 , detail= "Could not available: CreateFicRouter" ) return r.json() Tool の作成 Tool のパラメータには以下の3つがあります。 Tool としての名前 関数の説明 関数の引数 関数の引数を定義するためには Pydantic を用います。 関数の引数に関して、LangChain では引数を 1 つまでに限定しているようでした。 そこで、関数実行に必要な 5 引数を args という 1引数のオブジェクトとして内包させる仕組みにしました。 from pydantic import BaseModel # 関数を実行するために必要な引数 class SchemaCreateRouter (BaseModel): name: str userIpAddress: str area: str redundant: bool isPublicServiceEndpoint: bool # schemaCreateRouter で定義された5つの引数を args という一つの引数のオブジェクトにする class SchemaArgsCreateRouter (BaseModel): args: SchemaCreateRouter 最後に Tool を作成します。 langchain.tools にある BaseTool を拡張することで作成可能です。 下記のコードのポイントは 3点です。 description には関数の情報を詳細に記載する。この記述をもとに GPT が適切なタイミング・引数で実行判断を行う 非同期処理させたい場合は async def _arun(): で関数を実行するように記載する args_schema には run または _arun を実行するために必要な引数を定義する from langchain.tools import BaseTool from pydantic import BaseModel from typing import Type # 作成した関数を Tool にする class ToolCreateFicRouter (BaseTool): name = "createFicRouter" description = """ FIC に対して Fic-Router を作成する。 入力: name, userIpAddress (例: 192.168.0.0/27), area (JPEAST または JPWEST), redundant(Default: True), isPublicServiceEndpoint (Default: False) 備考: 非同期作成のため、作成完了までに時間がかかる。 """ args_schema: Type[BaseModel] = SchemaArgsCreateRouter # 同期処理 def _run (self, args): raise NotImplementedError ( "CreateFicRouter does not support sync" ) # 非同期処理 async def _arun (self, args): r = await createFicRouter(**args) return r 以上で Tool が完成しました。 Fuction calling を実行できる Agent の作成 上記の Tool を agent 初期化時に引数として渡すことで、 Agent が Function calling を実行できるようになります。 今回は Agent を非同期で動作させたいので、 initialize_agent の引数 async_mode に True を指定しましょう。 from langchain.agents import initialize_agent from langchain.agents import AgentType from langchain.chat_models import AzureChatOpenAI import os class ChatAgent : def __init__ (self): # LLM の定義 llm = AzureChatOpenAI( deployment_name=os.environ[ "DEPLOYMENT_NAME" ], # デプロイ名 model_name=os.environ[ "MODEL_NAME" ], # gpt4 temperature= 0 , max_tokens= 2000 , ) # Agent の定義 self.agent = initialize_agent( [ToolCreateFicRouter()], # 自作した Tool を配列に含める。Agent はこのなかで定義された Tool を使ってプロンプトに回答する llm, agent=AgentType.OPENAI_FUNCTIONS, verbose= True , return_intermediate_steps= False , async_mode= True , ) # Agent 実行用の関数定義 async def chat (self, message): r = await self.agent.arun({ "input" : message}) return r チャット用 API の作成 ここまで来れば後もう一歩です。 main.py という名前でファイルを作成し、 ChatAgent class を用いて回答を生成する API を定義してコードは完成です。 from fastapi import FastAPI from pydantic import BaseModel # リクエストボディのスキーマを定義 class PostChatRequestBody (BaseModel): request_message: str # レスポンスボディのスキーマを定義 class PostChatResponseBody (BaseModel): response_message: str app = FastAPI() # POST: /v1/chat で API を受け付ける @ app.post ( "/v1/chat" ) async def create_chat (body: PostChatRequestBody): # ChatAgent class のインスタンス化 chat_agent = ChatAgent() # chat() を実行 response_message = await chat_agent.chat(body.request_message) # レスポンスボディの返却 return PostChatResponseBody(response_message=response_message) アプリケーションの起動 環境変数を設定して FastAPI のサーバを起動します。 実行に必要な環境変数は以下のとおりです。 記載したコードのなかで現れたものとそうでないものがありますが、LangChain が自動で読み込むものもあります。 すべて設定してから FastAPI を起動しましょう。 # Azure OpenAI Service で OpenAI API を扱うために必要な環境変数 export OPENAI_API_TYPE = " azure " # プラットフォームの宣言 export OPENAI_API_VERSION = " 2023-07-01-preview " # Function calling は 2023-07-01-preview から利用可能 export OPENAI_API_BASE = " https://test-azure-openai-deployment01.openai.azure.com/ " # 作成した Azure OpenAI Service のエンドポイント export OPENAI_API_KEY = " xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx " # API Key export DEPLOYMENT_NAME = " test-gpt-4-turbo01 " # デプロイ名 export MODEL_NAME = " gpt4 " # モデル名 # 以下、FIC API を扱うために必要な環境変数 export FIC_TENANT_ID = " xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx " ︙ # サーバの起動 uvicorn main:app --reload おわりに 今回は Azure OpenAI Service を用いてチャットボットを作成しました。 チャットボットが必要に応じて外部 API を利用できるように、LangChain による Function calling も実装しました。 今回は Function calling に焦点を当てましたが、会話管理やアプリケーションのテストなどについても、また別の機会に取り組んでいきたいと思います。 それでは、明日の記事もお楽しみに!
この記事は NTTコミュニケーションズ Advent Calendar 2023 の15日目の記事です。 この記事では、ChatGPT と 音声認識モデルの Whisper を用いた発音練習アプリケーションをご紹介します。 ChatGPT に読み上げる文章を考えてもらい、その文章の読み上げた音声を Whisper で文字起こしします。 正確に発音できていれば、正確に文字起こしできる、という考えから、 原稿と文字起こし結果を比較すれば発音練習に使えるのではないかと考えました。 実際に使ってみた結果、発音のどこが悪かったのかといったフィードバックはもらえませんが、 自分の発話した音声に対して評価がつくだけでも、結構楽しく練習できると感じました。 音声認識を活用したアプリケーションは、一般に音声認識精度がネックになると思いますが、 このアプリケーションは音声認識精度が100%ではないことを逆手にとっている点と ChatGPT に比べると影の薄い Whisper くんの長所である多言語対応を活用できている点がユニークです。 本アプリケーションは Streamlit を用いて比較的簡単に作成できました。 Streamlit で音声データを扱うアプリケーションは文献が少ないように感じるので、 本記事が似たようなことをやりたい方の参考になれば幸いです。 目次 目次 はじめに 発音練習アプリの使い方 読み上げ原稿の用意 読み上げ文ごとに音声を録音する 結果を確認する 実装に使ったライブラリ・フレームワークの紹介 Streamlit streamlit-webrtc Whisper 振り返り Streamlit を使ってみた感想 音声認識を活用したアプリケーションの作成 今後やりたいこと 読み上げ原稿のリアルタイム生成 発音記号の実装 正誤判定処理の見直し アプリケーションの主要な構成要素の解説 Questionクラス 読み上げ原稿の表示 音声の録音 音声認識 結果表示 おわりに はじめに こんにちは、ソリューションサービス部の是松です。 普段はデータ利活用ソリューションに向けた、技術検証やデータ分析のPoCを行っています。 生成AIをはじめとする革新的な技術が次々と世に出てくる昨今、それらの新しい技術を使ってどんなことができるのかを手軽に試したいですよね。 Streamlit は、簡単に UI を含むアプリケーションを作成できるため、技術検証やデモをスピーディに行うのに便利です。 Streamlit は 3rd-party ライブラリがいくつも作られているのも特長で、 streamlit-webrtc というライブラリを使うことで、映像・音声も手軽に処理できます。 今回は (1) ChatGPT を使って読み上げ原稿を作成、 (2) streamlit-webrtc を使って音声を録音、(3) Whipser を使って文字起こし、という手順をとっています。 音声をあつかうアプリケーションは検索してもあまり情報がなく、はまりどころも多いので、本記事が音声を扱ったアプリケーションをつくりたい方の参考になれば幸いです。 発音練習アプリの使い方 アプリケーション全体の処理の流れは以下の通りです。 読み上げ原稿の用意 まず、自分が練習したい文章を ChatGPT に生成してもらいます。 今回は英語で比較的簡単な文章の練習をすることにします。 { " scripts ": [ " The quick brown fox jumps over the lazy dog. ", " A kind smile can make someone's day better. ", " Every morning, I enjoy a cup of green tea. ", " The library is quiet and perfect for studying. ", " On Sundays, we often go to the park. ", " She plays the piano beautifully and gracefully. ", " The cat sat on the warm windowsill. ", " Learning a new language is both challenging and rewarding. ", " He jogs around the lake every evening. ", " Birds sing cheerfully in the early morning. " ] } ...私にとっては結構難しそうな文章が出てきたので、今回はもっと簡単なもので試すことにします。 { " scripts ": [ " The cat sat on the mat. ", " I like to eat apples and bananas. ", " My friend is very kind. " ] } このjsonファイルを 所定の場所 に置いておきます。 読み上げ文ごとに音声を録音する アプリケーションを起動すると、以下のような画面が表示されます。 赤い Start ボタンを押すと、音声録音が始まります。 もう一度ボタンを押すと録音が止まり、録音した音声を確認できます。 音声の確認が済んだら、右上の Next ボタンを押して次の原稿に移ります。 これを繰り返します。 結果を確認する 全ての原稿の録音が終わると、結果画面が表示されます。 読み上げ原稿と、録音の文字起こしが完全に一致した場合を正解として、 正解した問題数が得点になります。 音声認識処理はバックグラウンドで動いているので、まだ処理が完了せず画面に反映されていない場合は、更新ボタンを押して画面を更新します。 このように、実際に自分の発話を聞いて音声認識モデルにどのように認識されるのかを確認することで、 自分一人でのスピーキングの反復練習に活用できます。 実装に使ったライブラリ・フレームワークの紹介 Streamlit Streamlit は、Python を使って簡単にWebアプリケーションを作成できるフレームワークです。 以下のような特長を持ちます。 シンプルで便利なウィジェットを組みあわせることで、少ない行数でアプリケーションを構成できる Python が得意なデータ処理や機械学習モデルとの組み合わせが容易にできる 3rd-party モジュールが多数公開されており、さまざまな用途に対応できる 詳しくは、 公式ドキュメント をご参照ください。 streamlit-webrtc streamlit-webrtc は、Streamlit 上でリアルタイム映像・音声信号処理ができる 3rd-party ライブラリです。 色々なユースケースのサンプルコードも公開されており、映像・音声を使ったアプリケーションの検証をするのに便利です。 WebRTC の特性上、streamlit-webrct ではクライアントとサーバ間で映像・音声ストリームのやり取りをリアルタイムに行います。 そのため、頻繁に画面が更新される Streamlit と相性が良くなかったり 1 、ネットワーク上の問題で正常に動作しなかったりと、使いこなす難易度は高いと感じました。 今回は以下のページを参考に実装しています。 streamlit-stt-app 簡単に原稿を読み上げて録音できるウェブアプリケーションをStreamlitで作った Whisper OpenAI の多言語音声認識モデルです。 ソースコード は公開されており、誰でも自由に試すことができます。 APIサービス も提供されています。(Large モデルのみ) 昨年のアドベントカレンダー企画で書いた記事 にて、Whisper を紹介していますので、よろしければご参照ください。 先月の OpenAI DevDay にて、large-v3 モデルの発表もありましたが、ChatGPT の話題性に比べて、あまり盛り上がっていないように感じられて悲しいです。 振り返り Streamlit を使ってみた感想 今回、フレームワークとして Streamlit を採用しましたが、プロトタイプアプリケーションを作成するのに、非常に有用だと感じました。 特に Whisper で音声認識をする処理を Python でそのまま書けるため、手元で Whisper を試すのと同じ感覚で、アプリケーション開発への組み込みができました。 音声録音処理は、はまりどころが多くて苦労したのですが、UIを全く意識することなく処理を実装できました。 課題としては、UI はあらかじめパーツが決まっており、自分でカスタマイズができないこと。 コードは短くシンプルに書けるが、裏側でどのように処理が行われているのかが見えづらいため、デバッグが難しいこと。 あくまでプロトタイプやデモアプリケーション向けのフレームワークであると感じました。 音声認識を活用したアプリケーションの作成 Whisper を使って誰でも手軽に高精度の音声認識モデルを触れるようになりましたが、 なかなか活用するのは難しいと感じていました。 Whisper はモデルタイプが複数あり、large モデルの性能が一番良いのですが、 その分、マシンスペックや実行時間が重くなってしまいます。 また、音声認識精度が高いとはいえ、ミスはどうしても発生してしまいます。 音声認識結果を活用しようとしても、認識ミスのせいで満足のいくクオリティのものが作れないという悩みを持つ人は多いのではないでしょうか? 今回作った発音練習アプリケーションは、音声認識に誤りがあることを逆に利用して、正しく音声認識させることを目的にしている点が面白いと思っています。 Whisper の軽量モデルは、large モデルと比較すると性能が落ちてしまうのですが、今回のアプリではそれが難易度上昇に繋がっていて、ポジティブにも捉えられると思います。 また、Whisper はさまざまな言語に対応しており言語推定機能を備えているため、読み上げ原稿さえ用意すれば、好きな言語で発音練習ができるはずです。 まだまだ機能としては荒削りですが、個人的に使いたいと思えるアプリケーションを作ることができました。 今後やりたいこと 読み上げ原稿のリアルタイム生成 アプリケーション上で ChatGPT への問い合わせを行なって、毎回ランダムな問題が表示できるようにしてみたいです。 API 料金がかかるので、リアルタイム生成結果はファイル保存した上で、リアルタイム生成かファイル参照かを選べるようにすると良いかなと思います。 発音記号の実装 JSON のフォーマットを変更して、発音記号などで読み方も表示されるようにしたいです。 正誤判定処理の見直し 現状は完全一致を正解としているので、もう少しゆるい正誤判定ルールに変更したいです。 アプリケーションの主要な構成要素の解説 ここからは、実際のアプリケーションのコードをかいつまんで解説します。 Questionクラス まず、読み上げる文章ごとに、録音音声や文字起こしテキストをまとめて管理するためのクラスを用意します。 import hashlib from dataclasses import dataclass from pathlib import Path @ dataclass class Question : script_index: int script: str transcript: str wav_dir_path: Path @ property def file_id (self): return hashlib.md5((self.script + str (self.script_index)).encode()).hexdigest() @ property def output_wav_name (self): return f "{self.file_id}.wav" @ property def wav_file_path (self): return self.wav_dir_path / self.output_wav_name @ property def record_info (self): return { "script" : self.script, "file_name" : self.output_wav_name} script は読み上げ文、 transcript は録音の文字起こし、 wav_dir_path は録音ファイルの保存先の情報を格納します。 ファイル名はハッシュ処理をしており、プロパティとして参照できます。 読み上げ原稿の表示 ChatGPT に作成してもらった読み上げ原稿情報を、 scipts/en.json に格納しておきます。 それを以下のようにして、アプリケーションで表示させています。 import streamlit as st from pathlib import Path from question import Question # 録音ファイルの保存先の設定 RECORD_DIR = Path( "./records" ) RECORD_DIR.mkdir(exist_ok= True ) # セッション状態の管理 if 'current_question_index' not in st.session_state: st.session_state[ 'current_question_index' ] = 0 # 問題文を読み込む script_file_path = Path( 'scripts/en.json' ) scripts = load_scripts(script_file_path) # 問題リストの初期化 if 'questions' not in st.session_state: st.session_state[ 'questions' ] = [ Question(script_index=i, script=script, transcript= "" , wav_dir_path=RECORD_DIR) for i, script in enumerate (scripts) ] # 現在の問題を取得 question = Question( script_index = st.session_state[ "current_question_index" ], script = scripts[st.session_state[ 'current_question_index' ]], transcript = "" , wav_dir_path = RECORD_DIR, ) # 読み上げ文を表示 st.markdown(f "# {question.script}" ) Streamlit では、画面に何かしらの変更があるたびに、コードが全て再実行される仕様となっているため、 通常の変数だと画面の更新のたびに初期化されてしまいます。 streamlit.session_state を使用することで、セッションを通して変数を保持できます。 上のコードでは current_quesiton_index と questions という2つの変数を、セッションを通して保持しています。 current_quesiton_index は、「現在表示している問題が何問目か」を管理する変数で、 questions は、読み上げ分の数だけ Question クラスオブジェクトを保持する変数です。 ファイルから読み上げ原稿を読み込んだ後、 questions を初期化します。 この時点では、文字起こしは存在しないため、 transcript の中身は空にしておきます。 最後に、 st.markdown() を使って、問題文を表示します。 音声の録音 録音用の WebRTCRecord クラスを定義します。 import queue import pydub import streamlit as st from streamlit_webrtc import WebRtcMode, webrtc_streamer class WebRTCRecord : def __init__ (self): self.webrtc_ctx = webrtc_streamer( key= "sendonly-audio" , mode=WebRtcMode.SENDONLY, audio_receiver_size= 256 , rtc_configuration={ "iceServers" : [{ "urls" : [ "stun:stun.l.google.com:19302" ]}]}, media_stream_constraints={ "audio" : True , }, ) if "audio_buffer" not in st.session_state: st.session_state[ "audio_buffer" ] = pydub.AudioSegment.empty() def recording (self, question): status_box = st.empty() while True : if self.webrtc_ctx.audio_receiver: try : audio_frames = self.webrtc_ctx.audio_receiver.get_frames(timeout= 1 ) except queue.Empty: status_box.warning( "No frame arrived." ) continue status_box.info( "Now Recording..." ) sound_chunk = pydub.AudioSegment.empty() for audio_frame in audio_frames: sound = pydub.AudioSegment( data=audio_frame.to_ndarray().tobytes(), sample_width=audio_frame.format.bytes, frame_rate=audio_frame.sample_rate, channels= len (audio_frame.layout.channels), ) sound_chunk += sound if len (sound_chunk) > 0 : st.session_state[ "audio_buffer" ] += sound_chunk else : break audio_buffer = st.session_state[ "audio_buffer" ] if not self.webrtc_ctx.state.playing and len (audio_buffer) > 0 : status_box.success( "Finish Recording" ) try : audio_buffer.export( str (question.wav_file_path), format = "wav" ) except BaseException : st.error( "Error while Writing wav to disk" ) # Reset st.session_state[ "audio_buffer" ] = pydub.AudioSegment.empty() 初期化処理で、 webrtc_streamer の設定します。 クライアントからサーバーへの送信のみを行うモード(サーバからクライアントへの送信は行わない)で、映像なし音声ありの設定です。 key は単なる識別子なのでなんでも良いです。 session_state で audio_buffer 変数を保持して、音声バイナリデータを溜めていきます。 recording 関数の中の、While 句の中身は、ユーザが録音開始ボタンを押してから停止ボタンを押すまでの間に実行されるループ処理です。 録音している音声バイナリを、 audio_buffer 内に蓄積させる処理が行われています。 停止ボタンが押されたら、 audio_buffer の中身を、ファイルに書き出して処理を終了します。 このコードからわかるように、録音中は音声データを蓄積し続けるので 停止をしないと、そのうちバッファが溢れアプリケーションが落ちます。 音声認識 import threading import whisper import streamlit as st def format_string (s): s = s.replace( ',' , '' ) s = s.replace( '.' , '' ) s = s.strip() return s def transcribe (file_path, model): result = model.transcribe( str (file_path)) return format_string(result[ "text" ]) def async_transcribe (question, model): transcript = transcribe(question.wav_file_path, model) question.transcript = transcript def start_transcription_thread (question): model = st.session_state[ "ASR_MODEL" ] x = threading.Thread(target=async_transcribe, args=(question, model)) x.start() # 次の問題へ if st.button( "Next >" ) and question.wav_file_path.exists(): # 音声ファイルがない場次へ行い current_question = st.session_state[ 'questions' ][st.session_s[ 'current_question_index' ]] # トランスクリプションをバックグラウンドで開始 start_transcription_thread(current_question) # 次の問題へ移動 st.session_state[ "current_question_index" ] += 1 音声認識処理は実行に時間がかかるため、別スレッドで実行しています。 音声認識結果は、 question.transcript に格納され、結果表示画面で参照されます。 また、音声認識処理開始と合わせて、 current_question_index 変数をインクリメントすることで、次の問題が表示されます。 結果表示 # 結果表示画面 if st.session_state[ 'current_question_index' ] >= len (scripts): score = 0 st.title( "結果発表" ) for question in st.session_state[ 'questions' ]: if question.script == question.transcript: score += 1 if question.transcript: st.markdown(f "### 問題 {question.script_index}" ) st.write(f "原稿:{question.script}" ) st.write(f "結果:{question.transcript}" ) else : st.markdown(f "### 問題 {question.script_index}" ) st.write(f "原稿:{question.script}" ) st.write(f "結果:処理中..." ) st.markdown(f "# あなたのスコアは... {score} 点!" ) # スレッドが完了した後にボタンを表示 if st.button( "画面を更新" ): st.rerun() 最後の問題が終わったら、結果表示画面に移ります。 questions に保持している情報を展開していくだけですが、音声認識が終わっていない場合は transcript 変数の中身は初期化時の空白のままなので、処理中.... と表示されます。 streamlit.rerun() を実行すると、強制的に画面の更新ができ、もし更新時に音声認識が終わっていれば、結果が正しく表示されます。 おわりに 本記事では、Streamlit を用いた発音練習アプリケーションを紹介しました。 音声を使ったアプリケーション作成の参考になれば幸いです。 それでは、明日の記事もお楽しみに! Streamlit自体は頻繁に画面更新が発生することを想定した書き方をするのに対して、WebRTCの部分は画面更新してもセッションが保持されるため、アプリケーションの挙動が理解しづらいように思います。streamlit-webrtc の実装も大変そうです。( 参考 ) ↩
この記事は NTTコミュニケーションズ Advent Calendar 2023 の14日目の記事です。 こんにちは、イノベーションセンター所属の志村です。 Metemcyberプロジェクトで脅威インテリジェンスに関する内製開発や、Network Analytics for Security (以下、NA4Sec)プロジェクトで攻撃インフラの解明・撲滅に関する技術開発を担当しています。 ソフトウェア開発プロセスにおけるセキュリティに関心が高まりつつあり、サプライチェーンセキュリティという言葉も広く使われるようになってきました。 またMetemcyberプロジェクトでは SBOMに関する取り組み を行っていますが、SBOMもサプライチェーンセキュリティの分野での活用が期待されている概念となります。 そこで本記事ではサプライチェーンセキュリティとはそもそも何か、具体的にどのような対策が存在するのかについて紹介し、サプライチェーンセキュリティへの取り組み方について考察します。 目次 目次 本記事で扱う「サプライチェーン」について なぜサプライチェーンセキュリティが重要なのか サプライチェーンセキュリティのフレームワーク SLSA S2C2F CIS Software Supply Chain Security Guide サプライチェーンセキュリティの具体的な対策 依存関係に関する対策 ビルドプロセスに関する対策 ビルド成果物に関する対策 サプライチェーンセキュリティの導入の難しさ セキュリティ担当の立場から 開発者の立場から 最後に 宣伝 本記事で扱う「サプライチェーン」について 「サプライチェーンセキュリティ」は、「サプライチェーン」に対する攻撃への対策を意味します。 ではサプライチェーンに対する攻撃とはどのようなものなのかと言いますと、現時点では複数の意味で使われているようです 1 。 標的とする組織の取引先の企業などに攻撃し、そこを踏み台として標的に侵入する攻撃手法 ここでのサプライチェーンは「下請け・取引先を含めた企業などのつながり」を意味する Island Hopping 攻撃とも呼ばれる ソフトウェア開発の一連のプロセス (開発者・ソースコードリポジトリ、ビルド環境、デプロイ環境など) に対する攻撃 ソフトウェアの開発から提供までの一連のプロセス (CICDパイプライン) をサプライチェーンと呼んでいる 本記事では2番の意味のサプライチェーン、すなわちソフトウェア開発のプロセスをどのように保護するか、という観点を取り扱います。 ソフトウェア開発のプロセスに関与する要素としては以下のようなものが挙げられます。 開発者 ソースコード、およびソースコードを管理するSCM (gitなど) ソフトウェアのビルド・テストに用いる環境・ソフトウェア 外部の依存コンポーネント ビルド成果物 上記のようなソフトウェアを開発に関与する要素への攻撃がサプライチェーン攻撃であり、それに対する防御手法がサプライチェーンセキュリティといえます。 なぜサプライチェーンセキュリティが重要なのか 近年、ソフトウェア開発プロセスの高度化に伴い、開発プロセスを構成する要素が増加しています。 つまり攻撃を受けやすい状態に変化してきていることから、その結果としてサプライチェーンセキュリティへの関心が高まっています。 以降でこのことについて詳しく説明します。 近年のソフトウェア開発では、OSSをはじめとする外部ソフトウェアをコンポーネントとして組み合わせる開発方式が一般的となりました。 またソフトウェアのテスト・ビルド・デプロイにおいても、GitHub Actionsなどのサービスインフラを利用して自動化したプロセスを構築し、その上でサードパーティーのツール (テストカバレッジの可視化など)を利用することが増えています。 このような技術によって、ソフトウェア開発の高速化・高機能化が進んできました。 一方で利用・依存するプラットフォームやソフトウェアが増えるということは、攻撃可能な対象が増えることを意味します。 そのうち1つでも侵害できれば、ソースコードやクレデンシャルを取得したり、ソフトウェアに悪意ある挙動を挿入できる可能性があるのです。 代表的な攻撃事例として、2020年に発生したSolarWinds社に対する攻撃があります。 SolarWinds社は自社のビルドプラットフォームを侵害されたことにより、製品にバックドアを仕込まれてしまいました。 これによりSolarWinds社の製品を利用していた組織がバックドア経由で侵入される被害を受けました。 また2021年には、著名なコードカバレッジ測定ツールCodecovのbashスクリプトが改竄された結果、Codecov利用者のソースコードなどの情報が流出するという事件も発覚しています。 このように、開発プロセスに対する攻撃が成功してしまうと、システムへの侵入や情報漏えいといった重大な問題が発生するというリスクがあります。 サプライチェーンセキュリティの実践により、ソフトウェア開発プロセスの各要素に対する攻撃を防いだり、攻撃を受けても被害範囲を限定するといったことが可能になるので、ソフトウェア開発のリスクを低減できます。 上記のような理由のため、サプライチェーンセキュリティが重要であるとして関心度が高まっています。 サプライチェーンセキュリティのフレームワーク サプライチェーンセキュリティのために、どのような対策を採用すれば良いでしょうか。 サプライチェーンセキュリティをどのように実現すべきかという方法論をまとめたフレームワークがいくつか存在しています。 SLSA OSS開発者に向けたフレームワーク S2C2F OSSを安全に利用することを目的としたフレームワーク CIS Software Supply Chain Security Guide 企業内部での開発に焦点を当てたガイド 各フレームワークの概要について簡単に紹介します。 SLSA SLSA(Supply-chain Levels for Software Artifacts) はLinux FoundationのプロジェクトであるOpenSSFが開発・公開しているフレームワークです。 SLSAはオープンソースをどのようにセキュアに開発・提供するかという内容が中心となっています。 SLSAは2023年12月現在バージョン1.0 がリリースされています。 SLSAでは、サプライチェーンの要素を以下のように定義しています。 開発者 コードリポジトリ (GitHub, GitLabなど) ビルドプロセス Dependencies (外部の依存コンポーネント) 成果物のパッケージ ソフトウェアの消費者 サプライチェーンの各要素と、要素間をつなぐ経路の間に攻撃が存在します。 ( 出典: https://slsa.dev/spec/v1.0/threats-overview ) SLSAはサプライチェーンにおいて満たすべき要件をレベルごとに定義しています。 SLSAの バージョン0.1 の段階ではソースコードの安全性などの幅広い分野の要件を定めていましたが、2023年4月のバージョン1.0のリリースにおいてはソフトウェアのビルド環境や来歴 (ビルド成果物がどのように生成されたかの証跡) の作成に焦点を当てたフレームワークとなっています。 そのためサプライチェーン全体の保護の参考にしたい場合、バージョン0.1を参考にした方が良いかもしれません。 S2C2F S2C2F(Secure Supply Chain Consumption Framework) はSLSAと対照的に、開発者がOSSを安全に使う方法を取り扱ったフレームワークです。 SLSAと同様に、セキュリティレベルごとに満たすべき要件を定めており、Level1 ではパッケージマネージャーを利用することや、既知の脆弱性をスキャンして発見することなどが含まれています。 ( 出典: https://www.microsoft.com/en-us/security/blog/2022/11/16/microsoft-contributes-s2c2f-to-openssf-to-improve-supply-chain-security/ ) CIS Software Supply Chain Security Guide CIS Controls などで知られる、インターネットセキュリティ標準化を目的とした団体であるCISが、Trivyなどで知られるAqua Security と共同で作成したガイドラインです。 公式サイト からPDFをダウンロードできます。 CIS Software Supply Chain Guideで想定しているソフトウェア開発サイクルはSLSAとほぼ同一です。 このガイドラインにはサプライチェーンを保護する100以上の推奨事項が含まれていて、以下のカテゴリに分類されています。 Source Code Build Pipelines Dependencies Artifacts Deployments CIS Software Supply Chain Guideでは、プラットフォームなどの詳細には踏み込まずに、一般的なガイドラインとして作成されています。 その手法をプラットフォームの詳細を踏まえて具体的な内容に落とし込んだ個別のガイダンス ( CIS GitHub Benchmark など) が別途存在しています。 サプライチェーンセキュリティの具体的な対策 各フレームワークでは共通している対策が多くあります。いくつか代表的なものをピックアップして紹介いたします。 依存関係に関する対策 OSSなどの外部コンポーネントへの依存を適切に管理し、脆弱性を早期に発見・対処することが重要になります。 主な対策としては以下のようなものがあります。 ソフトウェアに組み込む依存関係は、パッケージマネージャーなどを利用して明示的に依存する ソースコードをコピーするなど、暗黙的な依存を発生させない 利用するバージョンを固定することで、意図しないバージョンへの依存が発生することを防ぐ バージョンを固定しないとビルドした時期によって依存バージョンが変わり、脆弱性の判断などが困難になる SBOMなどを利用して依存関係の把握と脆弱性の追跡する SBOMは含まれるソフトウェア部品の一覧を可視化したリストで、脆弱性対応やライセンス管理などの文脈での活用が進められています。 パッケージマネージャーなどを利用して明示的に依存関係を管理することは、SBOMの作成という観点においても重要なプラクティスとなります。 SBOMに関しては 昨年のアドベントカレンダーの記事 もご覧いただければと思います。 ビルドプロセスに関する対策 ビルドプロセスへの攻撃は、ソフトウェアに意図しない挙動を挿入されたり、認証情報を盗まれるなどの被害を招きます。 具体的な対策は以下のようなものがあります。 ビルドは自動化されたスクリプトで行う ビルド環境が複数回利用されないようにする 使い捨てのコンテナやVMを活用してビルドする テスト・デプロイなどの工程ごとにCICDプロセスを分け、各フェーズでは最小権限の認証情報を付与する 他のフェーズに攻撃の及ぶことを防ぐ 侵害された場合の認証情報の流出を最小限にする 依存するソフトウェアの信頼性を確認する GitHub Actionsの場合、そのソフトウェアの信頼性を確認する (よく似た名前の偽パッケージでないか、利用者数など)。可能であればActionのコミットハッシュを指定し、固定されたバージョンが利用されるようにする ( 参考 ) ビルド成果物に関する対策 生成したソフトウェアやDockerイメージなどのビルド成果物はパッケージレジストリに保存され、そこから環境にデプロイされることが一般的です。 保存されたビルド成果物を改ざんなどの脅威から守ることが重要となります。 パッケージレジストリにアクセスできるユーザを制限し、多要素認証などを導入する ビルド成果物に対して署名し、改ざんを検知できるようにする ソフトウェアに署名・検証を用意にする Sigstore プロジェクトの Cosign などを活用する サプライチェーンセキュリティの導入の難しさ ここまで各フレームワークの概要と、共通する要素について紹介してきました。 具体的に導入すべき対策については、各フレームワークが規定している内容を参考にできます。 一方でサプライチェーンセキュリティの対策を組織に導入する際には、セキュリティとソフトウェア開発それぞれの立場から実施すべき内容があるため、通常のセキュリティ対策とは異なるアプローチが必要になります。 サプライチェーンセキュリティにどのように取り組むべきか、という観点についてセキュリティ担当者と開発者のそれぞれ立場について考察してみます。 セキュリティ担当の立場から 大前提として、サプライチェーンセキュリティの手法はソフトウェア開発の手法の上に成り立っています。 そのためサプライチェーンセキュリティの推進はセキュリティの知識だけでは完結せず、ソフトウェア開発の知識も必要となります。 組織のソフトウェア開発のプロセスを理解したうえで、フレームワークで推奨されているセキュリティ対策をどのように既存のプロセスに組み込むかを決定したり、必要に応じて開発プロセス自体の見直しを進めていくことになるでしょう。 またサプライチェーンセキュリティの対策は、依存ソフトウェアの選定やCICDプロセスの構築など、現場のソフトウェア開発者が意思決定の主体となるものが数多くあります。 そのため、現場のソフトウェア開発者に向けて、ソフトウェア観点でのセキュリティ知識向上のトレーニング実施なども重要です。 開発者の立場から サプライチェーンセキュリティの実現には開発者の貢献が必要です。 ただサプライチェーンセキュリティを構成する要素は広範囲に及ぶため、全ての要素に対する脅威とその対策を把握するのは困難でしょう。 セキュリティ専門家のサポートを受けることが望ましいですが、開発の初期段階から十分な支援を受けることが難しい場合も多いでしょう。 そのため開発の初期段階で意思決定され、後の変更が難しい要素については開発者自身が一定の知識を有することが望ましいと考えられます。 例えば使用する言語・フレームワークなどは開発の初期段階で意思決定が行われ、変更するには多大なコストがかかります。 これらはセキュリティも念頭においた意思決定をしておくと将来的のリスクを軽減できます。 言語の選定 可能であればパッケージマネージャーが利用可能で、依存関係を明示できる言語を選定する 依存ソフトウェアの選定 開発コミュニティは機能しているか、定期的な更新や脆弱性の修正がされているか また依存しているサービスやプラットフォームがセキュリティのガイドラインを発行していることも多いです。 このようなガイドラインを参考にして設定することも重要になります。 GitHub Securing your software supply chain GitLab Secure your application 最後に サプライチェーンセキュリティの概要やフレームワークを紹介し、サプライチェーンセキュリティへの取り組み方について考察しました。 サプライチェーンセキュリティはソフトウェア開発のプラクティスと相互に影響して成立するため、ソフトウェア開発の進化に伴い対策の内容も変化していくことが予想されます。 最新の動向は各種フレームワークなどを参考にしていただければと思います。 宣伝 最後に宣伝ですが、Metemcyberでは今回紹介したSBOMに関する取り組みや、NA4Secと連携した情報配信を行なっております。興味ある方は公式アカウント (@Metemcyber) をフォローしていただけると幸いです。 またMetemcyber/ NA4Sec プロジェクトでは一緒に働く仲間を募集しています。 興味を持たれた方はぜひ応募してみてください。 NA4Sec (Threat Intelligence Analyst / 脅威インテリジェンスアナリスト) Metemcyber (Threat Intelligence Engineer / 脅威インテリジェンスエンジニア) なぜ、SSL-VPN製品の脆弱性は放置されるのか ~“サプライチェーン”攻撃という言葉の陰で見過ごされている攻撃原因について~ ↩
この記事は NTTコミュニケーションズ Advent Calendar 2023 の13日目の記事です。 こんにちは、イノベーションセンターの坂本です。 ソフトウェアエンジニアとしてノーコードAI開発ツール Node-AI の開発に取り組んでいます。 機械学習やその前処理などの計算にかかる時間はデータサイズや処理内容により大きく異なります。そのため機械学習やデータ分析に関するアプリケーションでは、冪等でない処理をイベント駆動型アーキテクチャ(EDA)で扱う難しさがあります。 今回は上記の課題とその解決策として採用している専用ゲームサーバ(DGS)用OSS Agonesを利用したEDAのWorkerについて紹介します。 Node-AIとは Node-AIはブラウザから以下の図のようにカードを直感的につなげるだけで時系列データの前処理からAIモデルの学習・評価までの一連のパイプラインを作成・実行できるツールです。 各計算処理ステップ(例えば移動平均や正規化など)がカードとして独立し1つずつ実行できるため、視覚的にデータ処理の流れを追いやすくなっています。複数人での同時作業にも対応しているため個人だけでなくチーム単位で効率的なデータ分析やAIモデル開発ができます。もし詳しく知りたい方がいれば過去の関連記事を参照してください。 ノーコードAI開発ツールNode-AIの紹介 ノーコードAIツールNode-AIを使って簡単に需要予測をしてみた 最近噂のノーコードAIモデル開発ツール Node-AIで時系列データの因果分析・重要度可視化・要因分析をしてみた Node-AIにおけるEDA Node-AIではこの各カードに応じた計算処理を非同期処理として専用のマイクロサービス(Worker)で行っています。 ユーザがカードを実行すると裏では大まかに次のような流れで計算処理されます。 なお1つの実行当たりのリソース上限を与えたいので、1つのWorkerが引き受けられるのは1つのmessageまでという作りにしています。 ユーザがNode-AI上のカードを実行する Broker(Redis)にどのような計算処理かといったmessageが格納される WorkerがBrokerからmessageを取得してそれに従った計算処理を実行する WorkerおよびEDAの課題/要件 Node-AIのような機械学習系のアプリケーションにおいて、Workerに関連する部分では以下のようなことが課題/要件として挙げられます。 運用者目線 いつでもアップデートしたい インフラ費用を下げたい ユーザ目線 計算処理が高速に実行されてほしい まずアップデートについて問題になるのは内部処理の安全性担保です。例えばサービス更新によって処理実行中にWorkerが強制終了してしまうとプロダクトの信頼性に関わります。うまくBlue/Green Deploymentのような仕組みを導入できればよさそうですが、そのための開発やメンテナンスのコストを考えると複雑なものや独自のCustom Resource Definitionsは極力採用したくありません。 次にインフラ費用についてです。これはオートスケールやオンデマンドなリソース確保を実現すれば概ね問題にはならないでしょう。 最後にユーザ目線の高速であって欲しいということについてです。そもそもユーザが体感するWorkerに関する待ち時間は ネットワーク遅延 + Workerがmessageを取得するまでの時間 + 計算にかかる時間 + その他 であるとします。 Workerがmessageを取得するまでの時間 について考えてみます。 これはmessageがキューに滞在する時間とも読み替えることができ、実態としてはノードのProvisioningやコンテナ作成、プロセス起動までにかかる時間があります。この滞在時間を小さくするためには、デメリットを無視すれば予めWorkerをたくさん用意したり高速なスケールアウトなどが実現できればよさそうです。 他にも検討した結果、Workerに求められることは以下の4つとなりました。 処理開始までの時間 カードを実行するとすぐに計算処理が開始されて欲しい マイクロサービス更新の安全性 マイクロサービスを更新する際に計算処理中のWorkerに影響を与えたくない スケーリングの安全性 スケールインの際に計算処理中のWorkerに影響を与えたくない インフラ費用面 コンピューティングリソースの維持費用を安く済ませたい 実装方式の比較 まずPodとJobでWorkerを実装した際に要件と合致するかどうかをみていきます。 なおNode-AIはデータ機密性に課題をもつお客さま向けにオンプレミス環境でも提供していたり機械学習を使う都合上柔軟なタイムアウトを設定する必要があるため、各クラウドベンダーのサーバレスサービスなどは選択肢から除外しています。 こちらが評価した結果です。 評価観点 Pod Job 処理開始までの時間 ✅ (条件付き) ❌ マイクロサービス更新の安全性 ❌ ✅ スケーリングの安全性 ❌ - インフラ費用面 ❌ ✅ Node-AIのリリース当初はWorkerをPodで構築していました。 いくつか抜粋すると、まず処理開始までの時間の問題は条件付きでクリアしています。 これはコンテナ作成や内部プロセス起動はPod作成時の一度きりで済むからです。一方でスケーリングを考えた際にはキューにあるmessage数をメトリクスとしてオートスケールさせるとよさそうですが、実際にやってみるとmessageが溜まってからスケーリングが開始するので待ち時間が発生しそうです。 また、マイクロサービス更新やスケールイン時の安全性ついては terminationGracePeriodSeconds や cluster-autoscaler.kubernetes.io/safe-to-evict を駆使するといったことも考えられますが、ライフサイクルの観点から厳しかったりサービス更新には非対応だったりと完璧な解決策にはなり得ませんでした。 次にJobで実装してみました。 JobではPodでの課題をほぼ解決できそうでしたが、今度は処理開始までの時間が大きく低下してしまう結果となりました。これは各message単位で行われるJobの作成とコンテナ中のプロセス起動に大きく時間がかかることに起因します。 Node-AIで使用しているマイクロサービスの場合、image cacheが効いたノードでも上記のオーバーヘッドが約8秒も(つまりPodでの実装パターンよりもカード実行開始までに約8秒多くかかる)あり、UXを大幅に低下させてしまうため採用に至りませんでした。 AgonesをWorkerに利用する これらの課題を鑑みた結果、DGS用OSS Agones を用いてWorkerを構築しました。なお本記事では少し古いですがAgonesのバージョンは 1.28.0 の内容となっています。 オンラインゲームを例とすると、DGSは運営側でホストされプレイヤー間の状態を同期しています。ゲームにもよりますが、おおよそ数分から数時間に及ぶプレイ中の計算結果をメモリに保存するためステートフルな状態となります。そのため実行中のゲームサーバーの保護をする仕組みが必要となります。また利用状況によってはリソースのスケーリングも必要です。AgonesはこうしたDGSのホスト、実行、スケーリングをKubernetes上で容易に実現するものです。 本記事の内容で利用するAgonesのリソースは以下です。 GameServer DGSそのもの、これをWorkerとして使います Fleet 割り当て可能なGameServerのセット FleetAutoscaler Fleetのスケーリングを行うもの DGSと機械学習処理にはステートフルかつ冪等ではない処理を扱うという共通点があります。 そこで具体的にはAgonesの次の機能を利用します。 常にStateが Allocated なGameServerの数を維持するオートスケールアルゴリズムを有する Allocated なStateをGameServerに付与することでアップデートやスケールインの対象外にできる 1つめの特徴を利用することでmessageを引き受けられる状態のWorkerを常に指定数だけPoolしておくことがFleetAutoscalerを用いて実現できます。動画のように例えば4つ用意する設定の場合には、そのうちの1つがmessageを受け取って計算処理状態(Allocated)になると自動で1つWorkerを作成する挙動となります。これによってある程度経済的でユーザに遅延を感じさせないスケーリングを実現できます。 2つめの特徴としてAgonesではStateがAllocatedなものを保護してくれます。つまりWorkerがmessageを取得したタイミングでAllocatedにすれば処理中のWorkerに影響を与えることなく安全なアップデートが可能になります。 また、うまくState遷移を設計すると擬似的にOne-ShotのJobのようなライフサイクルを持つWorkerを作ることができます。Node-AIのWorkerではプロセスの状態に合わせて概ね以下のようなState遷移を行なっています。 これらの特徴から我々の課題を全て一定の水準で解決できることが分かりました。 評価観点 Pod Job Agones 処理開始までの時間 ✅ (条件付き) ❌ ✅ マイクロサービス更新の安全性 ❌ ✅ ✅ スケーリングの安全性 ❌ - ✅ インフラ費用面 ❌ ✅ ✅ Worker内のアーキテクチャ Worker自体は下図のような構成をとっています。なおControllerとの通信部分の記述は省略しています。 AI/ML用コンテナはPythonで書いており、message取得や前処理/機械学習の処理を実行します。 SDK ServerはAgones APIとの通信やメタデータを提供しています。 制御用コンテナはGo言語で書いており、GameServerのlifetime管理や再スケジューリングを行います。( 実装例 ) GameServerを高頻度で作成&削除を繰り返すとFleetで spec.scheduling: Pack としていてもリソースが分散して配置されることがあります。またGameServerのPodには cluster-autoscaler.kubernetes.io/safe-to-evict: false が付与されるのでノードのスケールインが効かなくなります。つまり高頻度でWorkerが使用された後にしばらく使われなくなるとその期間は使用率の低いノードが存在してしまい、ノード単位で課金される料金体系では無駄にコストがかかってしまいます。そこで制御用コンテナを用いて一定時間経つと自己的にGameServerを削除し、最小のノードのセットに詰め込むように再スケジューリングさせる仕組みを導入しています。 また、ゾンビリソースの防止のためにAllocatedなものでも一定時間たったものは強制削除するようにしています。この時間は各カードによって異なる値をAI/MLコンテナからSDK Serverのメタデータとして設定し、例えば正規化の場合は 10分+バッファ時間(5分) 、学習では 24時間+バッファ時間(5分) としています。ただしアプリケーション側にも処理のタイムアウトは入れているのでこれはあくまで保険的な使い方です。 他にもGameServerのhostPort機能などWorkerとして不要なものを無効化しています。 おわりに 本記事ではNode-AIで用いているWorkerについて紹介しました。 Node-AIはどなたでも こちら から無料で試用できるので、もしプロダクトや使われている技術に興味を持っていただけたらぜひ触ってみてください。 それでは、明日の記事もお楽しみに!
はじめに こんにちは、イノベーションセンターでノーコード分析ツール「Node-AI」開発チームの林です。 業務としては Node-AI のフロントエンドやバックエンド開発、最近では監視/可視化のプラットフォーム開発に携わっています。(先日 こちら の記事を執筆したりしています。) 本記事では、2023 年 11 月 14 日に開催した NTT ドコモ・NTT コミュニケーションズ・NTT コムウェアからなるドコモグループ(以下、DCC グループ)内の Google Cloud のユーザーコミュニティ「GINGER」 の第 5 回目のイベントをご紹介します。 GINGER 紹介 GINGER は Google Cloud Community In NTT Group Enterprise の頭文字をとって命名しました。 2023 年 4 月のドコモ在籍時にグーグル・クラウド・ジャパン合同会社様支援のもと立ち上げました。当初はドコモにとどまらず NTT グループ に広げたいという壮大な思いから上記の名前をつけました。 2023 年 7 月にコミュニケーションズへ出向することとなり、これを契機にコミュニケーションズ内に拡大していき、現状は DCC グループ内の Google Cloud ユーザーコミュニティ として活動しています。 活動としては、ドコモとコミュニケーションズにまたがっている slack チャンネルでの情報共有(コムウェアへの拡大は今後)や 2 ヶ月 1 回の頻度で開催しているオンラインとオフラインでのハイブリッド形式イベントが主なものとなっています。 コミュニティメンバーは Google Cloud やコミュニティ活動に興味がある方メンバーであったり、情報のキャッチアップしたいメンバーなどドコモとコミュニケーションズの社員/BP が入り混じって構成 されています。 活動の中でも 特にイベントでのオフラインのつながりを重要視 しています。Google Cloud に関するノウハウ共有を実施しつつ、 このコミュニティに参加したからこそ得られる業務を超えたつながりを価値 として感じてほしいと思っています! では、本題である GINGER Event#5 の開催報告に入りたいと思います! オープニング Event#5 では下記のアジェンダで開催しました! 恒例の LT 大会となっており、トップバッターは グーグル・クラウド・ジャパン合同会社様からネットワークスペシャリストである有賀さんによる「Google Cloud ネットワークの裏側」 についてお話しいただきました。 次にコミュニティにフル参加いただいている常連の NTT ドコモ データプラットフォーム部(以下、ドコモ DP 部) 兼子さんから「GCS に対する IP 制限をやろうと思ったら意外に結構難しかった件」 、最後に今回初めて参加いただいた NTT コミュニケーションズ コミュニケーション&アプリケーションサービス部 西谷さんから「ビジネス d アプリの GCP サーバーレスをフル活用した内製開発について」 という各所から素晴らしいみなさんにエントリーしてもらいお話しいただきました。 アジェンダを見ただけでもワクワクするような LT 3 本立てとなっていて、振り返ってもとても濃密な時間となっていました! (執筆者 NTTコミュニケーションズ/林 知範) 運営メンバー紹介 今回は運営チームもスケールさせたいという思いからコミュニティメンバーに呼びかけをしました。創設当初から参加いただいている NTT ドコモ DP 部の藤平さんに運営お手伝いを依頼したところ、藤平さんとともに働いているみなさんにも手伝ってもらえる形になったのでそのメンバーの紹介になります!(こちらの記事は運営メンバー 4 名による共同執筆となっています。) —-------------------------------- NTT ドコモ DP 部でお仕事させていただいている NTT データ 辰己と申します。普段は DP 部で藤平さんとお仕事をさせていただいており、その一環として当時 NTT ドコモ SI 部に所属されていた林さんと Google Cloud にかかわる技術交流をさせていただいておりました。 今年は気づいたら、その取り組みが、この GINGER として徐々に大きな活動になってきて、かつ、このコミュニティが自分のチームメンバーにとって刺激になっています。 GINGER メンバー・ドコモ DP 部・Googler の皆さまを含め本活動を支援いただいている全ての方に、この場をお借りしてお礼申し上げます。 (執筆者 NTTドコモ/藤平 亮, NTTデータ/辰己 勝俊) 同じく、NTT ドコモ DP 部でお仕事させていただいている NTT データ 三浦と申します。今年になってから初めて、Google Cloud を本格的に触り始めることになりました。この活動に関わる皆さま方からの刺激を受けながら日々研鑽を積んでいる中、辰己さんと同様に、ドコモ DP 部藤平さん推進の元、今回運営側のお手伝いをさせていただくこととなりました。 このような機会を設けていただいた皆さまに、改めて、この場を借りてお礼申し上げます。 (執筆者 NTTドコモ/藤平 亮, NTTデータ/三浦 佑晟) LT1:Google Cloud ネットワークの裏側 LT パートのトップバッターは グーグル・クラウド・ジャパン合同会社様からカスタマーエンジニアでネットワークスペシャリストの有賀さんから「Google Cloud ネットワークの裏側」 についてお話しいただきました。 有賀さんには Event#2 でもお話しいただいたのですが、時間の都合上途中で終わってしまったことがありました。 コミュニティメンバーからの続編を聴きたい!という強い声を届けたところ、快諾していただき 2 回目の登壇をしていただくこととなりました。 発表の内容としては Google Cloud のネットワークということで、下図にあるような リージョンやエッジローケーションがどのように点在しているのかといった話 から始まりました。リージョン数は 38 で利用可能地域は 200 以上に増えたとのことで Google Cloud の広まりを数字で実感する とともに、どのようにアクセスがルーティングされているかを図を交えながら説明いただき大変勉強になりました。 後半はデータセンターについて説明いただき、 全世界にある Google のデータセンターが同じ仕組みで統一されていること を教えていただきました。その設計のおかげでソフトウェア開発者がどのデータセンターのどのサーバーでも動かせるようになっているというのを知り、設計の凄さ・大切さを学ぶことができました。 濃密な内容を発表いただいて、 30 分という長さがあっという間だと感じるくらいの素晴らしい時間でした! まだまだ聴講したいというメンバーも多く、折を見てまたコミュニティメンバーの声を届けさせていただきたいなと思っています。 素晴らしい LT をありがとうございました! (執筆者 NTTコミュニケーションズ/林 知範) LT2:GCS に対する IP 制限をやろうと思ったら意外に結構難しかった件 次の LT は、 NTT ドコモ DP 部(from NTT データ)の兼子さん です。発表テーマは、特定の GCS バケットに対して、どうやって IP 制限をかけることができるか、という内容で発表いただきました。AWS でいうと S3 に対して IP 制限をかけることはバケットポリシーを利用すれば簡単に実現できますが、 Google Cloud 上でこれを実現しようと思うと、意外にもどう実装するかは、いくつかのパターンがあり、かつバケットポリシー程シンプルには実現ができません 。 最終的に兼子さんが採用された案としては、昨年 Google Cloud でリリースされたばかりの機能である エッジセキュリティポリシーという Google Cloud のエッジで動作する WAF 機能と Cloud Armor のサービスアカウントを元に署名付き URL を発行することでバケットへのアクセスの IP 制限を実現する というものでしたが、その実装に当たって API の仕様レベルでどこに実装上の難しさがあったのかを詳細に紹介いただきました。 ■参考リンク Cloud Armor の新しいエッジ セキュリティ ポリシーとプロキシ ロードバランサのサポートの一般提供開始のお知らせ | Google Cloud 公式ブログ 世の中的には AWS と Google Cloud はハイパースケーラーとして並列に語られることが多いですが、 実際には中の NW の仕組みは大きく異なっていました 。それに伴って同じことを実現しようと思ってもそれぞれで実装できることや逆に実装できることは同じだとしても、実装の仕方は大きく変わることがあるなというとても示唆深いLTでした! (執筆者 NTTドコモ/藤平 亮, NTTデータ/辰己 勝俊) LT3:ビジネス d アプリの GCP サーバーレスをフル活用した内製開発について LTのトリをご担当いただいたのは、 NTT コミュニケーションズ コミュニケーション&アプリケーションサービス部 第3サービス部門 西谷 智広さん です。今回始めて GINGER Event にご参加いただき、突然のご依頼にも関わらず快く LT へのご登壇を引き受けてくださいました。私自身初めてお会いしましたが、経歴紹介で多くの実績を残されていることを知りその後の LT への期待が大きくなっていました。 ご発表いただいた内容は現在開発中のビジネス d アプリというモバイルアプリを Google Cloud サーバレスに内製開発に挑んだお話 で、詳細を公表できないのがもどかしいほど濃密で幾重にも考慮が積み重なったお話を聞くことができました。 お伝えできる範囲でどんな発表だったかをお伝えすると、 コーディング経験が無い方を含めた開発プロジェクトにおいて設計/コード量/検証を極力減らした構成とするために App Engine, Cloud Run, Cloud Firestore, Cloud Spanner 等を組み合わせたGoogle Cloudのサーバレス環境を構築してアプリケーション開発に挑んだ 、という具体的な構成図含めてのご発表でした。ビジネスdアプリのサーバレスアーキテクチャについては、いずれ社外でも講演予定とのことです。 結果として、アプリケーションの開発のみに集中して取り組む体制を構築することにつながり、順調にリリースに向けた準備が整っているとのことで、今後の展開がますます楽しみです! 聴講していた参加者の皆さまも、ご自身の経験にもとづくご質問を数多く寄せていただき、非常に活発なQ&Aになったと思います。 (執筆者 NTTドコモ/藤平 亮, NTTデータ/三浦 佑晟) クロージング これにて LT パート終了となりました。が、最後に私ごとではあるのですが Google Cloud Partner Top Engineer 2024 に選出されたこと を報告させていただきました! NTT コミュニケーションズからは初選出ということと、1 年間選出されることを目標に取り組んでいたこともあり非常に嬉しい報告となりました。 こちらの選出に関しては、 GINGER を盛り上げてくれているコミュ二ティメンバーや全面的に支援いただいているグーグル・クラウド・ジャパン合同会社様のご助力あってこそ だと思っています。この場を借りて皆さまに感謝申し上げます。 来年は GINGER から複数人の Top Engineer が誕生することを願って、本イベント終了となりました! (執筆者 NTTコミュニケーションズ/林 知範)
この記事は、 NTT Communications Advent Calendar 2023 10日目の記事です。 そして、DevOpsプラットフォームの取り組みを紹介する9回目の記事です。 Qmonus Value Stream については、 当プロダクトの連載記事 をご覧ください。 はじめに こんにちは、イノベーションセンターの Qmonus Value Stream チームに所属している松本です。 私たちQmonus Value Stream チームのミッションはNTTコミュニケーションズおよびNTTグループ向けDevOpsプラットフォームであるQmonus Value Streamを開発してプロダクトチームに提供し、プロダクトチームを課題解決に集中させることでプロダクトの成功に寄与することです。 本記事は、そんなDevOpsプラットフォーム Qmonus Value Streamを使ってユーザに体験してほしい「目的を選ぶだけで作るクラウドアーキテクチャ」を実現するための取り組みを紹介します。 また、今回、Qmonus Value Streamを無料でお使いいただける機会を作りましたので、詳しくは記事の最後をご覧ください。 プロダクトとして開発される内部向けプラットフォーム 私たちのチームが提供するQmonus Value Streamはプロダクトと位置付けられて開発されています。そのためチームにはプロダクトマネージャー(以下、PdM)が配置され、PdMは開発者と共に開発を進めています。私たちのPdMはQmonus Value Streamの開発から提供までの戦略を立て、実行、意思決定する責任者です。 私も以前は内部向けプラットフォームを提供するチームに所属していましたが、内部向けプラットフォームはコスト削減やポリシーの適用を主な目的としていることから利用を強制されることも多く、厳しいルールや機能不足から利用者には窮屈で不便なものだと認識されることもありました。Qmonus Value Streamはこのような過去の内部向けプラットフォームとは目的が違い、Developer Experienceを改善するものです。Qmonus Value Streamは社内規定で強制するものではなく、PdMがプロダクトチームに成功ストーリーを伝え、プロダクトチームが試して自ら採用を決定する方法で利用を促進しています。ここでいうプロダクトチームは社内やグループ内で特定プロダクトを成功させるために組織された、PdM・開発者・デザイナー等のチームです。 ユーザ体験を追求する取り組み 私たちのチームでは開発者もPdMと同様にユーザ中心思考でユーザ体験の追求に取り組んでいます。今回はペルソナの作成とユーザテストについてご紹介します。 ペルソナの作成 プロダクトとして内部プラットフォームを作る際にも他のプロダクト開発と同様にユーザの理解は大事になります。 私たちのチームは、ISPで働いてたメンバー、情シスで働いてたメンバー、新規事業の開発をしていたメンバーなどさまざまなバックグラウンドを持ったメンバーで構成されています。メンバーは「内製開発チームのテックリード」といっても想像できなかったり、若手のリーダーを想像したり、ベテランのマネージャ級の人物を想像することもありました。 私たちのチームは共通のユーザ像を描きたかったのでPdMのメンバーが集まってペルソナを作ることにしました。まずは、社内の16チームのテックリードにインタビューして、テックリードたちがどのような課題に取り組んでいるか、その課題の解決にどのような苦労があるかを聞き出すことができました。そしてインタビュー記録を読み、それぞれのペルソナを書き出し認識を合わせる作業をしてペルソナを書き上げています。 今まで他人が作ったペルソナを見ることはありましたが、本当にこのようなペルソナは存在するのかと懐疑的なところもありました。この取り組みでは、ペルソナを作るプロセスで実在するプロダクトチームにインタビューをしてユーザは何処に関心ごとがあるかを確認しましたので裏付けがあり納得感のあるものができたと思います。 複数のペルソナを作成したのですが、例としてその1つをご覧ください。大手企業のDX推進担当が開発効率・品質向上という課題に取り組んでいるが、本来やるべき仕事に力を入れられず、さらに自己研鑽する時間の捻出に苦労している様子が分かります。開発者にもペルソナを共有することで、今後は問題解決の手法を議論するときにもペルソナだったらどう感じるだろうという視点も加わることを期待しています。 ユーザテスト 今年になってチームはユーザテストのプラクティスを取り入れました。ユーザテストのやり方は goodpatchさんの記事 を参考にさせてもらっています。 私たちのプロダクトではゴールを定めて開発し、ゴールに達成したらユーザテストをしてその効果を確認します。 おおよそ3名から5名に社内から協力いただき、ユーザとして目的を達成するような操作をしてもらいます。ユーザには操作しながら考えていることを発話するようにお願いするので、私たちは操作する画面と表情と考えてることを観察し、受け入れられているのか、つまずいたり迷ったりしていないかを読み取っていきます。観察した結果やインタビュー結果を持ち寄りインパクト分析して次のアクションを導き出しています。次のゴールを設定しゴールを達成したら評価するというフィードバックループを繰り返しているのです。 私たちのチームでは、ユーザテストを開発者が担当しました。これまでの開発チームは直接ユーザの声を聞く機会がなく、ユーザがどのように利用しているか想像できないという課題がありましたが、ユーザテスト後に開発者へヒアリングしてみるとユーザテストは概ね好評で「チームだけでは気付けないことが得られた」「プロダクト開発には必要なことだと考えている」との意見がでています。 ユーザテストを始めた初期のインパクト分析の結果をご紹介します。検出された項目は、影響の大きさで効果問題・効率問題・満足度問題に分類されます。効果問題はユーザが操作を続けられないほどの大きな影響であることを示しています。さらに何人からその問題を検出したかで分類し、3人全員のユーザから検出されたということは、おそらくほとんどのユーザが困難に直面することになる事態を推定できます。このインパクト分析で赤くぬった箇所、黄色く塗った箇所の順に優先順位が高いと分かりますので、最も効果の高い部分から問題を修正できました。 「目的を選ぶだけ」というユーザ体験 私たちのチームはプロダクトチームのテックリードは本来優先して時間を使うべきビジネスロジックの設計や実装に時間を使うべきなのに、直接の顧客価値を生まないクラウドアーキテクチャの構築やCI/CDパイプラインにも多くの時間を使っていることに着目しました。マニュアルを読んで調べたり定義ファイルや設定ファイルを作成したり試行錯誤しなくても良いように、「目的を選ぶだけで」クラウドアーキテクチャが作成でき、その Infrastructure as Code(以下、IaC)を組み込んだCI/CDパイプラインを作成できるという体験をさせたいと考えました。 今回、ペルソナの作成やユーザテストを取り入れフィードバックループを回すことで得られた成果として、「目的を選ぶだけで」というユーザ体験を「主目的」と「オプション」を選択するUIを作成できましたのでご紹介します。 まず、主目的の選択イメージを示します。当初は3層アーキテクチャを選択する画面だったのですが、現在のバージョンでは「スタンダードなWebアプリケーション」を選択するように変わりました。これなら、3層アーキテクチャを知らない人であっても、構築するシステムの目的が分かっている人であれば利用できます。ドキュメントを読む必要も設定ファイルや定義ファイルを書く必要がないだけでなく、他のメンバーやテックリードに力を借りることもなくクラウドアーキテクチャが構築できるようになりました。 次に、オプションの選択イメージを示します。ユーザのアプリケーションには事業特有の要件があったり、組織によって共通で求められるセキュリティポリシー要件が定められていることもあります。この画面ではシステムの冗長化とアプリケーションの脆弱性試験のオプションが選択されています。冗長化の設定はミスに気づきにくい課題がありますし、アプリケーションのパッケージの脆弱性試験もツールの検証から始める必要があり導入は困難ですが、私たちのDevOpsプラットフォームを使えば画面で選択するだけで構築できます。これからも多様なアプリケーションで利用できるようにオプションを拡充していきます。 まだまだ改善は続く 「目的を選ぶだけ」のユーザ体験を作ってからも複数名にユーザテストをしたところ、概ね好意的な反応を得られたのですが、アプリケーションによってはまだ十分な拡張性を得られてなかったりエラー表示時に自力で解決できない場合があったりと、多くの気づきを得ることができました。私たちはまだフィードバックループを繰り返す必要がありそうです。 Qmonus Value Streamの実証実験へのお誘い この「目的を選ぶだけ」でクラウドアーキテクチャを作る体験を、NTTグループだけでなく一般のお客さまにも試してもらいたいと考えています。 そこで、DevOpsプラットフォームの商用化を目標として機能や操作性の課題について利用者からのフィードバックを目的とした実証実験を行います。トライアル利用をご希望される方は以下の応募フォームからお申し込みください。 応募開始日:2023年12月10日  1 トライアル利用可能期間:2024年2月1日 - 2024年9月30日  2 , 3 対象チーム 日本の企業であること 開発チームは内製で開発していること 開発チームはアジャイルに開発していること 開発チームにCI/CDやIaCに理解があること 開発で利用するサービスの導入に関する決定権を持っていること、もしくは決定権者に対して導入の提案ができること 応募フォーム: こちら からお申し込みください。数日中にこちらから折り返しご連絡させていただきます。 利用料:無償 その他:本実証実験でのトライアル提供における導入から運用まで、無償でNTT Communicationsがサポートします。 最後まで、ご覧頂きありがとうございました! それでは、明日の記事もお楽しみに! 応募者多数の場合、本DevOpsプラットフォームの提供をお待ちいただく、もしくはお断りする場合があります。 ↩ トライアル利用は1ヶ月間ですが、トライアル利用期間中であれば1ヶ月単位で延伸できます。 ↩ NTT Com都合でトライアルを中止もしくはトライアル期間を短縮や延伸する場合があります。 ↩
マイクロサービスアーキテクチャにおいては、個々が独立に選定したデータベースを持つ複数のサービスにまたがって、データの整合性を維持する必要があります。 そのための方法として、Sagaパターンと呼ばれる設計方法がありますが、Sagaでは分離性が欠如しておりLost Update等の異常が発生しかねません。 そこで本記事では、Sagaの分離性を高めるための実装におけるTipsを解説します。 目次 目次 はじめに 複数サービス間での整合性維持における課題 Sagaパターン Sagaを構成するトランザクション Sagaによって実現される安全性 原子性(Atomicity) 整合性(Consistency) 分離性(Isolation) 永続性(Durability) 異常を防止/軽減する実装 分離性の欠如が引き起こす異常 分離性の欠如への対策 Semantic Lock Commutative Updates Pessimistic View / Optimistic View Reread Value Version File By Value 終わりに 参考文献 はじめに この記事は、 NTT Communications Advent Calendar 2023 12日目の記事です。 こんにちは、クラウド&ネットワークサービス部の川瀬(GitHub: hkws )です。業務では主に、NTT Comで提供しているサービスのバックエンド開発を担当しており、特に社内外の複数サービスをオーケストレーションする部分を実装してきました。 本記事では、複数のサービス間でデータの整合性を維持するための方法であるSagaパターンを紹介し、Sagaの分離性の欠如により起きうる異常と、その予防・軽減策を解説します。 複数サービス間での整合性維持における課題 複数のサービス間で整合性を持ってデータを更新しなければならないケースとして、オンラインショッピングサービスでの商品の購入やキャンセルをするという場面を検討してみましょう。ここで、オンラインショッピングサービスは以下のように、注文管理サービス、在庫管理サービス、決済サービスにより構成され、個々のサービスが独自にデータベースを持つDatabase per Serviceの構成とします。 ユーザにより商品が購入されると、以下のような処理が実行されるとします。 A1 在庫管理サービスが、購入された商品の在庫を確保 A2 注文管理サービスが、注文内容を記録する A3 決済サービスが、決済処理を行う A4 配送サービスが、配送依頼を記録する A5 メール送信サービスが、ユーザに購入完了メールを送信 その後、もし商品購入のキャンセルがリクエストされた場合には、以下のような処理を行うとします。 B1 配送サービスが、配送依頼を取り下げる B2 在庫管理サービスが、購入された商品の在庫を解放する B3 注文管理サービスが、注文のキャンセルを記録する B4 決済サービスが、返金処理を行う B5 メール送信サービスが、ユーザに購入キャンセル完了メールを送信 商品の購入(A1 ~ A5)、および商品購入のキャンセル(B1 ~ B5)は、複数のサービスにまたがってデータの変更が生じる処理です。これらの処理はトランザクショナルに、すなわちひとまとまりの処理として行われなければなりません。そうでなければ、決済処理が完了しなかったのに注文は記録された(A3で失敗した場合)、在庫が確保されているのに対応する注文が存在しない(A2で失敗した場合)といったデータの不整合が生じうるからです。 このように、複数のサービス(参加者)にまたがった処理をアトミックに行うため、2相コミット(2PC)のような分散トランザクション管理を利用することも選択肢の1つかもしれません。 しかしながら、2PCには以下のような課題があります。 レプリケーションされていないコーディネータは、システム全体にとっての単一障害点になってしまいます。 各参加者は参加者全員がコミットできるまでロックを保持し続けるため、ロックが長期化する傾向にあります。特に、コーディネータに障害があると、参加者は未確定のトランザクションによるロックを保持したままになってしまいます。 トランザクションのコミットが成功するためには、すべての参加者が反応を返さなければなりません。参加者のいずれかが反応を返せなくなってしまうと、トランザクションは必ず失敗してしまうのです。 一貫性を維持すべきサービスの全てが、2PCをサポートする必要があります。 このうち、2点目と4点目の課題を解決するためのアプローチが、Sagaパターンと呼ばれる方法です 1 。マイクロサービス間の疎結合性を損なわず、データの整合性を維持することを目指します。 Sagaパターン Sagaとは、一連のローカルトランザクションのシーケンスによって、複数サービス間でのデータの整合性を維持する方法です。操作するリソースごとにトランザクションを分解してローカルトランザクションとし、それらは対応するデータベースを更新して、次のローカルトランザクションを実行するためのメッセージやイベントを発行します。これにより、各サービスにおいてロックの保持が長期化することを回避します。 Sagaパターンにおいては、個々のローカルトランザクションは単一データベースにおけるトランザクションと同等です。よって、個々のサービスが持つデータベースは、2PCのような分散トランザクションに対応する必要はありません。 ただし、独立にローカルトランザクションがコミットされてしまうことは、単一データベースのトランザクションや2PCのようなロールバックが、サービスに跨ってできないことを意味します。あるローカルトランザクションがロールバックされたとしても、それ以前にコミットされたローカルトランザクションによる変更は残ったままになってしまうのです。 そこでSagaでは、ローカルトランザクションによるコミットされた変更を打ち消すためのトランザクション(補償トランザクション)によりロールバックを行います。もちろん、補償トランザクションも失敗する可能性がありますし、ビジネス的な要件からそもそも補償トランザクションを実装できないこともありえます。その対策として、ローカルトランザクションが成功するまで自動的にリトライするよう実装しなければなりません。 Sagaを構成するトランザクション Sagaは複数のローカルトランザクションのシーケンスとして構成されますが、各トランザクションはその振る舞いによって、以下の3つに分類できます。これらに対する理解は、後述する分離性の欠如への対策を検討する上で有用です。 補償可能トランザクション(Compensatable Transaction) 補償トランザクションによってロールバックされうるトランザクション ピボットトランザクション(Pivot Transaction) Sagaを最後まで実行するか、ロールバックするかを判断するポイントになるトランザクション ピボットトランザクションがコミットされれば、Sagaは最後まで実行される 補償可能トランザクションや再試行可能トランザクションとは異なるトランザクションになるとは限らず、Sagaにおける最後の補償可能トランザクションや、最初の再試行可能トランザクションになることがある 再試行可能トランザクション(Retriable Transaction) いつかは成功することが保証されているトランザクション ロールバックが不要であるため、対応する補償トランザクションを持たない ピボットトランザクションの前に配置される 前述したオンラインショッピングサービスにおける商品購入キャンセルSagaを、このモデルに照らし合わせると以下のようになります。 Step 種別 サービス トランザクション 補償トランザクション 1 補償可能 配送サービス 配送依頼の取り下げ 配送依頼取り下げのキャンセル 2 補償可能 在庫管理サービス 在庫の解放 在庫の再確保 3 補償可能 注文管理サービス 注文キャンセルの記録 注文キャンセルの取り消し 4 ピボット 決済サービス 返金処理の実施 - 5 再試行可能 メール送信サービス ユーザへの購入キャンセル完了メール送信 - Sagaによって実現される安全性 2PCのような分散トランザクション管理を利用せず、Sagaという代替策を利用するデメリットとして、トランザクションが提供する安全性を享受できないことがあげられます。ここでは、ACID特性と呼ばれる4つの性質(原子性、整合性、分離性、永続性)をSagaがどれほど満たしているか検討します。 原子性(Atomicity) 原子性とは、いくつかの書き込みがオールオアナッシングで行われるという性質です。グループ化された複数の書き込みが障害のため完了しなかった場合、その時点までに行われた書き込みは全て破棄されます。 MySQLやPostgreSQLでは、 START TRANSACTION によってトランザクションを開始し、複数の書き込みを行っている最中に障害が発生した場合、 ROLLBACK によって変更を取り消すことができました。しかしSagaパターンの場合、複数のローカルトランザクションのシーケンスにより書き込みを行うため、今までに行った変更の全てを単一のコマンドで取り消すことはできません。Sagaでは、補償トランザクションを順々に実行していくことで、ロールバックを実現します。 一般には、補償トランザクションによりロールバックできる点を踏まえ、Sagaは原子性を満たすと考えられています。しかしながら、原子性の実現には補償トランザクションが行うような巻き戻し機能だけでなく、分散合意や状態遷移先の出力といった機能も必要であり、Sagaにはそれが不足しているという 指摘 もあります。 整合性(Consistency) トランザクションの文脈において「整合性がある」とは、データについて常に真でなければならない何らかの言明(不変性)を満たしていることを示します。例えば、会計システムにおいて貸方と借方が等しくなければならないことは、会計システムにおける不変性の1つと言えるでしょう。 整合性は、原子性、分離性、永続性とは異なり、データベースのみで保証してくれる特性ではありません。外部キー制約やユニーク制約等があるとはいえ、アプリケーションがデータベースへ不変性に違反する書き込みを行ったとしても、データベースはそれを止めることはできません。 このことは、Sagaパターンの場合も同様です。Sagaパターンによって複数のサービスに書き込みを行う場合、単一のデータベースに対する書き込みと同じく、不変性を満たすようSagaを実装しなければなりません。 Sagaオーケストレーションフレームワーク Eventuate Tram の開発者であるChris Richardsonは、サービス内の参照完全性はローカルデータベースによって実現され、サービスの壁を超えた参照完全性はアプリケーションによって実現されるため、整合性を有すると 主張 しています。しかし、Sagaというアイデア自体が整合性を担保する仕組みを内包しているわけではないことから、筆者は整合性を有するとは言えないと考えています。 分離性(Isolation) 分離性とは、並行に実行されているトランザクション同士が、お互いに干渉できる度合いを示したものです。特に、複数のトランザクションを同時に実行した結果が、それぞれを順番に実行した時と同じ結果になることを保証する場合、直列化可能という最も強い分離性を有することになります。 Sagaは明らかに、分離性を満たしていません。Sagaの個々のローカルトランザクションによって変更がコミットされると、Sagaが全ステップを実行し終えていなくとも、その変更が他のSagaからも見えてしまいます。 永続性(Durability) 永続性とは、トランザクションのコミットが成功したら、仮にハードウェアの障害やデータベースのクラッシュがあったとしても、そのトランザクションで書き込まれた全てのデータは失われないという特性です。 Sagaパターンにおいて、永続性はローカルトランザクションのコミットによって実現されます。 このように、Sagaは分離性のみが欠如しているという意見がある一方、理論的な検討から原子性、整合性、分離性の3つが欠如しているという指摘もあるようです。 本記事では以降、両者が共通して指摘している分離性の欠如について、それが引き起こす異常とその軽減策を解説します。 異常を防止/軽減する実装 分離性の欠如が引き起こす異常 Sagaも単一のデータベースと同様、同時に複数のクライアントから実行される可能性があるものの、分離性を満たしていません。そのため、以下のような問題の発生が考えられます。 Lost Updates: ある Saga が、別の Saga による変更を読み取らずに書き込みを行うこと Dirty Reads: Saga が更新をまだ完了しないうちに、トランザクションまたは Saga がその更新を読み取ること Fuzzy/Nonrepeatable Reads: 読み取りと読み取りの間にデータ更新が発生したため、別の Saga ステップが別のデータを読み取ること MySQLやPostgreSQLでは、このような分離性の問題に対応するため複数のトランザクション分離レベルを備えています。しかしSagaでは、これらの問題を防止・軽減するための実装をアプリケーション開発者が行わなければなりません。 分離性の欠如への対策 Semantic Lock 分離性を高める方法の1つは、アプリケーションレベルでロックすることです。 作成または更新するレコードにフラグを設定することで、このレコードがコミットされておらず、変更される可能性があることを示します。 Semantic Lockを導入する際、当該レコードに対して別のSagaが読み取りや書き込みを試みた場合に、そのSagaがどのように振る舞うかも検討しなければなりません。 最も素朴な方法は、変更される可能性のあるレコードに対する操作を試みたSagaの実行を失敗させ、クライアントに再試行させることでしょう。これは、実装が単純なものの、クライアントに再試行の負担を強いることになります。また、ロックが解放されるまでリクエストをキューイングするという方法もあります。こちらはクライアントの再試行の手間を無くすことができますが、ロックの管理やデッドロックの検出など、サーバ側の実装が複雑になります。 前述したオンラインショッピングサービスの商品購入は、Semantic Lockの導入により以下のようなシーケンスになるでしょう。 注文内容の記録を1ステップ目(A'1)で実施し、その際注文ステータスを「処理中」に設定するよう変更しました。そして、新たに6ステップ目(A'6)を追加し、注文ステータスを「出荷準備中」にしています。このようにすることで、ステータスが「処理中」である注文は変更される可能性があることを示すことができます。 その上で商品購入キャンセル Saga では、以下のようにキャンセルしようとする注文が処理中かどうかを確認し、処理中である場合はキャンセル処理を中止します。これにより、商品購入と商品購入キャンセルの分離性を高めることができます。 Commutative Updates 分離性の欠如によって生じるLost Updatesは、正しい順序で更新操作が実施されなかった場合に発生する問題です。そこで、どのような順序で実行されても正しい更新結果が得られるよう、更新の仕方を工夫しようというアイデアがCommutative Updatesです。 銀行口座への振り込みや引き落としは、(引き落とし額が預金額を上回らない限り)可換な更新の一例です。例えば、残高が300,000円である銀行口座に対して、給与210,000円の振込と家賃70,000円の引き落としが同時にあったとしましょう。このとき、「口座残高を510,000円にする」という操作A、および「口座残高を230,000円にする」操作Bは可換ではありません。A -> Bの順序で実行された場合には給与の振込操作Aが、B -> Aの場合は家賃引き落とし操作Bがロストしてしまいます。しかし、「口座残高を210,000円増やす」という操作A'と「口座残高を70,000円減らす」という操作B'は可換です。どちらが先に実行されても、口座残高は440,000円になります。 よって、補償可能トランザクションを「口座残高を210,000円増やす」、対する補償トランザクションを「口座残高を増やす前(ここでは300,000円)の値に設定し直す」と定義してしまうと、後者は可換な更新ではないためLost Updatesの可能性が生じます。しかし、その補償トランザクションを「口座残高を210,000円減らす」と定義することで、他のSagaによる更新を上書きする危険を低減できます。 Pessimistic View / Optimistic View 開発しているアプリケーションの問題領域によっては、特定の属性について上限や下限を設定しなければならないことがあります。例えばコンサートの残空席は、(ビジネス上オーバーブッキングを許容する場合を除き)0を下回ることは許されません。 しかし、分離性の欠如によりDirty Readsが発生することで、定められた下限や上限を超えてしまう可能性があります。 Sagaでは、原子性を実現するために補償トランザクションが定義されますが、通常直近に実行した補償可能トランザクションに対する補償トランザクションから順に実行されていきます。すべての補償トランザクションが即座に実行されるわけではないため、Sagaの初期のローカルトランザクションで更新される値ほど不整合な値をとる時間が長くなってしまいます。 以下のシーケンス図にて具体例を示します。これは、前述したオンラインショッピングサービスにおいて、ユーザAにより商品Xの購入のキャンセルと、ユーザBによる商品Xの購入が同時に発生したケースです。ユーザA、ユーザBともに注文した商品Xは1つであり、ユーザAによる購入によって商品Xは在庫がなくなっていたものと仮定します。 ユーザAは商品購入のキャンセルを試みましたが、決済処理でエラーが発生したため、補償トランザクションが実行開始されたとします。このときから在庫解放処理に対する補償トランザクションが実行されるまで、在庫は開放された状態になっています。よってその間に同じ商品の購入をユーザBが試みると、在庫があるように見えるため成功してしまいます。しかし実際は、ユーザAによる商品Xの購入キャンセルは失敗したため、依然としてユーザAも商品Xを購入できている状態です。結果、提供できる商品は1つしかないものの、ユーザA、ユーザBともに購入できてしまいます。 Pessimistic Viewは、Sagaのステップの並びを工夫することによって、Dirty Readsが発生する可能性を小さくしようというアイデアです。 先程の商品購入キャンセルを行うSagaの場合、以下のように並び替えることで、商品在庫のDirty Readsを低減できるでしょう。商品在庫の解放を再試行可能トランザクションとすることで、在庫の再確保を不要にできるからです。 Step 種別 サービス トランザクション 補償トランザクション 1 補償可能 配送サービス 配送依頼の取り下げ 配送依頼取り下げのキャンセル 2 補償可能 注文管理サービス 注文キャンセルの記録 注文キャンセルの取り消し 3 ピボット 決済サービス 返金処理の実施 - 4 再試行可能 在庫管理サービス 在庫の解放 - 5 再試行可能 メール送信サービス ユーザへの購入キャンセル完了メール送信 - 一般化すると、Pessimistic View / Optimistic Viewは以下のように実装されます。 下限が存在するリソース(在庫や座席など)について ユーザの選択肢を制限する操作(座席予約など)は、補償可能トランザクションで行う ユーザの選択肢を増やす操作(座席予約のキャンセルなど)は、再試行可能トランザクションで行う 上限が存在するリソース(利用可能な課金額など)について ユーザの選択肢を増やす操作(利用可能な課金額の増加など)は、補償可能トランザクションで行う ユーザの選択肢を制限する操作(利用可能な課金額の減少など)は、再試行可能トランザクションで行う Reread Value データ更新が発生する個々のステップの中でも、分離性を高めるための工夫が可能です。 他のサービスに対するデータ更新を要求する直前に、更新対象データを要求することで、直近の状態に対して更新をかけることができます。 ただし、他のサービスとの通信が増えてしまうため、速度が重要になるサービスにおいては適用すべきではないかもしれません。 また、更新対象データの取得後から更新するまでの間に、別のクライアントによってデータが更新された場合には対応できません。 Version File 同時並行に複数のSagaが実行される場合、連携する各サービスにどのような順序でリクエストが届くかはわかりません。 例えば、商品購入と商品購入キャンセルが同時に実行された場合を考えます。注文管理サービスには「注文を記録する」リクエストと「注文をキャンセルする」リクエストが届きますが、Semantic Lockを使っていなければ、「注文を記録する」リクエストのあとに必ず「注文をキャンセルする」リクエストが届くとは限りません。まだ注文が存在しないのに、注文のキャンセル要求だけが届いてしまうこともありえます。 この対策として、注文管理サービスにおいてどのようなリクエストが届いたかを記録しておき、正しい順序で実行するという手段がありえるでしょう。「注文をキャンセルする」リクエストが「注文を記録する」リクエストより先に到着したとしても、注文管理サービスは「注文を記録」した後に「注文をキャンセル」するのです。 By Value By Valueは、リクエストの内容によって動的にロジックを切り替えるという手法です。例えばリスクの低い要求は saga で実行し、リスクの高い要求は分散トランザクションで実行するのです。 なにがリスクであり、それをどのように分類するかは、ビジネス要件を考慮しながら定義する必要があるでしょう。 オンラインショッピングサービスの場合は、法人からの大口の注文は分散トランザクションによって処理し、個人からの小口の注文についてはSagaで処理するといったロジックの切り替えがありえるかもしれません。 終わりに 本記事では、Sagaの弱点の1つである分離性の欠如に由来する問題を予防・軽減ためのさまざまな方法を解説しました。 これらの方法全てを活用することが、いつも正しいとは限りません。システムに対する要求と照らし合わせながら取捨選択し、安全性の高いサービス間連携を実現していきましょう。 それでは、明日の記事もお楽しみに! 参考文献 マイクロサービスパターン 実践的システムデザインのためのコード解説 データ指向アプリケーションデザイン ― 信頼性、拡張性、保守性の高い分散システム設計の原理 ソフトウェアアーキテクチャ・ハードパーツ ― 分散アーキテクチャのためのトレードオフ分析 Frank, Lars, and Torben U. Zahle. “Semantic Acid Properties in Multidatabases Using Remote Procedure Calls and Update Propagations.” Software, Practice & Experience, vol. 28, no. 1, 1998, pp. 77–98, https://doi.org/10.1002/(SICI)1097-024X(199801)28:1<77::AID-SPE148>3.0.CO;2-R. Frank, L. “Countermeasures against Consistency Anomalies in Distributed Integrated Databases with Relaxed ACID Properties.” 2011 International Conference on Innovations in Information Technology, IEEE, 2011, pp. 266–70, https://doi.org/10.1109/INNOVATIONS.2011.5893830. Garcia-Molina, Hector, and Kenneth Salem. "Sagas." ACM Sigmod Record 16.3 (1987): 249-259, https://doi.org/10.1145/38713.38742. Sagaを導入しても1点目と3点目の課題は残ります。Sagaは一般にオーケストレーションベースで実装されることが多く、オーケストレーターが単一障害点になります。また、Sagaでも参加者のいずれかが反応を返せなくなった場合は実行に失敗し(失敗箇所が補償可能な場合は)ロールバックされます。 ↩
この記事は、 NTTコミュニケーションズ Advent Calendar 2023 11日目の記事です。 はじめに こんにちは。コミュニケーション&アプリケーションサービス部の石井です。 今年はAI分野においては LLM 1 の話題で持ちきりの一年でしたが、そんな LLM とは全く関係のないグラフニューラルネットワーク(以下、GNN)の説明性に関する手法である GNNExplainer を題材に扱っていこうと思います。 GNN 2 とはグラフで表現された構造化データを深層学習で扱うためのニューラルネットワーク手法の総称です。グラフデータはさまざまな事象を表現できる可能性を秘めていて、GNN の予測結果を解釈できれば、人との関係性把握やマーケティングへの応用など幅広い活用が期待できると思っています。GNN に興味がない方もこんな技術があるのかと深く考えずに読んでもらえればと思います。 本記事で扱う内容 本記事では以下の内容について扱います。 説明可能な AI(XAI)について GNNExplainer とは GNNExplainer の実践 説明可能な AI(XAI)や GNNExplainer に関しての簡単な概要説明をした後に、サンプルデータを用いて構築したモデルに GNNExplainer を当てはめてみた結果についてコードと併せて解説をしていきます。説明可能なAI(XAI)や GNNExplainer の概要についてはすでに知っているという方は記事後半の「GNNExplainer の実践」まで飛ばして見てください。 説明可能なAI(XAI)について 機械学習モデルにおける意思決定を行う際に、そのモデルが下した判断を人間が理解できるように説明することを目的とした技術の総称を「説明可能なAI(XAI) 3 」と言います。昨今の技術発展は、ディープラーニングを皮切りにより高い予測精度を達成している一方で、予測における説明可能性というのは複雑化する関数近似によって犠牲になっています。つまり、予測精度と説明可能性の間にはトレードオフの関係があり、昨今では機械学習モデルを人間が理解することは極めて難しくなっています。 では、なぜ説明可能なAI(XAI)が大事なのかというと、それは現実世界やビジネス領域では意思決定における透明性や不偏性が要求されるためです。例えば、医療分野である人の病気の発症リスクを予測する判定を機械学習モデルで行い、予測結果として発症リスクが高いとの判断をした場合には、その結果と根拠理由を示さなければ納得が得られず信用を損なう可能性があります。 このように説明可能性は現実世界では極めて重要な要素の1つとなっており、一般的に人間は自分が解釈したり理解できないものを採用しない傾向があるため、機械学習モデルの説明可能性はあらゆるシーンにおいて無視できないものとなっています。そのため、 GNN においても同様にモデルの説明可能性は要求されることから、 GNN における説明性に焦点を当てた技術が開発されてきています。 GNNExplainer とは GNNExplainer 4 とは、2019年に NeurIPS 5 で採択された論文である「 GNNExplainer: Generating Explanations for Graph Neural Networks 」の中で提案された GNN の説明性に関する手法です。 GNNExplainer は学習済みの GNN モデルと予測結果を入力として与えると、出力として予測に影響を与えたノードの特徴量と予測を説明するサブグラフを返すことで予測結果を説明することを可能にします。 また、model-agnostic な手法であるため、特定のモデルに依存することなく扱うことができます。加えて、GNN で扱う問題設定にはいくつかの種類がありますが、ノード分類、リンク予測、グラフ分類など一般的なグラフの問題設定に対応しているため適用範囲が広いことが言えます。 論文内では、実世界のデータセットを用いて、GNNExplainer の説明性における妥当性を定量分析と定性分析の両側面から評価してどれくらい有効であるかを述べています。興味がある方は、論文を参照して詳しい実験内容について見てください。 GNNExplainer のアルゴリズムは、ノード が与えられた際に予測根拠をよく説明するようなサブグラフ とノードの特徴量 を特定することを目指していきます。そして、ここで述べているよく説明ができているサブグラフ を、当該アルゴリズムでは相互情報量 を最大化するような と定義しています。 上記の定式は、エントロピー と条件付きエントロピー の差分を最大化することを意味しており、学習済みの GNN では予測確率は固定であることからエントロピー の項は一定となるため、条件付きエントロピー の項を最小化するようなサブグラフ を探索していくことを実質的に行います。 もう少し簡単に説明すると、全体のグラフから対象ノード とは別のノードである を除外した際の予測確率 の増減を見て、予測確率が大きく減少する場合はノード は予測に良い影響を与えると判断して、予測に大きく寄与するエッジのみを選択していくことで有効なサブグラフの獲得を目指していきます。 実際には、このサブグラフの選定の際には全探索すると計算コストが膨大となるため、直接最適化問題を解くことはせずに条件付きエントロピーの式をイェンセンの不等式や平均場近似を用いてエッジの存在有無を示す期待値に変換して、サブグラフの隣接行列を計算して求めていくことで実現します。この辺りの詳細な説明については 元の論文 を参照ください。 GNNExplainer の使い方 GNNExplainer は PyTorch Geometric 6 内のモジュールとしてすでに実装済です。そのため、当該フレームワークを利用することで簡単に GNNExplainer の処理を再現できます。 PyTorch Geometric ではバージョン2.2から説明性のフレームワークとして explain モジュールを提供しており、さまざまなアルゴリズムを用いて GNN の説明性生成や可視化のための柔軟なインターフェースを提供しています。 PyTorch Geometric では GNNExplainer 以外のアルゴリズムとして、CaptumExplainer や PGExplainer 7 、 AttentionExplainer 8 などのアルゴリズムが用意されており利用することが可能です。以下にそれぞれの特徴をまとめます。 GNNExplainer はノードの重要特徴量だけでなくグラフトポロジに基づいた重要なサブグラフを同時に明らかにする GNN に対して有効な説明手法を提案した最初の技術です。単一のインスタンス(予測対象であるグラフやノードの単位)に対して、独立した説明性に関わる特徴量やサブグラフを生成するため、その説明性を対象としていない別のインスタンスに一般化することが困難であるという課題があります。しかし、複雑な GNN の理解に際して解釈を一助することは間違いないため、適切な状況下での利用や他の手法と組み合わせて解釈を補うことが可能です。 CaptumExplainer は Integrated Gradients 9 と呼ばれる勾配積分法の公理に基づいて各次元における特徴量の寄与度を算出する手法で、算出過程がモデル実装に依存しないため、model-agnostic な手法に分類されています。また、Integrated Gradients はあらゆる微分可能モデルへの適用が可能なため、GNN に限らずさまざまなモデルで説明性の技術として適用されています。一方で、Integrated Gradients はグラフに特化した手法ではないことから、ノード間などの相互作用などが考慮されないため、説明性についても当然に相互作用が考慮されない形で出力されるといった課題があります。 PGExplainer は GNNExplainer で課題となる単一インスタンスに制限される課題を、説明性の生成過程をニューラルネットワークよりパラメータ化することで、一連のインスタンスの予測をまとめてモデル全体としての説明性を取得することを可能にした手法です。こちらも model-agnostic な手法となっています。また、一連のインスタンスを予測する際に GNNExplainer では新しいインスタンスに対して再学習を要するが、PGExplainer では一度学習した説明器のモデルを帰納的な設定のみで新しいインスタンスを説明可能となることから、再学習を必要とせず大規模なデータセットに対しても手法適用が可能とされています。 ちなみに余談にはなりますが、PyTorch Geometric と同様に GNN を扱うフレームワークである DGL 10 でもバージョン 1.0.0 以降で GNNExplainer 等をサポートしています。どちらのフレームワークでも GNNExplainer を扱うことができるため、ご自身ですでに使い慣れているフレームワークに合わせて選択されると良いかと思います。 GNNExplainer の実践 ここからは実際にサンプルデータを用いて、構築した GNN モデルに GNNExplainer を適用して予測を解釈することを試していこうと思います。 実行環境・インストール まずは実行環境です。 今回は以下の内容で PyTorch Geometric が扱える環境を用意しました。 # cat /etc/os-release NAME="CentOS Linux" VERSION="7 (Core)" ID="centos" ID_LIKE="rhel fedora" # pip list | egrep "(torch)" torch 2.1.1 torch-cluster 1.6.3+pt21cpu torch_geometric 2.4.0 torch-scatter 2.1.2+pt21cpu torch-sparse 0.6.18+pt21cpu torch-spline-conv 1.2.2+pt21cpu torchaudio 2.1.1 torchvision 0.16.1 PyTorch や PyTorch Geometric のインストールは公式サイトに分かりやすく記載していますのでそちらを参照してコマンドを実行してみてください。 データ準備 今回はベンチマークとしても利用されているオープンデータである Amazon Dataset 11 を利用します。 このデータセットはノードが商品を示し、エッジは共同購入されたことを表したグラフデータです。 ノード特徴量は商品レビューを bag-of-words 12 によりベクトル変換したデータとなっており、745次元の特徴量を持っています。また、ノードのターゲットラベルは商品カテゴリを示しており、8つの商品カテゴリのいずれかに該当します。 項目 内容 ノード数 7,650 エッジ数 238,162 ノード特徴量(次元数) 745 クラス数 8 文章では少し分かりづらい部分もあるかと思いますので、ノードとエッジの関係性を簡易に示した図と実際のデータを NetworkX 13 にて可視化したグラフを載せておきます。 問題設定 問題設定はシンプルにマルチクラスのノード分類問題を考えます。以下に概要を示します。 予測対象となる商品ノードが8つの商品カテゴリのどれに当てはまるのかを予測する GNN モデルを構築することを目指します。 加えて、今回は説明性を獲得したいので構築した GNN モデルに対して、GNNExplainer を適用してその得られた説明性について確認していこうと思います。 モデル解釈 本題のモデル構築と GNNExplainer による説明性の獲得についてコードを載せながら述べていきます。まずは、GNN モデルを作成します。 import random from math import sqrt from collections import Counter import networkx as nx import torch import torch.nn as nn import torch.nn.functional as F import torch_geometric.transforms as T from torch_geometric.explain import Explainer, GNNExplainer from torch_geometric.explain.metric import fidelity from torch_geometric.explain.metric import groundtruth_metrics from torch_geometric.nn import GCNConv from torch_geometric.utils import to_networkx from torch_geometric.datasets import Amazon # パラメータ指定 DIM = 16 # データ読み込み dataset = Amazon(root= '../data' , name= 'Photo' ) data = dataset[ 0 ] # データ分割(学習データ:テストデータ:バリデーションデータ=7:2:1) split = T.RandomNodeSplit(num_val= 0.1 , num_test= 0.2 ) data = split(data) # モデル定義 class Model (nn.Module): def __init__ (self, num_features, dim= 16 , num_classes= 8 ): super ().__init__() self.conv1 = GCNConv(dataset.num_node_features, dim) self.conv2 = GCNConv(dim, num_classes) def forward (self, x, edge_index): x = self.conv1(x, edge_index) x = F.relu(x) out = self.conv2(x, edge_index) return F.log_softmax(out, dim= 1 ) def train (model, data, optimizer, criterion, epochs= 100 ): for epoch in range ( 1 , epochs + 1 ): model.train() optimizer.zero_grad() out = model(data.x, data.edge_index) loss = criterion(out[data.train_mask], data.y[data.train_mask]) loss.backward() optimizer.step() pred = out.argmax(dim= 1 ) acc_train = int ((pred[data.train_mask] == data.y[data.train_mask]).sum()) / int (data.train_mask.sum()) acc_val = eval_acc(model, data, data.val_mask) if epoch % 10 == 0 : print (f 'Epoch: {epoch:03d}, Train Loss: {loss:.3f}, Train Loss: {acc_train:.3f} ,Val Acc: {acc_val:.3f}' ) return model def eval_acc (model, data, mask): model.eval() pred = model(data.x, data.edge_index).argmax(dim= 1 ) correct = (pred[mask] == data.y[mask]).sum() acc = int (correct) / int (mask.sum()) return acc # パラメータセット device = torch.device( 'cuda' if torch.cuda.is_available() else 'cpu' ) data = data.to(device) model = Model(dataset.num_node_features, dim=DIM, num_classes=dataset.num_classes).to(device) optimizer = torch.optim.Adam(model.parameters(), lr= 0.005 , weight_decay= 5e-3 ) criterion = nn.CrossEntropyLoss() # モデル学習とテストデータによる精度評価 model_gnn = train(model, data, optimizer, criterion, 300 ) acc_test = eval_acc(model_gnn, data, data.test_mask) print (f 'Test Acc: {acc_test:.3f}' ) 上記のコードでは Amazon Dataset のデータ読み込みからモデル学習、評価までを実装しています。 学習結果はテストデータでの accuracy が 93.7% で正しく学習できている様子が伺えます。なかなかの高精度ですね。各カテゴリ別の正答一致数を混同行列より確認してみても、ほとんどのカテゴリで正しく予測が出来ていることが見てわかります。 さて、ここまでで構築された GNN モデルを用いて、いよいよ本題の GNNExplainer の実装に移っていきます。 先ほど学習したモデル( model_gnn )を Explainer クラスのパラメータとして渡して説明性を取得していきます。 # サブグラフを可視化する関数 def viz_subgraph (edge_index, edge_weight, target, node_index): target_color = '#FFFFFF' color_list = [ '#FCFFA4' , '#F7E425' , '#FEBA2C' , '#F89540' , '#F2844B' , '#E16462' , '#CC4778' , '#B12A90' ] if edge_weight is not None : edge_weight = edge_weight - edge_weight.min() edge_weight = edge_weight / edge_weight.max() if edge_weight is not None : mask = edge_weight > 1e-7 edge_index = edge_index[:, mask] edge_weight = edge_weight[mask] if edge_weight is None : edge_weight = torch.ones(edge_index.size( 1 )) subgraph_idx = np.unique(explanation.edge_index[:, mask][ 0 ]) target_idx = np.where(subgraph_idx == node_index)[ 0 ] target = [color_list[idx] for idx in target[subgraph_idx]] target[target_idx[ 0 ]] = target_color g = nx.DiGraph() node_size = 800 for node in edge_index.view(- 1 ).unique().tolist(): g.add_node(node) for (src, dst), w in zip (edge_index.t().tolist(), edge_weight.tolist()): g.add_edge(src, dst, alpha=w) plt.figure(figsize=( 15 , 8 )) ax = plt.gca() pos = nx.spring_layout(g) for src, dst, data in g.edges(data= True ): ax.annotate( '' , xy=pos[src], xytext=pos[dst], arrowprops= dict ( arrowstyle= "->" , alpha=data[ 'alpha' ], shrinkA=sqrt(node_size) / 2.0 , shrinkB=sqrt(node_size) / 2.0 , connectionstyle= "arc3,rad=0.1" , ), ) nodes = nx.draw_networkx_nodes(g, pos, node_size=node_size, node_color=target, margins= 0.1 ,) nodes.set_edgecolor( 'black' ) nx.draw_networkx_labels(g, pos, font_size= 10 ) plt.show() plt.close() # explainerインスタンスの定義 explainer = Explainer( model=model_gnn, algorithm=GNNExplainer(epochs= 200 ), explanation_type= 'model' , node_mask_type= 'attributes' , edge_mask_type= 'object' , model_config= dict ( mode= 'multiclass_classification' , task_level= 'node' , return_type= 'log_probs' , ), ) # 説明性に関わるノード指定と演算処理 node_index = 700 explanation = explainer(data.x, data.edge_index, index=node_index) # ノード特徴量における重要変数の可視化 explanation.visualize_feature_importance( "feature_importance.png" ,feat_labels= None , top_k= 10 ) # 指定したノードに関係するサブグラフ可視化 viz_subgraph(explanation.edge_index, explanation.edge_mask, explanation.target, node_index) 初めに定義している viz_subgraph 関数は GNNExplainer より得られた結果のエッジ情報及びマスク情報を元にサブグラフを可視化する処理を定義しています。 次いで、Explainer クラスを用いてインスタンスを作成することで、ノードの重要変数とサブグラフ取得などの説明性の獲得を行なっています。この Explainer クラスは全ての説明性に関わるパラメータを扱うようにデザインされたクラスで、フレームワーク利用者はこのクラスのパラメータを変更することで共通処理のままで複数のアルゴリズムを操作すること可能にしています。今回は GNNExplainer を利用するため、 algorithm の GNNExplainer を指定して、 model_config に GNN モデルのタスクに合わせて適切なパラメータを設定しています。 Explainer クラスのインスタンスを作成した後は、入力データと説明性の対象とするインスタンス(今回はノード)のインデックス番号を与えることで予測における説明性情報を取得します。インスタンスのインデックス番号を適当に 700 とした場合の結果は以下のようになりました。 まずは、ノードにおける重要変数(Feature Importance)ですが、重要度が高い方から top_k で指定した数だけ特徴量を棒グラフで可視化しています。今回のデータセットではノード特徴量は bag-of-words によってベクトル化した情報のため、どのようなワードが重要であるかを当該データから判別はできませんが、意味のあるラベル付きの情報であった場合は何が予測に効いているのかを把握するのに有効な方法だと思います。 最後に、ノードインデックスが 700 の予測に寄与しているサブグラフを可視化して見てみると、 700 の商品ノード(白い丸)には 2008 と 6347 の商品ノードが予測に密接に関係しているという結果が見られます。また、予測に寄与しているサブグラフの商品ノードは、ほとんどが同一カテゴリ(赤丸のノード)であることから、同一カテゴリの商品と一緒に購買されていていることが分かります。特に 2008 は単独で同時に購買されているが、 6347 はその他の関連する商品と同時に購買されていることが言えそうです。 GNNExplainer では特定のノードに焦点を当てているため、グラフデータ全体としての説明性を明言することは難しいですが、このように単一のノードを起点にした説明性の理解から大まかな全体傾向を掴むなどの活用に期待はできそうですね。 終わりに 今回は GNNExplainer の概要とモデルに対しての適用方法について紹介しました。 GNN によってグラフ構造をニューラルネットワークで扱えるようになりましたが、グラフ構造自体の複雑性も相まって説明性・解釈性は非常に高いわけではありません。 そのため、今回紹介した GNNExplainer などによる GNN の予測根拠の説明性向上は、追加特徴量の検討や予測根拠によるマーケティング活用など幅広く応用が効くものになるかと思います。 まだまだ、発展途上の分野ではありますが、今後もXAI領域の発展には期待したいですね。 それでは、明日の記事もお楽しみに! Large Language Model の略で、極めて大量のデータと深層学習技術によって構築された言語モデルです。 ↩ こちら で GNN についての解説が詳しく記載されています。 ↩ https://www.darpa.mil/program/explainable-artificial-intelligence ↩ https://arxiv.org/pdf/1903.03894.pdf ↩ https://nips.cc/ ↩ https://pytorch-geometric.readthedocs.io/en/latest/ ↩ https://arxiv.org/pdf/2011.04573.pdf ↩ Attention ベースの GNN により Attention 係数をエッジの説明に応用した解釈性の手法です。Attention 機構を扱った特定のアルゴリズムで学習したGNNモデルのみが利用可能となります。 ↩ https://arxiv.org/pdf/1703.01365.pdf ↩ https://www.dgl.ai/ ↩ https://arxiv.org/pdf/1811.05868.pdf ↩ 文章中に出現する単語の順番は考慮せずに、単語の出現回数のみからベクトルを表現する手法を指します。 ↩ https://networkx.org/ ↩
この記事は、 NTT Communications Advent Calendar 2023  8 日目の記事です。 はじめに こんにちは、イノベーションセンターでノーコード分析ツール「Node-AI」開発チームの林です。 業務としては Node-AI のフロントエンドやバックエンド開発、最近では監視/可視化のプラットフォーム開発に携わっています。 本記事ではこの監視/可視化のプラットフォームについて、検討段階ではあるのですがアーキテクチャを中心にまとめていきたいと思います。 Node-AI について Node-AI はノーコード分析ツールとなっていて「予測/異常検知モデルをすぐに・簡単に・わかりやすく作成可能」といったところを推しているツールとなっています。 インフラとしては、Google Cloud を利用しており Google Kubernetes Engine (以下、GKE)の上でアプリケーションが実行されています。 運用上の課題 現在は誰しもが同様に利用できる環境を提供していたり、個別の顧客環境としてカスタマイズしたものを提供したりと Google Cloud のプロジェクトレベルで別れた環境を複数提供しています。 そうなってくると運用する上で面倒なのがメトリクスやログといったテレメトリー情報が各プロジェクトに集約されていることです。コンソール上でプロジェクトを切り替えれば Cloud Monitoring や Cloud Logging の Explore で確認できますが、可能であれば 一元的に集約して「ここを見るだけ Node-AI の現状がわかる!!」 という状態にしたいと思いました。 また、 各環境を横串で分析したい という時もプロジェクトごとに BigQuery に蓄積していると一工夫が必要だったりするので、こういったケースでも一元的に集約できていると嬉しいことがありそうです。 加えて、 目的に応じてプロジェクトを切り出すことで権限制御の簡素化や IaC の肥大化を防ぐといったメリット もあると考えるため、その点も考慮したいと思います。 「テレメトリー集約基盤」爆誕(の予定) 「各環境のテレメトリーを集約」 をテーマに新しくプロジェクトを切り出して下記のようなアーキテクチャを検討しています。よくあるログの集約/可視化パターンではあるのですが、いくつか推しポイントがあるのでその点をご紹介したいと思います。また、これらの推しポイントの裏テーマとして 「極力手間をかけずに」 というのもかかげていたりします。 推しポイント① Grafana + Cloud Run 可視化のツールとして Grafana を採用して Cloud Run 上にホスティングしています。Cloud Run Service でホスティングすることで「運用負荷の軽減」「コスト削減」の 2 点のメリットが得られると考えたためです。 「運用負荷の軽減」 という点では、みなさんご存知の通り Cloud Run はサーバーレスサービスというところで Compute Engine など IaaS を利用した際の面倒ごと(例えば、柔軟性が高いが故の初期セットアップでのネットワークやセキュリティに関する適切な設定作業など)が少ない のが非常に嬉しいところです。 「コスト削減」 という点では、Cloud Run Service の最小インスタンス数を 0 にしておくことでリクエストがない間は 自動スケーリング機能 によりインスタンス数が 0 になります。インスタンス数が 0 のメリットとしてはその間課金されないところです、一方でリクエストを契機に起動することになるのでインスタンス数が 0 の状態での初回リクエストでは時間がかかる(コールドスタート)点がデメリットでもあります。 今回のユースケースでは Grafana のダッシュボードを見られれば良いのでコールドスタートを許容できます、なので最小インスタンス数を 0 にした自動スケーリング機能を活用してコスト削減の恩恵を受けられる形になっています。 推しポイント② Cloud Run + Wildcard Certificate + Identity-Aware Proxy Grafana on Cloud Run を運用者がアクセスするにあたって Cloud Load Balancing を利用してインターネット公開しています。公開にあたり「セキュリティ強化」と「今後の運用を見据えたドメイン紐付け」について紹介します。 Cloud Run Service でホスティングしてインターネット公開する方法はいくつかあると思いますが、「 セキュリティ」 を意識した時に Cloud Load Balancing や Cloud Armor を前段に置くパターンが一般的かと思います。今回はそれらに加えて、Identity-Aware Proxy(以下、IAP) というプロキシサービスを活用しています。こちらを導入すると Grafana にアクセスすると下記のように Google アカウントの認証画面に移動して、適切な権限を持っている Google アカウントでないと認証を通さない設定を簡単に入れる ことができます。 「今後の運用を見据えたドメイン紐付け」 という点では、Cloud Run Service の便利な機能である URL マスク と Certificate Manager による Wildcard Certificate を組み合わせて 1 つの DNS 設定で複数のサブドメインとサービスを紐づけられることができまる構成をとっています。こちらの導入により、今後運用に必要なツールを採用したい時に Cloud Run Service に新たなツールをデプロイするだけで追加の IP の払い出しや Load Balancer のプロビジョニングなどが必要なくなります。 推しポイント③ Grafana Plugin + Cloud Monitoring + Cloud Logging Grafana で Kubernetes のメトリクスやログを可視化しようとすると Prometheus, Promtail, Grafana Loki といった複数のコンポーネントが思い浮かびます。これらのコンポーネントを極力増やさない面倒をみないように工夫した「マネージドサービスと Plugin による拡張」について紹介します。 Prometheus については、 Google Cloud Managed Service for Prometheus という機能があり GKE 1.27 以降の新規クラスタではデフォルトで有効化されています。名前の通り、マネージドな Prometheus となっていて 利用者が新たにデプロイする必要がない点が手間がかからず嬉しいところ です。 また、テレメトリーの蓄積する場所も必要になってくるかと思います。ストレージ周りの追加の管理は大変な面もあるので、この点もマネージドに寄せられたらと思いました。Grafana を漁っていると今年の 3 月に Cloud Logging に接続できるプラグインがリリースされていることがわかりました。Grafana にはデフォルトで Cloud Monitoring のプラグインも導入されていて、これらを採用することで Grafana から Cloud Operations のサービスを利用でき、新たなコンポーネントの追加や管理から解き放たれました! まとめ 検討中のテレメトリー集約基盤について紹介しました!工夫のポイントとして 3 つ挙げています。 Grafana + Cloud Run による「運用負荷の軽減」「コスト削減」 Cloud Run + Wildcard Certificate + Identity-Aware Proxy による「セキュリティ強化」「今後の運用を見据えたドメイン紐付け」 Grafana Plugin + Cloud Monitoring + Cloud Logging による「マネージドサービスと Plugin による拡張」 この中でも 「極力手間をかけない」 という点も意識してマネージドサービスを採用したり置き換えたりしてみました。とはいえ、構築して終わりではなくこの基盤を使って「どのように運用を高度化するか」というのが重要だと思っています。 メトリクスやログを可視化して、 実際にサービスをユーザーに使ってもらう中で何を SLI として SLO を設定した上で、運用から得られるフィードバックを開発に活かすというサイクルを回せるところまで取り組めたらと思っています! DevOps の実践、楽しみです。 今後の展望 デフォルトで収集できるメトリクスやログだけでなく、 OpenTelemetry の導入によるさらなるテレメトリー収集も並行して検討 しています。こちらはチームの優秀なメンバーが担当してくれていて、近日中に導入できそうとか。非常に楽しみです。 将来的にはトレースとログを紐づけて分散トレースにも対応できる状態を目指しています。こちらも形になったら記事にしたいと思います! 最後まで、ご覧いただきありがとうございました! 参考記事 GKE で手間をかけずに Let's Observability!-前編- GKE で手間をかけずに Let's Observability!-後半- Cloud Certificate Manger による Wildcard Certificate を用いた Cloud Run の活用 Cloud Run を徹底解説!
この記事は、 NTT Communications Advent Calendar 2023 及び 高専キャリア Advent Calendar 2023 の7日目の記事です。 皆さんこんにちは、 SDPF クラウド/サーバー 仮想サーバーチームの宮岸( @daiking1756 )です。 昨日の 6日目の記事 を書いた @Kumassy_ と同じく、 普段はOpenStackベースの仮想サーバー基盤やバックエンドストレージ基盤の開発・運用をしています。 この記事では、私が2023/11/18に開催された HNK全国高専交流会 2023 in 白山 のLT枠で発表した 高専かるた について紹介します。 また、初めて オープンデータ に貢献して思ったことを書こうと思います。 LT枠での発表の録画と発表資料は公開されておりますので、興味がある方はぜひご覧下さい。 www.youtube.com docs.google.com 私と高専について 高専かるたについて 高専オープンデータについて なんで作ったの? 校章に興味を持ったきっかけ💡 何もしないうちに7年が経過⏳ 由来がないので追加し、ついでにかるた化🎴 皆さまからのデータ提供をお待ちしています🙌 おわりに 私と高専について タイトルにもある通り、この記事では高専が1つのテーマとなります。 そこでまずは簡単に私と高専の関わりをまとめた画像を載せておきます。 初対面の人と話すときに何か共通点があると話が弾むと思いますが、私の経験上「高専出身」というのは抜群に話が弾みます。 マイノリティであるが故のあるあるネタ、高専独特の間合い、全国高専〇〇大会話、止まらない寮の珍事件など、話題に事欠きません。 ※ あくまで私個人の感想です。 会社に入ってからも、高専出身という部分から広がった人脈が多くあり、嬉しい限りです。 高専かるたについて さて、今回作った 高専かるた を紹介します。 下記のURLからアクセスできます。 https://codeforkosen.github.io/kosen-apps/karuta.html また関連リンクは下記のサイトにまとめているので、こちらも載せておきます。 protopedia.net 高専かるたにアクセスすると画像の通り、63高専の校章と読み札が1枚表示されております。(画像の読み札は「石川高専」) 分からない場合はヒントボタンを押すと、読み札の校章の由来を確認できます。 正しい札を取ると、取った札は消えていきます。 最後の1枚まで到達したときの御手付きの回数によって、最後に表示される称号が変わるようになっています。 高専博士の称号を目指して遊んでみて下さい。 FYI: 現在はイージーモード中です https://t.co/OBp11ho3bn の #高専かるた ですが、 最後の1枚まで進むと御手付きの回数に応じた称号が得られます。 目指せ高専博士! (現在は「取札を強調」が使い放題なイージーモード中...) #高専 pic.twitter.com/F4uSZqRTL0 — daiking⊿🌗 (@daiking1756) 2023年11月19日 x.com 元々 高専オープンデータ を使ったサンプルページとして 高専の校章一覧ページ が公開されていました。(詳細は後述) このページにかるた機能を追加したものが、今回作った 高専かるた です。 高専オープンデータについて 今回お世話になった高専オープンデータは下記のリポジトリで管理されています。 github.com 今回は高専の校章に関するオープンデータ( data/kosen_school_emblem.csv )を利用しましたが、他にも下記のようなオープンデータがありました。 高専環境報告書オープンデータ(公式) 高専キャンパスオープンデータ(非公式) 高専プロコンオープンデータ(非公式) 高専カリキュラムオープンデータ(非公式) 高専カレンダーオープンデータ(非公式) 下記の通り、オープンデータの提供は大募集中のようで、高専有識者の方はぜひプルリク出しましょう。 高専の方、オープンデータ提供、お願いします! 各高専、各学科の非公式オープンデータの追記(プルリク)いただける方も、大募集! なんで作ったの? さて、流れが唐突だったので、これを読んでいる多くの方は「なんで高専かるた作ったの?」状態になっていると思います。 説明しても納得されない気もするのですが、ここからは高専かるたを作った背景を順番に書いていきます。 校章に興味を持ったきっかけ💡 まずは校章に興味を持ったきっかけについて書きます。 時は約7年前、私が高専在学中のある日の全校集会のこと。 早く集会終わらないかななどと考えながらぼーっと前方を眺めていたところ、ふとステージ中央の上部に置かれている母校石川高専の校章に目が留まりました。 当時の私:「ん・・・?この校章は・・・?石と川で高専という文字が挟まれてできている!!なんと!!」 驚愕の事実に気付いた私は 由来 を調べてみました。 すると、確かに睨んだ通り下記の由来が書かれていました。 「石川高専」であることを明確に打出したもの, というアピール性に眼目をおいて「高専」の文字を「石」と「川」で両側から円形に囲み, 創造と協調の精神が生きたわかりやすいものにしました。 当時の私:「校章って奥深い・・・。他高専はどうなってる?高専の校章一覧サイトとかあると面白いのでは?」 こうして私は校章に興味を 持ちそうに なりました。 何もしないうちに7年が経過⏳ 校章に興味を持ちそうになったものの、結局何もせずに時間が経ち、次第に当時の熱も冷めきっていきました。 そうして約7年の時が経った2023年10月。 とある記事を見つけます。 fukuno.jig.jp なんと、私がぼーっとしている間に 高専の校章一覧ページ ができていました!(ありがたい) 早速アクセスしてみると、各高専の校章にはそれぞれ特徴があり、想像通り眺めていて面白いものでした。 ただし、そこには私が欲していた校章の由来データは見つかりませんでした。 うーん、これは困った。 由来がないので追加し、ついでにかるた化🎴 無ければ自分で追加しよう!ということで、各高専のホームページを巡る旅に出ました。 地道に巡った結果、校章データが登録されていた63高専のうち、41高専のデータはホームページから取得できましたが、 残る22高専のデータは埋めることができませんでした。 github.com これが私にとって初めてのオープンデータへの貢献でした。 提供する側はデータの正確性や出典など気を遣う部分も多いですが、自分自身が欲しているデータがオープンデータとして追加されるのは気持ちが良いものですね。 そしてこのデータがまた別の活用をされていくと思うと、何だかワクワクします。 こうして校章とその由来情報が揃ったことで、「なんか上の句と下の句みたいでかるた作れそうだな」という発想が生まれ、高専かるたが作られたのでした。 皆さまからのデータ提供をお待ちしています🙌 「まだまだ校章由来情報が埋まっていない高専があるので、情報提供頂けると助かります!」と告知をしてLTを締めました。 すると、早速神山まるごと高専さんが情報提供して下さりました。(圧倒的感謝) 高専かるた、さっそく神山まるごと高専も入れていただきありがとうございます🙌 少し長くなりますが、神山まるごと高専の由来は以下の通りです。… — 【公式】神山まるごと高専 (@kamiyama_kosen) 2023年11月20日 x.com 頂いた情報は温かいうちにオープンデータへ反映するようにするように努めております。 神山まるごと高専さんから頂いたデータも既に反映済みです。 校章由来情報を管理しているCSVは下記です。 皆さまからの温かい校章由来情報の提供を心よりお待ちしております! data/kosen_school_emblem.csv へのプルリクエストを作って頂いても、宮岸( @daiking1756 )へ直接ご連絡頂いてもどちらでも構いません🙌 本記事の公開時点では残り21高専分の由来情報が埋まっていない状態です。 github.com おわりに 本記事では高専オープンデータを利用して高専かるたを作った事例を紹介しました。 オープンデータは使い方次第で面白い作品がたくさん作れると思うので、 オープンデータ自体への貢献とそれを使った作品づくりを継続しようと思いました。 最後に告知です! NTTドコモグループでは高専出身者はもちろん、高専本科生の 採用 も行っております。 興味がある方はぜひエントリーしてみて下さい! また、 学生さん向けのイベント も随時開催中ですので、こちらもチェックしてみて下さい。 (今後もじゃんじゃん追加予定です) さらに!今年の年末12/26(火) 17:00 - 20:00で IoT縛りの勉強会! IoTLT vol.106@NTT Com(忘年会IoTLTラジオ) がNTT Comのオフィス内で開催されることになりました🎉 NTT Com社員はもちろん、一般の方も参加/登壇可能なイベントです。 現地参加枠には限りがあるので、興味のある方はお早めにご応募下さい! 当日の様子はYouTube Liveにて配信予定です。 iotlt.connpass.com それでは明日の記事もお楽しみに〜👋
この記事は、 NTT Communications Advent Calendar 2023 6日目の記事です。 こんにちは。 SDPF クラウド・仮想サーバーチームの杉浦 ( @Kumassy_ ) です。 普段は OpenStack の開発・運用をしており、最近は Observability まわりを取り組んでいます。 この記事では、以前私が Tech-Night という社内 LT 会で発表した以下のプロジェクトのご紹介します。 Tech-Night については以下の記事をご覧ください。 きっかけ 今年は不安定な世界情勢と円安、猛暑により電気代を気にする機会が多かったのではないでしょうか。 私もあるとき 7-9 月の電気代を確認したところ、電力使用量が 330 kWh、電気代が 10,000 円を超えていました。これは私のチームの 4 人家族のご家庭と比べても多い値でした。 なぜ私の家では電気代がかかってしまうのか? 私のチームはリモートワークが中心なため、働きやすいように冷房をつけっぱなしにしていました。エアコンが原因でしょうか。 それとも 24 時間ゲーミング PC を起動して Cookie Clicker を動かしているからでしょうか?今使っているゲーミング PC は Aura Sync 対応パーツで揃えて自作したものです。 ライティングが美しいので、消費電力は実質ゼロであり、電気代とは無関係なはずです。 自宅の消費電力を測定する 電気代をケチる前に、一体何が電力を消費しているのか測定したいところです。 はじめに検討した方法はワットモニターを使うことです。 瞬間的な消費電力を測定するのには向いていそうですが、 1 日の消費電力を時系列で確認するのはつらそうです。他にもっと安い製品もありそうですが、少し価格も高めです。 次にスマートプラグも検討しました。 スマートプラグは本来コンセントそのものを IoT 化するためのデバイスだと思いますが、消費電力を測定できる機能をもつ製品もあります。 これなら消費電力をグラフとして確認できるのでよさそうです。 ただ、エアコンや冷蔵庫等 1 つずつスマートプラグをつけようとすると高くなってしまいそうです。 また、風呂場の換気扇等、スマートプラグをつけられなさそうな電化製品の電力は測れなさそうです。 SwitchBot プラグミニ(JP) 電力会社はどのように電力使用量を測定しているか 昔は検針員 1 が各住宅を巡回し、電力使用量を確認していました。 今では、住宅にはスマートメーターという通信機能つきの電力計が設置されており、電力使用量が自動的に収集されています。 都市部ではスマートメーター同士が P2P で通信し、電力会社の端末までデータを転送しているそうです。面白いですね。 電力会社が使う通信路を A ルートと呼ぶそうです。 スマートメーターには通信機能が備わっていることがわかりました。 実は B ルートという方式を使えば、一般人でもスマートメーターから情報を取り出すことができます。 Wi-SUN モジュールを使ってスマートメーターとおしゃべりする スマートメーターとおしゃべりするには、 Wi-SUN という無線規格を使います。 Wi-SUN には低電力で長距離伝送できることとメッシュネットワークを構成できることが特徴とのこと。 Wi−SUN に対応した専用のモジュールはいくつかありますが、家に転がっていた Raspberry Pi を有効活用したかったので BP35A1 というモジュールを購入しました。 BP35A1 の他にも USB タイプの Wi-SUN モジュールもあるようなので、そちらのほうがお手軽かもしれません。 B ルートは暗号化されているため、 ID とパスワードを入手する必要があります。 東京電力管内であれば以下のサイトから ID とパスワードを確認できます。 ちなみに、 ID は郵便で送られてきます。 シリアル通信周りの設定をして、 Wi−SUN モジュールと Raspberry Pi を接続します。 メス-メスのジャンパ線が家になかったのでブレッドボードを介して繋げておきました。 スマートメーターにパケットを送るには、 B ルート ID とパスワードを設定 ネットワークをスキャン PANA 認証 ECHONET Lite 規格のパケットを送信 という手順を踏みます。 まずは B ルート ID を設定するため、 SKSETRBID <B ルート ID><CRLF> といったコマンドを Wi-SUN モジュールに送信します。 すると送信したコマンドのエコーバックと OK<CRLF> が返ってきます。 Wi−SUN モジュールからの応答が想定通りかどうかをチェックしたいところです。 瞬間消費電力のリクエストを投げるときはどうでしょうか。このときは SKSENDTO コマンドを使います。 レスポンスとしては ERXUDP <DATA><CRLF> が返ってくるので、これもバリデーションしたいです。 <DATA> は ECHONET Lite というプロトコルのバイナリ形式のデータです。 ECHONET Lite は仕様書が公開されており、以下のページから確認できます。 パーサーを書く さて、 Wi-SUN モジュールからの応答には OK<CRLF> のような ASCII 文字列とバイナリ形式のデータが混じっていることがわかりました。 これをうまくパースしてバリデーションをしたいのですが、どのようなコードを書けばよいでしょうか。 出力が ASCII 文字列であれば <CRLF> で文字列を区切ってしまえば簡単にパースできそうです。 そのような実装もあります 2 が、 <DATA> には <CRLF> に相当する \x13 や \x10 が含まれることがあり、私の環境ではうまく動きませんでした。 また、実験の結果レスポンスは 10 bytes ずつ返ってきたので、一時的に出力をバッファしておく必要があります。 パース処理はバッファに対して複数回試行されるので、パース処理が失敗したとしてもバッファの中身が変更されないようにする必要があります。 以上のような要件にあうパーサーのフレームワークを探したところ、 nom がよさそうでした。 説明書きには byte 列を食べて (bite) くれるといったことが書かれており、遊び心があってよいですね。 nom はパーサーコンビネータと呼ばれる種類のパーサーのようですが、パーサーコンビネータとはなんでしょうか。 パーサーコンビネータは小さいシンプルなパーサーを組み合わせて所望のパーサーを実現する方式です。 私は大学でコンパイラを作る授業を受けたのですが、そのときは lex と yacc を使って、 正規表現を書いて字句解析する BNF 記法で文法を定義する parser generator を使ってパーサーを生成する というトップダウン的なアプローチでパーサーを作っていました。 lex, yacc に渡すファイルの書式が独特なことと、文法を定義することが大変でやや苦労しました。 パーサーコンビネータはこれとは対象的に、小さなパーサーを組み合わせるボトムアップ的なアプローチでパーサーを作ります。 試しに OK<CRLF> をパースするパーサーを作ってみましょう。 OK という文字列をパースするには、 tag を、 <CRLF> をパースするには crlf を使います。 これらの間には他の文字は入らないので、 tuple を使ってこれらが連続して出てきたときにのみパースが成功するようにします。 簡単なパーサーなのに早くも tag と crlf という 2 つのパーサーを組み合わせてしまいました! 同様に IPv6 アドレスのパーサーを作ってみましょう。 Wi−SUN モジュールでの IPv6 アドレスは 0 を省略せず、次のようなフォーマットで表します。 take_while_m_n は条件式が成立する限り m 以上 n 以下の長さのバイト列を切り取ります。 OK パーサーと同様に tuple を使ってパーサーを組み合わせればよいです。 次に EVENT のパーサーを作ってみましょう。 EVENT のフォーマットは次のようになります。 EVENT <イベント番号> <IPv6アドレス> <パラメータ><CRLF> <パラメータ> の部分はイベント番号によって存在したりしなかったりします。 このようなときは opt コンビネータを使うことで、パースできなかったときに None を返すようにできます。 map_res はパースした結果を加工するコンビネータです。 21 というバイト列は ASCII コードから \x32\x31 と解釈されてしまうので、代わりに \x21 を得るために from_hex_u8 という自作の関数を適用します。 IPv6 アドレスのパースには先程作成した IPv6 パーサーがそのまま使えますね! さらに複雑なバイト列も、これまで書いてきたパーサーを組み合わせることでパースできます。 このように自作のパーサーを組み上げることで、パースできる対象が広がっていくのが面白いところです。 さて、ここまでは ASCII 文字列のバイト列を扱ってきましたが、それ以外のバイト列はどのように扱えばよいのでしょうか? tag の代わりに be_u8 などのパーサーが利用できます。 図 3-6 は ECHONET Lite プロトコルのパケットです。 OPC が要求数で、後ろに何個要求が含まれるかを表します。 まずは OPC の値を読み取るために be_u8 を使います。次に要求をパースするのですが、 count というコンビネータが便利です。これは引数に与えたパーサーを指定の回数適用し、結果を Vec にまとめて返してくれるコンビネータです。 各要求のパーサーを parse_edata_property として作成しておいたので、あとは count と組み合わせるだけですね。 同じ要領で Wi-SUN モジュールの応答をパースできるパーサーを用意しました。 あとはこれらを alt コンビネータに渡せば完成です。 alt は複数のパーサーを受け取り、最初に成功したパーサーを適用した結果を返すコンビネータです。 ということで、できました。 ソースコードは以下のページに置いてあります。 Raspberry Pi の OS 設定や配線、 Grafana Agent の設定方法も書いてあるので、よければ参考にしてみてください。 Grafana Agent は Exporter を Scrape して外部にメトリクスを送信してくれるエージェントです。 私の環境では、 Granafa Cloud に向けてメトリクスを送信するように Grafana Agent を設定してみました。 一日の消費電力を時系列で測定してみた結果 Grafana Cloud で作成したダッシュボードはこのような見た目になります。 この日は 10:30 頃にゲーミング PC の電源を入れたようです。他の家電製品は触っていないので、おそらくゲーミング PC のアイドル時の消費電力は 250 W 程度なのでしょう。 13:30 ごろに電子レンジを使って昼食を温めていたようです。 電子レンジの出力は 700 W のはずですが、ダッシュボードをみるに 1400 W 近く消費しているようです。 本当に 1400 W も消費しているのか疑わしいのでワットメーターを購入して検証してみたいところです。 消費電力がスパイクしている 21:00 ごろは、おそらく夕食を温めていたのでしょう。 夜間は重めの 3D ゲームで遊んでおり、 450 W ほど消費電力が増えています。 アイドル時の消費電力は 250W ほどだったので、このときのゲーミング PC は 700 W 消費している計算になるのです。 さて、ゲーミング PC のアイドル時の消費電力を 200 W として 24 時間稼働させたときの月当たりの消費電力は以下のようになります。 ここに、クッキー工場の経営者として不都合な真実が浮かび上がります。 ゲーミング PC を 24 時間くらいつけっぱなしにすると、月 144 kWh くらい消費しており、月間の消費電力の半分近くを占める計算です。 対策として、 Intel N100 チップを搭載したミニ PC を購入し、お財布及び環境に配慮した形でクッキーを焼くようにしました。 まとめ 今回の発表のまとめです。 今では自宅に設置されている電力計はスマートメーターという通信機能がついたものに置き換わっています。 B ルートという仕組みを使うことで、一般人でもスマートメーターから瞬間消費電力などの情報を取り出すことができます。 Wi−SUN モジュールとおしゃべりしたいときなど、なにかをパースしなければならないときはパーサーコンビネータのフレームワークを使ってみるものいいでしょう。 パーサーコンビネータは、小さいパーサーを組み合わせることで目的のテキストやバイナリ列をパースできるようにするボトムアップ的なアプローチをとるものでした。 Rust では nom が有名なので、検討してみるとよいでしょう。 最後に、クッキーを焼くときは電気代に注意し、 CPS (Cookie per Second) だけではなく CPW (Cookie per Watt) にも気を配るようにしましょう。 参考資料 B ルートの話 https://qiita.com/rukihena/items/82266ed3a43e4b652adb http://myama808.net/archives/16196021.html https://kitto-yakudatsu.com/archives/7206 https://rabbit-note.com/2016/12/25/bp35a1-python/ parser の話 https://hazm.at/mox/lang/rust/nom/index.html https://docs.rs/nom/7.1.3/nom/ https://shigoto.mhlw.go.jp/User/Occupation/Detail/74 にて仕事の内容が動画付きで紹介されている。 ↩ https://qiita.com/rukihena/items/82266ed3a43e4b652adb ↩
この記事は NTTコミュニケーションズ Advent Calendar 2023 の5日目の記事です。 こんにちは、イノベーションセンター所属の岩瀬( @iwashi86 )です。普段は生成AIチームのエンジニアリングマネジメントをしています。 この記事では「組織の遠心力」をテーマに組織を強くする方法について書いていきます。本記事を読むことで、組織改善策の一案が得られることを狙っています。 なお、本記事は一人のエンジニアリングマネージャーである @iwashi86 の主観を多く含みます。NTT Com内には多くの考え方があり、その1つとして受け取っていただければ幸いです。 組織の遠心力って何だろう? 同じ組織の @mizuman_ が社内講演した「最強のチームが最高のプロダクトを作る」というスライドがあります。 詳細は上記スライドをぜひご覧いただければと思いますが、チームが良ければ良いほど、プロダクト(やソリューション)の成功率が高まります。最強のチームを作るために、優秀なメンバーがチームに残り続けてくれる必要があります。(チームからメンバーが抜け続けていては、コミュニケーションの文脈が失われ続けるため、効果的に働くことが難しくなるため) 一方で厄介なことに組織には遠心力が働きます。遠心力とはその名の通りで、中心から外側に向けて遠ざかる力です。中心が会社そのものだとすると、個人が外側(つまり会社の外)に引っ張られる力を本記事では、「組織の遠心力」と呼んでいます。この遠心力が一定の(個人ごとに異なる)閾値を超えると、メンバーの離職につながります。 この遠心力はさまざまなレイヤーで存在します。すなわち、会社全体・組織(たとえば、部や部門)・チームといったレイヤーです。それぞれのレイヤーで異なる遠心力が働きます。例えば、チーム自体には愛着があるが、会社全体に対してはあまりエンゲージメントを感じない、といったようにレイヤーによって力の強さが異なります。 この各レイヤーの遠心力が大きくなりすぎると良くない結果(たとえばメンバーの離職など)につながります。さて、よくない結果を止めるためにはどうしたらいいのでしょうか? 私たちも究極な絶対解を持っている訳ではありません。ですが少なくとも、私たちの会社(NTTコミュニケーションズ)、組織(イノベーションセンター)ではこうしています、という取り組みがあります。以降では、遠心力の発生事由を考察した後に、遠心力を抑える取り組みについて紹介します。 なぜ、遠心力が生まれるのだろう? 遠心力が生まれる原因は、企業や組織によって大きく異なります。ここでは、典型的・ありがちな例を組織から見た内部要因と外部要因の2つに分けて紹介します。(NTT Comの例というよりは、一般論です) まず、組織の内部要因で言えば、次のような例があります。 組織のゴールがよくわからない そもそも、ゴールを理解するためのコミュニケーションが存在しない 入社前はフルリモートと聞いていたが、方針変更で出社強制となった 次に組織の外部要因で言えば、次のような例があります。 仕事以外のプライベートで、勤務可能環境が変わった SNSで見る情報から隣の芝生が青い(他社の環境が素晴らしく見える) ある個人の興味分野が全く異なったものに変わった 状況のように遠心力は内部要因と外部要因でさまざまな理由から生まれます。 内部要因・外部要因へのアプローチのスタンス では、遠心力を抑えるためにはどちらの要因にアプローチすれば良いのでしょうか? 結論から言えば、外部要因ではなく内部要因に集中してアプローチします。なぜ外部要因に対するアプローチはうまくいかないのか一例を挙げてみましょう。 外部的な要因は組織外にあるため、組織からは原則アンコントローラブルな領域です。無理やりコントロールしようとしても結果的にうまくいきません。 たとえば、アンチパターンの1つではありますが、仮に「隣の芝生の青さを見せないために、外部の勉強会に参加禁止」みたいなルールを設けたとしましょう。このルールが適用されると、外部勉強会の参加に価値を置いているメンバーのエンゲージメントがだだ下がりになります。勉強会の情報は、ソーシャルメディアで簡単に入手可能ですし、各種サービスのレコメンドアルゴリズムに自然と情報に触れてしまうことも多いでしょう。ルールで縛ったとしても、意味がないどころか悪影響なのです。 (補足:NTT Comは積極的に外に出ようというカルチャーがあります。特に、私の所属するイノベーションセンターには「枠を超えよう」という組織バリューがあり、社内外問わず、どんどん外に出るのが是というスタンスです) 内部要因へのアプローチ 外部要因へのアプローチの難しさがわかったので、内部要因へアプローチしていくことになります。以下で、企業・組織・チームのレイヤーごとのアプローチの具体事例を紹介していきます。 企業レイヤーでのアプローチ 企業全体のアプローチの一例としては、幹部と社員の対話会があります。NTT Comでも例に漏れず、 KURUMAZA.exe という幹部対話会を開催しています(リンク資料 P5~6参照)。幹部と直接対話することで、疑問に思っていた内容を解消できるので、自分の業務に納得感を得やすくなります。 なお、このKURUMAZA.exeは現在、リモート開催がメインですが、当初は車座になって幹部と話し合うという場をオフラインで作っていました。KURUMAZA で EXEcutive(幹部) と話し合うことから、KURUMAZA.exe というネーミングだったわけです。 すでに3年以上開催しており、このイベントの開催方法については @Mahito が 別記事 で説明しておりますので、ご興味あればぜひご覧ください。 組織レイヤーでのアプローチ 私の所属するイノベーションセンターでは、組織内のメンバーであれば誰でも参加できるIC酒場というものを開催しています。 元々はこの前身には、「 ICカタリバ 」というオンライン座談会がありました。新型コロナウイルス感染症の5類感染症移行に伴って、対面のイベントを開催できることになったので、試しにお酒もあり(飲まなくてもかまいません)な場を作ってはどうか、という案で始まったものです。終業後の時間帯から、任意で人が集まってワイワイしてチームの壁を越えてネットワークが形成されています。組織に所属する人を知り、話すことで組織への帰属感を高まります。(=遠心力の軽減につながります。) その他、業務面での戦略的なアプローチとしては、部門やプロジェクト横断でチームやプロジェクトを組成するというアプローチがあります。新しい人やチームに働きかけにいくというのは、どちらかというとハードルが高いと感じる人が多い印象です。何らかの理由があって話す機会があれば話せるのですが、そもそも理由がない状態から突撃するのは、一定の難易度があります。だから、業務である程度の強制力を持たせて、それを理由として使ってもらいます。すると意図的に組織の人的ネットワークを構築できるわけです。(マネージャー陣の腕の見せどころかもしれません) チームレイヤーでのアプローチ チームレベルで言えば、チームビルディングのアプローチがあります。チームビルディングに関しては、NTT Comで実際に使っているノウハウをまとめた チームビルディングハンドブック を公開しておりますので、詳細な方法はそちらをご確認ください。 その他、チーム単位でふりかえり(アジャイル開発でいうレトロスペクティブ)を実践する方法も有効です。ふりかえりの過程で自然と対話が生まれます。その対話を通じて、チームメンバー同士の相互理解が進み、チームの結束力が高まります。(=遠心力の軽減につながります。) 双方向と一方向のハイブリッド ここまで事例を含め、アプローチをいくつか紹介してきました。それら全てに共通するのは「双方向の対話」です。実際に双方向で話し合う機会を作ることで、遠心力を軽減できます。 もちろん対話以外にも、ドキュメントを使って考えを発信するような一方向のコミュニケーションもあります。発信が増えることで、組織内の情報の透明性が高まります。その結果、各メンバーの業務の背景理解につながり、業務の納得感の醸成につながるわけです。 したがって、組織の状態に応じてどちらも利用することで高い効果が得られます。 おわりに 本記事では組織の遠心力・その発生理由・私たちの組織で取り組む対応策についてご紹介しました。1つでも使ってみたいと思うネタが見つかれば幸いです。 それでは、明日の記事もお楽しみに!
この記事は、 NTT Communications Advent Calendar 2023 4日目の記事です。 この記事では、Web標準の仕様と実際のブラウザの挙動についての体験談を紹介します。 W3C(World Wide Web Consortium) は Web Standards というWebの標準仕様を制定しています。 この中でブラウザのWeb APIの挙動についても定義されています。 挙動が統一されていないなら別ですが、長く使われ標準化もされている技術において、すべてのモダンブラウザ 1 で挙動が同じ場合、それが仕様化された動作だと思うでしょう。 しかし、実際にはすべてのブラウザが同じ仕様違反をしているという例を WebRTC 2 で用いられる RTCPeerConnection を用いて説明します。 SDP 3 に手を入れているような開発者の方には特に興味深いかもしれません。 目次 目次 はじめに 経緯 WebRTCとSDPの補足 差分と仕様上の問題点 ブラウザの仕様違反 歴史的経緯 余談: 実際の仕様変更の過程について まとめ はじめに こんにちは、イノベーションセンター テクノロジー部門の池田です。 普段は SkyWay に関連して WebRTC やその次世代となる技術の調査や検証をしています。 この記事では、Web標準の仕様と実際の全モダンブラウザの挙動の違いについて、気付いた経緯や歴史的経緯などを紹介します。 経緯 気になったタイミングは読書会の中で WebRTC 1.0 APIの仕様 を読んでいて、その中でも 4.4.2 Interface Definition を読んでいる時でした。 そこで何度か読み直しても自身の知っているブラウザの挙動と仕様の手順が異なっていることに気づきました。 具体的には以下のようなコードの場合です。 const pc = new RTCPeerConnection(); const offer = await pc.createOffer(); offer.sdp = offer.sdp.replace( '<対象の文字列>' , '<置換後の文字列>' ); // sdpの更新 await pc.setLocalDescription(offer); // 仕様上InvalidModificationErrorになるはず 上のコードは3行目を除けば WebRTC を使う場合にブラウザで実行される一般的なコードです。 4 3行目の操作は SDP の文字列を書き換える処理 5 で、必要に応じて置換する文字列を変えます。 上のコードでの置換には意味はないですが、実際のアプリケーションで操作する際はAPIで制御できないようなところまで変更を加えたい際にこの手法が利用されます。 例えば 以前書いたLyraの利用の記事 でもLyraを使うための下準備として SDP を直接変更しています。 WebRTCとSDPの補足 WebRTC について知っている方は読み飛ばしてください。 ブラウザで WebRTC を用いた映像などのデータを送受信可能にするには事前にそれを可能にする情報を交換しておく必要があります。 SDP はこの情報交換のために利用されます。 各ブラウザはJavaScriptのAPIを使うことで自身の伝えるべき情報を SDP として準備し、情報交換に備えます。 ここに更なるカスタマイズをしたい場合には、上のコードのように SDP Munging をしたり、別のAPIで変更をしたりする必要があります。 差分と仕様上の問題点 上のコードは何が問題なのでしょうか? 2行目にある createOffer の final steps to create an offer のstep5には以下のようにあります。 Set the [[LastCreatedOffer]] internal slot to sdpString. これは [[LastCreatedOffer]] にこの関数で生成された SDP が格納されることを意味します。 そして、4行目にある setLocalDescription (以下sLD)のstep 4.2は以下のようにあります。 If type is "offer", and sdp is not the empty string and not equal to connection.[[LastCreatedOffer]], then return a promise rejected with a newly created InvalidModificationError and abort these steps. これは引数と [[LastCreatedOffer]] が異なる場合にRejectすることを指示しています。 しかし、前述のとおりこのコードは引数として異なる値を渡しているにも関わらず、全モダンブラウザでRejectされることなく動きます。 つまり、ブラウザの挙動と仕様が全く違います。 あまりにもその差が謎だったため、仕様を管理しているリポジトリに issueを作成 しました。 ブラウザの仕様違反 issue作成1時間程でいくつかコメントをいただきました。 その結果、仕様の読み取り方は正しく、ブラウザ側が揃って仕様違反であり、許可されていない操作であることが分かりました。 これは、以前は許可されていたが歴史的経緯によって禁止されたとのことでした。 歴史的経緯 昔はWebRTCのAPIがあまり存在せず、変更を加えるには SDP を修正する必要がありました。 SDP は何かしらのオブジェクトではなくただの文字列として記述されているため、 特定部分を変更するのが難しく、書き換える際も意図していない部分を書き換えないように気をつける必要があります。 また、行いたい変更を SDP の記法で書き下す必要があり、API以外に SDP のドメイン知識も求められることがより難易度を上げています。 例えば setCodecPreferences() というAPIの利用例の1つとして特定のコーデックのみを利用したい場合があります。これをAPIを用いずに手動で変更するには、 SDP の文字列から該当する部分を特定し、必要な部分だけを残すという処理を文字列の置換で行う必要があります。 その後WebRTCが発展すると、 MediaStreamTrack や RTCRtpTransceiver 単位での操作をするAPIが生まれ、より細かい修正をAPIでできるようになってきました。 APIの一例と先ほど出てきた setCodecPreferences() が挙げられています。 API経由での操作だと SDP を扱わなくてもやりたいことができるようになるため、より WebRTC の開発が容易になると思います。 このようにAPIで操作する/できるようになったため、仕様上では SDP の直接の変更が禁止されたのだと思います。 しかし、上に書いたLyraのケースのように、APIでは設定できない事項もまだ存在します。 そのため、WebRTC 1.0 APIを拡張する 拡張ユースケース などがAPIをさらに充実させ、APIの範囲を広げることが必要と感じました。 一方、今回紹介した setCodecPreferences() ですら下図のように現在は Firefoxで利用できない ため実際にAPIのみですべてが完了する世界はまだ遠いと感じます。 このような状態で SDP の変更が禁止されるとできることが制限されてしまうため、APIを補うために仕様上禁止されていても実際には変更を許容するのは仕方ないのかなと思います。 https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpTransceiver/setCodecPreferences より 余談: 実際の仕様変更の過程について 実際にいつ頃に SDP の変更が禁止されたのかをGitHubにあるリポジトリのコミットを追って調査しました。 細かい文言や章編成の変更コミットなどがあり、変更を追うのが大変でしたが、 変更が提案されたのは 2016/11のissue で、 実際に変更されたのは 2017/02のPR でした。 この変更は IETF97のスライド 内の Option D によるものらしいです。 この中で setLocalDescription() の引数は後方互換性のためとありますが、結果的には変更したSDPを適用するために未だに使われています。 まとめ 本記事ではWeb標準の仕様と実際のブラウザの挙動についての体験談を紹介しました。 ブラウザの統一された挙動が仕様通りとは限らないということが伝わったのではないかと思います。 逆に仕様を完全に理解しても、実際のブラウザの挙動が想定できるとは限らないのは辛い点だと感じました。 明日もお楽しみに。 ここではGoogle Chrome/Firefox/Safari/Edgeを指します。 ↩ とてもざっくりと説明するとブラウザ間で直接映像を送ることができる技術です。 ↩ 利用するIPアドレスやコーデックなどメディア送受信に必要な情報を交換するために用いるテキストデータです。 ↩ SkyWayではこの辺りのAPIがわからなくても利用できるようなラッパーになっています。 ↩ SDP Mungingとも呼ばれます。 ↩
この記事は、 NTT Communications Advent Calendar 2023 3日目の記事です。 はじめに みなさんこんにちは、イノベーションセンターの益本 (@masaomi346) です。 Network Analytics for Security (以下、NA4Sec) プロジェクトのメンバーとして、脅威インテリジェンス(潜在的な脅威について収集されたデータを収集・分析したもの)の分析業務をしています。 本記事では、日本を狙ったフィッシングサイトの情報配信をはじめたことについて紹介します。 セキュリティにおける情報配信について興味がある方、フィッシングについて興味がある方は、ぜひ最後まで読んでみてください。 NA4Secについて NA4Secは、「NTTはインターネットを安心・安全にする社会的責務がある」を理念として、インターネットにおける攻撃インフラの解明・撲滅を目指した活動をしているプロジェクトです。 また、NTT Comグループにおける脅威インテリジェンスチームとしての側面も持ち合わせており、有事において脅威インテリジェンスを提供し、意思決定を支援することもあります。 NTTセキュリティ・ジャパンやエヌ・エフ・ラボラトリーズからもメンバーが参画しており、いろんな人が協力して、攻撃インフラを追跡しています。 本記事で紹介する内容は、NA4Secメンバーが過去に投稿した記事と関連しているので、ぜひ読んでみてください。 サイバー脅威インテリジェンス(CTI)配信はじめました Metemcyberについて Metemcyberは、食生活改善のような、セキュリティ運用の健全化を提供することを目標として活動しているプロジェクトです。 良質な脅威インテリジェンスをブロックチェーン上で流通させるための、脅威インテリジェンス流通基盤を開発しています。 プロジェクトの活動の詳細については、こちらのインターンシップの記事でも紹介しています。 インターンシップ体験記 〜セキュリティ運用の健全化を目指すMetemcyberの開発〜 また、NA4Secと兄弟関係にあるプロジェクトであり、Xのアカウント (@Metemcyber) でNA4Secと協力して、脅威インテリジェンス配信をしています。 フィッシングサイトの情報配信をはじめた経緯 私がNA4Secに加入してから、フィッシングに関する脅威インテリジェンスの分析をしてきました。 フィッシングを行っている攻撃者やフィッシングキットの分析などをしていましたが、社内で閉じずに、外部向けに何かフィッシングの脅威インテリジェンスの配信もしていきたいと考えていました。 そこで、こちらのマルウェアの情報配信のときと同様、Metemcyberアカウントでのフィッシング情報配信をやっていくことになりました。 サイバー脅威インテリジェンス(CTI)配信はじめました より多くの脅威情報を配信しつづけることで、多くの人に活用してもらえるようなり、インターネットの安全に貢献できればという思いで始めました。 投稿の目的・実現したいこと フィッシングサイトの報告件数は年々増加しており、被害も増加しています。 フィッシングサイトの情報発信をしていくことで、サービス事業者などでフィッシング対策している担当者の方々に、活用してもらえるようになることを目的としています。 そして、フィッシング詐欺の被害の減少に少しでも貢献していくことで、NA4Secの理念でもある「NTTはインターネットを安心、安全にする社会的責務がある」を実現していきます。 実際に配信してみる 🚨⚡ #Phishing #フィッシング詐欺 #フィッシング (🇯🇵) Brand: #SMBC #三井住友 IP: 🌍 192.252.189[.]72 (ASN:AS64050) URL: 🎣 hxxps://www.eovmfheuk9810.com/ 🎣 hxxps://www.igiaplfel5936.com/ 🎣 hxxps://www.mtmckoycfqd190.com/ 🎣 hxxps://www.tkurmciuvdq150.com/ H/T to Team NA4Sec pic.twitter.com/plqwdwZpPk — Metemcyber (@Metemcyber) 2023年11月20日 配信内容は以下のようになっています。 フィッシングのハッシュタグ ターゲットになっているブランド名 IPアドレス URL フィッシングサイトのスクリーンショット 配信内容(投稿フォーマット)で工夫したこと 活用してもらいやすくするためには、必要な情報をぱっと見でわかりやすくまとめなくてはいけません。 どういった情報を記載するかについては、フィッシングサイトの情報配信をしている方々の投稿内容を参考にしました。 また、視認性を上げるために絵文字を使ったり、スクリーンショットを添付したりしています。 警告 → パトランプ(🚨)と雷(⚡️)の絵文字 国 → 国旗(🇯🇵)の絵文字 IPアドレス → 地球(🌍)の絵文字 URL → 釣竿(🎣)の絵文字 今後の取り組みとフィードバックについて フィッシングサイトの情報配信で得られた知見を使って、新たなインテリジェンスを生み出していきたいと思います。 まだまだ情報配信を始めたばかりであるため、改良しつつ、フィッシングサイトの情報配信を続けていきます。 そのためにも、みなさんの意見を取り入れていきたいと思っています。 発信内容やフォーマットについてフィードバックしたい方、これ以外に何かお話ししたいことがある方は、 公式アカウント (@Metemcyber) もしくは、私のアカウント (@masaomi346) までご連絡ください。 さいごに 本記事では、日本を狙ったフィッシングサイトの情報配信をはじめたことについて紹介しました。 今後もさまざまな脅威インテリジェンスを発信していきます。 公式アカウント (@Metemcyber) をフォローしていただけると幸いです。 宣伝 NA4Sec/Metemcyberチームではそれぞれ一緒に働く仲間を募集しています。 脅威インテリジェンスに興味があり、プロジェクトの理念に共感していただける方は、ぜひ応募してみてください。 NA4Sec (Threat Intelligence Analyst / 脅威インテリジェンスアナリスト) Metemcyber (Threat Intelligence Engineer / 脅威インテリジェンスエンジニア) また、学生向けのインターンシップでは、以下のようなことを実施しました。 NA4Sec 攻撃者はいかにしてフィッシングサイトを隠すか?(インターンシップ体験記) インターンシップ体験記 〜Cobalt StrikeのC2サーバ追跡〜 Metemcyber インターンシップ体験記 〜セキュリティ運用の健全化を目指すMetemcyberの開発〜 学生の方で、NA4Sec/Metemcyberチームでのインターンシップに興味を持った方は、次回のインターンシップに参加してみてください。 最後まで、ご覧頂きありがとうございました! それでは、明日の記事もお楽しみに!
この記事は、 NTT Communications Advent Calendar 2023 3日目の記事です。 はじめに みなさんこんにちは、イノベーションセンターの益本 (@masaomi346) です。 Network Analytics for Security (以下、NA4Sec) プロジェクトのメンバーとして、脅威インテリジェンス(潜在的な脅威について収集されたデータを収集・分析したもの)の分析業務をしています。 本記事では、日本を狙ったフィッシングサイトの情報配信をはじめたことについて紹介します。 セキュリティにおける情報配信について興味がある方、フィッシングについて興味がある方は、ぜひ最後まで読んでみてください。 NA4Secについて NA4Secは、「NTTはインターネットを安心・安全にする社会的責務がある」を理念として、インターネットにおける攻撃インフラの解明・撲滅を目指した活動をしているプロジェクトです。 また、NTT Comグループにおける脅威インテリジェンスチームとしての側面も持ち合わせており、有事において脅威インテリジェンスを提供し、意思決定を支援することもあります。 NTTセキュリティ・ジャパンやエヌ・エフ・ラボラトリーズからもメンバーが参画しており、いろんな人が協力して、攻撃インフラを追跡しています。 本記事で紹介する内容は、NA4Secメンバーが過去に投稿した記事と関連しているので、ぜひ読んでみてください。 サイバー脅威インテリジェンス(CTI)配信はじめました Metemcyberについて Metemcyberは、食生活改善のような、セキュリティ運用の健全化を提供することを目標として活動しているプロジェクトです。 良質な脅威インテリジェンスをブロックチェーン上で流通させるための、脅威インテリジェンス流通基盤を開発しています。 プロジェクトの活動の詳細については、こちらのインターンシップの記事でも紹介しています。 インターンシップ体験記 〜セキュリティ運用の健全化を目指すMetemcyberの開発〜 また、NA4Secと兄弟関係にあるプロジェクトであり、Xのアカウント (@Metemcyber) でNA4Secと協力して、脅威インテリジェンス配信をしています。 フィッシングサイトの情報配信をはじめた経緯 私がNA4Secに加入してから、フィッシングに関する脅威インテリジェンスの分析をしてきました。 フィッシングを行っている攻撃者やフィッシングキットの分析などをしていましたが、社内で閉じずに、外部向けに何かフィッシングの脅威インテリジェンスの配信もしていきたいと考えていました。 そこで、こちらのマルウェアの情報配信のときと同様、Metemcyberアカウントでのフィッシング情報配信をやっていくことになりました。 サイバー脅威インテリジェンス(CTI)配信はじめました より多くの脅威情報を配信しつづけることで、多くの人に活用してもらえるようなり、インターネットの安全に貢献できればという思いで始めました。 投稿の目的・実現したいこと フィッシングサイトの報告件数は年々増加しており、被害も増加しています。 フィッシングサイトの情報発信をしていくことで、サービス事業者などでフィッシング対策している担当者の方々に、活用してもらえるようになることを目的としています。 そして、フィッシング詐欺の被害の減少に少しでも貢献していくことで、NA4Secの理念でもある「NTTはインターネットを安心、安全にする社会的責務がある」を実現していきます。 実際に配信してみる 🚨⚡ #Phishing #フィッシング詐欺 #フィッシング (🇯🇵) Brand: #SMBC #三井住友 IP: 🌍 192.252.189[.]72 (ASN:AS64050) URL: 🎣 hxxps://www.eovmfheuk9810.com/ 🎣 hxxps://www.igiaplfel5936.com/ 🎣 hxxps://www.mtmckoycfqd190.com/ 🎣 hxxps://www.tkurmciuvdq150.com/ H/T to Team NA4Sec pic.twitter.com/plqwdwZpPk — Metemcyber (@Metemcyber) 2023年11月20日 配信内容は以下のようになっています。 フィッシングのハッシュタグ ターゲットになっているブランド名 IPアドレス URL フィッシングサイトのスクリーンショット 配信内容(投稿フォーマット)で工夫したこと 活用してもらいやすくするためには、必要な情報をぱっと見でわかりやすくまとめなくてはいけません。 どういった情報を記載するかについては、フィッシングサイトの情報配信をしている方々の投稿内容を参考にしました。 また、視認性を上げるために絵文字を使ったり、スクリーンショットを添付したりしています。 警告 → パトランプ(🚨)と雷(⚡️)の絵文字 国 → 国旗(🇯🇵)の絵文字 IPアドレス → 地球(🌍)の絵文字 URL → 釣竿(🎣)の絵文字 今後の取り組みとフィードバックについて フィッシングサイトの情報配信で得られた知見を使って、新たなインテリジェンスを生み出していきたいと思います。 まだまだ情報配信を始めたばかりであるため、改良しつつ、フィッシングサイトの情報配信を続けていきます。 そのためにも、みなさんの意見を取り入れていきたいと思っています。 発信内容やフォーマットについてフィードバックしたい方、これ以外に何かお話ししたいことがある方は、 公式アカウント (@Metemcyber) もしくは、私のアカウント (@masaomi346) までご連絡ください。 さいごに 本記事では、日本を狙ったフィッシングサイトの情報配信をはじめたことについて紹介しました。 今後もさまざまな脅威インテリジェンスを発信していきます。 公式アカウント (@Metemcyber) をフォローしていただけると幸いです。 宣伝 NA4Sec/Metemcyberチームではそれぞれ一緒に働く仲間を募集しています。 脅威インテリジェンスに興味があり、プロジェクトの理念に共感していただける方は、ぜひ応募してみてください。 NA4Sec (Threat Intelligence Analyst / 脅威インテリジェンスアナリスト) Metemcyber (Threat Intelligence Engineer / 脅威インテリジェンスエンジニア) また、学生向けのインターンシップでは、以下のようなことを実施しました。 NA4Sec 攻撃者はいかにしてフィッシングサイトを隠すか?(インターンシップ体験記) インターンシップ体験記 〜Cobalt StrikeのC2サーバ追跡〜 Metemcyber インターンシップ体験記 〜セキュリティ運用の健全化を目指すMetemcyberの開発〜 学生の方で、NA4Sec/Metemcyberチームでのインターンシップに興味を持った方は、次回のインターンシップに参加してみてください。 最後まで、ご覧頂きありがとうございました! それでは、明日の記事もお楽しみに!
この記事は、  NTT Communications Advent Calendar 2023  2日目の記事です。 こんにちは、イノベーションセンターの坪井です。 1日目の記事を担当した平木と同じくNetwork Analytics for Securityというチーム(通称NA4Sec)に所属しています。 1日目の記事はこちらです。 https://engineers.ntt.com/entry/2023/12/01/102753 engineers.ntt.com NA4Secプロジェクトについては、  サイバー脅威インテリジェンス(CTI)配信はじめました  を読んでいただくと我々がどんな活動を行なっているかわかると思います。 先日の11/21(火)にInternet Week 2023の C10 DNS DAY というプログラムの中で「ランダムサブドメイン攻撃において事業者として行なった対策と解析について」というタイトルで講演をさせていただきました。 講演の中で、私はDNSハニーポットを運用してランダムサブドメイン攻撃を観測した話をさせていただいたのですが、アドベントカレンダー2日目の本記事では講演の中で出てきたサブドメイン列挙ツールについて、お話しします。 はじめに:免責事項 ランダムサブドメイン攻撃とは サブドメイン列挙とは SubBruteについて サブドメイン列挙とランダムサブドメイン攻撃 まとめ はじめに:免責事項 本記事は、あくまでも調査したサブドメイン列挙ツールの動きについてご紹介するものであり、利用を積極的に推奨するものではありません。また、これらのツールを悪意ある目的で使用することは禁止されています。 これらのツールを使用することによって発生したいかなる損害や問題について、ツール開発者や提供者及び本記事の執筆者・ブログ管理者は責任を負いません。予めご了承ください。 ランダムサブドメイン攻撃とは まず初めに、ランダムサブドメイン攻撃について簡単に説明します。 ランダムサブドメイン攻撃とはサイバー空間におけるDNSを使ったDDoS攻撃手法の1つです。 悪意のある攻撃者がランダムな文字列やパターンを使用して、特定のドメイン名に対して無差別にサブドメイン名を生成し、生成したサブドメイン名を使ってキャッシュDNSサーバに名前解決の問い合わせ(クエリ)をします。 キャッシュDNSサーバでは受けたクエリのドメイン名に対する情報をすでに保持している場合は保持している情報を返しますが、保持していない場合はそのドメイン名の権威DNSサーバにクエリをします 1 。 今回のようなランダムなサブドメイン名の場合、ほとんどのドメイン名についてはキャッシュDNSサーバには過去のクエリ応答情報が存在していないため、攻撃者からのクエリの都度、権威DNSサーバへクエリをすることになります。 一般的にランダムサブドメイン攻撃は、意図的に大量のDNSクエリを発生させて権威DNSサーバへ負荷をかけることで、各種リソースの逼迫にともなう関連サービスの品質低下またはサーバダウンなどによる関連サービスの停止(いわゆるDoS)を狙いとした攻撃と考えられています。 サブドメイン列挙とは 次に、今回のテーマであるサブドメイン列挙ツールが行う、サブドメイン列挙についてお話しします。 サブドメイン列挙(Subdomain Enumeration)とは、特定のドメインに関して利用されているサブドメイン 2 を調査し、そのドメイン名をリスト化するプロセスです。 サブドメイン列挙ツールは、セキュリティテストやネットワークスキャニング、ペネトレーションテスト、またはウェブアプリケーションのセキュリティ評価など、さまざまなセキュリティアクティビティで使用されます。 サブドメイン列挙ツールは以下のような手法を使用してサブドメインを収集します。 有名なサブドメイン列挙ツールについて、いくつか取り上げて比較してみました。 サブドメイン列挙ツールはどういう仕組みになっているか、今回はInternet Weekの講演の中でも名前が出たSubBruteについて中身を見ていきたいと思います。 SubBruteについて SubBruteはPythonで書かれたオープンソースのサブドメイン列挙ツールで主にブルートフォースを実行することに特化しています。DNS におけるブルートフォースは一般的なサブドメインの名前や文字列の組み合わせなどのパターンをさまざま試行する手法です。 指定されたドメインに対して複数の一般的なサブドメイン名を含むワードリストを使用し、DNSクエリを送信して有効なサブドメインを探索します。このプロセスにより、目標のドメインに紐づく未知のサブドメインを見つけることができます。 TheRook/subbrute: A DNS meta-query spider that enumerates DNS records, and subdomains. (github.com) SubBruteのプログラムを紐解いてみたところ、下記の機能を備えていることがわかりました。 なお、下記の機能名は処理内容から類推して付けた呼び名なので、実際の機能名とは関係ありません。 ターゲットドメイン情報取得機能 サブドメイン作成対象のターゲットドメインの権威DNSサーバを列挙し、応答性のテストを行い、正常応答の場合はDNSクエリ対象の権威DNSサーバ一覧に追加する。 サブドメインリスト生成機能 一般的に使用されるサブドメイン(wwwなど)や予め用意されたワードリストを使用して存在する可能性のあるサブドメインリストを生成し、DNSクエリをランダムな順序で行えるようにリストの順序をシャッフルさせる。権威DNSサーバがDNSクエリを異常とみなすことを防いだり、1つの権威DNSサーバに対するDNSクエリが集中することを避けることが目的と思われる。 DNSクエリ実行機能 サブドメインリスト生成機能で生成されたリストを元にDNSクエリを行う。この機能はマルチプロセスで実行され、大量のDNSクエリを並列で実行する。 DNSクエリ結果解析機能 DNSクエリ実行機能の結果から必要な情報を取り出し解析・整形する。 Aレコードの場合はドメイン名に対応するIPv4アドレスを、AAAAレコードの場合はIPv6アドレスを取り出す。CNAMEレコードからは別名となるドメイン名を取り出す。 取り出した結果をドメイン名と対応するIPアドレスで組み合わせた形に整形する。 例として example.jp をターゲットドメインとしてSubBruteを実行すると以下のような動きになります。 $ python subbrute.py example.jp メイン機能で example.jp をターゲットのドメインとして設定する。 ターゲットドメイン情報取得機能で指定されたターゲットドメイン  example.jp  のDNSクエリに応答する権威DNSサーバーのリストを生成する。 サブドメインリスト生成機能で、ターゲットドメイン  example.jp  のすべての可能性のあるサブドメインのリストを作成しリスト内をシャッフル。 DNSクエリ実行機能で、 example.jp  の各サブドメインに対するDNSクエリを並列で実行する。 DNSクエリ結果解析機能で、DNSクエリ実行機能の結果を解析・整形する。例えば  mail.example.jp www.example.jp  などの  example.jp  のサブドメイン名と、サブドメイン名に対応するIPアドレスを出力する。 以上の流れにより、 example.jp  のサブドメインと、それらのドメイン名に紐づくIPアドレスなどの情報を取得できます。 このようにワンライナーのコマンドでサブドメイン列挙ツールを実行するだけでサブドメイン列挙行為自体はとても簡単に行うことができます。 サブドメイン列挙とランダムサブドメイン攻撃 さて、今回はSubBruteの動作を紐解いてみました。SubBruteの動作の中では大量のサブドメイン候補からなるリストを生成し、DNSクエリを行っています。 「SubBruteについて」内では説明を省略していますが、SubBruteがDNSクエリを実行する際は、予め設定されているキャッシュDNSサーバを使用します。 キャッシュDNSサーバへクエリした時に、キャッシュDNSサーバがクエリ応答情報を保持していないサブドメインだった場合、権威DNSサーバにクエリを行うことになります。 あれ、これ何かに似てませんか? そう、冒頭で説明したランダムサブドメイン攻撃の構造に似ているんです。 サブドメイン列挙ツールはセキュリティテストやネットワークスキャニング、ペネトレーションテスト、またはウェブアプリケーションのセキュリティ評価といったシチュエーションでの利用を想定して開発されたツールですが、その動作過程上でDNSクエリを大量発生させ、ランダムサブドメイン攻撃に似た状況を作り出してしまうことがあるため、ツールの取り扱いには十分注意するようにしましょう。 まとめ 今回はInternet Weekで登壇させていただいた講演内容からサブドメイン列挙ツールについて深掘りして書いてみました。改めて、サブドメイン列挙とランダムサブドメイン攻撃は紙一重と感じました。最後まで、ご覧頂きありがとうございました。 明日も同じくNetwork Analytics for Securityチームに所属する益本の記事です!それでは、明日の記事もお楽しみに! 厳密にはいきなり目的の権威DNSサーバにクエリをする挙動をとるとは限りませんが、最終的には何らかの形で権威DNSサーバへのクエリにつながるので、ここでは簡単のためにこのように表現しています。 ↩ あるドメインの中の部分的な名前空間のことで、そのドメインに関連する特定のサービス(ウェブサイトやメールなど)を提供するサーバを識別するためなどに使われる。例えば「www」はウェブサーバを指す名前としてよく使われる。 ↩
この記事は、  NTT Communications Advent Calendar 2023  2日目の記事です。 こんにちは、イノベーションセンターの坪井です。 1日目の記事を担当した平木と同じくNetwork Analytics for Securityというチーム(通称NA4Sec)に所属しています。 1日目の記事はこちらです。 engineers.ntt.com NA4Secプロジェクトについては、  サイバー脅威インテリジェンス(CTI)配信はじめました  を読んでいただくと我々がどんな活動を行なっているかわかると思います。 先日の11/21(火)にInternet Week 2023の C10 DNS DAY というプログラムの中で「ランダムサブドメイン攻撃において事業者として行なった対策と解析について」というタイトルで講演をさせていただきました。 講演の中で、私はDNSハニーポットを運用してランダムサブドメイン攻撃を観測した話をさせていただいたのですが、アドベントカレンダー2日目の本記事では講演の中で出てきたサブドメイン列挙ツールについて、お話しします。 はじめに:免責事項 ランダムサブドメイン攻撃とは サブドメイン列挙とは SubBruteについて サブドメイン列挙とランダムサブドメイン攻撃 まとめ はじめに:免責事項 本記事は、あくまでも調査したサブドメイン列挙ツールの動きについてご紹介するものであり、利用を積極的に推奨するものではありません。また、これらのツールを悪意ある目的で使用することは禁止されています。 これらのツールを使用することによって発生したいかなる損害や問題について、ツール開発者や提供者及び本記事の執筆者・ブログ管理者は責任を負いません。予めご了承ください。 ランダムサブドメイン攻撃とは まず初めに、ランダムサブドメイン攻撃について簡単に説明します。 ランダムサブドメイン攻撃とはサイバー空間におけるDNSを使ったDDoS攻撃手法の1つです。 悪意のある攻撃者がランダムな文字列やパターンを使用して、特定のドメイン名に対して無差別にサブドメイン名を生成し、生成したサブドメイン名を使ってキャッシュDNSサーバに名前解決の問い合わせ(クエリ)をします。 キャッシュDNSサーバでは受けたクエリのドメイン名に対する情報をすでに保持している場合は保持している情報を返しますが、保持していない場合はそのドメイン名の権威DNSサーバにクエリをします 1 。 今回のようなランダムなサブドメイン名の場合、ほとんどのドメイン名についてはキャッシュDNSサーバには過去のクエリ応答情報が存在していないため、攻撃者からのクエリの都度、権威DNSサーバへクエリをすることになります。 一般的にランダムサブドメイン攻撃は、意図的に大量のDNSクエリを発生させて権威DNSサーバへ負荷をかけることで、各種リソースの逼迫にともなう関連サービスの品質低下またはサーバダウンなどによる関連サービスの停止(いわゆるDoS)を狙いとした攻撃と考えられています。 サブドメイン列挙とは 次に、今回のテーマであるサブドメイン列挙ツールが行う、サブドメイン列挙についてお話しします。 サブドメイン列挙(Subdomain Enumeration)とは、特定のドメインに関して利用されているサブドメイン 2 を調査し、そのドメイン名をリスト化するプロセスです。 サブドメイン列挙ツールは、セキュリティテストやネットワークスキャニング、ペネトレーションテスト、またはウェブアプリケーションのセキュリティ評価など、さまざまなセキュリティアクティビティで使用されます。 サブドメイン列挙ツールは以下のような手法を使用してサブドメインを収集します。 有名なサブドメイン列挙ツールについて、いくつか取り上げて比較してみました。 サブドメイン列挙ツールはどういう仕組みになっているか、今回はInternet Weekの講演の中でも名前が出たSubBruteについて中身を見ていきたいと思います。 SubBruteについて SubBruteはPythonで書かれたオープンソースのサブドメイン列挙ツールで主にブルートフォースを実行することに特化しています。DNS におけるブルートフォースは一般的なサブドメインの名前や文字列の組み合わせなどのパターンをさまざま試行する手法です。 指定されたドメインに対して複数の一般的なサブドメイン名を含むワードリストを使用し、DNSクエリを送信して有効なサブドメインを探索します。このプロセスにより、目標のドメインに紐づく未知のサブドメインを見つけることができます。 TheRook/subbrute: A DNS meta-query spider that enumerates DNS records, and subdomains. (github.com) SubBruteのプログラムを紐解いてみたところ、下記の機能を備えていることがわかりました。 なお、下記の機能名は処理内容から類推して付けた呼び名なので、実際の機能名とは関係ありません。 ターゲットドメイン情報取得機能 サブドメイン作成対象のターゲットドメインの権威DNSサーバを列挙し、応答性のテストを行い、正常応答の場合はDNSクエリ対象の権威DNSサーバ一覧に追加する。 サブドメインリスト生成機能 一般的に使用されるサブドメイン(wwwなど)や予め用意されたワードリストを使用して存在する可能性のあるサブドメインリストを生成し、DNSクエリをランダムな順序で行えるようにリストの順序をシャッフルさせる。権威DNSサーバがDNSクエリを異常とみなすことを防いだり、1つの権威DNSサーバに対するDNSクエリが集中することを避けることが目的と思われる。 DNSクエリ実行機能 サブドメインリスト生成機能で生成されたリストを元にDNSクエリを行う。この機能はマルチプロセスで実行され、大量のDNSクエリを並列で実行する。 DNSクエリ結果解析機能 DNSクエリ実行機能の結果から必要な情報を取り出し解析・整形する。 Aレコードの場合はドメイン名に対応するIPv4アドレスを、AAAAレコードの場合はIPv6アドレスを取り出す。CNAMEレコードからは別名となるドメイン名を取り出す。 取り出した結果をドメイン名と対応するIPアドレスで組み合わせた形に整形する。 例として example.jp をターゲットドメインとしてSubBruteを実行すると以下のような動きになります。 $ python subbrute.py example.jp メイン機能で example.jp をターゲットのドメインとして設定する。 ターゲットドメイン情報取得機能で指定されたターゲットドメイン  example.jp  のDNSクエリに応答する権威DNSサーバーのリストを生成する。 サブドメインリスト生成機能で、ターゲットドメイン  example.jp  のすべての可能性のあるサブドメインのリストを作成しリスト内をシャッフル。 DNSクエリ実行機能で、 example.jp  の各サブドメインに対するDNSクエリを並列で実行する。 DNSクエリ結果解析機能で、DNSクエリ実行機能の結果を解析・整形する。例えば  mail.example.jp www.example.jp  などの  example.jp  のサブドメイン名と、サブドメイン名に対応するIPアドレスを出力する。 以上の流れにより、 example.jp  のサブドメインと、それらのドメイン名に紐づくIPアドレスなどの情報を取得できます。 このようにワンライナーのコマンドでサブドメイン列挙ツールを実行するだけでサブドメイン列挙行為自体はとても簡単に行うことができます。 サブドメイン列挙とランダムサブドメイン攻撃 さて、今回はSubBruteの動作を紐解いてみました。SubBruteの動作の中では大量のサブドメイン候補からなるリストを生成し、DNSクエリを行っています。 「SubBruteについて」内では説明を省略していますが、SubBruteがDNSクエリを実行する際は、予め設定されているキャッシュDNSサーバを使用します。 キャッシュDNSサーバへクエリした時に、キャッシュDNSサーバがクエリ応答情報を保持していないサブドメインだった場合、権威DNSサーバにクエリを行うことになります。 あれ、これ何かに似てませんか? そう、冒頭で説明したランダムサブドメイン攻撃の構造に似ているんです。 サブドメイン列挙ツールはセキュリティテストやネットワークスキャニング、ペネトレーションテスト、またはウェブアプリケーションのセキュリティ評価といったシチュエーションでの利用を想定して開発されたツールですが、その動作過程上でDNSクエリを大量発生させ、ランダムサブドメイン攻撃に似た状況を作り出してしまうことがあるため、ツールの取り扱いには十分注意するようにしましょう。 まとめ 今回はInternet Weekで登壇させていただいた講演内容からサブドメイン列挙ツールについて深掘りして書いてみました。改めて、サブドメイン列挙とランダムサブドメイン攻撃は紙一重と感じました。最後まで、ご覧頂きありがとうございました。 明日も同じくNetwork Analytics for Securityチームに所属する益本の記事です!それでは、明日の記事もお楽しみに! 厳密にはいきなり目的の権威DNSサーバにクエリをする挙動をとるとは限りませんが、最終的には何らかの形で権威DNSサーバへのクエリにつながるので、ここでは簡単のためにこのように表現しています。 ↩ あるドメインの中の部分的な名前空間のことで、そのドメインに関連する特定のサービス(ウェブサイトやメールなど)を提供するサーバを識別するためなどに使われる。例えば「www」はウェブサーバを指す名前としてよく使われる。 ↩