--- title: "Deploying an ACI service with HTTPS and authentication" Author: Hong Ooi output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{RestRserve model deployment to ACI} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{utf8} --- This document shows how you can deploy a fitted model as a web service to an Azure Container Instance, using the [RestRserve](https://restrserve.org) package. RestRserve has a number of features that can make it more suitable than Plumber for building robust, production-ready services. These include: - Automatic parallelisation, based on the Rserve backend - Support for HTTPS - Support for basic and bearer HTTP authentication schemes In particular, we'll show how to implement the latter two features in this vignette. ## Deployment artifacts ### Model object For illustrative purposes, we'll reuse the random forest model and resource group from the Plumber deployment vignette. The code to fit the model is reproduced below for convenience. ```r data(Boston, package="MASS") library(randomForest) # train a model for median house price as a function of the other variables bos_rf <- randomForest(medv ~ ., data=Boston, ntree=100) # save the model saveRDS(bos_rf, "bos_rf.rds") ``` Basic authentication requires that we provide a list of usernames and passwords that grant access to the service. In a production setting, you would typically query a database, directory service or other backing store to authenticate users. To keep this example simple, we'll just create a flat file in the standard [Apache `.htpasswd` format](https://en.wikipedia.org/wiki/.htpasswd). In this format, the passwords are encrypted using a variety of algorithms, as a security measure; we'll use the bcrypt algorithm since an R implementation is available in the package of that name. ```r library(bcrypt) user_list <- list( c("user1", "password1"), c("user2", "password2") ) user_str <- sapply(user_list, function(x) paste(x[1], hashpw(x[2]), sep=":")) writeLines(user_str, ".htpasswd") ``` ### TLS certificate/private key To enable HTTPS, we need to provide a TLS certificate and private key. Again, in a production setting, the cert will typically be provided to you; for this vignette, we'll generate a self-signed cert instead. If you are running Linux or MacOS and have openssl installed, you can use that to generate the cert. Here, since we're already using Azure, we'll leverage the Azure Key Vault service to do it in a platform-independent manner. ```r library(AzureRMR) library(AzureContainers) library(AzureKeyVault) deployresgrp <- AzureRMR::get_azure_login()$ get_subscription("sub_id")$ get_resource_group("deployresgrp") # create the key vault vault_res <- deployresgrp$create_key_vault("mykeyvault") # get the vault endpoint kv <- vault_res$get_endpoint() # generate the certificate: use the DNS name of the ACI container endpoint kv$certificates$create( "deployrrsaci", "CN=deployrrsaci", x509=cert_x509_properties(dns_names=c("deployrrsaci.australiaeast.azurecontainer.io")) ) secret <- kv$secrets$get("deployrrsaci") key <- sub("-----BEGIN CERTIFICATE-----.*$", "", secret$value) cer <- sub("^.*-----END PRIVATE KEY-----\n", "", secret$value) writeLines(key, "cert.key") writeLines(cer, "cert.cer") ``` ### App Unlike Plumber, in RestRserve you define your service in R code, as a web app. An app is an object of R6 class `Application`: it contains various middleware and backend objects, and exposes the endpoint paths for your service. The overall server backend is of R6 class `BackendRserve`, and has responsibility for running and managing the app. The script below defines an app that exposes the scoring function on the `/score` path. Save this as `app.R`: ```r library(RestRserve) library(randomForest) bos_rf <- readRDS("bos_rf.rds") users <- local({ usr <- read.table(".htpasswd", sep=":", stringsAsFactors=FALSE) structure(usr[[2]], names=usr[[1]]) }) # scoring function: calls predict() on the provided dataset # - input is a jsonified data frame, in the body of a POST request # - output is the predicted values score <- function(request, response) { df <- jsonlite::fromJSON(rawToChar(request$body), simplifyDataFrame=TRUE) sc <- predict(bos_rf, df) response$set_body(jsonlite::toJSON(sc, auto_unbox=TRUE)) response$set_content_type("application/json") } # basic authentication against provided username/password values # use try() construct to ensure robustness against malicious input authenticate <- function(user, password) { res <- FALSE try({ res <- bcrypt::checkpw(password, users[[user]]) }, silent=TRUE) res } # chain of objects for app auth_backend <- AuthBackendBasic$new(FUN=authenticate) auth_mw <- AuthMiddleware$new(auth_backend=auth_backend, routes="/score") app <- Application$new(middleware=list(auth_mw)) app$add_post(path="/score", FUN=score) backend <- BackendRserve$new(app) ``` ### Dockerfile Here is the dockerfile for the image. Save this as `RestRserve-aci.dockerfile`: ```dockerfile FROM rexyai/restrserve # install required packages RUN Rscript -e "install.packages(c('randomForest', 'bcrypt'), repos='https://cloud.r-project.org')" # copy model object, cert files, user file and app script RUN mkdir /data COPY bos_rf.rds /data COPY .htpasswd /data COPY cert.cer /data COPY cert.key /data COPY app.R /data WORKDIR /data EXPOSE 8080 CMD ["Rscript", "-e", "source('app.R'); backend$start(app, http_port=-1, https.port=8080, tls.key=normalizePath('cert.key'), tls.cert=normalizePath('cert.cer'))"] ``` ## Create the container We now build the image and upload it to an Azure Container Registry. This assumes a fresh start; if you have created an ACR in this resource group already, you can reuse that instead by calling `get_acr` instead of `create_acr`. ```r call_docker("build -t rrs-aci -f RestRserve-aci.dockerfile .") deployreg_svc <- deployresgrp$create_acr("deployreg") deployreg <- deployreg_svc$get_docker_registry(as_admin=TRUE) deployreg$push("rrs-aci") ``` We can now deploy the image to ACI and obtain predicted values from the RestRserve app. Because we used a self-signed certificate in this example, we need to turn off the SSL verification check that curl performs by default. There may also be a short delay from when the container is started, to when the app is ready to accept requests. ```r # ensure the name of the resource matches the one on the cert we obtained above deployresgrp$create_aci("deployrrsaci", image="deployreg.azurecr.io/bos-rrs-https", registry_creds=deployreg, cores=2, memory=8, ports=aci_ports(8080)) Sys.sleep(30) # tell curl not to verify the cert unverified_handle <- function() { structure(list( handle=curl::handle_setopt(curl::new_handle(), ssl_verifypeer=FALSE), url="https://deployrrsaci.australiaeast.azurecontainer.io"), class="handle") } # send the username and password as part of the request response <- httr::POST("https://deployrrsaci.australiaeast.azurecontainer.io:8080/score", httr::authenticate("user1", "password1"), body=MASS::Boston[1:10, ], encode="json", handle=unverified_handle()) httr::content(response, simplifyVector=TRUE) #> [1] 25.9269 22.0636 34.1876 33.7737 34.8081 27.6394 21.8007 22.3577 16.7812 18.9785 ```