Deploying to Microsoft Azure

Introduction - IN PROGRESS

We are currently utilising Redwood as the tooling to build out a Startup software offer. We have chosen Microsoft Azure as our cloud provider.
Redwood offers multiple cloud options, however, Azure hasn’t made the list of supported solutions yet.

This guide will take you through how we have achieved a functional environment within Microsoft Azure.

Services

Within our current environment, we are utilising the following Azure Services:

  • Azure App Service (Application Service)
  • Azure Static Web App (Static Web App Service)
  • Azure Database for PostgreSQL flexible server (PostgreSQL Service)

We are also using the following services for CI/CD

  • Github
  • Github Actions
  • Github Container Registry (ghcr.io)

Application Service Configuration - API Side

1. Docker

As Redwood doesn’t support Azure Functions at the time of development, we needed to containerise our API side of the application.

The docker file performs the following actions

  1. Uses Node 16 Alpine.
  2. Sets the DATABASE_URL and SESSION_SECRET environmental variables.
  3. Creates an app directory (/app).
  4. Copies API, .nvmrc, graphql.config.js, package.json, redwood.toml & yarn.lock into app directory.
  5. Runs yarn install.
  6. Adds React, React-DOM to work around some issues seen within the build process.
  7. Runs the API build.
  8. Runs any outstanding migrations on the PostgreSQL.
  9. Removes the non-required app/api/src folder.
  10. Exposes 8911 and runs the API.

Dockerfile:

FROM node:16-alpine

ENV DATABASE_URL=
ENV SESSION_SECRET=

WORKDIR /app

COPY api api
COPY .nvmrc .
COPY graphql.config.js .
COPY package.json .
COPY redwood.toml .
COPY yarn.lock .

# RUN yarn install --frozen-lockfile
RUN yarn install
RUN yarn add react react-dom --ignore-workspace-root-check
RUN yarn rw build api
RUN yarn rw data-migrate up
RUN rm -rf ./api/src

WORKDIR /app/api
# WEBSITES_PORT=8911 in Azure Web Apps
EXPOSE 8911

ENTRYPOINT [ "yarn", "rw", "serve", "api", "--port", "8911", "--rootPath", "/api" ]

2. Github Actions

The purpose of the Github Action is to create an updated version of the docker image, it then pushes the image to the repositories Github Container Registry.

The below code performs the following operations:

name: Build & Push Docker Image

on:
  push:
    branches:
      - master

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: your_image_name
  DATABASE_URL: ${{ secrets.DATABASE_URL }}

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v3

      - name: Login to the container registry
        uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract tags & labels for Docker
        id: meta
        uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

      - name: Build and push image
        uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
        with:
          context: . # Searches the root of the repo for the dockerfile
          push: true
          tags: ghcr.io/{GITHUB_ORGANISATION_NAME}/{IMAGE_NAME}:latest
          labels: ${{ steps.meta.outputs.labels }}

3. Github Container Registry

Github container registry will store the image privately (if your main code repo is private).
There is no need to change anything in GHCR.

4. Azure App Service

Within your Azure App Service configuration

Static Web App Service - Web Side

PostgreSQL Service - Database

2 Likes

Thanks for the nice writeup Tom! We appreciate it!

@talk2MeGooseman also went through an Azure deploy adventure a few months back. Pinging him for the shared interest.

I wonder if it would be appropriate to, at some point, turn this into a how-to on the docs. I’m not sure. We did have self hosting as a how to, so it would fit in line with that one. It is great to be here too, and we can point people here who ask. Honestly, I thought Goose had put a short writeup together, but maybe not. The effort you are putting into formatting here is nice. Would it age well with azure, or are changes coming soon that would make some of this go away? I don’t know the ecosystem and am just thinking out loud.

Thanks again, and take care. B.

@PantheRedEye Here is the guide I made using a different deployment flow for App Service Deploy to Azure Static Web Apps - #14 by talk2MeGooseman.

My guide is purely focused on App Service and using the bare minimum flow for people that need to bootstrap an app deployment and not have to worry about other Docker, image building, or container registries. It would be awesome for people to have their choice of deployment process they would like to use depending on their knowledge level.

Before I attempt this, has this change in the last year :stuck_out_tongue:

@EverydayTinkerer, we recently migrated our deployment from AWS to Azure, following the guides provided by @tomdickson and Erik Guzman. Here is our current deployment code. I hope this is helpful for you and the community.

App Service

Dockerfile

FROM node:18-alpine

ARG DATABASE_URL

RUN apk update && apk add bash

RUN apk add --no-cache git openssh

WORKDIR /app

COPY api api

COPY .nvmrc .

COPY graphql.config.js .

COPY package.json .

COPY redwood.toml .

COPY yarn.lock .

COPY scripts scripts

RUN yarn install

RUN yarn rw prisma migrate deploy

RUN yarn rw prisma generate

RUN npx @redwoodjs/cli-data-migrate

RUN yarn rw build api

RUN echo "yarn rw build api done"

RUN rm -rf ./api/src

WORKDIR /app/api

EXPOSE 8911

ENTRYPOINT [ "yarn", "rw", "serve", "api", "--port", "8911" ]

Github Action

name: PROD Deploy backend to azure container app

on:
  push:
    tags:
      - v**

jobs:
  build:
    runs-on: ubuntu-latest-md
    concurrency: prod-backend-deployment

    steps:
      - uses: actions/checkout@v4

      - uses: azure/docker-login@v1
        with:
          login-server: example.azurecr.io
          username: ${{ secrets.USERNAME_AZURE_CONTAINER_REGISTRY }}
          password: ${{ secrets.PASSWORD_AZURE_CONTAINER_REGISTRY }}

      - run: |
          docker build \
            --build-arg "DATABASE_URL=${{ secrets.DIRECT_DB_URL }}" \
            . -t example.azurecr.io/example:${{ github.sha }}
          docker push example.azurecr.io/example:${{ github.sha }}

      - name: Log into Azure CLI with service principal
        uses: Azure/login@v1.4.6
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}

      - name: Authenticate with Azure and add environment variables
        uses: azure/login@v1
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}
      - uses: azure/appservice-settings@v1
        with:
          app-name: 'example'
          app-settings-json: '[
            { "name": "WEBSITES_PORT", "value": "8911" },
            { "name": "WEBSITE_WEBDEPLOY_USE_SCM", "value": "true" },
            { "name": "API_URL", "value": "https://example.azurewebsites.net" },
            { "name": "BASE_URL", "value": "https://example.app" },
            { "name": "DATABASE_URL", "value": "${{ secrets.DB_URL }}" },
            { "name": "LOG_LEVEL", "value": "info"}]'
        id: settings

      - name: Docker Login to Azure Container Registry
        uses: azure/docker-login@v1
        with:
          login-server: example.azurecr.io
          username: ${{ secrets.USERNAME_AZURE_CONTAINER_REGISTRY }}
          password: ${{ secrets.PASSWORD_AZURE_CONTAINER_REGISTRY }}
      - uses: azure/webapps-deploy@v2
        with:
          app-name: 'example'
          images: 'example.azurecr.io/example:${{ github.sha }}'

Static Web app

name: PROD Deploy frontend to azure static web app

on:
  push:
    tags:
      - v**

jobs:
  build_and_deploy_frontend:
    runs-on: ubuntu-latest
    name: Build and Deploy Job
    environment:
      name: production-frontend
      url: https://example.app
    concurrency: production-frontend-deployment

    steps:
      - uses: actions/checkout@v4
        with:
          submodules: true
      - name: build and deploy
        id: builddeploy
        env: # Add environment variables here
          API_URL: https://example.azurewebsites.net
          BASE_URL: https://example.app
        uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: ${{ secrets.STATIC_WEB_APP_TOKEN }}
          repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
          action: 'upload'
          ###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
          # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
          app_location: '/' # App source code path
          # api_location: 'api/dist' # Api source code path - optional
          output_location: 'web/dist' # Built app content directory - optional
          app_build_command: yarn rw build web
          ###### End of Repository/Build Configurations ######

staticwebapp.config.json

{
  "navigationFallback": {
    "rewrite": "/index.html",
    "exclude": ["*.{svg,png,jpg,gif}", "*.{css,scss}", "*.js"]
  },
  "routes": [
    {
      "route": "/data/info.json",
      "headers": {
        "cache-control": "no-store"
      }
    }
  ]
}

1 Like