Complete documentation suite for KiteStacks covering all 11 services across 2-host active-active architecture. Includes beginner track (with AI, 8 files) and advanced track (without AI, 7 files) with time estimates, real troubleshooting cases, and command-by-command explanations. Updates certifications roadmap to reflect July 7 2026 A+ Core 2 exam goal. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
333 lines
7.9 KiB
Markdown
333 lines
7.9 KiB
Markdown
# Without AI — Part 2: Bash Scripting
|
||
|
||
**Track:** Advanced (No AI)
|
||
**Time for this section:** 1–2 weeks
|
||
|
||
Bash is the language of the Linux shell. Almost every automation script in this
|
||
homelab is a Bash script. You do not need to master it — you need to be able to
|
||
read it, write simple scripts, and understand what a script does before you run it.
|
||
|
||
---
|
||
|
||
## What Is a Script?
|
||
|
||
A script is a text file containing a sequence of shell commands. Instead of typing
|
||
commands one by one, you put them in a file and run the file.
|
||
|
||
```bash
|
||
#!/usr/bin/env bash
|
||
# This is a comment
|
||
|
||
echo "Hello from my script"
|
||
```
|
||
|
||
The first line (`#!/usr/bin/env bash`) is called the **shebang**. It tells Linux
|
||
which interpreter to use to run this file. Without it, Linux may use the wrong shell.
|
||
|
||
To run a script:
|
||
```bash
|
||
chmod +x myscript.sh # Make it executable
|
||
./myscript.sh # Run it
|
||
```
|
||
|
||
Or without making it executable:
|
||
```bash
|
||
bash myscript.sh
|
||
```
|
||
|
||
---
|
||
|
||
## Variables
|
||
|
||
Variables store values you want to reuse:
|
||
|
||
```bash
|
||
name="kenpat"
|
||
port=3000
|
||
greeting="Hello, $name"
|
||
|
||
echo $name # prints: kenpat
|
||
echo $port # prints: 3000
|
||
echo $greeting # prints: Hello, kenpat
|
||
echo "${name}s" # prints: kenpats (braces needed when appending)
|
||
```
|
||
|
||
**Special variables:**
|
||
```bash
|
||
$0 # The script's own filename
|
||
$1 $2 $3 # Command-line arguments (first, second, third)
|
||
$# # Number of arguments passed
|
||
$? # Exit code of the last command (0 = success, non-zero = error)
|
||
$$ # Current process ID (PID)
|
||
$HOME # Your home directory path
|
||
$USER # Your username
|
||
```
|
||
|
||
**Read-only environment variables:**
|
||
```bash
|
||
export MY_VAR="value" # Make available to child processes
|
||
printenv # List all environment variables
|
||
printenv MY_VAR # Print one variable
|
||
```
|
||
|
||
---
|
||
|
||
## Conditionals (if/else)
|
||
|
||
```bash
|
||
if [[ condition ]]; then
|
||
# commands if true
|
||
elif [[ other_condition ]]; then
|
||
# commands if second condition is true
|
||
else
|
||
# commands if nothing was true
|
||
fi
|
||
```
|
||
|
||
**Common conditions:**
|
||
```bash
|
||
[[ -f /path/to/file ]] # True if file exists and is a regular file
|
||
[[ -d /path/to/dir ]] # True if directory exists
|
||
[[ -s /path/to/file ]] # True if file exists and is non-empty
|
||
[[ -z "$var" ]] # True if variable is empty
|
||
[[ -n "$var" ]] # True if variable is NOT empty
|
||
[[ "$a" == "$b" ]] # True if strings are equal
|
||
[[ "$a" != "$b" ]] # True if strings are NOT equal
|
||
[[ $n -eq 5 ]] # True if number equals 5
|
||
[[ $n -gt 5 ]] # True if number is greater than 5
|
||
[[ $n -lt 5 ]] # True if number is less than 5
|
||
```
|
||
|
||
**Real example from the homelab:**
|
||
```bash
|
||
if [[ $# -ne 1 ]]; then
|
||
echo "Usage: $0 '<cloudflare_tunnel_token>'" >&2
|
||
exit 2
|
||
fi
|
||
```
|
||
|
||
This checks that exactly one argument was provided (`$# -ne 1` means "number of args
|
||
is not equal to 1"). If not, it prints usage instructions and exits with code 2 (error).
|
||
The `>&2` sends the message to stderr (error output) instead of stdout (normal output).
|
||
|
||
---
|
||
|
||
## Loops
|
||
|
||
**For loop — iterate over a list:**
|
||
```bash
|
||
for item in one two three; do
|
||
echo "Item: $item"
|
||
done
|
||
|
||
# Iterate over files
|
||
for file in *.yml; do
|
||
echo "Found compose file: $file"
|
||
done
|
||
|
||
# Iterate over a range of numbers
|
||
for i in {1..10}; do
|
||
echo "Number: $i"
|
||
done
|
||
```
|
||
|
||
**While loop — repeat while a condition is true:**
|
||
```bash
|
||
count=0
|
||
while [[ $count -lt 5 ]]; do
|
||
echo "Count: $count"
|
||
count=$(( count + 1 ))
|
||
done
|
||
|
||
# Wait until a container is healthy
|
||
while [[ "$(docker inspect --format '{{.State.Health.Status}}' authentik)" != "healthy" ]]; do
|
||
echo "Waiting for authentik..."
|
||
sleep 5
|
||
done
|
||
echo "Authentik is healthy"
|
||
```
|
||
|
||
---
|
||
|
||
## Functions
|
||
|
||
```bash
|
||
greet() {
|
||
local name="$1" # local = only exists inside this function
|
||
echo "Hello, $name"
|
||
}
|
||
|
||
greet "kenpat" # prints: Hello, kenpat
|
||
greet "world" # prints: Hello, world
|
||
```
|
||
|
||
**Why local variables matter:** Without `local`, variables are global and can
|
||
accidentally overwrite values from other parts of the script.
|
||
|
||
---
|
||
|
||
## Error Handling
|
||
|
||
```bash
|
||
set -euo pipefail
|
||
```
|
||
|
||
Put this near the top of every script you write. It sets three behaviors:
|
||
- `-e` — exit immediately if any command fails (returns non-zero exit code)
|
||
- `-u` — exit if you use an undefined variable
|
||
- `-o pipefail` — if any command in a pipeline fails, the whole pipeline fails
|
||
|
||
Without this, a script can silently continue after an error, potentially causing
|
||
damage downstream (like deleting data after a failed backup).
|
||
|
||
**Checking a command's result:**
|
||
```bash
|
||
if curl -s https://example.com > /dev/null; then
|
||
echo "Site is up"
|
||
else
|
||
echo "Site is down"
|
||
fi
|
||
```
|
||
|
||
**Exit codes:**
|
||
```bash
|
||
exit 0 # Success
|
||
exit 1 # Generic error
|
||
exit 2 # Misuse (bad arguments)
|
||
```
|
||
|
||
---
|
||
|
||
## String Manipulation
|
||
|
||
```bash
|
||
var="TUNNEL_TOKEN=abc123"
|
||
|
||
# Split by delimiter, take field 2
|
||
echo "$var" | cut -d= -f2 # prints: abc123
|
||
|
||
# But what if the value itself contains = signs?
|
||
echo "$var" | cut -d= -f2- # prints: abc123 (f2- = from field 2 to end)
|
||
|
||
# Remove trailing newline
|
||
echo "hello" | tr -d '\n'
|
||
|
||
# Convert to lowercase
|
||
echo "HELLO" | tr '[:upper:]' '[:lower:]'
|
||
|
||
# Replace text
|
||
echo "hello world" | sed 's/world/there/' # prints: hello there
|
||
echo "aabbcc" | sed 's/b/B/g' # prints: aaBBcc (g = all occurrences)
|
||
|
||
# Extract with grep
|
||
echo "addr: 192.168.1.1" | grep -oP '\d+\.\d+\.\d+\.\d+' # prints: 192.168.1.1
|
||
```
|
||
|
||
---
|
||
|
||
## Here Documents (heredoc)
|
||
|
||
A heredoc lets you write multi-line strings inline:
|
||
|
||
```bash
|
||
cat <<'EOF'
|
||
This is line one
|
||
This is line two
|
||
Variables like $HOME are NOT expanded (because of the quotes around EOF)
|
||
EOF
|
||
|
||
cat <<EOF
|
||
This is line one
|
||
HOME is: $HOME (expanded because no quotes)
|
||
EOF
|
||
```
|
||
|
||
Used in this homelab to write multi-line content to files:
|
||
```bash
|
||
cat > /tmp/fix.sql <<'EOF'
|
||
BEGIN;
|
||
UPDATE ServerSetting SET Value='{"enabled":true}' WHERE "Key"=40;
|
||
COMMIT;
|
||
EOF
|
||
```
|
||
|
||
---
|
||
|
||
## Real Scripts in This Homelab
|
||
|
||
### The Token Rotation Script
|
||
|
||
`~/kitestacks-homelab/scripts/rollout-cloudflared-token.sh`:
|
||
```bash
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
if [[ $# -ne 1 ]]; then
|
||
echo "Usage: $0 '<cloudflare_tunnel_token>'" >&2
|
||
exit 2
|
||
fi
|
||
|
||
token="$1"
|
||
monk_dir="${MONK_CLOUDFLARED_DIR:-$HOME/kitestacks-live/docker/cloudflared}"
|
||
kscloud1_host="${KSCLOUD1_HOST:?set KSCLOUD1_HOST, for example user@host}"
|
||
kscloud1_key="${KSCLOUD1_KEY:-$HOME/.ssh/id_ed25519_kscloud1}"
|
||
kscloud1_dir="${KSCLOUD1_CLOUDFLARED_DIR:-/opt/kitestacks/docker/cloudflared}"
|
||
```
|
||
|
||
Walking through each line:
|
||
- `set -euo pipefail` — fail fast and safely
|
||
- `$# -ne 1` — check exactly one argument was given
|
||
- `${MONK_CLOUDFLARED_DIR:-$HOME/...}` — use environment variable if set, otherwise use default
|
||
- `${KSCLOUD1_HOST:?...}` — if `KSCLOUD1_HOST` is not set, exit with that error message
|
||
|
||
This is a real production script. Read it in full at that path.
|
||
|
||
---
|
||
|
||
## Writing Your Own Scripts
|
||
|
||
**Template for any script:**
|
||
```bash
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
# --- Configuration (change these) ---
|
||
MY_VAR="${MY_ENV_VAR:-default_value}"
|
||
TARGET_HOST="${1:?Usage: $0 <hostname>}"
|
||
|
||
# --- Functions ---
|
||
log() {
|
||
echo "[$(date '+%H:%M:%S')] $*"
|
||
}
|
||
|
||
die() {
|
||
echo "ERROR: $*" >&2
|
||
exit 1
|
||
}
|
||
|
||
# --- Main ---
|
||
log "Starting..."
|
||
|
||
if [[ ! -d "$TARGET_HOST" ]]; then
|
||
die "Directory does not exist: $TARGET_HOST"
|
||
fi
|
||
|
||
log "Done."
|
||
```
|
||
|
||
---
|
||
|
||
## Practice Exercises
|
||
|
||
1. Write a script that checks if Docker is running and prints "Docker is up" or "Docker is down"
|
||
2. Write a script that takes a service name as an argument and shows its logs:
|
||
`./show-logs.sh forgejo`
|
||
3. Write a script that loops through all directories in `~/kitestacks-live/docker/`
|
||
and prints the service name and whether it has a `.env` file
|
||
4. Write a script that checks if a URL returns 200 OK and prints "UP" or "DOWN":
|
||
`./check-url.sh https://gitforge.kitestacks.com`
|
||
5. Read and understand every line of `scripts/rollout-cloudflared-token.sh`
|
||
|
||
---
|
||
|
||
**Next:** [Part 3 — Python Basics](03-python-basics.md)
|