hey guys, I have a potential problem with Lambda l...
# help
s
hey guys, I have a potential problem with Lambda layers & binary executables. I have a version of sharp compiled for Amazon Linux that lives in the layer, and this works fine normally. but when I run in live debug mode, it doesn’t work:
Copy code
Something went wrong installing the "sharp" module

Cannot find module '../build/Release/sharp-darwin-x64.node'
Require stack:
- /Volumes/SuperData/Sites/reelcrafter/v2-microservices/.build/src/services/media-processor/functions/get-metadata.js
- /Volumes/SuperData/Sites/reelcrafter/v2-microservices/node_modules/@serverless-stack/cli/scripts/util/bootstrap.js
my theory is that the Lambda code is running locally on the Darwin architecture, but it’s using the Lambda layer which is Amazon Linux arch. how can I solve this?
t
Yeah I ran into this with Prisma
I'll send you a message in a bit
s
thanks Dax! I guess I could just build on Mac & copy the darwin executable into the
build/Release
folder along w/ the Amazon one, then upload that to the layer
not sure if that’d work
t
Okay so yeah that's exactly what I do. I had the opposite problem, Prisma by default installs what makes sense on my local machine. I use linux so I thought it would be fine but AWS requires something more specific. I had it install the more specific version when packaging out for aws
s
this works for me to use w/ layers:
Copy code
SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install --arch=x64 --platform=linux sharp
wish you could just do
platform=linux,darwin
t
yeah I had to do something similar with prisma, they look for an env variable to override the default
s
welp, that didn’t work 😞
this is hacky, but I wonder if i could install sharp locally on my Mac under a different package name.. like
sharp-mac
. and then in my Lambda func:
Copy code
const { default: sharp } = await import(process.env.IS_LOCAL ? 'sharp-mac' : 'sharp')
(I’m really ensuring devs on Windows can’t work on this) 😄
or in the function definition:
...(process.env.IS_LOCAL ? {} : { layers: [sharpLayer] }),
well, that did remove the layer.. but still the same error. I can’t imagine why I’d be getting this error 😕
maybe I’m confused about this live debugging. it’s supposed to be executing the code locally, right? so when I do
yarn start
and a Lambda func imports a dependency.. that’s looking for the dependency where, exactly? is it using my node_modules folder? because I don’t see the bundled code in
.build/cdk.out
t
When executing locally the code isn't bundled, it's just transpiled and executed so it should be using
node_modules
s
I figured. but I still get
Cannot find module '../build/Release/sharp-darwin-x64.node'
even though it’s there, locally
t
Was this ever working?
s
no
t
Can you try disabling the layer when in local mode?
s
I already did that:
Copy code
lambdaFunction: new sst.Function(stack, 'GetMetadataFunc', {
      functionName: `${stack.stackName}-audioProcGetMetadata`,
      handler: `${srcPath}/get-metadata.main`,
      ...(process.env.IS_LOCAL ? {} : { layers: [sharpLayer] }),
      permissions: [[props!.uploadBucket.s3Bucket, 'grantRead']],
    }),
and just for good measure, in the global SST app:
Copy code
bundle: {
      // sharp is a binary, and we have it in a Lambda layer, so exclude it
      externalModules: [...(process.env.IS_LOCAL ? [] : ['sharp']), 'knex'],
      loader: {
        '.node': 'binary',
      },
    },
t
Let me try to recreate your exact issue. I should be able to by creating a lambda function that references this library right
s
sure thing. I really appreciate it, as I’m totally blocked right now and it’s been frustrating 😕
t
😬 it works for me but I'm on linux
let me check something
So what I have in here
Copy code
node_modules/sharp/build/Release
is sharp-linux-x64.node
Do you have
srcPath
set to something?
s
gimme a min. it doesn’t work when the app is deployed now, which is not how it was before.. one sec
oh of course..because the layer isn’t included in this new test func I made. nvm, I’ll come back to that later. gotta get it working locally you talking about
srcPath
in tsconfig.json?
t
I was referring to srcPath in function props
This isn't a monorepo with multiple package.json is it?
s
it is. using yarn workspaces
but sharp is installed at the root
well.. hang on.
ok.. so in the one workspace, sharp is installed as a devDep
it’s not actualy installed globally
t
I think if it is a monorepo then it's expected to set the function's srcPath to the root of the sub-package the function is housed in. Otherwise it's not going to be using any package specific dependencies installed in
child/node_modules
For example I have
src/core
and
src/services
in my mono repo. In
app.setDefaultFunctionProps
I set
srcPath: "src/services"
and then define my
handler
relative to that. That will run all the esbuild stuff inside the srcPath which is what you want
s
ok, lemme try a few things
I’m not sure this is the right setting for my proj. is it gonna look for
package.json
in
src/services
? cuz there’s nothing there but folders
t
Yes but if there's no package.json there then it's not a subpackage in a monorepo is it?
s
it’s at the project root
<project root>/package.json
,
<project root>/node_modules
. and the package.json at the root defines
services/*
as workspaces
so then there’s
services/media-processor/package.json
t
Ah ok then your srcPath should be
services/media-processor
- I only have one subpackage for all my services not one per service
s
srcPath
defined at the stack level, per function, right? just for the media processor Lambdas
t
yeah that's right
s
ok. I’m actually testing this in my
rest-api
service instead, with just a dummy endpoint I can easily call to test.
Copy code
'GET /sentry-test': {
      function: {
        handler: 'functions/sentry-test.main',
        srcPath: 'src/services/rest-api',
      },
      authorizationType: sst.ApiAuthorizationType.NONE,
    },
and sharp is installed as a devDep in
services/rest-api
I guess it doesn’t need to be a devDep.. that was a holdover from Serverless Framework to prevent it from being bundled. but I can use SST’s bundle options to exclude it
t
So is it working?
s
dunno yet..
yarn start
is rather slow, and it bombed a few times with errors
just fixed what was wrong, trying again now
it would definitely be great to somehow define
srcPath
on a stack level
t
I'll be working on allowing that soon. In the mean time you technically can use
app.setDefaultFunctionProps
as long as you do it in each stack
Each call to it will override the previous one
s
ok.. running locally. deploying updates
duuuuuuuuude yes
ok. works locally, so that’s something
however, the docs say this about `srcPath`:
Note that for TypeScript functions, if the
srcPath
is not the project root, SST expects the
tsconfig.json
to be in this directory.
I have my tsconfig at project root only
didn’t seem to encounter any problems
it also says
Only applicable if the
bundle
option is set to
false
.
but I have bundle settings defined
I’m confused about how this works and it wasn’t obvious that setting
srcPath
was crucial for me
so basically, in a monorepo/multi workspace SST app, if your separate workspaces are
src/services/*
, you should always have a
srcPath
for each func set to the proper services subfolder, right?
t
Yeah we definitely need to make this clearer. I'd say yes you should always set srcPath to wherever package.json is
s
ahh man, that trick doesn’t work. redefining the srcPath. I have this in my MediaProcessor stack which is the last one that gets instantiated:
Copy code
app.setDefaultFunctionProps({
      srcPath: 'src/services/media-processor',
    });
but oddly enough, I get an error:
Copy code
Error: Cannot find a handler file for "src/services/graphql/functions/transcode-audio.main".
t
ah ok that one was a gamble
we'll add the stack.setDefaultFunctionProps soon, just added it to roadmap
s
that would be huge
I’ll just set it manually on each func for now, no biggie.
yep, now it wants tsconfig.json in
src/services/auth
.. in each service folder. ugh
I thought this was the point of monorepos, that you could have stuff at the root level 🤔 and not replicate every config file in every workspace
t
You can have your tsconfig extend from the base one. But also this was my reasoning for not having a subpackage for every service. I'm not fully sure of the benefits from having multiple packages besides enforcing some kind of boundary. I'm mostly concerned about seperating
core
vs
service
and not so much
service1
vs
service2
s
yeah, this is not practical 🤔
t
I'm not even fully sold on
core
vs
service
I'm doing it because it feels organized but I can't really articulate the benefits for having more than one package
s
so what do you have set as yarn workspaces at your project root?
t
I have 2 packages in my mono repo.
src/core
that contains all my domains and functions inside them and then
src/services
which contains lambdas that import from
src/core
I want to encourage putting most code in
src/core
and having
services
simply call that functionality in lambdas
s
so
src/core
is your supporting functions for the CoreStack in your infra, right?
like in my Core stack, I define Cognito and have an email trigger.. so that trigger code goes into
src/core
t
Yeah but the lambdas for it would go in
src/services
s
what’s in
src/core
then? sorry, I’m running on fumes the past 24 hrs
t
A more explicit example. In
src/core
I have
billing, bus, user
modules.
user
contains functions like
from_id
from_email
update_profile(...)
which all just talks to cognito.
billing
contains functions like
create
which creates a stripe customer. Then in
src/services
I have lambdas that react to a new Cognito user and publish an event
bus.publish
and another lambda subscribed to that event that calls
billing.create
to create a new stripe customer whenever a new cognito user is created
So as much business logic as possible in
src/core
and mostly glue in
src/services
s
ahhh ok, so your
src/services
is just Lambda handlers that import & run code from core
and so your two yarn workspaces are
src/core
and
src/services
, each with their own package.json/tsconfig/etc
t
yeah exactly
s
I guess I wanted to have separate stacks for the app, broken up into Core (auth, db, buckets), API (REST API, GraphQL), and MediaProcessor (handling uploads, transcoding media, processing thumbnails, etc)
I don’t know if there’s any particular advantage over just one giant stack.. 🤔
except that you could take down one stack without affecting the core services
t
I still have seperate stacks for each service
There's just very little upside to actually creating multiple subpackages in your repo. I can't really articulate anything besides it allows you to have different versions of dependencies but who really wants that
s
right.. yes please, fewer headaches 😄
so you have (for example) stacks A, B, C, D.. but all of them point to Lambda code in
src/services
.. and do you have that broken out by stack/service?
src/services/A
,
src/services/B
? I mean just folders, not as subpackages
t
Yeah roughly
sometimes it makes sense for one stack to deloy multiple services
s
yeah, my MediaProcessor deploys a step function full of Lambdas, and then separate Lambdas outside that
multiple services, but all with the same concern
ok. I definitely need to simplify this a lot. and actually, using your approach,
core
can have its own packages all related to biz logic, project root packages can be 100% about SST/CDK, and then
services
would just be Lambda related stuff like TS types for handlers or whatever
right now my packages are kinda all over the place, because shared business logic has no one place to define its packages.. so they’re at root level
@thdxr thanks a trillion! this has been incredibly helpful. it would be really useful as a guide at some point too 🙂
t
np! I've slowly been figuring out some good patterns over the past few months and will probably write up my opinionated version of project setup once I get my stuff out in production
t
I ran into this today, and this is what worked for me. 1. Added these envs in my
deploy
script:
Copy code
"deploy": "SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm_config_arch=x64 npm_config_platform=linux sst deploy",
2. Set my
bundle
config:
Copy code
defaultFunctionProps: {
      // Include `sharp-linux-x64.node` binary needed in Lambda
      bundle: {
        nodeModules: ['sharp'],
        loader: {
          '.node': 'binary',
        },
      },
Also using yarn workspaces. didn't have to hoist or anything
in the built function's `node_modules`:
s
thanks Ted! my sharp binary is in a layer, so it works totally fine when invoked in the cloud, and when in live debug mode, it uses the local Mac binary