DEV Community

Tossapol Ritcharoenwattu
Tossapol Ritcharoenwattu

Posted on

ลองใช้ trivy scan todolist api ที่สร้างจาก base image : scratch และ alpine

  1. สร้าง todolist api file : main.go
package main

import (
    "encoding/json"
    "log"
    "net/http"
    "strconv"
    "strings"
    "sync"
)

// Todo struct (model)
type Todo struct {
    ID   int    `json:"id"`
    Task string `json:"task"`
    Done bool   `json:"done"`
}

// In-memory store
var (
    todos  = make(map[int]Todo)
    nextID = 1
    mu     sync.Mutex // To make operations safe for concurrent use
)

func main() {
    // Seed some initial data
    todos[nextID] = Todo{ID: nextID, Task: "Build a CRUD API", Done: false}
    nextID++
    todos[nextID] = Todo{ID: nextID, Task: "Containerize it", Done: false}
    nextID++

    http.HandleFunc("/todos/", todosHandler)
    log.Println("Server starting on port 8080...")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

func todosHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    idStr := strings.TrimPrefix(r.URL.Path, "/todos/")

    // Route based on HTTP method
    switch r.Method {
    case http.MethodGet:
        if idStr == "" {
            // GET /todos - Get all todos
            getTodos(w, r)
        } else {
            // GET /todos/{id} - Get a single todo
            getTodoByID(w, r, idStr)
        }
    case http.MethodPost:
        // POST /todos - Create a new todo
        createTodo(w, r)
    case http.MethodPut:
        // PUT /todos/{id} - Update a todo
        updateTodo(w, r, idStr)
    case http.MethodDelete:
        // DELETE /todos/{id} - Delete a todo
        deleteTodo(w, r, idStr)
    default:
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    }
}

// GET /todos
func getTodos(w http.ResponseWriter, r *http.Request) {
    mu.Lock()
    defer mu.Unlock()
    // Convert map to slice for JSON output
    var todoList []Todo
    for _, todo := range todos {
        todoList = append(todoList, todo)
    }
    json.NewEncoder(w).Encode(todoList)
}

// POST /todos
func createTodo(w http.ResponseWriter, r *http.Request) {
    var newTodo Todo
    if err := json.NewDecoder(r.Body).Decode(&newTodo); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    mu.Lock()
    defer mu.Unlock()
    newTodo.ID = nextID
    todos[newTodo.ID] = newTodo
    nextID++

    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(newTodo)
}

// GET /todos/{id}
func getTodoByID(w http.ResponseWriter, r *http.Request, idStr string) {
    id, err := strconv.Atoi(idStr)
    if err != nil {
        http.Error(w, "Invalid ID", http.StatusBadRequest)
        return
    }

    mu.Lock()
    defer mu.Unlock()
    todo, ok := todos[id]
    if !ok {
        http.Error(w, "Todo not found", http.StatusNotFound)
        return
    }
    json.NewEncoder(w).Encode(todo)
}

// PUT /todos/{id}
func updateTodo(w http.ResponseWriter, r *http.Request, idStr string) {
    id, err := strconv.Atoi(idStr)
    if err != nil {
        http.Error(w, "Invalid ID", http.StatusBadRequest)
        return
    }

    var updatedTodo Todo
    if err := json.NewDecoder(r.Body).Decode(&updatedTodo); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    mu.Lock()
    defer mu.Unlock()
    if _, ok := todos[id]; !ok {
        http.Error(w, "Todo not found", http.StatusNotFound)
        return
    }

    updatedTodo.ID = id // Ensure ID is not changed
    todos[id] = updatedTodo
    json.NewEncoder(w).Encode(updatedTodo)
}

// DELETE /todos/{id}
func deleteTodo(w http.ResponseWriter, r *http.Request, idStr string) {
    id, err := strconv.Atoi(idStr)
    if err != nil {
        http.Error(w, "Invalid ID", http.StatusBadRequest)
        return
    }

    mu.Lock()
    defer mu.Unlock()
    if _, ok := todos[id]; !ok {
        http.Error(w, "Todo not found", http.StatusNotFound)
        return
    }

    delete(todos, id)
    w.WriteHeader(http.StatusNoContent)
}
Enter fullscreen mode Exit fullscreen mode

สร้างไฟล์ go.sum
go mod init todoapi
go mod tidy

  1. สร้าง Container Image (Scratch vs Alpine): เราจะใช้ Multi-stage build ใน Dockerfile เดียวเพื่อสร้าง image ทั้งสองแบบ file : Dockerfile
# Stage 1: Build the Go application
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY go.mod ./
RUN go mod download
COPY . .
# Build the application as a static binary
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .

# --- Image 1: Scratch Base ---
FROM scratch
WORKDIR /
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["/main"]

# --- Image 2: Alpine Base (uncomment to build) ---
# FROM alpine:latest
# WORKDIR /
# COPY --from=builder /app/main .
# EXPOSE 8080
# CMD ["/main"]
Enter fullscreen mode Exit fullscreen mode
  1. Build a scratch based image:
    docker build -t todo-api-scratch .
    Image description

  2. Build an alpine based image:
    ในไฟล์ Dockerfile ให้คอมเมนต์บรรทัดของ scratch และยกเลิกคอมเมนต์บรรทัดของ alpine
    Dockerfile

# --- Image 1: Scratch Base ---
# FROM scratch
# WORKDIR /
# COPY --from=builder /app/main .
# EXPOSE 8080
# CMD ["/main"]

# --- Image 2: Alpine Base (uncomment to build) ---
FROM alpine:latest
WORKDIR /
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["/main"]
Enter fullscreen mode Exit fullscreen mode

จากนั้น build image:
docker build -t todo-api-alpine .
Image description

  1. ใช้ Trivy Scan ดูความแตกต่าง ตอนนี้เรามาเปรียบเทียบผลลัพธ์การสแกนของ image ทั้งสองแบบ

Scan a scratch image:
trivy image todo-api-scratch
Image description

ไม่พบช่องโหว่ (0 vulnerabilities) เนื่องจาก scratch เป็น image ที่ว่างเปล่า

Scan an alpine image:
trivy image todo-api-alpine
Image description
พบช่องโหว่ (2 vulnerabilities)

สรุปความแตกต่าง: 📝
Scratch: ปลอดภัยสูงสุดเพราะไม่มีอะไรให้สแกนเลยนอกจาก api ที่เรา deploy แต่ก็ใช้งานยากกว่าเพราะไม่มี shell หรือเครื่องมือพื้นฐานใดๆ

Alpine: ปลอดภัยสูงและมีขนาดเล็ก แต่ยังคงมีพื้นผิวการโจมตี (attack surface) อยู่บ้างจาก package พื้นฐาน

Top comments (0)