TECH PLAY

NTTドコモビジネス

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

602

この記事は、 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についても機会がありましたら紹介したいと思います。 それでは、明日の記事もお楽しみに!
アバター
この記事は NTTコミュニケーションズ Advent Calendar 2021 18日目の記事です。 はじめに こんにちは、イノベーションセンターの齋藤 暁です。普段はコンピュータビジョンの技術開発やAI/MLシステムの検証に取り組んでいます。今回は、JetsonでJAXが使えるように環境の構築をしていくのですが、時間の関係上ビルドまでたどりつくことができませんでした。ただ、せっかくなので奮闘記として、どのような方法でエラーハンドリングしたかを残しておきたいと思いますので。自身への戒めという側面もありますが、次にJetsonでJAXを使いたい人が、この方法はクリティカルなエラーハンドリングではないんだということを知っていただくことが嬉しいです。 (JAXは勉強したいと思っているので、ビルドがうまく行った際には、情報を更新したいと考えておりますので、適宜のぞいてみていただけるとビルドできた例を見ることができるかもしれないです。) JAXとは google/jaxから引用 まずJAXについて軽く説明をしたいと思います。 JAXとはAutogradとXLAを組み合わせた機械学習のライブラリです。 特徴的な点としては、JAXがXLAを使ってGPUやTPU上でNumPyをコンパイルして実行できるところです。jitのデコレータによるコンパイルとgradによる自動微分を組み合わせて使用できます。 今後も新しい機能がでることを明言していることから、今後ますます発展するライブラリであると思っています。 モチベーション 現在私は、Jetsonなどのエッジデバイスを用いた映像解析アプリケーションの開発をしています。この開発では、エッジデバイスを含めたモデルの最適な配置についても開発の課題としております。 高速に計算をできれば、よりリアルタイムに近い推論をできる嬉しさがあると考えられます。ただ、JAXは2018年にローンチされたライブラリであるため、エッジデバイスで利用することに適しているのかという議論についてもあまり数がないように思いました。 そのため、今回はJAXをJetsonにそもそもインストールし、使用できるのかという部分から検証したいとモチベーションがあります。 環境構築 ※注意: まだJetsonでJAXを使えるようにできておりません。できた際には、この記事を更新したいと思っていますが、下記の方法では私の環境ではビルドできないことを確認しています。 最初は、Jetson Xavier NXで環境構築を試みたのですが、私の環境ではbuildの途中でメモリが溢れてしまいました。もしかしたらSwap領域を増やせば、落ちないかもしれないですが、今回はJetson AGX Xavierでビルドを行いました。(Jetson Nanoでできたという記事がありますが、Jetson Xavier NXでのビルドで落ちていたので、こちらもどこかでやりたいですね) Jetsonの環境 まず、Jetson環境の構築から始めます。 今回私が使用するJetsonのOSであるL4Tのバージョンは、32.6.1。Jetpackのバージョンは4.6を選択しました。内部ストレージは、32GBしかないため、NvMeのSSD 256GBを増設しました。 bootFromExternalStorage の手順に従ってJetPack4.6をNvMeから起動します。以下に簡単にまとめます。詳しい説明については、githubに載っているのでそちらの参照をお願いいたします。 # Ubuntu18.04 or Ubuntu20.04を入れたx86_64ベースのホストPCを用意します # git cloneでbootFromExternalStorageをインストールします git clone https://github.com/jetsonhacks/bootFromExternalStorage.git cd bootFromExternalStorage . install_dependencies.sh . get_jetson_files.sh # JetsonとホストPCをUSBケーブルでつなぐ # このスクリプトがうまくいくと、Jetson AGX Xavierが起動する . flash_jetson_external_storage.sh # Jetson AGX Xavierに、bootFromExternalStorageをインストールします # 以下のスクリプトを実行することによってcuda、cudnnなどをインストールできます . install_jetson_default_packages.sh JAXの環境構築 .bashrcの一番下に以下を追加します。 sudo vim ~/.bashrc export PATH=/usr/local/cuda/bin${PATH:+:${PATH}} export LD_LIBRARY_PATH=/usr/local/cuda/lib64${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}} 次に以下のスクリプトを実行してインストールするとビルドが始まります。 source ~/.bashrc #PPAの追加 sudo add-apt-repository ppa:deadsnakes/ppa sudo apt update # 現在(2021.12.15)のJAXは、python3.6を実行環境から外しているため、 # 今後アップデートの際にサポートから外れないように、python3.9を選択しました sudo apt install python3.9 python3.9-dev sudo apt install python3-pip sudo pip3 install virtualenv virtualenv -p /usr/bin/python3.9 jax source ./jax/bin/activate sudo apt-get install python3.9-distutils #必要なライブラリのインストール python -m pip install numpy scipy six wheel sudo apt install g++ #jaxのインストール git clone https://github.com/google/jax cd jax python ./build/build.py おやおや...? 何かエラーが出てますね長いエラーがでているので、とりあえず怪しい部分を抜粋してみます。 #エラー文 [2,118 / 5,712] Compiling llvm/lib/Target/X86/X86ISelLowering.cpp; 50s local ... (8 actions running) [2,155 / 5,712] Compiling llvm/lib/Target/X86/X86ISelLowering.cpp; 216s local ... (8 actions running) ERROR: /home/hoge/.cache/bazel/_bazel_nvidia/a5643b5cc286b9b13a96818003a4a7dd/external/org_tensorflow/tensorflow/python/lib/core/BUILD:49:11: Compiling tensorflow/python/lib/core/bfloat16.cc failed: (Exit 1): gcc failed: error executing command (cd /home/hoge/.cache/bazel/_bazel_nvidia/a5643b5cc286b9b13a96818003a4a7dd/execroot/__main__ ... JetsonのCPUのアーキテクチャはARM64ですが、X86のllvmをコンパイルしていますね。ですので、とりあえずarmのllvm一式をインストールします。 sudo apt-get -y install libllvm-7-ocaml-dev libllvm7 llvm-7 llvm-7-dev llvm-7-doc llvm-7-examples llvm-7-runtime エラー文は変わってますね、今度は /.cache/bazel のパーミッションがないのか? #エラー文 ERROR: /home/nvidia/.cache/bazel/_bazel_nvidia/a5643b5cc286b9b13a96818003a4a7dd/external/llvm-project/llvm/BUILD.bazel:2068:11: Compiling llvm/lib/Passes/PassBuilder.cpp failed: (Exit 4): gcc failed: error executing command とりあえず、パーミッションをこのコマンドで変えてみます。 sudo chmod -R u+rw ~/.cache/bazel/ エラーが止まらない..。 次にできることとして、JetPackを使うと感じることが、ライブラリのバージョンによって動く動かないがあるので、bazelのバージョンによる問題ではないかと仮定しました。 現在のJAXのbranchのbazelのバージョンは、4.2.1です。しかし、bazelのpreinstallができるバージョンを見ているとbazel4.2.1は、JetPack4.6.1に対応していないのかなと思われます。そのため、今回はJAXの最新バージョンを使うのではなく、bazel3.7.2がビルドされるバージョンを使用してみます。 #bazel3.7.2 且つarmに対応しているtagの選択 git checkout -b jax-v0.2.17 # .cache/bazel/ のパーミッション系の問題が起きないように sudo chmod -R u+rw ~/.cache/bazel/ python ./build/build.py うーん。さっきとエラーが変わっていないような気がします。 numpyのバージョンという線もあるので、一応numpy1.21.4から下げてビルドをしてみましたが、エラーは変わらず..。 #エラー文 ERROR: /home/nvidia/.cache/bazel/_bazel_nvidia/f136147ae544c503f5fd3870723c0471/external/org_tensorflow/tensorflow/compiler/xla/service/cpu/BUILD:393:11: C++ compilation of rule '@org_tensorflow//tensorflow/compiler/xla/service/cpu:ir_emitter' failed (Exit 4): gcc failed: error executing command ここで、タイムアップとなってしまいました。他にも細々とやったのですが、主にやったことが以上となっています。 所感 JetsonでJAXの環境構築にチャレンジしたのですが、冬休みの課題となりました。JAXで遊ぶことは今回達成できませんでしたが、中で何がコンパイルされているのか見ることはできたので、良い勉強になったかと思われます。また、この方法でやれば良いのではという情報を共有してくださる方がおりましたら str.saito@ntt.com まで教えていただけると嬉しいです。 おわりに 今回は、JAXをJetson AGX Xavierで環境構築をしたかったのですが、まだこれをすれば確実にビルドできる!という方法がなく1週間ほどでは難しかったです。そのため、とりあえずビルドをすることが今後の課題となりそうです。 そして今後は、JAXを使って物体検出のモデルを書いてみたいと思っているので、その際にベンチマークを測れればいいなと思います。(年末年始にチャレンジしてみたい) それでは、明日の記事もお楽しみに! 参考 @software{jax2018github, author = {James Bradbury and Roy Frostig and Peter Hawkins and Matthew James Johnson and Chris Leary and Dougal Maclaurin and George Necula and Adam Paszke and Jake Vander{P}las and Skye Wanderman-{M}ilne and Qiao Zhang}, title = {{JAX}: composable transformations of {P}ython+{N}um{P}y programs}, url = {http://github.com/google/jax}, version = {0.2.5}, year = {2018}, }
アバター
この記事は NTTコミュニケーションズ Advent Calendar 2021 の17日目の記事です。 はじめに こんにちは。プラットフォームサービス本部 データプラットフォームサービス部でSmart Data Platform(SDPF)のサービス企画を行っている安井・小野です。 閲覧頂きありがとうございます。我々は当社が提供しているSDPFサービスを組合わせた具体的な事例紹介をしております。 前回、 第1回目 では、いくつかの組み合わせ事例をご紹介しました。 今年を振り返ってみると、コロナ禍による在宅勤務が推進され、テレワークが大々的に進み、今ではオンラインミーティングツール(Microsoft Teams等)は必要不可欠なツールとなりました。 また、オンライン利用が広がる中、サイバー攻撃を受けるリスクもこれまでよりも高まっており、ランサムウェアの被害が社会問題にもなってきています。 SDPFサービス群の組合せ事例の第2回目の紹介として、「オンラインツール含むMicrosoft社のMicrosoft 365のデータを安全にランサムウェア対策をしてバックアップする」ユースケースについて、 SDPF上でどのように実現するのか、検証確認した結果についてご紹介させていただきます。 背景 Microsoft 365などのSaaSサービスがビジネスツールとして利用が進む ビジネスツールとしてMicrosoft 365の利用拡大は進んでおり、特にコロナ禍でのワークスタイル変革やテレワークの定着化でMicrosoft Teamsをオンラインツールとして利用が増々進みました。 このようなビジネス上でやり取りする重要なデータはSaaS/クラウド上に存在し、Microsoftの標準の機能でもバックアップしていますが、最終的にはデータ保護/管理責任はユーザー側にあります。 そのため、人為ミス対策や退職者データの復旧等に備え、Microsoftもユーザー自身でのバックアップを推奨しています。 ランサムウェア被害が広がり重要データの暗号化等の被害が発生 ユーザー自身がデータ消失のリスク(システム障害、故意/不意のデータ削除等)に備えてバックアップを取得しますが、昨今の情勢から、もう1つの観点で重要な対策を実施する必要があります。 それがランサムウェア対策です。 警察庁での広報資料(p13) では、脆弱性の探索行為等に関する観測状況について記載されており、脅威が増加傾向にあることが確認できます。 実際にランサムウェア被害は社会的な問題にもなり、医療機関にも広がる中、 厚生労働省はサイバー攻撃対策のガイドライン を示しています。 ランサムウェア攻撃への対策としてネットワークの境界対策やエンドポイントでの防御策を講じて感染を回避する入口対策の実施や、感染の検知と対応の事後対策をする体制構築が必要です。 上記を行った上でも実際の被害の発生が続いております。今日では「ランサムウェアに感染した場合でも速やかに復旧できるよう、 データの上書きや改ざんを防止したバックアップを取得しておき、データ暗号化やデータ消失、金銭的な被害を抑える対策を実施する」という考え方が広まりました。 SDPFサービスを利用してどのように実現したか 以降に詳細を記載しますが、NTT Comの各種サービスを組み合わせて実現してみました。 閉域網(Arcster Universal One)から各種クラウドサービスに接続するインターコネクトサービス(Flexible InterConnect)を使ったセキュアな通信経路の確立 安価なストレージサービス(Wasabiオブジェクトストレージ)を使い、かつデータ上書き/改ざんを防止するオブジェクトロック機能を使ったランサムウェア対策の実施 クラウド側の制限に縛られず、オンプレミス環境と同等のサービスレベルを実現するバックアップツール(Arcserve UDP)の利用 上記をお客様自身やSIerの方々で組み上げられることを実際に検証して確認 検証を通じた確認 実際に検証環境を作ってみた Flexible InterConnect (FIC) を活用することにより、 Wasabiオブジェクトストレージ(Wasabi) やMicrosoft 365等様々なサービスと閉域で接続可能。 Arcserve UDP はSDPFクラウド/サーバーのサーバーインスタンス上に導入。 実際に検証を実施してみた ①閉域網を活用したMicrosoft 365データのバックアップとリストア Arcserve UDPを用いて、Microsoft 365の各サービス Microsoft Exchange Online (Exchange Online)、Microsoft SharePoint Online (SharePoint Online)、Microsoft OneDrive (OneDrive)、Microsoft Teams (Teams)のバックアップデータを、 FIC経由でWasabiに保存、Microsoft 365やローカルストレージへリストアを行う検証をしました。 ②クラウドストレージのランサムウェア対策機能の活用 Wasabiに保存したバックアップに対して、Arcserve UDPの2次コピー(復旧ポイントのコピー) 機能を活用し、 Arcserve UDP 8.1からの新機能として選択可能となったオブジェクトロックが有効化されたWasabiを復旧ポイントのコピー先として指定することにより、 指定期間中、復旧ポイントのデータを安全に保持できるか検証しました。 確認後の具体的な気づきポイント 「①閉域網を活用したMicrosoft 365データのバックアップとリストア」では、Microsoft 365の仕様上、Teamsのリストアに関して少しテクニックが必要でした。 例えば、1対1のチャットやチャネル(グループ)のチャットを元の場所にリストアしたいとなった場合、リストアしてもTeams上にはリストアされませんでした。 確認する際は、Exchange Onlineからのリストアのため、Outlookにて確認する必要がありました。 そして、チャネル(グループ)のチャットに関してはチャネル(グループ)に関連づいているメールアドレスが特定のメールボックスを持たない仕様となります。 そのため、チャネル(グループ)のチャットをリストアして確認したい場合は、別の場所へのリストアでリストア先に個人のアカウントを指定し、そのアカウントのOutlookにて確認する必要がありました。 このように、Microsoft 365の仕様上、上記のような確認方法となるサービスもありました。 ですが、Microsoft 365の利用拡大が続いている今、クラウド側の障害や人為ミス等の不具合等でデータを損失した場合でもリストアできることが分かり、有効性が高いことを確認しました。 「②クラウドストレージのランサムウェア対策機能の活用」では、指定した期間中フル権限のユーザーでも上書き・削除等を不可能にする コンプライアンスモード と、 特定の権限を持つユーザーのみ上書き・削除等が可能な ガバナンスモード をそれぞれ確認しました。 利用用途に応じたモードの使い分けにより、被害を抑えるという対策としての有効性が高いことを確認しました。 注意点として、オブジェクトロックを有効化したWasabi上のデータを上書き・削除した場合、別バージョンとして元データが保持されていることを確認できますが、 Wasabiコンソール上から確認する際、バージョン表示モードをオンにしないと元ファイルが上書き・削除されているように見えるため注意が必要でした。 具体的な構成例や設定例の見える化 具体的な構成例や設定例、注意点等の知見についてはKnowledge Centerにて記載しておりますのでご確認ください。記載している情報は2021年12月時点の情報となります。 また、Arcserve社のサイトにて検証で利用した機能等に関する記事やマニュアルも確認できますのでご参考ください。 Knowledge Centerへの記載 Arcserve UDPによるMicrosoft 365のバックアップ&リストアガイド Arcserve UDP によるMicrosoft 365 のバックアップ速度検証 オブジェクトロックに関する記事 最後に 今後もクラウドへのデータ保存やデータ利活用を気軽に活用できるきっかけを活動を通じて発信していきます。 「ここの記載がわかりにくい」等のご意見や、「SDPFでこんなことができないか」等の疑問がありましたら、以下メールアドレスに対してお気軽にコメント等ご連絡ください。 sdpf-testbed-02@ntt.comにメールを送る ※お手数ですが@を全角文字から半角文字に置き換えてください。 それでは、明日の記事もお楽しみに!
アバター
AWS Lake Formationでのデータレイク登録からデータアクセスまで この記事は NTTコミュニケーションズ Advent Calendar 2021 の16日目の記事です。 はじめに はじめまして!BS本部SS部の荒井です。データマネジメントに関するプリセールスを担当しています。 今回はアドベントカレンダー企画ということで、 AWS Lake Formation に関する記事を投稿をさせていただきます。 データレイクとAWS Lake Formation 近年データ分析の盛り上がりなどから、散逸している様々な形式のデータを一元管理できるレポジトリ、いわゆるデータレイクを導入するケースが増えてきています(参考: データレイクとは )。 例えばシステムごとに保存されていた「会員データ」「購入履歴」「問合せ履歴」などのデータをデータレイクに集約することでシステム横断の顧客分析を手軽に行うことができ、利益率向上の施策を検討したり顧客離れの原因を理解することにつながります。 AWSではこのデータレイクを構築するための Lake Formation というサービスがあります。 Lake Formation ではデータカタログや権限管理などデータのマネジメントに関わる様々な機能を持っています。 今日はこの Lake Formation を用いて、以下の流れでデータレイク作成からデータカタログを介したデータアクセスまでの操作に関してお話ししたいと思います。 データレイクを作成する 保持しているファイルをデータレイクに保存する データレイクに保存したファイルのメタデータを自動取得しデータカタログに登録する データカタログを介してファイルの中身にアクセスする 権限周りの設定やフォルダ構成など、ところどころで注意点があるのでこちらも随時説明していけたらと思います。 全体図 全体のイメージは以下になります。 流れに沿うとポイントは以下の通りです。 Lake Formation は裏では Glue の機能をベースに作られているため、ここでも Glue の機能を使います。 データレイクを作成する ⇒ データレイクはAWSだと S3 になります。 保持しているファイルをデータレイクに保存する ⇒ 今回は手動で実施します。 データレイクに保存したファイルのメタデータを自動取得しデータカタログに登録する ⇒ メタデータの自動取得は Glue の Crawler を使います。データカタログも Glue の機能です。 データカタログを介してファイルの中身にアクセスする ⇒ 今回は手軽にSQLを叩いてアクセスできる Athena を使います。 データレイクを作成する データレイクとする S3 Bucket を作成して Lake Formation に登録します。 以降の作業はAWSのコンソールにログインして実行します。 まずは S3 Bucket を作成します。 S3 に移動 > バケットを作成 設定値で以下を入力(指定がないものはデフォルト) > バケットを作成 バケット名 : 任意の名称 次に作成した S3 Bucket を Lake Formation に登録します。 なお、登録時には IAM Role が必要となりますが通常は AWSServiceRoleForLakeFormationDataAccess というAWSが用意したものを使えば大丈夫です。 ただ、ユースケースによっては自分で作成する必要があります。詳しくは こちら を参照ください。 AWS Lake Formation に移動 初回だと Welcome to Lake Formation のダイアログが出るので、 Add myself > Get Started (データレイク管理者を自分に定義) Data lake locations > Register location 設定値で以下を入力(指定がないものはデフォルト) > Register location Amazon S3 path : 作成した S3 Bucket IAM Role : AWSServiceRoleForLakeFormationDataAccess or 自分で作成した IAM Role これでデータレイクの作成と登録は完了です! 保持しているファイルをデータレイクに保存する 次に、ファイルをデータレイクに保存します。 本記事では手動で行いますが、 Lambda 等を使って自動化しても大丈夫です。 今回はサンプルデータとしてよく使われるタイタニック号乗客者データを使います。 こちら などからダウンロードしておきます。 S3 > データレイクの S3 Bucket を選択 フォルダの作成 から titanic という名称のフォルダを作成 作成した titanic フォルダにタイタニック号乗客者データ( titanic.csv )をアップロード 無事データレイクにデータを保存できました! (補足) 今回のケースでは titanic フォルダを1つ作成するのみでしたが、より多くの種類のデータを保存する場合にはフォルダ構成は重要です。 この後で Crawler という機能を使ってデータカタログへ自動登録する際に、フォルダ構成に従って精査されたり各種名称が決められるためです。 以下にフォルダ構成の一例を示します。 例えば、サービスAの購買データ、アクセスデータ、会員データを保存する場合を考えます。 まず最初はサービスA用の大きくひとくくりのフォルダを作ります。 <データレイク用S3 Bucket> |- serviceA ←作成 次に、購買データ、アクセスデータ、会員データ用のフォルダ( buy , access , member )を作ります。 <データレイク用S3 Bucket> |- serviceA |- buy ←作成 |- access ←作成 |- member ←作成 購買データは年ごとの 2020.csv , 2021.csv というようなファイルだったとします。この場合は、作成した buy フォルダにそのままアップロードします。 <データレイク用S3 Bucket> |- serviceA |- buy | |- 2020.csv ←アップロード | |- 2021.csv ←アップロード |- access |- member アクセスデータは時間ごとの 2200.csv (22時のデータ), 2300.csv (23時のデータ)というようなファイルで、日ごとのフォルダが必要だったとします。この場合は作成した access フォルダ下に日付のフォルダを作り、その中にアップロードします。 <データレイク用S3 Bucket> |- serviceA |- buy | |- 2020.csv | |- 2021.csv |- access | |- 20210101 ←作成 | | |- 2200.csv ←アップロード | | |- 2300.csv ←アップロード | |- 20210102 ←作成 | | |- 2200.csv ←アップロード | | |- 2300.csv ←アップロード |- member 最後に、会員データは member.csv という1つだけのファイルだったとします。この場合も作成した member フォルダにそのままアップロードします。注意点は、この場合も会員データ用のフォルダ( member )を作らないといけなく、 serviceA フォルダ配下にアップロードしてはいけない点です。 <データレイク用S3 Bucket> |- serviceA |- buy | |- 2020.csv | |- 2021.csv |- access | |- 20210101 | | |- 2200.csv | | |- 2300.csv | |- 20210102 | | |- 2200.csv | | |- 2300.csv |- member |- member.csv ←アップロード |- member.csv ←ここにアップロードするのはNG! 上記のようなフォルダ構成を取ると、この後の手順でデータカタログを自動作成した時に buy , access , member というテーブルが作成されます。※今回はこの例のようにはなりません。 データレイクに保存したファイルのメタデータを自動取得しデータカタログに登録する 次に、 Glue の Crawler という機能を用いてデータレイクに保存したファイルのメタデータを自動取得し、データカタログに登録します。 メタデータとはフォーマット(csvやparquetなど)や構造(csvのカラムなど)、保存場所などデータがどのようなものかを表す情報になります。 まず、メタデータを登録する先となるデータカタログのデータベースを作成する作業をします。 Lake Formation > Data catalog 下の Databases > Create database 以下を入力(指定がないものはデフォルト) > Create database Name:任意の名称 また、任意で LF-Tag をデータベースにアサインします。 LF-Tag は Lake Formation Tag の略で、 Lake Formation 内で権限管理などに使えるタグになります。 Lake Formation ではこちらを設定して権限管理することが推奨されています。(この後の手順を進めると以下のように LF-Tag が recommended されているのが確認できます。) ただし、どのような LF-Tag を定義しておくかの設計が事前に必要なのと、データレイク管理者のみが LF-Tag を作成できることは注意が必要です。(参考: LFタグの作成 ) Lake Formation > Permissions 下の LF-Tags > Add LF-Tag Key と Values を設定して Add LF-Tag ここでは LF-Tag の事前の設計が必要ですが、例えば以下のような Key , Values などが考えられます。 Key = env , Values = development, staging, production Key = service , Values = serviceA, serviceB Key = source , Values = titanic, iris  ※今回はこちらを設定します。 iris の方は使用しません。 Databases に戻り、作成したデータベースを選択 > Actions > Edit LF-Tags Assign new LF-Tag を押して、設定したい Key と Values (今回は Key = source , Values = titanic )を選択して Save 次に、 Crawler で使用する IAMロール を作成し権限を付与します。 権限付与はいくつか必要になりますので少し長いですが、最後に補足で各手順がどのような用途のためなのかをまとめます。 IAM > ロール > ロールを作成 以下を入力(指定がないものはデフォルト) > 最後に ロールの作成 ユースケース: Glue を選択 アクセス権限: AWSGlueServiceRole ロール名:任意の名称 作成した IAMロール を選択 > +インラインポリシーの追加 JSON タブに切り替えて以下を入力して ポリシーの確認 > 名前 に任意の名称を入れて ポリシーの作成 { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "s3:*", "Resource": [ "arn:aws:s3:::<データレイクのS3バケット名>", "arn:aws:s3:::<データレイクのS3バケット名>/*" ] } ] } データベースへの権限付与を以下の手順で実施します。 Lake Formation に移動 > Permissions 下の Data lake permissions > Grant 以下を入力(指定がないものはデフォルト) > 最後に Grant IAM users and roles:上記で作成した IAMロール LF-Tags or catalog resources データベースに LF-Tag を設定した場合は Resources matched by LF-Tags (recommended) を選択 > Add LF-Tag でデータベースに設定した LF-Tag の Key , Values を選択(今回は Key = source , Values = titanic ) 設定していない場合は Named data catalog resources を選択 > Databases で作成したデータベースを選択 Database permissions: Database permissions の Super を選択 データレイク( S3 Bucket )への権限付与を以下の手順で実施します。 IAMロール にも手順 10 で S3 Bucket への権限を付与していますが、こちらの手順も必要です。 Permissions 下の Data locations > Grant 以下を入力(指定がないものはデフォルト) > 最後に Grant IAM users and roles:上記で作成した IAMロール Storage locations:データレイクの S3 Bucket を選択 (補足) 各手順で設定したアクセス権限は以下の用途です。 IAMロール への権限付与: データカタログを介さない S3 アクセスに必要 ※今回だと Crawler がファイルのメタデータを取得するときに必要 データベースへの権限付与:データカタログのデータベースアクセスに必要 ※今回だと Crawler がファイルのメタデータを登録するときに必要 データレイクへの権限付与: S3のデータに向いた データカタログを登録するときに必要 ※今回だと Crawler がファイルのメタデータを登録するときに必要 ここまで長かったですが、準備が整ったので Glue の Crawler を作成し、実行します。 Glue に移動 > クローラ > クローラの追加 以下を入力(指定がないものはデフォルト) > 最後に 完了 クローラの名前:任意の名称 Crawler source type : Data stores データストアの選択: S3 インクルードパス:フォルダマークを押して、 データレイク保存 で作成した titanic フォルダ IAM ロールの選択: 既存のIAMロールを選択 IAMロール:上記で作成した IAMロール データベース:上記で作成したデータベース 作成した Crawler を選択 > クローラの実行 完了した後に テーブル へ移動すると、自動的に titanic テーブルが追加されていることを確認できます。これが titanic.csv のメタデータになります。 さらに中身を見てみるとデータの場所や、下にスクロールすると以下のようにカラムの情報が自動的に取得されるところも確認できます。 ここまででメタデータの自動取得とデータカタログへの登録は完了です! データカタログを介してファイルの中身にアクセスする データへのアクセスができるか、 Athena で確認してみます。 Athena はサーバ不要でS3内のデータにクエリを投げられるサービスです。 クエリを投げる対象のファイルは Glue のデータカタログにメタデータが登録されている必要がありますが、ここまでの手順で登録が完了しているのですぐに使い始めることができます。 Athena へ移動 データベース に設定したデータベースを選択して、 テーブル に titanic テーブルが表示されることを確認 エディタで以下を入力して 実行 SELECT * FROM titanic limit 10; クエリの保存先に関するエラーが出たら、 こちら を参考にしてクエリの出力先のS3を別途作成・設定 実行 がうまくいくと以下のようにデータの中身が表示される Athena を使い、データカタログを用いたデータアクセスをすることが確認できました! 特にDB等に保存しているわけではないファイルにクエリを投げられるのは便利で、S3に置いてあるファイルに対してちょっとした分析をしたいときなどに手軽に使えます。 今回は単純な SELECT * でしたが、複雑なクエリを投げて分析することも可能です。 終わりに 今回は、AWSの Lake Formation でのデータレイク登録からデータアクセスまでを試してみました。 ご紹介したのはあくまで一例で、 Lake Formation やそのベースとなっている Glue でできることはもっと多くのことがあるので、興味のある方は使い込んでみてください! AWSを利用してデータレイクを構築するというユースケースは今後増えてくると思いますが、まだ調べてもサンプルケースが出てこない場合が多かったりします。 何か例を探している方がいましたら、この記事が参考になれば幸いです。 ご覧いただきありがとうございました! それでは、明日の記事もお楽しみに!
アバター
はじめまして。データプラットフォームサービス部の橋本です。 今回は、11/20(土)に開催したTechWorkshop「プロのネットワークエンジニアと学ぶ!ISPネットワークのつくりかた」について紹介します。 TechWorkshopとは 各技術分野のプロフェッショナル社員がお届けする、当社の最先端技術を体感し、また身に付けることができるワークショップ形式のプログラムです。 ( 公式ページ より抜粋) 過去にはこのようなワークショップを開催しました。 ソフトウェア開発ハンズオン Kubernetesハンズオン CI/CDハンズオン サイバー攻撃対応ハンズオン ソフトウェアエンジニア座談会 どのワークショップも、公式ページの説明の通り、各分野を主な業務にしている社員が講師となり、かつ自分たちでイベントの企画・運営をしています。 今後もいくつか開催予定ですので、興味のある方は TechWorkshopのページ をウォッチしていただき *1 、ぜひ参加申込をお願いします。 「プロのネットワークエンジニアと学ぶ!ISPネットワークのつくりかた」とは? 本ワークショップ「プロのネットワークエンジニアと学ぶ!ISPネットワークのつくりかた」では、ISPネットワークで利用されているような機器やプロトコルを用いて、ハンズオン形式で実際にネットワークを自分の手で作っていただくといった内容となっています。 ネットワークについていざ勉強しようとしたとき、学校の講義に出たり、書籍を読んだけど実際に触れる機会がない!と思ったことはありませんか? ネットワークの学習には、費用や環境などの制約により、「実際に手を動かしてネットワークをつくって身につけていく」ということが難しい側面があるのではないかと感じています。 我々はこのような課題に対し、ネットワークを勉強している学生のみなさんにも実際の機器に触れてネットワークをつくることができるような機会を提供することで、より多くの方々にネットワークに興味を持っていただきたいと考えています。 また、実際のネットワークに関連した業務につくNTT Comの社員と身近な距離でふれあい、ネットワークを基盤とした価値を提供するNTT Comの会社そのものにも興味をもっていただければいいなと思っています。 ワークショップは大きく分けて午前の講義と午後のハンズオンの2パートに分かれており、1日を通してISPネットワークのつくりかたを体験していただくことができます。 午前は、ハンズオン形式でのワークショップに取り組む前に、必要なネットワークの概念やプロトコル、運用にあたって考慮すべきことなどを学習します。 午後は、午前の講義で学習したプロトコルや考え方に基づき、実際にネットワーク機器の設定を自らの手で行います。 午前 座学・スライドを用いた講義 ネットワーク/インターネットの概要 ネットワーク設計の考え方.ルーティングとは何か? 各種プロトコルの紹介(OSPF/BGP/etc...) ISPネットワークを取り巻く課題と技術(運用/DDoS/BGP Flowspec/etc...) 社員のお仕事紹介 午後 ハンズオン 進め方やゴールの説明。基本的なルータのコマンドなどの説明。 インタフェースのアドレス設定 ルーティング(OSPF/iBGP/eBGP/static)の設定 自分の作ったネットワークでインターネットに通信ができるか検証 エクストラステージ 障害を起こしてみよう ルータの設定自動化 ???(参加者だけのお楽しみ) 懇親会 それぞれのパートについて詳しくご紹介します。 (午前)座学・スライドを用いた講義 講義は、実際にISPネットワークの設計・運用に携わっているメンバーを含むNTT Comの社員が行い、現場で実際に用いられている知識やノウハウなどを直接お伝えします。 ルーティングの基礎から1つずつ説明していきます。ルーティングって何?という学生の方でも大丈夫です。すでに理解されている方も、もう一度振り返って再確認します。 講義の後半ではより高レベルな、現代的なネットワーク技術についても紹介します。 講義で気になる点や不明な点がある場合にはチャットを用いて社員がその場でお答えしていきます。 参加者の方々からは活発に質問が飛び交い、中には社員でも回答に悩むような非常に鋭い質問も見受けられました。 社員のお仕事紹介 今回は、お昼ごはんを食べる前に、社員が実際にどのような業務を行っているかを紹介するコーナーを設けてみました。 ネットワークに関する仕事として、多種多様な業務があります。 このハンズオンには、複数の異なる部署からさまざまな業務を経験してきた社員が参加しています。 異なる経験を持つ複数の社員から、業務の幅広さや、実際の現場では具体的にどういうことをやっているのかという現場の声を直接お伝えすることで、よりネットワークやそれにまつわる仕事に興味を持っていただくことができるといいなと考えています。 今回は下記の業務について紹介しました。 OCNのネットワークコントローラを作ってる話 *2 GIN(Global IP Network) US-NOCとトラフィック分析 IXやIPoE関連の業務について 業務についてもたくさん興味を持っていただけたようで、業務紹介をした社員に対して、業務をより深堀りするような質問を多数いただきました。 (午後)ハンズオン さて、お昼ごはんを食べて休憩をとったあとはこのワークショップのメインであるハンズオンに取り組みました。 ハンズオンでは、ミニISPを作るというお題に沿って実施していきます。ISPなので、実際のユーザ相当のネットワークからインターネットに到達できることを満たす必要があります。 耐障害性、運用性などといった様々な視点で考え、複数のプロトコルを用いてルーティングの設定をします。 このミニISP環境は、複数台のルータを仮想環境上に構築しており、5つのルータからなる環境が個人それぞれに割り当てられます。 1人1環境割り当てられているので、他の人と競合したりせず、割り当てられた環境の中で自由に設定を変えることができます。 ネットワーク機器は共同利用者がいる場合が多く、自分1人で占領して使うのが難しいケースがあると思いますが、このハンズオンではあなただけのISPであり、構築から運用、障害試験までを全て経験できます。 この環境を使って、1つずつルータに設定を自らの手で投入していきます。 ハンズオンは参加者数人と社員2名ごとのグループをつくり、step-by-stepで時間を区切って行っていきます。 わからないことがあったり、設定したのにうまく動かない!といったことがあれば同じグループの社員にいつでも質問できます。 社員も理解をより深めてもらうために積極的にサポートします。 1つのステップごとに社員が設定方法や確認方法を解説する時間を設けています。 設定がまったくわからない、設定したはずなのに思い通りに動かなかった、ということがあっても大丈夫です。 この解説で改めて理解を確認し、社員と一緒に設定しましょう。 このように少しずつ段階を踏んで自分のISPネットワークを作っていくと、最後にはお客様のネットワークからインターネットに到達できるようになります。 ある社員いわく、「ping通った時がうれしいからこの仕事はやめられない」だそうです。 これには多くの社員も共感していました。参加者の皆さんにもこの気持ちを感じていただけたなら嬉しく思います。 エクストラステージ ハンズオンで無事お客様にインターネットへの接続性を提供できた方々には、エクストラステージにチャレンジしてもらいました。 エクストラステージは、自分の作ったミニISPネットワーク上で、運用等も視野に入れたより実践的なシナリオを解いていく内容になっています。 詳しくは参加者だけの秘密にしようと思いますが、はじめの1つについて少しだけ紹介します。 それは「自分のISPを構成しているリンクのうち、あるリンクが切れた場合の動きを観察せよ」といった内容です。 ネットワークの設計では通常、あるリンクが切れたり、ノードが故障することを事前に想定した冗長設計をします。 今回はリンクが切れたときにダイナミックルーティングが想定通り動作し、適切なルーティングが行えるか、お客様への影響を小さくすることが出来たかといった観点で観察してもらいます。 ルーティングプロトコルをより詳細に理解してもらうことを狙っています。 経路の切り替わりを、トラフィック量の変化で観察してもらうと、より直感的に理解してもらえるのではないかと考え、今回はSNMPでトラフィック量を可視化するツールを提供してみました。 traceroute などのコマンドと併用して、意図したとおりに切り替わっていることを確認します。 ほかにはconfig設定の自動化などに取り組んでもらえる内容も用意し、実践的な取り組みにもチャレンジしていただきました。 ここまでで午後のハンズオンの内容は終了です。参加者の皆様は普段慣れないルータの設定、コマンド操作、複数台にわたる設定といった、非常に多くの作業をこなし、終わった頃には相当の疲れと達成感があったのではないかと思います。 しかし参加者からは、「もっと長くハンズオンをやりたかった」などの声もあり、想像を超える熱意を嬉しく思っています。今後こういった声にもお答えできるように改善を検討させていただきます! ハンズオンのあとには休憩を挟んで、参加者と社員を含めて懇親会が行われました。お互いに疲れを癒やし、交流を深めていただける場となりました。 参加者の声 参加者の皆様からは、事後アンケートにて下記のようなコメントをいただきました! やはり、「実際に手を動かしてつくる」といった部分が好評いただけているようです。 午前中に、わかりやすく丁寧な講義をしていただき、午後からはそれを手を動かして実践できたことで、ルーティングに対する理解が深まったと思う。 ハンズオンでは、BGPやOSPFの基本的な設定から、LPやコストといった実践的な設定に触れることができたので、とても良い経験になりました。 NW入門ではISPがどのようなことに気を付けて他ASとBGPでやり取りしているかよくわかった。 ISPネットワークのリアルな現場を感じられる体験だった。また様々な分野の社員の方々とお会いしNTT Comの事業の幅広さとネットワークだけでも細かな多様な専門性を持つ部分を感じることが出来た。 想像以上に内容の濃いイベントでした。ネットワークの知識は授業で受けた程度だったのでとても勉強になりました。 他の参加者の皆さんもすごく知識があって、これからもっと頑張らないといけないと再認識できました。 ネットワークの勉強をする際の「実際に手を動かして学習する」部分の障壁、座学や本による学習では学びにくい実践的な部分をたくさん吸収していただけたようです。社員としても嬉しい限りです。 また、学生同士でのコミュニケーションも活発に行われており、同年代の参加者同士で互いに切磋琢磨する種になったり、参加者自らの刺激になったようでした。今後も継続してネットワークの分野を盛り上げていっていただけると嬉しいですね! まとめ この記事では、2021年11月20日に開催した、TechWorkshop「プロのネットワークエンジニアと学ぶ!ISPネットワークのつくりかた」について紹介しました。 ワークショップを通じて、よりネットワークに興味を持っていただけたり、よい刺激となっていたならば幸いです。 社員も学生の素直な疑問や鋭い質問に答えられるように邁進していきます。今後のイベントにもご期待ください。 さてこのTechWorkshopですが、来年1月頃にも同様の内容で開催予定なので、この記事を読んで興味を持った学生の方は、ぜひ参加申し込みをお願いします! また、「プロのネットワークエンジニアと学ぶ!ISPネットワークのつくりかた」以外にも複数のトピックで開催を予定しています。 NTT Comのイベント紹介のページ から興味のあるトピックに申し込みをしてみてください。皆様のご参加をお待ちしております! www.ntt.com *1 : NTT Com公式Twitter でもご案内します。 *2 : https://twitter.com/yoshiya_ito
アバター
この記事は NTTコミュニケーションズ Advent Calendar 2021 の11日目の記事です。 はじめに ヒューマンリソース部の岩瀬( @iwashi86 )です。普段は、全社の人材開発・組織開発を推進しており、業務の1つとして、"1on1" の全社展開をしております。 本記事では、その"1on1"の効果を高める具体的な技法を紹介いたします。アドベントカレンダーということで、ゆるめに書いてみます。 *1 NTT Com における1on1の目的とは? 技法を説明する前に、1on1の目的について説明します。技法はあくまで目的達成に向けたHowでしかないためです。 1on1の目的とは何でしょうか?1on1それ自体には、複数の目的が挙げられます。代表的なところで言えば次のようなものでしょうか。 信頼関係の構築 離職率の低下 メンバー育成 目標達成へ向けた支援 etc... どれが正解というものではなく、会社の置かれたコンテキストによって何を目的とするかが変わります。 NTTコミュニケーションズでは、1on1を "メンバーと1対1で行う、メンバーの成長支援を目的としたコミュニケーション" と定義しています。すなわち "成長支援" を主目的として置いているということです。 *2 1on1の効果を高める3技法 この1on1の効果を高めるために、具体的にどのような行動をすれば良いのでしょうか?様々な書籍やセミナーで1on1は解説されていますが、ここでは特に筆者が有効であると考える技法を紹介します。 *3 前提として、マネジメント目線の技法になっています。 1. 観察 -> メモ 1on1でチームメンバーの成長を促すためには、メンバー自身に内省を促し経験学習サイクルを回す必要があります。経験学習サイクルを簡単に表現すると "経験を振り返り、概念化・抽象化し、次の場でTryする" がループする学習モデルのことです。 *4 経験学習サイクルの起点となるのは"経験"です。経験はもちろん、1on1でチームメンバーから聞きだす方法もありますが、おすすめは普段から徹底的にチームメンバーを観察する方法です。たとえば、次のような方法があります。 オンラインでの打ち合わせにおける発言や表情を見ておく(この際は、他の参加者のリアクションも見ておく) SlackやTeamsでの発言に目を通す Pull RequestのReviewコメントを読む 徹底的に観察をすると、「あ、これはすごい!」「うーん、この発言はどうだろう」など様々な気付きがあるはずです。この気付きを忘れないうちに、チームメンバーごとのドキュメントにメモしておきます。(メモしないとすぐに忘れます…) メモした内容は、次の1on1でフィードバックします。具体的には「一昨日のミーティングでの、XXという発言は素晴らしかったね。ミーティングのゴールに一気に近づいたと思います。」というように、その行動と起きた結果について伝えます。そして次の問いを投げかけます。 「あの発言は、どういう考えから生まれたのでしょうか?」 と、良かった要因を掘り起こしにいきます。経験学習サイクルでいう "振り返り -> 概念化・抽象化" をチームメンバー本人に自ら振り返ってもらうようにします。上手く抽象化できれば、次回以降に再現性を高められるようになります。次のTryにつながりやすくなるということです。 この際、上手く概念化・抽象化できることもあれば、そうでないこともあります。マネージャー側から次の問いを投げかけて助け舟を出すこともありますが、個人的には短くとも1分待つ方法をおすすめします。最終的には、自身で自然と内省できると良いので、思考の癖をつけてもらうためです。(この待っている時間は、永遠と感じられることがあります笑) 突然ですが、となりのトトロという映画をご存知でしょうか?劇中の1つのシーンで、登場人物・キャラクターであるサツキ・メイ・トトロらが小さな畑をぐるぐる回って、埋めてある種に向かって「のびろー」と体を上下に伸ばすシーンがあります。 *5 筆者個人のイメージでは、概念化を待つ時間はこれにそっくりでチームメンバの前で、上手く概念化の芽がでてこーい!」と心の中で、常に応援している感じです。 2. 問いの手数を増やしておく 1on1ではチームメンバーのレベルや置かれている状況に応じて、コーチング・ティーチング・カウンセリングなど複数の手法を組み合わせることになります。特にコーチングスタンスで接する場合は、チームメンバー自身のありたい姿や、抱えている課題を気づいてもらうように、複数の問いを投げかけます。この問いも無数にありますが、次のような例があるでしょう。 目標設定 1年後、どんな状態になったらうれしいですか? ここをこのようにしたい、と思っていることはありますか? (目標がない場合は) いままでの経験で、一番うれしかったことはなんですか? 現状確認 いまどのような状況ですか? 100点満点だと何点ぐらいでしょうか? 目標に向けて、一番ブロッカーになっているのは何ですか? 手段検討 課題に対してどのような手段が考えられますか? 他にどのような方法がありますか? このような方法もありえますか? 次のアクション どの方法が最も良さそうでしょうか?(優先順位で1位はどれですか?) いつまでにできそうでしょうか? 私が手伝えるものはありますか? このような問いを、ぱっと思いつけるようにしておくと1on1の流れが非常にスムースになります。 *6 ダイアモンド社出版の書籍" 1兆ドルコーチ――シリコンバレーのレジェンド ビル・キャンベルの成功の教え "では、次のように記載されています。 ビルのリスニングは、たいてい山のような質問を伴った。ソクラテス式の対話だ。2016年の「ハーバード・ビジネス・レビュー」誌の論文によれば、こうした質問の姿勢は、すぐれた聞き手になるために欠かせないという。 山のような質問をするためには、経験豊富でない限り、事前に質問の引き出しを持っていた方が良いと筆者は考えています。では、どのようにして問いの手数を増やせば良いのだろうか、と思われるかもしれません。質問・問いに関する書籍が多く出版されていますので、2-3冊ほど手にとってメモしておくのが良いと筆者は考えています。 強いて1冊をあげるとすれば翔泳社から出版されている " 対人援助の現場で使える 質問する技術 便利帖 " がおすすめです。1on1特化の書籍ではありませんが、体系的に質問が整理されており、1on1へ応用可能なものばかりです。 3. マネージャー自身も内省する ここまで、チームメンバー自身の経験学習サイクルを回すことで、チームメンバーの成長支援を促す流れで説明してきました。もちろん、1on1はチームメンバーのための時間ではありますが、そこから成長するのはチームメンバーのみではありません。マネージャー自身も成長できます。 そこで、"1on1"という経験を起点として、次の内容を自分の中で振り返りします。 1on1で上手くいったこと、いかなかったことは何だろうか? なぜ上手くいった、いかなかったのか? どうすればもっと上手くできただろうか? 優先順位の高い方法はどれだろうか? と、まさにここまで紹介してきた2つの技法を自らに適用するわけです。さらに上位のマネージャが、経験から学びを引き出してくれる機会もあると思いますが、その機会を待たねばならないわけではありません。マネージャ自身も内省することで、常に成長することで、チームメンバとお互いに成長する関係が望ましいと、筆者は考えています。その意味で、1on1の最後にチームメンバーに対して「(1on1に限らず)もっとこうすればよくなる、ということはありますか?」と問いかけるのも有効だと考えています。 おわりに ここまで、1on1の3技法を紹介してきました。1つでも参考になる点があれば幸いです。 それでは、明日の記事もお楽しみに! *1 : 技術ブログですが、1on1の"技法"ということでご勘弁を! *2 : もちろん全ての1on1の目的を、成長支援に限定しているわけではありません。1on1を行うメンバーの置かれた状況によって、目的は変化します。 *3 : 会社の公式見解ではなく、筆者自身の考えが強く出ています。 *4 : 正確には Concrete Experience -> Reflective Observation -> Abstract Conceptualization -> Active Experimentation です。 *5 : BGMで、風の通り道が流れているシーン *6 : 筆者は、個人用のカンペを持っています。なお、コーチングを学んだ方ならピンとくるかもしれませんが、記事の質問例は GROWモデル に沿った流れです。
アバター
この記事は NTTコミュニケーションズ Advent Calendar 2021 の10日目の記事です。 はじめに こんにちは。イノベーションセンターテクノロジー部門の西野と申します。サイバー脅威インテリジェンス(CTI)のさらなる有効活用のため、この1年間サービス化に向けた技術開発を主導してきました。この経験を踏まえ、馴染みのない方にもわかりやすく企業とサイバー脅威インテリジェンスの実際についてお伝えしたいと思います。 サイバー脅威インテリジェンス(CTI)とは サイバー脅威インテリジェンス(CTI = Cyber Threat Intelligence)とは、「サイバー攻撃に関する情報を収集して整理・分析したもの(IOC等を含む)」を指します。会社や団体によって定義に幅はありますが、実用上はこの理解で問題ありません。 一般的に、サイバー脅威インテリジェンスは「サイバー攻撃の緩和や防御」を目的として利用されます。そのため実用上は、サイバー脅威インテリジェンスはIOCを含むことが極めて重要です。Indicators of compromise (IOC)とは、IP・ドメイン・URL・ファイルハッシュ等のサイバー攻撃にみられる直接的な痕跡を示す概念です。IOCが含まれることによって、セキュリティ製品との連携、他のサイバー攻撃と結びつけた分析、該当の検体解析等によるさらなる情報の収集が可能になります。 以降は、サイバー脅威インテリジェンスを「CTI」と表記します。また、CTIの分類にはStrategic/Tactical/Operationalと呼ばれる分類がありますが、本記事ではセキュリティ運用の現場で主に利用されるTactical/Operationalと呼ばれるCTIを対象にします。 CTIの利用目的 Bouwmanら(2020) 1 によって、CTIの利用目的に関する調査が行われました。14人の「CTIの専門家」へのインタビューをまとめたものが図1になります。 図1:CTIの利用目的 利用目的に関して、回答者が言及した割合を以下に示します。 ネットワーク上での検知 (93%) セキュリティ状況の把握 (64%) SOCの優先順位付け (50%) CISO等によるビジネス的な意思決定 (36%) 自分たちが保有しているCTIへの付加情報 (36%) ユーザーへの注意喚起 (29%) 脅威ハンティング (29%) システムの脆弱性管理や機能改修 (21%) 金融詐欺の低減 (14%) このインタビューの結果はCTIの主な利用目的(50%以上)が 「ネットワーク上での検知」「セキュリティ状況の把握」「SOCの優先順位付け」 であると示しています。ネットワーク上での検知やセキュリティ状況の把握にはIOCを利用するため、「CTIがIOCを含むべきだ」と主張する考えは実用的に妥当だとわかります。 企業におけるCTI活用の実際 先ほどの結果は「CTIの専門家」に対するインタビューである点に注意が必要です。実際、私たちが日本国内のいくつかのセキュリティ運用者に対してインタビューをした結果、「CTIに興味はあるが利用はしていない」「CTIを収集しているが上手く利用できていない」「CTIを上手く利用できているか自信がない」と話す言葉を耳にしました。 インタビューをした範囲では、CTIを上手く利用できている日本企業はあまり多くないようです。 また、利用している組織や研究者においても費用対効果の測定やCTIプロバイダーの選定など、さまざまな問題を抱えていることが判明しました。 今回の記事では、「サイバー脅威インテリジェンスの処方箋」として企業におけるCTI活用を CTIの性質 セキュリティ投資 これら2つの側面からみていきたいと思います。 CTIの性質 複数の文献 2   3 をまとめると、CTIで重視される性質は主に4つ(図2)あることが分かります。 意思決定の容易さ (Actionable) 速報性 (Timely) 正確性 (Accuracy) 消費者目線 (Audience-focused) Recorded Future社では「正確性」と「消費者目線」の性質をまとめて「Clear」と表現しています。 図2:良いCTIの特徴 これらの性質が優れているときCTIは「良いCTIである」といわれますが、この中には「客観的に評価できる性質(Timely, Accuracy)」と「利用者にしか評価できない性質(Actionable, Audience-focused)」の両方が含まれていることがわかります。 もし貴方が「CTIを収集しているが上手く利用できていない」と感じているのであれば、CTIを配布している団体や企業に「誰にどう使ってもらうことを想定しているのか?」と聞いてみることが大切です。期待した回答がもらえない場合でも、現状を伝えることが今後配布されるCTIの改善に繋がります。ActionableやAudience-focusedはCTIの利用を容易にする重要な性質ですが、その改善にはフィードバックが欠かせません。CTIの活用は「生産者と消費者が一体となって行う活動である」と改めて認識することが重要です。 TimelyやAccuracyの観点では、複数の情報ソースの比較や、マルウェア解析、パケット解析を用いた情報の裏付けが評価のために役立ちます。CTIの利用と評価は表裏一体の活動です。絶対に必要なスキルとまでは言えませんが、企業におけるCTI活用を考える担当者は 「OSINT」「マルウェア解析」「パケット解析」 に関するスキルの保有者を優先して採用するべきです。 セキュリティ投資 個人/組織におけるサイバーセキュリティのアクションや投資を議論するセキュリティモデルとしては、Lee(2015) 4 によって提唱された「The Sliding Scale of Cyber Security」(図3)があります。これを利用することで「どのカテゴリにおいてCTIへの投資が適切なのか」を前後のカテゴリと比較しながら判断できます。 図3:The Sliding Scale of Cyber Security このセキュリティモデルは連続する5つのカテゴリから構成されます。 Architecture … セキュリティを考慮した、システムの計画・構築・維持 Passive Defense … 脅威に対抗するための自動化されたシステムをアーキテクチャに追加 Active Defense … アナリストによるネットワーク内部の能動的な分析を実施 Intelligence … インテリジェンスサイクルに基づく能動的なセキュリティオペレーションを実施 Offense … アクティブ防衛(hack-back)や法的例外行動の実施 このモデルにおいてセキュリティ投資は、投資カテゴリを左から右へスライドさせる戦略をとります。具体的には「まずアーキテクチャに投資して優れたセキュリティ基盤を作り、次にパッシブ・ディフェンスに投資してシステムを保護し、それでも防げないサイバー攻撃をアクティブ・ディフェンスへの投資で対処する」と道筋を描くことができます。 CTI上のIOCの自動投入なども考えられますが、CTIの利用は主に「Active Defense」以上のカテゴリを対象としたセキュリティ投資(図4)であるとLee(2015)は主張しています。つまり「Architecture」と「Passive Defense」に対して十分な投資が終わったあとでなければ、CTIの利用によるセキュリティ強化の実感は困難です。 CTIの利用を開始する一例として、EDR製品の導入やログ収集基盤の構築が終わった後のタイミングが考えられます。 図4:CTIの利用と組織のカテゴリ またセキュリティ投資では、タイミングだけではなく費用対効果も重要なポイントです。しかし、CTIは単純な利用率による費用対効果の測定ができません。 一例として「水の入った袋」を想像してください。丈夫な袋は水を安全に運べます。袋を破れにくくする工夫も重要です。また、袋が破れていないか確認する工程も必要でしょう。袋に穴が空けば応急処置も大事です。けれど、応急処置ばかりで対処された袋を喜んで持ち帰るお客様はいません。 CTIの利用にも同様のことが言えます。「The Sliding Scale of Cyber Security」のモデルにおいて、セキュリティ強化の大枠は「Architecture」「Passive Defense」への投資で決まりますが、これは検査や応急処置が不要だと主張するものではありません。何よりセキュリティ運用の入れ替えは袋の交換ほど簡単ではなく、サイバー攻撃の被害も袋に空いた穴ほどわかりやすいものとは限りません。 とはいえ、「The Sliding Scale of Cyber Security」で定義されるカテゴリの連続性は、CTIの投資において非常に不可解な状況をもたらします。セキュリティ強化の大枠が強固になればなるほど、収集するCTIが増えれば増えるほど「CTIがほとんど利用されていない状況」が起こりやすくなるからです。一般的なセキュリティ強化では、利用率が低ければ費用対効果が悪いとみなされます。しかし前述の理由により、CTIの利用率は費用対効果の測定に不適切な場合があります。 サイバーリスクベースでの評価など様々な方法で費用対効果の測定は試みられていますが、現在のところCTIの費用対効果の測定に関する業界の標準はありません。 費用対効果がわからなくてもCTIを使う理由 企業におけるCTI活用は費用対効果の不明瞭な状態が一般的です。しかし、それでもCTIが利用される大きな理由は「現実のサイバー攻撃から身を守るには、現実のサイバー攻撃をベースに防御を考えるしかない」からです。 「 MITRE ATT&CK 」と呼ばれるサイバー攻撃の戦術やテクニックの分類手法なども用いつつ、企業は「実際のサイバー脅威」の観点から、自社のセキュリティ対策を継続的に見直し続ける必要があります。 ただ、CTIの分野は理想論やフォーマットばかりが先行している部分も多く、 MISP の習熟や RAWデータからATT&CKへのマッピング など運用上はかなりの困難を伴います。また、CTIの共有やフィードバックの運用に関しても様々な課題が存在し、CTIの共有戦略は学術的にも未解決な問題が山積みの状況です。SOCの現場、リサーチャー、コンサルタントの間でもCTIに関する期待感や運用コストへの考え方は三者三様であり、企業におけるCTI活用はまだまだ過渡期にあることを実感した一年でした。 まとめ CTIは非常に不明瞭な概念で、ほとんど利用されないこともある上に費用対効果も良くわかっていません。しかし、現実のサイバー攻撃をベースにしたセキュリティ対策にはCTIの利用が不可欠です。CTIは主に「ネットワーク上での検知」「セキュリティ状況の把握」「SOCの優先順位付け」のために利用されます。また、CTIの活用は生産者と消費者の協力が重要で、4つの特徴を満たす「良いCTI」を配布してもらうには消費者からのフィードバックが不可欠です。CTIの利用・評価には高いスキルをもつセキュリティエンジニアの配置が重要です。CTIの利用を開始する適切なタイミングの一例はEDR製品の導入やログ収集基盤の構築が終わった後のタイミングで、システムの保護や優れたセキュリティ基盤がない状態でのCTI投資はあまり意味がありません。 企業におけるCTI活用では、ステークホルダーの間で最低限これら5点に関して合意が必要です。 「対処すべき実際の脅威」の決定にCTIを利用する 自身の環境へ「アクション」を起こすためにCTIを収集する 良いCTIの特徴を共有しフィードバックの仕組みを作る 企業がCTIへ投資する適切なタイミングを理解する CTIの費用対効果をCTIの利用率で測定しない おわりに CTIの利用を手放しで推奨することは正直できません。CTIは本来、収集と消費だけでなく生成と配布も含めた一連のサイクルを考える必要があります。しかし、想定されている理想的な運用プロセスは一般的なセキュリティ担当者には重すぎます。また現実のCTIは品質が低いものも多く、CTIの消費にはサニタイジングのプロセスが必ず必要になります。 ただ、それでもなおCTIの利用を各企業が検討する理由は 「実際のサイバー脅威を理解して対抗できる唯一の手段」 だからです。 そして何より、CTI利用者の共通点である「現実のサイバー攻撃を理解する能力とそのモチベーション」はセキュリティ対策に極めて有益な資質です。社内におけるCTI利用者の存在は、 セキュリティシアター とよばれる「なんちゃってセキュリティ」に対抗する強力な予防措置となるでしょう。 明日は、Iwaseさんの「1on1の効果を高める3つの技法」です。お楽しみに! Xander Bouwman et al., 2020. A different cup of TI? The added value of commercial threat intelligence. USENIX Security Symposium 2020: 433-450 ↩ Scott J. Roberts and Rebekah Brown. 2017. Intelligence-Driven Incident Response: Outwitting the Adversary. O'Reilly Media. ↩ Christopher Ahlberg et al., 2020. The Security Intelligence Handbook Third Edition. CyberEdge Group, LLC ↩ Robert M. Lee. 2015. The Sliding Scale of Cyber Security. SANS Information Security White Papers. ↩
アバター
これは NTT Communications Advent Calendar 2021 1 9日目の記事です。 こんにちは、イノベーションセンター RedTeamプロジェクトの久保・山本です。 今回はSafe Mode Boot 2 という攻撃テクニックを私達がMITRE ATT&CKにContributionした話をします。 MITRE ATT&CK Contributionの経緯に先立ち、まずはContribution先であるMITRE ATT&CKを説明します。 MITRE ATT&CKとは MITRE ATT&CK 3 とは、攻撃者の行動を戦術や戦法から分類したナレッジベースです。これはセキュリティ界隈では代表的なフレームワークの1つであり、MITRE社により運営されています。MITRE社はCVE(共通脆弱性識別子) 4 を管理していることでも知られる米国の非営利組織です。 MITRE ATT&CKの利用シーン 私達RedTeamプロジェクトは、攻撃者視点でセキュリティ対策の有効性を評価する技術開発や、組織を横断した業務支援活動をしています。その中でMITRE ATT&CKを度々利用します。例えば、技術開発で各メンバーが取り組む小さなテーマ選定や成果を可視化する際に、先のMITRE ATT&CKのフレームワークを利用しています。また社内やお客様へサイバー攻撃演習を提供し、その評価結果を報告する際にもMITRE ATT&CKを利用します。 セキュリティ業界においてもMITRE ATT&CKはセキュリティ対策の評価のフレームワークとして広く利用されています。セキュリティ製品のアラートが指す攻撃テクニックにMITRE ATT&CKのテクニック番号が記載されることがあります。またMITRE ATT&CKを用いたセキュリティの評価サービスもあります。 Safe Mode Boot Safe Mode Bootとは WindowsにはPCに問題が発生したときに使用する診断用の起動モードとしてセーフモード 5 が用意されています。 セーフモードでは限られたドライバーやサービスを使用してWindowsを起動します。 問題が発生したPCをセーフモードで起動することで、トラブルシュートが可能となり、問題の原因の絞り込みや解決に役立ちます。 一方で、攻撃者はこのWindowsのセーフモードを悪用して、エンドポイントのセキュリティ機能を無効化できます。上述の通りセーフモードでは限られたドライバーとサービスでWindowsを起動するため、AV/EDRなどのエンドポイントセキュリティが起動しない場合があります。攻撃者はこの特性を利用してDefense Evasion 6 を実現しその後の攻撃シナリオを達成できるようになります。 手法を発見した経緯 とある案件にてTLPT 7 のシナリオを遂行するにあたって、Windows上のエンドポイントセキュリティを無効化する必要がありました。 先のMITREでいうところのImpair Defenses: Disable or Modify Tools 8 を達成する必要がありました。 これを実現する一般的な方法としては、以下の2つのアプローチがあります。 エンドポイントセキュリティのプロセス/サービスを止める エンドポイントサービスのサービスレジストリを削除/変更して無効化する しかし、どちらもエンドポイントセキュリティによって妨げられる、または検知されるという問題を抱えていました。例えばサービスレジストリを書き換えて無効化しようとすると検知される恐れがありました。 達成したいこととその手法は明確にあるものの、「エンドポイントセキュリティを無効化するためにはエンドポイントセキュリティを無効化する必要がある」という堂々巡りの状態に陥りました。 半ば諦めかけていたところセーフモードからBootしてAVのSymantec Endpoint Protection(SEP)をDisableする記事 9 を発見し、これを攻撃者視点で利用すれば今回の目的を達成できるのではないかと考えました。 実際に以下のロジックを構築し、目的としていたエンドポイントセキュリティの無効化に成功しました。 Impair Defenses: Safe Mode Boot 目的:Exploitによる攻撃/サービスレジストリの書き換えを検知させないため、一時的にエンドポイントセキュリティを無効化 手段:攻撃対象のPCをセーフモードからBootする Exploitation for Privilege Escalation 目的:サービスレジストリ書き換えのための管理者権限の取得 手段:Exploitによりローカルの権限昇格の脆弱性を突き、一般ユーザからSYSTEMに権限昇格する Impair Defenses: Disable or Modify Tools 目的:エンドポイントセキュリティの恒久的な無効化 手段:サービスレジストリを書き換えてエンドポイントセキュリティを無効化する MITRE ATT&CK Contribution Safe Mode Boot が当時のMITRE ATT&CKになかったため、私達の間でContributionの可能性が浮上しました。以下では、実際にContributionするまでの過程を共有します。 MITRE ATT&CK Contributeの条件 ATT&CKの記載 10 によると、新たなテクニックのContributeの条件は2つあるようです。 テクニックが現時点で公開されているMITRE ATT&CKにないこと テクニックが攻擊者により実際に利用されていること 前者、当時のATT&CKにSafe Mode Bootの記述がないことは既に確認していました。後者のSafe Mode Bootが攻撃者により利用された実例があるかを調査すると、実際にREvil 11 やSnatch 12 、MedusaLocker 13 などの複数のランサムウェアで実例があることを確認できました。 投稿から採用までのタイムライン 条件を満たしていることが確認できたので、約1日で内容を整理してMITRE ATT&CKへ投稿しました。約1ヶ月後に一度返信があったものの、その後約5ヶ月間は音信不通な状態が続きました。もし採用されるのならば、次のバージョンアップと考え、待ちました。そして2021年10月22日のバージョン10の公開と同時に、Safe Mode Bootの採用が明らかとなりました。 Contributionの困難性、希少性、そして価値 今回のContributionの困難性を客観的に示すものはないのですが、希少性はありそうです。Contributorは全世界で308名(内、企業名も含む)で、日本人は私達含めて10名です(MITRE ATT&CK v10公開時点) MITRE ATT&CKは先に触れたようにセキュリティ業界で広く活用されています。今回のSafe Mode BootがMITRE ATT&CKを通じて全世界で参照されることで、世の中のセキュリティ対策が少し高度化する。これがContributionの価値だと私達は考えています。 最後に 私達がTLPTで利用したSafe Mode Bootという手法がMITRE ATT&CK v10でTechinquesの1つとして採用されました。今回のATT&CKへのContributionを通じて世の中のセキュリティ高度化に少し貢献できたと思っています。今後、セキュリティ製品のアラートなどでSafe Mode Bootの表記が記載されることを期待しています。 明日もセキュリティ関係の内容をお送りいたします、お楽しみに。 https://adventar.org/calendars/6680 ↩ https://attack.mitre.org/techniques/T1562/009/ ↩ https://attack.mitre.org/ ↩ https://www.ipa.go.jp/security/vuln/CVE.html ↩ https://support.microsoft.com/en-us/windows/start-your-pc-in-safe-mode-in-windows-92c27cff-db89-8644-1ce4-b3e5e56fe234 ↩ https://attack.mitre.org/tactics/TA0005/ ↩ TLPT(Threat Led Penetration Test)とは、攻撃シナリオにもとづいて疑似的な攻撃をすることで、セキュリティ対策状況を評価する手法です。 ↩ https://attack.mitre.org/techniques/T1562/001/ ↩ https://www.alitajran.com/disable-symantec-endpoint-protection-sep/ ↩ https://attack.mitre.org/resources/contribute/ ↩ https://www.bleepingcomputer.com/news/security/revil-ransomware-has-a-new-windows-safe-mode-encryption-mode/ ↩ https://news.sophos.com/en-us/2019/12/09/snatch-ransomware-reboots-pcs-into-safe-mode-to-bypass-protection/ ↩ https://www.cybereason.com/blog/medusalocker-ransomware ↩
アバター
この記事は、 NTT Communications Advent Calendar 2021 7日目の記事です。 はじめに はじめまして!PS本部DPS部門の福島です。 コンテンツデリバリーネットワークというサービスのセールスエンジニアをやっています。 今回はアドベントカレンダー企画と言うことで、Microsoft Power AutomateとTeamsを使って確認事項や締め切り通知を自動化したお話をします。 エンジニアブログとしては、まだこういったRPA(Robotic Process Automation)のお話は未投稿でした。 そこで、コードいらずで誰でも実践しやすいMicrosoft Power Automateで作る RPA が初回記事としてピッタリと思って今回投稿しています。 Microsoft Power Automateとは Microsoft Power Automateは公式サイト( https://powerautomate.microsoft.com/ja-jp/ ) にあるように、確認のプロセスや反復作業などを自動化するためのツールになります。 各デスクトップ上で動作できる無料の Power Automate Desktop と従来のクラウド型サービスである Power Automate があり、前者は各クライアント端末上での自動化に、後者はチーム内のプロセス自動化に、それぞれ長けていると思います。 それぞれアプリケーションも用意されていて、今回説明するPower Automateはブラウザ上でも操作できます。 Desktop版と違い、O365の利用が必要だったり、有償だったりという点はあるのですが、担当全体を巻き込んだプロセスの効率化を実現できます。 やりたかったこと 今回の目的=実現したいこととしては 「リストから締め切りの近い項目について、通知してくれるRPAの実現」 になります。 うちの担当では、検証機で導入しているツールのライセンスや、監視サービスなどを利用しています。 この利用料について社内で決裁を毎年取っていて、担当の週次定例で決裁期限の近いものがないか、みんなで確認をしています。 ただ、普段その決裁を対応している人がうっかり漏らすと、対応が遅れ、決裁期限を過ぎてしまうということになりかねません。 実際にはそういったうっかりはなかったものの、忘れていたせいで対応を開始したかった時期より遅れてしまうということは、時々起きていました。 そこで決裁について対応期限が近付いているものを、人で確認するのではなく、自動で通知してくれるようなRPAが組めないかと思い、手を出したのがPower Automateになります。 設計 流れの説明 作成に当たり、以下のような流れを想定しました。  ①エクセルの決裁リストを取り込む  ②取り込んだリストから期限の近づいているものを識別する  ③期限の近いものがあればTeamsで色んな人を巻き込んで通知する いくつか準備や段階は踏まなければいけませんが、結果的にこの流れで実現できました。 ①は定例で利用している既存のものがありましたので、そちらを流用することにしました。 ※画像はサンプルです。 ②はそのリストをPower Automateで取り込み、期限について何日前なのか確認する処理になります。 リストでは決裁の実際の期限と別に、その決裁の対応を開始すべき日程も入れてみました。 ③は期限確認した決裁について、近いものがあればTeamsで通知する処理です。 Teamsに投稿することでチームのみんなが見られるようにしています。 また、チームの複数名をメンションすることで、担当者が漏らしても周りの人が気づけるようにしてみました。 事前準備 実際に作成したときは、既に存在しているエクセルの決裁リストを利用しました。 今回はサンプルで用意した先ほどのエクセルを利用して、実際に作成しながら記事にしていきたいと思います。 まずは準備として、エクセルのリストをPower Automateが読み込める形にします。 目視で見やすいリストにするだけではだめで、エクセルの機能として「テーブル化」というものがあり、これを実行する必要があります。 テーブルにしたい範囲を選択して「挿入タブ」の「テーブル」を押すと、テーブルにできます。 これで準備完了です。 テーブルにしたセルを選択しているとき、「テーブルデザイン」というタブが現れるようになります。 後々使いやすいように、テーブルの名前を変えておいてもいいかもしれません。 Power Automateでのフロー作成 ここまでの準備が出来たら実際の設計に移ります。 Power Automateのメニュー画面で「作成」を選択すると、以下のようにフローを作成する画面になります。 今回作りたいのは定期的にリストを確認し、締め切りに近いものが無いか確認する仕組みなので、スケジュール済みクラウドフローで作成していきます。 今回のRPAは、最初の処理としては以下のようにしてみました。 定期実行してくれるトリガーが最初に入っていますので、このフローは設定した頻度で自動実行されるようになります。 その後、締め切り通知してほしいリストが入力されているエクセルファイルを読み込みます。 各処理の名称はわかりやすい形に変更できるのですが、後でどの機能かわかりにくくなるため、処理を挿入する際に表示される機能名は別途(機能名:~~)という形で併記しました。 その後の処理は以下のようになっています。 締切を確認するために現在時刻を「シリアル値」と呼ばれる形に変換しています。 また、この後計算で利用するために、変数の定義として「決裁開始日」「契約開始日」を準備し知恵ます。 コードの知識が不要、と言いつつ複雑な計算式は要求してしまうのですが・・・。 ここではざっくり、以下の計算式を使うと、現在時刻がPower Automate上で扱いやすい形になる、とだけ紹介しておきます。 「値」の入力欄をクリックしたあとに出てくる吹き出しメニューから、式のタブを選択することで入力できます。 div(sub(ticks(startOfDay(convertFromUtc(utcNow(), 'Tokyo Standard Time'))), ticks(startOfDay(convertFromUtc('1899-12-31T00:00:00Z','Tokyo Standard Time')))), 864000000000) Power Automateでは、エクセルから取得する日付を、シリアル値、と呼ばれる形式で管理します。 その対応をやりやすくするために、あらかじめ現在時刻のシリアル値を用意しています。 詳しいことが知りたい場合はGoogleなどで「Power Automate 現在時刻 シリアル値」と検索してみてください。 最後の「Teamsに投稿」の部分は省略表示になっていますが、ここまでがフローの全体像です。 最後のTeamsに投稿する処理については、以下のようにしています。 Apply to eachという、繰り返しの処理を行ってくれる機能を入れて、そこにエクセルから取得したデータ=valueを設定することで、リスト内の各項目に対して繰り返しの処理を実行してくれるようにしています。 ここも全部説明したいのですが、長くなってしまうので割愛しながら書くと  ①リストにデータが入っていること=空白でないことを確認し  ②現在確認している項目の、決裁開始日や契約開始日に応じて条件を設定し(条件、条件2、条件3)  ③合致した条件に応じたメッセージを投稿する という流れになっています。 ①と②の間に、念のため型判定の処理も入れています。 これはPower Automateにおいて、エクセルから取得される日付の形式が、通常はシリアル値なのですが、たまにMicrosoftが仕様変更して一時的にタイムスタンプ形式になっているということがあったためそのエラー対策になります。 条件、条件2、条件3 の中で実際にTeamsへメッセージが投稿されています。 やっていることは基本的に3つともすべて同じで、契約開始日や決裁開始日より何日前か、の日数ごとに条件を作成しています。 例として条件2の内部を紹介すると以下のようになっています。 条件2は、決裁期限=契約開始まで15日を切ったもの、を通知してくれる処理になります。 ※ちなみに 条件 では、1か月前の通知、条件3 では、遡及警告、をそれぞれ担ってます。 決裁開始日が今日の日付を過ぎており、契約開始日まで15日を切っている、ただし契約開始日はまだ先である、という3段階の条件文になっています。 これに合致したときの処理が以下のようになっています。 Teamsの対応するチャネルにメッセージ文を投稿しています。 決裁件名や、いつまでに対応が必要か、などがわかるような投稿文にしてみました。 また <at> </at> で組織ID(主にメールアドレス)を囲うことで、Teamsで使っている「メンション」の機能を利用できます。 これを使って、決裁の担当者のメールアドレスをエクセルに記録しておけば、担当者にメンションで通知させることができます。 実際に投稿されている様子が上の画像の通りになります。 担当者にメンションしつつ、チャネルにも投稿することで担当のメンバーが対応されているか確認しやすくなっています。 作ったうえでの効果 実際に作ってみて担当として効果があったかですが、先にも書いた通り、そもそも対応忘れによる遡及まで発展したケースはこれまでありませんでした。 ただ、対応開始遅れは年に1,2件発生していました。それがこの仕組みを導入してからは発生しなくなっています。 また、人の手で個別に確認する手間もなくなったので、年1回の決裁対応後にリストを更新さえしておけばよく、確認稼働もある程度低減できています。 決裁開始期限、契約期限、という一見ややこしい2つの日付を用意したおかげで、決裁ごとに通知タイミングも柔軟に設計できるようになりました。 これをわざわざ用意した理由は 「各決裁ごとに、対応を始めたい期間に差異があるから」 になります。 例えば海外とのやり取りが発生するような決裁では、見積りのやり取りなど、準備だけで時間がかかります。 そういった対応まで加味して、決裁開始期限、を設定しておけば、決裁開始期限になったときの通知で「あ、そろそろあの会社にメール送っておかないと」というふうに気づきやすくなると思います。 うちの担当においては、この仕組みを導入することで、対応漏れの防止や稼働効率化に一定の効果があったと考えています。 おわりに 新型コロナが日本では落ち着いてきているとはいえ、世界的な感染状況を考えると、リモートワークはまだまだ続けていきたいという方や、そのように指示している企業も多いかもしれません。 リモートワークは通勤などのストレスが無く、自分で働く環境を設計しやすい反面、最大の欠点として「他のメンバーとのコミュニケーションレスに陥りやすい」ということが挙げられると思います。 コミュニケーションレスが原因となって引き起こされる事象は多々あるかと思いますが、今回挙げたような事務対応においても、担当者がちゃんとやっているか、確認しづらくなるということもあるのではないでしょうか。 そんなとき、Teamsなどで担当全体に通知してくれる仕組みがあれば、事務対応の取りこぼしも起こりにくくできます。 また、コロナの感染状況に関わらず、今後は自動化やRPAの活用がどんどん進んでいくことと思います。 今回紹介したのはそれほど大きく稼働を削減するような内容ではありませんが、それでも一定の効果は出せています。 こうした事務作業のところから徐々に自動化を進めて行ければと考えていますし、今回の記事が少しでも皆様のお役に立てれば幸いです。 明日の記事もお楽しみに! 付録 ここからは今回使用した式の簡単な紹介等になりますので、読み飛ばしていただいても問題ありません。 また、あくまで簡易紹介ですので、詳細は別途Googleなどで調べてください。(社内の方は福島までご質問いただければ時間のある時に回答できるかもしれません) 型判定 最後のTeamsに投稿する処理の繰り返し文で、日付の型判定は以下の式で行いました。 タイムスタンプ:endsWith(string(items('Teamsに投稿(機能名:Apply_to_each)')?['契約開始日']), '.000Z') この式の動的部分は  items('Teamsに投稿(機能名:Apply_to_each)')?['契約開始日'])  になります。 これは「Teamsに投稿」と名付けた機能に設定された「value」から、リストの「契約開始日」を見る動的コンテンツになります。 「value」には一番最初に読み込んだエクセルのテーブルに入っているデータが格納されています。 この処理でエクセルから取得された契約開始日が、「000Z」で終わるタイムスタンプ形式になっていないか確認しており、この条件に合致すればタイムスタンプ形式で日付を取得してしまっています。 今のところ、タイムスタンプかシリアル値のいずれかが、Power Automateで利用されている日付表現方式なので、この式に合致しなければ高確率でシリアル値です。 シリアル値での処理を想定して作成されていますので、タイムスタンプで取得した場合は、シリアル値に以下の式で直します。 シリアル値への変換式:div(sub(ticks(items('Teamsに投稿(機能名:Apply_to_each)')?['契約開始日']), ticks(startOfDay(convertFromUtc('1899-12-31T00:00:00Z', 'Tokyo Standard Time')))), 864000000000) 動的コンテンツが入力できない時 動的なコンテンツ、では読み込んだリストの各項目を動的変数のような形で入力可能です。 しかし、場合によっては以下のようにコンテンツが出てきてくれないことがあります。 ちなみに動的コンテンツが出てくるときはこんな感じです。リストの項目が直接入力できるようになってます。 こういうときは、どこか別のところで該当の動的コンテンツや変数を入力していないか確認します。 入力していたら、該当のコンテンツをマウスオーバーしてみてください。 代替テキストとして、式の中身が出てくるはずです。 変数もこのようにどういう式を書けばそれが入力できるのかマウスオーバーすれば確認できます。 動的コンテンツが自動入力できない時や、動的コンテンツ・変数を利用した式を書きたい時には、マウスオーバーすることで、直接入力するための文字列を確認できますので試してみてください。
アバター
この記事は NTTコミュニケーションズ Advent Calendar 2021 5日目の記事です。 はじめに データプラットフォームサービス部の増田( @tomo_makes )です。 組織内に点在するデータを一つのプラットフォーム上で融合して利活用を加速する Smart Data Platform ラインナップの1つ、IoTプラットフォーム Things Cloud や、それを活用したソリューションのエンジニアリングマネージャをしています。 お客様企業のデジタルトランスフォーメーションに関する技術コンサルティングも一部行っています。 さて、タイトルの「グラフィックファシリテーション」を聞いて、どんなものを思い浮かべるでしょうか。 講演やパネルディスカッションなどを「魅せる」形で色鮮やかにまとめたものを想起されるかもしれません。しかし「魅せる」ほどのクオリティでなくとも、簡単な図解をカジュアルに描き・共有することは、リモートワーク下で伝えられる情報量を増やし生産性を上げます。 私はこの1年あまり、iPad・Apple Pencil・ Concepts (コンセプト) という無限キャンバス(縦横に描いただけ、描画領域を拡張できる)のドローイングアプリを組み合わせて使っています。手元のメモから始めて、ホワイトボードのようにビデオ会議で画面共有しながら描き、会議のファシリテーションや意識合わせに使うようになりました。 具体的には、以下のシーンで役に立っています。 ミーティング中のクイックな意識合わせに活用する 読書や仕事のメモで、自身の思考を深堀りしたり、共有することに活用する 複数の要素を含む検証・PoCやシステムトラブルを、探索的に解決する 新しい顧客や、新しいプロジェクトのミーティングで活用する チーム内のビジョンを合わせるために活用する ミーティング中のクイックな意識合わせ まず概念図を手描きし、すぐに見せられる手になじんだツールを持つことは、大きな効果を生みます。私の場合はそれがConceptsでした。 少し言葉では伝えづらいなという情報を、図解の力を借りて容易に伝えられます。逆に自分が文脈を理解できているか不安なら、自分の理解を図示し相手に見てもらえば、理解のずれをすぐ指摘してもらえます。 読書やドキュメント通読・共有時のメモ 特に技術書は、本を前後に行き来し、ある概念の実例をいくつか確認してはじめて理解できることが多くあります。目次よりさらに具体のキービジュアルを描き留めておくと、理解や思考の深堀りに役立ちます。こちらは及川卓也ほか著『 プロダクトマネジメントのすべて 』の部内輪読会で使ったメモです。 複数のステークホルダでのPoC要件の意識合わせ ステークホルダが3以上の場合、システム検証という具体的なプロジェクトであっても最初は空中戦になりがちです。それぞれの視点や詳細度で持ち寄られたドキュメントを、1つのキャンバス上に描き出すと、途端に議論が進みやすくなります。(一部マスクしています) 顧客やプロジェクトのミーティング 残念ながらこの場では具体例を出せませんが、ステークホルダ間で大仰な資料を作らず、メモでクイックに情報共有したり相談をする文化も醸成できます。それがプロジェクトの生産性を上げることにつながります。 業務で使っている中で「これどうやってるの」と聞かれたり、ツールを紹介すると愛用される方が多く、今後の紹介用に経験を棚卸ししてみました。読まれた方が、明日からの仕事にちょっと変化を持ち込む、周囲に紹介してみようか、と思うきっかけとなれば幸いです。 グラフィックファシリテーションの効果 (Aggregation - Reframing - Collaboration) 清水淳子著『 Graphic Recorder ―議論を可視化するグラフィックレコーディングの教科書 (2017) 』では、以下のステップと効果がまとめられています。ここでは「グラフィックレコーディング」という言葉が使われていますが、議論の「ファシリテーション」に使う「魅せる」だけでない議論の可視化についての理解が深まる本です。 (出典) 清水淳子著『 Graphic Recorder ―議論を可視化するグラフィックレコーディングの教科書 』 私自身の仕事に対応づけると、IoT・AI・それらを用いたデジタルトランスフォーメーション(IT活用によるビジネス変革)実現は、複数要素が相互に影響するシステムや定性的な事柄について、仮説を立て作用する・応答を観察する・全体像を徐々に明らかにする・最終的に最も効果が高いポイントを見出す・そこへ作用するアクションをするという探索的なプロセスです。ちょうど某RPGで、暗闇の中たいまつだけを頼りに限られた視野でダンジョンを歩き回る。通った場所を書きとめ徐々に地図を作る。それを頼りに出口や宝箱を探す、という感覚に近い気がします(例が古いですけど)。 特に顧客ヒアリング、プロジェクト初期は、予め準備されたテンプレートなどを埋めることのみで仕事が進むことはありません。そういった時にグラフィックファシリテーションを使います。 Aggregation 何を話すべきかがわかるようになる。話の全体像が見えることで、広い視野で必要な話をするようになる。 発散のフェーズです。経験上「事実や一次情報ベースで発散させきった(情報収集を幅広に行った)上で、そこへ考察を加え収束させること」が大事と感じます。そのため、お客様含む参加者から事実を集めるため、その場作りをし、まずファシリテータになります。そして全体像を知るために、適切に発散させ、すぐに関係しない事実も含め取り上げられているかに気を配ります。各参加者の声を拾い、ファシリテータがわからないものでも、一旦描いてみて、ニュアンスやプロットするべき場所があっているかを確認します。 Reframing 別の視点を発見して前向きになる。偏りを客観的に眺めることで、どういう方向に進めば目的地に着くか明確になる。 考察を加え収束させるフェーズです。一見バラバラに見えた内容を、ひとつのキャンバス上に配置することで、発見が始まります。RPGの比喩では、一旦歩き回ったダンジョンの地図を起こし全体像が見えると、「どうもこの(通路が通っていない不自然な)空間は怪しい。隠し扉や宝箱などがあるのでは。」などの嗅覚が働くことに似ています。 Collaboration 多様性として楽しめるようになる。お互いの考えや専門の違いを安心して共有し、積極的に協力ができる関係になる。 合意と記録、実行のフェーズです。ここまでがうまくいけば、すでに関係者で(真に)合意した実行内容が洗い出せています。あとは納得する分担を決め、実行することで、協力してゴールに近づきます。 リモートワーク下でこその効能 上記の本は、顔を合わせての会議シーンを想定しています。しかし私は、リモートワーク下ではさらに効果が高まるように感じています。 コミュニケーションの形態は、「時間」と「場所」という観点から整理すると、以下3種類が考えられます。 同じ時間、同じ場所 同じ時間、違う場所 違う時間、違う場所 リモートワークが進むと、単純に1つ目の「同じ時間、同じ場所」で起こるコミュニケーションが失われます。3つ目の「違う時間、違う場所」で使われてきた、「文章」や「時間をかけて作った図表」(パワーポイントなど)による伝達が増えます。それにはどうしても時間がかかり、カジュアルに交換できる「形」「位置」「図表」を使ったコミュニケーションは失われたままです。 2つ目の「同じ時間・違う場所」をうまく活用したいところです。「同じ時間、違う場所」でのコミュニケーションは、ビデオ会議でカメラをONにして表情など伝え、より「同じ時間、同じ場所」に近づけることがTipsのひとつとされます。手描きによるグラフィックファシリテーションは、「同じ時間、違う場所」なりに、リッチな視覚情報で会議参加者の目線を合わせ、発言を促進する効果もあります。 やってみよう 以下は、実際にコンセプトを用いて描いた導入ガイドです。 Conceptsの導入とそのTips 「 Concepts (コンセプト) 」を導入します。私自身はiPad・Apple Pencilとの組み合わせで使っていますが、Android版、Windows版もあるようです。まずは無料版で使用感を確認してみてください。アプリ自体はサブスクリプション課金がありますが、これは機能が豊富なデザイナーやアーティスト向けです。ビジネスユースでは無料版または買い切り版で十分でしょう。 使用方法は 公式のマニュアル や、オンライン上にいくつも存在するチュートリアル、ビデオ (一例として 【iPad】Concepts(コンセプト)アプリの紹介や使い方など | ENHANCE ) に譲ります。 以下に、便利だと感じる設定やTipsを列挙します。 設定 > ジェスチャーは以下の設定にする 2本指の「キャンバスの回転を有効にする」チェックを外す 長押しを「投げ輪」ツールに割り当てる 2本指タップを「元へ戻す」に割り当てる 最初は以下の設定で始める 倍率100%とし、グリッドを表示しておき文字を書く ブラシは「ペン 3pt」「ダイナミックペン 12pt」などを使う 描き込みが進んだら、ズームアウト、より太いペンを使うなどして、広いキャンバスを有効に使う 「投げ輪」ツールを使い、描いたものを移動したり、複製する 物理のホワイトボードと異なり、描く位置を間違えたな、と思ったらまとめて「移動」できる システム構成図などは、判断用に複数のパターンを描くときに「複製」が便利 Aggregation - Reframing - Collaborationで、適宜描き込む「レイヤ」「ブラシ」「色」を分ける 例えば、Aggregationには「ペン 3pt」を、Reframing - Collaborationには「ソフト鉛筆 10pt」をといった使い分けをする 「レイヤ」を分けておくと、その表示・非表示切り替えにより、ある時点まで議論を巻き戻したり、再度積み上げたりがやりやすくなる 描き方 Warm Up (Aggregationの準備) まず、 単位時間で共有される情報を最大にするための場作りが必要です。 マウンティングがなく、発言が許容されるポジティブな雰囲気を作りましょう。できれば冒頭に、アイスブレイクなどで全ての参加者が一度は発言した状態を作るとよいです。またファシリテータは、正直に知らないことは知らないと表明し聞き返すなど、謙虚な姿勢を崩さないようにします。描く人と場のファシリテータは同一人物でもよいし、分担することもできます。 Aggregation - 描いていく 再び、清水氏の書籍から引用します。グラフィックスといっても、おもむろに「絵」を描き始める必要はありません。箇条書きから始めて、必要な概念図を足していきます。エンジニアリングにおいては、システム構成図、データモデルなどを簡単に手描きで加えることもできます。 (出典) 清水淳子著『 Graphic Recorder ―議論を可視化するグラフィックレコーディングの教科書 』 描くもの (=議論の対象) の全体像が決まっている場合と、決まっていない場合があります。決まっていない場合は、描くことが周辺を発見することにつながります。 例えば、ある複雑な事象を解明するためにシステム構成図を描いた場合、そこに事実を書き込んでいくと、もともと着目した領域の外側に、真に議論したい内容があることがあります。構成図は「複製」を有効活用できます。 新規ビジネスの場合、ビジネスモデルキャンバスや、リーンキャンバス、バリュープロポジションキャンバスなどから始められますが、周辺に詳細を書き込みたい場合もあるでしょう。 慣れている方の場合、そのフェーズにおいて聞きたいことをちりばめていき、デジタルツールの強みを生かして、「複製」「移動」などで位置関係を整えながら、全体の地図を明らかにしていくこともできます。 課題に対応した図解の具体バリエーションや考え方は、以下の書籍が参考になります。 よりビジネス寄りのフレームワーク、抽象思考のトレーニング 平井孝志著『 本質思考 - MIT式課題設定&問題解決 』『 武器としての図で考える習慣 - 「抽象化思考」のレッスン 』 具体的な描き方のトレーニング 日高由美子著『 なんでも図解 - 絵心ゼロでもできる!爆速アウトプット術 』 久保田麻美著『 はじめてのグラフィックレコーディング 考えを図にする、会議を絵にする。 』 Reframing - まとめる 一定描き出している間に、位置関係などを修正していきます。事実に対して考察を加え、グルーピングし、それらの依存関係を矢印などを使い記述します。前述のTipsの通り、レイヤ分けをしておくと、後で切り替えられて便利です。 冒頭の『プロダクトマネジメントのすべて』に、以下のReframingレイヤを加えてみます。 するとこのようになります。一部バラバラに捉えられたもののつながり・構造が見えてきませんか? Collaboration - 合意と記録、実行 終わったらすぐ、エクスポートメニューから、Slackその他のコミュニケーションチャンネルへ共有できます。カジュアルな意識合わせなどはこれを議事に代えられます。 ひとつ面白い点として、Conceptsはベクターグラフィックス (SVG) 形式でのエクスポートに対応しています。描いたオブジェクトは、ひとつひとつ時系列で記録されています。外部のソフトウェアを使うと、描いた順番に再生することなどもできます。 その他の方法 今回は「 Concepts (コンセプト) 」による手描きを中心に紹介しました。「絵やイラストは得意ではないので自分で手描きをするのはちょっと… 」という方は、 Miro などを使うと付箋やオブジェクトを配置・構成しながら議論を進められ、同様の効果が得られます。 また、Conceptsは共同編集には対応していません。付箋で良ければ Miro など、手描きでの共同編集を行いたい場合はGoogle Jamboard(ただし、無限キャンバスではない)などの活用が考えられます。 例えば、 NTT Communications Digital Showcase 2021 のDXセミナー『 現場主導で始める!映像やIoTを活用したスモールスタートで実現するDX (DX-2) 』では、もともとConceptsを使ったグラフィックファシリテーションから、Miroを用いてより多くのメンバが実施できる形態としたワークショップ例を紹介しています。 おわりに グラフィックファシリテーションや、紹介したツールが、明日からの仕事にちょっと変化を持ち込むきっかけとなれば嬉しいです。 明日の記事も、ぜひお楽しみにお待ちください!
アバター
この記事は、 NTT Communications Advent Calendar 2021 4日目の記事です。 こんにちは、イノベーションセンターでSREとして働いている昔農( @TAR_O_RIN )です。主にNTT Comのソフトウェアライフサイクルの改善への取り組みやアーキテクトに関わる仕事をしております。本日は サービスメッシュ を題材に,その中で用いられるEnvoyの活用パターンを手を動かして理解するお話をさせていただきます。 また,昨年までのアドベントカレンダー記事もご興味があればご覧ください! 2020年 How do you like k3s ? - CoreDNSで作るお家DNS Cacheコンテナ 2019年 TektonでCI/CDパイプラインを手の内化しよう 2018年 DevOpsってこんな仕事!考え方とスキルセットのまとめ 2017年 DockerのnetworkをCalicoでBGP接続する tl;dr サービスメッシュは便利だけど,本当に素敵なのはそのデータプレーンを支える透過型プロキシだと思っています! The network should be transparent to applications / ネットワークはアプリケーションにとって透過的であるべき 実践的に組み上げられたサービスメッシュを使うのも良いけれど,透過型プロキシの1つである Envoy を利用してその面白さを知ろう Double proxy with mTLS encryption というユースケースをインターネット越しで利用してみます サービスメッシュにおけるEnvoyの役割 サービスメッシュとは 今回の記事ではサービスメッシュ自体の説明を詳しくは実施しませんが,どんな問題を解決しようとしている仕組みなのか簡単にご紹介します。基本的には複数のマイクロサービスを用いて構築されたシステムにおいて,マイクロサービス間の通信をスマートにハンドリングするための概念及びそのソフトウェアのことです。 下記の図はサービスメッシュ実装の1つであるIstioのアーキテクチャ図です。ここで概念として理解しておきたいのは,サービスメッシュはマイクロサービスのトラフィックを転送する プロキシ(データプレーン) と,それらを管理する コントローラ(コントローラプレーン) に役割が分かれていることです。この概念が サービスメッシュ であり,その実装が Istio などの具体的な製品やOSSになります。 少し本題と外れますがネットワークの業界にある程度いると, データプレーン と コントローラプレーン を分離するという考え方はSDNの概念が生まれた頃からよく登場していたと記憶しています。私は学生時代にOpenFlowというSDNの1つを研究題材にしていたのですが,初めてサービスメッシュを見た時はよく似たアーキテクチャだなと感じたことを覚えています。 引用元: https://istio.io/latest/docs/ops/deployment/architecture/ Envoyとは 本日の主役でございます。詳細なお話は 公式ページ に譲ることとして,下記の図で赤く示している Proxyの正体がEnvoy となります。図中の緑の矢印から見て取れるように,数多くの通信を扱うコンポーネントであることが分かるかと思います。特にProxy間の通信を通してService AとService Bを繋いでいるような構成はサービスメッシュの文脈では多く登場します。 本日,具体的に深堀りしていくのはこのプロキシ間で通信するパターンをコントローラなしで作り上げてみて,実際にプロキシを介して通信が行えるのか,プロキシ間はどのように接続されているのか,をおうちのネットワークを活用して動かしてみましょう。 引用元: https://istio.io/latest/docs/ops/deployment/architecture/ インターネット越しでDouble proxyをやってみる Double Proxyについて Double proxyとはサービスメッシュとしてよく出てくるEnvoyの利用パターンの1つで,下記のようにアプリケーション間でEnvoyをサイドカーとして組み込むことでマイクロサービス間や下記の図のようなアプリケーションとミドルウェアとの間を接続する構成です。この構成を作ることで FlaskApp はあたかも隣に PostgreSQL がいるように利用できます。 今回はサンプル例として1対1の接続例となりますが,実際にサービスメッシュとして利用する際はもっと複雑な構成になることが多いでしょう。ミドルウェアへの接続として応用している実例としてはGCPにおける Cloud SQL Auth proxy などが挙げられるでしょう。 mTLSによる暗号化と認証 もう1つ重要なポイントとしてプロキシ間の通信の暗号化と通信相手の認証があります。mTLSでは通信の暗号化にはTLSを用い,相互のプロキシ間の認証には証明書を用います。これにより今日のデモのようにインターネットを超えて通信を起こすような場合でも安全に通信を提供できます。mTLSについてもっと詳しく調べて見たい方は こちらのサイト が参考になるかと思います。 自宅とパブリッククラウドをEnvoyで接続してみよう それでは実際に動作を確認する構成を作ってみましょう!下記の図のようにクライアント,サーバの双方でDockerコンテナを用いてプロキシを立てます。また,プロキシ間の通信にはIPv6を利用することとしました。この記事を読んでいる皆さん向け簡単に流れをご紹介します。 環境 サーバ OS: Ubuntu 20.04.3 LTS Kernel Version: 5.4.0-90-generic Docker Server Version: 20.10.7 Envoy: v1.20.1 ※ 諸般の事情でネットワーク帯域幅が100Mbps制限。 クライアント OS: Ubuntu 18.04.3 LTS Kernel Version: 5.4.0-80-generic Docker Server Version: 20.10.7 Envoy: v1.20.1 構築の流れ Envoyから 素敵な公式手順書 があるので, 基本的にはそれに沿って進めますが一部詰まったところや気になったところを補足していきます。 mTLSに利用する証明書を準備する(これが少し面倒ですが,手順書に沿って進めれば大丈夫です) 認証局(certificate authority)を作成 する 接続に利用するドメイン向けの鍵を生成する 証明書署名要求(CSR)を生成する 証明書に署名する Envoy Configの準備については 公式リポジトリのサンプルコード を参考に編集していきます Client側の参考Envoy Configを下記に示します。 static_resources: listeners: - name: iperf_listener address: socket_address: address: 0.0.0.0 port_value: 12345 # プロキシで通信を受け付けるポート番号なので任意で良い filter_chains: - filters: - name: envoy.filters.network.tcp_proxy typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy stat_prefix: iperf_tcp cluster: iperf_cluster clusters: - name: iperf_cluster type: STRICT_DNS connect_timeout: 10s load_assignment: cluster_name: iperf_cluster endpoints: - lb_endpoints: - endpoint: address: socket_address: address: <ご自身で取得したサブドメイン>.i.open.ad.jp # IPv6のアドレスを直に書くとエラーになるようなのでFQDNを書く port_value: 3022 # 対向のEnvoyとの通信に利用するポートなので任意で良い transport_socket: name: envoy.transport_sockets.tls typed_config: "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext common_tls_context: tls_certificates: - certificate_chain: filename: certs/clientcert.pem # 証明書をEnvoyコンテナにマウントすることを忘れずに private_key: filename: certs/clientkey.pem # 秘密鍵をEnvoyコンテナにマウントすることを忘れずに validation_context: match_subject_alt_names: - exact: edge-proxy-server01.sekinet.example # ご自身で利用するサブジェクトALT名を入れてください trusted_ca: filename: certs/cacert.pem # CA CertsをEnvoyコンテナにマウントすることを忘れずに サーバ側の参考Envoy Configを下記に示します。 static_resources: listeners: - name: iperf_server_listener address: socket_address: address: <EnvoyでLISTENするアドレス> # ここはIPv6アドレスを入れても大丈夫 port_value: 3022 # 対向のEnvoyとの通信に利用するポートなので任意で良い listener_filters: - name: "envoy.filters.listener.tls_inspector" filter_chains: - filters: - name: envoy.filters.network.tcp_proxy typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy stat_prefix: iperf_tcp cluster: iperf3_server_for_client01_cluster transport_socket: name: envoy.transport_sockets.tls typed_config: "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext require_client_certificate: true common_tls_context: tls_certificates: - certificate_chain: filename: certs/servercert.pem # 証明書をEnvoyコンテナにマウントすることを忘れずに private_key: filename: certs/serverkey.pem # 秘密鍵をEnvoyコンテナにマウントすることを忘れずに validation_context: match_subject_alt_names: - exact: edge-proxy-client01.sekinet.example # ご自身で利用するサブジェクトALT名を入れてください trusted_ca: filename: certs/cacert.pem   # CA CertsをEnvoyコンテナにマウントすることを忘れずに clusters: - name: iperf3_server_for_client01_cluster type: static connect_timeout: 10s load_assignment: cluster_name: iperf3_server_cluster endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 172.17.0.2 # ローカルで立ち上げたiperfのアドレス port_value: 5201 # ローカルで立ち上げたiperfのLISTENポート ※ IPv6アドレスのエンドポイントをFQDNで引けるようにするため OPEN IPv6 ダイナミック DNS for フレッツ・光ネクスト を利用してます。 Envoyプロキシの立ち上げ 色々な方式がありますが, 基本的には立ち上げ時にEnvoyConfigと証明書に関連するファイルを正しくEnvoyに与えることが出来れば良いです。私の場合は,一度公式のEnvoyコンテナをラップするコンテナをビルドしていますが,同じことができれば公式のEnvoyコンテナをそのまま利用しても構いません。 Client側 Scriptで起動時にEnvoyConfigや証明書関連のファイルをマウントしています。 #!/usr/bin/env bash docker run -d --restart unless-stopped \ --net=host \ --volume $(pwd)/envoy.yaml:/etc/envoy.yaml:ro \ --volume $(pwd)/envoy-certs/ca.crt:/certs/cacert.pem:ro \ --volume $(pwd)/envoy-certs/edge-proxy-client01.sekinet.tokyo.crt:/certs/clientcert.pem:ro \ --volume $(pwd)/envoy-certs/edge.sekinet.example.key:/certs/clientkey.pem:ro \ --name sekinet-edge-gateway-envoyproxy \ sekinet/edge-gateway-envoyproxy Server側 Scriptで起動時にEnvoyConfigや証明書関連のファイルをマウントしています。 #!/usr/bin/env bash docker run -d --restart unless-stopped \ --net=host \ --volume $(pwd)/envoy-backend.yaml:/etc/envoy.yaml:ro \ --volume $(pwd)/envoy-certs/ca.crt:/certs/cacert.pem:ro \ --volume $(pwd)/envoy-certs/edge-proxy-server01.sekinet.tokyo.crt:/certs/servercert.pem:ro \ --volume $(pwd)/envoy-certs/edge.sekinet.example.key:/certs/serverkey.pem:ro \ --name sekinet-edge-gateway-envoyproxy \ sekinet/edge-gateway-envoyproxy ※ 実際にはiper3もDockerで起動していますがここでは割愛します。 iperf3で計測してみる クライアント側でEnvoyプロキシに対してiperf3を走らせます。ポート番号はEnvoyの設定ファイルで指定している番号になるので,必ずしもiperf3 Server側と同じポートである必要はありません。下記の結果では,100Mbps上限の環境下で十分なスループットが出ていることが観測されました。また,少なくとも100Mbps程度ではEnvoyのDouble proxyはボトルネックにはならないという結果が得られました。 sekinet@sekinet:~/envoy-mtls$ iperf3 -c 127.0.0.1 -p 12345 Connecting to host 127.0.0.1, port 12345 [ 4] local 127.0.0.1 port 57018 connected to 127.0.0.1 port 12345 [ ID] Interval Transfer Bandwidth Retr Cwnd [ 4] 0.00-1.00 sec 23.7 MBytes 198 Mbits/sec 14 4.56 MBytes [ 4] 1.00-2.00 sec 10.9 MBytes 91.2 Mbits/sec 6 4.56 MBytes [ 4] 2.00-3.00 sec 11.7 MBytes 98.5 Mbits/sec 7 4.56 MBytes [ 4] 3.00-4.00 sec 10.4 MBytes 87.5 Mbits/sec 10 4.56 MBytes [ 4] 4.00-5.00 sec 11.9 MBytes 99.5 Mbits/sec 11 4.56 MBytes [ 4] 5.00-6.00 sec 11.5 MBytes 96.4 Mbits/sec 8 4.56 MBytes [ 4] 6.00-7.00 sec 10.8 MBytes 90.6 Mbits/sec 10 4.56 MBytes [ 4] 7.00-8.00 sec 11.7 MBytes 98.5 Mbits/sec 11 4.56 MBytes [ 4] 8.00-9.00 sec 11.1 MBytes 92.7 Mbits/sec 9 4.56 MBytes [ 4] 9.00-10.00 sec 10.9 MBytes 91.7 Mbits/sec 7 4.56 MBytes - - - - - - - - - - - - - - - - - - - - - - - - - [ ID] Interval Transfer Bandwidth Retr [ 4] 0.00-10.00 sec 125 MBytes 105 Mbits/sec 93 sender [ 4] 0.00-10.00 sec 112 MBytes 93.9 Mbits/sec receiver iperf Done. また,自宅の別のノードからクライアントプロキシに対して計測しても同様の結果が得られました。 [Mac]:~/ iperf3 -c 192.168.1.254 -p 12345 Connecting to host 192.168.1.254, port 12345 [ 5] local 192.168.1.69 port 64173 connected to 192.168.1.254 port 12345 [ ID] Interval Transfer Bitrate [ 5] 0.00-1.00 sec 16.5 MBytes 139 Mbits/sec [ 5] 1.00-2.00 sec 14.4 MBytes 121 Mbits/sec [ 5] 2.00-3.00 sec 11.4 MBytes 95.1 Mbits/sec [ 5] 3.00-4.00 sec 11.8 MBytes 99.3 Mbits/sec [ 5] 4.00-5.00 sec 11.3 MBytes 94.9 Mbits/sec [ 5] 5.00-6.00 sec 11.0 MBytes 92.1 Mbits/sec [ 5] 6.00-7.00 sec 11.2 MBytes 93.8 Mbits/sec [ 5] 7.00-8.00 sec 11.0 MBytes 91.9 Mbits/sec [ 5] 8.00-9.00 sec 11.9 MBytes 100 Mbits/sec [ 5] 9.00-10.00 sec 10.9 MBytes 91.5 Mbits/sec - - - - - - - - - - - - - - - - - - - - - - - - - [ ID] Interval Transfer Bitrate [ 5] 0.00-10.00 sec 121 MBytes 102 Mbits/sec sender [ 5] 0.00-10.01 sec 112 MBytes 93.8 Mbits/sec receiver まとめ 今回はサービスメッシュを切り口に透過型プロキシであるEnvoyを使ってインターネット越しでアプリケーション間を接続する構成を試しました。このような取り組みはサービスメッシュを最初に触るとコントローラによって隠蔽してくれているペインポイントが如実に現れます。例えば証明書の管理やEnvoyConfigの管理,動的なUpdateなどはその最たる例でしょう。この経験を通してコントローラ側に何が求められるか,どんな機能が追加されていくのかを追いかけると少し違った目線でサービスメッシュという技術を楽しめるのではないでしょうか。 また,アーキテクチャとしても従来は拠点間をIPSECやWireguardのようなトンネル型VPNで接続する構成が多かったですが,要件によっては透過型プロキシやサービスメッシュを広域に展開するようなユースケースも選択肢として現れるかもしれません。ぜひ皆さんもおうちネットワークにEnvoyを導入して拠点を超えたアプリケーションを接続してみてください。 それでは、明日の記事もお楽しみに! 参考 https://www.redhat.com/ja/topics/microservices/what-is-a-service-mesh https://www.envoyproxy.io/docs/envoy/latest/ https://www.envoyproxy.io/docs/envoy/latest/start/sandboxes/double-proxy https://i.open.ad.jp/#hosts https://cloud.google.com/sql/docs/mysql/connect-admin-proxy
アバター
これは NTT Communications Advent Calendar 2021 3日目の記事です。 こんにちは、イノベーションセンターの松田 ( @take4mats ) です。 当社の Smart Data Platform (SDPF) のサービスラインナップの多くは、お客さまがサービスご利用に必要な操作を統一的に行うための Web UI に加え、同等の Web API を提供しています。 API 仕様は Knowledge Center にてサービスごとに一般公開されているのをご存知でしょうか? (Knowledge Center で各サービス内の APIリファレンス のページをご覧ください。例えば こちらのリンク ) この一般公開されている API 仕様はサービス開発初期に作成され、開発期間にも重要な役割を果たしています。 本記事では、その中で私が携わったサービスから、 API 仕様ドリブンな開発事例を紹介をします。また、これを後押ししてくれた便利なツールもご紹介します。 OAS と他の REST API 仕様の記述形式 前置きとして OpenAPI Specification (OAS) について少しご紹介します。既に OAS をご存じの方は読み飛ばして下さい。 OAS は REST API を記述、生成、使用、及び視覚化するための機械可読性を持ったインターフェースファイルの仕様です。 Version 2 まで Swagger という名前で知られ、主に SmartBear 社のリードで開発されてきました。 Version 3 からは Linux Foundation 配下のオープンプロジェクトとして、 OpenAPI Initiative がリードしています。執筆時点での最新バージョンは v3.1.0 です。 REST API 仕様の記述形式に OAS を採用することで、以下のようなメリットを享受できます。 書きやすさ: YAML 形式 (あるいは後述のツールによる支援) 読みやすさ: YAML から読みやすい HTML を生成するツールが充実している 記述構文に制約: 仕様がブレにくく、人間同士のコミュニケーションコストも削減される 実装に近い: モックを生成できるツールの存在、 JSON Schema 形式の記述部分がそのままコードに流用できる、等 また、念のための程度の情報ですが、 REST API 仕様の記述形式には OAS 以外にいくつかあります。 PowerPoint や Word (!): 一般的なドキュメントを書く際に手を出しがちかも知れませんが、差分管理や親切すぎるオートコレクト(ダブルクオーテーションなど)、記述形式が自由すぎる点など多数の難点があります。これ以上のコメントは差し控えます。 Markdown などテキスト形式: バージョン管理はしやすくなりますが、記述形式を制限できないために、仕様がファジーになる点は変わりません。読みやすさも実装との距離感も課題ありです。 API Blueprint: OAS 以外にもこういった書きやすく、読みやすく、実行できる形式があります。概ね実現したいことを叶えてくれますが、認知度と周辺ツールの充実度で OAS が秀でていると考えています。 とは言っても「OAS の YAML 書くのつらいんだよね…」という方もいると思いますので、色々と楽できる使い方を書いていきます。 とある SDPF サービスでの開発プロセス事例 ここでは、「 API 仕様ドリブンで開発のプロセスを上手く回せたかな」というケースについて Good とし、そうではない Bad な (そしてありがちな) パターンと対比しながら、フェーズごとに紹介していきます。 最初に絵で比較しておくと、以下の図のとおりです。 開発要件策定 〜 API 仕様ラフ案策定 まず何よりも先に提供するべき機能、実現されるべきことを定義していきます。この時点で提供物をリソースとしてモデリングします(手法の詳細は割愛)。 次にこれに対応する形で API 仕様を作っていきますが、いきなり OAS 形式で書くのも大変です。 リソースのパスやメソッドを好きな形で書きながら、リクエスト・レスポンス例を JSON で書いて、イメージを膨らませます。 OAS 形式の仕様策定 いよいよ本格的な API 仕様記述です。ここでいかに品質高い仕様を作るかが全体の品質とリードタイムを左右すると言っても過言ではない、というのが私の考えです。 Bad でも早く実装に進みたいし、 YAML ばかりを書くのもシンドイ。全システム同じメンバーで兼任してるから曖昧な記述でも大丈夫。そんな誘惑に駆られることもあるかも知れません…。 Good 私個人の見解ですが、どんな人数規模でも、システム間に REST API があるなら仕様記述は緩めずにしっかりやっておきたいです。 仮にチームが小規模でも(自分1人だけだとしても)、人間はエラーをするもの、忘れるものだということを心に刻みます。 実際の執筆によく使うのが、 Stoplight Studio です。 詳細はツール紹介の中で触れますが、直感的なフォームを埋めていくだけで記述でき、一方で生成された YAML の直接編集も可能な頼れるエディタで、執筆作業を楽にしてくれます。 また、 lint 機能として Spectral も利用し、ヒューマンエラーに神経をすり減らすことなく作業を進められます。 そしてこの YAML はアプリケーションコードと同様に GitHub でバージョン管理します。 各システムの実装 いざ、各システムの実装です。 Bad API 仕様書の品質がイマイチだと、仕様の解釈がブレて実装誤りを生みます。 あるいは、仕様の確認のためのコミュニケーションコストが発生します。 なかなか手を動かすことに集中できなくなりますね…。 Good 仕様が上手に運用されていると、その API を提供するバックエンドシステムの実装にも直接的に活かすことが出来ます。 バックエンドの実装言語やフレームワークによっては、 Swagger Codegen などで OAS から自動生成されたコードを流用できるかも知れません。 最低でも、 API リクエストのバリデーション部分に JSON Schema として OAS の一部分を活用できるでしょう。 また、この API を叩いて連携する各システム (フロントエンドや他の連携するシステム) の実装にも、この OAS が役立ちます。 仮にこれらシステムの開発者が別のチーム (更には委託などで社外) だとしても、 OAS のファイルさえ渡せば人と人のコミュニケーションを最低限に抑えることが出来て、全員がより実装に専念できます。 単体試験・結合試験・総合試験 想定した API 仕様通りに作ったことを確認し、実際にシステム間をつないで想定通りに動くことを確認するフェーズですね。 Bad API 仕様の運用がイマイチだと、実装誤りに単体試験の段階で気づくことが出来ません。 いざシステム間を結合試験して上手く行かない、さあどっちが悪い!?みたいな戦いに時間を費やすことになるでしょう…。 Good 単体試験には Stoplight Prism が生成する API クライアント向けのモックサーバーが役立ちます。これによって API クライアント (連携システム) 側では、自身のリクエスト・レスポンスハンドリングを仕様に則って確認することが出来ます。 これによって単体試験の時点で各システムの API 仕様準拠のレベルを上げることが出来て、複数チームの時間を同時に拘束する必要のある結合試験・総合試験をスムーズに進められるようになります。 リリース 開発・試験が完了すると、デプロイして運用フェーズを迎えますが、外部に公開するAPIの場合はそのドキュメントの作成も書かせません。 Bad 公開用ドキュメントをリリース直前に慌てて執筆することになるかも知れません。そしてかなり時間を食うことになるでしょう。また、公開したドキュメントと実装の齟齬が発覚して、ドキュメントあるいは実装の修正なんてこともあるかもしれません…。 Good API 仕様を初期から OAS 形式で記述していると、一般公開のための執筆時間がほぼゼロに出来ます。 OAS の YAML から仕様ドキュメントをホスティングしてくれる有償・無償のサービスを利用したり、あるいはツールを使って YAML から HTML を自動生成し、自前のドキュメントサイトに組み込む事ができます。 今回のケースでは統合されたドキュメントサイトである Knowledge Center に組み込むため、 ReDoc を使った後者の方法を採用しました。 全体 一見面倒そうな OAS での運用ですが、ツールを上手く使うって効率化しつつ、乗り越えていくだけの価値があると考えています。 Bad パターンは色々と極端に思われる方もいるかも知れませんが、こういった幾多の手戻り・ムダを排除し、品質の向上と、開発時間の短縮に大きく貢献するはずです。 私自身もこの経験だけで完璧だとは思っていないので、細かい課題や別のプロジェクトに合わせた改善を重ねていきたいです。 ツール紹介 ここでは、開発プロセス事例で触れたツールを紹介していきます。 Stoplight や ReDoc はとても良いツールだと思うのですが、なかなか日本語の情報がなく、役に立てばという思いもあって記事にしました。 先に API Spec とシステムとツールの関係性を絵で紹介すると下図のようになります。 エディタ: Stoplight Studio 執筆用には2つのビューを持っていて、 Form view では直感的なフォームを埋めていくだけで記述していけるので、 OAS 形式の YAML の構文に自信がなくても記述していくことが出来ます。 裏では実際に OAS 形式の YAML をリアルタイムに生成していて、 Code view でそれを直接編集できます。 request/response のサンプルを複数書く方法のように、 Stoplight Studio を使って初めて理解できた記法もありました。 また、 body の定義の漏れや手間を圧倒的に解消してくれたのもありがたいところです。 ちなみに私の使い方ですが、 Code view の VS Code ライクなコードエディタ画面で直接ざっと書いてから、細かい部分 (request/response body の examples や components/schemas 配下の スキーマなど) の記述に Form view を使う方法が好みです。 編集画面は図のとおりです。左にファイルや API パス、中央が編集画面(下部が example response を定義する場面)、右に Spectral による lint の結果がリアルタイムで表示されています。 私は公式サイトから Mac 版 Desktop App をインストールして使用していますが、 Windows 版やブラウザ版もあります。 Linter: Stoplight Spectral VS Code エクステンションの openapi-lint などで事足りるのですが、 Stoplight 製品群に合わせる目的で Spectral を使うようにしています。 前述の通り、 Studio を使っていれば同梱されていて自動で使うことになります。 GitHub Actions に組み込む自動チェックにもこれを用いています。 NPM でインストール可能なので、 CLI でも javascript の中でも利用可能です。 # インストール npm install -g @stoplight/spectral-cli # OAS 標準ルールだけを使用 echo ' extends: "spectral:oas" ' > .spectral.yml # 検査対象のファイルを指定して実行 spectral lint ./path/to/spec.yaml 独自の拡張ルールも定義できるので、こだわりのある人には更に価値がありそうです。 モック: Stoplight Prism Prism はインストールも簡単で複数の役割をこなせる、とても手軽で便利なプロダクトです。 YAML ファイルを食わせて Docker コンテナとして稼働できるので、どこでも気軽に立ち上げることが出来ます。 Mock モード: クライアント開発用に API サーバーをモックしてくれる 正常リクエストへの応答: API 仕様に沿う形で header/body を自動生成してレスポンス 仕様非準拠のリクエストへの応答: 仕様のどこに準拠していないのか、 body で説明してくれる Validation Proxy モード: Prism には Validation Proxy というモードも有り、 API サーバーのレスポンスの正当性を検証することも可能です。 Mock と Validation Proxy 、2つのモードを図解するとこのようになります。 Mock を実際に動かしてみます。 Spec file は有名な PetStore を利用します。 まずはじめに GET の成功例です。 左が Mock 起動とモニタリングログの様子、右が API client としての HTTPie コマンドの様子です。 無事に 200 応答を受けています。 続いて、 POST に失敗した時の様子です。画面右で、 body に必要な name を抜いた状態で request し、 response 422 が返ってきています。 sl-violation というヘッダに仕様違反の内容が記載されています。 他にも、 Prefer header を付与してレスポンスを指定の example にしたり、 YAML に x-faker extension で追記することで Faker.js を使ったリアルな値を自動生成するなど、凝った使い方もできます。 ここまでの機能が欲しくなるかはわかりませんが、気になる方は 公式 doc をご参照下さい。 HTML の自動生成: ReDoc 作った API 仕様書は自社の Knowledge Center という Web サイトに統合する必要があり、その観点で ReDoc に行き着きました。 ReDoc は静的 HTML を生成してくれるためポータビリティがあり、かつ UI はリッチです。 リリースのサイクルとしては、コードをプッシュ&マージしたら自動生成・パブリッシュできるように GitOps を組んでいます。 利用方法は簡単で、以下のようなコマンドで Docker で起動できます(CLI ツールである redoc-cli を NPM でインストールして使うことも出来ます) docker run -p 8080:80 -e SPEC_URL =https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/examples/v3. 0 /petstore-expanded.yaml redocly/redoc これで localhost:8080 にアクセスするとご覧のとおりです。 Stoplight ファミリーはと言いますと、選定当時は Hosted Docs (Stoplight のサイトで公開される) しか提供されていませんでしたが、最近は Elements という名称で Web コンポーネントとして無償提供されているようです。 自前の Web App にも簡単に埋め込めるし Spec file の実体を Stoplight に預けるわけでもないため、これは良いかも知れません…。 おわりに OAS ドリブンな開発事例と、それに関わるツール紹介というちょっとニッチな?話をしました。 よりよく使ってもらえるプロダクトやアプリケーションを作ることが第一ですが、そのためのプロセスや道具も磨いていきたいものです。 また、今回の開発プロセスにはまだ改善点がありますし、ツール類も日々進化していきますので、しばらくすると自分の中でもベストプラクティスが変わっていきそうです。 この記事が参考になれば幸いですが、お読みいただいた皆さんもぜひ、ご自身のやり方を教えて下さい。 それでは、明日の記事もお楽しみに!
アバター