Preview Environments are a powerful way for users and automation to provide rapid feedback on proposed application changes. Whether you know them by the moniker Ephemeral Environments, Review/Release Apps, or Pull Request Apps, they achieve the same end; providing a low overhead, per change environment where your proposed changes can be evaluated by both people and testing automation.
Developers are constantly making changes, so preview environments must keep pace. To harness the value of preview environments, they should:
Using modern cloud technologies, these objectives can be easily met, as we’ll discuss below.
For our example, we’ll use the ubiquitous Single Page Application (SPA) where static HTML and Javascript files render the user facing application in the browser. SPAs are well suited for preview environments because of their compact deployment size, static nature, and the ease with which a human can readily spot defects. In this specific case, we’ll work with a ReactJs SPA.
At the high level, our preview environment has three parts:
The beauty of SPAs as preview environments is their static nature. Large applications can be compiled down to only a few dozen files, meaning deployment is fast and hosting can be very cost effective. Here we’ll use storage buckets which are a common building block on modern cloud providers, such as AWS S3, Google Cloud Storage, and Azure Blob Storage.
These buckets provide the following useful characteristics:
To access multiple versions of our SPA, we simply place the version into the domain or path.
For example
In the first example, the application name is petstore and the version, from the git hash, is abc1234. Because every new commit will have a different commit hash, we can host many versions using this pattern.
In the second example, the application name and version are in the sub-domain name. This can be achieved by using wildcard domain names, so the only DNS entry used is *.preview.acme.com, allowing our servers to handle all requests for preview environments the same way. When the requests hit our server, then we can parse the sub-domain and send back the proper SPA artifacts.
While the path approach is simpler, many applications assume they are running on the domain root (e.g. foo.com/), which can easily be handled by the sub-domain approach. Additionally, some authentication frameworks, like Auth0, don’t work with the path approach, but do work well with sub-domains.
With React and similar SPA frameworks, the path (or URL) is often dynamic, depending on the browser to properly interpret the path and render the expected page. For example, `https://foo.com/widgets/7` would render the widgets page in your application and display widget number 7, but regardless of the URL requested there is only a single page in this application, index.html.
We must make sure that regardless of the URL requested from the hosting provider, index.html is always the webpage served. For this we will use URL rewriting, where the hosting server will transform the path requested (`/widgets/7`) to index.html, but without doing a redirect, so that the requested path remains the path rendered in the browser, where the SPA will parse the path and render the proper page.
There are a number of ways to handle this path routing, but we’ll use a simple nginx server in the following example code.
Usually an SPA frontend will evolve independently from its backend APIs. This means the preview environment does not need to host the APIs, but rather can point many versions of the application at the same API. In this example, we’ll do just that, sharing the same integration environment backend API across frontend preview environments.
If you need to test multiple backend APIs, one technique is to publish multiple variants of your preview environment artifacts, each with a configuration pointing to a different set of APIs. For example, you can deploy the same set of artifacts to `/dev/<app-name>/<app-hash>`, `/uat/<app-name>/<app-hash>`, and `/dev/<app-name>/<app-hash>`. Now, by visiting different URLs, you can run your SPA against your developer, UAT, or production APIs for testing purposes.
To deploy on Google Cloud Platform, we’ll use the following services:
Data will flow through our preview environments according to the following diagram.
We’ll refer to PROJECT_ID as your GCP project identifier.
First create a new Cloud Storage bucket. This can be easily done with the gsutil command line tool, or from the GCP Web Console (see console instructions).
gsutil mb -p <PROJECT_ID> gs://<BUCKET_NAME>
We now have a bucket that can hold our application artifacts.
We’ll use Cloud Run to host a simple nginx container that will rewrite routes and send them to the proper part of our Cloud Storage bucket. The following gist will build the necessary Dockerfile.
Note: in the gist, replace “YOUR-BUCKET-NAME-HERE” with your GCS bucket name.
Note: in the gist, replace “preview.acme.com” with your testing domain name.
To publish your SPA to GCS, simply use gsutil to copy the files over, the flags mean:
gsutil -m cp -r -Z -a public-read build/ gs://${BUCKET_NAME}/${APP_NAME}/${APP_HASH}/
Where:
Visiting your preview environment at https://<your cloud run service URL>/${APP_NAME}/${APP_HASH}/ will now load the index.html page of your application.
To deploy on Amazon Web Services, we’ll use the following services:
Data will flow through our preview environments according to the following diagram.
First create a new S3 bucket. This can be easily done with the aws command line tool, or from the AWS Web Console (see console instructions).
We’ll use CloudFront plus Lamda@Edge to handle transforming URLs and sending them to the proper location in your S3 bucket.
While we could discuss the details of this setup here, Ed Knowles has done an excellent job in this blog post, going into excellent detail, so we’ll refer you to that excellent description.
Congratulations, you now have highly scalable preview environments! However, you’re not done yet. Now you need to make the best use of your preview environments. You can harness this powerful resource by: