Tutorial ini akan menjelaskan cara untuk membuat aplikasi dengan arsitektur microservice. Disini kita akan membuat 2 service, yaitu:
- account-service (Register dan Authenticate)
- 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
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{}
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)
}
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)
}
}
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
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,
}))
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)
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
}
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"`
}
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
}
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
}
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)
}
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)
}
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"`
}
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)
}
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)
}
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)
}
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"]
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
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!"
Masuk ke dalam folder project, lalu jalankan perintah untuk menjalankan container docker.
make up_build
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.
Dan pada docker log akan muncul log dari account-service.
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
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{}
Selanjutnya buat fungsi main untuk menjalankan server.
func main() {
app := Config{}
go app.gRPCListen()
}
Kode lengkapnya akan menjadi seperti di bawah.
package main
const (
gRpcPort = "50001"
)
type Config struct{}
func main() {
app := Config{}
go app.gRPCListen()
}
Buat folder proto di dalam log-service, lalu buat file logs.proto. Kode dibawah berfungsi untuk mendefiniskan versi dari sintaks proto.
syntax = "proto3";
Kode dibawah berfungsi untuk mendefinisikan nama package pada file hasil generate.
package proto;
option go_package = "/proto";
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;
}
Dan kode yang terakhir berfungsi untuk mendefinisikan method yang akan di consume oleh service yang lain.
service LogService {
rpc WriteLog(LogRequest) returns (LogResponse);
}
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);
}
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
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{}
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
}
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
}
Tambahkan kode pada handler Authenticate dan Register tepat sebelum app.WriteJson di panggil.
srv := services.LogService{}
srv.LogItemViaGRPC(payload.Message)
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)
}
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!"
Update script up_build.
up_build: build_account build_log
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!"
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
Masuk ke folder project, lalu jalankan perintah untuk menjalankan container docker.
make up_build
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.
Penutup
Kita telah membuat 2 service dan membuat account-service memanggil log-service dengan menggunakan gRPC. Bagaimana menurut kalian?
Top comments (0)