Deploy a Nuxt.js Site to AWS Using CDK
Automated deployments with feature branches and staged environments doesn't require an expensive deployment platform provider. Deploy your Nuxt.js sites directly to AWS infrastructure using AWS Cloud Development Kit (CDK) instead.
Deploying a Nuxt.js site to AWS using the AWS Cloud Development Kit (CDK) streamlines the deployment process without incurring additional costs from deployment platforms. The static site assets will be deployed to an S3 bucket and distributed via CloudFront CDN. Dynamic assets will be generated by a Lambda function and cached by the same CloudFront CDN.
Step 1: Install the Required Packages
To begin, you’ll need to install the following packages:
aws-cdk
aws-cdk-lib
@trautonen/cdk-dns-validated-certificate
tsx
constructs
Run the following command to install them:
npm install --save-dev aws-cdk aws-cdk-lib @trautonen/cdk-dns-validated-certificate tsx constructs
This will install the CDK core libraries and the tools to manage SSL certificates and construct abstractions. These packages will allow your Nuxt.js app to integrate seamlessly with AWS infrastructure, while tsx lets us write our CDK files in TypeScript.
Step 2: Update Nuxt Configuration for AWS Lambda
To ensure your Nuxt.js app deploys correctly on AWS Lambda, you need to adjust the nitro.preset
in the nuxt.config.ts
file to target aws-lambda
.
export default defineNuxtConfig({
nitro: {
preset: 'aws-lambda'
}
})
Nuxt.js’ Nitro framework includes the ability to run on various serverless platforms, and the aws-lambda
preset ensures your app is packaged for Lambda deployment. This shift is crucial since AWS Lambda behaves differently than traditional servers, especially in terms of request handling and environment lifecycles.
Step 3: Create a cdk.json
Configuration File
Next, we need to tell CDK how to bootstrap the application. Create a cdk.json
file in the root of your project with the following content:
{
"app": "npx tsx ./deployment/index.ts",
"watch": {
"include": [
"**"
],
"exclude": [
"README.md",
"cdk*.json",
"**/*.d.ts",
"**/*.js",
"tsconfig.json",
"package*.json",
"yarn.lock",
"node_modules",
"test",
".husky",
".vscode",
".nuxt"
]
}
}
This file tells CDK to execute index.ts
from the deployments
folder as the entry point for deployment. Here, tsx
handles TypeScript execution, simplifying our deployment setup.
Step 4: Create the Deployment Directory
To organize the deployment logic, create a deployments
directory. This will store the infrastructure definitions that the AWS CDK will use to build and deploy resources like S3, CloudFront, and Lambda. Inside this directory, add two files: index.ts
and nuxtSite.ts
.
/deployments
├── index.ts
└── nuxtSite.ts
Step 5: Define the Cloud Resources in nuxtSite.ts
The nuxtSite.ts
file defines how AWS resources like S3, CloudFront, and Lambda interact to host your Nuxt.js application. This setup ensures that the static assets are served via an S3 bucket, and CloudFront distributes them globally, deploys the server runtime as a Lambda function, and generates the required SSL certificates to serve the site over HTTPS.
import type { Stack } from 'aws-cdk-lib'
import type { IGrantable } from 'aws-cdk-lib/aws-iam'
import { DnsValidatedCertificate } from '@trautonen/cdk-dns-validated-certificate'
import { CfnOutput, Duration, RemovalPolicy } from 'aws-cdk-lib'
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'
import * as cloudfront_origins from 'aws-cdk-lib/aws-cloudfront-origins'
import { Architecture, AssetCode, Function, FunctionUrlAuthType, Runtime } from 'aws-cdk-lib/aws-lambda'
import * as route53 from 'aws-cdk-lib/aws-route53'
import * as targets from 'aws-cdk-lib/aws-route53-targets'
import * as s3 from 'aws-cdk-lib/aws-s3'
import * as s3deploy from 'aws-cdk-lib/aws-s3-deployment'
import { Construct } from 'constructs'
import { DeploymentType } from '.'
export interface NuxtSiteProps {
/**
* The type of deployment for the Nuxt.js site.
*/
readonly deploymentType: DeploymentType
/**
* The domain name for the Nuxt.js site.
*/
readonly domainName: string
/**
* The environment variables for the Lambda Function.
*
* * @default - none
*/
readonly environment?: Record<string, string>
/**
* Should the S3 and RDS retain data when the stack is destroyed?
*/
readonly shouldRetainData: boolean
/**
* The subdomain for the Nuxt.js site.
*/
readonly siteSubDomain: null | string | undefined
}
export class NuxtSite extends Construct {
/**
* The Lambda Function that serves the Nuxt.js site.
*/
readonly lambdaFunctionGrantable: IGrantable
constructor(parent: Stack, name: string, props: NuxtSiteProps) {
super(parent, name)
const isProd = props.deploymentType === DeploymentType.Production
const isStage = props.deploymentType === DeploymentType.Stage
const zone = route53.HostedZone.fromLookup(this, 'Zone', { domainName: props.domainName })
const siteDomain = props.siteSubDomain ? `${props.siteSubDomain}.${props.domainName}` : props.domainName
const siteBucket = new s3.Bucket(this, 'SiteBucket', {
autoDeleteObjects: !props.shouldRetainData,
blockPublicAccess: new s3.BlockPublicAccess({
blockPublicAcls: false,
blockPublicPolicy: false,
ignorePublicAcls: false,
restrictPublicBuckets: false,
}),
bucketName: siteDomain,
objectOwnership: s3.ObjectOwnership.OBJECT_WRITER,
publicReadAccess: true,
removalPolicy: props.shouldRetainData ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY,
websiteIndexDocument: 'index.html',
})
const lambdaFunction = new Function(this, 'LambdaFunction', {
architecture: Architecture.X86_64,
code: new AssetCode('./.output/server'),
environment: props.environment,
handler: 'index.handler',
memorySize: 1024,
runtime: Runtime.NODEJS_20_X,
timeout: Duration.seconds(30),
})
const lambdaFunctionUrl = lambdaFunction.addFunctionUrl({ authType: FunctionUrlAuthType.NONE })
const certificate = new DnsValidatedCertificate(this, 'SiteCertificate', {
certificateRegion: 'us-east-1',
domainName: siteDomain,
validationHostedZones: [
{
hostedZone: zone,
},
],
})
const lambdaFunctionOrigin = new cloudfront_origins.FunctionUrlOrigin(lambdaFunctionUrl)
const s3SiteOrigin = new cloudfront_origins.HttpOrigin(siteBucket.bucketWebsiteDomainName, {
protocolPolicy: cloudfront.OriginProtocolPolicy.HTTP_ONLY,
})
const requestPolicy = new cloudfront.OriginRequestPolicy(this, 'OriginRequestPolicy', {
cookieBehavior: cloudfront.OriginRequestCookieBehavior.all(),
headerBehavior: cloudfront.OriginRequestHeaderBehavior.allowList(
'Origin',
'Content-Type',
),
queryStringBehavior: cloudfront.OriginRequestCookieBehavior.all(),
})
const distribution = new cloudfront.Distribution(this, 'SiteDistribution', {
additionalBehaviors: {
'/api/*': {
allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
compress: true,
origin: lambdaFunctionOrigin,
originRequestPolicy: requestPolicy,
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
},
'/api/_content/*': {
allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
compress: true,
origin: s3SiteOrigin,
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
},
},
certificate: certificate,
defaultBehavior: {
allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
compress: true,
origin: new cloudfront_origins.OriginGroup({
fallbackOrigin: lambdaFunctionOrigin,
fallbackStatusCodes: [ 404, 403 ],
primaryOrigin: s3SiteOrigin,
}),
originRequestPolicy: requestPolicy,
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
},
domainNames: [ siteDomain ],
minimumProtocolVersion: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021,
})
new route53.ARecord(this, 'SiteAliasRecord', {
recordName: siteDomain,
target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(distribution)),
zone,
})
new s3deploy.BucketDeployment(this, 'DeployShortLivedAssets', {
cacheControl: isProd
? [
s3deploy.CacheControl.maxAge(Duration.hours(1)),
s3deploy.CacheControl.staleWhileRevalidate(Duration.minutes(5)),
]
: isStage
? [
s3deploy.CacheControl.maxAge(Duration.minutes(5)),
s3deploy.CacheControl.staleWhileRevalidate(Duration.minutes(1)),
]
: [
s3deploy.CacheControl.maxAge(Duration.seconds(0)),
s3deploy.CacheControl.noCache(),
s3deploy.CacheControl.noStore(),
s3deploy.CacheControl.mustRevalidate(),
],
destinationBucket: siteBucket,
distribution,
distributionPaths: [ '/*' ],
exclude: [ '_nuxt/*', '_fonts/*' ],
prune: true,
sources: [ s3deploy.Source.asset('./.output/public') ],
})
new s3deploy.BucketDeployment(this, 'DeployLongLivedAssets', {
cacheControl: isProd || isStage
? [
s3deploy.CacheControl.maxAge(Duration.days(30)),
s3deploy.CacheControl.staleWhileRevalidate(Duration.days(1)),
]
: [
s3deploy.CacheControl.maxAge(Duration.days(1)),
s3deploy.CacheControl.staleWhileRevalidate(Duration.hours(1)),
],
destinationBucket: siteBucket,
distribution,
distributionPaths: [ '/_nuxt/*', '/_fonts/*' ],
exclude: [ '*' ],
include: [ '_nuxt/*', '_fonts/*' ],
prune: false,
sources: [ s3deploy.Source.asset('./.output/public') ],
})
/**
* Setting class properties
*/
this.lambdaFunctionGrantable = lambdaFunction
new CfnOutput(this, 'Site', { key: 'Site', value: 'https://' + siteDomain })
}
}
This script will likely show an error for the DeploymentType
import as this does not exist yet, but we’ll get to that next!
Step 6: Set Up the Deployment Logic in index.ts
Now, configure the index.ts
file, which is responsible for initializing your CDK app and passing the necessary parameters (like domain name and certificate ARN) to the NuxtSite
construct:
import * as cdk from 'aws-cdk-lib'
import { NuxtSite } from './nuxtSite'
const envWest = { account: '12345678', region: 'us-west-2' }
const developmentDomain = 'test.com'
const productionDomain = 'example.com'
export const enum DeploymentType {
Development = 'Development',
Production = 'Production',
Stage = 'Stage',
}
export interface NuxtStackProps extends cdk.StackProps {
readonly branchName?: string
readonly deploymentType: DeploymentType
readonly releaseVersion?: string
}
class NuxtStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props: NuxtStackProps) {
const { branchName, deploymentType } = props
const isProd = deploymentType === DeploymentType.Production
const isStage = deploymentType === DeploymentType.Stage
const envAbbreviation = isProd ? 'prod' : isStage ? 'stage' : 'dev'
const shouldRetainData = isProd || isStage
const domainName = isProd ? productionDomain : developmentDomain
const siteSubDomain = isProd ? null : isStage ? 'stage.site' : `${branchName}.site`
const siteOrigin = !siteSubDomain ? `https://${domainName}` : `https://${siteSubDomain}.${domainName}`
const stackProps: cdk.StackProps = {
...props,
crossRegionReferences: true,
description: `Nuxt Example - Site ${deploymentType}${branchName ? ` (${branchName.toUpperCase()})` : ''}`,
stackName: `nuxt-example-site-${envAbbreviation}${branchName ? `-${props.branchName}` : ''}`,
tags: {
'Environment': deploymentType,
'IaC': 'CDK',
'Stack': 'Nuxt Example Site',
},
}
super(scope, id, stackProps)
const nuxtSite = new NuxtSite(this, 'NuxtSite', {
deploymentType,
domainName,
environment: {
NUXT_PUBLIC_RELEASE_VERSION: props.releaseVersion ?? `0.0.0-${envAbbreviation}`,
NUXT_PUBLIC_SITE_ORIGIN: siteOrigin,
},
shouldRetainData,
siteSubDomain,
})
}
}
const app = new cdk.App()
const environment: DeploymentType | undefined = app.node.tryGetContext('env')
const branchName: string | undefined = app.node.tryGetContext('branch')?.toString().toLowerCase()
const releaseVersion: string | undefined = app.node.tryGetContext('version')?.toString().replace(/^v/, '')
if (!environment) {
throw new Error('Please provide an environment context value')
}
if (environment === DeploymentType.Development && !branchName) {
throw new Error('Please provide a branch context value')
}
const envShortName = environment === DeploymentType.Production ? 'Prod' : environment === DeploymentType.Stage ? 'Stage' : 'Dev'
const stackName = `Nuxt-Example-Site-${envShortName}${branchName ? `-${branchName.toUpperCase()}` : ''}`
new NuxtStack(app, stackName, {
branchName,
deploymentType: environment,
env: envWest,
releaseVersion,
})
Replace the account
, region
, developmentDomain
and productionDomain
with your deployment details.
Step 7: Add Deployment Scripts to package.json
To simplify deployment, add commands to your package.json
under the scripts
section:
{
"scripts": {
"deploy:dev": "npx aws-cdk deploy --all -c env=Development -c branch=dev --ci --hotswap-fallback --require-approval never ",
"deploy:stage": "npx aws-cdk deploy --all -c env=Stage --ci --require-approval never",
"deploy:production": "npx aws-cdk deploy --all -c env=Production --ci --require-approval never",
}
}
These commands allow for the easy deployment of the Nuxt.js site in multiple modes and environments. The deploy:dev
script can be modified to support a branch specifier to allow deployments of PRs or Ad-Hoc branches.
Conclusion
By following these steps, you’ve set up a fully automated deployment pipeline for your Nuxt.js site using AWS Lambda, S3, and CloudFront, all managed with AWS CDK. With this approach, you can take advantage of serverless architecture, making it easy to scale and manage your application on AWS.
By integrating Nuxt.js’ Nitro framework with AWS CDK, you now have a flexible, highly available solution for deploying modern serverless websites. Happy deploying!