From 250c1d7ba64d19f69baa8b391272668ca16c70cf Mon Sep 17 00:00:00 2001 From: DJ Bubu Date: Fri, 5 Jun 2026 14:31:25 +1000 Subject: [PATCH 1/5] docs: add PR template Signed-off-by: DJ Bubu --- .github/pull_request_template.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..1a68db5e5 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,13 @@ +## Goal + + +## Changes +- + +## Testing + + +## Checklist +- [ ] Title is a clear sentence (≤ 70 chars) +- [ ] Commits are signed (`git log --show-signature`) +- [ ] `submissions/labN.md` updated From de067a7495b28baa73d61d2c8d7332c6c80ec90d Mon Sep 17 00:00:00 2001 From: DJ Bubu Date: Fri, 5 Jun 2026 20:08:30 +1000 Subject: [PATCH 2/5] docs: upstream moved while you worked Signed-off-by: DJ Bubu From 87a70a3bd1cc877dc8a80f6d4737bd835ebfd775 Mon Sep 17 00:00:00 2001 From: DJ Bubu Date: Wed, 8 Jul 2026 15:13:49 +1000 Subject: [PATCH 3/5] =?UTF-8?q?feat(lab10):=20release=20workflow=20?= =?UTF-8?q?=E2=80=94=20build=20+=20push=20QuickNotes=20to=20GHCR=20on=20v*?= =?UTF-8?q?=20tag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 51 ++++++++++++++++++++++++++++ app/Dockerfile | 64 +++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100644 app/Dockerfile diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..1ccae40e5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,51 @@ +name: release + +# Build the QuickNotes image and push it to GHCR whenever a semver tag is pushed. +on: + push: + tags: + - "v*" + +# Least privilege: read the repo, write packages (GHCR). Nothing else. +permissions: + contents: read + packages: write + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + # All third-party actions pinned by 40-char commit SHA (comment = version). + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + + - name: Log in to GHCR + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Derive tags/labels from the git ref. Produces (e.g. 0.1.0) + # from the tag, plus a moving `latest`. metadata-action lowercases the + # image path (DevOps-Intro -> devops-intro) as GHCR requires. + - name: Docker metadata + id: meta + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5 + with: + images: ghcr.io/${{ github.repository }}/quicknotes + tags: | + type=semver,pattern={{version}} + type=raw,value=latest + + - name: Build and push + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 + with: + context: ./app + platforms: linux/amd64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 000000000..5c50f244b --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,64 @@ +# ─── Stage 1: builder ─────────────────────────────────────────────────────── +# Use official Go image on Alpine for a small build environment. +# Bumped 1.24 -> 1.25 in Lab 9: Go stdlib 1.24.13 carried 11 HIGH CVEs +# (net/url, crypto/x509, crypto/tls, net, http2, net/mail — all DoS-class), +# all fixed in >= 1.25.11. Rebuilding on 1.25 clears every image finding. +FROM golang:1.25-alpine AS builder + +WORKDIR /src + +# Copy dependency files first to maximize layer cache reuse. +# go.mod and go.sum change less frequently than source code — +# so if only source changes, the go mod download layer is reused. +COPY go.mod ./ +RUN go mod download + +# Copy source code after dependencies are cached +COPY . . + +# Build a static binary: +# CGO_ENABLED=0 — disables C bindings, produces a fully static binary +# -trimpath — removes local file paths from the binary (reproducibility) +# -ldflags='-s -w' — strips debug symbols and DWARF info (smaller binary) +RUN CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o /quicknotes . + +# Build a minimal HTTP healthcheck binary inline. +# This single-file Go program does GET /health and exits 0 on 200, 1 otherwise. +# We build it here rather than copying wget from busybox to avoid relying on +# external image tags and to guarantee a fully static binary compatible with +# distroless/static (which has no libc or dynamic linker). +RUN printf 'package main\nimport ("net/http";"os")\nfunc main(){r,e:=http.Get("http://localhost:8080/health");if e!=nil{os.Exit(1)};if r.StatusCode!=200{os.Exit(1)}}\n' \ + > /tmp/healthcheck.go && \ + CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o /healthcheck /tmp/healthcheck.go + +# Pre-create the /data directory owned by nonroot (uid 65532). +# When Docker mounts a fresh named volume here, it copies the image's /data +# into the volume — preserving ownership — so the nonroot process can write to it. +RUN mkdir -p /data + +# ─── Stage 2: runtime ─────────────────────────────────────────────────────── +# Distroless static image: no shell, no apt, no package manager, minimal CVEs. +# The :nonroot tag sets the default user to uid 65532 (nonroot). +FROM gcr.io/distroless/static:nonroot + +# Copy only the compiled binary from the builder stage +COPY --from=builder /quicknotes /quicknotes + +# Copy the compiled healthcheck binary (distroless has no shell, curl, or wget) +COPY --from=builder /healthcheck /healthcheck + +# Copy the pre-created /data directory with nonroot ownership so a fresh +# named volume inherits the correct permissions without a separate init container. +COPY --from=builder --chown=65532:65532 /data /data + +# Copy the seed data file needed by QuickNotes on startup +COPY seed.json /seed.json + +# Document that the container listens on port 8080 +EXPOSE 8080 + +# Run as nonroot user (uid 65532) — never as root +USER nonroot + +# Use exec form (not shell form) so the process gets PID 1 directly +ENTRYPOINT ["/quicknotes"] \ No newline at end of file From d5a8a68872180eecb6fccb3725922abd7b52fa89 Mon Sep 17 00:00:00 2001 From: DJ Bubu Date: Wed, 8 Jul 2026 19:41:30 +1000 Subject: [PATCH 4/5] =?UTF-8?q?docs(lab10):=20CI=E2=86=92GHCR,=20HF=20Spac?= =?UTF-8?q?es=20deploy=20+=20warm=20latency,=20Cloudflare=20tunnel=20bonus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- submissions/lab10.md | 176 ++++++++++++++++++ .../screenshots/lab10-tunnel-phone.jpg | Bin 0 -> 17459 bytes 2 files changed, 176 insertions(+) create mode 100644 submissions/lab10.md create mode 100644 submissions/screenshots/lab10-tunnel-phone.jpg diff --git a/submissions/lab10.md b/submissions/lab10.md new file mode 100644 index 000000000..4413bc3f6 --- /dev/null +++ b/submissions/lab10.md @@ -0,0 +1,176 @@ +# Lab 10 Submission - Cloud Computing: Ship QuickNotes to a Real Cloud + +> Registry: **ghcr.io** - Hosted platform: **Hugging Face Spaces** (Docker SDK) - Bonus: **Cloudflare Tunnel** + +--- + +## Task 1 - CI-Automated Push to ghcr.io + +### 1.1 Release workflow (`.github/workflows/release.yml`) + +```yaml +name: release + +on: + push: + tags: + - "v*" + +permissions: + contents: read # least privilege + packages: write # push to GHCR — nothing else + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + # All third-party actions pinned by 40-char commit SHA (comment = version). + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - id: meta + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5 + with: + images: ghcr.io/${{ github.repository }}/quicknotes + tags: | + type=semver,pattern={{version}} + type=raw,value=latest + - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 + with: + context: ./app + platforms: linux/amd64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} +``` + +### 1.2 Trigger + registry + +```text +$ git tag -a v0.1.0 -m "Lab 10 release" +$ git push origin v0.1.0 +# -> workflow "release" runs green (Actions run: release #1, commit 87a70a3) +``` + +Image published to: **`ghcr.io/blacktree-lab/devops-intro/quicknotes`**, tags `0.1.0` + `latest`. + +### 1.3 Clean, unauthenticated pull (proof it's public) + +```text +$ docker logout ghcr.io +Removing login credentials for ghcr.io +$ docker pull ghcr.io/blacktree-lab/devops-intro/quicknotes:0.1.0 +... +Digest: sha256:f00df24cce80ea2698330aedbed911763630ffa0ade8d53caeff58615d1135f2 +Status: Downloaded newer image for ghcr.io/blacktree-lab/devops-intro/quicknotes:0.1.0 + +$ docker run --rm -d -p 127.0.0.1:8081:8080 ghcr.io/.../quicknotes:0.1.0 +$ curl -s http://127.0.0.1:8081/health +{"notes":0,"status":"ok"} +``` +Pulls with **no login** -> publicly pullable from any clean machine. + +### 1.4 Design Questions + +**a) OIDC vs `GITHUB_TOKEN` for pushing to ghcr.io.** +For pushing to the **same repo's** GHCR, the built-in `GITHUB_TOKEN` with `packages: write` is enough, it authenticates to GitHub's own services. You reach for **OIDC** when the workflow must authenticate to an **external** system (AWS/GCP/Azure, another registry) *without storing long-lived secrets*: the job exchanges a short-lived, cryptographically-verifiable identity token for cloud credentials. OIDC gives you **keyless, short-lived, auditable** federation and fine-grained trust policies (which repo/branch/environment may assume which cloud role) — none of which `GITHUB_TOKEN` provides, because it only talks to GitHub. + +**b) `:latest` vs immutable `:v0.1.0` — why ship both?** +`:v0.1.0` is **immutable** - deploys and rollbacks reference an exact, reproducible artifact (ideally by digest). `:latest` is **mutable** and moves each release. You still ship `:latest` as a convenience pointer for humans, docs, demos, and "just give me the newest" pulls where reproducibility doesn't matter. The discipline: **pin the immutable tag/digest in production**, use `:latest` only where convenience beats determinism. Shipping both serves both audiences. + +**c) `packages: write` scope only - principle + concrete attack prevented.** +Principle: **least privilege**. A broad `write: all` token can modify repo contents, releases, issues/PRs, deployments, and workflows. Scoping to `packages: write` (+ `contents: read`) means that if a **compromised third-party action** running in the job is exploited, the token can only push packages, it **cannot commit a backdoor to the repo, open/merge a malicious PR, or rewrite CI** to persist. The narrow scope caps the blast radius to the registry. + +--- + +## Task 2 - Deploy to Hugging Face Spaces + +### 2.1 The Space (Docker SDK) + +Space: **`https://huggingface.co/spaces/BarberryML/quicknotes`** — public, Docker SDK, free CPU-basic hardware. Two files: + +```dockerfile +# Dockerfile — pull the immutable GHCR image published by Task 1's CI +FROM ghcr.io/blacktree-lab/devops-intro/quicknotes:0.1.0 +# HF runs Space containers as UID 1000, which can't write the image's /data +# (owned by distroless uid 65532), so persist notes in world-writable /tmp. +ENV ADDR=":8080" DATA_PATH="/tmp/notes.json" SEED_PATH="/seed.json" +EXPOSE 8080 +``` + +```yaml +# README.md frontmatter +--- +title: QuickNotes +emoji: 📝 +sdk: docker +app_port: 8080 # QuickNotes listens on 8080; HF defaults to 7860 +pinned: false +--- +``` + +Public URL: **`https://barberryml-quicknotes.hf.space`** + +```text +$ curl -s https://barberryml-quicknotes.hf.space/health +{"notes":4,"status":"ok"} +$ curl -s https://barberryml-quicknotes.hf.space/notes +[{"id":2,...},{"id":3,...},{"id":4,...},{"id":1,...}] # 4 seeded notes +``` + +### 2.2 Scale-to-zero (HF "sleep") cold vs warm latency + +Free-tier Spaces sleep after ~30 min idle; the wake-up is the cold start. + +| Measurement | time_total | +|-------------|-----------:| +| Warm p50 (50 consecutive requests) | 2.65 s | +| Cold start #1 (confirmed "Sleeping" first) | 4.54 s | +| Cold start #2 | `` s | +| Cold start #3 | `` s | + +Command: `curl -w '%{time_total}\n' -o /dev/null -s https://barberryml-quicknotes.hf.space/health` + +### 2.3 Design Questions + +**d) HF "sleep" vs Cloud Run "scale to zero" — why is HF's wake so much slower?** +Both deallocate when idle to save resources. **Cloud Run** is engineered for production request-serving: images are pre-distributed on its fleet, the cold-start path is highly optimized, and a tiny Go image boots in ~1–2 s, billed per request. **HF Spaces free tier** optimizes for *cheap shared hosting of demos/ML apps*, not latency: after ~30 min it fully **stops the Space and releases the node**, so waking means **re-scheduling onto a node, pulling/loading the image, and starting the container** — tens of seconds. HF trades wake latency for cost/fairness on shared free infrastructure (and ML images are often huge); Cloud Run trades cost for fast, predictable cold starts. + +**e) Why `app_port: 8080`? What's HF's default and why?** +HF defaults to **port 7860** — the historic **Gradio** default, and most Spaces are Gradio/Streamlit apps. QuickNotes listens on **8080**, so `app_port: 8080` tells HF's reverse proxy to forward public traffic to 8080. Without it, HF would proxy to 7860 where nothing listens and the Space would never become reachable. + +**f) Pull the ghcr image vs build the Dockerfile inside the Space - trade-off.** +**Pulling the pre-built image** (what we did): fast Space builds (just a pull), and crucially **reproducibility + provenance**: the Space runs the *exact* immutable artifact that CI built, scanned, and could sign (single source of truth). Cost: the Space repo has no source, so it's **less debuggable/editable in place**, and you depend on the registry being reachable and the image public. **Building from source in the Space**: self-contained and editable, HF caches layers — but slower, and it rebuilds in a less-controlled environment that can **drift from CI**, losing the "deploy the exact tested artifact" guarantee. For production-style deploys, pulling the CI artifact wins; for rapid in-place iteration, building in the Space is handier. + +--- + +## Bonus Task - Cloudflare Tunnel + Cross-Platform Comparison + +Local QuickNotes exposed via a Cloudflare **quick tunnel**: +`https://airline-carlos-chair-declaration.trycloudflare.com` (ephemeral — changes on each `cloudflared` restart). Verified reachable from a **phone on cellular** (a different network) — screenshot `screenshots/lab10-tunnel-phone.jpg`. Latency from a 50-run `curl` loop. + +| Metric | HF Spaces (hosted) | Cloudflare Tunnel (local-via-edge) | +|--------|-------------------:|-----------------------------------:| +| Warm p50 | **2.65 s** | **1.67 s** | +| Warm p95 | **3.01 s** | **3.10 s** | +| Cold start | `` | N/A (continuously local) | +| Public URL stability | stable | ephemeral on restart | +| Cost | free | free | + +Tunnel latency = 50 `curl` runs from the host against the `trycloudflare.com` URL: p50 **1.665 s**, p95 **3.096 s** (min 1.45 / max 3.30). Each request traverses host -> Cloudflare edge -> back down the tunnel to the *same* laptop — i.e. the residential uplink **twice** — which is why it's slower and more variable than a datacenter-based prober (Checkly saw ~0.78 s p50 to the same tunnel from Frankfurt/Singapore, because edge↔datacenter is fast and only the edge->Melbourne leg is over the slow link). + +**Surprising result:** the *hosted* HF Space (warm p50 **2.65 s**) was actually **slower** than the *local-via-edge* tunnel (**1.67 s**) from Melbourne — HF's datacenter is geographically distant (plus free-tier reverse-proxy overhead), while the tunnel exits through a nearby Cloudflare edge. Being "in the cloud" doesn't guarantee lower latency; **distance and platform overhead dominate**. + +**g) Which is "really cloud", and does it matter to users?** +**HF Spaces is the real cloud deployment** — HF runs the container on *their* datacenter infrastructure, independent of my machine. With **Cloudflare Tunnel the compute runs on my own laptop**; Cloudflare only supplies the edge that proxies public traffic in. To a user hitting the URL *right now*, both look identical — a public HTTPS endpoint returning JSON. But the distinction reaches users over time as **availability**: the HF version stays up when my laptop is asleep/off and doesn't depend on my home internet, whereas the tunnel dies the instant I close the lid, lose power, or restart `cloudflared` (and its URL changes). "Really cloud" = someone else runs the compute and keeps it available — and that reliability difference is what users ultimately feel. + +**h) Latency dominator for each.** +- **HF Spaces (warm):** the round-trip network distance client ↔ HF datacenter. **(cold):** the wake-up — re-schedule + image pull/load + container start — tens of seconds, dwarfing the network time. +- **Cloudflare Tunnel:** the **double traversal of my residential uplink** — client -> nearest Cloudflare edge -> *back down the tunnel over my home connection* to the laptop -> back out. My home upload bandwidth and the edge↔laptop leg dominate (measured p50 1.67 s / p95 3.10 s from the same laptop, crossing that link twice). A datacenter prober sees far less (Checkly ~0.78 s) because only the edge->Melbourne leg is over the slow link. + +**i) When is Cloudflare Tunnel the right production pick, and when never?** +**Right** when the thing you're exposing genuinely lives outside a normal cloud and you want secure inbound access without opening ports: **home-lab / self-hosted services, on-prem or legacy systems** bridged to the internet, **internal tools behind zero-trust** (a *named* tunnel + Cloudflare Access with SSO), and quick **dev / stakeholder-review** URLs. **Never** as the primary host for a scalable public app: all traffic funnels through one machine-to-edge pipe (a bottleneck and single point of failure), availability equals that one box's uptime, and quick tunnels are ephemeral. For a real public app you want actual hosted compute (Cloud Run / HF / etc.) that scales and stays up independently of any single machine. diff --git a/submissions/screenshots/lab10-tunnel-phone.jpg b/submissions/screenshots/lab10-tunnel-phone.jpg new file mode 100644 index 0000000000000000000000000000000000000000..aa7f99aa53c7a2070b7f2924a51e5f00f85b953d GIT binary patch literal 17459 zcmeHu1z1&0yXe|M*WPqWNOwthcef&4(y4R`NJ$7t!v-W2RHOuHkPwg(LAn%By8CYY z{{M^rKk=M<&$;K^`}oeRcix&=Gw)urCf4k$iK}@4rlO#%0DwRsKnWRut3}X+vaGC` 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 L!{08$tI7WbkL+gF literal 0 HcmV?d00001 From af8376e09d6efe26a0363b7110afd2adcffc812a Mon Sep 17 00:00:00 2001 From: DJ Bubu Date: Wed, 8 Jul 2026 20:46:01 +1000 Subject: [PATCH 5/5] docs(lab10): CI->GHCR release, HF Spaces deploy (warm+cold latency), Cloudflare tunnel bonus --- submissions/lab10.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/submissions/lab10.md b/submissions/lab10.md index 4413bc3f6..4dee0a89b 100644 --- a/submissions/lab10.md +++ b/submissions/lab10.md @@ -130,8 +130,10 @@ Free-tier Spaces sleep after ~30 min idle; the wake-up is the cold start. |-------------|-----------:| | Warm p50 (50 consecutive requests) | 2.65 s | | Cold start #1 (confirmed "Sleeping" first) | 4.54 s | -| Cold start #2 | `` s | -| Cold start #3 | `` s | +| Cold start #2 | 8.69 s | +| Cold start #3 | 9.59 s | + +**Cold vs warm:** cold starts (4.54 / 8.69 / 9.59 s, avg ~7.6 s) run **~2–3.6× the warm p50 (2.65 s)**. The spread reflects how much the wake must do — a still-cached image is just a container restart (~4.5 s), while a fuller re-schedule/reload costs ~9–10 s. Command: `curl -w '%{time_total}\n' -o /dev/null -s https://barberryml-quicknotes.hf.space/health` @@ -157,7 +159,7 @@ Local QuickNotes exposed via a Cloudflare **quick tunnel**: |--------|-------------------:|-----------------------------------:| | Warm p50 | **2.65 s** | **1.67 s** | | Warm p95 | **3.01 s** | **3.10 s** | -| Cold start | `` | N/A (continuously local) | +| Cold start | ~4.5–9.6 s (avg ~7.6 s) | N/A (continuously local) | | Public URL stability | stable | ephemeral on restart | | Cost | free | free |