- สร้าง 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)
}
สร้างไฟล์ go.sum
go mod init todoapi
go mod tidy
- สร้าง 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"]
Build a scratch based image:
docker build -t todo-api-scratch .
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"]
จากนั้น build image:
docker build -t todo-api-alpine .
- ใช้ Trivy Scan ดูความแตกต่าง ตอนนี้เรามาเปรียบเทียบผลลัพธ์การสแกนของ image ทั้งสองแบบ
Scan a scratch image:
trivy image todo-api-scratch
ไม่พบช่องโหว่ (0 vulnerabilities) เนื่องจาก scratch เป็น image ที่ว่างเปล่า
Scan an alpine image:
trivy image todo-api-alpine
พบช่องโหว่ (2 vulnerabilities)
สรุปความแตกต่าง: 📝
Scratch: ปลอดภัยสูงสุดเพราะไม่มีอะไรให้สแกนเลยนอกจาก api ที่เรา deploy แต่ก็ใช้งานยากกว่าเพราะไม่มี shell หรือเครื่องมือพื้นฐานใดๆ
Alpine: ปลอดภัยสูงและมีขนาดเล็ก แต่ยังคงมีพื้นผิวการโจมตี (attack surface) อยู่บ้างจาก package พื้นฐาน
Top comments (0)