├── .Rbuildignore ├── .gitignore ├── Dockerfile ├── LICENSE.md ├── README.md ├── functions.R ├── r-on-lambda.Rproj └── runtime.R /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^LICENSE\.md$ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | temp 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/lambda/provided 2 | 3 | ENV R_VERSION=4.0.3 4 | 5 | RUN yum -y install wget 6 | 7 | RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm \ 8 | && wget https://cdn.rstudio.com/r/centos-7/pkgs/R-${R_VERSION}-1-1.x86_64.rpm \ 9 | && yum -y install R-${R_VERSION}-1-1.x86_64.rpm \ 10 | && rm R-${R_VERSION}-1-1.x86_64.rpm 11 | 12 | ENV PATH="${PATH}:/opt/R/${R_VERSION}/bin/" 13 | 14 | # System requirements for R packages 15 | RUN yum -y install openssl-devel 16 | 17 | RUN Rscript -e "install.packages(c('httr', 'jsonlite', 'logger'), repos = 'https://cloud.r-project.org/')" 18 | 19 | COPY runtime.R functions.R ${LAMBDA_TASK_ROOT}/ 20 | RUN chmod 755 -R ${LAMBDA_TASK_ROOT}/ 21 | 22 | RUN printf '#!/bin/sh\ncd $LAMBDA_TASK_ROOT\nRscript runtime.R' > /var/runtime/bootstrap \ 23 | && chmod +x /var/runtime/bootstrap 24 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2020 r-on-lambda authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # R on AWS Lambda 2 | 3 | This is an attempt to get an R runtime and function working on AWS Lambda using a container. [My blog post on this subject is available here](https://mdneuzerling.com/post/r-on-aws-lambda-with-containers/), but the code has since been updated. 4 | 5 | Thanks to @rensa for contributions to the error handling code. 6 | -------------------------------------------------------------------------------- /functions.R: -------------------------------------------------------------------------------- 1 | #' Determine if the given integer is even or odd 2 | #' 3 | #' @param number Integer 4 | #' 5 | #' @return "even" or "odd" 6 | #' @export 7 | #' 8 | #' @examples 9 | #' parity(3) # odd 10 | #' parity(4) # even 11 | parity <- function(number) { 12 | list(parity = if (as.integer(number) %% 2 == 0) "even" else "odd") 13 | } 14 | 15 | #' A nullary function that returns the current version of R 16 | #' 17 | #' @return character 18 | #' @export 19 | #' 20 | #' @examples 21 | #' hello() 22 | hello <- function() { 23 | list(response = paste("Hello from", version$version.string)) 24 | } 25 | -------------------------------------------------------------------------------- /r-on-lambda.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: No 4 | SaveWorkspace: No 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | LineEndingConversion: Posix 18 | 19 | BuildType: Package 20 | PackageUseDevtools: Yes 21 | PackageInstallArgs: --no-multiarch --with-keep.source 22 | PackageRoxygenize: rd,collate,namespace 23 | -------------------------------------------------------------------------------- /runtime.R: -------------------------------------------------------------------------------- 1 | library(httr) 2 | library(logger) 3 | log_formatter(formatter_paste) 4 | log_threshold(INFO) 5 | 6 | #' Convert a list to a single character, preserving names 7 | #' prettify_list(list("a" = 1, "b" = 2, "c" = 3)) 8 | #' # "a=5, b=5, c=5" 9 | prettify_list <- function(x) { 10 | paste( 11 | paste(names(x), x, sep = "="), 12 | collapse = ", " 13 | ) 14 | } 15 | 16 | # error handling with http codes 17 | # from http://adv-r.had.co.nz/Exceptions-Debugging.html 18 | condition <- function(subclass, message, code, call = sys.call(-1), ...) { 19 | structure( 20 | class = c(subclass, "condition"), 21 | list(message = message, code = code, call = call), 22 | ... 23 | ) 24 | } 25 | stop_api <- function(message, code = 500, call = sys.call(-1), ...) { 26 | stop(condition(c("api_error", "error"), message, code = code, call = call, 27 | ...)) 28 | } 29 | 30 | log_debug("Deriving lambda runtime API endpoints from environment variables") 31 | lambda_runtime_api <- Sys.getenv("AWS_LAMBDA_RUNTIME_API") 32 | if (lambda_runtime_api == "") { 33 | error_message <- "AWS_LAMBDA_RUNTIME_API environment variable undefined" 34 | log_error(error_message) 35 | stop(error_message) 36 | } 37 | next_invocation_endpoint <- paste0( 38 | "http://", lambda_runtime_api, "/2018-06-01/runtime/invocation/next" 39 | ) 40 | initialisation_error_endpoint <- paste0( 41 | "http://", lambda_runtime_api, "/2018-06-01/runtime/init/error" 42 | ) 43 | 44 | tryCatch( 45 | { 46 | log_debug("Determining handler from environment variables") 47 | handler <- Sys.getenv("_HANDLER") 48 | if (is.null(handler) || handler == "") { 49 | stop_api("_HANDLER environment variable undefined") 50 | } 51 | log_info("Handler found:", handler) 52 | handler_split <- strsplit(handler, ".", fixed = TRUE)[[1]] 53 | file_name <- paste0(handler_split[1], ".R") 54 | function_name <- handler_split[2] 55 | log_info("Using function", function_name, "from", file_name) 56 | 57 | log_debug("Checking if", file_name, "exists") 58 | if (!file.exists(file_name)) { 59 | stop_api(file_name, " doesn't exist in ", getwd()) 60 | } 61 | source(file_name) 62 | 63 | log_debug("Checking if", function_name, "is defined") 64 | if (!exists(function_name)) { 65 | stop_api("Function name ", function_name, " isn't defined in R") 66 | } 67 | log_debug("Checking if", function_name, "is a function") 68 | if (!is.function(eval(parse(text = function_name)))) { 69 | stop_api("Function name ", function_name, " is not a function") 70 | } 71 | }, 72 | api_error = function(e) { 73 | log_error(as.character(e)) 74 | POST( 75 | url = initialisation_error_endpoint, 76 | body = list( 77 | statusCode = e$code, 78 | error_message = as.character(e$message)), 79 | encode = "json" 80 | ) 81 | stop(e) 82 | } 83 | ) 84 | 85 | handle_event <- function(event) { 86 | status_code <- status_code(event) 87 | log_debug("Status code:", status_code) 88 | if (status_code != 200) { 89 | stop_api("Didn't get status code 200. Status code: ", status_code, 90 | code = 400) 91 | } 92 | event_headers <- headers(event) 93 | 94 | # HTTP headers are case-insensitive 95 | names(event_headers) <- tolower(names(event_headers)) 96 | log_debug("Event headers:", prettify_list(event_headers)) 97 | 98 | aws_request_id <- event_headers[["lambda-runtime-aws-request-id"]] 99 | if (is.null(aws_request_id)) { 100 | stop_api("Could not find lambda-runtime-aws-request-id header in event", 101 | code = 400) 102 | } 103 | 104 | # According to the AWS guide, the below is used by "X-Ray SDK" 105 | runtime_trace_id <- event_headers[["lambda-runtime-trace-id"]] 106 | if (!is.null(runtime_trace_id)) { 107 | Sys.setenv("_X_AMZN_TRACE_ID" = runtime_trace_id) 108 | } 109 | 110 | # we need to parse the event in four contexts before sending to the lambda fn: 111 | # 1a) direct invocation with no function args (empty event) 112 | # 1b) direct invocation with function args (parse and send entire event) 113 | # 2a) api endpoint with no args (parse HTTP request, confirm null request 114 | # element; send empty list) 115 | # 2b) api endpoint with args (parse HTTP request, confirm non-null request 116 | # element; extract and send it) 117 | 118 | unparsed_content <- httr::content(event, "text", encoding = "UTF-8") 119 | # Thank you to Menno Schellekens for this fix for Cloudwatch events 120 | is_scheduled_event <- grepl("Scheduled Event", unparsed_content) 121 | if(is_scheduled_event) log_info("Event type is scheduled") 122 | log_debug("Unparsed content:", unparsed_content) 123 | if (unparsed_content == "" || is_scheduled_event) { 124 | # (1a) direct invocation with no args (or scheduled request) 125 | event_content <- list() 126 | } else { 127 | # (1b, 2a or 2b) 128 | event_content <- jsonlite::fromJSON(unparsed_content) 129 | } 130 | 131 | # if you want to do any additional inspection of the event body (including 132 | # other http request elements if it's an endpoint), you can do that here! 133 | 134 | # change `http_req_element` if you'd prefer to send the http request `body` to 135 | # the lambda fn, rather than the query parameters 136 | # (note that query string params are always strings! your lambda fn may need to 137 | # convert them back to numeric/logical/Date/etc.) 138 | is_http_req <- FALSE 139 | http_req_element <- "queryStringParameters" 140 | 141 | if (http_req_element %in% names(event_content)) { 142 | is_http_req <- TRUE 143 | if (is.null(event_content[[http_req_element]])) { 144 | # (2a) api request with no args 145 | event_content <- list() 146 | } else { 147 | # (2b) api request with args 148 | event_content <- event_content[[http_req_element]] 149 | } 150 | } 151 | 152 | result <- do.call(function_name, event_content) 153 | log_debug("Result:", as.character(result)) 154 | response_endpoint <- paste0( 155 | "http://", lambda_runtime_api, "/2018-06-01/runtime/invocation/", 156 | aws_request_id, "/response" 157 | ) 158 | # aws api gateway is a bit particular about the response format 159 | body <- if (is_http_req) { 160 | list( 161 | isBase64Encoded = FALSE, 162 | statusCode = 200L, 163 | body = as.character(jsonlite::toJSON(result, auto_unbox = TRUE)) 164 | ) 165 | } else { 166 | result 167 | } 168 | POST( 169 | url = response_endpoint, 170 | body = body, 171 | encode = "json" 172 | ) 173 | rm("aws_request_id") # so we don't report errors to an outdated endpoint 174 | } 175 | 176 | log_info("Querying for events") 177 | while (TRUE) { 178 | tryCatch( 179 | { 180 | event <- GET(url = next_invocation_endpoint) 181 | log_debug("Event received") 182 | handle_event(event) 183 | }, 184 | api_error = function(e) { 185 | log_error(as.character(e)) 186 | aws_request_id <- 187 | headers(event)[["lambda-runtime-aws-request-id"]] 188 | if (exists("aws_request_id")) { 189 | log_debug("POSTing invocation error for ID:", aws_request_id) 190 | invocation_error_endpoint <- paste0( 191 | "http://", lambda_runtime_api, "/2018-06-01/runtime/invocation/", 192 | aws_request_id, "/error" 193 | ) 194 | POST( 195 | url = invocation_error_endpoint, 196 | body = list( 197 | statusCode = e$code, 198 | error_message = as.character(e$message)), 199 | encode = "json" 200 | ) 201 | } else { 202 | log_debug("No invocation ID!", 203 | "Can't clear this request from the queue.") 204 | } 205 | }, 206 | error = function(e) { 207 | log_error(as.character(e)) 208 | aws_request_id <- 209 | headers(event)[["lambda-runtime-aws-request-id"]] 210 | if (exists("aws_request_id")) { 211 | log_debug("POSTing invocation error for ID:", aws_request_id) 212 | invocation_error_endpoint <- paste0( 213 | "http://", lambda_runtime_api, "/2018-06-01/runtime/invocation/", 214 | aws_request_id, "/error" 215 | ) 216 | POST( 217 | url = invocation_error_endpoint, 218 | body = list(error_message = as.character(e)), 219 | encode = "json" 220 | ) 221 | } else { 222 | log_debug("No invocation ID!", 223 | "Can't clear this request from the queue.") 224 | } 225 | } 226 | ) 227 | } 228 | --------------------------------------------------------------------------------