Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Figure out how to deploy Datasette to AWS Lambda using function URLs and Mangum #6

Closed
simonw opened this issue Sep 18, 2022 · 65 comments
Labels

Comments

@simonw
Copy link
Owner

simonw commented Sep 18, 2022

Initial research and proof of concept for the datasette-publish-lambda plugin.

@simonw simonw changed the title One more try at Mangum Datasette Have another go at deploying Datasette to Lambda with Mangum Sep 18, 2022
@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

YouTube video from three months ago about deploying fast api on lambda https://youtu.be/RGIM4JfsSk0

Wow that video genuinely has everything I need to know - including how to make the zip file and how to deploy (via the console) and how to add a function URL

https://github.com/pixegami/fastapi-tutorial

from mangum import Mangum

app = FastAPI()
handler = Mangum(app)
pip install -t lib -r requirements.txt
(cd lib; zip ../lambda_function.zip -r .)
zip lambda_function.zip -u main.py
zip lambda_function.zip -u books.json

Looks like you need cloudfront if you want a custom domain or subdomain though: https://medium.com/@walid.karray/configuring-a-custom-domain-for-aws-lambda-function-url-with-serverless-framework-c0d78abdc253

(I bet cloudflare is easier)

Limit is still 50 MB (zipped, for direct upload) 250 MB (unzipped)

I want "datasette publish lambda"

I'm really tempted to have the datasette-publish-lambda GitHub repo build a release which is a zip file that has everything it needs - then the plugin itself downloads (and caches) the latest releases zip file, adds the SQLite DB and metadata and maybe some templates and plugins and static files to that zip file, adds any --install plugins to it and then ships the result

But the first release can build the zip file from scratch every time. That's probably simpler!

So the plugin mainly creates a working zip file for you and deploys it.

It could even have a --test option which creates the zip file, then unzips it into a temp virtual environment to test it before deploying it

Trickiest code here is the code that deploys the function. I'm VERY tempted to build a separate asgi-lambda-publish python package here that this depends on, because I'm so angry about how hard this is

Start with a TIL though.

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

Could I start by doing a full lambda deployment entirely using the AWS CLI tool?

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-awscli.html is VERY useful.

aws iam create-role \
  --role-name lambda-ex \
  --assume-role-policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"}
    ]}'

aws iam attach-role-policy \
  --role-name lambda-ex \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

Then index.js:

exports.handler = async function(event, context) {
  console.log("ENVIRONMENT VARIABLES\n" + JSON.stringify(process.env, null, 2))
  console.log("EVENT\n" + JSON.stringify(event, null, 2))
  return context.logStreamName
}

Create function.zip:

zip function.zip index.js

Next step needs account ID:

export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text) 
echo "hello:${AWS_ACCOUNT_ID}:there"

Outputs:

hello:462092780466:there

To create function:

aws lambda create-function \
  --function-name my-test-lambda-nodejs-function \
  --zip-file fileb://function.zip \
  --handler index.handler \
  --runtime nodejs16.x \
  --role "arn:aws:iam::${AWS_ACCOUNT_ID}:role/lambda-ex"

Then invoke the function like this:

aws lambda invoke --function-name my-test-lambda-nodejs-function out --log-type Tail

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html#create-url-cli shows how to add a function URL afterwards.

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

Tried out those steps, got to:

/tmp % aws lambda create-function \
  --function-name my-test-lambda-nodejs-function \
  --zip-file fileb://function.zip \
  --handler index.handler \
  --runtime nodejs16.x \
  --role "arn:aws:iam::${AWS_ACCOUNT_ID}:role/lambda-ex"
{
    "FunctionName": "my-test-lambda-nodejs-function",
    "FunctionArn": "arn:aws:lambda:us-east-1:462092780466:function:my-test-lambda-nodejs-function",
    "Runtime": "nodejs16.x",
    "Role": "arn:aws:iam::462092780466:role/lambda-ex",
    "Handler": "index.handler",
    "CodeSize": 320,
    "Description": "",
    "Timeout": 3,
    "MemorySize": 128,
    "LastModified": "2022-09-18T17:44:07.957+0000",
    "CodeSha256": "swC+abUCZgQL7frXislbhKvIYkeClJjEGa9r7WwFTCg=",
    "Version": "$LATEST",
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "62a67b98-7992-4fc9-97b5-653371bbaf28",
    "State": "Pending",
    "StateReason": "The function is being created.",
    "StateReasonCode": "Creating",
    "PackageType": "Zip",
    "Architectures": [
        "x86_64"
    ]
}

This didn't work though:

/tmp % aws lambda invoke --function-name my-test-lambda-nodejs-function out --log-type Tail
{
    "StatusCode": 200,
    "FunctionError": "Unhandled",
    "LogResult": "MjAyMi0wOS0xOFQxNzo0NDoyNS45MzhaCXVuZGVmaW5lZAlFUlJPUglVbmNhdWdodCBFeGNlcHRpb24gCXsiZXJyb3JUeXBlIjoiUnVudGltZS5Vc2VyQ29kZVN5bnRheEVycm9yIiwiZXJyb3JNZXNzYWdlIjoiU3ludGF4RXJyb3I6IEludmFsaWQgb3IgdW5leHBlY3RlZCB0b2tlbiIsInN0YWNrIjpbIlJ1bnRpbWUuVXNlckNvZGVTeW50YXhFcnJvcjogU3ludGF4RXJyb3I6IEludmFsaWQgb3IgdW5leHBlY3RlZCB0b2tlbiIsIiAgICBhdCBfbG9hZFVzZXJBcHAgKGZpbGU6Ly8vdmFyL3J1bnRpbWUvaW5kZXgubWpzOjk0ODoxNykiLCIgICAgYXQgYXN5bmMgT2JqZWN0LlVzZXJGdW5jdGlvbi5qcy5tb2R1bGUuZXhwb3J0cy5sb2FkIChmaWxlOi8vL3Zhci9ydW50aW1lL2luZGV4Lm1qczo5NzY6MjEpIiwiICAgIGF0IGFzeW5jIHN0YXJ0IChmaWxlOi8vL3Zhci9ydW50aW1lL2luZGV4Lm1qczoxMTM3OjIzKSIsIiAgICBhdCBhc3luYyBmaWxlOi8vL3Zhci9ydW50aW1lL2luZGV4Lm1qczoxMTQzOjEiXX0KU1RBUlQgUmVxdWVzdElkOiBhMjBjZDhlOS1lYmFiLTRmNmUtOTMxOS1iZmY4NmNjZDExZmIgVmVyc2lvbjogJExBVEVTVAoyMDIyLTA5LTE4VDE3OjQ0OjI3LjI3OVoJdW5kZWZpbmVkCUVSUk9SCVVuY2F1Z2h0IEV4Y2VwdGlvbiAJeyJlcnJvclR5cGUiOiJSdW50aW1lLlVzZXJDb2RlU3ludGF4RXJyb3IiLCJlcnJvck1lc3NhZ2UiOiJTeW50YXhFcnJvcjogSW52YWxpZCBvciB1bmV4cGVjdGVkIHRva2VuIiwic3RhY2siOlsiUnVudGltZS5Vc2VyQ29kZVN5bnRheEVycm9yOiBTeW50YXhFcnJvcjogSW52YWxpZCBvciB1bmV4cGVjdGVkIHRva2VuIiwiICAgIGF0IF9sb2FkVXNlckFwcCAoZmlsZTovLy92YXIvcnVudGltZS9pbmRleC5tanM6OTQ4OjE3KSIsIiAgICBhdCBhc3luYyBPYmplY3QuVXNlckZ1bmN0aW9uLmpzLm1vZHVsZS5leHBvcnRzLmxvYWQgKGZpbGU6Ly8vdmFyL3J1bnRpbWUvaW5kZXgubWpzOjk3NjoyMSkiLCIgICAgYXQgYXN5bmMgc3RhcnQgKGZpbGU6Ly8vdmFyL3J1bnRpbWUvaW5kZXgubWpzOjExMzc6MjMpIiwiICAgIGF0IGFzeW5jIGZpbGU6Ly8vdmFyL3J1bnRpbWUvaW5kZXgubWpzOjExNDM6MSJdfQpFTkQgUmVxdWVzdElkOiBhMjBjZDhlOS1lYmFiLTRmNmUtOTMxOS1iZmY4NmNjZDExZmIKUkVQT1JUIFJlcXVlc3RJZDogYTIwY2Q4ZTktZWJhYi00ZjZlLTkzMTktYmZmODZjY2QxMWZiCUR1cmF0aW9uOiAxNTIzLjA2IG1zCUJpbGxlZCBEdXJhdGlvbjogMTUyNCBtcwlNZW1vcnkgU2l6ZTogMTI4IE1CCU1heCBNZW1vcnkgVXNlZDogMTIgTUIJClVua25vd24gYXBwbGljYXRpb24gZXJyb3Igb2NjdXJyZWQKUnVudGltZS5Vc2VyQ29kZVN5bnRheEVycm9yCg==",
    "ExecutedVersion": "$LATEST"
}

Ran that through base64 --decode like this:

/tmp % cat out.json | jq .LogResult -r | base64 --decode
Unknown application error occurred
Runtime.UserCodeSyntaxError
2022-09-18T17:44:54.675Z	undefined	ERROR	Uncaught Exception 	{"errorType":"Runtime.UserCodeSyntaxError","errorMessage":"SyntaxError: Invalid or unexpected token","stack":["Runtime.UserCodeSyntaxError: SyntaxError: Invalid or unexpected token","    at _loadUserApp (file:///var/runtime/index.mjs:948:17)","    at async Object.UserFunction.js.module.exports.load (file:///var/runtime/index.mjs:976:21)","    at async start (file:///var/runtime/index.mjs:1137:23)","    at async file:///var/runtime/index.mjs:1143:1"]}
START RequestId: f71d6749-43af-4916-abe8-649249be31dd Version: $LATEST
2022-09-18T17:45:07.299Z	undefined	ERROR	Uncaught Exception 	{"errorType":"Runtime.UserCodeSyntaxError","errorMessage":"SyntaxError: Invalid or unexpected token","stack":["Runtime.UserCodeSyntaxError: SyntaxError: Invalid or unexpected token","    at _loadUserApp (file:///var/runtime/index.mjs:948:17)","    at async Object.UserFunction.js.module.exports.load (file:///var/runtime/index.mjs:976:21)","    at async start (file:///var/runtime/index.mjs:1137:23)","    at async file:///var/runtime/index.mjs:1143:1"]}
END RequestId: f71d6749-43af-4916-abe8-649249be31dd
REPORT RequestId: f71d6749-43af-4916-abe8-649249be31dd	Duration: 1486.95 ms	Billed Duration: 1487 ms	Memory Size: 128 MB	Max Memory Used: 12 MB	
Unknown application error occurred
Runtime.UserCodeSyntaxError

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

That's because I broke my index.js file:

/tmp % cat index.js 
exports.handler = async function(event, context) {
  console.log("ENVIRONMENT VARIABLES
" + JSON.stringify(process.env, null, 2))
  console.log("EVENT
" + JSON.stringify(event, null, 2))
  return context.logStreamName
}

Trying again with a fixed one.

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

I recreated the functions.zip file - I think this will update the function:

aws lambda update-function-code \
  --function-name my-test-lambda-nodejs-function \
  --zip-file fileb://function.zip

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

That seems to work!

aws lambda invoke --function-name my-test-lambda-nodejs-function out --log-type Tail
{
    "StatusCode": 200,
    "LogResult": "U1RBUlQgUmVxdWVzdElkOiBmYmMxMGJmMC0yZWZkLTRkYTctOTA0Yy1hNGE4MTAyYzA1MzMgVmVyc2lvbjogJExBVEVTVAoyMDIyLTA5LTE4VDE3OjQ4OjQ2LjA5N1oJZmJjMTBiZjAtMmVmZC00ZGE3LTkwNGMtYTRhODEwMmMwNTMzCUlORk8JRU5WSVJPTk1FTlQgVkFSSUFCTEVTCnsKICAiQVdTX0xBTUJEQV9GVU5DVElPTl9WRVJTSU9OIjogIiRMQVRFU1QiLAogICJBV1NfU0VTU0lPTl9UT0tFTiI6ICJJUW9KYjNKcFoybHVYMlZqRUlyLy8vLy8vLy8vL3dFYUNYVnpMV1ZoYzNRdE1TSkhNRVVDSUNkQmV5OTFYcnNvTjdWNmtoRkgwTkpsVXhuVy9pN0N2cDlGNitCS0NEUjlBaUVBc1k3VUMzZnF1ZXpwam93QmswRE5TTUlEY2pTeXY3Y1RUMkJYVFhCbjZqZ3FrUU1JTXhBREdndzBOakl3T1RJM09EQTBOallpREFzYXFlcE41ZG5Rb0N5aklTcnVBbEVtQVJIRTVzVkpKWTdkcGNmL2VhcXd3TGZDMUc4cmJ2OHdkY250dS9ZUVVnWjAreGtLbGZRSG9wU1JaV3NhQ3hyaFBqRitCVDRvUGg5Y0xYU2R2ZFhUS2VVOGM1c0k2NmNWVUtzT3drS2NXK2poNmp6THpvQXpJdkFyYlB4b3duL3BmSDBBQ2dxNGFsU0RVQWdyVWJiV3dSNk1Vb1FsOUhMUmd2VnFEWjlSbnR6OXpOcHcwU0pnS0lSWTRYZloxeFNQTGlnVGRmblRLcTVja1RBaFFDWEgrbFN4WW12aWlsRGJNUXdlYzJSczYvdzNJZ0I5MkFJcDJYdkVNL00wRnZuK01QbjRCMTQ5Q2piaFRCTmVGdlFlWkY3Wmd4b1Q1eWFveFRLOGFBL0YyTG9mQkt4RzF0akxyeDc5OXFwQ0JWZ2d4bkFPcVErT1dJTUFkVjZjYTFkNUFtUm5ybHdISUMzV3FkL1FwTGNBTTdTL3pvSU41Rk5VQm1WOXlMcG5FR2pVVVJLYU1Fa05xZld1MFVvZVZoaHVPY1hwdVZSR01zZENTMXR1amc2QmJYcXJWS0U5K05uZHd3UVVRa0h3R0tKcGlOa2VXWWIyYjJENlp4WmZ5VGpKMHRqc21jZGZmcTlNL05DRlJqRDZzNTJaQmpxZEFYR1VVTnBabWFQSWMvNk4yZ21BNWZlTHpyZml2UEhaVEM0K3VqMDI2VFNLRUJsa2RPTTk3N2RPbzBMTGJPL3grdWUzRXZOWTZVa1N1ek9kQktRNWVuNTJwVXBQUmd6RTVvWUI1WFlHMVJZc0xaVkx6di9lNzNmaHROemtvVFpkdGdiK2FSWUdXbzczZHoxUkM3NnJ1NTlvdlAwSE9YNkU1bXRtTm1lWnhRU1BvRDVQamRxUzhYQWQ5K1RwMDBBRGdncjV3M1M5MkFWVFFqL2RncHc9IiwKICAiQVdTX0xBTUJEQV9MT0dfR1JPVVBfTkFNRSI6ICIvYXdzL2xhbWJkYS9teS10ZXN0LWxhbWJkYS1ub2RlanMtZnVuY3Rpb24iLAogICJMRF9MSUJSQVJZX1BBVEgiOiAiL3Zhci9sYW5nL2xpYjovbGliNjQ6L3Vzci9saWI2NDovdmFyL3J1bnRpbWU6L3Zhci9ydW50aW1lL2xpYjovdmFyL3Rhc2s6L3Zhci90YXNrL2xpYjovb3B0L2xpYiIsCiAgIkxBTUJEQV9UQVNLX1JPT1QiOiAiL3Zhci90YXNrIiwKICAiQVdTX0xBTUJEQV9SVU5USU1FX0FQSSI6ICIxMjcuMC4wLjE6OTAwMSIsCiAgIkFXU19MQU1CREFfTE9HX1NUUkVBTV9OQU1FIjogIjIwMjIvMDkvMTgvWyRMQVRFU1RdNzNkZWI3NGRkOGI1NDczNzhkNzQxOGQwMjVkZGE2NzAiLAogICJBV1NfRVhFQ1VUSU9OX0VOViI6ICJBV1NfTGFtYmRhX25vZGVqczE2LngiLAogICJBV1NfTEFNQkRBX0ZVTkNUSU9OX05BTUUiOiAibXktdGVzdC1sYW1iZGEtbm9kZWpzLWZ1bmN0aW9uIiwKICAiQVdTX1hSQVlfREFFTU9OX0FERFJFU1MiOiAiMTY5LjI1NC43OS4xMjk6MjAwMCIsCiAgIlBBVEgiOiAiL3Zhci9sYW5nL2JpbjovdXNyL2xvY2FsL2JpbjovdXNyL2Jpbi86L2Jpbjovb3B0L2JpbiIsCiAgIkFXU19ERUZBVUxUX1JFR0lPTiI6ICJ1cy1lYXN0LTEiLAogICJQV0QiOiAiL3Zhci90YXNrIiwKICAiQVdTX1NFQ1JFVF9BQ0NFU1NfS0VZIjogIlBXOXRrQWlibFNjQXJpUG14aHRNVWlrWVFvUmtLRDdGdmtLSG5pdGoiLAogICJMQU1CREFfUlVOVElNRV9ESVIiOiAiL3Zhci9ydW50aW1lIiwKICAiTEFORyI6ICJlbl9VUy5VVEYtOCIsCiAgIkFXU19MQU1CREFfSU5JVElBTElaQVRJT05fVFlQRSI6ICJvbi1kZW1hbmQiLAogICJOT0RFX1BBVEgiOiAiL29wdC9ub2RlanMvbm9kZTE2L25vZGVfbW9kdWxlczovb3B0L25vZGVqcy9ub2RlX21vZHVsZXM6L3Zhci9ydW50aW1lL25vZGVfbW9kdWxlczovdmFyL3J1bnRpbWU6L3Zhci90YXNrIiwKICAiVFoiOiAiOlVUQyIsCiAgIkFXU19SRUdJT04iOiAidXMtZWFzdC0xIiwKICAiQVdTX0FDQ0VTU19LRVlfSUQiOiAiQVNJQVdYRlhBSU9aTVlUVDZBTUciLAogICJTSExWTCI6ICIwIiwKICAiX0FXU19YUkFZX0RBRU1PTl9BRERSRVNTIjogIjE2OS4yNTQuNzkuMTI5IiwKICAiX0FXU19YUkFZX0RBRU1PTl9QT1JUIjogIjIwMDAiLAogICJBV1NfWFJBWV9DT05URVhUX01JU1NJTkciOiAiTE9HX0VSUk9SIiwKICAiX0hBTkRMRVIiOiAiaW5kZXguaGFuZGxlciIsCiAgIkFXU19MQU1CREFfRlVOQ1RJT05fTUVNT1JZX1NJWkUiOiAiMTI4IiwKICAiTk9ERV9FWFRSQV9DQV9DRVJUUyI6ICIvZXRjL3BraS90bHMvY2VydHMvY2EtYnVuZGxlLmNydCIsCiAgIl9YX0FNWk5fVFJBQ0VfSUQiOiAiUm9vdD0xLTYzMjc1OWZlLTA3MTdkNTc0MTVhM2NlNjMzNjljN2I0MztQYXJlbnQ9MTM4NGI3NzMxNmU3MTcyZDtTYW1wbGVkPTAiCn0KMjAyMi0wOS0xOFQxNzo0ODo0Ni4wOTdaCWZiYzEwYmYwLTJlZmQtNGRhNy05MDRjLWE0YTgxMDJjMDUzMwlJTkZPCUVWRU5UCnt9CkVORCBSZXF1ZXN0SWQ6IGZiYzEwYmYwLTJlZmQtNGRhNy05MDRjLWE0YTgxMDJjMDUzMwpSRVBPUlQgUmVxdWVzdElkOiBmYmMxMGJmMC0yZWZkLTRkYTctOTA0Yy1hNGE4MTAyYzA1MzMJRHVyYXRpb246IDEuNDggbXMJQmlsbGVkIER1cmF0aW9uOiAyIG1zCU1lbW9yeSBTaXplOiAxMjggTUIJTWF4IE1lbW9yeSBVc2VkOiA1NyBNQgkK",
    "ExecutedVersion": "$LATEST"
}

Or:

aws lambda invoke \
  --function-name my-test-lambda-nodejs-function
  out --log-type Tail | jq .LogResult -r | base64 --decode
START RequestId: 6c293d94-6af9-4217-9ed4-ad0abb28870f Version: $LATEST
2022-09-18T17:49:20.143Z	6c293d94-6af9-4217-9ed4-ad0abb28870f	INFO	ENVIRONMENT VARIABLES
{
  "AWS_LAMBDA_FUNCTION_VERSION": "$LATEST",
  "AWS_SESSION_TOKEN": "IQoJb3JpZ2luX2VjEIr//////////wEaCXVzLWVhc3QtMSJHMEUCICdBey91XrsoN7V6khFH0NJlUxnW/i7Cvp9F6+BKCDR9AiEAsY7UC3fquezpjowBk0DNSMIDcjSyv7cTT2BXTXBn6jgqkQMIMxADGgw0NjIwOTI3ODA0NjYiDAsaqepN5dnQoCyjISruAlEmARHE5sVJJY7dpcf/eaqwwLfC1G8rbv8wdcntu/YQUgZ0+xkKlfQHopSRZWsaCxrhPjF+BT4oPh9cLXSdvdXTKeU8c5sI66cVUKsOwkKcW+jh6jzLzoAzIvArbPxown/pfH0ACgq4alSDUAgrUbbWwR6MUoQl9HLRgvVqDZ9Rntz9zNpw0SJgKIRY4XfZ1xSPLigTdfnTKq5ckTAhQCXH+lSxYmviilDbMQwec2Rs6/w3IgB92AIp2XvEM/M0Fvn+MPn4B149CjbhTBNeFvQeZF7ZgxoT5yaoxTK8aA/F2LofBKxG1tjLrx799qpCBVggxnAOqQ+OWIMAdV6ca1d5AmRnrlwHIC3Wqd/QpLcAM7S/zoIN5FNUBmV9yLpnEGjUURKaMEkNqfWu0UoeVhhuOcXpuVRGMsdCS1tujg6BbXqrVKE9+NndwwQUQkHwGKJpiNkeWYb2b2D6ZxZfyTjJ0tjsmcdffq9M/NCFRjD6s52ZBjqdAXGUUNpZmaPIc/6N2gmA5feLzrfivPHZTC4+uj026TSKEBlkdOM977dOo0LLbO/x+ue3EvNY6UkSuzOdBKQ5en52pUpPRgzE5oYB5XYG1RYsLZVLzv/e73fhtNzkoTZdtgb+aRYGWo73dz1RC76ru59ovP0HOX6E5mtmNmeZxQSPoD5PjdqS8XAd9+Tp00ADggr5w3S92AVTQj/dgpw=",
  "AWS_LAMBDA_LOG_GROUP_NAME": "/aws/lambda/my-test-lambda-nodejs-function",
  "LD_LIBRARY_PATH": "/var/lang/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib:/opt/lib",
  "LAMBDA_TASK_ROOT": "/var/task",
  "AWS_LAMBDA_RUNTIME_API": "127.0.0.1:9001",
  "AWS_LAMBDA_LOG_STREAM_NAME": "2022/09/18/[$LATEST]73deb74dd8b547378d7418d025dda670",
  "AWS_EXECUTION_ENV": "AWS_Lambda_nodejs16.x",
  "AWS_LAMBDA_FUNCTION_NAME": "my-test-lambda-nodejs-function",
  "AWS_XRAY_DAEMON_ADDRESS": "169.254.79.129:2000",
  "PATH": "/var/lang/bin:/usr/local/bin:/usr/bin/:/bin:/opt/bin",
  "AWS_DEFAULT_REGION": "us-east-1",
  "PWD": "/var/task",
  "AWS_SECRET_ACCESS_KEY": "PW9tkAiblScAriPmxhtMUikYQoRkKD7FvkKHnitj",
  "LAMBDA_RUNTIME_DIR": "/var/runtime",
  "LANG": "en_US.UTF-8",
  "AWS_LAMBDA_INITIALIZATION_TYPE": "on-demand",
  "NODE_PATH": "/opt/nodejs/node16/node_modules:/opt/nodejs/node_modules:/var/runtime/node_modules:/var/runtime:/var/task",
  "TZ": ":UTC",
  "AWS_REGION": "us-east-1",
  "AWS_ACCESS_KEY_ID": "ASIAWXFXAIOZMYTT6AMG",
  "SHLVL": "0",
  "_AWS_XRAY_DAEMON_ADDRESS": "169.254.79.129",
  "_AWS_XRAY_DAEMON_PORT": "2000",
  "AWS_XRAY_CONTEXT_MISSING": "LOG_ERROR",
  "_HANDLER": "index.handler",
  "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "128",
  "NODE_EXTRA_CA_CERTS": "/etc/pki/tls/certs/ca-bundle.crt",
  "_X_AMZN_TRACE_ID": "Root=1-63275a20-5e0ae45d25fd0b166aa8ced5;Parent=3a5d154b313dd6df;Sampled=0"
}
2022-09-18T17:49:20.143Z	6c293d94-6af9-4217-9ed4-ad0abb28870f	INFO	EVENT
{}
END RequestId: 6c293d94-6af9-4217-9ed4-ad0abb28870f
REPORT RequestId: 6c293d94-6af9-4217-9ed4-ad0abb28870f	Duration: 10.98 ms	Billed Duration: 11 ms	Memory Size: 128 MB	Max Memory Used: 57 MB	

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

I had to upgrade awscli first. I did that using:

/tmp % head -n 1 $(which aws) 
#!/usr/local/opt/[email protected]/bin/python3.9

Then the upgrade with:

/usr/local/opt/[email protected]/bin/pip3 install -U awscli

Then I ran this:

aws lambda create-function-url-config \
  --function-name my-test-lambda-nodejs-function \
  --auth-type NONE
{
    "FunctionUrl": "https://wtqdq25qylflndenvxzlssyhmi0zksbf.lambda-url.us-east-1.on.aws/",
    "FunctionArn": "arn:aws:lambda:us-east-1:462092780466:function:my-test-lambda-nodejs-function",
    "AuthType": "NONE",
    "CreationTime": "2022-09-18T17:55:55.359524Z"
}

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

https://wtqdq25qylflndenvxzlssyhmi0zksbf.lambda-url.us-east-1.on.aws/ returns:

{
  "Message": "Forbidden"
}

Probably because I need to deploy a function that actually handles HTTP requests. That's the next step - I'll likely switch to Python at this point too.

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

Generated this with GPT-3:

/* A node.js AWS lambda function that returns a 200 text/html response saying "HEllo World" */

exports.handler = async function(event, context) {
    return {
        statusCode: 200,
        headers: {
            "Content-Type": "text/html"
        },
        body: "<h1>Hello World</h1>"
    };
};

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

rm -f function.zip
zip function.zip index.js
aws lambda update-function-code \
  --function-name my-test-lambda-nodejs-function \
  --zip-file fileb://function.zip

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

Still getting that Forbidden error from https://wtqdq25qylflndenvxzlssyhmi0zksbf.lambda-url.us-east-1.on.aws/ - spotted this HTTP header:

x-amzn-ErrorType: AccessDeniedException

I thought this would fix that:

aws lambda create-function-url-config \
  --function-name my-test-lambda-nodejs-function \
  --auth-type NONE

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

On https://us-east-1.console.aws.amazon.com/lambda/home?region=us-east-1#/functions/my-test-lambda-nodejs-function?tab=configure found this:

image

To allow unauthenticated requests, choose the Permissions tab and and create a resource-based policy that grants lambda:invokeFunctionUrl permissions to all principals (*).

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

Another tutorial: https://docs.aws.amazon.com/lambda/latest/dg/urls-tutorial.html

That tutorial includes this step (I added my-test-lambda-nodejs-function here):

aws lambda add-permission \
  --function-name my-test-lambda-nodejs-function \
  --action lambda:InvokeFunctionUrl \
  --principal "*" \
  --function-url-auth-type "NONE" \
  --statement-id url

Response:

{
    "Statement": "{\"Sid\":\"url\",\"Effect\":\"Allow\",\"Principal\":\"*\",\"Action\":\"lambda:InvokeFunctionUrl\",\"Resource\":\"arn:aws:lambda:us-east-1:462092780466:function:my-test-lambda-nodejs-function\",\"Condition\":{\"StringEquals\":{\"lambda:FunctionUrlAuthType\":\"NONE\"}}}"
}

Decoded:

/tmp % echo '{
    "Statement": "{\"Sid\":\"url\",\"Effect\":\"Allow\",\"Principal\":\"*\",\"Action\":\"lambda:InvokeFunctionUrl\",\"Resource\":\"arn:aws:lambda:us-east-1:462092780466:function:my-test-lambda-nodejs-function\",\"Condition\":{\"StringEquals\":{\"lambda:FunctionUrlAuthType\":\"NONE\"}}}"
}' | jq .Statement -r | jq
{
  "Sid": "url",
  "Effect": "Allow",
  "Principal": "*",
  "Action": "lambda:InvokeFunctionUrl",
  "Resource": "arn:aws:lambda:us-east-1:462092780466:function:my-test-lambda-nodejs-function",
  "Condition": {
    "StringEquals": {
      "lambda:FunctionUrlAuthType": "NONE"
    }
  }
}

And now this works! https://wtqdq25qylflndenvxzlssyhmi0zksbf.lambda-url.us-east-1.on.aws/

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

OK let's try a Python one instead. I'll call that my-test-lambda-nodejs-function.

Trying this as lambda_function.py:

def lambda_handler(event, context): 
    return {
        "statusCode": 200,
        "headers": {
            "Content-Type": "text/html"
        },
        "body": "<h1>Hello World</h1>"
    }
rm -f function.zip 
zip function.zip lambda_function.py 

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

aws lambda create-function \
  --function-name my-test-lambda-python-function \
  --zip-file fileb://function.zip \
  --runtime python3.9 \
  --handler "lambda_function.lambda_handler" \
  --role "arn:aws:iam::${AWS_ACCOUNT_ID}:role/lambda-ex"

Reply:

{
    "FunctionName": "my-test-lambda-python-function",
    "FunctionArn": "arn:aws:lambda:us-east-1:462092780466:function:my-test-lambda-python-function",
    "Runtime": "python3.9",
    "Role": "arn:aws:iam::462092780466:role/lambda-ex",
    "Handler": "lambda_function.lambda_handler",
    "CodeSize": 323,
    "Description": "",
    "Timeout": 3,
    "MemorySize": 128,
    "LastModified": "2022-09-18T19:11:15.900+0000",
    "CodeSha256": "TfPlhn/OBnr7Pn3zb3rm8FTMKWUD92XG1YsRGfvkjvU=",
    "Version": "$LATEST",
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "afb3a3e0-5187-4a2b-ab52-a2e0232f27c5",
    "State": "Pending",
    "StateReason": "The function is being created.",
    "StateReasonCode": "Creating",
    "PackageType": "Zip",
    "Architectures": [
        "x86_64"
    ],
    "EphemeralStorage": {
        "Size": 512
    }
}

I tried removing --handler index.handler because I'm using the lambda_function.lambda_handler default value instead, but I got this error:

An error occurred (InvalidParameterValueException) when calling the CreateFunction operation: Runtime and Handler are mandatory parameters for functions created with deployment packages.

I got python3.9 from aws lambda create-function help.

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

Next:

aws lambda add-permission \
  --function-name my-test-lambda-python-function \
  --action lambda:InvokeFunctionUrl \
  --principal "*" \
  --function-url-auth-type "NONE" \
  --statement-id url
aws lambda create-function-url-config \
  --function-name my-test-lambda-python-function \
  --auth-type NONE

https://fnkvspusjrl5dxytaxnuwidxem0hverw.lambda-url.us-east-1.on.aws/ seems to work now - but it's giving me the same response as the JavaScript one did.

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

Update that to:

def lambda_handler(event, context): 
    return {
        "statusCode": 200,
        "headers": {
            "Content-Type": "text/html"
        },
        "body": "<h1>Hello World from Python</h1>"
    }

Then re-deploy with:

rm -f function.zip 
zip function.zip lambda_function.py 
aws lambda update-function-code \
  --function-name my-test-lambda-python-function \
  --zip-file fileb://function.zip

That worked!

https://fnkvspusjrl5dxytaxnuwidxem0hverw.lambda-url.us-east-1.on.aws/ now returns "Hello World from Python".

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

OK, I'm going to try deploying Datasette(!).

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

OMG that worked! https://fnkvspusjrl5dxytaxnuwidxem0hverw.lambda-url.us-east-1.on.aws/

build-function-zip.sh:

#!/bin/bash
python3 -m pip install -t lib -r requirements.txt

rm -f lambda_function.zip

(cd lib; zip ../lambda_function.zip -r .)

# Now add my code
zip lambda_function.zip lambda_function.py

Then deploy like this:

aws lambda update-function-code \
 --function-name my-test-lambda-python-function \
 --zip-file fileb://lambda_function.zip

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

First hit gets this error:

image

Second hit got it too. Third hit worked fine.

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

I bundled fixtures.db by doing this:

wget latest.datasette.io/fixtures.db
zip lambda_function.zip fixtures.db

Then modified my lambda_function.py to this:

import mangum
from datasette.app import Datasette

ds = Datasette(["fixtures.db"])

lambda_handler = mangum.Mangum(ds.app())

Then this to over-write the zipped version of that file with the new one:

zip lambda_function.zip lambda_function.py

Then deploy:

aws lambda update-function-code \
 --function-name my-test-lambda-python-function \
 --zip-file fileb://lambda_function.zip

And now: https://fnkvspusjrl5dxytaxnuwidxem0hverw.lambda-url.us-east-1.on.aws/fixtures/sortable

image

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

Note that I'm not calling await ds.invoke_startup() yet.

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

Mangum class constructor is useful: https://github.com/jordaneremieff/mangum/blob/c58e85745d6bd7c9cf8734398750e179751b716d/mangum/adapter.py#L30-L47

It tries these handlers in turn:

HANDLERS: List[Type[LambdaHandler]] = [
    ALB,
    HTTPGateway,
    APIGateway,
    LambdaAtEdge,
]

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

It looks like it sets the original AWS event and context inside the ASGI scope: https://github.com/jordaneremieff/mangum/blob/c58e85745d6bd7c9cf8734398750e179751b716d/mangum/handlers/api_gateway.py#L195-L196

            "aws.event": self.event,
            "aws.context": self.context,

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

I'm going to build datasette-debug-mangum inspired by https://datasette.io/plugins/datasette-debug-asgi

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

Another clue in https://github.com/aws-samples/aws-lambda-layer-create-script

#!/bin/bash

if [ "$1" != "" ] || [$# -gt 1]; then
	echo "Creating layer compatible with python version $1"
	docker run -v "$PWD":/var/task "lambci/lambda:build-python$1" /bin/sh -c "pip install -r requirements.txt -t python/lib/python$1/site-packages/; exit"
	zip -r layer.zip python > /dev/null
	rm -r python
	echo "Done creating layer!"
	ls -lah layer.zip

else
	echo "Enter python version as argument - ./createlayer.sh 3.6"
fi

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

I like the look of https://github.com/hmrc/grant-ssh-access/blob/b7515aa1933723855c269c381f97794fbcdc7738/Jenkinsfile#L20

        sh('docker run -t -v $(pwd):/data public.ecr.aws/sam/build-python3.9:latest  /data/package.sh')

Where package.sh contains:

!/bin/bash

set -xeou

BASEDIR=/data
PIPPACKAGESDIR=${BASEDIR}/lambda-packages

cd ${BASEDIR}

zip grant_ssh_access.zip grant_ssh_access.py mdtp.pem

mkdir -p ${PIPPACKAGESDIR}
pip install -t ${PIPPACKAGESDIR} -r requirements.txt
cd ${PIPPACKAGESDIR}
zip -r ../grant_ssh_access.zip .
rm -rf ${PIPPACKAGESDIR}/*

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

So could I do this?

docker run -t -v $(pwd):/mnt public.ecr.aws/sam/build-python3.9:latest \
  /bin/sh -c "pip install -r /mnt/requirements.txt -t /mnt/lib"

Trying that now - I deleted my lib folder first.

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

That might have worked! lib/ is populated with the expected files.

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

Ran this:

rm -f lambda_function.zip
(cd lib; zip ../lambda_function.zip -r .)
zip lambda_function.zip lambda_function.py
zip lambda_function.zip fixtures.db

And ./update.sh to update the deployed function.

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

https://fnkvspusjrl5dxytaxnuwidxem0hverw.lambda-url.us-east-1.on.aws/-/versions now shows:

{
    "python": {
        "version": "3.9.13",
        "full": "3.9.13 (main, Jun 10 2022, 16:49:31) \n[GCC 7.3.1 20180712 (Red Hat 7.3.1-15)]"
    },
    "datasette": {
        "version": "0.62"
    },
    "asgi": "3.0",
    "uvicorn": "0.18.3",
    "sqlite": {
        "version": "3.39.2",
        "fts_versions": [
            "FTS5",
            "FTS4",
            "FTS3"
        ],
        "extensions": {
            "json1": null
        },
        "compile_options": [
            "ATOMIC_INTRINSICS=1",
            "COMPILER=gcc-6.3.0 20170516",
            "DEFAULT_AUTOVACUUM",
            "DEFAULT_CACHE_SIZE=-2000",
            "DEFAULT_FILE_FORMAT=4",
            "DEFAULT_JOURNAL_SIZE_LIMIT=-1",
            "DEFAULT_MMAP_SIZE=0",
            "DEFAULT_PAGE_SIZE=4096",
            "DEFAULT_PCACHE_INITSZ=20",
            "DEFAULT_RECURSIVE_TRIGGERS",
            "DEFAULT_SECTOR_SIZE=4096",
            "DEFAULT_SYNCHRONOUS=2",
            "DEFAULT_WAL_AUTOCHECKPOINT=1000",
            "DEFAULT_WAL_SYNCHRONOUS=2",
            "DEFAULT_WORKER_THREADS=0",
            "ENABLE_FTS3",
            "ENABLE_FTS3_PARENTHESIS",
            "ENABLE_FTS4",
            "ENABLE_FTS5",
            "ENABLE_LOAD_EXTENSION",
            "ENABLE_MATH_FUNCTIONS",
            "ENABLE_RTREE",
            "ENABLE_STAT4",
            "ENABLE_UPDATE_DELETE_LIMIT",
            "MALLOC_SOFT_LIMIT=1024",
            "MAX_ATTACHED=10",
            "MAX_COLUMN=2000",
            "MAX_COMPOUND_SELECT=500",
            "MAX_DEFAULT_PAGE_SIZE=8192",
            "MAX_EXPR_DEPTH=1000",
            "MAX_FUNCTION_ARG=127",
            "MAX_LENGTH=1000000000",
            "MAX_LIKE_PATTERN_LENGTH=50000",
            "MAX_MMAP_SIZE=1099511627776",
            "MAX_PAGE_COUNT=1073741823",
            "MAX_PAGE_SIZE=65536",
            "MAX_SQL_LENGTH=1000000000",
            "MAX_TRIGGER_DEPTH=1000",
            "MAX_VARIABLE_NUMBER=250000",
            "MAX_VDBE_OP=250000000",
            "MAX_WORKER_THREADS=0",
            "MUTEX_PTHREADS",
            "SOUNDEX",
            "SYSTEM_MALLOC",
            "TEMP_STORE=3",
            "THREADSAFE=1",
            "USE_URI"
        ]
    },
    "pysqlite3": "0.4.7.post7"
}

And I'm not getting that parameter error any more, which I think was caused by the old SQLite version.

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

The default timeout of a Lambda function is three seconds.

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

Trying this (guessed by Copilot):

aws lambda update-function-configuration \
  --function-name my-test-lambda-python-function \
  --timeout 10

After that first hit to https://fnkvspusjrl5dxytaxnuwidxem0hverw.lambda-url.us-east-1.on.aws/fixtures/compound_three_primary_keys seemed to take about 7s - subsequent hits were faster.

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

I wonder if giving it more RAM would result in faster responses, so I wouldn't need the 10s timeout?

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

Relevant info from aws lambda update-function-configuration help:

       --timeout (integer)
          The amount of time (in seconds) that Lambda allows a function to run
          before  stopping  it.  The default is 3 seconds. The maximum allowed
          value is 900 seconds. For additional information, see Lambda  execu-
          tion environment .

       --memory-size (integer)
          The  amount of memory available to the function at runtime. Increas-
          ing the function memory  also  increases  its  CPU  allocation.  The
          default value is 128 MB. The value can be any multiple of 1 MB.

I'm going to try --memory-size 256 --timeout 3

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

aws lambda update-function-configuration \
  --function-name my-test-lambda-python-function \
  --timeout 3 \
  --memory-size 256

https://fnkvspusjrl5dxytaxnuwidxem0hverw.lambda-url.us-east-1.on.aws/fixtures/facetable feels a bit happier.

aws lambda update-function-configuration \
  --function-name my-test-lambda-python-function \
  --timeout 3 \
  --memory-size 512

Now it feels positively snappy - might be imagining it a bit though.

I wonder how much this really does affect the cost?

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

The output of aws lambda update-function-configuration is interesting:

{
    "FunctionName": "my-test-lambda-python-function",
    "FunctionArn": "arn:aws:lambda:us-east-1:462092780466:function:my-test-lambda-python-function",
    "Runtime": "python3.9",
    "Role": "arn:aws:iam::462092780466:role/lambda-ex",
    "Handler": "lambda_function.lambda_handler",
    "CodeSize": 9316796,
    "Description": "",
    "Timeout": 3,
    "MemorySize": 512,
    "LastModified": "2022-09-18T20:36:25.000+0000",
    "CodeSha256": "84Vc6tqX6VTWBdSh5zWiV7RgFTD98/vWf7ByykRKQG0=",
    "Version": "$LATEST",
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "d8a6d6c0-3cda-4c4e-ab56-e8e7a05027c0",
    "State": "Active",
    "LastUpdateStatus": "InProgress",
    "LastUpdateStatusReason": "The function is being created.",
    "LastUpdateStatusReasonCode": "Creating",
    "PackageType": "Zip",
    "Architectures": [
        "x86_64"
    ],
    "EphemeralStorage": {
        "Size": 512
    }
}

The EphemeralStorage looks noteworthy. And could I get a speedup by switching architectures to ARM?

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

Also interesting:

       --layers (list)
          A list of function layers to add to the function's  execution  envi-
          ronment. Specify each layer by its ARN, including the version.

          (string)

Might be worth researching the option of publishing a Datasette layer somewhere.

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

       --file-system-configs (list)
          Connection settings for an Amazon EFS file system.

          (structure)
              Details  about  the  connection between a Lambda function and an
              Amazon EFS file system .

              Arn -> (string)
                 The Amazon Resource Name (ARN) of the Amazon EFS access point
                 that provides access to the file system.

              LocalMountPath -> (string)
                 The  path  where  the  function  can  access the file system,
                 starting with /mnt/ .

Datasette serving off EFS remains very interesting indeed. Not sure how I'd ensure multiple lambdas didn't try to write at the same time, but could be good for scaling reads against large DBs.

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

       --ephemeral-storage (structure)
          The size of the functions /tmp directory in MB. The default value is
          512, but can be any whole number between 512 and 10240 MB.

          Size -> (integer)
              The size of the functions /tmp directory.

So you can request up to 10GB of ephemeral storage.

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

You can pass --timeout and --memory-size to the initial aws lambda create-function command too.

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

You can pass environment variables to create-function too - could use those for Datasette plugin secrets:

       --environment (structure)
          Environment  variables that are accessible from function code during
          execution.

          Variables -> (map)
              Environment variable key-value pairs. For more information,  see
              Using Lambda environment variables .

              key -> (string)

              value -> (string)

       Shorthand Syntax:

          Variables={KeyName1=string,KeyName2=string}

       JSON Syntax:

          {
            "Variables": {"string": "string"
              ...}
          }

@simonw
Copy link
Owner Author

simonw commented Sep 18, 2022

I think that's everything I need to know in order to build a datasette-publish-lambda plugin.

@simonw simonw closed this as completed Sep 18, 2022
@simonw simonw changed the title Have another go at deploying Datasette to Lambda with Mangum Figure out how to deploy Datasette to AWS Lambda using function URLs and Mangum Sep 18, 2022
@simonw simonw transferred this issue from another repository Sep 18, 2022
@simonw simonw transferred this issue from another repository Oct 16, 2022
@simonw simonw transferred this issue from simonw/temp Oct 16, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant