# Deploying an ACI service with HTTPS and authentication

This document shows how you can deploy a fitted model as a web service to an Azure Container Instance, using the RestRserve 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.

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. 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.

library(bcrypt)

user_list <- list(
)
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.

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: 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")
}

# use try() construct to ensure robustness against malicious input
{
res <- FALSE
try({
}, 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)

## 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.

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. # 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")
}

#> [1] 25.9269 22.0636 34.1876 33.7737 34.8081 27.6394 21.8007 22.3577 16.7812 18.9785