🔧 Microservices Architecture in Golang: A Transformative Experience! 🚀
Nachrichtenbereich: 🔧 Programmierung
🔗 Quelle: dev.to
Introduction: As Steve Jobs, co-founder of Apple, once said, “Technology moves the world.” In this article, I will share my experience with microservices architecture in Golang and how it has revolutionized the way I build scalable and flexible systems. Through a solution implemented for a client, I will explore the concepts and technologies involved, providing valuable insights for those looking to adopt this architecture in their projects. Let’s embark on this journey of microservices empowered by Golang! 💡💻
🌐 Building a REST API in Golang: As highlighted by Martin Fowler, a renowned author in the technology field, in his book “Patterns of Enterprise Application Architecture” “Software architecture is the fundamental organization of a system, embodying its key responsibilities and providing a skeleton for its implementation.” My journey started with building a REST API in Golang, leveraging its advanced concurrency and efficiency features. This API served as a service for the front-end, providing structured data from a MySQL database.
example of code:
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/users", getUsersHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
func getUsersHandler(w http.ResponseWriter, r *http.Request) {
// Handle GET request to fetch users from the database
fmt.Fprintln(w, "Fetching users...")
}
🔒 Managing Sensitive Data: John Allspaw, a renowned author and software reliability expert, mentions in his book “The Art of Capacity Planning” the importance of protecting sensitive data. In the case of the system I developed, the MySQL database stored user information, including login details, access levels, and service history in a queue. These data were managed through a microservices architecture, where security was a priority. As part of this process, I implemented recommended practices for encryption and key management to ensure the confidentiality of user data.
example of code:
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"fmt"
"io"
)
func encryptData(data []byte, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
encrypted := make([]byte, aes.BlockSize+len(data))
iv := encrypted[:aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return nil, err
}
stream := cipher.NewCFBEncrypter(block, iv)
stream.XORKeyStream(encrypted[aes.BlockSize:], data)
return encrypted, nil
}
💬 Using WebSockets for Real-Time Communication: “Effective communication is the key to any successful relationship” said Tony Hsieh, founder of Zappos. Inspired by this statement, I adopted WebSockets technology to provide real-time communication between the API and employees responsible for service handling. WebSockets enable bidirectional communication with support for real-time events. Events such as queue loading, changing service order, and removing a service were managed by the server using Golang’s native WebSocket library.
example of code:
package main
import (
"log"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{}
func handleWebSocket(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("Failed to upgrade to WebSocket:", err)
return
}
// Handle WebSocket communication and events
// ...
}
📊 Integrating Unstructured Data with Redis: To handle real-time unstructured data, I sought an agile and efficient solution. Event-Driven Microservices. When dealing with unstructured data, having an efficient caching storage layer is essential. Based on this principle, I integrated the Redis database into the microservices architecture, using it to store and manage data related to services. To ensure the security and efficiency of accessing Redis data, I developed a dedicated service called the Broker, which performed read, write, and delete operations in the Redis queue.
example of code:
package main
import (
"fmt"
"github.com/go-redis/redis"
)
func main() {
redisClient := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password set
DB: 0, // use default DB
})
// Example of writing data to Redis
err := redisClient.Set("key", "value", 0).Err()
if err != nil {
panic(err)
}
// Example of reading data from Redis
val, err := redisClient.Get("key").Result()
if err != nil {
panic(err)
}
fmt.Println("key:", val)
}
📨 Asynchronous Communication with RabbitMQ: To retrieve service data from a messaging service, I chose RabbitMQ as the asynchronous interface. As stated by Gregor Hohpe, author of “Enterprise Integration Patterns” “Asynchronous communication is an efficient strategy for integrating distributed systems.” Through a service called the consumer, my system connected to RabbitMQ, captured, analyzed, and filtered service requests, forwarding them to the Broker for insertion into the Redis queue. Thus, I established efficient and reliable communication between microservices.
example of code:
package main
import (
"log"
"os"
"os/signal"
"github.com/streadway/amqp"
)
func main() {
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
if err != nil {
log.Fatalf("Failed to connect to RabbitMQ: %v", err)
}
defer conn.Close()
ch, err := conn.Channel()
if err != nil {
log.Fatalf("Failed to open a channel: %v", err)
}
defer ch.Close()
// Consume messages from RabbitMQ
msgs, err := ch.Consume(
"queue_name",
"",
true,
false,
false,
false,
nil,
)
if err != nil {
log.Fatalf("Failed to register a consumer: %v", err)
}
forever := make(chan os.Signal, 1)
signal.Notify(forever, os.Interrupt)
// Process received messages
go func() {
for d := range msgs {
log.Printf("Received a message: %s", d.Body)
// Forward the message to the Broker for further processing
}
}()
log.Println("Waiting for messages...")
<-forever
}
🚀 Enhancing Efficiency with gRPC: “Efficiency is never accidental,” said Elon Musk, founder of Tesla. Inspired by this vision, I opted to use gRPC for communication between microservices. As mentioned by Brendan Burns, one of the creators of Kubernetes, in “Designing Distributed Systems” “gRPC is a high-performance, data-interchange communication framework.” By adopting gRPC, I could accelerate communication between microservices, optimizing performance and efficiency, in contrast to the REST API used for serving the front-end.
example of code:
package main
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestGetUsersHandler(t *testing.T) {
req, err := http.NewRequest("GET", "/users", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(getUsersHandler)
handler.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("Expected status code %v, but got %v", http.StatusOK, rr.Code)
}
expected := "Fetching users...\n"
if rr.Body.String() != expected {
t.Errorf("Expected response body %v, but got %v", expected, rr.Body.String())
}
}
🔧 Managing Deployment Lifecycle with ArgoCD: As mentioned by Jez Humble and David Farley in “Continuous Delivery: Reliable Software Releases through Build, Test, and Deployment Automation” an automated deployment cycle is essential to ensure agility and reliability in software development. To manage the deployment lifecycle of services, I utilized ArgoCD. This tool monitored a repository containing YAML configuration files of the services, allowing for automatic deployment to a staging environment whenever a new version was detected.
Here is an example of an ArgoCD Application manifest file:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
namespace: my-namespace
spec:
destination:
namespace: my-namespace
server: https://kubernetes.default.svc
source:
repoURL: https://github.com/my-repo
targetRevision: main
path: my-app
project: my-project
By defining the desired state of the application in the manifest file, ArgoCD automatically synchronized the deployment with the Kubernetes cluster, ensuring consistent and reliable application updates.
⚙️ Infrastructure as Code with Kubernetes: In the era of cloud computing, managing infrastructure as code has become crucial. Kubernetes, an industry-leading container orchestration platform, allows for declarative management of infrastructure resources. By defining Kubernetes manifests, I could describe the desired state of the system, including deployments, services, ingress, and more.
Here is an example of a Kubernetes Deployment manifest file:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
labels:
app: my-app
spec:
replicas: 3
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: my-app
image: my-registry/my-app:latest
ports:
- containerPort: 8080
With Kubernetes, I could easily manage and scale microservices, ensuring high availability and fault tolerance.
Diagram of architecture:
Fowler, M. (2002). Patterns of Enterprise Application Architecture. Addison-Wesley Professional.
Allspaw, J. (2017), The Art of Capacity Planning: Scaling Web Resources in the Cloud
Gregor H. (2003) Enterprise Integration Patterns
Farley, D.(2010) Continuous Delivery: Reliable Software Releases through Build, Test, and Deployment Automation