Part 2 of 3GO: 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.

Show me the code: https://github.com/woodman231/api_dialing_grpc

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" ]
Essentially what we are doing here is copying the protos folder and the strings_service folder to the container. We then download dependencies, build the service, delete the source code from the container, and run the strings_service executable

With that file in place execute the following command while at /workspaces/api_dialing_grpc

docker build -t strings_service -f StringsServiceRunDockerfile .
After it has completed building run it using the following command
docker run -it -d -p 50051:50051 strings_service
You should get a response back giving you the instance id of the service. Using the -d command made it so that it would allow us to continue to use our terminal while it is running the service (daemon mode). Using the -p command told it to expose port 50051 and pass it through.

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

Do a right-click on the strings_service and select View Logs

You should see that it is listening on port 50051 right away.

Go to your Postman and use the MakeUpperCase gRPC Method Request that we made earlier (not the HTTP One that we made most recently but the gRPC one we made before that) Generate another example message or use the one that you already have and click on Invoke.

Your Postman request should look something like this.

Your place in Visual Studio where you are viewing the logs should look like this:
So far, so good we have the strings_service running in its own container, and we are able to interact with our gRPC methods through it.

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" ]
Execute the following command from /workspaces/api_dialing_grpc
docker build -t numbers_service -f NumbersServiceRunDockerfile .
After it is done building, execute the following command to run it:
docker run -it -d -p 50052:50052 numbers_service
Again, you can use Postman to test out the gRPC requests that were made earlier for the numbers service. Test out invoking each. You can also read the logs as we did with the strings_service as well.

Sample Postman request while this service is running:

Again, when your done remove the numbers_service container in Visual Studio code.

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" ]
So once again we are copying the protos and api directory. Building the application and then deleting the source code and running the application.

Execute the following command to build the container

docker build -t api -f ApiRunDockerfile .
After it is done building run it using the following command:
docker run -it -d -p 8080:8080 api
Once it is running use your Postman to make a request to the /api/hello endpoint. You can also test the others, but you will get an error about being unable to connect to the GRPC services.

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
Now that they have all been built at least once and have a network to connect to we can just execute the following three commands to run all three of them. Notice that we are using the daemon mode (-d) switches so we can just do all of this in one terminal.
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
The containers area of your Visual Studio Code should look like this:
The terminal should look something like this:
At this point all of the end points that you created in your Postman workspace should work whether it is over gRPC or the HTTP API. Feel free to Invoke / Send to your hearts content with all the items in the Postman workspace.

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"
        }
    ]
}
With this in place now when you do CTRL+SHIFT+P and select the Tasks: Run Tasks option you will now be presented with allot to choose from. Feel free to experiment with them all to run each service individually or collectively as you see fit. Review the Docker tab of Visual Studio code and test out the Postman workspace as you see fit. Probably the most useful tasks here are the “Build and Run All 3 Containers” and “Remove All 3 Containers” tasks. The other tasks are there to help with the individual development lifecycle of any given service as well as to be tasks that the Build, Run, or Remove All tasks can “dependsOn” while being executed. As you can see from the code all that is here is basically all of the command lines we went over earlier to build and run any of the services. They are just now available as build tasks in Visual Studio code to make it easier for future developers to work on this project.

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
In a command prompt execute the following commands:
docker compose -f docker-run-compose.yml build
docker compose -f docker-run-compose.yml up -d
Use the Postman collection that we created earlier to test out all of the various services. You should notice that they will work without a hitch.

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.

You can do a right-click on that api_dialing_grpc top piece and select “Compose Logs”. And you can see logs from all of the services at once. For example:
When you are done you can right-click on the api_dialing_grpc and select Compose Down or execute the following command:
Docker compose -f docker-run-compose.yml down
And of course, we are going to add these as build tasks to the tasks.json file. Here are the extra tasks to add to the file:
        {
            "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}"
            }
        }
When you do CTRL+SHIFT+P and select the Tasks: Run Tasks option you will now have these tasks available. Please feel free to test them out and ensure they are working by inspecting the Containers area of your Visual Studio Code and Testing out the services in the Postman collection that we made.

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.

Show me the code: https://github.com/woodman231/api_dialing_grpc

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.