Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions app/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
22 changes: 22 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
@@ -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:
89 changes: 89 additions & 0 deletions submissions/lab12.md
Original file line number Diff line number Diff line change
@@ -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 <m.shulaev@innopolis.university>"]
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.
199 changes: 199 additions & 0 deletions submissions/lab6.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 2 additions & 0 deletions wasm/moscow-time/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
main.wasm
.spin/
15 changes: 15 additions & 0 deletions wasm/moscow-time/go.mod
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions wasm/moscow-time/go.sum
Original file line number Diff line number Diff line change
@@ -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=
Loading