Reactでインフラ構築してみたい

最近、インフラ周りを触ってますが、ダッシュボードで各サービスの画面がバラバラに配置されているので、

サービスがどういう構成で動いているのか、どこでアラートが発生しているのかがわかりにくいんです :cry: 

ふと思ったのは、みなさんがよく書くインフラ図がそのまま構築に使えたらわかりやすいのでは？です

近いイメージだと [Cloudcraft](https://cloudcraft.co/) ですね

もちろん、インフラの知識は必要ですが、インフラ構成とステータス情報がひと目でわかります :+1: 

この記事では、Custom React RendererでTerraformのconfiguration(JSON)を出力してみます :smile: 

ユースケースは、Webで構成したCompoenntをJSONで出力してダウンロードしたものをTerraformで利用したりします

サンプルコードは一応動作しますが、機能不足のため参考程度でお願いします :pray: 

## React Rendererとは

ReactのComponentをあらゆる形式にrenderするもので、おなじみReactDOMやモバイル用のReactNativeなどがあります

他にもたくさんrendererが公開されています

参考: https://github.com/chentsulin/awesome-react-renderer

### Reconciler

ReconcilerはStateが変更されたときの差分を検知し、検知すると特定のfunctionを呼び出します

この呼び出されるfunctionを定義するところが `Host Config` と呼ばれるObjectです

Host Configのfunctionの振る舞いを変更することで、Custom rendererを実装することができるのです :+1:

reconcilerは[package](https://www.npmjs.com/package/react-reconciler)化されています

## Custom rendererを自作する

```index.js
import React from "react";
// import ReactDOM from "react-dom";
import TerraformRenderer from "./renderer";

// Root compoennt
const Config = props => <config>{props.children}</config>;

// Resource component
const Resource = props => (
  <resource name={props.name}>{props.children}</resource>
);

// Instance component
const AwsInstance = props => <instance {...props} />;

const App = () => {
  return (
    <Config>
      <Resource>
        <AwsInstance name="example" instance_type="t2.micro" ami="ami-abc123" />
      </Resource>
    </Config>
  );
};

TerraformRenderer.render(<App />, document.getElementById("root"));
```

index.jsはこんな感じですが、rendererを実装していないのでエラーになります

### Custom renderを作成する

custom renderを作成します

```./renderer.js
const TerraformRenderer = {
  render(element, renderDom) {
  }
};

export default TerraformRenderer;
```

これでひとまずエラーは表示されませんが、画面が真っ白、、

### Elementのクラスを作成

React elementからインスタンスを作成するために、config, resource, instance element用のクラスを作成します

```./elements/config.js
class Config {
  constructor(props) {
    this.props = props;
    this.children = [];
  }

  appendChild(child) {
    this.children.push(child);
  }

  render() {
    const config = this.children.reduce((acc, cur) => {
      const render = cur.render();
      return { ...acc, ...render };
    }, {});
    return { ...config };
  }
}
export default Config;
```

```./elements/resource.js
class Resource {
  constructor(props) {
    this.props = props;
    this.children = [];
  }

  appendChild(child) {
    this.children.push(child);
  }

  render() {
    const resources = this.children.reduce((acc, cur) => {
      const render = cur.render();
      return { ...acc, ...render };
    }, {});
    return {
      resource: resources
    };
  }
}
export default Resource;
```

```./elements/awsInstance.js
class AwsInstance {
  constructor(props) {
    this.props = props;
    this.children = [];
  }

  appendChild(child) {}

  render() {
    return {
      aws_instance: {
        [this.props.name]: {
          instance_type: this.props.instance_type,
          ami: this.props.ami
        }
      }
    };
  }
}
export default AwsInstance;
```

各クラスに `appendChild` と `render` functionを定義しておきます

### Host configの修正

ここに独自のロジックを実装していきます

- ポイント
    - `createInstance` のパラメータにReact elementのtypeとpropsがくるので対応するインスタンスを作成
    - `appendInitialChild` で親インスタンスに子インスタンスをセット
    -  `appendChildToContainer` でrootコンテナにJSONをrender

```./HostConf.js
import Config from './elements/config';
import Resource from './elements/resource';
import AwsInstance from './elements/awsInstance';

const constructors = {
  config: Config,
  resource: Resource,
  instance: AwsInstance
};

const HostConfig = {
  getRootHostContext: rootContainerInstance => {},
  prepareForCommit: containerInfo => {},
  resetAfterCommit: containerInfo => {},
  getChildHostContext: (parentHostContext, type, rootContainerInstance) => {},
  shouldSetTextContent: (type, props) => {},
  createInstance: (type, props) => {
    const { children, ...args } = props;
    return new constructors[type](args);
  },
  createTextInstance: (
    type,
    rootContainerInstance,
    hostContext,
    internalInstanceHandle
  ) => {},
  finalizeInitialChildren: (
    domElement,
    type,
    props,
    rootContainerInstance,
    hostContext
  ) => {},
  appendChildToContainer: (container, child) => {
    container.innerHTML = JSON.stringify(child.render());
  },
  appendChild: (parent, child) => {},
  appendInitialChild: (parentInstance, child) => {
    parentInstance.appendChild(child);
  },
  supportsMutation: true
};
export default HostConfig;
```

参考: https://github.com/facebook/react/blob/master/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js

react-reconcilerをインストールします

```
$ npm install react-reconciler
```

最後にrendererを修正します

```./renderer.js
import Reconciler from "react-reconciler";
import HostConfig from './HostConf';

const reconciler = Reconciler(HostConfig);

const TerraformRenderer = {
  render(element, renderDom) {
    const container = reconciler.createContainer(renderDom, false, false);
    reconciler.updateContainer(element, container, null, null);
  }
};

export default TerraformRenderer;
```

![result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6470/e8f176c3-3b9e-9b17-feea-222c89d309a1.png)


表示できました :tada: 

## まとめ

ひとまず、Custom rendererでJSONを画面に表示することができました :smiley: 

ドキュメントや本体のコードを参考にしましたが、Custom rendererを実装することでReactの知識がさらに深まりました

あと、renderer周りを調べていて、役割毎にpackageが分割されてるんですよねー (とても参考になります

類似のサービスはあるし、APIで取得できる情報も限られていると思いますが、Reactでインフラ構築を今後も個人プロジェクトとしてやっていきですー :grin: 

あと、テストも書かないとね、、 :smiling_imp:

こちらの動画もとても参考になりましたので、是非ご覧ください
[React Conf 2019 Building a Custom React Renderer | Sophie Alpert](https://www.youtube.com/watch?v=CGpMlWVcHok)

Enjoy Custom React Renderer ヘ(^o^)ノ