skip to Main Content

I’ve setup an image pushed to the ECR and then is used by my lambda function. My scripts include a custom runtime in R, my lambda function, and a bootstrap file to change the working directory to $LAMBDA_TASK_ROOT.

I’m a beginner with working with AWS cloud applications and developing a containerized image to be used by lambda. I’ve been using this link as a guide: https://mdneuzerling.com/post/r-on-aws-lambda-with-containers/

This is my current error from testing the function through the AWS interface. I’m unable to test the function and dockerfile locally because of some restrictions with my work computer.

/lambda-entrypoint.sh: line 14: /var/runtime/bootstrap: No such file or directory
/lambda-entrypoint.sh: line 14: /var/runtime/bootstrap: No such file or directory
START RequestId: a0fea004-ed78-46a5-972f-5463e89ccad2 Version: $LATEST
RequestId: a0fea004-ed78-46a5-972f-5463e89ccad2 Error: Runtime exited with error: exit status 127
Runtime.ExitError
END RequestId: a0fea004-ed78-46a5-972f-5463e89ccad2
REPORT RequestId: a0fea004-ed78-46a5-972f-5463e89ccad2  Duration: 22.76 ms  Billed Duration: 23 ms  Memory Size: 128 MB Max Memory Used: 3 MB   

Here is my dockerfile

FROM public.ecr.aws/lambda/provided:al2

ENV R_VERSION=4.3.0

RUN yum -y install amazon-linux-extras && 
    yum clean all

RUN amazon-linux-extras install kernel-5.15 && 
    yum clean all

RUN yum -y install wget-1.14 && 
    yum -y install glib2-2.56.1-9.amzn2.0.3 && 
    yum -y install libssh2-1.4.3-12.amzn2.2.4 && 
    yum clean all

RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm 
  && wget --no-verbose https://cdn.rstudio.com/r/centos-7/pkgs/R-${R_VERSION}-1-1.x86_64.rpm 
  && yum -y install R-${R_VERSION}-1-1.x86_64.rpm 
  && rm R-${R_VERSION}-1-1.x86_64.rpm && 
  yum clean all

ENV PATH="${PATH}:/opt/R/${R_VERSION}/bin/"

# System requirements for R packages
RUN yum -y install openssl-devel && 
    Rscript -e "install.packages(c('httr', 'jsonlite', 'logger', 'logging', 'sp', 'raster', 'parallel'), Ncpus=2L, INSTALL_opts=c('--no-docs', '--no-help','--no-html', '--no-multiarch', '--no-test-load'), dependencies=TRUE, repos=c('https://artifactory.wma.chs.usgs.gov/artifactory/r-cran-mirror/','https://RForge.net'))" && 
    Rscript -e "install.packages('aws.s3', Ncpus=2L, INSTALL_opts=c('--no-docs', '--no-help','--no-html', '--no-multiarch', '--no-test-load'), repos = c('https://RForge.net'))" && 
    yum install -y 
    openldap-2.4.44-25.amzn2.0.5 && 
    yum clean all && rm -rf /var/lib/apt/lists/*

COPY runtime.r process_landsat_temp.r ${LAMBDA_TASK_ROOT}/
RUN chmod 755 -R "${LAMBDA_TASK_ROOT}"

WORKDIR /var/runtime
COPY bootstrap bootstrap
RUN chmod +x bootstrap

My runtime R script.

library(httr)
library(logger)
log_formatter(formatter_paste)
log_threshold(INFO)

#' Convert a list to a single character, preserving names
#' prettify_list(list("a" = 1, "b" = 2, "c" = 3))
#' # "a=5, b=5, c=5"
prettify_list <- function(x) {
  paste(
    paste(names(x), x, sep = "="),
    collapse = ", "
  )
}

# error handling with http codes
# from http://adv-r.had.co.nz/Exceptions-Debugging.html
condition <- function(subclass, message, code, call = sys.call(-1), ...) {
  structure(
    class = c(subclass, "condition"),
    list(message = message, code = code, call = call),
    ...
  )
}
stop_api <- function(message, code = 500, call = sys.call(-1), ...) {
  stop(condition(c("api_error", "error"), message, code = code, call = call,
                 ...))
}

log_debug("Deriving lambda runtime API endpoints from environment variables")
lambda_runtime_api <- Sys.getenv("AWS_LAMBDA_RUNTIME_API")
if (lambda_runtime_api == "") {
  error_message <- "AWS_LAMBDA_RUNTIME_API environment variable undefined"
  log_error(error_message)
  stop(error_message)
}
next_invocation_endpoint <- paste0(
  "http://", lambda_runtime_api, "/2018-06-01/runtime/invocation/next"
)
initialisation_error_endpoint <- paste0(
  "http://", lambda_runtime_api, "/2018-06-01/runtime/init/error"
)

tryCatch(
  {
    log_debug("Determining handler from environment variables")
    handler <- Sys.getenv("_HANDLER")
    if (is.null(handler) || handler == "") {
      stop_api("_HANDLER environment variable undefined")
    }
    log_info("Handler found:", handler)
    handler_split <- strsplit(handler, ".", fixed = TRUE)[[1]]
    file_name <- paste0(handler_split[1], ".R")
    function_name <- handler_split[2]
    log_info("Using function", function_name, "from", file_name)
    
    log_debug("Checking if", file_name, "exists")
    if (!file.exists(file_name)) {
      stop_api(file_name, " doesn't exist in ", getwd())
    }
    source(file_name)
    
    log_debug("Checking if", function_name, "is defined")
    if (!exists(function_name)) {
      stop_api("Function name ", function_name, " isn't defined in R")
    }
    log_debug("Checking if", function_name, "is a function")
    if (!is.function(eval(parse(text = function_name)))) {
      stop_api("Function name ", function_name, " is not a function")
    }
  },
  api_error = function(e) {
    log_error(as.character(e))
    POST(
      url = initialisation_error_endpoint,
      body = list(
        statusCode = e$code,
        error_message = as.character(e$message)),
      encode = "json"
    )
    stop(e)
  }
)

handle_event <- function(event) {
  status_code <- status_code(event)
  log_debug("Status code:", status_code)
  if (status_code != 200) {
    stop_api("Didn't get status code 200. Status code: ", status_code,
             code = 400)
  }
  event_headers <- headers(event)
  
  # HTTP headers are case-insensitive
  names(event_headers) <- tolower(names(event_headers))
  log_debug("Event headers:", prettify_list(event_headers))
  
  aws_request_id <- event_headers[["lambda-runtime-aws-request-id"]]
  if (is.null(aws_request_id)) {
    stop_api("Could not find lambda-runtime-aws-request-id header in event",
             code = 400)
  }
  
  # According to the AWS guide, the below is used by "X-Ray SDK"
  runtime_trace_id <- event_headers[["lambda-runtime-trace-id"]]
  if (!is.null(runtime_trace_id)) {
    Sys.setenv("_X_AMZN_TRACE_ID" = runtime_trace_id)
  }
  
  # we need to parse the event in four contexts before sending to the lambda fn:
  # 1a) direct invocation with no function args (empty event)
  # 1b) direct invocation with function args (parse and send entire event)
  # 2a) api endpoint with no args (parse HTTP request, confirm null request
  #   element; send empty list)
  # 2b) api endpoint with args (parse HTTP request, confirm non-null request
  #   element; extract and send it)
  
  unparsed_content <- httr::content(event, "text", encoding = "UTF-8")
  # Thank you to Menno Schellekens for this fix for Cloudwatch events
  is_scheduled_event <- grepl("Scheduled Event", unparsed_content)
  if(is_scheduled_event) log_info("Event type is scheduled")
  log_debug("Unparsed content:", unparsed_content)
  if (unparsed_content == "" || is_scheduled_event) {
    # (1a) direct invocation with no args (or scheduled request)
    event_content <- list()
  } else {
    # (1b, 2a or 2b)
    event_content <- jsonlite::fromJSON(unparsed_content)
  }
  
  # if you want to do any additional inspection of the event body (including
  # other http request elements if it's an endpoint), you can do that here!
  
  # change `http_req_element` if you'd prefer to send the http request `body` to
  # the lambda fn, rather than the query parameters
  # (note that query string params are always strings! your lambda fn may need to
  # convert them back to numeric/logical/Date/etc.)
  is_http_req <- FALSE
  http_req_element <- "queryStringParameters"
  
  if (http_req_element %in% names(event_content)) {
    is_http_req <- TRUE
    if (is.null(event_content[[http_req_element]])) {
      # (2a) api request with no args
      event_content <- list()
    } else {
      # (2b) api request with args
      event_content <- event_content[[http_req_element]]
    }
  }
  
  result <- do.call(function_name, event_content)
  log_debug("Result:", as.character(result))
  response_endpoint <- paste0(
    "http://", lambda_runtime_api, "/2018-06-01/runtime/invocation/",
    aws_request_id, "/response"
  )
  # aws api gateway is a bit particular about the response format
  body <- if (is_http_req) {
    list(
      isBase64Encoded = FALSE,
      statusCode = 200L,
      body =  as.character(jsonlite::toJSON(result, auto_unbox = TRUE))
    )
  } else {
    result
  }
  POST(
    url = response_endpoint,
    body = body,
    encode = "json"
  )
  rm("aws_request_id") # so we don't report errors to an outdated endpoint
}

log_info("Querying for events")
while (TRUE) {
  tryCatch(
    {
      event <- GET(url = next_invocation_endpoint)
      log_debug("Event received")
      handle_event(event)
    },
    api_error = function(e) {
      log_error(as.character(e))
      aws_request_id <-
        headers(event)[["lambda-runtime-aws-request-id"]]
      if (exists("aws_request_id")) {
        log_debug("POSTing invocation error for ID:", aws_request_id)
        invocation_error_endpoint <- paste0(
          "http://", lambda_runtime_api, "/2018-06-01/runtime/invocation/",
          aws_request_id, "/error"
        )
        POST(
          url = invocation_error_endpoint,
          body = list(
            statusCode = e$code,
            error_message = as.character(e$message)),
          encode = "json"
        )
      } else {
        log_debug("No invocation ID!",
                  "Can't clear this request from the queue.")
      }
    },
    error = function(e) {
      log_error(as.character(e))
      aws_request_id <-
        headers(event)[["lambda-runtime-aws-request-id"]]
      if (exists("aws_request_id")) {
        log_debug("POSTing invocation error for ID:", aws_request_id)
        invocation_error_endpoint <- paste0(
          "http://", lambda_runtime_api, "/2018-06-01/runtime/invocation/",
          aws_request_id, "/error"
        )
        POST(
          url = invocation_error_endpoint,
          body = list(error_message = as.character(e)),
          encode = "json"
        )
      } else {
        log_debug("No invocation ID!",
                  "Can't clear this request from the queue.")
      }
    }
  )
}

And my bootstrap file

#!/bin/sh Rscript
cd $LAMBDA_TASK_ROOT
Rscript runtime.r

2

Answers


  1. Disclaimer: I am not familiar with R

    I could not build a working lambda as the question did not include the implementation for process_landsat_temp.r. Having said that, I supplemented it with a sample and could not reproduce the error as the bootstrap file was present under /var/runtime.

    Following that, I tried to run the container from the reference that you had given in the description. I could build it but when I tried to access the API, the service never responded.

    A difference that I observed is that with your Dockerfile the working directory of the lambada changed to /var/runtime while the one from the reference article remained /var/task.

    Login or Signup to reply.
  2. I was having the same issue and with a bit of reverse engineering I got where I was wrong.

    The default ENTRYPOINT of the base image is indeed set to /lambda-entrypoint.sh that contains such script:

    #!/bin/sh
    # Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    
    if [ $# -ne 1 ]; then
      echo "entrypoint requires the handler name to be the first argument" 1>&2
      exit 142
    fi
    export _HANDLER="$1"
    
    RUNTIME_ENTRYPOINT=/var/runtime/bootstrap
    if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then
      exec /usr/local/bin/aws-lambda-rie $RUNTIME_ENTRYPOINT
    else
      exec $RUNTIME_ENTRYPOINT
    fi
    

    As you can assume, it’s kind of expecting that the executable of the docker container is placed here:

    RUNTIME_ENTRYPOINT=/var/runtime/bootstrap
    

    Why it does not work in your case?

    Be extra careful when you build a docker image and you perform a push overriding an existing tag.
    Even if you have a new image tagged as latest in ECR (or in any other container registry), it might happen that the image referenced in lambda function still refers to the old one (you can check this by hitting on deploy new image and check if the sha256 signature of the current image reported in Docker container image from selected repository is the same as the one you have in ECR).

    In my case I was building && pushing always a latest image in ECR and I was struggling why it was not taking my last changes.

    How can you make it easier to troubleshoot?

    Instead of relying on the default entrypoint, which is there mostly to support local testing, you could simply override it in your Dockerfile as follows

    ...
    WORKDIR /
    COPY bootstrap /bootstrap
    RUN chmod +x bootstrap
    ENTRYPOINT ["/bootstrap"]
    

    Not a big change but at least you know that in case of any issue you must check your entrypoint or your code.

    Hope this gives you an hint and will save a bit of time for the others who might struggle on that.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search