loading...

React/Redux application with Azure DevOps: Part 8 Integrate backend/frontend and CI/CD

kenakamu profile image Kenichiro Nakamura Updated on ・5 min read

In the previous post, I update frontend so that it can interact with backend. There are two applications at the moment, and I need to integrate them into one application.

React and express

React is just another frontend web application framework, thus once transpiled, produced results are static html/js/css files. In that case, express can serve it without any special configuration.

1. First thing first, transpile react into static files.

npm run build

2. Once the process completed, I can see build folder and items are added.
Alt Text

3. Update Server.ts in react-backend/src folder to serve the build folder as static folder. Use it as root, too.

/// Server.ts

import cookieParser from 'cookie-parser';
import express from 'express';
import { Request, Response } from 'express';
import logger from 'morgan';
import path from 'path';
import BaseRouter from './routes';

// Init express
const app = express();

// Add middleware/settings/routes to express.
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({extended: true}));
app.use(cookieParser());
app.use('/api', BaseRouter);

/**
 * Point express to the 'views' directory. If you're using a
 * single-page-application framework like react or angular
 * which has its own development server, you might want to
 * configure this to only serve the index file while in
 * production mode.
 */
const buildDir = path.join(__dirname, '../../build');
app.set('buildDir', buildDir);
const staticDir = path.join(__dirname, '../../build');
app.use(express.static(staticDir));
app.get('*', (req: Request, res: Response) => {
    res.sendFile('index.html', {root: buildDir});
});

// Export express instance
export default app;

4. Run the backend server by start debugging or npm command in react-backend folder.

npm run start:dev

5. Open browser and access to localhost:3001. I can see the application is up and running.
Alt Text

6. Now I confirm how it work. Next I update the package.json to copy the build output into backend src folder so that I can package them together. The "postbuild" section runs after build script.

"scripts": {
  "start": "react-scripts start",
  "build": "react-scripts build",
  "postbuild": "mv build ./react-backend/src",
  "test": "react-scripts test",
  "eject": "react-scripts eject"
},

7. Update Server.ts to point new build folder.

const buildDir = path.join(__dirname, './build');

8. Update util/build.js to copy the build folder into dist when transpile the backend. This is necessary as it clears the folder every time. I also comment out unused folder copy.

const fs = require('fs-extra');
const childProcess = require('child_process');

try {
    // Remove current build
    fs.removeSync('./dist/');
    // Copy front-end files
    fs.copySync('./src/build', './dist/build');
    //fs.copySync('./src/public', './dist/public');
    //fs.copySync('./src/views', './dist/views');
    // Transpile the typescript files
    childProcess.exec('tsc --build tsconfig.prod.json');
} catch (err) {
    console.log(err);
}

9. I deleted all unused code from backend, such as MockDB, UserRoute, Views, etc so that I only have what I need.

Update .gitignore and commit

I didn't update .gitignore after added backend, which affect push to git server. I added following entries.

# backend
/react-backend/node_modules
/react-backend/env
/react-backend/logs
/react-backend/dist
/react-backend/src/build
/react-backend/spec/junitresults*.xml

Then commit the change, but not push yet.

git add .
git commit -m "backend integration"

Pipeline

Before push the changes to the repo, it's time to update build pipeline.
As I have done so many changes, I need to think about how to accommodate the change.

There are several things I need to think.

  • Build order: At the moment, I need to build frontend first, then backend.
  • Environment file: I cannot commit plan file with secure key, that's why I ignore env folder, but backend server needs it.
  • Artifact: I don't need entire files but I just need backend project now.

1. First of all, upload env files to Azure DevOps Secure File where I can sacurly store the file. Go to Azure DevOps | Pipelines | Library | Secure files.

Upload production.env, development.env and test.env which contains environment information.
Alt Text

2. Edit current pipeline. I added/removed/modified several things to accommodate the changes.

  • Download and copy environment files
  • Update trigger so that it won't trigger by pipeline definition change
  • Update build section to test and build backend/frontend
  • Publish test results to cover both backend/frontend
  • Create drop based on react-backend folder
  • Update publish as it's express application rather than react and pass -- --env=development to control the environment settings.
# Node.js React Web App to Linux on Azure
# Build a Node.js React app and deploy it to Azure as a Linux web app.
# Add steps that analyze code, save build artifacts, deploy, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript

trigger:
  branches:
    include:
    - master
  paths:
    exclude:
    - azure-pipelines.yml

variables:

  # Azure Resource Manager connection created during pipeline creation
  azureSubscription: '2e4ad0a4-f9aa-4469-be0d-8c8f03f5eb85'

  # Web app name
  devWebAppName: 'mycatdogvoting-dev'
  prodWebAppName: 'mycatdogvoting'

  # Environment name
  devEnvironmentName: 'Dev'
  prodEnvironmentName: 'Prod'

  # Agent VM image name
  vmImageName: 'ubuntu-latest'

stages:
- stage: Build
  displayName: Build stage
  jobs:  
  - job: Build
    displayName: Build
    pool:
      vmImage: $(vmImageName)

    steps:
    - task: DownloadSecureFile@1
      name: productionEnv
      inputs:
        secureFile: 'production.env'

    - task: DownloadSecureFile@1
      name: developmentEnv
      inputs:
        secureFile: 'development.env'

    - task: DownloadSecureFile@1
      name: testEnv
      inputs:
        secureFile: 'test.env'

    - script: |
        mkdir $(System.DefaultWorkingDirectory)/react-backend/env
        mv $(productionEnv.secureFilePath) $(System.DefaultWorkingDirectory)/react-backend/env
        mv $(developmentEnv.secureFilePath) $(System.DefaultWorkingDirectory)/react-backend/env
        mv $(testEnv.secureFilePath) $(System.DefaultWorkingDirectory)/react-backend/env
      displayName: 'copy env file'
    - task: NodeAndNpmTool@1
      inputs:
        versionSpec: '12.x'

    - script: |
        npm install
        CI=true npm test -- --reporters=jest-junit --reporters=default
        npm run build
      displayName: 'test and build frontend'

    - script: |
        cd react-backend
        npm install
        npm run test
        npm run build
      displayName: 'test and build backend'

    - task: PublishTestResults@2
      inputs:
        testResultsFormat: 'JUnit'
        testResultsFiles: |
          junit.xml
          **/*junit*.xml
        failTaskOnFailedTests: true

    - task: ArchiveFiles@2
      displayName: 'Archive files'
      inputs:
        rootFolderOrFile: '$(Build.SourcesDirectory)/react-backend'
        includeRootFolder: false
        archiveType: zip
        archiveFile: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
        replaceExistingArchive: true

    - upload: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
      artifact: drop

- stage: DeployToDev
  displayName: Deploy to Dev stage
  dependsOn: Build
  condition: succeeded()
  jobs:
  - deployment: Deploy
    displayName: Deploy to Dev
    environment: $(devEnvironmentName)
    pool: 
      vmImage: $(vmImageName)
    strategy:
      runOnce:
        deploy:
          steps:            
          - task: AzureRmWebAppDeployment@4
            displayName: 'Azure App Service Deploy: $(devWebAppName)'
            inputs:
              azureSubscription: $(azureSubscription)
              appType: webAppLinux
              WebAppName: $(devWebAppName)
              packageForLinux: '$(Pipeline.Workspace)/drop/$(Build.BuildId).zip'
              RuntimeStack: 'NODE|12-lts'
              StartupCommand: 'npm run start -- --env=development'

- stage: DeployToProd
  displayName: Deploy to Prod stage
  dependsOn: DeployToDev
  condition: succeeded()
  jobs:
  - deployment: Deploy
    displayName: Deploy to Prod
    environment: $(prodEnvironmentName)
    pool: 
      vmImage: $(vmImageName)
    strategy:
      runOnce:
        deploy:
          steps:            
          - task: AzureRmWebAppDeployment@4
            displayName: 'Azure App Service Deploy: $(prodWebAppName)'
            inputs:
              ConnectionType: 'AzureRM'
              azureSubscription: '$(azureSubscription)'
              appType: 'webAppLinux'
              WebAppName: '$(prodWebAppName)'
              packageForLinux: '$(Pipeline.Workspace)/drop/$(Build.BuildId).zip'
              RuntimeStack: 'NODE|12-lts'
              StartupCommand: 'npm run start'

3. Save the change and confirm it won't trigger the pipeline.

CI/CD

Now it's time to run the pipeline.

1. Make sure to commit all from local. I need to run git pull first to get latest yaml change from the repo.

git add .
git commit -m 'backend added'
git pull
git push

2. The pipeline is triggered. Once all the deployment completed, confirm everything worked as expected.
Alt Text

3. I can confirm the test results for both frontend and backend are published.
Alt Text

4. Application runs as expected on both environment.
Alt Text

I know that both environment points to same Redis Cache, but you got the idea how to use different configuration file, right?

Summary

I merged backend and frontend and run CI/CD successfully. I will take a look for integration in the next article.

Go to next article

Discussion

pic
Editor guide