グラフDBで構成情報を可視化したい：neo4j篇

* 本記事は2021/12/12のアドベントカレンダー原稿を落とした私が後日追加した記事です

以前、仕事の必要に迫られてシステムのNW構成を把握するためにneo4jを使う機会があったため、また使い始めてみようと改めて触りました。
ローカル環境のJupyterLabから同じくローカル起動のneo4j serverにクエリすることで、AWS構成情報のグラフ化を試しました。

## はじめに
冒頭からなんですが、但し書きです。
本記事を作成する中で断念したことは次の通りです。

- [aws/graph-notebook](https://github.com/aws/graph-notebook)のウォークスルーをJupyterLab環境で動かす
    - JupyterLabではなくJupyterノートブック前提のnbextensionであり、拡張機能に互換性がなさそうだったため中止しました
- Neo4jのWebコンソール同等の可視化グラフ
    - グラフ構造の可視化にあたっては色々と実装上の調整が必要だったため、触りだけ確認しました
    - その辺りの操作性だけ先に確認したい人は[Neo4j Sandbox](https://neo4j.com/sandbox/)がおすすめ
- ローカル環境からAmazon Neptuneにクエリする
    - サービス仕様として不可... RDSやAESではパブリックアクセスやData APIが可能なので盲点でした
    - [Amazon Neptune の制限](https://docs.aws.amazon.com/ja_jp/neptune/latest/userguide/limits.html)

> Amazon Neptune は、仮想プライベートクラウド (VPC) 専用サービスです。
> また、VPC の外部からインスタンスにアクセスすることはできません。

### 前提
- JupyterLab（ローカル）
    - 今回はAWS認証情報なし
    - pipenv環境で動かす
- 主に必要なPythonパッケージ
    - `neo4j`
    - `networkx`
    - `matplotlib`
- Neo4j（ローカル）
    - [neo4j - macOS installation](https://neo4j.com/docs/operations-manual/current/installation/osx/)
    - [Neo4j Bolt Driver for Python](https://github.com/neo4j/neo4j-python-driver) 	
- サンプルデータ
    - 件数は極小ですが、今回はAWS Configが出力するjsonを使ってます


## グラフDBとNeo4jについて
（ネット検索のキーワード精度が落ちる一因ですが、、）ここでいう「グラフ」はデータ構造のことで、Neo4jはDBエンジンやクエリ仕様を意味してます。
「有向グラフ」や「無向グラフ」でキーワード検索すると、関連する結果になるはずです。

自分自身、グラフ理論を語るほど理解していないですがグラフDBのユースケースは明確です。

- 多：多の関係を高速に検索したい
- 可視化ツールと組み合わせて複雑なデータ（大抵は多：多）の視認性を上げたい

概要については、AWSのアーキテクト資料がとても分かりやすかったので参考になります。
[グラフDBを用いた開発のポイント](https://pages.awscloud.com/rs/112-TZM-766/images/20211119_DevAxConnect_Neptune_Kempei-Igarashi.pdf)

多：多なのにどうやってデータを高速に扱えるのか、
という点はグラフ理論のコア実装に関わる部分で自分はまだ十分に理解できていません...
少なくとも、データロードで作成するエンティティ（ノードやエッジなど）には全てインデックスを作成する仕様になっているようです。

### Neo4j
Neo4jはローカルファイルをデータストアとして使うため、可用性を上げたい場合に[Neo4j Fabric](https://neo4j.com/blog/getting-started-with-neo4j-fabric/)を導入することになり、データベース構築に頭を使う必要がありそうです。その場合、マルチDB仕様のためfabric構成で台数を増やすごとに性能もスケールアウトするようです。

自分がグラフDBを初めて知った契機はRDFでしたが、グラフDBの強力さを知ったきっかけはパナマ文書の印象が強いです。
パナマ文書のデータセットはNeo4jが無償提供しているため、注意書きを理解した上で使いましょう。
参考：[Offshore Leaks Database](https://offshoreleaks.icij.org/pages/database)

なお、今回使うNeo4jはOpenCypherというクエリ仕様ですが、他にはGremlinとRDFが標準的です。
参考：[GremlinとOpenCypher](https://docs.aws.amazon.com/ja_jp/neptune/latest/userguide/access-graph-opencypher-overview.html#access-graph-opencypher-overview-with-gremlin)

AWSはneo4jからNeptuneへの移行をブログで紹介してます...さすが。
https://aws.amazon.com/jp/blogs/news/migrating-a-neo4j-graph-database-to-amazon-neptune-with-a-fully-automated-utility/


## サンプルデータ準備
Neo4jにデータロードするため、AWS Configのjsonを簡単に加工しておきます。
と言っても、必要な項目だけパースしてcsvに変換するだけです。

どのような形にパースすれば良いか、という点が最初の関門ですが、一般的なデータ設計と同様のステップが必要です。
参考：[Graph Modeling Guidelines](https://neo4j.com/developer/guide-data-modeling/)

Neo4jでは、NodeとRelationshipsのデータを分けてロードすることになります。

### 元データ(json)
今回はIAM RoleとLambda関数のAWS Configデータです。

```json
{
  "fileVersion": "1.0",
  "configurationItems": [
    {
      "relatedEvents": [],
      "relationships": [
        {
          "resourceName": "lambda_test",
          "resourceType": "AWS::IAM::Role",
          "name": "Is associated with "
        }
      ],
      "configuration": {
        "functionName": "Athena_Cost-Query",
        "functionArn": "arn:aws:lambda:ap-northeast-1:123456789012:function:Athena_Cost-Query",
        "runtime": "python3.8",
        "role": "arn:aws:iam::123456789012:role/lambda_test",
        "handler": "lambda_function.lambda_handler",
        "codeSize": 831,
        "description": "",
        "timeout": 210,
        "memorySize": 128,
        "lastModified": "2020-02-20T13:14:36.105+0000",
        "codeSha256": ".....",
        "version": "$LATEST",
        "tracingConfig": {
          "mode": "PassThrough"
        },
        "revisionId": "42275ada-2c5b-4700-9c3b-e567f527f7e7",
        "layers": [],
        "state": "Active",
        "lastUpdateStatus": "Successful",
        "fileSystemConfigs": [],
        "packageType": "Zip",
        "architectures": [
          "x86_64"
        ]
      },
      "supplementaryConfiguration": {
        "Tags": {}
      },
      "tags": {},
      "configurationItemVersion": "1.3",
      "configurationItemCaptureTime": "2021-11-12T21:14:00.811Z",
      "configurationStateId": 1636751640811,
      "awsAccountId": "123456789012",
      "configurationItemStatus": "OK",
      "resourceType": "AWS::Lambda::Function",
      "resourceId": "Athena_Cost-Query",
      "resourceName": "Athena_Cost-Query",
      "ARN": "arn:aws:lambda:ap-northeast-1:123456789012:function:Athena_Cost-Query",
      "awsRegion": "ap-northeast-1",
      "availabilityZone": "Not Applicable",
      "configurationStateMd5Hash": ""
    },
    (以下、取得されたConfig数の配列)
```

### 加工済みデータ(csv)
```csv
AccountId	ResourceRegion	Az	SubjectName	SubjectType	Relation	ObjectName	ObjectType
123456789012	ap-northeast-1	Not Applicable	lambda_test	AWS::IAM::Role	Is associated with 	Athena_Cost-Query	AWS::Lambda::Function
#(以下略).....
```

### データファイルの保存先
neo4jをコマンドラインで起動すると、下記のようなメッセージが標準出力されます。
`import`ディレクトリに準備したデータファイルを保存すればOKです。

```bash
$ neo4j start 
Directories in use:
  home:         /usr/local/bin/neo4j
  config:       /usr/local/bin/neo4j/conf
  logs:         /usr/local/bin/neo4j/logs
  plugins:      /usr/local/bin/neo4j/plugins
  import:       /usr/local/bin/neo4j/import
  data:         /usr/local/bin/neo4j/data
  certificates: /usr/local/bin/neo4j/certificates
  run:          /usr/local/bin/neo4j/run
Starting Neo4j.
Started neo4j (pid 19749). It is available at http://localhost:7474/
There may be a short delay until the server is ready.
See /usr/local/bin/neo4j/logs/neo4j.log for current status.
```

## neo4jデータロード
個人的見解ですが、データロードが完了すれば山場は超えたようなものです。
参考：[How-To: Import CSV Data with Neo4j Desktop](https://neo4j.com/developer/desktop-csv-import/)

今回はロード方法までプログラミングしていませんが、`LOAD CSV`ステートメントではURL指定が可能なのでS3 Pre-signed URLを発行すれば任意のS3ファイルをNeo4jにロードできるため、実運用では役立つこと間違いなしです。

Neo4jをローカル起動後、Webコンソールにログインして次のクエリを実行します。
＊ファイルパスは`import`ディレクトリからの相対パス

```sql
-- AWS Config 
-- about IAM Roles attached Lambda function
-- Nodes: IAM Role
LOAD CSV WITH HEADERS FROM "file:///AwsConfig_sample_subject-node.csv" as sbj
CREATE (:IAMRole {iam_role_name: sbj.SubjectName});
-- Nodes: Lambda function
LOAD CSV WITH HEADERS FROM "file:///AwsConfig_sample_object-node.csv" as obj
CREATE (:AWSLambda {lambda_function_name: obj.ObjectName});
-- Relationships: roles associated with functions
LOAD CSV WITH HEADERS FROM "file:///AwsConfig_sample_data.csv" as data
MATCH (roles:IAMRole {iam_role_name: data.SubjectName}), (functions:AWSLambda {lambda_function_name: data.ObjectName})
CREATE (roles)-[:ATTACHMENT {relation_name: data.Relation}]->(functions);
```

次のクエリでデータ検索結果をグラフで確認できます

```sql
MATCH (r:IAMRole)-[]->(f:AWSLambda)
RETURN r, f
```

<details><summary>グラフデータ（画面キャプチャ）</summary><div>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215094/3fe207f1-4a25-5a02-0c76-6e3c6f09df42.png)

</div></details>


## JupyterLabにグラフを描画する
Pythonでは[NetworkX](https://networkx.org/documentation/stable/tutorial.html)というライブラリがデファクトスタンダードっぽいため、今回もそちらを採用しました。JupyterLabで基本的な動作検証ができれば、Webアプリに組み込む際の目処もつきそうです。

＊コードブロックはJyupyterLabのセル単位だと思ってください

```python
from neo4j import GraphDatabase
import networkx as nx

driver = GraphDatabase.driver('bolt://localhost:7687', auth=('neo4j', 'Alskdjfh6'))
#有向グラフのインスタンス
Di = nx.DiGraph()


def create_edge(node_edge):
    #networkx仕様により、エッジの両端(ノード)が存在しなければノード・エッジを同時作成する
    #引数はtupple
    Di.add_edges_from(node_edge)
    
def get_RoleAttachedLambda(tx):
    query = '''MATCH (r:IAMRole)-[]->(f:AWSLambda)
RETURN r.iam_role_name, f.lambda_function_name
    '''
    node_edge = []
    for record in tx.run(query):
        edge_data = (record['r.iam_role_name'], record['f.lambda_function_name'])
        node_edge.append(edge_data)
    
    create_edge(node_edge)

with driver.session() as session:
    session.read_transaction(get_RoleAttachedLambda)
```

ひとまずグラフ描画できるかお試し
参考：[Drawing graphs](https://networkx.org/documentation/stable/tutorial.html#drawing-graphs)

```python
#グラフ描画
#networkx自身に描画機能はなく、draw_networkx()の実行にはmatplotlibが必要
nx.draw_networkx(Di)
```

<details><summary>役立ちそうにないグラフができました...</summary><div>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215094/b28f62c2-7f1c-9bbf-1a6c-0c9845b648d7.png)

</div></details>

描画エリアとノード間の距離を調整して再実行

```python
%matplotlib inline
import matplotlib.pyplot as plt

plt.figure(figsize=(30,20))

pos = nx.spring_layout(Di, k=0.2)
nx.draw_networkx(Di, pos)
```

少しは役立ちそうなグラフができました:rocket:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215094/cf90d250-c58c-0586-b3ed-c416dfabe028.png)



## 参考記事
The Cypher Style Guide
https://github.com/opencypher/openCypher/blob/master/docs/style-guide.adoc#recommendations

グラフ理論とNetworkX
https://docs.pyq.jp/python/math_opt/graph.html

[Python]NetworkXでQiitaのタグ関係図を描く
https://qiita.com/inoory/items/088f719f2fd9a2ea4ee5

Neo4jについてちょちょいと調べたまとめ
https://the.igreque.info/posts/2014-06-08-neo4j.html