Part 2 of 3 – GO: API Dialing GRPC Endpoints – Running Each Service In Its Own Docker Container
The purpose of this article is to demonstrate how to setup a development environment that is containerized and has multiple microservices in a mono-repo that is all debugable with breakpoints at each point of the call chain from the HTTP Request, to the GRPC Request, the GRPC Response, and the HTTP Response.
3-Part Overview Review
In this overview we will be creating 3 microservices:
- An HTTP API
- A Strings Service
- A Numbers Service
We will be defining the Strings and Numbers service through Protobuff files and then generate server code. The HTTP API will implement the necessary client side code to communicate with these GRPC services.
We want to setup the Strings Service to convert an input string to a fully Upper-Case or fully Lower-Case string.
We want to setup the Numbers service to accept two numbers and then Add them or Subtract them.
We want the API to have the following HTTP endpoints and then communicate with the Strings or Numbers service as appropriate over GRPC and then return the GRPC response as the HTTP Response body:
- api/StringsService/MakeUpperCase
- api/StringsService/MakeLowerCase
- api/NumbersService/AddTwoNumbers
- api/NumbersService/SubtractTwoNumbers
We also want to be able to debug the entire operation from the HTTP Request, to the GRPC Request, to the Service Operations, to the GRPC Response, and the final HTTP Response.
Part 1: Setting Up The Development Container And Building The Services — Link
Part 2: Running Each Service In Its Own Docker Container
Part 3: Debug Each Service While It Is Running In Its Own Docker Container — Link
Part 2 – Running Each Service In Its Own Docker Container
So far this is great and all, but microservices were meant to do one thing, and do it well, and in most microservice architectures it is the only program running on the Operating System. This is the reason we added docker-indocker (moby) support earlier. Let’s simulate these services running in their own container.
Build a Dockerfile to run the strings_service
In the root of the project i.e. /workspaces/api_dialing_grpc add a new file called: “StringsServiceRunDockerfile”
Give it the following code:
# syntax=docker/dockerfile:1
FROM golang:1.18
WORKDIR /app
COPY protos/ protos/
COPY strings_service/ strings_service/
WORKDIR /app/strings_service
RUN go mod download
RUN go build -o /strings_service
WORKDIR /
RUN rm -rf /app
EXPOSE 50051
CMD [ "/strings_service" ]
With that file in place execute the following command while at /workspaces/api_dialing_grpc
docker build -t strings_service -f StringsServiceRunDockerfile .
docker run -it -d -p 50051:50051 strings_service
In your visual studio click the containers button and then refresh the containers area if necessary. You should now see something like this (your name might vary).
You should see that it is listening on port 50051 right away.
Your Postman request should look something like this.
Feel free to do additional tests. When you are done right click on that strings_service in the containers area of Visual Studio code and select “Remove”. Don’t worry we will only need to do the run in the future, but we may want to build as well which is why I recommend removing instead of just stopping.
To close out of the logs hover over the logs area and press the trashcan icon.
Build a Dockerfile to run the numbers_service
We will be doing something very similar to what we did with the strings_service. In the root of the application, i.e. add a new file called “NumbersServiceRunDockerfile”. Give it the following code:
# syntax=docker/dockerfile:1
FROM golang:1.18
RUN go install github.com/go-delve/delve/cmd/dlv@latest
WORKDIR /app
COPY protos/ protos/
COPY numbers_service/ numbers_service/
WORKDIR /app/numbers_service
RUN go mod download
RUN go build -o /numbers_service
WORKDIR /
RUN rm -rf /app
EXPOSE 50052
CMD [ "/numbers_service" ]
docker build -t numbers_service -f NumbersServiceRunDockerfile .
docker run -it -d -p 50052:50052 numbers_service
Sample Postman request while this service is running:
Build a Dockerfile to run the API / HTTP service
This one is a little bit more interesting than the others because it does need to coordinate gRPC requests from other microservices. Technically the only endpoint that will work with this is the /api/hello route, the others will say that it couldn’t connect to the GRPC service, and that’s ok. We do have our ways of getting all 3 up at the same time, but we will cover that later. For now let’s do a Dockerfile that will get this up and running and at least the /api/hello route working.
In the root of the application i.e. /workspaces/api_dialing_grpc/ add a new file called “ApiRunDockerfile”. Give it the following code.
# syntax=docker/dockerfile:1
FROM golang:1.18
WORKDIR /app
COPY protos/ protos/
COPY api/ api/
WORKDIR /app/api
RUN go mod download
RUN go build -o /api
WORKDIR /
RUN rm -rf /app
EXPOSE 8080
CMD [ "/api", "-strings_server_host=strings_server", "-numbers_server_host=numbers_server" ]
Execute the following command to build the container
docker build -t api -f ApiRunDockerfile .
docker run -it -d -p 8080:8080 api
When you are done remove the api container using your Visual Studio Code.
Manually run all 3 services at the same time
Before we run the services, we need to create a Docker network for the services to run on. Execute the following command:
docker network create api_dialing_grpc
docker run -it -d -p 50051:50051 --name=strings_service --network api_dialing_grpc --hostname=strings_server strings_service
docker run -it -d -p 50052:50052 --name=numbers_service --network api_dialing_grpc --hostname=numbers_server numbers_service
docker run -it -d -p 8080:8080 --name=api --network=api_dialing_grpc --hostname=api api
When you are done remove each of the running containers by doing a right-click and remove in Visual Studio Code.
So, at this point we have 3 services running. The string_service and number_service are accessible both via our Postman, and the API can access it as well, and technically it is accessing it at a different hostname than itself. So, we have successfully configured gRPC the way it is meant to be. Services running on different systems communicating with each other.
Obviously running these commands will be difficult to remember. We will use docker compose in just a moment but for now let’s create some build tasks in Visual Studio Code so w can access them via CTLR+SHIFT+P, and selecting the run task option.
Create Build Tasks in Visual Studio Code to build and run the containers separately or collectively
In the .vscode folder and the tasks.json file we are going to add a lot of tasks to build and run the services individually, or collectively. We will also make it so we can remove them individually or collectively as well.
Here is the updated code for this file:
{
"version": "2.0.0",
"tasks": [
{
"label": "Compile Strings Service Protobuff",
"type": "shell",
"command": [
"protoc",
"--go_out=./stringspb/",
"--go_opt=paths=source_relative",
"--go-gRPC_out=./stringspb/",
"--go-gRPC_opt=paths=source_relative",
"string_service.proto"
],
"options": {
"cwd": "${workspaceFolder}/protos"
}
},
{
"label": "Compile Numbers Service Protobuff",
"type": "shell",
"command": [
"protoc",
"--go_out=./numberspb/",
"--go_opt=paths=source_relative",
"--go-gRPC_out=./numberspb/",
"--go-gRPC_opt=paths=source_relative",
"./number_service.proto"
],
"options": {
"cwd": "${workspaceFolder}/protos"
}
},
{
"label": "Build the api_dialing_grpc docker network",
"type": "shell",
"command": [
"docker",
"network",
"create",
"api_dialing_grpc"
]
},
{
"label": "Build the strings_service Run Container",
"type": "shell",
"command": [
"docker",
"build",
"-t strings_service",
"-f StringsServiceRunDockerfile",
"."
],
"options": {
"cwd": "${workspaceFolder}"
}
},
{
"label": "Build the numbers_service Run Container",
"type": "shell",
"command": [
"docker",
"build",
"-t numbers_service",
"-f NumbersServiceRunDockerfile",
"."
],
"options": {
"cwd": "${workspaceFolder}"
}
},
{
"label": "Build the api Run Container",
"type": "shell",
"command": [
"docker",
"build",
"-t api",
"-f ApiRunDockerfile",
"."
],
"options": {
"cwd": "${workspaceFolder}"
}
},
{
"label": "Run the strings_service Container",
"type": "shell",
"command": [
"docker",
"run",
"-it",
"-d",
"-p 50051:50051",
"--name=strings_service",
"--network=api_dialing_grpc",
"--hostname=strings_server",
"strings_service"
],
"options": {
"cwd": "${workspaceFolder}"
}
},
{
"label": "Run the numbers_service Container",
"type": "shell",
"command": [
"docker",
"run",
"-it",
"-d",
"-p 50052:50052",
"--name=numbers_service",
"--network=api_dialing_grpc",
"--hostname=numbers_server",
"numbers_service"
],
"options": {
"cwd": "${workspaceFolder}"
}
},
{
"label": "Run the api Container",
"type": "shell",
"command": [
"docker",
"run",
"-it",
"-d",
"-p 8080:8080",
"--name=api",
"--network=api_dialing_grpc",
"--hostname=api",
"api"
],
"options": {
"cwd": "${workspaceFolder}"
}
},
{
"label": "Build All 3 Run Containers",
"dependsOn": [
"Build the strings_service Run Container",
"Build the numbers_service Run Container",
"Build the api Run Container"
],
"dependsOrder": "parallel"
},
{
"label": "Run All 3 Containers",
"dependsOn": [
"Run the strings_service Container",
"Run the numbers_service Container",
"Run the api Container"
],
"dependsOrder": "parallel"
},
{
"label": "Build and Run All 3 Containers",
"dependsOn": [
"Build All 3 Run Containers",
"Run All 3 Containers"
],
"dependsOrder": "sequence"
},
{
"label": "Remove the strings_service Container",
"type": "shell",
"command": [
"docker",
"rm",
"-f",
"strings_service"
]
},
{
"label": "Remove the numbers_service Container",
"type": "shell",
"command": [
"docker",
"rm",
"-f",
"numbers_service"
]
},
{
"label": "Remove the api Container",
"type": "shell",
"command": [
"docker",
"rm",
"-f",
"api"
],
"options": {
"cwd": "${workspaceFolder}"
}
},
{
"label": "Remove All 3 Containers",
"dependsOn":[
"Remove the strings_service Container",
"Remove the numbers_service Container",
"Remove the api Container"
],
"dependsOrder": "parallel"
}
]
}
Compose the Services to run up
While having these tasks are indeed useful, it is more appropriate to use docker compose in order to bring these services up or down.
To the root of the application create a new file called docker-run-compose.yml
version: '3'
services:
strings:
build:
dockerfile: StringsServiceRunDockerfile
hostname: strings_server
ports:
- 50051:50051
numbers:
build:
dockerfile: NumbersServiceRunDockerfile
hostname: numbers_server
ports:
- 50052:50052
api:
build:
dockerfile: ApiRunDockerfile
ports:
- 8080:8080
depends_on:
- strings
- numbers
docker compose -f docker-run-compose.yml build
docker compose -f docker-run-compose.yml up -d
Furthermore, the containers section of your Visual Studio Code should look like this now. Notice how all 3 of the services are now grouped together instead of on their own.
Docker compose -f docker-run-compose.yml down
{
"label": "Build All 3 Containers - Compose",
"type": "shell",
"command": [
"docker",
"compose",
"-f docker-run-compose.yml",
"build"
],
"options": {
"cwd": "${workspaceFolder}"
}
},
{
"label": "Run All 3 Containers - Compose",
"type": "shell",
"command":[
"docker",
"compose",
"-f docker-run-compose.yml",
"up",
"-d"
],
"options": {
"cwd": "${workspaceFolder}"
}
},
{
"label": "Build and Run All 3 Containers - Compose",
"dependsOn": [
"Build All 3 Containers - Compose",
"Run All 3 Containers - Compose",
],
"dependsOrder": "sequence"
},
{
"label": "Remove All 3 Containers - Compose",
"type": "shell",
"command": [
"docker",
"compose",
"-f docker-run-compose.yml",
"down"
],
"options": {
"cwd": "${workspaceFolder}"
}
}
So far, so good. But something is still missing:Debugging. In the next section we will cover how to debug these services such that when you share a project like this you can give it to a developer and have them simply run a few build tasks and then hit F5 going forward to be able to begin work and be productive.
Conclusion
We have now set up the services to run within their own docker container, and they are still able to communicate with each other without having to be on the same host. This is most similar to what we would do when these services are hosted. This article series does not cover the deployment of these services as the steps would differ depending on the cloud provider that you wanted to use. However, we will discuss how you can debug these services in just a single instance of Visual Studio Code. Read along to find out how.
Part 1: Setting Up The Development Container And Building The Services — Link
Part 2: Running Each Service In Its Own Docker Container
Part 3: Debug Each Service While It Is Running In Its Own Docker Container — Link
About Intertech
Intertech is a Software Development Consulting Firm that provides single and multiple turnkey software development teams, available on your schedule and configured to achieve success as defined by your requirements independently or in co-development with your team. Intertech teams combine proven full-stack, DevOps, Agile-experienced lead consultants with Delivery Management, User Experience, Software Development, and QA experts in Business Process Automation (BPA), Microservices, Client- and Server-Side Web Frameworks of multiple technologies, Custom Portal and Dashboard development, Cloud Integration and Migration (Azure and AWS), and so much more. Each Intertech employee leads with the soft skills necessary to explain complex concepts to stakeholders and team members alike and makes your business more efficient, your data more valuable, and your team better. In addition, Intertech is a trusted partner of more than 4000 satisfied customers and has a 99.70% “would recommend” rating.