28

Motivation for doing this approach in the first place comes from Amazon: https://aws.amazon.com/blogs/compute/resize-images-on-the-fly-with-amazon-s3-aws-lambda-and-amazon-api-gateway/ (before they added the 'update'...)

In our AWS Lambda resize function it resizes the image and stores it the new image on S3.

const s3_bucket = process.env.s3_bucket;
S3.putObject({
  Body: buffer,
  Bucket: s3_bucket,
  ContentType: contentType,
  CacheControl: 'max-age=31536000',
  Key: key,
  StorageClass: 'STANDARD'
}).promise()

Now we want this to work for all our test/staging environments as well as production.. So I found "Environment Variables", I though great! But when I try to deploy a new version all I get is:

Environment variables not supported

Have we set up something incorrectly in CloudFront? We are using Node version 6.10. I find it hard to believe if we have to hardcode the buckets and keep different versions of the code just to handle this? If that is the case then we wasted a lot of time using AWS Lambda...

Edit: What we do is take a request for an image like "media/catalog/product/3/0/30123/768x/lorem.jpg", then we use the original image located at "media/catalog/product/3/0/30123.jpg", resize it to 768px and webp if the browser supports that and then return the new image (if not already cached).

Yves M.
  • 29,855
  • 23
  • 108
  • 144
OZZIE
  • 6,609
  • 7
  • 55
  • 59
  • 4
    why the down vote? the option is available in admin, it doesn't say that it doesn't work until you press deploy, at that point I had implemented it in my code, uploaded it to s3.. spent at least an hour.. – OZZIE Feb 22 '19 at 15:35

6 Answers6

42

Workaround using custom origin headers

Environment variables are not supported by Lambda@Edge as specified in the limitations documentation.

But if you are using Lambda@Edge either on origin request or origin response, you can use a workaround with CloudFront Origin Custom Headers.

Basically instead of environment variable, you can set custom headers within your CloudFront origin. Those "static" headers will then be passed to your origin request/response Lambda@Edge.

enter image description here

Then you can access them in your Lambda@Edge function code via:

const foo = request.origin.custom.customHeaders["x-env-foo"][0].value;

Or when using S3 as the origin:

const foo = request.origin.s3.customHeaders["x-env-foo"][0].value;

See also https://medium.com/@mnylen/lambda-edge-gotchas-and-tips-93083f8b4152

Yves M.
  • 29,855
  • 23
  • 108
  • 144
  • Great ideia! I try this but somehow my customHeaders is empty, even if i set them on cloudfront. I try to add them to a cache-policy too but i can't make it work: the customHeaders on lambda event object is always empty regardless of my settings. – victor israe Dec 22 '20 at 21:21
  • I can't for the life of me find where to add customer headers for a CloudFront distribution. Any insight? The AWS docs aren't helpful either. – Jonny May 12 '22 at 08:58
  • I found it - in the distribution, click on Origins tab, then edit the origin you have. Inside there is a section called "Add custom header - optional". – Jonny May 12 '22 at 09:27
5

Workaround using lambda function name

Also you can check process.env.AWS_LAMBDA_FUNCTION_NAME if you happen to have separate functions for your dev/prod.

const getIsDev = () => {
    // lambda at edges cant set environment variables in the console.
    if (!process.env.AWS_LAMBDA_FUNCTION_NAME) {
        return true // << double check this is the behavior you want to fall back to.
    }
    if (process.env.AWS_LAMBDA_FUNCTION_NAME.indexOf("-dev") => 0) {
        // does the function name contain the word -dev?  
        return true
    }
    return false
}
jfo8000
  • 151
  • 2
  • 2
4

As an alternative to the selected answer...

The same could be achieved using the Serverless Framework, Webpack and the serverless-webpack plugin.

You can access options used for the serverless operation with:

const slsw = require('serverless-webpack');
const stage = slsw.lib.options.stage;

Or, you can access information in the serverless.yml file with:

const serverless = slsw.lib.serverless;

Then simply add this to the plugins of your webpack.config.js file:

plugins: [new EnvironmentPlugin({ VAR1: var1, VAR2: var2, STAGE: stage })]

This method can provide you with an easy way to manage environment variables in Lambda@Edge functions.

Maurice
  • 11,482
  • 2
  • 25
  • 45
2

As mentioned in this documentation for CloudFront Lambda limitations:

Environment variables aren't supported.

What you could do instead is use SSM Parameter Store to manage your function's variables. You can edit Parameter Store variables through the console or programmatically, and you can get the variables using the ssm.getParameter() function

Deiv
  • 3,000
  • 2
  • 18
  • 30
  • 3
    I really hate lambda.. I have never seen a tool so unintuitive, difficult and time consuming to use.. but thanks I will try that! Just spent more than a week implementing this.. I'm so damn tired of it.. – OZZIE Feb 22 '19 at 14:30
  • 3
    why even show Environment Variables in the interface then if it's not supported? Just to waste people's time? – OZZIE Feb 22 '19 at 14:31
  • 2
    Environment Variables aren't supported only in Lambda@Edge. In regular functions, they're normally supported. Do you really need to use Lambda@Edge? A regular Lambda won't do the trick? – Thales Minussi Feb 22 '19 at 14:34
  • I mean, it's a resize function triggered by an S3Event which, by nature, is asynchronous and eventual consistent. Why bother having Lambda@Edge to achieve this? – Thales Minussi Feb 22 '19 at 14:36
  • 1
    I'm not sure I understand this answer fully :( Okay, I can store something there, and I can fetch it.. but how does it help in determining if the current request was from staging or production for example? They could have concurrent requests as well.. – OZZIE Feb 25 '19 at 07:31
  • Technically, you should have 2 separate lambdas, one for staging and one for production (if you only have those two environments). However, if you want to keep only one, if you hit the Lambda through API gateway (or any requesting service), then you can simply see the path that was requested (/staging vs /production) from inside the event the Lambda received and act accordingly. Likewise, you should have parameter store values for both environments. – Deiv Feb 25 '19 at 14:25
1

I had this as a comment but I think it's worth adding it as an answer.

Why do you need to use Lambda@Edge to begin with? I understand your frustration, but Lambda@Edge was designed to achieve a completely different set of things. See some use cases here

In your use case, you upload an object to S3 and the PUT object event will trigger your Lambda function, which is, by nature, asynchronous and eventual consistent. Your users really don't need the optimised thumbnail generation execution time since you'd only be gaining a few hundreds of milliseconds anyways. By the time they need the thumbnail, it will already be there regardless.

In regular Lambda functions, you can absolutely make use of environment variables, making it very easy to apply different settings to different environments (dev, test, prod).

You can see how to set environment variables in regular Lambda Functions here

Thales Minussi
  • 6,965
  • 1
  • 30
  • 48
  • I don't know to be honest. What we do is take a request for an image like "media/catalog/product/3/0/30123/768x/lorem.jpg", then we use the original image located at "media/catalog/product/3/0/30123.jpg", resize it to 768px and webp if the browser supports that and then return the new image (if not already cached) - do we not need @Edge for this? – OZZIE Feb 22 '19 at 14:52
  • I think your application can me more proactive than reactive here. Why not trigger the Resizing upon Upload? Then your application doesn't need to process anything during the request lifespan, making it even faster for the end user. Can you please let me know on what point in time this image is uploaded? Is it the user who decides the size of the thumbnail or is it a predefine application rule? If the user decides, I suggest processing the resizing asynchronously and returning something like: 'Your request is being processed. It will be available soon", then your frontend can poll it – Thales Minussi Feb 22 '19 at 14:58
  • 2
    Generally speaking, it's not a good idea to do image processing upon a user request since it'll usually take quite a while. Please let me know if what I said makes sense to you. I hope it helps! – Thales Minussi Feb 22 '19 at 14:59
  • 1
    If your goal is to have image processing based on a user's request, then you can also just have an API gateway -> Lambda, that way you can use env variables. However, the lack of env variables in Lambda edge isn't really a downside because as mentioned before, you can achieve this with Parameter Store (and can use modules such as node-cache to implement caching of said parameters to save on processing time) – Deiv Feb 22 '19 at 15:02
  • https://aws.amazon.com/blogs/compute/resize-images-on-the-fly-with-amazon-s3-aws-lambda-and-amazon-api-gateway/ "Instead of processing and resizing images into all necessary sizes upon upload, the approach of processing images on the fly has several upsides: Increased agility Reduced storage costs Resilience to failure" – OZZIE Feb 22 '19 at 15:15
  • This is why I asked if it was a rule defined by the user or by the application. If the users choose the sizes, then it's impossible to predict every possible size they'd want. If it's an application rule I insist it should be done right after the upload – Thales Minussi Feb 22 '19 at 15:17
1

I solved it by prepending the s3_bucket to the js file in the bash build script. So I specify build.sh [s3_bucket] [environment-name]

if [ ! $# -eq 2 ]; then
    echo 'You need to provide two parameters: [s3_bucket] [environment]'
    echo 'example: build.sh imagetest-us-east-1 next'
    echo 'example: build.sh [s3_bucket_to_be_defined] production'
    exit 1
fi

filename='index.js'
setCurrentEnvironment() {
    jsEnv="const s3_bucket='$1';"
    mv "$filename" "$filename".orig && cp "$filename".orig "$filename"
    echo -e "$jsEnv\n\n$(cat ${filename})" > "$filename"
}
restoreDefault() {
    rm -rf "$filename"
    mv "$filename".orig "$filename"
}

setCurrentEnvironment $1
zip -FS -q -r "../../dist/resize__$2.zip" *
restoreDefault
OZZIE
  • 6,609
  • 7
  • 55
  • 59