serverless-nextjs / serverless-next.js

⚡ Deploy your Next.js apps on AWS Lambda@Edge via Serverless Components
MIT License
4.44k stars 452 forks source link

Use environment variables #184

Open dhmacs opened 4 years ago

dhmacs commented 4 years ago

How can I pass environment variables? I would like to pass some api key and secrets both to api and frontend lambdas, how can I do that?

I tried like this but it doesn't work:

# serverless.yml
myApp:
  component: serverless-next.js
  inputs:
    env:
      API_KEY: demo
danielcondemarin commented 4 years ago

@macs91 Use next's build time configuration . This is the recommended approach for managing environment config. in your pages.

dhmacs commented 4 years ago

@danielcondemarin I tried it, but I see that it leaks env variables to the client... Given that I intend to store secrets I can't use that 😬

danielcondemarin commented 4 years ago

@danielcondemarin I tried it, but I see that it leaks env variables to the client... Given that I intend to store secrets I can't use that 😬

Ohh I see. Sorry I wasn't entirely clear how build time env. works. Sounds like there is scope then for injecting env. variables via serverless.yaml. Reopening ...

danielcondemarin commented 4 years ago

@macs91 Could you share how you're accessing the env. config. in the page? i.e. on getInitialProps I'm guessing?

dhmacs commented 4 years ago

Yes, the secret will be accessed from getInitialProps.. I dug a bit more, and it seems that only the env variables that are explicitly referenced on client side code are visible in the bundle, so basically env variables are not exposed unless they are explicitly included in client side code?

So If I don't want the env variable to get included in the bundle maybe it's enough to use it under this condition:

if (!process.browser) {
   console.log("my secret", process.env.SECRET);
}

Ok so maybe this way it can work, but we have to be careful to make sure the env variable are not referenced inside the client bundle.

danielcondemarin commented 4 years ago

@macs91 Sounds incredibly easy to leak the secure config. by accident., so I'll be adding support for runtime only env. configuration via a serverless.yml input. Stay tuned 😄

dhmacs commented 4 years ago

Yep I thought that too 😅 When env variables are set only on the lambda it's much harder to leak secrets by mistake. Thanks 👍

danielcondemarin commented 4 years ago

@macs91 Found out today Lambda@Edge doesn't support environment variables :/

Hopefully this will be supported / available soon! But for now you'll have to workaround it by using the build time config. with care. I'll keep the issue opened.

dhmacs commented 4 years ago

Bummer! I didn't know that.. Hopefully they will announce support at re:Invent 😁

danielcondemarin commented 4 years ago

Bummer! I didn't know that.. Hopefully they will announce support at re:Invent 😁

That's what I'm hoping. All I can do for now is ask for it 🙂

gbwashleybrown commented 4 years ago

Oh no... is there still no way to do this? I used NextJS and this component to handle a simple contact form but I'd like to not leak the email address the form sends the submission to.... Anyway to handle this? If not then this has practically killed my app with this dead in the water :(

danielcondemarin commented 4 years ago

@gbwashleybrown Did you have a look at build.env ? https://github.com/danielcondemarin/serverless-next.js#inputs It uses next's build time env variables as documented in https://nextjs.org/docs/api-reference/next.config.js/environment-variables

gbwashleybrown commented 4 years ago

@danielcondemarin According to the conversation further up this post, doing that will leak values into the client? So I would prefer not to do that as I don't need to access them in the client.

My serverless function loops trough an array of email addresses and picks one to send the contact form submission depending on some stuff. So being able to access all the email addresses on the server side only without exposing them to the client is key for me to be able to use this in a production environment.😕

Podders commented 4 years ago

@gbwashleybrown The safest way to do this is to create a handler in a /api route then POST/GET to that handler to loop your array etc, /api routes are not exposed to the client as they are a simple request/response and always run on the server side, you may find using this that you don't need process.env vars at all,

Take a look at https://nextjs.org/docs/api-routes/introduction

And also a rest example here: https://github.com/zeit/next.js/tree/canary/examples/api-routes-rest

gbwashleybrown commented 4 years ago

@Podders Thank you.

I think I may have misunderstood how Next is bundled.

The email array and sendmail method is within /pages/api/sendmail.js and I was under the impression that /api/* is exposed to the client.

For as long as server-side functions (/pages/api/*) files aren't exposed then I should be ok. 😁

bobrown101 commented 4 years ago

I found this issue when I had a lambda function (created with '@serverless/backend') that would be deployed along side my next.js app and I wanted to expose an environment variable to the next.js app during build time that was equal to the url of the newly deployed lambda function.

my solution was as follows

// serverless.yml
api:
  component: '@serverless/backend'
  inputs:
    code:
      src: api/src
    env:
      dbName: ${database.name}
      dbRegion: ${database.region}

frontend:
  component: "serverless-next.js"
  inputs:
    domain: "yourdomain.com"
    build:
      env:
        API_URL: ${api.url}
// next.config.js
module.exports = {
    target: "serverless",
    env: {
        API_URL: process.env.API_URL
    }
}
// pages/index.js
export async function getStaticProps() {
  const API_URL = process.env.API_URL
  return {
    props: {
      API_URL
    }
  }
}
godson-ikhokha commented 4 years ago

[CORRECTION] Adding my env variables in the next.config.js file suffuced:

module.exports = {
  env: {
    //env variables here
  }
}
nicovalencia commented 4 years ago

What we ended up doing was using AWS Secrets Manager, since you can request from Lambda@Edge. Downside is having to make a region-specific request to get the secrets from the Lambda, but works as a stop-gap for now.

andrew310 commented 4 years ago

@gbwashleybrown Did you have a look at build.env ? https://github.com/danielcondemarin/serverless-next.js#inputs It uses next's build time env variables as documented in https://nextjs.org/docs/api-reference/next.config.js/environment-variables

Is this working? I have been trying this method and it doesn't seem like build.env does anything at all...

MelMacaluso commented 4 years ago

Maybe I am misunderstanding how this works, so far in order to set a env variable I used:

appName:
  component: '@sls-next/serverless-component@1.17.0-alpha.6'
  build:
    env:
      NEXTAUTH_URL: https://example.cloudfront.net

Say NEXTAUTH_URL thought is not being set at all, as in for this specific example is not fetched by next-auth, what am I missing?

EDIT:

Of course the following would work:

// next.config.js
module.exports = {
  env: {
      NEXTAUTH_URL: https://example.cloudfront.net
  }
}

But wouldn't that both expose the env variable to the bundle and also to VCS? How can we avoid that?

andrew310 commented 4 years ago

@MelMacaluso I had the same problem. I am also using next-auth. Look at the solution by @bobrown101. That worked for me. build.env must go under inputs. So the documentation is slightly off.

MelMacaluso commented 4 years ago

@andrew310 thanks mate! Will try out and let you know 🙏

MelMacaluso commented 4 years ago

Actually the only thing that works for me is defining the envs in the next.config.js which is not ideal as is commited...if anyone has some other solutions would be greatly appreciated 🙏

Diljeetsinghsuri commented 3 years ago

Actually I am facing issue to maintain the deployment process with the same build which I created for my DEV environment. As you know we need to deploy the same code on different environments(DEV, UAT & PROD), and have different endpoints for each environment. So I want to deploy the build which I created for DEV environment to UAT & PROD, but how can I change the end-point without making the new build of nextjs code?

murtyjones commented 3 years ago

Below is how I was able to include environment variables without needing to commit them to source (assumes you're using GitHub actions to deploy your app).

Tradeoffs of this approach:

1. Store environment variables as secrets in your GitHub project's "Secrets" section

This will be at https://github.com/<username>/<project>/settings/secrets

image

2. Have your GitHub workflow include the variables during the build process

Assuming your GitHub workflow looks something like this one, include your desired environment variables in the step where the app is built and deployed

      - name: Deploy to AWS
        run: npx serverless
        env:
          MY_SECRET_API_KEY: ${{ secrets.MY_SECRET_API_KEY }}

3. In your serverless yml, include the variables under build.env (again assumes a structure somewhat like this one))

myApp:
  component: serverless-next.js
  inputs:
    ...
    build:
      env:
        MY_SECRET_API_KEY: ${env.MY_SECRET_API_KEY}

4. In your next.config.js, add the variables you want included

module.exports = {
  target: 'serverless',
  env: {
    MY_SECRET_API_KEY: process.env.MY_SECRET_API_KEY
  }
};

... and that's pretty much it. This isn't a great solution because of the tradeoffs already mentioned, and if you have the time to sink into figuring out how to get AWS Secret Manager working, that would probably be a better workaround. It's unfortunate that there's no easy way to configure serverless next.js without committing your env vars to source as that's an anti-pattern.

PaulKushch commented 3 years ago

Just a note from me. I was also struggling with the same issue and @murtyjones solution just did not work for me. The problem seems to be step 3 where I needed to change

myApp:
  component: '@sls-next/serverless-component@1.18.0'
  inputs:
    ...
    build:
      env:
        MY_SECRET_API_KEY: ${env.MY_SECRET_API_KEY}

to

myApp:
  component: '@sls-next/serverless-component@1.18.0'
  inputs:
    ...
    env:
      MY_SECRET_API_KEY: ${env.MY_SECRET_API_KEY}

So I needed to remove build nesting for env in serverless.yml file.

Best regards

nocturnalcodehack commented 3 years ago

@murtyjones Your comment "figuring out how to get AWS Secret Manager working, that would probably be a better workaround," -- Makes me believe I am pushing into a rope trying to use AWS Secrets Manager with nextjs to handle both oAuth2 secrets and DB passwords. I think I should stop trying an concede to the anti-pattern...

murtyjones commented 3 years ago

@murtyjones Your comment "figuring out how to get AWS Secret Manager working, that would probably be a better workaround," -- Makes me believe I am pushing into a rope trying to use AWS Secrets Manager with nextjs to handle both oAuth2 secrets and DB passwords. I think I should stop trying an concede to the anti-pattern...

yeah, it's definitely doable but it takes some effort. i've also played around with AWS Systems Manager, which looks like it might be easier to use with AWS Lambda than the Secrets Manager, but I haven't tried that yet so not sure if there are any gotchas.

this is one of those things where someone who publishes a blog post guide will rake in the traffic 😄

gbwashleybrown commented 3 years ago

Environment variables are almost always needed with the backend (secret keys specifically), so everyone is going to be facing this same problem - which I suspect is a heck of a lot people? Why can't we just make this none Lambda@Edge and just use normal Lambda instead? I would to help, if I can, but I'm not particularly great at writing things like this and don't really know the codebase too well.

With that said, I'm having to store secrets in /api/* files directly, rather than an .env file. This is fine for now, but obviously not ideal as it gets saved in VCS and as you know, it's just a really insecure way of storing your code.

kylekirkby commented 3 years ago

The approach I've gone with for a multi-env i18n Next.js site is to set a BUILD_ENV variable which is used to determine which environment variables to load in. So my serverless.yml file looks like:

myNextApp:
    component: '@sls-next/serverless-component@1.19.0-alpha.0'
    build:
        env:
            BUILD_ENV: ${env.BUILD_ENV}
    inputs:
        timeout: 30
        build:
            postBuildCommands: ['node serverless-post-build.js', 'node sitemap.xml.js']
    memory: 1024

Then in the root of the repo I've got a environments folder containing .env.localhost, .env.develop and .env.main:

These files look something like:

NEXT_PUBLIC_BUILD_ENV=develop
NEXT_PUBLIC_AUTH_DOMAIN=auth.staging.yoursite.com
NEXT_PUBLIC_SITE_URL=https://yoursite.cloudfront.net/

In my next.config.js file I include the necessaray environment variables with dotenv:

require('dotenv').config({
    path: `environments/.env.${process.env.BUILD_ENV || 'localhost'}`,
});

If you've got additional post-build scripts as I have then you'll need to include the above snippet to i.e in sitemap.xml.js

Then all that is left to do is run:

BUILD_ENV=develop npx serverless

huksley commented 3 years ago

The key point in this issue is to get environment variables from environment not from .env files. Using tools like chamber you can securely store them in AWS Parameter Store, or similar secure remote storage, so you do not need to commit secrets any more to repository.

Also works for CI/CD - you just use AWS access secrets + chamber to get all required env vars without having a need to configure them one by one everythere.

mattvb91 commented 3 years ago

Just a note from me. I was also struggling with the same issue and @murtyjones solution just did not work for me. The problem seems to be step 3 where I needed to change

myApp:
  component: '@sls-next/serverless-component@1.18.0'
  inputs:
    ...
    build:
      env:
        MY_SECRET_API_KEY: ${env.MY_SECRET_API_KEY}

to

myApp:
  component: '@sls-next/serverless-component@1.18.0'
  inputs:
    ...
    env:
      MY_SECRET_API_KEY: ${env.MY_SECRET_API_KEY}

So I needed to remove build nesting for env in serverless.yml file.

Best regards

@PaulKushch im still having issues even with this that nothing shows up in my lambda env? Currently struggling with this env issue over in #1010

Do your vars show up in the lambda config? i feel like im missing something obious

image

karimkawambwa commented 3 years ago

I tried adding the env varaibles to the apiEdgeLambdaInput https://github.com/serverless-nextjs/serverless-next.js/blob/master/packages/serverless-components/nextjs-component/src/component.ts#L479-L503

        name: readLambdaInputValue("name", "apiLambda", undefined) as
          | string
          | undefined,
        env: inputs.env?.apiLambda
      };

But I got the error InvalidLambdaFunctionAssociation: The function cannot have environment variables.

@macs91 Found out today Lambda@Edge doesn't support environment variables :/

Hopefully this will be supported / available soon! But for now you'll have to workaround it by using the build time config. with care. I'll keep the issue opened.

albehrens commented 2 years ago

We should add @murtyjones steps to the official documentation. I spent a day to figure this out. It is important that you follow all 4 steps. And what is very important to point out that you have to use a . instead of : when accessing env variables in the serverless.yaml (e.g. ${env.SECRET} instead of ${env:SECRET})

nick-myers-dt commented 2 years ago

I came to this thread as I was having issues getting the environment variable for NextAuth (NEXTAUTH_URL) to be picked up in my deployed distribution.

Just in case it's useful, I saw a question about how to get values from AWS Secrets Manager. I thought I'd share this code I used in my Next.js App to get values from AWS Secrets Manager (apologies if this taking the thread off topic and if the code isn't very good):

import { AWSError, SSM } from 'aws-sdk'
import SecretsManager, { GetSecretValueResponse } from 'aws-sdk/clients/secretsmanager'
import { PromiseResult } from 'aws-sdk/lib/request'

const client = new SecretsManager({ region: 'us-east-1' })

function getAwsSecret(
  secretName: string
): Promise<PromiseResult<GetSecretValueResponse, AWSError>> {
  return client.getSecretValue({ SecretId: secretName }).promise()
}

export const getAwsSecretAsync = async (
  secretName: string
): Promise<PromiseResult<GetSecretValueResponse, AWSError>> => {
  try {
    const response = await getAwsSecret(secretName)
    return response
  } catch (error) {
    console.error('Error occurred while retrieving AWS secret')
    console.error(error)
    throw new Error(error)
  }
}

For completeness, I use this with AWS Parameter Store

const getParameterWorker = async (name: string, decrypt: boolean): Promise<string> => {
  const ssm = new SSM({ region: 'us-east-1' })
  const result = await ssm.getParameter({ Name: name, WithDecryption: decrypt }).promise()
  if (result?.Parameter?.Value !== undefined) return result.Parameter.Value
  throw Error('Could not retrieve paramater for: ' + name)
}

export const getParameter = async (name: string): Promise<string> => {
  return getParameterWorker(name, true)
}

The IAM role associated with the distribution of the API Lambda then needs permission to get the value, with a policy along the lines of:

 {
    "Sid": "VisualEditor2",
    "Effect": "Allow",
    "Action": [
        "ssm:GetParametersByPath",
        "ssm:GetParameters",
        "ssm:GetParameter"
    ],
    "Resource": [
        "arn:aws:ssm:*:<account-number>:parameter/<parameter-name>"
    ]
}

In our CI/CD (Gitlab) we do this sort of thing to get parameters from SSM (with AWS Access Key ID and AWS Secret Access Key values in out Gitlab Environment variables) using an IAM account in one AWS account with permissions to assume roles in other accounts:

default:
  ...
  before_script:
    - mkdir ~/.aws && touch ~/.aws/config && touch ~/.aws/credentials
    - echo -e "[profile dev]\nregion=us-east-1 \nrole_arn=arn:aws:iam::<account-number>:role/DevAccount \nsource_profile = master" >> ~/.aws/config
    - echo -e "[profile test]\nregion=us-east-1 \nrole_arn=arn:aws:iam::<account-number>:role/TestAccount \nsource_profile = master" >> ~/.aws/config
    - echo -e "[profile prod]\nregion=us-east-1 \nrole_arn=arn:aws:iam::<account-number>:role/ProdAccount \nsource_profile = master" >> ~/.aws/config
    - echo -e "[master]\naws_access_key_id=$CI_MASTER_AWS_ACCESS_KEY_ID\naws_secret_access_key=$CI_MASTER_AWS_SECRET_ACCESS_KEY" > ~/.aws/credentials
    - export AWS_CONFIG_FILE=~/.aws/config
    - export AWS_SHARED_CREDENTIALS_FILE=~/.aws/credentials
    - export AWS_SDK_LOAD_CONFIG=true
  ...
deploy dev:
  ...
  variables:
    AWS_PROFILE: 'dev'
  ...
  script:
    - export API_KEY=$(aws ssm get-parameter --region us-east-1 --name /secret/api/key --with-decryption --query Parameter.Value --output text)
  ...
myriky commented 2 years ago

Just a note from me. I was also struggling with the same issue and @murtyjones solution just did not work for me. The problem seems to be step 3 where I needed to change

myApp:
  component: '@sls-next/serverless-component@1.18.0'
  inputs:
    ...
    build:
      env:
        MY_SECRET_API_KEY: ${env.MY_SECRET_API_KEY}

to

myApp:
  component: '@sls-next/serverless-component@1.18.0'
  inputs:
    ...
    env:
      MY_SECRET_API_KEY: ${env.MY_SECRET_API_KEY}

So I needed to remove build nesting for env in serverless.yml file. Best regards

@PaulKushch im still having issues even with this that nothing shows up in my lambda env? Currently struggling with this env issue over in #1010

Do your vars show up in the lambda config? i feel like im missing something obious

image

same problem here. i wanna set value to lambda environment. like a TimeZone

YaseenFSD commented 2 years ago

Would this work? I tried this on serverless@3.17.0 but not sure if it is exposed to the client by any chance. My local machine's OS is ubuntu 20.04

export MY_VARIABLE=value (shell command) In your code use process.env.MY_VARIABLE

Then deploy from local in your shell components-v1 or (depending on serverless version) serverless

I assume it is not exposed since here on the documentation they might be using this method in the "Getting Started" section for their AWS credentials.