Imagine you’re working on a service within a cloud native distributed system, but acceptance testing is taking forever! You’d like to know if your changes work in harmony with other services, but running everything on your machine will potentially require numerous compilation tools and eat through your system’s resources. You could deploy your service to a cloud-based environment, but that would take a long time. What should you do? You can’t just skip testing, you’re a responsible developer!
Approaches to developing in a multi-service ecosystem
There are a few approaches to solving this problem, each having their own tradeoffs:
- Running your services locally. This entails building and running multiple services directly on your workstation. This requires disk space, memory, CPU, and eventually might consume enough resources to render the workstation inoperable. In addition, it can require compilation tools and run-time environments for any of the first-party services. Furthermore, any third-party services (e.g. databases, message queues) will need to be set up in order to run everything locally.
- Running your services in the cloud. This entails deploying one or more services to the cloud every time you want feedback. This approach enables you to validate your services in an environment that more closely resembles your production environment. However, it might be the most time intensive, and will require infrastructure to support deploying your services.
- A hybrid of local and cloud. This entails configuring any locally running services to communicate with the services that you have deployed to the cloud. The hybrid approach has some of the advantages and disadvantages of the previous two approaches, and also can require managing proxying, firewall, and other network configuration.
- A hybrid of local and containerization. Containerization allows you to run services on a virtual machine in a custom environment pre-configured for each service. This approach removes some of the complexities and disadvantages of the other approaches, allows you to test your service locally, abstracts away the configuration and dependencies of each service, and mitigates some of the resource impacts of running all services locally.
This guide covers the hybrid of local and containerization using Docker as our virtual environment. You can use Docker to put your dependencies (e.g. compilers, runtimes, even entire databases) in a box, called a container, so that your only dependency is on Docker itself, and not the myriad of dependencies that each individual application service might have.
Whether you’re brand new to Docker, or have worked with it for decades, there are some questions you inevitably run into: how do I create these containers? And where can I put them? A popular method is to build the containers using an automated CI/CD pipeline, and then store them in a container registry. Container Management is an extensive topic, and out of scope for this guide. This guide uses basic containers, with some basic build dependencies, and without any customization.
This guide walks you through step-by-step instructions for using Docker to containerize inter-dependent services, and then confirming that communication is occurring between the services on your workstation and a service running within Docker.
Prerequisites
This guide requires the following tools, applications and environment:
- Docker
- Docker Compose (Usually installed with Docker * depending on your Operating System)
- Git
- Java 17 or later
- TCP Port availability for port numbers
8888
,8889
, and48081
- Postman, curl or similar tool capable of executing a HTTP POST request
Launching the Sample Application Services
To illustrate communication between local and containerized services, this guide has an accompanying repository of three services mimicking a banking domain. Our three services are:
- Debit Service – This service allows bank customers to perform transactions
- Account Service – This service can support balance inquiries, deposits, and withdrawals
- Audit Service – This service logs any requests it receives
This guide illustrates running two of these services on your local computer, and the third service virtually, demonstrating communication between both local and virtual systems.
NOTE: While the steps in this guide work with any operating system, the commands below assume a Mac or Linux environment. If you’re running on Windows or any other operating system, you may need to tweak commands to suit your needs.
You are ready to start!
-
Start the Docker Engine, if necessary. This ensures that the virtual environment is running. To check, execute the following command:
docker info
If your Docker Engine is not running, you will receive an error similar to:
ERROR: Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
-
Clone the repository, located at https://github.com/vmware-tanzu-labs/simple-distributed-bank-services-demo:
git clone https://github.com/vmware-tanzu-labs/simple-distributed-bank-services-demo
-
Run the Account Service inside a virtual environment using the following command:
docker compose up account-service
This command takes a while to download everything needed to run the container. However you do not need to wait here. You can proceed to the next step. When the service is ready to receive a request, a message similar to the following log entry appears in the console:
AccountServiceApplication : Started AccountServiceApplication in 2.656 seconds (process running for 2.984)
-
Run the Debit Service locally by using the following commands:
cd debit-service SERVER_PORT=8889 ACCOUNT_SERVICE_URL=http://localhost:48081 AUDIT_SERVICE_URL=http://localhost:8888 ./gradlew bootRun
This creates a long-running process. When the service is ready to receive a request, a message similar to the following log entry appears in the console:
DebitServiceApplication : Started DebitServiceApplication in 2.656 seconds (process running for 2.984)
You’ll need to run this in a dedicated terminal session. This guide assumes you’ll use dedicated terminal sessions for each task.
The command to start this service specifies three environment variables used to configure how it will run:
- SERVER_PORT dictates that the Debit Service will run on port
8889
. - ACCOUNT_SERVICE_URL specifies that the Debit Service will communicate with the Account Service using a host address and port of
localhost:48081
.48081
is the port that Docker exposes to the local workstation to allow communication into the account-service container, and ultimately, the account service. - AUDIT_SERVICE_URL specifies that the Debit Service will communicate with the Audit Service using a host address and port of
localhost:8888
.
- SERVER_PORT dictates that the Debit Service will run on port
-
Run the Audit Service locally on a different port, via the following commands:
cd audit-service SERVER_PORT=8888 ./gradlew bootRun
When the service is ready to receive a request, a message similar to the following log entry appears in the console:
AuditServiceApplication : Started AuditServiceApplication in 2.656 seconds (process running for 2.984)
The command to start this service specifies one environment variable used to configure how it will run:
- SERVER_PORT dictates that the Audit Service will run on port
8888
.
- SERVER_PORT dictates that the Audit Service will run on port
-
Verify that the 3 services started successfully and are ready to receive requests
-
Make a request against the Debit Service. You can make the request in multiple ways. (e.g. Curl, Postman, etc.). Using curl to make the request, execute the following command:
curl --verbose --location --request POST 'localhost:8889/purchase' --header 'Content-Type: application/json' --data-raw '{ "amount": 10000 }'
-
Inspect the Audit Service console. The request in the previous step results in the Debit Service sending a DEBIT type request to the Audit Service, and the Account Service sending a TRANSACTION type request to the Audit Service. In the console you should see:
{type=DEBIT, status=SUCCESS, amount=10000} {type=TRANSACTION, status=SUCCESS, amount=10000}
Explaining the Magic
After completing the steps above the Account Service, running as a containerized service in Docker, communicated with the Audit Service, running locally on your workstation. But how does this work?
In Step 5 you executed a command that started the Account Service. This application, like the others that run locally, requires configuration. The docker-compose.yml file stores this configuration for the Account Service container in the account-service
section of services
. It looks like this:
services:
account-service:
image: gradle:7.6.0-jdk17
command: "./gradlew clean bootRun"
environment:
- AUDIT_SERVICE_URL=http://host.docker.internal:8888
- SERVER_PORT=8081
ports:
- 48081:8081
working_dir: /account-service
volumes:
- ./account-service:/account-service
By configuring the AUDIT_SERVICE_URL
as host.docker.internal:8888
, this allows the containerized application to communicate with the applications running locally. Herein lies the magic! The domain host.docker.internal
maps to a special network address provided by Docker to allow containers to access the host network on the workstation.
NOTE:
host.docker.internal
is a specific Docker feature available only for Windows and MacOS. Docker for Linux provides access to the host network via a different mechanism.
Conclusion
Developing Cloud Native applications provides many benefits and some challenges. As your suite of Cloud Native applications and their associated service dependencies grow this can also impact your Developer Experience (DevX) in undesirable ways.
Today, you learned how to configure and use Docker to support your development efforts while minimizing, or potentially even eliminating, the need to understand how to configure and run every dependency required to test and validate your application.
NOTE: Due to Docker’s licensing requirements and some compatibility issues with Apple’s silicon (e.g. M1, M2), the approach we describe in this guide may be inaccessible for some. Luckily, Podman is a very capable alternative for container and image management while providing similar network configuration.
Credits
Thanks to Amanda White, Brian Watkins, Will Sather, Al Bonney, and Chris Gunadi, who contributed their time and insights in support of this guide.
Helpful Links
Docker Docs – Install Docker Compose
Docker Docs – I want to connect from a container to a service on the host
Tanzu Developer Center – Secure Software Supply Chains Learning Path