Exploring Consul’s Service Mesh: Setup, Code Samples, and Deep Dive
This article walks through upgrading Consul to 1.2, configuring service‑mesh support, building two Go micro‑services, registering them with Consul, reloading the configuration, testing the mesh with curl, and provides a detailed analysis of Consul’s service‑mesh architecture, advantages, and limitations.
Upgrade Consul to 1.2
On macOS use Homebrew to upgrade: brew update consul Add the -config-dir flag to the launch plist at /usr/local/opt/consul/homebrew.mxcl.consul.plist. Example plist snippet:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>KeepAlive</key><true/>
<key>Label</key><string>homebrew.mxcl.consul</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/opt/consul/bin/consul</string>
<string>agent</string>
<string>-dev</string>
<string>-advertise</string>
<string>127.0.0.1</string>
<string>-config-dir</string>
<string>/usr/local/etc/consul.d</string>
</array>
<key>RunAtLoad</key><true/>
<key>WorkingDirectory</key><string>/usr/local/var</string>
<key>StandardErrorPath</key><string>/usr/local/var/log/consul.log</string>
<key>StandardOutPath</key><string>/usr/local/var/log/consul.log</string>
</dict>
</plist>Create two mock micro‑services
service1.go
package main
import (
"net/http"
"log"
"io"
)
func TestServer(w http.ResponseWriter, req *http.Request) {
resp, err := http.Get("http://127.0.0.1:38082/test2")
if resp != nil && resp.Body != nil {
defer resp.Body.Close()
}
if err != nil {
w.Write([]byte("make request failed
"))
return
}
io.Copy(w, resp.Body)
}
func main() {
http.HandleFunc("/test1", TestServer)
if err := http.ListenAndServe(":8081", nil); err != nil {
log.Fatal("ListenAndServe: ", err)
}
}service2.go
package main
import (
"net/http"
"log"
"io"
)
func TestServer(w http.ResponseWriter, req *http.Request) {
io.WriteString(w, "hello, world!
")
}
func main() {
http.HandleFunc("/test2", TestServer)
if err := http.ListenAndServe(":8082", nil); err != nil {
log.Fatal("ListenAndServe: ", err)
}
}Register the services in Consul
Place the following JSON files under /usr/local/etc/consul.d:
01_service1.json
{
"service": {
"name": "service1",
"port": 8081,
"connect": {
"proxy": {
"config": {
"upstreams": [{
"destination_name": "service2",
"local_bind_port": 38082
}]
}
}
}
}
}01_service2.json
{
"service": {
"name": "service2",
"port": 8082,
"connect": {
"proxy": {}
}
}
}Reload Consul to apply the new configuration:
consul reloadRun and test
Start both services in the background and issue a curl request to service1:
go run service1.go &> /dev/null
go run service2.go &> /dev/null
curl http://127.0.0.1:8081/test1
# Expected output: hello, world!Consul Service‑Mesh architecture
When a service definition contains a "connect":{"proxy":{}} block, Consul creates a dedicated tunnel proxy for each instance. The proxy establishes a TLS‑encrypted TCP tunnel to the original service; higher‑level protocols (HTTP/1.1, HTTP/2, gRPC) flow through this tunnel. The proxy implementation lives in connect/proxy/listener.go (function NewPublicListener).
Service‑to‑service calls use an UpstreamListener declared in the consumer’s configuration. The upstream listener acts as a reverse proxy that connects to the provider’s ConnectProxy (function NewUpstreamListener). Together they form the complete data path, providing TLS encryption and enabling Consul Intentions for fine‑grained access control. The tunnel also supports HTTP/2 when using the Connect‑Native integration.
Pros and cons
Supports any TCP‑based protocol (HTTP/1.1, HTTP/2, gRPC) via the TLS tunnel.
Implementation is compact (≈20 source files), making the logic easy to follow.
Deep integration with Consul’s service registration and discovery.
Load‑balancing is currently a simple random selection.
Advanced features such as timeouts, retries, circuit breaking, and traffic shaping are not built‑in; they must be added by extending connect/proxy/listener.go.
Manual configuration is required: service definitions must be edited and callers must use the upstream address instead of the original service address.
Conclusion
Consul’s service‑mesh implementation is lightweight and functional, offering a solid foundation for further extension and a good study case for service‑mesh internals.
References
https://www.hashicorp.com/blog/consul-1-2-service-mesh
https://www.consul.io/intro/getting-started/connect.html
https://www.consul.io/docs/agent/options.html
https://www.consul.io/docs/connect/intentions.html
https://www.consul.io/docs/connect/native.html
https://www.consul.io/docs/connect/native/go.html
https://www.consul.io/docs/connect/configuration.html
https://www.consul.io/docs/connect/proxies.html
https://www.consul.io/docs/connect/dev.html
https://www.consul.io/docs/connect/ca/consul.html
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
