portfolio pass: correct the CSP claim to match what nginx sends, rename the project outcome label, plain-english the projects intro and bio, standardise on Argo CD and single-touch, drop the education filler line
build-and-deploy / build (push) Failing after 14m34s
build-and-deploy / build (push) Failing after 14m34s
This commit is contained in:
@@ -10,7 +10,7 @@ interface Props {
|
|||||||
const { name, caption } = Astro.props;
|
const { name, caption } = Astro.props;
|
||||||
|
|
||||||
const captions: Record<string, string> = {
|
const captions: Record<string, string> = {
|
||||||
"edge-ai": "Design → single-press pipeline → readiness-gated GPU inference at the edge",
|
"edge-ai": "Design → single-touch pipeline → readiness-gated GPU inference at the edge",
|
||||||
"iac-fleet": "One source of truth → AWX/Ansible → identical edge nodes, even air-gapped",
|
"iac-fleet": "One source of truth → AWX/Ansible → identical edge nodes, even air-gapped",
|
||||||
homelab: "Bare metal → GitOps clusters → services, exposed outbound-only via a tunnel",
|
homelab: "Bare metal → GitOps clusters → services, exposed outbound-only via a tunnel",
|
||||||
"authentik-sso": "One identity provider, 2FA at the flow; apps validate over an internal back channel",
|
"authentik-sso": "One identity provider, 2FA at the flow; apps validate over an internal back channel",
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const emailHref = socials.find((s) => s.label === "Email")?.href;
|
|||||||
<p class="term__out">
|
<p class="term__out">
|
||||||
HEAD <span class="term__val">{buildInfo.sha}</span>
|
HEAD <span class="term__val">{buildInfo.sha}</span>
|
||||||
<span class="term__sep">·</span> built <span class="term__val">{buildInfo.builtAt}</span>
|
<span class="term__sep">·</span> built <span class="term__val">{buildInfo.builtAt}</span>
|
||||||
<span class="term__sep">·</span> reconciled by <span class="term__val">ArgoCD</span>
|
<span class="term__sep">·</span> reconciled by <span class="term__val">Argo CD</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="term__line"><span class="term__prompt">$</span> contact --priority low-ms</p>
|
<p class="term__line"><span class="term__prompt">$</span> contact --priority low-ms</p>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const dossierNo = String(index).padStart(3, "0");
|
|||||||
<h3 class="dossier__title">
|
<h3 class="dossier__title">
|
||||||
{p.title} <span class="dossier__arrow" aria-hidden="true">→</span>
|
{p.title} <span class="dossier__arrow" aria-hidden="true">→</span>
|
||||||
</h3>
|
</h3>
|
||||||
<p class="dossier__argument">The argument: {p.outcome}</p>
|
<p class="dossier__argument"><span class="dossier__outcome-label">Outcome</span> {p.outcome}</p>
|
||||||
<ul class="dossier__chips mono" role="list">
|
<ul class="dossier__chips mono" role="list">
|
||||||
{p.stack.map((s) => <li class="dchip">{s}</li>)}
|
{p.stack.map((s) => <li class="dchip">{s}</li>)}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -146,6 +146,14 @@ const dossierNo = String(index).padStart(3, "0");
|
|||||||
.dossier:hover .dossier__arrow {
|
.dossier:hover .dossier__arrow {
|
||||||
transform: translateX(4px);
|
transform: translateX(4px);
|
||||||
}
|
}
|
||||||
|
.dossier__outcome-label {
|
||||||
|
font-family: var(--mono, ui-monospace, monospace);
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: .08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--accent, #3fbaf5);
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
.dossier__argument {
|
.dossier__argument {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: "Shipping this site: GitOps from a homelab to the public internet"
|
title: "Shipping this site: GitOps from a homelab to the public internet"
|
||||||
date: 2026-06-15
|
date: 2026-06-15
|
||||||
summary: "How this portfolio is built and served - Astro to a container image, a self-hosted Gitea registry, ArgoCD, and a Cloudflare Tunnel - with security as acceptance criteria, not polish."
|
summary: "How this portfolio is built and served - Astro to a container image, a self-hosted Gitea registry, Argo CD, and a Cloudflare Tunnel - with security as acceptance criteria, not polish."
|
||||||
tags: ["gitops", "astro", "homelab", "security"]
|
tags: ["gitops", "astro", "homelab", "security"]
|
||||||
hero: "/blog/shipping-this-site.webp"
|
hero: "/blog/shipping-this-site.webp"
|
||||||
heroAlt: "A glowing data container travels a luminous rail from a small server rack toward a softly glowing globe of Earth, wrapped by a reconcile loop."
|
heroAlt: "A glowing data container travels a luminous rail from a small server rack toward a softly glowing globe of Earth, wrapped by a reconcile loop."
|
||||||
@@ -18,7 +18,7 @@ by GitOps. No special case for "it's just a website."
|
|||||||
The build is baked into a hardened `nginx-unprivileged` image and pushed to a **self-hosted public
|
The build is baked into a hardened `nginx-unprivileged` image and pushed to a **self-hosted public
|
||||||
Gitea registry**, deliberately a separate instance from the private one holding my infrastructure
|
Gitea registry**, deliberately a separate instance from the private one holding my infrastructure
|
||||||
code, so the public artifact and the private estate never share a trust boundary. The image digest
|
code, so the public artifact and the private estate never share a trust boundary. The image digest
|
||||||
is then pinned in a private `home-ops` repo, **ArgoCD** reconciles that repo onto the cluster, and
|
is then pinned in a private `home-ops` repo, **Argo CD** reconciles that repo onto the cluster, and
|
||||||
a **Cloudflare Tunnel** exposes exactly one service, this site, outbound-only.
|
a **Cloudflare Tunnel** exposes exactly one service, this site, outbound-only.
|
||||||
|
|
||||||
Follow the chain and notice what's missing. No open ports: the tunnel dials out. No server runtime:
|
Follow the chain and notice what's missing. No open ports: the tunnel dials out. No server runtime:
|
||||||
@@ -33,7 +33,7 @@ gesture at. The site didn't ship until every box was ticked:
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
[x] Static output - no server runtime to attack
|
[x] Static output - no server runtime to attack
|
||||||
[x] Strict CSP, no unsafe-inline / unsafe-eval
|
[x] CSP: script-src 'self', no inline or eval JavaScript (style-src keeps 'unsafe-inline' for Shiki)
|
||||||
[x] Self-hosted fonts - zero third-party requests
|
[x] Self-hosted fonts - zero third-party requests
|
||||||
[x] No secrets in the client bundle (verified by build-time grep)
|
[x] No secrets in the client bundle (verified by build-time grep)
|
||||||
[x] Outbound-only tunnel, single hostname, no catch-all
|
[x] Outbound-only tunnel, single hostname, no catch-all
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ traffic on the way out), pin-downable with a `/32`. That's how the LLM gateway's
|
|||||||
be a single line: one legitimate consumer, the agent cluster next door, exactly one `/32` allowed in.
|
be a single line: one legitimate consumer, the agent cluster next door, exactly one `/32` allowed in.
|
||||||
|
|
||||||
Here's the one that costs a 2am. I made that change with `kubectl patch` - quick, out-of-band, worked
|
Here's the one that costs a 2am. I made that change with `kubectl patch` - quick, out-of-band, worked
|
||||||
immediately. The service was GitOps-managed with self-heal on. Within seconds ArgoCD noticed the live
|
immediately. The service was GitOps-managed with self-heal on. Within seconds Argo CD noticed the live
|
||||||
object differed from git and put it back the way the repo said, which re-enabled the SNAT, which
|
object differed from git and put it back the way the repo said, which re-enabled the SNAT, which
|
||||||
re-broke the policy, which took DNS down with it. **The change must be committed**, or the platform
|
re-broke the policy, which took DNS down with it. **The change must be committed**, or the platform
|
||||||
will politely undo your fix while you sleep.
|
will politely undo your fix while you sleep.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "Single-Touch Edge AI Platform"
|
title: "Single-Touch Edge AI Platform"
|
||||||
outcome: "Turned a high-level edge-AI design into a single-press deployment running on Kubernetes at the store edge."
|
outcome: "Turned a high-level edge-AI design into a single-touch deployment running on Kubernetes at the store edge."
|
||||||
summary: "Store-edge Kubernetes running GPU-backed AI workloads, deployed from one command, with readiness-gated GPUs so inference never starts before the hardware is ready."
|
summary: "Store-edge Kubernetes running GPU-backed AI workloads, deployed from one command, with readiness-gated GPUs so inference never starts before the hardware is ready."
|
||||||
role: "Infrastructure / DevOps Engineer · Woolworths"
|
role: "Infrastructure / DevOps Engineer · Woolworths"
|
||||||
period: "2025 - Present"
|
period: "2025 - Present"
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: "Self-Hosted AI & Homelab Platform"
|
title: "Self-Hosted AI & Homelab Platform"
|
||||||
outcome: "A production-grade homelab - GitOps from bare metal to local AI, and the platform that serves this very site."
|
outcome: "A production-grade homelab - GitOps from bare metal to local AI, and the platform that serves this very site."
|
||||||
summary: "Proxmox with PCIe passthrough under Talos and OpenShift clusters, all driven by ArgoCD GitOps: local LLM inference, split-horizon DNS, 2FA/SSO VPN, full observability and NAS-backed backups."
|
summary: "Proxmox with PCIe passthrough under Talos and OpenShift clusters, all driven by Argo CD GitOps: local LLM inference, split-horizon DNS, 2FA/SSO VPN, full observability and NAS-backed backups."
|
||||||
role: "Owner / Operator"
|
role: "Owner / Operator"
|
||||||
period: "Ongoing"
|
period: "Ongoing"
|
||||||
stack: ["Talos", "OpenShift", "ArgoCD", "Proxmox", "Local LLM", "Cloudflare Tunnel"]
|
stack: ["Talos", "OpenShift", "Argo CD", "Proxmox", "Local LLM", "Cloudflare Tunnel"]
|
||||||
featured: true
|
featured: true
|
||||||
order: 15
|
order: 15
|
||||||
cover: "self-hosted-ai-homelab"
|
cover: "self-hosted-ai-homelab"
|
||||||
@@ -26,7 +26,7 @@ genuine proving ground for Kubernetes, GPUs, AI and security, not a pile of cont
|
|||||||
## Design
|
## Design
|
||||||
|
|
||||||
Proxmox provides the hypervisor layer with **PCIe passthrough** (GPU and storage) into
|
Proxmox provides the hypervisor layer with **PCIe passthrough** (GPU and storage) into
|
||||||
single-node **Talos** and **OpenShift** clusters. Everything is **ArgoCD GitOps** - the
|
single-node **Talos** and **OpenShift** clusters. Everything is **Argo CD GitOps** - the
|
||||||
cluster state lives in git and reconciles itself. On top: **local LLM inference** on a
|
cluster state lives in git and reconciles itself. On top: **local LLM inference** on a
|
||||||
Blackwell-class GPU, **split-horizon DNS** via Pi-hole, a VPN with **2FA/SSO**, and a
|
Blackwell-class GPU, **split-horizon DNS** via Pi-hole, a VPN with **2FA/SSO**, and a
|
||||||
**Prometheus / Grafana** observability stack. ZFS handles storage tiering; restic ships
|
**Prometheus / Grafana** observability stack. ZFS handles storage tiering; restic ships
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const experience: Role[] = [
|
|||||||
org: "Woolworths",
|
org: "Woolworths",
|
||||||
period: "Aug 2025 - Present",
|
period: "Aug 2025 - Present",
|
||||||
summary:
|
summary:
|
||||||
"Core DevOps infrastructure engineer for store-edge solutions - turning high-level designs into single-press deployments and CD pipelines.",
|
"Core DevOps infrastructure engineer for store-edge solutions - turning high-level designs into single-touch deployments and CD pipelines.",
|
||||||
highlights: [
|
highlights: [
|
||||||
"Single-touch deployment of an edge AI solution running on Kubernetes at the store edge",
|
"Single-touch deployment of an edge AI solution running on Kubernetes at the store edge",
|
||||||
"GPU brought online as code - passthrough via ESXi, end-state manifests / Helm charts",
|
"GPU brought online as code - passthrough via ESXi, end-state manifests / Helm charts",
|
||||||
@@ -76,6 +76,5 @@ export const education: string[] = [
|
|||||||
"Advanced Diploma in IT Networking and Security",
|
"Advanced Diploma in IT Networking and Security",
|
||||||
"Diploma in IT Networking",
|
"Diploma in IT Networking",
|
||||||
"Certificate IV in IT Networking",
|
"Certificate IV in IT Networking",
|
||||||
"Ongoing study in IT certifications and new techniques",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
// Facts the hero typing bar cycles through. Real, verbatim - no invented numbers.
|
// Facts the hero typing bar cycles through. Real, verbatim - no invented numbers.
|
||||||
export const fleetFacts = [
|
export const fleetFacts = [
|
||||||
"6 single-node Talos clusters · 0 SSH daemons · every machine is a document",
|
"6 single-node Talos clusters / 0 SSH daemons / every node rebuilt from committed config",
|
||||||
"139 ArgoCD Applications reconciled from one hub",
|
"139 Argo CD Applications reconciled from one hub",
|
||||||
"170+ CiliumNetworkPolicies fleet-wide - every namespace starts at default-deny",
|
"170+ CiliumNetworkPolicies fleet-wide - every namespace starts at default-deny",
|
||||||
"zero open inbound ports - the origin dials out",
|
"zero open inbound ports - the origin dials out",
|
||||||
"*.bztmon.org - a Let's Encrypt wildcard per cluster · DNS-01 · auto-renewed",
|
"*.bztmon.org - a Let's Encrypt wildcard per cluster · DNS-01 · auto-renewed",
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ export const site = {
|
|||||||
"Edge AI",
|
"Edge AI",
|
||||||
],
|
],
|
||||||
// One-paragraph elevator pitch for the About section.
|
// One-paragraph elevator pitch for the About section.
|
||||||
bio: "I'm an infrastructure and DevOps engineer who turns high-level designs into single-press deployments. My day job is store-edge Kubernetes running GPU-backed AI; my craft is the automation, GitOps and network policy that make a fleet behave the same way every time. I've run global infrastructure at enterprise scale - a thousand VMs, multi-region migrations, segmented networks - and I bring that same rigour to the smallest edge node. I'm an IaC enthusiast: if it's a manual step, I want it gone.",
|
bio: "I'm an infrastructure and platform engineer. I spend most of my time on the automation people usually leave in runbooks: store-edge Kubernetes running GPU-backed AI workloads, the GitOps pipelines that deploy them, and the network policy that keeps a fleet behaving the same way on every node. Before that I worked across larger enterprise estates - around a thousand VMs, multi-region migrations, segmented networks. I like infrastructure that can be rebuilt and explained from the repo. If a deployment still depends on someone remembering the right sequence of clicks, there's usually another piece worth automating.",
|
||||||
|
|
||||||
// Canonical URL (used for OG/sitemap/RSS).
|
// Canonical URL (used for OG/sitemap/RSS).
|
||||||
url: "https://www.bztmon.com",
|
url: "https://www.bztmon.com",
|
||||||
|
|||||||
+1
-1
@@ -50,7 +50,7 @@ export const skills: SkillGroup[] = [
|
|||||||
"vSphere / VMware ESXi",
|
"vSphere / VMware ESXi",
|
||||||
"Proxmox",
|
"Proxmox",
|
||||||
"containerd",
|
"containerd",
|
||||||
"ArgoCD / GitOps",
|
"Argo CD / GitOps",
|
||||||
"Helm & Kustomize",
|
"Helm & Kustomize",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ const projects = (await getCollection("projects")).sort(
|
|||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Projects" path="/projects" description="Selected platform & infrastructure projects by Jonathon Wright.">
|
<Layout title="Projects" path="/projects" description="Selected platform & infrastructure projects by Jonathon Wright.">
|
||||||
<Section id="all-projects" eyebrow="Projects" index="*" title="Some bytes I'm proud of - a starting line, not a trophy shelf.">
|
<Section id="all-projects" eyebrow="Projects" index="*" title="Systems I've built and run.">
|
||||||
<p class="lead projects__intro" data-reveal>
|
<p class="lead projects__intro" data-reveal>
|
||||||
Edge Kubernetes, GPU inference, self-hosted AI, productionised network automation, and
|
Edge Kubernetes, GPU inference, self-hosted AI, productionised network automation, and
|
||||||
the infrastructure rigour that ties it together - each with the problem, the design, and
|
the infrastructure rigour that ties it together - each with the problem, the design, and
|
||||||
the outcome. More always cooking.
|
the outcome.
|
||||||
</p>
|
</p>
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
{projects.map((entry, i) => <ProjectCard entry={entry} index={i + 1} />)}
|
{projects.map((entry, i) => <ProjectCard entry={entry} index={i + 1} />)}
|
||||||
|
|||||||
Reference in New Issue
Block a user