Accessing Secure String in Next.js from SSM Parameter Store
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/
Related Articles
Building A Reusable Generic Http Client In Nextjs 14
Learn how to build a reusable, type-safe HTTP client for making API requests with GET, POST, PUT, and DELETE methods.
04/10/2024
Recommended tsconfig settings For Nextjs 14
Recommended tsconfig settings for Nextjs 14
03/10/2024
How to Use CASL in Next.js for Role Based Access Control
How to use CASL in Next.js for role based access control
01/10/2024
How to Add Sitemap to Next.js 14
Guide on adding a sitemap to a Next.js 14 application, including setup and automation.
20/08/2024