How to Deploy a Rust API to Posit Connect
No matter your programming skill level - you can become a versatile developer or familiar with extending open source by familiarizing yourself with a few software engineering concepts and design patterns. In this post, I will present how I went about understanding the <a href="https://www.rplumber.io/" target="_blank" rel="noopener"><b>Plumber</b></a> code base, and show how you can extend it to <b>deploy a Rust API to Posit Connect</b>. TOC: <ul><li><a href="#connect">The Problem: Posit Connect and APIs</a></li><li><a href="#approach">Software Engineering Approach: Design Patterns and Concepts</a></li><li><a href="#apis">Posit Connect and Plumber APIs Written in R</a></li><li><a href="#httpserver">The Solution to Deploy Rust on Posit Connect</a></li><li><a href="#concepts">Using Software Engineering Concepts to Deploy Rust API on Posit Connect</a></li></ul> <hr /> <h2 id="connect"><strong>The Problem: Posit Connect and APIs</strong></h2> I am a big fan of <a href="https://www.rust-lang.org/">Rust</a>. I have been using it to build data intensive applications for a while now, mostly using <a href="https://actix.rs/">Actix</a>. For a recent project I wanted to use an API to interact with the data in a fast and type-safe manner, however the requirements of the project were such that I had to deploy the entirety of the application (a Shiny app front end and the Rust API) to a Posit Connect instance. This is where things got a bit tricky. <b>Posit Connect only has official support for Plumber APIs</b> which are written in R and some Python frameworks making it near-impossible to deploy an API written in other languages and frameworks without some work. <h2 id="approach"><strong>Software Engineering Approach: Design Patterns and Concepts</strong></h2> Software engineering is all about breaking down problems and understanding the common patterns and abstractions that are used to solve them. Here I will show you how I went about <b>breaking down the deployment of</b> <b>a Rust API to Posit Connect</b>. <h3><strong>Posit Connect and HTTP Servers</strong></h3> <a href="https://posit.co/products/enterprise/connect/?utm_medium=referral&utm_source=appsilon&utm_campaign=article" target="_blank" rel="noopener">Posit Connect offers</a> much more than a data app (Shiny apps, APIs, etc) deployment platform. However, for the purposes of this post, we will focus on the app deployment part. <h3><strong>How Does Posit Connect Serve Apps?</strong></h3> In short, it's a reverse proxy that forwards authenticated requests to an HTTP server running on the same machine. For APIs in R and Python, Posit Connect takes care of starting the HTTP server correctly, however for other languages and frameworks we will have to do this ourselves. <h2 id="apis"><strong>Posit Connect and Plumber APIs Written in R</strong></h2> So, how exactly does Posit Connect start the HTTP server for a Plumber API written in R? In order to answer this question we will have to take a look at the Plumber code base. Every Plumber API has an entry point which is just an R script that contains code that looks like this: <pre><code> library(plumber) # 'plumber.R' is the location of the file shown above pr("plumber.R") |> pr_run(port=8000)</code></pre> This code is pretty simple; it loads the Plumber library and then starts the HTTP server on port 8000. This must be pretty similar to how Posit Connect starts the HTTP server for a Plumber API, so let's take a look at the Plumber code base to see if we can find the code that does this. <h3><strong>Exploring the Plumber Code Base</strong></h3> Let's clone the <a href="https://github.com/rstudio/plumber" target="_blank" rel="noopener">Plumber Github repo</a> and immediately do a search for the definition of the pr_run function. <h4><strong>‘pr_run’ Function Definition</strong></h4> We can see that the <b><code>pr_run</code> function is defined in the <code>R/pr.R</code> file</b>. Let's take a look at the definition of the function. <pre><code> pr_run <- function(pr, host = '127.0.0.1', port = getOption('plumber.port', NULL), ..., debug = missing_arg(), docs = missing_arg(), swaggerCallback = missing_arg(), quiet = FALSE ) { validate_pr(pr) ellipsis::check_dots_empty() pr$run(host = host, port = port, debug = debug, docs = docs, swaggerCallback = swaggerCallback, quiet = quiet) } </code></pre> This function is pretty simple, it just <b>calls the <code>run</code> method on some <code>pr</code> object</b>. If we take a look back at our entry point, we see that the <b><code>pr</code> object is created by calling the <code>pr</code> function</b>. So, let's take a look at the definition of the pr function. <h4><strong>‘pr’ Function Definition</strong></h4> <pre><code> pr <- function(file = NULL, filters = defaultPlumberFilters, envir = new.env(parent = .GlobalEnv)) { Plumber$new(file = file, filters = filters, envir = envir) } </code></pre> We are getting closer! The <b><code>pr</code> object appears to be an instance of the <code>Plumber</code> class</b>. Now, let's look for the Plumber class definition. <h4><strong>‘Plumber’ Class Definition</strong></h4> The Plumber class has a very long definition, however we can see that<b> it inherits from the <code>Hookable</code> class</b>. So we need to take note of that. <pre><code> Plumber <- R6Class( "Plumber", inherit = Hookable, public = list( ... ) ) </code></pre> Since we know that the logic necessary for starting our HTTP server is somewhere in this class or maybe in one of its parent classes, let's look for a typical variable used when starting an HTTP server, such as port. <h4><strong>Finding the Variable for Starting the HTTP Server</strong></h4> After some quick digging, we find the run method which takes in a port argument. Lo and behold, we find that <b>it calls a <code>runServer</code> function</b>. We found it! <pre><code> run = function( host = '127.0.0.1', port = getOption('plumber.port', NULL), swagger = deprecated(), debug = missing_arg(), swaggerCallback = missing_arg(), ..., # any new args should go below `...` docs = missing_arg(), quiet = FALSE ) { ... httpuv::runServer(host, port, self) } </code></pre> At this point we have everything we need to trick Posit Connect into thinking that our Rust API is a Plumber API. We just need to <b>create a mock <code>Plumber</code> class that has a <code>run</code> method</b>. <h2 id="httpserver"><strong>The Solution to Deploy Rust on Posit Connect</strong></h2> Now that we know what we need to do, we can start writing some code. I started by creating a mock Plumber class that had a run method that started the Rust HTTP server on the specified port. In order for this to work, I compiled the Rust code into a binary in the same operating system that Posit Connect runs on and then bundled it with the R project. I then set up some command line arguments that would allow me to specify the host and port that the Rust HTTP server should run on. I also added some code that would make the binary executable. The code for the mock Plumber class is shown below: <pre><code> run = function( host = '127.0.0.1', port = getOption('plumber.port', NULL), swagger = deprecated(), debug = missing_arg(), swaggerCallback = missing_arg(), ..., # any new args should go below `...` docs = missing_arg(), quiet = FALSE ) { command <- paste0("./fetching_api --host=", host, " --port=", port) Sys.setenv("RUST_LOG" = "info") # Necessary only because the bundled binary is not executable system("chmod +x fetching_api") system(command) } </code></pre> <h3><strong>Mock Plumber API Error on Posit Connect</strong></h3> I then tried to deploy the API to Posit Connect and.. it didn't work. I got an error: <pre><code> 2023/07/18 10:37:54 AM: Starting R with process ID: '3129349' 2023/07/18 10:37:54 AM: Error in plumb(dir = getwd()) : 2023/07/18 10:37:54 AM: 'entrypoint.R' must return a runnable Plumber router. 2023/07/18 10:37:54 AM: Plumber API exiting ... </code></pre> As we can see, Posit Connect is not happy with our mock Plumber class. Since R is an interpreted language, in theory there should be no difference between our mock Plumber class. Perhaps Posit Connect is making use of other methods in the Plumber class. Because Posit Connect is not open source, we can't really know for sure. However, we can try to make our mock Plumber class more similar to the real Plumber class. <h3><strong>Mock ‘Hookable’ Class</strong></h3> So, I added a new <b>mock <code>Hookable</code> class that the mock <code>Plumber</code> class inherits from</b>. Then I added empty methods for all of the methods in the real Plumber class. Then I tried to deploy the API once again and.. it worked! I was able to deploy the API to Posit Connect and make requests to it. <pre><code> 2023/08/07 3:45:26 AM: Starting R with process ID: '755944' 2023/08/07 3:45:26 AM: [2023-08-07T08:45:26Z INFO actix_server::builder] starting 2 workers 2023/08/07 3:45:26 AM: [2023-08-07T08:45:26Z INFO actix_server::server] Actix runtime found; starting in Actix runtime </code></pre> <img class="aligncenter size-full wp-image-20598" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b01abe4c2c8ea263ddaab6_Rust-API-connect-deployed-to-Posit-Connect-and-requests.webp" alt="Rust API connect deployed to Posit Connect and requests" width="1600" height="525" /> Now we can take advantage of all of the features that Posit Connect has to offer, such as API versioning, authentication and more. We can also use the Posit Connect UI to monitor the API and view logs. <h2 id="concepts"><strong>Using Software Engineering Concepts to Deploy Rust API on Posit Connect</strong></h2> The main takeaway from this post is that understanding <b>software engineering concepts and design patterns can be very useful</b> when trying to solve problems. In this case, we were able to solve the problem of deploying an unsupported API by understanding how reverse proxy servers work and identifying typical design patterns used in starting HTTP servers. Ultimately, this allowed us to logically traverse and understand the Plumber code base. Even if you are a data scientist without formal development training, understanding these concepts can be very useful in order to become a versatile developer. Of course, if you still need assistance, we are here to help. <a href="https://appsilon.com/#contact">Reach out</a> and make the most out of your deployment experience.