Deploy to Azure Static Web Apps

Since Azure Static Web Apps is still in preview, it is free to use for the time being.

Steps on how to deploy a Redwood app through Azure Static Web Apps:

Prerequisite: Azure & GitHub account, VS Code

  1. Install Azure Static Web Apps VS Code extension and follow “Create your first static web app” instruction on the extension page.

  2. When asked for the app folder name, enter “/” (without the quotes)

  3. When asked for the build artifact folder name, enter “web/dist” (without the quotes, take note that there is no slash before the ‘web’)

  4. After created successfully, pull changes from your GitHub repo because Azure will add a new file at the following path .github\workflows\azure-static-web-apps-*<some-random-value>*.yml

  5. Open the .github\workflows\azure-static-web-apps-*<some-random-value>*.yml file above, add the following line:
    app_build_command: yarn rw build
    under this section: jobs > build_and_deploy_job > steps > name > with
    You may refer this sample code or this screenshot :point_down:.

  6. Create a new config file staticwebapp.config.json at the root of the project with the following content:
    image
    The explanation of the code above can be found in the Fallback Routes section of the Azure Static Web Apps Documentation.

  7. Commit and push your code. Go to your GitHub Actions section you shall see 2 workflow runs, the first one failed because it doesn’t have the custom build command yet, the second successful with the custom build command. Sample link

  8. To view the published website, you may get the URL from your Azure portal or from the GitHub Action log > Build And Deploy

I’m still researching how to set up:

  1. API side to use Azure Functions

I will update here if I have any progress. In the meantime, feel free to share your config if you know how to do it. :grin::pray:

Following are some of my doubts:

  1. Why select location in Azure Static Web Apps?
7 Likes

Thanks a lot for sharing @andrew-hh! I started looking at that forever ago and hit a roadblock at the Azure Functions step (and then life happened), I am looking forward to your updates :slight_smile:

2 Likes

Thanks for posting this @andrew-hh! I know we’ve been looking at this for deployment and we appreciate you writing out these steps.

1 Like

:sweat: I need some help in setting up the API side with Azure Functions, it’s too complex for me :cry:

As someone who has gone down this path with a handful for different deployment providers you are definitely NOT the only one who finds this stuff complex.

For a lot of these deployment platforms we are working directly with the people building them and it requires a lot of understanding around both the platform and the framework.

Let us know what your blocker is and we’ll work it out :muscle:.

1 Like

Azure Functions is expecting a different kind of folder structure that isn’t aligned with the dist folder structure which yarn rw deploy produced and I don’t know how to make that happen. :disappointed_relieved:

Cool, I’ll follow along with the steps you’ve got outlined and see how far I get. I have a couple friends at Microsoft I can bug also.

Notes from Thomas:

You could not have asked a better question! My current project has a static site front end and Azure Functions backend.

Here is the repo

My approach so far is to deploy HTTP functions and call to them using code in the “actions” directory of my React code. I’m really taking the Grokking Simplicity book to heart there.

Here it is deployed

But it doesn’t do anything yet.

And I have a bug in the sign in code that causes a 500

Behind the scenes, they use something called Oryx to build. It’s what’s used on all Linux App Service apps (static or otherwise).

It’s open source so you can read through it if you need to, but you can write a config to tell it how to set up your app. It will detect the runtime based on the files and then (for node) run npm build.

It would also run through a similar set of commands for a Python, C#, Java, etc. app

1 Like

Okay, it looks like we’re gonna end up writing a specific yaml file that’ll define the whole build, much like we’re doing with Render. I’ll touch base with Thomas this weekend and we should be able to work it out.

1 Like

A million thanks @ajcwebdev :pray:

In the meantime, I’ve created a pull request to add config for RedwoodJS to the docs of Configure front-end frameworks and libraries with Azure Static Web Apps Preview.

1 Like

Hi @andrew-hh,

I just got off of a call with @ajcwebdev where we tried to replace the Lambda service with Azure Functions. We made a bit of progress, but got stuck on (a) calls being proxied through the host port when running locally and (b) standing up the GraphQL server in an Azure Function.

What you have set up for the frontend is awesome. Super impressed on you getting that up and going.

As for running the service with Azure Functions replacing the Lambdas, it’s definitely possible. I would need a better understanding of how Redwood proxies calls to the API when it’s running locally and then I could work out how to fully replace the API layer.

I’m curious is there is a way to wrap the existing Lambdas so that they can be executed as Azure Functions (I don’t know if this is possible).

If we got a group together to try and work this out, I would be happy to be a part of it. We would definitely need to have a core dev to speed up the process. I have a lot of stupid questions to ask.

Ultimately, if we wanted Redwood to be easily deployable to Azure, we would need to create some alternate API template that would produce Azure Functions instead of Lambdas. It would be useful to gauge interest on that as it’s a sizable investment.

2 Likes

Hi Thomas & @ajcwebdev,

Thanks for taking the time for trying it out.

I wonder if @peterp is able to lend a hand on this.

Hi All! Excited to see people working on this. There’s more under the hood going on, which means this isn’t exactly a plug-n-play. I’m no expert on Azure either. So for now attempting to dump a lot of details to help others connect some dots.

I do not believe this is just a matter of replicating Redwood’s API Build process in a way that organizes functions specific to Azure.

Redwood’s API uses Apollo Server Lambda

If you look at the Redwood API package, you’ll see the GraphQL Server is apollo-server-lamba

Apollo has a distinct package for Azure Functions

Unfortunately, AWS and Azure functions are not equivalent:

I quickly dug into the code for each to verify there’s more going on than just naming:

:frowning_face:

Using Apollo Azure package

I believe the “right” way to do this will be to make the apollo-server swappable in Redwood’s API. As an experiment, one could fork Redwood and patch the API package to use Azure instead of AWS version.

Adding a “Wrapper”?

Maybe another POC path forward could be creating another package like @redwoodjs/api-server, which effectively uses Experess to server Lambda Functions. Take a look at awsLambda.ts to see how Peter does this.

I’m skeptical of this approach (admittedly very naive). Offering it as it might influence other ideas.

Using Azure Function Containers

This is likely the path of least resistance (although I don’t know how it compares to just Azure Functions).

Put the Redwood API in a container using api-server (self-hosting method). Might be a brutal start-up, but viable with tools we have right now.

1 Like

I believe the “right” way to do this will be to make the apollo-server swappable in Redwood’s API. As an experiment, one could fork Redwood and patch the API package to use Azure instead of AWS version.

I agree that this sounds like the way to go.

2 Likes

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

@talk2MeGooseman you’re a legend!!! Thank you so much for the write up, we’ve been waiting on someone to crack the code on Azure for a while. Sounds like there’s a less hacky way to accomplish this that is still in progress, but we appreciate you forging ahead and figuring out something that works.

1 Like

Thanks @talk2MeGooseman. Incredible helpful write up.

@ajcwebdev Do you mean this solution with “less hack way” you are seeing for the future:

That was an idea that isn’t as relevant now since we’re using GraphQL Yoga instead of Apollo Server and there is much better built in support for non-Lambda based runtimes. Right now it makes more sense to provide setup commands to automatically apply the provider specific configuration.

What I meant by a less hacky way would be a Redwood setup command like yarn rw setup deploy azure. This would include most of the boilerplate configuration code that you currently need to write by hand as demonstrated in @talk2MeGooseman’s example.

1 Like

I managed to setup the backend on the azure app service following the setup suggested by @talk2MeGooseman. But app service is a different product than azure static web app which is the vercel competitor. App service is just a deploy to a container … and in my opinion quite buggy.

The goal would be to deploy to a static web app where frontend hosting and backend on an azure function would work together.

I would love to try out to deploy the backend to azure functions. I found this example how to setup GraphQL Yoga on an azure function: graphql-yoga/index.ts at v2 · dotansimha/graphql-yoga · GitHub

Can you point me in the right direction how to connect this with our graphql.ts.

1 Like

@talk2MeGooseman I love this article. However if I follow the steps you did here, I end up getting an error in the selectNodeVersion of Kudu:

Omitting next output lines...
/opt/Kudu/Scripts/selectNodeVersion.js:166
An error has occurred during web site deployment.
    throw new Error('Unable to locate node.js installation directory at ' + nodejsDir);
select node version failed
    ^

Error: Unable to locate node.js installation directory at /opt/nodejs
    at Object.<anonymous> (/opt/Kudu/Scripts/selectNodeVersion.js:166:11)
    at Module._compile (internal/modules/cjs/loader.js:1085:14)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1114:10)
    at Module.load (internal/modules/cjs/loader.js:950:32)
    at Function.Module._load (internal/modules/cjs/loader.js:790:12)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:75:12)
    at internal/main/run_main_module.js:17:47
/opt/Kudu/Scripts/selectNodeVersion.js:166\n    throw new Error('Unable to locate node.js installation directory at ' + nodejsDir);\n    ^\n\nError: Unable to locate node.js installation directory at /opt/nodejs\n    at Object.<anonymous> (/opt/Kudu/Scripts/selectNodeVersion.js:166:11)\n    at Module._compile (internal/modules/cjs/loader.js:1085:14)\n    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1114:10)\n    at Module.load (internal/modules/cjs/loader.js:950:32)\n    at Function.Module._load (internal/modules/cjs/loader.js:790:12)\n    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:75:12)\n    at internal/main/run_main_module.js:17:47\n/opt/Kudu/Scripts/starter.sh bash deploy.sh
Deployment Failed. deployer = GITHUB_ZIP_DEPLOY deploymentPath = ZipDeploy. Extract zip.

It seems as node just can’t be found in the selectNodeVersion helper function in deploy.sh… I tried various things of setting it manually, but no chance.

Have you encountered that before? My Github Action and deploy.sh look exactly like yours. (I even copy pasted to verify no difference :wink:

Thanks so much in advance.

I will take a look, there is a strong possibility the interface has changed and instructions need to be updated.