Backstage by Example (Part 3)
We wrap up this series by exploring the steps required to deploy Backstage to a container runtime.
This is article is part of a series starting with Backstage by Example (Part 1).
Please note: In the interest in keeping this article focused on the key points, it does not call out all the commands required to bring up / down the containers using Docker Compose. Essentially, every time we change the docker-compose.yml file or backstage container image, we will need to bring down the containers and then bring it back up. Also not shown is the commands to update one’s workstation environment variables ever time one updates environment.sh.
Containerization
To keep things simple, we will follow the Backstage instructions on building a single container image that serves both the frontend and backend applications. The instructions amount to running the following commands from the project’s root folder; but we need read further before proceeding.
$ yarn install
$ yarn tsc
$ yarn build
$ yarn build-image
Happens to be as of this writing there is a bug that causes the last command to fail; this is resolved in a PR Update Dockerfile to node:16-bullseye-slim, add sqlite3 dependencies. By the time you read this, this likely will be resolved. The fix amounts to updating the following file; essentially changing the source image and then installing a number of Debian packages.
packages/backend/Dockerfile
Please note: It is a bit unsavory to have to carry around a bunch of Debian packages in our container; but oh well, not much we can do about this.
Also, because the frontend is now served from the container, running on port 7007, we need to change the two references to http://localhost:3000 to http://localhost:7007 in the app.config.yaml.
Also, because we do not want to include secrets in container image so we add both environment.sh and github-app-backstage-mybackstageapp-credentials.yaml to our .dockerignore file.
With these fixes in place, we can now properly build our backstage Docker image.
Next, because we have changed the frontend to http://localhost:7007, we need to update the Homepage URL in the GitHub OAuth App.
With all this in place, we can update our docker-compose.yml file to also run a container running the built backstage image.
Things to observe:
- We no longer need to expose the PostgreSQL database port to our workstation
- We mount the github-app-backstage-mybackstageapp-credentials.yaml file into the container
- Here we pass our workstation environment variables into the container
- Not shown is that we need to change the POSTGRES_HOST in our environment.sh from localhost to db; this is because the backstage container access the PostgreSQL database over the Docker network
After restarting the containers, we browse to http://localhost:7007 and see that we can login to the Backstage App.
Fixing Profile Image
One oddity when running the Backstage App from a container is that our profile image no longer is visible.
Looking at the Browser console, we see the following error:
Refused to load the image 'https://avatars.githubusercontent.com/u/4379100?v=4' because it violates the following Content Security Policy directive: "img-src 'self' data:".
Looking carefully at the HTTP headers of the HTML page served from the backend, we indeed see that a Content-Security-Policy header is the problem.
Content-Security-Policy: default-src 'self';base-uri 'self';block-all-mixed-content;font-src 'self' https: data:;frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self' 'unsafe-eval';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests;connect-src 'self' http: https:
So, what is this all about?
Content Security Policy (CSP) is an added layer of security that helps to detect and mitigate certain types of attacks, including Cross-Site Scripting (XSS) and data injection attacks. These attacks are used for everything from data theft, to site defacement, to malware distribution.
CSP is designed to be fully backward compatible (except CSP version 2 where there are some explicitly-mentioned inconsistencies in backward compatibility; more details here section 1.1). Browsers that don’t support it still work with servers that implement it, and vice-versa: browsers that don’t support CSP ignore it, functioning as usual, defaulting to the standard same-origin policy for web content. If the site doesn’t offer the CSP header, browsers likewise use the standard same-origin policy.
To enable CSP, you need to configure your web server to return the Content-Security-Policy HTTP header. (Sometimes you may see mentions of the X-Content-Security-Policy header, but that’s an older version and you don’t need to specify it anymore.)
— Content Security Policy (CSP)
Luckily, the fix is easy. We update app-config.yaml adding the img-src key shown:
We then run the following command to clean out the previous build; going forward we will want to do this before building the backstage container.
$ yarn clean
We then can rebuild the backstage container image and restart the containers.
Only Using Environment Variables
While most, if not all, container runtimes support environment variables, many do not support mounting files as we did with Docker Compose and the github-app-backstage-mybackstageapp-credentials.yaml file. So, here we convert to only using environment variables.
First we need to update the Dockerfile file; specially changing the CMD line to write the GITHUB_APP_CREDENTIALS environment variable to the github-app-backstage-mybackstageapp-credentials.yaml file before starting the application.
packages/backend/Dockerfile
CMD ["sh", "-c", "echo \"$GITHUB_APP_CREDENTIALS\" > github-app-backstage-mybackstageapp-credentials.yaml & node packages/backend --config app-config.yaml"]
We then add a line in environment.sh to read the contents of the file app-backstage-mybackstageapp-credentials.yaml into the GITHUB_APP_CREDENTIALS environment variable.
We then update docker-compose.yaml by removing the volumes key and adding the GITHUB_APP_CREDENTIALS.
We start the Backstage App and see that is works as expected.
PostgreSQL Connection String
Rather than using the PostgreSQL host, port, user, and password as separate strings, it is more common to use a PostgreSQL Connection String. We make the following changes.
app-config.yaml
database:
# config options: https://node-postgres.com/api/client
client: pg
connection: ${DATABASE_URL}
docker-compose.yml
environment:
- DATABASE_URL=${DATABASE_URL}
- AUTH_GITHUB_CLIENT_ID=${AUTH_GITHUB_CLIENT_ID}
- AUTH_GITHUB_CLIENT_SECRET=${AUTH_GITHUB_CLIENT_SECRET}
- GITHUB_APP_CREDENTIALS=${GITHUB_APP_CREDENTIALS}
environment.sh
export DATABASE_URL=postgresql://postgres:example@db:5432
After sourcing the environment.sh file, we start the Backstage App and see that is works as expected.
Deploying to an Example Cloud Container Runtime (Google Cloud Run)
Now that we have a containerized application there are numerous ways to run our Backstage App in the cloud. Here I chose to use Google Cloud Run.
Cloud Run is a managed compute platform that enables you to run containers that are invocable via requests or events. Cloud Run is serverless: it abstracts away all infrastructure management, so you can focus on what matters most — building great applications.
Here are the changes to our Backstage App that I was required to make:
- Because I was using Cloud Run with Cloud SQL with a Public IP, I had to switch back to using the POSTGRES_HOST, POSTGRES_USER, and POSTGRES_PASSWORD (POSTGRES_PORT is not needed) environment variables because I could not figure out how to supply a PostgreSQL connection string for a Unix domain socket
- Because Cloud Run supplies the environment variable PORT that our application is to run on, I changed the port from 7007 to ${PORT} in app-config.yaml
- Updated the three references of http://localhost:7007 to the Cloud Run Service URL in app-config.yaml, i.e., two baseURL and one origin key
- Updated the home and callback values in the GitHub OAuth App using the Cloud Run Service URL
Wrap Up
While there is plenty of features to explore, e.g, Kubernetes, Software Templates, Search, TechDocs, and Plugins, will wrap up with series as we have accomplished our goal of standing a production-worthy Backstage App.