Skip to main content

Deploying with Kubernetes

Kubernetes is a system for deploying, scaling and managing containerized applications. Backstage is designed to fit this model and run as a stateless application with an external PostgreSQL database.

There are many different tools and patterns for Kubernetes clusters, so the best way to deploy to an existing Kubernetes setup is the same way you deploy everything else.

This guide covers basic Kubernetes definitions needed to get Backstage up and running in a typical cluster. The object definitions might look familiar, since the Backstage software catalog also uses the Kubernetes object format for its entity definition files!

Testing locally

To test out these concepts locally before deploying to a production Kubernetes cluster, first install kubectl, the Kubernetes command-line tool.

Next, install minikube. This creates a single-node Kubernetes cluster on your local machine:

# Assumes Mac + Homebrew; see the minikube site for other installations
$ brew install minikube
$ minikube start

...
Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default.

Now you can run kubectl commands and have changes applied to the minikube cluster. You should be able to see the kube-system Kubernetes pods running:

$ kubectl get pods -A

When you're done with the tutorial, use minikube stop to halt the cluster and free up resources.

Creating a namespace

Deployments in Kubernetes are commonly assigned to their own namespace to isolate services in a multi-tenant environment.

This can be done through kubectl directly:

$ kubectl create namespace backstage
namespace/backstage created

Alternatively, create and apply a Namespace definition:

# kubernetes/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: backstage
$ kubectl apply -f kubernetes/namespace.yaml
namespace/backstage created

Creating the PostgreSQL database

Backstage in production uses PostgreSQL as a database. To isolate the database from Backstage app deployments, we can create a separate Kubernetes deployment for PostgreSQL.

Creating a PostgreSQL secret

First, create a Kubernetes Secret for the PostgreSQL username and password. This will be used by both the PostgreSQL database and Backstage deployments:

# kubernetes/postgres-secrets.yaml
apiVersion: v1
kind: Secret
metadata:
name: postgres-secrets
namespace: backstage
type: Opaque
data:
POSTGRES_USER: YmFja3N0YWdl
POSTGRES_PASSWORD: aHVudGVyMg==

The data in Kubernetes secrets are base64-encoded. The values can be generated on the command line:

$ echo -n "backstage" | base64
YmFja3N0YWdl
Note

Secrets are base64-encoded, but not encrypted. Be sure to enable Encryption at Rest for the cluster. For storing secrets in Git, consider SealedSecrets or other solutions.

The secrets can now be applied to the Kubernetes cluster:

$ kubectl apply -f kubernetes/postgres-secrets.yaml
secret/postgres-secrets created

Creating a PostgreSQL persistent volume

PostgreSQL needs a persistent volume to store data; we'll create one along with a PersistentVolumeClaim. In this case, we're claiming the whole volume - but claims can ask for only part of a volume as well.

# kubernetes/postgres-storage.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: postgres-storage
namespace: backstage
labels:
type: local
spec:
storageClassName: manual
capacity:
storage: 2G
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
hostPath:
path: '/mnt/data'
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-storage-claim
namespace: backstage
spec:
storageClassName: manual
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2G

This file contains definitions for two different kinds, separated by a line with a triple dash. This syntax is helpful if you want to consolidate related Kubernetes definitions in a single file and apply them at the same time.

Note the volume type: local; this creates a volume using local disk on Kubernetes nodes. More likely in a production scenario, you'd want to use a more highly available type of PersistentVolume.

Apply the storage volume and claim to the Kubernetes cluster:

$ kubectl apply -f kubernetes/postgres-storage.yaml
persistentvolume/postgres-storage created
persistentvolumeclaim/postgres-storage-claim created

Creating a PostgreSQL deployment

Now we can create a Kubernetes Deployment descriptor for the PostgreSQL database deployment itself:

# kubernetes/postgres.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
namespace: backstage
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:13.2-alpine
imagePullPolicy: 'IfNotPresent'
ports:
- containerPort: 5432
envFrom:
- secretRef:
name: postgres-secrets
volumeMounts:
- mountPath: /var/lib/postgresql/data
name: postgresdb
subPath: data
volumes:
- name: postgresdb
persistentVolumeClaim:
claimName: postgres-storage-claim

If you're not used to Kubernetes, this is a lot to take in. We're describing a Deployment (one or more instances of an application) that we'd like Kubernetes to know about in the metadata block.

The spec block describes the desired state. Here we've requested Kubernetes create 1 replica (running instance of PostgreSQL), and to create the replica with the given pod template, which again contains Kubernetes metadata and a desired state. The template spec shows one container, created from the published postgres:13.2-alpine Docker image.

Note the envFrom and secretRef - this tells Kubernetes to fill environment variables in the container with values from the Secret we created. We've also referenced the volume created for the deployment, and given it the mount path expected by PostgreSQL.

Apply the PostgreSQL deployment to the Kubernetes cluster:

$ kubectl apply -f kubernetes/postgres.yaml
deployment.apps/postgres created

$ kubectl get pods --namespace=backstage
NAME READY STATUS RESTARTS AGE
postgres-56c86b8bbc-66pt2 1/1 Running 0 21s

Verify the deployment by connecting to the pod:

$ kubectl exec -it --namespace=backstage postgres-56c86b8bbc-66pt2 -- /bin/bash
bash-5.1# psql -U $POSTGRES_USER
psql (13.2)
backstage=# \q
bash-5.1# exit

Creating a PostgreSQL service

The database pod is running, but how does another pod connect to it?

Kubernetes pods are transient - they can be stopped, restarted, or created dynamically. Therefore we don't want to try to connect to pods directly, but rather create a Kubernetes Service. Services keep track of pods and direct traffic to the right place.

The final step for our database is to create the service descriptor:

# kubernetes/postgres-service.yaml
apiVersion: v1
kind: Service
metadata:
name: postgres
namespace: backstage
spec:
selector:
app: postgres
ports:
- port: 5432

Apply the service to the Kubernetes cluster:

$ kubectl apply -f kubernetes/postgres-service.yaml
service/postgres created

$ kubectl get services --namespace=backstage
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
postgres ClusterIP 10.96.5.103 <none> 5432/TCP 29s

Creating the Backstage instance

Now that we have PostgreSQL up and ready to store data, we can create the Backstage instance. This follows similar steps as the PostgreSQL deployment.

Creating a Backstage secret

For any Backstage configuration secrets, such as authorization tokens, we can create a similar Kubernetes Secret as we did for PostgreSQL, remembering to base64 encode the values:

# kubernetes/backstage-secrets.yaml
apiVersion: v1
kind: Secret
metadata:
name: backstage-secrets
namespace: backstage
type: Opaque
data:
GITHUB_TOKEN: VG9rZW5Ub2tlblRva2VuVG9rZW5NYWxrb3ZpY2hUb2tlbg==

Apply the secret to the Kubernetes cluster:

$ kubectl apply -f kubernetes/backstage-secrets.yaml
secret/backstage-secrets created

Creating a Backstage deployment

To create the Backstage deployment, first create a Docker image. We'll use this image to create a Kubernetes deployment. For this example, we'll use the standard host build with the frontend bundled and served from the backend.

First, create a Kubernetes Deployment descriptor:

# kubernetes/backstage.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: backstage
namespace: backstage
spec:
replicas: 1
selector:
matchLabels:
app: backstage
template:
metadata:
labels:
app: backstage
spec:
containers:
- name: backstage
image: backstage:1.0.0
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 7007
envFrom:
- secretRef:
name: postgres-secrets
- secretRef:
name: backstage-secrets
# Uncomment if health checks are enabled in your app:
# https://backstage.io/docs/plugins/observability#health-checks
# readinessProbe:
# httpGet:
# port: 7007
# path: /healthcheck
# livenessProbe:
# httpGet:
# port: 7007
# path: /healthcheck

For production deployments, the image reference will usually be a full URL to a repository on a container registry (for example, ECR on AWS).

For testing locally with minikube, you can point the local Docker daemon to the minikube internal Docker registry and then rebuild the image to install it:

$ eval $(minikube docker-env)
$ yarn build-image --tag backstage:1.0.0

There is no special wiring needed to access the PostgreSQL service. Since it's running on the same cluster, Kubernetes will inject POSTGRES_SERVICE_HOST and POSTGRES_SERVICE_PORT environment variables into our Backstage container. These can be used in the Backstage app-config.yaml along with the secrets. Apply this to app-config.production.yaml as well if you have one:

backend:
database:
client: pg
connection:
host: ${POSTGRES_SERVICE_HOST}
port: ${POSTGRES_SERVICE_PORT}
user: ${POSTGRES_USER}
password: ${POSTGRES_PASSWORD}

Make sure to rebuild the Docker image after applying app-config.yaml changes.

Apply this Deployment to the Kubernetes cluster:

$ kubectl apply -f kubernetes/backstage.yaml
deployment.apps/backstage created

$ kubectl get deployments --namespace=backstage
NAME READY UP-TO-DATE AVAILABLE AGE
backstage 1/1 1 1 1m
postgres 1/1 1 1 10m

$ kubectl get pods --namespace=backstage
NAME READY STATUS RESTARTS AGE
backstage-54bfcd6476-n2jkm 1/1 Running 0 58s
postgres-56c86b8bbc-66pt2 1/1 Running 0 9m

Beautiful! 🎉 The deployment and pod are running in the cluster. If you run into any trouble, check the container logs from the pod:

# -f to tail, <pod> -c <container>
$ kubectl logs --namespace=backstage -f backstage-54bfcd6476-n2jkm -c backstage

Creating a Backstage service

Like the PostgreSQL service above, we need to create a Kubernetes Service for Backstage to handle connecting requests to the correct pods.

Create the Kubernetes Service descriptor:

# kubernetes/backstage-service.yaml
apiVersion: v1
kind: Service
metadata:
name: backstage
namespace: backstage
spec:
selector:
app: backstage
ports:
- name: http
port: 80
targetPort: http

The selector here is telling the Service which pods to target, and the port mapping translates normal HTTP port 80 to the backend http port (7007) on the pod.

Apply this Service to the Kubernetes cluster:

$ kubectl apply -f kubernetes/backstage-service.yaml
service/backstage created

Now we have a fully operational Backstage deployment! 🎉 For a grand reveal, you can forward a local port to the service:

$ sudo kubectl port-forward --namespace=backstage svc/backstage 80:80
Forwarding from 127.0.0.1:80 -> 7007

This shows port 7007 since port-forward doesn't really support services, so it cheats by looking up the first pod for a service and connecting to the mapped pod port.

Note that app.baseUrl and backend.baseUrl in your app-config.yaml should match what we're forwarding here (port omitted in this example since we're using the default HTTP port 80):

# app-config.yaml
app:
baseUrl: http://localhost

organization:
name: Spotify

backend:
baseUrl: http://localhost
listen:
port: 7007
cors:
origin: http://localhost

If you're using an auth provider, it should also have this address configured for the authentication pop-up to work properly.

Now you can open a browser on your machine to localhost and browse your Kubernetes-deployed Backstage instance. 🚢🚢🚢

Further steps

This is most of the way to a full production deployment of Backstage on Kubernetes. There's a few additional steps to that will likely be needed beyond the scope of this document.

Set up a more reliable volume

The PersistentVolume configured above uses local Kubernetes node storage. This should be replaced with a cloud volume, network attached storage, or something more persistent beyond a Kubernetes node.

Expose the Backstage service

The Kubernetes Service is not exposed for external connections from outside the cluster. This is generally done with a Kubernetes ingress or an external load balancer.

Update the Deployment image

To update the Kubernetes deployment to a newly published version of your Backstage Docker image, update the image tag reference in backstage.yaml and then apply the changes with kubectl apply -f kubernetes/backstage.yaml.

For production purposes, this image tag will generally be a full-fledged URL pointing to a container registry where built Docker images are hosted. This can be hosted internally in your infrastructure, or a managed one offered by a cloud provider.