Company Logo

Bisht Bytes

Accessing Secure String in Next.js from SSM Parameter Store

Published On: 14 Aug 2024
Reading Time: 5 minutes

Overview


When working with environment variables in a Next.js application, especially when fetching secret strings from AWS SSM Parameter Store, you might encounter issues if you try to access them directly within your code. This approach may seem straightforward, but it can break your application during the build or deployment phases due to how Next.js handles its build process.

Challenges

If you follow the AWS Amplify documentation for accessing secrets (secure string), you might run into another roadblock. The Amplify setup is designed specifically for Amplify-based apps and doesn't align well with Next.js projects, where the build process differs significantly. This discrepancy can lead to failures, as the methods recommended by Amplify don't account for the separation between the build and deployment phases in Next.js.

The root of the issue lies in Next.js's architecture. During the build phase, the application code is bundled, and any attempt to access secrets directly from the SSM Parameter Store will be fetched. But the same properties when the deployment happens will be undefined. You might face the issue vice versa based on how you tried to access the variable. But inspite that the solution to problem remains the same.

Solution

The correct approach is to access environment variables through the Next.js configuration file (next.config.js). By defining your environment variables there, you ensure that they are properly injected into the build process and available at runtime. This method guarantees that your secrets are handled securely and that your application remains stable throughout both the build and deployment phases.

import { GetParameterCommand, SSMClient } from '@aws-sdk/client-ssm'

export const AWS_PRIMARY_REGION = 'ap-south-1'

async function getParameter(secretPath) {
  // Get secret from AWS Systems Manager
  const client = new SSMClient({
    region: AWS_PRIMARY_REGION,
  })
  const command = new GetParameterCommand({
    Name: secretPath,
    WithDecryption: true,
  })

  try {
    const data = await client.send(command)
    return data.Parameter?.Value
  } catch (err) {
    console.error(err)
    throw err
  }
}

async function getSsmSecret(propertyName, type) {
  const AWS_SSM_PARAMS_BRANCH = process.env.AWS_SSM_PARAMS_BRANCH ?? ''
  const AWS_SSM_SECRET_PATH = AWS_SSM_PARAMS_BRANCH + propertyName
  const AWS_SSM_SECRET_VALUE = await getParameter(AWS_SSM_SECRET_PATH)
  return AWS_SSM_SECRET_VALUE ?? ''
}

/** @type {import('next').NextConfig} */
const nextConfig = {
  env: {
    MONGO_URI: await getSsmSecret('MONGO_URI'),
    JWT_SECRET_KEY: await getSsmSecret('JWT_SECRET_KEY'),
    AWS_ACCESS_KEY_ID: await getSsmSecret('AWS_ACCESS_KEY_ID'),
    AWS_ACCESS_SECRET_KEY: await getSsmSecret('AWS_ACCESS_SECRET_KEY'),
  },
}

export default nextConfig

Note that SSM secret path in parameter store could look like this:

# .env
AWS_SSM_PARAMS_BRANCH=/amplify/<app-id>/<some>-branch-<hash>/

Multi Branch Setup

import { GetParameterCommand, SSMClient } from '@aws-sdk/client-ssm'

export const AWS_PRIMARY_REGION = 'ap-south-1'

async function getParameter(secretPath) {
  // same as above
}

/* type: 'shared' | 'branch'*/
async function getSsmSecret(propertyName, type) {
  const AWS_SSM_PARAMS_BRANCH = process.env.AWS_SSM_PARAMS_BRANCH ?? ''
  const AWS_SSM_PARAMS_SHARED = process.env.AWS_SSM_PARAMS_SHARED ?? ''
  const APPLICABLE_PARAM_PATH = type === 'branch' ? AWS_SSM_PARAMS_BRANCH : AWS_SSM_PARAMS_SHARED
  const AWS_SSM_SECRET_PATH = APPLICABLE_PARAM_PATH + propertyName
  const AWS_SSM_SECRET_VALUE = await getParameter(AWS_SSM_SECRET_PATH)
  return AWS_SSM_SECRET_VALUE ?? ''
}


/** @type {import('next').NextConfig} */
const nextConfig = {
  env: {
    MONGO_URI: await getSsmSecret('MONGO_URI', 'branch'),
    JWT_SECRET_KEY: await getSsmSecret('JWT_SECRET_KEY', 'shared'),
    AWS_ACCESS_KEY_ID: await getSsmSecret('AWS_ACCESS_KEY_ID', 'shared'),
    AWS_ACCESS_SECRET_KEY: await getSsmSecret('AWS_ACCESS_SECRET_KEY', 'shared'),
  },
}

export default nextConfig

Note that SSM secret path in parameter store in multi branch setup, will be different thats why we toggle between paths based on where environment is defined. The path could look like this:

# .env
AWS_SSM_PARAMS_BRANCH=/amplify/<app-id>/staging-branch-<hash>/
AWS_SSM_PARAMS_SHARED=/amplify/shared/<app-id>/

NOTE: The code above is for mutli branch setup where you have defined secrets for main and staging brach seperately.

In summary, while it might be tempting to directly fetch secrets from SSM within your Next.js code, doing so can lead to unpredictable behavior and build failures. Instead, rely on Next.js's environment configuration to safely and reliably manage your environment variables.

Reference

https://docs.amplify.aws/react/deploy-and-host/fullstack-branching/secrets-and-vars/


Page Views: -