diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..b4dd1a0 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,62 @@ +name: Publish site + +# Builds the static site and publishes it to GitHub Pages. +# Pull requests run the checks only; pushes to main also deploy. +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +# One deployment at a time; let an in-flight deploy finish rather than cancel it. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Check out + uses: actions/checkout@v4 + + - name: Validate links, alt text, and titles + run: python3 scripts/check-site.py + + - name: Assemble site + run: | + mkdir _site + cp index.html 404.html robots.txt .nojekyll _site/ + cp -r assets blog _site/ + find _site -type f | sort + + - name: Configure Pages + if: github.event_name != 'pull_request' + uses: actions/configure-pages@v5 + + - name: Upload artifact + if: github.event_name != 'pull_request' + uses: actions/upload-pages-artifact@v3 + with: + path: _site + + deploy: + if: github.event_name != 'pull_request' + needs: build + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f86d0fb --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Source material, not part of the published site (the bio contains a birthday) +Michael Vance.md +michael-vance-builder-pixel-art.jpg +.remember/ +_site/ +.DS_Store diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/404.html b/404.html new file mode 100644 index 0000000..6120b08 --- /dev/null +++ b/404.html @@ -0,0 +1,33 @@ + + + + + + Page not found | Michael Vance + + + + + +
+

ERR 404

+

Measured twice, cut once. This page isn't here.

+

The board you were looking for was never milled.

+

Back to the workbench

+ +
+ + diff --git a/assets/css/style.css b/assets/css/style.css new file mode 100644 index 0000000..618fd9c --- /dev/null +++ b/assets/css/style.css @@ -0,0 +1,305 @@ +/* Michael Vance - blueprint meets workbench. + Palette: kraft paper, blueprint ink, walnut, and tape-measure yellow. */ + +:root { + --paper: #f3ebdb; + --paper-2: #eae0cb; + --ink: #2b2118; + --ink-soft: #5b4d3f; + --blueprint: #1d3b63; + --blueprint-soft: #3b5f8f; + --wood: #8b5a2b; + --tape: #f0b81f; + --rule: rgba(43, 33, 24, 0.22); + --grid: rgba(29, 59, 99, 0.07); + --card: #fbf6ea; + --code-bg: #14253f; + --code-ink: #e8f0fb; + --radius: 6px; + --serif: "Fraunces", Georgia, "Times New Roman", serif; + --sans: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + --measure: 68ch; + --wrap: 1080px; +} + +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) { + --paper: #101c2e; + --paper-2: #0c1626; + --ink: #efe6d3; + --ink-soft: #b9ad98; + --blueprint: #8fb6ee; + --blueprint-soft: #6f98d4; + --wood: #d09a5c; + --rule: rgba(239, 230, 211, 0.2); + --grid: rgba(143, 182, 238, 0.08); + --card: #15253d; + --code-bg: #0a1424; + } +} + +*, *::before, *::after { box-sizing: border-box; } + +html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; } + +body { + margin: 0; + background-color: var(--paper); + background-image: + linear-gradient(var(--grid) 1px, transparent 1px), + linear-gradient(90deg, var(--grid) 1px, transparent 1px); + background-size: 32px 32px; + color: var(--ink); + font-family: var(--sans); + font-size: 1.0625rem; + line-height: 1.65; +} + +img, svg { max-width: 100%; height: auto; display: block; } + +a { color: var(--blueprint); text-underline-offset: 3px; text-decoration-thickness: 1px; } +a:hover { color: var(--wood); } +:focus-visible { outline: 3px solid var(--tape); outline-offset: 3px; border-radius: 2px; } + +.skip-link { + position: absolute; left: 1rem; top: -4rem; z-index: 100; + background: var(--tape); color: #2b2118; padding: .6rem 1rem; border-radius: var(--radius); + font-weight: 600; text-decoration: none; +} +.skip-link:focus { top: 1rem; } + +.wrap { width: min(100% - 2rem, var(--wrap)); margin-inline: auto; } + +/* ---------- header ---------- */ +.site-header { + position: sticky; top: 0; z-index: 50; + background: color-mix(in srgb, var(--paper) 92%, transparent); + backdrop-filter: blur(8px); + border-bottom: 2px solid var(--ink); +} +.site-header .wrap { + display: flex; align-items: center; justify-content: space-between; + gap: 1rem; padding-block: .7rem; flex-wrap: wrap; +} +.brand { + font-family: var(--serif); font-weight: 700; font-size: 1.2rem; + color: var(--ink); text-decoration: none; letter-spacing: .01em; + display: inline-flex; align-items: center; gap: .55rem; +} +.brand-mark { + width: 1.9rem; height: 1.9rem; display: grid; place-items: center; + background: var(--tape); color: #2b2118; border: 2px solid var(--ink); + border-radius: 4px; font-family: var(--mono); font-size: .8rem; font-weight: 700; +} +.nav { display: flex; gap: .25rem 1.25rem; flex-wrap: wrap; font-family: var(--mono); font-size: .85rem; } +.nav a { color: var(--ink); text-decoration: none; padding: .25rem 0; border-bottom: 2px solid transparent; } +.nav a:hover, .nav a[aria-current="page"] { border-bottom-color: var(--tape); color: var(--ink); } + +/* tape-measure strip under header */ +.ruler { + height: 14px; + background: + repeating-linear-gradient(90deg, var(--ink) 0 1px, transparent 1px 8px) bottom / 100% 6px no-repeat, + repeating-linear-gradient(90deg, var(--ink) 0 1px, transparent 1px 40px) bottom / 100% 12px no-repeat, + var(--tape); + border-bottom: 2px solid var(--ink); +} + +/* ---------- hero ---------- */ +.hero { padding-block: clamp(2.5rem, 6vw, 5rem); } +.hero-grid { + display: grid; gap: clamp(1.5rem, 4vw, 3.5rem); + grid-template-columns: 1.25fr .85fr; align-items: center; +} +.eyebrow { + font-family: var(--mono); font-size: .8rem; letter-spacing: .12em; text-transform: uppercase; + color: var(--blueprint); margin: 0 0 .75rem; +} +.hero h1 { + font-family: var(--serif); font-size: clamp(2.4rem, 6vw, 4.2rem); + line-height: 1.05; margin: 0 0 1rem; letter-spacing: -.01em; +} +.hero h1 em { font-style: italic; color: var(--wood); } +.lede { font-size: 1.2rem; color: var(--ink-soft); max-width: 52ch; margin: 0 0 1.5rem; } +.philosophy { + margin: 0 0 1.75rem; padding: .9rem 1.1rem; + border-left: 4px solid var(--tape); background: var(--card); + font-family: var(--serif); font-style: italic; font-size: 1.1rem; + max-width: 52ch; +} +.philosophy cite { display: block; font-family: var(--mono); font-size: .75rem; font-style: normal; color: var(--ink-soft); margin-top: .4rem; letter-spacing: .06em; } + +.btn-row { display: flex; gap: .75rem; flex-wrap: wrap; } +.btn { + display: inline-block; padding: .7rem 1.2rem; border: 2px solid var(--ink); + border-radius: var(--radius); font-weight: 600; text-decoration: none; + font-size: .95rem; box-shadow: 3px 3px 0 var(--ink); transition: transform .08s, box-shadow .08s; +} +.btn:hover { transform: translate(2px, 2px); box-shadow: 1px 1px 0 var(--ink); } +.btn-primary { background: var(--tape); color: #2b2118; } +.btn-primary:hover { color: #2b2118; } +.btn-ghost { background: var(--card); color: var(--ink); } +.btn-ghost:hover { color: var(--ink); } + +.portrait { + margin: 0; position: relative; background: var(--card); + border: 2px solid var(--ink); border-radius: var(--radius); + padding: .75rem .75rem 0; box-shadow: 8px 8px 0 var(--blueprint); +} +.portrait img { border: 2px solid var(--ink); border-radius: 3px; aspect-ratio: 1; object-fit: cover; width: 100%; } +.portrait figcaption { + font-family: var(--mono); font-size: .75rem; color: var(--ink-soft); + padding: .6rem .1rem .7rem; display: flex; justify-content: space-between; gap: .5rem; flex-wrap: wrap; +} + +.facts { + display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 0; + margin-top: clamp(2rem, 5vw, 3.5rem); border: 2px solid var(--ink); background: var(--card); +} +.facts > div { padding: 1rem 1.1rem; border-right: 2px dashed var(--rule); } +.facts > div:last-child { border-right: 0; } +.facts dt { font-family: var(--mono); font-size: .72rem; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-soft); } +.facts dd { margin: .15rem 0 0; font-family: var(--serif); font-size: 1.7rem; font-weight: 700; line-height: 1.2; } + +/* ---------- sections ---------- */ +section { padding-block: clamp(2.5rem, 5vw, 4rem); } +.section-head { display: flex; align-items: baseline; gap: 1rem; margin-bottom: 1.75rem; } +.section-head .tag { + font-family: var(--mono); font-size: .75rem; letter-spacing: .1em; + background: var(--blueprint); color: var(--paper); padding: .2rem .55rem; border-radius: 3px; + white-space: nowrap; +} +.section-head h2 { font-family: var(--serif); font-size: clamp(1.7rem, 3.5vw, 2.3rem); margin: 0; line-height: 1.15; } +.section-head::after { content: ""; flex: 1; height: 2px; align-self: center; background: repeating-linear-gradient(90deg, var(--ink) 0 8px, transparent 8px 14px); opacity: .5; } + +.prose { max-width: var(--measure); } +.prose p { margin: 0 0 1.1rem; } + +.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(1.25rem, 3vw, 2.5rem); } +.panel { + background: var(--card); border: 2px solid var(--ink); border-radius: var(--radius); + padding: 1.4rem 1.5rem; position: relative; +} +.panel h3 { font-family: var(--serif); margin: 0 0 .25rem; font-size: 1.35rem; } +.panel .panel-label { font-family: var(--mono); font-size: .72rem; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-soft); display: block; margin-bottom: .6rem; } +.panel.blueprint { border-style: dashed; border-color: var(--blueprint); } +.panel.wood { border-color: var(--wood); } +.panel ul { margin: .5rem 0 0; padding-left: 1.2rem; } +.panel li { margin-bottom: .3rem; } + +/* ---------- experience ---------- */ +.jobs { list-style: none; margin: 0; padding: 0; border-left: 3px solid var(--ink); } +.job { position: relative; padding: 0 0 2rem 1.75rem; } +.job::before { + content: ""; position: absolute; left: -10px; top: .45rem; width: 17px; height: 17px; + background: var(--tape); border: 3px solid var(--ink); border-radius: 50%; +} +.job h3 { font-family: var(--serif); font-size: 1.3rem; margin: 0; } +.job .where { font-family: var(--mono); font-size: .82rem; color: var(--ink-soft); margin: .1rem 0 .7rem; } +.job ul { margin: 0; padding-left: 1.2rem; max-width: 74ch; } +.job li { margin-bottom: .4rem; } + +.chips { display: flex; flex-wrap: wrap; gap: .45rem; list-style: none; margin: .6rem 0 0; padding: 0; } +.chips li { + font-family: var(--mono); font-size: .78rem; padding: .22rem .6rem; + border: 1.5px solid var(--ink); border-radius: 999px; background: var(--paper); +} + +/* ---------- post cards ---------- */ +.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(290px, 1fr)); gap: 1.5rem; } +.card { + display: flex; flex-direction: column; background: var(--card); + border: 2px solid var(--ink); border-radius: var(--radius); overflow: hidden; + box-shadow: 5px 5px 0 var(--ink); transition: transform .1s, box-shadow .1s; +} +.card:hover { transform: translate(2px, 2px); box-shadow: 3px 3px 0 var(--ink); } +.card-cover { border-bottom: 2px solid var(--ink); background: var(--blueprint); aspect-ratio: 1200 / 630; } +.card-cover img { width: 100%; height: 100%; object-fit: cover; } +.card-body { padding: 1.1rem 1.2rem 1.3rem; display: flex; flex-direction: column; gap: .45rem; flex: 1; } +.card-meta { font-family: var(--mono); font-size: .74rem; color: var(--ink-soft); display: flex; gap: .6rem; flex-wrap: wrap; align-items: center; } +.pill { padding: .1rem .5rem; border-radius: 3px; font-weight: 600; color: #2b2118; } +.pill.workshop { background: var(--tape); } +.pill.engineering { background: var(--blueprint); color: var(--paper); } +.card h3 { font-family: var(--serif); font-size: 1.25rem; line-height: 1.25; margin: 0; } +.card h3 a { color: var(--ink); text-decoration: none; } +.card h3 a::after { content: ""; position: absolute; inset: 0; } /* whole-card hit area */ +.card { position: relative; } +.card p { margin: 0; color: var(--ink-soft); font-size: .97rem; } + +.group-title { font-family: var(--serif); font-size: 1.5rem; margin: 2.5rem 0 1.1rem; display: flex; align-items: center; gap: .6rem; } +.group-title:first-of-type { margin-top: 0; } + +/* ---------- article ---------- */ +.article-head { padding-block: clamp(2rem, 5vw, 3.5rem) 1.5rem; } +.article-head h1 { font-family: var(--serif); font-size: clamp(2rem, 5vw, 3.2rem); line-height: 1.1; margin: .4rem 0 1rem; } +.article-head .dek { font-size: 1.2rem; color: var(--ink-soft); max-width: 60ch; margin: 0 0 1rem; } +.byline { font-family: var(--mono); font-size: .8rem; color: var(--ink-soft); display: flex; gap: .4rem 1rem; flex-wrap: wrap; align-items: center; } +.article-cover { border: 2px solid var(--ink); border-radius: var(--radius); overflow: hidden; box-shadow: 6px 6px 0 var(--blueprint); margin-bottom: 2.25rem; background: var(--blueprint); } + +.article { max-width: var(--measure); margin-inline: auto; padding-bottom: 3rem; } +.article h2 { font-family: var(--serif); font-size: 1.65rem; margin: 2.4rem 0 .6rem; line-height: 1.2; } +.article h3 { font-family: var(--serif); font-size: 1.25rem; margin: 1.8rem 0 .4rem; } +.article p, .article ul, .article ol { margin: 0 0 1.15rem; } +.article li { margin-bottom: .35rem; } +.article blockquote { + margin: 1.6rem 0; padding: .8rem 1.2rem; border-left: 4px solid var(--tape); + background: var(--card); font-family: var(--serif); font-style: italic; font-size: 1.12rem; +} +.article figure { margin: 1.8rem 0; } +.article figure img, .article figure svg { border: 2px solid var(--ink); border-radius: var(--radius); background: var(--card); width: 100%; } +.article figcaption { font-family: var(--mono); font-size: .78rem; color: var(--ink-soft); margin-top: .5rem; } +.article code { font-family: var(--mono); font-size: .88em; background: var(--paper-2); padding: .1em .35em; border-radius: 3px; } +.article pre { + background: var(--code-bg); color: var(--code-ink); border: 2px solid var(--ink); border-radius: var(--radius); + padding: 1rem 1.15rem; overflow-x: auto; font-size: .85rem; line-height: 1.55; margin: 0 0 1.4rem; +} +.article pre code { background: none; padding: 0; color: inherit; font-size: inherit; } +.article pre .c { color: #8aa4c8; } +.callout { + border: 2px dashed var(--blueprint); background: var(--card); border-radius: var(--radius); + padding: 1rem 1.2rem; margin: 1.6rem 0; +} +.callout strong:first-child { font-family: var(--mono); font-size: .78rem; letter-spacing: .1em; text-transform: uppercase; color: var(--blueprint); display: block; margin-bottom: .25rem; } +.cutlist { width: 100%; border-collapse: collapse; margin: 1.4rem 0; font-size: .95rem; background: var(--card); } +.cutlist caption { text-align: left; font-family: var(--mono); font-size: .78rem; color: var(--ink-soft); padding-bottom: .4rem; } +.cutlist th, .cutlist td { border: 1.5px solid var(--ink); padding: .5rem .75rem; text-align: left; } +.cutlist th { background: var(--tape); color: #2b2118; font-family: var(--mono); font-size: .78rem; letter-spacing: .06em; text-transform: uppercase; } + +.post-nav { display: flex; justify-content: space-between; gap: 1rem; flex-wrap: wrap; border-top: 2px solid var(--ink); padding-top: 1.25rem; margin-top: 2.5rem; font-family: var(--mono); font-size: .85rem; } + +/* ---------- contact + footer ---------- */ +.contact-box { + background: var(--blueprint); color: #f3ebdb; border: 2px solid var(--ink); border-radius: var(--radius); + padding: clamp(1.5rem, 4vw, 2.5rem); box-shadow: 8px 8px 0 var(--tape); + background-image: linear-gradient(rgba(255,255,255,.07) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.07) 1px, transparent 1px); + background-size: 24px 24px; +} +@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) .contact-box { background-color: #1d3b63; } } +.contact-box h2 { font-family: var(--serif); margin: 0 0 .5rem; font-size: clamp(1.6rem, 3.5vw, 2.2rem); } +.contact-box p { margin: 0 0 1.25rem; max-width: 52ch; } +.contact-box a:not(.btn) { color: var(--tape); } + +.site-footer { border-top: 2px solid var(--ink); background: var(--paper-2); padding-block: 1.75rem; font-family: var(--mono); font-size: .8rem; color: var(--ink-soft); } +.site-footer .wrap { display: flex; justify-content: space-between; gap: 1rem; flex-wrap: wrap; } +.site-footer a { color: var(--ink-soft); } + +/* ---------- responsive ---------- */ +@media (max-width: 800px) { + .hero-grid, .two-col { grid-template-columns: 1fr; } + .portrait { max-width: 420px; order: -1; } + .facts > div { border-right: 0; border-bottom: 2px dashed var(--rule); } + .facts > div:last-child { border-bottom: 0; } + .site-header { position: static; } +} + +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } + .btn, .card { transition: none; } +} + +@media print { + .site-header, .ruler, .site-footer, .btn-row, .skip-link { display: none; } + body { background: #fff; color: #000; } + .card, .panel, .facts { box-shadow: none; } +} diff --git a/assets/favicon.svg b/assets/favicon.svg new file mode 100644 index 0000000..d554b8e --- /dev/null +++ b/assets/favicon.svg @@ -0,0 +1 @@ +MV diff --git a/assets/img/cover-felling-oak.svg b/assets/img/cover-felling-oak.svg new file mode 100644 index 0000000..7a13159 --- /dev/null +++ b/assets/img/cover-felling-oak.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + +SAFE DISTANCE +FELLING PLAN / FALL LINE AWAY FROM HOUSE diff --git a/assets/img/cover-kanvas-snapshot.svg b/assets/img/cover-kanvas-snapshot.svg new file mode 100644 index 0000000..f910af7 --- /dev/null +++ b/assets/img/cover-kanvas-snapshot.svg @@ -0,0 +1,6 @@ + + + +IngressServiceDeploymentDeploymentStatefulSet +kubectl get -o json +CLUSTER SNAPSHOT / STATE AT A GLANCE diff --git a/assets/img/cover-meshery-teams.svg b/assets/img/cover-meshery-teams.svg new file mode 100644 index 0000000..72e140d --- /dev/null +++ b/assets/img/cover-meshery-teams.svg @@ -0,0 +1,6 @@ + + + + +PLATFORMTEAM ATEAM BTEAM CTEAM D +SELF-SERVICE WITH GUARDRAILS diff --git a/assets/img/cover-mesheryctl-axi.svg b/assets/img/cover-mesheryctl-axi.svg new file mode 100644 index 0000000..8b3153d --- /dev/null +++ b/assets/img/cover-mesheryctl-axi.svg @@ -0,0 +1,7 @@ + + + +$ mesheryctl-axi system statusserver: runningadapters: 3 healthyconnections: 12help: mesheryctl-axi design list + + +MESHERYCTL-AXI / AGENT-READY CLI diff --git a/assets/img/cover-walnut-table.svg b/assets/img/cover-walnut-table.svg new file mode 100644 index 0000000..5764706 --- /dev/null +++ b/assets/img/cover-walnut-table.svg @@ -0,0 +1,10 @@ + + + + + + + + +72 in +WALNUT DINING TABLE / SHEET 1 OF 1 diff --git a/assets/img/michael-vance.jpg b/assets/img/michael-vance.jpg new file mode 100644 index 0000000..ff7a84c Binary files /dev/null and b/assets/img/michael-vance.jpg differ diff --git a/blog/felling-the-oak/index.html b/blog/felling-the-oak/index.html new file mode 100644 index 0000000..587b6d5 --- /dev/null +++ b/blog/felling-the-oak/index.html @@ -0,0 +1,90 @@ + + + + + + Felling the oak next to the house | Michael Vance + + + + + + + + + + + + + + + +
+
+

Workshop

+

Felling the oak next to the house

+

A tree, a roofline, and a plan. The most important cut happens before the saw starts.

+ +
+
Blueprint-style plan showing an oak tree beside a house, with a dashed fall line pointing away from the roof and a marked safe distance.
+ +
+

There was an oak next to our house that I had been worrying about for two summers. One large limb hung over the roofline, the trunk had a split near the base, and every storm warning sent me out to the yard to look up at it. This fall I decided it was time.

+ +
+ Safety note + Felling a tree near a structure is one of the most dangerous jobs a homeowner can take on. I only did this because I have training, the right gear, and a second person on the ground. If a tree can hit your house, a power line, or a road, hire a certified arborist. +
+ +

The plan is the project

+

I treated it like an incident runbook. Before touching a saw I wrote down the goal (the tree lands in the open lawn, away from the house), the hazards (the roofline, the fence, the neighbor's shed), the escape routes, and the abort criteria.

+
    +
  1. Read the tree. Lean, weight of the crown, the split at the base, and any dead limbs ("widowmakers") overhead.
  2. +
  3. Pick the fall line. The open lawn, roughly 30 degrees off the natural lean, so I would need to steer the tree rather than just let it go.
  4. +
  5. Clear two escape paths. Each at about 45 degrees back from the fall line, free of tripping hazards, and walked in advance.
  6. +
  7. Check the weather. No wind forecast, no rain. Wind can undo a good plan in a second.
  8. +
+ +

The cuts

+

A directional felling cut comes in three parts. The notch (or face cut) sets the direction. The back cut releases the tree. The hinge, the strip of wood left between them, controls the fall.

+ +
The hinge does the steering. If you cut through it, you have given up control of the tree.
+ +

On the ground

+

The oak came down exactly along the line I had drawn, and landed in the lawn with room to spare. It was strangely quiet afterward. My helper and I stood there for a moment before either of us said anything.

+

The trunk had a larger area of decay at the base than I had guessed. That confirmed the decision to take it down, and also reminded me how much a tree hides from the outside.

+ +

The good part

+

The best part of felling a tree is what it becomes. Some of the trunk is now in the garage on stickers, drying slowly for future projects. The rest became firewood, which is probably the most honest use of any oak limb.

+

I took one lesson back to the office: the most important work happens before the first cut. A good plan with explicit abort criteria is worth more than skill with the tool.

+ + +
+
+ + + + diff --git a/blog/index.html b/blog/index.html new file mode 100644 index 0000000..2fde174 --- /dev/null +++ b/blog/index.html @@ -0,0 +1,95 @@ + + + + + + Blog | Michael Vance + + + + + + + + + + + + +
+
+
+

The blog

+

Notes from two workbenches

+

Platform engineering on one side, sawdust on the other. Both are about making things that hold up.

+
+ +

Engineering

+
+ +
+
+
+
Engineering
+

How Meshery empowered my teams

+

Why Meshery became core to the platform I engineer, and what changed for the developers who use it.

+
+
+ +
+ +

Workshop

+
+ +
+
+
+
Workshop
+

Building a walnut dining table

+

From rough boards to a table that seats six, and what a cut list has in common with an architecture diagram.

+
+
+
+
+
+ + + + diff --git a/blog/kubectl-to-kanvas-snapshots/index.html b/blog/kubectl-to-kanvas-snapshots/index.html new file mode 100644 index 0000000..0420e12 --- /dev/null +++ b/blog/kubectl-to-kanvas-snapshots/index.html @@ -0,0 +1,124 @@ + + + + + + From kubectl to a picture of my whole cluster | Michael Vance + + + + + + + + + + + + + + + +
+
+

Engineering / Kanvas

+

From kubectl to a picture of my whole cluster

+

A JSON export, one kubectl plugin, and a script that keeps a snapshot of my clusters on this site.

+ +
+
A diagram of an ingress connected to a service, which connects to three workloads.
+ +
+

I found Kanvas by accident, while doing something very ordinary. I wanted a quick snapshot of the state of my clusters and of the applications running on them, and I was doing it the way I have for years: with kubectl.

+ +

The old way: a wall of text

+

I would export what I cared about to a file. Usually that was YAML, but in this case it was JSON. Either works.

+ +
kubectl get deployments,statefulsets,services,ingresses,configmaps \
+  --all-namespaces -o json > cluster.json
+ +

That gives you everything, and it is also nearly impossible to read. Thousands of lines describe which things exist, but the thing I actually wanted to know was how they relate: which ingress routes to which service, which service selects which workload, what depends on what.

+ +

The shortcut: the Kanvas snapshot plugin

+

That is where the kubectl plugin for Kanvas came in. I fed it the export, and it gave me a snapshot: a visual diagram of the relationships between all of the infrastructure I had configured. For the first time I could see the cluster instead of reading it.

+ +
# 1. Export the live state
+kubectl get deployments,statefulsets,services,ingresses --all-namespaces -o json > cluster.json
+
+# 2. Turn it into a visual snapshot
+kubectl kanvas snapshot cluster.json
+ +

And the part I like most: I can do it as many times as I want. It is cheap, it does not change anything in the cluster, and it is repeatable. A snapshot before a change and one after is the fastest review of an infrastructure change I have found.

+ +

Making it a habit

+

I do this a lot, so I stopped doing it by hand. I now run a small bash script that takes snapshots over time and updates my personal website, the one you are reading. The history ends up as a record of how my clusters changed.

+ +
#!/usr/bin/env bash
+# scripts/snapshot-clusters.sh
+# Export live cluster state, snapshot it, and commit the result to the site.
+set -euo pipefail
+
+CONTEXTS="${CONTEXTS:-$(kubectl config current-context)}"
+OUT_DIR="${OUT_DIR:-assets/snapshots}"
+STAMP="$(date -u +%Y-%m-%dT%H%M%SZ)"
+# Override if your plugin invocation differs.
+SNAPSHOT_CMD="${SNAPSHOT_CMD:-kubectl kanvas snapshot}"
+
+mkdir -p "$OUT_DIR"
+
+for ctx in $CONTEXTS; do
+  raw="$(mktemp)"
+  kubectl --context "$ctx" get deployments,statefulsets,services,ingresses \
+    --all-namespaces -o json > "$raw"
+
+  # Keep the raw export beside the visual snapshot for later diffing.
+  cp "$raw" "$OUT_DIR/${ctx}-${STAMP}.json"
+  $SNAPSHOT_CMD "$raw"
+  rm -f "$raw"
+done
+
+git add "$OUT_DIR"
+git commit -s -m "snapshots: $STAMP" || echo "no changes to commit"
+git push
+ +

Run it from cron or a scheduled workflow, and the site keeps itself up to date. Because everything is committed, the same GitHub Actions workflow that publishes this site publishes the new snapshots.

+ +
+ A caution + Exports can contain more than you expect. Stick to resource kinds that do not carry secrets (leave out secrets entirely), and review what you are about to publish before pointing this at a public repository. +
+ +

Why it stuck

+ +

It is the same idea that moved my team from a Wall of YAML to Infrastructure as Design. Code is how you run a system. A diagram is how you understand it.

+ + +
+
+ + + + diff --git a/blog/meshery-empowers-teams/index.html b/blog/meshery-empowers-teams/index.html new file mode 100644 index 0000000..ba5181d --- /dev/null +++ b/blog/meshery-empowers-teams/index.html @@ -0,0 +1,90 @@ + + + + + + How Meshery empowered my teams | Michael Vance + + + + + + + + + + + + + + + +
+
+

Engineering / Meshery

+

How Meshery empowered my teams

+

Why Meshery became core to the platform I engineer, and what changed for the developers who use it.

+ +
+
Four team boxes connected by dashed lines to a central platform block.
+ +
+

I used to run a platform team that behaved like a help desk. Developers filed a ticket, we wrote or edited a Terraform module, and a few days later their environment appeared. It worked, in the sense that everything eventually got built. It also made my team the slowest part of every project.

+ +

The problem was not the code

+

The infrastructure code was fine. The problem was that only a few people could read it, and none of the developers could see the whole system. A misplaced bracket in a large module once caused an outage, and the post-mortem made the real issue obvious: we had optimized for execution and ignored understanding.

+ +

What Meshery changed

+

Meshery is now core to the platform I engineer at work. It gave us three things we did not have before.

+
    +
  1. A shared model. Components, relationships, and designs give everyone, from a new hire to a staff engineer, the same picture of how services connect.
  2. +
  3. Guardrails that are enforced, not documented. Instead of a wiki page of rules, the platform checks designs against organizational policy as they are composed.
  4. +
  5. Self-service with feedback. A developer composes an environment visually and sees immediately whether it fits the rules, without waiting for review.
  6. +
+ +

What it did for the teams

+

The effect on my own team was as important as the effect on developers. We stopped being the gatekeepers and became the people who design the road.

+ +
When the platform is a product, the best sign of success is the ticket nobody had to file.
+ +

Pairing it with agents

+

The same model that helps new engineers also helps AI agents. With mesheryctl-axi, my team and I can ask an agent to inspect connections, read designs, and explain how a system fits together, then decide what to do. It speeds up the work without taking judgment out of it.

+ +

What I would tell another platform lead

+ + + +
+
+ + + + diff --git a/blog/mesheryctl-axi/index.html b/blog/mesheryctl-axi/index.html new file mode 100644 index 0000000..0c35c9b --- /dev/null +++ b/blog/mesheryctl-axi/index.html @@ -0,0 +1,108 @@ + + + + + + Meshery is AI-centric, and mesheryctl-axi shows it | Michael Vance + + + + + + + + + + + + + + + +
+
+

Engineering / Meshery

+

Meshery is AI-centric, and mesheryctl-axi shows it

+

A token-efficient, agent-friendly wrapper that makes mesheryctl easy for people and agents to drive.

+ +
+
A terminal running mesheryctl-axi system status next to a small node graph.
+ +
+

Meshery is core to the platform I engineer at work, and I have been enjoying it more with every month that AI agents become a normal part of how my team operates. The reason is simple: Meshery was built around a model of infrastructure (components, relationships, designs, connections) that an agent can reason about just as well as a person can. The tool that makes that obvious in daily use is mesheryctl-axi.

+ +

What "AXI" means here

+

AXI stands for agent experience interface. It is a thin wrapper around an existing CLI that reshapes the output so it is cheap and unambiguous for an AI agent to read, and still pleasant for a human. mesheryctl-axi wraps mesheryctl and gives it a small, discoverable surface.

+ +
$ mesheryctl-axi --help
+usage: mesheryctl-axi [command] [args] [flags]
+commands[5]:
+  (none)=home, connection, system, design, model, component
+flags[2]:
+  --help, -v/-V/--version
+requires:
+  mesheryctl installed and authenticated
+ +

Five command groups, and a home view when you give it nothing. That is the entire mental model, and it fits in the first screen of output.

+ +

Why this works so well for agents

+ + +
# What is the state of my Meshery server?
+mesheryctl-axi system status
+
+# What clusters and cloud accounts does Meshery know about?
+mesheryctl-axi connection list
+
+# What designs exist, and what is inside one?
+mesheryctl-axi design list
+mesheryctl-axi design content my-design --format yaml
+
+# What can Meshery model, and what are the components?
+mesheryctl-axi model content kubernetes --format json
+mesheryctl-axi component list
+ +

A workflow I actually use

+

When something looks off in a cluster, I now start by asking an agent a plain-language question and letting it drive mesheryctl-axi. It checks system status, lists the connection entries to find the right cluster, pulls the relevant design in YAML, and tells me where the running state and the intended design diverge.

+

That is not a demo. It is a few seconds of reading that used to take me a handful of terminal tabs and a lot of scrolling.

+ +
+ Keeping it current + mesheryctl-axi update --check reports your version against the latest published one, and mesheryctl-axi update upgrades it. Set MESHERYCTL_BIN if your mesheryctl binary is not on your PATH. +
+ +

Why I call Meshery AI-centric

+

Plenty of tools bolt a chat box onto an old interface. Meshery's strength is underneath. Everything is a typed, relationship-aware model: a component knows what it can connect to, a design is a document that describes a whole system, and a connection is a first-class thing. Those are exactly the shapes that agents are good at working with. A wrapper like mesheryctl-axi only has to get out of the way.

+

In the next post I will cover what that has meant for the teams who build on the platform.

+ + +
+
+ + + + diff --git a/blog/walnut-dining-table/index.html b/blog/walnut-dining-table/index.html new file mode 100644 index 0000000..879e107 --- /dev/null +++ b/blog/walnut-dining-table/index.html @@ -0,0 +1,95 @@ + + + + + + Building a walnut dining table | Michael Vance + + + + + + + + + + + + + + + +
+
+

Workshop

+

Building a walnut dining table

+

From rough boards to a table that seats six, and what a cut list has in common with an architecture diagram.

+ +
+
Blueprint-style drawing of a walnut dining table showing top and front views with dimensions.
+ +
+

My wife and I had been eating at a table that was too small for six people for about four years. I kept saying I would build a replacement. This summer I finally ran out of excuses, bought a stack of rough walnut, and cleared the garage.

+ +

Design first, saw second

+

I spent more evenings on paper than at the saw. I sketched the top, the base, and every joint, then redrew it until I could explain why each piece existed. The habit comes straight from my day job: if you cannot draw how the parts connect, you are not ready to build them.

+

The plan was a 72 by 38 inch top on a trestle-style base, with the legs joined to the stretcher by through mortise and tenon joints. No screws visible, no brackets, nothing a future owner would have to guess about.

+ + + + + + + + + + +
Cut list, in the order I milled it
PartQtyFinished sizeNotes
Top boards572 x 7.6 x 1.25 inBook-matched where the grain allowed
Legs228 x 4 x 3 inMortised for the stretcher
Stretcher148 x 4 x 2 inThrough tenons, wedged
Breadboard ends238 x 3 x 1.25 inAllow for seasonal movement
+ +

Milling is the unglamorous part that decides everything

+

Rough lumber is honest. It cups, twists, and hides surprises. I let the boards sit in the garage for two weeks, then milled them in stages: joint one face, joint one edge, plane to thickness, rip to width. Between each stage I stacked and stickered the boards and let them settle.

+

That patience paid off at glue-up. Boards that had been flattened gradually met each other with no gaps, and I needed far fewer clamps than I feared.

+ +

Wood moves, so design for it

+

The single most useful lesson of the build was seasonal movement. A solid wood top will swell and shrink across the grain through the year. If you pin it down rigidly, it will crack. The breadboard ends are attached with a center tenon glued and the outer ones left free in elongated holes, so the top can move while staying flat.

+
Good joinery is a constraint you design on purpose. Bad joinery is a constraint the material enforces later.
+

I think about platform guardrails the same way. A rule that fights how people actually work will get cracked and worked around. A rule that leaves room for the natural movement of the system holds for years.

+ +

Finish

+

I used three coats of a hardwax oil, sanding lightly between coats. Walnut does most of the work on its own, so I wanted the finish to disappear and let the grain speak.

+ +

What I would do differently

+ + +

The table has been in daily use for a few months now. Dinner is louder and the table is just big enough, which is the best review a table can get.

+ + +
+
+ + + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..a90eb4b --- /dev/null +++ b/index.html @@ -0,0 +1,206 @@ + + + + + + Michael Vance | Platform engineer and weekend woodworker + + + + + + + + + + + + + + + + + + +
+
+
+
+
+

Platform Engineering Lead / Indianapolis, IN

+

I design platforms by day and build things by weekend.

+

A decade in cloud-native infrastructure, a garage full of hand tools, and one conviction: whether it is a Kubernetes platform or a walnut table, the joinery is what holds it together.

+
+ Code dictates what is built; design communicates why and how it connects. + MY WORKING RULE +
+ +
+
+ Pixel-art portrait of Michael Vance in a red flannel shirt and denim overalls with a tape measure on his belt, in front of cedar shingles. +
Michael VanceTape measure: always on hand
+
+
+ +
+
Experience
12+ years
+
Developers enabled
200+
+
Provisioning time
4 days to 15 min
+
Team led
8 engineers
+
+
+
+ +
+
+
SEC. 01

About

+
+
+

I am a pragmatic, forward-thinking platform engineer. I am known for staying calm during severity-1 outages, and I have built a career around one goal: reducing cognitive load for the developers who depend on the platform.

+

I started as a junior sysadmin at a logistics company in downtown Indianapolis, fell for Infrastructure as Code, and became the person who could automate days of manual work into seconds. Terraform, Ansible, and Helm were my golden hammers.

+

Then I built a "Wall of YAML". As the engineering team grew from 20 to 150 developers, almost every infrastructure change became a ticket for my team. A post-mortem for an outage caused by one misplaced bracket in a huge Terraform module made it clear: code is great for execution and terrible for shared mental models.

+
+
+

So I changed the approach. I now champion Infrastructure as Design: visual, collaborative, and guardrailed. Developers compose their environments on a canvas, the platform generates the compliant code underneath, and everyone can see how the services connect.

+

Today I lead a platform team with a clear mandate: treat the platform as a product, and make the developer experience intuitive, visual, and self-service.

+

Away from the keyboard, I am in the garage. Woodworking taught me the same lesson from the other direction: measure twice, design the joint before you cut it, and respect the grain you are given.

+
+
+
+
+ +
+
+
SEC. 02

Work history

+
    +
  1. +

    Platform Engineering Lead

    +

    Apex Financial Technologies / Indianapolis, IN / Aug 2021 - Present

    +
      +
    • Led the shift from purely code-based infrastructure modules (Terraform) to visual Infrastructure as Design, enabling 200+ developers to compose and provision their own environments.
    • +
    • Cut average infrastructure provisioning time from 4 days (Jira tickets) to 15 minutes (self-service design canvases).
    • +
    • Integrated Meshery into the developer portal for real-time visual feedback on infrastructure configurations and automatic enforcement of organizational guardrails.
    • +
    • Lead a team of 8 platform and site reliability engineers, treating the internal developer platform as a first-class product.
    • +
    +
  2. +
  3. +

    Senior DevOps Engineer

    +

    Midwest Cloud Logistics / Indianapolis, IN / Feb 2017 - Aug 2021

    +
      +
    • Architected and maintained multi-region Kubernetes clusters serving 50,000+ daily active users.
    • +
    • Authored 100+ reusable Terraform modules and Helm charts to standardize microservice deployments.
    • +
    • Spotted the limits of massive IaC repositories when onboarding times spiked, and started the first internal pilots of visual topology mapping.
    • +
    • Implemented GitOps workflows with ArgoCD, reducing deployment errors by 40%.
    • +
    +
  4. +
  5. +

    Cloud Systems Administrator

    +

    IndyTech Solutions / Carmel, IN / Jun 2013 - Jan 2017

    +
      +
    • Automated legacy on-premise migrations to AWS using Ansible and Python.
    • +
    • Managed patching, networking, and access controls for a fleet of 300+ EC2 instances.
    • +
    • Primary incident commander for off-hours infrastructure alerts.
    • +
    +
  6. +
+

Education: B.S. in Computer Science, Indiana University, Bloomington (2013)

+
+
+ +
+
+
SEC. 03

Two workbenches

+
+
+ Workbench 01 / Engineering +

The blueprint side

+

Designing platforms, reading topologies, and removing bottlenecks.

+
    +
  • Infrastructure as Design
  • Infrastructure as Code
  • GitOps
  • Platform as a Product
  • DevSecOps
  • +
  • Kubernetes
  • AWS (EKS, VPC, IAM)
  • GCP
  • Terraform
  • Pulumi
  • Helm
  • +
  • ArgoCD
  • GitHub Actions
  • GitLab CI
  • Meshery
  • Kanvas
  • Envoy
  • Linux
  • CCNA
  • +
+
+
+ Workbench 02 / Workshop +

The sawdust side

+

Furniture, joinery, and the occasional tree that has to come down. Same discipline, different material.

+
    +
  • Furniture building
  • Hand-cut joinery
  • Hardwoods
  • Milling lumber
  • Tree felling
  • Shop safety
  • +
+

Read how the dining table came together

+
+
+
+
+ +
+
+
SEC. 04

Latest from both benches

+
+ +
+
+
+
Workshop
+

Felling the oak next to the house

+

A tree, a roofline, and a plan. Why the cut that matters most is the one you make before you start the saw.

+
+
+ +
+

All five posts

+
+
+ +
+
+
+

Let's talk platforms, or planers.

+

Whether you are untangling a Wall of YAML, designing a developer platform, or deciding between hard maple and white oak, I like a good conversation.

+ +
+
+
+
+ + + + diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..c2a49f4 --- /dev/null +++ b/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Allow: / diff --git a/scripts/check-site.py b/scripts/check-site.py new file mode 100644 index 0000000..82a8b7b --- /dev/null +++ b/scripts/check-site.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +"""Fail the build if the static site has broken local links, missing alt text, +missing titles, or the banned em dash. Standard library only.""" +import pathlib +import re +import sys +from html.parser import HTMLParser + +ROOT = pathlib.Path(__file__).resolve().parent.parent +SKIP_DIRS = {".git", ".github", "scripts", "_site", ".remember", "node_modules"} + + +class Page(HTMLParser): + def __init__(self): + super().__init__() + self.refs, self.ids, self.errors = [], set(), [] + self.title = "" + self._in_title = False + + def handle_starttag(self, tag, attrs): + a = dict(attrs) + if "id" in a: + self.ids.add(a["id"]) + if tag == "title": + self._in_title = True + if tag == "img" and "alt" not in a: + self.errors.append(f" has no alt attribute") + for key in ("href", "src"): + if key in a: + self.refs.append(a[key]) + + def handle_endtag(self, tag): + if tag == "title": + self._in_title = False + + def handle_data(self, data): + if self._in_title: + self.title += data + + +def pages(): + for p in sorted(ROOT.rglob("*.html")): + if not SKIP_DIRS & set(p.relative_to(ROOT).parts): + yield p + + +def resolve(page, ref): + target = (page.parent / ref.split("#")[0].split("?")[0]).resolve() + if target.is_dir(): + target = target / "index.html" + return target + + +def main(): + parsed, problems = {}, [] + for p in pages(): + text = p.read_text(encoding="utf-8") + parser = Page() + parser.feed(text) + parsed[p.resolve()] = parser + rel = p.relative_to(ROOT) + if "—" in text: + problems.append(f"{rel}: contains an em dash") + if not parser.title.strip(): + problems.append(f"{rel}: missing ") + problems += [f"{rel}: {e}" for e in parser.errors] + + for page, parser in parsed.items(): + rel = page.relative_to(ROOT) + for ref in parser.refs: + if re.match(r"^(https?:|mailto:|tel:|data:|//)", ref) or ref.startswith("/"): + continue + if ref.startswith("#"): + if ref[1:] and ref[1:] not in parser.ids: + problems.append(f"{rel}: anchor {ref} not found") + continue + target = resolve(page, ref) + if not target.exists(): + problems.append(f"{rel}: broken link {ref}") + elif "#" in ref and target in parsed and ref.split("#")[1] not in parsed[target].ids: + problems.append(f"{rel}: anchor in {ref} not found") + + if problems: + print("\n".join(problems)) + sys.exit(1) + print(f"OK: {len(parsed)} pages checked") + + +main() diff --git a/scripts/snapshot-clusters.sh b/scripts/snapshot-clusters.sh new file mode 100755 index 0000000..c95f29f --- /dev/null +++ b/scripts/snapshot-clusters.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# scripts/snapshot-clusters.sh +# Export live cluster state, snapshot it, and commit the result to the site. +set -euo pipefail + +CONTEXTS="${CONTEXTS:-$(kubectl config current-context)}" +OUT_DIR="${OUT_DIR:-assets/snapshots}" +STAMP="$(date -u +%Y-%m-%dT%H%M%SZ)" +# Override if your plugin invocation differs. +SNAPSHOT_CMD="${SNAPSHOT_CMD:-kubectl kanvas snapshot}" + +mkdir -p "$OUT_DIR" + +for ctx in $CONTEXTS; do + raw="$(mktemp)" + kubectl --context "$ctx" get deployments,statefulsets,services,ingresses \ + --all-namespaces -o json > "$raw" + + # Keep the raw export beside the visual snapshot for later diffing. + cp "$raw" "$OUT_DIR/${ctx}-${STAMP}.json" + $SNAPSHOT_CMD "$raw" + rm -f "$raw" +done + +git add "$OUT_DIR" +git commit -s -m "snapshots: $STAMP" || echo "no changes to commit" +git push