[GCP Cloud Run] ມາລອງ Hello CHOCO ດ້ວຍ Cloud Run ກັນເຖາະ

Pao Phouminith
LaoITDev
Published in
7 min readFeb 19, 2023
Her name is CHOCO

ບົດຄວາມນີ້ຈະແນະນຳຂັ້ນຕອນການ Deploy API To Google Cloud Run. ຈະໃຊ້ພາສາ Go ສຳຫລັບຂຽນ API Hello CHOCO. (source code here)

What is Cloud Run? (ແບບສັ້ນໆ)

Cloud Run is a managed compute platform that enables you to run containers that are invocable via requests or events. Cloud Run is serverless: it abstracts away all infrastructure management, so you can focus on what matters most — building great applications. (reference)

Cloud Run ເປັນ compute platform ແບບ Serverless ທີ່ Auto scaling Up-Down ໄດ້ຕາມຈຳນວນ Traffic ທີ່ເຂົ້າ Service ນັ້ນໆ. ເຊິ່ງແນ່ນອນການທີ່ Auto scaling Up-Down ແບບນີ້ໄດ້ Service ທີ່ເຮົາຈະເອົາຂື້ນໄປ Run ກໍຕ້ອງໄດ້ອອກແບບໃຫ້ເປັນ Stateless ຄືກັນ.

ຂັ້ນຕອນ

ຂັ້ນຕອນທີ 1: ສ້າງ Project ດ້ວຍ Google Cloud Console

  • ເຂົ້າໄປທີ່ Google Cloud Console (console.cloud.google.com) ແລະ ລົງຊື່ເຂົ້າໃຊ້(ຖ້າຫາກຜູ້ໃຊ້ຍັງບໍ່ມີບັນຊີແມ່ນສາມາດສ້າງບັນຊີເຂົ້າໃຊ້ງານໄດ້ເລີຍ).
  • ຢູ່ສ່ວນເທິງຂອງເມນູໃຫ້ກົດທີ່ເມນູ project drop-down
  • ແລ້ວເລືອກ “NEW PROJECT
  • ເມື່ອກົດປຸ່ມ “NEW PROJECT” ຈະໄດ້ຫນ້າສຳຫລັບການສ້າງ project

Project Name ແມ່ນຊື່ຂອງ project ທີ່ຈະໃຊ້ (ເຮົາສາມາດກຳນົດ Project ID ເອງໄດ້ ແຕ່ຈະບໍ່ສາມາດປ່ຽນໄດ້ຫລັງຈາກ project ນັ້ນຖືກສ້າງ).

Note: Project ID ຈະຕ້ອງເປັນ Unique ບໍ່ສາມາດຊ້ຳກັບຂອງຄົນອື່ນໄດ້.

ບາງ Account ຈະມີ Billing Account ມາໃຫ້ເລືອກໃນຕອນທີ່ສ້າງ project. ສາມາດເລືອກ Billing Account ທີ່ເຮົາຈະເປີດໄວ້ກັບ project ນັ້ນໄດ້ເລີຍ ແຕ່ຖ້າຍັງບໍ່ມີ Billing Account ກໍສາມາດກົດສ້າງໄດ້.

Location ເປັນ Parent organization ຫລື folder ທີ່ project ຈະຖືກສ້າງຂື້ນໃນນັ້ນ. ໃນບົດຄວາມນີ້ຈະເລືອກເປັນ No organization.

  • ເມື່ອປ້ອນຂໍ້ມູນຄົບແລ້ວກໍກົດປຸ່ມ “Create”

ຫລັງຈາກທີ່ເຮົາສ້າງ proejct ແລະ ເປີດ billing account ແລ້ວກໍໄປຕໍ່ທີ່ຂັ້ນຕອນຕໍ່ໄປກັນເລີຍ

ຂັ້ນຕອນທີ 2: ສ້າງ Repository ຢູ່ Artifact Registry

Artifact Registry ແມ່ນຫຍັງ?

ມັນແມ່ນບ່ອນຈັດເກັບ ແລະ ຈັດການ Container Image ຫລື Software Package ໃນ Registry. ໂດຍທົ່ວໄປແລ້ວ Container Images ຈະໃຊ້ເພື່ອເຮັດ package ແລະ deploy application ໃນຮູບແບບຂອງ Containerized Enviroment. ສຳຫລັບສ່ວນຂອງ Software Packages ຈະໃຊ້ເພື່ອ distribute ແລະ install Software Dependencies.

ເຮົາມາເລີ່ມຕົ້ນສ້າງ Repository ກັນດີກວ່າ

  • ເລີ່ມຈາກກົດເລືອກຫາເມນູທີ່ຊື່ວ່າ “Artifact Registry” ເຊິ່ງມັນຈະຢູ່ໃນ section ຂອງ CI/CD
  • ກົດ “Create Repository”
  • Congifure Repository ທີ່ເຮົາຈະສ້າງເຊັ່ນຊື່, Format, Mode, Location type ແລະ Encryption

ໃນບົດຄວາມນີ້ຜູ້ຂຽນຈະຕັ້ງຊື່ Repository ວ່າ hal0kitty ແລະ ມີ format ເປັນ docker. (ເຮົາສະໃຊ້ docker ໃນການ build container image)

  • ກົດ “Create” ເພື່ອສ້າງ Repo.

ຫລັງຈາກທີ່ສ້າງສຳເລັດກໍຈະໄດ້ Repo ເປົ່າໆດັ່ງຮູບພາບດ້ານເທິງ.

ກໍຖືວ່າສຳເລັດສຳຫລັບການສ້າງ Repo ເພື່ອໄວ້ເກັບ Container Images ທີ່ເຮົາຈະໃຊ້ສຳຫລັບການ deploy ຂັ້ນຕອນຕໍ່ໄປຈະເປັນການ build container image ດ້ວຍ docker.

ຂັ້ນຕອນທີ 3: Build Container Image ດ້ວຍ Docker

ກ່ອນທີ່ເຮົາຈະ build ເຮົາກໍຕ້ອງມາສ້າງ API ຂອງເຮົາຂື້ນມາກ່ອນ ຜູ້ຂຽນຈະໃຊ້ Golang ສຳຫລັບສ້າງ API Hello CHOCO.

  • ເລີ່ມສ້າງ Go project ໂດຍ “go mod init hallochoco”
  • ແລ້ວກໍສ້າງ file ທີ່ມີຊື່ວ່າ main.go ເຊິ່ງພາຍໃນ file ຈະປະກອບໄປດ້ວຍ code ລຸ່ມນີ້:
package main

import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"time"

"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)

func getEnv(key, fallback string) string {
if value := os.Getenv(key); value != "" {
return value
}
return fallback
}

func main() {
ctx := context.Background()
e := echo.New()

e.Use(middleware.Logger())
e.Use(middleware.Recover())

e.GET("/", func(c echo.Context) error {
return c.JSON(http.StatusOK, echo.Map{
"message": "Hello CHOCO, this is a message from Golang running on CloudRun.",
})
})

errCh := make(chan error, 1)
go func() {
errCh <- e.Start(fmt.Sprintf(":%s", getEnv("PORT", "3001")))
}()

ctx, cancel := signal.NotifyContext(ctx, os.Kill, os.Interrupt)
defer cancel()

select {
case err := <-errCh:
if err != nil && err != http.ErrServerClosed {
log.Fatal("failed to start server")
}
log.Println("server shutdown gracefully")

case <-ctx.Done():
ctx, cancel := context.WithTimeout(ctx, time.Second*15)
defer cancel()

log.Println("shutting down server...")
if err := e.Shutdown(ctx); err != nil {
log.Fatal("failed to shutdown server")
}
log.Println("shutdown server gracefully")
}
}

ເຮົາຈະສ້າງ API ໃຫ້ໄດ້ Response ຕາມຕົວຢ່າງລຸ່ມນີ້ເມື່ອມີຄົນ Request ເຂົ້າມາທີ່ API.

{
"message": "Hello CHOCO, this is a message from Golang running on CloudRun."
}
  • ສ້າງ Dockerfile
# Start from golang base image as building stage
FROM golang:1.19-alpine as builder

# Set necessary environment variables needed for our image
ENV GO111MODULE=on CGO_ENABLED=0 GOOS=linux GOARCH=amd64

# Set the current working directory inside the container
WORKDIR /build

# Copy and download dependency using go mod
COPY go.mod .
COPY go.sum .
RUN apk add --no-cache ca-certificates git tzdata && \
go mod tidy

# Copy the code into the container
COPY . .

# Build the Go application
RUN go build -ldflags "-s -w -extldflags '-static'" -installsuffix cgo -o /bin/service main.go

# Use alpine image as runtime
FROM alpine:3.16 as release

COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /bin/service /bin/service

# Command to run
ENTRYPOINT ["/bin/service"]%
  • ສັ່ງ build container image
docker build -t {region}-docker.pkg.dev/{projectName}/{repositoryName}/{imageName}:{tag} .

- region ຈະຂື້ນກັບ Location type ທີ່ເຮົາເລືອກຕອນສ້າງ Repo
- projectName
ແມ່ນຊື່ຂອງ project ທີ່ເຮົາສ້າງໄວ້ຢູ່ Google Cloud
- repositoryName ແມ່ນຊື່ຂອງ Repo ທີ່ເຮົາສ້າງໄວ້ໃນ Artifact Registry
- imageName ແມ່ນຊື່ image ທີ່ເຮົາຈະຕັ້ງໃຫ້
- tag ແມ່ນ tag ທີ່ເຮົາຈະລະບຸໃຫ້ກັບ image ຕົວດັ່ງກ່າວເຊັ່ນ ເລກເວີເຊິນຂອງ image

ເຮົາສາມາດເຂົ້າໄປເອົາຂື້ນມູນສ່ວນນີ້ໄດ້ຈາກ Repo ທີ່ເຮົາສ້າງໄວ້ໄດ້ເລີຍ

ພຽງແຕ່ເຮົາກົດ “Copy” ເຮົາກໍຈະໄດ້ຂໍ້ມູນສ່ວນດັ່ງກ່າວ

ຄຳສັ່ງຂອງຜູ້ຂຽນກໍຈະໄດ້ອອກມາດັ່ງນີ້:

docker build -t asia-southeast1-docker.pkg.dev/phuangpheth/hal0kiity/api:v1.0.0 .

ເມື່ອກົດ Enter ຂະບວນການ build ກໍຈະດຳເນີນ

ຫລັງຈາກ build ສຳເລັດເຮົາກໍຈະໄດ້ image ຕົວຫນຶ່ງທີ່ເຮົາຈະໃຊ້ push ຂື້ນໄປ repo ຂອງເຮົາ.

  • Push container image ຂື້ນ Artifact Registry
docker push {region}-docker.pkg.dev/{projectName}/{repositoryName}/{imageName}:{tag}

ກໍລະນີທີ່ເກີດ error ບໍ່ສາມາດ push ຂື້ນ Repo ທີ່ສ້າງໄວ້ໄດ້ທີ່ເປັນ error permission denied ຫລື It may not exist ອາດຈະເປັນຍ້ອນເຮົາຍັງບໍ່ໄດ້ config auth. ສາມາດໃຊ້ຄຳສັ່ງລຸ່ມນີ້ເພື່ອແກ້ໄຂ

gcloud auth configure-docker \                                             
{region}-docker.pkg.dev

ສຳຫລັບ region ແມ່ນໃຫ້ໃສ່ຕາມທີ່ເຮົາເລືອກໄວ້ໃນຕອນທີ່ເຮົາສ້າງ Repository.

ຄຳສັ່ງຂອງຜູ້ຂຽນກໍຈະໄດ້ອອກມາດັ່ງນີ້:

docker push asia-southeast1-docker.pkg.dev/phuangpheth/hal0kiity/api:v1.0.0 

ເມື່ອກົດ push ສຳເລັດກໍໄດ້ດັ່ງນີ້:

ຫລັງຈາກ push ຂື້ນສຳເລັດເຮົາມາກວດເບິ່ງ Repo ຂອງເຮົາຢູ່ Google Cloud Console ເຮົາກໍເຫັນ Image ທີ່ເຮົາໄດ້ push ຂື້ນ

ແລະ ເມື່ອກົດໃສ່ Image ເຮົາກໍໄດ້ຫນ້າສະແດງລາຍລະອຽດຂອງ image ຕົວດັ່ງກ່າວ

ເຊິ່ງໃນລາຍລະອຽດຈະສະແດງວ່າ image ຂອງເຮົານັ້ນມີ tags ຫຍັງແດ່(ຕາມທີ່ເຮົາໄດ້ກຳນົດໄວ້ໃນຕອນ build).

ມາຮອດຈຸດນີ້ກໍຖືກວ່າຕົວ API ຂອງເຮົາແມ່ນພ້ອມແລ້ວສຳຫລັບການ deploy.

ຂັ້ນຕອນທີ 4: Deploy Hello CHOCO API to Cloud Run

  • ເລີ່ມຈາກກົດທີ່ເມນູແລ້ວເລືອກຫາເມນູທີ່ຊື່ວ່າ “Cloud Run”
  • ກົດ “Create Service”
  • ຈະໄດ້ຫນ້າສຳຫລັບ configure
  1. ເລືອກ Container Image ກົດ “SELECT”

ໃຫ້ເຮົາເລືອກ container image ທີ່ເຮົາຈະໃຊ້ແລ້ວກົດ “SELECT”.

2. ຕັ້ງຊື່ service

3. ເລືອກ region ທີ່ເຮົາຈະໃຊ້

4. CPU Allocation and pricing ຈະມີ 2 ຕົວເລືອກໃຫ້ເລືອກລະຫວ່າງ

  • CPU is only allocated during request processing: ເມື່ອມີຄົນເຂົ້າໃຊ້ຈຶ່ງເປີດ
  • CPU is always allocated: ເປີດໄວ້ຕະຫລອດເວລາຕໍ່ໃຫ້ບໍ່ມີຄົນໃຊ້ກໍຕາມ

5. Autoscaling ການກຳນົດ scaling ຂອງ service ວ່າຈະໃຫ້ scale ໄດ້ສູງສຸດເທົ່າໃດ ແລະ ຕ່ຳສຸດເທົ່າໃດ

6. Authentication ເປັນການກຳນົດວ່າເຮົາຕ້ອງການໃຫ້ມີ Authen ຫລືບໍ່ເມື່ອມີຄົນຕ້ອງການເອີ້ນໃຊ້ service ຂອງເຮົາ.

7. ການ configure Container, Networking, Security

  • ສ່ວນຂອງ Container ເຮົາສາມາດກໍນົດຄ່າຂອງ: PORT ທີ່ຈະໃຊ້ run, Memory, CPU, Request timeout, Max request per container, Excution environment, environment variables, secret environment variables, Health checks ແລະ Cloud SQL connections.
  • ສ່ວນຂອງ Networking (ສ່ວນນີ້ແມ່ນເຮົາຈະຍັງບໍ່ໄດ້ config ຫຍັງຈະໃຊ້ default)
  • ສ່ວນຂອງ Security (ສ່ວນນີ້ແມ່ນເຮົາຈະຍັງບໍ່ໄດ້ config ຫຍັງຈະໃຊ້ default)

ຫລັງຈາກທີ່ເຮົາກຳນົດຄ່າຕ່າງໆຂອງ Service ເຮົາແລ້ວກົດ “CREATE

ເມື່ອ service ຂອງເຮົາສ້າງສຳເລັດຈະໄດ້ດັ່ງນີ້

ເຮົາສາມາດ copy ເອົາ URL ຂອງ service ທີ່ເຮົາ deploy ໄປທົດສອບໄດ້ເລີຍ

ເມື່ອເຮົາທົດສອບ Request ໄປຫາ API ທີ່ເຮົາ deploy ໄວ້ເຮົາກໍຈະໄດ້

ເຢ້ໆ message ທີ່ຕອບກັບມາຄືກັນທີ່ເຮົາຄາດຫວັງໄວ້ເລີຍຍຍຍ

ມາຮອດນີ້ກໍຈະຖືວ່າສຳເລັດແລ້ວສຳເລັດການ deploy API Hello CHOCO. ແລ້ວກໍລະນີທີ່ເຮົາຈະອັບເດດ API ເຮົາເດ ເຮົາຈະຕ້ອງໄດ້ເຮັດແນວໃດ ຂັ້ນຕອນຕໍ່ໄປເຮົາຈະມາອັບເດດ API ຂອງເຮົາໃຫ້ມີ Endpoint ສຳຫລັບ List ລາຍການສິນຄ້າອອກມາສະແດງ.

ຂັ້ນຕອນທີ 5: Update Hello CHOCO API ໃຫ້ມີ List Products

  1. ເພີ່ມ code ສ່ວນ List Products ໃສ່ file main.go
package main

import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"time"

"github.com/google/uuid"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/shopspring/decimal"
)

func getEnv(key, fallback string) string {
if value := os.Getenv(key); value != "" {
return value
}
return fallback
}

func main() {
ctx := context.Background()
e := echo.New()

e.Use(middleware.Logger())
e.Use(middleware.Recover())

e.GET("/", func(c echo.Context) error {
return c.JSON(http.StatusOK, echo.Map{
"message": "Hello CHOCO, this is a message from Golang running on CloudRun.",
})
})
e.GET("/products", listProducts)

errCh := make(chan error, 1)
go func() {
errCh <- e.Start(fmt.Sprintf(":%s", getEnv("PORT", "3001")))
}()

ctx, cancel := signal.NotifyContext(ctx, os.Kill, os.Interrupt)
defer cancel()

select {
case err := <-errCh:
if err != nil && err != http.ErrServerClosed {
log.Fatal("failed to start server")
}
log.Println("server shutdown gracefully")

case <-ctx.Done():
ctx, cancel := context.WithTimeout(ctx, time.Second*15)
defer cancel()

log.Println("shutting down server...")
if err := e.Shutdown(ctx); err != nil {
log.Fatal("failed to shutdown server")
}
log.Println("shutdown server gracefully")
}
}

type Product struct {
ID string `json:"product"`
Name string `json:"name"`
Currency string `json:"currency"`
Price decimal.Decimal `json:"price"`
Quantity decimal.Decimal `json:"quantity"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}

func listProducts(c echo.Context) error {
products := make([]Product, 0)
products = append(products, Product{
ID: uuid.NewString(),
Name: "Apple iPhone 14 Pro Max",
Currency: "LAK",
Price: decimal.NewFromInt(25240000),
Quantity: decimal.NewFromInt(50),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})

products = append(products, Product{
ID: uuid.NewString(),
Name: "Apple MacBook Pro M2 (Chip M2 Pro)",
Currency: "LAK",
Price: decimal.NewFromInt(65200000),
Quantity: decimal.NewFromInt(15),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
return c.JSON(http.StatusOK, echo.Map{
"products": products,
})
}

2. Build Container Image

3. Push Container Image to Artifact Registry Repo

4. Edit & Deploy New Revision

ເຂົ້າໄປທີ່ເມນູ Cloud Run ແລ້ວກົດ “Edit & Deploy New Revision”.

ໃນສ່ວນຂອງຫນ້າແກ້ໄຂໃຫ້ປ່ຽນ Container Image ທີ່ເຮົາຈະໃຊ້

  • ກົດ “SELECT”
  • ເລືອກ Container Image ທີ່ເຮົາຈະ Deploy
  • ເມື່ອເລືອກສຳເລັດກົດ “SELECT”
  • ກົດ “Deploy” ຖ້າໃຫ້ Service Deploy ສຳເລັດ

ຫລັງຈາກ Deploy ສຳເລັດເຮົາທົດສອບ Endpoint ທີ່ເຮົາເພີ່ມເຂົ້າໄປໃຫມ່ວ່າມັນອັບເດດໃຫ້ເຮົາແທ້ບໍ

ເຢ້ໆ It’s working well.

Conclusion

ຖ້າມີຂໍ້ຜິດພາດປະການໃດ ຫລື ມີສິ່ງໃດຢາກນຳແນະກໍແນະນຳໄດ້ເດີ້ ຜູ້ຂຽນເອງກໍກຳລັງຮຽນໃຊ້ຢູ່ຄືກັນ 5555 ສາມາດແລກປ່ຽນກັນໄດ້

--

--