From 80f82dcde774bf5337bfe242ccea5d49d1f28858 Mon Sep 17 00:00:00 2001 From: user Date: Sat, 21 Feb 2026 20:33:34 +0100 Subject: [PATCH] docs: add ansible howto collection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- TASKS.md | 4 +- TODO.md | 2 +- topics/ansible-inventory.md | 214 ++++++++++++++++++++++++++++++++++++ topics/ansible-roles.md | 211 +++++++++++++++++++++++++++++++++++ topics/ansible-variables.md | 201 +++++++++++++++++++++++++++++++++ topics/ansible.md | 127 +++++++++++++++++++++ 6 files changed, 756 insertions(+), 3 deletions(-) create mode 100644 topics/ansible-inventory.md create mode 100644 topics/ansible-roles.md create mode 100644 topics/ansible-variables.md create mode 100644 topics/ansible.md diff --git a/TASKS.md b/TASKS.md index a76d126..1ac73bd 100644 --- a/TASKS.md +++ b/TASKS.md @@ -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 diff --git a/TODO.md b/TODO.md index 45fc474..09fe978 100644 --- a/TODO.md +++ b/TODO.md @@ -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 diff --git a/topics/ansible-inventory.md b/topics/ansible-inventory.md new file mode 100644 index 0000000..e30c770 --- /dev/null +++ b/topics/ansible-inventory.md @@ -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 `. + +```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 diff --git a/topics/ansible-roles.md b/topics/ansible-roles.md new file mode 100644 index 0000000..80da1a0 --- /dev/null +++ b/topics/ansible-roles.md @@ -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 diff --git a/topics/ansible-variables.md b/topics/ansible-variables.md new file mode 100644 index 0000000..57e926d --- /dev/null +++ b/topics/ansible-variables.md @@ -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/` | Medium | Environment/group-specific | +| `host_vars/` | 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 diff --git a/topics/ansible.md b/topics/ansible.md new file mode 100644 index 0000000..371dfb5 --- /dev/null +++ b/topics/ansible.md @@ -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 +``` + +## 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