Aller au contenu
← Retour aux projets

Myapp

buymeacoffee

learning-cloud-native-go/myapp

learning-rust.github.io dumindu/axum E25DX

#Learning Cloud Native Go - myapp

#In this series,

We build a production-ready containerized RESTful API server application using following packages and tools.

  • Go Standard Library net/http: The most idiomatic way to write web API applications in Go.
  • Chi: The most idiomatic router with middleware and route groups support.
  • Zerolog: Zero Allocation JSON Logger and Faster than slog.
  • Goose: The database migration CLI builder library with lease dependency.
  • Gorm CLI: Generate Go generics-based, type safe, repository functions with no runtime wrappers.
  • Swag and Validator v10: The most prominent OpenAPI specification generator and validation library in Go.
  • Use of GitHub Actions to run linters and tests, and to build and push production images to the registry.
  • Use of GitOps with ArgoCD to automate declarative environment orchestration and application lifecycle management.

#Containerization Environment

Environment Go Image Type Go Image Size Postgres Image Type Postgres Image Size
Development golang:1.27-alpine ~ 800 MB postgres:18-alpine ~ 300MB
Production distroless/static-debian13:nonroot ~ 30 MB

#Endpoints

type Handler struct {
    validator *validator.Validate
    bookRepo  IBookRepo
}

func (h *Handler) Register(r chi.Router) {
    r.Get("/", h.list)
    r.With(m.Validate[Form](h.validator)).Post("/", h.create)
    r.Get("/{id}", h.read)
    r.With(m.Validate[Form](h.validator)).Put("/{id}", h.update)
    r.Delete("/{id}", h.delete)
}

// ...
Name HTTP Method Route
List Books GET /v1/books
Create Book POST /v1/books
Read Book GET /v1/books/{id}
Update Book PUT /v1/books/{id}
Delete Book DELETE /v1/books/{id}
Health GET /livez

#Request (POST/PUT)

{
  "title": "Harry Potter and the Deathly Hallows",
  "description": "It is the seventh and final novel in the Harry Potter series",
  "image_url": "https://upload.wikimedia.org/wikipedia/en/a/a9/Harry_Potter_and_the_Deathly_Hallows.jpg",
  "published_date": "2007-07-21",
  "status": "verified"
}

#Response (GET/POST/PUT)

{
  "id": "01bbbbbb-bbbb-7bbb-8bbb-bbbbbbbbbbbb",
  "created_at": "2027-01-01T00:00:00.123456Z",
  "updated_at": "2027-01-01T00:00:00.123456Z",
  "published_date": "2007-07-21",
  "title": "Harry Potter and the Deathly Hallows",
  "description": "It is the seventh and final novel in the Harry Potter series",
  "image_url": "https://upload.wikimedia.org/wikipedia/en/a/a9/Harry_Potter_and_the_Deathly_Hallows.jpg"
  "status": "verified",
}

[!note] The list endpoint returns an array of above response JSON.

#Form Validation

{
  "errors": {
    "image_url": "Must be a valid URL",
    "status": "Must be one of pending, verified",
    "title": "This field is required",
    "published_date": "Must be a valid date"
  }
}

#Database Design

To keep this simple, we use only a single database table named books.

Column Name Datatype Not Null Primary Key
created_at TIMESTAMPTZ
updated_at TIMESTAMPTZ
id UUID
published_date DATE
status SMALLINT
title TEXT
description TEXT
image_url TEXT

[!important]

  • For high-traffic systems with very large PostgreSQL tables that containing millions/billions of rows, arranging fixed-width columns by decreasing alignment requirements can reduce tuple alignment padding; potentially minimize row/ storage size. This technique is called "Column Tetris".
  • For this optimization, order fixed-width table columns by decreasing alignment requirements.
    • 8-byte alignment types: bigint, bigserial, double precision/ float8, timestamp, timestamptz, time, interval
    • 4-byte alignment types: integer, serial, real/ float4, uuid, date
    • 2-byte alignment types: smallint, smallserial
    • 1-byte alignment types: boolean
    • Variable-width types (at last): numeric, text, character varying/ varchar, bytea
  • However, it's ok to follow a more readable column format, when your table schema changes frequently.

#Just commands

MYAPP
    help               # List available commands
    install            # Install development tools
    app                # Run server app
    migration cmd="up" # Run DB migration CLI (defaults to up)
    build              # Run docker compose build
    up cmd=""          # Run docker compose up
    down               # Run docker compose down
    lint               # Run lints using gofumpt, go vet, staticcheck and govulncheck
    test               # Run tests
    gen                # Run go generate for all packages
    apidoc             # Generate openapi.yaml
    repos              # Generate gorm repositories using gorm cli

#Sample Request Logs

db-1  | 2018-01-10 01:00:00.000 +08 [1] LOG:  database system is ready to accept connections
Container myapp-db-1 Healthy
app-1  | 2018/01/10 01:00:00 OK   00001_create_books_table.sql (2.21ms)
app-1  | 2018/01/10 01:00:00 goose: successfully migrated database to version: 1
app-1  |
app-1  | {"level":"info","time":"2018-01-10T02:00:00+08:00","message":"Starting server :8080"}
app-1  |
app-1  | [7.218ms] [rows:1] INSERT INTO books (id, created_at, updated_at, title, published_date, image_url, description, status) VALUES ('38ba23d1-9565-40ed-b781-aacd2f84018d', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'Death Note', 'Light Yagami', '2006-10-04 00:00:00', 'https://static.wikia.nocookie.net/deathnote/images/9/94/A_Death_Note.jpg', 'A supernatural volume dropped into the human world by the Shinigami Ryuk', 0) RETURNING *
app-1  | {"level":"info","request_id":"d5mq7oi6hkls7397s43g","received_time":"2018-01-10T03:00:00+08:00","method":"POST","url":"/v1/books","header_size":135,"body_size":0,"agent":"yaak","referer":"","proto":"HTTP/1.1","remote_ip":"192.168.65.1","server_ip":"172.19.0.3","status":201,"resp_header_size":47,"resp_body_size":296,"latency":8.307,"time":"2018-01-10T03:00:00+08:00"}
app-1  | {"level":"info","request_id":"d5mq7oi6hkls7397s43g","id":"38ba23d1-9565-40ed-b781-aacd2f84018d","time":"2018-01-10T03:00:00+08:00","message":"new book created"}
app-1  |
app-1  | [2.541ms] [rows:1] SELECT * FROM books WHERE id = '38ba23d1-9565-40ed-b781-aacd2f84018d'
app-1  | {"level":"info","request_id":"d5mqa6a6hkls7397s44g","received_time":"2018-01-10T04:00:00+08:00","method":"GET","url":"/v1/books/38ba23d1-9565-40ed-b781-aacd2f84018d","header_size":82,"body_size":0,"agent":"yaak","referer":"","proto":"HTTP/1.1","remote_ip":"192.168.65.1","server_ip":"172.19.0.3","status":200,"resp_header_size":47,"resp_body_size":296,"latency":2.674625,"time":"2018-01-10T04:00:00+08:00"}
app-1  |
app-1  | [3.744ms] [rows:1] UPDATE books SET updated_at=CURRENT_TIMESTAMP, title='Death Note', published_date='2004-11-04', image_url='https://static.wikia.nocookie.net/deathnote/images/9/94/A_Death_Note.jpg', description='Light Yagami''s buried notebook', status=1 WHERE id = '38ba23d1-9565-40ed-b781-aacd2f84018d' RETURNING *
app-1  | {"level":"info","request_id":"d5mqesa6hkls7397s45g","id":"38ba23d1-9565-40ed-b781-aacd2f84018d","time":"2018-01-10T05:00:00+08:00","message":"book updated"}
app-1  | {"level":"info","request_id":"d5mqesa6hkls7397s45g","received_time":"2018-01-10T05:00:00+08:00","method":"PUT","url":"/v1/books/38ba23d1-9565-40ed-b781-aacd2f84018d","header_size":135,"body_size":0,"agent":"yaak","referer":"","proto":"HTTP/1.1","remote_ip":"192.168.65.1","server_ip":"172.19.0.3","status":200,"resp_header_size":47,"resp_body_size":252,"latency":4.018875,"time":"2018-01-10T05:00:00+08:00"}
app-1  |
app-1  | [3.035ms] [rows:1] DELETE FROM books WHERE id = '38ba23d1-9565-40ed-b781-aacd2f84018d' RETURNING true
app-1  | {"level":"info","request_id":"d5mqfgi6hkls7397s460","received_time":"2018-01-10T06:00:00+08:00","method":"DELETE","url":"/v1/books/38ba23d1-9565-40ed-b781-aacd2f84018d","header_size":82,"body_size":0,"agent":"yaak","referer":"","proto":"HTTP/1.1","remote_ip":"192.168.65.1","server_ip":"172.19.0.3","status":200,"resp_header_size":47,"resp_body_size":0,"latency":3.265,"time":"2018-01-10T06:00:00+08:00"}
app-1  | {"level":"info","request_id":"d5mqfgi6hkls7397s460","id":"38ba23d1-9565-40ed-b781-aacd2f84018d","time":"2018-01-10T06:00:00+08:00","message":"book deleted"}
app-1  |
app-1  | [2.573ms] [rows:1] SELECT * FROM books LIMIT 10 OFFSET 0
app-1  | {"level":"info","request_id":"d5mq9gi6hkls7397s440","received_time":"2018-01-10T07:00:00+08:00","method":"GET","url":"/v1/books","header_size":82,"body_size":0,"agent":"yaak","referer":"","proto":"HTTP/1.1","remote_ip":"192.168.65.1","server_ip":"172.19.0.3","status":200,"resp_header_size":47,"resp_body_size":298,"latency":2.926916,"time":"2018-01-10T07:00:00+08:00"}
app-1  |
app-1  |
app-1  | [1.661ms] [rows:0] DELETE FROM books WHERE id = '38ba23d1-9565-40ed-b781-aacd2f84018d' RETURNING true
app-1  | {"level":"info","request_id":"d5mrj8ppsdvs73dkfct0","received_time":"2018-02-01T01:00:00+08:00","method":"DELETE","url":"/v1/books/38ba23d1-9565-40ed-b781-aacd2f84018d","header_size":82,"body_size":0,"agent":"yaak","referer":"","proto":"HTTP/1.1","remote_ip":"192.168.65.1","server_ip":"172.19.0.3","status":404,"resp_header_size":47,"resp_body_size":0,"latency":1.80125,"time":"2018-02-01T01:00:00+08:00"}
app-1  | [1.384ms] [rows:0] UPDATE books SET updated_at=CURRENT_TIMESTAMP, title='Death Note', published_date='2004-11-04 00:00:00', image_url='https://static.wikia.nocookie.net/deathnote/images/9/94/A_Death_Note.jpg', description='Light Yagami''s buried notebook', status=1 WHERE id = '38ba23d1-9565-40ed-b781-aacd2f84018d' RETURNING *
app-1  | {"level":"info","request_id":"d5mqjmhqvtmc73foh3dg","received_time":"2018-02-02T08:00:00:00","method":"PUT","url":"/v1/books/38ba23d1-9565-40ed-b781-aacd2f84018d","header_size":135,"body_size":0,"agent":"yaak","referer":"","proto":"HTTP/1.1","remote_ip":"192.168.65.1","server_ip":"172.19.0.3","status":404,"resp_header_size":47,"resp_body_size":0,"latency":1.576,"time":"2018-02-02T08:00:00+08:00"}

// 💯 Real logs collected locally but with few rearrangements to make it easier to read.

#Folder Structure

#API Server

├── cmd
│   ├── app
│   │   └── main.go
│   └── migration
│       ├── main.go
│       ├── usage_embed.go
│       ├── usage_no_embed.go
│       └── migrations
│           └── 00001_create_books_table.sql
│
├── app
│   ├── book
│   │   ├── bookrepo  # 💡generated with gorm-cli via the interface in book/repository.go
│   │   │   └── repository.go
│   │   ├── form.go   # 💡validate via Go generics based form validation middleware
│   │   ├── handler.go
│   │   └── repository.go
│   └── router
│       └── router.go
│
├── model
│   ├── book.go
│   ├── book_status.go
│   └── date.go
│
├── config
│   └── config.go
│
├── pkg (middleware, validator, ctxutil, paramsutil, errors)
│
├── openapi.yaml
├── compose.yml
├── Dockerfile
└── prod.Dockerfile

#ArgoCD and Kustomize

└── k8s
    │
    ├── bootstrap
    │   ├── argocd
    │   └── argocd-config
    │       ├── clusters
    │       ├── projects
    │       └── applications
    │
    ├── platform
    │   ├── metrics-server
    │   ├── gateway-api
    │   ├── istio-ambient
    │   └── cloudnative-pg
    │
    ├── components
    │   └── myapp-db
    ├── services
    │   ├── base
    │   │   └── myapp
    │   └── overlays
    │       ├── dev
    │       ├── prod
    │       └── stage
    │
    └── gateways

[!tip] Sample Kind Dev Cluster

kind create cluster --name dev
kubectl apply -k k8s/bootstrap/argocd
kubectl apply -k k8s/bootstrap/argocd-config
kubectl apply -k k8s/platform/istio-ambient
kubectl apply -k k8s/platform/gateway-api
kubectl apply -k k8s/gateways
kubectl port-forward svc/shared-gateway-dev-istio -n istio-ingress 8081:8081 # 💡 Shared Dev Gateway
curl -X GET 'localhost:8081/myapp/v1/books' --header 'Accept: application/json'

kubectl port-forward svc/argocd-server -n argocd 8080:443 # 💡 ArgoCD Dashboard(admin/password)

Nouvelle version disponible.