Part 3 of 3 – GO: API Dialing GRPC Endpoints – Debug Each Service While It Is Running 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 — Link
Part 3: Debug Each Service While It Is Running In Its Own Docker Container
Part 3 – Debug Each Service While It Is Running In Its Own Docker Container
Thanks to DLV, we can have remote debugging. Since services are supposed to run in their own containers in production, we might as well debug them on our computers in the same way. It is a little tricky to get set up due to some security features we have to manipulate on our local containers, but that is not so bad.
Thus far when the services are being built via the command we gave it via docker it is building with the default settings and as such is what is known as an optimized and compiled binary for the OS that it was built under. We also deleted the source code from the server after the build was complete. These builds are not able to be debugged and therefore we need to both build and run our services differently than we have been building them. We will demonstrate using different arguments to the build command, installing dlv in to the container and telling dlv to run our service as well as tell visual studio code how to map the source files on the remote container to the source files in our development container.
Debug the strings_service Service
To begin we will need a new Dockerfile which will compile our source code to a binary that is able to be debugged. To the root of the project i.e /workspaces/api_dialing_grpc add a new file called: “StringsServiceDebugDockerfile”. 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 strings_service/ strings_service/
WORKDIR /app/strings_service
RUN go build -gcflags="all=-N -l"
EXPOSE 50051 40001
CMD ["/go/bin/dlv", "--listen=:40001", "--headless=true", "--api-version=2", "--accept-multiclient", "exec", "./strings_service"]
Next, we need to tell Visual Studio Code how to build, run, and connect to this container for debugging.
Add the following tasks to /.vscode/tasks.json
{
"label": "Build the strings_service Debug Container",
"type": "shell",
"command": [
"docker",
"build",
"-t strings_service",
"-f StringsServiceDebugDockerfile",
"."
],
"options": {
"cwd": "${workspaceFolder}"
}
},
{
"label": "Debug the strings_service Container",
"type": "shell",
"command": [
"docker",
"run",
"-it",
"-d",
"-p 50051:50051",
"-p 40001:40001",
"--name=strings_service",
"--network=api_dialing_grpc",
"--hostname=strings_server",
"--cap-add=SYS_PTRACE",
"--security-opt=\"seccomp=unconfined\"",
"strings_service"
],
"options": {
"cwd": "${workspaceFolder}"
}
},
{
"label": "Build and Debug the strings_service Container",
"dependsOn": [
"Build the strings_service Debug Container",
"Debug the strings_service Container"
],
"dependsOrder": "sequence"
}
Do the CTRL+SHIFT+P to Run a Task and Run the “Build and Debug the strings_sevice Container”.
Your containers area should look like this:
Create a new file in the .vscode folder called “launch.json”. Give it the following code
{
"version": "0.2.0",
"configurations": [
{
"name": "Attach to strings_service Debug Container",
"type": "go",
"request": "attach",
"mode": "remote",
"remotePath": "/app/strings_service",
"cwd": "${workspaceFolder}/strings_service",
"host": "127.0.0.1",
"port": 40001,
"trace": "verbose",
"preLaunchTask": "Build and Debug the strings_service Container",
"postDebugTask": "Remove the strings_service Container"
}
]
}
If you click on the Run and Debug button within your Visual Studio you will now have this “Attach to strings_Service Debug Container” as an option to use. Select that option and click on the green play button.
Once everything is done it should show you a “Debug Console” similar to this:
As you do it should log it to the strings_service container log that you are viewing right now.
As you do Visual Studio Code should turn orange in your task bar indicating that it needs attention.
Inspect the in parameter and the InputString it should be the same string as your input_string in the Postman gRPC request
When you are done testing click on the Disconnect button in the Debug toolbar.
You can also return to line 27 in /strings_service/main.go and press F9 if you no longer want the breakpoint. But I am going to leave mine there for now.
Debug the numbers_service Service
Debugging the numbers_service will be very much like debugging the strings_service.
In the root of the application i.e /workspaces/api_dialing_grpc create a new file called “NumbersServiceDebugDockerfile”. 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 build -gcflags="all=-N -l"
EXPOSE 50052 40002
CMD ["/go/bin/dlv", "--listen=:40002", "--headless=true", "--api-version=2", "--accept-multiclient", "exec", "./numbers_service"]
Add the following tasks to /.vscode/tasks.json
{
"label": "Build the numbers_service Debug Container",
"type": "shell",
"command": [
"docker",
"build",
"-t numbers_service",
"-f NumbersServiceDebugDockerfile",
"."
],
"options": {
"cwd": "${workspaceFolder}"
}
},
{
"label": "Debug the numbers_service Container",
"type": "shell",
"command": [
"docker",
"run",
"-it",
"-d",
"-p 50052:50052",
"-p 40002:40002",
"--name=numbers_service",
"--network=api_dialing_grpc",
"--hostname=numbers_server",
"--cap-add=SYS_PTRACE",
"--security-opt=\"seccomp=unconfined\"",
"numbers_service"
],
"options": {
"cwd": "${workspaceFolder}"
}
},
{
"label": "Build and Debug the numbers_service Container",
"dependsOn": [
"Build the numbers_service Debug Container",
"Debug the numbers_service Container"
],
"dependsOrder": "sequence"
}
{
"name": "Attach to numbers_service Debug Container",
"type": "go",
"request": "attach",
"mode": "remote",
"remotePath": "/app/numbers_service",
"cwd": "${workspaceFolder}/numbers_service",
"host": "127.0.0.1",
"port": 40002,
"trace": "verbose",
"preLaunchTask": "Build and Debug the numbers_service Container",
"postDebugTask": "Remove the numbers_service Container"
}
You can test both the strings_service and numbers_service at the same time by using the drop down in the Debug menu and pressing the green button each time you have a new item selected in the drop down.
Debug the API / HTTP Service
Debugging the API / HTTP Service will be much like we did for the others. Of course we have a dependency on the others with this but for now we will just focus on getting it running and hitting our /api/hello endpoint.
Create a new file called “ApiDebugDockerfile”. 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 api/ api/
WORKDIR /app/api
RUN go build -gcflags="all=-N -l"
EXPOSE 8080 40000
CMD ["/go/bin/dlv", "--listen=:40000", "--headless=true", "--api-version=2", "--accept-multiclient", "exec", "./api", "--", "--strings_server_host=strings_server", "--numbers_server_host=numbers_server"]
Add the following tasks to /.vscode/tasks.json
{
"label": "Build the api Debug Container",
"type": "shell",
"command": [
"docker",
"build",
"-t api",
"-f ApiDebugDockerfile",
"."
],
"options": {
"cwd": "${workspaceFolder}"
}
},
{
"label": "Debug the api Container",
"type": "shell",
"command":[
"docker",
"run",
"-it",
"-d",
"-p 8080:8080",
"-p 40000:40000",
"--name=api",
"--network=api_dialing_grpc",
"--hostname=api",
"--cap-add=SYS_PTRACE",
"--security-opt=\"seccomp=unconfined\"",
"api"
],
"options": {
"cwd": "${workspaceFolder}"
}
},
{
"label": "Build and Debug the api Container",
"dependsOn": [
"Build the api Debug Container",
"Debug the api Container"
],
"dependsOrder": "sequence"
}
{
"name": "Attach to api Debug Container",
"type": "go",
"request": "attach",
"mode": "remote",
"remotePath": "/app/api",
"cwd": "${workspaceFolder}/api",
"host": "127.0.0.1",
"port": 40000,
"trace": "verbose",
"preLaunchTask": "Build and Debug the api Container",
"postDebugTask": "Remove the api Container"
}
If you put break points in lines 81 and 119 in /api/main.go you should get a break point every time you use an HTTP Request in Postman. You will also hit the break point in the strings or numbers service if the code executes as part of the HTTP Request. This allows you to be able to debug the request from the beging of the HTTP Request, the Beginning of the gRPC request, the end of the gRPC Response and the end of the HTTP Response despite the fact that there are three different services running on three different containers, all debuggers are in one IDE with this setup.
Stop the services when you are done testing this sort of connectivity.
Use Docker Compose to Debug All 3 Services
Of course, selecting these debuggers and running them over and over again could be tedious. However, they are also useful if you are only debugging one service at a time. To get all three services up and running in debug mode and our Visual Studio Code connected to all 3 debuggers with a single click this is how we do it.
Create a new file in the root of the application called ” docker-debug-compose.yml”.
version: '3'
services:
strings:
build:
dockerfile: StringsServiceDebugDockerfile
hostname: strings_server
security_opt:
- "seccomp:unconfined"
cap_add:
- SYS_PTRACE
ports:
- 50051:50051
- 40001:40001
numbers:
build:
dockerfile: NumbersServiceDebugDockerfile
hostname: numbers_server
security_opt:
- "seccomp:unconfined"
cap_add:
- SYS_PTRACE
ports:
- 50052:50052
- 40002:40002
api:
build:
dockerfile: ApiDebugDockerfile
security_opt:
- "seccomp:unconfined"
cap_add:
- SYS_PTRACE
ports:
- 8080:8080
- 40000:40000
depends_on:
- strings
- numbers
Add the following tasks to tasks.json
{
"label": "Build All 3 Debug Containers - Compose",
"type": "shell",
"command": [
"docker",
"compose",
"-f docker-debug-compose.yml",
"build"
],
"options": {
"cwd": "${workspaceFolder}"
}
},
{
"label": "Debug All 3 Containers - Compose",
"type": "shell",
"command":[
"docker",
"compose",
"-f docker-debug-compose.yml",
"up",
"-d"
],
"options": {
"cwd": "${workspaceFolder}"
}
},
{
"label": "Build and Debug All 3 Containers - Compose",
"dependsOn":[
"Build All 3 Debug Containers - Compose",
"Debug All 3 Containers - Compose"
],
"dependsOrder": "sequence"
},
{
"label": "Remove All 3 Debug Containers - Compose",
"type": "shell",
"command": [
"docker",
"compose",
"-f docker-debug-compose.yml",
"down"
],
"options": {
"cwd": "${workspaceFolder}"
}
}
{
"name": "Attach to composed strings_service Debug Container",
"type": "go",
"request": "attach",
"mode": "remote",
"remotePath": "/app/strings_service",
"cwd": "${workspaceFolder}/strings_service",
"host": "127.0.0.1",
"port": 40001,
"trace": "verbose"
},
{
"name": "Attach to composed numbers_service Debug Container",
"type": "go",
"request": "attach",
"mode": "remote",
"remotePath": "/app/numbers_service",
"cwd": "${workspaceFolder}/numbers_service",
"host": "127.0.0.1",
"port": 40002,
"trace": "verbose"
},
{
"name": "Attach to composed api Debug Container",
"type": "go",
"request": "attach",
"mode": "remote",
"remotePath": "/app/api",
"cwd": "${workspaceFolder}/api",
"host": "127.0.0.1",
"port": 40000,
"trace": "verbose",
"postDebugTask": "Remove All 3 Debug Containers - Compose"
}
"compounds": [
{
"name": "Build and attach to composed Debug Containers",
"configurations": [
"Attach to composed strings_service Debug Container",
"Attach to composed numbers_service Debug Container",
"Attach to composed api Debug Container"
],
"preLaunchTask": "Build and Debug All 3 Containers - Compose",
"stopAll": true
}
]
In the Debug Area you will see that you have something similar to this now. It’s a little bit of a bummer to see these with the composed in it since you will only be using the compound one, but that just is what it is at this time. So really when you use this project you will most likely be using just the “Build and Debug All 3 Containers – Compose” configuration every time you make a change to the service code.
Retrospective
The world of microservices are meant to be one code repository for one service. This architecture is more of a mono repo with all of the services. There are some pros and cons to consider for this sort of a setup. On the one hand while working on these as a team it will be pretty clear how changes in one service could impact changes in another. On the other hand if there are some major changes needed just on one service and not the others then the code base could get stale quickly from which the developer has checked out their branch from.
A couple of things that we could have done differently include having each proto file in each service instead of having a single point / package for the protos. This would certainly make the separation of concerns more clear, but might make the orchestrator / api a little bit more difficult to manage since the client and server compiled protofiles usually need to be changed at the same time.
Regardless this is still a great example of spinning up microservices in to their own containers, running and debugging them and experiencing the orchestration in order to keep a grip on what is going on in the big picture.
Here is a challenge for you if you are up to it: Can you add a MultipleTwoNumbers method to the numbers_service service?
Conclusion
Microservices are a great way to separate processing power and later coordinate services all together to accomplish the greater goal. I hope that you enjoyed this article.
Part 1: Setting Up The Development Container And Building The Services — Link
Part 2: Running Each Service In Its Own Docker Container — Link
Part 3: Debug Each Service While It Is Running In Its Own Docker Container
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.