├── .dockerignore ├── Scripts ├── push.sh └── build.sh ├── .gitignore ├── Examples ├── aws-sam │ ├── http-mode │ │ ├── .gitignore │ │ ├── example-http-function │ │ │ ├── Dockerfile │ │ │ └── http-handler-file.wl │ │ ├── template.yaml │ │ └── README.md │ └── raw-mode │ │ ├── .gitignore │ │ ├── example-raw-function │ │ ├── Dockerfile │ │ └── raw-handler-file.wl │ │ ├── template.yaml │ │ └── README.md └── .images │ ├── HTTP-Function-FormPage.png │ └── HTTP-Function-URLDispatcher.png ├── runtime-entrypoint.sh ├── AWSLambdaRuntime ├── PacletInfo.wl └── Kernel │ ├── Modes │ ├── Modes.wl │ ├── Raw.wl │ └── HTTP.wl │ ├── Utility.wl │ ├── API.wl │ └── AWSLambdaRuntime.wl ├── LICENSE ├── runtime-kernel-wrapper.sh ├── Dockerfile ├── CONTRIBUTING.md └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | Examples/ 2 | Scripts/ -------------------------------------------------------------------------------- /Scripts/push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker push \ 4 | wolframresearch/aws-lambda-wolframlanguage:latest -------------------------------------------------------------------------------- /Scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker build . \ 4 | -t wolframresearch/aws-lambda-wolframlanguage:latest -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.code-workspace 2 | 3 | Scripts/build-test.sh 4 | Scripts/push-test.sh 5 | Scripts/run-test.sh 6 | 7 | Test/* -------------------------------------------------------------------------------- /Examples/aws-sam/http-mode/.gitignore: -------------------------------------------------------------------------------- 1 | # Local machine-specific build data 2 | .aws-sam 3 | 4 | # `sam deploy` configuration 5 | samconfig.toml -------------------------------------------------------------------------------- /Examples/aws-sam/raw-mode/.gitignore: -------------------------------------------------------------------------------- 1 | # Local machine-specific build data 2 | .aws-sam 3 | 4 | # `sam deploy` configuration 5 | samconfig.toml -------------------------------------------------------------------------------- /Examples/.images/HTTP-Function-FormPage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WolframResearch/AWSLambda-WolframLanguage/master/Examples/.images/HTTP-Function-FormPage.png -------------------------------------------------------------------------------- /Examples/.images/HTTP-Function-URLDispatcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WolframResearch/AWSLambda-WolframLanguage/master/Examples/.images/HTTP-Function-URLDispatcher.png -------------------------------------------------------------------------------- /Examples/aws-sam/http-mode/example-http-function/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM wolframresearch/aws-lambda-wolframlanguage:latest 2 | 3 | COPY *.wl /var/task/ 4 | 5 | # Selected handler can be overridden by providing a different command in the template directly. 6 | CMD ["http-handler-file"] 7 | -------------------------------------------------------------------------------- /Examples/aws-sam/raw-mode/example-raw-function/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM wolframresearch/aws-lambda-wolframlanguage:latest 2 | 3 | COPY *.wl /var/task/ 4 | 5 | # Selected handler can be overridden by providing a different command in the template directly. 6 | CMD ["raw-handler-file"] 7 | -------------------------------------------------------------------------------- /runtime-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # use, in order: existing _HANDLER; first argument; string "app" 4 | export _HANDLER="${_HANDLER:-${1:-app}}" 5 | 6 | if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then 7 | exec /usr/local/bin/aws-lambda-rie /runtime-kernel-wrapper.sh $_HANDLER 8 | else 9 | exec /runtime-kernel-wrapper.sh 10 | fi -------------------------------------------------------------------------------- /AWSLambdaRuntime/PacletInfo.wl: -------------------------------------------------------------------------------- 1 | PacletObject[<| 2 | "Name" -> "AWSLambdaRuntime", 3 | "Creator" -> "Jesse Friedman ", 4 | "Version" -> "1.1.0", 5 | "WolframVersion" -> "12.2+", 6 | "Loading" -> Automatic, 7 | "Extensions" -> { 8 | {"Kernel", 9 | "Root" -> "Kernel", 10 | "Context" -> {"AWSLambdaRuntime`"} 11 | } 12 | } 13 | |>] -------------------------------------------------------------------------------- /AWSLambdaRuntime/Kernel/Modes/Modes.wl: -------------------------------------------------------------------------------- 1 | BeginPackage["AWSLambdaRuntime`Modes`"] 2 | 3 | Begin["`Private`"] 4 | 5 | Get["AWSLambdaRuntime`Modes`HTTP`"] 6 | Get["AWSLambdaRuntime`Modes`Raw`"] 7 | 8 | (* ::Section:: *) 9 | (* Fallthrough for ValidateHandler and EvaluateHandler) *) 10 | 11 | invalidModeFailure := Failure[ 12 | "InvalidHandlerMode", 13 | <| 14 | "MessageTemplate" -> "The handler mode `1` is not valid", 15 | "MessageParameters" -> {AWSLambdaRuntime`Handler`$AWSLambdaHandlerMode} 16 | |> 17 | ] 18 | 19 | AWSLambdaRuntime`Modes`ValidateHandler[_, ___] := invalidModeFailure 20 | AWSLambdaRuntime`Modes`EvaluateHandler[_, ___] := invalidModeFailure 21 | 22 | End[] 23 | 24 | EndPackage[] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Wolfram Research Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /runtime-kernel-wrapper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export MATHEMATICA_USERBASE="/tmp/home/.WolframEngine" 4 | export MATHEMATICAPLAYER_USERBASE="/tmp/home/.WolframEngine" 5 | export WOLFRAM_CACHEBASE="/tmp/home/.cache/Wolfram" 6 | export WOLFRAM_LOG_DIRECTORY="/tmp/home/.Wolfram/Logs" 7 | 8 | # change the home directory (and other things) to /tmp/home 9 | WL_INIT_DIRECTORIES='Developer`ConfigureUser[None, "/tmp/home"];'\ 10 | 'Unprotect[$HomeDirectory, $UserDocumentsDirectory, $WolframDocumentsDirectory];'\ 11 | '$UserDocumentsDirectory = $HomeDirectory = HomeDirectory[];'\ 12 | '$WolframDocumentsDirectory = FileNameJoin[{$HomeDirectory, "WolframDocuments"}];'\ 13 | 'Protect[$HomeDirectory, $UserDocumentsDirectory, $WolframDocumentsDirectory];' 14 | 15 | # launch the runtime 16 | WL_RUNTIME_START='Get["AWSLambdaRuntime`"];'\ 17 | 'AWSLambdaRuntime`StartRuntime[];'\ 18 | 'Exit[0]' 19 | 20 | exec /usr/local/bin/WolframKernel \ 21 | -pwfile '!cloudlm.wolfram.com' \ 22 | -entitlement $WOLFRAMSCRIPT_ENTITLEMENTID \ 23 | -pacletreadonly \ 24 | -noinit \ 25 | -runfirst "$WL_INIT_DIRECTORIES" \ 26 | -run "$WL_RUNTIME_START" -------------------------------------------------------------------------------- /AWSLambdaRuntime/Kernel/Modes/Raw.wl: -------------------------------------------------------------------------------- 1 | BeginPackage["AWSLambdaRuntime`Modes`Raw`"] 2 | 3 | AWSLambdaRuntime`Modes`ValidateHandler 4 | AWSLambdaRuntime`Modes`EvaluateHandler 5 | 6 | Begin["`Private`"] 7 | 8 | Needs["AWSLambdaRuntime`API`"] 9 | 10 | (* ::Section:: *) 11 | (* Initialize mode implementation (load dependencies) *) 12 | 13 | AWSLambdaRuntime`Modes`InitializeMode["Raw"] := Null (* nothing currently needed here *) 14 | 15 | (* ::Section:: *) 16 | (* Validate handler (called during initialization) *) 17 | 18 | (* any expression is considered valid (it will be called as a function) *) 19 | AWSLambdaRuntime`Modes`ValidateHandler[ 20 | "Raw", 21 | handler_ 22 | ] := Success["Valid", <||>] 23 | 24 | (* ::Section:: *) 25 | (* Evaluate handler *) 26 | 27 | AWSLambdaRuntime`Modes`EvaluateHandler[ 28 | "Raw", 29 | handler_, 30 | requestBody_, 31 | requestContextData_ 32 | ] := Module[{ 33 | handlerOutput 34 | }, 35 | handlerOutput = AWSLambdaRuntime`Utility`WithCleanContext[ 36 | handler[requestBody] 37 | ]; 38 | 39 | Return[handlerOutput] 40 | ] 41 | 42 | End[] 43 | 44 | EndPackage[] -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM wolframresearch/wolframengine:latest 2 | 3 | USER root 4 | 5 | # temporary workaround for bug(413167) 6 | RUN apt-get update -y && \ 7 | apt-get install -y libglib2.0-0 8 | 9 | ENV LAMBDA_TASK_ROOT=/var/task 10 | ENV LAMBDA_RUNTIME_DIR=/var/runtime 11 | 12 | # add the Lambda RIE for debugging 13 | ADD https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie /usr/local/bin/aws-lambda-rie 14 | 15 | COPY AWSLambdaRuntime /usr/share/WolframEngine/Applications/AWSLambdaRuntime 16 | 17 | COPY runtime-entrypoint.sh /runtime-entrypoint.sh 18 | COPY runtime-kernel-wrapper.sh /runtime-kernel-wrapper.sh 19 | 20 | # patch layout with libiomp5.so from WL 12.3.1, to work around OpenMP/MKL 21 | # incompatibility issue in 13.0.0 relating to /dev/shm not working in Lambda 22 | COPY --from=wolframresearch/wolframengine:12.3.1 \ 23 | /usr/local/Wolfram/WolframEngine/12.3/SystemFiles/Libraries/Linux-x86-64/libiomp5.so \ 24 | /usr/local/Wolfram/WolframEngine/13.0/SystemFiles/Libraries/Linux-x86-64/libiomp5.so 25 | 26 | RUN chmod a+rx \ 27 | /usr/local/bin/aws-lambda-rie \ 28 | /runtime-entrypoint.sh \ 29 | /runtime-kernel-wrapper.sh 30 | 31 | USER wolframengine 32 | 33 | WORKDIR $LAMBDA_TASK_ROOT 34 | 35 | ENTRYPOINT ["/runtime-entrypoint.sh"] -------------------------------------------------------------------------------- /Examples/aws-sam/raw-mode/example-raw-function/raw-handler-file.wl: -------------------------------------------------------------------------------- 1 | (* 2 | This is a function handler file containing a single raw-mode handler. 3 | 4 | As this file is named `raw-handler-file.wl`, a function can be configured 5 | to use the handler in this file by giving the handler specification 6 | "raw-handler-file" as the command line (CMD) in the Dockerfile or in 7 | the function's ImageConfig in `template.yaml`. 8 | 9 | If instead of a single handler this file contained an association of 10 | multiple handlers, like: 11 | <| 12 | "myhandler" -> Function[...], 13 | "anotherhandler" -> APIFunction[...] 14 | |> 15 | 16 | ...then these handlers could be accessed with handler specifications like: 17 | - "raw-handler-file.myhandler" 18 | - "raw-handler-file.anotherhandler" 19 | 20 | The pure function below accepts an association (i.e. JSON object) with an 21 | "input" key containing a string, reverses the letters in the string, and 22 | returns another association (JSON object) with the reversed string in the 23 | "reversed" key. 24 | 25 | This function does not perform any validation its input, so invocations 26 | will fail of the "input" key is not present or is not a string. 27 | *) 28 | 29 | Function[ 30 | <| 31 | "reversed" -> StringReverse[#input] 32 | |> 33 | ] -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Wolfram® 2 | 3 | Thank you for taking the time to contribute to the [Wolfram Research](https://github.com/wolframresearch) repos on GitHub. 4 | 5 | ## Licensing of Contributions 6 | 7 | By contributing to Wolfram, you agree and affirm that: 8 | 9 | > Wolfram may release your contribution under the terms of the [MIT license](https://opensource.org/licenses/MIT); and 10 | 11 | > You have read and agreed to the [Developer Certificate of Origin](http://developercertificate.org/), version 1.1 or later. 12 | 13 | Please see [LICENSE](LICENSE) for licensing conditions pertaining 14 | to individual repositories. 15 | 16 | 17 | ## Bug reports 18 | 19 | ### Security Bugs 20 | 21 | Please **DO NOT** file a public issue regarding a security issue. 22 | Rather, send your report privately to security@wolfram.com. Security 23 | reports are appreciated and we will credit you for it. We do not offer 24 | a security bounty, but the forecast in your neighborhood will be cloudy 25 | with a chance of Wolfram schwag! 26 | 27 | ### General Bugs 28 | 29 | Please use the repository issues page to submit general bug issues. 30 | 31 | Please do not duplicate issues. 32 | 33 | Please do send a complete and well-written report to us. Note: **the 34 | thoroughness of your report will positively correlate to our willingness 35 | and ability to address it**. 36 | 37 | When reporting issues, always include: 38 | 39 | * Lambda function [console logs](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-logging.html) from a failed invocation, if relevant. 40 | * If possible, set the environment variable `WOLFRAM_LAMBDA_DEBUG_LOGS=1` on the Lambda function in order to include debugging information in the console logs. -------------------------------------------------------------------------------- /Examples/aws-sam/raw-mode/template.yaml: -------------------------------------------------------------------------------- 1 | # This file is derived from an AWS-provided SAM CLI application template. 2 | # The original file from which this file has been modified is located at: 3 | # https://github.com/aws/aws-sam-cli-app-templates/blob/de97a7aac7ee8416f3310d7bd005b391f1ff1ac0/nodejs14.x-image/cookiecutter-aws-sam-hello-nodejs-lambda-image/%7B%7Bcookiecutter.project_name%7D%7D/template.yaml 4 | # The repository containing the original file is licensed under the Apache-2.0 License 5 | # (https://github.com/aws/aws-sam-cli-app-templates/blob/115fc2d1557d70690b1826ce79d0bc033e09728e/LICENSE) 6 | # and carries the following notice: 7 | # > Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 8 | 9 | AWSTemplateFormatVersion: "2010-09-09" 10 | Transform: AWS::Serverless-2016-10-31 11 | Description: > 12 | Example AWS SAM app using the Wolfram Language Lambda runtime 13 | with a raw-mode function 14 | 15 | Parameters: 16 | OnDemandLicenseEntitlementID: 17 | Type: String 18 | NoEcho: true 19 | Description: > 20 | The ID of a Wolfram on-demand license entitlement to use to 21 | activate the Wolfram Engine within your function container(s). 22 | 23 | # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst 24 | Globals: 25 | Function: 26 | Timeout: 30 27 | MemorySize: 512 28 | Environment: 29 | Variables: 30 | WOLFRAMSCRIPT_ENTITLEMENTID: !Ref OnDemandLicenseEntitlementID 31 | 32 | Resources: 33 | ExampleRawFunction: 34 | Type: AWS::Serverless::Function 35 | Metadata: 36 | DockerContext: ./example-raw-function 37 | Dockerfile: Dockerfile 38 | Properties: 39 | PackageType: Image 40 | ImageConfig: 41 | # This corresponds to the single handler inside 42 | # "raw-handler-file.wl" in the build context folder. 43 | # This setting overrides any `CMD` instruction in the Dockerfile. 44 | Command: ["raw-handler-file"] 45 | Environment: 46 | Variables: 47 | # This is not strictly necessary, as raw mode is the default. 48 | # Handler mode can also be configured in the handler file via the 49 | # $AWSLambdaHandlerMode variable. 50 | WOLFRAM_LAMBDA_HANDLER_MODE: Raw 51 | 52 | Outputs: 53 | ExampleRawFunction: 54 | Description: Example raw-mode Lambda function 55 | Value: !GetAtt ExampleRawFunction.Arn 56 | ExampleRawFunctionRole: 57 | Description: Implicit IAM role created for example raw-mode function 58 | Value: !GetAtt ExampleRawFunctionRole.Arn -------------------------------------------------------------------------------- /Examples/aws-sam/http-mode/example-http-function/http-handler-file.wl: -------------------------------------------------------------------------------- 1 | (* 2 | This is a function handler file containing a single HTTP-mode handler. 3 | 4 | As this file is named `http-handler-file.wl`, a function can be configured 5 | to use the handler in this file by giving the handler specification 6 | "http-handler-file" as the command line (CMD) in the Dockerfile or in 7 | the function's ImageConfig in `template.yaml`. 8 | 9 | If instead of a single handler this file contained an association of 10 | multiple handlers, like: 11 | <| 12 | "myhandler" -> APIFunction[...], 13 | "anotherhandler" -> FormFunction[...] 14 | |> 15 | 16 | ...then these handlers could be accessed with handler specifications like: 17 | - "http-handler-file.myhandler" 18 | - "http-handler-file.anotherhandler" 19 | *) 20 | 21 | 22 | (* 23 | Setting $AWSLambdaHandlerMode as done here makes explicit that the handler in 24 | this file is to be used in HTTP-mode, but doing so is redundant if 25 | the environment variable WOLFRAM_LAMBDA_HANDLER_MODE is also set (as it is 26 | in `template.yaml`) 27 | *) 28 | $AWSLambdaHandlerMode = "HTTP" 29 | 30 | 31 | (* 32 | This handler is a URLDispatcher, which allows URL routing rules to be defined with Wolfram Language code. 33 | *) 34 | 35 | URLDispatcher[{ 36 | (* This is an APIFunction that returns the population of a given country in a given year. *) 37 | "/api" -> APIFunction[ 38 | { 39 | "country" -> "Country", 40 | "year" -> "Integer" :> DateValue["Year"] (* default to the current year *) 41 | }, 42 | <| 43 | "population" -> QuantityMagnitude@EntityValue[ 44 | #country, 45 | Dated["Population", #year] 46 | ] 47 | |> &, 48 | "JSON" 49 | ], 50 | 51 | (* This is a FormFunction that applies an effect to an uploaded image. *) 52 | "/form" -> FormFunction[ 53 | {"image" -> "Image", "filter" -> ImageEffect[]}, 54 | ImageEffect[#image, #filter] &, 55 | "PNG" 56 | ], 57 | 58 | (* This is a computed ("delayed") response containing a PNG file. *) 59 | "/image" -> Delayed[RandomEntity["Pokemon"]["Image"], "PNG"], 60 | 61 | (* This is a path-based routing pattern that returns a result based on parameters in the URL. *) 62 | StringExpression[ 63 | "/power/", 64 | base : Repeated[DigitCharacter, 3], 65 | "^", 66 | power : Repeated[DigitCharacter, 3] 67 | ] :> ( 68 | FromDigits[base] ^ FromDigits[power] 69 | ), 70 | 71 | (* This is a computed HTML string. *) 72 | "/" -> Delayed@ExportForm[ 73 | Echo["Received request for root route"]; 74 | TemplateApply@StringJoin@{ 75 | "Hello! I am a URLDispatcher running in version ", 76 | "<* $VersionNumber *> of the Wolfram Engine. ", 77 | "Try one of these links: ", 78 | "/api, ", 79 | "/form, ", 80 | "/image, ", 81 | "/power/42^42", 82 | "

", 83 | "Here is the current HTTPRequestData[]:
", 84 | "<* ToString[HTTPRequestData[], InputForm] *>
", 85 | "And the $HTTPRequest:
", 86 | "<* ToString[$HTTPRequest, InputForm] *>" 87 | }, 88 | "HTML" 89 | ] 90 | }] -------------------------------------------------------------------------------- /Examples/aws-sam/http-mode/template.yaml: -------------------------------------------------------------------------------- 1 | # This file is derived from an AWS-provided SAM CLI application template. 2 | # The original file from which this file has been modified is located at: 3 | # https://github.com/aws/aws-sam-cli-app-templates/blob/de97a7aac7ee8416f3310d7bd005b391f1ff1ac0/nodejs14.x-image/cookiecutter-aws-sam-hello-nodejs-lambda-image/%7B%7Bcookiecutter.project_name%7D%7D/template.yaml 4 | # The repository containing the original file is licensed under the Apache-2.0 License 5 | # (https://github.com/aws/aws-sam-cli-app-templates/blob/115fc2d1557d70690b1826ce79d0bc033e09728e/LICENSE) 6 | # and carries the following notice: 7 | # > Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 8 | 9 | AWSTemplateFormatVersion: "2010-09-09" 10 | Transform: AWS::Serverless-2016-10-31 11 | Description: > 12 | Example AWS SAM app using the Wolfram Language Lambda runtime 13 | with an HTTP-mode function connected to an API Gateway API 14 | 15 | Parameters: 16 | OnDemandLicenseEntitlementID: 17 | Type: String 18 | NoEcho: true 19 | Description: > 20 | The ID of a Wolfram on-demand license entitlement to use to 21 | activate the Wolfram Engine within your function container(s). 22 | 23 | # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst 24 | Globals: 25 | Function: 26 | Timeout: 30 27 | MemorySize: 512 28 | Environment: 29 | Variables: 30 | WOLFRAMSCRIPT_ENTITLEMENTID: !Ref OnDemandLicenseEntitlementID 31 | 32 | Api: 33 | BinaryMediaTypes: 34 | # This tells API Gateway to represent HTTP request and response bodies 35 | # as Base64-encoded binary data instead of as plain text. This is necessary 36 | # in order for requests and responses containing binary data (e.g. images) 37 | # to work. If you disable this, then you should instruct the Wolfram 38 | # Language Lambda runtime to skip encoding responses to Base64. You can do 39 | # this by setting the WOLFRAM_LAMBDA_HTTP_USE_PLAIN_TEXT_RESPONSE 40 | # environment variable to 1 in the function configuration, or by setting 41 | # the $AWSLambdaUsePlainTextResponse Wolfram Language variable to True 42 | # in your handler file. 43 | - "*/*" 44 | 45 | Resources: 46 | ExampleHTTPFunction: 47 | Type: AWS::Serverless::Function 48 | Metadata: 49 | DockerContext: ./example-http-function 50 | Dockerfile: Dockerfile 51 | Properties: 52 | PackageType: Image 53 | ImageConfig: 54 | # This corresponds to the single handler inside 55 | # "http-handler-file.wl" in the build context folder. 56 | # This setting overrides any `CMD` instruction in the Dockerfile. 57 | Command: ["http-handler-file"] 58 | Environment: 59 | Variables: 60 | # Handler mode can also be configured in the handler file via the 61 | # $AWSLambdaHandlerMode variable. 62 | WOLFRAM_LAMBDA_HANDLER_MODE: HTTP 63 | Events: 64 | ExampleURLDispatcherRoot: 65 | Type: Api 66 | Properties: 67 | # This matches requests to the "root" of the API (e.g. /Prod/). 68 | Path: / 69 | Method: any 70 | ExampleURLDispatcherProxy: 71 | Type: Api 72 | Properties: 73 | # This matches requests to URLs under the "root" (e.g./Prod/foo). 74 | Path: /{proxy+} 75 | Method: any 76 | 77 | # This instructs SAM to automatically assign the alias "Deployed" 78 | # to new versions of the function, and to configure that alias with 79 | # one unit of provisioned concurrency. 80 | # Remove the following three lines if you don't want to use 81 | # provisioned concurrency. 82 | AutoPublishAlias: Deployed 83 | ProvisionedConcurrencyConfig: 84 | ProvisionedConcurrentExecutions: 1 85 | 86 | Outputs: 87 | ExampleHTTPFunction: 88 | Description: Example HTTP-mode Lambda function 89 | Value: !GetAtt ExampleHTTPFunction.Arn 90 | ExampleHTTPFunctionRole: 91 | Description: Implicit IAM role created for example HTTP-mode function 92 | Value: !GetAtt ExampleHTTPFunctionRole.Arn 93 | 94 | # ServerlessRestApi is an implicit API created out of Events keys under Serverless::Function. 95 | # Find out more about other implicit resources you can reference within SAM: 96 | # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api 97 | ExampleHTTPFunctionAPI: 98 | Description: API Gateway endpoint URL for Prod stage for example HTTP-based function 99 | Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/" 100 | -------------------------------------------------------------------------------- /AWSLambdaRuntime/Kernel/Utility.wl: -------------------------------------------------------------------------------- 1 | BeginPackage["AWSLambdaRuntime`Utility`"] 2 | 3 | AWSLambdaRuntime`Utility`CapitalizeAllKeys 4 | AWSLambdaRuntime`Utility`ProxyFormatToHTTPRequest 5 | AWSLambdaRuntime`Utility`HTTPResponseToProxyFormat 6 | AWSLambdaRuntime`Utility`WithCleanContext 7 | 8 | Begin["`Private`"] 9 | 10 | AWSLambdaRuntime`Handler`$AWSLambdaUsePlainTextResponse = Lookup[ 11 | GetEnvironment[], 12 | "WOLFRAM_LAMBDA_HTTP_USE_PLAIN_TEXT_RESPONSE" 13 | ] === "1" 14 | 15 | (* ::Subsection:: *) 16 | (* Logging *) 17 | 18 | $debugLogsEnabled = Lookup[GetEnvironment[], "WOLFRAM_LAMBDA_DEBUG_LOGS"] === "1" 19 | 20 | SetAttributes[AWSLambdaRuntime`Utility`DebugEcho, HoldAll] 21 | AWSLambdaRuntime`Utility`DebugEcho[args___] := If[ 22 | $debugLogsEnabled, 23 | Echo[args] 24 | ] 25 | 26 | SetAttributes[AWSLambdaRuntime`Utility`DebugLogTiming, HoldAll] 27 | AWSLambdaRuntime`Utility`DebugLogTiming[msg_] := If[ 28 | $debugLogsEnabled, 29 | Echo[msg, DateList[]] 30 | ] 31 | 32 | (* ::Subsection:: *) 33 | (* WithCleanContext - evaluate an expression with clean $Context and $ContextPath *) 34 | 35 | SetAttributes[AWSLambdaRuntime`Utility`WithCleanContext, HoldFirst] 36 | 37 | AWSLambdaRuntime`Utility`WithCleanContext[ 38 | expr_, 39 | OptionsPattern[{ 40 | "ExtraContexts" -> {"AWSLambdaRuntime`Handler`"} 41 | }] 42 | ] := Block[{ 43 | $Context = "Global`", 44 | $ContextPath = Join[ 45 | OptionValue["ExtraContexts"], 46 | {"System`", "Global`"} 47 | ] 48 | }, 49 | expr 50 | ] 51 | 52 | 53 | (* ::Subsection:: *) 54 | (* CapitalizeAllKeys - recursively capitalize the keys in an association *) 55 | 56 | AWSLambdaRuntime`Utility`CapitalizeAllKeys[expr_] := Replace[ 57 | expr, 58 | assoc_Association :> KeyMap[Capitalize, assoc], 59 | All 60 | ] 61 | 62 | 63 | (* ::Section:: *) 64 | (* ProxyFormatToHTTPRequest - Convert from the API Gateway proxy integration request format to an HTTPRequest *) 65 | 66 | AWSLambdaRuntime`Utility`ProxyFormatToHTTPRequest[ 67 | proxyRequestData_Association 68 | ] := Module[{ 69 | headers, 70 | lowerHeaders, 71 | queryParameters, 72 | requestBody 73 | }, 74 | (* flatten out multi-value headers with Thread *) 75 | headers = Normal@Join[ 76 | (* these keys can have null values *) 77 | Lookup[proxyRequestData, "headers", <||>, Replace[Null -> <||>]], 78 | Lookup[proxyRequestData, "multiValueHeaders", <||>, Replace[Null -> <||>]] 79 | ] // Map[Thread] // Flatten; 80 | lowerHeaders = KeyMap[ToLowerCase, Association@headers]; 81 | 82 | queryParameters = Normal@Join[ 83 | Lookup[proxyRequestData, "queryStringParameters", <||>, Replace[Null -> <||>]], 84 | Lookup[proxyRequestData, "multiValueQueryStringParameters", <||>, Replace[Null -> <||>]] 85 | ] // Map[Thread] // Flatten; 86 | 87 | requestBody = proxyRequestData["body"] // Replace[Except[_String] -> ""]; 88 | If[ 89 | (* if the request body is Base64-encoded *) 90 | TrueQ[proxyRequestData["isBase64Encoded"]] && StringLength[requestBody] > 0, 91 | (* then decode it to a ByteArray *) 92 | requestBody = BaseDecode[requestBody] 93 | ]; 94 | 95 | Return@HTTPRequest[ 96 | <| 97 | "Scheme" -> First[ 98 | (* try to get the protocol from a header; fall back to https *) 99 | DeleteMissing@Lookup[ 100 | lowerHeaders, 101 | ToLowerCase@{ 102 | "X-Forwarded-Proto", 103 | "CloudFront-Forwarded-Proto" 104 | } 105 | ], 106 | "https" 107 | ], 108 | "User" -> None, 109 | "Domain" -> SelectFirst[ 110 | (* try to get the domain from requestContext.domainName 111 | and then from the Host header *) 112 | { 113 | proxyRequestData["requestContext", "domainName"], 114 | lowerHeaders["host"] 115 | }, 116 | StringQ, 117 | None 118 | ], 119 | "Port" -> None, 120 | "Path" -> SelectFirst[ 121 | (* try to get the path from requestContext.path first 122 | (it includes the stage name if applicable) *) 123 | { 124 | proxyRequestData["requestContext", "path"], 125 | proxyRequestData["path"] 126 | }, 127 | StringQ, 128 | None 129 | ], 130 | "Query" -> queryParameters, 131 | "Fragment" -> None 132 | |>, <| 133 | "HTTPVersion" -> "1.1", (* TODO: try to get from requestContext.protocol or Via header *) 134 | "Method" -> Lookup[proxyRequestData, "httpMethod", None], 135 | "Headers" -> headers, 136 | "Body" -> requestBody 137 | |> 138 | ] 139 | ] 140 | 141 | 142 | (* ::Section:: *) 143 | (* HTTPResponseToProxyFormat - Convert from an HTTPResponse to the API Gateway proxy integration response format *) 144 | 145 | AWSLambdaRuntime`Utility`HTTPResponseToProxyFormat[ 146 | httpResponse_HTTPResponse 147 | ] := Module[{ 148 | statusCode = httpResponse["StatusCode"], 149 | headers = httpResponse["CompleteHeaders"], 150 | bodyByteArray = httpResponse["BodyByteArray"] 151 | }, 152 | If[ 153 | (* if something's fishy about the HTTPResponse properties *) 154 | !And[ 155 | IntegerQ[statusCode], 156 | MatchQ[headers, {Rule[_String, _String]...}], 157 | MatchQ[bodyByteArray, _ByteArray?ByteArrayQ | {}] 158 | ], 159 | (* then return a Failure *) 160 | Return@Failure["InvalidHTTPResponse", <| 161 | "MessageTemplate" -> "The HTTPResponse expression returned by the handler is not valid" 162 | |>] 163 | ]; 164 | 165 | Return@<| 166 | "statusCode" -> statusCode, 167 | "multiValueHeaders" -> GroupBy[headers, First -> Last], 168 | 169 | If[ 170 | TrueQ@AWSLambdaRuntime`Handler`$AWSLambdaUsePlainTextResponse, 171 | <| 172 | "body" -> ByteArrayToString[bodyByteArray], 173 | "isBase64Encoded" -> False 174 | |>, 175 | <| 176 | "body" -> Replace[bodyByteArray, { 177 | ba_ByteArray :> BaseEncode[ba], 178 | {} -> "" 179 | }], 180 | "isBase64Encoded" -> True 181 | |> 182 | ] 183 | |> 184 | ] 185 | 186 | 187 | End[] 188 | 189 | EndPackage[] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wolfram Language runtime for AWS Lambda 2 | 3 | ## Introduction 4 | 5 | The Wolfram Language runtime for [AWS Lambda](https://aws.amazon.com/lambda/) is a [Lambda container image runtime](https://docs.aws.amazon.com/lambda/latest/dg/lambda-images.html) that allows you to write [Lambda functions](https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-concepts.html#gettingstarted-concepts-function) using the [Wolfram Language](https://www.wolfram.com/language/). 6 | 7 | You can use the WL Lambda runtime to deploy your Wolfram Language code scalably in AWS' global infrastructure and integrate it with other applications and clients both within and outside of the AWS cloud. 8 | 9 | You can also integrate the WL Lambda runtime with [Amazon API Gateway](https://aws.amazon.com/api-gateway/) to host Wolfram Language-based web applications, such as [APIs](https://reference.wolfram.com/language/guide/CreatingAnInstantAPI.html) and [web forms](https://reference.wolfram.com/language/guide/CreatingFormsAndApps.html), on AWS Lambda. 10 | 11 | ## Quick reference 12 | 13 | - **Maintained by**: [Wolfram Research](https://www.wolfram.com/) 14 | - **Where to get help**: [Wolfram Community](https://community.wolfram.com/), [GitHub issue tracker](https://github.com/WolframResearch/AWSLambda-WolframLanguage/issues) 15 | - **Source code**: [GitHub repository](https://github.com/WolframResearch/AWSLambda-WolframLanguage) 16 | - **Container image**: [Docker Hub repository](https://hub.docker.com/r/wolframresearch/aws-lambda-wolframlanguage) 17 | - **License**: 18 | - The contents of the [Wolfram Language Lambda Runtime source code repository](https://github.com/WolframResearch/AWSLambda-WolframLanguage) are available under the [MIT License](https://github.com/WolframResearch/AWSLambda-WolframLanguage/blob/master/LICENSE). 19 | - The Wolfram Engine product contained within the runtime container image is proprietary software, subject to the [terms of use](http://www.wolfram.com/legal/terms/wolfram-engine.html) listed on the Wolfram Research website. 20 | - To use the Wolfram Engine, you will need to sign up for a [(free) developer license](https://www.wolfram.com/developer-license). The developer license requires the creation of a Wolfram ID and acceptance of the terms of use. 21 | 22 | 23 | ## Function modes 24 | 25 | The WL Lambda runtime supports two main modes of operation: 26 | 27 | ### Raw mode _([walkthrough »](https://github.com/WolframResearch/AWSLambda-WolframLanguage/blob/master/Examples/aws-sam/raw-mode/README.md))_ 28 | 29 | Raw-mode functions behave like conventional Lambda functions written in languages such as [JavaScript](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html) and [Python](https://docs.aws.amazon.com/lambda/latest/dg/python-handler.html). Raw-mode functions are written as ordinary Wolfram Language functions. Raw-mode functions accept JSON data as input and return JSON or binary data as output. 30 | 31 | A raw-mode Lambda function can be written as a pure function accepting an association of deserialized JSON data, like: 32 | ```wl 33 | Function[<| 34 | "reversed" -> StringReverse[#inputString] 35 | |>] 36 | ``` 37 | This function would accept input JSON like: 38 | ```json 39 | {"inputString": "Hello World"} 40 | ``` 41 | ...and return as output (automatically serialized to JSON): 42 | ```json 43 | {"reversed": "dlroW olleH"} 44 | ``` 45 | 46 | Raw-mode functions can be [invoked](https://docs.aws.amazon.com/lambda/latest/dg/lambda-invocation.html) using the [AWS Lambda API](https://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html); [AWS SDKs](https://aws.amazon.com/tools/), the [AWS CLI](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/invoke.html) and other AWS tools; and [other AWS services](https://docs.aws.amazon.com/lambda/latest/dg/lambda-services.html) such as [Lex](https://docs.aws.amazon.com/lambda/latest/dg/services-lex.html), [S3](https://docs.aws.amazon.com/lambda/latest/dg/with-s3.html) and [SNS](https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html). Wolfram Language clients can invoke arbitrary Lambda functions using the [AWS service connection](https://reference.wolfram.com/language/ref/service/AWS.html). 47 | 48 | For a complete walkthrough of deploying an raw-mode function, see the [raw mode example](https://github.com/WolframResearch/AWSLambda-WolframLanguage/blob/master/Examples/aws-sam/raw-mode/README.md). The template and code in this example can be adapted for your own applications. 49 | 50 | ### HTTP mode _([walkthrough »](https://github.com/WolframResearch/AWSLambda-WolframLanguage/blob/master/Examples/aws-sam/http-mode/README.md))_ 51 | 52 | HTTP-mode functions are intended to integrate with an [Amazon API Gateway](https://aws.amazon.com/api-gateway/) API and [proxy integration](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-create-api-as-simple-proxy). Much like applications using the [Wolfram Web Engine for Python](https://github.com/WolframResearch/WolframWebEngineForPython), HTTP-mode functions are written using high-level HTTP-aware primitives such as [`APIFunction`](https://reference.wolfram.com/language/ref/APIFunction.html) and [`FormFunction`](https://reference.wolfram.com/language/ref/FormFunction.html). Any primitive supported by the Wolfram Language function [`GenerateHTTPResponse`](https://reference.wolfram.com/language/ref/GenerateHTTPResponse.html) can be used in an HTTP-mode function. HTTP-mode functions accept HTTP request data as input and return HTTP response data as output. 53 | 54 | An HTTP-mode Lambda function can be written using Wolfram Language HTTP-aware primitives, such as [`FormPage`](https://reference.wolfram.com/language/ref/FormPage.html): 55 | ``` 56 | FormPage[ 57 | {"image" -> "Image"}, 58 | ImageEffect[#image, "Charcoal"] & 59 | ] 60 | ``` 61 | When deployed to AWS Lambda and Amazon API Gateway, the form page is accessible in a web browser via an API Gateway URL: 62 | 63 | ![HTML page served by a FormPage in an HTTP-mode Lambda function](https://raw.githubusercontent.com/WolframResearch/AWSLambda-WolframLanguage/master/Examples/.images/HTTP-Function-FormPage.png) 64 | 65 | HTTP-mode functions can be invoked via API Gateway by a web browser or any HTTP-capable program, including by Wolfram Language-based clients. 66 | 67 | For a complete walkthrough of deploying an HTTP-mode function, see the [HTTP mode example](https://github.com/WolframResearch/AWSLambda-WolframLanguage/blob/master/Examples/aws-sam/http-mode/README.md). The template and code in this example can be adapted for your own applications. 68 | 69 | 70 | ## Container image information 71 | 72 | ### Supported image tags 73 | 74 | - `latest` [_(Dockerfile)_](https://github.com/WolframResearch/AWSLambda-WolframLanguage/blob/master/Dockerfile) 75 | 76 | 77 | ### Image variants 78 | 79 | #### `wolframresearch/aws-lambda-wolframlanguage:latest` 80 | 81 | *Base: [`wolframresearch/wolframengine:latest`](https://hub.docker.com/r/wolframresearch/wolframengine)* 82 | This image is based on the `latest` tag of the `wolframresearch/wolframengine` image, and hence contains the latest version of the [Wolfram Engine](https://www.wolfram.com/engine/). -------------------------------------------------------------------------------- /AWSLambdaRuntime/Kernel/API.wl: -------------------------------------------------------------------------------- 1 | BeginPackage["AWSLambdaRuntime`API`"] 2 | 3 | AWSLambdaRuntime`API`GetNextInvocation 4 | AWSLambdaRuntime`API`SendInvocationResponse 5 | AWSLambdaRuntime`API`SendInvocationError 6 | AWSLambdaRuntime`API`ExitWithInitializationError 7 | 8 | Begin["`Private`"] 9 | 10 | Needs["AWSLambdaRuntime`Utility`"] 11 | 12 | (* ::Section:: *) 13 | (* API requests *) 14 | 15 | (* ::Subsection:: *) 16 | (* Long-poll for next invocation *) 17 | 18 | AWSLambdaRuntime`API`GetNextInvocation[] := Module[{ 19 | request, 20 | response 21 | }, 22 | request = buildAPIRequest[<| 23 | "Method" -> "GET", 24 | "Path" -> "runtime/invocation/next" 25 | |>]; 26 | response = handleAPIResponseError@URLRead[ 27 | request, 28 | TimeConstraint -> Infinity 29 | ]; 30 | AWSLambdaRuntime`Utility`DebugEcho[response, {DateList[], "Received invocation"}]; 31 | 32 | If[ 33 | (* if the request failed *) 34 | FailureQ[response], 35 | (* then print an error and exit (to avoid 36 | getting stuck in an infinite loop) *) 37 | Print["RUNTIME ERROR: " <> response["Message"]]; 38 | Exit[43]; 39 | ]; 40 | 41 | Return[response] 42 | ] 43 | 44 | (* ::Subsection:: *) 45 | (* Send invocation response *) 46 | 47 | (* ::Subsubsection:: *) 48 | (* Verbatim ByteArray *) 49 | 50 | (* TODO: something about empty ByteArrays being {} *) 51 | 52 | AWSLambdaRuntime`API`SendInvocationResponse[ 53 | requestID_String, 54 | data_ByteArray 55 | ] := Module[{ 56 | request, 57 | response 58 | }, 59 | request = buildAPIRequest[<| 60 | "Method" -> "POST", 61 | "Path" -> {"runtime/invocation", requestID, "response"}, 62 | "Body" -> data 63 | |>]; 64 | AWSLambdaRuntime`Utility`DebugEcho[request, {DateList[], "Sending response"}]; 65 | 66 | response = handleAPIResponseError@URLRead[request]; 67 | AWSLambdaRuntime`Utility`DebugEcho[response, {DateList[], "Sent response"}]; 68 | 69 | If[ 70 | FailureQ[response], 71 | Print["RUNTIME ERROR: " <> response["Message"]] 72 | ]; 73 | ] 74 | 75 | (* ::Subsubsection:: *) 76 | (* Verbatim string *) 77 | 78 | AWSLambdaRuntime`API`SendInvocationResponse[ 79 | requestID_String, 80 | str_String 81 | ] := AWSLambdaRuntime`API`SendInvocationResponse[ 82 | requestID, 83 | StringToByteArray[str] 84 | ] 85 | 86 | (* ::Subsubsection:: *) 87 | (* Failure (to invocation error) *) 88 | 89 | AWSLambdaRuntime`API`SendInvocationResponse[ 90 | requestID_String, 91 | failure_Failure 92 | ] := AWSLambdaRuntime`API`SendInvocationError[requestID, failure] 93 | 94 | (* ::Subsubsection:: *) 95 | (* ExportForm wrapper (for GenerateHTTPResponse) *) 96 | 97 | AWSLambdaRuntime`API`SendInvocationResponse[ 98 | requestID_String, 99 | wrapper:ExportForm[expr_, format_, ___] 100 | ] := Module[{ 101 | responseBytes = GenerateHTTPResponse[wrapper]["BodyByteArray"] 102 | }, 103 | If[ 104 | (* if the handler output was successfully serialized *) 105 | ByteArrayQ[responseBytes], 106 | (* then send it *) 107 | AWSLambdaRuntime`API`SendInvocationResponse[ 108 | requestID, 109 | responseBytes 110 | ], 111 | (* else send an error *) 112 | AWSLambdaRuntime`API`SendInvocationError[ 113 | requestID, 114 | Failure["SerializationFailure", <| 115 | "MessageTemplate" -> StringJoin[{ 116 | "Handler output expression with head `head` could not ", 117 | "be exported to format `format`" 118 | }], 119 | "MessageParameters" -> <| 120 | "head" -> ToString[Head[expr], InputForm], 121 | "format" -> ToString[Head[format], InputForm] 122 | |> 123 | |>] 124 | ] 125 | ] 126 | ] 127 | 128 | (* ::Subsubsection:: *) 129 | (* HTTPRedirect/HTTPErrorResponse (to HTTPResponse) *) 130 | 131 | AWSLambdaRuntime`API`SendInvocationResponse[ 132 | requestID_String, 133 | expr:(_HTTPRedirect | _HTTPErrorResponse) 134 | ] := AWSLambdaRuntime`API`SendInvocationResponse[ 135 | requestID, 136 | GenerateHTTPResponse[expr] 137 | ] 138 | 139 | (* ::Subsubsection:: *) 140 | (* HTTPResponse (to API Gateway proxy response format JSON) *) 141 | 142 | AWSLambdaRuntime`API`SendInvocationResponse[ 143 | requestID_String, 144 | response_HTTPResponse 145 | ] := AWSLambdaRuntime`API`SendInvocationResponse[ 146 | requestID, 147 | AWSLambdaRuntime`Utility`HTTPResponseToProxyFormat[response] 148 | ] 149 | 150 | (* ::Subsubsection:: *) 151 | (* Arbitrary expression (to JSON) *) 152 | 153 | AWSLambdaRuntime`API`SendInvocationResponse[ 154 | requestID_String, 155 | expr_ 156 | ] := Module[{ 157 | responseBytes = ExportByteArray[ 158 | expr, 159 | "RawJSON", 160 | "Compact" -> True 161 | ] 162 | }, 163 | If[ 164 | (* if the handler output was successfully serialized to JSON *) 165 | ByteArrayQ[responseBytes], 166 | (* then send it *) 167 | AWSLambdaRuntime`API`SendInvocationResponse[ 168 | requestID, 169 | responseBytes 170 | ], 171 | (* else send an error *) 172 | AWSLambdaRuntime`API`SendInvocationError[ 173 | requestID, 174 | Failure["SerializationFailure", <| 175 | "MessageTemplate" -> StringJoin[{ 176 | "Handler output expression with head `head` could not ", 177 | "be serialized to JSON" 178 | }], 179 | "MessageParameters" -> <| 180 | "head" -> ToString[Head[expr], InputForm] 181 | |> 182 | |>] 183 | ] 184 | ] 185 | ] 186 | 187 | (* ::Subsection:: *) 188 | (* Runtime initialization error *) 189 | 190 | AWSLambdaRuntime`API`SendInvocationError[ 191 | requestID_String, 192 | failure_Failure 193 | ] := Module[{ 194 | request, 195 | response 196 | }, 197 | request = buildAPIRequest[<| 198 | "Method" -> "POST", 199 | "Path" -> {"runtime/invocation", requestID, "error"}, 200 | failureToErrorRequest[failure] 201 | |>]; 202 | AWSLambdaRuntime`Utility`DebugEcho[request, {DateList[], "Sending error"}]; 203 | 204 | response = handleAPIResponseError@URLRead[request]; 205 | AWSLambdaRuntime`Utility`DebugEcho[response, {DateList[], "Sent error"}]; 206 | 207 | If[ 208 | FailureQ[response], 209 | Print["RUNTIME ERROR: " <> response["Message"]] 210 | ]; 211 | ] 212 | 213 | (* ::Subsection:: *) 214 | (* Runtime initialization error (send and immediately exit) *) 215 | 216 | AWSLambdaRuntime`API`ExitWithInitializationError[ 217 | failure_Failure 218 | ] := Module[{ 219 | request, 220 | response 221 | }, 222 | Print["RUNTIME ERROR: " <> failure["Message"]]; 223 | request = buildAPIRequest[<| 224 | "Method" -> "POST", 225 | "Path" -> "runtime/init/error", 226 | failureToErrorRequest[failure] 227 | |>]; 228 | AWSLambdaRuntime`Utility`DebugEcho[request, {DateList[], "Sending init error"}]; 229 | 230 | response = handleAPIResponseError@URLRead[request]; 231 | AWSLambdaRuntime`Utility`DebugEcho[response, {DateList[], "Sent init error"}]; 232 | 233 | Exit[41] 234 | ] 235 | 236 | AWSLambdaRuntime`API`ExitWithInitializationError[ 237 | ___ 238 | ] := AWSLambdaRuntime`API`ExitWithInitializationError[ 239 | Failure["UnknownFailure", <| 240 | "MessageTemplate" -> "An unknown failure occurred." 241 | |>] 242 | ] 243 | 244 | (* ::Section:: *) 245 | (* Utilities *) 246 | 247 | (* ::Subsection:: *) 248 | (* buildAPIRequest - build an HTTPRequest to the runtime API endpoint *) 249 | 250 | buildAPIRequest[requestData_Association] := Module[{}, 251 | Return@HTTPRequest[<| 252 | requestData, 253 | "Scheme" -> "http", 254 | "Domain" -> AWSLambdaRuntime`$LambdaRuntimeAPIHost, 255 | "Path" -> Flatten@{ 256 | AWSLambdaRuntime`$LambdaRuntimeAPIVersion, 257 | Lookup[requestData, "Path", {}] 258 | } 259 | |>] 260 | ] 261 | 262 | 263 | (* ::Subsection:: *) 264 | (* failureToErrorRequest - convert a Failure expression into request parameters *) 265 | 266 | failureToErrorRequest[failure_Failure] := Module[{ 267 | failureTag = Replace[ 268 | failure["Tag"], 269 | Except[_String] -> "UnknownFailure" 270 | ] 271 | }, 272 | Return@<| 273 | "ContentType" -> "application/vnd.aws.lambda.error+json", 274 | "Headers" -> <| 275 | "Lambda-Runtime-Function-Error-Type" -> failureTag 276 | |>, 277 | "Body" -> ExportByteArray[<| 278 | "errorType" -> failureTag, 279 | "errorMessage" -> ToString@Replace[ 280 | failure["Message"], 281 | Except[_String] -> "An unknown failure occurred." 282 | ] 283 | |>, "RawJSON"] 284 | |> 285 | ] 286 | 287 | 288 | (* ::Subsection:: *) 289 | (* handleAPIResponseError - catch error responses based on status code; pass through success *) 290 | 291 | (* Failure from URLRead *) 292 | handleAPIResponseError[failure_Failure] := failure 293 | 294 | (* HTTP response indicating a failure *) 295 | handleAPIResponseError[response_HTTPResponse] := Switch[ 296 | response["StatusCode"], 297 | 298 | _Integer?(Between[{200, 299}]), 299 | Return[response], 300 | 301 | (* per API spec: "Container error. Non-recoverable state. 302 | Runtime should exit promptly." *) 303 | 500, 304 | Print@StringTemplate[ 305 | "RUNTIME ERROR: Non-recoverable container error (code `1`)" 306 | ][response["StatusCode"]]; 307 | If[ 308 | (* if there's a nonempty response body *) 309 | StringLength[response["Body"]] > 0, 310 | (* then print it *) 311 | Print[response["Body"]] 312 | ]; 313 | Exit[42], 314 | 315 | 400 | 403 | 413, 316 | Return[errorResponseToFailure[response]], 317 | 318 | _, 319 | Return@Failure["UnknownStatusCode", <| 320 | "MessageTemplate" -> "Unknown response status code `1`", 321 | "MessageParameters" -> {response["StatusCode"]} 322 | |>] 323 | ] 324 | 325 | 326 | (* ::Subsection:: *) 327 | (* errorResponseToFailure - convert a JSON error response to a Failure *) 328 | 329 | errorResponseToFailure[response_HTTPResponse] := Module[{ 330 | errorData = Replace[ 331 | Quiet@ImportByteArray[response["BodyByteArray"], "RawJSON"], 332 | 333 | (* handle a failed parse *) 334 | Except[_Association] -> <| 335 | "errorMessage" -> Replace[ 336 | StringTrim[response["Body"]], 337 | ("" | Except[_String]) -> "Unknown error" 338 | ], 339 | "errorType" -> "UnknownError" 340 | |> 341 | ] 342 | }, 343 | Return@Failure[ 344 | Lookup[errorData, "errorType", "UnknownError"], 345 | <| 346 | "MessageTemplate" -> "`message` (code `code`)", 347 | "MessageParameters" -> <| 348 | "message" -> Lookup[errorData, "errorMessage", "Unknown error"], 349 | "code" -> response["StatusCode"] 350 | |>, 351 | "StatusCode" -> response["StatusCode"] 352 | |> 353 | ] 354 | ] 355 | 356 | End[] 357 | 358 | EndPackage[] -------------------------------------------------------------------------------- /AWSLambdaRuntime/Kernel/Modes/HTTP.wl: -------------------------------------------------------------------------------- 1 | BeginPackage["AWSLambdaRuntime`Modes`HTTP`"] 2 | 3 | AWSLambdaRuntime`Modes`ValidateHandler 4 | AWSLambdaRuntime`Modes`EvaluateHandler 5 | 6 | Begin["`Private`"] 7 | 8 | Needs["AWSLambdaRuntime`API`"] 9 | Needs["AWSLambdaRuntime`Utility`"] 10 | 11 | (* ::Section:: *) 12 | (* Initialize mode implementation (load dependencies) *) 13 | 14 | AWSLambdaRuntime`Modes`InitializeMode["HTTP"] := ( 15 | Needs["Forms`"]; 16 | Needs["MIMETools`"]; 17 | ) 18 | 19 | (* ::Section:: *) 20 | (* Validate handler (called during initialization) *) 21 | 22 | (* valid *) 23 | AWSLambdaRuntime`Modes`ValidateHandler[ 24 | "HTTP", 25 | handler_ 26 | ] := Success["Valid", <||>] 27 | 28 | (* ::Section:: *) 29 | (* Evaluate handler *) 30 | 31 | AWSLambdaRuntime`Modes`EvaluateHandler[ 32 | "HTTP", 33 | handler_, 34 | requestBody_, 35 | requestContextData_ 36 | ] := Module[{ 37 | httpRequest, 38 | rawRequestMetadata, 39 | 40 | requesterIPAddress, 41 | requestUserAgent, 42 | apiGatewayRequestID, 43 | 44 | pathParameters, 45 | dispatchPathString, 46 | 47 | httpRequestData, 48 | 49 | httpResponse 50 | }, 51 | If[ 52 | !Association[requestBody], 53 | Return@Failure["InvalidRequestBody", <| 54 | "MessageTemplate" -> "The request is not in the expected proxy request format" 55 | |>] 56 | ]; 57 | 58 | httpRequest = AWSLambdaRuntime`Utility`ProxyFormatToHTTPRequest[ 59 | requestBody 60 | ]; 61 | 62 | (* raw data to make available to the handler *) 63 | rawRequestMetadata = KeyDrop[requestBody, "body"]; 64 | 65 | requesterIPAddress = Replace[ 66 | requestBody["requestContext", "identity", "sourceIp"], 67 | Except[_String] -> None 68 | ]; 69 | requestUserAgent = SelectFirst[ 70 | { 71 | Lookup[ 72 | (* check the raw headers because "UserAgent" 73 | defaults to "Wolfram HTTPClient" *) 74 | httpRequest["RawLowerHeaders"], 75 | "user-agent" 76 | ], 77 | 78 | requestBody["requestContext", "identity", "userAgent"] 79 | }, 80 | StringQ, 81 | None 82 | ]; 83 | apiGatewayRequestID = Replace[ 84 | requestBody["requestContext", "requestId"], 85 | Except[_String] -> None 86 | ]; 87 | 88 | 89 | pathParameters = Lookup[ 90 | requestBody, 91 | "pathParameters", 92 | <||> 93 | ] // Replace[Null -> <||>]; 94 | 95 | dispatchPathString = Lookup[ 96 | pathParameters, 97 | First[ 98 | StringCases[ 99 | Lookup[requestBody, "resource", "/"], 100 | "{" ~~ paramName:(Except["}"]..) ~~ "+}" :> paramName 101 | ], 102 | None 103 | ], 104 | "/" 105 | ] // Replace[Except[_String] -> "/"] // URLDecode; 106 | 107 | (* prepend a slash if there isn't one already *) 108 | If[ 109 | !StringStartsQ[dispatchPathString, "/"], 110 | dispatchPathString = "/" <> dispatchPathString 111 | ]; 112 | 113 | 114 | (* for second argument of GenerateHTTPResponse *) 115 | httpRequestData = <| 116 | (* extract the relevant properties of the HTTPRequest *) 117 | httpRequest[{ 118 | "Method", 119 | "Scheme", "User", "Domain", "Port", "PathString", "QueryString", "Fragment", 120 | "Cookies", 121 | "BodyByteArray" 122 | }], 123 | 124 | "DispatchPathString" -> dispatchPathString, 125 | 126 | (* avoid the auto-inserted user-agent header in "Headers" *) 127 | "Headers" -> httpRequest["RawHeaders"], 128 | 129 | "Parameters" -> Join[ 130 | httpRequest["Query"] // Replace[Except[_List] -> {}], 131 | parseHTTPRequestURLEncodedParameters[httpRequest] 132 | ], 133 | 134 | "MultipartElements" -> parseHTTPRequestMultipartElements[httpRequest], 135 | 136 | "RequesterAddress" -> requesterIPAddress, 137 | "SessionID" -> apiGatewayRequestID, 138 | 139 | "AWSLambdaContextData" -> requestContextData, 140 | "AWSLambdaRawRequestMetadata" -> rawRequestMetadata 141 | |>; 142 | 143 | 144 | httpResponse = With[{ 145 | $handler = handler, 146 | $httpRequestData = httpRequestData 147 | }, 148 | Block[{ 149 | System`$RequesterAddress = requesterIPAddress, 150 | System`$UserAgentString = requestUserAgent, 151 | AWSLambdaRuntime`Handler`$AWSLambdaRawRequestMetadata = rawRequestMetadata 152 | }, 153 | (* TODO: catch explictly thrown failures (Confirm/Enclose) 154 | and return them verbatim so they get converted to 155 | invocation errors *) 156 | AWSLambdaRuntime`Utility`WithCleanContext@GenerateHTTPResponse[ 157 | $handler, 158 | $httpRequestData 159 | ] 160 | ] 161 | ]; 162 | 163 | Return[httpResponse] 164 | ] 165 | 166 | 167 | (* ::Section:: *) 168 | (* Request body parsing *) 169 | 170 | (* ::Subsection:: *) 171 | (* URL-encoded requests *) 172 | 173 | (* ::Subsubsection:: *) 174 | (* parseHTTPRequestURLEncodedParameters - parse the URL-encoded body of an HTTPRequest into a list, or {} if not applicable *) 175 | 176 | parseHTTPRequestURLEncodedParameters[request_HTTPRequest] := Module[{ 177 | parameters 178 | }, 179 | If[ 180 | (* if the Content-Type isn't application/x-www-form-urlencoded *) 181 | !And[ 182 | StringQ[request["ContentType"]], 183 | StringStartsQ[request["ContentType"], "application/x-www-form-urlencoded"] 184 | ], 185 | (* then return None *) 186 | Return@{} 187 | ]; 188 | 189 | parameters = URLQueryDecode@ByteArrayToString[request["BodyByteArray"]]; 190 | 191 | Return@Switch[parameters, 192 | {Rule[_String, _String] ..}, 193 | parameters, 194 | 195 | _, 196 | {} 197 | ] 198 | ] 199 | 200 | (* ::Subsection:: *) 201 | (* Multipart requests *) 202 | 203 | (* ::Subsubsection:: *) 204 | (* httpRequestIsMultipartQ - return whether an HTTPRequest's Content-Type is multipart *) 205 | 206 | httpRequestIsMultipartQ[request_HTTPRequest] := TrueQ@And[ 207 | StringQ[request["ContentType"]], 208 | StringStartsQ[ToLowerCase[request["ContentType"]], "multipart"] 209 | ] 210 | 211 | (* ::Subsubsection:: *) 212 | (* parseHTTPRequestMultipartElements - parse the body of an HTTPRequest into a MultipartElements list, or None if not applicable *) 213 | 214 | parseHTTPRequestMultipartElements[request_HTTPRequest] := Module[{ 215 | requestHeaderBytes, 216 | mimeMessage, 217 | requestContentType, 218 | requestParts, 219 | defaultEncoding, 220 | multipartElements 221 | }, 222 | If[ 223 | (* if the request doesn't look like a nonempty multipart request *) 224 | Or[ 225 | !httpRequestIsMultipartQ[request], 226 | !ByteArrayQ[request["BodyByteArray"]] (* catches "empty ByteArrays" *) 227 | ], 228 | (* then return None *) 229 | Return[None] 230 | ]; 231 | 232 | (* reconstitute the request header so that MIMETools can parse it *) 233 | requestHeaderBytes = StringToByteArray[ 234 | StringJoin[ 235 | Map[ 236 | StringRiffle[ 237 | Replace[List @@ #, Except[_String] -> "", 1], 238 | {"", ": ", "\r\n"} 239 | ] &, 240 | request["RawHeaders"] 241 | ] 242 | ] <> "\r\n", 243 | "ISO8859-1" 244 | ]; 245 | 246 | mimeMessage = MIMETools`MIMEMessageOpen[ 247 | ByteArrayToString[ 248 | Join[ 249 | (* prepend the reconstituted header to the request body *) 250 | ByteArray[requestHeaderBytes], 251 | request["BodyByteArray"] 252 | ], 253 | "ISO8859-1" 254 | ] 255 | ] // checkMIMEToolsException; 256 | 257 | If[ 258 | (* if we couldn't open the message *) 259 | Head[mimeMessage] =!= MIMETools`MIMEMessage, 260 | (* then return None *) 261 | Return[None] 262 | ]; 263 | 264 | requestContentType = MIMETools`MIMEMessageRead[ 265 | mimeMessage, 266 | "MessageContentType" 267 | ] // checkMIMEToolsException; 268 | 269 | If[ 270 | (* if the Content-Type isn't multipart *) 271 | Not@And[ 272 | StringQ[requestContentType], 273 | TrueQ@StringStartsQ[ 274 | requestContentType, 275 | "multipart", 276 | IgnoreCase -> True 277 | ] 278 | ], 279 | (* then return None *) 280 | MIMETools`MIMEMessageClose[mimeMessage]; 281 | Return[None] 282 | ]; 283 | 284 | requestParts = MIMETools`MIMEMessageRead[ 285 | mimeMessage, 286 | "DecodedRawAttachments" 287 | ]; 288 | MIMETools`MIMEMessageClose[mimeMessage]; 289 | 290 | If[ 291 | (* if we couldn't get the body parts *) 292 | !ListQ[requestParts], 293 | (* then return None *) 294 | Return[None] 295 | ]; 296 | 297 | defaultEncoding = getFormDataDefaultEncoding[requestParts]; 298 | 299 | multipartElements = Select[ 300 | parseMultipartFormElement[ 301 | #, 302 | "DefaultCharacterEncoding" -> Replace[defaultEncoding, Except[_String] -> None] 303 | ] & /@ requestParts, 304 | AssociationQ (* parseMultipartFormElement returns None if the element is unusable or irrelevant *) 305 | ]; 306 | 307 | (* convert from list of associations into the format GenerateHTTPResponse expects *) 308 | multipartElements = #FieldName -> # & /@ multipartElements; 309 | 310 | Return[multipartElements] 311 | ] 312 | 313 | 314 | (* ::Subsubsection:: *) 315 | (* checkMIMEToolsException - handle MIMETools exceptions *) 316 | 317 | checkMIMEToolsException[exception_MIMETools`MIMEToolsException] := ( 318 | Print["Runtime encountered MIMETools exception: ", InputForm[exception]]; 319 | $Failed 320 | ) 321 | 322 | checkMIMEToolsException[expr_] := expr 323 | 324 | 325 | (* ::Subsubsection:: *) 326 | (* getFormDataDefaultEncoding - get the encoding of a form's fields as indicated by the "_charset_" field *) 327 | 328 | getFormDataDefaultEncoding[bodyParts_List] := Module[{ 329 | charsetPart = SelectFirst[ 330 | bodyParts, 331 | And[ 332 | #["Name"] === "_charset_", 333 | !StringQ[#["FileName"]], (* field is form field, not uploaded file *) 334 | StringQ[#["Contents"]], (* has a body *) 335 | StringLength[#["Contents"]] < 50 (* the body isn't unexpectedly long *) 336 | ] &, 337 | None 338 | ] 339 | }, 340 | If[ 341 | !AssociationQ[charsetPart], 342 | Return[None] 343 | ]; 344 | 345 | Return@Replace[ 346 | CloudObject`ToCharacterEncoding[charsetPart["Contents"], None], 347 | Except[_String] -> None 348 | ] 349 | ] 350 | 351 | 352 | (* ::Subsubsection:: *) 353 | (* parseMultipartFormElement - parse one MIME entity representing a form field *) 354 | 355 | Options[parseMultipartFormElement] = {"DefaultCharacterEncoding" -> None} 356 | 357 | parseMultipartFormElement[rawEntity_Association, OptionsPattern[]] := Module[{ 358 | contentType = rawEntity["ContentType"], 359 | fieldName = rawEntity["Name"], 360 | originalFileName = Lookup[rawEntity, "FileName", None], 361 | 362 | bodyByteArray, 363 | 364 | isFormField, 365 | contentTypeEncoding, 366 | bodyString, 367 | 368 | elementData 369 | }, 370 | If[ 371 | (* if the required "name" field is missing *) 372 | !StringQ[fieldName], 373 | (* then return None (such elements will get filtered out later) *) 374 | Return[None] 375 | ]; 376 | 377 | bodyByteArray = StringToByteArray[ 378 | Lookup[rawEntity, "Contents", ""], 379 | "ISO8859-1" 380 | ]; 381 | 382 | isFormField = !StringQ[originalFileName]; 383 | 384 | If[ 385 | (* if the part is a form field (rather than an uploaded file) *) 386 | isFormField, 387 | 388 | (* then look for an indicated charset and use it to decode the body *) 389 | contentTypeEncoding = Lookup[ 390 | rawEntity, 391 | "CharacterEncoding", 392 | OptionValue["DefaultCharacterEncoding"], 393 | CloudObject`ToCharacterEncoding[#, None] & 394 | ]; 395 | 396 | If[ 397 | (* if an encoding was specified *) 398 | StringQ[contentTypeEncoding], 399 | (* then try to decode the body *) 400 | bodyString = ByteArrayToString[bodyByteArray, contentTypeEncoding]; 401 | ]; 402 | ]; 403 | 404 | (* TODO: support writing bodies above some configurable threshold to temp files *) 405 | elementData = <| 406 | (* if a string was decoded, then use it; otherwise use the raw ByteArray *) 407 | "ContentString" -> If[StringQ[bodyString], bodyString, bodyByteArray], 408 | 409 | "FieldName" -> fieldName, 410 | "ContentType" -> contentType, 411 | "OriginalFileName" -> originalFileName, 412 | "ByteCount" -> Length[bodyByteArray], 413 | "FormField" -> isFormField, 414 | "InMemory" -> True 415 | |>; 416 | 417 | Return[elementData] 418 | ] 419 | 420 | End[] 421 | 422 | EndPackage[] -------------------------------------------------------------------------------- /AWSLambdaRuntime/Kernel/AWSLambdaRuntime.wl: -------------------------------------------------------------------------------- 1 | BeginPackage["AWSLambdaRuntime`"] 2 | 3 | AWSLambdaRuntime`StartRuntime 4 | 5 | Begin["`Private`"] 6 | 7 | AWSLambdaRuntime`Utility`DebugLogTiming["Before loading dependencies"] 8 | Block[{$ContextPath}, 9 | Needs["CloudObject`"]; 10 | Needs["CURLLink`"]; 11 | ] 12 | AWSLambdaRuntime`Utility`DebugLogTiming["After loading dependencies"] 13 | 14 | Needs["AWSLambdaRuntime`API`"] 15 | Needs["AWSLambdaRuntime`Modes`"] 16 | Needs["AWSLambdaRuntime`Utility`"] 17 | 18 | (* ::Section:: *) 19 | (* Environment variables *) 20 | 21 | AWSLambdaRuntime`$LambdaRuntimeAPIHost = Environment["AWS_LAMBDA_RUNTIME_API"] 22 | AWSLambdaRuntime`$LambdaRuntimeAPIVersion = "2018-06-01" 23 | 24 | 25 | (* ::Section:: *) 26 | (* Handler initialization and main loop *) 27 | 28 | AWSLambdaRuntime`StartRuntime[] := Module[{ 29 | handler, 30 | validateResult, 31 | invocationData 32 | }, 33 | AWSLambdaRuntime`Utility`DebugLogTiming["Start of StartRuntime"]; 34 | If[ 35 | (* if the API host is not set *) 36 | !StringQ[AWSLambdaRuntime`$LambdaRuntimeAPIHost], 37 | (* then print an error and quit (we can't emit a proper initialization error 38 | to the API if we don't know what the API host is...) *) 39 | Print["FATAL RUNTIME ERROR: AWS_LAMBDA_RUNTIME_API environment variable not set"]; 40 | Exit[40]; 41 | ]; 42 | 43 | (* load the handler expression from the handler file *) 44 | AWSLambdaRuntime`Utility`DebugLogTiming["Before loading handler"]; 45 | handler = loadHandler[]; 46 | AWSLambdaRuntime`Utility`DebugLogTiming["After loading handler"]; 47 | 48 | If[ 49 | (* if loadHandler failed *) 50 | FailureQ[handler], 51 | (* then emit an error and exit *) 52 | AWSLambdaRuntime`API`ExitWithInitializationError[handler] 53 | ]; 54 | 55 | (* perform initialization steps like loading dependencies *) 56 | AWSLambdaRuntime`Utility`DebugLogTiming["Before initializing handler mode"]; 57 | Block[{$ContextPath}, 58 | AWSLambdaRuntime`Modes`InitializeMode[ 59 | AWSLambdaRuntime`Handler`$AWSLambdaHandlerMode 60 | ]; 61 | ]; 62 | AWSLambdaRuntime`Utility`DebugLogTiming["After initializing handler mode"]; 63 | 64 | validateResult = AWSLambdaRuntime`Modes`ValidateHandler[ 65 | AWSLambdaRuntime`Handler`$AWSLambdaHandlerMode, 66 | handler 67 | ]; 68 | 69 | If[ 70 | (* if the handler is invalid *) 71 | FailureQ[validateResult], 72 | (* then emit an error and exit *) 73 | AWSLambdaRuntime`API`ExitWithInitializationError[validateResult] 74 | ]; 75 | 76 | AWSLambdaRuntime`Utility`DebugLogTiming["Starting main loop"]; 77 | 78 | (* main loop: long-poll for invocations and process them *) 79 | While[True, 80 | invocationData = AWSLambdaRuntime`API`GetNextInvocation[]; 81 | processInvocation[invocationData, handler]; 82 | ]; 83 | 84 | Exit[0] 85 | ] 86 | 87 | (* ::Subsection:: *) 88 | (* Load user handler *) 89 | 90 | (* "Raw" or "HTTP"; 91 | used if AWSLambdaRuntime`Handler`$AWSLambdaHandlerMode isn't set *) 92 | $defaultHandlerMode = "Raw" 93 | 94 | loadHandler[] := Module[{ 95 | taskRootDirectory = Lookup[ 96 | GetEnvironment[], 97 | "LAMBDA_TASK_ROOT", 98 | "/var/task" 99 | ], 100 | handlerSpec = Lookup[ 101 | GetEnvironment[], 102 | "_HANDLER", 103 | "app" 104 | ], 105 | allowedHandlerFileExtensions = "wl" | "m" | "mx" | "wxf", 106 | 107 | handlerFileBaseName, 108 | handlerName, 109 | matchingHandlerFilenames, 110 | handlerFileName, 111 | 112 | handlerFileReturnValue 113 | }, 114 | If[ 115 | (* if we can't change to the task root directory (i.e. it doesn't exist) *) 116 | FailureQ[SetDirectory[taskRootDirectory]], 117 | (* then return a Failure *) 118 | Return@Failure["SetDirectoryFailure", <| 119 | "MessageTemplate" -> "Failed to set current directory to `1`", 120 | "MessageParameters" -> {taskRootDirectory} 121 | |>] 122 | ]; 123 | 124 | {handlerFileBaseName, handlerName} = PadRight[StringSplit[handlerSpec, ".", 2], 2, None]; 125 | AWSLambdaRuntime`Handler`$AWSLambdaHandlerName = handlerName; 126 | 127 | (* can be overridden by handler file initialization code *) 128 | AWSLambdaRuntime`Handler`$AWSLambdaHandlerMode = Lookup[ 129 | GetEnvironment[], 130 | "WOLFRAM_LAMBDA_HANDLER_MODE", 131 | $defaultHandlerMode 132 | ]; 133 | 134 | matchingHandlerFilenames = FileNames[StringExpression[ 135 | handlerFileBaseName, 136 | ".", 137 | allowedHandlerFileExtensions 138 | ]]; 139 | 140 | If[ 141 | (* if there's no file matching the expected handler name pattern *) 142 | Length[matchingHandlerFilenames] === 0, 143 | (* then return a Failure *) 144 | Return@Failure["HandlerFileNotFound", <| 145 | "MessageTemplate" -> "Could not find a handler file with name `name`.[`exts`] in directory `directory`", 146 | "MessageParameters" -> <| 147 | "name" -> handlerFileBaseName, 148 | "exts" -> StringRiffle[List @@ allowedHandlerFileExtensions, ","], 149 | "directory" -> taskRootDirectory 150 | |> 151 | |>] 152 | ]; 153 | 154 | handlerFileName = ExpandFileName@First[matchingHandlerFilenames]; 155 | 156 | (* load the handler file, allowing any initialization code to run *) 157 | handlerFileReturnValue = AWSLambdaRuntime`Utility`WithCleanContext[ 158 | Get[handlerFileName] 159 | ]; 160 | 161 | handler = sanitizeHandler[handlerFileReturnValue]; 162 | 163 | If[ 164 | (* if the value returned by the handler file is invalid *) 165 | FailureQ[handler], 166 | (* then return the corresponding Failure *) 167 | Return[handler] 168 | ]; 169 | 170 | If[ 171 | (* if the handler file returned a set of multiple handlers *) 172 | AssociationQ[handler], 173 | (* then attempt to extract the one indicated in the handler spec string *) 174 | Which[ 175 | (* there's no handler expression name *) 176 | !StringQ[handlerName], 177 | Return@Failure["NoHandlerName", <| 178 | "MessageTemplate" -> StringJoin[ 179 | "The handler file returned a set of multiple handler expression ", 180 | "(`handlerNames`), but the supplied handler string \"`handlerSpec`\" ", 181 | "does not indicate a handler by name; try giving a handler string ", 182 | "like \"`handlerFileBaseName`.`firstHandlerName`\"" 183 | ], 184 | "MessageParameters" -> <| 185 | "handlerNames" -> ToString[Keys@handler, InputForm], 186 | "handlerSpec" -> handlerSpec, 187 | "handlerFileBaseName" -> handlerFileBaseName, 188 | "firstHandlerName" -> First@Keys[handler] 189 | |> 190 | |>], 191 | 192 | (* the specified handler name doesn't exist *) 193 | !KeyExistsQ[handler, handlerName], 194 | Return@Failure["NamedHandlerMissing", <| 195 | "MessageTemplate" -> StringJoin[ 196 | "The set of handler expressions (`handlerNames`) returned by the handler file does not ", 197 | "include the named handler \"`handlerName`\"" 198 | ], 199 | "MessageParameters" -> <| 200 | "handlerNames" -> StringRiffle[Keys@handler, ", "], 201 | "handlerName" -> handlerName 202 | |> 203 | |>] 204 | ]; 205 | 206 | handler = handler[handlerName] 207 | ]; 208 | 209 | Return[handler]; 210 | ] 211 | 212 | (* ::Subsubsection:: *) 213 | (* Sanitize/validate the return value from a handler file *) 214 | 215 | sanitizeHandler[ 216 | ExternalBundle[items:(_Association | {Rule[_String, _]..}), ___] 217 | ] := sanitizeHandler[items] 218 | 219 | sanitizeHandler[rules:{__Rule}] := sanitizeHandler[<|rules|>] 220 | 221 | sanitizeHandler[assoc_Association?(And[ 222 | Length[#] > 0, 223 | AllTrue[Keys[#], StringQ] 224 | ] &)] := assoc 225 | 226 | sanitizeHandler[assoc_Association] := Failure["InvalidHandler", <| 227 | "MessageTemplate" -> StringJoin[{ 228 | "The association returned by the handler file is empty or ", 229 | "does not have strings as keys" 230 | }] 231 | |>] 232 | 233 | sanitizeHandler[Null] := Failure["NullHandler", <| 234 | "MessageTemplate" -> StringJoin[{ 235 | "The handler file did not return an expression or ", 236 | "association of expressions" 237 | }] 238 | |>] 239 | 240 | sanitizeHandler[handler_] := handler 241 | 242 | (* handle weird things like Sequence *) 243 | sanitizeHandler[___] := sanitizeHandler[Null] 244 | 245 | (* ::Subsection:: *) 246 | (* Process an invocation request using the handler *) 247 | 248 | processInvocation[ 249 | invocationData_HTTPResponse, 250 | handler_ 251 | ] := Module[{ 252 | environment = GetEnvironment[], 253 | parseJSONHeader = (AWSLambdaRuntime`Utility`CapitalizeAllKeys[ 254 | ImportString[#, "RawJSON"] 255 | ] &), 256 | requestHeaders, 257 | requestID, 258 | 259 | requestBody, 260 | requestContextData, 261 | 262 | outputSpec, 263 | handlerOutput 264 | }, 265 | requestHeaders = invocationData["Headers"]; 266 | requestID = Lookup[ 267 | requestHeaders, 268 | ToLowerCase["Lambda-Runtime-Aws-Request-Id"] 269 | ]; 270 | 271 | If[ 272 | !StringQ[requestID], 273 | Print["RUNTIME ERROR: Could not get request ID"]; 274 | Exit[44]; 275 | ]; 276 | 277 | requestBody = ImportByteArray[ 278 | invocationData["BodyByteArray"], 279 | "RawJSON" 280 | ]; 281 | 282 | If[ 283 | (* if the request didn't parse *) 284 | FailureQ[requestBody], 285 | (* then emit an error and return to the main loop *) 286 | AWSLambdaRuntime`API`SendInvocationError[ 287 | requestID, 288 | Failure["InvocationParseFailure", <| 289 | "MessageTemplate" -> "Failed to parse request payload as JSON", 290 | "ImportResult" -> requestBody 291 | |>] 292 | ]; 293 | Return[] 294 | ]; 295 | 296 | requestContextData = KeySort@<| 297 | "AWSRequestID" -> requestID, 298 | 299 | (* header data *) 300 | 301 | "ExecutionDeadline" -> Lookup[ 302 | requestHeaders, 303 | ToLowerCase["Lambda-Runtime-Deadline-Ms"], 304 | Missing["NotAvailable"], 305 | FromUnixTime[FromDigits[#] / 1000] & 306 | ], 307 | "InvokedFunctionARN" -> Lookup[ 308 | requestHeaders, 309 | ToLowerCase["Lambda-Runtime-Invoked-Function-Arn"], 310 | Missing["NotAvailable"] 311 | ], 312 | "ClientContext" -> Lookup[ 313 | requestHeaders, 314 | ToLowerCase["Lambda-Runtime-Client-Context"], 315 | Missing["NotAvailable"], 316 | parseJSONHeader 317 | ], 318 | "CognitoIdentity" -> Lookup[ 319 | requestHeaders, 320 | ToLowerCase["Lambda-Runtime-Cognito-Identity"], 321 | Missing["NotAvailable"], 322 | parseJSONHeader 323 | ], 324 | 325 | 326 | (* process environment data *) 327 | 328 | "FunctionName" -> Lookup[ 329 | environment, 330 | "AWS_LAMBDA_FUNCTION_NAME", 331 | Missing["NotAvailable"] 332 | ], 333 | "FunctionVersion" -> Lookup[ 334 | environment, 335 | "AWS_LAMBDA_FUNCTION_VERSION", 336 | Missing["NotAvailable"] 337 | ], 338 | "LogGroupName" -> Lookup[ 339 | environment, 340 | "AWS_LAMBDA_LOG_GROUP_NAME", 341 | Missing["NotAvailable"] 342 | ], 343 | "LogStreamName" -> Lookup[ 344 | environment, 345 | "AWS_LAMBDA_LOG_STREAM_NAME", 346 | Missing["NotAvailable"] 347 | ], 348 | 349 | (* delayed rule to avoid causing QuantityUnits` to load *) 350 | Lookup[ 351 | environment, 352 | "AWS_LAMBDA_FUNCTION_MEMORY_SIZE", 353 | "MemoryLimit" -> Missing["NotAvailable"], 354 | With[{n = FromDigits[#]}, 355 | "MemoryLimit" :> Quantity[n, "Megabytes"] 356 | ] & 357 | ] 358 | |>; 359 | 360 | 361 | SetEnvironment[ 362 | "_X_AMZN_TRACE_ID" -> Lookup[ 363 | requestHeaders, 364 | ToLowerCase["Lambda-Runtime-Trace-Id"], 365 | None 366 | ] 367 | ]; 368 | 369 | AWSLambdaRuntime`Utility`DebugLogTiming["Before evaluating handler"]; 370 | handlerOutput = Block[{ 371 | AWSLambdaRuntime`Handler`$AWSLambdaContextData = requestContextData 372 | }, 373 | AWSLambdaRuntime`Modes`EvaluateHandler[ 374 | AWSLambdaRuntime`Handler`$AWSLambdaHandlerMode, 375 | handler, 376 | requestBody, 377 | requestContextData 378 | ] 379 | ]; 380 | AWSLambdaRuntime`Utility`DebugLogTiming["After evaluating handler"]; 381 | 382 | AWSLambdaRuntime`API`SendInvocationResponse[requestID, handlerOutput]; 383 | ] 384 | 385 | 386 | End[] 387 | 388 | EndPackage[] -------------------------------------------------------------------------------- /Examples/aws-sam/http-mode/README.md: -------------------------------------------------------------------------------- 1 | # Example AWS SAM app - HTTP mode function 2 | 3 | This document is a comprehensive walkthrough for the process of deploying an example piece of Wolfram Language code to AWS Lambda and API Gateway as an HTTP-mode function. After installing the necessary tools, you will configure Wolfram Engine licensing for your function, create an [Amazon ECR](https://aws.amazon.com/ecr/) container image repository, and finally deploy the function and API Gateway environment using the [AWS SAM](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) CLI. 4 | 5 | The [`Examples/aws-sam/http-mode`](./) directory contains source code and supporting files for an example Wolfram Language-based serverless application that you can deploy with the [AWS SAM](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) CLI. It includes the following files and folders: 6 | 7 | - [`example-http-function/`](example-http-function/): The application's Lambda function. 8 | - [`http-handler-file.wl`](example-http-function/http-handler-file.wl): Code for the function (a [`URLDispatcher`](https://reference.wolfram.com/language/ref/URLDispatcher.html) containing resources like [`APIFunction`](https://reference.wolfram.com/language/ref/APIFunction.html) and [`FormFunction`](https://reference.wolfram.com/language/ref/FormFunction.html)) 9 | - [`Dockerfile`](example-http-function/Dockerfile): Build configuration for the function's associated container image. 10 | - [`template.yaml`](template.yaml): A template that defines the application's AWS resources. 11 | 12 | The application uses several AWS resources, including a Lambda function and an [API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html) API. These resources are defined in the [`template.yaml`](template.yaml) file in this project. You can update the template to add AWS resources through the same deployment process that updates your application code. 13 | 14 | **NOTE:** The [default configuration](template.yaml) of this example enables [provisioned concurrency](https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html#configuration-concurrency-provisioned) on the application's Lambda function in order to ensure quick response times from the deployed API. Provisioned concurrency has an [associated cost](https://aws.amazon.com/lambda/pricing/#Provisioned_Concurrency_Pricing). At the time of writing, for this example function as configured, the cost in the `us-east-1` region is [~$0.0075 per hour (~$5.58/month)](https://calculator.aws/#/estimate?id=728556ecced1889b1d85f66786815ac8a397cc68), in addition to pricing based on request count and duration. If you don't want to use provisioned concurrency, you should remove the relevant lines in [`template.yaml`](template.yaml) before deploying the application. 15 | 16 | ## Clone the repository 17 | 18 | If you have not done so already, you should clone this Git repository so that the code for the example is available on your local filesystem: 19 | ```bash 20 | $ git clone https://github.com/WolframResearch/AWSLambda-WolframLanguage 21 | ``` 22 | 23 | ## Install dependencies 24 | 25 | If you already have some or all of these tools installed, you can skip the appropriate steps. 26 | 27 | To follow this walkthrough, you will need the following tools: 28 | - AWS CLI v2 - [Install the AWS CLI](https://aws.amazon.com/cli/); [Configure the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html#cli-configure-quickstart-config) 29 | - Docker - [Install Docker Engine](https://docs.docker.com/engine/install/) 30 | - AWS SAM CLI - [Install the AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) 31 | 32 | This walkthrough assumes that you have installed these tools, have access to [an AWS account](https://aws.amazon.com/premiumsupport/knowledge-center/create-and-activate-aws-account/) and [security credentials](https://docs.aws.amazon.com/general/latest/gr/aws-security-credentials.html), and have access to the Wolfram Language through a product like [Wolfram Mathematica](https://www.wolfram.com/mathematica/) or [Wolfram Engine](https://www.wolfram.com/engine/). 33 | 34 | 35 | ## Create a Wolfram Engine on-demand license entitlement 36 | 37 | In order for the [Wolfram Engine](https://www.wolfram.com/engine/) kernel inside the Lambda function's container to run, it must be [activated](https://reference.wolfram.com/language/tutorial/ActivatingMathematica.html) using **on-demand licensing**. 38 | 39 | On-demand licensing is a pay-as-you-go licensing method whereby Wolfram Engine usage is billed against your [Wolfram Service Credits](https://www.wolfram.com/service-credits/) balance at a per-kernel-hour rate. 40 | This method allows you to run arbitrary numbers of concurrent Wolfram Engine kernels for pennies per kernel per hour, and to scale up and down in a cost-effective manner. 41 | You may use the starter Service Credits quota available with a free Wolfram Cloud Basic account for initial experimentation before purchasing more Service Credits. With the instructions below, usage will be charged at 4 Service Credits per kernel per hour. 42 | 43 | An on-demand license entitlement is a reusable license key that can be used to activate one or more Wolfram Engine kernels. 44 | Creating an entitlement requires access to the Wolfram Language. 45 | If you do not have [Wolfram Mathematica](https://www.wolfram.com/mathematica/), a [Wolfram|One](https://www.wolfram.com/wolfram-one/) subscription or another Wolfram Language product, you can sign up for a free [Wolfram Cloud Basic](https://www.wolframcloud.com/) subscription and create an entitlement from within a cloud notebook. 46 | 47 | Use the [`CreateLicenseEntitlement` function](https://reference.wolfram.com/language/ref/CreateLicenseEntitlement.html) to create a new license entitlement linked to your Wolfram Account: 48 | ```wl 49 | In[1]:= entitlement = CreateLicenseEntitlement[<| 50 | "Policy" -> "WLMBDA", 51 | "StandardKernelLimit" -> 15, 52 | "LicenseExpiration" -> Quantity[1, "Week"], 53 | "EntitlementExpiration" -> Quantity[1, "Years"] 54 | |>] 55 | Out[1]= LicenseEntitlementObject["O-WLMBDA-DA42-5Z2SW6WKQQL", <| 56 | "PolicyID" -> "WLMBDA", "PolicyName" -> "AWS Lambda runtime", 57 | "BillingInterval" -> Quantity[900, "Seconds"], 58 | "KernelCosts" -> <| 59 | "Standard" -> Quantity[4., "Credits"/"Hours"], 60 | "Parallel" -> Quantity[4., "Credits"/"Hours"] 61 | |>, 62 | "KernelLimits" -> <|"Standard" -> 15, "Parallel" -> 0|>, 63 | "CreationDate" -> DateObject[{2021, 4, 28, 16, 50, 49.}, "Instant", "Gregorian", -4.], 64 | "ExpirationDate" -> DateObject[{2022, 4, 28, 16, 50, 49.}, "Instant", "Gregorian", -4.], 65 | "LicenseExpirationDuration" -> Quantity[MixedMagnitude[{7, 0.}], MixedUnit[{"Days", "Hours"}]] 66 | |>] 67 | 68 | In[2]:= entitlement["EntitlementID"] 69 | Out[2]= "O-WLMBDA-DA42-5Z2SW6WKQQL" 70 | ``` 71 | 72 | Take note of the returned entitlement ID (`O-WLMBDA-DA42-5Z2SW6WKQQL` above); you will need it when you deploy your application in a subsequent step. This entitlement ID should be treated as an application secret and not committed to source control or exposed to the public. 73 | 74 | The meanings of the specified entitlement settings are: 75 | - `"Policy" -> "WLMBDA"`: Use the `WLMBDA` licensing policy, which is tailored for use with AWS Lambda. The associated on-demand license fee is 4 [Service Credits](https://www.wolfram.com/service-credits/) per kernel per hour. 76 | - `"StandardKernelLimit" -> 15`: Up to 15 kernels may run concurrently. (This means 15 instances of your Lambda function.) 77 | - `"LicenseExpiration" -> Quantity[1, "Week"]`: Each kernel may run for up to one week at a time. 78 | - `"EntitlementExpiration" -> Quantity[1, "Years"]`: The entitlement expires one year after creation. (This means you must create a new entitlement and replace it in your application once a year.) 79 | 80 | You may adjust these settings as needed for your use case. For more information, see the documentation for [`CreateLicenseEntitlement`](https://reference.wolfram.com/language/ref/CreateLicenseEntitlement.html). 81 | 82 | 83 | ## Create an ECR repository 84 | 85 | *The instructions in this section are based on the AWS blog post ["Using container image support for AWS Lambda with AWS SAM"](https://aws.amazon.com/blogs/compute/using-container-image-support-for-aws-lambda-with-aws-sam/).* 86 | 87 | Before deploying your application, you must create an [Amazon Elastic Container Registry (ECR)](https://docs.aws.amazon.com/AmazonECR/latest/userguide/what-is-ecr.html) repository in which to store the container image for your function. 88 | 89 | **NOTE:** If you wish, you can use one repository with multiple applications. Doing so can greatly reduce the time spent on the initial push, because large layers (e.g. the OS and Wolfram Engine) can be shared between images. If you have already created the `example-wl-sam-apps` repository during the [raw-mode walkthrough](../raw-mode/README.md), you can use the `repositoryUri` from before and skip this step. 90 | 91 | To create the repository, run the following in your shell: 92 | ```bash 93 | $ aws ecr create-repository --repository-name example-wl-sam-apps 94 | ``` 95 | 96 | This will return a JSON document like: 97 | ```json 98 | { 99 | "repository": { 100 | "repositoryArn": "arn:aws:ecr:us-east-1:123456789012:repository/example-wl-sam-apps", 101 | "registryId": "123456789012", 102 | "repositoryName": "example-wl-sam-apps", 103 | "repositoryUri": "123456789012.dkr.ecr.us-east-1.amazonaws.com/example-wl-sam-apps", 104 | "createdAt": "2021-04-28T17:27:48-04:00", 105 | "imageTagMutability": "MUTABLE", 106 | "imageScanningConfiguration": { 107 | "scanOnPush": false 108 | }, 109 | "encryptionConfiguration": { 110 | "encryptionType": "AES256" 111 | } 112 | } 113 | } 114 | ``` 115 | 116 | Take note of the `repositoryUri`; you will need it when you deploy your application in the next step. 117 | 118 | Ensure that your local Docker daemon is [authenticated to your account's ECR registry](https://docs.aws.amazon.com/AmazonECR/latest/userguide/getting-started-cli.html#cli-authenticate-registry): 119 | 120 | ```bash 121 | $ aws ecr get-login-password | docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com 122 | ``` 123 | (Replace `123456789012.dkr.ecr.us-east-1.amazonaws.com` with the domain name component of the `repositoryUri` from the previous command result.) 124 | 125 | You can also install the [Amazon ECR Docker Credential Helper](https://github.com/awslabs/amazon-ecr-credential-helper) to facilitate Docker authentication with Amazon ECR. 126 | 127 | 128 | ## Deploy the example application 129 | 130 | The Serverless Application Model Command Line Interface (SAM CLI) is an extension of the AWS CLI that adds functionality for building and testing Lambda applications. It uses Docker to build a container image containing your function code, and it interfaces with [AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/Welcome.html) to deploy your application to AWS. For more information on using container image-based Lambda functions with AWS SAM, see the AWS blog post ["Using container image support for AWS Lambda with AWS SAM"](https://aws.amazon.com/blogs/compute/using-container-image-support-for-aws-lambda-with-aws-sam/). 131 | 132 | To build and deploy your application for the first time, run the following in your shell from within the [`Examples/aws-sam/http-mode`](./) directory of the cloned Git repository: 133 | 134 | ```bash 135 | $ cd Examples/aws-sam/http-mode 136 | $ sam build 137 | $ sam deploy --guided 138 | ``` 139 | 140 | The first command will build a container image from the [Dockerfile](example-http-function/Dockerfile). The second command will package and deploy your application to AWS after a series of prompts: 141 | 142 | - **Stack Name**: The name of the stack to deploy to CloudFormation. This should be unique to your account and region. In this example, we will use `example-http-wl-sam-app`. 143 | - **AWS Region**: The [AWS region](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-regions) you want to deploy your app to. 144 | - **Parameter OnDemandLicenseEntitlementID**: Your license entitlement ID from the previous step. This parameter is masked, so the text you type/paste will not be echoed back to you. 145 | - **Image Repository for ExampleHTTPFunction**: The ECR `repositoryUri` from the previous step. 146 | - **Confirm changes before deploy**: If enabled, any change sets will be shown to you for manual review before execution. If disabled, the AWS SAM CLI will automatically deploy application changes without prompting for review. 147 | - **Allow SAM CLI IAM role creation**: Enter `y`. Many AWS SAM templates, including this example, create AWS IAM roles required for the included AWS Lambda function(s) to access AWS services. By default, these are scoped down to minimum required permissions. To deploy an AWS CloudFormation stack that creates or modifies IAM roles, the `CAPABILITY_IAM` value for `capabilities` must be provided. If permission isn't provided through this prompt, to deploy this example you must explicitly pass `--capabilities CAPABILITY_IAM` to the `sam deploy` command. 148 | - **ExampleHTTPFunction may not have authorization defined, Is this okay?**: This is [a security message](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-deploying.html#serverless-deploying-troubleshooting) warning you that the API Gateway API route linked to your Lambda function is configured to be publicly accessible. You can safely bypass this warning for the purposes of this walkthrough by typing `y`. In production scenarios or situations involving sensitive data, you should evaluate the security requirements of your application. 149 | - **Save arguments to configuration file**: Enter `y`. If enabled, your choices will be saved to a `samlconfig.toml` configuration file in the current directory, so that in the future you can just re-run `sam deploy` without parameters to deploy changes to your application. 150 | - **SAM configuration file** and **SAM configuration environment**: If you enabled the previous option, these options allow you to configure how the configuration file is saved. You may leave these options at their default values. 151 | 152 | For more information, see [the documentation for `sam deploy`](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-deploy.html). 153 | 154 | The initial deployment of your application may take several minutes, as Docker will have to push the entire container image - including the Wolfram Engine base layers - to your ECR repository. Subsequent deployments will be faster, as only the changed portions of the image will be pushed. 155 | 156 | You can find the URL of your API Gateway endpoint in the output values displayed after deployment: 157 | ``` 158 | Key ExampleHTTPFunctionAPI 159 | Description API Gateway endpoint URL for Prod stage for example HTTP-based function 160 | Value https://sqn96odt1j.execute-api.us-east-1.amazonaws.com/Prod/ 161 | ``` 162 | 163 | If you visit this URL, you should see the root route of the [`URLDispatcher`](https://reference.wolfram.com/language/ref/URLDispatcher.html) from [the function's source file](example-http-function/http-handler-file.wl): 164 | 165 | ![HTML page served by the URLDispatcher in http-handler-file.wl](../../.images/HTTP-Function-URLDispatcher.png) 166 | 167 | 168 | ## Add a resource to your application 169 | 170 | The [application template](template.yaml) uses the AWS Serverless Application Model (AWS SAM) to define application resources. AWS SAM is an extension of AWS CloudFormation with a simpler syntax for configuring common serverless application resources such as functions, triggers, and APIs. For resources not included in [the SAM specification](https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md), you can use standard [AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html) resource types. 171 | 172 | 173 | ## Fetch, tail, and filter Lambda function logs 174 | 175 | To simplify troubleshooting, the SAM CLI has a command called `sam logs`. `sam logs` lets you fetch logs generated by your deployed Lambda function from the command line. 176 | 177 | **NOTE:** This command works for all AWS Lambda functions; not just the ones you deploy using SAM. 178 | 179 | ```bash 180 | $ sam logs -n ExampleHTTPFunction --stack-name example-http-wl-sam-app 181 | ``` 182 | ``` 183 | 2021/04/28/[1]394e19114a6149f097066a19c6a5da39 2021-04-28T23:02:03.084000 START RequestId: c3bf978f-59da-4bb3-9eb7-9a105fa93692 Version: 1 184 | 2021/04/28/[1]394e19114a6149f097066a19c6a5da39 2021-04-28T23:02:03.093000 >> Received request for root route 185 | 2021/04/28/[1]394e19114a6149f097066a19c6a5da39 2021-04-28T23:02:03.361000 END RequestId: c3bf978f-59da-4bb3-9eb7-9a105fa93692 186 | 2021/04/28/[1]394e19114a6149f097066a19c6a5da39 2021-04-28T23:02:03.361000 REPORT RequestId: c3bf978f-59da-4bb3-9eb7-9a105fa93692 Duration: 274.85 ms Billed Duration: 275 ms Memory Size: 512 MB Max Memory Used: 317 MB 187 | ``` 188 | 189 | You can add the `--tail` option to stream logs to your terminal in near-real time. You can find information and examples about filtering Lambda function logs in the [SAM CLI documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-logging.html). 190 | 191 | 192 | ## Cleanup 193 | 194 | To delete the sample application that you created, use the AWS CLI to delete the application's CloudFormation stack: 195 | 196 | ```bash 197 | $ aws cloudformation delete-stack --stack-name example-http-wl-sam-app 198 | ``` 199 | 200 | 201 | ## Resources 202 | 203 | See the [AWS SAM developer guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) for an introduction to SAM specification, the SAM CLI, and serverless application concepts. 204 | 205 | 206 |
207 | 208 | *This file is derived from an AWS-provided SAM CLI application template. The original document from which this walkthrough has been modified is located [here](https://github.com/aws/aws-sam-cli-app-templates/blob/de97a7aac7ee8416f3310d7bd005b391f1ff1ac0/nodejs14.x-image/cookiecutter-aws-sam-hello-nodejs-lambda-image/%7B%7Bcookiecutter.project_name%7D%7D/README.md).* 209 | *The repository containing the original document is licensed under the [Apache-2.0 License](https://github.com/aws/aws-sam-cli-app-templates/blob/115fc2d1557d70690b1826ce79d0bc033e09728e/LICENSE), and carries the following notice:* 210 | *`Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.`* -------------------------------------------------------------------------------- /Examples/aws-sam/raw-mode/README.md: -------------------------------------------------------------------------------- 1 | # Example AWS SAM app - raw mode function 2 | 3 | This document is a comprehensive walkthrough for the process of deploying a simple piece of Wolfram Language code to AWS Lambda as a raw-mode function. After installing the necessary tools, you will configure Wolfram Engine licensing for your function, create an [Amazon ECR](https://aws.amazon.com/ecr/) container image repository, and finally deploy the function using the [AWS SAM](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) CLI. 4 | 5 | The [`Examples/aws-sam/raw-mode`](./) directory contains source code and supporting files for an example Wolfram Language-based serverless application that you can deploy with the [AWS SAM](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) CLI. It includes the following files and folders: 6 | 7 | - [`example-raw-function/`](example-raw-function/): The application's Lambda function. 8 | - [`raw-handler-file.wl`](example-raw-function/raw-handler-file.wl): Code for the function (a pure function that reverses the characters in a string) 9 | - [`Dockerfile`](example-raw-function/Dockerfile): Build configuration for the function's associated container image. 10 | - [`template.yaml`](template.yaml): A template that defines the application's AWS resources. 11 | 12 | The application uses several AWS resources, including a Lambda function. These resources are defined in the [`template.yaml`](template.yaml) file in this project. You can update the template to add AWS resources through the same deployment process that updates your application code. 13 | 14 | ## Clone the repository 15 | 16 | If you have not done so already, you should clone this Git repository so that the code for the example is available on your local filesystem: 17 | ```bash 18 | $ git clone https://github.com/WolframResearch/AWSLambda-WolframLanguage 19 | ``` 20 | 21 | ## Install dependencies 22 | 23 | If you already have some or all of these tools installed, you can skip the appropriate steps. 24 | 25 | To follow this walkthrough, you will need the following tools: 26 | - AWS CLI v2 - [Install the AWS CLI](https://aws.amazon.com/cli/); [Configure the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html#cli-configure-quickstart-config) 27 | - Docker - [Install Docker Engine](https://docs.docker.com/engine/install/) 28 | - AWS SAM CLI - [Install the AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) 29 | 30 | This walkthrough assumes that you have installed these tools, have access to [an AWS account](https://aws.amazon.com/premiumsupport/knowledge-center/create-and-activate-aws-account/) and [security credentials](https://docs.aws.amazon.com/general/latest/gr/aws-security-credentials.html), and have access to the Wolfram Language through a product like [Wolfram Mathematica](https://www.wolfram.com/mathematica/) or [Wolfram Engine](https://www.wolfram.com/engine/). 31 | 32 | 33 | ## Create a Wolfram Engine on-demand license entitlement 34 | 35 | In order for the [Wolfram Engine](https://www.wolfram.com/engine/) kernel inside the Lambda function's container to run, it must be [activated](https://reference.wolfram.com/language/tutorial/ActivatingMathematica.html) using **on-demand licensing**. 36 | 37 | On-demand licensing is a pay-as-you-go licensing method whereby Wolfram Engine usage is billed against your [Wolfram Service Credits](https://www.wolfram.com/service-credits/) balance at a per-kernel-hour rate. 38 | This method allows you to run arbitrary numbers of concurrent Wolfram Engine kernels for pennies per kernel per hour, and to scale up and down in a cost-effective manner. 39 | You may use the starter Service Credits quota available with a free Wolfram Cloud Basic account for initial experimentation before purchasing more Service Credits. With the instructions below, usage will be charged at 4 Service Credits per kernel per hour. 40 | 41 | An on-demand license entitlement is a reusable license key that can be used to activate one or more Wolfram Engine kernels. 42 | Creating an entitlement requires access to the Wolfram Language. 43 | If you do not have [Wolfram Mathematica](https://www.wolfram.com/mathematica/), a [Wolfram|One](https://www.wolfram.com/wolfram-one/) subscription or another Wolfram Language product, you can sign up for a free [Wolfram Cloud Basic](https://www.wolframcloud.com/) subscription and create an entitlement from within a cloud notebook. 44 | 45 | Use the [`CreateLicenseEntitlement` function](https://reference.wolfram.com/language/ref/CreateLicenseEntitlement.html) to create a new license entitlement linked to your Wolfram Account: 46 | ```wl 47 | In[1]:= entitlement = CreateLicenseEntitlement[<| 48 | "Policy" -> "WLMBDA", 49 | "StandardKernelLimit" -> 15, 50 | "LicenseExpiration" -> Quantity[1, "Week"], 51 | "EntitlementExpiration" -> Quantity[1, "Years"] 52 | |>] 53 | Out[1]= LicenseEntitlementObject["O-WLMBDA-DA42-5Z2SW6WKQQL", <| 54 | "PolicyID" -> "WLMBDA", "PolicyName" -> "AWS Lambda runtime", 55 | "BillingInterval" -> Quantity[900, "Seconds"], 56 | "KernelCosts" -> <| 57 | "Standard" -> Quantity[4., "Credits"/"Hours"], 58 | "Parallel" -> Quantity[4., "Credits"/"Hours"] 59 | |>, 60 | "KernelLimits" -> <|"Standard" -> 15, "Parallel" -> 0|>, 61 | "CreationDate" -> DateObject[{2021, 4, 28, 16, 50, 49.}, "Instant", "Gregorian", -4.], 62 | "ExpirationDate" -> DateObject[{2022, 4, 28, 16, 50, 49.}, "Instant", "Gregorian", -4.], 63 | "LicenseExpirationDuration" -> Quantity[MixedMagnitude[{7, 0.}], MixedUnit[{"Days", "Hours"}]] 64 | |>] 65 | 66 | In[2]:= entitlement["EntitlementID"] 67 | Out[2]= "O-WLMBDA-DA42-5Z2SW6WKQQL" 68 | ``` 69 | 70 | Take note of the returned entitlement ID (`O-WLMBDA-DA42-5Z2SW6WKQQL` above); you will need it when you deploy your application in a subsequent step. This entitlement ID should be treated as an application secret and not committed to source control or exposed to the public. 71 | 72 | The meanings of the specified entitlement settings are: 73 | - `"Policy" -> "WLMBDA"`: Use the `WLMBDA` licensing policy, which is tailored for use with AWS Lambda. The associated on-demand license fee is 4 [Service Credits](https://www.wolfram.com/service-credits/) per kernel per hour. 74 | - `"StandardKernelLimit" -> 15`: Up to 15 kernels may run concurrently. (This means 15 instances of your Lambda function.) 75 | - `"LicenseExpiration" -> Quantity[1, "Week"]`: Each kernel may run for up to one week at a time. 76 | - `"EntitlementExpiration" -> Quantity[1, "Years"]`: The entitlement expires one year after creation. (This means you must create a new entitlement and replace it in your application once a year.) 77 | 78 | You may adjust these settings as needed for your use case. For more information, see the documentation for [`CreateLicenseEntitlement`](https://reference.wolfram.com/language/ref/CreateLicenseEntitlement.html). 79 | 80 | 81 | ## Create an ECR repository 82 | 83 | *The instructions in this section are based on the AWS blog post ["Using container image support for AWS Lambda with AWS SAM"](https://aws.amazon.com/blogs/compute/using-container-image-support-for-aws-lambda-with-aws-sam/).* 84 | 85 | Before deploying your application, you must create an [Amazon Elastic Container Registry (ECR)](https://docs.aws.amazon.com/AmazonECR/latest/userguide/what-is-ecr.html) repository in which to store the container image for your function. 86 | 87 | **NOTE:** If you wish, you can use one repository with multiple applications. Doing so can greatly reduce the time spent on the initial push, because large layers (e.g. the OS and Wolfram Engine) can be shared between images. If you have already created the `example-wl-sam-apps` repository during the [HTTP-mode walkthrough](../http-mode/README.md), you can use the `repositoryUri` from before and skip this step. 88 | 89 | To create the repository, run the following in your shell: 90 | ```bash 91 | $ aws ecr create-repository --repository-name example-wl-sam-apps 92 | ``` 93 | 94 | This will return a JSON document like: 95 | ```json 96 | { 97 | "repository": { 98 | "repositoryArn": "arn:aws:ecr:us-east-1:123456789012:repository/example-wl-sam-apps", 99 | "registryId": "123456789012", 100 | "repositoryName": "example-wl-sam-apps", 101 | "repositoryUri": "123456789012.dkr.ecr.us-east-1.amazonaws.com/example-wl-sam-apps", 102 | "createdAt": "2021-04-28T17:27:48-04:00", 103 | "imageTagMutability": "MUTABLE", 104 | "imageScanningConfiguration": { 105 | "scanOnPush": false 106 | }, 107 | "encryptionConfiguration": { 108 | "encryptionType": "AES256" 109 | } 110 | } 111 | } 112 | ``` 113 | 114 | Take note of the `repositoryUri`; you will need it when you deploy your application in the next step. 115 | 116 | Ensure that your local Docker daemon is [authenticated to your account's ECR registry](https://docs.aws.amazon.com/AmazonECR/latest/userguide/getting-started-cli.html#cli-authenticate-registry): 117 | 118 | ```bash 119 | $ aws ecr get-login-password | docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com 120 | ``` 121 | (Replace `123456789012.dkr.ecr.us-east-1.amazonaws.com` with the domain name component of the `repositoryUri` from the previous command result.) 122 | 123 | You can also install the [Amazon ECR Docker Credential Helper](https://github.com/awslabs/amazon-ecr-credential-helper) to facilitate Docker authentication with Amazon ECR. 124 | 125 | 126 | ## Deploy the example application 127 | 128 | The Serverless Application Model Command Line Interface (SAM CLI) is an extension of the AWS CLI that adds functionality for building and testing Lambda applications. It uses Docker to build a container image containing your function code, and it interfaces with [AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/Welcome.html) to deploy your application to AWS. For more information on using container image-based Lambda functions with AWS SAM, see the AWS blog post ["Using container image support for AWS Lambda with AWS SAM"](https://aws.amazon.com/blogs/compute/using-container-image-support-for-aws-lambda-with-aws-sam/). 129 | 130 | To build and deploy your application for the first time, run the following in your shell from within the [`Examples/aws-sam/raw-mode`](./) directory of the cloned Git repository: 131 | 132 | ```bash 133 | $ cd Examples/aws-sam/raw-mode 134 | $ sam build 135 | $ sam deploy --guided 136 | ``` 137 | 138 | The first command will build a container image from the [Dockerfile](example-raw-function/Dockerfile). The second command will package and deploy your application to AWS after a series of prompts: 139 | 140 | - **Stack Name**: The name of the stack to deploy to CloudFormation. This should be unique to your account and region. In this example, we will use `example-raw-wl-sam-app`. 141 | - **AWS Region**: The [AWS region](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-regions) you want to deploy your app to. 142 | - **Parameter OnDemandLicenseEntitlementID**: Your license entitlement ID from the previous step. This parameter is masked, so the text you type/paste will not be echoed back to you. 143 | - **Image Repository for ExampleRawFunction**: The ECR `repositoryUri` from the previous step. 144 | - **Confirm changes before deploy**: If enabled, any change sets will be shown to you for manual review before execution. If disabled, the AWS SAM CLI will automatically deploy application changes without prompting for review. 145 | - **Allow SAM CLI IAM role creation**: Enter `y`. Many AWS SAM templates, including this example, create AWS IAM roles required for the included AWS Lambda function(s) to access AWS services. By default, these are scoped down to minimum required permissions. To deploy an AWS CloudFormation stack that creates or modifies IAM roles, the `CAPABILITY_IAM` value for `capabilities` must be provided. If permission isn't provided through this prompt, to deploy this example you must explicitly pass `--capabilities CAPABILITY_IAM` to the `sam deploy` command. 146 | - **Save arguments to configuration file**: Enter `y`. If enabled, your choices will be saved to a `samlconfig.toml` configuration file in the current directory, so that in the future you can just re-run `sam deploy` without parameters to deploy changes to your application. 147 | - **SAM configuration file** and **SAM configuration environment**: If you enabled the previous option, these options allow you to configure how the configuration file is saved. You may leave these options at their default values. 148 | 149 | For more information, see [the documentation for `sam deploy`](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-deploy.html). 150 | 151 | The initial deployment of your application may take several minutes, as Docker will have to push the entire container image - including the Wolfram Engine base layers - to your ECR repository. Subsequent deployments will be faster, as only the changed portions of the image will be pushed. 152 | 153 | You can find the [ARN](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html) of your deployed Lambda function in the output values displayed after deployment: 154 | ``` 155 | Key ExampleRawFunction 156 | Description Example raw-mode Lambda function 157 | Value arn:aws:lambda:us-east-1:123456789012:function:example-raw-wl-sam-app-ExampleRawFunction-AKWQGDATPPTP 158 | ``` 159 | 160 | You can invoke this function from the AWS CLI using [`aws lambda invoke`](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/invoke.html). 161 | On Linux and macOS (Bash-like shells): 162 | ```bash 163 | $ aws lambda invoke \ 164 | --function-name arn:aws:lambda:us-east-1:123456789012:function:example-raw-wl-sam-app-ExampleRawFunction-AKWQGDATPPTP \ 165 | --payload '{"input": "Hello World"}' \ 166 | --cli-binary-format raw-in-base64-out \ 167 | /tmp/output.json && cat /tmp/output.json 168 | 169 | { 170 | "StatusCode": 200, 171 | "ExecutedVersion": "$LATEST" 172 | } 173 | {"reversed":"dlroW olleH"} 174 | ``` 175 | 176 | In Powershell on Windows: 177 | ```powershell 178 | PS C:\> aws lambda invoke --function-name arn:aws:lambda:us-east-1:123456789012:function:example-raw-wl-sam-app-ExampleRawFunction-AKWQGDATPPTP --payload '{""input"": ""Hello World""}' --cli-binary-format raw-in-base64-out output.json 179 | { 180 | "StatusCode": 200, 181 | "ExecutedVersion": "$LATEST" 182 | } 183 | 184 | PS C:\> cat .\output.json 185 | {"reversed":"dlroW olleH"} 186 | ``` 187 | 188 | 189 | In addition to the AWS CLI, raw-mode functions can be [invoked](https://docs.aws.amazon.com/lambda/latest/dg/lambda-invocation.html) using the [AWS Lambda API](https://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html); [AWS SDKs](https://aws.amazon.com/tools/); and [other AWS services](https://docs.aws.amazon.com/lambda/latest/dg/lambda-services.html) such as [Lex](https://docs.aws.amazon.com/lambda/latest/dg/services-lex.html), [S3](https://docs.aws.amazon.com/lambda/latest/dg/with-s3.html) and [SNS](https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html). Wolfram Language clients can invoke arbitrary Lambda functions using the [AWS service connection](https://reference.wolfram.com/language/ref/service/AWS.html). 190 | 191 | **NOTE:** If you wish to minimize invocation latency, consider enabling [provisioned concurrency](https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html#configuration-concurrency-provisioned) as described in the [HTTP-mode walkthrough](../http-mode/README.md). 192 | 193 | 194 | ## Add a resource to your application 195 | 196 | The [application template](template.yaml) uses the AWS Serverless Application Model (AWS SAM) to define application resources. AWS SAM is an extension of AWS CloudFormation with a simpler syntax for configuring common serverless application resources such as functions, triggers, and APIs. For resources not included in [the SAM specification](https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md), you can use standard [AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html) resource types. 197 | 198 | 199 | ## Fetch, tail, and filter Lambda function logs 200 | 201 | To simplify troubleshooting, the SAM CLI has a command called `sam logs`. `sam logs` lets you fetch logs generated by your deployed Lambda function from the command line. 202 | 203 | **NOTE:** This command works for all AWS Lambda functions; not just the ones you deploy using SAM. 204 | 205 | ```bash 206 | $ sam logs -n ExampleRawFunction --stack-name example-raw-wl-sam-app 207 | ``` 208 | ``` 209 | 2021/04/28/[$LATEST]b0d33af52c2644e6ae9c6d76a6cda134 2021-04-28T23:02:02.084000 START RequestId: f27d9984-efa0-4220-aec9-fdb863e0244e Version: $LATEST 210 | 2021/04/28/[$LATEST]b0d33af52c2644e6ae9c6d76a6cda134 2021-04-28T23:02:02.507000 Wolfram Language 12.3.0 Engine for Linux x86 (64-bit) 211 | 2021/04/28/[$LATEST]b0d33af52c2644e6ae9c6d76a6cda134 2021-04-28T23:02:05.726000 Copyright 1988-2021 Wolfram Research, Inc. 212 | 2021/04/28/[$LATEST]b0d33af52c2644e6ae9c6d76a6cda134 2021-04-28T23:02:07.424000 END RequestId: f27d9984-efa0-4220-aec9-fdb863e0244e 213 | 2021/04/28/[$LATEST]b0d33af52c2644e6ae9c6d76a6cda134 2021-04-28T23:02:07.424000 REPORT RequestId: f27d9984-efa0-4220-aec9-fdb863e0244e Duration: 169.94 ms Billed Duration: 6767 ms Memory Size: 512 MB Max Memory Used: 234 MB Init Duration: 6596.33 ms 214 | ``` 215 | 216 | You can add the `--tail` option to stream logs to your terminal in near-real time. You can find information and examples about filtering Lambda function logs in the [SAM CLI documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-logging.html). 217 | 218 | 219 | ## Cleanup 220 | 221 | To delete the sample application that you created, use the AWS CLI to delete the application's CloudFormation stack: 222 | 223 | ```bash 224 | $ aws cloudformation delete-stack --stack-name example-raw-wl-sam-app 225 | ``` 226 | 227 | 228 | ## Resources 229 | 230 | See the [AWS SAM developer guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) for an introduction to SAM specification, the SAM CLI, and serverless application concepts. 231 | 232 | 233 |
234 | 235 | *This file is derived from an AWS-provided SAM CLI application template. The original document from which this walkthrough has been modified is located [here](https://github.com/aws/aws-sam-cli-app-templates/blob/de97a7aac7ee8416f3310d7bd005b391f1ff1ac0/nodejs14.x-image/cookiecutter-aws-sam-hello-nodejs-lambda-image/%7B%7Bcookiecutter.project_name%7D%7D/README.md).* 236 | *The repository containing the original document is licensed under the [Apache-2.0 License](https://github.com/aws/aws-sam-cli-app-templates/blob/115fc2d1557d70690b1826ce79d0bc033e09728e/LICENSE), and carries the following notice:* 237 | *`Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.`* --------------------------------------------------------------------------------