March 7, 2023
9
min read

Ephemeral Webterminal Environments for Command Line Applications

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

Why Uffizzi Created a Solution for Ephemeral Environments for Command Line Applications

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.

Challenges while working on command line applications

  • Syncing the local environment to match the runtime environment when reviewing changes : When starting development on command line applications, setting the dependencies on a local system can be challenging and time-consuming. Especially when you are syncing dependencies and trying to update your environment to match dependencies for every pull request. It becomes even harder during collaboration on these tools. With every pull request developers have to pull the changes from the relevant branch, build it, and test it which can be quite cumbersome to do over and over again, particularly as your project grows.

  • UAT testing in a production-like environment : Unit tests can be configured to test the basic execution of the application and then integration tests can be useful for checking the end to end working of the application locally but if the CLI tool has 3rd party dependencies,has a complex runtime setup etc. UAT testing in a production-like environment becomes critical. Environments for UAT more often than not are shared and create bottlenecks that inhibit a project’s development velocity.  Developers must coordinate for a contested environment which means that testing is often blocked and delayed.

How these challenges can be solved with Ephemeral Environments

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 : 

  • Standardization.  Ephemeral environments are by design secure, portable and shareable - making it possible for users to pass them around for testing and ease of collaboration. Engineers don’t have to worry about setting up their local environments to match changes in a pull request in this specific case.

  • Fresh Environment. These environments are also designed to replicate production environments as closely as possible with all necessary runtime dependencies already installed. So when it comes time to test the application in an environment such as this one, the user will always have a fresh ephemeral environment to work with.

Let’s start with creating ephemeral environments on every pull request

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.

Structure of the Uffizzi Ephemeral Environment

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: 

  • First, set permissions in Kubernetes and your cloud provider to configure who can have access to the environments and only then
  • Get remote shell access into the container by opening a connection from your local shell and then access the environment to test the command line application.

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. 

Building and testing the CLI application locally

Getting acquainted with the command line application

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). 

  1. Build the application and output the docker-gs-ping binary. 
  2. Copy the binary from the first stage to the second and use it.

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. 

Adding TTYD support in our environment

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

Using docker-compose instead of docker to run the environment

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

Setting up the Uffizzi pipeline for Webterminal Ephemeral Environments

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. 

Defining the 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. 

Updating the Uffizzi configuration to make the echo server accessible

The requests should be routed to the individual ports on the ttyd container in the following fashion: 

Networking within the Uffizzi Webterminal CLI Ephemeral Environment

Here,

  1. When the request comes in to the Uffizzi URL (formatted as https://app.uffizzi.com/<link_to_github_PR>) it will be intercepted by the nginx container as the ingress points to it as you can see it under the x-uffizzi section of the docker-compose.
  2. The configuration for the nginx container dictates that requests coming to nginx need to be redirected to the certain ports exposed by the other container.
  3. This other container is running TTYD and has the docker-gs-ping (echo server) binary built from the pull requests. TTYD itself is exposed on the 7681 port which is used to interact with the container environment from where the user can run the docker-gs-ping binary. This allows the user to run docker-gs-ping with whatever arguments they would like to and test it however they think right. Docker-gs-ping itself is exposed on the 8080 port.
  4. Due to the nature of these ephemeral environments it is necessary that we do not allow users access to more than one port from the ingress. Nginx allows us to work around this by mapping the root of the ingress to the TTYD port so that upon accessing the ephemeral environments, users are instantly dropped into the terminal and can run the docker-gs-ping command and start previewing the changes in the PR.

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. 

Final Uffizzi configuration with nginx support

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

Triggering Uffizzi from Github Actions

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.

Testing ephemeral environments creation on every pull request

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.

CLI Webterminal Ephemeral Environment URL is posted as a Comment in this Pull Request. An ephemeral environment is created for every Pull Request to expedite iterative test-develop cycles.

 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.

Accessing the ttyd webterminal from the ephemeral environment URL

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.

Impact and Results

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

  • Enhanced Interactive Experience: With Uffizzi ephemeral environments, your CLI application can provide non-developer users with a more visual and hence interactive experience that helps them quickly identify the information they need. This could be particularly beneficial for users who are less familiar with complex search algorithms or technical jargon.

  • Increased Engagement and Faster Development Speed: By providing an engaging and interactive way of reviewing pull requests with Uffizzi ephemeral environments, developers and non-developers are encouraged to interact with newer changes brought to the command line application without any setup time or bureaucratic delays. This would ultimately lead to faster development speeds and in the process better time to market.

Ephemeral Webterminal Environments in the Wild

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.

1. Meilisearch

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.

Uffizzi logo
Environments as a Service
Learn More
preview icon
Empower your devs with an environment for every pull request
time icon
Avoid release delays that cost millions in missed revenue
Velocity icon
Improve development velocity at scale by up to 50%
Lifecycle icon
Plan capacity in real time with lifecycle management built in
Learn More