Building Your Developer Portal with Backstage
0 Background
"DevOps is dead; long live platform engineering."
"Developer portal."
"Backstage."
In 2023, we started seeing these terms more often in DevOps. While I wouldn't go so far as to say that DevOps is dead, Backstage is undoubtedly becoming more popular.
If you haven't heard of Backstage yet, you can give it a quick look here. Here's my brief summary for those TL;DR guys:
Backstage isn't a "developer portal" but a tool to build your developer portal. Think of "create-react-app" V.S., the actual react app you are creating with it.
Backstage has a React frontend and a Node.js backend.
Backstage extends its capabilities by plugins, and installing plugins requires changing some simple TSX code, hence a bit challenging to use.
Some plugin documentation isn't detailed enough, which increases the difficulty if you want to build many features around it.
Enough said; today, we will start from scratch and build a developer portal ourselves. After this tutorial, you can scaffold a new repository using templates, check the CI/CD status, see the deployment status, and view the documentation in the same place. And there are more features.
Without further adieu, let's get started.
1 Prerequisites
A Unix-based operating system. For example, you can run this on macOS, Linux, or Windows Subsystem for Linux (WSL).
curl, git, Docker
Node.js, yarn
For a DevOps engineer, you probably have already gotten most of them on your laptop, maybe with the exception being Node.js and yarn. So, let's talk about these two slightly more:
It's recommended to use nvm to install Node.js. First, install nvm by running:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
Then we run the following command to install the latest LTS version of Node.js:
nvm install node
Yarn is a dependency management tool for Node.js, which can be installed by running:
npm install --global yarn
2 Create Portal Code
OK, now that the dependencies are cleared out of our way, let's create the code base of our developer portal by running only one command:
$ npx @backstage/create-app@latest
? Enter a name for the app [required] my-developer-portal
Creating the app...
...
This runs the backstage/create-app package directly, which generates the default developer portal code for us, and the generated directory is named "my-developer-portal".
Again, given not all DevOps engineers are familiar with Node.js or TSX, let's do a quick walkthrough of the generated code base:
The frontend source code is at
packages/app/src/
.The main file is
packages/pp/src/App.tsx
. When installing a plugin, sometimes we need to change the source code, and this file is highly subjective to change.The main software catalog page is at
packages/app/src/components/catalog/EntityPage.tsx
. If you want to add more tabs and stuff for the detail page of a component, this is the place to look.The backend source code is at
packages/backend/src/
.Since most features are implemented by plugins, the plugins' code is at
packages/backend/src/plugins/
. For example, put the backend source code here to create another plugin.The main config file is at
app-config.yaml
, and the local environment configuration (not for production) is atapp-config.local.yaml
.
OK, with the knowledge above, let's move on to other dependencies.
3 Create the Database Used by Our Developer Portal
Some configurations and stuff of our developer portal are stored in a Postgres DB, so let's prepare and configure it.
First, let's install Postgres locally using brew
:
brew install postgresql@14
brew services start postgresql@14
Create a user used by our portal:
psql postgres
create user backstage with encrypted password 'backstage';
alter role backstage with superuser;
Note that this is only for local development and not for production (which explains why we are making the role a superuser).
Then, let's add the PostgreSQL client to our portal. From the root directory of the folder "my-developer-portal", run:
yarn add --cwd packages/backend pg
Next, let's configure our portal to use this DB we created. In the file app-config.yaml
, change the "backend" section to the following:
backend:
database:
client: pg
connection:
host: 127.0.0.1
port: 5432
user: "backstage"
password: "backstage"
OK, after everything, once we start our portal by running:
yarn dev
Some databases will be created automatically, and our portal will use the DB as the persistent storage.
4 Single Sign On
Next, we want to use GitHub for single sign-on so that there is no need to register and manage users, which is one less overhead for us to worry about.
Go to https://github.com/settings/applications/new to create your OAuth App. The Homepage URL should point to Backstage's frontend; in our tutorial, it would be http://localhost:3000. The Authorization callback URL is http://localhost:7007/api/auth/github/handler/frame.
Generate a client secret, copy the client id/secret, and configure them accordingly in the config file "app-config.yaml" in the "auth" section:
auth:
environment: development
providers:
github:
development:
clientId: <YOUR CLIENT ID HERE>
clientSecret: <YOUR CLIENT SECRET HERE>
Backstage then will read the configuration, and we need to update our frontend so that we can log in via GitHub SSO:
Open packages/app/src/App.tsx
, and below the last import line, add:
import { githubAuthApiRef } from "@backstage/core-plugin-api";
import { SignInPage } from "@backstage/core-components";
Search for const app = createApp({
in this file, and below apis
, add:
components: {
SignInPage: props => (
<SignInPage
{...props}
auto
provider={{
id: 'github-auth-provider',
title: 'GitHub',
message: 'Sign in using GitHub',
apiRef: githubAuthApiRef,
}}
/>
),
},
OK, now we can log in to our developer portal using GitHub single sign-on!
5 GitHub Integration
The GitHub integration will allow us to load catalog entities from GitHub and create repositories using Backstage templates.
Create a personal access token here, select repo and workflow for scope (enough for this tutorial), copy the token, and configure it in the app-config.local.yaml
file:
integrations:
github:
- host: github.com
token: <YOUR GITHUB PERSONAL ACCESS TOKEN HERE>
After this, let's restart our portal by pressing CTRL+C in the terminal where we ran yarn dev
previously and run yarn dev
again.
6 Mid-Way Test
Before we run some tests to see if our setup is correct, let's configure our portal to allow importing templates using URLs.
In file app-config.yaml
, for the "catalog" section, let's add "Template" as part of the "rules":
catalog:
import:
entityFilename: catalog-info.yaml
pullRequestBranchName: backstage-integration
rules:
- allow: [Component, System, API, Resource, Location, Template]
...
OK, we are all set, and let's test:
Go to http://localhost:3000, and we should be able to log in to our portal using GitHub single sign-on.
Click "Create" -> "Register New", and put the URL: "https://github.com/IronCore864/backstage-k8s-github-template/blob/main/template.yaml" (which is a template I created for this tutorial), and we should be able to import a template to our software catalog.
You can see the template when you click "Create" again. Use the newly added template to create a repository named "my-kubernetes-component" You will have it created in your GitHub account!
See the created sample here: https://github.com/IronCore864/my-kubernetes-component.
The created repository has GitHub Actions as continuous integration, and it should run successfully. Go to the software catalog in the developer portal, and click on the newly created component under the CI/CD tab. You should see the CI results in our developer portal directly, which means our GitHub integration is working all right.
The created repository also contains simple documentation created by mkdocs, and under the Docs tab, you should see it rendered and displayed correctly.
7 Mid-Way Summary
So far, our developer portal has achieved the following functionalities:
software catalog: a centralized place for all your software
repository/service scaffolding: creating a new repository using templates
CI: show the status of the CI workflows
Docs: show the documentation of our service
We want to add more value to our portal. Specifically, we want to add continuous deployment using Argo CD and see the deployed pods in Kubernetes clusters.
So, let's do that.
8 Adding Argo CD
8.1 Prepare Our Argo CD
We will use minikube to create a local Kubernetes cluster and install Argo CD:
Install minikube, make sure docker is up and running, then run:
minikube start --driver=docker
This creates a local Kubernetes cluster inside a Docker container.
Then, install Argo CD by running:
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
This creates the "argocd" namespace and installs Argo CD there.
In another window, run:
kubectl port-forward svc/argocd-server -n argocd 8080:443
This forwards the Argo CD server to our local 8080 port so that we can access it.
Let's initialize the password for the admin user:
argocd admin initial-password
argocd login localhost:8080
argocd account update-password
Create an Argo CD project role in the "default" project and an API token:
argocd proj role create default backstage
argocd proj role create default backstage
argocd proj role create-token default backstage
Store the token temporarily somewhere, which will be used in the next section for our portal-ArgoCD integration.
Then enable Argo CD role-based access control (RBAC):
kubectl edit configmap argocd-rbac-cm
Add the "data" part using the following content:
data:
policy.csv: |
p, role:org-admin, applications, *, */*, allow
p, role:org-admin, clusters, get, *, allow
p, role:org-admin, repositories, get, *, allow
p, role:org-admin, repositories, create, *, allow
p, role:org-admin, repositories, update, *, allow
p, role:org-admin, repositories, delete, *, allow
p, role:backstage, applications, *, */*, allow
policy.default: role:readonly
This enables RBAC in our Argo CD and grants permission to the newly created role "backstage".
8.2 Argo CD Integration
Install the plugin into Backstage.
cd packages/app
yarn add @roadiehq/backstage-plugin-argo-cd
In the config app-config.yaml
, in the section proxy
, add the following:
proxy:
"/argocd/api":
target: https://localhost:8080/api/v1/
changeOrigin: true
secure: false
headers:
Cookie:
$env: ARGOCD_AUTH_TOKEN
Note: copy and paste the above section entirely without changing the last line. Note that the last line (keep ARGOCD_AUTH_TOKEN as it is without pasting the actual token there).
Then, let's add Argo CD to our portal's UI. In file packages/app/src/components/catalog/EntityPage.tsx
, add the import:
import {
EntityArgoCDOverviewCard,
EntityArgoCDHistoryCard,
isArgocdAvailable,
} from "@roadiehq/backstage-plugin-argo-cd";
In the same file, search for const overviewContent
and update the content as follows:
const overviewContent = (
<Grid container spacing={3} alignItems="stretch">
...
<EntitySwitch>
<EntitySwitch.Case if={(e) => Boolean(isArgocdAvailable(e))}>
<Grid item sm={4}>
<EntityArgoCDOverviewCard />
</Grid>
<Grid item sm={4}>
<EntityArgoCDHistoryCard />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
...
</Grid>
);
If you read some TSX code, you will know that this adds two cards in the overview tab.
Stop our portal, export ARGOCD_AUTH_TOKEN="argocd.token=YOUR_ARGOCD_ROLE_API_TOKEN_HERE
, then restart the portal.
8.3 Deploy an App and Test the Result
Now that Argo CD integration is done, let's test it by deploying some pods which belong to the "my-kubernetes-component" we created earlier in section 6.
You can do this via Argo CD UI, but to make it easier to reproduce the exact same result, prepare a file application.yaml
:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-kubernetes-component
namespace: argocd
spec:
destination:
namespace: default
server: https://kubernetes.default.svc
project: default
source:
helm:
valueFiles:
- values.yaml
path: go-hello-http
repoURL: https://github.com/IronCore864/gitops-argocd
targetRevision: HEAD
syncPolicy:
automated: {}
And apply it:
kubectl apply -f application.yaml
This deploys a hello-world pod linked to our "my-kubernetes-component" using annotations.
If we go to our component in our portal, under the overview tab, we shall see the latest Argo CD deploy status and Argo CD history.
9 Kubernetes Integration
Now that we can see the continuous deployment's status, how about the actual resources of the deployment? To have that in our portal, we need to integrate Kubernetes.
9.1 Add Kubernetes Plugins
First, let's add the Kubernetes frontend/backend plugins:
# from your my-developer-portal root directory
yarn add --cwd packages/app @backstage/plugin-kubernetes
yarn add --cwd packages/backend @backstage/plugin-kubernetes-backend
In packages/app/src/components/catalog/EntityPage.tsx
, add the following code:
import { EntityKubernetesContent } from '@backstage/plugin-kubernetes';
// You can add the tab to any number of pages, the service page is shown as an
// example here
const serviceEntityPage = (
<EntityLayout>
{/* other tabs... */}
<EntityLayout.Route path="/kubernetes" title="Kubernetes">
<EntityKubernetesContent refreshIntervalMs={30000} />
</EntityLayout.Route>
Create a file called kubernetes.ts
inside packages/backend/src/plugins/
and add the following (code for the Kubernetes plugin):
import { KubernetesBuilder } from '@backstage/plugin-kubernetes-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
import { CatalogClient } from '@backstage/catalog-client';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
const catalogApi = new CatalogClient({ discoveryApi: env.discovery });
const { router } = await KubernetesBuilder.createBuilder({
logger: env.logger,
config: env.config,
catalogApi,
}).build();
return router;
}
Then in packages/backend/src/index.ts
, import the plugin:
import kubernetes from './plugins/kubernetes';
// ...
async function main() {
// ...
const kubernetesEnv = useHotMemoize(module, () => createEnv('kubernetes'));
// ...
apiRouter.use('/kubernetes', await kubernetes(kubernetesEnv));
That's it! The Kubernetes frontend and backend have now been added to your Backstage app.
9.2 Configure Kubernetes
We will use a service account (the "default" service account in the "default" namespace) for our portal to access Kubernetes resources.
First, let's give it cluster admin. Create a file named crb.yaml
:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: default-cluster-admin
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: default
namespace: default
And apply it:
kubectl apply -f crb.yaml
And we need a token for this service account. We need to do this manually because:
Versions of Kubernetes before v1.22 automatically created long-term credentials for accessing the Kubernetes API. This older mechanism was based on creating token Secrets that could then be mounted into running Pods. In more recent versions, including Kubernetes v1.26, API credentials are obtained directly using the TokenRequest API and are mounted into Pods using a projected volume. The tokens obtained using this method have bounded lifetimes and are automatically invalidated when the Pod they are mounted into is deleted.
You can still manually create a service account token Secret; for example, if you need a token that never expires. However, using the TokenRequest subresource to obtain a token to access the API is recommended instead.
To create the token, run:
kubectl apply -f - <<EOF
apiVersion: v1
kind: Secret
metadata:
name: default-secret
annotations:
kubernetes.io/service-account.name: default
type: kubernetes.io/service-account-token
EOF
Then run the following to get the base64 decoded value:
kubectl -n default get secret default-secret -o=json | jq -r '.data["token"]' | base64 --decode
The Kubernetes API URL can be obtained by running the following command.
kubectl cluster-info
OK, with the API URL and the token, we can configure our portal to access the minikube Kubernetes cluster. In file app-config.yaml
, add the below section:
kubernetes:
serviceLocatorMethod:
type: multiTenant
clusterLocatorMethods:
- type: config
clusters:
- url: https://127.0.0.1:49671 # replace your API URL value here
name: minikube
authProvider: serviceAccount
skipTLSVerify: true
skipMetricsLookup: true
serviceAccountToken: YOUR_K8S_SERVICE_ACCOUNT_TOKEN_HERE
9.3 Test
Now, let's restart our portal, and in the component "my-kubernetes-component", under the Kubernetes tab, we shall see some pods running.
10 Summary
In this tutorial, we learned how to:
generate the base code of your developer portal using Backstage (and a quick walkthrough of the code base)
install plugins and change frontend/backend.
enable GitHub SSO
integrate with GitHub
integrate with Argo CD
integrate with Kubernetes
Of course, building this portal isn't easy, but at least compared to creating one from the ground up, Backstage saved us a whole lot of time.
11 Where to Go from Here
For interested readers, these are the things you can do:
Customize the entity page by playing with the file
packages/app/src/components/catalog/EntityPage.tsx
to give it a more personal look. For example, you might want to remove the dependency card from the overview tab, or you might want to put those Argo CD cards in a separate tab.Configure the looks of your portal.
Check out other integrations and plugins(https://backstage.io/plugins/).
Heck, you can even decide to create a plugin all by yourself. The only limit is your imagination (and your ability to tweak a bit of TSX code, let's be honest :)
If you like this tutorial, please click like, comment, and subscribe. Your support is my supreme motivation. See you next time.
Subscribe to my newsletter
Read articles from Tiexin Guo directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by