Deploy Backstage.io app on RedHat OpenShift Cluster

Mansura H.
Web Application Development
11 min readNov 25, 2022

Backstage.io is a widely popular application framework. Today I am going to deploy a backstage application to the RedHat OpenShift Cluster.

Prerequisite

1. Nodejs

Let's check the Nodejs version on the development workstation by running the following command on the terminal.

node --version

For this exercise, I used “v16.15.1” as the version for Nodejs

2. React

3. RedHat OpenShift Cluster

For this task, I have used a RedHat OpenShift 4.10 Cluster on IBM Cloud with a Classic deployment pattern.

Step 1: Create the application

The first step is to run the following command to create an application

npx @backstage/create-app

As it prompts for the project name, let's use “backstage-demo” for the project.

Running the backstage/create-app command

This process can take around 5–7 minutes. Let me refill my coffee mug :).

Application creation complete

To verify the installation, let's run the following commands

  1. Install dependencies locally with the following command
yarn install

2. Run the backstage application by running the following command

yarn dev
Run the backstage application locally

3. Go to the URL “http://localhost:3000/” we will see the application running

Step 3: Configure the Application configuration

We can modify the “backstage-demo/app-config.yaml” file to add more components systems and others.

catalog:
import:
entityFilename: catalog-info.yaml
pullRequestBranchName: backstage-integration
rules:
- allow: [Component, System, API, Resource, Location]
locations:
# Local example data, file locations are relative to the backend process, typically `packages/backend`
- type: file
target: ../../examples/entities.yaml

# Local example template
- type: file
target: ../../examples/template/template.yaml
rules:
- allow: [Template]

## Uncomment these lines to add more example data
- type: url
target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all.yaml

## Uncomment these lines to add an example org
- type: url
target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/acme-corp.yaml
rules:
- allow: [User, Group]

For example, as the template.YAML defined a template for the Nodejs application. Here in this template, the user can input data for the execution of the template and then the template scaffold the application setup and pushes that to a git repository.

apiVersion: scaffolder.backstage.io/v1beta3
# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-template
kind: Template
metadata:
name: example-nodejs-template
title: Example Node.js Template
description: An example template for the scaffolder that creates a simple Node.js service
spec:
owner: user:guest
type: service

# These parameters are used to generate the input form in the frontend, and are
# used to gather input data for the execution of the template.
parameters:
- title: Fill in some steps
required:
- name
properties:
name:
title: Name
type: string
description: Unique name of the component
ui:autofocus: true
ui:options:
rows: 5
- title: Choose a location
required:
- repoUrl
properties:
repoUrl:
title: Repository Location
type: string
ui:field: RepoUrlPicker
ui:options:
allowedHosts:
- github.com

# These steps are executed in the scaffolder backend, using data that we gathered
# via the parameters above.
steps:
# Each step executes an action, in this case one templates files into the working directory.
- id: fetch-base
name: Fetch Base
action: fetch:template
input:
url: ./content
values:
name: ${{ parameters.name }}

# This step publishes the contents of the working directory to GitHub.
- id: publish
name: Publish
action: publish:github
input:
allowedHosts: ['github.com']
description: This is ${{ parameters.name }}
repoUrl: ${{ parameters.repoUrl }}

# The final step is to register our new component in the catalog.
- id: register
name: Register
action: catalog:register
input:
repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }}
catalogInfoPath: '/catalog-info.yaml'

# Outputs are displayed to the user after a successful execution of the template.
output:
links:
- title: Repository
url: ${{ steps.publish.output.remoteUrl }}
- title: Open in catalog
icon: catalog
entityRef: ${{ steps.register.output.entityRef }}

This template is defined to allow “Template” in the app-config.yaml file.

# Local example template
- type: file
target: ../../examples/template/template.yaml
rules:
- allow: [Template]

So when we go under the home page and select Template we can see “Example Node.js Template” template.

Template on Home page

A set of data for different kind types of backstage catalogue items can be found here

Step 4: Separate the backend application into a separate project

I prefer to deploy the application as S2I on the Openshift cluster in order to avoid complex deployment and also to separate the backend, and the front end helps to make changes in the backstage templates independently.

4.1 Let's create a folder on the root level of the application called “backstage-demo-backend”

Create a new folder for backend application

4.2 Move back to the project folder with the following command

cd backstage-demo

4.3 Copy backend content to the new folder with the following command

cp -R packages/backend/ ./../backstage-demo-backend/

4.4 Copy the “app-config.yaml” file to the new “backstage-demo-backend” folder

cp app-config.yaml ./../backstage-demo-backend/

4.5 Copy the “examples” folder to the new “backstage-demo-backend” folder

cp -R examples/ ./../backstage-demo-backend/

4.6 Now lest verify if the contents are moved to the destination “backstage-demo-backend” folder as follows

$ cd ./../backstage-demo-backend/$ lsDockerfile  dist   org.yaml  srcREADME.md  entities.yaml  package-lock.json templateapp-config.yaml  node_modules  package.json

4.7 Remove the link with the front-end application in the main project. From the package.json inside “backstage-demo-backend”, we need to remove the “app”:”link:../app”, line.

"dependencies": {
"app": "link:../app",

4.8 Remove the following line from backstage-demo-backend/src/index.ts file

import app from './plugins/app';

Otherwise, we might get the following error

Backend failed to start up Error: Cannot find module 'app/package.json'
Require stack:
- /Users/.../backstage-demo-backend/node_modules/@backstage/backend-common/dist/index.cjs.js
- /Users/.../backstage-demo-backend/dist/main.js
at Function.Module._resolveFilename (node:internal/modules/cjs/loader:933:15)
at Function.resolve (node:internal/modules/cjs/helpers:108:19)
at Object.resolvePackagePath (/Users/mansurah/dapp/backstage-app/backstage-demo-backend/node_modules/@backstage/backend-common/dist/index.cjs.js:1680:35)
at createRouter (/Users/mansurah/dapp/backstage-app/backstage-demo-backend/node_modules/@backstage/plugin-app-backend/dist/index.cjs.js:215:36)
at createPlugin (webpack-internal:///./src/plugins/app.ts:9:93)
at main (webpack-internal:///./src/index.ts:104:233) {
code: 'MODULE_NOT_FOUND',
requireStack: [
'/Users/../backstage-demo-backend/node_modules/@backstage/backend-common/dist/index.cjs.js',
'/Users/.../backstage-demo-backend/dist/main.js'
]
}

4.9 Install dependencies by running the following command

npm install

4.10 Run the application with the following command

npm start

This will run the application on port 7007 as per the application-config.yaml

app:
title: Scaffolded Backstage App
baseUrl: http://localhost:3000

organization:
name: My Company

backend:
# Used for enabling authentication, secret is shared by all backend plugins
# See https://backstage.io/docs/tutorials/backend-to-backend-auth for
# information on the format
# auth:
# keys:
# - secret: ${BACKEND_SECRET}
baseUrl: http://localhost:7007
listen:
port: 7007
# Uncomment the following host directive to bind to all IPv4 interfaces and
# not just the baseUrl hostname.
# host: 0.0.0.0
csp:
connect-src: ["'self'", 'http:', 'https:']
# Content-Security-Policy directives follow the Helmet format: https://helmetjs.github.io/#reference
# Default Helmet Content-Security-Policy values can be removed by setting the key to false
cors:
origin: http://localhost:3000
methods: [GET, HEAD, PATCH, POST, PUT, DELETE]
credentials: true
# This is for local development only, it is not recommended to use this in production
# The production database configuration is stored in app-config.production.yaml
database:
client: better-sqlite3
connection: ':memory:'
cache:
store: memory
# workingDirectory: /tmp # Use this to configure a working directory for the scaffolder, defaults to the OS temp-dir

integrations:
github:
- host: github.com
# This is a Personal Access Token or PAT from GitHub. You can find out how to generate this token, and more information
# about setting up the GitHub integration here: https://backstage.io/docs/getting-started/configuration#setting-up-a-github-integration
token: ${GITHUB_TOKEN}
### Example for how to add your GitHub Enterprise instance using the API:
# - host: ghe.example.net
# apiBaseUrl: https://ghe.example.net/api/v3
# token: ${GHE_TOKEN}

proxy:
'/test':
target: 'https://example.com'
changeOrigin: true

# Reference documentation http://backstage.io/docs/features/techdocs/configuration
# Note: After experimenting with basic setup, use CI/CD to generate docs
# and an external cloud storage when deploying TechDocs for production use-case.
# https://backstage.io/docs/features/techdocs/how-to-guides#how-to-migrate-from-techdocs-basic-to-recommended-deployment-approach
techdocs:
builder: 'local' # Alternatives - 'external'
generator:
runIn: 'docker' # Alternatives - 'local'
publisher:
type: 'local' # Alternatives - 'googleGcs' or 'awsS3'. Read documentation for using alternatives.

auth:
# see https://backstage.io/docs/auth/ to learn about auth providers
providers: {}

scaffolder:
# see https://backstage.io/docs/features/software-templates/configuration for software template options

catalog:
import:
entityFilename: catalog-info.yaml
pullRequestBranchName: backstage-integration
rules:
- allow: [Component, System, API, Resource, Location]
locations:
# Local example data, file locations are relative to the backend process, typically `packages/backend`
- type: file
target: ../../examples/entities.yaml
rules:
- allow: [Component]

# Local example template
- type: file
target: ../../template/template.yaml
rules:
- allow: [Template]

## Uncomment these lines to add more example data
- type: url
target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all.yaml

## Uncomment these lines to add an example org
- type: url
target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/acme-corp.yaml
rules:
- allow: [User, Group]

Run the following API on the browser, and it will get the result.

http://localhost:7007/api/catalog/entities?offset=0&limit=500

The backend application can be found on GitHub.

Step 5: Separate the frontend application into a separate project

5.1 Let’s create a folder on the root level of the application called “backstage-demo-frontend” by running the following command

mkdir backstage-demo-frontend

5.2 Move back to the project folder with the following command

cd backstage-demo

4.3 Copy frontend content to the new folder with the following command

cp -R packages/app/ ./../backstage-demo-frontend/

5.4 Copy the “app-config.yaml” file to the new “backstage-demo-frontend” folder

cp app-config.yaml ./../backstage-demo-frontend/

5.5 Copy the “yarn.lock” file to the new “backstage-demo-frontend” folder

cp yarn.lock ./../backstage-demo-frontend/

5.6 Now lest verify if the contents are moved to the destination “backstage-demo-frontend” folder as follows

$ cd ./../backstage-demo-frontend/
$ ls

app-config.yaml node_modules public
cypress package-lock.json src
cypress.json package.json yarn.lock

5.7 Add the following dependencies in the package.json

{
"name": "app",
... ... ... ....
"backstage": {
"role": "frontend"
},
"dependencies": {
... ... ... ... ...
"react-hot-loader":"^4.13.0",
"typescript": "~4.6.4",
"@backstage/test-utils": "^1.1.1",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^26.0.7",
"@types/node": "^14.14.32",
"@types/react-dom": "*",
"cross-env": "^7.0.0",
"cypress": "^9.7.0",
"eslint-plugin-cypress": "^2.10.3",
"start-server-and-test": "^1.10.11"
},
... ... ... ... ...
}

5.8 Install dependencies by running the following command

npm install

5.9 Run the application with the following command

npm start

This will run the application on port 3000 as per the application-config.yaml

5.10 Go to the browser and run http://localhost:3000/ . The front end application will be up and running (make sure the backend application is running on localhost:7007).

The backend application can be found on GitHub.

Step 6: Deploy the postgresql database on OpenShift using the operator or YAML file

  • Select Database from the Add page on Openshift Cluster
Add Database
  • Click on the “Instantiate Template” Button
Create Postgres Database from OpenShift operator

Input the username, password and database name

Once the database pod is ready, go to the terminal of the pod and run the following command

psql
Start postgres command

Run each of the following commands one after one. This will give the new user admin to all required databases.

CREATE DATABASE backstage_plugin_catalog;
GRANT ALL PRIVILEGES ON DATABASE backstage_plugin_catalog TO admin ;

CREATE DATABASE backstage_plugin_scaffolder;
GRANT ALL PRIVILEGES ON DATABASE backstage_plugin_scaffolder TO admin;

CREATE DATABASE backstage_plugin_auth;
GRANT ALL PRIVILEGES ON DATABASE backstage_plugin_auth TO admin;

CREATE DATABASE backstage_plugin_search;
GRANT ALL PRIVILEGES ON DATABASE backstage_plugin_search TO admin;

Finally, verify the access for the admin user by running the following command

\l

This will show the access permission for the admin user for all databases.

                                         List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges
-----------------------------+----------+----------+------------+------------+-----------------------
backstage_plugin_auth | postgres | UTF8 | en_US.utf8 | en_US.utf8 | =Tc/postgres +
| | | | | postgres=CTc/postgres+
| | | | | admin=CTc/postgres
backstage_plugin_catalog | postgres | UTF8 | en_US.utf8 | en_US.utf8 | =Tc/postgres +
| | | | | postgres=CTc/postgres+
| | | | | admin=CTc/postgres
backstage_plugin_scaffolder | postgres | UTF8 | en_US.utf8 | en_US.utf8 | =Tc/postgres +
| | | | | postgres=CTc/postgres+
| | | | | admin=CTc/postgres
backstage_plugin_search | postgres | UTF8 | en_US.utf8 | en_US.utf8 | =Tc/postgres +
| | | | | postgres=CTc/postgres+

Step 7: Create secret

For the database and backend application, we need to create the following secret.

  • In the Admin perspective of the OpenShift cluster, go to Secret ˘ and Select “Key/value secret”

Add key-value pair for the following parameters and hit Create button.

This will create a secret with the name “postgresql”.

As we will use S2I to deploy the application directly from Github, we will create a GitHub secret as well. Use the following YAML file to create the GitHub secret. The username can be blank, and the password would be the personal access token in GitHub.

kind: Secret
apiVersion: v1
metadata:
name: secret-github
namespace: <project name for the apps to be deployed>
data:
password: <personal access token>
username: ''
type: kubernetes.io/basic-auth

Step 8: Deploy the application on OpenShift

Use S2I to deploy the front-end and back-end applications to OpenShift Cluster.

Backend application update

Once the front-end application is deployed and a route is created, change the “backend” properties in the backstage-demo-backend/app-config.yaml file as follows. The origin for cors will be the new route URL of the front-end application.

backend:
# Used for enabling authentication, secret is shared by all backend plugins
# See https://backstage.io/docs/tutorials/backend-to-backend-auth for
# information on the format
# auth:
# keys:
# - secret: ${BACKEND_SECRET}
baseUrl: http://0.0.0.0:8080
listen:
port: 8080
# Uncomment the following host directive to bind to all IPv4 interfaces and
# not just the baseUrl hostname.
host: 0.0.0.0
cors:
origin: <the url of the front end>
methods: [GET, POST, PUT, DELETE]
# credentials: true
csp:
connect-src: ["'self'", 'http:', 'https:']
# Content-Security-Policy directives follow the Helmet format: https://helmetjs.github.io/#reference
# Default Helmet Content-Security-Policy values can be removed by setting the key to false
database:
client: pg
connection:
host: ${POSTGRES_HOST}
ensureExists: false
pluginDivisonMode: 'schema'
port: ${POSTGRES_PORT}
user: ${POSTGRES_USER}
password: ${POSTGRES_PASSWORD}

Here is an example app-config.yaml file.

Apply S2I to deploy the backstage-demo-backend application from GitHub to OpenShift Cluster. Once the backstage-demo-backend application is deployed, go to the Deployment resource in the Openshift cluster and add the following Environment Variables.

Here POSTGRES_HOST is the Cluster IP of the Postgres database service deployed at step 6.

Once the build is complete for both applications is complete, we will see the Postgres database, backed and the front end (uI) apps are up and running in the cluster, voila!

--

--

Mansura H.
Web Application Development

Platform Architect @ IBM; Author of Hybrid Cloud Infrastructure and Operations Explained ..