Pull Request perfection: Automating Preview Environments with ease
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.
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
- A (free) Bunnyshell account.
- Your Git account integrated with Bunnyshell, so the platform can use your code to build container images.
- 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.
Then, from the environment’s page, click Configuration.
Next, paste the environment definition below.
⚠️ Please make sure to replace the gitRepo
attribute with your repository, for both api
and frontend
with 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.
The environment will be deployed now.
You can also see the deployment logs in real-time, by clicking View Pipeline.
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.
Test that everything works by adding a book. Or multiple.
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.
Go to Ephemeral Environments and enable both toggles — to create and also destroy ephemeral environments on Pull Requests.
Then, also enable the automatic deployment of ephemeral environments, and select the Bunnyshell cluster as the Destination cluster. Then, hit Save.
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 api
folder: 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.
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.
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.
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.
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.