R - Shinyアプリ/管理サーバー テンプレート

# はじめに
R - Shinyでデータ分析用のアプリを作って複数の担当者で利用するということを考えたときに、実際の運用を想定すると色々と壁にぶちあたることがありました。
そこで個別に対応したノウハウをまとめて単純なサンプルを作成し、自分用のテンプレートとして使えるようにしてみました。
モノはGitHubに公開しているのでよろしければご利用ください。
https://github.com/tomotagwork/RShinyTemplate


## 関連記事
[インフラ屋さんのためのR言語: 環境構築編](http://qiita.com/tomotagwork/items/5cd9362f386d330fe96a)
[オフラインでのR環境構築 on RHEL](http://qiita.com/tomotagwork/items/9e86d2fc67a7c7b644d5) 
[z/OSにRを導入してみた](http://qiita.com/tomotagwork/items/83791c46421fef8de912) 
[インフラ屋さんのためのR言語: プログラミング編](http://qiita.com/tomotagwork/items/24d1d11a12bb38aa9f35)
[R Markdownによるレポート生成](http://qiita.com/tomotagwork/items/c92fb40a76f56ea16aa4)
[R MarkdownのHTMLレポートをブラッシュアップ](http://qiita.com/tomotagwork/items/3061a74e1d25cf46e891)
[R - ShinyによるWebアプリケーション作成: 基礎編](http://qiita.com/tomotagwork/items/b56071ea2b276ba87476)
[R - ShinyによるWebアプリケーション作成: shinydashboard編](http://qiita.com/tomotagwork/items/0efe3f08ddd1f268c8f8)
[R - ShinyによるWebアプリケーション作成Tips: shinydashboardでの画面遷移制御](https://qiita.com/tomotagwork/items/bc061a363eb36382d2e2#_reference-c7c49926f78c0fc44cc0)
[R - ShinyによるWebアプリケーション作成Tips: UIオブジェクトの動的制御](http://qiita.com/tomotagwork/items/d3c3a0533224e3faea7d)
[R - Shinyアプリ/管理サーバー テンプレート](https://qiita.com/tomotagwork/items/3041efde7ae0b46e4c5f) <= 当記事
[R - ShinyアプリでJリーグの勝点推移グラフを作成してみた](https://qiita.com/tomotagwork/items/3162cfef78c71ef4b977)


# サンプル概要
このサンプルでは主に以下の項目について実装しています。
(他にも細かなノウハウを組込んでいますが)

【Shinyアプリ管理】

* なんちゃってマルチプロセス対応
* なんちゃってユーザー管理

Rは基本的にシングルスレッドで稼働するので並行処理が苦手です。そのため、Shinyを利用してWebアプリのようにブラウザから使えるようにしたとしても複数ユーザーで利用させようとすると"待ち"が発生してしまう場合があります。特に大量データを分析する場合などを考えると、あるユーザーが重たい処理を実行してしまうと、別のユーザーが同じアプリにアクセスしようとしても画面がなかなか表示されないといったことがおき、使い勝手が悪くなります。
Shinyアプリを管理するには、ShinyServerというのを立てるというのが王道なのかなと思いますが、並行処理(マルチプロセス化)させるためには、有償のShinyServer Proを使わないといけないようです。有償版だと年間$9,995らしい(参考: [RStudio Shiny Server Pro Pricing](https://www.rstudio.com/pricing/shiny-server-pro/))。
無償の範囲でやりたいので独自のShinyアプリ管理サーバーを作ってみました。Shinyアプリを利用したいユーザーが個別に自分専用のShinyアプリを起動/停止できるイメージです。この時、ユーザーID/パスワードを設定しておき、起動されたShinyアプリではそのユーザーID/パスワードチェックの仕組みを仕込んでおき、マッチした場合しかそのアプリが使えないように制御します。

【ShinyアプリとR Markdownの連携】
ShinyはWebアプリとして動作するので、動的にパラメータ変えて、いろんな観点でデータ分析させるアプリ作るのに便利ですが、Shinyを使えない環境でもレポートを展開したい場合など、静的レポートを作りたい場合も出てきます。その場合、R MarkdownでHTMLの静的レポートが作れるので、それと組み合わせると、どちらもブラウザで利用できて相性がよいので、ShinyアプリからR Markdownの静的レポートを生成させて、それをShinyアプリでダウンロードする、という仕組みを作ってみました。

## 前提
このサンプルは、Windowsをベースに作成しています。Rのコードの中でWindowsのコマンドやバッチを呼び出している箇所がいくつかあります。Linuxなどで動かそうとした場合はその辺りに手を入れる必要がありますが、大まかな骨格はほぼ参考にして頂けると思います。
色々訳あって、このアプリは以下の環境で稼働確認しています。
Windows Server 2012 / Windows7
R 3.5.1
RStudio 1.1.456 (Pandoc 1.19.2.1)


# サーバーマネージャーサンプル

## 全体像
Shinyアプリ管理を行う部分のイメージです。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/132331/45999591-83d7-6b5d-e1b8-1fe242a747dd.png)

サーバーマネージャー自体もShinyアプリとして作成しており、管理画面は以下のようになっています。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/132331/2e3169bb-d199-393f-eeda-070cac46dd57.png)

ユーザーID, パスワードを指定して、起動したいShinyアプリを選択します。
次にCreateボタンを押すと、指定したユーザー専用のShinyアプリが起動します。
アプリが起動すると、下の表に起動されたアプリの一覧が表示されるので、対象のアプリのURLをクリックすると指定したShinyアプリが使えるようになります。
(起動されるShinyアプリ側では、他のユーザーから使われないようにユーザー認証の仕組みを実装しておく。=>後述)

## アプリ補足

各Shinyアプリケーションには専用のポート番号をアサインし、それを識別子としてアクセスさせるイメージです。事前に割当て可能なポート番号のリストをテキストファイル(portList.csv)として用意しておき、サーバーマネージャーではCreateのリクエストがある度に、順次そのポート番号の中から割り当てて専用Shinyアプリを起動します。従って、このリストに定義しているポート番号の数が、Shinyアプリの並行稼働数の上限ということになります。

当サンプルでは、同一ユーザーID/Shinyアプリの組合せで、1アプリしか起動できないようにしています。
ただ、Create要求ではユーザー認証/認可は特に行っておらず、任意のユーザー/パスワードの組合せでShinyアプリの起動ができるようになっています。従って、実際には別のユーザーIDを指定すれば同じShinyアプリを複数立てることができちゃいますが、そこは紳士協定的運用を想定しています。これは、ある閉じた組織内での利用を前提としているためです。
厳密にShinyアプリ作成時にも権限制御が必要であれば、適宜そのあたりは追加でロジックを組込んで頂く必要があります。

### 各種機能
以下に、それぞれのボタンと、関連する主要ファイルを補足します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/132331/c413a465-b5be-f501-c5d7-c1dbdb9b6639.png)


#### Shiny Appリスト
管理対象としたいShinyアプリケーションは、ServerApp\ディレクトリ以下に配置します。このディレクトリ直下にあるディレクトリ名をリストとして表示しています。

#### Createボタン
Createボタンを押すと、指定したユーザー専用のShinyアプリを起動します。
この時、portList.csvで指定されているポート番号のリストから、未使用のものを若い順にアサインして割り当てます。この割り当てられたポート番号でShinyアプリを実行するよう、createServer.batを実行します。
また、そのShinyアプリがどのユーザーのものかを判定できるように、指定されたユーザーID,パスワードの情報を、nnnnn_AppName.csvファイルに保持しておきます(nnnnnはポート番号、AppNameはShinyアプリ名)。同一ユーザー、同一ShinyAppは1時点では1つしか稼働させられない想定です。
Shinyアプリ側では、このファイルを参照することでユーザー認証のロジックを組込むことが可能となります(後述)。
※前述の通り、ここではある閉じた組織内での利用を想定しており、ある程度信頼されたユーザーの利用が前提です。そのため、パスワードの暗号化などは行っておらず、ファイルにベタ書きで保存しています。httpsも使ってないのでネットワーク上もベタでパスワード情報が流れます。

#### Deleteボタン
Deleteボタンを押すと、指定したユーザー専用のShinyアプリを停止します。
実際には停止用のバッチdeleteServer.batを実行することで、対象のShinyアプリに割り当てられたポートをListenしているRscript.exeプロセスをkillしています。
また、Create時に作成されたnnnnn_AppName.csvファイルの削除も行います。
削除を行う際は、当該ShinyアプリをCreateした時に指定したユーザーID/パスワードの組合せを指定する必要があります(つまりパスワードを知る本人のみがDelete可能)。

#### Refreshボタン
最新情報を下段の表に表示します。
具体的には以下のような処理を行っています。

* portList.csvファイルから、割当て可能なポート番号のリストを判断します。
* serverInfo\以下のnnnnn_AppName.csvファイルを読み込んで、ポートとShinyアプリがどのユーザーから起動要求されたのかを判断します。
* netstatコマンドの結果から、上のポートが実際にListenされているか、また、どのプロセスIDでListenされているかを判断します。
* Rのpsパッケージを使って、上のPIDがRscript.exeかどうかを判断します。

※Create, Delete実行後も、Refreshと同様の動作を実装して表示をリフレッシュさせています。

#### ステータス表示

上で取得した状況から、ステータスを以下のように判断して表に出力します。

##### 「# of Active Servers」
 
最大いくつ分のShinyアプリが起動できて、そのうち現時点でいくつActiveになっているかを示す。
3/5 となっていれば、最大5つのうち3つ使われているという状態。

##### テーブル

各ポート毎のステータスを表形式で表示します。以下表中のStatusの意味です。

* down: ポート番号はアサインされていない状態(ポートのアサインが可能な状態。一般ユーザーにはこの状態のものは表示されない。)
* starting: Create要求が出されてポートをアサインし、nnnnn_AppName.csvファイルが作成されているが、当該ポート番号がListenされていない状態。通常はしばらく待ってRefreshすればup状態になるはず。 (起動失敗や、Shinyアプリが予期せずダウンした場合などでもこの状態になる。)
* up: nnnnn_AppName.csvファイルが作成され、かつ、当該ポート番号がRscript.exeによりListenされている状態(Shinyアプリが利用可能な状態)
* zombie: 当該ポート番号がRscript.exeでListenされているが、nnnnn_AppName.csvファイルが存在していない状態(意図しない状態。手動でプロセス殺すしかない。)

また、表中では専用のURLをハイパーリンクで表示するようにしているので、そのリンクをクリックすると、目的のShinyアプリを表示させることができます。

また、一般ユーザーの場合は、userID, shinyApp, PID, status, URLの情報しか表示されませんが、管理者ユーザー(事前に指定してある管理者用ユーザーID/パスワードの組合せ)を指定してRefreshボタンを押した場合のみ、各ユーザーのパスワード情報も表示されるようにしています。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/132331/84f8caaf-1878-c9c7-8b26-bbe5bf2c843c.png)
これにより、管理者はパスワードを知ることができるので強制的にShinyアプリを停止させることが可能です。

# Shinyアプリケーションサンプル
上のサーバーマネージャーで管理される対象のShinyアプリのサンプルです。

## 全体像
Shinyアプリサンプルのイメージです。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/132331/0a4a7250-696e-bc56-b38e-7ed26d61b345.png)

メニューとしては「ログイン」「メニュー01」「レポートダウンロード」の3つです(他のメニューはダミーで中身はありません)。

まずログイン画面でログイン処理を行います。
次に、「メニュー01」で各種データ分析処理を行う画面の想定です。ここでは、csvファイルをアップロードして、それを元にデータ分析を行い、さらに静的レポート生成を行う、ということを想定しています。
「レポートダウンロード」では、生成された静的レポートをダウンロードします。

## アプリ補足

### ログイン
![image.png](https://qiita-image-store.s3.amazonaws.com/0/132331/cb9dfad4-503b-9ebe-9156-ac0031d841d1.png)

ログイン処理を行わせます。ここで指定するユーザーID、パスワードはサーバーマネージャーでCreate時に指定したものと一致していなければなりません。
アプリ中では、自アプリがListenしているポート番号(sessionオブジェクトから取得)と、自アプリの名前(getwd()のパスから取得)を元に、nnnnn_AppName.csvファイルの中身を読み込み、ユーザーID/パスワードのセットを取得し、入力された文字列とのマッチングを行うことにより認証しています。
ここではできるだけシンプルに追加コンポーネント無しに実装することを前提にしているのでこのようにしてます。きちんとやるならLDAP使うとか考えないといけないかもしれませんが、まぁ"その人専用のShinyアプリを立ち上げるための一時的なアカウント"という位置づけなのでこれで充分かと思います。

### メニュー01 (データ分析)
ここがメインとなる分析画面です。
ここでの想定は、分析対象のCSVファイルをユーザーのPCからサーバーにアップロードし、それを元にShinyアプリでいろいろ動的に分析する、ということをイメージしています。

このメニューは、ログインしないと使えない想定にしているので、ログイン済みかどうかのフラグを元に、表示させる画面を切り替えています。ログイン未済の場合はログインを促す画面を表示し、ログイン済みの場合、提供したい画面レイアウトを表示するようにしています。
これで、なんちゃってアクセス制御&ユーザー毎の並行処理が実現できます。

このメニューには3つの機能を実装しています。

#### (1) ファイルアップロード
![image.png](https://qiita-image-store.s3.amazonaws.com/0/132331/e24beba0-0ba9-8909-5ae9-876972a18772.png)

そこそこ大きなサイズのデータを扱うイメージです。分析作業自体は各ユーザーがそれぞれ行いたいけど、元にするデータは共有したいという前提なので、誰かがアップロードしたらそれを指定して読み込んで分析させる、みたいな使い方を想定しています。
そのため、まずはファイルを一旦サーバー上の特定のフォルダにアップロードさせる、という作りにしています。
ファイルのアップロードには、fileInput()関数を利用していますが、これはテンポラリーに勝手に適当な名前のファイルがアップロードされてそのファイル名が渡される感じになるので、そのファイルを置きたい場所にコピーする、ということをやっています。

#### (2) データ加工
![image.png](https://qiita-image-store.s3.amazonaws.com/0/132331/09649380-7c72-ee96-718e-629a1b00fba3.png)
アップロードされたファイルのリスト(特定のディレクトリ下のファイルのリスト)を、ラジオボタンで表示させ、ファイルを選択できるようにします。対象のファイルを選択して、Submitボタンを押すと、そのファイルを読み取ってデータの加工を行い、画面に結果を表示します。
ここでは単純なサンプルなので、実質的な加工は行っておらず、ただ読み込んだデータをdataTable、および、pivotTableとして表示しています。(本来であれば、各種UIオブジェクトを使ってインタラクティブにデータの分析なんかを行えるアプリを作りこむイメージです。)

##### Tips: dataTableで大きなサイズのデータ(数100MBとか)を扱う場合の考慮
ここで、dataTableのTipsも紹介しておきます。DTパッケージのdataTableオブジェクトは表形式のデータをインタラクティブに扱えるステキな機能が多々提供されています。Shinyでは重宝するでしょう。
参考: [DT: An R interface to the DataTables library](https://rstudio.github.io/DT/)
通常、ShinyのアプリはHTML, JavaScriptをベースにしているので、表データなどを扱う場合でもブラウザ側にデータを全部もってきて、そこで表示内容を加工する、ということが行われます。ですので、そこそこサイズの大きなデータを扱う場合、ブラウザの反応がむちゃくちゃ重くなります。そこで、dataTableでは、server-side processingという機能があり、処理はサーバーサイドで行わせて必要なデータだけクライアント(ブラウザ)に送るということができます(オプションでserver=TRUEを指定)。
参考: 
[Using DT in Shiny](https://rstudio.github.io/DT/shiny.html)
[Server-side Processing in R](https://rstudio.github.io/DT/server.html)
ただし、この機能を使うと、DTのダウンロード機能がうまく動きません。downloadはdataTableのオプションでボタンを追加してdataTableのデータをユーザーのローカル環境にcsvとしてダウンロードできる機能なのですが、server-seide processing機能を使うと、ブラウザ側には表示部分の情報しか無いため、ダウンロードするとその部分の情報しか保存されません。
ダウンロード機能も結構需要があるので、どうにかして両方を実現したい！
で、ダウンロード部分は個別に実装することで解決しています。表の上に「Download」ボタンを用意して、元にしているデータをダウンロードできるようにしています。

##### Tips: dataTable, pivotTableの横スクロール
dataTableやpivotTableをShinyで使う場合に、横幅が長くなりすぎて画面に収まらない場合があります。その場合、デフォルトでは横スクロールできるようにならないので、右側部分が見られません。
その対応のために、対象のオブジェクトに対して明示的にcssの定義を追加することで、スクロールさせることができます。
例

```r:コード抜粋
        tabPanel("DataTable", "",
                 uiOutput("uiOut_DTDownload_Menu01"),
                 br(),
                 tags$head(tags$style(type='text/css', '#dataTable_rawData_Menu01{ overflow-x: scroll; }')),
                 dataTableOutput("dataTable_rawData_Menu01")
        ),
        tabPanel("PivotTable", "",
                 tags$head(tags$style(type='text/css', '#pivot_rawData_Menu01{ overflow-x: scroll; }')),
                 rpivotTableOutput("pivot_rawData_Menu01")
```

tag$... で指定している箇所です。dataTableOutput, rpivotTableOutputのそれぞれのオブジェクトに対して、overflow-x: scroll の指定を追加してあげます。

#### (3) 静的レポート生成
![image.png](https://qiita-image-store.s3.amazonaws.com/0/132331/4fef8863-9207-5e2c-ce5d-a2cbce2b8247.png)

さて、なんやかんや(2)でデータを加工して、例えば特定の条件で絞込みをして意味のあるデータだけ抽出した時に、それを静的レポートとして作成したい、みたいなストーリーを想定しています。
そのため、(2)で加工した内容を元に静的レポートを作成する、というシナリオです。
(ここでは単純なサンプルとして作成しているので、実質的な加工は行っておらず、(2)で表示させた内容とそのまま同じ内容ものを静的レポートとして生成する、ということをやっています)。
キーワードを指定して、Exportボタンを押すと、(2)で操作した内容を引き継いで、事前に準備してあるR Markdownのレポート生成バッチを起動します。
この時、Shinyアプリ - R Markdown間で受け渡しが必要なデータは、一旦テンポラリーのファイル(xxx.rdata)に保持し、引数としてそのファイル名を渡します。バッチからR Markdownによりレポートを生成させる際、xxx.radataファイルから必要なデータを読みこみ、特定のディレクトリにHTMLの静的レポートを生成します。

### レポートダウンロード
ここもログイン済の場合に、適切な画面を表示するよう制御しています。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/132331/e0549a59-0e13-937c-7a4b-18812e1a4721.png)

レポート配置先のhtmlファイルをリストするようにしているので、レポートが生成されるとここに表示されることになります。
ここもラジオボタンでファイルを選択できるようにしているので、ファイルを選択してdownloadボタンを押すと、ファイルがローカルに保存されます。
HTMLファイルなのでそのままブラウザで開いてみることができます。また、これはHTML単体で完結する静的レポートなので、メールとかでShinyサーバーにアクセスできない人との間で共有することもできます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/132331/1a32d065-b8b9-1bdb-26b4-f2aef1ad28d8.png)

# 実行方法

## 事前準備
R, Pandocをインストールしておきます。
(PandocはRStudio付属のものでもよいです。)

以下のサンプルを丸ごと適当なディレクトリにコピーします(ここではxxxとする)。
https://github.com/tomotagwork/RShinyTemplate
(ディレクトリ構成も合わせるようにしてください。)

## 変更箇所
環境に合わせていくつかカスタマイズする必要があります。

### (1)xxx\env.bat
serverPortNumber: サーバーマネージャーがListenするポート番号
path: Rインストールディレクトリ下のbin、および、pandoc.exeのパスの設定
(RStudioに含まれるpandocを利用する場合は、RStudioインストールディレクトリ下のbin\pandocに配置されてるはず)


```bat:xxx\env.bat
 @echo off

set serverManagerApp=ServerManager
set serverPortNumber=10000

set PATH=C:\x\R\R-3.5.1\bin;C:\x\RStudio\bin\pandoc;%PATH%
```

### (2) xxx\ServerManager\global_env.R

serverAddress: ブラウザからShinyアプリを利用する時にアクセスするURLのホスト名部分(http://*server01*:10001)。ブラウザからアクセスする際に名前解決できる必要があります(もしくはIPアドレス直接指定でも可)。
AdminUserID/AdminPassword: 管理者用のユーザーID, パスワード

```R:xxx\ServerManager\global_env.R
Sys.setenv(TZ="Asia/Tokyo")

### Variables ###################################################################
serverAddress <- "server01"
AdminUserID <- "admin"
AdminPassword <- "ABCDEFGH"

serverInfoDir <- "../serverInfo/"
shinyAppDir="../ShinyApps/"
portListFileName <- "./portList.csv"

strCreateServerCommand <- "./Batch/createServer.bat"
strDeleteServerCommand <- "./Batch/deleteServer.bat"
```

### (3) xxx\ServerManager\portList.csv

Shinyアプリ用に利用するポート番号のリスト
ここに記載したポートの数が、Shinyアプリの並行稼働数の上限となります。

```csv:xxx\ServerManager\portList.csv
portNumber
10001
10002
10003
10004
10005
```

## サーバーマネージャーの起動

xxx\ServerManager_process_check.bat を実行します。
上で指定したサーバーマネージャー用のポートがListenされていなければ、サーバーマネージャーが起動していないとみなして起動を行います。Listenされていれば起動中とみなし何もしません。
このバッチをタスクスケジューラーで定期的に実行しておけば、意図せずサーバーマネージャーが落ちてしまった場合でも自動起動されることになります。

## サーバーマネージャーの停止

xxx\ServerManagerTerminate.batを実行します。
これはサーバーマネージャーのみを停止させるだけで、ここから起動されたShinyアプリは停止されません。Shinyアプリを停止するには個別にサーバーマネージャーからDelete処理を行う必要があります。

# おわりに
自分で再利用する時のテンプレートとして作りましたが、部分的にでも利用できる所はあると思うので参考にして頂ければと思います。詳細はコードをご参照ください。
セキュリティの制御やマルチユーザーの制御をもっとちゃんとやりたいのであれば、恐らく有償版のShinyServer Proを使えばいいんだと思います。(使ったこと無いけど...)