This is an interactive Terraform tutorial website built with VitePress (Markdown-driven static site generator) and Killercoda (cloud sandbox provider). The tutorial content is written in Chinese (zh-CN).
- Frontend: VitePress site under
docs/, deployed to GitHub Pages via GitHub Actions. - Sandbox scenarios: Killercoda scenario definitions under
terraform-tutorial/, each providing a real Linux terminal with Terraform + LocalStack pre-installed. - CI/CD:
.github/workflows/deploy.yml— pushes tomaintriggernpm run build→ deploy to GitHub Pages.
docs/ # VitePress content (Markdown files)
index.md # Homepage (layout: home), NOT a tutorial chapter
intro.md # Chapter: course introduction
basics.md # Chapter: Init / Plan / Apply
state.md # Chapter: state management
tflint.md # Chapter: TFLint linting
modules.md # Chapter: module patterns
.vitepress/
config.mjs # VitePress config (sidebar auto-managed)
theme/index.js # Custom theme — registers global Vue components
components/
KillercodaEmbed.vue # <KillercodaEmbed> component (link button, NOT iframe)
terraform-tutorial/ # Killercoda scenario definitions
structure.json # Lists all scenarios for Killercoda discovery
terraform-basics/ # One directory per scenario
index.json # Scenario metadata, step list, asset mapping, init scripts
init/
background.sh # Silent setup (sources setup-common.sh, seeds files)
foreground.sh # User-facing progress messages
init.md # Intro page shown before Step 1
step1/text.md # Each step is a directory with text.md
step2/text.md
step3/text.md
finish/finish.md # Completion page
assets/ # Files copied into the student's environment
setup-common.sh # AUTO-GENERATED — do not edit (copied by sync-setup)
main.tf
docker-compose.yml
scripts/
setup-common.sh # Shared setup functions (SOURCE OF TRUTH)
sync-setup-common.mjs # Copies setup-common.sh into every scenario's assets/
sync-sidebar.mjs # Auto-generates sidebar from docs/*.md frontmatter
.github/workflows/deploy.yml # GitHub Pages deployment pipeline
- Create
docs/<slug>.mdwith required frontmatter:--- order: <number> # Sidebar sort order (lower = higher) title: <display text> # Sidebar label (falls back to first H1 heading) ---
- If the chapter has a hands-on lab, link to the sandbox:
Note: Killercoda blocks iframe embedding (
<KillercodaEmbed src="https://killercoda.com/lonegunman-terraform-tutorial/course/terraform-tutorial/<SCENARIO_NAME>" />
X-Frame-Options: DENY), so the component renders a link button that opens in a new tab. - Run
npm run sync-sidebar(or it runs automatically duringnpm run buildvia theprebuildhook). This updates the// @auto-sidebar-start ... // @auto-sidebar-endblock inconfig.mjs.
Follow the structure in https://github.com/killercoda/scenarios-istio.
- Create a new directory under
terraform-tutorial/<scenario-name>/. - Add the scenario to
terraform-tutorial/structure.json:{ "path": "<scenario-name>" } - Every scenario MUST use this directory layout:
<scenario-name>/ index.json init/ background.sh foreground.sh init.md step1/text.md step2/text.md ... finish/finish.md assets/ setup-common.sh # AUTO-GENERATED — do not edit main.tf docker-compose.yml - The
index.jsonMUST reference init scripts in theintroblock:{ "details": { "intro": { "text": "init/init.md", "background": "init/background.sh", "foreground": "init/foreground.sh" }, "steps": [ { "title": "...", "text": "step1/text.md" } ], "finish": { "text": "finish/finish.md" }, "assets": { "host01": [ { "file": "setup-common.sh", "target": "/root", "chmod": "+x" }, { "file": "main.tf", "target": "/root/workspace" } ] } }, "backend": { "imageid": "ubuntu" }, "interface": { "layout": "editor-terminal" } }- Steps use
stepN/text.mdpaths (directory-based, NOT flatstepN.md) - Assets use filenames relative to the
assets/directory (NOTworkspace/main.tf) - Killercoda asset copy only uses the basename of
file, placed into thetargetdir.{"file": "step1/main.tf", "target": "/root/workspace"}copies to/root/workspace/main.tf(NOT/root/workspace/step1/main.tf). If multiple files share the same basename they overwrite each other — settargetto the full subdirectory path (e.g."target": "/root/workspace/step1") to avoid collisions. - The
backgroundandforegroundkeys underintroare what make the scripts execute setup-common.shMUST be the first asset, targeted to/rootwithchmod: "+x"
- Steps use
- The
init/background.shscript should:- Log to
/tmp/background.logwithexec > /tmp/background.log 2>&1andset -xfor debugging source /root/setup-common.shto load shared functions- Create
/root/workspaceand seed files as fallback (wrapped inif [ ! -f ... ]) - Call shared functions:
install_terraform,start_localstack,install_theia_plugin,finish_setup - Optionally call
install_awscli(AWS CLI v2 +awslocalwrapper) for scenarios that need AWS CLI verification - Optionally call
install_tflint(only in scenarios that need it) - For scenarios needing pre-applied state, call
terraform init/terraform applybeforefinish_setup - Debugging: All output is captured in
/tmp/background.log. When a Killercoda scenario fails during environment setup (e.g.terraform applyerrors like "connection refused", tools not found), ask the user to runcat /tmp/background.login the Killercoda terminal and share the output. This log contains the full trace (set -x) of every command executed during setup, which is essential for diagnosing issues like failed downloads, missing packages, or services not starting.
- Log to
- The
init/foreground.shpollswhile [ ! -f /tmp/.setup-done ]and prints progress messages. - The
assets/main.tfmust configure the AWS provider to use LocalStack endpoints (http://localhost:4566) with fake credentials (access_key = "test",secret_key = "test"), skip credential validation, and sets3_use_path_style = true. - The
assets/docker-compose.ymlmust uselocalstack/localstack:3image, expose port 4566, setSERVICESto only the needed AWS services, and limit memory to 1536M.
For Azure-flavoured scenarios (e.g. terraform-cli-apply-azure), substitute LocalStack with miniblue:
init/background.shcallsstart_miniblueinstead ofstart_localstack. Do NOT call both — each scenario picks one cloud emulator.assets/docker-compose.ymluses imageghcr.io/lonegunmanb/miniblue:sha-11ef0e8(a fork that fixes case-insensitive ARM routing for azurerm v4 compatibility, persists blob metadata on?comp=metadataand plainPUT {blob}, and adds theMINIBLUE_DISABLE_SHAREDKEY_AUTHenv var so the azurerm backend's lock + state-write flow works without per-request SharedKey signing; version pinned by SHA tag, neverlatest), exposes ports4566(HTTP) and4567(HTTPS), and limits memory to 512M. The composeenvironment:block setsMINIBLUE_STORAGE_ENDPOINT=http://localhost:4566(so ARM returns local blob endpoints) andMINIBLUE_DISABLE_SHAREDKEY_AUTH=1(so curl/azlocal can hit the data plane without HMAC-SHA256 signing — strictly a tutorial convenience; real Azure always requires SharedKey). Do NOT bind-mount the cert directory — the image is distroless and runs asnonroot, so the cert lives at/home/nonroot/.miniblue/cert.peminside the container;start_miniblueextracts it viadocker cpinstead.start_minibluewritesSSL_CERT_FILE=/root/.miniblue/cert.pemto both/etc/profile.d/miniblue.shand/root/.bashrc(Killercoda terminals are non-login shells), and exports it for the current shell. It ALSO installs the cert into the system CA trust store viaupdate-ca-certificates(/usr/local/share/ca-certificates/miniblue.crt) — this is the primary trust mechanism, because Killercoda terminals are opened BEFOREbackground.shruns, so the~/.bashrcappend does not affect the user's existing shell. With the cert in the system store, Terraform/azurerm trustshttps://localhost:4567regardless ofSSL_CERT_FILE.- After
start_miniblue, callinstall_azlocalto extract the bundledazlocalbinary (/azlocalinside the container) to/usr/local/bin/.azlocalis a standalone Go CLI (noazdependency) that talks to miniblue over HTTP 4566 — use it in step text instead of rawcurlfor resource verification (azlocal group list,azlocal dns zone list --resource-group ...,azlocal network vnet list --resource-group ...,azlocal network vnet subnet list --resource-group <rg> --vnet-name <vnet>). assets/main.tfMUST use the azurerm v4 provider:Do NOT useterraform { required_providers { azurerm = { source = "hashicorp/azurerm" version = "~> 4.0" } } } provider "azurerm" { features {} metadata_host = "localhost:4567" resource_provider_registrations = "none" # v4 replacement for v3's skip_provider_registration subscription_id = "00000000-0000-0000-0000-000000000000" tenant_id = "00000000-0000-0000-0000-000000000001" client_id = "miniblue" client_secret = "miniblue" }
~> 3.0or the deprecatedskip_provider_registration = true— this tutorial standardises on azurerm v4.- miniblue accepts any UUID/string for credentials; do not put real Azure secrets anywhere.
- Source of truth:
scripts/setup-common.sh— edit ONLY this file for shared logic. - Auto-copied:
scripts/sync-setup-common.mjscopies it into everyterraform-tutorial/*/assets/directory. - Run
npm run sync-setupafter editing, or it runs automatically viaprebuild. - Do NOT edit
terraform-tutorial/*/assets/setup-common.shdirectly — changes will be overwritten. - Available functions:
install_terraform,install_awscli,install_tflint,start_localstack,start_miniblue,install_azlocal,install_theia_plugin,finish_setup. install_awscliinstalls AWS CLI v2 (official binary) and creates anawslocalshell wrapper that sets--endpoint-url=http://localhost:4566automatically.start_localstackauto-installs Docker Compose v2 plugin if missing before runningdocker compose up -d.start_minibluerunsdocker compose up -d, waits forhttp://localhost:4566/health, primes the HTTPS port to materialise the self-signed cert, copies the cert out viadocker cp, installs it into the system CA trust store (update-ca-certificates), and also exportsSSL_CERT_FILE=/root/.miniblue/cert.pemglobally via/etc/profile.d/miniblue.sh+~/.bashrcas a belt-and-braces fallback.- Versions can be overridden via env vars:
TERRAFORM_VERSION,TFLINT_VERSION. The miniblue container image tag is not an env var — it lives inscripts/miniblue-image.mjs(single source of truth) and is propagated into everydocker-compose.ymlandbackground.shheredoc bynpm run sync-miniblue.
- Source of truth:
scripts/miniblue-image.mjsexports the canonical image tag (e.g.ghcr.io/lonegunmanb/miniblue:sha-11ef0e8). - Auto-propagated:
scripts/sync-miniblue-image.mjsrewrites everyimage: ghcr.io/lonegunmanb/miniblue:sha-11ef0e8line interraform-tutorial/*/assets/docker-compose.yml,terraform-tutorial/*/init/background.sh, and the docs reference in this file. - Run
npm run sync-miniblueafter bumping, or it runs automatically viaprebuild. - Do NOT edit the image tag in any docker-compose.yml or background.sh by hand — change
miniblue-image.mjsand resync.
- The sidebar in
config.mjsis managed byscripts/sync-sidebar.mjs. Never edit the sidebar block manually — it will be overwritten. - The script reads frontmatter
orderandtitlefrom eachdocs/*.mdfile (excludingindex.md). - Setting
sidebar: falsein frontmatter hides a page from the sidebar. - The managed region is delimited by
// @auto-sidebar-startand// @auto-sidebar-endcomments inconfig.mjs. Do NOT remove or modify these markers.
- Registered globally in
docs/.vitepress/theme/index.js. - Renders as a styled link button (opens Killercoda in a new tab) because Killercoda blocks iframe embedding via
X-Frame-Options: DENY. - Props:
src(required, must behttps://...killercoda.com...),title(optional),height(optional, default"70vh"). - The component validates URLs — only
https://*.killercoda.comorigins are allowed.
| Command | Purpose |
|---|---|
npm run dev |
Start VitePress dev server with hot reload. Does NOT run sidebar sync — run npm run sync-sidebar manually after adding/removing .md files. |
npm run build |
Production build. Automatically runs prebuild (sidebar sync + setup sync) first. Output: docs/.vitepress/dist/ |
npm run preview |
Preview the production build locally. |
npm run sync-sidebar |
Manually sync sidebar config from docs/*.md frontmatter. |
npm run sync-setup |
Manually copy scripts/setup-common.sh into every scenario's assets/. |
- All tutorial prose is written in Chinese (zh-CN).
- Code comments in Terraform files may be in English or Chinese.
- Use VitePress Markdown extensions:
::: tip,::: warning,::: infofor callout blocks. - Each Killercoda step should be completable in 3–5 minutes.
background.shlogs to/tmp/background.log— check this file in Killercoda terminal for debugging.
- Do NOT edit the sidebar block in
config.mjsby hand. - Do NOT remove the
// @auto-sidebar-start/// @auto-sidebar-endmarkers. - Do NOT put real AWS credentials anywhere — all scenarios use LocalStack with
test/testfake credentials. - Do NOT add services to LocalStack's
SERVICESenv var unless the chapter actually uses them (memory is limited to 1.5GB). - Do NOT skip the
touch /tmp/.setup-donesignal at the end ofbackground.sh—foreground.shdepends on it. - Do NOT use
docker-compose(v1) — usedocker compose(v2 plugin) instead. - Do NOT place
background.sh/foreground.shat the scenario root — they must be ininit/and referenced inindex.json'sintroblock, otherwise they will not execute. - Do NOT use flat step files (
step1.md) — must bestep1/text.mddirectory format. - Do NOT edit
terraform-tutorial/*/assets/setup-common.shdirectly — it is auto-generated fromscripts/setup-common.shand will be overwritten bynpm run sync-setup. - Do NOT use dense inline backticks in Killercoda step files (
terraform-tutorial/*/step*/text.md) — the Killercoda renderer injects copy-button<span>elements on each inline code fragment, which breaks rendering. Use plain text or fenced code blocks instead. This does NOT affect the VitePress docs (docs/*.md).