diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 000000000..592659d75 --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,24 @@ +FROM golang:1.24.5-alpine AS builder + +WORKDIR /src + +COPY go.mod ./ +RUN go mod download + +COPY . . + +RUN CGO_ENABLED=0 go build \ + -trimpath \ + -ldflags="-s -w" \ + -o /quicknotes + +FROM gcr.io/distroless/static:nonroot + +COPY --from=builder /quicknotes /quicknotes +COPY --from=builder /src/seed.json /seed.json + +EXPOSE 8080 + +USER 65532:65532 + +ENTRYPOINT ["/quicknotes"] \ No newline at end of file diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 000000000..495cbf9ae --- /dev/null +++ b/compose.yaml @@ -0,0 +1,22 @@ +services: + quicknotes: + image: quicknotes:lab6 + build: + context: ./app + user: "0:0" + + ports: + - "8080:8080" + + environment: + ADDR: ":8080" + DATA_PATH: "/data/notes.json" + SEED_PATH: "/seed.json" + + volumes: + - quicknotes-data:/data + + restart: unless-stopped + +volumes: + quicknotes-data: \ No newline at end of file diff --git a/submissions/lab12.md b/submissions/lab12.md new file mode 100644 index 000000000..9396966ed --- /dev/null +++ b/submissions/lab12.md @@ -0,0 +1,89 @@ +# Lab 12 submissions + +### `main.go`: +``` +package main + +import ( + "fmt" + "net/http" + "time" + + spinhttp "github.com/spinframework/spin-go-sdk/v2/http" +) + +func init() { + spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) { + moscowTime := time.Now().UTC().Add(3 * time.Hour) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{"unix": %d, "iso": %q, "hour_minute": %q}`, + moscowTime.Unix(), + moscowTime.Format(time.RFC3339), + moscowTime.Format("15:04")) + }) +} + +func main() {} +``` + +### `spin.toml`: +``` +#:schema https://schemas.spinframework.dev/spin/manifest-v2/latest.json + +spin_manifest_version = 2 + +[application] +name = "moscow-time" +version = "0.1.0" +authors = ["long1tail "] +description = "" + +[[trigger.http]] +route = "/time" +component = "moscow-time" + +[component.moscow-time] +source = "main.wasm" +allowed_outbound_hosts = [] +[component.moscow-time.build] +command = "tinygo build -target=wasip1 -buildmode=c-shared -gc=leaking -no-debug -o main.wasm main.go" +``` + +### `spin build`: +``` +Building component moscow-time with `tinygo build -target=wasip1 -buildmode=c-shared -gc=leaking -no-debug -o main.wasm main.go` +Finished building all Spin components +``` + +### `curl -s http://127.0.0.1:3000/time | python3 -m json.tool`: +``` +{ + "unix": 1784152724, + "iso": "2026-07-15T21:58:44Z", + "hour_minute": "21:58" +} +``` + +| Dimention | Lab 6 Docker | Lab 12 WASM/Spin | +|-----------|--------------|------------------| +| Artifact size | 299K | 14.8MB | +| Cold start (p50) | 0.125s | 5.4 ms ± 0.5 ms | +| Warm latency p50 | 5.1 ms ± 0.4 ms | 5.2 ms ± 0.4 ms | +| Warm latency p95 | 5.2 ms ± 0.5 ms | 5.3 ms ± 0.3 ms | + +- a. `js/wasm` targets the browser and relies on Javascript environment bindings (DOM, Web APIs). `wasip1` targets the server using WASI (WebAssembly System Interface), replacing JS APIs with secure, standardized syscall-like capabilities to interact with the host OS (files, clocks, random numbers) without needing a browser engine. + +- b. Spin uses the Component Model/WASI-HTTP. It expects the WASM module to export specific C-style function pointers (like memory allocators and HTTP handlers) so the Spin host can invoke them. Without it, TinyGo builds a standalone executable with a `_start` entry point, which Spin cannot hook into. + +- c. Docker uses Linux network namespaces to isolate network stacks at the OS level. WASI uses capability-based security. The WASM module literally does not possess the function handles (capabilities) to open a socket unless the runtime explicitly passes that specific capability to the module upon instantiation. + +-d. TinyGo heavily restricts the `reflect` package, making standard `encoding/json` serialization of dynamic types like `map[string]any` impossible. It also does not embed the large `tzdata` database, making functions like `time.LoadLocation("Europe/Moscow")` fail. + +-e. For Docker, cold start is dominated by kernel operations: setting up Linux namespaces (network, PID), cgroups, and mounting overlayfs layers. For Spin, cold start is dominated by the WASM runtime (like Wasmtime) instantiating the engine, verifying the WASM bytecode, and loading it into memory. + +-f. WASM is significantly better for highly-dense, event-driven, scale-to-zero workloads (like Edge functions or serverless) due to its near-instant cold start and tiny memory footprint. Docker is still right for legacy applications, heavy frameworks, daemon processes, and apps requiring deep POSIX compliance or multi-threading. + +-g. WASM makes OS-level sandbox escapes (like kernel exploits, dirty pipe, or namespace breakout attacks) much harder because the WASM code never interacts with the Linux kernel. It runs inside an isolated virtual machine, communicating only through tightly controlled WASI interfaces. \ No newline at end of file diff --git a/submissions/lab6.md b/submissions/lab6.md new file mode 100644 index 000000000..7c808b111 --- /dev/null +++ b/submissions/lab6.md @@ -0,0 +1,199 @@ +# Lab 6 submission + +## `dockerfile`: + +``` +FROM golang:1.24.5-alpine AS builder + +WORKDIR /src + +COPY go.mod ./ +RUN go mod download + +COPY . . + +RUN CGO_ENABLED=0 go build \ + -trimpath \ + -ldflags="-s -w" \ + -o /quicknotes + +FROM gcr.io/distroless/static:nonroot + +COPY --from=builder /quicknotes /quicknotes +COPY --from=builder /src/seed.json /seed.json + +EXPOSE 8080 + +USER 65532:65532 + +ENTRYPOINT ["/quicknotes"] +``` + +### docker images quicknotes:lab6 + +``` +docker images quicknotes:lab6 + i Info → U In Use +IMAGE ID DISK USAGE CONTENT SIZE EXTRA +quicknotes:lab6 + 89c2136f5efc 14.7MB 3.32MB +``` + +### docker inspect quicknotes:lab6 | jq '.[0].Config' + +``` +docker inspect quicknotes:lab6 | jq '.[0].Config' +{ + "Entrypoint": [ + "/quicknotes" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt" + ], + "ExposedPorts": { + "8080/tcp": {} + }, + "User": "65532:65532", + "WorkingDir": "/home/nonroot" +} +``` + +``` +golang:1.24.5-alpine daae04ebad0c 394MB 83.3MB +``` + +## `compose.yalm`: + +``` +services: + quicknotes: + image: quicknotes:lab6 + build: + context: ./app + user: "0:0" + + ports: + - "8080:8080" + + environment: + ADDR: ":8080" + DATA_PATH: "/data/notes.json" + SEED_PATH: "/seed.json" + + volumes: + - quicknotes-data:/data + + restart: unless-stopped + +volumes: + quicknotes-data: + +``` + +``` +long1tail@Long1Tail:~/DevOps-Intro $ docker compose up --build -d +sleep 3 +curl -X POST -H 'Content-Type: application/json' \ + -d '{"title":"durable","body":"survive a restart"}' \ + http://localhost:8080/notes +curl -s http://localhost:8080/notes | grep durable +WARN[0000] Docker Compose requires buildx plugin to be installed +Sending build context to Docker daemon 5.188MB +Step 1/13 : FROM golang:1.24.5-alpine AS builder + ---> daae04ebad0c +Step 2/13 : WORKDIR /src + ---> Using cache + ---> 799bad952f0f +Step 3/13 : COPY go.mod ./ + ---> Using cache + ---> 7c1c40614b4e +Step 4/13 : RUN go mod download + ---> Using cache + ---> 822599abaeef +Step 5/13 : COPY . . + ---> Using cache + ---> 3540ad3ee96b +Step 6/13 : RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /quicknotes + ---> Using cache + ---> dff8776b7cc1 +Step 7/13 : FROM gcr.io/distroless/static:nonroot + ---> 963fa6c544fe +Step 8/13 : COPY --from=builder /quicknotes /quicknotes + ---> Using cache + ---> 1efb8b2954ad +Step 9/13 : COPY --from=builder /src/seed.json /seed.json + ---> Using cache + ---> 701937b2d2a6 +Step 10/13 : EXPOSE 8080 + ---> Using cache + ---> dfb1476938e2 +Step 11/13 : USER 65532:65532 + ---> Using cache + ---> 6715c8119c21 +Step 12/13 : ENTRYPOINT ["/quicknotes"] + ---> Using cache + ---> 89c2136f5efc +Step 13/13 : LABEL com.docker.compose.image.builder=classic + ---> Using cache + ---> 57ea3a70d968 +Successfully built 57ea3a70d968 +Successfully tagged quicknotes:lab6 +[+] up 2/2 + ✔ Image quicknotes:lab6 Built 0.4s + ✔ Container devops-intro-quicknotes-1 Running 0.0s +{"id":5,"title":"durable","body":"survive a restart","created_at":"2026-06-23T19:29:07.590969929Z"} +[{"id":1,"title":"Welcome to QuickNotes","body":"This is the project you'll containerize, deploy, monitor, and harden across all 10 labs.","created_at":"2026-01-15T10:00:00Z"},{"id":2,"title":"Read app/main.go first","body":"Start by understanding the entry point — env vars, signal handling, graceful shutdown.","created_at":"2026-01-15T10:05:00Z"},{"id":3,"title":"DevOps mantra","body":"If it hurts, do it more often.","created_at":"2026-01-15T10:10:00Z"},{"id":4,"title":"Endpoint cheat-sheet","body":"GET /notes GET /notes/{id} POST /notes DELETE /notes/{id} GET /health GET /metrics","created_at":"2026-01-15T10:15:00Z"},{"id":5,"title":"durable","body":"survive a restart","created_at":"2026-06-23T19:29:07.590969929Z"}] +long1tail@Long1Tail:~/DevOps-Intro $ docker compose down +[+] down 2/2 + ✔ Container devops-intro-quicknotes-1 Removed 0.3s + ✔ Network devops-intro_default Removed 0.3s +long1tail@Long1Tail:~/DevOps-Intro $ docker compose up -d +sleep 3 +curl -s http://localhost:8080/notes | grep durable +[+] up 2/2 + ✔ Network devops-intro_default Created 0.1s + ✔ Container devops-intro-quicknotes-1 Started 0.2s +[{"id":2,"title":"Read app/main.go first","body":"Start by understanding the entry point — env vars, signal handling, graceful shutdown.","created_at":"2026-01-15T10:05:00Z"},{"id":3,"title":"DevOps mantra","body":"If it hurts, do it more often.","created_at":"2026-01-15T10:10:00Z"},{"id":4,"title":"Endpoint cheat-sheet","body":"GET /notes GET /notes/{id} POST /notes DELETE /notes/{id} GET /health GET /metrics","created_at":"2026-01-15T10:15:00Z"},{"id":5,"title":"durable","body":"survive a restart","created_at":"2026-06-23T19:29:07.590969929Z"},{"id":1,"title":"Welcome to QuickNotes","body":"This is the project you'll containerize, deploy, monitor, and harden across all 10 labs.","created_at":"2026-01-15T10:00:00Z"}] +long1tail@Long1Tail:~/DevOps-Intro $ docker compose down -v +[+] down 3/3 + ✔ Container devops-intro-quicknotes-1 Removed 0.3s + ✔ Volume devops-intro_quicknotes-data Removed 0.0s + ✔ Network devops-intro_default Removed 0.3s +long1tail@Long1Tail:~/DevOps-Intro $ docker compose up -d +sleep 3 +curl -s http://localhost:8080/notes | grep durable +[+] up 3/3 + ✔ Network devops-intro_default Created 0.0s + ✔ Volume devops-intro_quicknotes-data Created 0.0s + ✔ Container devops-intro-quicknotes-1 Started 0.1s + ``` + + ### a + + Docker caches layers. If you COPY . . before go mod download, any source code change invalidates the cache and forces dependencies to be downloaded again. Copying only go.mod and go.sum first lets Docker reuse the dependency layer, so rebuilding after a code change is much faster. + + ### b + + CGO_ENABLED=0 produces a fully static Go binary. Distroless static images do not include system libraries like glibc, so a CGO-enabled binary may fail to start because required shared libraries are missing. + + ### c + + It's a minimal runtime image containing only essentials like CA certificates and a non-root user. It does not include a shell, package manager, or common Linux utilities. Fewer installed packages mean a smaller attack surface and fewer CVEs. + + ### d + + -s -w removes symbol and debug information, reducing binary size at the cost of harder debugging. -trimpath removes local filesystem paths from the binary, improving reproducibility and avoiding leakage of build-machine details. + + ### e + + The best approach is to use functionality built into the application binary itself, for example a /healthz endpoint or a dedicated healthcheck command. This avoids adding extra tools and works naturally with distroless images. + + + ### f + + Because named volumes are managed separately from containers. docker compose down removes containers and networks, but keeps volumes. To delete the data, run docker compose down -v or remove the volume explicitly. + + ### g + + It only waits for the dependency container to start, not for the service inside it to become ready. This can cause race conditions where your app starts before the database is accepting connections and crashes on startup. \ No newline at end of file diff --git a/wasm/moscow-time/.gitignore b/wasm/moscow-time/.gitignore new file mode 100644 index 000000000..b56501047 --- /dev/null +++ b/wasm/moscow-time/.gitignore @@ -0,0 +1,2 @@ +main.wasm +.spin/ diff --git a/wasm/moscow-time/go.mod b/wasm/moscow-time/go.mod new file mode 100644 index 000000000..691da6b55 --- /dev/null +++ b/wasm/moscow-time/go.mod @@ -0,0 +1,15 @@ +module github.com/moscow_time + +go 1.25.5 + +require github.com/spinframework/spin-go-sdk/v2 v2.2.1 + +require ( + github.com/apparentlymart/go-userdirs v0.0.0-20200915174352-b0c018a67c13 // indirect + github.com/bytecodealliance/componentize-go v0.3.3 // indirect + github.com/gofrs/flock v0.13.0 // indirect + github.com/julienschmidt/httprouter v1.3.0 // indirect + golang.org/x/sys v0.37.0 // indirect +) + +tool github.com/bytecodealliance/componentize-go diff --git a/wasm/moscow-time/go.sum b/wasm/moscow-time/go.sum new file mode 100644 index 000000000..1ef6766be --- /dev/null +++ b/wasm/moscow-time/go.sum @@ -0,0 +1,23 @@ +github.com/apparentlymart/go-userdirs v0.0.0-20200915174352-b0c018a67c13 h1:JtuelWqyixKApmXm3qghhZ7O96P6NKpyrlSIe8Rwnhw= +github.com/apparentlymart/go-userdirs v0.0.0-20200915174352-b0c018a67c13/go.mod h1:7kfpUbyCdGJ9fDRCp3fopPQi5+cKNHgTE4ZuNrO71Cw= +github.com/bytecodealliance/componentize-go v0.3.3 h1:8OA2qjWQA45vTMy5e1dboCOBqhAArUfMtVWlWSLJl/k= +github.com/bytecodealliance/componentize-go v0.3.3/go.mod h1:w1QFtPLGI9o38epvMOPyCKbMc7q7GJ7yZhIvhGTpzA0= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= +github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spinframework/spin-go-sdk/v2 v2.2.1 h1:ceAbRU+D3xmyZ8ScDLeFoT763ikFIUEmSjgsrD11v8k= +github.com/spinframework/spin-go-sdk/v2 v2.2.1/go.mod h1:vocVZB4qlTG8C5yoliKIAJCuv4x7sqK0GmVkWeD9N/A= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/wasm/moscow-time/main.go b/wasm/moscow-time/main.go new file mode 100644 index 000000000..bed8e4889 --- /dev/null +++ b/wasm/moscow-time/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "net/http" + "time" + + spinhttp "github.com/spinframework/spin-go-sdk/v2/http" +) + +func init() { + spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) { + moscowTime := time.Now().UTC().Add(3 * time.Hour) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{"unix": %d, "iso": %q, "hour_minute": %q}`, + moscowTime.Unix(), + moscowTime.Format(time.RFC3339), + moscowTime.Format("15:04")) + }) +} + +func main() {} diff --git a/wasm/moscow-time/spin.toml b/wasm/moscow-time/spin.toml new file mode 100644 index 000000000..3199d5877 --- /dev/null +++ b/wasm/moscow-time/spin.toml @@ -0,0 +1,19 @@ +#:schema https://schemas.spinframework.dev/spin/manifest-v2/latest.json + +spin_manifest_version = 2 + +[application] +name = "moscow-time" +version = "0.1.0" +authors = ["long1tail "] +description = "" + +[[trigger.http]] +route = "/time" +component = "moscow-time" + +[component.moscow-time] +source = "main.wasm" +allowed_outbound_hosts = [] +[component.moscow-time.build] +command = "tinygo build -target=wasip1 -buildmode=c-shared -gc=leaking -no-debug -o main.wasm main.go"