I'm trying to get my head around the different tre...
# help
w
I'm trying to get my head around the different treatment of environment variables in NextjsSite vs ReactStaticSite (and StaticSite). In my particular use case I want to reference other constructs in my SST app in my Nextjs source code (eg environment variables such as MY_OTHER_LAMBDA_API_URL and MY_CONTENT_BUCKET). With ReactStaticSite and StaticSite I can reference them, but not in NextjsSite. Just wondering why that is and whether it would be possible to make handling of NextjsSite source code more like ReactStaticSite and StaticSite? Or should I split my app into two different sst apps and somehow pass the outputs from one sst app into a sst NextJs app?
f
Hey @Warwick Grigg, this should be TOTALLY possible 🙂
Can you share a bit more context on where are you using the env var that is not replaced?
w
It's getting those values into getStaticProps that I'd like to streamline. ReactStaticSite can get constructs (bucket names etc) passed in at build time and I'm wondering whether something similar could be done for getStaticProps in NextjsSite. Otherwise perhaps split off my bucket construct into a separate sst app and pass its name or arn into the sst NextjsSite?
f
Can you show a snippet of how you are creating the
NextjsSite
construct?
w
Hey @Frank sure:
const ContentBucket = new sst.Bucket(this, "ContentBucket");
    
new s3deploy.BucketDeployment(this, "DeployContentPostBucket", {
      
sources: [s3deploy.Source.asset("./content/bucket/blog")],
      
destinationBucket: ContentBucket.s3Bucket,
      
destinationKeyPrefix: "blog", // optional prefix in destination bucket
    
});
    
// Create a Next.js site
    
const site = new sst.NextjsSite(this, "Site", {
      
path: "frontend",
      
environment: {
        
// Pass the bucket details to our app
        
REGION: scope.region,
        
BUCKET_NAME: ContentBucket.bucketName,
      
},
    
});
    
// Allow the Next.js site to access the content bucket
    
site.attachPermissions([ContentBucket]);
There's a cut down version of my project at https://github.com/warwickgrigg/sst-nextjs-blog
@Frank could you give me some pointers how I could feed
ContentBucket.bucketName
into the NextjsSite construct at build time. Should I split
sst.Bucket
and
sst.NextjsSite
into different sst apps or stacks, and if so how do I feed
ContentBucket.bucketName
from one to the other in an automated way? If there's some documentation covering that, either in sst or cdk, that would be great.
f
Hey @Warwick Grigg, sorry about missing ur previous message. With v0.46.0,
REGION
will be accessible inside
getStaticProps
b/c it’s a constant. But
BUCKET_NAME
is not available at build time. The actual value of
BUCKET_NAME
is only resolved at deploy time.
Imagine you are building for the first time, the S3 bucket hasn’t been created. So when you are building the Next.js app, the bucket name is not known at build time. Hence your
getPost()
call in ur Next.js app doesn’t know where to load the post from.
A work around for this would be to create an SSM parameter with the value, and read it at build time. For example:
Copy code
const ContentBucket = new sst.Bucket(this, "ContentBucket");

const ssmParamName = scope.logicalPrefixedName("ContentBucketName");
const ssmBucketName = new ssm.StringParameter(stack, 'ContentBucketNameParameter', {
  parameterName: ssmParamName,
  stringValue: ContentBucket.bucketName,
});

const site = new sst.NextjsSite(this, "Site", {
  path: "frontend",
  environment: {
    REGION: scope.region,
    BUCKET_NAME: ssm.StringParameter.valueFromLookup(stack, ssmParamName),
  },
});
What this does is
ssm.StringParameter.valueFromLookup
makes an AWS SDK call to SSM to fetch the value, so the bucket name is available at build time.
Let me know if this makes sense.
w
@Frank I've tried this but got this error on build
SSM parameter not available in account nnnnnnn, region us-east-1: warwick-nextjs-blog-ContentBucketName
I had changed
ssm.StringParameter(stack, 'ContentBucketNameParameter', ...
to
ssm.StringParameter(this, 'ContentBucketNameParameter',
. Should I have split my sst code into two stacks?
f
Hey @Warwick Grigg, sorry for getting back to this late. I think u r right, on the first deploy
ssm.StringParameter.valueFromLookup(this, ssmParamName)
doesn’t exist yet b/c the SSM parameter hasn’t been created.
Give this a try: 1. Same as above, create an SSM parameter after the bucket is created:
Copy code
const ContentBucket = new sst.Bucket(this, "ContentBucket");

const ssmParamName = scope.logicalPrefixedName("ContentBucketName");
const ssmBucketName = new ssm.StringParameter(stack, 'ContentBucketNameParameter', {
  parameterName: ssmParamName,
  stringValue: ContentBucket.bucketName,
});
2. In our index file (ie.
lib/index.js
or
stacks/index.js
) make a call to the SSM value using AWS SDK. If the value does not exist (ie. in the case of the first deployment), default the value to a “placeholder”. 3. Pass the value to the stack, and assign it to the environment
Copy code
const site = new sst.NextjsSite(this, "Site", {
  path: "frontend",
  environment: {
    REGION: scope.region,
    BUCKET_NAME: props.VALUE_FROM_AWS_SDK,
  },
});
Let me know if it makes sense.
w
@Frank That makes sense, thank you. Leading on from that: 1) On detecting a first-time deployment is there any way I can "force" a redeploy? 2) Would it work similarly (and be simpler) to eliminate the SSM parameter and just create the bucket like this:
{bucketName: this.logicalPrefixedName("ContentBucket")}
. Then in getStaticPaths / getstaticProps, detect a first time deployment by testing existence of the bucket with the name the value of
process.env.BUCKET_NAME
? 3) Is there any way to serialise two stacks (or apps) so the second stack (or app) "awaits" completion of the first?
d
Warwick please let me know if you get this working. @Frank I wasn’t quite able to figure this out.
f
1) On detecting a first-time deployment is there any way I can “force” a redeploy?
Not out of the box. You can write an ie. bash script to check if the SSM exists, and run
sst deploy
twice.
2) Would it work to just create the bucket like this: 
{bucketName: this.logicalPrefixedName("ContentBucket")}
Yes, hardcoding the name will work in this case. Thought it’s always tricky to hard code bucket names b/c the they need to unique across all region/all users on AWS.
3) Is there any way to serialise two stacks (or apps) so the second stack (or app) “awaits” completion of the first?
No, all stacks are built before hand. You can’t built stackA, deploy stackA, then build stackB.
Hey @Devin, yeah this workaround is a bit cumbersome. I don’t have working code with me, does the 3 steps above make sense? Let me know which step is confusing, and I can elaborate on it. https://serverless-stack.slack.com/archives/C01JG3B20RY/p1634575874311200?thread_ts=1633529817.256700&cid=C01JG3B20RY
w
@Frank thank you. I get your point about S3's global namespace. I'll continue with the SSM parameter as you suggested. @Devin I'll update my (public) repo accordingly and let you know - it's a very basic proof of concept
d
Thanks to both of you! I’ll have some time to play with this after work. So close!
w
@Frank I've made the changes and deployed the app fully, but there's something really weird. The static pages generated properly at sst build time, picking up the content from the bucket (with the bucket name fetched from the environment variable). When deployed I can fetch the static page https://drd1ao6qxhdcc.cloudfront.net/s3posts/a.txt . But the server generated page https://drd1ao6qxhdcc.cloudfront.net/s3posts/b.txt returns 404. I can see why: inspecting the Lambda SiteMainFunction code on the AWS console, I can see that none of the process.env values have been replaced by the NextjsSite environment: they remain as process.env.BUCKET_NAME etc. My repo is at https://github.com/warwickgrigg/sst-nextjs-blog.
d
Going for it @Warwick Grigg thanks for linking your demo
well it deployed which is awesome! But amplify is incorrectly configured on
prod
so I suspect I still have something to figure out with
env
variables and passing everything around
Thanks heaps for your help!
w
@Frank I think SST NextjsSite is replacing process.env vars in the initial build phase but not in the deployment SiteMainFunction Lambda. Is that possible?
@Frank Here are the steps to reproduce the problem "NextjsSite replacing process.env vars in the initial build phase but not in the deployment SiteMainFunction Lambda". 1) git clone https://github.com/warwickgrigg/sst-nextjs-blog 2) cd sst-nextjs-blog 3) run npm install (and in frontend) 4) npx sst deploy --stage warwickrepo 5) repeat stage 4 6) open webpage s3posts/b.txt for the cloudfront site 7) check the cloudwatch logs which show { bucketName: undefined, testVar: undefined, ...} 8 ) check the SiteMainFunction Lambda code in pages/s3posts/[id].js which contains process.env.BUCKET_NAME etc
d
I am experiencing the same issue with the undefined variables.
f
Hey @Warwick Grigg @Devin I’m going to try and reproduce it today. Will keep you posted!
w
@Frank github issue #937 looks remarkably similar; my environment variables are javascript string values, not CDK tokens. Maybe the same root cause?
@Frank @Devin A trick I've used before is a viable workaround in the meantime. Adding this to next.config.js:
Copy code
env: {
    BUCKET_NAME: process.env.BUCKET_NAME,
    TEST_VAR: process.env.TEST_VAR,
},
d
This broke my jest tests when I was doing it this way before, but did pass my variables correctly
w
@Devin Maybe try this next.config.js and put the actual test env values in env.test (or env.test.local)? https://nextjs.org/docs/basic-features/environment-variables#test-environment-variables
@Frank Thanks for all your help with this. The strategy you devised for getting CDK resource ids into the build via SSM is working very well. I've got the POC deployed very nicely via SST the way I hoped, with combined SSG/SSR pages via {fallback: blocking}. I'll update my repo with my workaround for the environment variables, and a basic README
f
@Warwick Grigg thanks for the update. I’m going to take a crack at the GitHub issue.
Hey guys, the environment variable issue is fixed in v0.49.2
a
Having read the docs and these threads, I'm still a little confused about some aspects of the environment vars with next.js. My user case is to use Amplify libraries that require properties from cognito user pools. Ideally I would set this values in the generated JS at build time. It is my understanding is that the
{{ ENV_VAR }}
are replaced in the generated html and js. However the static files uploaded to S3 for the site do not seem to have this set. Would any of the following suggestions work for making
{{ ENV_VAR }}
available at build time would be: • Suggestion 1: Two phase builds: ◦ Phase 1: build the site so generate the manifests for lambda and S3 storage. ◦ Run stack deploy ◦ Phase 2: build the site with generated pages with
{{ ENV_VAR }}
populated and upload to the S3 bucket • Suggestion 2: Lambda builder - Build the site locally to generate the manifests and deploy the stack. Add to the stack a Lambda that can build the site and deploy the static pages to S3. Post deploy completion trigger the lambda builder process
w
In my nextjs code, I just use process.env.ENV_VAR. It works for me. I think, {{ ENV_VAR}} is just in sst's internal build step and you can ignore it.
a
Does that work in static pages as well as API routes?
Do you need to use the
NEXT_PUBLIC_
prefix for them to be available to the client side JS after
next build
has been run?
Does anyone have an example repo with the
env
values used in a none API route or in
_app
?
w
Regarding the client side env vars, I use the NEXT_PUBLIC_ prefix in Nextjs. I haven't tried anything else for client side as it's the way I've always done it, whether deploying on vercel, exporting static sites from Nextjs etc. Sorry I don't have a minimal repo for this at hand. I was encountering quite a few glitches with environment variables with sst's NextjsSite construct which have now been fixed. I'm feeling confident about using sst's NextjsSite Yes, process.env.ENV_VAR works in Nextjs static pages and Nextjs API routes. One thing that is tricky is setting environment variables needed for static generation that won't be known until SST deploys the site (eg setting the name of a bucket). That's because SST generates the pages (next build) before it creates the unique bucket name. There's a recent thread about this. It works for me, albeit having to run sst deploy twice when I deploy to a new stage/account for the first time
a
Okay, thanks. That confirmation at least helps. I wonder if either of my two suggestions would allow an option where there is no need to deploy twice manually? I would look to build this and submit a PR if people think either of these would be a workable option
w
@Alistair Stead I had similar ideas, but I don't know enough about SST, CDK, Cloudformation and Nextjs internals to validate the idea. I was also curious how SST's StaticSite and ReactSite constructs handle this in a single phase deployment.
d
So far, I’ve not been able to get Amplify available on the front end either. I can do it locally tho!
a
I've been able to get Amplify setup and working using ENV vars as per the SST documentation. I can run locally as well as deploy to AWS with ‘npx sst deploy’. The strange issue I have now is that when deploying from GitHub actions the deployed JS seems to be different and results in errors with Amplify is the browser
f
Hey @Alistair Stead, can you check if the GitHub actions is using the latest version or the same version of SST as ur local.
The suggestion #2 is a bit tricky as the Lambda function only has 500MB of disk space. Might not be enough to build the Next.js app. As for suggest #1, that’s pretty much what @Warwick Grigg was doing by fetching the SSM values manually using AWS SDK. The first time it builds, the SSM values are not set because it has not been deployed yet. You have to build it again to pick up the SSM values.
@Devin, let me write something up with an example, and I will share it with you.
a
@Frank Thanks for the feedback. I’m pretty sure all the versions are the same across local and CI/CD. However, I’ll create a public minimal example to validate the problem.
d
let me write something up with an example, and I will share it with you.
Thanks Frank! I’m available to help however I can. Please let me know if there’s anything I can do.
f
Will do! Thanks @Devin
k
I'm starting to look into this now to configure Amplify Auth on the frontend. Will ping back here with updates.
Solid it worked. Late here so I'll work on getting something out tmrw
@Devin, @Frank and @Warwick Grigg, adding some notes on this (going to add to a blog so ignore the circular references 😅 ) SST deployments were working fine locally, but I ran into errors with the UserPool when attempting to configure Amplify on the frontend. The UserID was was exposed through a
NEXT_PUBLIC_
env var through the
NextJsSite
construct. But because the User Pool wasn't yet created, the User Pool Name wasn't accessible. This is how I'm calling Amplify on the frontend:
Copy code
javascript
// amplifyLib.js
import { Amplify } from "aws-amplify";
import awsExports from "src/aws-exports";

export function configureAmplify() {
  return Amplify.configure({ ...awsExports, ssr: true });
}

// src/aws-exports.js
const awsExports = {
  Auth: {
    mandatorySignIn: false,
    region: process.env.NEXT_PUBLIC_REGION,
    userPoolId: process.env.NEXT_PUBLIC_USER_POOL_ID,
    identityPoolId: process.env.NEXT_PUBLIC_IDENTITY_POOL_ID,
    userPoolWebClientId: process.env.NEXT_PUBLIC_USER_POOL_CLIENT_ID,
  }
};
export default awsExports;

// index.js
import { configureAmplify } from "src/common/libs/amplifyLib";
// ... more imports
configureAmplify();
I ended up adapting the solutions from Frank and Warwick in this thread this thread . I also referred to Warwick's GitHub which was very helpful! There wasn't much of a difference outside of creating an SSM parameter for the User Pool ID Name as opposed to the S3 bucket. Relevant code for the backend:
Copy code
javascript
// index.js
import { SSMClient, GetParameterCommand } from "@aws-sdk/client-ssm"

const getParameter = async (paramName, region) => {
  const params = {
    Name: paramName,
    WithDecryption: false,
  };

  const ssmClient = new SSMClient({ region });
  try {
    const { Parameter: { Value }} = await ssmClient.send(
      new GetParameterCommand(params)
    );
    return Value;
  } catch (err) {
    return undefined;
  }
}

export default async function main(app) {
  const ssmParamName = app.logicalPrefixedName("UserPoolIdName");
  const userPoolIdName = await getParameter(ssmParamName, app.region);

  console.log({ ssmParamName, userPoolIdName });
  new MyStack(app, "my-stack", { ssmParamName, userPoolIdName });
}


// MyStack.js
import { StringParameter } from "aws-cdk-lib/aws-ssm";
export default class MyStack extends sst.Stack {
  constructor(scope, id, props) {
    super(scope, id, props);

    const { ssmParamName, userPoolIdName = "" } = props;
    console.log({ ssmParamName, userPoolIdName });

    const auth = new sst.Auth(this, "Auth", {
      cognito: {
        userPool: {
          signInAliases: {
            email: true,
          },
        },
      },
    });

    // Define SSM Parameter
    new StringParameter(this, "UserPoolIdParameter", {
      parameterName: ssmParamName,
      stringValue: auth.cognitoUserPool.userPoolId
    });

    const site = new sst.NextjsSite(this, "ProdSite", {
      path: "frontend",
      customDomain: {
        domainName:
          scope.stage === "prod" ? "<http://productionSiteURL.io|productionSiteURL.io>" : `${scope.stage}.<http://productionSiteURL.io|productionSiteURL.io>`,
        domainAlias: scope.stage === "prod" ? "<http://www.productionSiteURL.io|www.productionSiteURL.io>" : undefined,
        cdk: {
          certificate: Certificate.fromCertificateArn(
            this,
            "ProductionCert",
            process.env.PRODUCTION_CERT_ARN
          ),
        },
          hostedZone: HostedZone.fromHostedZoneAttributes(
            this,
            "ProdZone",
            {
              hostedZoneId: process.env.HOSTED_ZONE_ID,
              zoneName: process.env.ZONE_NAME,
            }
          ),
      },
      environment: {
        NEXT_PUBLIC_REGION: scope.region,
        NEXT_PUBLIC_USER_POOL_ID: userPoolIdName,
        NEXT_PUBLIC_USER_POOL_ARN: auth.cognitoUserPool.userPoolArn,
        NEXT_PUBLIC_IDENTITY_POOL_ID: auth.cognitoCfnIdentityPool.ref,
        NEXT_PUBLIC_USER_POOL_CLIENT_ID:
          auth.cognitoUserPoolClient.userPoolClientId,
      },
      waitForInvalidation: false, // dev only
    });

    this.addOutputs({
      MESSAGE: userPoolIdName
        ? "" 
        : "Rerun `sst deploy` to generate UserPoolID",
    });
  }
}
I ran
npx sst start
once and indeed the
UserPoolIdName
was undefined. Running again as suggested produced a valid
UserPoolIdName
as the resource name was being pulled from SSM. Running
npx sst deploy
ran successfully. My sense is that exposing other User Pool properties as Next environment variables should throw an error similar to what I experienced with the
UserPoolIdName
. These are also passed into the Amplify configuration, though perhaps the User Pool ID is all that is required for the initialization. I also don't have an immediate need for Auth, so haven't given the Identity pool or User pool client an opportunity to yell at me 😄 Similar to others, I suppose there must be a cleaner way to do this. Scott considers in this thread the idea of moving the frontend to a separate repo, which then fetches the vars from a config. Another thought would be to create a separate stack for SSM params, deploy that first, and then deploy the Main stack. Though expect these may require separate
sst.json
files.