docs: add ansible howto collection

Four reference files covering:
- ansible.md — core commands, ansible.cfg, key settings
- ansible-inventory.md — static/dynamic inventory, directory layout
- ansible-variables.md — 22-level precedence, scoping, merge behavior
- ansible-roles.md — structure, defaults vs vars, dependencies
This commit is contained in:
user
2026-02-21 20:33:34 +01:00
parent c6b4a88736
commit 80f82dcde7
6 changed files with 756 additions and 3 deletions

View File

@@ -3,8 +3,8 @@
## Current
- [x] Scaffold project structure
- [ ] Create topic template
- [ ] Write first topic
- [x] Create topic template
- [x] Write first topic (ansible — 4 files)
## Backlog

View File

@@ -3,7 +3,7 @@
## Topics to Write
- [ ] git — common workflows, rebase, stash, bisect
- [ ] ansible — playbook patterns, inventory, vault
- [x] ansible — playbook patterns, inventory, vault, variables, roles
- [ ] podman — build, run, compose, volumes
- [ ] jq — filters, select, map, slurp
- [ ] curl — headers, auth, methods, output

214
topics/ansible-inventory.md Normal file
View File

@@ -0,0 +1,214 @@
# Ansible Inventory
> Define which hosts to manage, how to group them, and where to find them.
## Inventory Formats
### INI Format
```ini
# inventory/hosts.ini
[webservers]
web1.example.com
web2.example.com ansible_port=2222
[dbservers]
db1.example.com ansible_user=postgres
db[1:3].example.com # db1, db2, db3
[loadbalancers]
lb1.example.com
# Group of groups
[production:children]
webservers
dbservers
loadbalancers
# Group variables
[webservers:vars]
http_port=8080
app_env=production
[all:vars]
ansible_python_interpreter=/usr/bin/python3
```
### YAML Format (preferred)
```yaml
# inventory/hosts.yml
all:
vars:
ansible_python_interpreter: /usr/bin/python3
children:
production:
children:
webservers:
hosts:
web1.example.com:
web2.example.com:
ansible_port: 2222
vars:
http_port: 8080
dbservers:
hosts:
db1.example.com:
ansible_user: postgres
loadbalancers:
hosts:
lb1.example.com:
```
## Directory Layout
```
inventory/
├── hosts.yml # Host definitions
├── group_vars/
│ ├── all.yml # Vars for every host
│ ├── all/ # Dir form (merged, alphabetical)
│ │ ├── common.yml
│ │ └── vault.yml # Encrypted secrets
│ ├── webservers.yml # Vars for webservers group
│ └── production.yml # Vars for production parent group
└── host_vars/
├── web1.example.com.yml
└── db1.example.com/ # Dir form
├── main.yml
└── vault.yml
```
### Multiple Environments
```
inventories/
├── staging/
│ ├── hosts.yml
│ ├── group_vars/
│ └── host_vars/
└── production/
├── hosts.yml
├── group_vars/
└── host_vars/
```
```bash
ansible-playbook site.yml -i inventories/staging/
ansible-playbook site.yml -i inventories/production/
```
## Dynamic Inventories
### Script-Based (legacy)
A script that outputs JSON when called with `--list` or `--host <name>`.
```bash
#!/usr/bin/env python3
# inventory/dynamic.py — must be executable (chmod +x)
import json, sys
def get_inventory():
return {
"webservers": {
"hosts": ["web1.example.com", "web2.example.com"],
"vars": {"http_port": 8080}
},
"dbservers": {
"hosts": ["db1.example.com"]
},
"_meta": {
"hostvars": {
"web1.example.com": {"ansible_port": 22},
"db1.example.com": {"ansible_user": "postgres"}
}
}
}
if "--list" in sys.argv:
print(json.dumps(get_inventory(), indent=2))
elif "--host" in sys.argv:
host = sys.argv[sys.argv.index("--host") + 1]
hostvars = get_inventory().get("_meta", {}).get("hostvars", {})
print(json.dumps(hostvars.get(host, {})))
```
The `_meta.hostvars` key avoids per-host `--host` calls (efficiency).
### Plugin-Based (modern, preferred)
```yaml
# inventory/aws_ec2.yml — filename must end with the plugin suffix
plugin: amazon.aws.aws_ec2
regions:
- eu-west-1
keyed_groups:
- key: tags.Environment
prefix: env
- key: instance_type
prefix: type
filters:
tag:Managed: ansible
compose:
ansible_host: private_ip_address
```
Common inventory plugins:
| Plugin | Source | Suffix |
|-------------------------------|------------|-----------------|
| `amazon.aws.aws_ec2` | AWS EC2 | `aws_ec2.yml` |
| `azure.azcollection.azure_rm`| Azure | `azure_rm.yml` |
| `google.cloud.gcp_compute` | GCP | `gcp.yml` |
| `community.general.proxmox` | Proxmox | `proxmox.yml` |
| `community.docker.docker_containers` | Docker | `docker.yml` |
| `ansible.builtin.constructed` | Derived | `constructed.yml`|
```bash
# Verify dynamic inventory output
ansible-inventory -i inventory/aws_ec2.yml --graph
ansible-inventory -i inventory/aws_ec2.yml --list
```
### Constructed Inventory (compose groups from existing data)
```yaml
# inventory/constructed.yml
plugin: ansible.builtin.constructed
strict: false
groups:
is_debian: ansible_os_family == "Debian"
is_large: ansible_memtotal_mb > 8192
keyed_groups:
- key: ansible_distribution | lower
prefix: os
```
## Special Variables
| Variable | Purpose |
|-----------------------|------------------------------------|
| `ansible_host` | IP/hostname to connect to |
| `ansible_port` | SSH port (default: 22) |
| `ansible_user` | SSH user |
| `ansible_ssh_private_key_file` | SSH key path |
| `ansible_become` | Enable privilege escalation |
| `ansible_connection` | Connection type (`ssh`, `local`) |
| `ansible_python_interpreter` | Python path on target |
## Gotchas
- Script inventories must be executable (`chmod +x`) and have a shebang
- Plugin inventory files must end with the correct suffix or be listed in `enable_plugins`
- `group_vars/` directory must sit next to the inventory file, or at playbook level
- YAML inventory: hosts need trailing colon even with no vars (`web1.example.com:`)
- Groups named `all` and `ungrouped` are implicit — don't redefine them
- Directory form `group_vars/all/` merges files alphabetically — name carefully
## See Also
- `ansible` — core commands and ansible.cfg
- `ansible-variables` — variable precedence across inventory layers
- `ansible-roles` — role structure and organization

211
topics/ansible-roles.md Normal file
View File

@@ -0,0 +1,211 @@
# Ansible Roles
> Reusable, self-contained units of automation with a standard directory structure.
## Role Directory Structure
```
roles/
└── nginx/
├── defaults/
│ └── main.yml # Low-precedence defaults (user overrides these)
├── vars/
│ └── main.yml # High-precedence vars (internal to role)
├── tasks/
│ └── main.yml # Entry point for tasks
├── handlers/
│ └── main.yml # Handlers triggered by notify
├── templates/
│ └── nginx.conf.j2 # Jinja2 templates
├── files/
│ └── index.html # Static files for copy module
├── meta/
│ └── main.yml # Dependencies, galaxy metadata
└── README.md
```
All directories are optional — include only what the role needs.
### What Goes Where
| Directory | Purpose | Precedence |
|-------------|--------------------------------|-------------|
| `defaults/` | Default variable values | **Lowest** — meant to be overridden |
| `vars/` | Internal role variables | **High** — not easily overridden |
| `tasks/` | Main task list | Entry point: `tasks/main.yml` |
| `handlers/` | Restart/reload triggers | Run once at end of play |
| `templates/` | Jinja2 files (`.j2`) | Referenced as `template: nginx.conf.j2` |
| `files/` | Static files | Referenced as `copy: src=index.html` |
| `meta/` | Role deps, platform info | Processed before role tasks run |
## Creating a Role
```bash
# Scaffold with ansible-galaxy
ansible-galaxy role init roles/nginx
# Minimal manual structure
mkdir -p roles/nginx/{tasks,defaults,templates,handlers,meta}
```
## Using Roles
### In a Playbook
```yaml
# site.yml
- hosts: webservers
roles:
- nginx # simple
- role: nginx # with params
vars:
nginx_listen_port: 8080
- role: nginx # conditional
when: install_nginx | bool
- role: nginx # tagged
tags: [web, nginx]
```
### With `include_role` / `import_role`
```yaml
tasks:
# Static import — processed at parse time
- import_role:
name: nginx
# Dynamic include — processed at runtime (supports loops/conditionals)
- include_role:
name: "{{ item }}"
loop:
- nginx
- certbot
```
| | `import_role` | `include_role` |
|---|---|---|
| When parsed | Playbook load | Runtime |
| Tags/when | Applied to all tasks inside | Applied to include itself |
| Loops | Not supported | Supported |
| Handlers | Visible globally | Scoped to include |
## Role Dependencies
```yaml
# roles/nginx/meta/main.yml
dependencies:
- role: common
- role: firewall
vars:
firewall_allowed_ports:
- 80
- 443
```
Dependencies run **before** the role's own tasks. Duplicate dependencies are skipped unless `allow_duplicates: true` is set.
## Example Role: nginx
```yaml
# roles/nginx/defaults/main.yml
nginx_listen_port: 80
nginx_worker_processes: auto
nginx_server_name: "_"
nginx_root: /var/www/html
nginx_access_log: /var/log/nginx/access.log
```
```yaml
# roles/nginx/tasks/main.yml
- name: Install nginx
apt:
name: nginx
state: present
update_cache: true
- name: Deploy configuration
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: "0644"
notify: Reload nginx
- name: Enable and start nginx
service:
name: nginx
state: started
enabled: true
```
```yaml
# roles/nginx/handlers/main.yml
- name: Reload nginx
service:
name: nginx
state: reloaded
```
## Splitting Tasks
```yaml
# roles/nginx/tasks/main.yml
- import_tasks: install.yml
- import_tasks: configure.yml
- import_tasks: service.yml
# Conditional platform tasks
- import_tasks: debian.yml
when: ansible_os_family == "Debian"
- import_tasks: redhat.yml
when: ansible_os_family == "RedHat"
```
## Project Layout with Roles
```
ansible-project/
├── ansible.cfg
├── site.yml # Master playbook
├── webservers.yml # Play for web tier
├── dbservers.yml # Play for db tier
├── inventory/
│ ├── hosts.yml
│ ├── group_vars/
│ └── host_vars/
├── roles/
│ ├── common/ # Shared baseline
│ ├── nginx/
│ ├── postgresql/
│ └── requirements.yml # Galaxy dependencies
└── collections/
└── requirements.yml # Collection dependencies
```
```yaml
# roles/requirements.yml
- src: geerlingguy.docker
version: "6.1.0"
- src: geerlingguy.certbot
```
```bash
ansible-galaxy install -r roles/requirements.yml -p roles/
```
## Gotchas
- `defaults/` is for users to override; `vars/` is for role internals — mixing them up breaks precedence
- Handlers run at end of play, not after each task — use `meta: flush_handlers` if needed mid-play
- Role names must be valid Python identifiers (no hyphens) when used as variable namespaces
- `import_role` makes role tasks visible to `--list-tasks`; `include_role` does not
- Dependencies declared in `meta/` run every time the role is referenced unless deduplicated
- Template paths are relative to `templates/` — use `nginx.conf.j2`, not `templates/nginx.conf.j2`
- File paths in `copy` are relative to `files/` — use `index.html`, not `files/index.html`
## See Also
- `ansible` — core commands and ansible.cfg
- `ansible-variables` — how defaults/ vs vars/ fits into precedence
- `ansible-inventory` — inventory structure for role-consuming projects

201
topics/ansible-variables.md Normal file
View File

@@ -0,0 +1,201 @@
# Ansible Variables
> Variable precedence, inheritance, and scoping — the part everyone gets wrong.
## Precedence (lowest to highest)
Ansible merges variables from many sources. **Higher number wins.**
```
1. command line values (for constants, not variables)
2. role defaults (roles/x/defaults/main.yml)
3. inventory file or script group vars
4. inventory group_vars/all
5. playbook group_vars/all
6. inventory group_vars/*
7. playbook group_vars/*
8. inventory file or script host vars
9. inventory host_vars/*
10. playbook host_vars/*
11. host facts / cached set_facts
12. play vars (vars: in play)
13. play vars_prompt
14. play vars_files
15. role vars (roles/x/vars/main.yml)
16. block vars (vars: on block)
17. task vars (vars: on task)
18. include_vars
19. set_facts / registered vars
20. role params (role: {role: x, param: val})
21. include params
22. extra vars (-e / --extra-vars) *** always wins ***
```
### Practical Summary
| Source | Precedence | Use for |
|-------------------------|:----------:|--------------------------------|
| Role defaults | Lowest | Sane defaults, meant to be overridden |
| `group_vars/all` | Low | Organization-wide defaults |
| `group_vars/<group>` | Medium | Environment/group-specific |
| `host_vars/<host>` | Medium+ | Host-specific overrides |
| Play/block/task `vars:` | High | Playbook-scoped values |
| Role vars | High | Role internals, not for override |
| `set_fact` | High | Runtime-computed values |
| Extra vars (`-e`) | Highest | CLI overrides, CI/CD pipelines |
## Group Variable Inheritance
When a host belongs to multiple groups, merge order is:
1. **`all` group** (lowest)
2. **Parent groups** (alphabetical within same level)
3. **Child groups** (override parents)
4. **Host vars** (override all groups)
### Controlling Merge Order
```yaml
# inventory/hosts.yml
all:
children:
env_staging:
vars:
app_env: staging
log_level: debug
env_production:
vars:
app_env: production
log_level: warn
webservers:
hosts:
web1.example.com: # member of webservers AND env_production
vars:
http_port: 8080
```
Use `ansible_group_priority` to force ordering (default: 1):
```yaml
# group_vars/env_production.yml
ansible_group_priority: 10 # higher = wins over other groups
app_env: production
```
## Variable Scoping
| Scope | Defined in | Visible to |
|----------|-----------------------------------|---------------------------|
| Global | `extra_vars`, config, env | Everything |
| Play | `vars:`, `vars_files:`, `include_vars` | Current play |
| Host | Inventory, facts, `set_fact` | That host across plays |
| Role | `defaults/`, `vars/` | Role + dependents |
| Task | `register`, task-level `vars:` | Subsequent tasks (same host) |
## Common Patterns
### Layered Defaults
```yaml
# group_vars/all.yml — base defaults
ntp_server: ntp.pool.org
log_level: info
app_port: 8080
# group_vars/production.yml — override for prod
log_level: warn
# host_vars/web1.example.com.yml — host-specific
app_port: 9090
```
Result for `web1` in `production`: `ntp_server=ntp.pool.org`, `log_level=warn`, `app_port=9090`.
### Role Defaults vs Role Vars
```yaml
# roles/nginx/defaults/main.yml — LOW precedence, user overrides these
nginx_worker_processes: auto
nginx_listen_port: 80
# roles/nginx/vars/main.yml — HIGH precedence, internal to role
nginx_conf_path: /etc/nginx/nginx.conf # don't let users change this
nginx_user: www-data
```
**Rule of thumb:** put everything in `defaults/` unless the role breaks if the value changes.
### Merging Dictionaries and Lists
By default, Ansible **replaces** (does not merge) dicts and lists. To merge:
```ini
# ansible.cfg
[defaults]
hash_behaviour = merge # CAUTION: global, can cause surprises
```
Prefer explicit merging with `combine` filter:
```yaml
- name: Merge defaults with overrides
set_fact:
final_config: "{{ default_config | combine(override_config, recursive=True) }}"
```
### Registered Variables
```yaml
- name: Check service status
command: systemctl is-active nginx
register: nginx_status
ignore_errors: true
- name: Restart if stopped
service:
name: nginx
state: started
when: nginx_status.rc != 0
```
### Vault-Encrypted Variables
```yaml
# group_vars/all/vault.yml (encrypted)
vault_db_password: "s3cret"
# group_vars/all/main.yml (references vault)
db_password: "{{ vault_db_password }}"
```
Prefix vault variables with `vault_` so it's clear where values originate.
## Debugging Variables
```bash
# Show all vars for a host
ansible -m debug -a "var=hostvars[inventory_hostname]" web1.example.com
# Show specific variable
ansible -m debug -a "var=http_port" web1.example.com
# In a playbook
- debug: var=ansible_facts
- debug: msg="{{ http_port }} on {{ inventory_hostname }}"
```
## Gotchas
- `extra_vars` always win — they override even role `vars/`. Don't use `-e` for defaults
- `set_fact` persists for the host across plays in the same run (not across runs unless cached)
- `hash_behaviour = merge` is global and rarely what you want — prefer `combine` filter
- Variable names are flat — `foo.bar` is dict access, not a separate variable
- Jinja2 variables in `when:` don't need `{{ }}` — just `when: my_var == "x"`
- Undefined variables fail by default. Use `{{ my_var | default('fallback') }}`
- `group_vars/` at playbook level AND inventory level both load — watch for conflicts
## See Also
- `ansible` — core commands and ansible.cfg
- `ansible-inventory` — inventory structure, dynamic inventories
- `ansible-roles` — role structure and defaults vs vars

127
topics/ansible.md Normal file
View File

@@ -0,0 +1,127 @@
# Ansible
> Agentless automation — push-based configuration management over SSH.
## Core Concepts
| Concept | Description |
|-------------|--------------------------------------------------|
| Inventory | Hosts and groups to manage |
| Playbook | YAML file defining tasks to run on hosts |
| Role | Reusable unit of tasks, vars, templates, handlers|
| Task | Single action (install pkg, copy file, etc.) |
| Module | Built-in unit of work (`apt`, `copy`, `template`)|
| Handler | Task triggered by `notify`, runs once at end |
| Facts | Auto-gathered host info (`ansible_os_family`) |
| Vault | Encrypted secrets storage |
## Common Commands
```bash
# Run ad-hoc command on all hosts
ansible all -m ping
ansible all -m shell -a "uptime"
# Run playbook
ansible-playbook site.yml
ansible-playbook site.yml -l webservers # limit to group
ansible-playbook site.yml --tags deploy # run tagged tasks only
ansible-playbook site.yml --skip-tags debug # skip tagged tasks
ansible-playbook site.yml -e "version=2.1" # extra vars (highest precedence)
# Dry run / check mode
ansible-playbook site.yml --check --diff
# List hosts that would be affected
ansible-playbook site.yml --list-hosts
ansible-playbook site.yml --list-tasks
ansible-playbook site.yml --list-tags
# Vault
ansible-vault create secrets.yml
ansible-vault edit secrets.yml
ansible-vault encrypt existing.yml
ansible-vault decrypt existing.yml
ansible-playbook site.yml --ask-vault-pass
ansible-playbook site.yml --vault-password-file ~/.vault_pass
# Galaxy (roles/collections)
ansible-galaxy role install geerlingguy.docker
ansible-galaxy role init my_role
ansible-galaxy collection install community.general
# Debug / info
ansible --version
ansible-config dump --only-changed
ansible-inventory --graph
ansible-inventory --host <hostname>
```
## ansible.cfg
Config is loaded from the **first file found** in this order:
1. `$ANSIBLE_CONFIG` (env variable)
2. `./ansible.cfg` (current directory)
3. `~/.ansible.cfg` (home directory)
4. `/etc/ansible/ansible.cfg` (system-wide)
### Practical ansible.cfg
```ini
[defaults]
inventory = ./inventory/
roles_path = ./roles/
collections_path = ./collections/
remote_user = deploy
private_key_file = ~/.ssh/id_ed25519
host_key_checking = False
retry_files_enabled = False
stdout_callback = yaml
callback_enabled = timer, profile_tasks
forks = 20
timeout = 30
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 3600
vault_password_file = ~/.vault_pass
interpreter_python = auto_silent
log_path = ./ansible.log
[privilege_escalation]
become = True
become_method = sudo
become_user = root
become_ask_pass = False
[ssh_connection]
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no
control_path_dir = /tmp/.ansible-cp
```
### Key Settings Explained
| Setting | Effect |
|------------------------|-------------------------------------------------|
| `forks` | Parallel host connections (default: 5) |
| `pipelining` | Reduces SSH operations, major speed gain |
| `gathering = smart` | Cache facts, skip re-gathering |
| `stdout_callback=yaml` | Human-readable output instead of JSON |
| `fact_caching` | Persist facts between runs |
| `retry_files_enabled` | Disable `.retry` file clutter |
## Gotchas
- Config in current dir (`./ansible.cfg`) is ignored if the directory is world-writable
- `become: true` in `ansible.cfg` applies globally — prefer setting it per play
- `host_key_checking = False` is fine for labs, not for production
- `forks` above ~50 can exhaust file descriptors on the control node
- `pipelining` requires `requiretty` disabled in sudoers on targets
## See Also
- `ansible-inventory` — inventory structure and dynamic inventories
- `ansible-variables` — variable precedence and inheritance
- `ansible-roles` — role structure and best practices