Hello there! This is Ueda from the Backend Team at MonstarLab. Recently, in a project I was involved in, the topic of using gRPC came up. So, I went through various trials and errors to quickly set up the optimal development environment. In this article, I'll introduce the method I used.
- The code is based on the Connect-go tutorial.
- We'll build a container development environment using Dev Container,
- Introduce Hot Reload using tools like Air
- Demonstrate the use of protoc-gen-validate for validation.
To focus on setting up the development environment, I'll exclude technical explanations or architecture design related to gRPC and Connect, as well as topics like CI/CD, from this article. You can find the repository for the sample code here.
Operating Environment
I have conducted operational tests in the following environment:
- M1 MacBook Pro (Ventura 13.4.1)
- Docker Desktop for Mac Version 4.20.1
- VSCode Version 1.79.2
- Dev Containers v0.295.0
Technology Stack
My technology stack includes:
Setting Up the Environment
Personally, I believe there are three key criteria for creating the optimal development environment:
- Maintaining a certain level of code quality without relying solely on human attention.
- Compiling written code quickly to receive immediate feedback.
- Allowing anyone to start development promptly.
Taking these criteria into account for gRPC development environment, I have selected the following tools and libraries: VSCode Dev Container as the IDE, Golang as the programming language, Connect as the gRPC library, and libraries like Air for achieving Hot Reload. The tools chosen for this setup are considered industry standards and were also selected based on personal preferences after testing and evaluation.
Setting Up VSCode Dev Container
I added configurations based on the Go template created with the "Add Dev Container Configuration Files" command from the Dev Containers extension. To install Go configurations in the container, I set up the container build using a Dockerfile. The other main configuration items are primarily these three:
- Introducing Features.
- Configuring VSCode settings and introducing extensions.
- Installing necessary tools for Go development in the Dockerfile.
While there are other things I might want to do, such as setting up git pre-commit configurations, I prioritized starting gRPC development and kept the setup minimal.
Introducing Features
Configuring Oh My Zsh
To establish my shell environment, I have adopted Oh My Zsh. Since people often have their own preferences when it comes to shells, it's best to choose something you like. Here are the steps to introduce Oh My Zsh to the Dev Container:
Installing Zsh and Oh My Zsh
By installing the common-utils
feature, you can incorporate both Zsh and Oh My Zsh. Configure it within the "features" section in the devcontainer.json
file like this:
{
// // Features to add to the dev container. More info: https://containers.dev/features.
"features": {
"ghcr.io/devcontainers/features/common-utils:2": {
"configureZshAsDefaultShell": true
}
}
}
Setting Zsh as the Default Shell
You can change the default shell to Zsh by adding the following script to the Dev Container's dockerfile
. Reference: stackoverflow
RUN echo "if [ -t 1 ]; then" >> /root/.bashrc
RUN echo "exec zsh" >> /root/.bashrc
RUN echo "fi" >> /root/.bashrc
Configuring Oh My Zsh in .zshrc
Include the configuration for Oh My Zsh in the .zshrc
file within the .devcontainer
directory. You can use the zsh-template
from Oh My Zsh for this purpose.
Copying .zshrc to the Container
Finally, add the following line to your Dockerfile to copy the .zshrc
file to the home directory within the container:
ADD .zshrc $HOME
Configuring VSCode Settings and Introducing Extensions
Now let's dive into configuring VSCode. We'll start with the settings.json
configuration, where I will use the official default settings. You can customize these settings to match your personal preferences. When it comes to introducing extensions, this is the part where personal preferences shine through the most. In my case, I went with the following setup. Since these are all well-known extensions, I'll skip detailed descriptions for each. Just remember to add the necessary configurations to your settings files as needed.
In your .devcontainer.json
:
{
"customizations": {
"vscode": {
"extensions": [
"EditorConfig.EditorConfig",
"streetsidesoftware.code-spell-checker",
"wayou.vscode-todo-highlight",
"ms-azuretools.vscode-docker",
"shardulm94.trailing-spaces",
"bungcip.better-toml"
]
}
}
}
Configuring Dockerfile for Golang Development
An essential part of Golang development is the Go extension from Extensions. Installing this extension is enough to get started with development.
In your .devcontainer.json
:
{
// Configure tool-specific properties.
"customizations": {
"vscode": {
"extensions": ["golang.go"]
}
}
}
In the Dockerfile, you'll want to install dependencies required by the Go extension, such as gopls and dlv, along with any other necessary tools to complete your Golang development environment.
Dockerfile
RUN go install github.com/cweill/gotests/gotests@latest
RUN go install github.com/josharian/impl@latest
RUN go install github.com/go-delve/delve/cmd/dlv@latest
RUN go install honnef.co/go/tools/cmd/staticcheck@latest
RUN go install golang.org/x/tools/gopls@latest
Setting Up Connect-go Environment
With the Golang environment set up as described in the previous sections, we can now proceed to set up Connect-go. Essentially, by following the tutorial, you can easily get Connect-go up and running.
Introducing Extensions
For the Dockerfile, add the following commands based on the tutorial's instructions:
Dockerfile
RUN go install github.com/bufbuild/buf/cmd/buf@latest
RUN go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
RUN go install github.com/bufbuild/connect-go/cmd/protoc-gen-connect-go@latest
Writing Sample Code
Following the tutorial, create go.mod
, buf.yaml
, and buf.gen.yaml
files, write your main code, and execute main.go
to complete the Connect-go setup.
Introducing Hot Reload
To enhance the development environment, we will introduce Hot Reload. Hot Reload requires two steps:
- Automatic code generation.
- Hot Reload for Golang code.
Automatic Code Generation
For automatic code generation, utilize the buf generate
command upon saving proto files. You can achieve this using the "Run on Save" extension. Add "emeraldwalk.RunOnSave"
to your devcontainer.json
and configure it in .vscode/settings.json
:
{
// Run on Save
// proto-gen
"emeraldwalk.runonsave": {
"commands": [
{
"match": "\\.proto$",
"cmd": "rm -rf gen | buf generate"
}
]
}
}
This cmd
is used to remove all files within the gen
directory. This precaution is taken because the buf generate
command might not remove inadvertently generated files.
As the scale of your project increases, I think I will need to reconsider it and potentially implement a more refined approach.
Introducing Golang's Hot Reload with Air
For Golang's Hot Reload, we've introduced the use of Air. There are various ways to install it, but by adding RUN go install github.com/cosmtrek/air@latest
to your Dockerfile, the setup is complete. The configuration file needs to be set up as well. Running the air init
command within the container will generate the .air.toml
file, which you can modify based on the .air_sample.toml
, adapting it for your project's structure.
In this setup, we've set delay=500
. This delay is important because without it, when RunOnSave
operates, it might not be able to read files under the gen
directory.
Once the Hot Reload configuration is in place, make sure Air automatically starts after the container is up. Use the postStartCommand
in your devcontainer.json
:
{
"postStartCommand": "air -c .air.toml"
}
With these steps, your Hot Reload configuration is now fully set up.
Adding Request Validation Feature
Now, let's move on to adding the request validation feature, something that would be useful in practical usage. We'll use protoc-gen-validate (PGV) for Connect's validation.
Add RUN go install github.com/envoyproxy/protoc-gen-validate@latest
to your Dockerfile. Then, as documented, add the necessary configurations to buf.gen.yaml
and buf.yaml
. Be careful not to forget adding opt
according to your environment's needs.
buf.gen.yaml
plugins:
- plugin: buf.build/bufbuild/validate-go
out: gen
opt: paths=source_relative
buf.yaml
deps:
- buf.build/envoyproxy/protoc-gen-validate
After this, run the buf mod update
command to generate the buf.gen.yaml
file. Be aware that automatically running bef generate
might prevent you from receiving error messages. If things don't work smoothly, manually running the commands is recommended.
Now, let's implement validation. Import validate
in your greet.proto
file and modify the validation rules for GreetRequest
, like so:
greet/v1/greet.proto
syntax = "proto3";
package greet.v1;
import "validate/validate.proto";
option go_package= "connect-sample/gen/greet/v1;greetv1";
message GreetRequest {
string name = 1 [(validate.rules).string = {min_len: 5, max_len: 10}];
}
message GreetResponse {
string greeting = 1;
}
service GreetService {
rpc Greet(GreetRequest) returns (GreetResponse) {}
}
Running the buf generate
command will generate the gen.pb.validate.go
file. Upon inspecting its contents, you'll notice that the GreetRequest
type struct includes methods like Validate
and ValidateAll
. You can use these methods. For instance, you can add validation like this:
func (s *GreetServer) Greet(
ctx context.Context,
req *connect.Request[greetv1.GreetRequest],
) (*connect.Response[greetv1.GreetResponse], error) {
if err := req.Msg.Validate(); err != nil {
return nil, err
}
log.Println("Request headers: ", req.Header())
res := connect.NewResponse(&greetv1.GreetResponse{
Greeting: fmt.Sprintf("Hello, %s!", req.Msg.Name),
})
res.Header().Set("Greet-Version", "v1")
return res, nil
}
With this, the validation setup is complete.
Testing the Setup
You can test the setup using the following three commands:
git clone https://github.com/Yukio0315/connect-sample
devcontainer open # if devcontainer CLI installed
root ➜ /workspaces/connect-sample (main) $ go run ./cmd/client/main.go
YYYY/MM/DD hh:mm:ss unknown: invalid GreetRequest.Name: value length must be between 5 and 10 runes, inclusive
With these steps, you should see that the server runs without issues and the validation is working as expected.
Conclusion
This article has explained how to set up the optimal gRPC development environment using Connect-go within a Dev Container, all in a speedy manner. By incorporating features like linting and hot reload, I have been able to create an environment that's fairly ideal for development. While there might still be areas that need improvement, I have managed to establish the essential environment needed to kickstart development. I hope this article proves helpful to those involved in gRPC development and beyond.
Article Photo by Simon Berger