更新履歴
- 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
- バグや質問はここでもよく議論されている : https://support.openanalytics.eu/c/shinyproxy/l/latest?no_subcategories=false&page=5
Github
- ConfigurationのExamples集 : https://github.com/openanalytics/shinyproxy-config-examples
- Demoで使用するshiny Appのdocker image : https://github.com/openanalytics/shinyproxy-demo
- ShinyProxyで動かすShiny Appのdocker imageのtemplate : https://github.com/openanalytics/shinyproxy-template
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の一覧が以下のように見れます
例えば、ここで Hello Applicationにアクセスしてみます。
そうすると、以下のような Launching ... という画面が数秒見えたあとで、
Hello ApplicationのShiny Appが立ち上がります
ここで、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に沿ってやってみます
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箇所を変更します
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-backend
をk8s
に変更して、k8s用のapplication.ymlを用意します。以下の4つの箇所が変更ポイントです。
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を制限する必要があると思いますので、以下を参考に適宜変更してください
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を立てたり、消したりしてくれることが可能になります
$ 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のようです。しかし、詳細な内容は調べてみてください(自分も理解が足りてないところがあります...)
これで完了です!おつかれさまでした
おまけ
実際に使用するときに、他に設定したり試したりしたことを載せておきます
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追記)
デフォルトのログインページのタイトル等を変えたいときは簡単に編集できます
-
https://support.openanalytics.eu/t/login-page-customization/727 の議論を参考に、shinyproxyのjarファイルを解凍して、
login.html
をダウンロードします。タイトル等の中身を編集します -
https://github.com/openanalytics/shinyproxy-config-examples/tree/master/04-custom-html-template の
templates/
配下をダウンロードし、application.yml
と同じ階層に置きます -
application.yml
にtemplate-path: templates/2col
と記述します。そうすると、templates/2col/
配下のテンプレートが自動で使用されます -
https://github.com/openanalytics/shinyproxy-config-examples/issues/6 のIssueであがってるとおり、なぜか
login.html
のテンプレートがtemplates/
に入っていないので、1.で編集したlogin.html
をtemplates/2col/
に移動してきます - 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
の設定
-
application.yml
で、authentication: none
にすることで、認証を無くすこともできる。認証機能が不要な場合はこれで十分かと思われる - topにあるnavigation bar が不要だなと思ったら、
application.yml
でhide-navbar: true
に設定すればOK。これで見た目は普通のShiny Serverのように使えるので、ユーザは違和感を覚えないと思います - faviconの設定方法
Assets(CSSやJS)のLoadが失敗する503エラーの原因とは?
ShinyProxyを例えばChromeの開発者ツールを通してNetworkを見ていると、CSS等のファイルが503エラーによりロードされない事象に出くわすかもしれません(それもランダムに読めたり、読めなかったりします)。
これの原因は以下の2つ目のリンクのIssueで議論されていますが、httpsでの通信でHTTP/2
を有効にしていると起こるようです。理由はまだShinyProxy or ShinyのPerformace限界でしょうか(?)。実際自分が運用しているShinyProxyでもHTTP/2
設定をOFFにしたらこの手の503エラーが減りました。
- Assets don’t load (HTTP 503) with ShinyProxy behind AWS Load Balancer
- 503 Response being returned from Shinyproxy to Loadbalancer
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を運用することになる人はぜひそこまで挑戦してほしいと思います