Deploying pre-built functions to Azure Static Web Apps

Posted 30 November 2022, 10:50 | by | Perma-link

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:

  1. It keeps everything on the same site and domain (all functions are under the /api/ route).
  2. 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

Hosting Next.js apps on Azure Web Apps

Posted 24 November 2022, 02:52 | by | Perma-link

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:

  1. Restore your packages
  2. Build your application
  3. Copy the contents of the .next/standalone folder into the root of a staging folder
  4. Copy the contents of the .next/static folder into the same folder within the staging folder
  5. Copy the content of the public folder into the same folder within the staging folder
  6. Create a zip archive of the staging folder, without including the root folder
  7. 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