A guide on how to set-up per-feature ephemeral environments with Uffizzi for command line applications to support faster iterations and improve development velocity for these tools
CLI applications are an essential part of any developer's toolkit. Command line tools such as git, docker, npm (or any other package manager) etc. are now staples of the development trade. Even though they are so ubiquitous, solutions for creating ephemeral environments specifically made for development of CLI tools haven’t been seen in the wild.
On-demand ephemeral environments would be empowering in this case as developers wouldn’t be dependent on the work of other to fully test new features :
Ephemeral environments are temporarily created for users on demand so that they don’t have to create it themselves every time they need one. In this case let’s say that we are creating it on every new pull request. These disposable environments will be created specifically for a change to a CLI application.
In this blog, we will explore these benefits by implementing ephemeral environments for CLI applications using Uffizzi and walk through the steps to set up and use them in your developer workflow.
Uffizzi runs on top of Kubernetes but the end user doesn’t have to bother with all that. If the application has a preconfigured Dockerfile and docker-compose ready to go with it then configuring Uffizzi is going to be straightforward. If your application doesn’t already have a Dockerfile and/or a docker-compose config we will be discussing a little bit about that in this blog, but these two configurations are prerequisites for Uffizzi.
In our case we need to showcase a CLI application in a container. Usually to do so you would need:
To make it easier we will use a web terminal application called ttyd, which will allow us to have cli access to the environment via a web browser without having to ssh or remote shell access. Let’s use ttyd as a base to scaffold the CLI application environment and then build out the Uffizzi ephemeral environment configuration based on docker-compose.
To get started let's take a look at the application that we are working with. This is a simple echo server written in golang. And below is the Dockerfile for the same. The application and all the configuration files used to demonstrate creating ephemeral environments can be found here.
##
## Build - build the binary
##
FROM golang:1.17-buster AS build
WORKDIR /app
COPY go.mod .
COPY go.sum .
RUN go mod download
COPY *.go ./
RUN go build -o /docker-gs-ping
##
## Run - copy the built binary from the first step and run
##
FROM gcr.io/distroless/base-debian10
WORKDIR /
COPY --from=build /docker-gs-ping /docker-gs-ping
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/docker-gs-ping"]
Dockerfile - Simple Echo Server
The above Dockerfile shows how the image for the echo server is built in two stages (also known as a multistage build).
If you want to learn more about multistage builds in docker check out the Official Docker Documentation.
We can test run the above Dockerfile as follows. All the files used in this demo can be found in the UffizziCloud/cli-previews repo. We will be cloning the same from github and testing.
git clone https://github.com/UffizziCloud/cli-previews
git checkout main
docker build . -t cli-previews-test:v1
docker run -p 8080:8080 cli-previews-test:v1
____ __
/ __/___/ / ___
/ _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.10.2
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
O\
⇨ http server started on [::]:8080
Running the Simple Echo Server using docker
Now that we know that the application works well, and the build works reliably we can showcase the application binary in an ephemeral environment instead. For this, we will replace the base image of the final stage of the above multistage build to use an image which runs ttyd so that we can access the environment where the binary will be running from from a browser.
We will be updating the second stage of the workflow as follows. We would still be copying the binary from the first stage to the second stage and will be changing the entrypoint to ttyd.
##
## Build - build the binary
##
FROM golang:1.17-alpine AS build
WORKDIR /app
COPY go.mod .
COPY go.sum .
RUN go mod download
COPY *.go ./
RUN go build -o /docker-gs-ping
##
## Run - copy the built binary from the first stage and run it in a ttyd env
##
FROM uffizzi/ttyd:alpine
RUN apk update --quiet \
&& apk add -q --no-cache libgcc tini curl
WORKDIR /
COPY --from=build /docker-gs-ping /bin/docker-gs-ping
RUN chmod +x /bin/docker-gs-ping && ln -s /bin/docker-gs-ping /docker-gs-ping
ENTRYPOINT ["tini", "--"]
CMD ["ttyd", "/bin/zsh"]
Dockerfile.ttyd - Simple Echo Server running in TTYD image
Once we are done with the above we can test with the following command and going to localhost:7681 will take us to the ttyd terminal from where you should be able to access the docker-gs-ping command.
docker build . -f Dockerfile.ttyd -t cli-previews-test:v2
docker run -p 7681:7681 cli-previews-test:v2
Running the web terminal env with simple echo server using docker
You can use the following docker-compose definition by running the command
docker-compose up
docker-compose command to run the web terminal env with simple echo server
version: "3"
services:
cli-preview:
build:
context: ./
dockerfile: ./Dockerfile.ttyd
restart: unless-stopped
ports:
- "7681:7681"
- "8080:8080"
docker-compose configuration for running the webterminal env image
In the above docker-compose example we are basically running the same web terminal environment. But, instead of having to provide inline configuration we can define it all in a file and run the web terminal environment with just one command. As we can see here, if the image is not already built, the build predicate builds the image using the Dockerfile.ttyd file and exposes the 7681 (ttyd) and the 8080 (echo server) ports.
Once this container is up, we can access the web terminal by going to http://localhost:7681. As we get access to the web terminal we can go ahead and run the echo server which will make it possible for the container to start accepting requests on http://localhost:8080.
Now that we know how to package an application binary to be used in a ephemeral webterminal environment, let’s automate that process further by allowing Uffizzi to create an ephemeral environment on every pull request that is created on github as we initially set out to do.
Now that the prerequisites for creating an Uffizzi configuration have been satisfied i.e. having a Dockerfile, docker-compose, we can build a Uffizzi configuration.
Let’s take the existing docker-compose and spice it up so that it becomes Uffizzi compatible.
version: "3"
x-uffizzi:
ingress:
service: cli-preview
port: 7681
services:
cli-preview:
build:
context: ./
dockerfile: ./Dockerfile.ttyd
restart: unless-stopped
ports:
- "7681:7681"
- "8080:8080"
docker-compose configuration for running the webterminal env image converted to a uffizzi configuration by adding x-uffizzi
The configuration above is the same as the one we have seen before. The only difference here is the x-uffizzi block which defines the uffizzi configuration. The ingress for the ephemeral webterminal environment will point to the cli-preview service and all requests from the ingress will be directed to the 7681 port.
The problem here is that the echo server listens on port 8080 so we won’t have access to it once the preview is up. For this reason we will add a nginx container which will help us configure the container to help route the requests to the echo server. This way we will have access to TTYD and the Echo Server once it is up and running.
The requests should be routed to the individual ports on the ttyd container in the following fashion:
Here,
The web terminal ephemeral environment cannot be deployed by itself as once the echo server is running there is no way we can access it from outside, so for this, let’s use a nginx ingress which we can use to access the echo server from the /echo endpoint on the Uffizzi url. The configuration looks like the following.
events {
worker_connections 4096;
}
http {
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 8081;
location / {
proxy_pass http://localhost:7681;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
location /echo/ {
proxy_pass http://localhost:8080/;
}
}
}
nginx configuration derived from the above diagram
In the above nginx configuration we can see that all the connections coming in at / are being upgraded to http 1.1, this is because ttyd uses websockets and for all the incoming requests coming at /echo will be redirected to the port 8080 which is where the echo server will be running once we run it manually in ttyd. We will store the above configuration in uffizzi/nginx/nginx.conf.
Once the final nginx configuration is, we can set up our ephemeral environment’s ingress to nginx to route the requests as we need them to, to test our application.
version: "3"
x-uffizzi:
ingress:
service: nginx
port: 8081
services:
cli-preview:
image: ${CLI_PREVIEW_IMAGE}
restart: unless-stopped
deploy:
resources:
limits:
memory: 500M
nginx:
image: nginx:alpine
restart: unless-stopped
volumes:
- ./uffizzi/nginx:/etc/nginx
Uffizzi configuration with nginx to help with accessing both the ports on the cli-preview container
Uffizzi can leverage your existing build workflows from github itself through the reusable workflow for github actions. Once the workflow is set up, Uffizzi compose will be updated to use the image from the build done for the pull request. This is the github actions build file which builds the image for the echo server and then pushes it and then is subsequently used by the Uffizzi configuration above. The UffizziCloud/cli-previews github repo uses the two stage workflow as it enables outside contributors to create previews. It is preferable to use the reusable workflow for github actions mentioned earlier for easier setup.
These configurations need to be committed to the repo so that they can run when a pull request is opened against the repository.
Once we have all the configurations defined and committed to the repo along with the reusable workflow, opening a pull request against the repo will trigger a two stage workflow which upon completion will create a web terminal ephemeral environment and a comment will be posted on the pull request which will look something like the following.
Accessing the url will take you to the environment which will look like the below. Running the `ls` command you can see that the docker-gs-ping binary which has been built from the PR also exists there.
You can test the binary by running it and then checking if the /echo endpoint is accessible. You should also be able to see a response back on the tab which has the binary running.
It is clear that this integration has the potential to improve the developer experience and previewing experience for non-developers, making the CLI application more accessible. Uffizzi’s ephemeral webterminal environments for command line applications can guarantee the following
You can see examples of CLI tools using ephemeral environments on Github Open Source repository's for these two popular projects - Meilisearch and Lazygit. For each repo there are three key folder/files that contribute to the ephemeral environment per pull request capability. Note that if you want to do this on a private repository there's not need to break the build and deploy github action workflows into separate files - a single workflow file is sufficient. The 2-stage workflow is used for security reasons for outside contributors to open source projects.
Application Definition Folder for Ephemeral Environments-
https://github.com/meilisearch/meilisearch/tree/main/.github/uffizzi
Ephemeral Environment Build Workflow-
https://github.com/meilisearch/meilisearch/blob/main/.github/workflows/uffizzi-build.yml
Ephemeral Environment Deploy Workflow-
https://github.com/meilisearch/meilisearch/blob/main/.github/workflows/uffizzi-preview-deploy.yml
2. Lazygit
Application Definition for Ephemeral Environments-
https://github.com/jesseduffield/lazygit/tree/master/uffizzi
Ephemeral Environment Build Workflow-
https://github.com/jesseduffield/lazygit/blob/master/.github/workflows/uffizzi-build.yml
Ephemeral Environment Deploy Workflow-
https://github.com/jesseduffield/lazygit/blob/master/.github/workflows/uffizzi-preview.yml
If you'd like to try ephemeral environments with the example from this blog - https://github.com/UffizziCloud/cli-previews - or with your own CLI tool you can prototype for free with Uffizzi Cloud.