LoginSignup
31
18

More than 3 years have passed since last update.

ShinyProxyを使ったEnterprise向けShiny Appの構築

Last updated at Posted at 2019-08-06

更新履歴

  • 2019-12-25 : "Kubernetes モードを試してみる" を追加
  • 2019-12-10 : "Containerized モードを試してみる" を追加

はじめに

アクセスする人数が多くなってきたときに、どうやってShiny Appをスケールさせるかなーと考えていたときに、たまたまこのOSSを見つけたので検証してみました

Shiny Server の運用上の課題

  • OSSのShiny Serverでは、1つのShinyappが 1 processで動くため、複数人のユーザでアクセスすると、急に動きがもっさりしがちです。例えば、ユーザA、Bの順でアクセスして、データをロードしはじめると、ユーザAの処理が終わるまで、ユーザBは待たないといけないので、待っている間ストレスがかかります
  • OSSのShiny Server自体では、認証の機能を持っていないので(Shiny Server Proが必要)、別途Reverse Proxyとして、認証機能のWebサーバ(nginxなど)を構築する必要がありました

ShinyProxyとは?

自分の言葉でいうなら、

  • 事前にビルドしたShiny Appのdocker imageを用意しておき、ShinyProxyにひもづけておけば、そのShiny Appにアクセスしたときに、ユーザごとにそのimageのdocker containerが立ち上がる デプロイ環境を提供してくれる素敵なプロダクトです
    • 結果、1 userあたり、1 Shinyapp = 1 container の構成になり、ユーザが互いにボトルネックにならない環境を用意できる
  • ユーザの認証機能として、複数のProtocol(LDAP、Kerberosなど)も利用できます。すごい
  • ShinyProxy自体はJavaのSpring bootで実装されている
  • Githubのリリースをみると、2016年くらいから開発しているみたいで、今年になってもアップデートされている(2019-06-22にver. 2.3.0がリリースされている)
  • Apache License 2.0

ManualにもShiny AppをEnterprise化するためのOSSであると強調しており、企業でShiny Appを使うときの問題を解決しようとOpen Analyticsという団体?が取り組んでいるようです。Open Analyticsの活動は、Twitterとかで眺めてみると良いと思います(自分も今日フォローしました)

先にマニュアルを以下に載せておきます

公式マニュアル

Open Analytics Community Support

Github

ShinyProxyの構築

Standalone vs containerized

ShinyProxyは2つのデプロイ方法があります

  • Standalone モード : javaが動くHost自体にshinyproxyをインストールして使う
  • containerized モード : shinyproxy自体もcontainerとして動かす

single node vs swarm vs kubernetes

また、Shiny AppをDocker containerとしてデプロイするときに、一番基本となるdocker engineがインストールされているHostにsingle nodeとしてデプロイする他、swarmやkubernetesを使ったdeployもできるみたいです。kubernetes対応やCloudサービス対応もすでに例があるようなので、完成度が高い印象を受けました

Standaloneモードを試してみる

Install and Launch

2019/08/06時点で最新のversion 2.3.0をインストールしていきます。Cent OS 7系を使用しています。Dockerのインストール方法は省略します。

注意点としては、docker hostにShinyProxyからアクセスしてコンテナを立ち上げる操作が発生するので、Docker startup optionsの設定を事前に実施しておいてください

$ wget https://www.shinyproxy.io/downloads/shinyproxy_2.3.0_x86_64.rpm
$ sudo rpm -U shinyproxy_2.3.0_x86_64.rpm

$ sudo systemctl enable shinyproxy
$ sudo systemctl start shinyproxy
$ sudo systemctl status shinyproxy
● shinyproxy.service - ShinyProxy
   Loaded: loaded (/etc/systemd/system/shinyproxy.service; enabled; vendor preset: disabled)
   Active: active (running) since 火 2019-08-06 10:25:52 JST; 2min 16s ago
 Main PID: 82468 (java)
    Tasks: 18
   Memory: 517.8M
   CGroup: /system.slice/shinyproxy.service
           └─82468 /usr/bin/java -jar /opt/shinyproxy/shinyproxy.jar
<snip>

インストールしたHostにアクセス(例えば、http://10.1.1.1:8080 とか)してログイン画面が出ればOK。(User nameなんだろとなるが、一旦ログインはスキップ)

demo image を pull

デモ用に動かすdocker imageをpullしておきます

$ sudo docker pull openanalytics/shinyproxy-demo

Configuration

application.yml というシングルファイルで管理します。設定ファイルはrpmでインストールした場合は、/etc/shinyproxy配下に置けばOK

https://www.shinyproxy.io/configuration/ の default configからauthenticationをsimpleに変更して配置します。各項目の詳細は、リンク先を参照してください

$ sudo vim /etc/shinyproxy/application.yml
proxy:
  title: OKIYUKI Shiny Proxy
  logo-url: http://www.openanalytics.eu/sites/www.openanalytics.eu/themes/oa/logo.png
  landing-page: /
  heartbeat-rate: 10000
  heartbeat-timeout: 60000
  port: 8080
  authentication: simple
  admin-groups: scientists

  # Example: 'simple' authentication configuration
  users:
  - name: jack
    password: password
    groups: scientists
  - name: jeff
    password: password
    groups: mathematicians

  # Docker configuration
  docker:
    cert-path: /home/none
    url: http://localhost:2375
    port-range-start: 20000
  specs:
  - id: 01_hello
    display-name: Hello Application
    description: Application which demonstrates the basics of a Shiny app
    container-cmd: ["R", "-e", "shinyproxy::run_01_hello()"]
    container-image: openanalytics/shinyproxy-demo
    access-groups: [scientists, mathematicians]
  - id: 06_tabsets
    container-cmd: ["R", "-e", "shinyproxy::run_06_tabsets()"]
    container-image: openanalytics/shinyproxy-demo
    access-groups: scientists

logging:
  file:
    /var/log/shinyproxy/shinyproxy.log

# For Logging
$ sudo mkdir -p /var/log/shinyproxy
$ sudo chown shinyproxy:shinyproxy /var/log/shinyproxy

restart

$ sudo systemctl restart shinyproxy

# stop 
# sudo systemctl stop shinyproxy

再度アクセスして、jackでログインしてみます。

アクセスできるShiny Appの一覧が以下のように見れます

image.png

例えば、ここで Hello Applicationにアクセスしてみます。

そうすると、以下のような Launching ... という画面が数秒見えたあとで、

Hello ApplicationのShiny Appが立ち上がります

image.png

ここで、Docker Hostでdocker psをしてみると、以下のように、Shiny Appのコンテナが立っていることが確認できました

$ sudo docker ps --no-trunc
CONTAINER ID                                                       IMAGE                           COMMAND                             CREATED             STATUS              PORTS                     NAMES
4fd3b5bcaee638093c8696ab9dadc42175eddb5fb7ef27a2b53c2ff959721d98   openanalytics/shinyproxy-demo   "R -e shinyproxy::run_01_hello()"   2 minutes ago       Up 2 minutes        0.0.0.0:20000->3838/tcp   jolly_satoshi

さらに、この状態のまま、06_tabsetsのShiny Appにアクセスする。再びdocker psをすると、2つのコンテナが立っていることが分かります

$ sudo docker ps --no-trunc
CONTAINER ID                                                       IMAGE                           COMMAND                               CREATED             STATUS              PORTS                     NAMES
c925ae0f43d35cc3e2c41d58153382d6178afb54cadd919b6dac0ab1cd675419   openanalytics/shinyproxy-demo   "R -e shinyproxy::run_06_tabsets()"   13 seconds ago      Up 12 seconds       0.0.0.0:20001->3838/tcp   happy_goldwasser
4fd3b5bcaee638093c8696ab9dadc42175eddb5fb7ef27a2b53c2ff959721d98   openanalytics/shinyproxy-demo   "R -e shinyproxy::run_01_hello()"     4 minutes ago       Up 4 minutes        0.0.0.0:20000->3838/tcp   jolly_satoshi

複数人でのアクセスを想定して、この状態のまま別のブラウザから、今度はjeffでログインして、Hello Applicationにアクセスしてみる。再びdocker psをすると、3つのコンテナが立っていることが分かる。ユーザごとに1 app = 1 containerになっていることが確認できました

$ sudo docker ps --no-trunc
CONTAINER ID                                                       IMAGE                           COMMAND                               CREATED             STATUS              PORTS                     NAMES
bae351ecb8f5992b301a7c7c7f0754a6e39001dcc017283b6a7bd6292c4f9b3b   openanalytics/shinyproxy-demo   "R -e shinyproxy::run_01_hello()"     17 seconds ago      Up 16 seconds       0.0.0.0:20002->3838/tcp   musing_dijkstra
c925ae0f43d35cc3e2c41d58153382d6178afb54cadd919b6dac0ab1cd675419   openanalytics/shinyproxy-demo   "R -e shinyproxy::run_06_tabsets()"   2 minutes ago       Up 2 minutes        0.0.0.0:20001->3838/tcp   happy_goldwasser
4fd3b5bcaee638093c8696ab9dadc42175eddb5fb7ef27a2b53c2ff959721d98   openanalytics/shinyproxy-demo   "R -e shinyproxy::run_01_hello()"     6 minutes ago       Up 6 minutes        0.0.0.0:20000->3838/tcp   jolly_satoshi

また、jackのHello Applicationをブラウザで閉じると、そのコンテナがHostから消えることを確認できます。すぐに消えるわけではないので、その辺の細かい設定はなにかでできるのかもしれません。

Containerized モードを試してみる

ShinyProxy自体もdocker containerとして動かすContainerized モードを試してみます。以下の公式のREADMEに沿ってやってみます

参考 : https://github.com/openanalytics/shinyproxy-config-examples/tree/master/02-containerized-docker-engine

Dockerfile for ShinyProxyの作成

公式通りのDockerfileを作成します

FROM openjdk:8-jre

RUN mkdir -p /opt/shinyproxy/
RUN wget https://www.shinyproxy.io/downloads/shinyproxy-2.3.0.jar -O /opt/shinyproxy/shinyproxy.jar
COPY application.yml /opt/shinyproxy/application.yml

WORKDIR /opt/shinyproxy/
CMD ["java", "-jar", "/opt/shinyproxy/shinyproxy.jar"]

application.ymlの変更

Docker間ネットワーク内でShinyProxyからDockerアクセスできるように application.yml にdocker networkに関する設定を追加します。具体的には、以下の3箇所を変更します

application.yml
proxy:
  <snip>
  container-backend: docker
  <snip>

  # Docker configuration
  docker:
    cert-path: /home/none
    # url: http://localhost:2375   #(0) <--- comment out
    internal-networking: true      #(1) <--- add

  <snip>
  # app
  specs:
  - id: sample
    <snip> 
    container-network: sample-net  #(2) <--- add
    <snip>

Docker NetworkをBridgeモードでつなぐために、(1)と(2)が必要になります。(0)はもともと書いてなければ無視してください。(2)の定義にそってdocker networkを以下のように作成します

$ docker network create --driver bridge sample-net

Docker ImageのBuildとRun

上で用意したDockerfileをもとにDocker Imageをビルドします。

$ docker build -t okiyuki/shiny-proxy:latest .

作成したImageをRunします。docker compose(v2)を利用した例が以下です

version: '2'
services:
  shiny-proxy:
    hostname: shiny-proxy
    container_name: shiny-proxy
    image: okiyuki/shiny-proxy:latest
    ports:
      - 8080:8080
    restart: always
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /var/log/shinyproxy:/var/log/shinyproxy
    networks:
      - sample-net

networks:
  sample-net:
    external: true

それを起動して完!

$ docker-compose -f docker-shiny-proxy.yml up -d

Kubernetes モードを試してみる

ShinyProxyをKubernetes(k8s)クラスタ内で起動させることで、Shinyコンテナをよりk8sクラスタ上で動かすことが可能になります。以下の公式のREADMEに沿ってやってみます。少し長い上にいくつかハマりポイントがあるので適宜補足します

参考 : https://github.com/openanalytics/shinyproxy-config-examples/tree/master/03-containerized-kubernetes

k8sクラスタの構築完了済みで、kubectlでクラスタを操作できるところまでは省略します。

ちなみに、k8sのことはググるか、Kubernetes完全ガイド impress top gearシリーズを読むことをおすすめします

Docker image for kube-proxy-sidecar のBuild

kube-proxy-sidecarはShinyProxyからk8s上のクラスタにShinyコンテナ(k8sでいうPod)を立ち上げるために、kubectlがインストールされたDocker imageです。

FROM alpine:3.6
ADD https://storage.googleapis.com/kubernetes-release/release/v1.17.0/bin/linux/amd64/kubectl /usr/local/bin/kubectl
RUN chmod +x /usr/local/bin/kubectl
EXPOSE 8001
ENTRYPOINT ["/usr/local/bin/kubectl", "proxy"]

Dockerfile作成後は、いつもどおりDocker imageをbuildしておきます。

$ docker build -t okiyuki/kube-proxy-sidecar:latest .

Docker image for ShinyProxy のBuild

Dockerfileは "Containerized モードを試してみる" で紹介したときと同じDockerfileなので省略します。

k8s用にapplication.ymlを変更しましょう。container-backendk8sに変更して、k8s用のapplication.ymlを用意します。以下の4つの箇所が変更ポイントです。

application.yml
proxy:
  <snip>
  container-backend: kubernetes
  <snip>

  # kubernetes configuration
  kubernetes:
    internal-networking: true       # <--- (1) add
    url: http://localhost:8001      # <--- (2) add
    namespace: example              # <--- (3) add
    image-pull-policy: IfNotPresent # <--- (4) add
  <snip>

(1) ~ (3)はk8s上でPod(Shinyコンテナ)を構築するために必要なものですので、一旦追加するものと考えてください。(4)はk8sのマニフェストで変更するため、ここでは特に不要ですが、Podを構築するホストがそのShinyコンテナのDocker Imageを持ってないときはDocker pullするよっていう意味です。明示的に書いています

あとはいつもどおりDocker imageをビルドします

$ docker build -t okiyuki/shiny-proxy-k8s:latest .

Roleリソースに関するマニフェストの適用

ここでは、テストのため cluster-adminのRole(何でもできるRole)をdefaultのService Accountに与えます。分析環境によってはRoleを制限する必要があると思いますので、以下を参考に適宜変更してください

参考 : shinyproxy-config-examples/03-containerized-kubernetes at master · openanalytics/shinyproxy-config-examples

authorization.yml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: shinyproxy-auth
subjects:
  - kind: ServiceAccount
    name: default
    namespace: default
  - kind: ServiceAccount
    name: default
    namespace: example
roleRef:
  kind: ClusterRole
  name: cluster-admin
  apiGroup: rbac.authorization.k8s.io

マニフェストを適用します。これでServiceAccountにClusterRoleBindingのRoleが付与されました

$ kubectl create -f authorization.yaml

Service と Deploymentリソースに関するマニフェストの適用

ここではもっともシンプルにnodePortを使って、外からk8sクラスタ上のShinyProxyコンテナが見えるように、30080番ポートを外にさらすようにServiceリソースを定義します

次にShinyProxyコンテナと最初に用意したkube-proxyコンテナをDeploymentリソースの上に定義します。8001番はShinyProxyのapplication.ymlでも記述したポート番号だったことを覚えていますでしょうか。これにより、8001番に立ったkube-proxyコンテナに命令を行い、Podを立てたり、消したりしてくれることが可能になります

shinyproxy.yml
$ cat dev/shinyproxy.yaml
apiVersion: v1
kind: Service
metadata:
  name: shinyproxy
  namespace: example
spec:
  type: NodePort
  selector:
    run: shinyproxy
  ports:
  - protocol: TCP
    port: 8080
    targetPort: 8080
    nodePort: 30080

---

apiVersion: apps/v1
kind: Deployment
metadata:
  name: shinyproxy
  namespace: example
spec:
  selector:
    matchLabels:
      run: shinyproxy
  replicas: 1
  template:
    metadata:
      labels:
        run: shinyproxy
    spec:
      containers:
      - name: shinyproxy
        image: okiyuki/shiny-proxy-k8s:latest
        imagePullPolicy: Always
        ports:
        - containerPort: 8080
      - name: kube-proxy-sidecar
        image: okiyuki/kube-proxy-sidecar:latest
        imagePullPolicy: Always
        ports:
        - containerPort: 8001

マニフェストを適用します。これでServiceとDeploymentが付与されました

$ kubectl create -f shinyproxy.yaml

ちなみに、replicas : 1 にしていますが、これを2以上に増やすことで、複数ノードに分散配置されますが、1点注意点があります。以下のSupportでも上がっていますが、ただ数字を増やすだけではうまく起動しません。というのも、ShinyProxy自体がステートフルなアプリであるため、Serviceリソース内でsticky sessionを有効にする必要があります。

イメージとしては、ブラウザからShinyProxyにアクセスしたときのセッションがどのノードとやり取りしてるものかを維持しないと、ShinyProxyがうまく動かないようです。必要な設定は、ServiceにsessionAffinity: ClientIPを追加すればとりあえずOKのようです。しかし、詳細な内容は調べてみてください(自分も理解が足りてないところがあります...)

参考 : Setting kubernetes pod fields and using multiple replica sets - ShinyProxy - Open Analytics Community Support

これで完了です!おつかれさまでした

おまけ

実際に使用するときに、他に設定したり試したりしたことを載せておきます

nginxの設定

指定したドメインからShinyProxyにアクセスする場合は、リバースプロキシとしてnginxを利用するのが良いかと思います。設定方法は、以下の記事がそのまま参考になるかと思います。

Docker Imageの用意

ShinyProxyで動かすShiny Appのdocker imageのtemplateは https://github.com/openanalytics/shinyproxy-template のURLが参考になるかと思います。Docker Imageの中に、Shiny Appに必要なRのライブラリをインストールしておき、ui.RなどのファイルをすべてCOPYで配置し、docker buildでdocker imageを作成すればOKかと思います

loginページのカスタマイズ(2019/08/15追記)

デフォルトのログインページのタイトル等を変えたいときは簡単に編集できます

  1. https://support.openanalytics.eu/t/login-page-customization/727 の議論を参考に、shinyproxyのjarファイルを解凍して、login.html をダウンロードします。タイトル等の中身を編集します
  2. https://github.com/openanalytics/shinyproxy-config-examples/tree/master/04-custom-html-templatetemplates/配下をダウンロードし、application.yml と同じ階層に置きます
  3. application.ymltemplate-path: templates/2col と記述します。そうすると、templates/2col/配下のテンプレートが自動で使用されます
  4. https://github.com/openanalytics/shinyproxy-config-examples/issues/6 のIssueであがってるとおり、なぜかlogin.htmlのテンプレートがtemplates/に入っていないので、1.で編集したlogin.htmltemplates/2col/に移動してきます
  5. shinyproxyを再起動して完了です

最終的なフォルダ構成は以下になってると思います

$ tree .
.
├── application.yml
└── templates
    └── 2col
        ├── assets
        │   ├── css
        │   │   └── 2-col-portfolio.css
        │   └── img
        │       ├── 01_hello.png
        │       └── 06_tabsets.png
        ├── index.html
        └── login.html

よくやりそうな application.yml の設定

Assets(CSSやJS)のLoadが失敗する503エラーの原因とは?

ShinyProxyを例えばChromeの開発者ツールを通してNetworkを見ていると、CSS等のファイルが503エラーによりロードされない事象に出くわすかもしれません(それもランダムに読めたり、読めなかったりします)。

これの原因は以下の2つ目のリンクのIssueで議論されていますが、httpsでの通信でHTTP/2を有効にしていると起こるようです。理由はまだShinyProxy or ShinyのPerformace限界でしょうか(?)。実際自分が運用しているShinyProxyでもHTTP/2設定をOFFにしたらこの手の503エラーが減りました。

LDAPを使ったuser levelのauthorizationは未実装

In the ShinyProxy Configurations how can I limit particular apps for a specific user - ShinyProxy - Open Analytics Community Support で議論されてますが、実装は進んでなさそう

おわりに

ユーザ数が多くなってきて、Shiny Appにスケールが必要になってくる場面の選択肢として、シンプルで使いやすいとても便利なプロダクトだと感じました。また、すでにk8sクラスタ上でよりスケールさせる仕組みもあるので、Shinyを運用することになる人はぜひそこまで挑戦してほしいと思います

31
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
18