TECH PLAY

NTTドコモビジネス

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

613

はじめに こんにちは、イノベーションセンターの福田です。 前回 に引き続き、 AWS Outposts について紹介していきます。 今回は、特に AWS CDK による AWS Outposts の Infrastructure as Code (以降 IaC) を行う方法について記載していきたいと思います。 AWS CloudFormation AWS CDK では AWS CloudFormation というサービスをバックエンドで利用しているため、まずはこの AWS CloudFormation について解説します。 AWS CloudFormation は、AWS の各種リソースを JSON/YAML 形式で記述した"テンプレート"として管理します。 これによって AWS リソースの IaC を実現してくれますが、サービスとして IaC を実現するものというよりは AWS リソースの管理を AWS がマネージしてくれるものといった方が近いです。 料金はデプロイされた各種リソースにかかる料金のみで、 CloudFormation の利用自体に料金はかかりません。 CloudFormation では自動で依存関係等を解決し、適切な形でリソースのライフサイクルを管理してくれます。 デプロイされたリソース群は スタック という単位で管理されます。 このスタックへ、リソースを新しく追加したり、削除したりしても CloudFormation が依存するものの管理をおこなってくれます。 これによってユーザ側で Web コンソールや AWS CLI 等で頑張ってリソースの状態等を管理することから解放されます。 さらに、デプロイに失敗しても以前に成功してくれた状態にロールバックしてくれるため、自前で AWS のリソースのデプロイに失敗したから自分で再度構築しなおすということも行う必要はありません。 AWS CDK AWS CDK とは AWS Cloud Development Kit の略です 1 。 AWS CDK 自体は一般的なプログラミング言語(TypeScript, Python, Java 等)で AWS リソースを定義・管理できるオープンソースのフレームワークです。 最終的には AWS CloudFormation のテンプレートに直り、そのテンプレートを通して各種リソースを AWS CloudFormation へプロビジョニングします。 AWS CloudFormation では JSON/YAML でテンプレートを記載していく関係上、定義が長くなりがちです。 一方、 AWS CDK はテンプレートを汎用のプログラミング言語で定義できるため、非常に短いコードで CloudFormation テンプレートを定義できます。 また、プログラミング言語を使って動的に設定できる値を自動で解決してくれるため、全てを自分で書いて定義しなければならない CloudFormation よりもコードを短くできます。 AWS CDK では各種リソースを コンストラクター というもので定義します。 コンストラクターとは要は各種リソースに対応した CDK 上のコンポーネントになります。 このコンストラクターは主に2つに分けられます 2 。 ローレベルコンストラクター CloudFormation のテンプレートにおけるリソースと一対一対応したコンストラクターです。 例えば aws-cdk-lib/aws-ec2 の CfnVpc コンストラクターは CloudFormation 上の AWS::EC2::VPC リソースにそのまま対応します ハイレベルコンストラクター ローレベルコンストラクターを利用しやすくラップしたり、各種リソース配下で利用するリソース(たとえば VPC における subnet 等)も一緒に簡単な設定値を指定すると自動でそれに必要なリソースも一緒に定義してくれたりしてくれるより抽象度の高いコンストラクターです。 例えば aws-cdk-lib/aws-ec2 の Vpc コンストラクターは subnetConfiguration というプロパティに subnet の設定を入れると subnet の定義や、そこに展開される NAT の定義等も個別に定義せずともまとめて行ってくれます。 ローレベルコンストラクターありきなので、ローレベルコンストラクターがある機能に対応してなければハイレベルコンストラクターも対応していません。 基本的にはハイレベルコンストラクターを利用し、ハイレベルコンストラクターで対応できない事情があればローレベルコンストラクターを利用します 3 。 現在、 v1 と v2 がそれぞれ存在している状態なのですが、ここでは v2 ベースにして解説していきます。 カスタムリソース 現在、 AWS Outposts 上に展開する一部リソースは CloudFormation では未対応な部分があるため、 CloudFormation の カスタムリソース という機能を利用する必要があります。 ここでは本題へ入る前に、今回使用するカスタムリソースを解説します。 カスタムリソースとは CloudFormation におけるリソースのカスタムプロビジョニングロジックを定義する方法です。 例えば、 CloudFormation が対応してないリソースを CloudFormation テンプレートで管理したい CloudFormation が対応してない設定をリソースに定義したい といったような場合に利用し、 CloudFormation で対応してないリソースでもテンプレート上で管理できるようになります。 実はカスタムリソースの実体は Lambda の関数です。 定義しておいた Lambda の関数をスタックのプロビジョニング時、つまりスタック上の他のリソースがプロビジョニングされているときに呼ぶことでリソースを作成/更新/修正する AWS API を呼び出し、リソースのプロビジョニングを行う仕組みです。 これによって、 AWS API が提供されているものの CloudFormation に対応してない新サービスのリソースをプロビジョニングすることを実現しています。 AWS CDK でもこの CloudFormation のカスタムリソースに対応しています。 特に、 AWS CDK ではカスタムリソースを利用しやすくする Provider Framework があるため、簡単にカスタムリソースを提供できます。 AWS CDK のリファレンスではいくつかの カスタムリソース実装例 が提供されていますので、詳細はそちらをご確認ください。 ここではカスタムリソースを定義する Provider Framework について少し解説しておきます。 カスタムリソースを利用するには、まずリソースの管理する Lambda 関数を定義します。 この際、 AWS API を呼び出す場合は、その API と操作対象のリソースへの操作許可ポリシーを定義しておく必要があります。 これは Function コンストラクター の initialPolicy プロパティー で定義します。 import { Stack , StackProps } from 'aws-cdk-lib' ; import { Construct } from 'constructs' ; import * as lambda from 'aws-cdk-lib/aws-lambda' ; import * as iam from 'aws-cdk-lib/aws-iam' ; export class Stack extends cdk.Stack { constructor( scope: Construct , id: string , props?: StackProps ) { // ... const handler = new lambda. Function ( this , "Handler" , { // ... initialPolicy: [ new iam.PolicyStatement ( { resources: [ "*" ] , actions: [ // ここに Lambda で扱うリソースを操作する API に対応したアクションを追加する。 ] , } ), ] , } ); } } ハンドラーを定義したあとは次のようなボイラープレートコードを生成しておけばカスタムリソースの定義は完了です。 import { Stack , StackProps } from 'aws-cdk-lib' ; import { Construct } from 'constructs' ; import * as lambda from 'aws-cdk-lib/aws-lambda' ; import * as iam from 'aws-cdk-lib/aws-iam' ; import * as cr from 'aws-cdk-lib/custom-resources' ; import * as cdk from 'aws-cdk-lib' ; export class Stack extends cdk.Stack { constructor( scope: Construct , id: string , props?: StackProps ) { // ... const handler = new lambda. Function ( this , "Handler" , { // ... } ); const provider = new cr.Provider ( this , "Provider" , { onEgventHandler: handler , } ); const resource = new cdk.CustomResource ( this , "Resource" , { serviceToken: provider.serviceToken , } ); } } 次に、カスタムリソースのライフサイクルを管理する Lambda 関数を作成していきます。 Lambda 関数は作成/更新/削除時に呼ばれますが、これらのイベントの種別は Lambda 関数の引数に RequestType というプロパティとしてわたってきます。 なので、 Lambda 関数を次のような形で記載してあげるとリソースを管理できます 4 。 import * as lambda from 'aws-lambda' ; const createHandler = async ( event: lambda.CloudFormationCustomResourceCreateEvent ) : Promise < lambda.CloudFormationCustomResourceResponse > => { // ここは後で実装します } const updateHandler = async ( event: lambda.CloudFormationCustomResourceUpdateEvent ) : Promise < lambda.CloudFormationCustomResourceResponse > => { // ここは後で実装します } const deleteHandler = async ( event: lambda.CloudFormationCustomResourceDeleteEvent ) : Promise < lambda.CloudFormationCustomResourceResponse > => { // ここは後で実装します } export const handler = async ( event: lambda.CloudFormationCustomResourceEvent ) : Promise < lambda.CloudFormationCustomResourceResponse > => { switch ( event.RequestType ) { case 'Create' : return createHandler ( event ); case 'Update' : return updateHandler ( event ); case 'Delete' : return deleteHandler ( event ); default : throw new Error ( "unreachable" ); } } このように定義した Lambda 関数から リンク先 のようなレスポンスを返すと、 CloudFormation がその結果を利用してどのようなリソースを管理しているのかを把握します。 各種レスポンス値の意味は次の通りです。 Status : リソースの作成に成功したかを表すステータスです。ここは SUCCESS を固定で指定します。 LogicalReousrceId : CloudFormation の Logical Resource ID を指定します。これは event.LogicalResourceId として渡ってくるので、これをそのまま指定します PhysicalResourceId : 実際に作成されたリソースにつけられる ID や ARN 等、そのリソースを一意に表現する値を指定します(基本的にはリソースの ID か ARN を指定することになります) RequestId : Lambda の実行がリクエストされた際に付与される ID です。これは event.RequestId として既に渡ってきているので、これをそのまま指定します StackId : デプロイを管理しているスタックの ID を指定します。これは event.StackId として既に渡ってきているので、これをそのまま指定します 各種ステータスや作成したいリソースの設定値等は event.ResourceProperties というプロパティに詰めらています。 import * as lambda from 'aws-lambda' ; /** * Lambda 実行時に例外が出だときに CloudFormation 側でエラーを補足してスタックの * ロールバックを行ってくれます。 * * 実はドキュメントだとエラーがおこった際は FAILED を Status に設定したレスポンスを * 返すよう指示がされていますが、これを丁寧に守って返すとエラーが発生しても CloudFormation 側は * エラーがおこったことを無視してデプロイに成功したとします。 * * ref: https://docs.aws.amazon.com/cdk/api/v1/docs/custom-resources-readme.html#handling-lifecycle-events-onevent */ const createHandler = async ( event: lambda.CloudFormationCustomResourceCreateEvent ) : Promise < lambda.CloudFormationCustomResourceResponse > => { // ResourceProperties にリソースの設定値が辞書形式で格納される const properties = event.ResourceProperties ; // ここで AWS API を呼び出す等してリソースを作成する。 return { Status: "SUCCESS" , LogicalResourceId: event.LogicalResourceId , PhysicalResourceId: "<ここには作成したリソースに付くユニークな ID や ARN を指定する>" , RequestId: event.RequestId , StackId: event.StackId , } ; } const updateHandler = async ( event: lambda.CloudFormationCustomResourceUpdateEvent ) : Promise < lambda.CloudFormationCustomResourceResponse > => { // ResourceProperties にリソースの設定値が辞書形式で格納される。 // もし、 Update が失敗した場合は以前の設定値が ResourceProperties に保存してくるので、これを元に既存のリソースをつくりなおすことになる。 const properties = event.ResourceProperties ; // ここでリソースを作り直す return { Status: "SUCCESS" , LogicalResourceId: event.LogicalResourceId , // ドキュメントによると新しい Physical Resource ID を指定すると // 更に Delete イベントが走ってリソースの削除を行おうとする。 // // ref: https://docs.aws.amazon.com/cdk/api/v1/docs/custom-resources-readme.html#handling-lifecycle-events-onevent PhysicalResourceId: "<作り直したリソースに付いている ID や ARN>" , RequestId: event.RequestId , StackId: event.StackId , } ; } const deleteHandler = async ( event: lambda.CloudFormationCustomResourceDeleteEvent ) : Promise < lambda.CloudFormationCustomResourceResponse > => { const properties = event.ResourceProperties ; // ここでリソースを削除する return { Status: "SUCCESS" , LogicalResourceId: event.LogicalResourceId , PhysicalResourceId: event.PhysicalResourceId , RequestId: event.RequestId , StackId: event.StackId , } ; } もし付加的な情報を返す場合、 lambda.CloudFormationCustomResourceResponse に Data というプロパティを利用します。 Data に辞書形式で値とキーを指定しておけば、後で CloudFormation にある Fn::GetAttr 関数 5 へキーを指定することで Data に指定したキーを持つ値を参照できます。 たとえば subnet を作成した上でその ID を後で参照しようとしたとき、まずは Lambda から Data プロパティを介して値を参照できるようにします(ここでは SubnetId というキーとします)。 import * as lambda from 'aws-lambda' ; const createHandler = async ( event: lambda.CloudFormationCustomResourceCreateEvent ) : Promise < lambda.CloudFormationCustomResourceResponse > => { // ResourceProperties にリソースの設定値が辞書形式で格納される const properties = event.ResourceProperties ; // ここで AWS API を呼び出す等してリソースを作成する。 return { Status: "SUCCESS" , LogicalResourceId: event.LogicalResourceId , PhysicalResourceId: "<ここには作成したリソースに付くユニークな ID や ARN を指定する>" , RequestId: event.RequestId , StackId: event.StackId , Data: { SubnetId: "作成された subnet の ID" , } } ; } これを CDK 側で参照する場合は次のようにします。 import { Stack , StackProps } from 'aws-cdk-lib' ; import { Construct } from 'constructs' ; import * as cdk from 'aws-cdk-lib' ; export class Stack extends cdk.Stack { constructor( scope: Construct , id: string , props?: StackProps ) { // ... const resoure = new cdk.CustomResource ( this , "CustomResource" , { /* ... */ } ); const subnetId = resource.getAtt ( "SubnetId" ); // Data に登録された SubnetId の値を取得 } } ただし、 getAtt は AWS CDK 上だと文字列ではく aws-cdk-lib の IResolvable 型として返ってきます。 返ってきた値を文字列として参照したい場合は次のように aws-cdk-lib の token.asString メソッドを噛まして変換をしておく必要があります 6 。 import { Stack , StackProps } from 'aws-cdk-lib' ; import { Construct } from 'constructs' ; import * as cdk from 'aws-cdk-lib' ; export class Stack extends cdk.Stack { constructor( scope: Construct , id: string , props?: StackProps ) { // ... const resource = new cdk.CustomResource ( this , "CustomResource" , { /* ... */ } ); const subnetId = cdk.Token.asString ( resource.getAtt ( "SubnetId" )); } } 一方で、ライフサイクルを管理する Lambda 関数を呼び出すとき、引数を渡したい場合があると思います。 たとえば、 ALB を作成/更新/削除するカスタムリソースを作成したとします。 すると、 CreateLoadBalancer API には ALB につける名前を定義する必要がありますが、これを動的に CDK で管理したい場合などが該当します。 このような場合、 aws-cdk-lib の CustomResource にある properties というプロパティを使用します。 具体的には、次のように properties へ辞書形式で設定値を Lambda 関数へ流し込むことになります。 import { Stack , StackProps } from 'aws-cdk-lib' ; import { Construct } from 'constructs' ; import * as lambda from 'aws-cdk-lib/aws-lambda' ; import * as iam from 'aws-cdk-lib/aws-iam' ; import * as cr from 'aws-cdk-lib/custom-resources' ; import * as cdk from 'aws-cdk-lib' ; export class Stack extends cdk.Stack { constructor( scope: Construct , id: string , props?: StackProps ) { // ... const name = "<ALB の名前>" ; const handler = new lambda. Function ( this , "Handler" , { // ... } ); const provider = new cr.Provider ( this , "Provider" , { onEgventHandler: handler , } ); const resource = new cdk.CustomResource ( this , "Resource" , { serviceToken: provider.serviceToken , // properties に辞書形式で引数を登録します。 // ここでは例として、 subnet id を `subnetId` という引数名で渡しています。 properties: { name: name , } , } ); } } すると、 Lambda 関数では次のように properties の値を参照できます。 import * as lambda from 'aws-lambda' ; export const handler = async ( event: lambda.CloudFormationCustomResourceEvent ) : Promise < lambda.CloudFormationCustomResourceResponse > => { // event に ResourceProperties というプロパティがあり、ここに // cdk.CustomResource の properties に指定した値が格納される。 const { name } = event.ResourceProperties ; // ここで properties で指定した name を取り出しています。 } 以上で、カスタムリソースのライフサイクルを管理する Lambda 関数が定義できました。 ここまででカスタムリソースを作成する一連の方法について解説していきました。 詳細なライフサイクル管理について知りたいかたは Provider Framework のドキュメントをご覧ください。 特に、 Update や Delete, Create 時に失敗したときにこちらがどのように対応すべきかについては Important cases to handle にまとまっていますので、一読しておくことをおすすめします。 Outposts IaC with AWS CDK ここまでで読み進めるにあたって必要な準備が整いましたので、さっそく本題に入ります。 今回は AWS CDK を使って Outposts に展開するリソースを定義する方法について私達が試した範囲で記載していきます。 subnet subnet 自体をデプロイするのは簡単です。 既に CloudFormation で展開したい Outpost 筐体の ARN を指定すればデプロイできるようになっています。 ただし、ローレベルコンストラクターのみ Outpost ARN の設定に対応しています。 ハイレベルコンストラクターからどの Outpost 筐体へデプロイするかの設定はできませんが、どの Outpost 筐体にデプロイされたのかを subnetOutpostArn から取得することのみはサポートされています 7 。 import { Stack , StackProps } from 'aws-cdk-lib' ; import { Construct } from 'constructs' ; import * as ec2 from 'aws-cdk-lib/aws-ec2' ; export class Stack extends cdk.Stack { constructor( scope: Construct , id: string , props?: StackProps ) { // ... new ec2.CfnSubnet ( this , "Subnet" , { // ... outpostArn: "arn:aws:outposts:<デプロイ先 Outpost 筐体がデプロイされているリージョン>:<デプロイ先 Outpost 筐体を所持するAWS アカウント ID>:outpost/<デプロイ先 Outpost 筐体の Outposts ID>" , // outpostARN を指定する場合、対象 outpost 筐体が所属している AZ も // 指定しておかなければならない availabilityZone: "<デプロイ先 Outpost が所属している AZ>" , } ); } } ちなみに、 CloudFormation で subnet へ展開するリソースに public IP を自動で付与する設定 8 はありますが、 customer owned IP address 9 に対応するそのような設定は現在 CloudFormation ではサポートされていません。 これが設定できないと、例えば ALB や NAT をデプロイした際に customer owned IP address を自動でつけてくれないので、オンプレミス側から NAT や ALB を参照できません 10 。 現状、この設定をするにはカスタムリソースを使って AWS API を呼び出して設定しなければなりませんので、ここでカスタムリソースを使う必要があります。 実際には ModifySubnetAttribute を呼び出せば目的は達成できるため、前述の Lambda 関数中で対応する AWS API を作成/更新時に呼び出せば OK です。 サンプルコードは こちら に用意したので、参考にしてください。 EBS Volume EC2 インスタンスデプロイ時にくっつくルートボリュームを担う EBS Volume は、 EC2 インスタンスを Outpost 筐体上の subnet に配置することで自動的にその subnet と同じ Outpost 筐体上にデプロイされます。 一方、自分で追加の EBS Volume をつくる場合はローレベルコンストラクターである CfnVolume コンストラクター にデプロイ先 Outpost 筐体の ARN を指定すれば OK です。 import { Stack , StackProps } from 'aws-cdk-lib' ; import { Construct } from 'constructs' ; import * as ec2 from 'aws-cdk-lib/aws-ec2' ; export class Stack extends cdk.Stack { constructor( scope: Construct , id: string , props?: StackProps ) { // ... new ec2.CfnVolume ( this , "Volume" , { // ... outpostArn: "arn:aws:outposts:<デプロイ先 Outpost 筐体がデプロイされているリージョン>:<デプロイ先 Outpost 筐体を所持するAWS アカウント ID>:outpost/<デプロイ先 Outpost 筐体の Outposts ID>" , } ); } } ただし、注意点として、 EC2 インスタンスに対応するハイレベルコンストラクターである Instance コンストラクター 内の EBS Volume を定義するためのプロパティである blockDevices では、 Outposts ARN を指定できません 11 。 したがって Outpost 筐体上に展開する追加の EBS Volume を EC2 インスタンスへアタッチする場合は、 ローレベルである CfnVolume を使って Outpost 筐体上にデプロイするように設定し、 CfnVolumeAttachment でアタッチする そもそもローレベルコンストラクターでインスタンスも定義する の2択になります。 次のコードは 1. の選択肢をとった場合の例です。 import { Stack , StackProps } from 'aws-cdk-lib' ; import { Construct } from 'constructs' ; import * as ec2 from 'aws-cdk-lib/aws-ec2' ; export class Stack extends cdk.Stack { constructor( scope: Construct , id: string , props?: StackProps ) { // ... // インスタンスを用意 const instance = new ec2.Instance ( this , "Instance" , { /* ... */ } ); // 追加の EBS Volume を用意 const additionalVolume = new ec2.CfnVolume ( this , "AdditionalVolume" , { // ... outpostArn: "arn:aws:outposts:<デプロイ先 Outpost 筐体がデプロイされているリージョン>:<デプロイ先 Outpost 筐体を所持するAWS アカウント ID>:outpost/<デプロイ先 Outpost 筐体の Outposts ID>" , } ); //EBS Volume をアタッチ new ec2.CfnVolumeAttachment ( this , "AdditionalVolumeAttachment" , { device: '<アタッチ先デバイス名>' , volumeId: additionalVolume.ref , instanceId: instance.instanceId , } ); } } ALB ALB の場合は customer owned IP address を付与しない場合はハイレベルコンストラクターでも設置先 subnet を指定できる 12 ので、 Outposts 筐体上に展開しているサブネットへデプロイすれば Outpost 筐体上へ自動でデプロイされます。 しかし、 customer owned IP address を付与しようとした ALB は現在 CloudFormation できません 13 。 なので、 customer owned IP address を ALB へ付与する場合はロードバランサーの作成段階からカスタムリソースを使う必要があります 14 。 実際のコードは リポジトリー に 例 を載せたのでそちらを確認してください。 基本的にはロードバランサーを作成/削除してるだけです。 S3 on Outposts S3 on Outposts はローレベルなもののみ提供されています 15 。 ちなみに、 CDK で提供されているモジュールはリファレンスを参照するとまだプレビュー段階であることが伺えます。 まだ安定はしてないようなので、利用する際には注意が必要です。 The construct library for this service is in preview. Since it is not stable yet, it is distributed as a separate package so that you can pin its version independently of the rest of the CDK. https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3outposts-readme.html 前回も書きましたが、 S3 on Outposts を利用するためには次の2つを必要とします。 Outposts エンドポイント S3 アクセスポイント これらは VPC 上へ設置することになるため、 VPC からのみ S3 on Outposts バケットへアクセスできます。 したがって、 S3 on Outposts を利用できるようにするにはバケットを用意しつつ、この2つを VPC 上に設置すれば OK です。 S3 アクセスポイントは 接続先バケットの ARN デプロイ先 VPC の VPC ID アクセスポイント名 の設定がデプロイに最低限必要です。 Outposts エンドポイントは デプロイ先 Outpost 筐体についている Outposts ID エンドポイントにつける セキュリティグループ ID デプロイ先サブネットの ID の3つがデプロイに必要です。 ちなみに、もしデプロイする場合、 CfnEndpoint のドキュメントにもありますが、エンドポイントの作成に時間がかかるので注意してください。 It can take up to 5 minutes for this resource to be created. https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3outposts.CfnEndpoint.html これらをデプロイするコード例は次のようになります。 import { Stack , StackProps } from 'aws-cdk-lib' ; import { Construct } from 'constructs' ; import * as ec2 from 'aws-cdk-lib/aws-ec2' ; import * as s3outposts from 'aws-cdk-lib/aws-s3outposts' ; export class Stack extends cdk.Stack { constructor( scope: Construct , id: string , props?: StackProps ) { // ... // S3 アクセスポイントは VPC にデプロイされるので、 S3 アクセスポイントを設置したい VPC を定義 const vpc = new ec2.Vpc ( this , "Vpc" , { /* ... */ } ); // Outposts エンドポイントはサブネットへ紐付くため、 Outposts エンドポイントを設置したい サブネットを定義 const subnet = new ec2.Subnet ( this , "Subnet" , { // ... vpcId: vpc.vpcId , outpostArn: "arn:aws:outposts:<デプロイ先 Outpost 筐体がデプロイされているリージョン>:<デプロイ先 Outpost 筐体を所持するAWS アカウント ID>:outpost/<デプロイ先 Outpost 筐体の Outposts ID>" , } ); // S3 on Outposts バケットを用意する const bucket = new s3outposts.CfnBucket ( this , "Bucket" , { bucketName: "<デプロイしたいバケット名>" , outpostId: "<デプロイ先 Outpost 筐体の Outposts ID>" , } ); // S3 アクセスポイントを用意する new s3outposts.CfnAccessPoint ( this , "AccessPoint" , { bucket: bucket.ref , // このアクセスポイントで接続する S3 on Outposts のバケット ARN を指定する。ここでは先にバケットを定義しており、そこからできる ARN を参照するようにする。 // デプロイ先の VPC の設定をする vpcConfiguration: { vpcId: vpc.vpcId , // 現状、デプロイ先 VPC の ID しか設定するプロパティが用意されてない } , name: "<アクセスポイントに設定したいアクセスポイント名>" , } ); // Outposts エンドポイントは Security Group を要求してくるので用意する const endpointSecurityGroup = new ec2.SecurityGroup ( this , "EndpointSecurityGroup" , { /* ... */ } ); // Outposts エンドポイントを用意する new s3outposts.CfnEndpoint ( this , "Endpoint" , { outpostId: "<デプロイ先 Outpost 筐体の Outposts ID>" , securityGroupId: endpointSecurityGroup.securityGroupId , subnetId: subnet.ref , } ); } } まとめ 今回は AWS CDK で Outposts を IaC する方法の紹介をしてきました。 一部サービスはまだ多少こちらでカスタムリソースを使って管理しなければならないことが見てとれると思います。 カスタムリソースを利用するには多少 AWS API を利用して AWS リソースを作成することに習熟しておく必要があり、また AWS CDK でカスタムリソースを用意する知識が必要なため、ハードルは高めです。 そのため、現状では AWS CDK で Outposts をやるのはまだ難しいところがあります。 しかし、少しずつ改善はされ、以前はできなかったことができるようになってきたりなどしているので、今後も状況の改善がなされていくことが期待できます。 https://aws.amazon.com/jp/cdk/ ↩ これ以外にも patterns というものがあります。こちらはハイレベルコンストラクターのものをまとめて、一般的に利用されるシーンのものに合わせてさらに抽象度を高めています。例としては定期的に実行する Fargate Task を定義する ScheduledFargateTask という pattern が提供されています。 ↩ AWS の新サービスだとローレベルコンストラクターが提供されているがハイレベルコンストラクターは提供されてないという場合があったり、ハイレベルなものが提供されているものの、設定したいものがハイレベルだと設定できないということがあります。そういう場合にローレベルコンストラクターを使います(実はハイレベルコンストラクターのプロパティには node というプロパティが生えており、そこから実際にバックで利用しているローレベルコンストラクターにアクセスできるのでそちらから一部設定値をいじることができたりします)。 ↩ ここでは @types/aws-lambda パッケージを利用して型付けしています ↩ CDK だと aws-cdk-lib/custom-resources の CustomResource コンポーネントに生えてる getAtt メソッドが対応する関数になります。 ↩ 他にも数値型として参照できたりします。詳しくは ドキュメント をご覧ください。 ↩ https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.Subnet.html#subnetoutpostarn ↩ AWS::EC2::Subnet の MapPublicIpOnLaunch ↩ customer owned IP address については前回の記事をご覧ください。 ↩ 後で設定はできるので、デプロイ後に自分で設定するといったことはできるのですが、ここではこれも自動管理する話をしています。 ↩ blockDevices の型 ( aws-cdk-lib/aws-ec2 の BlockDevice インターフェース ) を辿っても Outpost ARN を指定するためのプロパティが表れないため、指定できないということがわかります。 ↩ https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_elasticloadbalancingv2.ApplicationLoadBalancer.html#vpcsubnets ↩ https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-loadbalancer.html ↩ subnet と同じように ModifyLoadBalancerAttributes で customer owned IP address を設定できるか確認したのですが、現状では設定できないようなので、そもそも作成段階からカスタムリソースで管理する必要があります。 ↩ https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3outposts-readme.html ↩
はじめに こんにちは、イノベーションセンターの福田・鈴ヶ嶺です。 普段はクラウドサービスをオンプレ環境でも同様のUI/UXで使用を可能とするハイブリッドクラウド製品の技術検証をしています。 本記事は、今回日本で初めて導入した AWS Outposts ラックの仕様、導入方法、利用方法について徹底解説します。次の画像は、実際に導入した AWS Outposts ラックの画像です。 NTT Com では「Node-AI on AWS Outposts」に関するニュースリリースを2022年3月14日に発表いたしました。 今後は「Node-AI on AWS Outposts」を軸に、自社の多様なサービスやソリューションと組み合わせることで、AWSをご利用されるお客さまのデジタルトランスフォーメーションを支援していきます。 国内初、「AWS Outposts」に自社データ分析ツールを組み込んだソリューションを開発 AWS Outposts ラックとは AWS Outposts ラックは AWS が出しているハイブリッドクラウド AWS Outposts ファミリーの一部であり、42Uの専用ラックをデータセンターなどのオンプレ環境に設置して使用します。 以降は簡易に Outposts と記述します。 Outposts は、 AWS のサービスを オンプレミス上で Public AWS と同じ体験で 実現してくれるものです。 実際、各種 API も展開先の Outposts ARN を指定したりするプロパティが追加される等、 Outposts で利用したいリソースと Public 上の AWS で利用したいリソースとの間で API が分離されているということは(一部を除いて)ありません 1 。 つまり、慣れた AWS API の中でデプロイ先 Outpost 筐体を指定すれば Outpost 筐体上でリソースが展開されるようになっています。 ただし、AWS Outposts 上で展開されるサービスは AWS のフルセットではなく、サブセットになります。 具体的には、 EC2, S3, EKS などの AWS サービスが実行可能です 2 。 Outposts は提供されるハードウェアも含めて AWS によるフルマネージドな管理がなされます。 これによって従来のオンプレシステムに比べて管理稼働の削減が期待されます。 例えば、Outposts 筐体のハードウェアが故障した際には AWS がハードウェア故障の調査をしてくれます。 ユースケース Outposts の主なユースケースは次の通りです。 Public 上の AWS だとネットワークのレイテンシーが大きくなって要件に合わないような環境で、近接環境に AWS Outposts をデプロイすることで、レイテンシーを低減しながら AWS サービスを利用する データレジリエンシーの観点からクラウドにデータを流せないような地域でも AWS Outposts をデプロイすることでデータを特定の地域に留めつつ AWS サービスを利用する 外出しできないようなデータがあるような場所に AWS Outposts をデプロイすることでデータを中に留めつつ AWS サービスを利用する オンプレ環境に Outposts をデプロイし、一部サービスをそちらに移行することで段階的にクラウド化を実施する Outposts を使えばこれらの要件を満たしつつ、クラウドサービスの恩恵を受けることができます。 例えば、次のような環境の場合、 Public 上の AWS を利用するには Public なインターネットを通したり、 AWS の提供する網内を通るためにクラウドサービスを利用できませんでした。 セキュリティ保護要件上データはオンプレ上に保管 オンプレ環境からのみデータへのアクセスを可能にしなければならない AWS Outposts を利用すれば、このような環境でもオンプレ内にデータを留めたまま EC2 インスタンスでそのデータを処理するといったことを実現できます。 AWS Outposts の立ち位置 運用面ではデプロイ先リージョン内の AZ へ紐付く形になります。 例えば ap-northeast-1 リージョンにデプロイし、 ap-northeast-1a に紐付けるという形で運用します。 形としてはある AZ が AWS Outposts によって拡張されるようなイメージです。 したがって、 Outpost 筐体を利用する場合はリージョンだけでなく AZ のレベルを意識しておく必要があります。 データの暗号化 Nitro システムを搭載しているため、 Outpost 筐体内のデータは全て暗号化されています。 Public AWS との接続にも Service Link を介して接続 されており、転送時にもしっかり暗号化されます。 AWS Outposts には Nitro Security Key という機器が設置されており、その機器がデータの暗号化を担当しています。 復元もこの機器が担当しているため、この機器を破壊すると内部のデータの復元は困難となります 3 。 Outposts 導入要件 Outposts はここまで述べたように、従来 Public クラウドで利用が難しかった局面でもクラウドサービスを利用できるようにしてくれるサービスです。 そんな夢のようなサービスですが、 Outposts 導入には幾つかの要件が存在します。 AWS Direct Connect を用いた AWS リージョンの接続 専用線を用意するには準備に多くの稼働や時間が必要となります 接続するネットワークの帯域も AWS から指定されています ラック、ネットワークに関する責任 Outposts ラックの物理的なセキュリティの責任を負います Outposts へのネットワーク接続の一貫性の責任を負います 今回、私達が実際の構築にあたった際には社内で提供しているサービスを利用して構築しました。 これによって要件を満たしつつ、迅速に AWS Outposts を利用することが可能となりました。 NTT ComによるOutpost導入方法 AWS Direct Connect を用いた AWS リージョンの接続 Outposts は AWS リソースとの連携を可能とするために AWS リージョンとの接続が必須となります。 この接続は、 Service Link と呼ばれ AWS Direct Connect を利用した専用接続が必要になります。 データ伝送速度は 1Gbps 以上が推奨されています。 通常 AWS Direct Connect などの専用線を用意するには多くの稼働や NW 機器などのハードウェアの準備が必要となりますが、 NTT Com の次世代インターコネクトサービス Flexible InterConnect(FIC) の FIC-Connection AWS を用いることで迅速に安定した接続を可能とします。 また、 FIC は最大 10Gbps の広帯域接続にも対応しているため高品質なネットワークを提供できます。 次の画像はFICのポータルです。このように専用の物理的な拠点とAWSの閉域接続をポータルで設定・管理することを可能とします。 ラック、ネットワークに関する責任 Outposts の物理的なラックの安全性は利用者側で責任を負います。 導入には重量・電力制限などの様々な設備に対応する必要があります。 これらの課題に対して NTT Com のデータセンターサービス Nexcenter を利用することで対応できました。 Nextcenter では、グローバル統一の「設備基準」や「サービス運用基準」に準拠することで高品質なサービスを提供できます。 また、ネットワークに関してはデータセンター内接続、 FIC による AWS リージョン接続などのそれぞれの接続を冗長設計することにより安定した運用が実現可能です。 オンプレミスネットワークとの統合 オンプレミスネットワークとは Local Gateway を通して接続されます。 この Local Gateway へ与えられたルートテーブルに VPC を関連付けることで、その VPC 内のリソースとオンプレミスネットワークが通信可能な状態になります 4 。 ただし、これだけでは各種 Outposts 内の AWS リソースへ通信はきません。 こちら側から IP アドレスプールを提供し、 Outposts 内の各種 AWS リソースに適切にその IP アドレスプール内にある IP アドレス 5 を付与する必要があります 6 。 例えば、 EC2 インスタンスであれば ENI にその IP アドレスプールから IP アドレスを付与し、その ENI をアタッチしなければオンプレミスネットワークと接続はできません。 もし VPC と Local Gateway の関連付けを行わなかった場合、例え与えた IP アドレスプールから ENI ヘ IP アドレスを払出していてもオンプレミスネットワークと通信できません。 これは Local Gateway のルートテーブルのみがオンプレミスネットワークの内容を知っていて、 AWS の提供する VPC 側は AWS Outposts の存在を知らないからです。 接続したければ、こちらから接続先を明示してあげる必要があるというわけです。 提供されるサービス Outposts 上では様々なサービスを展開できますが、そのうち私達が試した Outposts 上で使用できるサービスについて解説します。 VPC と subnet VPC は Local Gateway のルートテーブルを紐付ければ良いだけで他に設定は必要ありません。 一方、 subnet の方だけ Outposts 上で利用する設定が必要です。 subnet を作成する CreateSubnet API を見ればわかりますが、デプロイ時に Outpost ARN を指定することでその Outpost ARN を持つ Outpost 筐体上にデプロイされます。 注意点として、マネージドコンソール上から作成するには Outposts コンソールから行う必要があります。 現在の VPC コンソール上における subnet の作成画面には Outpost ARN を直接指定するインターフェースがないため、 VPC コンソールからは AWS Outposts 上に subnet を作成できません。 必ず AWS Outposts コンソール上から作成する必要があります。 使用感は Public 上の AWS へデプロイする普通の subnet と変わりません。 管理も AWS マネージドコンソール上からも可能です。 唯一、 subnet 上にのっているリソースだけ(設定していれば) Local Gateway を通してオンプレミスネットワークと疎通できるところが違います。 EC2 インスタンス EC2 インスタンスを立ち上げる API を見ていただければわかるのですが、 EC2 インスタンスの操作自体は Outpost 上へデプロイする設定を持ちません。 代わりに、 Outpost 筐体上にデプロイしている subnet へ EC2 インスタンスをデプロイすることで自動的に Outpost 筐体上へインスタンスがデプロイされる仕組みになっています。 このインスタンスをオンプレミスネットワークと通信させるようにするには こちらが提供する IP アドレスプールから IP アドレスを割り当てられた ENI Local Gateway へのルーティング がそれぞれ必要になります。 使用感は Outpost 上へデプロイしない他のインスタンスとまったく同じで、こちらも AWS のマネージドコンソールから管理が可能です。 Outposts 上で展開される EC2 で利用可能なインスタンスタイプについては Outpost 筐体注文時に指定します。 現在は m5, g4dn, c5, r5 などが指定可能です。 EBS Volume CreateVolume API を見ると Outpost ARN を指定できるようになっていることが確認できます。 他の EBS Volume とは指定された Outpost 筐体上にデプロイされているところが違うだけで、 Public 上の AWS にあるインスタンスからもマウントでき、シームレスに連携できます。 S3 on Outposts Outposts 上の S3 は S3 on Outposts と呼称されていますが、 S3 と S3 on Outposts は操作感が異なります。 オブジェクトを put する、などの操作にひと手間必要 発行されるアクションが S3 と一部異なる AWS CLI の high level な操作 ( copy 等) は使えない AWS マネージドコンソールからバケット内のオブジェクトが確認できない などの違いがあります。 バケットへの接続 S3 on Outposts では以下のものを VPC に設置しなければバケットへ接続できません。 access point Outposts endpoint これらリソースを設置した上で、それらリソースが設置された VPC からのみアクセスが許可されています 7 。 このように現在の S3 on Outposts を利用するには Public 上の S3 とは違ってひと手間必要になります。 こちらでも使用感がより良くなるよう気になるところは適宜フィードバックした上で改善を待っています。 皆さんも使用した上で気になる部分があれば AWS へフィードバックをお願いします。 API さて、肝心の S3 on Outposts のデプロイについてですが、 S3 とは API が 別 です。 Public AWS 上の S3 にバケットをつくるには Amazon S3 の CreateBucket API です。 一方、 S3 on Outposts にバケットをつくるには Amazon S3 Control の CreateBucket API です。 そもそもからして API が異なります。 他のサービスは同じ API 内にデプロイ先 Outpost 筐体を指定すればよかったのですが、これだけ例外で他のものと API レベルから分離されています 8 。 EKS Outposts 上で Amazon EKS ノードを実行しオンプレ環境でマネージド k8s サービスの利用を可能とします。 マネージド形ノードはサポートされておらず、自らEC2を作成してクラスターにノードとして登録するセルフマネージド型ノードのみがサポートされています。 セルフマネージド型ノードは AWS Outposts にデプロイできますが、マネージド型ノードや Fargate ノードにはデプロイできません。 https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/eks-on-outposts.html また、コントロールプレーンである EKS クラスターは Public 上の AWS における subnet 上に作成する必要があります。 クラスターの作成時に、 Outposts サブネットを渡すことはできません。 https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/eks-on-outposts.html Application Load Balancer Outposts 上で Application Load Balncer(ALB) をデプロイするには c5/c5d, m5/m5d, r5/r5d インスタンスが必要となります。 また、オンプレミスネットワークから接続する場合はこちらが払出す IP アドレスプールから IP アドレスを割り当てる必要があります。 An Application Load Balancer can be deployed on c5/c5d, m5/m5d, or r5/r5d instances on an Outpost. The following table shows the size and EBS volume per instance type that the load balancer can use on an Outpost: 使用感としては普通の ALB と変わりません。 AWS 上のマネージドコンソールからも管理可能です。 容量 Outpost 筐体上に展開できる EBS や S3 ストレージのサイズ、 EC2 のインスタンス数の制限などは当然デプロイしている Outpost 筐体の物理的制約に依存します。 消費しているリソースや、空き容量といったものは Outposts コンソールからデプロイしている Outpost 筐体毎に確認できます。 使用できる空き容量がない場合、容量のないリソースのデプロイはできなくなります。 容量がない時にデプロイしようとするとデプロイは空きがでるまで待たされます。 ちなみに、 EC2 インスタンスの容量上限はインスタンスタイプ毎にあるので、 m5 と g4dn をそれぞれ利用できるようにした Outposts 筐体における EC2 インスタンスの上限は m5 と g4dn で別々です 9 。 まとめ 本記事では Outposts について NTT ComによるAWS Outposts ラックの導入方法について オンプレ環境との統合について Outpostsで提供されるサービスについて について記載しました。 Outposts はその使用感を Public 上の AWS と同じにしつつ、従来は難しかった場面でもクラウドサービスを利用できるようにしてくれる素敵なサービスであることが見ていただけたかと思います。 細かいところで注意すべき点があったりもしますが、それらを差し引いても AWS サービスをオンプレ内で、しかも外にデータを出さずに管理できるというところは魅力あるところです。 データ利用の壁があり、今まで Public クラウド上にあった AWS サービスを利用し辛かったところはこれを機に AWS Outposts の利用を検討してみてはいかかでしょうか? 例えば、サブネットを作る CreateSubnet API にデプロイ先 Outpost 筐体を示すための OutpostArn というプロパティが追加されていますが、 Outposts 専用に API は切られておらず、 Public のものと同一です( https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CreateSubnet.html#API_CreateSubnet_RequestParameters )。 ↩ 提供されるサービスについての詳細は 公式ページ へ譲ります。 ↩ 「Q: データセンターに設置された Outposts ラックの物理的なセキュリティに対する責任を負うのは誰ですか?」 https://aws.amazon.com/jp/outposts/rack/faqs/ ↩ デフォルトでは VPC と Local Gateway に与えられたルートテーブルは関連付けられていません。 ↩ この IP アドレスプールのことを customer owned IP address または 顧客所有の IP アドレス , CoIP と呼称します。 ↩ customer owned IP address を持たせることは AWS API からはできるようになっていますが、一部 CloudFormation コンストラクターについては未対応です。そのため、 CDK 等を利用して IaC をやるのは現状では少し難しいです。これについては次回以降説明します。 ↩ https://docs.aws.amazon.com/AmazonS3/latest/userguide/S3OutpostsNetworking.html ↩ オブジェクトの操作 API 名はかわりませんが、 API に指定するバケット名は Public 上の S3 とは異なり、 S3 on Outposts 上のバケット名ではなく、バケットへの接続を確保している access point の ARN になります。 ↩ ただし、 EBS 容量は全てのインスタンスタイプで共通してるので、そちらの上限にひっかかる可能性はあります。 ↩
みなさんこんにちは、社内のエンジニアが働きやすくすることを目標にする Engineer Empowerment プロジェクトの  @Mahito です。 先日 NTT グループのソフトウェアエンジニアを対象とした Git / GitHub の研修を NTT グループのエンジニア有志で行ったので、そのことについてお話しします。 ちなみに、以前に 社内のソースコードをGitHub Enterprise にとりまとめてる話 という記事も書いたことがありますので、興味があればそちらもご覧ください。 背景 本研修のきっかけは、ソフトウェアエンジニアの育成に関して NTT グループ内のエンジニアたちと議論が盛り上がったことです。 現在 NTT グループにはグループのエンジニア有志の集まる非公式なコミュニティがあります。私は不定期に Meetup を開催しているのですが、そこで「 ソフトウェアエンジニア育成 」をテーマにした議論をした際に以下のような意見がありました。 NTT グループ各社でのソフトウェアエンジニア育成の状況は様々だが、上手くいっている会社は少ない 上手くいっている会社や高スキル者のノウハウを NTT グループに横展開することで、グループ内のソフトウェアエンジニアの技術力を高めたい また、タイムリーに 「チームメンバに Git / GitHub を教える必要があるが、どうすればわかりやすく教えられるか」 という話題が上記のエンジニアコミュニティ内で話題になっていました。 最近のソフトウェアエンジニアの必須スキルとして Git / GitHub が挙げられるものの、最初に使うまでの一歩が難しいスキルかも知れないということで、 Git / GitHub を利用したことがない 、または なんとなく使ってみたもののあまり理解できていない という人を対象にした本研修を企画しました。 研修について 今回の研修は、バージョン管理システムである Git と、ソースコードホスティングサービスを提供する GitHub の初歩的な利用方法について、講義とハンズオンを通して理解してもらう形で行いました。 「何を教えるのか」、「研修のゴールをどうするのか」というところを研修スタッフの間で何度も話し合った結果、Git / GitHub で基本となる操作について、実際に手を動かして理解してもらうことにしました。これは初心者の方にはまずは基本をしっかり理解してもらうことが大事だと考えたからです。 以下は実施した研修の内容です。 研修は、日本電信電話(NTT)の研究所、NTTドコモ、NTT Com のエンジニアが講師とサポーターを担当しました。サポーターの役回りはハンズオンのサポートや質問への回答で、私もサポーターとして研修に参加しました。 また、事前の研修企画や取りまとめは私が行いましたが、NTT グループへの周知についてはエンジニアコミュニティや NTT の人事、技術企画の方々の力をお借りしました。 参加募集を始める前は 「50 人ぐらい来ればいいよね」とスタッフで話をしていたのですが、募集開始から 1 週間経たずに 100 人近い応募がありこれ以上の人数はサポートしきれないと慌てて応募フォームを閉め、後日同様の研修をもう一度行いました。 おかげで2回の研修で 170 名ほどに参加してもらい、 Git / GitHub について学びながら触ってもらうことができました。 参加者には事前アンケートに回答してもらっていたため、回答内容からこの研修の参加目的を確認したのですが、以下のような理由が多かったです。 現在バーション管理をしていない文書などをバージョン管理していきたい 現在 Subversion を利用しているが、Git への乗り換えを検討している Git / GitHub の基礎が知りたい 独学で Git を学んだが、GitHub を利用した開発フローを学びたい NTT グループの中でソフトウェアを扱う会社も多くありますが、一方でこれからソフトウェアに関わっていく会社も多く、NTT グループの中でソフトウェアエンジニアの数はまだまだ多いと言える状況ではありません。 しかし、こうした研修を用意することで、これからソフトウェアに関する技術を学ぼうとする方が多くいることを知ることができました。 また、参加者の中にはエンジニアではなく営業やマネージャー職の方が 1 割程度参加しているのも見受けられました。 研修当日の様子 2 回開催した研修は全てオンラインで行いました。 事前アンケートでは参加者の約 1/3 が Git や GitHub を利用したことがないという状況でしたので、研修は時間を多めにとって2時間〜2時間半をかけて行いました。(1 回目のアンケートでペースが早いという声が多くあり 2 回目は時間を延ばしました。) 研修環境については事前に設定資料を配布し設定をお願いしていたことや、なにか問題があった人や質問がある人については Slack でサポーターが対応をしていたため、特に大きな問題はありませんでした。 当日の質問では、研修内容についての質問や、Git / GitHub のユースケースやベストプラクティス、社内でどのように使っているのかなどの質問がありました。 研修後アンケート 研修の満足度(2回目アンケートより) 研修後のアンケートでは、回答してくれた方の多くが満足する形になりましたが、やはり初心者向けの研修だったということもありすでに Git / GitHub を触ったことがある人にとっては物足りない研修だったという声もありました。 研修の難易度(2回目アンケートより) 研修の難易度もアンケートを見る限り初心者向けとしてはいいレベルで実施できたのではないかと思います。 参加者の声 研修に参加した方からは以下のようなコメントをいただきました。 概念の話と実技をバランスよく教えていただき、非常にわかりやすかった ファイルの変更内容が履歴として残るのはあらゆる作業や情報共有で役に立つと思う 今回の学びを他の初心者にシェアできると感じた 改めて基礎がわかりました。応用編も開催してほしいです 一方不満としてはこのような声がありました。 conflictまでは大体よく使うので、もう少し応用例が欲しかった 現状 Subversion 文化、Excel 文化であり、既存プロジェクトへの導入については変更する利点を伝えるコストや教育コストの方が大きいように思われる また、次回以降どんな研修を期待するかという質問については以下のような様々なコメントをいただきました。 Git のより高度な使い方や応用例 アジャイル開発 クラウドの利用方法 機械学習 セキュリティ etc... 今回の研修では参加者に 「Git / GitHub で基本となる操作について、実際に手を動かして理解してもらう」 というテーマで行いましたが、アンケートからこれは達成できたのかなと思いました。 一方で、 Git / GitHub に対する発展的な研修や、それ以外の技術に関する研修への期待や要望というものも明らかになりました。 今回のアンケート結果は NTT の人事などにも共有をして、NTT グループの中で技術研修が求められていることを伝えています。 また、現在こうした意見に応える形で、NTT グループのエンジニアコミュニティの力を借りながら、次の研修の企画を進めています。 まとめ 今回の取り組みは、NTT グループのエンジニア有志が NTT グループのエンジニアの声を拾いつつ、主体的に研修の企画・開催することで、エンジニアの成長を助けるような取り組みのきっかけを作ることができました。 今回は Git / GitHub を対象とした研修になりましたが、アンケートでは様々な技術についての研修を開催してほしいというリクエストもいただいており、現在次の研修をエンジニアコミュニティの面々が企画をしてくれています。 上述した Git / GitHub の発展的な研修も含め、NTT グループのソフトウェアエンジニアが互いに持っているノウハウを共有することで、NTT グループや日本のソフトウェアエンジニアリングをもっと盛り上げていければと考えています。
はじめに はじめまして。イノベーションセンターの西野と申します。セキュリティ運用改善サービス「Metemcyber」のプロジェクトリーダーを担当しています。 本記事は、NTTコミュニケーションズの職場体験型インターンシップに参加していただいた学生お二方からの寄稿になります。どうぞよろしくお願いいたします。 今村さん はじめまして。今回Metemcyberプロジェクトに配属されたインターンシップ生の今村(GitHub: apeiria-zero )です。現在大学3年生で、大学ではBitcoinを用いたマイクロペイメント技術や、Hashed Time Lock Contract(HTLC)に関する研究をしております。 私はこれまで大規模な開発経験が無く、ほとんど個人での開発経験しかありませんでした。ですので、実際の開発がどのようなものか気になっていました。大規模開発に関われる機会を探していたとき、NTTコミュニケーションズの職場体験型インターンシップの存在を知り、よりリアルな開発経験を積むことができると思い応募しました。 木村さん はじめまして。今回Metemcyberプロジェクトに配属されたインターン生の木村(GitHub: keigokimura )と申します。現在、大学院でブロックチェーンのセキュリティ、NFTの不正取引に関する研究を行なっています。 最近やたらとニュースになっているNFTについて、自分は研究で扱っているけれど、企業ではどのように捉えているのか?そもそも新しい技術に対し、どういう切り口で関わっていくのか?ということが気になっていました。 そんな中、NTTコミュニケーションズのインターンシップのコースの中に、NFT技術を用いた研究開発を行うコースを見つけました。これは企業の方の考えを知るチャンスであり、「自分の研究が企業でどう生きるのか」「企業での研究ってどういう視点で技術を捉えるのか」といった自分の疑問を明らかにする機会だと考え、チャレンジすることを決めました。 インターンシップの概要 今回私たちは、2週間のインターンシップに参加し、イノベーションセンターでセキュリティ運用改善サービス「Metemcyber」の研究開発を行いました。 Metemcyberとは ここでは、Metemcyberのサービスの目的について説明します。 www.metemcyber.ntt.com Metemcyberの目的は、組織におけるセキュリティ運用の健全化を目指すことです。一般的なセキュリティ運用改善サービスと異なる点は、「食生活改善のようなセキュリティ運用の改善」を行える点にあります。 食生活の改善には、日々の食事記録を管理し、栄養バランスなどを元に食生活における問題を見える化することが必要です。さらに、見える化された問題に対し、健康栄養士の方からのフィードバックをいただくことができれば、ユーザはそれを元に食生活を改善していくことができます。 食生活に関していえば、これを実現するサービスとして「 あすけん 」と呼ばれる有名なサービスがあります。しかし、セキュリティ運用において、このようなサービスは現状存在していません。 Metemcyberは、食生活改善のようなセキュリティ運用の健全化を提供することを目標とします。サイバー攻撃のインパクトが大きくなる中、実際のサイバー攻撃を踏まえた正しいセキュリティ対策を講じ、健全に運用できているかどうかは、組織の信頼に関わる大きな問題となります。しかし以下の理由から、組織のセキュリティ運用が健全に行われているかを把握することは難しいと考えられます。 セキュリティ対策はその貢献度や大変さが成果として見えにくい 定期的な監査では日常的な運用が健全なのか分からない 脅威の発見や対策の根拠となる情報が十分だったのか把握できていない そこでMetemcyberは、食事記録のようにサイバー脅威情報の利用を記録し、それに対するセキュリティアクションを管理することで、組織におけるセキュリティ運用の健全性を見える化します。また、見える化によってアクションの達成度が明らかになるため、アクションを行った組織あるいはエンジニア個人を評価できます。Metemcyberはアクションを実行した人にバッジを付与することで評価します。 実際にMetemcyberは図1の3つのコンポーネントから構成されています。 図1 Metemcyberを構成する3つのコンポーネント セキュリティ運用の健全化を見える化するだけでなく、頑張った人を評価できることも、セキュリティ対策へのモチベーション向上に繋がりそうです。 開発環境や期間中のコミュニケーション ここでは実際に今回のインターンで行った内容を説明します。インターンでは、主にFastAPIというWebフレームワークを利用したAPI開発を進めました。 全日リモート形式での実施 インターン前日に参加のための備品が到着 NTTコミュニケーションズから社内連絡用のPC インターン期間の昼食代が入金されたカード Metemcyberの開発自体はGCP上に用意されたVM環境でVScodeからアクセス チームの方々とはSlackでやり取りをし、ソースコード管理にはGitHubを利用 図2 GitHub上のやりとり また、昨日の進捗と本日の予定を報告し合う「朝会」というミーティングに毎朝参加させて頂きました。 他にも様々なミーティングに参加させていただき、現場のリアルを知ることができました。 今村の成果:アクションの記録からメタデータ取得まで Threat Connectome開発の背景 今村が担当したアクションログの生成の部分について説明します。アクションログの内容の前に、Threat Connectomeを改めて説明します。Threat Connectomeとは、「セキュリティアクションを頑張った人をほめたたえたい」をコンセプトに開発されたプラットフォームです。 セキュリティアクションをもとにバッジを発行し、セキュリティアクションを見える化することで貢献を評価します。 図3 Threat Connectomeの目的 アクションログの概要 アクションログというのは、セキュリティアクションの情報やそれを実行したユーザー情報等を含むログの名称です。今回今村が担当した業務は、「セキュリティアクションの記録」と「バッジ発行に必要な情報を生成」する機能の開発です。大まかな全体像は図のようになります。アクションログを生成する create_log 、アクションログ一覧を取得する get_logs 、メタデータを生成する get_metadata という3つの関数があります。まずはアクションログを生成する create_log を説明します。 図4 actionlogsの全体像 アクションログの生成(create_log) create_log の目的はセキュリティアクションからアクションログを生成することです。入力として、アクションID、トピックID、ユーザーIDと実行時刻(入力は任意)があります。これらの情報をもとにアクションログを生成します。 図5 アクションログ生成の流れ 図6 アクションログの内容 アクションログ一覧の取得 get_logs の説明です。先ほどの create_log で作成したアクションログの一覧を取得します。(図7) 実行時刻( executed_at )と、レコード作成時刻( created_at )のタイムゾーンがUTCであることを末尾に情報をつけることで明確化しています。また、デフォルトのソートはlatest順、つまりレコード作成時刻( created_at )の新しいレコードが一番上へ来るようにしています。 図7 アクションログ一覧 バッジ発行に必要なメタデータの生成 最後にアクションログからバッジ発行に必要なメタデータを生成する get_metadata の説明です。アクションログのロギングIDをパラメータとし、バッジ画像やバッジ名が生成されています。imageにバッジ画像のURLがありますが、アクセスすると画像が表示されます。実際のレスポンスは図8のようになります。 図8 メタデータの内容 アクションログまとめ 以上が私の活動でした。まとめとして図9を載せます。セキュリティアクションからメタデータの生成ができたので、この情報をバッジ発行のAPIへ渡します。インターン前はセキュリティアクションとバッジ生成のAPIが分離していたので、そこを繋げる事が出来ました。以降の処理は木村さんの担当に移ります。 図9 アクションログからメタデータ取得まで 木村の成果:バッジの発行からNFT化まで 3. バッジの生成 セキュリティバッジの発行 次に木村が担当したバッジ生成の部分について説明します。Metemcyberではセキュリティアクションを実行した人にバッジを付与することでセキュリティ運用者を評価します。今回は前節で説明があった、セキュリティアクションのログ情報からそれに応じたバッジを発行します。 まず、Threat Connectome内で発行されるバッジをセキュリティバッジと呼び、メタデータからセキュリティバッジを発行できる機能を作成しました。APIとしては以下のようなものを用意し、発行したバッジには固有のIDが付与されます。機能としては図10の2つを用意しました。 図10 Threat Connectome セキュリティバッジ関連の機能 入力としてアクションに関するメタデータを渡してあげると、結果として図11の情報を持ったセキュリティバッジが発行されます。内容としてはそのアクションの実行時間や概要など、アクションに関する情報などが入っています。 図11 セキュリティバッジの内容 セキュリティバッジのNFT化 これでセキュリティアクションを実行した人をバッジによって評価が可能になりました。次に、バッジを受け取った人が自分の成果として示せるように、ブロックチェーン上にセキュリティバッジをNFTとして発行する機能を実現します。最近ではブロックチェーン上でアート作品やデジタルコンテンツがNFT化されて売買されており、NFTは注目を集めています。そんな注目されるNFTとしてセキュリティバッジを発行でき、注目の的になれば、エンジニアの勲章としてすごく良さそうです。 NFTに関する機能としては図12の2つを用意しました。 図12 Threat Connectome NFT関連の機能 入力としてセキュリティバッジのIDを渡してあげると、セキュリティバッジがNFTとして発行されます。図13には発行した結果を示しており、NFTの共通的な情報と、今回のユースケース固有の情報に分かれています。 図13 NFTの内容 また、今回はセキュリティバッジとNFT化されたバッジの紐付けも行っています。セキュリティバッジのフィールドの中に nft_id というものがあり、そのセキュリティバッジがNFT化されていれば、NFTの固有IDが入るようになっています。今回は上記で発行したセキュリティバッジをそのままNFT化しました。図14においてセキュリティバッジの一覧取得機能を用いて再度確認してみると、先ほどは null だった nft_id に値が入っており、しっかり紐付けが行われていることがわかります。 図14 セキュリティバッジ内のNFT ID NFTバッジを手元のMetaMaskにインポートしてみる 最後に自分が手に入れたセキュリティバッジをMetaMaskにインポートしてみます。NFTバッジの情報をもとに自分のMetamaskに登録していきます。図15のように表示できていることがわかります。これだけで達成感が湧いてきますね。 図15 NFTをMetaMaskに登録 4. インターン通しての成果 今回のインターンを通して、図16の流れでセキュリティアクションからバッジを発行する機能を実現しました。 図16 インターンを通しての成果 NTT コミュニケーションズのインターンを終えた感想 今村からの振り返り 今回私はこれまでやってきた個人レベルでの開発と、企業レベルでの大規模な開発がどのような点で違うのかを体験したいと思いインターンシップに参加させていただきました。約2週間NTTコミュニケーションズの現場で開発作業を行いましたが、最も重要だと感じた点はチームで決めた方針に沿いながらコードを書いていくことです。 あたりまえの事かもしれませんが、ずっと個人で開発してきた私はかなり苦労しました。個人の開発では個人のアウトプットに最適化すればいいのですが、チームの開発ではチームのアウトプットに最適化しなければなりません。きちんとチーム内でコンセンサスを取り、他の方が担当してる箇所に影響が出ないか。それを吟味しながらの開発は想像以上に重要であり、個人開発ではなかなか意識できない体験ができました。 今回のインターンシップを通して、現場で働くプロの方々の開発の進め方を少しでも自分の中に落とし込んで今後のエンジニア人生に活かしていきます! 木村からの振り返り 今回自分は大学院で行う研究と比べて、企業ではどのような視点で研究開発を行っているのか、自分の研究を将来企業でどのように役立てることができるのかを知りたいという思いでインターンシップに応募しました。そんな思いで2週間Metemcyberの開発に携わる中で、企業での研究開発は常にユーザを見据えて開発を進めることで、そのために必要であり最も有力であると考えられる技術を剪定するということを学びました。 インターン前は、企業の研究開発は新しい技術を使って何かできないかといった視線で開発していると思っていました。しかし、インターンシップの中で、ブロックチェーンやNFTは新しい技術だから研究開発しているのではなく、それが価値を生み出せるから使っていることを知りました。またそういう考えの上で、それぞれの技術が課題解決に対し本当に最適なのかはしっかり見極めなければならないと学びました。 そんな学びを得た上で、今はMetemcyberが今後広がっていき、MetemcyberのNFTバッジがエンジニアの評価の一基準になっている姿を想像するとワクワクしてきます。 今回のインターンシップを通して、そのプロダクトがどういったユーザを想定し、どういった価値を届けられるのかといった要件定義の部分については現場を見てしっかりと学ぶことができました。この経験を踏まえ、今後もエンジニアとして成長していきたいと思います! トレーナーからのコメント トレーナーの西野です。今村さん木村さん、お二人ともお疲れ様でした。 今村さんはエンジニアとしての初心を見つめ直す鋭い質問や、学生ならではの観点からチームの開発環境を見直す素晴らしい機会を提供してくれました。個人での開発とチームで行う業務の違いに戸惑った部分もあるかもしれませんが、見事克服して素晴らしい成果を残してくださったと思います。本インターンが、エンジニアとして羽ばたくためのお手伝いになったのであれば幸いです。 木村さんは優れた課題解決能力を活かし今回のインターンを牽引してくれた存在でした。研究開発業務においても、「プロダクトアウトやマーケットインを踏まえた実装がより社会貢献できるものにつながる」と肌で感じてもらえたことは、トレーナーとしても嬉しい限りです。その高い能力を発揮し、社会に貢献できる素晴らしいエンジニアとして邁進されることを祈念しております。 お二方の技術的な貢献を社会に役立つサービスとして世に出せるよう、Metemcyberプロジェクトでは引き続き研究開発を進めていこうと思います。ご参加いただきありがとうございます。
はじめに こんにちは、Things Cloud のカスタマーサクセスチーム 伊藤、佐々木、高橋、三橋です。私たちは、データプラットフォームサービス部で IoT プラットフォーム「 Things Cloud 」のサービス開発やお客様への技術支援を担当しています。 さて、皆さんはデータを見やすいようにグラフ化したり、表にまとめる時はどんな方法を使っていますか?最近では、JupyterLab や Python ライブラリの Pandas を利用されている方が多いかもしれませんね。私たちのチームでは、 Observable という Web サービスを利用しています。Observable を使うと、下図のようなカラフルで見やすいグラフを短時間で作成できます。 本記事では、Observable を活用した IoT データ可視化における探索と、私たちが携わっている IoT プラットフォーム「Things Cloud」との親和性について紹介します。 出典: https://observablehq.com/@d3/gallery Observable は Web ブラウザ上で動作する JavaScript ランタイムを提供するサービスで、ノートブックと呼ばれる単位でコードを管理できます。Observable のサイトにあるように、データを扱いながら、創る・コラボレーションする・学習するための場を提供しています。 IoTデータの可視化について Observable を活用した具体的な事例紹介へ入る前に、IoT の世界でデータを可視化することの重要性を説明します。 IoT の世界では、モノの世界とコトの世界の大きく2つの要素に分類できます。下図は簡略化した2つの世界のイメージです。 IoTはモノの世界とコトの世界から構成される モノの世界 モノの世界には、スマートフォンやトラクター、工場のラインなど管理対象となるモノや場所と、それらに取り付けて現実世界の情報を電子データに変換したりクラウドにデータをアップロードするセンサーやデバイスが存在します。モノの世界では、センサーデータだけを管理するのではなく、センサーを取り付けたモノや場所、またモノや場所同士の関係性を管理することが重要です。 (例: 【センサーと場所の関係】会議室Aに取り付けた人感センサA、【場所と場所同士の関係】ビルAの会議室A) コトの世界 コトの世界には、人間が IoT データを閲覧したり、モノの世界に対して制御するためのユーザインタフェースや、ユーザインタフェースを見る人間(エンドユーザ、業務オペレータ、業務の管理者など)が存在します。 クラウドに蓄積されたデータは単なるデータとしてではなく、何らかの意味付けをしてユーザが直感的に理解しやすい形に表現する必要があります。わかりやすく表現されたデータを見ることで、例えば工場を遠隔監視しているオペレータが、メンテナンスが必要な時に適切なタイミングで指示を出したり、機器の不調傾向を分析したりするなど業務上必要なアクションを実行するのに役立てられます。 様々なセンサーデータが存在する中で、オペレータやデータを見る人にとって IoT データを適切な見せ方に作り上げることを1ステップで済ますのは難しく、試行錯誤が必要な領域です。単なるセンターデータの折れ線グラフが最適な場合もあれば、ヒストグラムが適している場合もあったり、さらに視覚的に見せるためにバブルチャートのようなものが適している可能性もあります。 次の章では、主にコトの世界におけるIoTデータの最適な見せ方を探求する上で、学習コストが低く、かつスピーディにデータ可視化を試行錯誤できる方法として Observable を紹介します。 Observableの紹介 ここでは、Observableでのデータ可視化について簡単に紹介します。 Observableについて Observableは、ブラウザ上でJavaScriptをセル単位で記載し、グラフ等を容易に作成できるWebアプリケーションです。( JupyterLab のJavaScript版のようなイメージとなります。) JupyterLab同様、セル単位で実行できます。またJupyterLabと違い、セルの実行順序を意識する必要はありません。 Observableの概要に関しては以下が参考になります。 5分でわかる概要 Tutorial (日本語) また、Observable 公式が公開している Observable の使い方動画も参考になると思います。 グラフ表示を試してみる Observableではグラフの種類変更や閾値線の表示等を容易に行うことができます。例えば、Observableのグラフ表示メソッドである Plot を用いると、簡単にグラフを表示させることができます。 Plotに関する例については Observable Plot をご参照ください。 下記は棒グラフを表示させるセルとなっています。 Plot.plot の引数へはオブジェクト型の変数を指定し、その中へ Plot: Marks という、グラフを表示させる主体となる値を格納しています。 Plot.plot({ marks: [ Plot.rectY(aapl, Plot.binX({y: "count"}, {x: d => Math.log10(d.Volume), normalize: true})), ] }) 今回は棒グラフ(時系列)を表示させるので、marks内で Plot: Rect を指定しています。 ここでは、 Plot: Rect を例にグラフを表示させています。グラフ表示のために使用しているデータは、Apple株の1日あたりの取引量となります。 ここから試行錯誤の代表的な例として、グラフの種類変更及び閾値線を表示します。 グラフを変えてみる グラフの種類を変更するには、 marks 内へ指定する値を変更します。 棒グラフを表示するために Plot.rectY を指定していましたが、折れ線グラフを表示するように変更してみます。 折れ線グラフを表示させるには、 Plot.line を指定します。 以下では、 Plot: Line を例にグラフを表示させています。 グラフ表示のために使用しているデータは、先ほどと同様、Apple株の1日あたりの取引量となります。 Plot.plot({ y: { grid: true }, marks: [ Plot.line(aapl, {x: "Date", y: "Close"}) ] }) 閾値線を表示してみる 閾値線を表示するには、 marks 内へ Plot: Rule を追加します。 ここでは、y軸の値が100の箇所へ赤い閾値線を表示します。 Plot.ruleY の第一引数へ、配列型式で閾値線を表示させる値を指定し、第二引数へオブジェクト形式で色の指定等を行います。 Plot.plot({ y: { grid: true }, marks: [ Plot.line(aapl, {x: "Date", y: "Close"}), Plot.ruleY([100], {stroke: "red"}) ] }) Things Cloud 紹介 Things Cloud(R) サービスについて、簡単にご紹介します。 Things Cloud(R) は、NTT Communications の Smart Data Platform (略称SDPF) の中で、データ収集機能を提供するプラットフォームです。 センサーにつながる IoT デバイスからデータを収集し、蓄積できるクラウドサービスで、簡単な操作でデバイス情報を閲覧したりデータの可視化を行ったり、ユーザー管理/通知/デバイス管理等、IoTに必要な機能一式が揃っています。 Things Cloud Things Cloud の提供機能(明るい青の部分) データ可視化の原理原則について ここで、可視化するデータをどのような形にすればグラフに読み込ませやすいか、データを可視化する際にどのようなグラフで、またはどのような配色を選択すればより効果的に表現できるか データ可視化の原理原則について説明します。 クレンジングと前処理 一般的に、データを可視化、分析をする際には、元となるデータ(自身で収集したデータ、オープンデータなど)を目的の可視化や分析に適した形に処理する必要があります。データを適した形に処理するという工程は、「クレンジング」と「前処理」から構成されています。以降では、クレンジングと前処理について説明します。 出典: ©︎矢崎裕一 クレンジング クレンジングは、データの可視化や分析ができるように、最低限のデータ処理を行う行為です。主に、構造と品質の観点でクレンジングを実施します。 構造観点 データ形式を変える マトリックス形式のデータは扱いづらいため、リスト形式のデータへ加工する 参考: 整然データとは何か データセットの形を理解する 扱うデータセットが ロング型 もしくは ワイド型 かを理解する。必要であれば適した型へ変換する ロング型:データが増えると行が増える。基本的にロング型にしておくとデータ可視化・分析しやすい ワイド型:データが増えると列が増える。年度ごと/国ごと/都道府県ごとに列が増えるパターンが多い 参考: R for Data Science - 12 Tidy data 品質観点 基準にもとづき、現状の品質を把握する 正確性(適合性): データに表記の揺れはないか? 完全性: データに欠損はないか? 一貫性: データに不整合はないか? 信頼性(精度): データに誤りやノイズはないか? 重複度: データに重複はないか? 参考: データ品質の国際基準 ISO/IEC 25012:2008(en) 参考: データ分析に必要なデータクレンジングの方法 前処理 前処理は、目的の可視化や分析を実施するために、積極的にデータを処理、加工する行為です。例えば、以下のような場合はデータの前処理が必要です。 地図上にデータを描画したい。データに住所は存在するが、緯度経度の情報が存在しない → 住所やその他既存データと組み合わせて緯度経度を算出する前処理が必要 日本の人口を表すヒストグラムを表示したい → データから階級幅を計算する前処理が必要 可視化 クレンジングと前処理を行い整えたデータを、意図的に最適なグラフの種類や色を選択することで、より効果的に見せることができます。では、どのように可視化すればより伝わるかについて説明します。 分類 さまざまな状況によって最適なグラフを選ぶために役立つグラフの分類として Financial Times の Visual Journalism Team による Visual Vocabulary があります。このようなリソースを利用することで可視化するデータを表現する最適なグラフを選択するのに役立てることができます。 出典: https://www.ft.com/about-us 9つに分類されたカテゴリーの中で、どのような時にどう可視化するかを さまざまなグラフでそれぞれ説明されています。 その他にも、 visualizing.jp という Web サイトではデータ可視化にまつわる実践的な手法や実例が多く紹介されています。気になった方は是非ご覧ください。 Things Cloudでは Things Cloudのグラフ分類も、この原理原則に沿っています。 Measurementのような時系列データの表示では、時系列変化を表す折れ線グラフで一定期間内の変化を表す手法を選択しています。 カスタムウィジェット化した複数データの相関関係の表示では、相関関係を表す散布図グラフで2項目の関係を示す手法を選択しています。 色 データ可視化の際、色により意味や雰囲気を効果的に伝えることができます。データの性質により表現したいものに対応した色の使い方を適用すると、データの性質を損ねずに視覚的に表現できます。一方、順序づけのあるデータに定性的な色を使用すると大小の関係がわからなくなったりと視覚的な錯覚を起こし見誤らせる原因にもなりうるため、注意が必要です。色とデータの種類には以下の対応があります。 色 データ 効果 定性的 順序づけしないデータ (都道府県など) 上下関係がなくなる 連続的 順序づけするデータ (成績など) 上下関係を表現できる 分岐的 数値データのゼロなどを基準にして、増加と減少があるデータ (人口の増減など) 分岐を表現できる Things Cloudでは Things Cloudの配色についても、この原理原則に沿って効果的な色を選択しています。 Measurement のデータポイントのように順序づけする必要のない場合は、定性的な色を選択しています。 CO2濃度可視化でカスタムウィジェット化したヒートマップのように順序づけのある場合は、連続的な色を選択しています。 CO2ヒートマップは、 OPEN HUB でご覧いただけます。 gradient: {0.2:'green', 0.5:'yellow', 0.8:'orange', 0.9:'red'} この原理原則に従い、Observable で ヒストグラムを表示してみる 私たちは、このデータ可視化の原理原則に従い、Things Cloud に報告されるデバイスのセンサーデータ(今回はシミュレートしたデータ)を使って、Observable Notebookで エンジンスピードとエンジン警告の関係性を ヒストグラムで可視化してみました。 まずは、Things Cloud の センサーデータ は、品質観点ではデバイスのソフトウェアによりある程度担保されていますが、構造観点ではグラフに読み込ませやすい形ではないため、以下のようにセンサーデータのデータ構造を簡易化するようなクレンジングを行いました。 // Measurement 配列でのオブジェクトの階層を平易化。この構造になっていれば Observable での基本的な可視化が可能 // この時点でのデータ構造: [ {type: "com_EngineMeasurement", com_EngineMeasurement_speed_unit: "rpm", com_EngineMeasurement_speed_value: "1000", time: "..."}, {type: "com_EngineMeasurement", com_EngineMeasurement_speed_unit: "rpm", com_EngineMeasurement_speed_value: "2000", time: "..."}, {type: "com_EngineMeasurement", com_EngineMeasurement_speed_unit: "rpm", com_EngineMeasurement_speed_value: "2500", time: "..."} ] 次に、エンジンスピードとエンジン警告の関係性を描画する際に、一定のスピード幅における警告数といった前処理にあたる計算を行う処理が必要ですが、これは前述したPlotライブラリで前処理含めて描画できるため、以下のように前処理から可視化までを行いました。 Plot.plot({ y: { grid: true }, color: { legend: true }, marks: [ Plot.rectY(measurements, Plot.binX({y: "sum"}, {x: "com_EngineMeasurement_speed_value", y: "com_EngineMeasurement_warning_value", fill: "source_id"})), // 前処理(ここではPlotが行ってくれる) Plot.ruleY([1000], {stroke: "red"}) ] }); Plot.rectY() の第一引数に クレンジングした Measurement のオブジェクトの配列( [{measurement1}, {measurement2},...] の形式)を与えることによって、Plot ライブラリが自動的に階級幅などを算出、グラフを描画します。 Things Cloudへの転記 ここまで、Observable で、あれこれと試行錯誤しながら表示したグラフを そのまま Things Cloudで実現してみませんか? Things Cloud には、標準で搭載されていて簡単に表示できるIoTデータを可視化する ウィジェット に加え新しい可視化として、今回試行したヒストグラムのように表示したいグラフのウィジェットをカスタマイズして、追加し利用できます。 私たちは、Observable で Plotライブラリを利用して試行錯誤したNotebookのJavaScriptのコードをそのままウィジェットに転記できるようなカスタマイズ用のテンプレートを作成しました。 ご興味のある方は、Things Cloud の 開発者レポートで、 Observable を活用したIoTデータ可視化の探索からカスタムウィジェット開発までのチュートリアル を紹介していますので、ぜひ ご覧ください。 ※ Things Cloud のご利用には契約が必要です。ご利用を希望されるお客様は iot-info@ntt.com までお問い合わせください(お手数ですが@を半角文字に置き換えてください)。 おわりに いかがでしたか? IoTデータを取得した後の可視化で、Observable によりプロトタイピングし、ある程度固まったら Things Cloud にコードを反映というサイクル、とてもイメージが膨らみますね。メンバーも使ってみて、 Observable の対話性、Plot 等のライブラリのパワーに夢は広がり、興味が尽きません。 Things Cloud カスタマーサクセスチームでは、ますます広がっていく IoT ユースケースを広げ、より便利な使い方を開拓すべく内製開発を行っています。今後もIoTに関連する開発ネタを発信していきたいと思っています。 問い合わせ先 お手数ですが@を半角文字に置き換えてください。 Things Cloud サービスについて:iot-info@ntt.com Things Cloud カスタマーサクセスチーム:iot-app@ntt.com
はじめに 2月14日から25日までの2週間、NTTコミュニケーションズのインターンシップに参加させていただいた八木です。普段は大学院で画像処理の高速化に関する研究をしています。インターンシップでは技術コースのうち「 AI/MLシステムとの統合を志向した、メディアAI技術の研究開発 」ポストに応募しました。全日リモートでの参加で、joinしたチームのマルチA100 GPUサーバなどを用いて画像認識モデルを学習し、NTT Com で独自に構築しているデータセットでその性能評価をしました。この記事では、その体験談を記載します。 インターンシップまでの経緯 就活イベントで NTT Com の紹介を聞いたのですが、そこで色々と説明してくださった社員の方からメディアAI技術開発チームを紹介してもらい、後日今回のメンターさんらと懇談していただきました。その際、チームの紹介を受け興味を持ち、このインターンシップに応募しました。面談で期待することを聞かれた際には、取り組みに用いるデータサイズに対し適切な計算リソースを提供してほしいと伝えたのですが、その希望が反映されたテーマだったと感じます。インターンシップの中ではチームが持つGPUサーバ群の紹介もしていただき、それらがラッキングされた NTT Com のデータセンターを写真(例えば以下)で紹介いただく機会もありました。 インターンシップで取り組んだこと インターンシップでは、大きく以下の3つに取り組みました: 公知の人物検出データセットを複数組み合わせて学習データの規模を大きくする 構築した学習データを用いて人物検出器を学習する NTT Com が内製で構築しているデータセットを用いて学習したモデルを評価する 公知のデータセットにはいくつか選択肢があったのですが、今回は COCO と CUHK-SYSU を選びました。人物検出に関する各データセットの統計を以下の表にまとめます。 平均物体サイズは、物体(人物)バウンディングボックスの画素数をそれが含まれる画像の画素数で割って算出しました。画像数・物体数ともにCOCOが最も多いですが、CUHK-SYSUは画像の解像度が高めな一方で物体は小さめに映り込んでいることが分かります。NTT Com で内製構築しているデータセット(NTT Com 内製)の平均物体サイズは、それらのおよそ中間でした。 COCOとCUHK-SYSUはアノテーションのフォーマットが異なるため、そのままでは1つのデータセットとして用いることができません。そこで今回は、CUHK-SYSUのフォーマットをCOCOのそれにコンバートし、かつそれをCOCOの人物アノテーションと結合することで1つのアノテーションデータを構築しました。コーディングにはPythonを使用しました。CUHK-SYSUのフォーマットを解読したり、複数データセットのインデックスを正しくアラインメントしたりするのはなかなか大変で、アノテーションを可視化してフォーマットの解釈が正しいか確認したり、またメンターと画面共有しながらデバッグしたりもしました。 モデル学習速度の比較 そんなこんなで構築したデータセット(COCO + CUHK-SYSU、総画像枚数64115+18184=82299枚)を用いて、まずは異なるGPUサーバでモデルの学習速度を比較しました。結果を以下の図に示します。異なるGPUサーバとして、チームが持つV100 GPUを4基搭載するサーバ(V100 x4)、A100を4基搭載するサーバ(A100 x4)およびA100を8基搭載するサーバ(A100 x8)を使用させていただきました。人物検出モデルはFaster R-CNNで揃えています。 図から、どちらのバッチサイズのケースでも、バッチあたりの学習にかかる時間はV100 x4よりもA100 x4の方が短く、A100 x8では更に短いことが分かります。具体的には、A100 x4はV100 x4の1.4倍程度、A100 x8はA100 x4の1.7倍程度高速でした。大きなバッチサイズですと当然バッチあたりの時間は増えますが、2倍のバッチサイズに対しかかる時間は1.8倍程度に抑えられていました。どの条件でも12エポック学習を回しましたが、V100 x4で9時間程度かかっていた学習がA100 x8では3.5時間ちょっとで終わりました。 A100速い 。 NTT Com 内製データセットを用いた性能比較 続いて、データセットを組み合わせてそもそも人物検出の精度が向上しているのか確認するため、NTT Com 内製データセットをテストデータとして性能評価をしました。比較のため、COCO・CUHK-SYSUいずれかのみを学習データとして得られたモデルでの評価もしています。以下の表に、Intersection-over-Union (IoU) の閾値を0.5と設定したときのAverage Precision (AP)を示します。 表から分かるように、CUHK-SYSU単独よりもCOCO単独の方が検出性能は高くなります。けれども、それらを1つのデータセットとして組み合わせることで、APが更に0.02以上向上しています。より規模の大きなデータセットでモデルを学習することは、実際に性能向上に寄与することが分かります。なお今回の例では、バッチサイズを16よりも8とした方がわずかながら性能は高い傾向でした。 検出結果例を以下に示します。人物が小さめに映り込むCUHK-SYSUをCOCOと組み合わせてモデルを学習することで、COCO単独の場合に比べ、小さな物体の検出漏れが減っている印象を受けました。 インターンシップの感想 普段は主にC++/CUDAを用いて、深層学習とは直接関連しない画像処理を高速化する研究をしているため、Pythonを用いた深層学習は不慣れな点がありました。しかし、メンターやチームの皆さんのサポートもあり、内部の成果発表までできました。トライ&エラーを含めると結構な回数の学習を回しましたが、期間中にまとめられたのはjoinしたチームが潤沢な計算リソースを有していたことも大きかったと思います。貴重な体験をさせていただき、ありがとうございました。 メンターからのコメント メンターを担当したイノベーションセンターの田良島です。八木さんインターンシップお疲れ様でした!今回のインターンシップはコロナの影響で完全リモートでの開催でした。チームメンバーとのコミュニケーションはオンラインのみで、また大学での研究とは毛色の異なる作業もあってきっと大変だったと思うのですが、根気強く課題に取り組んでいただいてとても感謝しています。複数のV100やA100が搭載された我々のGPUサーバに触れていただけたことはよかったのかなと感じています。本当は、NTT Com のデータセンターに来てもらい、GPUサーバ群を見学いただけるとよりよかったのですが、今回は残念ながら叶いませんでした。 このインターンシップでの経験が今後の八木さんの研究やキャリアに少しでも役立てば嬉しいです。ありがとうございました!
こんにちは! 今年もう2ヶ月ほど経ちましたがまだまだ寒い日が続いていますね。 イノベーションセンターの原田です。 本日は2021年11月頃に実施しました新入社員研修の取り組みについてご紹介します。 研修の概要について このハンズオンは新入社員が半年ぐらい業務に携わった頃に おそらく対峙する or したであろうレガシーなコードについて、 どのように立ち向かうべきか?を5日間チームで手を動かしながら学習していただく研修です。 この研修には2つのゴールがあり、1つ目が「レガシーコードを安全に変更するための土台作りの方法を学ぶ」、2つ目が「どのようにレガシーコードを戦略的に改善していくか意識付ける」です。 受講生は 20 名で、2021 年度入社以外の社員も数名参加しました。 また講師・メンターを社員7名が担当し、 研修は全体を通してオンラインで行いました。 下記は全体のスケジュール表になります。 こちらの表の通り、 まず1日目にレガシーコードをドンッ!と配って仕様を理解した後、 2日目でテストコードを用意し、3日目からリファクタリングや新規機能の実装に入ります。 また演習の前後にはチーム間レビュー(隣のチーム同士で意見交換)とふりかえり(KPT)を用意しており、 他のチームが得た知見や気づきを共有する場を設けています。 なお研修のジャンルとしてソフトウェアが中心となりますが、 新卒研修として NTT Com では他にもネットワーク研修、AI・データサイエンス研修などがあります! なぜこの研修を実施したか ソフトウェアをテーマとした新卒研修は毎年行われていますが、 昨年までの内容が IaaS や CaaS、CI/CD が中心でコードを書くことがメインでは無かったため、 新入社員から「もっとコードを書きたかった!」と意見を頂いたことがきっかけとなります。 また、NTT Com ではNeWorkなど内製開発に注力していますが、 社員が開発中の悩みをヒアリングした結果 既存のコードについてのリファクタリング・リアーキテクチャに対する悩みを持っていることが分かりました。 よって本研修を通してレガシーコードに対するアプローチ方法を学ぶことで開発力に磨きをかけ、 エンジニアとしてのキャリアを形成する後押しができればと思い企画しました。 研修の一例について ここからは各日の研修の流れを簡単に紹介していきます。 Day1 レガシーコードを読む レガシーコードの定義から説明し、どのようにコードを読み進めていくか解説しました。 なおレガシーコードとして N-PTC 2020 で利用した コード を題材として取り上げ、受講生は手探りで仕様を理解していきました。 Day2 テスト可能にする 1 日目に読み込んでもらったレガシーコードについて、手を加える前にテストを書きます。 テストとしては単体テスト・結合テスト・受け入れテストなどをそれぞれ実装してもらいましたが、 まずは結合テスト(API テスト)から実装してもらいました。 もちろん一日で全てのテストを書ききることは難しいため、3 日以降もテスト実装に時間を割いても OK にしています。 Day3 レガシーコードの継続開発 おおよそテストが実装できたらレガシーコードをリファクタリングして改善していきます。 また、このレガシーコードはビジネスと直接紐付いているというシナリオの元、新規機能の追加実装も業務として入ってきます。 そのため、演習者はソフトウェアを破壊しないように注意しつつ、新規機能の実装やリファクタリングを行う必要が出てきます。 また講義では、レガシーコード改善の真の目的が「ユーザへの価値提供の速度を最大化する」であることに触れ、 これから演習者がどのような実装戦略を立ててユーザへの価値を最大化するかの意識付けを行いました。 Day4/Day5 ここでは具体的なリファクタリング方法や細かなTipsを説明しました。 なお新規機能の追加や新たなお題の出題は無く、演習者がこれまで出された要求に対して取り組む時間となります。 受講生の反応 ここからはデータプラットフォームサービス部の堀内が、研修の受講生の反応についてご紹介します。 研修に参加した動機 事前アンケートで研修に期待することを聞いたところ、下記のような回答が得られました。 レガシーコードに対して具体的にどう対処すれば良いか、どういう観点でリファクタリングすれば良いか学びたい。 テスト駆動開発と絡めてレガシーコードをリファクタリングする手法について学びたい。 業務でレガシーコードを多く目にするので、研修での学びをチーム内で展開するなど、それらを減らす取り組みをしたい。 また、新入社員の方が中心ということもあり、受講生のスキルは下記のような状況でした。 テストケースを考えたことや単体テストを実装したことはあるが、E2E などの結合テストは未経験という人が多い。 CI (Continuous Integration) を使ったことがある人は多いが、設定したことがある人は少ない。 GitHub を使ったことがない人や、ペアプログラミング / モブプログラミングをしたことがない人はそれぞれ半数程度。 研修中の様子 受講生は 2 人 1 組でチームを組んでもらい、そこにメンターが 1 人付くという形で演習に取り組んでもらいました。 研修中のコミュニケーションのために Slack を提供し、ペア作業ではチーム毎の Channel で Huddle を使いました。 Day1 のコードリーディングでは、準備した我々も「分かる」と言いたくなるようなコメントが多く上がりました。 コメント / ドキュメント / テストがなく、処理の内容が分からない。 変数名 / 関数名が分かりにくく、意図が読み取れない。 ソースコードのネストが深かったり、同じような処理が散らばっていて、追い掛けにくい。 Day2 では、レガシーコードに手を入れ始める前に、 まず単体テスト (pytest) と結合テスト (Postman) の実装をしてもらったのですが、 チームメイトと初対面の人も多い中、Visual Studio Live Share などを活用して上手くペアで作業を進めており、非常に驚きました。 Day3 〜 5 では業務に沿ったシナリオで演習に取り組んでもらったため、実際に直面しがちな問題に向き合ってもらいました。 各チームの振り返り (KPT) を覗いてみると、下記のようなコメントが見られました。 良かった点 レガシーコードの課題を GitHub の Issue に洗い出し、チームメイトとこまめに情報共有ができた。 テストコードを先に作り、それを通すように実装することで、テスト駆動開発を実践できた。 「ユーザに価値があるか?」を基準にして、機能追加とリファクタリングの優先順位を判断できた。 docker-compose と GitHub Actions を使って CI を実現できた。 悪かった点 機能追加を優先した結果、リファクタリングがおろそかになり、レガシーコードを生み出してしまった。 リファクタリングを後回しにした結果、デバッグのコストが増え、実装を完了できなかった。 Postman など、演習で利用するツールを使いこなすのに手間取ってしまった。 メンターからのアドバイスとしては、NTT Com の技術顧問でもある @t_wada さんの下記資料が共有されていました。 研修後の評価 5 日間の研修を終えた受講生に事後アンケートで「今回の研修で学んだ知識・経験は業務で活かせそうだと思いますか?」と聞いてみました。 どの質問も「そう思う」「非常にそう思う」と回答した人が 8 割以上で、メンターとしてはホッとする結果でした。 また、アンケートには下記のようなコメントが寄せられました。 実際の現場では様々なコードがあり、そこから作成者の意図などを読み解く必要があるので、今回の経験を活かしていきたいと思った。 テスト駆動開発を実践できたのは非常に得るものが多かった、日頃の開発でもテストを書いてから実装に取り組みたい。 リファクタリングをする上での観点 / 考え方や、機能追加との兼ね合いなど色々な側面での知見が得られたとので、今後の業務に反映していきたい。 講師側の所感 後日、講師/メンターをした社員で集って振り返りを行いました。 Keep 資料に重要なポイントが落とし込めていて分かりやすかった。 GitHub の Issue で進捗の見える化をした結果、研修準備のプロジェクトが大炎上しなかった。 (ラフではあるが) 事前に研修をテストプレイをして資料や題材をブラッシュアップできた。 Problem Day3 以降、受講者がデザインパターンやリファクタリングなどの知識不足によって遭難しはじめる様子が見て取れた。 講義資料がギリギリまで出来上がらなかったり、コードと資料の齟齬があったりした。 Git / GitHub や Postman の使い方に関する資料が足りていなかった。 研修題材にわかりにくい部分があった。 Try Git / GitHub 研修の資料を流用するなどして、事前学習をもう少し充実させたい。 @t_wada さんに受講してもらって模範回問を用意する、研修の流れや分量についてアドバイスをもらう。 今回の研修内容は全体的に難しめだったので、難易度の調整をする。 まとめ・今後について 今回の研修の評価 / 反省を受けて、2022 年も新入社員向けのソフトウェア研修を実施したいと思っています!!
はじめに はじめまして!情報セキュリティ部で社内バグバウンティ(NTT Com Bug Bounty Program)の運営をやっている長妻です。 この記事では、NTT Comで開催している社内バグバウンティを紹介します。また、社内バグバウンティに限らず、一般的なバグバウンティへ未経験の方にも参加してもらうために基本的なバグ調査環境を紹介します。 NTT Com Bug Bounty Programについては以下の記事もご確認ください。 社員が“バグハンター”として自社サービス・システムの堅牢化に参画 NTT Comが社内バグバウンティプログラムをスタート - Shines|NTTコミュニケーションズ そもそもバグバウンティとは? バグバウンティという言葉を聞いたことはあるでしょうか? 近頃はニュースにもされるようになってきたので、この記事の読者層は知っている人も少なくはないのかなと想像しています。 バグバウンティとは、企業などが自社のサービスや製品についてバグ調査案件を公開し、それを見たバグハンターにバグを調査・報告してもらう制度です。報告の対価としてバグハンターには報奨金が支払われます。 報告内容によっては報奨金が数百万円にもなることがあり、バグバウンティで生計を立てる人もいるとか。。。 企業としても脆弱性診断などでは見つからないようなバグが見つかる可能性があるため、多額の報奨金をかけてバグバウンティ制度を設けている会社もいるわけですね。 NTT Comでも、社内に閉じた社内バグバウンティを始めました。 社内バグバウンティでは社員に活躍してもらう 社内バグバウンティは、主にNTT Com社員(と一部のグループ会社の社員)にバグハンターとしてバグを探してもらおうという取り組みです。 社内システムのバグを、社員が見つけて報告するという構図で、もちろん報奨金も支払われます。 参加の際には、調査を募集するシステムも、調査を実施する社員も社内バグバウンティへ参加登録をしてもらいます。 特に調査に参加する社員には、調査対象システムへ高負荷をかける調査は行わない等の禁止事項や参加規約を読んで同意してもらっています。 参加登録が完了した社員は、業務としてではなく個人の活動としてバグハントを行います(もちろん、参加登録したからといって活動を強制されるということではありません)。 報奨金が支払われるのは報告内容が脆弱性に認定された場合のみですが、社員はお金目的だけでなく、スキルアップ、力試し、品質向上への貢献、といったいろいろな目的で参加してくれています。 オープンなバグバウンティ制度では多くのバグハンターへアプローチできる一方で、相手の身元が分からないなどのリスクがあります。(例えば、相手が反社会的勢力で、報奨金が利益供与にあたらないか?など) 社内バグバウンティでは信頼のおける社員(と一部のグループ会社の社員)に絞ったバグハンターの募集となるため、システム運用担当者としても安心して参加できる制度です。 バグバウンティの入門として 社内バグバウンティは一般的なバグバウンティと比べると報奨金は控え目ですが、一方で社外にオープンではないためライバルが少ないです。 熟練者がひしめき合うようなオープンなバグバウンティプログラムと比べると、初参加でもチャンスがあるかもしれません。 少額でも良いから調査してバグを見つけて達成感を味わいたい人に向いている制度かなと思います。 また、多くの貢献をしてくれたバグハンターの方は、『Hall of Fame』として社内バグバウンティのオフィシャルページに掲載されるので、ぜひチャレンジしてみてください。 バグバウンティをこれから始める方のために ここからはバグ調査で使う基本的なツールの説明を行います。ローカルプロキシを使ってリクエストの改ざんが行える方は、これ以降の文章は読み飛ばしてもらって大丈夫です。 調査環境のセットアップ ここからは以下の調査環境をセットアップするまでについて説明していきます。 ローカルプロキシ:Burp Suite Community Edition 有名なローカルプロキシツールです。WEBリクエストの改ざんや複製など様々な操作を行えます。 https://portswigger.net/burp/communitydownload WEBブラウザ:Firefox & FoxyProxy Standardアドオン FoxyProxyは、Firefoxのプロキシ転送設定を簡単にしてくれるアドオンです。 https://addons.mozilla.org/ja/firefox/addon/foxyproxy-standard/ Burp Suite Community EditionのProxyサービスの設定 まずはBurp Suite Community Edition(以下、Burp)をインストールして、Proxyサービスの設定を見てみます。 下の画像のようにデフォルトでは 127.0.0.1:8080 にてポートを開放して待ち受けています。この画面から細かい設定も可能ですが、このままデフォルト設定で説明を続けます。 Firefoxのプロキシ転送設定 次にFoxyProxy Starndardのアドオンをインストールしたら、Burpのローカルプロキシへの転送設定を行います。 アドオンが適用されると、下図のようにブラウザの右上にアイコンが表示されます。 続いて、 アイコンをクリック > Options > Addと遷移して、プロキシ設定を登録します。 Burpのプロキシは 127.0.0.1:8080 で立ち上がっていたので、下図のように埋めたらSaveします。 設定の登録が完了すると、下図のようにブラウザの右上からどのプロキシ設定を適用するかを選択できるようになります。 作成したプロキシ設定にチェックマークを付けたら、指定したプロキシを経由するようになります。 そこで、 https://example.com へアクセスしてみると、、、 上図のように警告が表示されました。これはhttps通信をする際にプロキシを経由させたため、中間者攻撃とブラウザに判断されてセキュリティ機能が働いたためです。 FirefoxにBurpのルート証明書をインポートする 続いて、Burpのルート証明書をブラウザに読み込ませて、こうした警告が出ないようにします。 証明書は以下のBurpの「Proxy > Options」画面から、「import/export CA certificate」ボタンを押すことで取得できます。 取得したルート証明書をFirefoxにインポートします。 Firefoxの設定画面を開いたら、「証明書」と検索して証明書マネージャーを開きます。 先ほど取得したルート証明書をインポートしてください。インポートが完了すると、「PortSwigger CA」が追加されます。 ここまでやれば設定は完了です。再び https://example.com へアクセスして確認してみます。 ローカルプロキシを介してアクセスできました。 「Intercept」や「Repeater」といった機能を使ってリクエストを改ざんできます。 あとはバグバウンティプログラムへ参加し、ルールを守ったうえで調査に参加してみてください。
はじめに こんにちは、データプラットフォームサービス部で IoT 系サービスやフル MVNO 基盤、ローカル 5G サービスの設計開発を担当している真山です。 本ブログでは過去に IoT Connect Gateway を使ってみた 第3回 〜観葉植物育成状況の可視化〜 について投稿しています。今回は、当社のローカル5Gのサービス紹介だけでなく、ユースケースや周辺技術、検証実験の内容の一部を発信していきたいと思っています。また皆様からのフィードバックを次期開発に活かしたいと考えていますので、リクエストやコメントを頂けますと幸いです。 そもそも、ローカル 5G とは? ローカル 5G は携帯電話事業者による全国向け 5G サービスとは別に、地域の企業や自治体等が個別に利用できる 5G ネットワークのことです。地域・産業のニーズに応じて、自らの建物内や敷地内などの特定のエリアにおいて自営の 5G ネットワークを構築・利用・運用できます。利用には国で指定された無線局免許の取得が必要であり、自ら取得することも、免許取得した他社のシステムを利用することも可能です。利用できる周波数帯は、Sub6 (n79) と呼ばれる 4.6~4.9 GHz と、ミリ波 (n257) と呼ばれる 28.2~29.1 GHz です。 NTT Communications のローカル 5G とは? NTT Communications はローカル 5G サービスを2021年3月30日にリリースしました。 www.ntt.com www.ntt.com NTT Com および NTT グループの知見を活かし、ローカル 5G の利用に必要なプロセス (導入コンサルティング、免許取得、機器構築、運用の支援) をワンストップで提供します。 ローカル 5G サービスの特徴 1. NTT ドコモの無線技術・ノウハウを活用し、高信頼のサービスを提供 ローカル 5G 導入前のエリア調査や回線設計、設置工事、運用の一部などにおいて、無線技術の知見を持つ 株式会社 NTT ドコモのローカル 5G 構築支援サービス を活用します。NTT グループのアセットを活用することで、お客様の導入に際する障壁を低減したうえで信頼性の高いローカル 5G 環境を提供します。 2. 5G 無線基地局装置と 5G コアを必要な期間のみ利用可能 Sub6 帯に対応したローカル 5G の機器類一式を NTT Com から一元的に提供します。また、ローカル 5G の環境構築において 5G コアおよび無線基地局設備を月額利用型のサービスとして提供しています (お買い上げも可能)。これにより、お客さまの初期コストを低減しながら、お客さまの技術検証などでの必要な期間のみローカル 5G の利用が可能となります。最低利用期間は 1 年間となります。 3. Smart Data Platform とのワンストップ提供でデータ利活用環境を実現 NTT Com の Smart Data Platform (SDPF) と一体的に提供可能です。専門的な知識や技術、手続きが必要となるローカル 5G システムの環境構築だけでなく、データ利活用に必要なデータ収集・蓄積・分析などの機能をワンストップで提供し、お客さまの DX を推進します。SDPF と組み合わせたユースケースについても今後ブログのなかで発信したいと思います。 NTT Com のローカル 5G での強み 1. クラウドコア型構成 ローカル 5G の提供体型としてオンプレ型が多い中、NTT Com の提供するローカル 5G サービスは現在クラウドコア型のアーキテクチャを採用しています。5G の制御を司る各種コア装置をクラウド上に配置し、お客様拠点に配置される無線基地局設備 (RAN 装置) と分離しています。5G コアではアクセスとモビリティ管理を担う AMF やセッション管理を担う SMF などのネットワーク制御 (C-Plane) 機能を提供します。また 5G コアのユーザーデータ処理 (U-Plane) 機能部である UPF についてはお客様拠点内に配置することでお客様ネットワークまでの伝送距離を短縮し、低遅延な通信を実現します。C-Plane と U-Plane を分離し、C-Plane をクラウドに集約することで導入時や運用時のお客様の負担を軽減します。 2. 同一 5G コア・複数拠点構成 お客様のご要望によっては、複数拠点でのローカル 5G サービスの利用を希望されるケースがあるとか思います。このようなご要望については拠点を増やす毎に新たに 5G コアをご契約いただくかなくとも、クラウドコア型のメリットを活かし、同一 5G コアを利用しながらも複数拠点に RAN 設備を配置してローカル 5G をご利用いただくことが可能です。アドレス設計や 5G 端末挙動についてもサポートします。 3. 高セキュリティを実現する閉域の中継ネットワーク クラウド上の 5G コアと RAN 装置の間の接続には、 NTT Com の閉域ネットワークサービスである Arcstar Universal One と Flexible InterConnect (FIC) を中継回線として利用し、ローカル 5G に求められる高いセキュリティをネットワークレイヤでも実現しています。5G コアおよび中継回線におけるお客様拠点の RAN 設備から 5G コアへの制御系信号用ネットワークはお客様専用としてご利用いただきます。 最後に ローカル 5G は黎明期でありサービス構築だけでなく、様々なユースケースの創造も我々のミッションと考えています。ローカル 5G の無線基盤としての新機能開発・提供だけでなく、従来技術では解決が困難であったお客様の課題をローカル 5G を用いることでいかに解決できるのか?といった観点のもと、様々なアプリケーションとの組み合わせによる相乗効果を検討していきます。この内容についても今後ブログの中でご紹介できればと思います。また、ローカル 5G を使って試してみたい機能やソリューション、アイディアがあればぜひご連絡をいただけると幸いです。 次回予告 NTT Com の本社で構築したローカル 5G の無線環境調査の検証内容について紹介させていただきます。 ローカル 5G サービス関するお問い合わせ先 ローカル 5G に関するお問い合わせ お問い合わせフォーム メールでのお問い合わせ l5g-support@ntt.com ※お手数ですが@を半角文字に置き換えてください
はじめまして。プラットフォームサービス本部セキュリティサービス部門の大倉です。普段はインシデントレスポンスチームに所属し、お客様で発生したセキュリティインシデントの調査を業務として行っています。 今回、2021年12月4日(土)に開催された TechWorkshop「NTTコムのセキュリティ業務紹介&サイバー攻撃対応ワークショップ」の内容について紹介します。 TechWorkshopについて 各技術分野のプロフェッショナル社員がお届けする、当社の最先端技術を体感し、また身に付けることができるワークショップ形式のプログラムです。 ―― イベント紹介(採用情報) より抜粋 過去にはこのようなワークショップを開催しました。 コーディング体験 ISPネットワーク構築・運用体験 Kubernetesハンズオン CI/CDハンズオン いくつかのTechworkshopイベントは、当日の様子をブログとして公開しています。ご興味がある方はこちらのブログ記事もご覧ください。 TechWorkshop 「現場のエンジニアと一緒に解く!コーディング体験」を開催しました! TechWorkshop「プロのネットワークエンジニアと学ぶ!ISPネットワークのつくりかた」を開催しました! どのワークショップも各分野を主な業務にしている/業務に使っている社員が講師となり、かつ自分たちでイベントの企画・運営をしています。今後もいくつか開催予定ですので、興味のある方は TechWorkshopのページ をチェックしていただき( NTT Com公式Twitter でもご案内します。)、ぜひ参加申込をお願いします。 「NTTコムのセキュリティ業務紹介&サイバー攻撃対応ワークショップ」とは? 当日の大まかなスケジュールは以下となっていました。午前は業務紹介がメインとなっており、午後から演習を行いました。 午前 会社紹介 NTTグループとは? NTTコミュニケーションズってどんな会社? NTTコミュニケーションズ×セキュリティ セキュリティ関連部署の業務紹介 NTTコミュニケーションズ内の各セキュリティ部署(CSIRT、研究開発など)の業務 NTTセキュリティ・ジャパンの業務 エヌ・エフ・ラボラトリーズの業務 午後 演習 問題説明、演習環境へアクセスする方法の説明 演習開始 各チームに分かれて攻撃の解析 発見した攻撃に応じて、サーバの設定変更・脆弱性の修正 発見した攻撃内容の発表・答え合わせ 実際に行われた攻撃を解説 各チーム毎に発見した攻撃を発表 懇親会 午前の部 : 会社紹介 & セキュリティ関連部署の業務紹介 ワークショップの午前は、NTTコミュニケーションズの会社説明、および、各セキュリティ部門の社員から実際の業務内容の紹介しました。当日は、NTTコミュニケーションズ所属の社員だけでなく、NTTセキュリティ・ジャパンやエヌ・エフ・ラボラトリーズの社員も参加していたため、多様なセキュリティ業務を紹介できました。それぞれの業務ごとに異なる魅力がありますが、入社したあとどのような仕事があるのか、参加者の皆さんもイメージしやすくなったことと思います。 午後の部 : サイバー攻撃対応ワークショップ 演習概要 本演習の目的は、「セキュリティエンジニアの仕事を体験する」ことです。インシデントレスポンスの流れを基に、以下を経験してもらいます。 システム環境に対するサイバー攻撃を検知したり、対処したりしてもらい、セキュリティオペレーション業務を体験する。 システム環境に存在する脆弱性の発見・修正をすることで脆弱性診断、ペネトレーションテスト業務の一部を体験する。 本演習は、グループ会社のエヌ・エフ・ラボラトリーズより提供いただいたコンテンツを利用しています。 まず、演習のはじめにルールや見るべきログなどを説明しました。その後、演習環境へのアクセス方法や接続確認を行いました。 演習では参加者4名ほどでグループを作り、クラウド(AWS)上に構築された演習環境へアクセスしてもらいます。演習環境では、Webサイトがホストされているサーバが存在しており、そのサーバに対してインターネットを想定したネットワークから様々な通信が行われています。参加者の皆さんにはWebサイトの可用性を維持しつつ、時間内に行われる多くの攻撃を防いでもらいました。 本イベントでは、現在のシステム状況を把握しやすいように、スコアを別途表示していました。今回はコンテストではないため、順位を争うものではありませんでしたが、自分たちの設定が想定どおりに反映されているかどうかの判断に使ってもらいました。参加者は、行った対処が正しい方向であればより高得点が得られ、一方、アクセス断等誤った対処をした場合加点されない、といったフィードバックを得られました。 演習中の様子 演習の詳細は、本Workshopを再度開催する予定であるため書くことはできないのですが、サーバに記録されていく多種多様なログ(認証ログや動作しているサービスのアクセスログ、FTPログ、IDSのアラートログ等)を解析し、どのような攻撃がきているのかを確認してもらいます。さらに、稼働しているサービスに脆弱性が存在しているため、ソースコードを読むことも求められます。普段見たことがないログや扱ったことのない言語に戸惑う人が多かったようですが、自分たちでログの仕様や言語を確認してもらいました。 攻撃の痕跡や脆弱性を発見した後は、その対応策の実施が必要になります。設定の変更やソースコードの変更などをしてもらうことになりますが、思い切った変更をするチームが多かった印象です。そのため、変更内容によっては予期しないサービス停止、アクセス断が発生する場合もあります。当日も演習中にSSHの設定を変更しサーバに接続できなくなりそうなトラブルや、バックアップをとっていなかったためソースコードを改変した後、元に戻せなかったチーム等がありました。実際に運用する際に注意深く対応しないと、二次被害が発生するという良い教訓になったことでしょう。なお、設定ミス等によりサーバにログインできなくなるなど、どうしようもないお手上げ状態になってしまった場合は、対象サーバの状態をリセットする救済措置も行いました。 その他、参加者の様子を見守っていましたが、チームで通話を繋ぎっぱなしにして細かくタスクを確認するチームがいる一方で、細かく相談はせずに見つけた攻撃を淡々と共有するチームもあり、各チームそれぞれ特色が見られ面白かったです。また、チーム内で教え合いや議論が行われている場面もありました。 攻撃の解説・発見した攻撃内容の発表 演習の終了後には攻撃内容の解説をしました。参加者の中には、見逃してしまった痕跡や攻撃を知り、感心や悔しがる様子が見受けられ、準備したかいがありました。 その後、各チーム毎に発見した攻撃を発表してもらいました。ログやコマンド結果に対して詳細に注釈を入れて示すチームや、自分たちが見つけた脆弱性とその対応状況を表に分かりやすくまとめるチームなど、短時間で伝わりやすいスライドを作成しており驚きました。 なお、我々が用意していた攻撃すべてを発見・対応したチームは残念ながら存在しませんでした。演習の時間が3時間程度となっており、攻撃の種類も存分に楽しんでもらえるように盛り盛りにしていたため、難しかったと思います。すべての攻撃を発見するには、個人の技術力も重要ですが、チーム力も必要です。実際のインシデント対応では、関係各所との調整やタスクの割り振りなども必要になります。チームで行っていることを最大限活かして力を発揮しなければいけないことを実感いただけたことでしょう。 参加者の声 Workshop後に参加者の方からいただいたアンケートでは、多くの方に「大変良かった」「いろいろ学ぶことができた」などのコメントを多くいただき、とても嬉しく思いました。以下はコメントの一部抜粋になります。 攻撃を特定するためにどのログを見なければならないのかを学ぶことができた。 ログの解析だけでなく、実際に手を動かしてその対策を自分たちで実施するという流れで大変勉強になった。 チームで作業をするというのが最近なかったので、メンバと話し合いながら作業をするのが楽しかった。 さらにイベントの内容だけでなく、質問対応などから職場や社員の雰囲気を感じ取ることができた方も多かったようで、ポジティブなコメントをいただきました。オンラインのイベントだったため、コミュニケーション不足などこちらにも至らない点が多かったと思いますが、参加者の皆さん、ありがとうございました。 おわりに この記事では、2021年12月4日(土)に開催された TechWorkshop「NTT Comのセキュリティ業務紹介&サイバー攻撃対応ワークショップ」について紹介しました。昨年も似た内容でイベントを企画しましたが、今年はさらに多くの方に参加していただきました。こういった攻撃対応を経験することは少ないかと思いますので、業務をイメージできる良い機会になったのではないでしょうか。 我々社員としても、NTT Comならびにセキュリティ業界に興味を持っていただけたなら幸いです。 ちなみに 同じ内容のイベント を2022年2月19日(土)に開催予定です。応募締め切りは、2022年2月4日(金)23:59までとなっておりますので、この記事を読んで興味を持った方はぜひご応募ください!
はじめに 初めまして!イノベーションセンターで ノーコードAI開発ツール「Node-AI」 のプロダクトオーナーやXAI・因果分析の研究をしております、切通恵介( @kirikei )です。 Node-AIは2021年10月11日にリリースされたNTT Communicationsの内製開発サービスで、その名の通りブラウザ上からノーコードでAIモデルを開発できるサービスで、製造業のお客様を中心に異常検知やプラント運転支援などの様々な領域で活用されています。(ニュースリリースは こちら や こちら や こちら ) いつもはサービスの営業的な紹介をすることが多いのですが、今回はEngineer's Blogでの執筆ということで、エンジニアの方向けの技術、プロダクトマネジメント、チームビルディング、スクラムなどの様々な観点でお伝えできればと考えています。とはいえ、Node-AIに関しては詳細に書きたいことが山ほどあって1つの記事には到底収まらない(!)ので、何回かに分けてシリーズモノとしてお送りする予定です。 そんな中で記念すべき第1回の今回の記事では Node-AIがどのようなサービスであるか Node-AIで扱う技術領域にはどのようなものが含まれるか Node-AIをどのように開発しているか の3点に関してをプロダクトオーナーである私とテックリードである内藤理大、スクラムマスターの小澤暖から述べます。 Node-AIとは Node-AIはノーコードAI開発ツールというその名の通り、ブラウザから以下の図のようにカード(モジュール)を直感的につなげるだけで時系列データの前処理からAIモデルの学習・評価までの一連のパイプラインを作成・実行できるツールです。 Node-AIで目指す世界 Node-AIチームには「 データ分析にまつわる全て(機能・人)が集まってコミュニケーションしながらデータ分析を継続的に実行し様々な課題を解決し続けている世界 」を目指す、というビジョンがあります。 少しこのビジョンを掘り下げてみます。 【課題解決に必要なもの】 昨今、AIという言葉の流行によって様々なデータ分析案件が生まれ、課題解決に利用する流れができてきた一方で「うまく進まない」例もたくさんあります。私自身データサイエンティスト的な役割、すなわちお客様から課題をヒアリングし、データを受け取りそれを分析して最終的な課題解決を行うといういわゆる「データ分析案件」に携わる中で様々な失敗してきています。 この原因としては、「 課題解決 」というものの範囲が広く、そして複雑であることが挙げられます。例えば以下のような案件があったとしましょう。 データ分析者な我々にとってはまずプラントに存在する時系列のセンサーデータをcsvなどでもらって、その中の不純物(シリカ)の量を表すカラムを目的変数として選択し、その未来の値を教師データとしてcsvのデータから特徴量を作成して何かしらのモデル(ARやLightGBMでもいいしTransformerのような高級なモデルを使ってもいいし...)で予測すれば良い、というのが頭に思い浮かびます。 しかしながら、このモデルを作成するという部分が課題解決自体を示すわけではありません。このゴールは少なくとも「 精度の高い予測値をリアルタイムに可視化し、その結果を運用者が見て攪拌のための操作を変えて鉄の収集量が上がること 」です。つまりモデルを作成した後に業務に導入するところまで必要となります。 そして「少なくとも」とした通り、このゴールも残念ながら完全ではありません。例えば「精度の高い」はどれくらいの精度なのか、「予測値」とはどれだけ先の未来を当てるのか、「リアルタイム」とはどれくらいの遅延が許されるのか、「運用者が見て」とはどのようなシステムでどんなUIで見るのかなどを課題に関わる様々な ステークホルダーとすり合わせ なければなりません。 さらに、上記の具体的なゴールを初めに決めたとしてもステークホルダーとの話し合いは終わるのではありません。例えば特徴量の作成には実際にデータを見ている 現場の運用者 の知見が必要になります。さらに十分な精度を得たとしても実際に導入できるかどうかは最終的なGoサインを出す 意思決定者 との話し合いが必要になりますし、最終的にモデルを何らかのシステム、例えば工場であれば工場で既に使われているデータ可視化ツールに組み込むためには 工場のシステム担当 とも意思疎通を図る必要があります。 そして、導入を行ったとしてもそこでプロジェクトが終わることはありません。例えば季節変化による精度の劣化への対応や、ビジネス要件やデータ定義の変化によるモデルの再作成や問題の再定義など、ステークホルダーとコミュニケーションしながら改善を進めていく必要があります。 長々と書きましたが、ここまでまとめると、以下となります。 データ分析による課題解決を継続的に行うには、モデルを作成して終わりではなく、意思決定やシステム導入に至るまでが必要。またデータ分析者で完結することはなく、課題に関係するステークホルダーとのすり合わせ・相互理解が重要。 実際にこれらが実現できているかというと、そこには様々な壁があります。 【 壁その1:ステークホルダーの分析内容の相互理解 】 上で述べたとおり、課題解決にはステークホルダーとのすり合わせ、特に分析中は精度を上げたり導入判断を行うためのフィードバックが重要になります。この時、 データ分析者とステークホルダーは分析内容をしっかり把握することで、より深いフィードバックが可能になります 。 例えば、時系列データであれば時系列の前処理において時間窓をどれくらい取るのか、サンプリング間隔をどれくらいにするのかを運用者の知見から決めることがあるでしょうし、特徴選択においては運用者が普段不純物の状態を確かめるのに必要なセンサーを教えてもらうこともあります。このような精度を上げるためのフィードバックには、専門家にもどのような前処理や学習を行っていてどのようなパラメータがあるのかを詳細に理解してもらう必要があります。 また、その理解は意思決定者にも必要になります。意思決定者は導入のための責任を負っているので、その分析によって真に課題が解決でき、自分や現場の知見と照らし合わせても問題ないかどうかを確かめる必要があります。この時に内部のフローを理解していることで、意思決定に必要な情報が得られるため、正当な判断をしやすくなります。導入を担当する社内のシステム担当者にとっても、現状運用を行なっている可視化システムに繋ぐためにデータのフローを理解するのは非常に重要です。 一方で、データ分析者の分析の多くはPythonやRなどのプログラミング言語でなされており、専門外であることも多い ステークホルダーにとってはその分析自体がブラックボックス として捉えられてしまいます。この状態では上記で挙げたような精度を上げるための深いフィードバックや、もっと手前の課題設定が正当性の把握などが遅れてしまい、精度が上がるのに時間がかかったり、導入に至らず課題設定からやり直したりするようなことが起きてしまいます。 【 壁その2:コミュニケーションの重さ 】 ステークホルダーとのすり合わせを行うために、データ分析者とステークホルダーで例えば2週間に1回打ち合わせをすることがあるでしょう(ステークホルダーは大体忙しいので意思決定者が御登場するのは1ヶ月に1回かもしれない)。この時データ分析者は打ち合わせのために自分の統計量の可視化結果や予測値と実測値のグラフ、はたまた特徴量の設計方法などをJupyterLabのスクショをとってスライドにぺたぺた貼って、さらにステークホルダーにも分かるように平易な言葉で説明を並べ、2週間空いているので課題やKPIも改めて確認して...などの報告書を作成します。これはデータ分析者にとっては重労働で、 本来は分析に利用したい稼働の大半を報告書作りに割いてしまう こととなります。 さらに打ち合わせの中では月一参加の意思決定者から課題設定が正当でないことを指摘されてしまったり、現場の運用者から新しい知見が与えられて前処理を追加しなければならない、といったことが起きます。これによりデータ分析者の2週間の稼働は無駄になってしまうことも多々あります。打ち合わせの間隔を1週間にすればこれも防げるように思えますが、先に述べた報告資料の重さを考えると2週間程度のスパンは必要ですし、まさにデッドロック状態になってしまいます。このように コミュニケーションの重さからフィードバックの間隔が空いてしまうことにより、最終的な課題解決のための右往左往が増えてしまうことになります。 そうやっている間に投資判断を行う人から継続をNGにされてしまうこともあるでしょう。 【 壁その3:導入の判断と導入自体の難しさ 】 上記の壁をなんとか乗り越え、分析がある程度進みモデルが作成できたとしましょう。ここからは導入に向けた壁を乗り越える必要があります。 導入には意思決定者のGoサインが必要になります。これを判断する際問題になるのが「モデルの信頼性」です。例えば1時間後の不純物の割合の予測によると、不純物が減る兆候を示しており、それをもとに運用者が攪拌のスピードを上げるとしましょう。もしこのモデルの予測が誤っていた場合、攪拌のスピードアップによってプラントが傷ついてしまい生産がストップする...と言うこともあり得ます。これを防ぐためには様々な方法がありますが、その1つとしてモデルが何を見て予測をしているかを明らかにし、それが現場で知られている知見と整合性が取れるか確かめるというものがあります。運用者の間では攪拌の際温度を見ながら操作することが提唱されていると言うような知見があれば、モデルが温度を入力として重要視しているかを確かめることで、そのモデルの信頼性を測ることができます。一方、 昨今利用される非線形性を含む複雑なモデルではそのような予測の判断根拠を抽出するのは難しく、さらに抽出したとしても説明自体を解釈するにはより専門的な知識が必要である と言う問題があります(この分野はXAIと呼ばれ、私が専門としている分野で色々と語りたいですが、ここでは割愛)。 また、モデルが仮にTensorflowで用いられるSavedModel形式で保存されていたり、前処理がsklearnのpreprocessingで行われているとしましょう。このモデルの導入や運用を行う工場のシステム担当者にとっては、 作成されたモデルを扱うにはPythonといった言語や機械学習ライブラリ、Webアプリケーションの知識が必要であり、かなりハードルが高い ものとなります。したがって、技術的な観点でも現場にモデルを導入していくことは課題があります。 Node-AIの解決策 ここまで長々と課題を述べてきましたが、ここからはそういった課題を解決するためにNode-AIがどのような特徴を持っているのか、説明していきたいと思います。 Node-AIが他のツールと比べて最も異なるのは、上に挙げたような「ステークホルダーとのコラボレーション」を重要視している点です。 今までのツールのようにデータ分析者のみが分析を効率化するのに使うのではなく、ステークホルダーも含めて利用することでデータ分析での課題解決全体を効率化することを目的 とした一風変わった特徴を持っています。すなわち、最終的な課題解決のために必要不可欠な分析の理解のためのコミュニケーションや、導入判断などを打ち合わせだけではなく、ブラウザからアクセスするNode-AIの上で全部やってしまおうと言うアイデアです。したがって、単なる機械学習モデルを作成するツールではなく、 データ分析プロジェクトを効率化するツール と言う側面も持っています。以下の項ではそれをどのように実現しているか、いわゆるHowな部分を紐解いていきます。 【 ビジュアルプログラミング 】 Node-AIは上記の画像の通り、モジュール(我々はカードと呼んでいます)をドラッグ&ドロップでつなげることでデータの前処理、モデルの定義、学習、評価といった一連の分析のフローを実現します。また、各カード内で処理されたデータやその統計量はカードを開くことで逐一確認できるようになっています。これにより、プログラミングの知識のないユーザでも機械学習モデルの作成を行うことができるだけでなく、 分析フローが可視化される と言うメリットがあります。 これにより、課題の1つのステークホルダーとの分析内容の相互理解が促進され、 データ分析者だけでなくステークホルダーにとってもどのように分析しているかが明確になり、分析工程自体がブラックボックスになることを防ぎます。 意思決定者にとってはどのような分析が行われているか知ることで分析の信頼性を得ることが容易になりますし、データの専門家にとっても具体的なパラメータに対するアドバイスがやりやすくなります。加えて、データ分析者にとっても、複数人で分析する場合にその分析フローを引き継いだり比較しやすくなると言うメリットもあります。 【 コラボレーション機能 】 報告資料作成やフィードバックが早くもらえないというコミュニケーションの重さも課題解決の壁の1つでした。 Node-AIでは可視化された分析フローの横でMarkdown形式による資料作成 ができます。今まではスライドに可視化した画像を貼り付けたり、前処理の中身を書いたりと、報告書を作成するのに時間がかかっていましたが、可視化すべきデータや分析フローはNode-AI上に全てあるので資料作成のためにデータを取り出す必要もありません。さらにデータの全容もその場で確認できるため、より深い議論が可能になります。 また、フィードバックに関しても、 コメント機能を利用することでステークホルダーにメンションを飛ばして質問できます 。メンションされたコメントはユーザにメールで通知され、ユーザはそのコメントを見に行くことができます。これにより分析中に出てくる相談をクイックに行うことで、打ち合わせのみと比べて効率的な分析を行えます。 ちなみにNode-AIの分析フローを作成する場所をキャンバスと呼んでいて、文字を書いたり分析フローを作ったりできるまるで自由帳のような働きができるようにしています。 【要因分析・因果分析機能】 導入の難しさの原因の1つは機械学習モデルがブラックボックスであることです。この対応のためにNode-AIでは要因分析、いわゆるモデルの説明性を可視化する機能を備えています。この機能では予測に対してどの特徴のどの遅れ時間のデータが効いていたのかを重要度としてヒートマップで表示できます。さらに、ニューラルネットワークを利用した場合は各時刻における重要度の変化を閲覧することもできます。 さらに、特徴間の時系列を考慮した因果関係を可視化する因果分析機能を兼ね備えており、特徴量の選択やシステムの理解に役立ちます。例えば制御を意味する特徴が入っているデータの場合にはその特徴を変化させるとどういった結果が得られるのかなど、実応用の考察に必要な情報を与えることができます。 【開発したモデルを他システムに組み込みやすくする機能】 モデルの利用に対して機械学習の知識が必要になることから生じるモデル利用の困難さに対しては、Node-AI Berryというモデルを簡単に利用するためのライブラリを用意しています。このライブラリはNode-AIからダウンロードしたモデルを利用して、データを受け取って前処理からモデルの推論までを行いその結果を返すAPIを簡単に作成できます。具体的にはNode-AIからモデルファイルをダウンロードし、所定のフォルダにおいた状態で docker run するだけです。 さらに、学習データを与えることでそのモデルを再学習するような機能もついているので、課題解決の先にあるモデルの運用という部分もカバーしています。 小括 ここまでデータ分析による課題解決の困難さ・難しさを述べてきました。また、それをNode-AIでどのように解決しようとしているかを紹介しました。 世の中で様々なデータ分析案件が現れ、そして失敗していくのを見てきました(し、自分たちでも失敗してきた)が、一方で私たちはデータ分析の導入は既存のビジネスに絶大な効果を齎すことを信じています。実際に様々なデータ分析案件が真に課題を解決し、色々なところで成果を出し始めています。この流れを加速させるための道具として私たちはNode-AIというサービスを開発しています。(すっごい雑にいうと、もっと全員でデータ分析で色々解決しようぜ!って感じです。) Node-AIで扱う技術領域 本章の紹介は、 Node-AI のテックリードをしております、内藤理大(github.com/ridai)から紹介させていただきます。 Node-AIの開発では、コンテナ技術からアプリケーションの設計・実装まで、幅広い技術を検証した上で利用しています。 本章では、簡単に扱っている技術を紹介したいと思います。(各技術の中で苦労した話や工夫した話は後日、別記事にて紹介させていただきますので、よろしければそちらもご覧ください。) Infra技術  Node-AIは「機械学習を取り扱う」という性質上、GPUを備えたサーバもdeploy先環境として取り扱っています。具体的には、NTT ComAIインフラPJが提供するオンプレミスのGPUクラスタ環境と、GCPのComputeEngine環境を利用しています。(AWSやAzureでの構築も検討中)  また、開発環境と本番環境をコードで管理し、同一の環境を簡単に構築するためにも、アプリケーション自体はDockerコンテナ上に構築しています。  そして、アプリケーションを構成する複数のコンテナの管理を楽にするために、docker-composeを利用するパターンと、より複雑なオーケストレーションを行うためにKubernetesを利用するパターンを用意しています。 CI/CD/テスト  Node-AIでは、開発プロセス中のテストやdeploy作業の自動化にも取り組んでいます。テストについては、開発したPythonモジュールの機能テストのほか、RobotFrameworkを用いたエンドツーエンドテストを用意しています。  まず、テストの流れとしては開発した機能をGithubの作業ブランチにpushするたびに、CircleCIまたはGithubActionsを使った機能の単体テストが自動で行われます。また、本番環境にdeployを行う際には、NTT ComプロダクトのQmonusを利用して機能テストとエンドツーエンドテストが自動で行われます。  次に、deployの流れとしては、同様にQmonusを利用し、ターゲットのVM環境にアプリケーションを自動で配信し、立ち上げを行います。 コード管理  Node-AIでは、Gitを用いてアプリケーションのソースコードのリビジョン管理を行なっています。そして、それらコードのリポジトリとして、GitHubを利用しています。 UI/UX  Node-AIのフロントエンドは、Vue.js + Typescriptを用いたSinglePageApplicationがベースとなっています。ユーザの入力やサーバでの処理結果を動的に処理し、単一のページ上で軽快に機械学習が実行できるUXを提供しています。  また、UIについても、ElementUIを用いたデザインの統一と、SCSSを用いたスタイル指定の効率化に取り組んでいます。 Webアプリ開発  機械学習ではPythonでのライブラリ開発が活発であることから、Node-AI開発でもPythonを採用した開発しています。そして、Webアプリケーションフレームワークとしては、django/dinago REST frameworkを採用しています。  また、機械学習は非常に処理時間がかかるため、celeryというタスクのキューイング処理フレームワーク採用して、非同期的に学習が実行できる仕組みにしています。  他にも、実行に関わる永続データの保持には、MySQLなどのRDBMSの他、ObjectStorageを採用しています。これらデータの保存サービスとして、MySQLやMinIOをDockerコンテナとして立ち上げて利用できるほか、GCPのSaaSサービス等も利用できるような仕組みとなっています。  開発チームでは、これら仕組みを効率的に実装・実現させるため、DDDという開発手法とCleanArchitectureという設計思想を用いています。 機械学習  Node-AIでは、数値の時系列データをターゲットとした、異常検知と予測モデルの作成を可能としています。そこで用いる学習処理としては、sk-learnを用いた線形回帰処理と、tensorflowを用いたNeuralNetworkモデルの回帰/自己符号化器処理となります。  しかし、もっとも特徴的な機能は、「要因分析の可視化」や「因果分析」といったComオリジナルの技術にあります。この機能は、Comの機械学習の研究者による成果であり、Node-AIの開発者と共同で研究技術の導入・提供を行なうことができる体制になっているため、実現できています。  他にも、tensorflowを用いた処理では、optunaを用いたハイパーパラメータの自動探索機能とGPUリソースを判別して利用する仕組みを導入しています。また、mlflowを用いて、ユーザの学習処理の記録も保持しているため、結果の比較・検討も容易にできる仕組みになっています。 Node-AIのアジャイル内製開発 Node-AIチームのスクラムマスター(最近は社内のアジャイル開発案件支援をしながら社内へのアジャイル開発普及活動も)やってます小澤です。ここではNode-AIチームが日々どのように開発業務を行っているかご紹介したいと思います。 Node-AIの扱うAI(機械学習)やデータ分析といった分野は、最新のアルゴリズムは日々更新されお客様の求めるニーズも変化しやすい等、変化が激しく不確実性の高い領域です。これに対応するため、Node-AI開発では初期からアジャイル開発手法の1つである「スクラム」という開発フレームワーク(下図参照、詳細な説明は割愛します)を採用することでアジャイルに内製開発を進めています。 【スクラムフレームワーク】 チームメンバー構成としては、 プロダクトオーナー(認定資格(CSPO)) 1名 スクラムマスター(認定資格(A-CSM)) 1名 開発者(フロントエンド、バックエンド、QA等) 9名 デザイナー 2名 となっています。スクラムに関して外部認定資格を持ったメンバーもおり、それぞれ専門的な知識とスキルを持った10名程度の少数精鋭メンバーでチームが構成されています。 【スクラムチーム】 またスクラムは「スプリント」と呼ばれる通常1〜4週間といった比較的短期間の固定の時間枠(タイムボックス)を定め、この中で優先度の高い機能から順次開発したり、様々なスクラムイベント(後述)などを行っていきます(短期間のスプリントを何度も繰り返すことで変化する顧客や市場のニーズに迅速に対応しながら開発します)。Node-AIチームでは1週間スプリントを採用しています。大まかな1週間の流れとしては以下の通りです。 水曜PM スプリントプランニング(2−4時間程度): スプリントの始まり、スプリントの詳細な計画をチーム全員で立てます(このスプリントのゴールは何か、どんな機能をどのように作るのか、等を決めます)。 木曜〜火曜 朝会(毎日15分): 毎日決まった時間にチーム全員が集まり、15分間でチームの一日の行動計画を立てます。障害になっていること(課題)の共有も行い、朝会後に別途課題解決の時間を設けることもあります(2次会)。 プロダクトバックログリファインメント(週1回1時間): 直近のスプリントで実施予定のプロダクトバックログ(機能等)に関して、開発者とプロダクトオーナーが協力して仕様の詳細化や開発規模の見積もりをします。 デザインミーティング(週1回1時間): スクラムに定義されていないNode-AIチーム独自のイベントです。デザイナー、プロダクトオーナー、開発者が協力し、新規機能の画面デザイン検討やユーザーヒアリング等の結果を受けて既存機能のUI/UX改善に関する議論をしたり、ペルソナの作成・修正等を行います。 水曜AM スプリントレビュー(1時間): そのスプリントで完成した成果物(Node-AIの場合は動くソフトウェア)を実際の顧客などのステークホルダーにデモのような形で披露しフィードバックを得るイベントです。このフィードバックによってその時点で顧客にとって本当に価値のあるプロダクトになっているかチーム全体で確認ができ、またプロダクトの価値の最適化(プロダクトバックログやリリース計画の見直し等)を図ることができます。 スプリントレトロスペクティブ(1時間〜1.5時間): スプリントの最後に行われ、スプリントの振り返りを行うイベントです。そのスプリントにおけるスクラムチームの動きなど主にプロセスに注目して振り返りを行い、良かった点や改善したほうが良い点・改善案などを出し合います。Node-AIチームではKPT(Keep・Problem・Try)やFDL(Fun・Done・Learn)、Timelineといったフレームワークを使ったり、みんなで雑談しながらチーム状況を振り返ったりと、いろいろと状況によって使い分けたりしています。 【振り返り例(FDL)】 1週間スプリントだとイベントなどのコストが増えやすいですが、その分細かく軌道修正できる・詳細な計画が立てやすい・改善が進みやすいなどのメリットもあります(個人的には特にアジャイル開発に慣れていないチームほど短いスプリントをおすすめします)。またNode-AIチームでは各イベントのファシリテーター(進行役)を固定せず、ローテーションで回しています。これによりファシリテーターの負荷分散やチームメンバーの誰もがチームをリードする意識、当事者意識を持てるように工夫しています。 現在、日々の開発やイベントはすべてリモートワークで行っています。スクラムでの開発ではチーム連携が非常に重要であるため、NeWorkやSlack、Trello等のコミュニケーションツールやタスク管理ツールをフルで活用しながら、リモート環境下でも、なるべく誰がどこでどのような作業をしているのか可視化したり、ペア作業や雑談等を行いやすい環境を整えています。 またここ最近は実施できていませんが、チームの結束を高めたり、チームビルディングを効果的に行うために「開発合宿」を定期的に実施したりしています。こういった活動もアフターコロナにおいては積極的に実施していきたいと思っています。 【開発合宿】 終わりに ここまで長文を読んでいただきありがとうございました。今回の記事から技術部分も開発体制もさらに掘り下げて様々な発信をしていこうと考えていますので、今後ともNode-AIをよろしくお願いします!
この記事は、 NTT Communications Advent Calendar 2021  25日目の記事です。 はじめに こんにちは、データプラットフォームサービス部(以下DPS部)白土です。 普段は企画部門で部内メンバーの業務支援やスキルアップ支援を行っています。 今回は、DPSとして行っているスキルアップ支援の具体的な取り組み内容を紹介します。(NTT Com全社施策ではなく部署独自の取り組みです。) ※Engineers' Blogに記載していますが、技術的な内容ではなく、Engineerのスキルアップ等に向けた社内の仕組みについて紹介です。 背景 これまで会社負担で業務に必要なスキルアップを目的とする研修への参加方法は、以下が主流でした。 ヒューマンリソース部や所属部署が主催する研修への参加 自身で上長や予算管理部門へ交渉して決裁を取得した上で実施 しかし、新しい技術の取得や自律的な学びを促進する上で、以下のような課題が出てきました。 社員 忙しい中で、会社主催の長時間・日にち限定の研修に参加しづらい 会社が提供する研修以外の技術・領域の学習が必要になっている 個人で研修の申し込みは、事務処理が必要(で面倒) 組織 研修費用を抑えつつ、自律的な学びを促進したい ネットワークからクラウド、IoT、まで幅広い学びの機会を提供するのは困難 こんな課題の中で、 早い・安い・うまい、みたいな研修スキームを提供できないか と、と考えたのが今回の取り組みです。 取組内容① DPS学び放題 まず始めたのが課題部分で記述した社員の負担をできるだけ削減する事を目的とした「DPS学び放題」という取り組みです。 DPS学び放題とは? Udemyなど、オンライン研修を基本に社員が自身のキャリアや業務に関連する研修コンテンツを自由に選択して受講できるスキーム。 社員の申し込みフロー オンライン研修(MOOC)を基本として、社員が受けたい研修を選んでくる 上長と相談し、スキルアップの方向性の合意・稼働時間の確保 アンケートフォームに事前申請して申し込み完了(決裁不要) またこの取り組みについてはもう1点狙いがあり、社員自身が担当領域において新たに必要となるスキルは何かを自発的に考え、継続的にスキルアップに取り組む事を期待して行っています。(代わりに、予算が青天井になってしまう恐怖を担当者は毎年味わっています笑) 導入効果 導入すると、実際に以下のような効果が出てきました。 受講数の拡大 【導入前】全社研修の受講率 17% 【導入後】学び放題利用率 50% 受講領域の拡大(データサイエンス・データ解析領域など) オンライン研修を推進したことで、時間や場所に制限なく、また丸2日研修で仕事を留守にするようなこともなく、これまで受講したことがなかった社員も気軽に活用する事が可能になったのが大きかったです。 2人に1人受講しているけれど、予算的には大丈夫なの?と思うかもしれませんが、オンライン研修はコスパがとてもよいので広く呼び掛けることができました。 取組内容②ジョブ部活「デザインチーム」「データ活用部」 「DPS学び放題」については、社員からも好評で利用者も増えていたのですが、一方で オンライン研修中心のスキルアップが中心になると、課題解決の糸口がつかめない等の壁にあたってしまったときに気軽に相談する相手がいないという新たな課題も出てきました。オンサイトの勤務環境であれば、講師や同僚等と気軽にコミュニケーションをとって解決できていた場合も多かったと思います。しかしながら、NTT ComではCOVID-19以降は在宅勤務を推奨しており、DPSにおける在宅勤務率も90%前後をキープしている状況のため、従来の方法では対応が難しい状況でした。 そこで、組織として取得を推進している共通スキル「デザインシンキング」「データドリブン」に関するスキルを学び、共に高めあう事を目的に「デザインチーム」「データ活用部」という2つのジョブ部活を設立しました。(デザインチームは2019年度、データ活用部は2020年度設立) 活動メンバはDPS部内で公募された有志で、主な活動内容としては毎週の定例会、活動領域別チームにおけるノウハウや悩みの共有、半期毎の成果報告会等を実施しています。年度が進む度に参加メンバも拡大し、実業務における活用までつながってきています。活動が順調に進んできた背景として、デザインチームについては KOEL 、データ活用部はデータドリブンマネジメント推進部門(デジタル改革推進部)という社内CoE (Center of Excellence) 組織にサポートいただいたため、よりアクティブな活動につながりました。 こういった活動のメリットとして、お互いのスキルやノウハウを共有するという当初の目的がある事はもちろんなのですが、これまで業務において関わりがなかった社員間での横のつながりが発生する事も非常に大きいです。本Engineers' Blog過去記事の「 社内データ分析コンペティション 」も、そういったつながりから生まれた企画のひとつです。 また、研修という領域を一歩超えて、デザインやデータサイエンスの領域を学びながら業務に取り入れていくこと、業務の課題をこの部活動に持ち込み、解決に向けて活動することがこの部活動の大きなメリットやモチベーションになっています。 先述したように、COVID-19以降、NTT Comはリモートワークを主体としている中で、既存メンバはもちろん、新入社員や新規採用メンバは新たなコミュニケーションのきっかけが不足している状態でした。 そういった社員間の新しい繋がりを推進している事も実施して良かった点となっています。 最後に これまでDPS内のスキルアップ支援取り組みについて語ってきましたが、本取組において最も重要なのは仕組みや制度を作る事だけではなく、課題認識や目標等といった共通の価値観を関係者の中で持っておくことが大事だと考えています。 この制度を使って、どういう姿を、なぜ目指してほしいのかその部分を事前に関係者/活動の参加者にも伝えていく事で、取り組み自体の質が上がっていくと考えています。
はじめに こちらは NTT Communions Advent Calender 2021 の 24 日目の記事です。 はじめまして、データプラットフォームサービス部の tnkgw と申します。 普段は、Smart Data Platform の契約管理機能を開発しています。 本記事では、クラウドネイティブデータベースを実現する技術の一端を理解するということでAlibaba Cloud で提供されている PolarDB のファイルシステムである PolarFS で用いられている分散合意プロトコルの ParallelRaft 1 について解説します。 Raft Raft は Ongaro らにより提唱された 2 理解と実装のしやすさに重きをおいて考案された分散合意アルゴリズムです。 この章では、本記事を読むにあたって前提知識となる Raft の各要素について概要を紹介します。 Raft が実現すること Raft は、いくつかのノードで構成されたクラスタにおいて各ノードにログエントリという形でデータを複製することでクラスタとして一貫した状態をクライアントへ提供できます。 また、クラスタ内のノードが一貫した状態を持つことによりノードが故障しても残ったノードで継続して稼働し続けることによりクラスタの可用性を実現します。 各用語について まず、Raft において扱われる各用語について説明します。 ノードの役割 Leader 後述する Leader Election により選ばれるノードに割り当てられる。 クライアントからのクラスタへのリクエストは全て Leader に対して送信され、そのリクエスト内容は後述のログレプリケーションにより Follower ノードへ複製される。 Leader は、自身が故障していないことを知らせるためにハートビートメッセージをクラスタ内の各ノードへ送信する。 Follower Leader から受け取ったログエントリを含むメッセージや後述する Candidate から受け取った投票要求メッセージに対してアクションを行うのみの役割。 Candidate Follower から遷移して、次の Leader となる役割。 ログエントリ クラスタが受信したデータ等を保持しておく形式で、配列で各ノードは保持する。ログエントリには、複製されるデータ本体の他にログエントリが作成されたタームの値とインデックスの値が含まれる。 インデックス 各ノードが連続した順番で保持するログエントリに対して振られる番号。 ターム Raft における論理時間で、単調増加する数値で表現される。Raft における合意はその区切られたタームにて行われる。また、1つのタームにおいて Leader はクラスタ内に1つのみ存在するという制約がある。 図1. Raft におけるタームのイメージ Leader Election Leader Election では、クラスタ内の各タームの唯一の Leader となるノードを決定するフェーズとなります。 Leader Election はクラスタが始動し始めたタイミングや予め存在していた Leader のノードが故障して Leader が不在となったタイミングで行われます。 Leader Election の流れについては以下の順番で行われます。 クラスタの初期状態は、全てのノードは Follower からスタートする。各ノードは、それぞれランダムな値でタイムアウトするタイマーを持っておりメッセージを受信しない場合タイマーの値はデクリメントしていく。 タイムアウト後、Follower は Candidate へ遷移する。 Candidate へ遷移したノードは、自身が保持しているタームの値を1つ増やして、投票要求のメッセージを自身以外のノードへ送信される。 投票要求メッセージには、送信時のタームの値と保持しているログエントリの最新のインデックスが含まれる。 投票要求メッセージを受信した Follower は基本的に最初に受け取ったメッセージに対して true の値を返信する。 加えて投票要求メッセージに含まれるタームの値とインデックスの値に対して以下の条件を判定要素とする。 投票要求メッセージに含まれるタームの値 > 自身の最新のログエントリのタームの値 投票要求メッセージに含まれるインデックスの値 > 自身のログエントリのインデックスの値 (自身の最新のログエントリのタームの値と同じ場合) Candidate は過半数の Follower から true のメッセージを受信すると Leader へ遷移する。 図2. Leader Election の流れ また、ノードの各役割の遷移は以下のようになります。 図3. Raft のノードの役割の遷移図 Log Replication Log Replication では、Leader が保持するログエントリをクラスタ内の他のノードへ複製するフェーズです。 このフェーズによりクラスタ内のノード間の一貫性した状態がつくられて、クラスタの可用性を実現します。 Log Replication の流れについては以下の順番で行われます。 クラスタへリクエストがあった場合は、Leader がそのリクエストを受け取る。Leader は、現在のタームの値を含めたログエントリを作成して、リストへ追加する。 Leader は作成したログエントリをログエントリ複製メッセージに含めて Follower ノードへ送信する。また、メッセージには、送信するログエントリの1つ前のログエントリのタームの値とインデックスの値を含める。 Follower は、受け取ったメッセージについて自身が保持しているログエントリとの関係を判定する。判定には、受け取ったログエントリの1つ前のタームとインデックスの値を用いる。 Follower は、このタームとインデックスの値が保持する先頭のログエントリのものと一致するか判定する。一致した場合、受信したログエントリを自身のリストへ追加して、Leader に true の値を返信する。 また、異なった場合は false の値を返信する。 Leader は、過半数の Follower から true の値を受信すると追加したログエントリをコミットする。その後、Leader はコミットしたログエントリのインデックスを含めたメッセージを送信する。 Follower は、Leader から受信したコミットされたインデックスの値を確認してそのインデックスまでの自身のログエントリをコミットする。 図4. P1 を Leader とした Log Replication の例 ParallelRaft ParallelRaft は、PolarFS における ストレージサーバ間の一貫性と可用性を実現するために Raft をカスタマイズして考案された分散合意プロトコルです。 既存の Raft と大きく異なるポイントとしては、Raft の根本的なコンセプトである厳密な順序でログエントリを複製するという制約をなくしていることです。 そのような制約をなくしたうえで ParallelRaft がどのようにノード間の一貫性を実現しているか各要素について紹介していきます。 Raft の課題 まず、PolarFS の開発チームが Raft を改善するに至った背景について紹介します。 Raft を高スループットな分散化されたストレージサーバの可用性を実現するために用いた場合以下のような理由でパフォーマンスに課題があります。(原論文) Raft の作成されたログエントリを順繰りに複製・コミットするという特性上、クラスタに対して書き込み要求が多数実行された場合も順繰りに適用されるため待ち時間が発生してスループットが低下する。 複数のコネクションを張ったノード間では、ネットワーク遅延等で必ずしも順繰りにログが届かずに行き違いが発生してしまう可能性がある。 行き違いが発生した場合、そのログエントリは Leader が再送しなければならず場合によっては Leader のコミットが遅れる。 しかし、通信網の可用性と高度な同時処理環境を実現するためには複数のコネクションが必要となる。 図5. ログエントリがインデックスの順番通りに届かず、コミットが遅れる例 Out-of-Order Log Replication Out-of-Order Log Replication は、上記の Raft の課題を解決するために ParallelRaft において提案されている Log Replication の手法です。 Out-of-Order Log Replication では、 Out-of-Order Acknowledge と Out-of-Order Commit の2つのステップによりノード間の一貫性が実現されます。 Out-of-Order Acknowledge Follower が Leader から受信したログエントリは自身が保持するログエントリとの関係性を確認せず即時に追加する。 変更の背景として、順繰りにログエントリを受け取る過程で発生する余分な待ち時間による書き込みのレイテンシを短縮するため。 Out-of-Order Commit Leader は、過半数の Follower に対して複製できたと確認できた時点で先行するログエントリがコミットされていなくてもコミットする。 このようなコミットセマンティクスについては、強力な一貫性を保証しないストレージシステムにとっては許容できるものとなっている。(原論文) 図6. ログリストが欠落している例 以上のように順不同でログエントリを追加、コミットした場合、Raft のように厳密な順序を保証されたうえでの複製ではないため前後のログエントリが欠落して書き込まれる可能性があります。 そこでログエントリをストレージに書き込む際、書き込まれる LBA が重複しないように Look Behind Buffer というデータ構造をログエントリに追加します。 Look Behind Buffer には、直前の N 個のログが変更した LBA の情報が含まれています。 Follower は、このデータ構造を参照することで欠落したログを補完することが可能となります。 ParallelRaft における Leader Election 次に ParallelRaft における Leader Election について紹介します。 ParallelRaft における Leader Election でも複製された最新のログレプリケーションを保持する Candidate が Leader に選ばれることは同じです。 ただし、前述したとおり ParallelRaft では Follower がログリストを欠落した状態で保持している可能性があり、投票において Candidate へ遷移したノードもこれに当てはまります。 それでは、そのような状況下で全てのログエントリを保持する Leader を選出するにはどうすれば良いでしょうか? そこでParallelRaft の Leader Election では、Candidate から Leader へ遷移する前にノード同士でログをマージするフェーズが追加されます。 そのフェーズでは、Candidate が Leader Candidate 、Follower が Follower Candidate というテンポラリな役割に遷移します。 また、ログエントリをマージする際には以下の制約が設けられます。(原論文) Leader Candidate においてコミットされているが、保持されていないログエントリについては1つ以上の Follower Candidate が保持している。 いずれの Leader Candidate、Follower Candidate にコミットされず保存もされていないログエントリについては、Raft と同様にそのログエントリを無視してもよい。 コミットされていないログエントリを保存している Follower Candidate が存在した場合、Leader Candidate は最も最新のタームの値を持つログエントリを有効なものとして認識する。 マージステージを含んだ Leader Election の流れについては、以下の順序で実行されます。 Follower から Candidate へ遷移して、他の Candidate へ投票するまでの課程は Raft と同じ Follower Candidate が Leader Candidate に自身のログエントリを送信する Leader Candidate は、受け取ったこれらのログエントリと自身のログエントリをマージする Leader Candidate は、マージして補完されたログエントリを Follower Candidate へ送信して状態を同期する Leader Candidate は、マージされたログエントリを全てコミットしてそれを他の Follower Candidate へ通知する Leader Candidate と Follower Candidate はそれぞれ Leader と Follower へ遷移する 図7. P1 = Leader Candidate、P2, P3 = Follower Candidate としたときのマージステージの例 以上のステップによって、新しい Leader が全てのコミットされたログエントリを保持する状態が作られます。 Correctness of ParallelRaft ParallelRaft の整合性について紹介するためにまず Raft における整合性を担保するための制約を紹介します。 Raft では、以下が保証されています。(原論文) Election Safety 1つのタームにおいて、Leader は1つしか存在しない Leader Append-Only Leader は自身のログリストに対して追加のみ行う Log Matching 2つのログリストを比較した際、あるログエントリが同じインデックスで同じタームの値をもつ場合にそれまでのログエントリは同一である Leader Completeness Leader はそれまでにコミットされたログエントリを全て保持する State Machine Safety 全ノードはステートマシンに対して同じ順序で同じログエントリを適用する これらの制約に対して、ParallelRaft では Election Safety 、 Leader Append-Only 、 Log Machine については変更されていないためこれら3つの制約は保証されます。 Leader Completeness と State Machine Safety については ParallelRaft による変更に対して以下の理由で制約が保証されます。 Leader Completeness ParallelRaft の Leader Election において、ログエントリのマージステップがあるため保証される State Machine Safety Look Behind Buffer によって、競合せずにログエントリはログリストへ適用されるため さいごに 今回は、クラウドネイティブデータベースを実現する技術の一端を理解するということで ParallelRaft について解説してみました。 ParallelRaft は、あくまでストレージサーバ間の一貫性を実現するプロトコルでありクラウドネイティブサーバを実現する技術は多岐に渡りますのでこれをきっかけに他の技術要素についても知見を深めたいと思いました。 また、Raft については近年の分散システムにおいて広く利用されており今後も独自にカスタマイズされたプロトコルが提案されていくと思いますので、それらについても関心を持って調査していきたいです。 この記事を読まれることによって、分散システムを支えるプロトコルやクラウドネイティブデータベースを支える技術に少しでも関心を持っていただけると幸いです。 明日は、ついに NTT Communions Advent Calender 2021 の最終日です。明日の記事もお楽しみに! 参考文献 Wei Cao, Zhenjun Liu, Peng Wang, et al. PolarFS: An Ultra-low Latency and Failure Resilient Distributed File System for Shared Storage Cloud Database ↩ D. Ongaro and J. K. Ousterhout. In Search an Understandable Consensus Algorithm (Extended Version) ↩
この記事は NTTコミュニケーションズ Advent Calendar 2021 23日目の記事です。 はじめに こんにちは。デジタル改革推進部の髙田( @mikit_t )です。 業務では社内向けのデータ分析基盤の設計・開発および運用を行なっています。 データドリブン経営を推進するため、社内に散らばる様々なデータを収集・蓄積。データサイエンティストはもちろんのこと、各部署でのデータドリブンな意思決定に貢献できるよう活動しています。 社内のデータ分析コンペティションの環境も我々の分析基盤上で開催しています。 今回はデータ分析については触れませんので、データ分析に興味がある方は 社内でデータ分析コンペティションを開催しました の記事を参照してみてください。 本稿では、オンプレでサーバ運用するにあたって必ず必要になってくる DNSフルリゾルバ のうち、比較的新しい実装の Knot Resolver を紹介します。 書き始めたら大変長くなってしまったので、何回かに分割して掲載していきたいと思います。 DNSフルリゾルバ おさらい アプリケーションがホスト名による通信するためには、ホスト名を IPアドレスに変換する必要があります。 一般に、これを名前解決といいます。 名前解決をしてくれるのが「フルリゾルバ」、フルリゾルバに対して名前解決要求を出すのが「スタブリゾルバ」です。 「フルリゾルバ」は OS のネットワーク設定のところに設定する IP アドレスというとわかりやすいかもしれません。 Knot Resolver 公式URL: https://www.knot-resolver.cz/ チェコ CZ NIC がメンテしている実装で、2014年ごろ登場しました。 シンプルな core と拡張モジュールでの実装となっています。 拡張モジュール開発はユーザも任意に行えます。言語としては C, Lua, Go が使えます。 設定自体も Lua で書きます。 ほかのフルリゾルバ実装ではあまり見かけない、魅力的な特徴は以下の通りです。 かなりモダンな設計になっています。 シングルスタックでの実装となっており、複雑なスレッドプログラミングをしていない キャッシュのバックエンドを永続化できる バックエンドには lmdb , etcd を利用できる インスタンス起動時に「あたためた」キャッシュをプリロードできる Zero downtime restart 複数インスタンスを並列起動することが推奨されている インスタンスをひとつずつ順番に再起動することで、ダウンタイムを 0 にできる BIND でいう rndc、unbound でいう unbound-control のような管理ツールが用意されていない UNIX ドメインソケット経由で、設定の確認・変更を行える Prometheus メトリクスのエンドポイントを内蔵している まだ開発途上の新しいソフトウェアのため、設定項目名がカジュアルに変更されたり、メモリリークの修正が入ったりなどしているのが観測されています。 利用する場合はアップデートの際に ChangeLog をよく読むことをお勧めします。 利用実績としては Cloudflare が利用していると発表しています。 インストール・起動設定 インストール コマンドラインについては Ubuntu 20.04 LTS で実施した内容となっています。 https://www.knot-resolver.cz/download/ の通りやっていきます。 # wget https://secure.nic.cz/files/knot-resolver/knot-resolver-release.deb # dpkg -i knot-resolver-release.deb # apt update # apt install -y knot-resolver Ubuntu 20.04 LTS の公式レポジトリにも Knot Resolver はありますが、 3.2.1-3ubuntu2 と古いバージョンのコードベースとなっています。 執筆時点での最新版は 5.4.3 となっています。新しい機能を使うためには、CZNIC 公式レポジトリからのインストールをする必要があります。 systemd-resolved を止める ローカルインタフェースの port 53 を listen している systemd-resolved を止めます。 $ sudo systemctl stop systemd-resolved.service $ sudo systemctl disable systemd-resolved.service systemd-resolved に名前解決要求をするようになっているため、これを無効化します。 $ sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf 起動設定 パッケージをインストールしてもサービスが有効化されませんので、やっておきます。 $ sudo systemctl enable --now kresd@1.service 確認 動いているか確認してみます。 $ systemctl |grep kres kres-cache-gc.service loaded active running Knot Resolver Garbage Collector daemon kresd@1.service loaded active running Knot Resolver daemon system-kresd.slice loaded active active system-kresd.slice サービス起動OK。ひとつ DNS 名前解決してみましょう。 $ dig engineers.ntt.com @127.0.0.1 ; <<>> DiG 9.16.1-Ubuntu <<>> engineers.ntt.com @127.0.0.1 ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 26986 ;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 1232 ;; QUESTION SECTION: ;engineers.ntt.com. IN A ;; ANSWER SECTION: engineers.ntt.com. 300 IN A 13.115.18.61 engineers.ntt.com. 300 IN A 13.230.115.161 ;; Query time: 120 msec ;; SERVER: 127.0.0.1#53(127.0.0.1) ;; WHEN: Thu Dec 23 02:20:55 UTC 2021 ;; MSG SIZE rcvd: 78 OK です。 設定 デフォルト設定 デフォルトの設定ファイルが /etc/knot-resolver/kresd.conf に入っています。 -- SPDX-License-Identifier: CC0-1.0 -- vim:syntax=lua:set ts=4 sw=4: -- Refer to manual: https://knot-resolver.readthedocs.org/en/stable/ -- Network interface configuration net.listen( '127.0.0.1' , 53 , { kind = 'dns' } ) net.listen( '127.0.0.1' , 853 , { kind = 'tls' } ) --net.listen('127.0.0.1', 443, { kind = 'doh2' }) net.listen( '::1' , 53 , { kind = 'dns' , freebind = true } ) net.listen( '::1' , 853 , { kind = 'tls' , freebind = true } ) --net.listen('::1', 443, { kind = 'doh2' }) -- Load useful modules modules = { 'hints > iterate' , -- Allow loading /etc/hosts or custom root hints 'stats' , -- Track internal statistics 'predict' , -- Prefetch expiring/frequent records } -- Cache size cache.size = 100 * MB -- で始まる行はコメントになります。 内容を見ていきます。 net.listen() どのアドレスとポートで、どんなサービスをするかを設定します。 kind: dns 通常の DNS 名前解決のサービスです。 kind: tls DNS over TLS のサービスです。 modules ロードするモジュールを指定します。この部分はこのままで特に問題ないです。 hints > iterate : キャッシュよりもヒントを優先させることを指定しています。 stats : 各種メトリクスを収集します。prometheus エンドポイントを利用する場合は必須の設定です。 predict : キャッシュヒットの効率を高めるため、プリフェッチを行います。 cache.size キャッシュに利用するメモリサイズを指定します。 運用において必要となりそうな設定を上げてみます。 サービスポートの設定 net.listen() が 127.0.0.1 ::1 のみだと自ホストからしか使えません。 オンプレ環境の他の機器からの名前解決要求を受け付けるための IPアドレスとポートを設定します。 net.listen( '192.0.2.53' , 53 , { kind = 'dns' } ) アクセス元制限の設定 net.listen() でサービス用のアドレスを設定したら、アクセス元制限をする必要があります。 これを書かないと オープンリゾルバ となってしまいますので、特に GIP を net.listen() で設定する場合には気をつけましょう。 -- ACL modules = { 'view' } view:addr( '192.0.2.0/24' , policy.all(policy.PASS)) view:addr( '127.0.0.1' , policy.all(policy.PASS)) view:addr( '::1' , policy.all(policy.PASS)) view:addr( '0.0.0.0/0' , policy.all(policy.REFUSE)) view:addr( '::0/0' , policy.all(policy.REFUSE)) この例では、 192.0.2.0/24 とローカルインタフェースからの接続のみ許可、その他は拒否するようにしています。 log ログレベルの設定をします。 crit, err, warning, notice, info, debug のいずれかを設定します。デフォルトは notice です。 -- log log_level( 'debug' ) bogus_log DNSSEC 検証に失敗したログを出力します。 -- dnssec validation failure logging modules. load ( 'bogus_log' ) nsid RFC 5001 で定義されている nsid を使うと、複数インスタンスでの運用時、どのインスタンスが答えを返したかがわかるようになり便利です。 -- nsid local systemd_instance = os.getenv ( "SYSTEMD_INSTANCE" ) modules. load ( 'nsid' ) nsid.name(systemd_instance) 設定を保存 ここまでの設定を /etc/knot-resolver/kresd.conf に書いておきます。 Run-time reconfiguration nc や socat を使って UNIX ドメインソケット経由で knot resolver のインスタンスと通信し、インスタンスの設定をライブに確認・変更することができます。 ソケットファイルを確認します。 $ sudo ls -l /run/knot-resolver/control/ total 0 srwxr-xr-x 1 knot-resolver knot-resolver 0 Dec 22 20:13 1 今はインスタンスが 1つしかいないので、ソケットファイルも 1つだけあるのが確認できます。 ソケットファイルを指定して、socat を起動します。 $ sudo socat - UNIX-CONNECT:/run/knot-resolver/control/1 > help() 'help() show this help quit() quit hostname() hostname package_version() return package version user(name[, group]) change process user (and group) log_level(level) logging level (crit, err, warning, notice, info or debug) (snip) 設定内容を確認してみます。 > log_level() 'notice' ログレベルのデフォルト設定が返ってきました。 キャッシュのクリア キャッシュのクリアをしてみます。 $ dig engineers.ntt.com @127.0.0.1 (snip) ;; ANSWER SECTION: engineers.ntt.com. 300 IN A 13.115.18.61 engineers.ntt.com. 300 IN A 13.230.115.161 これでキャッシュに engineers.ntt.com の A レコードが保持されました。TTL は 300秒です。 > cache.clear('com.') { ['count'] = 16, ['round'] = 1, } com. 配下のキャッシュを削除しました。 count は消したレコードの数です。 再び名前解決を行うと、TTL が 300 の同じ結果が返ってくるはずです。 cache.clear() は指定された名前空間配下のすべてのキャッシュを消しますが、第二引数に true を指定すると、その名前だけを削除します。 > cache.clear('com.', true) { ['count'] = 3, ['round'] = 1, } true としたため、 com. の 3レコードのみ削除されたことがわかります。 knot-resolver には残念ながら、キャッシュの内容を dump するようなインタフェースはまだ用意されていません Multiple Instances インスタンスを2つ追加起動してみます。 $ sudo systemctl start kresd@2.service $ sudo systemctl start kresd@3.service 確認してみます。dig に +nsid をつけて、nsid を要求します。 $ dig engineers.ntt.com +nsid @127.0.0.1 ; <<>> DiG 9.16.1-Ubuntu <<>> engineers.ntt.com +nsid @127.0.0.1 ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 17096 ;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 1232 ; NSID: 33 ("3") ;; QUESTION SECTION: ;engineers.ntt.com. IN A ;; ANSWER SECTION: engineers.ntt.com. 269 IN A 13.115.18.61 engineers.ntt.com. 269 IN A 13.230.115.161 ;; Query time: 0 msec ;; SERVER: 127.0.0.1#53(127.0.0.1) ;; WHEN: Thu Dec 23 03:02:56 UTC 2021 ;; MSG SIZE rcvd: 83 NSID: 33 ("3") とあるとおり、3番目のインスタンスが返事をしています。 $ dig engineers.ntt.com +nsid @127.0.0.1 ; <<>> DiG 9.16.1-Ubuntu <<>> engineers.ntt.com +nsid @127.0.0.1 ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 94 ;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 1232 ;; QUESTION SECTION: ;engineers.ntt.com. IN A ;; ANSWER SECTION: engineers.ntt.com. 278 IN A 13.230.115.161 engineers.ntt.com. 278 IN A 13.115.18.61 ;; Query time: 0 msec ;; SERVER: 127.0.0.1#53(127.0.0.1) ;; WHEN: Thu Dec 23 03:02:47 UTC 2021 ;; MSG SIZE rcvd: 78 nsid を返してこない返答もあります。これは最初に起動した、1つ目のインスタンスの返事です。 ソケットファイルも 3つできています。 $ sudo ls -l /run/knot-resolver/control/ total 0 srwxr-xr-x 1 knot-resolver knot-resolver 0 Dec 23 02:06 1 srwxr-xr-x 1 knot-resolver knot-resolver 0 Dec 23 03:02 2 srwxr-xr-x 1 knot-resolver knot-resolver 0 Dec 23 03:02 3 Zero-downtime restarts 複数インスタンスがサービスを分散処理しているのを確認できました。 1つ目のインスタンスに設定を読み込ませるため、再起動してみましょう。 別端末で dig を仕掛けて、名前解決に問題が起きないか確認しておきます。 $ while true; do echo "`date`; `dig engineers.ntt.com @127.0.0.1 +nsid | grep NSID`"; done Thu 23 Dec 2021 03:12:51 AM UTC; Thu 23 Dec 2021 03:12:51 AM UTC; Thu 23 Dec 2021 03:12:51 AM UTC; Thu 23 Dec 2021 03:12:51 AM UTC; ; NSID: 32 ("2") Thu 23 Dec 2021 03:12:51 AM UTC; Thu 23 Dec 2021 03:12:51 AM UTC; Thu 23 Dec 2021 03:12:51 AM UTC; ; NSID: 32 ("2") Thu 23 Dec 2021 03:12:51 AM UTC; ; NSID: 33 ("3") Thu 23 Dec 2021 03:12:51 AM UTC; ; NSID: 32 ("2") NSID つき、NSID なし、2種類の応答があります。 $ sudo systemctl restart kresd@1.service と再起動すると、新しい設定が読み込まれます。 Thu 23 Dec 2021 03:12:53 AM UTC; Thu 23 Dec 2021 03:12:53 AM UTC; ; NSID: 33 ("3") Thu 23 Dec 2021 03:12:53 AM UTC; ; NSID: 32 ("2") Thu 23 Dec 2021 03:12:53 AM UTC; ; NSID: 33 ("3") Thu 23 Dec 2021 03:12:53 AM UTC; ; NSID: 32 ("2") Thu 23 Dec 2021 03:12:53 AM UTC; Thu 23 Dec 2021 03:12:53 AM UTC; ; NSID: 32 ("2") Thu 23 Dec 2021 03:12:53 AM UTC; ; NSID: 32 ("2") Thu 23 Dec 2021 03:12:53 AM UTC; ; NSID: 32 ("2") Thu 23 Dec 2021 03:12:53 AM UTC; ; NSID: 32 ("2") Thu 23 Dec 2021 03:12:53 AM UTC; ; NSID: 32 ("2") Thu 23 Dec 2021 03:12:53 AM UTC; ; NSID: 32 ("2") Thu 23 Dec 2021 03:12:53 AM UTC; ; NSID: 33 ("3") Thu 23 Dec 2021 03:12:53 AM UTC; ; NSID: 33 ("3") Thu 23 Dec 2021 03:12:54 AM UTC; ; NSID: 31 ("1") Thu 23 Dec 2021 03:12:54 AM UTC; ; NSID: 33 ("3") Thu 23 Dec 2021 03:12:54 AM UTC; ; NSID: 32 ("2") Thu 23 Dec 2021 03:12:54 AM UTC; ; NSID: 32 ("2") NSID 1 の応答が返ってくるようになりました。 おわりに これまで紹介してきましたように、Knot Resolver はとてもモダンで、調べれば調べるほど面白いソフトウェアです。が、この記事では、魅力を十分に伝え切れたとは言えません。 Prometheus endpoint によるサーバ状況の可視化と監視、etcd でのキャッシュ内容永続化とインスタンス間の共有、DNSTAP によるクエリログ取得と分析など、まだまだ書きたいトピックがありますので、また時間を見つけて書いていきたいと思います。 それでは、明日の記事をお楽しみに!
こんにちは。なんの因果か NTTコミュニケーションズのエバンジェリスト をやっている 西塚 です。 この記事は、 NTT Communications Advent Calendar 2021 22日目の記事です。 5分でわかる「Trino」 「Trino」は、異なるデータソースに対しても高速でインタラクティブに分析ができる高性能分散SQLエンジンです。 以下の特徴を持っており、ビッグデータ分析を支える重要なOSS(オープンソースソフトウェア)の1つです。 SQL-on-Anything : Hadoopだけでなく従来のRDBMS(リレーショナルデータベース)やNoSQLまで、標準SQL(ANSI SQL)に準拠したアクセスをワンストップに提供 並列処理 でビッグデータに対して容易にスケールアップ しかも 高速 (hiveの数十倍) Netflix, LinkedIn, Salesforce, Shopify, LINEなど世界中の企業のデータサイエンスチームに使われていますが、日本語での情報はまだ少ないです。 今日のブログエントリは、「Trino」の概要を5分程度で紹介し、「Trino」について日本語で検索した時に一番に表示されるサイトを狙っていきます。 知っておきたいTrinoとPrestoの関係 「Trino」はかつて「Presto」と呼ばれていました。 歴史を紐解いて関係を整理しておきましょう。 Prestoは、2012年にFacebook内で開発されました。 もともとは、 300PBのHiveデータに対するクエリが遅かったことを解決するためのプロジェクトだったと言います。2013年に、Apache Licenceの元、OSS化されました。 この系列のPrestoを PrestoDB と呼びます。 2018年に、Prestoのコアの開発者がFacebookを去り("PrestoのOSS開発に集中するため"といいます)、2019年にPresto Software Foundationを立ち上げました。 オープンソースコミュニティとしてPrestoDBよりも多様なユースケースを取り入れるために PrestoSQL の系列が作られました。 そして、2020年12月、 PrestoSQLは「Trino」と名称を変更 しました。 PrestoSQLで"Presto"の名称が使えなくなった経緯についての詳細は こちら(英語) に記載されています。 github上には、 prestodb と trino の両方のリポジトリがありますが、2019年を境にコア開発者がTrinoでの活動に移っています。 「Trino(Presto)」と併記する場合もありますが、今後は「Trino」が定着していくと思います。 Presto Software Foundationは、Trino Software Foundationに名称が変わり、かわいいマスコットキャラクターも産まれました。 Commander Bun Bun です。 NTT ComのTrinoへの貢献 私の所属するデータ分析専門部隊では、NTTコミュニケーションズをデータドリブン企業に変革するために、社内の各種システムからデータを一元的に収集するデータプラットフォームを構築し、その上でデータサイエンティストたちがブレーンとして分析に取り組んでいます。 Trinoは、データプラットフォーム(Hadoopを中心に作られています)と、様々な分析ツールを繋ぐ重要な役割を担っています。 企業ユーザとしてTrinoの公式Webサイトにも ロゴを載せています 。 また、チームメンバーがTrinoのOSS contributeをしています。 Trinoのユースケース Trinoの特徴を活かす 冒頭でSQL-on-Anythingと説明したように、Trinoは 様々なデータソース を扱うことができます。 例えば、 hive上の巨大なテーブルの特定のカラムに対して、MySQL上の属性データを結合して情報を付加する BigQueryとオンプレのデータを結合して分析する といった分析者がやりたいことがSQL文で簡単にできてしまいます。 全てのデータソースに対して透過的に1つのSQLアクセスを提供してくれるため、BIツールによるダッシュボード作成から、高度な機械学習のための複雑なクエリまで幅広いニーズに対応します。 この特徴から、データ仮想化製品と比較されることがありますが、重要な点として、Trino単体ではデータストレージとしての機能を持ちません。ですので従来のRDBを置き換えるものではありません。データの書き出しについては、hiveを利用できるため後述します。 「分散」の名称からわかる通り、並列処理のworkerを増やすことによって性能がスケールアウトしますが、データソースから取得してきたデータをworkerのオンメモリに展開してJOIN等の処理をするアーキテクチャになっているので、メモリ容量を超えるとクエリは失敗します。その代わり、オンメモリ処理のため超高速です。 そうです、Trinoを使う時はメモリをリッチに使いましょう。また、メモリに関する各種パラメータのチューニングが重要です。 Trinoによる分散処理のアーキテクチャの概要 NTT Comでの活用事例 我々のチームでは2017年頃からすでにTrino(当時はPresto)を使っておりました。 事例1: pmacct->kafka->presto->re:dashを使った高速なflow解析 2017年にJANOG39で発表した内容です。 この例では、ルータ等ネットワーク機器からリアルタイムに生成されるトラフィック情報をkafkaに入れてPrestoで参照しています。可視化部分(BIツール)としてはre:dashを使っていました。 事例2: データ分析コンペ 今年の夏、インターネットトラフィックを予測する社内コンペを開催しました。 参加者に Jupyter labベースのkaggleライクなコンペ環境を用意し、Python/Rを使ってインターネットのトラフィックデータを分析できるようにしました。 データセットはHadoop上に置き、Trinoを使って自由に参加者がSQLを叩いてアクセスできるようにしました。これにより参加者全員がTrinoでの高速クエリのメリットを享受することができました。 当然ですが扱うデータは通信系のデータだけではありません。他にもSalesforceのデータを取得し、企業情報を付加して分析するようなことも行っています。 また、IBM Netezzaにデータを蓄積している社内システムがあったため、Trinoからデータ参照するためにNetezza Connectorを開発したりしています。 Trino、認証/認可も大丈夫だってよ ここまでの説明で、認証/認可の機能が備わっているのか気になった方もいるかと思います。ご安心ください。 Trinoと Apache Ranger を組み合わせて利用することにより、Trinoから各データソースへのクエリにおいて、AzureADなどのIdP(IDプロバイダー)と連携したユーザ認証を行い、スキーマ/テーブル/カラム単位でアクセスを制御できます。 例えば機微なデータを特定の個人やグループにのみ公開するなどのポリシーをRangerのGUIで管理できます。 TrinoとApache Rangerの連携部分は我々のチームが 精力的にコミュニティに貢献 しています。 Trino+Rangerによるデータへのアクセスの制御 広がるTrinoの活用領域: Trino for ETL これからは、TrinoをデータのETLにも利用したいと考えています。 ETLとは、Extract/Transform/Loadの頭文字をとったデータ前処理のことです。 Trinoは INSERT INTO ... SELECT 構文により、データをHDFSやS3などに書き出すことができます。 これにより、シンプルにRDBなどのデータソースから取得(Extract)したデータを加工(Transform)してhiveに書き込む(Load)ことができます。 RDBを利用しているシステムからhdfsにデータを移動してhiveにデータを読み込ませる(Data Ingestion)にはいくつもの方法があります。Apache SqoopやEmbulkも試しましたが、パーティショニングに関する機能が不足していたり、データ加工に関わるモデル化やコードのメンテナンスが困難でした。 対してTrinoを使えば、データ加工・モデル化と分析の両方が同時に賄えるため大きなメリットになります。Apache Airflow(ワークフロー管理)やdbt(data build tool)との相性も抜群です。 Salesforce社のEngineer blog でもTrino for ETLの利点についての記事がでており、注目されている領域です。 Trino 利用イメージ Trino CLI $ trino --server https://<trino hostname> --user <ID> --password Password: (パスワードを入力) trino> show catalogs; Catalog ------------------------------- hive mysql trino> select * from hive.table1 join mysql.table2 on hive.table1.column1 = mysql.table2.column2 limit 10; python import trino cur = trino.dbapi.connect( host= '<trino hostname>' , port= 443 , http_scheme= 'https' , verify= False , auth=trino.auth.BasicAuthentication( '<ID>' , '<PW>' ), user= '<ID>' , ).cursor() cur.execute( 'show catalogs' ) print (cur.fetchall()) 上記はBasic認証の例ですが、我々はJWTを用いた認証を使っています。 Tableau サーバ接続でPrestoを指定することで利用可能です。 現時点(2021/12)でTrinoとPrestoのドライバは共通です。 最後に Trinoは、データを武器にしているデータドリブン企業に活発に利用されています。大量のデータが毎日生まれるサービスにおいてデータに基づいた仮説検証を行うためには、分析者に使いやすいデータプラットフォームが欠かせません。 我々のデータプラットフォームでは、オンプレのkubernetes基盤上にTrinoを構築し、日々の分析で活用しています(我々はオンプレですが、Trino自体はクラウドでも場所は選びません)。 OSS開発も活発で、海外企業での利用が広がっています。日本でもTrinoの導入事例が増えることを期待してやみません。 Trino利用開始に役立つリンク集 Trino: The Definitive Guide 概要からアーキテクチャまで、このオライリー本1冊を読めば間違いない(英語版のみ)。 Trino Documentation 対応している型やSQL文のシンタックスがわからなくなった時に参照します。公式のドキュメントが充実している点もTrinoの素晴らしい点です。 github/trino 公式のリポジトリです。バグらしきものを見つけたときに探します。 Trino Forum Q&Aのフォーラムもあります。 Trino Slack ユーザから開発者まで集うslackです。困ったことがあれば、呟けば直ぐに他ユーザや開発者自身が助けてくれます。
はじめに こんにちは、データプラットフォームサービス部 モバイルネットワーク開発チームの真山です。IoT Connect Gateway (ICGW) などの IoT サービスや、ローカル5G、フル MVNO の開発を担当しています。これまで栗原が紹介している ICGW シリーズですが、今回は私からも ICGW の活用例をご紹介したいと思います。過去のシリーズは下記よりご参照ください。 IoT Connect Gateway を使ってみた 第1回 〜ICGWのご紹介〜 IoT Connect Gateway を使ってみた 第2回 〜AWS IoTCoreに接続してみよう〜 観葉植物、育ててますか? さて、私は趣味で観葉植物を育てています。特に、マダガスカルや南アフリカ、中米に自生する現地の植物や、現地の植物の種を播き実生として育てることにハマっています。2 年前から少しずつ購入したり播種しているうちに、気がついたら 2021 年 12 月時点で 170 株 (105 鉢) を超えていました。このように植物をたくさん育てていると、植物の健康維持やより良い育成環境整備のために育成環境をデータとして記録し、可視化してみたいと思うようになりました。 例えば、冬の時期になると気温の低下により植物の健康状態が悪くなったり、灌水後に用土が乾かないことによる根腐れのリスクが高まります。一方、春から秋にかけて一般的な夏型植物はよく成長しますが、日照量が多すぎると葉焼けをしたり、また灌水頻度を高くしないと水切れを起こすリスクがあります。植物の種類にもよりますが、季節的な環境の変化により弱ってしまう、さらには枯れてしまうことがよくあり、年中通して適切な管理をする必要があります。皆さんも、旅行などで数日間外出すると植物の管理状態を把握できず、心配になってしまことはあるかと思います。 そこで、今回は園芸や栽培というライフワークの中で植物の育成状態を管理する目的として、ICGW を活用した事例を紹介させていただきます。 ICGW のおさらい ICGW はプロトコル変換機能やクラウドアダプター機能を提供するサービスです。 プロトコル変換機能 HTTP/MQTT等のプロトコルをIoT Connect Gatewayサービスを通じて暗号化通信(HTTPS/MQTTS)に変換することで簡単かつセキュアにクラウドサービスへ接続ができます。暗号化通信機能をデバイスではなくサービス側で行うことでデバイスの設計を簡素化できることに加え、暗号化プロトコルにかかるオーバーヘッドはモバイル回線区間のトラフィックには影響せず、データ通信料金を節約できます。 クラウドアダプター機能 各種クラウドサービスへ簡単に接続できるクラウドアダプター機能をセットで提供するため、利用するクラウドに合わせた接続用パラメータなどのIoTデバイスへの設定が必要なくなり作業負荷の軽減が図れ、かつ暗号化通信に変換することでセキュアにクラウドサービスへ接続できます。 コンセプト 植物育成環境の計測に必要な情報を各種センサで収集し、Raspberry Pi 4に集約します。Raspberry Pi 4 に LTE 通信モジュールである 4GPi を接続し、NTT Communications のモバイルサービス である IoT Connect Mobile® Type S (ICMS) の SIM を装着します。 4GPi から集約したセンサ情報をモバイルネットワークを通じて MQTT のプロトコルを利用し ICGW にオフロードします。 ICGW ではプロトコル変換機能として MQTT から MQTTS に変換しデータの暗号化を行うことに加え、 ICGW と AWS IoT Core を接続します。 AWS IoT Core に送信された MQTTS のデータを AWS OpenSearch Service に転送し、 Kibana を利用してセンサ情報を時系列データとしてプロットします。Kibana はログと時系列の分析、アプリケーションのモニタリングとして利用されるデータの視覚化および調査ツールで、OpenSearch と連携します。 用意したモノ SIMカード (ICMS) ICGW をご利用の場合は、別途 ICMS 対応 SIM が必要となります。申し込み、トライアルのご相談は記事下部にあるご連絡先よりお問い合わせください。 Raspberry pi 4 今回は、Raspberry Pi 4 が WiFi の Access Point となり WiFi モジュールを搭載したセンサから、また直接接続しているブレッドボード上のセンサからデータを収集し、4GPi (LTE 通信用モジュール) を用いてモバイルネットワークを経由し ICGW へ送信します。 各種センサ Wio-Node: 技適取得済みのESP-WROOM-02を搭載した小型IoTモジュール GROVEコネクタを2つ搭載し、REST APIを利用して GROVE モジュールよりデータを取得します。今回は GROVE モジュールは下記のセンサを利用します。 GROVE 水分センサ GROVE デジタル温度・湿度センサ 光センサ: BH1750 を搭載した 6 ビット周辺光センサモジュール ブレッドボードを介して Raspberry Pi 4 に接続します。 Raspberry Pi4 と BH1770 の接続設定例 を参考に設定します。 植物 アガベ ティタノタ / Agave Titanota プレステラ 90 に植えているカキ仔 (子株) を測定対象としました。 測定環境 植物の育成環境と各種センサの配置を下図に示します。 Raspberry Pi 4 (4GPi) のセットアップ SIMを差し込む 4GPi の SIM カードスロットは標準 SIM カードのサイズに対応します。 ICMS はマルチカット SIM カードとしてご提供しますので、標準 SIM カードのサイズにカットしスロットへ挿入します。 4GPi OSインストール作業 4GPi 対応のRaspberry Pi OS イメージファイルを下記サイトからダウンロードし、SD カードにインストールします。 4GPi Imagerダウンロード先 Raspberry Pi Imagerを使うことで簡単にインストールが可能になります。Raspberry Pi Imagerの詳しい使い方は、 ICGW 第2回 ページ に紹介していますのでご参照ください。 Raspberru Pi 4 セットアップ Raspverry Pi 4 にモニターとキーボードを接続、または ssh でログインし、設定を行います。初期ログイン ID と Password は 配布元情報 をご参照ください。必要なレポジトリ等は こちら を参考に設定します。 $ curl https://mechatrax.github.io/setup.sh | sudo bash $ sudo apt install 4gpi-utils 4gpi-net-mods 4gpi-networkmanager SIM 情報の設定 ICMS の SIM カードを ICGW 用途でご利用いただく場合 APN 等の情報は IoTデバイス設定情報 よりご確認いただけます。CONNAME は任意の文字列を設定してください。 CONNAME: icgw APN: mobiledata.ntt.com User: mobile@icms-p.ntt.com Password: protconv Raspberry Pi 4 に下記コマンドを投入します。 $ sudo nmcli con add type gsm ifname "*" con-name icgw apn mobiledata.ntt.com user mobile@icms-p.ntt.com password protconv 設定・動作状況の確認 設定した APN、User (ID)、Password を確認します。 $ sudo 4gpi-nm-helper show default all mobiledata.ntt.com mobile@icms-p.ntt.com protconv Raspberry Pi 4 のインターフェース設定を確認します。 $ nmcli con NAME UUID TYPE DEVICE icms 5692d797-cc8a-41cf-**** - ************ gsm cdc-wdm0 Wired connection 1 78da327c-0e8b-349f-**** - ************ ethernet -- 下記コマンドで、cdc-wdm0 が表示され、connected の状態であればモバイルネットワークへの接続が完了です。 $ nmcli dev status DEVICE TYPE STATE CONNECTION cdc-wdm0 gsm connected icgw eth0 ethernet unavailable -- wlan0 wifi unavailable -- lo loopback unmanaged -- ifconfig コマンドで wwan0 (SIM) に付与されたIPアドレスを確認します。 $ ifconfig wwan0 wwan0: flags= 4305 < UP,POINTOPOINT,RUNNING,NOARP,MULTICAST > mtu 1420 inet x.x.x. 3 netmask 255.255 . 255.248 destination x.x.x. 3 unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00 txqueuelen 1000 (UNSPEC) RX packets 1084972 bytes 527930831 ( 503.4 MiB) RX errors 54 dropped 0 overruns 0 frame 38 TX packets 1018724 bytes 111312134 ( 106.1 MiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 Raspberry Pi 4 (4GPi) のモバイルネットワークの接続設定と確認は以上です。 ICGWの設定 ICGW Portal で AWS IoT Core へ接続するための設定を行います。ICGW から AWS IoT Core への接続手順は ICGW 第2回 ページ に紹介されていますので、ご参照ください。 SIMの情報登録 認証情報の登録 データ転送先設定 データ転送先設定として AWS IoT Core に MQTT で接続します。ICGW では、IMSI / IMEI / MSISDN / Device Name (ICGW Portal 上で設定する任意の文字列) をメタデータとして MQTT トピックに付与できます。今回は MSISDN を設定します。 各種センサから Raspberry Pi 4 にデータを集約 Raspberry Pi 4 の WiFi 設定 今回はセンサ情報を Raspberry Pi 4 へ集約するため、NetworkManager の設定で Raspberry Pi 4 を WiFi Access Point として動作させます。下記設定にてアクセスポイントを作成します。IP アドレスは環境に合わせた値を設定してください。 CONNAME: Hotspot-pi SSID: 4GPi-AP Password: 4GPi-PW Access Point の IP アドレス: 192.168.100.1/24 $ sudo nmcli device wifi Hotspot-pi ifnamne wlan0 ssid 4GPi-AP password 4GPi-PW $ sudo nmcli connection modify Hotspot-pi autoconnect yes $ sudo nmcli connection modify Hotspot-pi ipv4.addresses 192.168 . 100.1 / 24 dnsmasp インストールし DHCP サーバー機能を有効化する。 $ sudo apt install dnsmasq dnsmasq の設定ファイル (/etc/dnsmasq.conf) に IP アドレスの割り当て範囲とリース時間を追記します。 interface=wlan0 dhcp-range= 192.168 . 100.2 , 192.168 . 100.20 , 255.255 . 255.0 ,24h 設定を反映するため、dnsmasq を再起動します。 $ sudo systemctl restart dnsmasq Raspberry Pi 4 にルーティング機能を持たせるため、IP パケット転送や IP マスカレードを設定します。 IP パケット転送のため /etc/ufw/sysctl.conf の net/ipv4/ip_forward=1 のコメントを外し、/etc/default/ufw に DEFAULT_FORWARD_POLICY="ACCEPT" と設定します。 IP マスカレードのため /etc/ufw/before.rules に 192.168.100.0/24 (Raspberry Pi 4 LANセグメント) からのパケットを wwan0 に転送するよう下記のルールを設定します。 *nat :POSTROUTING ACCEPT [ 0 : 0 ] -F -A POSTROUTING -s 192.168 . 100.0 / 24 -o wwan0 -j MASQUERADE COMMIT 最後に ufw を有効にします。 $ sudo ufw disable && sudo ufw enable Wio-Node の設定 Wio-Node 設定 を参考に進めていきます。設定は、スマートフォンへのアプリのインストールが必要となります。Wio アプリをインストール後、アカウントを作成します。Wio-Node の登録は下図の通り進めます。Wio-Node の Func Button を 4 秒間長押しすると Wio-Node が WiFi Access Point 動作します。スマートフォンを WiFi (SSID: Wio_xxxxxx) に接続し、Wio-Node の接続先である Raspberry Pi 4 の SSID: 4GPi-AP を指定します。 Wio-Node に Grove モジュールを接続します。上記で登録した Wio-Node を選択し、利用する Grove モジュールを追加します。追加後、View API を押下すると、GET Method が生成され、Request を送信すると Curl コマンドが表示されます。このコマンドをスクリプトに組み込み、センサデータを取得していきます。 Raspberry Pi 4 から ICGW 経由で MQTT メッセージを送信 Raspberry Pi 4 の設定 MQTT Client としてメッセージを送信するため、下記パッケージのインストールを実施し、mosquitto コマンドを扱えるようにします。 $ sudo apt install mosquitto-clients Raspberry Pi 4 で各種センサから収集したからデータを MQTT メッセージとして ICGW 経由で AWS IoTCore に送信します。Wio-Node に紐付いた GROVE センサ や Raspberry Pi 直結のボード上センサを情報取得するスクリプトを 1 分に 1 回実行します。内容は割愛しますが、センサの不具合で外れ値が出ることが稀にあるので、条件判定をした後一度各種センサ情報を1行のファイルに書き出すようスクリプトを書いています。センサ情報ファイルを読み出し、MQTT メッセージを ICGW へ送信するスクリプトも同様の頻度で実行します。スクリプトの内容を下記に示します。 #!/bin/bash s_device= "001" s_plant= "Agave" s_time= `date + '%Y%m%d%H%M%S' ` while IFS=, read s_lux s_hum s_tem s_moi do ## Topic MESSAGE= "{ \" time \" : $s_time , \" deviceid \" : \" $s_device \" , \" plant \" : \" $s_plant \" , \" lux \" : $s_lux , \" humidity \" : $s_hum , \" temperature \" : $s_tem , \" moisture \" : $s_moi }" ## MQTT Publish to AWS mosquitto_pub --id client -h pconv-stg.nspp2.com -t "iot/topic/ $s_plant " -m " $MESSAGE " --debug done < /home/xxx/iot-home/mqtt_pub/sensor_agave. log ICGW 経由で送信した MQTT メッセージを AWS IoT Core で確認します。AWS IoT Core のテストメニューから、MQTT テストクライアントに進み、トピックをサブスクライブします。設定として iot/topic/Agave/"MSISDN" をトピックとして指定しているため、その文字列ので結果を下図に示します。 センサ情報の可視化 今回は、AWS のツールで Amazon OpenSearch Service (Amazon Elasticsearch Service の後継サービス) を利用します。 Amazon OpenSearch Service の開始方法 を参考に、ドメインを作成し各種設定を行います。 次に AWS IoT Core の ACT にてルールを作成し、Publish されたメッセージを OpenSearch Service に送信します。 可視化ツールとして、Amazon OpenSearch Service が提供する Kibana を利用します。OpenSearch のダッシュボードにて、Kibana の URL にアクセスします。Discover を選択し、Create index pattern から index を生成します。Define an index pattern として、IoT Core で作成したルールの索引に対応させます。複数の日付に対応するようワイルドカードを利用します。 Index を生成後、Discover に移行し、AWS IoT Core からのメッセージが OpenSearch Service で受信できていることを確認します。 次は、Kibana でグラフを作成します。Visualize を選択し、Create new visualization から時系列データをプロットします。Rasberry Pi 4 で収集したセンサ情報である、温度・湿度・照度・土壌水分量を下図に示します。 以上より、室内の観葉植物の育成状況を可視化できました。 今回は観葉植物と ICGW を組み合わせたユースケースをご紹介しました。今後は、観葉植物をプロトタイプとしてフィードバック機能を拡張し、自動灌水機能や自動加温機能などの制御機構を組み込み、将来的には農業 IoT の施策への展開を目指しております。本記事では ICGW と AWS IoT Core を接続しましたが、Google Cloud IoT Core やAzure IoT Hub、Things Cloud など多くの接続先クラウドサービスと簡単に接続できます。ICGW の活用方法として本記事をご参考いただければ幸いです。 ICGWに関するお問い合わせ先 トライアル、サービス導入に関するお問い合わせ 資料請求・お問い合わせフォーム 開発チームへのお問い合わせ icgw-dev@ntt.com までメールでお寄せください。 ※お手数ですが@を半角文字に置き換えてください。
この記事は、 NTT Communications Advent Calendar 2021 21日目の記事です。 はじめに こんにちは、イノベーションセンターの鈴ヶ嶺( @suzu_3_14159265 )です。普段は、クラウド・ハイブリッドクラウド・エッジデバイスなどを利用したAI/MLシステムに関する業務に従事しています。本日は、Rustで動的メモリ確保(dynamic memory allocation)のmallocを実装してPythonやvimを動かしてみようという内容をお届けします。 また、去年もRustネタのアドベントカレンダーを書いているのでぜひ見ていただけると嬉しいです! NTTコミュニケーションズ Advent Calendar 2020 Rustで実装するNetflow Collector 実装するmallocのアルゴリズム 今回実装するmallocのアルゴリズムは小さなメモリサイズにはSegregated Free Listを用いて、大きなメモリサイズにはmmapを用いる方針で実装します。まず、最初に動的メモリ確保における重要な課題であるメモリ断片化の概要について説明し、それぞれのアルゴリズムの手法について説明します。 課題:メモリ断片化 上記の図ように、時間が経過するにつれてメモリの解放や格納などが行われた結果として未使用領域が飛び飛びに配置され、合計としての空き容量は存在するが連続して要求されたメモリ領域を提供できない課題をメモリ断片化と言います。このような虫食い状態のメモリ配置では、有効な資源(メモリ)を活用できません。このメモリ断片化の課題を次のSegregated Free Listによって解決します。 Segregated Free List Segregated Free Listは上記の図のように、複数のリストをそれぞれのメモリサイズごとに管理する方式です。これによりメモリ断片化が起きないbest fitなメモリ割り当てを実現します。また、未使用メモリを探索する計算量もそれぞれの管理するリストからunlinkするだけなのでO(1)で済みます。割り当てるメモリ領域がない場合は、 sbrk システムコールによるヒープ拡張を用いてメモリを確保してListに追加するように実装する必要があります。しかし、このSegregated Free Listのみでは全ての要求されうるメモリサイズのリストを用意する必要があり現実的ではないため、今回は512Byte以上のメモリについては後述するmmapを用いて別管理する方法を取ります。 mmap 今回、512byte以上の大きなメモリサイズについては mmap で別管理してメモリを確保、解放する方法を採用しました。本来のmmapはファイルをメモリにマップするためのシステムコールですが、ファイルディスクリプタ fd  に-1を指定し、無名マッピング MAP_ANONYMOUS  を利用することでメモリ確保APIとしても使用可能です。この命令を用いて直接kernelからメモリを取得して割り当てます。 非スレッドセーフ また、今回マルチスレッドには未対応な実装となっているためいくつかのプログラムは普通にSegmentation faultで落ちるので注意してください。glibcなどで実装されている詳しいmallocのアルゴリズムについては知りたい方はDoug Leaのmallocという通称 dlmalloc をおすすめします。1つのファイルに実装がまとめられており比較的読みやすいものになっています。 ちなみに以下は、dlmallocのコンパイル方法です。 wget http://gee.cs.oswego.edu/pub/misc/malloc-2.8.4.c gcc -fPIC -shared -o dlmalloc.so malloc-2.8.4.c Rustで共有ライブラリを作成する方法 wrap jemalloc まず、 cargo new --lib malloc-rs で新しくライブラリプロジェクトを作成します。そして以下のように Cargo.toml の crate-type に cdylib を追加しましょう。これで cargo build  と実行することで *.so が生成され、Rustを用いて他言語から使用可能なライブラリが作成可能になります。 Cargo.toml [package] name = "malloc-rs" version = "0.1.0" edition = "2021" [lib] crate-type = ["cdylib"] [dependencies] libc = "0.2.112" jemalloc-sys = "0.3.2" 試しに中身を jemalloc にwrapして置き換えるようにコードを書くと次のようになります。実装する関数は malloc , free , calloc , realloc となります。jemallocはメモリ断片化の回避と並行処理に対してスケーラブルに動作するmalloc実装です。ちなみに余談ですがRust1.31.1までmallocの実装に jemallocが標準 として使用されていました。しかし、システム言語であるのにシステムのメモリアロケータを使用しないのは不自然な点やバイナリサイズの増加が問題として浮かび上がってきたため現在ではシステムのアロケータが使用されるようになっています。 以下のコードを見ていただいたら分かるように、 extern "C" と記述することでC APIとして公開できます。そのままだとmangleされるので #[no_mangele] と付け加えましょう。 実際の動作には、Linuxで LD_PRELOAD という環境変数に共有ライブラリを指定して、動的ライブラリを差し替えることができる仕組みを使います。今回、mallocを呼ぶと標準出力で call malloc と出力されるような実装にしたので実際に動作させて確認してみます。 またここでは、writeシステムコールを使用して出力しています。Rustで一般的に使われる println! やlibcの printf などは内部でmallocを使用しているためSegmentation faultで落ちるので注意してください。 src/lib.rs use libc :: size_t; extern crate jemalloc_sys ; extern crate libc ; #[no_mangle] pub unsafe extern "C" fn malloc (size: size_t) -> *mut libc :: c_void { let message = "call malloc \n " ; let buf = message. as_ptr () as *const libc :: c_void; let buf_len = message. len (); libc :: write ( 1 , buf, buf_len); jemalloc_sys :: malloc (size) } #[no_mangle] pub unsafe extern "C" fn realloc (p: *mut libc :: c_void, size: size_t) -> *mut libc :: c_void { jemalloc_sys :: realloc (p, size) } #[no_mangle] pub unsafe extern "C" fn calloc (number: size_t, size: size_t) -> *mut libc :: c_void { jemalloc_sys :: calloc (number, size) } #[no_mangle] pub unsafe extern "C" fn free (p: *mut libc :: c_void) { jemalloc_sys :: free (p) } 実行結果 cargo new --lib malloc-rs # vim malloc-rs/src/lib.rs cargo build export LD_PRELOAD=`pwd`/target/debug/libmalloc_rs.so ls # 適当なコマンドを実行してみる mallocの実装 Headerはやや冗長ですが、3つのメンバのメモリサイズの size , mmapされたメモリか is_mmap , 次の要素を指し示す next を設定しました。今回のケースにおいて、mmapされたメモリかどうかはsizeで判断可能(512Byteより大きい)です。また本来であれば、割り当ては8Byteにアライメントされるためsizeの下位3bitに空き容量があります。それも今回は、実装の見通しやすさを優先して次の図のようにしました。 全体的な実装は以下のようになります。 get_header は、割り当てたメモリのポインタからHeaderサイズを引いてHeaderを取得します。 init_malloc で、Segregated Free Listを初期化します。 add_list は、Segregated Free Listに割り当て可能なメモリがない場合にヒープから sbrk を用いてメモリを確保してリストを追加します find_chunk は、Segregated Free Listからsize(Byte)のメモリを探索します extern crate libc ; use libc :: {c_void, size_t}; /// malloc Header struct Header { /// size of buffer size: size_t, /// flag of mmap is_mmap: size_t, // next header next: *mut Header, } /// 8byte aligment const ALIGN: usize = 8 ; /// max byte in segregated free list const MAX_BYTE: usize = 512 ; /// init size of one free list const INIT_LIST_SIZE: usize = 512 ; /// add size of free list const ADD_LIST_SIZE: usize = 512 ; /// number of free list const NUM_LIST: usize = MAX_BYTE / ALIGN + 1 ; /// init size of heap(sbrk) const INIT_HEAP_SIZE: usize = NUM_LIST * (INIT_LIST_SIZE + std :: mem :: size_of :: < Header > ()); /// flag of call init_malloc static mut IS_INIT_MALLOC: bool = false ; /// segregated free list static mut FREE_LISTS: [ *mut Header; (NUM_LIST)] = [ std :: ptr :: null_mut (); (NUM_LIST)]; fn get_align (size: usize ) -> usize { (size + ALIGN - 1 ) / ALIGN * ALIGN } unsafe fn get_header (p: *mut c_void) -> *mut Header { let header = p. sub ( std :: mem :: size_of :: < Header > ()) as *mut Header; header } /// init malloc function /// Setup the initial value of the segregated free list using sbrk from heap. unsafe fn init_malloc () -> Result < (), *mut c_void > { IS_INIT_MALLOC = true ; let current_p = libc :: sbrk ( 0 ); let ret = libc :: sbrk ((INIT_HEAP_SIZE as isize ). try_into (). unwrap ()); if ret != current_p { // fail sbrk return Err (ret); } // init segregated free list let mut p = ret; for i in 1 ..NUM_LIST { FREE_LISTS[i] = p as *mut Header; let num_header = INIT_LIST_SIZE / (i * ALIGN); for j in 0 ..num_header { let mut header = p as *mut Header; let size = i * ALIGN; ( * header).size = size; ( * header).is_mmap = 0 ; ( * header).next = std :: ptr :: null_mut (); let next_p = p. add (size + std :: mem :: size_of :: < Header > ()); if j != (num_header - 1 ) { ( * header).next = next_p as *mut Header; } else { // last element ( * header).next = std :: ptr :: null_mut (); } p = next_p; } } Ok (()) } /// add segregated free list /// When there is no more memory in the segregated free list, use sbrk to add memory from the heap. unsafe fn add_list (size: usize ) -> Result < *mut Header, *mut c_void > { let current_p = libc :: sbrk ( 0 ); let num_header = ADD_LIST_SIZE / size; let ret = libc :: sbrk ( (num_header * (size + std :: mem :: size_of :: < Header > ())) . try_into () . unwrap (), ); if ret != current_p { // fail sbrk return Err (ret); } let mut p = ret; for j in 0 ..num_header { let mut header = p as *mut Header; ( * header).size = size; ( * header).is_mmap = 0 ; ( * header).next = std :: ptr :: null_mut (); let next_p = p. add (size + std :: mem :: size_of :: < Header > ()); if j != (num_header - 1 ) { ( * header).next = next_p as *mut Header; } else { // last element ( * header).next = std :: ptr :: null_mut (); } p = next_p; } Ok (ret as *mut Header) } /// find header function /// Get a header of a given size from the segregated free list. unsafe fn find_chunk (size: usize ) -> Result < *mut Header, *mut c_void > { // index of segregated free list let index = size / 8 ; if FREE_LISTS[index] == std :: ptr :: null_mut () { let new_list_ret = add_list (size); match new_list_ret { Ok (new_list) => { FREE_LISTS[index] = new_list; } Err (err) => { return Err (err); } } } let header = FREE_LISTS[index]; // unlink chunk FREE_LISTS[index] = ( * header).next; Ok (header) } /// malloc function #[no_mangle] pub unsafe extern "C" fn malloc (size: size_t) -> *mut c_void { if size == 0 { return std :: ptr :: null_mut (); } if ! IS_INIT_MALLOC { if init_malloc (). is_err () { return std :: ptr :: null_mut (); } } let size_align = get_align (size); if size_align <= MAX_BYTE { // get memory from segregated free list let header_ret = find_chunk (size_align); if header_ret. is_err () { return std :: ptr :: null_mut (); } let header = header_ret. unwrap (); return (header as *mut c_void). add ( std :: mem :: size_of :: < Header > ()); } let mmap_size = std :: mem :: size_of :: < Header > () + size; let p = libc :: mmap ( :: std :: ptr :: null_mut (), mmap_size, libc :: PROT_READ | libc :: PROT_WRITE | libc :: PROT_EXEC, libc :: MAP_ANONYMOUS | libc :: MAP_PRIVATE, - 1 , 0 , ); if p == libc :: MAP_FAILED { return std :: ptr :: null_mut (); } let mut header = p as *mut Header; ( * header).size = mmap_size; ( * header).is_mmap = 1 ; p. add ( std :: mem :: size_of :: < Header > ()) } /// realloc function #[no_mangle] pub unsafe extern "C" fn realloc (p: *mut c_void, size: size_t) -> *mut c_void { let size_align = get_align (size); if p == std :: ptr :: null_mut () { return malloc (size_align); } let new_p = malloc (size_align); let header = get_header (p); let memcpy_size = if ( * header).size < size_align { ( * header).size } else { size_align }; libc :: memcpy (new_p, p, memcpy_size); free (p); return new_p; } /// calloc function #[no_mangle] pub unsafe extern "C" fn calloc (number: size_t, size: size_t) -> *mut c_void { let new_p = malloc (size * number); libc :: memset (new_p, 0 , size * number); return new_p; } /// free function #[no_mangle] pub unsafe extern "C" fn free (p: *mut c_void) { if p == std :: ptr :: null_mut () { return ; } let header = get_header (p); let size = ( * header).size; if ( * header).is_mmap == 1 { // free mmap let munmap_ret = libc :: munmap (p. sub ( std :: mem :: size_of :: < Header > ()), size); debug_assert! (munmap_ret == 0 ); } else { // reuse in segregated free list let index = size / ALIGN; let first_header = FREE_LISTS[index]; FREE_LISTS[index] = header; ( * header).next = first_header; } } 実行結果 実行には、シングルスレッドで動作するプログラムとしてPythonとvimを実行してみました。 Python(numpy) ここでは以下のように、numpyでランダムに大きなサイズの配列を作成し続ける処理で問題ないかを確認しました。 cargo build export LD_PRELOAD=`pwd`/target/debug/libmalloc_rs.so python3 <<EOF import numpy as np import random while True: print(np.sum(np.random.random(random.randint(1000, 10000)))) EOF 上記の結果から、動作には問題なく正常にmalloc, freeができていることが分かります。 invader.vim 次にvimで動作するインベーダーゲームの mattn/invader-vim を動作させてみました。 cargo build export LD_PRELOAD=`pwd`/target/debug/libmalloc_rs.so wget https://raw.githubusercontent.com/mattn/invader-vim/master/plugin/invader.vim vim #:source ./invader.vim #:Invader 問題なく動作していることが分かります。(というかvimでインベーダーが動かせるのすごいなぁ…) まとめ コードを見て分かるとおりunsafeを多用していたりpure-Rustな実装ではないため、あまりRustの恩恵を感じることができないものになってしまいました。一方で、Resultなどの高機能なパターンマッチをmallocを実装する上で使用可能な点や、libcなどのライブラリとのスムーズな連携は素晴らしい点だと感じることができました。 Rustは現在では、 ralloc のようにpure-Rust実装のメモリアロケータが実装されており、またLinux Kernelにカーネル内の第二言語としてサポートを提案するような RFC の議論もあるため個人的に今後の活躍を期待しています。 今回実装したコードは以下に置いておきました。 suzusuzu/malloc-rs それでは、明日の記事もお楽しみに! 参考 Glibc malloc internal A Memory Allocator by Doug Lea dlmalloc version 2.8.4.c Dynamic Memory Allocation in the Heap jemalloc mattn/invader-vim
この記事は NTTコミュニケーションズ Advent Calendar 2021 の20日目の記事です。 はじめに こんにちは。プラットフォームサービス本部アプリケーションサービス部の是松です。 NTTコミュニケーションズでは自然言語処理、機械翻訳、音声認識・合成、要約、映像解析などのAI関連技術を活用した法人向けサービスを提供しています。( COTOHA シリーズ ) NTTコミュニケーションズがこのようなAI関連技術を活用したサービスを展開する強みとして、 NTT研究所の研究成果が利用可能であること 自社の他サービスを利用しているお客様に対してシナジーのあるサービスを提案できること この2点が挙げられると思います。 実際に、私が担当している COTOHA Voice Insight は 通話音声テキスト化によってコンタクトセンターの業務効率化・高度化を実現するサービスなのですが、 NTT研究所の音声認識技術を活用しており、自社サービスとの連携も積極的に行っています。 ターゲットとしているコンタクトセンターのDX市場は変化が激しい業界でありながら、 私たちのサービスはその変化についていく体制が整っていないことが課題だと感じています。 様々な事情から、実際にサービスで使用している音声認識モデルを気軽に試すことは難しいのですが、オープンソースの音声認識技術を活用することでサービスの品質向上につながる知見を集めることが可能だと考え、技術調査をしてきました。 本記事では、そのような取り組みの1つとして、wav2vec 2.0 というオープンソースを用いて、事前学習された音声認識モデルを少量のデータセットでチューニングする方法をご紹介します。 手元で簡単に音声認識を試すことができるようになれば、PoC(Proof of Concept)などを通して知見を集めることができ、 なかなか小回りが効かないサービス開発の効率向上が期待できると考えています。 wav2vec 2.0 とは 2020.6 に wav2vec 2.0: A Framework for Self-Supervised Learning of Speech Representations という論文で提案された音声認識フレームワークです。 wav2vec 2.0 では、ラベル付き(書き起こし文がある)の音声データだけでなくラベルなし音声データも学習に活用する、自己教師あり学習の手法を採用しています。 少量のラベル付きデータ + たくさんのラベルなしデータでも、それまでの手法に匹敵する音声認識精度を達成し、 ラベル付きデータの量を増やしていくことで音声認識精度がさらに向上していくことを示しました。 現在でも wav2vec 2.0 を基にした手法が複数の音声認識タスクで最高性能を記録しています。 https://paperswithcode.com/task/speech-recognition wav2vec 2.0 の詳細は原論文や、解説記事( こちら が分かりやすかったです)を参照ください。 wav2vec 2.0 の素晴らしい点として、大規模音声データを用いて事前学習されたモデルに対して、少量のデータセットを用いてパラメータを再調整(Fine Tuning)することによって、後から追加した音声データにうまく適合したモデルを作れることが挙げられます。 しかも、wav2vec 2.0 は、ソースコードと事前学習されたモデルが公開されているため( こちら )、誰でも簡単にモデルをチューニングできます。 日本語データセットを使って音声認識モデルをチューニングする wav2vec 2.0 の効果的な使い方として、事前学習された言語非依存のモデルを少量の特定言語データセットで Fine Tuning する手法があります。 この手法は、話者が少ないなどの理由でデータセットが十分存在しない言語の音声認識モデルを作成する際に効果的です。 他にも、方言や特定の話者に特化したモデルや電話音声など特定のドメインのみで使用するモデルなど、これまでは十分なデータを用意できていなかった分野での活用が期待されます。 今回は、言語非依存の事前学習モデルに対して、少量の日本語音声データを使って Fine Tuning を行います。 大まかな流れは、英語データセットを使用してチューニングを行なっている こちら の記事を参考にしています。解説もわかりやすいので興味のある方は元の記事をご参照ください。 環境 Google Colaboraoty Pro を使用しました。 無料版でやる場合は、おそらく学習時にGPUのメモリが足らなくなるので、バッチサイズを小さくするなどして対応してください。 事前に必要なライブラリをインストールします。 %%capture # 日本語データセットのダウンロード !pip install datasets==1.13.3 # 機械学習ライブラリ !pip install transformers==4.11.3 !pip install torchaudio # 音声データ処理用ライブラリ !pip install librosa # 形態素解析 !pip install mecab-python3 !pip install unidic # かなローマ字変換 !pip install romkan 最新の日本語辞書をダウンロードしておきます。 !python -m unidic download 学習データの準備 まずは、Fine Tuning に使用する日本語データセットを用意しましょう。 common voice というオープンソースの音声データセットを構築するプロジェクトがあり、そこでは様々な言語の音声データが収録・公開されています。 下記のライブラリを使うことで、common voice データセットを簡単に使うことができます。 https://pypi.org/project/datasets/ 余談ですが、common voice のwebサイトでは、表示されるテキストを読み上げて録音したり、録音された音声の品質を評価したりすることでプロジェクトに貢献できます。興味がある方はぜひ試してみてください。 datasets.load_dataset() を使って様々な言語のデータセットをダウンロード可能です。 日本語の場合は第2引数に "ja" と入力します。 データセットは、train, validation, test の3種類に分かれており、今回は train データと test データの2つに分ければ十分なので、test と validation を train データとします。 from datasets import load_dataset, load_metric, Audio common_voice_train = load_dataset("common_voice", "ja", split="train+validation") common_voice_test = load_dataset("common_voice", "ja", split="test") データセットは、train, validation, test の3種類に分かれていますが、今回は train と test データの2つに分けて使用します。 今回は、trainデータが1308組、testデータが632組でした。 len(common_voice_train), len(common_voice_test) (1308, 632) データセットの中身を見てみましょう。 common_voice_train[0] {'accent': '', 'age': 'twenties', 'audio': {'array': array([ 0. , 0. , 0. , ..., 0.00085527, -0.00014246, -0.00077921], dtype=float32), 'path': '/root/.cache/huggingface/datasets/downloads/extracted/07b0a73f2103df267f566548f7597fe8d75f8d4bdd37b7f556478ae85378bd6a/cv-corpus-6.1-2020-12-11/ja/clips/common_voice_ja_19817895.mp3', 'sampling_rate': 48000}, 'client_id': 'b067e4a64d0c78c7c24b8eb93f9efc165121f9281fa6c31386d872529c2951a5a1f144ee8e5679c1bd41003695583b1c341de7d67fea995d584b5724b91ce984', 'down_votes': 1, 'gender': 'male', 'locale': 'ja', 'path': '/root/.cache/huggingface/datasets/downloads/extracted/07b0a73f2103df267f566548f7597fe8d75f8d4bdd37b7f556478ae85378bd6a/cv-corpus-6.1-2020-12-11/ja/clips/common_voice_ja_19817895.mp3', 'segment': "''", 'sentence': '予想外の事態に、電力会社も、ちょっぴり困惑気味だ。', 'up_votes': 2} 様々な情報が含まれていますが、今回は音声データとテキスト情報だけがあればいいので、 不要な情報を除きます。 common_voice_train = common_voice_train.remove_columns(["accent", "age", "client_id", "down_votes", "gender", "locale", "segment", "up_votes"]) common_voice_test = common_voice_test.remove_columns(["accent", "age", "client_id", "down_votes", "gender", "locale", "segment", "up_votes"]) 音声データの確認 次に音声データを確認してみましょう。 "path" に実際の音声データが格納されているファイルの場所が記載されています。 しかし、既に"audio" に音声ファイルの中身のバイナリデータが格納されているので、 下記の通り "audio" の中身を適切に処理することで音声データを再生できます。 import IPython.display as ipd import numpy as np import random rand_int = random.randint(0, len(common_voice_train)-1) ipd.Audio(data=common_voice_train[rand_int]["audio"]["array"], autoplay=True, rate=16000) テキストデータの確認 音声データがどんな内容を話しているかは、"sentence" の中に記載されています。 今回は句読点を除いた状態でチューニングを行うので、句読点などの不要な記号を除去する関数を作成し、テキストを整形します。 import re chars_to_ignore_regex = '[、,。]' def remove_special_characters(batch): batch["sentence"] = re.sub(chars_to_ignore_regex, '', batch["sentence"]).lower() + " " return batch common_voice_train = common_voice_train.map(remove_special_characters) common_voice_test = common_voice_test.map(remove_special_characters) ここで、データセットの中身をランダムに10文出力する関数を作成して、表示してみます。 from datasets import ClassLabel import random import pandas as pd from IPython.display import display, HTML def show_random_elements(dataset, num_examples=10): assert num_examples <= len(dataset), "Can't pick more elements than there are in the dataset." picks = [] for _ in range(num_examples): pick = random.randint(0, len(dataset)-1) while pick in picks: pick = random.randint(0, len(dataset)-1) picks.append(pick) df = pd.DataFrame(dataset[picks]) display(HTML(df.to_html())) show_random_elements(common_voice_test.remove_columns(["path","audio"])) sentence 0 このカレーはとても辛いです 1 ボーハンはイーストマンらギャングのスピークイージーの上がりから賄賂を取っていたとも噂された 2 母はいつもわたしに買い物を頼みます 3 危ないのでそちらへ行かないでください 4 それはたいてい一時間にも及ぶ 5 不満やいらだちはもっぱら受験や身のまわりに向けられている 6 来月の初め国へ帰ります 7 この箱はとても重いです 8 娘のフィアンセでこいつだけにはどうしても負けられない 9 うちの中学は弁当制で持って行けない場合は五十円の学校販売のパンを買う 句読点が除去されていることが確認できました。 これで、必要な音声データとテキスト情報のセットの準備ができました。 学習データの変換(かな漢字・カナ・ローマ字) 次に、3パターンのデータセットを用意したいと思います。 1つ目は前節で用意したデータセット(かな漢字文)をそのまま使用するもの。 2つ目はかな漢字文をカナ文に変換したもの。 3つ目はカナ文をローマ字に変換したものです。 かな漢字文をカナ文に変換するために、形態素解析器を使用します。 今回は MeCab という有名な形態素解析ライブラリを使用します。 まずはMeCabを使用してみましょう。 import MeCab import unidic import romkan mecab = MeCab.Tagger() sentence = common_voice_train[0]['sentence'] print(sentence) print(mecab.parse(sentence)) 予想外の事態に電力会社もちょっぴり困惑気味だ 予想 名詞,普通名詞,サ変可能,,,,ヨソウ,予想,予想,ヨソー,予想,ヨソー,漢,"","","","","","",体,ヨソウ,ヨソウ,ヨソウ,ヨソウ,"0","C2","",10819203040944640,39360 外 接尾辞,名詞的,一般,,,,ガイ,外,外,ガイ,外,ガイ,漢,"","","","","","",接尾体,ガイ,ガイ,ガイ,ガイ,"","C3","",2169894821044736,7894 の 助詞,格助詞,,,,,ノ,の,の,ノ,の,ノ,和,"","","","","","",格助,ノ,ノ,ノ,ノ,"","名詞%F1","",7968444268028416,28989 事態 名詞,普通名詞,一般,,,,ジタイ,事態,事態,ジタイ,事態,ジタイ,漢,"","","","","","",体,ジタイ,ジタイ,ジタイ,ジタイ,"1","C1","",4922247303275008,17907 に 助詞,格助詞,,,,,ニ,に,に,ニ,に,ニ,和,"","","","","","",格助,ニ,ニ,ニ,ニ,"","名詞%F1","",7745518285496832,28178 電力 名詞,普通名詞,一般,,,,デンリョク,電力,電力,デンリョク,電力,デンリョク,漢,"","","","","","",体,デンリョク,デンリョク,デンリョク,デンリョク,"0,1","C2","",7095706913481216,25814 会社 名詞,普通名詞,一般,,,,カイシャ,会社,会社,カイシャ,会社,カイシャ,漢,"カ濁","基本形","","","","",体,カイシャ,カイシャ,カイシャ,カイシャ,"0","C2","",1577258053673472,5738 も 助詞,係助詞,,,,,モ,も,も,モ,も,モ,和,"","","","","","",係助,モ,モ,モ,モ,"","動詞%F2@-1,形容詞%F4@-2,名詞%F1","",10324972564259328,37562 ちょっぴり 副詞,,,,,,チョッピリ,ちょっぴり,ちょっぴり,チョッピリ,ちょっぴり,チョッピリ,和,"","","","","","",相,チョッピリ,チョッピリ,チョッピリ,チョッピリ,"3","","",6652053971673600,24200 困惑 名詞,普通名詞,サ変可能,,,,コンワク,困惑,困惑,コンワク,困惑,コンワク,漢,"","","","","","",体,コンワク,コンワク,コンワク,コンワク,"0","C2","",3654785274356224,13296 気味 名詞,普通名詞,一般,,,,キミ,気味,気味,ギミ,気味,ギミ,漢,"キ濁","濁音形","","","","",体,ギミ,ギミ,ギミ,キミ,"2","C3","",2424706640790016,8821 だ 助動詞,,,,助動詞-ダ,終止形-一般,ダ,だ,だ,ダ,だ,ダ,和,"","","","","","",助動,ダ,ダ,ダ,ダ,"","名詞%F1","",6299110739157675,22916 EOS かな漢字文からカナ文への変換 上記より得られる出力のうち、カナの情報だけを集めて1つの文に繋げ直すための関数を作成します。 def convert_sentence_to_kana(batch): s = mecab.parse(batch["sentence"]) kana = "" for line in s.split("\n"): if line.find("\t")<=0: continue columns = line.split(',') if len(columns) < 10: kana += line.split('\t')[0] else: kana += columns[9] batch["kana"] = kana return batch 不格好な関数ですが、これで今回使用する日本語データのテキストを全てカナ文に変換できます。 注意した点として、もともとカタカナだった単語などは読みの情報が出力されないので、 場合分けをして元の表記をそのまま使用するようにしています。 では、正しく処理ができたかどうか確認してみましょう。 show_random_elements(common_voice_test.remove_columns(["path","audio"])) sentence kana 0 人々は花の苗や種を焼却し畑の花を全部抜きとってしまう ヒトビトワハナノナエヤタネオショーキャクシハタケノハナオゼンブヌキトッテシマウ 1 毎日忙しいのであまり休むことができません マイニチイソガシーノデアマリヤスムコトガデキマセン 2 女性とは逆で何とか常識を破ってめだってやろうと意気込む人がほとんどだ ジョセートワギャクデナントカジョーシキオヤブッテメダッテヤロートイキゴムヒトガホトンドダ 3 細長い指先で激しく鍵を叩く ホソナガイユビサキデハゲシクカギオタタク 4 クィーンズアベニューアルファに所属している クィーンズアベニューアルファニショゾクシテイル 5 山田さんは来月東京へ行くそうです ヤマダサンワライゲツトーキョーエイクソーデス 6 野球の後のビールぐらいうまいものはない ヤキューノアトノビールグライウマイモノワナイ 7 山田さんはおもしろい人です ヤマダサンワオモシロイヒトデス 8 わたしは歌がへたです ワタシワウタガヘタデス 9 熱いお茶が飲みたいです アツイオチャガノミタイデス カナ文からローマ字文への変換 カナ文への変換ができたら、ローマ字文への変換は簡単です。 カナとローマ字を変換してくれる romkan ライブラリを使います。 def convert_sentence_to_roman(batch): s = mecab.parse(batch["sentence"]) kana = "" for line in s.split("\n"): if line.find("\t")<=0: continue columns = line.split(',') if len(columns) < 10: kana += line.split('\t')[0] else: kana += columns[9] roman = romkan.to_roma(kana) batch["roman"] = kana return batch こちらも正しく変換ができたか確認してみましょう。 # "kana" は邪魔なので除いておく show_random_elements(common_voice_test.remove_columns(["path","audio", "kana"])) sentence roman 0 イさんはかぜをひいているので元気じゃありません isanwakazeohi-teirunodegenkijaarimasen 1 ツュレンハルト領はヴュルテンベルク領に編入された tsuxyurenharutoryo-wabyurutenberukuryo-nihen'nyu-sareta 2 航空事故を限りなくゼロに近づけるにはそれほどなり振りかまわぬ努力がいる ko-ku-jikookagirinakuzeronichikazukeruniwasorehodonarifurikamawanudoryokugairu 3 お偉方がぞくぞくと登場し恐縮する oerakatagazokuzokutoto-jo-shikyo-shukusuru 4 小林さんは青い傘を持っています kobayashisanwaaoikasaomotteimasu 5 冷蔵庫に卵や野菜や果物などがあります re-zo-konitamagoyayasaiyakudamononadogaarimasu 6 この絵は色がきれいです konoewairogakire-desu 7 ペンシルベニア州フィラデルフィアの郊外ウィンウッドのランケナウ病院で生まれた penshirubeniashu-firaderufianoko-gaiwin'uddonorankenaubyo-indeumareta 8 危ないのであそこの窓を開けてはいけません abunainodeasokonomadooaketewaikemasen 9 先月わたしは会社をやめました sengetsuwatashiwakaishaoyamemashita Tokenizerの作成 ここからは用意した日本語データセットを使って、Fine Tuning するための準備を進めていきます。 事前学習された wav2vec 2.0 のモデルは、言語非依存のモデルなので出力が日本語に対応していません。 出力を日本語データセットの形式に合わせるために、専用の Tokenizer というものを作成する必要があります。 その準備のために、テキストデータを1文字ずつに分割して、重複を除去したものを vocab_dict_{sentence|kana|roman} に辞書形式で格納します。 3パターンの辞書(vocab_dict)を作成しているコード(クリックすると開きます) def extract_all_chars_sentence(batch): all_text = " ".join(batch["sentence"]) vocab = list(set(all_text)) return {"vocab": [vocab], "all_text": [all_text]} def extract_all_chars_kana(batch): all_text = " ".join(batch["kana"]) vocab = list(set(all_text)) return {"vocab": [vocab], "all_text": [all_text]} def extract_all_chars_kana(batch): all_text = " ".join(batch["roman"]) vocab = list(set(all_text)) return {"vocab": [vocab], "all_text": [all_text]} vocab_train_sentence = common_voice_train.map(extract_all_chars_sentence, batched=True, batch_size=-1, keep_in_memory=True, remove_columns=common_voice_train.column_names) vocab_test_sentence = common_voice_test.map(extract_all_chars_sentence, batched=True, batch_size=-1, keep_in_memory=True, remove_columns=common_voice_test.column_names) vocab_list_sentence = list(set(vocab_train_sentence["vocab"][0]) | set(vocab_test_sentence["vocab"][0])) vocab_dict_sentence = {v: k for k, v in enumerate(vocab_list_sentence)} vocab_train_kana = common_voice_train.map(extract_all_chars_kana, batched=True, batch_size=-1, keep_in_memory=True, remove_columns=common_voice_train.column_names) vocab_test_kana = common_voice_test.map(extract_all_chars_kana, batched=True, batch_size=-1, keep_in_memory=True, remove_columns=common_voice_test.column_names) vocab_list_kana = list(set(vocab_train_kana["vocab"][0]) | set(vocab_test_kana["vocab"][0])) vocab_dict_kana = {v: k for k, v in enumerate(vocab_list_kana)} vocab_train_roman = common_voice_train.map(extract_all_chars_roman, batched=True, batch_size=-1, keep_in_memory=True, remove_columns=common_voice_train.column_names) vocab_test_roman = common_voice_test.map(extract_all_chars_roman, batched=True, batch_size=-1, keep_in_memory=True, remove_columns=common_voice_test.column_names) vocab_list_roman = list(set(vocab_train_roman["vocab"][0]) | set(vocab_test_roman["vocab"][0])) vocab_dict_roman = {v: k for k, v in enumerate(vocab_list_roman)} それぞれの辞書の数を見てみましょう。 len(vocab_dict_sentence), len(vocab_dict_kana), len(vocab_dict_roman) (1432, 82, 30) かな漢字文の場合は、漢字がたくさん含まれているので、かなりの数になります。 カナ文やローマ字の場合は漢字を読みに変換しているので、 情報量が落ちている代わりに vocab_dict の数を抑えられています。 ここからは、3種類のデータセットで行う処理が全く同じなので、vocab_dict_{sentence|kana|roman} を vocab_dict に統一して記載します。実際に動かす場合は、それぞれ読み替えてください。 vocab_dict にこのあとの処理で必要な、未知語を意味する"[UNK]"と、空白を意味する"[PAD]"を追加して、jsonファイルに出力します。 CTC(Connectionist Temporal Classification)というアルゴリズムで、音声の時系列とテキストの対応を計算する際に必要な処理です。( https://distill.pub/2017/ctc/ ) vocab_dict["[UNK]"] = len(vocab_dict) vocab_dict["[PAD]"] = len(vocab_dict) import json with open('vocab.json', 'w') as vocab_file: json.dump(vocab_dict, vocab_file) 出力したjsonファイルを使用して、Tokenizer を作成します。 from transformers import Wav2Vec2CTCTokenizer tokenizer = Wav2Vec2CTCTokenizer("./vocab.json", unk_token="[UNK]", pad_token="[PAD]", word_delimiter_token="|") Feature Extractor の作成 次に、音声データを事前学習モデルに入力できる形に変換するための Feature Extractorを作成します。 from transformers import Wav2Vec2FeatureExtractor feature_extractor = Wav2Vec2FeatureExtractor(feature_size=1, sampling_rate=16000, padding_value=0.0, do_normalize=True, return_attention_mask=True) これから使う事前学習モデルはサンプリングレート16000Hzで学習されているので、sampling_rate は 16000 に設定する必要があります。 この後の処理を簡単にするために、Feature Extractor と Tokenizer をまとめた Processor を作成します。 from transformers import Wav2Vec2Processor processor = Wav2Vec2Processor(feature_extractor=feature_extractor, tokenizer=tokenizer) データの前処理 common_voice データセットの音声データは16000Hzではないかもしれないので、全部16000Hzにリサンプリングします。 common_voice_train = common_voice_train.cast_column("audio", Audio(sampling_rate=16_000)) common_voice_test = common_voice_test.cast_column("audio", Audio(sampling_rate=16_000)) ここで先ほど tokenizer と feature Extractor をまとめた processor を使って、音声データと書き起こし文をこの後処理しやすい形に変換します。 def prepare_dataset(batch): audio = batch["audio"] # batched output is "un-batched" batch["input_values"] = processor(audio["array"], sampling_rate=audio["sampling_rate"]).input_values[0] with processor.as_target_processor(): batch["labels"] = processor(batch["sentence"]).input_ids return batch common_voice_train = common_voice_train.map(prepare_dataset, remove_columns=common_voice_train.column_names, num_proc=4) common_voice_test = common_voice_test.map(prepare_dataset, remove_columns=common_voice_test.column_names, num_proc=4) 学習と推論 data collector を定義します。これ以降の学習処理は基本的に参考にした記事と同じにしています。 data collector の詳細 import torch from dataclasses import dataclass, field from typing import Any, Dict, List, Optional, Union @dataclass class DataCollatorCTCWithPadding: """ Data collator that will dynamically pad the inputs received. Args: processor (:class:`~transformers.Wav2Vec2Processor`) The processor used for proccessing the data. padding (:obj:`bool`, :obj:`str` or :class:`~transformers.tokenization_utils_base.PaddingStrategy`, `optional`, defaults to :obj:`True`): Select a strategy to pad the returned sequences (according to the model's padding side and padding index) among: * :obj:`True` or :obj:`'longest'`: Pad to the longest sequence in the batch (or no padding if only a single sequence if provided). * :obj:`'max_length'`: Pad to a maximum length specified with the argument :obj:`max_length` or to the maximum acceptable input length for the model if that argument is not provided. * :obj:`False` or :obj:`'do_not_pad'` (default): No padding (i.e., can output a batch with sequences of different lengths). max_length (:obj:`int`, `optional`): Maximum length of the ``input_values`` of the returned list and optionally padding length (see above). max_length_labels (:obj:`int`, `optional`): Maximum length of the ``labels`` returned list and optionally padding length (see above). pad_to_multiple_of (:obj:`int`, `optional`): If set will pad the sequence to a multiple of the provided value. This is especially useful to enable the use of Tensor Cores on NVIDIA hardware with compute capability >= 7.5 (Volta). """ processor: Wav2Vec2Processor padding: Union[bool, str] = True max_length: Optional[int] = None max_length_labels: Optional[int] = None pad_to_multiple_of: Optional[int] = None pad_to_multiple_of_labels: Optional[int] = None def __call__(self, features: List[Dict[str, Union[List[int], torch.Tensor]]]) -> Dict[str, torch.Tensor]: # split inputs and labels since they have to be of different lenghts and need # different padding methods input_features = [{"input_values": feature["input_values"]} for feature in features] label_features = [{"input_ids": feature["labels"]} for feature in features] batch = self.processor.pad( input_features, padding=self.padding, max_length=self.max_length, pad_to_multiple_of=self.pad_to_multiple_of, return_tensors="pt", ) with self.processor.as_target_processor(): labels_batch = self.processor.pad( label_features, padding=self.padding, max_length=self.max_length_labels, pad_to_multiple_of=self.pad_to_multiple_of_labels, return_tensors="pt", ) # replace padding with -100 to ignore loss correctly labels = labels_batch["input_ids"].masked_fill(labels_batch.attention_mask.ne(1), -100) batch["labels"] = labels return batch data_collator = DataCollatorCTCWithPadding(processor=processor, padding=True) 事前学習されたモデルを用意します。 今回は、wav2vec2-large-xlsr-53 という53言語のデータを用いて事前学習された大規模モデルを使用しています。 モデルの詳細 from transformers import Wav2Vec2ForCTC model = Wav2Vec2ForCTC.from_pretrained( "facebook/wav2vec2-large-xlsr-53", attention_dropout=0.1, hidden_dropout=0.1, feat_proj_dropout=0.0, mask_time_prob=0.05, layerdrop=0.1, ctc_loss_reduction="mean", pad_token_id=processor.tokenizer.pad_token_id, vocab_size=len(processor.tokenizer) ) # Fine Tuning で Feature Extractor が変化しないように設定を入れる model.freeze_feature_extractor() 学習時の設定値を決めます。 from transformers import TrainingArguments training_args = TrainingArguments( output_dir="./wav2vec2-large-xlsr-japanese-demo", group_by_length=True, per_device_train_batch_size=16, gradient_accumulation_steps=2, evaluation_strategy="steps", num_train_epochs=30, fp16=True, save_steps=100, eval_steps=100, logging_steps=10, learning_rate=3e-4, warmup_steps=500, save_total_limit=2, ) 今まで作成してきたものを使って、Trainer を作成します。 from transformers import Trainer trainer = Trainer( model=model, data_collator=data_collator, args=training_args, train_dataset=common_voice_train, eval_dataset=common_voice_test, tokenizer=processor.feature_extractor, ) ここまでお疲れ様でした。 後は実際に学習を実行して、想定したものが正しくできているかを確認するだけです。 私の環境では学習に2時間ほどかかりました。 trainer.train() 学習時の出力例(カナ文の場合) Step Training Loss Validation Loss 100 11.832900 9.277085 200 4.031800 4.175367 300 3.988200 4.157576 400 3.896900 4.027470 500 3.612100 3.709400 600 1.262500 1.241253 700 0.588300 0.966568 800 0.411600 0.989113 900 0.343900 0.975543 1000 0.280400 1.019831 1100 0.268400 0.964782 1200 0.282000 0.991331 それでは、学習した3種類のモデルについて、それぞれテストデータを入力してどのような文字列が出力されるかを確認してみましょう。 カナ漢字文 pred_str text 先んのはいがはかませんてでした 先生の話は意味が分かりませんでした 先日のにが日とがいあります 先生の机の上に辞書が二冊と雑誌が一冊あります そなでまどがきます 少し暑いので窓を開けます わんのにとがあります かばんの中にノートがあります こ前とてをしました 五年前妻と結婚しました スはあるかられどとさんれた テロがあるからやめろとさんざんいわれた 先あのけ本ありました 先週姉の結婚パーティーがありました テもりるのここはの学かにいきををします 友情思いやり協力の心は将来の社会生活に強い影響を及ぼします 認識精度はお世辞にも良いとは言えません。 その理由としては、vocab_dict の数が多く、同じ漢字が出現することが少ないために学習がうまくいかなかったことが挙げられそうです。 また学習データの中に出現しなかった漢字が、テストデータの中に出現していそうですね。 カナ文 pred_str text センセーノハシワイーミカワカネマセンテシタ センセーノハナシワイミガワカリマセンデシタ センセーノツクエノウエニチショガニサツトナッシガイッサツアリマス センセーノツクエノウエニジショガニサツトザッシガイッサツアリマス スコシアツイナデマドオガアキマス スコシアツイノデマドオアケマス コバンノナカニノートガアリマス カバンノナカニノートガアリマス ゴネンマエキマトケッコンシマシタ ゴネンゼンサイトケッコンシマシタ テロガアルカラリャトートサンザンリワレタ テロガアルカラヤメロトサンザンイワレタ センキューアネノケッコンパーテイガアリマシタ センシューアネノケッコンパーティーガアリマシタ ユージョーオモアリキョールクノココロワショーライノシャカイセーカツニズヨイエーキョーオオーボシマス ユージョーオモイヤリキョーリョクノココロワショーライノシャカイセーカツニツヨイエーキョーオオヨボシマス カレノソキロホンルリダッテケキケキナサラナナガオサメタ カレモショキューオホンルイダシテゲキテキナサヨナラガチオオサメタ ニッポンデワタミタゼガツヨイデス ニッポンデワシュンプーガツヨイデス 一見わかりにくいですが、カナ漢字文と比較して音が綺麗に推測できていると言えそうです。 一方で、うまくいかなかったケースもありました。 例えば最後の文は、かな漢字文だと「日本では春風が強いです」であり、 音声は「シュンプー」ではなく「ハルカゼ」と読んでいると思われます。 形態素解析のミスによってデータにノイズが含まれていることが示唆されます。 ニッポンデワタミタゼガツヨイデス ニッポンデワシュンプーガツヨイデス ローマ字 pred_str text sensu-noharashiwai-mikawakaremasenteshita sensu-nohekuta-runazuhiwaimigawakarimasendeshita sense-notsukuenoueniiichishogan'isatsutoyasshigaissatsuarimasu sensu-nottosukuenouenijizuhogan'izuattosutozasshigaissatsuarimasu sok-shiwatsunademadogaakemasu sukoshiatsuinodemadooakemasu gabannonakanio-togaarimasu kabannonakanino-togaarimasu gonenmaeimatokekonshimashita go-nenzensaitokekkonshimashita terogaaa-rukararyajho-tosanzaniwareta terogaarukarayamerotosanzaniwareta senkyu-anenokekkonpa-teigaarimashita senshu-anenokekkonpa-ti-gaarimashita yu-jo-omoiarikyo-rukunokokorowasho-rainoshakaise-katshunizuyoie-kyo-oooboshimasu yu-jo-omoiyarikyo-ryokunokokorowasho-rainoshakaise-katsunitsuyoie-kyo-ooyoboshimasu tarnoosokyrohponru-ridat-tsute-kekikekunasarananakakuo-sameta karemoshokyu-ohon'a-ruyu-aidi-azuhittoegekittoekinasayonaragachioosameta nippondewatabikazegatsugyoidesu nippondewashunpu-gatsuyoidesu カナ文からさらに読みづらくはありますが、比較的うまくいっていそうです。 ローマ字の中にシングルクオート「'」が含まれていますが、これは例えば「んい」と「に」を区別するための記号です。 おわりに この記事では、手軽に自分だけの音声認識モデルを構築する方法についてご紹介しました。 日本語データセットを整備して、かな漢字・カナ・ローマ字のそれぞれでどのようなモデルが生成できるのかを確認しました。 カナ漢字文をそのまま使う場合は今回のようなシンプルなやり方だとうまくいきませんでした。 カナ文とローマ字文の場合は、認識精度が比較的高いモデルができました。 ただし、出力される文もカナ・ローマ字のままになってしまうので、 実用にあたっては、後処理でカナ漢字文に変換するなどの対応が必要です。 また、今回は言語モデルを使用せずにシンプルに音声データとテキスト情報の対応をとりました。 事前学習済みの言語モデルを使用することで単語の前後関係や文脈を考慮したより高い性能の音声認識モデルを構築できると考えられます。 大規模データセットで学習された音声認識モデルが無償で誰でも使えるなんて、素晴らしい時代に生まれたと感じます。 自前で音声認識モデルをチューニングできることで、音声認識サービスのトライアンドエラーをどんどん回すことが可能になると期待しています。 今回ご紹介した手法はまだまだ改善の余地があるので、引き続き技術検討を進めたいと思います。 それでは、明日の記事もお楽しみに! 参考にしたもの https://maelfabien.github.io/machinelearning/wav2vec/ https://huggingface.co/blog/fine-tune-wav2vec2-english/ https://tech.fusic.co.jp/posts/2021-03-30-wav2vec-fune-tune/
この記事は NTTコミュニケーションズ Advent Calendar 2021 の19日目の記事です。 はじめに こんにちは。イノベーションセンターテクノロジー部門の田中と申します。インターネットにおける攻撃インフラ撲滅に向けた追跡活動を主に行っています。例えば、追跡中のIPアドレスは真に該当マルウェアのC2であるか、現在も活動中であるか等を OSINT を活用して精度を上げて特定していくのですが、さらに情報が必要になるケースがあります。その際に、有力な技術の1つになるのが、マルウェアやC2に与える情報を制御し、挙動の差異を観測するという手法です。本記事では、 Frida というツールを利用して、解析・変更の初歩の部分について行ってみたいと思います。 概要 前半は、準備として API Monitor というツールを用い、APIコールをトレースし、マルウェアの挙動を簡単に把握します。後半は、動的バイナリ計装(DBI:Dynamic Binary Instrumentation)フレームワークの1つである Frida を使ってマルウェアの一部の動作をフックし、その動作を変更してみます。DBIは、実行中のプログラムにコードを挿入可能な技術で、関数をフックして入出力の読み取り・書き換えすることが可能となります。API Monitorはマルウェア解析トレーニングコースである SANS FOR610 でも扱われますが概要にとどまるのと、Fridaは、マルウェア解析適用事例が少ないため、今回紹介することにしました。 解析環境の準備 マルウェアを動作させて解析するため、専用の仮想環境を準備します。VirtualBoxやVMware等上にWindows10をセットアップし、解析ツールを準備します。今回は、デバッガ x64dbg 、 API Monitor 、ネットワークツール Fakenet-ng を用います。各サイトからダウンロードしてインストールしてください。解析ツールを一括導入してくれる FlareVM を用いるのもお勧めです。仮想環境を構築したらスナップショットを取得し、マルウェア解析後は、元の状態へ戻すようにしてください。(尚、Fridaについては後半で導入方法から紹介します。) 解析検体 今回は、 Azorult というマルウェアを対象にします。 Anyrun から入手したサンプルを使用します( Azorultサンプル解析ページ )。Azorultサンプルの 解析ページ を確認して頂くとわかるように、このAzorultサンプルは、コロナ感染状況パネルを模擬したドロッパーから、プロセスID:2636でドロップされたものです。(ドロッパーはAzorultではないのでご注意ください) API Monitorによる簡易解析 準備した解析環境で、Azorultサンプルを解析していきます。まず、Fakenet-ngを起動します。Fakenet-ngはダミーのDNSサーバやWebサーバ相当の動作をローカル環境で担ってくれ、マルウェアのC2等の通信を仮想環境内に閉じ込めてくれます。 次に、API Monitorを用いてサンプルを起動してAPIコールをトレースします。API Monitorは2013年に開発が止まっていますが、有益なツールなので使い方の一部を解説します。API Monitor(x86)を起動後、File->Monitor New Processで、Azorultのサンプルを選択します。(選択画面で、exeファイルのみしか選択できない仕様のため、Azorultサンプルの拡張子が異なる場合は、exeに変更してください。)サンプルを選択しOK押下すると、Azorultが起動され、以下のように呼び出されたAPIが記録されます。 起動後、数秒後にFile->Pause Monitoringでトレースを停止します。その後以下の図のように、API Filter画面のDisplayをクリック、Add Filterをクリックすると、Display Filterダイアログがでるので、「Calling Module Name」を選択肢、Azorultサンプルのファイル名(ここではazorult.exe)を入力しOK押下。 すると、azorult.exeから呼び出されたAPIコールのみにフィルタできます。サンプル起動直後のAPIコールは下図のように、 LoadLibraryA と GetProcAddress によりマルウェアが呼び出したいAPIのアドレス解決をしていることがわかります。これは、Windows仕様における 明示的リンク(Explicit Linking) の挙動です。ファイルヘッダの IAT(Import Address Table) に記載され、表層解析で容易にAPIを発見な 暗黙的リンク(Implicit Linking) とは対象的です。尚、IATは、Windows実行ファイルである PE 形式で定義されるヘッダ情報です。 もう少し下にスクロールしてみると、下図のように、なにやらWindowsバージョン情報と、 CreateMutexA でMutexが作られていることがわかります。Mutexは2重起動等を防ぐため等に正規アプリケーションで用いられますが、マルウェアでも散見される挙動です。 さらにスクロールしてみると、下図のように、 wininet.dll がロードされ、インターネット接続に関するAPI群をアドレス解決している様子を見ることができます。 Fakenet-ngのログを見ると、ドメインcoronavirusstatus[.]spaceに対し101バイトの長さの通信を行っていることがわかります。(実際にはFakenet-ngが該当ドメインの権威サーバに代わり、DNS/HTTP疑似応答していることがログでわかります。尚、執筆時点で、該当ドメインのAレコード応答はありませんでした。) Step.1 FridaによるCreateMutexAの追跡 前節のAPI Monitorによる解析で、Azorultサンプルは、動的に用いるAPI関数のアドレス解決していました。さらに、ハイフンにより繋がれた文字列でMutexを作り、あるドメインに接続し101バイトの通信することがわかりました。Fridaを使いCreateMutexAにフックをかけてみます。FlareVMではfridaは導入されないので以下の様にインストールします。 pip install frida-tools ここでは以下のコードを準備しました。Fridaでは、解析プロセスの立ち上げや解析スクリプトの適用はpythonにより行いますが、フック等を明示する解析スクリプト自体はJavaScriptにより記述します。 Module のgetExportByNameにより、DLL名とAPI名を指定しアドレスを得た(1)あとで、 Interceptor のattachを用い、指定アドレスをフックし、関数が呼ばれる前の処理(onEnter)、呼ばれたあとの処理(onLeave)を記載できます。このように、フックしたあとで、ユーザが任意のコードを実行させることがFridaの特徴になります。ここでは、 CreateMutexA をフックし(2)、引数の2番目のlpNameを表示(3)するようにします。(リンクからCreateMutexAのプロトタイプを確認できます) import frida import sys file = 'C: \\ work \\ frida \\ azorult.exe' pid = frida.spawn( file ) session = frida.attach(pid) script = session.create_script( """ console.log("Starting ...."); // CreateMutexAのアドレスを取得 (1) var CreateMutexA = Module.getExportByName("kernel32.DLL", "CreateMutexA"); // 該当アドレスでフック (2) Interceptor.attach(CreateMutexA, { // 関数が呼ばれる際に引数を取得し表示 (3) onEnter: function (args) { console.log("Entering .... CreateMutexA"); console.log("[*] CreateMutexA args: " + args[2].readAnsiString()); }, }); """ ) script.load() frida.resume(pid) sys.stdin.read() 動作させると以下のように、API Monitorで見たMutexが作られていることがわかります。 Step2. FridaによるMutex文字列生成ロジック追跡 では、このハイフン区切りのMutex文字列はどのように生成されるのでしょうか?Azorultを解析した blackberry社のブログ記事 によると、下図のようなロジックで生成されるとあります。感染Windows端末における GUID 、プロダクト名、ユーザ名、コンピュータ名を入力として、ある関数(図ではID generate func())で4Byteの固定長に変換され、それがハイフンで繋がれMutex文字列になっていることがわかります。 引用元: https://blogs.blackberry.com/en/2019/06/threat-spotlight-analyzing-azorult-infostealer-malware この関数(以下、符号化関数と呼びます)をデバッガを用い確認してみましょう。Azorultサンプルは32bitバイナリですので、x32dbgを起動し、Azorultサンプルを開き、0x00406204にブレークポイントを設定します。(x32dbg左下のコマンド窓から、bp 406204と入力しエンター押下。)ブレークポイント設定後、実行(F9キー押下)を数回起動すると、該当アドレスで停止します。(数回が何回なのかは、x32dbgの「設定->Events」の設定によります。また、ブレークポイントを過ぎてしまった場合、Ctrl+F2キー押下で開始時点からやり直せます。)0x00406204のブレークポイントに到達したら、gキーを押下します。すると以下のグラフ画面が表示されます。 0x00406204が符号化関数の先頭になります。この符号化関数は何度か呼ばれており、画面は、1回目の呼び出し時になります。1回目の符号化対象はGUIDで、関数内のローカル変数を意味する[ebp-4]にこのGUID文字列のポインタが格納されているのがわかります。真ん中のブロックで、xor演算やシフト演算(shl,shr)が含まれ、ループにより繰り返し回実行されることがわかります。繰り返しは対象文字列の先頭から末尾まで続きます。特に興味深いのが、xorで固定の値(ここでは0x6521458a)が使われていることです。このサンプルはこの値をキーとして、符号化処理をしていることがわかります。こういった文字列は IOC として活用できる可能性があります。 符号化関数のアドレスが確認できたので、Fridaで関数をフックしてみます。先程のコードに以下を追加します。先程のようにAPIではなく任意のアドレスでフックさせたい場合は、 NativePointer を用いポインタを作っておきます(4),(6)。符号化関数の入力値については、eaxレジスタに文字列へのポインタが入っているので、this.context.[レジスタ名]で読み取り、 Memory.readAnsiString でポインタの示す文字列を取得します(5)。符号化関数の出力値はebxレジスタに格納されるので、同様にthis.context.ebxで値そのものを取得します(7)。 // 復号化関数開始アドレスのフック用ポインタ作成 ( 4 ) var enc_func_start = new NativePointer( "0x00406204" ) Interceptor.attach(enc_func_start, { onEnter: function (args) { console.log( "entering .... Encode_function" ); // eaxレジスタの値の示す文字列取得 ( 5 ) console.log( "[*] Input_value : " + Memory.readAnsiString(this.context.eax)); }, }); // 復号化関数終了アドレスのフック用ポインタ作成 ( 6 ) var enc_func_end = new NativePointer( "0x00406265" ) Interceptor.attach(enc_func_end, { onEnter: function (args) { //console.log( "exiting .... Encode_function" ); // ebxレジスタの値取得 ( 7 ) console.log( "[*] Output_value : " + this.context.ebx); }, }); 上記コードの実行結果は以下のようになります。 GUID 、プロダクト名、ユーザ名、コンピュータ名、及び前述4つをつなげた文字列、の計5個が順番に、Input_valueとして符号化がされて、4Byteの文字列がOutput_valueとして生成されているのがわかります。最終的にハイフンを挿入されてMutexが作られています。 Step.3 FridaによるGetComputerNameW結果の書き換え 最後に、Fridaを用い、マルウェアに誤った情報を伝える一例として、 GetComputerNameW の結果を書き換えてみます。前節までの分析でみたように、本Azorultサンプルは、コンピュータ名等を用いMutexを作り、それをHTTP通信によりビーコンとしてC2に送ります。一般に、C2側ではこの情報をデコードして感染端末情報を入手することで、ターゲットか否かを判断し、情報搾取等次の行動に移ります。 ここでは以下のコードを準備しました。Step.1と同様に、 Module のgetExportByNameで、DLL名とAPI名を指定しアドレスを得た(8)あと、 Interceptor のattachで、指定アドレスをフックします。関数の呼ばれたあとの処理(onLeave)を記載していきます。まず、書き換え対象の文字列を Memory .allocでUTF16としてメモリを確保し変数stに割り当てます(10)。次に、GetComputerNameWにより得られたコンピュータ名の格納アドレスを確認していくのですが、ここで問題発生しました。GetComputerNameWのLeave時に、コンピュータ名を格納したメモリアドレスは、[ESP-8]と想定したのですが、そこにはありませんでした。そこで、 Memory .readByteArrayを用いESPから100Byteダンプします(11)。すると[ESP+8]にコンピュータ名の存在が確認できるので、このアドレスを書き換え対象としてフック用のポインタを作成します(12)。 Memory .copyで、先程用意した、変数stに書き換えます(13)。書き換わった後のメモリを表示すると確かに「NTTcom8213」にコンピュータ名が変わっていることを確認できます。 import frida import sys file = 'C: \\ work \\ frida \\ azorult.exe' pid = frida.spawn( file ) session = frida.attach(pid) script = session.create_script( """ console.log("Starting ...."); // GetComputerNameWのアドレスを得る (8) var gcExportAddress = Module.getExportByName("kernel32.DLL", "GetComputerNameW"); // フック設定 (9) Interceptor.attach(gcExportAddress, { onEnter: function (args) { console.log("Entering .... GetComputerNameW"); }, onLeave: function (retval) { console.log("Leaving .... GetComputerNameW"); // 書き換え文字列のメモリ確保 (10) var st=Memory.allocUtf16String("NTTcom8213"); // espを起点にメモリダンプ (11) var esp = this.context.esp; var pointer = new NativePointer(esp); var mem = Memory.readByteArray(pointer, 100); console.log("Stack memory dump ... ESP: " + esp); console.log(mem); console.log("[*] Return Value exists ESP + 8"); // esp+8のアドレスを取得しポインタ作成 (12) esp = parseInt(esp) + 8; var pointer = new NativePointer(esp); console.log("[*] Original Return Value: " + Memory.readUtf16String(pointer)); // 書き換え実施 (13) Memory.copy(pointer, st, 22); console.log("[*] Altered Return Value: " + Memory.readUtf16String(pointer)); } }); var CreateMutexA = Module.getExportByName("kernel32.DLL", "CreateMutexA"); Interceptor.attach(CreateMutexA, { onEnter: function (args) { console.log("entering .... CreateMutexA"); console.log("[*] CreateMutexA arg: " + args[2].readAnsiString()); }, }); """ ) script.load() frida.resume(pid) sys.stdin.read() 本スクリプトの動作結果は以下になります。GetComputerNameWの結果をフックにより書き換え、Mutex文字列における、コンピュータ名から生成される、後半2つのブロックの文字列の変更を確認できました。AzorultのC2側ではこの文字列を逆のロジックでデコードし、コンピュータ名を得るので、NTTcom8213という誤った情報を与えることができました。 補足 今回Fridaを検証してみて、気づいた点を書いておきます。 アドレスによりフックが失敗するケースあり Step.2の検証では、WindowsAPIではなく、任意のアドレスをNativePointerにしてフックを仕掛けました。アドレスによってはクラッシュやエラーメッセージが出て、追跡対象のプログラムが正しく動作しなくなるケースがありました。 異なる構文や工夫をしてフックを試みましたが、断念しました。(おそらくデバッガのブレークポイントと同じ様に、停止させたいメモリアドレスに対しInt3命令(0xCC)に置き換えることで、実現していると思ったので何かしらやりようがあると思いましたが) Interceptor.attachのonLeave動作仕様の正しい理解 Step.3検証で、想定したメモリアドレスとずれていたことを述べましたが、OnLeaveの仕様を私がちゃんと理解できていないと思われます。私の想定では、onEnterで関数の頭、onLeaveで関数のRETもしくはRET後の想定だったのですが、それぞれ EIP を表示させると、OnLeaveに関してもOnEnterと同一の関数冒頭のアドレスでした。(つまりRet時にthis.context.[レジスタ]で想定した値が取れない) NativeFunction を用い関数を定義することでうまく行かないかなと試しましたが、現状で解決策が見つかっていません。 サンプルAzorult検体は、API MonitorのFakenet-ngログで見たように、HTTP POST通信により、Mutexに登録したユーザ情報をエンコードしてC2サーバへの送信を試みることがわかりました。この際、先程の符号化関数の処理に加えて、さらに3Byteの別のxorキーで、あるロジックにより難読化を行い最終的なPOSTデータを生成します。また、感染後はビーコン以外のC2通信も観測できます。興味のある方は、該当キーやロジック等さらなる解明に挑戦してみてください。 今回用いたサンプルAzorultのsha256 fda64c0ac9be3d10c28035d12ac0f63d85bb0733e78fe634a51474c83d0a0df8 終わりに いかがだったでしょうか。今回はFridaを使いマルウェアの動作を解析し、誤った情報を与えることを行いました。ちょっとした動作の変更であればデバッガ上で可能ですが、任意のコード実行を含む大きな変更を伴う場合などは、動的バイナリ計装の恩恵は大きくなります。実は当初、最近勢いがあるエミュレーター Qiling で同様の解析を行うつもりで検証をしていたのですが、上記補足に書いた以上にインパクトのある問題が出て、今回の目標だと適さないことがわかったので急遽本検証に変更しました。マルウェア解析では常に2つ以上のツールを用意するのが王道と言われますが、その必要性を実感しました。Qilingについても機会がありましたら紹介したいと思います。 それでは、明日の記事もお楽しみに!
この記事は NTTコミュニケーションズ Advent Calendar 2021 の19日目の記事です。 はじめに こんにちは。イノベーションセンターテクノロジー部門の田中と申します。インターネットにおける攻撃インフラ撲滅に向けた追跡活動を主に行っています。例えば、追跡中のIPアドレスは真に該当マルウェアのC2であるか、現在も活動中であるか等を OSINT を活用して精度を上げて特定していくのですが、さらに情報が必要になるケースがあります。その際に、有力な技術の1つになるのが、マルウェアやC2に与える情報を制御し、挙動の差異を観測するという手法です。本記事では、 Frida というツールを利用して、解析・変更の初歩の部分について行ってみたいと思います。 概要 前半は、準備として API Monitor というツールを用い、APIコールをトレースし、マルウェアの挙動を簡単に把握します。後半は、動的バイナリ計装(DBI:Dynamic Binary Instrumentation)フレームワークの1つである Frida を使ってマルウェアの一部の動作をフックし、その動作を変更してみます。DBIは、実行中のプログラムにコードを挿入可能な技術で、関数をフックして入出力の読み取り・書き換えすることが可能となります。API Monitorはマルウェア解析トレーニングコースである SANS FOR610 でも扱われますが概要にとどまるのと、Fridaは、マルウェア解析適用事例が少ないため、今回紹介することにしました。 解析環境の準備 マルウェアを動作させて解析するため、専用の仮想環境を準備します。VirtualBoxやVMware等上にWindows10をセットアップし、解析ツールを準備します。今回は、デバッガ x64dbg 、 API Monitor 、ネットワークツール Fakenet-ng を用います。各サイトからダウンロードしてインストールしてください。解析ツールを一括導入してくれる FlareVM を用いるのもお勧めです。仮想環境を構築したらスナップショットを取得し、マルウェア解析後は、元の状態へ戻すようにしてください。(尚、Fridaについては後半で導入方法から紹介します。) 解析検体 今回は、 Azorult というマルウェアを対象にします。 Anyrun から入手したサンプルを使用します( Azorultサンプル解析ページ )。Azorultサンプルの 解析ページ を確認して頂くとわかるように、このAzorultサンプルは、コロナ感染状況パネルを模擬したドロッパーから、プロセスID:2636でドロップされたものです。(ドロッパーはAzorultではないのでご注意ください) API Monitorによる簡易解析 準備した解析環境で、Azorultサンプルを解析していきます。まず、Fakenet-ngを起動します。Fakenet-ngはダミーのDNSサーバやWebサーバ相当の動作をローカル環境で担ってくれ、マルウェアのC2等の通信を仮想環境内に閉じ込めてくれます。 次に、API Monitorを用いてサンプルを起動してAPIコールをトレースします。API Monitorは2013年に開発が止まっていますが、有益なツールなので使い方の一部を解説します。API Monitor(x86)を起動後、File->Monitor New Processで、Azorultのサンプルを選択します。(選択画面で、exeファイルのみしか選択できない仕様のため、Azorultサンプルの拡張子が異なる場合は、exeに変更してください。)サンプルを選択しOK押下すると、Azorultが起動され、以下のように呼び出されたAPIが記録されます。 起動後、数秒後にFile->Pause Monitoringでトレースを停止します。その後以下の図のように、API Filter画面のDisplayをクリック、Add Filterをクリックすると、Display Filterダイアログがでるので、「Calling Module Name」を選択肢、Azorultサンプルのファイル名(ここではazorult.exe)を入力しOK押下。 すると、azorult.exeから呼び出されたAPIコールのみにフィルタできます。サンプル起動直後のAPIコールは下図のように、 LoadLibraryA と GetProcAddress によりマルウェアが呼び出したいAPIのアドレス解決をしていることがわかります。これは、Windows仕様における 明示的リンク(Explicit Linking) の挙動です。ファイルヘッダの IAT(Import Address Table) に記載され、表層解析で容易にAPIを発見な 暗黙的リンク(Implicit Linking) とは対象的です。尚、IATは、Windows実行ファイルである PE 形式で定義されるヘッダ情報です。 もう少し下にスクロールしてみると、下図のように、なにやらWindowsバージョン情報と、 CreateMutexA でMutexが作られていることがわかります。Mutexは2重起動等を防ぐため等に正規アプリケーションで用いられますが、マルウェアでも散見される挙動です。 さらにスクロールしてみると、下図のように、 wininet.dll がロードされ、インターネット接続に関するAPI群をアドレス解決している様子を見ることができます。 Fakenet-ngのログを見ると、ドメインcoronavirusstatus[.]spaceに対し101バイトの長さの通信を行っていることがわかります。(実際にはFakenet-ngが該当ドメインの権威サーバに代わり、DNS/HTTP疑似応答していることがログでわかります。尚、執筆時点で、該当ドメインのAレコード応答はありませんでした。) Step.1 FridaによるCreateMutexAの追跡 前節のAPI Monitorによる解析で、Azorultサンプルは、動的に用いるAPI関数のアドレス解決していました。さらに、ハイフンにより繋がれた文字列でMutexを作り、あるドメインに接続し101バイトの通信することがわかりました。Fridaを使いCreateMutexAにフックをかけてみます。FlareVMではfridaは導入されないので以下の様にインストールします。 pip install frida-tools ここでは以下のコードを準備しました。Fridaでは、解析プロセスの立ち上げや解析スクリプトの適用はpythonにより行いますが、フック等を明示する解析スクリプト自体はJavaScriptにより記述します。 Module のgetExportByNameにより、DLL名とAPI名を指定しアドレスを得た(1)あとで、 Interceptor のattachを用い、指定アドレスをフックし、関数が呼ばれる前の処理(onEnter)、呼ばれたあとの処理(onLeave)を記載できます。このように、フックしたあとで、ユーザが任意のコードを実行させることがFridaの特徴になります。ここでは、 CreateMutexA をフックし(2)、引数の2番目のlpNameを表示(3)するようにします。(リンクからCreateMutexAのプロトタイプを確認できます) import frida import sys file = 'C: \\ work \\ frida \\ azorult.exe' pid = frida.spawn( file ) session = frida.attach(pid) script = session.create_script( """ console.log("Starting ...."); // CreateMutexAのアドレスを取得 (1) var CreateMutexA = Module.getExportByName("kernel32.DLL", "CreateMutexA"); // 該当アドレスでフック (2) Interceptor.attach(CreateMutexA, { // 関数が呼ばれる際に引数を取得し表示 (3) onEnter: function (args) { console.log("Entering .... CreateMutexA"); console.log("[*] CreateMutexA args: " + args[2].readAnsiString()); }, }); """ ) script.load() frida.resume(pid) sys.stdin.read() 動作させると以下のように、API Monitorで見たMutexが作られていることがわかります。 Step2. FridaによるMutex文字列生成ロジック追跡 では、このハイフン区切りのMutex文字列はどのように生成されるのでしょうか?Azorultを解析した blackberry社のブログ記事 によると、下図のようなロジックで生成されるとあります。感染Windows端末における GUID 、プロダクト名、ユーザ名、コンピュータ名を入力として、ある関数(図ではID generate func())で4Byteの固定長に変換され、それがハイフンで繋がれMutex文字列になっていることがわかります。 引用元: https://blogs.blackberry.com/en/2019/06/threat-spotlight-analyzing-azorult-infostealer-malware この関数(以下、符号化関数と呼びます)をデバッガを用い確認してみましょう。Azorultサンプルは32bitバイナリですので、x32dbgを起動し、Azorultサンプルを開き、0x00406204にブレークポイントを設定します。(x32dbg左下のコマンド窓から、bp 406204と入力しエンター押下。)ブレークポイント設定後、実行(F9キー押下)を数回起動すると、該当アドレスで停止します。(数回が何回なのかは、x32dbgの「設定->Events」の設定によります。また、ブレークポイントを過ぎてしまった場合、Ctrl+F2キー押下で開始時点からやり直せます。)0x00406204のブレークポイントに到達したら、gキーを押下します。すると以下のグラフ画面が表示されます。 0x00406204が符号化関数の先頭になります。この符号化関数は何度か呼ばれており、画面は、1回目の呼び出し時になります。1回目の符号化対象はGUIDで、関数内のローカル変数を意味する[ebp-4]にこのGUID文字列のポインタが格納されているのがわかります。真ん中のブロックで、xor演算やシフト演算(shl,shr)が含まれ、ループにより繰り返し回実行されることがわかります。繰り返しは対象文字列の先頭から末尾まで続きます。特に興味深いのが、xorで固定の値(ここでは0x6521458a)が使われていることです。このサンプルはこの値をキーとして、符号化処理をしていることがわかります。こういった文字列は IOC として活用できる可能性があります。 符号化関数のアドレスが確認できたので、Fridaで関数をフックしてみます。先程のコードに以下を追加します。先程のようにAPIではなく任意のアドレスでフックさせたい場合は、 NativePointer を用いポインタを作っておきます(4),(6)。符号化関数の入力値については、eaxレジスタに文字列へのポインタが入っているので、this.context.[レジスタ名]で読み取り、 Memory.readAnsiString でポインタの示す文字列を取得します(5)。符号化関数の出力値はebxレジスタに格納されるので、同様にthis.context.ebxで値そのものを取得します(7)。 // 復号化関数開始アドレスのフック用ポインタ作成 ( 4 ) var enc_func_start = new NativePointer( "0x00406204" ) Interceptor.attach(enc_func_start, { onEnter: function (args) { console.log( "entering .... Encode_function" ); // eaxレジスタの値の示す文字列取得 ( 5 ) console.log( "[*] Input_value : " + Memory.readAnsiString(this.context.eax)); }, }); // 復号化関数終了アドレスのフック用ポインタ作成 ( 6 ) var enc_func_end = new NativePointer( "0x00406265" ) Interceptor.attach(enc_func_end, { onEnter: function (args) { //console.log( "exiting .... Encode_function" ); // ebxレジスタの値取得 ( 7 ) console.log( "[*] Output_value : " + this.context.ebx); }, }); 上記コードの実行結果は以下のようになります。 GUID 、プロダクト名、ユーザ名、コンピュータ名、及び前述4つをつなげた文字列、の計5個が順番に、Input_valueとして符号化がされて、4Byteの文字列がOutput_valueとして生成されているのがわかります。最終的にハイフンを挿入されてMutexが作られています。 Step.3 FridaによるGetComputerNameW結果の書き換え 最後に、Fridaを用い、マルウェアに誤った情報を伝える一例として、 GetComputerNameW の結果を書き換えてみます。前節までの分析でみたように、本Azorultサンプルは、コンピュータ名等を用いMutexを作り、それをHTTP通信によりビーコンとしてC2に送ります。一般に、C2側ではこの情報をデコードして感染端末情報を入手することで、ターゲットか否かを判断し、情報搾取等次の行動に移ります。 ここでは以下のコードを準備しました。Step.1と同様に、 Module のgetExportByNameで、DLL名とAPI名を指定しアドレスを得た(8)あと、 Interceptor のattachで、指定アドレスをフックします。関数の呼ばれたあとの処理(onLeave)を記載していきます。まず、書き換え対象の文字列を Memory .allocでUTF16としてメモリを確保し変数stに割り当てます(10)。次に、GetComputerNameWにより得られたコンピュータ名の格納アドレスを確認していくのですが、ここで問題発生しました。GetComputerNameWのLeave時に、コンピュータ名を格納したメモリアドレスは、[ESP-8]と想定したのですが、そこにはありませんでした。そこで、 Memory .readByteArrayを用いESPから100Byteダンプします(11)。すると[ESP+8]にコンピュータ名の存在が確認できるので、このアドレスを書き換え対象としてフック用のポインタを作成します(12)。 Memory .copyで、先程用意した、変数stに書き換えます(13)。書き換わった後のメモリを表示すると確かに「NTTcom8213」にコンピュータ名が変わっていることを確認できます。 import frida import sys file = 'C: \\ work \\ frida \\ azorult.exe' pid = frida.spawn( file ) session = frida.attach(pid) script = session.create_script( """ console.log("Starting ...."); // GetComputerNameWのアドレスを得る (8) var gcExportAddress = Module.getExportByName("kernel32.DLL", "GetComputerNameW"); // フック設定 (9) Interceptor.attach(gcExportAddress, { onEnter: function (args) { console.log("Entering .... GetComputerNameW"); }, onLeave: function (retval) { console.log("Leaving .... GetComputerNameW"); // 書き換え文字列のメモリ確保 (10) var st=Memory.allocUtf16String("NTTcom8213"); // espを起点にメモリダンプ (11) var esp = this.context.esp; var pointer = new NativePointer(esp); var mem = Memory.readByteArray(pointer, 100); console.log("Stack memory dump ... ESP: " + esp); console.log(mem); console.log("[*] Return Value exists ESP + 8"); // esp+8のアドレスを取得しポインタ作成 (12) esp = parseInt(esp) + 8; var pointer = new NativePointer(esp); console.log("[*] Original Return Value: " + Memory.readUtf16String(pointer)); // 書き換え実施 (13) Memory.copy(pointer, st, 22); console.log("[*] Altered Return Value: " + Memory.readUtf16String(pointer)); } }); var CreateMutexA = Module.getExportByName("kernel32.DLL", "CreateMutexA"); Interceptor.attach(CreateMutexA, { onEnter: function (args) { console.log("entering .... CreateMutexA"); console.log("[*] CreateMutexA arg: " + args[2].readAnsiString()); }, }); """ ) script.load() frida.resume(pid) sys.stdin.read() 本スクリプトの動作結果は以下になります。GetComputerNameWの結果をフックにより書き換え、Mutex文字列における、コンピュータ名から生成される、後半2つのブロックの文字列の変更を確認できました。AzorultのC2側ではこの文字列を逆のロジックでデコードし、コンピュータ名を得るので、NTTcom8213という誤った情報を与えることができました。 補足 今回Fridaを検証してみて、気づいた点を書いておきます。 アドレスによりフックが失敗するケースあり Step.2の検証では、WindowsAPIではなく、任意のアドレスをNativePointerにしてフックを仕掛けました。アドレスによってはクラッシュやエラーメッセージが出て、追跡対象のプログラムが正しく動作しなくなるケースがありました。 異なる構文や工夫をしてフックを試みましたが、断念しました。(おそらくデバッガのブレークポイントと同じ様に、停止させたいメモリアドレスに対しInt3命令(0xCC)に置き換えることで、実現していると思ったので何かしらやりようがあると思いましたが) Interceptor.attachのonLeave動作仕様の正しい理解 Step.3検証で、想定したメモリアドレスとずれていたことを述べましたが、OnLeaveの仕様を私がちゃんと理解できていないと思われます。私の想定では、onEnterで関数の頭、onLeaveで関数のRETもしくはRET後の想定だったのですが、それぞれ EIP を表示させると、OnLeaveに関してもOnEnterと同一の関数冒頭のアドレスでした。(つまりRet時にthis.context.[レジスタ]で想定した値が取れない) NativeFunction を用い関数を定義することでうまく行かないかなと試しましたが、現状で解決策が見つかっていません。 サンプルAzorult検体は、API MonitorのFakenet-ngログで見たように、HTTP POST通信により、Mutexに登録したユーザ情報をエンコードしてC2サーバへの送信を試みることがわかりました。この際、先程の符号化関数の処理に加えて、さらに3Byteの別のxorキーで、あるロジックにより難読化を行い最終的なPOSTデータを生成します。また、感染後はビーコン以外のC2通信も観測できます。興味のある方は、該当キーやロジック等さらなる解明に挑戦してみてください。 今回用いたサンプルAzorultのsha256 fda64c0ac9be3d10c28035d12ac0f63d85bb0733e78fe634a51474c83d0a0df8 終わりに いかがだったでしょうか。今回はFridaを使いマルウェアの動作を解析し、誤った情報を与えることを行いました。ちょっとした動作の変更であればデバッガ上で可能ですが、任意のコード実行を含む大きな変更を伴う場合などは、動的バイナリ計装の恩恵は大きくなります。実は当初、最近勢いがあるエミュレーター Qiling で同様の解析を行うつもりで検証をしていたのですが、上記補足に書いた以上にインパクトのある問題が出て、今回の目標だと適さないことがわかったので急遽本検証に変更しました。マルウェア解析では常に2つ以上のツールを用意するのが王道と言われますが、その必要性を実感しました。Qilingについても機会がありましたら紹介したいと思います。 それでは、明日の記事もお楽しみに!