Deploy to Azure Static Web Apps

Hey folks, after much tinkering, I got RedwoodJS deployed to Azure App Service. Not sure if this should go here, but here we go. Azure App Service is the hosting server service offered by Azure that works with my tech stacks. It technically runs your app in a docker container but makes all the configuration transparent to the user.
If you were attempting to configure your app to run node.js and push your code. By default, it will try to auto-build your application by running yarn workspace, which will fail. If you somehow were to get it to install, it would automatically run yarn build.

Azure offers a deployment process using Github Actions to install, build, and upload assets, but this process is broken. You can see issues with it here Deployment is *very* slow (nodeJS with publish profile) · Issue #60 · Azure/webapps-deploy · GitHub.

A custom deployment script is the best approach I have found to succeed. App Service uses a preconfigured Kudu build process to build your application, but you can generate and customize this file. And that was the route I took. You can find the guide here.

Create a .deployment file at the root of your project and add the following.

[config]
command = bash deploy.sh

Next, create a deploy.sh file and add the following.

#!/bin/bash

# ----------------------
# KUDU Deployment Script
# Version: 1.0.17
# ----------------------

# Helpers
# -------

exitWithMessageOnError () {
  if [ ! $? -eq 0 ]; then
    echo "An error has occurred during web site deployment."
    echo $1
    exit 1
  fi
}

# Prerequisites
# -------------

# Verify node.js installed
hash node 2>/dev/null
exitWithMessageOnError "Missing node.js executable, please install node.js, if already installed make sure it can be reached from current environment."

# Setup
# -----

SCRIPT_DIR="${BASH_SOURCE[0]%\\*}"
SCRIPT_DIR="${SCRIPT_DIR%/*}"
ARTIFACTS=$SCRIPT_DIR/../artifacts
KUDU_SYNC_CMD=${KUDU_SYNC_CMD//\"}

if [[ ! -n "$DEPLOYMENT_SOURCE" ]]; then
  DEPLOYMENT_SOURCE=$SCRIPT_DIR
fi

if [[ ! -n "$NEXT_MANIFEST_PATH" ]]; then
  NEXT_MANIFEST_PATH=$ARTIFACTS/manifest

  if [[ ! -n "$PREVIOUS_MANIFEST_PATH" ]]; then
    PREVIOUS_MANIFEST_PATH=$NEXT_MANIFEST_PATH
  fi
fi

if [[ ! -n "$DEPLOYMENT_TARGET" ]]; then
  DEPLOYMENT_TARGET=$ARTIFACTS/wwwroot
else
  KUDU_SERVICE=true
fi

if [[ ! -n "$KUDU_SYNC_CMD" ]]; then
  # Install kudu sync
  echo Installing Kudu Sync
  npm install kudusync -g --silent
  exitWithMessageOnError "npm failed"

  if [[ ! -n "$KUDU_SERVICE" ]]; then
    # In case we are running locally this is the correct location of kuduSync
    KUDU_SYNC_CMD=kuduSync
  else
    # In case we are running on kudu service this is the correct location of kuduSync
    KUDU_SYNC_CMD=$APPDATA/npm/node_modules/kuduSync/bin/kuduSync
  fi
fi

# Node Helpers
# ------------

selectNodeVersion () {
  if [[ -n "$KUDU_SELECT_NODE_VERSION_CMD" ]]; then
    SELECT_NODE_VERSION="$KUDU_SELECT_NODE_VERSION_CMD \"$DEPLOYMENT_SOURCE\" \"$DEPLOYMENT_TARGET\" \"$DEPLOYMENT_TEMP\""
    eval $SELECT_NODE_VERSION
    exitWithMessageOnError "select node version failed"

    if [[ -e "$DEPLOYMENT_TEMP/__nodeVersion.tmp" ]]; then
      NODE_EXE=`cat "$DEPLOYMENT_TEMP/__nodeVersion.tmp"`
      exitWithMessageOnError "getting node version failed"
    fi

    if [[ -e "$DEPLOYMENT_TEMP/__npmVersion.tmp" ]]; then
      NPM_JS_PATH=`cat "$DEPLOYMENT_TEMP/__npmVersion.tmp"`
      exitWithMessageOnError "getting npm version failed"
    fi

    if [[ ! -n "$NODE_EXE" ]]; then
      NODE_EXE=node
    fi

    NPM_CMD="\"$NODE_EXE\" \"$NPM_JS_PATH\""
  else
    NPM_CMD=npm
    NODE_EXE=node
  fi
}

##################################################################################################################################
# Deployment
# ----------

echo Handling node.js deployment.

# 1. KuduSync
if [[ "$IN_PLACE_DEPLOYMENT" -ne "1" ]]; then
  "$KUDU_SYNC_CMD" -v 50 -f "$DEPLOYMENT_SOURCE" -t "$DEPLOYMENT_TARGET" -n "$NEXT_MANIFEST_PATH" -p "$PREVIOUS_MANIFEST_PATH" -i ".git;.hg;.deployment;deploy.sh"
  exitWithMessageOnError "Kudu Sync failed"
fi

# 2. Select node version
selectNodeVersion

# 3. Install npm packages
if [ -e "$DEPLOYMENT_TARGET/package.json" ]; then
  cd "$DEPLOYMENT_TARGET"

  echo "Running yarn install"
  eval yarn install
  echo "Build!"
  eval yarn rw build api
  exitWithMessageOnError "yarn failed"
  cd - > /dev/null
fi

##################################################################################################################################
echo "Finished successfully."

Most of this is the default stuff, the main change in the deploy process was to do yarn install instead of yarn workspace to get dev dependencies installed. Next was to run redwoods yarn rw build api.

After Kudu runs that custom deployment it will start the server inside the nodejs container automatically using yarn start. Updating the package.json and adding a script to run yarn rw build api is the next thing to do.

{
  "private": true,
  "scripts": {
    "start": "yarn rw serve api"
  },
  ... 
}

Next I needed to have the API is use what ever port App Service wants it to use. Updating the redwood.toml file I needed to allow for it to set the port from the env.

...
[api]
  port = "${PORT:8911}"
[browser]
  open = false

The last thing I did was update the Github Action to automate the deployment to Azure.

# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
# More GitHub Actions for Azure: https://github.com/Azure/actions

# name: Build and deploy Node.js app to Azure Web App - redwood-goose

on:
  push:
    branches:
      - master
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2

      - name: Upload artifact for deployment job
        uses: actions/upload-artifact@v2
        with:
          name: node-app
          path: .

  deploy:
    runs-on: ubuntu-latest
    needs: build
    environment:
      name: 'Production'
      url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}

    steps:
      - name: Download artifact from build job
        uses: actions/download-artifact@v2
        with:
          name: node-app

      - name: 'Deploy to Azure Web App'
        id: deploy-to-webapp
        uses: azure/webapps-deploy@v2
        with:
          app-name: 'redwood-goose'
          slot-name: 'Production'
          publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE}}
          package: .

I completely removed the nodejs build phase of the original template and I just have the action pushing the code to Azure. From here Azure will run the custom Kudu deploys.sh and then start the server using yarn start which invokes yarn rw build api. From the dashboard on Azure I used the SSH tool to go into the app and run migrate to get the database going after configuring the URL.

There is a way to get Azure Static Webapps and App Service to play nice with each other that I want to look into. But that’s for another day.

4 Likes