In my last post I talked about hosting a Next.js app on Azure Web Apps, and I was asked why I didn't use Static Web Apps
Out of curiosity, is there a reason that you chose to go AppService over Static Web Apps (which has native Next.js support)?
Aaron Powell
Which is good question, the main answer to which is that we like to ensure that what we deploy has been fully tested, barring any configuration changes - we build the application once and then deploy that single package to every environment - static web apps supports this for a number of application styles, but sadly not for Next.js apps at this time - from the linked article on the feature:
During the preview, the following features of Static Web Apps are unsupported for Next.js with server-side rendering: […] skip_app_build
and skip_api_build
can't be used.
However, I do like the Static Web Apps offer, so we've been using them for a few landing pages recently and we recently had a need to front a secured API call, which is where this post comes in.
The Problem: We have a Static Web App that needs to call an API with a key, and we don't want that key (or indeed the API endpoint) exposed to the end users. We also want to follow our usual build and deploy process, with a single build that is deployed to each environment, and as an additional benefit if we can enable Password Protection on both the site and the API, that would be great.
Managed Functions to the Rescue!
The primary way to hide the key and endpoint of the API is by using an intermediary API - to hide the key, it must be stored server side, and added to the request. I considered using Front Door to add the header to requests, but that would still expose an additional URL I didn't really want to use, so using a function app seemed like a better way to go. Having this as a Managed Function answered two of my asks:
- It keeps everything on the same site and domain (all functions are under the
/api/
route).
- Password Protection is enabled for the
/api/
route as well as the site pages.
However, when I initially tried to pre-build and deploy the function application, the builds failed with:
Cannot deploy to the function app because Function language info isn't provided.
Which was a little annoying.
The answer to this is on the Build Configuration documentation under Skip building the API:
If you want to skip building the API, you can bypass the automatic build and deploy the API built in a previous step.
Steps to skip building the API:
- In the
staticwebapp.config.json
file, set apiRuntime
to the correct runtime and version. Refer to Configure Azure Static Web Apps for the list of supported runtimes and versions.
- Set
skip_api_build
to true
.
- Set
api_location
to the folder containing the built API app to deploy. This path is relative to the repository root in GitHub Actions and cwd in Azure Pipelines.
So, adding the following into our staticwebapp.config.json
was the final piece in the puzzle:
{
"platform": {
"apiRuntime": "dotnet:6.0"
}
}
So now I can create a single build package, apply variable replacements to the appSettings.Production.json
file for the function app to provide sandbox/production endpoints and keys, and have the API protected in the same way as the static site.
Filed under: Azure, DevOps
We've started using Storybook for a few of the sites we're pulling together at work, while continuing to use Azure DevOps and deploying into our Azure estates. Running a simple static app produced from a Next.js site is pretty straightforward, but it turns out that running a full Next.js application on Azure App Service has a few less than obvious hoops you need to jump through to get everything lined up and working.
The key to solving this was the "With Docker" example, which provided a decent working point to compare what we had, and what we need.
Configure Next.js Output: Standalone
The main requirement is to ensure that your next.js application is configured to output as a Standalone application. In next.config.js
, ensure you've set:
module.exports = {
output: 'standalone',
}
This will create two core folders in the .next
output folder:
standalone
- This is the self contained application, including the required node_modules packages, along with a minimal server.js
file which can be used instead of next start
.
static
- This is the standard static site which needs to be deployed as well.
Finally, you also have your existing public
folder.
Set up your App Service
We created a Linux based App Service Plan, in our case a B1 instance.
The App Service is configured with:
- Stack:
Node
- Major version:
Node 16
- Minor version:
Node 16 LTS
- Startup Command: Provided by deployment
Everything else as you would normally configure it (e.g. FTP disabled, HTTP 2.0, HTTPS Only On, Minimum TLS Version 1.2, etc.).
Build and Deployment
Using Azure DevOps pipelines the following tasks will create a deployable package:
- stage: Build
jobs:
- job: NextBuild
pool:
vmImage: ubuntu-latest
steps:
- script: |
yarn install --frozen-lockfile
workingDirectory: $(Build.SourcesDirectory)
env:
CI: true
displayName: "Installing packages"
# Assumes output: 'standalone' configured on the next.config.
- script: |
yarn build
workingDirectory: $(Build.SourcesDirectory)
env:
CI: true
NODE_ENV: "production"
displayName: "Building Next application"
- task: CopyFiles@2
inputs:
sourceFolder: $(Build.SourcesDirectory)/.next/standalone
contents: |
**/*
targetFolder: $(Build.ArtifactStagingDirectory)/site-deploy
displayName: 'Copy standalone into the root'
- task: CopyFiles@2
inputs:
sourceFolder: $(Build.SourcesDirectory)/.next/static
contents: |
**/*
targetFolder: $(Build.ArtifactStagingDirectory)/site-deploy/.next/static
displayName: 'Copy static into the .next folder'
- task: CopyFiles@2
inputs:
sourceFolder: $(Build.SourcesDirectory)/public
contents: |
**/*
targetFolder: $(Build.ArtifactStagingDirectory)/site-deploy/public
flattenFolders: false
displayName: 'Copy Public folder'
- task: ArchiveFiles@2
inputs:
rootFolderOrFile: $(build.artifactStagingDirectory)/site-deploy
includeRootFolder: false
archiveType: "zip"
archiveFile: $(Build.ArtifactStagingDirectory)/Nextjs-site.zip
replaceExistingArchive: true
displayName: "Package Next application"
- task: PublishPipelineArtifact@1
inputs:
artifactName: Nextjs-site
targetPath: $(Build.ArtifactStagingDirectory)/Nextjs-site.zip
displayName: "Publish Next Application artifact"
Obviously you'll need to update the workingDirectory
and sourceFolder
paths as appropriate to your repo.
Basically, the steps are:
- Restore your packages
- Build your application
- Copy the contents of the
.next/standalone
folder into the root of a staging folder
- Copy the contents of the
.next/static
folder into the same folder within the staging folder
- Copy the content of the
public
folder into the same folder within the staging folder
- Create a zip archive of the staging folder, without including the root folder
- Publish the archive as a Pipeline Artifact
The published artifact should now contain the following structure:
+ /.next
¦ + /server
¦ ¦ + /chunks
¦ ¦ + /pages
¦ + /static
¦ + /chunks
¦ + /[hash]
+ /node_modules
+ /public
+ .env
+ package.json
+ server.js
Deployment is simply a case of deploying this package with the AzureWebApp
task:
- stage: Deploy
dependsOn: Build
condition: and(succeeded())
jobs:
- deployment: NextJs
environment: "QA"
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
inputs:
appType: webAppLinux
azureSubscription: azureRMServiceConnection
appName: NextWebAppName
package: $(Pipeline.Workspace)/Nextjs-site.zip
deploymentMethod: "zipDeploy"
runtimeStack: 'Node|16-lts'
startupCommand: 'node server.js'
displayName: "Deploy site to Azure"
The important settings to call out are:
appType
- Needs to be set to webAppLinux
so that we can use the startupCommand
.
runtimeStack
- Allows the development team to adjust the major and minor versions if necessary for their specific build.
startupCommand
- Needs to be set to node server.js
. This tells the App Service container to call the server.js
file from node, starting your application ready to receive requests.
And with that, you should now have a NextJS application running on a Linux based Azure App Service.
Filed under: Azure, DevOps, Next.js