Pull Request perfection: Automating Preview Environments with ease

Sorin Dumitrescu
7 min readJan 3, 2024

--

Preview Environments are dynamic environments, short-lived and disposable, created specifically for a given feature, so:

  • testing can be performed in isolation and in parallel, before emerging
  • feedback can be given by any person within the team.

Part of a larger concept — Environments-as-a-Service — Preview Environments are also called Ephemeral Environments, due to their temporary nature. Usually, a Preview Environment gets created when a Pull Request or branch gets created, and after the Pull Request or branch no longer exists, the environment is automatically deleted.

Preview Environments — illustration

If you want to find out more about the Environments-as-a-Service (or EaaS) concept, you can explore this EaaS introductory article. In short, EaaS helps software development teams easily create environments, so their working paradigm changes for the better, as they can now use short-lived, disposable environments in a reproducible manner.

An environment is created through automation, usually from a blueprint. In this article, we will explore how to do this in Bunnyshell, an EaaS platform. But first, you need to have a few things in place before you can enable Preview Environments using a toggle.

Pre-requisites

  1. A (free) Bunnyshell account.
  2. Your Git account integrated with Bunnyshell, so the platform can use your code to build container images.
  3. A working environment, so preview environments can be created based on it.

ℹ️ The examples shown in this article use GitHub, but Bunnyshell works the same with GitLab, BitBucket and Azure DevOps as well.

Our working environment

Just to keep things simple, we will create an environment based on a template that Bunnyshell provides.

Step 1 — Clone the Git repository so you make it your own

This is the repository you need to clone / fork. You will have both the frontend (frontend) and the backend (api) applications in the repository, as well as the bunnyshell.yaml file, which contains the definition of the environment.

ℹ️ Hint: if you fork the repository, copy all branches, as it will save you some time later on, when creating a Pull Request.

Step 2 — Create an environment in Bunnyshell, using the YAML definition

From the environment listing, click Create environment.

Environment listing in Bunnyshell

Then, from the environment’s page, click Configuration.

Environment details in Bunnyshell, empty environment

Next, paste the environment definition below.

⚠️ Please make sure to replace the gitRepo attribute with your repository, for both api and frontendwith your actual repository. Assuming you forked the repo on GitHub, just replace {MY_USER} with your slug.

kind: Environment
name: Spring Boot / React / MySQL boilerplate
type: primary
components:
-
kind: Application
name: api
gitRepo: 'https://github.com/{MY_USER}/demo-books-springboot-react.git'
gitBranch: main
gitApplicationPath: api
dockerCompose:
build:
context: ./api
dockerfile: .docker/Dockerfile
target: dev
environment:
DATABASE_DBNAME: bunny_books
DATABASE_HOST: db
DATABASE_PASSWORD: need-to-replace
DATABASE_PORT: '3306'
DATABASE_USER: books
FRONTEND_URL: 'https://{{ components.frontend.ingress.hosts[0].hostname }}'
ports:
- '9000:9000'
hosts:
-
hostname: 'api-{{ env.base_domain }}'
path: /
servicePort: 9000
-
kind: Database
name: db
dockerCompose:
command: '--default-authentication-plugin=mysql_native_password'
environment:
MYSQL_DATABASE: bunny_books
MYSQL_PASSWORD: need-to-replace
MYSQL_ROOT_PASSWORD: need-to-replace
MYSQL_USER: books
image: 'mysql:8.0.32'
ports:
- '3306:3306'
volumes:
-
name: db-data
mount: /var/lib/mysql
subPath: ''
-
kind: Application
name: frontend
gitRepo: 'https://github.com/{MY_USER}/demo-books-springboot-react.git'
gitBranch: main
gitApplicationPath: frontend
dockerCompose:
build:
context: ./frontend
dockerfile: .docker/Dockerfile
target: dev
environment:
REACT_APP_BASE_API: 'https://{{ components.api.ingress.hosts[0] }}'
ports:
- '8080:8080'
hosts:
-
hostname: 'app-{{ env.base_domain }}'
path: /
servicePort: 8080
volumes:
-
name: db-data
size: 1Gi
type: disk

Then click Save changes.

Step 3 — Deploy the environment

Click Deploy, from the same Configuration screen. Select Bunnyshell cluster and click Deploy again.

Deploy the environment for the first time

The environment will be deployed now.

Environment details in Bunnyshell, deploying environment

You can also see the deployment logs in real-time, by clicking View Pipeline.

Pipeline logs in Bunnyshell

Step 4 — Check the application runs as expected

Once the environment is deployed, you can open the frontend app by clicking the external link icon near the component name or opening the URLs panel.

Environment details in Bunnyshell for a deployed environment

Test that everything works by adding a book. Or multiple.

Demo application (frontend)

Congratulations! 👏 You have just successfully deployed your first environment.

Enable Preview Environments with 1-click

Once you have a working environment, you only have to enable a toggle in the environment’s settings.

Go to Environment Settings.

Environment details in Bunnyshell; go to Settings

Go to Ephemeral Environments and enable both toggles — to create and also destroy ephemeral environments on Pull Requests.

Environment settings in Bunnyshell

Then, also enable the automatic deployment of ephemeral environments, and select the Bunnyshell cluster as the Destination cluster. Then, hit Save.

Environment settings in Bunnyshell

OK, there were in fact 5 clicks to also enable the automatic deployment and destruction of ephemeral environments… but it’s still a negligible effort, right? 😃

How Preview Environments get created

We’re going to make a code change to the backend, then create a Pull Request for it and observe how the Preview Environment will get created.

📓 If you forked the repository with all branches, you can skip the Application changes part, use the existing new-name branch and go directly to Create the Pull Request.

Application changes

Go into GitHub and open the file BookController.java from the apifolder: src/main/java/com/bunnyshell/templates/springbootrestapi/controller/. Edit the file and add something visible, like the ID of the book in its title: book.setTitle(book.getTitle() + "(" + book.getId() + ")");. Full method below, for the details route:

    @GetMapping("/books/{id}")
public ResponseEntity<Book> getBookById(@PathVariable(value = "id") Long bookId)
throws ResourceNotFoundException {
Book book = bookRepository
.findById(bookId)
.orElseThrow(() -> new ResourceNotFoundException("Book not found on :: " + bookId));

book.setTitle(book.getTitle() + "(" + book.getId() + ")");

return ResponseEntity.ok(book);
}

Don’t forget to commit the changes in a new branch, so we can also create a Pull Request.

GitHub — create Pull Request

Create the Pull Request

Then create the pull request. If you forked the repository, be careful to create the PR towards your repository, and not Bunnyshell’s.

Observe the Preview Environment get created and deployed

A new environment, named My First Env / new-name:main / PR-1 was created, and has immediately started deploying.
Notice it was created by the system, and it has a slightly different icon, with a clock embedded, suggesting it’s a temporary environment.

Environment listing in Bunnyshell, deploying environment

Switching back to Github, you can see that a comment also appeared on the Pull Request. Developers don’t need to leave their Git platform to open the applications and have frequently accessed screens from the platform at their fingertips.

Github — comment on Pull Request with useful links

How Preview Environment get destroyed

Simply merge or close the Pull Request.

Observe the Preview Environment get destroyed

The environment previously created, named My First Env / new-name:main / PR-1 has already started to be deleted.

Environment listing in Bunnyshell, deleting environment

Advanced use-case: Pipeline integration

If you need to create Preview Environments from existing workflows or pipelines, you can do that as well, using the Bunnyshell CLI tool.

The typical flow for creating an environment — and optionally running something on it — is as follows:

  • create the environment — optionally, label it, for easy retrieval
  • deploy the environment
  • (optionally) run tests or other operations based on deployed URLs

The typical snippet for the setup part is outlined below.
⚠️ Error handling was intentionally omitted to keep the commands easy to follow and understand.

PR_ID="injected_by_pipeline"


ENV_ID=`bns env create \
--from-template lx4n701Lwe \
--name "Programatically created" \
--label "pr=$PR_ID" \
--output json | jq -r .id`


BACKEND_URL=`bns env deploy --id $ENV_ID --k8s gn3d4ok3jK --output json | jq -r ".[1].endpoints.[0]"`


run-test-suite --host=$BACKEND_URL

As for the teardown of the environment, it can be done:

  • by ID — if it was stored when creating the environment
  • by label — so the environment ID doesn’t need to be stored
PR_ID="injected_by_pipeline"


ENV_ID=`bns env list --label "pr=$PR_ID" --output json | jq -r "._embedded.item.[0].id"`


bns env delete --id $ENV_ID

This topic will be covered in detail in a dedicated, future article.

I hope you found the concept of Preview Environments useful, and that seeing a concrete implementation gave you a few ideas on how you could benefit from using them, either in code reviews, manual/automated testing or improving collaboration.

--

--

Sorin Dumitrescu

Driving change in the DX space, with a background in web development and high-traffic applications and a passion for efficiency in technology and processes.