DEV Community

drajatisme
drajatisme

Posted on

Microservice menggunakan Go dan gRPC

Tutorial ini akan menjelaskan cara untuk membuat aplikasi dengan arsitektur microservice. Disini kita akan membuat 2 service, yaitu:

  1. account-service (Register dan Authenticate)
  2. log-service (WriteLog)

Service yang akan kita buat hanya akan menampilkan log pada console, agar kode menjadi sederhana, sehingga lebih memudahkan dalam memahami konsep microservice.

Saya berasumsi pada laptop/komputer kalian telah terinstall golang, protobuf, text editor, postman, dan docker. Mari kita mulai.

account-service

Buat folder account-service, lalu masuk ke dalam folder tersebut. jalankan perintah dibawah untuk menginisialisasi go modul.

go mod init account-service
Enter fullscreen mode Exit fullscreen mode

Buat folder cmd/api, lalu buat file main.go di dalam folder tersebut. Buat konstanta bertipe string dengan nama webPort dengan value 80. Lalu buat struct bernama Config.

const webPort = "80"

type Config struct{}
Enter fullscreen mode Exit fullscreen mode

Selanjutnya buat fungsi main untuk menjalankan server.

log.Println("starting account service")

app := Config{}

// configure server 
srv := &http.Server{
    Addr:    fmt.Sprintf(":%s", webPort),
    Handler: app.routes(),
}

// start server 
err := srv.ListenAndServe()
if err != nil {
    log.Panic(err)
}
Enter fullscreen mode Exit fullscreen mode

Kode lengkapnya akan menjadi seperti di bawah.

package main

import (
    "fmt"
    "log"
    "net/http"
)

const webPort = "80"

type Config struct{}

func main() {
    log.Println("starting account service")

    app := Config{}

    // configure server 
    srv := &http.Server{
        Addr:    fmt.Sprintf(":%s", webPort),
        Handler: app.routes(),
    }

    // start server 
    err := srv.ListenAndServe()
    if err != nil {
        log.Panic(err)
    }
} 
Enter fullscreen mode Exit fullscreen mode

Jalankan perintah dibawah untuk mendownload package yang diperlukan untuk mengkonfigurasi endpoint.

go get github.com/go-chi/chi/v5
go get github.com/go-chi/cors
Enter fullscreen mode Exit fullscreen mode

Selanjutnya kita buat file routes.go di dalam folder cmd/api. Buat fungsi route dengan nilai balikan http.Handler. Masukan kode untuk mengkonfigurasi router.

mux := chi.NewRouter()

mux.Use(cors.Handler(cors.Options{
    AllowedOrigins:   []string{"http://*", "http://*"},
    AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
    AllowedHeaders:   []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
    AllowCredentials: true,
    MaxAge:           300,
}))
Enter fullscreen mode Exit fullscreen mode

Dan juga kode untuk membuat endpoint. Kita akan membuat 2 endpoint, yaitu authenticate dan register.

// configure endpoint
mux.Post("/authenticate", app.Authenticate)
mux.Post("/register", app.Register)
Enter fullscreen mode Exit fullscreen mode

Kode lengkapnya akan menjadi seperti di bawah.

package main

import (
    "net/http"

    "github.com/go-chi/chi/v5"
    "github.com/go-chi/cors"
)

func (app *Config) routes() http.Handler {
    // configure router 
    mux := chi.NewRouter()

    mux.Use(cors.Handler(cors.Options{
        AllowedOrigins:   []string{"http://*", "http://*"},
        AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
        AllowedHeaders:   []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
        AllowCredentials: true,
        MaxAge:           300,
    }))

    // configure endpoint
    mux.Post("/authenticate", app.Authenticate)
    mux.Post("/register", app.Register)

    return mux
}
Enter fullscreen mode Exit fullscreen mode

Selanjutnya kita buat file helper.go di dalam folder cmd/api. Buat kode untuk mendefinisikan struct bernama jsonResponse.

type jsonResponse struct {
    Error   bool   `json:"error"`
    Message string `json:"message"`
    Data    any    `json:"data,omitempty"`
}
Enter fullscreen mode Exit fullscreen mode

Buat fungsi readJson dengan receiver Config untuk mengubah json dari request menjadi value pada go.

func (app *Config) readJSON(w http.ResponseWriter, r *http.Request, data any) error {
    maxBytes := 1048576 // 1MB

    r.Body = http.MaxBytesReader(w, r.Body, int64(maxBytes))

    dec := json.NewDecoder(r.Body)
    err := dec.Decode(data)
    if err != nil {
        return err
    }

    err = dec.Decode(&struct{}{})
    if err != io.EOF {
        return errors.New("body must have only a single JSON value")
    }

    return nil
}
Enter fullscreen mode Exit fullscreen mode

Buat fungsi writeJson dengan receiver Config untuk mengeset header untuk balikan endpoint.

func (app *Config) writeJSON(w http.ResponseWriter, status int, data any, headers ...http.Header) error {
    out, err := json.Marshal(data)
    if err != nil {
        return err
    }

    if len(headers) > 0 {
        for key, value := range headers[0] {
            w.Header()[key] = value
        }
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    _, err = w.Write(out)
    if err != nil {
        return err
    }

    return nil
}
Enter fullscreen mode Exit fullscreen mode

Buat fungsi errorJson dengan receiver Config untuk mengeset header untuk balikan endpoint yang error.

func (app *Config) errorJSON(w http.ResponseWriter, err error, status ...int) error {
    statusCode := http.StatusBadRequest

    if len(status) > 0 {
        statusCode = status[0]
    }

    var payload jsonResponse
    payload.Error = true
    payload.Message = err.Error()

    return app.writeJSON(w, statusCode, payload)
}
Enter fullscreen mode Exit fullscreen mode

Kode lengkapnya akan menjadi seperti di bawah.

package main

import (
    "encoding/json"
    "errors"
    "io"
    "net/http"
)

type jsonResponse struct {
    Error   bool   `json:"error"`
    Message string `json:"message"`
    Data    any    `json:"data,omitempty"`
}

func (app *Config) readJSON(w http.ResponseWriter, r *http.Request, data any) error {
    maxBytes := 1048576 // 1MB

    r.Body = http.MaxBytesReader(w, r.Body, int64(maxBytes))

    dec := json.NewDecoder(r.Body)
    err := dec.Decode(data)
    if err != nil {
        return err
    }

    err = dec.Decode(&struct{}{})
    if err != io.EOF {
        return errors.New("body must have only a single JSON value")
    }

    return nil
}

func (app *Config) writeJSON(w http.ResponseWriter, status int, data any, headers ...http.Header) error {
    out, err := json.Marshal(data)
    if err != nil {
        return err
    }

    if len(headers) > 0 {
        for key, value := range headers[0] {
            w.Header()[key] = value
        }
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    _, err = w.Write(out)
    if err != nil {
        return err
    }

    return nil
}

func (app *Config) errorJSON(w http.ResponseWriter, err error, status ...int) error {
    statusCode := http.StatusBadRequest

    if len(status) > 0 {
        statusCode = status[0]
    }

    var payload jsonResponse
    payload.Error = true
    payload.Message = err.Error()

    return app.writeJSON(w, statusCode, payload)
}
Enter fullscreen mode Exit fullscreen mode

Selanjutnya kita buat file handler.go di dalam folder cmd/api. File ini berisi kode untuk menghandle logic endpoint. Pertama kita buat struct dengan nama requestPayload.

var requestPayload struct {
    Email    string `json:"email"`
    Password string `json:"password"`
}
Enter fullscreen mode Exit fullscreen mode

Lalu, kita buat fungsi Authentication dengan receiver Config.

func (app *Config) Authenticate(w http.ResponseWriter, r *http.Request) {
    err := app.readJSON(w, r, &requestPayload)
    if err != nil {
        app.errorJSON(w, err, http.StatusBadRequest)
        return
    }

    payload := jsonResponse{
        Error:   false,
        Message: fmt.Sprintf("Logged in user %s", requestPayload.Email),
        Data:    requestPayload,
    }

    log.Println(payload.Message)
    app.writeJSON(w, http.StatusAccepted, payload)
}
Enter fullscreen mode Exit fullscreen mode

Dan juga fungsi Register dengan receiver Config.

func (app *Config) Register(w http.ResponseWriter, r *http.Request) {
    err := app.readJSON(w, r, &requestPayload)
    if err != nil {
        app.errorJSON(w, err, http.StatusBadRequest)
        return
    }

    payload := jsonResponse{
        Error:   false,
        Message: fmt.Sprintf("%s registered", requestPayload.Email),
        Data:    requestPayload,
    }

    log.Println(payload.Message)
    app.writeJSON(w, http.StatusAccepted, payload)
}
Enter fullscreen mode Exit fullscreen mode

Kode lengkapnya akan menjadi seperti di bawah.

package main

import (
    "fmt"
    "log"
    "net/http"
)

var requestPayload struct {
    Email    string `json:"email"`
    Password string `json:"password"`
}

func (app *Config) Authenticate(w http.ResponseWriter, r *http.Request) {
    err := app.readJSON(w, r, &requestPayload)
    if err != nil {
        app.errorJSON(w, err, http.StatusBadRequest)
        return
    }

    payload := jsonResponse{
        Error:   false,
        Message: fmt.Sprintf("Logged in user %s", requestPayload.Email),
        Data:    requestPayload,
    }

    log.Println(payload.Message)
    app.writeJSON(w, http.StatusAccepted, payload)
}

func (app *Config) Register(w http.ResponseWriter, r *http.Request) {
    err := app.readJSON(w, r, &requestPayload)
    if err != nil {
        app.errorJSON(w, err, http.StatusBadRequest)
        return
    }

    payload := jsonResponse{
        Error:   false,
        Message: fmt.Sprintf("%s registered", requestPayload.Email),
        Data:    requestPayload,
    }

    log.Println(payload.Message)
    app.writeJSON(w, http.StatusAccepted, payload)
}
Enter fullscreen mode Exit fullscreen mode

Terakhir, buat file account-service.dockerfile di dalam folder account-service. Masukan script seperti di bawah.

# build
FROM alpine:latest

RUN mkdir /app

COPY accountApp /app

CMD ["/app/accountApp"]
Enter fullscreen mode Exit fullscreen mode

Docker

Buat folder yang sejajar dengan account-service dengan nama project, lalu buat file docker-compose.yml dan Makefile.

Untuk docker-compose.yml masukan script seperti di bawah.

version: '3'

services: 

  account-service:
    build:
      context: ./../account-service
      dockerfile: ./../account-service/account-service.dockerfile
    restart: always
    ports:
      - "8080:80"
    deploy:
      mode: replicated
      replicas: 1
Enter fullscreen mode Exit fullscreen mode

Untuk Makefile masukan script seperti di bawah.

ACCOUNT_BINARY=accountApp

## up: starts all containers in the background without forcing build
up:
    @echo "Starting Docker images..."
    docker-compose up -d
    @echo "Docker images started!"

## up_build: stops docker-compose (if running), builds all projects and starts docker compose
up_build: build_account
    @echo "Stopping docker images (if running...)"
    docker-compose down
    @echo "Building (when required) and starting docker images..."
    docker-compose up --build -d
    @echo "Docker images built and started!"

## down: stop docker compose
down:
    @echo "Stopping docker compose..."
    docker-compose down
    @echo "Done!"

## build_account: builds the account binary as a linux executable
build_account:
    @echo "Building account binary..."
    cd ../account-service && env GOOS=linux CGO_ENABLED=0 go build -o ${ACCOUNT_BINARY} ./cmd/api
    @echo "Done!"
Enter fullscreen mode Exit fullscreen mode

Masuk ke dalam folder project, lalu jalankan perintah untuk menjalankan container docker.

make up_build
Enter fullscreen mode Exit fullscreen mode

docker-project-1

Kita tes API dengan menggunakan Postman. Hit url http://localhost:8080/register dan http://localhost:8080/authenticate. Jika berhasil maka hasilnya akan seperti gambar di bawah.

postman-result-1

postman-result-2

Dan pada docker log akan muncul log dari account-service.
docker-project-log-1

Service account-service sudah selesai dibuat. Kita lanjut ke service berikutnya.

log-service

Buat folder log-service sejajar dengan account-service. Jalankan perintah untuk menginisialisasi modul.

go mod init log-service
Enter fullscreen mode Exit fullscreen mode

Buat folder cmd/api, lalu buat file main.go di dalam folder tersebut. Buat konstanta bertipe string dengan nama gRpcPort dengan value 50001. Lalu buat struct bernama Config.

const webPort = "50001"

type Config struct{}
Enter fullscreen mode Exit fullscreen mode

Selanjutnya buat fungsi main untuk menjalankan server.

func main() {

    app := Config{}

    go app.gRPCListen()
}
Enter fullscreen mode Exit fullscreen mode

Kode lengkapnya akan menjadi seperti di bawah.

package main

const (
    gRpcPort = "50001"
)

type Config struct{}

func main() {
    app := Config{}

    go app.gRPCListen()
}
Enter fullscreen mode Exit fullscreen mode

Buat folder proto di dalam log-service, lalu buat file logs.proto. Kode dibawah berfungsi untuk mendefiniskan versi dari sintaks proto.

syntax = "proto3";
Enter fullscreen mode Exit fullscreen mode

Kode dibawah berfungsi untuk mendefinisikan nama package pada file hasil generate.

package proto;

option go_package = "/proto";
Enter fullscreen mode Exit fullscreen mode

Kode di bawah berfungsi untuk mendefinisikan model atau tipe data.

message Log{
    string name = 1;
    string data = 2;
}

message LogRequest{
    Log logEntry = 1;
}

message LogResponse {
    string result = 1;
}
Enter fullscreen mode Exit fullscreen mode

Dan kode yang terakhir berfungsi untuk mendefinisikan method yang akan di consume oleh service yang lain.

service LogService {
    rpc WriteLog(LogRequest) returns (LogResponse);
}
Enter fullscreen mode Exit fullscreen mode

Kode lengkapnya akan menjadi seperti di bawah.

syntax = "proto3";

package proto;

option go_package = "/proto";

message Log{
    string name = 1;
    string data = 2;
}

message LogRequest{
    Log logEntry = 1;
}

message LogResponse {
    string result = 1;
}

service LogService {
    rpc WriteLog(LogRequest) returns (LogResponse);
}
Enter fullscreen mode Exit fullscreen mode

Masuk ke dalam folder proto, jalankan perintah untuk menggenerate kode RPC. Dengan menjalankan perintah tersebut, protobuf akan menggenerate 2 file.

protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative logs.proto 
Enter fullscreen mode Exit fullscreen mode

generate-proto-1

Update account-service

Buat folder proto di dalam folder account-service. lalu buat file logs.proto di dalam folder tersebut. Untuk mempersingkat waktu, salin seluruh code pada file log-service/proto/logs.proto ke file account-service/proto/logs.proto, dan jalankan perintah generate kode RPC seperti di atas.

Buat folder services di dalam folder account-service, dan buat file log-service.go. Buat struct bernama LogService.

type LogService struct{}
Enter fullscreen mode Exit fullscreen mode

Buat fungsi dengan receiver LogService bernama connect. Fungsi ini akan mengkoneksikan ke service log.

func (s *LogService) connect() (*grpc.ClientConn, error) {
    conn, err := grpc.Dial(
        "log-service:50001",
        grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock(),
    )
    if err != nil {
        return nil, err
    }

    return conn, nil
}
Enter fullscreen mode Exit fullscreen mode

Buat fungsi dengan receiver LogService bernama LogItemViaGRPC. Fungsi inilah yang akan dipanggil ketika akan melakukan hit ke service log.

func (s *LogService) LogItemViaGRPC(message string) error {
    conn, err := s.connect()
    if err != nil {
        return err
    }
    defer conn.Close()

    c := proto.NewLogServiceClient(conn)
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()

    _, err = c.WriteLog(ctx, &proto.LogRequest{
        LogEntry: &proto.Log{
            Name: "account-service",
            Data: message,
        },
    })
    if err != nil {
        return err
    }

    return nil
}
Enter fullscreen mode Exit fullscreen mode

Tambahkan kode pada handler Authenticate dan Register tepat sebelum app.WriteJson di panggil.

srv := services.LogService{}
srv.LogItemViaGRPC(payload.Message)
Enter fullscreen mode Exit fullscreen mode

Sehingga kode pada cmd/api/handler.go akan menjadi seperti di bawah.

package main

import (
    "account-service/services"
    "fmt"
    "log"
    "net/http"
)

var requestPayload struct {
    Email    string `json:"email"`
    Password string `json:"password"`
}

func (app *Config) Authenticate(w http.ResponseWriter, r *http.Request) {
    err := app.readJSON(w, r, &requestPayload)
    if err != nil {
        app.errorJSON(w, err, http.StatusBadRequest)
        return
    }

    payload := jsonResponse{
        Error:   false,
        Message: fmt.Sprintf("Logged in user %s", requestPayload.Email),
        Data:    requestPayload,
    }

    log.Println(payload.Message)
    srv := services.LogService{}
    srv.LogItemViaGRPC(payload.Message)
    app.writeJSON(w, http.StatusAccepted, payload)
}

func (app *Config) Register(w http.ResponseWriter, r *http.Request) {
    err := app.readJSON(w, r, &requestPayload)
    if err != nil {
        app.errorJSON(w, err, http.StatusBadRequest)
        return
    }

    payload := jsonResponse{
        Error:   false,
        Message: fmt.Sprintf("%s registered", requestPayload.Email),
        Data:    requestPayload,
    }

    log.Println(payload.Message)
    srv := services.LogService{}
    srv.LogItemViaGRPC(payload.Message)
    app.writeJSON(w, http.StatusAccepted, payload)
}
Enter fullscreen mode Exit fullscreen mode

Docker

Tambahkan script pada file project/Makefile

LOG_BINARY=logApp

build_log:
    @echo "Building log binary..."
    cd ../log-service && env GOOS=linux CGO_ENABLED=0 go build -o ${LOG_BINARY} ./cmd/api
    @echo "Done!"
Enter fullscreen mode Exit fullscreen mode

Update script up_build.

up_build: build_account build_log
Enter fullscreen mode Exit fullscreen mode

Sehingga scriptnya akan menjadi seperti di bawah.

ACCOUNT_BINARY=accountApp
LOG_BINARY=logApp

## up: starts all containers in the background without forcing build
up:
    @echo "Starting Docker images..."
    docker-compose up -d
    @echo "Docker images started!"

## up_build: stops docker-compose (if running), builds all projects and starts docker compose
up_build: build_account build_log build_mail
    @echo "Stopping docker images (if running...)"
    docker-compose down
    @echo "Building (when required) and starting docker images..."
    docker-compose up --build -d
    @echo "Docker images built and started!"

## down: stop docker compose
down:
    @echo "Stopping docker compose..."
    docker-compose down
    @echo "Done!"

## build_account: builds the account binary as a linux executable
build_account:
    @echo "Building account binary..."
    cd ../account-service && env GOOS=linux CGO_ENABLED=0 go build -o ${ACCOUNT_BINARY} ./cmd/api
    @echo "Done!"

## build_log: builds the log binary as a linux executable
build_log:
    @echo "Building log binary..."
    cd ../log-service && env GOOS=linux CGO_ENABLED=0 go build -o ${LOG_BINARY} ./cmd/api
    @echo "Done!"
Enter fullscreen mode Exit fullscreen mode

Terakhir tambahkan script pada file docker-compose.yml

  log-service:
    build:
      context: ./../log-service
      dockerfile: ./../log-service/log-service.dockerfile
    restart: always
    ports:
      - "8081:80"
    deploy:
      mode: replicated
      replicas: 1
Enter fullscreen mode Exit fullscreen mode

Masuk ke folder project, lalu jalankan perintah untuk menjalankan container docker.

make up_build
Enter fullscreen mode Exit fullscreen mode

docker-project-2

Kita tes microservice yang telah kita buat. Hit url register dan authenticate menggunakan postman. Jika log pada docker terdapat log dari masing-masing service, maka kita telah berhasil membuat 2 service saling terhubung.

docker-project-log-2

Penutup

Kita telah membuat 2 service dan membuat account-service memanggil log-service dengan menggunakan gRPC. Bagaimana menurut kalian?

Top comments (0)