chore: link vault wiki to Gitea
This commit is contained in:
0
01-linux/shell-scripting/.keep
Normal file
0
01-linux/shell-scripting/.keep
Normal file
208
01-linux/shell-scripting/ansible-getting-started.md
Normal file
208
01-linux/shell-scripting/ansible-getting-started.md
Normal file
@@ -0,0 +1,208 @@
|
||||
---
|
||||
title: "Ansible Getting Started: Inventory, Playbooks, and Ad-Hoc Commands"
|
||||
domain: linux
|
||||
category: shell-scripting
|
||||
tags: [ansible, automation, infrastructure, linux, idempotent]
|
||||
status: published
|
||||
created: 2026-03-08
|
||||
updated: 2026-03-08
|
||||
---
|
||||
|
||||
# Ansible Getting Started: Inventory, Playbooks, and Ad-Hoc Commands
|
||||
|
||||
Ansible is how I manage infrastructure at scale — or even just across a handful of machines. You write what you want the end state to look like, Ansible figures out how to get there. No agents needed on the remote machines, just SSH.
|
||||
|
||||
## The Short Answer
|
||||
|
||||
```bash
|
||||
# Install Ansible
|
||||
pip install ansible
|
||||
|
||||
# Run a one-off command on all hosts
|
||||
ansible all -i inventory.ini -m ping
|
||||
|
||||
# Run a playbook
|
||||
ansible-playbook -i inventory.ini site.yml
|
||||
```
|
||||
|
||||
## Core Concepts
|
||||
|
||||
**Inventory** — the list of machines Ansible manages. Can be a static file or dynamically generated.
|
||||
|
||||
**Playbook** — a YAML file describing tasks to run on hosts. The main thing you write.
|
||||
|
||||
**Module** — the building blocks of tasks. `apt`, `dnf`, `service`, `copy`, `template`, `user`, etc. Ansible has modules for almost everything.
|
||||
|
||||
**Idempotency** — run the same playbook ten times, the result is the same as running it once. Ansible modules are designed this way. This matters because it means you can re-run playbooks safely without side effects.
|
||||
|
||||
## Inventory File
|
||||
|
||||
```ini
|
||||
# inventory.ini
|
||||
|
||||
[webservers]
|
||||
web1.example.com
|
||||
web2.example.com ansible_user=admin
|
||||
|
||||
[databases]
|
||||
db1.example.com ansible_user=ubuntu ansible_port=2222
|
||||
|
||||
[all:vars]
|
||||
ansible_user=myuser
|
||||
ansible_ssh_private_key_file=~/.ssh/id_ed25519
|
||||
```
|
||||
|
||||
Test connectivity:
|
||||
|
||||
```bash
|
||||
ansible all -i inventory.ini -m ping
|
||||
```
|
||||
|
||||
A successful response looks like:
|
||||
|
||||
```
|
||||
web1.example.com | SUCCESS => {
|
||||
"ping": "pong"
|
||||
}
|
||||
```
|
||||
|
||||
## Ad-Hoc Commands
|
||||
|
||||
For quick one-offs without writing a playbook:
|
||||
|
||||
```bash
|
||||
# Run a shell command
|
||||
ansible all -i inventory.ini -m shell -a "uptime"
|
||||
|
||||
# Install a package
|
||||
ansible webservers -i inventory.ini -m apt -a "name=nginx state=present" --become
|
||||
|
||||
# Restart a service
|
||||
ansible webservers -i inventory.ini -m service -a "name=nginx state=restarted" --become
|
||||
|
||||
# Copy a file
|
||||
ansible all -i inventory.ini -m copy -a "src=./myfile dest=/tmp/myfile"
|
||||
```
|
||||
|
||||
`--become` escalates to sudo.
|
||||
|
||||
## Writing a Playbook
|
||||
|
||||
```yaml
|
||||
---
|
||||
# site.yml
|
||||
- name: Configure web servers
|
||||
hosts: webservers
|
||||
become: true
|
||||
|
||||
vars:
|
||||
app_port: 8080
|
||||
|
||||
tasks:
|
||||
- name: Update apt cache
|
||||
ansible.builtin.apt:
|
||||
update_cache: true
|
||||
cache_valid_time: 3600
|
||||
|
||||
- name: Install nginx
|
||||
ansible.builtin.apt:
|
||||
name: nginx
|
||||
state: present
|
||||
|
||||
- name: Start and enable nginx
|
||||
ansible.builtin.service:
|
||||
name: nginx
|
||||
state: started
|
||||
enabled: true
|
||||
|
||||
- name: Deploy config file
|
||||
ansible.builtin.template:
|
||||
src: templates/nginx.conf.j2
|
||||
dest: /etc/nginx/nginx.conf
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
notify: Reload nginx
|
||||
|
||||
handlers:
|
||||
- name: Reload nginx
|
||||
ansible.builtin.service:
|
||||
name: nginx
|
||||
state: reloaded
|
||||
```
|
||||
|
||||
Run it:
|
||||
|
||||
```bash
|
||||
ansible-playbook -i inventory.ini site.yml
|
||||
|
||||
# Dry run — shows what would change without doing it
|
||||
ansible-playbook -i inventory.ini site.yml --check
|
||||
|
||||
# Verbose output
|
||||
ansible-playbook -i inventory.ini site.yml -v
|
||||
```
|
||||
|
||||
## Handlers
|
||||
|
||||
Handlers run at the end of a play, only if notified. The canonical use is "reload service after config change":
|
||||
|
||||
```yaml
|
||||
tasks:
|
||||
- name: Deploy config
|
||||
ansible.builtin.template:
|
||||
src: templates/app.conf.j2
|
||||
dest: /etc/app/app.conf
|
||||
notify: Restart app
|
||||
|
||||
handlers:
|
||||
- name: Restart app
|
||||
ansible.builtin.service:
|
||||
name: myapp
|
||||
state: restarted
|
||||
```
|
||||
|
||||
If the config file didn't change (idempotent — it was already in the right state), the notify never fires and the service isn't restarted.
|
||||
|
||||
## Roles
|
||||
|
||||
Once playbooks get complex, organize them into roles:
|
||||
|
||||
```
|
||||
roles/
|
||||
webserver/
|
||||
tasks/
|
||||
main.yml
|
||||
handlers/
|
||||
main.yml
|
||||
templates/
|
||||
nginx.conf.j2
|
||||
defaults/
|
||||
main.yml
|
||||
```
|
||||
|
||||
Use a role in a playbook:
|
||||
|
||||
```yaml
|
||||
- name: Set up web servers
|
||||
hosts: webservers
|
||||
become: true
|
||||
roles:
|
||||
- webserver
|
||||
```
|
||||
|
||||
Roles keep things organized and reusable across projects.
|
||||
|
||||
## Gotchas & Notes
|
||||
|
||||
- **YAML indentation matters.** Two spaces is standard. Tab characters will break your playbooks.
|
||||
- **`--check` is your friend.** Always dry-run against production before applying changes.
|
||||
- **SSH key access is required.** Ansible connects over SSH — password auth works but key auth is what you want for automation.
|
||||
- **`gather_facts: false`** speeds up playbooks when you don't need host facts (OS, IP, etc.). Add it at the play level for simple playbooks.
|
||||
- **Ansible is not idempotent by magic.** Shell and command modules run every time regardless of state. Use the appropriate module (`apt`, `service`, `file`, etc.) instead of `shell` whenever possible.
|
||||
- **The `ansible-lint` tool** catches common mistakes before they run. Worth adding to your workflow.
|
||||
|
||||
## See Also
|
||||
|
||||
- [[managing-linux-services-systemd-ansible]]
|
||||
- [[linux-server-hardening-checklist]]
|
||||
215
01-linux/shell-scripting/bash-scripting-patterns.md
Normal file
215
01-linux/shell-scripting/bash-scripting-patterns.md
Normal file
@@ -0,0 +1,215 @@
|
||||
---
|
||||
title: "Bash Scripting Patterns for Sysadmins"
|
||||
domain: linux
|
||||
category: shell-scripting
|
||||
tags: [bash, scripting, automation, linux, shell]
|
||||
status: published
|
||||
created: 2026-03-08
|
||||
updated: 2026-03-08
|
||||
---
|
||||
|
||||
# Bash Scripting Patterns for Sysadmins
|
||||
|
||||
These are the patterns I reach for when writing bash scripts for server automation and maintenance tasks. Not a tutorial from scratch — this assumes you know basic bash syntax and want to write scripts that don't embarrass you later.
|
||||
|
||||
## The Short Answer
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
```
|
||||
|
||||
Start every script with these two lines. `set -e` exits on error. `set -u` treats unset variables as errors. `set -o pipefail` catches errors in pipes. Together they prevent a lot of silent failures.
|
||||
|
||||
## Script Header
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# ── Config ─────────────────────────────────────────────────────────────────────
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
LOG_FILE="/var/log/myscript.log"
|
||||
# ───────────────────────────────────────────────────────────────────────────────
|
||||
```
|
||||
|
||||
## Logging
|
||||
|
||||
```bash
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
log "Script started"
|
||||
log "Processing $1"
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```bash
|
||||
# Exit with a message
|
||||
die() {
|
||||
echo "ERROR: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check a condition
|
||||
[ -f "$CONFIG_FILE" ] || die "Config file not found: $CONFIG_FILE"
|
||||
|
||||
# Trap to run cleanup on exit
|
||||
cleanup() {
|
||||
log "Cleaning up temp files"
|
||||
rm -f "$TMPFILE"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
```
|
||||
|
||||
## Checking Dependencies
|
||||
|
||||
```bash
|
||||
check_deps() {
|
||||
local deps=("curl" "jq" "rsync")
|
||||
for dep in "${deps[@]}"; do
|
||||
command -v "$dep" &>/dev/null || die "Required dependency not found: $dep"
|
||||
done
|
||||
}
|
||||
|
||||
check_deps
|
||||
```
|
||||
|
||||
## Argument Parsing
|
||||
|
||||
```bash
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $SCRIPT_NAME [OPTIONS] <target>
|
||||
|
||||
Options:
|
||||
-v, --verbose Enable verbose output
|
||||
-n, --dry-run Show what would be done without doing it
|
||||
-h, --help Show this help
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
VERBOSE=false
|
||||
DRY_RUN=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-v|--verbose) VERBOSE=true; shift ;;
|
||||
-n|--dry-run) DRY_RUN=true; shift ;;
|
||||
-h|--help) usage ;;
|
||||
--) shift; break ;;
|
||||
-*) die "Unknown option: $1" ;;
|
||||
*) TARGET="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -z "${TARGET:-}" ]] && die "Target is required"
|
||||
```
|
||||
|
||||
## Running Commands
|
||||
|
||||
```bash
|
||||
# Dry-run aware command execution
|
||||
run() {
|
||||
if $DRY_RUN; then
|
||||
echo "DRY RUN: $*"
|
||||
else
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
run rsync -av /source/ /dest/
|
||||
run systemctl restart myservice
|
||||
```
|
||||
|
||||
## Working with Files and Directories
|
||||
|
||||
```bash
|
||||
# Check existence before use
|
||||
[[ -d "$DIR" ]] || mkdir -p "$DIR"
|
||||
[[ -f "$FILE" ]] || die "Expected file not found: $FILE"
|
||||
|
||||
# Safe temp files
|
||||
TMPFILE="$(mktemp)"
|
||||
trap 'rm -f "$TMPFILE"' EXIT
|
||||
|
||||
# Loop over files
|
||||
find /path/to/files -name "*.log" -mtime +30 | while read -r file; do
|
||||
log "Processing: $file"
|
||||
run gzip "$file"
|
||||
done
|
||||
```
|
||||
|
||||
## String Operations
|
||||
|
||||
```bash
|
||||
# Extract filename without extension
|
||||
filename="${filepath##*/}" # basename
|
||||
stem="${filename%.*}" # strip extension
|
||||
|
||||
# Check if string contains substring
|
||||
if [[ "$output" == *"error"* ]]; then
|
||||
die "Error detected in output"
|
||||
fi
|
||||
|
||||
# Convert to lowercase
|
||||
lower="${str,,}"
|
||||
|
||||
# Trim whitespace
|
||||
trimmed="${str#"${str%%[![:space:]]*}"}"
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
**Backup with timestamp:**
|
||||
|
||||
```bash
|
||||
backup() {
|
||||
local source="$1"
|
||||
local dest="${2:-/backup}"
|
||||
local timestamp
|
||||
timestamp="$(date '+%Y%m%d_%H%M%S')"
|
||||
local backup_path="${dest}/$(basename "$source")_${timestamp}.tar.gz"
|
||||
|
||||
log "Backing up $source to $backup_path"
|
||||
run tar -czf "$backup_path" -C "$(dirname "$source")" "$(basename "$source")"
|
||||
}
|
||||
```
|
||||
|
||||
**Retry on failure:**
|
||||
|
||||
```bash
|
||||
retry() {
|
||||
local max_attempts="${1:-3}"
|
||||
local delay="${2:-5}"
|
||||
shift 2
|
||||
local attempt=1
|
||||
|
||||
until "$@"; do
|
||||
if ((attempt >= max_attempts)); then
|
||||
die "Command failed after $max_attempts attempts: $*"
|
||||
fi
|
||||
log "Attempt $attempt failed, retrying in ${delay}s..."
|
||||
sleep "$delay"
|
||||
((attempt++))
|
||||
done
|
||||
}
|
||||
|
||||
retry 3 10 curl -f https://example.com/health
|
||||
```
|
||||
|
||||
## Gotchas & Notes
|
||||
|
||||
- **Always quote variables.** `"$var"` not `$var`. Unquoted variables break on spaces and glob characters.
|
||||
- **Use `[[` not `[` for conditionals.** `[[` is a bash built-in with fewer edge cases.
|
||||
- **`set -e` exits on the first error — including in pipes.** Add `set -o pipefail` or you'll miss failures in `cmd1 | cmd2`.
|
||||
- **`$?` after `if` is almost always wrong.** Use `if command; then` not `command; if [[ $? -eq 0 ]]; then`.
|
||||
- **Bash isn't great for complex data.** If your script needs real data structures or error handling beyond strings, consider Python.
|
||||
|
||||
## See Also
|
||||
|
||||
- [[ansible-getting-started]]
|
||||
- [[managing-linux-services-systemd-ansible]]
|
||||
Reference in New Issue
Block a user