498 lines
20 KiB
HTML
498 lines
20 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<link rel="icon" href="data:,">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>rig — local environment installer</title>
|
|
<style>
|
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap');
|
|
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
body {
|
|
background: #0a0e17;
|
|
color: #e8eaf0;
|
|
font-family: 'Inter', sans-serif;
|
|
line-height: 1.6;
|
|
height: 100vh;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
header {
|
|
padding: 16px 24px;
|
|
border-bottom: 1px solid #1e2a4a;
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 16px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
header h1 {
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 22px;
|
|
font-weight: 600;
|
|
letter-spacing: 3px;
|
|
color: #0066ff;
|
|
}
|
|
|
|
header .subtitle {
|
|
font-size: 13px;
|
|
color: #4a5568;
|
|
letter-spacing: 1px;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.layout { display: flex; flex: 1; min-height: 0; }
|
|
|
|
nav {
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: 200px;
|
|
flex-shrink: 0;
|
|
background: #121829;
|
|
border-right: 1px solid #1e2a4a;
|
|
padding: 8px 0;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
nav a {
|
|
padding: 10px 20px;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 12px;
|
|
color: #8892a8;
|
|
text-decoration: none;
|
|
border-left: 2px solid transparent;
|
|
transition: all 0.15s;
|
|
cursor: pointer;
|
|
}
|
|
|
|
nav a:hover { color: #e8eaf0; background: #1a2340; }
|
|
nav a.active { color: #0066ff; border-left-color: #0066ff; background: #0d1a33; }
|
|
|
|
main { flex: 1; overflow: auto; padding: 32px 48px; }
|
|
|
|
.section { display: none; animation: fadeIn 0.2s ease; }
|
|
.section.active { display: block; }
|
|
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
|
|
|
.section h2 {
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 15px;
|
|
font-weight: 500;
|
|
color: #8892a8;
|
|
margin-bottom: 8px;
|
|
letter-spacing: 1px;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.section > p.lede {
|
|
font-size: 13px;
|
|
color: #4a5568;
|
|
margin-bottom: 24px;
|
|
max-width: 800px;
|
|
}
|
|
|
|
.prose { max-width: 820px; }
|
|
.prose p { font-size: 14px; color: #b4bccf; line-height: 1.7; margin-bottom: 14px; }
|
|
.prose p b { color: #e8eaf0; }
|
|
.prose ul { margin: 0 0 16px 20px; }
|
|
.prose li { font-size: 14px; color: #b4bccf; margin-bottom: 6px; }
|
|
|
|
.prose h3 {
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 13px;
|
|
text-transform: uppercase;
|
|
color: #e8eaf0;
|
|
margin: 32px 0 10px;
|
|
letter-spacing: 1px;
|
|
}
|
|
|
|
.prose code, pre code {
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 12px;
|
|
color: #7ab0ff;
|
|
background: #121829;
|
|
padding: 1px 5px;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
pre {
|
|
background: #121829;
|
|
border: 1px solid #1e2a4a;
|
|
padding: 16px;
|
|
overflow: auto;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
pre code { background: none; padding: 0; }
|
|
pre .c { color: #4a5568; }
|
|
pre .k { color: #0066ff; }
|
|
|
|
.graph-container { margin: 16px 0; }
|
|
.graph-container img {
|
|
display: block;
|
|
max-width: 100%;
|
|
background: #0a0e17;
|
|
border: 1px solid #1e2a4a;
|
|
padding: 12px;
|
|
}
|
|
|
|
dl {
|
|
display: grid;
|
|
grid-template-columns: max-content 1fr;
|
|
gap: 10px 24px;
|
|
margin: 16px 0;
|
|
max-width: 820px;
|
|
}
|
|
|
|
dt {
|
|
font-family: 'JetBrains Mono', monospace;
|
|
color: #0066ff;
|
|
font-size: 13px;
|
|
padding-top: 2px;
|
|
}
|
|
|
|
dd { font-size: 14px; color: #b4bccf; line-height: 1.6; }
|
|
|
|
table { border-collapse: collapse; margin: 16px 0; max-width: 820px; }
|
|
th, td {
|
|
text-align: left;
|
|
padding: 7px 16px 7px 0;
|
|
font-size: 13px;
|
|
border-bottom: 1px solid #1e2a4a;
|
|
color: #b4bccf;
|
|
}
|
|
th {
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 11px;
|
|
text-transform: uppercase;
|
|
color: #8892a8;
|
|
letter-spacing: 1px;
|
|
}
|
|
td code { white-space: nowrap; }
|
|
|
|
.note {
|
|
border-left: 2px solid #ffc107;
|
|
background: #17130a;
|
|
padding: 12px 16px;
|
|
margin: 16px 0;
|
|
max-width: 820px;
|
|
}
|
|
.note p { margin: 0; font-size: 13px; color: #b4bccf; }
|
|
.note b { color: #ffc107; }
|
|
|
|
.menu-toggle {
|
|
display: none;
|
|
background: transparent;
|
|
border: 1px solid #1e2a4a;
|
|
color: #8892a8;
|
|
padding: 6px 10px;
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
line-height: 1;
|
|
margin-left: auto;
|
|
}
|
|
.menu-toggle:hover { background: #1a2340; }
|
|
|
|
.nav-backdrop {
|
|
display: none;
|
|
position: absolute;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
z-index: 10;
|
|
}
|
|
.layout.nav-open .nav-backdrop { display: block; }
|
|
|
|
@media (max-width: 720px) {
|
|
header { padding: 10px 12px; gap: 8px; }
|
|
header h1 { font-size: 16px; letter-spacing: 1px; }
|
|
header .subtitle { display: none; }
|
|
.menu-toggle { display: inline-block; }
|
|
.layout { position: relative; }
|
|
nav {
|
|
position: absolute; left: 0; top: 0; bottom: 0;
|
|
width: 200px; z-index: 20;
|
|
transform: translateX(-100%);
|
|
transition: transform 0.2s ease;
|
|
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.5);
|
|
}
|
|
.layout.nav-open nav { transform: translateX(0); }
|
|
main { padding: 16px; }
|
|
.section h2 { font-size: 13px; }
|
|
.prose p, .prose li { font-size: 13px; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header>
|
|
<h1>RIG</h1>
|
|
<span class="subtitle">local environment installer</span>
|
|
<button class="menu-toggle">☰</button>
|
|
</header>
|
|
|
|
<div class="layout">
|
|
<div class="nav-backdrop"></div>
|
|
<nav>
|
|
<a href="#start">Start here</a>
|
|
<a href="#install">Installation</a>
|
|
<a href="#environments">Environments</a>
|
|
<a href="#overlays">Overlays</a>
|
|
<a href="#troubleshooting">Troubleshooting</a>
|
|
</nav>
|
|
|
|
<main>
|
|
|
|
<section class="section" id="start">
|
|
<h2>Start here</h2>
|
|
<div class="prose">
|
|
<p>rig builds a disposable Kubernetes environment on your machine, so you can
|
|
see how a system fits together without access to any of it. It is for
|
|
<b>onboarding and exploration</b>, not a production replica.</p>
|
|
|
|
<p>Most services in it are <b>not real</b>. What has to be faithful is the
|
|
topology: names, ports, dependency order, who can reach whom, and how it fails.
|
|
That is what makes a whole estate fit on a laptop. A real 20-service platform
|
|
does not fit once in 14 GB; stubs of about 30 MB each fit three times.</p>
|
|
|
|
<h3>The only prerequisite</h3>
|
|
<p><b>Docker.</b> No curl, no jq, no python, no apt repositories to configure.</p>
|
|
<pre><code><span class="c"># in the environment directory:</span>
|
|
make check <span class="c"># is this machine ready? reports, never changes anything</span>
|
|
make deps <span class="c"># install the pinned toolchain; safe to re-run</span>
|
|
make cluster up <span class="c"># cluster + registry + addons; ports derive by themselves</span>
|
|
</code></pre>
|
|
<p>Read <code>make check</code> before <code>make deps</code>: it lists what is
|
|
missing, including the steps only you can do, such as anything needing
|
|
<code>sudo</code> or a Windows-side restart.</p>
|
|
|
|
<h3>make cluster up</h3>
|
|
<p>It prints what the kind config locks in before spending the time, because
|
|
that config is fixed when the cluster is created. Re-running is safe and
|
|
continues where an interrupted run stopped, rather than reporting that the
|
|
cluster already exists and leaving every node <code>NotReady</code>.</p>
|
|
<pre><code>make cluster up <span class="c"># built-in defaults — no profile needed</span>
|
|
make cluster up PROFILE=mirror <span class="c"># images through an internal registry</span>
|
|
make cluster up OVERLAY=examples/data <span class="c"># an overlay: what runs, kept outside rig</span>
|
|
make cluster reset <span class="c"># destroy and rebuild: how an edited kind config takes effect</span>
|
|
</code></pre>
|
|
|
|
<h3>Checking on things</h3>
|
|
<dl>
|
|
<dt>make cluster list</dt><dd>Every cluster on the machine, its memory cost and its port block. A new cluster usually fails to start because of an old one; <code>make cluster free</code> stops them without deleting.</dd>
|
|
<dt>make check</dt><dd>Host, toolchain, and whether this cluster fits, its ports, registry and addons. Only problems print; <code>make check all</code> prints everything.</dd>
|
|
<dt>make check mem</dt><dd>Memory in depth: what caps it, how far it climbs, and on WSL the <code>.wslconfig</code> backup and restore.</dd>
|
|
<dt>make docs</dt><dd>Serves these pages from a throwaway container. It needs no cluster and no toolchain, because these are the instructions for building both.</dd>
|
|
</dl>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="section" id="install">
|
|
<h2>Installation</h2>
|
|
<div class="graph-container">
|
|
<a href="viewer.html?src=graphs/01-install.svg"><img src="graphs/01-install.svg" alt="How the installer works"></a>
|
|
</div>
|
|
<div class="prose">
|
|
<p>The installer is a container, not a shell script, because a stock slim Debian
|
|
has no curl, wget, jq, python3 or CA bundle: a shell installer could not make a
|
|
verified HTTPS request, let alone check one. The container carries its own
|
|
toolchain, so the host needs only Docker. Nothing runs inside it afterwards —
|
|
kind, kubectl, tilt and jq run natively.</p>
|
|
|
|
<p>Every tool is a single binary at a pinned version, checked against a published
|
|
SHA256. Node images are pinned <b>by digest</b>, so upgrading kind cannot move
|
|
your Kubernetes version.</p>
|
|
|
|
<h3>Two tiers</h3>
|
|
<p>A managed or corporate-issued machine is not somewhere to install development
|
|
tools by default, so installing cluster tooling is a decision rather than a side
|
|
effect:</p>
|
|
<table>
|
|
<tr><th>tier</th><th>installs</th><th>for</th></tr>
|
|
<tr><td><code>core</code></td><td>kubectl, jq</td><td>talk to a cluster someone else runs</td></tr>
|
|
<tr><td><code>dev</code></td><td>+ kind, tilt</td><td>build clusters and hot-reload into them</td></tr>
|
|
</table>
|
|
<pre><code>make deps core <span class="c"># kubectl and jq only — nothing that creates a cluster</span>
|
|
make deps <span class="c"># dev, the default</span>
|
|
</code></pre>
|
|
|
|
<h3>Air-gapped</h3>
|
|
<pre><code>make deps image full <span class="c"># bakes every binary into the image</span>
|
|
docker save …-deps:full | gzip > rig.tgz
|
|
<span class="c"># carry that one file in, then:</span>
|
|
docker load < rig.tgz && make cluster up PROFILE=offline <span class="c"># from env.d/offline.env.example</span>
|
|
</code></pre>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="section" id="environments">
|
|
<h2>Environments</h2>
|
|
<div class="graph-container">
|
|
<a href="viewer.html?src=graphs/02-environment.svg"><img src="graphs/02-environment.svg" alt="Environment derivation"></a>
|
|
</div>
|
|
<div class="prose">
|
|
<p>One folder is one environment. Running several versions of a system at once
|
|
means several clusters on one machine, and everything that could collide is
|
|
derived from the folder name (the overlay's, when one is named):</p>
|
|
<dl>
|
|
<dt>cluster + context</dt><dd><code>platform-v2/</code> builds <code>platform-v2</code> on <code>kind-platform-v2</code>.</dd>
|
|
<dt>port block</dt><dd>Ten ports from a hash of the name, above 20000 — clear of 80, 443, 3000, 5432, 8000 and 8080.</dd>
|
|
<dt>registry + images</dt><dd>Named after the environment, so two copies never share one.</dd>
|
|
</dl>
|
|
<p>So two copies never collide, and neither one's <code>make cluster down</code>
|
|
can touch the other. <code>make check</code> shows the block;
|
|
<code>bash ctrl/ports.sh persist</code> freezes it into <code>ctrl/.env</code>.</p>
|
|
|
|
<h3>Configuration layers</h3>
|
|
<p>Weakest first, later wins: built-in defaults → pinned versions → a profile, if
|
|
you name one → the overlay's <code>rig.env</code> → <code>ctrl/.env</code> → the
|
|
caller's environment. So a value on the command line always beats every file.</p>
|
|
|
|
<h3>Profiles</h3>
|
|
<p>A profile says how this machine reaches the world. rig needs none.</p>
|
|
<table>
|
|
<tr><th>example</th><th>registry</th><th>for</th></tr>
|
|
<tr><td>none</td><td><code>local</code></td><td>the built-in defaults</td></tr>
|
|
<tr><td><code>mirror.env.example</code></td><td><code>mirror</code></td><td>images through an internal registry</td></tr>
|
|
<tr><td><code>offline.env.example</code></td><td><code>local</code></td><td>air-gapped</td></tr>
|
|
</table>
|
|
|
|
<h3>Registry modes</h3>
|
|
<table>
|
|
<tr><th>mode</th><th>what it does</th></tr>
|
|
<tr><td><code>none</code></td><td>images are built straight into the node</td></tr>
|
|
<tr><td><code>local</code></td><td>a registry container wired into the cluster</td></tr>
|
|
<tr><td><code>mirror</code></td><td>that container as a pull-through cache of the corporate registry</td></tr>
|
|
<tr><td><code>remote</code></td><td>no local container; pull direct with an <code>imagePullSecret</code></td></tr>
|
|
</table>
|
|
<p><code>mirror</code> is what a locked-down network looks like: images come from
|
|
the corporate registry, you do not hammer it, and you keep working when the
|
|
connection drops. It is also only reachable from where that registry is routable —
|
|
from a laptop, use <code>local</code> or <code>offline</code>.</p>
|
|
<p><b>Corporate CA trust is the usual failure.</b> It has to reach three places:
|
|
the host Docker daemon, every node's containerd (nodes do not inherit host trust),
|
|
and any in-cluster client. Set <code>REGISTRY_CA_FILE</code>;
|
|
<code>make check</code> reports which is still missing. Without it the error is
|
|
<code>x509: certificate signed by unknown authority</code>.</p>
|
|
|
|
<h3>LoadBalancer services</h3>
|
|
<p>Real manifests use <code>type: LoadBalancer</code>. On a bare local cluster
|
|
those Services sit at <code>EXTERNAL-IP <pending></code> forever, with no
|
|
error: the deployment looks healthy and is unreachable. The <code>metallb</code>
|
|
addon fixes that, so upstream manifests work unchanged. Its address pool is
|
|
derived from the cluster's Docker network at install time, because Docker picks
|
|
that subnet and it differs between machines.</p>
|
|
<p>Those addresses are reachable from the Linux side, including inside WSL. A
|
|
browser on Windows has no route to them: use the ingress host ports instead.</p>
|
|
|
|
<h3>Networking and memory</h3>
|
|
<p>The cluster uses kind's built-in networking, which does enforce standard
|
|
NetworkPolicy — verified against a no-policy control. A pluggable CNI was tried
|
|
and removed: it added policy features nothing here needs, in exchange for a
|
|
slower boot and one more thing to get right at creation time.</p>
|
|
<p>Every cluster is a running container tree whether you use it or not.
|
|
<code>make cluster list</code> shows what each costs, and
|
|
<code>make cluster free</code> stops the others without deleting them.</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="section" id="overlays">
|
|
<h2>Overlays</h2>
|
|
<div class="prose">
|
|
<p>An overlay is one folder, kept outside rig's version control, holding a use
|
|
case. rig reads it and never writes into it. Every piece is optional:</p>
|
|
<table>
|
|
<tr><th>in the overlay</th><th>what rig does with it</th></tr>
|
|
<tr><td><code>rig.env</code></td><td>a config layer: addons, namespaces, images — anything a profile could set</td></tr>
|
|
<tr><td><code>k8s/overlays/dev/</code></td><td>the manifests the dev loop applies</td></tr>
|
|
<tr><td><code>kind-config.yaml.tpl</code></td><td>the cluster's shape, when it needs its own (mounts, ports)</td></tr>
|
|
<tr><td><code>addons/<name>.sh</code></td><td>addons, found before rig's own; each applies its own <code><name>.yaml</code></td></tr>
|
|
<tr><td><code>Tiltfile</code></td><td>the workload's half of the dev loop, included by rig's</td></tr>
|
|
</table>
|
|
<pre><code>cp -r examples/starter local/myenv <span class="c"># local/ is gitignored</span>
|
|
OVERLAY=local/myenv make cluster up
|
|
</code></pre>
|
|
<p>With none named, rig runs its own <code>examples/starter</code>.
|
|
<code>examples/data</code> carries postgres, redis and airflow as an overlay's own
|
|
addons. A project can also carry rig at <code><project>/rig/</code> and be
|
|
the overlay itself, with a three-line forwarding Makefile — see
|
|
<code>docs/notes/overlay.md</code>.</p>
|
|
|
|
<h3>What runs in it</h3>
|
|
<p>Each component is one of three things, and switching between them is a
|
|
one-line change:</p>
|
|
<dl>
|
|
<dt>real</dt><dd>Built from source and hot-reloaded. What you are working on — usually exactly one.</dd>
|
|
<dt>mock</dt><dd>A generic stub with canned responses. Everything you do not care about today.</dd>
|
|
<dt>remote</dt><dd>No pod at all: a Service of type <code>ExternalName</code> pointing at the real system. In-cluster DNS resolves identically, so callers never change.</dd>
|
|
</dl>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="section" id="troubleshooting">
|
|
<h2>Troubleshooting</h2>
|
|
<div class="prose">
|
|
<h3>Tilt stops noticing file changes</h3>
|
|
<p>Almost always inotify limits, and it fails silently: nothing errors, changes
|
|
just stop being picked up. Defaults on WSL are far too low.
|
|
<code>make check</code> reports it and prints the fix.</p>
|
|
|
|
<h3>Cluster creation dies halfway with a port error</h3>
|
|
<p>Docker reports <code>failed to bind host port … address already in use</code>
|
|
partway through creating the cluster. <code>make check</code> tests every port in
|
|
this environment's block before anything is built.</p>
|
|
|
|
<h3>Every node stays NotReady</h3>
|
|
<p>Usually a cluster created with the default CNI disabled and the real CNI never
|
|
installed, after an interrupted first run. Run <code>make cluster up</code> again:
|
|
it finishes the missing steps instead of stopping because the cluster exists.</p>
|
|
|
|
<h3>x509: certificate signed by unknown authority</h3>
|
|
<p>Corporate CA trust has not reached one of the three places it needs to be. See
|
|
<a href="#environments">Environments</a>.</p>
|
|
|
|
<h3>kubectl says the context does not exist</h3>
|
|
<p>The cluster can exist while its context does not — a reset or a switched
|
|
<code>KUBECONFIG</code> loses it. <code>make cluster up</code> detects this and
|
|
re-exports the context.</p>
|
|
</div>
|
|
</section>
|
|
|
|
</main>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
var layout = document.querySelector('.layout');
|
|
var main = document.querySelector('main');
|
|
|
|
function syncActive() {
|
|
var hash = location.hash.slice(1) || 'start';
|
|
document.querySelectorAll('.section').forEach(function (s) { s.classList.remove('active'); });
|
|
document.querySelectorAll('nav a').forEach(function (a) { a.classList.remove('active'); });
|
|
var section = document.getElementById(hash);
|
|
if (section) section.classList.add('active');
|
|
var link = document.querySelector('nav a[href="#' + hash + '"]');
|
|
if (link) link.classList.add('active');
|
|
if (main) main.scrollTop = 0;
|
|
layout.classList.remove('nav-open');
|
|
}
|
|
|
|
window.addEventListener('hashchange', syncActive);
|
|
window.addEventListener('DOMContentLoaded', syncActive);
|
|
syncActive();
|
|
|
|
document.addEventListener('click', function (e) {
|
|
if (e.target.closest('.menu-toggle') || e.target.closest('.nav-backdrop')) {
|
|
layout.classList.toggle('nav-open');
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|