<p><div><div><div><div data-show-faces="false" data-height="21" data-width="100" data-layout="button_count" data-send="true" data-href="https://www.r-bloggers.com/how-to-deploy-a-predictive-service-to-kubernetes-with-r-and-the-azurecontainers-package/"></div></div><div></div></div><div></div></div>
<div>
(This article was first published on <strong><a href="https://blog.revolutionanalytics.com/2018/12/azurecontainers.html"> Revolutions</a></strong>, and kindly contributed to <a rel="nofollow" href="https://www.r-bloggers.com/">R-bloggers)</a> </div>
<div>
<p>It's easy to create a function in R, but what if you want to call that function from a different application, with the scale to support a large number of simultaneous requests? This article shows how you can deploy an R fitted model as a <a rel="nofollow" href="https://www.rplumber.io/" target="_blank">Plumber web service</a> in Kubernetes, using <a rel="nofollow" href="https://docs.microsoft.com/azure/container-registry/?WT.mc_id=Revolutions-blog-davidsmi" target="_blank">Azure Container Registry</a> (ACR) and <a rel="nofollow" href="https://docs.microsoft.com/en-us/azure/aks/?WT.mc_id=Revolutions-blog-davidsmi" target="_blank">Azure Kubernetes Service</a> (AKS). We use the <a rel="nofollow" href="https://github.com/cloudyr/AzureContainers" target="_blank">AzureContainers</a> package to create the necessary resources and deploy the service.</p>
<div>
<h2>Fit the model</h2>
<p>We’ll fit a simple model for illustrative purposes, using the <a rel="nofollow" href="https://www.rdocumentation.org/packages/MASS/versions/7.3-51.1/topics/Boston?tap_a=5644-dce66f&tap_s=10907-287229" target="_blank">Boston housing dataset</a> (which ships with R in the MASS package). To make the deployment process more interesting, the model we fit will be a random forest, using the randomForest package. This is <em>not</em> part of R, so we’ll have to install it from CRAN.</p>
<div>
<pre><code><span>data</span>(Boston, <span>package=</span><span>&quot;MASS&quot;</span>)
<span>install.packages</span>(<span>&quot;randomForest&quot;</span>)
<span>library</span>(randomForest)

<span># train a model for median house price</span>
bos_rf &lt;- <span>randomForest</span>(medv ~ ., <span>data=</span>Boston, <span>ntree=</span><span>100</span>)

<span># save the model</span>
<span>saveRDS</span>(bos.rf, <span>&quot;bos_rf.rds&quot;</span>)</code></pre>
</div>
</div>
<div>
<h2>Scoring script for plumber</h2>
<p>Now that we have the model, we also need a script to obtain predicted values from it given a set of inputs:</p>
</div>
<div>
<pre><code><span># save as bos_rf_score.R</span>

bos_rf &lt;- <span>readRDS</span>(<span>&quot;bos_rf.rds&quot;</span>)
<span>library</span>(randomForest)

<span>#* @param df data frame of variables</span>
<span>#* @post /score</span>
function(req, df)
{
    df &lt;- <span>as.data.frame</span>(df)
    <span>predict</span>(bos_rf, df)
}</code></pre>
</div>
<p>This is fairly straightforward, but the comments may require some explanation. They are plumber annotations that tell it to call the function if the server receives a HTTP POST request with the path <code>/score</code>, and query parameter <code>df</code>. The value of the <code>df</code> parameter is then converted to a data frame, and passed to the randomForest <code>predict</code> method. For a fuller description of how Plumber works, see the <a rel="nofollow" href="https://rplumber.io/" target="_blank">Plumber website</a>.</p>
<div>
<h2>Create a Dockerfile</h2>
<p>Let’s package up the model and the scoring script into a Docker image. A Dockerfile to do this is shown below. This uses the base image supplied by Plumber (<code>trestletech/plumber</code>), installs randomForest, and then adds the model and the above scoring script. Finally, it runs the code that will start the server and listen on port 8000.</p>
<pre><code># example Dockerfile to expose a plumber service

FROM trestletech/plumber

# install the randomForest package
RUN R -e 'install.packages(c(&quot;randomForest&quot;))'

# copy model and scoring script
RUN mkdir /data
COPY bos_rf.rds /data
COPY bos_rf_score.R /data
WORKDIR /data

# plumb and run server
EXPOSE 8000
ENTRYPOINT [&quot;R&quot;, &quot;-e&quot;, \
    &quot;pr &lt;- plumber::plumb('/data/bos_rf_score.R'); \
    pr$run(host='0.0.0.0', port=8000)&quot;]</code></pre>
</div>
<div>
<h2>Build and upload the image</h2>
<p>The code to store our image on Azure Container Registry is as follows. This calls <a rel="nofollow" href="https://cran.r-project.org/package=AzureRMR" target="_blank">AzureRMR</a> to login to Resource Manager, creates an Azure Container Registry resource (a Docker registry hosted in Azure), and then pushes the image to the registry.</p>
<p>If this is the first time you are using AzureRMR, you’ll have to create a service principal first. For more information on how to do this, see the <a rel="nofollow" href="https://github.com/cloudyr/AzureRMR" target="_blank">AzureRMR readme</a>.</p>
<div>
<pre><code><span>library</span>(AzureContainers)

az &lt;- AzureRMR::az_rm$<span>new</span>(
    <span>tenant=</span><span>&quot;myaadtenant.onmicrosoft.com&quot;</span>,
    <span>app=</span><span>&quot;app_id&quot;</span>,
    <span>password=</span><span>&quot;password&quot;</span>)

<span># create a resource group for our deployments</span>
deployresgrp &lt;- az$
    <span>get_subscription</span>(<span>&quot;subscription_id&quot;</span>)$
    <span>create_resource_group</span>(<span>&quot;deployresgrp&quot;</span>, <span>location=</span><span>&quot;australiaeast&quot;</span>)

<span># create container registry</span>
deployreg_svc &lt;- deployresgrp$<span>create_acr</span>(<span>&quot;deployreg&quot;</span>)

<span># build image 'bos_rf'</span>
<span>call_docker</span>(<span>&quot;build -t bos_rf .&quot;</span>)

<span># upload the image to Azure</span>
deployreg &lt;- deployreg_svc$<span>get_docker_registry</span>()
deployreg$<span>push</span>(<span>&quot;bos_rf&quot;</span>)</code></pre>
</div>
<p>If you run this code, you should see a lot of output indicating that R is downloading, compiling and installing randomForest, and finally that the image is being pushed to Azure. (You will see this output even if your machine already has the randomForest package installed. This is because the package is being installed to the R session <em>inside the container</em>, which is distinct from the one running the code shown here.)</p>
<p>All docker calls in AzureContainers, like the one to build the image, return the actual docker commandline as the <code>cmdline</code> attribute of the (invisible) returned value. In this case, the commandline is <code>docker build -t bos_rf .</code> Similarly, the <code>push()</code> method actually involves two Docker calls, one to retag the image, and the second to do the actual pushing; the returned value in this case will be a 2-component list with the command lines being <code>docker tag bos_rf deployreg.azurecr.io/bos_rf</code> and <code>docker push deployreg.azurecr.io/bos_rf</code>.</p>
</div>
<div>
<h2>Deploy to a Kubernetes cluster</h2>
<p>The code to create an AKS resource (a managed Kubernetes cluster in Azure) is quite simple:</p>
<div>
<pre><code><span># create a Kubernetes cluster with 2 nodes, running Linux</span>
deployclus_svc &lt;- deployresgrp$<span>create_aks</span>(<span>&quot;deployclus&quot;</span>,
    <span>agent_pools=</span><span>aks_pools</span>(<span>&quot;pool1&quot;</span>, <span>2</span>))</code></pre>
</div>
<p>Creating a Kubernetes cluster can take several minutes. By default, the <code>create_aks()</code> method will wait until the cluster provisioning is complete before it returns.</p>
<p>Having created the cluster, we can deploy our model and create a service. We’ll use a YAML configuration file to specify the details for the deployment and service API.</p>
<pre><code>apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: bos-rf
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: bos-rf
    spec:
      containers:
      - name: bos-rf
        image: deployreg.azurecr.io/bos_rf
        ports:
        - containerPort: 8000
        resources:
          requests:
            cpu: 250m
          limits:
            cpu: 500m
      imagePullSecrets:
      - name: deployreg.azurecr.io
---
apiVersion: v1
kind: Service
metadata:
  name: bos-rf-svc
spec:
  selector:
    app: bos-rf
  type: LoadBalancer
  ports:
  - protocol: TCP
    port: 8000</code></pre>
<p>The following code will obtain the cluster endpoint from the AKS resource and then deploy the image and service to the cluster. The configuration details for the <code>deployclus</code> cluster are stored in a file located in the R temporary directory; all of the cluster’s methods will use this file. Unless told otherwise, AzureContainers does not touch your default Kubernetes configuration (<code>~/kube/config</code>).</p>
<div>
<pre><code><span># get the cluster endpoint</span>
deployclus &lt;- deployclus_svc$<span>get_cluster</span>()

<span># pass registry authentication details to the cluster</span>
deployclus$<span>create_registry_secret</span>(deployreg,
    <span>email=</span><span>&quot;me@example.com&quot;</span>)

<span># create and start the service</span>
deployclus$<span>create</span>(<span>&quot;bos_rf.yaml&quot;</span>)</code></pre>
</div>
<p>To check on the progress of the deployment, run the <code>get()</code> methods specifying the type and name of the resource to get information on. As with Docker, these correspond to calls to the <code>kubectl</code> commandline tool, and again, the actual commandline is stored as the <code>cmdline</code> attribute of the returned value.</p>
<div>
<pre><code>deployclus$<span>get</span>(<span>&quot;deployment bos-rf&quot;</span>)
<span>#> Kubernetes operation: get deployment bos-rf --kubeconfig=&quot;.../kubeconfigxxxx&quot;</span>
<span>#> NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE</span>
<span>#> bos-rf 1 1 1 1 5m</span>

deployclus$<span>get</span>(<span>&quot;service bos-rf-svc&quot;</span>)
<span>#> Kubernetes operation: get service bos-rf-svc --kubeconfig=&quot;.../kubeconfigxxxx&quot;</span>
<span>#> NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE</span>
<span>#> bos-rf-svc LoadBalancer 10.0.8.189 52.187.249.58 8000:32276/TCP 5m </span></code></pre>
</div>
<p>Once the service is up and running, as indicated by the presence of an external IP in the service details, let’s test it with a HTTP request. The response should look like this.</p>
<div>
<pre><code>response &lt;- httr::<span>POST</span>(<span>&quot;http://52.187.249.58:8000/score&quot;</span>,
    <span>body=</span><span>list</span>(<span>df=</span>MASS::Boston[<span>1</span>:<span>10</span>,]), <span>encode=</span><span>&quot;json&quot;</span>)
httr::<span>content</span>(response, <span>simplifyVector=</span><span>TRUE</span>)
<span>#> [1] 25.9269 22.0636 34.1876 33.7737 34.8081 27.6394 21.8007 22.3577 16.7812 18.9785</span></code></pre>
</div>
<p>Finally, once we are done, we can tear down the service and deployment:</p>
<div>
<pre><code>deployclus$<span>delete</span>(<span>&quot;service&quot;</span>, <span>&quot;bos-rf-svc&quot;</span>)
deployclus$<span>delete</span>(<span>&quot;deployment&quot;</span>, <span>&quot;bos-rf&quot;</span>)</code></pre>
</div>
<p>And if required, we can also delete all the resources created here, by simply deleting the resource group (AzureContainers will prompt you for confirmation):</p>
<div>
<pre><code>deployresgrp$<span>delete</span>()</code></pre>
</div>
</div>
<div>
<h2>See also</h2>
<p>An alternative to Plumber is the <a rel="nofollow" href="https://docs.microsoft.com/en-us/machine-learning-server/what-is-operationalization" target="_blank">model operationalisation</a> framework found in <a rel="nofollow" href="https://www.microsoft.com/en-us/sql-server/machinelearningserver" target="_blank">Microsoft Machine Learning Server</a>. While it is proprietary software, unlike Plumber which is open source, ML Server provides a number of features not available in the latter. These include model management, so that you can easily access multiple versions of a given model; user authentication, so that only authorised users can access your service; and batch (asynchronous) requests. For more information, see the MMLS documentation.</p>
</div>
</div>
	<p>
<div>
<div>To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://blog.revolutionanalytics.com/2018/12/azurecontainers.html"> Revolutions</a></strong>.</div>
<hr>
<a rel="nofollow" href="https://www.r-bloggers.com/">R-bloggers.com</a> offers <strong><a rel="nofollow" href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers">daily e-mail updates</a></strong> about <a rel="nofollow" href="https://www.r-project.org/" title="The R Project for Statistical Computing">R</a> news and <a rel="nofollow" href="https://www.r-bloggers.com/search/tutorial" title="R tutorials">tutorials</a> on topics such as: <a rel="nofollow" href="https://www.r-bloggers.com/search/data%20science" title="Data science">Data science</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/Big%20Data" title="Big Data">Big Data, <a rel="nofollow" href="https://www.r-users.com/" title="R jobs">R jobs</a>, visualization (<a rel="nofollow" href="https://www.r-bloggers.com/search/ggplot2" title="ggplot and ggplot2 tutorials">ggplot2</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/boxplot" title="Boxplots using lattice and ggplot2 tutorials">Boxplots</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/map" title="Maps and gis">maps</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/animation" title="Animation in R">animation</a>), programming (<a rel="nofollow" href="https://www.r-bloggers.com/search/RStudio" title="RStudio IDE for R">RStudio</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/sweave" title="Sweave and literate programming">Sweave</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/LaTeX" title="LaTeX in R">LaTeX</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/SQL" title="SQL and databases">SQL</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/eclipse" title="Eclipse IDE for R">Eclipse</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/git" title="git and github, Version Control System">git</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/hadoop" title="Large data in R using Hadoop">hadoop</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/Web+Scraping" title="Web Scraping of google, facebook, yahoo, twitter and more using R">Web Scraping</a>) statistics (<a rel="nofollow" href="https://www.r-bloggers.com/search/regression" title="Regressions and ANOVA analysis tutorials">regression</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/PCA" title="principal component analysis tutorial">PCA</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/time+series" title="Time series">time series</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/trading" title="finance trading">trading</a>) and more...
<img width="1" alt="" src="http://feeds.feedburner.com/~r/RBloggers/~4/fAIYE4nhZyM" height="1"></a></div>