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
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.