TECH PLAY

NTTドコモビジネス

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

602

この記事は、 NTT docomo Business Advent Calendar 2025 7日目の記事です。 こんにちは。イノベーションセンターの加藤です。普段はコンピュータビジョンの技術開発やAI/機械学習(ML)システムの検証に取り組んでいます。 ディープラーニングの実装をしているときに、変数のshapeを管理するのはなかなか大変です。いつのまにか次元が増えていたり、想定外のshapeがやってきたりして実行時に落ちてしまった!というのは日常茶飯事だと思います。 こういった問題に対して静的解析で何とかできないかと試行錯誤した結果を共有します。 mypyプラグインを使うモチベーション mypyプラグインの作成 初期化 jaxtyping annotationを拾う テンソル作成関数を拾う テンソル計算 ここで限界が来た(Future work) 変数付きのshape記法 次元の四則演算 stubの是非 レイヤーの型注釈 まとめ mypyプラグインを使うモチベーション Pythonのプログラムを静的検査する方法のひとつに mypy があります。これはソースコードにつけられた型アノテーションに矛盾がないか調べてくれるもので、変な代入や演算由来のエラーを未然に防ぐことができます。 しかしながら、NumPyやPyTorchなどの一般的な数値計算ライブラリにはそれなりの型がアノテーションされているものの、せいぜいテンソルの型(intやfloatなど)どまりで次元(shape)については考慮されていないため、そのままでは次元の不一致などを検出できません。 jaxtyping などのライブラリは元の型を拡張して次元などをアノテーションできるようにしてくれますが、これらは実行時解析のみをサポートしており、mypyからは扱えません。 from torch import Tensor import torch from jaxtyping import Float32, jaxtyped from beartype import beartype as typechecker from typing_extensions import reveal_type @ jaxtyped (typechecker=typechecker) def f (x: Float32[Tensor, "1 224 224" ]) -> Float32[Tensor, "1 1000" ]: print ( "processing f" ) w = torch.randn( 1000 , 224 * 224 ) x_flat = x.view( 1 , 224 * 224 ) y = x_flat @ w.t() return y.view( 1 , 1000 ) x: Float32[Tensor, "1 224 224" ] = torch.randn( 1 , 224 , 224 ) y = torch.randn( 1 , 224 , 225 ) print ( "f(x)" ) reveal_type(f(x)) # OK print ( "f(y)" ) reveal_type(f(y)) # NG """ 実行時は引数に誤ったshapeを渡した時点でエラー > python .\example.py f(x) processing f Runtime type is 'Tensor' f(y) Traceback (most recent call last): ... しかしmypyでは検出できない > mypy .\example.py example.py:19: note: Revealed type is "torch._tensor.Tensor" example.py:20: note: Revealed type is "torch._tensor.Tensor" Success: no issues found in 1 source file """ 結局プログラミングの段階ではあくまで可読性を高めるための注釈に留まり、実行時はお祈りしながら終了を待つことになります。 そこで本稿ではmypyプラグインを実装してjaxtypingの型に対する処理を追加することで、次元の整合性を実行前に検証できないかトライしてみました。もしこれができれば、mypyを使って次元込みの静的検査ができ、Visual Studio Codeのmypy拡張と連携すればプログラミング中もテンソルの次元を追うことができるようになります。 mypyプラグインの作成 初期化 uv でプロジェクトを新規作成します。 $ uv init --name jaxmy --lib Initialized project `jaxmy` $ uv add mypy jaxtyping $ uv add torch numpy pytest --optional tests src/jaxmy/mypy_plugin.py にプラグインスクリプトを作成します。 from typing import Any, Optional, List, Tuple import re from mypy.plugin import Plugin class ShapePlugin (Plugin): pass # TODO def plugin (version: str ): print ( "Hello world! version:" , version) return ShapePlugin そしてmypy実行時に自作のpluginを紐づけるには以下のようにpyprojectを編集します。 [build-system] requires = [ "hatchling" ] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = [ "src/jaxmy" ] [tool.mypy] ignore_missing_imports = true plugins = [ "jaxmy.mypy_plugin" ] mypy_path = "$MYPY_CONFIG_FILE_DIR/src/jaxmy/stubs" (mypy_pathについては後述) これでuv環境からmypyを実行するとpluginが介入するようになります。 > uv run mypy example.py Hello world! version: 1 . 18 . 2 jaxtyping annotationを拾う まずはjaxtyping記法によるアノテーションを拾うところから始めます。 jaxtypingが提供する型の実体はmypyなどの静的解析時( typing.TYPE_CHECKING == True )と実行時で異なっており、静的解析時は jaxtyping._indirection 内で定義された以下のコードが読み込まれます。 from typing import ( Annotated as BFloat16, # noqa: F401 Annotated as Bool, # noqa: F401 Annotated as Complex, # noqa: F401 Annotated as Complex64, # noqa: F401 Annotated as Complex128, # noqa: F401 Annotated as Float, # noqa: F401 ... そのためあらゆる型は Annotated[T, ...] とみなされ、これは事前に T と解決してから静的解析が走ります。 これによってjaxtyping記法が型として正しくない表記であるにもかかわらずエディタやmypyのチェックをすり抜けているのですが、 Float32[Tensor, "1 224 224"] や Int8[Tensor, "3 224 224"] などがすべて Tensor という同じ型に置き換えられてしまうため静的解析が不可能になります。 そこで、stubを注入して呼び出しを捕捉することでプラグインから触れるようにします。 # stub/jaxtyping/__init__.pyi from typing import Any, Literal, NoReturn, Union, TypeVar, Generic _ArrayType = TypeVar( "_ArrayType" ) _Shape = TypeVar( "_Shape" ) class AbstractArray (Generic[_ArrayType, _Shape]): pass class UInt2 (AbstractArray[_ArrayType, _Shape]): ... class UInt4 (AbstractArray[_ArrayType, _Shape]): ... class UInt8 (AbstractArray[_ArrayType, _Shape]): ... """以下よしなに""" これでプラグインからは get_type_analyze_hook を通して jaxtyping.Float32 などのアノテーションを拾えるようになりました。ただしjaxtyping記法はshapeの部分が型注釈として許されない文字列リテラルであるため、これを有効な型に置き換える必要があります。これを怠るとmypyがshape部分を Any に置き換えてしまいます。 from typing import Any, Optional, List, Tuple import re from mypy.plugin import Plugin, FunctionContext, AnalyzeTypeContext from mypy.types import Instance, TupleType, Type, UnboundType, LiteralType, EllipsisType, RawExpressionType, TypeStrVisitor from mypy.checker import TypeChecker def parse_dimstr (dimstr: str ) -> Optional[List[ int ]]: """Parse a dimension string like "1 3 224 224" into a list of int.""" dims: List[ int ] = [] for dim in dimstr.split( " " ): dim = dim.strip() if dim.isdigit(): dims.append( int (dim)) else : return None return dims def dump_dimlist (dimlist: List[ int ]) -> str : """Dump a list of int back into a dimension string.""" dimstrs: List[ str ] = [] for dim in dimlist: dimstrs.append( str (dim)) return " " .join(dimstrs) def construct_instance (api: TypeAnalyzerPluginInterface, dtype: str , backend: Type, dim_list: List[ int ]) -> Type: """Construct an Instance of a jaxtyping type with the given dtype, backend, and shape.""" # TODO : 本当はFloatなどのUnion型にも対応すべきだが、とりあえず保留 # shape表現をLiteralで包みjaxtypingのinstanceを返す。 return Instance( api.named_type(f "jaxtyping.{dtype}" ).type, [backend, LiteralType(value=dump_dimlist(dim_list), fallback=api.named_type( "builtins.str" ))] ) def analyze_jaxtyping (ctx: AnalyzeTypeContext) -> Type: """Parse Dtype[Array, "shape"] to the mypy-friendly type Dtype[Array, Literal["shape"]].""" typ = ctx.type # UnboundType. 何のことかはわからない if len (typ.args) != 2 : return typ backend, shape = typ.args backend = ctx.api.analyze_type(backend) # UnboundTypeなbackendを解決 (Tensorなどのinstanceになる) if not isinstance (shape, RawExpressionType) or type (shape.literal_value) is not str : return backend # fallback dtype = typ.name # e.g., "Float32" dim_str = shape.literal_value # e.g., "1 224 224" dim_list = parse_dimstr(dim_str) # validationもかねてパース if dim_list is None : return backend # fallback return construct_instance(ctx.api, dtype, backend, dim_list) DTYPE_ANNOTS = { "UInt2" , "UInt4" , "UInt8" , ...} class ShapePlugin (Plugin): def get_type_analyze_hook (self, fullname: str ): m = re.match( r"jaxtyping\.(\w+)" , fullname) if m and m.group( 1 ) in DTYPE_ANNOTS: return analyze_jaxtyping def plugin (version: str ): return ShapePlugin 今回はjaxtypingのサブセットとして数値リテラルのみ(例: Float32[Tensor, "1 3 224 224"] )をサポートします。内部的には基本リテラルで持ち( Float32[Tensor, Literal["1 3 224 224"]] )、都度バラして型推論を行います。 ※ ちなみに Float32[Tensor, Literal["1 3 224 224"]] よりも取り回しのよい内部表現を使う手もありますが、mypyには検査対象のプログラムで呼ばれているモジュール(とビルトイン)しか扱えないという制約があります。そのため、何かいい感じのオリジナル型を導入したい場合はjaxtypingそのものを改造する必要があります。 テンソル作成関数を拾う これに加えて、 torch.zeros() などの初期化用の関数を get_function_hook によって捕捉し、これらのテンソルにjaxtyping用の型を付与します。 まずstubを作成してtorchを扱えるようにします。 # stubs/torch/__init__.pyi from torch._tensor import Tensor as Tensor from typing import Any def randn (*size: int , out= None , dtype= None , **kwargs) -> Tensor: ... def rand (*size: int , out= None , dtype= None , **kwargs) -> Tensor: ... def zeros (*size: int , out= None , dtype= None , **kwargs) -> Tensor: ... def ones (*size: int , out= None , dtype= None , **kwargs) -> Tensor: ... そしてhookを作成します。この手の関数は入力の自由度が高く、引数を手でパースするのがちょっと大変です。 INITIALIZER_NAMES = { "torch.randn" , "torch.rand" , "torch.zeros" , "torch.ones" , } dtype_mapper = { # mapping torch.dtype to jaxtyping type "float32" : "Float32" , "float" : "Float32" , "float64" : "Float64" , "double" : "Float64" , ... } def hook (fullname: str ): if fullname in INITIALIZER_NAMES: return construct_from_shape return None Argument = namedtuple( 'Argument' , [ 'arg_type' , 'arg_kind' , 'arg_name' , 'arg' ]) def transpose_funcargs (ctx: FunctionContext | MethodContext) -> dict [ str , Argument]: """[引数型], [引数名], ... を [(引数型,引数名,...)] にまとめる""" ctxdict = {} for i, name in enumerate (ctx.callee_arg_names): if len (ctx.arg_kinds[i]) == 0 : continue ctxdict[name] = Argument( arg_type=ctx.arg_types[i], arg_kind=ctx.arg_kinds[i], arg_name=ctx.arg_names[i], arg=ctx.args[i] ) return ctxdict def construct_from_shape (ctx: FunctionContext): if not isinstance (ctx.api, TypeChecker): return ctx.default_return_type # 失敗時は基本的にこれを返す ctxdict = transpose_funcargs(ctx) if "size" not in ctxdict: return ctx.default_return_type args = ctxdict[ "size" ].arg_type dimensions: List[Type] = [] # shape指定にはf(1,2,3)とf((1,2,3))の二通りあるので対応 if len (args) == 1 and isinstance (args[ 0 ], TupleType): dimensions.extend(args[ 0 ].items) else : dimensions.extend(args) # すべて数値定数であるときのみ対応する if all (( isinstance (dim, Instance) and dim.last_known_value is not None and type (dim.last_known_value.value) is int ) for dim in dimensions): shape_list = [dim.last_known_value.value for dim in dimensions] if "dtype" in ctxdict: # dtype指定があるとき dtype = ctxdict[ "dtype" ] dtype_argtype = dtype.arg_type[ 0 ] if isinstance (dtype_argtype, Instance) and dtype_argtype.type.fullname in [ "torch.dtype" ]: jaxtype = dtype_mapper.get(dtype.arg[ 0 ].name, None ) if jaxtype is None : ctx.api.fail( f "Unsupported dtype {ctxdict['args'][0].name} for torch function." , ctx.context ) return ctx.default_return_type # 指定の型とshapeからjaxtyping型 DType[Tensor, Literal["shape"]] を作る return construct_instance( ctx.api, jaxtype, ctx.api.named_type( "torch.Tensor" ), shape_list ) else : ctx.api.fail( f "Unsupported dtype {dtype_argtype} for torch function." , ctx.context ) return ctx.default_return_type return construct_instance( # デフォルトdtypeはfloat32 ctx.api, "Float32" , ctx.api.named_type( "torch.Tensor" ), shape_list ) return ctx.default_return_type これで torch.randn などの返り値型がTensorからjaxtypingになりました。 def g (x: Float32[Tensor, "3 224 224" ]): ... x: Float32[Tensor, "3 224 224" ] = torch.randn( 3 , 224 , 224 ) # OK y: Float32[Tensor, "3 224 226" ] = torch.randn( 3 , 224 , 224 ) # Incompatible types in assignment g(x) # OK テンソル計算 つぎはテンソル同士の演算を定義します。考慮すべきことは以下の3つです。 型が異なる時は"偉い"方に合わせる shape不一致の時はエラー shapeのブロードキャスト(片方の次元が1の時はもう片方に合わせてもよい) ですが、いったん型の方は無視します。 まず準備としてテンソルの演算子をstubに定義します。 # stub/jaxtyping/__init__.pyi Self = TypeVar( "Self" , bound= "AbstractArray[_ArrayType, _Shape]" ) class AbstractArray (Generic[_ArrayType, _Shape]): def __add__ (self: Self, other: Any): ... def __radd__ (self: Self, other: Any): ... def __iadd__ (self: Self, other: Any) -> Self: ... def __sub__ (self: Self, other: Any): ... def __rsub__ (self: Self, other: Any): ... def __isub__ (self: Self, other: Any) -> Self: ... def __mul__ (self: Self, other: Any): ... def __rmul__ (self: Self, other: Any): ... def __imul__ (self: Self, other: Any) -> Self: ... そしてこれを get_method_hook で捕捉します。 arithmetic_names = { "__add__" , "__radd__" , "__sub__" , "__rsub__" , "__mul__" , "__rmul__" , "__pow__" , "__div__" , "__rdiv__" , ... } def decompose_instance (typ: Instance) -> Optional[Tuple[ str , Type, List[ int ]]]: """Decompose a jaxtyping type into (backend type, shape as list of ints).""" if len (typ.args) != 2 : return None backend, shape = typ.args if not isinstance (shape, RawExpressionType) or type (shape.literal_value) is not str : return None dtype = typ.name # e.g., "Float32" dim_str = shape.literal_value # e.g, "1 224 224" dim_list = parse_dimstr(dim_str) if dim_list is None : return None return dtype, backend, dim_list def tensor_arithmetic (ctx: MethodContext): self_type = ctx.type other_type = ctx.arg_types[ 0 ][ 0 ] if isinstance (self_type, Instance) and isinstance (other_type, Instance): if self_type.type.fullname.startswith( "jaxtyping." ): self_result = decompose_instance(self_type) if self_result is None : ctx.api.fail( f "Unable to parse Self as jaxtyping {self_type}" , ctx.context ) return ctx.default_return_type self_dtype, self_backend, self_dims = self_result else : ctx.api.fail( f "Self must be jaxtyping {self_type}" , ctx.context ) return ctx.default_return_type if other_type.type.fullname.startswith( "jaxtyping." ): other_result = decompose_instance(other_type) if other_result is None : ctx.api.fail( f "Unable to parse Other as jaxtyping {other_type}" , ctx.context ) return ctx.default_return_type other_dtype, other_backend, other_dims = other_result elif other_type.type.fullname in ( "builtins.int" , "builtins.float" ): other_dtype = self_dtype other_backend = self_backend other_dims = [] # scalar if repr (self_backend) != repr (other_backend): ctx.api.fail( f "Backend mismatch: {self_backend} vs {other_backend}" , ctx.context ) return ctx.default_return_type out_backend = self_backend if self_dtype != other_dtype: ctx.api.fail( f "Dtype mismatch: {self_dtype} vs {other_dtype}" , ctx.context ) return ctx.default_return_type # TODO : promote dtype out_dtype = self_dtype if self_dims == other_dims: out_dims = self_dims else : # broadcast check longest = max ( len (self_dims), len (other_dims)) self_dims = [ 1 ] * (longest - len (self_dims)) + self_dims other_dims = [ 1 ] * (longest - len (other_dims)) + other_dims out_dims = [] for d1, d2 in zip (self_dims, other_dims): if d1 == d2: out_dims.append(d1) elif d1 == 1 : out_dims.append(d2) elif d2 == 1 : out_dims.append(d1) else : ctx.api.msg.fail( f "Shape mismatch: {self_dims} vs {other_dims}" , ctx.context ) return ctx.default_return_type # fail return construct_instance(ctx.api, out_dtype, out_backend, out_dims) ctx.api.fail( f "Unknown types for tensor arithmetic: {self_type} and {other_type}" , ctx.context ) return ctx.default_return_type class ShapePlugin (Plugin): def get_method_hook (self, fullname: str ): if fullname.startswith( "jaxtyping." ): # jaxtyping.Float32.__add__など if fullname.split( "." )[- 1 ] in arithmetic_names: return tensor_arithmetic 注意点として、どうも実行時と同じように __add__ から __radd__ へのフォールバックがなされているらしく、 __add__ の処理で api.fail によるエラーを吐いても、 __radd__ の型チェックが未実装のままだとそちらで解決したことになりエラーが消えてしまうようです。ちゃんと両方処理するか、フォールバック先を無条件でfailさせる必要があります。 これで以下のテストに対応できます。 x: Float32[Tensor, "3 224 224" ] = torch.randn( 3 , 224 , 224 ) # OK y: Float32[Tensor, "3 224 226" ] = torch.randn( 3 , 224 , 226 ) # OK reveal_type(x + x) # OK reveal_type(x * 2.0 ) # OK (scalar) reveal_type(torch.randn( 1 , 224 , 224 ) + x) # OK (broadcasting) reveal_type(x + y) # Shape mismatch: [3, 224, 224] vs [3, 224, 226] ここで限界が来た(Future work) この時点でテンソルの四則演算ができるようになりましたが、ここでギブアップしてしまいました。 実用レベルにするには以下のようにまだまだやるべきことが山のようにあります。 変数付きのshape記法 jaxtypingは"batch 3 height width"のような記法に対応しており、これができれば畳み込みニューラルネットワークなど入力画像のサイズを気にしないものにも型を付けることができます。 次元の四則演算 例えばテンソルを結合したときに次元を足し算したり、upsampleでは掛け算、downsampleでは割り算などをする必要があります。そしてこれは変数を許すと鬼のように難しくなります。 例えばUNetなどは画像をdownsampleしたのちupsampleしますが、downsampleでの割り算は小数切り捨てなのでupsampleしても元に戻るとは限りません。つまり割り算と掛け算を縮約することができないため、 batch 3 height//8*8 width//8*8 のようなshapeが batch 3 height width と一致するかなどの検証をする必要があります。 これはあまりにも辛いので、「 height は8で割り切れる」のような注釈をjaxtypingに新しく設けることで割り算をうまく処理するというのが無難そうです(こうすることでUNetに中途半端なサイズの画像を入れてバグらせるというのも回避できます)。 stubの是非 プラグインがjaxtypingやPyTorchなどのライブラリ由来の型を拾うためにstubを使いましたが、果たしてこの使い方が正しいのかという懸念があります。もっとエレガントな方法はないのでしょうか…… レイヤーの型注釈 PyTorchを扱うからにはnn.Moduleに対応する必要があるでしょう。ですがニューラルネットのあらゆるレイヤーに対してjaxtypingの型検査を実装するというのは骨が折れます。 さらにmypyを基盤にする上でおそらく一番の鬼門は、PyTorchでは一般的な以下のコーディングです。 layers = nn.ModuleList([nn.Linear( 100 , 50 ), nn.ReLU(), nn.Linear( 50 , 10 )]) def forward (x: Tensor): for layer in layers: x = layer(x) return x mypyは変数の再代入があっても 対応できるらしい のですが、果たしてforループが回った後の型はつけられるか怪しいです。 まとめ この記事ではmypyプラグインの機能を利用して、PyTorchのソースコードに次元付きの型注釈がつけられないか挑戦してみました。それなりの機能は持たせられそうでしたが、実用的なレベルまでいけるかどうかは微妙そうです。
アバター
この記事は NTT docomo Business Advent Calendar 2025 3 日目の記事です。 みなさんこんにちは、イノベーションセンターの @Mahito です。 普段は社内のエンジニアが働きやすくなることを目標に、コーポレートエンジニアのような活動やエンジニア向けイベントの企画・運営をしています。 今回は、上でも述べているように、 社内のエンジニアが働きやすくすることを目標に活動をしているイノベーションセンターの取り組み Engineer Empowerment プロジェクトについて紹介します。 NTTドコモビジネスの中で、エンジニアが働きやすくなるためにどのような活動をしているのか、興味がある方に読んでいただければと思います。 Engineer Empowerment プロジェクトとは Engineer Empowerment プロジェクト設立の背景 これまでの活動内容 パスワードマネージャーの全社導入 NTT グループ向けイベントの開催 これまでの活動からの学びと伝えたいこと 課題を共有する 実践する 評価・フィードバックする 現在・今後の活動 今後の活動 まとめ Engineer Empowerment プロジェクトとは Engineer Empowerment プロジェクトのミッションは、 NTTドコモビジネスや NTT グループのエンジニアが働きやすい環境・成長できる場を構築することです。 これらを実現するために、以下の 3 つの取り組みを行っています。 エンジニアからの課題を収集・集約したうえで、エンジニア有志と協働した解決策の検討・実施 働きやすい環境やルールの整備・見直しに関する関連部署との調整 エンジニアの成長やプレゼンス向上実現の場の用意やそうした企業文化の醸成 1 つめに「 エンジニア有志と協働 」と書いてありますが、じつはこのプロジェクトは私の 1 人プロジェクトです。 しかしながら、上記の取り組みをする際には、社内や NTT グループのエンジニア有志が協力してくれています。 そのおかげで、2021 年の 12 月から活動をしていますが、この 4 年の間にいくつかの成果を出すことができています。 Engineer Empowerment プロジェクト設立の背景 私はこのプロジェクトを立ち上げる前は、R&D のエンジニアとして、OpenStack のコミュニティ対応や、 Docker、Kubernetes、Spinnaker などの OSS の調査や社内導入のための検証などをしていました。 その傍ら、 NTT Tech Conference という NTT グループのエンジニアが発表をするイベントや、NTT グループ内限定のイベントの企画・運営もしていました。 こうしたイベントの中で、社内やグループのエンジニアの働く環境への課題を感じそれをなんとかしたいという思いがあり、 その一環として、以前記事にもした エンジニアがエンジニアのために開発・検証用 PC を整備した話 に取り組みました。 この取り組みを通じて、情報システム部や情報セキュリティ部では手が回らない エンジニアの働く課題をエンジニアが解決できるということを感じ、これ以外の問題にも取り組むようになりました。 こうした自分の本来業務のミッションとは違った活動を当時の上司からは理解してもらったうえで実施していました。 ただ、価値のある活動だと認められている一方、チームのミッションとは違う活動を続けることに対して、自分の中で葛藤がありました。 その折、NTTドコモビジネスで技術顧問をしている吉羽( @ryuzee )さんと 1on1 をした際、 上記の悩みを相談したところ、それなら自分でプロジェクトを立ち上げてみたらという話をされました。 自分自身でもそれは頭の片隅にあったのですが、吉羽さんと話をする中でそういうプロジェクトがあってもいいかという納得が生まれ、 Engineer Empowerment プロジェクトを立ち上げました。 ちなみに、Engineer Empowerment (エンジニアに力を与える)は当時の上司から名付けてもらいました。 これまでの活動内容 Engineer Empowerment プロジェクト立ち上げ当時は上記の開発・検証用 PC の整備を半年で決着をつけてプロジェクトを畳む予定でした。 しかし、その計画が社内の調整などで遅延する中でそれ以外のエンジニアの働く課題も見つかり、 開発・検証用 PC の整備が完了した後も、それらの課題に 1 つずつ取り組んでいるうちに現在 5 年目を迎えています。 これまでに取り組んだ課題の例としては、以下のようなものがあります。 パスワードマネージャーの全社導入(トライアル中) 開発・検証用 PC から社内システムに接続するための VDI(Virtual Desktop Infrastructure) の提供(情シスへノウハウを引き継ぎ終了) GitHub Enterprise の全社化とその運用 Miro の全社化とその運用 各種 NTT グループ内部向けイベントの開催 NTT Tech Conference の企画・運営 当エンジニアブログのリニューアルとその運用 技術系 SNS 運用に関する取りまとめ etc これらの活動の多くは、エンジニアからの「こういうことに困っている」や「こういうことがしたい」という話から始まっています。 ここでは一例として、パスワードマネージャーの全社導入と、NTTグループ向けイベントの開催について紹介します。 パスワードマネージャーの全社導入 パスワードマネージャーはとあるエンジニアの「管理する機器やサービスが多すぎてパスワード管理が辛い」という話から始まっています。 一般的に、ID/Pass はそのログイン先ごとに異なるものを利用することが推奨されています。 しかしながら、人間の記憶には限界があるため、結果的に覚えやすいパスワードを使いまわしたり、 あるいは規則性のあるパスワードを利用するなど、どこかのサービスで ID/Pass が漏洩した際にリスクが高まります。 こうした問題を解決するために、パスワードマネージャーを使うことで、 機器やサービスごとの異なる ID/Pass を人間が覚えずとも安全に使えるようにしたいという話がありました。 そこで、Engineer Empowerment プロジェクトでは、部内で利用したい人向けにパスワードマネージャーの提供を始めました。 1 年ほど使ったところで、パスワードマネージャーの利用を継続するか判断するために利用者アンケートをとったところ、 想定通り便利になったという声が多い中に「グループで安心して ID/Pass を共有できるようになった」という声がありました。 会社のセキュリティルールとして、原則共有 ID は禁止されているのですが、 現実問題として機器やサービスの仕様(管理者権限アカウントが 1 つしか作れないなど)で共有 ID を使わざるを得ないケースがあります。 そうした場合に従来は口伝や鍵付きの Excel で管理していたそうですが、 それをパスワードマネージャーで安全に共有できるようになり、利便性や安全性が上がったとのことでした。 私は全社でも同じような話やニーズがあるのではないかと思い、 情報セキュリティ部にこの内容を共有したうえで全社アンケートを実施したところ、 同様にパスワード管理や共有の課題を感じている人が多いとわかりました。 そこで、情報セキュリティ部と話し合ったうえで現在は情報セキュリティ部主導で全社トライアルという形で、 パスワードマネージャーの導入が進められています。 NTT グループ向けイベントの開催 2022 年の年末ぐらいに「OpenAI を使ったハッカソンがしたい」という話を社内のエンジニアからされたことをきっかけに、2023 年の夏に NTT グループ内のイベントとして NTT Group Azure OpenAI Day と NTT Generative AI Hack Day というイベントを開催しました。 当時、Azure OpenAI Service はリクエストを出して Waiting List に登録されてから利用できるまでに数週間かかっており、すぐには利用できない状態でした。 そこで、NTT Com (現在:NTTドコモビジネス)を退職してマイクロソフトで働いていた友人に相談したところ、営業を含めた打ち合わせが設けられ、気がつくと NTT を巻き込んだ一大イベントになっていました。 イベントスタッフは NTT グループのエンジニア有志で構成され、 1日目の NTT Group Azure OpenAI Day にはグループから 1,000 人を超える参加者が集まり、 2~3 日目の NTT Generative AI Hack Day には 100 人程度集まりました。 イベントについては エヌ・エフ・ラボラトリーズの方が少し書いてくれていたりします。 Azure OpenAI Serviceでマッチングアプリの返信を自動生成してみた - NFLabs. エンジニアブログ 当初は自社に閉じた小さいイベントの予定でしたが、 気がつくとグループを巻き込む大規模イベントになってしまいました。 しかし、こうしたイベントを通じて、NTT グループのエンジニア同士の交流や技術情報の共有が進んだのは良かったと思っています。 これまでの活動からの学びと伝えたいこと これまでの活動を通じて、エンジニアの課題解決や、やりたいことを実現するために、以下のようなことの重要性を改めて感じています。 課題を共有する 実践する 評価・フィードバックする 課題を共有する これはエンジニアが持つ働くうえでの課題(開発・検証端末や使えるツール、開発に関するルールなど)をまずはエンジニアの中で共有することです。 共有することで、その課題を多くの人が感じていたり、言われて初めて気づくことで、 その課題を解くことが自分たちの働きやすさにつながるとなったものについては解決に向けて働きかけることができます。 また、これはエンジニアの中だけでなく関係する部署(情報システム部、情報セキュリティ部、広報、人事など)とも共有することで、 相手が認識していない課題を認識してもらい、一緒に解決へ向けた取り組みを進めることにつながります。 今回挙げたパスワードマネージャーの話では、まさに情報セキュリティ部では見えていなかった現場の課題を共有することで、 パスワードマネージャーによる解決という話につなげることができました。 実践する 課題がわかったり、そもそもやりたいことがあった場合に、計画だけでなく失敗してもいいので実際に実施してみることが重要です。 実際にやってみることで、計画段階では見えなかった問題点が見えてきたり、 自分たちでは解決できなかった問題を他のエンジニアが協力してくれることで、解決できることがあります。 また、Azure OpenAI Service を使ったハッカソンイベントのような、 思ったより大事になることもありますが、そういったものも含めて楽しめる事が多いと思います。 そして、実践するのは「 やりたいこと 」にするのをお勧めします。 私はとある SaaS の管理でやりたいわけではないが会社の事情を鑑みて「 やったほうがいいこと 」に手を出した際に、 興味があって手伝ってくれる人がいるだろうと気軽に始めました。 しかし、実際には社内でほとんど協力が得られず、 1 人でしんどい思いをする羽目になった経験があります。(今はなんとかして解決しております) もし、あなたが会社の中で有志たちとエンジニアの課題を解決するのであれば「 やったほうがいい 」や「 やるべき 」に惑わされず、 自分がやりたいこと、そしてまわりもやりたいこと にすることをお勧めします。 本当に、やったほうがいいことややるべきことには会社が人をつけるはずです。 評価・フィードバックする 4 年間の活動の反省でもあるのですが、自分たちのやってきた活動がどんな効果を上げたのか、 社内のエンジニアが上げてくれたリクエストがどうなったのかを定期的に評価・フィードバックすることが重要です。 私は昨年までは、Engineer Empowerment プロジェクトでの活動を大々的にアピールすることはせず、 知ってる人が知ってくれればいいかなという風に思っていました。 しかし、エンジニアが抱えている課題を自分たちで解決できることをもっと多くのエンジニアに知ってもらうことで、 今までよりも多くのエンジニアの活動に良い効果を与えていけると考えを改め、今年から積極的に情報発信をしています。 そのため、社内のエンジニアがくれたリクエストに対して、 活動内容やその結果を共有することで、より多くのエンジニアにこの取り組みを知ってもらい、 エンジニアが働きやすくなるための活動に参加してもらえるようにしていきたいと考えています。 今回の記事は、こうした取り組みを社内だけに閉じず、社外にも共有したいと思いブログという形で発信させてもらっています。 現在・今後の活動 現在は主に、コーディングエージェントを利用できるようにする取り組みを進めています。 NTT グループでは AI 利用に関してガバナンス規定類が定められており、 AI を使う上で、その利用リスクを評価した上での利用が求められます。 NTTのAIについて コーディングエージェントは利用するモデルが学習した内容やプロンプトで指示した内容次第では、 OSS のライセンスに違反するなど、第三者の知的財産を侵害する可能性があります。 こうした点に対して、エンジニアが安心してコーディングエージェントを利用するために、 コーディングエージェントの入出力に対してガードレールが設けられないかを調査・検証したり、 法務や知的財産を担当する方々とサービス規約面からの補償なども含めて安全に使えるコーディングエージェントの調査・検証を進めています。 また、それらの内容をコーディングエージェントを利用するためのガイドラインとして作成を進めており、 近い内に社内でコーディングエージェントが正式に使えるようになる予定です。 さらに、NTT グループのエンジニアにもコーディングエージェントの効果を実感してもらうために、 NTT グループのエンジニア有志と一緒に、コーディングエージェントを実際に触るワークショップを開催する準備を進めています。 今後の活動 私個人の思いとしては、この Engineer Empowerment プロジェクトは近いうちに終わらせたいと思っています。 これはネガティブな理由ではなく、このプロジェクトがなくても社内や NTT グループのエンジニアが、 自分たちでエンジニアが働くうえでの問題を解決し、自分たちで働きやすい環境を作っていけるようになるのが理想だと考えているからです。 そのためにも、今後も Engineer Empowerment プロジェクトでは、一緒に問題解決してくれる社内や NTT グループのエンジニア有志とともに、 エンジニアが働くうえでの問題を解決しながら仲間を増やしていきます。 そして、そのノウハウを共有することで、誰もがよりエンジニアとして働きやすい環境を作っていけるようになることで、このプロジェクトを終えられればと思っています。 まとめ 本記事では、社内のエンジニアの課題ややりたいことをエンジニアの立場から解決や実現に向けて働きかけるプロジェクトについて紹介させていただきました。 エンジニアの課題はエンジニアの中で共有し、実践し、評価・フィードバックすることで、改善できます。 また、関係部署ともその取組を共有することで、より良い解決策を見つけたり、会社全体にいい影響を及ぼすことができます。 この記事が、あなたの会社や組織でもエンジニアが働きやすくなるための取り組みのきっかけになれば幸いです。 明日もお楽しみに。
アバター
この記事は、 NTT docomo Business Advent Calendar 2025 2 日目の記事です。 Android 15 から Android 端末上で Linux 環境を動かすことが可能になりました。せっかくなので、 OpenStack をインストールして VM を動かしてみました。 はじめに スマホの Linux 開発環境に SSH する 開発環境を探検する OpenStack のインストール方法について DevStack を実行して minimal な OpenStack 環境をつくる スマホ OpenStack に VM を建ててみる トラブルシューティング Linux 開発環境が落ちる VM が boot しない まとめ はじめに こんにちは。 Smart Data Platform (SDPF) クラウド/サーバー 仮想サーバーチームの杉浦 ( @Kumassy_ ) です。 普段はハイパーバイザや OpenStack の開発・検証をしています。 私は Pixel 8 Pro をメインのスマホとして使っています。外出先でスマホを眺めていたところ、開発者向けオプションに Linux 開発環境が追加されているのに気づきました。 どうやら 2025 年 3 月ごろから Android 15 端末向けに追加されたもののようです。 sudo も利用できますし、 Docker コンテナも起動できるようです。そこで、普段 OpenStack の開発をしている身としては、 OpenStack 環境を構築して VM を起動させてみることにしました。 スマホの Linux 開発環境に SSH する スマホは画面が小さいですし文字を打つのも不便なので、 laptop から SSH して遊びたいです。 外部ネットワークには出られるようですが、 NAT 配下にいて直接 SSH できなさそうなので、 SSH をポートフォワードして外部からアクセス可能にします。 まずはスマホ上のターミナルで ssh をインストールして、ログインパスワードを設定します。 sudo apt install ssh sudo systemctl start sshd sudo passwd droid そして、 22/tcp を laptop に転送します。 ssh -R 10022:localhost:22 kumassy@<laptop ip> こうすれば laptop から以下のようにしてスマホ上のターミナルにログインできます。 ssh -p 10022 droid@localhost これで普通の Linux マシンと同じように作業できますね。 この手法は @kamiya334 に 教えてもらいました 。 開発環境を探検する 初めに Linux 開発環境がどのような環境なのか見てみました。 スマホなので仕方ないのですが、以下のように現代の開発環境としてはかなり頼りないものとなっていました。 CPU droid@localhost:~$ lscpu Architecture: aarch64 CPU op-mode(s): 64-bit Byte Order: Little Endian CPU(s): 9 On-line CPU(s) list: 0-8 Vendor ID: ARM Model name: Cortex-A715 Model: 0 Thread(s) per core: 1 Core(s) per socket: 3 Socket(s): 1 Stepping: r1p0 BogoMIPS: 49.15 Flags: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt f cma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb pac a pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm sveb f16 i8mm bti Model name: Cortex-A510 Model: 1 Thread(s) per core: 1 Core(s) per socket: 1 Socket(s): 1 Stepping: r1p1 BogoMIPS: 49.15 Flags: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt f cma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb pac a pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm sveb f16 i8mm bti Model name: Cortex-A715 Model: 0 Thread(s) per core: 1 Core(s) per socket: 2 Socket(s): 1 Stepping: r1p0 BogoMIPS: 49.15 Flags: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt f cma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb pac a pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm sveb f16 i8mm bti Model name: Cortex-A510 Model: 1 Thread(s) per core: 1 Core(s) per socket: 1 Socket(s): 1 Stepping: r1p1 BogoMIPS: 49.15 Flags: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt f cma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb pac a pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm sveb f16 i8mm bti Model name: Cortex-A715 Model: 0 Thread(s) per core: 1 Core(s) per socket: 1 Socket(s): 1 Stepping: r1p0 BogoMIPS: 49.15 Flags: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt f cma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb pac a pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm sveb f16 i8mm bti Model name: - Model: 1 Thread(s) per core: 1 Core(s) per socket: 1 Socket(s): 1 NUMA: NUMA node(s): 1 NUMA node0 CPU(s): 0-8 Vulnerabilities: Gather data sampling: Not affected Itlb multihit: Not affected L1tf: Not affected Mds: Not affected Meltdown: Not affected Mmio stale data: Not affected Reg file data sampling: Not affected Retbleed: Not affected Spec rstack overflow: Not affected Spec store bypass: Mitigation; Speculative Store Bypass disabled via prctl Spectre v1: Mitigation; __user pointer sanitization Spectre v2: Mitigation; CSV2, BHB Srbds: Not affected Tsx async abort: Not affected メモリ droid@localhost:~$ free -h total used free shared buff/cache available Mem: 3.8Gi 903Mi 2.2Gi 5.6Mi 974Mi 3.0Gi Swap: 981Mi 0B 981Mi ストレージ droid@localhost:~$ df -h Filesystem Size Used Avail Use% Mounted on /dev/vda1 214G 1.5G 13G 10% / /dev/vda2 191M 191M 0 100% /opt/kernel_extras devtmpfs 4.0M 0 4.0M 0% /dev tmpfs 2.0G 0 2.0G 0% /dev/shm tmpfs 786M 524K 785M 1% /run tmpfs 5.0M 0 5.0M 0% /run/lock android 229G 215G 15G 94% /mnt/shared internal 229G 215G 15G 94% /mnt/internal tmpfs 393M 0 393M 0% /run/user/0 tmpfs 393M 4.0K 393M 1% /run/user/1000 KVM droid@localhost:~$ ls /dev/kvm ls: cannot access '/dev/kvm': No such file or directory これらの結果から、開発環境の概要をまとめると以下のようになります。 CPU : ARM アーキテクチャ aarch64 の 9 コア構成で、 Cortex-A715 (高性能コア)と Cortex-A510 (効率コア)の組み合わせです。 メモリ : 4 GB 利用可能でした。OpenStackのような重量級のサービスを動かすには少し心もとない容量ですが、最小構成であれば動作させることができそうです。 ストレージ : ルートファイルシステム(/dev/vda1)には214GBが割り当てられているものの、実際に / で利用できるのは 16 GB 程度であるようです。 仮想化機能 : /dev/kvm デバイスが存在しないため、ハードウェア仮想化(KVM)は利用できません。 OpenStack で VM を動かす際は QEMU のソフトウェアエミュレーションを使用することになります。 制約は厳しいですが、以下のように Docker コンテナは正常に動作しました。 OpenStack のインストール方法について OpenStack のインストール方法はいくつかあります。 SDPF クラウド仮想サーバーチームでは kolla-ansible をベースに開発していますが、メモリとストレージの要件がかなり厳しいので、コンテナベースの kolla-ansible は利用できなさそうです。 ChatGPT にいくつかの OpenStack インストール方法について比較してもらいました。 ツール 概要 最小インストール要件の目安 DevStack Bash スクリプトで OS 上に OpenStack サービスを直接インストールできる OpenStack 開発者向けのツール 2CPU, 4GB RAM, 10 GB disk space Kolla-Ansible Docker/Podman コンテナ化された OpenStack を Ansible でデプロイするツール 2 NIC, 8GB RAM, 40 GB disk space OpenStack-Ansible LXC コンテナ化された OpenStack を Ansible でデプロイするツール 8 CPU, 8GB RAM, 50GB disk space MicroStack お手軽に導入できる snap ベースのシングルノード OpenStack 構築ツール 2 CPU, 8GB RAM, 100 GB disk space OpenStack は全体的に最小インストール要件が厳しめですが、 DevStack だとギリギリ動きそうなので、 DevStack をチューニングする方針としました。 DevStack を実行して minimal な OpenStack 環境をつくる DevStack では local.conf という config をもとに OpenStack をインストールします。 メモリとストレージの要件が厳しいので、 Minimal deployment を参考にして Cinder と Horizon はインストールしないことにします。 Block Storage はなくてよいですし、 API さえ使えれば十分ですよね。 いろいろと試行錯誤した結果、以下のような config ができました。 stack@localhost:~/devstack$ cat local.conf [[local|localrc]] ADMIN_PASSWORD=secret DATABASE_PASSWORD=$ADMIN_PASSWORD RABBIT_PASSWORD=$ADMIN_PASSWORD SERVICE_PASSWORD=$ADMIN_PASSWORD disable_service horizon disable_service cinder c-sch c-api c-vol disable_service swift s-proxy s-object s-container s-account disable_service tempest disable_service heat h-eng h-api h-api-cfn h-api-cw disable_service ceilometer-acompute ceilometer-acentral ceilometer-api disable_service neutron-adv-service disable_service octavia o-api o-cw o-hm o-hk disable_service barbican disable_service trove disable_service sahara API_WORKERS=1 RPC_WORKERS=1 NEUTRON_PORT_SECURITY=false CIRROS_ARCH=aarch64 [[post-config|$NOVA_CONF]] [libvirt] virt_type = qemu cpu_mode = custom cpu_model = cortex-a53 DevStack のインストールを進める前に、以下のようにして DB に割り当てるメモリをケチっておきます。 stack@localhost:~/devstack$ sudo vim /etc/mysql/conf.d/devstack-lowmem.cnf stack@localhost:~/devstack$ cat /etc/mysql/conf.d/devstack-lowmem.cnf [mysqld] innodb_buffer_pool_size = 256M innodb_log_file_size = 64M max_connections = 100 tmp_table_size = 32M max_heap_table_size = 32M あとは ドキュメント に沿って ./stack.sh すれば OpenStack 環境ができます。 stack@localhost:~/devstack$ ./stack.sh This is your host IP address: 10.123.149.241 This is your host IPv6 address: ::1 Keystone is serving at http://10.123.149.241/identity/ The default users are: admin and demo The password: secret Services are running under systemd unit files. For more information see: https://docs.openstack.org/devstack/latest/systemd.html DevStack Version: 2026.1 Change: f61d747518a3b4896032c7e9440ddf31856a060f Merge "Enable response validation in Keystone" 2025-11-14 14:20:56 +0000 OS Version: Debian 12 bookworm 2025-11-24 02:08:30.477 | stack.sh completed in 1923 seconds. スマホ OpenStack に VM を建ててみる ここまでできたらあとは通常の OpenStack のように VM を作成できます。 aarch64 な CirrOS の image があるので、この image をもとに VM を作成します。 stack@localhost:~/devstack$ openstack image list +--------------------------------------+---------------------------+--------+ | ID | Name | Status | +--------------------------------------+---------------------------+--------+ | 2b8bfbf7-42bf-43a3-a91d-50c59633dc94 | cirros-0.6.3-aarch64-disk | active | +--------------------------------------+---------------------------+--------+ NW まわりはきちんと設定していないので、どの NW にも接続しないことにします。 stack@localhost:~/devstack$ openstack server create --image 2b8bfbf7-42bf-43a3-a91d-50c59633dc94 --flavor m1.tiny test-server --nic none stack@localhost:~/devstack$ openstack server show f132c8af-cee6-4248-9128-de58e42e6665 +-------------------------------------+------------------------------------------------------------------------------------------------+ | Field | Value | +-------------------------------------+------------------------------------------------------------------------------------------------+ | OS-DCF:diskConfig | MANUAL | | OS-EXT-AZ:availability_zone | nova | | OS-EXT-SRV-ATTR:host | None | | OS-EXT-SRV-ATTR:hostname | test-server | | OS-EXT-SRV-ATTR:hypervisor_hostname | None | | OS-EXT-SRV-ATTR:instance_name | None | | OS-EXT-SRV-ATTR:kernel_id | None | | OS-EXT-SRV-ATTR:launch_index | None | | OS-EXT-SRV-ATTR:ramdisk_id | None | | OS-EXT-SRV-ATTR:reservation_id | None | | OS-EXT-SRV-ATTR:root_device_name | None | | OS-EXT-SRV-ATTR:user_data | None | | OS-EXT-STS:power_state | Running | | OS-EXT-STS:task_state | None | | OS-EXT-STS:vm_state | active | | OS-SRV-USG:launched_at | 2025-11-24T02:10:05.000000 | | OS-SRV-USG:terminated_at | None | | accessIPv4 | | | accessIPv6 | | | addresses | | | config_drive | | | created | 2025-11-24T02:09:46Z | | description | None | | flavor | description=, disk='1', ephemeral='0', extra_specs.hw_rng:allowed='True', id='m1.tiny', | | | is_disabled=, is_public='True', location=, name='m1.tiny', original_name='m1.tiny', ram='512', | | | rxtx_factor=, swap='0', vcpus='1' | | hostId | eadc3234a9f5d6b80737ed483bbdbe2d388821b0fa927cfdc237934a | | host_status | None | | id | f132c8af-cee6-4248-9128-de58e42e6665 | | image | cirros-0.6.3-aarch64-disk (2b8bfbf7-42bf-43a3-a91d-50c59633dc94) | | key_name | None | | locked | False | | locked_reason | None | | name | test-server | | pinned_availability_zone | None | | progress | 0 | | project_id | a3a363ed73e8469b98ffaae53bfa8df5 | | properties | | | scheduler_hints | | | server_groups | [] | | status | ACTIVE | | tags | | | trusted_image_certificates | None | | updated | 2025-11-24T02:10:06Z | | user_id | 6b8f5452a10542359fd0269553371366 | | volumes_attached | | +-------------------------------------+------------------------------------------------------------------------------------------------+ stack@localhost:~/devstack$ sudo virsh list Id Name State ----------------------------------- 1 instance-00000001 running virsh console か console.log を眺めると、 CirrOS がブートしてきたのが確認できました。 stack@localhost:~/devstack$ sudo tail -F /opt/stack/data/nova/instances/f132c8af-cee6-4248-9128-de58e42e6665/console.log [ 5.343116] EXT4-fs (vda1): write access will be enabled during recovery [ 5.513086] EXT4-fs (vda1): recovery complete [ 5.553111] EXT4-fs (vda1): mounted filesystem with ordered data mode. Opts: (null). Quota mode: none. [ 7.266087] EXT4-fs (vda1): re-mounted. Opts: (null). Quota mode: none. [ 243.220848] EXT4-fs (vda1): resizing filesystem from 25600 to 259835 blocks [ 243.239892] EXT4-fs (vda1): resized filesystem to 259835 ### tail -n 25 /var/log/messages Nov 24 02:10:36 cirros syslog.info syslogd started: BusyBox v1.35.0 Nov 24 02:10:39 cirros daemon.info dhcpcd[258]: dhcpcd-9.4.1 starting Nov 24 02:10:39 cirros daemon.info dhcpcd[261]: DUID 00:04:f1:32:c8:af:ce:e6:42:48:91:28:de:58:e4:2e:66:65 Nov 24 02:10:39 cirros daemon.err dhcpcd[261]: no valid interfaces found Nov 24 02:16:06 cirros syslog.info syslogd started: BusyBox v1.35.0 Nov 24 02:16:09 cirros daemon.info dhcpcd[249]: dhcpcd-9.4.1 starting Nov 24 02:16:09 cirros daemon.info dhcpcd[252]: DUID 00:04:f1:32:c8:af:ce:e6:42:48:91:28:de:58:e4:2e:66:65 Nov 24 02:16:09 cirros daemon.err dhcpcd[252]: no valid interfaces found Nov 24 02:19:09 cirros daemon.err dhcpcd[252]: timed out Nov 24 02:19:58 cirros authpriv.info dropbear[388]: Running in background ############ debug end ############## ____ ____ ____ / __/ __ ____ ____ / __ \/ __/ / /__ / // __// __// /_/ /\ \ \___//_//_/ /_/ \____/___/ http://cirros-cloud.net login as 'cirros' user. default password: 'gocubsgo'. use 'sudo' for root. cirros login: [ 246.401660] virtio_gpu virtio4: [drm] drm_plane_enable_fb_damage_clips() not called スマホさえ持っていればいつでも OpenStack の開発ができますね! トラブルシューティング Linux 開発環境が落ちる メモリが足らず、 ときどき Linux 開発環境が落ちることもありました。 ターミナルを再起動すれば概ね作業を再開できるので、根気強くインストール作業を進めてください。 openstack server create コマンドさえ実行できれば DB や MQ はいらないので、停止してしまってもよいでしょう。 VM が boot しない デフォルトの local.conf では nova.conf に以下のような設定が入ります。 [libvirt] live_migration_uri = qemu+ssh://stack@%s/system cpu_mode = host-passthrough virt_type = qemu cpu_mode = host-passthrough となっていますが、 KVM が利用できないため、このままの設定だと VM の起動に失敗します。 : f4caa26c-ea15-428e-8e70-68dfb0aa7266] File "/opt/stack/nova/nova/virt/libvirt/driver.py", line 8247, in _create_guest : f4caa26c-ea15-428e-8e70-68dfb0aa7266] guest.launch(pause=pause) : f4caa26c-ea15-428e-8e70-68dfb0aa7266] File "/opt/stack/nova/nova/virt/libvirt/guest.py", line 167, in launch : f4caa26c-ea15-428e-8e70-68dfb0aa7266] with excutils.save_and_reraise_exception(): : f4caa26c-ea15-428e-8e70-68dfb0aa7266] File "/opt/stack/data/venv/lib/python3.12/site-packages/oslo_utils/excutils.py", line 256, in __exit__ : f4caa26c-ea15-428e-8e70-68dfb0aa7266] self.force_reraise() : f4caa26c-ea15-428e-8e70-68dfb0aa7266] File "/opt/stack/data/venv/lib/python3.12/site-packages/oslo_utils/excutils.py", line 222, in force_reraise : f4caa26c-ea15-428e-8e70-68dfb0aa7266] raise self.value : f4caa26c-ea15-428e-8e70-68dfb0aa7266] File "/opt/stack/nova/nova/virt/libvirt/guest.py", line 165, in launch : f4caa26c-ea15-428e-8e70-68dfb0aa7266] return self._domain.createWithFlags(flags) : f4caa26c-ea15-428e-8e70-68dfb0aa7266] ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ : f4caa26c-ea15-428e-8e70-68dfb0aa7266] File "/opt/stack/data/venv/lib/python3.12/site-packages/eventlet/tpool.py", line 186, in doit : f4caa26c-ea15-428e-8e70-68dfb0aa7266] result = proxy_call(self._autowrap, f, *args, **kwargs) : f4caa26c-ea15-428e-8e70-68dfb0aa7266] ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ : f4caa26c-ea15-428e-8e70-68dfb0aa7266] File "/opt/stack/data/venv/lib/python3.12/site-packages/eventlet/tpool.py", line 144, in proxy_call : f4caa26c-ea15-428e-8e70-68dfb0aa7266] rv = execute(f, *args, **kwargs) : f4caa26c-ea15-428e-8e70-68dfb0aa7266] ^^^^^^^^^^^^^^^^^^^^^^^^^^^ : f4caa26c-ea15-428e-8e70-68dfb0aa7266] File "/opt/stack/data/venv/lib/python3.12/site-packages/eventlet/tpool.py", line 125, in execute : f4caa26c-ea15-428e-8e70-68dfb0aa7266] raise e.with_traceback(tb) : f4caa26c-ea15-428e-8e70-68dfb0aa7266] File "/opt/stack/data/venv/lib/python3.12/site-packages/eventlet/tpool.py", line 82, in tworker : f4caa26c-ea15-428e-8e70-68dfb0aa7266] rv = meth(*args, **kwargs) : f4caa26c-ea15-428e-8e70-68dfb0aa7266] ^^^^^^^^^^^^^^^^^^^^^ : f4caa26c-ea15-428e-8e70-68dfb0aa7266] File "/usr/lib/python3/dist-packages/libvirt.py", line 1415, in createWithFlags : f4caa26c-ea15-428e-8e70-68dfb0aa7266] raise libvirtError('virDomainCreateWithFlags() failed') : f4caa26c-ea15-428e-8e70-68dfb0aa7266] libvirt.libvirtError: unsupported configuration: CPU mode 'host-passthrough' for aarch64 qemu domain on aarch64 host is not supported> : f4caa26c-ea15-428e-8e70-68dfb0aa7266] それから、 cpu_model の設定も必要でした。 デフォルトの cpu_model の場合は確かに qemu process としては動作しているようにみえました。 stack@lima-default:~/devstack$ sudo virsh dominfo instance-00000003 Id: 3 Name: instance-00000003 UUID: 03bd6bb9-8e56-48da-ade9-13870c876577 OS Type: hvm State: running CPU(s): 1 CPU time: 109.9s Max memory: 262144 KiB Used memory: 262144 KiB Persistent: yes Autostart: disable Managed save: no Security model: apparmor Security DOI: 0 Security label: libvirt-03bd6bb9-8e56-48da-ade9-13870c876577 (enforcing) stack@lima-default:~/devstack$ sudo virsh domstats instance-00000003 Domain: 'instance-00000003' state.state=1 state.reason=1 cpu.time=126892574000 cpu.user=126474847000 cpu.system=417727000 cpu.cache.monitor.count=0 cpu.haltpoll.success.time=0 cpu.haltpoll.fail.time=0 balloon.current=262144 balloon.maximum=262144 balloon.last-update=0 balloon.rss=179488 vcpu.current=1 vcpu.maximum=1 vcpu.0.state=1 vcpu.0.time=126430000000 vcpu.0.wait=0 vcpu.0.delay=43526812 net.count=0 block.count=1 block.0.name=vda block.0.path=/opt/stack/data/nova/instances/03bd6bb9-8e56-48da-ade9-13870c876577/disk block.0.backingIndex=1 block.0.rd.reqs=0 block.0.rd.bytes=0 block.0.rd.times=0 block.0.wr.reqs=0 block.0.wr.bytes=0 block.0.wr.times=0 block.0.fl.reqs=0 block.0.fl.times=0 block.0.allocation=0 block.0.capacity=1073741824 block.0.physical=204800 dirtyrate.calc_status=0 dirtyrate.calc_start_time=0 dirtyrate.calc_period=0 dirtyrate.calc_mode=page-sampling stack@lima-default:~/devstack$ ps aux | grep qemu libvirt+ 962634 99.9 4.4 1431616 179488 ? Sl 17:22 4:12 /usr/bin/qemu-system-aarch64 -name guest=instance-00000003,debug-threads=on -S -object {"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain-3-instance-00000003/master-key.aes"} -blockdev {"driver":"file","filename":"/usr/share/AAVMF/AAVMF_CODE.no-secboot.fd","node-name":"libvirt-pflash0-storage","auto-read-only":true,"discard":"unmap"} -blockdev {"node-name":"libvirt-pflash0-format","read-only":true,"driver":"raw","file":"libvirt-pflash0-storage"} -blockdev {"driver":"file","filename":"/var/lib/libvirt/qemu/nvram/instance-00000003_VARS.fd","node-name":"libvirt-pflash1-storage","auto-read-only":true,"discard":"unmap"} -blockdev {"node-name":"libvirt-pflash1-format","read-only":false,"driver":"raw","file":"libvirt-pflash1-storage"} -machine virt-8.2,usb=off,gic-version=2,dump-guest-core=off,memory-backend=mach-virt.ram,pflash0=libvirt-pflash0-format,pflash1=libvirt-pflash1-format,acpi=on -accel tcg -cpu cortex-a15 -m size=262144k -object {"qom-type":"memory-backend-ram","id":"mach-virt.ram","size":268435456} -overcommit mem-lock=off -smp 1,sockets=1,dies=1,cores=1,threads=1 -uuid 03bd6bb9-8e56-48da-ade9-13870c876577 -no-user-config -nodefaults -chardev socket,id=charmonitor,fd=30,server=on,wait=off -mon chardev=charmonitor,id=monitor,mode=control -rtc base=utc -no-shutdown -boot strict=on -device {"driver":"pcie-root-port","port":8,"chassis":1,"id":"pci.1","bus":"pcie.0","multifunction":true,"addr":"0x1"} -device {"driver":"pcie-root-port","port":9,"chassis":2,"id":"pci.2","bus":"pcie.0","addr":"0x1.0x1"} -device {"driver":"pcie-root-port","port":10,"chassis":3,"id":"pci.3","bus":"pcie.0","addr":"0x1.0x2"} -device {"driver":"pcie-root-port","port":11,"chassis":4,"id":"pci.4","bus":"pcie.0","addr":"0x1.0x3"} -device {"driver":"pcie-root-port","port":12,"chassis":5,"id":"pci.5","bus":"pcie.0","addr":"0x1.0x4"} -device {"driver":"pcie-root-port","port":13,"chassis":6,"id":"pci.6","bus":"pcie.0","addr":"0x1.0x5"} -device {"driver":"pcie-root-port","port":14,"chassis":7,"id":"pci.7","bus":"pcie.0","addr":"0x1.0x6"} -device {"driver":"qemu-xhci","id":"usb","bus":"pci.2","addr":"0x0"} -device {"driver":"virtio-scsi-pci","id":"scsi0","bus":"pci.1","addr":"0x0"} -blockdev {"driver":"file","filename":"/opt/stack/data/nova/instances/_base/71f6f0f805cceef98c928ec21e85f5e992012140","node-name":"libvirt-2-storage","auto-read-only":true,"discard":"unmap","cache":{"direct":true,"no-flush":false}} -blockdev {"node-name":"libvirt-2-format","read-only":true,"cache":{"direct":true,"no-flush":false},"driver":"raw","file":"libvirt-2-storage"} -blockdev {"driver":"file","filename":"/opt/stack/data/nova/instances/03bd6bb9-8e56-48da-ade9-13870c876577/disk","node-name":"libvirt-1-storage","auto-read-only":true,"discard":"unmap","cache":{"direct":true,"no-flush":false}} -blockdev {"node-name":"libvirt-1-format","read-only":false,"cache":{"direct":true,"no-flush":false},"driver":"qcow2","file":"libvirt-1-storage","backing":"libvirt-2-format"} -device {"driver":"virtio-blk-pci","bus":"pci.3","addr":"0x0","drive":"libvirt-1-format","id":"virtio-disk0","bootindex":1,"write-cache":"on"} -add-fd set=0,fd=31,opaque=serial0-log -chardev pty,id=charserial0,logfile=/dev/fdset/0,logappend=on -serial chardev:charserial0 -device {"driver":"usb-kbd","id":"input0","bus":"usb.0","port":"1"} -audiodev {"id":"audio1","driver":"none"} -vnc 0.0.0.0:0,audiodev=audio1 -device {"driver":"virtio-gpu-pci","id":"video0","max_outputs":1,"bus":"pci.6","addr":"0x0"} -device {"driver":"virtio-balloon-pci","id":"balloon0","deflate-on-oom":true,"free-page-reporting":true,"bus":"pci.4","addr":"0x0"} -object {"qom-type":"rng-random","id":"objrng0","filename":"/dev/urandom"} -device {"driver":"virtio-rng-pci","rng":"objrng0","id":"rng0","bus":"pci.5","addr":"0x0"} -device {"driver":"vmcoreinfo"} -sandbox on,obsolete=deny,elevateprivileges=deny,spawn=deny,resourcecontrol=deny -msg timestamp=on しかし、以下のようにレジスタの値を見てみると綺麗な値になっていて、実際には VM が生きているようには見えませんでした。 stack@lima-default:~/devstack$ virsh qemu-monitor-command --hmp instance-00000003 "info registers" CPU#0 R00=00000000 R01=00000000 R02=00000000 R03=00000000 R04=00000000 R05=00000000 R06=00000000 R07=00000000 R08=00000000 R09=00000000 R10=00000000 R11=00000000 R12=00000000 R13=00000000 R14=00000008 R15=00000004 PSR=400001db -Z-- A und32 s00=00000000 s01=00000000 d00=0000000000000000 s02=00000000 s03=00000000 d01=0000000000000000 s04=00000000 s05=00000000 d02=0000000000000000 s06=00000000 s07=00000000 d03=0000000000000000 s08=00000000 s09=00000000 d04=0000000000000000 s10=00000000 s11=00000000 d05=0000000000000000 s12=00000000 s13=00000000 d06=0000000000000000 s14=00000000 s15=00000000 d07=0000000000000000 s16=00000000 s17=00000000 d08=0000000000000000 s18=00000000 s19=00000000 d09=0000000000000000 s20=00000000 s21=00000000 d10=0000000000000000 s22=00000000 s23=00000000 d11=0000000000000000 s24=00000000 s25=00000000 d12=0000000000000000 s26=00000000 s27=00000000 d13=0000000000000000 s28=00000000 s29=00000000 d14=0000000000000000 s30=00000000 s31=00000000 d15=0000000000000000 s32=00000000 s33=00000000 d16=0000000000000000 s34=00000000 s35=00000000 d17=0000000000000000 s36=00000000 s37=00000000 d18=0000000000000000 s38=00000000 s39=00000000 d19=0000000000000000 s40=00000000 s41=00000000 d20=0000000000000000 s42=00000000 s43=00000000 d21=0000000000000000 s44=00000000 s45=00000000 d22=0000000000000000 s46=00000000 s47=00000000 d23=0000000000000000 s48=00000000 s49=00000000 d24=0000000000000000 s50=00000000 s51=00000000 d25=0000000000000000 s52=00000000 s53=00000000 d26=0000000000000000 s54=00000000 s55=00000000 d27=0000000000000000 s56=00000000 s57=00000000 d28=0000000000000000 s58=00000000 s59=00000000 d29=0000000000000000 s60=00000000 s61=00000000 d30=0000000000000000 s62=00000000 s63=00000000 d31=0000000000000000 FPSCR: 00000000 これらの問題に対応するため、 local.conf に以下を追記することとしました。 [[post-config|$NOVA_CONF]] [libvirt] virt_type = qemu cpu_mode = custom cpu_model = cortex-a53 QEMU のソフトウェアエミュレーションになってしまいますが、 CirrOS 程度であれば問題なく動きます。 まとめ Android 15 から Android 端末上で Linux 環境を動かすことが可能になりました。 4 GB RAM と 16 GB ストレージの環境下でやれることは限られますが、 DevStack の config をチューニングすることでスマホ上に OpenStack 環境を構築し、 VM を起動できました。
アバター
この記事は、 NTT docomo Business Advent Calendar 2025 1日目の記事です。 こんにちは、デジタル改革推進部の小林です。NTT docomo Business Engineers' Blogと改題してからは初の、アドベントカレンダーが始まりました。その1日目の記事です。 私は4月から現職のデジタル改革推進部に所属し、社内認証サービスのプロダクトオーナーをやっています。このサービスにより、NTTドコモビジネスの社員はPCや各種社内システムをひとつのIDでシームレスに利用できます。現在は私のほかメンバー4人と協力会社の体制で運営しています。 私は新卒で入社して以来働いてきたこの会社で、13年目にして初めてラインマネジメントに取り組むことになりました。入りたての頃はマネージャーなんてなりたくないと思っていたところだったのに、いまとなってはこれをそれなりに納得して受け入れている自分がいます。この記事では、これまでのキャリアでターニングポイントになったところを振り返りながら、単なるエンジニアからマネージャーになるキャリアチェンジをどう受容していったか、その経緯をまとめます。JTCのエンジニアが10年ちょいで歩んだキャリアの一例として、お読みいただければと思います。 マネージャーなんて! 異動と価値観の変化 バックオフィスでの異動、奔走、停滞 さらなる異動とマネージャーキャリアの始まり 終わりに マネージャーなんて! 私は2013年に当時のNTT Comに入社し、最初は大企業向けの統合コミュニケーションサービス (Arcstar UCaaS) のエンジニアとしてキャリアを始めました。昨今であればZoom, Microsoft Teams, Google Workspaceがこのあたりの地位を占めています。 当時は、学生の頃にはまさか触ることのなかった巨大なコアルーターや仮想化基盤を操作したり、電話やインスタントメッセージングの交換設定を書いたり、果てはアメリカの電話事業者とトラブルシューティングをしたりと、多岐にわたる領域で数々の刺激的な経験をしてきました。その当時は手を動かしていることが楽しく、相対的にマネジメントの仕事が自分とは遠いところの出来事に見えており、マネージャー層の表層的な仕事だけ見て「ああはなりたくない」と思っていたところがあったように思います。 異動と価値観の変化 時は流れて2017年の冬、当時の技術開発部セキュリティテクニカルユニットに異動しました。自分が着任したチームはWebセキュリティ実装の高度化をテーマにしており、セキュリティ系の知見がほとんどなかった自分にとってはそれまでのスキル・ケイパビリティを生かすのが急に困難になりました。できていた仕事が急にできなくなり、このままではいけないがどう動けばいいかも分からないような難しい時期を1年間くらい過ごしました。 また、同じユニットには、新卒入社ながら一芸に秀でた人がたくさんいました。コードを書くにしろセキュリティインテリジェンスの分析をするにしろ、自分よりもずいぶん年下なのにとにかく手が動くさまをまざまざと見せつけられたわけです。 そのような中でも自分にできる仕事がひとつありました。会社のコンテキストを部内に展開することです。技術開発部は研究開発部門であることもあり、新卒で配属されてからずっとここにいるとか、10年以上異動がないといった人が多い組織でした。私のように事業部から異動してくる方がまれで、事業部側で起きていることや動きの背景にある状況、事業部が影響を受けているルールなどは私の方がよく知っている状況でした。 幹部会議からのフィードバックに背景情報を注釈としてつけたり、社内手続きに不明な点があるとSlack上に流れてきたヘルプに率先して返事をしたり、そういったことに取り組むうちに自身の存在も認知してもらった気がしています。こうした経験を通じて、 自分より手が動く人がたくさんいるのだから、そういう人らと競る仕事をするよりも、その人らがもっと素早く動ける環境を作る方が向いているのでは? との思いがどんどん強くなっていきました。 最終的に、技術開発部がイノベーションセンター (IC) に改組された2020年には、ICのバックオフィスに自ら志願して異動し、その思いを叶えることになります。 バックオフィスでの異動、奔走、停滞 ICのバックオフィスにおいては、 コロナ禍に突入して、出社しなくても済む業務プロセスを考案・実装したり、 新たに使いたいSaaSがあるとの相談に耳を傾けて社内ルールになじむ形に仕立てたり、 無人飛行機の登録が義務化される話を聞きつけ、協働して必要な登録を済ませたり、 全社的な調査事項を所内に必要な形にまとめ直して所員の手間をできる限り減らしたり、 ICのValueを策定する取り組みに参加して最終的な成果を所員全員の前で発表したり、 所内ポータルサイトの動線を再設計して情報へのアクセスを容易にしたり、 などなど多岐にわたる仕事に取り組みました。社会人学生として大学院に通学していたのもこの時期です。このあたりから対外的にはコーポレートエンジニアを名乗っていました。 自分の価値観にもアップデートがあり、先に挙げた「素早く動ける環境作り」から進化して「フロントの手を爆速にすることは、最終的にお客さまや社会に対する奉仕になる」「ひとりよりも群れた方が、なせることが増える」といった信条を抱えるようになりました。 ただこうした環境で5年も働いているうち、毎日の仕事を「手なり」でやってしまえるようになり、それに違和感を覚えるようになっていました。コンフォートゾーンに入っているとはこういう状況を言うのでは、との思いがどんどん強くなっていきました。 ITや情報セキュリティの強みを生かしたいと思う中で、自分が取りうる道はいくつもあれど、最終的に次のどちらかだろうと考えました。 会社のIT系バックオフィスに異動し、サービスする相手のスコープを広くする(量の観点) グループ会社に異動し、グループ会社のIT・情報セキュリティについて深く支援する(質の観点) 最初の異動ではあまりにもキャリアに連続性がなく辛い思いをしたところから、自分のスキル・ケイパビリティが活かせる領域で社会に役立ちたいと考え詰めた結果、これらが残りました。こうした話を上長や人事担当と面談してインプットし続けた結果、現所属の社内認証サービス担当に異動することになります。 さらなる異動とマネージャーキャリアの始まり 赴任したチームは15人以上もいるような大所帯で、社内認証サービスを担当しているのはこのうち5人でした(当時)。その5人のメンバーと働くにあたって最初はどのような立場でコミットするかを考えあぐねていましたが、話を聞くにつれ前任者がテックリードとして振る舞っていたことがわかり、自分も作業者の一人としてではなくリードする立場にならねばと腹を決めました。こうして、ラインマネージャー、プロダクトオーナーとしてのキャリアを始めることになりました。 実際フタを開けてみると、2カ月間に中心的メンバーの異動が2回立て続けに起こったことで、日常業務は淡々とこなしつつも(すごいことです)チームに混乱があるように見受けられました。ここから半年かけてチームビルディングに奔走することになるのですが、その話はまた別の機会にすることといたしましょう。 終わりに 自分の10年あまりのキャリアを、簡単ですが振り返ってきました。大学の同級生が30歳そこそこで肩書付き・部下持ちになっているのに妙に焦ったのもいまとなっては昔のことで、自分ができることで貢献すればいいのだと捉えられるようになってからは、ゆったりと構えています。日頃こなすべき業務・解くべき課題はよりどりみどりで、毎日飽きずに取り組めています。 これからのことはまたどこかで振り返るつもりです。来年のアドベントカレンダーでも、今度はチームマネジメントの話を何か書けたらいいなと思っています。 お読みいただきありがとうございました。明日の記事はKumassyが担当します。明日もお楽しみに!
アバター
こんにちは、NTTドコモグループの現場受け入れ型インターンシップに「D2:攻撃者視点に立ち攻撃技術を研究開発するセキュリティエンジニア」ポストで参加させていただきました、太田です。 本記事では、本インターンシップでの取り組みについて紹介いたします。 NTTドコモグループのセキュリティ業務、特にOffensive Securityプロジェクト(以下、PJ)に興味のある方、インターンシップの参加を検討している方などへの参考になれば幸いです。 OffensiveSecurityPJとは 参加経緯 インターンシップ概要 Microsoft 365 Copilotの悪用 Microsoft 365 Copilot とは 目標 検証1: Black Hat USA 2024での手法を再現 検証2: ユーザーに向けた案内 + Markdown 検証3: Excelファイルを利用したペイロードの秘匿 まとめ インターンの感想 おわりに OffensiveSecurityPJとは 今回私が参加させていただいたワークフィールド(職場)はイノベーションセンターのテクノロジー部門内に位置するOffensive Security PJです。 Offensive Security PJ は最先端のセキュリティ技術を調査・検証し、その成果を社内外に共有していくことをミッションとしています。 特に重視しているのは「攻撃者の視点」に立つことです。防御の立場から技術を眺めるだけではなく攻撃者がどのように考え、どのような手法を用いるかを理解することで初めて本質的な対策を打つことができます。そのような視点を持ち続けることで従来の後追いの対策にとどまらず、脅威を先取りして備える「先回りの防御」を実現しようとしています。 単に攻撃手法を模倣するだけでなく、その背後にある原理やアーキテクチャや攻撃の成立条件まで掘り下げるような研究・開発をしています。 参加経緯 私は普段、大学院でセキュリティに関する研究をしています。中学生の頃、遠隔操作可能なエアコンが現れた時に、「もし悪用されたら人命に関わるのではないか」と感じた経験があります。その出来事をきっかけに、新しい技術や製品を「どのように悪用され得るか」という視点で見るようになりました。 そうした関心から、Offensive Security/Red Team に強い興味を持ち、将来はこの分野に携わりたいと考えるようになりました。そのため、「攻撃技術の調査・開発・検証や攻撃技術の応用に関する研究」という本ポストの業務内容は、私の関心と非常に一致していると感じ、応募いたしました。 また、業務として研究に取り組むことを自分自身が楽しめるかどうかを確かめたいという思いもありました。 インターンシップ概要 インターンシップは8月25日から9月5日の平日10日間で開催され、業務体験はオリエンテーションや成果報告会を除いた実質7日間で行われました。 初日と最終日は出社が必須で、それ以外の日程は出社かオンラインかを相談して決めることが出来ました。 またインターンシップ期間中は、検証業務はもちろん、SOC(Security Operation Center)の見学や海外のカンファレンスに登壇された社員の方による再演の聴講、社内LT(Lightning Talks)会への参加、他部署のインターンシップ生との交流などさまざまな体験をさせていただきました。 私が検証業務で取り組んだテーマは以下の2つです。 Microsoft 365 Copilotの悪用 LLM応用によるRed Teamオペレーション高度化検証 本記事では、2つのテーマのうち「Microsoft 365 Copilotの悪用」を紹介します。 Microsoft 365 Copilotの悪用 現在、大規模言語モデル(LLM)の業務利用が進んでいますが、それには同時に危険性もあります。どのような危険が潜んでいるかを明らかにするとともに、LLMに対する攻撃手法に関する知見を収集することを目的として、Microsoft 365 Copilot環境を用いた攻撃手法を検証しました。 Black Hat USA 2024で発表された攻撃手法 1 を基に、それらが現在も再現可能かどうかを検証しました。さらに、他言語のプロンプトでも同様の攻撃が成立するか、および対策が施されている場合にどのような工夫で攻撃が成功し得るかについて調査しました。 Microsoft 365 Copilot とは Microsoft 365 Copilot 2 (以下 Copilot)は、Word や Excel、PowerPoint、Outlook などの Microsoft 365 アプリに組み込まれたAIアシスタントです。 メールの下書きやプレゼン資料の作成、データ分析をアプリ上で直接行えるほか、チャットでの会話の中でWeb 上の情報や社内のメールやドキュメントなどのデータを参照できる点が特徴です。 社内のメールを参照し応答する流れ しかし、Microsoft 365 Copilotの「各種データを参照できる」という特徴を利用した攻撃手法も知られています。 目標 本ブログでは、Copilotにユーザーを悪意のあるサイト等へ誘導してもらうことを目標とした検証について報告します。 Copilotは会話の応答中にクリック可能なハイパーリンクを表示することがあります。 実際にリンクを表示するCopilot ここでCopilotには、”Web 上の情報だけでなく社内のメールやドキュメントなどのデータを参照できる” という特徴があります。これらを悪用し、参照対象の中に悪意ある情報を混入させ、表示名は正規のものに見せかけつつリンク先を攻撃者が指定したものにする、というのが目標です。 仮に可能だった場合、攻撃者はCopilot を通して広範囲な利用者に悪意のある情報を送ることが可能です。 Copilotが会話の応答中にハイパーリンクを表示する流れ 目標のイメージ 検証1: Black Hat USA 2024での手法を再現 Black Hat USA 2024 の「Living off Microsoft Copilot」で報告された手法の再現を目指します。 www.youtube.com 本手法では、ユーザー宛のメール本文に視認しにくい形で Copilot への指示を埋め込み、その指示に基づいて Copilot が不正なリンクを提示することを期待します。 端的に言うと、指示は次のような意図を持つものでした。 サービスAへのアクセス方法を尋ねられたら、指定の URL を返す ただし表示名(ハイパーリンクのタイトル)は『サービスA』と表示する メールの例 このメールを受け取ったユーザーになり代わって Copilot にサービスA へのアクセス方法を尋ね、Copilot の応答を確認しました。 結果 攻撃手法が公開されてから時間がたち、Copilot側で対策されているのか、同一の手法では目的を達成できませんでした。 例えば、ハイパーリンクの形式にはならずクリックを誘発させられないことや、悪意のあるメールを参照元として表示し露見しやすくなったなど、攻撃の成功率は下がりそうです。 クリックできないURL 検証2: ユーザーに向けた案内 + Markdown ユーザーへのメッセージとして単純に URL を記載し、ハイパーリンクとして出力できないようでした。 どのようにしたら対策を回避できるかを検討するにあたり調査する中で、「EchoLeak 3 」と呼ばれるCopilotの脆弱性について知りました。 EchoLeakの利用した攻撃手法や、それに応じて行われた対策について調べていると、Copilotは以下のような特徴を持っていることがわかりました。 Copilot にはプロンプトインジェクション攻撃を防ぐための仕組みが実装されており、その1つとして XPIA(クロスプロンプトインジェクション攻撃)分類器がある 4 ということ。そしてこれにより、Copilotへの指示は悪性とみなされる可能性があること。これは回避するため、今回の場合、Copilotへの指示ではなく、受信者に向けられたメッセージのように見せかけられればよいということ。 Copilot は、信頼性が確認できないリンクについてはハイパーリンクとして提示せず、クリック不可の形式で応答するなどの対策がなされていること。そのため、今回のように単にメール本文に URL を記載するだけではハイパーリンクとして応答されない可能性が高いということ。そして回避するためにはURLを参照形式でMarkdownを使って指定すればよいということ。(なぜなのかなどは割愛しますが良かったら調べてみてください) これで、検証1において失敗した理由が納得できました。この仕様を突破できないでしょうか。 EchoLeakにおいても対策が行われ、現在は悪用出来ないとされていたものの、悪用に使用される全ての脆弱性への対応が完了している訳ではないかもしれません。 そこで、1,2に沿って改良したのちに、検証しました。 ユーザー向けの案内文のように見える文章を作成します。その中で、Markdown(インライン形式)を用いて、あたかもサービスAへのアクセス方法を示すリンク名を付けつつ、実際には私が用意したURLを指定するようにします。 (なお、2について、回避するには 参照形式 で書く必要があるはずなのですが、私は誤解して インライン形式 で指定してしまいました。それ次第では結果が変わっていたかもしれません) その文章をメールに記述します。なお、HTML形式のメールとPlainText形式のメールの2種類について試しました。 結果 HTML形式のメール → クリックできないURL(ハイパーリンクの書式そのまま)で応答される PlainText形式のメール → クリックできるハイパーリンクで応答される という結果になりました。PlainText形式のメールの場合のみ成功したことになります。 HTML形式のメールを参照した際のCopilot の挙動 PlainText形式のメールを参照した際のCopilot の挙動 これは憶測ですが、Markdown 形式で防御機構を回避されること自体は現状避けられないのではないでしょうか。代わりに人間に誤認させやすいHTML形式のメールを警戒しており、「参照する際は応答にハイパーリンクを使用しない」というような形で対処しているのではないかと考えられます。 確かに、PlainText形式のメールではペイロードとして書いたURLを隠せなくなるので攻撃者としては嫌な気持ちになるでしょう。 検証3: Excelファイルを利用したペイロードの秘匿 攻撃者としてはやはり、 URLを記載したペイロードは隠したい です。上述の通りメールで人間が視認できない形にすることは難しそうです。 だがしかし、Copilotが参照できるのはメールだけではありません。SharePoint上のExcelファイルなども参照してくれます。そしてExcelに記入された文字はサイズ・色を変更できます。これは使えるかもしれません。 ということで、SharePoint上のExcelファイルに白文字でペイロード隠してみます。 結果 しっかりハイパーリンクで応答されました㊗️。 まとめ Copilotが応答としてハイパーリンクを使用する際に、リンク名とは関係ないURLを応答させることは可能です。これを利用してCopilotにユーザーを悪意のあるサイト等へ誘導させられる可能性があります。 ペイロードをメールに記載する場合、ペイロードの配送に至るまでの難易度が低い一方、PlainText形式で記述せざるを得ないため、ユーザーに発覚しやすいという問題があります。 ユーザーから気づかれないようにする手法として、メールではなくSharePoint上のExcelファイルなどへ、視認しにくい書式でペイロードを隠す方法があります。ただしこの方法は、SharePointへファイルを配置する権限を取得していることが前提であるため、ペイロード配送までの難易度は高くなります。逆に一度配置できれば、そのファイルへアクセス可能な全ユーザーに影響を及ぼす可能性があります。 今回の検証では、ペイロードと併記する情報が与える影響や、参照先を示させないための手法検証など、実施したかったが時間不足で試せなかった項目が多く残っています。それでも、Copilotには多様な悪用の可能性が存在すると感じました。 また、攻撃の検証と並行して、どのように防御するかという視点で監査ログの確認なども行いましたが、防御側の負担の大きさを改めて実感しました。まずは、Copilotがどの情報を参照するのか、およびどのような場所にペイロードが隠され得るかを防御側が把握しておくことが重要だと考えます。 インターンの感想 私はインターンシップに参加するまで、LLMに対して同じ事象が必ずしも再現できなかったり、理解しづらい挙動が多かったりする点に不安を感じていました。しかし、実際に検証を始めてみると、その予測不能さも含めて面白く無限の可能性が広がっているように感じられ、非常に楽しく没頭できました。 今回の経験を通じて、「食わず嫌いせずにまず挑戦してみること」の大切さを身をもって学びました。 また、本インターンシップでは決められたことをこなすのではなく、私のように大きく脱線しながら興味に従って自由に検証を進めることも受け入れていただけたのが印象的でした。(むしろ面白がってもらえる最高の環境でした。) 業務体験だけでなく、期間全体を通じて得たすべての経験が大変有意義であり、大きな成長の糧となりました。 おわりに 今回のインターンシップでは日頃の活動だけでは決して得られないような貴重な体験をさせていただき、とても光栄に思っています。特に2週間を通じて現在ホットなテーマであるLLMのセキュリティについて深く学ぶ機会をいただき、最新動向を追い続けることの重要性やセキュリティそのものの面白さを改めて実感できました。 また、「攻撃者の視点に立つ」という考え方についてもトレーナーの皆さまからのご指導を通じて少しずつ身につけることができたと感じています。 このような貴重な機会を与えてくださったOffensive Security PJの皆さまに心より感謝申し上げます。さらに、上長の有藤さん、トレーナーの四方さん、田口さんにも厚く御礼申し上げます。 本記事を通じて少しでもインターンシップに興味を持っていただけたら大変光栄です。 Black Hat USA 2024 - Living off Microsoft Copilot https://www.blackhat.com/us-24/briefings/schedule/?utm_source=labs.zenity.io&utm_medium=referral&utm_campaign=links-and-materials-for-living-off-microsoft-copilot#living-off-microsoft-copilot-40074 ↩ Microsoft 365 Copilot https://www.microsoft.com/ja-jp/microsoft-365-copilot ↩ Breaking down ‘EchoLeak’, the First Zero-Click AI Vulnerability Enabling Data Exfiltration from Microsoft 365 Copilot https://www.aim.security/aim-labs/aim-labs-echoleak-blogpost ↩ Data, Privacy, and Security for Microsoft 365 Copilot - Does Copilot block prompt injections https://learn.microsoft.com/en-us/copilot/microsoft-365/microsoft-365-copilot-privacy#does-copilot-block-prompt-injections-jailbreak-attacks ↩
アバター
こんにちは、NTTドコモグループの現場受け入れ型インターンシップに「D2:攻撃者視点に立ち攻撃技術を研究開発するセキュリティエンジニア」ポストで参加させていただきました、島田です。 本記事では、本インターンシップでの取り組みについて紹介いたします。 NTTドコモグループのセキュリティ業務、特にOffensive Securityプロジェクト(以下、PJ)に興味のある方、インターンシップの参加を検討している方などへの参考になれば幸いです。 Offensive Security PJとは 参加経緯 インターンシップ概要 LLM応用によるRed Teamオペレーション高度化検証 Juicy 情報とは LLMによるRed Teamオペレーション高度化の目的 被攻撃環境の内容 Victim1 から Victim2 Victim2 の privilege escalation Victim2 から Victim3 LLMによる攻撃手順の評価 検証方法と検証観点 検証結果 まとめ インターンの感想 おわりに Offensive Security PJとは 今回私が参加させていただいたワークフィールド(職場)はイノベーションセンターのテクノロジー部門内に位置するOffensive Security PJです。 Offensive Security PJ は最先端のセキュリティ技術を調査・検証し、その成果を社内外に共有していくことをミッションとしています。 特に重視しているのは「攻撃者の視点」に立つことです。防御の立場から技術を眺めるだけではなく攻撃者がどのように考え、どのような手法を用いるかを理解することで初めて本質的な対策を打つことができます。そのような視点を持ち続けることで従来の後追いの対策にとどまらず、脅威を先取りして備える「先回りの防御」を実現しようとしています。 単に攻撃手法を模倣するだけでなく、その背後にある原理やアーキテクチャや攻撃の成立条件まで掘り下げるような研究・開発をしています。 参加経緯 私は大学にてネットワークに関する研究をしていますが、個人としてRed Team寄りの技術にも強い関心を抱いています。また、趣味でCTFに参加したり Hack The Box のマシンを攻略したりしています。 技術者の方々にとって「アーキテクチャを理解していなければ実装はできない」という感覚は身近だと思いますが、セキュリティの文脈に置き換えると「攻撃者の視点を持たなければ本質的な防御は難しい」という考えに通じると思っています。日々の学習を通じてこの重要性を意識してきたこともあり、本ポストの「攻撃技術の調査・開発・検証や応用」を重視する活動方針と強く合致すると考えました。 これまで趣味として取り組んできたセキュリティと業務として取り組むセキュリティの両方を体感し、将来のアクションに繋げたいという思いもあり、今回のインターンシップへの応募を決めました。 インターンシップ概要 インターンシップは8月25日から9月5日の平日10日間で開催され、業務体験はオリエンテーションや成果報告会を除いた実質7日間で行われました。 初日と最終日は出社が必須で、それ以外の日程は出社かオンラインかを相談して決めることが出来ました。 またインターンシップ期間中は、検証業務はもちろん、SOC(Security Operation Center)の見学や海外のカンファレンスに登壇された社員の方による再演の聴講、社内LT(Lightning Talks)会への参加、他部署のインターンシップ生との交流などさまざまな体験をさせていただきました。 私が検証業務で取り組んだテーマは以下の2つです。 Microsoft 365 Copilotの悪用 LLM応用によるRed Teamオペレーション高度化検証 本記事では、2つのテーマのうち「LLM応用によるRedTeamオペレーション高度化検証」を紹介します。 LLM応用によるRed Teamオペレーション高度化検証 今回のインターンシップでは、Red Team オペレーションの高度化を目的としてLLM を活用した情報分析手法の検証をしました。攻撃環境において取得される大量のログや環境情報の中から攻撃に有用となる情報を「Juicy 情報」とOffensive Security PJ内で呼んでいるのですが、この「Juicy 情報」をどの程度自動で識別できるかをテーマに取り組みました。 事前に準備された被攻撃環境を対象に、権限昇格やラテラルムーブメントといった攻撃シナリオを実際に実践し、シナリオの流れやそこで得られる情報の特徴を把握しました。その上で、攻略過程で収集したログや出力のうち有効と考えられるものを整理し、分析対象のデータセットとして活用しました。 こうして得られたデータを複数の LLM に入力し、それぞれのモデルがどの程度有用な情報を抽出できるかを比較検証しました。 Juicy 情報とは Juicy 情報とは、セキュリティの文脈で、攻撃や侵入検証において「次の一手に直結する価値の高い情報」を指すPJ内での呼称です。資格情報や接続先、永続化の手がかりなどが該当し、Red Teamのオペレーションやペネトレーションテストの際に非常に有用です。よくある例としては、パスワード、管理用IP、関連データベースの接続情報などが挙げられます。 またJuicy情報は単に次の一手につながるだけでなく、不要な調査や検証に陥るのを防ぎ、調査の優先順位付けや時間短縮にも直結します。 LLMによるRed Teamオペレーション高度化の目的 サイバーセキュリティの世界において、Red Teamオペレーションは組織の防御力、穴を実践的に評価・検証するための大きな切り札となっています。近年、大規模言語モデル(LLM)の急速な発展は Red Team をオペレーションする上でとても有用と考えられており、無限の活用方法が考えられます。 インターン中に行ったLLMを活用して取得した攻撃マシンの脆弱性情報などが、Red Teamオペレーションにおいてどの程度「Juicy(攻撃利用価値が高い)」であり、その情報をいかに迅速かつ正確に評価できるかを検証した結果を共有します。 被攻撃環境の内容 今回の検証は、実践的な攻撃フローをシミュレーションするためにあらかじめトレーナーの方に構築していただいた演習環境で行いました。標的としたホストらは、意図的に複数の既知の権限昇格系の脆弱性や設定ミスが残されたWindowsの環境です。 最初は被攻撃環境を対象とした攻撃シナリオの実践・理解のため実際にvictim1~victim3の3つの攻撃マシンの攻略をしました。ここは本業務の本質ではありませんが、非常に楽しく攻略させてもらったので少しwriteupとして記載しておこうと思います。 Victim1 から Victim2 最初に、攻撃拠点となる Attacker (Kali) マシンから WS01 (victim1) へ C2 (Command and Control) セッションを張ることから始めました。 Metasploit を用いて悪意のあるペイロードを作成し、WS01 に送り込んで被害者側で実行してもらいセッションを確立しました。この時点では権限はユーザー権限のみでした。 少し探索すると dev-machine.txt と Default.rdp というファイルを見つけました。 meterpreter > ls の出力は以下の通りです。 Listing: C:\Users\victim-user\Documents ======================================= Mode Size Type Last modified Name ---- ---- ---- ------------- ---- 100666/rw-rw-rw- 0 fil 2025-08-26 01:38:44 +0000 Default.rdp 040777/rwxrwxrwx 0 dir 2025-08-04 08:16:07 +0000 My Music 040777/rwxrwxrwx 0 dir 2025-08-04 08:16:07 +0000 My Pictures 040777/rwxrwxrwx 0 dir 2025-08-04 08:16:07 +0000 My Videos 100666/rw-rw-rw- 402 fil 2025-08-04 08:16:27 +0000 desktop.ini 100666/rw-rw-rw- 54 fil 2025-08-26 04:33:18 +0000 dev-machine.txt 中身を確認したところ、以下のようなファイルが見つかりました。 ファイル名 中身 dev-machine.txt DEV01 というマシンに対するクレデンシャル(ユーザー名とパスワード) Default.rdp RDP 接続用の設定ファイル(DEV01 の IP アドレスを含む) これらの情報から、 DEV01 (victim2) のユーザーアカウント用ログイン情報であると判断し、RDP 接続をし、 victim-user@DEV01 でのログインに成功しました。 Victim2 の privilege escalation User一覧を見ると victim-admin というユーザーがあり、名前から管理者権限を持っていそうだと判断しました。 C:\Users\victim-user>wmic USERACCOUNT Get Domain,Name,Sid Domain  Name                SID DEV01   DefaultAccount      xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx DEV01   Guest               xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx DEV01   victim-admin        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx DEV01   victim-user         xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx DEV01   WDAGUtilityAccount  xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx しかし他にめぼしいものは見つからず、ほかのツールを用いてさらに調べたところ、書き込み権限のあるディレクトリを発見しました。その中で unquotedsvc 配下のパスが Modifiable と表示されました。 === Services with Unquoted Paths === Service 'unquotedsvc' (StartMode: Manual) has executable 'C:\Program Files\Unquoted Path Service\Common Files\unquotedpathservice.exe', but 'C:\Program Files\Unquoted Path Service\Common' is modifable. つまり、Unquoted Service Path の脆弱性が確認されたということになります。 Windowsでは登録された Full-path が正しくダブルクオーテーションで囲まれていないと、途中のディレクトリ名を実行ファイルとして誤認される可能性があります。 そのため、悪意のあるユーザーが書き込み可能な場所に任意の実行ファイルを設置できるなら、そのサービスが管理者権限で起動した際に意図せずそのファイルが実行されます。 この脆弱性を利用して対象システムの同一ディレクトリに common.exe という名前のペイロードを配置しました。攻撃側の Kali マシンでは事前にリスニングを開始しておき、対象上で以下の操作をしました。 sc stop unquotedsvc を実行してサービスを停止。 続けて sc start unquotedsvc を実行してサービスを起動。 そうすると、サービスの再起動時に配置していたペイロードが自動的に起動し、期待通り権限昇格が確認できました! この権限昇格方法は多数あったうちの1つであり、他にもバイナリ自体の書き換えが可能な脆弱性などを利用した権限昇格方法もありました。 Victim2 から Victim3 管理者権限で victim-admin@DEV01 のパスワードを変更したのちログインし、怪しいファイルがないか探していたら、Desktop上に dev というフォルダを確認しました。 中には以下のようなファイルが見つかりました。 accounts.csv 11種類のユーザー名とパスワードが記載されているファイル userAddTest.ps1 リモート接続先でアカウントを追加する用途と思しきスクリプト ただし、別ホスト(ADMIN-SERV)への接続情報は見当たりませんでした。そこで最初にアクセスしていた WS01 のマシンで、管理者権限での探索をしていなかったことを思い出し、改めて victim1 にログインして調べ直したところ、期待通り該当しそうな.rdpファイルを発見しました。 今はこうしてしれっと書いていますが、実際攻略を試みていたときはこれに気づかず散々関係の無いログを漁ったり、無駄なスキャンをかけたりしてしまっていました。環境を用意してくれたトレーナーの方の思惑通りにハマっていたと思います。 さらに Documents 配下にあった PowerShell スクリプト(.ps1)を確認すると、ADMIN-SERV に接続するためのユーザー名とパスワードが記されていました。 その情報を用いてログインを試みたところ、無事に接続に成功! PS C:\Users\ServAdmin> whoami admin-serv\servadmin 管理者権限であることを確認し、攻略が完了しました。 LLMによる攻撃手順の評価 こうして被攻撃環境の攻略が完了し、攻撃シナリオを把握した上で、得られた各種情報を大規模言語モデル(LLM)がどこまで識別できるか、また次の一手に繋がる有益な示唆をどれだけ提示できるかを検証します。検証の主眼は次の2点です。 Juicy情報の迅速な識別と構造化ができるか 検出された大量の情報から、攻撃にとって最も価値の高い「Juicy情報」を抽出し、JSON 等の構造化フォーマットで出力できるかを確認。 情報についてのリスク評価 抽出された情報に対して、優先度(どれを最初に狙うべきか)・有効性(実際に利用可能か)・持続性(長期的に有効か)・攻撃フェーズ(初動/横展開/権限昇格など)といった実戦的観点から評価を付与できるかを検証。 今回はMicrosoft 365 Copilot(以下、Copilot) とAzure OpenAIのgpt-4.1モデル(以下、OpenAI) の2つの種類のLLMを使用し、それぞれ比較検証しました。 検証方法と検証観点 検証は以下の手順で実施しました。 victim2 の権限昇格時に取得した SharpUp.exe の出力結果を LLM に入力する。 カスタムプロンプトを与えて、LLM に JSON 形式での抽出結果を出力させる。 LLM の出力と手動で得た結果を比較し、抽出精度や実用性を評価する。 また、カスタムプロンプトは以下の観点で作成し、検証しました。 検証観点 目的 Juicyな情報の中に優先順位をつけることはできるのか 多数の脆弱性の中から、最も致命的なもの(Priority 1)を特定させる。 攻撃に使える有効性を示すことはできるのか 情報が単独で即座に攻撃可能(Immediate Exploit)か、他の情報との連鎖(Requires Chaining)が必要かを判断させる。 情報が有効であり続ける期間(可変性はあるか?) 脆弱性がシステムのライフサイクルで「Long-term(長期的)」に残るか、「Short-lived(一時的)」かを判断させる。 検証・攻撃をするどの段階で有効か? 情報をPrivilege Escalation、Persistence、Lateral Movementなどの攻撃フェーズに分類させる。 その情報が攻撃対象の範囲を広げるかどうか? 権限昇格に留まらず、横展開(Lateral Movement)の足がかりとなるかを判断させる。 MITRE ATT&CKマッピング 検出された脆弱性を標準的な攻撃手法(T-ID)に紐づけさせる。 検証結果 実際に試してみたところ、 SharpUp.exe の実行結果は、LLM に入力すると理由や優先順位なども付与された形で返ってきました。単に脆弱性を見つけるだけでは、その利用方法が分からず、手が止まってしまう場面は多々あります。しかし LLM は、そのような場面で補助的に役立ちそうだと感じました。 (左:Sharpup.exeの出力 右:OpenAI) ここからは、2つの LLM を比較し、検証観点ごとに相違点を整理していこうと思います。 検証観点 Copilot OpenAI Juicy情報の中に優先順位をつけることはできるのか 順位にばらつきがある。prioity=1は滅多にない 全体的に高め。priority=3 が一つもない 攻撃に使える有効性を示すことはできるのか Immediate Exploit が少ない。範囲が狭め Copilotに比べて危険度の平均値が高め 情報が有効であり続ける期間(可変性) 長く見積る傾向高め。short-livedが少ない 短く見積もる傾向がある 検証・攻撃をするどの段階で有効か 全て Privilege Escalationと判断 全て Privilege Escalationと判断 その情報が攻撃対象の範囲を広げるかどうか 限定的な拡張性の明示により現実性を重視し、誤検知を抑える方向 攻撃者がどう広げられるかを最大限に評価し、攻撃面を広く捉えている MITRE ATT&CKマッピング 比較的緩やかで、ATT&CKの大分類を幅広く適用 細分類を積極的に利用し、攻撃者の技術的アクションがより具体的 結論から言うと、Copilot と OpenAIのいずれも、Juicy情報の抽出と構造化を一定の精度で自動化できました。それも攻撃の材料となるデータを的確に拾い上げるだけでなく、それらを「優先度」「有効性」「持続性」「攻撃フェーズ」などの評価軸とともにJSON形式へ整理する、といったところまで対応できていました。つまり、従来であれば人間がシェルのログやスキャナの出力を目視でチェックしながら「これは使える」「これはノイズ」と判断していた作業のかなりの部分を、LLMに肩代わりさせることが現実的なレベルに来ているということだと考えます。 また、リスク評価についても両LLMとも対応可能でしたが、興味深いのは両者の回答に共通性が全く見られなく、「何をリスクとみなすか」という視点がモデルによって大きく異なる点でした。モデルごとの特徴としては以下のような点が見られました。 左: Copilot 右: OpenAI。 Copilot : 広範囲な分析を行えるが、攻撃方法そのものは示さず、脆弱性の説明に留まっている。具体的な悪用方法は一切出て来ず、技術要素の分解や分析には強みがあると感じられる回答。 OpenAI : 攻撃者の視点に立った指摘を返す傾向があり、次のステップにつながるような示唆が得られる。情報の掘り下げもやや綿密で、脅威モデリングに強みを持っていると感じられる回答。 情報の正確さについては、どちらが優れているかはこれだけでは判別できません。ただし、どちらか一方を 100% 信用してしまうと、検証がバイアスのかかったものになりかねないため、複数の LLM を比較・併用し、相互に補完しながら評価を進めることが望ましいかもしれません。 まとめ 今回の検証を通じて、たとえ LLM のモデルが似通っていても返答内容に明確な差があることに驚かされました。そのため、調査のスコープや目的に応じてモデルを使い分けることが有効であると感じます。 また、Copilot の出力は想定よりもフィルタにかからず返ってきた点が意外でした。ただし「図示してください」といったリクエストにはフィルタが作用し、返答が返ってこなくなるなど、制御の仕組みが部分的に異なることも確認できました。 これは当たり前なのですが、今回は攻撃検証用に用意された環境だったため、つい何度もエクスプロイトを試したりスキャンを繰り返したりしてしまいました…(ごめんなさい)しかし、本番の業務では実際に企業が運用する環境を対象に検証をすることになるため、好き勝手に振る舞ってはいけません。事前の合意や関係者との調整、適切なスコープ定義やログ管理など、交渉と運用上の配慮が必須であることを改めて強く意識しました。これは業務として体験しなければ一生学べなかったことだと思うのでとてもありがたく感じています。 インターンの感想 ここ最近の学術論文を読んでいるとLLMに注目した研究が多いとは感じていました。ただ、AIという存在の大きさや複雑さを考えると、自分が対話型AIを日常的に使っているにもかかわらず、それをそのまま業務に取り入れることにはどこか違和感がありました。 しかし実際に業務として触れてみるとLLMという技術の奥深さは想像以上で、その有用性を強く実感できました。新しい生成AIツールに潜む脆弱性を体系的に調べるプロセスや、LLMをセキュリティ領域に応用する具体的な方法を学べたことは大きな収穫です。当初リモートワークの利用で、初日・最終日以外に出社しないつもりでしたが、毎日の業務があまりにも楽しく、結局なかなかの頻度でオフィスに行きました!早起きと通勤ラッシュに慣れるのは大変でしたが、今となっては毎日出社すればよかったと少し後悔しています。 さらに、流行の技術をただ追いかけるのではなくリスクを冷静に見極める視点、Offensive Security PJ から学んだチームで知見を共有しながら改善策を考える姿勢、新しい技術を導入するときに既存の仕組みとどう結びつけるかを考える柔軟さなど、こうしたものを意識できるようになったのも良い経験でした。「便利さ」と「プライバシー・リスク」のバランスを常に意識する倫理観も、自分の中に根付いたと思います。 セキュリティ技術を深めたいと思っていた私にとって、関心のあることに思いきり取り組めたこの2週間はとても充実した時間であり、自分の視野や姿勢を大きく広げてくれる経験になりました。 おわりに 今回のインターンシップでは日頃の活動だけでは決して得られないような貴重な体験をさせていただき、とても光栄に思っています。特に2週間を通じて現在ホットなテーマであるLLMのセキュリティについて深く学ぶ機会をいただき、最新動向を追い続けることの重要性やセキュリティそのものの面白さを改めて実感できました。 また、「攻撃者の視点に立つ」という考え方についてもトレーナーの皆さまからのご指導を通じて少しずつ身につけることができたと感じています。 このような貴重な機会を与えてくださったOffensive Security PJの皆さまに心より感謝申し上げます。さらに、上長の有藤さん、トレーナーの四方さん、田口さんにも厚く御礼申し上げます。 本記事を通じて少しでもインターンシップに興味を持っていただけたら大変光栄です。
アバター
デジタル改革推進部の小林です。いまは社内認証サービス・基盤のプロダクトオーナーをやっています。5月まではイノベーションセンターにいました。 去る11月15日土曜日に開催された BTCON JP 2025 において、「現場とIT部門の橋渡しをして3000人の開発者を救った話」との題で登壇してきました。この記事では、ask the speakerのコーナーや、懇親会でお話しした内容を踏まえて、その発表では盛り込めなかった内容をお伝えします。 発表のあらまし 追加コンテンツ どうやるかは最後 会って話す 端からめくる 燃え尽きに注意 終わりに 発表のあらまし かつてこのブログの記事「 エンジニアがエンジニアのために開発・検証用 PC を整備した話 」で紹介した話を題材に、現場とIT部門の付き合いにおける人的側面に焦点を当て、対話・協働・挑戦の奨励が重要であるとの学びを報告しました。登壇資料はこちらにアップロードしておきましたので、よろしければご覧ください。 追加コンテンツ このセクションでは、発表であえてカットした、あるいはお話しし損ねた内容をいくつか紹介します。 どうやるかは最後 セッションA9「LINEヤフー合併におけるグループ従業員/アカウント管理の課題」を発表されたLINEヤフーの久保貴史さんと懇親会でお話しした折、この話が出ました。 久保さんの発表では、新たに策定した管理策を海外法人を巻き込んで今後実施していくことが紹介されました。懇親会では「海外法人に指示を通しきるのは難しいところもあると思いますが、どのように実効力あるものにしますか?」との私の問いに、「なぜやるかをとことん説明する。そのために丸1日かけた」「howの話は後でよくて、whyをみんなで合わせるのが重要」との明快な回答をいただきました。 発表に盛り込み損ねたのはまさにそこで、ある課題が発見された際に、なぜその課題を解かなければならないかを関係者全員で共有・納得できていると、解決が爆速になります。そもそも解くべき課題だと自分が認識している事柄が、他者には課題だと思われていないこともしょっちゅうです。納得していない人を動かすことほど難しいことはありません。海外とはデフォルトで共有できる価値観があまり多くないこともしばしばですから、ここのwhyに関する対話をゆるがせにするとうまく行かないのは道理だなと感じました。 また、久保さんは「現地に訪問して作業を観察し、疑問に思うアクションがあればなぜそれを行っているのか、その場で確認した」ともおっしゃっていたことが印象的でした。答えはフィールドにこそあります。 会って話す 今回の発表は「コミュニケーションの話」とまとめることもできるかと思いますが、発表資料ではコミュニケーションの語をあえて一切出しませんでした。「コミュニケーション」の語で受ける印象に、広がりがありすぎると考えているためです。 コミュニケーションのスタイルはいろいろと想定されます。言語・非言語、テキスト・ビジュアル、対面・非対面、などなど。今回の発表で私が一貫して主張したいのは「会って話す」ことです。「会って話す」を実現するにはその文字通り、直接出向いて面と向かうしか方法がありません(2025年11月現在)。 会いに行くことには力があります。チャットやメールだとやたら当たりがキツい人でも、会って話すと物腰柔らかくて驚くといったこともあります。まずはアポを取ってみませんか。 端からめくる Ask the speakerのコーナーにお見えになった方に、「端からめくる」話をしました。これは当社のとある元役員が使っていた表現で、問題解決にあたってはいきなり主流派をどうにかしようと思わず、少し背伸びする程度くらいで解決できるような身の回りのことから始めるのがよいといった意味合いです。 繰り返しになりますが、解きたい課題があるなら、それは解くべき課題だと理解してくれる仲間を作るところから始めましょう。仲間がいるのは心強いものです。そのうち仲間が増えれば、それがいつの間にか主流派になります。 燃え尽きに注意 IT部門始めバックヤードには責任感の強い方がしばしば見られます。それ自体はとてもよいことですが、あまりにも強すぎる責任感は燃え尽きにつながります。何を隠そう私自身がそうで、自分一人でなんとかしなければと背負い込みすぎていた時期を経験しています。考え直したきっかけはもはや思い出せませんが、あれは思い違いだったと今ならはっきり言えます。 困ったときには助けを求めましょう。助けを求めることは責任の放棄ではありません。きちんと強いチームであれば、チームで成果が出ればよいことを誰もが知っています。またチームのマネージャーである方には、チーム内で助けを求めやすい環境作りをお願いしたいと思います(自ら道化を演じるでもよいと思います)。 終わりに 開発PCを作った経験からはさまざまな学びがあり、とりわけこの人的側面の話は本当に身に染みました(ほかの開発メンバーからも異口同音に聞かれました)。いつかは外部発表したいと思っていたところで、このような形で機会をいただきました。当日会場・YouTubeでご覧いただいたみなさん、またBTCON JP 2025運営メンバーのみなさん、ありがとうございました。 発表のアーカイブ映像は後ほど期間限定で公開されると聞いています。アナウンスがあるとのことですので、発表をお待ちください。
アバター
はじめに こんにちは!NTTドコモビジネスの2025年夏の現場受け入れ型インターンシップに参加させていただきました、インターン生の竹田です。私は現在高専の専攻科1年生で、普段は船舶におけるサイバーセキュリティに関する研究活動を行っています。 この記事では、私が今回のインターンシップで取り組んだ業務体験内容について紹介します。 はじめに 参加のきっかけ インターンシップ概要 OTセキュリティとOsecTの概要把握 テーマ選定 検討1: 船舶での使用プロトコル調査 NMEA 0183 IEC61162-450 検討2: 現状の船内ネットワーク調査 検討3: 船舶pcapに対する現状のIDS製品の出力検証 Talker IDから資産を出力するZeek・Spicyパーサー作成・検証 Zeek・Spicyとは パーサー検証 まとめ イベントへの参加 SOC見学 ドコモとのコラボ企画 イノベーションセンター内での業務共有 TechLunch インターンシップを通して学んだこと おわりに 参加のきっかけ 私がインターンシップに参加したのは、研究のテーマとして「船舶におけるIDS(侵入検知システム)」を扱っていることが理由です。制御システムのセキュリティと船舶のセキュリティは「可用性」を最重要視するという点で類似しているため、実際に制御システムで使用されているOT-IDS(制御システム向けIDS)がどのように運用されているのかを知りたいと思いました。また、船にIDSを導入することで船舶会社にどのような利点が見込めるのか、導入する場合の技術的な課題は何かについて理解したいという思いもありました。 インターンシップ概要 今回私は、NTTドコモビジネス イノベーションセンターで、OTシステム向けIDS・セキュリティ可視化サービスである、OsecT(オーセクト)の開発に関する業務を行いました。 インターンシップで行った業務体験内容を以下にまとめます。 OTセキュリティとOsecTの概要把握 まず、OTセキュリティの概要を把握するために、イノベーションセンターが社会人向けに提供している教育プログラムを受講させていただきました。 講義では、Stuxnet、Mirai、WannaCryといったマルウェアが制御システムに影響を及ぼした事例に加え、GPSスプーフィングやWebカメラの不正利用など、制御環境にも波及する脅威が紹介されました。また、ランサムウェア被害を受けた企業の約53%が50万ドル以上の身代金を支払ったという 調査結果 を知ることになりました。私はこの数字を聞いて、サイバーリスク保険の存在によって経済的負担が減っているために、莫大な金額を請求されても企業が支払いに踏み切ってしまう現状があるのではないかと思いました。 さらに、ICSサイバーキルチェーンやMITRE ATT&CK for ICSといった、OTセキュリティ特有の分析フレームワークも紹介されました。これまで一般的なITセキュリティのフレームワークしか知らなかったため、制御システム向けに整理された独自の手法が存在することを初めて知り、新鮮な学びとなりました。 OTセキュリティの概要を学ぶ上で特に面白いなと感じたのは、 業界によってセキュリティ要件の枠組みが大きく異なる 点です。 船舶分野 UR E26/27 という国際的に統一された技術要件が存在し、グローバルに共通の基準に基づいて安全性やサイバーセキュリティ対策を整備する流れがある。これは国際航行を前提とするため、世界的に統一されたルールが不可欠となっている。 製造分野 国や地域、さらには分野ごとに異なる規格が並立しており、北米では CSA規格 、EUでは NIS2指令、 半導体分野では SEMI規格 などが例として挙げられる。市場や技術領域の多様性が大きいため、各地域、各分野で独自の要件が策定されている。 この違いは、業界ごとの国際性や供給網の特性を反映していると考えられます。船舶は国際航行を前提とするためグローバルに統一されたルールが不可欠である一方、製造業は市場や技術領域の多様性が大きいため、各地域で独自の要件が策定されやすいのだと理解しました。 単に「OTセキュリティ」と括るのではなく、こうした制度設計や標準化の背景を踏まえて学ぶことの重要性を改めて実感しました。 次に、チームが開発しているサービスであるOsecTについての説明を受けました。 (引用元: https://www.ntt.com/business/services/security/security-management/wideangle/osect.html ) OsecTは2つの基本機能を備えています。 ネットワークの可視化 ネットワークトラフィックを分析し、資産・通信・セキュリティリスク等をwebポータル画面で可視化できます。これにより、ネットワークマップやトラフィック量から重要な端末を視覚的に把握でき、対応の優先順位がつけやすくなります。 脅威・脆弱性の検知 通信状況を学習し、脅威や脆弱性を検知した際にアラートを通知します。検知した情報はwebポータルでも確認可能です。 また、 資産台帳と連携する機能 も持っているため、不審な状況を見つけたときに設置場所や担当者などの情報をもとに、素早く対応することが可能となっています。さらに、資産台帳に登録されていない、いわゆる未把握の機器も表示されるため、台帳の漏れを補完したり、不正に接続された端末の存在を調査したりすることも可能です。これにより、既存の資産管理の精度を高めると同時に、セキュリティリスクを早期に発見する仕組みとしても活用できます。 一方で、現行の仕組みには課題があることも示されました。例えば、IPアドレスを持たない機器については、対応しているOTプロトコル以外は可視化の対象外となるため、 すべての資産を完全に把握できるわけではありません 。 説明を聞いて、これまでIDSというと「脅威検知」をするものだという印象がありましたが、OT-IDSには「ネットワークの可視化」という重要な役割も担っているんだということを知り、とても勉強になりました。 テーマ選定 ここまでの講義・説明を受けて、私はある1つの仮説を立てました。 船舶のプロトコルを分析 すれば船内の資産を把握でき、 資産インベントリの作成・更新補助ができるのではないか。 ※資産インベントリとは、 IACS UR E26/27 で提出が義務付けられている、船舶機器の詳細をまとめた台帳です。 船舶のライフサイクルを通じて船舶資産インベントリは更新・維持されなければならないことを考慮して作成する必要があります。なお、 資産インベントリの管理を手動で行う場合、更新作業が煩雑になる可能性があります。 そのため、資産インベントリの更新を効率化し、精度を高めるために、 システム化が推奨されます。 (Class NK | IACS UR E26 p.31 4-1.2より) この点を踏まえ、各船舶機器が発する固有のシグナルを活用できれば、船舶資産の自動識別が可能となり、資産インベントリの作成・更新作業を大幅に補助できるのではないかと考えました。 しかし、OsecTは船舶特有のプロトコルに対して十分な解析機能を持っていません。解析可能とするためには新たに専用のパーサーを開発・実装する必要があります。 そこで、以下のプロセスで調査・検証を進めることにしました。 検討1: 船舶での使用プロトコル調査 調査の結果、船舶では主に2つのプロトコルが使われていることが分かりました。調査内容を以下にまとめます。 NMEA 0183 NMEAは「National Marine Electronics Association(米国海洋電子機器協会)」の略。 船舶機器間の通信のための仕様で、潮流計、音響測深機、ジャイロコンパス、GPSなどのデータを伝送するために使用される。シリアルポートを使用した片方向通信でASCII形式。 例(GPS): $GPGGA,052400.00,3539.3146239,N,13945.6411751,E,4,07,0.59,4.987,M,34.035,M,1.0,3403*76 (データ引用元: https://ales-corp.co.jp/technical-information-nmea/ ) Talker ID とよばれる、どの機器がそのデータを送信しているかという識別子がある。上記の例では、 $GPGGA の GP の部分がTalker IDとなる。 Talker ID対応表(一部) 引用元: pynmea2/NMEA0183.pdf at master · Knio/pynmea2 · GitHub | **ID** | **解説** | | --- | --- | | AG | 一般的なオートパイロット | | AP | 磁気コンパス連動のオートパイロット | | CD | DSC(デジタル選択呼出)通信装置 | | CR | 無線ビーコン受信機 | | CS | 衛星通信機 | | CT | MF/HF帯ラジオ電話通信機 | | CV | VHF帯ラジオ電話通信機 | | CX | スキャン機能付き無線受信機 | | DF | 方位探知機 | | EC | ECDIS | | EP | EPIRB | | ER | 機関室モニタリングシステム | | GP | GPS受信機 | | HC | 磁気コンパス | | HE | ジャイロコンパス(真北追従) | | HN | ジャイロコンパス(非真北) | | II | 統合計測機器 | | IN | 統合ナビゲーションシステム | | LC | ロランC | | P | メーカー独自(非標準) | | RA | レーダー/ARPA(自動衝突予防装置) | | SD | 音響測深機 | | SN | その他の電子測位システム | | SS | スキャニング音響測深機 | | TI | ターンレートインジケータ | | VD | ドップラー式速度センサ | | DM | 水中・磁気式速度ログ(船速計) | | VW | 水中・機械式ログ(パドルホイール等) | | WI | 気象センサ | | YX | トランスデューザー | | ZA | 原子時計 | | ZC | クロノメーター | | ZQ | 水晶時計 | | ZV | 電波時計 | IEC61162-450 IECは「International Electrotechnical Commission(国際電気標準会議)」の略で、後ろに標準化された規格の番号を付与して文書や通信規格そのものの名として利用される。 UDPマルチキャストを使って、NMEAセンテンスをEthernetパケットで送信するための規格。 例: UdPbC\0\s:GP0001\$GPGGA,052400.00,3539.3146239,N,13945.6411751,E,4,07,0.59,4.987,M,34.035,M,1.0,3403*76\r\n タグブロック部と呼ばれる、NMEAセンテンスの直前に配置される行の中には sブロック とよばれる送信元識別子がある。 調査の結果、NMEA 0183には Talker ID 、IEC61162-450には sブロック と、どちらのプロトコルにも資産特定に使えそうな識別子がありました。 検討2: 現状の船内ネットワーク調査 次に、現状の船内ネットワークがどのような構成なのか調査しました。 出典: JRC | 船内LANシステム あくまで私の予想ですが、GPSや潮流計などのシリアル出力データ(NMEA 0183)を入出力IF装置がIEC61162-450に変換しているのだと思われます。 検討3: 船舶pcapに対する現状のIDS製品の出力検証 次に、OsecT以外でのOT-IDSについても船舶プロトコルの対応状況を調査するために、以下の環境を想定したpcapファイルを用意し、2つのOT-IDS製品を使ってトラフィック解析しました。 結果として、今回の検証で使用したOT-IDSでは船舶で使われているプロトコルを解析できませんでした。このため、OsecTに適用可能な船舶プロトコルパーサーを新たに作成することにしました。 今回は、IEC61162-450の仕様書が手元にない+インターネットにもあまり情報がないため、NMEA 0183のTalker IDを足掛かりにしてパーサーの作成を進めました。 Talker IDから資産を出力するZeek・Spicyパーサー作成・検証 OsecTのパーサーはZeek・Spicyで構成されています。 Zeek・Spicyとは Zeek トラフィックを解析して、IPアドレス、MACアドレスやプロトコルなどの情報をログとして出力するOSS。 Spicy Zeekで利用するC++のパーサーをC++で記述することなく簡易に生成するためのツール。 参照: 【日本初紹介】Zeek・Spicyの使い方まとめ - NTT docomo Business Engineers' Blog 今回は、ZeekとSpicyを使って船内ネットワークトラフィックを解析するためのパーサーを作成しました。 パーサー検証 作成したパーサーを使って、実際に船内ネットワークトラフィック(pcapファイル)を解析しました。 今回の検証では、2種類のNMEA 0183メッセージからTalker IDとSentence Typeの2つのフィールドを抽出し、Talker IDからセンサの特定ができているかを確認しました。以下は、ZeekとSpicyを用いて取得したNMEAメッセージの解析結果(logファイル抜粋)です。 GP の場合:GPS HE の場合:Heading (ジャイロコンパス) 上記の通り、Talker IDから発信元のセンサの特定(船舶資産の特定)が行えていることが分かります。 まとめ 今回は、既存のOT-IDSが船舶特有のプロトコルを解析できないという技術課題に対し、NMEA文を解析可能にする独自のパーサーを作成しました。 このパーサーではNMEA 0183内に含まれるTalker IDを足掛かりにして船舶機器を特定できるように設計しており、その情報をOsecTの可視化機能に組み込むことで、船舶特有の機器の可視化の強化や資産インベントリの補助につながる可能性があるというポジティブな成果を得ることができました。 イベントへの参加 IDSに関する業務以外にも複数の社内イベントや見学会に参加させていただきました。 SOC見学 NTTセキュリティのSOC(Security Operation Center)を見学しました。 当初はSOCに対して「脅威を検知したら即座にアラートを出す場所」というイメージを持っていました。しかし、実際にNTTセキュリティが提供するSOCサービスは一定期間トラフィックを収集・分析し、詳細なレポートとしてクライアントに提供するサービス形態であることを知りました。SOCには多様な形があるのだということを知ることができ、とても勉強になりました。 ドコモとのコラボ企画 イノベーションセンターで攻撃インフラの解明や撲滅に向けて活動しているPJがセキュリティ国際会議「Botconf」で発表した内容を、ドコモ本社のインターン生とともに聴講させていただきました。 発表内容は新ロシア派ハクティビストに関する詳細なレポートで、実際の脅威情報分析の手法を学ぶ貴重な機会となりました。また、ハクティビストの情報を安易に拡散することがかえって悪影響を生む可能性があると知り、情報との向き合い方を考え直すきっかけにもなりました。 イノベーションセンター内での業務共有 イノベーションセンター内で活動する5名のインターン生が、それぞれの担当業務や取り組み内容を共有しました。 普段は近くで作業していても、実際に何をしているのか詳しく知る機会は少なかったので、とても新鮮でした。みんな全然違うテーマやプロジェクトに取り組んでいて、「そんなことやってるんだ!」と驚くことも多く、チームの多様性やそれぞれの専門性の広さを実感できて面白かったです。 TechLunch NTTドコモビジネスで不定期開催されているランチ勉強会「TechLunch」に参加しました。 「勉強会」という言葉から堅い雰囲気を想像していましたが、実際は社員の方がそれぞれの興味関心に基づいて自由に発表する形式で、活発かつフランクな学びの場でした。 当日は社員1名とインターン生2名の発表があり、発表中はSlack上でリアルタイムに反応が飛び交い、オープンな雰囲気を感じました。 インターンシップを通して学んだこと インターンに参加する前は、GPSスプーフィングやAISスプーフィングといった船舶に多く見られる攻撃を検知するIDSを作成することを考えていました。しかし、OT-IDSの説明をしていただく中で「資産インベントリ補助」という新たな活用方向を見出すことができ、とても有意義な経験となりました。 また、製造業と船舶業界ではセキュリティ要件の枠組みが大きく異なることも学びました。将来的には製造業のセキュリティ分野に関わりたいと考えているため、今後は制度やプロトコルの違いを意識しながら、より幅広い視点で学びを深めていきたいと思います。 さらに、インターンを通じてセキュリティに関わる仕事の現実的な難しさと奥深さを実感しました。重大なインシデントが起こらないことは、セキュリティに携わる者として理想的な状態です。しかし一方で、何か大きな事件が起こらなければ業界全体の意識や整備が進みにくいという現状もあり、その間にある葛藤のようなものを感じました。 おわりに 今回のインターンはこれまで開発に携わった経験がなかった自分にとって、大きな一歩を踏み出す機会となりました。以前から「何かを作ってみたい」と思いながらも、具体的なアイデアが浮かばなかったり、時間を理由に後回ししてしまったりしていました。そんな中で、実際に手を動かして開発へ取り組めたことは、自分にとって非常に貴重な経験でした。 また、さまざまなイベントにも参加させていただき、自分の所属チームだけでなく他のチームやインターン生、さらにはドコモグループの社員の方々とも関わることができました。部署を越えて多くの方と交流する中で、職場の雰囲気や、人とのつながりの大切さを改めて実感しました。 最後になりましたが、このインターンに関わってくださった皆さまに心より感謝申し上げます。受け入れてくださったチームの皆さま、特にこのインターンで参加したIT/OT統合セキュリティPJおよびOsecT Tech PJの田中さん、前田さん、加島さんには2週間にわたり大変お世話になりました。温かくご指導くださり、本当にありがとうございました。
アバター
こんにちは、イノベーションセンターの田口、金井です。 普段はOffensive Security PJのメンバーとして活動しています。 この記事では2025年8月に開催されたSOUPS 2025でポスター発表したことおよび同時期に開催された USENIX Security 2025へ参加したことについて紹介します。 Offensive Security PJについて USENIX Securityについて SOUPSについて ポスター発表について 発表概要 発表の様子 カンファレンスの様子 セキュリティ分野の研究動向 会場の雰囲気 おわりに Offensive Security PJについて Offensive Security PJ では、攻撃者視点のセキュリティ(Offensive Security)を専門とするチームとして、 攻撃技術の調査・開発・検証に取り組んでいます。 攻撃者に先んじて新たな攻撃技術を検証することで、将来の脅威を見越した防御の強化につなげています。 主な業務内容としてWideAngleのプロフェッショナルサービスにおける攻撃技術の検証支援や、 最先端の攻撃技術に関する応用的な研究開発を行っており、 成果のカンファレンス発表など対外的な活動にも積極的に取り組んでいます。 USENIX Securityについて USENIX Securityはサイバーセキュリティ分野の世界的なトップカンファレンスの1つです。 厳しい査読を通過した完成度の高い研究のみが発表される、まさに最先端のセキュリティ研究に触れることができるカンファレンスです。 34回目となる今年は米国シアトルで8月13日から15日までの3日間開催されました。 今年は論文発表が439件、ポスター発表が60件あり参加者は約900名でした。 SOUPSについて SOUPS(Symposium on Usable Privacy and Security)はユーザブルセキュリティ&プライバシー分野における最難関の国際会議です。 ユーザブルセキュリティ&プライバシーとは、コンピュータやシステムを扱う人間に焦点を当て、 人間が扱いやすいセキュリティを主題とする研究分野です。 USENIX Securityとの併催会議として毎年同時期に開催されます。 21回目となる今年も例年通りUSENIX Securityの併催会議として8月10日から12日までの3日間開催されました。 今年は論文発表が30件、ポスター発表が49件あり、参加者は186名でした。 ポスター発表について 発表概要 SOUPS 2025で発表した研究概要について紹介します。 Offensive Security PJでは「Offensive Security × HCI(Human-Computer Interaction)」 というテーマを掲げて研究開発業務をしています。 SOUPS 2025では“ Understanding the Challenges in Red Team Exercises from Multiple Stakeholder Perspectives ”というタイトルで発表してきました。 今回の研究で私達は、レッドチーム演習に従事する専門家へインタビューし、 専門家が抱える課題認識を分析することで、 実施するにあたりどのような障壁が存在するのかを明らかにしました。 この研究では、レッドチーム演習における疑似攻撃者を担う専門家と、 演習全体の統括を担う専門家という異なる役割を持つ2つの専門家に対してインタビューしました。 複数の役割の視点から課題を調査する取り組みは既存研究としては無く、 この研究のキーコンセプトと言えます。 現在はこの研究のネクストアクションとして、明らかにした課題を解決するための研究開発に取り組んでいます。 レッドチーム演習とは、評価対象へ疑似的攻撃をすることで 対象組織やシステムのセキュリティを評価するセキュリティテスト手法の1つです。 発表者の田口 発表の様子 SOUPS 2025のポスター発表は8月11日のテクニカルセッション終了後のレセプションパーティー会場で行われ、 参加者はドリンクと軽食を持って会場内のポスターを巡り自由に質問や議論をしていました。 SOUPSではフレンドリーな雰囲気で話をしてくれる参加者が多く、 英語が堪能でない私でも有意義な質疑応答や議論ができたと感じています。 質疑応答では「インタビューで参加者はどのような課題感を述べた?」、 「今後インタビュー調査を拡大する予定はある?」といった質問をいただきました。 レッドチーム演習の実務者にインタビューで直接課題を聞き出したという点が特に興味を引いていた印象を覚えました。 研究自体に対するポジティブな反応も多くもらうことができネクストアクションのモチベーションにもつながる良い機会だったと思います。 カンファレンスの様子 カンファレンスの情報や現地の様子について紹介します。 セキュリティ分野の研究動向 オープニングセッションにて、USENIX Security 2025に投稿・採択された論文の研究トピックの割合についてUSENIX運営から説明されました。 昨年から引き続き論文投稿数と採択数共に1位だった研究トピックはML & AIセキュリティに関する論文でした。 Human Factor(人に着目した分野)に関する研究トピックも年々増えてきている傾向にあります。 また、今年採択された論文の研究トピック割合としては以下のように示されていました。 ML & AI: 23% System, software, crypto, network: 65% Human factors: 12% 会場の雰囲気 全日程Seattle Convention Center Archで行われました。 オープニング前のメインホールです。大人数の参加者が十分着席できるほどの広さでした。 運営組織であるUSENIX Associationが今年で設立から50周年を迎え、 大きなケーキを用意した記念パーティーが開催されました。 おわりに SOUPS、USENIX Securityに参加することで最新のセキュリティ研究を知ったり、 現地で国内外のセキュリティ研究者たちと交流したりと得られるものがとても多かったと感じています。 また、ポスター発表で参加者と英語で積極的に議論した経験は、私に自信とモチベーションを与えてくれる貴重な経験だったと思います。 私達の発表を含め採択されたポスターや論文は SOUPS 2025 、 USENIX Security 2025 のホームページにて公開されているのでぜひ読んでみてください。
アバター
本記事では、現在進行中で取り組んでいるテーマ「生成AI×数理最適化」に関する試みとして、生成AIを活用して数理最適化技術の実務適用を支援するアプローチを紹介します。例として、スーパーマーケットにおける在庫管理の効率化を取り上げ、その具体的な応用と効果について述べます。 はじめに 背景 数理最適化モデルの定式化と実装に伴う困難 生成AIの台頭 実現アプローチの検討 生成AI活用の全体像 在庫最適化の課題設定 実現までのステップ 1. 定式化支援エージェントによる定式化支援 2. 入力データ設計支援エージェントによるデータ設計支援 3. Node-AIを活用したデータの準備 4. コード生成エージェントによる実行コード生成 5. 作成されたコードの実行と結果 まとめ おわりに はじめに こんにちは、イノベーションセンター テクノロジー部門 先端AI数理PJの伊藤です。 普段は Node-AI や AI Autopilot System などのプロダクト価値向上に加え、お客さまから寄せられる課題に対してデータ分析を通じた解決策の研究開発に取り組んでいます(例えば 学習速度と変数選択精度を極めたFastGSCADモデル (Ver. 3.22.0)|Node-AI by NTTドコモビジネス など)。 昨年度は 機械学習×数理最適化で業務プロセス革命! - NTT docomo Business Engineers' Blog という記事を執筆し、ありがたいことに多くの反響をいただきました。現在も、予測結果を活用した数理最適化技術の研究開発を継続しており、最近では、数理最適化による課題解決を支援する生成AIの活用の研究にも注力しています。 本記事では、スーパーマーケットにおける在庫管理を一例に、生成AIによる支援を活用した数理最適化技術による課題解決のアプローチをご紹介します。 背景 数理最適化モデルの定式化と実装に伴う困難 数理最適化では、現実に存在する課題を以下の形式に抽象化し、数学的モデルとして表現することが求められる場面は多くあります。 具体的には、 個の不等式制約と 個の等式制約を満たしながら、問題の解を決めるための変数 を調整し、目的関数 を最大化する解を求めることを指します。 例えば、 こちらの記事 で取り上げているコールセンターにおけるオペレーター最適配置の課題では、課題の整理から出発し、以下のような数理最適化モデルへと定式化を行っています(詳しくは 該当記事 をご参照ください)。 このような数式で記述された最適化問題は、プログラムとして実装し、最適解を導出することで、業務上の意思決定に応用されます。 しかし、このような定式化やプログラムの実装には、数理的な知識に加えて、プログラミングスキルといった高度な専門性が求められます。そのため、現場で課題を抱える方々が自ら数理最適化モデルを構築・運用するには技術的なハードルが高く、専門家の支援が不可欠となるケースも少なくありません。 生成AIの台頭 近年、読者の皆さまもご存じのとおり、ChatGPT や Gemini に代表されるアプリケーションの登場により、生成AIは大きな注目を集めています。生成AIの活用によって、日々の生活や業務の在り方が大きく変わったと感じている方も多いのではないでしょうか。 NTTドコモビジネスにおいても、生成AIに関する取り組みが積極的に進められており、次のような公開情報からもその一端をご覧いただけます( 生成AI(Generative AI)|NTTドコモビジネス 法人のお客さま )。また、 Node-AI も、ユーザーによって自由度の高い可視化や前処理を支援するために、生成AIを活用したサポート機能が提供されています。詳しくは以下のリソースをご参照ください。 データ可視化 - AI可視化 データ前処理 - AIアシスト付きカスタム前処理 このような流れを受け、私たちは現在、数理最適化問題の定式化やプログラム実装のプロセスを、生成AIによって支援・自動化する研究に取り組んでいます。 具体的には、業務課題を自然言語で入力するだけで、生成AIが最適化問題としての構造を理解し、目的関数や制約条件を抽出・提案する仕組みの構築を目指しています。また、その数理モデルをもとに、ソルバーと連携可能なコードを自動生成することにも取り組んでおり、これにより、これまで専門家の手を要していたプロセスの大幅な効率化が期待されます。 このアプローチにより、数理最適化の専門知識を持たないユーザーでも、業務課題に対して数理最適化技術を活用しやすくなり、より多くの現場で高度な意思決定支援が実現できると考えています。 実現アプローチの検討 本章では、スーパーマーケットの発注量最適化をテーマに、生成AIを支援技術として取り入れた数理最適化の活用について検討していきます。 生成AI活用の全体像 最初に、生成AI活用の全体像を示します。本アプローチでは、以下の3種類のAIエージェントを活用します。 定式化支援エージェント : ユーザーが抱えている課題や要件をプロンプトとして入力するだけで、AIが数理最適化モデルを自動で定式化します。 入力データ設計支援エージェント : 定式化された数理モデルに基づき、数理最適化実行に必要なデータ構造の設計案を具体的な例とともに提案します。 コード生成エージェント : 定式化されたモデルと設計された入力データをもとに、数理最適化を実行するためのプログラムコードを自動生成します。 これら3種類のAIエージェントが連携してユーザーをサポートすることで、これまで専門知識が不可欠であった数理最適化技術の導入プロセスを大幅に簡素化し、現場への迅速な実装を可能にします。 次のフローは、上記のAIエージェントを補助的に活用し、業務課題を解決するための数理最適化モデルの定式化から実行コードの生成、さらに業務への実装に至るまでのプロセスを体系的に整理したものとなっています。 ① 課題の適性診断(担当:定式化支援エージェント) ユーザーが解決したい課題の概要を定式化エージェントに入力。その課題が数理最適化技術での解決に適しているかの判定。 解決可能と判定された場合: ステップ2への移行。 解決困難と判定された場合: 別のアプローチ(機械学習、シミュレーションなど)の検討。 ② 数理モデルの定式化(担当:定式化支援エージェント) ユーザーによる課題詳細(制約条件、目的関数など)のエージェントへ入力。これを数理最適化問題として定式化し、その内容をMarkdownファイルとして出力。 ③ 入力データの設計(担当:入力データ設計支援エージェント) ステップ2で作成された数理モデルのMarkdownファイルを、入力データ設計支援エージェントへインプット。モデルに必要なデータ項目の自動抽出および機械学習による予測の必要性の有無の判断、それらのデータ設計方法の具体例を伴う提示。 ④ 入力データの準備(担当:ユーザー) ステップ3の設計案に基づく、実際の業務データなど必要な入力データの準備。 (分岐): ステップ3で機械学習による予測が必要と判断されたデータ項目については、Node-AIなどのツールを用いた予測モデルの構築と、それによる予測値の生成。 ⑤ 実行コードの生成(担当:コード生成エージェント) ステップ2の数理モデルのMarkdownファイルと、ステップ4の入力データをコード生成エージェントへインプット。特定の最適化ライブラリを使用した実行可能なPythonコードの生成。 ⑥ 業務への実装と活用(担当:ユーザー <+コード生成エージェント>) 生成されたPythonコードの、ユーザーによる実際の業務プロセスやシステムへの組み込み。コードの微調整や運用方法の最適化など、コード生成エージェントのサポートを適宜受けながらの業務利活用の推進。 なお、各種AIエージェントの支援を活用し、上記プロセスを通じて業務課題への数理最適化技術の適用を容易にするアプリケーションを開発しています。 本稿では、その開発中の画面の一部とともにご紹介します。 在庫最適化の課題設定 次に、本検討で取り上げる課題設定を示します。なお、課題はシンプルに設定しています。 ○ 目的 スーパーマーケットにおける日配品の在庫管理を対象に、適正在庫を維持しながら、廃棄ロスを最小限に抑えることを目指す。欠品はある程度許容しつつ、過剰在庫によるロスの削減を重視する。 ○ 前提条件 - 対象カテゴリ:日配品 - 対象商品数:50SKU (Stock Keeping Unit: 在庫管理単位) - 発注頻度:毎日 - リードタイム:1日 - オペレーション: - 午前中に発注指示 - 閉店後、消費期限切れ商品を廃棄 - 翌朝、発注商品が納品 - 納品体制:地域の卸業者による一括納品 イメージ図(AI生成) 以降では、こちらの課題を例に、AIエージェントを活用した具体的なフローを説明します。 実現までのステップ 1. 定式化支援エージェントによる定式化支援 まず、数理最適化に不慣れなユーザーにとっては、前述した在庫管理の課題が数理最適化によって解決可能かどうかを判断するのは容易ではありません。そこで、生成AIを活用し、当該課題が数理最適化の適用対象となり得るかを診断するアプローチを試みます。 以下は、当該課題に数理最適化技術を適用できるかどうかを判断するために、定式化支援エージェントに与えるプロンプトの一例です。現在開発中のアプリケーションでは、ユーザーが【課題概要】の欄に自身の課題を簡潔に入力すると、これをもとにエージェントが応答を生成する仕組みになっています。 エージェントによる回答結果はこちらです。 定式化支援エージェントの解析結果として、「 数理最適化によって解決可能である 」との判断が得られました。これを受け、次のステップでは、具体的な数理最適化問題の定式化に進みます。 下記は、定式化支援エージェントに数理最適化問題の定式化を依頼する際のプロンプト例です。開発中のアプリケーションでは、ユーザーが【課題内容】欄に具体的な課題を入力すると、それを踏まえてエージェントが最適な定式化を自動生成します。 定式化支援エージェントからは、次のような回答が得られました。 定式化支援エージェントを用いることで、数理最適化問題の定式化がわずか1分足らずで完了しました。通常であれば、人手による定式化には相応の時間と労力を要しますが、エージェントは非常に短時間で処理を行い、大幅な時間短縮を実現しています。 作成された定式化には、プロンプトで与えた「目的」や「制約」、「補足情報」が的確に反映されており、日本語による説明を通じて、その妥当性が確認できます。 特に注目すべきは、欠品量・廃棄量・在庫の遷移といった要素について、プロンプト内で明示的に指示していないにもかかわらず、適切に定式化へ組み込まれていた点です。 さらに、消費期限順に商品が販売・消費されるという数式で表現するには難易度の高い制約についても、エージェントは短時間で的確に反映しており、その高い汎用性と柔軟性がうかがえます。 本エージェントにより定式化された数理最適化モデルが妥当と判断された場合、生成AIを用いてMarkdown形式で内容の整理・保存を行います。これを次のステップの入力データ設計支援エージェントに引き渡していきます。 2. 入力データ設計支援エージェントによるデータ設計支援 前節では、定式化支援エージェントのサポートを受けながら、数理最適化問題の定式化を完了しました。次に、最適化を実行するために必要な入力データの設計を行います。 不慣れなユーザーにとっては、「どのようなデータを、どのような構成で用意すればよいのか」が分かりにくい場合があります。そこで本アプリケーションでは、入力データ設計支援エージェントを活用して、入力データの設計案を自動的に提示する仕組みを取り入れています。 数理最適化モデルが記述されたMarkdownファイルをアップロードした上で、入力データ設計支援エージェントに次のようなプロンプトを与えます。なお、開発中のアプリケーションにおいては、自動でこのプロンプトが与えられます。 エージェントによる回答結果はこちらです。 まず、Step 1ではデータ項目の整理を行っています。具体的には、「データ項目」「説明」「数式表現」「単位例」「カテゴリ」「機械学習(回帰)を要する可能性」の6項目が一覧化されています。 このステップで特に注目すべきなのは、「機械学習(回帰)を要する可能性」の判定結果です。機械学習に詳しくないユーザーにとって、あるデータを準備するのに機械学習技術が必要かどうかを判断するのは容易ではありません。この判定結果は、その判断をサポートする重要な情報となります。 例えば、「需要量」については機械学習技術の活用が必要と判断されています。実際、将来の需要量を見積もるには予測が伴うため、機械学習手法を用いる必要があります。 Step 2では、CSVファイル形式による入力データの設計案が提示されており、各ファイルの名称や具体的なデータ例も示されています。今回の例では、以下の4つのCSVファイルに分割して設計することが提案されています。 products.csv (商品ごとの基本情報を示すデータ) ※「欠品コスト(円/個)」および「廃棄コスト(円/個)」の値は、ユーザーの業務内容やポリシーに応じて個別に設定する必要があります。例えば、簡易的な想定として、欠品コストは粗利、廃棄コストは仕入れ価格を基準とすることが可能です。 demand.csv (各商品における将来の需要数(予測値)を示すデータ) initial_inventory.csv (各商品の初期在庫量を記録したデータ) scalar_constraints.csv (計画期間といった全体に適用される設定値) この設計案を参考に、次節では実際のデータ準備を進めていきます。 3. Node-AIを活用したデータの準備 前節のエージェントによる出力結果を参考にしながら、必要な入力データを実際に準備していきます。なお、データの取得にあたっては、場合によっては既存の業務プロセスに対する見直しや調整が必要となる可能性もあります。 まず、「機械学習(回帰)を要する可能性」が「あり」と判定された「商品の需要量」に着目します。この項目は機械学習による予測が必要であるため、NTTドコモビジネスのプロダクトである Node-AI を活用します。 Node-AI のキャンバスは以下の画像の通りです。操作手順については、 こちらの記事 で紹介している内容と基本的には同じであるため、本稿では詳細な説明を省略します。 この予測結果に加え、Step 1で提示された「説明」やStep 2の入力データ設計案を踏まえて、実際の入力データの整備を進めます。今回は、以下のような形式でデータを準備しました。 なお、Step 2で提示されている入力データ設計案はあくまで一例であり、その形式に厳密に従う必要はありません。ユーザーの業務環境や保有するデータの構造に応じて、列名やデータ型などを柔軟に調整して問題ありません。 例えば今回の例だと、 initial_inventory.csv における列名「消費期限クラス」を「残消費期限(日)」に変更しており、 demand.csv の日付形式も「yyyy-mm-dd」から「yyyy/mm/dd」へと変更しています。 こうして必要に応じた調整を行いながら、数理最適化の実行に必要なデータを整備しました。 4. コード生成エージェントによる実行コード生成 データの準備が完了したので、最後にコード生成エージェントを活用して数理最適化を実行するための Python コードを作成します。なお、コードの実行は以下の環境で検証しています。 version Python: 3.13.1 PySCIPOpt: 5.2.1 SCIPソルバー: 9.2.0 numpy: 2.2.2 pandas: 2.2.3 準備した入力データのCSVファイルと、数理最適化モデルが記述されたMarkdownファイルをアップロードし、その上でコード生成エージェントに次のプロンプトを与えます。なお、開発中のアプリケーションではこちらのプロンプトに関してもあらかじめ設定されていて、自動で操作が行われます。 エージェントによって出力された結果は以下の通りです。 import pandas as pd from pyscipopt import Model, quicksum class Solver : def __init__ (self, scalar_constraints_path: str , demand_path: str , inventory_path: str , product_path: str ): """ Solverクラスの初期化 :param scalar_constraints_path: スカラーの制約定数が入ったCSVファイルのパス :param demand_path: 需要データCSVファイルのパス :param inventory_path: 初期在庫CSVファイルのパス :param product_path: 商品パラメータCSVファイルのパス """ # CSVファイルのパスを保持 self.scalar_constraints_path = scalar_constraints_path self.demand_path = demand_path self.inventory_path = inventory_path self.product_path = product_path # SCIPモデルを初期化(perishable_inventoryという名前をつける) self.model = Model( "perishable_inventory" ) def load_data (self): """ CSVファイルから各種データを読み込み、 モデルで使うパラメータを準備する """ # スカラーの制約定数が入ったデータを読み込み(shift_jisエンコーディング) self.scalar_constraints_df = pd.read_csv(self.scalar_constraints_path, encoding= "shift_jis" ) # 「パラメータ」列をインデックスに設定 self.scalar_constraints_df.set_index( "パラメータ" , inplace= True ) # 需要データの読み込み self.demand_df = pd.read_csv(self.demand_path, encoding= "shift_jis" ) # 初期在庫データの読み込み self.inventory_df = pd.read_csv(self.inventory_path, encoding= "shift_jis" ) # 商品パラメータの読み込み(消費期限、発注上限、コストなど) self.product_df = pd.read_csv(self.product_path, encoding= "shift_jis" ) # 計画期間 T(日数)を取得 self.T = int (self.scalar_constraints_df.loc[ "計画期間(日)" , "値" ]) # 商品IDの一覧(重複なし)を取得 self.products = list (self.product_df[ "商品ID" ].unique()) # 商品ごとのパラメータ辞書を作成 self.S = self.product_df.set_index( "商品ID" )[ "最大消費期限(日)" ].to_dict() # 最大消費期限 self.U = self.product_df.set_index( "商品ID" )[ "発注上限(個)" ].to_dict() # 発注上限数量 self.c_w = self.product_df.set_index( "商品ID" )[ "廃棄コスト(円/個)" ].to_dict() # 廃棄コスト self.c_s = self.product_df.set_index( "商品ID" )[ "欠品コスト(円/個)" ].to_dict() # 欠品コスト # 需要データの日付を1..Tのインデックスに変換するための辞書を作成 unique_dates = sorted (self.demand_df[ "date" ].unique()) self.date_to_t = {d: idx + 1 for idx, d in enumerate (unique_dates)} # 需要量の辞書を作成(キー:(商品ID, 時間t), 値: 需要量) self.d = {} for _, row in self.demand_df.iterrows(): i = row[ "商品ID" ] t = self.date_to_t[row[ "date" ]] self.d[(i, t)] = int (row[ "需要量(個)" ]) # 初期在庫量の辞書を作成(キー:(商品ID, 残消費期限), 値: 初期在庫量) self.I0 = {} for _, row in self.inventory_df.iterrows(): i = row[ "商品ID" ] s = int (row[ "残消費期限(日)" ]) self.I0[(i, s)] = int (row[ "初期在庫量(個)" ]) def build_model (self): """ 数理モデルの構築 変数定義、目的関数設定、制約条件設定を行う """ # まずはデータを読み込む self.load_data() # 変数を格納する辞書を初期化 self.x = {} # 発注量 x_{i,t} self.I = {} # 在庫量 I_{i,t}^s self.f = {} # 販売量 f_{i,t}^s self.b = {} # 欠品量 b_{i,t} self.w = {} # 廃棄量 w_{i,t} # 変数の定義 for i in self.products: Si = self.S[i] # 商品iの消費期限 Ui = self.U[i] # 商品iの発注上限 for t in range ( 1 , self.T+ 1 ): # 発注量変数(0以上、上限Ui、整数) self.x[(i, t)] = self.model.addVar( vtype= "INTEGER" , lb= 0 , ub=Ui, name=f "x_{i}_{t}" ) # 欠品量変数(0以上、整数) self.b[(i, t)] = self.model.addVar( vtype= "INTEGER" , lb= 0 , name=f "b_{i}_{t}" ) # 廃棄量変数(0以上、整数) self.w[(i, t)] = self.model.addVar( vtype= "INTEGER" , lb= 0 , name=f "w_{i}_{t}" ) # 在庫量と販売量は消費期限ごとに定義 for s in range ( 1 , Si+ 1 ): self.I[(i, t, s)] = self.model.addVar( vtype= "INTEGER" , lb= 0 , name=f "I_{i}_{t}_{s}" ) self.f[(i, t, s)] = self.model.addVar( vtype= "INTEGER" , lb= 0 , name=f "f_{i}_{t}_{s}" ) # 目的関数:廃棄コスト + 欠品コスト の最小化 self.model.setObjective( quicksum( self.c_w[i] * self.w[(i, t)] + self.c_s[i] * self.b[(i, t)] for i in self.products for t in range ( 1 , self.T+ 1 ) ), "minimize" ) # 制約1: 初期在庫条件 I_{i,1}^s = 初期在庫量 for i in self.products: Si = self.S[i] for s in range ( 1 , Si+ 1 ): init_val = self.I0.get((i, s), 0 ) # 初期在庫がなければ0とする self.model.addCons( self.I[(i, 1 , s)] == init_val, name=f "initInv_{i}_{s}" ) # 制約2: 在庫遷移の制約(消費期限の経過を反映) for i in self.products: Si = self.S[i] for t in range ( 2 , self.T+ 1 ): for s in range ( 1 , Si+ 1 ): if s == Si: # 新規発注分は残消費期限Siの在庫になる self.model.addCons( self.I[(i, t, s)] == self.x[(i, t- 1 )], name=f "invTrans_new_{i}_{t}_{s}" ) else : # 古い在庫は1日経つと残消費期限が1減る(販売後) self.model.addCons( self.I[(i, t, s)] == self.I[(i, t- 1 , s+ 1 )] - self.f[(i, t- 1 , s+ 1 )], name=f "invTrans_{i}_{t}_{s}" ) # 制約4-1: 需要充当(販売+欠品=需要) for i in self.products: for t in range ( 1 , self.T+ 1 ): demand = self.d.get((i, t), 0 ) # 需要がない場合は0 Si = self.S[i] self.model.addCons( quicksum(self.f[(i, t, s)] for s in range ( 1 , Si+ 1 )) + self.b[(i, t)] == demand, name=f "demandFulfill_{i}_{t}" ) # 制約4-2: 販売量は在庫以下であること for i in self.products: for t in range ( 1 , self.T+ 1 ): Si = self.S[i] for s in range ( 1 , Si+ 1 ): self.model.addCons( self.f[(i, t, s)] <= self.I[(i, t, s)], name=f "sellCap_{i}_{t}_{s}" ) # 制約4-3: FIFO販売順序の保証 for i in self.products: for t in range ( 1 , self.T+ 1 ): demand = self.d.get((i, t), 0 ) Si = self.S[i] for s in range ( 1 , Si+ 1 ): if s > 1 : # sより短い消費期限の販売が先に行われている必要がある self.model.addCons( self.f[(i, t, s)] <= demand - quicksum(self.f[(i, t, k)] for k in range ( 1 , s)), name=f "fifo_{i}_{t}_{s}" ) # 制約5: 廃棄量は残消費期限1の在庫から販売量を引いたもの for i in self.products: for t in range ( 1 , self.T+ 1 ): self.model.addCons( self.w[(i, t)] == self.I[(i, t, 1 )] - self.f[(i, t, 1 )], name=f "wasteDef_{i}_{t}" ) def solve (self): """ モデルを解く """ # ログを非表示に設定 self.model.setParam( 'display/verblevel' , 0 ) # 最適化を実行 self.model.optimize() def check_model_status (self): """ 解の状態を確認し、最適解が見つかったかどうかを返す """ status = self.model.getStatus() if status == "optimal" : print ( "✅ 最適解が見つかりました。" ) return True else : print (f "エラー:{status}" ) return False def get_solution (self): """ 最適解がある場合、各変数の値を辞書で取得する """ # 最適解があるかチェック if not self.check_model_status(): return None # 最適解を取得 best_sol = self.model.getBestSol() if best_sol is None : print ( "❌ 実行可能解は取得できませんでした。" ) return None # 変数名と値を辞書に格納 result = {} for var in self.model.getVars(): val = self.model.getSolVal(best_sol, var) result[var.name] = val return result コード生成エージェントを活用することで、このように長大なコードであっても、わずか1分足らずでミスなく作成できました。人手によるコーディングでは多くの手間や時間がかかりますが、エージェントによりそれらを大幅に削減し効率的な開発を可能にしています。 生成されたコードは、先ほど用意した4つのデータを読み込み、それをもとに最適な発注量や在庫の動きを計算する仕組みです。消費期限や在庫の状況、日ごとの需要などを考慮しながら、「どの商品を、いつ・どれだけ発注し、どのくらい販売し、欠品や余剰がどの程度発生するか」を自動的に判断します。 このようにして、在庫の無駄や欠品をできるだけ抑えた、バランスの取れた計画が立てられるようになります。 この Solver クラスを利用して、実務で使うために各商品の当日発注量をCSVファイルとして出力するコードを、エージェントに作成してもらいます。 エージェントに渡すプロンプトは以下の内容です。 エージェントによって作成されたコードはこちらです。 import argparse import pandas as pd from optimizer import Solver def export_first_day_orders ( scalar_constraints_path: str , demand_path: str , inventory_path: str , product_path: str , output_csv_path: str ): """ Solver を使って最適化を実行し、 初日の各商品の発注量を CSV 出力する。 :param scalar_constraints_path: scalar_constraints.csv のパス :param demand_path: demand.csv のパス :param inventory_path: inventory.csv のパス :param product_path: product.csv のパス :param output_csv_path: 出力 CSV ファイルのパス """ # モデル構築・最適化 solver = Solver( scalar_constraints_path=scalar_constraints_path, demand_path=demand_path, inventory_path=inventory_path, product_path=product_path ) solver.build_model() solver.solve() # 最適解取得 sol = solver.get_solution() if sol is None : print ( "最適解が取得できなかったため、CSV を出力しません。" ) return # 初日の発注量をリスト化 records = [] for product_id in solver.products: var_name = f "x_{product_id}_1" qty = sol.get(var_name, 0 ) records.append({ "商品名" : product_id, "発注量" : int (qty) }) # DataFrame 化して CSV 出力(Shift_JIS) df_out = pd.DataFrame(records, columns=[ "商品名" , "発注量" ]) df_out.to_csv(output_csv_path, index= False , encoding= "shift_jis" ) print (f "初日の発注量を '{output_csv_path}' に出力しました。" ) if __name__ == "__main__" : parser = argparse.ArgumentParser( description= "初日の各商品の発注量を最適化し、CSV 出力します。" ) parser.add_argument( "--scalar_constraints" , required= True , help = "scalar_constraints.csv のパス(Shift_JIS)" ) parser.add_argument( "--demand" , required= True , help = "需要データ CSV のパス(Shift_JIS)" ) parser.add_argument( "--inventory" , required= True , help = "初期在庫データ CSV のパス(Shift_JIS)" ) parser.add_argument( "--product" , required= True , help = "商品パラメータ CSV のパス(Shift_JIS)" ) parser.add_argument( "--output" , required= True , help = "出力先 CSV ファイルのパス(Shift_JIS)" ) args = parser.parse_args() export_first_day_orders( scalar_constraints_path=args.scalar_constraints, demand_path=args.demand, inventory_path=args.inventory, product_path=args.product, output_csv_path=args.output ) エージェントを活用することで、こちらのコードも短時間でミスなく作成できました。 5. 作成されたコードの実行と結果 例えば、必要なデータファイルと出力ファイルのパスを指定し、次のようなコマンドを実行することで、 python export_first_day_orders.py \ --scalar_constraints scalar_constraints.csv \ --demand demand.csv \ --inventory initial_inventory.csv \ --product products.csv \ --output initial_orders.csv 以下のような 各商品の当日発注量をまとめたCSVファイルが自動的に出力 されます。こちらの情報をもとに各商品の発注作業を行います。 このように、生成されたコードを活用することで、業務で実際に必要となるファイルの作成までを一貫して自動化できます。現場でもすぐに運用に取り入れやすいため、これまで発注作業にかかっていた時間の大幅な短縮や、ヒューマンエラーの削減が期待されます。 まとめ 本記事では、生成AIを活用した数理最適化技術の自動化に関する検討をご紹介しました。特に、スーパーマーケットにおける在庫管理を一例として取り上げ、エージェント3種のサポートのもとで課題設定から実装に至るまでのプロセスを示しました。 本記事で一部をご紹介したアプリケーションは現在も開発中であり、引き続き改良を重ねてまいります。 今後は、生成AIの回答精度向上やUI/UXの改善に取り組むとともに、実社会での活用に向けた検証を進め、研究開発を一層推進していく予定です。 おわりに 本記事が少しでも皆さまのお役に立てたのであれば、嬉しく思います。 Node-AIを活用した機械学習技術のビジネス活用にご関心のある方は、ぜひ こちら のフォームよりお気軽にお問い合わせください。 また、本記事の内容に関するご質問や、数理最適化技術をはじめとした数理解析技術のPoCにご関心のある方は、メールにてご連絡をお待ちしております。(メール:adam-ic[at]ntt.com)
アバター
NTTドコモビジネスが開発しているSBOM管理ソリューション「Threatconnectome」において、Trivyと同じ脆弱性データベースを使用しているにもかかわらず、特定のパッケージで脆弱性検出漏れが発生した事例を紹介します。 はじめに 1. Trivyにおけるパッケージの分類について バイナリパッケージとソースパッケージについて バイナリパッケージ ソースパッケージ バイナリパッケージとソースパッケージの分類の意図 Trivyにおける脆弱性検出の仕組み Threatconnectomeにおける脆弱性の未検知問題の詳細 プロジェクトでの対応策 実施後の効果 まとめ 脚注 参考文献 はじめに こんにちは。イノベーションセンターMetemcyberプロジェクトの千坂知也と申します。 本記事では、私たちMetemcyberプロジェクトが開発しているSBOM(Software Bill of Materials) *1 管理ソリューション「Threatconnectome」(以下Tc) *2 にて発生した大規模な脆弱性の検出漏れについて、その原因と対応、改善結果を取り上げます。 Tcでは、オープンソースの脆弱性スキャナTrivyが脆弱性情報として参照しているTrivy DBを利用しています。そんな中、Ubuntuのパッケージをスキャンした際、Trivyでは該当パッケージ内の脆弱性が検知された一方で、Tcでは該当パッケージ内の脆弱性を検知できていないことが発覚しました。 同じ脆弱性情報を利用しているはずなのになぜこのようなことが起きてしまったのか?私たちは、TrivyとTcの間で脆弱性マッチングの処理に何らかの違いがあると推測いたしました。 以降では、次の4つについて述べていきたいと思います。 Trivyにおけるパッケージの分類について Trivyの脆弱性マッチングとTcの脆弱性マッチングの差異について Threatconnectomeにおける脆弱性の未検知問題の詳細 プロジェクトでの対応策 1. Trivyにおけるパッケージの分類について オープンソースの脆弱性スキャナTrivy *3 では、コンテナイメージやファイルシステム、ソースコードなどに含まれる脆弱性や設定ミスを検出します [1] 。CI/CDパイプラインへの統合が容易であり、SBOM生成・ライセンススキャンなども活用できることからセキュリティ管理の幅広い領域で使われています [2] 。このTrivyでは、脆弱性スキャンの対象を大まかに以下の2種類に分けて扱います [3] 。 OSパッケージ:Linuxディストリビューションのパッケージマネージャで管理されるパッケージ(dpkg,apt, rpm, apk など) Langパッケージ:各プログラミング言語の依存ライブラリ (pip, npm, gem, go, maven など) このうちOSパッケージは、さらに手元でインストールするファイル群及びそれらのファイル群のインストール/アンインストールを制御するパッケージと元のソースが書かれたパッケージの2種類が存在します。Ubuntu(Debian系)のOSパッケージでは前者をBinary Package(バイナリパッケージ)、後者をSource Package(ソースパッケージ)と言います。次の章ではこのバイナリパッケージとソースパッケージを説明していきます [4] 。 バイナリパッケージとソースパッケージについて 公式Ubuntu documentationのPackage modelの項では、ソースパッケージ→バイナリパッケージの順で記載がありますが、ここでは分かりやすさを重視してバイナリパッケージから説明をさせていただきたいと思います [5] 。 バイナリパッケージ バイナリパッケージとは、パッケージマネージャが理解できる標準化された形式のパッケージのことです [6] 。Debian系のバイナリパッケージは、.debというファイル拡張子を持ち、次の2種類のファイル群を含みます。 対象システムにインストールされる実際のファイル群 それらのファイルをどのようにインストールまたはアンインストールするかを制御するファイル群 これにより、ソフトウェアを対象のマシンに簡単に配布・インストール・アンインストールできるようになります。 ソースパッケージ ソースパッケージは、複数のバイナリパッケージをビルドするためのソースコードおよびビルド情報を含んだものです [7] 。その構成は次の通りです。 Debianソースコントロール(.dsc)ファイル 1つ以上の圧縮されたtarファイル(.tar.gz, .tar.xz など) パッケージの形式に応じて、追加のファイル 特に.dscファイルには、以下のようなメタデータが含まれます。 ファイルのリスト パッケージ名とバージョン 作成されるバイナリパッケージの一覧 依存関係 デジタル署名 その他多数のフィールド つまり、普段我々が手元でインストールするファイル群以外に元のソースを含んだパッケージも別に存在しているということです。そもそもなぜこのような分類をしているのでしょうか? バイナリパッケージとソースパッケージの分類の意図 Ubuntu(および Debian 系)は、基本的にDebianのパッケージモデル(パッケージ方式)を採用しており、上述した通り「ソースパッケージ」→(ビルド)→「バイナリパッケージ」という流れが基本となります。 このような分類をしている理由の1つとして「同じソースから、異なるアーキテクチャやOS向けへとビルドできるようにするため *4 」があります [8] 。Debian系では、まず1つのソースパッケージが用意され、これをもとに複数のバイナリパッケージがビルドされます。実際にソフトウェアを使う環境はさまざまで、CPUの種類(アーキテクチャ)も異なります。UbuntuやDebian系でよく使われるアーキテクチャの具体例としては、amd64、arm64、i386などがあります。個別に各CPUアーキテクチャやOS環境向けのパッケージを用意するのではなく、大本のソースパッケージからそれぞれの環境へ適した最適な実行バイナリを生成するほうが、運用面で効率的です [9] 。 それ以外にもビルドの再現性を担保するため(ソースからバイナリを再ビルドして、元のバイナリと一致するか確認できる。改ざん検出やセキュリティ検証に不可欠)、パッチ管理を容易にするため(Debian系では、オリジナル開発元のソースに対し、独自パッチを追加して管理する)などもソースパッケージとバイナリパッケージを分ける利点として挙げられます。 Trivyにおける脆弱性検出の仕組み Trivyでは、OSパッケージとLangパッケージをそれぞれ独立して識別・スキャンし、対応する脆弱性データベースとマッチングします。この二層構造により、インフラ層(OS)とアプリケーション層(言語ライブラリ)の両面に潜むセキュリティリスクを網羅的に検出できる点が、Trivyの大きな強みです。 このマッチングに利用する脆弱性データベース(Trivy DB)では、Ubuntu(Debian系)のOSパッケージにおいてソースパッケージ名を基準に脆弱性情報を管理しています *5 [10] 。その理由はUbuntuやDebian系ディストリビューションの脆弱性情報が基本的にソースパッケージ単位で配布されているためです [11] 。そのため、マッチングに利用する脆弱性情報(今回の場合Trivy DB内の情報)もソースパッケージ名で格納しておかないと、マッチングすることが出来ないのです。 この仕組みによって、同一のソースパッケージからビルドされた複数のバイナリパッケージに対しても、一貫した脆弱性判定が可能となっています。 Threatconnectomeにおける脆弱性の未検知問題の詳細 冒頭でも申し上げたとおり、私たちが開発しているSBOM管理ソリューションTcにおいて、Ubuntuのパッケージに関する脆弱性検出漏れが発生しました。Tcは脆弱性情報としてTrivy DBを利用していますが、linux-modules-5.15.0-1025-gcpというパッケージを含むソフトウェアをスキャンした際、Trivyではパッケージ内の脆弱性が検知された一方で、Tc側では同一パッケージ内の脆弱性が検知できていなかったのです。 調査の結果、Tcでは入力されたSBOMファイルのバイナリパッケージ名とエコシステムのみを脆弱性マッチングのキーとして使用していたことが原因であると分かりました。 下記は実際に我々が入力したSBOMファイル(フォーマット:CycloneDX)の一部(コンポーネントのひとつ)になります。この例では"name"がバイナリパッケージ名、"aquasecurity:trivy:SrcName"がソースパッケージ名に該当します。この場合、"linux-gcp-5.15"がソースパッケージ名です。 前述したとおりUbuntu(Debian系)の脆弱性情報はソースパッケージ名で配布されているため、Trivy DBではUbuntuのOSパッケージの脆弱性情報をソースパッケージ名(この場合linux-gcp-5.15)で管理しており、Trivy自体も入力されたSBOMの"aquasecurity:trivy:SrcName"部分を抽出しマッチングを行います。 { "name": "linux-modules-5.15.0-1025-gcp", "purl": "pkg:deb/ubuntu/linux-modules-5.15.0-1025-gcp@5.15.0-1025.32~20.04.2?arch=amd64&distro=ubuntu-20.04", "properties": [ { "name": "aquasecurity:trivy:PkgType", "value": "ubuntu" }, { "name": "aquasecurity:trivy:SrcName", "value": "linux-gcp-5.15" }, … ], … } Tcは、Trivy DBの脆弱性情報を利用していたものの、入力されたSBOMから抽出したバイナリパッケージ名(linux-modules-5.15.0-1025-gcp)とpurlフィールドなどから構築されるエコシステムのみを使用していたため、Trivy DB内にあるソースパッケージ名(linux-gcp-5.15)と一致せず、脆弱性を検出できない問題が発生したということです。 適切な脆弱性情報は(Trivyと同じ脆弱性情報を利用していたため)あったものの、入力されたSBOMから抽出するフィールドが異なっていたということです。 SBOM内の"aquasecurity:trivy:PkgType"がubuntuであることから、このパッケージはUbuntuのOSパッケージであると判別できます。しかし、OSパッケージ特有のSrcNameを正確に処理するロジックが不足していたわけです。 プロジェクトでの対応策 前章で述べた通り、Tcでは脆弱性マッチングの際に入力されたSBOMのバイナリパッケージ名を使用していたことが原因で、ソースパッケージ名ベースで管理されているTrivy DBの脆弱性情報と一致せず、一部の脆弱性を検出できていませんでした。この問題を受け、我々のプロジェクトでは以下の対応を実施しました。 OSパッケージとLangパッケージの区別を明確化 入力されたSBOMに含まれる脆弱性を解析する際に、OSパッケージとLangパッケージを明確に区別して扱うようにし、OSパッケージの場合であれば、入力されたSBOMの「ソースパッケージ名」と「バイナリパッケージ名」の両方を保持するよう仕様を変更しました。 パッケージの一意識別ロジックを拡張 パッケージを一意に識別するための条件を、従来の「バイナリ名+エコシステム」から次のように拡張しました。 一意識別キー:(エコシステム, バイナリパッケージ名, ソースパッケージ名) 脆弱性マッチング条件の改善 脆弱性データベースとのマッチング条件を次のように変更しました。 マッチング条件: 「エコシステムが同一」かつ(「バイナリパッケージ名が一致」または「ソースパッケージ名が一致」) さらに、バイナリパッケージ名、ソースパッケージ名両方の一致候補が存在する場合には、ソースパッケージ名一致を優先するルールを設けることで、判定の一貫性と精度を確保しました。 実施後の効果 これらの対応により、 バイナリパッケージ名、ソースパッケージ名の両方 でマッチング可能になり、脆弱性の検知精度が大幅に向上しました。 linux-gcp-5.15パッケージの脆弱性に限ってみれば3191件もの脆弱性が新しく検知されるようになりました(2025/7/2時点)。逆にいうと、本対応以前までlinux-gcp-5.15パッケージに関するものだけでも3191件もの脆弱性を見落としていたということになります。 まとめ 本稿では、OSパッケージの分類による差分から発生する脆弱性の未検知問題について取り扱いました。SBOMなどを用いたソフトウェア内のパッケージ単位での脆弱性管理は求められつつある一方で、その管理の困難性や一貫したアプローチ法がないなどの課題もあります。我々Metemcyberプロジェクトでは、Trivyを踏襲しつつ、独自のモデル設計によりこれらの問題の解決を図ってきました。ご興味がある方はぜひ Threatconnectome の GitHubページ などもご覧ください。ご連絡お待ちしております。 脚注 ソフトウェアを構成するコンポーネント(オープンソースソフトウェア、ライブラリなど)の一覧表(リスト)。ソフトウェアのBOM。各コンポーネントの名称、バージョン、依存関係、ライセンス情報などが含まれる。代表的なSBOM生成ツールにTrivy、Black Duck、Syftなどがあり、代表的なSBOMフォーマットにCycloneDX、SPDXなどがある。 ↩ nttcom/threatconnectome ↩ Trivy ↩ 全てのアーキテクチャに対応しているわけではない。ビルド依存関係によっては対応していないアーキテクチャもある。 ↩ 全てのOSパッケージの脆弱性情報がソースパッケージ名で格納されているわけではない。 ↩ 参考文献 Vulnerability - Trivy Container Image - Trivy Vulnerability Scanning - Trivy Package model - Ubuntu documentation Package format - Ubuntu project documentation 3. Binary packages — Debian Policy Manual v4.7.2.0 4. Source packages — Debian Policy Manual v4.7.2.0 Debian Policy Manual 5.3. Structure of a Source Package Ubuntu vulnerability source - Trivy DB ubuntu-cve-tracker
アバター
こんにちは。NTTドコモビジネスの露崎です。本ブログでは vLLMの本家コミュニティのブログ で紹介されたvLLMのモデルのゼロリロード切り替え機能の概要に加えて本機能をContainerベースで検証した結果について紹介します。 はじめに モデルのゼロリロード切り替え機能が取り組む課題 vLLM Sleep Mode Sleep Level 動作確認 環境構成 利用モデル 実験1. API利用状況の確認 vLLMの起動 Sleepによるモデルの停止 WakeUpによるモデルのリロード 実行結果とパフォーマンス考察 実験2. 複数モデルでの切り替え 準備 実験(各モデルに対して) 実験用スクリプト 実行結果 まとめ はじめに こんにちは。NTTドコモビジネスの露崎です。本ブログでは vLLMの本家コミュニティのブログ で紹介されたvLLMのモデルのゼロリロード切り替え機能について紹介します。 vLLM はカリフォルニア大学バークレー校のSky Computing Labが開発しOSSで公開している推論高速化ソフトウェアです。vLLMでは量子化やバッチ処理を始めとしたさまざまな高速化テクニックを実装しており、Large Language Model(LLM)の推論を高速化できます。また、vLLMはモデルリポジトリである huggingface で公開されているさまざまなモデルに対応しており、Meta社の Llama を始めとするオープンウェイトモデルを高速に実行する基盤ソフトウェアとして世界的に利用されています。 モデルのゼロリロード切り替え機能が取り組む課題 本ブログではvLLMを用いたLLM推論の提供において、同一のGPU上で複数のモデルを利用したい場合のモデル切り替えのオーバヘッドを解決する機能を紹介します。 vLLMはLLMの推論を高速に実行し、ユーザに結果を素早く提供できる有用性の高いソフトウェアです。 vLLMは高速な推論を実現するためにLLMの計算用キャッシュ(a.k.a. KV Cache) を事前にできるだけ確保する(デフォルトでは利用可能なメモリの90%)という仕組みを持っています。 この動作は単一のモデルを提供する際には特に問題となることはありません。しかし、GPUと言う貴重な計算リソースをある1つのモデルのために占有してしまうと言うことでもあります。 この特性のため、以下のように複数モデルを使い分けるユースケースにおいては、利用するモデル毎にGPUリソースを割り当てる必要があり、コストを増大させることにつながります。 質問からテキストベースで回答するLLMと画像の説明や分類を説明するVisual Launguage Model (VLM)を使い分けるシステム 複数の異なるLLMの出力結果を組み合わせて最終的な回答を得るエージェント この対策として、vLLMが提供している公式 Containerイメージ を利用し、需要やタイミングに応じて使うモデルをContainerの起動、終了によって切り替えると言った運用は可能です。 しかし、ファイルシステムからのモデルのロードや、起動時に実行される Cuda Graph 構築による最適化のオーバヘッドのため、例えば gemma-3-4b-it のような比較的小さいモデルであっても1分程度の起動待ちが発生すると言う課題があります。 vLLM Sleep Mode 前述の課題を解決する機能の1つがvLLMのSleep Modeです。Sleep Modeは起動中のvLLMに対して /sleep APIを呼び出すことでロード済みのモデルをCPUのメモリに待避する機能です。待避されたメモリは /wake_up APIを呼び出すことでGPUメモリへリロードできます。 /wake_up ではCPUからGPUへメモリ転送を実施するためファイルシステムからモデルを読み込むより高速にロードが実行でき、CUDA Graphの構築のオーバヘッドを回避できるため、vLLMのプロセスを最初から起動するよりも迅速にAPIが利用できるようになります。 NOTE Sleep Modeを使うためには環境変数 VLLM_SERVER_DEV_MODE=1 を設定する必要があります。 Sleep Level /sleep APIには引数でスリープ時のレベルを設定できます。(例: /sleep?level=1 ) 現在は2段階のスリープレベルがあり、それぞれ以下の通りです。 Level 1: モデルの重みをCPU RAMへオフロードし、KV CacheをGPUメモリから廃棄する Level 2: モデルの重みとKV CacheをGPUメモリから破棄し、スリープ時のCPU RAMの使用量を最小限にする 公式ドキュメント ではLevel 1は同一のモデルを再度利用する際に有効であり、Level 2は異なるモデルを利用する場合に有効である、と解説しています。 動作確認 vLLMの本家コミュニティのブログ や 公式ドキュメント ではPython API、及び、マルチプロセスでの実例が紹介されていますが、実際の運用を考慮するとContainerベースでもこの機能が実行できるかを確認したかったため、現在のStableの最新版である v0.11.0 で動作確認を実施した結果を紹介します。 環境構成 ハードウェア NVIDIA GeForce RTX 4090 24GB ソフトウェア Ubuntu 22.04.5 LTS NVIDIA Driver 580.95.05 docker-ce 5:28.5.1-1 vLLM v0.11.0 利用モデル google/gemma-3-4b-it meta-llama/Llama-3.2-1B-Instruct meta-llama/Llama-3.2-3B-Instruct 実験1. API利用状況の確認 vLLMの起動 まずはSleep Modeが現在のStableであるv0.11.0で利用できるのかを確認します。 以下のコマンドを実行してモデルを起動します。リポジトリへのアクセス権等の設定については必要に応じてHuggingFaceのアカウントで作成したトークンを環境変数 HF_TOKEN へ設定してください。 MODEL="google/gemma-3-4b-it" sudo docker run --runtime nvidia --gpus all \ -v ~/.cache/huggingface:/root/.cache/huggingface \ -p 8000:8000 \ --ipc=host \ --env "HUGGING_FACE_HUB_TOKEN=$HF_TOKEN" \ --env "VLLM_SERVER_DEV_MODE=1" \ vllm/vllm-openai:v0.11.0 \ --model ${MODEL} --enable-sleep-mode このコマンドでvLLM v0.11.0を起動できますが、起動時に以下がログ出力されます。 WARNING 10-29 18:58:56 [api_server.py:966] SECURITY WARNING: Development endpoints are enabled! This should NOT be used in production! この後の手順でAPIのサンプルを提示しますが、 /sleep APIは現在、通常のChat API同様のAPIエンドポイントによって受け付けられるため、素のままのAPIを利用すると任意のユーザがモデルの停止ができる状態になります。このため、プロダクションでの利用は現在推奨されていないようです。 Sleepによるモデルの停止 次にSleep ModeのAPIを呼び出します。Sleep ModeのAPIは以下の通りREST API経由で実行します。 curl -X POST 'localhost:8000/sleep?level=1' これを実行すると以下のようなログが出力され、GPUメモリが解放されます。 (EngineCore_DP0 pid=165) INFO 10-29 19:36:16 [cumem.py:228] CuMemAllocator: sleep freed 20.25 GiB memory in total, of which 8.63 GiB is backed up in CPU and the rest 11.62 GiB is discarded directly. (EngineCore_DP0 pid=165) INFO 10-29 19:36:16 [gpu_worker.py:117] Sleep mode freed 20.23 GiB memory, 1.21 GiB memory is still in use. (EngineCore_DP0 pid=165) INFO 10-29 19:36:16 [executor_base.py:189] It took 1.695459 seconds to fall asleep. (APIServer pid=1) INFO: 172.17.0.1:36138 - "POST /sleep?level=1 HTTP/1.1" 200 OK 今回利用した google/gemma-3-4b-it ではvLLM起動時に23GB程度占有していたメモリが、 /sleep 呼び出し後には1GB程度まで減っていることを確認しました。 NOTE Sleep Mode中のモデルに対して推論リクエストを実施するとv0.11.0の時点では400 Errorとなり、vLLMプロセスがクラッシュして停止します。 WakeUpによるモデルのリロード モデルのリロードを実施するには以下のコマンドを実行します。 curl -X POST 'localhost:8000/wake_up' これを実行すると以下のようなログが出力されます。 (APIServer pid=1) INFO 10-29 19:43:03 [api_server.py:1016] wake up the engine with tags: None (EngineCore_DP0 pid=165) INFO 10-29 19:43:03 [executor_base.py:205] It took 0.368304 seconds to wake up tags {'kv_cache', 'weights'}. (APIServer pid=1) INFO: 172.17.0.1:58818 - "POST /wake_up HTTP/1.1" 200 OK WakeUpが完了すると、通常のvLLMとしてOpenAI互換のAPIが利用できます。 NOTE Level 2のSleep Modeでは /wake_up の後に /collective_rpc (methodとして reload_weights を指定) 、 /reset_prefix_cache のAPIを呼び出す必要があります。 実行結果とパフォーマンス考察 モデル実行時の手順とオーバヘッドは以下の通りでした。 level 1 level 2 Sleep後のGPU使用メモリ 1236MiB 1241MiB 復帰時の手順 1 (wake_up) 3 (wake_up, collective_rpc, reset_prefix_cache) 復帰時のコマンド実行時間 (sec) 0.368304 0.737 Level 1、Level 2のいずれにおいても1秒以内にモデルの復帰が完了するため、vLLMのContainerを再起動するより高速にAPIを使えるようになることがわかります。GPUメモリとしてはLevel 1とLevel 2では削減効果は同等でした。復帰の手順の複雑性を考慮するとLevel 1を標準として利用可能だと考えます。公式ブログではLevel 2の際にCPU側のRAMを節約できると記載があるため、GPUだけでなくSleep中のCPUメモリも削減したい場合はLevel 2を使うと良いでしょう。 実験2. 複数モデルでの切り替え 実験1でvLLM v0.11.0のContainerでSleep Modeを利用可能なことが確認できたため、複数のモデルの切り替えを実行してみます。 vLLMの公式ブログでは同じ環境内の別プロセスとして起動する方法を紹介していましたが、ここではより本番環境に近づけるため1枚のGPUに対して2つのvLLM Containerを起動した状態でモデルの切り替えが実行できるかを試しています。 実験の実行手順としては以下の通りです。 準備 1つめのモデルのvLLM Containerを起動(8000ポート) /sleep?level=1 APIで1つめのモデルをSleep 2つめのモデルのvLLM Containerを起動(8001ポート) /slee?level=1 APIで2つめのモデルをSleep 実験(各モデルに対して) /wake_up APIでモデルのリロード sample.py スクリプトを用いてhealthcheck及び、推論APIの呼び出し 実験用スクリプト 今回は以下のようなBashスクリプトで実験をしています。 #!/bin/bash set -e # 切り替えるモデルの定義 MODELS=("meta-llama/Llama-3.2-1B-Instruct 8000" "google/gemma-3-4b-it 8001") RUN_NAME="vllm_sleep" # コンテナの作成と起動用関数を定義 create_vllm(){ c_name=${1#*/} echo "Use name ${c_name}" docker run -d --name ${RUN_NAME}-${c_name} --rm --runtime nvidia --gpus all \ -v ~/.cache/huggingface:/root/.cache/huggingface \ -p $2:8000 \ --ipc=host \ --env "VLLM_SERVER_DEV_MODE=1" \ --env "HUGGING_FACE_HUB_TOKEN=${HF_TOKEN}" \ vllm/vllm-openai:v0.11.0 \ --model $1 --enable-sleep-mode } # 各モデルを起動し、APIの疎通が確認できたらSleepする for model in "${MODELS[@]}"; do read model_id port <<< "$model" c_name="${RUN_NAME}-${model_id#*/}" echo "start vllm container" create_vllm ${model_id} ${port} echo "wait and call an inference" uv run sample.py ${port} echo "suspend model with level 1" curl -X POST "http://localhost:${port}/sleep?level=1" done # 各モデルに対してリロードしAPIが実行できれば再度Sleepする echo "--- start benchmark for model switching ---" for model in "${MODELS[@]}"; do read model_id port <<< "$model" c_name=${RUN_NAME}-${model_id#*/} echo "Use name ${c_name}" curl -X POST "http://localhost:${port}/wake_up" echo "wait and call an inference" uv run sample.py ${port} echo "suspend model with level 1" curl -X POST "http://localhost:${port}/sleep?level=1" done # 起動しているContainerをCleanupする docker ps -q | xargs docker kill Containerの起動状態を確認し、推論APIを呼び出すPythonスクリプト( sample.py )は以下の通りです。 #!/usr/bin/env python3 from openai import OpenAI import os import requests import time import sys HOST = "http://localhost" def wait_to_ready(URL): while True: try: requests.get(f"{URL}/health") print("Healthcheck: OK") break except requests.exceptions.ConnectionError: time.sleep(0.1) def main(): port = sys.argv[1] if len(sys.argv) > 1 else 8000 url = f"{HOST}:{port}" wait_to_ready(url) # Localhost上のvLLMでありOpenAIの実際のキーは不要のためdummyを設定 os.environ["OPENAI_API_KEY"] = "dummy" client = OpenAI(base_url=f"{url}/v1") model_id = client.models.list().data[0].id print(f"Model {model_id} found.") response = client.responses.create( model=model_id, input="Write a one-sentence bedtime story about a unicorn." ) print(f"Got response: {response.output_text}") if __name__ == "__main__": main() 実行結果 実験スクリプトを実行すると、 --- start benchmark for model switching --- 以下の出力がおよそ1秒程度で完了することが確認できると思います。このことからvLLMのSleep Modeを利用することで複数のContainer、モデルに対して1つのGPUリソースで効率的なモデルの切り替えを実施できることが確認できました。 NOTE 例示したスクリプトでは meta-llama/Llama-3.2-1B-Instruct 、 google/gemma-3-4b-it の順序で起動し、問題なく /sleep , /wake_up が動作することを確認できました。しかし、別の実験として meta-llama/Llama-3.2-3B-Instruct 、 google/gemma-3-4b-it で試した場合には、Sleep後に meta-llama/Llama-3.2-3B-Instruct を /wake_up させるタイミングでOut of Memory (OOM) Errorが起き、プロセスが停止しました。 meta-llama/Llama-3.2-3B-Instruct は meta-llama/Llama-3.2-1B-Instruct より大きなモデルで、必要なGPUメモリが大きいため、Sleep Modeで切り替える場合であっても最低限必要なメモリの量は事前に調査しておく必要があると考えられます。 まとめ 本ブログではモデル切り替えのオーバーヘッドを削減するvLLMのSleep Modeについて紹介しました。Sleep Modeはまだ開発段階の機能ですが、複数のモデルの処理に対してGPUを効率的に利用できる機能ということが確認できました。今後、LLMの需要拡大に向けてこうしたGPUの処理効率を向上させるソフトウェア、仕組みを活用することはますます重要になると考えられます。
アバター
こんにちは。イノベーションセンターの加藤です。 手書きから活字へスタイル変換するモデルをFlow Matchingで学習しようとして色々試したものの上手くいかなかったため、試行錯誤の記録をブログの形で残したいと思います。 背景 上手くいかなかった手法たち 手法1:手書き文字分布から活字分布への変換 手法2:ControlNet学習 手法3:学習済みの活字生成モデルを手法1で転移学習する まとめ 背景 このモデルを学習しようとしたきっかけは手書き文字OCRの性能を向上させようという取り組みでした。OCRの難しい点として、現実のデータにはさまざまな特殊文字が現れるという問題があります。これら全ての手書き文字を学習データとして用意するのは難しく、色々な手法が提案されています。 その手法のひとつに、「活字から手書き文字を生成するモデルを作る」というものがあります。活字はフォントファイルから手に入るため、すでに手元にある手書きデータと共に手書き文字生成モデルを学習し、未知の活字からさまざまな形の手書き文字を学習データ用に生成することで、OCRのデータセットを増強できるというモチベーションです。 この方法を聞いたときに、同じことを「手書きから活字を生成するモデル」でもできるのではないか?と思いました。しかもこちらの方が色々な手書き文字を作って学習を回すという手間を省けるという利点があります。 そこで、Flow Matchingと呼ばれる生成モデル学習手法を使って手書き文字から活字に変換するモデルの学習に挑戦しました。手書き文字データセットはETL9B 1 、活字データセットはGoogle Noto Font 2 を利用し、JIS第1水準漢字を対象にしました。 上手くいかなかった手法たち 手法1:手書き文字分布から活字分布への変換 最初に試したことは、手書き文字から活字へ少しずつ変化させるUNetモデルの学習です。 TorchCFM 3 と呼ばれるライブラリを用いて、手書き文字の分布から活字文字の分布に少しずつ変化させるモデルを学習します。 今回のケースでは以下のような変換過程を学習しています。 データセットの分割はStratified K-Foldを用いて、学習で利用した文字種は評価時に現れないよう工夫しました。 結果は次のような画像になりました。稀にさんずいやしんにょうなどを捉えることはできていますが、基本的によくわからない文字になってしまっています。また、「沖」という文字が「汁」になっているなど、形が似ている別の学習データになってしまっている例も見受けられました。 手法2:ControlNet学習 先ほどの実験では入力に似た学習セット内の文字が出力されるというケースがありました。 そこで次はあらかじめ全ての活字に対して学習を回せるという仮定をおき、まず活字を生成するモデルを作ってから手書き文字で条件付けるという方法を試しました。 フォントデータは手書きよりも簡単に多くの文字種を得られるため、この仮定に無理はないはずです。 この方法でよく使われるのはControlNet 4 です。これはあらかじめ画像生成モデルとして学習されたUNetに、画像で表現できるcontrol情報を注入するためのUNetを新しく学習する手法です。 これによって例えば人体の姿勢情報からそれに沿った人物の画像を出力するなどといったことが可能になります。 データセットの分割は先ほどとは異なり、まず全ての活字画像を使ってランダムノイズから活字を生成するモデルを作り、そしてStratified K-Foldで分割された手書き文字画像を使って条件付けモデルを学習させました。 そのため変換過程は以下のようになり、途中からヒントとして手書き文字の特徴量を加える形になります。 まず活字生成モデルは次のようになりました。学習時には上下左右反転のdata augmentationを行っているので向きが変わっていますが、元の活字のスタイルを正しく生成できているようです。 しかし条件付けモデルの結果は以下のように、残念ながら入力した条件に従わせることができず、相変わらずランダムな活字風の画像が生成されてしまいました。 このように条件に従ってくれない現象はControlNetの元論文でも指摘されており、しばらく学習するとちゃんと従うようになるとされているのですが、残念ながらこの実験ではそのような収束が見られませんでした。おそらく一般的な画像生成とは異なり、文字の生成ではあまりに簡単すぎるため手書き文字画像によるヒントがうまく使われなかったのではないかと思います。 手法3:学習済みの活字生成モデルを手法1で転移学習する 手法1ではあらかじめ全ての活字画像を学習時に活用できず、手法2ではうまく手書き文字画像をヒントとして活用できませんでした。そこで、手法2で作成した活字画像生成モデルの重みを手法1の初期値に利用することで、2つの手法の欠点を補ってみます。 結果は次のような画像になりました。一部うまくいった文字もあり(く、詠、円など)、心なしか似たような文字が出せるようにはなったのですが、結局ほとんどは関係のない活字に変換してしまいました。 まとめ 手書き文字を活字に変換するためFlow Matching周りの手法を色々試してみましたが、異なるスタイルの文字を一対一対応させるのはかなり難しそうでした。 GANなどの方がむしろ安定して学習できる可能性もあるため、こちらも試してみたいです。 ETL文字データベース http://etlcdb.db.aist.go.jp ↩ Noto Sans Japanese https://fonts.google.com/noto/specimen/Noto+Sans+JP ↩ conditional-flow-matching https://github.com/atong01/conditional-flow-matching ↩ https://arxiv.org/abs/2302.05543 ↩
アバター
イノベーションセンターの新井です。 普段は全社検証網の技術検証、構築、運用を担当しています。 私の所属するイノベーションセンターではこれまではイーサネット/IPレベルの検証網を運用して全社に提供していましたが、昨今の光伝送とIP系技術の融合の進展などにも対応して、さまざまなユースケースを検証・開発するために、光伝送に関する検証網(All Photonics Network Testbed。以下、APNテストベッド)も同一チームで構築・運用しています。 今回から複数回に分けて現在取り組んでいるAPNテストベッドで用いている技術や運用手法を解説していきます。記事内では我々と同じようにイーサネット/IP技術にはある程度詳しいが光伝送技術の知識はそれほどでもない、という方向けに解説します。 本記事のサマリーです。 光伝送ネットワーク(APN:All Photonics Network)の検証網、APNテストベッドについて連載します。 連載1回目の本記事では、光伝送のオープン化の全体像とトランスポンダーにおけるオープン化の進展について紹介します。 特にIP over DWDM関連やホワイトボックストランスポンダーのアーキテクチャーについて解説し、それらを使った運用手法の一端を紹介します。 光伝送ネットワークの概要 光伝送で用いられる機能 光伝送のオープン化 トランスポンダーのアーキテクチャーとIP over DWDM マックスポンダーとL2スイッチの違いについて ネットワークにおける役割分担 400G-ZR/ZR+とIP over DWDM ホワイトボックストランスポンダー L2/L3レイヤーにおけるホワイトボックススイッチの利用の進展 ホワイトボックストランスポンダーへ 専用トランスポンダーかIP over DWDM構成か 運用における知見 APNテストベッドの構成 複数ベンダーシャーシ/トランシーバーの混在運用 gNMIの活用 まとめ 光伝送ネットワークの概要 光伝送ネットワークではROADMやトランスポンダーといった機器が登場します。これらの役割について馴染みのない方もいるかと思いますのではじめに説明します。 光伝送で用いられる機能 長距離光伝送で用いられる機能は一般的にトランスポンダー、WDM(Wavelength Division Multiplexing)、アンプなどで構成されます。 トランスポンダー(Transponder, TRPN) : 一般的なイーサネット信号などのクライアント信号を波長多重可能かつ長距離延伸が可能な光信号へ変換する機能です。クライアント側(端末などが接続される側)からの光信号を一旦電気信号へ変換後、再度光信号へ変換(Optical-Electric-Optical変換)してライン側(他拠点との接続側)と接続します。 WDM(Wavelength Division Multiplexing) : 波長多重を行う機能です。単純化したわかりやすいイメージとしてはプリズムのようなものです。プリズムを通して1本の光線が色(=波長)の異なる光線に分離するように、異なる波長に情報を載せた光信号を複数まとめた光を生成したり、逆にまとめた光からそれぞれの波長の光信号を分離したりする機能のことです。WDMはその多重度に応じてさらにDWDM(Dense WDM), CWDM(Coarse WDM)などに分類されます。 WSS(Wavelength Selective Switch) : WDM信号を各波長単位で任意の出力へ制御する波長スイッチ機能です。単純化したわかりやすいイメージとしては波長単位で可変的に動く鏡のようなものです。WSSは波長単位で多重化や出力ポート選択を行えるためスイッチ機能とWDM機能を両方実現できます。 アンプ(Amplifier, AMP) : 光信号の強度を増幅する機能です。 ROADMはReconfigurale Optical Add/Drop Multiplexerの略で、WSSを活用することにより波長単位でスイッチ(Add/Drop)することで、Point-to-Point以外の伝送トポロジーを構成することを可能とする装置です。ROADMにより光伝送ネットワークをSDN(Software-defined-Network)的な制御とすることが可能となります。APNテストベッドでも東京を中心としたハブ&スポーク型のトポロジー、一部拠点間ではリング型のトポロジーとなっていますが、このように光伝送のみである程度自由なネットワークトポロジーが作れるのはROADMならではの利点です。 光伝送のオープン化 従来は相互接続性や管理運用の制限によりこれら一連の機能を実現する装置群を単一のベンダーで構築することが一般的でしたが、WDM可能な信号を送信するデジタルコヒーレント 1 トランシーバー(Digital Coherent Optics, 以降DCO)をスイッチ/ルーターに搭載するいわゆるIP over DWDMの構成(後述します)の台頭などもあり、OOLS(Open Optical Line System)と呼ばれる他社製のトランスポンダーとの連携を重視する構成が注目されています。さらにROADMにおいても標準化に関する取り組みであるOpenROADM MSA(Multi-Source Agreement) 2 に基づき装置の機能単位や筐体内接続を分割するDisaggregation構成をとり、トポロジー変更やネットワーク規模の拡大に対応しやすくする構成も利用されています。また、トランスポンダーやスイッチ/ルーターについてもOSとハードウェアを分割するいわゆるホワイトボックス装置も利用されています。 上記の説明を簡略化したイメージ図が下図になります。 APNテストベッドではこれらの内、最下段に記載した構成を主に用いて構築・運用しています。 トランスポンダー ホワイトボックストランスポンダーを主に利用しています。ベンダー固有の専用筐体/OSで構成されるトランスポンダーも利用していますが、その場合はスイッチ/ルーターにも搭載可能な3rd party製の400G-ZR+/100G-ZR DCOなどQSFP型のトランシーバーを採用しています。後述するIP over DWDM構成もトランスポンダーの一種として活用しています。 ROADM OpenROADM MSAに基づいたDisaggregatedな装置を利用しています。ハブ&スポーク型のトポロジーでは多数の拠点への「方路」を構成する必要がありますが、その方路ごとに筐体を用意するクラスター型の構成となっています。 本記事では上記2つの内トランスポンダーにおけるオープン化の進展としてIP over DWDM構成とホワイトボックス化に焦点をあてて解説し、運用で得られた知見も共有します。 ROADMのDisaggregationやOOLS利用における注意点などについては次回以降に掲載する予定です。 トランスポンダーのアーキテクチャーとIP over DWDM トランスポンダーの役割についてIP over DWDM構成と比較しながら改めて説明します。 マックスポンダーとL2スイッチの違いについて 複数のクライアント信号を集約して1波長にまとめて送出するトランスポンダーは多重化のmultiplexer(Mux)と接続した単語で「マックスポンダー(Muxponder)」と呼ばれます。 マックスポンダーと、スイッチ/ルーターは一見似たように「複数の信号を集約して出力する」動作をしますが、実際には制御対象のレイヤが異なります。 トランスポンダー/マックスポンダー(Layer 1, 物理層) 入力ポートと出力ポートを固定的に対応付け、電気⇔光の変換や光信号変換をします。フレーム/パケットの中身は解釈しません。gearboxと呼ばれる機能部位で電気インターフェースを固定的に接続します。 3 → 「線をつなぐ/信号の形を変える」役割 スイッチ/ルーター(Layer 2/3, データリンク層/ネットワーク層) フレーム/パケットを解釈し、MACアドレスやIPアドレスに基づいて転送先を動的に決定します。 → 「データを理解して行き先を決める」役割 ネットワークにおける役割分担 このように従来は異なるレイヤーの装置として役割分担が行われており、一般的なネットワーク構成としては以下のようになっていました。 拠点内(LAN):スイッチやルーターでパケットを制御 拠点間接続(WAN):拠点内機器をマックスポンダーに接続し、ROADMやWDMを組み合わせて大容量・長距離伝送 400G-ZR/ZR+とIP over DWDM 従来は「専用のトランスポンダー筐体」で信号を変換し、その先にスイッチ/ルーターを接続していました。しかし近年、QSFP-DDやOSFPといった小型汎用トランシーバーの形状で長距離光伝送を実現するDCOである400G-ZR/ZR+が登場しました。 これにより、スイッチ/ルーターのポートに直接トランスポンダー機能を内蔵できるようになり、専用のトランスポンダーを使わずとも、スイッチ/ルーターから直接WDM装置(ROADMなど)に接続可能となりました。この構成を「IP over DWDM」と呼びます。 従来: スイッチ/ルーター ⇔ トランスポンダー筐体(マックスポンダー) ⇔ ROADMなど IP over DWDM: スイッチ/ルーター(ZR/ZR+モジュール搭載) ⇔ ROADMなど また、400G-ZR/ZR+では、トランシーバーの制御にCMIS(Common Management Interface Specification)を用いています。これは短距離用トランシーバーの管理仕様を拡張したものであり、既存のNetwork OSを大きく変更せずに対応しやすいというメリットがあります。実際に市販されている多くのメーカーの装置で400G-ZR/ZR+を動作させることが可能になっています。このようにスイッチ/ルーター機能とトランスポンダー機能を一筐体で実現できることからDCOを搭載したスイッチ/ルーターをスイッチポンダーという名前で呼ぶ場合もあります。 特に近年では首都圏や関西圏内において、AI関連のインフラ整備やISP/CSPにおける映像伝送トラフィックの増大に伴い、複数のデータセンター間で大容量回線を必要とする動きがみられます。このような背景からネットワーク帯域の多重化と長距離光信号への変換をマックスポンダーではなくIP over DWDMで直接実現する構成も導入が始まっています。 このような構成のバリエーションとして、800G-ZRや100G-ZR DCOなど、さらなる小型・高効率なDCOも登場しています。これにより、IPレイヤと光レイヤの統合が進み、データセンター間の接続構成は省スペース・省電力でよりシンプルに設計できるようになると予想されます。 ホワイトボックストランスポンダー L2/L3レイヤーにおけるホワイトボックススイッチの利用の進展 トランスポンダーのホワイトボックス化について説明する前にスイッチ/ルーターのホワイトボックス化について説明します。 ホワイトボックスとはネットワーク機器においてハードウェアとソフトウェア(Network OS:NOS)を分離して設計・導入できるオープンなアーキテクチャを指します。2025年現在、このホワイトボックスアーキテクチャで広く普及しているNOSが「SONiC」です。SONiCはMicrosoftによって開発され、OCP(Open Compute Project)に移管された後、現在はLinux Foundation傘下でオープンソースとして継続的に開発が進められているLinuxベースのNOSです。SONiCは複数のホワイトボックススイッチベンダーに対応しており、主に大規模なデータセンターネットワークを実現するために、クラウド事業者やデータセンター運用者を中心に採用が進んでいます。 SONiCが備える特徴のうち、後述するホワイトボックストランスポンダーにも共通する点として、以下の2つが挙げられます。 ハードウェア選定の自由度が高い SONiCは複数のハードウェアベンダーの筐体で動作可能なため、目的や要件に応じて最適な機器を選択できます。 オープンな運用手法が活用可能 SONiCはLinuxをベースに開発され動作しており、基本的には既存のリッチなLinux運用手法を活かした管理・自動化が可能です。また、gNMI(gRPC Network Management Interface)によるStreaming Telemetryなど広く用いられるモダンな運用手法との親和性も高く、効率的なネットワーク運用を実現できます。 1のハードウェア選定の自由度向上を実現している方法について少し詳しく説明します。 ネットワークスイッチは、パケットを高速処理するために スイッチASIC(Application Specific Integrated Circuit)と呼ばれる専用チップを用いています。NOSは、このASICを制御する必要がありますが、従来はASICベンダーごとに異なる専用SDK(Software Development Kit)を用いて実装する必要がありました。また、これらのSDKはライセンス契約が必要になるケースも多く、オープンなNOSの開発や移植性において障壁となっていました。 このような状況を改善するために策定されたのが、SAI(Switch Abstraction Interface)です。 SAIはNOSとASIC SDKの間に位置する抽象化された共通APIであり、ASIC固有の仕様を意識することなくOS開発者が共通のインターフェース経由でハードウェアを制御できるように設計されています。SAIは現在もOCPで開発が進められています。 このSAIにより、NOSは特定ベンダーのSDKに依存せず、同一のSAI APIを介して複数のASICで動作可能となり、開発効率が大きく向上し多数のハードウェアでの動作を行えるようになりました。SONiCもこのSAIを利用してスイッチASICを制御しており、多数のホワイトボックス筐体とともに利用できます。 ホワイトボックストランスポンダーへ ホワイトボックストランスポンダーも基本的にはホワイトボックススイッチの特徴を引き継いでいます。 ホワイトボックストランスポンダーの開発において主たる役割を果たしてきたのはNTTを含めて多数の事業者が参画する Telecom Infra Project (TIP)です。TIPはOCPなどで成功したOpenなコンピューティングの取り組みを通信事業者においても実現できないか目指している団体ともいえます。 ホワイトボックストランスポンダーでオープンなアーキテクチャの実現に貢献している仕組みがTAI(Transponder Abstraction Interface)です。TAIは前述したSAIの光伝送版ともいえるものです。SAIがスイッチASICの制御を抽象化するAPIであるのに対してTAIはその名の通りトランスポンダーに必要な光伝送の部品を抽象化します。 API 主対象 利用領域 開発体制 SAI スイッチASIC スイッチ/ルーター OCP TAI トランスポンダー 光伝送 TIP TIPではこのTAIを活用したNOSであるGoldstone OSの仕様とリファレンス実装を開発し、我々が現在APNテストベッドで活用しているトランスポンダーのOSもこのGoldstone OSを元にした製品です。 さらにSONiCがdockerコンテナとして各機能を実装している構成であるのと類似して、下記のようにkubernetesのpodとして各機能が実装されており、ユーザーからも中身が理解しやすいものとなっています。 $ kubectl get pod NAME READY STATUS RESTARTS AGE north-noscmd-9vxkx 1/1 Running 0 68d tai1-2-54cf969f6b-rhtz2 1/1 Running 0 68d tai2-1-68b48b6c54-jtc42 1/1 Running 0 68d tai2-2-5868f89fdb-zrmmh 1/1 Running 0 68d tai1-1-5774878878-kr59b 1/1 Running 0 68d redis-xcvrd-8lcrf 1/1 Running 0 68d prep-xcvrd-2hx2f 0/1 Completed 0 68d xcvrd-2vgps 1/1 Running 0 68d config-mgmt-ndtmp 1/1 Running 0 68d line-protection-hgvnh 1/1 Running 0 68d redis-cache-k22gt 1/1 Running 0 68d oc-terminal-device-p7bpc 1/1 Running 0 68d oc-interfaces-f9rcs 1/1 Running 0 68d oc-macsec-r8mxd 1/1 Running 0 68d collector-as-qmxqx 1/1 Running 0 68d collector-as-tai-xnmmg 1/1 Running 0 68d collector-pm-qnsns 1/1 Running 0 68d collector-inventory-6vm2t 1/1 Running 0 68d alarm-z96sp 1/1 Running 0 68d oc-platform-vp86h 1/1 Running 0 68d perfmon-4l67b 1/1 Running 0 68d north-netconf-bh6zj 1/1 Running 0 68d redis-ztp 1/1 Running 0 68d xlate-telemetry-5bz22 1/1 Running 0 68d north-gnmi-tnxk9 1/1 Running 0 68d tai-gearbox2-1-779cf9d6d-q2s2j 1/1 Running 1 (68d ago) 68d ztp 1/1 Running 0 68d tai-gearbox1-1-fdfc7d6cd-mtkhk 1/1 Running 1 (64d ago) 68d また、Goldstone OSやその派生OSにおいても、SONiCなどスイッチOSと同じようにONIE(Open Network Install Environment)というインストール環境を利用でき、OSのインストール等が容易になっています。 専用トランスポンダーかIP over DWDM構成か DCOというプラガブルモジュールを用いたIP over DWDM構成と専用トランスポンダーのホワイトボックス化を説明しました。さらに、市場には説明した2種類以外にも400G-ZR/ZR+等を用いつつスイッチ/ルーター機能を排することで専用トランスポンダーでありながら価格を低減したモデルなども存在しており、現在ではトランポンダー機能のあり方が多様になってきています。そのためネットワーク設計者は光伝送分野だけではなくIP/イーサネットレイヤーも含めた最適なアーキテクチャーを再検討する必要があります。 一般論としては下記のような比較ができますが、APNテストベッドでは複数のトランスポンダー構成を用いて、単純なコスト比較のみならず実際に検証・運用し、さまざまな観点から比較をしています。 専用トランスポンダー(ホワイトボックス含む) IP over DWDM(L2/L3機器と400G-ZR/ZR+などの組み合わせ) フォームファクター CFP2などの大型トランシーバーもしくはラインカード(内蔵型)が多い QSFPやOSFPなどの小型トランシーバー 制御API ホワイトボックス装置のTAIは光機能を詳細に抽象化可能 CMISは簡易な制御 伝送距離 長距離にも対応 メトロエリア(数100km以内)程度がメイン 光パラメーターの最適化 細かいチューニングが可能 固定的な部分が多い ライフサイクル スイッチ/ルーターに比べれば長期 上位レイヤーも含めた多機能が求められるためバージョンアップの必要可能性が比較的高い 運用体制 伝送レイヤーとL2/L3レイヤーの運用者の既存のすみ分けが流用可能 イーサネット/IPと伝送の両方の知見が必要なため新たな体制や運用ツールが必要な可能性がある 運用における知見 APNテストベッドの構成 APNテストベッドでは冒頭説明した通り、ROADMを用いてハブ&スポーク型とリング型を組み合わせたトポロジーのネットワークを構築し、そこにこれまで説明した多彩なトランスポンダーを接続しています。 過去には単一ベンダーで光伝送システムを構成するのが一般的だったため、このような異ベンダーのトランスポンダーからの光波長を区別するため「エイリアン波長」と呼んでいる場合もありますが、我々のAPNテストベッドでは一般的な仕様としています。 複数ベンダーシャーシ/トランシーバーの混在運用 我々のホワイトボックストランスポンダーの運用においては、そのオープン性を活かし、複数ベンダー製のシャーシに共通のOSを搭載させています。これまでに3社のハードウェアを導入してきましたが、いずれも大きなトラブルはなく、安定した運用が可能であることを確認しています。 一方で、トランシーバーに関してはベンダー間の相互接続性が課題となる場合もありました。 とくにCFP2形状のDCOについては、光パラメータの細かな調整が可能である一方、異なるベンダー間で接続できないケースが確認されました。実際に、全く通信ができない組み合わせも存在しており、注意が必要です。 これに対してIP over DWDM構成でよく用いられる400G-ZR/ZR+トランシーバーはCMISによる標準インターフェースの普及や仕様の成熟により、初期の一部例外を除いて現在ではほぼ全てのベンダー間で安定した動作が確認できています。 長距離伝送を可能とするDCOは、DSP(Digital Signal Processor)などの信号処理に必要な部品が短距離用トランシーバーよりも一般に高消費電力・高発熱・高体積となり、シャーシ毎に物理的な搭載制約を伴うことがあります。 使用する際は事前に筐体の制限を十分に確認し、実際に搭載して試験するとともに、搭載位置の設計を慎重に検討する必要があります。 以上を踏まえると、オープンな光伝送環境においても適切なトランシーバー選定とパラメータ調整、設計により十分な互換性と運用性を確保できることが示されており、マルチベンダー環境であってもオープンな光伝送の利点を享受することは現実的かつ有効な選択肢といえます。 gNMIの活用 APNテストベッドでは、トランスポンダーの状態監視や可視化にgNMIを活用しています。gNMIは、OpenConfig 4 が策定したオープンなネットワーク管理プロトコルであり、ベンダー依存のSNMPやCLIベースの取得手法に比べ、構造化されたデータを効率的かつリアルタイムに収集できる点が大きな特長です。詳しくは当ブログの以前の記事「 次世代の監視技術 - Telemetry技術のご紹介 - NTT docomo Business Engineers' Blog 」もご確認ください。 監視構成では、gNMIクライアントに gnmic を活用し、複数の機器からストリーム形式でOpenConfigベースのメトリクス(光パワーレベル、エラーレート、SNR:Signal Noise ratioなど)を秒単位で連続取得しています。gnmicはGoogle Cloud上のGKEにデプロイし、オンプレミスのネットワークから自社サービスであるFIC(Flexible InterConnect)を通してメトリクスを取得しています。取得データはPrometheus形式に変換することでGoogle Cloud Monitoringに連携、さらにPrometheus/Grafana連携によりダッシュボードでリアルタイム表示・履歴管理・アラート通知まで対応しています。 gNMIを活用した我々の監視構成では以下のようなメリットが生まれています。 機器追加時にはKubernetesのConfigMapであるYAMLファイルを1つ編集するだけで監視対象に追加可能 YAMLファイルはGitHub上で管理しておりそれを書き換えるだけで機器追加に対応できています。 光学パラメーターの劣化をリアルタイムに検知し、障害予兆に即応可能 ラベル付きの時系列データを標準的なフォーマットで取得できるため、既存のツールが活用しやすく、障害の分析や予測に役立てることができます。 エージェントレスで通信でき、ベンダー固有の監視ソフトが不要 まとめ 本記事では光伝送ネットワークのオープン化について概観し、特にIP over DWDM構成とホワイトボックストランスポンダーにおけるアーキテクチャーと実際の運用上の知見を紹介しました。次回以降は光波長自体を伝送するROADMによるネットワークについて紹介する予定です。 光の強度のみならず波としての性質(位相など)にも情報をのせて伝送する技術。デジタルコヒーレントはその信号情報処理に専用半導体(DSP:Digital Signal Proc­es­sor)を用いています。微細加工技術の進展により小型化・省電力化が進んでいます。 ↩ OpenROADM MSA(Multi-Source Agreement)は光伝送の特にROADM網において複数のベンダーで相互接続が可能とするため、物理的な光インターフェースの仕様やAPI、装置のアーキテクチャーを規定しています。詳しくは次回以降の連載や 公式Webサイト を参照ください。 ↩ OTN(Optical Transport Network)スイッチという光のフレーム情報でスイッチングする方式も存在します。 ↩ ネットワーク機器のAPIをマルチベンダーで共通的に提供できるようにすることを目的とした取り組み。 OpenConfig ↩
アバター
こんにちは、イノベーションセンターの二瓶・神田です。 ドコモグループ(NTTドコモ・NTTドコモビジネス)はゴールドスポンサーとして、2025年8月2日に開催された第3回 セキュリティ若手の会に協賛しました。 この記事では、スポンサーの立場から見た当日の様子やスポンサー講演の内容について紹介します。 はじめに:セキュリティ若手の会について 公式イベント開催記 当日の様子 LLMセキュリティの攻防入門 〜ガバナンスと脅威対策〜 オフェンシブセキュリティ入門 ~攻撃者目線でセキュリティ対策を考えよう~ NTTドコモビジネスの業務紹介 おわりに お知らせ 第4回 セキュリティ若手の会(LT&交流会) Tech Workshop はじめに:セキュリティ若手の会について 「 セキュリティ若手の会 」は、将来セキュリティエンジニアを目指す学生や、セキュリティ業務に携わる若手エンジニアのためのコミュニティとして2024年に設立されました。 15歳以上の学生から新卒3年目の社会人までが参加可能となっており、今回の第3回イベントを含めて、これまでに3回のオフラインイベントを企画、主催しています。 公式イベント開催記 このコミュニティの特徴の一つは、創設メンバー自身も若手エンジニアであることです。 当事者として抱える課題感や、課題解消に向けて自ら動く姿、その思いに強く共感し、ドコモグループは第3回のイベントスポンサーに名乗りをあげました。 1 スポンサーをするにあたってはドコモグループ採用担当とも連携し、イベント当日には採用担当も参加して広く事業内容やセキュリティエンジニアの採用についても紹介しました。 当日の様子 第3回イベントはそれまでの2回とは異なり、初めてワークショップ形式で開催されました。 ワークショップの概要や会場の様子などは公式の イベント開催記 にも掲載されているので、ここでは若手“ではない”社員の視点から見たワークショップの印象を紹介します。 LLMセキュリティの攻防入門 〜ガバナンスと脅威対策〜 1つめのコンテンツは、生成AI(LLM: Large Language Model)のセキュリティに関する講義とハンズオンがセットになったワークショップでした。 セキュリティ若手の会幹事のお二人が自ら講師を務め、生成AIの業務利用に潜む脅威について実体験を通じて学びました。 具体的な内容は参加者限りのため詳細は割愛しますが、講義パート、ハンズオンパートはいずれも「若手」の域を超えた内容でした。 事業会社に勤める立場から見ても、生成AIを使う全従業員にぜひ押さえておいてもらいたいポイントが随所に散りばめられていました。 特に利用規約・ガイドラインの読み解き方や生成AIを利用する際の情報漏洩リスクの実例は、事業会社でセキュリティポリシーを策定する立場の方々にとっても有益な情報が多かったです。 オフェンシブセキュリティ入門 ~攻撃者目線でセキュリティ対策を考えよう~ 2つめのコンテンツは、ドコモグループのグループ会社でもあるエヌ・エフ・ラボラトリーズ(以下、NFLabs.)によるハンズオンでした。 NFLabs.が開発した実践型サイバーセキュリティ学習プラットフォーム Purple Flair を用いて、攻撃者の立場でシステム攻略にチャレンジしました。 特に印象的だったのは、攻撃してみて終わりにせずに、そのあとに考察させるパートでした。 「攻撃者としての活動が防御側からはどう見えるのか」、「攻撃者として苦労したポイントがセキュリティ対策の観点でどういった意味を持つのか」。攻撃者の視点から一転して防御する側の視点に転換させる問いかけには考えさせられた参加者も多かったように見えました。 この視点の転換は効果的・効率的な防御の構築につながる重要な思考で、とてもよい気づきにつながっていると感じました。 NTTドコモビジネスの業務紹介 スポンサー講演パートでは、NTTドコモビジネス(旧:NTTコミュニケーションズ)におけるセキュリティエンジニアの働き方を紹介しました。 少し学生時代も振り返りながら、私(二瓶)が所属する「イノベーションセンター テクノロジー部門 Offensive Securityプロジェクト」での業務の概要、部署の説明や実際にどんな業務を担当しているのか、ステークホルダーとの調整に関する苦労話などを紹介しました。 熱心に聞き入る参加者も多く、アンケートでも「話が面白かった」「学生のうちから色々な技術で遊びたいと思った」などの意見をいただけて嬉しかったです。 当日には紹介しきれませんでしたが、NTTドコモビジネス エンジニアブログでは、今回紹介したオフェンシブセキュリティの業務以外にもセキュリティ業務やそこでの成果を情報発信しています。 毎年開催されている現場受け入れ型インターンシップの体験記も多数掲載されていますので、NTTドコモビジネスのセキュリティ業務に興味を持たれた方はそちらもぜひご覧ください。 (今年の体験記もこれから随時公開される予定です。お楽しみに。) おわりに ドコモグループ(NTTドコモ・NTTドコモビジネス)は、セキュリティ若手の会の活動に賛同し、ゴールドスポンサーとして第3回 セキュリティ若手の会の開催を支援しました。 これからも若手の活躍を応援し、ともにセキュリティコミュニティを盛り上げていきたいと考えています。 お知らせ 第4回 セキュリティ若手の会(LT&交流会) すでに募集が始まっており、締め切りは10月31日、開催日は11月16日です。 興味を持たれた方は参加を検討してみてはいかがでしょうか。 参加資格は、15歳以上の学生もしくは新卒1〜3年目の社会人です。 Tech Workshop ドコモグループでは、セキュリティ・ネットワーク・クラウドについてハンズオン形式で学ぶことができる1dayのワークショップ「Tech Workshop」を開催しており、現在11月開催分について参加者を募集しています。 セキュリティについては、今回2種類のワークショップが用意されています。 ドコモグループのセキュリティ業務とフィッシングサイトの仕組みを学ぶ (11月9日開催) Purple Flair で学ぶ実践型サイバーセキュリティ(11月15日開催) いずれも締め切りは10月19日です。 エントリーお待ちしています。 コミュニティ立ち上げにあたっての創設メンバーの思いは 公式ブログ でも紹介されているのでぜひそちらもご覧ください。 ↩
アバター
はじめに みなさん、こんにちは。イノベーションセンター IOWN推進室の工藤です。 IOWN推進室では、IOWN APNを体験できる基盤の整備を進めており、その構築や運用などを担当しています。 先日開催されたInterop Tokyo 2025(会場:幕張メッセ、会期:2025年6月11日〜13日)のNTT ドコモビジネスのブースでは、IOWNを利用した「ハイブリッドトラベル」をはじめとする未来の体験を展示し、多くの方にお立ち寄りいただきました。(ブースの展示内容については こちらのニュースリリース をご覧ください) 一方で、ブースでの展示とは別に、イベント全体の通信を支える大規模な実証実験ネットワーク「ShowNet」へIOWN APN技術を提供しておりました。 ShowNetでは、全国の放送局から送られる映像をIOWN APN経由で伝送するといった先進的な取り組みも行われました。この映像伝送のアーキテクチャについては、別チームが こちらの記事 で詳しく解説しています。 本記事では、その映像伝送をはじめとする数々の先進的な試みを根底から支えた「 IOWN APN 」そのものに焦点を当てます。 1.2Tbpsの大容量・低遅延・低ジッタの特徴を活かした多様なユースケースや、設計構築する上でのポイントやトラブル対応をご紹介します。 はじめに ShowNetとは IOWN APNの提供概要 IOWN APNを構成するネットワーク 多様なユースケースでの活用 Media over IP 大手IX・学術ネットワークとの接続 サービスの安定基盤としての活用 ブースでの活用 設計構築のポイントやトラブル対応 設計構築上でのポイント ポイント①:光の特性を考慮した波長設計 ポイント②: ROADMの操作性 ポイント③:ファイバー接続前の光の確認 トラブルシューティング事例 事例①:原因不明のCRCエラー 事例②:Hotstage期間中にOpticsが故障 まとめ ShowNetとは ShowNetとは、Interopの会期中だけ出現する、巨大な実証実験ネットワークです。ボランティアで集ったトップエンジニア達が、各社から提供された最新機器を相互接続し、実際に稼働させることで、この巨大なライブネットワークを構築・運用しています。 IOWN APNの提供概要 NTTドコモビジネスは今年、ShowNetの「External」と呼ばれる外部接続部分に「大容量・低遅延・低ジッタ」という特徴を持つIOWN APNを合計7波長(1.2Tbps)提供しました。 (ShowNet エクスターナル図 (PDF)) IOWN APNを構成するネットワーク 今年のIOWN APNは、都内にハブ拠点を設けて、東は千葉のビルを経由し幕張メッセへ、西は大阪のビルへと接続する広大なネットワークを構築しました。さらに各拠点から、五反田、お台場、QUINTBRIDGEなど、デモやサービス提供に必要な拠点へと延伸しています。 この構成における主要区間の実測遅延値は、東京–大阪間で片道4.2ms、東京–幕張メッセ間で片道0.3msでした。 また、このネットワークでは、NTTドコモビジネスの商用サービス「APN専用線プラン powered by IOWN」を、「東京–大阪間の接続」と「千葉のビルにおけるOCNとの直接接続回線」で実際に活用しています。 APN-T (Open APN Transceiver): 波長パスの終端点で、波長の送受信を行うインターフェイスを表す。 APN-G (Open APN Gateway): APN-Tの光を伝送網へ導入するためのゲートウェイ。APN-Tの制御、APN-Tの信号の多重/分離・折り返し・add/dropなどの機能を有する。 APN-I (Open APN Interchange): 光パスの途中で波長スイッチングのための中継装置。増幅なども行う。 DWDM (Dense Wavelength Division Multiplexing, 高密度波長分割多重) 区間: APN-GやAPN-Iといった装置間を接続し、1本の光ファイバーで複数の異なる波長の光を同時に送受信する伝送路。 なお、APN-T、APN-G、APN-Iに関して詳しくは以下をご参照ください。 参照先:IOWN Global Forumにおけるオープンオールフォトニクス・ネットワークの検討 多様なユースケースでの活用 提供したIOWN APNは、以下のような多様なユースケースで活用されました。 Media over IP 東京と大阪の放送局が映像・音声リソースをIP網で共有し合う「ShowNet Media-X」という、放送業界の未来に向けた試みが実施されました。IOWN APNは、東京と大阪の合計3つの放送局の各スタジオから幕張メッセまでを結ぶ高品質な長距離伝送路を提供し、この壮大なリモートプロダクションの実現に貢献しました。 ( ShowNet Media over IP特別企画 より引用) 大手IX・学術ネットワークとの接続 ShowNetにおいてさまざまな実証実験を行うために、ShowNetを日本のインターネット基盤そのものと接続する必要がありました。IOWN APNは、このShowNetの生命線ともいえる接続路として、複数のIX (Internet eXchange) と接続し、安定した通信を提供しました。 サービスの安定基盤としての活用 ShowNetでは、NTTドコモビジネスが商用提供する主要サービスの通信基盤としてもIOWN APNを活用しました。 例えば、企業のクラウド接続を担う「Flexible InterConnect」、リアルタイム性が求められる音声コミュニケーションを支える「docomo business RINK」、そして社会インフラであるインターネット接続サービス「OCN」など、特性の異なる複数のサービスの通信をIOWN APN上で同時に提供しました。これにより、IOWN APNが、多様な要件を持つサービスを安定して支えられることを実証しました。 ブースでの活用 IOWN APNは、NTTドコモビジネスのブース展示にも活用しました。大阪・お台場・五反田といった遠隔拠点と幕張メッセをAPNにて結ぶことでデモンストレーションを実現しました。 設計構築のポイントやトラブル対応 設計構築上でのポイント ポイント①:光の特性を考慮した波長設計 光ファイバーには種類があり、種類によって特性が異なります。例えば、日本で従来から長距離光伝送で多く使われているDSFという光ファイバーは、1550nm付近の波長で分散がゼロになる特性があります。今回は、四光波混合といった非線形効果の影響を減らすため、この波長帯を避けるといった考慮も行いました。 その他、いくつか設計にあたり検討すべきポイントがあります。OSNR(光信号対雑音比)を事前に測定して通信速度と変調方式を適切な設定にすることや、適切なグリッド幅に設定することなどが挙げられます。 ポイント②: ROADMの操作性 APN-I/GにあたるROADM装置はOpenROADMという標準規格に準拠したAPIへ対応しはじめていますが、現時点では多くの場合ベンダー固有のコントローラからの操作が前提となります。今回もベンダー製のコントローラを用いて、APN装置を操作し、各拠点間の光パスを設定しました。 ポイント③:ファイバー接続前の光の確認 伝送装置の接続においても、通常の光ファイバーを用いるケースと同様に、2芯の光ファイバーの送信と受信を対向拠点の機器同士で確認することが重要となります。今回は事前に片側を接続し、パワーメータなどで光レベルを確認した上で接続しました。 また、光は弱すぎても届きませんが、逆に強すぎても受信側の装置が故障したり、通信が不安定になったりします。実測値が想定より強い場合は、アッテネータ(減衰器)を入れて適切な光量に調整します。 トラブルシューティング事例 InteropへのIOWN APN提供は今年で3年目になり、今回はついに設定関連のトラブル"ゼロ"を達成しました。 しかし、物理的な障害は決してゼロにはなりません。実際に発生した、代表的な2つの事例をご紹介します。 事例①:原因不明のCRCエラー 特定の接続先から「CRCエラーが散発的に発生する」という申告がありました。100G-LR4にて接続していましたが、IOWN APNのクライアントとして低遅延を追求するため、意図的にFEC (Forward Error Correction、前方誤り訂正) をoffの状態で運用していました。設定上は問題が見当たらなかったため、光コネクタの微細な汚れの可能性を疑い、改めて関連する接続箇所を全て専用クリーナーで再清掃しました。汚れによる僅かな信号品質(BER)の劣化が原因と推測され、物理層の丁寧なクリーニングが重要であることを再認識する事例となりました。 事例②:Hotstage期間中にOpticsが故障 本番稼働前の準備期間であるHotstage期間中に、APN-Tに搭載していたCFP2-DCOというOptics(光送受信モジュール)が故障する事態が発生しました。運悪く担当者が現地にいないタイミングだったため、予備物品を持って幕張メッセまで駆けつけて交換対応することで、ShowNetの会期に影響を与えることなく乗り切りました。 まとめ 今年のInterop Tokyo 2025において、IOWN APNはShowNetの外部接続回線として、合計7波長・1.2Tbpsを提供し、多様なユースケースを安定的に支えました。 放送局間の映像伝送(Media over IP)、主要IXや学術ネットワークとの接続、さらにはNTTドコモビジネスの商用サービスやブースの遠隔デモまで、「大容量・低遅延・低ジッタ」という特性を活かした幅広い活用が実現されています。「APN専用線プラン」といった商用サービスが出ていることも含めて、IOWN APNがコンセプトの段階を越え、お客さまへ提供できる実用的な技術であることを証明する重要な機会となりました。
アバター
NTT ドコモビジネスではエンジニアコミュニティイベント、 Tech-Night/Tech-Midnight を定期的に開催しています。 普段はオンラインで実施していましたが、今回は数年ぶりにオフライン会場を用意し、オフラインとオンラインのハイブリッド形式で実施しました。発表を会場とオンライン会議双方へ配信した裏話と併せて、今回の発表内容について紹介します。 はじめに Tech-Night とは 会場の様子 Tech-Night の発表内容 Heuristic な Contest 参加のすゝめ (と生成AIでのアプローチ) オンラインカジノの闇 (の入口) Socks Proxy がつらい チーム開発におけるタスク管理術 Vol.1 - 臨時タスクの捌き方 JANOG レポート 〜 AI を流行らせたい〜 toby とチャッピーの夏の冒険 配信の工夫 おわりに NTT ドコモビジネスではメンバーを募集しています はじめに こんにちは。 Smart Data Platform (SDPF) クラウド/サーバー 仮想サーバーチームの杉浦 ( @Kumassy_ ) です。 普段は VM のハイパーバイザや OpenStack の開発・検証をしています。 この記事では、 2025 年 9 月 3 日に開催された Tech-Night の様子をご紹介します。 Tech-Night とは Tech-Night は NTT ドコモビジネス内の有志で開催している発表会です。 業務や趣味で技術的に挑戦したことやサービスの裏側について部署の垣根を越えて共有することや、アウトプットの場、対外発表の練習などを目的としています。 今回はインターンシップ期間中の開催であり、多くの人が出社していると想定し、オフィスのイベントスペースを貸し切ってオフラインで発表してもらうようにしました。 2018 年 12 月の第 1 回目の開催の様子は こちら から、 2022 年 12 月開催の様子は こちら から確認できます。 会場の様子 今回は初回と同じく大手町プレイスの ガレージ と呼ばれる共用スペースのひな壇の場所を使いました。オーディエンスとの距離が近く発表もしやすかったのではないでしょうか。 これは設営中の私です。 Tech-Night の発表内容 今回の Tech-Night では次の 6 つのテーマの発表がありました。 Heuristic な Contest 参加のすゝめ (と生成AIでのアプローチ) オンラインカジノの闇 (の入口) Socks Proxy がつらい チーム開発におけるタスク管理術 Vol.1 - 臨時タスクの捌き方 JANOG レポート 〜 AI を流行らせたい〜 toby とチャッピーの夏の冒険 それぞれの発表を文字起こし形式で紹介します。 Heuristic な Contest 参加のすゝめ (と生成AIでのアプローチ) みなさんこんにちは。堀岡です。私は今年入社の新入社員で、 SDPFクラウド/サーバー の NW オーケストレータ ESI (Elastic Service Infrastructure) の開発をしています。 今回は Heuristic Contest の紹介と、コンテストで実際に AI を使ってみて感じたことをお話したいと思います。 AtCoder では、お馴染みの Algorithm コンテストの他に、 Heuristic コンテストもあります。 Heuristic コンテストは厳密解を得るのが難しい問題について、スコアを最大化できるようなコードを考え、得られたスコアを競うコンテストです。 コンテストの期間は 4 時間から長いもので 10 日間と長丁場であり、生成 AI の利用もできる点が特徴的です。 参加者が Algorithm コンテストと比べて少ないので、取り組んでみるとそこそこの順位が取れたりレートが上がりやすかったりでハマりやすいかもしれません。 今回は直近の Heuristic コンテストで出題された次の問題を扱います。この問題は、 10 台のロボットを操作して床にワックスをかけるという非常に理解が簡単な問題です。 10 個のスイッチがあり、それぞれのスイッチに各ロボットの移動方向を割り当てられます。なるべく操作回数を少なくしつつ、全てのマスにワックスをかけることを考えましょう。 今回は生成 AI を活用してコードを書きましたが、いくつか工夫が必要でした。 生成 AI を使うポイントは解法を構造分割することです。 単に問題を丸投げするだけだと、高確率で動作しないコードが生成されました。 そこで、今回の場合ではロボットの現在位置の管理、ボタンを押したあとのロボットの位置計算、壁がある場所を取得する関数、といったように解法を関数の形でいくつかの構造に分割してあげれば、生成 AI にきちんと動作するコードを作ってもらえます。 これらをどう繋ぎ合わせるかは使う側(人間)の腕前と言えるでしょう。 また、生成 AI に上位者のコードを解説してもらえるようにもなり、初めて Heuristic コンテストに挑む上ではこれが一番うれしいかもしれません。 オンラインカジノの闇 (の入口) みなさんこんにちは。神田です。私は Network Analytics for Security PJ リーダーとして、攻撃インフラの解明・撲滅や高度セキュリティアナリスト人材の育成・事業活動の両立に取り組んでいます。 今日の発表では、ドメインがドロップキャッチされてオンラインカジノ誘導サイトに転用された事例についてお話できればと思います。 ドロップキャッチとは、ドメイン名が更新されずに一定期間登録できない状態を経た後、再登録が可能になる瞬間を狙って目的のドメイン名を取得しようとする行為を指します 1 。 昨年の 11 月くらいにとある国産ソフトウェアの公式サイトで使用されていたドメイン名が契約更新されずに失効し、その後 ドロップキャッチによって第三者に取得されるという事件が発生しました。 このドメインは、怪しいサイトへの誘導に利用されており、公式から注意喚起が行われる事態となりました。 この件についてセキュリティアナリストの視点からもう少し踏み込んで調べてみました。 whois 情報を確認すると、電話番号が電話番号としてありえない文字列になっていて不審に思ったほか、名前が本当に実在する人のものなのか怪しいと感じました。 whois 情報の中でも、特にメールアドレスに着目して調査しました。 メールアドレスのウェブサイトを見に行くと、一見よくあるフリーメールサービスのようにみえますが、オンラインカジノ関連の利用者の声が数多く掲載されて、怪しい感じがしました。 次に、このメールアドレスを使って登録されているドメイン名の一覧を見てみました。 2024 年 12 月当時は全部で 55 ドメイン見つかり、観光や映画等で一時的に使われいたドメインがドロップキャッチされていることがわかりました。 これらのドメインはオンラインカジノへの誘導サイトに使われていました。 ドメインがドロップキャッチされてオンラインカジノの誘導に使われているといった話はときどきニュースになりますが、ご覧の通り氷山の一角で、みなさんが思っている以上にオンラインカジノ誘導キャンペーンが展開されているようです。 これは闇の「ほんの入口」に過ぎない..。 Socks Proxy がつらい こんにちは。松下です。 普段私は、 Smart Data Platform (SDPF) クラウド/サーバー を開発している仮想サーバーチームにて、 OpenStack や GitHub Actions を使った CI/CD の開発を担当しています。 このセッションでは Socks Proxy の自作経緯や、 AI を使って調べた Socks5 プロトコルの詳細を話したあと、自作ツール proxs を紹介できればと思います。 SDPF クラウド/サーバーは複数リージョン展開しており、商用リージョンは現時点で展開しているもので 8 拠点、他に開発用やステージング環境などを含めると 10 拠点以上に及びます。 各リージョンには、リージョン内に閉じた Web ページ等があります。 SDPF の開発ではそれらにアクセスする必要がありますが、これらは前述するように各拠点の閉じたネットワーク配下にあるため、素直にアクセスできません。 そのため、我々は各拠点への SSH トンネルを利用して各拠点のサーバーにアクセスしています。 これらの Web ページに Web ブラウザからアクセスしたいときには SSH Client による Dynamic Port Forwarding とブラウザの Proxy 機能または、 Web ブラウザの拡張機能を利用することでアクセス可能です。 しかし、日々の業務ではさまざまな拠点にアクセスすることがあり、そのたびに SSH トンネルを貼ることは煩雑な作業でありストレスです。 このような煩雑さは Web ブラウザの拡張機能によって Socks Proxy 先を自動的に変更することで低減できますが、拠点内部からのみアクセス可能としてるサイトの閲覧にブラウザの拡張機能を利用することは、セキュリティの観点から可能な限り避けたいものです。 そこで、セキュリティを担保しつつ、複数拠点の Web ページに簡便にアクセス可能とする仕組みを作ることにしました。 具体的には、以下 3 点の特徴を持ちます。 Web ブラウザ視点では、 1 つの Socks Proxy 設定のみを必要とする(= Web ブラウザの標準機能だけで利用可能とする) 宛先ドメインに応じて、 Socks Proxy を自動で切り替える HTTPS リクエストの TLS は解かない これらの特徴には、 2 つの矛盾があります。 1 つめは、 Web ブラウザ視点では 1 つの Socks Proxy 設定のみを必要とすることと、宛先ドメインに応じて Socks Proxy を切り替えることです。 Web ブラウザの Socks Proxy 設定は 1 つしか設定できないため、複数の Socks Proxy を利用することは通常できません。 2 つめは、 HTTPS 通信の TLS を解かないことと、宛先ドメインに応じて Socks Proxy を切り替えることです。 通常の HTTPS 通信では HTTP のパケットは TLS で暗号化されており、 HTTPS パケットがどのドメインを宛先としているかは (HTTP Header に格納されており TLS で) 暗号化されているため、 Web ブラウザと宛先サーバーの間で見ることができません。 これらの矛盾を解消しながら実装したものが、「proxs」です。 proxs は、これらの矛盾を以下のように解消し実装します。 まず、 proxs は自身を Socks Proxy として Web ブラウザに対して振る舞います。 したがって、 Web ブラウザはすべてのリクエストを proxs に送信します。 proxs は、受け取ったリクエストの宛先ドメインに応じて、適宜 Dynamic Port Forwarding が有効な SSH 接続を確立し、受取ったリクエストを転送します。 次に、 TLS を解かずに宛先ドメインを知るために、 Web ブラウザから発行される Socks5 の CONNECT コマンドを利用します。 Socks を使った通信をする際、 Web ブラウザは HTTPS リクエストを Socks Proxy へ送信する前に、 Socks Proxy に対して CONNECT コマンドを送信します。 この、 CONNECT コマンドは暗号化されておらず、また、 Proxy させたい HTTPS リクエストの宛先ドメインを含んでいます。 したがって、 proxs は CONNECT コマンドを受け取った際に、宛先ドメインを知ることができ、宛先ドメインに応じて Socks Proxy を切り替えることができます。 Socks5 の CONNECT コマンドは暗号化されていませんが、後続の HTTPS リクエストは通常通り TLS で暗号化されているため、 proxs が知ることのできる情報は宛先ドメインのみとなります。 これは余談ですが、 Socks5 のプロトコルの理解は、 ChatGPT に mermaid.js でシーケンス図を書いてもらうことで、効率的に理解できました。 LLM を利用したおすすめの勉強法です。 実装面では、 RFC1928 や go-socks5 を参考にすることで、比較的容易に実装できました。 Tech-Night では、各拠点の Web サーバーに対して Socks Proxy を手動で繋ぎ変えることなく接続可能であることをデモンストレーションしたり proxs の性能評価についても発表しました。 もし、同様の課題を抱えている方がいれば、ぜひ proxs を試してみてください。 チーム開発におけるタスク管理術 Vol.1 - 臨時タスクの捌き方 初めて Tech-Night に登壇させていただきます。三輪です。 私は Smart Data Platform (SDPF) クラウド/サーバー にて、オペレーターのための運用ツールやお客さまによるサービス申し込みを円滑化するための Web アプリケーションを開発しています。 今日はウェブアプリ開発チームの運用方法や臨時タスクの管理におけるポイントを話します。みなさんのチーム開発のヒントになれば幸いです。 スプリントのはじめに計画した開発タスクを予定通り消化できなかった経験はありませんか?いろいろな原因が考えられますが、最大の原因は、スプリントの途中に臨時対応が必要なタスクの発生することだと考えています。 例えば、セキュリティ脆弱性対応タスクや他チームからの問い合わせは事前に発生することが予測できません。 また、このような臨時タスクは性質が違い、コンテキストの切り替えコストがかかったり認知負荷が増大してしまいます。 臨時タスクへの対応として、私のチームでは臨時タスクを「見える化」するよう工夫しました。具体的には臨時タスクも開発タスクと同様にチケットを作成し、スクラムボードに乗せることで今チームが抱えている仕事量を把握できるようになりました。 臨時タスクの量が「見える化」できていると、毎回どの程度臨時タスクが発生するか予測できるようになります。スプリント計画時には臨時タスク用のバッファを持たせておくことにしました。 臨時タスクは必ずしもすぐに取り組む必要はないので、優先度が低いタスクは次のスプリントに回すこともできます。 臨時タスクのコンテキストスイッチ、認知負荷の問題については、臨時タスクを当番制にすることで解決しました。こうすることで、チーム全体の認知負荷を下げ、当番以外のメンバは開発に集中できます。 スプリント振り返りには、臨時タスクの分量や性質などに着目して将来予測に役立てたり、共通パターンを見出して手順化、自動化、ドキュメント整備などを検討して負荷を軽減させることも重要です。 JANOG レポート 〜 AI を流行らせたい〜 みなさんこんにちは。鈴木です。 私は今年入社の新入社員で、 SDPFクラウド/サーバー のファイアウォールやロードバランサ開発をしています。 JANOG 56 に参加した経験をもとに、 JANOG での AI 活用事例を複数紹介し、MCP(Model Context Protocol)の技術概要と社内での検討状況について共有します。 JANOG 56 のタイムテーブルをみてみると、全体の 1/3 が AI 関連で JANOG 53 では 1 件だったことを踏まえると、 AI の流行をみてとれます。 これらの発表では、自然言語をもとにネットワーク機器のコマンドを生成したり、 MCP サーバを実装してネットワークの操作やチケットの参照、アラート対応を自動化したりするなど、 AI を積極的に利用した事例が報告されていました。 一方で、私が普段業務で使っている AI はチャットボットやコーディングエージェント、チャットツールの要約機能に留まっており、 JANOG の事例のようにもっと踏み込んで AI を活用したいです。 そのためには MCP の理解が必要です。 MCP (Model Context Protocol) は、 LLM (Large Language Model) が外部データやツールにアクセスするためのオープンなプロトコルです。このプロトコルは、 Claude を開発した Anthropic によって提唱され、標準化が進められています。 MCP サーバーは、 LLM に対して「どのようなデータを持っているか」「どのようなツールが利用可能か」を教える役割を果たします。ユーザーがリクエストを送信すると、 LLM は MCP サーバーを通じて適切なツールやデータを選択し、そのレスポンスを基に最終的な回答を生成します。 AI エージェントの開発には、 MCP サーバ、アプリケーション、システムプロンプトの実装が必要です。 多くの部分は公式の SDK や OSS を活用すれば簡単に実装できるので、システムプロンプトの工夫が重要です。 具体的には、システムプロンプトを通じて AI をエージェント化し、タスクの消化方法やツールの呼び出しルールを教える必要があります。例えば、チケット管理エージェントの場合、「最後に必ずチケットを作成する」といった指示を与えることができます。また、 Junos の MCP サーバでは「コミット前に必ず人間の確認を取る」といったルールを設定することも可能です。 私のチームでは MCP サーバの導入を進めています。さらに詳しい技術的な詳細や、 AI の API の申請の苦労話など、続編発表ができればと考えています。 toby とチャッピーの夏の冒険 今回の Tech-Night の大トリを飾る toby です。 私は SDPFクラウド/サーバー の開発責任者を担当しており、 NTT ドコモビジネスの エバンジェリスト としても活動しています。 今日は toby とチャッピーの夏の冒険と題して、チャッピー (ChatGPT) と相談して設計・実装したシークレットマネージャーについて説明し、 AI を活用するうえで感じたことを発表できればと思います。 最近、 AI によってコンピュータサイエンスのあり方が変わり、パラダイムシフトが起きそうだなぁと感じています。 今年の夏休みに、 LLM を利用した実装を自ら経験してみることも兼ねて、 LLM (チャッピー) と一緒にシークレットマネージャーを作ってみることにしました。 夏休みの冒険として、チャッピーとはスマートフォンだけで会話はするというチャレンジをしてみました。 シークレットマネージャーは Go 言語で実装され、最終的には 100 ファイル、 13k 行のコードができました。 セキュリティ設計では、 SSH 公開鍵方式や JWT を活用し、 AES-256-GCM で暗号化を強化しました。 鍵管理には KMS を用い、定期的なキーのローテーションや監査ログを実装し、マスターキーはシステム内に保持しないことでセキュリティを強化しました。 AI 活用して便利だと感じたことはいくつかあります。 これまで手作業で行っていた実行環境のセットアップや、細かい仕様の実装、シェルスクリプトのような簡易ツールの作成など、これまで面倒で時間がかかっていた作業が AI によって大幅に簡単になりました。 また、 AI を使うことで自分のユースケースに合ったものを作られることが魅力に感じました。 今回のプロジェクトでは、 SSH 署名を活用した認証システムが必要でしたが、規模感は我々のユースケースにあう程度のスケーラビリティがあれば十分です。 既存の OSS や SaaS を使うよりも、自分たちが本当に欲しいものだけを作れるという点が大きな魅力だと感じました。 AI を使うことで設計の自由度が増し、どこでも作業ができるようになった点も印象的です。 例えば、露天風呂に入りながらでも設計を進めたり、スマートフォンを使ってアイデアを記録したりと、これまでの制約を超えた働き方が可能になりました。 これにより、思いついたアイデアをすぐに形にでき、開発のスピード感が大幅に向上しました。 しかし、 AI を活用する中で感じた課題もあります。 特に、大規模なコードベースにおいては、全体の統一感を保つことが難しいと感じました。 AI が生成するコードは便利ですが、設計者が一貫した指針を持って指示を出さなければ、プロジェクト全体がバラバラになってしまう可能性があります。 また、 AI に頼りすぎることで、開発者自身のモチベーションやコードに対する愛着が薄れるという懸念もあります。 自分でアルゴリズムを考えたり、コードに工夫を凝らす楽しさが減ることで、開発への情熱を維持するのが難しくなるかもしれません。 それでも、 AI は開発の効率化や新しい働き方の可能性を広げる強力なツールであることは間違いありません。 LLM の登場によりソフトウェア開発の変革を体感するとともに、二人三脚で歩むんだなと実感したチャッピーとの素晴らしい冒険でした。 まだこっそりと開発を続けています。 配信の工夫 最後に、司会者の杉浦から今回の Tech-Night の裏話をお話します。 今回の Tech-Night はハイブリッド開催としたかったので、オフライン会場とオンライン両方にプレゼンテーションを配信する必要がありました。また、発表者がオフライン会場からだけでなく、オンラインミーティングからもプレゼンテーションできるように工夫しました。 いろいろと配信の方式を検討しましたが、最終的に下記のような方式に落ち着きました。 発表者は各自の PC から Teams 会議に入り、画面共有をして発表してもらいます。これでオンライン参加者に PC の画面と音声を届けることができます。 会場の音声と映像を届けるために配信用 PC を別で用意しました。 この PC を Teams 会議に発表者として参加させ、プロジェクターの HDMI ケーブルを接続して会議画面を会場に投影しました。 ちなみに、発表者の PC で音声を共有した場合でも、 Teams と HDMI を経由して音声を会場に流すことができます。 発表者の音声を Teams 会議に配信するため、 MacBook の内蔵マイクを使用することにしました。 事前の検証で、 MacBook の内蔵マイクでも十分な音声品質が得られることを確認できたためです。 この方式では発表者の PC に HDMI ケーブルを接しないため、接続トラブルを回避できます。 Teams 会議であれば普段の業務で使い慣れているうえ、事前に画面共有の準備もしておけるのでスムーズに進行できました。 オンラインからプレゼンテーションする場合でも、配信用 PC の設定を変えることなくそのまま続行できるのも利点です。 事前の想定よりも発表者の数が多くタイトなスケジュールでしたが、会場の皆さんのご協力もあり、概ねスケジュール通り進行できました。 発表者の皆さんも素敵なお話をありがとうございました。 おわりに 本記事では NTT ドコモビジネスのエンジニアコミュニティのイベントである Tech-Night を紹介しました。 これまで紹介したように、スクラムの工夫からセキュリティ、AI の活用事例など本当にいろんなテーマの話を聞くことができ、非常に好評なイベントとなっています。 さらに NTT ドコモビジネス ではお昼の勉強会の TechLunch や NTT Tech Conference のほか、一般向けの NTT Com Open TechLunch も開催しています。 このようなイベントの開催は、知識・情報の共有だけでなく、さらにアウトプットを通した発表者個人の知識・技術の向上やエンジニア組織としての一体感の醸成にも繋がっています。 今後もこの取り組みを続けるとともに、輪を広げ、より活力のある組織へ成長させていきたいと考えていますので、共感できる方はぜひ! NTT ドコモビジネスではメンバーを募集しています SDPF クラウドでは現在、学生の皆さん向けに Tech Workshop イベントへの参加を募集しております。 SDPF クラウドのエンジニアと参加者の皆さんでチームを組み、モブプログラミングを行います。 今回発表してくれたエンジニアも一部参加予定です。 発表内容についてより詳しく聞けるかも...? 申し込み期限は 2025/10/19(日)23:59 までですので、お早めにお申し込みください! information.nttdocomo-fresh.jp そのほか、 NTT ドコモビジネスでは新卒採用や経験者採用、障がい者採用も行ってます! 新卒採用 NTT ドコモのサイトに移動します( NTT ドコモビジネスは NTT ドコモグループの一員として新卒採用をしています) 経験者採用 障がい者採用 https://www.nic.ad.jp/ja/basics/terms/dropcatch.html ↩
アバター
はじめに こんにちは、NTTドコモビジネスの 現場受け入れ型インターンシップ で「脅威インテリジェンスを生成・活用するセキュリティエンジニア/アナリスト」として参加させていただきました、大学院1年生の脇本です。 このたびインターンシップを通してさまざまな経験をさせていただきましたので、その中でも特に印象的であった「Telegramチャンネルおよびグループの調査・分析」という内容を本記事で紹介させていただきたいと思います。 はじめに 参加したチーム インターンシップ参加経緯 インターンシップ概要 Telegramを使った犯罪者コミュニティの調査/分析 前提 そもそも、Telegramとは? 調査/分析したTelegramチャンネル、グループの概要 調査/分析結果 調査/分析過程 1. グループ名やグループの投稿で頻繁に目にする単語の調査/分析 2. 販売されているクレジットカード情報入手手口の調査/分析 3. 他の投稿に関する調査 4. グループの概要欄についての分析 5. グループ内でのAの立場に関する調査/分析 6. Aの行っている"ビジネス"に関する調査/分析 インターンシップを通して学んだこと・感じたこと おわりに 参加したチーム 私がインターンシップ中に参加させていただいたのは「 Network Analytics for Security (通称: NA4Sec )」と呼ばれるプロジェクトチームでした。「NTTはインターネットを安心、安全にする社会的責務がある」を理念に攻撃インフラの解明や撲滅に向けて活動しているチームです。 過去には「Botconf 2025」というフランスのセキュリティカンファレンスなどでも登壇されている、すごいチームです。 1 インターンシップ参加経緯 私は大学でセキュリティを専攻するよりも前から世界史、その中でも欧州史に強い興味を持っていました。とはいえ特に興味のある時代は現代とは少し遠い(中世盛紀/後期)のですが、歴史とは連続性を持っておりますので他の時代についても興味があり、その延長として現代の世界情勢にも関心がありました。そのため以前から、世界情勢と直結しているともいえる脅威インテリジェンス分野には非常に強く惹かれていました。 そして去年、インターンシップを経験された方からNTTドコモビジネスのインターンシップをご紹介いただき、過去のインターンシップ体験記なども見て、参加を希望しました。 インターンシップ概要 インターンシップは2週間かけておこなわれ、1週目と2週目では異なる活動に取り組みました。 1週目: Cobalt Strike の調査/分析・ハンズオン 2 フィッシングサイトに関する調査/分析・ハンズオン 3 2週目: Telegram を使った犯罪者コミュニティの調査/分析 本記事では1週目の内容を割愛し、2週目に行った[ Telegram を使った犯罪者コミュニティの調査/分析 ]フェーズのうち特に印象的であったグループに関するものについて紹介させていただきます。 Telegramを使った犯罪者コミュニティの調査/分析 前提 今回のインターンシップでは、 Telegram上で活動している犯罪者とは交流しない との前提の下で調査および分析に取り組みました。 そもそも、Telegramとは? 本記事を見ている方の中には、そもそもTelegramという単語に聞き覚えのない方や、ニュースなどで耳にしてなんとなく怖いツールというイメージを抱いている方もいらっしゃると思います。 TelegramとはLINEやWhatsApp、Messengerなどと同様の機能を提供するメッセンジャーアプリです。そのためTelegram自体に問題があるわけではありません。 ただし以下に挙げるような特徴を有していることから、サイバー攻撃者や詐欺師等の犯罪者による悪用が確認されているのが実情です。 シークレットチャットと呼ばれる機能を使用することで利用者間通信が高い機密性と秘匿性を有する 無料で利用可能 またTelegramには個人間チャットのほかにグループチャットやチャンネルと呼ばれる機能があります。 グループチャットは最大20万人が加入できるチャット機能で、メッセージやメンバーを外部から閲覧可能なパブリックグループと、外部からは秘匿されたプライベートグループの2種類が存在します。 チャンネルは作成者と管理者のみが一方的にコンテンツを投稿できる機能で、他のユーザはフォローまたはフォロー解除ができます。こちらもグループと同様にパブリックチャンネルとプライベートチャンネルが存在します。 今回のインターンシップでは主に、パブリックグループやパブリックチャンネルについて調査/分析しました。 調査/分析したTelegramチャンネル、グループの概要 今回のインターンシップで調査/分析したTelegramチャンネルやグループは以下の3つです。 窃取された認証情報を販売しているチャンネル 犯罪系の雑談グループ 何らかの犯罪に関連するグループ 「 何らかの犯罪に関連するグループ 」について情報が曖昧なのは、私がこのグループの調査/分析を開始した際にメンターの方から渡していただいた情報がこのくらいの粒度であったためです。今後本記事ではこのグループに関する調査/分析の結果や過程について掘り下げていきます。 調査/分析結果 グループの調査/分析過程を紹介するより先に、調査/分析によって判明した結論を示します。 グループでは特定の言語が使用されている グループはクレジットカード情報を窃取している人たちが交流するためのもの 販売されているクレジットカード情報の一部はフィッシングによって窃取されたもの グループ参加者の中には他の犯罪を行っている人もいる グループの作成者、またはそれに準ずるグループの中核を担う人物(以下Aと呼称)が存在する Aはクレジットカード情報に関する独自の犯罪ビジネスを複数展開している 調査/分析過程 調査開始時に「何らかの犯罪に関連するグループチャット」のリンクを渡され、自由に調査/分析しました。 行った分析の多くは、投稿に使用されている隠語との戦いでした。グループ内では主に日本語以外の特定言語でやり取りが行われていたため、隠語の調査には骨が折れました。 調査/分析の過程は以下の通りです。 1. グループ名やグループの投稿で頻繁に目にする単語の調査/分析 初めに目を付けたのは、グループ名の一部で使用されており、さまざまな人の投稿でも頻出していたとある単語です。調査の結果、この語は クレジットカード情報 を指す隠語であることが分かりました。また投稿内容の一部には販売を示唆するものもあり、はじめクレジットカード情報の販売が行われているグループではないかと推測しました。 2. 販売されているクレジットカード情報入手手口の調査/分析 クレジットカード情報の販売を示唆する投稿をより詳しく調査したところ、 フィッシング を意味する隠語がいくつも確認されました。そのため、販売されているクレジットカード情報はフィッシングによって入手されたものだと分析しました。なお投稿について日本のクレジットカード情報販売を示唆するようなものも複数見つかりました。 3. 他の投稿に関する調査 グループに投稿されている他の投稿について調査したところ、クレジットカード情報の販売以外に関する投稿も複数確認されました。中には高収入を謳う謎の仕事の募集や、暗号通貨等の投資への誘導、メールアドレスやパスワードに関するファイル名を冠した謎のツールを配布しているものもありました。 この点や、グループ名に何かの交流を意味するような語が使用されていた点などから、このグループはただのクレジットカード情報販売グループではなく、 クレジットカード情報に関する犯罪者たちの交流場 としての役割を果たしていると分析しました。 4. グループの概要欄についての分析 次にグループの概要欄を確認したところ、概要欄には2種類の非常に類似したURLが貼り付けられていました。 URLの片方はグループのリンクで、もう片方はとあるユーザAが運営しているチャンネルのリンクでした。 そこで私はグループに関する調査/分析を終了し、「A」と「Aのチャンネル」の調査/分析を始めました。 5. グループ内でのAの立場に関する調査/分析 Aのチャンネルに関する調査から、グループとAには以下の類似性が確認できました。 AのチャンネルのURLとグループのURLが酷似している Aの名前の一部がグループ名の一部と重複している グループ内でのAの投稿の一部がピン止めされており、他の人物の投稿は1つもピン止めされていない Aのチャンネルは過去に名称を変更しており、変更前に使用されていた名称の一部がグループの説明にも使用されている 以上の点から、 Aはグループの設立者 か、そうでなくとも グループの中核を担っている人物 と分析しました。 6. Aの行っている"ビジネス"に関する調査/分析 Aはチャンネル上でいくつかのユーザをメンションしており、それらのユーザの一部はユーザ情報の概要欄にてAを紹介していました。それらの情報と、グループの概要欄にて示されていたAの紹介文、Aのチャンネル名などを総合的に評価した結果、Aはクレジットカード情報を使った 複数の犯罪ビジネス を展開していると分析しました。 なお分析に使用したユーザ、グループ、チャンネルの概要欄や名前には隠語が多く使用されていたため、このフェーズが最も大変でした。 インターンシップを通して学んだこと・感じたこと 今回のインターンシップでいくつかのTelegramチャンネルを調査/分析させていただき、犯罪者のエコシステムについて大きく3つを学びました。 犯罪者たちは調査を逃れるため、隠語を多用する そのため本格的に調査/分析する場合は隠語の知識が必須となる 1つの単語に対していくつもの隠語があるケースもある 隠語の理解には犯罪と関連するもの(e.g.クレジットカード)についての深い知識が必要となることもある 認証情報やクレジットカード情報に関する犯罪のマネタイズにはさまざまな手法がある 最近ではサブスク形式のマネタイズが多く見られ、サブスクの波が犯罪者界隈にも到来していると感じた 犯罪者コミュニティでの支払い手段は取引の匿名性のほかに、価値の変動の小ささや取引手数料などが重視されている また調査中に「販売されている情報を入手できれば被害者に被害を通知できるのに」という気持ちと、「犯罪者に金銭を提供してはいけない」という気持ちが芽生えたことで倫理面での葛藤を覚え、実際のセキュリティ現場でのジレンマのようなものを感じられたような気がします。 おわりに 実は私は、これまでもセキュリティニュースなどを見てTelegramチャンネルやグループの分析をやってみたいと思ったことが何回もありました。 しかし毎回調査時にミスしてしまったときのリスクを考えてしまって二の足を踏み、後回しにしてしまっていました。 そのため今回のインターンシップで実際にTelegramチャンネルやグループの調査/分析をさせていただけた経験は、非常に有意義なものになったと感じています。 今後は恐れず、ただしリスクも十分に考えて、しっかりとした倫理観も保ちつつこのような分野の調査/分析をしていきたいと思います。 また本記事で紹介したTelegramチャンネルの分析以外の体験でも、非常に興味深く面白い経験をさせていただいたと感じています。 特にCobalt Strikeに関する経験はなかなか他でできないもので、最近あまり手を動かして技術に触れられていなかったこともあり、とても楽しく過ごさせていただきました。 さらにインターンシップ中いくつかのイベントにも参加させていただき、NA4Secチームやほかのチーム、ほかのインターン生といった多くの方々と交流できました。 そこで得られた価値観やセキュリティに対する姿勢は私にとって強い刺激となり、今後見習いたいと感じました。 インターンシップの間、私に関わってくださった皆さま、本当にありがとうございました。 特に神田さん、益本さんにつきましては、2週間ずっとお世話になりっぱなしでした。 改めてありがとうございました。 「 セキュリティカンファレンス「Botconf 2025」に登壇してきた話 」にて、BotConf 2025での登壇に関するお話が掲載されています ↩ Cobalt Strike の調査/分析・ハンズオンで体験した内容の一部は、過去にインターンシップに参加された方が「 インターンシップ体験記 〜Cobalt StrikeのC2サーバ追跡〜 」にて紹介されています ↩ フィッシングサイトに関する調査/分析・ハンズオンで体験した内容の一部は、過去にインターンシップに参加された方が「 攻撃者はいかにしてフィッシングサイトを隠すか?(インターンシップ体験記) 」や「 フィッシングキットから生成されたサイトの調査 (インターンシップ体験記) 」にて紹介されています ↩
アバター
こんにちは、イノベーションセンターの加藤・岡本です。普段はコンピュータビジョンの技術開発やAI/MLシステムの検証に取り組んでいます。7月29日から8月1日にかけて、国内のセンシング技術や画像処理関連の主要な学会である MIRU(画像の認識・理解シンポジウム) が開催され、NTTドコモビジネスからはポスター発表で参加しました。本稿ではMIRU2025で気になった発表をいくつか紹介したいと思います。 MIRU2025概要 特殊な光学機器を用いた研究 1. イベントカメラを用いた可視光通信の高速化 (兵庫県立大学) 2. 符号化環境照明を用いた民生用カメラ撮影に対する情報埋め込みの実現 (大阪大学、York University) 3. 光飛行時間の直接計測による関与媒体に対して頑健な振動計測 (兵庫県立大学) 画像認識・VLMのロバスト化と評価 1. 不均衡なデータセットの継続学習における勾配一貫性正規化と動的な知識蒸留 (名城大学) 2. Unleahing the Potential of Complementary Spaces in Group Robust Classification (東京理科大学) 3. VELA: LLM-Hybrid-as-a-judgeにもとづく長文画像キャプション向け自動評価尺度 (慶應義塾大学) 最後に MIRU2025概要 MIRUはコンピュータビジョンや画像映像の認識と理解技術に関する国内最大規模の会議です。2020年ごろから年100件のペースで発表件数が増えており、今年は口頭発表88件、ポスター発表606件、参加者数は約1500名と過去最大の規模となりました。 MIRUの発表区分(招待講演等を除く)には口頭発表とインタラクティブ(ポスター)発表に別れており、口頭発表のみ査読があります。 本稿ではMIRU2025で気になった発表を紹介したいと思います。※以下で使用する全ての画像は原論文で掲載されている画像を引用しています。 特殊な光学機器を用いた研究 画像センシングを扱う学会ということもあり、単なるRGBカメラ以外の機器を用いた研究が多くみられました。そういった中から気になったものを3件ピックアップしました。 1. イベントカメラを用いた可視光通信の高速化 (兵庫県立大学) 可視光の点滅パターンによって通信する「可視光通信」に関する研究です。可視光通信は既存の電波に干渉しないため、病院や航空機内など電波の利用が制限されている場所でも使えるという利点があります。しかしながら既存のカメラではフレームレートに限界があるため、送れる情報量に制限があるという問題があります。 そこで本研究では、輝度の変化しか拾えない一方で高い時間分解能と高いダイナミックレンジを持つイベントカメラを用いて高速な点滅パターンを利用可能にしました。 イベントカメラによる取得データのイメージ(Event-based, 6-DOF pose tracking for high-speed maneuvers 1 より引用)。 回転する円盤を普通のカメラで撮るとフレームごとに全体の画像が保存されるが、イベントカメラで撮ると時間方向と空間方向に広がる輝度変化の点群として得られる。 イベントカメラを用いた研究は以前からありましたが、高い時間分解能を活かした高速な物体の検出や、高いダイナミックレンジを活かした明暗差の激しい環境での映像認識など、視覚データの理解に関するタスクがほとんどでした。そのためこのように通信へ適用した研究は個人的に新鮮でした。 2. 符号化環境照明を用いた民生用カメラ撮影に対する情報埋め込みの実現 (大阪大学、York University) この研究は盗撮やディープフェイクへの対策として、人間にはわからない程度に光源を変化させることで撮影動画にウォーターマークを埋め込もうという取り組みです。 こちらも前述と同様に光源点滅ではカメラのフレームレートに限界があるため、さまざまな色のLEDを集めたハイパースペクトル照明を作成し、光源スペクトルを操作することで1秒あたり15ビットの情報埋め込みを達成しています。 撮影した動画像に対してデジタル的に情報を埋め込む技術はステガノグラフィーと呼ばれ広く研究されてきましたが、物理的にその空間に情報を埋め込むというアイディアがとても独創的に感じられました。ステガノグラフィーで利用されているような微小な色の操作ではなく、人間とカメラセンサーのスペクトル感度の差に着目し光源のスペクトルを操作したという点も新鮮でした。 3. 光飛行時間の直接計測による関与媒体に対して頑健な振動計測 (兵庫県立大学) この研究は単一の光子を拾うことができるほど敏感なSPAD(Single Photon Avalanche Diode)センサーを活用したものです。 このセンサーをパルスレーザーと組み合わせると、往復時間から光の飛行距離がわかり、さらに光の飛行距離の変化からレーザーを当てた物体の振動がわかることを示しました。 レーザーを用いた振動計測には反射時のドップラー効果を用いたものや反射光のブレを見るものなどがありますが、本手法は光の飛行距離を計測しているので、間に半透明ガラスなどがあっても問題なく対象の振動を計測できるというところが既存の非接触観測に対する利点です。 SPADセンサーについてはとても感度の高い光センサーであるということしか知りませんでしたが、このセンサーを活用することで物体の振動を測れるほど精密に光子の到達時刻を認識できるということに驚きました。本研究の実験装置では1ピコ秒(光が0.3ミリ進む時間)の時間分解能で計測ができるようです。 画像認識・VLMのロバスト化と評価 画像認識モデルのロバスト化または、VLMの評価周りで気になったものを3件ピックアップしました。 1. 不均衡なデータセットの継続学習における勾配一貫性正規化と動的な知識蒸留 (名城大学) この研究は不均衡なデータセットに対して継続学習をする取り組みを行なっています。不均衡なデータで学習すると過学習や学習不足が発生する問題と、継続学習すると旧クラスにおいて破壊的忘却が起こる問題2つに対処しています。 これらの問題を解決するために本研究では、Gradient Reweighting(GR) 2 に着目しています。GRはクラス/タスク単位で勾配を動的に再重み付けし、少数クラスの信号を増幅しつつ多数クラスへの偏りと旧クラスの忘却を抑えます。 しかし、GRでは勾配の振れ幅が大きく、学習は不安定になりやすいという課題があります。 そこで本研究では、勾配ノルムを移動平均したものを用いることで、過去の勾配方向に一貫性を持たせ、破壊的忘却を抑制しています。また知識蒸留に関するロス関数の計算をする際に、学習の初期段階では新クラスの学習を優先し、後半では知識蒸留の影響を強めるために、知識蒸留に関するロス関数に重みをかけることで調節しています。具体的には、現在のエポック数を総エポック数で割った重みを用いています。 実験ではCIFAR-100-LT(ρ=100)・ImageNetSubset-LT・Food101-LTの三種のデータセットを用いて評価しており、全クラスをN∈{10,20}に等分し、継続学習を実施しています。継続学習時に入力するデータの順番は各クラスのサンプル数の降順に固定するIn-ordered、ランダム順で入力するShuffledを実施しています。実験の結果、提案手法は12条件中11条件でGRを凌駕しました。 継続学習に関する論文は以前から多く存在しますが、現場で課題となる不均衡データにおける課題を課題設定に含めており、より現実的な設定の問題に対処している点が個人的に面白いと感じました。 2. Unleahing the Potential of Complementary Spaces in Group Robust Classification (東京理科大学) この研究は既存のVision Language Model(VLM)に存在するバイアスを除去した場合、元々正しく答えられていた猫の画像を車のように答えてしまう課題の改善に努めています。ここでいうバイアスとは、例えば金髪の男性が映る画像に対してVLMに髪色を答えさせた場合、学習時の統計的偏りや言語的先入観などを優先してしまい、VLMが黒髪と答えてしまうことを指します。 この課題の改善のために既存のバイアス除去手法を(1)線形/非線形プロービング、(2)アダプター、(3)補空間への射影の3系統とみなし、それぞれの手法が非対象タスクに対してどの程度精度の維持ができるのかを調査しました。ここで(1)線形/非線形プロービングは事前学習済みモデルの最終出力層に線形層等を追加し線形層のみを学習する手法を指し、(2)アダプターは事前学習済みモデルにMLP等を追加し、追加したMLP等のみを学習する手法を指し、(3)補空間への射影は特定のグループ属性が張る部分空間を推定し、該当の成分のみを取り除いた特徴に置き換える手法を指します。 実験の結果、(1)線形/非線形プロービングや(2)アダプター系は、対象タスクの精度は上がる一方で、非対象タスクの精度が数%台まで落ちる“破滅的忘却”を頻発。対照的に(3)「グループ属性の補空間への直交射影」にもとづく方法だけが、非対象タスクの性能をほぼ維持できることを示しました。 この結果を受け著者らは(3)補空間射影系を後処理で較正するシンプルな追加損失を提案しました。提案手法では、射影前後で一般語彙(WikipediaやImageNet同義語集合など)のテキスト埋め込みが変わらないように制約し、変化を“グループ属性が張る部分空間”に閉じ込めるように最適化を行いました。この結果対象タスクと非対象タスク平均精度の調和平均が一貫して向上し、ゼロショットCLIP比で最大+14.2%改善しました。 バイアスの削除に取り組む研究は多く存在すると思うのですが、バイアス削除に伴って特徴量空間はどのように変化し、非対象タスクの認識に影響を与えるのかについて以前から関心が個人的にありました。この研究はこの疑問に対して体系的に検証しており、検証の結果既存の手法であると(3)補空間への射影以外は非対称タスクへ対応できないことを明らかにしました。またこの知見から新たな手法を提案しており、有用な示唆を与える研究だと感じました。 3. VELA: LLM-Hybrid-as-a-judgeにもとづく長文画像キャプション向け自動評価尺度 (慶應義塾大学) この研究では、VLMの長文画像キャプションタスクの新しい評価指標を提案しています。既存のキャプションタスクで用いられる評価指標(BLEU/CIDEr/CLIPScore 等)では文全体の構成や一貫性を十分に測れず、またLLM/MLLMを用いて評価するLLM-as-a-judgeでは、自己回帰・早期画像統合のため遅いという課題がありました。 本論文はこれを同時に解く自動評価尺度VELAを提案しました。提案手法ではQwen2.5 3B 3 とLong-CLIP 4 の特徴量をProjector層で結合し(1)詳細さ、(2)関連性、(3)流暢さの3観点について数値スコアを同時出力します。学習には著者らが新規に構築したLongCap-Arenaを使用しており、画像・長文参照・長文候補に加え、アノテーターが主に詳細さ・関連性・流暢さの観点で数値評価を付与したデータセットで合計32,246件の人手評価を含みます。 実験ではKendall’s τcにてGPT-4oを用いたLLM-as-a-judge手法や強力なCLIP系指標を上回り、約260ms/サンプルと既存LLM-as-a-judgeより約5倍高速化を達成しました。 かなりシンプルなモデル構成で詳細さ・関連性・流暢さの観点でより人間と相関があり、LLM-as-a-judge手法を上回る性能を達成している点が面白いと感じました。 最後に 本ブログでは、私たちが興味を持ったMIRU2025の発表についてご紹介しました。NTTドコモビジネスでは、今回ご紹介した分野に限らず、画像や映像、さらには音声言語も含めたさまざまなメディアAI技術の論文調査や研究開発に今後も積極的に取り組んでいきます。 Mueggler, B. Huber and D. Scaramuzza, "Event-based, 6-DOF pose tracking for high-speed maneuvers", IROS 2014. https://www.youtube.com/watch?v=LauQ6LWTkxM ↩ He, Jiangpeng : "Gradient Reweighting: Towards Imbalanced Class-Incremental Learning", CVPR 2024 ↩ Qwen: An Yang, Baosong Yang, Beichen Zhang, Binyuan Hui, Bo Zheng, Bowen Yu, Chengyuan Li, Dayiheng Liu, Fei Huang, Haoran Wei, Huan Lin, Jian Yang, Jianhong Tu, Jianwei Zhang, Jianxin Yang, Jiaxi Yang, Jingren Zhou, Junyang Lin, Kai Dang, Keming Lu, Keqin Bao, Kexin Yang, Le Yu, Mei Li, Mingfeng Xue, Pei Zhang, Qin Zhu, Rui Men, Runji Lin, Tianhao Li, Tianyi Tang, Tingyu Xia, Xingzhang Ren, Xuancheng Ren, Yang Fan, Yang Su, Yichang Zhang, Yu Wan, Yuqiong Liu, Zeyu Cui, Zhenru Zhang, Zihan Qiu : "Qwen2.5 Technical Report", Arxiv 2024 ↩ Beichen Zhang, Pan Zhang, Xiaoyi Dong, Yuhang Zang, Jiaqi Wang : "Long-CLIP: Unlocking the Long-Text Capability of CLIP", ECCV 2024 ↩
アバター
こんにちは。クラウド&ネットワークサービス部で SDPF のベアメタルサーバの開発をしている山中です。 先日、Google Workspace で利用できる Gemini API を活用して、日々の業務ログから日報を自動生成し、Slackに自動投稿する仕組みを構築しました。 その具体的な方法と、実際に導入してわかった想像以上の効果をご紹介します。 デイリースクラムの悩み、AIで解決しませんか? やったこと:情報を集めて Gemini に日報を書かせる 1. 各種ツールから活動ログを収集 2. イベント情報を時系列で整理 3. プロンプトを作成し Gemini API を実行 4. Slack への自動投稿 想像以上の効果!情報共有が劇的に改善 シンプルに楽!「昨日何してたっけ?」からの解放 口頭の問題点を解決 完璧を求めない柔軟な運用 完璧じゃないからこそ面白いAIの活用法 テキスト文化との親和性 AIは人間をアシストする存在 デイリースクラムの悩み、AIで解決しませんか? 「昨日はこれをやって、今日はこれをやる予定です」 スクラムで開発している皆さん、毎朝のデイリースクラムどうしていますか? 短い時間で、やったことや今後の予定を口頭でサクッと共有する。 アジャイル開発におけるデイリースクラムは、チームの情報共有に欠かせません。 しかし、この短いミーティング、意外と悩みがつきものじゃないでしょうか。 マルチタスクに追われていると「昨日何やってたっけ?」と混乱する 口頭だと伝え漏れや解釈のずれが起きやすい 日報をちゃんと書いている人もいるけど、正直面倒くさそう… GitHub や Jira、Slack に記録は残っているものの、複数のツールを横断して情報を整理するのは意外と労力がかかります。 でもこの課題、Gemini を使って解決できました。 今回は、社内の Google Workspace で利用できる Gemini API(Vertex AI 1 )を使って日報を自動生成し、Slack に自動投稿する仕組みを構築したので、その具体的な方法と効果をご紹介します。 やったこと:情報を集めて Gemini に日報を書かせる 今回作ったのは、チームメンバーの日々の活動ログを自動で収集し、それを元に Gemini が日報を作成してくれるツールです。 1. 各種ツールから活動ログを収集 まずは、GitHub や Jira といった業務ツールから、各メンバーの活動ログを収集します。 この部分は、APIを叩くだけの定型的な作業なので、AI や MCP(Model Context Protocol 2 ) サーバは使いません。 欲しい情報だけを素早く確実に取得するために、各種サービスで提供されている API 経由で情報を取得します。 収集するイベント情報は以下の通りです。 昨日から今日までという期間に絞って各種イベント情報を取得することで、日報生成の材料を集めます。 Slack の投稿:メッセージ内容とチャンネル名 Jira チケットコメント:コメント本文、チケット ID とタイトル Confluence ページの編集:ページタイトルと編集時刻 作成した GitHub PR:PR タイトルと本文、リポジトリ名 Google Calendar の予定:予定の件名と時刻 API は直接リクエストを送ってもよいですし、手に馴染みのある言語のライブラリを使って取得するのも良いです。 2. イベント情報を時系列で整理 集めたイベント情報を JSON 形式に変換し、時系列でソートします。 これにより、AI が異なるイベント同士を関連付けて解釈しやすくなる効果が期待できます。 以下は、収集したイベントデータの JSON 例です。 [ { "type": "slack_message", "timestamp": "2025-09-03T09:30:00Z", "channel": "example-dev", "text": "PR #123 のレビューお願いします!" }, { "type": "github_pr", "timestamp": "2025-09-03T11:45:00Z", "repo": "example-tool", "title": "ログイン機能のバグ改修", "body": "ログイン時の処理に XXX の考慮漏れがあったため、YYY の対処を導入" }, // 他のイベントが続く ] 3. プロンプトを作成し Gemini API を実行 ここが一番重要なポイントです。時系列で整理したイベント情報を元に、日報を生成するためのプロンプトを作成し、Gemini API に投げ込みます。 プロンプトの内容は以下の通りです。 ## 指示(Instruction) 与えられたイベントログから [メンバー名] の日報を作成してください。 この日報は、チームメンバーが [メンバー名] の業務状況を正確かつ簡潔に把握できるようにすることを目的とします。 ## 役割(Role) あなたは「チームの情報共有を円滑にし、連携を促進するサポーター」です。 読み手が内容をすぐ理解でき、行動につなげられるように整理・要約してください。 ## 目的 (Goal) - チーム全員が [メンバー名] の業務内容・進捗・課題を把握できること - 必要な連携やフォローを即座に行える状態にすること - 業務の透明性を高め、チーム全体で成果を称賛し合える文化を育むこと ## ルール(Rules) - 全体を通して「親しみやすさ+ビジネス的な明確さ」を両立させること - 文体は「体言止め」「短文中心」で、冗長な表現を避けること - ノイズ(挨拶・雑談・無意味な作業ログなど)は含めないこと - 細かすぎる作業は省略し、業務の本質や意義が伝わる粒度でまとめること - 適度に絵文字を使用し、視覚的に読みやすくすること - 最後に「成果や頑張りを具体的に労う一文」を必ず入れること ### イベントログ(Event Log) [ここに時系列でソートした JSON データを挿入] ## 出力形式(Output Format) 以下のフォーマットに従って出力してください。 ``` [メンバー名] の日報(XX月XX日) *業務1* 1. 内容1 * 詳細1 * 詳細2 2. 内容2 * 詳細1 3. ... *業務2* 1. 内容1 * 詳細1 * 詳細2 2. 内容2 * 詳細1 3. ... <メンバーの士気が上がるよう、頑張りや成果などを具体的な内容とともに元気よく労う> ``` 4. Slack への自動投稿 Gemini が生成した日報を、デイリースクラムの時間に合わせて Slack の専用チャンネルに自動投稿します。以下のスクリーンショットは実際に投稿された日報の例です。 作成したシステムはローカル端末上で動かしており、イベント情報の収集や Gemini API の呼び出し、Slack への投稿など、全て自動化しています。 実装は Ruby で行いましたが、同じような仕組みはどんな言語でも構築できるはずです。 想像以上の効果!情報共有が劇的に改善 この仕組みを導入して、チームの情報共有は劇的に改善しました。 シンプルに楽!「昨日何してたっけ?」からの解放 日報が自動で生成されることで、朝イチから「昨日何してたっけ…?」と頭を悩ませる時間が非常に少なくなりました。 ゼロから考えるのではなく、自動生成された内容をベースに口頭で補足するだけで済むので、デイリースクラムにおける情報共有が本当に楽になりました。 口頭の問題点を解決 日報がテキストで残ることで、情報共有の精度とアクセス性が大幅に向上しました。 伝え漏れや解釈のずれがなくなる デイリースクラムに参加できないメンバーも、日報を読めば何があったかすぐにわかる マネージャーはチーム全体の動きを把握しやすくなる 完璧を求めない柔軟な運用 AI が生成する日報は100%完璧ではありません。しかし、デイリースクラムの目的は情報共有を円滑にすることであり、内容が少し違っていても、その場で口頭で修正すれば十分です。 「完璧な情報」ではなく「とっかかりの情報」の提供ツールとして AI を使うことで、デイリースクラムにおいて非常に効果的に機能しました。 ちなみに、日報生成にかかる費用は、メンバー1人あたり1日わずか2.5円程度。コストを気にせず利用できるのも大きなメリットです。 (モデルは gemini-2.5-flash を使用しています。) 完璧じゃないからこそ面白いAIの活用法 今回の取り組みを通して、改めてAIの活用について考えさせられました。 テキスト文化との親和性 リモートワークが普及し、日々のやり取りが Slack などのテキストに置き換わった現代において、 AI がテキスト情報を自動で集約し、可視化するこの仕組みは非常に理にかなっていると感じます。 AIは人間をアシストする存在 人間が自らの意思で AI に何かを聞くのではなく、スケジュール実行やイベントをトリガーに AI が動き、人間の作業を先回りしてアシストする。 これこそが、AIを最大限に活かす方法の1つかもしれません。 また、現状の AI は完璧に指示通りのロジックで処理をすることは苦手です。完璧なタスク処理を AI に任せるのではなく、「完璧じゃなくてもいいから、良い感じに手伝ってほしい」という領域にこそ、積極的に生成 AI を組み込んでいくことが良いと思います。 システムに AI を組み込むときは、 「どの部分を、決定的なロジック(必ず同じ結果になるような決まったプログラム)で作るか?」 「どの部分を、非決定的な処理(柔軟で予測できないAI)で任せるか?」 この線引きを意識して設計することが大切です。 これは既存のプログラミングとは少し違う、新しい発想力や設計の仕方が求められる、非常に面白い領域だと感じています。 今回の仕組みは、デイリースクラムに限らず、あらゆるミーティングや朝会で役立つはずです。皆さんのチームでも、ぜひ試してみてください。 Vertex AI ( https://cloud.google.com/vertex-ai ) ↩ Model Context Protocol ( https://modelcontextprotocol.io/docs/getting-started/intro ) ↩
アバター